Skip to main content

laddu_physics/
histogram.rs

1use fastrand::Rng;
2use fastrand_contrib::RngExt;
3use serde::{Deserialize, Serialize};
4
5use crate::{LadduPhysicsError, LadduPhysicsResult};
6
7/// A simple weighted histogram with explicit bin edges.
8#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
9pub struct Histogram {
10    /// The number of counts in each bin (can be [`f64`]s since these might be weighted counts)
11    counts: Vec<f64>,
12    /// The edges of each bin (length is one greater than `counts`)
13    bin_edges: Vec<f64>,
14    underflow: f64,
15    overflow: f64,
16    errors: Vec<f64>,
17}
18
19impl Histogram {
20    /// Construct and validate a histogram from weighted bin counts and bin edges.
21    ///
22    /// The argument order matches `numpy.histogram`, so its result can be forwarded directly.
23    ///
24    /// # Errors
25    ///
26    /// Returns [`LadduPhysicsError`] when counts or edges are non-finite, the
27    /// edge count is inconsistent with the bins, or edges are not increasing.
28    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    /// Construct a histogram including explicit underflow and overflow weights.
33    ///
34    /// # Errors
35    ///
36    /// Returns [`LadduPhysicsError`] when counts, flow weights, or edges are
37    /// non-finite, lengths are inconsistent, or edges are not increasing.
38    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    /// Construct an empty, uniformly binned histogram.
56    ///
57    /// # Errors
58    ///
59    /// Returns [`LadduPhysicsError`] when `bins` is zero or the limits are
60    /// non-finite or not increasing.
61    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    /// Construct an empty histogram from explicit bin edges.
69    ///
70    /// # Errors
71    ///
72    /// Returns [`LadduPhysicsError`] when fewer than two finite, strictly
73    /// increasing edges are supplied.
74    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    /// Fill a uniformly binned histogram from values and optional weights.
80    ///
81    /// # Errors
82    ///
83    /// Returns [`LadduPhysicsError`] when histogram geometry is invalid,
84    /// weights have the wrong length, or a value or weight is non-finite.
85    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    /// Fill an explicitly binned histogram from values and optional weights.
112    ///
113    /// # Errors
114    ///
115    /// Returns [`LadduPhysicsError`] when edges are invalid, weights have the
116    /// wrong length, or a value or weight is non-finite.
117    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    /// Replace the uncertainties on all bins.
143    ///
144    /// # Errors
145    ///
146    /// Returns [`LadduPhysicsError`] when `errors` has the wrong length or
147    /// contains a negative or non-finite uncertainty.
148    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    /// Add one unit-weight entry.
163    ///
164    /// # Errors
165    ///
166    /// Returns [`LadduPhysicsError`] when `value` is non-finite.
167    pub fn fill(&mut self, value: f64) -> LadduPhysicsResult<()> {
168        self.fill_weighted(value, 1.0)
169    }
170
171    /// Add an entry with an explicit weight.
172    ///
173    /// # Errors
174    ///
175    /// Returns [`LadduPhysicsError`] when `value` or `weight` is non-finite.
176    ///
177    /// # Panics
178    ///
179    /// Panics if this histogram's validated edge list is unexpectedly empty.
180    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    /// Add one unit-weight entry with the given entry uncertainty.
213    ///
214    /// # Errors
215    ///
216    /// Returns [`LadduPhysicsError`] when `value` is non-finite or `error` is
217    /// negative or non-finite.
218    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    /// Add an entry with an explicit weight and uncertainty.
223    ///
224    /// # Errors
225    ///
226    /// Returns [`LadduPhysicsError`] when `value` or `weight` is non-finite, or
227    /// `error` is negative or non-finite.
228    ///
229    /// # Panics
230    ///
231    /// Panics if this histogram's validated edge list is unexpectedly empty.
232    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    /// Return the number of weighted counts in each bin.
279    pub fn counts(&self) -> &[f64] {
280        &self.counts
281    }
282
283    /// Replace the contents of all bins without changing their uncertainties.
284    ///
285    /// # Errors
286    ///
287    /// Returns [`LadduPhysicsError`] when `counts` has the wrong length or
288    /// contains a non-finite value.
289    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    /// Manually set the counts in a bin.
304    ///
305    /// # Errors
306    ///
307    /// Returns [`LadduPhysicsError`] when `bin_index` is out of range or
308    /// `value` is non-finite.
309    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    /// Manually set the uncertainty in a bin.
331    ///
332    /// # Errors
333    ///
334    /// Returns [`LadduPhysicsError`] when `bin_index` is out of range or
335    /// `error` is negative or non-finite.
336    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    /// Return the uncertainties on each bin.
352    ///
353    /// # Note
354    ///
355    /// Histograms filled from values use the square root of the sum of squared
356    /// weights. Histograms constructed from counts default to
357    /// `sqrt(abs(count))`.
358    pub fn errors(&self) -> &[f64] {
359        &self.errors
360    }
361
362    /// Return the bin edges.
363    pub fn bin_edges(&self) -> &[f64] {
364        &self.bin_edges
365    }
366
367    /// Return the accumulated underflow weight.
368    pub fn underflow(&self) -> f64 {
369        self.underflow
370    }
371
372    /// Return the accumulated overflow weight.
373    pub fn overflow(&self) -> f64 {
374        self.overflow
375    }
376
377    /// Return the total histogram weight.
378    pub fn total_weight(&self) -> f64 {
379        self.counts.iter().sum()
380    }
381
382    /// Return total weight including underflow and overflow.
383    pub fn total_weight_with_flow(&self) -> f64 {
384        self.underflow + self.total_weight() + self.overflow
385    }
386
387    /// Return the lowest and highest bin edges.
388    pub fn limits(&self) -> (f64, f64) {
389        (self.bin_edges[0], self.bin_edges[self.bin_edges.len() - 1])
390    }
391
392    /// Return the number of bins.
393    pub fn bins(&self) -> usize {
394        self.counts.len()
395    }
396
397    /// Return the bin index for a value.
398    ///
399    /// The lower edge is inclusive and the upper edge is exclusive.
400    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    /// Return a normalized histogram whose in-range bin counts sum to 1.
427    ///
428    /// Underflow and overflow are discarded because they are outside the
429    /// histogram domain.
430    ///
431    /// Negative bin counts are allowed, so this is an algebraic normalization,
432    /// not necessarily a probability distribution.
433    ///
434    /// # Errors
435    ///
436    /// Returns [`LadduPhysicsError`] when the histogram is invalid or has zero
437    /// or non-finite in-range total weight.
438    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    /// Return a normalized histogram whose bins plus underflow/overflow sum to 1.
461    ///
462    /// Negative counts are allowed, so this is algebraic normalization, not
463    /// necessarily a probability distribution.
464    ///
465    /// # Errors
466    ///
467    /// Returns [`LadduPhysicsError`] when the histogram is invalid or has zero
468    /// or non-finite total weight including flow bins.
469    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    /// Return a probability density histogram.
497    ///
498    /// Requires nonnegative in-range counts. Underflow and overflow are discarded
499    /// because they do not have finite bin widths.
500    ///
501    /// # Errors
502    ///
503    /// Returns [`LadduPhysicsError`] when bin counts are negative or non-finite,
504    /// or their in-range total is not positive and finite.
505    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    /// Return a signed density histogram.
536    ///
537    /// This allows negative weights and is useful for weighted MC, interference
538    /// terms, or background-subtracted histograms. It should not be sampled from.
539    ///
540    /// # Errors
541    ///
542    /// Returns [`LadduPhysicsError`] when the histogram is invalid or has zero
543    /// or non-finite in-range total weight.
544    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    /// Sample a value from the histogram, assuming counts define bin probabilities.
575    ///
576    /// Samples uniformly within the selected bin.
577    ///
578    /// # Errors
579    ///
580    /// Returns [`LadduPhysicsError`] when counts are negative or non-finite, or
581    /// their total is not positive and finite.
582    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        // Handles tiny floating-point roundoff.
599        let last = self.counts.len() - 1;
600        Ok(rng.f64_range(self.bin_edges[last]..self.bin_edges[last + 1]))
601    }
602
603    /// Return the center of a bin.
604    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}