1#[derive(Debug, Clone, Copy, PartialEq)]
19pub enum DecimateMethod {
20 Lttb,
22 MinMax,
24}
25
26pub const DEFAULT_DECIMATE_THRESHOLD: usize = 5000;
38
39#[derive(Debug, Clone, Copy, PartialEq, Default)]
54pub enum DecimateMode {
55 #[default]
57 Auto,
58 Off,
60 Explicit(usize, DecimateMethod),
62}
63
64impl DecimateMode {
65 pub fn resolve_indices(self, x: &[f64], y: &[f64]) -> Vec<usize> {
73 let n = x.len();
74 match self {
75 DecimateMode::Off => (0..n).collect(),
76 DecimateMode::Auto => {
77 if n > DEFAULT_DECIMATE_THRESHOLD {
78 lttb(x, y, DEFAULT_DECIMATE_THRESHOLD)
79 } else {
80 (0..n).collect()
81 }
82 }
83 DecimateMode::Explicit(threshold, method) => {
84 if n > threshold {
85 match method {
86 DecimateMethod::Lttb => lttb(x, y, threshold),
87 DecimateMethod::MinMax => minmax(x, y, threshold),
88 }
89 } else {
90 (0..n).collect()
91 }
92 }
93 }
94 }
95}
96
97pub fn lttb(x: &[f64], y: &[f64], threshold: usize) -> Vec<usize> {
111 assert_eq!(x.len(), y.len(), "x and y must have the same length");
112
113 let n = x.len();
114
115 if threshold == 0 {
117 return vec![];
118 }
119 if n == 0 {
120 return vec![];
121 }
122 if threshold == 1 {
123 return vec![0];
125 }
126 if threshold >= n {
127 return (0..n).collect();
128 }
129
130 let valid: Vec<usize> = (0..n)
132 .filter(|&i| x[i].is_finite() && y[i].is_finite())
133 .collect();
134 let valid_n = valid.len();
135
136 if valid_n == 0 {
137 return vec![];
138 }
139 if threshold >= valid_n {
140 return valid;
141 }
142 if threshold == 1 {
143 return vec![valid[0]];
144 }
145
146 let mut selected = Vec::with_capacity(threshold);
147
148 selected.push(valid[0]);
150
151 let bucket_count = threshold - 2;
152 let interior = valid_n - 2;
154
155 let mut prev_selected_idx = 0usize; for bucket_i in 0..bucket_count {
158 let bucket_start = 1 + (bucket_i * interior) / bucket_count;
160 let bucket_end = 1 + ((bucket_i + 1) * interior) / bucket_count;
161
162 let next_start = if bucket_i + 1 < bucket_count {
164 1 + ((bucket_i + 1) * interior) / bucket_count
165 } else {
166 valid_n - 1
167 };
168 let next_end = if bucket_i + 1 < bucket_count {
169 1 + ((bucket_i + 2) * interior) / bucket_count
170 } else {
171 valid_n
172 };
173
174 let next_count = (next_end - next_start) as f64;
176 let (avg_x, avg_y) = if next_count > 0.0 {
177 let mut sx = 0.0;
178 let mut sy = 0.0;
179 for &vi in &valid[next_start..next_end] {
180 sx += x[vi];
181 sy += y[vi];
182 }
183 (sx / next_count, sy / next_count)
184 } else {
185 let li = valid[valid_n - 1];
187 (x[li], y[li])
188 };
189
190 let prev_orig = valid[prev_selected_idx];
192 let (px, py) = (x[prev_orig], y[prev_orig]);
193
194 let mut best_area = -1.0_f64;
196 let mut best_valid_idx = bucket_start;
197
198 for (vi, &orig) in valid
199 .iter()
200 .enumerate()
201 .skip(bucket_start)
202 .take(bucket_end - bucket_start)
203 {
204 let area = triangle_area(px, py, x[orig], y[orig], avg_x, avg_y);
205 if area > best_area {
206 best_area = area;
207 best_valid_idx = vi;
208 }
209 }
210
211 selected.push(valid[best_valid_idx]);
212 prev_selected_idx = best_valid_idx;
213 }
214
215 selected.push(valid[valid_n - 1]);
217
218 selected
219}
220
221pub fn minmax(x: &[f64], y: &[f64], threshold: usize) -> Vec<usize> {
232 assert_eq!(x.len(), y.len(), "x and y must have the same length");
233
234 let n = x.len();
235
236 if threshold == 0 {
237 return vec![];
238 }
239 if n == 0 {
240 return vec![];
241 }
242 if threshold == 1 {
243 return vec![0];
244 }
245 if threshold >= n {
246 return (0..n).collect();
247 }
248
249 let valid: Vec<usize> = (0..n)
251 .filter(|&i| x[i].is_finite() && y[i].is_finite())
252 .collect();
253 let valid_n = valid.len();
254
255 if valid_n == 0 {
256 return vec![];
257 }
258 if threshold >= valid_n {
259 return valid;
260 }
261
262 let mut selected = Vec::with_capacity(threshold);
263
264 selected.push(valid[0]);
266
267 let pairs = (threshold - 2) / 2;
270 let bucket_count = if pairs == 0 { 1 } else { pairs };
271 let interior = valid_n - 2; for bucket_i in 0..bucket_count {
274 let bucket_start = 1 + (bucket_i * interior) / bucket_count;
275 let bucket_end = 1 + ((bucket_i + 1) * interior) / bucket_count;
276
277 if bucket_start >= bucket_end {
278 continue;
279 }
280
281 let mut min_idx = bucket_start;
282 let mut max_idx = bucket_start;
283 let mut min_val = y[valid[bucket_start]];
284 let mut max_val = y[valid[bucket_start]];
285
286 for vi in bucket_start..bucket_end {
287 let yv = y[valid[vi]];
288 if yv < min_val {
289 min_val = yv;
290 min_idx = vi;
291 }
292 if yv > max_val {
293 max_val = yv;
294 max_idx = vi;
295 }
296 }
297
298 if min_idx == max_idx {
300 selected.push(valid[min_idx]);
301 } else if min_idx < max_idx {
302 selected.push(valid[min_idx]);
303 selected.push(valid[max_idx]);
304 } else {
305 selected.push(valid[max_idx]);
306 selected.push(valid[min_idx]);
307 }
308 }
309
310 let last = valid[valid_n - 1];
312 if selected.last() != Some(&last) {
314 selected.push(last);
315 }
316
317 selected
318}
319
320#[inline]
328fn triangle_area(x_a: f64, y_a: f64, x_b: f64, y_b: f64, x_c: f64, y_c: f64) -> f64 {
329 (x_a * (y_b - y_c) + x_b * (y_c - y_a) + x_c * (y_a - y_b)).abs()
330}
331
332#[cfg(test)]
337mod tests {
338 use super::*;
339 use std::f64::consts::PI;
340
341 #[test]
344 fn lttb_identity_when_threshold_ge_len() {
345 let x = vec![0.0, 1.0, 2.0, 3.0, 4.0];
346 let y = vec![0.0, 1.0, 4.0, 9.0, 16.0];
347 let indices = lttb(&x, &y, 5);
348 assert_eq!(indices, vec![0, 1, 2, 3, 4]);
349
350 let indices = lttb(&x, &y, 100);
351 assert_eq!(indices, vec![0, 1, 2, 3, 4]);
352 }
353
354 #[test]
355 fn lttb_first_and_last_always_preserved() {
356 let x: Vec<f64> = (0..100).map(|i| i as f64).collect();
357 let y: Vec<f64> = x.iter().map(|v| v * v).collect();
358 let indices = lttb(&x, &y, 10);
359 assert_eq!(*indices.first().unwrap(), 0);
360 assert_eq!(*indices.last().unwrap(), 99);
361 assert_eq!(indices.len(), 10);
362 }
363
364 #[test]
365 fn lttb_threshold_2_returns_first_and_last() {
366 let x = vec![0.0, 1.0, 2.0, 3.0, 4.0];
367 let y = vec![10.0, 20.0, 30.0, 40.0, 50.0];
368 let indices = lttb(&x, &y, 2);
369 assert_eq!(indices, vec![0, 4]);
370 }
371
372 #[test]
373 fn lttb_threshold_3_returns_first_middle_last() {
374 let x = vec![0.0, 1.0, 2.0, 3.0, 4.0];
375 let y = vec![0.0, 5.0, 0.0, 5.0, 0.0];
376 let indices = lttb(&x, &y, 3);
377 assert_eq!(indices.len(), 3);
378 assert_eq!(indices[0], 0);
379 assert_eq!(*indices.last().unwrap(), 4);
380 }
381
382 #[test]
383 fn lttb_known_triangle_area() {
384 let area = triangle_area(0.0, 0.0, 1.0, 0.0, 0.0, 1.0);
387 assert!((area - 1.0).abs() < 1e-12);
388
389 let area = triangle_area(0.0, 0.0, 1.0, 1.0, 2.0, 2.0);
391 assert!(area.abs() < 1e-12);
392 }
393
394 #[test]
395 fn lttb_linear_data_any_subset_looks_identical() {
396 let n = 50;
399 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
400 let y: Vec<f64> = x.iter().map(|v| 2.0 * v + 1.0).collect();
401 let indices = lttb(&x, &y, 10);
402 assert_eq!(indices.len(), 10);
403
404 for &idx in &indices {
406 let expected = 2.0 * x[idx] + 1.0;
407 assert!(
408 (y[idx] - expected).abs() < 1e-12,
409 "point at index {} deviates from y = 2x + 1",
410 idx
411 );
412 }
413 }
414
415 #[test]
416 fn lttb_sinusoidal_peaks_preserved() {
417 let n = 1000;
419 let x: Vec<f64> = (0..n).map(|i| i as f64 * 2.0 * PI / 100.0).collect();
420 let y: Vec<f64> = x.iter().map(|v| v.sin()).collect();
421
422 let indices = lttb(&x, &y, 50);
423
424 let selected_y: Vec<f64> = indices.iter().map(|&i| y[i]).collect();
426 let max_selected = selected_y.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
427 let min_selected = selected_y.iter().cloned().fold(f64::INFINITY, f64::min);
428
429 assert!(
430 max_selected > 0.95,
431 "peak not preserved: max = {}",
432 max_selected
433 );
434 assert!(
435 min_selected < -0.95,
436 "trough not preserved: min = {}",
437 min_selected
438 );
439 }
440
441 #[test]
442 fn lttb_single_point() {
443 let x = vec![42.0];
444 let y = vec![7.0];
445 let indices = lttb(&x, &y, 5);
446 assert_eq!(indices, vec![0]);
447 }
448
449 #[test]
450 fn lttb_two_points() {
451 let x = vec![1.0, 2.0];
452 let y = vec![3.0, 4.0];
453 let indices = lttb(&x, &y, 5);
454 assert_eq!(indices, vec![0, 1]);
455 }
456
457 #[test]
458 fn lttb_nan_handling() {
459 let x = vec![0.0, 1.0, f64::NAN, 3.0, 4.0, 5.0];
460 let y = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
461 let indices = lttb(&x, &y, 4);
462 assert!(!indices.contains(&2), "NaN index should not be selected");
464 assert_eq!(*indices.first().unwrap(), 0);
466 assert_eq!(*indices.last().unwrap(), 5);
467 }
468
469 #[test]
470 fn lttb_empty_input() {
471 let x: Vec<f64> = vec![];
472 let y: Vec<f64> = vec![];
473 let indices = lttb(&x, &y, 10);
474 assert!(indices.is_empty());
475 }
476
477 #[test]
478 fn lttb_threshold_zero() {
479 let x = vec![0.0, 1.0, 2.0];
480 let y = vec![0.0, 1.0, 2.0];
481 let indices = lttb(&x, &y, 0);
482 assert!(indices.is_empty());
483 }
484
485 #[test]
486 fn lttb_threshold_one() {
487 let x = vec![0.0, 1.0, 2.0];
488 let y = vec![0.0, 1.0, 2.0];
489 let indices = lttb(&x, &y, 1);
490 assert_eq!(indices, vec![0]);
491 }
492
493 #[test]
496 fn minmax_preserves_extremes() {
497 let x: Vec<f64> = (0..20).map(|i| i as f64).collect();
499 let mut y: Vec<f64> = vec![0.0; 20];
500 y[5] = 100.0; y[15] = -100.0; let indices = minmax(&x, &y, 10);
504 let selected_y: Vec<f64> = indices.iter().map(|&i| y[i]).collect();
505
506 assert!(selected_y.contains(&100.0), "spike not preserved");
507 assert!(selected_y.contains(&-100.0), "dip not preserved");
508 }
509
510 #[test]
511 fn minmax_identity_when_threshold_ge_len() {
512 let x = vec![0.0, 1.0, 2.0, 3.0];
513 let y = vec![1.0, 2.0, 3.0, 4.0];
514 let indices = minmax(&x, &y, 10);
515 assert_eq!(indices, vec![0, 1, 2, 3]);
516 }
517
518 #[test]
519 fn minmax_first_and_last_preserved() {
520 let x: Vec<f64> = (0..50).map(|i| i as f64).collect();
521 let y: Vec<f64> = x.iter().map(|v| v.sin()).collect();
522 let indices = minmax(&x, &y, 10);
523 assert_eq!(*indices.first().unwrap(), 0);
524 assert_eq!(*indices.last().unwrap(), 49);
525 }
526
527 #[test]
528 fn minmax_empty_input() {
529 let indices = minmax(&[], &[], 5);
530 assert!(indices.is_empty());
531 }
532
533 #[test]
534 fn minmax_threshold_zero() {
535 let indices = minmax(&[1.0, 2.0], &[3.0, 4.0], 0);
536 assert!(indices.is_empty());
537 }
538
539 #[test]
540 fn minmax_nan_handling() {
541 let x = vec![0.0, 1.0, f64::NAN, 3.0, 4.0];
542 let y = vec![0.0, 10.0, 5.0, -10.0, 0.0];
543 let indices = minmax(&x, &y, 4);
544 assert!(!indices.contains(&2), "NaN index should not be selected");
545 }
546
547 #[test]
550 fn lttb_large_dataset_smoke() {
551 let n = 100_000;
552 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
553 let y: Vec<f64> = x
554 .iter()
555 .map(|v| (v * 0.01).sin() + (v * 0.1).cos())
556 .collect();
557
558 let threshold = 500;
559 let indices = lttb(&x, &y, threshold);
560
561 assert_eq!(indices.len(), threshold);
562 assert_eq!(indices[0], 0);
563 assert_eq!(*indices.last().unwrap(), n - 1);
564
565 for w in indices.windows(2) {
567 assert!(w[0] < w[1], "indices must be strictly increasing");
568 }
569 }
570
571 #[test]
572 fn minmax_large_dataset_smoke() {
573 let n = 100_000;
574 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
575 let y: Vec<f64> = x.iter().map(|v| (v * 0.01).sin()).collect();
576
577 let threshold = 500;
578 let indices = minmax(&x, &y, threshold);
579
580 assert!(indices.len() <= threshold);
581 assert_eq!(indices[0], 0);
582 assert_eq!(*indices.last().unwrap(), n - 1);
583
584 for w in indices.windows(2) {
586 assert!(w[0] < w[1], "indices must be strictly increasing");
587 }
588 }
589
590 #[test]
593 fn lttb_bucket_boundaries_no_gaps_or_overlaps() {
594 let n = 12;
597 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
598 let y: Vec<f64> = x.clone();
599
600 let threshold = 6;
601 let indices = lttb(&x, &y, threshold);
602
603 assert_eq!(indices.len(), threshold);
604 assert_eq!(indices[0], 0);
605 assert_eq!(*indices.last().unwrap(), n as usize - 1);
606
607 for &idx in &indices {
609 assert!(idx < n as usize);
610 }
611 }
612
613 #[test]
614 fn lttb_all_nan_returns_empty() {
615 let x = vec![f64::NAN; 5];
616 let y = vec![f64::NAN; 5];
617 let indices = lttb(&x, &y, 3);
618 assert!(indices.is_empty());
619 }
620
621 #[test]
622 fn minmax_all_nan_returns_empty() {
623 let x = vec![f64::NAN; 5];
624 let y = vec![f64::NAN; 5];
625 let indices = minmax(&x, &y, 3);
626 assert!(indices.is_empty());
627 }
628
629 #[test]
630 fn lttb_indices_are_sorted() {
631 let n = 200;
632 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
633 let y: Vec<f64> = x.iter().map(|v| (v * 0.1).sin()).collect();
634 let indices = lttb(&x, &y, 20);
635 for w in indices.windows(2) {
636 assert!(w[0] < w[1], "LTTB indices must be strictly increasing");
637 }
638 }
639
640 #[test]
643 fn mode_default_is_auto() {
644 assert_eq!(DecimateMode::default(), DecimateMode::Auto);
645 }
646
647 #[test]
648 fn mode_auto_decimates_above_threshold_to_default() {
649 let n = DEFAULT_DECIMATE_THRESHOLD + 1;
650 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
651 let y: Vec<f64> = x.iter().map(|v| (v * 0.01).sin()).collect();
652 let indices = DecimateMode::Auto.resolve_indices(&x, &y);
653 assert_eq!(indices.len(), DEFAULT_DECIMATE_THRESHOLD);
654 assert_eq!(*indices.first().unwrap(), 0);
656 assert_eq!(*indices.last().unwrap(), n - 1);
657 for w in indices.windows(2) {
659 assert!(w[0] < w[1]);
660 }
661 }
662
663 #[test]
664 fn mode_auto_is_noop_exactly_at_threshold() {
665 let n = DEFAULT_DECIMATE_THRESHOLD;
666 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
667 let y = x.clone();
668 let indices = DecimateMode::Auto.resolve_indices(&x, &y);
669 assert_eq!(indices, (0..n).collect::<Vec<_>>());
670 }
671
672 #[test]
673 fn mode_auto_is_noop_below_threshold() {
674 let n = 10;
675 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
676 let y = x.clone();
677 let indices = DecimateMode::Auto.resolve_indices(&x, &y);
678 assert_eq!(indices, vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
679 }
680
681 #[test]
682 fn mode_off_never_decimates() {
683 let n = DEFAULT_DECIMATE_THRESHOLD * 4;
684 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
685 let y = x.clone();
686 let indices = DecimateMode::Off.resolve_indices(&x, &y);
687 assert_eq!(indices.len(), n);
688 assert_eq!(*indices.first().unwrap(), 0);
689 assert_eq!(*indices.last().unwrap(), n - 1);
690 }
691
692 #[test]
693 fn mode_explicit_lttb_uses_given_threshold() {
694 let n = 4000;
695 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
696 let y: Vec<f64> = x.iter().map(|v| (v * 0.05).sin()).collect();
697 let indices = DecimateMode::Explicit(250, DecimateMethod::Lttb).resolve_indices(&x, &y);
698 assert_eq!(indices.len(), 250);
699 assert_eq!(*indices.first().unwrap(), 0);
700 assert_eq!(*indices.last().unwrap(), n - 1);
701 }
702
703 #[test]
704 fn mode_explicit_minmax_uses_given_method() {
705 let n = 4000;
706 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
707 let y: Vec<f64> = x.iter().map(|v| (v * 0.05).sin()).collect();
708 let indices = DecimateMode::Explicit(250, DecimateMethod::MinMax).resolve_indices(&x, &y);
709 assert!(indices.len() <= 250);
710 assert_eq!(*indices.first().unwrap(), 0);
711 assert_eq!(*indices.last().unwrap(), n - 1);
712 }
713
714 #[test]
715 fn mode_explicit_is_noop_below_its_threshold() {
716 let n = 100;
718 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
719 let y = x.clone();
720 let indices = DecimateMode::Explicit(5000, DecimateMethod::Lttb).resolve_indices(&x, &y);
721 assert_eq!(indices, (0..n).collect::<Vec<_>>());
722 }
723
724 #[test]
725 fn mode_resolve_is_deterministic() {
726 let n = 20_000;
728 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
729 let y: Vec<f64> = x
730 .iter()
731 .map(|v| (v * 0.003).sin() + (v * 0.07).cos())
732 .collect();
733 let a = DecimateMode::Auto.resolve_indices(&x, &y);
734 let b = DecimateMode::Auto.resolve_indices(&x, &y);
735 assert_eq!(a, b);
736 }
737}