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};
7#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
8use core::arch::x86_64::*;
9#[cfg(not(target_arch = "wasm32"))]
10use rayon::prelude::*;
11use std::convert::AsRef;
12use thiserror::Error;
13
14#[cfg(all(feature = "python", feature = "cuda"))]
15mod vosc_python_cuda_handle {
16 use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
17 use cust::context::Context;
18 use cust::memory::DeviceBuffer;
19 use pyo3::exceptions::PyValueError;
20 use pyo3::prelude::*;
21 use pyo3::types::PyDict;
22 use std::ffi::c_void;
23 use std::sync::Arc;
24
25 #[pyclass(module = "ta_indicators.cuda", unsendable, name = "DeviceArrayF32Py")]
26 pub struct DeviceArrayF32Py {
27 pub(crate) buf: Option<DeviceBuffer<f32>>,
28 pub(crate) rows: usize,
29 pub(crate) cols: usize,
30 pub(crate) _ctx: Arc<Context>,
31 pub(crate) device_id: u32,
32 }
33
34 #[pymethods]
35 impl DeviceArrayF32Py {
36 #[getter]
37 fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
38 let d = PyDict::new(py);
39 d.set_item("shape", (self.rows, self.cols))?;
40 d.set_item("typestr", "<f4")?;
41 d.set_item(
42 "strides",
43 (
44 self.cols * std::mem::size_of::<f32>(),
45 std::mem::size_of::<f32>(),
46 ),
47 )?;
48 let ptr = self
49 .buf
50 .as_ref()
51 .ok_or_else(|| PyValueError::new_err("buffer already exported via __dlpack__"))?
52 .as_device_ptr()
53 .as_raw() as usize;
54 d.set_item("data", (ptr, false))?;
55 d.set_item("version", 3)?;
56 Ok(d)
57 }
58
59 fn __dlpack_device__(&self) -> (i32, i32) {
60 let mut device_ordinal: i32 = self.device_id as i32;
61 unsafe {
62 let attr = cust::sys::CUpointer_attribute::CU_POINTER_ATTRIBUTE_DEVICE_ORDINAL;
63 let mut value = std::mem::MaybeUninit::<i32>::uninit();
64 let ptr = self
65 .buf
66 .as_ref()
67 .map(|b| b.as_device_ptr().as_raw())
68 .unwrap_or(0);
69 if ptr != 0 {
70 let rc = cust::sys::cuPointerGetAttribute(
71 value.as_mut_ptr() as *mut c_void,
72 attr,
73 ptr,
74 );
75 if rc == cust::sys::CUresult::CUDA_SUCCESS {
76 device_ordinal = value.assume_init();
77 }
78 }
79 }
80 (2, device_ordinal)
81 }
82
83 #[pyo3(signature = (stream=None, max_version=None, dl_device=None, copy=None))]
84 fn __dlpack__<'py>(
85 &mut self,
86 py: Python<'py>,
87 stream: Option<pyo3::PyObject>,
88 max_version: Option<pyo3::PyObject>,
89 dl_device: Option<pyo3::PyObject>,
90 copy: Option<pyo3::PyObject>,
91 ) -> PyResult<PyObject> {
92 let (kdl, alloc_dev) = self.__dlpack_device__();
93 if let Some(dev_obj) = dl_device.as_ref() {
94 if let Ok((dev_ty, dev_id)) = dev_obj.extract::<(i32, i32)>(py) {
95 if dev_ty != kdl || dev_id != alloc_dev {
96 let wants_copy = copy
97 .as_ref()
98 .and_then(|c| c.extract::<bool>(py).ok())
99 .unwrap_or(false);
100 if wants_copy {
101 return Err(PyValueError::new_err(
102 "device copy not implemented for __dlpack__",
103 ));
104 } else {
105 return Err(PyValueError::new_err("dl_device mismatch for __dlpack__"));
106 }
107 }
108 }
109 }
110 let _ = stream;
111
112 let buf = self
113 .buf
114 .take()
115 .ok_or_else(|| PyValueError::new_err("__dlpack__ may only be called once"))?;
116
117 let rows = self.rows;
118 let cols = self.cols;
119 let max_version_bound = max_version.map(|obj| obj.into_bound(py));
120
121 export_f32_cuda_dlpack_2d(py, buf, rows, cols, alloc_dev, max_version_bound)
122 }
123 }
124
125 pub use DeviceArrayF32Py as VoscDeviceArrayF32Py;
126}
127
128#[cfg(all(feature = "python", feature = "cuda"))]
129use self::vosc_python_cuda_handle::VoscDeviceArrayF32Py;
130#[cfg(feature = "python")]
131use crate::utilities::kernel_validation::validate_kernel;
132#[cfg(feature = "python")]
133use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
134#[cfg(feature = "python")]
135use pyo3::exceptions::PyValueError;
136#[cfg(feature = "python")]
137use pyo3::prelude::*;
138#[cfg(feature = "python")]
139use pyo3::types::PyDict;
140
141#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
142use serde::{Deserialize, Serialize};
143#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
144use wasm_bindgen::prelude::*;
145
146impl<'a> AsRef<[f64]> for VoscInput<'a> {
147 #[inline(always)]
148 fn as_ref(&self) -> &[f64] {
149 match &self.data {
150 VoscData::Slice(slice) => slice,
151 VoscData::Candles { candles, source } => source_type(candles, source),
152 }
153 }
154}
155
156#[derive(Debug, Clone)]
157pub enum VoscData<'a> {
158 Candles {
159 candles: &'a Candles,
160 source: &'a str,
161 },
162 Slice(&'a [f64]),
163}
164
165#[derive(Debug, Clone)]
166pub struct VoscOutput {
167 pub values: Vec<f64>,
168}
169
170#[derive(Debug, Clone)]
171#[cfg_attr(
172 all(target_arch = "wasm32", feature = "wasm"),
173 derive(Serialize, Deserialize)
174)]
175pub struct VoscParams {
176 pub short_period: Option<usize>,
177 pub long_period: Option<usize>,
178}
179
180impl Default for VoscParams {
181 fn default() -> Self {
182 Self {
183 short_period: Some(2),
184 long_period: Some(5),
185 }
186 }
187}
188
189#[derive(Debug, Clone)]
190pub struct VoscInput<'a> {
191 pub data: VoscData<'a>,
192 pub params: VoscParams,
193}
194
195impl<'a> VoscInput<'a> {
196 #[inline]
197 pub fn from_candles(c: &'a Candles, s: &'a str, p: VoscParams) -> Self {
198 Self {
199 data: VoscData::Candles {
200 candles: c,
201 source: s,
202 },
203 params: p,
204 }
205 }
206 #[inline]
207 pub fn from_slice(sl: &'a [f64], p: VoscParams) -> Self {
208 Self {
209 data: VoscData::Slice(sl),
210 params: p,
211 }
212 }
213 #[inline]
214 pub fn with_default_candles(c: &'a Candles) -> Self {
215 Self::from_candles(c, "volume", VoscParams::default())
216 }
217 #[inline]
218 pub fn get_short_period(&self) -> usize {
219 self.params.short_period.unwrap_or(2)
220 }
221 #[inline]
222 pub fn get_long_period(&self) -> usize {
223 self.params.long_period.unwrap_or(5)
224 }
225}
226
227#[derive(Copy, Clone, Debug)]
228pub struct VoscBuilder {
229 short_period: Option<usize>,
230 long_period: Option<usize>,
231 kernel: Kernel,
232}
233
234impl Default for VoscBuilder {
235 fn default() -> Self {
236 Self {
237 short_period: None,
238 long_period: None,
239 kernel: Kernel::Auto,
240 }
241 }
242}
243
244impl VoscBuilder {
245 #[inline(always)]
246 pub fn new() -> Self {
247 Self::default()
248 }
249 #[inline(always)]
250 pub fn short_period(mut self, n: usize) -> Self {
251 self.short_period = Some(n);
252 self
253 }
254 #[inline(always)]
255 pub fn long_period(mut self, n: usize) -> Self {
256 self.long_period = Some(n);
257 self
258 }
259 #[inline(always)]
260 pub fn kernel(mut self, k: Kernel) -> Self {
261 self.kernel = k;
262 self
263 }
264
265 #[inline(always)]
266 pub fn apply(self, c: &Candles) -> Result<VoscOutput, VoscError> {
267 let p = VoscParams {
268 short_period: self.short_period,
269 long_period: self.long_period,
270 };
271 let i = VoscInput::from_candles(c, "volume", p);
272 vosc_with_kernel(&i, self.kernel)
273 }
274
275 #[inline(always)]
276 pub fn apply_slice(self, d: &[f64]) -> Result<VoscOutput, VoscError> {
277 let p = VoscParams {
278 short_period: self.short_period,
279 long_period: self.long_period,
280 };
281 let i = VoscInput::from_slice(d, p);
282 vosc_with_kernel(&i, self.kernel)
283 }
284
285 #[inline(always)]
286 pub fn into_stream(self) -> Result<VoscStream, VoscError> {
287 let p = VoscParams {
288 short_period: self.short_period,
289 long_period: self.long_period,
290 };
291 VoscStream::try_new(p)
292 }
293}
294
295#[derive(Debug, Error)]
296pub enum VoscError {
297 #[error("vosc: empty input data")]
298 EmptyInputData,
299 #[error("vosc: invalid period: period = {period}, data length = {data_len}")]
300 InvalidPeriod { period: usize, data_len: usize },
301 #[error("vosc: short_period > long_period")]
302 ShortPeriodGreaterThanLongPeriod,
303 #[error("vosc: Not enough valid data: needed = {needed}, valid = {valid}")]
304 NotEnoughValidData { needed: usize, valid: usize },
305 #[error("vosc: All values are NaN.")]
306 AllValuesNaN,
307 #[error("vosc: output length mismatch: expected = {expected}, got = {got}")]
308 OutputLengthMismatch { expected: usize, got: usize },
309 #[error("vosc: invalid batch range: start = {start}, end = {end}, step = {step}")]
310 InvalidRange {
311 start: usize,
312 end: usize,
313 step: usize,
314 },
315 #[error("vosc: invalid kernel for batch: {0:?}")]
316 InvalidKernelForBatch(Kernel),
317}
318
319#[inline]
320pub fn vosc(input: &VoscInput) -> Result<VoscOutput, VoscError> {
321 vosc_with_kernel(input, Kernel::Auto)
322}
323
324pub fn vosc_with_kernel(input: &VoscInput, kernel: Kernel) -> Result<VoscOutput, VoscError> {
325 let data: &[f64] = match &input.data {
326 VoscData::Candles { candles, source } => source_type(candles, source),
327 VoscData::Slice(sl) => sl,
328 };
329
330 if data.is_empty() {
331 return Err(VoscError::EmptyInputData);
332 }
333
334 let short_period = input.get_short_period();
335 let long_period = input.get_long_period();
336
337 if short_period == 0 || short_period > data.len() {
338 return Err(VoscError::InvalidPeriod {
339 period: short_period,
340 data_len: data.len(),
341 });
342 }
343 if long_period == 0 || long_period > data.len() {
344 return Err(VoscError::InvalidPeriod {
345 period: long_period,
346 data_len: data.len(),
347 });
348 }
349 if short_period > long_period {
350 return Err(VoscError::ShortPeriodGreaterThanLongPeriod);
351 }
352
353 let first = match data.iter().position(|&x| !x.is_nan()) {
354 Some(idx) => idx,
355 None => return Err(VoscError::AllValuesNaN),
356 };
357 if (data.len() - first) < long_period {
358 return Err(VoscError::NotEnoughValidData {
359 needed: long_period,
360 valid: data.len() - first,
361 });
362 }
363
364 let chosen = match kernel {
365 Kernel::Auto => Kernel::Scalar,
366 other => other,
367 };
368
369 let warmup_period = first
370 .checked_add(long_period)
371 .and_then(|v| v.checked_sub(1))
372 .ok_or(VoscError::InvalidPeriod {
373 period: long_period,
374 data_len: data.len(),
375 })?;
376 let mut out = alloc_with_nan_prefix(data.len(), warmup_period);
377
378 unsafe {
379 match chosen {
380 Kernel::Scalar | Kernel::ScalarBatch => {
381 vosc_scalar(data, short_period, long_period, first, &mut out)
382 }
383 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
384 Kernel::Avx2 | Kernel::Avx2Batch => {
385 vosc_avx2(data, short_period, long_period, first, &mut out)
386 }
387 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
388 Kernel::Avx512 | Kernel::Avx512Batch => {
389 vosc_avx512(data, short_period, long_period, first, &mut out)
390 }
391 _ => unreachable!(),
392 }
393 }
394
395 Ok(VoscOutput { values: out })
396}
397
398#[inline]
399pub fn vosc_scalar(
400 data: &[f64],
401 short_period: usize,
402 long_period: usize,
403 first_valid: usize,
404 out: &mut [f64],
405) {
406 let short_div = 1.0 / (short_period as f64);
407 let long_div = 1.0 / (long_period as f64);
408
409 let start = first_valid;
410 let end_init = start + long_period;
411 let short_start = end_init - short_period;
412
413 let mut short_sum = 0.0f64;
414 let mut long_sum = 0.0f64;
415
416 unsafe {
417 let mut i = start;
418 while i < end_init {
419 let v = *data.get_unchecked(i);
420 long_sum += v;
421 if i >= short_start {
422 short_sum += v;
423 }
424 i += 1;
425 }
426
427 let mut idx = end_init - 1;
428
429 let mut lavg = long_sum * long_div;
430 let mut savg = short_sum * short_div;
431 *out.get_unchecked_mut(idx) = 100.0 * (savg - lavg) / lavg;
432
433 let mut t_s = end_init - short_period;
434 let mut t_l = start;
435
436 let mut j = end_init;
437 let len = data.len();
438
439 while j < len {
440 let x_new = *data.get_unchecked(j);
441
442 short_sum += x_new;
443 short_sum -= *data.get_unchecked(t_s);
444
445 long_sum += x_new;
446 long_sum -= *data.get_unchecked(t_l);
447
448 t_s += 1;
449 t_l += 1;
450
451 idx += 1;
452
453 lavg = long_sum * long_div;
454 savg = short_sum * short_div;
455 *out.get_unchecked_mut(idx) = 100.0 * (savg - lavg) / lavg;
456
457 j += 1;
458 }
459 }
460}
461
462#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
463#[inline]
464pub fn vosc_avx512(
465 data: &[f64],
466 short_period: usize,
467 long_period: usize,
468 first_valid: usize,
469 out: &mut [f64],
470) {
471 unsafe {
472 let short_div = 1.0 / (short_period as f64);
473 let long_div = 1.0 / (long_period as f64);
474
475 let start = first_valid;
476 let end_init = start + long_period;
477 let short_start = end_init - short_period;
478 let len = data.len();
479 let dptr = data.as_ptr();
480
481 let mut short_sum = 0.0f64;
482 let mut long_sum = 0.0f64;
483
484 let mut i = start;
485 let end_a = short_start;
486 if end_a > i {
487 let mut acc = _mm512_setzero_pd();
488 while i + 8 <= end_a {
489 let v = _mm512_loadu_pd(dptr.add(i));
490 acc = _mm512_add_pd(acc, v);
491 i += 8;
492 }
493 let mut tmp = [0.0f64; 8];
494 _mm512_storeu_pd(tmp.as_mut_ptr(), acc);
495 long_sum += tmp.iter().sum::<f64>();
496 while i < end_a {
497 long_sum += *dptr.add(i);
498 i += 1;
499 }
500 }
501
502 let end_b = end_init;
503 if end_b > i {
504 let mut acc = _mm512_setzero_pd();
505 while i + 8 <= end_b {
506 let v = _mm512_loadu_pd(dptr.add(i));
507 acc = _mm512_add_pd(acc, v);
508 i += 8;
509 }
510 let mut tmp = [0.0f64; 8];
511 _mm512_storeu_pd(tmp.as_mut_ptr(), acc);
512 let block = tmp.iter().sum::<f64>();
513 long_sum += block;
514 short_sum += block;
515 while i < end_b {
516 let x = *dptr.add(i);
517 long_sum += x;
518 short_sum += x;
519 i += 1;
520 }
521 }
522
523 let mut idx = end_b - 1;
524 let mut lavg = long_sum * long_div;
525 let mut savg = short_sum * short_div;
526 *out.get_unchecked_mut(idx) = 100.0 * (savg - lavg) / lavg;
527
528 let mut t_s = end_init - short_period;
529 let mut t_l = start;
530 let mut j = end_init;
531 while j < len {
532 let x = *dptr.add(j);
533 short_sum += x;
534 short_sum -= *dptr.add(t_s);
535 long_sum += x;
536 long_sum -= *dptr.add(t_l);
537 t_s += 1;
538 t_l += 1;
539 j += 1;
540 idx += 1;
541 lavg = long_sum * long_div;
542 savg = short_sum * short_div;
543 *out.get_unchecked_mut(idx) = 100.0 * (savg - lavg) / lavg;
544 }
545 }
546}
547#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
548#[inline]
549pub fn vosc_avx2(
550 data: &[f64],
551 short_period: usize,
552 long_period: usize,
553 first_valid: usize,
554 out: &mut [f64],
555) {
556 unsafe {
557 let short_div = 1.0 / (short_period as f64);
558 let long_div = 1.0 / (long_period as f64);
559
560 let start = first_valid;
561 let end_init = start + long_period;
562 let short_start = end_init - short_period;
563 let len = data.len();
564 let dptr = data.as_ptr();
565
566 let mut short_sum = 0.0f64;
567 let mut long_sum = 0.0f64;
568
569 let mut i = start;
570 let end_a = short_start;
571 if end_a > i {
572 let mut acc = _mm256_setzero_pd();
573 while i + 4 <= end_a {
574 let v = _mm256_loadu_pd(dptr.add(i));
575 acc = _mm256_add_pd(acc, v);
576 i += 4;
577 }
578 let mut tmp = [0.0f64; 4];
579 _mm256_storeu_pd(tmp.as_mut_ptr(), acc);
580 long_sum += tmp.iter().sum::<f64>();
581 while i < end_a {
582 long_sum += *dptr.add(i);
583 i += 1;
584 }
585 }
586
587 let end_b = end_init;
588 if end_b > i {
589 let mut acc = _mm256_setzero_pd();
590 while i + 4 <= end_b {
591 let v = _mm256_loadu_pd(dptr.add(i));
592 acc = _mm256_add_pd(acc, v);
593 i += 4;
594 }
595 let mut tmp = [0.0f64; 4];
596 _mm256_storeu_pd(tmp.as_mut_ptr(), acc);
597 let block = tmp.iter().sum::<f64>();
598 long_sum += block;
599 short_sum += block;
600 while i < end_b {
601 let x = *dptr.add(i);
602 long_sum += x;
603 short_sum += x;
604 i += 1;
605 }
606 }
607
608 let mut idx = end_b - 1;
609 let mut lavg = long_sum * long_div;
610 let mut savg = short_sum * short_div;
611 *out.get_unchecked_mut(idx) = 100.0 * (savg - lavg) / lavg;
612
613 let mut t_s = end_init - short_period;
614 let mut t_l = start;
615 let mut j = end_init;
616 while j < len {
617 let x = *dptr.add(j);
618 short_sum += x;
619 short_sum -= *dptr.add(t_s);
620 long_sum += x;
621 long_sum -= *dptr.add(t_l);
622 t_s += 1;
623 t_l += 1;
624 j += 1;
625 idx += 1;
626 lavg = long_sum * long_div;
627 savg = short_sum * short_div;
628 *out.get_unchecked_mut(idx) = 100.0 * (savg - lavg) / lavg;
629 }
630 }
631}
632
633#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
634#[inline]
635pub unsafe fn vosc_avx512_short(
636 data: &[f64],
637 short_period: usize,
638 long_period: usize,
639 first_valid: usize,
640 out: &mut [f64],
641) {
642 vosc_scalar(data, short_period, long_period, first_valid, out)
643}
644#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
645#[inline]
646pub unsafe fn vosc_avx512_long(
647 data: &[f64],
648 short_period: usize,
649 long_period: usize,
650 first_valid: usize,
651 out: &mut [f64],
652) {
653 vosc_scalar(data, short_period, long_period, first_valid, out)
654}
655
656#[inline]
657pub fn vosc_row_scalar(
658 data: &[f64],
659 first: usize,
660 short_period: usize,
661 long_period: usize,
662 out: &mut [f64],
663) {
664 vosc_scalar(data, short_period, long_period, first, out)
665}
666
667#[inline(always)]
668fn vosc_row_scalar_prefix(
669 data: &[f64],
670 prefix: &[f64],
671 first: usize,
672 short_period: usize,
673 long_period: usize,
674 out: &mut [f64],
675) {
676 let warm = match first
677 .checked_add(long_period)
678 .and_then(|v| v.checked_sub(1))
679 {
680 Some(w) => w,
681 None => return,
682 };
683 let short_div = 1.0 / (short_period as f64);
684 let long_div = 1.0 / (long_period as f64);
685 let len = data.len();
686 if warm >= len {
687 return;
688 }
689 unsafe {
690 let mut i = warm;
691 while i < len {
692 let ip1 = i + 1;
693 let long_sum = *prefix.get_unchecked(ip1) - *prefix.get_unchecked(ip1 - long_period);
694 let short_sum = *prefix.get_unchecked(ip1) - *prefix.get_unchecked(ip1 - short_period);
695 let lavg = long_sum * long_div;
696 let savg = short_sum * short_div;
697 *out.get_unchecked_mut(i) = 100.0 * (savg - lavg) / lavg;
698 i += 1;
699 }
700 }
701}
702
703#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
704#[inline]
705pub unsafe fn vosc_row_avx2(
706 data: &[f64],
707 first: usize,
708 short_period: usize,
709 long_period: usize,
710 out: &mut [f64],
711) {
712 vosc_scalar(data, short_period, long_period, first, out)
713}
714#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
715#[inline]
716pub unsafe fn vosc_row_avx512(
717 data: &[f64],
718 first: usize,
719 short_period: usize,
720 long_period: usize,
721 out: &mut [f64],
722) {
723 vosc_scalar(data, short_period, long_period, first, out)
724}
725#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
726#[inline]
727pub unsafe fn vosc_row_avx512_short(
728 data: &[f64],
729 first: usize,
730 short_period: usize,
731 long_period: usize,
732 out: &mut [f64],
733) {
734 vosc_scalar(data, short_period, long_period, first, out)
735}
736#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
737#[inline]
738pub unsafe fn vosc_row_avx512_long(
739 data: &[f64],
740 first: usize,
741 short_period: usize,
742 long_period: usize,
743 out: &mut [f64],
744) {
745 vosc_scalar(data, short_period, long_period, first, out)
746}
747
748#[derive(Debug, Clone)]
749pub struct VoscStream {
750 short_period: usize,
751 long_period: usize,
752
753 buf: Vec<f64>,
754 head: usize,
755 count: usize,
756
757 short_sum: f64,
758 long_sum: f64,
759 short_nan: usize,
760 long_nan: usize,
761
762 inv_short: f64,
763 inv_long: f64,
764}
765
766impl VoscStream {
767 pub fn try_new(params: VoscParams) -> Result<Self, VoscError> {
768 let short_period = params.short_period.unwrap_or(2);
769 let long_period = params.long_period.unwrap_or(5);
770
771 if short_period == 0 || long_period == 0 {
772 return Err(VoscError::InvalidPeriod {
773 period: short_period.max(long_period),
774 data_len: 0,
775 });
776 }
777 if short_period > long_period {
778 return Err(VoscError::ShortPeriodGreaterThanLongPeriod);
779 }
780
781 Ok(Self {
782 short_period,
783 long_period,
784
785 buf: vec![f64::NAN; long_period],
786 head: 0,
787 count: 0,
788
789 short_sum: 0.0,
790 long_sum: 0.0,
791 short_nan: short_period,
792 long_nan: long_period,
793
794 inv_short: 1.0 / (short_period as f64),
795 inv_long: 1.0 / (long_period as f64),
796 })
797 }
798
799 #[inline(always)]
800 pub fn update(&mut self, value: f64) -> Option<f64> {
801 let L = self.long_period;
802 let S = self.short_period;
803 let head = self.head;
804
805 let old_long = unsafe { *self.buf.get_unchecked(head) };
806
807 let old_short = if self.count >= S {
808 unsafe { *self.buf.get_unchecked((head + L - S) % L) }
809 } else {
810 0.0
811 };
812
813 if value.is_nan() {
814 self.long_nan += 1;
815 self.short_nan += 1;
816 } else {
817 self.long_sum += value;
818 self.short_sum += value;
819 }
820
821 if self.count >= L {
822 if old_long.is_nan() {
823 self.long_nan -= 1;
824 } else {
825 self.long_sum -= old_long;
826 }
827 } else {
828 self.long_nan -= 1;
829 }
830
831 if self.count >= S {
832 if old_short.is_nan() {
833 self.short_nan -= 1;
834 } else {
835 self.short_sum -= old_short;
836 }
837 } else {
838 self.short_nan -= 1;
839 }
840
841 unsafe {
842 *self.buf.get_unchecked_mut(head) = value;
843 }
844 let mut next = head + 1;
845 if next == L {
846 next = 0;
847 }
848 self.head = next;
849
850 if self.count < L {
851 self.count += 1;
852 if self.count < L {
853 return None;
854 }
855 }
856
857 debug_assert!(self.count >= S);
858
859 if self.long_nan != 0 || self.short_nan != 0 {
860 return Some(f64::NAN);
861 }
862
863 let lavg = self.long_sum * self.inv_long;
864 let savg = self.short_sum * self.inv_short;
865 Some(100.0 * (savg - lavg) / lavg)
866 }
867}
868
869#[derive(Clone, Debug)]
870pub struct VoscBatchRange {
871 pub short_period: (usize, usize, usize),
872 pub long_period: (usize, usize, usize),
873}
874
875impl Default for VoscBatchRange {
876 fn default() -> Self {
877 Self {
878 short_period: (2, 2, 0),
879 long_period: (5, 254, 1),
880 }
881 }
882}
883
884#[derive(Clone, Debug, Default)]
885pub struct VoscBatchBuilder {
886 range: VoscBatchRange,
887 kernel: Kernel,
888}
889
890impl VoscBatchBuilder {
891 pub fn new() -> Self {
892 Self::default()
893 }
894 pub fn kernel(mut self, k: Kernel) -> Self {
895 self.kernel = k;
896 self
897 }
898 #[inline]
899 pub fn short_period_range(mut self, start: usize, end: usize, step: usize) -> Self {
900 self.range.short_period = (start, end, step);
901 self
902 }
903 #[inline]
904 pub fn short_period_static(mut self, n: usize) -> Self {
905 self.range.short_period = (n, n, 0);
906 self
907 }
908 #[inline]
909 pub fn long_period_range(mut self, start: usize, end: usize, step: usize) -> Self {
910 self.range.long_period = (start, end, step);
911 self
912 }
913 #[inline]
914 pub fn long_period_static(mut self, n: usize) -> Self {
915 self.range.long_period = (n, n, 0);
916 self
917 }
918 pub fn apply_slice(self, data: &[f64]) -> Result<VoscBatchOutput, VoscError> {
919 vosc_batch_with_kernel(data, &self.range, self.kernel)
920 }
921 pub fn with_default_slice(data: &[f64], k: Kernel) -> Result<VoscBatchOutput, VoscError> {
922 VoscBatchBuilder::new().kernel(k).apply_slice(data)
923 }
924 pub fn apply_candles(self, c: &Candles, src: &str) -> Result<VoscBatchOutput, VoscError> {
925 let slice = source_type(c, src);
926 self.apply_slice(slice)
927 }
928 pub fn with_default_candles(c: &Candles) -> Result<VoscBatchOutput, VoscError> {
929 VoscBatchBuilder::new()
930 .kernel(Kernel::Auto)
931 .apply_candles(c, "volume")
932 }
933}
934
935pub fn vosc_batch_with_kernel(
936 data: &[f64],
937 sweep: &VoscBatchRange,
938 k: Kernel,
939) -> Result<VoscBatchOutput, VoscError> {
940 let kernel = match k {
941 Kernel::Auto => Kernel::ScalarBatch,
942 other if other.is_batch() => other,
943 _ => return Err(VoscError::InvalidKernelForBatch(k)),
944 };
945 let mut simd = match kernel {
946 Kernel::Avx512Batch => Kernel::Avx512,
947 Kernel::Avx2Batch => Kernel::Avx2,
948 Kernel::ScalarBatch => Kernel::Scalar,
949 _ => unreachable!(),
950 };
951
952 #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
953 {
954 simd = Kernel::Scalar;
955 }
956 vosc_batch_par_slice(data, sweep, simd)
957}
958
959#[derive(Clone, Debug)]
960pub struct VoscBatchOutput {
961 pub values: Vec<f64>,
962 pub combos: Vec<VoscParams>,
963 pub rows: usize,
964 pub cols: usize,
965}
966impl VoscBatchOutput {
967 pub fn row_for_params(&self, p: &VoscParams) -> Option<usize> {
968 self.combos.iter().position(|c| {
969 c.short_period.unwrap_or(2) == p.short_period.unwrap_or(2)
970 && c.long_period.unwrap_or(5) == p.long_period.unwrap_or(5)
971 })
972 }
973
974 pub fn values_for(&self, p: &VoscParams) -> Option<&[f64]> {
975 self.row_for_params(p).map(|row| {
976 let start = row * self.cols;
977 &self.values[start..start + self.cols]
978 })
979 }
980}
981
982#[inline(always)]
983fn expand_grid(r: &VoscBatchRange) -> Vec<VoscParams> {
984 fn axis_usize((start, end, step): (usize, usize, usize)) -> Vec<usize> {
985 if step == 0 || start == end {
986 return vec![start];
987 }
988 let mut out = Vec::new();
989 if start <= end {
990 let mut v = start;
991 while v <= end {
992 out.push(v);
993 match v.checked_add(step) {
994 Some(next) if next > v => v = next,
995 _ => break,
996 }
997 }
998 } else {
999 let mut v = start;
1000 loop {
1001 out.push(v);
1002 if v <= end {
1003 break;
1004 }
1005 match v.checked_sub(step) {
1006 Some(next) if next < v => {
1007 v = next;
1008 if v < end {
1009 break;
1010 }
1011 }
1012 _ => break,
1013 }
1014 }
1015 }
1016 out
1017 }
1018 let shorts = axis_usize(r.short_period);
1019 let longs = axis_usize(r.long_period);
1020 let mut out = Vec::with_capacity(shorts.len() * longs.len());
1021 for &s in &shorts {
1022 for &l in &longs {
1023 if s <= l {
1024 out.push(VoscParams {
1025 short_period: Some(s),
1026 long_period: Some(l),
1027 });
1028 }
1029 }
1030 }
1031 out
1032}
1033
1034#[inline(always)]
1035pub fn vosc_batch_slice(
1036 data: &[f64],
1037 sweep: &VoscBatchRange,
1038 kern: Kernel,
1039) -> Result<VoscBatchOutput, VoscError> {
1040 vosc_batch_inner(data, sweep, kern, false)
1041}
1042
1043#[inline(always)]
1044pub fn vosc_batch_par_slice(
1045 data: &[f64],
1046 sweep: &VoscBatchRange,
1047 kern: Kernel,
1048) -> Result<VoscBatchOutput, VoscError> {
1049 vosc_batch_inner(data, sweep, kern, true)
1050}
1051
1052#[inline(always)]
1053fn vosc_batch_inner(
1054 data: &[f64],
1055 sweep: &VoscBatchRange,
1056 kern: Kernel,
1057 parallel: bool,
1058) -> Result<VoscBatchOutput, VoscError> {
1059 if data.is_empty() {
1060 return Err(VoscError::EmptyInputData);
1061 }
1062
1063 let combos = expand_grid(sweep);
1064 if combos.is_empty() {
1065 return Err(VoscError::InvalidRange {
1066 start: sweep.short_period.0,
1067 end: sweep.short_period.1,
1068 step: sweep.short_period.2,
1069 });
1070 }
1071
1072 let data_len = data.len();
1073 let mut max_long = 0usize;
1074 for c in &combos {
1075 let sp = c.short_period.unwrap();
1076 let lp = c.long_period.unwrap();
1077 if sp == 0 || sp > data_len {
1078 return Err(VoscError::InvalidPeriod {
1079 period: sp,
1080 data_len,
1081 });
1082 }
1083 if lp == 0 || lp > data_len {
1084 return Err(VoscError::InvalidPeriod {
1085 period: lp,
1086 data_len,
1087 });
1088 }
1089 if lp > max_long {
1090 max_long = lp;
1091 }
1092 }
1093
1094 let first = data
1095 .iter()
1096 .position(|x| !x.is_nan())
1097 .ok_or(VoscError::AllValuesNaN)?;
1098 if data.len() - first < max_long {
1099 return Err(VoscError::NotEnoughValidData {
1100 needed: max_long,
1101 valid: data.len() - first,
1102 });
1103 }
1104
1105 let rows = combos.len();
1106 let cols = data.len();
1107
1108 let _total = rows.checked_mul(cols).ok_or(VoscError::InvalidRange {
1109 start: sweep.short_period.0,
1110 end: sweep.short_period.1,
1111 step: sweep.short_period.2,
1112 })?;
1113
1114 let mut buf_mu = make_uninit_matrix(rows, cols);
1115
1116 let mut warmup_periods = Vec::with_capacity(combos.len());
1117 for c in &combos {
1118 let lp = c.long_period.unwrap();
1119 let warm = first.checked_add(lp).and_then(|v| v.checked_sub(1)).ok_or(
1120 VoscError::InvalidPeriod {
1121 period: lp,
1122 data_len: data.len(),
1123 },
1124 )?;
1125 warmup_periods.push(warm);
1126 }
1127
1128 init_matrix_prefixes(&mut buf_mu, cols, &warmup_periods);
1129
1130 let values = unsafe {
1131 let ptr = buf_mu.as_mut_ptr() as *mut f64;
1132 let len = buf_mu.len();
1133 let cap = buf_mu.capacity();
1134 std::mem::forget(buf_mu);
1135 Vec::from_raw_parts(ptr, len, cap)
1136 };
1137
1138 let mut values = values;
1139
1140 let mut prefix = Vec::with_capacity(cols + 1);
1141 prefix.push(0.0f64);
1142 let mut acc = 0.0f64;
1143 for &v in data.iter() {
1144 acc += v;
1145 prefix.push(acc);
1146 }
1147
1148 let do_row = |row: usize, out_row: &mut [f64]| unsafe {
1149 let short = combos[row].short_period.unwrap();
1150 let long = combos[row].long_period.unwrap();
1151 match kern {
1152 Kernel::Scalar => vosc_row_scalar_prefix(data, &prefix, first, short, long, out_row),
1153 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1154 Kernel::Avx2 => vosc_row_avx2(data, first, short, long, out_row),
1155 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1156 Kernel::Avx512 => vosc_row_avx512(data, first, short, long, out_row),
1157 _ => unreachable!(),
1158 }
1159 };
1160 if parallel {
1161 #[cfg(not(target_arch = "wasm32"))]
1162 {
1163 values
1164 .par_chunks_mut(cols)
1165 .enumerate()
1166 .for_each(|(row, slice)| do_row(row, slice));
1167 }
1168
1169 #[cfg(target_arch = "wasm32")]
1170 {
1171 for (row, slice) in values.chunks_mut(cols).enumerate() {
1172 do_row(row, slice);
1173 }
1174 }
1175 } else {
1176 for (row, slice) in values.chunks_mut(cols).enumerate() {
1177 do_row(row, slice);
1178 }
1179 }
1180
1181 Ok(VoscBatchOutput {
1182 values,
1183 combos,
1184 rows,
1185 cols,
1186 })
1187}
1188
1189#[inline(always)]
1190fn vosc_batch_inner_into(
1191 data: &[f64],
1192 sweep: &VoscBatchRange,
1193 kern: Kernel,
1194 parallel: bool,
1195 out: &mut [f64],
1196) -> Result<Vec<VoscParams>, VoscError> {
1197 if data.is_empty() {
1198 return Err(VoscError::EmptyInputData);
1199 }
1200
1201 let combos = expand_grid(sweep);
1202 if combos.is_empty() {
1203 return Err(VoscError::InvalidRange {
1204 start: sweep.short_period.0,
1205 end: sweep.short_period.1,
1206 step: sweep.short_period.2,
1207 });
1208 }
1209
1210 let data_len = data.len();
1211 let mut max_long = 0usize;
1212 for c in &combos {
1213 let sp = c.short_period.unwrap();
1214 let lp = c.long_period.unwrap();
1215 if sp == 0 || sp > data_len {
1216 return Err(VoscError::InvalidPeriod {
1217 period: sp,
1218 data_len,
1219 });
1220 }
1221 if lp == 0 || lp > data_len {
1222 return Err(VoscError::InvalidPeriod {
1223 period: lp,
1224 data_len,
1225 });
1226 }
1227 if lp > max_long {
1228 max_long = lp;
1229 }
1230 }
1231
1232 let first = data
1233 .iter()
1234 .position(|x| !x.is_nan())
1235 .ok_or(VoscError::AllValuesNaN)?;
1236 if data.len() - first < max_long {
1237 return Err(VoscError::NotEnoughValidData {
1238 needed: max_long,
1239 valid: data.len() - first,
1240 });
1241 }
1242
1243 let rows = combos.len();
1244 let cols = data.len();
1245
1246 let total = rows.checked_mul(cols).ok_or(VoscError::InvalidRange {
1247 start: sweep.short_period.0,
1248 end: sweep.short_period.1,
1249 step: sweep.short_period.2,
1250 })?;
1251 if out.len() != total {
1252 return Err(VoscError::OutputLengthMismatch {
1253 expected: total,
1254 got: out.len(),
1255 });
1256 }
1257
1258 let mut warm = Vec::with_capacity(combos.len());
1259 for c in &combos {
1260 let lp = c.long_period.unwrap();
1261 let w = first.checked_add(lp).and_then(|v| v.checked_sub(1)).ok_or(
1262 VoscError::InvalidPeriod {
1263 period: lp,
1264 data_len: data.len(),
1265 },
1266 )?;
1267 warm.push(w);
1268 }
1269
1270 let out_mu: &mut [std::mem::MaybeUninit<f64>] = unsafe {
1271 std::slice::from_raw_parts_mut(
1272 out.as_mut_ptr() as *mut std::mem::MaybeUninit<f64>,
1273 out.len(),
1274 )
1275 };
1276 init_matrix_prefixes(out_mu, cols, &warm);
1277
1278 let mut prefix = Vec::with_capacity(cols + 1);
1279 prefix.push(0.0f64);
1280 let mut acc = 0.0f64;
1281 for &v in data.iter() {
1282 acc += v;
1283 prefix.push(acc);
1284 }
1285
1286 let do_row = |row: usize, out_row: &mut [f64]| unsafe {
1287 let s = combos[row].short_period.unwrap();
1288 let l = combos[row].long_period.unwrap();
1289 match kern {
1290 Kernel::Scalar => vosc_row_scalar_prefix(data, &prefix, first, s, l, out_row),
1291 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1292 Kernel::Avx2 => vosc_row_avx2(data, first, s, l, out_row),
1293 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1294 Kernel::Avx512 => vosc_row_avx512(data, first, s, l, out_row),
1295 _ => unreachable!(),
1296 }
1297 };
1298
1299 if parallel {
1300 #[cfg(not(target_arch = "wasm32"))]
1301 {
1302 out.par_chunks_mut(cols)
1303 .enumerate()
1304 .for_each(|(row, slice)| do_row(row, slice));
1305 }
1306 #[cfg(target_arch = "wasm32")]
1307 {
1308 for (row, slice) in out.chunks_mut(cols).enumerate() {
1309 do_row(row, slice);
1310 }
1311 }
1312 } else {
1313 for (row, slice) in out.chunks_mut(cols).enumerate() {
1314 do_row(row, slice);
1315 }
1316 }
1317
1318 Ok(combos)
1319}
1320
1321#[cfg(test)]
1322mod tests {
1323 use super::*;
1324 use crate::skip_if_unsupported;
1325 use crate::utilities::data_loader::read_candles_from_csv;
1326
1327 fn check_vosc_accuracy(
1328 test_name: &str,
1329 kernel: Kernel,
1330 ) -> Result<(), Box<dyn std::error::Error>> {
1331 skip_if_unsupported!(kernel, test_name);
1332 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1333 let candles = read_candles_from_csv(file_path)?;
1334 let volume = candles
1335 .select_candle_field("volume")
1336 .expect("Failed to extract volume data");
1337 let params = VoscParams {
1338 short_period: Some(2),
1339 long_period: Some(5),
1340 };
1341 let input = VoscInput::from_candles(&candles, "volume", params);
1342 let vosc_result = vosc_with_kernel(&input, kernel)?;
1343
1344 assert_eq!(
1345 vosc_result.values.len(),
1346 volume.len(),
1347 "VOSC length mismatch"
1348 );
1349 let expected_last_five_vosc = [
1350 -39.478510754298895,
1351 -25.886077312645188,
1352 -21.155087549723756,
1353 -12.36093768813373,
1354 48.70809369473075,
1355 ];
1356 let start_index = vosc_result.values.len() - 5;
1357 let result_last_five_vosc = &vosc_result.values[start_index..];
1358 for (i, &value) in result_last_five_vosc.iter().enumerate() {
1359 let expected_value = expected_last_five_vosc[i];
1360 assert!(
1361 (value - expected_value).abs() < 1e-1,
1362 "VOSC mismatch at index {}: expected {}, got {}",
1363 i,
1364 expected_value,
1365 value
1366 );
1367 }
1368 for i in 0..(5 - 1) {
1369 assert!(vosc_result.values[i].is_nan());
1370 }
1371
1372 let default_input = VoscInput::with_default_candles(&candles);
1373 let default_vosc_result = vosc_with_kernel(&default_input, kernel)?;
1374 assert_eq!(default_vosc_result.values.len(), volume.len());
1375 Ok(())
1376 }
1377
1378 fn check_vosc_zero_period(
1379 test: &str,
1380 kernel: Kernel,
1381 ) -> Result<(), Box<dyn std::error::Error>> {
1382 skip_if_unsupported!(kernel, test);
1383 let input_data = [10.0, 20.0, 30.0];
1384 let params = VoscParams {
1385 short_period: Some(0),
1386 long_period: Some(5),
1387 };
1388 let input = VoscInput::from_slice(&input_data, params);
1389 let res = vosc_with_kernel(&input, kernel);
1390 assert!(
1391 res.is_err(),
1392 "[{}] VOSC should fail with zero short_period",
1393 test
1394 );
1395 Ok(())
1396 }
1397
1398 fn check_vosc_short_gt_long(
1399 test: &str,
1400 kernel: Kernel,
1401 ) -> Result<(), Box<dyn std::error::Error>> {
1402 skip_if_unsupported!(kernel, test);
1403 let input_data = [10.0, 20.0, 30.0, 40.0, 50.0];
1404 let params = VoscParams {
1405 short_period: Some(5),
1406 long_period: Some(2),
1407 };
1408 let input = VoscInput::from_slice(&input_data, params);
1409 let res = vosc_with_kernel(&input, kernel);
1410 assert!(
1411 res.is_err(),
1412 "[{}] VOSC should fail when short_period > long_period",
1413 test
1414 );
1415 Ok(())
1416 }
1417
1418 fn check_vosc_not_enough_valid(
1419 test: &str,
1420 kernel: Kernel,
1421 ) -> Result<(), Box<dyn std::error::Error>> {
1422 skip_if_unsupported!(kernel, test);
1423 let data = [f64::NAN, f64::NAN, 1.0, 2.0, 3.0];
1424 let params = VoscParams {
1425 short_period: Some(2),
1426 long_period: Some(5),
1427 };
1428 let input = VoscInput::from_slice(&data, params);
1429 let res = vosc_with_kernel(&input, kernel);
1430 assert!(
1431 res.is_err(),
1432 "[{}] VOSC should fail with not enough valid data",
1433 test
1434 );
1435 Ok(())
1436 }
1437
1438 fn check_vosc_all_nan(test: &str, kernel: Kernel) -> Result<(), Box<dyn std::error::Error>> {
1439 skip_if_unsupported!(kernel, test);
1440 let data = [f64::NAN, f64::NAN, f64::NAN];
1441 let params = VoscParams {
1442 short_period: Some(2),
1443 long_period: Some(3),
1444 };
1445 let input = VoscInput::from_slice(&data, params);
1446 let res = vosc_with_kernel(&input, kernel);
1447 assert!(res.is_err(), "[{}] VOSC should fail with all NaN", test);
1448 Ok(())
1449 }
1450
1451 fn check_vosc_streaming(test: &str, kernel: Kernel) -> Result<(), Box<dyn std::error::Error>> {
1452 skip_if_unsupported!(kernel, test);
1453 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1454 let candles = read_candles_from_csv(file_path)?;
1455 let volume = candles
1456 .select_candle_field("volume")
1457 .expect("Failed to extract volume data");
1458 let short_period = 2;
1459 let long_period = 5;
1460 let input = VoscInput::from_candles(
1461 &candles,
1462 "volume",
1463 VoscParams {
1464 short_period: Some(short_period),
1465 long_period: Some(long_period),
1466 },
1467 );
1468 let batch_output = vosc_with_kernel(&input, kernel)?.values;
1469
1470 let mut stream = VoscStream::try_new(VoscParams {
1471 short_period: Some(short_period),
1472 long_period: Some(long_period),
1473 })?;
1474 let mut stream_values = Vec::with_capacity(volume.len());
1475 for &v in volume {
1476 match stream.update(v) {
1477 Some(val) => stream_values.push(val),
1478 None => stream_values.push(f64::NAN),
1479 }
1480 }
1481 assert_eq!(batch_output.len(), stream_values.len());
1482 for (i, (&b, &s)) in batch_output.iter().zip(stream_values.iter()).enumerate() {
1483 if b.is_nan() && s.is_nan() {
1484 continue;
1485 }
1486 let diff = (b - s).abs();
1487 assert!(
1488 diff < 1e-9,
1489 "[{}] VOSC streaming f64 mismatch at idx {}: batch={}, stream={}, diff={}",
1490 test,
1491 i,
1492 b,
1493 s,
1494 diff
1495 );
1496 }
1497 Ok(())
1498 }
1499
1500 #[cfg(debug_assertions)]
1501 fn check_vosc_no_poison(
1502 test_name: &str,
1503 kernel: Kernel,
1504 ) -> Result<(), Box<dyn std::error::Error>> {
1505 skip_if_unsupported!(kernel, test_name);
1506
1507 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1508 let candles = read_candles_from_csv(file_path)?;
1509
1510 let test_params = vec![
1511 VoscParams::default(),
1512 VoscParams {
1513 short_period: Some(1),
1514 long_period: Some(2),
1515 },
1516 VoscParams {
1517 short_period: Some(1),
1518 long_period: Some(5),
1519 },
1520 VoscParams {
1521 short_period: Some(2),
1522 long_period: Some(10),
1523 },
1524 VoscParams {
1525 short_period: Some(5),
1526 long_period: Some(20),
1527 },
1528 VoscParams {
1529 short_period: Some(10),
1530 long_period: Some(50),
1531 },
1532 VoscParams {
1533 short_period: Some(20),
1534 long_period: Some(100),
1535 },
1536 VoscParams {
1537 short_period: Some(3),
1538 long_period: Some(5),
1539 },
1540 VoscParams {
1541 short_period: Some(10),
1542 long_period: Some(10),
1543 },
1544 VoscParams {
1545 short_period: Some(4),
1546 long_period: Some(12),
1547 },
1548 VoscParams {
1549 short_period: Some(7),
1550 long_period: Some(21),
1551 },
1552 VoscParams {
1553 short_period: Some(14),
1554 long_period: Some(28),
1555 },
1556 ];
1557
1558 for (param_idx, params) in test_params.iter().enumerate() {
1559 let input = VoscInput::from_candles(&candles, "volume", params.clone());
1560 let output = vosc_with_kernel(&input, kernel)?;
1561
1562 for (i, &val) in output.values.iter().enumerate() {
1563 if val.is_nan() {
1564 continue;
1565 }
1566
1567 let bits = val.to_bits();
1568
1569 if bits == 0x11111111_11111111 {
1570 panic!(
1571 "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
1572 with params: short_period={}, long_period={} (param set {})",
1573 test_name,
1574 val,
1575 bits,
1576 i,
1577 params.short_period.unwrap_or(2),
1578 params.long_period.unwrap_or(5),
1579 param_idx
1580 );
1581 }
1582
1583 if bits == 0x22222222_22222222 {
1584 panic!(
1585 "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
1586 with params: short_period={}, long_period={} (param set {})",
1587 test_name,
1588 val,
1589 bits,
1590 i,
1591 params.short_period.unwrap_or(2),
1592 params.long_period.unwrap_or(5),
1593 param_idx
1594 );
1595 }
1596
1597 if bits == 0x33333333_33333333 {
1598 panic!(
1599 "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
1600 with params: short_period={}, long_period={} (param set {})",
1601 test_name,
1602 val,
1603 bits,
1604 i,
1605 params.short_period.unwrap_or(2),
1606 params.long_period.unwrap_or(5),
1607 param_idx
1608 );
1609 }
1610 }
1611 }
1612
1613 Ok(())
1614 }
1615
1616 #[cfg(not(debug_assertions))]
1617 fn check_vosc_no_poison(
1618 _test_name: &str,
1619 _kernel: Kernel,
1620 ) -> Result<(), Box<dyn std::error::Error>> {
1621 Ok(())
1622 }
1623
1624 macro_rules! generate_all_vosc_tests {
1625 ($($test_fn:ident),*) => {
1626 paste::paste! {
1627 $(
1628 #[test]
1629 fn [<$test_fn _scalar_f64>]() {
1630 let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
1631 }
1632 )*
1633 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1634 $(
1635 #[test]
1636 fn [<$test_fn _avx2_f64>]() {
1637 let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
1638 }
1639 #[test]
1640 fn [<$test_fn _avx512_f64>]() {
1641 let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
1642 }
1643 )*
1644 }
1645 }
1646 }
1647
1648 #[cfg(feature = "proptest")]
1649 #[allow(clippy::float_cmp)]
1650 fn check_vosc_property(
1651 test_name: &str,
1652 kernel: Kernel,
1653 ) -> Result<(), Box<dyn std::error::Error>> {
1654 use proptest::prelude::*;
1655 skip_if_unsupported!(kernel, test_name);
1656
1657 let strat = (1usize..=50, 1usize..=50).prop_flat_map(|(short, long)| {
1658 let max_period = short.max(long);
1659 (
1660 prop::collection::vec(
1661 (0.1f64..1e6f64).prop_filter("finite", |x| x.is_finite()),
1662 max_period..400,
1663 ),
1664 Just((short, long)),
1665 )
1666 });
1667
1668 proptest::test_runner::TestRunner::default()
1669 .run(&strat, |(data, (short_period, long_period))| {
1670 if short_period > long_period {
1671 return Ok(());
1672 }
1673
1674 let params = VoscParams {
1675 short_period: Some(short_period),
1676 long_period: Some(long_period),
1677 };
1678 let input = VoscInput::from_slice(&data, params);
1679
1680 let VoscOutput { values: out } = vosc_with_kernel(&input, kernel).unwrap();
1681 let VoscOutput { values: ref_out } =
1682 vosc_with_kernel(&input, Kernel::Scalar).unwrap();
1683
1684 for i in 0..(long_period - 1) {
1685 prop_assert!(
1686 out[i].is_nan(),
1687 "Expected NaN during warmup at index {}, got {}",
1688 i,
1689 out[i]
1690 );
1691 }
1692
1693 for i in (long_period - 1)..data.len() {
1694 let y = out[i];
1695 let r = ref_out[i];
1696
1697 let y_bits = y.to_bits();
1698 let r_bits = r.to_bits();
1699
1700 if !y.is_finite() || !r.is_finite() {
1701 prop_assert!(
1702 y.to_bits() == r.to_bits(),
1703 "finite/NaN mismatch idx {}: {} vs {}",
1704 i,
1705 y,
1706 r
1707 );
1708 continue;
1709 }
1710
1711 let ulp_diff: u64 = y_bits.abs_diff(r_bits);
1712
1713 prop_assert!(
1714 (y - r).abs() <= 1e-9 || ulp_diff <= 4,
1715 "Kernel mismatch idx {}: {} vs {} (ULP={})",
1716 i,
1717 y,
1718 r,
1719 ulp_diff
1720 );
1721 }
1722
1723 for i in long_period..data.len() {
1724 let short_start = i + 1 - short_period;
1725 let long_start = i + 1 - long_period;
1726
1727 let short_sum: f64 = data[short_start..=i].iter().sum();
1728 let long_sum: f64 = data[long_start..=i].iter().sum();
1729
1730 let short_avg = short_sum / short_period as f64;
1731 let long_avg = long_sum / long_period as f64;
1732
1733 let expected = 100.0 * (short_avg - long_avg) / long_avg;
1734 let actual = out[i];
1735
1736 prop_assert!(
1737 (actual - expected).abs() <= 1e-9,
1738 "Formula mismatch at idx {}: expected {}, got {}",
1739 i,
1740 expected,
1741 actual
1742 );
1743 }
1744
1745 if short_period == long_period {
1746 for i in (long_period - 1)..data.len() {
1747 prop_assert!(
1748 out[i].abs() <= 1e-9,
1749 "Expected 0 when periods equal at idx {}: got {}",
1750 i,
1751 out[i]
1752 );
1753 }
1754 }
1755
1756 if data.windows(2).all(|w| (w[0] - w[1]).abs() <= f64::EPSILON) {
1757 for i in (long_period - 1)..data.len() {
1758 prop_assert!(
1759 out[i].abs() <= 1e-9,
1760 "Expected 0 for constant volume at idx {}: got {}",
1761 i,
1762 out[i]
1763 );
1764 }
1765 }
1766
1767 Ok(())
1768 })
1769 .unwrap();
1770
1771 Ok(())
1772 }
1773
1774 generate_all_vosc_tests!(
1775 check_vosc_accuracy,
1776 check_vosc_zero_period,
1777 check_vosc_short_gt_long,
1778 check_vosc_not_enough_valid,
1779 check_vosc_all_nan,
1780 check_vosc_streaming,
1781 check_vosc_no_poison
1782 );
1783
1784 #[cfg(feature = "proptest")]
1785 generate_all_vosc_tests!(check_vosc_property);
1786
1787 fn check_batch_default_row(
1788 test: &str,
1789 kernel: Kernel,
1790 ) -> Result<(), Box<dyn std::error::Error>> {
1791 skip_if_unsupported!(kernel, test);
1792 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1793 let c = read_candles_from_csv(file)?;
1794 let output = VoscBatchBuilder::new()
1795 .kernel(kernel)
1796 .apply_candles(&c, "volume")?;
1797 let def = VoscParams::default();
1798 let row = output.values_for(&def).expect("default row missing");
1799 assert_eq!(row.len(), c.volume.len());
1800
1801 let expected = [
1802 -39.478510754298895,
1803 -25.886077312645188,
1804 -21.155087549723756,
1805 -12.36093768813373,
1806 48.70809369473075,
1807 ];
1808 let start = row.len() - 5;
1809 for (i, &v) in row[start..].iter().enumerate() {
1810 assert!(
1811 (v - expected[i]).abs() < 1e-1,
1812 "[{test}] default-row mismatch at idx {i}: {v} vs {expected:?}"
1813 );
1814 }
1815 Ok(())
1816 }
1817
1818 #[cfg(debug_assertions)]
1819 fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn std::error::Error>> {
1820 skip_if_unsupported!(kernel, test);
1821
1822 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1823 let c = read_candles_from_csv(file)?;
1824
1825 let test_configs = vec![
1826 (1, 5, 1, 2, 10, 2),
1827 (2, 10, 2, 5, 20, 5),
1828 (10, 20, 5, 20, 50, 10),
1829 (1, 3, 1, 3, 6, 1),
1830 (5, 15, 2, 10, 30, 5),
1831 (2, 2, 0, 5, 25, 5),
1832 (1, 10, 3, 10, 10, 0),
1833 (3, 9, 3, 9, 27, 9),
1834 (1, 5, 1, 5, 5, 0),
1835 ];
1836
1837 for (cfg_idx, &(s_start, s_end, s_step, l_start, l_end, l_step)) in
1838 test_configs.iter().enumerate()
1839 {
1840 let output = VoscBatchBuilder::new()
1841 .kernel(kernel)
1842 .short_period_range(s_start, s_end, s_step)
1843 .long_period_range(l_start, l_end, l_step)
1844 .apply_candles(&c, "volume")?;
1845
1846 for (idx, &val) in output.values.iter().enumerate() {
1847 if val.is_nan() {
1848 continue;
1849 }
1850
1851 let bits = val.to_bits();
1852 let row = idx / output.cols;
1853 let col = idx % output.cols;
1854 let combo = &output.combos[row];
1855
1856 if bits == 0x11111111_11111111 {
1857 panic!(
1858 "[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
1859 at row {} col {} (flat index {}) with params: short_period={}, long_period={}",
1860 test,
1861 cfg_idx,
1862 val,
1863 bits,
1864 row,
1865 col,
1866 idx,
1867 combo.short_period.unwrap_or(2),
1868 combo.long_period.unwrap_or(5)
1869 );
1870 }
1871
1872 if bits == 0x22222222_22222222 {
1873 panic!(
1874 "[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
1875 at row {} col {} (flat index {}) with params: short_period={}, long_period={}",
1876 test,
1877 cfg_idx,
1878 val,
1879 bits,
1880 row,
1881 col,
1882 idx,
1883 combo.short_period.unwrap_or(2),
1884 combo.long_period.unwrap_or(5)
1885 );
1886 }
1887
1888 if bits == 0x33333333_33333333 {
1889 panic!(
1890 "[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
1891 at row {} col {} (flat index {}) with params: short_period={}, long_period={}",
1892 test,
1893 cfg_idx,
1894 val,
1895 bits,
1896 row,
1897 col,
1898 idx,
1899 combo.short_period.unwrap_or(2),
1900 combo.long_period.unwrap_or(5)
1901 );
1902 }
1903 }
1904 }
1905
1906 Ok(())
1907 }
1908
1909 #[cfg(not(debug_assertions))]
1910 fn check_batch_no_poison(
1911 _test: &str,
1912 _kernel: Kernel,
1913 ) -> Result<(), Box<dyn std::error::Error>> {
1914 Ok(())
1915 }
1916
1917 macro_rules! gen_batch_tests {
1918 ($fn_name:ident) => {
1919 paste::paste! {
1920 #[test] fn [<$fn_name _scalar>]() {
1921 let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
1922 }
1923 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1924 #[test] fn [<$fn_name _avx2>]() {
1925 let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
1926 }
1927 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1928 #[test] fn [<$fn_name _avx512>]() {
1929 let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
1930 }
1931 #[test] fn [<$fn_name _auto_detect>]() {
1932 let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
1933 }
1934 }
1935 };
1936 }
1937 gen_batch_tests!(check_batch_default_row);
1938 gen_batch_tests!(check_batch_no_poison);
1939
1940 #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1941 #[test]
1942 fn test_vosc_into_matches_api() -> Result<(), Box<dyn std::error::Error>> {
1943 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1944 let candles = read_candles_from_csv(file_path)?;
1945 let volume = candles
1946 .select_candle_field("volume")
1947 .expect("Failed to extract volume data");
1948
1949 let input = VoscInput::with_default_candles(&candles);
1950
1951 let baseline = vosc(&input)?.values;
1952
1953 let mut out = vec![0.0f64; volume.len()];
1954 vosc_into(&input, &mut out)?;
1955
1956 assert_eq!(baseline.len(), out.len());
1957
1958 #[inline]
1959 fn eq_or_both_nan(a: f64, b: f64) -> bool {
1960 (a.is_nan() && b.is_nan()) || (a == b) || (a - b).abs() <= 1e-12
1961 }
1962
1963 for (i, (&a, &b)) in baseline.iter().zip(out.iter()).enumerate() {
1964 assert!(
1965 eq_or_both_nan(a, b),
1966 "VOSC parity mismatch at index {}: api={}, into={}",
1967 i,
1968 a,
1969 b
1970 );
1971 }
1972
1973 Ok(())
1974 }
1975}
1976
1977#[cfg(feature = "python")]
1978#[pyfunction(name = "vosc")]
1979#[pyo3(signature = (data, short_period=2, long_period=5, kernel=None))]
1980pub fn vosc_py<'py>(
1981 py: Python<'py>,
1982 data: PyReadonlyArray1<'py, f64>,
1983 short_period: usize,
1984 long_period: usize,
1985 kernel: Option<&str>,
1986) -> PyResult<Bound<'py, PyArray1<f64>>> {
1987 use numpy::{IntoPyArray, PyArrayMethods};
1988
1989 let slice_in = data.as_slice()?;
1990 let kern = validate_kernel(kernel, false)?;
1991
1992 let params = VoscParams {
1993 short_period: Some(short_period),
1994 long_period: Some(long_period),
1995 };
1996 let input = VoscInput::from_slice(slice_in, params);
1997
1998 let result_vec: Vec<f64> = py
1999 .allow_threads(|| vosc_with_kernel(&input, kern).map(|o| o.values))
2000 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2001
2002 Ok(result_vec.into_pyarray(py))
2003}
2004
2005#[cfg(feature = "python")]
2006#[pyclass(name = "VoscStream")]
2007pub struct VoscStreamPy {
2008 stream: VoscStream,
2009}
2010
2011#[cfg(feature = "python")]
2012#[pymethods]
2013impl VoscStreamPy {
2014 #[new]
2015 fn new(short_period: usize, long_period: usize) -> PyResult<Self> {
2016 let params = VoscParams {
2017 short_period: Some(short_period),
2018 long_period: Some(long_period),
2019 };
2020 let stream =
2021 VoscStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
2022 Ok(VoscStreamPy { stream })
2023 }
2024
2025 fn update(&mut self, value: f64) -> Option<f64> {
2026 self.stream.update(value)
2027 }
2028}
2029
2030#[cfg(feature = "python")]
2031#[pyfunction(name = "vosc_batch")]
2032#[pyo3(signature = (data, short_period_range, long_period_range, kernel=None))]
2033pub fn vosc_batch_py<'py>(
2034 py: Python<'py>,
2035 data: PyReadonlyArray1<'py, f64>,
2036 short_period_range: (usize, usize, usize),
2037 long_period_range: (usize, usize, usize),
2038 kernel: Option<&str>,
2039) -> PyResult<Bound<'py, PyDict>> {
2040 use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
2041 use pyo3::types::PyDict;
2042
2043 let slice_in = data.as_slice()?;
2044 let kern = validate_kernel(kernel, true)?;
2045
2046 let sweep = VoscBatchRange {
2047 short_period: short_period_range,
2048 long_period: long_period_range,
2049 };
2050
2051 let combos = expand_grid(&sweep);
2052 if combos.is_empty() {
2053 return Err(PyValueError::new_err("No valid parameter combinations"));
2054 }
2055 let rows = combos.len();
2056 let cols = slice_in.len();
2057
2058 let total = rows
2059 .checked_mul(cols)
2060 .ok_or_else(|| PyValueError::new_err("rows * cols overflow in vosc_batch_py"))?;
2061
2062 let out_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2063 let slice_out = unsafe { out_arr.as_slice_mut()? };
2064
2065 let combos = py
2066 .allow_threads(|| -> Result<Vec<VoscParams>, VoscError> {
2067 let kernel = match kern {
2068 Kernel::Auto => detect_best_batch_kernel(),
2069 k => k,
2070 };
2071
2072 let simd = match kernel {
2073 Kernel::Avx512Batch => Kernel::Avx512,
2074 Kernel::Avx2Batch => Kernel::Avx2,
2075 Kernel::ScalarBatch => Kernel::Scalar,
2076 _ => kernel,
2077 };
2078
2079 vosc_batch_inner_into(slice_in, &sweep, simd, true, slice_out)
2080 })
2081 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2082
2083 let dict = PyDict::new(py);
2084 dict.set_item("values", out_arr.reshape((rows, cols))?)?;
2085 dict.set_item(
2086 "short_periods",
2087 combos
2088 .iter()
2089 .map(|p| p.short_period.unwrap() as u64)
2090 .collect::<Vec<_>>()
2091 .into_pyarray(py),
2092 )?;
2093 dict.set_item(
2094 "long_periods",
2095 combos
2096 .iter()
2097 .map(|p| p.long_period.unwrap() as u64)
2098 .collect::<Vec<_>>()
2099 .into_pyarray(py),
2100 )?;
2101
2102 Ok(dict)
2103}
2104
2105#[cfg(all(feature = "python", feature = "cuda"))]
2106#[pyfunction(name = "vosc_cuda_batch_dev")]
2107#[pyo3(signature = (data_f32, short_period_range, long_period_range, device_id=0))]
2108pub fn vosc_cuda_batch_dev_py(
2109 py: Python<'_>,
2110 data_f32: numpy::PyReadonlyArray1<'_, f32>,
2111 short_period_range: (usize, usize, usize),
2112 long_period_range: (usize, usize, usize),
2113 device_id: usize,
2114) -> PyResult<VoscDeviceArrayF32Py> {
2115 use crate::cuda::cuda_available;
2116 use crate::cuda::CudaVosc;
2117
2118 if !cuda_available() {
2119 return Err(PyValueError::new_err("CUDA not available"));
2120 }
2121 let slice_in: &[f32] = data_f32.as_slice()?;
2122 let sweep = VoscBatchRange {
2123 short_period: short_period_range,
2124 long_period: long_period_range,
2125 };
2126 let (dev, ctx, dev_id_u32) = py.allow_threads(|| {
2127 let cuda = CudaVosc::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2128 let ctx = cuda.context_arc();
2129 let dev_id_u32 = cuda.device_id();
2130 cuda.vosc_batch_dev(slice_in, &sweep)
2131 .map(|(dev, _combos)| (dev, ctx, dev_id_u32))
2132 .map_err(|e| PyValueError::new_err(e.to_string()))
2133 })?;
2134 Ok(VoscDeviceArrayF32Py {
2135 buf: Some(dev.buf),
2136 rows: dev.rows,
2137 cols: dev.cols,
2138 _ctx: ctx,
2139 device_id: dev_id_u32,
2140 })
2141}
2142
2143#[cfg(all(feature = "python", feature = "cuda"))]
2144#[pyfunction(name = "vosc_cuda_many_series_one_param_dev")]
2145#[pyo3(signature = (data_tm_f32, short_period, long_period, device_id=0))]
2146pub fn vosc_cuda_many_series_one_param_dev_py(
2147 py: Python<'_>,
2148 data_tm_f32: numpy::PyReadonlyArray2<'_, f32>,
2149 short_period: usize,
2150 long_period: usize,
2151 device_id: usize,
2152) -> PyResult<VoscDeviceArrayF32Py> {
2153 use crate::cuda::cuda_available;
2154 use crate::cuda::CudaVosc;
2155 use numpy::PyUntypedArrayMethods;
2156
2157 if !cuda_available() {
2158 return Err(PyValueError::new_err("CUDA not available"));
2159 }
2160 let flat_in: &[f32] = data_tm_f32.as_slice()?;
2161 let rows = data_tm_f32.shape()[0];
2162 let cols = data_tm_f32.shape()[1];
2163 let params = VoscParams {
2164 short_period: Some(short_period),
2165 long_period: Some(long_period),
2166 };
2167 let (dev, ctx, dev_id_u32) = py.allow_threads(|| {
2168 let cuda = CudaVosc::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2169 let ctx = cuda.context_arc();
2170 let dev_id_u32 = cuda.device_id();
2171 cuda.vosc_many_series_one_param_time_major_dev(flat_in, cols, rows, ¶ms)
2172 .map(|dev| (dev, ctx, dev_id_u32))
2173 .map_err(|e| PyValueError::new_err(e.to_string()))
2174 })?;
2175 Ok(VoscDeviceArrayF32Py {
2176 buf: Some(dev.buf),
2177 rows: dev.rows,
2178 cols: dev.cols,
2179 _ctx: ctx,
2180 device_id: dev_id_u32,
2181 })
2182}
2183
2184#[inline]
2185pub fn vosc_into_slice(dst: &mut [f64], input: &VoscInput, kern: Kernel) -> Result<(), VoscError> {
2186 let data: &[f64] = match &input.data {
2187 VoscData::Candles { candles, source } => source_type(candles, source),
2188 VoscData::Slice(sl) => sl,
2189 };
2190
2191 if data.is_empty() {
2192 return Err(VoscError::EmptyInputData);
2193 }
2194
2195 if dst.len() != data.len() {
2196 return Err(VoscError::OutputLengthMismatch {
2197 expected: data.len(),
2198 got: dst.len(),
2199 });
2200 }
2201
2202 let short_period = input.get_short_period();
2203 let long_period = input.get_long_period();
2204
2205 if short_period == 0 || short_period > data.len() {
2206 return Err(VoscError::InvalidPeriod {
2207 period: short_period,
2208 data_len: data.len(),
2209 });
2210 }
2211 if long_period == 0 || long_period > data.len() {
2212 return Err(VoscError::InvalidPeriod {
2213 period: long_period,
2214 data_len: data.len(),
2215 });
2216 }
2217 if short_period > long_period {
2218 return Err(VoscError::ShortPeriodGreaterThanLongPeriod);
2219 }
2220
2221 let first = match data.iter().position(|&x| !x.is_nan()) {
2222 Some(idx) => idx,
2223 None => return Err(VoscError::AllValuesNaN),
2224 };
2225
2226 if (data.len() - first) < long_period {
2227 return Err(VoscError::NotEnoughValidData {
2228 needed: long_period,
2229 valid: data.len() - first,
2230 });
2231 }
2232
2233 let chosen = match kern {
2234 Kernel::Auto => Kernel::Scalar,
2235 other => other,
2236 };
2237
2238 unsafe {
2239 match chosen {
2240 Kernel::Scalar | Kernel::ScalarBatch => {
2241 vosc_scalar(data, short_period, long_period, first, dst)
2242 }
2243 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2244 Kernel::Avx2 | Kernel::Avx2Batch => {
2245 vosc_avx2(data, short_period, long_period, first, dst)
2246 }
2247 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2248 Kernel::Avx512 | Kernel::Avx512Batch => {
2249 vosc_avx512(data, short_period, long_period, first, dst)
2250 }
2251 _ => unreachable!(),
2252 }
2253 }
2254
2255 let warm = match first
2256 .checked_add(long_period)
2257 .and_then(|v| v.checked_sub(1))
2258 {
2259 Some(w) => w,
2260 None => {
2261 return Err(VoscError::InvalidPeriod {
2262 period: long_period,
2263 data_len: data.len(),
2264 })
2265 }
2266 };
2267 let limit = warm.min(dst.len());
2268 for v in &mut dst[..limit] {
2269 *v = f64::NAN;
2270 }
2271
2272 Ok(())
2273}
2274
2275#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
2276#[inline]
2277pub fn vosc_into(input: &VoscInput, out: &mut [f64]) -> Result<(), VoscError> {
2278 vosc_into_slice(out, input, Kernel::Auto)
2279}
2280
2281#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2282#[wasm_bindgen]
2283pub fn vosc_js(data: &[f64], short_period: usize, long_period: usize) -> Result<Vec<f64>, JsValue> {
2284 let params = VoscParams {
2285 short_period: Some(short_period),
2286 long_period: Some(long_period),
2287 };
2288 let input = VoscInput::from_slice(data, params);
2289
2290 let mut output = vec![0.0; data.len()];
2291 vosc_into_slice(&mut output, &input, Kernel::Auto)
2292 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2293
2294 Ok(output)
2295}
2296
2297#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2298#[wasm_bindgen]
2299pub fn vosc_into(
2300 in_ptr: *const f64,
2301 out_ptr: *mut f64,
2302 len: usize,
2303 short_period: usize,
2304 long_period: usize,
2305) -> Result<(), JsValue> {
2306 if in_ptr.is_null() || out_ptr.is_null() {
2307 return Err(JsValue::from_str("Null pointer provided"));
2308 }
2309
2310 unsafe {
2311 let data = std::slice::from_raw_parts(in_ptr, len);
2312 let params = VoscParams {
2313 short_period: Some(short_period),
2314 long_period: Some(long_period),
2315 };
2316 let input = VoscInput::from_slice(data, params);
2317
2318 if in_ptr == out_ptr {
2319 let mut temp = vec![0.0; len];
2320 vosc_into_slice(&mut temp, &input, Kernel::Auto)
2321 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2322 let out = std::slice::from_raw_parts_mut(out_ptr, len);
2323 out.copy_from_slice(&temp);
2324 } else {
2325 let out = std::slice::from_raw_parts_mut(out_ptr, len);
2326 vosc_into_slice(out, &input, Kernel::Auto)
2327 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2328 }
2329 Ok(())
2330 }
2331}
2332
2333#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2334#[wasm_bindgen]
2335pub fn vosc_alloc(len: usize) -> *mut f64 {
2336 let mut vec = Vec::<f64>::with_capacity(len);
2337 let ptr = vec.as_mut_ptr();
2338 std::mem::forget(vec);
2339 ptr
2340}
2341
2342#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2343#[wasm_bindgen]
2344pub fn vosc_free(ptr: *mut f64, len: usize) {
2345 if !ptr.is_null() {
2346 unsafe {
2347 let _ = Vec::from_raw_parts(ptr, len, len);
2348 }
2349 }
2350}
2351
2352#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2353#[derive(Serialize, Deserialize)]
2354pub struct VoscBatchConfig {
2355 pub short_period_range: (usize, usize, usize),
2356 pub long_period_range: (usize, usize, usize),
2357}
2358
2359#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2360#[derive(Serialize, Deserialize)]
2361pub struct VoscBatchJsOutput {
2362 pub values: Vec<f64>,
2363 pub combos: Vec<VoscParams>,
2364 pub rows: usize,
2365 pub cols: usize,
2366}
2367
2368#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2369#[wasm_bindgen(js_name = vosc_batch)]
2370pub fn vosc_batch_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
2371 let config: VoscBatchConfig = serde_wasm_bindgen::from_value(config)
2372 .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
2373
2374 let sweep = VoscBatchRange {
2375 short_period: config.short_period_range,
2376 long_period: config.long_period_range,
2377 };
2378
2379 let output = vosc_batch_inner(data, &sweep, detect_best_kernel(), false)
2380 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2381
2382 let js_output = VoscBatchJsOutput {
2383 values: output.values,
2384 combos: output.combos,
2385 rows: output.rows,
2386 cols: output.cols,
2387 };
2388
2389 serde_wasm_bindgen::to_value(&js_output)
2390 .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2391}
2392
2393#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2394#[wasm_bindgen]
2395pub fn vosc_batch_into(
2396 in_ptr: *const f64,
2397 out_ptr: *mut f64,
2398 len: usize,
2399 short_start: usize,
2400 short_end: usize,
2401 short_step: usize,
2402 long_start: usize,
2403 long_end: usize,
2404 long_step: usize,
2405) -> Result<usize, JsValue> {
2406 if in_ptr.is_null() || out_ptr.is_null() {
2407 return Err(JsValue::from_str("null pointer passed to vosc_batch_into"));
2408 }
2409 unsafe {
2410 let data = std::slice::from_raw_parts(in_ptr, len);
2411 let sweep = VoscBatchRange {
2412 short_period: (short_start, short_end, short_step),
2413 long_period: (long_start, long_end, long_step),
2414 };
2415
2416 let combos = expand_grid(&sweep);
2417 let rows = combos.len();
2418 let cols = len;
2419
2420 if rows == 0 {
2421 return Err(JsValue::from_str(
2422 "vosc_batch_into: no valid parameter combinations",
2423 ));
2424 }
2425 let total = rows
2426 .checked_mul(cols)
2427 .ok_or_else(|| JsValue::from_str("vosc_batch_into: rows * cols overflow"))?;
2428
2429 let out = std::slice::from_raw_parts_mut(out_ptr, total);
2430
2431 vosc_batch_inner_into(data, &sweep, detect_best_kernel(), false, out)
2432 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2433
2434 Ok(rows)
2435 }
2436}