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.iter().enumerate().skip(bucket_start).take(bucket_end - bucket_start) {
199 let area = triangle_area(px, py, x[orig], y[orig], avg_x, avg_y);
200 if area > best_area {
201 best_area = area;
202 best_valid_idx = vi;
203 }
204 }
205
206 selected.push(valid[best_valid_idx]);
207 prev_selected_idx = best_valid_idx;
208 }
209
210 selected.push(valid[valid_n - 1]);
212
213 selected
214}
215
216pub fn minmax(x: &[f64], y: &[f64], threshold: usize) -> Vec<usize> {
227 assert_eq!(x.len(), y.len(), "x and y must have the same length");
228
229 let n = x.len();
230
231 if threshold == 0 {
232 return vec![];
233 }
234 if n == 0 {
235 return vec![];
236 }
237 if threshold == 1 {
238 return vec![0];
239 }
240 if threshold >= n {
241 return (0..n).collect();
242 }
243
244 let valid: Vec<usize> = (0..n)
246 .filter(|&i| x[i].is_finite() && y[i].is_finite())
247 .collect();
248 let valid_n = valid.len();
249
250 if valid_n == 0 {
251 return vec![];
252 }
253 if threshold >= valid_n {
254 return valid;
255 }
256
257 let mut selected = Vec::with_capacity(threshold);
258
259 selected.push(valid[0]);
261
262 let pairs = (threshold - 2) / 2;
265 let bucket_count = if pairs == 0 { 1 } else { pairs };
266 let interior = valid_n - 2; for bucket_i in 0..bucket_count {
269 let bucket_start = 1 + (bucket_i * interior) / bucket_count;
270 let bucket_end = 1 + ((bucket_i + 1) * interior) / bucket_count;
271
272 if bucket_start >= bucket_end {
273 continue;
274 }
275
276 let mut min_idx = bucket_start;
277 let mut max_idx = bucket_start;
278 let mut min_val = y[valid[bucket_start]];
279 let mut max_val = y[valid[bucket_start]];
280
281 for vi in bucket_start..bucket_end {
282 let yv = y[valid[vi]];
283 if yv < min_val {
284 min_val = yv;
285 min_idx = vi;
286 }
287 if yv > max_val {
288 max_val = yv;
289 max_idx = vi;
290 }
291 }
292
293 if min_idx == max_idx {
295 selected.push(valid[min_idx]);
296 } else if min_idx < max_idx {
297 selected.push(valid[min_idx]);
298 selected.push(valid[max_idx]);
299 } else {
300 selected.push(valid[max_idx]);
301 selected.push(valid[min_idx]);
302 }
303 }
304
305 let last = valid[valid_n - 1];
307 if selected.last() != Some(&last) {
309 selected.push(last);
310 }
311
312 selected
313}
314
315#[inline]
323fn triangle_area(x_a: f64, y_a: f64, x_b: f64, y_b: f64, x_c: f64, y_c: f64) -> f64 {
324 (x_a * (y_b - y_c) + x_b * (y_c - y_a) + x_c * (y_a - y_b)).abs()
325}
326
327#[cfg(test)]
332mod tests {
333 use super::*;
334 use std::f64::consts::PI;
335
336 #[test]
339 fn lttb_identity_when_threshold_ge_len() {
340 let x = vec![0.0, 1.0, 2.0, 3.0, 4.0];
341 let y = vec![0.0, 1.0, 4.0, 9.0, 16.0];
342 let indices = lttb(&x, &y, 5);
343 assert_eq!(indices, vec![0, 1, 2, 3, 4]);
344
345 let indices = lttb(&x, &y, 100);
346 assert_eq!(indices, vec![0, 1, 2, 3, 4]);
347 }
348
349 #[test]
350 fn lttb_first_and_last_always_preserved() {
351 let x: Vec<f64> = (0..100).map(|i| i as f64).collect();
352 let y: Vec<f64> = x.iter().map(|v| v * v).collect();
353 let indices = lttb(&x, &y, 10);
354 assert_eq!(*indices.first().unwrap(), 0);
355 assert_eq!(*indices.last().unwrap(), 99);
356 assert_eq!(indices.len(), 10);
357 }
358
359 #[test]
360 fn lttb_threshold_2_returns_first_and_last() {
361 let x = vec![0.0, 1.0, 2.0, 3.0, 4.0];
362 let y = vec![10.0, 20.0, 30.0, 40.0, 50.0];
363 let indices = lttb(&x, &y, 2);
364 assert_eq!(indices, vec![0, 4]);
365 }
366
367 #[test]
368 fn lttb_threshold_3_returns_first_middle_last() {
369 let x = vec![0.0, 1.0, 2.0, 3.0, 4.0];
370 let y = vec![0.0, 5.0, 0.0, 5.0, 0.0];
371 let indices = lttb(&x, &y, 3);
372 assert_eq!(indices.len(), 3);
373 assert_eq!(indices[0], 0);
374 assert_eq!(*indices.last().unwrap(), 4);
375 }
376
377 #[test]
378 fn lttb_known_triangle_area() {
379 let area = triangle_area(0.0, 0.0, 1.0, 0.0, 0.0, 1.0);
382 assert!((area - 1.0).abs() < 1e-12);
383
384 let area = triangle_area(0.0, 0.0, 1.0, 1.0, 2.0, 2.0);
386 assert!(area.abs() < 1e-12);
387 }
388
389 #[test]
390 fn lttb_linear_data_any_subset_looks_identical() {
391 let n = 50;
394 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
395 let y: Vec<f64> = x.iter().map(|v| 2.0 * v + 1.0).collect();
396 let indices = lttb(&x, &y, 10);
397 assert_eq!(indices.len(), 10);
398
399 for &idx in &indices {
401 let expected = 2.0 * x[idx] + 1.0;
402 assert!(
403 (y[idx] - expected).abs() < 1e-12,
404 "point at index {} deviates from y = 2x + 1",
405 idx
406 );
407 }
408 }
409
410 #[test]
411 fn lttb_sinusoidal_peaks_preserved() {
412 let n = 1000;
414 let x: Vec<f64> = (0..n).map(|i| i as f64 * 2.0 * PI / 100.0).collect();
415 let y: Vec<f64> = x.iter().map(|v| v.sin()).collect();
416
417 let indices = lttb(&x, &y, 50);
418
419 let selected_y: Vec<f64> = indices.iter().map(|&i| y[i]).collect();
421 let max_selected = selected_y.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
422 let min_selected = selected_y.iter().cloned().fold(f64::INFINITY, f64::min);
423
424 assert!(max_selected > 0.95, "peak not preserved: max = {}", max_selected);
425 assert!(min_selected < -0.95, "trough not preserved: min = {}", min_selected);
426 }
427
428 #[test]
429 fn lttb_single_point() {
430 let x = vec![42.0];
431 let y = vec![7.0];
432 let indices = lttb(&x, &y, 5);
433 assert_eq!(indices, vec![0]);
434 }
435
436 #[test]
437 fn lttb_two_points() {
438 let x = vec![1.0, 2.0];
439 let y = vec![3.0, 4.0];
440 let indices = lttb(&x, &y, 5);
441 assert_eq!(indices, vec![0, 1]);
442 }
443
444 #[test]
445 fn lttb_nan_handling() {
446 let x = vec![0.0, 1.0, f64::NAN, 3.0, 4.0, 5.0];
447 let y = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
448 let indices = lttb(&x, &y, 4);
449 assert!(!indices.contains(&2), "NaN index should not be selected");
451 assert_eq!(*indices.first().unwrap(), 0);
453 assert_eq!(*indices.last().unwrap(), 5);
454 }
455
456 #[test]
457 fn lttb_empty_input() {
458 let x: Vec<f64> = vec![];
459 let y: Vec<f64> = vec![];
460 let indices = lttb(&x, &y, 10);
461 assert!(indices.is_empty());
462 }
463
464 #[test]
465 fn lttb_threshold_zero() {
466 let x = vec![0.0, 1.0, 2.0];
467 let y = vec![0.0, 1.0, 2.0];
468 let indices = lttb(&x, &y, 0);
469 assert!(indices.is_empty());
470 }
471
472 #[test]
473 fn lttb_threshold_one() {
474 let x = vec![0.0, 1.0, 2.0];
475 let y = vec![0.0, 1.0, 2.0];
476 let indices = lttb(&x, &y, 1);
477 assert_eq!(indices, vec![0]);
478 }
479
480 #[test]
483 fn minmax_preserves_extremes() {
484 let x: Vec<f64> = (0..20).map(|i| i as f64).collect();
486 let mut y: Vec<f64> = vec![0.0; 20];
487 y[5] = 100.0; y[15] = -100.0; let indices = minmax(&x, &y, 10);
491 let selected_y: Vec<f64> = indices.iter().map(|&i| y[i]).collect();
492
493 assert!(selected_y.contains(&100.0), "spike not preserved");
494 assert!(selected_y.contains(&-100.0), "dip not preserved");
495 }
496
497 #[test]
498 fn minmax_identity_when_threshold_ge_len() {
499 let x = vec![0.0, 1.0, 2.0, 3.0];
500 let y = vec![1.0, 2.0, 3.0, 4.0];
501 let indices = minmax(&x, &y, 10);
502 assert_eq!(indices, vec![0, 1, 2, 3]);
503 }
504
505 #[test]
506 fn minmax_first_and_last_preserved() {
507 let x: Vec<f64> = (0..50).map(|i| i as f64).collect();
508 let y: Vec<f64> = x.iter().map(|v| v.sin()).collect();
509 let indices = minmax(&x, &y, 10);
510 assert_eq!(*indices.first().unwrap(), 0);
511 assert_eq!(*indices.last().unwrap(), 49);
512 }
513
514 #[test]
515 fn minmax_empty_input() {
516 let indices = minmax(&[], &[], 5);
517 assert!(indices.is_empty());
518 }
519
520 #[test]
521 fn minmax_threshold_zero() {
522 let indices = minmax(&[1.0, 2.0], &[3.0, 4.0], 0);
523 assert!(indices.is_empty());
524 }
525
526 #[test]
527 fn minmax_nan_handling() {
528 let x = vec![0.0, 1.0, f64::NAN, 3.0, 4.0];
529 let y = vec![0.0, 10.0, 5.0, -10.0, 0.0];
530 let indices = minmax(&x, &y, 4);
531 assert!(!indices.contains(&2), "NaN index should not be selected");
532 }
533
534 #[test]
537 fn lttb_large_dataset_smoke() {
538 let n = 100_000;
539 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
540 let y: Vec<f64> = x.iter().map(|v| (v * 0.01).sin() + (v * 0.1).cos()).collect();
541
542 let threshold = 500;
543 let indices = lttb(&x, &y, threshold);
544
545 assert_eq!(indices.len(), threshold);
546 assert_eq!(indices[0], 0);
547 assert_eq!(*indices.last().unwrap(), n - 1);
548
549 for w in indices.windows(2) {
551 assert!(w[0] < w[1], "indices must be strictly increasing");
552 }
553 }
554
555 #[test]
556 fn minmax_large_dataset_smoke() {
557 let n = 100_000;
558 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
559 let y: Vec<f64> = x.iter().map(|v| (v * 0.01).sin()).collect();
560
561 let threshold = 500;
562 let indices = minmax(&x, &y, threshold);
563
564 assert!(indices.len() <= threshold);
565 assert_eq!(indices[0], 0);
566 assert_eq!(*indices.last().unwrap(), n - 1);
567
568 for w in indices.windows(2) {
570 assert!(w[0] < w[1], "indices must be strictly increasing");
571 }
572 }
573
574 #[test]
577 fn lttb_bucket_boundaries_no_gaps_or_overlaps() {
578 let n = 12;
581 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
582 let y: Vec<f64> = x.clone();
583
584 let threshold = 6;
585 let indices = lttb(&x, &y, threshold);
586
587 assert_eq!(indices.len(), threshold);
588 assert_eq!(indices[0], 0);
589 assert_eq!(*indices.last().unwrap(), n as usize - 1);
590
591 for &idx in &indices {
593 assert!(idx < n as usize);
594 }
595 }
596
597 #[test]
598 fn lttb_all_nan_returns_empty() {
599 let x = vec![f64::NAN; 5];
600 let y = vec![f64::NAN; 5];
601 let indices = lttb(&x, &y, 3);
602 assert!(indices.is_empty());
603 }
604
605 #[test]
606 fn minmax_all_nan_returns_empty() {
607 let x = vec![f64::NAN; 5];
608 let y = vec![f64::NAN; 5];
609 let indices = minmax(&x, &y, 3);
610 assert!(indices.is_empty());
611 }
612
613 #[test]
614 fn lttb_indices_are_sorted() {
615 let n = 200;
616 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
617 let y: Vec<f64> = x.iter().map(|v| (v * 0.1).sin()).collect();
618 let indices = lttb(&x, &y, 20);
619 for w in indices.windows(2) {
620 assert!(w[0] < w[1], "LTTB indices must be strictly increasing");
621 }
622 }
623
624 #[test]
627 fn mode_default_is_auto() {
628 assert_eq!(DecimateMode::default(), DecimateMode::Auto);
629 }
630
631 #[test]
632 fn mode_auto_decimates_above_threshold_to_default() {
633 let n = DEFAULT_DECIMATE_THRESHOLD + 1;
634 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
635 let y: Vec<f64> = x.iter().map(|v| (v * 0.01).sin()).collect();
636 let indices = DecimateMode::Auto.resolve_indices(&x, &y);
637 assert_eq!(indices.len(), DEFAULT_DECIMATE_THRESHOLD);
638 assert_eq!(*indices.first().unwrap(), 0);
640 assert_eq!(*indices.last().unwrap(), n - 1);
641 for w in indices.windows(2) {
643 assert!(w[0] < w[1]);
644 }
645 }
646
647 #[test]
648 fn mode_auto_is_noop_exactly_at_threshold() {
649 let n = DEFAULT_DECIMATE_THRESHOLD;
650 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
651 let y = x.clone();
652 let indices = DecimateMode::Auto.resolve_indices(&x, &y);
653 assert_eq!(indices, (0..n).collect::<Vec<_>>());
654 }
655
656 #[test]
657 fn mode_auto_is_noop_below_threshold() {
658 let n = 10;
659 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
660 let y = x.clone();
661 let indices = DecimateMode::Auto.resolve_indices(&x, &y);
662 assert_eq!(indices, vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
663 }
664
665 #[test]
666 fn mode_off_never_decimates() {
667 let n = DEFAULT_DECIMATE_THRESHOLD * 4;
668 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
669 let y = x.clone();
670 let indices = DecimateMode::Off.resolve_indices(&x, &y);
671 assert_eq!(indices.len(), n);
672 assert_eq!(*indices.first().unwrap(), 0);
673 assert_eq!(*indices.last().unwrap(), n - 1);
674 }
675
676 #[test]
677 fn mode_explicit_lttb_uses_given_threshold() {
678 let n = 4000;
679 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
680 let y: Vec<f64> = x.iter().map(|v| (v * 0.05).sin()).collect();
681 let indices =
682 DecimateMode::Explicit(250, DecimateMethod::Lttb).resolve_indices(&x, &y);
683 assert_eq!(indices.len(), 250);
684 assert_eq!(*indices.first().unwrap(), 0);
685 assert_eq!(*indices.last().unwrap(), n - 1);
686 }
687
688 #[test]
689 fn mode_explicit_minmax_uses_given_method() {
690 let n = 4000;
691 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
692 let y: Vec<f64> = x.iter().map(|v| (v * 0.05).sin()).collect();
693 let indices =
694 DecimateMode::Explicit(250, DecimateMethod::MinMax).resolve_indices(&x, &y);
695 assert!(indices.len() <= 250);
696 assert_eq!(*indices.first().unwrap(), 0);
697 assert_eq!(*indices.last().unwrap(), n - 1);
698 }
699
700 #[test]
701 fn mode_explicit_is_noop_below_its_threshold() {
702 let n = 100;
704 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
705 let y = x.clone();
706 let indices =
707 DecimateMode::Explicit(5000, DecimateMethod::Lttb).resolve_indices(&x, &y);
708 assert_eq!(indices, (0..n).collect::<Vec<_>>());
709 }
710
711 #[test]
712 fn mode_resolve_is_deterministic() {
713 let n = 20_000;
715 let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
716 let y: Vec<f64> = x.iter().map(|v| (v * 0.003).sin() + (v * 0.07).cos()).collect();
717 let a = DecimateMode::Auto.resolve_indices(&x, &y);
718 let b = DecimateMode::Auto.resolve_indices(&x, &y);
719 assert_eq!(a, b);
720 }
721}