crc-framework 0.2.0__tar.gz → 0.2.4__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.4}/.github/workflows/release.yaml +2 -5
  2. {crc_framework-0.2.0 → crc_framework-0.2.4}/Cargo.lock +12 -52
  3. {crc_framework-0.2.0 → crc_framework-0.2.4}/Cargo.toml +2 -2
  4. {crc_framework-0.2.0 → crc_framework-0.2.4}/PKG-INFO +8 -3
  5. {crc_framework-0.2.0 → crc_framework-0.2.4}/README.md +4 -2
  6. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/Cargo.toml +1 -1
  7. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/src/distribution.rs +53 -0
  8. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/src/lib.rs +2 -2
  9. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/src/reference/curve_fitting.rs +27 -0
  10. {crc_framework-0.2.0 → crc_framework-0.2.4}/pyproject.toml +5 -0
  11. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/__init__.py +9 -7
  12. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/_core.pyi +1 -0
  13. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/distributions/__init__.py +5 -3
  14. crc_framework-0.2.4/python/crc_framework/distributions/point_mass.py +55 -0
  15. {crc_framework-0.2.0 → crc_framework-0.2.4}/src/lib.rs +28 -11
  16. {crc_framework-0.2.0 → crc_framework-0.2.4}/tests/test_sdk.py +19 -11
  17. {crc_framework-0.2.0 → crc_framework-0.2.4}/.gitignore +0 -0
  18. {crc_framework-0.2.0 → crc_framework-0.2.4}/LICENSE +0 -0
  19. {crc_framework-0.2.0 → crc_framework-0.2.4}/THIRD_PARTY_NOTICES.md +0 -0
  20. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/src/constants.rs +0 -0
  21. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/src/error.rs +0 -0
  22. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/src/metrics.rs +0 -0
  23. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/src/reference/climate/ag_params.rs +0 -0
  24. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/src/reference/climate/energy_params.rs +0 -0
  25. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/src/reference/climate/flood_params.rs +0 -0
  26. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/src/reference/climate/mod.rs +0 -0
  27. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/src/reference/climate/precipitation_thresholds.rs +0 -0
  28. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/src/reference/climate/spei_params.rs +0 -0
  29. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/src/reference/climate/wind_params.rs +0 -0
  30. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/src/reference/mod.rs +0 -0
  31. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/src/reference/spatial/continent_r4.rs +0 -0
  32. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/src/reference/spatial/geography_r5.rs +0 -0
  33. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/src/reference/spatial/ipcc.rs +0 -0
  34. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/src/reference/spatial/mod.rs +0 -0
  35. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/src/risk.rs +0 -0
  36. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/src/spatial.rs +0 -0
  37. {crc_framework-0.2.0 → crc_framework-0.2.4}/crates/core/src/transform.rs +0 -0
  38. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/constants.py +0 -0
  39. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/distributions/base.py +0 -0
  40. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/distributions/empirical.py +0 -0
  41. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/distributions/fitted.py +0 -0
  42. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/distributions/fitting.py +0 -0
  43. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/distributions/hurdle.py +0 -0
  44. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/distributions/tabulated.py +0 -0
  45. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/metrics/__init__.py +0 -0
  46. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/metrics/distribution.py +0 -0
  47. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/metrics/microscores.py +0 -0
  48. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/metrics/risk.py +0 -0
  49. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/metrics/spanning.py +0 -0
  50. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/models.py +0 -0
  51. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/py.typed +0 -0
  52. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/spatial.py +0 -0
  53. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/transforms/__init__.py +0 -0
  54. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/transforms/base.py +0 -0
  55. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/transforms/builtin.py +0 -0
  56. {crc_framework-0.2.0 → crc_framework-0.2.4}/python/crc_framework/transforms/climate.py +0 -0
  57. {crc_framework-0.2.0 → crc_framework-0.2.4}/tests/test_impacts.py +0 -0
  58. {crc_framework-0.2.0 → crc_framework-0.2.4}/tests/typing_smoke.py +0 -0
@@ -69,7 +69,6 @@ jobs:
69
69
  3.11
70
70
  3.12
71
71
  3.13
72
- 3.13t
73
72
  3.14
74
73
  3.14t
75
74
  - name: Build wheels
@@ -79,12 +78,11 @@ jobs:
79
78
  with:
80
79
  target: ${{ matrix.target }}
81
80
  args: >-
82
- --release
83
- --out dist
81
+ --release
82
+ --out dist
84
83
  --find-interpreter
85
84
  sccache: "true"
86
85
  container: "off"
87
- rust-toolchain: nightly-2026-01-27
88
86
  - name: Upload wheels
89
87
  uses: actions/upload-artifact@v4
90
88
  with:
@@ -102,7 +100,6 @@ jobs:
102
100
  with:
103
101
  command: sdist
104
102
  args: --out dist
105
- rust-toolchain: nightly-2026-01-27
106
103
  - name: Upload sdist
107
104
  uses: actions/upload-artifact@v4
108
105
  with:
@@ -15,12 +15,6 @@ dependencies = [
15
15
  "zerocopy",
16
16
  ]
17
17
 
18
- [[package]]
19
- name = "autocfg"
20
- version = "1.5.1"
21
- source = "registry+https://github.com/rust-lang/crates.io-index"
22
- checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
23
-
24
18
  [[package]]
25
19
  name = "cfg-if"
26
20
  version = "1.0.4"
@@ -49,7 +43,7 @@ dependencies = [
49
43
 
50
44
  [[package]]
51
45
  name = "crc-framework-core"
52
- version = "0.2.0"
46
+ version = "0.2.4"
53
47
  dependencies = [
54
48
  "h3o",
55
49
  "phf",
@@ -58,7 +52,7 @@ dependencies = [
58
52
 
59
53
  [[package]]
60
54
  name = "crc-framework-rs"
61
- version = "0.2.0"
55
+ version = "0.2.4"
62
56
  dependencies = [
63
57
  "crc-framework-core",
64
58
  "h3o",
@@ -137,15 +131,6 @@ version = "0.5.0"
137
131
  source = "registry+https://github.com/rust-lang/crates.io-index"
138
132
  checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
139
133
 
140
- [[package]]
141
- name = "indoc"
142
- version = "2.0.7"
143
- source = "registry+https://github.com/rust-lang/crates.io-index"
144
- checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
145
- dependencies = [
146
- "rustversion",
147
- ]
148
-
149
134
  [[package]]
150
135
  name = "libc"
151
136
  version = "0.2.188"
@@ -158,15 +143,6 @@ version = "0.2.16"
158
143
  source = "registry+https://github.com/rust-lang/crates.io-index"
159
144
  checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
160
145
 
161
- [[package]]
162
- name = "memoffset"
163
- version = "0.9.1"
164
- source = "registry+https://github.com/rust-lang/crates.io-index"
165
- checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
166
- dependencies = [
167
- "autocfg",
168
- ]
169
-
170
146
  [[package]]
171
147
  name = "once_cell"
172
148
  version = "1.21.4"
@@ -242,35 +218,32 @@ dependencies = [
242
218
 
243
219
  [[package]]
244
220
  name = "pyo3"
245
- version = "0.27.2"
221
+ version = "0.29.0"
246
222
  source = "registry+https://github.com/rust-lang/crates.io-index"
247
- checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d"
223
+ checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c"
248
224
  dependencies = [
249
- "indoc",
250
225
  "libc",
251
- "memoffset",
252
226
  "once_cell",
253
227
  "portable-atomic",
254
228
  "pyo3-build-config",
255
229
  "pyo3-ffi",
256
230
  "pyo3-macros",
257
- "unindent",
258
231
  ]
259
232
 
260
233
  [[package]]
261
234
  name = "pyo3-build-config"
262
- version = "0.27.2"
235
+ version = "0.29.0"
263
236
  source = "registry+https://github.com/rust-lang/crates.io-index"
264
- checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6"
237
+ checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078"
265
238
  dependencies = [
266
239
  "target-lexicon",
267
240
  ]
268
241
 
269
242
  [[package]]
270
243
  name = "pyo3-ffi"
271
- version = "0.27.2"
244
+ version = "0.29.0"
272
245
  source = "registry+https://github.com/rust-lang/crates.io-index"
273
- checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089"
246
+ checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b"
274
247
  dependencies = [
275
248
  "libc",
276
249
  "pyo3-build-config",
@@ -278,9 +251,9 @@ dependencies = [
278
251
 
279
252
  [[package]]
280
253
  name = "pyo3-macros"
281
- version = "0.27.2"
254
+ version = "0.29.0"
282
255
  source = "registry+https://github.com/rust-lang/crates.io-index"
283
- checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02"
256
+ checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771"
284
257
  dependencies = [
285
258
  "proc-macro2",
286
259
  "pyo3-macros-backend",
@@ -290,13 +263,12 @@ dependencies = [
290
263
 
291
264
  [[package]]
292
265
  name = "pyo3-macros-backend"
293
- version = "0.27.2"
266
+ version = "0.29.0"
294
267
  source = "registry+https://github.com/rust-lang/crates.io-index"
295
- checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9"
268
+ checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362"
296
269
  dependencies = [
297
270
  "heck",
298
271
  "proc-macro2",
299
- "pyo3-build-config",
300
272
  "quote",
301
273
  "syn 2.0.119",
302
274
  ]
@@ -345,12 +317,6 @@ dependencies = [
345
317
  "getrandom 0.3.4",
346
318
  ]
347
319
 
348
- [[package]]
349
- name = "rustversion"
350
- version = "1.0.23"
351
- source = "registry+https://github.com/rust-lang/crates.io-index"
352
- checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
353
-
354
320
  [[package]]
355
321
  name = "serde"
356
322
  version = "1.0.229"
@@ -429,12 +395,6 @@ version = "1.0.24"
429
395
  source = "registry+https://github.com/rust-lang/crates.io-index"
430
396
  checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
431
397
 
432
- [[package]]
433
- name = "unindent"
434
- version = "0.2.4"
435
- source = "registry+https://github.com/rust-lang/crates.io-index"
436
- checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
437
-
438
398
  [[package]]
439
399
  name = "version_check"
440
400
  version = "0.9.5"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "crc-framework-rs"
3
- version = "0.2.0"
3
+ version = "0.2.4"
4
4
  edition = "2024"
5
5
  rust-version = "1.85"
6
6
  description = "Rust-powered Python bindings for the Climate Risk Commons Framework"
@@ -18,4 +18,4 @@ crate-type = ["cdylib"]
18
18
  [dependencies]
19
19
  crc-framework-core = { path = "crates/core" }
20
20
  h3o = "=0.9.3"
21
- pyo3 = { version = "0.27", features = ["extension-module"] }
21
+ pyo3 = { version = "0.29", features = ["extension-module"] }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crc-framework
3
- Version: 0.2.0
3
+ Version: 0.2.4
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
@@ -20,6 +20,9 @@ Author: Riskthinking.AI
20
20
  License-Expression: AGPL-3.0-or-later
21
21
  Requires-Python: >=3.9
22
22
  Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
23
+ Project-URL: Homepage, https://github.com/RiskThinking/crc-framework-rs
24
+ Project-URL: Issues, https://github.com/RiskThinking/crc-framework-rs/issues
25
+ Project-URL: Repository, https://github.com/RiskThinking/crc-framework-rs
23
26
 
24
27
  # Climate Risk Commons Framework
25
28
 
@@ -92,6 +95,7 @@ Choose a distribution according to the form of the available data:
92
95
  - `FittedDistribution` for a known parametric family and its parameters.
93
96
  - `HurdleDistribution` for a point mass followed by a truncated parametric
94
97
  tail.
98
+ - `PointMassDistribution` for a value that is constant at every probability.
95
99
 
96
100
  `TabulatedDistribution` does not extrapolate by default. Querying outside its
97
101
  declared probability range raises an error unless `extrapolate=True` is set.
@@ -307,8 +311,9 @@ continent and intersecting ISO3 country codes.
307
311
  The package exports the following primary interfaces:
308
312
 
309
313
  - Distributions: `EmpiricalDistribution`, `TabulatedDistribution`,
310
- `FittedDistribution`, `HurdleDistribution`, `fit_distribution`,
311
- `fit_quantiles`, `fit_hurdle_quantiles`, `fit_all`, and `quality_metrics`.
314
+ `FittedDistribution`, `HurdleDistribution`, `PointMassDistribution`,
315
+ `fit_distribution`, `fit_quantiles`, `fit_hurdle_quantiles`, `fit_all`, and
316
+ `quality_metrics`.
312
317
  - Transforms: `impacts`, `ImpactRegistry`, `ImpactFunction`, `ClimateImpact`,
313
318
  `LinearImpact`, `SigmoidImpact`, `PiecewiseLinearImpact`, `CallableImpact`,
314
319
  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.4"
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::{
@@ -469,6 +469,17 @@ where
469
469
  let mut values: Vec<f64> = simplex.iter().map(|point| objective(point)).collect();
470
470
  let mut evaluations = values.len();
471
471
 
472
+ // Multi-start callers (e.g. GenExtreme's 5 shape starts) run this to
473
+ // completion for every start even when a start is clearly not
474
+ // converging. Nelder-Mead's improvement curve is front-loaded, so a
475
+ // start that hasn't beaten its best objective in STALL_LIMIT iterations
476
+ // is treated the same as one that exhausted max_iterations: it returns
477
+ // early as not-converged, which the caller already ranks below any
478
+ // converged candidate.
479
+ const STALL_LIMIT: usize = 300;
480
+ let mut best_seen = f64::INFINITY;
481
+ let mut stall_count = 0usize;
482
+
472
483
  for iteration in 0..max_iterations {
473
484
  let mut order: Vec<usize> = (0..simplex.len()).collect();
474
485
  order.sort_by(|&left, &right| values[left].total_cmp(&values[right]));
@@ -498,6 +509,22 @@ where
498
509
  };
499
510
  }
500
511
 
512
+ if values[best] < best_seen - objective_tolerance {
513
+ best_seen = values[best];
514
+ stall_count = 0;
515
+ } else {
516
+ stall_count += 1;
517
+ if stall_count >= STALL_LIMIT {
518
+ return SimplexResult {
519
+ parameters: simplex[best].clone(),
520
+ objective: values[best],
521
+ converged: false,
522
+ iterations: iteration,
523
+ evaluations,
524
+ };
525
+ }
526
+ }
527
+
501
528
  let mut centroid = vec![0.0; dimensions];
502
529
  for &index in &order[..dimensions] {
503
530
  for (dimension, value) in centroid.iter_mut().enumerate() {
@@ -28,6 +28,11 @@ classifiers = [
28
28
  ]
29
29
  dynamic = ["version"]
30
30
 
31
+ [project.urls]
32
+ Homepage = "https://github.com/RiskThinking/crc-framework-rs"
33
+ Repository = "https://github.com/RiskThinking/crc-framework-rs"
34
+ Issues = "https://github.com/RiskThinking/crc-framework-rs/issues"
35
+
31
36
  [project.optional-dependencies]
32
37
  test = ["mypy", "pytest"]
33
38
 
@@ -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