Skip to main content

chronix_analytics/multivariate/
correlation.rs

1//! Correlation analysis: Pearson, Spearman, Kendall, rolling, lag, cross-correlation.
2
3use crate::compute::{simd_dot_product, simd_mean};
4
5use crate::multivariate::error::MultivariateError;
6
7/// Pairwise NaN deletion — keeps only indices where both x and y are finite.
8fn filter_nan_pairs(x: &[f64], y: &[f64]) -> (Vec<f64>, Vec<f64>) {
9    let (mut xf, mut yf) = (Vec::with_capacity(x.len()), Vec::with_capacity(y.len()));
10    for (&xv, &yv) in x.iter().zip(y.iter()) {
11        if xv.is_finite() && yv.is_finite() {
12            xf.push(xv);
13            yf.push(yv);
14        }
15    }
16    (xf, yf)
17}
18
19/// Common trait for pairwise correlation methods.
20pub trait CorrelationMethod: Send + Sync {
21    /// Compute the correlation coefficient between two equal-length slices.
22    fn compute(&self, x: &[f64], y: &[f64]) -> Result<f64, MultivariateError>;
23
24    /// Whether this method supports incremental (online) updates.
25    fn supports_incremental(&self) -> bool;
26}
27
28/// Pearson correlation coefficient.
29pub struct PearsonCorrelation;
30
31impl CorrelationMethod for PearsonCorrelation {
32    fn compute(&self, x: &[f64], y: &[f64]) -> Result<f64, MultivariateError> {
33        Self::compute(x, y)
34    }
35    fn supports_incremental(&self) -> bool {
36        true
37    }
38}
39
40impl PearsonCorrelation {
41    /// Compute Pearson correlation between two equal-length slices.
42    ///
43    /// `NaN` pairs are deleted pairwise — `rolling_corr` does the same within
44    /// each window. When either series has **zero variance** the correlation
45    /// is undefined and the result is `NaN`, not `0.0`: `0.0` asserts "these
46    /// series are uncorrelated" where the truth is "there is nothing to
47    /// correlate", and a caller acts on the first.
48    ///
49    /// That sentence used to claim `rolling_corr` shared this convention
50    /// while `rolling_corr` folded a `NaN` into a sliding accumulator and
51    /// nulled the rest of its partition. A doc comment asserting that two
52    /// implementations agree is worth a test that they do; see
53    /// `chronix/tests/analytics_null_semantics.rs`.
54    ///
55    /// # Errors
56    ///
57    /// [`MultivariateError::DimensionMismatch`] on unequal lengths, and
58    /// [`MultivariateError::InsufficientData`] with fewer than two usable
59    /// pairs.
60    pub fn compute(x: &[f64], y: &[f64]) -> Result<f64, MultivariateError> {
61        if x.len() != y.len() {
62            return Err(MultivariateError::DimensionMismatch {
63                expected: x.len(),
64                got: y.len(),
65            });
66        }
67        // Pairwise NaN deletion
68        let (x, y) = filter_nan_pairs(x, y);
69        if x.len() < 2 {
70            return Err(MultivariateError::InsufficientData {
71                min: 2,
72                got: x.len(),
73            });
74        }
75        let mx = simd_mean(&x);
76        let my = simd_mean(&y);
77        let dx: Vec<f64> = x.iter().map(|v| v - mx).collect();
78        let dy: Vec<f64> = y.iter().map(|v| v - my).collect();
79        let cov = simd_dot_product(&dx, &dy).map_err(|_| MultivariateError::InsufficientData {
80            min: 2,
81            got: dx.len(),
82        })?;
83        let sx = simd_dot_product(&dx, &dx)
84            .map_err(|_| MultivariateError::InsufficientData {
85                min: 2,
86                got: dx.len(),
87            })?
88            .sqrt();
89        let sy = simd_dot_product(&dy, &dy)
90            .map_err(|_| MultivariateError::InsufficientData {
91                min: 2,
92                got: dy.len(),
93            })?
94            .sqrt();
95        if sx < 1e-15 || sy < 1e-15 {
96            return Ok(f64::NAN);
97        }
98        Ok(cov / (sx * sy))
99    }
100}
101
102/// Spearman rank correlation.
103pub struct SpearmanCorrelation;
104
105impl CorrelationMethod for SpearmanCorrelation {
106    fn compute(&self, x: &[f64], y: &[f64]) -> Result<f64, MultivariateError> {
107        Self::compute(x, y)
108    }
109    fn supports_incremental(&self) -> bool {
110        false
111    }
112}
113
114impl SpearmanCorrelation {
115    /// Compute Spearman correlation (rank-based).
116    pub fn compute(x: &[f64], y: &[f64]) -> Result<f64, MultivariateError> {
117        if x.len() != y.len() {
118            return Err(MultivariateError::DimensionMismatch {
119                expected: x.len(),
120                got: y.len(),
121            });
122        }
123        // Pairwise NaN deletion
124        let (x, y) = filter_nan_pairs(x, y);
125        if x.len() < 2 {
126            return Err(MultivariateError::InsufficientData {
127                min: 2,
128                got: x.len(),
129            });
130        }
131        let rx = rank(&x);
132        let ry = rank(&y);
133        PearsonCorrelation::compute(&rx, &ry)
134    }
135}
136
137fn rank(data: &[f64]) -> Vec<f64> {
138    let n = data.len();
139    let mut indexed: Vec<(usize, f64)> = data.iter().copied().enumerate().collect();
140    indexed.sort_by(|a, b| a.1.total_cmp(&b.1));
141    let mut ranks = vec![0.0; n];
142    let mut i = 0;
143    while i < n {
144        let mut j = i;
145        while j < n && (indexed[j].1 - indexed[i].1).abs() < 1e-15 {
146            j += 1;
147        }
148        let avg_rank = (i + j + 1) as f64 / 2.0;
149        for item in &indexed[i..j] {
150            ranks[item.0] = avg_rank;
151        }
152        i = j;
153    }
154    ranks
155}
156
157/// Kendall Tau-a rank correlation.
158pub struct KendallTau;
159
160impl CorrelationMethod for KendallTau {
161    fn compute(&self, x: &[f64], y: &[f64]) -> Result<f64, MultivariateError> {
162        Self::compute(x, y)
163    }
164    fn supports_incremental(&self) -> bool {
165        false
166    }
167}
168
169impl KendallTau {
170    /// Compute Kendall Tau-b using O(n log n) merge-sort algorithm (Knight 2006).
171    ///
172    /// Counts discordant pairs via merge-sort-based inversion counting on the
173    /// y-ranks after sorting by x-ranks.  Tau-b accounts for ties on both
174    /// x and y through the denominator √((n₀−n₁)(n₀−n₂)).
175    pub fn compute(x: &[f64], y: &[f64]) -> Result<f64, MultivariateError> {
176        if x.len() != y.len() {
177            return Err(MultivariateError::DimensionMismatch {
178                expected: x.len(),
179                got: y.len(),
180            });
181        }
182        // Pairwise NaN deletion
183        let (x, y) = filter_nan_pairs(x, y);
184        let n = x.len();
185        if n < 2 {
186            return Err(MultivariateError::InsufficientData { min: 2, got: n });
187        }
188
189        // Sort pairs by x, breaking ties by y
190        let mut pairs: Vec<(f64, f64)> = x.iter().copied().zip(y.iter().copied()).collect();
191        pairs.sort_by(|a, b| a.0.total_cmp(&b.0).then_with(|| a.1.total_cmp(&b.1)));
192
193        // Extract y-values in x-sorted order; count inversions via merge sort
194        let mut y_sorted: Vec<f64> = pairs.iter().map(|p| p.1).collect();
195        let mut scratch = vec![0.0f64; n];
196        let discordant = Self::merge_sort_count(&mut y_sorted, &mut scratch);
197
198        let n0 = (n * (n - 1) / 2) as f64; // total pairs
199
200        // Count tied pairs on x
201        let mut ties_x: i64 = 0;
202        let mut i = 0;
203        while i < n {
204            let mut j = i + 1;
205            while j < n && (pairs[j].0 - pairs[i].0).abs() < 1e-15 {
206                j += 1;
207            }
208            let run = (j - i) as i64;
209            ties_x += run * (run - 1) / 2;
210            i = j;
211        }
212
213        // Count tied pairs on y (from the sorted y values — re-sort by y)
214        let mut y_vals: Vec<f64> = pairs.iter().map(|p| p.1).collect();
215        y_vals.sort_by(f64::total_cmp);
216        let mut ties_y: i64 = 0;
217        let mut i = 0;
218        while i < n {
219            let mut j = i + 1;
220            while j < n && (y_vals[j] - y_vals[i]).abs() < 1e-15 {
221                j += 1;
222            }
223            let run = (j - i) as i64;
224            ties_y += run * (run - 1) / 2;
225            i = j;
226        }
227
228        // Count joint ties (tied on BOTH x and y).
229        let mut ties_xy: i64 = 0;
230        let mut i = 0;
231        while i < n {
232            let mut j = i + 1;
233            while j < n
234                && (pairs[j].0 - pairs[i].0).abs() < 1e-15
235                && (pairs[j].1 - pairs[i].1).abs() < 1e-15
236            {
237                j += 1;
238            }
239            let run = (j - i) as i64;
240            ties_xy += run * (run - 1) / 2;
241            i = j;
242        }
243
244        // concordant = n0 - discordant - (ties_x - ties_xy) - (ties_y - ties_xy) - ties_xy
245        //            = n0 - discordant - ties_x - ties_y + ties_xy
246        let concordant = n0 as i64 - discordant as i64 - ties_x - ties_y + ties_xy;
247        let denom = ((n0 - ties_x as f64) * (n0 - ties_y as f64)).sqrt();
248        if denom < 1e-15 {
249            return Ok(0.0);
250        }
251        let tau = (concordant - discordant as i64) as f64 / denom;
252        Ok(tau.clamp(-1.0, 1.0))
253    }
254
255    /// Merge-sort-based inversion count — O(n log n).
256    ///
257    /// Uses `scratch` as a pre-allocated temporary buffer to avoid
258    /// per-recursion allocations.  `scratch` must be at least as long
259    /// as `arr`.
260    fn merge_sort_count(arr: &mut [f64], scratch: &mut [f64]) -> usize {
261        let n = arr.len();
262        if n <= 1 {
263            return 0;
264        }
265        let mid = n / 2;
266
267        let mut count = Self::merge_sort_count(&mut arr[..mid], &mut scratch[..mid])
268            + Self::merge_sort_count(&mut arr[mid..], &mut scratch[mid..n]);
269
270        // Merge into scratch, then copy back
271        let (mut i, mut j, mut k) = (0, 0, 0);
272        while i < mid && j < n - mid {
273            if arr[i] <= arr[mid + j] {
274                scratch[k] = arr[i];
275                i += 1;
276            } else {
277                scratch[k] = arr[mid + j];
278                count += mid - i; // all remaining left elements are inversions
279                j += 1;
280            }
281            k += 1;
282        }
283        while i < mid {
284            scratch[k] = arr[i];
285            i += 1;
286            k += 1;
287        }
288        while j < n - mid {
289            scratch[k] = arr[mid + j];
290            j += 1;
291            k += 1;
292        }
293        arr[..n].copy_from_slice(&scratch[..n]);
294        count
295    }
296}
297
298/// Rolling Pearson correlation with O(1) update per point.
299pub struct RollingCorrelation {
300    window: usize,
301}
302
303impl RollingCorrelation {
304    /// Creates a new rolling correlator with the given window size.
305    pub fn new(window: usize) -> Self {
306        Self {
307            window: window.max(1),
308        }
309    }
310
311    /// Compute rolling Pearson correlation between two series.
312    pub fn compute(&self, x: &[f64], y: &[f64]) -> Result<Vec<f64>, MultivariateError> {
313        if x.len() != y.len() {
314            return Err(MultivariateError::DimensionMismatch {
315                expected: x.len(),
316                got: y.len(),
317            });
318        }
319        let n = x.len();
320        if n < self.window {
321            return Err(MultivariateError::InsufficientData {
322                min: self.window,
323                got: n,
324            });
325        }
326
327        let mut result = Vec::with_capacity(n);
328        let mut sx = 0.0;
329        let mut sy = 0.0;
330        let mut sxx = 0.0;
331        let mut syy = 0.0;
332        let mut sxy = 0.0;
333
334        for i in 0..n {
335            sx += x[i];
336            sy += y[i];
337            sxx += x[i] * x[i];
338            syy += y[i] * y[i];
339            sxy += x[i] * y[i];
340
341            if i >= self.window {
342                let old = i - self.window;
343                sx -= x[old];
344                sy -= y[old];
345                sxx -= x[old] * x[old];
346                syy -= y[old] * y[old];
347                sxy -= x[old] * y[old];
348            }
349
350            // Periodic FP drift recomputation every 1024 steps.
351            if i > 0 && i % 1024 == 0 {
352                let start = i.saturating_sub(self.window.saturating_sub(1));
353                sx = x[start..=i].iter().sum();
354                sy = y[start..=i].iter().sum();
355                sxx = x[start..=i].iter().map(|&v| v * v).sum();
356                syy = y[start..=i].iter().map(|&v| v * v).sum();
357                sxy = x[start..=i]
358                    .iter()
359                    .zip(&y[start..=i])
360                    .map(|(&a, &b)| a * b)
361                    .sum();
362            }
363
364            // Emit NaN until the window is full, and for an
365            // undefined correlation.
366            //
367            // This function previously reported a value computed from a
368            // *partial* window for the first `window - 1` rows, and `0.0`
369            // when either series had zero variance. Both are wrong and they
370            // disagreed with `preprocess::rolling_corr`, which returns NaN in
371            // exactly those cases — two functions with the same name and
372            // purpose gave different answers for the same input.
373            //
374            // NaN is also the semantically correct value: `0.0` claims "these
375            // series are uncorrelated" when the truth is "correlation is
376            // undefined here". This matches pandas
377            // `rolling(window).corr()`, whose `min_periods` defaults to the
378            // window size.
379            if i + 1 < self.window {
380                result.push(f64::NAN);
381                continue;
382            }
383
384            let count = (i + 1).min(self.window) as f64;
385            let mx = sx / count;
386            let my = sy / count;
387            let cov = sxy / count - mx * my;
388            let vx = (sxx / count - mx * mx).max(0.0);
389            let vy = (syy / count - my * my).max(0.0);
390            let denom = (vx * vy).sqrt();
391            if denom < 1e-15 {
392                result.push(f64::NAN);
393            } else {
394                result.push(cov / denom);
395            }
396        }
397        Ok(result)
398    }
399}
400
401/// Lag correlation: Pearson at various time offsets.
402pub struct LagCorrelation;
403
404impl LagCorrelation {
405    /// Compute Pearson correlation at each lag in `lags`.
406    ///
407    /// A lag whose correlation is **undefined** — the shift consumes the whole
408    /// series, or either window has zero variance — yields `NaN`, not `0.0`.
409    /// `0.0` is the worse answer of the two because it conflates "uncorrelated"
410    /// with "there is not enough information to say", and it is *actionable*
411    /// where a `NaN` is visibly absent (the same convention
412    /// [`RollingCorrelation`] follows).
413    ///
414    /// Cost is O(n) per lag, so O(n·|lags|) overall — ask for the lags wanted
415    /// rather than a range and then one of them.
416    ///
417    /// # Errors
418    ///
419    /// [`MultivariateError::DimensionMismatch`] if `x` and `y` differ in
420    /// length.
421    pub fn compute(
422        x: &[f64],
423        y: &[f64],
424        lags: &[i32],
425    ) -> Result<Vec<(i32, f64)>, MultivariateError> {
426        if x.len() != y.len() {
427            return Err(MultivariateError::DimensionMismatch {
428                expected: x.len(),
429                got: y.len(),
430            });
431        }
432        let mut results = Vec::with_capacity(lags.len());
433        for &lag in lags {
434            let (xs, ys) = if lag >= 0 {
435                let l = lag as usize;
436                if l >= x.len() {
437                    results.push((lag, f64::NAN));
438                    continue;
439                }
440                (&x[l..], &y[..x.len() - l])
441            } else {
442                let l = (-lag) as usize;
443                if l >= y.len() {
444                    results.push((lag, f64::NAN));
445                    continue;
446                }
447                (&x[..y.len() - l], &y[l..])
448            };
449            let r = PearsonCorrelation::compute(xs, ys).unwrap_or(f64::NAN);
450            results.push((lag, r));
451        }
452        Ok(results)
453    }
454}
455
456/// Full N×N pairwise Pearson correlation matrix.
457pub struct CrossCorrelationMatrix;
458
459impl CrossCorrelationMatrix {
460    /// Compute pairwise Pearson correlation for all series.
461    ///
462    /// Returns a symmetric N×N matrix.
463    pub fn compute(data: &[&[f64]]) -> Result<Vec<Vec<f64>>, MultivariateError> {
464        let _start = std::time::Instant::now();
465        let n = data.len();
466        let mut matrix = vec![vec![0.0; n]; n];
467        for i in 0..n {
468            matrix[i][i] = 1.0;
469            for j in (i + 1)..n {
470                let r = PearsonCorrelation::compute(data[i], data[j]).unwrap_or(f64::NAN);
471                matrix[i][j] = r;
472                matrix[j][i] = r;
473            }
474        }
475        metrics::histogram!("chronix_correlation_compute_duration_seconds")
476            .record(_start.elapsed().as_secs_f64());
477        Ok(matrix)
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484
485    #[test]
486    fn pearson_perfectly_correlated() {
487        let x: Vec<f64> = (0..100).map(|i| i as f64).collect();
488        let y: Vec<f64> = (0..100).map(|i| i as f64 * 2.0 + 5.0).collect();
489        let r = PearsonCorrelation::compute(&x, &y).unwrap();
490        assert!((r - 1.0).abs() < 1e-6);
491    }
492
493    #[test]
494    fn pearson_anti_correlated() {
495        let x: Vec<f64> = (0..100).map(|i| i as f64).collect();
496        let y: Vec<f64> = (0..100).map(|i| -(i as f64)).collect();
497        let r = PearsonCorrelation::compute(&x, &y).unwrap();
498        assert!((r - (-1.0)).abs() < 1e-6);
499    }
500
501    #[test]
502    fn spearman_rank_correlation() {
503        let x: Vec<f64> = (0..50).map(|i| i as f64).collect();
504        let y: Vec<f64> = (0..50).map(|i| (i as f64).powi(2)).collect();
505        let r = SpearmanCorrelation::compute(&x, &y).unwrap();
506        // Monotonic relationship → Spearman = 1.0
507        assert!((r - 1.0).abs() < 1e-6);
508    }
509
510    #[test]
511    fn kendall_tau() {
512        let x: Vec<f64> = (0..20).map(|i| i as f64).collect();
513        let y: Vec<f64> = (0..20).map(|i| i as f64 * 3.0).collect();
514        let r = KendallTau::compute(&x, &y).unwrap();
515        assert!((r - 1.0).abs() < 1e-6);
516    }
517
518    /// `RollingCorrelation` and `preprocess::rolling_corr` compute
519    /// the same statistic and must agree on conventions — partial windows and
520    /// undefined correlations are NaN in both.
521    #[test]
522    fn rolling_correlation_conventions_match_preprocess() {
523        let n = 60;
524        let window = 10;
525        let x: Vec<f64> = (0..n).map(|i| (i as f64 * 0.7).sin()).collect();
526        let y: Vec<f64> = (0..n).map(|i| (i as f64 * 0.7).cos()).collect();
527
528        let ours = RollingCorrelation::new(window).compute(&x, &y).unwrap();
529        let theirs = crate::preprocess::rolling_corr(&x, &y, window);
530
531        assert_eq!(ours.len(), theirs.len());
532        for i in 0..n {
533            assert_eq!(
534                ours[i].is_nan(),
535                theirs[i].is_nan(),
536                "NaN convention differs at index {i}: {} vs {}",
537                ours[i],
538                theirs[i]
539            );
540            if !ours[i].is_nan() {
541                assert!(
542                    (ours[i] - theirs[i]).abs() < 1e-9,
543                    "values differ at index {i}: {} vs {}",
544                    ours[i],
545                    theirs[i]
546                );
547            }
548        }
549
550        // And the convention is specifically: NaN until the window is full.
551        assert!(ours[..window - 1].iter().all(|v| v.is_nan()));
552        assert!(!ours[window - 1].is_nan());
553    }
554
555    /// A constant series has zero variance, so the correlation is undefined —
556    /// not zero. Reporting `0.0` claimed "uncorrelated" for "unknown".
557    #[test]
558    fn zero_variance_is_nan_not_zero() {
559        let x = vec![5.0; 40];
560        let y: Vec<f64> = (0..40).map(|i| i as f64).collect();
561        let r = RollingCorrelation::new(10).compute(&x, &y).unwrap();
562        assert!(
563            r[39].is_nan(),
564            "zero-variance correlation should be NaN, got {}",
565            r[39]
566        );
567    }
568
569    #[test]
570    fn rolling_matches_offline() {
571        let n = 200;
572        let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
573        let y: Vec<f64> = (0..n).map(|i| (i as f64 * 0.5) + 3.0).collect();
574        let rc = RollingCorrelation::new(50);
575        let result = rc.compute(&x, &y).unwrap();
576        // After the first window, should be close to 1.0
577        assert!((result[n - 1] - 1.0).abs() < 1e-3);
578    }
579
580    #[test]
581    fn cross_correlation_matrix() {
582        let a: Vec<f64> = (0..100).map(|i| i as f64).collect();
583        let b: Vec<f64> = (0..100).map(|i| i as f64 * 2.0).collect();
584        let c: Vec<f64> = (0..100).map(|i| -(i as f64)).collect();
585        let data: Vec<&[f64]> = vec![&a, &b, &c];
586        let mat = CrossCorrelationMatrix::compute(&data).unwrap();
587        assert_eq!(mat.len(), 3);
588        assert!((mat[0][1] - 1.0).abs() < 1e-6); // a and b perfectly correlated
589        assert!((mat[0][2] - (-1.0)).abs() < 1e-6); // a and c anti-correlated
590        assert!((mat[0][0] - 1.0).abs() < 1e-15); // diagonal = 1
591    }
592
593    #[test]
594    fn lag_correlation() {
595        let x: Vec<f64> = (0..100).map(|i| i as f64).collect();
596        let y: Vec<f64> = (0..100).map(|i| i as f64).collect();
597        let results = LagCorrelation::compute(&x, &y, &[-1, 0, 1]).unwrap();
598        assert_eq!(results.len(), 3);
599        // At lag 0 should be ~1.0
600        assert!((results[1].1 - 1.0).abs() < 1e-3);
601    }
602
603    /// An undefined lag correlation is `NaN`, never `0.0`. `0.0` asserts
604    /// "these series are uncorrelated" where the truth is "there is not enough
605    /// information to say", and a caller acts on the first and ignores the
606    /// second.
607    #[test]
608    fn an_undefined_lag_correlation_is_nan() {
609        let x = vec![1.0, 2.0, 3.0, 4.0];
610        let y = vec![2.0, 4.0, 6.0, 8.0];
611
612        // A shift longer than the series leaves nothing to correlate.
613        let out = LagCorrelation::compute(&x, &y, &[10, -10]).unwrap();
614        assert!(out.iter().all(|(_, r)| r.is_nan()), "{out:?}");
615
616        // Zero variance in one window is undefined too.
617        let flat = vec![7.0; 4];
618        let out = LagCorrelation::compute(&flat, &y, &[0]).unwrap();
619        assert!(out[0].1.is_nan(), "{out:?}");
620
621        // A defined lag is still a number.
622        let out = LagCorrelation::compute(&x, &y, &[0]).unwrap();
623        assert!((out[0].1 - 1.0).abs() < 1e-9, "{out:?}");
624    }
625
626    #[test]
627    fn correlation_method_trait_dispatch() {
628        let x: Vec<f64> = (0..50).map(|i| i as f64).collect();
629        let y: Vec<f64> = (0..50).map(|i| i as f64 * 2.0 + 1.0).collect();
630
631        // Use trait objects for dynamic dispatch
632        let methods: Vec<Box<dyn CorrelationMethod>> = vec![
633            Box::new(PearsonCorrelation),
634            Box::new(SpearmanCorrelation),
635            Box::new(KendallTau),
636        ];
637
638        for method in &methods {
639            let r = method.compute(&x, &y).unwrap();
640            assert!(
641                (r - 1.0).abs() < 1e-6,
642                "{}: expected ~1.0, got {}",
643                if method.supports_incremental() {
644                    "Pearson"
645                } else {
646                    "other"
647                },
648                r
649            );
650        }
651
652        // Verify supports_incremental flag
653        assert!(PearsonCorrelation.supports_incremental());
654        assert!(!SpearmanCorrelation.supports_incremental());
655        assert!(!KendallTau.supports_incremental());
656    }
657}