1use fastrand::Rng;
2use fastrand_contrib::RngExt;
3use serde::{Deserialize, Serialize};
4
5use crate::{LadduPhysicsError, LadduPhysicsResult};
6
7#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
9pub struct Histogram {
10 counts: Vec<f64>,
12 bin_edges: Vec<f64>,
14 underflow: f64,
15 overflow: f64,
16 errors: Vec<f64>,
17}
18
19impl Histogram {
20 pub fn new(counts: Vec<f64>, bin_edges: Vec<f64>) -> LadduPhysicsResult<Self> {
29 Self::new_with_flow(counts, bin_edges, 0.0, 0.0)
30 }
31
32 pub fn new_with_flow(
39 counts: Vec<f64>,
40 bin_edges: Vec<f64>,
41 underflow: f64,
42 overflow: f64,
43 ) -> LadduPhysicsResult<Self> {
44 let histogram = Self {
45 counts: counts.clone(),
46 bin_edges,
47 underflow,
48 overflow,
49 errors: counts.into_iter().map(|count| count.abs().sqrt()).collect(),
50 };
51 histogram.validate()?;
52 Ok(histogram)
53 }
54
55 pub fn empty(bins: usize, limits: (f64, f64)) -> LadduPhysicsResult<Self> {
62 Self::validate_bins(bins)?;
63 Self::validate_limits(limits)?;
64 let bin_edges = Self::calculate_bin_edges(bins, limits);
65 Self::empty_with_edges(bin_edges)
66 }
67
68 pub fn empty_with_edges(bin_edges: Vec<f64>) -> LadduPhysicsResult<Self> {
75 let counts = vec![0.0; bin_edges.len().saturating_sub(1)];
76 Self::new(counts, bin_edges)
77 }
78
79 pub fn from_values(
86 values: &[f64],
87 bins: usize,
88 limits: (f64, f64),
89 weights: Option<&[f64]>,
90 ) -> LadduPhysicsResult<Self> {
91 if let Some(weights) = weights
92 && values.len() != weights.len()
93 {
94 return Err(LadduPhysicsError::invalid_length(
95 "`weights`",
96 format!("same length as `values` ({})", values.len()),
97 weights.len(),
98 ));
99 }
100
101 let mut histogram = Self::empty(bins, limits)?;
102
103 for (i, &value) in values.iter().enumerate() {
104 let weight = weights.map_or(1.0, |weights| weights[i]);
105 histogram.fill_weighted(value, weight)?;
106 }
107
108 Ok(histogram)
109 }
110
111 pub fn from_values_with_edges(
118 values: &[f64],
119 bin_edges: Vec<f64>,
120 weights: Option<&[f64]>,
121 ) -> LadduPhysicsResult<Self> {
122 if let Some(weights) = weights
123 && values.len() != weights.len()
124 {
125 return Err(LadduPhysicsError::invalid_length(
126 "`weights`",
127 format!("same length as `values` ({})", values.len()),
128 weights.len(),
129 ));
130 }
131
132 let mut histogram = Self::empty_with_edges(bin_edges)?;
133
134 for (i, &value) in values.iter().enumerate() {
135 let weight = weights.map_or(1.0, |weights| weights[i]);
136 histogram.fill_weighted(value, weight)?;
137 }
138
139 Ok(histogram)
140 }
141
142 pub fn set_errors(&mut self, errors: &[f64]) -> LadduPhysicsResult<()> {
149 if self.counts.len() != errors.len() {
150 return Err(LadduPhysicsError::invalid_length(
151 "`errors`",
152 format!("same length as `counts` ({})", self.counts.len(),),
153 errors.len(),
154 ));
155 }
156
157 Self::validate_errors(errors)?;
158 self.errors = errors.to_vec();
159 Ok(())
160 }
161
162 pub fn fill(&mut self, value: f64) -> LadduPhysicsResult<()> {
168 self.fill_weighted(value, 1.0)
169 }
170
171 pub fn fill_weighted(&mut self, value: f64, weight: f64) -> LadduPhysicsResult<()> {
181 if !value.is_finite() {
182 return Err(LadduPhysicsError::invalid_value(
183 "histogram fill value",
184 "finite",
185 value,
186 ));
187 }
188
189 if !weight.is_finite() {
190 return Err(LadduPhysicsError::invalid_value(
191 "histogram fill weight",
192 "finite",
193 weight,
194 ));
195 }
196
197 let first = self.bin_edges[0];
198 let last = *self.bin_edges.last().unwrap();
199
200 if value < first {
201 self.underflow += weight;
202 } else if value >= last {
203 self.overflow += weight;
204 } else if let Some(index) = self.bin_index(value) {
205 self.counts[index] += weight;
206 self.errors[index] = self.errors[index].hypot(weight);
207 }
208
209 Ok(())
210 }
211
212 pub fn fill_with_error(&mut self, value: f64, error: f64) -> LadduPhysicsResult<()> {
219 self.fill_weighted_with_error(value, 1.0, error)
220 }
221
222 pub fn fill_weighted_with_error(
233 &mut self,
234 value: f64,
235 weight: f64,
236 error: f64,
237 ) -> LadduPhysicsResult<()> {
238 if !value.is_finite() {
239 return Err(LadduPhysicsError::invalid_value(
240 "histogram fill value",
241 "finite",
242 value,
243 ));
244 }
245
246 if !weight.is_finite() {
247 return Err(LadduPhysicsError::invalid_value(
248 "histogram fill weight",
249 "finite",
250 weight,
251 ));
252 }
253
254 Self::validate_error("histogram fill error", error)?;
255
256 let first = self.bin_edges[0];
257 let last = *self.bin_edges.last().unwrap();
258
259 if value < first {
260 self.underflow += weight;
261 } else if value >= last {
262 self.overflow += weight;
263 } else if let Some(index) = self.bin_index(value) {
264 self.counts[index] += weight;
265 self.errors[index] = self.errors[index].hypot(error);
266 }
267
268 Ok(())
269 }
270
271 fn calculate_bin_edges(bins: usize, limits: (f64, f64)) -> Vec<f64> {
272 let bin_width = (limits.1 - limits.0) / (bins as f64);
273 (0..=bins)
274 .map(|i| limits.0 + (i as f64 * bin_width))
275 .collect()
276 }
277
278 pub fn counts(&self) -> &[f64] {
280 &self.counts
281 }
282
283 pub fn set_counts(&mut self, counts: &[f64]) -> LadduPhysicsResult<()> {
290 if self.counts.len() != counts.len() {
291 return Err(LadduPhysicsError::invalid_length(
292 "`counts`",
293 format!("same length as existing `counts` ({})", self.counts.len()),
294 counts.len(),
295 ));
296 }
297
298 Self::validate_counts(counts)?;
299 self.counts.copy_from_slice(counts);
300 Ok(())
301 }
302
303 pub fn set_count(&mut self, bin_index: usize, value: f64) -> LadduPhysicsResult<()> {
310 if !value.is_finite() {
311 return Err(LadduPhysicsError::invalid_value(
312 "histogram bin count",
313 "finite",
314 value,
315 ));
316 }
317
318 if bin_index >= self.counts.len() {
319 return Err(LadduPhysicsError::invalid_value(
320 "histogram bin index",
321 format!("less than {}", self.counts.len()),
322 bin_index,
323 ));
324 }
325
326 self.counts[bin_index] = value;
327 Ok(())
328 }
329
330 pub fn set_error(&mut self, bin_index: usize, error: f64) -> LadduPhysicsResult<()> {
337 Self::validate_error("histogram bin error", error)?;
338
339 if bin_index >= self.errors.len() {
340 return Err(LadduPhysicsError::invalid_value(
341 "histogram bin index",
342 format!("less than {}", self.errors.len()),
343 bin_index,
344 ));
345 }
346
347 self.errors[bin_index] = error;
348 Ok(())
349 }
350
351 pub fn errors(&self) -> &[f64] {
359 &self.errors
360 }
361
362 pub fn bin_edges(&self) -> &[f64] {
364 &self.bin_edges
365 }
366
367 pub fn underflow(&self) -> f64 {
369 self.underflow
370 }
371
372 pub fn overflow(&self) -> f64 {
374 self.overflow
375 }
376
377 pub fn total_weight(&self) -> f64 {
379 self.counts.iter().sum()
380 }
381
382 pub fn total_weight_with_flow(&self) -> f64 {
384 self.underflow + self.total_weight() + self.overflow
385 }
386
387 pub fn limits(&self) -> (f64, f64) {
389 (self.bin_edges[0], self.bin_edges[self.bin_edges.len() - 1])
390 }
391
392 pub fn bins(&self) -> usize {
394 self.counts.len()
395 }
396
397 pub fn bin_index(&self, value: f64) -> Option<usize> {
401 let (&first, remaining) = self.bin_edges.split_first()?;
402 let &last = remaining.last()?;
403 if !value.is_finite() {
404 return None;
405 }
406
407 if value < first || value >= last {
408 return None;
409 }
410
411 match self
412 .bin_edges
413 .binary_search_by(|edge| edge.total_cmp(&value))
414 {
415 Ok(index) => {
416 if index == self.counts.len() {
417 None
418 } else {
419 Some(index)
420 }
421 }
422 Err(index) => Some(index - 1),
423 }
424 }
425
426 pub fn normalized(&self) -> LadduPhysicsResult<Self> {
439 self.validate_normalizable()?;
440
441 let total_weight = self.total_weight();
442
443 let counts = self
444 .counts
445 .iter()
446 .map(|count| count / total_weight)
447 .collect();
448
449 let mut histogram = Self::new_with_flow(counts, self.bin_edges.clone(), 0.0, 0.0)?;
450 histogram.set_errors(
451 &self
452 .errors
453 .iter()
454 .map(|error| error / total_weight.abs())
455 .collect::<Vec<_>>(),
456 )?;
457 Ok(histogram)
458 }
459
460 pub fn normalized_with_flow(&self) -> LadduPhysicsResult<Self> {
470 self.validate_normalizable_with_flow()?;
471
472 let total_weight = self.total_weight_with_flow();
473
474 let counts = self
475 .counts
476 .iter()
477 .map(|count| count / total_weight)
478 .collect();
479
480 let mut histogram = Self::new_with_flow(
481 counts,
482 self.bin_edges.clone(),
483 self.underflow / total_weight,
484 self.overflow / total_weight,
485 )?;
486 histogram.set_errors(
487 &self
488 .errors
489 .iter()
490 .map(|error| error / total_weight.abs())
491 .collect::<Vec<_>>(),
492 )?;
493 Ok(histogram)
494 }
495
496 pub fn density(&self) -> LadduPhysicsResult<Self> {
506 self.validate_probability_like()?;
507
508 let total_weight = self.total_weight();
509
510 let counts = self
511 .counts
512 .iter()
513 .enumerate()
514 .map(|(i, count)| {
515 let width = self.bin_edges[i + 1] - self.bin_edges[i];
516 count / (total_weight * width)
517 })
518 .collect();
519
520 let mut histogram = Self::new_with_flow(counts, self.bin_edges.clone(), 0.0, 0.0)?;
521 histogram.set_errors(
522 &self
523 .errors
524 .iter()
525 .enumerate()
526 .map(|(i, error)| {
527 let width = self.bin_edges[i + 1] - self.bin_edges[i];
528 error / (total_weight * width)
529 })
530 .collect::<Vec<_>>(),
531 )?;
532 Ok(histogram)
533 }
534
535 pub fn signed_density(&self) -> LadduPhysicsResult<Self> {
545 self.validate_normalizable()?;
546
547 let total_weight = self.total_weight();
548
549 let counts = self
550 .counts
551 .iter()
552 .enumerate()
553 .map(|(i, count)| {
554 let width = self.bin_edges[i + 1] - self.bin_edges[i];
555 count / (total_weight * width)
556 })
557 .collect();
558
559 let mut histogram = Self::new_with_flow(counts, self.bin_edges.clone(), 0.0, 0.0)?;
560 histogram.set_errors(
561 &self
562 .errors
563 .iter()
564 .enumerate()
565 .map(|(i, error)| {
566 let width = self.bin_edges[i + 1] - self.bin_edges[i];
567 error / (total_weight.abs() * width)
568 })
569 .collect::<Vec<_>>(),
570 )?;
571 Ok(histogram)
572 }
573
574 pub fn sample(&self, rng: &mut Rng) -> LadduPhysicsResult<f64> {
583 self.validate_probability_like()?;
584
585 let total_weight = self.total_weight();
586 let mut threshold = rng.f64() * total_weight;
587
588 for (i, count) in self.counts.iter().enumerate() {
589 threshold -= count;
590
591 if threshold <= 0.0 {
592 let low = self.bin_edges[i];
593 let high = self.bin_edges[i + 1];
594 return Ok(rng.f64_range(low..high));
595 }
596 }
597
598 let last = self.counts.len() - 1;
600 Ok(rng.f64_range(self.bin_edges[last]..self.bin_edges[last + 1]))
601 }
602
603 pub fn bin_center(&self, index: usize) -> Option<f64> {
605 if index < self.counts.len() {
606 Some(self.bin_center_unchecked(index))
607 } else {
608 None
609 }
610 }
611
612 fn bin_center_unchecked(&self, index: usize) -> f64 {
613 0.5 * (self.bin_edges[index] + self.bin_edges[index + 1])
614 }
615
616 fn validate_bins(bins: usize) -> LadduPhysicsResult<()> {
617 if bins == 0 {
618 return Err(LadduPhysicsError::invalid_length(
619 "histogram bins",
620 "at least 1",
621 bins,
622 ));
623 }
624 Ok(())
625 }
626
627 fn validate_limits(limits: (f64, f64)) -> LadduPhysicsResult<()> {
628 if !limits.0.is_finite() || !limits.1.is_finite() {
629 return Err(LadduPhysicsError::invalid_value(
630 "histogram limits",
631 "finite lower and upper edges",
632 format!("({}, {})", limits.0, limits.1),
633 ));
634 }
635
636 if limits.1 <= limits.0 {
637 return Err(LadduPhysicsError::invalid_relation(format!(
638 "histogram upper edge must be greater than lower edge, got ({}, {})",
639 limits.0, limits.1
640 )));
641 }
642 Ok(())
643 }
644
645 fn validate_structure(&self) -> LadduPhysicsResult<()> {
646 if self.bin_edges.len() < 2 {
647 return Err(LadduPhysicsError::invalid_length(
648 "histogram bin edges",
649 "at least 2",
650 self.bin_edges.len(),
651 ));
652 }
653
654 if self.counts.len() + 1 != self.bin_edges.len() {
655 return Err(LadduPhysicsError::invalid_length(
656 "histogram counts/bin_edges",
657 "counts.len() + 1 == bin_edges.len()",
658 format!(
659 "{} counts and {} edges",
660 self.counts.len(),
661 self.bin_edges.len()
662 ),
663 ));
664 }
665
666 if self.errors.len() != self.counts.len() {
667 return Err(LadduPhysicsError::invalid_length(
668 "histogram errors",
669 format!("same length as counts ({})", self.counts.len()),
670 self.errors.len(),
671 ));
672 }
673
674 for (index, edges) in self.bin_edges.windows(2).enumerate() {
675 if edges[1] <= edges[0] {
676 return Err(LadduPhysicsError::invalid_relation(format!(
677 "histogram bin edges must be strictly increasing at edge pair {index}"
678 )));
679 }
680 }
681
682 Ok(())
683 }
684
685 fn validate_finite(&self) -> LadduPhysicsResult<()> {
686 for (index, edge) in self.bin_edges.iter().enumerate() {
687 if !edge.is_finite() {
688 return Err(LadduPhysicsError::invalid_value(
689 format!("histogram bin edge {index}"),
690 "finite",
691 *edge,
692 ));
693 }
694 }
695
696 for (index, count) in self.counts.iter().enumerate() {
697 if !count.is_finite() {
698 return Err(LadduPhysicsError::invalid_value(
699 format!("histogram count {index}"),
700 "finite",
701 *count,
702 ));
703 }
704 }
705
706 Self::validate_errors(&self.errors)?;
707
708 if !self.underflow.is_finite() {
709 return Err(LadduPhysicsError::invalid_value(
710 "histogram underflow",
711 "finite",
712 self.underflow,
713 ));
714 }
715
716 if !self.overflow.is_finite() {
717 return Err(LadduPhysicsError::invalid_value(
718 "histogram overflow",
719 "finite",
720 self.overflow,
721 ));
722 }
723
724 Ok(())
725 }
726
727 fn validate_counts(counts: &[f64]) -> LadduPhysicsResult<()> {
728 for (index, count) in counts.iter().enumerate() {
729 if !count.is_finite() {
730 return Err(LadduPhysicsError::invalid_value(
731 format!("histogram count {index}"),
732 "finite",
733 *count,
734 ));
735 }
736 }
737 Ok(())
738 }
739
740 fn validate_error(name: impl Into<String>, error: f64) -> LadduPhysicsResult<()> {
741 if !error.is_finite() || error < 0.0 {
742 return Err(LadduPhysicsError::invalid_value(
743 name,
744 "finite and nonnegative",
745 error,
746 ));
747 }
748 Ok(())
749 }
750
751 fn validate_errors(errors: &[f64]) -> LadduPhysicsResult<()> {
752 for (index, error) in errors.iter().enumerate() {
753 Self::validate_error(format!("histogram error {index}"), *error)?;
754 }
755 Ok(())
756 }
757
758 fn validate_nonnegative_counts(&self) -> LadduPhysicsResult<()> {
759 for (index, count) in self.counts.iter().enumerate() {
760 if *count < 0.0 {
761 return Err(LadduPhysicsError::invalid_value(
762 format!("histogram count {index}"),
763 "nonnegative",
764 *count,
765 ));
766 }
767 }
768
769 if self.underflow < 0.0 {
770 return Err(LadduPhysicsError::invalid_value(
771 "histogram underflow",
772 "nonnegative",
773 self.underflow,
774 ));
775 }
776
777 if self.overflow < 0.0 {
778 return Err(LadduPhysicsError::invalid_value(
779 "histogram overflow",
780 "nonnegative",
781 self.overflow,
782 ));
783 }
784
785 Ok(())
786 }
787
788 fn validate_positive_total_weight(&self) -> LadduPhysicsResult<()> {
789 let total_weight = self.total_weight();
790
791 if total_weight <= 0.0 {
792 return Err(LadduPhysicsError::invalid_value(
793 "histogram total weight",
794 "positive",
795 total_weight,
796 ));
797 }
798
799 Ok(())
800 }
801
802 fn validate_positive_total_weight_with_flow(&self) -> LadduPhysicsResult<()> {
803 let total_weight = self.total_weight_with_flow();
804
805 if total_weight <= 0.0 {
806 return Err(LadduPhysicsError::invalid_value(
807 "histogram total weight with flow",
808 "positive",
809 total_weight,
810 ));
811 }
812
813 Ok(())
814 }
815
816 fn validate_normalizable(&self) -> LadduPhysicsResult<()> {
817 self.validate_structure()?;
818 self.validate_finite()?;
819 self.validate_positive_total_weight()?;
820 Ok(())
821 }
822
823 fn validate_normalizable_with_flow(&self) -> LadduPhysicsResult<()> {
824 self.validate_structure()?;
825 self.validate_finite()?;
826 self.validate_positive_total_weight_with_flow()?;
827 Ok(())
828 }
829
830 fn validate_probability_like(&self) -> LadduPhysicsResult<()> {
831 self.validate_structure()?;
832 self.validate_finite()?;
833 self.validate_nonnegative_counts()?;
834 self.validate_positive_total_weight()?;
835 Ok(())
836 }
837
838 fn validate(&self) -> LadduPhysicsResult<()> {
839 self.validate_structure()?;
840 self.validate_finite()?;
841 Ok(())
842 }
843}
844
845#[cfg(test)]
846mod tests {
847 use approx::assert_relative_eq;
848
849 use super::*;
850
851 #[test]
852 fn new_accepts_valid_histograms() {
853 let hist = Histogram::new(vec![2.0], vec![0.0, 1.0]).unwrap();
854
855 assert_relative_eq!(hist.counts(), &[2.0][..]);
856 assert_relative_eq!(hist.bin_edges(), &[0.0, 1.0][..]);
857 assert_relative_eq!(hist.underflow(), 0.0);
858 assert_relative_eq!(hist.overflow(), 0.0);
859 }
860
861 #[test]
862 fn new_accepts_zero_and_negative_weight_histograms() {
863 assert!(Histogram::new(vec![0.0], vec![0.0, 1.0]).is_ok());
864 assert!(Histogram::new(vec![-1.0], vec![0.0, 1.0]).is_ok());
865 }
866
867 #[test]
868 fn new_with_flow_accepts_valid_flow() {
869 let hist = Histogram::new_with_flow(vec![2.0], vec![0.0, 1.0], 3.0, 4.0).unwrap();
870
871 assert_relative_eq!(hist.counts(), &[2.0][..]);
872 assert_relative_eq!(hist.underflow(), 3.0);
873 assert_relative_eq!(hist.overflow(), 4.0);
874 assert_relative_eq!(hist.total_weight(), 2.0);
875 assert_relative_eq!(hist.total_weight_with_flow(), 9.0);
876 }
877
878 #[test]
879 fn new_rejects_invalid_structure() {
880 assert!(Histogram::new(vec![], vec![0.0]).is_err());
881 assert!(Histogram::new(vec![1.0, 2.0], vec![0.0, 1.0]).is_err());
882 assert!(Histogram::new(vec![1.0], vec![0.0, 0.0]).is_err());
883 assert!(Histogram::new(vec![1.0], vec![0.0, -1.0]).is_err());
884 }
885
886 #[test]
887 fn new_rejects_nonfinite_values() {
888 assert!(Histogram::new(vec![1.0], vec![0.0, f64::NAN]).is_err());
889 assert!(Histogram::new(vec![1.0], vec![0.0, f64::INFINITY]).is_err());
890 assert!(Histogram::new(vec![f64::NAN], vec![0.0, 1.0]).is_err());
891 assert!(Histogram::new(vec![f64::INFINITY], vec![0.0, 1.0]).is_err());
892
893 assert!(Histogram::new_with_flow(vec![1.0], vec![0.0, 1.0], f64::NAN, 0.0).is_err());
894 assert!(Histogram::new_with_flow(vec![1.0], vec![0.0, 1.0], 0.0, f64::NAN).is_err());
895 assert!(Histogram::new_with_flow(vec![1.0], vec![0.0, 1.0], f64::INFINITY, 0.0).is_err());
896 assert!(Histogram::new_with_flow(vec![1.0], vec![0.0, 1.0], 0.0, f64::INFINITY).is_err());
897 }
898
899 #[test]
900 fn empty_constructs_evenly_spaced_histogram() {
901 let hist = Histogram::empty(4, (0.0, 1.0)).unwrap();
902
903 assert_eq!(hist.bins(), 4);
904 assert_eq!(hist.limits(), (0.0, 1.0));
905 assert_relative_eq!(hist.counts(), &[0.0, 0.0, 0.0, 0.0][..]);
906 assert_relative_eq!(hist.bin_edges(), &[0.0, 0.25, 0.5, 0.75, 1.0][..]);
907 }
908
909 #[test]
910 fn empty_rejects_invalid_bins_and_limits() {
911 assert!(Histogram::empty(0, (0.0, 1.0)).is_err());
912 assert!(Histogram::empty(4, (1.0, 0.0)).is_err());
913 assert!(Histogram::empty(4, (0.0, 0.0)).is_err());
914 assert!(Histogram::empty(4, (f64::NAN, 1.0)).is_err());
915 assert!(Histogram::empty(4, (0.0, f64::NAN)).is_err());
916 }
917
918 #[test]
919 fn empty_with_edges_constructs_nonuniform_empty_histogram() {
920 let hist = Histogram::empty_with_edges(vec![0.0, 0.1, 0.4, 1.0]).unwrap();
921
922 assert_eq!(hist.bins(), 3);
923 assert_eq!(hist.limits(), (0.0, 1.0));
924 assert_relative_eq!(hist.counts(), &[0.0, 0.0, 0.0][..]);
925 assert_relative_eq!(hist.bin_edges(), &[0.0, 0.1, 0.4, 1.0][..]);
926 }
927
928 #[test]
929 fn from_values_fills_even_histogram_without_weights() {
930 let values = vec![-0.1, 0.0, 0.2, 0.25, 0.7, 0.99, 1.0, 1.2];
931 let hist = Histogram::from_values(&values, 4, (0.0, 1.0), None).unwrap();
932
933 assert_relative_eq!(hist.counts(), &[2.0, 1.0, 1.0, 1.0][..]);
934 assert_relative_eq!(hist.underflow(), 1.0);
935 assert_relative_eq!(hist.overflow(), 2.0);
936 assert_relative_eq!(hist.total_weight(), 5.0);
937 assert_relative_eq!(hist.total_weight_with_flow(), 8.0);
938 }
939
940 #[test]
941 fn from_values_fills_even_histogram_with_weights() {
942 let values = vec![-0.1, 0.1, 0.4, 0.8, 1.2];
943 let weights = vec![10.0, 1.0, 2.0, 3.0, 20.0];
944
945 let hist = Histogram::from_values(&values, 2, (0.0, 1.0), Some(&weights)).unwrap();
946
947 assert_relative_eq!(hist.counts(), &[3.0, 3.0][..]);
948 assert_relative_eq!(hist.underflow(), 10.0);
949 assert_relative_eq!(hist.overflow(), 20.0);
950 assert_relative_eq!(hist.total_weight(), 6.0);
951 assert_relative_eq!(hist.total_weight_with_flow(), 36.0);
952 }
953
954 #[test]
955 fn from_values_rejects_mismatched_weights() {
956 let values = vec![0.1, 0.2];
957 let weights = vec![1.0];
958
959 assert!(Histogram::from_values(&values, 2, (0.0, 1.0), Some(&weights)).is_err());
960 }
961
962 #[test]
963 fn from_values_rejects_nonfinite_values_and_weights() {
964 assert!(Histogram::from_values(&[f64::NAN], 2, (0.0, 1.0), None).is_err());
965 assert!(Histogram::from_values(&[0.5], 2, (0.0, 1.0), Some(&[f64::NAN])).is_err());
966 assert!(Histogram::from_values(&[0.5], 2, (0.0, 1.0), Some(&[f64::INFINITY])).is_err());
967 }
968
969 #[test]
970 fn from_values_with_edges_fills_nonuniform_histogram() {
971 let values = vec![-0.1, 0.0, 0.05, 0.1, 0.39, 0.4, 0.99, 1.0];
972 let hist =
973 Histogram::from_values_with_edges(&values, vec![0.0, 0.1, 0.4, 1.0], None).unwrap();
974
975 assert_relative_eq!(hist.counts(), &[2.0, 2.0, 2.0][..]);
976 assert_relative_eq!(hist.underflow(), 1.0);
977 assert_relative_eq!(hist.overflow(), 1.0);
978 }
979
980 #[test]
981 fn from_values_with_edges_rejects_mismatched_weights() {
982 let values = vec![0.1, 0.2];
983 let weights = vec![1.0];
984
985 assert!(
986 Histogram::from_values_with_edges(&values, vec![0.0, 1.0], Some(&weights)).is_err()
987 );
988 }
989
990 #[test]
991 fn fill_adds_unit_weight() {
992 let mut hist = Histogram::empty(2, (0.0, 1.0)).unwrap();
993
994 hist.fill(0.25).unwrap();
995 hist.fill(0.75).unwrap();
996 hist.fill(0.75).unwrap();
997
998 assert_relative_eq!(hist.counts(), &[1.0, 2.0][..]);
999 }
1000
1001 #[test]
1002 fn fill_weighted_tracks_underflow_and_overflow() {
1003 let mut hist = Histogram::empty(2, (0.0, 1.0)).unwrap();
1004
1005 hist.fill_weighted(-0.1, 2.0).unwrap();
1006 hist.fill_weighted(0.0, 3.0).unwrap();
1007 hist.fill_weighted(0.5, 4.0).unwrap();
1008 hist.fill_weighted(1.0, 5.0).unwrap();
1009
1010 assert_relative_eq!(hist.counts(), &[3.0, 4.0][..]);
1011 assert_relative_eq!(hist.underflow(), 2.0);
1012 assert_relative_eq!(hist.overflow(), 5.0);
1013 }
1014
1015 #[test]
1016 fn fill_weighted_accepts_negative_weights() {
1017 let mut hist = Histogram::empty(2, (0.0, 1.0)).unwrap();
1018
1019 hist.fill_weighted(0.25, -2.0).unwrap();
1020 hist.fill_weighted(-0.1, -3.0).unwrap();
1021 hist.fill_weighted(1.0, -4.0).unwrap();
1022
1023 assert_relative_eq!(hist.counts(), &[-2.0, 0.0][..]);
1024 assert_relative_eq!(hist.underflow(), -3.0);
1025 assert_relative_eq!(hist.overflow(), -4.0);
1026 }
1027
1028 #[test]
1029 fn fill_weighted_rejects_nonfinite_value_or_weight() {
1030 let mut hist = Histogram::empty(2, (0.0, 1.0)).unwrap();
1031
1032 assert!(hist.fill_weighted(f64::NAN, 1.0).is_err());
1033 assert!(hist.fill_weighted(f64::INFINITY, 1.0).is_err());
1034 assert!(hist.fill_weighted(0.5, f64::NAN).is_err());
1035 assert!(hist.fill_weighted(0.5, f64::INFINITY).is_err());
1036 }
1037
1038 #[test]
1039 fn errors_default_to_sqrt_absolute_counts() {
1040 let hist = Histogram::new(vec![4.0, -9.0], vec![0.0, 1.0, 2.0]).unwrap();
1041
1042 assert_relative_eq!(hist.errors(), &[2.0, 3.0][..]);
1043 }
1044
1045 #[test]
1046 fn weighted_fills_accumulate_uncertainties_in_quadrature() {
1047 let mut hist = Histogram::empty(1, (0.0, 1.0)).unwrap();
1048
1049 hist.fill_weighted(0.5, 3.0).unwrap();
1050 hist.fill_weighted(0.5, -4.0).unwrap();
1051
1052 assert_relative_eq!(hist.counts(), &[-1.0][..]);
1053 assert_relative_eq!(hist.errors(), &[5.0][..]);
1054 }
1055
1056 #[test]
1057 fn explicit_fill_errors_accumulate_in_quadrature() {
1058 let mut hist = Histogram::empty(1, (0.0, 1.0)).unwrap();
1059
1060 hist.fill_weighted_with_error(0.5, 10.0, 3.0).unwrap();
1061 hist.fill_weighted_with_error(0.5, 20.0, 4.0).unwrap();
1062
1063 assert_relative_eq!(hist.counts(), &[30.0][..]);
1064 assert_relative_eq!(hist.errors(), &[5.0][..]);
1065 assert!(hist.fill_with_error(0.5, -1.0).is_err());
1066 }
1067
1068 #[test]
1069 fn manual_counts_and_errors_are_validated() {
1070 let mut hist = Histogram::empty(2, (0.0, 1.0)).unwrap();
1071
1072 hist.set_counts(&[2.0, -3.0]).unwrap();
1073 hist.set_errors(&[0.5, 1.5]).unwrap();
1074 hist.set_count(1, 4.0).unwrap();
1075 hist.set_error(0, 0.25).unwrap();
1076
1077 assert_relative_eq!(hist.counts(), &[2.0, 4.0][..]);
1078 assert_relative_eq!(hist.errors(), &[0.25, 1.5][..]);
1079 assert!(hist.set_counts(&[1.0]).is_err());
1080 assert!(hist.set_counts(&[1.0, f64::NAN]).is_err());
1081 assert!(hist.set_errors(&[1.0]).is_err());
1082 assert!(hist.set_errors(&[1.0, -1.0]).is_err());
1083 assert!(hist.set_count(2, 1.0).is_err());
1084 assert!(hist.set_error(2, 1.0).is_err());
1085 }
1086
1087 #[test]
1088 fn bin_index_uses_lower_inclusive_upper_exclusive_edges() {
1089 let hist = Histogram::empty(4, (0.0, 1.0)).unwrap();
1090
1091 assert_eq!(hist.bin_index(-0.1), None);
1092 assert_eq!(hist.bin_index(0.0), Some(0));
1093 assert_eq!(hist.bin_index(0.249), Some(0));
1094 assert_eq!(hist.bin_index(0.25), Some(1));
1095 assert_eq!(hist.bin_index(0.5), Some(2));
1096 assert_eq!(hist.bin_index(0.75), Some(3));
1097 assert_eq!(hist.bin_index(0.999), Some(3));
1098 assert_eq!(hist.bin_index(1.0), None);
1099 }
1100
1101 #[test]
1102 fn bin_index_handles_nonuniform_edges() {
1103 let hist = Histogram::empty_with_edges(vec![0.0, 0.1, 0.4, 1.0]).unwrap();
1104
1105 assert_eq!(hist.bin_index(0.0), Some(0));
1106 assert_eq!(hist.bin_index(0.099), Some(0));
1107 assert_eq!(hist.bin_index(0.1), Some(1));
1108 assert_eq!(hist.bin_index(0.399), Some(1));
1109 assert_eq!(hist.bin_index(0.4), Some(2));
1110 assert_eq!(hist.bin_index(0.999), Some(2));
1111 assert_eq!(hist.bin_index(1.0), None);
1112 }
1113
1114 #[test]
1115 fn normalized_scales_counts_by_in_range_weight() {
1116 let hist = Histogram::new_with_flow(vec![2.0, 6.0], vec![0.0, 1.0, 2.0], 4.0, 8.0).unwrap();
1117
1118 let normalized = hist.normalized().unwrap();
1119
1120 assert_relative_eq!(normalized.counts(), &[0.25, 0.75][..]);
1121 assert_relative_eq!(normalized.underflow(), 0.0);
1122 assert_relative_eq!(normalized.overflow(), 0.0);
1123 assert_relative_eq!(normalized.total_weight(), 1.0);
1124 assert_relative_eq!(normalized.total_weight_with_flow(), 1.0);
1125 }
1126
1127 #[test]
1128 fn normalization_and_density_scale_errors() {
1129 let mut hist = Histogram::new(vec![2.0, 6.0], vec![0.0, 1.0, 3.0]).unwrap();
1130 hist.set_errors(&[1.0, 3.0]).unwrap();
1131
1132 let normalized = hist.normalized().unwrap();
1133 assert_relative_eq!(normalized.errors(), &[0.125, 0.375][..]);
1134
1135 let density = hist.density().unwrap();
1136 assert_relative_eq!(density.errors(), &[0.125, 0.1875][..]);
1137 }
1138
1139 #[test]
1140 fn normalized_with_flow_scales_counts_and_flow_by_total_weight_with_flow() {
1141 let hist = Histogram::new_with_flow(vec![2.0, 6.0], vec![0.0, 1.0, 2.0], 4.0, 8.0).unwrap();
1142
1143 let normalized = hist.normalized_with_flow().unwrap();
1144
1145 assert_relative_eq!(normalized.counts(), &[0.1, 0.3][..]);
1146 assert_relative_eq!(normalized.underflow(), 0.2);
1147 assert_relative_eq!(normalized.overflow(), 0.4);
1148 assert_relative_eq!(normalized.total_weight(), 0.4);
1149 assert_relative_eq!(normalized.total_weight_with_flow(), 1.0);
1150 }
1151
1152 #[test]
1153 fn normalized_rejects_zero_in_range_weight() {
1154 let hist = Histogram::new_with_flow(vec![0.0], vec![0.0, 1.0], 1.0, 1.0).unwrap();
1155
1156 assert!(hist.normalized().is_err());
1157 }
1158
1159 #[test]
1160 fn density_converts_counts_to_probability_density_and_drops_flow() {
1161 let hist = Histogram::new_with_flow(vec![2.0, 6.0], vec![0.0, 1.0, 3.0], 4.0, 8.0).unwrap();
1162
1163 let density = hist.density().unwrap();
1164
1165 assert_relative_eq!(density.counts(), &[0.25, 0.375][..]);
1166 assert_relative_eq!(density.underflow(), 0.0);
1167 assert_relative_eq!(density.overflow(), 0.0);
1168
1169 let integral = density.counts()[0] * 1.0 + density.counts()[1] * 2.0;
1170 assert_relative_eq!(integral, 1.0);
1171 }
1172
1173 #[test]
1174 fn density_rejects_negative_counts_or_flow() {
1175 let negative_count = Histogram::new(vec![-1.0], vec![0.0, 1.0]).unwrap();
1176 assert!(negative_count.density().is_err());
1177
1178 let negative_underflow =
1179 Histogram::new_with_flow(vec![1.0], vec![0.0, 1.0], -1.0, 0.0).unwrap();
1180 assert!(negative_underflow.density().is_err());
1181
1182 let negative_overflow =
1183 Histogram::new_with_flow(vec![1.0], vec![0.0, 1.0], 0.0, -1.0).unwrap();
1184 assert!(negative_overflow.density().is_err());
1185 }
1186
1187 #[test]
1188 fn sample_returns_value_inside_histogram_limits() {
1189 let hist = Histogram::new(vec![1.0, 1.0], vec![0.0, 1.0, 2.0]).unwrap();
1190 let mut rng = Rng::with_seed(12345);
1191
1192 for _ in 0..100 {
1193 let value = hist.sample(&mut rng).unwrap();
1194 assert!((0.0..2.0).contains(&value));
1195 }
1196 }
1197
1198 #[test]
1199 fn sample_rejects_non_probability_like_histograms() {
1200 let negative_count = Histogram::new(vec![-1.0], vec![0.0, 1.0]).unwrap();
1201 assert!(negative_count.sample(&mut Rng::with_seed(1)).is_err());
1202
1203 let zero_count = Histogram::new(vec![0.0], vec![0.0, 1.0]).unwrap();
1204 assert!(zero_count.sample(&mut Rng::with_seed(1)).is_err());
1205 }
1206
1207 #[test]
1208 fn bin_center_returns_center_for_valid_index() {
1209 let hist = Histogram::empty_with_edges(vec![0.0, 0.5, 2.0]).unwrap();
1210
1211 assert_relative_eq!(hist.bin_center(0).unwrap(), 0.25);
1212 assert_relative_eq!(hist.bin_center(1).unwrap(), 1.25);
1213 }
1214
1215 #[test]
1216 fn bin_center_returns_none_for_invalid_index() {
1217 let hist = Histogram::empty_with_edges(vec![0.0, 0.5, 2.0]).unwrap();
1218
1219 assert_eq!(hist.bin_center(2), None);
1220 }
1221}