Skip to main content

gammalooprs/processes/
process.rs

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;
14// use bincode::{Decode, Encode};
15use 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    // Best attempt at creating what process definition matches the given graphs
309    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            // don't know how the looop count is really intended, for now it doesn't matter I think
417            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                    // Create a folder for each amplitude
738                    let amp_path = path.join(&amp.name);
739                    fs::create_dir_all(&amp_path).with_context(|| {
740                        format!(
741                            "Trying to create directory for amplitude {}",
742                            amp_path.display()
743                        )
744                    })?;
745
746                    amp.export_standalone(&amp_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                    // Create a folder for each cross section
755                    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                    // Create a folder for each amplitude
782                    let amp_path = path.join(&amp.name);
783                    fs::create_dir_all(&amp_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                        // Save all graphs combined in one file
792                        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                        // Save each graph in its own file
799                        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                    // Create a folder for each cross section
813                    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                        // Save all graphs combined in one file
823                        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                        // Save each supergraph in its own file
830                        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                    // TODO: construct a better default definition from graph (i.e. at least the external IDs)
931                    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                    // TODO: construct a better default definition from graph (i.e. at least the external IDs)
950                    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                        &amplitude.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}