Skip to main content

gammaloop_api/commands/
evaluate.rs

1use std::{fs, path::PathBuf};
2
3use clap::Args;
4use gammalooprs::processes::{AmplitudeGraph, AnalyticalEvaluationConfig};
5
6use gammalooprs::processes::{Amplitude, ProcessCollection};
7
8use gammalooprs::utils::vakint;
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11
12use color_eyre::Result;
13use colored::Colorize;
14use gammalooprs::settings::RuntimeSettings;
15use symbolica::atom::{Atom, AtomCore};
16use tracing::{info, warn};
17
18use crate::{
19    completion::CompletionArgExt,
20    state::{ProcessRef, State},
21    CLISettings,
22};
23
24#[cfg_attr(
25    feature = "python_api",
26    pyo3::pyclass(from_py_object, unsendable, name = "EvaluationSettings")
27)]
28#[derive(Debug, Args, Serialize, Deserialize, Clone, JsonSchema, PartialEq)]
29pub struct Evaluate {
30    /// Process reference: `#<id>`, `name:<name>`, or `<id>/<name>`
31    #[arg(
32        short = 'p',
33        long = "process",
34        value_name = "PROCESS",
35        completion_process_selector(crate::completion::SelectorKind::Amplitude)
36    )]
37    pub process: Option<ProcessRef>,
38
39    /// The integrand name to evaluate
40    #[arg(
41        short = 'i',
42        long = "integrand-name",
43        value_name = "NAME",
44        completion_integrand_selector(crate::completion::SelectorKind::Amplitude)
45    )]
46    pub graphs_group_name: Option<String>,
47
48    /// Write the canonical symbolic evaluation result to this TOML file
49    #[arg(short = 'o', long, value_hint = clap::ValueHint::FilePath)]
50    pub result_path: Option<PathBuf>,
51
52    /// Whether to evaluate numerically or not the resulting analytical expression
53    #[arg(short = 'm', long = "numerical")]
54    pub numerical: bool,
55
56    /// The number of terms in the epsilon expansion to compute
57    /// Defaults is automatically inferred from input graph to get to the order O(epsilon^0)
58    #[arg(short = 'e', long = "n-epsilon-terms")]
59    pub number_of_terms_in_epsilon_expansion: Option<usize>,
60}
61
62impl Evaluate {
63    pub fn run(
64        &self,
65        state: &mut State,
66        global_cli_settings: &CLISettings,
67        default_runtime_settings: &RuntimeSettings,
68    ) -> Result<Atom> {
69        let (process_id, integrand_name) =
70            state.find_integrand_ref(self.process.as_ref(), self.graphs_group_name.as_ref())?;
71
72        let amplitude: &Amplitude = match &state.process_list.processes[process_id].collection {
73            ProcessCollection::Amplitudes(amplitudes) => amplitudes.get(&integrand_name).unwrap(),
74            ProcessCollection::CrossSections(_) => {
75                return Err(color_eyre::eyre::eyre!(
76                    "Evaluate command does not support cross-section graphs"
77                ));
78            }
79        };
80        // Always resolve the model through the targeted integrand. `state.model` only carries the
81        // shared structural/default model and would ignore any per-integrand parameter overlay.
82        let model = state.resolve_model_for_integrand(process_id, &integrand_name)?;
83
84        let mut true_settings = global_cli_settings
85            .global
86            .generation
87            .uv
88            .vakint
89            .true_settings();
90        let refresh_model_values = amplitude.integrand.is_some();
91
92        let vakint = vakint()?;
93
94        if let Some(n_terms) = self.number_of_terms_in_epsilon_expansion {
95            true_settings.number_of_terms_in_epsilon_expansion = n_terms as i64;
96        }
97
98        let mut full_evaluation = Atom::Zero;
99
100        for graph_term in amplitude.graphs.iter() {
101            let g = &graph_term.graph;
102            let mut complete_evaluation_for_this_graph = Atom::num(1);
103            if g.n_externals() != 0 {
104                return Err(color_eyre::eyre::eyre!(
105                    "Graph named: {} has external legs. Analytical evaluation in gammaloop only supports vacuum graphs.",
106                    graph_term.graph.name
107                ));
108            }
109            let connected_components = g.connected_components(&g.full_filter());
110            if connected_components.len() > 1 && g.global_prefactor.num != Atom::num(1) {
111                warn!(
112                    "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.",
113                    graph_term.graph.name,
114                    connected_components.len(),
115                    g.global_prefactor.num
116                );
117            }
118            for (i_gc, gc) in connected_components.iter().enumerate() {
119                info!(
120                    "Evaluating connected component #{} of graph named: {}",
121                    i_gc + 1,
122                    graph_term.graph.name.blue()
123                );
124                complete_evaluation_for_this_graph *= graph_term.analytical_evaluation(
125                    gc,
126                    AnalyticalEvaluationConfig {
127                        model: &model,
128                        refresh_model_values,
129                        evaluate_numerically: self.numerical,
130                        vakint,
131                        true_settings: &true_settings,
132                        settings: &global_cli_settings.global.generation.uv.vakint,
133                        run_time_settings: default_runtime_settings,
134                        // Only include the overall global numerator on the first connected component.
135                        include_global_numerator: i_gc == 0,
136                    },
137                )?;
138            }
139            complete_evaluation_for_this_graph *= &g.global_prefactor.projector * &g.overall_factor;
140
141            full_evaluation += complete_evaluation_for_this_graph;
142        }
143
144        if let Some(p) = self.result_path.clone() {
145            info!(
146                "Saving evaluation result for process {} to path: {}",
147                state.process_list.processes[process_id]
148                    .definition
149                    .folder_name
150                    .green(),
151                p.display()
152            );
153            fs::write(
154                p,
155                toml::to_string_pretty(&full_evaluation.to_canonical_string())?,
156            )?;
157        } else if self.numerical {
158            let numerical_evaluation_result =
159                AmplitudeGraph::to_numerical(full_evaluation.as_view(), &true_settings)?;
160            info!(
161                "Numerical evaluation of the analytical result for process {}:\n{}",
162                state.process_list.processes[process_id]
163                    .definition
164                    .folder_name
165                    .green(),
166                numerical_evaluation_result
167            );
168        } else {
169            info!(
170                "Analytical result for process {}:\n{}",
171                state.process_list.processes[process_id]
172                    .definition
173                    .folder_name
174                    .green(),
175                full_evaluation
176            );
177        }
178
179        Ok(full_evaluation)
180    }
181}