1use std::{
2 collections::{BTreeMap, BTreeSet},
3 fs,
4 path::Path,
5};
6
7use 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 #[serde(default, skip_serializing_if = "is_false")]
81 pub do_algebra: bool,
82 #[serde(
84 default = "evaluator_default_iterative_orientation_optimization",
85 skip_serializing_if = "is_true"
86 )]
87 pub iterative_orientation_optimization: bool,
88 #[serde(default, skip_serializing_if = "is_false")]
90 pub summed: bool,
91 #[serde(default, skip_serializing_if = "is_false")]
94 pub summed_function_map: bool,
95 #[serde(default, skip_serializing_if = "is_false")]
97 pub compile: bool,
98 #[serde(default, skip_serializing_if = "is_false")]
100 pub store_atom: bool,
101 #[serde(default, skip_serializing_if = "is_false")]
103 pub do_fn_map_replacements: bool,
104 #[serde(
106 default = "evaluator_default_inline_numerator_functions",
107 skip_serializing_if = "is_true"
108 )]
109 pub inline_numerator_functions: bool,
110 #[serde(
112 default = "evaluator_default_direct_translation",
113 skip_serializing_if = "is_true"
114 )]
115 pub direct_translation: bool,
116 #[serde(
118 default = "evaluator_default_horner_iterations",
119 skip_serializing_if = "is_usize::<1>"
120 )]
121 pub horner_iterations: usize,
122 #[serde(
124 default = "evaluator_default_n_cores",
125 skip_serializing_if = "is_usize::<1>"
126 )]
127 pub n_cores: usize,
128 #[serde(default, skip_serializing_if = "IsDefault::is_default")]
130 pub cpe_iterations: Option<usize>,
131 #[serde(default, skip_serializing_if = "IsDefault::is_default")]
133 pub abort_level: usize,
134
135 #[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 #[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 #[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 #[serde(default, skip_serializing_if = "IsDefault::is_default")]
157 pub tensor_network_contraction_order: TensorNetworkContractionOrder,
158 #[serde(default, skip_serializing_if = "is_false")]
160 pub verbose: bool,
161
162 #[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 processes: Vec<Process>,
261}
262
263#[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#[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 combine_diagrams: bool,
436 pub with_uv: bool,
438 pub output_full_numerator: bool,
440 pub split_xs_by_initial_states: bool,
442 pub do_gamma_algebra: bool,
444 pub do_color_algebra: bool,
446 #[serde(default, skip_serializing_if = "is_false")]
447 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 #[new]
502 fn new() -> Self {
503 DotExportSettings::default()
504 }
505}
506
507impl Default for DotExportSettings {
508 fn default() -> Self {
509 Self {
510 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
538impl ProcessList {
540 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 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 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 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(&litude, 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}