1use std::{ops::ControlFlow, path::PathBuf, str::FromStr};
9
10use clap::{builder::ArgExt, Arg, Subcommand};
11use color_eyre::Report;
12use gammalooprs::settings::RuntimeSettings;
13use save::SaveState;
14use schemars::JsonSchema;
15use serde::{Deserialize, Serialize};
16
17use crate::{
18 state::{CommandHistory, RunHistory, State},
19 CLISettings,
20};
21use symbolica::atom::Atom;
22pub mod approach;
23pub use approach::Approach;
24pub mod bench;
25pub use bench::Bench;
26pub mod commands_block;
27pub use commands_block::StartCommandsBlock;
28pub mod display;
29pub use display::Display;
30pub mod duplicate;
31pub use duplicate::Duplicate;
32pub mod generate;
33pub use generate::Generate;
34pub mod import;
35pub use import::Import;
36pub mod inspect;
37pub use inspect::Inspect;
38pub mod integrate;
39pub use integrate::{Integrate, IntegrationOutput};
40pub mod remove;
41pub use remove::Remove;
42pub mod save;
43pub use save::Save;
44pub mod select;
45pub use select::Select;
46pub mod set;
47pub mod shell;
48pub use set::Set;
49pub use shell::Shell;
50pub mod threedreps;
51pub use threedreps::ThreeDRep;
52pub mod run;
53pub use run::Run;
54pub mod evaluate;
55pub mod evaluate_samples;
56pub use evaluate::Evaluate;
57pub mod renormalize;
58pub use renormalize::Renormalize;
59pub mod profile;
60pub use profile::Profile;
61pub(crate) mod process_settings;
62
63#[doc(hidden)]
65#[derive(Clone, Debug, Default)]
66pub struct CliArgumentMetadata {
67 pub requires: Vec<&'static str>,
68 pub default_missing_values: Vec<&'static str>,
69}
70
71impl ArgExt for CliArgumentMetadata {}
72
73pub(crate) trait CliArgumentMetadataExt {
74 fn cli_requires(self, id: &'static str) -> Self;
75 fn cli_default_missing_value(self, value: &'static str) -> Self;
76}
77
78impl CliArgumentMetadataExt for Arg {
79 fn cli_requires(self, id: &'static str) -> Self {
80 let mut metadata = self
81 .get::<CliArgumentMetadata>()
82 .cloned()
83 .unwrap_or_default();
84 metadata.requires.push(id);
85 self.requires(id).add(metadata)
86 }
87
88 fn cli_default_missing_value(self, value: &'static str) -> Self {
89 let mut metadata = self
90 .get::<CliArgumentMetadata>()
91 .cloned()
92 .unwrap_or_default();
93 metadata.default_missing_values.push(value);
94 self.default_missing_value(value).add(metadata)
95 }
96}
97
98#[derive(Debug, Clone, Default)]
104pub enum CommandOutput {
105 #[default]
107 None,
108 Evaluate(Atom),
110 Integrate(IntegrationOutput),
113}
114
115#[derive(Debug, Clone)]
121pub struct CommandExecution {
122 pub flow: ControlFlow<SaveState>,
124 pub output: CommandOutput,
126}
127
128impl CommandExecution {
129 pub fn continue_with(output: CommandOutput) -> Self {
130 Self {
131 flow: ControlFlow::Continue(()),
132 output,
133 }
134 }
135
136 pub fn continue_without_output() -> Self {
137 Self::continue_with(CommandOutput::None)
138 }
139
140 pub fn break_with(save_state: SaveState) -> Self {
141 Self {
142 flow: ControlFlow::Break(save_state),
143 output: CommandOutput::None,
144 }
145 }
146}
147
148#[allow(clippy::large_enum_variant)]
149#[derive(Subcommand, Debug, Serialize, Deserialize, Clone, JsonSchema, PartialEq)]
150pub enum Commands {
151 #[clap(subcommand)]
153 Display(Display),
154 #[clap(subcommand)]
156 Duplicate(Duplicate),
157 #[clap(subcommand)]
159 Set(Set),
160 #[clap(subcommand)]
162 Import(Import),
163 #[clap(subcommand)]
165 Save(Save),
166
167 Run(Run),
169 #[command(name = "start_commands_block")]
171 StartCommandsBlock(StartCommandsBlock),
172 #[command(name = "finish_commands_block")]
174 FinishCommandsBlock,
175
176 #[command(name = "remove")]
178 #[clap(subcommand)]
179 Remove(Remove),
180
181 Integrate(Integrate),
183
184 Generate(Generate),
185
186 Select(Select),
188
189 Quit(SaveState),
191 Inspect(Inspect),
194
195 Approach(Approach),
197
198 Evaluate(Evaluate),
200
201 Renormalize(Renormalize),
203
204 Bench(Bench),
206 #[clap(subcommand)]
208 Profile(Profile),
209
210 #[command(name = "3Drep")]
212 #[clap(subcommand)]
213 ThreeDRep(ThreeDRep),
214
215 Batch {
217 #[arg(value_name = "PROCESS_FILE", value_hint = clap::ValueHint::FilePath)]
219 process_file: PathBuf,
220 #[arg(value_name = "BATCH_INPUT_FILE", value_hint = clap::ValueHint::FilePath)]
222 batch_input_file: PathBuf,
223 #[arg(short = 'n', long, value_name = "NAME")]
225 name: String,
226 #[arg(value_name = "NAME")]
228 output_name: String,
229 },
230 #[command(name = "!")]
232 Shell(Shell),
233
234 #[doc(hidden)]
235 #[command(skip)]
236 CommandTemplate(String),
237}
238
239impl FromStr for Commands {
240 type Err = Report;
241 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
242 Ok(CommandHistory::from_raw_string(s)?.command)
243 }
244}
245
246impl Commands {
247 pub fn run(
248 self,
249 state: &mut State,
250 run_history: &mut RunHistory,
251 global_cli_settings: &mut CLISettings,
252 default_runtime_settings: &mut RuntimeSettings,
253 ) -> Result<CommandExecution, Report> {
254 match self {
255 Commands::Profile(mut p) => {
256 if let profile::ProfileResult::UltraViolet(analysis) =
257 p.run(state, global_cli_settings)?
258 {
259 let verdict = analysis.pass_fail(gammalooprs::uv::profile::UV_PROFILE_MAX_DOD);
260 if verdict.failed > 0 {
261 let Profile::UltraViolet(options) = &mut p else {
262 unreachable!()
263 };
264 let (process_id, integrand_name) = state.find_integrand_ref(
265 options.process.as_ref(),
266 options.integrand_name.as_ref(),
267 )?;
268 options.process = Some(crate::state::ProcessRef::Id(process_id));
269 options.integrand_name = Some(integrand_name.clone());
270 options.seed = Some(options.seed.unwrap_or(42));
271 if !options.uv_ray_directions.is_empty() && options.uv_ray_norms.is_empty()
272 {
273 options.uv_ray_norms.push(
274 state
275 .process_list
276 .get_integrand_mut(process_id, &integrand_name)?
277 .get_settings()
278 .kinematics
279 .e_cm,
280 );
281 }
282 let scope = if analysis.stopped_early {
283 "Stopped after the first failing limit; only completed limits are reported."
284 } else {
285 "All selected limits were profiled."
286 };
287 let reproduction =
290 toml::to_string_pretty(&std::collections::BTreeMap::from([(
291 "commands",
292 vec![Commands::Profile(p)],
293 )]))?;
294 return Err(eyre::eyre!(
295 "{verdict}\n{scope}\nReproduce in the same generated state with this run-card fragment:\n{reproduction}"
296 ));
297 }
298 }
299 }
300 Commands::ThreeDRep(command) => {
301 command.run(state, global_cli_settings)?;
302 }
303 Commands::Quit(s) => {
304 return Ok(CommandExecution::break_with(s));
305 }
306 Commands::Inspect(inspect) => {
307 if let Some(path) = &inspect.json_output {
308 global_cli_settings.ensure_write_target_outside_active_state(
309 path,
310 "write inspect JSON output",
311 )?;
312 }
313 let _ = inspect.run(state)?;
314 }
315 Commands::Approach(approach) => {
316 let _ = approach.run(state, global_cli_settings)?;
317 }
318 Commands::Bench(bench) => {
319 if let Some(path) = &bench.json_output {
320 global_cli_settings.ensure_write_target_outside_active_state(
321 path,
322 "write benchmark JSON output",
323 )?;
324 }
325 bench.run(state)?;
326 }
327 Commands::Import(s) => s.run(state, global_cli_settings)?,
328 Commands::Save(s) => s.run(
329 state,
330 run_history,
331 default_runtime_settings,
332 global_cli_settings,
333 )?,
334 Commands::Set(s) => s.run(state, global_cli_settings, default_runtime_settings)?,
335 Commands::Generate(g) => {
336 let would_compile_into_active_state = global_cli_settings.session.read_only_state
337 && global_cli_settings.global.generation.evaluator.compile
338 && matches!(
339 g.mode.as_ref(),
340 None | Some(generate::GenerateCmd::Existing(_))
341 );
342 if would_compile_into_active_state {
343 return Err(Report::msg(format!(
344 "Cannot compile generated integrands into '{}' because this session was started with --read-only-state. Disable `global.generation.evaluator.compile`, restart without --read-only-state, or save the state elsewhere first.",
345 global_cli_settings.state.folder.display()
346 )));
347 }
348
349 g.run(
350 state,
351 &global_cli_settings.state.folder,
352 global_cli_settings.override_state,
353 &global_cli_settings.global,
354 default_runtime_settings,
355 )?
356 }
357 Commands::Select(s) => {
358 s.run(state, global_cli_settings)?;
359 }
360 Commands::Integrate(g) => {
361 return Ok(CommandExecution::continue_with(CommandOutput::Integrate(
362 g.run(state, global_cli_settings)?,
363 )));
364 }
365 Commands::Evaluate(g) => {
366 return Ok(CommandExecution::continue_with(CommandOutput::Evaluate(
367 g.run(state, global_cli_settings, default_runtime_settings)?,
368 )));
369 }
370 Commands::Renormalize(r) => {
371 _ = r.run(state, global_cli_settings)?;
372 }
373 Commands::Display(l) => {
374 l.run(
375 state,
376 global_cli_settings,
377 default_runtime_settings,
378 run_history,
379 )?;
380 }
381 Commands::Duplicate(command) => {
382 command.run(state)?;
383 }
384 Commands::Run(r) => {
385 return r.run(
386 state,
387 global_cli_settings,
388 default_runtime_settings,
389 run_history,
390 );
391 }
392 Commands::StartCommandsBlock(_) | Commands::FinishCommandsBlock => {
393 return Err(Report::msg(
394 "Command block definition commands must be handled by the CLI session",
395 ));
396 }
397 Commands::Remove(r) => r.run(state)?,
398 Commands::Batch {
399 process_file: _process_file,
400 batch_input_file: _batch_input_file,
401 name: _name,
402 output_name: _output_name,
403 } => {
404 todo!("Batch command not implemented yet");
405 }
406 Commands::Shell(s) => {
407 s.run()?;
408 }
409 Commands::CommandTemplate(_) => {
410 return Err(Report::msg(
411 "Command templates can only be executed through `run` with `-D/--define` variables",
412 ));
413 }
414 }
415 Ok(CommandExecution::continue_without_output())
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use std::path::PathBuf;
422
423 use super::{Approach, Bench, Commands, Inspect, Renormalize};
424 use crate::{
425 commands::generate::{Generate, GenerateCmd, ProcessArgs},
426 state::{ProcessRef, RunHistory, State},
427 CLISettings,
428 };
429 use gammalooprs::settings::RuntimeSettings;
430
431 #[test]
432 fn select_command_parses_graph_names() {
433 let command: Commands =
434 "select -p #0 -i default --with-only-graph-names GL02 GL03 --with-graph-names GL04 GL05 --without-graph-names GL06"
435 .parse()
436 .unwrap();
437 match command {
438 Commands::Select(select) => {
439 assert_eq!(select.process, Some(ProcessRef::Id(0)));
440 assert_eq!(select.integrand_name.as_deref(), Some("default"));
441 assert_eq!(
442 select.with_graph_names,
443 vec!["GL04".to_string(), "GL05".to_string()]
444 );
445 assert_eq!(
446 select.with_only_graph_names,
447 vec!["GL02".to_string(), "GL03".to_string()]
448 );
449 assert_eq!(select.without_graph_names, vec!["GL06".to_string()]);
450 }
451 other => panic!("expected select command, got {other:?}"),
452 }
453 }
454
455 #[test]
456 fn select_command_requires_a_with_only_graph_name() {
457 let error = "select -p #0 -i default --with-only-graph-names"
458 .parse::<Commands>()
459 .unwrap_err();
460 assert!(error.to_string().contains("--with-only-graph-names"));
461 }
462
463 #[test]
464 fn select_command_parses_filter_options() {
465 let command: Commands = "select -p #0 -i default --amplitude-graphs --with-raised-propagator-signatures '[2]' --without-massive-raised-propagator-signatures '[]' --with-cycle-signatures '[(3,21)]' --without-cycle-signatures '[(ghost)]' --with-vertices '[V_6,V_9]' --without-vertices '[V_36]' --with-particles '[t,b]' '[g]' --without-particles '(e+,e-)'"
466 .parse()
467 .unwrap();
468 match command {
469 Commands::Select(select) => {
470 assert!(select.amplitude_graphs);
471 assert_eq!(
472 select.with_raised_propagator_signatures,
473 vec!["[2]".to_string()]
474 );
475 assert_eq!(
476 select.without_massive_raised_propagator_signatures,
477 vec!["[]".to_string()]
478 );
479 assert_eq!(select.with_cycle_signatures, vec!["[(3,21)]".to_string()]);
480 assert_eq!(
481 select.without_cycle_signatures,
482 vec!["[(ghost)]".to_string()]
483 );
484 assert_eq!(select.with_vertices, vec!["[V_6,V_9]".to_string()]);
485 assert_eq!(select.without_vertices, vec!["[V_36]".to_string()]);
486 assert_eq!(
487 select.with_particles,
488 vec!["[t,b]".to_string(), "[g]".to_string()]
489 );
490 assert_eq!(select.without_particles, vec!["(e+,e-)".to_string()]);
491 }
492 other => panic!("expected select command, got {other:?}"),
493 }
494 }
495
496 #[test]
497 fn select_command_parses_output_targets() {
498 let command: Commands = "select -p #0 -i default --with-graph-names GL04 --output_process selected_proc --output_integrand selected_itg --clear-existing-processes"
499 .parse()
500 .unwrap();
501 match command {
502 Commands::Select(select) => {
503 assert_eq!(select.process, Some(ProcessRef::Id(0)));
504 assert_eq!(select.integrand_name.as_deref(), Some("default"));
505 assert_eq!(select.output_process.as_deref(), Some("selected_proc"));
506 assert_eq!(select.output_integrand.as_deref(), Some("selected_itg"));
507 assert!(select.clear_existing_processes);
508 }
509 other => panic!("expected select command, got {other:?}"),
510 }
511 }
512
513 #[test]
514 fn approach_command_parses_axes_spacing_and_output() {
515 let command: Commands = "approach -p #0 -i default -x 0.5 0.25 --approach-axis 1.0,0.0 --axis-label soft --approach-axis 0.0,1.0 --axis-label threshold --n-points 3 --skip-midpoint --logarithmic --min-abs-t 1e-6 --max-abs-t 1e-2 --parameter-name lambda --n-cores 2 --output-results approach.json"
516 .parse()
517 .unwrap();
518 match command {
519 Commands::Approach(approach) => {
520 assert_eq!(approach.process, Some(ProcessRef::Id(0)));
521 assert_eq!(approach.integrand_name.as_deref(), Some("default"));
522 assert_eq!(approach.point, vec![0.5, 0.25]);
523 assert_eq!(
524 approach.approach_axes,
525 vec!["1.0,0.0".to_string(), "0.0,1.0".to_string()]
526 );
527 assert_eq!(approach.axis_labels, vec!["soft", "threshold"]);
528 assert_eq!(approach.n_points, 3);
529 assert!(approach.skip_midpoint);
530 assert!(approach.logarithmic);
531 assert!(!approach.linear);
532 assert_eq!(approach.min_abs_t, 1.0e-6);
533 assert_eq!(approach.max_abs_t, 1.0e-2);
534 assert_eq!(approach.parameter_name, "lambda");
535 assert_eq!(approach.n_cores, Some(2));
536 assert_eq!(
537 approach.output_results.as_deref(),
538 Some("approach.json".as_ref())
539 );
540 }
541 other => panic!("expected approach command, got {other:?}"),
542 }
543 }
544
545 #[test]
546 fn approach_command_parses_momentum_space_selectors() {
547 let command: Commands = "approach -p #0 -i default -x 1.0,-7.0e-2,0.0,-0.2,0.3,0.4 --approach-axis=-1.0e-3,0.0,0.0,0.0,0.0,0.0 --n-points 1 --linear --n-cores 1 --momentum-space --graph-id 2 --orientation-id 1"
548 .parse()
549 .unwrap();
550 match command {
551 Commands::Approach(approach) => {
552 assert_eq!(approach.point, vec![1.0, -7.0e-2, 0.0, -0.2, 0.3, 0.4]);
553 assert_eq!(approach.approach_axes, vec!["-1.0e-3,0.0,0.0,0.0,0.0,0.0"]);
554 assert!(approach.momentum_space);
555 assert!(approach.linear);
556 assert_eq!(approach.graph_id, Some(2));
557 assert_eq!(approach.orientation_id, Some(1));
558 assert_eq!(approach.n_cores, Some(1));
559 }
560 other => panic!("expected approach command, got {other:?}"),
561 }
562 }
563
564 #[test]
565 fn generate_rejects_compilation_into_active_state_in_read_only_mode() {
566 let mut state = State::new_test();
567 let mut run_history = RunHistory::default();
568 let mut cli_settings = CLISettings::default();
569 let mut runtime_settings = RuntimeSettings::default();
570 cli_settings.session.read_only_state = true;
571 cli_settings.global.generation.evaluator.compile = true;
572
573 let err = Commands::Generate(Generate {
574 keep_sources: false,
575 mode: Some(GenerateCmd::Existing(ProcessArgs {
576 process: None,
577 integrand_name: None,
578 })),
579 })
580 .run(
581 &mut state,
582 &mut run_history,
583 &mut cli_settings,
584 &mut runtime_settings,
585 )
586 .unwrap_err();
587
588 assert!(format!("{err:?}").contains("--read-only-state"));
589 }
590
591 #[test]
592 fn inspect_rejects_json_output_inside_read_only_state_before_integrand_lookup() {
593 let mut state = State::new_test();
594 let mut run_history = RunHistory::default();
595 let mut cli_settings = CLISettings::default();
596 let mut runtime_settings = RuntimeSettings::default();
597 cli_settings.state.folder = PathBuf::from("/tmp/read_only_state");
598 cli_settings.session.read_only_state = true;
599
600 let err = Commands::Inspect(Inspect {
601 point: vec![0.1, 0.2],
602 json_output: Some(PathBuf::from("/tmp/read_only_state/inspect.json")),
603 ..Inspect::default()
604 })
605 .run(
606 &mut state,
607 &mut run_history,
608 &mut cli_settings,
609 &mut runtime_settings,
610 )
611 .unwrap_err();
612
613 let rendered = format!("{err:?}");
614 assert!(
615 rendered.contains("Cannot write inspect JSON output"),
616 "{rendered}"
617 );
618 assert!(rendered.contains("--read-only-state"), "{rendered}");
619 }
620
621 #[test]
622 fn bench_rejects_json_output_inside_read_only_state_before_integrand_lookup() {
623 let mut state = State::new_test();
624 let mut run_history = RunHistory::default();
625 let mut cli_settings = CLISettings::default();
626 let mut runtime_settings = RuntimeSettings::default();
627 cli_settings.state.folder = PathBuf::from("/tmp/read_only_state");
628 cli_settings.session.read_only_state = true;
629
630 let err = Commands::Bench(Bench {
631 point: vec![0.1, 0.2],
632 duration: "1ms".to_string(),
633 json_output: Some(PathBuf::from("/tmp/read_only_state/bench.json")),
634 ..Bench::default()
635 })
636 .run(
637 &mut state,
638 &mut run_history,
639 &mut cli_settings,
640 &mut runtime_settings,
641 )
642 .unwrap_err();
643
644 let rendered = format!("{err:?}");
645 assert!(
646 rendered.contains("Cannot write benchmark JSON output"),
647 "{rendered}"
648 );
649 assert!(rendered.contains("--read-only-state"), "{rendered}");
650 }
651
652 #[test]
653 fn approach_rejects_output_inside_read_only_state_before_integrand_lookup() {
654 let mut state = State::new_test();
655 let mut run_history = RunHistory::default();
656 let mut cli_settings = CLISettings::default();
657 let mut runtime_settings = RuntimeSettings::default();
658 cli_settings.state.folder = PathBuf::from("/tmp/read_only_state");
659 cli_settings.session.read_only_state = true;
660
661 let err = Commands::Approach(Approach {
662 point: vec![0.1, 0.2],
663 output_results: Some(PathBuf::from("/tmp/read_only_state/approach.json")),
664 ..Approach::default()
665 })
666 .run(
667 &mut state,
668 &mut run_history,
669 &mut cli_settings,
670 &mut runtime_settings,
671 )
672 .unwrap_err();
673
674 let rendered = format!("{err:?}");
675 assert!(
676 rendered.contains("Cannot write approach results"),
677 "{rendered}"
678 );
679 assert!(rendered.contains("--read-only-state"), "{rendered}");
680 }
681
682 #[test]
683 fn renormalize_rejects_output_inside_read_only_state_before_integrand_lookup() {
684 let mut state = State::new_test();
685 let mut run_history = RunHistory::default();
686 let mut cli_settings = CLISettings::default();
687 let mut runtime_settings = RuntimeSettings::default();
688 cli_settings.state.folder = PathBuf::from("/tmp/read_only_state");
689 cli_settings.session.read_only_state = true;
690
691 let err = Commands::Renormalize(Renormalize {
692 result_path: Some(PathBuf::from("/tmp/read_only_state/renormalization")),
693 ..Renormalize::default()
694 })
695 .run(
696 &mut state,
697 &mut run_history,
698 &mut cli_settings,
699 &mut runtime_settings,
700 )
701 .unwrap_err();
702
703 let rendered = format!("{err:?}");
704 assert!(
705 rendered.contains("Cannot write renormalization results"),
706 "{rendered}"
707 );
708 assert!(rendered.contains("--read-only-state"), "{rendered}");
709 }
710}