Skip to main content

gammaloop_api/commands/
profile.rs

1use std::path::PathBuf;
2
3use crate::{
4    commands::CliArgumentMetadataExt,
5    completion::CompletionArgExt,
6    state::{ProcessRef, State},
7    CLISettings,
8};
9use color_eyre::Result;
10use eyre::eyre;
11use gammalooprs::{
12    integrands::process::{
13        ir::{IRProfileSetting, IrLimitTestReport},
14        OrientationProfileMode, ProcessIntegrand,
15    },
16    processes::ProcessCollection,
17    uv::{
18        profile::{ProfileSettings, UVProfileFixedRay, UVProfileable},
19        UVProfileAnalysis,
20    },
21};
22use schemars::JsonSchema;
23use serde::{Deserialize, Serialize};
24use tracing::{info, instrument};
25
26use clap::{Args, Subcommand};
27
28#[derive(Subcommand, Debug, Serialize, Deserialize, Clone, JsonSchema, PartialEq)]
29pub enum Profile {
30    /// Sample ultraviolet scaling rays and report the fitted large-momentum behavior.
31    UltraViolet(#[command(flatten)] UltraVioletProfile),
32    /// Probe multiple infrared scaling limits and report their fitted behavior in one run.
33    #[command(name = "bulk")]
34    InfraRed(#[command(flatten)] InfraRedProfile),
35}
36
37#[derive(Args, Debug, Serialize, Deserialize, Clone, JsonSchema, PartialEq)]
38pub struct UltraVioletProfile {
39    /// Process reference: `#<id>`, `name:<name>`, or `<id>/<name>`
40    #[arg(
41        short = 'p',
42        long = "process",
43        value_name = "PROCESS",
44        completion_process_selector(crate::completion::SelectorKind::Any)
45    )]
46    pub process: Option<ProcessRef>,
47
48    /// The integrand name to inspect
49    #[arg(
50        short = 'i',
51        long = "integrand-name",
52        value_name = "NAME",
53        completion_integrand_selector(crate::completion::SelectorKind::Any)
54    )]
55    pub integrand_name: Option<String>,
56
57    /// Number of scaling points to sample
58    #[arg(long = "n-points", default_value_t = 20)]
59    pub n_points: usize,
60
61    /// Starting exponent of the sampled ultraviolet scaling range
62    #[arg(long = "min-scaling", default_value_t = 3.0)]
63    pub min_scale_exponent: f64,
64
65    /// Ending exponent of the sampled ultraviolet scaling range
66    #[arg(long = "max-scaling", default_value_t = 6.0)]
67    pub max_scale_exponent: f64,
68
69    /// Use f128 precision for evaluation
70    #[arg(long = "use_f128")]
71    pub use_f128: bool,
72
73    /// Fit the symbolic ultraviolet scaling expression in addition to sampled numerical values.
74    #[arg(long = "analyse_analytically")]
75    pub analyse_analytically: bool,
76
77    /// Profile each visible orientation separately and include per-orientation results
78    #[arg(long = "per-orientation")]
79    pub per_orientation: bool,
80
81    /// Random seed for momentum sampling
82    #[arg(long = "seed")]
83    pub seed: Option<u64>,
84
85    /// Fixed UV ray directions as flattened 3-vectors, repeated for all rays if one direction is supplied
86    #[arg(
87        long = "uv-ray-directions",
88        num_args = 1..,
89        value_delimiter = ',',
90        allow_negative_numbers = true
91    )]
92    pub uv_ray_directions: Vec<f64>,
93
94    /// Fixed UV ray starting norms, repeated for all rays if one norm is supplied
95    #[arg(
96        long = "uv-ray-norms",
97        num_args = 1..,
98        value_delimiter = ',',
99        allow_negative_numbers = true,
100        cli_requires("uv_ray_directions")
101    )]
102    pub uv_ray_norms: Vec<f64>,
103
104    /// Output file for results (optional)
105    #[arg(short = 'o', long = "output", value_hint = clap::ValueHint::FilePath)]
106    pub output_file: Option<PathBuf>,
107}
108
109#[derive(Args, Debug, Serialize, Deserialize, Clone, JsonSchema, PartialEq)]
110pub struct InfraRedProfile {
111    /// Process reference: `#<id>`, `name:<name>`, or `<id>/<name>`
112    #[arg(
113        short = 'p',
114        long = "process",
115        value_name = "PROCESS",
116        completion_process_selector(crate::completion::SelectorKind::CrossSection)
117    )]
118    pub process: Option<ProcessRef>,
119
120    /// The cross-section name to inspect
121    #[arg(
122        short = 'i',
123        long = "integrand-name",
124        value_name = "NAME",
125        completion_integrand_selector(crate::completion::SelectorKind::CrossSection)
126    )]
127    pub integrand_name: Option<String>,
128
129    /// Number of scaling points to sample
130    #[arg(long = "n-points", default_value_t = 20)]
131    pub n_points: usize,
132
133    /// Starting exponent of the sampled bulk scaling range
134    #[arg(long = "min-scaling", default_value_t = -2.0)]
135    pub min_scale_exponent: f64,
136
137    /// Ending exponent of the sampled bulk scaling range
138    #[arg(long = "max-scaling", default_value_t = -3.0)]
139    pub max_scale_exponent: f64,
140
141    /// Random seed for momentum sampling
142    #[arg(long = "seed")]
143    pub seed: Option<u64>,
144
145    /// Output file for results (optional)
146    #[arg(short = 'o', long = "output", value_hint = clap::ValueHint::FilePath)]
147    pub output_file: Option<PathBuf>,
148
149    /// restrict test to particular graphs or limits
150    #[arg(short = 's', long = "select")]
151    pub select: Option<String>,
152
153    /// Profile each visible orientation separately and report one row per orientation
154    #[arg(long = "per-orientation")]
155    pub per_orientation: bool,
156
157    /// Retain generated event data so per-cut IR fits can be computed and displayed
158    #[arg(long = "show-per-cut-info")]
159    pub show_per_cut_info: bool,
160}
161
162impl Default for InfraRedProfile {
163    fn default() -> Self {
164        Self {
165            process: None,
166            integrand_name: None,
167            n_points: 20,
168            min_scale_exponent: -2.0,
169            max_scale_exponent: -3.0,
170            seed: None,
171            output_file: None,
172            select: None,
173            per_orientation: false,
174            show_per_cut_info: false,
175        }
176    }
177}
178impl Default for UltraVioletProfile {
179    fn default() -> Self {
180        Self {
181            process: None,
182            integrand_name: None,
183            n_points: 20,
184            min_scale_exponent: 3.0,
185            max_scale_exponent: 6.0,
186            use_f128: false,
187            analyse_analytically: false,
188            per_orientation: false,
189            seed: None,
190            uv_ray_directions: Vec::new(),
191            uv_ray_norms: Vec::new(),
192            output_file: None,
193        }
194    }
195}
196
197pub enum ProfileResult {
198    UltraViolet(UVProfileAnalysis),
199    InfraRed(IrLimitTestReport),
200}
201
202impl ProfileResult {
203    pub fn unwrap_uv(self) -> UVProfileAnalysis {
204        match self {
205            ProfileResult::UltraViolet(uv_analyis) => uv_analyis,
206            _ => panic!("result does not contain uv profilel analysis data"),
207        }
208    }
209
210    pub fn unwrap_ir(self) -> IrLimitTestReport {
211        match self {
212            ProfileResult::InfraRed(ir_analyis) => ir_analyis,
213            _ => panic!("result does not contain ir profile analysis data"),
214        }
215    }
216}
217
218impl Profile {
219    #[instrument(skip_all)]
220    #[allow(clippy::needless_update)]
221    pub fn run(
222        &self,
223        state: &mut State,
224        _global_cli_settings: &CLISettings,
225    ) -> Result<ProfileResult> {
226        match self {
227            Profile::UltraViolet(UltraVioletProfile {
228                process,
229                integrand_name,
230                n_points,
231                min_scale_exponent,
232                max_scale_exponent,
233                use_f128,
234                seed,
235                uv_ray_directions,
236                uv_ray_norms,
237                analyse_analytically,
238                per_orientation,
239                output_file,
240            }) => {
241                let (process_id, integrand_name) =
242                    state.find_integrand_ref(process.as_ref(), integrand_name.as_ref())?;
243                let model = state.resolve_model_for_integrand(process_id, &integrand_name)?;
244                let default_uv_ray_norm = {
245                    let process = &mut state.process_list.processes[process_id];
246                    match &mut process.collection {
247                        ProcessCollection::Amplitudes(amplitudes) => {
248                            let amplitude =
249                                amplitudes.get_mut(&integrand_name).ok_or_else(|| {
250                                    eyre!(
251                                        "No amplitude named '{}' in process '{}'",
252                                        integrand_name,
253                                        process.definition.folder_name
254                                    )
255                                })?;
256                            let integrand = amplitude.integrand.as_mut().ok_or(eyre!(
257                                "Integrand {} has not yet been generated, but exists",
258                                amplitude.name
259                            ))?;
260                            integrand.warm_up(&model)?;
261                            integrand.get_settings().kinematics.e_cm
262                        }
263                        ProcessCollection::CrossSections(cross_sections) => {
264                            let cross_section =
265                                cross_sections.get_mut(&integrand_name).ok_or_else(|| {
266                                    eyre!(
267                                        "No cross section named '{}' in process '{}'",
268                                        integrand_name,
269                                        process.definition.folder_name
270                                    )
271                                })?;
272                            let integrand = cross_section.integrand.as_mut().ok_or(eyre!(
273                                "Integrand {} has not yet been generated, but exists",
274                                cross_section.name
275                            ))?;
276                            integrand.warm_up(&model)?;
277                            integrand.get_settings().kinematics.e_cm
278                        }
279                    }
280                };
281
282                let fixed_uv_ray = if uv_ray_directions.is_empty() {
283                    None
284                } else {
285                    let uv_ray_norms = if uv_ray_norms.is_empty() {
286                        vec![default_uv_ray_norm]
287                    } else {
288                        uv_ray_norms.clone()
289                    };
290                    Some(UVProfileFixedRay::from_flat_components(
291                        uv_ray_directions,
292                        &uv_ray_norms,
293                    )?)
294                };
295
296                let profile_settings = ProfileSettings {
297                    n_points: *n_points,
298                    min_scale_exponent: *min_scale_exponent,
299                    max_scale_exponent: *max_scale_exponent,
300                    seed: (*seed).unwrap_or(42),
301                    use_f128: *use_f128,
302                    analyse_analytically: *analyse_analytically,
303                    orientation_mode: if *per_orientation {
304                        OrientationProfileMode::PerOrientation
305                    } else {
306                        OrientationProfileMode::Summed
307                    },
308                    fixed_uv_ray,
309                    ..Default::default()
310                };
311                let profile_res = {
312                    let process = &mut state.process_list.processes[process_id];
313                    match &mut process.collection {
314                        ProcessCollection::Amplitudes(amplitudes) => amplitudes
315                            .get_mut(&integrand_name)
316                            .ok_or_else(|| {
317                                eyre!(
318                                    "No amplitude named '{}' in process '{}'",
319                                    integrand_name,
320                                    process.definition.folder_name
321                                )
322                            })?
323                            .profile(&model, &profile_settings)?,
324                        ProcessCollection::CrossSections(cross_sections) => cross_sections
325                            .get_mut(&integrand_name)
326                            .ok_or_else(|| {
327                                eyre!(
328                                    "No cross section named '{}' in process '{}'",
329                                    integrand_name,
330                                    process.definition.folder_name
331                                )
332                            })?
333                            .profile(&model, &profile_settings)?,
334                    }
335                }
336                .analyse();
337
338                for t in profile_res.tables_per_graph(-0.9) {
339                    info!("\n{}", t);
340                }
341
342                for t in profile_res.analytic_tables_per_graph() {
343                    let Some(t) = t else {
344                        continue;
345                    };
346                    info!("\n{}", t);
347                }
348
349                for t in profile_res.per_orientation_tables_per_graph(-0.9) {
350                    let Some(t) = t else {
351                        continue;
352                    };
353                    info!("\n{}", t);
354                }
355
356                if let Some(file) = output_file {
357                    profile_res.write_profile_data(file)?
358                }
359
360                Ok(ProfileResult::UltraViolet(profile_res))
361            }
362            Profile::InfraRed(InfraRedProfile {
363                process,
364                integrand_name,
365                n_points,
366                min_scale_exponent,
367                max_scale_exponent,
368                seed,
369                output_file: _,
370                select,
371                per_orientation,
372                show_per_cut_info,
373            }) => {
374                let ir_profile_settings = IRProfileSetting {
375                    lambda_exp_start: *min_scale_exponent,
376                    lambda_exp_end: *max_scale_exponent,
377                    steps: *n_points,
378                    seed: seed.unwrap_or(420),
379                    select_limits_and_graphs: select.clone(),
380                    orientation_mode: if *per_orientation {
381                        OrientationProfileMode::PerOrientation
382                    } else {
383                        OrientationProfileMode::Summed
384                    },
385                    show_per_cut_info: *show_per_cut_info,
386                };
387
388                let (process_id, integrand_name) =
389                    state.find_integrand_ref(process.as_ref(), integrand_name.as_ref())?;
390
391                let model = state.resolve_model_for_integrand(process_id, &integrand_name)?;
392
393                let process = &mut state.process_list.processes[process_id];
394                let integrand = match &mut process.collection {
395                    ProcessCollection::Amplitudes(amplitudes) => amplitudes
396                        .get_mut(&integrand_name)
397                        .ok_or_else(|| {
398                            eyre!(
399                                "No amplitude named '{}' in process '{}'",
400                                integrand_name,
401                                process.definition.folder_name
402                            )
403                        })?
404                        .integrand
405                        .as_mut(),
406                    ProcessCollection::CrossSections(xs) => xs
407                        .get_mut(&integrand_name)
408                        .ok_or_else(|| {
409                            eyre!(
410                                "No xs named '{}' in process '{}'",
411                                integrand_name,
412                                process.definition.folder_name
413                            )
414                        })?
415                        .integrand
416                        .as_mut(),
417                };
418
419                let profile_result = match integrand.ok_or(eyre!(
420                    "Integrand {} has not yet been generated",
421                    integrand_name
422                ))? {
423                    ProcessIntegrand::CrossSection(cross_section_integrand) => {
424                        cross_section_integrand.test_ir(&ir_profile_settings, &model)?
425                    }
426                    ProcessIntegrand::Amplitude(amplitude_integrand) => {
427                        amplitude_integrand.test_ir(&ir_profile_settings, &model)?
428                    }
429                };
430
431                info!("\n{}", profile_result);
432                Ok(ProfileResult::InfraRed(profile_result))
433            }
434        }
435    }
436}