Skip to main content

vector_ta/indicators/moving_averages/
highpass.rs

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, &params)
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}