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, ¶ms)
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}