crc-framework 0.2.5__tar.gz → 0.3.0__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.
- {crc_framework-0.2.5 → crc_framework-0.3.0}/Cargo.lock +2 -2
- {crc_framework-0.2.5 → crc_framework-0.3.0}/Cargo.toml +1 -1
- {crc_framework-0.2.5 → crc_framework-0.3.0}/PKG-INFO +26 -1
- {crc_framework-0.2.5 → crc_framework-0.3.0}/README.md +25 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/Cargo.toml +1 -1
- crc_framework-0.3.0/crates/core/src/distribution/lmoments.rs +215 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/src/distribution.rs +395 -11
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/src/lib.rs +1 -1
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/src/reference/curve_fitting.rs +35 -10
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/_core.pyi +1 -1
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/distributions/fitting.py +18 -1
- {crc_framework-0.2.5 → crc_framework-0.3.0}/src/lib.rs +24 -5
- crc_framework-0.3.0/tests/test_lmoments.py +138 -0
- crc_framework-0.3.0/tools/benchmark_lmoments.py +442 -0
- crc_framework-0.3.0/tools/benchmarks/lmoments-audit-m2-pro.json +851 -0
- crc_framework-0.3.0/tools/requirements-lmoments-benchmark.txt +6 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/.github/workflows/release.yaml +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/.gitignore +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/.python-version +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/LICENSE +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/THIRD_PARTY_NOTICES.md +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/src/constants.rs +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/src/error.rs +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/src/metrics.rs +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/src/reference/climate/ag_params.rs +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/src/reference/climate/energy_params.rs +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/src/reference/climate/flood_params.rs +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/src/reference/climate/mod.rs +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/src/reference/climate/precipitation_thresholds.rs +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/src/reference/climate/spei_params.rs +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/src/reference/climate/wind_params.rs +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/src/reference/mod.rs +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/src/reference/spatial/continent_r4.rs +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/src/reference/spatial/geography_r5.rs +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/src/reference/spatial/ipcc.rs +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/src/reference/spatial/mod.rs +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/src/risk.rs +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/src/spatial.rs +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/src/transform.rs +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/tests/quantile_fit_golden.rs +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/crates/core/tests/quantile_fit_golden.txt +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/pyproject.toml +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/__init__.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/constants.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/distributions/__init__.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/distributions/base.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/distributions/empirical.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/distributions/fitted.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/distributions/hurdle.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/distributions/point_mass.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/distributions/tabulated.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/metrics/__init__.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/metrics/distribution.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/metrics/microscores.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/metrics/risk.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/metrics/spanning.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/models.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/py.typed +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/spatial.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/transforms/__init__.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/transforms/base.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/transforms/builtin.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/python/crc_framework/transforms/climate.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/tests/test_impacts.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/tests/test_sdk.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/tests/typing_smoke.py +0 -0
- {crc_framework-0.2.5 → crc_framework-0.3.0}/uv.lock +0 -0
|
@@ -43,7 +43,7 @@ dependencies = [
|
|
|
43
43
|
|
|
44
44
|
[[package]]
|
|
45
45
|
name = "crc-framework-core"
|
|
46
|
-
version = "0.
|
|
46
|
+
version = "0.3.0"
|
|
47
47
|
dependencies = [
|
|
48
48
|
"h3o",
|
|
49
49
|
"phf",
|
|
@@ -52,7 +52,7 @@ dependencies = [
|
|
|
52
52
|
|
|
53
53
|
[[package]]
|
|
54
54
|
name = "crc-framework-rs"
|
|
55
|
-
version = "0.
|
|
55
|
+
version = "0.3.0"
|
|
56
56
|
dependencies = [
|
|
57
57
|
"crc-framework-core",
|
|
58
58
|
"h3o",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crc-framework
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -121,6 +121,31 @@ Kolmogorov–Smirnov p-value. The supported families are `genextreme`,
|
|
|
121
121
|
diagnostics. Passing a tabulated or fitted distribution is an error: those
|
|
122
122
|
values are not independent observations.
|
|
123
123
|
|
|
124
|
+
For a standalone L-moment estimate of GEV or Gumbel observations, select the
|
|
125
|
+
estimator explicitly:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from crc_framework import fit_distribution
|
|
129
|
+
|
|
130
|
+
result = fit_distribution(annual_extremes, family="genextreme", method="lmoments")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
This uses unbiased sample L-moments (Hosking, 1990), with an exact numerical
|
|
134
|
+
solution of the GEV L-skewness equation. It supports `genextreme`, `gumbel_r`
|
|
135
|
+
and `gumbel_l`, requires at least four finite, nonconstant observations, and
|
|
136
|
+
performs no likelihood optimization or automatic family selection. GEV shape
|
|
137
|
+
uses the SciPy convention (`c = -xi`); the finite-mean domain is `c > -1`.
|
|
138
|
+
Degenerate L-skewness is rejected rather than clipped. Fit constraints can
|
|
139
|
+
accept or reject the estimated distribution without refitting it.
|
|
140
|
+
|
|
141
|
+
The native implementation sorts once, uses compensated sums in scaled and
|
|
142
|
+
centered coordinates, and reuses that sort for diagnostics. Work is O(n log n)
|
|
143
|
+
with O(n) storage. The Python binding releases the GIL during native fitting.
|
|
144
|
+
The returned KS p-value is a descriptive diagnostic, not a calibrated hypothesis
|
|
145
|
+
test after parameter estimation. Defaults and the existing sample fitter are
|
|
146
|
+
unchanged. See [NIST's L-moments reference](https://www.itl.nist.gov/div898/software/dataplot/refman2/auxillar/lmoment.htm)
|
|
147
|
+
for background and the Hosking references.
|
|
148
|
+
|
|
124
149
|
Use `fit_quantiles` for probability/value knots. It minimizes weighted
|
|
125
150
|
value-space differences between the supplied values and the selected family's
|
|
126
151
|
PPF, requires an explicit family, and reports residual diagnostics rather than
|
|
@@ -95,6 +95,31 @@ Kolmogorov–Smirnov p-value. The supported families are `genextreme`,
|
|
|
95
95
|
diagnostics. Passing a tabulated or fitted distribution is an error: those
|
|
96
96
|
values are not independent observations.
|
|
97
97
|
|
|
98
|
+
For a standalone L-moment estimate of GEV or Gumbel observations, select the
|
|
99
|
+
estimator explicitly:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from crc_framework import fit_distribution
|
|
103
|
+
|
|
104
|
+
result = fit_distribution(annual_extremes, family="genextreme", method="lmoments")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
This uses unbiased sample L-moments (Hosking, 1990), with an exact numerical
|
|
108
|
+
solution of the GEV L-skewness equation. It supports `genextreme`, `gumbel_r`
|
|
109
|
+
and `gumbel_l`, requires at least four finite, nonconstant observations, and
|
|
110
|
+
performs no likelihood optimization or automatic family selection. GEV shape
|
|
111
|
+
uses the SciPy convention (`c = -xi`); the finite-mean domain is `c > -1`.
|
|
112
|
+
Degenerate L-skewness is rejected rather than clipped. Fit constraints can
|
|
113
|
+
accept or reject the estimated distribution without refitting it.
|
|
114
|
+
|
|
115
|
+
The native implementation sorts once, uses compensated sums in scaled and
|
|
116
|
+
centered coordinates, and reuses that sort for diagnostics. Work is O(n log n)
|
|
117
|
+
with O(n) storage. The Python binding releases the GIL during native fitting.
|
|
118
|
+
The returned KS p-value is a descriptive diagnostic, not a calibrated hypothesis
|
|
119
|
+
test after parameter estimation. Defaults and the existing sample fitter are
|
|
120
|
+
unchanged. See [NIST's L-moments reference](https://www.itl.nist.gov/div898/software/dataplot/refman2/auxillar/lmoment.htm)
|
|
121
|
+
for background and the Hosking references.
|
|
122
|
+
|
|
98
123
|
Use `fit_quantiles` for probability/value knots. It minimizes weighted
|
|
99
124
|
value-space differences between the supplied values and the selected family's
|
|
100
125
|
PPF, requires an explicit family, and reports residual diagnostics rather than
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
//! Unbiased sample L-moments (Hosking, 1990) for annual extreme families.
|
|
2
|
+
//! One O(n log n) sort, O(n) moment/diagnostic passes, no likelihood optimizer.
|
|
3
|
+
|
|
4
|
+
use super::{DistributionFamily, FittedDistribution};
|
|
5
|
+
use crate::error::{CrcError, Result};
|
|
6
|
+
|
|
7
|
+
const EULER: f64 = 0.577_215_664_901_532_9;
|
|
8
|
+
|
|
9
|
+
// Neumaier summation keeps the signed order-statistic sums accurate.
|
|
10
|
+
#[derive(Default)]
|
|
11
|
+
struct Sum(f64, f64);
|
|
12
|
+
impl Sum {
|
|
13
|
+
fn add(&mut self, x: f64) {
|
|
14
|
+
let next = self.0 + x;
|
|
15
|
+
self.1 += if self.0.abs() >= x.abs() {
|
|
16
|
+
(self.0 - next) + x
|
|
17
|
+
} else {
|
|
18
|
+
(x - next) + self.0
|
|
19
|
+
};
|
|
20
|
+
self.0 = next;
|
|
21
|
+
}
|
|
22
|
+
fn value(&self) -> f64 {
|
|
23
|
+
self.0 + self.1
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
pub(super) fn fit(
|
|
28
|
+
samples: &[f64],
|
|
29
|
+
family: DistributionFamily,
|
|
30
|
+
) -> Result<(FittedDistribution, Vec<f64>)> {
|
|
31
|
+
if !matches!(
|
|
32
|
+
family,
|
|
33
|
+
DistributionFamily::GenExtreme
|
|
34
|
+
| DistributionFamily::GumbelRight
|
|
35
|
+
| DistributionFamily::GumbelLeft
|
|
36
|
+
) {
|
|
37
|
+
return Err(CrcError::Unsupported(
|
|
38
|
+
"L-moments supports genextreme, gumbel_r and gumbel_l only".into(),
|
|
39
|
+
));
|
|
40
|
+
}
|
|
41
|
+
if samples.len() < 4 || samples.iter().any(|x| !x.is_finite()) {
|
|
42
|
+
return Err(CrcError::InvalidInput(
|
|
43
|
+
"L-moments requires at least four finite samples".into(),
|
|
44
|
+
));
|
|
45
|
+
}
|
|
46
|
+
let mut sorted = samples.to_vec();
|
|
47
|
+
sorted.sort_unstable_by(f64::total_cmp);
|
|
48
|
+
if sorted[0] == sorted[sorted.len() - 1] {
|
|
49
|
+
return Err(CrcError::InvalidInput(
|
|
50
|
+
"L-moments requires nonconstant samples".into(),
|
|
51
|
+
));
|
|
52
|
+
}
|
|
53
|
+
// Work in centered, bounded coordinates, avoiding overflow and cancellation
|
|
54
|
+
// in records with a large offset or a very small physical scale.
|
|
55
|
+
let magnitude = sorted[0].abs().max(sorted[sorted.len() - 1].abs());
|
|
56
|
+
let offset = sorted[sorted.len() / 2];
|
|
57
|
+
let n = sorted.len() as f64;
|
|
58
|
+
let mut sums = [Sum::default(), Sum::default(), Sum::default()];
|
|
59
|
+
for (i, &x) in sorted.iter().enumerate() {
|
|
60
|
+
let i = i as f64;
|
|
61
|
+
let delta = x - offset;
|
|
62
|
+
let y = if delta.is_finite() {
|
|
63
|
+
delta / magnitude
|
|
64
|
+
} else {
|
|
65
|
+
x / magnitude - offset / magnitude
|
|
66
|
+
};
|
|
67
|
+
let w1 = i / (n - 1.0);
|
|
68
|
+
let w2 = i * (i - 1.0) / ((n - 1.0) * (n - 2.0));
|
|
69
|
+
sums[0].add(y);
|
|
70
|
+
sums[1].add((2.0 * w1 - 1.0) * y);
|
|
71
|
+
sums[2].add((6.0 * w2 - 6.0 * w1 + 1.0) * y);
|
|
72
|
+
}
|
|
73
|
+
let l1 = sums[0].value() / n;
|
|
74
|
+
let l2 = sums[1].value() / n;
|
|
75
|
+
let l3 = sums[2].value() / n;
|
|
76
|
+
if !l2.is_finite() || l2 <= 0.0 {
|
|
77
|
+
return Err(CrcError::InvalidInput(
|
|
78
|
+
"L-moments requires positive L-scale".into(),
|
|
79
|
+
));
|
|
80
|
+
}
|
|
81
|
+
let (shape, location, scale) = match family {
|
|
82
|
+
DistributionFamily::GumbelRight => {
|
|
83
|
+
let scale = l2 / std::f64::consts::LN_2;
|
|
84
|
+
(None, l1 - EULER * scale, scale)
|
|
85
|
+
}
|
|
86
|
+
DistributionFamily::GumbelLeft => {
|
|
87
|
+
let scale = l2 / std::f64::consts::LN_2;
|
|
88
|
+
(None, l1 + EULER * scale, scale)
|
|
89
|
+
}
|
|
90
|
+
DistributionFamily::GenExtreme => gev_parameters(l1, l2, l3 / l2)?,
|
|
91
|
+
_ => unreachable!(),
|
|
92
|
+
};
|
|
93
|
+
let distribution = FittedDistribution::from_parameters(
|
|
94
|
+
family,
|
|
95
|
+
shape,
|
|
96
|
+
location * magnitude + offset,
|
|
97
|
+
scale * magnitude,
|
|
98
|
+
)?;
|
|
99
|
+
Ok((distribution, sorted))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fn gev_skew(shape: f64) -> f64 {
|
|
103
|
+
if shape.abs() < 1e-8 {
|
|
104
|
+
// Continuous limit and first derivative at the Gumbel shape.
|
|
105
|
+
let ratio = 3.0_f64.ln() / std::f64::consts::LN_2;
|
|
106
|
+
return 2.0 * ratio - 3.0 - ratio * (3.0_f64.ln() - std::f64::consts::LN_2) * shape;
|
|
107
|
+
}
|
|
108
|
+
2.0 * (-shape * 3.0_f64.ln()).exp_m1() / (-shape * std::f64::consts::LN_2).exp_m1() - 3.0
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fn gev_parameters(l1: f64, l2: f64, skew: f64) -> Result<(Option<f64>, f64, f64)> {
|
|
112
|
+
if !skew.is_finite() || skew.abs() >= 1.0 {
|
|
113
|
+
return Err(CrcError::InvalidInput(
|
|
114
|
+
"GEV L-skewness must be strictly between -1 and 1".into(),
|
|
115
|
+
));
|
|
116
|
+
}
|
|
117
|
+
// The exact L-skewness equation is monotone for c > -1 (finite mean).
|
|
118
|
+
// Bisection avoids approximation/clipping of strongly skewed samples.
|
|
119
|
+
let mut low = -1.0;
|
|
120
|
+
let mut high = 1.0;
|
|
121
|
+
for _ in 0..7 {
|
|
122
|
+
if gev_skew(high) <= skew {
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
high *= 2.0;
|
|
126
|
+
}
|
|
127
|
+
if gev_skew(high) > skew {
|
|
128
|
+
return Err(CrcError::InvalidInput(
|
|
129
|
+
"GEV L-skewness cannot be resolved numerically".into(),
|
|
130
|
+
));
|
|
131
|
+
}
|
|
132
|
+
for _ in 0..64 {
|
|
133
|
+
let mid = (low + high) / 2.0;
|
|
134
|
+
if mid == low || mid == high {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
if gev_skew(mid) > skew {
|
|
138
|
+
low = mid;
|
|
139
|
+
} else {
|
|
140
|
+
high = mid;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
let shape = (low + high) / 2.0;
|
|
144
|
+
if shape.abs() < 1e-10 {
|
|
145
|
+
let scale = l2 / std::f64::consts::LN_2;
|
|
146
|
+
return Ok((Some(0.0), l1 - EULER * scale, scale));
|
|
147
|
+
}
|
|
148
|
+
let log_gamma = log_gamma_one_plus(shape);
|
|
149
|
+
let denominator = -(-shape * std::f64::consts::LN_2).exp_m1();
|
|
150
|
+
let scale = l2 * (shape / denominator) * (-log_gamma).exp();
|
|
151
|
+
// Algebraically scale*(Gamma(1+c)-1)/c, without gamma overflow.
|
|
152
|
+
let location = l1 + l2 / denominator * -(-log_gamma).exp_m1();
|
|
153
|
+
Ok((Some(shape), location, scale))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fn log_gamma_one_plus(x: f64) -> f64 {
|
|
157
|
+
if x.abs() < 0.01 {
|
|
158
|
+
// log Gamma(1+x) = -gamma*x + sum (-1)^k zeta(k)*x^k/k.
|
|
159
|
+
return x
|
|
160
|
+
* (-EULER
|
|
161
|
+
+ x * (0.822_467_033_424_113_2
|
|
162
|
+
+ x * (-0.400_685_634_386_531_4
|
|
163
|
+
+ x * (0.270_580_808_427_784_5
|
|
164
|
+
+ x * (-0.207_385_551_028_674
|
|
165
|
+
+ x * (0.169_557_176_997_408_2 - x * 0.144_049_896_768_846_1))))));
|
|
166
|
+
}
|
|
167
|
+
// Lanczos in log space; recurrence keeps the argument >= 1, even c -> -1.
|
|
168
|
+
let z = 1.0 + x;
|
|
169
|
+
if z < 1.0 {
|
|
170
|
+
return log_gamma_one_plus(x + 1.0) - z.ln();
|
|
171
|
+
}
|
|
172
|
+
let coefficients = [
|
|
173
|
+
676.520_368_121_885_1,
|
|
174
|
+
-1_259.139_216_722_402_8,
|
|
175
|
+
771.323_428_777_653_1,
|
|
176
|
+
-176.615_029_162_140_6,
|
|
177
|
+
12.507_343_278_686_905,
|
|
178
|
+
-0.138_571_095_265_720_12,
|
|
179
|
+
9.984_369_578_019_572e-6,
|
|
180
|
+
1.505_632_735_149_311_6e-7,
|
|
181
|
+
];
|
|
182
|
+
let mut a = 0.999_999_999_999_809_9;
|
|
183
|
+
for (i, coefficient) in coefficients.iter().enumerate() {
|
|
184
|
+
a += coefficient / (x + i as f64 + 1.0);
|
|
185
|
+
}
|
|
186
|
+
let t = x + 7.5;
|
|
187
|
+
0.918_938_533_204_672_7 + (x + 0.5) * t.ln() - t + a.ln()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#[cfg(test)]
|
|
191
|
+
mod tests {
|
|
192
|
+
use super::*;
|
|
193
|
+
#[test]
|
|
194
|
+
fn exact_shape_equation_including_heavy_tails() {
|
|
195
|
+
for shape in [-0.95, -0.7, -0.2, -1e-5, 0.0, 1e-5, 0.2, 1.0, 5.0] {
|
|
196
|
+
let (actual, _, scale) = gev_parameters(2.0, 1.0, gev_skew(shape)).unwrap();
|
|
197
|
+
assert!(
|
|
198
|
+
(actual.unwrap() - shape).abs() < 1e-10,
|
|
199
|
+
"shape {shape}: {actual:?}"
|
|
200
|
+
);
|
|
201
|
+
assert!(scale > 0.0 && scale.is_finite());
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
#[test]
|
|
205
|
+
fn small_scales_and_large_offsets() {
|
|
206
|
+
for samples in [
|
|
207
|
+
[1e-20, 2e-20, 3e-20, 4e-20],
|
|
208
|
+
[1e15, 1e15 + 1.0, 1e15 + 2.0, 1e15 + 3.0],
|
|
209
|
+
[-1e308, -5e307, 5e307, 1e308],
|
|
210
|
+
] {
|
|
211
|
+
let (fit, _) = fit(&samples, DistributionFamily::GumbelRight).unwrap();
|
|
212
|
+
assert!(fit.scale.is_finite() && fit.scale > 0.0);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|