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, ¶ms)
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}