Skip to main content

gammaloop_api/commands/
evaluate.rs

1use std::{fs, path::PathBuf};
2
3use clap::Args;
4use gammalooprs::{
5    graph::FeynmanGraph,
6    processes::{AmplitudeGraph, AnalyticalEvaluationConfig},
7    uv::UltravioletGraph,
8};
9
10use gammalooprs::processes::{Amplitude, ProcessCollection};
11
12use gammalooprs::utils::vakint;
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15
16use color_eyre::Result;
17use colored::Colorize;
18use gammalooprs::settings::RuntimeSettings;
19use symbolica::atom::{Atom, AtomCore};
20use tracing::{info, warn};
21
22use crate::{
23    completion::CompletionArgExt,
24    state::{ProcessRef, State},
25    CLISettings,
26};
27
28#[cfg_attr(
29    feature = "python",
30    pyo3::pyclass(from_py_object, unsendable, name = "EvaluationSettings")
31)]
32#[derive(Debug, Args, Serialize, Deserialize, Clone, JsonSchema, PartialEq)]
33pub struct Evaluate {
34    /// Process reference: `#<id>`, `name:<name>`, or `<id>/<name>`
35    #[arg(
36        short = 'p',
37        long = "process",
38        value_name = "PROCESS",
39        completion_process_selector(crate::completion::SelectorKind::Amplitude)
40    )]
41    pub process: Option<ProcessRef>,
42
43    /// The integrand name to evaluate
44    #[arg(
45        short = 'i',
46        long = "integrand-name",
47        value_name = "NAME",
48        completion_integrand_selector(crate::completion::SelectorKind::Amplitude)
49    )]
50    pub graphs_group_name: Option<String>,
51
52    /// Write the canonical symbolic evaluation result to this TOML file
53    #[arg(short = 'o', long, value_hint = clap::ValueHint::FilePath)]
54    pub result_path: Option<PathBuf>,
55
56    /// Whether to evaluate numerically or not the resulting analytical expression
57    #[arg(short = 'm', long = "numerical")]
58    pub numerical: bool,
59
60    /// The number of terms in the epsilon expansion to compute
61    /// Defaults is automatically inferred from input graph to get to the order O(epsilon^0)
62    #[arg(short = 'e', long = "n-epsilon-terms")]
63    pub number_of_terms_in_epsilon_expansion: Option<usize>,
64}
65
66impl Evaluate {
67    pub fn run(
68        &self,
69        state: &mut State,
70        global_cli_settings: &CLISettings,
71        default_runtime_settings: &RuntimeSettings,
72    ) -> Result<Atom> {
73        if let Some(path) = &self.result_path {
74            global_cli_settings
75                .ensure_write_target_outside_active_state(path, "write evaluation result")?;
76        }
77
78        let (process_id, integrand_name) =
79            state.find_integrand_ref(self.process.as_ref(), self.graphs_group_name.as_ref())?;
80
81        let amplitude: &Amplitude = match &state.process_list.processes[process_id].collection {
82            ProcessCollection::Amplitudes(amplitudes) => amplitudes.get(&integrand_name).unwrap(),
83            ProcessCollection::CrossSections(_) => {
84                return Err(color_eyre::eyre::eyre!(
85                    "Evaluate command does not support cross-section graphs"
86                ));
87            }
88        };
89        // Always resolve the model through the targeted integrand. `state.model` only carries the
90        // shared structural/default model and would ignore any per-integrand parameter overlay.
91        let model = state.resolve_model_for_integrand(process_id, &integrand_name)?;
92
93        let mut true_settings = global_cli_settings
94            .global
95            .generation
96            .uv
97            .vakint
98            .true_settings();
99        let vakint = vakint()?;
100
101        if let Some(n_terms) = self.number_of_terms_in_epsilon_expansion {
102            true_settings.number_of_terms_in_epsilon_expansion = n_terms as i64;
103        }
104
105        let mut full_evaluation = Atom::Zero;
106
107        for graph_term in amplitude.graphs.iter() {
108            let g = &graph_term.graph;
109            if self.number_of_terms_in_epsilon_expansion.is_none() {
110                true_settings.number_of_terms_in_epsilon_expansion =
111                    g.n_loops(&g.no_dummy()) as i64 + 1;
112            }
113            let mut complete_evaluation_for_this_graph = Atom::num(1);
114            if g.n_externals() != 0 {
115                return Err(color_eyre::eyre::eyre!(
116                    "Graph named: {} has external legs. Analytical evaluation in gammaloop only supports vacuum graphs.",
117                    graph_term.graph.name
118                ));
119            }
120            let connected_components = g.connected_components(&g.full_filter());
121            if connected_components.len() > 1 && g.global_prefactor.num != Atom::num(1) {
122                warn!(
123                    "Graph named: {} has more than one ({}) connected components and its expression contains a global numerator ({}), which will only be applied to the first connected component. Make sure this is intended.",
124                    graph_term.graph.name,
125                    connected_components.len(),
126                    g.global_prefactor.num
127                );
128            }
129            for (i_gc, gc) in connected_components.iter().enumerate() {
130                info!(
131                    "Evaluating connected component #{} of graph named: {}",
132                    i_gc + 1,
133                    graph_term.graph.name.blue()
134                );
135                complete_evaluation_for_this_graph *= graph_term.analytical_evaluation(
136                    gc,
137                    AnalyticalEvaluationConfig {
138                        model: &model,
139                        evaluate_numerically: self.numerical,
140                        vakint,
141                        true_settings: &true_settings,
142                        settings: &global_cli_settings.global.generation.uv.vakint,
143                        run_time_settings: default_runtime_settings,
144                        // Only include the overall global numerator on the first connected component.
145                        include_global_numerator: i_gc == 0,
146                    },
147                )?;
148            }
149            complete_evaluation_for_this_graph *= &g.global_prefactor.projector * &g.overall_factor;
150
151            full_evaluation += complete_evaluation_for_this_graph;
152        }
153
154        if let Some(p) = self.result_path.clone() {
155            info!(
156                "Saving evaluation result for process {} to path: {}",
157                state.process_list.processes[process_id]
158                    .definition
159                    .folder_name
160                    .green(),
161                p.display()
162            );
163            fs::write(
164                p,
165                toml::to_string_pretty(&full_evaluation.to_canonical_string())?,
166            )?;
167        } else if self.numerical {
168            let numerical_evaluation_result =
169                AmplitudeGraph::to_numerical(full_evaluation.as_view(), &true_settings)?;
170            info!(
171                "Numerical evaluation of the analytical result for process {}:\n{}",
172                state.process_list.processes[process_id]
173                    .definition
174                    .folder_name
175                    .green(),
176                numerical_evaluation_result
177            );
178        } else {
179            info!(
180                "Analytical result for process {}:\n{}",
181                state.process_list.processes[process_id]
182                    .definition
183                    .folder_name
184                    .green(),
185                full_evaluation
186            );
187        }
188
189        Ok(full_evaluation)
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::Evaluate;
196    use crate::{
197        commands::{
198            generate::Generate,
199            import::{model::ImportModel, Import},
200        },
201        state::State,
202        CLISettings,
203    };
204    use gammalooprs::{
205        initialisation::test_initialise,
206        settings::{global::MediumMode, RuntimeSettings},
207    };
208    use std::path::PathBuf;
209
210    #[test]
211    fn evaluate_rejects_generated_thermal_graphs_after_defaults_change() -> color_eyre::Result<()> {
212        test_initialise()?;
213        let temp = tempfile::tempdir()?;
214        for mode in [
215            MediumMode::ThermodynamicEquilibrium,
216            MediumMode::ZeroTemperatureEquilibrium,
217        ] {
218            let mut state = State::new_test();
219            let mut cli_settings = CLISettings::default();
220            cli_settings.state.folder = temp.path().join("state");
221            let generation = &mut cli_settings.global.generation;
222            generation.medium.mode = mode;
223            generation.evaluator.compile = false;
224            generation.evaluator.summed = false;
225            generation.evaluator.iterative_orientation_optimization = false;
226            generation.uv.subtract_uv = false;
227            generation.uv.generate_integrated = false;
228            generation.threshold_subtraction.enable_thresholds = false;
229            generation
230                .tropical_subgraph_table
231                .disable_tropical_generation = true;
232            ImportModel {
233                path: "scalars-default.json".into(),
234                simplify_model: true,
235            }
236            .run(&mut state)?;
237            Import::Graphs {
238                source: None,
239                inline_dot: Some(
240                    r#"digraph thermal_tadpole {
241                        node [num=1];
242                        a -> a [particle="scalar_1" num=1 id=0 lmb_id=0];
243                    }"#
244                    .to_string(),
245                ),
246                process_spec: None,
247                process: None,
248                integrand_name: Some("tadpole".to_string()),
249                overwrite: false,
250                append: false,
251            }
252            .run(&mut state, &cli_settings)?;
253            let runtime = RuntimeSettings::default();
254            Generate {
255                keep_sources: false,
256                mode: None,
257            }
258            .run(
259                &mut state,
260                temp.path(),
261                false,
262                &cli_settings.global,
263                &runtime,
264            )?;
265
266            // Evaluation must use the generated graph's mode, even if subsequent
267            // commands select vacuum defaults for future generation.
268            cli_settings.global.generation.medium.mode = MediumMode::Vacuum;
269            let result_path = temp.path().join("result.toml");
270            for numerical in [false, true] {
271                let err = Evaluate {
272                    process: None,
273                    graphs_group_name: Some("tadpole".to_string()),
274                    result_path: Some(result_path.clone()),
275                    numerical,
276                    number_of_terms_in_epsilon_expansion: None,
277                }
278                .run(&mut state, &cli_settings, &runtime)
279                .unwrap_err();
280                assert!(
281                    err.to_string().contains("does not support thermal modes"),
282                    "{mode:?}, numerical={numerical}: {err:?}"
283                );
284                assert!(!result_path.exists());
285            }
286        }
287        Ok(())
288    }
289
290    #[test]
291    fn evaluate_rejects_result_path_inside_read_only_state() {
292        let mut cli_settings = CLISettings::default();
293        cli_settings.state.folder = PathBuf::from("/tmp/read_only_state");
294        cli_settings.session.read_only_state = true;
295
296        let err = Evaluate {
297            process: None,
298            graphs_group_name: None,
299            result_path: Some(PathBuf::from("/tmp/read_only_state/result.toml")),
300            numerical: false,
301            number_of_terms_in_epsilon_expansion: None,
302        }
303        .run(
304            &mut State::new_test(),
305            &cli_settings,
306            &RuntimeSettings::default(),
307        )
308        .unwrap_err();
309
310        assert!(format!("{err:?}").contains("--read-only-state"));
311    }
312}