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 #[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 #[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 #[arg(short = 'o', long, value_hint = clap::ValueHint::FilePath)]
54 pub result_path: Option<PathBuf>,
55
56 #[arg(short = 'm', long = "numerical")]
58 pub numerical: bool,
59
60 #[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 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 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 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}