1use crate::Real;
2use crate::error::KrigingError;
3
4fn matern_semivariance(d: Real, nugget: Real, partial_sill: Real, range: Real, nu: Real) -> Real {
6 if d <= 0.0 {
7 return nugget;
8 }
9 let nu_f64 = nu as f64;
10 let x_f64 = (d as f64) * (2.0 * nu_f64).sqrt() / (range as f64);
11 if x_f64 <= 0.0 {
12 return nugget;
13 }
14 let (_i_nu, k_nu) = puruspe::bessel::Inu_Knu(nu_f64, x_f64);
15 let gamma_nu = puruspe::gamma::gamma(nu_f64);
16 let factor = (2.0_f64).powf(1.0 - nu_f64) / gamma_nu * x_f64.powf(nu_f64) * k_nu;
17 let correlation = factor.clamp(0.0, 1.0);
18 nugget + partial_sill * (1.0 - (correlation as Real))
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum VariogramType {
24 Spherical,
25 Exponential,
26 Gaussian,
27 Cubic,
28 Stable,
30 Matern,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq)]
40pub enum VariogramModel {
41 Spherical {
42 nugget: Real,
43 sill: Real,
44 range: Real,
45 },
46 Exponential {
47 nugget: Real,
48 sill: Real,
49 range: Real,
50 },
51 Gaussian {
52 nugget: Real,
53 sill: Real,
54 range: Real,
55 },
56 Cubic {
57 nugget: Real,
58 sill: Real,
59 range: Real,
60 },
61 Stable {
62 nugget: Real,
63 sill: Real,
64 range: Real,
65 alpha: Real,
66 },
67 Matern {
68 nugget: Real,
69 sill: Real,
70 range: Real,
71 nu: Real,
72 },
73}
74
75impl VariogramModel {
76 pub fn new(
78 nugget: Real,
79 sill: Real,
80 range: Real,
81 model_type: VariogramType,
82 ) -> Result<Self, KrigingError> {
83 if !nugget.is_finite() || nugget < 0.0 {
84 return Err(KrigingError::FittingError(
85 "nugget must be finite and non-negative".to_string(),
86 ));
87 }
88 if !sill.is_finite() || sill <= nugget {
89 return Err(KrigingError::FittingError(
90 "sill must be finite and greater than nugget".to_string(),
91 ));
92 }
93 if !range.is_finite() || range <= 0.0 {
94 return Err(KrigingError::FittingError(
95 "range must be finite and positive".to_string(),
96 ));
97 }
98 Ok(match model_type {
99 VariogramType::Spherical => VariogramModel::Spherical {
100 nugget,
101 sill,
102 range,
103 },
104 VariogramType::Exponential => VariogramModel::Exponential {
105 nugget,
106 sill,
107 range,
108 },
109 VariogramType::Gaussian => VariogramModel::Gaussian {
110 nugget,
111 sill,
112 range,
113 },
114 VariogramType::Cubic => VariogramModel::Cubic {
115 nugget,
116 sill,
117 range,
118 },
119 VariogramType::Stable => VariogramModel::Stable {
120 nugget,
121 sill,
122 range,
123 alpha: 1.0,
124 },
125 VariogramType::Matern => VariogramModel::Matern {
126 nugget,
127 sill,
128 range,
129 nu: 0.5,
130 },
131 })
132 }
133
134 pub fn new_with_shape(
137 nugget: Real,
138 sill: Real,
139 range: Real,
140 model_type: VariogramType,
141 shape: Real,
142 ) -> Result<Self, KrigingError> {
143 if !nugget.is_finite() || nugget < 0.0 {
144 return Err(KrigingError::FittingError(
145 "nugget must be finite and non-negative".to_string(),
146 ));
147 }
148 if !sill.is_finite() || sill <= nugget {
149 return Err(KrigingError::FittingError(
150 "sill must be finite and greater than nugget".to_string(),
151 ));
152 }
153 if !range.is_finite() || range <= 0.0 {
154 return Err(KrigingError::FittingError(
155 "range must be finite and positive".to_string(),
156 ));
157 }
158 Ok(match model_type {
159 VariogramType::Spherical => VariogramModel::Spherical {
160 nugget,
161 sill,
162 range,
163 },
164 VariogramType::Exponential => VariogramModel::Exponential {
165 nugget,
166 sill,
167 range,
168 },
169 VariogramType::Gaussian => VariogramModel::Gaussian {
170 nugget,
171 sill,
172 range,
173 },
174 VariogramType::Cubic => VariogramModel::Cubic {
175 nugget,
176 sill,
177 range,
178 },
179 VariogramType::Stable => {
180 if !shape.is_finite() || shape <= 0.0 || shape > 2.0 {
181 return Err(KrigingError::FittingError(
182 "Stable shape (alpha) must be in (0, 2]".to_string(),
183 ));
184 }
185 VariogramModel::Stable {
186 nugget,
187 sill,
188 range,
189 alpha: shape,
190 }
191 }
192 VariogramType::Matern => {
193 if !shape.is_finite() || shape <= 0.0 {
194 return Err(KrigingError::FittingError(
195 "Matérn shape (nu) must be positive".to_string(),
196 ));
197 }
198 VariogramModel::Matern {
199 nugget,
200 sill,
201 range,
202 nu: shape,
203 }
204 }
205 })
206 }
207
208 pub fn variogram_type(&self) -> VariogramType {
209 match self {
210 Self::Spherical { .. } => VariogramType::Spherical,
211 Self::Exponential { .. } => VariogramType::Exponential,
212 Self::Gaussian { .. } => VariogramType::Gaussian,
213 Self::Cubic { .. } => VariogramType::Cubic,
214 Self::Stable { .. } => VariogramType::Stable,
215 Self::Matern { .. } => VariogramType::Matern,
216 }
217 }
218
219 pub fn params(&self) -> (Real, Real, Real) {
220 match self {
221 Self::Spherical {
222 nugget,
223 sill,
224 range,
225 }
226 | Self::Exponential {
227 nugget,
228 sill,
229 range,
230 }
231 | Self::Gaussian {
232 nugget,
233 sill,
234 range,
235 }
236 | Self::Cubic {
237 nugget,
238 sill,
239 range,
240 }
241 | Self::Stable {
242 nugget,
243 sill,
244 range,
245 ..
246 }
247 | Self::Matern {
248 nugget,
249 sill,
250 range,
251 ..
252 } => (*nugget, *sill, *range),
253 }
254 }
255
256 pub fn shape(&self) -> Option<Real> {
258 match self {
259 Self::Stable { alpha, .. } => Some(*alpha),
260 Self::Matern { nu, .. } => Some(*nu),
261 _ => None,
262 }
263 }
264
265 pub fn semivariance(&self, distance: Real) -> Real {
267 let d = distance.max(0.0);
268 let (nugget, sill, range) = self.params();
269 let partial_sill = sill - nugget;
270 let r = range.max(Real::EPSILON);
271
272 match self {
273 Self::Spherical { .. } => {
274 if d >= range {
275 sill
276 } else {
277 let x = d / r;
278 nugget + partial_sill * (1.5 * x - 0.5 * x.powi(3))
279 }
280 }
281 Self::Exponential { .. } => nugget + partial_sill * (1.0 - (-3.0 * d / r).exp()),
282 Self::Gaussian { .. } => {
283 nugget + partial_sill * (1.0 - (-3.0 * (d * d) / (r * r)).exp())
284 }
285 Self::Cubic { .. } => {
286 if d >= range {
287 sill
288 } else {
289 let x = d / r;
290 let poly = 7.0 * x * x - 8.5 * x.powi(3) + 3.5 * x.powi(5) - 0.5 * x.powi(7);
291 nugget + partial_sill * poly
292 }
293 }
294 Self::Stable { alpha, .. } => {
295 let x = (d / r).powf(*alpha);
296 nugget + partial_sill * (1.0 - (-x).exp())
297 }
298 Self::Matern { nu, .. } => matern_semivariance(d, nugget, partial_sill, r, *nu),
299 }
300 }
301
302 pub fn covariance(&self, distance: Real) -> Real {
303 let (_, sill, _) = self.params();
304 sill - self.semivariance(distance)
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use approx::assert_relative_eq;
312
313 #[test]
314 fn spherical_hits_sill_at_range() {
315 let model = VariogramModel::new(0.1, 1.0, 10.0, VariogramType::Spherical).unwrap();
316 assert_relative_eq!(model.semivariance(10.0), 1.0, epsilon = 1e-6);
317 assert_relative_eq!(model.semivariance(20.0), 1.0, epsilon = 1e-6);
318 }
319
320 #[test]
321 fn exponential_and_gaussian_start_at_nugget() {
322 let exp = VariogramModel::new(0.2, 1.2, 15.0, VariogramType::Exponential).unwrap();
323 let gauss = VariogramModel::new(0.2, 1.2, 15.0, VariogramType::Gaussian).unwrap();
324 assert_relative_eq!(exp.semivariance(0.0), 0.2, epsilon = 1e-6);
325 assert_relative_eq!(gauss.semivariance(0.0), 0.2, epsilon = 1e-6);
326 }
327
328 #[test]
329 fn covariance_complements_semivariance() {
330 let model = VariogramModel::new(0.1, 1.0, 5.0, VariogramType::Exponential).unwrap();
331 let d = 2.2;
332 assert_relative_eq!(
333 model.covariance(d) + model.semivariance(d),
334 1.0,
335 epsilon = 1e-5
336 );
337 }
338
339 #[test]
340 fn cubic_hits_sill_at_range() {
341 let model = VariogramModel::new(0.1, 1.0, 10.0, VariogramType::Cubic).unwrap();
342 assert_relative_eq!(model.semivariance(10.0), 1.0, epsilon = 1e-5);
343 assert_relative_eq!(model.semivariance(20.0), 1.0, epsilon = 1e-5);
344 }
345
346 #[test]
347 fn cubic_stable_matern_start_at_nugget() {
348 let cubic = VariogramModel::new(0.2, 1.2, 15.0, VariogramType::Cubic).unwrap();
349 let stable = VariogramModel::new(0.2, 1.2, 15.0, VariogramType::Stable).unwrap();
350 let matern = VariogramModel::new(0.2, 1.2, 15.0, VariogramType::Matern).unwrap();
351 assert_relative_eq!(cubic.semivariance(0.0), 0.2, epsilon = 1e-6);
352 assert_relative_eq!(stable.semivariance(0.0), 0.2, epsilon = 1e-6);
353 assert_relative_eq!(matern.semivariance(0.0), 0.2, epsilon = 1e-6);
354 }
355
356 #[test]
357 fn stable_with_alpha_one_increases_to_sill() {
358 let stable =
359 VariogramModel::new_with_shape(0.1, 2.0, 10.0, VariogramType::Stable, 1.0).unwrap();
360 let (nugget, sill, _) = stable.params();
361 assert_relative_eq!(stable.semivariance(0.0), nugget, epsilon = 1e-6);
362 let mut prev = nugget;
363 for d in [1.0, 3.0, 5.0, 10.0, 20.0] {
364 let g = stable.semivariance(d);
365 assert!(
366 g >= prev && g <= sill,
367 "stable semivariance should increase toward sill"
368 );
369 prev = g;
370 }
371 assert_relative_eq!(stable.semivariance(100.0), sill, epsilon = 1e-4);
372 }
373
374 #[test]
375 fn shape_returns_none_for_three_param_models() {
376 let m = VariogramModel::new(0.0, 1.0, 1.0, VariogramType::Cubic).unwrap();
377 assert_eq!(m.shape(), None);
378 }
379
380 #[test]
381 fn shape_returns_some_for_stable_and_matern() {
382 let stable =
383 VariogramModel::new_with_shape(0.0, 1.0, 1.0, VariogramType::Stable, 1.5).unwrap();
384 let matern =
385 VariogramModel::new_with_shape(0.0, 1.0, 1.0, VariogramType::Matern, 2.5).unwrap();
386 assert_relative_eq!(stable.shape().unwrap(), 1.5, epsilon = 1e-6);
387 assert_relative_eq!(matern.shape().unwrap(), 2.5, epsilon = 1e-6);
388 }
389}