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}