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