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 path::{Path, PathBuf},
11};
12use tabled::{
13 builder::Builder,
14 settings::{style::HorizontalLine, themes::Theme, Style},
15 Tabled,
16};
17use tracing::info;
18use walkdir::WalkDir;
19
20use color_eyre::Result;
21use eyre::{eyre, Context};
22use gammalooprs::{
23 processes::{Amplitude, CrossSection, Process, ProcessCollection},
24 settings::RuntimeSettings,
25};
26
27use crate::{
28 commands::generate::{render_generation_summary, ProcessArgs},
29 commands::process_settings::{
30 observable_kind, quantity_kind, selector_kind, serialize_runtime_named_settings,
31 summarize_observable, summarize_quantity, summarize_selector, NamedProcessSettingKind,
32 },
33 commands::CliArgumentMetadataExt,
34 completion::CompletionArgExt,
35 integrand_info::{
36 IntegrandCutThresholdInfo, IntegrandEsurfaceClassification, IntegrandGraphGroupInfo,
37 IntegrandInfo, IntegrandKind,
38 },
39 session::display_command,
40 settings_tree::{serialize_settings_with_defaults, value_at_path},
41 state::{CommandsBlock, IntegrandGenerationSummary, ProcessRef, RunHistory, State},
42 CLISettings,
43};
44
45#[derive(Subcommand, Debug, Serialize, Deserialize, Clone, JsonSchema, PartialEq)]
46pub enum Display {
47 Model {
49 #[arg(short = 'c', long = "show-couplings", default_value_t = false)]
51 show_couplings: bool,
52 #[arg(short = 'v', long = "show-vertices", default_value_t = false)]
54 show_vertices: bool,
55 #[arg(short = 'r', long = "show-parameters", default_value_t = false)]
57 show_parameters: bool,
58 #[arg(long = "show-particles", default_value_t = false)]
60 show_particles: bool,
61 #[arg(short = 'a', long = "show-all", default_value_t = false)]
63 show_all: bool,
64 #[arg(
66 short = 'p',
67 long = "process",
68 value_name = "PROCESS",
69 cli_requires("integrand_name"),
70 completion_process_selector(crate::completion::SelectorKind::Any)
71 )]
72 process: Option<ProcessRef>,
73 #[arg(
75 short = 'i',
76 long = "integrand-name",
77 value_name = "NAME",
78 cli_requires("process"),
79 completion_integrand_selector(crate::completion::SelectorKind::Any)
80 )]
81 integrand_name: Option<String>,
82 },
83 Processes,
85 #[command(name = "integrand")]
87 Integrands {
88 #[arg(
90 short = 'p',
91 long = "process",
92 value_name = "PROCESS",
93 completion_process_selector(crate::completion::SelectorKind::Any)
94 )]
95 process: Option<ProcessRef>,
96 #[arg(
98 short = 'i',
99 long = "integrand-name",
100 value_name = "NAME",
101 cli_requires("process"),
102 completion_integrand_selector(crate::completion::SelectorKind::Any)
103 )]
104 integrand_name: Option<String>,
105 #[arg(
107 short = 'g',
108 long = "graph",
109 value_name = "MASTER_GRAPH",
110 num_args = 1..,
111 cli_requires("integrand_name"),
112 completion_selected_master_graph()
113 )]
114 graphs: Vec<String>,
115 #[arg(
117 long = "category",
118 value_name = "CATEGORY",
119 num_args = 1..,
120 cli_requires("integrand_name"),
121 completion_selected_integrand_category()
122 )]
123 categories: Vec<IntegrandDisplayCategory>,
124 #[arg(
126 long = "hide-non-existing-thresholds",
127 default_value_t = false,
128 cli_requires("integrand_name")
129 )]
130 #[serde(default)]
131 hide_non_existing_thresholds: bool,
132 },
133 Quantities {
135 #[command(flatten)]
136 target: DisplayProcessNamedSettingsArgs,
137 },
138 Observables {
140 #[command(flatten)]
141 target: DisplayProcessNamedSettingsArgs,
142 },
143 Selectors {
145 #[command(flatten)]
146 target: DisplayProcessNamedSettingsArgs,
147 },
148 #[command(name = "command_block")]
150 CommandBlock {
151 #[arg(value_name = "NAME")]
153 name: Option<String>,
154 },
155 Settings {
157 #[command(subcommand)]
158 target: DisplaySettingsTarget,
159 },
160}
161
162#[derive(Args, Debug, Serialize, Deserialize, Clone, JsonSchema, PartialEq)]
163pub struct DisplayProcessNamedSettingsArgs {
164 #[command(flatten)]
165 process: ProcessArgs,
166 #[arg(value_name = "NAME")]
168 name: Option<String>,
169}
170
171#[derive(Subcommand, Debug, Serialize, Deserialize, Clone, JsonSchema, PartialEq)]
172pub enum DisplaySettingsTarget {
173 Global {
175 #[arg(value_name = "PATH")]
177 path: Option<String>,
178 },
179 #[command(alias = "defaults")]
181 DefaultRuntime {
182 #[arg(value_name = "PATH")]
184 path: Option<String>,
185 },
186 Process {
188 #[command(flatten)]
189 process: ProcessArgs,
190 #[arg(value_name = "PATH")]
192 path: Option<String>,
193 },
194}
195
196impl Display {
197 pub fn run(
198 &self,
199 state: &State,
200 global_settings: &CLISettings,
201 default_runtime_settings: &RuntimeSettings,
202 run_history: &RunHistory,
203 ) -> Result<()> {
204 match self {
205 Display::Integrands {
206 process,
207 integrand_name,
208 graphs,
209 categories,
210 hide_non_existing_thresholds,
211 } => {
212 let process_id = process
213 .as_ref()
214 .map(|process_ref| state.resolve_process_ref(Some(process_ref)))
215 .transpose()?;
216 if let Some(integrand_name) = integrand_name.as_deref() {
217 render_integrand_detail(
218 state,
219 &global_settings.state.folder,
220 process_id.expect("clap requires --process when --integrand-name is set"),
221 integrand_name,
222 graphs,
223 categories,
224 *hide_non_existing_thresholds,
225 )?;
226 } else {
227 render_integrands_table(state, &global_settings.state.folder, process_id)?;
228 }
229 }
230 Display::Processes => {
231 render_processes_table(state, &global_settings.state.folder)?;
232 }
233 Display::Quantities { target } => {
234 render_named_process_settings(state, target, NamedProcessSettingKind::Quantity)?;
235 }
236 Display::Observables { target } => {
237 render_named_process_settings(state, target, NamedProcessSettingKind::Observable)?;
238 }
239 Display::Selectors { target } => {
240 render_named_process_settings(state, target, NamedProcessSettingKind::Selector)?;
241 }
242 Display::CommandBlock { name } => {
243 render_command_blocks(run_history, name.as_deref())?;
244 }
245 Display::Model {
246 show_couplings,
247 show_vertices,
248 show_parameters,
249 show_particles,
250 show_all,
251 process,
252 integrand_name,
253 } => {
254 let model = if let (Some(process), Some(integrand_name)) =
255 (process.as_ref(), integrand_name.as_ref())
256 {
257 let process_id = state.resolve_process_ref(Some(process))?;
258 state.resolve_model_for_integrand(process_id, integrand_name)?
259 } else {
260 state.model.clone()
261 };
262 info!(
263 "\n{}",
264 model.get_description(
265 *show_particles || *show_all,
266 *show_parameters || *show_all,
267 *show_vertices || *show_all,
268 *show_couplings || *show_all,
269 )
270 )
271 }
272 Display::Settings { target } => {
273 target.run(state, global_settings, default_runtime_settings)?;
274 }
275 }
276 Ok(())
277 }
278}
279
280#[derive(Clone, Debug)]
281struct IntegrandMetrics {
282 name: String,
283 graphs: usize,
284 graph_groups: usize,
285 bin_disk_size_bytes: Option<u64>,
286 so_disk_size_bytes: Option<u64>,
287}
288
289#[derive(Clone, Debug, Default)]
290struct IntegrandArtifactSizes {
291 bin_disk_size_bytes: Option<u64>,
292 so_disk_size_bytes: Option<u64>,
293}
294
295#[derive(
296 Debug, Clone, Copy, Serialize, Deserialize, ValueEnum, JsonSchema, PartialEq, Eq, Hash,
297)]
298#[serde(rename_all = "snake_case")]
299pub enum IntegrandDisplayCategory {
300 #[value(name = "generation")]
301 Generation,
302 #[value(name = "orientation")]
303 Orientation,
304 #[value(name = "loop_momentum_basis")]
305 LoopMomentumBasis,
306 #[value(name = "cuts")]
307 Cuts,
308}
309
310impl IntegrandDisplayCategory {
311 fn title(self) -> &'static str {
312 match self {
313 Self::Generation => "Generation",
314 Self::Orientation => "Orientations",
315 Self::LoopMomentumBasis => "Loop momentum bases",
316 Self::Cuts => "Cuts",
317 }
318 }
319
320 fn value_header(self) -> &'static str {
321 match self {
322 Self::Generation => "generation",
323 Self::Orientation => "orientation",
324 Self::LoopMomentumBasis => "loop momentum basis",
325 Self::Cuts => "cut",
326 }
327 }
328}
329
330#[derive(Tabled)]
331struct DetailSummaryRow {
332 field: String,
333 value: String,
334}
335
336fn orientation_column_widths(edge_ids: &[usize], fallback_count: usize) -> Vec<usize> {
337 if edge_ids.is_empty() {
338 vec![1; fallback_count]
339 } else {
340 edge_ids
341 .iter()
342 .map(|edge_id| edge_id.to_string().len().max(1))
343 .collect()
344 }
345}
346
347fn render_orientation_edge_ids(edge_ids: &[usize]) -> String {
348 if edge_ids.is_empty() {
349 return String::new();
350 }
351
352 let widths = orientation_column_widths(edge_ids, edge_ids.len());
353 edge_ids
354 .iter()
355 .zip(widths.iter())
356 .map(|(edge_id, width)| {
357 format!("{edge_id:<width$}", width = *width)
358 .magenta()
359 .to_string()
360 })
361 .join(" ")
362}
363
364fn render_orientation_signature(signature: &[i8], edge_ids: &[usize]) -> String {
365 let widths = orientation_column_widths(edge_ids, signature.len());
366 signature
367 .iter()
368 .zip(widths.iter())
369 .map(|(sign, width)| {
370 let padded = format!(
371 "{:<width$}",
372 match sign {
373 1 => "+",
374 -1 => "-",
375 0 => "0",
376 _ => "?",
377 },
378 width = *width
379 );
380 match sign {
381 1 => padded.green().bold().to_string(),
382 -1 => padded.red().bold().to_string(),
383 0 => padded.dimmed().to_string(),
384 _ => padded.to_string(),
385 }
386 })
387 .join(" ")
388}
389
390fn render_edge_ids_with_highlight(edge_ids: &[usize], highlight: bool) -> String {
391 if edge_ids.is_empty() {
392 return String::new();
393 }
394
395 let rendered = format!(
396 "({})",
397 edge_ids.iter().map(|edge_id| edge_id.to_string()).join(",")
398 );
399 if highlight {
400 rendered.green().bold().to_string()
401 } else {
402 rendered.magenta().to_string()
403 }
404}
405
406fn render_edge_ids(edge_ids: &[usize]) -> String {
407 render_edge_ids_with_highlight(edge_ids, false)
408}
409
410fn render_threshold_counterterms(
411 thresholds: &[IntegrandCutThresholdInfo],
412 hide_non_existing_thresholds: bool,
413) -> String {
414 let visible = thresholds
415 .iter()
416 .filter(|threshold| !hide_non_existing_thresholds || threshold.status.is_currently_viable())
417 .collect_vec();
418 if visible.is_empty() {
419 return "none".dimmed().to_string();
420 }
421 format!(
422 "[{}]",
423 visible
424 .iter()
425 .map(|threshold| {
426 let id = if threshold.status.can_become_pinched() {
427 format!("{}*", threshold.esurface_id)
428 } else {
429 threshold.esurface_id.to_string()
430 };
431 if threshold.status.is_currently_viable() {
432 id.cyan().to_string()
433 } else {
434 id.dimmed().to_string()
435 }
436 })
437 .join(",")
438 )
439}
440
441fn render_threshold_esurface(edge_ids: &[usize]) -> String {
442 render_edge_ids(edge_ids)
443}
444
445fn render_cut(edge_ids: &[usize], raising_power: usize) -> String {
446 let edge_ids = render_edge_ids(edge_ids);
447 if edge_ids.is_empty() || raising_power <= 1 {
448 edge_ids
449 } else {
450 format!("{edge_ids}^{raising_power}").magenta().to_string()
451 }
452}
453
454#[derive(Clone)]
455struct CategoryRow {
456 primary_id: String,
457 secondary_id: String,
458 tertiary_id: String,
459 value: String,
460}
461
462fn render_category_id(id: usize, highlight: bool) -> String {
463 let rendered = format!("#{id}");
464 if highlight {
465 rendered.green().bold().to_string()
466 } else {
467 rendered.yellow().to_string()
468 }
469}
470
471fn render_optional_category_id(id: Option<usize>, highlight: bool) -> String {
472 id.map(|id| render_category_id(id, highlight))
473 .unwrap_or_default()
474}
475
476fn master_graph(
477 group: &IntegrandGraphGroupInfo,
478) -> Result<&crate::integrand_info::IntegrandGraphInfo> {
479 group
480 .graphs
481 .iter()
482 .find(|graph| graph.is_master)
483 .ok_or_else(|| eyre!("Graph group #{} has no master graph", group.group_id))
484}
485
486fn available_integrand_categories(detail: &IntegrandInfo) -> Vec<IntegrandDisplayCategory> {
487 match detail.kind {
488 IntegrandKind::Amplitude => vec![
489 IntegrandDisplayCategory::Generation,
490 IntegrandDisplayCategory::Orientation,
491 IntegrandDisplayCategory::LoopMomentumBasis,
492 IntegrandDisplayCategory::Cuts,
493 ],
494 IntegrandKind::CrossSection => vec![
495 IntegrandDisplayCategory::Generation,
496 IntegrandDisplayCategory::Orientation,
497 IntegrandDisplayCategory::LoopMomentumBasis,
498 IntegrandDisplayCategory::Cuts,
499 ],
500 }
501}
502
503fn selected_integrand_categories(
504 detail: &IntegrandInfo,
505 requested: &[IntegrandDisplayCategory],
506) -> Result<Vec<IntegrandDisplayCategory>> {
507 let available = available_integrand_categories(detail);
508 if requested.is_empty() {
509 return Ok(available);
510 }
511
512 let mut selected = Vec::new();
513 for category in requested.iter().copied() {
514 if !available.contains(&category) {
515 return Err(eyre!(
516 "Category '{}' is not available for {} integrands.",
517 category.to_possible_value().expect("value enum").get_name(),
518 detail.kind
519 ));
520 }
521 if !selected.contains(&category) {
522 selected.push(category);
523 }
524 }
525 Ok(selected)
526}
527
528fn filtered_graph_groups<'a>(
529 detail: &'a IntegrandInfo,
530 requested_graphs: &[String],
531) -> Result<Vec<&'a IntegrandGraphGroupInfo>> {
532 if requested_graphs.is_empty() {
533 return Ok(detail.graph_groups.iter().collect());
534 }
535
536 let available_master_names = detail
537 .graph_groups
538 .iter()
539 .map(master_graph)
540 .collect::<Result<Vec<_>>>()?
541 .into_iter()
542 .map(|graph| graph.name.clone())
543 .collect::<Vec<_>>();
544 let missing = requested_graphs
545 .iter()
546 .filter(|graph_name| {
547 !available_master_names
548 .iter()
549 .any(|name| name == *graph_name)
550 })
551 .cloned()
552 .collect::<Vec<_>>();
553 if !missing.is_empty() {
554 return Err(eyre!(
555 "Unknown master graph filter(s): {}. Available master graphs: {}",
556 missing.join(", "),
557 available_master_names.join(", ")
558 ));
559 }
560
561 Ok(detail
562 .graph_groups
563 .iter()
564 .filter(|group| {
565 master_graph(group)
566 .map(|graph| requested_graphs.iter().any(|name| name == &graph.name))
567 .unwrap_or(false)
568 })
569 .collect())
570}
571
572fn render_integrand_detail(
573 state: &State,
574 state_folder: &Path,
575 process_id: usize,
576 integrand_name: &str,
577 requested_graphs: &[String],
578 requested_categories: &[IntegrandDisplayCategory],
579 hide_non_existing_thresholds: bool,
580) -> Result<()> {
581 let detail = state.get_integrand_info(
582 Some(&ProcessRef::Id(process_id)),
583 Some(&integrand_name.to_string()),
584 )?;
585 let process = &state.process_list.processes[process_id];
586 let artifact_sizes =
587 collect_integrand_artifact_sizes(state_folder, process, &detail.integrand_name)?;
588 let generation_summary = state.generation_summary(process_id, &detail.integrand_name);
589 render_integrand_detail_from_info(
590 &detail,
591 &artifact_sizes,
592 generation_summary,
593 requested_graphs,
594 requested_categories,
595 hide_non_existing_thresholds,
596 )
597}
598
599fn category_rows(
600 group: &IntegrandGraphGroupInfo,
601 category: IntegrandDisplayCategory,
602 hide_non_existing_thresholds: bool,
603) -> Vec<CategoryRow> {
604 match category {
605 IntegrandDisplayCategory::Generation => Vec::new(),
606 IntegrandDisplayCategory::Orientation => group
607 .orientations
608 .iter()
609 .map(|orientation| CategoryRow {
610 primary_id: render_category_id(orientation.orientation_id, false),
611 secondary_id: String::new(),
612 tertiary_id: String::new(),
613 value: render_orientation_signature(
614 &orientation.signature,
615 &group.orientation_edge_ids,
616 ),
617 })
618 .collect(),
619 IntegrandDisplayCategory::LoopMomentumBasis => group
620 .loop_momentum_bases
621 .iter()
622 .map(|basis| CategoryRow {
623 primary_id: render_category_id(basis.basis_id, basis.matches_generation_basis),
624 secondary_id: render_optional_category_id(
625 basis.channel_id,
626 basis.matches_generation_basis,
627 ),
628 tertiary_id: String::new(),
629 value: render_edge_ids_with_highlight(
630 &basis.edge_ids,
631 basis.matches_generation_basis,
632 ),
633 })
634 .collect(),
635 IntegrandDisplayCategory::Cuts => group
636 .cuts
637 .iter()
638 .map(|cut| CategoryRow {
639 primary_id: render_category_id(cut.cut_id, false),
640 secondary_id: render_threshold_counterterms(
641 &cut.left_thresholds,
642 hide_non_existing_thresholds,
643 ),
644 tertiary_id: render_threshold_counterterms(
645 &cut.right_thresholds,
646 hide_non_existing_thresholds,
647 ),
648 value: render_cut(&cut.edge_ids, cut.raising_power),
649 })
650 .collect(),
651 }
652}
653
654fn category_group_header(
655 group: &IntegrandGraphGroupInfo,
656 category: IntegrandDisplayCategory,
657) -> String {
658 match category {
659 IntegrandDisplayCategory::Orientation => {
660 render_orientation_edge_ids(&group.orientation_edge_ids)
661 }
662 IntegrandDisplayCategory::Generation
663 | IntegrandDisplayCategory::LoopMomentumBasis
664 | IntegrandDisplayCategory::Cuts => String::new(),
665 }
666}
667
668fn render_integrand_thresholds_table(
669 groups: &[&IntegrandGraphGroupInfo],
670 integrand_kind: IntegrandKind,
671) -> Result<Option<String>> {
672 let status_header = match integrand_kind {
673 IntegrandKind::Amplitude => "Classification",
674 IntegrandKind::CrossSection => "Active in cuts",
675 };
676 let mut builder = Builder::new();
677 builder.push_record([
678 "Group #".bold().blue().to_string(),
679 "Graph".bold().blue().to_string(),
680 "Esurface".bold().blue().to_string(),
681 "edges".bold().blue().to_string(),
682 status_header.bold().blue().to_string(),
683 ]);
684
685 let mut inserted_blocks = 0usize;
686 let mut separator_rows = vec![1usize];
687 let mut next_row = 1usize;
688
689 for group in groups {
690 if group.threshold_esurfaces.is_empty() {
691 continue;
692 }
693
694 let master = master_graph(group)?;
695 inserted_blocks += 1;
696 builder.push_record([
697 format!("#{}", group.group_id).blue().to_string(),
698 format!("#{} : {}", master.graph_id, master.name)
699 .green()
700 .bold()
701 .to_string(),
702 String::new(),
703 String::new(),
704 String::new(),
705 ]);
706 next_row += 1;
707 separator_rows.push(next_row);
708
709 for threshold in &group.threshold_esurfaces {
710 let representative_graph = group
711 .graphs
712 .iter()
713 .find(|graph| graph.graph_id == threshold.representative_graph_id)
714 .ok_or_else(|| {
715 eyre!(
716 "Threshold E-surface #{} in graph group #{} references missing representative graph #{}",
717 threshold.esurface_id,
718 group.group_id,
719 threshold.representative_graph_id,
720 )
721 })?;
722 let graph_label = if representative_graph.is_master {
723 String::new()
724 } else {
725 format!(
726 "#{} : {}",
727 representative_graph.graph_id, representative_graph.name
728 )
729 .yellow()
730 .to_string()
731 };
732 let status = match integrand_kind {
733 IntegrandKind::Amplitude => match threshold.classification.ok_or_else(|| {
734 eyre!(
735 "Amplitude threshold E-surface #{} in graph group #{} has no classification",
736 threshold.esurface_id,
737 group.group_id,
738 )
739 })? {
740 IntegrandEsurfaceClassification::Existing => {
741 "existing".cyan().to_string()
742 }
743 IntegrandEsurfaceClassification::Pinched => {
744 "pinched".yellow().to_string()
745 }
746 IntegrandEsurfaceClassification::NonExisting => {
747 "non-existing".dimmed().to_string()
748 }
749 },
750 IntegrandKind::CrossSection => {
751 if threshold.active_cuts.is_empty() {
752 "none".dimmed().to_string()
753 } else {
754 threshold
755 .active_cuts
756 .iter()
757 .map(|cut| {
758 if cut.can_become_pinched {
759 format!("#{}*", cut.cut_id)
760 } else {
761 format!("#{}", cut.cut_id)
762 }
763 })
764 .join(",")
765 .cyan()
766 .to_string()
767 }
768 }
769 };
770 builder.push_record([
771 String::new(),
772 graph_label,
773 format!("#{}", threshold.esurface_id).yellow().to_string(),
774 render_threshold_esurface(&threshold.edge_ids),
775 status,
776 ]);
777 next_row += 1;
778 }
779 separator_rows.push(next_row);
780 }
781
782 if inserted_blocks == 0 {
783 return Ok(None);
784 }
785
786 let mut table = builder.build();
787 let mut style = Theme::from_style(Style::rounded().remove_horizontals());
788 let mut unique_separator_rows = separator_rows.into_iter().unique().collect_vec();
789 if let Some(last_row) = unique_separator_rows.pop() {
790 for row in unique_separator_rows {
791 style.insert_horizontal_line(
792 row,
793 HorizontalLine::new('─')
794 .intersection('┼')
795 .left('├')
796 .right('┤'),
797 );
798 }
799 style.insert_horizontal_line(
800 last_row,
801 HorizontalLine::new('─')
802 .intersection('┴')
803 .left('╰')
804 .right('╯'),
805 );
806 }
807 table.with(style);
808 Ok(Some(table.to_string()))
809}
810
811fn render_integrand_category_table(
812 groups: &[&IntegrandGraphGroupInfo],
813 category: IntegrandDisplayCategory,
814 hide_non_existing_thresholds: bool,
815) -> Result<Option<String>> {
816 let mut builder = Builder::new();
817 match category {
818 IntegrandDisplayCategory::LoopMomentumBasis => {
819 builder.push_record([
820 "Group #".bold().blue().to_string(),
821 "Graph".bold().blue().to_string(),
822 "basis ID".bold().blue().to_string(),
823 "channel ID".bold().blue().to_string(),
824 category.value_header().bold().blue().to_string(),
825 ]);
826 }
827 IntegrandDisplayCategory::Cuts => {
828 builder.push_record([
829 "Group #".bold().blue().to_string(),
830 "Graph".bold().blue().to_string(),
831 "ID".bold().blue().to_string(),
832 category.value_header().bold().blue().to_string(),
833 "left thresholds".bold().blue().to_string(),
834 "right thresholds".bold().blue().to_string(),
835 ]);
836 }
837 _ => {
838 builder.push_record([
839 "Group #".bold().blue().to_string(),
840 "Graph".bold().blue().to_string(),
841 "ID".bold().blue().to_string(),
842 category.value_header().bold().blue().to_string(),
843 ]);
844 }
845 }
846
847 let mut inserted_blocks = 0usize;
848 let mut separator_rows = vec![1usize];
849 let mut next_row = 1usize;
850
851 for group in groups {
852 let rows = category_rows(group, category, hide_non_existing_thresholds);
853 if rows.is_empty() {
854 continue;
855 }
856
857 let master = master_graph(group)?;
858 inserted_blocks += 1;
859 match category {
860 IntegrandDisplayCategory::LoopMomentumBasis => {
861 builder.push_record([
862 format!("#{}", group.group_id).blue().to_string(),
863 format!("#{} : {}", master.graph_id, master.name)
864 .green()
865 .bold()
866 .to_string(),
867 String::new(),
868 String::new(),
869 category_group_header(group, category),
870 ]);
871 }
872 IntegrandDisplayCategory::Cuts => {
873 builder.push_record([
874 format!("#{}", group.group_id).blue().to_string(),
875 format!("#{} : {}", master.graph_id, master.name)
876 .green()
877 .bold()
878 .to_string(),
879 String::new(),
880 category_group_header(group, category),
881 String::new(),
882 String::new(),
883 ]);
884 }
885 _ => {
886 builder.push_record([
887 format!("#{}", group.group_id).blue().to_string(),
888 format!("#{} : {}", master.graph_id, master.name)
889 .green()
890 .bold()
891 .to_string(),
892 String::new(),
893 category_group_header(group, category),
894 ]);
895 }
896 }
897 next_row += 1;
898 separator_rows.push(next_row);
899
900 for row in rows {
901 match category {
902 IntegrandDisplayCategory::LoopMomentumBasis => {
903 builder.push_record([
904 String::new(),
905 String::new(),
906 row.primary_id,
907 row.secondary_id,
908 row.value,
909 ]);
910 }
911 IntegrandDisplayCategory::Cuts => {
912 builder.push_record([
913 String::new(),
914 String::new(),
915 row.primary_id,
916 row.value,
917 row.secondary_id,
918 row.tertiary_id,
919 ]);
920 }
921 _ => {
922 builder.push_record([String::new(), String::new(), row.primary_id, row.value]);
923 }
924 }
925 next_row += 1;
926 }
927 separator_rows.push(next_row);
928 }
929
930 if inserted_blocks == 0 {
931 return Ok(None);
932 }
933
934 let mut table = builder.build();
935 let mut style = Theme::from_style(Style::rounded().remove_horizontals());
936 let mut unique_separator_rows = separator_rows.into_iter().unique().collect_vec();
937 if let Some(last_row) = unique_separator_rows.pop() {
938 for row in unique_separator_rows {
939 style.insert_horizontal_line(
940 row,
941 HorizontalLine::new('─')
942 .intersection('┼')
943 .left('├')
944 .right('┤'),
945 );
946 }
947 style.insert_horizontal_line(
948 last_row,
949 HorizontalLine::new('─')
950 .intersection('┴')
951 .left('╰')
952 .right('╯'),
953 );
954 }
955 table.with(style);
956 Ok(Some(table.to_string()))
957}
958
959fn render_generation_category(summary: Option<&IntegrandGenerationSummary>) -> String {
960 summary
961 .and_then(|summary| {
962 render_generation_summary(&summary.reports, summary.peak_ram_bytes, None, None)
963 })
964 .unwrap_or_else(|| {
965 "No generation summary has been recorded for this integrand."
966 .yellow()
967 .to_string()
968 })
969}
970
971fn render_integrand_detail_from_info(
972 detail: &IntegrandInfo,
973 artifact_sizes: &IntegrandArtifactSizes,
974 generation_summary: Option<&IntegrandGenerationSummary>,
975 requested_graphs: &[String],
976 requested_categories: &[IntegrandDisplayCategory],
977 hide_non_existing_thresholds: bool,
978) -> Result<()> {
979 info!(
980 "{}",
981 format!(
982 "Integrand '{}' for process #{} ({})",
983 detail.integrand_name, detail.process_id, detail.process_name
984 )
985 .bold()
986 .blue()
987 );
988
989 let summary_rows = vec![
990 DetailSummaryRow {
991 field: "kind".to_string(),
992 value: detail.kind.to_string().yellow().to_string(),
993 },
994 DetailSummaryRow {
995 field: "compile enabled".to_string(),
996 value: detail
997 .generation_compilation
998 .compile_enabled()
999 .to_string()
1000 .yellow()
1001 .to_string(),
1002 },
1003 DetailSummaryRow {
1004 field: "generation backend".to_string(),
1005 value: detail
1006 .generation_compilation
1007 .active_backend_name()
1008 .yellow()
1009 .to_string(),
1010 },
1011 DetailSummaryRow {
1012 field: "generation compile options".to_string(),
1013 value: detail
1014 .generation_compilation
1015 .external_options()
1016 .map(ToString::to_string)
1017 .unwrap_or_else(|| "(none)".to_string())
1018 .magenta()
1019 .to_string(),
1020 },
1021 DetailSummaryRow {
1022 field: "active f64 backend".to_string(),
1023 value: detail.active_f64_backend.to_string().yellow().to_string(),
1024 },
1025 DetailSummaryRow {
1026 field: "graphs".to_string(),
1027 value: detail.graph_count.to_string().yellow().to_string(),
1028 },
1029 DetailSummaryRow {
1030 field: "graph groups".to_string(),
1031 value: detail.graph_group_count.to_string().yellow().to_string(),
1032 },
1033 DetailSummaryRow {
1034 field: ".bin size".to_string(),
1035 value: format_artifact_size(artifact_sizes.bin_disk_size_bytes),
1036 },
1037 DetailSummaryRow {
1038 field: ".so size".to_string(),
1039 value: format_artifact_size(artifact_sizes.so_disk_size_bytes),
1040 },
1041 ];
1042 let mut summary_table = tabled::Table::new(summary_rows);
1043 summary_table.with(Style::rounded());
1044 info!(
1045 "
1046{summary_table}"
1047 );
1048
1049 let groups = filtered_graph_groups(detail, requested_graphs)?;
1050 let categories = selected_integrand_categories(detail, requested_categories)?;
1051 for category in categories {
1052 if category == IntegrandDisplayCategory::Generation {
1053 let table = render_generation_category(generation_summary);
1054 info!(
1055 "
1056{}
1057{table}",
1058 category.title().bold().blue()
1059 );
1060 continue;
1061 }
1062
1063 if let Some(table) =
1064 render_integrand_category_table(&groups, category, hide_non_existing_thresholds)?
1065 {
1066 info!(
1067 "
1068{}
1069{table}",
1070 category.title().bold().blue()
1071 );
1072 }
1073
1074 if category == IntegrandDisplayCategory::Cuts {
1075 if let Some(table) = render_integrand_thresholds_table(&groups, detail.kind)? {
1076 info!(
1077 "
1078{}
1079{table}",
1080 "Threshold esurfaces".bold().blue()
1081 );
1082 }
1083 }
1084 }
1085
1086 Ok(())
1087}
1088
1089fn render_processes_table(state: &State, state_folder: &Path) -> Result<()> {
1090 if state.process_list.processes.is_empty() {
1091 info!("{}", "No processes generated yet.".yellow());
1092 return Ok(());
1093 }
1094
1095 let mut builder = Builder::new();
1096 builder.push_record([
1097 "process #".bold().blue().to_string(),
1098 "process".bold().blue().to_string(),
1099 "# integrands".bold().blue().to_string(),
1100 "integrand names".bold().blue().to_string(),
1101 "graphs / integrand".bold().blue().to_string(),
1102 "graph groups / integrand".bold().blue().to_string(),
1103 ".bin size / integrand".bold().blue().to_string(),
1104 ".so size / integrand".bold().blue().to_string(),
1105 ]);
1106
1107 for process in &state.process_list.processes {
1108 let metrics = collect_integrand_metrics(state_folder, process)?;
1109 builder.push_record([
1110 format!("#{}", process.definition.process_id)
1111 .blue()
1112 .to_string(),
1113 process
1114 .definition
1115 .folder_name
1116 .clone()
1117 .green()
1118 .bold()
1119 .to_string(),
1120 metrics.len().to_string().yellow().to_string(),
1121 format_integrand_names(&metrics),
1122 format_metrics_column(&metrics, |metric| metric.graphs.to_string()),
1123 format_metrics_column(&metrics, |metric| metric.graph_groups.to_string()),
1124 format_metrics_column(&metrics, |metric| {
1125 format_artifact_size(metric.bin_disk_size_bytes)
1126 }),
1127 format_metrics_column(&metrics, |metric| {
1128 format_artifact_size(metric.so_disk_size_bytes)
1129 }),
1130 ]);
1131 }
1132
1133 let mut table = builder.build();
1134 table.with(Style::rounded());
1135 info!("{}", "Processes".bold().blue());
1136 info!("\n{table}");
1137 Ok(())
1138}
1139
1140fn render_integrands_table(
1141 state: &State,
1142 state_folder: &Path,
1143 process_filter: Option<usize>,
1144) -> Result<()> {
1145 if state.process_list.processes.is_empty() {
1146 info!("{}", "No processes generated yet.".yellow());
1147 return Ok(());
1148 }
1149
1150 let mut builder = Builder::new();
1151 builder.push_record([
1152 "process #".bold().blue().to_string(),
1153 "process".bold().blue().to_string(),
1154 "integrand".bold().blue().to_string(),
1155 "graphs".bold().blue().to_string(),
1156 "graph groups".bold().blue().to_string(),
1157 ".bin size".bold().blue().to_string(),
1158 ".so size".bold().blue().to_string(),
1159 ]);
1160
1161 let mut num_rows = 0usize;
1162 for (process_index, process) in state.process_list.processes.iter().enumerate() {
1163 if process_filter.is_some_and(|selected| selected != process_index) {
1164 continue;
1165 }
1166 let metrics = collect_integrand_metrics(state_folder, process)?;
1167 for metric in metrics {
1168 num_rows += 1;
1169 builder.push_record([
1170 format!("#{}", process.definition.process_id)
1171 .blue()
1172 .to_string(),
1173 process
1174 .definition
1175 .folder_name
1176 .clone()
1177 .green()
1178 .bold()
1179 .to_string(),
1180 metric.name.cyan().to_string(),
1181 metric.graphs.to_string().yellow().to_string(),
1182 metric.graph_groups.to_string().yellow().to_string(),
1183 format_artifact_size(metric.bin_disk_size_bytes),
1184 format_artifact_size(metric.so_disk_size_bytes),
1185 ]);
1186 }
1187 }
1188
1189 if num_rows == 0 {
1190 if let Some(process_id) = process_filter {
1191 let process = &state.process_list.processes[process_id];
1192 info!(
1193 "{}",
1194 format!(
1195 "No integrands found for process #{} ({})",
1196 process.definition.process_id, process.definition.folder_name
1197 )
1198 .yellow()
1199 );
1200 } else {
1201 info!("{}", "No integrands found.".yellow());
1202 }
1203 return Ok(());
1204 }
1205
1206 let mut table = builder.build();
1207 table.with(Style::rounded());
1208
1209 if let Some(process_id) = process_filter {
1210 let process = &state.process_list.processes[process_id];
1211 info!(
1212 "{}",
1213 format!(
1214 "Integrands for process #{} ({})",
1215 process.definition.process_id, process.definition.folder_name
1216 )
1217 .bold()
1218 .blue()
1219 );
1220 } else {
1221 info!("{}", "Integrands".bold().blue());
1222 }
1223 info!("\n{table}");
1224 Ok(())
1225}
1226
1227fn collect_integrand_metrics(
1228 state_folder: &Path,
1229 process: &Process,
1230) -> Result<Vec<IntegrandMetrics>> {
1231 match &process.collection {
1232 ProcessCollection::Amplitudes(amplitudes) => amplitudes
1233 .values()
1234 .map(|amplitude| integrand_metrics_from_amplitude(state_folder, process, amplitude))
1235 .collect(),
1236 ProcessCollection::CrossSections(cross_sections) => cross_sections
1237 .values()
1238 .map(|cross_section| {
1239 integrand_metrics_from_cross_section(state_folder, process, cross_section)
1240 })
1241 .collect(),
1242 }
1243}
1244
1245fn integrand_metrics_from_amplitude(
1246 state_folder: &Path,
1247 process: &Process,
1248 amplitude: &Amplitude,
1249) -> Result<IntegrandMetrics> {
1250 let artifact_sizes = collect_integrand_artifact_sizes(state_folder, process, &litude.name)?;
1251 Ok(IntegrandMetrics {
1252 name: amplitude.name.clone(),
1253 graphs: amplitude.graphs.len(),
1254 graph_groups: amplitude.graph_group_structure.len(),
1255 bin_disk_size_bytes: artifact_sizes.bin_disk_size_bytes,
1256 so_disk_size_bytes: artifact_sizes.so_disk_size_bytes,
1257 })
1258}
1259
1260fn integrand_metrics_from_cross_section(
1261 state_folder: &Path,
1262 process: &Process,
1263 cross_section: &CrossSection,
1264) -> Result<IntegrandMetrics> {
1265 let artifact_sizes =
1266 collect_integrand_artifact_sizes(state_folder, process, &cross_section.name)?;
1267 Ok(IntegrandMetrics {
1268 name: cross_section.name.clone(),
1269 graphs: cross_section.supergraphs.len(),
1270 graph_groups: cross_section.graph_group_structure.len(),
1271 bin_disk_size_bytes: artifact_sizes.bin_disk_size_bytes,
1272 so_disk_size_bytes: artifact_sizes.so_disk_size_bytes,
1273 })
1274}
1275
1276fn collect_integrand_artifact_sizes(
1277 state_folder: &Path,
1278 process: &Process,
1279 integrand_name: &str,
1280) -> Result<IntegrandArtifactSizes> {
1281 let artifact_root = integrand_artifact_root(state_folder, process, integrand_name);
1282 Ok(IntegrandArtifactSizes {
1283 bin_disk_size_bytes: disk_usage_for_extension(&artifact_root, "bin")?,
1284 so_disk_size_bytes: disk_usage_for_extension(&artifact_root, "so")?,
1285 })
1286}
1287
1288fn integrand_artifact_root(
1289 state_folder: &Path,
1290 process: &Process,
1291 integrand_name: &str,
1292) -> PathBuf {
1293 let collection_dir = match &process.collection {
1294 ProcessCollection::Amplitudes(_) => "amplitudes",
1295 ProcessCollection::CrossSections(_) => "cross_sections",
1296 };
1297 state_folder
1298 .join("processes")
1299 .join(collection_dir)
1300 .join(&process.definition.folder_name)
1301 .join(integrand_name)
1302}
1303
1304fn disk_usage_for_extension(root: &Path, extension: &str) -> Result<Option<u64>> {
1305 if !root.try_exists().with_context(|| {
1306 format!(
1307 "While checking saved integrand artifacts under {}",
1308 root.display()
1309 )
1310 })? {
1311 return Ok(None);
1312 }
1313
1314 let mut total = 0u64;
1315 for entry in WalkDir::new(root) {
1316 let entry = entry.with_context(|| {
1317 format!(
1318 "While walking saved integrand artifacts under {}",
1319 root.display()
1320 )
1321 })?;
1322 if !entry.file_type().is_file() || entry.path().extension() != Some(OsStr::new(extension)) {
1323 continue;
1324 }
1325
1326 let metadata = entry
1327 .metadata()
1328 .with_context(|| format!("While reading metadata for {}", entry.path().display()))?;
1329 total = total
1330 .checked_add(file_disk_usage_bytes(&metadata))
1331 .ok_or_else(|| {
1332 eyre!(
1333 "Artifact size overflow while summing .{} files under {}",
1334 extension,
1335 root.display()
1336 )
1337 })?;
1338 }
1339 Ok(Some(total))
1340}
1341
1342fn file_disk_usage_bytes(metadata: &fs::Metadata) -> u64 {
1343 #[cfg(unix)]
1344 {
1345 use std::os::unix::fs::MetadataExt;
1346 metadata.blocks().saturating_mul(512)
1347 }
1348
1349 #[cfg(not(unix))]
1350 {
1351 metadata.len()
1352 }
1353}
1354
1355fn format_integrand_names(metrics: &[IntegrandMetrics]) -> String {
1356 if metrics.is_empty() {
1357 return "(none)".dimmed().to_string();
1358 }
1359 metrics
1360 .iter()
1361 .map(|metric| metric.name.clone().cyan().to_string())
1362 .collect::<Vec<_>>()
1363 .join("\n")
1364}
1365
1366fn format_metrics_column(
1367 metrics: &[IntegrandMetrics],
1368 format_value: impl Fn(&IntegrandMetrics) -> String,
1369) -> String {
1370 if metrics.is_empty() {
1371 return "(none)".dimmed().to_string();
1372 }
1373 metrics
1374 .iter()
1375 .map(|metric| format!("{}: {}", metric.name.cyan(), format_value(metric)))
1376 .collect::<Vec<_>>()
1377 .join("\n")
1378}
1379
1380fn format_artifact_size(size: Option<u64>) -> String {
1381 size.map(|size| format_bytes(size).magenta().to_string())
1382 .unwrap_or_else(|| "(not saved)".dimmed().to_string())
1383}
1384
1385fn format_bytes(size: u64) -> String {
1386 const KIB: u64 = 1024;
1387 const MIB: u64 = KIB * 1024;
1388 const GIB: u64 = MIB * 1024;
1389
1390 if size < KIB {
1391 format!("{size} B")
1392 } else if size < MIB {
1393 format!("{:.2} KiB", size as f64 / KIB as f64)
1394 } else if size < GIB {
1395 format!("{:.2} MiB", size as f64 / MIB as f64)
1396 } else {
1397 format!("{:.2} GiB", size as f64 / GIB as f64)
1398 }
1399}
1400
1401fn command_block_contents(block: &CommandsBlock) -> String {
1402 if block.commands.is_empty() {
1403 return "(empty)".to_string();
1404 }
1405
1406 block
1407 .commands
1408 .iter()
1409 .map(display_command)
1410 .collect::<Vec<_>>()
1411 .join("\n")
1412}
1413
1414fn render_command_blocks(run_history: &RunHistory, selected_name: Option<&str>) -> Result<()> {
1415 if let Some(name) = selected_name {
1416 let block = run_history.command_block(name).ok_or_else(|| {
1417 eyre!(
1418 "Unknown command block '{}'. Available command blocks: {}",
1419 name,
1420 run_history
1421 .command_blocks
1422 .iter()
1423 .map(|block| block.name.as_str())
1424 .collect::<Vec<_>>()
1425 .join(", ")
1426 )
1427 })?;
1428 info!("\n{}", command_block_contents(block));
1429 return Ok(());
1430 }
1431
1432 if run_history.command_blocks.is_empty() {
1433 info!("{}", "No active command blocks.".yellow());
1434 return Ok(());
1435 }
1436
1437 info!("{}", "Command blocks".bold().blue());
1438 info!("\n{}", render_command_blocks_table(run_history));
1439 Ok(())
1440}
1441
1442fn render_command_blocks_table(run_history: &RunHistory) -> String {
1443 let mut builder = Builder::new();
1444 builder.push_record([
1445 "name".bold().blue().to_string(),
1446 "commands".bold().blue().to_string(),
1447 ]);
1448
1449 for block in &run_history.command_blocks {
1450 builder.push_record([
1451 block.name.green().bold().to_string(),
1452 command_block_contents(block),
1453 ]);
1454 }
1455
1456 let mut table = builder.build();
1457 let mut style = Theme::from_style(Style::rounded().remove_horizontals());
1458 for row in 1..=run_history.command_blocks.len() {
1459 style.insert_horizontal_line(
1460 row,
1461 HorizontalLine::new('─')
1462 .intersection('┼')
1463 .left('├')
1464 .right('┤'),
1465 );
1466 }
1467 table.with(style);
1468 table.to_string()
1469}
1470
1471fn render_named_process_settings(
1472 state: &State,
1473 target: &DisplayProcessNamedSettingsArgs,
1474 setting_kind: NamedProcessSettingKind,
1475) -> Result<()> {
1476 let process_id = state.resolve_process_ref(target.process.process.as_ref())?;
1477 let process = &state.process_list.processes[process_id];
1478 let integrand_names = selected_integrand_names(process, target.process.integrand_name.as_ref());
1479
1480 if let Some(name) = target.name.as_deref() {
1481 return render_named_process_setting_detail(
1482 state,
1483 process_id,
1484 &process.definition.folder_name,
1485 process.definition.process_id,
1486 &integrand_names,
1487 setting_kind,
1488 name,
1489 );
1490 }
1491
1492 let mut builder = Builder::new();
1493 let show_integrand = integrand_names.len() > 1;
1494 let mut header = Vec::new();
1495 if show_integrand {
1496 header.push("integrand".bold().blue().to_string());
1497 }
1498 header.push("name".bold().blue().to_string());
1499 header.push("kind".bold().blue().to_string());
1500 header.push("details".bold().blue().to_string());
1501 builder.push_record(header);
1502
1503 let mut num_rows = 0usize;
1504 for integrand_name in &integrand_names {
1505 let integrand = state
1506 .process_list
1507 .get_integrand(process_id, integrand_name)?
1508 .require_generated()?;
1509 match setting_kind {
1510 NamedProcessSettingKind::Quantity => {
1511 for (name, settings) in &integrand.get_settings().quantities {
1512 num_rows += 1;
1513 let mut record = Vec::new();
1514 if show_integrand {
1515 record.push(integrand_name.clone().cyan().to_string());
1516 }
1517 record.push(name.clone().green().to_string());
1518 record.push(quantity_kind(settings).yellow().to_string());
1519 record.push(summarize_quantity(settings));
1520 builder.push_record(record);
1521 }
1522 }
1523 NamedProcessSettingKind::Observable => {
1524 for (name, settings) in &integrand.get_settings().observables {
1525 num_rows += 1;
1526 let mut record = Vec::new();
1527 if show_integrand {
1528 record.push(integrand_name.clone().cyan().to_string());
1529 }
1530 record.push(name.clone().green().to_string());
1531 record.push(observable_kind(settings).yellow().to_string());
1532 record.push(summarize_observable(settings));
1533 builder.push_record(record);
1534 }
1535 }
1536 NamedProcessSettingKind::Selector => {
1537 for (name, settings) in &integrand.get_settings().selectors {
1538 num_rows += 1;
1539 let mut record = Vec::new();
1540 if show_integrand {
1541 record.push(integrand_name.clone().cyan().to_string());
1542 }
1543 record.push(name.clone().green().to_string());
1544 record.push(selector_kind(settings).yellow().to_string());
1545 record.push(summarize_selector(settings));
1546 builder.push_record(record);
1547 }
1548 }
1549 }
1550 }
1551
1552 if num_rows == 0 {
1553 info!(
1554 "{}",
1555 format!(
1556 "No {} configured for process #{} ({})",
1557 setting_kind.plural(),
1558 process.definition.process_id,
1559 process.definition.folder_name
1560 )
1561 .yellow()
1562 );
1563 return Ok(());
1564 }
1565
1566 let mut table = builder.build();
1567 table.with(Style::rounded());
1568 let title = if show_integrand {
1569 format!(
1570 "{} for process #{} ({})",
1571 setting_kind.plural(),
1572 process.definition.process_id,
1573 process.definition.folder_name
1574 )
1575 } else {
1576 format!(
1577 "{} for process #{} ({}) integrand '{}'",
1578 setting_kind.plural(),
1579 process.definition.process_id,
1580 process.definition.folder_name,
1581 integrand_names[0]
1582 )
1583 };
1584 info!("{}", title.bold().blue());
1585 info!("\n{table}");
1586 Ok(())
1587}
1588
1589fn render_named_process_setting_detail(
1590 state: &State,
1591 process_id: usize,
1592 process_name: &str,
1593 process_display_id: usize,
1594 integrand_names: &[String],
1595 setting_kind: NamedProcessSettingKind,
1596 name: &str,
1597) -> Result<()> {
1598 let mut selected_roots = Vec::new();
1599 let mut missing_integrands = Vec::new();
1600
1601 for integrand_name in integrand_names {
1602 let integrand = state
1603 .process_list
1604 .get_integrand(process_id, integrand_name)?
1605 .require_generated()?;
1606 let serialized = serialize_runtime_named_settings(integrand.get_settings())?;
1607 let map = match setting_kind {
1608 NamedProcessSettingKind::Quantity => &serialized.quantities,
1609 NamedProcessSettingKind::Observable => &serialized.observables,
1610 NamedProcessSettingKind::Selector => &serialized.selectors,
1611 };
1612 if let Some(root) = map.get(name) {
1613 selected_roots.push((integrand_name.clone(), root.clone()));
1614 } else {
1615 missing_integrands.push(integrand_name.clone());
1616 }
1617 }
1618
1619 if selected_roots.is_empty() {
1620 return Err(eyre!(
1621 "No {} named '{}' found for process #{} ({})",
1622 setting_kind.singular(),
1623 name,
1624 process_display_id,
1625 process_name
1626 ));
1627 }
1628
1629 if !missing_integrands.is_empty() {
1630 return Err(eyre!(
1631 "{} '{}' is missing from integrand(s): {}",
1632 setting_kind.singular(),
1633 name,
1634 missing_integrands.join(", ")
1635 ));
1636 }
1637
1638 for (integrand_name, root) in selected_roots {
1639 render_settings_table(
1640 &format!(
1641 "{} '{}' for process #{} ({}) integrand '{}'",
1642 setting_kind.singular(),
1643 name,
1644 process_display_id,
1645 process_name,
1646 integrand_name
1647 ),
1648 &root,
1649 None,
1650 )?;
1651 }
1652
1653 Ok(())
1654}
1655
1656fn selected_integrand_names(process: &Process, requested: Option<&String>) -> Vec<String> {
1657 if let Some(integrand_name) = requested {
1658 vec![integrand_name.clone()]
1659 } else {
1660 process
1661 .get_integrand_names()
1662 .into_iter()
1663 .map(str::to_string)
1664 .collect()
1665 }
1666}
1667
1668impl DisplaySettingsTarget {
1669 fn run(
1670 &self,
1671 state: &State,
1672 global_settings: &CLISettings,
1673 default_runtime_settings: &RuntimeSettings,
1674 ) -> Result<()> {
1675 match self {
1676 DisplaySettingsTarget::Global { path } => {
1677 let root = serialize_settings_with_defaults(
1678 global_settings,
1679 "global settings for display",
1680 )?;
1681 render_settings_table("global settings", &root, path.as_deref())?;
1682 }
1683 DisplaySettingsTarget::DefaultRuntime { path } => {
1684 let root = serialize_settings_with_defaults(
1685 default_runtime_settings,
1686 "default runtime settings for display",
1687 )?;
1688 render_settings_table("default runtime settings", &root, path.as_deref())?;
1689 }
1690 DisplaySettingsTarget::Process { process, path } => {
1691 let process_id = state.resolve_process_ref(process.process.as_ref())?;
1692 let process_ref = &state.process_list.processes[process_id];
1693 if let Some(name) = &process.integrand_name {
1694 let integrand = state
1695 .process_list
1696 .get_integrand(process_id, name)?
1697 .require_generated()?;
1698 let settings = serialize_settings_with_defaults(
1699 integrand.get_settings(),
1700 "process runtime settings for display",
1701 )?;
1702 render_settings_table(
1703 &format!(
1704 "process settings for #{} ({}) integrand '{}'",
1705 process_ref.definition.process_id,
1706 process_ref.definition.folder_name,
1707 name
1708 ),
1709 &settings,
1710 path.as_deref(),
1711 )?;
1712 } else {
1713 for integrand_name in process_ref.get_integrand_names() {
1714 let integrand = state
1715 .process_list
1716 .get_integrand(process_id, integrand_name)?
1717 .require_generated()?;
1718 let settings = serialize_settings_with_defaults(
1719 integrand.get_settings(),
1720 "process runtime settings for display",
1721 )?;
1722 render_settings_table(
1723 &format!(
1724 "process settings for #{} ({}) integrand '{}'",
1725 process_ref.definition.process_id,
1726 process_ref.definition.folder_name,
1727 integrand_name
1728 ),
1729 &settings,
1730 path.as_deref(),
1731 )?;
1732 }
1733 }
1734 }
1735 }
1736 Ok(())
1737 }
1738}
1739
1740fn render_settings_table(title: &str, root: &JsonValue, key_path: Option<&str>) -> Result<()> {
1741 let key_path = key_path.map(str::trim).filter(|path| !path.is_empty());
1742 let selected = if let Some(path) = key_path {
1743 value_at_path(root, path)?
1744 } else {
1745 root
1746 };
1747 let mut builder = Builder::new();
1748 builder.push_record(["key", "value"]);
1749
1750 match selected {
1751 JsonValue::Object(map) => {
1752 let mut keys: Vec<_> = map.keys().cloned().collect();
1753 keys.sort();
1754 for key in keys {
1755 let value = map
1756 .get(&key)
1757 .expect("sorted keys were derived from the same map");
1758 builder.push_record([key, format_json_value(value)]);
1759 }
1760 }
1761 JsonValue::Array(array) => {
1762 if array.is_empty() {
1763 builder.push_record(["(empty)".to_string(), "[]".to_string()]);
1764 } else {
1765 for (index, value) in array.iter().enumerate() {
1766 builder.push_record([index.to_string(), format_json_value(value)]);
1767 }
1768 }
1769 }
1770 primitive => {
1771 builder.push_record(["value".to_string(), format_json_value(primitive)]);
1772 }
1773 }
1774
1775 let mut table = builder.build();
1776 table.with(Style::rounded());
1777 if let Some(path) = key_path {
1778 info!("{title} (path: {path}):");
1779 } else {
1780 info!("{title}:");
1781 }
1782 info!("\n{table}");
1783 Ok(())
1784}
1785
1786fn format_json_value(value: &JsonValue) -> String {
1787 match value {
1788 JsonValue::Null => "null".to_string(),
1789 JsonValue::Bool(_) | JsonValue::Number(_) => value.to_string(),
1790 JsonValue::String(s) => s.clone(),
1791 _ => serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string()),
1792 }
1793}
1794
1795#[cfg(test)]
1796mod test {
1797 use clap::Parser;
1798 use serde_json::json;
1799
1800 use crate::{
1801 commands::Commands,
1802 integrand_info::{
1803 IntegrandActiveThresholdCutInfo, IntegrandCutThresholdInfo,
1804 IntegrandEsurfaceClassification, IntegrandGraphGroupInfo, IntegrandGraphInfo,
1805 IntegrandKind, IntegrandThresholdEsurfaceInfo, IntegrandThresholdStatus,
1806 },
1807 state::{CommandHistory, CommandsBlock, ProcessRef, RunHistory},
1808 CLISettings, Repl,
1809 };
1810 use gammalooprs::settings::RuntimeSettings;
1811
1812 use super::{
1813 command_block_contents, format_bytes, render_command_blocks_table,
1814 render_integrand_thresholds_table, render_threshold_counterterms,
1815 serialize_settings_with_defaults, value_at_path, Display, DisplaySettingsTarget,
1816 IntegrandDisplayCategory,
1817 };
1818
1819 #[test]
1820 fn threshold_rendering_dims_or_hides_non_existing_associations() {
1821 let thresholds = vec![
1822 IntegrandCutThresholdInfo {
1823 esurface_id: 71,
1824 status: IntegrandThresholdStatus::CanBecomePinched,
1825 cut_boundary_edge_ids: vec![1, 2],
1826 threshold_boundary_edge_ids: vec![3, 4],
1827 invariant_bound_is_applicable: true,
1828 },
1829 IntegrandCutThresholdInfo {
1830 esurface_id: 83,
1831 status: IntegrandThresholdStatus::ProvenNonExisting,
1832 cut_boundary_edge_ids: vec![1, 2],
1833 threshold_boundary_edge_ids: vec![5],
1834 invariant_bound_is_applicable: true,
1835 },
1836 ];
1837
1838 let shown = render_threshold_counterterms(&thresholds, false);
1839 assert!(shown.contains("71*"));
1840 assert!(shown.contains("83"));
1841
1842 let hidden = render_threshold_counterterms(&thresholds, true);
1843 assert!(hidden.contains("71*"));
1844 assert!(!hidden.contains("83"));
1845 }
1846
1847 #[test]
1848 fn threshold_inventory_lists_active_cut_and_pinch_marker() {
1849 let group = IntegrandGraphGroupInfo {
1850 group_id: 0,
1851 graphs: vec![IntegrandGraphInfo {
1852 graph_id: 0,
1853 name: "generic".to_string(),
1854 is_master: true,
1855 }],
1856 orientation_edge_ids: Vec::new(),
1857 orientations: Vec::new(),
1858 loop_momentum_bases: Vec::new(),
1859 threshold_esurface_ids: vec![5],
1860 threshold_esurfaces: vec![IntegrandThresholdEsurfaceInfo {
1861 esurface_id: 5,
1862 representative_graph_id: 0,
1863 edge_ids: vec![1, 2],
1864 classification: None,
1865 active_cuts: vec![IntegrandActiveThresholdCutInfo {
1866 cut_id: 7,
1867 can_become_pinched: true,
1868 }],
1869 }],
1870 cuts: Vec::new(),
1871 };
1872
1873 let rendered = render_integrand_thresholds_table(&[&group], IntegrandKind::CrossSection)
1874 .unwrap()
1875 .unwrap();
1876 assert!(rendered.contains("Active in cuts"));
1877 assert!(rendered.contains("#7*"));
1878 }
1879
1880 #[test]
1881 fn amplitude_threshold_inventory_reports_source_graph_and_classification() {
1882 let group = IntegrandGraphGroupInfo {
1883 group_id: 0,
1884 graphs: vec![
1885 IntegrandGraphInfo {
1886 graph_id: 0,
1887 name: "master".to_string(),
1888 is_master: true,
1889 },
1890 IntegrandGraphInfo {
1891 graph_id: 1,
1892 name: "member".to_string(),
1893 is_master: false,
1894 },
1895 ],
1896 orientation_edge_ids: Vec::new(),
1897 orientations: Vec::new(),
1898 loop_momentum_bases: Vec::new(),
1899 threshold_esurface_ids: vec![5],
1900 threshold_esurfaces: vec![IntegrandThresholdEsurfaceInfo {
1901 esurface_id: 5,
1902 representative_graph_id: 1,
1903 edge_ids: vec![1, 2],
1904 classification: Some(IntegrandEsurfaceClassification::Pinched),
1905 active_cuts: Vec::new(),
1906 }],
1907 cuts: Vec::new(),
1908 };
1909
1910 let rendered = render_integrand_thresholds_table(&[&group], IntegrandKind::Amplitude)
1911 .unwrap()
1912 .unwrap();
1913 assert!(rendered.contains("Classification"));
1914 assert!(!rendered.contains("Active in cuts"));
1915 assert!(rendered.contains("#1 : member"));
1916 assert!(rendered.contains("pinched"));
1917 }
1918
1919 #[test]
1920 fn parse_display_settings_process_with_path() {
1921 let repl = Repl::try_parse_from([
1922 "gammaloop",
1923 "display",
1924 "settings",
1925 "process",
1926 "-p",
1927 "epem_a_tth",
1928 "-i",
1929 "LO",
1930 "integrator.n_max",
1931 ])
1932 .unwrap();
1933
1934 match repl.command {
1935 Commands::Display(Display::Settings { target }) => match target {
1936 DisplaySettingsTarget::Process { process, path } => {
1937 assert_eq!(
1938 process.process,
1939 Some(ProcessRef::Unqualified("epem_a_tth".to_string()))
1940 );
1941 assert_eq!(process.integrand_name, Some("LO".to_string()));
1942 assert_eq!(path.as_deref(), Some("integrator.n_max"));
1943 }
1944 other => panic!("Expected process target, got {other:?}"),
1945 },
1946 other => panic!("Expected display settings command, got {other:?}"),
1947 }
1948 }
1949
1950 #[test]
1951 fn parse_display_settings_process_without_path() {
1952 let repl = Repl::try_parse_from([
1953 "gammaloop",
1954 "display",
1955 "settings",
1956 "process",
1957 "-p",
1958 "epem_a_tth",
1959 "-i",
1960 "LO",
1961 ])
1962 .unwrap();
1963
1964 match repl.command {
1965 Commands::Display(Display::Settings { target }) => match target {
1966 DisplaySettingsTarget::Process { path, .. } => {
1967 assert_eq!(path, None);
1968 }
1969 other => panic!("Expected process target, got {other:?}"),
1970 },
1971 other => panic!("Expected display settings command, got {other:?}"),
1972 }
1973 }
1974
1975 #[test]
1976 fn parse_display_model_target() {
1977 let repl = Repl::try_parse_from([
1978 "gammaloop",
1979 "display",
1980 "model",
1981 "-p",
1982 "epem_a_tth",
1983 "-i",
1984 "LO",
1985 "--show-particles",
1986 ])
1987 .unwrap();
1988
1989 match repl.command {
1990 Commands::Display(Display::Model {
1991 process,
1992 integrand_name,
1993 show_particles,
1994 ..
1995 }) => {
1996 assert_eq!(
1997 process,
1998 Some(ProcessRef::Unqualified("epem_a_tth".to_string()))
1999 );
2000 assert_eq!(integrand_name, Some("LO".to_string()));
2001 assert!(show_particles);
2002 }
2003 other => panic!("Expected display model command, got {other:?}"),
2004 }
2005 }
2006
2007 #[test]
2008 fn display_model_requires_process_and_integrand_together() {
2009 let err = Repl::try_parse_from(["gammaloop", "display", "model", "-p", "epem_a_tth"])
2010 .unwrap_err();
2011 assert!(err.to_string().contains("--integrand-name"));
2012 }
2013
2014 #[test]
2015 fn parse_display_quantities_without_name() {
2016 let repl = Repl::try_parse_from([
2017 "gammaloop",
2018 "display",
2019 "quantities",
2020 "-p",
2021 "epem_a_tth",
2022 "-i",
2023 "LO",
2024 ])
2025 .unwrap();
2026
2027 match repl.command {
2028 Commands::Display(Display::Quantities { target }) => {
2029 assert_eq!(
2030 target.process.process,
2031 Some(ProcessRef::Unqualified("epem_a_tth".to_string()))
2032 );
2033 assert_eq!(target.process.integrand_name, Some("LO".to_string()));
2034 assert_eq!(target.name, None);
2035 }
2036 other => panic!("Expected display quantities command, got {other:?}"),
2037 }
2038 }
2039
2040 #[test]
2041 fn parse_display_selector_with_name() {
2042 let repl = Repl::try_parse_from([
2043 "gammaloop",
2044 "display",
2045 "selectors",
2046 "-p",
2047 "epem_a_tth",
2048 "top_cut",
2049 ])
2050 .unwrap();
2051
2052 match repl.command {
2053 Commands::Display(Display::Selectors { target }) => {
2054 assert_eq!(
2055 target.process.process,
2056 Some(ProcessRef::Unqualified("epem_a_tth".to_string()))
2057 );
2058 assert_eq!(target.process.integrand_name, None);
2059 assert_eq!(target.name.as_deref(), Some("top_cut"));
2060 }
2061 other => panic!("Expected display selectors command, got {other:?}"),
2062 }
2063 }
2064
2065 #[test]
2066 fn parse_display_command_block_without_name() {
2067 let repl = Repl::try_parse_from(["gammaloop", "display", "command_block"]).unwrap();
2068
2069 match repl.command {
2070 Commands::Display(Display::CommandBlock { name }) => {
2071 assert_eq!(name, None);
2072 }
2073 other => panic!("Expected display command_block command, got {other:?}"),
2074 }
2075 }
2076
2077 #[test]
2078 fn parse_display_command_block_with_name() {
2079 let repl =
2080 Repl::try_parse_from(["gammaloop", "display", "command_block", "alpha"]).unwrap();
2081
2082 match repl.command {
2083 Commands::Display(Display::CommandBlock { name }) => {
2084 assert_eq!(name.as_deref(), Some("alpha"));
2085 }
2086 other => panic!("Expected display command_block command, got {other:?}"),
2087 }
2088 }
2089
2090 #[test]
2091 fn parse_display_command_block_rejects_hyphenated_name() {
2092 assert!(Repl::try_parse_from(["gammaloop", "display", "command-block"]).is_err());
2093 }
2094
2095 #[test]
2096 fn parse_display_settings_global_without_query() {
2097 let repl = Repl::try_parse_from(["gammaloop", "display", "settings", "global"]).unwrap();
2098
2099 match repl.command {
2100 Commands::Display(Display::Settings { target }) => match target {
2101 DisplaySettingsTarget::Global { path } => {
2102 assert_eq!(path, None);
2103 }
2104 other => panic!("Expected global target, got {other:?}"),
2105 },
2106 other => panic!("Expected display settings command, got {other:?}"),
2107 }
2108 }
2109
2110 #[test]
2111 fn parse_display_settings_defaults_alias() {
2112 let repl = Repl::try_parse_from([
2113 "gammaloop",
2114 "display",
2115 "settings",
2116 "defaults",
2117 "integrator.n_start",
2118 ])
2119 .unwrap();
2120
2121 match repl.command {
2122 Commands::Display(Display::Settings { target }) => match target {
2123 DisplaySettingsTarget::DefaultRuntime { path } => {
2124 assert_eq!(path.as_deref(), Some("integrator.n_start"));
2125 }
2126 other => panic!("Expected default runtime target, got {other:?}"),
2127 },
2128 other => panic!("Expected display settings command, got {other:?}"),
2129 }
2130 }
2131
2132 #[test]
2133 fn value_lookup_supports_nested_paths() {
2134 let value = json!({
2135 "integrator": {
2136 "n_max": 42
2137 }
2138 });
2139 let found = value_at_path(&value, "integrator.n_max").unwrap();
2140 assert_eq!(found, &json!(42));
2141 }
2142
2143 #[test]
2144 fn serialized_settings_include_default_values() {
2145 let global = CLISettings::default();
2146 let global_json = serialize_settings_with_defaults(&global, "global settings").unwrap();
2147 assert!(value_at_path(&global_json, "global.n_cores.generate").is_ok());
2148
2149 let runtime = RuntimeSettings::default();
2150 let runtime_json =
2151 serialize_settings_with_defaults(&runtime, "default runtime settings").unwrap();
2152 assert!(value_at_path(&runtime_json, "integrator.n_start").is_ok());
2153 }
2154
2155 #[test]
2156 fn parse_display_integrand_without_process() {
2157 let repl = Repl::try_parse_from(["gammaloop", "display", "integrand"]).unwrap();
2158
2159 match repl.command {
2160 Commands::Display(Display::Integrands {
2161 process,
2162 integrand_name,
2163 graphs,
2164 categories,
2165 hide_non_existing_thresholds,
2166 }) => {
2167 assert_eq!(process, None);
2168 assert_eq!(integrand_name, None);
2169 assert!(graphs.is_empty());
2170 assert!(categories.is_empty());
2171 assert!(!hide_non_existing_thresholds);
2172 }
2173 other => panic!("Expected display integrand command, got {other:?}"),
2174 }
2175 }
2176
2177 #[test]
2178 fn structured_display_integrand_defaults_missing_threshold_filter() {
2179 let mut value = serde_json::to_value(Display::Integrands {
2180 process: None,
2181 integrand_name: None,
2182 graphs: Vec::new(),
2183 categories: Vec::new(),
2184 hide_non_existing_thresholds: false,
2185 })
2186 .unwrap();
2187 value
2188 .get_mut("Integrands")
2189 .and_then(serde_json::Value::as_object_mut)
2190 .unwrap()
2191 .remove("hide_non_existing_thresholds");
2192
2193 let display: Display = serde_json::from_value(value).unwrap();
2194 assert!(matches!(
2195 display,
2196 Display::Integrands {
2197 hide_non_existing_thresholds: false,
2198 ..
2199 }
2200 ));
2201 }
2202
2203 #[test]
2204 fn parse_display_integrand_detail() {
2205 let repl = Repl::try_parse_from([
2206 "gammaloop",
2207 "display",
2208 "integrand",
2209 "-p",
2210 "epem_a_tth",
2211 "-i",
2212 "LO",
2213 ])
2214 .unwrap();
2215
2216 match repl.command {
2217 Commands::Display(Display::Integrands {
2218 process,
2219 integrand_name,
2220 graphs,
2221 categories,
2222 hide_non_existing_thresholds,
2223 }) => {
2224 assert_eq!(
2225 process,
2226 Some(ProcessRef::Unqualified("epem_a_tth".to_string()))
2227 );
2228 assert_eq!(integrand_name.as_deref(), Some("LO"));
2229 assert!(graphs.is_empty());
2230 assert!(categories.is_empty());
2231 assert!(!hide_non_existing_thresholds);
2232 }
2233 other => panic!("Expected display integrand command, got {other:?}"),
2234 }
2235 }
2236
2237 #[test]
2238 fn parse_display_integrand_detail_filters() {
2239 let repl = Repl::try_parse_from([
2240 "gammaloop",
2241 "display",
2242 "integrand",
2243 "-p",
2244 "epem_a_tth",
2245 "-i",
2246 "LO",
2247 "-g",
2248 "GL0",
2249 "GL2",
2250 "--category",
2251 "generation",
2252 "orientation",
2253 "cuts",
2254 "--hide-non-existing-thresholds",
2255 ])
2256 .unwrap();
2257
2258 match repl.command {
2259 Commands::Display(Display::Integrands {
2260 process,
2261 integrand_name,
2262 graphs,
2263 categories,
2264 hide_non_existing_thresholds,
2265 }) => {
2266 assert_eq!(
2267 process,
2268 Some(ProcessRef::Unqualified("epem_a_tth".to_string()))
2269 );
2270 assert!(hide_non_existing_thresholds);
2271 assert_eq!(integrand_name.as_deref(), Some("LO"));
2272 assert_eq!(graphs, vec!["GL0".to_string(), "GL2".to_string()]);
2273 assert_eq!(
2274 categories,
2275 vec![
2276 IntegrandDisplayCategory::Generation,
2277 IntegrandDisplayCategory::Orientation,
2278 IntegrandDisplayCategory::Cuts,
2279 ]
2280 );
2281 }
2282 other => panic!("Expected display integrand command, got {other:?}"),
2283 }
2284 }
2285
2286 #[test]
2287 fn format_bytes_uses_binary_units() {
2288 assert_eq!(format_bytes(999), "999 B");
2289 assert_eq!(format_bytes(1024), "1.00 KiB");
2290 assert_eq!(format_bytes(1024 * 1024), "1.00 MiB");
2291 }
2292
2293 #[test]
2294 fn command_block_contents_preserve_raw_commands() {
2295 let block = CommandsBlock {
2296 name: "alpha".to_string(),
2297 commands: vec![
2298 CommandHistory::from_raw_string("display processes").unwrap(),
2299 CommandHistory::from_raw_string("set process -p triangle -i LO defaults").unwrap(),
2300 ],
2301 };
2302
2303 assert_eq!(
2304 command_block_contents(&block),
2305 "display processes\nset process -p triangle -i LO defaults"
2306 );
2307 }
2308
2309 #[test]
2310 fn command_block_contents_marks_empty_blocks() {
2311 let run_history = RunHistory {
2312 command_blocks: vec![CommandsBlock {
2313 name: "empty".to_string(),
2314 commands: Vec::new(),
2315 }],
2316 ..RunHistory::default()
2317 };
2318
2319 assert_eq!(
2320 command_block_contents(&run_history.command_blocks[0]),
2321 "(empty)"
2322 );
2323 }
2324
2325 #[test]
2326 fn command_blocks_table_separates_each_block() {
2327 let run_history = RunHistory {
2328 command_blocks: vec![
2329 CommandsBlock {
2330 name: "alpha".to_string(),
2331 commands: vec![CommandHistory::from_raw_string("display processes").unwrap()],
2332 },
2333 CommandsBlock {
2334 name: "beta".to_string(),
2335 commands: vec![CommandHistory::from_raw_string("display model").unwrap()],
2336 },
2337 ],
2338 ..RunHistory::default()
2339 };
2340
2341 let rendered = render_command_blocks_table(&run_history);
2342
2343 assert!(rendered.contains("alpha"), "{rendered}");
2344 assert!(rendered.contains("beta"), "{rendered}");
2345 assert_eq!(
2346 rendered
2347 .lines()
2348 .filter(|line| line.starts_with('├') && line.ends_with('┤'))
2349 .count(),
2350 2,
2351 "{rendered}"
2352 );
2353 }
2354}