Skip to main content

vector_ta/indicators/
gatorosc.rs

1use crate::utilities::data_loader::{source_type, Candles};
2use crate::utilities::enums::Kernel;
3use crate::utilities::helpers::{
4    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
5    make_uninit_matrix,
6};
7use aligned_vec::{AVec, CACHELINE_ALIGN};
8#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
9use core::arch::x86_64::*;
10#[cfg(not(target_arch = "wasm32"))]
11use rayon::prelude::*;
12use thiserror::Error;
13
14#[derive(Debug, Clone)]
15pub enum GatorOscData<'a> {
16    Candles {
17        candles: &'a Candles,
18        source: &'a str,
19    },
20    Slice(&'a [f64]),
21}
22
23impl<'a> AsRef<[f64]> for GatorOscInput<'a> {
24    #[inline(always)]
25    fn as_ref(&self) -> &[f64] {
26        match &self.data {
27            GatorOscData::Slice(slice) => slice,
28            GatorOscData::Candles { candles, source } => source_type(candles, source),
29        }
30    }
31}
32
33#[derive(Debug, Clone)]
34pub struct GatorOscOutput {
35    pub upper: Vec<f64>,
36    pub lower: Vec<f64>,
37    pub upper_change: Vec<f64>,
38    pub lower_change: Vec<f64>,
39}
40
41#[derive(Debug, Clone)]
42#[cfg_attr(
43    all(target_arch = "wasm32", feature = "wasm"),
44    derive(Serialize, Deserialize)
45)]
46pub struct GatorOscParams {
47    pub jaws_length: Option<usize>,
48    pub jaws_shift: Option<usize>,
49    pub teeth_length: Option<usize>,
50    pub teeth_shift: Option<usize>,
51    pub lips_length: Option<usize>,
52    pub lips_shift: Option<usize>,
53}
54
55impl Default for GatorOscParams {
56    fn default() -> Self {
57        Self {
58            jaws_length: Some(13),
59            jaws_shift: Some(8),
60            teeth_length: Some(8),
61            teeth_shift: Some(5),
62            lips_length: Some(5),
63            lips_shift: Some(3),
64        }
65    }
66}
67
68#[derive(Debug, Clone)]
69pub struct GatorOscInput<'a> {
70    pub data: GatorOscData<'a>,
71    pub params: GatorOscParams,
72}
73
74impl<'a> GatorOscInput<'a> {
75    #[inline]
76    pub fn from_candles(c: &'a Candles, s: &'a str, p: GatorOscParams) -> Self {
77        Self {
78            data: GatorOscData::Candles {
79                candles: c,
80                source: s,
81            },
82            params: p,
83        }
84    }
85    #[inline]
86    pub fn from_slice(sl: &'a [f64], p: GatorOscParams) -> Self {
87        Self {
88            data: GatorOscData::Slice(sl),
89            params: p,
90        }
91    }
92    #[inline]
93    pub fn with_default_candles(c: &'a Candles) -> Self {
94        Self::from_candles(c, "close", GatorOscParams::default())
95    }
96    #[inline]
97    pub fn get_jaws_length(&self) -> usize {
98        self.params.jaws_length.unwrap_or(13)
99    }
100    #[inline]
101    pub fn get_jaws_shift(&self) -> usize {
102        self.params.jaws_shift.unwrap_or(8)
103    }
104    #[inline]
105    pub fn get_teeth_length(&self) -> usize {
106        self.params.teeth_length.unwrap_or(8)
107    }
108    #[inline]
109    pub fn get_teeth_shift(&self) -> usize {
110        self.params.teeth_shift.unwrap_or(5)
111    }
112    #[inline]
113    pub fn get_lips_length(&self) -> usize {
114        self.params.lips_length.unwrap_or(5)
115    }
116    #[inline]
117    pub fn get_lips_shift(&self) -> usize {
118        self.params.lips_shift.unwrap_or(3)
119    }
120}
121
122#[derive(Clone, Debug)]
123pub struct GatorOscBuilder {
124    jaws_length: Option<usize>,
125    jaws_shift: Option<usize>,
126    teeth_length: Option<usize>,
127    teeth_shift: Option<usize>,
128    lips_length: Option<usize>,
129    lips_shift: Option<usize>,
130    kernel: Kernel,
131}
132
133impl Default for GatorOscBuilder {
134    fn default() -> Self {
135        Self {
136            jaws_length: None,
137            jaws_shift: None,
138            teeth_length: None,
139            teeth_shift: None,
140            lips_length: None,
141            lips_shift: None,
142            kernel: Kernel::Auto,
143        }
144    }
145}
146
147impl GatorOscBuilder {
148    #[inline(always)]
149    pub fn new() -> Self {
150        Self::default()
151    }
152    #[inline(always)]
153    pub fn jaws_length(mut self, n: usize) -> Self {
154        self.jaws_length = Some(n);
155        self
156    }
157    #[inline(always)]
158    pub fn jaws_shift(mut self, x: usize) -> Self {
159        self.jaws_shift = Some(x);
160        self
161    }
162    #[inline(always)]
163    pub fn teeth_length(mut self, n: usize) -> Self {
164        self.teeth_length = Some(n);
165        self
166    }
167    #[inline(always)]
168    pub fn teeth_shift(mut self, x: usize) -> Self {
169        self.teeth_shift = Some(x);
170        self
171    }
172    #[inline(always)]
173    pub fn lips_length(mut self, n: usize) -> Self {
174        self.lips_length = Some(n);
175        self
176    }
177    #[inline(always)]
178    pub fn lips_shift(mut self, x: usize) -> Self {
179        self.lips_shift = Some(x);
180        self
181    }
182    #[inline(always)]
183    pub fn kernel(mut self, k: Kernel) -> Self {
184        self.kernel = k;
185        self
186    }
187    #[inline(always)]
188    pub fn apply(self, c: &Candles) -> Result<GatorOscOutput, GatorOscError> {
189        let p = GatorOscParams {
190            jaws_length: self.jaws_length,
191            jaws_shift: self.jaws_shift,
192            teeth_length: self.teeth_length,
193            teeth_shift: self.teeth_shift,
194            lips_length: self.lips_length,
195            lips_shift: self.lips_shift,
196        };
197        let i = GatorOscInput::from_candles(c, "close", p);
198        gatorosc_with_kernel(&i, self.kernel)
199    }
200    #[inline(always)]
201    pub fn apply_slice(self, d: &[f64]) -> Result<GatorOscOutput, GatorOscError> {
202        let p = GatorOscParams {
203            jaws_length: self.jaws_length,
204            jaws_shift: self.jaws_shift,
205            teeth_length: self.teeth_length,
206            teeth_shift: self.teeth_shift,
207            lips_length: self.lips_length,
208            lips_shift: self.lips_shift,
209        };
210        let i = GatorOscInput::from_slice(d, p);
211        gatorosc_with_kernel(&i, self.kernel)
212    }
213    #[inline(always)]
214    pub fn into_stream(self) -> Result<GatorOscStream, GatorOscError> {
215        let p = GatorOscParams {
216            jaws_length: self.jaws_length,
217            jaws_shift: self.jaws_shift,
218            teeth_length: self.teeth_length,
219            teeth_shift: self.teeth_shift,
220            lips_length: self.lips_length,
221            lips_shift: self.lips_shift,
222        };
223        GatorOscStream::try_new(p)
224    }
225}
226
227#[derive(Debug, Error)]
228pub enum GatorOscError {
229    #[error("gatorosc: Input data slice is empty.")]
230    EmptyInputData,
231    #[error("gatorosc: All values are NaN.")]
232    AllValuesNaN,
233    #[error("gatorosc: Invalid period: period={period} data_len={data_len}")]
234    InvalidPeriod { period: usize, data_len: usize },
235    #[error("gatorosc: Not enough valid data: needed = {needed}, valid = {valid}")]
236    NotEnoughValidData { needed: usize, valid: usize },
237    #[error("gatorosc: output length mismatch: expected={expected} got={got}")]
238    OutputLengthMismatch { expected: usize, got: usize },
239    #[error("gatorosc: invalid range: start={start} end={end} step={step}")]
240    InvalidRange {
241        start: usize,
242        end: usize,
243        step: usize,
244    },
245    #[error("gatorosc: invalid kernel for batch: {0:?}")]
246    InvalidKernelForBatch(crate::utilities::enums::Kernel),
247    #[error("gatorosc: invalid input: {0}")]
248    InvalidInput(String),
249}
250
251#[inline(always)]
252fn gator_warmups(
253    first: usize,
254    jl: usize,
255    js: usize,
256    tl: usize,
257    ts: usize,
258    ll: usize,
259    ls: usize,
260) -> (usize, usize, usize, usize) {
261    let upper_needed = jl.max(tl) + js.max(ts);
262    let lower_needed = tl.max(ll) + ts.max(ls);
263
264    let upper_warmup = first + upper_needed.saturating_sub(1);
265    let lower_warmup = first + lower_needed.saturating_sub(1);
266    let upper_change_warmup = upper_warmup + 1;
267    let lower_change_warmup = lower_warmup + 1;
268
269    (
270        upper_warmup,
271        lower_warmup,
272        upper_change_warmup,
273        lower_change_warmup,
274    )
275}
276
277#[inline]
278pub fn gatorosc(input: &GatorOscInput) -> Result<GatorOscOutput, GatorOscError> {
279    gatorosc_with_kernel(input, Kernel::Auto)
280}
281
282pub fn gatorosc_with_kernel(
283    input: &GatorOscInput,
284    kernel: Kernel,
285) -> Result<GatorOscOutput, GatorOscError> {
286    let (
287        data,
288        jaws_length,
289        jaws_shift,
290        teeth_length,
291        teeth_shift,
292        lips_length,
293        lips_shift,
294        first,
295        chosen,
296    ) = gatorosc_prepare(input, kernel)?;
297
298    let (upper_warmup, lower_warmup, upper_change_warmup, lower_change_warmup) = gator_warmups(
299        first,
300        jaws_length,
301        jaws_shift,
302        teeth_length,
303        teeth_shift,
304        lips_length,
305        lips_shift,
306    );
307
308    let mut upper = alloc_with_nan_prefix(data.len(), upper_warmup);
309    let mut lower = alloc_with_nan_prefix(data.len(), lower_warmup);
310    let mut upper_change = alloc_with_nan_prefix(data.len(), upper_change_warmup);
311    let mut lower_change = alloc_with_nan_prefix(data.len(), lower_change_warmup);
312
313    gatorosc_compute_into(
314        data,
315        jaws_length,
316        jaws_shift,
317        teeth_length,
318        teeth_shift,
319        lips_length,
320        lips_shift,
321        first,
322        chosen,
323        &mut upper,
324        &mut lower,
325        &mut upper_change,
326        &mut lower_change,
327    );
328
329    for v in &mut upper[..upper_warmup] {
330        *v = f64::NAN;
331    }
332    for v in &mut lower[..lower_warmup] {
333        *v = f64::NAN;
334    }
335    for v in &mut upper_change[..upper_change_warmup] {
336        *v = f64::NAN;
337    }
338    for v in &mut lower_change[..lower_change_warmup] {
339        *v = f64::NAN;
340    }
341
342    Ok(GatorOscOutput {
343        upper,
344        lower,
345        upper_change,
346        lower_change,
347    })
348}
349
350#[inline(always)]
351pub unsafe fn gatorosc_scalar(
352    data: &[f64],
353    jaws_length: usize,
354    jaws_shift: usize,
355    teeth_length: usize,
356    teeth_shift: usize,
357    lips_length: usize,
358    lips_shift: usize,
359    first_valid: usize,
360    upper: &mut [f64],
361    lower: &mut [f64],
362    upper_change: &mut [f64],
363    lower_change: &mut [f64],
364) {
365    let n = data.len();
366    if first_valid >= n {
367        return;
368    }
369
370    let ja = 2.0 / (jaws_length as f64 + 1.0);
371    let ta = 2.0 / (teeth_length as f64 + 1.0);
372    let la = 2.0 / (lips_length as f64 + 1.0);
373    let jma = 1.0 - ja;
374    let tma = 1.0 - ta;
375    let lma = 1.0 - la;
376
377    let (uw, lw, ucw, lcw) = gator_warmups(
378        first_valid,
379        jaws_length,
380        jaws_shift,
381        teeth_length,
382        teeth_shift,
383        lips_length,
384        lips_shift,
385    );
386
387    let mut jema = if data[first_valid].is_nan() {
388        0.0
389    } else {
390        data[first_valid]
391    };
392    let mut tema = jema;
393    let mut lema = jema;
394
395    let max_shift = jaws_shift.max(teeth_shift).max(lips_shift);
396    let buf_len = max_shift + 1;
397
398    let mut jring: AVec<f64> = AVec::with_capacity(CACHELINE_ALIGN, buf_len);
399    let mut tring: AVec<f64> = AVec::with_capacity(CACHELINE_ALIGN, buf_len);
400    let mut lring: AVec<f64> = AVec::with_capacity(CACHELINE_ALIGN, buf_len);
401    jring.resize(buf_len, jema);
402    tring.resize(buf_len, tema);
403    lring.resize(buf_len, lema);
404
405    let mut rpos: usize = 0;
406
407    let mut u_prev = 0.0;
408    let mut l_prev = 0.0;
409    let mut have_u = false;
410    let mut have_l = false;
411
412    let mut i = first_valid;
413    while i < n {
414        let x = {
415            let xi = *data.get_unchecked(i);
416            if xi.is_nan() {
417                jema
418            } else {
419                xi
420            }
421        };
422
423        jema = jma.mul_add(jema, ja * x);
424        tema = tma.mul_add(tema, ta * x);
425        lema = lma.mul_add(lema, la * x);
426
427        *jring.get_unchecked_mut(rpos) = jema;
428        *tring.get_unchecked_mut(rpos) = tema;
429        *lring.get_unchecked_mut(rpos) = lema;
430
431        let mut jj = rpos + buf_len - jaws_shift;
432        if jj >= buf_len {
433            jj -= buf_len;
434        }
435        let mut tt = rpos + buf_len - teeth_shift;
436        if tt >= buf_len {
437            tt -= buf_len;
438        }
439        let mut ll = rpos + buf_len - lips_shift;
440        if ll >= buf_len {
441            ll -= buf_len;
442        }
443
444        if i >= uw {
445            let u = (*jring.get_unchecked(jj) - *tring.get_unchecked(tt)).abs();
446            *upper.get_unchecked_mut(i) = u;
447
448            if i == uw {
449                u_prev = u;
450                have_u = true;
451            } else if i >= ucw && have_u {
452                *upper_change.get_unchecked_mut(i) = u - u_prev;
453                u_prev = u;
454            }
455        }
456
457        if i >= lw {
458            let l = -(*tring.get_unchecked(tt) - *lring.get_unchecked(ll)).abs();
459            *lower.get_unchecked_mut(i) = l;
460            if i == lw {
461                l_prev = l;
462                have_l = true;
463            } else if i >= lcw && have_l {
464                *lower_change.get_unchecked_mut(i) = -(l - l_prev);
465                l_prev = l;
466            }
467        }
468
469        rpos += 1;
470        if rpos == buf_len {
471            rpos = 0;
472        }
473
474        i += 1;
475    }
476}
477
478#[cfg(all(target_feature = "simd128", target_arch = "wasm32"))]
479#[inline(always)]
480pub unsafe fn gatorosc_simd128(
481    data: &[f64],
482    jaws_length: usize,
483    jaws_shift: usize,
484    teeth_length: usize,
485    teeth_shift: usize,
486    lips_length: usize,
487    lips_shift: usize,
488    first_valid: usize,
489    upper: &mut [f64],
490    lower: &mut [f64],
491    upper_change: &mut [f64],
492    lower_change: &mut [f64],
493) {
494    gatorosc_scalar(
495        data,
496        jaws_length,
497        jaws_shift,
498        teeth_length,
499        teeth_shift,
500        lips_length,
501        lips_shift,
502        first_valid,
503        upper,
504        lower,
505        upper_change,
506        lower_change,
507    );
508}
509
510#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
511#[inline(always)]
512pub unsafe fn gatorosc_avx2(
513    data: &[f64],
514    jaws_length: usize,
515    jaws_shift: usize,
516    teeth_length: usize,
517    teeth_shift: usize,
518    lips_length: usize,
519    lips_shift: usize,
520    first_valid: usize,
521    upper: &mut [f64],
522    lower: &mut [f64],
523    upper_change: &mut [f64],
524    lower_change: &mut [f64],
525) {
526    use core::arch::x86_64::*;
527
528    let n = data.len();
529    if first_valid >= n {
530        return;
531    }
532
533    let ja = 2.0 / (jaws_length as f64 + 1.0);
534    let ta = 2.0 / (teeth_length as f64 + 1.0);
535    let la = 2.0 / (lips_length as f64 + 1.0);
536
537    let a = _mm256_set_pd(0.0, la, ta, ja);
538    let one = _mm256_set1_pd(1.0);
539    let oma = _mm256_sub_pd(one, a);
540
541    let (uw, lw, ucw, lcw) = gator_warmups(
542        first_valid,
543        jaws_length,
544        jaws_shift,
545        teeth_length,
546        teeth_shift,
547        lips_length,
548        lips_shift,
549    );
550
551    let mut jema = if data.get_unchecked(first_valid).is_nan() {
552        0.0
553    } else {
554        *data.get_unchecked(first_valid)
555    };
556    let mut tema = jema;
557    let mut lema = jema;
558
559    let mut e = _mm256_set_pd(0.0, lema, tema, jema);
560
561    let max_shift = jaws_shift.max(teeth_shift).max(lips_shift);
562    let buf_len = max_shift + 1;
563    let mut jring: AVec<f64> = AVec::with_capacity(CACHELINE_ALIGN, buf_len);
564    let mut tring: AVec<f64> = AVec::with_capacity(CACHELINE_ALIGN, buf_len);
565    let mut lring: AVec<f64> = AVec::with_capacity(CACHELINE_ALIGN, buf_len);
566    jring.resize(buf_len, jema);
567    tring.resize(buf_len, tema);
568    lring.resize(buf_len, lema);
569
570    let mut rpos: usize = 0;
571    let mut u_prev = 0.0;
572    let mut l_prev = 0.0;
573    let mut have_u = false;
574    let mut have_l = false;
575    let mut lanes: [f64; 4] = core::mem::zeroed();
576
577    let mut i = first_valid;
578    while i < n {
579        let x = {
580            let xi = *data.get_unchecked(i);
581            if xi.is_nan() {
582                jema
583            } else {
584                xi
585            }
586        };
587        let vx = _mm256_set1_pd(x);
588        let oma_e = _mm256_mul_pd(oma, e);
589        let a_vx = _mm256_mul_pd(a, vx);
590        e = _mm256_add_pd(oma_e, a_vx);
591
592        _mm256_storeu_pd(lanes.as_mut_ptr(), e);
593        jema = lanes[0];
594        tema = lanes[1];
595        lema = lanes[2];
596
597        *jring.get_unchecked_mut(rpos) = jema;
598        *tring.get_unchecked_mut(rpos) = tema;
599        *lring.get_unchecked_mut(rpos) = lema;
600
601        let mut jj = rpos + buf_len - jaws_shift;
602        if jj >= buf_len {
603            jj -= buf_len;
604        }
605        let mut tt = rpos + buf_len - teeth_shift;
606        if tt >= buf_len {
607            tt -= buf_len;
608        }
609        let mut ll = rpos + buf_len - lips_shift;
610        if ll >= buf_len {
611            ll -= buf_len;
612        }
613
614        if i >= uw {
615            let u = (*jring.get_unchecked(jj) - *tring.get_unchecked(tt)).abs();
616            *upper.get_unchecked_mut(i) = u;
617            if i == uw {
618                u_prev = u;
619                have_u = true;
620            } else if i >= ucw && have_u {
621                *upper_change.get_unchecked_mut(i) = u - u_prev;
622                u_prev = u;
623            }
624        }
625
626        if i >= lw {
627            let l = -(*tring.get_unchecked(tt) - *lring.get_unchecked(ll)).abs();
628            *lower.get_unchecked_mut(i) = l;
629            if i == lw {
630                l_prev = l;
631                have_l = true;
632            } else if i >= lcw && have_l {
633                *lower_change.get_unchecked_mut(i) = -(l - l_prev);
634                l_prev = l;
635            }
636        }
637
638        rpos += 1;
639        if rpos == buf_len {
640            rpos = 0;
641        }
642        i += 1;
643    }
644}
645
646#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
647#[inline(always)]
648pub unsafe fn gatorosc_avx512(
649    data: &[f64],
650    jaws_length: usize,
651    jaws_shift: usize,
652    teeth_length: usize,
653    teeth_shift: usize,
654    lips_length: usize,
655    lips_shift: usize,
656    first_valid: usize,
657    upper: &mut [f64],
658    lower: &mut [f64],
659    upper_change: &mut [f64],
660    lower_change: &mut [f64],
661) {
662    if jaws_length <= 32 && teeth_length <= 32 && lips_length <= 32 {
663        gatorosc_avx512_short(
664            data,
665            jaws_length,
666            jaws_shift,
667            teeth_length,
668            teeth_shift,
669            lips_length,
670            lips_shift,
671            first_valid,
672            upper,
673            lower,
674            upper_change,
675            lower_change,
676        );
677    } else {
678        gatorosc_avx512_long(
679            data,
680            jaws_length,
681            jaws_shift,
682            teeth_length,
683            teeth_shift,
684            lips_length,
685            lips_shift,
686            first_valid,
687            upper,
688            lower,
689            upper_change,
690            lower_change,
691        );
692    }
693}
694
695#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
696#[inline(always)]
697pub unsafe fn gatorosc_avx512_short(
698    data: &[f64],
699    jaws_length: usize,
700    jaws_shift: usize,
701    teeth_length: usize,
702    teeth_shift: usize,
703    lips_length: usize,
704    lips_shift: usize,
705    first_valid: usize,
706    upper: &mut [f64],
707    lower: &mut [f64],
708    upper_change: &mut [f64],
709    lower_change: &mut [f64],
710) {
711    use core::arch::x86_64::*;
712
713    let n = data.len();
714    if first_valid >= n {
715        return;
716    }
717
718    let ja = 2.0 / (jaws_length as f64 + 1.0);
719    let ta = 2.0 / (teeth_length as f64 + 1.0);
720    let la = 2.0 / (lips_length as f64 + 1.0);
721
722    let a = _mm512_set_pd(0.0, 0.0, 0.0, 0.0, 0.0, la, ta, ja);
723    let one = _mm512_set1_pd(1.0);
724    let oma = _mm512_sub_pd(one, a);
725
726    let (uw, lw, ucw, lcw) = gator_warmups(
727        first_valid,
728        jaws_length,
729        jaws_shift,
730        teeth_length,
731        teeth_shift,
732        lips_length,
733        lips_shift,
734    );
735
736    let mut jema = if data.get_unchecked(first_valid).is_nan() {
737        0.0
738    } else {
739        *data.get_unchecked(first_valid)
740    };
741    let mut tema = jema;
742    let mut lema = jema;
743
744    let mut e = _mm512_set_pd(0.0, 0.0, 0.0, 0.0, 0.0, lema, tema, jema);
745
746    let max_shift = jaws_shift.max(teeth_shift).max(lips_shift);
747    let buf_len = max_shift + 1;
748    let mut jring: AVec<f64> = AVec::with_capacity(CACHELINE_ALIGN, buf_len);
749    let mut tring: AVec<f64> = AVec::with_capacity(CACHELINE_ALIGN, buf_len);
750    let mut lring: AVec<f64> = AVec::with_capacity(CACHELINE_ALIGN, buf_len);
751    jring.resize(buf_len, jema);
752    tring.resize(buf_len, tema);
753    lring.resize(buf_len, lema);
754
755    let mut rpos: usize = 0;
756    let mut u_prev = 0.0;
757    let mut l_prev = 0.0;
758    let mut have_u = false;
759    let mut have_l = false;
760    let mut lanes: [f64; 8] = core::mem::zeroed();
761
762    let mut i = first_valid;
763    while i < n {
764        let x = {
765            let xi = *data.get_unchecked(i);
766            if xi.is_nan() {
767                jema
768            } else {
769                xi
770            }
771        };
772        let vx = _mm512_set1_pd(x);
773        let oma_e = _mm512_mul_pd(oma, e);
774        let a_vx = _mm512_mul_pd(a, vx);
775        e = _mm512_add_pd(oma_e, a_vx);
776
777        _mm512_storeu_pd(lanes.as_mut_ptr(), e);
778
779        jema = lanes[0];
780        tema = lanes[1];
781        lema = lanes[2];
782
783        *jring.get_unchecked_mut(rpos) = jema;
784        *tring.get_unchecked_mut(rpos) = tema;
785        *lring.get_unchecked_mut(rpos) = lema;
786
787        let mut jj = rpos + buf_len - jaws_shift;
788        if jj >= buf_len {
789            jj -= buf_len;
790        }
791        let mut tt = rpos + buf_len - teeth_shift;
792        if tt >= buf_len {
793            tt -= buf_len;
794        }
795        let mut ll = rpos + buf_len - lips_shift;
796        if ll >= buf_len {
797            ll -= buf_len;
798        }
799
800        if i >= uw {
801            let u = (*jring.get_unchecked(jj) - *tring.get_unchecked(tt)).abs();
802            *upper.get_unchecked_mut(i) = u;
803            if i == uw {
804                u_prev = u;
805                have_u = true;
806            } else if i >= ucw && have_u {
807                *upper_change.get_unchecked_mut(i) = u - u_prev;
808                u_prev = u;
809            }
810        }
811
812        if i >= lw {
813            let l = -(*tring.get_unchecked(tt) - *lring.get_unchecked(ll)).abs();
814            *lower.get_unchecked_mut(i) = l;
815            if i == lw {
816                l_prev = l;
817                have_l = true;
818            } else if i >= lcw && have_l {
819                *lower_change.get_unchecked_mut(i) = -(l - l_prev);
820                l_prev = l;
821            }
822        }
823
824        rpos += 1;
825        if rpos == buf_len {
826            rpos = 0;
827        }
828        i += 1;
829    }
830}
831
832#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
833#[inline(always)]
834pub unsafe fn gatorosc_avx512_long(
835    data: &[f64],
836    jaws_length: usize,
837    jaws_shift: usize,
838    teeth_length: usize,
839    teeth_shift: usize,
840    lips_length: usize,
841    lips_shift: usize,
842    first_valid: usize,
843    upper: &mut [f64],
844    lower: &mut [f64],
845    upper_change: &mut [f64],
846    lower_change: &mut [f64],
847) {
848    gatorosc_avx512_short(
849        data,
850        jaws_length,
851        jaws_shift,
852        teeth_length,
853        teeth_shift,
854        lips_length,
855        lips_shift,
856        first_valid,
857        upper,
858        lower,
859        upper_change,
860        lower_change,
861    );
862}
863
864#[inline]
865fn gatorosc_prepare<'a>(
866    input: &'a GatorOscInput<'a>,
867    kernel: Kernel,
868) -> Result<
869    (
870        &'a [f64],
871        usize,
872        usize,
873        usize,
874        usize,
875        usize,
876        usize,
877        usize,
878        Kernel,
879    ),
880    GatorOscError,
881> {
882    let data: &[f64] = input.as_ref();
883
884    if data.is_empty() {
885        return Err(GatorOscError::EmptyInputData);
886    }
887
888    let first = data
889        .iter()
890        .position(|x| !x.is_nan())
891        .ok_or(GatorOscError::AllValuesNaN)?;
892
893    let jaws_length = input.get_jaws_length();
894    let jaws_shift = input.get_jaws_shift();
895    let teeth_length = input.get_teeth_length();
896    let teeth_shift = input.get_teeth_shift();
897    let lips_length = input.get_lips_length();
898    let lips_shift = input.get_lips_shift();
899
900    if jaws_length == 0 {
901        return Err(GatorOscError::InvalidPeriod {
902            period: jaws_length,
903            data_len: data.len(),
904        });
905    }
906    if teeth_length == 0 {
907        return Err(GatorOscError::InvalidPeriod {
908            period: teeth_length,
909            data_len: data.len(),
910        });
911    }
912    if lips_length == 0 {
913        return Err(GatorOscError::InvalidPeriod {
914            period: lips_length,
915            data_len: data.len(),
916        });
917    }
918
919    let needed = jaws_length.max(teeth_length).max(lips_length)
920        + jaws_shift.max(teeth_shift).max(lips_shift);
921    if data.len() - first < needed {
922        return Err(GatorOscError::NotEnoughValidData {
923            needed,
924            valid: data.len() - first,
925        });
926    }
927
928    let chosen = match kernel {
929        Kernel::Auto => Kernel::Scalar,
930        other => other,
931    };
932
933    Ok((
934        data,
935        jaws_length,
936        jaws_shift,
937        teeth_length,
938        teeth_shift,
939        lips_length,
940        lips_shift,
941        first,
942        chosen,
943    ))
944}
945
946#[inline]
947fn gatorosc_compute_into(
948    data: &[f64],
949    jaws_length: usize,
950    jaws_shift: usize,
951    teeth_length: usize,
952    teeth_shift: usize,
953    lips_length: usize,
954    lips_shift: usize,
955    first: usize,
956    kernel: Kernel,
957    upper: &mut [f64],
958    lower: &mut [f64],
959    upper_change: &mut [f64],
960    lower_change: &mut [f64],
961) {
962    unsafe {
963        #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
964        {
965            if matches!(kernel, Kernel::Scalar | Kernel::ScalarBatch) {
966                gatorosc_simd128(
967                    data,
968                    jaws_length,
969                    jaws_shift,
970                    teeth_length,
971                    teeth_shift,
972                    lips_length,
973                    lips_shift,
974                    first,
975                    upper,
976                    lower,
977                    upper_change,
978                    lower_change,
979                );
980                return;
981            }
982        }
983        match kernel {
984            Kernel::Scalar | Kernel::ScalarBatch => gatorosc_scalar(
985                data,
986                jaws_length,
987                jaws_shift,
988                teeth_length,
989                teeth_shift,
990                lips_length,
991                lips_shift,
992                first,
993                upper,
994                lower,
995                upper_change,
996                lower_change,
997            ),
998            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
999            Kernel::Avx2 | Kernel::Avx2Batch => gatorosc_avx2(
1000                data,
1001                jaws_length,
1002                jaws_shift,
1003                teeth_length,
1004                teeth_shift,
1005                lips_length,
1006                lips_shift,
1007                first,
1008                upper,
1009                lower,
1010                upper_change,
1011                lower_change,
1012            ),
1013            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1014            Kernel::Avx512 | Kernel::Avx512Batch => gatorosc_avx512(
1015                data,
1016                jaws_length,
1017                jaws_shift,
1018                teeth_length,
1019                teeth_shift,
1020                lips_length,
1021                lips_shift,
1022                first,
1023                upper,
1024                lower,
1025                upper_change,
1026                lower_change,
1027            ),
1028            _ => unreachable!(),
1029        }
1030    }
1031}
1032
1033#[inline]
1034pub fn gatorosc_into_slice(
1035    upper_dst: &mut [f64],
1036    lower_dst: &mut [f64],
1037    upper_change_dst: &mut [f64],
1038    lower_change_dst: &mut [f64],
1039    input: &GatorOscInput,
1040    kernel: Kernel,
1041) -> Result<(), GatorOscError> {
1042    let (
1043        data,
1044        jaws_length,
1045        jaws_shift,
1046        teeth_length,
1047        teeth_shift,
1048        lips_length,
1049        lips_shift,
1050        first,
1051        chosen,
1052    ) = gatorosc_prepare(input, kernel)?;
1053
1054    let expected = data.len();
1055    if upper_dst.len() != expected {
1056        return Err(GatorOscError::OutputLengthMismatch {
1057            expected,
1058            got: upper_dst.len(),
1059        });
1060    }
1061    if lower_dst.len() != expected {
1062        return Err(GatorOscError::OutputLengthMismatch {
1063            expected,
1064            got: lower_dst.len(),
1065        });
1066    }
1067    if upper_change_dst.len() != expected {
1068        return Err(GatorOscError::OutputLengthMismatch {
1069            expected,
1070            got: upper_change_dst.len(),
1071        });
1072    }
1073    if lower_change_dst.len() != expected {
1074        return Err(GatorOscError::OutputLengthMismatch {
1075            expected,
1076            got: lower_change_dst.len(),
1077        });
1078    }
1079
1080    gatorosc_compute_into(
1081        data,
1082        jaws_length,
1083        jaws_shift,
1084        teeth_length,
1085        teeth_shift,
1086        lips_length,
1087        lips_shift,
1088        first,
1089        chosen,
1090        upper_dst,
1091        lower_dst,
1092        upper_change_dst,
1093        lower_change_dst,
1094    );
1095
1096    let (upper_warmup, lower_warmup, upper_change_warmup, lower_change_warmup) = gator_warmups(
1097        first,
1098        jaws_length,
1099        jaws_shift,
1100        teeth_length,
1101        teeth_shift,
1102        lips_length,
1103        lips_shift,
1104    );
1105
1106    for v in &mut upper_dst[..upper_warmup] {
1107        *v = f64::NAN;
1108    }
1109    for v in &mut lower_dst[..lower_warmup] {
1110        *v = f64::NAN;
1111    }
1112    for v in &mut upper_change_dst[..upper_change_warmup] {
1113        *v = f64::NAN;
1114    }
1115    for v in &mut lower_change_dst[..lower_change_warmup] {
1116        *v = f64::NAN;
1117    }
1118
1119    Ok(())
1120}
1121
1122#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1123#[inline]
1124pub fn gatorosc_into(
1125    input: &GatorOscInput,
1126    upper: &mut [f64],
1127    lower: &mut [f64],
1128    upper_change: &mut [f64],
1129    lower_change: &mut [f64],
1130) -> Result<(), GatorOscError> {
1131    gatorosc_into_slice(
1132        upper,
1133        lower,
1134        upper_change,
1135        lower_change,
1136        input,
1137        Kernel::Auto,
1138    )
1139}
1140
1141#[derive(Debug, Clone)]
1142pub struct GatorOscStream {
1143    ja: f64,
1144    ta: f64,
1145    la: f64,
1146    jema: f64,
1147    tema: f64,
1148    lema: f64,
1149    initialized: bool,
1150
1151    jaws_shift: usize,
1152    teeth_shift: usize,
1153    lips_shift: usize,
1154
1155    jring: AVec<f64>,
1156    tring: AVec<f64>,
1157    lring: AVec<f64>,
1158    rpos: usize,
1159
1160    idx: usize,
1161
1162    first_valid: Option<usize>,
1163    upper_needed: usize,
1164    lower_needed: usize,
1165    warmup_upper: usize,
1166    warmup_lower: usize,
1167    warmup_uc: usize,
1168    warmup_lc: usize,
1169
1170    prev_u: f64,
1171    prev_l: f64,
1172    have_prev_u: bool,
1173    have_prev_l: bool,
1174}
1175
1176#[inline(always)]
1177fn ema_update(prev: f64, x: f64, a: f64) -> f64 {
1178    (x - prev).mul_add(a, prev)
1179}
1180
1181#[inline(always)]
1182fn fast_abs_f64(x: f64) -> f64 {
1183    f64::from_bits(x.to_bits() & 0x7FFF_FFFF_FFFF_FFFF)
1184}
1185
1186#[inline(always)]
1187fn wrap_back(pos: usize, len: usize, back: usize) -> usize {
1188    let mut idx = pos + len - back;
1189    if idx >= len {
1190        idx -= len;
1191    }
1192    idx
1193}
1194
1195impl GatorOscStream {
1196    pub fn try_new(params: GatorOscParams) -> Result<Self, GatorOscError> {
1197        let jaws_length = params.jaws_length.unwrap_or(13);
1198        let jaws_shift = params.jaws_shift.unwrap_or(8);
1199        let teeth_length = params.teeth_length.unwrap_or(8);
1200        let teeth_shift = params.teeth_shift.unwrap_or(5);
1201        let lips_length = params.lips_length.unwrap_or(5);
1202        let lips_shift = params.lips_shift.unwrap_or(3);
1203
1204        if jaws_length == 0 {
1205            return Err(GatorOscError::InvalidPeriod {
1206                period: jaws_length,
1207                data_len: 0,
1208            });
1209        }
1210        if teeth_length == 0 {
1211            return Err(GatorOscError::InvalidPeriod {
1212                period: teeth_length,
1213                data_len: 0,
1214            });
1215        }
1216        if lips_length == 0 {
1217            return Err(GatorOscError::InvalidPeriod {
1218                period: lips_length,
1219                data_len: 0,
1220            });
1221        }
1222
1223        let ja = 2.0 / (jaws_length as f64 + 1.0);
1224        let ta = 2.0 / (teeth_length as f64 + 1.0);
1225        let la = 2.0 / (lips_length as f64 + 1.0);
1226
1227        let buf_len = jaws_shift.max(teeth_shift).max(lips_shift) + 1;
1228        let mut jring: AVec<f64> = AVec::with_capacity(CACHELINE_ALIGN, buf_len);
1229        let mut tring: AVec<f64> = AVec::with_capacity(CACHELINE_ALIGN, buf_len);
1230        let mut lring: AVec<f64> = AVec::with_capacity(CACHELINE_ALIGN, buf_len);
1231        jring.resize(buf_len, 0.0);
1232        tring.resize(buf_len, 0.0);
1233        lring.resize(buf_len, 0.0);
1234
1235        let upper_needed = jaws_length.max(teeth_length) + jaws_shift.max(teeth_shift);
1236        let lower_needed = teeth_length.max(lips_length) + teeth_shift.max(lips_shift);
1237
1238        Ok(Self {
1239            ja,
1240            ta,
1241            la,
1242            jema: 0.0,
1243            tema: 0.0,
1244            lema: 0.0,
1245            initialized: false,
1246
1247            jaws_shift,
1248            teeth_shift,
1249            lips_shift,
1250
1251            jring,
1252            tring,
1253            lring,
1254            rpos: 0,
1255            idx: 0,
1256
1257            first_valid: None,
1258            upper_needed,
1259            lower_needed,
1260            warmup_upper: usize::MAX,
1261            warmup_lower: usize::MAX,
1262            warmup_uc: usize::MAX,
1263            warmup_lc: usize::MAX,
1264
1265            prev_u: 0.0,
1266            prev_l: 0.0,
1267            have_prev_u: false,
1268            have_prev_l: false,
1269        })
1270    }
1271
1272    #[inline(always)]
1273    pub fn update(&mut self, value: f64) -> Option<(f64, f64, f64, f64)> {
1274        let i = self.idx;
1275        self.idx = i + 1;
1276
1277        if !self.initialized {
1278            if !value.is_finite() {
1279                return None;
1280            }
1281            self.jema = value;
1282            self.tema = value;
1283            self.lema = value;
1284            self.initialized = true;
1285            self.first_valid = Some(i);
1286
1287            self.warmup_upper = i + self.upper_needed.saturating_sub(1);
1288            self.warmup_lower = i + self.lower_needed.saturating_sub(1);
1289            self.warmup_uc = self.warmup_upper + 1;
1290            self.warmup_lc = self.warmup_lower + 1;
1291        } else {
1292            let x = if value.is_nan() { self.jema } else { value };
1293            self.jema = ema_update(self.jema, x, self.ja);
1294            self.tema = ema_update(self.tema, x, self.ta);
1295            self.lema = ema_update(self.lema, x, self.la);
1296        }
1297
1298        let r = self.rpos;
1299        self.jring[r] = self.jema;
1300        self.tring[r] = self.tema;
1301        self.lring[r] = self.lema;
1302
1303        let len = self.jring.len();
1304        let jj = wrap_back(r, len, self.jaws_shift);
1305        let tt = wrap_back(r, len, self.teeth_shift);
1306        let ll = wrap_back(r, len, self.lips_shift);
1307
1308        let mut next = r + 1;
1309        if next == len {
1310            next = 0;
1311        }
1312        self.rpos = next;
1313
1314        if i == self.warmup_upper {
1315            let u0 = fast_abs_f64(self.jring[jj] - self.tring[tt]);
1316            self.prev_u = u0;
1317            self.have_prev_u = true;
1318        }
1319        if i == self.warmup_lower {
1320            let l0 = -fast_abs_f64(self.tring[tt] - self.lring[ll]);
1321            self.prev_l = l0;
1322            self.have_prev_l = true;
1323        }
1324
1325        if i < self.warmup_lc {
1326            return None;
1327        }
1328
1329        let u = fast_abs_f64(self.jring[jj] - self.tring[tt]);
1330        let l = -fast_abs_f64(self.tring[tt] - self.lring[ll]);
1331
1332        let uc = if i >= self.warmup_uc && self.have_prev_u {
1333            let d = u - self.prev_u;
1334            self.prev_u = u;
1335            d
1336        } else {
1337            f64::NAN
1338        };
1339        let lc = if i >= self.warmup_lc && self.have_prev_l {
1340            let d = self.prev_l - l;
1341            self.prev_l = l;
1342            d
1343        } else {
1344            f64::NAN
1345        };
1346
1347        Some((u, l, uc, lc))
1348    }
1349}
1350
1351#[derive(Clone, Debug)]
1352pub struct GatorOscBatchRange {
1353    pub jaws_length: (usize, usize, usize),
1354    pub jaws_shift: (usize, usize, usize),
1355    pub teeth_length: (usize, usize, usize),
1356    pub teeth_shift: (usize, usize, usize),
1357    pub lips_length: (usize, usize, usize),
1358    pub lips_shift: (usize, usize, usize),
1359}
1360
1361impl Default for GatorOscBatchRange {
1362    fn default() -> Self {
1363        Self {
1364            jaws_length: (13, 262, 1),
1365            jaws_shift: (8, 8, 0),
1366            teeth_length: (8, 8, 0),
1367            teeth_shift: (5, 5, 0),
1368            lips_length: (5, 5, 0),
1369            lips_shift: (3, 3, 0),
1370        }
1371    }
1372}
1373
1374#[derive(Clone, Debug, Default)]
1375pub struct GatorOscBatchBuilder {
1376    range: GatorOscBatchRange,
1377    kernel: Kernel,
1378}
1379
1380impl GatorOscBatchBuilder {
1381    pub fn new() -> Self {
1382        Self::default()
1383    }
1384    pub fn kernel(mut self, k: Kernel) -> Self {
1385        self.kernel = k;
1386        self
1387    }
1388    pub fn jaws_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1389        self.range.jaws_length = (start, end, step);
1390        self
1391    }
1392    pub fn jaws_shift_range(mut self, start: usize, end: usize, step: usize) -> Self {
1393        self.range.jaws_shift = (start, end, step);
1394        self
1395    }
1396    pub fn teeth_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1397        self.range.teeth_length = (start, end, step);
1398        self
1399    }
1400    pub fn teeth_shift_range(mut self, start: usize, end: usize, step: usize) -> Self {
1401        self.range.teeth_shift = (start, end, step);
1402        self
1403    }
1404    pub fn lips_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1405        self.range.lips_length = (start, end, step);
1406        self
1407    }
1408    pub fn lips_shift_range(mut self, start: usize, end: usize, step: usize) -> Self {
1409        self.range.lips_shift = (start, end, step);
1410        self
1411    }
1412    pub fn apply_slice(self, data: &[f64]) -> Result<GatorOscBatchOutput, GatorOscError> {
1413        gatorosc_batch_with_kernel(data, &self.range, self.kernel)
1414    }
1415}
1416
1417#[derive(Clone, Debug)]
1418pub struct GatorOscBatchOutput {
1419    pub upper: Vec<f64>,
1420    pub lower: Vec<f64>,
1421    pub upper_change: Vec<f64>,
1422    pub lower_change: Vec<f64>,
1423    pub combos: Vec<GatorOscParams>,
1424    pub rows: usize,
1425    pub cols: usize,
1426}
1427
1428pub fn gatorosc_batch_with_kernel(
1429    data: &[f64],
1430    sweep: &GatorOscBatchRange,
1431    k: Kernel,
1432) -> Result<GatorOscBatchOutput, GatorOscError> {
1433    let combos = expand_grid_gatorosc(sweep)?;
1434    let kernel = match k {
1435        Kernel::Auto => detect_best_batch_kernel(),
1436        other if other.is_batch() => other,
1437        _ => return Err(GatorOscError::InvalidKernelForBatch(k)),
1438    };
1439    let simd = match kernel {
1440        Kernel::Avx512Batch => Kernel::Avx512,
1441        Kernel::Avx2Batch => Kernel::Avx2,
1442        Kernel::ScalarBatch => Kernel::Scalar,
1443        _ => kernel,
1444    };
1445    gatorosc_batch_inner(data, &combos, simd)
1446}
1447
1448fn expand_grid_gatorosc(r: &GatorOscBatchRange) -> Result<Vec<GatorOscParams>, GatorOscError> {
1449    fn axis((start, end, step): (usize, usize, usize)) -> Vec<usize> {
1450        if step == 0 || start == end {
1451            return vec![start];
1452        }
1453        if start < end {
1454            return (start..=end).step_by(step.max(1)).collect();
1455        }
1456
1457        let mut v = Vec::new();
1458        let mut cur = start;
1459        let s = step.max(1);
1460        while cur >= end {
1461            v.push(cur);
1462            if cur < end + s {
1463                break;
1464            }
1465            cur = cur.saturating_sub(s);
1466            if cur == usize::MAX {
1467                break;
1468            }
1469        }
1470        v
1471    }
1472
1473    let jaws_lengths = axis(r.jaws_length);
1474    if jaws_lengths.is_empty() {
1475        return Err(GatorOscError::InvalidRange {
1476            start: r.jaws_length.0,
1477            end: r.jaws_length.1,
1478            step: r.jaws_length.2,
1479        });
1480    }
1481    let jaws_shifts = axis(r.jaws_shift);
1482    if jaws_shifts.is_empty() {
1483        return Err(GatorOscError::InvalidRange {
1484            start: r.jaws_shift.0,
1485            end: r.jaws_shift.1,
1486            step: r.jaws_shift.2,
1487        });
1488    }
1489    let teeth_lengths = axis(r.teeth_length);
1490    if teeth_lengths.is_empty() {
1491        return Err(GatorOscError::InvalidRange {
1492            start: r.teeth_length.0,
1493            end: r.teeth_length.1,
1494            step: r.teeth_length.2,
1495        });
1496    }
1497    let teeth_shifts = axis(r.teeth_shift);
1498    if teeth_shifts.is_empty() {
1499        return Err(GatorOscError::InvalidRange {
1500            start: r.teeth_shift.0,
1501            end: r.teeth_shift.1,
1502            step: r.teeth_shift.2,
1503        });
1504    }
1505    let lips_lengths = axis(r.lips_length);
1506    if lips_lengths.is_empty() {
1507        return Err(GatorOscError::InvalidRange {
1508            start: r.lips_length.0,
1509            end: r.lips_length.1,
1510            step: r.lips_length.2,
1511        });
1512    }
1513    let lips_shifts = axis(r.lips_shift);
1514    if lips_shifts.is_empty() {
1515        return Err(GatorOscError::InvalidRange {
1516            start: r.lips_shift.0,
1517            end: r.lips_shift.1,
1518            step: r.lips_shift.2,
1519        });
1520    }
1521
1522    let cap = jaws_lengths
1523        .len()
1524        .checked_mul(jaws_shifts.len())
1525        .and_then(|v| v.checked_mul(teeth_lengths.len()))
1526        .and_then(|v| v.checked_mul(teeth_shifts.len()))
1527        .and_then(|v| v.checked_mul(lips_lengths.len()))
1528        .and_then(|v| v.checked_mul(lips_shifts.len()))
1529        .ok_or_else(|| GatorOscError::InvalidInput("batch sweep size overflow".into()))?;
1530
1531    let mut out = Vec::with_capacity(cap);
1532    for &jl in &jaws_lengths {
1533        for &js in &jaws_shifts {
1534            for &tl in &teeth_lengths {
1535                for &ts in &teeth_shifts {
1536                    for &ll in &lips_lengths {
1537                        for &ls in &lips_shifts {
1538                            out.push(GatorOscParams {
1539                                jaws_length: Some(jl),
1540                                jaws_shift: Some(js),
1541                                teeth_length: Some(tl),
1542                                teeth_shift: Some(ts),
1543                                lips_length: Some(ll),
1544                                lips_shift: Some(ls),
1545                            });
1546                        }
1547                    }
1548                }
1549            }
1550        }
1551    }
1552    Ok(out)
1553}
1554
1555fn gatorosc_batch_inner(
1556    data: &[f64],
1557    combos: &[GatorOscParams],
1558    kern: Kernel,
1559) -> Result<GatorOscBatchOutput, GatorOscError> {
1560    if data.is_empty() {
1561        return Err(GatorOscError::EmptyInputData);
1562    }
1563
1564    let first = data
1565        .iter()
1566        .position(|x| !x.is_nan())
1567        .ok_or(GatorOscError::AllValuesNaN)?;
1568    let max_jl = combos.iter().map(|c| c.jaws_length.unwrap()).max().unwrap();
1569    let max_js = combos.iter().map(|c| c.jaws_shift.unwrap()).max().unwrap();
1570    let max_tl = combos
1571        .iter()
1572        .map(|c| c.teeth_length.unwrap())
1573        .max()
1574        .unwrap();
1575    let max_ts = combos.iter().map(|c| c.teeth_shift.unwrap()).max().unwrap();
1576    let max_ll = combos.iter().map(|c| c.lips_length.unwrap()).max().unwrap();
1577    let max_ls = combos.iter().map(|c| c.lips_shift.unwrap()).max().unwrap();
1578    let needed = max_jl.max(max_tl).max(max_ll) + max_js.max(max_ts).max(max_ls);
1579    if data.len() - first < needed {
1580        return Err(GatorOscError::NotEnoughValidData {
1581            needed,
1582            valid: data.len() - first,
1583        });
1584    }
1585    let rows = combos.len();
1586    let cols = data.len();
1587
1588    let mut upper_mu = make_uninit_matrix(rows, cols);
1589    let mut lower_mu = make_uninit_matrix(rows, cols);
1590    let mut upper_change_mu = make_uninit_matrix(rows, cols);
1591    let mut lower_change_mu = make_uninit_matrix(rows, cols);
1592
1593    let warm_upper: Vec<usize> = combos
1594        .iter()
1595        .map(|c| {
1596            let (uw, _, _, _) = gator_warmups(
1597                first,
1598                c.jaws_length.unwrap(),
1599                c.jaws_shift.unwrap(),
1600                c.teeth_length.unwrap(),
1601                c.teeth_shift.unwrap(),
1602                c.lips_length.unwrap(),
1603                c.lips_shift.unwrap(),
1604            );
1605            uw
1606        })
1607        .collect();
1608
1609    let warm_lower: Vec<usize> = combos
1610        .iter()
1611        .map(|c| {
1612            let (_, lw, _, _) = gator_warmups(
1613                first,
1614                c.jaws_length.unwrap(),
1615                c.jaws_shift.unwrap(),
1616                c.teeth_length.unwrap(),
1617                c.teeth_shift.unwrap(),
1618                c.lips_length.unwrap(),
1619                c.lips_shift.unwrap(),
1620            );
1621            lw
1622        })
1623        .collect();
1624
1625    let warm_uc: Vec<usize> = combos
1626        .iter()
1627        .map(|c| {
1628            let (_, _, ucw, _) = gator_warmups(
1629                first,
1630                c.jaws_length.unwrap(),
1631                c.jaws_shift.unwrap(),
1632                c.teeth_length.unwrap(),
1633                c.teeth_shift.unwrap(),
1634                c.lips_length.unwrap(),
1635                c.lips_shift.unwrap(),
1636            );
1637            ucw
1638        })
1639        .collect();
1640
1641    let warm_lc: Vec<usize> = combos
1642        .iter()
1643        .map(|c| {
1644            let (_, _, _, lcw) = gator_warmups(
1645                first,
1646                c.jaws_length.unwrap(),
1647                c.jaws_shift.unwrap(),
1648                c.teeth_length.unwrap(),
1649                c.teeth_shift.unwrap(),
1650                c.lips_length.unwrap(),
1651                c.lips_shift.unwrap(),
1652            );
1653            lcw
1654        })
1655        .collect();
1656
1657    init_matrix_prefixes(&mut upper_mu, cols, &warm_upper);
1658    init_matrix_prefixes(&mut lower_mu, cols, &warm_lower);
1659    init_matrix_prefixes(&mut upper_change_mu, cols, &warm_uc);
1660    init_matrix_prefixes(&mut lower_change_mu, cols, &warm_lc);
1661
1662    let mut u_guard = core::mem::ManuallyDrop::new(upper_mu);
1663    let mut l_guard = core::mem::ManuallyDrop::new(lower_mu);
1664    let mut uc_guard = core::mem::ManuallyDrop::new(upper_change_mu);
1665    let mut lc_guard = core::mem::ManuallyDrop::new(lower_change_mu);
1666
1667    let upper: &mut [f64] =
1668        unsafe { core::slice::from_raw_parts_mut(u_guard.as_mut_ptr() as *mut f64, u_guard.len()) };
1669    let lower: &mut [f64] =
1670        unsafe { core::slice::from_raw_parts_mut(l_guard.as_mut_ptr() as *mut f64, l_guard.len()) };
1671    let upper_change: &mut [f64] = unsafe {
1672        core::slice::from_raw_parts_mut(uc_guard.as_mut_ptr() as *mut f64, uc_guard.len())
1673    };
1674    let lower_change: &mut [f64] = unsafe {
1675        core::slice::from_raw_parts_mut(lc_guard.as_mut_ptr() as *mut f64, lc_guard.len())
1676    };
1677
1678    let do_row = |row: usize, u: &mut [f64], l: &mut [f64], uc: &mut [f64], lc: &mut [f64]| {
1679        let prm = &combos[row];
1680        gatorosc_compute_into(
1681            data,
1682            prm.jaws_length.unwrap(),
1683            prm.jaws_shift.unwrap(),
1684            prm.teeth_length.unwrap(),
1685            prm.teeth_shift.unwrap(),
1686            prm.lips_length.unwrap(),
1687            prm.lips_shift.unwrap(),
1688            first,
1689            kern,
1690            u,
1691            l,
1692            uc,
1693            lc,
1694        );
1695    };
1696
1697    #[cfg(not(target_arch = "wasm32"))]
1698    {
1699        upper
1700            .par_chunks_mut(cols)
1701            .zip(lower.par_chunks_mut(cols))
1702            .zip(upper_change.par_chunks_mut(cols))
1703            .zip(lower_change.par_chunks_mut(cols))
1704            .enumerate()
1705            .for_each(|(row, (((u, l), uc), lc))| {
1706                do_row(row, u, l, uc, lc);
1707            });
1708    }
1709    #[cfg(target_arch = "wasm32")]
1710    {
1711        for row in 0..rows {
1712            let start = row * cols;
1713            let end = start + cols;
1714            do_row(
1715                row,
1716                &mut upper[start..end],
1717                &mut lower[start..end],
1718                &mut upper_change[start..end],
1719                &mut lower_change[start..end],
1720            );
1721        }
1722    }
1723
1724    let upper = unsafe {
1725        Vec::from_raw_parts(
1726            u_guard.as_mut_ptr() as *mut f64,
1727            u_guard.len(),
1728            u_guard.capacity(),
1729        )
1730    };
1731    let lower = unsafe {
1732        Vec::from_raw_parts(
1733            l_guard.as_mut_ptr() as *mut f64,
1734            l_guard.len(),
1735            l_guard.capacity(),
1736        )
1737    };
1738    let upper_change = unsafe {
1739        Vec::from_raw_parts(
1740            uc_guard.as_mut_ptr() as *mut f64,
1741            uc_guard.len(),
1742            uc_guard.capacity(),
1743        )
1744    };
1745    let lower_change = unsafe {
1746        Vec::from_raw_parts(
1747            lc_guard.as_mut_ptr() as *mut f64,
1748            lc_guard.len(),
1749            lc_guard.capacity(),
1750        )
1751    };
1752
1753    Ok(GatorOscBatchOutput {
1754        upper,
1755        lower,
1756        upper_change,
1757        lower_change,
1758        combos: combos.to_vec(),
1759        rows,
1760        cols,
1761    })
1762}
1763
1764#[inline]
1765pub fn gatorosc_batch_inner_into(
1766    data: &[f64],
1767    sweep: &GatorOscBatchRange,
1768    kernel: Kernel,
1769    parallel: bool,
1770    upper_out: &mut [f64],
1771    lower_out: &mut [f64],
1772    upper_change_out: &mut [f64],
1773    lower_change_out: &mut [f64],
1774) -> Result<Vec<GatorOscParams>, GatorOscError> {
1775    let combos = expand_grid_gatorosc(sweep)?;
1776
1777    if data.is_empty() {
1778        return Err(GatorOscError::EmptyInputData);
1779    }
1780
1781    let first = data
1782        .iter()
1783        .position(|x| !x.is_nan())
1784        .ok_or(GatorOscError::AllValuesNaN)?;
1785
1786    let rows = combos.len();
1787    let cols = data.len();
1788    let expected = rows
1789        .checked_mul(cols)
1790        .ok_or_else(|| GatorOscError::InvalidInput("rows*cols overflow".into()))?;
1791    if upper_out.len() != expected {
1792        return Err(GatorOscError::OutputLengthMismatch {
1793            expected,
1794            got: upper_out.len(),
1795        });
1796    }
1797    if lower_out.len() != expected {
1798        return Err(GatorOscError::OutputLengthMismatch {
1799            expected,
1800            got: lower_out.len(),
1801        });
1802    }
1803    if upper_change_out.len() != expected {
1804        return Err(GatorOscError::OutputLengthMismatch {
1805            expected,
1806            got: upper_change_out.len(),
1807        });
1808    }
1809    if lower_change_out.len() != expected {
1810        return Err(GatorOscError::OutputLengthMismatch {
1811            expected,
1812            got: lower_change_out.len(),
1813        });
1814    }
1815
1816    for (row, combo) in combos.iter().enumerate() {
1817        let (upper_warmup, lower_warmup, upper_change_warmup, lower_change_warmup) = gator_warmups(
1818            first,
1819            combo.jaws_length.unwrap(),
1820            combo.jaws_shift.unwrap(),
1821            combo.teeth_length.unwrap(),
1822            combo.teeth_shift.unwrap(),
1823            combo.lips_length.unwrap(),
1824            combo.lips_shift.unwrap(),
1825        );
1826
1827        let row_start = row * cols;
1828
1829        for i in 0..upper_warmup.min(cols) {
1830            upper_out[row_start + i] = f64::NAN;
1831        }
1832
1833        for i in 0..lower_warmup.min(cols) {
1834            lower_out[row_start + i] = f64::NAN;
1835        }
1836
1837        for i in 0..upper_change_warmup.min(cols) {
1838            upper_change_out[row_start + i] = f64::NAN;
1839        }
1840
1841        for i in 0..lower_change_warmup.min(cols) {
1842            lower_change_out[row_start + i] = f64::NAN;
1843        }
1844    }
1845
1846    #[cfg(not(target_arch = "wasm32"))]
1847    if parallel {
1848        use rayon::prelude::*;
1849
1850        let chunk_size = cols;
1851        let upper_chunks = upper_out.chunks_mut(chunk_size);
1852        let lower_chunks = lower_out.chunks_mut(chunk_size);
1853        let upper_change_chunks = upper_change_out.chunks_mut(chunk_size);
1854        let lower_change_chunks = lower_change_out.chunks_mut(chunk_size);
1855
1856        upper_chunks
1857            .zip(lower_chunks)
1858            .zip(upper_change_chunks)
1859            .zip(lower_change_chunks)
1860            .enumerate()
1861            .par_bridge()
1862            .for_each(|(row, (((upper, lower), upper_change), lower_change))| {
1863                let prm = &combos[row];
1864
1865                gatorosc_compute_into(
1866                    data,
1867                    prm.jaws_length.unwrap(),
1868                    prm.jaws_shift.unwrap(),
1869                    prm.teeth_length.unwrap(),
1870                    prm.teeth_shift.unwrap(),
1871                    prm.lips_length.unwrap(),
1872                    prm.lips_shift.unwrap(),
1873                    first,
1874                    kernel,
1875                    upper,
1876                    lower,
1877                    upper_change,
1878                    lower_change,
1879                );
1880            });
1881    } else {
1882        for row in 0..rows {
1883            let prm = &combos[row];
1884            let start = row * cols;
1885            let end = start + cols;
1886
1887            gatorosc_compute_into(
1888                data,
1889                prm.jaws_length.unwrap(),
1890                prm.jaws_shift.unwrap(),
1891                prm.teeth_length.unwrap(),
1892                prm.teeth_shift.unwrap(),
1893                prm.lips_length.unwrap(),
1894                prm.lips_shift.unwrap(),
1895                first,
1896                kernel,
1897                &mut upper_out[start..end],
1898                &mut lower_out[start..end],
1899                &mut upper_change_out[start..end],
1900                &mut lower_change_out[start..end],
1901            );
1902        }
1903    }
1904
1905    Ok(combos)
1906}
1907
1908#[inline(always)]
1909unsafe fn gatorosc_row_scalar(
1910    data: &[f64],
1911    first: usize,
1912    jaws_length: usize,
1913    jaws_shift: usize,
1914    teeth_length: usize,
1915    teeth_shift: usize,
1916    lips_length: usize,
1917    lips_shift: usize,
1918    upper: &mut [f64],
1919    lower: &mut [f64],
1920    upper_change: &mut [f64],
1921    lower_change: &mut [f64],
1922) {
1923    gatorosc_scalar(
1924        data,
1925        jaws_length,
1926        jaws_shift,
1927        teeth_length,
1928        teeth_shift,
1929        lips_length,
1930        lips_shift,
1931        first,
1932        upper,
1933        lower,
1934        upper_change,
1935        lower_change,
1936    );
1937}
1938
1939#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1940#[inline(always)]
1941unsafe fn gatorosc_row_avx2(
1942    data: &[f64],
1943    first: usize,
1944    jaws_length: usize,
1945    jaws_shift: usize,
1946    teeth_length: usize,
1947    teeth_shift: usize,
1948    lips_length: usize,
1949    lips_shift: usize,
1950    upper: &mut [f64],
1951    lower: &mut [f64],
1952    upper_change: &mut [f64],
1953    lower_change: &mut [f64],
1954) {
1955    gatorosc_row_scalar(
1956        data,
1957        first,
1958        jaws_length,
1959        jaws_shift,
1960        teeth_length,
1961        teeth_shift,
1962        lips_length,
1963        lips_shift,
1964        upper,
1965        lower,
1966        upper_change,
1967        lower_change,
1968    );
1969}
1970
1971#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1972#[inline(always)]
1973unsafe fn gatorosc_row_avx512(
1974    data: &[f64],
1975    first: usize,
1976    jaws_length: usize,
1977    jaws_shift: usize,
1978    teeth_length: usize,
1979    teeth_shift: usize,
1980    lips_length: usize,
1981    lips_shift: usize,
1982    upper: &mut [f64],
1983    lower: &mut [f64],
1984    upper_change: &mut [f64],
1985    lower_change: &mut [f64],
1986) {
1987    gatorosc_row_scalar(
1988        data,
1989        first,
1990        jaws_length,
1991        jaws_shift,
1992        teeth_length,
1993        teeth_shift,
1994        lips_length,
1995        lips_shift,
1996        upper,
1997        lower,
1998        upper_change,
1999        lower_change,
2000    );
2001}
2002
2003#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2004#[inline(always)]
2005unsafe fn gatorosc_row_avx512_short(
2006    data: &[f64],
2007    first: usize,
2008    jaws_length: usize,
2009    jaws_shift: usize,
2010    teeth_length: usize,
2011    teeth_shift: usize,
2012    lips_length: usize,
2013    lips_shift: usize,
2014    upper: &mut [f64],
2015    lower: &mut [f64],
2016    upper_change: &mut [f64],
2017    lower_change: &mut [f64],
2018) {
2019    gatorosc_row_scalar(
2020        data,
2021        first,
2022        jaws_length,
2023        jaws_shift,
2024        teeth_length,
2025        teeth_shift,
2026        lips_length,
2027        lips_shift,
2028        upper,
2029        lower,
2030        upper_change,
2031        lower_change,
2032    );
2033}
2034
2035#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2036#[inline(always)]
2037unsafe fn gatorosc_row_avx512_long(
2038    data: &[f64],
2039    first: usize,
2040    jaws_length: usize,
2041    jaws_shift: usize,
2042    teeth_length: usize,
2043    teeth_shift: usize,
2044    lips_length: usize,
2045    lips_shift: usize,
2046    upper: &mut [f64],
2047    lower: &mut [f64],
2048    upper_change: &mut [f64],
2049    lower_change: &mut [f64],
2050) {
2051    gatorosc_row_scalar(
2052        data,
2053        first,
2054        jaws_length,
2055        jaws_shift,
2056        teeth_length,
2057        teeth_shift,
2058        lips_length,
2059        lips_shift,
2060        upper,
2061        lower,
2062        upper_change,
2063        lower_change,
2064    );
2065}
2066
2067#[inline(always)]
2068pub fn gatorosc_batch_slice(
2069    data: &[f64],
2070    sweep: &GatorOscBatchRange,
2071    kern: Kernel,
2072) -> Result<GatorOscBatchOutput, GatorOscError> {
2073    let combos = expand_grid_gatorosc(sweep)?;
2074    gatorosc_batch_inner(data, &combos, kern)
2075}
2076
2077#[inline(always)]
2078pub fn gatorosc_batch_par_slice(
2079    data: &[f64],
2080    sweep: &GatorOscBatchRange,
2081    kern: Kernel,
2082) -> Result<GatorOscBatchOutput, GatorOscError> {
2083    let combos = expand_grid_gatorosc(sweep)?;
2084
2085    if data.is_empty() {
2086        return Err(GatorOscError::EmptyInputData);
2087    }
2088
2089    let first = data
2090        .iter()
2091        .position(|x| !x.is_nan())
2092        .ok_or(GatorOscError::AllValuesNaN)?;
2093    let rows = combos.len();
2094    let cols = data.len();
2095
2096    let mut upper_mu = make_uninit_matrix(rows, cols);
2097    let mut lower_mu = make_uninit_matrix(rows, cols);
2098    let mut upper_change_mu = make_uninit_matrix(rows, cols);
2099    let mut lower_change_mu = make_uninit_matrix(rows, cols);
2100
2101    let warm_upper: Vec<usize> = combos
2102        .iter()
2103        .map(|c| {
2104            let (uw, _, _, _) = gator_warmups(
2105                first,
2106                c.jaws_length.unwrap(),
2107                c.jaws_shift.unwrap(),
2108                c.teeth_length.unwrap(),
2109                c.teeth_shift.unwrap(),
2110                c.lips_length.unwrap(),
2111                c.lips_shift.unwrap(),
2112            );
2113            uw
2114        })
2115        .collect();
2116
2117    let warm_lower: Vec<usize> = combos
2118        .iter()
2119        .map(|c| {
2120            let (_, lw, _, _) = gator_warmups(
2121                first,
2122                c.jaws_length.unwrap(),
2123                c.jaws_shift.unwrap(),
2124                c.teeth_length.unwrap(),
2125                c.teeth_shift.unwrap(),
2126                c.lips_length.unwrap(),
2127                c.lips_shift.unwrap(),
2128            );
2129            lw
2130        })
2131        .collect();
2132
2133    let warm_uc: Vec<usize> = combos
2134        .iter()
2135        .map(|c| {
2136            let (_, _, ucw, _) = gator_warmups(
2137                first,
2138                c.jaws_length.unwrap(),
2139                c.jaws_shift.unwrap(),
2140                c.teeth_length.unwrap(),
2141                c.teeth_shift.unwrap(),
2142                c.lips_length.unwrap(),
2143                c.lips_shift.unwrap(),
2144            );
2145            ucw
2146        })
2147        .collect();
2148
2149    let warm_lc: Vec<usize> = combos
2150        .iter()
2151        .map(|c| {
2152            let (_, _, _, lcw) = gator_warmups(
2153                first,
2154                c.jaws_length.unwrap(),
2155                c.jaws_shift.unwrap(),
2156                c.teeth_length.unwrap(),
2157                c.teeth_shift.unwrap(),
2158                c.lips_length.unwrap(),
2159                c.lips_shift.unwrap(),
2160            );
2161            lcw
2162        })
2163        .collect();
2164
2165    init_matrix_prefixes(&mut upper_mu, cols, &warm_upper);
2166    init_matrix_prefixes(&mut lower_mu, cols, &warm_lower);
2167    init_matrix_prefixes(&mut upper_change_mu, cols, &warm_uc);
2168    init_matrix_prefixes(&mut lower_change_mu, cols, &warm_lc);
2169
2170    let mut u_guard = core::mem::ManuallyDrop::new(upper_mu);
2171    let mut l_guard = core::mem::ManuallyDrop::new(lower_mu);
2172    let mut uc_guard = core::mem::ManuallyDrop::new(upper_change_mu);
2173    let mut lc_guard = core::mem::ManuallyDrop::new(lower_change_mu);
2174
2175    let upper: &mut [f64] =
2176        unsafe { core::slice::from_raw_parts_mut(u_guard.as_mut_ptr() as *mut f64, u_guard.len()) };
2177    let lower: &mut [f64] =
2178        unsafe { core::slice::from_raw_parts_mut(l_guard.as_mut_ptr() as *mut f64, l_guard.len()) };
2179    let upper_change: &mut [f64] = unsafe {
2180        core::slice::from_raw_parts_mut(uc_guard.as_mut_ptr() as *mut f64, uc_guard.len())
2181    };
2182    let lower_change: &mut [f64] = unsafe {
2183        core::slice::from_raw_parts_mut(lc_guard.as_mut_ptr() as *mut f64, lc_guard.len())
2184    };
2185    #[cfg(not(target_arch = "wasm32"))]
2186    use rayon::prelude::*;
2187
2188    #[cfg(not(target_arch = "wasm32"))]
2189    {
2190        upper
2191            .par_chunks_mut(cols)
2192            .zip(lower.par_chunks_mut(cols))
2193            .zip(upper_change.par_chunks_mut(cols))
2194            .zip(lower_change.par_chunks_mut(cols))
2195            .enumerate()
2196            .for_each(|(row, (((u, l), uc), lc))| {
2197                let prm = &combos[row];
2198                unsafe {
2199                    gatorosc_row_scalar(
2200                        data,
2201                        first,
2202                        prm.jaws_length.unwrap(),
2203                        prm.jaws_shift.unwrap(),
2204                        prm.teeth_length.unwrap(),
2205                        prm.teeth_shift.unwrap(),
2206                        prm.lips_length.unwrap(),
2207                        prm.lips_shift.unwrap(),
2208                        u,
2209                        l,
2210                        uc,
2211                        lc,
2212                    );
2213                }
2214            });
2215    }
2216    #[cfg(target_arch = "wasm32")]
2217    {
2218        for row in 0..rows {
2219            let start = row * cols;
2220            let end = start + cols;
2221            let prm = &combos[row];
2222            unsafe {
2223                gatorosc_row_scalar(
2224                    data,
2225                    first,
2226                    prm.jaws_length.unwrap(),
2227                    prm.jaws_shift.unwrap(),
2228                    prm.teeth_length.unwrap(),
2229                    prm.teeth_shift.unwrap(),
2230                    prm.lips_length.unwrap(),
2231                    prm.lips_shift.unwrap(),
2232                    &mut upper[start..end],
2233                    &mut lower[start..end],
2234                    &mut upper_change[start..end],
2235                    &mut lower_change[start..end],
2236                );
2237            }
2238        }
2239    }
2240
2241    let upper = unsafe {
2242        Vec::from_raw_parts(
2243            u_guard.as_mut_ptr() as *mut f64,
2244            u_guard.len(),
2245            u_guard.capacity(),
2246        )
2247    };
2248    let lower = unsafe {
2249        Vec::from_raw_parts(
2250            l_guard.as_mut_ptr() as *mut f64,
2251            l_guard.len(),
2252            l_guard.capacity(),
2253        )
2254    };
2255    let upper_change = unsafe {
2256        Vec::from_raw_parts(
2257            uc_guard.as_mut_ptr() as *mut f64,
2258            uc_guard.len(),
2259            uc_guard.capacity(),
2260        )
2261    };
2262    let lower_change = unsafe {
2263        Vec::from_raw_parts(
2264            lc_guard.as_mut_ptr() as *mut f64,
2265            lc_guard.len(),
2266            lc_guard.capacity(),
2267        )
2268    };
2269
2270    Ok(GatorOscBatchOutput {
2271        upper,
2272        lower,
2273        upper_change,
2274        lower_change,
2275        combos,
2276        rows,
2277        cols,
2278    })
2279}
2280
2281#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2282use serde::{Deserialize, Serialize};
2283#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2284use wasm_bindgen::prelude::*;
2285
2286#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2287#[derive(Serialize, Deserialize)]
2288pub struct GatorOscJsOutput {
2289    pub values: Vec<f64>,
2290    pub rows: usize,
2291    pub cols: usize,
2292}
2293
2294#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2295#[wasm_bindgen]
2296pub fn gatorosc_js(
2297    data: &[f64],
2298    jaws_length: usize,
2299    jaws_shift: usize,
2300    teeth_length: usize,
2301    teeth_shift: usize,
2302    lips_length: usize,
2303    lips_shift: usize,
2304) -> Result<JsValue, JsValue> {
2305    let params = GatorOscParams {
2306        jaws_length: Some(jaws_length),
2307        jaws_shift: Some(jaws_shift),
2308        teeth_length: Some(teeth_length),
2309        teeth_shift: Some(teeth_shift),
2310        lips_length: Some(lips_length),
2311        lips_shift: Some(lips_shift),
2312    };
2313    let input = GatorOscInput::from_slice(data, params);
2314
2315    let len = data.len();
2316    let mut values = vec![0.0; 4 * len];
2317
2318    let (upper_part, rest) = values.split_at_mut(len);
2319    let (lower_part, rest) = rest.split_at_mut(len);
2320    let (upper_change_part, lower_change_part) = rest.split_at_mut(len);
2321
2322    gatorosc_into_slice(
2323        upper_part,
2324        lower_part,
2325        upper_change_part,
2326        lower_change_part,
2327        &input,
2328        Kernel::Auto,
2329    )
2330    .map_err(|e| JsValue::from_str(&e.to_string()))?;
2331
2332    let output = GatorOscJsOutput {
2333        values,
2334        rows: 4,
2335        cols: len,
2336    };
2337
2338    serde_wasm_bindgen::to_value(&output)
2339        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2340}
2341
2342#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2343#[wasm_bindgen]
2344pub fn gatorosc_alloc(len: usize) -> *mut f64 {
2345    let mut vec = Vec::<f64>::with_capacity(len);
2346    let ptr = vec.as_mut_ptr();
2347    std::mem::forget(vec);
2348    ptr
2349}
2350
2351#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2352#[wasm_bindgen]
2353pub fn gatorosc_free(ptr: *mut f64, len: usize) {
2354    if !ptr.is_null() {
2355        unsafe {
2356            let _ = Vec::from_raw_parts(ptr, len, len);
2357        }
2358    }
2359}
2360
2361#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2362#[wasm_bindgen]
2363pub fn gatorosc_into(
2364    in_ptr: *const f64,
2365    upper_ptr: *mut f64,
2366    lower_ptr: *mut f64,
2367    upper_change_ptr: *mut f64,
2368    lower_change_ptr: *mut f64,
2369    len: usize,
2370    jaws_length: usize,
2371    jaws_shift: usize,
2372    teeth_length: usize,
2373    teeth_shift: usize,
2374    lips_length: usize,
2375    lips_shift: usize,
2376) -> Result<(), JsValue> {
2377    if in_ptr.is_null()
2378        || upper_ptr.is_null()
2379        || lower_ptr.is_null()
2380        || upper_change_ptr.is_null()
2381        || lower_change_ptr.is_null()
2382    {
2383        return Err(JsValue::from_str("Null pointer provided"));
2384    }
2385
2386    unsafe {
2387        let data = std::slice::from_raw_parts(in_ptr, len);
2388        let params = GatorOscParams {
2389            jaws_length: Some(jaws_length),
2390            jaws_shift: Some(jaws_shift),
2391            teeth_length: Some(teeth_length),
2392            teeth_shift: Some(teeth_shift),
2393            lips_length: Some(lips_length),
2394            lips_shift: Some(lips_shift),
2395        };
2396        let input = GatorOscInput::from_slice(data, params);
2397
2398        let needs_temp = in_ptr == upper_ptr as *const f64
2399            || in_ptr == lower_ptr as *const f64
2400            || in_ptr == upper_change_ptr as *const f64
2401            || in_ptr == lower_change_ptr as *const f64;
2402
2403        if needs_temp {
2404            let mut temp = vec![0.0; 4 * len];
2405
2406            let (temp_upper, rest) = temp.split_at_mut(len);
2407            let (temp_lower, rest) = rest.split_at_mut(len);
2408            let (temp_upper_change, temp_lower_change) = rest.split_at_mut(len);
2409
2410            gatorosc_into_slice(
2411                temp_upper,
2412                temp_lower,
2413                temp_upper_change,
2414                temp_lower_change,
2415                &input,
2416                Kernel::Auto,
2417            )
2418            .map_err(|e| JsValue::from_str(&e.to_string()))?;
2419
2420            let upper_out = std::slice::from_raw_parts_mut(upper_ptr, len);
2421            let lower_out = std::slice::from_raw_parts_mut(lower_ptr, len);
2422            let upper_change_out = std::slice::from_raw_parts_mut(upper_change_ptr, len);
2423            let lower_change_out = std::slice::from_raw_parts_mut(lower_change_ptr, len);
2424
2425            upper_out.copy_from_slice(temp_upper);
2426            lower_out.copy_from_slice(temp_lower);
2427            upper_change_out.copy_from_slice(temp_upper_change);
2428            lower_change_out.copy_from_slice(temp_lower_change);
2429        } else {
2430            let upper_out = std::slice::from_raw_parts_mut(upper_ptr, len);
2431            let lower_out = std::slice::from_raw_parts_mut(lower_ptr, len);
2432            let upper_change_out = std::slice::from_raw_parts_mut(upper_change_ptr, len);
2433            let lower_change_out = std::slice::from_raw_parts_mut(lower_change_ptr, len);
2434
2435            gatorosc_into_slice(
2436                upper_out,
2437                lower_out,
2438                upper_change_out,
2439                lower_change_out,
2440                &input,
2441                Kernel::Auto,
2442            )
2443            .map_err(|e| JsValue::from_str(&e.to_string()))?;
2444        }
2445
2446        Ok(())
2447    }
2448}
2449
2450#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2451#[derive(Serialize, Deserialize)]
2452pub struct GatorOscBatchConfig {
2453    pub jaws_length_range: (usize, usize, usize),
2454    pub jaws_shift_range: (usize, usize, usize),
2455    pub teeth_length_range: (usize, usize, usize),
2456    pub teeth_shift_range: (usize, usize, usize),
2457    pub lips_length_range: (usize, usize, usize),
2458    pub lips_shift_range: (usize, usize, usize),
2459}
2460
2461#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2462#[derive(Serialize, Deserialize)]
2463pub struct GatorOscBatchJsOutput {
2464    pub values: Vec<f64>,
2465    pub combos: Vec<GatorOscParams>,
2466    pub rows: usize,
2467    pub cols: usize,
2468    pub outputs: usize,
2469}
2470
2471#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2472#[wasm_bindgen(js_name = gatorosc_batch)]
2473pub fn gatorosc_batch_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
2474    let config: GatorOscBatchConfig = serde_wasm_bindgen::from_value(config)
2475        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
2476
2477    let sweep = GatorOscBatchRange {
2478        jaws_length: config.jaws_length_range,
2479        jaws_shift: config.jaws_shift_range,
2480        teeth_length: config.teeth_length_range,
2481        teeth_shift: config.teeth_shift_range,
2482        lips_length: config.lips_length_range,
2483        lips_shift: config.lips_shift_range,
2484    };
2485
2486    let combos = expand_grid_gatorosc(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2487    let n_combos = combos.len();
2488    let len = data.len();
2489
2490    let total_size = n_combos
2491        .checked_mul(len)
2492        .ok_or_else(|| JsValue::from_str("gatorosc_batch_js: rows*cols overflow"))?;
2493    let slots = total_size
2494        .checked_mul(4)
2495        .ok_or_else(|| JsValue::from_str("gatorosc_batch_js: output size overflow"))?;
2496    let mut values = vec![0.0; slots];
2497
2498    let (upper_part, rest) = values.split_at_mut(total_size);
2499    let (lower_part, rest) = rest.split_at_mut(total_size);
2500    let (upper_change_part, lower_change_part) = rest.split_at_mut(total_size);
2501
2502    gatorosc_batch_inner_into(
2503        data,
2504        &sweep,
2505        Kernel::Auto,
2506        false,
2507        upper_part,
2508        lower_part,
2509        upper_change_part,
2510        lower_change_part,
2511    )
2512    .map_err(|e| JsValue::from_str(&e.to_string()))?;
2513
2514    let js_output = GatorOscBatchJsOutput {
2515        values,
2516        combos,
2517        rows: n_combos,
2518        cols: len,
2519        outputs: 4,
2520    };
2521
2522    serde_wasm_bindgen::to_value(&js_output)
2523        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2524}
2525
2526#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2527#[wasm_bindgen]
2528pub fn gatorosc_batch_into(
2529    in_ptr: *const f64,
2530    upper_ptr: *mut f64,
2531    lower_ptr: *mut f64,
2532    upper_change_ptr: *mut f64,
2533    lower_change_ptr: *mut f64,
2534    len: usize,
2535    jaws_length_start: usize,
2536    jaws_length_end: usize,
2537    jaws_length_step: usize,
2538    jaws_shift_start: usize,
2539    jaws_shift_end: usize,
2540    jaws_shift_step: usize,
2541    teeth_length_start: usize,
2542    teeth_length_end: usize,
2543    teeth_length_step: usize,
2544    teeth_shift_start: usize,
2545    teeth_shift_end: usize,
2546    teeth_shift_step: usize,
2547    lips_length_start: usize,
2548    lips_length_end: usize,
2549    lips_length_step: usize,
2550    lips_shift_start: usize,
2551    lips_shift_end: usize,
2552    lips_shift_step: usize,
2553) -> Result<usize, JsValue> {
2554    if in_ptr.is_null()
2555        || upper_ptr.is_null()
2556        || lower_ptr.is_null()
2557        || upper_change_ptr.is_null()
2558        || lower_change_ptr.is_null()
2559    {
2560        return Err(JsValue::from_str("Null pointer provided"));
2561    }
2562
2563    unsafe {
2564        let data = std::slice::from_raw_parts(in_ptr, len);
2565        let sweep = GatorOscBatchRange {
2566            jaws_length: (jaws_length_start, jaws_length_end, jaws_length_step),
2567            jaws_shift: (jaws_shift_start, jaws_shift_end, jaws_shift_step),
2568            teeth_length: (teeth_length_start, teeth_length_end, teeth_length_step),
2569            teeth_shift: (teeth_shift_start, teeth_shift_end, teeth_shift_step),
2570            lips_length: (lips_length_start, lips_length_end, lips_length_step),
2571            lips_shift: (lips_shift_start, lips_shift_end, lips_shift_step),
2572        };
2573
2574        let combos = expand_grid_gatorosc(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2575        let n_combos = combos.len();
2576        let total_size = n_combos
2577            .checked_mul(len)
2578            .ok_or_else(|| JsValue::from_str("gatorosc_batch_into: rows*cols overflow"))?;
2579
2580        let upper_out = std::slice::from_raw_parts_mut(upper_ptr, total_size);
2581        let lower_out = std::slice::from_raw_parts_mut(lower_ptr, total_size);
2582        let upper_change_out = std::slice::from_raw_parts_mut(upper_change_ptr, total_size);
2583        let lower_change_out = std::slice::from_raw_parts_mut(lower_change_ptr, total_size);
2584
2585        gatorosc_batch_inner_into(
2586            data,
2587            &sweep,
2588            Kernel::Auto,
2589            false,
2590            upper_out,
2591            lower_out,
2592            upper_change_out,
2593            lower_change_out,
2594        )
2595        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2596
2597        Ok(n_combos)
2598    }
2599}
2600
2601#[cfg(test)]
2602mod tests {
2603    use super::*;
2604    use crate::skip_if_unsupported;
2605    use crate::utilities::data_loader::read_candles_from_csv;
2606    #[cfg(feature = "proptest")]
2607    use proptest::prelude::*;
2608
2609    fn check_gatorosc_partial_params(
2610        test_name: &str,
2611        kernel: Kernel,
2612    ) -> Result<(), Box<dyn std::error::Error>> {
2613        skip_if_unsupported!(kernel, test_name);
2614        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2615        let candles = read_candles_from_csv(file_path)?;
2616        let default_params = GatorOscParams::default();
2617        let input = GatorOscInput::from_candles(&candles, "close", default_params);
2618        let output = gatorosc_with_kernel(&input, kernel)?;
2619        assert_eq!(output.upper.len(), candles.close.len());
2620        Ok(())
2621    }
2622
2623    #[test]
2624    #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
2625    fn test_gatorosc_into_matches_api() -> Result<(), Box<dyn std::error::Error>> {
2626        let len = 256;
2627        let mut data = vec![0.0_f64; len];
2628        for i in 0..len {
2629            data[i] = (i as f64).sin() * 10.0 + ((i % 7) as f64) * 0.25;
2630        }
2631
2632        if len >= 3 {
2633            data[0] = f64::NAN;
2634            data[1] = f64::NAN;
2635            data[2] = f64::NAN;
2636        }
2637
2638        let input = GatorOscInput::from_slice(&data, GatorOscParams::default());
2639
2640        let baseline = gatorosc(&input)?;
2641
2642        let mut up = vec![0.0; len];
2643        let mut lo = vec![0.0; len];
2644        let mut upc = vec![0.0; len];
2645        let mut loc = vec![0.0; len];
2646
2647        gatorosc_into(&input, &mut up, &mut lo, &mut upc, &mut loc)?;
2648
2649        assert_eq!(baseline.upper.len(), len);
2650        assert_eq!(baseline.lower.len(), len);
2651        assert_eq!(baseline.upper_change.len(), len);
2652        assert_eq!(baseline.lower_change.len(), len);
2653
2654        fn eq_or_both_nan(a: f64, b: f64) -> bool {
2655            (a.is_nan() && b.is_nan()) || (a == b)
2656        }
2657
2658        for i in 0..len {
2659            assert!(
2660                eq_or_both_nan(baseline.upper[i], up[i]),
2661                "upper mismatch at {}: {:?} vs {:?}",
2662                i,
2663                baseline.upper[i],
2664                up[i]
2665            );
2666            assert!(
2667                eq_or_both_nan(baseline.lower[i], lo[i]),
2668                "lower mismatch at {}: {:?} vs {:?}",
2669                i,
2670                baseline.lower[i],
2671                lo[i]
2672            );
2673            assert!(
2674                eq_or_both_nan(baseline.upper_change[i], upc[i]),
2675                "upper_change mismatch at {}: {:?} vs {:?}",
2676                i,
2677                baseline.upper_change[i],
2678                upc[i]
2679            );
2680            assert!(
2681                eq_or_both_nan(baseline.lower_change[i], loc[i]),
2682                "lower_change mismatch at {}: {:?} vs {:?}",
2683                i,
2684                baseline.lower_change[i],
2685                loc[i]
2686            );
2687        }
2688
2689        Ok(())
2690    }
2691
2692    fn check_gatorosc_nan_handling(
2693        test_name: &str,
2694        kernel: Kernel,
2695    ) -> Result<(), Box<dyn std::error::Error>> {
2696        skip_if_unsupported!(kernel, test_name);
2697        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2698        let candles = read_candles_from_csv(file_path)?;
2699        let input = GatorOscInput::from_candles(&candles, "close", GatorOscParams::default());
2700        let output = gatorosc_with_kernel(&input, kernel)?;
2701        assert_eq!(output.upper.len(), candles.close.len());
2702        if output.upper.len() > 24 {
2703            for &val in &output.upper[24..] {
2704                assert!(!val.is_nan(), "Found unexpected NaN in upper");
2705            }
2706        }
2707        Ok(())
2708    }
2709
2710    fn check_gatorosc_zero_setting(
2711        test_name: &str,
2712        kernel: Kernel,
2713    ) -> Result<(), Box<dyn std::error::Error>> {
2714        skip_if_unsupported!(kernel, test_name);
2715        let data = [10.0, 20.0, 30.0];
2716        let params = GatorOscParams {
2717            jaws_length: Some(0),
2718            ..Default::default()
2719        };
2720        let input = GatorOscInput::from_slice(&data, params);
2721        let res = gatorosc_with_kernel(&input, kernel);
2722        assert!(
2723            res.is_err(),
2724            "[{}] GatorOsc should fail with zero setting",
2725            test_name
2726        );
2727        Ok(())
2728    }
2729
2730    fn check_gatorosc_small_dataset(
2731        test_name: &str,
2732        kernel: Kernel,
2733    ) -> Result<(), Box<dyn std::error::Error>> {
2734        skip_if_unsupported!(kernel, test_name);
2735        let single = [42.0];
2736        let params = GatorOscParams::default();
2737        let input = GatorOscInput::from_slice(&single, params);
2738        let res = gatorosc_with_kernel(&input, kernel);
2739        assert!(
2740            res.is_err(),
2741            "[{}] GatorOsc should fail with insufficient data",
2742            test_name
2743        );
2744        Ok(())
2745    }
2746
2747    fn check_gatorosc_default_candles(
2748        test_name: &str,
2749        kernel: Kernel,
2750    ) -> Result<(), Box<dyn std::error::Error>> {
2751        skip_if_unsupported!(kernel, test_name);
2752        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2753        let candles = read_candles_from_csv(file_path)?;
2754        let input = GatorOscInput::with_default_candles(&candles);
2755        let output = gatorosc_with_kernel(&input, kernel)?;
2756        assert_eq!(output.upper.len(), candles.close.len());
2757        Ok(())
2758    }
2759
2760    fn check_gatorosc_batch_default_row(
2761        test_name: &str,
2762        kernel: Kernel,
2763    ) -> Result<(), Box<dyn std::error::Error>> {
2764        skip_if_unsupported!(kernel, test_name);
2765        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2766        let c = read_candles_from_csv(file)?;
2767        let output = GatorOscBatchBuilder::new()
2768            .kernel(kernel)
2769            .apply_slice(&c.close)?;
2770        let def = GatorOscParams::default();
2771        let row = output
2772            .combos
2773            .iter()
2774            .position(|p| {
2775                p.jaws_length.unwrap_or(13) == def.jaws_length.unwrap_or(13)
2776                    && p.jaws_shift.unwrap_or(8) == def.jaws_shift.unwrap_or(8)
2777                    && p.teeth_length.unwrap_or(8) == def.teeth_length.unwrap_or(8)
2778                    && p.teeth_shift.unwrap_or(5) == def.teeth_shift.unwrap_or(5)
2779                    && p.lips_length.unwrap_or(5) == def.lips_length.unwrap_or(5)
2780                    && p.lips_shift.unwrap_or(3) == def.lips_shift.unwrap_or(3)
2781            })
2782            .expect("default row missing");
2783        let u = &output.upper[row * output.cols..][..output.cols];
2784        assert_eq!(u.len(), c.close.len());
2785        Ok(())
2786    }
2787
2788    #[cfg(debug_assertions)]
2789    fn check_gatorosc_no_poison(
2790        test_name: &str,
2791        kernel: Kernel,
2792    ) -> Result<(), Box<dyn std::error::Error>> {
2793        skip_if_unsupported!(kernel, test_name);
2794
2795        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2796        let candles = read_candles_from_csv(file_path)?;
2797
2798        let test_params = vec![
2799            GatorOscParams::default(),
2800            GatorOscParams {
2801                jaws_length: Some(2),
2802                jaws_shift: Some(0),
2803                teeth_length: Some(2),
2804                teeth_shift: Some(0),
2805                lips_length: Some(2),
2806                lips_shift: Some(0),
2807            },
2808            GatorOscParams {
2809                jaws_length: Some(5),
2810                jaws_shift: Some(2),
2811                teeth_length: Some(4),
2812                teeth_shift: Some(1),
2813                lips_length: Some(3),
2814                lips_shift: Some(1),
2815            },
2816            GatorOscParams {
2817                jaws_length: Some(20),
2818                jaws_shift: Some(10),
2819                teeth_length: Some(15),
2820                teeth_shift: Some(8),
2821                lips_length: Some(10),
2822                lips_shift: Some(5),
2823            },
2824            GatorOscParams {
2825                jaws_length: Some(50),
2826                jaws_shift: Some(20),
2827                teeth_length: Some(30),
2828                teeth_shift: Some(15),
2829                lips_length: Some(20),
2830                lips_shift: Some(10),
2831            },
2832            GatorOscParams {
2833                jaws_length: Some(5),
2834                jaws_shift: Some(3),
2835                teeth_length: Some(8),
2836                teeth_shift: Some(5),
2837                lips_length: Some(13),
2838                lips_shift: Some(8),
2839            },
2840            GatorOscParams {
2841                jaws_length: Some(10),
2842                jaws_shift: Some(5),
2843                teeth_length: Some(10),
2844                teeth_shift: Some(5),
2845                lips_length: Some(10),
2846                lips_shift: Some(5),
2847            },
2848            GatorOscParams {
2849                jaws_length: Some(13),
2850                jaws_shift: Some(0),
2851                teeth_length: Some(8),
2852                teeth_shift: Some(0),
2853                lips_length: Some(5),
2854                lips_shift: Some(0),
2855            },
2856            GatorOscParams {
2857                jaws_length: Some(10),
2858                jaws_shift: Some(20),
2859                teeth_length: Some(8),
2860                teeth_shift: Some(15),
2861                lips_length: Some(5),
2862                lips_shift: Some(10),
2863            },
2864        ];
2865
2866        for (param_idx, params) in test_params.iter().enumerate() {
2867            let input = GatorOscInput::from_candles(&candles, "close", params.clone());
2868            let output = gatorosc_with_kernel(&input, kernel)?;
2869
2870            for (i, &val) in output.upper.iter().enumerate() {
2871                if val.is_nan() {
2872                    continue;
2873                }
2874
2875                let bits = val.to_bits();
2876
2877                if bits == 0x11111111_11111111 {
2878                    panic!(
2879                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
2880						 in upper output with params: {:?} (param set {})",
2881                        test_name, val, bits, i, params, param_idx
2882                    );
2883                }
2884
2885                if bits == 0x22222222_22222222 {
2886                    panic!(
2887                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
2888						 in upper output with params: {:?} (param set {})",
2889                        test_name, val, bits, i, params, param_idx
2890                    );
2891                }
2892
2893                if bits == 0x33333333_33333333 {
2894                    panic!(
2895                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
2896						 in upper output with params: {:?} (param set {})",
2897                        test_name, val, bits, i, params, param_idx
2898                    );
2899                }
2900            }
2901
2902            for (i, &val) in output.lower.iter().enumerate() {
2903                if val.is_nan() {
2904                    continue;
2905                }
2906
2907                let bits = val.to_bits();
2908
2909                if bits == 0x11111111_11111111 {
2910                    panic!(
2911                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
2912						 in lower output with params: {:?} (param set {})",
2913                        test_name, val, bits, i, params, param_idx
2914                    );
2915                }
2916
2917                if bits == 0x22222222_22222222 {
2918                    panic!(
2919                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
2920						 in lower output with params: {:?} (param set {})",
2921                        test_name, val, bits, i, params, param_idx
2922                    );
2923                }
2924
2925                if bits == 0x33333333_33333333 {
2926                    panic!(
2927                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
2928						 in lower output with params: {:?} (param set {})",
2929                        test_name, val, bits, i, params, param_idx
2930                    );
2931                }
2932            }
2933
2934            for (i, &val) in output.upper_change.iter().enumerate() {
2935                if val.is_nan() {
2936                    continue;
2937                }
2938
2939                let bits = val.to_bits();
2940
2941                if bits == 0x11111111_11111111 {
2942                    panic!(
2943                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
2944						 in upper_change output with params: {:?} (param set {})",
2945                        test_name, val, bits, i, params, param_idx
2946                    );
2947                }
2948
2949                if bits == 0x22222222_22222222 {
2950                    panic!(
2951                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
2952						 in upper_change output with params: {:?} (param set {})",
2953                        test_name, val, bits, i, params, param_idx
2954                    );
2955                }
2956
2957                if bits == 0x33333333_33333333 {
2958                    panic!(
2959                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
2960						 in upper_change output with params: {:?} (param set {})",
2961                        test_name, val, bits, i, params, param_idx
2962                    );
2963                }
2964            }
2965
2966            for (i, &val) in output.lower_change.iter().enumerate() {
2967                if val.is_nan() {
2968                    continue;
2969                }
2970
2971                let bits = val.to_bits();
2972
2973                if bits == 0x11111111_11111111 {
2974                    panic!(
2975                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
2976						 in lower_change output with params: {:?} (param set {})",
2977                        test_name, val, bits, i, params, param_idx
2978                    );
2979                }
2980
2981                if bits == 0x22222222_22222222 {
2982                    panic!(
2983                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
2984						 in lower_change output with params: {:?} (param set {})",
2985                        test_name, val, bits, i, params, param_idx
2986                    );
2987                }
2988
2989                if bits == 0x33333333_33333333 {
2990                    panic!(
2991                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
2992						 in lower_change output with params: {:?} (param set {})",
2993                        test_name, val, bits, i, params, param_idx
2994                    );
2995                }
2996            }
2997        }
2998
2999        Ok(())
3000    }
3001
3002    #[cfg(not(debug_assertions))]
3003    fn check_gatorosc_no_poison(
3004        _test_name: &str,
3005        _kernel: Kernel,
3006    ) -> Result<(), Box<dyn std::error::Error>> {
3007        Ok(())
3008    }
3009
3010    #[cfg(feature = "proptest")]
3011    #[allow(clippy::float_cmp)]
3012    fn check_gatorosc_property(
3013        test_name: &str,
3014        kernel: Kernel,
3015    ) -> Result<(), Box<dyn std::error::Error>> {
3016        use proptest::prelude::*;
3017        skip_if_unsupported!(kernel, test_name);
3018
3019        let strat = (
3020            (5usize..=50),
3021            (1usize..=10),
3022            (3usize..=30),
3023            (1usize..=8),
3024            (2usize..=20),
3025            (1usize..=5),
3026        )
3027            .prop_flat_map(
3028                |(jaws_len, jaws_shift, teeth_len, teeth_shift, lips_len, lips_shift)| {
3029                    let min_data_len = jaws_len.max(teeth_len).max(lips_len)
3030                        + jaws_shift.max(teeth_shift).max(lips_shift)
3031                        + 10;
3032                    (
3033                        prop::collection::vec(
3034                            (10.0f64..100000.0f64).prop_filter("finite", |x| x.is_finite()),
3035                            min_data_len..400,
3036                        ),
3037                        Just(jaws_len),
3038                        Just(jaws_shift),
3039                        Just(teeth_len),
3040                        Just(teeth_shift),
3041                        Just(lips_len),
3042                        Just(lips_shift),
3043                    )
3044                },
3045            );
3046
3047        proptest::test_runner::TestRunner::default()
3048            .run(
3049                &strat,
3050                |(
3051                    data,
3052                    jaws_length,
3053                    jaws_shift,
3054                    teeth_length,
3055                    teeth_shift,
3056                    lips_length,
3057                    lips_shift,
3058                )| {
3059                    let params = GatorOscParams {
3060                        jaws_length: Some(jaws_length),
3061                        jaws_shift: Some(jaws_shift),
3062                        teeth_length: Some(teeth_length),
3063                        teeth_shift: Some(teeth_shift),
3064                        lips_length: Some(lips_length),
3065                        lips_shift: Some(lips_shift),
3066                    };
3067                    let input = GatorOscInput::from_slice(&data, params);
3068
3069                    let test_output = gatorosc_with_kernel(&input, kernel).unwrap();
3070                    let ref_output = gatorosc_with_kernel(&input, Kernel::Scalar).unwrap();
3071
3072                    let GatorOscOutput {
3073                        upper,
3074                        lower,
3075                        upper_change,
3076                        lower_change,
3077                    } = test_output;
3078                    let GatorOscOutput {
3079                        upper: ref_upper,
3080                        lower: ref_lower,
3081                        upper_change: ref_upper_change,
3082                        lower_change: ref_lower_change,
3083                    } = ref_output;
3084
3085                    let mut first_finite_upper = None;
3086                    let mut first_finite_lower = None;
3087
3088                    for i in 0..upper.len() {
3089                        if upper[i].is_finite() && first_finite_upper.is_none() {
3090                            first_finite_upper = Some(i);
3091                        }
3092                        if lower[i].is_finite() && first_finite_lower.is_none() {
3093                            first_finite_lower = Some(i);
3094                        }
3095                    }
3096
3097                    if let Some(idx) = first_finite_upper {
3098                        prop_assert!(
3099						idx > 0,
3100						"Upper should have at least some warmup period, but first finite value is at index {}",
3101						idx
3102					);
3103                    }
3104
3105                    if let Some(idx) = first_finite_lower {
3106                        prop_assert!(
3107						idx > 0,
3108						"Lower should have at least some warmup period, but first finite value is at index {}",
3109						idx
3110					);
3111                    }
3112
3113                    let safe_start = (jaws_length.max(teeth_length).max(lips_length)
3114                        + jaws_shift.max(teeth_shift).max(lips_shift))
3115                    .min(data.len() - 1);
3116
3117                    for i in safe_start..upper.len() {
3118                        prop_assert!(
3119                            upper[i].is_finite() || upper[i].is_nan(),
3120                            "Upper should be finite or NaN at index {}: got {}",
3121                            i,
3122                            upper[i]
3123                        );
3124                    }
3125
3126                    for i in safe_start..lower.len() {
3127                        prop_assert!(
3128                            lower[i].is_finite() || lower[i].is_nan(),
3129                            "Lower should be finite or NaN at index {}: got {}",
3130                            i,
3131                            lower[i]
3132                        );
3133                    }
3134
3135                    for i in 0..data.len() {
3136                        if upper[i].is_finite() && ref_upper[i].is_finite() {
3137                            let ulp_diff = upper[i].to_bits().abs_diff(ref_upper[i].to_bits());
3138                            prop_assert!(
3139                                (upper[i] - ref_upper[i]).abs() <= 1e-9 || ulp_diff <= 4,
3140                                "Upper mismatch at {}: {} vs {} (ULP={})",
3141                                i,
3142                                upper[i],
3143                                ref_upper[i],
3144                                ulp_diff
3145                            );
3146                        } else {
3147                            prop_assert_eq!(
3148                                upper[i].is_nan(),
3149                                ref_upper[i].is_nan(),
3150                                "Upper NaN mismatch at {}",
3151                                i
3152                            );
3153                        }
3154
3155                        if lower[i].is_finite() && ref_lower[i].is_finite() {
3156                            let ulp_diff = lower[i].to_bits().abs_diff(ref_lower[i].to_bits());
3157                            prop_assert!(
3158                                (lower[i] - ref_lower[i]).abs() <= 1e-9 || ulp_diff <= 4,
3159                                "Lower mismatch at {}: {} vs {} (ULP={})",
3160                                i,
3161                                lower[i],
3162                                ref_lower[i],
3163                                ulp_diff
3164                            );
3165                        } else {
3166                            prop_assert_eq!(
3167                                lower[i].is_nan(),
3168                                ref_lower[i].is_nan(),
3169                                "Lower NaN mismatch at {}",
3170                                i
3171                            );
3172                        }
3173
3174                        if upper_change[i].is_finite() && ref_upper_change[i].is_finite() {
3175                            let ulp_diff = upper_change[i]
3176                                .to_bits()
3177                                .abs_diff(ref_upper_change[i].to_bits());
3178                            prop_assert!(
3179                                (upper_change[i] - ref_upper_change[i]).abs() <= 1e-9
3180                                    || ulp_diff <= 4,
3181                                "Upper change mismatch at {}: {} vs {} (ULP={})",
3182                                i,
3183                                upper_change[i],
3184                                ref_upper_change[i],
3185                                ulp_diff
3186                            );
3187                        } else {
3188                            prop_assert_eq!(
3189                                upper_change[i].is_nan(),
3190                                ref_upper_change[i].is_nan(),
3191                                "Upper change NaN mismatch at {}",
3192                                i
3193                            );
3194                        }
3195
3196                        if lower_change[i].is_finite() && ref_lower_change[i].is_finite() {
3197                            let ulp_diff = lower_change[i]
3198                                .to_bits()
3199                                .abs_diff(ref_lower_change[i].to_bits());
3200                            prop_assert!(
3201                                (lower_change[i] - ref_lower_change[i]).abs() <= 1e-9
3202                                    || ulp_diff <= 4,
3203                                "Lower change mismatch at {}: {} vs {} (ULP={})",
3204                                i,
3205                                lower_change[i],
3206                                ref_lower_change[i],
3207                                ulp_diff
3208                            );
3209                        } else {
3210                            prop_assert_eq!(
3211                                lower_change[i].is_nan(),
3212                                ref_lower_change[i].is_nan(),
3213                                "Lower change NaN mismatch at {}",
3214                                i
3215                            );
3216                        }
3217                    }
3218
3219                    for i in safe_start..upper.len() {
3220                        prop_assert!(
3221                            upper[i] >= -1e-10,
3222                            "Upper should be non-negative at {}: got {}",
3223                            i,
3224                            upper[i]
3225                        );
3226                    }
3227
3228                    for i in safe_start..lower.len() {
3229                        prop_assert!(
3230                            lower[i] <= 1e-10,
3231                            "Lower should be non-positive at {}: got {}",
3232                            i,
3233                            lower[i]
3234                        );
3235                    }
3236
3237                    for i in 1..data.len() {
3238                        if !upper[i].is_nan() && !upper[i - 1].is_nan() {
3239                            let expected_change = upper[i] - upper[i - 1];
3240                            if upper_change[i].is_finite() {
3241                                prop_assert!(
3242                                    (upper_change[i] - expected_change).abs() <= 1e-9,
3243                                    "Upper change incorrect at {}: got {}, expected {}",
3244                                    i,
3245                                    upper_change[i],
3246                                    expected_change
3247                                );
3248                            }
3249                        }
3250
3251                        if !lower[i].is_nan() && !lower[i - 1].is_nan() {
3252                            let expected_change = -(lower[i] - lower[i - 1]);
3253                            if lower_change[i].is_finite() {
3254                                prop_assert!(
3255                                    (lower_change[i] - expected_change).abs() <= 1e-9,
3256                                    "Lower change incorrect at {}: got {}, expected {}",
3257                                    i,
3258                                    lower_change[i],
3259                                    expected_change
3260                                );
3261                            }
3262                        }
3263                    }
3264
3265                    let min_price = data.iter().cloned().fold(f64::INFINITY, f64::min);
3266                    let max_price = data.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
3267                    let price_range = max_price - min_price;
3268
3269                    for i in safe_start..upper.len() {
3270                        prop_assert!(
3271                            upper[i] <= price_range + 1e-9,
3272                            "Upper exceeds price range at {}: {} > {}",
3273                            i,
3274                            upper[i],
3275                            price_range
3276                        );
3277                    }
3278
3279                    for i in safe_start..lower.len() {
3280                        prop_assert!(
3281                            lower[i] >= -(price_range + 1e-9),
3282                            "Lower exceeds negative price range at {}: {} < {}",
3283                            i,
3284                            lower[i],
3285                            -price_range
3286                        );
3287                    }
3288
3289                    if data.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-9) {
3290                        for i in (data.len() * 3 / 4)..data.len() {
3291                            if upper[i].is_finite() {
3292                                prop_assert!(
3293                                    upper[i].abs() <= 1e-6,
3294                                    "Upper should be near zero with constant data at {}: {}",
3295                                    i,
3296                                    upper[i]
3297                                );
3298                            }
3299                            if lower[i].is_finite() {
3300                                prop_assert!(
3301                                    lower[i].abs() <= 1e-6,
3302                                    "Lower should be near zero with constant data at {}: {}",
3303                                    i,
3304                                    lower[i]
3305                                );
3306                            }
3307                        }
3308                    }
3309
3310                    for i in 0..data.len() {
3311                        if upper[i].is_finite() {
3312                            prop_assert_ne!(upper[i].to_bits(), 0x11111111_11111111u64);
3313                            prop_assert_ne!(upper[i].to_bits(), 0x22222222_22222222u64);
3314                            prop_assert_ne!(upper[i].to_bits(), 0x33333333_33333333u64);
3315                        }
3316                        if lower[i].is_finite() {
3317                            prop_assert_ne!(lower[i].to_bits(), 0x11111111_11111111u64);
3318                            prop_assert_ne!(lower[i].to_bits(), 0x22222222_22222222u64);
3319                            prop_assert_ne!(lower[i].to_bits(), 0x33333333_33333333u64);
3320                        }
3321                        if upper_change[i].is_finite() {
3322                            prop_assert_ne!(upper_change[i].to_bits(), 0x11111111_11111111u64);
3323                            prop_assert_ne!(upper_change[i].to_bits(), 0x22222222_22222222u64);
3324                            prop_assert_ne!(upper_change[i].to_bits(), 0x33333333_33333333u64);
3325                        }
3326                        if lower_change[i].is_finite() {
3327                            prop_assert_ne!(lower_change[i].to_bits(), 0x11111111_11111111u64);
3328                            prop_assert_ne!(lower_change[i].to_bits(), 0x22222222_22222222u64);
3329                            prop_assert_ne!(lower_change[i].to_bits(), 0x33333333_33333333u64);
3330                        }
3331                    }
3332
3333                    Ok(())
3334                },
3335            )
3336            .unwrap();
3337
3338        Ok(())
3339    }
3340
3341    macro_rules! generate_all_gatorosc_tests {
3342        ($($test_fn:ident),*) => {
3343            paste::paste! {
3344                $(
3345                    #[test]
3346                    fn [<$test_fn _scalar_f64>]() {
3347                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
3348                    }
3349                )*
3350                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3351                $(
3352                    #[test]
3353                    fn [<$test_fn _avx2_f64>]() {
3354                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
3355                    }
3356                    #[test]
3357                    fn [<$test_fn _avx512_f64>]() {
3358                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
3359                    }
3360                )*
3361            }
3362        }
3363    }
3364
3365    generate_all_gatorosc_tests!(
3366        check_gatorosc_partial_params,
3367        check_gatorosc_nan_handling,
3368        check_gatorosc_zero_setting,
3369        check_gatorosc_small_dataset,
3370        check_gatorosc_default_candles,
3371        check_gatorosc_batch_default_row,
3372        check_gatorosc_no_poison
3373    );
3374
3375    #[cfg(feature = "proptest")]
3376    generate_all_gatorosc_tests!(check_gatorosc_property);
3377    fn check_batch_default_row(
3378        test: &str,
3379        kernel: Kernel,
3380    ) -> Result<(), Box<dyn std::error::Error>> {
3381        skip_if_unsupported!(kernel, test);
3382
3383        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3384        let c = read_candles_from_csv(file)?;
3385
3386        let output = GatorOscBatchBuilder::new()
3387            .kernel(kernel)
3388            .apply_slice(&c.close)?;
3389
3390        let def = GatorOscParams::default();
3391        let row = output
3392            .combos
3393            .iter()
3394            .position(|p| {
3395                p.jaws_length == def.jaws_length
3396                    && p.jaws_shift == def.jaws_shift
3397                    && p.teeth_length == def.teeth_length
3398                    && p.teeth_shift == def.teeth_shift
3399                    && p.lips_length == def.lips_length
3400                    && p.lips_shift == def.lips_shift
3401            })
3402            .expect("default row missing");
3403
3404        let upper = &output.upper[row * output.cols..][..output.cols];
3405        let lower = &output.lower[row * output.cols..][..output.cols];
3406
3407        assert_eq!(upper.len(), c.close.len());
3408        assert_eq!(lower.len(), c.close.len());
3409        Ok(())
3410    }
3411
3412    fn check_batch_multi_param_sweep(
3413        test: &str,
3414        kernel: Kernel,
3415    ) -> Result<(), Box<dyn std::error::Error>> {
3416        skip_if_unsupported!(kernel, test);
3417
3418        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3419        let c = read_candles_from_csv(file)?;
3420
3421        let builder = GatorOscBatchBuilder::new()
3422            .kernel(kernel)
3423            .jaws_length_range(8, 14, 3)
3424            .jaws_shift_range(5, 8, 3)
3425            .teeth_length_range(5, 8, 3)
3426            .teeth_shift_range(3, 5, 2)
3427            .lips_length_range(3, 5, 2)
3428            .lips_shift_range(2, 3, 1);
3429
3430        let output = builder.apply_slice(&c.close)?;
3431
3432        assert!(output.rows > 1, "Should have multiple param sweeps");
3433        assert_eq!(output.cols, c.close.len());
3434
3435        let some_upper = output
3436            .upper
3437            .chunks(output.cols)
3438            .any(|row| row.iter().any(|&x| !x.is_nan()));
3439        assert!(some_upper);
3440
3441        Ok(())
3442    }
3443
3444    fn check_batch_not_enough_data(
3445        test: &str,
3446        kernel: Kernel,
3447    ) -> Result<(), Box<dyn std::error::Error>> {
3448        skip_if_unsupported!(kernel, test);
3449
3450        let short = [1.0, 2.0, 3.0, 4.0, 5.0];
3451        let mut sweep = GatorOscBatchRange::default();
3452        sweep.jaws_length = (6, 6, 0);
3453
3454        let res = gatorosc_batch_with_kernel(&short, &sweep, kernel);
3455        assert!(res.is_err());
3456        Ok(())
3457    }
3458
3459    #[cfg(debug_assertions)]
3460    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn std::error::Error>> {
3461        skip_if_unsupported!(kernel, test);
3462
3463        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3464        let c = read_candles_from_csv(file)?;
3465
3466        let test_configs = vec![
3467            (5, 20, 5, 8, 8, 0, 8, 8, 0, 5, 5, 0, 5, 5, 0, 3, 3, 0),
3468            (13, 13, 0, 3, 10, 2, 8, 8, 0, 5, 5, 0, 5, 5, 0, 3, 3, 0),
3469            (13, 13, 0, 8, 8, 0, 3, 10, 2, 5, 5, 0, 5, 5, 0, 3, 3, 0),
3470            (13, 13, 0, 8, 8, 0, 8, 8, 0, 2, 8, 2, 5, 5, 0, 3, 3, 0),
3471            (13, 13, 0, 8, 8, 0, 8, 8, 0, 5, 5, 0, 2, 8, 2, 3, 3, 0),
3472            (13, 13, 0, 8, 8, 0, 8, 8, 0, 5, 5, 0, 5, 5, 0, 1, 5, 1),
3473            (8, 14, 3, 5, 8, 3, 5, 8, 3, 3, 5, 2, 3, 5, 2, 2, 3, 1),
3474            (10, 10, 0, 5, 5, 0, 8, 8, 0, 4, 4, 0, 5, 5, 0, 2, 2, 0),
3475            (13, 13, 0, 8, 8, 0, 8, 8, 0, 5, 5, 0, 5, 5, 0, 3, 3, 0),
3476            (2, 5, 1, 0, 3, 1, 2, 5, 1, 0, 3, 1, 2, 5, 1, 0, 3, 1),
3477            (
3478                30, 50, 10, 10, 20, 5, 20, 30, 5, 8, 15, 3, 10, 20, 5, 5, 10, 2,
3479            ),
3480        ];
3481
3482        for (
3483            cfg_idx,
3484            &(
3485                jl_s,
3486                jl_e,
3487                jl_st,
3488                js_s,
3489                js_e,
3490                js_st,
3491                tl_s,
3492                tl_e,
3493                tl_st,
3494                ts_s,
3495                ts_e,
3496                ts_st,
3497                ll_s,
3498                ll_e,
3499                ll_st,
3500                ls_s,
3501                ls_e,
3502                ls_st,
3503            ),
3504        ) in test_configs.iter().enumerate()
3505        {
3506            let output = GatorOscBatchBuilder::new()
3507                .kernel(kernel)
3508                .jaws_length_range(jl_s, jl_e, jl_st)
3509                .jaws_shift_range(js_s, js_e, js_st)
3510                .teeth_length_range(tl_s, tl_e, tl_st)
3511                .teeth_shift_range(ts_s, ts_e, ts_st)
3512                .lips_length_range(ll_s, ll_e, ll_st)
3513                .lips_shift_range(ls_s, ls_e, ls_st)
3514                .apply_slice(&c.close)?;
3515
3516            let check_poison = |matrix: &[f64], matrix_name: &str| {
3517                for (idx, &val) in matrix.iter().enumerate() {
3518                    if val.is_nan() {
3519                        continue;
3520                    }
3521
3522                    let bits = val.to_bits();
3523                    let row = idx / output.cols;
3524                    let col = idx % output.cols;
3525                    let combo = &output.combos[row];
3526
3527                    if bits == 0x11111111_11111111 {
3528                        panic!(
3529							"[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
3530							at row {} col {} (flat index {}) in {} output with params: jl={}, js={}, tl={}, ts={}, ll={}, ls={}",
3531							test, cfg_idx, val, bits, row, col, idx, matrix_name,
3532							combo.jaws_length.unwrap_or(13), combo.jaws_shift.unwrap_or(8),
3533							combo.teeth_length.unwrap_or(8), combo.teeth_shift.unwrap_or(5),
3534							combo.lips_length.unwrap_or(5), combo.lips_shift.unwrap_or(3)
3535						);
3536                    }
3537
3538                    if bits == 0x22222222_22222222 {
3539                        panic!(
3540							"[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
3541							at row {} col {} (flat index {}) in {} output with params: jl={}, js={}, tl={}, ts={}, ll={}, ls={}",
3542							test, cfg_idx, val, bits, row, col, idx, matrix_name,
3543							combo.jaws_length.unwrap_or(13), combo.jaws_shift.unwrap_or(8),
3544							combo.teeth_length.unwrap_or(8), combo.teeth_shift.unwrap_or(5),
3545							combo.lips_length.unwrap_or(5), combo.lips_shift.unwrap_or(3)
3546						);
3547                    }
3548
3549                    if bits == 0x33333333_33333333 {
3550                        panic!(
3551							"[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
3552							at row {} col {} (flat index {}) in {} output with params: jl={}, js={}, tl={}, ts={}, ll={}, ls={}",
3553							test, cfg_idx, val, bits, row, col, idx, matrix_name,
3554							combo.jaws_length.unwrap_or(13), combo.jaws_shift.unwrap_or(8),
3555							combo.teeth_length.unwrap_or(8), combo.teeth_shift.unwrap_or(5),
3556							combo.lips_length.unwrap_or(5), combo.lips_shift.unwrap_or(3)
3557						);
3558                    }
3559                }
3560            };
3561
3562            check_poison(&output.upper, "upper");
3563            check_poison(&output.lower, "lower");
3564            check_poison(&output.upper_change, "upper_change");
3565            check_poison(&output.lower_change, "lower_change");
3566        }
3567
3568        Ok(())
3569    }
3570
3571    #[cfg(not(debug_assertions))]
3572    fn check_batch_no_poison(
3573        _test: &str,
3574        _kernel: Kernel,
3575    ) -> Result<(), Box<dyn std::error::Error>> {
3576        Ok(())
3577    }
3578
3579    macro_rules! gen_batch_tests {
3580        ($fn_name:ident) => {
3581            paste::paste! {
3582                #[test] fn [<$fn_name _scalar>]()      {
3583                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
3584                }
3585                #[cfg(all(target_feature = "simd128", target_arch = "wasm32"))]
3586                #[test] fn [<$fn_name _simd128>]()     {
3587                    let _ = $fn_name(stringify!([<$fn_name _simd128>]), Kernel::Simd128Batch);
3588                }
3589                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3590                #[test] fn [<$fn_name _avx2>]()        {
3591                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
3592                }
3593                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3594                #[test] fn [<$fn_name _avx512>]()      {
3595                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
3596                }
3597                #[test] fn [<$fn_name _auto_detect>]() {
3598                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
3599                }
3600            }
3601        };
3602    }
3603
3604    gen_batch_tests!(check_batch_default_row);
3605    gen_batch_tests!(check_batch_multi_param_sweep);
3606    gen_batch_tests!(check_batch_not_enough_data);
3607    gen_batch_tests!(check_batch_no_poison);
3608}
3609
3610#[cfg(feature = "python")]
3611use crate::utilities::kernel_validation::validate_kernel;
3612#[cfg(feature = "python")]
3613use numpy::{IntoPyArray, PyArray1};
3614#[cfg(feature = "python")]
3615use pyo3::exceptions::PyValueError;
3616#[cfg(feature = "python")]
3617use pyo3::prelude::*;
3618#[cfg(feature = "python")]
3619use pyo3::types::PyDict;
3620
3621#[cfg(feature = "python")]
3622#[pyfunction(name = "gatorosc")]
3623#[pyo3(signature = (data, jaws_length=13, jaws_shift=8, teeth_length=8, teeth_shift=5, lips_length=5, lips_shift=3, kernel=None))]
3624pub fn gatorosc_py<'py>(
3625    py: Python<'py>,
3626    data: numpy::PyReadonlyArray1<'py, f64>,
3627    jaws_length: usize,
3628    jaws_shift: usize,
3629    teeth_length: usize,
3630    teeth_shift: usize,
3631    lips_length: usize,
3632    lips_shift: usize,
3633    kernel: Option<&str>,
3634) -> PyResult<(
3635    Bound<'py, PyArray1<f64>>,
3636    Bound<'py, PyArray1<f64>>,
3637    Bound<'py, PyArray1<f64>>,
3638    Bound<'py, PyArray1<f64>>,
3639)> {
3640    use numpy::{IntoPyArray, PyArrayMethods};
3641
3642    let slice_in = data.as_slice()?;
3643    let kern = validate_kernel(kernel, false)?;
3644
3645    let params = GatorOscParams {
3646        jaws_length: Some(jaws_length),
3647        jaws_shift: Some(jaws_shift),
3648        teeth_length: Some(teeth_length),
3649        teeth_shift: Some(teeth_shift),
3650        lips_length: Some(lips_length),
3651        lips_shift: Some(lips_shift),
3652    };
3653    let input = GatorOscInput::from_slice(slice_in, params);
3654
3655    let (upper_vec, lower_vec, upper_change_vec, lower_change_vec) = py
3656        .allow_threads(|| {
3657            gatorosc_with_kernel(&input, kern)
3658                .map(|o| (o.upper, o.lower, o.upper_change, o.lower_change))
3659        })
3660        .map_err(|e| PyValueError::new_err(e.to_string()))?;
3661
3662    Ok((
3663        upper_vec.into_pyarray(py),
3664        lower_vec.into_pyarray(py),
3665        upper_change_vec.into_pyarray(py),
3666        lower_change_vec.into_pyarray(py),
3667    ))
3668}
3669
3670#[cfg(feature = "python")]
3671#[pyclass(name = "GatorOscStream")]
3672pub struct GatorOscStreamPy {
3673    stream: GatorOscStream,
3674}
3675
3676#[cfg(feature = "python")]
3677#[pymethods]
3678impl GatorOscStreamPy {
3679    #[new]
3680    #[pyo3(signature = (jaws_length=13, jaws_shift=8, teeth_length=8, teeth_shift=5, lips_length=5, lips_shift=3))]
3681    fn new(
3682        jaws_length: usize,
3683        jaws_shift: usize,
3684        teeth_length: usize,
3685        teeth_shift: usize,
3686        lips_length: usize,
3687        lips_shift: usize,
3688    ) -> PyResult<Self> {
3689        let params = GatorOscParams {
3690            jaws_length: Some(jaws_length),
3691            jaws_shift: Some(jaws_shift),
3692            teeth_length: Some(teeth_length),
3693            teeth_shift: Some(teeth_shift),
3694            lips_length: Some(lips_length),
3695            lips_shift: Some(lips_shift),
3696        };
3697        let stream =
3698            GatorOscStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
3699        Ok(GatorOscStreamPy { stream })
3700    }
3701
3702    fn update(&mut self, value: f64) -> Option<(f64, f64, f64, f64)> {
3703        self.stream.update(value)
3704    }
3705}
3706
3707#[cfg(feature = "python")]
3708#[pyfunction(name = "gatorosc_batch")]
3709#[pyo3(signature = (data, jaws_length_range=(13, 13, 0), jaws_shift_range=(8, 8, 0), teeth_length_range=(8, 8, 0), teeth_shift_range=(5, 5, 0), lips_length_range=(5, 5, 0), lips_shift_range=(3, 3, 0), kernel=None))]
3710pub fn gatorosc_batch_py<'py>(
3711    py: Python<'py>,
3712    data: numpy::PyReadonlyArray1<'py, f64>,
3713    jaws_length_range: (usize, usize, usize),
3714    jaws_shift_range: (usize, usize, usize),
3715    teeth_length_range: (usize, usize, usize),
3716    teeth_shift_range: (usize, usize, usize),
3717    lips_length_range: (usize, usize, usize),
3718    lips_shift_range: (usize, usize, usize),
3719    kernel: Option<&str>,
3720) -> PyResult<Bound<'py, PyDict>> {
3721    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
3722    use pyo3::types::PyDict;
3723
3724    let slice_in = data.as_slice()?;
3725    let kern = validate_kernel(kernel, true)?;
3726
3727    let sweep = GatorOscBatchRange {
3728        jaws_length: jaws_length_range,
3729        jaws_shift: jaws_shift_range,
3730        teeth_length: teeth_length_range,
3731        teeth_shift: teeth_shift_range,
3732        lips_length: lips_length_range,
3733        lips_shift: lips_shift_range,
3734    };
3735
3736    let combos = expand_grid_gatorosc(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
3737    let rows = combos.len();
3738    let cols = slice_in.len();
3739
3740    let total = rows
3741        .checked_mul(cols)
3742        .ok_or_else(|| PyValueError::new_err("gatorosc_batch_py: rows*cols overflow"))?;
3743    let upper_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
3744    let lower_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
3745    let upper_change_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
3746    let lower_change_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
3747
3748    let slice_upper = unsafe { upper_arr.as_slice_mut()? };
3749    let slice_lower = unsafe { lower_arr.as_slice_mut()? };
3750    let slice_upper_change = unsafe { upper_change_arr.as_slice_mut()? };
3751    let slice_lower_change = unsafe { lower_change_arr.as_slice_mut()? };
3752
3753    let combos = py
3754        .allow_threads(|| {
3755            let kernel = match kern {
3756                Kernel::Auto => detect_best_batch_kernel(),
3757                k => k,
3758            };
3759            let simd = match kernel {
3760                Kernel::Avx512Batch => Kernel::Avx512,
3761                Kernel::Avx2Batch => Kernel::Avx2,
3762                Kernel::ScalarBatch => Kernel::Scalar,
3763                _ => unreachable!(),
3764            };
3765
3766            gatorosc_batch_inner_into(
3767                slice_in,
3768                &sweep,
3769                simd,
3770                true,
3771                slice_upper,
3772                slice_lower,
3773                slice_upper_change,
3774                slice_lower_change,
3775            )
3776        })
3777        .map_err(|e| PyValueError::new_err(e.to_string()))?;
3778
3779    let dict = PyDict::new(py);
3780    dict.set_item("upper", upper_arr.reshape((rows, cols))?)?;
3781    dict.set_item("lower", lower_arr.reshape((rows, cols))?)?;
3782    dict.set_item("upper_change", upper_change_arr.reshape((rows, cols))?)?;
3783    dict.set_item("lower_change", lower_change_arr.reshape((rows, cols))?)?;
3784    dict.set_item(
3785        "jaws_lengths",
3786        combos
3787            .iter()
3788            .map(|p| p.jaws_length.unwrap() as u64)
3789            .collect::<Vec<_>>()
3790            .into_pyarray(py),
3791    )?;
3792    dict.set_item(
3793        "jaws_shifts",
3794        combos
3795            .iter()
3796            .map(|p| p.jaws_shift.unwrap() as u64)
3797            .collect::<Vec<_>>()
3798            .into_pyarray(py),
3799    )?;
3800    dict.set_item(
3801        "teeth_lengths",
3802        combos
3803            .iter()
3804            .map(|p| p.teeth_length.unwrap() as u64)
3805            .collect::<Vec<_>>()
3806            .into_pyarray(py),
3807    )?;
3808    dict.set_item(
3809        "teeth_shifts",
3810        combos
3811            .iter()
3812            .map(|p| p.teeth_shift.unwrap() as u64)
3813            .collect::<Vec<_>>()
3814            .into_pyarray(py),
3815    )?;
3816    dict.set_item(
3817        "lips_lengths",
3818        combos
3819            .iter()
3820            .map(|p| p.lips_length.unwrap() as u64)
3821            .collect::<Vec<_>>()
3822            .into_pyarray(py),
3823    )?;
3824    dict.set_item(
3825        "lips_shifts",
3826        combos
3827            .iter()
3828            .map(|p| p.lips_shift.unwrap() as u64)
3829            .collect::<Vec<_>>()
3830            .into_pyarray(py),
3831    )?;
3832
3833    Ok(dict)
3834}
3835
3836#[cfg(all(feature = "python", feature = "cuda"))]
3837use crate::cuda::cuda_available;
3838#[cfg(all(feature = "python", feature = "cuda"))]
3839use crate::cuda::oscillators::gatorosc_wrapper::CudaGatorOsc;
3840#[cfg(all(feature = "python", feature = "cuda"))]
3841use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
3842#[cfg(all(feature = "python", feature = "cuda"))]
3843use cust::context::Context;
3844#[cfg(all(feature = "python", feature = "cuda"))]
3845use cust::memory::DeviceBuffer;
3846#[cfg(all(feature = "python", feature = "cuda"))]
3847use std::sync::Arc;
3848
3849#[cfg(all(feature = "python", feature = "cuda"))]
3850#[pyclass(
3851    module = "ta_indicators.cuda",
3852    name = "GatorDeviceArrayF32",
3853    unsendable
3854)]
3855pub struct DeviceArrayF32GatorPy {
3856    pub(crate) inner: crate::cuda::moving_averages::DeviceArrayF32,
3857    _ctx_guard: Arc<Context>,
3858    _device_id: u32,
3859}
3860
3861#[cfg(all(feature = "python", feature = "cuda"))]
3862#[pymethods]
3863impl DeviceArrayF32GatorPy {
3864    #[getter]
3865    fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
3866        let d = PyDict::new(py);
3867        d.set_item("shape", (self.inner.rows, self.inner.cols))?;
3868        d.set_item("typestr", "<f4")?;
3869        d.set_item(
3870            "strides",
3871            (
3872                self.inner.cols * std::mem::size_of::<f32>(),
3873                std::mem::size_of::<f32>(),
3874            ),
3875        )?;
3876        let ptr_val: usize = if self.inner.rows == 0 || self.inner.cols == 0 {
3877            0
3878        } else {
3879            self.inner.buf.as_device_ptr().as_raw() as usize
3880        };
3881        d.set_item("data", (ptr_val, false))?;
3882        d.set_item("version", 3)?;
3883        Ok(d)
3884    }
3885
3886    fn __dlpack_device__(&self) -> (i32, i32) {
3887        (2, self._device_id as i32)
3888    }
3889
3890    #[pyo3(signature = (stream=None, max_version=None, dl_device=None, copy=None))]
3891    fn __dlpack__<'py>(
3892        &mut self,
3893        py: Python<'py>,
3894        stream: Option<pyo3::PyObject>,
3895        max_version: Option<pyo3::PyObject>,
3896        dl_device: Option<pyo3::PyObject>,
3897        copy: Option<pyo3::PyObject>,
3898    ) -> PyResult<pyo3::PyObject> {
3899        let (kdl, alloc_dev) = self.__dlpack_device__();
3900        if let Some(dev_obj) = dl_device.as_ref() {
3901            if let Ok((dev_ty, dev_id)) = dev_obj.extract::<(i32, i32)>(py) {
3902                if dev_ty != kdl || dev_id != alloc_dev {
3903                    let wants_copy = copy
3904                        .as_ref()
3905                        .and_then(|c| c.extract::<bool>(py).ok())
3906                        .unwrap_or(false);
3907                    if wants_copy {
3908                        return Err(PyValueError::new_err(
3909                            "__dlpack__(copy=True) not implemented for Gator CUDA handle",
3910                        ));
3911                    } else {
3912                        return Err(PyValueError::new_err(
3913                            "dl_device mismatch for Gator DLPack tensor",
3914                        ));
3915                    }
3916                }
3917            }
3918        }
3919        let _ = stream;
3920
3921        if let Some(copy_obj) = copy.as_ref() {
3922            let do_copy: bool = copy_obj.extract(py)?;
3923            if do_copy {
3924                return Err(PyValueError::new_err(
3925                    "__dlpack__(copy=True) not implemented for Gator CUDA handle",
3926                ));
3927            }
3928        }
3929
3930        let dummy =
3931            DeviceBuffer::from_slice(&[]).map_err(|e| PyValueError::new_err(e.to_string()))?;
3932        let rows = self.inner.rows;
3933        let cols = self.inner.cols;
3934        let inner = std::mem::replace(
3935            &mut self.inner,
3936            crate::cuda::moving_averages::DeviceArrayF32 {
3937                buf: dummy,
3938                rows: 0,
3939                cols: 0,
3940            },
3941        );
3942
3943        let max_version_bound = max_version.map(|obj| obj.into_bound(py));
3944
3945        export_f32_cuda_dlpack_2d(py, inner.buf, rows, cols, alloc_dev, max_version_bound)
3946    }
3947}
3948
3949#[cfg(all(feature = "python", feature = "cuda"))]
3950#[pyfunction(name = "gatorosc_cuda_batch_dev")]
3951#[pyo3(signature = (data_f32, jaws_length_range=(13,13,0), jaws_shift_range=(8,8,0), teeth_length_range=(8,8,0), teeth_shift_range=(5,5,0), lips_length_range=(5,5,0), lips_shift_range=(3,3,0), device_id=0))]
3952pub fn gatorosc_cuda_batch_dev_py(
3953    py: Python<'_>,
3954    data_f32: numpy::PyReadonlyArray1<'_, f32>,
3955    jaws_length_range: (usize, usize, usize),
3956    jaws_shift_range: (usize, usize, usize),
3957    teeth_length_range: (usize, usize, usize),
3958    teeth_shift_range: (usize, usize, usize),
3959    lips_length_range: (usize, usize, usize),
3960    lips_shift_range: (usize, usize, usize),
3961    device_id: usize,
3962) -> PyResult<(
3963    DeviceArrayF32GatorPy,
3964    DeviceArrayF32GatorPy,
3965    DeviceArrayF32GatorPy,
3966    DeviceArrayF32GatorPy,
3967)> {
3968    if !cuda_available() {
3969        return Err(PyValueError::new_err("CUDA not available"));
3970    }
3971    let data = data_f32.as_slice()?;
3972    let sweep = GatorOscBatchRange {
3973        jaws_length: jaws_length_range,
3974        jaws_shift: jaws_shift_range,
3975        teeth_length: teeth_length_range,
3976        teeth_shift: teeth_shift_range,
3977        lips_length: lips_length_range,
3978        lips_shift: lips_shift_range,
3979    };
3980    let (upper, lower, upper_change, lower_change) = py.allow_threads(|| {
3981        let cuda =
3982            CudaGatorOsc::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
3983        let dev_id = cuda.device_id();
3984        let ctx = cuda.ctx();
3985        let quad = cuda
3986            .gatorosc_batch_dev(data, &sweep)
3987            .map_err(|e| PyValueError::new_err(e.to_string()))?;
3988        Ok::<_, PyErr>((
3989            DeviceArrayF32GatorPy {
3990                inner: quad.upper,
3991                _ctx_guard: ctx.clone(),
3992                _device_id: dev_id,
3993            },
3994            DeviceArrayF32GatorPy {
3995                inner: quad.lower,
3996                _ctx_guard: ctx.clone(),
3997                _device_id: dev_id,
3998            },
3999            DeviceArrayF32GatorPy {
4000                inner: quad.upper_change,
4001                _ctx_guard: ctx.clone(),
4002                _device_id: dev_id,
4003            },
4004            DeviceArrayF32GatorPy {
4005                inner: quad.lower_change,
4006                _ctx_guard: ctx,
4007                _device_id: dev_id,
4008            },
4009        ))
4010    })?;
4011    Ok((upper, lower, upper_change, lower_change))
4012}
4013
4014#[cfg(all(feature = "python", feature = "cuda"))]
4015#[pyfunction(name = "gatorosc_cuda_many_series_one_param_dev")]
4016#[pyo3(signature = (prices_tm_f32, cols, rows, jaws_length=13, jaws_shift=8, teeth_length=8, teeth_shift=5, lips_length=5, lips_shift=3, device_id=0))]
4017pub fn gatorosc_cuda_many_series_one_param_dev_py(
4018    py: Python<'_>,
4019    prices_tm_f32: numpy::PyReadonlyArray1<'_, f32>,
4020    cols: usize,
4021    rows: usize,
4022    jaws_length: usize,
4023    jaws_shift: usize,
4024    teeth_length: usize,
4025    teeth_shift: usize,
4026    lips_length: usize,
4027    lips_shift: usize,
4028    device_id: usize,
4029) -> PyResult<(
4030    DeviceArrayF32GatorPy,
4031    DeviceArrayF32GatorPy,
4032    DeviceArrayF32GatorPy,
4033    DeviceArrayF32GatorPy,
4034)> {
4035    if !cuda_available() {
4036        return Err(PyValueError::new_err("CUDA not available"));
4037    }
4038    let prices = prices_tm_f32.as_slice()?;
4039    let expected = cols
4040        .checked_mul(rows)
4041        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
4042    if prices.len() != expected {
4043        return Err(PyValueError::new_err("time-major input length mismatch"));
4044    }
4045    let (upper, lower, upper_change, lower_change) = py.allow_threads(|| {
4046        let cuda =
4047            CudaGatorOsc::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
4048        let dev_id = cuda.device_id();
4049        let ctx = cuda.ctx();
4050        let quad = cuda
4051            .gatorosc_many_series_one_param_time_major_dev(
4052                prices,
4053                cols,
4054                rows,
4055                jaws_length,
4056                jaws_shift,
4057                teeth_length,
4058                teeth_shift,
4059                lips_length,
4060                lips_shift,
4061            )
4062            .map_err(|e| PyValueError::new_err(e.to_string()))?;
4063        Ok::<_, PyErr>((
4064            DeviceArrayF32GatorPy {
4065                inner: quad.upper,
4066                _ctx_guard: ctx.clone(),
4067                _device_id: dev_id,
4068            },
4069            DeviceArrayF32GatorPy {
4070                inner: quad.lower,
4071                _ctx_guard: ctx.clone(),
4072                _device_id: dev_id,
4073            },
4074            DeviceArrayF32GatorPy {
4075                inner: quad.upper_change,
4076                _ctx_guard: ctx.clone(),
4077                _device_id: dev_id,
4078            },
4079            DeviceArrayF32GatorPy {
4080                inner: quad.lower_change,
4081                _ctx_guard: ctx,
4082                _device_id: dev_id,
4083            },
4084        ))
4085    })?;
4086    Ok((upper, lower, upper_change, lower_change))
4087}