vector_ta/indicators/
deviation.rs

1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use serde::{Deserialize, Serialize};
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use wasm_bindgen::prelude::*;
14
15#[cfg(all(feature = "python", feature = "cuda"))]
16use crate::cuda::cuda_available;
17#[cfg(all(feature = "python", feature = "cuda"))]
18use crate::cuda::deviation_wrapper::CudaDeviation;
19use crate::utilities::data_loader::{source_type, Candles};
20#[cfg(all(feature = "python", feature = "cuda"))]
21use crate::utilities::dlpack_cuda::{make_device_array_py, DeviceArrayF32Py};
22use crate::utilities::enums::Kernel;
23use crate::utilities::helpers::{
24    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
25    make_uninit_matrix,
26};
27#[cfg(feature = "python")]
28use crate::utilities::kernel_validation::validate_kernel;
29#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
30use core::arch::x86_64::*;
31#[cfg(not(target_arch = "wasm32"))]
32use rayon::prelude::*;
33use std::convert::AsRef;
34use thiserror::Error;
35
36#[inline(always)]
37fn deviation_auto_kernel() -> Kernel {
38    let k = detect_best_kernel();
39    #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
40    {
41        if k == Kernel::Avx512
42            && std::arch::is_x86_feature_detected!("avx2")
43            && std::arch::is_x86_feature_detected!("fma")
44        {
45            return Kernel::Avx2;
46        }
47    }
48    k
49}
50
51impl<'a> AsRef<[f64]> for DeviationInput<'a> {
52    #[inline(always)]
53    fn as_ref(&self) -> &[f64] {
54        match &self.data {
55            DeviationData::Slice(slice) => slice,
56            DeviationData::Candles { candles, source } => source_type(candles, source),
57        }
58    }
59}
60
61#[derive(Debug, Clone)]
62pub enum DeviationData<'a> {
63    Candles {
64        candles: &'a Candles,
65        source: &'a str,
66    },
67    Slice(&'a [f64]),
68}
69
70#[derive(Debug, Clone)]
71pub struct DeviationInput<'a> {
72    pub data: DeviationData<'a>,
73    pub params: DeviationParams,
74}
75
76impl<'a> DeviationInput<'a> {
77    #[inline]
78    pub fn from_candles(c: &'a Candles, s: &'a str, p: DeviationParams) -> Self {
79        Self {
80            data: DeviationData::Candles {
81                candles: c,
82                source: s,
83            },
84            params: p,
85        }
86    }
87
88    #[inline]
89    pub fn from_slice(data: &'a [f64], params: DeviationParams) -> Self {
90        Self {
91            data: DeviationData::Slice(data),
92            params,
93        }
94    }
95
96    #[inline]
97    pub fn with_defaults(data: &'a [f64]) -> Self {
98        Self {
99            data: DeviationData::Slice(data),
100            params: DeviationParams::default(),
101        }
102    }
103
104    #[inline]
105    pub fn with_default_candles(c: &'a Candles) -> Self {
106        Self::from_candles(c, "close", DeviationParams::default())
107    }
108
109    #[inline]
110    pub fn get_period(&self) -> usize {
111        self.params.period.unwrap_or(9)
112    }
113
114    #[inline]
115    pub fn get_devtype(&self) -> usize {
116        self.params.devtype.unwrap_or(0)
117    }
118
119    #[inline]
120    fn as_slice(&self) -> &[f64] {
121        self.as_ref()
122    }
123}
124
125#[derive(Debug, Clone)]
126pub struct DeviationOutput {
127    pub values: Vec<f64>,
128}
129
130#[derive(Debug, Clone)]
131#[cfg_attr(
132    all(target_arch = "wasm32", feature = "wasm"),
133    derive(Serialize, Deserialize)
134)]
135pub struct DeviationParams {
136    pub period: Option<usize>,
137    pub devtype: Option<usize>,
138}
139impl Default for DeviationParams {
140    fn default() -> Self {
141        Self {
142            period: Some(9),
143            devtype: Some(0),
144        }
145    }
146}
147
148#[derive(Copy, Clone, Debug)]
149pub struct DeviationBuilder {
150    period: Option<usize>,
151    devtype: Option<usize>,
152    kernel: Kernel,
153}
154impl Default for DeviationBuilder {
155    fn default() -> Self {
156        Self {
157            period: None,
158            devtype: None,
159            kernel: Kernel::Auto,
160        }
161    }
162}
163impl DeviationBuilder {
164    #[inline(always)]
165    pub fn new() -> Self {
166        Self::default()
167    }
168    #[inline(always)]
169    pub fn period(mut self, n: usize) -> Self {
170        self.period = Some(n);
171        self
172    }
173    #[inline(always)]
174    pub fn devtype(mut self, d: usize) -> Self {
175        self.devtype = Some(d);
176        self
177    }
178    #[inline(always)]
179    pub fn kernel(mut self, k: Kernel) -> Self {
180        self.kernel = k;
181        self
182    }
183    #[inline(always)]
184    pub fn apply(self, c: &Candles, s: &str) -> Result<DeviationOutput, DeviationError> {
185        let p = DeviationParams {
186            period: self.period,
187            devtype: self.devtype,
188        };
189        let i = DeviationInput::from_candles(c, s, p);
190        deviation_with_kernel(&i, self.kernel)
191    }
192
193    #[inline(always)]
194    pub fn apply_slice(self, d: &[f64]) -> Result<DeviationOutput, DeviationError> {
195        let p = DeviationParams {
196            period: self.period,
197            devtype: self.devtype,
198        };
199        let i = DeviationInput::from_slice(d, p);
200        deviation_with_kernel(&i, self.kernel)
201    }
202    #[inline(always)]
203    pub fn into_stream(self) -> Result<DeviationStream, DeviationError> {
204        let p = DeviationParams {
205            period: self.period,
206            devtype: self.devtype,
207        };
208        DeviationStream::try_new(p)
209    }
210}
211
212#[derive(Debug, Error)]
213pub enum DeviationError {
214    #[error("deviation: empty input data")]
215    EmptyInputData,
216    #[error("deviation: All values are NaN.")]
217    AllValuesNaN,
218    #[error("deviation: Invalid period: period = {period}, data length = {data_len}")]
219    InvalidPeriod { period: usize, data_len: usize },
220    #[error("deviation: Not enough valid data: needed = {needed}, valid = {valid}")]
221    NotEnoughValidData { needed: usize, valid: usize },
222    #[error("deviation: output length mismatch: expected={expected} got={got}")]
223    OutputLengthMismatch { expected: usize, got: usize },
224    #[error("deviation: Invalid devtype (must be 0, 1, or 2). devtype={devtype}")]
225    InvalidDevType { devtype: usize },
226    #[error("deviation: invalid range expansion start={start} end={end} step={step}")]
227    InvalidRange {
228        start: usize,
229        end: usize,
230        step: usize,
231    },
232    #[error("deviation: non-batch kernel passed to batch path: {0:?}")]
233    InvalidKernelForBatch(Kernel),
234    #[error("deviation: Calculation error: {0}")]
235    CalculationError(String),
236}
237
238#[inline(always)]
239pub fn deviation(input: &DeviationInput) -> Result<DeviationOutput, DeviationError> {
240    deviation_with_kernel(input, Kernel::Auto)
241}
242
243#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
244#[inline(always)]
245pub fn deviation_into(input: &DeviationInput, out: &mut [f64]) -> Result<(), DeviationError> {
246    deviation_into_slice(out, input, Kernel::Auto)
247}
248
249#[inline(always)]
250fn deviation_prepare<'a>(
251    input: &'a DeviationInput,
252    kernel: Kernel,
253) -> Result<(&'a [f64], usize, usize, usize, Kernel), DeviationError> {
254    let data = input.as_slice();
255    let len = data.len();
256    if len == 0 {
257        return Err(DeviationError::EmptyInputData);
258    }
259    let first = data
260        .iter()
261        .position(|x| !x.is_nan())
262        .ok_or(DeviationError::AllValuesNaN)?;
263    let period = input.get_period();
264    let devtype = input.get_devtype();
265
266    if period == 0 || period > len {
267        return Err(DeviationError::InvalidPeriod {
268            period,
269            data_len: len,
270        });
271    }
272    if len - first < period {
273        return Err(DeviationError::NotEnoughValidData {
274            needed: period,
275            valid: len - first,
276        });
277    }
278    if !(0..=3).contains(&devtype) {
279        return Err(DeviationError::InvalidDevType { devtype });
280    }
281
282    let chosen = match kernel {
283        Kernel::Auto => deviation_auto_kernel(),
284        k => k,
285    };
286    Ok((data, period, devtype, first, chosen))
287}
288
289#[inline(always)]
290fn deviation_compute_into(
291    data: &[f64],
292    period: usize,
293    devtype: usize,
294    first: usize,
295    kernel: Kernel,
296    out: &mut [f64],
297) -> Result<(), DeviationError> {
298    match kernel {
299        Kernel::Scalar | Kernel::ScalarBatch => match devtype {
300            0 => standard_deviation_rolling_into(data, period, first, out),
301            1 => mean_absolute_deviation_rolling_into(data, period, first, out),
302            2 => median_absolute_deviation_rolling_into(data, period, first, out),
303            3 => mode_deviation_rolling_into(data, period, first, out),
304            _ => unreachable!(),
305        },
306        Kernel::Avx2 | Kernel::Avx2Batch => {
307            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
308            {
309                if devtype == 0 || devtype == 3 {
310                    deviation_avx2(data, period, first, devtype, out);
311                    return Ok(());
312                }
313            }
314            match devtype {
315                0 => standard_deviation_rolling_into(data, period, first, out),
316                1 => mean_absolute_deviation_rolling_into(data, period, first, out),
317                2 => median_absolute_deviation_rolling_into(data, period, first, out),
318                3 => mode_deviation_rolling_into(data, period, first, out),
319                _ => unreachable!(),
320            }
321        }
322        Kernel::Avx512 | Kernel::Avx512Batch => {
323            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
324            {
325                if devtype == 0 || devtype == 3 {
326                    deviation_avx512(data, period, first, devtype, out);
327                    return Ok(());
328                }
329            }
330            match devtype {
331                0 => standard_deviation_rolling_into(data, period, first, out),
332                1 => mean_absolute_deviation_rolling_into(data, period, first, out),
333                2 => median_absolute_deviation_rolling_into(data, period, first, out),
334                3 => mode_deviation_rolling_into(data, period, first, out),
335                _ => unreachable!(),
336            }
337        }
338        Kernel::Auto => match devtype {
339            0 => standard_deviation_rolling_into(data, period, first, out),
340            1 => mean_absolute_deviation_rolling_into(data, period, first, out),
341            2 => median_absolute_deviation_rolling_into(data, period, first, out),
342            3 => mode_deviation_rolling_into(data, period, first, out),
343            _ => unreachable!(),
344        },
345    }
346}
347
348pub fn deviation_with_kernel(
349    input: &DeviationInput,
350    kernel: Kernel,
351) -> Result<DeviationOutput, DeviationError> {
352    let (data, period, devtype, first, chosen) = deviation_prepare(input, kernel)?;
353    let mut out = alloc_with_nan_prefix(data.len(), first + period - 1);
354    deviation_compute_into(data, period, devtype, first, chosen, &mut out)?;
355    Ok(DeviationOutput { values: out })
356}
357
358pub fn deviation_into_slice(
359    dst: &mut [f64],
360    input: &DeviationInput,
361    kernel: Kernel,
362) -> Result<(), DeviationError> {
363    let (data, period, devtype, first, chosen) = deviation_prepare(input, kernel)?;
364    if dst.len() != data.len() {
365        return Err(DeviationError::OutputLengthMismatch {
366            expected: data.len(),
367            got: dst.len(),
368        });
369    }
370    deviation_compute_into(data, period, devtype, first, chosen, dst)?;
371
372    let warm = first + period - 1;
373    for v in &mut dst[..warm] {
374        *v = f64::NAN;
375    }
376    Ok(())
377}
378
379#[inline(always)]
380pub fn deviation_scalar(
381    data: &[f64],
382    period: usize,
383    devtype: usize,
384) -> Result<Vec<f64>, DeviationError> {
385    match devtype {
386        0 => standard_deviation_rolling(data, period)
387            .map_err(|e| DeviationError::CalculationError(e.to_string())),
388        1 => mean_absolute_deviation_rolling(data, period)
389            .map_err(|e| DeviationError::CalculationError(e.to_string())),
390        2 => median_absolute_deviation_rolling(data, period)
391            .map_err(|e| DeviationError::CalculationError(e.to_string())),
392        3 => mode_deviation_rolling(data, period)
393            .map_err(|e| DeviationError::CalculationError(e.to_string())),
394        _ => Err(DeviationError::InvalidDevType { devtype }),
395    }
396}
397
398#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
399#[inline]
400pub fn deviation_avx2(data: &[f64], period: usize, first: usize, devtype: usize, out: &mut [f64]) {
401    if !(devtype == 0 || devtype == 3) {
402        let _ = standard_deviation_rolling_into(data, period, first, out);
403        return;
404    }
405    if period == 0 || data.len() - first < period {
406        let _ = standard_deviation_rolling_into(data, period, first, out);
407        return;
408    }
409    unsafe {
410        use core::arch::x86_64::*;
411        let warm = first + period - 1;
412        let n = period as f64;
413
414        let mut sumv = _mm256_setzero_pd();
415        let mut sqrv = _mm256_setzero_pd();
416        let mut bad = 0usize;
417
418        let zero = _mm256_setzero_pd();
419        let sign_mask = _mm256_set1_pd(-0.0f64);
420        let v_inf = _mm256_set1_pd(f64::INFINITY);
421
422        let mut j = first;
423        let end = first + period;
424        while j + 4 <= end {
425            let x = _mm256_loadu_pd(data.as_ptr().add(j));
426
427            let isnan = _mm256_cmp_pd(x, x, _CMP_NEQ_UQ);
428            let xabs = _mm256_andnot_pd(sign_mask, x);
429            let isinf = _mm256_cmp_pd(xabs, v_inf, _CMP_EQ_OQ);
430            let bad_bits = (_mm256_movemask_pd(isnan) | _mm256_movemask_pd(isinf)) as u32;
431            bad += bad_bits.count_ones() as usize;
432
433            let bad_mask = _mm256_or_pd(isnan, isinf);
434            let good = _mm256_blendv_pd(x, zero, bad_mask);
435            sumv = _mm256_add_pd(sumv, good);
436            sqrv = _mm256_fmadd_pd(good, good, sqrv);
437
438            j += 4;
439        }
440
441        let mut tmp = [0.0f64; 4];
442        _mm256_storeu_pd(tmp.as_mut_ptr(), sumv);
443        let mut sum = tmp.iter().sum::<f64>();
444        _mm256_storeu_pd(tmp.as_mut_ptr(), sqrv);
445        let mut sumsq = tmp.iter().sum::<f64>();
446
447        while j < end {
448            let v = *data.get_unchecked(j);
449            if !v.is_finite() {
450                bad += 1;
451            } else {
452                sum += v;
453                sumsq = v.mul_add(v, sumsq);
454            }
455            j += 1;
456        }
457
458        if bad > 0 || !sum.is_finite() || !sumsq.is_finite() {
459            out[warm] = f64::NAN;
460        } else {
461            let mean = sum / n;
462            let mut var = (sumsq / n) - mean * mean;
463            if var < 0.0 {
464                var = 0.0;
465            }
466            out[warm] = var.sqrt();
467        }
468
469        let mut i = warm + 1;
470        while i < data.len() {
471            let v_in = *data.get_unchecked(i);
472            let v_out = *data.get_unchecked(i - period);
473            if !v_in.is_finite() {
474                bad += 1;
475            } else {
476                sum += v_in;
477                sumsq = v_in.mul_add(v_in, sumsq);
478            }
479            if !v_out.is_finite() {
480                bad = bad.saturating_sub(1);
481            } else {
482                sum -= v_out;
483                sumsq -= v_out * v_out;
484            }
485
486            if bad > 0 || !sum.is_finite() || !sumsq.is_finite() {
487                if bad == 0 {
488                    let start = i + 1 - period;
489                    let mut s = 0.0;
490                    let mut s2 = 0.0;
491                    let mut k = start;
492                    while k <= i {
493                        let v = *data.get_unchecked(k);
494                        s += v;
495                        s2 = v.mul_add(v, s2);
496                        k += 1;
497                    }
498                    if s.is_finite() && s2.is_finite() {
499                        let mean = s / n;
500                        let mut var = (s2 / n) - mean * mean;
501                        if var < 0.0 {
502                            var = 0.0;
503                        }
504                        out[i] = var.sqrt();
505                    } else {
506                        out[i] = f64::NAN;
507                    }
508                } else {
509                    out[i] = f64::NAN;
510                }
511            } else {
512                let mean = sum / n;
513                let mut var = (sumsq / n) - mean * mean;
514                if var < 0.0 {
515                    var = 0.0;
516                }
517                out[i] = var.sqrt();
518            }
519            i += 1;
520        }
521    }
522}
523
524#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
525#[inline]
526pub fn deviation_avx512(
527    data: &[f64],
528    period: usize,
529    first: usize,
530    devtype: usize,
531    out: &mut [f64],
532) {
533    if !(devtype == 0 || devtype == 3) {
534        let _ = standard_deviation_rolling_into(data, period, first, out);
535        return;
536    }
537    if period == 0 || data.len() - first < period {
538        let _ = standard_deviation_rolling_into(data, period, first, out);
539        return;
540    }
541    unsafe {
542        use core::arch::x86_64::*;
543        let warm = first + period - 1;
544        let n = period as f64;
545
546        let mut sumv = _mm512_setzero_pd();
547        let mut sqrv = _mm512_setzero_pd();
548        let mut bad = 0usize;
549
550        let v_inf = _mm512_set1_pd(f64::INFINITY);
551        let sign_mask = _mm512_set1_pd(-0.0f64);
552
553        let mut j = first;
554        let end = first + period;
555        while j + 8 <= end {
556            let x = _mm512_loadu_pd(data.as_ptr().add(j));
557            let xabs = _mm512_andnot_pd(sign_mask, x);
558
559            let mask_nan: u8 = _mm512_cmp_pd_mask(x, x, _CMP_NEQ_UQ);
560            let mask_inf: u8 = _mm512_cmp_pd_mask(xabs, v_inf, _CMP_EQ_OQ);
561            let mask_good: u8 = !(mask_nan | mask_inf);
562
563            bad += (mask_nan | mask_inf).count_ones() as usize;
564
565            let good = _mm512_maskz_mov_pd(mask_good, x);
566            sumv = _mm512_add_pd(sumv, good);
567            sqrv = _mm512_fmadd_pd(good, good, sqrv);
568
569            j += 8;
570        }
571
572        let mut tmp = [0.0f64; 8];
573        _mm512_storeu_pd(tmp.as_mut_ptr(), sumv);
574        let mut sum = tmp.iter().sum::<f64>();
575        _mm512_storeu_pd(tmp.as_mut_ptr(), sqrv);
576        let mut sumsq = tmp.iter().sum::<f64>();
577
578        while j < end {
579            let v = *data.get_unchecked(j);
580            if !v.is_finite() {
581                bad += 1;
582            } else {
583                sum += v;
584                sumsq = v.mul_add(v, sumsq);
585            }
586            j += 1;
587        }
588
589        if bad > 0 || !sum.is_finite() || !sumsq.is_finite() {
590            out[warm] = f64::NAN;
591        } else {
592            let mean = sum / n;
593            let mut var = (sumsq / n) - mean * mean;
594            if var < 0.0 {
595                var = 0.0;
596            }
597            out[warm] = var.sqrt();
598        }
599
600        let mut i = warm + 1;
601        while i < data.len() {
602            let v_in = *data.get_unchecked(i);
603            let v_out = *data.get_unchecked(i - period);
604            if !v_in.is_finite() {
605                bad += 1;
606            } else {
607                sum += v_in;
608                sumsq = v_in.mul_add(v_in, sumsq);
609            }
610            if !v_out.is_finite() {
611                bad = bad.saturating_sub(1);
612            } else {
613                sum -= v_out;
614                sumsq -= v_out * v_out;
615            }
616
617            if bad > 0 || !sum.is_finite() || !sumsq.is_finite() {
618                if bad == 0 {
619                    let start = i + 1 - period;
620                    let mut s = 0.0;
621                    let mut s2 = 0.0;
622                    let mut k = start;
623                    while k <= i {
624                        let v = *data.get_unchecked(k);
625                        s += v;
626                        s2 = v.mul_add(v, s2);
627                        k += 1;
628                    }
629                    if s.is_finite() && s2.is_finite() {
630                        let mean = s / n;
631                        let mut var = (s2 / n) - mean * mean;
632                        if var < 0.0 {
633                            var = 0.0;
634                        }
635                        out[i] = var.sqrt();
636                    } else {
637                        out[i] = f64::NAN;
638                    }
639                } else {
640                    out[i] = f64::NAN;
641                }
642            } else {
643                let mean = sum / n;
644                let mut var = (sumsq / n) - mean * mean;
645                if var < 0.0 {
646                    var = 0.0;
647                }
648                out[i] = var.sqrt();
649            }
650            i += 1;
651        }
652    }
653}
654
655#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
656#[inline]
657pub fn deviation_avx512_short(
658    data: &[f64],
659    period: usize,
660    first: usize,
661    devtype: usize,
662    out: &mut [f64],
663) {
664    deviation_avx512(data, period, first, devtype, out);
665}
666
667#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
668#[inline]
669pub fn deviation_avx512_long(
670    data: &[f64],
671    period: usize,
672    first: usize,
673    devtype: usize,
674    out: &mut [f64],
675) {
676    deviation_avx512(data, period, first, devtype, out);
677}
678
679#[derive(Clone, Debug)]
680pub struct DeviationBatchRange {
681    pub period: (usize, usize, usize),
682    pub devtype: (usize, usize, usize),
683}
684impl Default for DeviationBatchRange {
685    fn default() -> Self {
686        Self {
687            period: (9, 258, 1),
688            devtype: (0, 0, 0),
689        }
690    }
691}
692
693#[derive(Clone, Debug, Default)]
694pub struct DeviationBatchBuilder {
695    range: DeviationBatchRange,
696    kernel: Kernel,
697}
698impl DeviationBatchBuilder {
699    pub fn new() -> Self {
700        Self::default()
701    }
702    pub fn kernel(mut self, k: Kernel) -> Self {
703        self.kernel = k;
704        self
705    }
706    #[inline]
707    pub fn period_range(mut self, start: usize, end: usize, step: usize) -> Self {
708        self.range.period = (start, end, step);
709        self
710    }
711    #[inline]
712    pub fn period_static(mut self, p: usize) -> Self {
713        self.range.period = (p, p, 0);
714        self
715    }
716    #[inline]
717    pub fn devtype_range(mut self, start: usize, end: usize, step: usize) -> Self {
718        self.range.devtype = (start, end, step);
719        self
720    }
721    #[inline]
722    pub fn devtype_static(mut self, d: usize) -> Self {
723        self.range.devtype = (d, d, 0);
724        self
725    }
726    pub fn apply_slice(self, data: &[f64]) -> Result<DeviationBatchOutput, DeviationError> {
727        deviation_batch_with_kernel(data, &self.range, self.kernel)
728    }
729    pub fn with_default_slice(
730        data: &[f64],
731        k: Kernel,
732    ) -> Result<DeviationBatchOutput, DeviationError> {
733        DeviationBatchBuilder::new().kernel(k).apply_slice(data)
734    }
735}
736
737pub fn deviation_batch_with_kernel(
738    data: &[f64],
739    sweep: &DeviationBatchRange,
740    k: Kernel,
741) -> Result<DeviationBatchOutput, DeviationError> {
742    let kernel = match k {
743        Kernel::Auto => detect_best_batch_kernel(),
744        other if other.is_batch() => other,
745        _ => return Err(DeviationError::InvalidKernelForBatch(k)),
746    };
747    let simd = match kernel {
748        Kernel::Avx512Batch => Kernel::Avx512,
749        Kernel::Avx2Batch => Kernel::Avx2,
750        Kernel::ScalarBatch => Kernel::Scalar,
751        _ => unreachable!(),
752    };
753    deviation_batch_par_slice(data, sweep, simd)
754}
755
756#[derive(Clone, Debug)]
757pub struct DeviationBatchOutput {
758    pub values: Vec<f64>,
759    pub combos: Vec<DeviationParams>,
760    pub rows: usize,
761    pub cols: usize,
762}
763impl DeviationBatchOutput {
764    pub fn row_for_params(&self, p: &DeviationParams) -> Option<usize> {
765        self.combos.iter().position(|c| {
766            c.period.unwrap_or(9) == p.period.unwrap_or(9)
767                && c.devtype.unwrap_or(0) == p.devtype.unwrap_or(0)
768        })
769    }
770    pub fn values_for(&self, p: &DeviationParams) -> Option<&[f64]> {
771        self.row_for_params(p).map(|row| {
772            let start = row * self.cols;
773            &self.values[start..start + self.cols]
774        })
775    }
776}
777
778#[inline(always)]
779fn expand_grid(r: &DeviationBatchRange) -> Vec<DeviationParams> {
780    fn expand_axis(start: usize, end: usize, step: usize) -> Option<Vec<usize>> {
781        if step == 0 {
782            return Some(vec![start]);
783        }
784        let mut v = Vec::new();
785        if start <= end {
786            let mut cur = start;
787            while cur <= end {
788                v.push(cur);
789                match cur.checked_add(step) {
790                    Some(n) => {
791                        if n == cur {
792                            break;
793                        }
794                        cur = n;
795                    }
796                    None => break,
797                }
798            }
799        } else {
800            let mut cur = start;
801            loop {
802                v.push(cur);
803                if cur <= end {
804                    break;
805                }
806                match cur.checked_sub(step) {
807                    Some(n) => cur = n,
808                    None => break,
809                }
810                if cur < end {
811                    break;
812                }
813            }
814        }
815        if v.is_empty() {
816            None
817        } else {
818            Some(v)
819        }
820    }
821    let periods = match expand_axis(r.period.0, r.period.1, r.period.2) {
822        Some(v) => v,
823        None => return Vec::new(),
824    };
825    let devtypes = match expand_axis(r.devtype.0, r.devtype.1, r.devtype.2) {
826        Some(v) => v,
827        None => return Vec::new(),
828    };
829    let cap = match periods.len().checked_mul(devtypes.len()) {
830        Some(c) => c,
831        None => 0,
832    };
833    let mut out = Vec::with_capacity(cap);
834    for &p in &periods {
835        for &d in &devtypes {
836            out.push(DeviationParams {
837                period: Some(p),
838                devtype: Some(d),
839            });
840        }
841    }
842    out
843}
844
845#[inline(always)]
846fn deviation_batch_inner_into(
847    data: &[f64],
848    sweep: &DeviationBatchRange,
849    kern: Kernel,
850    parallel: bool,
851    out: &mut [f64],
852) -> Result<Vec<DeviationParams>, DeviationError> {
853    let combos = expand_grid(sweep);
854    if combos.is_empty() {
855        return Err(DeviationError::InvalidRange {
856            start: sweep.period.0,
857            end: sweep.period.1,
858            step: sweep.period.2,
859        });
860    }
861    let first = data
862        .iter()
863        .position(|x| !x.is_nan())
864        .ok_or(DeviationError::AllValuesNaN)?;
865    let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
866    if data.len() - first < max_p {
867        return Err(DeviationError::NotEnoughValidData {
868            needed: max_p,
869            valid: data.len() - first,
870        });
871    }
872
873    let rows = combos.len();
874    let cols = data.len();
875    let expected = rows.checked_mul(cols).ok_or(DeviationError::InvalidRange {
876        start: sweep.period.0,
877        end: sweep.period.1,
878        step: sweep.period.2,
879    })?;
880    if out.len() != expected {
881        return Err(DeviationError::OutputLengthMismatch {
882            expected,
883            got: out.len(),
884        });
885    }
886
887    let out_mu = unsafe {
888        std::slice::from_raw_parts_mut(
889            out.as_mut_ptr() as *mut std::mem::MaybeUninit<f64>,
890            out.len(),
891        )
892    };
893    let warms: Vec<usize> = combos
894        .iter()
895        .map(|c| {
896            let warmup = first + c.period.unwrap() - 1;
897            warmup.min(cols)
898        })
899        .collect();
900    init_matrix_prefixes(out_mu, cols, &warms);
901
902    let mut ps: Vec<f64> = Vec::new();
903    let mut ps2: Vec<f64> = Vec::new();
904    let mut pc: Vec<usize> = Vec::new();
905    if combos
906        .iter()
907        .any(|c| matches!(c.devtype, Some(0) | Some(3)))
908    {
909        ps.resize(cols + 1, 0.0);
910        ps2.resize(cols + 1, 0.0);
911        pc.resize(cols + 1, 0);
912        let mut i = 0;
913        while i < cols {
914            let v = unsafe { *data.get_unchecked(i) };
915            ps[i + 1] = if v.is_finite() { ps[i] + v } else { ps[i] };
916            ps2[i + 1] = if v.is_finite() {
917                v.mul_add(v, ps2[i])
918            } else {
919                ps2[i]
920            };
921            pc[i + 1] = pc[i] + if v.is_finite() { 0 } else { 1 };
922            i += 1;
923        }
924    }
925
926    let do_row = |row: usize, row_mu: &mut [std::mem::MaybeUninit<f64>]| {
927        let period = combos[row].period.unwrap();
928        let devtype = combos[row].devtype.unwrap();
929        let dst = unsafe {
930            std::slice::from_raw_parts_mut(row_mu.as_mut_ptr() as *mut f64, row_mu.len())
931        };
932
933        if (devtype == 0 || devtype == 3) && !ps.is_empty() {
934            let n = period as f64;
935            let warm = first + period - 1;
936
937            let mut i = warm;
938            while i < cols {
939                let l = i + 1 - period;
940                let r = i;
941
942                if pc[r + 1] - pc[l] > 0 {
943                    dst[i] = f64::NAN;
944                } else {
945                    let sum = ps[r + 1] - ps[l];
946                    let sumsq = ps2[r + 1] - ps2[l];
947                    let mean = sum / n;
948                    let mut var = (sumsq / n) - mean * mean;
949                    if var < 0.0 {
950                        var = 0.0;
951                    }
952                    dst[i] = var.sqrt();
953                }
954                i += 1;
955            }
956        } else {
957            let _ = deviation_compute_into(data, period, devtype, first, kern, dst);
958        }
959    };
960
961    if parallel {
962        #[cfg(not(target_arch = "wasm32"))]
963        {
964            use rayon::prelude::*;
965            out_mu
966                .par_chunks_mut(cols)
967                .enumerate()
968                .for_each(|(r, chunk)| do_row(r, chunk));
969        }
970        #[cfg(target_arch = "wasm32")]
971        {
972            for (r, chunk) in out_mu.chunks_mut(cols).enumerate() {
973                do_row(r, chunk);
974            }
975        }
976    } else {
977        for (r, chunk) in out_mu.chunks_mut(cols).enumerate() {
978            do_row(r, chunk);
979        }
980    }
981
982    Ok(combos)
983}
984
985#[inline(always)]
986pub fn deviation_batch_slice(
987    data: &[f64],
988    sweep: &DeviationBatchRange,
989    kern: Kernel,
990) -> Result<DeviationBatchOutput, DeviationError> {
991    deviation_batch_inner(data, sweep, kern, false)
992}
993
994#[inline(always)]
995pub fn deviation_batch_par_slice(
996    data: &[f64],
997    sweep: &DeviationBatchRange,
998    kern: Kernel,
999) -> Result<DeviationBatchOutput, DeviationError> {
1000    deviation_batch_inner(data, sweep, kern, true)
1001}
1002
1003#[inline(always)]
1004fn deviation_batch_inner(
1005    data: &[f64],
1006    sweep: &DeviationBatchRange,
1007    kern: Kernel,
1008    parallel: bool,
1009) -> Result<DeviationBatchOutput, DeviationError> {
1010    let combos = expand_grid(sweep);
1011    if combos.is_empty() {
1012        return Err(DeviationError::InvalidRange {
1013            start: sweep.period.0,
1014            end: sweep.period.1,
1015            step: sweep.period.2,
1016        });
1017    }
1018    let rows = combos.len();
1019    let cols = data.len();
1020    let _expected = rows.checked_mul(cols).ok_or(DeviationError::InvalidRange {
1021        start: sweep.period.0,
1022        end: sweep.period.1,
1023        step: sweep.period.2,
1024    })?;
1025
1026    let mut buf_mu = make_uninit_matrix(rows, cols);
1027    let mut guard = core::mem::ManuallyDrop::new(buf_mu);
1028    let out_f64 =
1029        unsafe { core::slice::from_raw_parts_mut(guard.as_mut_ptr() as *mut f64, guard.len()) };
1030
1031    let _ = deviation_batch_inner_into(data, sweep, kern, parallel, out_f64)?;
1032
1033    let values = unsafe {
1034        Vec::from_raw_parts(
1035            guard.as_mut_ptr() as *mut f64,
1036            guard.len(),
1037            guard.capacity(),
1038        )
1039    };
1040    Ok(DeviationBatchOutput {
1041        values,
1042        combos,
1043        rows,
1044        cols,
1045    })
1046}
1047
1048#[inline]
1049fn standard_deviation_rolling_into(
1050    data: &[f64],
1051    period: usize,
1052    first: usize,
1053    out: &mut [f64],
1054) -> Result<(), DeviationError> {
1055    if period == 1 {
1056        for i in first..data.len() {
1057            out[i] = 0.0;
1058        }
1059        return Ok(());
1060    }
1061    if period < 1 {
1062        return Err(DeviationError::InvalidPeriod {
1063            period,
1064            data_len: data.len(),
1065        });
1066    }
1067    if data.len() - first < period {
1068        return Err(DeviationError::NotEnoughValidData {
1069            needed: period,
1070            valid: data.len() - first,
1071        });
1072    }
1073
1074    let n = period as f64;
1075    let warm = first + period - 1;
1076
1077    let mut sum = 0.0f64;
1078    let mut sumsq = 0.0f64;
1079    let mut bad = 0usize;
1080
1081    let mut j = first;
1082    let end0 = first + period;
1083    while j < end0 {
1084        let v = unsafe { *data.get_unchecked(j) };
1085        if !v.is_finite() {
1086            bad += 1;
1087        } else {
1088            sum += v;
1089            sumsq = v.mul_add(v, sumsq);
1090        }
1091        j += 1;
1092    }
1093
1094    if bad > 0 || !sum.is_finite() || !sumsq.is_finite() {
1095        out[warm] = f64::NAN;
1096    } else {
1097        let mean = sum / n;
1098        let mut var = (sumsq / n) - mean * mean;
1099
1100        let scale = (sumsq / n).abs();
1101        if var.abs() / (scale.max(1e-30)) < 1e-10 {
1102            let start = warm + 1 - period;
1103            let mut v2 = 0.0;
1104            let mut k = start;
1105            while k <= warm {
1106                let x = unsafe { *data.get_unchecked(k) };
1107                let d = x - mean;
1108                v2 = d.mul_add(d, v2);
1109                k += 1;
1110            }
1111            var = v2 / n;
1112        }
1113        if var < 0.0 {
1114            var = 0.0;
1115        }
1116        out[warm] = var.sqrt();
1117    }
1118
1119    let mut i = warm + 1;
1120    while i < data.len() {
1121        let v_in = unsafe { *data.get_unchecked(i) };
1122        let v_out = unsafe { *data.get_unchecked(i - period) };
1123
1124        if !v_in.is_finite() {
1125            bad += 1;
1126        } else {
1127            sum += v_in;
1128            sumsq = v_in.mul_add(v_in, sumsq);
1129        }
1130        if !v_out.is_finite() {
1131            bad = bad.saturating_sub(1);
1132        } else {
1133            sum -= v_out;
1134            sumsq -= v_out * v_out;
1135        }
1136
1137        if bad > 0 || !sum.is_finite() || !sumsq.is_finite() {
1138            if bad == 0 {
1139                let start = i + 1 - period;
1140                let mut s = 0.0;
1141                let mut s2 = 0.0;
1142                let mut k = start;
1143                while k <= i {
1144                    let v = unsafe { *data.get_unchecked(k) };
1145                    s += v;
1146                    s2 = v.mul_add(v, s2);
1147                    k += 1;
1148                }
1149                if s.is_finite() && s2.is_finite() {
1150                    let mean = s / n;
1151                    let mut var = (s2 / n) - mean * mean;
1152
1153                    let scale = (s2 / n).abs();
1154                    if var.abs() / (scale.max(1e-30)) < 1e-10 {
1155                        let mut v2 = 0.0;
1156                        let mut k = start;
1157                        while k <= i {
1158                            let x = unsafe { *data.get_unchecked(k) };
1159                            let d = x - mean;
1160                            v2 = d.mul_add(d, v2);
1161                            k += 1;
1162                        }
1163                        var = v2 / n;
1164                    }
1165                    if var < 0.0 {
1166                        var = 0.0;
1167                    }
1168                    out[i] = var.sqrt();
1169                } else {
1170                    out[i] = f64::NAN;
1171                }
1172            } else {
1173                out[i] = f64::NAN;
1174            }
1175        } else {
1176            let mean = sum / n;
1177            let mut var = (sumsq / n) - mean * mean;
1178            let scale = (sumsq / n).abs();
1179            if var.abs() / (scale.max(1e-30)) < 1e-10 {
1180                let start = i + 1 - period;
1181                let mut v2 = 0.0;
1182                let mut k = start;
1183                while k <= i {
1184                    let x = unsafe { *data.get_unchecked(k) };
1185                    let d = x - mean;
1186                    v2 = d.mul_add(d, v2);
1187                    k += 1;
1188                }
1189                var = v2 / n;
1190            }
1191            if var < 0.0 {
1192                var = 0.0;
1193            }
1194            out[i] = var.sqrt();
1195        }
1196        i += 1;
1197    }
1198    Ok(())
1199}
1200
1201#[inline]
1202fn standard_deviation_rolling(
1203    data: &[f64],
1204    period: usize,
1205) -> Result<Vec<f64>, Box<dyn std::error::Error>> {
1206    if period < 2 {
1207        return Err("Period must be >= 2 for standard deviation.".into());
1208    }
1209    let first_valid_idx = match data.iter().position(|&x| !x.is_nan()) {
1210        Some(idx) => idx,
1211        None => return Err("All values are NaN.".into()),
1212    };
1213    if data.len() - first_valid_idx < period {
1214        return Err(format!(
1215            "Not enough valid data: need {}, but only {} valid from index {}.",
1216            period,
1217            data.len() - first_valid_idx,
1218            first_valid_idx
1219        )
1220        .into());
1221    }
1222    let mut result = alloc_with_nan_prefix(data.len(), first_valid_idx + period - 1);
1223
1224    let mut sum = 0.0;
1225    let mut sumsq = 0.0;
1226    let mut has_nan = false;
1227
1228    for &val in &data[first_valid_idx..(first_valid_idx + period)] {
1229        if val.is_nan() {
1230            has_nan = true;
1231        }
1232        sum += val;
1233        sumsq += val * val;
1234    }
1235
1236    let idx = first_valid_idx + period - 1;
1237    if has_nan {
1238        result[idx] = f64::NAN;
1239    } else {
1240        let mean = sum / (period as f64);
1241        let var = (sumsq / (period as f64)) - mean * mean;
1242        result[idx] = var.sqrt();
1243    }
1244
1245    for i in (idx + 1)..data.len() {
1246        let val_in = data[i];
1247        let val_out = data[i - period];
1248
1249        let window_start = i + 1 - period;
1250        has_nan = data[window_start..=i].iter().any(|&x| x.is_nan());
1251
1252        if has_nan {
1253            result[i] = f64::NAN;
1254        } else {
1255            sum += val_in - val_out;
1256            sumsq += val_in * val_in - val_out * val_out;
1257            let mean = sum / (period as f64);
1258            let var = (sumsq / (period as f64)) - mean * mean;
1259            result[i] = var.sqrt();
1260        }
1261    }
1262    Ok(result)
1263}
1264
1265#[inline]
1266fn mean_absolute_deviation_rolling_into(
1267    data: &[f64],
1268    period: usize,
1269    first: usize,
1270    out: &mut [f64],
1271) -> Result<(), DeviationError> {
1272    if data.len() - first < period {
1273        return Err(DeviationError::NotEnoughValidData {
1274            needed: period,
1275            valid: data.len() - first,
1276        });
1277    }
1278
1279    let n = period as f64;
1280    let warm = first + period - 1;
1281
1282    let mut sum = 0.0f64;
1283    let mut bad = 0usize;
1284
1285    let mut j = first;
1286    let end0 = first + period;
1287    while j < end0 {
1288        let v = unsafe { *data.get_unchecked(j) };
1289        if !v.is_finite() {
1290            bad += 1;
1291        } else {
1292            sum += v;
1293        }
1294        j += 1;
1295    }
1296
1297    if bad > 0 {
1298        out[warm] = f64::NAN;
1299    } else {
1300        let start = warm + 1 - period;
1301        let a = unsafe { *data.get_unchecked(start) };
1302        let mut res = 0.0f64;
1303        let mut k = start + 1;
1304        while k <= warm {
1305            res += unsafe { *data.get_unchecked(k) } - a;
1306            k += 1;
1307        }
1308        let mean = a + res / n;
1309
1310        let mut abs_sum = 0.0f64;
1311        let mut k2 = start;
1312        let stop = k2 + (period & !3);
1313        while k2 < stop {
1314            let a0 = unsafe { *data.get_unchecked(k2) };
1315            let a1 = unsafe { *data.get_unchecked(k2 + 1) };
1316            let a2 = unsafe { *data.get_unchecked(k2 + 2) };
1317            let a3 = unsafe { *data.get_unchecked(k2 + 3) };
1318            abs_sum += (a0 - mean).abs();
1319            abs_sum += (a1 - mean).abs();
1320            abs_sum += (a2 - mean).abs();
1321            abs_sum += (a3 - mean).abs();
1322            k2 += 4;
1323        }
1324        while k2 <= warm {
1325            let a = unsafe { *data.get_unchecked(k2) };
1326            abs_sum += (a - mean).abs();
1327            k2 += 1;
1328        }
1329        out[warm] = abs_sum / n;
1330    }
1331
1332    let mut i = warm + 1;
1333    while i < data.len() {
1334        let v_in = unsafe { *data.get_unchecked(i) };
1335        let v_out = unsafe { *data.get_unchecked(i - period) };
1336
1337        if !v_in.is_finite() {
1338            bad += 1;
1339        } else {
1340            sum += v_in;
1341        }
1342        if !v_out.is_finite() {
1343            bad = bad.saturating_sub(1);
1344        } else {
1345            sum -= v_out;
1346        }
1347
1348        if bad > 0 {
1349            out[i] = f64::NAN;
1350        } else {
1351            let start = i + 1 - period;
1352            let mean = if sum.is_finite() {
1353                sum / n
1354            } else {
1355                let a0 = unsafe { *data.get_unchecked(start) };
1356                let mut res = 0.0f64;
1357                let mut k = start + 1;
1358                while k <= i {
1359                    res += unsafe { *data.get_unchecked(k) } - a0;
1360                    k += 1;
1361                }
1362                a0 + res / n
1363            };
1364            let mut k = start;
1365            let mut abs_sum = 0.0f64;
1366            let stop = k + (period & !3);
1367            while k < stop {
1368                let a0 = unsafe { *data.get_unchecked(k) };
1369                let a1 = unsafe { *data.get_unchecked(k + 1) };
1370                let a2 = unsafe { *data.get_unchecked(k + 2) };
1371                let a3 = unsafe { *data.get_unchecked(k + 3) };
1372                abs_sum += (a0 - mean).abs();
1373                abs_sum += (a1 - mean).abs();
1374                abs_sum += (a2 - mean).abs();
1375                abs_sum += (a3 - mean).abs();
1376                k += 4;
1377            }
1378            while k <= i {
1379                let a = unsafe { *data.get_unchecked(k) };
1380                abs_sum += (a - mean).abs();
1381                k += 1;
1382            }
1383            out[i] = abs_sum / n;
1384        }
1385        i += 1;
1386    }
1387    Ok(())
1388}
1389
1390#[inline]
1391fn mean_absolute_deviation_rolling(
1392    data: &[f64],
1393    period: usize,
1394) -> Result<Vec<f64>, Box<dyn std::error::Error>> {
1395    let first_valid_idx = match data.iter().position(|&x| !x.is_nan()) {
1396        Some(idx) => idx,
1397        None => return Err("All values are NaN.".into()),
1398    };
1399    if data.len() - first_valid_idx < period {
1400        return Err(format!(
1401            "Not enough valid data: need {}, but only {} valid from index {}.",
1402            period,
1403            data.len() - first_valid_idx,
1404            first_valid_idx
1405        )
1406        .into());
1407    }
1408    let mut result = alloc_with_nan_prefix(data.len(), first_valid_idx + period - 1);
1409    let start_window_end = first_valid_idx + period - 1;
1410    for i in start_window_end..data.len() {
1411        let window_start = i + 1 - period;
1412        if window_start < first_valid_idx {
1413            continue;
1414        }
1415        let window = &data[window_start..=i];
1416
1417        if window.iter().any(|&x| x.is_nan()) {
1418            result[i] = f64::NAN;
1419        } else {
1420            let mean = window.iter().sum::<f64>() / (period as f64);
1421            let abs_sum = window.iter().fold(0.0, |acc, &x| acc + (x - mean).abs());
1422            result[i] = abs_sum / (period as f64);
1423        }
1424    }
1425    Ok(result)
1426}
1427
1428#[inline]
1429fn median_absolute_deviation_rolling_into(
1430    data: &[f64],
1431    period: usize,
1432    first: usize,
1433    out: &mut [f64],
1434) -> Result<(), DeviationError> {
1435    if data.len() - first < period {
1436        return Err(DeviationError::NotEnoughValidData {
1437            needed: period,
1438            valid: data.len() - first,
1439        });
1440    }
1441
1442    const STACK_SIZE: usize = 256;
1443    let mut stack: [f64; STACK_SIZE] = [0.0; STACK_SIZE];
1444    let mut heap: Vec<f64> = if period > STACK_SIZE {
1445        vec![0.0; period]
1446    } else {
1447        Vec::new()
1448    };
1449
1450    let warm = first + period - 1;
1451    let mut bad = 0usize;
1452
1453    #[inline(always)]
1454    fn median_in_place(buf: &mut [f64]) -> f64 {
1455        let len = buf.len();
1456        let mid = len >> 1;
1457        if (len & 1) == 1 {
1458            let (_, m, _) = buf.select_nth_unstable_by(mid, |a, b| a.partial_cmp(b).unwrap());
1459            *m
1460        } else {
1461            let (left, m, _right) =
1462                buf.select_nth_unstable_by(mid, |a, b| a.partial_cmp(b).unwrap());
1463            let hi = *m;
1464            let mut lo = left[0];
1465            for &v in &left[1..] {
1466                if v > lo {
1467                    lo = v;
1468                }
1469            }
1470            0.5 * (lo + hi)
1471        }
1472    }
1473
1474    {
1475        let start = warm + 1 - period;
1476        let mut j = start;
1477        while j <= warm {
1478            if !unsafe { *data.get_unchecked(j) }.is_finite() {
1479                bad += 1;
1480            }
1481            j += 1;
1482        }
1483        if bad > 0 {
1484            out[warm] = f64::NAN;
1485        } else {
1486            let buf = if period <= STACK_SIZE {
1487                let tmp = &mut stack[..period];
1488                let mut k = 0;
1489                while k < period {
1490                    unsafe { *tmp.get_unchecked_mut(k) = *data.get_unchecked(start + k) };
1491                    k += 1;
1492                }
1493                tmp
1494            } else {
1495                let tmp = &mut heap[..period];
1496                tmp.copy_from_slice(&data[start..=warm]);
1497                tmp
1498            };
1499
1500            let med = median_in_place(buf);
1501            let mut k = 0;
1502            while k < period {
1503                unsafe {
1504                    let x = *buf.get_unchecked(k);
1505                    *buf.get_unchecked_mut(k) = (x - med).abs();
1506                }
1507                k += 1;
1508            }
1509            out[warm] = median_in_place(buf);
1510        }
1511    }
1512
1513    let mut i = warm + 1;
1514    while i < data.len() {
1515        let v_in = unsafe { *data.get_unchecked(i) };
1516        let v_out = unsafe { *data.get_unchecked(i - period) };
1517        if !v_in.is_finite() {
1518            bad += 1;
1519        }
1520        if !v_out.is_finite() {
1521            bad = bad.saturating_sub(1);
1522        }
1523
1524        if bad > 0 {
1525            out[i] = f64::NAN;
1526        } else {
1527            let start = i + 1 - period;
1528            let buf = if period <= STACK_SIZE {
1529                let tmp = &mut stack[..period];
1530                let mut k = 0;
1531                while k < period {
1532                    unsafe { *tmp.get_unchecked_mut(k) = *data.get_unchecked(start + k) };
1533                    k += 1;
1534                }
1535                tmp
1536            } else {
1537                let tmp = &mut heap[..period];
1538                tmp.copy_from_slice(&data[start..=i]);
1539                tmp
1540            };
1541
1542            let med = median_in_place(buf);
1543            let mut k = 0;
1544            while k < period {
1545                unsafe {
1546                    let x = *buf.get_unchecked(k);
1547                    *buf.get_unchecked_mut(k) = (x - med).abs();
1548                }
1549                k += 1;
1550            }
1551            out[i] = median_in_place(buf);
1552        }
1553        i += 1;
1554    }
1555
1556    Ok(())
1557}
1558
1559#[inline]
1560fn median_absolute_deviation_rolling(
1561    data: &[f64],
1562    period: usize,
1563) -> Result<Vec<f64>, Box<dyn std::error::Error>> {
1564    let first_valid_idx = match data.iter().position(|&x| !x.is_nan()) {
1565        Some(idx) => idx,
1566        None => return Err("All values are NaN.".into()),
1567    };
1568    if data.len() - first_valid_idx < period {
1569        return Err(format!(
1570            "Not enough valid data: need {}, but only {} valid from index {}.",
1571            period,
1572            data.len() - first_valid_idx,
1573            first_valid_idx
1574        )
1575        .into());
1576    }
1577    let mut result = alloc_with_nan_prefix(data.len(), first_valid_idx + period - 1);
1578    let start_window_end = first_valid_idx + period - 1;
1579
1580    const STACK_SIZE: usize = 256;
1581    let mut stack_buffer: [f64; STACK_SIZE] = [0.0; STACK_SIZE];
1582    let mut heap_buffer: Vec<f64> = if period > STACK_SIZE {
1583        vec![0.0; period]
1584    } else {
1585        Vec::new()
1586    };
1587
1588    for i in start_window_end..data.len() {
1589        let window_start = i + 1 - period;
1590        if window_start < first_valid_idx {
1591            continue;
1592        }
1593        let window = &data[window_start..=i];
1594
1595        if window.iter().any(|&x| x.is_nan()) {
1596            result[i] = f64::NAN;
1597        } else {
1598            let median = find_median(window);
1599
1600            let abs_devs = if period <= STACK_SIZE {
1601                &mut stack_buffer[..period]
1602            } else {
1603                &mut heap_buffer[..period]
1604            };
1605
1606            for (j, &x) in window.iter().enumerate() {
1607                abs_devs[j] = (x - median).abs();
1608            }
1609
1610            result[i] = find_median(abs_devs);
1611        }
1612    }
1613    Ok(result)
1614}
1615
1616#[inline]
1617fn mode_deviation_rolling_into(
1618    data: &[f64],
1619    period: usize,
1620    first: usize,
1621    out: &mut [f64],
1622) -> Result<(), DeviationError> {
1623    standard_deviation_rolling_into(data, period, first, out)
1624}
1625
1626#[inline]
1627fn mode_deviation_rolling(
1628    data: &[f64],
1629    period: usize,
1630) -> Result<Vec<f64>, Box<dyn std::error::Error>> {
1631    standard_deviation_rolling(data, period)
1632}
1633
1634#[inline]
1635fn find_median(slice: &[f64]) -> f64 {
1636    if slice.is_empty() {
1637        return f64::NAN;
1638    }
1639
1640    const STACK_SIZE: usize = 256;
1641
1642    if slice.len() <= STACK_SIZE {
1643        let mut buf: [f64; STACK_SIZE] = [0.0; STACK_SIZE];
1644        let n = slice.len();
1645        buf[..n].copy_from_slice(slice);
1646        let b = &mut buf[..n];
1647
1648        let mid = n >> 1;
1649        if (n & 1) == 1 {
1650            let (_, m, _) = b.select_nth_unstable_by(mid, |a, b| a.partial_cmp(b).unwrap());
1651            *m
1652        } else {
1653            let (left, m, _right) = b.select_nth_unstable_by(mid, |a, b| a.partial_cmp(b).unwrap());
1654            let hi = *m;
1655            let mut lo = left[0];
1656            for &v in &left[1..] {
1657                if v > lo {
1658                    lo = v;
1659                }
1660            }
1661            0.5 * (lo + hi)
1662        }
1663    } else {
1664        let mut v = slice.to_vec();
1665        let n = v.len();
1666        let mid = n >> 1;
1667        if (n & 1) == 1 {
1668            let (_, m, _) = v.select_nth_unstable_by(mid, |a, b| a.partial_cmp(b).unwrap());
1669            *m
1670        } else {
1671            let (left, m, _right) = v.select_nth_unstable_by(mid, |a, b| a.partial_cmp(b).unwrap());
1672            let hi = *m;
1673            let mut lo = left[0];
1674            for &x in &left[1..] {
1675                if x > lo {
1676                    lo = x;
1677                }
1678            }
1679            0.5 * (lo + hi)
1680        }
1681    }
1682}
1683
1684#[derive(Debug, Clone)]
1685#[cfg_attr(all(target_arch = "wasm32", feature = "wasm"), wasm_bindgen)]
1686pub struct DeviationStream {
1687    period: usize,
1688    devtype: usize,
1689    buffer: Vec<f64>,
1690    head: usize,
1691    filled: bool,
1692
1693    sum: f64,
1694    sum_sq: f64,
1695    count: usize,
1696
1697    inv_n: f64,
1698
1699    tree: OstTreap,
1700}
1701
1702impl DeviationStream {
1703    pub fn try_new(params: DeviationParams) -> Result<Self, DeviationError> {
1704        let period = params.period.unwrap_or(9);
1705        if period == 0 {
1706            return Err(DeviationError::InvalidPeriod {
1707                period,
1708                data_len: 0,
1709            });
1710        }
1711        let devtype = params.devtype.unwrap_or(0);
1712        if !(0..=3).contains(&devtype) {
1713            return Err(DeviationError::InvalidDevType { devtype });
1714        }
1715        Ok(Self {
1716            period,
1717            devtype,
1718            buffer: vec![f64::NAN; period],
1719            head: 0,
1720            filled: false,
1721            sum: 0.0,
1722            sum_sq: 0.0,
1723            count: 0,
1724            inv_n: 1.0 / (period as f64),
1725            tree: OstTreap::new(),
1726        })
1727    }
1728
1729    #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1730    #[inline(always)]
1731    pub fn update(&mut self, value: f64) -> Option<f64> {
1732        self.update_impl(value)
1733    }
1734
1735    #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1736    #[inline(always)]
1737    fn std_dev_ring_o1(&self) -> f64 {
1738        if self.count == 0 {
1739            return f64::NAN;
1740        }
1741
1742        if self.period == 1 {
1743            return 0.0;
1744        }
1745
1746        if self.count < self.period {
1747            return f64::NAN;
1748        }
1749
1750        let mean = self.sum * self.inv_n;
1751        let var = (self.sum_sq * self.inv_n) - mean * mean;
1752        var.sqrt()
1753    }
1754
1755    #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1756    #[inline(always)]
1757    fn mean_abs_dev_ring(&self) -> f64 {
1758        if self.buffer.iter().any(|&x| !x.is_finite()) {
1759            return f64::NAN;
1760        }
1761
1762        let n = self.period as f64;
1763        let sum: f64 = self.buffer.iter().sum();
1764        let mean = sum / n;
1765        if !mean.is_finite() {
1766            return f64::NAN;
1767        }
1768        let abs_sum = self
1769            .buffer
1770            .iter()
1771            .fold(0.0, |acc, &x| acc + (x - mean).abs());
1772        abs_sum / n
1773    }
1774
1775    #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1776    #[inline(always)]
1777    fn median_abs_dev_ring(&self) -> f64 {
1778        if self.buffer.iter().any(|&x| x.is_nan()) {
1779            return f64::NAN;
1780        }
1781
1782        let median = find_median(&self.buffer);
1783        let mut abs_devs: Vec<f64> = self.buffer.iter().map(|&x| (x - median).abs()).collect();
1784        find_median(&abs_devs)
1785    }
1786}
1787
1788impl DeviationStream {
1789    #[inline(always)]
1790    fn push_pop(&mut self, value: f64) {
1791        let out = self.buffer[self.head];
1792        if out.is_finite() {
1793            self.sum -= out;
1794            self.sum_sq -= out * out;
1795            self.count -= 1;
1796            self.tree.erase(out);
1797        }
1798        self.buffer[self.head] = value;
1799        if value.is_finite() {
1800            self.sum += value;
1801            self.sum_sq = value.mul_add(value, self.sum_sq);
1802            self.count += 1;
1803            self.tree.insert(value);
1804        }
1805        self.head += 1;
1806        if self.head == self.period {
1807            self.head = 0;
1808            if !self.filled {
1809                self.filled = true;
1810            }
1811        }
1812    }
1813
1814    #[inline(always)]
1815    fn stddev_o1(&self) -> f64 {
1816        if !self.filled || self.count < self.period {
1817            return f64::NAN;
1818        }
1819        if self.period == 1 {
1820            return 0.0;
1821        }
1822        let mean = self.sum * self.inv_n;
1823        let mut var = (self.sum_sq * self.inv_n) - mean * mean;
1824        if var < 0.0 {
1825            var = 0.0;
1826        }
1827        sqrt_fast(var)
1828    }
1829
1830    #[inline(always)]
1831    fn mad_log_n(&self) -> f64 {
1832        if !self.filled || self.count < self.period {
1833            return f64::NAN;
1834        }
1835        let n = self.period as i64;
1836        let m = self.sum * self.inv_n;
1837        let k_le = self.tree.count_leq(m) as i64;
1838        let s_le = self.tree.sum_leq(m);
1839        let s_all = self.tree.sum_all();
1840        let abs_sum = m * ((2 * k_le - n) as f64) + (s_all - 2.0 * s_le);
1841        abs_sum * self.inv_n
1842    }
1843
1844    #[inline(always)]
1845    fn medad_log_n(&self) -> f64 {
1846        if !self.filled || self.count < self.period {
1847            return f64::NAN;
1848        }
1849        let n = self.period;
1850        let med = if (n & 1) == 1 {
1851            self.tree.kth((n / 2 + 1) as u32)
1852        } else {
1853            let a = self.tree.kth((n / 2) as u32);
1854            let b = self.tree.kth((n / 2 + 1) as u32);
1855            0.5 * (a + b)
1856        };
1857        if (n & 1) == 1 {
1858            self.kth_abs_distance(med, (n / 2 + 1) as u32)
1859        } else {
1860            let d1 = self.kth_abs_distance(med, (n / 2) as u32);
1861            let d2 = self.kth_abs_distance(med, (n / 2 + 1) as u32);
1862            0.5 * (d1 + d2)
1863        }
1864    }
1865
1866    #[inline(always)]
1867    fn kth_abs_distance(&self, m: f64, k: u32) -> f64 {
1868        let n = self.period as u32;
1869        let n_l = self.tree.count_lt(m) as u32;
1870        let n_r = n - n_l;
1871        let left_at = |idx: u32| -> f64 {
1872            let rank = n_l - idx;
1873            let x = self.tree.kth(rank);
1874            m - x
1875        };
1876        let right_at = |idx: u32| -> f64 {
1877            let rank = n_l + 1 + idx;
1878            let x = self.tree.kth(rank);
1879            x - m
1880        };
1881        let mut lo = k.saturating_sub(n_r);
1882        let mut hi = k.min(n_l);
1883        while lo <= hi {
1884            let i = (lo + hi) >> 1;
1885            let j = k - i;
1886            let a_left = if i == 0 {
1887                f64::NEG_INFINITY
1888            } else {
1889                left_at(i - 1)
1890            };
1891            let a_right = if i == n_l { f64::INFINITY } else { left_at(i) };
1892            let b_left = if j == 0 {
1893                f64::NEG_INFINITY
1894            } else {
1895                right_at(j - 1)
1896            };
1897            let b_right = if j == n_r { f64::INFINITY } else { right_at(j) };
1898            if a_left <= b_right && b_left <= a_right {
1899                return a_left.max(b_left);
1900            } else if a_left > b_right {
1901                hi = i - 1;
1902            } else {
1903                lo = i + 1;
1904            }
1905        }
1906        0.0
1907    }
1908
1909    #[inline(always)]
1910    fn update_impl(&mut self, value: f64) -> Option<f64> {
1911        self.push_pop(value);
1912        if !self.filled {
1913            return None;
1914        }
1915        Some(match self.devtype {
1916            0 => self.stddev_o1(),
1917            1 => self.mad_log_n(),
1918            2 => self.medad_log_n(),
1919            3 => self.stddev_o1(),
1920            _ => f64::NAN,
1921        })
1922    }
1923}
1924
1925#[inline(always)]
1926fn norm(x: f64) -> f64 {
1927    if x == 0.0 {
1928        0.0
1929    } else {
1930        x
1931    }
1932}
1933
1934#[inline(always)]
1935fn sqrt_fast(x: f64) -> f64 {
1936    x.sqrt()
1937}
1938
1939#[derive(Debug, Clone, Default)]
1940struct OstTreap {
1941    root: Link,
1942    rng: u64,
1943}
1944type Link = Option<Box<Node>>;
1945#[derive(Debug, Clone)]
1946struct Node {
1947    key: f64,
1948    pri: u64,
1949    cnt: u32,
1950    size: u32,
1951    sum: f64,
1952    l: Link,
1953    r: Link,
1954}
1955
1956impl OstTreap {
1957    #[inline(always)]
1958    fn new() -> Self {
1959        Self {
1960            root: None,
1961            rng: 0x9E3779B97F4A7C15,
1962        }
1963    }
1964
1965    #[inline(always)]
1966    fn insert(&mut self, x: f64) {
1967        debug_assert!(x.is_finite());
1968        self.root = Self::ins(self.root.take(), norm(x), self.next());
1969    }
1970    #[inline(always)]
1971    fn erase(&mut self, x: f64) {
1972        debug_assert!(x.is_finite());
1973        self.root = Self::del(self.root.take(), norm(x));
1974    }
1975    #[inline(always)]
1976    fn count_lt(&self, x: f64) -> usize {
1977        Self::cnt_lt(&self.root, x) as usize
1978    }
1979    #[inline(always)]
1980    fn count_leq(&self, x: f64) -> usize {
1981        Self::cnt_leq(&self.root, x) as usize
1982    }
1983    #[inline(always)]
1984    fn sum_leq(&self, x: f64) -> f64 {
1985        Self::sum_leq_impl(&self.root, x)
1986    }
1987    #[inline(always)]
1988    fn sum_all(&self) -> f64 {
1989        Self::sum(&self.root)
1990    }
1991    #[inline(always)]
1992    fn kth(&self, k: u32) -> f64 {
1993        debug_assert!(k >= 1 && k <= Self::sz(&self.root));
1994        Self::kth_impl(&self.root, k)
1995    }
1996
1997    #[inline(always)]
1998    fn next(&mut self) -> u64 {
1999        let mut x = self.rng;
2000        x ^= x << 13;
2001        x ^= x >> 7;
2002        x ^= x << 17;
2003        self.rng = x;
2004        x
2005    }
2006    #[inline(always)]
2007    fn sz(n: &Link) -> u32 {
2008        n.as_ref().map(|p| p.size).unwrap_or(0)
2009    }
2010    #[inline(always)]
2011    fn sum(n: &Link) -> f64 {
2012        n.as_ref().map(|p| p.sum).unwrap_or(0.0)
2013    }
2014    #[inline(always)]
2015    fn pull(n: &mut Box<Node>) {
2016        n.size = n.cnt + Self::sz(&n.l) + Self::sz(&n.r);
2017        n.sum = n.cnt as f64 * n.key + Self::sum(&n.l) + Self::sum(&n.r);
2018    }
2019
2020    #[inline(always)]
2021    fn rot_right(mut y: Box<Node>) -> Box<Node> {
2022        let mut x = y.l.take().expect("rotate right");
2023        y.l = x.r.take();
2024        Self::pull(&mut y);
2025        x.r = Some(y);
2026        Self::pull(&mut x);
2027        x
2028    }
2029    #[inline(always)]
2030    fn rot_left(mut x: Box<Node>) -> Box<Node> {
2031        let mut y = x.r.take().expect("rotate left");
2032        x.r = y.l.take();
2033        Self::pull(&mut x);
2034        y.l = Some(x);
2035        Self::pull(&mut y);
2036        y
2037    }
2038
2039    fn ins(t: Link, key: f64, pri: u64) -> Link {
2040        match t {
2041            None => Some(Box::new(Node {
2042                key,
2043                pri,
2044                cnt: 1,
2045                size: 1,
2046                sum: key,
2047                l: None,
2048                r: None,
2049            })),
2050            Some(mut n) => match key.total_cmp(&n.key) {
2051                core::cmp::Ordering::Equal => {
2052                    n.cnt += 1;
2053                    n.size += 1;
2054                    n.sum += key;
2055                    Some(n)
2056                }
2057                core::cmp::Ordering::Less => {
2058                    n.l = Self::ins(n.l.take(), key, pri);
2059                    if n.l.as_ref().unwrap().pri > n.pri {
2060                        n = Self::rot_right(n);
2061                    } else {
2062                        Self::pull(&mut n);
2063                    }
2064                    Some(n)
2065                }
2066                core::cmp::Ordering::Greater => {
2067                    n.r = Self::ins(n.r.take(), key, pri);
2068                    if n.r.as_ref().unwrap().pri > n.pri {
2069                        n = Self::rot_left(n);
2070                    } else {
2071                        Self::pull(&mut n);
2072                    }
2073                    Some(n)
2074                }
2075            },
2076        }
2077    }
2078
2079    fn del(t: Link, key: f64) -> Link {
2080        match t {
2081            None => None,
2082            Some(mut n) => match key.total_cmp(&n.key) {
2083                core::cmp::Ordering::Equal => {
2084                    if n.cnt > 1 {
2085                        n.cnt -= 1;
2086                        n.size -= 1;
2087                        n.sum -= key;
2088                        Some(n)
2089                    } else {
2090                        match (n.l.take(), n.r.take()) {
2091                            (None, None) => None,
2092                            (Some(l), None) => Some(l),
2093                            (None, Some(r)) => Some(r),
2094                            (Some(l), Some(r)) => {
2095                                if l.pri > r.pri {
2096                                    let mut new = Self::rot_right(Box::new(Node {
2097                                        l: Some(l),
2098                                        r: Some(r),
2099                                        ..*n
2100                                    }));
2101                                    new.r = Self::del(new.r.take(), key);
2102                                    Self::pull(&mut new);
2103                                    Some(new)
2104                                } else {
2105                                    let mut new = Self::rot_left(Box::new(Node {
2106                                        l: Some(l),
2107                                        r: Some(r),
2108                                        ..*n
2109                                    }));
2110                                    new.l = Self::del(new.l.take(), key);
2111                                    Self::pull(&mut new);
2112                                    Some(new)
2113                                }
2114                            }
2115                        }
2116                    }
2117                }
2118                core::cmp::Ordering::Less => {
2119                    n.l = Self::del(n.l.take(), key);
2120                    Self::pull(&mut n);
2121                    Some(n)
2122                }
2123                core::cmp::Ordering::Greater => {
2124                    n.r = Self::del(n.r.take(), key);
2125                    Self::pull(&mut n);
2126                    Some(n)
2127                }
2128            },
2129        }
2130    }
2131
2132    fn kth_impl(t: &Link, mut k: u32) -> f64 {
2133        let n = t.as_ref().unwrap();
2134        let ls = Self::sz(&n.l);
2135        if k <= ls {
2136            return Self::kth_impl(&n.l, k);
2137        }
2138        k -= ls;
2139        if k <= n.cnt {
2140            return n.key;
2141        }
2142        k -= n.cnt;
2143        Self::kth_impl(&n.r, k)
2144    }
2145
2146    fn cnt_leq(t: &Link, x: f64) -> u32 {
2147        match t {
2148            None => 0,
2149            Some(n) => match x.total_cmp(&n.key) {
2150                core::cmp::Ordering::Less => Self::cnt_leq(&n.l, x),
2151                core::cmp::Ordering::Equal => Self::sz(&n.l) + n.cnt,
2152                core::cmp::Ordering::Greater => Self::sz(&n.l) + n.cnt + Self::cnt_leq(&n.r, x),
2153            },
2154        }
2155    }
2156
2157    fn cnt_lt(t: &Link, x: f64) -> u32 {
2158        match t {
2159            None => 0,
2160            Some(n) => match x.total_cmp(&n.key) {
2161                core::cmp::Ordering::Less => Self::cnt_lt(&n.l, x),
2162                core::cmp::Ordering::Equal => Self::sz(&n.l),
2163                core::cmp::Ordering::Greater => Self::sz(&n.l) + n.cnt + Self::cnt_lt(&n.r, x),
2164            },
2165        }
2166    }
2167
2168    fn sum_leq_impl(t: &Link, x: f64) -> f64 {
2169        match t {
2170            None => 0.0,
2171            Some(n) => match x.total_cmp(&n.key) {
2172                core::cmp::Ordering::Less => Self::sum_leq_impl(&n.l, x),
2173                core::cmp::Ordering::Equal => Self::sum(&n.l) + n.cnt as f64 * n.key,
2174                core::cmp::Ordering::Greater => {
2175                    Self::sum(&n.l) + n.cnt as f64 * n.key + Self::sum_leq_impl(&n.r, x)
2176                }
2177            },
2178        }
2179    }
2180}
2181
2182#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2183#[wasm_bindgen]
2184impl DeviationStream {
2185    #[wasm_bindgen(constructor)]
2186    pub fn new(period: usize, devtype: usize) -> Result<DeviationStream, JsValue> {
2187        let params = DeviationParams {
2188            period: Some(period),
2189            devtype: Some(devtype),
2190        };
2191        DeviationStream::try_new(params).map_err(|e| JsValue::from_str(&e.to_string()))
2192    }
2193
2194    #[wasm_bindgen]
2195    pub fn update(&mut self, value: f64) -> Option<f64> {
2196        self.update_impl(value)
2197    }
2198
2199    #[inline(always)]
2200    fn std_dev_ring_o1(&self) -> f64 {
2201        if self.count == 0 {
2202            return f64::NAN;
2203        }
2204
2205        if self.period == 1 {
2206            return 0.0;
2207        }
2208
2209        if self.count < self.period {
2210            return f64::NAN;
2211        }
2212
2213        let mean = self.sum * self.inv_n;
2214        let var = (self.sum_sq * self.inv_n) - mean * mean;
2215        var.sqrt()
2216    }
2217
2218    #[inline(always)]
2219    fn mean_abs_dev_ring(&self) -> f64 {
2220        if self.buffer.iter().any(|&x| !x.is_finite()) {
2221            return f64::NAN;
2222        }
2223
2224        let n = self.period as f64;
2225        let sum: f64 = self.buffer.iter().sum();
2226        let mean = sum / n;
2227        if !mean.is_finite() {
2228            return f64::NAN;
2229        }
2230        let abs_sum = self
2231            .buffer
2232            .iter()
2233            .fold(0.0, |acc, &x| acc + (x - mean).abs());
2234        abs_sum / n
2235    }
2236
2237    #[inline(always)]
2238    fn median_abs_dev_ring(&self) -> f64 {
2239        if self.buffer.iter().any(|&x| x.is_nan()) {
2240            return f64::NAN;
2241        }
2242
2243        let median = find_median(&self.buffer);
2244        let mut abs_devs: Vec<f64> = self.buffer.iter().map(|&x| (x - median).abs()).collect();
2245        find_median(&abs_devs)
2246    }
2247}
2248
2249#[inline(always)]
2250pub unsafe fn deviation_row_scalar(
2251    data: &[f64],
2252    first: usize,
2253    period: usize,
2254    _stride: usize,
2255    devtype: usize,
2256    out: &mut [f64],
2257) {
2258    match devtype {
2259        0 => {
2260            let _ = standard_deviation_rolling_into(data, period, first, out);
2261        }
2262        1 => {
2263            let _ = mean_absolute_deviation_rolling_into(data, period, first, out);
2264        }
2265        2 => {
2266            let _ = median_absolute_deviation_rolling_into(data, period, first, out);
2267        }
2268        3 => {
2269            let _ = mode_deviation_rolling_into(data, period, first, out);
2270        }
2271        _ => {}
2272    }
2273}
2274
2275#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2276#[inline(always)]
2277pub unsafe fn deviation_row_avx2(
2278    data: &[f64],
2279    first: usize,
2280    period: usize,
2281    stride: usize,
2282    devtype: usize,
2283    out: &mut [f64],
2284) {
2285    deviation_row_scalar(data, first, period, stride, devtype, out);
2286}
2287
2288#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2289#[inline(always)]
2290pub unsafe fn deviation_row_avx512(
2291    data: &[f64],
2292    first: usize,
2293    period: usize,
2294    stride: usize,
2295    devtype: usize,
2296    out: &mut [f64],
2297) {
2298    if period <= 32 {
2299        deviation_row_avx512_short(data, first, period, stride, devtype, out);
2300    } else {
2301        deviation_row_avx512_long(data, first, period, stride, devtype, out);
2302    }
2303}
2304
2305#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2306#[inline(always)]
2307pub unsafe fn deviation_row_avx512_short(
2308    data: &[f64],
2309    first: usize,
2310    period: usize,
2311    stride: usize,
2312    devtype: usize,
2313    out: &mut [f64],
2314) {
2315    deviation_row_scalar(data, first, period, stride, devtype, out);
2316}
2317
2318#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2319#[inline(always)]
2320pub unsafe fn deviation_row_avx512_long(
2321    data: &[f64],
2322    first: usize,
2323    period: usize,
2324    stride: usize,
2325    devtype: usize,
2326    out: &mut [f64],
2327) {
2328    deviation_row_scalar(data, first, period, stride, devtype, out);
2329}
2330
2331#[inline(always)]
2332pub fn deviation_expand_grid(r: &DeviationBatchRange) -> Vec<DeviationParams> {
2333    expand_grid(r)
2334}
2335
2336pub use DeviationError as DevError;
2337pub use DeviationInput as DevInput;
2338pub use DeviationParams as DevParams;
2339
2340use std::ops::{Index, IndexMut};
2341use std::slice::{Iter, IterMut};
2342impl Index<usize> for DeviationOutput {
2343    type Output = f64;
2344    fn index(&self, idx: usize) -> &Self::Output {
2345        &self.values[idx]
2346    }
2347}
2348impl IndexMut<usize> for DeviationOutput {
2349    fn index_mut(&mut self, idx: usize) -> &mut Self::Output {
2350        &mut self.values[idx]
2351    }
2352}
2353impl<'a> IntoIterator for &'a DeviationOutput {
2354    type Item = &'a f64;
2355    type IntoIter = Iter<'a, f64>;
2356    fn into_iter(self) -> Self::IntoIter {
2357        self.values.iter()
2358    }
2359}
2360impl<'a> IntoIterator for &'a mut DeviationOutput {
2361    type Item = &'a mut f64;
2362    type IntoIter = IterMut<'a, f64>;
2363    fn into_iter(self) -> Self::IntoIter {
2364        self.values.iter_mut()
2365    }
2366}
2367impl DeviationOutput {
2368    pub fn last(&self) -> Option<&f64> {
2369        self.values.last()
2370    }
2371    pub fn len(&self) -> usize {
2372        self.values.len()
2373    }
2374    pub fn is_empty(&self) -> bool {
2375        self.values.is_empty()
2376    }
2377}
2378
2379#[cfg(test)]
2380mod tests {
2381    use super::*;
2382    use crate::skip_if_unsupported;
2383    use crate::utilities::data_loader::read_candles_from_csv;
2384    use std::error::Error;
2385
2386    #[test]
2387    fn test_deviation_into_matches_api_v2() -> Result<(), Box<dyn std::error::Error>> {
2388        let len = 256;
2389        let mut data = Vec::with_capacity(len);
2390        for i in 0..len {
2391            let x = (i as f64 * 0.1).sin() * 10.0 + (i % 7) as f64;
2392            data.push(x);
2393        }
2394
2395        if len >= 2 {
2396            data[0] = f64::NAN;
2397            data[1] = f64::NAN;
2398        }
2399
2400        let input = DeviationInput::from_slice(&data, DeviationParams::default());
2401
2402        let baseline = deviation(&input)?.values;
2403
2404        let mut into_out = vec![0.0; len];
2405        #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
2406        {
2407            deviation_into(&input, &mut into_out)?;
2408        }
2409        #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2410        {
2411            return Ok(());
2412        }
2413
2414        fn eq_or_both_nan(a: f64, b: f64) -> bool {
2415            (a.is_nan() && b.is_nan()) || (a - b).abs() <= 1e-12
2416        }
2417
2418        assert_eq!(baseline.len(), into_out.len());
2419        for i in 0..len {
2420            assert!(
2421                eq_or_both_nan(baseline[i], into_out[i]),
2422                "mismatch at index {}: baseline={}, into={}",
2423                i,
2424                baseline[i],
2425                into_out[i]
2426            );
2427        }
2428
2429        Ok(())
2430    }
2431
2432    fn check_deviation_partial_params(
2433        test: &str,
2434        kernel: Kernel,
2435    ) -> Result<(), Box<dyn std::error::Error>> {
2436        skip_if_unsupported!(kernel, test);
2437        let data = [1.0, 2.0, 3.0, 4.0, 5.0];
2438        let input = DeviationInput::with_defaults(&data);
2439        let output = deviation_with_kernel(&input, kernel)?;
2440        assert_eq!(output.values.len(), data.len());
2441        Ok(())
2442    }
2443    fn check_deviation_accuracy(
2444        test: &str,
2445        kernel: Kernel,
2446    ) -> Result<(), Box<dyn std::error::Error>> {
2447        skip_if_unsupported!(kernel, test);
2448        let data = [1.0, 2.0, 3.0, 4.0, 5.0];
2449        let params = DeviationParams {
2450            period: Some(3),
2451            devtype: Some(0),
2452        };
2453        let input = DeviationInput::from_slice(&data, params);
2454        let result = deviation_with_kernel(&input, kernel)?;
2455        let expected = 0.816496580927726;
2456        for &val in &result.values[2..] {
2457            assert!(
2458                (val - expected).abs() < 1e-12,
2459                "[{test}] deviation mismatch: got {}, expected {}",
2460                val,
2461                expected
2462            );
2463        }
2464        Ok(())
2465    }
2466    fn check_deviation_default_params(
2467        test: &str,
2468        kernel: Kernel,
2469    ) -> Result<(), Box<dyn std::error::Error>> {
2470        skip_if_unsupported!(kernel, test);
2471        let data = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0];
2472        let input = DeviationInput::with_defaults(&data);
2473        let output = deviation_with_kernel(&input, kernel)?;
2474        assert_eq!(output.values.len(), data.len());
2475        Ok(())
2476    }
2477    fn check_deviation_zero_period(
2478        test: &str,
2479        kernel: Kernel,
2480    ) -> Result<(), Box<dyn std::error::Error>> {
2481        skip_if_unsupported!(kernel, test);
2482        let data = [1.0, 2.0, 3.0];
2483        let params = DeviationParams {
2484            period: Some(0),
2485            devtype: Some(0),
2486        };
2487        let input = DeviationInput::from_slice(&data, params);
2488        let res = deviation_with_kernel(&input, kernel);
2489        assert!(
2490            res.is_err(),
2491            "[{test}] deviation should error with zero period"
2492        );
2493        Ok(())
2494    }
2495    fn check_deviation_period_exceeds_length(
2496        test: &str,
2497        kernel: Kernel,
2498    ) -> Result<(), Box<dyn std::error::Error>> {
2499        skip_if_unsupported!(kernel, test);
2500        let data = [1.0, 2.0, 3.0];
2501        let params = DeviationParams {
2502            period: Some(10),
2503            devtype: Some(0),
2504        };
2505        let input = DeviationInput::from_slice(&data, params);
2506        let res = deviation_with_kernel(&input, kernel);
2507        assert!(
2508            res.is_err(),
2509            "[{test}] deviation should error if period > data.len()"
2510        );
2511        Ok(())
2512    }
2513    fn check_deviation_very_small_dataset(
2514        test: &str,
2515        kernel: Kernel,
2516    ) -> Result<(), Box<dyn std::error::Error>> {
2517        skip_if_unsupported!(kernel, test);
2518        let single = [42.0];
2519        let params = DeviationParams {
2520            period: Some(9),
2521            devtype: Some(0),
2522        };
2523        let input = DeviationInput::from_slice(&single, params);
2524        let res = deviation_with_kernel(&input, kernel);
2525        assert!(
2526            res.is_err(),
2527            "[{test}] deviation should error if not enough data"
2528        );
2529        Ok(())
2530    }
2531    fn check_deviation_nan_handling(
2532        test: &str,
2533        kernel: Kernel,
2534    ) -> Result<(), Box<dyn std::error::Error>> {
2535        skip_if_unsupported!(kernel, test);
2536        let data = [f64::NAN, f64::NAN, 1.0, 2.0, 3.0, 4.0, 5.0];
2537        let params = DeviationParams {
2538            period: Some(3),
2539            devtype: Some(0),
2540        };
2541        let input = DeviationInput::from_slice(&data, params);
2542        let res = deviation_with_kernel(&input, kernel)?;
2543        assert_eq!(res.values.len(), data.len());
2544        for (i, &v) in res.values.iter().enumerate().skip(4) {
2545            assert!(!v.is_nan(), "[{test}] Unexpected NaN at out-index {}", i);
2546        }
2547        Ok(())
2548    }
2549    fn check_deviation_streaming(
2550        test: &str,
2551        kernel: Kernel,
2552    ) -> Result<(), Box<dyn std::error::Error>> {
2553        skip_if_unsupported!(kernel, test);
2554        let data = [1.0, 2.0, 3.0, 4.0, 5.0];
2555        let period = 3;
2556        let devtype = 0;
2557        let input = DeviationInput::from_slice(
2558            &data,
2559            DeviationParams {
2560                period: Some(period),
2561                devtype: Some(devtype),
2562            },
2563        );
2564        let batch_output = deviation_with_kernel(&input, kernel)?.values;
2565        let mut stream = DeviationStream::try_new(DeviationParams {
2566            period: Some(period),
2567            devtype: Some(devtype),
2568        })?;
2569        let mut stream_values = Vec::with_capacity(data.len());
2570        for &val in &data {
2571            match stream.update(val) {
2572                Some(out_val) => stream_values.push(out_val),
2573                None => stream_values.push(f64::NAN),
2574            }
2575        }
2576        assert_eq!(batch_output.len(), stream_values.len());
2577        for (i, (b, s)) in batch_output.iter().zip(stream_values.iter()).enumerate() {
2578            if b.is_nan() && s.is_nan() {
2579                continue;
2580            }
2581            assert!(
2582                (b - s).abs() < 1e-9,
2583                "[{test}] streaming f64 mismatch at idx {}: batch={}, stream={}, diff={}",
2584                i,
2585                b,
2586                s,
2587                (b - s).abs()
2588            );
2589        }
2590        Ok(())
2591    }
2592    fn check_deviation_mean_absolute(
2593        test: &str,
2594        kernel: Kernel,
2595    ) -> Result<(), Box<dyn std::error::Error>> {
2596        skip_if_unsupported!(kernel, test);
2597        let data = [1.0, 2.0, 3.0, 4.0, 5.0];
2598        let params = DeviationParams {
2599            period: Some(3),
2600            devtype: Some(1),
2601        };
2602        let input = DeviationInput::from_slice(&data, params);
2603        let result = deviation_with_kernel(&input, kernel)?;
2604        let expected = 2.0 / 3.0;
2605        for &val in &result.values[2..] {
2606            assert!(
2607                (val - expected).abs() < 1e-12,
2608                "[{test}] mean abs deviation mismatch: got {}, expected {}",
2609                val,
2610                expected
2611            );
2612        }
2613        Ok(())
2614    }
2615    fn check_deviation_median_absolute(
2616        test: &str,
2617        kernel: Kernel,
2618    ) -> Result<(), Box<dyn std::error::Error>> {
2619        skip_if_unsupported!(kernel, test);
2620        let data = [1.0, 2.0, 3.0, 4.0, 5.0];
2621        let params = DeviationParams {
2622            period: Some(3),
2623            devtype: Some(2),
2624        };
2625        let input = DeviationInput::from_slice(&data, params);
2626        let result = deviation_with_kernel(&input, kernel)?;
2627        let expected = 1.0;
2628        for &val in &result.values[2..] {
2629            assert!(
2630                (val - expected).abs() < 1e-12,
2631                "[{test}] median abs deviation mismatch: got {}, expected {}",
2632                val,
2633                expected
2634            );
2635        }
2636        Ok(())
2637    }
2638    fn check_deviation_invalid_devtype(
2639        test: &str,
2640        kernel: Kernel,
2641    ) -> Result<(), Box<dyn std::error::Error>> {
2642        skip_if_unsupported!(kernel, test);
2643        let data = [1.0, 2.0, 3.0, 4.0];
2644        let params = DeviationParams {
2645            period: Some(2),
2646            devtype: Some(999),
2647        };
2648        let input = DeviationInput::from_slice(&data, params);
2649        let result = deviation_with_kernel(&input, kernel);
2650        assert!(
2651            matches!(result, Err(DeviationError::InvalidDevType { .. })),
2652            "[{test}] invalid devtype should error"
2653        );
2654        Ok(())
2655    }
2656
2657    #[cfg(debug_assertions)]
2658    fn check_deviation_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2659        skip_if_unsupported!(kernel, test_name);
2660
2661        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2662        let candles = read_candles_from_csv(file_path)?;
2663        let data = candles.select_candle_field("close")?;
2664
2665        let test_params = vec![
2666            DeviationParams::default(),
2667            DeviationParams {
2668                period: Some(2),
2669                devtype: Some(0),
2670            },
2671            DeviationParams {
2672                period: Some(5),
2673                devtype: Some(0),
2674            },
2675            DeviationParams {
2676                period: Some(5),
2677                devtype: Some(1),
2678            },
2679            DeviationParams {
2680                period: Some(5),
2681                devtype: Some(2),
2682            },
2683            DeviationParams {
2684                period: Some(20),
2685                devtype: Some(0),
2686            },
2687            DeviationParams {
2688                period: Some(20),
2689                devtype: Some(1),
2690            },
2691            DeviationParams {
2692                period: Some(20),
2693                devtype: Some(2),
2694            },
2695            DeviationParams {
2696                period: Some(50),
2697                devtype: Some(0),
2698            },
2699            DeviationParams {
2700                period: Some(50),
2701                devtype: Some(1),
2702            },
2703            DeviationParams {
2704                period: Some(100),
2705                devtype: Some(0),
2706            },
2707            DeviationParams {
2708                period: Some(100),
2709                devtype: Some(2),
2710            },
2711        ];
2712
2713        for (param_idx, params) in test_params.iter().enumerate() {
2714            let input = DeviationInput::from_slice(&data, params.clone());
2715            let output = deviation_with_kernel(&input, kernel)?;
2716
2717            for (i, &val) in output.values.iter().enumerate() {
2718                if val.is_nan() {
2719                    continue;
2720                }
2721
2722                let bits = val.to_bits();
2723
2724                if bits == 0x11111111_11111111 {
2725                    panic!(
2726                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
2727						 with params: period={}, devtype={} (param set {})",
2728                        test_name,
2729                        val,
2730                        bits,
2731                        i,
2732                        params.period.unwrap_or(9),
2733                        params.devtype.unwrap_or(0),
2734                        param_idx
2735                    );
2736                }
2737
2738                if bits == 0x22222222_22222222 {
2739                    panic!(
2740                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
2741						 with params: period={}, devtype={} (param set {})",
2742                        test_name,
2743                        val,
2744                        bits,
2745                        i,
2746                        params.period.unwrap_or(9),
2747                        params.devtype.unwrap_or(0),
2748                        param_idx
2749                    );
2750                }
2751
2752                if bits == 0x33333333_33333333 {
2753                    panic!(
2754                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
2755						 with params: period={}, devtype={} (param set {})",
2756                        test_name,
2757                        val,
2758                        bits,
2759                        i,
2760                        params.period.unwrap_or(9),
2761                        params.devtype.unwrap_or(0),
2762                        param_idx
2763                    );
2764                }
2765            }
2766        }
2767
2768        Ok(())
2769    }
2770
2771    #[cfg(not(debug_assertions))]
2772    fn check_deviation_no_poison(_test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
2773        Ok(())
2774    }
2775
2776    macro_rules! generate_all_deviation_tests {
2777        ($($test_fn:ident),*) => {
2778            paste::paste! {
2779                $(
2780                    #[test]
2781                    fn [<$test_fn _scalar_f64>]() {
2782                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
2783                    }
2784                )*
2785                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2786                $(
2787                    #[test]
2788                    fn [<$test_fn _avx2_f64>]() {
2789                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
2790                    }
2791                    #[test]
2792                    fn [<$test_fn _avx512_f64>]() {
2793                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
2794                    }
2795                )*
2796            }
2797        }
2798    }
2799    generate_all_deviation_tests!(
2800        check_deviation_partial_params,
2801        check_deviation_accuracy,
2802        check_deviation_default_params,
2803        check_deviation_zero_period,
2804        check_deviation_period_exceeds_length,
2805        check_deviation_very_small_dataset,
2806        check_deviation_nan_handling,
2807        check_deviation_streaming,
2808        check_deviation_mean_absolute,
2809        check_deviation_median_absolute,
2810        check_deviation_invalid_devtype,
2811        check_deviation_no_poison
2812    );
2813
2814    #[test]
2815    fn test_deviation_into_matches_api() -> Result<(), Box<dyn std::error::Error>> {
2816        let mut data = Vec::with_capacity(256);
2817        data.extend_from_slice(&[f64::NAN, f64::NAN, f64::NAN, f64::NAN, f64::NAN]);
2818        for i in 0..251usize {
2819            let x = (i as f64 * 0.113).sin() * 3.7 + (i as f64 * 0.017).cos() * 1.3 + 42.0;
2820            data.push(x);
2821        }
2822
2823        let input = DeviationInput::with_defaults(&data);
2824
2825        let baseline = deviation(&input)?.values;
2826
2827        let mut out = vec![0.0f64; data.len()];
2828        #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
2829        {
2830            deviation_into(&input, &mut out)?;
2831        }
2832        #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2833        {
2834            deviation_into_slice(&mut out, &input, Kernel::Auto)?;
2835        }
2836
2837        assert_eq!(baseline.len(), out.len());
2838        fn eq_or_both_nan(a: f64, b: f64) -> bool {
2839            (a.is_nan() && b.is_nan()) || (a == b)
2840        }
2841        for (i, (&a, &b)) in baseline.iter().zip(out.iter()).enumerate() {
2842            assert!(
2843                eq_or_both_nan(a, b),
2844                "deviation_into parity mismatch at index {}: vec={} into={}",
2845                i,
2846                a,
2847                b
2848            );
2849        }
2850        Ok(())
2851    }
2852
2853    #[cfg(feature = "proptest")]
2854    generate_all_deviation_tests!(check_deviation_property);
2855
2856    #[cfg(test)]
2857    mod deviation_property_tests {
2858        use super::*;
2859        use proptest::prelude::*;
2860
2861        proptest! {
2862            #[test]
2863            fn deviation_property_test(
2864                data in prop::collection::vec(prop::num::f64::ANY, 10..=1000),
2865                period in 2usize..=50,
2866                devtype in 0usize..=2
2867            ) {
2868
2869                if data.iter().all(|x| x.is_nan()) {
2870                    return Ok(());
2871                }
2872
2873
2874                let first_valid = data.iter().position(|x| !x.is_nan()).unwrap_or(0);
2875                if data.len() - first_valid < period {
2876                    return Ok(());
2877                }
2878
2879                let params = DeviationParams {
2880                    period: Some(period),
2881                    devtype: Some(devtype),
2882                };
2883                let input = DeviationInput::from_slice(&data, params);
2884
2885
2886                let result = deviation(&input);
2887
2888
2889                if let Ok(output) = result {
2890                    prop_assert_eq!(output.values.len(), data.len());
2891
2892
2893                    for i in 0..(first_valid + period - 1).min(data.len()) {
2894                        prop_assert!(output.values[i].is_nan());
2895                    }
2896
2897
2898                    if devtype <= 2 {
2899                        for i in (first_valid + period - 1)..data.len() {
2900
2901                            let window_start = if i >= period - 1 { i + 1 - period } else { 0 };
2902                            let window = &data[window_start..=i];
2903                            let window_has_nan = window.iter().any(|x| x.is_nan());
2904
2905
2906                            let would_overflow = match devtype {
2907                                0 => {
2908
2909                                    let sum: f64 = window.iter().sum();
2910                                    let sumsq: f64 = window.iter().map(|&x| x * x).sum();
2911                                    !sum.is_finite() || !sumsq.is_finite()
2912                                },
2913                                1 => {
2914
2915                                    window.iter().any(|&x| !x.is_finite())
2916                                },
2917                                2 => {
2918
2919                                    window.iter().any(|&x| !x.is_finite())
2920                                },
2921                                _ => false,
2922                            };
2923
2924
2925                            if window_has_nan || would_overflow {
2926                                prop_assert!(output.values[i].is_nan());
2927                            } else {
2928                                prop_assert!(!output.values[i].is_nan());
2929                            }
2930                        }
2931                    }
2932                }
2933            }
2934        }
2935    }
2936
2937    #[cfg(feature = "proptest")]
2938    fn check_deviation_property(
2939        test_name: &str,
2940        kernel: Kernel,
2941    ) -> Result<(), Box<dyn std::error::Error>> {
2942        use proptest::prelude::*;
2943        skip_if_unsupported!(kernel, test_name);
2944
2945        let strat = (2usize..=50).prop_flat_map(|period| {
2946            (
2947                prop::collection::vec(
2948                    (10.0f64..10000.0f64).prop_filter("finite", |x| x.is_finite()),
2949                    period + 10..400,
2950                ),
2951                Just(period),
2952                0usize..=2,
2953            )
2954        });
2955
2956        proptest::test_runner::TestRunner::default()
2957            .run(&strat, |(data, period, devtype)| {
2958                let params = DeviationParams {
2959                    period: Some(period),
2960                    devtype: Some(devtype),
2961                };
2962                let input = DeviationInput::from_slice(&data, params);
2963
2964                let DeviationOutput { values: out } =
2965                    deviation_with_kernel(&input, kernel).unwrap();
2966                let DeviationOutput { values: ref_out } =
2967                    deviation_with_kernel(&input, Kernel::Scalar).unwrap();
2968
2969                let first_valid = data.iter().position(|x| !x.is_nan()).unwrap_or(0);
2970                let warmup_period = first_valid + period - 1;
2971
2972                for i in 0..warmup_period.min(data.len()) {
2973                    prop_assert!(
2974                        out[i].is_nan(),
2975                        "Expected NaN during warmup at index {}, got {}",
2976                        i,
2977                        out[i]
2978                    );
2979                }
2980
2981                prop_assert_eq!(out.len(), data.len());
2982
2983                for i in warmup_period..data.len() {
2984                    let y = out[i];
2985                    let r = ref_out[i];
2986
2987                    prop_assert!(
2988                        y.is_nan() || y >= -1e-12,
2989                        "Deviation at index {} is negative: {}",
2990                        i,
2991                        y
2992                    );
2993
2994                    let window = &data[i + 1 - period..=i];
2995                    let all_same = window.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-14);
2996                    if all_same && window.iter().all(|x| x.is_finite()) {
2997                        prop_assert!(
2998								y.abs() < 1e-2 || y.is_nan(),
2999								"Deviation should be ~0 (or NaN due to precision bug) for constant window at index {}: {}",
3000								i,
3001								y
3002							);
3003                    }
3004
3005                    if devtype == 0 && y.is_finite() && y > 1e-10 {
3006                        let variance = y * y;
3007
3008                        let window_mean = window.iter().sum::<f64>() / (period as f64);
3009                        let computed_var = window
3010                            .iter()
3011                            .map(|&x| (x - window_mean).powi(2))
3012                            .sum::<f64>()
3013                            / (period as f64);
3014
3015                        let var_diff = (variance - computed_var).abs();
3016                        let relative_error = var_diff / computed_var.max(1e-10);
3017                        let var_tol = computed_var.abs() * 1e-6 + 1e-8;
3018                        prop_assert!(
3019							var_diff <= var_tol,
3020							"Variance relationship failed at index {}: stddev²={} vs computed_var={} (rel_err={})",
3021							i,
3022							variance,
3023							computed_var,
3024							relative_error
3025						);
3026                    }
3027
3028                    if !y.is_finite() || !r.is_finite() {
3029                        prop_assert!(
3030                            y.to_bits() == r.to_bits(),
3031                            "finite/NaN mismatch at index {}: {} vs {}",
3032                            i,
3033                            y,
3034                            r
3035                        );
3036                        continue;
3037                    }
3038
3039                    let ulp_diff: u64 = y.to_bits().abs_diff(r.to_bits());
3040                    let abs_diff = (y - r).abs();
3041
3042                    let tol = (r.abs() * 1e-7_f64).max(1e-6_f64);
3043                    prop_assert!(
3044                        abs_diff <= tol || ulp_diff <= 4,
3045                        "Kernel mismatch at index {}: {} vs {} (ULP={})",
3046                        i,
3047                        y,
3048                        r,
3049                        ulp_diff
3050                    );
3051
3052                    let window_min = window.iter().cloned().fold(f64::INFINITY, f64::min);
3053                    let window_max = window.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
3054                    let window_range = window_max - window_min;
3055
3056                    prop_assert!(
3057                        y <= window_range + 1e-9,
3058                        "Deviation {} exceeds window range {} at index {}",
3059                        y,
3060                        window_range,
3061                        i
3062                    );
3063
3064                    match devtype {
3065                        0 => {
3066                            if y.is_finite() && y > 0.0 {
3067                                let window_mean = window.iter().sum::<f64>() / (period as f64);
3068                                let theoretical_var = window
3069                                    .iter()
3070                                    .map(|&x| (x - window_mean).powi(2))
3071                                    .sum::<f64>()
3072                                    / (period as f64);
3073                                let theoretical_std = theoretical_var.sqrt();
3074
3075                                let tolerance = theoretical_std * 1e-4 + 1e-10;
3076                                prop_assert!(
3077									y <= theoretical_std + tolerance,
3078									"StdDev {} exceeds theoretical value {} by more than tolerance at index {}",
3079									y,
3080									theoretical_std,
3081									i
3082								);
3083
3084                                let window_min =
3085                                    window.iter().cloned().fold(f64::INFINITY, f64::min);
3086                                let window_max =
3087                                    window.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
3088                                let max_possible_std = (window_max - window_min) / 2.0;
3089                                let max_bound = max_possible_std * 1.001 + 1e-8;
3090
3091                                prop_assert!(
3092                                    y <= max_bound,
3093                                    "StdDev {} exceeds maximum possible {} (bound={}) at index {}",
3094                                    y,
3095                                    max_possible_std,
3096                                    max_bound,
3097                                    i
3098                                );
3099                            }
3100                        }
3101                        1 => {
3102                            let std_dev_params = DeviationParams {
3103                                period: Some(period),
3104                                devtype: Some(0),
3105                            };
3106                            let std_input = DeviationInput::from_slice(&data, std_dev_params);
3107                            if let Ok(std_output) = deviation_with_kernel(&std_input, kernel) {
3108                                let std_val = std_output.values[i];
3109                                if std_val.is_finite() && y.is_finite() {
3110                                    let tolerance = std_val * 1e-7 + 1e-9;
3111                                    prop_assert!(
3112                                        y <= std_val + tolerance,
3113                                        "MAD {} exceeds StdDev {} at index {}",
3114                                        y,
3115                                        std_val,
3116                                        i
3117                                    );
3118                                }
3119                            }
3120                        }
3121                        2 => {
3122                            if y.is_finite() && y > 0.0 {
3123                                prop_assert!(
3124                                    y <= window_range + 1e-12,
3125                                    "MedianAbsDev {} exceeds window range {} at index {}",
3126                                    y,
3127                                    window_range,
3128                                    i
3129                                );
3130
3131                                let std_dev_params = DeviationParams {
3132                                    period: Some(period),
3133                                    devtype: Some(0),
3134                                };
3135                                let std_input = DeviationInput::from_slice(&data, std_dev_params);
3136                                if let Ok(std_output) = deviation_with_kernel(&std_input, kernel) {
3137                                    let std_val = std_output.values[i];
3138                                    if std_val.is_finite() && std_val > 0.0 {
3139                                        prop_assert!(
3140                                            y <= std_val * 1.5 + 1e-9,
3141                                            "MedAD {} exceeds 1.5x StdDev {} at index {}",
3142                                            y,
3143                                            std_val,
3144                                            i
3145                                        );
3146                                    }
3147                                }
3148
3149                                let mut sorted_window: Vec<f64> = window.iter().cloned().collect();
3150                                sorted_window.sort_by(|a, b| {
3151                                    a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
3152                                });
3153                                let median = if period % 2 == 0 {
3154                                    (sorted_window[period / 2 - 1] + sorted_window[period / 2])
3155                                        / 2.0
3156                                } else {
3157                                    sorted_window[period / 2]
3158                                };
3159                                let identical_count = window
3160                                    .iter()
3161                                    .filter(|&&x| (x - median).abs() < 1e-14)
3162                                    .count();
3163                                if identical_count > period / 2 {
3164                                    prop_assert!(
3165										y < 1e-9,
3166										"MedAD should be ~0 when >50% values are identical at index {}: {}",
3167										i,
3168										y
3169									);
3170                                }
3171                            }
3172                        }
3173                        _ => {}
3174                    }
3175
3176                    if i >= warmup_period + period && y.is_finite() {
3177                        let old_idx = i - period - 1;
3178                        if old_idx < data.len() {
3179                            let current_window = &data[i + 1 - period..=i];
3180                            let window_variance = match devtype {
3181                                0 => {
3182                                    let mean = current_window.iter().sum::<f64>() / (period as f64);
3183                                    let var = current_window
3184                                        .iter()
3185                                        .map(|&x| (x - mean).powi(2))
3186                                        .sum::<f64>()
3187                                        / (period as f64);
3188                                    var.sqrt()
3189                                }
3190                                1 => {
3191                                    let mean = current_window.iter().sum::<f64>() / (period as f64);
3192                                    current_window
3193                                        .iter()
3194                                        .map(|&x| (x - mean).abs())
3195                                        .sum::<f64>()
3196                                        / (period as f64)
3197                                }
3198                                2 => y,
3199                                _ => y,
3200                            };
3201
3202                            if devtype != 2 {
3203                                let diff = (y - window_variance).abs();
3204                                let tolerance = window_variance * 1e-6 + 1e-8;
3205                                prop_assert!(
3206									diff <= tolerance,
3207									"Rolling window deviation mismatch at index {}: computed {} vs expected {} (diff={})",
3208									i,
3209									y,
3210									window_variance,
3211									diff
3212								);
3213                            }
3214                        }
3215                    }
3216                }
3217
3218                Ok(())
3219            })
3220            .unwrap();
3221
3222        Ok(())
3223    }
3224
3225    fn check_batch_default_row(
3226        test: &str,
3227        kernel: Kernel,
3228    ) -> Result<(), Box<dyn std::error::Error>> {
3229        skip_if_unsupported!(kernel, test);
3230        let data = [1.0, 2.0, 3.0, 4.0, 5.0];
3231        let output = DeviationBatchBuilder::new()
3232            .kernel(kernel)
3233            .apply_slice(&data)?;
3234        let def = DeviationParams::default();
3235        let row = output.values_for(&def).expect("default row missing");
3236        assert_eq!(row.len(), data.len());
3237        let single = DeviationInput::from_slice(&data, def.clone());
3238        let single_out = deviation_with_kernel(&single, kernel)?.values;
3239        for (i, (r, s)) in row.iter().zip(single_out.iter()).enumerate() {
3240            if r.is_nan() && s.is_nan() {
3241                continue;
3242            }
3243            assert!(
3244                (r - s).abs() < 1e-12,
3245                "[{test}] default-row batch mismatch at idx {}: {} vs {}",
3246                i,
3247                r,
3248                s
3249            );
3250        }
3251        Ok(())
3252    }
3253    fn check_batch_varying_params(
3254        test: &str,
3255        kernel: Kernel,
3256    ) -> Result<(), Box<dyn std::error::Error>> {
3257        skip_if_unsupported!(kernel, test);
3258        let data = [1.0, 2.0, 3.0, 4.0, 5.0];
3259        let batch_output = DeviationBatchBuilder::new()
3260            .period_range(2, 3, 1)
3261            .devtype_range(0, 2, 1)
3262            .kernel(kernel)
3263            .apply_slice(&data)?;
3264        assert!(batch_output.rows >= 3, "[{test}] Not enough batch rows");
3265        for params in &batch_output.combos {
3266            let single = DeviationInput::from_slice(&data, params.clone());
3267            let single_out = deviation_with_kernel(&single, kernel)?.values;
3268            let row = batch_output.values_for(params).unwrap();
3269            for (i, (r, s)) in row.iter().zip(single_out.iter()).enumerate() {
3270                if r.is_nan() && s.is_nan() {
3271                    continue;
3272                }
3273                assert!(
3274                    (r - s).abs() < 1e-12,
3275                    "[{test}] batch grid row mismatch at idx {}: {} vs {}",
3276                    i,
3277                    r,
3278                    s
3279                );
3280            }
3281        }
3282        Ok(())
3283    }
3284
3285    #[cfg(debug_assertions)]
3286    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3287        skip_if_unsupported!(kernel, test);
3288
3289        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3290        let c = read_candles_from_csv(file)?;
3291        let data = c.select_candle_field("close")?;
3292
3293        let test_configs = vec![
3294            (2, 10, 2, 0, 2, 1),
3295            (5, 25, 5, 0, 0, 0),
3296            (5, 25, 5, 1, 1, 0),
3297            (5, 25, 5, 2, 2, 0),
3298            (30, 60, 15, 0, 2, 1),
3299            (2, 5, 1, 0, 2, 1),
3300            (50, 100, 25, 0, 0, 0),
3301            (10, 10, 0, 0, 2, 1),
3302        ];
3303
3304        for (cfg_idx, &(p_start, p_end, p_step, d_start, d_end, d_step)) in
3305            test_configs.iter().enumerate()
3306        {
3307            let output = DeviationBatchBuilder::new()
3308                .kernel(kernel)
3309                .period_range(p_start, p_end, p_step)
3310                .devtype_range(d_start, d_end, d_step)
3311                .apply_slice(&data)?;
3312
3313            for (idx, &val) in output.values.iter().enumerate() {
3314                if val.is_nan() {
3315                    continue;
3316                }
3317
3318                let bits = val.to_bits();
3319                let row = idx / output.cols;
3320                let col = idx % output.cols;
3321                let combo = &output.combos[row];
3322
3323                if bits == 0x11111111_11111111 {
3324                    panic!(
3325                        "[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
3326						 at row {} col {} (flat index {}) with params: period={}, devtype={}",
3327                        test,
3328                        cfg_idx,
3329                        val,
3330                        bits,
3331                        row,
3332                        col,
3333                        idx,
3334                        combo.period.unwrap_or(9),
3335                        combo.devtype.unwrap_or(0)
3336                    );
3337                }
3338
3339                if bits == 0x22222222_22222222 {
3340                    panic!(
3341                        "[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
3342						 at row {} col {} (flat index {}) with params: period={}, devtype={}",
3343                        test,
3344                        cfg_idx,
3345                        val,
3346                        bits,
3347                        row,
3348                        col,
3349                        idx,
3350                        combo.period.unwrap_or(9),
3351                        combo.devtype.unwrap_or(0)
3352                    );
3353                }
3354
3355                if bits == 0x33333333_33333333 {
3356                    panic!(
3357                        "[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
3358						 at row {} col {} (flat index {}) with params: period={}, devtype={}",
3359                        test,
3360                        cfg_idx,
3361                        val,
3362                        bits,
3363                        row,
3364                        col,
3365                        idx,
3366                        combo.period.unwrap_or(9),
3367                        combo.devtype.unwrap_or(0)
3368                    );
3369                }
3370            }
3371        }
3372
3373        Ok(())
3374    }
3375
3376    #[cfg(not(debug_assertions))]
3377    fn check_batch_no_poison(_test: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
3378        Ok(())
3379    }
3380
3381    macro_rules! gen_batch_tests {
3382        ($fn_name:ident) => {
3383            paste::paste! {
3384                #[test] fn [<$fn_name _scalar>]() {
3385                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
3386                }
3387                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3388                #[test] fn [<$fn_name _avx2>]() {
3389                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
3390                }
3391                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3392                #[test] fn [<$fn_name _avx512>]() {
3393                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
3394                }
3395                #[test] fn [<$fn_name _auto_detect>]() {
3396                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
3397                }
3398            }
3399        };
3400    }
3401    gen_batch_tests!(check_batch_default_row);
3402    gen_batch_tests!(check_batch_varying_params);
3403    gen_batch_tests!(check_batch_no_poison);
3404}
3405
3406#[cfg(feature = "python")]
3407#[pyfunction(name = "deviation")]
3408#[pyo3(signature = (data, period, devtype, kernel=None))]
3409pub fn deviation_py<'py>(
3410    py: Python<'py>,
3411    data: numpy::PyReadonlyArray1<'py, f64>,
3412    period: usize,
3413    devtype: usize,
3414    kernel: Option<&str>,
3415) -> PyResult<Bound<'py, numpy::PyArray1<f64>>> {
3416    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
3417    let slice_in = data.as_slice()?;
3418    let kern = validate_kernel(kernel, false)?;
3419    let params = DeviationParams {
3420        period: Some(period),
3421        devtype: Some(devtype),
3422    };
3423    let input = DeviationInput::from_slice(slice_in, params);
3424    let vec_out: Vec<f64> = py
3425        .allow_threads(|| deviation_with_kernel(&input, kern).map(|o| o.values))
3426        .map_err(|e| PyValueError::new_err(e.to_string()))?;
3427    Ok(vec_out.into_pyarray(py))
3428}
3429
3430#[cfg(feature = "python")]
3431#[pyclass(name = "DeviationStream")]
3432pub struct DeviationStreamPy {
3433    stream: DeviationStream,
3434}
3435
3436#[cfg(feature = "python")]
3437#[pymethods]
3438impl DeviationStreamPy {
3439    #[new]
3440    fn new(period: usize, devtype: usize) -> PyResult<Self> {
3441        let params = DeviationParams {
3442            period: Some(period),
3443            devtype: Some(devtype),
3444        };
3445        let stream =
3446            DeviationStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
3447        Ok(DeviationStreamPy { stream })
3448    }
3449
3450    fn update(&mut self, value: f64) -> Option<f64> {
3451        self.stream.update(value)
3452    }
3453}
3454
3455#[cfg(feature = "python")]
3456#[pyfunction(name = "deviation_batch")]
3457#[pyo3(signature = (data, period_range, devtype_range, kernel=None))]
3458pub fn deviation_batch_py<'py>(
3459    py: Python<'py>,
3460    data: PyReadonlyArray1<'py, f64>,
3461    period_range: (usize, usize, usize),
3462    devtype_range: (usize, usize, usize),
3463    kernel: Option<&str>,
3464) -> PyResult<Bound<'py, PyDict>> {
3465    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
3466
3467    let slice_in = data.as_slice()?;
3468    let kern = validate_kernel(kernel, true)?;
3469
3470    let sweep = DeviationBatchRange {
3471        period: period_range,
3472        devtype: devtype_range,
3473    };
3474
3475    let combos = expand_grid(&sweep);
3476    let rows = combos.len();
3477    let cols = slice_in.len();
3478    let total = rows
3479        .checked_mul(cols)
3480        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
3481
3482    let out_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
3483    let slice_out = unsafe { out_arr.as_slice_mut()? };
3484
3485    let combos = py
3486        .allow_threads(|| deviation_batch_inner_into(slice_in, &sweep, kern, true, slice_out))
3487        .map_err(|e| PyValueError::new_err(e.to_string()))?;
3488
3489    let dict = PyDict::new(py);
3490    dict.set_item("values", out_arr.reshape((rows, cols))?)?;
3491    dict.set_item(
3492        "periods",
3493        combos
3494            .iter()
3495            .map(|p| p.period.unwrap() as u64)
3496            .collect::<Vec<_>>()
3497            .into_pyarray(py),
3498    )?;
3499    dict.set_item(
3500        "devtypes",
3501        combos
3502            .iter()
3503            .map(|p| p.devtype.unwrap() as u64)
3504            .collect::<Vec<_>>()
3505            .into_pyarray(py),
3506    )?;
3507    Ok(dict)
3508}
3509
3510#[cfg(all(feature = "python", feature = "cuda"))]
3511#[pyfunction(name = "deviation_cuda_batch_dev")]
3512#[pyo3(signature = (data_f32, period_range, devtype_range=(0,0,0), device_id=0))]
3513pub fn deviation_cuda_batch_dev_py<'py>(
3514    py: Python<'py>,
3515    data_f32: PyReadonlyArray1<'py, f32>,
3516    period_range: (usize, usize, usize),
3517    devtype_range: (usize, usize, usize),
3518    device_id: usize,
3519) -> PyResult<(DeviceArrayF32Py, Bound<'py, PyDict>)> {
3520    if !cuda_available() {
3521        return Err(PyValueError::new_err("CUDA not available"));
3522    }
3523    let slice_in = data_f32.as_slice()?;
3524    let sweep = DeviationBatchRange {
3525        period: period_range,
3526        devtype: devtype_range,
3527    };
3528    let (inner, combos) = py.allow_threads(|| {
3529        let cuda =
3530            CudaDeviation::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
3531        cuda.deviation_batch_dev(slice_in, &sweep)
3532            .map_err(|e| PyValueError::new_err(e.to_string()))
3533    })?;
3534    let dict = PyDict::new(py);
3535    let periods: Vec<u64> = combos.iter().map(|p| p.period.unwrap() as u64).collect();
3536    let devtypes: Vec<u64> = combos.iter().map(|p| p.devtype.unwrap() as u64).collect();
3537    dict.set_item("periods", periods.into_pyarray(py))?;
3538    dict.set_item("devtypes", devtypes.into_pyarray(py))?;
3539    let dev = make_device_array_py(device_id, inner)?;
3540    Ok((dev, dict))
3541}
3542
3543#[cfg(all(feature = "python", feature = "cuda"))]
3544#[pyfunction(name = "deviation_cuda_many_series_one_param_dev")]
3545#[pyo3(signature = (data_tm_f32, cols, rows, period, devtype=0, device_id=0))]
3546pub fn deviation_cuda_many_series_one_param_dev_py<'py>(
3547    py: Python<'py>,
3548    data_tm_f32: PyReadonlyArray1<'py, f32>,
3549    cols: usize,
3550    rows: usize,
3551    period: usize,
3552    devtype: usize,
3553    device_id: usize,
3554) -> PyResult<DeviceArrayF32Py> {
3555    if !cuda_available() {
3556        return Err(PyValueError::new_err("CUDA not available"));
3557    }
3558    if devtype != 0 {
3559        return Err(PyValueError::new_err(
3560            "unsupported devtype for CUDA (only 0=stddev)",
3561        ));
3562    }
3563    let slice_tm = data_tm_f32.as_slice()?;
3564    let params = DeviationParams {
3565        period: Some(period),
3566        devtype: Some(devtype),
3567    };
3568    let inner = py.allow_threads(|| {
3569        let cuda =
3570            CudaDeviation::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
3571        cuda.deviation_many_series_one_param_time_major_dev(slice_tm, cols, rows, &params)
3572            .map_err(|e| PyValueError::new_err(e.to_string()))
3573    })?;
3574    make_device_array_py(device_id, inner)
3575}
3576
3577#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3578#[wasm_bindgen]
3579pub fn deviation_js(data: &[f64], period: usize, devtype: usize) -> Result<Vec<f64>, JsValue> {
3580    let params = DeviationParams {
3581        period: Some(period),
3582        devtype: Some(devtype),
3583    };
3584    let input = DeviationInput::from_slice(data, params);
3585    let mut out = vec![0.0; data.len()];
3586    deviation_into_slice(&mut out, &input, detect_best_kernel())
3587        .map_err(|e| JsValue::from_str(&e.to_string()))?;
3588    Ok(out)
3589}
3590
3591#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3592#[derive(Serialize, Deserialize)]
3593pub struct DeviationBatchConfig {
3594    pub period_range: (usize, usize, usize),
3595    pub devtype_range: (usize, usize, usize),
3596}
3597
3598#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3599#[derive(Serialize, Deserialize)]
3600pub struct DeviationBatchJsOutput {
3601    pub values: Vec<f64>,
3602    pub combos: usize,
3603    pub rows: usize,
3604    pub cols: usize,
3605}
3606
3607#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3608#[wasm_bindgen(js_name = deviation_batch)]
3609pub fn deviation_batch_unified_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
3610    let cfg: DeviationBatchConfig = serde_wasm_bindgen::from_value(config)
3611        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
3612    let sweep = DeviationBatchRange {
3613        period: cfg.period_range,
3614        devtype: cfg.devtype_range,
3615    };
3616    let out = deviation_batch_inner(data, &sweep, detect_best_kernel(), false)
3617        .map_err(|e| JsValue::from_str(&e.to_string()))?;
3618    let js_out = DeviationBatchJsOutput {
3619        values: out.values,
3620        combos: out.combos.len(),
3621        rows: out.rows,
3622        cols: out.cols,
3623    };
3624    serde_wasm_bindgen::to_value(&js_out)
3625        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
3626}
3627
3628#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3629#[wasm_bindgen]
3630pub fn deviation_batch_metadata(
3631    period_start: usize,
3632    period_end: usize,
3633    period_step: usize,
3634    devtype_start: usize,
3635    devtype_end: usize,
3636    devtype_step: usize,
3637) -> Vec<f64> {
3638    let sweep = DeviationBatchRange {
3639        period: (period_start, period_end, period_step),
3640        devtype: (devtype_start, devtype_end, devtype_step),
3641    };
3642
3643    let combos = expand_grid(&sweep);
3644    let mut metadata = Vec::with_capacity(combos.len() * 2);
3645
3646    for combo in combos {
3647        metadata.push(combo.period.unwrap() as f64);
3648        metadata.push(combo.devtype.unwrap() as f64);
3649    }
3650
3651    metadata
3652}
3653
3654#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3655#[wasm_bindgen]
3656pub fn deviation_alloc(len: usize) -> *mut f64 {
3657    let mut vec = Vec::<f64>::with_capacity(len);
3658    let ptr = vec.as_mut_ptr();
3659    std::mem::forget(vec);
3660    ptr
3661}
3662
3663#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3664#[wasm_bindgen]
3665pub fn deviation_free(ptr: *mut f64, len: usize) {
3666    if !ptr.is_null() {
3667        unsafe {
3668            let _ = Vec::from_raw_parts(ptr, len, len);
3669        }
3670    }
3671}
3672
3673#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3674#[wasm_bindgen]
3675pub fn deviation_into(
3676    in_ptr: *const f64,
3677    len: usize,
3678    period: usize,
3679    devtype: usize,
3680    out_ptr: *mut f64,
3681) -> Result<(), JsValue> {
3682    if in_ptr.is_null() || out_ptr.is_null() {
3683        return Err(JsValue::from_str("null pointer passed to deviation_into"));
3684    }
3685    unsafe {
3686        let data = std::slice::from_raw_parts(in_ptr, len);
3687        let params = DeviationParams {
3688            period: Some(period),
3689            devtype: Some(devtype),
3690        };
3691        let input = DeviationInput::from_slice(data, params);
3692        if in_ptr as *const u8 == out_ptr as *const u8 {
3693            let mut tmp = vec![0.0; len];
3694            deviation_into_slice(&mut tmp, &input, detect_best_kernel())
3695                .map_err(|e| JsValue::from_str(&e.to_string()))?;
3696            let out = std::slice::from_raw_parts_mut(out_ptr, len);
3697            out.copy_from_slice(&tmp);
3698        } else {
3699            let out = std::slice::from_raw_parts_mut(out_ptr, len);
3700            deviation_into_slice(out, &input, detect_best_kernel())
3701                .map_err(|e| JsValue::from_str(&e.to_string()))?;
3702        }
3703        Ok(())
3704    }
3705}
3706
3707#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3708#[wasm_bindgen]
3709pub fn deviation_batch_into(
3710    in_ptr: *const f64,
3711    out_ptr: *mut f64,
3712    len: usize,
3713    period_start: usize,
3714    period_end: usize,
3715    period_step: usize,
3716    devtype_start: usize,
3717    devtype_end: usize,
3718    devtype_step: usize,
3719) -> Result<usize, JsValue> {
3720    if in_ptr.is_null() || out_ptr.is_null() {
3721        return Err(JsValue::from_str(
3722            "null pointer passed to deviation_batch_into",
3723        ));
3724    }
3725    unsafe {
3726        let data = std::slice::from_raw_parts(in_ptr, len);
3727        let sweep = DeviationBatchRange {
3728            period: (period_start, period_end, period_step),
3729            devtype: (devtype_start, devtype_end, devtype_step),
3730        };
3731        let combos = expand_grid(&sweep);
3732        let rows = combos.len();
3733        let cols = len;
3734        let total = rows
3735            .checked_mul(cols)
3736            .ok_or_else(|| JsValue::from_str("rows*cols overflow in deviation_batch_into"))?;
3737        let out = std::slice::from_raw_parts_mut(out_ptr, total);
3738        deviation_batch_inner_into(data, &sweep, detect_best_kernel(), false, out)
3739            .map_err(|e| JsValue::from_str(&e.to_string()))?;
3740        Ok(rows)
3741    }
3742}