1use std::{
2 collections::{HashMap, HashSet},
3 sync::Arc,
4};
5
6use fastrand::Rng;
7use laddu_core::{
8 math::{q_m, Histogram, Sheet},
9 Dataset, DatasetMetadata, Expression, LadduError, LadduResult, Particle, Reaction, Vec3, Vec4,
10 PI,
11};
12use serde::{Deserialize, Serialize};
13
14use crate::distributions::{
15 Distribution, HistogramSampler, LadduGenRngExt, MandelstamTDistribution, SimpleDistribution,
16};
17
18#[derive(Clone, Debug, PartialEq, Eq)]
24pub enum GeneratedStorage {
25 All,
27 Only(Vec<String>),
29}
30
31impl GeneratedStorage {
32 pub fn all() -> Self {
34 Self::All
35 }
36
37 pub fn only<I, S>(ids: I) -> Self
39 where
40 I: IntoIterator<Item = S>,
41 S: Into<String>,
42 {
43 Self::Only(ids.into_iter().map(Into::into).collect())
44 }
45
46 pub fn stores(&self, id: &str) -> bool {
48 match self {
49 Self::All => true,
50 Self::Only(ids) => ids.iter().any(|stored_id| stored_id == id),
51 }
52 }
53
54 fn validate(&self, available_ids: &[String]) -> LadduResult<()> {
55 let available = available_ids
56 .iter()
57 .map(String::as_str)
58 .collect::<HashSet<_>>();
59 let Self::Only(ids) = self else {
60 return Ok(());
61 };
62 let mut seen = HashSet::new();
63 for id in ids {
64 if !seen.insert(id.as_str()) {
65 return Err(LadduError::Custom(format!(
66 "generated storage contains duplicate particle ID '{id}'"
67 )));
68 }
69 if !available.contains(id.as_str()) {
70 return Err(LadduError::Custom(format!(
71 "generated storage references unknown particle ID '{id}'"
72 )));
73 }
74 }
75 Ok(())
76 }
77
78 fn stored_labels(&self, all_labels: &[String]) -> Vec<String> {
79 all_labels
80 .iter()
81 .filter(|label| self.stores(label))
82 .cloned()
83 .collect()
84 }
85}
86
87#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
93pub enum ParticleSpecies {
94 Code {
96 id: i64,
98 namespace: Option<String>,
100 },
101 Label(String),
103}
104
105impl ParticleSpecies {
106 pub fn code(id: i64) -> Self {
108 Self::Code {
109 id,
110 namespace: None,
111 }
112 }
113
114 pub fn with_namespace(namespace: impl Into<String>, id: i64) -> Self {
116 Self::Code {
117 id,
118 namespace: Some(namespace.into()),
119 }
120 }
121
122 pub fn label(label: impl Into<String>) -> Self {
124 Self::Label(label.into())
125 }
126}
127
128fn basis(z: Vec3) -> (Vec3, Vec3, Vec3) {
129 let z = z.unit();
130 let ref_axis = if z.z.abs() < 0.9 {
131 Vec3::z()
132 } else {
133 Vec3::y()
134 };
135 let x = ref_axis.cross(&z).unit();
136 let y = z.cross(&x);
137 (x, y, z)
138}
139
140#[derive(Clone, Debug)]
142pub struct InitialGenerator {
143 mass: f64,
144 energy_distribution: SimpleDistribution,
145}
146
147impl InitialGenerator {
148 pub fn beam_with_fixed_energy(mass: f64, energy: f64) -> Self {
150 debug_assert!(mass >= 0.0, "Mass cannot be negative!\nMass: {}", mass);
151 debug_assert!(energy > 0.0, "Energy must be positive!\nEnergy: {}", energy);
152 Self {
153 mass,
154 energy_distribution: SimpleDistribution::Fixed(energy),
155 }
156 }
157
158 pub fn beam(mass: f64, min_energy: f64, max_energy: f64) -> Self {
160 debug_assert!(mass >= 0.0, "Mass cannot be negative!\nMass: {}", mass);
161 debug_assert!(
162 min_energy > 0.0,
163 "Minimum energy must be positive!\nMinimum Energy: {}",
164 min_energy
165 );
166 debug_assert!(
167 max_energy > min_energy,
168 "Maximum energy must be greater than minimum energy!"
169 );
170 Self {
171 mass,
172 energy_distribution: SimpleDistribution::Uniform {
173 min: min_energy,
174 max: max_energy,
175 },
176 }
177 }
178
179 pub fn beam_with_energy_histogram(mass: f64, energy: Histogram) -> LadduResult<Self> {
181 debug_assert!(
182 mass >= 0.0,
183 "Mass must be positive and greater than zero!\nMass: {}",
184 mass
185 );
186 let sampler = HistogramSampler::new(energy)?;
187 debug_assert!(
188 sampler.hist.bin_edges()[0] >= mass,
189 "Mass cannot be greater than the minimum allowed energy!\nMass: {}\nMinimum Energy: {}",
190 mass,
191 sampler.hist.bin_edges()[0]
192 );
193 Ok(Self {
194 mass,
195 energy_distribution: SimpleDistribution::Histogram(sampler),
196 })
197 }
198
199 pub fn target(mass: f64) -> Self {
201 Self {
202 mass,
203 energy_distribution: SimpleDistribution::Fixed(mass),
204 }
205 }
206}
207
208#[derive(Clone, Debug)]
210pub struct CompositeGenerator {
211 mass_distribution: SimpleDistribution,
212}
213
214impl CompositeGenerator {
215 pub fn new(min_mass: f64, max_mass: f64) -> Self {
217 Self {
218 mass_distribution: SimpleDistribution::Uniform {
219 min: min_mass,
220 max: max_mass,
221 },
222 }
223 }
224
225 fn sample_mass(&self, rng: &mut Rng) -> f64 {
226 self.mass_distribution.sample(rng)
227 }
228}
229
230#[derive(Clone, Debug)]
232pub struct StableGenerator {
233 mass_distribution: SimpleDistribution,
234}
235
236impl StableGenerator {
237 pub fn new(mass: f64) -> Self {
239 debug_assert!(mass >= 0.0, "Mass cannot be negative!\nMass: {}", mass);
240 Self {
241 mass_distribution: SimpleDistribution::Fixed(mass),
242 }
243 }
244
245 fn sample_mass(&self, rng: &mut Rng) -> f64 {
246 self.mass_distribution.sample(rng)
247 }
248}
249
250#[derive(Clone, Debug, PartialEq)]
252pub enum Reconstruction {
253 Stored,
255 Fixed(Vec4),
257 Missing,
259 Composite,
261}
262
263#[derive(Clone, Debug)]
265pub enum GeneratedParticle {
266 Initial {
268 id: String,
269 generator: InitialGenerator,
270 reconstruction: Reconstruction,
271 species: Option<ParticleSpecies>,
272 },
273 Stable {
275 id: String,
276 generator: StableGenerator,
277 reconstruction: Reconstruction,
278 species: Option<ParticleSpecies>,
279 },
280 Composite {
282 id: String,
283 generator: CompositeGenerator,
284 daughters: (Box<GeneratedParticle>, Box<GeneratedParticle>),
285 reconstruction: Reconstruction,
286 species: Option<ParticleSpecies>,
287 },
288}
289
290impl GeneratedParticle {
291 pub fn initial(
293 id: impl Into<String>,
294 generator: InitialGenerator,
295 reconstruction: Reconstruction,
296 ) -> Self {
297 Self::Initial {
298 id: id.into(),
299 generator,
300 reconstruction,
301 species: None,
302 }
303 }
304
305 pub fn stable(
307 id: impl Into<String>,
308 generator: StableGenerator,
309 reconstruction: Reconstruction,
310 ) -> Self {
311 Self::Stable {
312 id: id.into(),
313 generator,
314 reconstruction,
315 species: None,
316 }
317 }
318
319 pub fn composite(
321 id: impl Into<String>,
322 generator: CompositeGenerator,
323 daughters: (&GeneratedParticle, &GeneratedParticle),
324 reconstruction: Reconstruction,
325 ) -> Self {
326 Self::Composite {
327 id: id.into(),
328 generator,
329 daughters: (Box::new(daughters.0.clone()), Box::new(daughters.1.clone())),
330 reconstruction,
331 species: None,
332 }
333 }
334
335 pub fn with_species(mut self, species: ParticleSpecies) -> Self {
337 match &mut self {
338 Self::Initial {
339 species: particle_species,
340 ..
341 }
342 | Self::Stable {
343 species: particle_species,
344 ..
345 }
346 | Self::Composite {
347 species: particle_species,
348 ..
349 } => *particle_species = Some(species),
350 }
351 self
352 }
353
354 pub fn id(&self) -> &str {
356 match self {
357 Self::Initial { id, .. } | Self::Stable { id, .. } | Self::Composite { id, .. } => id,
358 }
359 }
360
361 pub fn species(&self) -> Option<&ParticleSpecies> {
363 match self {
364 Self::Initial { species, .. }
365 | Self::Stable { species, .. }
366 | Self::Composite { species, .. } => species.as_ref(),
367 }
368 }
369
370 pub fn reconstruction(&self) -> &Reconstruction {
372 match self {
373 Self::Initial { reconstruction, .. }
374 | Self::Stable { reconstruction, .. }
375 | Self::Composite { reconstruction, .. } => reconstruction,
376 }
377 }
378
379 fn p4_labels(&self) -> Vec<String> {
380 let mut labels = vec![self.id().to_string()];
381 if let Self::Composite { daughters, .. } = self {
382 labels.append(&mut daughters.0.p4_labels());
383 labels.append(&mut daughters.1.p4_labels());
384 }
385 labels
386 }
387
388 fn append_decay_layout(
389 &self,
390 parent_id: Option<usize>,
391 produced_vertex_id: Option<usize>,
392 storage: &GeneratedStorage,
393 particles: &mut Vec<GeneratedParticleLayout>,
394 vertices: &mut Vec<GeneratedVertexLayout>,
395 ) -> usize {
396 let product_id = particles.len();
397 particles.push(GeneratedParticleLayout {
398 id: self.id().to_string(),
399 product_id,
400 parent_id,
401 species: self.species().cloned(),
402 p4_label: storage.stores(self.id()).then(|| self.id().to_string()),
403 produced_vertex_id,
404 decay_vertex_id: None,
405 });
406 if let Self::Composite { daughters, .. } = self {
407 let vertex_id = vertices.len();
408 particles[product_id].decay_vertex_id = Some(vertex_id);
409 vertices.push(GeneratedVertexLayout {
410 vertex_id,
411 kind: GeneratedVertexKind::Decay,
412 incoming_product_ids: vec![product_id],
413 outgoing_product_ids: Vec::new(),
414 });
415 let daughter_1_id = daughters.0.append_decay_layout(
416 Some(product_id),
417 Some(vertex_id),
418 storage,
419 particles,
420 vertices,
421 );
422 let daughter_2_id = daughters.1.append_decay_layout(
423 Some(product_id),
424 Some(vertex_id),
425 storage,
426 particles,
427 vertices,
428 );
429 vertices[vertex_id].outgoing_product_ids = vec![daughter_1_id, daughter_2_id];
430 }
431 product_id
432 }
433
434 fn sample_mass(&self, rng: &mut Rng) -> f64 {
435 match self {
436 Self::Initial { generator, .. } => generator.mass,
437 Self::Stable { generator, .. } => generator.sample_mass(rng),
438 Self::Composite { generator, .. } => generator.sample_mass(rng),
439 }
440 }
441
442 fn generated_particle(&self) -> LadduResult<Particle> {
443 match self.reconstruction() {
444 Reconstruction::Stored => Ok(Particle::stored(self.id())),
445 Reconstruction::Fixed(p4) => Ok(Particle::fixed(self.id(), *p4)),
446 Reconstruction::Missing => Ok(Particle::missing(self.id())),
447 Reconstruction::Composite => {
448 let Self::Composite { daughters, .. } = self else {
449 return Err(LadduError::Custom(format!(
450 "particle '{}' cannot use composite reconstruction without daughters",
451 self.id()
452 )));
453 };
454 let daughter_1 = daughters.0.generated_particle()?;
455 let daughter_2 = daughters.1.generated_particle()?;
456 Particle::composite(self.id(), (&daughter_1, &daughter_2))
457 }
458 }
459 }
460
461 fn validate_reconstruction(&self) -> LadduResult<()> {
462 match (self, self.reconstruction()) {
463 (Self::Composite { daughters, .. }, Reconstruction::Composite) => {
464 daughters.0.validate_reconstruction()?;
465 daughters.1.validate_reconstruction()?;
466 Ok(())
467 }
468 (Self::Composite { .. }, _) => Ok(()),
469 (_, Reconstruction::Composite) => Err(LadduError::Custom(format!(
470 "particle '{}' cannot use composite reconstruction without daughters",
471 self.id()
472 ))),
473 _ => Ok(()),
474 }
475 }
476
477 fn collect_ids<'a>(&'a self, seen: &mut HashSet<&'a str>) -> LadduResult<()> {
478 if !seen.insert(self.id()) {
479 return Err(LadduError::Custom(format!(
480 "duplicate generated particle identifier '{}'",
481 self.id()
482 )));
483 }
484 if let Self::Composite { daughters, .. } = self {
485 daughters.0.collect_ids(seen)?;
486 daughters.1.collect_ids(seen)?;
487 }
488 Ok(())
489 }
490
491 fn generate_decay(
492 &self,
493 rng: &mut Rng,
494 p4_cm: Vec4,
495 cm_to_lab_boost: &Vec3,
496 p4_storage: &mut HashMap<String, Vec<Vec4>>,
497 ) {
498 let p4_lab = p4_cm.boost(cm_to_lab_boost);
499 if let Some(storage) = p4_storage.get_mut(self.id()) {
500 storage.push(p4_lab);
501 }
502
503 let Self::Composite { daughters, .. } = self else {
504 return;
505 };
506 let d1 = &daughters.0;
507 let d2 = &daughters.1;
508 let parent_mass = p4_cm.m();
509 let m1 = d1.sample_mass(rng);
510 let m2 = d2.sample_mass(rng);
511 let q = q_m(parent_mass, m1, m2, Sheet::Physical).re;
512 let parent_msq = parent_mass * parent_mass;
513 let msq1 = m1 * m1;
514 let msq2 = m2 * m2;
515 let e1 = (parent_msq + msq1 - msq2) / (2.0 * parent_mass);
516 let e2 = (parent_msq + msq2 - msq1) / (2.0 * parent_mass);
517
518 let cos_theta = rng.uniform(-1.0, 1.0);
519 let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
520 let phi = rng.uniform(0.0, 2.0 * PI);
521 let (sin_phi, cos_phi) = phi.sin_cos();
522
523 let dir = Vec3::new(sin_theta * cos_phi, sin_theta * sin_phi, cos_theta);
524 let p1_p4_rest = (dir * q).with_energy(e1);
525 let p2_p4_rest = (-dir * q).with_energy(e2);
526 let parent_to_cm_boost = p4_cm.beta();
527 let p1_p4_cm = p1_p4_rest.boost(&parent_to_cm_boost);
528 let p2_p4_cm = p2_p4_rest.boost(&parent_to_cm_boost);
529 d1.generate_decay(rng, p1_p4_cm, cm_to_lab_boost, p4_storage);
530 d2.generate_decay(rng, p2_p4_cm, cm_to_lab_boost, p4_storage);
531 }
532}
533
534#[derive(Clone, Debug)]
536pub struct GeneratedTwoToTwoReaction {
537 p1: GeneratedParticle,
538 p2: GeneratedParticle,
539 p3: GeneratedParticle,
540 p4: GeneratedParticle,
541 tdist: MandelstamTDistribution,
542 p1_p3_lab_dir: Vec3,
543 p2_p3_lab_dir: Vec3,
544}
545
546impl GeneratedTwoToTwoReaction {
547 pub fn new(
549 p1: GeneratedParticle,
550 p2: GeneratedParticle,
551 p3: GeneratedParticle,
552 p4: GeneratedParticle,
553 tdist: MandelstamTDistribution,
554 ) -> LadduResult<Self> {
555 validate_initial_role(&p1, "p1")?;
556 validate_initial_role(&p2, "p2")?;
557 validate_final_role(&p3, "p3")?;
558 validate_final_role(&p4, "p4")?;
559 let reaction = Self {
560 p1,
561 p2,
562 p3,
563 p4,
564 tdist,
565 p1_p3_lab_dir: Vec3::z(),
566 p2_p3_lab_dir: -Vec3::z(),
567 };
568 reaction.validate()?;
569 Ok(reaction)
570 }
571
572 fn validate(&self) -> LadduResult<()> {
573 let mut seen = HashSet::new();
574 for particle in [&self.p1, &self.p2, &self.p3, &self.p4] {
575 particle.collect_ids(&mut seen)?;
576 particle.validate_reconstruction()?;
577 }
578 Ok(())
579 }
580
581 fn p4_labels(&self) -> Vec<String> {
582 let mut labels = Vec::new();
583 for particle in [&self.p1, &self.p2, &self.p3, &self.p4] {
584 labels.append(&mut particle.p4_labels());
585 }
586 labels
587 }
588
589 fn layout_components(
590 &self,
591 storage: &GeneratedStorage,
592 ) -> (Vec<GeneratedParticleLayout>, Vec<GeneratedVertexLayout>) {
593 let mut particles = Vec::new();
594 let mut vertices = vec![GeneratedVertexLayout {
595 vertex_id: 0,
596 kind: GeneratedVertexKind::Production,
597 incoming_product_ids: Vec::new(),
598 outgoing_product_ids: Vec::new(),
599 }];
600 let p1_id = self
601 .p1
602 .append_decay_layout(None, None, storage, &mut particles, &mut vertices);
603 let p2_id = self
604 .p2
605 .append_decay_layout(None, None, storage, &mut particles, &mut vertices);
606 let p3_id =
607 self.p3
608 .append_decay_layout(None, Some(0), storage, &mut particles, &mut vertices);
609 let p4_id =
610 self.p4
611 .append_decay_layout(None, Some(0), storage, &mut particles, &mut vertices);
612 vertices[0].incoming_product_ids = vec![p1_id, p2_id];
613 vertices[0].outgoing_product_ids = vec![p3_id, p4_id];
614 (particles, vertices)
615 }
616
617 fn particle_layouts(&self) -> Vec<GeneratedParticleLayout> {
618 self.particle_layouts_with_storage(&GeneratedStorage::All)
619 }
620
621 fn particle_layouts_with_storage(
622 &self,
623 storage: &GeneratedStorage,
624 ) -> Vec<GeneratedParticleLayout> {
625 self.layout_components(storage).0
626 }
627
628 fn vertex_layouts(&self) -> Vec<GeneratedVertexLayout> {
629 self.layout_components(&GeneratedStorage::All).1
630 }
631
632 fn reconstructed_reaction(&self) -> LadduResult<Reaction> {
633 Reaction::two_to_two(
634 &self.p1.generated_particle()?,
635 &self.p2.generated_particle()?,
636 &self.p3.generated_particle()?,
637 &self.p4.generated_particle()?,
638 )
639 }
640
641 fn generate_event(&self, rng: &mut Rng, p4_storage: &mut HashMap<String, Vec<Vec4>>) {
642 let GeneratedParticle::Initial {
643 id: p1_id,
644 generator: p1_generator,
645 ..
646 } = &self.p1
647 else {
648 unreachable!("validated generated two-to-two p1 role")
649 };
650 let GeneratedParticle::Initial {
651 id: p2_id,
652 generator: p2_generator,
653 ..
654 } = &self.p2
655 else {
656 unreachable!("validated generated two-to-two p2 role")
657 };
658
659 let p1_e = p1_generator.energy_distribution.sample(rng);
660 let p1_m = p1_generator.mass;
661 let p1_msq = p1_m * p1_m;
662 let p1_p4_lab = rng.p4(p1_m, p1_e, self.p1_p3_lab_dir);
663 if let Some(storage) = p4_storage.get_mut(p1_id) {
664 storage.push(p1_p4_lab)
665 }
666
667 let p2_e = p2_generator.energy_distribution.sample(rng);
668 let p2_m = p2_generator.mass;
669 let p2_msq = p2_m * p2_m;
670 let p2_p4_lab = rng.p4(p2_m, p2_e, self.p2_p3_lab_dir);
671 if let Some(storage) = p4_storage.get_mut(p2_id) {
672 storage.push(p2_p4_lab)
673 }
674
675 let cm = p1_p4_lab + p2_p4_lab;
676 let cm_boost = -cm.beta();
677 let s = cm.mag2();
678 let sqrt_s = s.sqrt();
679
680 let p1_p4_cm = p1_p4_lab.boost(&cm_boost);
681 let p3_m = self.p3.sample_mass(rng);
682 let p3_msq = p3_m * p3_m;
683 let p4_m = self.p4.sample_mass(rng);
684 let p4_msq = p4_m * p4_m;
685 let p_in_mag = q_m(sqrt_s, p1_m, p2_m, Sheet::Physical).re;
686 let p_out_mag = q_m(sqrt_s, p3_m, p4_m, Sheet::Physical).re;
687 let p1_e_cm = (s + p1_msq - p2_msq) / (2.0 * sqrt_s);
688 let p3_e_cm = (s + p3_msq - p4_msq) / (2.0 * sqrt_s);
689 let p4_e_cm = (s + p4_msq - p3_msq) / (2.0 * sqrt_s);
690 let a = p1_msq + p3_msq - 2.0 * p1_e_cm * p3_e_cm;
691 let b_angle = 2.0 * p_in_mag * p_out_mag;
692 let t_lo = a - b_angle;
693 let t_hi = a + b_angle;
694 let t = self.tdist.sample(rng, Some((t_lo, t_hi)));
695 let costheta = (t - a) / b_angle;
696 let sintheta = (1.0 - costheta * costheta).sqrt();
697 let phi = rng.uniform(0.0, 2.0 * PI);
698 let (sin_phi, cos_phi) = phi.sin_cos();
699 let (x, y, z) = basis(p1_p4_cm.vec3());
700 let p3_dir_cm = x * (sintheta * cos_phi) + y * (sintheta * sin_phi) + z * costheta;
701
702 let p3_p4_cm = (p3_dir_cm * p_out_mag).with_energy(p3_e_cm);
703 self.p3
704 .generate_decay(rng, p3_p4_cm, &-cm_boost, p4_storage);
705 let p4_p4_cm = (-p3_dir_cm * p_out_mag).with_energy(p4_e_cm);
706 self.p4
707 .generate_decay(rng, p4_p4_cm, &-cm_boost, p4_storage);
708 }
709}
710
711fn validate_initial_role(particle: &GeneratedParticle, role: &str) -> LadduResult<()> {
712 if matches!(particle, GeneratedParticle::Initial { .. }) {
713 Ok(())
714 } else {
715 Err(LadduError::Custom(format!(
716 "generated two-to-two role '{role}' requires an initial particle"
717 )))
718 }
719}
720
721fn validate_final_role(particle: &GeneratedParticle, role: &str) -> LadduResult<()> {
722 if matches!(
723 particle,
724 GeneratedParticle::Stable { .. } | GeneratedParticle::Composite { .. }
725 ) {
726 Ok(())
727 } else {
728 Err(LadduError::Custom(format!(
729 "generated two-to-two role '{role}' requires an outgoing particle"
730 )))
731 }
732}
733
734#[derive(Clone, Debug)]
736pub enum GeneratedReactionTopology {
737 TwoToTwo(GeneratedTwoToTwoReaction),
739}
740
741impl GeneratedReactionTopology {
742 fn p4_labels(&self) -> Vec<String> {
743 match self {
744 Self::TwoToTwo(reaction) => reaction.p4_labels(),
745 }
746 }
747
748 fn particle_layouts(&self) -> Vec<GeneratedParticleLayout> {
749 match self {
750 Self::TwoToTwo(reaction) => reaction.particle_layouts(),
751 }
752 }
753
754 fn particle_layouts_with_storage(
755 &self,
756 storage: &GeneratedStorage,
757 ) -> Vec<GeneratedParticleLayout> {
758 match self {
759 Self::TwoToTwo(reaction) => reaction.particle_layouts_with_storage(storage),
760 }
761 }
762
763 fn vertex_layouts(&self) -> Vec<GeneratedVertexLayout> {
764 match self {
765 Self::TwoToTwo(reaction) => reaction.vertex_layouts(),
766 }
767 }
768
769 fn reconstructed_reaction(&self) -> LadduResult<Reaction> {
770 match self {
771 Self::TwoToTwo(reaction) => reaction.reconstructed_reaction(),
772 }
773 }
774
775 fn generate_event(&self, rng: &mut Rng, p4_storage: &mut HashMap<String, Vec<Vec4>>) {
776 match self {
777 Self::TwoToTwo(reaction) => reaction.generate_event(rng, p4_storage),
778 }
779 }
780}
781
782#[derive(Clone, Debug)]
784pub struct GeneratedReaction {
785 topology: GeneratedReactionTopology,
786}
787
788impl GeneratedReaction {
789 pub fn two_to_two(
791 p1: GeneratedParticle,
792 p2: GeneratedParticle,
793 p3: GeneratedParticle,
794 p4: GeneratedParticle,
795 tdist: MandelstamTDistribution,
796 ) -> LadduResult<Self> {
797 Ok(Self {
798 topology: GeneratedReactionTopology::TwoToTwo(GeneratedTwoToTwoReaction::new(
799 p1, p2, p3, p4, tdist,
800 )?),
801 })
802 }
803
804 pub fn p4_labels(&self) -> Vec<String> {
806 self.topology.p4_labels()
807 }
808
809 pub fn particle_layouts(&self) -> Vec<GeneratedParticleLayout> {
811 self.topology.particle_layouts()
812 }
813
814 pub fn particle_layouts_with_storage(
816 &self,
817 storage: &GeneratedStorage,
818 ) -> Vec<GeneratedParticleLayout> {
819 self.topology.particle_layouts_with_storage(storage)
820 }
821
822 pub fn vertex_layouts(&self) -> Vec<GeneratedVertexLayout> {
824 self.topology.vertex_layouts()
825 }
826
827 pub fn reconstructed_reaction(&self) -> LadduResult<Reaction> {
829 self.topology.reconstructed_reaction()
830 }
831
832 fn generate(
833 &self,
834 rng: &mut Rng,
835 p4_storage: &mut HashMap<String, Vec<Vec4>>,
836 n_events: usize,
837 ) {
838 for _ in 0..n_events {
839 self.topology.generate_event(rng, p4_storage);
840 }
841 }
842}
843
844#[derive(Clone, Debug, PartialEq, Eq)]
846pub struct GeneratedParticleLayout {
847 id: String,
848 product_id: usize,
849 parent_id: Option<usize>,
850 species: Option<ParticleSpecies>,
851 p4_label: Option<String>,
852 produced_vertex_id: Option<usize>,
853 decay_vertex_id: Option<usize>,
854}
855
856impl GeneratedParticleLayout {
857 pub fn id(&self) -> &str {
859 &self.id
860 }
861
862 pub fn product_id(&self) -> usize {
864 self.product_id
865 }
866
867 pub fn parent_id(&self) -> Option<usize> {
869 self.parent_id
870 }
871
872 pub fn species(&self) -> Option<&ParticleSpecies> {
874 self.species.as_ref()
875 }
876
877 pub fn p4_label(&self) -> Option<&str> {
879 self.p4_label.as_deref()
880 }
881
882 pub fn produced_vertex_id(&self) -> Option<usize> {
884 self.produced_vertex_id
885 }
886
887 pub fn decay_vertex_id(&self) -> Option<usize> {
889 self.decay_vertex_id
890 }
891}
892
893#[derive(Clone, Copy, Debug, PartialEq, Eq)]
895pub enum GeneratedVertexKind {
896 Production,
898 Decay,
900}
901
902#[derive(Clone, Debug, PartialEq, Eq)]
904pub struct GeneratedVertexLayout {
905 vertex_id: usize,
906 kind: GeneratedVertexKind,
907 incoming_product_ids: Vec<usize>,
908 outgoing_product_ids: Vec<usize>,
909}
910
911impl GeneratedVertexLayout {
912 pub fn vertex_id(&self) -> usize {
914 self.vertex_id
915 }
916
917 pub fn kind(&self) -> GeneratedVertexKind {
919 self.kind
920 }
921
922 pub fn incoming_product_ids(&self) -> &[usize] {
924 &self.incoming_product_ids
925 }
926
927 pub fn outgoing_product_ids(&self) -> &[usize] {
929 &self.outgoing_product_ids
930 }
931}
932
933#[derive(Clone, Debug, PartialEq, Eq)]
935pub struct GeneratedEventLayout {
936 p4_labels: Vec<String>,
937 aux_labels: Vec<String>,
938 particles: Vec<GeneratedParticleLayout>,
939 vertices: Vec<GeneratedVertexLayout>,
940}
941
942impl GeneratedEventLayout {
943 pub fn new(
945 p4_labels: Vec<String>,
946 aux_labels: Vec<String>,
947 particles: Vec<GeneratedParticleLayout>,
948 vertices: Vec<GeneratedVertexLayout>,
949 ) -> Self {
950 Self {
951 p4_labels,
952 aux_labels,
953 particles,
954 vertices,
955 }
956 }
957
958 pub fn p4_labels(&self) -> &[String] {
960 &self.p4_labels
961 }
962
963 pub fn aux_labels(&self) -> &[String] {
965 &self.aux_labels
966 }
967
968 pub fn particles(&self) -> &[GeneratedParticleLayout] {
970 &self.particles
971 }
972
973 pub fn particle(&self, id: &str) -> Option<&GeneratedParticleLayout> {
975 self.particles.iter().find(|particle| particle.id() == id)
976 }
977
978 pub fn product(&self, product_id: usize) -> Option<&GeneratedParticleLayout> {
980 self.particles
981 .iter()
982 .find(|particle| particle.product_id() == product_id)
983 }
984
985 pub fn vertices(&self) -> &[GeneratedVertexLayout] {
987 &self.vertices
988 }
989
990 pub fn vertex(&self, vertex_id: usize) -> Option<&GeneratedVertexLayout> {
992 self.vertices
993 .iter()
994 .find(|vertex| vertex.vertex_id() == vertex_id)
995 }
996
997 pub fn production_vertex(&self) -> Option<&GeneratedVertexLayout> {
999 self.vertices
1000 .iter()
1001 .find(|vertex| vertex.kind() == GeneratedVertexKind::Production)
1002 }
1003
1004 pub fn decay_products(&self, parent_product_id: usize) -> Vec<&GeneratedParticleLayout> {
1006 self.particles
1007 .iter()
1008 .filter(|particle| particle.parent_id() == Some(parent_product_id))
1009 .collect()
1010 }
1011
1012 pub fn production_incoming(&self) -> Vec<&GeneratedParticleLayout> {
1014 self.production_vertex_products(GeneratedVertexLayout::incoming_product_ids)
1015 }
1016
1017 pub fn production_outgoing(&self) -> Vec<&GeneratedParticleLayout> {
1019 self.production_vertex_products(GeneratedVertexLayout::outgoing_product_ids)
1020 }
1021
1022 fn production_vertex_products(
1023 &self,
1024 ids: impl FnOnce(&GeneratedVertexLayout) -> &[usize],
1025 ) -> Vec<&GeneratedParticleLayout> {
1026 self.production_vertex()
1027 .map(|vertex| {
1028 ids(vertex)
1029 .iter()
1030 .filter_map(|product_id| self.product(*product_id))
1031 .collect()
1032 })
1033 .unwrap_or_default()
1034 }
1035}
1036
1037#[derive(Clone, Debug)]
1039pub struct GeneratedBatch {
1040 dataset: Dataset,
1041 reaction: GeneratedReaction,
1042 layout: GeneratedEventLayout,
1043}
1044
1045impl GeneratedBatch {
1046 pub fn new(
1048 dataset: Dataset,
1049 reaction: GeneratedReaction,
1050 layout: GeneratedEventLayout,
1051 ) -> Self {
1052 Self {
1053 dataset,
1054 reaction,
1055 layout,
1056 }
1057 }
1058
1059 pub fn dataset(&self) -> &Dataset {
1061 &self.dataset
1062 }
1063
1064 pub fn into_dataset(self) -> Dataset {
1066 self.dataset
1067 }
1068
1069 pub fn reaction(&self) -> &GeneratedReaction {
1071 &self.reaction
1072 }
1073
1074 pub fn layout(&self) -> &GeneratedEventLayout {
1076 &self.layout
1077 }
1078}
1079
1080#[derive(Clone, Debug)]
1082pub struct EventGenerator {
1083 reaction: GeneratedReaction,
1084 aux_generators: HashMap<String, Distribution>,
1085 storage: GeneratedStorage,
1086 seed: u64,
1087}
1088
1089#[derive(Clone, Debug)]
1091pub struct GeneratedBatchIter {
1092 generator: EventGenerator,
1093 remaining_events: usize,
1094 batch_size: usize,
1095 rng: Rng,
1096}
1097
1098impl Iterator for GeneratedBatchIter {
1099 type Item = LadduResult<GeneratedBatch>;
1100
1101 fn next(&mut self) -> Option<Self::Item> {
1102 if self.remaining_events == 0 {
1103 return None;
1104 }
1105 let n_events = self.batch_size.min(self.remaining_events);
1106 self.remaining_events -= n_events;
1107 Some(
1108 self.generator
1109 .generate_batch_with_rng(n_events, &mut self.rng),
1110 )
1111 }
1112}
1113
1114pub trait BatchIntensity {
1116 fn evaluate(&self, batch: &GeneratedBatch) -> LadduResult<Vec<f64>>;
1118
1119 fn evaluate_checked(&self, batch: &GeneratedBatch) -> LadduResult<Vec<f64>> {
1121 let intensities = self.evaluate(batch)?;
1122 if intensities.len() != batch.dataset().n_events() {
1123 return Err(LadduError::Custom(format!(
1124 "intensity length mismatch: expected {}, got {}",
1125 batch.dataset().n_events(),
1126 intensities.len()
1127 )));
1128 }
1129 for (index, weight) in intensities.iter().enumerate() {
1130 if !weight.is_finite() || *weight < 0.0 {
1131 return Err(LadduError::Custom(format!(
1132 "intensity at event {index} must be finite and nonnegative, got {weight}"
1133 )));
1134 }
1135 }
1136 Ok(intensities)
1137 }
1138}
1139
1140impl<F> BatchIntensity for F
1141where
1142 F: Fn(&GeneratedBatch) -> LadduResult<Vec<f64>>,
1143{
1144 fn evaluate(&self, batch: &GeneratedBatch) -> LadduResult<Vec<f64>> {
1145 self(batch)
1146 }
1147}
1148
1149#[derive(Clone, Debug)]
1151pub struct ExpressionIntensity {
1152 expression: Expression,
1153 parameters: Vec<f64>,
1154}
1155
1156impl ExpressionIntensity {
1157 pub fn new(expression: Expression, parameters: Vec<f64>) -> Self {
1159 Self {
1160 expression,
1161 parameters,
1162 }
1163 }
1164}
1165
1166impl BatchIntensity for ExpressionIntensity {
1167 fn evaluate(&self, batch: &GeneratedBatch) -> LadduResult<Vec<f64>> {
1168 let dataset = Arc::new(batch.dataset().clone());
1169 let evaluator = self.expression.load(&dataset)?;
1170 evaluator
1171 .evaluate(&self.parameters)?
1172 .into_iter()
1173 .enumerate()
1174 .map(|(index, value)| {
1175 if !value.im.is_finite() || value.im.abs() > f64::EPSILON {
1176 return Err(LadduError::Custom(format!(
1177 "expression intensity at event {index} must be real-valued, got {value}"
1178 )));
1179 }
1180 Ok(value.re)
1181 })
1182 .collect()
1183 }
1184}
1185
1186#[derive(Clone, Debug)]
1188pub enum RejectionEnvelope {
1189 Fixed {
1191 max_weight: f64,
1193 },
1194 Pilot {
1196 pilot_events: usize,
1198 batch_size: Option<usize>,
1200 safety_factor: f64,
1202 },
1203}
1204
1205#[derive(Clone, Debug)]
1207pub struct RejectionSamplingOptions {
1208 pub target_accepted: usize,
1210 pub generation_batch_size: usize,
1212 pub output_batch_size: usize,
1214 pub envelope: RejectionEnvelope,
1216 pub seed: u64,
1218}
1219
1220#[derive(Clone, Debug, Default)]
1222pub struct RejectionSamplingDiagnostics {
1223 pub generated_events: usize,
1225 pub accepted_events: usize,
1227 pub rejected_events: usize,
1229 pub max_observed_weight: f64,
1231 pub envelope_max_weight: f64,
1233 pub envelope_violations: usize,
1235}
1236
1237impl RejectionSamplingDiagnostics {
1238 pub fn acceptance_efficiency(&self) -> f64 {
1240 if self.generated_events == 0 {
1241 0.0
1242 } else {
1243 self.accepted_events as f64 / self.generated_events as f64
1244 }
1245 }
1246}
1247
1248#[derive(Clone, Debug)]
1250pub struct RejectionSampler<I> {
1251 generator: EventGenerator,
1252 intensity: I,
1253 max_weight: f64,
1254 options: RejectionSamplingOptions,
1255}
1256
1257impl<I> RejectionSampler<I>
1258where
1259 I: BatchIntensity,
1260{
1261 pub fn new(
1263 generator: EventGenerator,
1264 intensity: I,
1265 options: RejectionSamplingOptions,
1266 ) -> LadduResult<Self> {
1267 if options.generation_batch_size == 0 {
1268 return Err(LadduError::Custom(
1269 "generation_batch_size must be greater than zero".to_string(),
1270 ));
1271 }
1272 if options.output_batch_size == 0 {
1273 return Err(LadduError::Custom(
1274 "output_batch_size must be greater than zero".to_string(),
1275 ));
1276 }
1277 let max_weight = match options.envelope {
1278 RejectionEnvelope::Fixed { max_weight } => max_weight,
1279 RejectionEnvelope::Pilot {
1280 pilot_events,
1281 batch_size,
1282 safety_factor,
1283 } => estimate_rejection_envelope(
1284 &generator,
1285 &intensity,
1286 pilot_events,
1287 batch_size.unwrap_or(options.generation_batch_size),
1288 safety_factor,
1289 )?,
1290 };
1291 if !max_weight.is_finite() || max_weight <= 0.0 {
1292 return Err(LadduError::Custom(
1293 "rejection envelope max_weight must be finite and positive".to_string(),
1294 ));
1295 }
1296 Ok(Self {
1297 generator,
1298 intensity,
1299 max_weight,
1300 options,
1301 })
1302 }
1303
1304 pub fn accepted_batches(self) -> RejectionSampleIter<I> {
1306 RejectionSampleIter {
1307 generation_rng: Rng::with_seed(self.generator.seed),
1308 rejection_rng: Rng::with_seed(self.options.seed),
1309 diagnostics: RejectionSamplingDiagnostics {
1310 envelope_max_weight: self.max_weight,
1311 ..Default::default()
1312 },
1313 sampler: self,
1314 current_batch: None,
1315 current_intensities: Vec::new(),
1316 current_index: 0,
1317 }
1318 }
1319}
1320
1321fn estimate_rejection_envelope<I>(
1322 generator: &EventGenerator,
1323 intensity: &I,
1324 pilot_events: usize,
1325 batch_size: usize,
1326 safety_factor: f64,
1327) -> LadduResult<f64>
1328where
1329 I: BatchIntensity,
1330{
1331 if pilot_events == 0 {
1332 return Err(LadduError::Custom(
1333 "pilot_events must be greater than zero".to_string(),
1334 ));
1335 }
1336 if batch_size == 0 {
1337 return Err(LadduError::Custom(
1338 "pilot batch_size must be greater than zero".to_string(),
1339 ));
1340 }
1341 if !safety_factor.is_finite() || safety_factor <= 0.0 {
1342 return Err(LadduError::Custom(
1343 "pilot safety_factor must be finite and positive".to_string(),
1344 ));
1345 }
1346
1347 let mut max_observed = 0.0_f64;
1348 let mut rng = Rng::with_seed(generator.seed);
1349 let mut remaining = pilot_events;
1350 while remaining > 0 {
1351 let n_events = remaining.min(batch_size);
1352 let batch = generator.generate_batch_with_rng(n_events, &mut rng)?;
1353 let weights = intensity.evaluate_checked(&batch)?;
1354 for weight in weights {
1355 max_observed = max_observed.max(weight);
1356 }
1357 remaining -= n_events;
1358 }
1359
1360 let max_weight = max_observed * safety_factor;
1361 if !max_weight.is_finite() || max_weight <= 0.0 {
1362 return Err(LadduError::Custom(format!(
1363 "pilot envelope produced invalid max_weight {max_weight}; observed maximum was {max_observed}"
1364 )));
1365 }
1366 Ok(max_weight)
1367}
1368
1369#[derive(Clone, Debug)]
1371pub struct RejectionSampleIter<I> {
1372 sampler: RejectionSampler<I>,
1373 generation_rng: Rng,
1374 rejection_rng: Rng,
1375 diagnostics: RejectionSamplingDiagnostics,
1376 current_batch: Option<GeneratedBatch>,
1377 current_intensities: Vec<f64>,
1378 current_index: usize,
1379}
1380
1381impl<I> RejectionSampleIter<I> {
1382 pub fn diagnostics(&self) -> &RejectionSamplingDiagnostics {
1384 &self.diagnostics
1385 }
1386}
1387
1388impl<I> RejectionSampleIter<I>
1389where
1390 I: BatchIntensity,
1391{
1392 fn load_next_source_batch(&mut self) -> LadduResult<()> {
1393 let batch = self.sampler.generator.generate_batch_with_rng(
1394 self.sampler.options.generation_batch_size,
1395 &mut self.generation_rng,
1396 )?;
1397 let intensities = self.sampler.intensity.evaluate_checked(&batch)?;
1398 self.diagnostics.generated_events += batch.dataset().n_events();
1399 self.current_batch = Some(batch);
1400 self.current_intensities = intensities;
1401 self.current_index = 0;
1402 Ok(())
1403 }
1404
1405 fn empty_output_batch(source: &GeneratedBatch) -> GeneratedBatch {
1406 GeneratedBatch::new(
1407 Dataset::empty_local(source.dataset().metadata().clone()),
1408 source.reaction().clone(),
1409 source.layout().clone(),
1410 )
1411 }
1412}
1413
1414impl<I> Iterator for RejectionSampleIter<I>
1415where
1416 I: BatchIntensity,
1417{
1418 type Item = LadduResult<GeneratedBatch>;
1419
1420 fn next(&mut self) -> Option<Self::Item> {
1421 if self.diagnostics.accepted_events >= self.sampler.options.target_accepted {
1422 return None;
1423 }
1424
1425 let mut output: Option<GeneratedBatch> = None;
1426 while self.diagnostics.accepted_events < self.sampler.options.target_accepted {
1427 let needs_batch = self
1428 .current_batch
1429 .as_ref()
1430 .map(|batch| self.current_index >= batch.dataset().n_events())
1431 .unwrap_or(true);
1432 if needs_batch {
1433 if let Err(err) = self.load_next_source_batch() {
1434 return Some(Err(err));
1435 }
1436 }
1437
1438 let source = self
1439 .current_batch
1440 .as_ref()
1441 .expect("source batch should be loaded");
1442 if output.is_none() {
1443 output = Some(Self::empty_output_batch(source));
1444 }
1445
1446 let weight = self.current_intensities[self.current_index];
1447 self.diagnostics.max_observed_weight = self.diagnostics.max_observed_weight.max(weight);
1448 let envelope_max = self.sampler.max_weight;
1449 if weight > envelope_max {
1450 self.diagnostics.envelope_violations += 1;
1451 return Some(Err(LadduError::Custom(format!(
1452 "rejection envelope violation: observed weight {weight} exceeds max_weight {envelope_max}"
1453 ))));
1454 }
1455
1456 let accepted = self.rejection_rng.f64() * envelope_max < weight;
1457 if accepted {
1458 let event = match source.dataset().event_global(self.current_index) {
1459 Ok(event) => event,
1460 Err(err) => return Some(Err(err)),
1461 };
1462 if let Err(err) = output.as_mut().unwrap().dataset.push_event_local(
1463 event.p4s.clone(),
1464 event.aux.clone(),
1465 event.weight,
1466 ) {
1467 return Some(Err(err));
1468 }
1469 self.diagnostics.accepted_events += 1;
1470 } else {
1471 self.diagnostics.rejected_events += 1;
1472 }
1473 self.current_index += 1;
1474
1475 if output.as_ref().unwrap().dataset().n_events()
1476 >= self.sampler.options.output_batch_size
1477 || self.diagnostics.accepted_events >= self.sampler.options.target_accepted
1478 {
1479 break;
1480 }
1481 }
1482
1483 output
1484 .filter(|batch| batch.dataset().n_events() > 0)
1485 .map(Ok)
1486 }
1487}
1488
1489impl EventGenerator {
1490 pub fn new(
1492 reaction: GeneratedReaction,
1493 aux_generators: HashMap<String, Distribution>,
1494 seed: Option<u64>,
1495 ) -> Self {
1496 Self {
1497 reaction,
1498 aux_generators,
1499 storage: GeneratedStorage::All,
1500 seed: seed.unwrap_or_else(|| fastrand::u64(..)),
1501 }
1502 }
1503
1504 pub fn storage(&self) -> &GeneratedStorage {
1506 &self.storage
1507 }
1508
1509 pub fn with_storage(mut self, storage: GeneratedStorage) -> LadduResult<Self> {
1511 storage.validate(&self.reaction.p4_labels())?;
1512 self.storage = storage;
1513 Ok(self)
1514 }
1515
1516 fn aux_entries(&self) -> Vec<(&String, &Distribution)> {
1517 let mut aux_entries = self.aux_generators.iter().collect::<Vec<_>>();
1518 aux_entries.sort_by_key(|(label, _)| *label);
1519 aux_entries
1520 }
1521
1522 fn generate_batch_with_rng(
1523 &self,
1524 n_events: usize,
1525 rng: &mut Rng,
1526 ) -> LadduResult<GeneratedBatch> {
1527 let all_p4_labels = self.reaction.p4_labels();
1528 self.storage.validate(&all_p4_labels)?;
1529 let p4_labels = self.storage.stored_labels(&all_p4_labels);
1530 let aux_entries = self.aux_entries();
1531 let aux_labels = aux_entries
1532 .iter()
1533 .map(|(label, _)| (*label).clone())
1534 .collect::<Vec<_>>();
1535 let mut p4_data: HashMap<String, Vec<Vec4>> = p4_labels
1536 .iter()
1537 .map(|label| (label.clone(), Vec::with_capacity(n_events)))
1538 .collect();
1539 let metadata = DatasetMetadata::new(p4_labels.clone(), aux_labels.clone())?;
1540 let mut aux: Vec<Vec<f64>> = aux_entries
1541 .iter()
1542 .map(|_| Vec::with_capacity(n_events))
1543 .collect();
1544 let weights = vec![1.0; n_events];
1545 for _ in 0..n_events {
1546 for ((_, distribution), column) in aux_entries.iter().zip(aux.iter_mut()) {
1547 column.push(distribution.sample(rng));
1548 }
1549 self.reaction.generate(rng, &mut p4_data, 1);
1550 }
1551 let p4 = p4_labels
1552 .iter()
1553 .filter_map(|label| p4_data.remove(label))
1554 .collect();
1555 let dataset = Dataset::from_columns_local(metadata, p4, aux, weights)?;
1556 Ok(GeneratedBatch::new(
1557 dataset,
1558 self.reaction.clone(),
1559 GeneratedEventLayout::new(
1560 p4_labels,
1561 aux_labels,
1562 self.reaction.particle_layouts_with_storage(&self.storage),
1563 self.reaction.vertex_layouts(),
1564 ),
1565 ))
1566 }
1567
1568 pub fn generate_batch(&self, n_events: usize) -> LadduResult<GeneratedBatch> {
1570 let mut rng = Rng::with_seed(self.seed);
1571 self.generate_batch_with_rng(n_events, &mut rng)
1572 }
1573
1574 pub fn generate_batches(
1579 &self,
1580 total_events: usize,
1581 batch_size: usize,
1582 ) -> LadduResult<GeneratedBatchIter> {
1583 if batch_size == 0 {
1584 return Err(LadduError::Custom(
1585 "batch_size must be greater than zero".to_string(),
1586 ));
1587 }
1588 Ok(GeneratedBatchIter {
1589 generator: self.clone(),
1590 remaining_events: total_events,
1591 batch_size,
1592 rng: Rng::with_seed(self.seed),
1593 })
1594 }
1595
1596 pub fn generate_dataset(&self, n_events: usize) -> LadduResult<Dataset> {
1598 Ok(self.generate_batch(n_events)?.into_dataset())
1599 }
1600
1601 pub fn rejection_sampler_with_expression(
1603 &self,
1604 expression: Expression,
1605 parameters: Vec<f64>,
1606 options: RejectionSamplingOptions,
1607 ) -> LadduResult<RejectionSampler<ExpressionIntensity>> {
1608 RejectionSampler::new(
1609 self.clone(),
1610 ExpressionIntensity::new(expression, parameters),
1611 options,
1612 )
1613 }
1614}
1615
1616#[cfg(test)]
1617mod tests {
1618 use approx::assert_relative_eq;
1619 use laddu_core::{traits::Variable, Channel, Expression, Frame};
1620
1621 use super::*;
1622
1623 fn demo_reaction() -> GeneratedReaction {
1624 let beam = GeneratedParticle::initial(
1625 "beam",
1626 InitialGenerator::beam_with_fixed_energy(0.0, 8.0),
1627 Reconstruction::Stored,
1628 );
1629 let target = GeneratedParticle::initial(
1630 "target",
1631 InitialGenerator::target(0.938272),
1632 Reconstruction::Missing,
1633 );
1634 let ks1 = GeneratedParticle::stable(
1635 "kshort1",
1636 StableGenerator::new(0.497611),
1637 Reconstruction::Stored,
1638 );
1639 let ks2 = GeneratedParticle::stable(
1640 "kshort2",
1641 StableGenerator::new(0.497611),
1642 Reconstruction::Stored,
1643 );
1644 let kk = GeneratedParticle::composite(
1645 "kk",
1646 CompositeGenerator::new(1.1, 1.6),
1647 (&ks1, &ks2),
1648 Reconstruction::Composite,
1649 );
1650 let recoil = GeneratedParticle::stable(
1651 "recoil",
1652 StableGenerator::new(0.938272),
1653 Reconstruction::Stored,
1654 );
1655 let tdist = MandelstamTDistribution::Exponential { slope: 0.1 };
1656 GeneratedReaction::two_to_two(beam, target, kk, recoil, tdist).unwrap()
1657 }
1658
1659 #[test]
1660 fn test_generation() {
1661 let reaction = demo_reaction();
1662 let generator = EventGenerator::new(reaction, HashMap::new(), Some(12345));
1663 let n_events = 1_000;
1664 let dataset = generator.generate_dataset(n_events).unwrap();
1665 assert_eq!(dataset.n_events(), n_events);
1666 let metadata = dataset.metadata();
1667 assert!(metadata.p4_index("beam").is_some());
1668 assert!(metadata.p4_index("target").is_some());
1669 assert!(metadata.p4_index("kk").is_some());
1670 assert!(metadata.p4_index("kshort1").is_some());
1671 assert!(metadata.p4_index("kshort2").is_some());
1672 assert!(metadata.p4_index("recoil").is_some());
1673
1674 for event in dataset.events_global() {
1675 let beam_p4 = event.p4("beam").unwrap();
1676 let target_p4 = event.p4("target").unwrap();
1677 let kk_p4 = event.p4("kk").unwrap();
1678 let kshort1_p4 = event.p4("kshort1").unwrap();
1679 let kshort2_p4 = event.p4("kshort2").unwrap();
1680 let recoil_p4 = event.p4("recoil").unwrap();
1681
1682 assert!(beam_p4.e().is_finite());
1683 assert!(target_p4.e().is_finite());
1684 assert!(kk_p4.e().is_finite());
1685 assert!(kshort1_p4.e().is_finite());
1686 assert!(kshort2_p4.e().is_finite());
1687 assert!(recoil_p4.e().is_finite());
1688
1689 assert_relative_eq!(kk_p4, kshort1_p4 + kshort2_p4, epsilon = 1e-10);
1690 assert_relative_eq!(beam_p4 + target_p4, kk_p4 + recoil_p4, epsilon = 1e-10);
1691 assert_relative_eq!(kshort1_p4.m(), 0.497611, epsilon = 1e-10);
1692 assert_relative_eq!(kshort2_p4.m(), 0.497611, epsilon = 1e-10);
1693 assert_relative_eq!(recoil_p4.m(), 0.938272, epsilon = 1e-10);
1694 }
1695 }
1696
1697 #[test]
1698 fn test_reconstructed_reaction() {
1699 let generated = demo_reaction();
1700 let reaction = generated.reconstructed_reaction().unwrap();
1701 let dataset = EventGenerator::new(generated, HashMap::new(), Some(12345))
1702 .generate_dataset(4)
1703 .unwrap();
1704 let mass = reaction.mass("kk").value_on(&dataset).unwrap();
1705 let angles = reaction
1706 .decay("kk")
1707 .unwrap()
1708 .angles("kshort1", Frame::Helicity)
1709 .unwrap();
1710 let mandelstam = reaction
1711 .mandelstam(Channel::S)
1712 .unwrap()
1713 .value_on(&dataset)
1714 .unwrap();
1715
1716 assert_eq!(mass.len(), 4);
1717 assert_eq!(
1718 angles.costheta.to_string(),
1719 "CosTheta(parent=kk, daughter=kshort1, frame=Helicity)"
1720 );
1721 assert_eq!(mandelstam.len(), 4);
1722 }
1723
1724 #[test]
1725 fn test_generated_batch_metadata() {
1726 let generated = demo_reaction();
1727 let generator = EventGenerator::new(
1728 generated,
1729 HashMap::from([("pol_angle".to_string(), Distribution::Fixed(0.25))]),
1730 Some(12345),
1731 );
1732 let batch = generator.generate_batch(4).unwrap();
1733
1734 assert_eq!(batch.dataset().n_events(), 4);
1735 assert_eq!(
1736 batch.layout().p4_labels(),
1737 &["beam", "target", "kk", "kshort1", "kshort2", "recoil"]
1738 );
1739 assert_eq!(batch.layout().aux_labels(), &["pol_angle"]);
1740 assert_eq!(
1741 batch.reaction().p4_labels(),
1742 vec!["beam", "target", "kk", "kshort1", "kshort2", "recoil"]
1743 );
1744 assert_eq!(batch.dataset().p4_names(), batch.layout().p4_labels());
1745 assert_eq!(batch.dataset().aux_names(), batch.layout().aux_labels());
1746 let particles = batch.layout().particles();
1747 assert_eq!(particles.len(), 6);
1748 assert_eq!(particles[0].id(), "beam");
1749 assert_eq!(particles[0].product_id(), 0);
1750 assert_eq!(particles[0].parent_id(), None);
1751 assert_eq!(particles[0].produced_vertex_id(), None);
1752 assert_eq!(particles[0].decay_vertex_id(), None);
1753 assert_eq!(particles[1].id(), "target");
1754 assert_eq!(particles[1].parent_id(), None);
1755 assert_eq!(particles[1].produced_vertex_id(), None);
1756 assert_eq!(particles[1].decay_vertex_id(), None);
1757 assert_eq!(particles[2].id(), "kk");
1758 assert_eq!(particles[2].product_id(), 2);
1759 assert_eq!(particles[2].parent_id(), None);
1760 assert_eq!(particles[2].produced_vertex_id(), Some(0));
1761 assert_eq!(particles[2].decay_vertex_id(), Some(1));
1762 assert_eq!(particles[3].id(), "kshort1");
1763 assert_eq!(particles[3].parent_id(), Some(2));
1764 assert_eq!(particles[3].produced_vertex_id(), Some(1));
1765 assert_eq!(particles[3].decay_vertex_id(), None);
1766 assert_eq!(particles[4].id(), "kshort2");
1767 assert_eq!(particles[4].parent_id(), Some(2));
1768 assert_eq!(particles[4].produced_vertex_id(), Some(1));
1769 assert_eq!(particles[4].decay_vertex_id(), None);
1770 assert_eq!(particles[5].id(), "recoil");
1771 assert_eq!(particles[5].parent_id(), None);
1772 assert_eq!(particles[5].produced_vertex_id(), Some(0));
1773 assert_eq!(particles[5].decay_vertex_id(), None);
1774 for particle in particles {
1775 assert_eq!(particle.p4_label(), Some(particle.id()));
1776 }
1777 let vertices = batch.layout().vertices();
1778 assert_eq!(vertices.len(), 2);
1779 assert_eq!(vertices[0].vertex_id(), 0);
1780 assert_eq!(vertices[0].kind(), GeneratedVertexKind::Production);
1781 assert_eq!(vertices[0].incoming_product_ids(), &[0, 1]);
1782 assert_eq!(vertices[0].outgoing_product_ids(), &[2, 5]);
1783 assert_eq!(vertices[1].vertex_id(), 1);
1784 assert_eq!(vertices[1].kind(), GeneratedVertexKind::Decay);
1785 assert_eq!(vertices[1].incoming_product_ids(), &[2]);
1786 assert_eq!(vertices[1].outgoing_product_ids(), &[3, 4]);
1787
1788 assert_eq!(batch.layout().particle("kk"), Some(&particles[2]));
1789 assert_eq!(batch.layout().particle("missing_id"), None);
1790 assert_eq!(batch.layout().product(5), Some(&particles[5]));
1791 assert_eq!(batch.layout().product(6), None);
1792 assert_eq!(batch.layout().vertex(1), Some(&vertices[1]));
1793 assert_eq!(batch.layout().vertex(2), None);
1794 assert_eq!(batch.layout().production_vertex(), Some(&vertices[0]));
1795 assert_eq!(
1796 batch
1797 .layout()
1798 .production_incoming()
1799 .iter()
1800 .map(|particle| particle.id())
1801 .collect::<Vec<_>>(),
1802 vec!["beam", "target"]
1803 );
1804 assert_eq!(
1805 batch
1806 .layout()
1807 .production_outgoing()
1808 .iter()
1809 .map(|particle| particle.id())
1810 .collect::<Vec<_>>(),
1811 vec!["kk", "recoil"]
1812 );
1813 assert_eq!(
1814 batch
1815 .layout()
1816 .decay_products(2)
1817 .iter()
1818 .map(|particle| particle.id())
1819 .collect::<Vec<_>>(),
1820 vec!["kshort1", "kshort2"]
1821 );
1822 assert!(batch.layout().decay_products(5).is_empty());
1823 }
1824
1825 #[test]
1826 fn generated_storage_only_projects_dataset_columns() {
1827 let generated = demo_reaction();
1828 let generator = EventGenerator::new(generated, HashMap::new(), Some(12345))
1829 .with_storage(GeneratedStorage::only([
1830 "beam", "target", "kshort1", "kshort2", "recoil",
1831 ]))
1832 .unwrap();
1833 let batch = generator.generate_batch(4).unwrap();
1834
1835 assert_eq!(
1836 batch.reaction().p4_labels(),
1837 vec!["beam", "target", "kk", "kshort1", "kshort2", "recoil"]
1838 );
1839 assert_eq!(
1840 batch.layout().p4_labels(),
1841 &["beam", "target", "kshort1", "kshort2", "recoil"]
1842 );
1843 assert_eq!(batch.dataset().p4_names(), batch.layout().p4_labels());
1844 assert!(batch.dataset().metadata().p4_index("kk").is_none());
1845
1846 let particles = batch.layout().particles();
1847 assert_eq!(particles.len(), 6);
1848 assert_eq!(particles[2].id(), "kk");
1849 assert_eq!(particles[2].p4_label(), None);
1850 assert_eq!(particles[3].p4_label(), Some("kshort1"));
1851 assert_eq!(particles[4].p4_label(), Some("kshort2"));
1852
1853 for index in 0..batch.dataset().n_events() {
1854 let event = batch.dataset().event_global(index).unwrap();
1855 assert_relative_eq!(
1856 event.p4("beam").unwrap() + event.p4("target").unwrap(),
1857 event.p4("kshort1").unwrap()
1858 + event.p4("kshort2").unwrap()
1859 + event.p4("recoil").unwrap(),
1860 epsilon = 1e-10
1861 );
1862 }
1863 }
1864
1865 #[test]
1866 fn generated_storage_rejects_unknown_and_duplicate_ids() {
1867 assert!(
1868 EventGenerator::new(demo_reaction(), HashMap::new(), Some(12345))
1869 .with_storage(GeneratedStorage::only(["beam", "does_not_exist"]))
1870 .is_err()
1871 );
1872 assert!(
1873 EventGenerator::new(demo_reaction(), HashMap::new(), Some(12345))
1874 .with_storage(GeneratedStorage::only(["beam", "beam"]))
1875 .is_err()
1876 );
1877 }
1878
1879 #[test]
1880 fn generated_species_metadata_propagates_to_layout() {
1881 let beam = GeneratedParticle::initial(
1882 "beam",
1883 InitialGenerator::beam_with_fixed_energy(0.0, 8.0),
1884 Reconstruction::Stored,
1885 )
1886 .with_species(ParticleSpecies::code(22));
1887 let target = GeneratedParticle::initial(
1888 "target",
1889 InitialGenerator::target(0.938272),
1890 Reconstruction::Missing,
1891 )
1892 .with_species(ParticleSpecies::with_namespace("pdg", 2212));
1893 let kshort1 = GeneratedParticle::stable(
1894 "kshort1",
1895 StableGenerator::new(0.497611),
1896 Reconstruction::Stored,
1897 )
1898 .with_species(ParticleSpecies::label("KShort"));
1899 let kshort2 = GeneratedParticle::stable(
1900 "kshort2",
1901 StableGenerator::new(0.497611),
1902 Reconstruction::Stored,
1903 )
1904 .with_species(ParticleSpecies::label("KShort"));
1905 let kk = GeneratedParticle::composite(
1906 "kk",
1907 CompositeGenerator::new(1.1, 1.6),
1908 (&kshort1, &kshort2),
1909 Reconstruction::Composite,
1910 )
1911 .with_species(ParticleSpecies::label("KK"));
1912 let recoil = GeneratedParticle::stable(
1913 "recoil",
1914 StableGenerator::new(0.938272),
1915 Reconstruction::Stored,
1916 )
1917 .with_species(ParticleSpecies::code(2212));
1918 let reaction = GeneratedReaction::two_to_two(
1919 beam,
1920 target,
1921 kk,
1922 recoil,
1923 MandelstamTDistribution::Exponential { slope: 0.1 },
1924 )
1925 .unwrap();
1926 let particles = reaction.particle_layouts();
1927
1928 assert_eq!(particles[0].species(), Some(&ParticleSpecies::code(22)));
1929 assert_eq!(
1930 particles[1].species(),
1931 Some(&ParticleSpecies::with_namespace("pdg", 2212))
1932 );
1933 assert_eq!(particles[2].species(), Some(&ParticleSpecies::label("KK")));
1934 assert_eq!(
1935 particles[3].species(),
1936 Some(&ParticleSpecies::label("KShort"))
1937 );
1938 assert_eq!(
1939 particles[4].species(),
1940 Some(&ParticleSpecies::label("KShort"))
1941 );
1942 assert_eq!(particles[5].species(), Some(&ParticleSpecies::code(2212)));
1943 }
1944
1945 #[test]
1946 fn generated_batches_match_one_shot_generation() {
1947 let generated = demo_reaction();
1948 let generator = EventGenerator::new(
1949 generated,
1950 HashMap::from([(
1951 "pol_angle".to_string(),
1952 Distribution::Uniform { min: 0.0, max: 1.0 },
1953 )]),
1954 Some(12345),
1955 );
1956 let one_shot = generator.generate_dataset(7).unwrap();
1957 let batches = generator
1958 .generate_batches(7, 3)
1959 .unwrap()
1960 .collect::<LadduResult<Vec<_>>>()
1961 .unwrap();
1962 let batch_sizes = batches
1963 .iter()
1964 .map(|batch| batch.dataset().n_events())
1965 .collect::<Vec<_>>();
1966 assert_eq!(batch_sizes, vec![3, 3, 1]);
1967
1968 let mut offset = 0;
1969 for batch in batches {
1970 for local_index in 0..batch.dataset().n_events() {
1971 let expected = one_shot.event_global(offset + local_index).unwrap();
1972 let actual = batch.dataset().event_global(local_index).unwrap();
1973 for name in one_shot.p4_names() {
1974 assert_relative_eq!(
1975 actual.p4(name).unwrap(),
1976 expected.p4(name).unwrap(),
1977 epsilon = 1e-10
1978 );
1979 }
1980 for aux_index in 0..one_shot.aux_names().len() {
1981 assert_relative_eq!(actual.aux[aux_index], expected.aux[aux_index]);
1982 }
1983 assert_relative_eq!(actual.weight(), expected.weight());
1984 }
1985 offset += batch.dataset().n_events();
1986 }
1987 assert_eq!(offset, one_shot.n_events());
1988 assert!(generator.generate_batches(1, 0).is_err());
1989 }
1990
1991 #[test]
1992 fn fixed_envelope_rejection_sampler_streams_accepted_batches() {
1993 let generator = EventGenerator::new(demo_reaction(), HashMap::new(), Some(12345));
1994 let sampler = RejectionSampler::new(
1995 generator,
1996 |batch: &GeneratedBatch| Ok(vec![1.0; batch.dataset().n_events()]),
1997 RejectionSamplingOptions {
1998 target_accepted: 5,
1999 generation_batch_size: 4,
2000 output_batch_size: 2,
2001 envelope: RejectionEnvelope::Fixed { max_weight: 1.0 },
2002 seed: 67890,
2003 },
2004 )
2005 .unwrap();
2006
2007 let mut iter = sampler.accepted_batches();
2008 let mut accepted_batches = Vec::new();
2009 for batch in iter.by_ref() {
2010 accepted_batches.push(batch.unwrap());
2011 }
2012 assert_eq!(
2013 accepted_batches
2014 .iter()
2015 .map(|batch| batch.dataset().n_events())
2016 .collect::<Vec<_>>(),
2017 vec![2, 2, 1]
2018 );
2019 assert_eq!(iter.diagnostics().generated_events, 8);
2020 assert_eq!(iter.diagnostics().accepted_events, 5);
2021 assert_eq!(iter.diagnostics().rejected_events, 0);
2022 assert_relative_eq!(iter.diagnostics().acceptance_efficiency(), 5.0 / 8.0);
2023 for batch in accepted_batches {
2024 assert_eq!(
2025 batch.layout().p4_labels(),
2026 &["beam", "target", "kk", "kshort1", "kshort2", "recoil"]
2027 );
2028 }
2029 }
2030
2031 #[test]
2032 fn fixed_envelope_rejection_sampler_rejects_violations() {
2033 let generator = EventGenerator::new(demo_reaction(), HashMap::new(), Some(12345));
2034 let sampler = RejectionSampler::new(
2035 generator,
2036 |batch: &GeneratedBatch| Ok(vec![2.0; batch.dataset().n_events()]),
2037 RejectionSamplingOptions {
2038 target_accepted: 1,
2039 generation_batch_size: 1,
2040 output_batch_size: 1,
2041 envelope: RejectionEnvelope::Fixed { max_weight: 1.0 },
2042 seed: 67890,
2043 },
2044 )
2045 .unwrap();
2046
2047 let mut iter = sampler.accepted_batches();
2048 let err = iter.next().expect("sampler should produce an error");
2049 assert!(err.is_err());
2050 assert_eq!(iter.diagnostics().envelope_violations, 1);
2051 assert_relative_eq!(iter.diagnostics().max_observed_weight, 2.0);
2052 }
2053
2054 #[test]
2055 fn rejection_sampler_validates_custom_batch_intensities() {
2056 let generator = EventGenerator::new(demo_reaction(), HashMap::new(), Some(12345));
2057 let sampler = RejectionSampler::new(
2058 generator,
2059 |batch: &GeneratedBatch| Ok(vec![f64::NAN; batch.dataset().n_events()]),
2060 RejectionSamplingOptions {
2061 target_accepted: 1,
2062 generation_batch_size: 1,
2063 output_batch_size: 1,
2064 envelope: RejectionEnvelope::Fixed { max_weight: 1.0 },
2065 seed: 67890,
2066 },
2067 )
2068 .unwrap();
2069
2070 let err = sampler
2071 .accepted_batches()
2072 .next()
2073 .expect("sampler should produce an error");
2074 assert!(err.is_err());
2075 }
2076
2077 #[test]
2078 fn expression_rejection_sampler_streams_exact_target_count() {
2079 let generator = EventGenerator::new(demo_reaction(), HashMap::new(), Some(12345));
2080 let sampler = generator
2081 .rejection_sampler_with_expression(
2082 Expression::from(1.0),
2083 vec![],
2084 RejectionSamplingOptions {
2085 target_accepted: 5,
2086 generation_batch_size: 4,
2087 output_batch_size: 2,
2088 envelope: RejectionEnvelope::Fixed { max_weight: 1.0 },
2089 seed: 67890,
2090 },
2091 )
2092 .unwrap();
2093
2094 let batches = sampler
2095 .accepted_batches()
2096 .collect::<LadduResult<Vec<_>>>()
2097 .unwrap();
2098 assert_eq!(
2099 batches
2100 .iter()
2101 .map(|batch| batch.dataset().n_events())
2102 .collect::<Vec<_>>(),
2103 vec![2, 2, 1]
2104 );
2105 assert_eq!(
2106 batches
2107 .iter()
2108 .map(|batch| batch.dataset().n_events())
2109 .sum::<usize>(),
2110 5
2111 );
2112 }
2113
2114 #[test]
2115 fn expression_rejection_sampler_supports_pilot_envelope() {
2116 let generator = EventGenerator::new(demo_reaction(), HashMap::new(), Some(12345));
2117 let sampler = generator
2118 .rejection_sampler_with_expression(
2119 Expression::from(1.0),
2120 vec![],
2121 RejectionSamplingOptions {
2122 target_accepted: 3,
2123 generation_batch_size: 2,
2124 output_batch_size: 2,
2125 envelope: RejectionEnvelope::Pilot {
2126 pilot_events: 4,
2127 batch_size: Some(2),
2128 safety_factor: 1.25,
2129 },
2130 seed: 67890,
2131 },
2132 )
2133 .unwrap();
2134 let mut iter = sampler.accepted_batches();
2135 let batches = iter.by_ref().collect::<LadduResult<Vec<_>>>().unwrap();
2136 assert_eq!(
2137 batches
2138 .iter()
2139 .map(|batch| batch.dataset().n_events())
2140 .sum::<usize>(),
2141 3
2142 );
2143 assert_relative_eq!(iter.diagnostics().envelope_max_weight, 1.25);
2144 }
2145
2146 #[test]
2147 fn expression_rejection_sampler_rejects_invalid_intensities() {
2148 let generator = EventGenerator::new(demo_reaction(), HashMap::new(), Some(12345));
2149 let err = generator
2150 .rejection_sampler_with_expression(
2151 Expression::from(-1.0),
2152 vec![],
2153 RejectionSamplingOptions {
2154 target_accepted: 1,
2155 generation_batch_size: 1,
2156 output_batch_size: 1,
2157 envelope: RejectionEnvelope::Fixed { max_weight: 1.0 },
2158 seed: 67890,
2159 },
2160 )
2161 .unwrap()
2162 .accepted_batches()
2163 .next()
2164 .expect("sampler should emit an error");
2165 assert!(err.is_err());
2166 }
2167
2168 #[test]
2169 fn duplicate_generated_particle_ids_are_rejected() {
2170 let beam = GeneratedParticle::initial(
2171 "beam",
2172 InitialGenerator::beam_with_fixed_energy(0.0, 8.0),
2173 Reconstruction::Stored,
2174 );
2175 let target = GeneratedParticle::initial(
2176 "target",
2177 InitialGenerator::target(0.938272),
2178 Reconstruction::Missing,
2179 );
2180 let duplicate = GeneratedParticle::stable(
2181 "beam",
2182 StableGenerator::new(0.497611),
2183 Reconstruction::Stored,
2184 );
2185 let recoil = GeneratedParticle::stable(
2186 "recoil",
2187 StableGenerator::new(0.938272),
2188 Reconstruction::Stored,
2189 );
2190
2191 assert!(GeneratedReaction::two_to_two(
2192 beam,
2193 target,
2194 duplicate,
2195 recoil,
2196 MandelstamTDistribution::Exponential { slope: 0.1 },
2197 )
2198 .is_err());
2199 }
2200}