1use std::{fs, path::Path};
2
3use bincode_trait_derive::{Decode, Encode};
5use color_eyre::Result;
6use eyre::{Context, eyre};
7use rayon::ThreadPool;
8use schemars::JsonSchema;
9use symbolica::evaluate::OptimizationSettings;
10use tracing::debug;
11
12use crate::{
13 GammaLoopContext, GammaLoopContextContainer,
14 settings::{GlobalSettings, runtime::LockedRuntimeSettings},
15 utils::serde_utils::{IsDefault, is_false, is_true, is_usize, show_defaults_helper},
16 uv::export::UVForestExportSettings,
17};
18use serde::{Deserialize, Serialize};
19
20use crate::model::Model;
21
22mod generation_report;
23pub use generation_report::{
24 EvaluatorBuildTimings, GeneratedGraphKey, GeneratedGraphReport, GraphGenerationStats,
25 NamedGraphGenerationReport, merge_generated_graph_reports,
26};
27mod generation_progress;
28pub use generation_progress::{
29 GenerationProcessKind, GenerationProgressMode, GenerationProgressModeGuard,
30 GenerationProgressObserver, GenerationProgressObserverGuard, GenerationProgressPhase,
31 begin_phase, cut_finished, detailed_progress_enabled, detailed_progress_message,
32 enter_detailed_progress_span, enter_progress_context,
33};
34mod selection;
35pub use selection::{
36 CycleSignature, GraphGroupSelectionMode, GraphGroupSelectionPlan, GraphGroupSelectionReport,
37 GraphGroupSelectionSpec, GraphSelectionSignatureInventory, ParticleSignature,
38 RaisedCutSignatureInventory, RaisedPropagatorScope, RaisedPropagatorSignature,
39 SelectionPolarity, VertexSignature,
40};
41pub(crate) use selection::{GraphCutSelectionSubject, GraphSelectionSubject};
42
43#[cfg_attr(feature = "python_api", pyo3::pyclass(from_py_object))]
44#[derive(
45 Debug, Clone, Copy, Default, Serialize, Deserialize, Encode, Decode, PartialEq, Eq, JsonSchema,
46)]
47#[serde(rename_all = "snake_case")]
48pub enum TensorNetworkContractionOrder {
49 #[default]
50 SparseAtomAware,
51 AtomAware,
52 ResultRankOnly,
53 EntryAware,
54}
55
56#[cfg_attr(feature = "python_api", pyo3::pyclass(from_py_object))]
57#[derive(Debug, Clone, Copy, Serialize, Deserialize, Encode, Decode, PartialEq, JsonSchema)]
58pub enum ExecutionMode {
59 Sequential,
60 Parallel,
61 SequentialRef,
62 SequentialExtract,
63}
64
65#[cfg_attr(feature = "python_api", pyo3::pyclass(from_py_object))]
66#[derive(Debug, Clone, Copy, Serialize, Deserialize, Encode, Decode, PartialEq, JsonSchema)]
67pub enum ContractionMode {
68 SmallestDegree,
69 MinResultRank,
70}
71#[cfg_attr(feature = "python_api", pyo3::pyclass(from_py_object))]
72#[derive(Debug, Clone, Copy, Serialize, Deserialize, Encode, Decode, PartialEq, JsonSchema)]
73pub struct EvaluatorSettings {
74 #[serde(default, skip_serializing_if = "is_false")]
76 pub do_algebra: bool,
77 #[serde(
79 default = "evaluator_default_iterative_orientation_optimization",
80 skip_serializing_if = "is_true"
81 )]
82 pub iterative_orientation_optimization: bool,
83 #[serde(default, skip_serializing_if = "is_false")]
85 pub summed: bool,
86 #[serde(default, skip_serializing_if = "is_false")]
88 pub summed_function_map: bool,
89 #[serde(default, skip_serializing_if = "is_false")]
91 pub compile: bool,
92 #[serde(default, skip_serializing_if = "is_false")]
94 pub store_atom: bool,
95 #[serde(default, skip_serializing_if = "is_false")]
97 pub do_fn_map_replacements: bool,
98 #[serde(
100 default = "evaluator_default_direct_translation",
101 skip_serializing_if = "is_true"
102 )]
103 pub direct_translation: bool,
104 #[serde(
106 default = "evaluator_default_horner_iterations",
107 skip_serializing_if = "is_usize::<1>"
108 )]
109 pub horner_iterations: usize,
110 #[serde(
112 default = "evaluator_default_n_cores",
113 skip_serializing_if = "is_usize::<1>"
114 )]
115 pub n_cores: usize,
116 #[serde(default, skip_serializing_if = "IsDefault::is_default")]
118 pub cpe_iterations: Option<usize>,
119 #[serde(default, skip_serializing_if = "IsDefault::is_default")]
121 pub abort_level: usize,
122
123 #[serde(
125 default = "evaluator_default_max_horner_scheme_variables",
126 skip_serializing_if = "is_usize::<500>"
127 )]
128 pub max_horner_scheme_variables: usize,
129
130 #[serde(
132 default = "evaluator_default_max_common_pair_cache_entries",
133 skip_serializing_if = "is_usize::<1000000>"
134 )]
135 pub max_common_pair_cache_entries: usize,
136
137 #[serde(
139 default = "evaluator_default_max_common_pair_distance",
140 skip_serializing_if = "is_usize::<1000>"
141 )]
142 pub max_common_pair_distance: usize,
143 #[serde(default, skip_serializing_if = "IsDefault::is_default")]
145 pub tensor_network_contraction_order: TensorNetworkContractionOrder,
146 #[serde(default, skip_serializing_if = "is_false")]
148 pub verbose: bool,
149
150 #[serde(
152 default = "evaluator_default_spenso_execution_mode",
153 skip_serializing_if = "is_default_spenso_execution_mode"
154 )]
155 pub spenso_execution_mode: (ExecutionMode, ContractionMode),
156}
157
158const fn evaluator_default_iterative_orientation_optimization() -> bool {
159 true
160}
161
162const fn evaluator_default_direct_translation() -> bool {
163 true
164}
165
166const fn evaluator_default_horner_iterations() -> usize {
167 1
168}
169
170const fn evaluator_default_n_cores() -> usize {
171 1
172}
173
174const fn evaluator_default_max_horner_scheme_variables() -> usize {
175 500
176}
177
178const fn evaluator_default_max_common_pair_cache_entries() -> usize {
179 1_000_000
180}
181
182const fn evaluator_default_max_common_pair_distance() -> usize {
183 1000
184}
185
186const fn evaluator_default_spenso_execution_mode() -> (ExecutionMode, ContractionMode) {
187 (ExecutionMode::Sequential, ContractionMode::MinResultRank)
188}
189
190fn is_default_spenso_execution_mode(mode: &(ExecutionMode, ContractionMode)) -> bool {
191 show_defaults_helper(mode == &evaluator_default_spenso_execution_mode())
192}
193
194impl Default for EvaluatorSettings {
195 fn default() -> Self {
196 Self {
197 iterative_orientation_optimization:
198 evaluator_default_iterative_orientation_optimization(),
199 summed: false,
200 do_algebra: false,
201 summed_function_map: false,
202 direct_translation: evaluator_default_direct_translation(),
203 compile: false,
204 do_fn_map_replacements: false,
205 store_atom: false,
206 horner_iterations: evaluator_default_horner_iterations(),
207 n_cores: evaluator_default_n_cores(),
208 cpe_iterations: None,
209 abort_level: 0,
210 max_horner_scheme_variables: evaluator_default_max_horner_scheme_variables(),
211 max_common_pair_cache_entries: evaluator_default_max_common_pair_cache_entries(),
212 max_common_pair_distance: evaluator_default_max_common_pair_distance(),
213 tensor_network_contraction_order: TensorNetworkContractionOrder::default(),
214 verbose: false,
215 spenso_execution_mode: evaluator_default_spenso_execution_mode(),
216 }
217 }
218}
219
220impl EvaluatorSettings {
221 pub fn optimization_settings(&self) -> OptimizationSettings {
222 OptimizationSettings::new()
223 .horner_iterations(self.horner_iterations)
224 .cores(self.n_cores)
225 .cpe_iterations(self.cpe_iterations)
226 .abort_check(Some(Box::new(
227 crate::is_interrupt_requested as fn() -> bool,
228 )))
229 .abort_level(self.abort_level)
230 .max_horner_scheme_variables(self.max_horner_scheme_variables)
231 .max_common_pair_cache_entries(self.max_common_pair_cache_entries)
232 .max_common_pair_distance(self.max_common_pair_distance)
233 .verbose(self.verbose)
234 .direct_translation(self.direct_translation)
235 }
236}
237
238#[derive(Clone, Encode, Decode)]
239#[trait_decode(trait = GammaLoopContext)]
240pub struct ProcessList {
241 pub processes: Vec<Process>,
244}
245
246#[derive(Serialize, Deserialize, Debug, Clone)]
248#[cfg_attr(feature = "python_stubgen", pyo3_stub_gen::derive::gen_stub_pyclass)]
249#[cfg_attr(
250 feature = "python_api",
251 pyo3::pyclass(from_py_object, get_all, set_all)
252)]
253pub struct DotExportSettings {
254 pub combine_diagrams: bool,
257 pub with_uv: bool,
259 pub output_full_numerator: bool,
261 pub split_xs_by_initial_states: bool,
263 pub do_gamma_algebra: bool,
265 pub do_color_algebra: bool,
267 #[serde(default, skip_serializing_if = "is_false")]
268 pub include_autogenerated_fields: bool,
270}
271
272#[derive(Serialize, Deserialize, Debug, Clone)]
273#[cfg_attr(
274 feature = "python_api",
275 pyo3::pyclass(from_py_object, get_all, set_all)
276)]
277pub struct StandaloneExportSettings {
278 #[serde(default)]
279 pub mode: StandaloneExportMode,
280 #[serde(default)]
281 pub format: StandaloneDataFormat,
282 #[serde(default)]
283 pub precision: StandaloneNumericTarget,
284}
285
286#[cfg_attr(feature = "python_api", pyo3::pyclass(from_py_object))]
287#[derive(
288 Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode, JsonSchema,
289)]
290#[serde(rename_all = "snake_case")]
291pub enum StandaloneExportMode {
292 #[default]
293 Rust,
294 Python,
295}
296
297#[cfg_attr(feature = "python_api", pyo3::pyclass(from_py_object))]
298#[derive(
299 Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode, JsonSchema,
300)]
301#[serde(rename_all = "snake_case")]
302pub enum StandaloneDataFormat {
303 #[default]
304 Binary,
305 Json,
306}
307
308#[cfg_attr(feature = "python_api", pyo3::pyclass(from_py_object))]
309#[derive(
310 Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode, JsonSchema,
311)]
312#[serde(rename_all = "snake_case")]
313pub enum StandaloneNumericTarget {
314 #[default]
315 Double,
316 Quad,
317 Arb,
318}
319
320#[cfg(feature = "python_api")]
321#[cfg_attr(feature = "python_stubgen", pyo3_stub_gen::derive::gen_stub_pymethods)]
322#[cfg_attr(feature = "python_api", pyo3::pymethods)]
323impl DotExportSettings {
324 #[new]
326 fn new() -> Self {
327 DotExportSettings::default()
328 }
329}
330
331impl Default for DotExportSettings {
332 fn default() -> Self {
333 Self {
334 split_xs_by_initial_states: false,
336 output_full_numerator: false,
337 do_gamma_algebra: false,
338 do_color_algebra: true,
339 combine_diagrams: false,
340 with_uv: false,
341 include_autogenerated_fields: false,
342 }
343 }
344}
345
346impl Default for StandaloneExportSettings {
347 fn default() -> Self {
348 Self {
349 format: StandaloneDataFormat::Binary,
350 mode: StandaloneExportMode::Rust,
351 precision: StandaloneNumericTarget::Double,
352 }
353 }
354}
355
356impl Default for ProcessList {
357 fn default() -> Self {
358 Self::new()
359 }
360}
361
362impl ProcessList {
364 pub fn new() -> Self {
366 ProcessList { processes: vec![] }
367 }
368
369 pub fn warm_up_all_processes(&mut self, model: &Model) -> Result<()> {
370 for process in &mut self.processes.iter_mut() {
371 process.warm_up(model)?;
372 }
373
374 Ok(())
375 }
376
377 pub fn load(path: impl AsRef<Path>, context: GammaLoopContextContainer) -> Result<Self> {
378 let mut process_list = Self::new();
379
380 let path = path.as_ref().join("processes");
381 let amplitudes_path = path.join("amplitudes");
382 if amplitudes_path.exists() {
383 debug!("Looking for amplitudes in {}", amplitudes_path.display());
384
385 for entry in fs::read_dir(amplitudes_path).context("Error reading dir")? {
386 let Ok(entry) = entry else {
387 debug!("Skipping invalid entry");
388
389 continue;
390 };
391 let path = entry.path();
392 process_list.processes.push(
393 Process::load_amplitude(path, context).context("Error loading amplitude")?,
394 );
395 }
396 }
397
398 let cross_sections_path = path.join("cross_sections");
399 if cross_sections_path.exists() {
400 debug!(
401 "Looking for cross sections in {}",
402 cross_sections_path.display()
403 );
404
405 for entry in fs::read_dir(cross_sections_path)? {
406 let entry = entry?;
407 let path = entry.path();
408 process_list
409 .processes
410 .push(Process::load_cross_section(path, context)?);
411 }
412 }
413 process_list
414 .processes
415 .sort_by_key(|p| p.definition.process_id);
416
417 Ok(process_list)
418 }
419
420 pub fn save(&mut self, folder: impl AsRef<Path>, override_existing: bool) -> Result<()> {
421 let path = folder.as_ref().join("processes");
422
423 let r = fs::create_dir_all(&path);
424 if !override_existing {
425 r?;
426 }
427
428 for p in self.processes.iter_mut() {
429 p.save(&path, override_existing)?;
430 }
431
432 Ok(())
433 }
434
435 pub fn compile(
436 &mut self,
437 folder: impl AsRef<Path>,
438 override_existing: bool,
439 process_id: Option<usize>,
440 integrand_name: Option<String>,
441 thread_pool: &ThreadPool,
442 ) -> Result<Vec<GeneratedGraphReport>> {
443 let path = folder.as_ref().join("processes");
444
445 let r = fs::create_dir_all(&path);
446 if !override_existing {
447 r?;
448 }
449
450 let mut reports = Vec::new();
451 for p in self.processes.iter_mut() {
452 if let Some(id) = process_id
453 && p.definition.process_id != id
454 {
455 continue;
456 }
457 reports.extend(p.compile(
458 &path,
459 override_existing,
460 integrand_name.clone(),
461 thread_pool,
462 )?);
463 }
464
465 Ok(reports)
466 }
467
468 pub fn activate_loaded_integrand_backends(
469 &mut self,
470 allow_symjit_fallback: bool,
471 ) -> Result<()> {
472 for process in &mut self.processes {
473 process.activate_loaded_integrand_backends(allow_symjit_fallback)?;
474 }
475 Ok(())
476 }
477
478 pub fn get_integrand(
479 &self,
480 process_id: usize,
481 integrand_name: impl AsRef<str>,
482 ) -> Result<crate::processes::process::ResolvedIntegrandRef<'_>> {
483 let process = &self.processes[process_id];
484 process.get_integrand(integrand_name)
485 }
486
487 pub fn get_integrand_mut(
488 &mut self,
489 process_id: usize,
490 integrand_name: impl AsRef<str>,
491 ) -> Result<&mut crate::integrands::process::ProcessIntegrand> {
492 let process = &mut self.processes[process_id];
493 process.get_integrand_mut(integrand_name)
494 }
495
496 pub fn export_dot(&self, path: impl AsRef<Path>, settings: &DotExportSettings) -> Result<()> {
497 let path = path.as_ref().join("processes");
498 fs::create_dir_all(&path)?;
499
500 for p in self.processes.iter() {
501 p.export_dot(&path, settings)?;
502 }
503 Ok(())
504 }
505
506 pub fn export_uv_forests(
507 &self,
508 path: impl AsRef<Path>,
509 process_id: usize,
510 integrand_name: &str,
511 graph_ids: &[usize],
512 settings: &UVForestExportSettings,
513 ) -> Result<()> {
514 let path = path.as_ref().join("processes");
515 fs::create_dir_all(&path)?;
516 let process = self.processes.get(process_id).ok_or_else(|| {
517 eyre!(
518 "Process id {} is out of range; there are {} processes",
519 process_id,
520 self.processes.len()
521 )
522 })?;
523 process.export_uv_forests(&path, integrand_name, graph_ids, settings)
524 }
525
526 pub fn export_standalone(
527 &self,
528 path: impl AsRef<Path>,
529 settings: &StandaloneExportSettings,
530 ) -> Result<()> {
531 let path = path.as_ref().join("processes");
532 fs::create_dir_all(&path)?;
533
534 for p in self.processes.iter() {
535 p.export_standalone(&path, settings)?;
536 }
537 Ok(())
538 }
539
540 pub fn add_process(&mut self, process: Process) {
541 self.processes.push(process);
542 }
543
544 pub fn preprocess(
546 &mut self,
547 model: &Model,
548 settings: &GlobalSettings,
549 locked_runtime_settings: &LockedRuntimeSettings,
550 thread_pool: &ThreadPool,
551 ) -> Result<Vec<GeneratedGraphReport>> {
552 let mut reports = Vec::new();
553 for process in self.processes.iter_mut() {
554 reports.extend(process.preprocess(
555 model,
556 settings,
557 locked_runtime_settings,
558 thread_pool,
559 )?);
560 }
561 Ok(reports)
562 }
563
564 pub fn generate_integrands(
565 &mut self,
566 model: &Model,
567 global_settings: &GlobalSettings,
568 runtime_default: LockedRuntimeSettings,
569 thread_pool: &ThreadPool,
570 ) -> Result<Vec<GeneratedGraphReport>> {
571 let mut reports = Vec::new();
572 for process in &mut self.processes {
573 reports.extend(process.generate_integrands(
574 model,
575 global_settings,
576 runtime_default,
577 thread_pool,
578 )?);
579 }
580 Ok(reports)
581 }
582
583 pub fn find_process(&self, process_id: Option<usize>) -> Result<usize> {
584 if let Some(id) = process_id {
585 if id >= self.processes.len() {
586 return Err(color_eyre::eyre::eyre!(
587 "Invalid process id {}. Number of processes: {}",
588 id,
589 self.processes.len()
590 ));
591 }
592 Ok(id)
593 } else if self.processes.is_empty() {
594 Err(color_eyre::eyre::eyre!("No processes generated yet."))
595 } else if self.processes.len() > 1 {
596 Err(color_eyre::eyre::eyre!(
597 "There are {} processes available. Please specify a process.",
598 self.processes.len()
599 ))
600 } else {
601 Ok(0)
602 }
603 }
604
605 pub fn find_integrand(
606 &self,
607 process_id: Option<usize>,
608 integrand_name: Option<&String>,
609 ) -> Result<(usize, String)> {
610 let p_id = self.find_process(process_id)?;
611 let integrand_name = self.processes[p_id]
612 .collection
613 .find_integrand(integrand_name.cloned())?;
614 Ok((p_id, integrand_name))
615 }
616}
617
618pub mod process;
619pub use process::*;
620pub mod amplitude;
621pub use amplitude::*;
622pub mod cross_section;
623pub use cross_section::*;
624
625#[cfg(test)]
626mod tests {
627 use std::fs::OpenOptions;
628
629 use linnet::half_edge::{
630 involution::EdgeIndex,
631 subgraph::{SuBitGraph, SubSetLike},
632 };
633
634 use symbolica::state::State;
635
636 use crate::{
637 GammaLoopContextContainer, dot,
638 graph::{Graph, LoopMomentumBasis, parse::IntoGraph},
639 momentum::signature::LoopExtSignature,
640 settings::{
641 RuntimeSettings,
642 global::{
643 CompilationMode, CompilationOptimizationLevel, GammaloopCompileOptions,
644 GenerationSettings, TropicalSubgraphTableSettings,
645 },
646 runtime::LockedRuntimeSettings,
647 },
648 utils::load_generic_model,
649 };
650
651 use super::AmplitudeGraph;
652
653 mod failing {
654 use super::*;
655
656 #[test]
657 fn test_encode_decode_amplitude_graph() {
658 let model = load_generic_model("sm");
660
661 let mut graph: Graph = dot!(
662 digraph G{
663 e1 [flow=sink]
664 e2 [flow=source]
665 e3 [flow=source]
666 e1 -> n1 [particle=h]
667 e2 -> n4 [particle=h]
668 n1 -> n2 [particle=h]
669 n1 -> n3 [particle=h]
670 n2 -> n3 [particle=t]
671 n3 -> n4 [particle=t]
672 n4 -> n2 [particle=t]
673 }
674 )
675 .unwrap();
676 let loop_momentum_basis = LoopMomentumBasis {
677 tree: SuBitGraph::empty(0),
678 loop_edges: vec![EdgeIndex::from(0), EdgeIndex::from(4)].into(),
679 ext_edges: vec![EdgeIndex::from(5), EdgeIndex::from(6)].into(),
680 edge_signatures: graph
681 .underlying
682 .new_edgevec(|_, _, _| LoopExtSignature::from((vec![], vec![]))),
683 };
684
685 graph.loop_momentum_basis = loop_momentum_basis;
690
691 let mut amplitude: AmplitudeGraph = AmplitudeGraph::new(graph.clone());
692
693 amplitude
694 .preprocess(
695 &model,
696 &GenerationSettings {
697 compile: GammaloopCompileOptions {
698 compilation_mode: CompilationMode::Cpp,
699 fast_math: false,
700 optimization_level: CompilationOptimizationLevel::O0,
701 unsafe_math: false,
702 compiler: crate::settings::global::default_external_compiler()
703 .to_owned(),
704 custom: Vec::new(),
705 },
706 tropical_subgraph_table: TropicalSubgraphTableSettings {
707 panic_on_fail: false,
708 target_omega: 1.0,
709 ..Default::default()
710 },
711 ..Default::default()
712 },
713 &LockedRuntimeSettings::from(&RuntimeSettings::default()),
714 )
715 .unwrap();
716
717 let mut temp = OpenOptions::new()
718 .write(true)
719 .create(true)
720 .truncate(true)
721 .open("test.bin")
722 .unwrap();
723
724 State::export(&mut temp).unwrap();
725 drop(temp);
726
727 let mut temp = OpenOptions::new().read(true).open("test.bin").unwrap();
728 let state_map = State::import(&mut temp, None).unwrap();
729
730 let context = GammaLoopContextContainer {
731 model: &model,
732 state_map: &state_map,
733 };
734
735 println!("context created");
736
737 let encoded_amplitude =
738 bincode::encode_to_vec(&litude, bincode::config::standard()).unwrap();
739
740 let _amplitude: AmplitudeGraph = bincode::decode_from_slice_with_context(
741 &encoded_amplitude,
742 bincode::config::standard(),
743 context,
744 )
745 .expect("amplitude decode failed")
746 .0;
747
748 println!("amplitude graph passed");
749 }
750 }
751}