1use std::{fs::File, 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::{
19 ProfileSettings, UVLimitSelection, UVProfileFixedRay, UVProfileable, UV_PROFILE_MAX_DOD,
20 },
21 UVProfileAnalysis,
22 },
23};
24use linnet::half_edge::involution::EdgeIndex;
25use schemars::JsonSchema;
26use serde::{Deserialize, Serialize};
27use tracing::{info, instrument};
28
29use clap::{Args, Subcommand};
30
31#[derive(Subcommand, Debug, Serialize, Deserialize, Clone, JsonSchema, PartialEq)]
32pub enum Profile {
33 UltraViolet(#[command(flatten)] UltraVioletProfile),
35 #[command(name = "bulk")]
37 InfraRed(#[command(flatten)] InfraRedProfile),
38}
39
40#[derive(Args, Debug, Serialize, Deserialize, Clone, JsonSchema, PartialEq)]
41pub struct UltraVioletProfile {
42 #[arg(
44 short = 'p',
45 long = "process",
46 value_name = "PROCESS",
47 completion_process_selector(crate::completion::SelectorKind::Any)
48 )]
49 pub process: Option<ProcessRef>,
50
51 #[arg(
53 short = 'i',
54 long = "integrand-name",
55 value_name = "NAME",
56 completion_integrand_selector(crate::completion::SelectorKind::Any)
57 )]
58 pub integrand_name: Option<String>,
59
60 #[arg(
62 short = 'g',
63 long = "graph",
64 value_name = "GRAPH",
65 completion_selected_master_graph()
66 )]
67 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub graph: Option<String>,
69
70 #[arg(
72 long = "cutkosky-cut",
73 visible_alias = "cut-edges",
74 value_name = "EDGE",
75 num_args = 1..,
76 value_delimiter = ',',
77 requires = "graph"
78 )]
79 #[serde(default, skip_serializing_if = "Vec::is_empty")]
80 pub cutkosky_cut: Vec<usize>,
81
82 #[arg(long = "selected-limits", value_enum, default_value = "only-divergent")]
84 #[serde(default)]
85 pub selected_limits: UVLimitSelection,
86
87 #[arg(long = "fail-fast")]
89 #[serde(
90 default,
91 skip_serializing_if = "gammalooprs::utils::serde_utils::is_false"
92 )]
93 pub fail_fast: bool,
94
95 #[arg(long = "n-points", default_value_t = 20)]
97 pub n_points: usize,
98
99 #[arg(long = "min-scaling", default_value_t = 3.0)]
101 pub min_scale_exponent: f64,
102
103 #[arg(long = "max-scaling", default_value_t = 6.0)]
105 pub max_scale_exponent: f64,
106
107 #[arg(long = "use_f128")]
109 pub use_f128: bool,
110
111 #[arg(long = "analyse_analytically")]
113 pub analyse_analytically: bool,
114
115 #[arg(long = "per-orientation")]
117 pub per_orientation: bool,
118
119 #[arg(long = "allow-vanishing-missing-fits")]
121 pub allow_vanishing_missing_fits: bool,
122
123 #[arg(long = "seed")]
125 pub seed: Option<u64>,
126
127 #[arg(
129 long = "uv-ray-directions",
130 num_args = 1..,
131 value_delimiter = ',',
132 allow_negative_numbers = true
133 )]
134 pub uv_ray_directions: Vec<f64>,
135
136 #[arg(
138 long = "uv-ray-norms",
139 num_args = 1..,
140 value_delimiter = ',',
141 allow_negative_numbers = true,
142 cli_requires("uv_ray_directions")
143 )]
144 pub uv_ray_norms: Vec<f64>,
145
146 #[arg(short = 'o', long = "output", value_hint = clap::ValueHint::DirPath)]
148 pub output_file: Option<PathBuf>,
149}
150
151#[derive(Args, Debug, Serialize, Deserialize, Clone, JsonSchema, PartialEq)]
152pub struct InfraRedProfile {
153 #[arg(
155 short = 'p',
156 long = "process",
157 value_name = "PROCESS",
158 completion_process_selector(crate::completion::SelectorKind::CrossSection)
159 )]
160 pub process: Option<ProcessRef>,
161
162 #[arg(
164 short = 'i',
165 long = "integrand-name",
166 value_name = "NAME",
167 completion_integrand_selector(crate::completion::SelectorKind::CrossSection)
168 )]
169 pub integrand_name: Option<String>,
170
171 #[arg(long = "n-points", default_value_t = 20)]
173 pub n_points: usize,
174
175 #[arg(long = "min-scaling", default_value_t = -2.0)]
177 pub min_scale_exponent: f64,
178
179 #[arg(long = "max-scaling", default_value_t = -3.0)]
181 pub max_scale_exponent: f64,
182
183 #[arg(long = "seed")]
185 pub seed: Option<u64>,
186
187 #[arg(short = 'o', long = "output", value_hint = clap::ValueHint::FilePath)]
189 pub output_file: Option<PathBuf>,
190
191 #[arg(short = 's', long = "select")]
193 pub select: Option<String>,
194
195 #[arg(long = "per-orientation")]
197 pub per_orientation: bool,
198
199 #[arg(long = "show-per-cut-info")]
201 pub show_per_cut_info: bool,
202}
203
204impl Default for InfraRedProfile {
205 fn default() -> Self {
206 Self {
207 process: None,
208 integrand_name: None,
209 n_points: 20,
210 min_scale_exponent: -2.0,
211 max_scale_exponent: -3.0,
212 seed: None,
213 output_file: None,
214 select: None,
215 per_orientation: false,
216 show_per_cut_info: false,
217 }
218 }
219}
220impl Default for UltraVioletProfile {
221 fn default() -> Self {
222 Self {
223 process: None,
224 integrand_name: None,
225 graph: None,
226 cutkosky_cut: Vec::new(),
227 selected_limits: UVLimitSelection::OnlyDivergent,
228 fail_fast: false,
229 n_points: 20,
230 min_scale_exponent: 3.0,
231 max_scale_exponent: 6.0,
232 use_f128: false,
233 analyse_analytically: false,
234 per_orientation: false,
235 allow_vanishing_missing_fits: false,
236 seed: None,
237 uv_ray_directions: Vec::new(),
238 uv_ray_norms: Vec::new(),
239 output_file: None,
240 }
241 }
242}
243
244pub enum ProfileResult {
245 UltraViolet(UVProfileAnalysis),
246 InfraRed(IrLimitTestReport),
247}
248
249impl ProfileResult {
250 pub fn unwrap_uv(self) -> UVProfileAnalysis {
251 match self {
252 ProfileResult::UltraViolet(uv_analyis) => uv_analyis,
253 _ => panic!("result does not contain uv profilel analysis data"),
254 }
255 }
256
257 pub fn unwrap_ir(self) -> IrLimitTestReport {
258 match self {
259 ProfileResult::InfraRed(ir_analyis) => ir_analyis,
260 _ => panic!("result does not contain ir profile analysis data"),
261 }
262 }
263}
264
265impl Profile {
266 #[instrument(skip_all)]
267 #[allow(clippy::needless_update)]
268 pub fn run(
269 &self,
270 state: &mut State,
271 global_cli_settings: &CLISettings,
272 ) -> Result<ProfileResult> {
273 match self {
274 Profile::UltraViolet(UltraVioletProfile {
275 process,
276 integrand_name,
277 graph,
278 cutkosky_cut,
279 selected_limits,
280 fail_fast,
281 n_points,
282 min_scale_exponent,
283 max_scale_exponent,
284 use_f128,
285 seed,
286 uv_ray_directions,
287 uv_ray_norms,
288 analyse_analytically,
289 per_orientation,
290 allow_vanishing_missing_fits,
291 output_file,
292 }) => {
293 let (process_id, integrand_name) =
294 state.find_integrand_ref(process.as_ref(), integrand_name.as_ref())?;
295 let model = state.resolve_model_for_integrand(process_id, &integrand_name)?;
296 let (default_uv_ray_norm, graph_id) = {
297 let integrand = state
298 .process_list
299 .get_integrand_mut(process_id, &integrand_name)?;
300 integrand.warm_up(&model)?;
301 let graph_id = graph
302 .as_ref()
303 .map(|graph_selector| {
304 let numeric_selector =
305 graph_selector.strip_prefix('#').unwrap_or(graph_selector);
306 if let Some(graph_id) = numeric_selector
307 .parse::<usize>()
308 .ok()
309 .filter(|graph_id| *graph_id < integrand.graph_count())
310 {
311 return Ok(graph_id);
312 }
313 if let Some(graph_id) = integrand.find_graph_id_by_name(graph_selector)
314 {
315 return Ok(graph_id);
316 }
317
318 let available = (0..integrand.graph_count())
319 .filter_map(|graph_id| {
320 integrand
321 .graph_name_by_id(graph_id)
322 .map(|name| format!("{graph_id}:{name}"))
323 })
324 .collect::<Vec<_>>()
325 .join(", ");
326 Err(eyre!(
327 "No graph '{}' exists in integrand '{}'. Available graphs: {}",
328 graph_selector,
329 integrand_name,
330 available
331 ))
332 })
333 .transpose()?;
334 if !cutkosky_cut.is_empty()
335 && matches!(integrand, ProcessIntegrand::Amplitude(_))
336 {
337 return Err(eyre!(
338 "Cutkosky-cut selection is only supported for cross sections"
339 ));
340 }
341 (integrand.get_settings().kinematics.e_cm, graph_id)
342 };
343
344 let fixed_uv_ray = if uv_ray_directions.is_empty() {
345 None
346 } else {
347 let uv_ray_norms = if uv_ray_norms.is_empty() {
348 vec![default_uv_ray_norm]
349 } else {
350 uv_ray_norms.clone()
351 };
352 Some(UVProfileFixedRay::from_flat_components(
353 uv_ray_directions,
354 &uv_ray_norms,
355 )?)
356 };
357
358 let profile_settings = ProfileSettings {
359 fail_fast: *fail_fast,
360 n_points: *n_points,
361 min_scale_exponent: *min_scale_exponent,
362 max_scale_exponent: *max_scale_exponent,
363 seed: (*seed).unwrap_or(42),
364 use_f128: *use_f128,
365 analyse_analytically: *analyse_analytically,
366 allow_vanishing_missing_fits: *allow_vanishing_missing_fits,
367 orientation_mode: if *per_orientation {
368 OrientationProfileMode::PerOrientation
369 } else {
370 OrientationProfileMode::Summed
371 },
372 fixed_uv_ray,
373 graph_id,
374 cutkosky_cut: (!cutkosky_cut.is_empty())
375 .then(|| cutkosky_cut.iter().copied().map(EdgeIndex::from).collect()),
376 selected_limits: *selected_limits,
377 ..Default::default()
378 };
379 let profile_res = {
380 let process = &mut state.process_list.processes[process_id];
381 match &mut process.collection {
382 ProcessCollection::Amplitudes(amplitudes) => amplitudes
383 .get_mut(&integrand_name)
384 .ok_or_else(|| {
385 eyre!(
386 "No amplitude named '{}' in process '{}'",
387 integrand_name,
388 process.definition.folder_name
389 )
390 })?
391 .profile(&model, &profile_settings)?,
392 ProcessCollection::CrossSections(cross_sections) => cross_sections
393 .get_mut(&integrand_name)
394 .ok_or_else(|| {
395 eyre!(
396 "No cross section named '{}' in process '{}'",
397 integrand_name,
398 process.definition.folder_name
399 )
400 })?
401 .profile(&model, &profile_settings)?,
402 }
403 }
404 .analyse();
405
406 for t in profile_res.tables_per_graph(UV_PROFILE_MAX_DOD) {
407 info!("\n{}", t);
408 }
409
410 for t in profile_res.analytic_tables_per_graph() {
411 let Some(t) = t else {
412 continue;
413 };
414 info!("\n{}", t);
415 }
416
417 for t in profile_res.per_orientation_tables_per_graph(UV_PROFILE_MAX_DOD) {
418 let Some(t) = t else {
419 continue;
420 };
421 info!("\n{}", t);
422 }
423
424 let verdict = profile_res.pass_fail(UV_PROFILE_MAX_DOD);
425 if profile_res.stopped_early {
426 info!(
427 "Stopped after the first failing UV limit; the summary covers completed limits."
428 );
429 }
430 info!("\n{}", verdict);
431
432 if let Some(file) = output_file {
433 global_cli_settings
434 .ensure_write_target_outside_active_state(file, "write profile output")?;
435 profile_res.write_profile_data(file)?
436 }
437
438 Ok(ProfileResult::UltraViolet(profile_res))
439 }
440 Profile::InfraRed(InfraRedProfile {
441 process,
442 integrand_name,
443 n_points,
444 min_scale_exponent,
445 max_scale_exponent,
446 seed,
447 output_file,
448 select,
449 per_orientation,
450 show_per_cut_info,
451 }) => {
452 let ir_profile_settings = IRProfileSetting {
453 lambda_exp_start: *min_scale_exponent,
454 lambda_exp_end: *max_scale_exponent,
455 steps: *n_points,
456 seed: seed.unwrap_or(420),
457 select_limits_and_graphs: select.clone(),
458 orientation_mode: if *per_orientation {
459 OrientationProfileMode::PerOrientation
460 } else {
461 OrientationProfileMode::Summed
462 },
463 show_per_cut_info: *show_per_cut_info,
464 };
465
466 let (process_id, integrand_name) =
467 state.find_integrand_ref(process.as_ref(), integrand_name.as_ref())?;
468
469 let model = state.resolve_model_for_integrand(process_id, &integrand_name)?;
470
471 let process = &mut state.process_list.processes[process_id];
472 let integrand = match &mut process.collection {
473 ProcessCollection::Amplitudes(amplitudes) => amplitudes
474 .get_mut(&integrand_name)
475 .ok_or_else(|| {
476 eyre!(
477 "No amplitude named '{}' in process '{}'",
478 integrand_name,
479 process.definition.folder_name
480 )
481 })?
482 .integrand
483 .as_mut(),
484 ProcessCollection::CrossSections(xs) => xs
485 .get_mut(&integrand_name)
486 .ok_or_else(|| {
487 eyre!(
488 "No xs named '{}' in process '{}'",
489 integrand_name,
490 process.definition.folder_name
491 )
492 })?
493 .integrand
494 .as_mut(),
495 };
496
497 let profile_result = match integrand.ok_or(eyre!(
498 "Integrand {} has not yet been generated",
499 integrand_name
500 ))? {
501 ProcessIntegrand::CrossSection(cross_section_integrand) => {
502 cross_section_integrand.test_ir(&ir_profile_settings, &model)?
503 }
504 ProcessIntegrand::Amplitude(amplitude_integrand) => {
505 amplitude_integrand.test_ir(&ir_profile_settings, &model)?
506 }
507 };
508
509 info!("\n{}", profile_result);
510 if let Some(file) = output_file {
511 global_cli_settings
512 .ensure_write_target_outside_active_state(file, "write profile output")?;
513 if let Some(parent) = file
514 .parent()
515 .filter(|parent| !parent.as_os_str().is_empty())
516 {
517 std::fs::create_dir_all(parent)?;
518 }
519 let graphs = profile_result
520 .results_per_graph
521 .iter()
522 .map(|graph| {
523 let reports = graph
524 .single_limit_reports
525 .iter()
526 .map(|report| {
527 serde_json::json!({
528 "limit_name": report.limit_name,
529 "orientation_label": report.orientation_label,
530 "ray_fingerprint": report.ray_fingerprint,
531 "passed": report.passed,
532 "scaling": report.scaling,
533 "r_squared": report.power_law_fit.r_squared(),
534 })
535 })
536 .collect::<Vec<_>>();
537 serde_json::json!({
538 "graph_name": graph.graph_name,
539 "all_limits_passed": graph.all_limits_passed,
540 "single_limit_reports": reports,
541 })
542 })
543 .collect::<Vec<_>>();
544 serde_json::to_writer_pretty(
545 File::create(file)?,
546 &serde_json::json!({
547 "settings": {
548 "n_points": n_points,
549 "min_scale_exponent": min_scale_exponent,
550 "max_scale_exponent": max_scale_exponent,
551 "seed": seed.unwrap_or(420),
552 "select": select,
553 "per_orientation": per_orientation,
554 },
555 "all_passed": profile_result.all_passed,
556 "graphs": graphs,
557 }),
558 )?;
559 }
560 Ok(ProfileResult::InfraRed(profile_result))
561 }
562 }
563 }
564}
565
566#[cfg(test)]
567mod tests {
568 use clap::Parser;
569 use gammalooprs::uv::profile::UVLimitSelection;
570
571 use crate::{commands::Commands, Repl};
572
573 use super::Profile;
574
575 #[test]
576 fn uv_profile_cli_defaults_to_only_divergent_limits() {
577 let repl = Repl::try_parse_from(["gammaloop", "profile", "ultra-violet"]).unwrap();
578 let Commands::Profile(Profile::UltraViolet(profile)) = repl.command else {
579 panic!("expected an ultraviolet profile command");
580 };
581
582 assert_eq!(profile.selected_limits, UVLimitSelection::OnlyDivergent);
583 assert_eq!(profile.graph, None);
584 assert!(profile.cutkosky_cut.is_empty());
585 assert!(!profile.fail_fast);
586 let mut legacy = serde_json::to_value(&profile).unwrap();
587 legacy.as_object_mut().unwrap().remove("fail_fast");
588 let restored: super::UltraVioletProfile = serde_json::from_value(legacy).unwrap();
589 assert!(!restored.fail_fast);
590 }
591
592 #[test]
593 fn uv_profile_cli_parses_graph_cut_and_all_limits() {
594 let repl = Repl::try_parse_from([
595 "gammaloop",
596 "profile",
597 "ultra-violet",
598 "--graph",
599 "GL2",
600 "--cutkosky-cut",
601 "5,2",
602 "--selected-limits",
603 "all",
604 "--fail-fast",
605 ])
606 .unwrap();
607 let Commands::Profile(Profile::UltraViolet(profile)) = repl.command else {
608 panic!("expected an ultraviolet profile command");
609 };
610
611 assert_eq!(profile.selected_limits, UVLimitSelection::All);
612 assert_eq!(profile.graph.as_deref(), Some("GL2"));
613 assert_eq!(profile.cutkosky_cut, [5, 2]);
614 assert!(profile.fail_fast);
615 assert_eq!(
616 serde_json::from_value::<super::UltraVioletProfile>(
617 serde_json::to_value(&profile).unwrap()
618 )
619 .unwrap(),
620 profile
621 );
622
623 let alias = Repl::try_parse_from([
624 "gammaloop",
625 "profile",
626 "ultra-violet",
627 "--graph",
628 "GL2",
629 "--cut-edges",
630 "5,2",
631 ])
632 .unwrap();
633 let Commands::Profile(Profile::UltraViolet(alias)) = alias.command else {
634 panic!("expected an ultraviolet profile command");
635 };
636 assert_eq!(alias.cutkosky_cut, [5, 2]);
637 }
638
639 #[test]
640 fn uv_profile_cli_requires_a_graph_for_a_cut() {
641 let result = Repl::try_parse_from([
642 "gammaloop",
643 "profile",
644 "ultra-violet",
645 "--cutkosky-cut",
646 "5,2",
647 ]);
648
649 assert!(result.is_err());
650 }
651}