1use clap::{Args, Subcommand, ValueEnum};
2use colored::Colorize;
3use itertools::Itertools;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use serde_json::Value as JsonValue;
7use std::{
8 ffi::OsStr,
9 fs,
10 io::{self, Write},
11 path::{Path, PathBuf},
12};
13use tabled::{
14 builder::Builder,
15 settings::{style::HorizontalLine, themes::Theme, Style},
16 Tabled,
17};
18use tracing::info;
19use walkdir::WalkDir;
20
21use color_eyre::Result;
22use eyre::{eyre, Context};
23use gammalooprs::{
24 processes::{Amplitude, CrossSection, Process, ProcessCollection, ThresholdCountertermSide},
25 settings::RuntimeSettings,
26};
27
28use crate::{
29 commands::generate::{render_generation_summary, ProcessArgs},
30 commands::process_settings::{
31 observable_kind, quantity_kind, selector_kind, serialize_runtime_named_settings,
32 summarize_observable, summarize_quantity, summarize_selector, NamedProcessSettingKind,
33 },
34 commands::CliArgumentMetadataExt,
35 completion::CompletionArgExt,
36 integrand_info::{
37 IntegrandCutThresholdInfo, IntegrandEsurfaceClassification, IntegrandGraphGroupInfo,
38 IntegrandInfo, IntegrandKind,
39 },
40 session::display_command,
41 settings_tree::{serialize_settings_with_defaults, value_at_path},
42 state::{CommandsBlock, IntegrandGenerationSummary, ProcessRef, RunHistory, State},
43 CLISettings,
44};
45
46mod sampling_metadata;
47mod threshold_metadata;
48
49#[derive(Debug, Clone, Copy, Serialize, Deserialize, ValueEnum, JsonSchema, PartialEq, Eq)]
51#[serde(rename_all = "snake_case")]
52pub enum MetadataDisplayFormat {
53 Pretty,
54 Toml,
55}
56
57#[derive(Subcommand, Debug, Serialize, Deserialize, Clone, JsonSchema, PartialEq)]
58pub enum Display {
59 Model {
61 #[arg(short = 'c', long = "show-couplings", default_value_t = false)]
63 show_couplings: bool,
64 #[arg(short = 'v', long = "show-vertices", default_value_t = false)]
66 show_vertices: bool,
67 #[arg(short = 'r', long = "show-parameters", default_value_t = false)]
69 show_parameters: bool,
70 #[arg(long = "show-particles", default_value_t = false)]
72 show_particles: bool,
73 #[arg(short = 'a', long = "show-all", default_value_t = false)]
75 show_all: bool,
76 #[arg(
78 short = 'p',
79 long = "process",
80 value_name = "PROCESS",
81 cli_requires("integrand_name"),
82 completion_process_selector(crate::completion::SelectorKind::Any)
83 )]
84 process: Option<ProcessRef>,
85 #[arg(
87 short = 'i',
88 long = "integrand-name",
89 value_name = "NAME",
90 cli_requires("process"),
91 completion_integrand_selector(crate::completion::SelectorKind::Any)
92 )]
93 integrand_name: Option<String>,
94 },
95 Processes,
97 #[command(name = "integrand", visible_alias = "integrands")]
99 Integrands {
100 #[arg(
102 short = 'p',
103 long = "process",
104 value_name = "PROCESS",
105 completion_process_selector(crate::completion::SelectorKind::Any)
106 )]
107 process: Option<ProcessRef>,
108 #[arg(
110 short = 'i',
111 long = "integrand-name",
112 value_name = "NAME",
113 cli_requires("process"),
114 completion_integrand_selector(crate::completion::SelectorKind::Any)
115 )]
116 integrand_name: Option<String>,
117 #[arg(
119 short = 'g',
120 long = "graph",
121 value_name = "MASTER_GRAPH",
122 num_args = 1..,
123 cli_requires("integrand_name"),
124 completion_selected_master_graph()
125 )]
126 graphs: Vec<String>,
127 #[arg(
129 long = "category",
130 value_name = "CATEGORY",
131 num_args = 1..,
132 cli_requires("integrand_name"),
133 completion_selected_integrand_category()
134 )]
135 categories: Vec<IntegrandDisplayCategory>,
136 #[arg(
138 long = "hide-non-existing-thresholds",
139 default_value_t = false,
140 cli_requires("integrand_name")
141 )]
142 #[serde(default)]
143 hide_non_existing_thresholds: bool,
144 #[arg(
146 long = "show-threshold-functions",
147 default_value_t = false,
148 requires = "integrand_name"
149 )]
150 #[serde(default)]
151 show_threshold_functions: bool,
152 #[arg(
154 long = "show_sampling",
155 visible_alias = "show-sampling",
156 value_name = "MODE",
157 cli_requires("integrand_name"),
158 cli_requires("process"),
159 conflicts_with = "show_threshold_subtraction"
160 )]
161 #[serde(default, skip_serializing_if = "Option::is_none")]
162 show_sampling: Option<MetadataDisplayFormat>,
163 #[arg(
165 long = "show_threshold_subtraction",
166 visible_alias = "show-threshold-subtraction",
167 value_name = "MODE",
168 cli_requires("integrand_name"),
169 cli_requires("process"),
170 conflicts_with = "show_sampling"
171 )]
172 #[serde(default, skip_serializing_if = "Option::is_none")]
173 show_threshold_subtraction: Option<MetadataDisplayFormat>,
174 },
175 Quantities {
177 #[command(flatten)]
178 target: DisplayProcessNamedSettingsArgs,
179 },
180 Observables {
182 #[command(flatten)]
183 target: DisplayProcessNamedSettingsArgs,
184 },
185 Selectors {
187 #[command(flatten)]
188 target: DisplayProcessNamedSettingsArgs,
189 },
190 #[command(name = "command_block")]
192 CommandBlock {
193 #[arg(value_name = "NAME")]
195 name: Option<String>,
196 },
197 Settings {
199 #[command(subcommand)]
200 target: DisplaySettingsTarget,
201 },
202}
203
204#[derive(Args, Debug, Serialize, Deserialize, Clone, JsonSchema, PartialEq)]
205pub struct DisplayProcessNamedSettingsArgs {
206 #[command(flatten)]
207 process: ProcessArgs,
208 #[arg(value_name = "NAME")]
210 name: Option<String>,
211}
212
213#[derive(Subcommand, Debug, Serialize, Deserialize, Clone, JsonSchema, PartialEq)]
214pub enum DisplaySettingsTarget {
215 Global {
217 #[arg(value_name = "PATH")]
219 path: Option<String>,
220 },
221 #[command(alias = "defaults")]
223 DefaultRuntime {
224 #[arg(value_name = "PATH")]
226 path: Option<String>,
227 },
228 Process {
230 #[command(flatten)]
231 process: ProcessArgs,
232 #[arg(value_name = "PATH")]
234 path: Option<String>,
235 },
236}
237
238impl Display {
239 pub fn run(
240 &self,
241 state: &State,
242 global_settings: &CLISettings,
243 default_runtime_settings: &RuntimeSettings,
244 run_history: &RunHistory,
245 ) -> Result<()> {
246 match self {
247 Display::Integrands {
248 process,
249 integrand_name,
250 graphs,
251 categories,
252 hide_non_existing_thresholds,
253 show_threshold_functions,
254 show_sampling,
255 show_threshold_subtraction,
256 } => {
257 if show_sampling.is_some() && show_threshold_subtraction.is_some() {
258 return Err(eyre!(
259 "--show_sampling and --show_threshold_subtraction are mutually exclusive"
260 ));
261 }
262 if let Some(format) = show_sampling.or(*show_threshold_subtraction) {
263 let process_ref = process.as_ref().ok_or_else(|| {
264 eyre!(
265 "Metadata display requires an explicit --process and --integrand-name"
266 )
267 })?;
268 let integrand_name = integrand_name.as_deref().ok_or_else(|| {
269 eyre!(
270 "Metadata display requires an explicit --process and --integrand-name"
271 )
272 })?;
273 if *show_threshold_subtraction == Some(MetadataDisplayFormat::Toml)
274 && graphs.len() != 1
275 {
276 return Err(eyre!(
277 "Threshold TOML display requires exactly one --graph master name"
278 ));
279 }
280 let process_id = state.resolve_process_ref(Some(process_ref))?;
281 let render = if show_sampling.is_some() {
282 sampling_metadata::render
283 } else {
284 threshold_metadata::render
285 };
286 let output = render(state, process_id, integrand_name, graphs, format)?;
287 match format {
288 MetadataDisplayFormat::Pretty => info!("\n{output}"),
289 MetadataDisplayFormat::Toml => {
290 let mut stdout = io::stdout().lock();
291 stdout.write_all(output.as_bytes())?;
292 stdout.flush()?;
293 }
294 }
295 return Ok(());
296 }
297 let process_id = process
298 .as_ref()
299 .map(|process_ref| state.resolve_process_ref(Some(process_ref)))
300 .transpose()?;
301 if let Some(integrand_name) = integrand_name.as_deref() {
302 let process_id = process_id
303 .ok_or_else(|| eyre!("--integrand-name requires an explicit --process"))?;
304 let detail = state.get_integrand_info(
305 Some(&ProcessRef::Id(process_id)),
306 Some(&integrand_name.to_string()),
307 )?;
308 let process = &state.process_list.processes[process_id];
309 let artifact_sizes = collect_integrand_artifact_sizes(
310 &global_settings.state.folder,
311 process,
312 &detail.integrand_name,
313 )?;
314 render_integrand_detail_from_info(
315 &detail,
316 &artifact_sizes,
317 state.generation_summary(process_id, &detail.integrand_name),
318 graphs,
319 categories,
320 *hide_non_existing_thresholds,
321 *show_threshold_functions,
322 )?;
323 } else {
324 render_integrands_table(state, &global_settings.state.folder, process_id)?;
325 }
326 }
327 Display::Processes => {
328 render_processes_table(state, &global_settings.state.folder)?;
329 }
330 Display::Quantities { target } => {
331 render_named_process_settings(state, target, NamedProcessSettingKind::Quantity)?;
332 }
333 Display::Observables { target } => {
334 render_named_process_settings(state, target, NamedProcessSettingKind::Observable)?;
335 }
336 Display::Selectors { target } => {
337 render_named_process_settings(state, target, NamedProcessSettingKind::Selector)?;
338 }
339 Display::CommandBlock { name } => {
340 render_command_blocks(run_history, name.as_deref())?;
341 }
342 Display::Model {
343 show_couplings,
344 show_vertices,
345 show_parameters,
346 show_particles,
347 show_all,
348 process,
349 integrand_name,
350 } => {
351 let model = if let (Some(process), Some(integrand_name)) =
352 (process.as_ref(), integrand_name.as_ref())
353 {
354 let process_id = state.resolve_process_ref(Some(process))?;
355 state.resolve_model_for_integrand(process_id, integrand_name)?
356 } else {
357 state.model.clone()
358 };
359 info!(
360 "\n{}",
361 model.get_description(
362 *show_particles || *show_all,
363 *show_parameters || *show_all,
364 *show_vertices || *show_all,
365 *show_couplings || *show_all,
366 )
367 )
368 }
369 Display::Settings { target } => {
370 target.run(state, global_settings, default_runtime_settings)?;
371 }
372 }
373 Ok(())
374 }
375}
376
377#[derive(Clone, Debug)]
378struct IntegrandMetrics {
379 name: String,
380 graphs: usize,
381 graph_groups: usize,
382 bin_disk_size_bytes: Option<u64>,
383 so_disk_size_bytes: Option<u64>,
384}
385
386#[derive(Clone, Debug, Default)]
387struct IntegrandArtifactSizes {
388 bin_disk_size_bytes: Option<u64>,
389 so_disk_size_bytes: Option<u64>,
390}
391
392#[derive(
393 Debug, Clone, Copy, Serialize, Deserialize, ValueEnum, JsonSchema, PartialEq, Eq, Hash,
394)]
395#[serde(rename_all = "snake_case")]
396pub enum IntegrandDisplayCategory {
397 #[value(name = "generation")]
398 Generation,
399 #[value(name = "orientation")]
400 Orientation,
401 #[value(name = "loop_momentum_basis")]
402 LoopMomentumBasis,
403 #[value(name = "cuts")]
404 Cuts,
405}
406
407impl IntegrandDisplayCategory {
408 fn title(self) -> &'static str {
409 match self {
410 Self::Generation => "Generation",
411 Self::Orientation => "Orientations",
412 Self::LoopMomentumBasis => "Loop momentum bases",
413 Self::Cuts => "Cuts",
414 }
415 }
416
417 fn value_header(self) -> &'static str {
418 match self {
419 Self::Generation => "generation",
420 Self::Orientation => "orientation",
421 Self::LoopMomentumBasis => "loop momentum basis",
422 Self::Cuts => "cut",
423 }
424 }
425}
426
427#[derive(Tabled)]
428struct DetailSummaryRow {
429 field: String,
430 value: String,
431}
432
433fn orientation_column_widths(edge_ids: &[usize], fallback_count: usize) -> Vec<usize> {
434 if edge_ids.is_empty() {
435 vec![1; fallback_count]
436 } else {
437 edge_ids
438 .iter()
439 .map(|edge_id| edge_id.to_string().len().max(1))
440 .collect()
441 }
442}
443
444fn render_orientation_edge_ids(edge_ids: &[usize]) -> String {
445 if edge_ids.is_empty() {
446 return String::new();
447 }
448
449 let widths = orientation_column_widths(edge_ids, edge_ids.len());
450 edge_ids
451 .iter()
452 .zip(widths.iter())
453 .map(|(edge_id, width)| {
454 format!("{edge_id:<width$}", width = *width)
455 .magenta()
456 .to_string()
457 })
458 .join(" ")
459}
460
461fn render_orientation_signature(signature: &[i8], edge_ids: &[usize]) -> String {
462 let widths = orientation_column_widths(edge_ids, signature.len());
463 signature
464 .iter()
465 .zip(widths.iter())
466 .map(|(sign, width)| {
467 let padded = format!(
468 "{:<width$}",
469 match sign {
470 1 => "+",
471 -1 => "-",
472 0 => "0",
473 _ => "?",
474 },
475 width = *width
476 );
477 match sign {
478 1 => padded.green().bold().to_string(),
479 -1 => padded.red().bold().to_string(),
480 0 => padded.dimmed().to_string(),
481 _ => padded.to_string(),
482 }
483 })
484 .join(" ")
485}
486
487fn render_edge_ids_with_highlight(edge_ids: &[usize], highlight: bool) -> String {
488 if edge_ids.is_empty() {
489 return String::new();
490 }
491
492 let rendered = format!(
493 "({})",
494 edge_ids.iter().map(|edge_id| edge_id.to_string()).join(",")
495 );
496 if highlight {
497 rendered.green().bold().to_string()
498 } else {
499 rendered.magenta().to_string()
500 }
501}
502
503fn render_edge_ids(edge_ids: &[usize]) -> String {
504 render_edge_ids_with_highlight(edge_ids, false)
505}
506
507fn render_threshold_counterterms(
508 thresholds: &[IntegrandCutThresholdInfo],
509 hide_non_existing_thresholds: bool,
510) -> String {
511 let visible = thresholds
512 .iter()
513 .filter(|threshold| !hide_non_existing_thresholds || threshold.status.is_currently_viable())
514 .collect_vec();
515 if visible.is_empty() {
516 return "none".dimmed().to_string();
517 }
518 format!(
519 "[{}]",
520 visible
521 .iter()
522 .map(|threshold| {
523 let id = if threshold.status.can_become_pinched() {
524 format!("{}*", threshold.esurface_id)
525 } else {
526 threshold.esurface_id.to_string()
527 };
528 if threshold.status.is_currently_viable() {
529 id.cyan().to_string()
530 } else {
531 id.dimmed().to_string()
532 }
533 })
534 .join(",")
535 )
536}
537
538fn render_threshold_esurface(edge_ids: &[usize]) -> String {
539 render_edge_ids(edge_ids)
540}
541
542fn render_cut(edge_ids: &[usize], raising_power: usize) -> String {
543 let edge_ids = render_edge_ids(edge_ids);
544 if edge_ids.is_empty() || raising_power <= 1 {
545 edge_ids
546 } else {
547 format!("{edge_ids}^{raising_power}").magenta().to_string()
548 }
549}
550
551#[derive(Clone)]
552struct CategoryRow {
553 primary_id: String,
554 secondary_id: String,
555 tertiary_id: String,
556 value: String,
557}
558
559fn render_category_id(id: usize, highlight: bool) -> String {
560 let rendered = format!("#{id}");
561 if highlight {
562 rendered.green().bold().to_string()
563 } else {
564 rendered.yellow().to_string()
565 }
566}
567
568fn render_optional_category_id(id: Option<usize>, highlight: bool) -> String {
569 id.map(|id| render_category_id(id, highlight))
570 .unwrap_or_default()
571}
572
573fn master_graph(
574 group: &IntegrandGraphGroupInfo,
575) -> Result<&crate::integrand_info::IntegrandGraphInfo> {
576 group
577 .graphs
578 .iter()
579 .find(|graph| graph.is_master)
580 .ok_or_else(|| eyre!("Graph group #{} has no master graph", group.group_id))
581}
582
583fn available_integrand_categories(detail: &IntegrandInfo) -> Vec<IntegrandDisplayCategory> {
584 match detail.kind {
585 IntegrandKind::Amplitude => vec![
586 IntegrandDisplayCategory::Generation,
587 IntegrandDisplayCategory::Orientation,
588 IntegrandDisplayCategory::LoopMomentumBasis,
589 IntegrandDisplayCategory::Cuts,
590 ],
591 IntegrandKind::CrossSection => vec![
592 IntegrandDisplayCategory::Generation,
593 IntegrandDisplayCategory::Orientation,
594 IntegrandDisplayCategory::LoopMomentumBasis,
595 IntegrandDisplayCategory::Cuts,
596 ],
597 }
598}
599
600fn selected_integrand_categories(
601 detail: &IntegrandInfo,
602 requested: &[IntegrandDisplayCategory],
603) -> Result<Vec<IntegrandDisplayCategory>> {
604 let available = available_integrand_categories(detail);
605 if requested.is_empty() {
606 return Ok(available);
607 }
608
609 let mut selected = Vec::new();
610 for category in requested.iter().copied() {
611 if !available.contains(&category) {
612 return Err(eyre!(
613 "Category '{}' is not available for {} integrands.",
614 category.to_possible_value().expect("value enum").get_name(),
615 detail.kind
616 ));
617 }
618 if !selected.contains(&category) {
619 selected.push(category);
620 }
621 }
622 Ok(selected)
623}
624
625fn filtered_graph_groups<'a>(
626 detail: &'a IntegrandInfo,
627 requested_graphs: &[String],
628) -> Result<Vec<&'a IntegrandGraphGroupInfo>> {
629 if requested_graphs.is_empty() {
630 return Ok(detail.graph_groups.iter().collect());
631 }
632
633 let available_master_names = detail
634 .graph_groups
635 .iter()
636 .map(master_graph)
637 .collect::<Result<Vec<_>>>()?
638 .into_iter()
639 .map(|graph| graph.name.clone())
640 .collect::<Vec<_>>();
641 let missing = requested_graphs
642 .iter()
643 .filter(|graph_name| {
644 !available_master_names
645 .iter()
646 .any(|name| name == *graph_name)
647 })
648 .cloned()
649 .collect::<Vec<_>>();
650 if !missing.is_empty() {
651 return Err(eyre!(
652 "Unknown master graph filter(s): {}. Available master graphs: {}",
653 missing.join(", "),
654 available_master_names.join(", ")
655 ));
656 }
657
658 Ok(detail
659 .graph_groups
660 .iter()
661 .filter(|group| {
662 master_graph(group)
663 .map(|graph| requested_graphs.iter().any(|name| name == &graph.name))
664 .unwrap_or(false)
665 })
666 .collect())
667}
668
669fn category_rows(
670 group: &IntegrandGraphGroupInfo,
671 category: IntegrandDisplayCategory,
672 hide_non_existing_thresholds: bool,
673) -> Vec<CategoryRow> {
674 match category {
675 IntegrandDisplayCategory::Generation => Vec::new(),
676 IntegrandDisplayCategory::Orientation => group
677 .orientations
678 .iter()
679 .map(|orientation| CategoryRow {
680 primary_id: render_category_id(orientation.orientation_id, false),
681 secondary_id: String::new(),
682 tertiary_id: String::new(),
683 value: render_orientation_signature(
684 &orientation.signature,
685 &group.orientation_edge_ids,
686 ),
687 })
688 .collect(),
689 IntegrandDisplayCategory::LoopMomentumBasis => group
690 .loop_momentum_bases
691 .iter()
692 .map(|basis| CategoryRow {
693 primary_id: render_category_id(basis.basis_id, basis.matches_generation_basis),
694 secondary_id: render_optional_category_id(
695 basis.channel_id,
696 basis.matches_generation_basis,
697 ),
698 tertiary_id: String::new(),
699 value: render_edge_ids_with_highlight(
700 &basis.edge_ids,
701 basis.matches_generation_basis,
702 ),
703 })
704 .collect(),
705 IntegrandDisplayCategory::Cuts => group
706 .cuts
707 .iter()
708 .map(|cut| CategoryRow {
709 primary_id: render_category_id(cut.cut_id, false),
710 secondary_id: render_threshold_counterterms(
711 &cut.left_thresholds,
712 hide_non_existing_thresholds,
713 ),
714 tertiary_id: render_threshold_counterterms(
715 &cut.right_thresholds,
716 hide_non_existing_thresholds,
717 ),
718 value: render_cut(&cut.edge_ids, cut.raising_power),
719 })
720 .collect(),
721 }
722}
723
724fn category_group_header(
725 group: &IntegrandGraphGroupInfo,
726 category: IntegrandDisplayCategory,
727) -> String {
728 match category {
729 IntegrandDisplayCategory::Orientation => {
730 render_orientation_edge_ids(&group.orientation_edge_ids)
731 }
732 IntegrandDisplayCategory::Generation
733 | IntegrandDisplayCategory::LoopMomentumBasis
734 | IntegrandDisplayCategory::Cuts => String::new(),
735 }
736}
737
738fn render_integrand_thresholds_table(
739 groups: &[&IntegrandGraphGroupInfo],
740 integrand_kind: IntegrandKind,
741) -> Result<Option<String>> {
742 let status_header = match integrand_kind {
743 IntegrandKind::Amplitude => "Classification",
744 IntegrandKind::CrossSection => "Active in cuts",
745 };
746 let mut builder = Builder::new();
747 builder.push_record([
748 "Group #".bold().blue().to_string(),
749 "Graph".bold().blue().to_string(),
750 "Esurface".bold().blue().to_string(),
751 "edges".bold().blue().to_string(),
752 status_header.bold().blue().to_string(),
753 ]);
754
755 let mut inserted_blocks = 0usize;
756 let mut separator_rows = vec![1usize];
757 let mut next_row = 1usize;
758
759 for group in groups {
760 if group.threshold_esurfaces.is_empty() {
761 continue;
762 }
763
764 let master = master_graph(group)?;
765 inserted_blocks += 1;
766 builder.push_record([
767 format!("#{}", group.group_id).blue().to_string(),
768 format!("#{} : {}", master.graph_id, master.name)
769 .green()
770 .bold()
771 .to_string(),
772 String::new(),
773 String::new(),
774 String::new(),
775 ]);
776 next_row += 1;
777 separator_rows.push(next_row);
778
779 for threshold in &group.threshold_esurfaces {
780 let representative_graph = group
781 .graphs
782 .iter()
783 .find(|graph| graph.graph_id == threshold.representative_graph_id)
784 .ok_or_else(|| {
785 eyre!(
786 "Threshold E-surface #{} in graph group #{} references missing representative graph #{}",
787 threshold.esurface_id,
788 group.group_id,
789 threshold.representative_graph_id,
790 )
791 })?;
792 let graph_label = if representative_graph.is_master {
793 String::new()
794 } else {
795 format!(
796 "#{} : {}",
797 representative_graph.graph_id, representative_graph.name
798 )
799 .yellow()
800 .to_string()
801 };
802 let status = match integrand_kind {
803 IntegrandKind::Amplitude => match threshold.classification.ok_or_else(|| {
804 eyre!(
805 "Amplitude threshold E-surface #{} in graph group #{} has no classification",
806 threshold.esurface_id,
807 group.group_id,
808 )
809 })? {
810 IntegrandEsurfaceClassification::Existing => {
811 "existing".cyan().to_string()
812 }
813 IntegrandEsurfaceClassification::Pinched => {
814 "pinched".yellow().to_string()
815 }
816 IntegrandEsurfaceClassification::NonExisting => {
817 "non-existing".dimmed().to_string()
818 }
819 },
820 IntegrandKind::CrossSection => {
821 if threshold.active_cuts.is_empty() {
822 "none".dimmed().to_string()
823 } else {
824 threshold
825 .active_cuts
826 .iter()
827 .map(|cut| {
828 if cut.can_become_pinched {
829 format!("#{}*", cut.cut_id)
830 } else {
831 format!("#{}", cut.cut_id)
832 }
833 })
834 .join(",")
835 .cyan()
836 .to_string()
837 }
838 }
839 };
840 builder.push_record([
841 String::new(),
842 graph_label,
843 format!("#{}", threshold.esurface_id).yellow().to_string(),
844 render_threshold_esurface(&threshold.edge_ids),
845 status,
846 ]);
847 next_row += 1;
848 }
849 separator_rows.push(next_row);
850 }
851
852 if inserted_blocks == 0 {
853 return Ok(None);
854 }
855
856 let mut table = builder.build();
857 let mut style = Theme::from_style(Style::rounded().remove_horizontals());
858 let mut unique_separator_rows = separator_rows.into_iter().unique().collect_vec();
859 if let Some(last_row) = unique_separator_rows.pop() {
860 for row in unique_separator_rows {
861 style.insert_horizontal_line(
862 row,
863 HorizontalLine::new('─')
864 .intersection('┼')
865 .left('├')
866 .right('┤'),
867 );
868 }
869 style.insert_horizontal_line(
870 last_row,
871 HorizontalLine::new('─')
872 .intersection('┴')
873 .left('╰')
874 .right('╯'),
875 );
876 }
877 table.with(style);
878 Ok(Some(table.to_string()))
879}
880
881fn render_threshold_counterterm_directives(
882 groups: &[&IntegrandGraphGroupInfo],
883 show_functions: bool,
884) -> Option<String> {
885 let graphs = groups
886 .iter()
887 .flat_map(|group| &group.graphs)
888 .filter(|graph| !graph.threshold_counterterm_directives.is_empty())
889 .collect_vec();
890 if graphs.is_empty() {
891 return None;
892 }
893
894 let mut builder = Builder::new();
895 builder.push_record([
896 "G".bold().blue().to_string(),
897 "C".bold().blue().to_string(),
898 "T".bold().blue().to_string(),
899 "V".bold().blue().to_string(),
900 "↔".bold().blue().to_string(),
901 "S".bold().blue().to_string(),
902 "P".bold().blue().to_string(),
903 "f".bold().blue().to_string(),
904 ]);
905
906 for graph in graphs {
907 let mut previous_cut: Option<&[usize]> = None;
908 let mut previous_threshold: Option<&[usize]> = None;
909 for (directive_index, directive) in
910 graph.threshold_counterterm_directives.iter().enumerate()
911 {
912 let resolved = graph.threshold_counterterms.as_ref().and_then(|registry| {
913 registry.variants.iter().find(|variant| {
914 variant.name == directive.name
915 && variant.associations.iter().any(|association| {
916 association.cut_edges == directive.cut_edge_ids
917 && association.threshold_edges == directive.threshold_edge_ids
918 })
919 })
920 });
921 let suffix = if directive.implicit_default { "*" } else { "" };
922 let variant = if directive.disabled {
923 format!("× {}{suffix}", directive.name).red().to_string()
924 } else if resolved.is_some_and(|variant| variant.active) {
925 format!("● {}{suffix}", directive.name).green().to_string()
926 } else {
927 format!("○ {}{suffix}", directive.name).yellow().to_string()
928 };
929 let side = resolved
930 .map(|variant| match variant.side {
931 ThresholdCountertermSide::Amplitude => "A".cyan().to_string(),
932 ThresholdCountertermSide::Left => "L".green().to_string(),
933 ThresholdCountertermSide::Right => "R".yellow().to_string(),
934 })
935 .unwrap_or_else(|| "?".dimmed().to_string());
936 let subspace = directive
937 .requested_subspace
938 .as_deref()
939 .map(render_edge_ids)
940 .unwrap_or_else(|| "max".dimmed().to_string());
941 let parent = directive
942 .requested_parent_lmb
943 .as_deref()
944 .map(render_edge_ids)
945 .unwrap_or_else(|| "auto".dimmed().to_string());
946 let multiplier = directive
947 .multiplier
948 .as_ref()
949 .map(|multiplier| {
950 if show_functions {
951 let definitions = multiplier
952 .function_map
953 .iter()
954 .map(|(name, expression)| format!("\n{name} = {expression}"))
955 .collect::<String>();
956 format!(
957 "{} [s={},o={}]{}",
958 multiplier.expression,
959 u8::from(multiplier.symmetrize),
960 u8::from(multiplier.opaque_derivatives),
961 definitions,
962 )
963 .magenta()
964 .to_string()
965 } else {
966 "ƒ".magenta().bold().to_string()
967 }
968 })
969 .unwrap_or_else(|| "1".dimmed().to_string());
970 let graph_label = if directive_index == 0 {
971 format!("#{} {}", graph.graph_id, graph.name)
972 .blue()
973 .to_string()
974 } else {
975 "·".dimmed().to_string()
976 };
977 let cut = if previous_cut == Some(directive.cut_edge_ids.as_slice()) {
978 "·".dimmed().to_string()
979 } else if directive.cut_edge_ids.is_empty() {
980 "∅".cyan().to_string()
981 } else {
982 render_edge_ids(&directive.cut_edge_ids)
983 };
984 let threshold = if previous_threshold == Some(directive.threshold_edge_ids.as_slice())
985 && previous_cut == Some(directive.cut_edge_ids.as_slice())
986 {
987 "·".dimmed().to_string()
988 } else {
989 format!(
990 "({})",
991 directive
992 .threshold_edge_ids
993 .iter()
994 .map(ToString::to_string)
995 .join(",")
996 )
997 .yellow()
998 .to_string()
999 };
1000 builder.push_record([
1001 graph_label,
1002 cut,
1003 threshold,
1004 variant,
1005 side,
1006 subspace,
1007 parent,
1008 multiplier,
1009 ]);
1010 previous_cut = Some(directive.cut_edge_ids.as_slice());
1011 previous_threshold = Some(directive.threshold_edge_ids.as_slice());
1012 }
1013 }
1014
1015 let mut table = builder.build();
1016 table.with(Style::rounded().remove_horizontals());
1017 Some(format!(
1018 "{table}\n{}",
1019 "● active ○ inactive/dormant × disabled · repeated * shorthand default; ↔ = A/L/R, S = subspace, P = parent LMB"
1020 .dimmed()
1021 ))
1022}
1023
1024fn render_integrand_category_table(
1025 groups: &[&IntegrandGraphGroupInfo],
1026 category: IntegrandDisplayCategory,
1027 hide_non_existing_thresholds: bool,
1028) -> Result<Option<String>> {
1029 let mut builder = Builder::new();
1030 match category {
1031 IntegrandDisplayCategory::LoopMomentumBasis => {
1032 builder.push_record([
1033 "Group #".bold().blue().to_string(),
1034 "Graph".bold().blue().to_string(),
1035 "basis ID".bold().blue().to_string(),
1036 "channel ID".bold().blue().to_string(),
1037 category.value_header().bold().blue().to_string(),
1038 ]);
1039 }
1040 IntegrandDisplayCategory::Cuts => {
1041 builder.push_record([
1042 "Group #".bold().blue().to_string(),
1043 "Graph".bold().blue().to_string(),
1044 "ID".bold().blue().to_string(),
1045 category.value_header().bold().blue().to_string(),
1046 "left thresholds".bold().blue().to_string(),
1047 "right thresholds".bold().blue().to_string(),
1048 ]);
1049 }
1050 _ => {
1051 builder.push_record([
1052 "Group #".bold().blue().to_string(),
1053 "Graph".bold().blue().to_string(),
1054 "ID".bold().blue().to_string(),
1055 category.value_header().bold().blue().to_string(),
1056 ]);
1057 }
1058 }
1059
1060 let mut inserted_blocks = 0usize;
1061 let mut separator_rows = vec![1usize];
1062 let mut next_row = 1usize;
1063
1064 for group in groups {
1065 let rows = category_rows(group, category, hide_non_existing_thresholds);
1066 if rows.is_empty() {
1067 continue;
1068 }
1069
1070 let master = master_graph(group)?;
1071 inserted_blocks += 1;
1072 match category {
1073 IntegrandDisplayCategory::LoopMomentumBasis => {
1074 builder.push_record([
1075 format!("#{}", group.group_id).blue().to_string(),
1076 format!("#{} : {}", master.graph_id, master.name)
1077 .green()
1078 .bold()
1079 .to_string(),
1080 String::new(),
1081 String::new(),
1082 category_group_header(group, category),
1083 ]);
1084 }
1085 IntegrandDisplayCategory::Cuts => {
1086 builder.push_record([
1087 format!("#{}", group.group_id).blue().to_string(),
1088 format!("#{} : {}", master.graph_id, master.name)
1089 .green()
1090 .bold()
1091 .to_string(),
1092 String::new(),
1093 category_group_header(group, category),
1094 String::new(),
1095 String::new(),
1096 ]);
1097 }
1098 _ => {
1099 builder.push_record([
1100 format!("#{}", group.group_id).blue().to_string(),
1101 format!("#{} : {}", master.graph_id, master.name)
1102 .green()
1103 .bold()
1104 .to_string(),
1105 String::new(),
1106 category_group_header(group, category),
1107 ]);
1108 }
1109 }
1110 next_row += 1;
1111 separator_rows.push(next_row);
1112
1113 for row in rows {
1114 match category {
1115 IntegrandDisplayCategory::LoopMomentumBasis => {
1116 builder.push_record([
1117 String::new(),
1118 String::new(),
1119 row.primary_id,
1120 row.secondary_id,
1121 row.value,
1122 ]);
1123 }
1124 IntegrandDisplayCategory::Cuts => {
1125 builder.push_record([
1126 String::new(),
1127 String::new(),
1128 row.primary_id,
1129 row.value,
1130 row.secondary_id,
1131 row.tertiary_id,
1132 ]);
1133 }
1134 _ => {
1135 builder.push_record([String::new(), String::new(), row.primary_id, row.value]);
1136 }
1137 }
1138 next_row += 1;
1139 }
1140 separator_rows.push(next_row);
1141 }
1142
1143 if inserted_blocks == 0 {
1144 return Ok(None);
1145 }
1146
1147 let mut table = builder.build();
1148 let mut style = Theme::from_style(Style::rounded().remove_horizontals());
1149 let mut unique_separator_rows = separator_rows.into_iter().unique().collect_vec();
1150 if let Some(last_row) = unique_separator_rows.pop() {
1151 for row in unique_separator_rows {
1152 style.insert_horizontal_line(
1153 row,
1154 HorizontalLine::new('─')
1155 .intersection('┼')
1156 .left('├')
1157 .right('┤'),
1158 );
1159 }
1160 style.insert_horizontal_line(
1161 last_row,
1162 HorizontalLine::new('─')
1163 .intersection('┴')
1164 .left('╰')
1165 .right('╯'),
1166 );
1167 }
1168 table.with(style);
1169 Ok(Some(table.to_string()))
1170}
1171
1172fn render_generation_category(summary: Option<&IntegrandGenerationSummary>) -> String {
1173 summary
1174 .and_then(|summary| {
1175 render_generation_summary(&summary.reports, summary.peak_ram_bytes, None, None)
1176 })
1177 .unwrap_or_else(|| {
1178 "No generation summary has been recorded for this integrand."
1179 .yellow()
1180 .to_string()
1181 })
1182}
1183
1184fn render_integrand_detail_from_info(
1185 detail: &IntegrandInfo,
1186 artifact_sizes: &IntegrandArtifactSizes,
1187 generation_summary: Option<&IntegrandGenerationSummary>,
1188 requested_graphs: &[String],
1189 requested_categories: &[IntegrandDisplayCategory],
1190 hide_non_existing_thresholds: bool,
1191 show_threshold_functions: bool,
1192) -> Result<()> {
1193 info!(
1194 "{}",
1195 format!(
1196 "Integrand '{}' for process #{} ({})",
1197 detail.integrand_name, detail.process_id, detail.process_name
1198 )
1199 .bold()
1200 .blue()
1201 );
1202
1203 let summary_rows = vec![
1204 DetailSummaryRow {
1205 field: "kind".to_string(),
1206 value: detail.kind.to_string().yellow().to_string(),
1207 },
1208 DetailSummaryRow {
1209 field: "compile enabled".to_string(),
1210 value: detail
1211 .generation_compilation
1212 .compile_enabled()
1213 .to_string()
1214 .yellow()
1215 .to_string(),
1216 },
1217 DetailSummaryRow {
1218 field: "generation backend".to_string(),
1219 value: detail
1220 .generation_compilation
1221 .active_backend_name()
1222 .yellow()
1223 .to_string(),
1224 },
1225 DetailSummaryRow {
1226 field: "generation compile options".to_string(),
1227 value: detail
1228 .generation_compilation
1229 .external_options()
1230 .map(ToString::to_string)
1231 .unwrap_or_else(|| "(none)".to_string())
1232 .magenta()
1233 .to_string(),
1234 },
1235 DetailSummaryRow {
1236 field: "active f64 backend".to_string(),
1237 value: detail.active_f64_backend.to_string().yellow().to_string(),
1238 },
1239 DetailSummaryRow {
1240 field: "graphs".to_string(),
1241 value: detail.graph_count.to_string().yellow().to_string(),
1242 },
1243 DetailSummaryRow {
1244 field: "graph groups".to_string(),
1245 value: detail.graph_group_count.to_string().yellow().to_string(),
1246 },
1247 DetailSummaryRow {
1248 field: ".bin size".to_string(),
1249 value: format_artifact_size(artifact_sizes.bin_disk_size_bytes),
1250 },
1251 DetailSummaryRow {
1252 field: ".so size".to_string(),
1253 value: format_artifact_size(artifact_sizes.so_disk_size_bytes),
1254 },
1255 ];
1256 let mut summary_table = tabled::Table::new(summary_rows);
1257 summary_table.with(Style::rounded());
1258 info!(
1259 "
1260{summary_table}"
1261 );
1262
1263 let groups = filtered_graph_groups(detail, requested_graphs)?;
1264 let categories = selected_integrand_categories(detail, requested_categories)?;
1265 for category in categories {
1266 if category == IntegrandDisplayCategory::Generation {
1267 let table = render_generation_category(generation_summary);
1268 info!(
1269 "
1270{}
1271{table}",
1272 category.title().bold().blue()
1273 );
1274 continue;
1275 }
1276
1277 if let Some(table) =
1278 render_integrand_category_table(&groups, category, hide_non_existing_thresholds)?
1279 {
1280 info!(
1281 "
1282{}
1283{table}",
1284 category.title().bold().blue()
1285 );
1286 }
1287
1288 if category == IntegrandDisplayCategory::Cuts {
1289 if let Some(table) = render_integrand_thresholds_table(&groups, detail.kind)? {
1290 info!(
1291 "
1292{}
1293{table}",
1294 "Threshold esurfaces".bold().blue()
1295 );
1296 }
1297 if let Some(table) =
1298 render_threshold_counterterm_directives(&groups, show_threshold_functions)
1299 {
1300 info!(
1301 "
1302{}
1303{table}",
1304 "Threshold counterterm directives".bold().blue()
1305 );
1306 }
1307 }
1308 }
1309
1310 Ok(())
1311}
1312
1313fn render_processes_table(state: &State, state_folder: &Path) -> Result<()> {
1314 if state.process_list.processes.is_empty() {
1315 info!("{}", "No processes generated yet.".yellow());
1316 return Ok(());
1317 }
1318
1319 let mut builder = Builder::new();
1320 builder.push_record([
1321 "process #".bold().blue().to_string(),
1322 "process".bold().blue().to_string(),
1323 "# integrands".bold().blue().to_string(),
1324 "integrand names".bold().blue().to_string(),
1325 "graphs / integrand".bold().blue().to_string(),
1326 "graph groups / integrand".bold().blue().to_string(),
1327 ".bin size / integrand".bold().blue().to_string(),
1328 ".so size / integrand".bold().blue().to_string(),
1329 ]);
1330
1331 for process in &state.process_list.processes {
1332 let metrics = collect_integrand_metrics(state_folder, process)?;
1333 builder.push_record([
1334 format!("#{}", process.definition.process_id)
1335 .blue()
1336 .to_string(),
1337 process
1338 .definition
1339 .folder_name
1340 .clone()
1341 .green()
1342 .bold()
1343 .to_string(),
1344 metrics.len().to_string().yellow().to_string(),
1345 format_integrand_names(&metrics),
1346 format_metrics_column(&metrics, |metric| metric.graphs.to_string()),
1347 format_metrics_column(&metrics, |metric| metric.graph_groups.to_string()),
1348 format_metrics_column(&metrics, |metric| {
1349 format_artifact_size(metric.bin_disk_size_bytes)
1350 }),
1351 format_metrics_column(&metrics, |metric| {
1352 format_artifact_size(metric.so_disk_size_bytes)
1353 }),
1354 ]);
1355 }
1356
1357 let mut table = builder.build();
1358 table.with(Style::rounded());
1359 info!("{}", "Processes".bold().blue());
1360 info!("\n{table}");
1361 Ok(())
1362}
1363
1364fn render_integrands_table(
1365 state: &State,
1366 state_folder: &Path,
1367 process_filter: Option<usize>,
1368) -> Result<()> {
1369 if state.process_list.processes.is_empty() {
1370 info!("{}", "No processes generated yet.".yellow());
1371 return Ok(());
1372 }
1373
1374 let mut builder = Builder::new();
1375 builder.push_record([
1376 "process #".bold().blue().to_string(),
1377 "process".bold().blue().to_string(),
1378 "integrand".bold().blue().to_string(),
1379 "graphs".bold().blue().to_string(),
1380 "graph groups".bold().blue().to_string(),
1381 ".bin size".bold().blue().to_string(),
1382 ".so size".bold().blue().to_string(),
1383 ]);
1384
1385 let mut num_rows = 0usize;
1386 for (process_index, process) in state.process_list.processes.iter().enumerate() {
1387 if process_filter.is_some_and(|selected| selected != process_index) {
1388 continue;
1389 }
1390 let metrics = collect_integrand_metrics(state_folder, process)?;
1391 for metric in metrics {
1392 num_rows += 1;
1393 builder.push_record([
1394 format!("#{}", process.definition.process_id)
1395 .blue()
1396 .to_string(),
1397 process
1398 .definition
1399 .folder_name
1400 .clone()
1401 .green()
1402 .bold()
1403 .to_string(),
1404 metric.name.cyan().to_string(),
1405 metric.graphs.to_string().yellow().to_string(),
1406 metric.graph_groups.to_string().yellow().to_string(),
1407 format_artifact_size(metric.bin_disk_size_bytes),
1408 format_artifact_size(metric.so_disk_size_bytes),
1409 ]);
1410 }
1411 }
1412
1413 if num_rows == 0 {
1414 if let Some(process_id) = process_filter {
1415 let process = &state.process_list.processes[process_id];
1416 info!(
1417 "{}",
1418 format!(
1419 "No integrands found for process #{} ({})",
1420 process.definition.process_id, process.definition.folder_name
1421 )
1422 .yellow()
1423 );
1424 } else {
1425 info!("{}", "No integrands found.".yellow());
1426 }
1427 return Ok(());
1428 }
1429
1430 let mut table = builder.build();
1431 table.with(Style::rounded());
1432
1433 if let Some(process_id) = process_filter {
1434 let process = &state.process_list.processes[process_id];
1435 info!(
1436 "{}",
1437 format!(
1438 "Integrands for process #{} ({})",
1439 process.definition.process_id, process.definition.folder_name
1440 )
1441 .bold()
1442 .blue()
1443 );
1444 } else {
1445 info!("{}", "Integrands".bold().blue());
1446 }
1447 info!("\n{table}");
1448 Ok(())
1449}
1450
1451fn collect_integrand_metrics(
1452 state_folder: &Path,
1453 process: &Process,
1454) -> Result<Vec<IntegrandMetrics>> {
1455 match &process.collection {
1456 ProcessCollection::Amplitudes(amplitudes) => amplitudes
1457 .values()
1458 .map(|amplitude| integrand_metrics_from_amplitude(state_folder, process, amplitude))
1459 .collect(),
1460 ProcessCollection::CrossSections(cross_sections) => cross_sections
1461 .values()
1462 .map(|cross_section| {
1463 integrand_metrics_from_cross_section(state_folder, process, cross_section)
1464 })
1465 .collect(),
1466 }
1467}
1468
1469fn integrand_metrics_from_amplitude(
1470 state_folder: &Path,
1471 process: &Process,
1472 amplitude: &Amplitude,
1473) -> Result<IntegrandMetrics> {
1474 let artifact_sizes = collect_integrand_artifact_sizes(state_folder, process, &litude.name)?;
1475 Ok(IntegrandMetrics {
1476 name: amplitude.name.clone(),
1477 graphs: amplitude.graphs.len(),
1478 graph_groups: amplitude.graph_group_structure.len(),
1479 bin_disk_size_bytes: artifact_sizes.bin_disk_size_bytes,
1480 so_disk_size_bytes: artifact_sizes.so_disk_size_bytes,
1481 })
1482}
1483
1484fn integrand_metrics_from_cross_section(
1485 state_folder: &Path,
1486 process: &Process,
1487 cross_section: &CrossSection,
1488) -> Result<IntegrandMetrics> {
1489 let artifact_sizes =
1490 collect_integrand_artifact_sizes(state_folder, process, &cross_section.name)?;
1491 Ok(IntegrandMetrics {
1492 name: cross_section.name.clone(),
1493 graphs: cross_section.supergraphs.len(),
1494 graph_groups: cross_section.graph_group_structure.len(),
1495 bin_disk_size_bytes: artifact_sizes.bin_disk_size_bytes,
1496 so_disk_size_bytes: artifact_sizes.so_disk_size_bytes,
1497 })
1498}
1499
1500fn collect_integrand_artifact_sizes(
1501 state_folder: &Path,
1502 process: &Process,
1503 integrand_name: &str,
1504) -> Result<IntegrandArtifactSizes> {
1505 let artifact_root = integrand_artifact_root(state_folder, process, integrand_name);
1506 Ok(IntegrandArtifactSizes {
1507 bin_disk_size_bytes: disk_usage_for_extension(&artifact_root, "bin")?,
1508 so_disk_size_bytes: disk_usage_for_extension(&artifact_root, "so")?,
1509 })
1510}
1511
1512fn integrand_artifact_root(
1513 state_folder: &Path,
1514 process: &Process,
1515 integrand_name: &str,
1516) -> PathBuf {
1517 let collection_dir = match &process.collection {
1518 ProcessCollection::Amplitudes(_) => "amplitudes",
1519 ProcessCollection::CrossSections(_) => "cross_sections",
1520 };
1521 state_folder
1522 .join("processes")
1523 .join(collection_dir)
1524 .join(&process.definition.folder_name)
1525 .join(integrand_name)
1526}
1527
1528fn disk_usage_for_extension(root: &Path, extension: &str) -> Result<Option<u64>> {
1529 if !root.try_exists().with_context(|| {
1530 format!(
1531 "While checking saved integrand artifacts under {}",
1532 root.display()
1533 )
1534 })? {
1535 return Ok(None);
1536 }
1537
1538 let mut total = 0u64;
1539 for entry in WalkDir::new(root) {
1540 let entry = entry.with_context(|| {
1541 format!(
1542 "While walking saved integrand artifacts under {}",
1543 root.display()
1544 )
1545 })?;
1546 if !entry.file_type().is_file() || entry.path().extension() != Some(OsStr::new(extension)) {
1547 continue;
1548 }
1549
1550 let metadata = entry
1551 .metadata()
1552 .with_context(|| format!("While reading metadata for {}", entry.path().display()))?;
1553 total = total
1554 .checked_add(file_disk_usage_bytes(&metadata))
1555 .ok_or_else(|| {
1556 eyre!(
1557 "Artifact size overflow while summing .{} files under {}",
1558 extension,
1559 root.display()
1560 )
1561 })?;
1562 }
1563 Ok(Some(total))
1564}
1565
1566fn file_disk_usage_bytes(metadata: &fs::Metadata) -> u64 {
1567 #[cfg(unix)]
1568 {
1569 use std::os::unix::fs::MetadataExt;
1570 metadata.blocks().saturating_mul(512)
1571 }
1572
1573 #[cfg(not(unix))]
1574 {
1575 metadata.len()
1576 }
1577}
1578
1579fn format_integrand_names(metrics: &[IntegrandMetrics]) -> String {
1580 if metrics.is_empty() {
1581 return "(none)".dimmed().to_string();
1582 }
1583 metrics
1584 .iter()
1585 .map(|metric| metric.name.clone().cyan().to_string())
1586 .collect::<Vec<_>>()
1587 .join("\n")
1588}
1589
1590fn format_metrics_column(
1591 metrics: &[IntegrandMetrics],
1592 format_value: impl Fn(&IntegrandMetrics) -> String,
1593) -> String {
1594 if metrics.is_empty() {
1595 return "(none)".dimmed().to_string();
1596 }
1597 metrics
1598 .iter()
1599 .map(|metric| format!("{}: {}", metric.name.cyan(), format_value(metric)))
1600 .collect::<Vec<_>>()
1601 .join("\n")
1602}
1603
1604fn format_artifact_size(size: Option<u64>) -> String {
1605 size.map(|size| format_bytes(size).magenta().to_string())
1606 .unwrap_or_else(|| "(not saved)".dimmed().to_string())
1607}
1608
1609fn format_bytes(size: u64) -> String {
1610 const KIB: u64 = 1024;
1611 const MIB: u64 = KIB * 1024;
1612 const GIB: u64 = MIB * 1024;
1613
1614 if size < KIB {
1615 format!("{size} B")
1616 } else if size < MIB {
1617 format!("{:.2} KiB", size as f64 / KIB as f64)
1618 } else if size < GIB {
1619 format!("{:.2} MiB", size as f64 / MIB as f64)
1620 } else {
1621 format!("{:.2} GiB", size as f64 / GIB as f64)
1622 }
1623}
1624
1625fn command_block_contents(block: &CommandsBlock) -> String {
1626 if block.commands.is_empty() {
1627 return "(empty)".to_string();
1628 }
1629
1630 block
1631 .commands
1632 .iter()
1633 .map(display_command)
1634 .collect::<Vec<_>>()
1635 .join("\n")
1636}
1637
1638fn render_command_blocks(run_history: &RunHistory, selected_name: Option<&str>) -> Result<()> {
1639 if let Some(name) = selected_name {
1640 let block = run_history.command_block(name).ok_or_else(|| {
1641 eyre!(
1642 "Unknown command block '{}'. Available command blocks: {}",
1643 name,
1644 run_history
1645 .command_blocks
1646 .iter()
1647 .map(|block| block.name.as_str())
1648 .collect::<Vec<_>>()
1649 .join(", ")
1650 )
1651 })?;
1652 info!("\n{}", command_block_contents(block));
1653 return Ok(());
1654 }
1655
1656 if run_history.command_blocks.is_empty() {
1657 info!("{}", "No active command blocks.".yellow());
1658 return Ok(());
1659 }
1660
1661 info!("{}", "Command blocks".bold().blue());
1662 info!("\n{}", render_command_blocks_table(run_history));
1663 Ok(())
1664}
1665
1666fn render_command_blocks_table(run_history: &RunHistory) -> String {
1667 let mut builder = Builder::new();
1668 builder.push_record([
1669 "name".bold().blue().to_string(),
1670 "placeholders".bold().blue().to_string(),
1671 "commands".bold().blue().to_string(),
1672 ]);
1673
1674 for block in &run_history.command_blocks {
1675 let placeholders = run_history
1676 .command_block_placeholder_specs(&block.name)
1677 .into_iter()
1678 .map(|spec| spec.render())
1679 .collect::<Vec<_>>()
1680 .join("\n");
1681 builder.push_record([
1682 block.name.green().bold().to_string(),
1683 if placeholders.is_empty() {
1684 "-".to_string()
1685 } else {
1686 placeholders
1687 },
1688 command_block_contents(block),
1689 ]);
1690 }
1691
1692 let mut table = builder.build();
1693 let mut style = Theme::from_style(Style::rounded().remove_horizontals());
1694 for row in 1..=run_history.command_blocks.len() {
1695 style.insert_horizontal_line(
1696 row,
1697 HorizontalLine::new('─')
1698 .intersection('┼')
1699 .left('├')
1700 .right('┤'),
1701 );
1702 }
1703 table.with(style);
1704 table.to_string()
1705}
1706
1707fn render_named_process_settings(
1708 state: &State,
1709 target: &DisplayProcessNamedSettingsArgs,
1710 setting_kind: NamedProcessSettingKind,
1711) -> Result<()> {
1712 let process_id = state.resolve_process_ref(target.process.process.as_ref())?;
1713 let process = &state.process_list.processes[process_id];
1714 let integrand_names = selected_integrand_names(process, target.process.integrand_name.as_ref());
1715
1716 if let Some(name) = target.name.as_deref() {
1717 return render_named_process_setting_detail(
1718 state,
1719 process_id,
1720 &process.definition.folder_name,
1721 process.definition.process_id,
1722 &integrand_names,
1723 setting_kind,
1724 name,
1725 );
1726 }
1727
1728 let mut builder = Builder::new();
1729 let show_integrand = integrand_names.len() > 1;
1730 let mut header = Vec::new();
1731 if show_integrand {
1732 header.push("integrand".bold().blue().to_string());
1733 }
1734 header.push("name".bold().blue().to_string());
1735 header.push("kind".bold().blue().to_string());
1736 header.push("details".bold().blue().to_string());
1737 builder.push_record(header);
1738
1739 let mut num_rows = 0usize;
1740 for integrand_name in &integrand_names {
1741 let integrand = state
1742 .process_list
1743 .get_integrand(process_id, integrand_name)?
1744 .require_generated()?;
1745 match setting_kind {
1746 NamedProcessSettingKind::Quantity => {
1747 for (name, settings) in &integrand.get_settings().quantities {
1748 num_rows += 1;
1749 let mut record = Vec::new();
1750 if show_integrand {
1751 record.push(integrand_name.clone().cyan().to_string());
1752 }
1753 record.push(name.clone().green().to_string());
1754 record.push(quantity_kind(settings).yellow().to_string());
1755 record.push(summarize_quantity(settings));
1756 builder.push_record(record);
1757 }
1758 }
1759 NamedProcessSettingKind::Observable => {
1760 for (name, settings) in &integrand.get_settings().observables {
1761 num_rows += 1;
1762 let mut record = Vec::new();
1763 if show_integrand {
1764 record.push(integrand_name.clone().cyan().to_string());
1765 }
1766 record.push(name.clone().green().to_string());
1767 record.push(observable_kind(settings).yellow().to_string());
1768 record.push(summarize_observable(settings));
1769 builder.push_record(record);
1770 }
1771 }
1772 NamedProcessSettingKind::Selector => {
1773 for (name, settings) in &integrand.get_settings().selectors {
1774 num_rows += 1;
1775 let mut record = Vec::new();
1776 if show_integrand {
1777 record.push(integrand_name.clone().cyan().to_string());
1778 }
1779 record.push(name.clone().green().to_string());
1780 record.push(selector_kind(settings).yellow().to_string());
1781 record.push(summarize_selector(settings));
1782 builder.push_record(record);
1783 }
1784 }
1785 }
1786 }
1787
1788 if num_rows == 0 {
1789 info!(
1790 "{}",
1791 format!(
1792 "No {} configured for process #{} ({})",
1793 setting_kind.plural(),
1794 process.definition.process_id,
1795 process.definition.folder_name
1796 )
1797 .yellow()
1798 );
1799 return Ok(());
1800 }
1801
1802 let mut table = builder.build();
1803 table.with(Style::rounded());
1804 let title = if show_integrand {
1805 format!(
1806 "{} for process #{} ({})",
1807 setting_kind.plural(),
1808 process.definition.process_id,
1809 process.definition.folder_name
1810 )
1811 } else {
1812 format!(
1813 "{} for process #{} ({}) integrand '{}'",
1814 setting_kind.plural(),
1815 process.definition.process_id,
1816 process.definition.folder_name,
1817 integrand_names[0]
1818 )
1819 };
1820 info!("{}", title.bold().blue());
1821 info!("\n{table}");
1822 Ok(())
1823}
1824
1825fn render_named_process_setting_detail(
1826 state: &State,
1827 process_id: usize,
1828 process_name: &str,
1829 process_display_id: usize,
1830 integrand_names: &[String],
1831 setting_kind: NamedProcessSettingKind,
1832 name: &str,
1833) -> Result<()> {
1834 let mut selected_roots = Vec::new();
1835 let mut missing_integrands = Vec::new();
1836
1837 for integrand_name in integrand_names {
1838 let integrand = state
1839 .process_list
1840 .get_integrand(process_id, integrand_name)?
1841 .require_generated()?;
1842 let serialized = serialize_runtime_named_settings(integrand.get_settings())?;
1843 let map = match setting_kind {
1844 NamedProcessSettingKind::Quantity => &serialized.quantities,
1845 NamedProcessSettingKind::Observable => &serialized.observables,
1846 NamedProcessSettingKind::Selector => &serialized.selectors,
1847 };
1848 if let Some(root) = map.get(name) {
1849 selected_roots.push((integrand_name.clone(), root.clone()));
1850 } else {
1851 missing_integrands.push(integrand_name.clone());
1852 }
1853 }
1854
1855 if selected_roots.is_empty() {
1856 return Err(eyre!(
1857 "No {} named '{}' found for process #{} ({})",
1858 setting_kind.singular(),
1859 name,
1860 process_display_id,
1861 process_name
1862 ));
1863 }
1864
1865 if !missing_integrands.is_empty() {
1866 return Err(eyre!(
1867 "{} '{}' is missing from integrand(s): {}",
1868 setting_kind.singular(),
1869 name,
1870 missing_integrands.join(", ")
1871 ));
1872 }
1873
1874 for (integrand_name, root) in selected_roots {
1875 render_settings_table(
1876 &format!(
1877 "{} '{}' for process #{} ({}) integrand '{}'",
1878 setting_kind.singular(),
1879 name,
1880 process_display_id,
1881 process_name,
1882 integrand_name
1883 ),
1884 &root,
1885 None,
1886 )?;
1887 }
1888
1889 Ok(())
1890}
1891
1892fn selected_integrand_names(process: &Process, requested: Option<&String>) -> Vec<String> {
1893 if let Some(integrand_name) = requested {
1894 vec![integrand_name.clone()]
1895 } else {
1896 process
1897 .get_integrand_names()
1898 .into_iter()
1899 .map(str::to_string)
1900 .collect()
1901 }
1902}
1903
1904impl DisplaySettingsTarget {
1905 fn run(
1906 &self,
1907 state: &State,
1908 global_settings: &CLISettings,
1909 default_runtime_settings: &RuntimeSettings,
1910 ) -> Result<()> {
1911 match self {
1912 DisplaySettingsTarget::Global { path } => {
1913 let root = serialize_settings_with_defaults(
1914 global_settings,
1915 "global settings for display",
1916 )?;
1917 render_settings_table("global settings", &root, path.as_deref())?;
1918 }
1919 DisplaySettingsTarget::DefaultRuntime { path } => {
1920 let root = serialize_settings_with_defaults(
1921 default_runtime_settings,
1922 "default runtime settings for display",
1923 )?;
1924 render_settings_table("default runtime settings", &root, path.as_deref())?;
1925 }
1926 DisplaySettingsTarget::Process { process, path } => {
1927 let process_id = state.resolve_process_ref(process.process.as_ref())?;
1928 let process_ref = &state.process_list.processes[process_id];
1929 if let Some(name) = &process.integrand_name {
1930 let integrand = state
1931 .process_list
1932 .get_integrand(process_id, name)?
1933 .require_generated()?;
1934 let settings = serialize_settings_with_defaults(
1935 integrand.get_settings(),
1936 "process runtime settings for display",
1937 )?;
1938 render_settings_table(
1939 &format!(
1940 "process settings for #{} ({}) integrand '{}'",
1941 process_ref.definition.process_id,
1942 process_ref.definition.folder_name,
1943 name
1944 ),
1945 &settings,
1946 path.as_deref(),
1947 )?;
1948 } else {
1949 for integrand_name in process_ref.get_integrand_names() {
1950 let integrand = state
1951 .process_list
1952 .get_integrand(process_id, integrand_name)?
1953 .require_generated()?;
1954 let settings = serialize_settings_with_defaults(
1955 integrand.get_settings(),
1956 "process runtime settings for display",
1957 )?;
1958 render_settings_table(
1959 &format!(
1960 "process settings for #{} ({}) integrand '{}'",
1961 process_ref.definition.process_id,
1962 process_ref.definition.folder_name,
1963 integrand_name
1964 ),
1965 &settings,
1966 path.as_deref(),
1967 )?;
1968 }
1969 }
1970 }
1971 }
1972 Ok(())
1973 }
1974}
1975
1976fn render_settings_table(title: &str, root: &JsonValue, key_path: Option<&str>) -> Result<()> {
1977 let key_path = key_path.map(str::trim).filter(|path| !path.is_empty());
1978 let selected = if let Some(path) = key_path {
1979 value_at_path(root, path)?
1980 } else {
1981 root
1982 };
1983 let mut builder = Builder::new();
1984 builder.push_record(["key", "value"]);
1985
1986 match selected {
1987 JsonValue::Object(map) => {
1988 let mut keys: Vec<_> = map.keys().cloned().collect();
1989 keys.sort();
1990 for key in keys {
1991 let value = map
1992 .get(&key)
1993 .expect("sorted keys were derived from the same map");
1994 builder.push_record([key, format_json_value(value)]);
1995 }
1996 }
1997 JsonValue::Array(array) => {
1998 if array.is_empty() {
1999 builder.push_record(["(empty)".to_string(), "[]".to_string()]);
2000 } else {
2001 for (index, value) in array.iter().enumerate() {
2002 builder.push_record([index.to_string(), format_json_value(value)]);
2003 }
2004 }
2005 }
2006 primitive => {
2007 builder.push_record(["value".to_string(), format_json_value(primitive)]);
2008 }
2009 }
2010
2011 let mut table = builder.build();
2012 table.with(Style::rounded());
2013 if let Some(path) = key_path {
2014 info!("{title} (path: {path}):");
2015 } else {
2016 info!("{title}:");
2017 }
2018 info!("\n{table}");
2019 Ok(())
2020}
2021
2022fn format_json_value(value: &JsonValue) -> String {
2023 match value {
2024 JsonValue::Null => "null".to_string(),
2025 JsonValue::Bool(_) | JsonValue::Number(_) => value.to_string(),
2026 JsonValue::String(s) => s.clone(),
2027 _ => serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string()),
2028 }
2029}
2030
2031#[cfg(test)]
2032mod test {
2033 use clap::Parser;
2034 use serde_json::json;
2035
2036 use crate::{
2037 commands::Commands,
2038 integrand_info::{
2039 IntegrandActiveThresholdCutInfo, IntegrandCutThresholdInfo,
2040 IntegrandEsurfaceClassification, IntegrandGraphGroupInfo, IntegrandGraphInfo,
2041 IntegrandKind, IntegrandThresholdCountertermDirectiveInfo,
2042 IntegrandThresholdEsurfaceInfo, IntegrandThresholdStatus,
2043 },
2044 state::{CommandHistory, CommandsBlock, ProcessRef, RunHistory, State},
2045 CLISettings, Repl,
2046 };
2047 use gammalooprs::{
2048 processes::{
2049 ThresholdCountertermAssociationMetadata, ThresholdCountertermMetadataRegistry,
2050 ThresholdCountertermMultiplierMetadata, ThresholdCountertermOrigin,
2051 ThresholdCountertermSide, ThresholdCountertermVariantMetadata,
2052 },
2053 settings::RuntimeSettings,
2054 };
2055
2056 use super::{
2057 command_block_contents, format_bytes, render_command_blocks_table,
2058 render_integrand_thresholds_table, render_threshold_counterterm_directives,
2059 render_threshold_counterterms, serialize_settings_with_defaults, value_at_path, Display,
2060 DisplaySettingsTarget, IntegrandDisplayCategory, MetadataDisplayFormat,
2061 };
2062
2063 #[test]
2064 fn threshold_rendering_dims_or_hides_non_existing_associations() {
2065 let thresholds = vec![
2066 IntegrandCutThresholdInfo {
2067 esurface_id: 71,
2068 status: IntegrandThresholdStatus::CanBecomePinched,
2069 cut_boundary_edge_ids: vec![1, 2],
2070 threshold_boundary_edge_ids: vec![3, 4],
2071 invariant_bound_is_applicable: true,
2072 },
2073 IntegrandCutThresholdInfo {
2074 esurface_id: 83,
2075 status: IntegrandThresholdStatus::ProvenNonExisting,
2076 cut_boundary_edge_ids: vec![1, 2],
2077 threshold_boundary_edge_ids: vec![5],
2078 invariant_bound_is_applicable: true,
2079 },
2080 ];
2081
2082 let shown = render_threshold_counterterms(&thresholds, false);
2083 assert!(shown.contains("71*"));
2084 assert!(shown.contains("83"));
2085
2086 let hidden = render_threshold_counterterms(&thresholds, true);
2087 assert!(hidden.contains("71*"));
2088 assert!(!hidden.contains("83"));
2089 }
2090
2091 #[test]
2092 fn threshold_inventory_lists_active_cut_and_pinch_marker() {
2093 let group = IntegrandGraphGroupInfo {
2094 group_id: 0,
2095 graphs: vec![IntegrandGraphInfo {
2096 graph_id: 0,
2097 name: "generic".to_string(),
2098 is_master: true,
2099 threshold_counterterm_directives: Vec::new(),
2100 threshold_counterterms: None,
2101 }],
2102 orientation_edge_ids: Vec::new(),
2103 orientations: Vec::new(),
2104 loop_momentum_bases: Vec::new(),
2105 threshold_esurface_ids: vec![5],
2106 threshold_esurfaces: vec![IntegrandThresholdEsurfaceInfo {
2107 esurface_id: 5,
2108 representative_graph_id: 0,
2109 edge_ids: vec![1, 2],
2110 classification: None,
2111 active_cuts: vec![IntegrandActiveThresholdCutInfo {
2112 cut_id: 7,
2113 can_become_pinched: true,
2114 }],
2115 }],
2116 cuts: Vec::new(),
2117 };
2118
2119 let rendered = render_integrand_thresholds_table(&[&group], IntegrandKind::CrossSection)
2120 .unwrap()
2121 .unwrap();
2122 assert!(rendered.contains("Active in cuts"));
2123 assert!(rendered.contains("#7*"));
2124 }
2125
2126 #[test]
2127 fn amplitude_threshold_inventory_reports_source_graph_and_classification() {
2128 let group = IntegrandGraphGroupInfo {
2129 group_id: 0,
2130 graphs: vec![
2131 IntegrandGraphInfo {
2132 graph_id: 0,
2133 name: "master".to_string(),
2134 is_master: true,
2135 threshold_counterterm_directives: Vec::new(),
2136 threshold_counterterms: None,
2137 },
2138 IntegrandGraphInfo {
2139 graph_id: 1,
2140 name: "member".to_string(),
2141 is_master: false,
2142 threshold_counterterm_directives: Vec::new(),
2143 threshold_counterterms: None,
2144 },
2145 ],
2146 orientation_edge_ids: Vec::new(),
2147 orientations: Vec::new(),
2148 loop_momentum_bases: Vec::new(),
2149 threshold_esurface_ids: vec![5],
2150 threshold_esurfaces: vec![IntegrandThresholdEsurfaceInfo {
2151 esurface_id: 5,
2152 representative_graph_id: 1,
2153 edge_ids: vec![1, 2],
2154 classification: Some(IntegrandEsurfaceClassification::Pinched),
2155 active_cuts: Vec::new(),
2156 }],
2157 cuts: Vec::new(),
2158 };
2159
2160 let rendered = render_integrand_thresholds_table(&[&group], IntegrandKind::Amplitude)
2161 .unwrap()
2162 .unwrap();
2163 assert!(rendered.contains("Classification"));
2164 assert!(!rendered.contains("Active in cuts"));
2165 assert!(rendered.contains("#1 : member"));
2166 assert!(rendered.contains("pinched"));
2167 }
2168
2169 #[test]
2170 fn threshold_counterterm_directive_display_is_compact_and_expression_optional() {
2171 let amplitude_registry = ThresholdCountertermMetadataRegistry {
2172 graph_name: "amp".to_string(),
2173 variants: vec![ThresholdCountertermVariantMetadata {
2174 variant_id: 0,
2175 name: "forced".to_string(),
2176 group_id: None,
2177 cut_group_id: None,
2178 associations: vec![ThresholdCountertermAssociationMetadata {
2179 cut_id: None,
2180 cut_edges: Vec::new(),
2181 threshold_edges: vec![3, 4],
2182 esurface_id: 2,
2183 eligible: true,
2184 origin: ThresholdCountertermOrigin::Explicit,
2185 }],
2186 side: ThresholdCountertermSide::Amplitude,
2187 threshold_esurface_ids: vec![2],
2188 requested_subspace: Some(vec![3]),
2189 resolved_subspace: vec![3],
2190 requested_parent_lmb: None,
2191 resolved_parent_lmb: vec![3],
2192 subspace_loop_count: 1,
2193 multiplier: Some(ThresholdCountertermMultiplierMetadata {
2194 expression: "eta(effective, eset(3, 4))^2".to_string(),
2195 function_map: Default::default(),
2196 symmetrize: false,
2197 opaque_derivatives: true,
2198 }),
2199 generated: true,
2200 active: true,
2201 }],
2202 evaluators: Vec::new(),
2203 components: Vec::new(),
2204 };
2205 let cross_registry = ThresholdCountertermMetadataRegistry {
2206 graph_name: "cross".to_string(),
2207 variants: vec![ThresholdCountertermVariantMetadata {
2208 variant_id: 0,
2209 name: "intrinsic".to_string(),
2210 group_id: None,
2211 cut_group_id: Some(0),
2212 associations: vec![ThresholdCountertermAssociationMetadata {
2213 cut_id: Some(0),
2214 cut_edges: vec![2, 4, 12],
2215 threshold_edges: vec![7, 8],
2216 esurface_id: 9,
2217 eligible: true,
2218 origin: ThresholdCountertermOrigin::Explicit,
2219 }],
2220 side: ThresholdCountertermSide::Left,
2221 threshold_esurface_ids: vec![9],
2222 requested_subspace: Some(vec![7]),
2223 resolved_subspace: vec![7],
2224 requested_parent_lmb: Some(vec![3, 4, 7, 10]),
2225 resolved_parent_lmb: vec![3, 4, 7, 10],
2226 subspace_loop_count: 1,
2227 multiplier: None,
2228 generated: true,
2229 active: true,
2230 }],
2231 evaluators: Vec::new(),
2232 components: Vec::new(),
2233 };
2234 let group = IntegrandGraphGroupInfo {
2235 group_id: 0,
2236 graphs: vec![
2237 IntegrandGraphInfo {
2238 graph_id: 0,
2239 name: "amp".to_string(),
2240 is_master: true,
2241 threshold_counterterm_directives: vec![
2242 IntegrandThresholdCountertermDirectiveInfo {
2243 cut_edge_ids: Vec::new(),
2244 threshold_edge_ids: vec![3, 4],
2245 name: "forced".to_string(),
2246 implicit_default: false,
2247 requested_subspace: Some(vec![3]),
2248 requested_parent_lmb: None,
2249 disabled: false,
2250 multiplier: Some(ThresholdCountertermMultiplierMetadata {
2251 expression: "eta(effective, eset(3, 4))^2".to_string(),
2252 function_map: Default::default(),
2253 symmetrize: false,
2254 opaque_derivatives: true,
2255 }),
2256 },
2257 ],
2258 threshold_counterterms: Some(amplitude_registry),
2259 },
2260 IntegrandGraphInfo {
2261 graph_id: 1,
2262 name: "cross".to_string(),
2263 is_master: false,
2264 threshold_counterterm_directives: vec![
2265 IntegrandThresholdCountertermDirectiveInfo {
2266 cut_edge_ids: vec![2, 4, 12],
2267 threshold_edge_ids: vec![7, 8],
2268 name: "intrinsic".to_string(),
2269 implicit_default: false,
2270 requested_subspace: Some(vec![7]),
2271 requested_parent_lmb: Some(vec![3, 4, 7, 10]),
2272 disabled: false,
2273 multiplier: None,
2274 },
2275 IntegrandThresholdCountertermDirectiveInfo {
2276 cut_edge_ids: vec![2, 4, 12],
2277 threshold_edge_ids: vec![7, 8],
2278 name: "off".to_string(),
2279 implicit_default: false,
2280 requested_subspace: None,
2281 requested_parent_lmb: None,
2282 disabled: true,
2283 multiplier: None,
2284 },
2285 ],
2286 threshold_counterterms: Some(cross_registry),
2287 },
2288 ],
2289 orientation_edge_ids: Vec::new(),
2290 orientations: Vec::new(),
2291 loop_momentum_bases: Vec::new(),
2292 threshold_esurface_ids: Vec::new(),
2293 threshold_esurfaces: Vec::new(),
2294 cuts: Vec::new(),
2295 };
2296
2297 let compact = render_threshold_counterterm_directives(&[&group], false).unwrap();
2298 assert!(compact.contains("#0 amp"));
2299 assert!(compact.contains("#1 cross"));
2300 assert!(compact.contains('∅'));
2301 assert!(compact.contains("● forced"));
2302 assert!(compact.contains('ƒ'));
2303 assert!(compact.contains("× off"));
2304 assert!(!compact.contains("eta(effective"));
2305 assert!(
2306 !compact.contains("\u{1b}["),
2307 "colour-disabled output: {compact}"
2308 );
2309
2310 let expanded = render_threshold_counterterm_directives(&[&group], true).unwrap();
2311 assert!(expanded.contains("eta(effective, eset(3, 4))^2 [s=0,o=1]"));
2312 }
2313
2314 #[test]
2315 fn threshold_counterterm_directive_display_omits_empty_specifications() {
2316 let group = IntegrandGraphGroupInfo {
2317 group_id: 0,
2318 graphs: vec![IntegrandGraphInfo {
2319 graph_id: 0,
2320 name: "legacy".to_string(),
2321 is_master: true,
2322 threshold_counterterm_directives: Vec::new(),
2323 threshold_counterterms: Some(ThresholdCountertermMetadataRegistry {
2324 graph_name: "legacy".to_string(),
2325 variants: Vec::new(),
2326 evaluators: Vec::new(),
2327 components: Vec::new(),
2328 }),
2329 }],
2330 orientation_edge_ids: Vec::new(),
2331 orientations: Vec::new(),
2332 loop_momentum_bases: Vec::new(),
2333 threshold_esurface_ids: Vec::new(),
2334 threshold_esurfaces: Vec::new(),
2335 cuts: Vec::new(),
2336 };
2337
2338 assert!(render_threshold_counterterm_directives(&[&group], false).is_none());
2339 }
2340
2341 #[test]
2342 fn parse_display_settings_process_with_path() {
2343 let repl = Repl::try_parse_from([
2344 "gammaloop",
2345 "display",
2346 "settings",
2347 "process",
2348 "-p",
2349 "epem_a_tth",
2350 "-i",
2351 "LO",
2352 "integrator.n_max",
2353 ])
2354 .unwrap();
2355
2356 match repl.command {
2357 Commands::Display(Display::Settings { target }) => match target {
2358 DisplaySettingsTarget::Process { process, path } => {
2359 assert_eq!(
2360 process.process,
2361 Some(ProcessRef::Unqualified("epem_a_tth".to_string()))
2362 );
2363 assert_eq!(process.integrand_name, Some("LO".to_string()));
2364 assert_eq!(path.as_deref(), Some("integrator.n_max"));
2365 }
2366 other => panic!("Expected process target, got {other:?}"),
2367 },
2368 other => panic!("Expected display settings command, got {other:?}"),
2369 }
2370 }
2371
2372 #[test]
2373 fn parse_display_settings_process_without_path() {
2374 let repl = Repl::try_parse_from([
2375 "gammaloop",
2376 "display",
2377 "settings",
2378 "process",
2379 "-p",
2380 "epem_a_tth",
2381 "-i",
2382 "LO",
2383 ])
2384 .unwrap();
2385
2386 match repl.command {
2387 Commands::Display(Display::Settings { target }) => match target {
2388 DisplaySettingsTarget::Process { path, .. } => {
2389 assert_eq!(path, None);
2390 }
2391 other => panic!("Expected process target, got {other:?}"),
2392 },
2393 other => panic!("Expected display settings command, got {other:?}"),
2394 }
2395 }
2396
2397 #[test]
2398 fn parse_display_model_target() {
2399 let repl = Repl::try_parse_from([
2400 "gammaloop",
2401 "display",
2402 "model",
2403 "-p",
2404 "epem_a_tth",
2405 "-i",
2406 "LO",
2407 "--show-particles",
2408 ])
2409 .unwrap();
2410
2411 match repl.command {
2412 Commands::Display(Display::Model {
2413 process,
2414 integrand_name,
2415 show_particles,
2416 ..
2417 }) => {
2418 assert_eq!(
2419 process,
2420 Some(ProcessRef::Unqualified("epem_a_tth".to_string()))
2421 );
2422 assert_eq!(integrand_name, Some("LO".to_string()));
2423 assert!(show_particles);
2424 }
2425 other => panic!("Expected display model command, got {other:?}"),
2426 }
2427 }
2428
2429 #[test]
2430 fn display_model_requires_process_and_integrand_together() {
2431 let err = Repl::try_parse_from(["gammaloop", "display", "model", "-p", "epem_a_tth"])
2432 .unwrap_err();
2433 assert!(err.to_string().contains("--integrand-name"));
2434 }
2435
2436 #[test]
2437 fn parse_display_quantities_without_name() {
2438 let repl = Repl::try_parse_from([
2439 "gammaloop",
2440 "display",
2441 "quantities",
2442 "-p",
2443 "epem_a_tth",
2444 "-i",
2445 "LO",
2446 ])
2447 .unwrap();
2448
2449 match repl.command {
2450 Commands::Display(Display::Quantities { target }) => {
2451 assert_eq!(
2452 target.process.process,
2453 Some(ProcessRef::Unqualified("epem_a_tth".to_string()))
2454 );
2455 assert_eq!(target.process.integrand_name, Some("LO".to_string()));
2456 assert_eq!(target.name, None);
2457 }
2458 other => panic!("Expected display quantities command, got {other:?}"),
2459 }
2460 }
2461
2462 #[test]
2463 fn parse_display_selector_with_name() {
2464 let repl = Repl::try_parse_from([
2465 "gammaloop",
2466 "display",
2467 "selectors",
2468 "-p",
2469 "epem_a_tth",
2470 "top_cut",
2471 ])
2472 .unwrap();
2473
2474 match repl.command {
2475 Commands::Display(Display::Selectors { target }) => {
2476 assert_eq!(
2477 target.process.process,
2478 Some(ProcessRef::Unqualified("epem_a_tth".to_string()))
2479 );
2480 assert_eq!(target.process.integrand_name, None);
2481 assert_eq!(target.name.as_deref(), Some("top_cut"));
2482 }
2483 other => panic!("Expected display selectors command, got {other:?}"),
2484 }
2485 }
2486
2487 #[test]
2488 fn parse_display_command_block_without_name() {
2489 let repl = Repl::try_parse_from(["gammaloop", "display", "command_block"]).unwrap();
2490
2491 match repl.command {
2492 Commands::Display(Display::CommandBlock { name }) => {
2493 assert_eq!(name, None);
2494 }
2495 other => panic!("Expected display command_block command, got {other:?}"),
2496 }
2497 }
2498
2499 #[test]
2500 fn parse_display_command_block_with_name() {
2501 let repl =
2502 Repl::try_parse_from(["gammaloop", "display", "command_block", "alpha"]).unwrap();
2503
2504 match repl.command {
2505 Commands::Display(Display::CommandBlock { name }) => {
2506 assert_eq!(name.as_deref(), Some("alpha"));
2507 }
2508 other => panic!("Expected display command_block command, got {other:?}"),
2509 }
2510 }
2511
2512 #[test]
2513 fn parse_display_command_block_rejects_hyphenated_name() {
2514 assert!(Repl::try_parse_from(["gammaloop", "display", "command-block"]).is_err());
2515 }
2516
2517 #[test]
2518 fn parse_display_settings_global_without_query() {
2519 let repl = Repl::try_parse_from(["gammaloop", "display", "settings", "global"]).unwrap();
2520
2521 match repl.command {
2522 Commands::Display(Display::Settings { target }) => match target {
2523 DisplaySettingsTarget::Global { path } => {
2524 assert_eq!(path, None);
2525 }
2526 other => panic!("Expected global target, got {other:?}"),
2527 },
2528 other => panic!("Expected display settings command, got {other:?}"),
2529 }
2530 }
2531
2532 #[test]
2533 fn parse_display_settings_defaults_alias() {
2534 let repl = Repl::try_parse_from([
2535 "gammaloop",
2536 "display",
2537 "settings",
2538 "defaults",
2539 "integrator.n_start",
2540 ])
2541 .unwrap();
2542
2543 match repl.command {
2544 Commands::Display(Display::Settings { target }) => match target {
2545 DisplaySettingsTarget::DefaultRuntime { path } => {
2546 assert_eq!(path.as_deref(), Some("integrator.n_start"));
2547 }
2548 other => panic!("Expected default runtime target, got {other:?}"),
2549 },
2550 other => panic!("Expected display settings command, got {other:?}"),
2551 }
2552 }
2553
2554 #[test]
2555 fn value_lookup_supports_nested_paths() {
2556 let value = json!({
2557 "integrator": {
2558 "n_max": 42
2559 }
2560 });
2561 let found = value_at_path(&value, "integrator.n_max").unwrap();
2562 assert_eq!(found, &json!(42));
2563 }
2564
2565 #[test]
2566 fn serialized_settings_include_default_values() {
2567 let global = CLISettings::default();
2568 let global_json = serialize_settings_with_defaults(&global, "global settings").unwrap();
2569 assert!(value_at_path(&global_json, "global.n_cores.generate").is_ok());
2570
2571 let runtime = RuntimeSettings::default();
2572 let runtime_json =
2573 serialize_settings_with_defaults(&runtime, "default runtime settings").unwrap();
2574 assert!(value_at_path(&runtime_json, "integrator.n_start").is_ok());
2575 }
2576
2577 #[test]
2578 fn parse_display_integrand_without_process() {
2579 let repl = Repl::try_parse_from(["gammaloop", "display", "integrand"]).unwrap();
2580
2581 match repl.command {
2582 Commands::Display(Display::Integrands {
2583 process,
2584 integrand_name,
2585 graphs,
2586 categories,
2587 hide_non_existing_thresholds,
2588 show_threshold_functions,
2589 show_sampling,
2590 show_threshold_subtraction,
2591 }) => {
2592 assert_eq!(process, None);
2593 assert_eq!(integrand_name, None);
2594 assert!(graphs.is_empty());
2595 assert!(categories.is_empty());
2596 assert!(!hide_non_existing_thresholds);
2597 assert!(!show_threshold_functions);
2598 assert_eq!(show_sampling, None);
2599 assert_eq!(show_threshold_subtraction, None);
2600 }
2601 other => panic!("Expected display integrand command, got {other:?}"),
2602 }
2603 }
2604
2605 #[test]
2606 fn structured_display_integrand_defaults_missing_threshold_options() {
2607 let mut value = serde_json::to_value(Display::Integrands {
2608 process: None,
2609 integrand_name: None,
2610 graphs: Vec::new(),
2611 categories: Vec::new(),
2612 hide_non_existing_thresholds: false,
2613 show_threshold_functions: false,
2614 show_sampling: None,
2615 show_threshold_subtraction: None,
2616 })
2617 .unwrap();
2618 value
2619 .get_mut("Integrands")
2620 .and_then(serde_json::Value::as_object_mut)
2621 .unwrap()
2622 .remove("hide_non_existing_thresholds");
2623 value
2624 .get_mut("Integrands")
2625 .and_then(serde_json::Value::as_object_mut)
2626 .unwrap()
2627 .remove("show_threshold_functions");
2628
2629 let display: Display = serde_json::from_value(value).unwrap();
2630 assert!(matches!(
2631 display,
2632 Display::Integrands {
2633 hide_non_existing_thresholds: false,
2634 show_threshold_functions: false,
2635 show_sampling: None,
2636 show_threshold_subtraction: None,
2637 ..
2638 }
2639 ));
2640 }
2641
2642 #[test]
2643 fn parse_display_integrand_detail() {
2644 let repl = Repl::try_parse_from([
2645 "gammaloop",
2646 "display",
2647 "integrand",
2648 "-p",
2649 "epem_a_tth",
2650 "-i",
2651 "LO",
2652 ])
2653 .unwrap();
2654
2655 match repl.command {
2656 Commands::Display(Display::Integrands {
2657 process,
2658 integrand_name,
2659 graphs,
2660 categories,
2661 hide_non_existing_thresholds,
2662 show_threshold_functions,
2663 show_sampling,
2664 show_threshold_subtraction,
2665 }) => {
2666 assert_eq!(
2667 process,
2668 Some(ProcessRef::Unqualified("epem_a_tth".to_string()))
2669 );
2670 assert_eq!(integrand_name.as_deref(), Some("LO"));
2671 assert!(graphs.is_empty());
2672 assert!(categories.is_empty());
2673 assert!(!hide_non_existing_thresholds);
2674 assert!(!show_threshold_functions);
2675 assert_eq!(show_sampling, None);
2676 assert_eq!(show_threshold_subtraction, None);
2677 }
2678 other => panic!("Expected display integrand command, got {other:?}"),
2679 }
2680 }
2681
2682 #[test]
2683 fn parse_display_integrand_detail_filters() {
2684 let repl = Repl::try_parse_from([
2685 "gammaloop",
2686 "display",
2687 "integrand",
2688 "-p",
2689 "epem_a_tth",
2690 "-i",
2691 "LO",
2692 "-g",
2693 "GL0",
2694 "GL2",
2695 "--category",
2696 "generation",
2697 "orientation",
2698 "cuts",
2699 "--hide-non-existing-thresholds",
2700 "--show-threshold-functions",
2701 ])
2702 .unwrap();
2703
2704 match repl.command {
2705 Commands::Display(Display::Integrands {
2706 process,
2707 integrand_name,
2708 graphs,
2709 categories,
2710 hide_non_existing_thresholds,
2711 show_threshold_functions,
2712 show_sampling,
2713 show_threshold_subtraction,
2714 }) => {
2715 assert_eq!(
2716 process,
2717 Some(ProcessRef::Unqualified("epem_a_tth".to_string()))
2718 );
2719 assert!(hide_non_existing_thresholds);
2720 assert!(show_threshold_functions);
2721 assert_eq!(show_sampling, None);
2722 assert_eq!(show_threshold_subtraction, None);
2723 assert_eq!(integrand_name.as_deref(), Some("LO"));
2724 assert_eq!(graphs, vec!["GL0".to_string(), "GL2".to_string()]);
2725 assert_eq!(
2726 categories,
2727 vec![
2728 IntegrandDisplayCategory::Generation,
2729 IntegrandDisplayCategory::Orientation,
2730 IntegrandDisplayCategory::Cuts,
2731 ]
2732 );
2733 }
2734 other => panic!("Expected display integrand command, got {other:?}"),
2735 }
2736 }
2737
2738 #[test]
2739 fn metadata_display_parses_formats_aliases_and_graph_lists() {
2740 for command in ["integrand", "integrands"] {
2741 for (flag, sampling) in [
2742 ("--show_sampling", true),
2743 ("--show-sampling", true),
2744 ("--show_threshold_subtraction", false),
2745 ("--show-threshold-subtraction", false),
2746 ] {
2747 for (value, format) in [
2748 ("pretty", MetadataDisplayFormat::Pretty),
2749 ("toml", MetadataDisplayFormat::Toml),
2750 ] {
2751 let mut args = vec![
2752 "gammaloop",
2753 "display",
2754 command,
2755 "-p",
2756 "ttH",
2757 "-i",
2758 "NNLO",
2759 flag,
2760 value,
2761 ];
2762 if sampling || format == MetadataDisplayFormat::Pretty {
2763 args.extend(["--graph", "GL297", "GL638"]);
2764 } else {
2765 args.extend(["--graph", "GL638"]);
2766 }
2767 let Commands::Display(display) = Repl::try_parse_from(args).unwrap().command
2768 else {
2769 panic!("expected display");
2770 };
2771 let Display::Integrands {
2772 show_sampling,
2773 show_threshold_subtraction,
2774 ..
2775 } = &display
2776 else {
2777 panic!("expected integrand display");
2778 };
2779 assert_eq!(*show_sampling, sampling.then_some(format));
2780 assert_eq!(*show_threshold_subtraction, (!sampling).then_some(format));
2781 let serialized = toml::to_string(&display).unwrap();
2782 assert_eq!(toml::from_str::<Display>(&serialized).unwrap(), display);
2783 }
2784 }
2785 }
2786 }
2787
2788 #[test]
2789 fn metadata_display_rejects_invalid_cli_options() {
2790 for tail in [
2791 vec!["--show_sampling"],
2792 vec!["--show_sampling", "json"],
2793 vec!["--show_threshold_subtraction", "json"],
2794 vec![
2795 "--show_sampling",
2796 "pretty",
2797 "--show_threshold_subtraction",
2798 "toml",
2799 ],
2800 ] {
2801 let mut args = vec![
2802 "gammaloop",
2803 "display",
2804 "integrands",
2805 "-p",
2806 "ttH",
2807 "-i",
2808 "NNLO",
2809 ];
2810 args.extend(tail);
2811 assert!(Repl::try_parse_from(args).is_err());
2812 }
2813 for flag in ["--show_sampling", "--show_threshold_subtraction"] {
2814 for context in [vec![], vec!["-p", "ttH"], vec!["-i", "NNLO"]] {
2815 let mut args = vec!["gammaloop", "display", "integrands", flag, "pretty"];
2816 args.extend(context);
2817 assert!(Repl::try_parse_from(args).is_err());
2818 }
2819 }
2820 }
2821
2822 #[test]
2823 fn metadata_display_validates_structured_requests_before_reading_state() {
2824 let state = State::new_test();
2825 for (process, integrand, graphs, sampling, thresholds, expected) in [
2826 (
2827 None,
2828 Some("NNLO"),
2829 vec![],
2830 Some(MetadataDisplayFormat::Pretty),
2831 None,
2832 "explicit --process and --integrand-name",
2833 ),
2834 (
2835 Some(ProcessRef::Id(0)),
2836 None,
2837 vec![],
2838 None,
2839 Some(MetadataDisplayFormat::Pretty),
2840 "explicit --process and --integrand-name",
2841 ),
2842 (
2843 Some(ProcessRef::Id(0)),
2844 Some("NNLO"),
2845 vec![],
2846 Some(MetadataDisplayFormat::Pretty),
2847 Some(MetadataDisplayFormat::Toml),
2848 "mutually exclusive",
2849 ),
2850 (
2851 Some(ProcessRef::Id(0)),
2852 Some("NNLO"),
2853 vec![],
2854 None,
2855 Some(MetadataDisplayFormat::Toml),
2856 "exactly one --graph",
2857 ),
2858 (
2859 Some(ProcessRef::Id(0)),
2860 Some("NNLO"),
2861 vec!["GL297".to_string(), "GL638".to_string()],
2862 None,
2863 Some(MetadataDisplayFormat::Toml),
2864 "exactly one --graph",
2865 ),
2866 ] {
2867 let command = Display::Integrands {
2868 process,
2869 integrand_name: integrand.map(str::to_string),
2870 graphs,
2871 categories: vec![IntegrandDisplayCategory::Cuts],
2872 hide_non_existing_thresholds: true,
2873 show_threshold_functions: true,
2874 show_sampling: sampling,
2875 show_threshold_subtraction: thresholds,
2876 };
2877 let command =
2878 serde_json::from_value::<Display>(serde_json::to_value(command).unwrap()).unwrap();
2879 let error = command
2880 .run(
2881 &state,
2882 &CLISettings::default(),
2883 &RuntimeSettings::default(),
2884 &RunHistory::default(),
2885 )
2886 .unwrap_err();
2887 assert!(error.to_string().contains(expected), "{error:?}");
2888 }
2889 }
2890
2891 #[test]
2892 fn format_bytes_uses_binary_units() {
2893 assert_eq!(format_bytes(999), "999 B");
2894 assert_eq!(format_bytes(1024), "1.00 KiB");
2895 assert_eq!(format_bytes(1024 * 1024), "1.00 MiB");
2896 }
2897
2898 #[test]
2899 fn command_block_contents_preserve_raw_commands() {
2900 let block = CommandsBlock {
2901 name: "alpha".to_string(),
2902 commands: vec![
2903 CommandHistory::from_raw_string("display processes").unwrap(),
2904 CommandHistory::from_raw_string("set process -p triangle -i LO defaults").unwrap(),
2905 ],
2906 };
2907
2908 assert_eq!(
2909 command_block_contents(&block),
2910 "display processes\nset process -p triangle -i LO defaults"
2911 );
2912 }
2913
2914 #[test]
2915 fn command_block_contents_marks_empty_blocks() {
2916 let run_history = RunHistory {
2917 command_blocks: vec![CommandsBlock {
2918 name: "empty".to_string(),
2919 commands: Vec::new(),
2920 }],
2921 ..RunHistory::default()
2922 };
2923
2924 assert_eq!(
2925 command_block_contents(&run_history.command_blocks[0]),
2926 "(empty)"
2927 );
2928 }
2929
2930 #[test]
2931 fn command_blocks_table_separates_each_block() {
2932 let run_history = RunHistory {
2933 command_blocks: vec![
2934 CommandsBlock {
2935 name: "alpha".to_string(),
2936 commands: vec![CommandHistory::from_raw_string("display processes").unwrap()],
2937 },
2938 CommandsBlock {
2939 name: "beta".to_string(),
2940 commands: vec![CommandHistory::from_raw_string("display model").unwrap()],
2941 },
2942 ],
2943 ..RunHistory::default()
2944 };
2945
2946 let rendered = render_command_blocks_table(&run_history);
2947
2948 assert!(rendered.contains("alpha"), "{rendered}");
2949 assert!(rendered.contains("beta"), "{rendered}");
2950 assert_eq!(
2951 rendered
2952 .lines()
2953 .filter(|line| line.starts_with('├') && line.ends_with('┤'))
2954 .count(),
2955 2,
2956 "{rendered}"
2957 );
2958 }
2959
2960 #[test]
2961 fn command_blocks_table_lists_command_block_placeholders() {
2962 let run_history = RunHistory {
2963 command_blocks: vec![
2964 CommandsBlock {
2965 name: "inner".to_string(),
2966 commands: vec![CommandHistory::from_raw_string(
2967 "set global kv global.display_directive=$(level:warn)",
2968 )
2969 .unwrap()],
2970 },
2971 CommandsBlock {
2972 name: "outer".to_string(),
2973 commands: vec![CommandHistory::from_raw_string(
2974 "run inner -D level=$(outer_level)",
2975 )
2976 .unwrap()],
2977 },
2978 ],
2979 ..RunHistory::default()
2980 };
2981
2982 let rendered = render_command_blocks_table(&run_history);
2983
2984 assert!(rendered.contains("placeholders"), "{rendered}");
2985 assert!(rendered.contains("$(level:warn)"), "{rendered}");
2986 assert!(rendered.contains("$(outer_level)"), "{rendered}");
2987 assert_eq!(
2988 run_history
2989 .command_block_placeholder_names("outer")
2990 .into_iter()
2991 .collect::<Vec<_>>(),
2992 vec!["outer_level".to_string()]
2993 );
2994 }
2995}