Skip to main content

vector_ta/indicators/
normalized_resonator.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(feature = "python")]
10use pyo3::wrap_pyfunction;
11
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use serde::{Deserialize, Serialize};
14#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
15use wasm_bindgen::prelude::*;
16
17use crate::utilities::data_loader::{source_type, Candles};
18use crate::utilities::enums::Kernel;
19use crate::utilities::helpers::{
20    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
21    make_uninit_matrix,
22};
23#[cfg(feature = "python")]
24use crate::utilities::kernel_validation::validate_kernel;
25#[cfg(not(target_arch = "wasm32"))]
26use rayon::prelude::*;
27use std::collections::VecDeque;
28use std::convert::AsRef;
29use std::f64::consts::PI;
30use std::mem::ManuallyDrop;
31use thiserror::Error;
32
33const DEFAULT_PERIOD: usize = 100;
34const DEFAULT_DELTA: f64 = 0.5;
35const DEFAULT_LOOKBACK_MULT: f64 = 1.0;
36const DEFAULT_SIGNAL_LENGTH: usize = 9;
37const DEFAULT_SOURCE: &str = "hl2";
38const MIN_VALID_SAMPLES: usize = 3;
39const WARMUP: usize = MIN_VALID_SAMPLES - 1;
40const FLOAT_TOL: f64 = 1e-12;
41
42impl<'a> AsRef<[f64]> for NormalizedResonatorInput<'a> {
43    #[inline(always)]
44    fn as_ref(&self) -> &[f64] {
45        match &self.data {
46            NormalizedResonatorData::Slice(slice) => slice,
47            NormalizedResonatorData::Candles { candles, source } => source_type(candles, source),
48        }
49    }
50}
51
52#[derive(Debug, Clone)]
53pub enum NormalizedResonatorData<'a> {
54    Candles {
55        candles: &'a Candles,
56        source: &'a str,
57    },
58    Slice(&'a [f64]),
59}
60
61#[derive(Debug, Clone)]
62pub struct NormalizedResonatorOutput {
63    pub oscillator: Vec<f64>,
64    pub signal: Vec<f64>,
65}
66
67#[derive(Debug, Clone, PartialEq)]
68#[cfg_attr(
69    all(target_arch = "wasm32", feature = "wasm"),
70    derive(Serialize, Deserialize)
71)]
72pub struct NormalizedResonatorParams {
73    pub period: Option<usize>,
74    pub delta: Option<f64>,
75    pub lookback_mult: Option<f64>,
76    pub signal_length: Option<usize>,
77}
78
79impl Default for NormalizedResonatorParams {
80    fn default() -> Self {
81        Self {
82            period: Some(DEFAULT_PERIOD),
83            delta: Some(DEFAULT_DELTA),
84            lookback_mult: Some(DEFAULT_LOOKBACK_MULT),
85            signal_length: Some(DEFAULT_SIGNAL_LENGTH),
86        }
87    }
88}
89
90#[derive(Debug, Clone)]
91pub struct NormalizedResonatorInput<'a> {
92    pub data: NormalizedResonatorData<'a>,
93    pub params: NormalizedResonatorParams,
94}
95
96impl<'a> NormalizedResonatorInput<'a> {
97    #[inline]
98    pub fn from_candles(
99        candles: &'a Candles,
100        source: &'a str,
101        params: NormalizedResonatorParams,
102    ) -> Self {
103        Self {
104            data: NormalizedResonatorData::Candles { candles, source },
105            params,
106        }
107    }
108
109    #[inline]
110    pub fn from_slice(slice: &'a [f64], params: NormalizedResonatorParams) -> Self {
111        Self {
112            data: NormalizedResonatorData::Slice(slice),
113            params,
114        }
115    }
116
117    #[inline]
118    pub fn with_default_candles(candles: &'a Candles) -> Self {
119        Self::from_candles(
120            candles,
121            DEFAULT_SOURCE,
122            NormalizedResonatorParams::default(),
123        )
124    }
125}
126
127#[derive(Clone, Copy, Debug, Default)]
128pub struct NormalizedResonatorBuilder {
129    period: Option<usize>,
130    delta: Option<f64>,
131    lookback_mult: Option<f64>,
132    signal_length: Option<usize>,
133    kernel: Kernel,
134}
135
136impl NormalizedResonatorBuilder {
137    #[inline]
138    pub fn new() -> Self {
139        Self::default()
140    }
141
142    #[inline]
143    pub fn period(mut self, period: usize) -> Self {
144        self.period = Some(period);
145        self
146    }
147
148    #[inline]
149    pub fn delta(mut self, delta: f64) -> Self {
150        self.delta = Some(delta);
151        self
152    }
153
154    #[inline]
155    pub fn lookback_mult(mut self, lookback_mult: f64) -> Self {
156        self.lookback_mult = Some(lookback_mult);
157        self
158    }
159
160    #[inline]
161    pub fn signal_length(mut self, signal_length: usize) -> Self {
162        self.signal_length = Some(signal_length);
163        self
164    }
165
166    #[inline]
167    pub fn kernel(mut self, kernel: Kernel) -> Self {
168        self.kernel = kernel;
169        self
170    }
171
172    #[inline]
173    pub fn apply(
174        self,
175        candles: &Candles,
176        source: &str,
177    ) -> Result<NormalizedResonatorOutput, NormalizedResonatorError> {
178        let input = NormalizedResonatorInput::from_candles(
179            candles,
180            source,
181            NormalizedResonatorParams {
182                period: self.period,
183                delta: self.delta,
184                lookback_mult: self.lookback_mult,
185                signal_length: self.signal_length,
186            },
187        );
188        normalized_resonator_with_kernel(&input, self.kernel)
189    }
190
191    #[inline]
192    pub fn apply_slice(
193        self,
194        data: &[f64],
195    ) -> Result<NormalizedResonatorOutput, NormalizedResonatorError> {
196        let input = NormalizedResonatorInput::from_slice(
197            data,
198            NormalizedResonatorParams {
199                period: self.period,
200                delta: self.delta,
201                lookback_mult: self.lookback_mult,
202                signal_length: self.signal_length,
203            },
204        );
205        normalized_resonator_with_kernel(&input, self.kernel)
206    }
207
208    #[inline]
209    pub fn into_stream(self) -> Result<NormalizedResonatorStream, NormalizedResonatorError> {
210        NormalizedResonatorStream::try_new(NormalizedResonatorParams {
211            period: self.period,
212            delta: self.delta,
213            lookback_mult: self.lookback_mult,
214            signal_length: self.signal_length,
215        })
216    }
217}
218
219#[derive(Debug, Error)]
220pub enum NormalizedResonatorError {
221    #[error("normalized_resonator: Input data slice is empty.")]
222    EmptyInputData,
223    #[error("normalized_resonator: All values are NaN.")]
224    AllValuesNaN,
225    #[error("normalized_resonator: Invalid period: {period}")]
226    InvalidPeriod { period: usize },
227    #[error("normalized_resonator: Invalid delta: {delta}")]
228    InvalidDelta { delta: f64 },
229    #[error("normalized_resonator: Invalid lookback_mult: {lookback_mult}")]
230    InvalidLookbackMult { lookback_mult: f64 },
231    #[error("normalized_resonator: Invalid signal_length: {signal_length}")]
232    InvalidSignalLength { signal_length: usize },
233    #[error("normalized_resonator: Not enough valid data: needed = {needed}, valid = {valid}")]
234    NotEnoughValidData { needed: usize, valid: usize },
235    #[error(
236        "normalized_resonator: Output length mismatch: expected = {expected}, oscillator = {oscillator_got}, signal = {signal_got}"
237    )]
238    OutputLengthMismatch {
239        expected: usize,
240        oscillator_got: usize,
241        signal_got: usize,
242    },
243    #[error("normalized_resonator: Invalid range: start={start}, end={end}, step={step}")]
244    InvalidRange {
245        start: String,
246        end: String,
247        step: String,
248    },
249    #[error("normalized_resonator: Invalid kernel for batch: {0:?}")]
250    InvalidKernelForBatch(Kernel),
251}
252
253#[derive(Clone, Copy, Debug)]
254struct ResolvedParams {
255    period: usize,
256    delta: f64,
257    lookback_mult: f64,
258    signal_length: usize,
259    peak_lookback: usize,
260    gain: f64,
261    c1: f64,
262    c2: f64,
263    ema_alpha: f64,
264}
265
266#[inline(always)]
267fn first_valid_value(data: &[f64]) -> usize {
268    let mut i = 0usize;
269    while i < data.len() {
270        if data[i].is_finite() {
271            return i;
272        }
273        i += 1;
274    }
275    data.len()
276}
277
278#[inline(always)]
279fn max_consecutive_valid_values(data: &[f64]) -> usize {
280    let mut best = 0usize;
281    let mut run = 0usize;
282    for &value in data {
283        if value.is_finite() {
284            run += 1;
285            if run > best {
286                best = run;
287            }
288        } else {
289            run = 0;
290        }
291    }
292    best
293}
294
295#[inline(always)]
296fn resolve_params(
297    params: &NormalizedResonatorParams,
298) -> Result<ResolvedParams, NormalizedResonatorError> {
299    let period = params.period.unwrap_or(DEFAULT_PERIOD);
300    if period < 2 {
301        return Err(NormalizedResonatorError::InvalidPeriod { period });
302    }
303
304    let delta = params.delta.unwrap_or(DEFAULT_DELTA);
305    if !delta.is_finite() || delta <= 0.0 || delta > 1.0 {
306        return Err(NormalizedResonatorError::InvalidDelta { delta });
307    }
308
309    let lookback_mult = params.lookback_mult.unwrap_or(DEFAULT_LOOKBACK_MULT);
310    if !lookback_mult.is_finite() || lookback_mult <= 0.0 {
311        return Err(NormalizedResonatorError::InvalidLookbackMult { lookback_mult });
312    }
313
314    let signal_length = params.signal_length.unwrap_or(DEFAULT_SIGNAL_LENGTH);
315    if signal_length == 0 {
316        return Err(NormalizedResonatorError::InvalidSignalLength { signal_length });
317    }
318
319    let alpha = (PI * delta / period as f64).tan();
320    if !alpha.is_finite() {
321        return Err(NormalizedResonatorError::InvalidDelta { delta });
322    }
323    let beta = (2.0 * PI / period as f64).cos();
324    let r = 1.0 / (1.0 + alpha);
325    let c1 = 2.0 * r * beta;
326    let c2 = -(2.0 * r - 1.0);
327    let gain = alpha * r;
328    let peak_lookback_raw = period as f64 * lookback_mult;
329    if !peak_lookback_raw.is_finite() || peak_lookback_raw > usize::MAX as f64 {
330        return Err(NormalizedResonatorError::InvalidLookbackMult { lookback_mult });
331    }
332    let peak_lookback = peak_lookback_raw.floor().max(1.0) as usize;
333    let ema_alpha = 2.0 / (signal_length as f64 + 1.0);
334
335    Ok(ResolvedParams {
336        period,
337        delta,
338        lookback_mult,
339        signal_length,
340        peak_lookback,
341        gain,
342        c1,
343        c2,
344        ema_alpha,
345    })
346}
347
348#[derive(Clone, Debug)]
349struct RollingAbsMax {
350    window: usize,
351    next_index: usize,
352    deque: VecDeque<(usize, f64)>,
353}
354
355impl RollingAbsMax {
356    #[inline]
357    fn new(window: usize) -> Self {
358        Self {
359            window: window.max(1),
360            next_index: 0,
361            deque: VecDeque::new(),
362        }
363    }
364
365    #[inline]
366    fn reset(&mut self) {
367        self.next_index = 0;
368        self.deque.clear();
369    }
370
371    #[inline]
372    fn update(&mut self, value: f64) -> f64 {
373        let index = self.next_index;
374        self.next_index = self.next_index.wrapping_add(1);
375
376        while let Some(&(_, back_value)) = self.deque.back() {
377            if back_value <= value {
378                self.deque.pop_back();
379            } else {
380                break;
381            }
382        }
383        self.deque.push_back((index, value));
384
385        let min_index = index.saturating_add(1).saturating_sub(self.window);
386        while let Some(&(front_index, _)) = self.deque.front() {
387            if front_index < min_index {
388                self.deque.pop_front();
389            } else {
390                break;
391            }
392        }
393
394        self.deque
395            .front()
396            .map(|&(_, max_value)| max_value)
397            .unwrap_or(0.0)
398    }
399}
400
401#[derive(Clone, Debug)]
402pub struct NormalizedResonatorStream {
403    params: ResolvedParams,
404    src_prev1: f64,
405    src_prev2: f64,
406    src_count: usize,
407    bp_prev1: f64,
408    bp_prev2: f64,
409    peak_window: RollingAbsMax,
410    ema_value: f64,
411    ema_seeded: bool,
412}
413
414impl NormalizedResonatorStream {
415    #[inline]
416    pub fn try_new(params: NormalizedResonatorParams) -> Result<Self, NormalizedResonatorError> {
417        let params = resolve_params(&params)?;
418        Ok(Self {
419            src_prev1: 0.0,
420            src_prev2: 0.0,
421            src_count: 0,
422            bp_prev1: 0.0,
423            bp_prev2: 0.0,
424            peak_window: RollingAbsMax::new(params.peak_lookback),
425            ema_value: 0.0,
426            ema_seeded: false,
427            params,
428        })
429    }
430
431    #[inline]
432    pub fn reset(&mut self) {
433        self.src_prev1 = 0.0;
434        self.src_prev2 = 0.0;
435        self.src_count = 0;
436        self.bp_prev1 = 0.0;
437        self.bp_prev2 = 0.0;
438        self.peak_window.reset();
439        self.ema_value = 0.0;
440        self.ema_seeded = false;
441    }
442
443    #[inline]
444    pub fn get_warmup_period(&self) -> usize {
445        WARMUP
446    }
447
448    #[inline]
449    fn advance_source_history(&mut self, value: f64) {
450        match self.src_count {
451            0 => {
452                self.src_prev1 = value;
453                self.src_count = 1;
454            }
455            1 => {
456                self.src_prev2 = self.src_prev1;
457                self.src_prev1 = value;
458                self.src_count = 2;
459            }
460            _ => {
461                self.src_prev2 = self.src_prev1;
462                self.src_prev1 = value;
463            }
464        }
465    }
466
467    #[inline]
468    pub fn update(&mut self, value: f64) -> Option<(f64, f64)> {
469        if !value.is_finite() {
470            self.reset();
471            return None;
472        }
473
474        let out = if self.src_count >= 2 {
475            let bp = self.params.gain * (value - self.src_prev2)
476                + self.params.c1 * self.bp_prev1
477                + self.params.c2 * self.bp_prev2;
478            let peak = self.peak_window.update(bp.abs());
479            let oscillator = if peak > 0.0 { bp / peak } else { 0.0 };
480            let signal = if self.ema_seeded {
481                self.ema_value += self.params.ema_alpha * (oscillator - self.ema_value);
482                self.ema_value
483            } else {
484                self.ema_value = oscillator;
485                self.ema_seeded = true;
486                oscillator
487            };
488            self.bp_prev2 = self.bp_prev1;
489            self.bp_prev1 = bp;
490            Some((oscillator, signal))
491        } else {
492            None
493        };
494
495        self.advance_source_history(value);
496        out
497    }
498}
499
500#[inline(always)]
501fn normalized_resonator_prepare<'a>(
502    input: &'a NormalizedResonatorInput,
503    kernel: Kernel,
504) -> Result<(&'a [f64], usize, ResolvedParams, Kernel), NormalizedResonatorError> {
505    let data = input.as_ref();
506    if data.is_empty() {
507        return Err(NormalizedResonatorError::EmptyInputData);
508    }
509
510    let first = first_valid_value(data);
511    if first >= data.len() {
512        return Err(NormalizedResonatorError::AllValuesNaN);
513    }
514
515    let params = resolve_params(&input.params)?;
516    let valid = max_consecutive_valid_values(data);
517    if valid < MIN_VALID_SAMPLES {
518        return Err(NormalizedResonatorError::NotEnoughValidData {
519            needed: MIN_VALID_SAMPLES,
520            valid,
521        });
522    }
523
524    let chosen = match kernel {
525        Kernel::Auto => detect_best_kernel(),
526        other => other.to_non_batch(),
527    };
528    Ok((data, first, params, chosen))
529}
530
531#[inline(always)]
532fn normalized_resonator_row_from_slice(
533    data: &[f64],
534    params: ResolvedParams,
535    oscillator_out: &mut [f64],
536    signal_out: &mut [f64],
537) {
538    let mut stream = NormalizedResonatorStream::try_new(NormalizedResonatorParams {
539        period: Some(params.period),
540        delta: Some(params.delta),
541        lookback_mult: Some(params.lookback_mult),
542        signal_length: Some(params.signal_length),
543    })
544    .unwrap();
545
546    for ((oscillator_slot, signal_slot), &value) in oscillator_out
547        .iter_mut()
548        .zip(signal_out.iter_mut())
549        .zip(data.iter())
550    {
551        if let Some((oscillator, signal)) = stream.update(value) {
552            *oscillator_slot = oscillator;
553            *signal_slot = signal;
554        } else {
555            *oscillator_slot = f64::NAN;
556            *signal_slot = f64::NAN;
557        }
558    }
559}
560
561#[inline]
562pub fn normalized_resonator(
563    input: &NormalizedResonatorInput,
564) -> Result<NormalizedResonatorOutput, NormalizedResonatorError> {
565    normalized_resonator_with_kernel(input, Kernel::Auto)
566}
567
568#[inline]
569pub fn normalized_resonator_with_kernel(
570    input: &NormalizedResonatorInput,
571    kernel: Kernel,
572) -> Result<NormalizedResonatorOutput, NormalizedResonatorError> {
573    let (data, first, params, _chosen) = normalized_resonator_prepare(input, kernel)?;
574    let warmup = first.saturating_add(WARMUP).min(data.len());
575    let mut oscillator = alloc_with_nan_prefix(data.len(), warmup);
576    let mut signal = alloc_with_nan_prefix(data.len(), warmup);
577    normalized_resonator_row_from_slice(data, params, &mut oscillator, &mut signal);
578    Ok(NormalizedResonatorOutput { oscillator, signal })
579}
580
581#[inline]
582pub fn normalized_resonator_into_slices(
583    oscillator_out: &mut [f64],
584    signal_out: &mut [f64],
585    input: &NormalizedResonatorInput,
586    kernel: Kernel,
587) -> Result<(), NormalizedResonatorError> {
588    let expected = input.as_ref().len();
589    if oscillator_out.len() != expected || signal_out.len() != expected {
590        return Err(NormalizedResonatorError::OutputLengthMismatch {
591            expected,
592            oscillator_got: oscillator_out.len(),
593            signal_got: signal_out.len(),
594        });
595    }
596    let (data, _first, params, _chosen) = normalized_resonator_prepare(input, kernel)?;
597    normalized_resonator_row_from_slice(data, params, oscillator_out, signal_out);
598    Ok(())
599}
600
601#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
602#[inline]
603pub fn normalized_resonator_into(
604    input: &NormalizedResonatorInput,
605    oscillator_out: &mut [f64],
606    signal_out: &mut [f64],
607) -> Result<(), NormalizedResonatorError> {
608    normalized_resonator_into_slices(oscillator_out, signal_out, input, Kernel::Auto)
609}
610
611#[derive(Debug, Clone)]
612#[cfg_attr(
613    all(target_arch = "wasm32", feature = "wasm"),
614    derive(Serialize, Deserialize)
615)]
616pub struct NormalizedResonatorBatchRange {
617    pub period: (usize, usize, usize),
618    pub delta: (f64, f64, f64),
619    pub lookback_mult: (f64, f64, f64),
620    pub signal_length: (usize, usize, usize),
621}
622
623impl Default for NormalizedResonatorBatchRange {
624    fn default() -> Self {
625        Self {
626            period: (DEFAULT_PERIOD, DEFAULT_PERIOD, 0),
627            delta: (DEFAULT_DELTA, DEFAULT_DELTA, 0.0),
628            lookback_mult: (DEFAULT_LOOKBACK_MULT, DEFAULT_LOOKBACK_MULT, 0.0),
629            signal_length: (DEFAULT_SIGNAL_LENGTH, DEFAULT_SIGNAL_LENGTH, 0),
630        }
631    }
632}
633
634#[derive(Debug, Clone)]
635pub struct NormalizedResonatorBatchOutput {
636    pub oscillator: Vec<f64>,
637    pub signal: Vec<f64>,
638    pub combos: Vec<NormalizedResonatorParams>,
639    pub rows: usize,
640    pub cols: usize,
641}
642
643impl NormalizedResonatorBatchOutput {
644    #[inline]
645    pub fn row_for_params(&self, params: &NormalizedResonatorParams) -> Option<usize> {
646        self.combos.iter().position(|combo| {
647            combo.period.unwrap_or(DEFAULT_PERIOD) == params.period.unwrap_or(DEFAULT_PERIOD)
648                && (combo.delta.unwrap_or(DEFAULT_DELTA) - params.delta.unwrap_or(DEFAULT_DELTA))
649                    .abs()
650                    < FLOAT_TOL
651                && (combo.lookback_mult.unwrap_or(DEFAULT_LOOKBACK_MULT)
652                    - params.lookback_mult.unwrap_or(DEFAULT_LOOKBACK_MULT))
653                .abs()
654                    < FLOAT_TOL
655                && combo.signal_length.unwrap_or(DEFAULT_SIGNAL_LENGTH)
656                    == params.signal_length.unwrap_or(DEFAULT_SIGNAL_LENGTH)
657        })
658    }
659
660    #[inline]
661    pub fn row_slices(&self, row: usize) -> Option<(&[f64], &[f64])> {
662        if row >= self.rows {
663            return None;
664        }
665        let start = row * self.cols;
666        let end = start + self.cols;
667        Some((&self.oscillator[start..end], &self.signal[start..end]))
668    }
669}
670
671#[derive(Clone, Debug, Default)]
672pub struct NormalizedResonatorBatchBuilder {
673    range: NormalizedResonatorBatchRange,
674    kernel: Kernel,
675}
676
677impl NormalizedResonatorBatchBuilder {
678    #[inline]
679    pub fn new() -> Self {
680        Self::default()
681    }
682
683    #[inline]
684    pub fn kernel(mut self, kernel: Kernel) -> Self {
685        self.kernel = kernel;
686        self
687    }
688
689    #[inline]
690    pub fn period_range(mut self, start: usize, end: usize, step: usize) -> Self {
691        self.range.period = (start, end, step);
692        self
693    }
694
695    #[inline]
696    pub fn delta_range(mut self, start: f64, end: f64, step: f64) -> Self {
697        self.range.delta = (start, end, step);
698        self
699    }
700
701    #[inline]
702    pub fn lookback_mult_range(mut self, start: f64, end: f64, step: f64) -> Self {
703        self.range.lookback_mult = (start, end, step);
704        self
705    }
706
707    #[inline]
708    pub fn signal_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
709        self.range.signal_length = (start, end, step);
710        self
711    }
712
713    #[inline]
714    pub fn apply_slice(
715        self,
716        data: &[f64],
717    ) -> Result<NormalizedResonatorBatchOutput, NormalizedResonatorError> {
718        normalized_resonator_batch_with_kernel(data, &self.range, self.kernel)
719    }
720
721    #[inline]
722    pub fn apply_candles(
723        self,
724        candles: &Candles,
725        source: &str,
726    ) -> Result<NormalizedResonatorBatchOutput, NormalizedResonatorError> {
727        self.apply_slice(source_type(candles, source))
728    }
729}
730
731#[inline(always)]
732fn expand_axis_usize(
733    (start, end, step): (usize, usize, usize),
734) -> Result<Vec<usize>, NormalizedResonatorError> {
735    if step == 0 || start == end {
736        return Ok(vec![start]);
737    }
738
739    let mut out = Vec::new();
740    if start < end {
741        let mut x = start;
742        while x <= end {
743            out.push(x);
744            let next = x.saturating_add(step);
745            if next == x {
746                break;
747            }
748            x = next;
749        }
750    } else {
751        let mut x = start;
752        loop {
753            out.push(x);
754            if x == end {
755                break;
756            }
757            let next = x.saturating_sub(step);
758            if next == x || next < end {
759                break;
760            }
761            x = next;
762        }
763    }
764
765    if out.is_empty() {
766        return Err(NormalizedResonatorError::InvalidRange {
767            start: start.to_string(),
768            end: end.to_string(),
769            step: step.to_string(),
770        });
771    }
772    Ok(out)
773}
774
775#[inline(always)]
776fn expand_axis_f64(start: f64, end: f64, step: f64) -> Result<Vec<f64>, NormalizedResonatorError> {
777    if !start.is_finite() || !end.is_finite() || !step.is_finite() || start > end {
778        return Err(NormalizedResonatorError::InvalidRange {
779            start: start.to_string(),
780            end: end.to_string(),
781            step: step.to_string(),
782        });
783    }
784    if (start - end).abs() < FLOAT_TOL {
785        if step.abs() > FLOAT_TOL {
786            return Err(NormalizedResonatorError::InvalidRange {
787                start: start.to_string(),
788                end: end.to_string(),
789                step: step.to_string(),
790            });
791        }
792        return Ok(vec![start]);
793    }
794    if step <= 0.0 {
795        return Err(NormalizedResonatorError::InvalidRange {
796            start: start.to_string(),
797            end: end.to_string(),
798            step: step.to_string(),
799        });
800    }
801
802    let mut values = Vec::new();
803    let mut value = start;
804    while value <= end + FLOAT_TOL {
805        values.push(value.min(end));
806        value += step;
807    }
808    if (values.last().copied().unwrap_or(start) - end).abs() > 1e-9 {
809        return Err(NormalizedResonatorError::InvalidRange {
810            start: start.to_string(),
811            end: end.to_string(),
812            step: step.to_string(),
813        });
814    }
815    Ok(values)
816}
817
818#[inline(always)]
819fn expand_grid_normalized_resonator(
820    sweep: &NormalizedResonatorBatchRange,
821) -> Result<Vec<NormalizedResonatorParams>, NormalizedResonatorError> {
822    let periods = expand_axis_usize(sweep.period)?;
823    let deltas = expand_axis_f64(sweep.delta.0, sweep.delta.1, sweep.delta.2)?;
824    let lookback_mults = expand_axis_f64(
825        sweep.lookback_mult.0,
826        sweep.lookback_mult.1,
827        sweep.lookback_mult.2,
828    )?;
829    let signal_lengths = expand_axis_usize(sweep.signal_length)?;
830
831    let mut combos = Vec::with_capacity(
832        periods.len() * deltas.len() * lookback_mults.len() * signal_lengths.len(),
833    );
834    for period in periods {
835        for &delta in &deltas {
836            for &lookback_mult in &lookback_mults {
837                for signal_length in signal_lengths.iter().copied() {
838                    let combo = NormalizedResonatorParams {
839                        period: Some(period),
840                        delta: Some(delta),
841                        lookback_mult: Some(lookback_mult),
842                        signal_length: Some(signal_length),
843                    };
844                    let _ = resolve_params(&combo)?;
845                    combos.push(combo);
846                }
847            }
848        }
849    }
850    Ok(combos)
851}
852
853#[inline]
854pub fn normalized_resonator_batch_with_kernel(
855    data: &[f64],
856    sweep: &NormalizedResonatorBatchRange,
857    kernel: Kernel,
858) -> Result<NormalizedResonatorBatchOutput, NormalizedResonatorError> {
859    let batch_kernel = match kernel {
860        Kernel::Auto => detect_best_batch_kernel(),
861        other if other.is_batch() => other,
862        other => return Err(NormalizedResonatorError::InvalidKernelForBatch(other)),
863    };
864    normalized_resonator_batch_par_slice(data, sweep, batch_kernel.to_non_batch())
865}
866
867#[inline]
868pub fn normalized_resonator_batch_slice(
869    data: &[f64],
870    sweep: &NormalizedResonatorBatchRange,
871    kernel: Kernel,
872) -> Result<NormalizedResonatorBatchOutput, NormalizedResonatorError> {
873    normalized_resonator_batch_inner(data, sweep, kernel, false)
874}
875
876#[inline]
877pub fn normalized_resonator_batch_par_slice(
878    data: &[f64],
879    sweep: &NormalizedResonatorBatchRange,
880    kernel: Kernel,
881) -> Result<NormalizedResonatorBatchOutput, NormalizedResonatorError> {
882    normalized_resonator_batch_inner(data, sweep, kernel, true)
883}
884
885#[inline]
886pub fn normalized_resonator_batch_inner(
887    data: &[f64],
888    sweep: &NormalizedResonatorBatchRange,
889    _kernel: Kernel,
890    parallel: bool,
891) -> Result<NormalizedResonatorBatchOutput, NormalizedResonatorError> {
892    let combos = expand_grid_normalized_resonator(sweep)?;
893    let rows = combos.len();
894    let cols = data.len();
895    if cols == 0 {
896        return Err(NormalizedResonatorError::EmptyInputData);
897    }
898
899    let first = first_valid_value(data);
900    if first >= cols {
901        return Err(NormalizedResonatorError::AllValuesNaN);
902    }
903
904    let valid = max_consecutive_valid_values(data);
905    if valid < MIN_VALID_SAMPLES {
906        return Err(NormalizedResonatorError::NotEnoughValidData {
907            needed: MIN_VALID_SAMPLES,
908            valid,
909        });
910    }
911
912    let mut oscillator_mu = make_uninit_matrix(rows, cols);
913    let mut signal_mu = make_uninit_matrix(rows, cols);
914    init_matrix_prefixes(
915        &mut oscillator_mu,
916        cols,
917        &vec![first.saturating_add(WARMUP).min(cols); rows],
918    );
919    init_matrix_prefixes(
920        &mut signal_mu,
921        cols,
922        &vec![first.saturating_add(WARMUP).min(cols); rows],
923    );
924
925    let mut oscillator_guard = ManuallyDrop::new(oscillator_mu);
926    let mut signal_guard = ManuallyDrop::new(signal_mu);
927    let oscillator_out = unsafe {
928        std::slice::from_raw_parts_mut(
929            oscillator_guard.as_mut_ptr() as *mut f64,
930            oscillator_guard.len(),
931        )
932    };
933    let signal_out = unsafe {
934        std::slice::from_raw_parts_mut(signal_guard.as_mut_ptr() as *mut f64, signal_guard.len())
935    };
936
937    let combos = normalized_resonator_batch_inner_into(
938        data,
939        sweep,
940        _kernel,
941        parallel,
942        oscillator_out,
943        signal_out,
944    )?;
945
946    let oscillator = unsafe {
947        Vec::from_raw_parts(
948            oscillator_guard.as_mut_ptr() as *mut f64,
949            oscillator_guard.len(),
950            oscillator_guard.capacity(),
951        )
952    };
953    let signal = unsafe {
954        Vec::from_raw_parts(
955            signal_guard.as_mut_ptr() as *mut f64,
956            signal_guard.len(),
957            signal_guard.capacity(),
958        )
959    };
960
961    Ok(NormalizedResonatorBatchOutput {
962        oscillator,
963        signal,
964        combos,
965        rows,
966        cols,
967    })
968}
969
970#[inline]
971pub fn normalized_resonator_batch_inner_into(
972    data: &[f64],
973    sweep: &NormalizedResonatorBatchRange,
974    _kernel: Kernel,
975    parallel: bool,
976    oscillator_out: &mut [f64],
977    signal_out: &mut [f64],
978) -> Result<Vec<NormalizedResonatorParams>, NormalizedResonatorError> {
979    let combos = expand_grid_normalized_resonator(sweep)?;
980    let rows = combos.len();
981    let cols = data.len();
982    if cols == 0 {
983        return Err(NormalizedResonatorError::EmptyInputData);
984    }
985
986    let total = rows
987        .checked_mul(cols)
988        .ok_or(NormalizedResonatorError::OutputLengthMismatch {
989            expected: usize::MAX,
990            oscillator_got: oscillator_out.len(),
991            signal_got: signal_out.len(),
992        })?;
993    if oscillator_out.len() != total || signal_out.len() != total {
994        return Err(NormalizedResonatorError::OutputLengthMismatch {
995            expected: total,
996            oscillator_got: oscillator_out.len(),
997            signal_got: signal_out.len(),
998        });
999    }
1000
1001    let first = first_valid_value(data);
1002    if first >= cols {
1003        return Err(NormalizedResonatorError::AllValuesNaN);
1004    }
1005
1006    let valid = max_consecutive_valid_values(data);
1007    if valid < MIN_VALID_SAMPLES {
1008        return Err(NormalizedResonatorError::NotEnoughValidData {
1009            needed: MIN_VALID_SAMPLES,
1010            valid,
1011        });
1012    }
1013
1014    if parallel {
1015        #[cfg(not(target_arch = "wasm32"))]
1016        oscillator_out
1017            .par_chunks_mut(cols)
1018            .zip(signal_out.par_chunks_mut(cols))
1019            .enumerate()
1020            .for_each(|(row, (oscillator_row, signal_row))| {
1021                let params = resolve_params(&combos[row]).unwrap();
1022                normalized_resonator_row_from_slice(data, params, oscillator_row, signal_row);
1023            });
1024
1025        #[cfg(target_arch = "wasm32")]
1026        for (row, (oscillator_row, signal_row)) in oscillator_out
1027            .chunks_mut(cols)
1028            .zip(signal_out.chunks_mut(cols))
1029            .enumerate()
1030        {
1031            let params = resolve_params(&combos[row]).unwrap();
1032            normalized_resonator_row_from_slice(data, params, oscillator_row, signal_row);
1033        }
1034    } else {
1035        for (row, (oscillator_row, signal_row)) in oscillator_out
1036            .chunks_mut(cols)
1037            .zip(signal_out.chunks_mut(cols))
1038            .enumerate()
1039        {
1040            let params = resolve_params(&combos[row]).unwrap();
1041            normalized_resonator_row_from_slice(data, params, oscillator_row, signal_row);
1042        }
1043    }
1044
1045    Ok(combos)
1046}
1047
1048#[cfg(feature = "python")]
1049#[pyfunction(name = "normalized_resonator")]
1050#[pyo3(signature = (
1051    data,
1052    period=DEFAULT_PERIOD,
1053    delta=DEFAULT_DELTA,
1054    lookback_mult=DEFAULT_LOOKBACK_MULT,
1055    signal_length=DEFAULT_SIGNAL_LENGTH,
1056    kernel=None
1057))]
1058pub fn normalized_resonator_py<'py>(
1059    py: Python<'py>,
1060    data: PyReadonlyArray1<'py, f64>,
1061    period: usize,
1062    delta: f64,
1063    lookback_mult: f64,
1064    signal_length: usize,
1065    kernel: Option<&str>,
1066) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
1067    let data = data.as_slice()?;
1068    let kernel = validate_kernel(kernel, false)?;
1069    let input = NormalizedResonatorInput::from_slice(
1070        data,
1071        NormalizedResonatorParams {
1072            period: Some(period),
1073            delta: Some(delta),
1074            lookback_mult: Some(lookback_mult),
1075            signal_length: Some(signal_length),
1076        },
1077    );
1078    let output = py
1079        .allow_threads(|| normalized_resonator_with_kernel(&input, kernel))
1080        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1081    Ok((
1082        output.oscillator.into_pyarray(py),
1083        output.signal.into_pyarray(py),
1084    ))
1085}
1086
1087#[cfg(feature = "python")]
1088#[pyclass(name = "NormalizedResonatorStream")]
1089pub struct NormalizedResonatorStreamPy {
1090    stream: NormalizedResonatorStream,
1091}
1092
1093#[cfg(feature = "python")]
1094#[pymethods]
1095impl NormalizedResonatorStreamPy {
1096    #[new]
1097    #[pyo3(signature = (
1098        period=DEFAULT_PERIOD,
1099        delta=DEFAULT_DELTA,
1100        lookback_mult=DEFAULT_LOOKBACK_MULT,
1101        signal_length=DEFAULT_SIGNAL_LENGTH
1102    ))]
1103    fn new(period: usize, delta: f64, lookback_mult: f64, signal_length: usize) -> PyResult<Self> {
1104        let stream = NormalizedResonatorStream::try_new(NormalizedResonatorParams {
1105            period: Some(period),
1106            delta: Some(delta),
1107            lookback_mult: Some(lookback_mult),
1108            signal_length: Some(signal_length),
1109        })
1110        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1111        Ok(Self { stream })
1112    }
1113
1114    fn update(&mut self, value: f64) -> Option<(f64, f64)> {
1115        self.stream.update(value)
1116    }
1117
1118    #[getter]
1119    fn warmup_period(&self) -> usize {
1120        self.stream.get_warmup_period()
1121    }
1122}
1123
1124#[cfg(feature = "python")]
1125#[pyfunction(name = "normalized_resonator_batch")]
1126#[pyo3(signature = (
1127    data,
1128    period_range=(DEFAULT_PERIOD, DEFAULT_PERIOD, 0),
1129    delta_range=(DEFAULT_DELTA, DEFAULT_DELTA, 0.0),
1130    lookback_mult_range=(DEFAULT_LOOKBACK_MULT, DEFAULT_LOOKBACK_MULT, 0.0),
1131    signal_length_range=(DEFAULT_SIGNAL_LENGTH, DEFAULT_SIGNAL_LENGTH, 0),
1132    kernel=None
1133))]
1134pub fn normalized_resonator_batch_py<'py>(
1135    py: Python<'py>,
1136    data: PyReadonlyArray1<'py, f64>,
1137    period_range: (usize, usize, usize),
1138    delta_range: (f64, f64, f64),
1139    lookback_mult_range: (f64, f64, f64),
1140    signal_length_range: (usize, usize, usize),
1141    kernel: Option<&str>,
1142) -> PyResult<Bound<'py, PyDict>> {
1143    let data = data.as_slice()?;
1144    let kernel = validate_kernel(kernel, true)?;
1145    let sweep = NormalizedResonatorBatchRange {
1146        period: period_range,
1147        delta: delta_range,
1148        lookback_mult: lookback_mult_range,
1149        signal_length: signal_length_range,
1150    };
1151    let combos = expand_grid_normalized_resonator(&sweep)
1152        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1153    let rows = combos.len();
1154    let cols = data.len();
1155    let total = rows
1156        .checked_mul(cols)
1157        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1158
1159    let oscillator_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1160    let signal_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1161    let oscillator_slice = unsafe { oscillator_arr.as_slice_mut()? };
1162    let signal_slice = unsafe { signal_arr.as_slice_mut()? };
1163
1164    let combos = py
1165        .allow_threads(|| {
1166            let batch = match kernel {
1167                Kernel::Auto => detect_best_batch_kernel(),
1168                other => other,
1169            };
1170            normalized_resonator_batch_inner_into(
1171                data,
1172                &sweep,
1173                batch.to_non_batch(),
1174                true,
1175                oscillator_slice,
1176                signal_slice,
1177            )
1178        })
1179        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1180
1181    let dict = PyDict::new(py);
1182    dict.set_item("oscillator", oscillator_arr.reshape((rows, cols))?)?;
1183    dict.set_item("signal", signal_arr.reshape((rows, cols))?)?;
1184    dict.set_item(
1185        "periods",
1186        combos
1187            .iter()
1188            .map(|combo| combo.period.unwrap_or(DEFAULT_PERIOD) as u64)
1189            .collect::<Vec<_>>()
1190            .into_pyarray(py),
1191    )?;
1192    dict.set_item(
1193        "deltas",
1194        combos
1195            .iter()
1196            .map(|combo| combo.delta.unwrap_or(DEFAULT_DELTA))
1197            .collect::<Vec<_>>()
1198            .into_pyarray(py),
1199    )?;
1200    dict.set_item(
1201        "lookback_multipliers",
1202        combos
1203            .iter()
1204            .map(|combo| combo.lookback_mult.unwrap_or(DEFAULT_LOOKBACK_MULT))
1205            .collect::<Vec<_>>()
1206            .into_pyarray(py),
1207    )?;
1208    dict.set_item(
1209        "signal_lengths",
1210        combos
1211            .iter()
1212            .map(|combo| combo.signal_length.unwrap_or(DEFAULT_SIGNAL_LENGTH) as u64)
1213            .collect::<Vec<_>>()
1214            .into_pyarray(py),
1215    )?;
1216    dict.set_item("rows", rows)?;
1217    dict.set_item("cols", cols)?;
1218    Ok(dict)
1219}
1220
1221#[cfg(feature = "python")]
1222pub fn register_normalized_resonator_module(
1223    module: &Bound<'_, pyo3::types::PyModule>,
1224) -> PyResult<()> {
1225    module.add_function(wrap_pyfunction!(normalized_resonator_py, module)?)?;
1226    module.add_function(wrap_pyfunction!(normalized_resonator_batch_py, module)?)?;
1227    module.add_class::<NormalizedResonatorStreamPy>()?;
1228    Ok(())
1229}
1230
1231#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1232#[derive(Serialize, Deserialize)]
1233pub struct NormalizedResonatorJsOutput {
1234    pub oscillator: Vec<f64>,
1235    pub signal: Vec<f64>,
1236}
1237
1238#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1239#[wasm_bindgen(js_name = "normalized_resonator_js")]
1240pub fn normalized_resonator_js(
1241    data: &[f64],
1242    period: usize,
1243    delta: f64,
1244    lookback_mult: f64,
1245    signal_length: usize,
1246) -> Result<JsValue, JsValue> {
1247    let input = NormalizedResonatorInput::from_slice(
1248        data,
1249        NormalizedResonatorParams {
1250            period: Some(period),
1251            delta: Some(delta),
1252            lookback_mult: Some(lookback_mult),
1253            signal_length: Some(signal_length),
1254        },
1255    );
1256    let output = normalized_resonator(&input).map_err(|e| JsValue::from_str(&e.to_string()))?;
1257    serde_wasm_bindgen::to_value(&NormalizedResonatorJsOutput {
1258        oscillator: output.oscillator,
1259        signal: output.signal,
1260    })
1261    .map_err(|e| JsValue::from_str(&e.to_string()))
1262}
1263
1264#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1265#[wasm_bindgen]
1266pub fn normalized_resonator_alloc(len: usize) -> *mut f64 {
1267    let mut vec = Vec::<f64>::with_capacity(len);
1268    let ptr = vec.as_mut_ptr();
1269    std::mem::forget(vec);
1270    ptr
1271}
1272
1273#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1274#[wasm_bindgen]
1275pub fn normalized_resonator_free(ptr: *mut f64, len: usize) {
1276    if !ptr.is_null() {
1277        unsafe {
1278            let _ = Vec::from_raw_parts(ptr, len, len);
1279        }
1280    }
1281}
1282
1283#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1284#[wasm_bindgen]
1285pub fn normalized_resonator_into(
1286    in_ptr: *const f64,
1287    oscillator_out_ptr: *mut f64,
1288    signal_out_ptr: *mut f64,
1289    len: usize,
1290    period: usize,
1291    delta: f64,
1292    lookback_mult: f64,
1293    signal_length: usize,
1294) -> Result<(), JsValue> {
1295    if in_ptr.is_null() || oscillator_out_ptr.is_null() || signal_out_ptr.is_null() {
1296        return Err(JsValue::from_str("Null pointer provided"));
1297    }
1298    unsafe {
1299        let data = std::slice::from_raw_parts(in_ptr, len);
1300        let input = NormalizedResonatorInput::from_slice(
1301            data,
1302            NormalizedResonatorParams {
1303                period: Some(period),
1304                delta: Some(delta),
1305                lookback_mult: Some(lookback_mult),
1306                signal_length: Some(signal_length),
1307            },
1308        );
1309        let oscillator_out = std::slice::from_raw_parts_mut(oscillator_out_ptr, len);
1310        let signal_out = std::slice::from_raw_parts_mut(signal_out_ptr, len);
1311        normalized_resonator_into_slices(oscillator_out, signal_out, &input, Kernel::Auto)
1312            .map_err(|e| JsValue::from_str(&e.to_string()))?;
1313    }
1314    Ok(())
1315}
1316
1317#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1318#[derive(Serialize, Deserialize)]
1319pub struct NormalizedResonatorBatchJsConfig {
1320    pub period_range: Option<(usize, usize, usize)>,
1321    pub delta_range: Option<(f64, f64, f64)>,
1322    pub lookback_mult_range: Option<(f64, f64, f64)>,
1323    pub signal_length_range: Option<(usize, usize, usize)>,
1324}
1325
1326#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1327#[derive(Serialize, Deserialize)]
1328pub struct NormalizedResonatorBatchJsOutput {
1329    pub oscillator: Vec<f64>,
1330    pub signal: Vec<f64>,
1331    pub combos: Vec<NormalizedResonatorParams>,
1332    pub rows: usize,
1333    pub cols: usize,
1334}
1335
1336#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1337#[wasm_bindgen(js_name = "normalized_resonator_batch_js")]
1338pub fn normalized_resonator_batch_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
1339    let config: NormalizedResonatorBatchJsConfig =
1340        serde_wasm_bindgen::from_value(config).map_err(|e| JsValue::from_str(&e.to_string()))?;
1341    let sweep = NormalizedResonatorBatchRange {
1342        period: config
1343            .period_range
1344            .unwrap_or((DEFAULT_PERIOD, DEFAULT_PERIOD, 0)),
1345        delta: config
1346            .delta_range
1347            .unwrap_or((DEFAULT_DELTA, DEFAULT_DELTA, 0.0)),
1348        lookback_mult: config.lookback_mult_range.unwrap_or((
1349            DEFAULT_LOOKBACK_MULT,
1350            DEFAULT_LOOKBACK_MULT,
1351            0.0,
1352        )),
1353        signal_length: config.signal_length_range.unwrap_or((
1354            DEFAULT_SIGNAL_LENGTH,
1355            DEFAULT_SIGNAL_LENGTH,
1356            0,
1357        )),
1358    };
1359    let output = normalized_resonator_batch_with_kernel(data, &sweep, Kernel::Auto)
1360        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1361    serde_wasm_bindgen::to_value(&NormalizedResonatorBatchJsOutput {
1362        oscillator: output.oscillator,
1363        signal: output.signal,
1364        combos: output.combos,
1365        rows: output.rows,
1366        cols: output.cols,
1367    })
1368    .map_err(|e| JsValue::from_str(&e.to_string()))
1369}
1370
1371#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1372#[wasm_bindgen]
1373pub fn normalized_resonator_batch_into(
1374    in_ptr: *const f64,
1375    oscillator_out_ptr: *mut f64,
1376    signal_out_ptr: *mut f64,
1377    len: usize,
1378    period_start: usize,
1379    period_end: usize,
1380    period_step: usize,
1381    delta_start: f64,
1382    delta_end: f64,
1383    delta_step: f64,
1384    lookback_mult_start: f64,
1385    lookback_mult_end: f64,
1386    lookback_mult_step: f64,
1387    signal_length_start: usize,
1388    signal_length_end: usize,
1389    signal_length_step: usize,
1390) -> Result<usize, JsValue> {
1391    if in_ptr.is_null() || oscillator_out_ptr.is_null() || signal_out_ptr.is_null() {
1392        return Err(JsValue::from_str("Null pointer provided"));
1393    }
1394
1395    let sweep = NormalizedResonatorBatchRange {
1396        period: (period_start, period_end, period_step),
1397        delta: (delta_start, delta_end, delta_step),
1398        lookback_mult: (lookback_mult_start, lookback_mult_end, lookback_mult_step),
1399        signal_length: (signal_length_start, signal_length_end, signal_length_step),
1400    };
1401
1402    unsafe {
1403        let data = std::slice::from_raw_parts(in_ptr, len);
1404        let combos = expand_grid_normalized_resonator(&sweep)
1405            .map_err(|e| JsValue::from_str(&e.to_string()))?;
1406        let rows = combos.len();
1407        let total = rows
1408            .checked_mul(len)
1409            .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
1410        let oscillator_out = std::slice::from_raw_parts_mut(oscillator_out_ptr, total);
1411        let signal_out = std::slice::from_raw_parts_mut(signal_out_ptr, total);
1412        let rows = normalized_resonator_batch_inner_into(
1413            data,
1414            &sweep,
1415            Kernel::Auto,
1416            false,
1417            oscillator_out,
1418            signal_out,
1419        )
1420        .map_err(|e| JsValue::from_str(&e.to_string()))?
1421        .len();
1422        Ok(rows)
1423    }
1424}
1425
1426#[cfg(test)]
1427mod tests {
1428    use super::*;
1429    use crate::utilities::data_loader::Candles;
1430
1431    fn sample_source(length: usize) -> Vec<f64> {
1432        let mut out = Vec::with_capacity(length);
1433        for i in 0..length {
1434            let x = i as f64;
1435            out.push(100.0 + x * 0.03 + (x * 0.09).sin() * 2.1 + (x * 0.02).cos() * 0.7);
1436        }
1437        out
1438    }
1439
1440    fn sample_candles(length: usize) -> Candles {
1441        let open: Vec<f64> = (0..length)
1442            .map(|i| 100.0 + i as f64 * 0.03 + (i as f64 * 0.07).sin())
1443            .collect();
1444        let close: Vec<f64> = open
1445            .iter()
1446            .enumerate()
1447            .map(|(i, &o)| o + (i as f64 * 0.11).cos() * 0.8)
1448            .collect();
1449        let high: Vec<f64> = open
1450            .iter()
1451            .zip(close.iter())
1452            .enumerate()
1453            .map(|(i, (&o, &c))| o.max(c) + 0.6 + (i as f64 * 0.05).sin().abs() * 0.2)
1454            .collect();
1455        let low: Vec<f64> = open
1456            .iter()
1457            .zip(close.iter())
1458            .enumerate()
1459            .map(|(i, (&o, &c))| o.min(c) - 0.6 - (i as f64 * 0.03).cos().abs() * 0.2)
1460            .collect();
1461        Candles::new(
1462            (0..length as i64).collect(),
1463            open,
1464            high,
1465            low,
1466            close,
1467            vec![1_000.0; length],
1468        )
1469    }
1470
1471    fn assert_series_eq(left: &[f64], right: &[f64], tol: f64) {
1472        assert_eq!(left.len(), right.len());
1473        for (&lhs, &rhs) in left.iter().zip(right.iter()) {
1474            if lhs.is_nan() && rhs.is_nan() {
1475                continue;
1476            }
1477            assert!((lhs - rhs).abs() <= tol, "lhs={lhs}, rhs={rhs}");
1478        }
1479    }
1480
1481    #[test]
1482    fn normalized_resonator_output_contract() {
1483        let data = sample_source(256);
1484        let out = normalized_resonator(&NormalizedResonatorInput::from_slice(
1485            &data,
1486            NormalizedResonatorParams::default(),
1487        ))
1488        .unwrap();
1489
1490        assert_eq!(out.oscillator.len(), data.len());
1491        assert_eq!(out.signal.len(), data.len());
1492        assert_eq!(
1493            out.oscillator.iter().position(|v| v.is_finite()),
1494            Some(WARMUP)
1495        );
1496        assert_eq!(out.signal.iter().position(|v| v.is_finite()), Some(WARMUP));
1497        assert!(out.oscillator.last().copied().unwrap().is_finite());
1498        assert!(out.signal.last().copied().unwrap().is_finite());
1499    }
1500
1501    #[test]
1502    fn normalized_resonator_rejects_invalid_parameters() {
1503        let data = sample_source(32);
1504
1505        let err = normalized_resonator(&NormalizedResonatorInput::from_slice(
1506            &data,
1507            NormalizedResonatorParams {
1508                period: Some(1),
1509                ..NormalizedResonatorParams::default()
1510            },
1511        ))
1512        .unwrap_err();
1513        assert!(matches!(
1514            err,
1515            NormalizedResonatorError::InvalidPeriod { .. }
1516        ));
1517
1518        let err = normalized_resonator(&NormalizedResonatorInput::from_slice(
1519            &data,
1520            NormalizedResonatorParams {
1521                delta: Some(0.0),
1522                ..NormalizedResonatorParams::default()
1523            },
1524        ))
1525        .unwrap_err();
1526        assert!(matches!(err, NormalizedResonatorError::InvalidDelta { .. }));
1527
1528        let err = normalized_resonator(&NormalizedResonatorInput::from_slice(
1529            &data,
1530            NormalizedResonatorParams {
1531                signal_length: Some(0),
1532                ..NormalizedResonatorParams::default()
1533            },
1534        ))
1535        .unwrap_err();
1536        assert!(matches!(
1537            err,
1538            NormalizedResonatorError::InvalidSignalLength { .. }
1539        ));
1540    }
1541
1542    #[test]
1543    fn normalized_resonator_builder_supports_candles() {
1544        let candles = sample_candles(180);
1545        let out = NormalizedResonatorBuilder::new()
1546            .apply(&candles, "hl2")
1547            .unwrap();
1548        assert_eq!(out.oscillator.len(), candles.close.len());
1549        assert_eq!(out.signal.len(), candles.close.len());
1550        assert!(out.oscillator.last().copied().unwrap().is_finite());
1551        assert!(out.signal.last().copied().unwrap().is_finite());
1552    }
1553
1554    #[test]
1555    fn normalized_resonator_stream_matches_batch_with_reset() {
1556        let mut data = sample_source(220);
1557        data[110] = f64::NAN;
1558
1559        let input = NormalizedResonatorInput::from_slice(
1560            &data,
1561            NormalizedResonatorParams {
1562                period: Some(48),
1563                delta: Some(0.4),
1564                lookback_mult: Some(1.2),
1565                signal_length: Some(7),
1566            },
1567        );
1568        let batch = normalized_resonator(&input).unwrap();
1569        let mut stream = NormalizedResonatorStream::try_new(input.params.clone()).unwrap();
1570
1571        let mut oscillator = Vec::with_capacity(data.len());
1572        let mut signal = Vec::with_capacity(data.len());
1573        for &value in &data {
1574            if let Some((osc, sig)) = stream.update(value) {
1575                oscillator.push(osc);
1576                signal.push(sig);
1577            } else {
1578                oscillator.push(f64::NAN);
1579                signal.push(f64::NAN);
1580            }
1581        }
1582
1583        assert_series_eq(&oscillator, &batch.oscillator, 1e-12);
1584        assert_series_eq(&signal, &batch.signal, 1e-12);
1585    }
1586
1587    #[test]
1588    fn normalized_resonator_into_matches_api() {
1589        let data = sample_source(128);
1590        let input =
1591            NormalizedResonatorInput::from_slice(&data, NormalizedResonatorParams::default());
1592        let direct = normalized_resonator(&input).unwrap();
1593        let mut oscillator = vec![f64::NAN; data.len()];
1594        let mut signal = vec![f64::NAN; data.len()];
1595        normalized_resonator_into(&input, &mut oscillator, &mut signal).unwrap();
1596        assert_series_eq(&oscillator, &direct.oscillator, 1e-12);
1597        assert_series_eq(&signal, &direct.signal, 1e-12);
1598    }
1599
1600    #[test]
1601    fn normalized_resonator_batch_single_param_matches_single() {
1602        let data = sample_source(192);
1603        let batch = normalized_resonator_batch_with_kernel(
1604            &data,
1605            &NormalizedResonatorBatchRange {
1606                period: (48, 48, 0),
1607                delta: (0.4, 0.4, 0.0),
1608                lookback_mult: (1.2, 1.2, 0.0),
1609                signal_length: (7, 7, 0),
1610            },
1611            Kernel::Auto,
1612        )
1613        .unwrap();
1614        let direct = normalized_resonator(&NormalizedResonatorInput::from_slice(
1615            &data,
1616            NormalizedResonatorParams {
1617                period: Some(48),
1618                delta: Some(0.4),
1619                lookback_mult: Some(1.2),
1620                signal_length: Some(7),
1621            },
1622        ))
1623        .unwrap();
1624
1625        assert_eq!(batch.rows, 1);
1626        assert_eq!(batch.cols, data.len());
1627        let (oscillator, signal) = batch.row_slices(0).unwrap();
1628        assert_series_eq(oscillator, &direct.oscillator, 1e-12);
1629        assert_series_eq(signal, &direct.signal, 1e-12);
1630    }
1631
1632    #[test]
1633    fn normalized_resonator_batch_metadata() {
1634        let data = sample_source(160);
1635        let batch = normalized_resonator_batch_with_kernel(
1636            &data,
1637            &NormalizedResonatorBatchRange {
1638                period: (40, 44, 4),
1639                delta: (0.3, 0.5, 0.2),
1640                lookback_mult: (1.0, 1.2, 0.2),
1641                signal_length: (5, 6, 1),
1642            },
1643            Kernel::Auto,
1644        )
1645        .unwrap();
1646
1647        assert_eq!(batch.rows, 16);
1648        assert_eq!(batch.cols, data.len());
1649        assert_eq!(batch.oscillator.len(), 16 * data.len());
1650        assert_eq!(batch.signal.len(), 16 * data.len());
1651    }
1652}