1use std::{
4 collections::HashMap,
5 sync::{
6 Arc, Mutex,
7 atomic::{AtomicU64, Ordering},
8 },
9};
10
11use auto_ops::impl_op_ex;
12use laddu_data::data::Dataset;
13use laddu_expr::{Expr, ExprNodeStructuralKey};
14use laddu_runtime::{DatasetExprExt, Execution};
15use rayon::prelude::*;
16
17use crate::{CrossSectionIntegrals, Likelihood, LikelihoodError, LikelihoodResult};
18
19static NEXT_SOURCE_ID: AtomicU64 = AtomicU64::new(1);
20
21pub fn next_uncertainty_source_id() -> u64 {
23 NEXT_SOURCE_ID.fetch_add(1, Ordering::Relaxed)
24}
25
26fn invalid(message: impl Into<String>) -> LikelihoodError {
27 LikelihoodError::InvalidCrossSection(message.into())
28}
29
30#[cfg(test)]
31thread_local! {
32 static SELECTION_INTENSITY_EVALUATIONS: std::cell::Cell<usize> = const {
33 std::cell::Cell::new(0)
34 };
35 static PREPARED_INTENSITY_EVALUATIONS: std::cell::Cell<usize> = const {
36 std::cell::Cell::new(0)
37 };
38 static BIN_ASSIGNMENT_EVALUATIONS: std::cell::Cell<usize> = const {
39 std::cell::Cell::new(0)
40 };
41}
42
43fn record_selection_intensity_evaluation() {
44 #[cfg(test)]
45 SELECTION_INTENSITY_EVALUATIONS.with(|count| count.set(count.get() + 1));
46}
47
48fn record_prepared_intensity_evaluation() {
49 #[cfg(test)]
50 PREPARED_INTENSITY_EVALUATIONS.with(|count| count.set(count.get() + 1));
51}
52
53fn record_bin_assignment_evaluation() {
54 #[cfg(test)]
55 BIN_ASSIGNMENT_EVALUATIONS.with(|count| count.set(count.get() + 1));
56}
57
58#[cfg(test)]
59fn reset_selection_intensity_evaluation_count() {
60 SELECTION_INTENSITY_EVALUATIONS.with(|count| count.set(0));
61}
62
63#[cfg(test)]
64fn selection_intensity_evaluation_count() -> usize {
65 SELECTION_INTENSITY_EVALUATIONS.with(std::cell::Cell::get)
66}
67
68#[cfg(test)]
69fn reset_projection_evaluation_counts() {
70 PREPARED_INTENSITY_EVALUATIONS.with(|count| count.set(0));
71 BIN_ASSIGNMENT_EVALUATIONS.with(|count| count.set(0));
72}
73
74#[cfg(test)]
75fn projection_evaluation_counts() -> (usize, usize) {
76 let intensities = PREPARED_INTENSITY_EVALUATIONS.with(std::cell::Cell::get);
77 let assignments = BIN_ASSIGNMENT_EVALUATIONS.with(std::cell::Cell::get);
78 (intensities, assignments)
79}
80
81#[derive(Clone)]
83pub struct Ensemble {
84 parameter_names: Vec<String>,
85 draws: Vec<Vec<f64>>,
86 source_id: u64,
87 replicas: Vec<Arc<Likelihood>>,
88 replicas_share_event_rows: bool,
89}
90
91#[derive(Debug, thiserror::Error)]
93pub enum BootstrapFitError<E> {
94 #[error(transparent)]
96 Likelihood(#[from] LikelihoodError),
97 #[error("bootstrap fit {index} failed: {source}")]
99 Fit {
100 index: usize,
102 #[source]
104 source: E,
105 },
106}
107
108impl std::fmt::Debug for Ensemble {
109 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 formatter
111 .debug_struct("Ensemble")
112 .field("parameter_names", &self.parameter_names)
113 .field("draws", &self.draws)
114 .field("source_id", &self.source_id)
115 .field("replicas", &self.replicas.len())
116 .field("replicas_share_event_rows", &self.replicas_share_event_rows)
117 .finish()
118 }
119}
120
121impl Ensemble {
122 pub fn new(parameter_names: Vec<String>, draws: Vec<Vec<f64>>) -> LikelihoodResult<Self> {
127 Self::with_source_id(parameter_names, draws, next_uncertainty_source_id())
128 }
129
130 pub fn with_source_id(
135 parameter_names: Vec<String>,
136 draws: Vec<Vec<f64>>,
137 source_id: u64,
138 ) -> LikelihoodResult<Self> {
139 if draws.is_empty() {
140 return Err(invalid("an ensemble must contain at least one draw"));
141 }
142 if draws
143 .iter()
144 .any(|draw| draw.len() != parameter_names.len() || draw.iter().any(|v| !v.is_finite()))
145 {
146 return Err(invalid(
147 "every ensemble draw must be finite and match the parameter-name count",
148 ));
149 }
150 Ok(Self {
151 parameter_names,
152 draws,
153 source_id,
154 replicas: Vec::new(),
155 replicas_share_event_rows: false,
156 })
157 }
158
159 pub fn with_replicas(
164 parameter_names: Vec<String>,
165 draws: Vec<Vec<f64>>,
166 replicas: Vec<Arc<Likelihood>>,
167 ) -> LikelihoodResult<Self> {
168 let mut ensemble = Self::new(parameter_names, draws)?;
169 if replicas.len() != ensemble.draws.len() {
170 return Err(invalid(
171 "bootstrap replica count must match the parameter draw count",
172 ));
173 }
174 ensemble.replicas = replicas;
175 Ok(ensemble)
176 }
177
178 pub fn from_chain(
183 parameter_names: Vec<String>,
184 chain: &[Vec<Vec<f64>>],
185 discard: usize,
186 thin: usize,
187 ) -> LikelihoodResult<Self> {
188 if thin == 0 {
189 return Err(invalid("MCMC thinning must be positive"));
190 }
191 if chain.is_empty()
192 || chain
193 .iter()
194 .any(|walker| discard >= walker.len() || walker.is_empty())
195 {
196 return Err(invalid(
197 "MCMC discard must leave at least one step in every walker",
198 ));
199 }
200 let draws = chain
201 .iter()
202 .flat_map(|walker| {
203 (discard..walker.len())
204 .step_by(thin)
205 .map(|step| walker[step].clone())
206 })
207 .collect();
208 Self::new(parameter_names, draws)
209 }
210
211 pub fn bootstrap_fit<E>(
221 likelihood: &Arc<Likelihood>,
222 samples: usize,
223 seed: u64,
224 mut fit: impl FnMut(&Arc<Likelihood>, usize) -> Result<Vec<f64>, E>,
225 ) -> Result<Self, BootstrapFitError<E>> {
226 if samples == 0 {
227 return Err(BootstrapFitError::Likelihood(invalid(
228 "bootstrap sample count must be positive",
229 )));
230 }
231 let parameter_names = likelihood
232 .params()
233 .free_params()
234 .iter()
235 .map(|id| likelihood.params().name(*id).map(str::to_owned))
236 .collect::<Result<Vec<_>, _>>()
237 .map_err(LikelihoodError::from)?;
238 let mut draws = Vec::with_capacity(samples);
239 let mut replicas = Vec::with_capacity(samples);
240 for index in 0..samples {
241 let replica = Arc::new(likelihood.bootstrap(seed.wrapping_add(index as u64))?);
242 let draw =
243 fit(&replica, index).map_err(|source| BootstrapFitError::Fit { index, source })?;
244 draws.push(draw);
245 replicas.push(replica);
246 }
247 let mut ensemble = Self::with_replicas(parameter_names, draws, replicas)?;
248 ensemble.replicas_share_event_rows = true;
249 Ok(ensemble)
250 }
251
252 pub fn parameter_names(&self) -> &[String] {
254 &self.parameter_names
255 }
256
257 pub fn draws(&self) -> &[Vec<f64>] {
259 &self.draws
260 }
261
262 pub fn source_id(&self) -> u64 {
264 self.source_id
265 }
266
267 pub fn replicas(&self) -> &[Arc<Likelihood>] {
269 &self.replicas
270 }
271
272 fn replica_bin_assignments(
273 &self,
274 dataset: Option<&Dataset>,
275 axes: &[Axis],
276 execution: &Execution,
277 ) -> LikelihoodResult<Option<BinAssignments>> {
278 if self.replicas_share_event_rows {
279 Ok(None)
280 } else {
281 dataset
282 .map(|dataset| evaluate_bin_assignments(dataset, axes, execution))
283 .transpose()
284 }
285 }
286
287 pub fn len(&self) -> usize {
289 self.draws.len()
290 }
291
292 pub fn is_empty(&self) -> bool {
294 self.draws.is_empty()
295 }
296}
297
298#[derive(Clone, Debug, PartialEq)]
300pub struct Estimate {
301 central: f64,
302 draws: Vec<f64>,
303 source_id: Option<u64>,
304}
305
306impl Estimate {
307 pub fn central(central: f64) -> LikelihoodResult<Self> {
312 Self::with_source_id(central, Vec::new(), None)
313 }
314
315 pub fn new(central: f64, draws: Vec<f64>) -> LikelihoodResult<Self> {
320 let source_id = (!draws.is_empty()).then(next_uncertainty_source_id);
321 Self::with_source_id(central, draws, source_id)
322 }
323
324 pub fn with_source_id(
329 central: f64,
330 draws: Vec<f64>,
331 source_id: Option<u64>,
332 ) -> LikelihoodResult<Self> {
333 if !central.is_finite() || draws.iter().any(|value| !value.is_finite()) {
334 return Err(invalid("estimate central value and draws must be finite"));
335 }
336 Ok(Self {
337 central,
338 draws,
339 source_id,
340 })
341 }
342
343 fn from_evaluation(central: f64, draws: Vec<f64>, source_id: Option<u64>) -> Self {
344 Self {
345 central,
346 draws,
347 source_id,
348 }
349 }
350
351 pub fn value(&self) -> f64 {
353 self.central
354 }
355
356 pub fn draws(&self) -> &[f64] {
358 &self.draws
359 }
360
361 pub fn source_id(&self) -> Option<u64> {
363 self.source_id
364 }
365
366 pub fn mean(&self) -> LikelihoodResult<f64> {
371 if self.draws.is_empty() {
372 return Err(invalid("estimate has no uncertainty draws"));
373 }
374 Ok(self.draws.iter().sum::<f64>() / self.draws.len() as f64)
375 }
376
377 pub fn std(&self) -> LikelihoodResult<f64> {
382 if self.draws.len() < 2 {
383 return Err(invalid("estimate needs at least two uncertainty draws"));
384 }
385 let mean = self.mean()?;
386 Ok((self
387 .draws
388 .iter()
389 .map(|value| (value - mean).powi(2))
390 .sum::<f64>()
391 / (self.draws.len() - 1) as f64)
392 .sqrt())
393 }
394
395 pub fn quantile(&self, probability: f64) -> LikelihoodResult<f64> {
400 if !(0.0..=1.0).contains(&probability) {
401 return Err(invalid("quantile probability must lie in [0, 1]"));
402 }
403 if self.draws.is_empty() {
404 return Err(invalid("estimate has no uncertainty draws"));
405 }
406 let mut values = self.draws.clone();
407 values.sort_by(f64::total_cmp);
408 let position = probability * (values.len() - 1) as f64;
409 let lower = position.floor() as usize;
410 let upper = position.ceil() as usize;
411 let fraction = position - lower as f64;
412 Ok(values[lower] * (1.0 - fraction) + values[upper] * fraction)
413 }
414
415 pub fn median(&self) -> LikelihoodResult<f64> {
420 self.quantile(0.5)
421 }
422
423 pub fn interval(&self, level: f64) -> LikelihoodResult<(f64, f64)> {
428 if !(0.0 < level && level < 1.0) {
429 return Err(invalid("interval level must lie in (0, 1)"));
430 }
431 let tail = (1.0 - level) * 0.5;
432 Ok((self.quantile(tail)?, self.quantile(1.0 - tail)?))
433 }
434
435 fn binary(&self, other: &Self, op: impl Fn(f64, f64) -> f64) -> Self {
436 let count = match (self.draws.len(), other.draws.len()) {
437 (0, 0) => 0,
438 (0, right) => right,
439 (left, 0) => left,
440 (left, right) => left.min(right),
441 };
442 let draws = (0..count)
443 .map(|index| {
444 let left = self.draws.get(index).copied().unwrap_or(self.central);
445 let right_index = if self.source_id == other.source_id {
446 index
447 } else {
448 (index.wrapping_mul(6364136223846793005usize).wrapping_add(1))
449 % other.draws.len().max(1)
450 };
451 let right = other
452 .draws
453 .get(right_index)
454 .copied()
455 .unwrap_or(other.central);
456 op(left, right)
457 })
458 .collect();
459 let source_id = match (self.draws.is_empty(), other.draws.is_empty()) {
460 (false, true) => self.source_id,
461 (true, false) => other.source_id,
462 (false, false) if self.source_id == other.source_id => self.source_id,
463 (false, false) => Some(next_uncertainty_source_id()),
464 (true, true) => None,
465 };
466 Self::from_evaluation(op(self.central, other.central), draws, source_id)
467 }
468}
469
470impl_op_ex!(+ |left: &Estimate, right: &Estimate| -> Estimate {
471 left.binary(right, |a, b| a + b)
472});
473impl_op_ex!(-|left: &Estimate, right: &Estimate| -> Estimate { left.binary(right, |a, b| a - b) });
474impl_op_ex!(*|left: &Estimate, right: &Estimate| -> Estimate { left.binary(right, |a, b| a * b) });
475impl_op_ex!(/ |left: &Estimate, right: &Estimate| -> Estimate {
476 left.binary(right, |a, b| a / b)
477});
478
479impl_op_ex!(+ |left: &Estimate, right: &f64| -> Estimate {
480 left.binary(
481 &Estimate::from_evaluation(*right, Vec::new(), None),
482 |a, b| a + b,
483 )
484});
485impl_op_ex!(-|left: &Estimate, right: &f64| -> Estimate {
486 left.binary(
487 &Estimate::from_evaluation(*right, Vec::new(), None),
488 |a, b| a - b,
489 )
490});
491impl_op_ex!(*|left: &Estimate, right: &f64| -> Estimate {
492 left.binary(
493 &Estimate::from_evaluation(*right, Vec::new(), None),
494 |a, b| a * b,
495 )
496});
497impl_op_ex!(/ |left: &Estimate, right: &f64| -> Estimate {
498 left.binary(
499 &Estimate::from_evaluation(*right, Vec::new(), None),
500 |a, b| a / b,
501 )
502});
503
504#[derive(Clone, Debug)]
506pub struct Axis {
507 expression: Expr,
508 edges: Vec<f64>,
509}
510
511impl Axis {
512 pub fn new(expression: Expr, edges: Vec<f64>) -> LikelihoodResult<Self> {
517 if edges.len() < 2
518 || edges.iter().any(|value| !value.is_finite())
519 || edges.windows(2).any(|pair| pair[0] >= pair[1])
520 {
521 return Err(invalid(
522 "axis edges must contain at least two finite increasing values",
523 ));
524 }
525 Ok(Self { expression, edges })
526 }
527
528 pub fn expression(&self) -> &Expr {
530 &self.expression
531 }
532
533 pub fn edges(&self) -> &[f64] {
535 &self.edges
536 }
537
538 pub fn bins(&self) -> usize {
540 self.edges.len() - 1
541 }
542}
543
544#[derive(Clone, Debug, PartialEq)]
546pub struct BinnedEstimate {
547 central: Vec<f64>,
548 draws: Vec<Vec<f64>>,
549}
550
551impl BinnedEstimate {
552 fn new(central: Vec<f64>, draws: Vec<Vec<f64>>) -> Self {
553 Self { central, draws }
554 }
555
556 pub fn values(&self) -> &[f64] {
558 &self.central
559 }
560
561 pub fn draws(&self) -> &[Vec<f64>] {
563 &self.draws
564 }
565
566 pub fn interval(&self, level: f64) -> LikelihoodResult<(Vec<f64>, Vec<f64>)> {
571 if self.draws.is_empty() {
572 return Err(invalid("binned estimate has no uncertainty draws"));
573 }
574 let mut lower = Vec::with_capacity(self.central.len());
575 let mut upper = Vec::with_capacity(self.central.len());
576 for bin in 0..self.central.len() {
577 let estimate = Estimate::from_evaluation(
578 self.central[bin],
579 self.draws.iter().map(|draw| draw[bin]).collect(),
580 None,
581 );
582 let interval = estimate.interval(level)?;
583 lower.push(interval.0);
584 upper.push(interval.1);
585 }
586 Ok((lower, upper))
587 }
588
589 pub fn covariance(&self) -> LikelihoodResult<Vec<Vec<f64>>> {
594 if self.draws.len() < 2 {
595 return Err(invalid(
596 "binned estimate needs at least two uncertainty draws",
597 ));
598 }
599 let count = self.draws.len() as f64;
600 let means: Vec<_> = (0..self.central.len())
601 .map(|bin| self.draws.iter().map(|draw| draw[bin]).sum::<f64>() / count)
602 .collect();
603 Ok((0..self.central.len())
604 .map(|left| {
605 (0..self.central.len())
606 .map(|right| {
607 self.draws
608 .iter()
609 .map(|draw| (draw[left] - means[left]) * (draw[right] - means[right]))
610 .sum::<f64>()
611 / (count - 1.0)
612 })
613 .collect()
614 })
615 .collect())
616 }
617}
618
619#[derive(Clone, Debug)]
621pub struct DifferentialCrossSection {
622 axes: Vec<Vec<f64>>,
623 shape: Vec<usize>,
624 data: BinnedEstimate,
625 model: BinnedEstimate,
626 components: HashMap<String, BinnedEstimate>,
627}
628
629type DifferentialValues = (Vec<f64>, Vec<f64>, HashMap<String, Vec<f64>>);
630
631impl DifferentialCrossSection {
632 pub fn axes(&self) -> &[Vec<f64>] {
634 &self.axes
635 }
636
637 pub fn shape(&self) -> &[usize] {
639 &self.shape
640 }
641
642 pub fn data(&self) -> &BinnedEstimate {
644 &self.data
645 }
646
647 pub fn model(&self) -> &BinnedEstimate {
649 &self.model
650 }
651
652 pub fn components(&self) -> &HashMap<String, BinnedEstimate> {
654 &self.components
655 }
656}
657
658#[derive(Clone, Debug)]
660pub struct Projection {
661 name: String,
662 axes: Vec<Axis>,
663}
664
665impl Projection {
666 pub fn new(name: impl Into<String>, axes: Vec<Axis>) -> LikelihoodResult<Self> {
671 let name = name.into();
672 if name.is_empty() {
673 return Err(invalid("projection names must not be empty"));
674 }
675 if axes.is_empty() {
676 return Err(invalid("each projection must contain at least one axis"));
677 }
678 Ok(Self { name, axes })
679 }
680
681 pub fn name(&self) -> &str {
683 &self.name
684 }
685
686 pub fn axes(&self) -> &[Axis] {
688 &self.axes
689 }
690}
691
692#[derive(Clone, Debug)]
694pub struct ProjectionSet {
695 entries: Vec<(String, DifferentialCrossSection)>,
696}
697
698impl ProjectionSet {
699 pub fn len(&self) -> usize {
701 self.entries.len()
702 }
703
704 pub fn is_empty(&self) -> bool {
706 self.entries.is_empty()
707 }
708
709 pub fn get(&self, name: &str) -> Option<&DifferentialCrossSection> {
711 self.entries
712 .iter()
713 .find_map(|(candidate, result)| (candidate == name).then_some(result))
714 }
715
716 pub fn iter(&self) -> impl ExactSizeIterator<Item = (&str, &DifferentialCrossSection)> {
718 self.entries
719 .iter()
720 .map(|(name, result)| (name.as_str(), result))
721 }
722}
723
724#[derive(Clone, Debug, Eq, Hash, PartialEq)]
726struct CanonicalTags(Vec<String>);
727
728impl CanonicalTags {
729 fn new(tags: &[String]) -> Self {
730 let mut canonical = tags.to_vec();
731 canonical.sort();
732 canonical.dedup();
733 Self(canonical)
734 }
735
736 fn as_slice(&self) -> &[String] {
737 &self.0
738 }
739}
740
741type IntegralCacheKey = (usize, Option<CanonicalTags>);
742type IntegralCache = Arc<Mutex<HashMap<IntegralCacheKey, CrossSectionIntegrals>>>;
743
744#[derive(Clone)]
746pub struct CrossSection {
747 likelihood: Arc<Likelihood>,
748 term_name: String,
749 generated_mc: Dataset,
750 full_integrals: CrossSectionIntegrals,
751 luminosity: f64,
752 parameters: Vec<f64>,
753 ensemble: Option<Ensemble>,
754 members: Option<Arc<Vec<(CrossSection, Estimate)>>>,
755 integral_cache: IntegralCache,
756 cache_hits: Arc<AtomicU64>,
757 cache_misses: Arc<AtomicU64>,
758}
759
760#[derive(Copy, Clone, Debug, PartialEq, Eq)]
762pub struct CrossSectionDiagnostics {
763 cache_hits: u64,
764 cache_misses: u64,
765 cached_integrals: usize,
766 prepared_bytes: usize,
767}
768
769impl CrossSectionDiagnostics {
770 pub fn cache_hits(&self) -> u64 {
772 self.cache_hits
773 }
774 pub fn cache_misses(&self) -> u64 {
776 self.cache_misses
777 }
778 pub fn cached_integrals(&self) -> usize {
780 self.cached_integrals
781 }
782 pub fn prepared_bytes(&self) -> usize {
784 self.prepared_bytes
785 }
786}
787
788#[derive(Clone)]
789struct BinnedMeasurement {
790 yields: Vec<f64>,
791 exposures: Vec<f64>,
792}
793
794struct CombinedMemberValues {
795 data: BinnedMeasurement,
796 model: BinnedMeasurement,
797 components: HashMap<String, BinnedMeasurement>,
798}
799
800struct CanonicalComponents {
801 aliases: HashMap<String, CanonicalTags>,
802 integrals: HashMap<CanonicalTags, CrossSectionIntegrals>,
803}
804
805struct CombinedMemberWorkspace {
806 luminosity: f64,
807 full: CrossSectionIntegrals,
808 components: CanonicalComponents,
809 projections: Vec<CombinedPreparedProjection>,
810 data_weights: Vec<f64>,
811 accepted_weights: Vec<f64>,
812 generated_weights: Vec<f64>,
813}
814
815struct CombinedPreparedProjection {
816 name: String,
817 request_axes: Vec<Axis>,
818 data_bins: BinAssignments,
819 accepted_bins: BinAssignments,
820 generated_bins: BinAssignments,
821}
822
823impl CanonicalComponents {
824 fn prepare(
825 member: &CrossSection,
826 likelihood: &Likelihood,
827 components: &HashMap<String, Vec<String>>,
828 ) -> LikelihoodResult<Self> {
829 let aliases = components
830 .iter()
831 .map(|(name, tags)| (name.clone(), CanonicalTags::new(tags)))
832 .collect::<HashMap<_, _>>();
833 let mut integrals = HashMap::new();
834 for tags in aliases.values() {
835 if !integrals.contains_key(tags) {
836 integrals.insert(
837 tags.clone(),
838 member.integrals_for(likelihood, Some(tags.as_slice()))?,
839 );
840 }
841 }
842 Ok(Self { aliases, integrals })
843 }
844}
845
846struct BinAssignments {
847 indices: Vec<Option<usize>>,
848 count: usize,
849}
850
851#[derive(Clone, Debug, PartialEq, Eq, Hash)]
852struct ProjectionKey(Vec<AxisKey>);
853
854#[derive(Clone, Debug, PartialEq, Eq, Hash)]
855struct AxisKey {
856 root: usize,
857 nodes: Vec<ExprNodeStructuralKey>,
858 edges: Vec<u64>,
859}
860
861impl ProjectionKey {
862 fn new(axes: &[Axis]) -> Self {
863 Self(
864 axes.iter()
865 .map(|axis| {
866 let graph = axis.expression.to_graph();
867 AxisKey {
868 root: graph.root().index(),
869 nodes: graph
870 .nodes()
871 .iter()
872 .map(|node| node.structural_key())
873 .collect(),
874 edges: axis.edges.iter().map(|edge| edge.to_bits()).collect(),
875 }
876 })
877 .collect(),
878 )
879 }
880}
881
882fn deduplicate_projections(projections: &[Projection]) -> (Vec<&Projection>, Vec<usize>) {
883 let mut unique_indexes = HashMap::with_capacity(projections.len());
884 let mut unique = Vec::with_capacity(projections.len());
885 let indexes = projections
886 .iter()
887 .map(|projection| {
888 let key = ProjectionKey::new(projection.axes());
889 if let Some(index) = unique_indexes.get(&key) {
890 return *index;
891 }
892 let index = unique.len();
893 unique.push(projection);
894 unique_indexes.insert(key, index);
895 index
896 })
897 .collect();
898 (unique, indexes)
899}
900
901struct PreparedProjection {
902 name: String,
903 request_axes: Vec<Axis>,
904 axes: Vec<Vec<f64>>,
905 shape: Vec<usize>,
906 volumes: Vec<f64>,
907 data_bins: BinAssignments,
908 accepted_bins: BinAssignments,
909 generated_bins: BinAssignments,
910}
911
912struct ProjectionReplica {
913 bins: Vec<Option<BinAssignments>>,
914 weights: Option<Vec<f64>>,
915 total_data: f64,
916}
917
918impl BinAssignments {
919 fn new(values: &[Vec<f64>], axes: &[Axis]) -> Self {
920 let event_count = values.first().map_or(0, Vec::len);
921 debug_assert!(
922 values
923 .iter()
924 .all(|coordinates| coordinates.len() == event_count)
925 );
926 let indices = (0..event_count)
927 .map(|event| {
928 axes.iter()
929 .zip(values)
930 .try_fold(0, |flat, (axis, coordinates)| {
931 bin_index(coordinates[event], &axis.edges)
932 .map(|index| flat * axis.bins() + index)
933 })
934 })
935 .collect();
936 Self {
937 indices,
938 count: axes.iter().map(Axis::bins).product(),
939 }
940 }
941
942 fn accumulate_weighted_block(
943 &self,
944 offset: usize,
945 weights: &[f64],
946 intensities: &[f64],
947 bins: &mut [f64],
948 ) {
949 debug_assert!(offset + intensities.len() <= self.indices.len());
950 debug_assert_eq!(self.indices.len(), weights.len());
951 debug_assert_eq!(self.count, bins.len());
952 let worker_count = rayon::current_num_threads().min(intensities.len());
953 if rayon::current_thread_index().is_none() || worker_count < 2 {
954 for (row, &intensity) in intensities.iter().enumerate() {
955 let event = offset + row;
956 if let Some(index) = self.indices[event] {
957 bins[index] += weights[event] * intensity;
958 }
959 }
960 return;
961 }
962 let chunk_size = intensities.len().div_ceil(worker_count);
963 let chunk_count = intensities.len().div_ceil(chunk_size);
964 let partials = (0..chunk_count)
965 .into_par_iter()
966 .map(|chunk_index| {
967 let start = chunk_index * chunk_size;
968 let end = (start + chunk_size).min(intensities.len());
969 let mut partial = vec![0.0; self.count];
970 for (row, &intensity) in intensities[start..end].iter().enumerate() {
971 let event = offset + start + row;
972 if let Some(index) = self.indices[event] {
973 partial[index] += weights[event] * intensity;
974 }
975 }
976 partial
977 })
978 .collect::<Vec<_>>();
979 for partial in partials {
980 for (bin, value) in bins.iter_mut().zip(partial) {
981 *bin += value;
982 }
983 }
984 }
985
986 fn accumulate_products(&self, weights: &[f64], intensities: Option<&[f64]>) -> Vec<f64> {
987 debug_assert_eq!(self.indices.len(), weights.len());
988 debug_assert!(intensities.is_none_or(|values| values.len() == weights.len()));
989 let mut bins = vec![0.0; self.count];
990 for (event, (&index, &weight)) in self.indices.iter().zip(weights).enumerate() {
991 if let Some(index) = index {
992 let intensity = intensities.map_or(1.0, |values| values[event]);
993 bins[index] += weight * intensity;
994 }
995 }
996 bins
997 }
998}
999
1000impl CombinedMemberWorkspace {
1001 fn prepare(
1002 member: &CrossSection,
1003 projections: &[&Projection],
1004 components: &HashMap<String, Vec<String>>,
1005 ) -> LikelihoodResult<Self> {
1006 let execution = member.likelihood.execution();
1007 let (data, _) = member.likelihood.intensity_datasets(&member.term_name)?;
1008 let full = member.integrals_for(&member.likelihood, None)?;
1009 let component_integrals =
1010 CanonicalComponents::prepare(member, &member.likelihood, components)?;
1011 let prepared_projections = projections
1012 .iter()
1013 .map(|projection| {
1014 Ok(CombinedPreparedProjection {
1015 name: projection.name().to_owned(),
1016 request_axes: projection.axes().to_vec(),
1017 data_bins: evaluate_bin_assignments(data, projection.axes(), execution)
1018 .map_err(|error| {
1019 invalid(format!(
1020 "projection `{}` data bin preparation failed: {error}",
1021 projection.name()
1022 ))
1023 })?,
1024 accepted_bins: evaluate_bin_assignments(
1025 full.accepted_mc_source(),
1026 projection.axes(),
1027 execution,
1028 )
1029 .map_err(|error| {
1030 invalid(format!(
1031 "projection `{}` accepted MC bin preparation failed: {error}",
1032 projection.name()
1033 ))
1034 })?,
1035 generated_bins: evaluate_bin_assignments(
1036 full.generated_mc_source(),
1037 projection.axes(),
1038 execution,
1039 )
1040 .map_err(|error| {
1041 invalid(format!(
1042 "projection `{}` generated MC bin preparation failed: {error}",
1043 projection.name()
1044 ))
1045 })?,
1046 })
1047 })
1048 .collect::<LikelihoodResult<Vec<_>>>()?;
1049 Ok(Self {
1050 luminosity: member.luminosity,
1051 projections: prepared_projections,
1052 data_weights: dataset_weights(data)
1053 .map_err(|error| invalid(format!("data weights: {error}")))?,
1054 accepted_weights: dataset_weights(full.accepted_mc_source())
1055 .map_err(|error| invalid(format!("accepted MC weights: {error}")))?,
1056 generated_weights: dataset_weights(full.generated_mc_source())
1057 .map_err(|error| invalid(format!("generated MC weights: {error}")))?,
1058 full,
1059 components: component_integrals,
1060 })
1061 }
1062
1063 fn evaluate_projections_with_draws(
1064 &self,
1065 member: &CrossSection,
1066 factor: &Estimate,
1067 draw_count: usize,
1068 reference_source: Option<u64>,
1069 position: usize,
1070 ) -> LikelihoodResult<Vec<Vec<CombinedMemberValues>>> {
1071 let ensemble = member.ensemble.as_ref();
1072 let draw_indexes = (0..draw_count)
1073 .map(|index| {
1074 ensemble.map(|ensemble| {
1075 paired_draw_index(
1076 index,
1077 position,
1078 ensemble.len(),
1079 Some(ensemble.source_id),
1080 reference_source,
1081 )
1082 })
1083 })
1084 .collect::<Vec<_>>();
1085 let parameter_sets = std::iter::once(member.parameters.as_slice())
1086 .chain(draw_indexes.iter().map(|draw_index| {
1087 draw_index
1088 .and_then(|draw_index| ensemble.and_then(|value| value.draws.get(draw_index)))
1089 .map(Vec::as_slice)
1090 .unwrap_or(&member.parameters)
1091 }))
1092 .collect::<Vec<_>>();
1093 let parameter_contexts = std::iter::once("central value".to_owned())
1094 .chain((0..draw_count).map(|index| format!("ensemble draw {index}")))
1095 .collect::<Vec<_>>();
1096
1097 let mut accepted_histograms = self
1098 .projections
1099 .iter()
1100 .map(|projection| vec![vec![0.0; projection.accepted_bins.count]; parameter_sets.len()])
1101 .collect::<Vec<_>>();
1102 record_prepared_intensity_evaluation();
1103 let full_accepted = self
1104 .full
1105 .visit_accepted_prepared_intensities_many(
1106 ¶meter_sets,
1107 ¶meter_contexts,
1108 |offset, parameter_index, intensities| {
1109 for (projection, histograms) in
1110 self.projections.iter().zip(&mut accepted_histograms)
1111 {
1112 projection.accepted_bins.accumulate_weighted_block(
1113 offset,
1114 &self.accepted_weights,
1115 intensities,
1116 &mut histograms[parameter_index],
1117 );
1118 }
1119 },
1120 )
1121 .map_err(|error| invalid(format!("accepted MC intensity evaluation: {error}")))?;
1122 let mut generated_histograms = self
1123 .projections
1124 .iter()
1125 .map(|projection| {
1126 vec![vec![0.0; projection.generated_bins.count]; parameter_sets.len()]
1127 })
1128 .collect::<Vec<_>>();
1129 record_prepared_intensity_evaluation();
1130 self.full
1131 .visit_generated_prepared_intensities_many(
1132 ¶meter_sets,
1133 ¶meter_contexts,
1134 |offset, parameter_index, intensities| {
1135 for (projection, histograms) in
1136 self.projections.iter().zip(&mut generated_histograms)
1137 {
1138 projection.generated_bins.accumulate_weighted_block(
1139 offset,
1140 &self.generated_weights,
1141 intensities,
1142 &mut histograms[parameter_index],
1143 );
1144 }
1145 },
1146 )
1147 .map_err(|error| invalid(format!("generated MC intensity evaluation: {error}")))?;
1148
1149 let mut component_histograms = HashMap::new();
1150 for (canonical_tags, selected) in &self.components.integrals {
1151 let mut accepted = self
1152 .projections
1153 .iter()
1154 .map(|projection| {
1155 vec![vec![0.0; projection.accepted_bins.count]; parameter_sets.len()]
1156 })
1157 .collect::<Vec<_>>();
1158 record_selection_intensity_evaluation();
1159 record_prepared_intensity_evaluation();
1160 selected
1161 .visit_accepted_prepared_intensities_many(
1162 ¶meter_sets,
1163 ¶meter_contexts,
1164 |offset, parameter_index, intensities| {
1165 for (projection, histograms) in self.projections.iter().zip(&mut accepted) {
1166 projection.accepted_bins.accumulate_weighted_block(
1167 offset,
1168 &self.accepted_weights,
1169 intensities,
1170 &mut histograms[parameter_index],
1171 );
1172 }
1173 },
1174 )
1175 .map_err(|error| {
1176 invalid(format!(
1177 "accepted MC component {:?} intensity evaluation: {error}",
1178 canonical_tags.as_slice()
1179 ))
1180 })?;
1181 let mut generated = self
1182 .projections
1183 .iter()
1184 .map(|projection| {
1185 vec![vec![0.0; projection.generated_bins.count]; parameter_sets.len()]
1186 })
1187 .collect::<Vec<_>>();
1188 record_selection_intensity_evaluation();
1189 record_prepared_intensity_evaluation();
1190 selected
1191 .visit_generated_prepared_intensities_many(
1192 ¶meter_sets,
1193 ¶meter_contexts,
1194 |offset, parameter_index, intensities| {
1195 for (projection, histograms) in self.projections.iter().zip(&mut generated)
1196 {
1197 projection.generated_bins.accumulate_weighted_block(
1198 offset,
1199 &self.generated_weights,
1200 intensities,
1201 &mut histograms[parameter_index],
1202 );
1203 }
1204 },
1205 )
1206 .map_err(|error| {
1207 invalid(format!(
1208 "generated MC component {:?} intensity evaluation: {error}",
1209 canonical_tags.as_slice()
1210 ))
1211 })?;
1212 component_histograms.insert(canonical_tags.clone(), (accepted, generated));
1213 }
1214
1215 let draw_data = draw_indexes
1216 .iter()
1217 .enumerate()
1218 .map(|(index, draw_index)| {
1219 let prepare = || {
1220 let replica_data = draw_index
1221 .and_then(|draw_index| {
1222 ensemble.and_then(|value| value.replicas.get(draw_index))
1223 })
1224 .map(|likelihood| likelihood.intensity_datasets(&member.term_name))
1225 .transpose()
1226 .map_err(|error| invalid(format!("replica data lookup: {error}")))?
1227 .map(|(data, _)| data);
1228 let weights = replica_data
1229 .map(dataset_weights)
1230 .transpose()
1231 .map_err(|error| invalid(format!("replica data weights: {error}")))?;
1232 let histograms = self
1233 .projections
1234 .iter()
1235 .map(|projection| {
1236 let bins = match ensemble {
1237 Some(ensemble) => ensemble.replica_bin_assignments(
1238 replica_data,
1239 &projection.request_axes,
1240 member.likelihood.execution(),
1241 ),
1242 None => Ok(None),
1243 }
1244 .map_err(|error| {
1245 invalid(format!(
1246 "projection `{}` replica data bin preparation: {error}",
1247 projection.name
1248 ))
1249 })?;
1250 Ok(bins
1251 .as_ref()
1252 .unwrap_or(&projection.data_bins)
1253 .accumulate_products(
1254 weights.as_deref().unwrap_or(&self.data_weights),
1255 None,
1256 ))
1257 })
1258 .collect::<LikelihoodResult<Vec<_>>>()?;
1259 Ok((
1260 histograms,
1261 weights
1262 .as_ref()
1263 .map(|values| values.iter().sum())
1264 .unwrap_or_else(|| self.full.data_weight_sum()),
1265 ))
1266 };
1267 prepare().map_err(|error: LikelihoodError| {
1268 invalid(format!(
1269 "member `{}` draw {index}: {error}",
1270 member.term_name
1271 ))
1272 })
1273 })
1274 .collect::<LikelihoodResult<Vec<_>>>()?;
1275 let factors = std::iter::once(factor.central)
1276 .chain((0..draw_count).map(|index| {
1277 if factor.draws.is_empty() {
1278 factor.central
1279 } else {
1280 let factor_index = paired_draw_index(
1281 index,
1282 position,
1283 factor.draws.len(),
1284 factor.source_id,
1285 reference_source,
1286 );
1287 factor.draws[factor_index]
1288 }
1289 }))
1290 .collect::<Vec<_>>();
1291
1292 Ok((0..parameter_sets.len())
1293 .map(|parameter_index| {
1294 self.projections
1295 .iter()
1296 .enumerate()
1297 .map(|(projection_index, projection)| {
1298 let (data_histogram, total_data) = if parameter_index == 0 {
1299 (
1300 projection
1301 .data_bins
1302 .accumulate_products(&self.data_weights, None),
1303 self.full.data_weight_sum(),
1304 )
1305 } else {
1306 let (histograms, total_data) = &draw_data[parameter_index - 1];
1307 (histograms[projection_index].clone(), *total_data)
1308 };
1309 let accepted = &accepted_histograms[projection_index][parameter_index];
1310 let generated = &generated_histograms[projection_index][parameter_index];
1311 let exposures = binned_exposures(
1312 self.luminosity * factors[parameter_index],
1313 accepted,
1314 generated,
1315 );
1316 let canonical_values = self
1317 .components
1318 .integrals
1319 .keys()
1320 .map(|tags| {
1321 let (accepted_histograms, generated_histograms) =
1322 &component_histograms[tags];
1323 let selected_accepted =
1324 &accepted_histograms[projection_index][parameter_index];
1325 let selected_generated =
1326 &generated_histograms[projection_index][parameter_index];
1327 (
1328 tags.clone(),
1329 BinnedMeasurement {
1330 yields: selected_accepted
1331 .iter()
1332 .map(|value| {
1333 total_data * value / full_accepted[parameter_index]
1334 })
1335 .collect(),
1336 exposures: binned_exposures(
1337 self.luminosity * factors[parameter_index],
1338 selected_accepted,
1339 selected_generated,
1340 ),
1341 },
1342 )
1343 })
1344 .collect::<HashMap<_, _>>();
1345 CombinedMemberValues {
1346 data: BinnedMeasurement {
1347 yields: data_histogram,
1348 exposures: exposures.clone(),
1349 },
1350 model: BinnedMeasurement {
1351 yields: accepted
1352 .iter()
1353 .map(|value| {
1354 total_data * value / full_accepted[parameter_index]
1355 })
1356 .collect(),
1357 exposures,
1358 },
1359 components: self
1360 .components
1361 .aliases
1362 .iter()
1363 .map(|(name, tags)| (name.clone(), canonical_values[tags].clone()))
1364 .collect(),
1365 }
1366 })
1367 .collect()
1368 })
1369 .collect())
1370 }
1371}
1372
1373impl std::fmt::Debug for CrossSection {
1374 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1375 formatter
1376 .debug_struct("CrossSection")
1377 .field("term_name", &self.term_name)
1378 .field("luminosity", &self.luminosity)
1379 .field("parameters", &self.parameters)
1380 .field("ensemble", &self.ensemble)
1381 .field(
1382 "members",
1383 &self.members.as_ref().map(|members| members.len()),
1384 )
1385 .finish_non_exhaustive()
1386 }
1387}
1388
1389impl CrossSection {
1390 pub fn new(
1395 likelihood: Arc<Likelihood>,
1396 term_name: impl Into<String>,
1397 generated_mc: Dataset,
1398 luminosity: f64,
1399 parameters: Vec<f64>,
1400 ) -> LikelihoodResult<Self> {
1401 Self::with_ensemble(
1402 likelihood,
1403 term_name,
1404 generated_mc,
1405 luminosity,
1406 parameters,
1407 None,
1408 )
1409 }
1410
1411 pub fn with_ensemble(
1416 likelihood: Arc<Likelihood>,
1417 term_name: impl Into<String>,
1418 generated_mc: Dataset,
1419 luminosity: f64,
1420 parameters: Vec<f64>,
1421 ensemble: Option<Ensemble>,
1422 ) -> LikelihoodResult<Self> {
1423 if !luminosity.is_finite() || luminosity <= 0.0 {
1424 return Err(LikelihoodError::NonPositiveLuminosity(luminosity));
1425 }
1426 likelihood.params().validate_free_values(¶meters)?;
1427 let term_name = term_name.into();
1428 if let Some(ensemble) = &ensemble {
1429 let names = likelihood
1430 .params()
1431 .free_params()
1432 .iter()
1433 .map(|id| likelihood.params().name(*id).map(str::to_owned))
1434 .collect::<Result<Vec<_>, _>>()?;
1435 if names != ensemble.parameter_names {
1436 return Err(invalid(
1437 "ensemble parameter names do not match the likelihood",
1438 ));
1439 }
1440 }
1441 let full_integrals = likelihood.cross_section_integrals(&term_name, &generated_mc)?;
1442 let likelihood_key = Arc::as_ptr(&likelihood) as usize;
1443 let mut integral_cache = HashMap::new();
1444 integral_cache.insert((likelihood_key, None), full_integrals.clone());
1445 Ok(Self {
1446 likelihood,
1447 term_name,
1448 generated_mc,
1449 full_integrals,
1450 luminosity,
1451 parameters,
1452 ensemble,
1453 members: None,
1454 integral_cache: Arc::new(Mutex::new(integral_cache)),
1455 cache_hits: Default::default(),
1456 cache_misses: Arc::new(AtomicU64::new(1)),
1457 })
1458 }
1459
1460 pub fn combine(members: Vec<CrossSection>) -> LikelihoodResult<Self> {
1465 let factors = (0..members.len())
1466 .map(|_| Estimate::central(1.0))
1467 .collect::<LikelihoodResult<Vec<_>>>()?;
1468 Self::combine_with_factors(members, factors)
1469 }
1470
1471 pub fn combine_with_factors(
1476 members: Vec<CrossSection>,
1477 factors: Vec<Estimate>,
1478 ) -> LikelihoodResult<Self> {
1479 if members.is_empty() {
1480 return Err(invalid("at least one CrossSection is required"));
1481 }
1482 if factors.len() != members.len()
1483 || factors.iter().any(|factor| {
1484 factor.central <= 0.0 || factor.draws.iter().any(|value| *value <= 0.0)
1485 })
1486 {
1487 return Err(invalid(
1488 "factors must contain one positive estimate per member",
1489 ));
1490 }
1491 let template = members[0].clone();
1492 Ok(Self {
1493 likelihood: Arc::clone(&template.likelihood),
1494 term_name: template.term_name,
1495 generated_mc: template.generated_mc,
1496 full_integrals: template.full_integrals,
1497 luminosity: template.luminosity,
1498 parameters: template.parameters,
1499 ensemble: None,
1500 members: Some(Arc::new(members.into_iter().zip(factors).collect())),
1501 integral_cache: template.integral_cache,
1502 cache_hits: template.cache_hits,
1503 cache_misses: template.cache_misses,
1504 })
1505 }
1506
1507 pub fn observed_total(&self) -> LikelihoodResult<Estimate> {
1512 self.observed_total_selected(None)
1513 }
1514
1515 pub fn diagnostics(&self) -> CrossSectionDiagnostics {
1517 let cache = self
1518 .integral_cache
1519 .lock()
1520 .unwrap_or_else(|error| error.into_inner());
1521 CrossSectionDiagnostics {
1522 cache_hits: self.cache_hits.load(Ordering::Relaxed),
1523 cache_misses: self.cache_misses.load(Ordering::Relaxed),
1524 cached_integrals: cache.len(),
1525 prepared_bytes: cache
1526 .values()
1527 .map(CrossSectionIntegrals::resident_bytes)
1528 .sum(),
1529 }
1530 }
1531
1532 pub fn observed_total_with_tags(&self, tags: &[String]) -> LikelihoodResult<Estimate> {
1537 self.observed_total_selected(Some(tags))
1538 }
1539
1540 fn observed_total_selected(&self, tags: Option<&[String]>) -> LikelihoodResult<Estimate> {
1541 if self.members.is_some() {
1542 return self.combined_total(tags);
1543 }
1544 self.evaluate_estimate(tags, |integrals, parameters| {
1545 integrals.observed_cross_section(parameters, self.luminosity)
1546 })
1547 }
1548
1549 pub fn fitted_total(&self) -> LikelihoodResult<Estimate> {
1555 self.fitted_total_selected(None)
1556 }
1557
1558 pub fn fitted_total_with_tags(&self, tags: &[String]) -> LikelihoodResult<Estimate> {
1564 self.fitted_total_selected(Some(tags))
1565 }
1566
1567 fn fitted_total_selected(&self, tags: Option<&[String]>) -> LikelihoodResult<Estimate> {
1568 self.evaluate_estimate(tags, |integrals, parameters| {
1569 integrals.fitted_cross_section(parameters, self.luminosity)
1570 })
1571 }
1572
1573 pub fn total(&self) -> LikelihoodResult<Estimate> {
1578 self.observed_total()
1579 }
1580
1581 pub fn total_with_tags(&self, tags: &[String]) -> LikelihoodResult<Estimate> {
1586 self.observed_total_with_tags(tags)
1587 }
1588
1589 pub fn acceptance(&self) -> LikelihoodResult<Estimate> {
1594 self.acceptance_selected(None)
1595 }
1596
1597 pub fn acceptance_with_tags(&self, tags: &[String]) -> LikelihoodResult<Estimate> {
1602 self.acceptance_selected(Some(tags))
1603 }
1604
1605 fn acceptance_selected(&self, tags: Option<&[String]>) -> LikelihoodResult<Estimate> {
1606 self.evaluate_estimate(tags, CrossSectionIntegrals::acceptance)
1607 }
1608
1609 pub fn corrected_yield(&self) -> LikelihoodResult<Estimate> {
1614 self.corrected_yield_selected(None)
1615 }
1616
1617 pub fn corrected_yield_with_tags(&self, tags: &[String]) -> LikelihoodResult<Estimate> {
1622 self.corrected_yield_selected(Some(tags))
1623 }
1624
1625 fn corrected_yield_selected(&self, tags: Option<&[String]>) -> LikelihoodResult<Estimate> {
1626 self.evaluate_estimate(tags, |integrals, parameters| {
1627 let accepted_yield = if tags.is_some() {
1628 integrals.data_weight_sum() * integrals.accepted_integral(parameters)?
1629 / integrals.full_accepted_integral(parameters)?
1630 } else {
1631 integrals.data_weight_sum()
1632 };
1633 integrals.acceptance_corrected_yield(parameters, accepted_yield)
1634 })
1635 }
1636
1637 pub fn differential(
1642 &self,
1643 axes: &[Axis],
1644 components: &HashMap<String, Vec<String>>,
1645 ) -> LikelihoodResult<DifferentialCrossSection> {
1646 if axes.is_empty() {
1647 return Err(invalid("at least one differential axis is required"));
1648 }
1649 let projection = Projection {
1650 name: "differential".to_owned(),
1651 axes: axes.to_vec(),
1652 };
1653 let mut entries = self
1654 .projection_set(std::slice::from_ref(&projection), components)?
1655 .entries;
1656 Ok(entries.remove(0).1)
1657 }
1658
1659 pub fn projection_set(
1665 &self,
1666 projections: &[Projection],
1667 components: &HashMap<String, Vec<String>>,
1668 ) -> LikelihoodResult<ProjectionSet> {
1669 if projections.is_empty() {
1670 return Err(invalid("at least one projection is required"));
1671 }
1672 let mut names = std::collections::HashSet::with_capacity(projections.len());
1673 for projection in projections {
1674 if !names.insert(projection.name()) {
1675 return Err(invalid(format!(
1676 "duplicate projection name: {}",
1677 projection.name()
1678 )));
1679 }
1680 }
1681 if self.members.is_some() {
1682 return self.combined_projection_set(projections, components);
1683 }
1684 self.single_projection_set(projections, components)
1685 }
1686
1687 fn integrals_for(
1688 &self,
1689 likelihood: &Likelihood,
1690 tags: Option<&[String]>,
1691 ) -> LikelihoodResult<CrossSectionIntegrals> {
1692 let key_tags = tags.map(CanonicalTags::new);
1693 let key = (likelihood as *const Likelihood as usize, key_tags.clone());
1694 if let Some(integrals) = self
1695 .integral_cache
1696 .lock()
1697 .unwrap_or_else(|error| error.into_inner())
1698 .get(&key)
1699 .cloned()
1700 {
1701 self.cache_hits.fetch_add(1, Ordering::Relaxed);
1702 return Ok(integrals);
1703 }
1704 self.cache_misses.fetch_add(1, Ordering::Relaxed);
1705 let integrals = match key_tags.as_ref() {
1706 Some(tags) => likelihood.cross_section_integrals_with_tags(
1707 &self.term_name,
1708 &self.generated_mc,
1709 tags.as_slice().iter().map(String::as_str),
1710 ),
1711 None => likelihood.cross_section_integrals(&self.term_name, &self.generated_mc),
1712 }?;
1713 self.integral_cache
1714 .lock()
1715 .unwrap_or_else(|error| error.into_inner())
1716 .insert(key, integrals.clone());
1717 Ok(integrals)
1718 }
1719
1720 fn evaluate_estimate(
1721 &self,
1722 tags: Option<&[String]>,
1723 function: impl Fn(&CrossSectionIntegrals, &[f64]) -> LikelihoodResult<f64>,
1724 ) -> LikelihoodResult<Estimate> {
1725 if self.members.is_some() {
1726 return Err(invalid(
1727 "operation is not defined directly for a combined CrossSection",
1728 ));
1729 }
1730 let integrals = self.integrals_for(&self.likelihood, tags)?;
1731 let central = function(&integrals, &self.parameters)?;
1732 let draws = self
1733 .ensemble
1734 .as_ref()
1735 .map(|ensemble| {
1736 ensemble
1737 .draws
1738 .iter()
1739 .enumerate()
1740 .map(|(index, draw)| {
1741 let replica_integrals = ensemble
1742 .replicas
1743 .get(index)
1744 .map(|likelihood| self.integrals_for(likelihood, tags))
1745 .transpose()?;
1746 function(replica_integrals.as_ref().unwrap_or(&integrals), draw)
1747 })
1748 .collect::<LikelihoodResult<Vec<_>>>()
1749 })
1750 .transpose()?
1751 .unwrap_or_default();
1752 Ok(Estimate::from_evaluation(
1753 central,
1754 draws,
1755 self.ensemble.as_ref().map(Ensemble::source_id),
1756 ))
1757 }
1758
1759 fn selected_measurement_for(
1760 &self,
1761 likelihood: &Likelihood,
1762 parameters: &[f64],
1763 tags: Option<&[String]>,
1764 factor: f64,
1765 ) -> LikelihoodResult<(f64, f64)> {
1766 let full = self.integrals_for(likelihood, None)?;
1767 let selected = self.integrals_for(likelihood, tags)?;
1768 let full_accepted = full.full_accepted_integral(parameters)?;
1769 let accepted = selected.accepted_integral(parameters)?;
1770 let generated = selected.generated_integral(parameters)?;
1771 if full_accepted <= 0.0 || accepted <= 0.0 || generated <= 0.0 {
1772 return Err(invalid(
1773 "cross-section combination requires positive integrals",
1774 ));
1775 }
1776 Ok((
1777 selected.data_weight_sum() * accepted / full_accepted,
1778 self.luminosity * factor * accepted / generated,
1779 ))
1780 }
1781
1782 fn combined_total(&self, tags: Option<&[String]>) -> LikelihoodResult<Estimate> {
1783 let members = self
1784 .members
1785 .as_ref()
1786 .ok_or_else(|| invalid("CrossSection is not combined"))?;
1787 let central = members.iter().try_fold(
1788 (0.0, 0.0),
1789 |(yield_sum, exposure_sum), (member, factor)| {
1790 let (yield_value, exposure) = member.selected_measurement_for(
1791 &member.likelihood,
1792 &member.parameters,
1793 tags,
1794 factor.central,
1795 )?;
1796 Ok::<_, LikelihoodError>((yield_sum + yield_value, exposure_sum + exposure))
1797 },
1798 )?;
1799 let draw_count = member_draw_count(members);
1800 let reference_source = member_reference_source(members);
1801 let mut draws = Vec::with_capacity(draw_count);
1802 for index in 0..draw_count {
1803 let mut yield_sum = 0.0;
1804 let mut exposure_sum = 0.0;
1805 for (position, (member, factor)) in members.iter().enumerate() {
1806 let draw_index = member.ensemble.as_ref().map(|ensemble| {
1807 paired_draw_index(
1808 index,
1809 position,
1810 ensemble.len(),
1811 Some(ensemble.source_id),
1812 reference_source,
1813 )
1814 });
1815 let parameters = draw_index
1816 .and_then(|draw_index| {
1817 member
1818 .ensemble
1819 .as_ref()
1820 .and_then(|ensemble| ensemble.draws.get(draw_index))
1821 })
1822 .map(Vec::as_slice)
1823 .unwrap_or(&member.parameters);
1824 let likelihood = draw_index
1825 .and_then(|draw_index| {
1826 member
1827 .ensemble
1828 .as_ref()
1829 .and_then(|ensemble| ensemble.replicas.get(draw_index))
1830 })
1831 .map(Arc::as_ref)
1832 .unwrap_or(&member.likelihood);
1833 let factor_index = (!factor.draws.is_empty()).then(|| {
1834 paired_draw_index(
1835 index,
1836 position,
1837 factor.draws.len(),
1838 factor.source_id,
1839 reference_source,
1840 )
1841 });
1842 let factor = factor_index
1843 .and_then(|draw_index| factor.draws.get(draw_index))
1844 .copied()
1845 .unwrap_or(factor.central);
1846 let (yield_value, exposure) =
1847 member.selected_measurement_for(likelihood, parameters, tags, factor)?;
1848 yield_sum += yield_value;
1849 exposure_sum += exposure;
1850 }
1851 draws.push(yield_sum / exposure_sum);
1852 }
1853 Ok(Estimate::from_evaluation(
1854 central.0 / central.1,
1855 draws,
1856 Some(next_uncertainty_source_id()),
1857 ))
1858 }
1859
1860 fn single_projection_set(
1861 &self,
1862 projections: &[Projection],
1863 components: &HashMap<String, Vec<String>>,
1864 ) -> LikelihoodResult<ProjectionSet> {
1865 let request_context = format!(
1866 "member `{}` projections [{}]",
1867 self.term_name,
1868 projections
1869 .iter()
1870 .map(|projection| projection.name())
1871 .collect::<Vec<_>>()
1872 .join(", ")
1873 );
1874 let execution = self.likelihood.execution();
1875 let (data, _) = self.likelihood.intensity_datasets(&self.term_name)?;
1876 let full = self.integrals_for(&self.likelihood, None)?;
1877 let data_weights = dataset_weights(data)?;
1878 let accepted_weights = dataset_weights(full.accepted_mc_source())?;
1879 let generated_weights = dataset_weights(full.generated_mc_source())?;
1880 let component_integrals = CanonicalComponents::prepare(self, &self.likelihood, components)?;
1881 let (unique_projections, projection_plans) = deduplicate_projections(projections);
1882 let plans = unique_projections
1883 .iter()
1884 .map(|projection| {
1885 let prepare = || {
1886 Ok(PreparedProjection {
1887 name: projection.name().to_owned(),
1888 request_axes: projection.axes().to_vec(),
1889 axes: projection
1890 .axes()
1891 .iter()
1892 .map(|axis| axis.edges.clone())
1893 .collect(),
1894 shape: projection.axes().iter().map(Axis::bins).collect(),
1895 volumes: bin_volumes(projection.axes()),
1896 data_bins: evaluate_bin_assignments(data, projection.axes(), execution)?,
1897 accepted_bins: evaluate_bin_assignments(
1898 full.accepted_mc_source(),
1899 projection.axes(),
1900 execution,
1901 )?,
1902 generated_bins: evaluate_bin_assignments(
1903 full.generated_mc_source(),
1904 projection.axes(),
1905 execution,
1906 )?,
1907 })
1908 };
1909 prepare().map_err(|error: LikelihoodError| {
1910 invalid(format!(
1911 "projection `{}` preparation failed: {error}",
1912 projection.name()
1913 ))
1914 })
1915 })
1916 .collect::<LikelihoodResult<Vec<_>>>()?;
1917 let parameter_sets = std::iter::once(self.parameters.as_slice())
1918 .chain(
1919 self.ensemble
1920 .iter()
1921 .flat_map(|ensemble| ensemble.draws.iter().map(Vec::as_slice)),
1922 )
1923 .collect::<Vec<_>>();
1924 let parameter_contexts = std::iter::once("central value".to_owned())
1925 .chain(
1926 (0..parameter_sets.len().saturating_sub(1))
1927 .map(|index| format!("ensemble draw {index}")),
1928 )
1929 .collect::<Vec<_>>();
1930 let mut accepted_histograms = plans
1931 .iter()
1932 .map(|plan| vec![vec![0.0; plan.accepted_bins.count]; parameter_sets.len()])
1933 .collect::<Vec<_>>();
1934 record_prepared_intensity_evaluation();
1935 let full_accepted_integrals = full
1936 .visit_accepted_prepared_intensities_many(
1937 ¶meter_sets,
1938 ¶meter_contexts,
1939 |offset, parameter_index, intensities| {
1940 for (plan, histograms) in plans.iter().zip(&mut accepted_histograms) {
1941 plan.accepted_bins.accumulate_weighted_block(
1942 offset,
1943 &accepted_weights,
1944 intensities,
1945 &mut histograms[parameter_index],
1946 );
1947 }
1948 },
1949 )
1950 .map_err(|error| {
1951 invalid(format!(
1952 "projection set {request_context} accepted MC intensity evaluation failed: {error}"
1953 ))
1954 })?;
1955 let mut generated_histograms = plans
1956 .iter()
1957 .map(|plan| vec![vec![0.0; plan.generated_bins.count]; parameter_sets.len()])
1958 .collect::<Vec<_>>();
1959 record_prepared_intensity_evaluation();
1960 full.visit_generated_prepared_intensities_many(
1961 ¶meter_sets,
1962 ¶meter_contexts,
1963 |offset, parameter_index, intensities| {
1964 for (plan, histograms) in plans.iter().zip(&mut generated_histograms) {
1965 plan.generated_bins.accumulate_weighted_block(
1966 offset,
1967 &generated_weights,
1968 intensities,
1969 &mut histograms[parameter_index],
1970 );
1971 }
1972 },
1973 )
1974 .map_err(|error| {
1975 invalid(format!(
1976 "projection set {request_context} generated MC intensity evaluation failed: {error}"
1977 ))
1978 })?;
1979 let mut component_histograms = HashMap::new();
1980 for (canonical_tags, selected) in &component_integrals.integrals {
1981 let mut histograms = plans
1982 .iter()
1983 .map(|plan| vec![vec![0.0; plan.generated_bins.count]; parameter_sets.len()])
1984 .collect::<Vec<_>>();
1985 record_selection_intensity_evaluation();
1986 record_prepared_intensity_evaluation();
1987 selected
1988 .visit_generated_prepared_intensities_many(
1989 ¶meter_sets,
1990 ¶meter_contexts,
1991 |offset, parameter_index, intensities| {
1992 for (plan, histograms) in plans.iter().zip(&mut histograms) {
1993 plan.generated_bins.accumulate_weighted_block(
1994 offset,
1995 &generated_weights,
1996 intensities,
1997 &mut histograms[parameter_index],
1998 );
1999 }
2000 },
2001 )
2002 .map_err(|error| {
2003 invalid(format!(
2004 "projection set {request_context} generated MC component {:?} intensity evaluation failed: {error}",
2005 canonical_tags.as_slice()
2006 ))
2007 })?;
2008 component_histograms.insert(canonical_tags.clone(), histograms);
2009 }
2010 let replicas = self
2011 .ensemble
2012 .as_ref()
2013 .map(|ensemble| {
2014 ensemble
2015 .draws
2016 .iter()
2017 .enumerate()
2018 .map(|(index, _)| {
2019 let replica_data = ensemble
2020 .replicas
2021 .get(index)
2022 .map(|likelihood| likelihood.intensity_datasets(&self.term_name))
2023 .transpose()?
2024 .map(|(data, _)| data);
2025 let replica_weights = replica_data.map(dataset_weights).transpose()?;
2026 let total_data = replica_weights
2027 .as_ref()
2028 .map(|weights| weights.iter().sum())
2029 .unwrap_or_else(|| full.data_weight_sum());
2030 let bins = plans
2031 .iter()
2032 .map(|plan| {
2033 ensemble
2034 .replica_bin_assignments(
2035 replica_data,
2036 &plan.request_axes,
2037 execution,
2038 )
2039 .map_err(|error| {
2040 invalid(format!(
2041 "projection `{}` draw {index} bin preparation failed: {error}",
2042 plan.name
2043 ))
2044 })
2045 })
2046 .collect::<LikelihoodResult<Vec<_>>>()?;
2047 Ok(ProjectionReplica {
2048 bins,
2049 weights: replica_weights,
2050 total_data,
2051 })
2052 })
2053 .collect::<LikelihoodResult<Vec<_>>>()
2054 })
2055 .transpose()?
2056 .unwrap_or_default();
2057 let unique_results = plans
2058 .iter()
2059 .enumerate()
2060 .map(|(plan_index, plan)| {
2061 let evaluate = |draw_index: usize,
2062 draw_data_bins: &BinAssignments,
2063 draw_data_weights: &[f64],
2064 total_data: f64|
2065 -> DifferentialValues {
2066 let data_histogram =
2067 draw_data_bins.accumulate_products(draw_data_weights, None);
2068 let accepted_histogram = &accepted_histograms[plan_index][draw_index];
2069 let generated_histogram = &generated_histograms[plan_index][draw_index];
2070 let full_accepted = full_accepted_integrals[draw_index];
2071 let data_cross_section = data_histogram
2072 .iter()
2073 .zip(accepted_histogram)
2074 .zip(generated_histogram)
2075 .zip(&plan.volumes)
2076 .map(|(((data, accepted), generated), volume)| {
2077 if *accepted > 0.0 {
2078 data * generated / accepted / self.luminosity / volume
2079 } else {
2080 f64::NAN
2081 }
2082 })
2083 .collect();
2084 let model = generated_histogram
2085 .iter()
2086 .zip(&plan.volumes)
2087 .map(|(generated, volume)| {
2088 total_data * generated / full_accepted / self.luminosity / volume
2089 })
2090 .collect();
2091 let component_values = component_integrals
2092 .aliases
2093 .iter()
2094 .map(|(name, canonical_tags)| {
2095 let bins =
2096 &component_histograms[canonical_tags][plan_index][draw_index];
2097 (
2098 name.clone(),
2099 bins.iter()
2100 .zip(&plan.volumes)
2101 .map(|(generated, volume)| {
2102 total_data * generated
2103 / full_accepted
2104 / self.luminosity
2105 / volume
2106 })
2107 .collect(),
2108 )
2109 })
2110 .collect();
2111 (data_cross_section, model, component_values)
2112 };
2113 let (data_cross_section, model, component_values) =
2114 evaluate(0, &plan.data_bins, &data_weights, full.data_weight_sum());
2115 let mut data_draws = Vec::with_capacity(replicas.len());
2116 let mut model_draws = Vec::with_capacity(replicas.len());
2117 let mut component_draws: HashMap<String, Vec<Vec<f64>>> = components
2118 .keys()
2119 .map(|name| (name.clone(), Vec::with_capacity(replicas.len())))
2120 .collect();
2121 for (index, replica) in replicas.iter().enumerate() {
2122 let draw_data_bins =
2123 replica.bins[plan_index].as_ref().unwrap_or(&plan.data_bins);
2124 let draw_data_weights = replica.weights.as_deref().unwrap_or(&data_weights);
2125 let (data, model, values) = evaluate(
2126 index + 1,
2127 draw_data_bins,
2128 draw_data_weights,
2129 replica.total_data,
2130 );
2131 data_draws.push(data);
2132 model_draws.push(model);
2133 for (name, values) in values {
2134 component_draws.entry(name).or_default().push(values);
2135 }
2136 }
2137 Ok(DifferentialCrossSection {
2138 axes: plan.axes.clone(),
2139 shape: plan.shape.clone(),
2140 data: BinnedEstimate::new(data_cross_section, data_draws),
2141 model: BinnedEstimate::new(model, model_draws),
2142 components: component_values
2143 .into_iter()
2144 .map(|(name, central)| {
2145 let draws = component_draws.remove(&name).unwrap_or_default();
2146 (name, BinnedEstimate::new(central, draws))
2147 })
2148 .collect(),
2149 })
2150 })
2151 .collect::<LikelihoodResult<Vec<_>>>()?;
2152 Ok(ProjectionSet {
2153 entries: projections
2154 .iter()
2155 .zip(projection_plans)
2156 .map(|(projection, plan)| (projection.name.clone(), unique_results[plan].clone()))
2157 .collect(),
2158 })
2159 }
2160
2161 fn combined_projection_set(
2162 &self,
2163 projections: &[Projection],
2164 components: &HashMap<String, Vec<String>>,
2165 ) -> LikelihoodResult<ProjectionSet> {
2166 let members = self
2167 .members
2168 .as_ref()
2169 .ok_or_else(|| invalid("CrossSection is not combined"))?;
2170 let (unique_projections, projection_indexes) = deduplicate_projections(projections);
2171 let projection_names = projections
2172 .iter()
2173 .map(Projection::name)
2174 .collect::<Vec<_>>()
2175 .join(", ");
2176 let workspaces = members
2177 .iter()
2178 .map(|(member, _)| {
2179 CombinedMemberWorkspace::prepare(member, &unique_projections, components)
2180 .map_err(|error| {
2181 invalid(format!(
2182 "projection set member `{}` projections [{projection_names}] preparation failed: {error}",
2183 member.term_name
2184 ))
2185 })
2186 })
2187 .collect::<LikelihoodResult<Vec<_>>>()?;
2188 let draw_count = member_draw_count(members);
2189 let reference_source = member_reference_source(members);
2190 let member_values = members
2191 .iter()
2192 .zip(&workspaces)
2193 .enumerate()
2194 .map(|(position, ((member, factor), workspace))| {
2195 workspace
2196 .evaluate_projections_with_draws(
2197 member,
2198 factor,
2199 draw_count,
2200 reference_source,
2201 position,
2202 )
2203 .map_err(|error| {
2204 invalid(format!(
2205 "projection set member `{}` projections [{projection_names}] evaluation failed: {error}",
2206 member.term_name
2207 ))
2208 })
2209 })
2210 .collect::<LikelihoodResult<Vec<_>>>()?;
2211 let unique_results = unique_projections
2212 .iter()
2213 .enumerate()
2214 .map(|(projection_index, projection)| {
2215 let volumes = bin_volumes(projection.axes());
2216 let central = member_values
2217 .iter()
2218 .map(|values| &values[0][projection_index])
2219 .collect::<Vec<_>>();
2220 let data = pool_binned(central.iter().map(|values| &values.data), &volumes);
2221 let model = pool_binned(central.iter().map(|values| &values.model), &volumes);
2222 let component_central = components
2223 .keys()
2224 .map(|name| {
2225 (
2226 name.clone(),
2227 pool_binned(
2228 central.iter().map(|values| &values.components[name]),
2229 &volumes,
2230 ),
2231 )
2232 })
2233 .collect::<HashMap<_, _>>();
2234 let mut data_draws = Vec::with_capacity(draw_count);
2235 let mut model_draws = Vec::with_capacity(draw_count);
2236 let mut component_draws = components
2237 .keys()
2238 .map(|name| (name.clone(), Vec::with_capacity(draw_count)))
2239 .collect::<HashMap<_, _>>();
2240 for draw_index in 0..draw_count {
2241 let draw = member_values
2242 .iter()
2243 .map(|values| &values[draw_index + 1][projection_index])
2244 .collect::<Vec<_>>();
2245 data_draws.push(pool_binned(
2246 draw.iter().map(|values| &values.data),
2247 &volumes,
2248 ));
2249 model_draws.push(pool_binned(
2250 draw.iter().map(|values| &values.model),
2251 &volumes,
2252 ));
2253 for name in components.keys() {
2254 component_draws.get_mut(name).unwrap().push(pool_binned(
2255 draw.iter().map(|values| &values.components[name]),
2256 &volumes,
2257 ));
2258 }
2259 }
2260 DifferentialCrossSection {
2261 axes: projection
2262 .axes()
2263 .iter()
2264 .map(|axis| axis.edges.clone())
2265 .collect(),
2266 shape: projection.axes().iter().map(Axis::bins).collect(),
2267 data: BinnedEstimate::new(data, data_draws),
2268 model: BinnedEstimate::new(model, model_draws),
2269 components: component_central
2270 .into_iter()
2271 .map(|(name, central)| {
2272 let draws = component_draws.remove(&name).unwrap_or_default();
2273 (name, BinnedEstimate::new(central, draws))
2274 })
2275 .collect(),
2276 }
2277 })
2278 .collect::<Vec<_>>();
2279 Ok(ProjectionSet {
2280 entries: projections
2281 .iter()
2282 .zip(projection_indexes)
2283 .map(|(projection, index)| (projection.name.clone(), unique_results[index].clone()))
2284 .collect(),
2285 })
2286 }
2287}
2288
2289impl Likelihood {
2290 pub fn cross_section(
2295 self: &Arc<Self>,
2296 term_name: impl Into<String>,
2297 generated_mc: Dataset,
2298 luminosity: f64,
2299 parameters: Vec<f64>,
2300 ) -> LikelihoodResult<CrossSection> {
2301 CrossSection::new(
2302 Arc::clone(self),
2303 term_name,
2304 generated_mc,
2305 luminosity,
2306 parameters,
2307 )
2308 }
2309
2310 pub fn cross_section_with_ensemble(
2315 self: &Arc<Self>,
2316 term_name: impl Into<String>,
2317 generated_mc: Dataset,
2318 luminosity: f64,
2319 parameters: Vec<f64>,
2320 ensemble: Ensemble,
2321 ) -> LikelihoodResult<CrossSection> {
2322 CrossSection::with_ensemble(
2323 Arc::clone(self),
2324 term_name,
2325 generated_mc,
2326 luminosity,
2327 parameters,
2328 Some(ensemble),
2329 )
2330 }
2331}
2332
2333fn member_draw_count(members: &[(CrossSection, Estimate)]) -> usize {
2334 members
2335 .iter()
2336 .flat_map(|(member, factor)| {
2337 [
2338 member.ensemble.as_ref().map(Ensemble::len),
2339 (!factor.draws.is_empty()).then_some(factor.draws.len()),
2340 ]
2341 })
2342 .flatten()
2343 .min()
2344 .unwrap_or(0)
2345}
2346
2347fn member_reference_source(members: &[(CrossSection, Estimate)]) -> Option<u64> {
2348 members.iter().find_map(|(member, factor)| {
2349 member
2350 .ensemble
2351 .as_ref()
2352 .map(Ensemble::source_id)
2353 .or(factor.source_id)
2354 })
2355}
2356
2357fn paired_draw_index(
2358 index: usize,
2359 position: usize,
2360 draw_count: usize,
2361 source_id: Option<u64>,
2362 reference_source: Option<u64>,
2363) -> usize {
2364 if source_id == reference_source {
2365 index % draw_count
2366 } else {
2367 (index.wrapping_mul(2 * position + 1) + position) % draw_count
2368 }
2369}
2370
2371fn binned_exposures(luminosity: f64, accepted: &[f64], generated: &[f64]) -> Vec<f64> {
2372 accepted
2373 .iter()
2374 .zip(generated)
2375 .map(|(accepted, generated)| {
2376 if *generated > 0.0 {
2377 luminosity * accepted / generated
2378 } else {
2379 0.0
2380 }
2381 })
2382 .collect()
2383}
2384
2385fn pool_binned<'a>(
2386 measurements: impl IntoIterator<Item = &'a BinnedMeasurement>,
2387 volumes: &[f64],
2388) -> Vec<f64> {
2389 let mut yields = vec![0.0; volumes.len()];
2390 let mut exposures = vec![0.0; volumes.len()];
2391 for measurement in measurements {
2392 for index in 0..volumes.len() {
2393 yields[index] += measurement.yields[index];
2394 exposures[index] += measurement.exposures[index];
2395 }
2396 }
2397 (0..volumes.len())
2398 .map(|index| {
2399 if exposures[index] > 0.0 {
2400 yields[index] / exposures[index] / volumes[index]
2401 } else {
2402 f64::NAN
2403 }
2404 })
2405 .collect()
2406}
2407
2408fn evaluate_coordinates(
2409 dataset: &Dataset,
2410 axes: &[Axis],
2411 execution: &Execution,
2412) -> LikelihoodResult<Vec<Vec<f64>>> {
2413 axes.iter()
2414 .map(|axis| {
2415 dataset
2416 .evaluate_real(&axis.expression, execution)
2417 .map_err(Into::into)
2418 })
2419 .collect()
2420}
2421
2422fn evaluate_bin_assignments(
2423 dataset: &Dataset,
2424 axes: &[Axis],
2425 execution: &Execution,
2426) -> LikelihoodResult<BinAssignments> {
2427 record_bin_assignment_evaluation();
2428 Ok(BinAssignments::new(
2429 &evaluate_coordinates(dataset, axes, execution)?,
2430 axes,
2431 ))
2432}
2433
2434fn dataset_weights(dataset: &Dataset) -> LikelihoodResult<Vec<f64>> {
2435 dataset
2436 .try_fold_events(Vec::new(), |mut weights, event| {
2437 weights.push(event.weight());
2438 Ok(weights)
2439 })
2440 .map_err(Into::into)
2441}
2442
2443fn bin_volumes(axes: &[Axis]) -> Vec<f64> {
2444 axes.iter().fold(vec![1.0], |volumes, axis| {
2445 volumes
2446 .into_iter()
2447 .flat_map(|volume| {
2448 axis.edges
2449 .windows(2)
2450 .map(move |pair| volume * (pair[1] - pair[0]))
2451 })
2452 .collect()
2453 })
2454}
2455
2456fn bin_index(value: f64, edges: &[f64]) -> Option<usize> {
2457 if !value.is_finite() || value < edges[0] || value >= *edges.last()? {
2458 return None;
2459 }
2460 edges.windows(2).position(|pair| value < pair[1])
2461}
2462
2463#[cfg(test)]
2464mod tests {
2465 use std::collections::HashSet;
2466
2467 use approx::assert_relative_eq;
2468 use laddu_compile::CompiledModel;
2469 use laddu_data::{
2470 data::{EventBatch, OwnedEvent},
2471 schema::Schema,
2472 };
2473 use laddu_expr::{Expr, event_scalar, parameter};
2474
2475 use super::*;
2476
2477 fn weighted_dataset(values: &[(f64, f64)]) -> Dataset {
2478 let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
2479 let batch = EventBatch::from_events(
2480 schema,
2481 values
2482 .iter()
2483 .map(|(x, weight)| OwnedEvent::weighted(vec![], vec![*x], *weight)),
2484 )
2485 .unwrap();
2486 Dataset::from_batches(vec![batch]).unwrap()
2487 }
2488
2489 fn weighted_dataset_2d(values: &[(f64, f64, f64)]) -> Dataset {
2490 let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x", "y"], true).unwrap());
2491 let batch = EventBatch::from_events(
2492 schema,
2493 values
2494 .iter()
2495 .map(|(x, y, weight)| OwnedEvent::weighted(vec![], vec![*x, *y], *weight)),
2496 )
2497 .unwrap();
2498 Dataset::from_batches(vec![batch]).unwrap()
2499 }
2500
2501 struct CanonicalSelectionFixture {
2502 likelihood: Arc<Likelihood>,
2503 generated: Dataset,
2504 axis: Axis,
2505 components: HashMap<String, Vec<String>>,
2506 }
2507
2508 fn canonical_selection_fixture() -> CanonicalSelectionFixture {
2509 let x = event_scalar("x");
2510 let signal = (Expr::from(parameter!("a", initial: 1.5)) * x.clone()).tagged("signal");
2511 let background = Expr::from(parameter!("b", initial: 0.75)).tagged("background");
2512 let model = CompiledModel::from_expr(&(signal + background).norm_sqr()).unwrap();
2513 let data = weighted_dataset(&[(0.25, 1.0), (0.75, 2.0), (1.25, 1.0)]);
2514 let accepted = weighted_dataset(&[(0.25, 1.0), (0.75, 1.0), (1.25, 1.0)]);
2515 let generated = weighted_dataset(&[(0.25, 1.0), (0.75, 1.0), (1.25, 1.0), (1.75, 1.0)]);
2516 let likelihood = Arc::new(
2517 Likelihood::new([crate::NllTerm::new("signal", &model, &data, &accepted).unwrap()])
2518 .unwrap(),
2519 );
2520 CanonicalSelectionFixture {
2521 likelihood,
2522 generated,
2523 axis: Axis::new(x, vec![0.0, 1.0, 2.0]).unwrap(),
2524 components: HashMap::from([
2525 ("ordered".into(), vec!["background".into(), "signal".into()]),
2526 (
2527 "reordered".into(),
2528 vec!["signal".into(), "background".into()],
2529 ),
2530 (
2531 "repeated".into(),
2532 vec!["signal".into(), "background".into(), "signal".into()],
2533 ),
2534 ]),
2535 }
2536 }
2537
2538 fn assert_projection_close(
2539 actual: &DifferentialCrossSection,
2540 expected: &DifferentialCrossSection,
2541 ) {
2542 fn assert_estimate_close(actual: &BinnedEstimate, expected: &BinnedEstimate) {
2543 let rows = std::iter::once((actual.values(), expected.values())).chain(
2544 actual
2545 .draws()
2546 .iter()
2547 .zip(expected.draws())
2548 .map(|(actual, expected)| (actual.as_slice(), expected.as_slice())),
2549 );
2550 assert_eq!(actual.draws().len(), expected.draws().len());
2551 for (actual, expected) in rows {
2552 assert_eq!(actual.len(), expected.len());
2553 for (actual, expected) in actual.iter().zip(expected) {
2554 if expected.is_nan() {
2555 assert!(actual.is_nan());
2556 } else {
2557 assert_relative_eq!(
2558 actual,
2559 expected,
2560 epsilon = 1e-10,
2561 max_relative = 1e-10
2562 );
2563 }
2564 }
2565 }
2566 }
2567
2568 assert_eq!(actual.axes(), expected.axes());
2569 assert_eq!(actual.shape(), expected.shape());
2570 assert_estimate_close(actual.data(), expected.data());
2571 assert_estimate_close(actual.model(), expected.model());
2572 assert_eq!(
2573 actual.components().keys().collect::<HashSet<_>>(),
2574 expected.components().keys().collect::<HashSet<_>>()
2575 );
2576 for (name, expected) in expected.components() {
2577 assert_estimate_close(&actual.components()[name], expected);
2578 }
2579 }
2580
2581 #[test]
2582 fn estimate_arithmetic_preserves_scalar_provenance() {
2583 let estimate = Estimate::with_source_id(2.0, vec![1.0, 3.0], Some(17)).unwrap();
2584 let scaled = &estimate * 4.0;
2585 assert_eq!(scaled.value(), 8.0);
2586 assert_eq!(scaled.draws(), &[4.0, 12.0]);
2587 assert_eq!(scaled.source_id(), Some(17));
2588 }
2589
2590 #[test]
2591 fn chain_adapter_discards_and_thins_each_walker() {
2592 let chain = vec![
2593 vec![vec![0.0], vec![1.0], vec![2.0], vec![3.0]],
2594 vec![vec![4.0], vec![5.0], vec![6.0], vec![7.0]],
2595 ];
2596 let ensemble = Ensemble::from_chain(vec!["x".into()], &chain, 1, 2).unwrap();
2597 assert_eq!(
2598 ensemble.draws(),
2599 &[vec![1.0], vec![3.0], vec![5.0], vec![7.0]]
2600 );
2601 }
2602
2603 #[test]
2604 fn bin_lookup_uses_half_open_intervals() {
2605 assert_eq!(bin_index(0.0, &[0.0, 1.0, 2.0]), Some(0));
2606 assert_eq!(bin_index(1.0, &[0.0, 1.0, 2.0]), Some(1));
2607 assert_eq!(bin_index(2.0, &[0.0, 1.0, 2.0]), None);
2608 }
2609
2610 #[test]
2611 fn joint_differential_preserves_bin_and_weight_semantics() {
2612 let model = CompiledModel::from_expr(&(event_scalar("x") + 1.0)).unwrap();
2613 let data = weighted_dataset_2d(&[
2614 (0.0, 0.0, 1.0),
2615 (0.0, 1.0, 2.0),
2616 (1.0, 0.0, -3.0),
2617 (1.0, 1.0, 4.0),
2618 (2.0, 0.5, 8.0),
2619 (f64::NAN, 0.5, 16.0),
2620 (0.5, f64::INFINITY, 32.0),
2621 (0.5, f64::NEG_INFINITY, 64.0),
2622 (-0.5, 0.5, 128.0),
2623 ]);
2624 let accepted = weighted_dataset_2d(&[
2625 (0.25, 0.25, 1.0),
2626 (0.25, 1.25, 1.0),
2627 (1.25, 0.25, 1.0),
2628 (1.25, 1.25, -1.0),
2629 ]);
2630 let generated = weighted_dataset_2d(&[
2631 (0.25, 0.25, 1.0),
2632 (0.25, 1.25, 1.0),
2633 (1.25, 0.25, 1.0),
2634 (1.25, 1.25, 1.0),
2635 ]);
2636 let likelihood = Arc::new(
2637 Likelihood::new([crate::NllTerm::new("signal", &model, &data, &accepted).unwrap()])
2638 .unwrap(),
2639 );
2640 let axes = [
2641 Axis::new(event_scalar("x"), vec![0.0, 1.0, 2.0]).unwrap(),
2642 Axis::new(event_scalar("y"), vec![0.0, 1.0, 2.0]).unwrap(),
2643 ];
2644 let differential = likelihood
2645 .cross_section("signal", generated, 1.0, Vec::new())
2646 .unwrap()
2647 .differential(&axes, &HashMap::new())
2648 .unwrap();
2649
2650 assert_eq!(differential.shape(), &[2, 2]);
2651 assert_eq!(&differential.data().values()[..3], &[1.0, 2.0, -3.0]);
2652 assert!(differential.data().values()[3].is_nan());
2653 }
2654
2655 #[test]
2656 fn rust_cross_section_api_covers_totals_differentials_and_bootstrap_pairing() {
2657 let model = CompiledModel::from_expr(&(event_scalar("x") + 1.0)).unwrap();
2658 let data = weighted_dataset(&[(0.25, 1.0), (1.25, 2.0)]);
2659 let accepted = weighted_dataset(&[(0.25, 1.0), (1.25, 1.0)]);
2660 let generated = weighted_dataset(&[(0.25, 1.0), (1.25, 1.0), (1.75, 1.0)]);
2661 let likelihood = Arc::new(
2662 Likelihood::new([crate::NllTerm::new("signal", &model, &data, &accepted).unwrap()])
2663 .unwrap(),
2664 );
2665 let cross_section = likelihood
2666 .cross_section("signal", generated, 10.0, Vec::new())
2667 .unwrap();
2668 assert!(cross_section.total().unwrap().value().is_finite());
2669 let before = cross_section.diagnostics();
2670 assert!(cross_section.total().unwrap().value().is_finite());
2671 let after = cross_section.diagnostics();
2672 assert_eq!(after.cache_hits(), before.cache_hits() + 1);
2673 assert_eq!(after.cache_misses(), 1);
2674 assert_eq!(after.cached_integrals(), 1);
2675 assert!(after.prepared_bytes() > 0);
2676
2677 let axis = Axis::new(event_scalar("x"), vec![0.0, 1.0, 2.0]).unwrap();
2678 let differential = cross_section
2679 .differential(&[axis], &HashMap::new())
2680 .unwrap();
2681 assert_eq!(differential.shape(), &[2]);
2682 assert_eq!(differential.data().values().len(), 2);
2683 assert_eq!(differential.model().values().len(), 2);
2684
2685 let ensemble = Ensemble::bootstrap_fit(&likelihood, 3, 42, |replica, _| {
2686 Ok::<_, std::convert::Infallible>(replica.default_params())
2687 })
2688 .unwrap();
2689 assert_eq!(ensemble.len(), 3);
2690 assert_eq!(ensemble.replicas().len(), 3);
2691 }
2692
2693 #[test]
2694 fn rust_projection_set_preserves_order_lookup_and_differential_results() {
2695 let fixture = canonical_selection_fixture();
2696 let cross_section = fixture
2697 .likelihood
2698 .cross_section("signal", fixture.generated, 2.0, vec![1.5, 0.75])
2699 .unwrap();
2700 let wide_axis = Axis::new(event_scalar("x"), vec![0.0, 2.0]).unwrap();
2701 let projections = vec![
2702 Projection::new("fine", vec![fixture.axis.clone()]).unwrap(),
2703 Projection::new("wide", vec![wide_axis.clone()]).unwrap(),
2704 ];
2705
2706 let expected_fine = cross_section
2707 .differential(std::slice::from_ref(&fixture.axis), &fixture.components)
2708 .unwrap();
2709 let expected_wide = cross_section
2710 .differential(std::slice::from_ref(&wide_axis), &fixture.components)
2711 .unwrap();
2712 let actual = cross_section
2713 .projection_set(&projections, &fixture.components)
2714 .unwrap();
2715
2716 assert_eq!(actual.len(), 2);
2717 assert_eq!(
2718 actual.iter().map(|(name, _)| name).collect::<Vec<_>>(),
2719 vec!["fine", "wide"]
2720 );
2721 assert_projection_close(actual.get("fine").unwrap(), &expected_fine);
2722 assert_projection_close(actual.get("wide").unwrap(), &expected_wide);
2723 assert!(actual.get("missing").is_none());
2724 }
2725
2726 #[test]
2727 fn projection_set_shares_intensities_and_identical_bin_assignments() {
2728 let fixture = canonical_selection_fixture();
2729 let cross_section = fixture
2730 .likelihood
2731 .cross_section("signal", fixture.generated, 2.0, vec![1.5, 0.75])
2732 .unwrap();
2733 let wide_axis = Axis::new(event_scalar("x"), vec![0.0, 2.0]).unwrap();
2734 let projections = vec![
2735 Projection::new("first", vec![fixture.axis.clone()]).unwrap(),
2736 Projection::new("alias", vec![fixture.axis]).unwrap(),
2737 Projection::new("wide", vec![wide_axis]).unwrap(),
2738 ];
2739
2740 reset_projection_evaluation_counts();
2741 let result = cross_section
2742 .projection_set(&projections, &fixture.components)
2743 .unwrap();
2744
2745 assert_eq!(result.len(), 3);
2746 assert_eq!(projection_evaluation_counts(), (3, 6));
2747 assert_projection_close(result.get("first").unwrap(), result.get("alias").unwrap());
2748 }
2749
2750 #[test]
2751 fn projection_set_rejects_invalid_requests_before_evaluation() {
2752 let fixture = canonical_selection_fixture();
2753 let cross_section = fixture
2754 .likelihood
2755 .cross_section("signal", fixture.generated, 2.0, vec![1.5, 0.75])
2756 .unwrap();
2757
2758 assert!(Projection::new("", vec![fixture.axis.clone()]).is_err());
2759 assert!(Projection::new("empty", Vec::new()).is_err());
2760 assert!(
2761 cross_section
2762 .projection_set(&[], &fixture.components)
2763 .is_err()
2764 );
2765 let duplicate = vec![
2766 Projection::new("same", vec![fixture.axis.clone()]).unwrap(),
2767 Projection::new("same", vec![fixture.axis]).unwrap(),
2768 ];
2769 reset_projection_evaluation_counts();
2770 let error = cross_section
2771 .projection_set(&duplicate, &fixture.components)
2772 .expect_err("duplicate names must fail");
2773
2774 assert!(
2775 error
2776 .to_string()
2777 .contains("duplicate projection name: same")
2778 );
2779 assert_eq!(projection_evaluation_counts(), (0, 0));
2780 }
2781
2782 #[test]
2783 fn projection_set_execution_errors_report_dataset_and_draw_context() {
2784 let expression: Expr = parameter!("scale", initial: 1.0).into();
2785 let model = CompiledModel::from_expr(&expression).unwrap();
2786 let data = weighted_dataset(&[(0.25, 1.0)]);
2787 let accepted = weighted_dataset(&[(0.25, 1.0)]);
2788 let generated = weighted_dataset(&[(0.25, 1.0)]);
2789 let likelihood = Arc::new(
2790 Likelihood::new([crate::NllTerm::new("signal", &model, &data, &accepted).unwrap()])
2791 .unwrap(),
2792 );
2793 let ensemble = Ensemble::new(vec!["scale".into()], vec![vec![-1.0]]).unwrap();
2794 let cross_section = likelihood
2795 .cross_section_with_ensemble("signal", generated, 1.0, vec![1.0], ensemble)
2796 .unwrap();
2797 let projections = [Projection::new(
2798 "x",
2799 vec![Axis::new(event_scalar("x"), vec![0.0, 1.0]).unwrap()],
2800 )
2801 .unwrap()];
2802
2803 let error = cross_section
2804 .projection_set(&projections, &HashMap::new())
2805 .expect_err("a negative draw intensity must fail");
2806 let message = error.to_string();
2807
2808 assert!(message.contains("accepted MC"), "{message}");
2809 assert!(message.contains("ensemble draw 0"), "{message}");
2810 assert!(message.contains("member `signal`"), "{message}");
2811 assert!(message.contains("projections [x]"), "{message}");
2812
2813 let combined = CrossSection::combine(vec![cross_section.clone(), cross_section]).unwrap();
2814 let error = combined
2815 .projection_set(&projections, &HashMap::new())
2816 .expect_err("a combined negative draw intensity must fail");
2817 let message = error.to_string();
2818 assert!(message.contains("accepted MC"), "{message}");
2819 assert!(message.contains("ensemble draw 0"), "{message}");
2820 assert!(message.contains("member `signal`"), "{message}");
2821 assert!(message.contains("projections [x]"), "{message}");
2822 }
2823
2824 #[test]
2825 fn extended_nll_cross_section_distinguishes_observed_and_fitted_totals() {
2826 let model =
2827 CompiledModel::from_expr(&(event_scalar("x") * parameter!("scale", initial: 0.25)))
2828 .unwrap();
2829 let data = weighted_dataset(&[(2.0, 1.0), (3.0, 1.0)]);
2830 let accepted = weighted_dataset(&[(4.0, 1.0)]);
2831 let generated = weighted_dataset(&[(6.0, 1.0)]);
2832 let likelihood = Arc::new(
2833 Likelihood::new([
2834 crate::ExtendedNllTerm::new("signal", &model, &data, &accepted).unwrap(),
2835 ])
2836 .unwrap(),
2837 );
2838 let cross_section = likelihood
2839 .cross_section("signal", generated, 10.0, likelihood.default_params())
2840 .unwrap();
2841
2842 assert_relative_eq!(cross_section.observed_total().unwrap().value(), 0.3);
2843 assert_relative_eq!(cross_section.fitted_total().unwrap().value(), 0.15);
2844 assert_relative_eq!(
2845 cross_section.total().unwrap().value(),
2846 cross_section.observed_total().unwrap().value()
2847 );
2848 }
2849
2850 #[test]
2851 fn differential_aliases_share_canonical_selection_evaluations() {
2852 let fixture = canonical_selection_fixture();
2853 let ensemble = Ensemble::new(
2854 vec!["a".into(), "b".into()],
2855 vec![vec![1.6, 0.7], vec![1.4, 0.8]],
2856 )
2857 .unwrap();
2858 let cross_section = fixture
2859 .likelihood
2860 .cross_section_with_ensemble(
2861 "signal",
2862 fixture.generated,
2863 10.0,
2864 fixture.likelihood.default_params(),
2865 ensemble,
2866 )
2867 .unwrap();
2868
2869 reset_selection_intensity_evaluation_count();
2870 let differential = cross_section
2871 .differential(std::slice::from_ref(&fixture.axis), &fixture.components)
2872 .unwrap();
2873
2874 assert_eq!(selection_intensity_evaluation_count(), 1);
2875 assert_eq!(differential.components().len(), 3);
2876 assert_eq!(
2877 differential.components()["ordered"].values(),
2878 differential.components()["reordered"].values()
2879 );
2880 assert_eq!(
2881 differential.components()["ordered"].values(),
2882 differential.components()["repeated"].values()
2883 );
2884 assert_eq!(
2885 differential.components()["ordered"].draws(),
2886 differential.components()["reordered"].draws()
2887 );
2888 assert_eq!(
2889 differential.components()["ordered"].draws(),
2890 differential.components()["repeated"].draws()
2891 );
2892 }
2893
2894 #[test]
2895 fn combined_differential_deduplicates_selections_per_member() {
2896 let fixture = canonical_selection_fixture();
2897 let members = [10.0, 15.0]
2898 .into_iter()
2899 .map(|luminosity| {
2900 fixture
2901 .likelihood
2902 .cross_section(
2903 "signal",
2904 fixture.generated.clone(),
2905 luminosity,
2906 fixture.likelihood.default_params(),
2907 )
2908 .unwrap()
2909 })
2910 .collect();
2911 let cross_section = CrossSection::combine(members).unwrap();
2912
2913 reset_selection_intensity_evaluation_count();
2914 let differential = cross_section
2915 .differential(std::slice::from_ref(&fixture.axis), &fixture.components)
2916 .unwrap();
2917
2918 assert_eq!(selection_intensity_evaluation_count(), 4);
2919 assert_eq!(differential.components().len(), 3);
2920 assert_eq!(
2921 differential.components()["ordered"].values(),
2922 differential.components()["reordered"].values()
2923 );
2924 assert_eq!(
2925 differential.components()["ordered"].values(),
2926 differential.components()["repeated"].values()
2927 );
2928 }
2929
2930 #[test]
2931 fn combined_projection_sets_match_independent_combined_differentials() {
2932 let fixture = canonical_selection_fixture();
2933 let members = [10.0, 15.0]
2934 .into_iter()
2935 .map(|luminosity| {
2936 fixture
2937 .likelihood
2938 .cross_section(
2939 "signal",
2940 fixture.generated.clone(),
2941 luminosity,
2942 fixture.likelihood.default_params(),
2943 )
2944 .unwrap()
2945 })
2946 .collect();
2947 let cross_section = CrossSection::combine(members).unwrap();
2948 let wide_axis = Axis::new(event_scalar("x"), vec![0.0, 2.0]).unwrap();
2949 let projections = vec![
2950 Projection::new("fine", vec![fixture.axis.clone()]).unwrap(),
2951 Projection::new("fine_alias", vec![fixture.axis.clone()]).unwrap(),
2952 Projection::new("wide", vec![wide_axis.clone()]).unwrap(),
2953 ];
2954
2955 reset_projection_evaluation_counts();
2956 let actual = cross_section
2957 .projection_set(&projections, &fixture.components)
2958 .unwrap();
2959 assert_eq!(projection_evaluation_counts(), (8, 12));
2960 assert_projection_close(
2961 actual.get("fine").unwrap(),
2962 actual.get("fine_alias").unwrap(),
2963 );
2964
2965 for (name, axes) in [("fine", vec![fixture.axis]), ("wide", vec![wide_axis])] {
2966 let expected = cross_section
2967 .differential(&axes, &fixture.components)
2968 .unwrap();
2969 assert_projection_close(actual.get(name).unwrap(), &expected);
2970 }
2971 }
2972
2973 #[test]
2974 fn combined_projection_sets_pair_distinct_ensemble_and_factor_sources() {
2975 let fixture = canonical_selection_fixture();
2976 let ensemble_a = Ensemble::new(
2977 vec!["a".into(), "b".into()],
2978 vec![vec![1.6, 0.7], vec![1.4, 0.8]],
2979 )
2980 .unwrap();
2981 let ensemble_b = Ensemble::new(
2982 vec!["a".into(), "b".into()],
2983 vec![vec![1.7, 0.6], vec![1.3, 0.9]],
2984 )
2985 .unwrap();
2986 let source_a = ensemble_a.source_id();
2987 let source_b = ensemble_b.source_id();
2988 let members = [ensemble_a.clone(), ensemble_b.clone()]
2989 .into_iter()
2990 .enumerate()
2991 .map(|(index, ensemble)| {
2992 fixture
2993 .likelihood
2994 .cross_section_with_ensemble(
2995 "signal",
2996 fixture.generated.clone(),
2997 10.0 + index as f64 * 5.0,
2998 fixture.likelihood.default_params(),
2999 ensemble,
3000 )
3001 .unwrap()
3002 })
3003 .collect::<Vec<_>>();
3004 let factors = vec![
3005 Estimate::with_source_id(1.0, vec![1.1, 1.2], Some(source_a)).unwrap(),
3006 Estimate::with_source_id(2.0, vec![2.1, 2.2], Some(source_b)).unwrap(),
3007 ];
3008 let combined = CrossSection::combine_with_factors(members, factors.clone()).unwrap();
3009 let projection = Projection::new("x", vec![fixture.axis.clone()]).unwrap();
3010 let actual = combined
3011 .projection_set(std::slice::from_ref(&projection), &fixture.components)
3012 .unwrap();
3013
3014 for index in 0..2 {
3015 let paired_b = paired_draw_index(index, 1, 2, Some(source_b), Some(source_a));
3016 let explicit_members = [
3017 fixture
3018 .likelihood
3019 .cross_section(
3020 "signal",
3021 fixture.generated.clone(),
3022 10.0,
3023 ensemble_a.draws()[index].clone(),
3024 )
3025 .unwrap(),
3026 fixture
3027 .likelihood
3028 .cross_section(
3029 "signal",
3030 fixture.generated.clone(),
3031 15.0,
3032 ensemble_b.draws()[paired_b].clone(),
3033 )
3034 .unwrap(),
3035 ];
3036 let expected = CrossSection::combine_with_factors(
3037 explicit_members.into(),
3038 vec![
3039 Estimate::central(factors[0].draws()[index]).unwrap(),
3040 Estimate::central(factors[1].draws()[paired_b]).unwrap(),
3041 ],
3042 )
3043 .unwrap()
3044 .differential(projection.axes(), &fixture.components)
3045 .unwrap();
3046 let actual = actual.get("x").unwrap();
3047 assert_eq!(actual.data().draws()[index], expected.data().values());
3048 assert_eq!(actual.model().draws()[index], expected.model().values());
3049 for name in fixture.components.keys() {
3050 assert_eq!(
3051 actual.components()[name].draws()[index],
3052 expected.components()[name].values()
3053 );
3054 }
3055 }
3056 }
3057
3058 #[test]
3059 fn combined_projection_sets_use_arbitrary_replica_event_rows() {
3060 let model = CompiledModel::from_expr(&(event_scalar("x") + 1.0)).unwrap();
3061 let accepted = weighted_dataset(&[(0.25, 1.0), (1.25, 1.0)]);
3062 let generated = weighted_dataset(&[(0.25, 1.0), (1.25, 1.0)]);
3063 let make_likelihood = |data: Dataset| {
3064 Arc::new(
3065 Likelihood::new([crate::NllTerm::new("signal", &model, &data, &accepted).unwrap()])
3066 .unwrap(),
3067 )
3068 };
3069 let likelihood = make_likelihood(weighted_dataset(&[(0.25, 1.0), (1.25, 1.0)]));
3070 let replicas = vec![
3071 make_likelihood(weighted_dataset(&[(0.25, 2.0)])),
3072 make_likelihood(weighted_dataset(&[(1.25, 3.0)])),
3073 ];
3074 let ensemble =
3075 Ensemble::with_replicas(Vec::new(), vec![Vec::new(), Vec::new()], replicas.clone())
3076 .unwrap();
3077 let members = [1.0, 2.0]
3078 .into_iter()
3079 .map(|luminosity| {
3080 likelihood
3081 .cross_section_with_ensemble(
3082 "signal",
3083 generated.clone(),
3084 luminosity,
3085 Vec::new(),
3086 ensemble.clone(),
3087 )
3088 .unwrap()
3089 })
3090 .collect();
3091 let axis = Axis::new(event_scalar("x"), vec![0.0, 1.0, 2.0]).unwrap();
3092 let projection = Projection::new("x", vec![axis.clone()]).unwrap();
3093 let actual = CrossSection::combine(members)
3094 .unwrap()
3095 .projection_set(std::slice::from_ref(&projection), &HashMap::new())
3096 .unwrap();
3097
3098 for (index, replica) in replicas.iter().enumerate() {
3099 let explicit_members = [1.0, 2.0]
3100 .into_iter()
3101 .map(|luminosity| {
3102 replica
3103 .cross_section("signal", generated.clone(), luminosity, Vec::new())
3104 .unwrap()
3105 })
3106 .collect();
3107 let expected = CrossSection::combine(explicit_members)
3108 .unwrap()
3109 .differential(std::slice::from_ref(&axis), &HashMap::new())
3110 .unwrap();
3111 assert_eq!(
3112 actual.get("x").unwrap().data().draws()[index],
3113 expected.data().values()
3114 );
3115 }
3116 }
3117
3118 #[test]
3119 fn optimized_bootstrap_differential_matches_individual_replica_evaluations() {
3120 let x = event_scalar("x");
3121 let selected = (Expr::from(parameter!("a", initial: 1.5)) * x.clone()).tagged("selected");
3122 let remainder = Expr::from(parameter!("b", initial: 0.75)).tagged("remainder");
3123 let model = CompiledModel::from_expr(&(selected + remainder).norm_sqr()).unwrap();
3124 let data = weighted_dataset(&[(0.25, 1.0), (0.75, 2.0), (1.25, 1.0)]);
3125 let accepted = weighted_dataset(&[(0.25, 1.0), (0.75, 1.0), (1.25, 1.0)]);
3126 let generated = weighted_dataset(&[(0.25, 1.0), (0.75, 1.0), (1.25, 1.0), (1.75, 1.0)]);
3127 let likelihood = Arc::new(
3128 Likelihood::new([crate::NllTerm::new("signal", &model, &data, &accepted).unwrap()])
3129 .unwrap(),
3130 );
3131 let ensemble = Ensemble::bootstrap_fit(&likelihood, 3, 73, |replica, index| {
3132 let mut parameters = replica.default_params();
3133 parameters[0] += index as f64 * 0.1;
3134 parameters[1] -= index as f64 * 0.05;
3135 Ok::<_, std::convert::Infallible>(parameters)
3136 })
3137 .unwrap();
3138 let axis = Axis::new(x, vec![0.0, 1.0, 2.0]).unwrap();
3139 let components = HashMap::from([("selected".into(), vec!["selected".into()])]);
3140 let propagated = likelihood
3141 .cross_section_with_ensemble(
3142 "signal",
3143 generated.clone(),
3144 10.0,
3145 likelihood.default_params(),
3146 ensemble.clone(),
3147 )
3148 .unwrap()
3149 .differential(std::slice::from_ref(&axis), &components)
3150 .unwrap();
3151
3152 for (index, (replica, parameters)) in
3153 ensemble.replicas().iter().zip(ensemble.draws()).enumerate()
3154 {
3155 let individual = replica
3156 .cross_section("signal", generated.clone(), 10.0, parameters.clone())
3157 .unwrap()
3158 .differential(std::slice::from_ref(&axis), &components)
3159 .unwrap();
3160 assert_eq!(propagated.data().draws()[index], individual.data().values());
3161 assert_eq!(
3162 propagated.model().draws()[index],
3163 individual.model().values()
3164 );
3165 assert_eq!(
3166 propagated.components()["selected"].draws()[index],
3167 individual.components()["selected"].values()
3168 );
3169 }
3170 }
3171
3172 #[test]
3173 fn arbitrary_replica_differentials_use_each_replicas_event_rows() {
3174 let model = CompiledModel::from_expr(&(event_scalar("x") + 1.0)).unwrap();
3175 let accepted = weighted_dataset(&[(0.25, 1.0), (1.25, 1.0)]);
3176 let generated = weighted_dataset(&[(0.25, 1.0), (1.25, 1.0)]);
3177 let make_likelihood = |data: Dataset| {
3178 Arc::new(
3179 Likelihood::new([crate::NllTerm::new("signal", &model, &data, &accepted).unwrap()])
3180 .unwrap(),
3181 )
3182 };
3183 let likelihood = make_likelihood(weighted_dataset(&[(0.25, 1.0), (1.25, 1.0)]));
3184 let replicas = vec![
3185 make_likelihood(weighted_dataset(&[(0.25, 2.0)])),
3186 make_likelihood(weighted_dataset(&[(1.25, 3.0)])),
3187 ];
3188 let ensemble =
3189 Ensemble::with_replicas(Vec::new(), vec![Vec::new(), Vec::new()], replicas.clone())
3190 .unwrap();
3191 let axis = Axis::new(event_scalar("x"), vec![0.0, 1.0, 2.0]).unwrap();
3192 let propagated = likelihood
3193 .cross_section_with_ensemble("signal", generated.clone(), 1.0, Vec::new(), ensemble)
3194 .unwrap()
3195 .differential(std::slice::from_ref(&axis), &HashMap::new())
3196 .unwrap();
3197
3198 for (index, replica) in replicas.iter().enumerate() {
3199 let individual = replica
3200 .cross_section("signal", generated.clone(), 1.0, Vec::new())
3201 .unwrap()
3202 .differential(std::slice::from_ref(&axis), &HashMap::new())
3203 .unwrap();
3204 assert_eq!(propagated.data().draws()[index], individual.data().values());
3205 }
3206 }
3207
3208 #[test]
3209 fn optimized_combined_differential_matches_explicit_draw_combinations() {
3210 let x = event_scalar("x");
3211 let selected = (Expr::from(parameter!("a", initial: 1.5)) * x.clone()).tagged("selected");
3212 let remainder = Expr::from(parameter!("b", initial: 0.75)).tagged("remainder");
3213 let model = CompiledModel::from_expr(&(selected + remainder).norm_sqr()).unwrap();
3214 let data = weighted_dataset(&[(0.25, 1.0), (0.75, 2.0), (1.25, 1.0)]);
3215 let accepted = weighted_dataset(&[(0.25, 1.0), (0.75, 1.0), (1.25, 1.0)]);
3216 let generated = weighted_dataset(&[(0.25, 1.0), (0.75, 1.0), (1.25, 1.0), (1.75, 1.0)]);
3217 let data_b = weighted_dataset(&[(0.25, 2.0), (0.75, 1.0), (1.75, 2.0)]);
3218 let accepted_b = weighted_dataset(&[(0.25, 1.0), (1.25, 1.0), (1.75, 1.0)]);
3219 let generated_b = weighted_dataset(&[(0.25, 1.0), (0.75, 1.0), (1.25, 2.0), (1.75, 1.0)]);
3220 let likelihood = Arc::new(
3221 Likelihood::new([
3222 crate::NllTerm::new("period_a", &model, &data, &accepted).unwrap(),
3223 crate::NllTerm::new("period_b", &model, &data_b, &accepted_b).unwrap(),
3224 ])
3225 .unwrap(),
3226 );
3227 let ensemble = Ensemble::bootstrap_fit(&likelihood, 3, 91, |replica, index| {
3228 let mut parameters = replica.default_params();
3229 parameters[0] += index as f64 * 0.1;
3230 parameters[1] -= index as f64 * 0.05;
3231 Ok::<_, std::convert::Infallible>(parameters)
3232 })
3233 .unwrap();
3234 let member_inputs = [
3235 ("period_a", generated.clone(), 10.0),
3236 ("period_b", generated_b.clone(), 15.0),
3237 ];
3238 let members = member_inputs
3239 .iter()
3240 .map(|(name, generated, luminosity)| {
3241 likelihood
3242 .cross_section_with_ensemble(
3243 *name,
3244 generated.clone(),
3245 *luminosity,
3246 likelihood.default_params(),
3247 ensemble.clone(),
3248 )
3249 .unwrap()
3250 })
3251 .collect();
3252 let axis = Axis::new(x, vec![0.0, 1.0, 2.0]).unwrap();
3253 let components = HashMap::from([("selected".into(), vec!["selected".into()])]);
3254 let propagated = CrossSection::combine(members)
3255 .unwrap()
3256 .differential(std::slice::from_ref(&axis), &components)
3257 .unwrap();
3258
3259 for (index, (replica, parameters)) in
3260 ensemble.replicas().iter().zip(ensemble.draws()).enumerate()
3261 {
3262 let explicit_members = member_inputs
3263 .iter()
3264 .map(|(name, generated, luminosity)| {
3265 replica
3266 .cross_section(*name, generated.clone(), *luminosity, parameters.clone())
3267 .unwrap()
3268 })
3269 .collect();
3270 let explicit = CrossSection::combine(explicit_members)
3271 .unwrap()
3272 .differential(std::slice::from_ref(&axis), &components)
3273 .unwrap();
3274 assert_eq!(propagated.data().draws()[index], explicit.data().values());
3275 assert_eq!(propagated.model().draws()[index], explicit.model().values());
3276 assert_eq!(
3277 propagated.components()["selected"].draws()[index],
3278 explicit.components()["selected"].values()
3279 );
3280 }
3281 }
3282}