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