Skip to main content

vector_ta/indicators/
dec_osc.rs

1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
10use serde::{Deserialize, Serialize};
11#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
12use wasm_bindgen::prelude::*;
13
14use crate::utilities::data_loader::{source_type, Candles};
15use crate::utilities::enums::Kernel;
16use crate::utilities::helpers::{
17    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
18    make_uninit_matrix,
19};
20#[cfg(feature = "python")]
21use crate::utilities::kernel_validation::validate_kernel;
22use aligned_vec::{AVec, CACHELINE_ALIGN};
23#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
24use core::arch::x86_64::*;
25#[cfg(not(target_arch = "wasm32"))]
26use rayon::prelude::*;
27use std::convert::AsRef;
28use std::f64::consts::PI;
29use std::mem::MaybeUninit;
30use thiserror::Error;
31
32impl<'a> AsRef<[f64]> for DecOscInput<'a> {
33    #[inline(always)]
34    fn as_ref(&self) -> &[f64] {
35        match &self.data {
36            DecOscData::Slice(slice) => slice,
37            DecOscData::Candles { candles, source } => source_type(candles, source),
38        }
39    }
40}
41
42#[derive(Debug, Clone)]
43pub enum DecOscData<'a> {
44    Candles {
45        candles: &'a Candles,
46        source: &'a str,
47    },
48    Slice(&'a [f64]),
49}
50
51#[derive(Debug, Clone)]
52pub struct DecOscOutput {
53    pub values: Vec<f64>,
54}
55
56#[derive(Debug, Clone)]
57#[cfg_attr(
58    all(target_arch = "wasm32", feature = "wasm"),
59    derive(Serialize, Deserialize)
60)]
61pub struct DecOscParams {
62    pub hp_period: Option<usize>,
63    pub k: Option<f64>,
64}
65
66impl Default for DecOscParams {
67    fn default() -> Self {
68        Self {
69            hp_period: Some(125),
70            k: Some(1.0),
71        }
72    }
73}
74
75#[derive(Debug, Clone)]
76pub struct DecOscInput<'a> {
77    pub data: DecOscData<'a>,
78    pub params: DecOscParams,
79}
80
81impl<'a> DecOscInput<'a> {
82    #[inline]
83    pub fn from_candles(c: &'a Candles, s: &'a str, p: DecOscParams) -> Self {
84        Self {
85            data: DecOscData::Candles {
86                candles: c,
87                source: s,
88            },
89            params: p,
90        }
91    }
92    #[inline]
93    pub fn from_slice(sl: &'a [f64], p: DecOscParams) -> Self {
94        Self {
95            data: DecOscData::Slice(sl),
96            params: p,
97        }
98    }
99    #[inline]
100    pub fn with_default_candles(c: &'a Candles) -> Self {
101        Self::from_candles(c, "close", DecOscParams::default())
102    }
103    #[inline]
104    pub fn get_hp_period(&self) -> usize {
105        self.params.hp_period.unwrap_or(125)
106    }
107    #[inline]
108    pub fn get_k(&self) -> f64 {
109        self.params.k.unwrap_or(1.0)
110    }
111}
112
113#[derive(Copy, Clone, Debug)]
114pub struct DecOscBuilder {
115    hp_period: Option<usize>,
116    k: Option<f64>,
117    kernel: Kernel,
118}
119
120impl Default for DecOscBuilder {
121    fn default() -> Self {
122        Self {
123            hp_period: None,
124            k: None,
125            kernel: Kernel::Auto,
126        }
127    }
128}
129
130impl DecOscBuilder {
131    #[inline(always)]
132    pub fn new() -> Self {
133        Self::default()
134    }
135    #[inline(always)]
136    pub fn hp_period(mut self, n: usize) -> Self {
137        self.hp_period = Some(n);
138        self
139    }
140    #[inline(always)]
141    pub fn k(mut self, v: f64) -> Self {
142        self.k = Some(v);
143        self
144    }
145    #[inline(always)]
146    pub fn kernel(mut self, k: Kernel) -> Self {
147        self.kernel = k;
148        self
149    }
150
151    #[inline(always)]
152    pub fn apply(self, c: &Candles) -> Result<DecOscOutput, DecOscError> {
153        let p = DecOscParams {
154            hp_period: self.hp_period,
155            k: self.k,
156        };
157        let i = DecOscInput::from_candles(c, "close", p);
158        dec_osc_with_kernel(&i, self.kernel)
159    }
160
161    #[inline(always)]
162    pub fn apply_slice(self, d: &[f64]) -> Result<DecOscOutput, DecOscError> {
163        let p = DecOscParams {
164            hp_period: self.hp_period,
165            k: self.k,
166        };
167        let i = DecOscInput::from_slice(d, p);
168        dec_osc_with_kernel(&i, self.kernel)
169    }
170
171    #[inline(always)]
172    pub fn into_stream(self) -> Result<DecOscStream, DecOscError> {
173        let p = DecOscParams {
174            hp_period: self.hp_period,
175            k: self.k,
176        };
177        DecOscStream::try_new(p)
178    }
179}
180
181#[derive(Debug, Error)]
182pub enum DecOscError {
183    #[error("dec_osc: Input data slice is empty.")]
184    EmptyInputData,
185
186    #[error("dec_osc: All values are NaN.")]
187    AllValuesNaN,
188
189    #[error("dec_osc: Invalid period: period = {period}, data length = {data_len}")]
190    InvalidPeriod { period: usize, data_len: usize },
191
192    #[error("dec_osc: Not enough valid data: needed = {needed}, valid = {valid}")]
193    NotEnoughValidData { needed: usize, valid: usize },
194
195    #[error("dec_osc: Invalid K: k = {k}")]
196    InvalidK { k: f64 },
197
198    #[error("dec_osc: output length mismatch: expected = {expected}, got = {got}")]
199    OutputLengthMismatch { expected: usize, got: usize },
200
201    #[error("dec_osc: invalid range: start={start}, end={end}, step={step}")]
202    InvalidRange {
203        start: usize,
204        end: usize,
205        step: usize,
206    },
207
208    #[error("dec_osc: invalid kernel for batch: {0:?}")]
209    InvalidKernelForBatch(Kernel),
210}
211
212#[inline]
213pub fn dec_osc(input: &DecOscInput) -> Result<DecOscOutput, DecOscError> {
214    dec_osc_with_kernel(input, Kernel::Auto)
215}
216
217#[inline(always)]
218fn dec_osc_prepare<'a>(
219    input: &'a DecOscInput,
220    kernel: Kernel,
221) -> Result<(&'a [f64], usize, f64, usize, Kernel), DecOscError> {
222    let data: &[f64] = input.as_ref();
223    let len = data.len();
224    if len == 0 {
225        return Err(DecOscError::EmptyInputData);
226    }
227
228    let first = data
229        .iter()
230        .position(|x| !x.is_nan())
231        .ok_or(DecOscError::AllValuesNaN)?;
232    let period = input.get_hp_period();
233    let k_val = input.get_k();
234
235    if period < 3 || period > len {
236        return Err(DecOscError::InvalidPeriod {
237            period,
238            data_len: len,
239        });
240    }
241    if (len - first) < 2 {
242        return Err(DecOscError::NotEnoughValidData {
243            needed: 2,
244            valid: len - first,
245        });
246    }
247    if k_val <= 0.0 || k_val.is_nan() {
248        return Err(DecOscError::InvalidK { k: k_val });
249    }
250
251    let chosen = match kernel {
252        Kernel::Auto => Kernel::Scalar,
253        other => other,
254    };
255
256    Ok((data, period, k_val, first, chosen))
257}
258
259pub fn dec_osc_with_kernel(
260    input: &DecOscInput,
261    kernel: Kernel,
262) -> Result<DecOscOutput, DecOscError> {
263    let (data, period, k_val, first, chosen) = dec_osc_prepare(input, kernel)?;
264
265    let mut out = alloc_with_nan_prefix(data.len(), first + 2);
266
267    unsafe {
268        match chosen {
269            Kernel::Scalar | Kernel::ScalarBatch => {
270                dec_osc_scalar(data, period, k_val, first, &mut out)
271            }
272            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
273            Kernel::Avx2 | Kernel::Avx2Batch => dec_osc_avx2(data, period, k_val, first, &mut out),
274            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
275            Kernel::Avx512 | Kernel::Avx512Batch => {
276                dec_osc_avx512(data, period, k_val, first, &mut out)
277            }
278            _ => unreachable!(),
279        }
280    }
281
282    Ok(DecOscOutput { values: out })
283}
284
285#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
286#[inline]
287pub fn dec_osc_into(out: &mut [f64], input: &DecOscInput) -> Result<(), DecOscError> {
288    dec_osc_into_slice(out, input, Kernel::Auto)
289}
290
291#[inline]
292pub fn dec_osc_into_slice(
293    dst: &mut [f64],
294    input: &DecOscInput,
295    kern: Kernel,
296) -> Result<(), DecOscError> {
297    let (data, period, k_val, first, chosen) = dec_osc_prepare(input, kern)?;
298
299    if dst.len() != data.len() {
300        return Err(DecOscError::OutputLengthMismatch {
301            expected: data.len(),
302            got: dst.len(),
303        });
304    }
305
306    unsafe {
307        match chosen {
308            Kernel::Scalar | Kernel::ScalarBatch => dec_osc_scalar(data, period, k_val, first, dst),
309            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
310            Kernel::Avx2 | Kernel::Avx2Batch => dec_osc_avx2(data, period, k_val, first, dst),
311            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
312            Kernel::Avx512 | Kernel::Avx512Batch => dec_osc_avx512(data, period, k_val, first, dst),
313            _ => unreachable!(),
314        }
315    }
316
317    let warmup_end = first + 2;
318    for v in &mut dst[..warmup_end] {
319        *v = f64::NAN;
320    }
321
322    Ok(())
323}
324
325#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
326#[inline]
327pub fn dec_osc_avx512(data: &[f64], period: usize, k_val: f64, first: usize, out: &mut [f64]) {
328    if period <= 32 {
329        unsafe { dec_osc_avx512_short(data, period, k_val, first, out) }
330    } else {
331        unsafe { dec_osc_avx512_long(data, period, k_val, first, out) }
332    }
333}
334
335#[inline]
336pub fn dec_osc_scalar(data: &[f64], period: usize, k_val: f64, first: usize, out: &mut [f64]) {
337    assert!(
338        out.len() >= data.len(),
339        "`out` must be at least as long as `data`"
340    );
341
342    let len = data.len();
343    if len == 0 || first + 1 >= len {
344        return;
345    }
346
347    let p = period as f64;
348    let half_p = p * 0.5;
349
350    let angle1 = 2.0 * PI * 0.707 / p;
351    let (sin1, cos1) = angle1.sin_cos();
352    let alpha1 = 1.0 + ((sin1 - 1.0) / cos1);
353    let t1 = 1.0 - alpha1 * 0.5;
354    let c1 = t1 * t1;
355    let oma1 = 1.0 - alpha1;
356    let two_oma1 = oma1 + oma1;
357    let oma1_sq = oma1 * oma1;
358
359    let angle2 = 2.0 * PI * 0.707 / half_p;
360    let (sin2, cos2) = angle2.sin_cos();
361    let alpha2 = 1.0 + ((sin2 - 1.0) / cos2);
362    let t2 = 1.0 - alpha2 * 0.5;
363    let c2 = t2 * t2;
364    let oma2 = 1.0 - alpha2;
365    let two_oma2 = oma2 + oma2;
366    let oma2_sq = oma2 * oma2;
367
368    let scale = 100.0 * k_val;
369
370    out[first] = f64::NAN;
371    out[first + 1] = f64::NAN;
372
373    let mut x2 = data[first];
374    let mut x1 = data[first + 1];
375    let mut hp_prev_2 = x2;
376    let mut hp_prev_1 = x1;
377    let mut decosc_prev_2 = 0.0f64;
378    let mut decosc_prev_1 = 0.0f64;
379
380    for i in (first + 2)..len {
381        let d0 = data[i];
382
383        let dx = d0 - 2.0 * x1 + x2;
384        let hp0 = c1 * dx + two_oma1 * hp_prev_1 - oma1_sq * hp_prev_2;
385
386        let dec = d0 - hp0;
387        let d_dec1 = x1 - hp_prev_1;
388        let d_dec2 = x2 - hp_prev_2;
389        let decdx = dec - 2.0 * d_dec1 + d_dec2;
390        let osc0 = c2 * decdx + two_oma2 * decosc_prev_1 - oma2_sq * decosc_prev_2;
391
392        out[i] = scale * osc0 / d0;
393
394        hp_prev_2 = hp_prev_1;
395        hp_prev_1 = hp0;
396        decosc_prev_2 = decosc_prev_1;
397        decosc_prev_1 = osc0;
398        x2 = x1;
399        x1 = d0;
400    }
401}
402
403#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
404#[inline]
405pub unsafe fn dec_osc_avx2(data: &[f64], period: usize, k_val: f64, first: usize, out: &mut [f64]) {
406    dec_osc_scalar(data, period, k_val, first, out)
407}
408
409#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
410#[inline]
411pub unsafe fn dec_osc_avx512_short(
412    data: &[f64],
413    period: usize,
414    k_val: f64,
415    first: usize,
416    out: &mut [f64],
417) {
418    dec_osc_scalar(data, period, k_val, first, out)
419}
420
421#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
422#[inline]
423pub unsafe fn dec_osc_avx512_long(
424    data: &[f64],
425    period: usize,
426    k_val: f64,
427    first: usize,
428    out: &mut [f64],
429) {
430    dec_osc_scalar(data, period, k_val, first, out)
431}
432
433#[inline(always)]
434pub fn dec_osc_batch_with_kernel(
435    data: &[f64],
436    sweep: &DecOscBatchRange,
437    k: Kernel,
438) -> Result<DecOscBatchOutput, DecOscError> {
439    let kernel = match k {
440        Kernel::Auto => detect_best_batch_kernel(),
441        other if other.is_batch() => other,
442        other => return Err(DecOscError::InvalidKernelForBatch(other)),
443    };
444
445    let simd = match kernel {
446        Kernel::Avx512Batch => Kernel::Avx512,
447        Kernel::Avx2Batch => Kernel::Avx2,
448        Kernel::ScalarBatch => Kernel::Scalar,
449        _ => unreachable!(),
450    };
451    dec_osc_batch_par_slice(data, sweep, simd)
452}
453
454#[derive(Clone, Debug)]
455pub struct DecOscBatchRange {
456    pub hp_period: (usize, usize, usize),
457    pub k: (f64, f64, f64),
458}
459
460impl Default for DecOscBatchRange {
461    fn default() -> Self {
462        Self {
463            hp_period: (125, 374, 1),
464            k: (1.0, 1.0, 0.0),
465        }
466    }
467}
468
469#[derive(Clone, Debug, Default)]
470pub struct DecOscBatchBuilder {
471    range: DecOscBatchRange,
472    kernel: Kernel,
473}
474
475impl DecOscBatchBuilder {
476    pub fn new() -> Self {
477        Self::default()
478    }
479    pub fn kernel(mut self, k: Kernel) -> Self {
480        self.kernel = k;
481        self
482    }
483    #[inline]
484    pub fn hp_period_range(mut self, start: usize, end: usize, step: usize) -> Self {
485        self.range.hp_period = (start, end, step);
486        self
487    }
488    #[inline]
489    pub fn hp_period_static(mut self, p: usize) -> Self {
490        self.range.hp_period = (p, p, 0);
491        self
492    }
493    #[inline]
494    pub fn k_range(mut self, start: f64, end: f64, step: f64) -> Self {
495        self.range.k = (start, end, step);
496        self
497    }
498    #[inline]
499    pub fn k_static(mut self, x: f64) -> Self {
500        self.range.k = (x, x, 0.0);
501        self
502    }
503
504    pub fn apply_slice(self, data: &[f64]) -> Result<DecOscBatchOutput, DecOscError> {
505        dec_osc_batch_with_kernel(data, &self.range, self.kernel)
506    }
507
508    pub fn with_default_slice(data: &[f64], k: Kernel) -> Result<DecOscBatchOutput, DecOscError> {
509        DecOscBatchBuilder::new().kernel(k).apply_slice(data)
510    }
511
512    pub fn apply_candles(self, c: &Candles, src: &str) -> Result<DecOscBatchOutput, DecOscError> {
513        let slice = source_type(c, src);
514        self.apply_slice(slice)
515    }
516
517    pub fn with_default_candles(c: &Candles) -> Result<DecOscBatchOutput, DecOscError> {
518        DecOscBatchBuilder::new()
519            .kernel(Kernel::Auto)
520            .apply_candles(c, "close")
521    }
522}
523
524#[derive(Clone, Debug)]
525pub struct DecOscBatchOutput {
526    pub values: Vec<f64>,
527    pub combos: Vec<DecOscParams>,
528    pub rows: usize,
529    pub cols: usize,
530}
531impl DecOscBatchOutput {
532    pub fn row_for_params(&self, p: &DecOscParams) -> Option<usize> {
533        self.combos.iter().position(|c| {
534            c.hp_period.unwrap_or(125) == p.hp_period.unwrap_or(125)
535                && (c.k.unwrap_or(1.0) - p.k.unwrap_or(1.0)).abs() < 1e-12
536        })
537    }
538
539    pub fn values_for(&self, p: &DecOscParams) -> Option<&[f64]> {
540        self.row_for_params(p).map(|row| {
541            let start = row * self.cols;
542            &self.values[start..start + self.cols]
543        })
544    }
545}
546
547#[inline(always)]
548fn expand_grid_checked(r: &DecOscBatchRange) -> Result<Vec<DecOscParams>, DecOscError> {
549    fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, DecOscError> {
550        if step == 0 || start == end {
551            return Ok(vec![start]);
552        }
553        let mut out = Vec::new();
554        if start < end {
555            let mut v = start;
556            while v <= end {
557                out.push(v);
558                v = match v.checked_add(step) {
559                    Some(n) if n != v => n,
560                    _ => break,
561                };
562            }
563        } else {
564            let mut v = start;
565            while v >= end {
566                out.push(v);
567                if v < end + step {
568                    break;
569                }
570                v -= step;
571                if v == 0 {
572                    break;
573                }
574            }
575        }
576        if out.is_empty() {
577            return Err(DecOscError::InvalidRange { start, end, step });
578        }
579        Ok(out)
580    }
581    fn axis_f64((start, end, step): (f64, f64, f64)) -> Vec<f64> {
582        if step.abs() < 1e-12 || (start - end).abs() < 1e-12 {
583            return vec![start];
584        }
585        let mut v = Vec::new();
586        if start <= end {
587            let mut x = start;
588            while x <= end + 1e-12 {
589                v.push(x);
590                x += step;
591            }
592        } else {
593            let mut x = start;
594            while x >= end - 1e-12 {
595                v.push(x);
596                x -= step.abs();
597            }
598        }
599        v
600    }
601
602    let periods = axis_usize(r.hp_period)?;
603    let ks = axis_f64(r.k);
604    let cap = periods
605        .len()
606        .checked_mul(ks.len())
607        .ok_or(DecOscError::InvalidRange {
608            start: r.hp_period.0,
609            end: r.hp_period.1,
610            step: r.hp_period.2,
611        })?;
612    let mut out = Vec::with_capacity(cap);
613    for &p in &periods {
614        for &k in &ks {
615            out.push(DecOscParams {
616                hp_period: Some(p),
617                k: Some(k),
618            });
619        }
620    }
621    Ok(out)
622}
623
624#[inline(always)]
625pub fn dec_osc_batch_slice(
626    data: &[f64],
627    sweep: &DecOscBatchRange,
628    kern: Kernel,
629) -> Result<DecOscBatchOutput, DecOscError> {
630    dec_osc_batch_inner(data, sweep, kern, false)
631}
632
633#[inline(always)]
634pub fn dec_osc_batch_par_slice(
635    data: &[f64],
636    sweep: &DecOscBatchRange,
637    kern: Kernel,
638) -> Result<DecOscBatchOutput, DecOscError> {
639    dec_osc_batch_inner(data, sweep, kern, true)
640}
641
642#[inline(always)]
643fn dec_osc_batch_inner(
644    data: &[f64],
645    sweep: &DecOscBatchRange,
646    kern: Kernel,
647    parallel: bool,
648) -> Result<DecOscBatchOutput, DecOscError> {
649    let combos = expand_grid_checked(sweep)?;
650
651    let first = data
652        .iter()
653        .position(|x| !x.is_nan())
654        .ok_or(DecOscError::AllValuesNaN)?;
655    let max_p = combos.iter().map(|c| c.hp_period.unwrap()).max().unwrap();
656    if data.len() - first < 2 {
657        return Err(DecOscError::NotEnoughValidData {
658            needed: 2,
659            valid: data.len() - first,
660        });
661    }
662
663    let rows = combos.len();
664    let cols = data.len();
665    let _total = rows.checked_mul(cols).ok_or(DecOscError::InvalidRange {
666        start: rows,
667        end: cols,
668        step: 0,
669    })?;
670
671    let mut buf_mu = make_uninit_matrix(rows, cols);
672
673    let warmup_periods: Vec<usize> = combos.iter().map(|_| first + 2).collect();
674
675    init_matrix_prefixes(&mut buf_mu, cols, &warmup_periods);
676
677    let mut buf_guard = core::mem::ManuallyDrop::new(buf_mu);
678    let out: &mut [f64] = unsafe {
679        core::slice::from_raw_parts_mut(buf_guard.as_mut_ptr() as *mut f64, buf_guard.len())
680    };
681
682    let do_row = |row: usize, out_row: &mut [f64]| unsafe {
683        let period = combos[row].hp_period.unwrap();
684        let k_val = combos[row].k.unwrap();
685        match kern {
686            Kernel::Scalar => dec_osc_row_scalar(data, first, period, k_val, out_row),
687            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
688            Kernel::Avx2 => dec_osc_row_avx2(data, first, period, k_val, out_row),
689            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
690            Kernel::Avx512 => dec_osc_row_avx512(data, first, period, k_val, out_row),
691            _ => unreachable!(),
692        }
693    };
694
695    if parallel {
696        #[cfg(not(target_arch = "wasm32"))]
697        {
698            out.par_chunks_mut(cols)
699                .enumerate()
700                .for_each(|(row, slice)| do_row(row, slice));
701        }
702
703        #[cfg(target_arch = "wasm32")]
704        {
705            for (row, slice) in out.chunks_mut(cols).enumerate() {
706                do_row(row, slice);
707            }
708        }
709    } else {
710        for (row, slice) in out.chunks_mut(cols).enumerate() {
711            do_row(row, slice);
712        }
713    }
714
715    let values = unsafe {
716        Vec::from_raw_parts(
717            buf_guard.as_mut_ptr() as *mut f64,
718            buf_guard.len(),
719            buf_guard.capacity(),
720        )
721    };
722    core::mem::forget(buf_guard);
723
724    Ok(DecOscBatchOutput {
725        values,
726        combos,
727        rows,
728        cols,
729    })
730}
731
732#[inline(always)]
733pub fn dec_osc_batch_inner_into(
734    data: &[f64],
735    sweep: &DecOscBatchRange,
736    kern: Kernel,
737    parallel: bool,
738    out: &mut [f64],
739) -> Result<Vec<DecOscParams>, DecOscError> {
740    let combos = expand_grid_checked(sweep)?;
741
742    let first = data
743        .iter()
744        .position(|x| !x.is_nan())
745        .ok_or(DecOscError::AllValuesNaN)?;
746    let max_p = combos.iter().map(|c| c.hp_period.unwrap()).max().unwrap();
747    if data.len() - first < 2 {
748        return Err(DecOscError::NotEnoughValidData {
749            needed: 2,
750            valid: data.len() - first,
751        });
752    }
753
754    let rows = combos.len();
755    let cols = data.len();
756
757    let expected = rows.checked_mul(cols).ok_or(DecOscError::InvalidRange {
758        start: rows,
759        end: cols,
760        step: 0,
761    })?;
762    if out.len() != expected {
763        return Err(DecOscError::OutputLengthMismatch {
764            expected,
765            got: out.len(),
766        });
767    }
768
769    let warmups: Vec<usize> = combos.iter().map(|_| first + 2).collect();
770
771    let out_mu: &mut [MaybeUninit<f64>] = unsafe {
772        core::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut MaybeUninit<f64>, out.len())
773    };
774    init_matrix_prefixes(out_mu, cols, &warmups);
775
776    let do_row = |row: usize, dst_mu: &mut [MaybeUninit<f64>]| unsafe {
777        let dst: &mut [f64] =
778            core::slice::from_raw_parts_mut(dst_mu.as_mut_ptr() as *mut f64, dst_mu.len());
779        let period = combos[row].hp_period.unwrap();
780        let k_val = combos[row].k.unwrap();
781        match kern {
782            Kernel::Scalar => dec_osc_row_scalar(data, first, period, k_val, dst),
783            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
784            Kernel::Avx2 => dec_osc_row_avx2(data, first, period, k_val, dst),
785            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
786            Kernel::Avx512 => dec_osc_row_avx512(data, first, period, k_val, dst),
787            _ => unreachable!(),
788        }
789    };
790
791    if parallel {
792        #[cfg(not(target_arch = "wasm32"))]
793        {
794            out_mu
795                .par_chunks_mut(cols)
796                .enumerate()
797                .for_each(|(row, slice)| do_row(row, slice));
798        }
799
800        #[cfg(target_arch = "wasm32")]
801        {
802            for (row, slice) in out_mu.chunks_mut(cols).enumerate() {
803                do_row(row, slice);
804            }
805        }
806    } else {
807        for (row, slice) in out_mu.chunks_mut(cols).enumerate() {
808            do_row(row, slice);
809        }
810    }
811
812    Ok(combos)
813}
814
815#[inline(always)]
816unsafe fn dec_osc_row_scalar(
817    data: &[f64],
818    first: usize,
819    period: usize,
820    k_val: f64,
821    out: &mut [f64],
822) {
823    dec_osc_scalar(data, period, k_val, first, out)
824}
825
826#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
827#[inline(always)]
828unsafe fn dec_osc_row_avx2(data: &[f64], first: usize, period: usize, k_val: f64, out: &mut [f64]) {
829    dec_osc_scalar(data, period, k_val, first, out)
830}
831
832#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
833#[inline(always)]
834pub unsafe fn dec_osc_row_avx512(
835    data: &[f64],
836    first: usize,
837    period: usize,
838    k_val: f64,
839    out: &mut [f64],
840) {
841    if period <= 32 {
842        dec_osc_row_avx512_short(data, first, period, k_val, out)
843    } else {
844        dec_osc_row_avx512_long(data, first, period, k_val, out)
845    }
846}
847
848#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
849#[inline(always)]
850pub unsafe fn dec_osc_row_avx512_short(
851    data: &[f64],
852    first: usize,
853    period: usize,
854    k_val: f64,
855    out: &mut [f64],
856) {
857    dec_osc_scalar(data, period, k_val, first, out)
858}
859
860#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
861#[inline(always)]
862pub unsafe fn dec_osc_row_avx512_long(
863    data: &[f64],
864    first: usize,
865    period: usize,
866    k_val: f64,
867    out: &mut [f64],
868) {
869    dec_osc_scalar(data, period, k_val, first, out)
870}
871
872#[derive(Debug, Clone)]
873pub struct DecOscStream {
874    period: usize,
875    scale: f64,
876
877    c1: f64,
878    two_oma1: f64,
879    oma1_sq: f64,
880
881    c2: f64,
882    two_oma2: f64,
883    oma2_sq: f64,
884
885    x1: f64,
886    x2: f64,
887
888    hp1: f64,
889    hp2: f64,
890
891    dec1: f64,
892    dec2: f64,
893
894    osc1: f64,
895    osc2: f64,
896
897    idx: usize,
898}
899
900impl DecOscStream {
901    #[inline(always)]
902    pub fn try_new(params: DecOscParams) -> Result<Self, DecOscError> {
903        let period = params.hp_period.unwrap_or(125);
904        if period < 2 {
905            return Err(DecOscError::InvalidPeriod {
906                period,
907                data_len: 0,
908            });
909        }
910        let k = params.k.unwrap_or(1.0);
911        if k <= 0.0 || k.is_nan() {
912            return Err(DecOscError::InvalidK { k });
913        }
914
915        let p = period as f64;
916        let angle1 = 2.0 * std::f64::consts::PI * 0.707 / p;
917        let (sin1, cos1) = angle1.sin_cos();
918        let alpha1 = 1.0 + ((sin1 - 1.0) / cos1);
919        let t1 = 1.0 - 0.5 * alpha1;
920        let c1 = t1 * t1;
921        let oma1 = 1.0 - alpha1;
922        let two_oma1 = oma1 + oma1;
923        let oma1_sq = oma1 * oma1;
924
925        let half_p = 0.5 * p;
926        let angle2 = 2.0 * std::f64::consts::PI * 0.707 / half_p;
927        let (sin2, cos2) = angle2.sin_cos();
928        let alpha2 = 1.0 + ((sin2 - 1.0) / cos2);
929        let t2 = 1.0 - 0.5 * alpha2;
930        let c2 = t2 * t2;
931        let oma2 = 1.0 - alpha2;
932        let two_oma2 = oma2 + oma2;
933        let oma2_sq = oma2 * oma2;
934
935        Ok(Self {
936            period,
937            scale: 100.0 * k,
938
939            c1,
940            two_oma1,
941            oma1_sq,
942
943            c2,
944            two_oma2,
945            oma2_sq,
946
947            x1: f64::NAN,
948            x2: f64::NAN,
949
950            hp1: f64::NAN,
951            hp2: f64::NAN,
952
953            dec1: 0.0,
954            dec2: 0.0,
955
956            osc1: 0.0,
957            osc2: 0.0,
958
959            idx: 0,
960        })
961    }
962
963    #[inline(always)]
964    pub fn update(&mut self, value: f64) -> Option<f64> {
965        if self.idx == 0 {
966            self.idx = 1;
967            self.x2 = value;
968            self.x1 = value;
969            self.hp2 = value;
970            self.hp1 = value;
971
972            return None;
973        }
974
975        if self.idx == 1 {
976            self.idx = 2;
977            self.x2 = self.x1;
978            self.x1 = value;
979            self.hp2 = self.hp1;
980            self.hp1 = value;
981
982            return None;
983        }
984
985        let dx = value - self.x1 - self.x1 + self.x2;
986
987        let hp = (-self.oma1_sq).mul_add(self.hp2, self.c1.mul_add(dx, self.two_oma1 * self.hp1));
988
989        let dec = value - hp;
990
991        let decdx = dec - self.dec1 - self.dec1 + self.dec2;
992        let osc =
993            (-self.oma2_sq).mul_add(self.osc2, self.c2.mul_add(decdx, self.two_oma2 * self.osc1));
994
995        let out = (self.scale * osc) / value;
996
997        self.hp2 = self.hp1;
998        self.hp1 = hp;
999
1000        self.dec2 = self.dec1;
1001        self.dec1 = dec;
1002
1003        self.osc2 = self.osc1;
1004        self.osc1 = osc;
1005
1006        self.x2 = self.x1;
1007        self.x1 = value;
1008
1009        Some(out)
1010    }
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015    use super::*;
1016    use crate::skip_if_unsupported;
1017    use crate::utilities::data_loader::read_candles_from_csv;
1018
1019    fn check_dec_osc_partial_params(
1020        test_name: &str,
1021        kernel: Kernel,
1022    ) -> Result<(), Box<dyn std::error::Error>> {
1023        skip_if_unsupported!(kernel, test_name);
1024        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1025        let candles = read_candles_from_csv(file_path)?;
1026
1027        let default_params = DecOscParams {
1028            hp_period: None,
1029            k: None,
1030        };
1031        let input = DecOscInput::from_candles(&candles, "close", default_params);
1032        let output = dec_osc_with_kernel(&input, kernel)?;
1033        assert_eq!(output.values.len(), candles.close.len());
1034
1035        Ok(())
1036    }
1037
1038    fn check_dec_osc_accuracy(
1039        test_name: &str,
1040        kernel: Kernel,
1041    ) -> Result<(), Box<dyn std::error::Error>> {
1042        skip_if_unsupported!(kernel, test_name);
1043        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1044        let candles = read_candles_from_csv(file_path)?;
1045        let input = DecOscInput::from_candles(&candles, "close", DecOscParams::default());
1046        let result = dec_osc_with_kernel(&input, kernel)?;
1047
1048        if result.values.len() > 5 {
1049            let expected_last_five = [
1050                -1.5036367540303395,
1051                -1.4037875172207006,
1052                -1.3174199471429475,
1053                -1.2245874070642693,
1054                -1.1638422627265639,
1055            ];
1056            let start = result.values.len().saturating_sub(5);
1057            for (i, &val) in result.values[start..].iter().enumerate() {
1058                let diff = (val - expected_last_five[i]).abs();
1059                assert!(
1060                    diff < 1e-7,
1061                    "[{}] DEC_OSC {:?} mismatch at idx {}: got {}, expected {}",
1062                    test_name,
1063                    kernel,
1064                    i,
1065                    val,
1066                    expected_last_five[i]
1067                );
1068            }
1069        }
1070        Ok(())
1071    }
1072
1073    fn check_dec_osc_default_candles(
1074        test_name: &str,
1075        kernel: Kernel,
1076    ) -> Result<(), Box<dyn std::error::Error>> {
1077        skip_if_unsupported!(kernel, test_name);
1078        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1079        let candles = read_candles_from_csv(file_path)?;
1080        let input = DecOscInput::with_default_candles(&candles);
1081        match input.data {
1082            DecOscData::Candles { source, .. } => assert_eq!(source, "close"),
1083            _ => panic!("Expected DecOscData::Candles"),
1084        }
1085        let output = dec_osc_with_kernel(&input, kernel)?;
1086        assert_eq!(output.values.len(), candles.close.len());
1087        Ok(())
1088    }
1089
1090    fn check_dec_osc_zero_period(
1091        test_name: &str,
1092        kernel: Kernel,
1093    ) -> Result<(), Box<dyn std::error::Error>> {
1094        skip_if_unsupported!(kernel, test_name);
1095        let input_data = [10.0, 20.0, 30.0];
1096        let params = DecOscParams {
1097            hp_period: Some(0),
1098            k: Some(1.0),
1099        };
1100        let input = DecOscInput::from_slice(&input_data, params);
1101        let res = dec_osc_with_kernel(&input, kernel);
1102        assert!(
1103            res.is_err(),
1104            "[{}] DEC_OSC should fail with zero period",
1105            test_name
1106        );
1107        Ok(())
1108    }
1109
1110    fn check_dec_osc_period_exceeds_length(
1111        test_name: &str,
1112        kernel: Kernel,
1113    ) -> Result<(), Box<dyn std::error::Error>> {
1114        skip_if_unsupported!(kernel, test_name);
1115        let data_small = [10.0, 20.0, 30.0];
1116        let params = DecOscParams {
1117            hp_period: Some(10),
1118            k: Some(1.0),
1119        };
1120        let input = DecOscInput::from_slice(&data_small, params);
1121        let res = dec_osc_with_kernel(&input, kernel);
1122        assert!(
1123            res.is_err(),
1124            "[{}] DEC_OSC should fail with period exceeding length",
1125            test_name
1126        );
1127        Ok(())
1128    }
1129
1130    fn check_dec_osc_very_small_dataset(
1131        test_name: &str,
1132        kernel: Kernel,
1133    ) -> Result<(), Box<dyn std::error::Error>> {
1134        skip_if_unsupported!(kernel, test_name);
1135        let single_point = [42.0];
1136        let params = DecOscParams {
1137            hp_period: Some(125),
1138            k: Some(1.0),
1139        };
1140        let input = DecOscInput::from_slice(&single_point, params);
1141        let res = dec_osc_with_kernel(&input, kernel);
1142        assert!(
1143            res.is_err(),
1144            "[{}] DEC_OSC should fail with insufficient data",
1145            test_name
1146        );
1147        Ok(())
1148    }
1149
1150    fn check_dec_osc_reinput(
1151        test_name: &str,
1152        kernel: Kernel,
1153    ) -> Result<(), Box<dyn std::error::Error>> {
1154        skip_if_unsupported!(kernel, test_name);
1155        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1156        let candles = read_candles_from_csv(file_path)?;
1157        let first_params = DecOscParams {
1158            hp_period: Some(50),
1159            k: Some(1.0),
1160        };
1161        let first_input = DecOscInput::from_candles(&candles, "close", first_params);
1162        let first_result = dec_osc_with_kernel(&first_input, kernel)?;
1163        let second_params = DecOscParams {
1164            hp_period: Some(50),
1165            k: Some(1.0),
1166        };
1167        let second_input = DecOscInput::from_slice(&first_result.values, second_params);
1168        let second_result = dec_osc_with_kernel(&second_input, kernel)?;
1169        assert_eq!(second_result.values.len(), first_result.values.len());
1170        Ok(())
1171    }
1172
1173    #[cfg(debug_assertions)]
1174    fn check_dec_osc_no_poison(
1175        test_name: &str,
1176        kernel: Kernel,
1177    ) -> Result<(), Box<dyn std::error::Error>> {
1178        skip_if_unsupported!(kernel, test_name);
1179
1180        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1181        let candles = read_candles_from_csv(file_path)?;
1182
1183        let test_params = vec![
1184            DecOscParams::default(),
1185            DecOscParams {
1186                hp_period: Some(2),
1187                k: Some(1.0),
1188            },
1189            DecOscParams {
1190                hp_period: Some(10),
1191                k: Some(1.0),
1192            },
1193            DecOscParams {
1194                hp_period: Some(50),
1195                k: Some(1.0),
1196            },
1197            DecOscParams {
1198                hp_period: Some(125),
1199                k: Some(1.0),
1200            },
1201            DecOscParams {
1202                hp_period: Some(200),
1203                k: Some(1.0),
1204            },
1205            DecOscParams {
1206                hp_period: Some(500),
1207                k: Some(1.0),
1208            },
1209            DecOscParams {
1210                hp_period: Some(50),
1211                k: Some(0.5),
1212            },
1213            DecOscParams {
1214                hp_period: Some(50),
1215                k: Some(2.0),
1216            },
1217            DecOscParams {
1218                hp_period: Some(125),
1219                k: Some(0.1),
1220            },
1221            DecOscParams {
1222                hp_period: Some(125),
1223                k: Some(10.0),
1224            },
1225            DecOscParams {
1226                hp_period: Some(20),
1227                k: Some(1.5),
1228            },
1229            DecOscParams {
1230                hp_period: Some(100),
1231                k: Some(0.75),
1232            },
1233        ];
1234
1235        for (param_idx, params) in test_params.iter().enumerate() {
1236            let input = DecOscInput::from_candles(&candles, "close", params.clone());
1237            let output = dec_osc_with_kernel(&input, kernel)?;
1238
1239            for (i, &val) in output.values.iter().enumerate() {
1240                if val.is_nan() {
1241                    continue;
1242                }
1243
1244                let bits = val.to_bits();
1245
1246                if bits == 0x11111111_11111111 {
1247                    panic!(
1248                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
1249						 with params: hp_period={}, k={} (param set {})",
1250                        test_name,
1251                        val,
1252                        bits,
1253                        i,
1254                        params.hp_period.unwrap_or(125),
1255                        params.k.unwrap_or(1.0),
1256                        param_idx
1257                    );
1258                }
1259
1260                if bits == 0x22222222_22222222 {
1261                    panic!(
1262                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
1263						 with params: hp_period={}, k={} (param set {})",
1264                        test_name,
1265                        val,
1266                        bits,
1267                        i,
1268                        params.hp_period.unwrap_or(125),
1269                        params.k.unwrap_or(1.0),
1270                        param_idx
1271                    );
1272                }
1273
1274                if bits == 0x33333333_33333333 {
1275                    panic!(
1276                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
1277						 with params: hp_period={}, k={} (param set {})",
1278                        test_name,
1279                        val,
1280                        bits,
1281                        i,
1282                        params.hp_period.unwrap_or(125),
1283                        params.k.unwrap_or(1.0),
1284                        param_idx
1285                    );
1286                }
1287            }
1288        }
1289
1290        Ok(())
1291    }
1292
1293    #[cfg(not(debug_assertions))]
1294    fn check_dec_osc_no_poison(
1295        _test_name: &str,
1296        _kernel: Kernel,
1297    ) -> Result<(), Box<dyn std::error::Error>> {
1298        Ok(())
1299    }
1300
1301    macro_rules! generate_all_dec_osc_tests {
1302        ($($test_fn:ident),*) => {
1303            paste::paste! {
1304                $(
1305                    #[test]
1306                    fn [<$test_fn _scalar_f64>]() {
1307                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
1308                    }
1309                )*
1310                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1311                $(
1312                    #[test]
1313                    fn [<$test_fn _avx2_f64>]() {
1314                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
1315                    }
1316                    #[test]
1317                    fn [<$test_fn _avx512_f64>]() {
1318                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
1319                    }
1320                )*
1321            }
1322        }
1323    }
1324
1325    generate_all_dec_osc_tests!(
1326        check_dec_osc_partial_params,
1327        check_dec_osc_accuracy,
1328        check_dec_osc_default_candles,
1329        check_dec_osc_zero_period,
1330        check_dec_osc_period_exceeds_length,
1331        check_dec_osc_very_small_dataset,
1332        check_dec_osc_reinput,
1333        check_dec_osc_no_poison
1334    );
1335
1336    #[cfg(feature = "proptest")]
1337    generate_all_dec_osc_tests!(check_dec_osc_property);
1338
1339    fn check_batch_default_row(
1340        test: &str,
1341        kernel: Kernel,
1342    ) -> Result<(), Box<dyn std::error::Error>> {
1343        skip_if_unsupported!(kernel, test);
1344        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1345        let c = read_candles_from_csv(file)?;
1346        let output = DecOscBatchBuilder::new()
1347            .kernel(kernel)
1348            .apply_candles(&c, "close")?;
1349        let def = DecOscParams::default();
1350        let row = output.values_for(&def).expect("default row missing");
1351        assert_eq!(row.len(), c.close.len());
1352        let expected = [
1353            -1.5036367540303395,
1354            -1.4037875172207006,
1355            -1.3174199471429475,
1356            -1.2245874070642693,
1357            -1.1638422627265639,
1358        ];
1359        let start = row.len() - 5;
1360        for (i, &v) in row[start..].iter().enumerate() {
1361            assert!(
1362                (v - expected[i]).abs() < 1e-7,
1363                "[{test}] default-row mismatch at idx {i}: {v} vs {expected:?}"
1364            );
1365        }
1366        Ok(())
1367    }
1368
1369    macro_rules! gen_batch_tests {
1370        ($fn_name:ident) => {
1371            paste::paste! {
1372                #[test] fn [<$fn_name _scalar>]()      {
1373                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
1374                }
1375                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1376                #[test] fn [<$fn_name _avx2>]()        {
1377                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
1378                }
1379                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1380                #[test] fn [<$fn_name _avx512>]()      {
1381                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
1382                }
1383                #[test] fn [<$fn_name _auto_detect>]() {
1384                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
1385                }
1386            }
1387        };
1388    }
1389    gen_batch_tests!(check_batch_default_row);
1390    gen_batch_tests!(check_batch_no_poison);
1391
1392    #[cfg(debug_assertions)]
1393    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn std::error::Error>> {
1394        skip_if_unsupported!(kernel, test);
1395
1396        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1397        let c = read_candles_from_csv(file)?;
1398
1399        let test_configs = vec![
1400            (2, 10, 2, 1.0, 1.0, 0.0),
1401            (10, 50, 10, 1.0, 1.0, 0.0),
1402            (50, 150, 25, 1.0, 1.0, 0.0),
1403            (125, 125, 0, 0.5, 2.0, 0.5),
1404            (20, 40, 5, 0.5, 1.5, 0.25),
1405            (100, 200, 50, 1.0, 1.0, 0.0),
1406            (2, 5, 1, 0.1, 10.0, 4.95),
1407        ];
1408
1409        for (cfg_idx, &(hp_start, hp_end, hp_step, k_start, k_end, k_step)) in
1410            test_configs.iter().enumerate()
1411        {
1412            let mut builder = DecOscBatchBuilder::new().kernel(kernel);
1413
1414            if hp_step > 0 {
1415                builder = builder.hp_period_range(hp_start, hp_end, hp_step);
1416            } else {
1417                builder = builder.hp_period_range(hp_start, hp_start, 1);
1418            }
1419
1420            if k_step > 0.0 {
1421                builder = builder.k_range(k_start, k_end, k_step);
1422            }
1423
1424            let output = builder.apply_candles(&c, "close")?;
1425
1426            for (idx, &val) in output.values.iter().enumerate() {
1427                if val.is_nan() {
1428                    continue;
1429                }
1430
1431                let bits = val.to_bits();
1432                let row = idx / output.cols;
1433                let col = idx % output.cols;
1434                let combo = &output.combos[row];
1435
1436                if bits == 0x11111111_11111111 {
1437                    panic!(
1438                        "[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
1439						 at row {} col {} (flat index {}) with params: hp_period={}, k={}",
1440                        test,
1441                        cfg_idx,
1442                        val,
1443                        bits,
1444                        row,
1445                        col,
1446                        idx,
1447                        combo.hp_period.unwrap_or(125),
1448                        combo.k.unwrap_or(1.0)
1449                    );
1450                }
1451
1452                if bits == 0x22222222_22222222 {
1453                    panic!(
1454                        "[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
1455						 at row {} col {} (flat index {}) with params: hp_period={}, k={}",
1456                        test,
1457                        cfg_idx,
1458                        val,
1459                        bits,
1460                        row,
1461                        col,
1462                        idx,
1463                        combo.hp_period.unwrap_or(125),
1464                        combo.k.unwrap_or(1.0)
1465                    );
1466                }
1467
1468                if bits == 0x33333333_33333333 {
1469                    panic!(
1470                        "[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
1471						 at row {} col {} (flat index {}) with params: hp_period={}, k={}",
1472                        test,
1473                        cfg_idx,
1474                        val,
1475                        bits,
1476                        row,
1477                        col,
1478                        idx,
1479                        combo.hp_period.unwrap_or(125),
1480                        combo.k.unwrap_or(1.0)
1481                    );
1482                }
1483            }
1484        }
1485
1486        Ok(())
1487    }
1488
1489    #[cfg(not(debug_assertions))]
1490    fn check_batch_no_poison(
1491        _test: &str,
1492        _kernel: Kernel,
1493    ) -> Result<(), Box<dyn std::error::Error>> {
1494        Ok(())
1495    }
1496
1497    #[cfg(feature = "proptest")]
1498    #[allow(clippy::float_cmp)]
1499    fn check_dec_osc_property(
1500        test_name: &str,
1501        kernel: Kernel,
1502    ) -> Result<(), Box<dyn std::error::Error>> {
1503        use proptest::prelude::*;
1504        skip_if_unsupported!(kernel, test_name);
1505
1506        let strat_random = (3usize..=200).prop_flat_map(|hp_period| {
1507            (
1508                prop::collection::vec(
1509                    (100.0f64..10000.0f64)
1510                        .prop_filter("finite positive", |x| x.is_finite() && *x > 50.0),
1511                    hp_period + 10..400,
1512                ),
1513                Just(hp_period),
1514                (0.1f64..3.0f64),
1515            )
1516        });
1517
1518        let strat_constant = (3usize..=100, 0.1f64..3.0f64).prop_map(|(hp_period, k)| {
1519            let value = 1000.0;
1520            let data = vec![value; hp_period.max(10) + 20];
1521            (data, hp_period, k)
1522        });
1523
1524        let strat_trending = (3usize..=100, 0.1f64..3.0f64, prop::bool::ANY).prop_map(
1525            |(hp_period, k, increasing)| {
1526                let len = hp_period.max(10) + 50;
1527                let data: Vec<f64> = if increasing {
1528                    (0..len).map(|i| 500.0 + i as f64 * 2.0).collect()
1529                } else {
1530                    (0..len).map(|i| 2000.0 - i as f64 * 2.0).collect()
1531                };
1532                (data, hp_period, k)
1533            },
1534        );
1535
1536        let strat_small_k = (10usize..=50, 0.01f64..0.5f64).prop_flat_map(|(hp_period, k)| {
1537            (
1538                prop::collection::vec(
1539                    (500.0f64..1500.0f64).prop_filter("finite", |x| x.is_finite()),
1540                    hp_period + 20..100,
1541                ),
1542                Just(hp_period),
1543                Just(k),
1544            )
1545        });
1546
1547        let strat_volatile = (5usize..=50, 0.1f64..2.0f64).prop_flat_map(|(hp_period, k)| {
1548            (
1549                prop::collection::vec(
1550                    prop::strategy::Union::new(vec![
1551                        (100.0f64..200.0f64).boxed(),
1552                        (800.0f64..1000.0f64).boxed(),
1553                    ]),
1554                    hp_period + 20..100,
1555                ),
1556                Just(hp_period),
1557                Just(k),
1558            )
1559        });
1560
1561        let combined_strat = prop::strategy::Union::new(vec![
1562            strat_random.boxed(),
1563            strat_constant.boxed(),
1564            strat_trending.boxed(),
1565            strat_small_k.boxed(),
1566            strat_volatile.boxed(),
1567        ]);
1568
1569        proptest::test_runner::TestRunner::default()
1570            .run(&combined_strat, |(data, hp_period, k)| {
1571                let params = DecOscParams {
1572                    hp_period: Some(hp_period),
1573                    k: Some(k),
1574                };
1575                let input = DecOscInput::from_slice(&data, params);
1576
1577                let DecOscOutput { values: out } = dec_osc_with_kernel(&input, kernel).unwrap();
1578                let DecOscOutput { values: ref_out } =
1579                    dec_osc_with_kernel(&input, Kernel::Scalar).unwrap();
1580
1581                let first = data.iter().position(|x| !x.is_nan()).unwrap_or(0);
1582                let warmup_end = first + 2;
1583
1584                for i in warmup_end..data.len() {
1585                    let y = out[i];
1586                    let r = ref_out[i];
1587
1588                    prop_assert!(
1589                        y.is_finite(),
1590                        "[{}] Output should be finite at index {} (after warmup): got {}",
1591                        test_name,
1592                        i,
1593                        y
1594                    );
1595
1596                    if hp_period > 2 {
1597                        let magnitude_limit = if hp_period >= 50 {
1598                            5000.0
1599                        } else if hp_period >= 20 {
1600                            20000.0
1601                        } else if hp_period >= 10 {
1602                            100000.0
1603                        } else {
1604                            1000000.0
1605                        };
1606
1607                        prop_assert!(
1608							y.abs() <= magnitude_limit,
1609							"[{}] Oscillator exceeds bounds at index {}: {} (> ±{}%) with hp_period={}",
1610							test_name, i, y, magnitude_limit, hp_period
1611						);
1612                    }
1613
1614                    if data.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-10)
1615                        && i > first + hp_period * 3
1616                        && hp_period > 2
1617                    {
1618                        let convergence_limit = if hp_period < 10 { 10.0 } else { 0.1 };
1619                        prop_assert!(
1620							y.abs() <= convergence_limit,
1621							"[{}] Constant data should converge near zero at index {}: got {} (hp_period={})",
1622							test_name, i, y, hp_period
1623						);
1624                    }
1625
1626                    if !y.is_finite() || !r.is_finite() {
1627                        prop_assert!(
1628                            y.to_bits() == r.to_bits(),
1629                            "[{}] NaN/Inf mismatch at index {}: {} vs {}",
1630                            test_name,
1631                            i,
1632                            y,
1633                            r
1634                        );
1635                        continue;
1636                    }
1637
1638                    let y_bits = y.to_bits();
1639                    let r_bits = r.to_bits();
1640                    let ulp_diff: u64 = y_bits.abs_diff(r_bits);
1641
1642                    prop_assert!(
1643                        (y - r).abs() <= 1e-7 || ulp_diff <= 20,
1644                        "[{}] Kernel mismatch at index {}: {} vs {} (ULP={}, diff={})",
1645                        test_name,
1646                        i,
1647                        y,
1648                        r,
1649                        ulp_diff,
1650                        (y - r).abs()
1651                    );
1652
1653                    prop_assert!(
1654                        y_bits != 0x11111111_11111111
1655                            && y_bits != 0x22222222_22222222
1656                            && y_bits != 0x33333333_33333333,
1657                        "[{}] Found poison value at index {}: {} (0x{:016X})",
1658                        test_name,
1659                        i,
1660                        y,
1661                        y_bits
1662                    );
1663
1664                    if k < 0.1 && i > first + hp_period * 2 && hp_period > 2 {
1665                        let k_limit = if hp_period >= 50 {
1666                            10000.0
1667                        } else if hp_period >= 20 {
1668                            40000.0
1669                        } else if hp_period >= 10 {
1670                            200000.0
1671                        } else {
1672                            2000000.0
1673                        };
1674
1675                        prop_assert!(
1676                            y.abs() <= k_limit,
1677                            "[{}] Unexpectedly large output with small k={} at index {}: got {}",
1678                            test_name,
1679                            k,
1680                            i,
1681                            y
1682                        );
1683                    }
1684                }
1685
1686                for i in 0..warmup_end.min(out.len()) {
1687                    prop_assert!(
1688                        out[i].is_nan(),
1689                        "[{}] Expected NaN during warmup at index {}: got {}",
1690                        test_name,
1691                        i,
1692                        out[i]
1693                    );
1694                }
1695
1696                Ok(())
1697            })
1698            .unwrap();
1699
1700        Ok(())
1701    }
1702
1703    #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1704    #[test]
1705    fn test_dec_osc_into_matches_api() -> Result<(), Box<dyn std::error::Error>> {
1706        let n = 256usize;
1707        let mut data = Vec::with_capacity(n);
1708        for i in 0..n {
1709            let t = i as f64;
1710            let v = 100.0 + 0.05 * t + 2.0 * (0.1 * t).sin();
1711            data.push(v);
1712        }
1713
1714        let input = DecOscInput::from_slice(&data, DecOscParams::default());
1715
1716        let baseline = dec_osc(&input)?.values;
1717
1718        let mut out = vec![0.0; n];
1719        super::dec_osc_into(&mut out, &input)?;
1720
1721        assert_eq!(baseline.len(), out.len());
1722
1723        fn eq_or_both_nan(a: f64, b: f64) -> bool {
1724            (a.is_nan() && b.is_nan()) || (a == b)
1725        }
1726
1727        for i in 0..n {
1728            assert!(
1729                eq_or_both_nan(baseline[i], out[i]),
1730                "mismatch at index {}: baseline={} vs into={}",
1731                i,
1732                baseline[i],
1733                out[i]
1734            );
1735        }
1736
1737        Ok(())
1738    }
1739}
1740
1741#[cfg(feature = "python")]
1742#[pyfunction(name = "dec_osc")]
1743#[pyo3(signature = (data, hp_period=125, k=1.0, kernel=None))]
1744pub fn dec_osc_py<'py>(
1745    py: Python<'py>,
1746    data: PyReadonlyArray1<'py, f64>,
1747    hp_period: usize,
1748    k: f64,
1749    kernel: Option<&str>,
1750) -> PyResult<Bound<'py, PyArray1<f64>>> {
1751    use numpy::{IntoPyArray, PyArrayMethods};
1752
1753    let slice_in = data.as_slice()?;
1754    let kern = validate_kernel(kernel, false)?;
1755
1756    let params = DecOscParams {
1757        hp_period: Some(hp_period),
1758        k: Some(k),
1759    };
1760    let input = DecOscInput::from_slice(slice_in, params);
1761
1762    let result_vec: Vec<f64> = py
1763        .allow_threads(|| dec_osc_with_kernel(&input, kern).map(|o| o.values))
1764        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1765
1766    Ok(result_vec.into_pyarray(py))
1767}
1768
1769#[cfg(feature = "python")]
1770#[pyclass(name = "DecOscStream")]
1771pub struct DecOscStreamPy {
1772    stream: DecOscStream,
1773}
1774
1775#[cfg(feature = "python")]
1776#[pymethods]
1777impl DecOscStreamPy {
1778    #[new]
1779    fn new(hp_period: usize, k: f64) -> PyResult<Self> {
1780        let params = DecOscParams {
1781            hp_period: Some(hp_period),
1782            k: Some(k),
1783        };
1784        let stream =
1785            DecOscStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
1786        Ok(DecOscStreamPy { stream })
1787    }
1788
1789    fn update(&mut self, value: f64) -> Option<f64> {
1790        self.stream.update(value)
1791    }
1792}
1793
1794#[cfg(feature = "python")]
1795#[pyfunction(name = "dec_osc_batch")]
1796#[pyo3(signature = (data, hp_period_range, k_range, kernel=None))]
1797pub fn dec_osc_batch_py<'py>(
1798    py: Python<'py>,
1799    data: PyReadonlyArray1<'py, f64>,
1800    hp_period_range: (usize, usize, usize),
1801    k_range: (f64, f64, f64),
1802    kernel: Option<&str>,
1803) -> PyResult<Bound<'py, PyDict>> {
1804    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
1805
1806    let slice_in = data.as_slice()?;
1807    let kern = validate_kernel(kernel, true)?;
1808
1809    let sweep = DecOscBatchRange {
1810        hp_period: hp_period_range,
1811        k: k_range,
1812    };
1813
1814    let rows = expand_grid_checked(&sweep)
1815        .map_err(|e| PyValueError::new_err(e.to_string()))?
1816        .len();
1817    let cols = slice_in.len();
1818    let total = rows
1819        .checked_mul(cols)
1820        .ok_or_else(|| PyValueError::new_err("rows*cols overflow in dec_osc_batch"))?;
1821
1822    let out_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1823    let slice_out = unsafe { out_arr.as_slice_mut()? };
1824
1825    let combos = py
1826        .allow_threads(|| {
1827            let kernel = match kern {
1828                Kernel::Auto => detect_best_batch_kernel(),
1829                k => k,
1830            };
1831            let simd = match kernel {
1832                Kernel::Avx512Batch => Kernel::Avx512,
1833                Kernel::Avx2Batch => Kernel::Avx2,
1834                Kernel::ScalarBatch => Kernel::Scalar,
1835                _ => unreachable!(),
1836            };
1837            dec_osc_batch_inner_into(slice_in, &sweep, simd, true, slice_out)
1838        })
1839        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1840
1841    let dict = PyDict::new(py);
1842    dict.set_item("values", out_arr.reshape((rows, cols))?)?;
1843    dict.set_item(
1844        "hp_periods",
1845        combos
1846            .iter()
1847            .map(|p| p.hp_period.unwrap() as u64)
1848            .collect::<Vec<_>>()
1849            .into_pyarray(py),
1850    )?;
1851    dict.set_item(
1852        "ks",
1853        combos
1854            .iter()
1855            .map(|p| p.k.unwrap())
1856            .collect::<Vec<_>>()
1857            .into_pyarray(py),
1858    )?;
1859
1860    Ok(dict)
1861}
1862
1863#[cfg(all(feature = "python", feature = "cuda"))]
1864use crate::cuda::cuda_available;
1865#[cfg(all(feature = "python", feature = "cuda"))]
1866use crate::cuda::moving_averages::DeviceArrayF32;
1867#[cfg(all(feature = "python", feature = "cuda"))]
1868use crate::cuda::oscillators::CudaDecOsc;
1869#[cfg(all(feature = "python", feature = "cuda"))]
1870use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
1871#[cfg(all(feature = "python", feature = "cuda"))]
1872use cust::context::Context;
1873#[cfg(all(feature = "python", feature = "cuda"))]
1874use std::sync::Arc;
1875
1876#[cfg(all(feature = "python", feature = "cuda"))]
1877#[pyfunction(name = "dec_osc_cuda_batch_dev")]
1878#[pyo3(signature = (data, hp_period_range, k_range))]
1879pub fn dec_osc_cuda_batch_dev_py(
1880    py: Python<'_>,
1881    data: PyReadonlyArray1<'_, f64>,
1882    hp_period_range: (usize, usize, usize),
1883    k_range: (f64, f64, f64),
1884) -> PyResult<DecOscDeviceArrayF32Py> {
1885    if !cuda_available() {
1886        return Err(PyValueError::new_err("CUDA device not available"));
1887    }
1888    let slice_in = data.as_slice()?;
1889    let data_f32: Vec<f32> = slice_in.iter().map(|&v| v as f32).collect();
1890    let sweep = DecOscBatchRange {
1891        hp_period: hp_period_range,
1892        k: k_range,
1893    };
1894    let (inner, ctx, dev_id) = py.allow_threads(|| {
1895        let cuda = CudaDecOsc::new(0).map_err(|e| PyValueError::new_err(e.to_string()))?;
1896        let dev_id = cuda.device_id();
1897        let ctx = cuda.context_arc();
1898        let inner = cuda
1899            .dec_osc_batch_dev(&data_f32, &sweep)
1900            .map_err(|e| PyValueError::new_err(e.to_string()))?;
1901        Ok::<_, PyErr>((inner, ctx, dev_id))
1902    })?;
1903    Ok(DecOscDeviceArrayF32Py {
1904        inner: Some(inner),
1905        _ctx_guard: ctx,
1906        _device_id: dev_id,
1907    })
1908}
1909
1910#[cfg(all(feature = "python", feature = "cuda"))]
1911#[pyfunction(name = "dec_osc_cuda_many_series_one_param_dev")]
1912#[pyo3(signature = (data_tm, cols, rows, hp_period, k))]
1913pub fn dec_osc_cuda_many_series_one_param_dev_py(
1914    py: Python<'_>,
1915    data_tm: PyReadonlyArray1<'_, f64>,
1916    cols: usize,
1917    rows: usize,
1918    hp_period: usize,
1919    k: f64,
1920) -> PyResult<DecOscDeviceArrayF32Py> {
1921    if !cuda_available() {
1922        return Err(PyValueError::new_err("CUDA device not available"));
1923    }
1924    let slice = data_tm.as_slice()?;
1925    let expected = cols
1926        .checked_mul(rows)
1927        .ok_or_else(|| PyValueError::new_err("cols*rows overflow"))?;
1928    if slice.len() != expected {
1929        return Err(PyValueError::new_err(
1930            "time-major array length != cols*rows",
1931        ));
1932    }
1933    let data_f32: Vec<f32> = slice.iter().map(|&v| v as f32).collect();
1934    let params = DecOscParams {
1935        hp_period: Some(hp_period),
1936        k: Some(k),
1937    };
1938    let (inner, ctx, dev_id) = py.allow_threads(|| {
1939        let cuda = CudaDecOsc::new(0).map_err(|e| PyValueError::new_err(e.to_string()))?;
1940        let dev_id = cuda.device_id();
1941        let ctx = cuda.context_arc();
1942        let inner = cuda
1943            .dec_osc_many_series_one_param_time_major_dev(&data_f32, cols, rows, &params)
1944            .map_err(|e| PyValueError::new_err(e.to_string()))?;
1945        Ok::<_, PyErr>((inner, ctx, dev_id))
1946    })?;
1947    Ok(DecOscDeviceArrayF32Py {
1948        inner: Some(inner),
1949        _ctx_guard: ctx,
1950        _device_id: dev_id,
1951    })
1952}
1953
1954#[cfg(all(feature = "python", feature = "cuda"))]
1955#[pyclass(module = "ta_indicators.cuda", name = "DecOscDeviceArrayF32")]
1956pub struct DecOscDeviceArrayF32Py {
1957    pub(crate) inner: Option<DeviceArrayF32>,
1958    _ctx_guard: Arc<Context>,
1959    _device_id: u32,
1960}
1961
1962#[cfg(all(feature = "python", feature = "cuda"))]
1963#[pymethods]
1964impl DecOscDeviceArrayF32Py {
1965    #[getter]
1966    fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
1967        let itemsize = std::mem::size_of::<f32>();
1968        let inner = self
1969            .inner
1970            .as_ref()
1971            .ok_or_else(|| PyValueError::new_err("buffer already exported via __dlpack__"))?;
1972        let d = PyDict::new(py);
1973        d.set_item("shape", (inner.rows, inner.cols))?;
1974        d.set_item("typestr", "<f4")?;
1975        d.set_item("strides", (inner.cols * itemsize, itemsize))?;
1976        let nelems = inner.rows.saturating_mul(inner.cols);
1977        let ptr_val: usize = if nelems == 0 {
1978            0
1979        } else {
1980            inner.device_ptr() as usize
1981        };
1982        d.set_item("data", (ptr_val, false))?;
1983
1984        d.set_item("version", 3)?;
1985        Ok(d)
1986    }
1987
1988    fn __dlpack_device__(&self) -> (i32, i32) {
1989        (2, self._device_id as i32)
1990    }
1991
1992    #[allow(clippy::too_many_arguments)]
1993    #[pyo3(signature = (stream=None, max_version=None, dl_device=None, copy=None))]
1994    fn __dlpack__<'py>(
1995        &mut self,
1996        py: Python<'py>,
1997        stream: Option<PyObject>,
1998        max_version: Option<PyObject>,
1999        dl_device: Option<PyObject>,
2000        copy: Option<PyObject>,
2001    ) -> PyResult<PyObject> {
2002        let (kdl, alloc_dev) = self.__dlpack_device__();
2003        if let Some(dev_obj) = dl_device.as_ref() {
2004            if let Ok((dev_ty, dev_id)) = dev_obj.extract::<(i32, i32)>(py) {
2005                if dev_ty != kdl || dev_id != alloc_dev {
2006                    let wants_copy = copy
2007                        .as_ref()
2008                        .and_then(|c| c.extract::<bool>(py).ok())
2009                        .unwrap_or(false);
2010                    if wants_copy {
2011                        return Err(PyValueError::new_err(
2012                            "device copy not implemented for __dlpack__",
2013                        ));
2014                    } else {
2015                        return Err(PyValueError::new_err("dl_device mismatch for __dlpack__"));
2016                    }
2017                }
2018            }
2019        }
2020        let _ = stream;
2021
2022        let inner = self
2023            .inner
2024            .take()
2025            .ok_or_else(|| PyValueError::new_err("__dlpack__ may only be called once"))?;
2026
2027        let rows = inner.rows;
2028        let cols = inner.cols;
2029        let buf = inner.buf;
2030
2031        let max_version_bound = max_version.map(|obj| obj.into_bound(py));
2032
2033        export_f32_cuda_dlpack_2d(py, buf, rows, cols, alloc_dev, max_version_bound)
2034    }
2035}
2036
2037#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2038#[wasm_bindgen]
2039pub fn dec_osc_js(data: &[f64], hp_period: usize, k: f64) -> Result<Vec<f64>, JsValue> {
2040    let params = DecOscParams {
2041        hp_period: Some(hp_period),
2042        k: Some(k),
2043    };
2044    let input = DecOscInput::from_slice(data, params);
2045
2046    let mut output = vec![0.0; data.len()];
2047
2048    #[cfg(target_arch = "wasm32")]
2049    let kernel = detect_best_kernel();
2050    #[cfg(not(target_arch = "wasm32"))]
2051    let kernel = Kernel::Auto;
2052
2053    dec_osc_into_slice(&mut output, &input, kernel)
2054        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2055
2056    Ok(output)
2057}
2058
2059#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2060#[wasm_bindgen]
2061pub fn dec_osc_into(
2062    in_ptr: *const f64,
2063    out_ptr: *mut f64,
2064    len: usize,
2065    hp_period: usize,
2066    k: f64,
2067) -> Result<(), JsValue> {
2068    if in_ptr.is_null() || out_ptr.is_null() {
2069        return Err(JsValue::from_str("Null pointer provided"));
2070    }
2071
2072    unsafe {
2073        let data = std::slice::from_raw_parts(in_ptr, len);
2074        let params = DecOscParams {
2075            hp_period: Some(hp_period),
2076            k: Some(k),
2077        };
2078        let input = DecOscInput::from_slice(data, params);
2079
2080        #[cfg(target_arch = "wasm32")]
2081        let kernel = detect_best_kernel();
2082        #[cfg(not(target_arch = "wasm32"))]
2083        let kernel = Kernel::Auto;
2084
2085        if in_ptr == out_ptr as *const f64 {
2086            let mut temp = vec![0.0; len];
2087            dec_osc_into_slice(&mut temp, &input, kernel)
2088                .map_err(|e| JsValue::from_str(&e.to_string()))?;
2089            let out = std::slice::from_raw_parts_mut(out_ptr, len);
2090            out.copy_from_slice(&temp);
2091        } else {
2092            let out = std::slice::from_raw_parts_mut(out_ptr, len);
2093            dec_osc_into_slice(out, &input, kernel)
2094                .map_err(|e| JsValue::from_str(&e.to_string()))?;
2095        }
2096        Ok(())
2097    }
2098}
2099
2100#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2101#[wasm_bindgen]
2102pub fn dec_osc_alloc(len: usize) -> *mut f64 {
2103    let mut vec = Vec::<f64>::with_capacity(len);
2104    let ptr = vec.as_mut_ptr();
2105    std::mem::forget(vec);
2106    ptr
2107}
2108
2109#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2110#[wasm_bindgen]
2111pub fn dec_osc_free(ptr: *mut f64, len: usize) {
2112    if !ptr.is_null() {
2113        unsafe {
2114            let _ = Vec::from_raw_parts(ptr, len, len);
2115        }
2116    }
2117}
2118
2119#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2120#[derive(Serialize, Deserialize)]
2121pub struct DecOscBatchConfig {
2122    pub hp_period_range: (usize, usize, usize),
2123    pub k_range: (f64, f64, f64),
2124}
2125
2126#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2127#[derive(Serialize, Deserialize)]
2128pub struct DecOscBatchJsOutput {
2129    pub values: Vec<f64>,
2130    pub combos: Vec<DecOscParams>,
2131    pub rows: usize,
2132    pub cols: usize,
2133}
2134
2135#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2136#[wasm_bindgen(js_name = dec_osc_batch)]
2137pub fn dec_osc_batch_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
2138    let config: DecOscBatchConfig = serde_wasm_bindgen::from_value(config)
2139        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
2140
2141    let sweep = DecOscBatchRange {
2142        hp_period: config.hp_period_range,
2143        k: config.k_range,
2144    };
2145
2146    #[cfg(target_arch = "wasm32")]
2147    let output = dec_osc_batch_inner(data, &sweep, detect_best_kernel(), false)
2148        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2149
2150    #[cfg(not(target_arch = "wasm32"))]
2151    let output = dec_osc_batch_inner(data, &sweep, Kernel::Auto, false)
2152        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2153
2154    let js_output = DecOscBatchJsOutput {
2155        values: output.values,
2156        combos: output.combos,
2157        rows: output.rows,
2158        cols: output.cols,
2159    };
2160
2161    serde_wasm_bindgen::to_value(&js_output)
2162        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2163}
2164
2165#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2166#[wasm_bindgen]
2167pub fn dec_osc_batch_into(
2168    in_ptr: *const f64,
2169    out_ptr: *mut f64,
2170    len: usize,
2171    hp_start: usize,
2172    hp_end: usize,
2173    hp_step: usize,
2174    k_start: f64,
2175    k_end: f64,
2176    k_step: f64,
2177) -> Result<usize, JsValue> {
2178    if in_ptr.is_null() || out_ptr.is_null() {
2179        return Err(JsValue::from_str("null pointer"));
2180    }
2181
2182    unsafe {
2183        let data = std::slice::from_raw_parts(in_ptr, len);
2184        let sweep = DecOscBatchRange {
2185            hp_period: (hp_start, hp_end, hp_step),
2186            k: (k_start, k_end, k_step),
2187        };
2188
2189        let combos = expand_grid_checked(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2190        let rows = combos.len();
2191        let cols = len;
2192        let total = rows
2193            .checked_mul(cols)
2194            .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
2195        let out = std::slice::from_raw_parts_mut(out_ptr, total);
2196
2197        dec_osc_batch_inner_into(data, &sweep, detect_best_kernel(), false, out)
2198            .map_err(|e| JsValue::from_str(&e.to_string()))?;
2199
2200        Ok(rows)
2201    }
2202}