gstools_core/
variogram.rs

1//! Estimate empirical variograms.
2//!
3//! Calculate the empirical variogram according to
4//! $$
5//! \gamma(r_k) = \frac{1}{2N(r_k)} \sum_{i=1}^{N(r_k)}(f(x_i) - f(x_i^\prime))^2
6//! $$
7//! with
8//! * $r_k \leq \lVert x_i - x_i^\prime \rVert < r_{k+1}$ being the bins
9//! * $N(r_k)$ being the number of points in bin $r_k$
10//!
11//! If the estimator type 'c' for Cressie was chosen, the variogram is calculated by
12//! $$
13//! \gamma(r_k) = \frac{\frac{1}{2} \left( \frac{1}{N(r_k)} \sum_{i=1}^{N(r_k)}|f(x_i) - f(x_i^\prime)|^{0.5}\right)^4}{0.457 + 0.494 / N(r_k) + 0.045 / N^2(r_k)}
14//! $$
15
16use ndarray::{s, Array1, Array2, ArrayView1, ArrayView2, FoldWhile, Zip};
17use rayon::iter::{IntoParallelIterator, ParallelExtend, ParallelIterator};
18
19trait Estimator {
20    fn estimate(f_diff: f64) -> f64;
21    fn normalize(v: &mut f64, c: u64);
22}
23
24macro_rules! choose_estimator {
25    ( $estimator_type:expr => $estimator:ident: $code:block ) => {
26        match $estimator_type {
27            'c' => {
28                type $estimator = Cressie;
29
30                $code
31            }
32            _ => {
33                type $estimator = Matheron;
34
35                $code
36            }
37        }
38    };
39}
40
41struct Matheron;
42
43impl Estimator for Matheron {
44    fn estimate(f_diff: f64) -> f64 {
45        f_diff.powi(2)
46    }
47
48    fn normalize(v: &mut f64, c: u64) {
49        let cf = if c == 0 { 1.0 } else { c as f64 };
50        *v /= 2.0 * cf;
51    }
52}
53
54struct Cressie;
55
56impl Estimator for Cressie {
57    fn estimate(f_diff: f64) -> f64 {
58        f_diff.abs().sqrt()
59    }
60
61    fn normalize(v: &mut f64, c: u64) {
62        let cf = if c == 0 { 1.0 } else { c as f64 };
63        *v = 0.5 * (1. / cf * *v).powi(4) / (0.457 + 0.494 / cf + 0.045 / (cf * cf))
64    }
65}
66
67trait Distance {
68    fn dist(lhs: ArrayView1<f64>, rhs: ArrayView1<f64>) -> f64;
69
70    fn check_dim(_dim: usize) {}
71}
72
73macro_rules! choose_distance {
74    ( $distance_type:expr => $distance:ident: $code:block ) => {
75        match $distance_type {
76            'e' => {
77                type $distance = Euclid;
78
79                $code
80            }
81            _ => {
82                type $distance = Haversine;
83
84                $code
85            }
86        }
87    };
88}
89
90struct Euclid;
91
92impl Distance for Euclid {
93    fn dist(lhs: ArrayView1<f64>, rhs: ArrayView1<f64>) -> f64 {
94        Zip::from(lhs)
95            .and(rhs)
96            .fold(0.0, |mut acc, lhs, rhs| {
97                acc += (lhs - rhs).powi(2);
98
99                acc
100            })
101            .sqrt()
102    }
103}
104
105struct Haversine;
106
107impl Distance for Haversine {
108    fn dist(lhs: ArrayView1<f64>, rhs: ArrayView1<f64>) -> f64 {
109        let diff_lat = (lhs[0] - rhs[0]).to_radians();
110        let diff_lon = (lhs[1] - rhs[1]).to_radians();
111
112        let arg = (diff_lat / 2.0).sin().powi(2)
113            + lhs[0].to_radians().cos()
114                * rhs[0].to_radians().cos()
115                * (diff_lon / 2.0).sin().powi(2);
116
117        2.0 * arg.sqrt().atan2((1.0 - arg).sqrt())
118    }
119
120    fn check_dim(dim: usize) {
121        assert_eq!(dim, 2, "Haversine: dim = {dim} != 2");
122    }
123}
124
125/// Variogram estimation on a structured grid.
126///
127/// Calculates the empirical variogram according to the equations shown in the [module documentation](crate::variogram).
128///
129/// # Arguments
130///
131/// * `f` - the spatially distributed data
132/// * `estimator_type` - the estimator function, can be
133///     * 'm' - Matheron, the standard method of moments by Matheron
134///     * 'c' - Cressie, an estimator more robust to outliers
135/// * `num_threads` - the number of parallel threads used, if None, use rayon's default
136pub fn variogram_structured(
137    f: ArrayView2<'_, f64>,
138    estimator_type: char,
139    num_threads: Option<usize>,
140) -> Array1<f64> {
141    fn inner<E: Estimator>(f: ArrayView2<'_, f64>, num_threads: Option<usize>) -> Array1<f64> {
142        let size = f.dim().0;
143
144        let mut variogram = Vec::with_capacity(size);
145
146        variogram.push(0.0);
147
148        rayon::ThreadPoolBuilder::new()
149            .num_threads(num_threads.unwrap_or(rayon::current_num_threads()))
150            .build()
151            .unwrap()
152            .install(|| {
153                variogram.par_extend((1..size).into_par_iter().map(|k| {
154                    let mut value = 0.0;
155                    let mut count = 0;
156
157                    Zip::from(f.slice(s![..size - k, ..]))
158                        .and(f.slice(s![k.., ..]))
159                        .for_each(|f_i, f_j| {
160                            value += E::estimate(f_i - f_j);
161                            count += 1;
162                        });
163
164                    E::normalize(&mut value, count);
165
166                    value
167                }))
168            });
169
170        Array1::from_vec(variogram)
171    }
172
173    choose_estimator!(estimator_type => E: {
174        inner::<E>(f, num_threads)
175    })
176}
177
178/// Variogram estimation of a masked field on a structured grid.
179///
180/// Calculates the empirical variogram according to the equations shown in the [module documentation](crate::variogram).
181///
182/// # Arguments
183///
184/// * `f` - the spatially distributed data
185/// * `mask` - the mask for the data `f`
186/// * `estimator_type` - the estimator function, can be
187///     * 'm' - Matheron, the standard method of moments by Matheron
188///     * 'c' - Cressie, an estimator more robust to outliers
189/// * `num_threads` - the number of parallel threads used, if None, use rayon's default
190pub fn variogram_ma_structured(
191    f: ArrayView2<'_, f64>,
192    mask: ArrayView2<'_, bool>,
193    estimator_type: char,
194    num_threads: Option<usize>,
195) -> Array1<f64> {
196    fn inner<E: Estimator>(
197        f: ArrayView2<'_, f64>,
198        mask: ArrayView2<'_, bool>,
199        num_threads: Option<usize>,
200    ) -> Array1<f64> {
201        let size = f.dim().0;
202
203        let mut variogram = Vec::with_capacity(size);
204
205        variogram.push(0.0);
206
207        rayon::ThreadPoolBuilder::new()
208            .num_threads(num_threads.unwrap_or(rayon::current_num_threads()))
209            .build()
210            .unwrap()
211            .install(|| {
212                variogram.par_extend((1..size).into_par_iter().map(|k| {
213                    let mut value = 0.0;
214                    let mut count = 0;
215
216                    Zip::from(f.slice(s![..size - k, ..]))
217                        .and(f.slice(s![k.., ..]))
218                        .and(mask.slice(s![..size - k, ..]))
219                        .and(mask.slice(s![k.., ..]))
220                        .for_each(|f_i, f_j, m_i, m_j| {
221                            if *m_i || *m_j {
222                                return;
223                            }
224
225                            value += E::estimate(f_i - f_j);
226                            count += 1;
227                        });
228
229                    E::normalize(&mut value, count);
230
231                    value
232                }))
233            });
234
235        Array1::from_vec(variogram)
236    }
237
238    choose_estimator!(estimator_type => E: {
239        inner::<E>(f, mask, num_threads)
240    })
241}
242
243fn dir_test(
244    dir: ArrayView1<'_, f64>,
245    pos_i: ArrayView1<'_, f64>,
246    pos_j: ArrayView1<'_, f64>,
247    dist: f64,
248    angles_tol: f64,
249    bandwidth: f64,
250) -> bool {
251    //scalar-product calculation for bandwidth projection and angle calculation
252    let s_prod = Zip::from(dir)
253        .and(pos_i)
254        .and(pos_j)
255        .fold(0.0, |mut acc, dir, pos_i, pos_j| {
256            acc += (pos_i - pos_j) * dir;
257
258            acc
259        });
260
261    //calculate band-distance by projection of point-pair-vec to direction line
262    if bandwidth > 0.0 {
263        let b_dist = Zip::from(dir)
264            .and(pos_i)
265            .and(pos_j)
266            .fold(0.0, |mut acc, dir, pos_i, pos_j| {
267                acc += ((pos_i - pos_j) - s_prod * dir).powi(2);
268
269                acc
270            })
271            .sqrt();
272
273        if b_dist >= bandwidth {
274            return false;
275        }
276    }
277
278    //allow repeating points (dist = 0)
279    if dist > 0.0 {
280        //use smallest angle by taking absolute value for arccos angle formula
281        let angle = s_prod.abs() / dist;
282        if angle < 1.0 {
283            //else same direction (prevent numerical errors)
284            if angle.acos() >= angles_tol {
285                return false;
286            }
287        }
288    }
289
290    true
291}
292
293/// Directional variogram estimation on an unstructured grid.
294///
295/// Calculates the empirical variogram according to the equations shown in the [module documentation](crate::variogram).
296///
297/// # Arguments
298///
299/// * `f` - the spatially distributed data
300///   <br>&nbsp;&nbsp;&nbsp;&nbsp; dim = (no. of data fields, no. of spatial data points per field $i$)
301/// * `bin_edges` - the bins of the variogram
302///   <br>&nbsp;&nbsp;&nbsp;&nbsp; dim = number of bins j
303/// * `pos` - the positions of the data `f`
304///   <br>&nbsp;&nbsp;&nbsp;&nbsp; dim = (spatial dim. $d$, no. of spatial data points $i$)
305/// * `direction` - directions in which the variogram will be estimated
306///   <br>&nbsp;&nbsp;&nbsp;&nbsp; dim = (no. of directions, spatial dim. $d$)
307/// * `angles_tol` - the tolerance of the angles
308/// * `bandwidth` - bandwidth to cut off the angular tolerance
309/// * `separate_dirs` - do the direction bands overlap?
310/// * `estimator_type` - the estimator function, can be
311///     * 'm' - Matheron, the standard method of moments by Matheron
312///     * 'c' - Cressie, an estimator more robust to outliers
313/// * `num_threads` - the number of parallel threads used, if None, use rayon's default
314#[allow(clippy::too_many_arguments)]
315pub fn variogram_directional(
316    f: ArrayView2<'_, f64>,
317    bin_edges: ArrayView1<'_, f64>,
318    pos: ArrayView2<'_, f64>,
319    direction: ArrayView2<'_, f64>, // should be normed
320    angles_tol: f64,
321    bandwidth: f64,
322    separate_dirs: bool,
323    estimator_type: char,
324    num_threads: Option<usize>,
325) -> (Array2<f64>, Array2<u64>) {
326    assert_eq!(
327        pos.dim().0,
328        direction.dim().1,
329        "dim(pos) = {} != dim(direction) = {}",
330        pos.dim().0,
331        direction.dim().1,
332    );
333    assert_eq!(
334        pos.dim().1,
335        f.dim().1,
336        "len(pos) = {} != len(f) = {}",
337        pos.dim().1,
338        f.dim().1,
339    );
340    assert!(
341        bin_edges.dim() > 1,
342        "len(bin_edges) = {} < 2 too small",
343        bin_edges.dim()
344    );
345    assert!(
346        angles_tol > 0.0,
347        "tolerance for angle search masks must be > 0",
348    );
349
350    fn inner<E: Estimator>(
351        f: ArrayView2<'_, f64>,
352        bin_edges: ArrayView1<'_, f64>,
353        pos: ArrayView2<'_, f64>,
354        direction: ArrayView2<'_, f64>,
355        angles_tol: f64,
356        bandwidth: f64,
357        separate_dirs: bool,
358        num_threads: Option<usize>,
359    ) -> (Array2<f64>, Array2<u64>) {
360        let out_size = (direction.dim().0, bin_edges.dim() - 1);
361        let in_size = pos.dim().1 - 1;
362
363        let mut variogram = Array2::<f64>::zeros(out_size);
364        let mut counts = Array2::<u64>::zeros(out_size);
365
366        rayon::ThreadPoolBuilder::new()
367            .num_threads(num_threads.unwrap_or(rayon::current_num_threads()))
368            .build()
369            .unwrap()
370            .install(|| {
371                Zip::from(bin_edges.slice(s![..out_size.1]))
372                    .and(bin_edges.slice(s![1..]))
373                    .and(variogram.columns_mut())
374                    .and(counts.columns_mut())
375                    .par_for_each(
376                        |lower_bin_edge, upper_bin_edge, mut variogram, mut counts| {
377                            Zip::indexed(f.slice(s![.., ..in_size]).columns())
378                                .and(pos.slice(s![.., ..in_size]).columns())
379                                .for_each(|i, f_i, pos_i| {
380                                    Zip::from(f.slice(s![.., i + 1..]).columns())
381                                        .and(pos.slice(s![.., i + 1..]).columns())
382                                        .for_each(|f_j, pos_j| {
383                                            let dist = Euclid::dist(pos_i, pos_j);
384                                            if dist < *lower_bin_edge || dist >= *upper_bin_edge {
385                                                return; //skip if not in current bin
386                                            }
387
388                                            Zip::from(direction.rows())
389                                                .and(&mut variogram)
390                                                .and(&mut counts)
391                                                .fold_while((), |(), dir, variogram, counts| {
392                                                    if !dir_test(
393                                                        dir, pos_i, pos_j, dist, angles_tol,
394                                                        bandwidth,
395                                                    ) {
396                                                        return FoldWhile::Continue(());
397                                                    }
398
399                                                    Zip::from(f_i).and(f_j).for_each(|f_i, f_j| {
400                                                        let f_ij = f_i - f_j;
401                                                        if f_ij.is_nan() {
402                                                            return; // skip no data values
403                                                        }
404
405                                                        *counts += 1;
406                                                        *variogram += E::estimate(f_ij);
407                                                    });
408
409                                                    //once we found a fitting direction
410                                                    //break the search if directions are separated
411                                                    if separate_dirs {
412                                                        return FoldWhile::Done(());
413                                                    }
414
415                                                    FoldWhile::Continue(())
416                                                });
417                                        });
418                                });
419
420                            Zip::from(variogram)
421                                .and(counts)
422                                .for_each(|variogram, counts| {
423                                    E::normalize(variogram, *counts);
424                                });
425                        },
426                    )
427            });
428
429        (variogram, counts)
430    }
431
432    choose_estimator!(estimator_type => E: {
433        inner::<E>(
434            f,
435            bin_edges,
436            pos,
437            direction,
438            angles_tol,
439            bandwidth,
440            separate_dirs,
441            num_threads,
442        )
443    })
444}
445
446/// Variogram estimation on an unstructured grid.
447///
448/// Calculates the empirical variogram according to the equations shown in the [module documentation](crate::variogram).
449///
450/// # Arguments
451///
452/// * `f` - the spatially distributed data
453///   <br> dim = (no. of data fields, no. of spatial data points per field $i$)
454/// * `bin_edges` - the bins of the variogram
455///   <br> dim = number of bins j
456/// * `pos` - the positions of the data `f`
457///   <br> dim = (spatial dim. $d$, no. of spatial data points $i$)
458/// * `estimator_type` - the estimator function, can be
459///     * 'm' - Matheron, the standard method of moments by Matheron
460///     * 'c' - Cressie, an estimator more robust to outliers
461/// * `distance_type` - the distance function, can be
462///     * 'e' - Euclidean, the Euclidean distance
463///     * 'h' - Haversine, the great-circle distance
464/// * `num_threads` - the number of parallel threads used, if None, use rayon's default
465pub fn variogram_unstructured(
466    f: ArrayView2<'_, f64>,
467    bin_edges: ArrayView1<'_, f64>,
468    pos: ArrayView2<'_, f64>,
469    estimator_type: char,
470    distance_type: char,
471    num_threads: Option<usize>,
472) -> (Array1<f64>, Array1<u64>) {
473    assert_eq!(
474        pos.dim().1,
475        f.dim().1,
476        "len(pos) = {} != len(f) = {}",
477        pos.dim().1,
478        f.dim().1,
479    );
480    assert!(
481        bin_edges.dim() > 1,
482        "len(bin_edges) = {} < 2 too small",
483        bin_edges.dim()
484    );
485
486    fn inner<E: Estimator, D: Distance>(
487        f: ArrayView2<'_, f64>,
488        bin_edges: ArrayView1<'_, f64>,
489        pos: ArrayView2<'_, f64>,
490        num_threads: Option<usize>,
491    ) -> (Array1<f64>, Array1<u64>) {
492        D::check_dim(pos.dim().0);
493
494        let out_size = bin_edges.dim() - 1;
495        let in_size = pos.dim().1 - 1;
496
497        let mut variogram = Array1::<f64>::zeros(out_size);
498        let mut counts = Array1::<u64>::zeros(out_size);
499
500        rayon::ThreadPoolBuilder::new()
501            .num_threads(num_threads.unwrap_or(rayon::current_num_threads()))
502            .build()
503            .unwrap()
504            .install(|| {
505                Zip::from(bin_edges.slice(s![..out_size]))
506                    .and(bin_edges.slice(s![1..]))
507                    .and(&mut variogram)
508                    .and(&mut counts)
509                    .par_for_each(|lower_bin_edge, upper_bin_edge, variogram, counts| {
510                        Zip::indexed(f.slice(s![.., ..in_size]).columns())
511                            .and(pos.slice(s![.., ..in_size]).columns())
512                            .for_each(|i, f_i, pos_i| {
513                                Zip::from(f.slice(s![.., i + 1..]).columns())
514                                    .and(pos.slice(s![.., i + 1..]).columns())
515                                    .for_each(|f_j, pos_j| {
516                                        let dist = D::dist(pos_i, pos_j);
517                                        if dist < *lower_bin_edge || dist >= *upper_bin_edge {
518                                            return; //skip if not in current bin
519                                        }
520
521                                        Zip::from(f_i).and(f_j).for_each(|f_i, f_j| {
522                                            let f_ij = f_i - f_j;
523                                            if f_ij.is_nan() {
524                                                return; // skip no data values
525                                            }
526
527                                            *counts += 1;
528                                            *variogram += E::estimate(f_ij);
529                                        });
530                                    });
531                            });
532
533                        E::normalize(variogram, *counts);
534                    })
535            });
536
537        (variogram, counts)
538    }
539
540    choose_estimator!(estimator_type => E: {
541        choose_distance!(distance_type => D: {
542            inner::<E, D>(f, bin_edges, pos, num_threads)
543        })
544    })
545}
546
547#[cfg(test)]
548mod tests {
549    use super::*;
550
551    use approx::assert_ulps_eq;
552    use ndarray::{arr1, arr2, stack, Axis};
553
554    struct SetupStruct {
555        field: Array2<f64>,
556    }
557
558    impl SetupStruct {
559        fn new() -> Self {
560            Self {
561                field: arr2(&[
562                    [41.2],
563                    [40.2],
564                    [39.7],
565                    [39.2],
566                    [40.1],
567                    [38.3],
568                    [39.1],
569                    [40.0],
570                    [41.1],
571                    [40.3],
572                ]),
573            }
574        }
575    }
576
577    #[test]
578    fn test_variogram_struct() {
579        let setup = SetupStruct::new();
580
581        assert_ulps_eq!(
582            variogram_structured(setup.field.view(), 'm', None),
583            arr1(&[
584                0.0,
585                0.49166666666666814,
586                0.7625000000000011,
587                1.090714285714288,
588                0.9016666666666685,
589                1.3360000000000025,
590                0.9524999999999989,
591                0.4349999999999996,
592                0.004999999999999788,
593                0.40500000000000513
594            ]),
595            max_ulps = 6
596        );
597    }
598
599    #[test]
600    fn test_variogram_ma_struct() {
601        let setup = SetupStruct::new();
602        let mask1 = arr2(&[
603            [false],
604            [false],
605            [false],
606            [false],
607            [false],
608            [false],
609            [false],
610            [false],
611            [false],
612            [false],
613        ]);
614        let mask2 = arr2(&[
615            [true],
616            [false],
617            [false],
618            [false],
619            [false],
620            [false],
621            [false],
622            [false],
623            [false],
624            [false],
625        ]);
626
627        assert_ulps_eq!(
628            variogram_ma_structured(setup.field.view(), mask1.view(), 'm', None),
629            arr1(&[
630                0.0,
631                0.49166666666666814,
632                0.7625000000000011,
633                1.090714285714288,
634                0.9016666666666685,
635                1.3360000000000025,
636                0.9524999999999989,
637                0.4349999999999996,
638                0.004999999999999788,
639                0.40500000000000513
640            ]),
641            max_ulps = 6
642        );
643        assert_ulps_eq!(
644            variogram_ma_structured(setup.field.view(), mask2.view(), 'm', None),
645            arr1(&[
646                0.0,
647                0.4906250000000017,
648                0.710714285714287,
649                0.9391666666666693,
650                0.9610000000000019,
651                0.6187499999999992,
652                0.5349999999999975,
653                0.29249999999999765,
654                0.004999999999999432,
655                0.0
656            ]),
657            max_ulps = 6
658        );
659    }
660
661    struct SetupUnstruct {
662        pos: Array2<f64>,
663        field: Array2<f64>,
664        bin_edges: Array1<f64>,
665    }
666
667    impl SetupUnstruct {
668        fn new() -> Self {
669            Self {
670                pos: stack![
671                    Axis(0),
672                    Array1::range(0., 10., 1.),
673                    Array1::range(0., 10., 1.)
674                ],
675                field: arr2(&[[
676                    -1.2427955,
677                    -0.59811704,
678                    -0.57745039,
679                    0.01531904,
680                    -0.26474262,
681                    -0.53626347,
682                    -0.85106795,
683                    -1.96939178,
684                    -1.83650493,
685                    -1.23548617,
686                ]]),
687                bin_edges: Array1::linspace(0., 5., 4),
688            }
689        }
690    }
691
692    #[test]
693    fn test_variogram_unstruct() {
694        let setup = SetupUnstruct::new();
695        let (gamma, cnts) = variogram_unstructured(
696            setup.field.view(),
697            setup.bin_edges.view(),
698            setup.pos.view(),
699            'm',
700            'e',
701            None,
702        );
703        assert_ulps_eq!(
704            gamma,
705            arr1(&[0.14712242466045536, 0.320522186616688, 0.5136105328106929]),
706            max_ulps = 6,
707        );
708        assert_eq!(cnts, arr1(&[9, 8, 7]),);
709    }
710
711    #[test]
712    fn test_variogram_unstruct_multi_field() {
713        let setup = SetupUnstruct::new();
714        let field2 = arr2(&[[
715            1.2427955,
716            1.59811704,
717            1.57745039,
718            -1.01531904,
719            1.26474262,
720            1.53626347,
721            1.85106795,
722            0.96939178,
723            0.83650493,
724            0.23548617,
725        ]]);
726        let field_multi = arr2(&[
727            [
728                -1.2427955,
729                -0.59811704,
730                -0.57745039,
731                0.01531904,
732                -0.26474262,
733                -0.53626347,
734                -0.85106795,
735                -1.96939178,
736                -1.83650493,
737                -1.23548617,
738            ],
739            [
740                1.2427955,
741                1.59811704,
742                1.57745039,
743                -1.01531904,
744                1.26474262,
745                1.53626347,
746                1.85106795,
747                0.96939178,
748                0.83650493,
749                0.23548617,
750            ],
751        ]);
752        let (gamma, _) = variogram_unstructured(
753            setup.field.view(),
754            setup.bin_edges.view(),
755            setup.pos.view(),
756            'm',
757            'e',
758            None,
759        );
760        let (gamma2, _) = variogram_unstructured(
761            field2.view(),
762            setup.bin_edges.view(),
763            setup.pos.view(),
764            'm',
765            'e',
766            None,
767        );
768        let (gamma_multi, _) = variogram_unstructured(
769            field_multi.view(),
770            setup.bin_edges.view(),
771            setup.pos.view(),
772            'm',
773            'e',
774            None,
775        );
776        let gamma_single = 0.5 * (&gamma + &gamma2);
777        assert_ulps_eq!(gamma_multi, gamma_single, max_ulps = 6,);
778
779        let direction = arr2(&[[0., std::f64::consts::PI], [0., 0.]]);
780        let (gamma, _) = variogram_directional(
781            setup.field.view(),
782            setup.bin_edges.view(),
783            setup.pos.view(),
784            direction.view(),
785            std::f64::consts::PI / 8.,
786            -1.0,
787            false,
788            'm',
789            None,
790        );
791        let (gamma2, _) = variogram_directional(
792            field2.view(),
793            setup.bin_edges.view(),
794            setup.pos.view(),
795            direction.view(),
796            std::f64::consts::PI / 8.,
797            -1.0,
798            false,
799            'm',
800            None,
801        );
802        let (gamma_multi, _) = variogram_directional(
803            field_multi.view(),
804            setup.bin_edges.view(),
805            setup.pos.view(),
806            direction.view(),
807            std::f64::consts::PI / 8.,
808            -1.0,
809            false,
810            'm',
811            None,
812        );
813
814        let gamma_single = 0.5 * (&gamma + &gamma2);
815        assert_ulps_eq!(gamma_multi, gamma_single, max_ulps = 6,);
816    }
817
818    #[test]
819    fn test_variogram_directional() {
820        let setup = SetupUnstruct::new();
821        let direction = arr2(&[[0., std::f64::consts::PI], [0., 0.]]);
822        let (gamma, cnts) = variogram_directional(
823            setup.field.view(),
824            setup.bin_edges.view(),
825            setup.pos.view(),
826            direction.view(),
827            std::f64::consts::PI / 8.,
828            -1.0,
829            false,
830            'm',
831            None,
832        );
833        assert_ulps_eq!(
834            gamma,
835            arr2(&[
836                [0.14712242466045536, 0.320522186616688, 0.5136105328106929],
837                [0., 0., 0.]
838            ]),
839            max_ulps = 6,
840        );
841        assert_eq!(cnts, arr2(&[[9, 8, 7], [0, 0, 0]]),);
842    }
843}