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(¶ms)?;
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}