Skip to main content

vector_ta/indicators/
minmax.rs

1use crate::utilities::data_loader::{source_type, Candles};
2#[cfg(all(feature = "python", feature = "cuda"))]
3use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
4use crate::utilities::enums::Kernel;
5use crate::utilities::helpers::{
6    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
7    make_uninit_matrix,
8};
9#[cfg(feature = "python")]
10use crate::utilities::kernel_validation::validate_kernel;
11use aligned_vec::{AVec, CACHELINE_ALIGN};
12#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
13use core::arch::x86_64::*;
14#[cfg(all(feature = "python", feature = "cuda"))]
15use cust::context::Context;
16#[cfg(all(feature = "python", feature = "cuda"))]
17use cust::memory::DeviceBuffer;
18#[cfg(all(feature = "python", feature = "cuda"))]
19use numpy::PyUntypedArrayMethods;
20#[cfg(feature = "python")]
21use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
22#[cfg(feature = "python")]
23use pyo3::exceptions::PyValueError;
24#[cfg(feature = "python")]
25use pyo3::prelude::*;
26#[cfg(feature = "python")]
27use pyo3::types::PyDict;
28#[cfg(not(target_arch = "wasm32"))]
29use rayon::prelude::*;
30#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
31use serde::{Deserialize, Serialize};
32use std::error::Error;
33#[cfg(all(feature = "python", feature = "cuda"))]
34use std::sync::Arc;
35use thiserror::Error;
36#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
37use wasm_bindgen::prelude::*;
38
39#[derive(Debug, Clone)]
40pub enum MinmaxData<'a> {
41    Candles {
42        candles: &'a Candles,
43        high_src: &'a str,
44        low_src: &'a str,
45    },
46    Slices {
47        high: &'a [f64],
48        low: &'a [f64],
49    },
50}
51
52#[derive(Debug, Clone)]
53pub struct MinmaxOutput {
54    pub is_min: Vec<f64>,
55    pub is_max: Vec<f64>,
56    pub last_min: Vec<f64>,
57    pub last_max: Vec<f64>,
58}
59
60#[derive(Debug, Clone)]
61#[cfg_attr(
62    all(target_arch = "wasm32", feature = "wasm"),
63    derive(Serialize, Deserialize)
64)]
65pub struct MinmaxParams {
66    pub order: Option<usize>,
67}
68
69impl Default for MinmaxParams {
70    fn default() -> Self {
71        Self { order: Some(3) }
72    }
73}
74
75#[derive(Debug, Clone)]
76pub struct MinmaxInput<'a> {
77    pub data: MinmaxData<'a>,
78    pub params: MinmaxParams,
79}
80
81impl<'a> MinmaxInput<'a> {
82    pub fn from_candles(
83        candles: &'a Candles,
84        high_src: &'a str,
85        low_src: &'a str,
86        params: MinmaxParams,
87    ) -> Self {
88        Self {
89            data: MinmaxData::Candles {
90                candles,
91                high_src,
92                low_src,
93            },
94            params,
95        }
96    }
97    pub fn from_slices(high: &'a [f64], low: &'a [f64], params: MinmaxParams) -> Self {
98        Self {
99            data: MinmaxData::Slices { high, low },
100            params,
101        }
102    }
103    pub fn with_default_candles(candles: &'a Candles) -> Self {
104        Self::from_candles(candles, "high", "low", MinmaxParams::default())
105    }
106    pub fn get_order(&self) -> usize {
107        self.params.order.unwrap_or(3)
108    }
109}
110
111#[derive(Copy, Clone, Debug)]
112pub struct MinmaxBuilder {
113    order: Option<usize>,
114    kernel: Kernel,
115}
116
117impl Default for MinmaxBuilder {
118    fn default() -> Self {
119        Self {
120            order: None,
121            kernel: Kernel::Auto,
122        }
123    }
124}
125
126impl MinmaxBuilder {
127    pub fn new() -> Self {
128        Self::default()
129    }
130    pub fn order(mut self, n: usize) -> Self {
131        self.order = Some(n);
132        self
133    }
134    pub fn kernel(mut self, k: Kernel) -> Self {
135        self.kernel = k;
136        self
137    }
138    pub fn apply(self, candles: &Candles) -> Result<MinmaxOutput, MinmaxError> {
139        let params = MinmaxParams { order: self.order };
140        let input = MinmaxInput::from_candles(candles, "high", "low", params);
141        minmax_with_kernel(&input, self.kernel)
142    }
143    pub fn apply_slices(self, high: &[f64], low: &[f64]) -> Result<MinmaxOutput, MinmaxError> {
144        let params = MinmaxParams { order: self.order };
145        let input = MinmaxInput::from_slices(high, low, params);
146        minmax_with_kernel(&input, self.kernel)
147    }
148    pub fn into_stream(self) -> Result<MinmaxStream, MinmaxError> {
149        let params = MinmaxParams { order: self.order };
150        MinmaxStream::try_new(params)
151    }
152}
153
154#[derive(Debug, Error)]
155pub enum MinmaxError {
156    #[error("minmax: Empty data provided.")]
157    EmptyInputData,
158    #[error("minmax: Invalid order: order = {order}, data length = {data_len}")]
159    InvalidOrder { order: usize, data_len: usize },
160    #[error("minmax: Not enough valid data: needed = {needed}, valid = {valid}")]
161    NotEnoughValidData { needed: usize, valid: usize },
162    #[error("minmax: All values are NaN.")]
163    AllValuesNaN,
164    #[error("minmax: Output length mismatch: expected {expected}, got {got}")]
165    OutputLengthMismatch { expected: usize, got: usize },
166    #[error("minmax: Invalid range: start={start}, end={end}, step={step}")]
167    InvalidRange {
168        start: String,
169        end: String,
170        step: String,
171    },
172    #[error("minmax: Invalid kernel for batch: {0:?}")]
173    InvalidKernelForBatch(Kernel),
174}
175
176#[inline]
177pub fn minmax(input: &MinmaxInput) -> Result<MinmaxOutput, MinmaxError> {
178    minmax_with_kernel(input, Kernel::Auto)
179}
180
181#[inline]
182pub fn minmax_into_slice(
183    is_min_dst: &mut [f64],
184    is_max_dst: &mut [f64],
185    last_min_dst: &mut [f64],
186    last_max_dst: &mut [f64],
187    input: &MinmaxInput,
188    kern: Kernel,
189) -> Result<(), MinmaxError> {
190    let (high, low) = match &input.data {
191        MinmaxData::Candles {
192            candles,
193            high_src,
194            low_src,
195        } => {
196            let h = source_type(candles, high_src);
197            let l = source_type(candles, low_src);
198            (h, l)
199        }
200        MinmaxData::Slices { high, low } => (*high, *low),
201    };
202
203    if high.is_empty() || low.is_empty() {
204        return Err(MinmaxError::EmptyInputData);
205    }
206    if high.len() != low.len() {
207        return Err(MinmaxError::InvalidOrder {
208            order: 0,
209            data_len: high.len().max(low.len()),
210        });
211    }
212
213    let len = high.len();
214    if is_min_dst.len() != len
215        || is_max_dst.len() != len
216        || last_min_dst.len() != len
217        || last_max_dst.len() != len
218    {
219        return Err(MinmaxError::OutputLengthMismatch {
220            expected: len,
221            got: is_min_dst.len(),
222        });
223    }
224
225    let order = input.get_order();
226    if order == 0 || order > len {
227        return Err(MinmaxError::InvalidOrder {
228            order,
229            data_len: len,
230        });
231    }
232
233    let first_valid_idx = high
234        .iter()
235        .zip(low.iter())
236        .position(|(&h, &l)| !(h.is_nan() || l.is_nan()))
237        .ok_or(MinmaxError::AllValuesNaN)?;
238
239    if (len - first_valid_idx) < order {
240        return Err(MinmaxError::NotEnoughValidData {
241            needed: order,
242            valid: len - first_valid_idx,
243        });
244    }
245
246    let qnan = f64::from_bits(0x7ff8_0000_0000_0000);
247    for i in 0..first_valid_idx {
248        is_min_dst[i] = qnan;
249        is_max_dst[i] = qnan;
250        last_min_dst[i] = qnan;
251        last_max_dst[i] = qnan;
252    }
253
254    let chosen = match kern {
255        Kernel::Auto => detect_best_kernel(),
256        other => other,
257    };
258
259    unsafe {
260        match chosen {
261            Kernel::Scalar | Kernel::ScalarBatch => minmax_scalar(
262                high,
263                low,
264                order,
265                first_valid_idx,
266                is_min_dst,
267                is_max_dst,
268                last_min_dst,
269                last_max_dst,
270            ),
271            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
272            Kernel::Avx2 | Kernel::Avx2Batch => minmax_avx2(
273                high,
274                low,
275                order,
276                first_valid_idx,
277                is_min_dst,
278                is_max_dst,
279                last_min_dst,
280                last_max_dst,
281            ),
282            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
283            Kernel::Avx512 | Kernel::Avx512Batch => minmax_avx512(
284                high,
285                low,
286                order,
287                first_valid_idx,
288                is_min_dst,
289                is_max_dst,
290                last_min_dst,
291                last_max_dst,
292            ),
293            _ => minmax_scalar(
294                high,
295                low,
296                order,
297                first_valid_idx,
298                is_min_dst,
299                is_max_dst,
300                last_min_dst,
301                last_max_dst,
302            ),
303        }
304    }
305
306    Ok(())
307}
308
309#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
310#[inline]
311pub fn minmax_into(
312    input: &MinmaxInput,
313    out_is_min: &mut [f64],
314    out_is_max: &mut [f64],
315    out_last_min: &mut [f64],
316    out_last_max: &mut [f64],
317) -> Result<(), MinmaxError> {
318    minmax_into_slice(
319        out_is_min,
320        out_is_max,
321        out_last_min,
322        out_last_max,
323        input,
324        Kernel::Auto,
325    )
326}
327
328pub fn minmax_with_kernel(
329    input: &MinmaxInput,
330    kernel: Kernel,
331) -> Result<MinmaxOutput, MinmaxError> {
332    let (high, low) = match &input.data {
333        MinmaxData::Candles {
334            candles,
335            high_src,
336            low_src,
337        } => {
338            let h = source_type(candles, high_src);
339            let l = source_type(candles, low_src);
340            (h, l)
341        }
342        MinmaxData::Slices { high, low } => (*high, *low),
343    };
344
345    if high.is_empty() || low.is_empty() {
346        return Err(MinmaxError::EmptyInputData);
347    }
348    if high.len() != low.len() {
349        return Err(MinmaxError::InvalidOrder {
350            order: 0,
351            data_len: high.len().max(low.len()),
352        });
353    }
354    let len = high.len();
355    let order = input.get_order();
356    if order == 0 || order > len {
357        return Err(MinmaxError::InvalidOrder {
358            order,
359            data_len: len,
360        });
361    }
362    let first_valid_idx = high
363        .iter()
364        .zip(low.iter())
365        .position(|(&h, &l)| !(h.is_nan() || l.is_nan()))
366        .ok_or(MinmaxError::AllValuesNaN)?;
367
368    if (len - first_valid_idx) < order {
369        return Err(MinmaxError::NotEnoughValidData {
370            needed: order,
371            valid: len - first_valid_idx,
372        });
373    }
374
375    let mut is_min = alloc_with_nan_prefix(len, first_valid_idx);
376    let mut is_max = alloc_with_nan_prefix(len, first_valid_idx);
377    let mut last_min = alloc_with_nan_prefix(len, first_valid_idx);
378    let mut last_max = alloc_with_nan_prefix(len, first_valid_idx);
379
380    let chosen = match kernel {
381        Kernel::Auto => detect_best_kernel(),
382        other => other,
383    };
384
385    unsafe {
386        match chosen {
387            Kernel::Scalar | Kernel::ScalarBatch => minmax_scalar(
388                high,
389                low,
390                order,
391                first_valid_idx,
392                &mut is_min,
393                &mut is_max,
394                &mut last_min,
395                &mut last_max,
396            ),
397            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
398            Kernel::Avx2 | Kernel::Avx2Batch => minmax_avx2(
399                high,
400                low,
401                order,
402                first_valid_idx,
403                &mut is_min,
404                &mut is_max,
405                &mut last_min,
406                &mut last_max,
407            ),
408            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
409            Kernel::Avx512 | Kernel::Avx512Batch => minmax_avx512(
410                high,
411                low,
412                order,
413                first_valid_idx,
414                &mut is_min,
415                &mut is_max,
416                &mut last_min,
417                &mut last_max,
418            ),
419            _ => unreachable!(),
420        }
421    }
422    Ok(MinmaxOutput {
423        is_min,
424        is_max,
425        last_min,
426        last_max,
427    })
428}
429
430#[cfg(all(feature = "python", feature = "cuda"))]
431use crate::cuda::minmax_wrapper::CudaMinmax;
432
433#[cfg(all(feature = "python", feature = "cuda"))]
434#[pyclass(
435    module = "ta_indicators.cuda",
436    name = "MinmaxDeviceArrayF32",
437    unsendable
438)]
439pub struct MinmaxDeviceArrayF32Py {
440    pub(crate) buf: Option<DeviceBuffer<f32>>,
441    pub(crate) rows: usize,
442    pub(crate) cols: usize,
443    pub(crate) ctx: Arc<Context>,
444    pub(crate) device_id: u32,
445}
446
447#[cfg(all(feature = "python", feature = "cuda"))]
448#[pymethods]
449impl MinmaxDeviceArrayF32Py {
450    #[getter]
451    fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
452        let d = PyDict::new(py);
453        d.set_item("shape", (self.rows, self.cols))?;
454        d.set_item("typestr", "<f4")?;
455        let row_stride = self
456            .cols
457            .checked_mul(std::mem::size_of::<f32>())
458            .ok_or_else(|| PyValueError::new_err("stride overflow in __cuda_array_interface__"))?;
459        d.set_item("strides", (row_stride, std::mem::size_of::<f32>()))?;
460        let buf = self
461            .buf
462            .as_ref()
463            .ok_or_else(|| PyValueError::new_err("buffer already exported via __dlpack__"))?;
464        let ptr = buf.as_device_ptr().as_raw() as usize;
465        d.set_item("data", (ptr, false))?;
466
467        d.set_item("version", 3)?;
468        Ok(d)
469    }
470
471    fn __dlpack_device__(&self) -> (i32, i32) {
472        (2, self.device_id as i32)
473    }
474
475    #[pyo3(signature = (stream=None, max_version=None, dl_device=None, copy=None))]
476    fn __dlpack__<'py>(
477        &mut self,
478        py: Python<'py>,
479        stream: Option<pyo3::PyObject>,
480        max_version: Option<(u8, u8)>,
481        dl_device: Option<(i32, i32)>,
482        copy: Option<bool>,
483    ) -> PyResult<PyObject> {
484        let _ = stream;
485        let _ = max_version;
486
487        if let Some((_ty, dev)) = dl_device {
488            if dev != self.device_id as i32 {
489                return Err(PyValueError::new_err("dlpack device mismatch"));
490            }
491        }
492        if matches!(copy, Some(true)) {
493            return Err(PyValueError::new_err(
494                "copy=True not supported for MinmaxDeviceArrayF32",
495            ));
496        }
497
498        let buf = self
499            .buf
500            .take()
501            .ok_or_else(|| PyValueError::new_err("__dlpack__ may only be called once"))?;
502
503        export_f32_cuda_dlpack_2d(py, buf, self.rows, self.cols, self.device_id as i32, None)
504    }
505}
506
507#[cfg(all(feature = "python", feature = "cuda"))]
508#[pyfunction(name = "minmax_cuda_batch_dev")]
509#[pyo3(signature = (high, low, order_range=(3,3,0), device_id=0))]
510pub fn minmax_cuda_batch_dev_py<'py>(
511    py: Python<'py>,
512    high: numpy::PyReadonlyArray1<'py, f32>,
513    low: numpy::PyReadonlyArray1<'py, f32>,
514    order_range: (usize, usize, usize),
515    device_id: usize,
516) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
517    if !crate::cuda::cuda_available() {
518        return Err(PyValueError::new_err("CUDA not available"));
519    }
520    let hs = high.as_slice()?;
521    let ls = low.as_slice()?;
522    let sweep = MinmaxBatchRange { order: order_range };
523    let (quad, combos, ctx, dev_id) = py.allow_threads(|| -> PyResult<_> {
524        let cuda = CudaMinmax::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
525        let (quad, combos) = cuda
526            .minmax_batch_dev(hs, ls, &sweep)
527            .map_err(|e| PyValueError::new_err(e.to_string()))?;
528        Ok((quad, combos, cuda.context_arc(), cuda.device_id()))
529    })?;
530    let dict = pyo3::types::PyDict::new(py);
531    dict.set_item(
532        "is_min",
533        Py::new(
534            py,
535            MinmaxDeviceArrayF32Py {
536                buf: Some(quad.is_min),
537                rows: combos.len(),
538                cols: hs.len(),
539                ctx: ctx.clone(),
540                device_id: dev_id,
541            },
542        )?,
543    )?;
544    dict.set_item(
545        "is_max",
546        Py::new(
547            py,
548            MinmaxDeviceArrayF32Py {
549                buf: Some(quad.is_max),
550                rows: combos.len(),
551                cols: hs.len(),
552                ctx: ctx.clone(),
553                device_id: dev_id,
554            },
555        )?,
556    )?;
557    dict.set_item(
558        "last_min",
559        Py::new(
560            py,
561            MinmaxDeviceArrayF32Py {
562                buf: Some(quad.last_min),
563                rows: combos.len(),
564                cols: hs.len(),
565                ctx: ctx.clone(),
566                device_id: dev_id,
567            },
568        )?,
569    )?;
570    dict.set_item(
571        "last_max",
572        Py::new(
573            py,
574            MinmaxDeviceArrayF32Py {
575                buf: Some(quad.last_max),
576                rows: combos.len(),
577                cols: hs.len(),
578                ctx,
579                device_id: dev_id,
580            },
581        )?,
582    )?;
583    use numpy::IntoPyArray;
584    dict.set_item(
585        "orders",
586        combos
587            .iter()
588            .map(|p| p.order.unwrap() as u64)
589            .collect::<Vec<_>>()
590            .into_pyarray(py),
591    )?;
592    dict.set_item("rows", combos.len())?;
593    dict.set_item("cols", hs.len())?;
594    Ok(dict)
595}
596
597#[cfg(all(feature = "python", feature = "cuda"))]
598#[pyfunction(name = "minmax_cuda_many_series_one_param_dev")]
599#[pyo3(signature = (high_tm, low_tm, order=3, device_id=0))]
600pub fn minmax_cuda_many_series_one_param_dev_py<'py>(
601    py: Python<'py>,
602    high_tm: numpy::PyReadonlyArray2<'py, f32>,
603    low_tm: numpy::PyReadonlyArray2<'py, f32>,
604    order: usize,
605    device_id: usize,
606) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
607    if !crate::cuda::cuda_available() {
608        return Err(PyValueError::new_err("CUDA not available"));
609    }
610    let sh = high_tm.shape();
611    let sl = low_tm.shape();
612    if sh.len() != 2 || sl.len() != 2 || sh != sl {
613        return Err(PyValueError::new_err(
614            "expected 2D arrays with identical shape",
615        ));
616    }
617    let rows = sh[0];
618    let cols = sh[1];
619    let hflat = high_tm.as_slice()?;
620    let lflat = low_tm.as_slice()?;
621    let params = MinmaxParams { order: Some(order) };
622    let (quad, ctx, dev_id) = py.allow_threads(|| -> PyResult<_> {
623        let cuda = CudaMinmax::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
624        let quad = cuda
625            .minmax_many_series_one_param_time_major_dev(hflat, lflat, cols, rows, &params)
626            .map_err(|e| PyValueError::new_err(e.to_string()))?;
627        Ok((quad, cuda.context_arc(), cuda.device_id()))
628    })?;
629    let dict = pyo3::types::PyDict::new(py);
630    dict.set_item(
631        "is_min",
632        Py::new(
633            py,
634            MinmaxDeviceArrayF32Py {
635                buf: Some(quad.is_min),
636                rows,
637                cols,
638                ctx: ctx.clone(),
639                device_id: dev_id,
640            },
641        )?,
642    )?;
643    dict.set_item(
644        "is_max",
645        Py::new(
646            py,
647            MinmaxDeviceArrayF32Py {
648                buf: Some(quad.is_max),
649                rows,
650                cols,
651                ctx: ctx.clone(),
652                device_id: dev_id,
653            },
654        )?,
655    )?;
656    dict.set_item(
657        "last_min",
658        Py::new(
659            py,
660            MinmaxDeviceArrayF32Py {
661                buf: Some(quad.last_min),
662                rows,
663                cols,
664                ctx: ctx.clone(),
665                device_id: dev_id,
666            },
667        )?,
668    )?;
669    dict.set_item(
670        "last_max",
671        Py::new(
672            py,
673            MinmaxDeviceArrayF32Py {
674                buf: Some(quad.last_max),
675                rows,
676                cols,
677                ctx,
678                device_id: dev_id,
679            },
680        )?,
681    )?;
682    dict.set_item("rows", rows)?;
683    dict.set_item("cols", cols)?;
684    dict.set_item("order", order)?;
685    Ok(dict)
686}
687
688#[inline]
689pub fn minmax_scalar(
690    high: &[f64],
691    low: &[f64],
692    order: usize,
693    first_valid_idx: usize,
694    is_min: &mut [f64],
695    is_max: &mut [f64],
696    last_min: &mut [f64],
697    last_max: &mut [f64],
698) {
699    #[inline(always)]
700    fn fmin(a: f64, b: f64) -> f64 {
701        if a < b {
702            a
703        } else {
704            b
705        }
706    }
707    #[inline(always)]
708    fn fmax(a: f64, b: f64) -> f64 {
709        if a > b {
710            a
711        } else {
712            b
713        }
714    }
715
716    let len = high.len();
717
718    for i in 0..first_valid_idx {
719        is_min[i] = f64::NAN;
720        is_max[i] = f64::NAN;
721        last_min[i] = f64::NAN;
722        last_max[i] = f64::NAN;
723    }
724
725    const SMALL_ORDER_THRESHOLD: usize = 8;
726    if order <= SMALL_ORDER_THRESHOLD {
727        let mut last_min_val = f64::NAN;
728        let mut last_max_val = f64::NAN;
729        for i in first_valid_idx..len {
730            let mut min_here = f64::NAN;
731            let mut max_here = f64::NAN;
732
733            if i >= order && i + order < len {
734                unsafe {
735                    let ch = *high.get_unchecked(i);
736                    let cl = *low.get_unchecked(i);
737                    if ch.is_finite() & cl.is_finite() {
738                        let mut less_than_neighbors = true;
739                        let mut greater_than_neighbors = true;
740
741                        let mut o = 1usize;
742                        while o <= order {
743                            let lh = *high.get_unchecked(i - o);
744                            let rh = *high.get_unchecked(i + o);
745                            let ll = *low.get_unchecked(i - o);
746                            let rl = *low.get_unchecked(i + o);
747
748                            if less_than_neighbors {
749                                if !(ll.is_finite() & rl.is_finite()) || !(cl < ll && cl < rl) {
750                                    less_than_neighbors = false;
751                                }
752                            }
753                            if greater_than_neighbors {
754                                if !(lh.is_finite() & rh.is_finite()) || !(ch > lh && ch > rh) {
755                                    greater_than_neighbors = false;
756                                }
757                            }
758
759                            if !less_than_neighbors && !greater_than_neighbors {
760                                break;
761                            }
762                            o += 1;
763                        }
764
765                        if less_than_neighbors {
766                            min_here = cl;
767                        }
768                        if greater_than_neighbors {
769                            max_here = ch;
770                        }
771                    }
772                }
773            }
774
775            is_min[i] = min_here;
776            is_max[i] = max_here;
777
778            if min_here.is_finite() {
779                last_min_val = min_here;
780            }
781            if max_here.is_finite() {
782                last_max_val = max_here;
783            }
784
785            last_min[i] = last_min_val;
786            last_max[i] = last_max_val;
787        }
788        return;
789    }
790
791    let n = len;
792    if first_valid_idx >= n {
793        return;
794    }
795
796    let mut left_min_low = vec![0.0f64; n];
797    let mut right_min_low = vec![0.0f64; n];
798    let mut left_max_high = vec![0.0f64; n];
799    let mut right_max_high = vec![0.0f64; n];
800
801    let mut left_all_low = vec![0u8; n];
802    let mut right_all_low = vec![0u8; n];
803    let mut left_all_high = vec![0u8; n];
804    let mut right_all_high = vec![0u8; n];
805
806    for i in 0..n {
807        unsafe {
808            let l = *low.get_unchecked(i);
809            let h = *high.get_unchecked(i);
810            let lf = l.is_finite() as u8;
811            let hf = h.is_finite() as u8;
812            if i % order == 0 {
813                *left_min_low.get_unchecked_mut(i) = l;
814                *left_max_high.get_unchecked_mut(i) = h;
815                *left_all_low.get_unchecked_mut(i) = lf;
816                *left_all_high.get_unchecked_mut(i) = hf;
817            } else {
818                let p = i - 1;
819                *left_min_low.get_unchecked_mut(i) = fmin(*left_min_low.get_unchecked(p), l);
820                *left_max_high.get_unchecked_mut(i) = fmax(*left_max_high.get_unchecked(p), h);
821                *left_all_low.get_unchecked_mut(i) = *left_all_low.get_unchecked(p) & lf;
822                *left_all_high.get_unchecked_mut(i) = *left_all_high.get_unchecked(p) & hf;
823            }
824        }
825    }
826
827    for i_rev in 0..n {
828        let i = n - 1 - i_rev;
829        unsafe {
830            let l = *low.get_unchecked(i);
831            let h = *high.get_unchecked(i);
832            let lf = l.is_finite() as u8;
833            let hf = h.is_finite() as u8;
834            if ((i + 1) % order) == 0 || i == n - 1 {
835                *right_min_low.get_unchecked_mut(i) = l;
836                *right_max_high.get_unchecked_mut(i) = h;
837                *right_all_low.get_unchecked_mut(i) = lf;
838                *right_all_high.get_unchecked_mut(i) = hf;
839            } else {
840                let n1 = i + 1;
841                *right_min_low.get_unchecked_mut(i) = fmin(*right_min_low.get_unchecked(n1), l);
842                *right_max_high.get_unchecked_mut(i) = fmax(*right_max_high.get_unchecked(n1), h);
843                *right_all_low.get_unchecked_mut(i) = *right_all_low.get_unchecked(n1) & lf;
844                *right_all_high.get_unchecked_mut(i) = *right_all_high.get_unchecked(n1) & hf;
845            }
846        }
847    }
848
849    let mut last_min_val = f64::NAN;
850    let mut last_max_val = f64::NAN;
851    for i in first_valid_idx..n {
852        unsafe {
853            let ch = *high.get_unchecked(i);
854            let cl = *low.get_unchecked(i);
855            let mut min_here = f64::NAN;
856            let mut max_here = f64::NAN;
857
858            if i >= order && i + order < n && ch.is_finite() && cl.is_finite() {
859                let s_l = i - order;
860                let e_l = i - 1;
861                let s_r = i + 1;
862                let e_r = i + order;
863
864                let left_low_ok =
865                    (*right_all_low.get_unchecked(s_l) & *left_all_low.get_unchecked(e_l)) == 1;
866                let right_low_ok =
867                    (*right_all_low.get_unchecked(s_r) & *left_all_low.get_unchecked(e_r)) == 1;
868                let left_high_ok =
869                    (*right_all_high.get_unchecked(s_l) & *left_all_high.get_unchecked(e_l)) == 1;
870                let right_high_ok =
871                    (*right_all_high.get_unchecked(s_r) & *left_all_high.get_unchecked(e_r)) == 1;
872
873                if left_low_ok & right_low_ok {
874                    let lmin = fmin(
875                        *right_min_low.get_unchecked(s_l),
876                        *left_min_low.get_unchecked(e_l),
877                    );
878                    let rmin = fmin(
879                        *right_min_low.get_unchecked(s_r),
880                        *left_min_low.get_unchecked(e_r),
881                    );
882                    if cl < lmin && cl < rmin {
883                        min_here = cl;
884                    }
885                }
886
887                if left_high_ok & right_high_ok {
888                    let lmax = fmax(
889                        *right_max_high.get_unchecked(s_l),
890                        *left_max_high.get_unchecked(e_l),
891                    );
892                    let rmax = fmax(
893                        *right_max_high.get_unchecked(s_r),
894                        *left_max_high.get_unchecked(e_r),
895                    );
896                    if ch > lmax && ch > rmax {
897                        max_here = ch;
898                    }
899                }
900            }
901
902            *is_min.get_unchecked_mut(i) = min_here;
903            *is_max.get_unchecked_mut(i) = max_here;
904
905            if min_here.is_finite() {
906                last_min_val = min_here;
907            }
908            if max_here.is_finite() {
909                last_max_val = max_here;
910            }
911            *last_min.get_unchecked_mut(i) = last_min_val;
912            *last_max.get_unchecked_mut(i) = last_max_val;
913        }
914    }
915}
916
917#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
918#[inline]
919pub unsafe fn minmax_avx2(
920    high: &[f64],
921    low: &[f64],
922    order: usize,
923    first_valid_idx: usize,
924    is_min: &mut [f64],
925    is_max: &mut [f64],
926    last_min: &mut [f64],
927    last_max: &mut [f64],
928) {
929    minmax_scalar(
930        high,
931        low,
932        order,
933        first_valid_idx,
934        is_min,
935        is_max,
936        last_min,
937        last_max,
938    )
939}
940
941#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
942#[inline]
943pub unsafe fn minmax_avx512(
944    high: &[f64],
945    low: &[f64],
946    order: usize,
947    first_valid_idx: usize,
948    is_min: &mut [f64],
949    is_max: &mut [f64],
950    last_min: &mut [f64],
951    last_max: &mut [f64],
952) {
953    if order <= 16 {
954        minmax_avx512_short(
955            high,
956            low,
957            order,
958            first_valid_idx,
959            is_min,
960            is_max,
961            last_min,
962            last_max,
963        )
964    } else {
965        minmax_avx512_long(
966            high,
967            low,
968            order,
969            first_valid_idx,
970            is_min,
971            is_max,
972            last_min,
973            last_max,
974        )
975    }
976}
977
978#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
979#[inline]
980pub unsafe fn minmax_avx512_short(
981    high: &[f64],
982    low: &[f64],
983    order: usize,
984    first_valid_idx: usize,
985    is_min: &mut [f64],
986    is_max: &mut [f64],
987    last_min: &mut [f64],
988    last_max: &mut [f64],
989) {
990    minmax_scalar(
991        high,
992        low,
993        order,
994        first_valid_idx,
995        is_min,
996        is_max,
997        last_min,
998        last_max,
999    )
1000}
1001
1002#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1003#[inline]
1004pub unsafe fn minmax_avx512_long(
1005    high: &[f64],
1006    low: &[f64],
1007    order: usize,
1008    first_valid_idx: usize,
1009    is_min: &mut [f64],
1010    is_max: &mut [f64],
1011    last_min: &mut [f64],
1012    last_max: &mut [f64],
1013) {
1014    minmax_scalar(
1015        high,
1016        low,
1017        order,
1018        first_valid_idx,
1019        is_min,
1020        is_max,
1021        last_min,
1022        last_max,
1023    )
1024}
1025
1026use std::collections::VecDeque;
1027
1028#[derive(Debug, Clone)]
1029pub struct MinmaxStream {
1030    order: usize,
1031    len: usize,
1032    idx: usize,
1033    seen: usize,
1034    filled: bool,
1035
1036    kplus1: usize,
1037    ring_pos: usize,
1038    ring_high: Vec<f64>,
1039    ring_low: Vec<f64>,
1040
1041    rq_min_low: VecDeque<(usize, f64)>,
1042    rq_max_high: VecDeque<(usize, f64)>,
1043
1044    right_flags_pos: usize,
1045    right_low_flags: Vec<u8>,
1046    right_high_flags: Vec<u8>,
1047    right_low_count: usize,
1048    right_high_count: usize,
1049
1050    hist_rmin_low: Vec<f64>,
1051    hist_rmax_high: Vec<f64>,
1052    hist_right_low_count: Vec<usize>,
1053    hist_right_high_count: Vec<usize>,
1054
1055    last_min: f64,
1056    last_max: f64,
1057}
1058
1059impl MinmaxStream {
1060    pub fn try_new(params: MinmaxParams) -> Result<Self, MinmaxError> {
1061        let order = params.order.unwrap_or(3);
1062        if order == 0 {
1063            return Err(MinmaxError::InvalidOrder { order, data_len: 0 });
1064        }
1065        let k = order;
1066        let kplus1 = k + 1;
1067        Ok(Self {
1068            order: k,
1069            len: k * 2 + 1,
1070            idx: 0,
1071            seen: 0,
1072            filled: false,
1073
1074            kplus1,
1075            ring_pos: 0,
1076            ring_high: vec![f64::NAN; kplus1],
1077            ring_low: vec![f64::NAN; kplus1],
1078
1079            rq_min_low: VecDeque::with_capacity(k),
1080            rq_max_high: VecDeque::with_capacity(k),
1081
1082            right_flags_pos: 0,
1083            right_low_flags: vec![0; k],
1084            right_high_flags: vec![0; k],
1085            right_low_count: 0,
1086            right_high_count: 0,
1087
1088            hist_rmin_low: vec![f64::NAN; kplus1],
1089            hist_rmax_high: vec![f64::NAN; kplus1],
1090            hist_right_low_count: vec![0; kplus1],
1091            hist_right_high_count: vec![0; kplus1],
1092
1093            last_min: f64::NAN,
1094            last_max: f64::NAN,
1095        })
1096    }
1097
1098    #[inline(always)]
1099    fn evict_old(&mut self) {
1100        let cutoff = self.idx.saturating_sub(self.order);
1101        while let Some(&(j, _)) = self.rq_min_low.front() {
1102            if j <= cutoff {
1103                self.rq_min_low.pop_front();
1104            } else {
1105                break;
1106            }
1107        }
1108        while let Some(&(j, _)) = self.rq_max_high.front() {
1109            if j <= cutoff {
1110                self.rq_max_high.pop_front();
1111            } else {
1112                break;
1113            }
1114        }
1115    }
1116
1117    #[inline(always)]
1118    fn push_right_low(&mut self, idx: usize, val: f64) {
1119        if val.is_finite() {
1120            while let Some(&(_, v)) = self.rq_min_low.back() {
1121                if v >= val {
1122                    self.rq_min_low.pop_back();
1123                } else {
1124                    break;
1125                }
1126            }
1127            self.rq_min_low.push_back((idx, val));
1128        }
1129    }
1130
1131    #[inline(always)]
1132    fn push_right_high(&mut self, idx: usize, val: f64) {
1133        if val.is_finite() {
1134            while let Some(&(_, v)) = self.rq_max_high.back() {
1135                if v <= val {
1136                    self.rq_max_high.pop_back();
1137                } else {
1138                    break;
1139                }
1140            }
1141            self.rq_max_high.push_back((idx, val));
1142        }
1143    }
1144
1145    #[inline(always)]
1146    fn update_right_counts(&mut self, high: f64, low: f64) {
1147        let pos = self.right_flags_pos;
1148        let old_low = self.right_low_flags[pos] as isize;
1149        let old_high = self.right_high_flags[pos] as isize;
1150        let new_low = low.is_finite() as u8;
1151        let new_high = high.is_finite() as u8;
1152        self.right_low_flags[pos] = new_low;
1153        self.right_high_flags[pos] = new_high;
1154        self.right_low_count =
1155            (self.right_low_count as isize + (new_low as isize - old_low)) as usize;
1156        self.right_high_count =
1157            (self.right_high_count as isize + (new_high as isize - old_high)) as usize;
1158
1159        if self.right_flags_pos + 1 == self.order {
1160            self.right_flags_pos = 0;
1161        } else {
1162            self.right_flags_pos += 1;
1163        }
1164    }
1165
1166    pub fn update(&mut self, high: f64, low: f64) -> (Option<f64>, Option<f64>, f64, f64) {
1167        let k = self.order;
1168        let kp = self.kplus1;
1169        let pos = self.ring_pos;
1170
1171        let left_min_low = self.hist_rmin_low[pos];
1172        let left_max_high = self.hist_rmax_high[pos];
1173        let left_low_count = self.hist_right_low_count[pos];
1174        let left_high_count = self.hist_right_high_count[pos];
1175
1176        self.ring_high[pos] = high;
1177        self.ring_low[pos] = low;
1178
1179        self.evict_old();
1180        self.push_right_low(self.idx, low);
1181        self.push_right_high(self.idx, high);
1182        self.update_right_counts(high, low);
1183
1184        let right_min_low = self.rq_min_low.front().map(|&(_, v)| v).unwrap_or(f64::NAN);
1185        let right_max_high = self
1186            .rq_max_high
1187            .front()
1188            .map(|&(_, v)| v)
1189            .unwrap_or(f64::NAN);
1190
1191        self.hist_rmin_low[pos] = right_min_low;
1192        self.hist_rmax_high[pos] = right_max_high;
1193        self.hist_right_low_count[pos] = self.right_low_count;
1194        self.hist_right_high_count[pos] = self.right_high_count;
1195
1196        self.idx = self.idx.wrapping_add(1);
1197        self.seen = self.seen.saturating_add(1);
1198        if self.ring_pos + 1 == kp {
1199            self.ring_pos = 0;
1200        } else {
1201            self.ring_pos += 1;
1202        }
1203        if !self.filled && self.seen >= self.len {
1204            self.filled = true;
1205        }
1206
1207        if !self.filled {
1208            return (None, None, self.last_min, self.last_max);
1209        }
1210
1211        let cpos = if pos + 1 == kp { 0 } else { pos + 1 };
1212        let ch = self.ring_high[cpos];
1213        let cl = self.ring_low[cpos];
1214
1215        let mut out_min: Option<f64> = None;
1216        let mut out_max: Option<f64> = None;
1217
1218        if ch.is_finite() & cl.is_finite() {
1219            if left_low_count == k
1220                && self.right_low_count == k
1221                && cl < left_min_low
1222                && cl < right_min_low
1223            {
1224                out_min = Some(cl);
1225                self.last_min = cl;
1226            }
1227            if left_high_count == k
1228                && self.right_high_count == k
1229                && ch > left_max_high
1230                && ch > right_max_high
1231            {
1232                out_max = Some(ch);
1233                self.last_max = ch;
1234            }
1235        }
1236
1237        (out_min, out_max, self.last_min, self.last_max)
1238    }
1239}
1240
1241#[derive(Clone, Debug)]
1242pub struct MinmaxBatchRange {
1243    pub order: (usize, usize, usize),
1244}
1245
1246impl Default for MinmaxBatchRange {
1247    fn default() -> Self {
1248        Self { order: (3, 252, 1) }
1249    }
1250}
1251
1252#[derive(Clone, Debug, Default)]
1253pub struct MinmaxBatchBuilder {
1254    range: MinmaxBatchRange,
1255    kernel: Kernel,
1256}
1257
1258impl MinmaxBatchBuilder {
1259    pub fn new() -> Self {
1260        Self::default()
1261    }
1262    pub fn kernel(mut self, k: Kernel) -> Self {
1263        self.kernel = k;
1264        self
1265    }
1266    pub fn order_range(mut self, start: usize, end: usize, step: usize) -> Self {
1267        self.range.order = (start, end, step);
1268        self
1269    }
1270    pub fn order_static(mut self, o: usize) -> Self {
1271        self.range.order = (o, o, 0);
1272        self
1273    }
1274    pub fn apply_slices(self, high: &[f64], low: &[f64]) -> Result<MinmaxBatchOutput, MinmaxError> {
1275        minmax_batch_with_kernel(high, low, &self.range, self.kernel)
1276    }
1277    pub fn with_default_slices(
1278        high: &[f64],
1279        low: &[f64],
1280        k: Kernel,
1281    ) -> Result<MinmaxBatchOutput, MinmaxError> {
1282        MinmaxBatchBuilder::new().kernel(k).apply_slices(high, low)
1283    }
1284    pub fn apply_candles(self, c: &Candles) -> Result<MinmaxBatchOutput, MinmaxError> {
1285        let high = source_type(c, "high");
1286        let low = source_type(c, "low");
1287        self.apply_slices(high, low)
1288    }
1289    pub fn with_default_candles(c: &Candles) -> Result<MinmaxBatchOutput, MinmaxError> {
1290        MinmaxBatchBuilder::new()
1291            .kernel(Kernel::Auto)
1292            .apply_candles(c)
1293    }
1294}
1295
1296pub fn minmax_batch_with_kernel(
1297    high: &[f64],
1298    low: &[f64],
1299    sweep: &MinmaxBatchRange,
1300    k: Kernel,
1301) -> Result<MinmaxBatchOutput, MinmaxError> {
1302    let kernel = match k {
1303        Kernel::Auto => detect_best_batch_kernel(),
1304        other if other.is_batch() => other,
1305        _ => {
1306            return Err(MinmaxError::InvalidKernelForBatch(k));
1307        }
1308    };
1309    let simd = match kernel {
1310        Kernel::Avx512Batch => Kernel::Avx512,
1311        Kernel::Avx2Batch => Kernel::Avx2,
1312        Kernel::ScalarBatch => Kernel::Scalar,
1313        _ => unreachable!(),
1314    };
1315    minmax_batch_par_slice(high, low, sweep, simd)
1316}
1317
1318#[derive(Clone, Debug)]
1319pub struct MinmaxBatchOutput {
1320    pub is_min: Vec<f64>,
1321    pub is_max: Vec<f64>,
1322    pub last_min: Vec<f64>,
1323    pub last_max: Vec<f64>,
1324    pub combos: Vec<MinmaxParams>,
1325    pub rows: usize,
1326    pub cols: usize,
1327}
1328
1329impl MinmaxBatchOutput {
1330    pub fn row_for_params(&self, p: &MinmaxParams) -> Option<usize> {
1331        self.combos
1332            .iter()
1333            .position(|c| c.order.unwrap_or(3) == p.order.unwrap_or(3))
1334    }
1335    pub fn is_min_for(&self, p: &MinmaxParams) -> Option<&[f64]> {
1336        self.row_for_params(p).map(|row| {
1337            let start = row * self.cols;
1338            &self.is_min[start..start + self.cols]
1339        })
1340    }
1341    pub fn is_max_for(&self, p: &MinmaxParams) -> Option<&[f64]> {
1342        self.row_for_params(p).map(|row| {
1343            let start = row * self.cols;
1344            &self.is_max[start..start + self.cols]
1345        })
1346    }
1347    pub fn last_min_for(&self, p: &MinmaxParams) -> Option<&[f64]> {
1348        self.row_for_params(p).map(|row| {
1349            let start = row * self.cols;
1350            &self.last_min[start..start + self.cols]
1351        })
1352    }
1353    pub fn last_max_for(&self, p: &MinmaxParams) -> Option<&[f64]> {
1354        self.row_for_params(p).map(|row| {
1355            let start = row * self.cols;
1356            &self.last_max[start..start + self.cols]
1357        })
1358    }
1359}
1360
1361#[inline(always)]
1362fn expand_grid(r: &MinmaxBatchRange) -> Result<Vec<MinmaxParams>, MinmaxError> {
1363    fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, MinmaxError> {
1364        if step == 0 || start == end {
1365            return Ok(vec![start]);
1366        }
1367        let mut out = Vec::new();
1368        if start < end {
1369            let st = step.max(1);
1370            let mut v = start;
1371            while v <= end {
1372                out.push(v);
1373                match v.checked_add(st) {
1374                    Some(next) => {
1375                        if next == v {
1376                            break;
1377                        }
1378                        v = next;
1379                    }
1380                    None => break,
1381                }
1382            }
1383        } else {
1384            let st = step.max(1) as isize;
1385            let mut v = start as isize;
1386            let end_i = end as isize;
1387            while v >= end_i {
1388                out.push(v as usize);
1389                v -= st;
1390            }
1391        }
1392        if out.is_empty() {
1393            return Err(MinmaxError::InvalidRange {
1394                start: start.to_string(),
1395                end: end.to_string(),
1396                step: step.to_string(),
1397            });
1398        }
1399        Ok(out)
1400    }
1401    let orders = axis_usize(r.order)?;
1402    let mut out = Vec::with_capacity(orders.len());
1403    for &o in &orders {
1404        out.push(MinmaxParams { order: Some(o) });
1405    }
1406    Ok(out)
1407}
1408
1409#[inline(always)]
1410pub fn minmax_batch_slice(
1411    high: &[f64],
1412    low: &[f64],
1413    sweep: &MinmaxBatchRange,
1414    kern: Kernel,
1415) -> Result<MinmaxBatchOutput, MinmaxError> {
1416    minmax_batch_inner(high, low, sweep, kern, false)
1417}
1418
1419#[inline(always)]
1420pub fn minmax_batch_par_slice(
1421    high: &[f64],
1422    low: &[f64],
1423    sweep: &MinmaxBatchRange,
1424    kern: Kernel,
1425) -> Result<MinmaxBatchOutput, MinmaxError> {
1426    minmax_batch_inner(high, low, sweep, kern, true)
1427}
1428
1429#[inline(always)]
1430fn minmax_batch_inner(
1431    high: &[f64],
1432    low: &[f64],
1433    sweep: &MinmaxBatchRange,
1434    kern: Kernel,
1435    parallel: bool,
1436) -> Result<MinmaxBatchOutput, MinmaxError> {
1437    if high.is_empty() || low.is_empty() {
1438        return Err(MinmaxError::EmptyInputData);
1439    }
1440    if high.len() != low.len() {
1441        return Err(MinmaxError::InvalidOrder {
1442            order: 0,
1443            data_len: high.len().max(low.len()),
1444        });
1445    }
1446
1447    let combos = expand_grid(sweep)?;
1448
1449    let len = high.len();
1450    let first = high
1451        .iter()
1452        .zip(low.iter())
1453        .position(|(&h, &l)| !(h.is_nan() || l.is_nan()))
1454        .ok_or(MinmaxError::AllValuesNaN)?;
1455    let max_o = combos.iter().map(|c| c.order.unwrap()).max().unwrap();
1456    if len - first < max_o {
1457        return Err(MinmaxError::NotEnoughValidData {
1458            needed: max_o,
1459            valid: len - first,
1460        });
1461    }
1462
1463    let rows = combos.len();
1464    let cols = len;
1465    let total = rows
1466        .checked_mul(cols)
1467        .ok_or_else(|| MinmaxError::InvalidRange {
1468            start: rows.to_string(),
1469            end: cols.to_string(),
1470            step: "rows*cols overflow".to_string(),
1471        })?;
1472
1473    let mut min_mu = make_uninit_matrix(rows, cols);
1474    let mut max_mu = make_uninit_matrix(rows, cols);
1475    let mut lmin_mu = make_uninit_matrix(rows, cols);
1476    let mut lmax_mu = make_uninit_matrix(rows, cols);
1477
1478    let warm = vec![first; rows];
1479    init_matrix_prefixes(&mut min_mu, cols, &warm);
1480    init_matrix_prefixes(&mut max_mu, cols, &warm);
1481    init_matrix_prefixes(&mut lmin_mu, cols, &warm);
1482    init_matrix_prefixes(&mut lmax_mu, cols, &warm);
1483
1484    let mut min_guard = core::mem::ManuallyDrop::new(min_mu);
1485    let mut max_guard = core::mem::ManuallyDrop::new(max_mu);
1486    let mut lmin_guard = core::mem::ManuallyDrop::new(lmin_mu);
1487    let mut lmax_guard = core::mem::ManuallyDrop::new(lmax_mu);
1488
1489    let is_min: &mut [f64] = unsafe {
1490        core::slice::from_raw_parts_mut(min_guard.as_mut_ptr() as *mut f64, min_guard.len())
1491    };
1492    let is_max: &mut [f64] = unsafe {
1493        core::slice::from_raw_parts_mut(max_guard.as_mut_ptr() as *mut f64, max_guard.len())
1494    };
1495    let last_min: &mut [f64] = unsafe {
1496        core::slice::from_raw_parts_mut(lmin_guard.as_mut_ptr() as *mut f64, lmin_guard.len())
1497    };
1498    let last_max: &mut [f64] = unsafe {
1499        core::slice::from_raw_parts_mut(lmax_guard.as_mut_ptr() as *mut f64, lmax_guard.len())
1500    };
1501
1502    let do_row = |row: usize,
1503                  out_min: &mut [f64],
1504                  out_max: &mut [f64],
1505                  out_lmin: &mut [f64],
1506                  out_lmax: &mut [f64]| unsafe {
1507        let o = combos[row].order.unwrap();
1508        match kern {
1509            Kernel::Scalar | Kernel::ScalarBatch => {
1510                minmax_row_scalar(high, low, first, o, out_min, out_max, out_lmin, out_lmax)
1511            }
1512            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1513            Kernel::Avx2 | Kernel::Avx2Batch => {
1514                minmax_row_avx2(high, low, first, o, out_min, out_max, out_lmin, out_lmax)
1515            }
1516            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1517            Kernel::Avx512 | Kernel::Avx512Batch => {
1518                minmax_row_avx512(high, low, first, o, out_min, out_max, out_lmin, out_lmax)
1519            }
1520            _ => minmax_row_scalar(high, low, first, o, out_min, out_max, out_lmin, out_lmax),
1521        }
1522    };
1523
1524    if parallel {
1525        #[cfg(not(target_arch = "wasm32"))]
1526        {
1527            is_min
1528                .par_chunks_mut(cols)
1529                .zip(is_max.par_chunks_mut(cols))
1530                .zip(
1531                    last_min
1532                        .par_chunks_mut(cols)
1533                        .zip(last_max.par_chunks_mut(cols)),
1534                )
1535                .enumerate()
1536                .for_each(|(row, ((m, x), (lm, lx)))| do_row(row, m, x, lm, lx));
1537        }
1538        #[cfg(target_arch = "wasm32")]
1539        for (row, ((m, x), (lm, lx))) in is_min
1540            .chunks_mut(cols)
1541            .zip(is_max.chunks_mut(cols))
1542            .zip(last_min.chunks_mut(cols).zip(last_max.chunks_mut(cols)))
1543            .enumerate()
1544        {
1545            do_row(row, m, x, lm, lx);
1546        }
1547    } else {
1548        for (row, ((m, x), (lm, lx))) in is_min
1549            .chunks_mut(cols)
1550            .zip(is_max.chunks_mut(cols))
1551            .zip(last_min.chunks_mut(cols).zip(last_max.chunks_mut(cols)))
1552            .enumerate()
1553        {
1554            do_row(row, m, x, lm, lx);
1555        }
1556    }
1557
1558    let is_min = unsafe {
1559        Vec::from_raw_parts(
1560            min_guard.as_mut_ptr() as *mut f64,
1561            min_guard.len(),
1562            min_guard.capacity(),
1563        )
1564    };
1565    let is_max = unsafe {
1566        Vec::from_raw_parts(
1567            max_guard.as_mut_ptr() as *mut f64,
1568            max_guard.len(),
1569            max_guard.capacity(),
1570        )
1571    };
1572    let last_min = unsafe {
1573        Vec::from_raw_parts(
1574            lmin_guard.as_mut_ptr() as *mut f64,
1575            lmin_guard.len(),
1576            lmin_guard.capacity(),
1577        )
1578    };
1579    let last_max = unsafe {
1580        Vec::from_raw_parts(
1581            lmax_guard.as_mut_ptr() as *mut f64,
1582            lmax_guard.len(),
1583            lmax_guard.capacity(),
1584        )
1585    };
1586
1587    Ok(MinmaxBatchOutput {
1588        is_min,
1589        is_max,
1590        last_min,
1591        last_max,
1592        combos,
1593        rows,
1594        cols,
1595    })
1596}
1597
1598#[inline(always)]
1599fn minmax_batch_inner_into(
1600    high: &[f64],
1601    low: &[f64],
1602    sweep: &MinmaxBatchRange,
1603    kern: Kernel,
1604    parallel: bool,
1605    is_min_out: &mut [f64],
1606    is_max_out: &mut [f64],
1607    last_min_out: &mut [f64],
1608    last_max_out: &mut [f64],
1609) -> Result<Vec<MinmaxParams>, MinmaxError> {
1610    if high.is_empty() || low.is_empty() {
1611        return Err(MinmaxError::EmptyInputData);
1612    }
1613    if high.len() != low.len() {
1614        return Err(MinmaxError::InvalidOrder {
1615            order: 0,
1616            data_len: high.len().max(low.len()),
1617        });
1618    }
1619
1620    let combos = expand_grid(sweep)?;
1621
1622    let len = high.len();
1623    let rows = combos.len();
1624    let cols = len;
1625    let total = rows
1626        .checked_mul(cols)
1627        .ok_or_else(|| MinmaxError::InvalidRange {
1628            start: rows.to_string(),
1629            end: cols.to_string(),
1630            step: "rows*cols overflow".to_string(),
1631        })?;
1632
1633    if is_min_out.len() != total
1634        || is_max_out.len() != total
1635        || last_min_out.len() != total
1636        || last_max_out.len() != total
1637    {
1638        return Err(MinmaxError::OutputLengthMismatch {
1639            expected: total,
1640            got: is_min_out.len(),
1641        });
1642    }
1643
1644    let first = high
1645        .iter()
1646        .zip(low.iter())
1647        .position(|(&h, &l)| !(h.is_nan() || l.is_nan()))
1648        .ok_or(MinmaxError::AllValuesNaN)?;
1649    let max_o = combos.iter().map(|c| c.order.unwrap()).max().unwrap();
1650    if len - first < max_o {
1651        return Err(MinmaxError::NotEnoughValidData {
1652            needed: max_o,
1653            valid: len - first,
1654        });
1655    }
1656
1657    let warm = vec![first; rows];
1658    let (min_mu, max_mu, lmin_mu, lmax_mu) = unsafe {
1659        (
1660            core::slice::from_raw_parts_mut(
1661                is_min_out.as_mut_ptr() as *mut std::mem::MaybeUninit<f64>,
1662                total,
1663            ),
1664            core::slice::from_raw_parts_mut(
1665                is_max_out.as_mut_ptr() as *mut std::mem::MaybeUninit<f64>,
1666                total,
1667            ),
1668            core::slice::from_raw_parts_mut(
1669                last_min_out.as_mut_ptr() as *mut std::mem::MaybeUninit<f64>,
1670                total,
1671            ),
1672            core::slice::from_raw_parts_mut(
1673                last_max_out.as_mut_ptr() as *mut std::mem::MaybeUninit<f64>,
1674                total,
1675            ),
1676        )
1677    };
1678    init_matrix_prefixes(min_mu, cols, &warm);
1679    init_matrix_prefixes(max_mu, cols, &warm);
1680    init_matrix_prefixes(lmin_mu, cols, &warm);
1681    init_matrix_prefixes(lmax_mu, cols, &warm);
1682
1683    let do_row = |row: usize,
1684                  out_min: &mut [f64],
1685                  out_max: &mut [f64],
1686                  out_lmin: &mut [f64],
1687                  out_lmax: &mut [f64]| unsafe {
1688        let o = combos[row].order.unwrap();
1689        match kern {
1690            Kernel::Scalar | Kernel::ScalarBatch => {
1691                minmax_row_scalar(high, low, first, o, out_min, out_max, out_lmin, out_lmax)
1692            }
1693            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1694            Kernel::Avx2 | Kernel::Avx2Batch => {
1695                minmax_row_avx2(high, low, first, o, out_min, out_max, out_lmin, out_lmax)
1696            }
1697            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1698            Kernel::Avx512 | Kernel::Avx512Batch => {
1699                minmax_row_avx512(high, low, first, o, out_min, out_max, out_lmin, out_lmax)
1700            }
1701            _ => minmax_row_scalar(high, low, first, o, out_min, out_max, out_lmin, out_lmax),
1702        }
1703    };
1704
1705    if parallel {
1706        #[cfg(not(target_arch = "wasm32"))]
1707        {
1708            is_min_out
1709                .par_chunks_mut(cols)
1710                .zip(is_max_out.par_chunks_mut(cols))
1711                .zip(
1712                    last_min_out
1713                        .par_chunks_mut(cols)
1714                        .zip(last_max_out.par_chunks_mut(cols)),
1715                )
1716                .enumerate()
1717                .for_each(|(row, ((m, x), (lm, lx)))| do_row(row, m, x, lm, lx));
1718        }
1719        #[cfg(target_arch = "wasm32")]
1720        for (row, ((m, x), (lm, lx))) in is_min_out
1721            .chunks_mut(cols)
1722            .zip(is_max_out.chunks_mut(cols))
1723            .zip(
1724                last_min_out
1725                    .chunks_mut(cols)
1726                    .zip(last_max_out.chunks_mut(cols)),
1727            )
1728            .enumerate()
1729        {
1730            do_row(row, m, x, lm, lx);
1731        }
1732    } else {
1733        for (row, ((m, x), (lm, lx))) in is_min_out
1734            .chunks_mut(cols)
1735            .zip(is_max_out.chunks_mut(cols))
1736            .zip(
1737                last_min_out
1738                    .chunks_mut(cols)
1739                    .zip(last_max_out.chunks_mut(cols)),
1740            )
1741            .enumerate()
1742        {
1743            do_row(row, m, x, lm, lx);
1744        }
1745    }
1746
1747    Ok(combos)
1748}
1749
1750#[inline(always)]
1751pub unsafe fn minmax_row_scalar(
1752    high: &[f64],
1753    low: &[f64],
1754    first_valid: usize,
1755    order: usize,
1756    is_min: &mut [f64],
1757    is_max: &mut [f64],
1758    last_min: &mut [f64],
1759    last_max: &mut [f64],
1760) {
1761    minmax_scalar(
1762        high,
1763        low,
1764        order,
1765        first_valid,
1766        is_min,
1767        is_max,
1768        last_min,
1769        last_max,
1770    )
1771}
1772
1773#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1774#[inline(always)]
1775pub unsafe fn minmax_row_avx2(
1776    high: &[f64],
1777    low: &[f64],
1778    first_valid: usize,
1779    order: usize,
1780    is_min: &mut [f64],
1781    is_max: &mut [f64],
1782    last_min: &mut [f64],
1783    last_max: &mut [f64],
1784) {
1785    minmax_row_scalar(
1786        high,
1787        low,
1788        first_valid,
1789        order,
1790        is_min,
1791        is_max,
1792        last_min,
1793        last_max,
1794    )
1795}
1796
1797#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1798#[inline(always)]
1799pub unsafe fn minmax_row_avx512(
1800    high: &[f64],
1801    low: &[f64],
1802    first_valid: usize,
1803    order: usize,
1804    is_min: &mut [f64],
1805    is_max: &mut [f64],
1806    last_min: &mut [f64],
1807    last_max: &mut [f64],
1808) {
1809    if order <= 16 {
1810        minmax_row_avx512_short(
1811            high,
1812            low,
1813            first_valid,
1814            order,
1815            is_min,
1816            is_max,
1817            last_min,
1818            last_max,
1819        )
1820    } else {
1821        minmax_row_avx512_long(
1822            high,
1823            low,
1824            first_valid,
1825            order,
1826            is_min,
1827            is_max,
1828            last_min,
1829            last_max,
1830        )
1831    }
1832}
1833
1834#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1835#[inline(always)]
1836pub unsafe fn minmax_row_avx512_short(
1837    high: &[f64],
1838    low: &[f64],
1839    first_valid: usize,
1840    order: usize,
1841    is_min: &mut [f64],
1842    is_max: &mut [f64],
1843    last_min: &mut [f64],
1844    last_max: &mut [f64],
1845) {
1846    minmax_row_scalar(
1847        high,
1848        low,
1849        first_valid,
1850        order,
1851        is_min,
1852        is_max,
1853        last_min,
1854        last_max,
1855    )
1856}
1857
1858#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1859#[inline(always)]
1860pub unsafe fn minmax_row_avx512_long(
1861    high: &[f64],
1862    low: &[f64],
1863    first_valid: usize,
1864    order: usize,
1865    is_min: &mut [f64],
1866    is_max: &mut [f64],
1867    last_min: &mut [f64],
1868    last_max: &mut [f64],
1869) {
1870    minmax_row_scalar(
1871        high,
1872        low,
1873        first_valid,
1874        order,
1875        is_min,
1876        is_max,
1877        last_min,
1878        last_max,
1879    )
1880}
1881
1882#[cfg(feature = "python")]
1883#[pyfunction(name = "minmax")]
1884#[pyo3(signature = (high, low, order, kernel=None))]
1885pub fn minmax_py<'py>(
1886    py: Python<'py>,
1887    high: numpy::PyReadonlyArray1<'py, f64>,
1888    low: numpy::PyReadonlyArray1<'py, f64>,
1889    order: usize,
1890    kernel: Option<&str>,
1891) -> PyResult<(
1892    Bound<'py, numpy::PyArray1<f64>>,
1893    Bound<'py, numpy::PyArray1<f64>>,
1894    Bound<'py, numpy::PyArray1<f64>>,
1895    Bound<'py, numpy::PyArray1<f64>>,
1896)> {
1897    use numpy::{IntoPyArray, PyArrayMethods};
1898
1899    let high_slice = high.as_slice()?;
1900    let low_slice = low.as_slice()?;
1901    let kern = validate_kernel(kernel, false)?;
1902
1903    let params = MinmaxParams { order: Some(order) };
1904    let input = MinmaxInput::from_slices(high_slice, low_slice, params);
1905
1906    let output = py
1907        .allow_threads(|| minmax_with_kernel(&input, kern))
1908        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1909
1910    Ok((
1911        output.is_min.into_pyarray(py),
1912        output.is_max.into_pyarray(py),
1913        output.last_min.into_pyarray(py),
1914        output.last_max.into_pyarray(py),
1915    ))
1916}
1917
1918#[cfg(feature = "python")]
1919#[pyclass(name = "MinmaxStream")]
1920pub struct MinmaxStreamPy {
1921    stream: MinmaxStream,
1922}
1923
1924#[cfg(feature = "python")]
1925#[pymethods]
1926impl MinmaxStreamPy {
1927    #[new]
1928    fn new(order: usize) -> PyResult<Self> {
1929        let params = MinmaxParams { order: Some(order) };
1930        let stream =
1931            MinmaxStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
1932        Ok(MinmaxStreamPy { stream })
1933    }
1934
1935    fn update(&mut self, high: f64, low: f64) -> (Option<f64>, Option<f64>, f64, f64) {
1936        self.stream.update(high, low)
1937    }
1938}
1939
1940#[cfg(feature = "python")]
1941#[pyfunction(name = "minmax_batch")]
1942#[pyo3(signature = (high, low, order_range, kernel=None))]
1943pub fn minmax_batch_py<'py>(
1944    py: Python<'py>,
1945    high: numpy::PyReadonlyArray1<'py, f64>,
1946    low: numpy::PyReadonlyArray1<'py, f64>,
1947    order_range: (usize, usize, usize),
1948    kernel: Option<&str>,
1949) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
1950    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
1951    use pyo3::types::PyDict;
1952
1953    let high_slice = high.as_slice()?;
1954    let low_slice = low.as_slice()?;
1955    let kern = validate_kernel(kernel, true)?;
1956
1957    let sweep = MinmaxBatchRange { order: order_range };
1958    let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
1959    let rows = combos.len();
1960    let cols = high_slice.len();
1961    let total = rows
1962        .checked_mul(cols)
1963        .ok_or_else(|| PyValueError::new_err("rows*cols overflow in minmax_batch_py"))?;
1964
1965    let is_min_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1966    let is_max_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1967    let last_min_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1968    let last_max_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1969
1970    let is_min_slice = unsafe { is_min_arr.as_slice_mut()? };
1971    let is_max_slice = unsafe { is_max_arr.as_slice_mut()? };
1972    let last_min_slice = unsafe { last_min_arr.as_slice_mut()? };
1973    let last_max_slice = unsafe { last_max_arr.as_slice_mut()? };
1974
1975    let combos = py
1976        .allow_threads(|| {
1977            let kernel = match kern {
1978                Kernel::Auto => detect_best_batch_kernel(),
1979                k => k,
1980            };
1981
1982            let simd = match kernel {
1983                Kernel::Avx512Batch => Kernel::Avx512,
1984                Kernel::Avx2Batch => Kernel::Avx2,
1985                Kernel::ScalarBatch => Kernel::Scalar,
1986                _ => kernel,
1987            };
1988
1989            minmax_batch_inner_into(
1990                high_slice,
1991                low_slice,
1992                &sweep,
1993                simd,
1994                true,
1995                is_min_slice,
1996                is_max_slice,
1997                last_min_slice,
1998                last_max_slice,
1999            )
2000        })
2001        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2002
2003    let dict = PyDict::new(py);
2004    dict.set_item("is_min", is_min_arr.reshape((rows, cols))?)?;
2005    dict.set_item("is_max", is_max_arr.reshape((rows, cols))?)?;
2006    dict.set_item("last_min", last_min_arr.reshape((rows, cols))?)?;
2007    dict.set_item("last_max", last_max_arr.reshape((rows, cols))?)?;
2008    dict.set_item(
2009        "orders",
2010        combos
2011            .iter()
2012            .map(|p| p.order.unwrap() as u64)
2013            .collect::<Vec<_>>()
2014            .into_pyarray(py),
2015    )?;
2016
2017    Ok(dict)
2018}
2019
2020#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2021#[derive(Serialize, Deserialize)]
2022pub struct MinmaxResult {
2023    pub values: Vec<f64>,
2024    pub rows: usize,
2025    pub cols: usize,
2026}
2027
2028#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2029#[wasm_bindgen]
2030pub fn minmax_js(high: &[f64], low: &[f64], order: usize) -> Result<JsValue, JsValue> {
2031    let input = MinmaxInput::from_slices(high, low, MinmaxParams { order: Some(order) });
2032
2033    let out =
2034        minmax_with_kernel(&input, Kernel::Auto).map_err(|e| JsValue::from_str(&e.to_string()))?;
2035
2036    let len = high.len();
2037    let mut values = Vec::with_capacity(4 * len);
2038    values.extend_from_slice(&out.is_min);
2039    values.extend_from_slice(&out.is_max);
2040    values.extend_from_slice(&out.last_min);
2041    values.extend_from_slice(&out.last_max);
2042
2043    let result = MinmaxResult {
2044        values,
2045        rows: 4,
2046        cols: len,
2047    };
2048    serde_wasm_bindgen::to_value(&result).map_err(|e| JsValue::from_str(&e.to_string()))
2049}
2050
2051#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2052#[wasm_bindgen]
2053pub fn minmax_alloc(len: usize) -> *mut f64 {
2054    let mut vec = Vec::<f64>::with_capacity(len);
2055    let ptr = vec.as_mut_ptr();
2056    std::mem::forget(vec);
2057    ptr
2058}
2059
2060#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2061#[wasm_bindgen]
2062pub fn minmax_free(ptr: *mut f64, len: usize) {
2063    if !ptr.is_null() {
2064        unsafe {
2065            let _ = Vec::from_raw_parts(ptr, len, len);
2066        }
2067    }
2068}
2069
2070#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2071#[wasm_bindgen]
2072pub fn minmax_into(
2073    high_ptr: *const f64,
2074    low_ptr: *const f64,
2075    is_min_ptr: *mut f64,
2076    is_max_ptr: *mut f64,
2077    last_min_ptr: *mut f64,
2078    last_max_ptr: *mut f64,
2079    len: usize,
2080    order: usize,
2081) -> Result<(), JsValue> {
2082    if high_ptr.is_null()
2083        || low_ptr.is_null()
2084        || is_min_ptr.is_null()
2085        || is_max_ptr.is_null()
2086        || last_min_ptr.is_null()
2087        || last_max_ptr.is_null()
2088    {
2089        return Err(JsValue::from_str("null pointer passed to minmax_into"));
2090    }
2091
2092    unsafe {
2093        let high = std::slice::from_raw_parts(high_ptr, len);
2094        let low = std::slice::from_raw_parts(low_ptr, len);
2095
2096        if order == 0 || order > len {
2097            return Err(JsValue::from_str("Invalid order"));
2098        }
2099
2100        let params = MinmaxParams { order: Some(order) };
2101        let input = MinmaxInput::from_slices(high, low, params);
2102
2103        let input_ptrs = [high_ptr as *const u8, low_ptr as *const u8];
2104        let output_ptrs = [
2105            is_min_ptr as *mut u8,
2106            is_max_ptr as *mut u8,
2107            last_min_ptr as *mut u8,
2108            last_max_ptr as *mut u8,
2109        ];
2110
2111        let mut needs_temp = false;
2112        for &inp in &input_ptrs {
2113            for &out in &output_ptrs {
2114                if inp == out {
2115                    needs_temp = true;
2116                    break;
2117                }
2118            }
2119            if needs_temp {
2120                break;
2121            }
2122        }
2123
2124        if needs_temp {
2125            let mut temp_is_min = vec![0.0; len];
2126            let mut temp_is_max = vec![0.0; len];
2127            let mut temp_last_min = vec![0.0; len];
2128            let mut temp_last_max = vec![0.0; len];
2129
2130            minmax_into_slice(
2131                &mut temp_is_min,
2132                &mut temp_is_max,
2133                &mut temp_last_min,
2134                &mut temp_last_max,
2135                &input,
2136                Kernel::Auto,
2137            )
2138            .map_err(|e| JsValue::from_str(&e.to_string()))?;
2139
2140            let is_min_out = std::slice::from_raw_parts_mut(is_min_ptr, len);
2141            let is_max_out = std::slice::from_raw_parts_mut(is_max_ptr, len);
2142            let last_min_out = std::slice::from_raw_parts_mut(last_min_ptr, len);
2143            let last_max_out = std::slice::from_raw_parts_mut(last_max_ptr, len);
2144
2145            is_min_out.copy_from_slice(&temp_is_min);
2146            is_max_out.copy_from_slice(&temp_is_max);
2147            last_min_out.copy_from_slice(&temp_last_min);
2148            last_max_out.copy_from_slice(&temp_last_max);
2149        } else {
2150            let is_min_out = std::slice::from_raw_parts_mut(is_min_ptr, len);
2151            let is_max_out = std::slice::from_raw_parts_mut(is_max_ptr, len);
2152            let last_min_out = std::slice::from_raw_parts_mut(last_min_ptr, len);
2153            let last_max_out = std::slice::from_raw_parts_mut(last_max_ptr, len);
2154
2155            minmax_into_slice(
2156                is_min_out,
2157                is_max_out,
2158                last_min_out,
2159                last_max_out,
2160                &input,
2161                Kernel::Auto,
2162            )
2163            .map_err(|e| JsValue::from_str(&e.to_string()))?;
2164        }
2165
2166        Ok(())
2167    }
2168}
2169
2170#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2171#[derive(Serialize, Deserialize)]
2172pub struct MinmaxBatchConfig {
2173    pub order_range: (usize, usize, usize),
2174}
2175
2176#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2177#[derive(Serialize, Deserialize)]
2178pub struct MinmaxBatchJsOutput {
2179    pub values: Vec<f64>,
2180    pub combos: Vec<MinmaxParams>,
2181    pub rows: usize,
2182    pub cols: usize,
2183}
2184
2185#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2186#[wasm_bindgen(js_name = minmax_batch)]
2187pub fn minmax_batch_unified_js(
2188    high: &[f64],
2189    low: &[f64],
2190    config: JsValue,
2191) -> Result<JsValue, JsValue> {
2192    let cfg: MinmaxBatchConfig = serde_wasm_bindgen::from_value(config)
2193        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
2194
2195    let sweep = MinmaxBatchRange {
2196        order: cfg.order_range,
2197    };
2198    let out = minmax_batch_with_kernel(high, low, &sweep, Kernel::Auto)
2199        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2200
2201    let rows = out.rows;
2202    let cols = out.cols;
2203
2204    let total = rows
2205        .checked_mul(cols)
2206        .ok_or_else(|| JsValue::from_str("rows*cols overflow in minmax_batch_unified_js"))?;
2207    let cap = total
2208        .checked_mul(4)
2209        .ok_or_else(|| JsValue::from_str("capacity overflow in minmax_batch_unified_js"))?;
2210    let mut values = Vec::with_capacity(cap);
2211
2212    for series in 0..4 {
2213        for r in 0..rows {
2214            let (src, start) = match series {
2215                0 => (&out.is_min, r * cols),
2216                1 => (&out.is_max, r * cols),
2217                2 => (&out.last_min, r * cols),
2218                _ => (&out.last_max, r * cols),
2219            };
2220            values.extend_from_slice(&src[start..start + cols]);
2221        }
2222    }
2223
2224    let js_out = MinmaxBatchJsOutput {
2225        values,
2226        combos: out.combos,
2227        rows: 4 * rows,
2228        cols,
2229    };
2230    serde_wasm_bindgen::to_value(&js_out).map_err(|e| JsValue::from_str(&e.to_string()))
2231}
2232
2233#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2234#[wasm_bindgen]
2235pub fn minmax_batch_into(
2236    high_ptr: *const f64,
2237    low_ptr: *const f64,
2238    is_min_ptr: *mut f64,
2239    is_max_ptr: *mut f64,
2240    last_min_ptr: *mut f64,
2241    last_max_ptr: *mut f64,
2242    len: usize,
2243    order_start: usize,
2244    order_end: usize,
2245    order_step: usize,
2246) -> Result<usize, JsValue> {
2247    if high_ptr.is_null()
2248        || low_ptr.is_null()
2249        || is_min_ptr.is_null()
2250        || is_max_ptr.is_null()
2251        || last_min_ptr.is_null()
2252        || last_max_ptr.is_null()
2253    {
2254        return Err(JsValue::from_str(
2255            "null pointer passed to minmax_batch_into",
2256        ));
2257    }
2258
2259    unsafe {
2260        let high = std::slice::from_raw_parts(high_ptr, len);
2261        let low = std::slice::from_raw_parts(low_ptr, len);
2262
2263        let sweep = MinmaxBatchRange {
2264            order: (order_start, order_end, order_step),
2265        };
2266
2267        let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2268        let rows = combos.len();
2269        let cols = len;
2270        let total = rows
2271            .checked_mul(cols)
2272            .ok_or_else(|| JsValue::from_str("rows*cols overflow in minmax_batch_into"))?;
2273
2274        let is_min_out = std::slice::from_raw_parts_mut(is_min_ptr, total);
2275        let is_max_out = std::slice::from_raw_parts_mut(is_max_ptr, total);
2276        let last_min_out = std::slice::from_raw_parts_mut(last_min_ptr, total);
2277        let last_max_out = std::slice::from_raw_parts_mut(last_max_ptr, total);
2278
2279        minmax_batch_inner_into(
2280            high,
2281            low,
2282            &sweep,
2283            Kernel::Auto,
2284            false,
2285            is_min_out,
2286            is_max_out,
2287            last_min_out,
2288            last_max_out,
2289        )
2290        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2291
2292        Ok(rows)
2293    }
2294}
2295
2296#[cfg(test)]
2297mod tests {
2298    use super::*;
2299    use crate::skip_if_unsupported;
2300    use crate::utilities::data_loader::read_candles_from_csv;
2301
2302    fn check_minmax_partial_params(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2303        skip_if_unsupported!(kernel, test_name);
2304        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2305        let candles = read_candles_from_csv(file_path)?;
2306        let params = MinmaxParams { order: None };
2307        let input = MinmaxInput::from_candles(&candles, "high", "low", params);
2308        let output = minmax_with_kernel(&input, kernel)?;
2309        assert_eq!(output.is_min.len(), candles.close.len());
2310        Ok(())
2311    }
2312
2313    fn check_minmax_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2314        skip_if_unsupported!(kernel, test_name);
2315        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2316        let candles = read_candles_from_csv(file_path)?;
2317        let params = MinmaxParams { order: Some(3) };
2318        let input = MinmaxInput::from_candles(&candles, "high", "low", params);
2319        let output = minmax_with_kernel(&input, kernel)?;
2320        assert_eq!(output.is_min.len(), candles.close.len());
2321        let count = output.is_min.len();
2322        assert!(count >= 5, "Not enough data to check last 5");
2323        let start_index = count - 5;
2324        for &val in &output.is_min[start_index..] {
2325            assert!(val.is_nan());
2326        }
2327        for &val in &output.is_max[start_index..] {
2328            assert!(val.is_nan());
2329        }
2330        let expected_last_five_min = [57876.0, 57876.0, 57876.0, 57876.0, 57876.0];
2331        let last_min_slice = &output.last_min[start_index..];
2332        for (i, &val) in last_min_slice.iter().enumerate() {
2333            let expected_val = expected_last_five_min[i];
2334            assert!(
2335                (val - expected_val).abs() < 1e-1,
2336                "Minmax last_min mismatch at idx {}: {} vs {}",
2337                i,
2338                val,
2339                expected_val
2340            );
2341        }
2342        let expected_last_five_max = [60102.0, 60102.0, 60102.0, 60102.0, 60102.0];
2343        let last_max_slice = &output.last_max[start_index..];
2344        for (i, &val) in last_max_slice.iter().enumerate() {
2345            let expected_val = expected_last_five_max[i];
2346            assert!(
2347                (val - expected_val).abs() < 1e-1,
2348                "Minmax last_max mismatch at idx {}: {} vs {}",
2349                i,
2350                val,
2351                expected_val
2352            );
2353        }
2354        Ok(())
2355    }
2356
2357    fn check_minmax_zero_order(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2358        skip_if_unsupported!(kernel, test_name);
2359        let high = [10.0, 20.0, 30.0];
2360        let low = [1.0, 2.0, 3.0];
2361        let params = MinmaxParams { order: Some(0) };
2362        let input = MinmaxInput::from_slices(&high, &low, params);
2363        let res = minmax_with_kernel(&input, kernel);
2364        assert!(
2365            res.is_err(),
2366            "[{}] Minmax should fail with zero order",
2367            test_name
2368        );
2369        Ok(())
2370    }
2371
2372    fn check_minmax_order_exceeds_length(
2373        test_name: &str,
2374        kernel: Kernel,
2375    ) -> Result<(), Box<dyn Error>> {
2376        skip_if_unsupported!(kernel, test_name);
2377        let high = [10.0, 20.0, 30.0];
2378        let low = [1.0, 2.0, 3.0];
2379        let params = MinmaxParams { order: Some(10) };
2380        let input = MinmaxInput::from_slices(&high, &low, params);
2381        let res = minmax_with_kernel(&input, kernel);
2382        assert!(
2383            res.is_err(),
2384            "[{}] Minmax should fail with order > length",
2385            test_name
2386        );
2387        Ok(())
2388    }
2389
2390    fn check_minmax_nan_handling(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2391        skip_if_unsupported!(kernel, test_name);
2392        let high = [f64::NAN, f64::NAN, f64::NAN];
2393        let low = [f64::NAN, f64::NAN, f64::NAN];
2394        let params = MinmaxParams { order: Some(1) };
2395        let input = MinmaxInput::from_slices(&high, &low, params);
2396        let res = minmax_with_kernel(&input, kernel);
2397        assert!(
2398            res.is_err(),
2399            "[{}] Minmax should fail with all NaN data",
2400            test_name
2401        );
2402        Ok(())
2403    }
2404
2405    fn check_minmax_very_small_dataset(
2406        test_name: &str,
2407        kernel: Kernel,
2408    ) -> Result<(), Box<dyn Error>> {
2409        skip_if_unsupported!(kernel, test_name);
2410        let high = [f64::NAN, 10.0];
2411        let low = [f64::NAN, 5.0];
2412        let params = MinmaxParams { order: Some(3) };
2413        let input = MinmaxInput::from_slices(&high, &low, params);
2414        let res = minmax_with_kernel(&input, kernel);
2415        assert!(
2416            res.is_err(),
2417            "[{}] Minmax should fail with not enough valid data",
2418            test_name
2419        );
2420        Ok(())
2421    }
2422
2423    fn check_minmax_basic_slices(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2424        skip_if_unsupported!(kernel, test_name);
2425        let high = [50.0, 55.0, 60.0, 55.0, 50.0, 45.0, 50.0, 55.0];
2426        let low = [40.0, 38.0, 35.0, 38.0, 40.0, 42.0, 41.0, 39.0];
2427        let params = MinmaxParams { order: Some(2) };
2428        let input = MinmaxInput::from_slices(&high, &low, params);
2429        let output = minmax_with_kernel(&input, kernel)?;
2430        assert_eq!(output.is_min.len(), 8);
2431        assert_eq!(output.is_max.len(), 8);
2432        assert_eq!(output.last_min.len(), 8);
2433        assert_eq!(output.last_max.len(), 8);
2434        Ok(())
2435    }
2436
2437    #[cfg(debug_assertions)]
2438    fn check_minmax_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2439        skip_if_unsupported!(kernel, test_name);
2440
2441        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2442        let candles = read_candles_from_csv(file_path)?;
2443
2444        let test_params = vec![
2445            MinmaxParams::default(),
2446            MinmaxParams { order: Some(1) },
2447            MinmaxParams { order: Some(2) },
2448            MinmaxParams { order: Some(5) },
2449            MinmaxParams { order: Some(10) },
2450            MinmaxParams { order: Some(20) },
2451            MinmaxParams { order: Some(50) },
2452            MinmaxParams { order: Some(100) },
2453        ];
2454
2455        for (param_idx, params) in test_params.iter().enumerate() {
2456            let input = MinmaxInput::from_candles(&candles, "high", "low", params.clone());
2457            let output = minmax_with_kernel(&input, kernel)?;
2458
2459            let arrays = [
2460                (&output.is_min, "is_min"),
2461                (&output.is_max, "is_max"),
2462                (&output.last_min, "last_min"),
2463                (&output.last_max, "last_max"),
2464            ];
2465
2466            for (array, array_name) in arrays.iter() {
2467                for (i, &val) in array.iter().enumerate() {
2468                    if val.is_nan() {
2469                        continue;
2470                    }
2471
2472                    let bits = val.to_bits();
2473
2474                    if bits == 0x11111111_11111111 {
2475                        panic!(
2476							"[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
2477							 in {} with params: order={} (param set {})",
2478							test_name, val, bits, i, array_name,
2479							params.order.unwrap_or(3), param_idx
2480						);
2481                    }
2482
2483                    if bits == 0x22222222_22222222 {
2484                        panic!(
2485							"[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
2486							 in {} with params: order={} (param set {})",
2487							test_name, val, bits, i, array_name,
2488							params.order.unwrap_or(3), param_idx
2489						);
2490                    }
2491
2492                    if bits == 0x33333333_33333333 {
2493                        panic!(
2494							"[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
2495							 in {} with params: order={} (param set {})",
2496							test_name, val, bits, i, array_name,
2497							params.order.unwrap_or(3), param_idx
2498						);
2499                    }
2500                }
2501            }
2502        }
2503
2504        Ok(())
2505    }
2506
2507    #[cfg(not(debug_assertions))]
2508    fn check_minmax_no_poison(_test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
2509        Ok(())
2510    }
2511
2512    macro_rules! generate_all_minmax_tests {
2513        ($($test_fn:ident),*) => {
2514            paste::paste! {
2515                $(
2516                    #[test]
2517                    fn [<$test_fn _scalar_f64>]() {
2518                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
2519                    }
2520                )*
2521                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2522                $(
2523                    #[test]
2524                    fn [<$test_fn _avx2_f64>]() {
2525                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
2526                    }
2527                    #[test]
2528                    fn [<$test_fn _avx512_f64>]() {
2529                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
2530                    }
2531                )*
2532            }
2533        }
2534    }
2535
2536    generate_all_minmax_tests!(
2537        check_minmax_partial_params,
2538        check_minmax_accuracy,
2539        check_minmax_zero_order,
2540        check_minmax_order_exceeds_length,
2541        check_minmax_nan_handling,
2542        check_minmax_very_small_dataset,
2543        check_minmax_basic_slices,
2544        check_minmax_no_poison
2545    );
2546
2547    #[cfg(feature = "proptest")]
2548    generate_all_minmax_tests!(check_minmax_property);
2549
2550    fn check_batch_default_row(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2551        skip_if_unsupported!(kernel, test);
2552        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2553        let c = read_candles_from_csv(file)?;
2554        let output = MinmaxBatchBuilder::new().kernel(kernel).apply_candles(&c)?;
2555        let def = MinmaxParams::default();
2556        let row = output.is_min_for(&def).expect("default row missing");
2557        assert_eq!(row.len(), c.close.len());
2558        Ok(())
2559    }
2560
2561    #[test]
2562    fn test_minmax_into_matches_api() {
2563        let mut high = Vec::with_capacity(256);
2564        let mut low = Vec::with_capacity(256);
2565
2566        for _ in 0..5 {
2567            high.push(f64::NAN);
2568            low.push(f64::NAN);
2569        }
2570
2571        for i in 0..200usize {
2572            let t = i as f64;
2573            high.push(100.0 + (t / 5.0).sin() * 10.0 + (t / 7.0).cos() * 3.0);
2574            low.push(90.0 - (t / 6.0).sin() * 9.0 - (t / 8.0).cos() * 2.0);
2575        }
2576
2577        for j in 0..51usize {
2578            let t = j as f64;
2579            high.push(105.0 + (t * 0.01).sin());
2580            low.push(95.0 - (t * 0.01).cos());
2581        }
2582
2583        let params = MinmaxParams::default();
2584        let input = MinmaxInput::from_slices(&high, &low, params);
2585
2586        let baseline = minmax(&input).expect("baseline minmax() should succeed");
2587
2588        let n = high.len();
2589        let mut is_min = vec![0.0; n];
2590        let mut is_max = vec![0.0; n];
2591        let mut last_min = vec![0.0; n];
2592        let mut last_max = vec![0.0; n];
2593
2594        #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
2595        {
2596            minmax_into(
2597                &input,
2598                &mut is_min,
2599                &mut is_max,
2600                &mut last_min,
2601                &mut last_max,
2602            )
2603            .expect("minmax_into should succeed");
2604
2605            assert_eq!(is_min.len(), baseline.is_min.len());
2606            assert_eq!(is_max.len(), baseline.is_max.len());
2607            assert_eq!(last_min.len(), baseline.last_min.len());
2608            assert_eq!(last_max.len(), baseline.last_max.len());
2609
2610            fn eq_or_both_nan(a: f64, b: f64) -> bool {
2611                (a.is_nan() && b.is_nan()) || (a == b)
2612            }
2613
2614            for i in 0..n {
2615                assert!(
2616                    eq_or_both_nan(is_min[i], baseline.is_min[i]),
2617                    "is_min mismatch at {}: {:?} vs {:?}",
2618                    i,
2619                    is_min[i],
2620                    baseline.is_min[i]
2621                );
2622                assert!(
2623                    eq_or_both_nan(is_max[i], baseline.is_max[i]),
2624                    "is_max mismatch at {}: {:?} vs {:?}",
2625                    i,
2626                    is_max[i],
2627                    baseline.is_max[i]
2628                );
2629                assert!(
2630                    eq_or_both_nan(last_min[i], baseline.last_min[i]),
2631                    "last_min mismatch at {}: {:?} vs {:?}",
2632                    i,
2633                    last_min[i],
2634                    baseline.last_min[i]
2635                );
2636                assert!(
2637                    eq_or_both_nan(last_max[i], baseline.last_max[i]),
2638                    "last_max mismatch at {}: {:?} vs {:?}",
2639                    i,
2640                    last_max[i],
2641                    baseline.last_max[i]
2642                );
2643            }
2644        }
2645    }
2646
2647    #[cfg(debug_assertions)]
2648    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2649        skip_if_unsupported!(kernel, test);
2650
2651        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2652        let c = read_candles_from_csv(file)?;
2653
2654        let test_configs = vec![
2655            (2, 10, 2),
2656            (5, 25, 5),
2657            (30, 60, 15),
2658            (2, 5, 1),
2659            (1, 1, 0),
2660            (10, 50, 10),
2661            (100, 100, 0),
2662        ];
2663
2664        for (cfg_idx, &(order_start, order_end, order_step)) in test_configs.iter().enumerate() {
2665            let output = MinmaxBatchBuilder::new()
2666                .kernel(kernel)
2667                .order_range(order_start, order_end, order_step)
2668                .apply_candles(&c)?;
2669
2670            let arrays = [
2671                (&output.is_min, "is_min"),
2672                (&output.is_max, "is_max"),
2673                (&output.last_min, "last_min"),
2674                (&output.last_max, "last_max"),
2675            ];
2676
2677            for (array, array_name) in arrays.iter() {
2678                for (idx, &val) in array.iter().enumerate() {
2679                    if val.is_nan() {
2680                        continue;
2681                    }
2682
2683                    let bits = val.to_bits();
2684                    let row = idx / output.cols;
2685                    let col = idx % output.cols;
2686                    let combo = &output.combos[row];
2687
2688                    if bits == 0x11111111_11111111 {
2689                        panic!(
2690							"[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
2691							 at row {} col {} (flat index {}) in {} with params: order={}",
2692							test, cfg_idx, val, bits, row, col, idx, array_name,
2693							combo.order.unwrap_or(3)
2694						);
2695                    }
2696
2697                    if bits == 0x22222222_22222222 {
2698                        panic!(
2699							"[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
2700							 at row {} col {} (flat index {}) in {} with params: order={}",
2701							test, cfg_idx, val, bits, row, col, idx, array_name,
2702							combo.order.unwrap_or(3)
2703						);
2704                    }
2705
2706                    if bits == 0x33333333_33333333 {
2707                        panic!(
2708                            "[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
2709							 at row {} col {} (flat index {}) in {} with params: order={}",
2710                            test,
2711                            cfg_idx,
2712                            val,
2713                            bits,
2714                            row,
2715                            col,
2716                            idx,
2717                            array_name,
2718                            combo.order.unwrap_or(3)
2719                        );
2720                    }
2721                }
2722            }
2723        }
2724
2725        Ok(())
2726    }
2727
2728    #[cfg(not(debug_assertions))]
2729    fn check_batch_no_poison(_test: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
2730        Ok(())
2731    }
2732
2733    #[cfg(feature = "proptest")]
2734    #[allow(clippy::float_cmp)]
2735    fn check_minmax_property(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2736        use proptest::prelude::*;
2737        skip_if_unsupported!(kernel, test_name);
2738
2739        let strat = (1usize..=50).prop_flat_map(|order| {
2740            (
2741                (order..400).prop_flat_map(move |len| {
2742                    prop::collection::vec(
2743                        (0.1f64..1000.0f64, 0.0f64..=0.2)
2744                            .prop_filter("finite", |(x, _)| x.is_finite()),
2745                        len,
2746                    )
2747                    .prop_map(move |pairs| {
2748                        let mut low = Vec::with_capacity(len);
2749                        let mut high = Vec::with_capacity(len);
2750
2751                        for (l, spread) in pairs {
2752                            low.push(l);
2753                            high.push(l * (1.0 + spread));
2754                        }
2755
2756                        (high, low)
2757                    })
2758                }),
2759                Just(order),
2760            )
2761        });
2762
2763        proptest::test_runner::TestRunner::default()
2764            .run(&strat, |((high, low), order)| {
2765                let params = MinmaxParams { order: Some(order) };
2766                let input = MinmaxInput::from_slices(&high, &low, params);
2767
2768                let output = minmax_with_kernel(&input, kernel)?;
2769                let ref_output = minmax_with_kernel(&input, Kernel::Scalar)?;
2770
2771                prop_assert_eq!(output.is_min.len(), high.len());
2772                prop_assert_eq!(output.is_max.len(), high.len());
2773                prop_assert_eq!(output.last_min.len(), high.len());
2774                prop_assert_eq!(output.last_max.len(), high.len());
2775
2776                for i in 0..order.min(high.len()) {
2777                    prop_assert!(
2778                        output.is_min[i].is_nan(),
2779                        "is_min[{}] should be NaN during warmup",
2780                        i
2781                    );
2782                    prop_assert!(
2783                        output.is_max[i].is_nan(),
2784                        "is_max[{}] should be NaN during warmup",
2785                        i
2786                    );
2787                }
2788
2789                for i in order..high.len().saturating_sub(order) {
2790                    if !output.is_min[i].is_nan() {
2791                        prop_assert_eq!(
2792                            output.is_min[i],
2793                            low[i],
2794                            "is_min[{}] should equal low[{}]",
2795                            i,
2796                            i
2797                        );
2798
2799                        for o in 1..=order {
2800                            if i >= o && i + o < low.len() {
2801                                prop_assert!(
2802                                    low[i] <= low[i - o] && low[i] <= low[i + o],
2803                                    "Detected min at {} not <= neighbors at {} and {}",
2804                                    i,
2805                                    i - o,
2806                                    i + o
2807                                );
2808                            }
2809                        }
2810                    }
2811
2812                    if !output.is_max[i].is_nan() {
2813                        prop_assert_eq!(
2814                            output.is_max[i],
2815                            high[i],
2816                            "is_max[{}] should equal high[{}]",
2817                            i,
2818                            i
2819                        );
2820
2821                        for o in 1..=order {
2822                            if i >= o && i + o < high.len() {
2823                                prop_assert!(
2824                                    high[i] >= high[i - o] && high[i] >= high[i + o],
2825                                    "Detected max at {} not >= neighbors at {} and {}",
2826                                    i,
2827                                    i - o,
2828                                    i + o
2829                                );
2830                            }
2831                        }
2832                    }
2833                }
2834
2835                let first_valid_idx = high
2836                    .iter()
2837                    .zip(low.iter())
2838                    .position(|(&h, &l)| !(h.is_nan() || l.is_nan()))
2839                    .unwrap_or(0);
2840
2841                for i in first_valid_idx..high.len() {
2842                    if i > first_valid_idx {
2843                        if output.is_min[i].is_nan() && !output.last_min[i - 1].is_nan() {
2844                            prop_assert_eq!(
2845                                output.last_min[i],
2846                                output.last_min[i - 1],
2847                                "last_min[{}] should equal last_min[{}]",
2848                                i,
2849                                i - 1
2850                            );
2851                        }
2852                        if output.is_max[i].is_nan() && !output.last_max[i - 1].is_nan() {
2853                            prop_assert_eq!(
2854                                output.last_max[i],
2855                                output.last_max[i - 1],
2856                                "last_max[{}] should equal last_max[{}]",
2857                                i,
2858                                i - 1
2859                            );
2860                        }
2861
2862                        if !output.is_min[i].is_nan() {
2863                            prop_assert_eq!(
2864                                output.last_min[i],
2865                                output.is_min[i],
2866                                "last_min[{}] should update to new minimum",
2867                                i
2868                            );
2869                        }
2870                        if !output.is_max[i].is_nan() {
2871                            prop_assert_eq!(
2872                                output.last_max[i],
2873                                output.is_max[i],
2874                                "last_max[{}] should update to new maximum",
2875                                i
2876                            );
2877                        }
2878                    }
2879                }
2880
2881                for i in 0..high.len() {
2882                    if output.is_min[i].is_finite() && ref_output.is_min[i].is_finite() {
2883                        let ulp_diff = output.is_min[i]
2884                            .to_bits()
2885                            .abs_diff(ref_output.is_min[i].to_bits());
2886                        prop_assert!(
2887                            ulp_diff <= 5,
2888                            "is_min[{}] kernel mismatch: {} vs {} (ULP={})",
2889                            i,
2890                            output.is_min[i],
2891                            ref_output.is_min[i],
2892                            ulp_diff
2893                        );
2894                    } else {
2895                        prop_assert_eq!(
2896                            output.is_min[i].to_bits(),
2897                            ref_output.is_min[i].to_bits(),
2898                            "is_min[{}] NaN mismatch",
2899                            i
2900                        );
2901                    }
2902
2903                    if output.is_max[i].is_finite() && ref_output.is_max[i].is_finite() {
2904                        let ulp_diff = output.is_max[i]
2905                            .to_bits()
2906                            .abs_diff(ref_output.is_max[i].to_bits());
2907                        prop_assert!(
2908                            ulp_diff <= 5,
2909                            "is_max[{}] kernel mismatch: {} vs {} (ULP={})",
2910                            i,
2911                            output.is_max[i],
2912                            ref_output.is_max[i],
2913                            ulp_diff
2914                        );
2915                    } else {
2916                        prop_assert_eq!(
2917                            output.is_max[i].to_bits(),
2918                            ref_output.is_max[i].to_bits(),
2919                            "is_max[{}] NaN mismatch",
2920                            i
2921                        );
2922                    }
2923
2924                    if output.last_min[i].is_finite() && ref_output.last_min[i].is_finite() {
2925                        let ulp_diff = output.last_min[i]
2926                            .to_bits()
2927                            .abs_diff(ref_output.last_min[i].to_bits());
2928                        prop_assert!(
2929                            ulp_diff <= 5,
2930                            "last_min[{}] kernel mismatch: {} vs {} (ULP={})",
2931                            i,
2932                            output.last_min[i],
2933                            ref_output.last_min[i],
2934                            ulp_diff
2935                        );
2936                    } else {
2937                        prop_assert_eq!(
2938                            output.last_min[i].to_bits(),
2939                            ref_output.last_min[i].to_bits(),
2940                            "last_min[{}] NaN mismatch",
2941                            i
2942                        );
2943                    }
2944
2945                    if output.last_max[i].is_finite() && ref_output.last_max[i].is_finite() {
2946                        let ulp_diff = output.last_max[i]
2947                            .to_bits()
2948                            .abs_diff(ref_output.last_max[i].to_bits());
2949                        prop_assert!(
2950                            ulp_diff <= 5,
2951                            "last_max[{}] kernel mismatch: {} vs {} (ULP={})",
2952                            i,
2953                            output.last_max[i],
2954                            ref_output.last_max[i],
2955                            ulp_diff
2956                        );
2957                    } else {
2958                        prop_assert_eq!(
2959                            output.last_max[i].to_bits(),
2960                            ref_output.last_max[i].to_bits(),
2961                            "last_max[{}] NaN mismatch",
2962                            i
2963                        );
2964                    }
2965                }
2966
2967                let min_low = low.iter().fold(f64::INFINITY, |a, &b| a.min(b));
2968                let max_high = high.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
2969
2970                for i in 0..high.len() {
2971                    if !output.is_min[i].is_nan() {
2972                        prop_assert!(
2973                            output.is_min[i] >= min_low && output.is_min[i] <= max_high,
2974                            "is_min[{}]={} outside data range [{}, {}]",
2975                            i,
2976                            output.is_min[i],
2977                            min_low,
2978                            max_high
2979                        );
2980                    }
2981                    if !output.is_max[i].is_nan() {
2982                        prop_assert!(
2983                            output.is_max[i] >= min_low && output.is_max[i] <= max_high,
2984                            "is_max[{}]={} outside data range [{}, {}]",
2985                            i,
2986                            output.is_max[i],
2987                            min_low,
2988                            max_high
2989                        );
2990                    }
2991                    if !output.last_min[i].is_nan() {
2992                        prop_assert!(
2993                            output.last_min[i] >= min_low && output.last_min[i] <= max_high,
2994                            "last_min[{}]={} outside data range [{}, {}]",
2995                            i,
2996                            output.last_min[i],
2997                            min_low,
2998                            max_high
2999                        );
3000                    }
3001                    if !output.last_max[i].is_nan() {
3002                        prop_assert!(
3003                            output.last_max[i] >= min_low && output.last_max[i] <= max_high,
3004                            "last_max[{}]={} outside data range [{}, {}]",
3005                            i,
3006                            output.last_max[i],
3007                            min_low,
3008                            max_high
3009                        );
3010                    }
3011                }
3012
3013                if order == 1 && high.len() >= 3 {
3014                    for i in 1..high.len() - 1 {
3015                        if low[i] < low[i - 1] && low[i] < low[i + 1] {
3016                            prop_assert!(
3017                                !output.is_min[i].is_nan(),
3018                                "Expected minimum at {} not detected",
3019                                i
3020                            );
3021                        }
3022
3023                        if high[i] > high[i - 1] && high[i] > high[i + 1] {
3024                            prop_assert!(
3025                                !output.is_max[i].is_nan(),
3026                                "Expected maximum at {} not detected",
3027                                i
3028                            );
3029                        }
3030                    }
3031                }
3032
3033                for i in 0..high.len() {
3034                    prop_assert!(
3035                        high[i] >= low[i],
3036                        "Invalid data: high[{}]={} < low[{}]={}",
3037                        i,
3038                        high[i],
3039                        i,
3040                        low[i]
3041                    );
3042                }
3043
3044                Ok(())
3045            })
3046            .unwrap();
3047
3048        Ok(())
3049    }
3050
3051    macro_rules! gen_batch_tests {
3052        ($fn_name:ident) => {
3053            paste::paste! {
3054                #[test] fn [<$fn_name _scalar>]()      {
3055                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
3056                }
3057                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3058                #[test] fn [<$fn_name _avx2>]()        {
3059                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
3060                }
3061                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3062                #[test] fn [<$fn_name _avx512>]()      {
3063                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
3064                }
3065                #[test] fn [<$fn_name _auto_detect>]() {
3066                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
3067                }
3068            }
3069        };
3070    }
3071    gen_batch_tests!(check_batch_default_row);
3072    gen_batch_tests!(check_batch_no_poison);
3073}