1use ahash::HashMap;
2use ahash::HashSet;
3use linnet::half_edge::involution::Flow;
4use linnet::half_edge::involution::HedgePair;
5use linnet::half_edge::involution::Orientation;
6use rayon::ThreadPool;
7use std::{
8 collections::BTreeMap,
9 fs::{self, File},
10 io::Write,
11 path::{Path, PathBuf},
12};
13use tracing::warn;
14use bincode_trait_derive::{Decode, Encode};
16use color_eyre::{Help, Result};
17use itertools::Itertools;
18use schemars::JsonSchema;
19use serde::{Deserialize, Serialize};
20use std::fmt;
21use tracing::debug;
22
23use crate::graph::FeynmanGraph;
24use crate::graph::edge::PossibleParticle;
25use crate::processes::DotExportSettings;
26
27use crate::processes::StandaloneExportSettings;
28use crate::{
29 GammaLoopContext, GammaLoopContextContainer,
30 feyngen::NumeratorAwareGraphGroupingOption,
31 integrands::process::ProcessIntegrand,
32 numerator::GlobalPrefactor,
33 settings::{GlobalSettings, RuntimeSettings, runtime::LockedRuntimeSettings},
34 uv::export::{UVForestExportSettings, sanitize_file_component},
35};
36use eyre::{Context, eyre};
37
38use crate::{
39 feyngen::{FeynGenFilters, GenerationType},
40 graph::Graph,
41 model::Model,
42 settings::global::GenerationSettings,
43};
44
45use super::{
46 Amplitude, CrossSection, GeneratedGraphReport, GenerationProcessKind, GenerationProgressPhase,
47 NamedGraphGenerationReport, generation_progress,
48};
49
50const SETTINGS_HISTORY_TOML: &str = "settings_history.toml";
51const SETTINGS_HISTORY_YAML: &str = "settings_history.yaml";
52
53pub struct ResolvedIntegrandRef<'a> {
54 pub canonical_name: String,
55 pub integrand: Option<&'a ProcessIntegrand>,
56}
57
58impl<'a> ResolvedIntegrandRef<'a> {
59 pub fn get_settings(&self) -> Option<&'a RuntimeSettings> {
60 self.integrand.map(ProcessIntegrand::get_settings)
61 }
62
63 pub fn require_generated(&self) -> Result<&'a ProcessIntegrand> {
64 self.integrand.ok_or_else(|| {
65 eyre!(
66 "Integrand {} has not yet been generated, but exists",
67 self.canonical_name
68 )
69 })
70 }
71}
72
73fn create_overwriting_file(path: &Path, file_kind: &str) -> Result<File> {
74 if path.exists() {
75 if path.is_dir() {
76 fs::remove_dir_all(path).with_context(|| {
77 format!(
78 "Trying to remove existing directory before exporting {file_kind} {}",
79 path.display()
80 )
81 })?;
82 } else {
83 fs::remove_file(path).with_context(|| {
84 format!(
85 "Trying to remove existing file before exporting {file_kind} {}",
86 path.display()
87 )
88 })?;
89 }
90 }
91
92 File::create(path).with_context(|| {
93 format!(
94 "Trying to create file to export {file_kind} {}",
95 path.display()
96 )
97 })
98}
99
100fn load_settings_history(path: &Path) -> Result<Option<GlobalSettings>> {
101 let settings_history_toml = path.join(SETTINGS_HISTORY_TOML);
102 if settings_history_toml.exists() {
103 let settings_history_raw =
104 fs::read_to_string(&settings_history_toml).with_context(|| {
105 format!(
106 "Error reading process settings history file {}",
107 settings_history_toml.display()
108 )
109 })?;
110 let settings_history = toml::from_str(&settings_history_raw).with_context(|| {
111 format!(
112 "Error parsing process settings history file {}",
113 settings_history_toml.display()
114 )
115 })?;
116 return Ok(Some(settings_history));
117 }
118
119 let settings_history_yaml = path.join(SETTINGS_HISTORY_YAML);
120 if settings_history_yaml.exists() {
121 warn!(
122 "Using legacy process settings history file {}. Re-save state to migrate to {}.",
123 settings_history_yaml.display(),
124 SETTINGS_HISTORY_TOML
125 );
126 let settings_history = serde_yaml::from_reader(File::open(&settings_history_yaml)?)
127 .with_context(|| {
128 format!(
129 "Error parsing legacy process settings history file {}",
130 settings_history_yaml.display()
131 )
132 })?;
133 return Ok(Some(settings_history));
134 }
135
136 Ok(None)
137}
138
139fn saved_child_dirs(root: &Path, expected_binary: &str, kind: &str) -> Result<Vec<PathBuf>> {
140 let mut saved_dirs = Vec::new();
141
142 for entry in fs::read_dir(root).with_context(|| format!("Error reading {}", root.display()))? {
143 let Ok(entry) = entry else {
144 debug!("Error reading entry");
145 continue;
146 };
147 if !entry.file_type()?.is_dir() {
148 continue;
149 }
150
151 let path = entry.path();
152 if !path.join(expected_binary).is_file() {
153 debug!(
154 "Skipping helper directory {} while loading {}s because '{}' is missing",
155 path.display(),
156 kind,
157 expected_binary
158 );
159 continue;
160 }
161
162 saved_dirs.push(path);
163 }
164
165 saved_dirs.sort();
166 Ok(saved_dirs)
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Encode, Decode)]
170#[trait_decode(trait = GammaLoopContext)]
171pub struct ProcessDefinition {
172 pub generation_type: GenerationType,
173 pub initial_pdgs: Vec<i64>,
174 pub final_pdgs_lists: Vec<Vec<i64>>,
175 pub loop_count_range: (usize, usize),
176 pub symmetrize_initial_states: bool,
177 pub symmetrize_final_states: bool,
178 pub symmetrize_left_right_states: bool,
179 pub allow_symmetrization_of_external_fermions_in_amplitudes: bool,
180 pub max_multiplicity_for_fast_cut_filter: usize,
181 pub amplitude_filters: FeynGenFilters,
182 pub cross_section_filters: FeynGenFilters,
183 pub folder_name: String,
184 pub process_id: usize,
185 pub numerator_grouping: NumeratorAwareGraphGroupingOption,
186 pub filter_self_loop: bool,
187 pub filter_zero_flow_edges: bool,
188 pub graph_prefix: String,
189 pub selected_graphs: Option<Vec<String>>,
190 pub vetoed_graphs: Option<Vec<String>>,
191 pub loop_momentum_bases: Option<HashMap<String, Vec<usize>>>,
192 pub prefactor: GlobalPrefactor,
193}
194
195impl fmt::Display for ProcessDefinition {
196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 write!(
198 f,
199 "Process #{}: '{}'\nGeneration type: {}{}{}\nInitial PDGs: {:?}{}\nFinal PDGs: {}{}\nLoop count: {}\nAmplitude filters:{}{}\nCross-section filters:{}{}",
200 self.process_id,
201 self.folder_name,
202 self.generation_type,
203 if self.symmetrize_left_right_states {
204 " (left-right symmetrized)"
205 } else {
206 ""
207 },
208 if self.allow_symmetrization_of_external_fermions_in_amplitudes
209 && self.generation_type == GenerationType::Amplitude
210 && (self.symmetrize_initial_states
211 || self.symmetrize_final_states
212 || self.symmetrize_left_right_states)
213 {
214 " (allowing fermion symmetrization)"
215 } else {
216 ""
217 },
218 self.initial_pdgs,
219 if self.symmetrize_initial_states {
220 " (symmetrized)"
221 } else {
222 ""
223 },
224 if self.final_pdgs_lists.len() == 1 {
225 format!("{:?}", self.final_pdgs_lists[0])
226 } else {
227 format!(
228 "[ {} ]",
229 self.final_pdgs_lists
230 .iter()
231 .map(|pdgs| format!("{:?}", pdgs))
232 .join(" | ")
233 )
234 },
235 if self.symmetrize_final_states {
236 " (symmetrized)"
237 } else {
238 ""
239 },
240 if self.loop_count_range.0 == self.loop_count_range.1 {
241 format!("{}", self.loop_count_range.0)
242 } else {
243 format!("{:?}", self.loop_count_range)
244 },
245 if self.amplitude_filters.0.is_empty() {
246 " None"
247 } else {
248 "\n"
249 },
250 if self.amplitude_filters.0.is_empty() {
251 "".into()
252 } else {
253 self.amplitude_filters
254 .0
255 .iter()
256 .map(|f| format!(" > {}", f))
257 .collect::<Vec<String>>()
258 .join("\n")
259 },
260 if self.cross_section_filters.0.is_empty() {
261 " None"
262 } else {
263 "\n"
264 },
265 if self.cross_section_filters.0.is_empty() {
266 "".into()
267 } else {
268 self.cross_section_filters
269 .0
270 .iter()
271 .map(|f| format!(" > {}", f))
272 .collect::<Vec<String>>()
273 .join("\n")
274 }
275 )
276 }
277}
278
279impl Default for ProcessDefinition {
280 fn default() -> Self {
281 Self {
282 generation_type: GenerationType::Amplitude,
283 initial_pdgs: vec![],
284 final_pdgs_lists: vec![],
285 loop_count_range: (1, 1),
286 symmetrize_initial_states: false,
287 symmetrize_final_states: false,
288 symmetrize_left_right_states: false,
289 allow_symmetrization_of_external_fermions_in_amplitudes: false,
290 max_multiplicity_for_fast_cut_filter: 6,
291 amplitude_filters: FeynGenFilters(vec![]),
292 cross_section_filters: FeynGenFilters(vec![]),
293 folder_name: "undefined_process".to_string(),
294 process_id: 0,
295 numerator_grouping: NumeratorAwareGraphGroupingOption::NoGrouping,
296 filter_self_loop: true,
297 graph_prefix: "GL".to_string(),
298 selected_graphs: None,
299 vetoed_graphs: None,
300 loop_momentum_bases: None,
301 prefactor: GlobalPrefactor::default(),
302 filter_zero_flow_edges: true,
303 }
304 }
305}
306
307impl ProcessDefinition {
308 pub fn from_graph_list(
310 graphs: &[Graph],
311 generation_type: GenerationType,
312 model: &Model,
313 ) -> Result<Self> {
314 let mut initial_pdgs = HashSet::default();
315
316 for g in graphs {
317 let mut initial_pdgs_of_graph = vec![];
318 match generation_type {
319 GenerationType::Amplitude => {
320 for (pair, _, edge) in g.iter_edges() {
321 if matches!(
322 pair,
323 HedgePair::Unpaired {
324 hedge: _,
325 flow: Flow::Sink
326 }
327 ) {
328 if let PossibleParticle::Particle(particle) = &edge.data.particle {
329 initial_pdgs_of_graph.push(particle.0.pdg_code as i64);
330 } else {
331 debug!("Edge without particle data in initial state");
332 }
333 }
334 }
335 }
336 GenerationType::CrossSection => {
337 for (_, _, edge) in g.iter_edges_of(&g.initial_state_cut) {
338 if let PossibleParticle::Particle(particle) = &edge.data.particle {
339 initial_pdgs_of_graph.push(particle.0.pdg_code as i64);
340 } else {
341 debug!("Edge without particle data in initial state");
342 }
343 }
344 }
345 }
346
347 initial_pdgs_of_graph.sort();
348 initial_pdgs.insert(initial_pdgs_of_graph);
349 }
350
351 let initial_pdgs = if initial_pdgs.len() == 1 {
352 initial_pdgs.into_iter().next().unwrap()
353 } else {
354 warn!("Multiple initial states found in graphs, setting initial state to empty");
355 vec![]
356 };
357
358 let mut final_states = HashSet::default();
359 match generation_type {
360 GenerationType::Amplitude => {
361 for g in graphs {
362 let mut final_pdgs_of_graph = vec![];
363 for (pair, _, edge) in g.iter_edges() {
364 if matches!(
365 pair,
366 HedgePair::Unpaired {
367 hedge: _,
368 flow: Flow::Source
369 }
370 ) {
371 if let PossibleParticle::Particle(particle) = &edge.data.particle {
372 final_pdgs_of_graph.push(particle.0.pdg_code as i64);
373 } else {
374 debug!("Edge without particle data in final state");
375 }
376 }
377 }
378 final_pdgs_of_graph.sort();
379 final_states.insert(final_pdgs_of_graph);
380 }
381 }
382 GenerationType::CrossSection => {
383 for g in graphs {
384 let (source_nodes, target_nodes) = g.get_source_and_target();
385 let st_cuts = g.all_st_cuts_for_cs(
386 source_nodes,
387 target_nodes,
388 &g.get_initial_state_tree().0,
389 );
390 for (_, cut, _) in st_cuts {
391 let mut final_pdgs_of_cut = vec![];
392 for (orientaion, edge) in cut.iter_edges(&g.underlying) {
393 if let PossibleParticle::Particle(particle) = &edge.data.particle {
394 if orientaion == Orientation::Reversed {
395 final_pdgs_of_cut
396 .push(particle.0.get_anti_particle(model).pdg_code as i64);
397 } else {
398 final_pdgs_of_cut.push(particle.0.pdg_code as i64);
399 }
400 } else {
401 debug!("Edge without particle data in final state");
402 }
403 }
404 final_pdgs_of_cut.sort();
405 final_states.insert(final_pdgs_of_cut);
406 }
407 }
408 }
409 }
410
411 let final_pdgs_lists = final_states.into_iter().sorted().collect_vec();
412 let mut min_loop_count = usize::MAX;
413 let mut max_loop_count = 0usize;
414
415 for g in graphs {
416 let lc = g.underlying.cyclotomatic_number(&g.full_filter());
418 if lc < min_loop_count {
419 min_loop_count = lc;
420 }
421 if lc > max_loop_count {
422 max_loop_count = lc;
423 }
424 }
425
426 let loop_count_range = (min_loop_count, max_loop_count);
427
428 Ok(Self {
429 generation_type,
430 initial_pdgs,
431 final_pdgs_lists,
432 loop_count_range,
433 ..Self::default()
434 })
435 }
436}
437
438#[derive(Clone, Encode, Decode)]
439#[trait_decode(trait = GammaLoopContext)]
440pub struct Process {
441 pub definition: ProcessDefinition,
442 pub settings_history: Option<GlobalSettings>,
443 pub collection: ProcessCollection,
444}
445
446impl Process {
447 pub fn warm_up(&mut self, model: &Model) -> Result<()> {
448 self.collection.warm_up(model)
449 }
450 pub fn preprocess(
451 &mut self,
452 model: &Model,
453 settings: &GlobalSettings,
454 locked_runtime_settings: &LockedRuntimeSettings,
455 thread_pool: &ThreadPool,
456 ) -> Result<Vec<GeneratedGraphReport>> {
457 let reports = self.collection.preprocess(
458 model,
459 &self.definition,
460 &settings.generation,
461 locked_runtime_settings,
462 thread_pool,
463 )?;
464 self.settings_history = Some(settings.clone());
465 Ok(self.attach_process_id(reports))
466 }
467
468 fn attach_process_id(
469 &self,
470 reports: Vec<NamedGraphGenerationReport>,
471 ) -> Vec<GeneratedGraphReport> {
472 reports
473 .into_iter()
474 .map(|report| GeneratedGraphReport {
475 process_id: self.definition.process_id,
476 integrand_name: report.integrand_name,
477 graph_name: report.graph_name,
478 stats: report.stats,
479 })
480 .collect()
481 }
482}
483
484impl Process {
485 pub(crate) fn load_amplitude(
486 path: impl AsRef<Path>,
487 context: GammaLoopContextContainer,
488 ) -> Result<Self> {
489 let binary = fs::read(path.as_ref().join("def.bin")).context(format!(
490 "Error reading def.bin in {}",
491 path.as_ref().display()
492 ))?;
493
494 let settings_history = load_settings_history(path.as_ref())?;
495
496 let (definition, _) =
497 bincode::decode_from_slice_with_context(&binary, bincode::config::standard(), context)
498 .context("Error decoding process definition")?;
499
500 let mut collection = ProcessCollection::new_amplitude();
501 for path in saved_child_dirs(path.as_ref(), "amp.bin", "amplitude")? {
502 debug!("loading amplitude at {}", path.display());
503 let amp = Amplitude::load(path, context).context("Error loading amplitude")?;
504
505 collection.add_amplitude(amp);
506 }
507
508 Ok(Self {
509 definition,
510 collection,
511 settings_history,
512 })
513 }
514
515 pub(crate) fn load_cross_section(
516 path: impl AsRef<Path>,
517 context: GammaLoopContextContainer,
518 ) -> Result<Self> {
519 let binary = fs::read(path.as_ref().join("def.bin"))?;
520 let (definition, _) =
521 bincode::decode_from_slice_with_context(&binary, bincode::config::standard(), context)?;
522
523 let mut collection = ProcessCollection::new_cross_section();
524 let settings_history = load_settings_history(path.as_ref())?;
525 for path in saved_child_dirs(path.as_ref(), "cs.bin", "cross section")? {
526 debug!("loading cross section at {}", path.display());
527 let cs = CrossSection::load(path, context).context("Error loading cross section")?;
528
529 collection.add_cross_section(cs);
530 }
531
532 Ok(Self {
533 definition,
534 collection,
535 settings_history,
536 })
537 }
538
539 pub fn save(&mut self, path: impl AsRef<Path>, override_existing: bool) -> Result<()> {
540 match &mut self.collection {
541 ProcessCollection::Amplitudes(a) => {
542 let p = path.as_ref().join("amplitudes");
543 fs::create_dir_all(&p)?;
544 let p = p.join(PathBuf::from(self.definition.folder_name.clone()));
545
546 let r = fs::create_dir_all(&p).with_context(|| {
547 format!(
548 "Trying to create directory to export amplitude dot {}",
549 p.display()
550 )
551 });
552 if override_existing {
553 r?;
554 }
555
556 let binary = bincode::encode_to_vec(&self.definition, bincode::config::standard())?;
557 fs::write(p.join("def.bin"), binary)?;
558
559 if let Some(a) = &self.settings_history {
560 File::create(p.join(SETTINGS_HISTORY_TOML))?
561 .write_all(toml::to_string_pretty(a)?.as_bytes())?;
562 }
563
564 for amp in a.values_mut() {
565 amp.save(&p, override_existing)?;
566 }
567 }
568 ProcessCollection::CrossSections(cs) => {
569 let p = path.as_ref().join("cross_sections");
570 fs::create_dir_all(&p)?;
571 let p = p.join(PathBuf::from(self.definition.folder_name.clone()));
572
573 let r = fs::create_dir_all(&p).with_context(|| {
574 format!(
575 "Trying to create directory to save cross section dot {}",
576 p.display()
577 )
578 });
579
580 if override_existing {
581 r?;
582 }
583
584 let binary = bincode::encode_to_vec(&self.definition, bincode::config::standard())?;
585 fs::write(p.join("def.bin"), binary)?;
586
587 if let Some(a) = &self.settings_history {
588 File::create(p.join(SETTINGS_HISTORY_TOML))?
589 .write_all(toml::to_string_pretty(a)?.as_bytes())?;
590 }
591
592 for cs in cs.values_mut() {
593 cs.save(&p, override_existing)?;
594 }
595 }
596 }
597
598 Ok(())
599 }
600
601 pub fn compile(
602 &mut self,
603 path: impl AsRef<Path>,
604 override_existing: bool,
605 integrand_name: Option<String>,
606 thread_pool: &ThreadPool,
607 ) -> Result<Vec<GeneratedGraphReport>> {
608 match &mut self.collection {
609 ProcessCollection::Amplitudes(a) => {
610 let p = path.as_ref().join("amplitudes");
611 fs::create_dir_all(&p)?;
612 let p = p.join(PathBuf::from(self.definition.folder_name.clone()));
613
614 let r = fs::create_dir_all(&p).with_context(|| {
615 format!(
616 "Trying to create directory to export amplitude dot {}",
617 p.display()
618 )
619 });
620 if override_existing {
621 r?;
622 }
623
624 let mut reports = Vec::new();
625 for amp in a.values_mut() {
626 if let Some(int_name) = integrand_name.clone()
627 && amp.name != int_name
628 {
629 continue;
630 }
631
632 reports.extend(amp.compile(&p, override_existing, thread_pool)?);
633 }
634 Ok(self.attach_process_id(reports))
635 }
636 ProcessCollection::CrossSections(cs) => {
637 let p = path.as_ref().join("cross_sections");
638 fs::create_dir_all(&p)?;
639 let p = p.join(PathBuf::from(self.definition.folder_name.clone()));
640
641 let r = fs::create_dir_all(&p).with_context(|| {
642 format!(
643 "Trying to create directory to export cross section dot {}",
644 p.display()
645 )
646 });
647 if override_existing {
648 r?;
649 }
650
651 let mut reports = Vec::new();
652 for cs in cs.values_mut() {
653 if let Some(int_name) = integrand_name.clone()
654 && cs.name != int_name
655 {
656 continue;
657 }
658
659 reports.extend(cs.compile(&p, override_existing, thread_pool)?);
660 }
661 Ok(self.attach_process_id(reports))
662 }
663 }
664 }
665
666 pub fn activate_loaded_integrand_backends(
667 &mut self,
668 allow_symjit_fallback: bool,
669 ) -> Result<()> {
670 match &mut self.collection {
671 ProcessCollection::Amplitudes(amplitudes) => {
672 for (integrand_name, amplitude) in amplitudes.iter_mut() {
673 if let Some(integrand) = amplitude.integrand.as_mut()
674 && let Some(reason) =
675 integrand.activate_runtime_backends_after_load(allow_symjit_fallback)?
676 {
677 warn!(
678 "Falling back to symjit for integrand '{}' in process #{} ({}) after external compiled evaluator loading failed: {}",
679 integrand_name,
680 self.definition.process_id,
681 self.definition.folder_name,
682 reason
683 );
684 }
685 }
686 }
687 ProcessCollection::CrossSections(cross_sections) => {
688 for (integrand_name, cross_section) in cross_sections.iter_mut() {
689 if let Some(integrand) = cross_section.integrand.as_mut()
690 && let Some(reason) =
691 integrand.activate_runtime_backends_after_load(allow_symjit_fallback)?
692 {
693 warn!(
694 "Falling back to symjit for integrand '{}' in process #{} ({}) after external compiled evaluator loading failed: {}",
695 integrand_name,
696 self.definition.process_id,
697 self.definition.folder_name,
698 reason
699 );
700 }
701 }
702 }
703 }
704
705 Ok(())
706 }
707
708 pub fn get_integrand(
709 &self,
710 integrand_name: impl AsRef<str>,
711 ) -> Result<ResolvedIntegrandRef<'_>> {
712 self.collection.get_integrand(integrand_name)
713 }
714
715 pub fn get_integrand_names(&self) -> Vec<&str> {
716 self.collection.get_integrand_names()
717 }
718
719 pub fn get_integrand_mut(
720 &mut self,
721 integrand_name: impl AsRef<str>,
722 ) -> Result<&mut ProcessIntegrand> {
723 self.collection.get_integrand_mut(integrand_name)
724 }
725
726 pub(crate) fn export_standalone(
727 &self,
728 path: impl AsRef<Path>,
729 settings: &StandaloneExportSettings,
730 ) -> Result<()> {
731 match &self.collection {
732 ProcessCollection::Amplitudes(a) => {
733 let p = path.as_ref().join("amplitudes");
734 let path = p.join(PathBuf::from(self.definition.folder_name.clone()));
735 fs::create_dir_all(&path)?;
736 for amp in a.values() {
737 let amp_path = path.join(&.name);
739 fs::create_dir_all(&_path).with_context(|| {
740 format!(
741 "Trying to create directory for amplitude {}",
742 amp_path.display()
743 )
744 })?;
745
746 amp.export_standalone(&_path, settings)?;
747 }
748 }
749 ProcessCollection::CrossSections(cs) => {
750 let p = path.as_ref().join("cross_sections");
751 let path = p.join(PathBuf::from(self.definition.folder_name.clone()));
752 fs::create_dir_all(&path)?;
753 for cs in cs.values() {
754 let cs_path = path.join(&cs.name);
756 fs::create_dir_all(&cs_path).with_context(|| {
757 format!(
758 "Trying to create directory for cross section {}",
759 cs_path.display()
760 )
761 })?;
762
763 cs.export_standalone(&cs_path, settings)?;
764 }
765 }
766 }
767 Ok(())
768 }
769
770 pub(crate) fn export_dot(
771 &self,
772 path: impl AsRef<Path>,
773 settings: &DotExportSettings,
774 ) -> Result<()> {
775 match &self.collection {
776 ProcessCollection::Amplitudes(a) => {
777 let p = path.as_ref().join("amplitudes");
778 let path = p.join(PathBuf::from(self.definition.folder_name.clone()));
779 fs::create_dir_all(&path)?;
780 for (amp_name, amp) in a {
781 let amp_path = path.join(&.name);
783 fs::create_dir_all(&_path).with_context(|| {
784 format!(
785 "Trying to create directory for amplitude {}",
786 amp_path.display()
787 )
788 })?;
789
790 if settings.combine_diagrams {
791 let output_path = amp_path.join(format!("{}_graphs.dot", amp_name.clone()));
793 let mut dot = create_overwriting_file(&output_path, "amplitude graph")?;
794 for graph in amp.graphs.iter() {
795 graph.graph.dot_serialize_io(&mut dot, settings)?;
796 }
797 } else {
798 for graph in amp.graphs.iter() {
800 let output_path = amp_path.join(format!("{}.dot", graph.graph.name));
801 let mut dot = create_overwriting_file(&output_path, "amplitude graph")?;
802 graph.graph.dot_serialize_io(&mut dot, settings)?;
803 }
804 }
805 }
806 }
807 ProcessCollection::CrossSections(cs) => {
808 let p = path.as_ref().join("cross_sections");
809 let path = p.join(PathBuf::from(self.definition.folder_name.clone()));
810 fs::create_dir_all(&path)?;
811 for (xs_name, cs) in cs {
812 let cs_path = path.join(&cs.name);
814 fs::create_dir_all(&cs_path).with_context(|| {
815 format!(
816 "Trying to create directory for cross section {}",
817 cs_path.display()
818 )
819 })?;
820
821 if settings.combine_diagrams {
822 let output_path = cs_path.join(format!("{}_graphs.dot", xs_name.clone()));
824 let mut dot = create_overwriting_file(&output_path, "cross section graph")?;
825 for graph in cs.supergraphs.iter() {
826 graph.graph.dot_serialize_io(&mut dot, settings)?;
827 }
828 } else {
829 for graph in cs.supergraphs.iter() {
831 let output_path = cs_path.join(format!("{}.dot", graph.graph.name));
832 let mut dot =
833 create_overwriting_file(&output_path, "cross section graph")?;
834 graph.graph.dot_serialize_io(&mut dot, settings)?;
835 }
836 }
837 }
838 }
839 }
840 Ok(())
841 }
842
843 pub(crate) fn export_uv_forests(
844 &self,
845 path: impl AsRef<Path>,
846 integrand_name: &str,
847 graph_ids: &[usize],
848 settings: &UVForestExportSettings,
849 ) -> Result<()> {
850 let generation_settings = &self
851 .settings_history
852 .as_ref()
853 .ok_or_else(|| {
854 eyre!(
855 "Cannot export UV forests for process {} without generation settings history",
856 self.definition.folder_name
857 )
858 })?
859 .generation;
860 let resolved = self.get_integrand(integrand_name)?;
861 let integrand = resolved.require_generated()?;
862 let integrand_path = match &self.collection {
863 ProcessCollection::Amplitudes(_) => path
864 .as_ref()
865 .join("amplitudes")
866 .join(PathBuf::from(self.definition.folder_name.clone()))
867 .join(&resolved.canonical_name),
868 ProcessCollection::CrossSections(_) => path
869 .as_ref()
870 .join("cross_sections")
871 .join(PathBuf::from(self.definition.folder_name.clone()))
872 .join(&resolved.canonical_name),
873 };
874 fs::create_dir_all(&integrand_path).with_context(|| {
875 format!(
876 "Trying to create directory for UV forest export {}",
877 integrand_path.display()
878 )
879 })?;
880
881 for &graph_id in graph_ids {
882 let export =
883 integrand.export_uv_forest_graph(graph_id, generation_settings, settings)?;
884 let graph_name = sanitize_file_component(&export.graph_name);
885 let forest_path = integrand_path.join(format!("{graph_name}.forest.dot"));
886 let mut forest_file = create_overwriting_file(&forest_path, "UV forest")?;
887 forest_file.write_all(export.forest_dot.as_bytes())?;
888
889 for term in export.node_terms {
890 let node_dir = integrand_path
891 .join(format!("{graph_name}_nodes"))
892 .join(format!("forest_{:03}", term.forest_index));
893 fs::create_dir_all(&node_dir).with_context(|| {
894 format!(
895 "Trying to create directory for UV forest node graph {}",
896 node_dir.display()
897 )
898 })?;
899 let mut dot = create_overwriting_file(
900 &node_dir.join(term.file_name()),
901 "UV forest node graph",
902 )?;
903 dot.write_all(term.dot.as_bytes())?;
904 }
905 }
906
907 Ok(())
908 }
909
910 pub fn from_graph_list(
911 process_name: String,
912 integrand_name: String,
913 graphs: Vec<Graph>,
914 generation_type: GenerationType,
915 definition: Option<ProcessDefinition>,
916 sub_classes: Option<Vec<Vec<String>>>,
917 model: &Model,
918 ) -> Result<Self> {
919 let mut proc_definition = definition.unwrap_or_default();
920 proc_definition.folder_name = process_name;
921 match generation_type {
922 GenerationType::Amplitude => {
923 let mut collection: ProcessCollection = ProcessCollection::new_amplitude();
924
925 if let Some(_sub_classes) = sub_classes {
926 todo!("implement seperation of processes into user defined sub classes");
927 } else {
928 collection.add_amplitude(Amplitude::from_graph_list(integrand_name, graphs)?);
929
930 Ok(Self {
932 settings_history: None,
933 definition: proc_definition,
934 collection,
935 })
936 }
937 }
938 GenerationType::CrossSection => {
939 let mut collection: ProcessCollection = ProcessCollection::new_cross_section();
940
941 if let Some(_sub_classes) = sub_classes {
942 todo!("implement seperation of processes into user defined sub classes");
943 } else {
944 collection.add_cross_section(CrossSection::from_graph_list(
945 integrand_name,
946 graphs,
947 model,
948 )?);
949 Ok(Self {
951 settings_history: None,
952 definition: proc_definition,
953 collection,
954 })
955 }
956 }
957 }
958 }
959
960 pub fn generate_integrands(
961 &mut self,
962 model: &Model,
963 global_settings: &GlobalSettings,
964 runtime_default: LockedRuntimeSettings,
965 thread_pool: &ThreadPool,
966 ) -> Result<Vec<GeneratedGraphReport>> {
967 let reports = self.collection.generate_integrands(
968 model,
969 &self.definition.folder_name,
970 global_settings,
971 runtime_default,
972 thread_pool,
973 )?;
974 Ok(self.attach_process_id(reports))
975 }
976}
977
978#[derive(Clone, Encode, Decode)]
979#[trait_decode(trait = GammaLoopContext)]
980pub enum ProcessCollection {
981 Amplitudes(BTreeMap<String, Amplitude>),
982 CrossSections(BTreeMap<String, CrossSection>),
983}
984
985impl ProcessCollection {
986 fn new_amplitude() -> Self {
987 Self::Amplitudes(BTreeMap::new())
988 }
989
990 pub fn get_integrand_names(&self) -> Vec<&str> {
991 match self {
992 Self::Amplitudes(amplitudes) => amplitudes.keys().map(|a| a.as_str()).collect(),
993 Self::CrossSections(cross_sections) => {
994 cross_sections.keys().map(|a| a.as_str()).collect()
995 }
996 }
997 }
998
999 fn get_integrand(&self, name: impl AsRef<str>) -> Result<ResolvedIntegrandRef<'_>> {
1000 let canonical_name = self.find_integrand(Some(name.as_ref().to_string()))?;
1001 let integrand = match self {
1002 Self::Amplitudes(amplitudes) => amplitudes
1003 .get(&canonical_name)
1004 .expect("resolved amplitude name must exist")
1005 .integrand
1006 .as_ref(),
1007 Self::CrossSections(cross_sections) => cross_sections
1008 .get(&canonical_name)
1009 .expect("resolved cross section name must exist")
1010 .integrand
1011 .as_ref(),
1012 };
1013
1014 Ok(ResolvedIntegrandRef {
1015 canonical_name,
1016 integrand,
1017 })
1018 }
1019
1020 pub fn find_integrand(&self, name: Option<String>) -> Result<String> {
1021 let all_integrand_names = self.get_integrand_names();
1022
1023 let integrand_name = if let Some(name) = name {
1024 if !all_integrand_names.contains(&name.as_str()) {
1025 return Err(color_eyre::eyre::eyre!(
1026 "No integrand named '{}' in process, Available integrands: {:?}",
1027 name,
1028 all_integrand_names
1029 ));
1030 }
1031 name
1032 } else {
1033 if all_integrand_names.len() != 1 {
1034 return Err(color_eyre::eyre::eyre!(
1035 "Multiple integrands in process,Please specify one of: {:?}",
1036 all_integrand_names
1037 ));
1038 }
1039 all_integrand_names[0].to_string()
1040 };
1041
1042 Ok(integrand_name)
1043 }
1044
1045 fn get_integrand_mut(&mut self, name: impl AsRef<str>) -> Result<&mut ProcessIntegrand> {
1046 let res = match self {
1047 Self::Amplitudes(amplitudes) => {
1048 if amplitudes.contains_key(name.as_ref()) {
1049 Ok(amplitudes
1050 .get_mut(name.as_ref())
1051 .unwrap()
1052 .integrand
1053 .as_mut())
1054 } else {
1055 let names = amplitudes.keys().map(|a| a.as_str()).collect::<Vec<_>>();
1056
1057 Err(eyre!("Integrand {} does not exist", name.as_ref()))
1058 .suggestion(format!("Available amplitude names: {}", names.join(", ")))
1059 }
1060 }
1061 Self::CrossSections(cross_sections) => {
1062 if cross_sections.contains_key(name.as_ref()) {
1063 Ok(cross_sections
1064 .get_mut(name.as_ref())
1065 .unwrap()
1066 .integrand
1067 .as_mut())
1068 } else {
1069 let names = cross_sections
1070 .keys()
1071 .map(|a| a.as_str())
1072 .collect::<Vec<_>>();
1073
1074 Err(eyre!("Integrand {} does not exist", name.as_ref())).suggestion(format!(
1075 "Available cross section names: {}",
1076 names.join(", ")
1077 ))
1078 }
1079 }
1080 }?;
1081
1082 match res {
1083 Some(integrand) => Ok(integrand),
1084 None => Err(eyre!(
1085 "Integrand {} has not yet been generated, but exists",
1086 name.as_ref()
1087 )),
1088 }
1089 }
1090
1091 fn new_cross_section() -> Self {
1092 Self::CrossSections(BTreeMap::new())
1093 }
1094 pub fn remove_integrand(&mut self, integrand_name: &str) -> Result<()> {
1095 match self {
1096 Self::Amplitudes(amplitudes) => {
1097 amplitudes
1098 .remove(integrand_name)
1099 .ok_or(eyre!("No amplitude named {}", integrand_name))?;
1100 }
1101 Self::CrossSections(cross_sections) => {
1102 cross_sections
1103 .remove(integrand_name)
1104 .ok_or(eyre!("No cross section named {}", integrand_name))?;
1105 }
1106 }
1107 Ok(())
1108 }
1109
1110 pub fn add_amplitude(&mut self, amplitude: Amplitude) {
1111 match self {
1112 Self::Amplitudes(amplitudes) => amplitudes.insert(amplitude.name.clone(), amplitude),
1113 _ => panic!("Cannot add amplitude to a cross section collection"),
1114 };
1115 }
1116
1117 pub fn add_cross_section(&mut self, cross_section: CrossSection) {
1118 match self {
1119 Self::CrossSections(cross_sections) => {
1120 cross_sections.insert(cross_section.name.clone(), cross_section);
1121 }
1122 _ => panic!("Cannot add cross section to an amplitude collection"),
1123 }
1124 }
1125
1126 fn preprocess(
1127 &mut self,
1128 model: &Model,
1129 process_definition: &ProcessDefinition,
1130 settings: &GenerationSettings,
1131 locked_runtime_settings: &LockedRuntimeSettings,
1132 thread_pool: &ThreadPool,
1133 ) -> Result<Vec<NamedGraphGenerationReport>> {
1134 match self {
1135 Self::Amplitudes(amplitudes) => {
1136 let mut reports = Vec::new();
1137 for amplitude in amplitudes.values_mut() {
1138 generation_progress::begin_phase(
1139 GenerationProgressPhase::GraphPreprocessing,
1140 GenerationProcessKind::Amplitude,
1141 &process_definition.folder_name,
1142 &litude.name,
1143 amplitude.graphs.len(),
1144 None,
1145 );
1146 reports.extend(amplitude.preprocess(
1147 model,
1148 settings,
1149 locked_runtime_settings,
1150 thread_pool,
1151 )?);
1152 }
1153 Ok(reports)
1154 }
1155 Self::CrossSections(cross_sections) => {
1156 let mut reports = Vec::new();
1157 for cross_section in cross_sections.values_mut() {
1158 reports.extend(cross_section.preprocess(
1159 model,
1160 process_definition,
1161 settings,
1162 *locked_runtime_settings,
1163 thread_pool,
1164 )?);
1165 }
1166 Ok(reports)
1167 }
1168 }
1169 }
1170
1171 pub fn warm_up(&mut self, model: &Model) -> Result<()> {
1172 match self {
1173 Self::Amplitudes(amplitudes) => {
1174 for amplitude in amplitudes.values_mut() {
1175 amplitude.warm_up(model)?;
1176 }
1177 }
1178 Self::CrossSections(cross_sections) => {
1179 for cross_section in cross_sections.values_mut() {
1180 cross_section.warm_up(model)?;
1181 }
1182 }
1183 }
1184 Ok(())
1185 }
1186
1187 fn generate_integrands(
1188 &mut self,
1189 model: &Model,
1190 process_name: &str,
1191 global_settings: &GlobalSettings,
1192 runtime_default: LockedRuntimeSettings,
1193 thread_pool: &ThreadPool,
1194 ) -> Result<Vec<NamedGraphGenerationReport>> {
1195 match self {
1196 Self::Amplitudes(amplitudes) => {
1197 let mut reports = Vec::new();
1198 for amplitude in amplitudes.values_mut() {
1199 reports.extend(amplitude.build_integrand(
1200 model,
1201 process_name,
1202 global_settings,
1203 runtime_default,
1204 thread_pool,
1205 )?);
1206 }
1207 Ok(reports)
1208 }
1209 Self::CrossSections(cross_sections) => {
1210 let mut reports = Vec::new();
1211 for cross_section in cross_sections.values_mut() {
1212 reports.extend(cross_section.build_integrand(
1213 model,
1214 process_name,
1215 global_settings,
1216 runtime_default,
1217 thread_pool,
1218 )?);
1219 }
1220 Ok(reports)
1221 }
1222 }
1223 }
1224}
1225
1226#[cfg(test)]
1227mod tests {
1228 use std::{
1229 fs,
1230 path::PathBuf,
1231 time::{SystemTime, UNIX_EPOCH},
1232 };
1233
1234 use crate::{GammaLoopContextContainer, utils::load_generic_model};
1235
1236 fn fresh_temp_dir(name: &str) -> PathBuf {
1237 let unique = SystemTime::now()
1238 .duration_since(UNIX_EPOCH)
1239 .unwrap()
1240 .as_nanos();
1241 let path = std::env::temp_dir().join(format!(
1242 "gammalooprs-{name}-{}-{unique}",
1243 std::process::id()
1244 ));
1245 fs::create_dir_all(&path).unwrap();
1246 path
1247 }
1248
1249 #[test]
1250 fn saved_child_dirs_skip_cross_section_compile_artifact_folders() {
1251 let temp = fresh_temp_dir("saved-child-dirs");
1252 let saved_dir = temp.join("NLO");
1253 let compiled_dir = temp.join("cs_NLO");
1254
1255 fs::create_dir_all(&saved_dir).unwrap();
1256 fs::write(saved_dir.join("cs.bin"), []).unwrap();
1257 fs::create_dir_all(compiled_dir.join("integrand").join("GL08")).unwrap();
1258
1259 let dirs = super::saved_child_dirs(&temp, "cs.bin", "cross section").unwrap();
1260
1261 assert_eq!(dirs, vec![saved_dir]);
1262 fs::remove_dir_all(temp).unwrap();
1263 }
1264
1265 mod failing {
1266 use super::*;
1267
1268 #[test]
1269 fn test_proc_definition_encode() {
1270 let def = crate::processes::ProcessDefinition::default();
1271 let encoded = bincode::encode_to_vec(&def, bincode::config::standard()).unwrap();
1272 let model_sm = load_generic_model("sm");
1273
1274 let mut state_file = std::fs::File::create("state_map.bin").unwrap();
1275 symbolica::state::State::export(&mut state_file).unwrap();
1276 let state_map = symbolica::state::State::import(&mut state_file, None).unwrap();
1277
1278 let context = GammaLoopContextContainer {
1279 model: &model_sm,
1280 state_map: &state_map,
1281 };
1282
1283 let (decoded, _): (crate::processes::ProcessDefinition, _) =
1284 bincode::decode_from_slice_with_context(
1285 &encoded,
1286 bincode::config::standard(),
1287 context,
1288 )
1289 .unwrap();
1290 assert_eq!(def, decoded);
1291 }
1292 }
1293}