Skip to main content

gammalooprs/processes/
mod.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    fs,
4    path::Path,
5};
6
7// use bincode::{Decode, Encode};
8use bincode_trait_derive::{Decode, Encode};
9use color_eyre::Result;
10use eyre::{Context, eyre};
11use rayon::ThreadPool;
12use schemars::JsonSchema;
13use symbolica::evaluate::OptimizationSettings;
14use tracing::debug;
15
16use crate::{
17    GammaLoopContext, GammaLoopContextContainer,
18    settings::{GlobalSettings, runtime::LockedRuntimeSettings},
19    utils::serde_utils::{IsDefault, is_false, is_true, is_usize, show_defaults_helper},
20    uv::export::UVForestExportSettings,
21};
22use serde::{Deserialize, Serialize};
23
24use crate::model::Model;
25
26mod generation_report;
27pub use generation_report::{
28    EvaluatorBuildTimings, GeneratedGraphKey, GeneratedGraphReport, GraphGenerationStats,
29    NamedGraphGenerationReport, merge_generated_graph_reports,
30};
31mod generation_progress;
32pub use generation_progress::{
33    GenerationProcessKind, GenerationProgressMode, GenerationProgressModeGuard,
34    GenerationProgressObserver, GenerationProgressObserverGuard, GenerationProgressPhase,
35    begin_phase, cut_finished, detailed_progress_enabled, detailed_progress_message,
36    enter_detailed_progress_span, enter_progress_context,
37};
38mod selection;
39pub use selection::{
40    CycleSignature, GraphGroupSelectionMode, GraphGroupSelectionPlan, GraphGroupSelectionReport,
41    GraphGroupSelectionSpec, GraphSelectionSignatureInventory, ParticleSignature,
42    RaisedCutSignatureInventory, RaisedPropagatorScope, RaisedPropagatorSignature,
43    SelectionPolarity, VertexSignature,
44};
45pub(crate) use selection::{GraphCutSelectionSubject, GraphSelectionSubject};
46
47#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
48#[derive(
49    Debug, Clone, Copy, Default, Serialize, Deserialize, Encode, Decode, PartialEq, Eq, JsonSchema,
50)]
51#[serde(rename_all = "snake_case")]
52pub enum TensorNetworkContractionOrder {
53    SparseAtomAware,
54    AtomAware,
55    ResultRankOnly,
56    EntryAware,
57    #[default]
58    IntermediateCost,
59}
60
61#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
62#[derive(Debug, Clone, Copy, Serialize, Deserialize, Encode, Decode, PartialEq, JsonSchema)]
63pub enum ExecutionMode {
64    Sequential,
65    Parallel,
66    SequentialRef,
67    SequentialExtract,
68}
69
70#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
71#[derive(Debug, Clone, Copy, Serialize, Deserialize, Encode, Decode, PartialEq, JsonSchema)]
72pub enum ContractionMode {
73    SmallestDegree,
74    MinResultRank,
75}
76#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
77#[derive(Debug, Clone, Copy, Serialize, Deserialize, Encode, Decode, PartialEq, JsonSchema)]
78pub struct EvaluatorSettings {
79    /// Perform symbolic tensor and scalar algebra before lowering to an evaluator.
80    #[serde(default, skip_serializing_if = "is_false")]
81    pub do_algebra: bool,
82    /// Re-optimize orientation expressions iteratively while building the evaluator.
83    #[serde(
84        default = "evaluator_default_iterative_orientation_optimization",
85        skip_serializing_if = "is_true"
86    )]
87    pub iterative_orientation_optimization: bool,
88    /// Build one evaluator for the sum of all contributions instead of separate evaluators.
89    #[serde(default, skip_serializing_if = "is_false")]
90    pub summed: bool,
91    /// Sum all orientations, retaining each selected orientation as a separate
92    /// Symbolica function-map alias instead of merging them into one expression.
93    #[serde(default, skip_serializing_if = "is_false")]
94    pub summed_function_map: bool,
95    /// Compile the lowered evaluator using the configured generation compilation backend.
96    #[serde(default, skip_serializing_if = "is_false")]
97    pub compile: bool,
98    /// Retain the intermediate Symbolica atom alongside the executable evaluator.
99    #[serde(default, skip_serializing_if = "is_false")]
100    pub store_atom: bool,
101    /// Apply function-map replacements before numerical lowering.
102    #[serde(default, skip_serializing_if = "is_false")]
103    pub do_fn_map_replacements: bool,
104    /// Inline generated numerator functions instead of retaining shared evaluator bodies.
105    #[serde(
106        default = "evaluator_default_inline_numerator_functions",
107        skip_serializing_if = "is_true"
108    )]
109    pub inline_numerator_functions: bool,
110    /// Translate compatible symbolic expressions directly into Spenso networks.
111    #[serde(
112        default = "evaluator_default_direct_translation",
113        skip_serializing_if = "is_true"
114    )]
115    pub direct_translation: bool,
116    /// Number of Horner-scheme optimization passes applied to scalar expressions.
117    #[serde(
118        default = "evaluator_default_horner_iterations",
119        skip_serializing_if = "is_usize::<1>"
120    )]
121    pub horner_iterations: usize,
122    /// Worker threads used while constructing one evaluator.
123    #[serde(
124        default = "evaluator_default_n_cores",
125        skip_serializing_if = "is_usize::<1>"
126    )]
127    pub n_cores: usize,
128    /// Optional number of common-pair-elimination optimization passes.
129    #[serde(default, skip_serializing_if = "IsDefault::is_default")]
130    pub cpe_iterations: Option<usize>,
131    /// Optimization stage at which evaluator construction aborts for diagnostic isolation.
132    #[serde(default, skip_serializing_if = "IsDefault::is_default")]
133    pub abort_level: usize,
134
135    /// Maximum variable count for attempting a Horner-scheme optimization.
136    #[serde(
137        default = "evaluator_default_max_horner_scheme_variables",
138        skip_serializing_if = "is_usize::<500>"
139    )]
140    pub max_horner_scheme_variables: usize,
141
142    /// Maximum cached candidate pairs during common-pair elimination.
143    #[serde(
144        default = "evaluator_default_max_common_pair_cache_entries",
145        skip_serializing_if = "is_usize::<1000000>"
146    )]
147    pub max_common_pair_cache_entries: usize,
148
149    /// Maximum expression-tree distance considered during common-pair elimination.
150    #[serde(
151        default = "evaluator_default_max_common_pair_distance",
152        skip_serializing_if = "is_usize::<1000>"
153    )]
154    pub max_common_pair_distance: usize,
155    /// Heuristic used to order tensor-network contractions.
156    #[serde(default, skip_serializing_if = "IsDefault::is_default")]
157    pub tensor_network_contraction_order: TensorNetworkContractionOrder,
158    /// Emit detailed evaluator-construction diagnostics.
159    #[serde(default, skip_serializing_if = "is_false")]
160    pub verbose: bool,
161
162    /// Spenso execution and contraction modes used by the lowered tensor network.
163    #[serde(
164        default = "evaluator_default_spenso_execution_mode",
165        skip_serializing_if = "is_default_spenso_execution_mode"
166    )]
167    pub spenso_execution_mode: (ExecutionMode, ContractionMode),
168}
169
170const fn evaluator_default_iterative_orientation_optimization() -> bool {
171    true
172}
173
174const fn evaluator_default_direct_translation() -> bool {
175    true
176}
177
178const fn evaluator_default_inline_numerator_functions() -> bool {
179    true
180}
181
182const fn evaluator_default_horner_iterations() -> usize {
183    1
184}
185
186const fn evaluator_default_n_cores() -> usize {
187    1
188}
189
190const fn evaluator_default_max_horner_scheme_variables() -> usize {
191    500
192}
193
194const fn evaluator_default_max_common_pair_cache_entries() -> usize {
195    1_000_000
196}
197
198const fn evaluator_default_max_common_pair_distance() -> usize {
199    1000
200}
201
202const fn evaluator_default_spenso_execution_mode() -> (ExecutionMode, ContractionMode) {
203    (ExecutionMode::Sequential, ContractionMode::MinResultRank)
204}
205
206fn is_default_spenso_execution_mode(mode: &(ExecutionMode, ContractionMode)) -> bool {
207    show_defaults_helper(mode == &evaluator_default_spenso_execution_mode())
208}
209
210impl Default for EvaluatorSettings {
211    fn default() -> Self {
212        Self {
213            iterative_orientation_optimization:
214                evaluator_default_iterative_orientation_optimization(),
215            summed: false,
216            do_algebra: false,
217            summed_function_map: false,
218            direct_translation: evaluator_default_direct_translation(),
219            compile: false,
220            do_fn_map_replacements: false,
221            inline_numerator_functions: evaluator_default_inline_numerator_functions(),
222            store_atom: false,
223            horner_iterations: evaluator_default_horner_iterations(),
224            n_cores: evaluator_default_n_cores(),
225            cpe_iterations: None,
226            abort_level: 0,
227            max_horner_scheme_variables: evaluator_default_max_horner_scheme_variables(),
228            max_common_pair_cache_entries: evaluator_default_max_common_pair_cache_entries(),
229            max_common_pair_distance: evaluator_default_max_common_pair_distance(),
230            tensor_network_contraction_order: TensorNetworkContractionOrder::default(),
231            verbose: false,
232            spenso_execution_mode: evaluator_default_spenso_execution_mode(),
233        }
234    }
235}
236
237impl EvaluatorSettings {
238    pub fn optimization_settings(&self) -> OptimizationSettings {
239        OptimizationSettings::new()
240            .horner_iterations(self.horner_iterations)
241            .cores(self.n_cores)
242            .cpe_iterations(self.cpe_iterations)
243            .abort_check(Some(Box::new(
244                crate::is_interrupt_requested as fn() -> bool,
245            )))
246            .abort_level(self.abort_level)
247            .max_horner_scheme_variables(self.max_horner_scheme_variables)
248            .max_common_pair_cache_entries(self.max_common_pair_cache_entries)
249            .max_common_pair_distance(self.max_common_pair_distance)
250            .verbose(self.verbose)
251            .direct_translation(self.direct_translation)
252    }
253}
254
255#[derive(Clone, Encode, Decode)]
256#[trait_decode(trait = GammaLoopContext)]
257pub struct ProcessList {
258    // pub amplitude_process: Vec<Process<Amplitude>>
259    // pub cross_section_process: Vec<Process<CrossSection>>
260    pub processes: Vec<Process>,
261}
262
263/// Restricts which saved processes and integrands are loaded into a process list.
264///
265/// An unqualified integrand selector applies to every process containing an
266/// integrand with that name, while a qualified selector applies only to its
267/// named process. Process selectors take precedence and load every integrand
268/// in the selected process.
269#[derive(Debug, Clone, Default, PartialEq, Eq)]
270pub struct ProcessLoadSelection {
271    pub process_names: BTreeSet<String>,
272    pub integrand_selectors: Vec<ProcessLoadIntegrandSelector>,
273}
274
275#[derive(Debug, Clone, PartialEq, Eq)]
276pub enum ProcessLoadIntegrandSelector {
277    Name(String),
278    Qualified {
279        process_name: String,
280        integrand_name: String,
281    },
282}
283
284impl ProcessLoadSelection {
285    pub fn is_unrestricted(&self) -> bool {
286        self.process_names.is_empty() && self.integrand_selectors.is_empty()
287    }
288
289    pub fn selected_integrand_names<'a, I>(
290        &self,
291        process_name: &str,
292        available_names: I,
293    ) -> BTreeSet<String>
294    where
295        I: IntoIterator<Item = &'a str>,
296    {
297        let available_names = available_names.into_iter();
298        if self.process_names.contains(process_name) {
299            return available_names.map(str::to_string).collect();
300        }
301
302        available_names
303            .filter(|integrand_name| {
304                self.integrand_selectors
305                    .iter()
306                    .any(|selector| match selector {
307                        ProcessLoadIntegrandSelector::Name(name) => name == *integrand_name,
308                        ProcessLoadIntegrandSelector::Qualified {
309                            process_name: selected_process,
310                            integrand_name: selected_integrand,
311                        } => {
312                            selected_process == process_name
313                                && selected_integrand == *integrand_name
314                        }
315                    })
316            })
317            .map(str::to_string)
318            .collect()
319    }
320}
321
322#[cfg(test)]
323mod process_load_selection_tests {
324    use super::{ProcessLoadIntegrandSelector, ProcessLoadSelection};
325    use std::collections::BTreeSet;
326
327    #[test]
328    fn process_and_integrand_selectors_are_unioned() {
329        let selection = ProcessLoadSelection {
330            process_names: BTreeSet::from(["all".to_string()]),
331            integrand_selectors: vec![
332                ProcessLoadIntegrandSelector::Name("shared".to_string()),
333                ProcessLoadIntegrandSelector::Qualified {
334                    process_name: "one".to_string(),
335                    integrand_name: "specific".to_string(),
336                },
337            ],
338        };
339
340        assert_eq!(
341            selection.selected_integrand_names("all", ["a", "b"]),
342            BTreeSet::from(["a".to_string(), "b".to_string()])
343        );
344        assert_eq!(
345            selection.selected_integrand_names("one", ["shared", "specific", "other"]),
346            BTreeSet::from(["shared".to_string(), "specific".to_string()])
347        );
348        assert_eq!(
349            selection.selected_integrand_names("two", ["shared", "specific"]),
350            BTreeSet::from(["shared".to_string()])
351        );
352    }
353}
354
355fn validate_process_load_selection(
356    processes_root: &Path,
357    selection: &ProcessLoadSelection,
358) -> Result<()> {
359    let mut available: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
360    for (directory, binary, kind) in [
361        ("amplitudes", "amp.bin", "amplitude"),
362        ("cross_sections", "cs.bin", "cross section"),
363    ] {
364        let root = processes_root.join(directory);
365        if !root.exists() {
366            continue;
367        }
368        for entry in
369            fs::read_dir(&root).with_context(|| format!("Error reading {}", root.display()))?
370        {
371            let entry = entry?;
372            if !entry.file_type()?.is_dir() {
373                continue;
374            }
375            let Some(process_name) = entry.file_name().to_str().map(str::to_owned) else {
376                continue;
377            };
378            let child_dirs = process::saved_child_dirs(&entry.path(), binary, kind)?;
379            let names = available.entry(process_name).or_default();
380            names.extend(child_dirs.into_iter().filter_map(|path| {
381                path.file_name()
382                    .and_then(|name| name.to_str())
383                    .map(str::to_owned)
384            }));
385        }
386    }
387
388    let mut errors = Vec::new();
389
390    for process_name in &selection.process_names {
391        if !available.contains_key(process_name) {
392            errors.push(format!("process '{process_name}' was not found"));
393        }
394    }
395    for selector in &selection.integrand_selectors {
396        match selector {
397            ProcessLoadIntegrandSelector::Name(integrand_name) => {
398                if !available.values().any(|names| names.contains(integrand_name)) {
399                    errors.push(format!("integrand '{integrand_name}' was not found"));
400                }
401            }
402            ProcessLoadIntegrandSelector::Qualified {
403                process_name,
404                integrand_name,
405            } => match available.get(process_name) {
406                None => errors.push(format!(
407                    "process '{process_name}' in selector '{process_name}@{integrand_name}' was not found"
408                )),
409                Some(names) if !names.contains(integrand_name) => errors.push(format!(
410                    "integrand '{integrand_name}' was not found in process '{process_name}'"
411                )),
412                Some(_) => {}
413            },
414        }
415    }
416
417    if errors.is_empty() {
418        return Ok(());
419    }
420
421    Err(eyre!(
422        "Cannot load the requested process/integrand selection: {}. Available integrands by process: {:?}",
423        errors.join("; "),
424        available,
425    ))
426}
427
428/// Controls which graph details and algebraic transformations are included in DOT exports.
429#[derive(Serialize, Deserialize, Debug, Clone)]
430#[cfg_attr(feature = "python-stubs", pyo3_stub_gen::derive::gen_stub_pyclass)]
431#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
432pub struct DotExportSettings {
433    // pub root_folder: PathBuf,
434    /// Write all diagrams of an integrand to one file during filesystem export.
435    pub combine_diagrams: bool,
436    /// Request UV counterterm graph output; currently retained for compatibility.
437    pub with_uv: bool,
438    /// Add the complete symbolic numerator as the graph-level `full_num` field.
439    pub output_full_numerator: bool,
440    /// Split cross-section graphs by distinct initial-state assignments.
441    pub split_xs_by_initial_states: bool,
442    /// Simplify gamma-matrix algebra in the full numerator before formatting it.
443    pub do_gamma_algebra: bool,
444    /// Simplify color algebra in the full numerator before formatting it.
445    pub do_color_algebra: bool,
446    #[serde(default, skip_serializing_if = "is_false")]
447    /// Include vertex and edge fields whose values GammaLoop generated automatically.
448    pub include_autogenerated_fields: bool,
449}
450
451#[derive(Serialize, Deserialize, Debug, Clone)]
452#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
453pub struct StandaloneExportSettings {
454    #[serde(default)]
455    pub mode: StandaloneExportMode,
456    #[serde(default)]
457    pub format: StandaloneDataFormat,
458    #[serde(default)]
459    pub precision: StandaloneNumericTarget,
460}
461
462#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
463#[derive(
464    Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode, JsonSchema,
465)]
466#[serde(rename_all = "snake_case")]
467pub enum StandaloneExportMode {
468    #[default]
469    Rust,
470    Python,
471}
472
473#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
474#[derive(
475    Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode, JsonSchema,
476)]
477#[serde(rename_all = "snake_case")]
478pub enum StandaloneDataFormat {
479    #[default]
480    Binary,
481    Json,
482}
483
484#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
485#[derive(
486    Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode, JsonSchema,
487)]
488#[serde(rename_all = "snake_case")]
489pub enum StandaloneNumericTarget {
490    #[default]
491    Double,
492    Quad,
493    Arb,
494}
495
496#[cfg(feature = "python")]
497#[cfg_attr(feature = "python-stubs", pyo3_stub_gen::derive::gen_stub_pymethods)]
498#[cfg_attr(feature = "python", pyo3::pymethods)]
499impl DotExportSettings {
500    /// Construct DOT export settings with GammaLoop's defaults.
501    #[new]
502    fn new() -> Self {
503        DotExportSettings::default()
504    }
505}
506
507impl Default for DotExportSettings {
508    fn default() -> Self {
509        Self {
510            // root_folder: PathBuf::from("."),
511            split_xs_by_initial_states: false,
512            output_full_numerator: false,
513            do_gamma_algebra: false,
514            do_color_algebra: true,
515            combine_diagrams: false,
516            with_uv: false,
517            include_autogenerated_fields: false,
518        }
519    }
520}
521
522impl Default for StandaloneExportSettings {
523    fn default() -> Self {
524        Self {
525            format: StandaloneDataFormat::Binary,
526            mode: StandaloneExportMode::Rust,
527            precision: StandaloneNumericTarget::Double,
528        }
529    }
530}
531
532impl Default for ProcessList {
533    fn default() -> Self {
534        Self::new()
535    }
536}
537
538// Python Api
539impl ProcessList {
540    /// Generates a new empty process list
541    pub fn new() -> Self {
542        ProcessList { processes: vec![] }
543    }
544
545    pub fn warm_up_all_processes(&mut self, model: &Model) -> Result<()> {
546        for process in &mut self.processes.iter_mut() {
547            process.warm_up(model)?;
548        }
549
550        Ok(())
551    }
552
553    pub fn load(path: impl AsRef<Path>, context: GammaLoopContextContainer) -> Result<Self> {
554        Self::load_with_selection(path, context, None)
555    }
556
557    pub fn load_with_selection(
558        path: impl AsRef<Path>,
559        context: GammaLoopContextContainer,
560        selection: Option<&ProcessLoadSelection>,
561    ) -> Result<Self> {
562        let mut process_list = Self::new();
563        let selection = selection.filter(|selection| !selection.is_unrestricted());
564
565        let path = path.as_ref().join("processes");
566        if let Some(selection) = selection {
567            validate_process_load_selection(&path, selection)?;
568        }
569        let amplitudes_path = path.join("amplitudes");
570        if amplitudes_path.exists() {
571            debug!("Looking for amplitudes in {}", amplitudes_path.display());
572
573            for entry in fs::read_dir(amplitudes_path).context("Error reading dir")? {
574                let Ok(entry) = entry else {
575                    debug!("Skipping invalid entry");
576
577                    continue;
578                };
579                let path = entry.path();
580                let process_name = path.file_name().and_then(|name| name.to_str());
581                let process_selected = selection.is_some_and(|selection| {
582                    process_name.is_some_and(|name| selection.process_names.contains(name))
583                });
584                let selected_integrands = match (selection, process_name) {
585                    (Some(selection), Some(process_name)) => {
586                        let available = process::saved_child_dirs(&path, "amp.bin", "amplitude")?;
587                        Some(
588                            selection.selected_integrand_names(
589                                process_name,
590                                available.iter().filter_map(|path| {
591                                    path.file_name().and_then(|name| name.to_str())
592                                }),
593                            ),
594                        )
595                    }
596                    _ => None,
597                };
598                if selected_integrands.as_ref().is_some_and(BTreeSet::is_empty) && !process_selected
599                {
600                    continue;
601                }
602                let process = Process::load_amplitude(path, context, selected_integrands.as_ref())
603                    .context("Error loading amplitude")?;
604                process_list.processes.push(process);
605            }
606        }
607
608        let cross_sections_path = path.join("cross_sections");
609        if cross_sections_path.exists() {
610            debug!(
611                "Looking for cross sections in {}",
612                cross_sections_path.display()
613            );
614
615            for entry in fs::read_dir(cross_sections_path)? {
616                let entry = entry?;
617                let path = entry.path();
618                let process_name = path.file_name().and_then(|name| name.to_str());
619                let process_selected = selection.is_some_and(|selection| {
620                    process_name.is_some_and(|name| selection.process_names.contains(name))
621                });
622                let selected_integrands =
623                    match (selection, process_name) {
624                        (Some(selection), Some(process_name)) => {
625                            let available =
626                                process::saved_child_dirs(&path, "cs.bin", "cross section")?;
627                            Some(selection.selected_integrand_names(
628                                process_name,
629                                available.iter().filter_map(|path| {
630                                    path.file_name().and_then(|name| name.to_str())
631                                }),
632                            ))
633                        }
634                        _ => None,
635                    };
636                if selected_integrands.as_ref().is_some_and(BTreeSet::is_empty) && !process_selected
637                {
638                    continue;
639                }
640                let process =
641                    Process::load_cross_section(path, context, selected_integrands.as_ref())?;
642                process_list.processes.push(process);
643            }
644        }
645        process_list
646            .processes
647            .sort_by_key(|p| p.definition.process_id);
648
649        if selection.is_some() {
650            for (process_id, process) in process_list.processes.iter_mut().enumerate() {
651                process.definition.process_id = process_id;
652            }
653        }
654
655        Ok(process_list)
656    }
657
658    pub fn save(&mut self, folder: impl AsRef<Path>, override_existing: bool) -> Result<()> {
659        let path = folder.as_ref().join("processes");
660
661        let r = fs::create_dir_all(&path);
662        if !override_existing {
663            r?;
664        }
665
666        for p in self.processes.iter_mut() {
667            p.save(&path, override_existing)?;
668        }
669
670        Ok(())
671    }
672
673    pub fn compile(
674        &mut self,
675        folder: impl AsRef<Path>,
676        override_existing: bool,
677        process_id: Option<usize>,
678        integrand_name: Option<String>,
679        thread_pool: &ThreadPool,
680    ) -> Result<Vec<GeneratedGraphReport>> {
681        let path = folder.as_ref().join("processes");
682
683        let r = fs::create_dir_all(&path);
684        if !override_existing {
685            r?;
686        }
687
688        let mut reports = Vec::new();
689        for p in self.processes.iter_mut() {
690            if let Some(id) = process_id
691                && p.definition.process_id != id
692            {
693                continue;
694            }
695            reports.extend(p.compile(
696                &path,
697                override_existing,
698                integrand_name.clone(),
699                thread_pool,
700            )?);
701        }
702
703        Ok(reports)
704    }
705
706    pub fn activate_loaded_integrand_backends(
707        &mut self,
708        allow_symjit_fallback: bool,
709    ) -> Result<()> {
710        for process in &mut self.processes {
711            process.activate_loaded_integrand_backends(allow_symjit_fallback)?;
712        }
713        Ok(())
714    }
715
716    pub fn get_integrand(
717        &self,
718        process_id: usize,
719        integrand_name: impl AsRef<str>,
720    ) -> Result<crate::processes::process::ResolvedIntegrandRef<'_>> {
721        let process = &self.processes[process_id];
722        process.get_integrand(integrand_name)
723    }
724
725    pub fn get_integrand_mut(
726        &mut self,
727        process_id: usize,
728        integrand_name: impl AsRef<str>,
729    ) -> Result<&mut crate::integrands::process::ProcessIntegrand> {
730        let process = &mut self.processes[process_id];
731        process.get_integrand_mut(integrand_name)
732    }
733
734    pub fn export_dot(&self, path: impl AsRef<Path>, settings: &DotExportSettings) -> Result<()> {
735        let path = path.as_ref().join("processes");
736        fs::create_dir_all(&path)?;
737
738        for p in self.processes.iter() {
739            p.export_dot(&path, settings)?;
740        }
741        Ok(())
742    }
743
744    pub fn export_uv_forests(
745        &self,
746        path: impl AsRef<Path>,
747        process_id: usize,
748        integrand_name: &str,
749        graph_ids: &[usize],
750        settings: &UVForestExportSettings,
751    ) -> Result<()> {
752        let path = path.as_ref().join("processes");
753        fs::create_dir_all(&path)?;
754        let process = self.processes.get(process_id).ok_or_else(|| {
755            eyre!(
756                "Process id {} is out of range; there are {} processes",
757                process_id,
758                self.processes.len()
759            )
760        })?;
761        process.export_uv_forests(&path, integrand_name, graph_ids, settings)
762    }
763
764    pub fn export_standalone(
765        &self,
766        path: impl AsRef<Path>,
767        settings: &StandaloneExportSettings,
768    ) -> Result<()> {
769        let path = path.as_ref().join("processes");
770        fs::create_dir_all(&path)?;
771
772        for p in self.processes.iter() {
773            p.export_standalone(&path, settings)?;
774        }
775        Ok(())
776    }
777
778    pub fn add_process(&mut self, process: Process) {
779        self.processes.push(process);
780    }
781
782    ///preprocesses the process list according to the settings
783    pub fn preprocess(
784        &mut self,
785        model: &Model,
786        settings: &GlobalSettings,
787        locked_runtime_settings: &LockedRuntimeSettings,
788        thread_pool: &ThreadPool,
789    ) -> Result<Vec<GeneratedGraphReport>> {
790        // Reject unsupported settings before any process changes its graphs or history.
791        for process in &self.processes {
792            settings
793                .generation
794                .validate_for_process(process.definition.generation_type)?;
795        }
796        let mut reports = Vec::new();
797        for process in self.processes.iter_mut() {
798            reports.extend(process.preprocess(
799                model,
800                settings,
801                locked_runtime_settings,
802                thread_pool,
803            )?);
804        }
805        Ok(reports)
806    }
807
808    pub fn generate_integrands(
809        &mut self,
810        model: &Model,
811        global_settings: &GlobalSettings,
812        runtime_default: LockedRuntimeSettings,
813        thread_pool: &ThreadPool,
814    ) -> Result<Vec<GeneratedGraphReport>> {
815        let mut reports = Vec::new();
816        for process in &mut self.processes {
817            reports.extend(process.generate_integrands(
818                model,
819                global_settings,
820                runtime_default,
821                thread_pool,
822            )?);
823        }
824        Ok(reports)
825    }
826
827    pub fn find_process(&self, process_id: Option<usize>) -> Result<usize> {
828        if let Some(id) = process_id {
829            if id >= self.processes.len() {
830                return Err(color_eyre::eyre::eyre!(
831                    "Invalid process id {}. Number of processes: {}",
832                    id,
833                    self.processes.len()
834                ));
835            }
836            Ok(id)
837        } else if self.processes.is_empty() {
838            Err(color_eyre::eyre::eyre!("No processes generated yet."))
839        } else if self.processes.len() > 1 {
840            Err(color_eyre::eyre::eyre!(
841                "There are {} processes available. Please specify a process.",
842                self.processes.len()
843            ))
844        } else {
845            Ok(0)
846        }
847    }
848
849    pub fn find_integrand(
850        &self,
851        process_id: Option<usize>,
852        integrand_name: Option<&String>,
853    ) -> Result<(usize, String)> {
854        let p_id = self.find_process(process_id)?;
855        let integrand_name = self.processes[p_id]
856            .collection
857            .find_integrand(integrand_name.cloned())?;
858        Ok((p_id, integrand_name))
859    }
860}
861
862pub mod process;
863pub use process::*;
864pub mod amplitude;
865pub use amplitude::*;
866pub mod cross_section;
867pub use cross_section::*;
868pub mod threshold_counterterms;
869pub use threshold_counterterms::*;
870
871#[cfg(test)]
872mod no_directive_equivalence_tests;
873#[cfg(test)]
874mod raised_cross_section_tests;
875
876#[cfg(test)]
877mod tests {
878    use symbolica::state::State;
879
880    use crate::{
881        GammaLoopContextContainer, dot,
882        graph::{Graph, parse::IntoGraph},
883        initialisation::test_initialise,
884        settings::{
885            RuntimeSettings,
886            global::{
887                CompilationMode, CompilationOptimizationLevel, GammaloopCompileOptions,
888                GenerationSettings, TropicalSubgraphTableSettings,
889            },
890            runtime::LockedRuntimeSettings,
891        },
892        utils::load_generic_model,
893    };
894
895    use super::{AmplitudeGraph, EvaluatorSettings};
896
897    #[test]
898    fn numerator_inlining_defaults_and_explicit_opt_out_round_trip() {
899        use crate::utils::serde_utils::ShowDefaultsGuard;
900
901        assert!(EvaluatorSettings::default().inline_numerator_functions);
902        for input in ["", "n_cores = 2"] {
903            let settings: EvaluatorSettings = toml::from_str(input).unwrap();
904            assert!(settings.inline_numerator_functions);
905        }
906
907        {
908            let _guard = ShowDefaultsGuard::new(false);
909            let settings = EvaluatorSettings::default();
910            let serialized = toml::to_string(&settings).unwrap();
911            assert!(!serialized.contains("inline_numerator_functions"));
912            assert_eq!(
913                toml::from_str::<EvaluatorSettings>(&serialized).unwrap(),
914                settings
915            );
916
917            let settings: EvaluatorSettings =
918                toml::from_str("inline_numerator_functions = false").unwrap();
919            let serialized = toml::to_string(&settings).unwrap();
920            assert!(serialized.contains("inline_numerator_functions = false"));
921            assert_eq!(
922                toml::from_str::<EvaluatorSettings>(&serialized).unwrap(),
923                settings
924            );
925        }
926
927        let _guard = ShowDefaultsGuard::new(true);
928        let serialized = toml::to_string(&EvaluatorSettings::default()).unwrap();
929        assert!(serialized.contains("inline_numerator_functions = true"));
930    }
931
932    #[test]
933    fn test_encode_decode_amplitude_graph() {
934        test_initialise().unwrap();
935        // Use the model routing produced by graph import for the persistence roundtrip.
936        let model = load_generic_model("sm");
937
938        let graph: Graph = dot!(
939            digraph G{
940                e1      [style=invis]
941                e2      [style=invis]
942                e1 -> n1  [particle=H]
943                n4 -> e2    [particle=H]
944                n1 -> n2    [particle=H]
945                n1 -> n3    [particle=H]
946                n2 -> n3    [particle=t]
947                n3 -> n4    [particle=t]
948                n4 -> n2    [particle=t]
949            }
950        )
951        .unwrap();
952        let mut amplitude: AmplitudeGraph = AmplitudeGraph::new(graph.clone());
953
954        amplitude
955            .preprocess(
956                &model,
957                &GenerationSettings {
958                    compile: GammaloopCompileOptions {
959                        compilation_mode: CompilationMode::Cpp,
960                        jit_direct_translation: true,
961                        fast_math: false,
962                        optimization_level: CompilationOptimizationLevel::O0,
963                        unsafe_math: false,
964                        compiler: crate::settings::global::default_external_compiler().to_owned(),
965                        custom: Vec::new(),
966                    },
967                    tropical_subgraph_table: TropicalSubgraphTableSettings {
968                        panic_on_fail: false,
969                        target_omega: 1.0,
970                        ..Default::default()
971                    },
972                    ..Default::default()
973                },
974                &LockedRuntimeSettings::from(&RuntimeSettings::default()),
975            )
976            .unwrap();
977
978        let mut symbols = Vec::new();
979        State::export(&mut symbols).unwrap();
980        let state_map = State::import(&mut std::io::Cursor::new(symbols), None).unwrap();
981
982        let context = GammaLoopContextContainer {
983            model: &model,
984            state_map: &state_map,
985        };
986
987        let encoded_amplitude =
988            bincode::encode_to_vec(&amplitude, bincode::config::standard()).unwrap();
989
990        let decoded: AmplitudeGraph = bincode::decode_from_slice_with_context(
991            &encoded_amplitude,
992            bincode::config::standard(),
993            context,
994        )
995        .expect("amplitude decode failed")
996        .0;
997
998        assert_eq!(decoded.graph.name, amplitude.graph.name);
999        assert_eq!(decoded.graph.overall_factor, amplitude.graph.overall_factor);
1000        assert_eq!(
1001            decoded.graph.loop_momentum_basis,
1002            amplitude.graph.loop_momentum_basis
1003        );
1004        assert_eq!(
1005            decoded.graph.param_builder.reps,
1006            amplitude.graph.param_builder.reps
1007        );
1008        assert_eq!(
1009            decoded.graph.param_builder.values,
1010            amplitude.graph.param_builder.values
1011        );
1012        assert_eq!(
1013            decoded.derived_data.all_mighty_integrand,
1014            amplitude.derived_data.all_mighty_integrand
1015        );
1016        assert_eq!(
1017            decoded.derived_data.all_mighty_numerators,
1018            amplitude.derived_data.all_mighty_numerators
1019        );
1020    }
1021}