crc-framework 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {crc_framework-0.2.0 → crc_framework-0.2.1}/Cargo.lock +2 -2
  2. {crc_framework-0.2.0 → crc_framework-0.2.1}/Cargo.toml +1 -1
  3. {crc_framework-0.2.0 → crc_framework-0.2.1}/PKG-INFO +5 -3
  4. {crc_framework-0.2.0 → crc_framework-0.2.1}/README.md +4 -2
  5. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/Cargo.toml +1 -1
  6. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/src/distribution.rs +53 -0
  7. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/src/lib.rs +2 -2
  8. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/__init__.py +9 -7
  9. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/_core.pyi +1 -0
  10. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/distributions/__init__.py +5 -3
  11. crc_framework-0.2.1/python/crc_framework/distributions/point_mass.py +55 -0
  12. {crc_framework-0.2.0 → crc_framework-0.2.1}/src/lib.rs +28 -11
  13. {crc_framework-0.2.0 → crc_framework-0.2.1}/tests/test_sdk.py +19 -11
  14. {crc_framework-0.2.0 → crc_framework-0.2.1}/.github/workflows/release.yaml +0 -0
  15. {crc_framework-0.2.0 → crc_framework-0.2.1}/.gitignore +0 -0
  16. {crc_framework-0.2.0 → crc_framework-0.2.1}/LICENSE +0 -0
  17. {crc_framework-0.2.0 → crc_framework-0.2.1}/THIRD_PARTY_NOTICES.md +0 -0
  18. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/src/constants.rs +0 -0
  19. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/src/error.rs +0 -0
  20. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/src/metrics.rs +0 -0
  21. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/src/reference/climate/ag_params.rs +0 -0
  22. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/src/reference/climate/energy_params.rs +0 -0
  23. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/src/reference/climate/flood_params.rs +0 -0
  24. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/src/reference/climate/mod.rs +0 -0
  25. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/src/reference/climate/precipitation_thresholds.rs +0 -0
  26. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/src/reference/climate/spei_params.rs +0 -0
  27. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/src/reference/climate/wind_params.rs +0 -0
  28. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/src/reference/curve_fitting.rs +0 -0
  29. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/src/reference/mod.rs +0 -0
  30. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/src/reference/spatial/continent_r4.rs +0 -0
  31. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/src/reference/spatial/geography_r5.rs +0 -0
  32. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/src/reference/spatial/ipcc.rs +0 -0
  33. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/src/reference/spatial/mod.rs +0 -0
  34. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/src/risk.rs +0 -0
  35. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/src/spatial.rs +0 -0
  36. {crc_framework-0.2.0 → crc_framework-0.2.1}/crates/core/src/transform.rs +0 -0
  37. {crc_framework-0.2.0 → crc_framework-0.2.1}/pyproject.toml +0 -0
  38. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/constants.py +0 -0
  39. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/distributions/base.py +0 -0
  40. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/distributions/empirical.py +0 -0
  41. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/distributions/fitted.py +0 -0
  42. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/distributions/fitting.py +0 -0
  43. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/distributions/hurdle.py +0 -0
  44. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/distributions/tabulated.py +0 -0
  45. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/metrics/__init__.py +0 -0
  46. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/metrics/distribution.py +0 -0
  47. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/metrics/microscores.py +0 -0
  48. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/metrics/risk.py +0 -0
  49. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/metrics/spanning.py +0 -0
  50. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/models.py +0 -0
  51. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/py.typed +0 -0
  52. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/spatial.py +0 -0
  53. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/transforms/__init__.py +0 -0
  54. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/transforms/base.py +0 -0
  55. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/transforms/builtin.py +0 -0
  56. {crc_framework-0.2.0 → crc_framework-0.2.1}/python/crc_framework/transforms/climate.py +0 -0
  57. {crc_framework-0.2.0 → crc_framework-0.2.1}/tests/test_impacts.py +0 -0
  58. {crc_framework-0.2.0 → crc_framework-0.2.1}/tests/typing_smoke.py +0 -0
@@ -49,7 +49,7 @@ dependencies = [
49
49
 
50
50
  [[package]]
51
51
  name = "crc-framework-core"
52
- version = "0.2.0"
52
+ version = "0.2.1"
53
53
  dependencies = [
54
54
  "h3o",
55
55
  "phf",
@@ -58,7 +58,7 @@ dependencies = [
58
58
 
59
59
  [[package]]
60
60
  name = "crc-framework-rs"
61
- version = "0.2.0"
61
+ version = "0.2.1"
62
62
  dependencies = [
63
63
  "crc-framework-core",
64
64
  "h3o",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "crc-framework-rs"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  edition = "2024"
5
5
  rust-version = "1.85"
6
6
  description = "Rust-powered Python bindings for the Climate Risk Commons Framework"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crc-framework
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
6
6
  Classifier: Operating System :: OS Independent
@@ -92,6 +92,7 @@ Choose a distribution according to the form of the available data:
92
92
  - `FittedDistribution` for a known parametric family and its parameters.
93
93
  - `HurdleDistribution` for a point mass followed by a truncated parametric
94
94
  tail.
95
+ - `PointMassDistribution` for a value that is constant at every probability.
95
96
 
96
97
  `TabulatedDistribution` does not extrapolate by default. Querying outside its
97
98
  declared probability range raises an error unless `extrapolate=True` is set.
@@ -307,8 +308,9 @@ continent and intersecting ISO3 country codes.
307
308
  The package exports the following primary interfaces:
308
309
 
309
310
  - Distributions: `EmpiricalDistribution`, `TabulatedDistribution`,
310
- `FittedDistribution`, `HurdleDistribution`, `fit_distribution`,
311
- `fit_quantiles`, `fit_hurdle_quantiles`, `fit_all`, and `quality_metrics`.
311
+ `FittedDistribution`, `HurdleDistribution`, `PointMassDistribution`,
312
+ `fit_distribution`, `fit_quantiles`, `fit_hurdle_quantiles`, `fit_all`, and
313
+ `quality_metrics`.
312
314
  - Transforms: `impacts`, `ImpactRegistry`, `ImpactFunction`, `ClimateImpact`,
313
315
  `LinearImpact`, `SigmoidImpact`, `PiecewiseLinearImpact`, `CallableImpact`,
314
316
  and `CallableTransform`.
@@ -69,6 +69,7 @@ Choose a distribution according to the form of the available data:
69
69
  - `FittedDistribution` for a known parametric family and its parameters.
70
70
  - `HurdleDistribution` for a point mass followed by a truncated parametric
71
71
  tail.
72
+ - `PointMassDistribution` for a value that is constant at every probability.
72
73
 
73
74
  `TabulatedDistribution` does not extrapolate by default. Querying outside its
74
75
  declared probability range raises an error unless `extrapolate=True` is set.
@@ -284,8 +285,9 @@ continent and intersecting ISO3 country codes.
284
285
  The package exports the following primary interfaces:
285
286
 
286
287
  - Distributions: `EmpiricalDistribution`, `TabulatedDistribution`,
287
- `FittedDistribution`, `HurdleDistribution`, `fit_distribution`,
288
- `fit_quantiles`, `fit_hurdle_quantiles`, `fit_all`, and `quality_metrics`.
288
+ `FittedDistribution`, `HurdleDistribution`, `PointMassDistribution`,
289
+ `fit_distribution`, `fit_quantiles`, `fit_hurdle_quantiles`, `fit_all`, and
290
+ `quality_metrics`.
289
291
  - Transforms: `impacts`, `ImpactRegistry`, `ImpactFunction`, `ClimateImpact`,
290
292
  `LinearImpact`, `SigmoidImpact`, `PiecewiseLinearImpact`, `CallableImpact`,
291
293
  and `CallableTransform`.
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "crc-framework-core"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  edition = "2024"
5
5
  rust-version = "1.85"
6
6
  description = "Core distribution and climate-risk algorithms for CRC Framework"
@@ -256,6 +256,47 @@ impl Distribution for EmpiricalDistribution {
256
256
  }
257
257
  }
258
258
 
259
+ /// A degenerate distribution whose entire probability mass is at one value.
260
+ #[derive(Debug, Clone, Copy)]
261
+ pub struct PointMassDistribution {
262
+ location: f64,
263
+ }
264
+
265
+ impl PointMassDistribution {
266
+ pub fn new(location: f64) -> Result<Self> {
267
+ if !location.is_finite() {
268
+ return Err(CrcError::InvalidInput(
269
+ "point-mass location must be finite".into(),
270
+ ));
271
+ }
272
+ Ok(Self { location })
273
+ }
274
+
275
+ pub fn location(&self) -> f64 {
276
+ self.location
277
+ }
278
+ }
279
+
280
+ impl Distribution for PointMassDistribution {
281
+ fn pdf(&self, _x: f64) -> f64 {
282
+ // A Dirac mass has no finite density with respect to Lebesgue measure.
283
+ 0.0
284
+ }
285
+
286
+ fn cdf(&self, x: f64) -> f64 {
287
+ if x < self.location { 0.0 } else { 1.0 }
288
+ }
289
+
290
+ fn ppf(&self, probability: f64) -> Result<f64> {
291
+ validate_probability(probability)?;
292
+ Ok(self.location)
293
+ }
294
+
295
+ fn sample(&self, size: usize, _seed: Option<u64>) -> Result<Vec<f64>> {
296
+ Ok(vec![self.location; size])
297
+ }
298
+ }
299
+
259
300
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
260
301
  pub enum DistributionFamily {
261
302
  GenExtreme,
@@ -1129,6 +1170,18 @@ fn erf(x: f64) -> f64 {
1129
1170
  mod tests {
1130
1171
  use super::*;
1131
1172
 
1173
+ #[test]
1174
+ fn point_mass_is_exact_at_every_probability() {
1175
+ let distribution = PointMassDistribution::new(2.5).unwrap();
1176
+ assert_eq!(
1177
+ distribution.quantiles(&[0.0, 0.5, 1.0]).unwrap(),
1178
+ vec![2.5; 3]
1179
+ );
1180
+ assert_eq!(distribution.cdf(2.49), 0.0);
1181
+ assert_eq!(distribution.cdf(2.5), 1.0);
1182
+ assert_eq!(distribution.sample(3, Some(42)).unwrap(), vec![2.5; 3]);
1183
+ }
1184
+
1132
1185
  #[test]
1133
1186
  fn return_periods_convert_to_upper_tail_probabilities() {
1134
1187
  let distribution = TabulatedDistribution::from_return_periods(
@@ -10,8 +10,8 @@ pub mod transform;
10
10
  pub use distribution::{
11
11
  DiagnosticMetrics, Distribution, DistributionFamily, EmpiricalDistribution, FitResult,
12
12
  FittedDistribution, HurdleDistribution, HurdleQuantileFitDiagnostics, HurdleQuantileFitResult,
13
- Interpolation, QuantileFitDiagnostics, QuantileFitResult, TabulatedDistribution, Tail,
14
- fit_hurdle_quantiles, fit_quantiles,
13
+ Interpolation, PointMassDistribution, QuantileFitDiagnostics, QuantileFitResult,
14
+ TabulatedDistribution, Tail, fit_hurdle_quantiles, fit_quantiles,
15
15
  };
16
16
  pub use error::{CrcError, Result};
17
17
  pub use metrics::{
@@ -11,6 +11,7 @@ from .distributions import (
11
11
  HurdleDistribution,
12
12
  HurdleQuantileFitDiagnostics,
13
13
  HurdleQuantileFitResult,
14
+ PointMassDistribution,
14
15
  QuantileFitDiagnostics,
15
16
  QuantileFitResult,
16
17
  TabulatedDistribution,
@@ -44,34 +45,35 @@ from .transforms import (
44
45
  )
45
46
 
46
47
  __all__ = [
48
+ "HORIZONS",
49
+ "PATHWAYS",
50
+ "RISK_FACTORS",
47
51
  "BinaryOutcome",
48
52
  "CallableImpact",
49
53
  "CallableTransform",
50
54
  "ClimateImpact",
51
55
  "Distribution",
52
56
  "EmpiricalDistribution",
53
- "FitDiagnostics",
54
57
  "FitConstraints",
58
+ "FitDiagnostics",
55
59
  "FitResult",
56
60
  "FittedDistribution",
61
+ "Geography",
57
62
  "HurdleDistribution",
58
63
  "HurdleQuantileFitDiagnostics",
59
64
  "HurdleQuantileFitResult",
60
- "Geography",
61
- "HORIZONS",
62
65
  "ImpactFunction",
63
66
  "ImpactRegistry",
64
67
  "LinearImpact",
65
68
  "Microscore",
66
69
  "MicroscoreSuite",
67
- "PATHWAYS",
68
70
  "Pathway",
69
71
  "PiecewiseLinearImpact",
70
- "RISK_FACTORS",
71
- "RiskFactor",
72
- "RiskResult",
72
+ "PointMassDistribution",
73
73
  "QuantileFitDiagnostics",
74
74
  "QuantileFitResult",
75
+ "RiskFactor",
76
+ "RiskResult",
75
77
  "ScenarioMetadata",
76
78
  "SigmoidImpact",
77
79
  "TabulatedDistribution",
@@ -100,6 +100,7 @@ class NativeRiskResult:
100
100
  branch_count: int
101
101
 
102
102
  def empirical_distribution(samples: Sequence[float]) -> NativeDistribution: ...
103
+ def point_mass_distribution(location: float) -> NativeDistribution: ...
103
104
  def tabulated_distribution(
104
105
  probabilities: Sequence[float],
105
106
  values: Sequence[float],
@@ -16,24 +16,26 @@ from .fitting import (
16
16
  quality_metrics,
17
17
  )
18
18
  from .hurdle import HurdleDistribution
19
+ from .point_mass import PointMassDistribution
19
20
  from .tabulated import Interpolation, TabulatedDistribution, Tail
20
21
 
21
22
  __all__ = [
22
23
  "Distribution",
23
24
  "DistributionFamily",
24
25
  "EmpiricalDistribution",
25
- "FitDiagnostics",
26
26
  "FitConstraints",
27
+ "FitDiagnostics",
27
28
  "FitResult",
28
29
  "FittedDistribution",
29
30
  "HurdleDistribution",
30
31
  "HurdleQuantileFitDiagnostics",
31
32
  "HurdleQuantileFitResult",
32
33
  "Interpolation",
33
- "TabulatedDistribution",
34
- "Tail",
34
+ "PointMassDistribution",
35
35
  "QuantileFitDiagnostics",
36
36
  "QuantileFitResult",
37
+ "TabulatedDistribution",
38
+ "Tail",
37
39
  "fit_all",
38
40
  "fit_distribution",
39
41
  "fit_hurdle_quantiles",
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+ import numpy.typing as npt
5
+
6
+ from crc_framework import _core
7
+
8
+ from .base import ArrayLike
9
+ from .empirical import _evaluate
10
+
11
+
12
+ class PointMassDistribution:
13
+ """A distribution concentrated exactly at one finite value."""
14
+
15
+ def __init__(
16
+ self,
17
+ location: float,
18
+ *,
19
+ _native: _core.NativeDistribution | None = None,
20
+ ) -> None:
21
+ self.location = float(location)
22
+ self._native = _native or _core.point_mass_distribution(self.location)
23
+
24
+ def point_mass(self, x: ArrayLike) -> float | npt.NDArray[np.float64]:
25
+ values = np.asarray(x, dtype=np.float64)
26
+ masses = np.where(values == self.location, 1.0, 0.0)
27
+ if values.ndim == 0:
28
+ return float(masses)
29
+ return np.asarray(masses, dtype=np.float64)
30
+
31
+ def pdf(self, x: ArrayLike) -> float | npt.NDArray[np.float64]:
32
+ return _evaluate(self._native, "pdf", x)
33
+
34
+ def cdf(self, x: ArrayLike) -> float | npt.NDArray[np.float64]:
35
+ return _evaluate(self._native, "cdf", x)
36
+
37
+ def ppf(self, probability: ArrayLike) -> float | npt.NDArray[np.float64]:
38
+ return _evaluate(self._native, "ppf", probability)
39
+
40
+ def quantiles(self, probabilities: ArrayLike) -> npt.NDArray[np.float64]:
41
+ values = np.asarray(probabilities, dtype=np.float64)
42
+ return np.asarray(
43
+ self._native.quantiles(values.reshape(-1).tolist()), dtype=np.float64
44
+ ).reshape(values.shape)
45
+
46
+ def sample(
47
+ self,
48
+ size: int,
49
+ *,
50
+ rng: np.random.Generator | None = None,
51
+ seed: int | None = None,
52
+ ) -> npt.NDArray[np.float64]:
53
+ if rng is not None and seed is not None:
54
+ raise ValueError("provide either rng or seed, not both")
55
+ return np.full(size, self.location, dtype=np.float64)
@@ -2,8 +2,8 @@ use std::{collections::HashMap, sync::Arc};
2
2
 
3
3
  use crc_framework_core::{
4
4
  BinaryOutcome, Distribution, DistributionFamily, EmpiricalDistribution, FittedDistribution,
5
- HurdleDistribution, ImpactContext, ImpactRegistry, Interpolation, ScenarioMetadata,
6
- TabulatedDistribution, Tail, Transform, compute_risk, compute_spanning_set,
5
+ HurdleDistribution, ImpactContext, ImpactRegistry, Interpolation, PointMassDistribution,
6
+ ScenarioMetadata, TabulatedDistribution, Tail, Transform, compute_risk, compute_spanning_set,
7
7
  fit_hurdle_quantiles as core_fit_hurdle_quantiles, fit_quantiles as core_fit_quantiles,
8
8
  generate_microscores,
9
9
  };
@@ -246,6 +246,18 @@ fn empirical_distribution(samples: Vec<f64>) -> PyResult<PyDistribution> {
246
246
  })
247
247
  }
248
248
 
249
+ #[pyfunction]
250
+ fn point_mass_distribution(location: f64) -> PyResult<PyDistribution> {
251
+ let distribution = PointMassDistribution::new(location).map_err(py_error)?;
252
+ Ok(PyDistribution {
253
+ inner: Arc::new(distribution),
254
+ family: None,
255
+ shape: None,
256
+ location: Some(location),
257
+ scale: None,
258
+ })
259
+ }
260
+
249
261
  fn interpolation(value: &str) -> PyResult<Interpolation> {
250
262
  match value {
251
263
  "linear_probability" => Ok(Interpolation::LinearProbability),
@@ -365,6 +377,7 @@ fn fit_candidates(samples: Vec<f64>) -> PyResult<Vec<PyFitResult>> {
365
377
  #[pyfunction]
366
378
  #[pyo3(signature = (probabilities, values, family, weights=None))]
367
379
  fn fit_quantiles(
380
+ py: Python<'_>,
368
381
  probabilities: Vec<f64>,
369
382
  values: Vec<f64>,
370
383
  family: &str,
@@ -372,7 +385,7 @@ fn fit_quantiles(
372
385
  ) -> PyResult<PyQuantileFitResult> {
373
386
  let family = DistributionFamily::from_name(family)
374
387
  .ok_or_else(|| PyValueError::new_err(format!("unknown distribution family {family}")))?;
375
- core_fit_quantiles(&probabilities, &values, weights.as_deref(), family)
388
+ py.detach(move || core_fit_quantiles(&probabilities, &values, weights.as_deref(), family))
376
389
  .map(quantile_fit_result_py)
377
390
  .map_err(py_error)
378
391
  }
@@ -387,6 +400,7 @@ fn fit_quantiles(
387
400
  weights=None
388
401
  ))]
389
402
  fn fit_hurdle_quantiles(
403
+ py: Python<'_>,
390
404
  probabilities: Vec<f64>,
391
405
  values: Vec<f64>,
392
406
  family: &str,
@@ -396,14 +410,16 @@ fn fit_hurdle_quantiles(
396
410
  ) -> PyResult<PyHurdleQuantileFitResult> {
397
411
  let family = DistributionFamily::from_name(family)
398
412
  .ok_or_else(|| PyValueError::new_err(format!("unknown distribution family {family}")))?;
399
- core_fit_hurdle_quantiles(
400
- &probabilities,
401
- &values,
402
- weights.as_deref(),
403
- family,
404
- atom_location,
405
- atom_probability,
406
- )
413
+ py.detach(move || {
414
+ core_fit_hurdle_quantiles(
415
+ &probabilities,
416
+ &values,
417
+ weights.as_deref(),
418
+ family,
419
+ atom_location,
420
+ atom_probability,
421
+ )
422
+ })
407
423
  .map(hurdle_quantile_fit_result_py)
408
424
  .map_err(py_error)
409
425
  }
@@ -869,6 +885,7 @@ fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
869
885
  m.add_class::<PyRiskLevel>()?;
870
886
  m.add_class::<PyRiskResult>()?;
871
887
  m.add_function(wrap_pyfunction!(empirical_distribution, m)?)?;
888
+ m.add_function(wrap_pyfunction!(point_mass_distribution, m)?)?;
872
889
  m.add_function(wrap_pyfunction!(tabulated_distribution, m)?)?;
873
890
  m.add_function(wrap_pyfunction!(return_period_distribution, m)?)?;
874
891
  m.add_function(wrap_pyfunction!(fitted_distribution, m)?)?;
@@ -1,13 +1,13 @@
1
1
  import unittest
2
2
 
3
3
  import numpy as np
4
-
5
4
  from crc_framework import (
6
5
  BinaryOutcome,
7
6
  EmpiricalDistribution,
8
7
  FitConstraints,
9
8
  FittedDistribution,
10
9
  HurdleDistribution,
10
+ PointMassDistribution,
11
11
  RiskFactor,
12
12
  ScenarioMetadata,
13
13
  TabulatedDistribution,
@@ -27,6 +27,18 @@ from crc_framework import (
27
27
 
28
28
 
29
29
  class DistributionTests(unittest.TestCase):
30
+ def test_point_mass_is_exact_at_every_probability(self):
31
+ distribution = PointMassDistribution(0.0)
32
+
33
+ np.testing.assert_array_equal(
34
+ distribution.quantiles([0.0, 0.5, 0.999, 1.0]),
35
+ np.zeros(4),
36
+ )
37
+ self.assertEqual(distribution.cdf(-0.1), 0.0)
38
+ self.assertEqual(distribution.cdf(0.0), 1.0)
39
+ self.assertEqual(distribution.point_mass(0.0), 1.0)
40
+ np.testing.assert_array_equal(distribution.sample(3), np.zeros(3))
41
+
30
42
  def test_return_period_input_uses_canonical_probability(self):
31
43
  distribution = TabulatedDistribution.from_return_periods(
32
44
  [10, 100, 1000],
@@ -44,7 +56,9 @@ class DistributionTests(unittest.TestCase):
44
56
  self.assertEqual(result.distribution.quantiles([0.1, 0.5, 0.9]).shape, (3,))
45
57
  self.assertEqual(result.distribution.sample(4, seed=42).shape, (4,))
46
58
  diagnostics = quality_metrics(source, result.distribution)
47
- self.assertAlmostEqual(diagnostics.ks_statistic, result.diagnostics.ks_statistic)
59
+ self.assertAlmostEqual(
60
+ diagnostics.ks_statistic, result.diagnostics.ks_statistic
61
+ )
48
62
  constrained = fit_distribution(
49
63
  source,
50
64
  constraints=FitConstraints(probability=0.99, maximum_value=100.0),
@@ -72,16 +86,12 @@ class DistributionTests(unittest.TestCase):
72
86
  )
73
87
 
74
88
  def test_sample_fit_rejects_probability_knots(self):
75
- tabulated = TabulatedDistribution(
76
- [0.1, 0.3, 0.6, 0.9], [0.0, 1.0, 2.0, 3.0]
77
- )
89
+ tabulated = TabulatedDistribution([0.1, 0.3, 0.6, 0.9], [0.0, 1.0, 2.0, 3.0])
78
90
  with self.assertRaisesRegex(ValueError, "fit_quantiles"):
79
91
  fit_distribution(tabulated) # type: ignore[arg-type]
80
92
 
81
93
  def test_hurdle_constructor_preserves_atom_and_truncated_tail(self):
82
- base = FittedDistribution.from_parameters(
83
- "gumbel_r", location=0.5, scale=1.2
84
- )
94
+ base = FittedDistribution.from_parameters("gumbel_r", location=0.5, scale=1.2)
85
95
  hurdle = HurdleDistribution(base, atom_probability=0.4)
86
96
  self.assertEqual(hurdle.ppf(0.4), 0.0)
87
97
  self.assertEqual(hurdle.point_mass(0.0), 0.4)
@@ -158,9 +168,7 @@ class PipelineTests(unittest.TestCase):
158
168
  class SpatialAndConstantTests(unittest.TestCase):
159
169
  def test_reference_spatial_tables_are_exposed_without_silent_defaults(self):
160
170
  self.assertEqual(lookup_continent(594475228122316799), "Europe")
161
- self.assertEqual(
162
- lookup_ipcc_region(594479566039285759), "Greenland/Iceland"
163
- )
171
+ self.assertEqual(lookup_ipcc_region(594479566039285759), "Greenland/Iceland")
164
172
  geography = lookup_geography(600550049193132031)
165
173
  self.assertIsNotNone(geography)
166
174
  self.assertEqual(geography.continent, "Africa")
File without changes
File without changes