1use crate::utilities::data_loader::{source_type, Candles};
2use crate::utilities::enums::Kernel;
3use crate::utilities::helpers::{
4 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
5 make_uninit_matrix,
6};
7use aligned_vec::{AVec, CACHELINE_ALIGN};
8#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
9use core::arch::x86_64::*;
10#[cfg(not(target_arch = "wasm32"))]
11use rayon::prelude::*;
12use std::convert::AsRef;
13use std::mem::MaybeUninit;
14use thiserror::Error;
15
16#[cfg(all(feature = "python", feature = "cuda"))]
17use crate::utilities::dlpack_cuda::{make_device_array_py, DeviceArrayF32Py};
18#[cfg(feature = "python")]
19use crate::utilities::kernel_validation::validate_kernel;
20#[cfg(feature = "python")]
21use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
22#[cfg(feature = "python")]
23use pyo3::{
24 exceptions::PyValueError,
25 pyclass, pyfunction, pymethods,
26 types::{PyDict, PyDictMethods},
27 Bound, PyErr, PyResult, Python,
28};
29#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
30use serde::{Deserialize, Serialize};
31#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
32use wasm_bindgen::prelude::*;
33
34impl<'a> AsRef<[f64]> for FoscInput<'a> {
35 #[inline(always)]
36 fn as_ref(&self) -> &[f64] {
37 match &self.data {
38 FoscData::Slice(slice) => slice,
39 FoscData::Candles { candles, source } => source_type(candles, source),
40 }
41 }
42}
43
44#[derive(Debug, Clone)]
45pub enum FoscData<'a> {
46 Candles {
47 candles: &'a Candles,
48 source: &'a str,
49 },
50 Slice(&'a [f64]),
51}
52
53#[derive(Debug, Clone)]
54pub struct FoscOutput {
55 pub values: Vec<f64>,
56}
57
58#[derive(Debug, Clone)]
59#[cfg_attr(
60 all(target_arch = "wasm32", feature = "wasm"),
61 derive(Serialize, Deserialize)
62)]
63pub struct FoscParams {
64 pub period: Option<usize>,
65}
66
67impl Default for FoscParams {
68 fn default() -> Self {
69 Self { period: Some(5) }
70 }
71}
72
73#[derive(Debug, Clone)]
74pub struct FoscInput<'a> {
75 pub data: FoscData<'a>,
76 pub params: FoscParams,
77}
78
79impl<'a> FoscInput<'a> {
80 #[inline]
81 pub fn from_candles(c: &'a Candles, s: &'a str, p: FoscParams) -> Self {
82 Self {
83 data: FoscData::Candles {
84 candles: c,
85 source: s,
86 },
87 params: p,
88 }
89 }
90 #[inline]
91 pub fn from_slice(sl: &'a [f64], p: FoscParams) -> Self {
92 Self {
93 data: FoscData::Slice(sl),
94 params: p,
95 }
96 }
97 #[inline]
98 pub fn with_default_candles(c: &'a Candles) -> Self {
99 Self::from_candles(c, "close", FoscParams::default())
100 }
101 #[inline]
102 pub fn get_period(&self) -> usize {
103 self.params.period.unwrap_or(5)
104 }
105}
106
107#[derive(Copy, Clone, Debug)]
108pub struct FoscBuilder {
109 period: Option<usize>,
110 kernel: Kernel,
111}
112
113impl Default for FoscBuilder {
114 fn default() -> Self {
115 Self {
116 period: None,
117 kernel: Kernel::Auto,
118 }
119 }
120}
121
122impl FoscBuilder {
123 #[inline(always)]
124 pub fn new() -> Self {
125 Self::default()
126 }
127 #[inline(always)]
128 pub fn period(mut self, n: usize) -> Self {
129 self.period = Some(n);
130 self
131 }
132 #[inline(always)]
133 pub fn kernel(mut self, k: Kernel) -> Self {
134 self.kernel = k;
135 self
136 }
137 #[inline(always)]
138 pub fn apply(self, c: &Candles) -> Result<FoscOutput, FoscError> {
139 let p = FoscParams {
140 period: self.period,
141 };
142 let i = FoscInput::from_candles(c, "close", p);
143 fosc_with_kernel(&i, self.kernel)
144 }
145 #[inline(always)]
146 pub fn apply_slice(self, d: &[f64]) -> Result<FoscOutput, FoscError> {
147 let p = FoscParams {
148 period: self.period,
149 };
150 let i = FoscInput::from_slice(d, p);
151 fosc_with_kernel(&i, self.kernel)
152 }
153 #[inline(always)]
154 pub fn into_stream(self) -> Result<FoscStream, FoscError> {
155 let p = FoscParams {
156 period: self.period,
157 };
158 FoscStream::try_new(p)
159 }
160}
161
162#[derive(Debug, Error)]
163pub enum FoscError {
164 #[error("fosc: Empty input data provided")]
165 EmptyInputData,
166 #[error("fosc: All values are NaN.")]
167 AllValuesNaN,
168 #[error("fosc: Invalid period: period = {period}, data length = {data_len}")]
169 InvalidPeriod { period: usize, data_len: usize },
170 #[error("fosc: Not enough valid data: needed = {needed}, valid = {valid}")]
171 NotEnoughValidData { needed: usize, valid: usize },
172 #[error("fosc: Output length mismatch: expected = {expected}, got = {got}")]
173 OutputLengthMismatch { expected: usize, got: usize },
174 #[error("fosc: Invalid kernel for batch operation: {0:?}")]
175 InvalidKernelForBatch(crate::utilities::enums::Kernel),
176 #[error("fosc: Invalid range expansion: start={start}, end={end}, step={step}")]
177 InvalidRange {
178 start: usize,
179 end: usize,
180 step: usize,
181 },
182}
183
184#[inline]
185pub fn fosc(input: &FoscInput) -> Result<FoscOutput, FoscError> {
186 fosc_with_kernel(input, Kernel::Auto)
187}
188
189pub fn fosc_with_kernel(input: &FoscInput, kernel: Kernel) -> Result<FoscOutput, FoscError> {
190 let data: &[f64] = input.as_ref();
191 let len = data.len();
192 if len == 0 {
193 return Err(FoscError::EmptyInputData);
194 }
195 let period = input.get_period();
196 let first = data
197 .iter()
198 .position(|x| !x.is_nan())
199 .ok_or(FoscError::AllValuesNaN)?;
200 if period == 0 || period > len {
201 return Err(FoscError::InvalidPeriod {
202 period,
203 data_len: len,
204 });
205 }
206 if (len - first) < period {
207 return Err(FoscError::NotEnoughValidData {
208 needed: period,
209 valid: len - first,
210 });
211 }
212 let chosen = match kernel {
213 Kernel::Auto => detect_best_kernel(),
214 other => other,
215 };
216
217 let mut out = alloc_with_nan_prefix(len, first + period - 1);
218 unsafe {
219 match chosen {
220 Kernel::Scalar | Kernel::ScalarBatch => fosc_scalar(data, period, first, &mut out),
221 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
222 Kernel::Avx2 | Kernel::Avx2Batch => fosc_avx2(data, period, first, &mut out),
223 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
224 Kernel::Avx512 | Kernel::Avx512Batch => fosc_avx512(data, period, first, &mut out),
225 _ => unreachable!(),
226 }
227 }
228 Ok(FoscOutput { values: out })
229}
230
231#[inline]
232pub fn fosc_into_slice(dst: &mut [f64], input: &FoscInput, kern: Kernel) -> Result<(), FoscError> {
233 let data: &[f64] = input.as_ref();
234 let len = data.len();
235 let period = input.get_period();
236
237 if len == 0 {
238 return Err(FoscError::EmptyInputData);
239 }
240
241 if dst.len() != len {
242 return Err(FoscError::OutputLengthMismatch {
243 expected: len,
244 got: dst.len(),
245 });
246 }
247
248 let first = data
249 .iter()
250 .position(|x| !x.is_nan())
251 .ok_or(FoscError::AllValuesNaN)?;
252 if period == 0 || period > len {
253 return Err(FoscError::InvalidPeriod {
254 period,
255 data_len: len,
256 });
257 }
258 if (len - first) < period {
259 return Err(FoscError::NotEnoughValidData {
260 needed: period,
261 valid: len - first,
262 });
263 }
264
265 let chosen = match kern {
266 Kernel::Auto => detect_best_kernel(),
267 other => other,
268 };
269
270 unsafe {
271 match chosen {
272 Kernel::Scalar | Kernel::ScalarBatch => fosc_scalar(data, period, first, dst),
273 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
274 Kernel::Avx2 | Kernel::Avx2Batch => fosc_avx2(data, period, first, dst),
275 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
276 Kernel::Avx512 | Kernel::Avx512Batch => fosc_avx512(data, period, first, dst),
277 _ => unreachable!(),
278 }
279 }
280
281 let warmup_end = first + period - 1;
282 for v in &mut dst[..warmup_end] {
283 *v = f64::NAN;
284 }
285
286 Ok(())
287}
288
289#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
290#[inline]
291pub fn fosc_into(input: &FoscInput, out: &mut [f64]) -> Result<(), FoscError> {
292 fosc_into_slice(out, input, Kernel::Auto)
293}
294
295#[inline(always)]
296unsafe fn fosc_core(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
297 let n = data.len();
298 if n == 0 || period == 0 || n < period {
299 return;
300 }
301
302 let begin = first + period - 1;
303 if begin >= n {
304 return;
305 }
306
307 let p = period as f64;
308 let x = 0.5 * p * (p + 1.0);
309 let x2 = (p * (p + 1.0) * (2.0 * p + 1.0)) / 6.0;
310 let denom = p * x2 - x * x;
311 let bd = if denom.abs() < f64::EPSILON {
312 0.0
313 } else {
314 1.0 / denom
315 };
316 let inv_p = 1.0 / p;
317
318 let p_bd = p * bd;
319 let x_bd = x * bd;
320
321 let tsf_coeff = 0.5 * (p + 1.0);
322
323 let mut y = 0.0f64;
324 let mut xy = 0.0f64;
325 let base = data.as_ptr().add(first);
326 let limit = period - 1;
327 let mut k = 0usize;
328 while k + 4 <= limit {
329 let d0 = *base.add(k + 0);
330 let d1 = *base.add(k + 1);
331 let d2 = *base.add(k + 2);
332 let d3 = *base.add(k + 3);
333
334 y += d0 + d1 + d2 + d3;
335 xy += d0 * (k + 1) as f64;
336 xy += d1 * (k + 2) as f64;
337 xy += d2 * (k + 3) as f64;
338 xy += d3 * (k + 4) as f64;
339 k += 4;
340 }
341 while k < limit {
342 let d = *base.add(k);
343 y += d;
344 xy += d * (k + 1) as f64;
345 k += 1;
346 }
347
348 let dp = data.as_ptr();
349 let op = out.as_mut_ptr();
350 let mut tsf_prev = 0.0f64;
351 let mut i = begin;
352 while i < n {
353 let newv = *dp.add(i);
354
355 let y_plus = y + newv;
356 let xy_plus = xy + newv * p;
357
358 *op.add(i) = if newv != 0.0 {
359 100.0 * (newv - tsf_prev) / newv
360 } else {
361 f64::NAN
362 };
363
364 let b = xy_plus * p_bd - y_plus * x_bd;
365 tsf_prev = y_plus * inv_p + b * tsf_coeff;
366
367 let old_idx = i + 1 - period;
368 let oldv = *dp.add(old_idx);
369 xy = xy_plus - y_plus;
370 y = y_plus - oldv;
371
372 i += 1;
373 }
374}
375
376#[inline]
377pub fn fosc_scalar(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
378 unsafe { fosc_core(data, period, first, out) }
379}
380
381#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
382#[inline]
383pub unsafe fn fosc_avx2(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
384 fosc_core(data, period, first, out)
385}
386
387#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
388#[inline]
389pub unsafe fn fosc_avx512(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
390 fosc_avx2(data, period, first, out)
391}
392
393#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
394#[inline]
395pub unsafe fn fosc_avx512_short(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
396 fosc_avx2(data, period, first, out)
397}
398
399#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
400#[inline]
401pub unsafe fn fosc_avx512_long(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
402 fosc_avx2(data, period, first, out)
403}
404
405#[derive(Debug, Clone)]
406pub struct FoscStream {
407 period: usize,
408 buffer: Vec<f64>,
409 idx: usize,
410 filled: bool,
411
412 x: f64,
413 x2: f64,
414 inv_den: f64,
415 inv_p: f64,
416 p_f64: f64,
417 p1: f64,
418
419 y: f64,
420 xy: f64,
421
422 tsf: f64,
423
424 count: usize,
425}
426
427impl FoscStream {
428 pub fn try_new(params: FoscParams) -> Result<Self, FoscError> {
429 let period = params.period.unwrap_or(5);
430 if period == 0 {
431 return Err(FoscError::InvalidPeriod {
432 period,
433 data_len: 0,
434 });
435 }
436 let p = period as f64;
437
438 let x = 0.5 * p * (p + 1.0);
439 let x2 = (p * (p + 1.0) * (2.0 * p + 1.0)) / 6.0;
440 let den = p * x2 - x * x;
441 let inv_den = if den.abs() < f64::EPSILON {
442 0.0
443 } else {
444 1.0 / den
445 };
446
447 Ok(Self {
448 period,
449 buffer: vec![0.0; period],
450 idx: 0,
451 filled: false,
452
453 x,
454 x2,
455 inv_den,
456 inv_p: 1.0 / p,
457 p_f64: p,
458 p1: p + 1.0,
459
460 y: 0.0,
461 xy: 0.0,
462 tsf: 0.0,
463 count: 0,
464 })
465 }
466
467 #[inline(always)]
468 pub fn update(&mut self, value: f64) -> Option<f64> {
469 if self.count < self.period {
470 self.buffer[self.idx] = value;
471 self.y += value;
472 self.xy += value * (self.count as f64 + 1.0);
473
474 self.idx += 1;
475 if self.idx == self.period {
476 self.idx = 0;
477 }
478 self.count += 1;
479
480 if self.count == self.period {
481 self.filled = true;
482
483 let b = (self.p_f64.mul_add(self.xy, -self.x * self.y)) * self.inv_den;
484 let a = (self.y - b * self.x) * self.inv_p;
485 self.tsf = b.mul_add(self.p1, a);
486 }
487 return None;
488 }
489
490 let out = if value.is_finite() && value != 0.0 {
491 100.0 * (1.0 - self.tsf / value)
492 } else {
493 f64::NAN
494 };
495
496 let old = self.buffer[self.idx];
497 self.buffer[self.idx] = value;
498 self.idx += 1;
499 if self.idx == self.period {
500 self.idx = 0;
501 }
502
503 let y_prev = self.y;
504 self.y = y_prev - old + value;
505 self.xy = self.xy - y_prev + self.p_f64 * value;
506
507 let b = (self.p_f64.mul_add(self.xy, -self.x * self.y)) * self.inv_den;
508 let a = (self.y - b * self.x) * self.inv_p;
509 self.tsf = b.mul_add(self.p1, a);
510
511 Some(out)
512 }
513}
514
515#[derive(Clone, Debug)]
516pub struct FoscBatchRange {
517 pub period: (usize, usize, usize),
518}
519
520impl Default for FoscBatchRange {
521 fn default() -> Self {
522 Self {
523 period: (5, 254, 1),
524 }
525 }
526}
527
528#[derive(Clone, Debug, Default)]
529pub struct FoscBatchBuilder {
530 range: FoscBatchRange,
531 kernel: Kernel,
532}
533
534impl FoscBatchBuilder {
535 pub fn new() -> Self {
536 Self::default()
537 }
538 pub fn kernel(mut self, k: Kernel) -> Self {
539 self.kernel = k;
540 self
541 }
542 #[inline]
543 pub fn period_range(mut self, start: usize, end: usize, step: usize) -> Self {
544 self.range.period = (start, end, step);
545 self
546 }
547 #[inline]
548 pub fn period_static(mut self, p: usize) -> Self {
549 self.range.period = (p, p, 0);
550 self
551 }
552 pub fn apply_slice(self, data: &[f64]) -> Result<FoscBatchOutput, FoscError> {
553 fosc_batch_with_kernel(data, &self.range, self.kernel)
554 }
555 pub fn with_default_slice(data: &[f64], k: Kernel) -> Result<FoscBatchOutput, FoscError> {
556 FoscBatchBuilder::new().kernel(k).apply_slice(data)
557 }
558 pub fn apply_candles(self, c: &Candles, src: &str) -> Result<FoscBatchOutput, FoscError> {
559 let slice = source_type(c, src);
560 self.apply_slice(slice)
561 }
562 pub fn with_default_candles(c: &Candles) -> Result<FoscBatchOutput, FoscError> {
563 FoscBatchBuilder::new()
564 .kernel(Kernel::Auto)
565 .apply_candles(c, "close")
566 }
567}
568
569#[derive(Clone, Debug)]
570pub struct FoscBatchOutput {
571 pub values: Vec<f64>,
572 pub combos: Vec<FoscParams>,
573 pub rows: usize,
574 pub cols: usize,
575}
576
577impl FoscBatchOutput {
578 pub fn row_for_params(&self, p: &FoscParams) -> Option<usize> {
579 self.combos
580 .iter()
581 .position(|c| c.period.unwrap_or(5) == p.period.unwrap_or(5))
582 }
583 pub fn values_for(&self, p: &FoscParams) -> Option<&[f64]> {
584 self.row_for_params(p).map(|row| {
585 let start = row * self.cols;
586 &self.values[start..start + self.cols]
587 })
588 }
589}
590
591#[inline(always)]
592fn expand_grid(r: &FoscBatchRange) -> Result<Vec<FoscParams>, FoscError> {
593 #[inline]
594 fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, FoscError> {
595 if step == 0 || start == end {
596 return Ok(vec![start]);
597 }
598 if start < end {
599 let v: Vec<usize> = (start..=end).step_by(step).collect();
600 return if v.is_empty() {
601 Err(FoscError::InvalidRange { start, end, step })
602 } else {
603 Ok(v)
604 };
605 } else {
606 let mut v = Vec::new();
607 let mut cur = start;
608 loop {
609 v.push(cur);
610 if cur <= end {
611 break;
612 }
613 match cur.checked_sub(step) {
614 Some(next) => cur = next,
615 None => break,
616 }
617 if cur == usize::MAX {
618 break;
619 }
620 if cur <= end {
621 break;
622 }
623 }
624 if v.is_empty() {
625 Err(FoscError::InvalidRange { start, end, step })
626 } else {
627 Ok(v)
628 }
629 }
630 }
631
632 let periods = axis_usize(r.period)?;
633 if periods.is_empty() {
634 return Err(FoscError::InvalidRange {
635 start: r.period.0,
636 end: r.period.1,
637 step: r.period.2,
638 });
639 }
640 let mut out = Vec::with_capacity(periods.len());
641 for &p in &periods {
642 out.push(FoscParams { period: Some(p) });
643 }
644 Ok(out)
645}
646
647#[inline(always)]
648pub fn fosc_batch_slice(
649 data: &[f64],
650 sweep: &FoscBatchRange,
651 kern: Kernel,
652) -> Result<FoscBatchOutput, FoscError> {
653 fosc_batch_inner(data, sweep, kern, false)
654}
655
656#[inline(always)]
657pub fn fosc_batch_par_slice(
658 data: &[f64],
659 sweep: &FoscBatchRange,
660 kern: Kernel,
661) -> Result<FoscBatchOutput, FoscError> {
662 fosc_batch_inner(data, sweep, kern, true)
663}
664
665pub fn fosc_batch_with_kernel(
666 data: &[f64],
667 sweep: &FoscBatchRange,
668 k: Kernel,
669) -> Result<FoscBatchOutput, FoscError> {
670 let kernel = match k {
671 Kernel::Auto => detect_best_batch_kernel(),
672 other if other.is_batch() => other,
673 other => return Err(FoscError::InvalidKernelForBatch(other)),
674 };
675 let simd = match kernel {
676 Kernel::Avx512Batch => Kernel::Avx512,
677 Kernel::Avx2Batch => Kernel::Avx2,
678 Kernel::ScalarBatch => Kernel::Scalar,
679 _ => unreachable!(),
680 };
681 fosc_batch_par_slice(data, sweep, simd)
682}
683
684#[inline(always)]
685fn fosc_batch_inner(
686 data: &[f64],
687 sweep: &FoscBatchRange,
688 kern: Kernel,
689 parallel: bool,
690) -> Result<FoscBatchOutput, FoscError> {
691 let combos = expand_grid(sweep)?;
692 let first = data
693 .iter()
694 .position(|x| !x.is_nan())
695 .ok_or(FoscError::AllValuesNaN)?;
696 let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
697 if data.len() - first < max_p {
698 return Err(FoscError::NotEnoughValidData {
699 needed: max_p,
700 valid: data.len() - first,
701 });
702 }
703 let rows = combos.len();
704 let cols = data.len();
705 let mut buf_mu = make_uninit_matrix(rows, cols);
706
707 let warmup_periods: Vec<usize> = combos
708 .iter()
709 .map(|c| first + c.period.unwrap_or(5) - 1)
710 .collect();
711 init_matrix_prefixes(&mut buf_mu, cols, &warmup_periods);
712
713 let mut guard = core::mem::ManuallyDrop::new(buf_mu);
714 let out_f64: &mut [f64] =
715 unsafe { core::slice::from_raw_parts_mut(guard.as_mut_ptr() as *mut f64, guard.len()) };
716
717 let actual = match kern {
718 Kernel::Auto => Kernel::ScalarBatch,
719 k => k,
720 };
721 let simd = match actual {
722 Kernel::ScalarBatch => Kernel::Scalar,
723 Kernel::Scalar => Kernel::Scalar,
724 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
725 Kernel::Avx2Batch => Kernel::Avx2,
726 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
727 Kernel::Avx2 => Kernel::Avx2,
728 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
729 Kernel::Avx512Batch => Kernel::Avx512,
730 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
731 Kernel::Avx512 => Kernel::Avx512,
732 _ => unreachable!(),
733 };
734
735 let do_row = |row: usize, out_row: &mut [f64]| unsafe {
736 let period = combos[row].period.unwrap();
737 match simd {
738 Kernel::Scalar => fosc_row_scalar(data, first, period, out_row),
739 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
740 Kernel::Avx2 => fosc_row_avx2(data, first, period, out_row),
741 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
742 Kernel::Avx512 => fosc_row_avx512(data, first, period, out_row),
743 _ => unreachable!(),
744 }
745 };
746
747 if parallel {
748 #[cfg(not(target_arch = "wasm32"))]
749 {
750 out_f64
751 .par_chunks_mut(cols)
752 .enumerate()
753 .for_each(|(row, slice)| do_row(row, slice));
754 }
755
756 #[cfg(target_arch = "wasm32")]
757 {
758 for (row, slice) in out_f64.chunks_mut(cols).enumerate() {
759 do_row(row, slice);
760 }
761 }
762 } else {
763 for (row, slice) in out_f64.chunks_mut(cols).enumerate() {
764 do_row(row, slice);
765 }
766 }
767
768 let values = unsafe {
769 Vec::from_raw_parts(
770 guard.as_mut_ptr() as *mut f64,
771 guard.len(),
772 guard.capacity(),
773 )
774 };
775
776 Ok(FoscBatchOutput {
777 values,
778 combos,
779 rows,
780 cols,
781 })
782}
783
784#[inline(always)]
785fn fosc_batch_inner_into(
786 data: &[f64],
787 sweep: &FoscBatchRange,
788 kern: Kernel,
789 parallel: bool,
790 out: &mut [f64],
791) -> Result<Vec<FoscParams>, FoscError> {
792 let combos = expand_grid(sweep)?;
793 let first = data
794 .iter()
795 .position(|x| !x.is_nan())
796 .ok_or(FoscError::AllValuesNaN)?;
797 let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
798 if data.len() - first < max_p {
799 return Err(FoscError::NotEnoughValidData {
800 needed: max_p,
801 valid: data.len() - first,
802 });
803 }
804
805 let cols = data.len();
806 let rows = combos.len();
807 let expected = rows.checked_mul(cols).ok_or(FoscError::InvalidRange {
808 start: sweep.period.0,
809 end: sweep.period.1,
810 step: sweep.period.2,
811 })?;
812 if out.len() != expected {
813 return Err(FoscError::OutputLengthMismatch {
814 expected,
815 got: out.len(),
816 });
817 }
818
819 let out_uninit: &mut [MaybeUninit<f64>] = unsafe {
820 core::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut MaybeUninit<f64>, out.len())
821 };
822
823 let warmup_periods: Vec<usize> = combos
824 .iter()
825 .map(|c| first + c.period.unwrap() - 1)
826 .collect();
827 init_matrix_prefixes(out_uninit, cols, &warmup_periods);
828
829 let actual = match kern {
830 Kernel::Auto => detect_best_batch_kernel(),
831 k => k,
832 };
833 let simd = match actual {
834 Kernel::ScalarBatch => Kernel::Scalar,
835 Kernel::Scalar => Kernel::Scalar,
836 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
837 Kernel::Avx2Batch => Kernel::Avx2,
838 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
839 Kernel::Avx2 => Kernel::Avx2,
840 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
841 Kernel::Avx512Batch => Kernel::Avx512,
842 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
843 Kernel::Avx512 => Kernel::Avx512,
844 _ => unreachable!(),
845 };
846
847 let do_row = |row: usize, dst_mu: &mut [MaybeUninit<f64>]| unsafe {
848 let period = combos[row].period.unwrap();
849
850 let dst = core::slice::from_raw_parts_mut(dst_mu.as_mut_ptr() as *mut f64, dst_mu.len());
851 match simd {
852 Kernel::Scalar => fosc_row_scalar(data, first, period, dst),
853 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
854 Kernel::Avx2 => fosc_row_avx2(data, first, period, dst),
855 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
856 Kernel::Avx512 => fosc_row_avx512(data, first, period, dst),
857 _ => unreachable!(),
858 }
859 };
860
861 if parallel {
862 #[cfg(not(target_arch = "wasm32"))]
863 {
864 out_uninit
865 .par_chunks_mut(cols)
866 .enumerate()
867 .for_each(|(r, sl)| do_row(r, sl));
868 }
869 #[cfg(target_arch = "wasm32")]
870 {
871 for (r, sl) in out_uninit.chunks_mut(cols).enumerate() {
872 do_row(r, sl);
873 }
874 }
875 } else {
876 for (r, sl) in out_uninit.chunks_mut(cols).enumerate() {
877 do_row(r, sl);
878 }
879 }
880
881 Ok(combos)
882}
883
884#[inline(always)]
885unsafe fn fosc_row_scalar(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
886 fosc_scalar(data, period, first, out);
887}
888
889#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
890#[inline(always)]
891unsafe fn fosc_row_avx2(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
892 fosc_avx2(data, period, first, out);
893}
894
895#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
896#[inline(always)]
897unsafe fn fosc_row_avx512(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
898 if period <= 32 {
899 fosc_row_avx512_short(data, first, period, out)
900 } else {
901 fosc_row_avx512_long(data, first, period, out)
902 }
903}
904
905#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
906#[inline(always)]
907unsafe fn fosc_row_avx512_short(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
908 fosc_avx512_short(data, period, first, out)
909}
910
911#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
912#[inline(always)]
913unsafe fn fosc_row_avx512_long(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
914 fosc_avx512_long(data, period, first, out)
915}
916
917#[cfg(feature = "python")]
918#[pyfunction(name = "fosc")]
919#[pyo3(signature = (data, period=5, kernel=None))]
920pub fn fosc_py<'py>(
921 py: Python<'py>,
922 data: PyReadonlyArray1<'py, f64>,
923 period: usize,
924 kernel: Option<&str>,
925) -> PyResult<Bound<'py, PyArray1<f64>>> {
926 let data_slice = data.as_slice()?;
927 let kernel_enum = validate_kernel(kernel, false)?;
928
929 let params = FoscParams {
930 period: Some(period),
931 };
932 let input = FoscInput::from_slice(data_slice, params);
933
934 py.allow_threads(|| {
935 let output = fosc_with_kernel(&input, kernel_enum)
936 .map_err(|e| PyErr::new::<PyValueError, _>(format!("Rust computation error: {}", e)))?;
937 Ok(output)
938 })
939 .map(|result| result.values.into_pyarray(py))
940}
941
942#[cfg(feature = "python")]
943#[pyfunction(name = "fosc_batch")]
944#[pyo3(signature = (data, period_range, kernel=None))]
945pub fn fosc_batch_py<'py>(
946 py: Python<'py>,
947 data: PyReadonlyArray1<'py, f64>,
948 period_range: (usize, usize, usize),
949 kernel: Option<&str>,
950) -> PyResult<Bound<'py, PyDict>> {
951 use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
952 use pyo3::types::PyDict;
953
954 let slice_in = data.as_slice()?;
955 let sweep = FoscBatchRange {
956 period: period_range,
957 };
958
959 let combos = expand_grid(&sweep).map_err(|e| PyErr::new::<PyValueError, _>(e.to_string()))?;
960 let rows = combos.len();
961 let cols = slice_in.len();
962
963 let total = rows
964 .checked_mul(cols)
965 .ok_or_else(|| PyErr::new::<PyValueError, _>("rows*cols overflow in fosc_batch_py"))?;
966 let out_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
967 let out_slice = unsafe { out_arr.as_slice_mut()? };
968
969 let kern = validate_kernel(kernel, true)?;
970
971 let combos = py
972 .allow_threads(|| {
973 let batch = match kern {
974 Kernel::Auto => detect_best_batch_kernel(),
975 k => k,
976 };
977 let simd = match batch {
978 Kernel::ScalarBatch => Kernel::Scalar,
979 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
980 Kernel::Avx2Batch => Kernel::Avx2,
981 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
982 Kernel::Avx512Batch => Kernel::Avx512,
983 _ => unreachable!(),
984 };
985 fosc_batch_inner_into(slice_in, &sweep, simd, true, out_slice)
986 })
987 .map_err(|e| PyValueError::new_err(e.to_string()))?;
988
989 let dict = PyDict::new(py);
990
991 dict.set_item("values", out_arr.reshape((rows, cols))?)?;
992
993 dict.set_item(
994 "periods",
995 combos
996 .iter()
997 .map(|p| p.period.unwrap_or(5) as u64)
998 .collect::<Vec<_>>()
999 .into_pyarray(py),
1000 )?;
1001 dict.set_item("rows", rows)?;
1002 dict.set_item("cols", cols)?;
1003 Ok(dict)
1004}
1005
1006#[cfg(all(feature = "python", feature = "cuda"))]
1007#[pyfunction(name = "fosc_cuda_batch_dev")]
1008#[pyo3(signature = (data_f32, period_range, device_id=0))]
1009pub fn fosc_cuda_batch_dev_py<'py>(
1010 py: Python<'py>,
1011 data_f32: numpy::PyReadonlyArray1<'py, f32>,
1012 period_range: (usize, usize, usize),
1013 device_id: usize,
1014) -> PyResult<DeviceArrayF32Py> {
1015 use crate::cuda::cuda_available;
1016 use crate::cuda::oscillators::fosc_wrapper::CudaFosc;
1017 if !cuda_available() {
1018 return Err(PyValueError::new_err("CUDA not available"));
1019 }
1020 let slice_in = data_f32.as_slice()?;
1021 let sweep = FoscBatchRange {
1022 period: period_range,
1023 };
1024 let inner = py.allow_threads(|| {
1025 let cuda = CudaFosc::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1026 cuda.fosc_batch_dev(slice_in, &sweep)
1027 .map_err(|e| PyValueError::new_err(e.to_string()))
1028 })?;
1029 let handle = make_device_array_py(device_id, inner)?;
1030 Ok(handle)
1031}
1032
1033#[cfg(all(feature = "python", feature = "cuda"))]
1034#[pyfunction(name = "fosc_cuda_many_series_one_param_dev")]
1035#[pyo3(signature = (data_tm_f32, period, device_id=0))]
1036pub fn fosc_cuda_many_series_one_param_dev_py(
1037 py: Python<'_>,
1038 data_tm_f32: numpy::PyReadonlyArray2<'_, f32>,
1039 period: usize,
1040 device_id: usize,
1041) -> PyResult<DeviceArrayF32Py> {
1042 use crate::cuda::cuda_available;
1043 use crate::cuda::oscillators::fosc_wrapper::CudaFosc;
1044 use numpy::PyUntypedArrayMethods;
1045
1046 if !cuda_available() {
1047 return Err(PyValueError::new_err("CUDA not available"));
1048 }
1049 let flat_in: &[f32] = data_tm_f32.as_slice()?;
1050 let rows = data_tm_f32.shape()[0];
1051 let cols = data_tm_f32.shape()[1];
1052 let params = FoscParams {
1053 period: Some(period),
1054 };
1055 let inner = py.allow_threads(|| {
1056 let cuda = CudaFosc::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1057 cuda.fosc_many_series_one_param_time_major_dev(flat_in, cols, rows, ¶ms)
1058 .map_err(|e| PyValueError::new_err(e.to_string()))
1059 })?;
1060 let handle = make_device_array_py(device_id, inner)?;
1061 Ok(handle)
1062}
1063
1064#[cfg(feature = "python")]
1065#[pyclass(name = "FoscStream")]
1066pub struct FoscStreamPy {
1067 inner: FoscStream,
1068}
1069
1070#[cfg(feature = "python")]
1071#[pymethods]
1072impl FoscStreamPy {
1073 #[new]
1074 #[pyo3(signature = (period=5))]
1075 pub fn new(period: usize) -> PyResult<Self> {
1076 let params = FoscParams {
1077 period: Some(period),
1078 };
1079 let inner = FoscStream::try_new(params).map_err(|e| {
1080 PyErr::new::<PyValueError, _>(format!("Failed to create stream: {}", e))
1081 })?;
1082 Ok(Self { inner })
1083 }
1084
1085 pub fn update(&mut self, value: f64) -> Option<f64> {
1086 self.inner.update(value)
1087 }
1088}
1089
1090#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1091#[wasm_bindgen]
1092pub fn fosc_js(data: &[f64], period: usize) -> Result<Vec<f64>, JsValue> {
1093 let params = FoscParams {
1094 period: Some(period),
1095 };
1096 let input = FoscInput::from_slice(data, params);
1097
1098 let mut output = vec![0.0; data.len()];
1099 fosc_into_slice(&mut output, &input, Kernel::Auto)
1100 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1101
1102 Ok(output)
1103}
1104
1105#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1106#[wasm_bindgen]
1107pub fn fosc_alloc(len: usize) -> *mut f64 {
1108 let mut vec = Vec::<f64>::with_capacity(len);
1109 let ptr = vec.as_mut_ptr();
1110 std::mem::forget(vec);
1111 ptr
1112}
1113
1114#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1115#[wasm_bindgen]
1116pub fn fosc_free(ptr: *mut f64, len: usize) {
1117 if !ptr.is_null() {
1118 unsafe {
1119 let _ = Vec::from_raw_parts(ptr, len, len);
1120 }
1121 }
1122}
1123
1124#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1125#[wasm_bindgen]
1126pub fn fosc_into(
1127 in_ptr: *const f64,
1128 out_ptr: *mut f64,
1129 len: usize,
1130 period: usize,
1131) -> Result<(), JsValue> {
1132 if in_ptr.is_null() || out_ptr.is_null() {
1133 return Err(JsValue::from_str("null pointer passed to fosc_into"));
1134 }
1135
1136 unsafe {
1137 let data = std::slice::from_raw_parts(in_ptr, len);
1138
1139 if period == 0 || period > len {
1140 return Err(JsValue::from_str("Invalid period"));
1141 }
1142
1143 let params = FoscParams {
1144 period: Some(period),
1145 };
1146 let input = FoscInput::from_slice(data, params);
1147
1148 if in_ptr == out_ptr {
1149 let mut temp = vec![0.0; len];
1150 fosc_into_slice(&mut temp, &input, Kernel::Auto)
1151 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1152 let out = std::slice::from_raw_parts_mut(out_ptr, len);
1153 out.copy_from_slice(&temp);
1154 } else {
1155 let out = std::slice::from_raw_parts_mut(out_ptr, len);
1156 fosc_into_slice(out, &input, Kernel::Auto)
1157 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1158 }
1159 Ok(())
1160 }
1161}
1162
1163#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1164#[derive(Serialize, Deserialize)]
1165pub struct FoscBatchConfig {
1166 pub period_range: (usize, usize, usize),
1167}
1168
1169#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1170#[derive(Serialize, Deserialize)]
1171pub struct FoscBatchJsOutput {
1172 pub values: Vec<f64>,
1173 pub combos: Vec<FoscParams>,
1174 pub rows: usize,
1175 pub cols: usize,
1176}
1177
1178#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1179#[wasm_bindgen(js_name = fosc_batch)]
1180pub fn fosc_batch_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
1181 let config: FoscBatchConfig = serde_wasm_bindgen::from_value(config)
1182 .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
1183
1184 let range = FoscBatchRange {
1185 period: config.period_range,
1186 };
1187
1188 let output = fosc_batch_inner(data, &range, Kernel::Auto, false)
1189 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1190
1191 let js_output = FoscBatchJsOutput {
1192 values: output.values,
1193 combos: output.combos,
1194 rows: output.rows,
1195 cols: output.cols,
1196 };
1197
1198 serde_wasm_bindgen::to_value(&js_output)
1199 .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
1200}
1201
1202#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1203#[wasm_bindgen]
1204pub fn fosc_batch_into(
1205 in_ptr: *const f64,
1206 out_ptr: *mut f64,
1207 len: usize,
1208 period_start: usize,
1209 period_end: usize,
1210 period_step: usize,
1211) -> Result<usize, JsValue> {
1212 if in_ptr.is_null() || out_ptr.is_null() {
1213 return Err(JsValue::from_str("null pointer passed to fosc_batch_into"));
1214 }
1215
1216 unsafe {
1217 let data = std::slice::from_raw_parts(in_ptr, len);
1218
1219 let range = FoscBatchRange {
1220 period: (period_start, period_end, period_step),
1221 };
1222
1223 let output = fosc_batch_inner(data, &range, Kernel::Auto, false)
1224 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1225
1226 let out = std::slice::from_raw_parts_mut(out_ptr, output.values.len());
1227 out.copy_from_slice(&output.values);
1228
1229 Ok(output.rows)
1230 }
1231}
1232
1233#[cfg(test)]
1234mod tests {
1235 use super::*;
1236 use crate::skip_if_unsupported;
1237 use crate::utilities::data_loader::read_candles_from_csv;
1238
1239 fn check_fosc_partial_params(
1240 test_name: &str,
1241 kernel: Kernel,
1242 ) -> Result<(), Box<dyn std::error::Error>> {
1243 skip_if_unsupported!(kernel, test_name);
1244 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1245 let candles = read_candles_from_csv(file_path)?;
1246 let default_params = FoscParams { period: None };
1247 let input = FoscInput::from_candles(&candles, "close", default_params);
1248 let output = fosc_with_kernel(&input, kernel)?;
1249 assert_eq!(output.values.len(), candles.close.len());
1250 Ok(())
1251 }
1252
1253 fn check_fosc_basic_accuracy(
1254 test_name: &str,
1255 kernel: Kernel,
1256 ) -> Result<(), Box<dyn std::error::Error>> {
1257 skip_if_unsupported!(kernel, test_name);
1258 let test_data = [
1259 81.59, 81.06, 82.87, 83.00, 83.61, 83.15, 82.84, 82.84, 83.99, 84.55, 84.36, 85.53,
1260 ];
1261 let period = 5;
1262 let input = FoscInput::from_slice(
1263 &test_data,
1264 FoscParams {
1265 period: Some(period),
1266 },
1267 );
1268 let result = fosc_with_kernel(&input, kernel)?;
1269 assert_eq!(result.values.len(), test_data.len());
1270 for i in 0..(period - 1) {
1271 assert!(result.values[i].is_nan());
1272 }
1273 Ok(())
1274 }
1275
1276 fn check_fosc_with_nan_data(
1277 test_name: &str,
1278 kernel: Kernel,
1279 ) -> Result<(), Box<dyn std::error::Error>> {
1280 skip_if_unsupported!(kernel, test_name);
1281 let input_data = [f64::NAN, f64::NAN, 1.0, 2.0, 3.0, 4.0, 5.0];
1282 let params = FoscParams { period: Some(3) };
1283 let input = FoscInput::from_slice(&input_data, params);
1284 let result = fosc_with_kernel(&input, kernel)?;
1285 assert_eq!(result.values.len(), input_data.len());
1286 Ok(())
1287 }
1288
1289 fn check_fosc_zero_period(
1290 test_name: &str,
1291 kernel: Kernel,
1292 ) -> Result<(), Box<dyn std::error::Error>> {
1293 skip_if_unsupported!(kernel, test_name);
1294 let input_data = [10.0, 20.0, 30.0];
1295 let params = FoscParams { period: Some(0) };
1296 let input = FoscInput::from_slice(&input_data, params);
1297 let res = fosc_with_kernel(&input, kernel);
1298 assert!(
1299 res.is_err(),
1300 "[{}] FOSC should fail with zero period",
1301 test_name
1302 );
1303 Ok(())
1304 }
1305
1306 fn check_fosc_period_exceeds_length(
1307 test_name: &str,
1308 kernel: Kernel,
1309 ) -> Result<(), Box<dyn std::error::Error>> {
1310 skip_if_unsupported!(kernel, test_name);
1311 let data_small = [10.0, 20.0, 30.0];
1312 let params = FoscParams { period: Some(10) };
1313 let input = FoscInput::from_slice(&data_small, params);
1314 let res = fosc_with_kernel(&input, kernel);
1315 assert!(
1316 res.is_err(),
1317 "[{}] FOSC should fail with period exceeding length",
1318 test_name
1319 );
1320 Ok(())
1321 }
1322
1323 fn check_fosc_very_small_dataset(
1324 test_name: &str,
1325 kernel: Kernel,
1326 ) -> Result<(), Box<dyn std::error::Error>> {
1327 skip_if_unsupported!(kernel, test_name);
1328 let single_point = [42.0];
1329 let params = FoscParams { period: Some(5) };
1330 let input = FoscInput::from_slice(&single_point, params);
1331 let res = fosc_with_kernel(&input, kernel);
1332 assert!(
1333 res.is_err(),
1334 "[{}] FOSC should fail with insufficient data",
1335 test_name
1336 );
1337 Ok(())
1338 }
1339
1340 fn check_fosc_all_values_nan(
1341 test_name: &str,
1342 kernel: Kernel,
1343 ) -> Result<(), Box<dyn std::error::Error>> {
1344 skip_if_unsupported!(kernel, test_name);
1345 let input_data = [f64::NAN, f64::NAN, f64::NAN];
1346 let params = FoscParams { period: Some(2) };
1347 let input = FoscInput::from_slice(&input_data, params);
1348 let res = fosc_with_kernel(&input, kernel);
1349 assert!(
1350 res.is_err(),
1351 "[{}] FOSC should fail with all NaN",
1352 test_name
1353 );
1354 Ok(())
1355 }
1356
1357 fn check_fosc_expected_values_reference(
1358 test_name: &str,
1359 kernel: Kernel,
1360 ) -> Result<(), Box<dyn std::error::Error>> {
1361 skip_if_unsupported!(kernel, test_name);
1362 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1363 let candles = read_candles_from_csv(file_path)?;
1364 let expected_last_five = [
1365 -0.8904444627923475,
1366 -0.4763353099245297,
1367 -0.2379782851444668,
1368 0.292790128025632,
1369 -0.6597902988090389,
1370 ];
1371 let params = FoscParams { period: Some(5) };
1372 let input = FoscInput::from_candles(&candles, "close", params);
1373 let result = fosc_with_kernel(&input, kernel)?;
1374 let valid_len = result.values.len();
1375 assert!(valid_len >= 5);
1376 let output_slice = &result.values[valid_len - 5..valid_len];
1377 for (i, &val) in output_slice.iter().enumerate() {
1378 let exp: f64 = expected_last_five[i];
1379 if exp.is_nan() {
1380 assert!(val.is_nan());
1381 } else {
1382 assert!(
1383 (val - exp).abs() < 1e-7,
1384 "Mismatch at index {}: expected {}, got {}",
1385 i,
1386 exp,
1387 val
1388 );
1389 }
1390 }
1391 Ok(())
1392 }
1393
1394 macro_rules! generate_all_fosc_tests {
1395 ($($test_fn:ident),*) => {
1396 paste::paste! {
1397 $(
1398 #[test]
1399 fn [<$test_fn _scalar_f64>]() {
1400 let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
1401 }
1402 )*
1403 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1404 $(
1405 #[test]
1406 fn [<$test_fn _avx2_f64>]() {
1407 let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
1408 }
1409 #[test]
1410 fn [<$test_fn _avx512_f64>]() {
1411 let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
1412 }
1413 )*
1414 }
1415 }
1416 }
1417
1418 #[cfg(debug_assertions)]
1419 fn check_fosc_no_poison(
1420 test_name: &str,
1421 kernel: Kernel,
1422 ) -> Result<(), Box<dyn std::error::Error>> {
1423 skip_if_unsupported!(kernel, test_name);
1424
1425 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1426 let candles = read_candles_from_csv(file_path)?;
1427
1428 let test_params = vec![
1429 FoscParams::default(),
1430 FoscParams { period: Some(2) },
1431 FoscParams { period: Some(3) },
1432 FoscParams { period: Some(5) },
1433 FoscParams { period: Some(7) },
1434 FoscParams { period: Some(10) },
1435 FoscParams { period: Some(14) },
1436 FoscParams { period: Some(20) },
1437 FoscParams { period: Some(30) },
1438 FoscParams { period: Some(50) },
1439 FoscParams { period: Some(100) },
1440 FoscParams { period: Some(200) },
1441 ];
1442
1443 for (param_idx, params) in test_params.iter().enumerate() {
1444 let input = FoscInput::from_candles(&candles, "close", params.clone());
1445 let output = fosc_with_kernel(&input, kernel)?;
1446
1447 for (i, &val) in output.values.iter().enumerate() {
1448 if val.is_nan() {
1449 continue;
1450 }
1451
1452 let bits = val.to_bits();
1453
1454 if bits == 0x11111111_11111111 {
1455 panic!(
1456 "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
1457 with params: period={} (param set {})",
1458 test_name,
1459 val,
1460 bits,
1461 i,
1462 params.period.unwrap_or(5),
1463 param_idx
1464 );
1465 }
1466
1467 if bits == 0x22222222_22222222 {
1468 panic!(
1469 "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
1470 with params: period={} (param set {})",
1471 test_name,
1472 val,
1473 bits,
1474 i,
1475 params.period.unwrap_or(5),
1476 param_idx
1477 );
1478 }
1479
1480 if bits == 0x33333333_33333333 {
1481 panic!(
1482 "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
1483 with params: period={} (param set {})",
1484 test_name,
1485 val,
1486 bits,
1487 i,
1488 params.period.unwrap_or(5),
1489 param_idx
1490 );
1491 }
1492 }
1493 }
1494
1495 Ok(())
1496 }
1497
1498 #[cfg(not(debug_assertions))]
1499 fn check_fosc_no_poison(
1500 _test_name: &str,
1501 _kernel: Kernel,
1502 ) -> Result<(), Box<dyn std::error::Error>> {
1503 Ok(())
1504 }
1505
1506 #[cfg(feature = "proptest")]
1507 #[allow(clippy::float_cmp)]
1508 fn check_fosc_property(
1509 test_name: &str,
1510 kernel: Kernel,
1511 ) -> Result<(), Box<dyn std::error::Error>> {
1512 use proptest::prelude::*;
1513 skip_if_unsupported!(kernel, test_name);
1514
1515 let strat1 = (2usize..=50).prop_flat_map(|period| {
1516 (
1517 prop::collection::vec(
1518 (1e-6f64..1e6f64)
1519 .prop_filter("finite and non-zero", |x| x.is_finite() && x.abs() > 1e-10),
1520 period..400,
1521 ),
1522 Just(period),
1523 )
1524 });
1525
1526 proptest::test_runner::TestRunner::default().run(&strat1, |(data, period)| {
1527 let params = FoscParams {
1528 period: Some(period),
1529 };
1530 let input = FoscInput::from_slice(&data, params);
1531
1532 let FoscOutput { values: out } = fosc_with_kernel(&input, kernel).unwrap();
1533 let FoscOutput { values: ref_out } = fosc_with_kernel(&input, Kernel::Scalar).unwrap();
1534
1535 prop_assert_eq!(out.len(), data.len());
1536 prop_assert_eq!(ref_out.len(), data.len());
1537
1538 for i in 0..(period - 1) {
1539 prop_assert!(out[i].is_nan(), "Expected NaN at index {} during warmup", i);
1540 prop_assert!(
1541 ref_out[i].is_nan(),
1542 "Expected NaN at index {} during warmup (scalar)",
1543 i
1544 );
1545 }
1546
1547 for i in (period - 1)..data.len() {
1548 let y = out[i];
1549 let r = ref_out[i];
1550
1551 if r.is_nan() {
1552 prop_assert!(
1553 y.is_nan(),
1554 "Kernel mismatch at {}: scalar is NaN but kernel is {}",
1555 i,
1556 y
1557 );
1558 } else if y.is_nan() {
1559 prop_assert!(
1560 r.is_nan(),
1561 "Kernel mismatch at {}: kernel is NaN but scalar is {}",
1562 i,
1563 r
1564 );
1565 } else {
1566 let diff = (y - r).abs();
1567 let tolerance = 1e-9 * r.abs().max(1.0);
1568 prop_assert!(
1569 diff <= tolerance,
1570 "Kernel mismatch at {}: {} vs {} (diff: {})",
1571 i,
1572 y,
1573 r,
1574 diff
1575 );
1576 }
1577
1578 let y_bits = y.to_bits();
1579 prop_assert_ne!(
1580 y_bits,
1581 0x11111111_11111111,
1582 "Found alloc_with_nan_prefix poison at {}",
1583 i
1584 );
1585 prop_assert_ne!(
1586 y_bits,
1587 0x22222222_22222222,
1588 "Found init_matrix_prefixes poison at {}",
1589 i
1590 );
1591 prop_assert_ne!(
1592 y_bits,
1593 0x33333333_33333333,
1594 "Found make_uninit_matrix poison at {}",
1595 i
1596 );
1597 }
1598
1599 Ok(())
1600 })?;
1601
1602 let strat2 = prop::collection::vec(
1603 (1e-6f64..1e6f64)
1604 .prop_filter("finite and non-zero", |x| x.is_finite() && x.abs() > 1e-10),
1605 10..1000,
1606 );
1607
1608 proptest::test_runner::TestRunner::default().run(&strat2, |data| {
1609 let period = 5;
1610 let params = FoscParams {
1611 period: Some(period),
1612 };
1613 let input = FoscInput::from_slice(&data, params);
1614
1615 let FoscOutput { values: out } = fosc_with_kernel(&input, kernel).unwrap();
1616
1617 for i in (period - 1)..data.len() {
1618 if !out[i].is_nan() {
1619 prop_assert!(
1620 out[i] >= -200.0 && out[i] <= 200.0,
1621 "FOSC value {} at index {} is out of reasonable bounds",
1622 out[i],
1623 i
1624 );
1625 }
1626 }
1627
1628 Ok(())
1629 })?;
1630
1631 let strat3 =
1632 (2usize..=20, 1e-6f64..10f64, 10usize..100).prop_map(|(period, start, len)| {
1633 let data: Vec<f64> = (0..len).map(|i| start + (i as f64) * 0.1).collect();
1634 (data, period)
1635 });
1636
1637 proptest::test_runner::TestRunner::default().run(&strat3, |(data, period)| {
1638 let params = FoscParams {
1639 period: Some(period),
1640 };
1641 let input = FoscInput::from_slice(&data, params);
1642
1643 let FoscOutput { values: out } = fosc_with_kernel(&input, kernel).unwrap();
1644
1645 let start_idx = if period > 0 { period } else { period - 1 };
1646 let valid_fosc: Vec<f64> = out
1647 .iter()
1648 .skip(start_idx)
1649 .filter(|v| !v.is_nan())
1650 .copied()
1651 .collect();
1652
1653 if valid_fosc.len() > 5 {
1654 for &val in &valid_fosc {
1655 prop_assert!(val.abs() < 5.0, "FOSC {} too large for linear trend", val);
1656 }
1657
1658 let mean: f64 = valid_fosc.iter().sum::<f64>() / valid_fosc.len() as f64;
1659 let variance: f64 = valid_fosc.iter().map(|v| (v - mean).powi(2)).sum::<f64>()
1660 / valid_fosc.len() as f64;
1661 let std_dev = variance.sqrt();
1662
1663 prop_assert!(
1664 std_dev < 1.0,
1665 "FOSC standard deviation {} too high for linear trend",
1666 std_dev
1667 );
1668 }
1669
1670 Ok(())
1671 })?;
1672
1673 let strat4 = (2usize..=20, 10usize..100).prop_map(|(period, len)| {
1674 let data: Vec<f64> = (0..len)
1675 .map(|i| 100.0 + 10.0 * (i as f64 * 0.5).sin())
1676 .collect();
1677 (data, period)
1678 });
1679
1680 proptest::test_runner::TestRunner::default().run(&strat4, |(data, period)| {
1681 let params = FoscParams {
1682 period: Some(period),
1683 };
1684 let input = FoscInput::from_slice(&data, params);
1685
1686 let FoscOutput { values: out } = fosc_with_kernel(&input, kernel).unwrap();
1687
1688 let valid_values: Vec<f64> = out
1689 .iter()
1690 .skip(period - 1)
1691 .filter(|v| !v.is_nan())
1692 .copied()
1693 .collect();
1694
1695 if valid_values.len() > 10 {
1696 let mean: f64 = valid_values.iter().sum::<f64>() / valid_values.len() as f64;
1697
1698 prop_assert!(
1699 mean.abs() < 10.0,
1700 "Mean FOSC {} is too far from zero for oscillating data",
1701 mean
1702 );
1703 }
1704
1705 Ok(())
1706 })?;
1707
1708 let strat5 = (2usize..=20, 50f64..200f64, 20usize..100).prop_flat_map(
1709 |(period, base_price, len)| {
1710 (
1711 prop::collection::vec(
1712 (-0.5f64..0.5f64, 0f64..0.5f64, 0f64..0.5f64).prop_map(
1713 move |(change, high_diff, low_diff)| {
1714 base_price * (1.0 + change * 0.01) + high_diff
1715 },
1716 ),
1717 len,
1718 ),
1719 Just(period),
1720 )
1721 },
1722 );
1723
1724 proptest::test_runner::TestRunner::default().run(&strat5, |(data, period)| {
1725 let params = FoscParams {
1726 period: Some(period),
1727 };
1728 let input = FoscInput::from_slice(&data, params);
1729
1730 let result = fosc_with_kernel(&input, kernel);
1731 prop_assert!(result.is_ok(), "FOSC failed for OHLC-like data");
1732
1733 let FoscOutput { values: out } = result.unwrap();
1734
1735 if period == 2 {
1736 for i in 1..data.len() {
1737 if !out[i].is_nan() {
1738 prop_assert!(
1739 out[i].abs() < 100.0,
1740 "FOSC {} at index {} unreasonable for period=2",
1741 out[i],
1742 i
1743 );
1744 }
1745 }
1746 }
1747
1748 if data.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-10) && data.len() > period {
1749 for i in (period - 1)..data.len() {
1750 if !out[i].is_nan() {
1751 prop_assert!(
1752 out[i].abs() < 1e-6,
1753 "FOSC {} at index {} should be ~0 for constant data",
1754 out[i],
1755 i
1756 );
1757 }
1758 }
1759 }
1760
1761 Ok(())
1762 })?;
1763
1764 let strat6 = (2usize..=10, 10usize..50).prop_flat_map(|(period, len)| {
1765 (
1766 prop::collection::vec(
1767 prop::strategy::Union::new(vec![
1768 (0.9f64..1.0)
1769 .prop_map(|p| {
1770 if p < 0.95 {
1771 100.0 + p
1772 } else if p < 0.98 {
1773 1e-12
1774 } else {
1775 0.0
1776 }
1777 })
1778 .boxed(),
1779 (50f64..150f64).boxed(),
1780 (-1e-12f64..1e-12f64)
1781 .prop_filter("not exactly zero", |x| x.abs() > 1e-15)
1782 .boxed(),
1783 ]),
1784 len,
1785 ),
1786 Just(period),
1787 )
1788 });
1789
1790 proptest::test_runner::TestRunner::default().run(&strat6, |(data, period)| {
1791 let params = FoscParams {
1792 period: Some(period),
1793 };
1794 let input = FoscInput::from_slice(&data, params);
1795
1796 let result = fosc_with_kernel(&input, kernel);
1797 prop_assert!(result.is_ok(), "FOSC failed for near-zero data");
1798
1799 let FoscOutput { values: out } = result.unwrap();
1800
1801 for i in (period - 1)..data.len() {
1802 if data[i] == 0.0 {
1803 prop_assert!(
1804 out[i].is_nan(),
1805 "FOSC should be NaN when price is 0.0 at index {}",
1806 i
1807 );
1808 } else if data[i].abs() < 1e-10 {
1809 if !out[i].is_nan() {
1810 prop_assert!(
1811 out[i].abs() < 10000.0,
1812 "FOSC {} unreasonably large for tiny price {} at index {}",
1813 out[i],
1814 data[i],
1815 i
1816 );
1817 }
1818 }
1819 }
1820
1821 Ok(())
1822 })?;
1823
1824 Ok(())
1825 }
1826
1827 generate_all_fosc_tests!(
1828 check_fosc_partial_params,
1829 check_fosc_basic_accuracy,
1830 check_fosc_with_nan_data,
1831 check_fosc_zero_period,
1832 check_fosc_period_exceeds_length,
1833 check_fosc_very_small_dataset,
1834 check_fosc_all_values_nan,
1835 check_fosc_expected_values_reference,
1836 check_fosc_no_poison
1837 );
1838
1839 #[cfg(feature = "proptest")]
1840 generate_all_fosc_tests!(check_fosc_property);
1841
1842 fn check_batch_default_row(
1843 test: &str,
1844 kernel: Kernel,
1845 ) -> Result<(), Box<dyn std::error::Error>> {
1846 skip_if_unsupported!(kernel, test);
1847 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1848 let c = read_candles_from_csv(file)?;
1849 let output = FoscBatchBuilder::new()
1850 .kernel(kernel)
1851 .apply_candles(&c, "close")?;
1852 let def = FoscParams::default();
1853 let row = output.values_for(&def).expect("default row missing");
1854 assert_eq!(row.len(), c.close.len());
1855 let expected = [
1856 -0.8904444627923475,
1857 -0.4763353099245297,
1858 -0.2379782851444668,
1859 0.292790128025632,
1860 -0.6597902988090389,
1861 ];
1862 let start = row.len() - 5;
1863 for (i, &v) in row[start..].iter().enumerate() {
1864 assert!(
1865 (v - expected[i]).abs() < 1e-7,
1866 "[{test}] default-row mismatch at idx {i}: {v} vs {expected:?}"
1867 );
1868 }
1869 Ok(())
1870 }
1871
1872 macro_rules! gen_batch_tests {
1873 ($fn_name:ident) => {
1874 paste::paste! {
1875 #[test] fn [<$fn_name _scalar>]() {
1876 let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
1877 }
1878 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1879 #[test] fn [<$fn_name _avx2>]() {
1880 let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
1881 }
1882 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1883 #[test] fn [<$fn_name _avx512>]() {
1884 let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
1885 }
1886 #[test] fn [<$fn_name _auto_detect>]() {
1887 let _ = $fn_name(stringify!([<$fn_name _auto_detect>]),
1888 Kernel::Auto);
1889 }
1890 }
1891 };
1892 }
1893 fn check_batch_sweep(test: &str, kernel: Kernel) -> Result<(), Box<dyn std::error::Error>> {
1894 skip_if_unsupported!(kernel, test);
1895
1896 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1897 let c = read_candles_from_csv(file)?;
1898
1899 let output = FoscBatchBuilder::new()
1900 .kernel(kernel)
1901 .period_range(5, 25, 5)
1902 .apply_candles(&c, "close")?;
1903
1904 let expected_combos = 5;
1905 assert_eq!(output.combos.len(), expected_combos);
1906 assert_eq!(output.rows, expected_combos);
1907 assert_eq!(output.cols, c.close.len());
1908
1909 Ok(())
1910 }
1911
1912 #[cfg(debug_assertions)]
1913 fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn std::error::Error>> {
1914 skip_if_unsupported!(kernel, test);
1915
1916 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1917 let c = read_candles_from_csv(file)?;
1918
1919 let test_configs = vec![
1920 (2, 10, 2),
1921 (5, 25, 5),
1922 (30, 60, 15),
1923 (2, 5, 1),
1924 (10, 20, 2),
1925 (5, 5, 0),
1926 (5, 50, 15),
1927 (100, 200, 50),
1928 ];
1929
1930 for (cfg_idx, &(p_start, p_end, p_step)) in test_configs.iter().enumerate() {
1931 let output = FoscBatchBuilder::new()
1932 .kernel(kernel)
1933 .period_range(p_start, p_end, p_step)
1934 .apply_candles(&c, "close")?;
1935
1936 for (idx, &val) in output.values.iter().enumerate() {
1937 if val.is_nan() {
1938 continue;
1939 }
1940
1941 let bits = val.to_bits();
1942 let row = idx / output.cols;
1943 let col = idx % output.cols;
1944 let combo = &output.combos[row];
1945
1946 if bits == 0x11111111_11111111 {
1947 panic!(
1948 "[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
1949 at row {} col {} (flat index {}) with params: period={}",
1950 test,
1951 cfg_idx,
1952 val,
1953 bits,
1954 row,
1955 col,
1956 idx,
1957 combo.period.unwrap_or(5)
1958 );
1959 }
1960
1961 if bits == 0x22222222_22222222 {
1962 panic!(
1963 "[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
1964 at row {} col {} (flat index {}) with params: period={}",
1965 test,
1966 cfg_idx,
1967 val,
1968 bits,
1969 row,
1970 col,
1971 idx,
1972 combo.period.unwrap_or(5)
1973 );
1974 }
1975
1976 if bits == 0x33333333_33333333 {
1977 panic!(
1978 "[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
1979 at row {} col {} (flat index {}) with params: period={}",
1980 test,
1981 cfg_idx,
1982 val,
1983 bits,
1984 row,
1985 col,
1986 idx,
1987 combo.period.unwrap_or(5)
1988 );
1989 }
1990 }
1991 }
1992
1993 Ok(())
1994 }
1995
1996 #[cfg(not(debug_assertions))]
1997 fn check_batch_no_poison(
1998 _test: &str,
1999 _kernel: Kernel,
2000 ) -> Result<(), Box<dyn std::error::Error>> {
2001 Ok(())
2002 }
2003
2004 gen_batch_tests!(check_batch_default_row);
2005 gen_batch_tests!(check_batch_sweep);
2006 gen_batch_tests!(check_batch_no_poison);
2007
2008 #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
2009 #[test]
2010 fn test_fosc_into_matches_api() -> Result<(), Box<dyn std::error::Error>> {
2011 let n = 512usize;
2012 let mut data = Vec::with_capacity(n);
2013 for i in 0..n {
2014 let x = i as f64;
2015
2016 data.push(50.0 + 0.05 * x + (0.1 * x).sin() * 2.0);
2017 }
2018
2019 let input = FoscInput::from_slice(&data, FoscParams::default());
2020
2021 let baseline = fosc_with_kernel(&input, Kernel::Auto)?.values;
2022
2023 let mut out = vec![0.0; data.len()];
2024 fosc_into(&input, &mut out)?;
2025
2026 assert_eq!(baseline.len(), out.len());
2027
2028 #[inline]
2029 fn eq_or_both_nan(a: f64, b: f64) -> bool {
2030 (a.is_nan() && b.is_nan()) || (a == b)
2031 }
2032
2033 for i in 0..out.len() {
2034 assert!(
2035 eq_or_both_nan(baseline[i], out[i]),
2036 "mismatch at {}: baseline={} out={}",
2037 i,
2038 baseline[i],
2039 out[i]
2040 );
2041 }
2042
2043 Ok(())
2044 }
2045}