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 completion::CompletionArgExt,
19 state::{CommandHistory, ProcessRef, RunHistory, State},
20 CLISettings,
21};
22use symbolica::atom::Atom;
23pub mod approach;
24pub use approach::Approach;
25pub mod commands_block;
26pub use commands_block::StartCommandsBlock;
27pub mod display;
28pub use display::Display;
29pub mod duplicate;
30pub use duplicate::Duplicate;
31pub mod generate;
32pub use generate::Generate;
33pub mod import;
34pub use import::Import;
35pub mod inspect;
36pub use inspect::Inspect;
37pub mod integrate;
38pub use integrate::{Integrate, IntegrationOutput};
39pub mod remove;
40pub use remove::Remove;
41pub mod save;
42pub use save::Save;
43pub mod select;
44pub use select::Select;
45pub mod set;
46pub mod shell;
47pub use set::Set;
48pub use shell::Shell;
49pub mod run;
50pub use run::Run;
51pub mod evaluate;
52pub mod evaluate_samples;
53pub use evaluate::Evaluate;
54pub mod renormalize;
55pub use renormalize::Renormalize;
56pub mod profile;
57pub use profile::Profile;
58pub(crate) mod process_settings;
59
60#[doc(hidden)]
62#[derive(Clone, Debug, Default)]
63pub struct CliArgumentMetadata {
64 pub requires: Vec<&'static str>,
65 pub default_missing_values: Vec<&'static str>,
66}
67
68impl ArgExt for CliArgumentMetadata {}
69
70pub(crate) trait CliArgumentMetadataExt {
71 fn cli_requires(self, id: &'static str) -> Self;
72 fn cli_default_missing_value(self, value: &'static str) -> Self;
73}
74
75impl CliArgumentMetadataExt for Arg {
76 fn cli_requires(self, id: &'static str) -> Self {
77 let mut metadata = self
78 .get::<CliArgumentMetadata>()
79 .cloned()
80 .unwrap_or_default();
81 metadata.requires.push(id);
82 self.requires(id).add(metadata)
83 }
84
85 fn cli_default_missing_value(self, value: &'static str) -> Self {
86 let mut metadata = self
87 .get::<CliArgumentMetadata>()
88 .cloned()
89 .unwrap_or_default();
90 metadata.default_missing_values.push(value);
91 self.default_missing_value(value).add(metadata)
92 }
93}
94
95#[derive(Debug, Clone, Default)]
101pub enum CommandOutput {
102 #[default]
104 None,
105 Evaluate(Atom),
107 Integrate(IntegrationOutput),
110}
111
112#[derive(Debug, Clone)]
118pub struct CommandExecution {
119 pub flow: ControlFlow<SaveState>,
121 pub output: CommandOutput,
123}
124
125impl CommandExecution {
126 pub fn continue_with(output: CommandOutput) -> Self {
127 Self {
128 flow: ControlFlow::Continue(()),
129 output,
130 }
131 }
132
133 pub fn continue_without_output() -> Self {
134 Self::continue_with(CommandOutput::None)
135 }
136
137 pub fn break_with(save_state: SaveState) -> Self {
138 Self {
139 flow: ControlFlow::Break(save_state),
140 output: CommandOutput::None,
141 }
142 }
143}
144
145#[allow(clippy::large_enum_variant)]
146#[derive(Subcommand, Debug, Serialize, Deserialize, Clone, JsonSchema, PartialEq)]
147pub enum Commands {
148 #[clap(subcommand)]
150 Display(Display),
151 #[clap(subcommand)]
153 Duplicate(Duplicate),
154 #[clap(subcommand)]
156 Set(Set),
157 #[clap(subcommand)]
159 Import(Import),
160 #[clap(subcommand)]
162 Save(Save),
163
164 Run(Run),
166 #[command(name = "start_commands_block")]
168 StartCommandsBlock(StartCommandsBlock),
169 #[command(name = "finish_commands_block")]
171 FinishCommandsBlock,
172
173 #[command(name = "remove")]
175 #[clap(subcommand)]
176 Remove(Remove),
177
178 Integrate(Integrate),
180
181 Generate(Generate),
182
183 Select(Select),
185
186 Quit(SaveState),
188 Inspect(Inspect),
191
192 Approach(Approach),
194
195 Evaluate(Evaluate),
197
198 Renormalize(Renormalize),
200
201 Bench {
203 #[arg(short = 's', long, value_name = "SAMPLES")]
205 samples: usize,
206 #[arg(
208 short = 'p',
209 long = "process",
210 value_name = "PROCESS",
211 completion_process_selector(crate::completion::SelectorKind::Any)
212 )]
213 process: ProcessRef,
214
215 #[arg(
217 short = 'i',
218 long = "integrand-name",
219 value_name = "NAME",
220 completion_integrand_selector(crate::completion::SelectorKind::Any)
221 )]
222 integrand_name: String,
223 #[arg(short = 'c', long)]
225 n_cores: usize,
226 },
227 #[clap(subcommand)]
229 Profile(Profile),
230
231 Batch {
233 #[arg(value_name = "PROCESS_FILE", value_hint = clap::ValueHint::FilePath)]
235 process_file: PathBuf,
236 #[arg(value_name = "BATCH_INPUT_FILE", value_hint = clap::ValueHint::FilePath)]
238 batch_input_file: PathBuf,
239 #[arg(short = 'n', long, value_name = "NAME")]
241 name: String,
242 #[arg(value_name = "NAME")]
244 output_name: String,
245 },
246 #[command(name = "!")]
248 Shell(Shell),
249}
250
251impl FromStr for Commands {
252 type Err = Report;
253 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
254 Ok(CommandHistory::from_raw_string(s)?.command)
255 }
256}
257
258impl Commands {
259 pub fn run(
260 self,
261 state: &mut State,
262 run_history: &mut RunHistory,
263 global_cli_settings: &mut CLISettings,
264 default_runtime_settings: &mut RuntimeSettings,
265 ) -> Result<CommandExecution, Report> {
266 match self {
267 Commands::Profile(p) => {
268 p.run(state, global_cli_settings)?;
269 }
270 Commands::Quit(s) => {
271 return Ok(CommandExecution::break_with(s));
272 }
273 Commands::Inspect(inspect) => {
274 let _ = inspect.run(state)?;
275 }
276 Commands::Approach(approach) => {
277 let _ = approach.run(state, global_cli_settings)?;
278 }
279 Commands::Bench {
280 samples,
281 process,
282 integrand_name,
283 n_cores,
284 } => {
285 let process_id = process.resolve(&state.process_list)?;
286 state.bench(samples, process_id, integrand_name, n_cores)?;
287 }
288 Commands::Import(s) => s.run(state, global_cli_settings)?,
289 Commands::Save(s) => s.run(
290 state,
291 run_history,
292 default_runtime_settings,
293 global_cli_settings,
294 )?,
295 Commands::Set(s) => s.run(state, global_cli_settings, default_runtime_settings)?,
296 Commands::Generate(g) => {
297 let would_compile_into_active_state = global_cli_settings.session.read_only_state
298 && global_cli_settings.global.generation.evaluator.compile
299 && matches!(
300 g.mode.as_ref(),
301 None | Some(generate::GenerateCmd::Existing(_))
302 );
303 if would_compile_into_active_state {
304 return Err(Report::msg(format!(
305 "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.",
306 global_cli_settings.state.folder.display()
307 )));
308 }
309
310 g.run(
311 state,
312 &global_cli_settings.state.folder,
313 global_cli_settings.override_state,
314 &global_cli_settings.global,
315 default_runtime_settings,
316 )?
317 }
318 Commands::Select(s) => {
319 s.run(state, global_cli_settings)?;
320 }
321 Commands::Integrate(g) => {
322 return Ok(CommandExecution::continue_with(CommandOutput::Integrate(
323 g.run(state, global_cli_settings)?,
324 )));
325 }
326 Commands::Evaluate(g) => {
327 return Ok(CommandExecution::continue_with(CommandOutput::Evaluate(
328 g.run(state, global_cli_settings, default_runtime_settings)?,
329 )));
330 }
331 Commands::Renormalize(r) => {
332 _ = r.run(state, global_cli_settings)?;
333 }
334 Commands::Display(l) => {
335 l.run(
336 state,
337 global_cli_settings,
338 default_runtime_settings,
339 run_history,
340 )?;
341 }
342 Commands::Duplicate(command) => {
343 command.run(state)?;
344 }
345 Commands::Run(r) => {
346 return r.run(
347 state,
348 global_cli_settings,
349 default_runtime_settings,
350 run_history,
351 );
352 }
353 Commands::StartCommandsBlock(_) | Commands::FinishCommandsBlock => {
354 return Err(Report::msg(
355 "Command block definition commands must be handled by the CLI session",
356 ));
357 }
358 Commands::Remove(r) => r.run(state)?,
359 Commands::Batch {
360 process_file: _process_file,
361 batch_input_file: _batch_input_file,
362 name: _name,
363 output_name: _output_name,
364 } => {
365 todo!("Batch command not implemented yet");
366 }
367 Commands::Shell(s) => {
368 s.run()?;
369 }
370 }
371 Ok(CommandExecution::continue_without_output())
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::Commands;
378 use crate::{
379 commands::generate::{Generate, GenerateCmd, ProcessArgs},
380 state::{ProcessRef, RunHistory, State},
381 CLISettings,
382 };
383 use gammalooprs::settings::RuntimeSettings;
384
385 #[test]
386 fn select_command_parses_graph_names() {
387 let command: Commands =
388 "select -p #0 -i default --with-only-graph-names GL02 GL03 --with-graph-names GL04 GL05 --without-graph-names GL06"
389 .parse()
390 .unwrap();
391 match command {
392 Commands::Select(select) => {
393 assert_eq!(select.process, Some(ProcessRef::Id(0)));
394 assert_eq!(select.integrand_name.as_deref(), Some("default"));
395 assert_eq!(
396 select.with_graph_names,
397 vec!["GL04".to_string(), "GL05".to_string()]
398 );
399 assert_eq!(
400 select.with_only_graph_names,
401 vec!["GL02".to_string(), "GL03".to_string()]
402 );
403 assert_eq!(select.without_graph_names, vec!["GL06".to_string()]);
404 }
405 other => panic!("expected select command, got {other:?}"),
406 }
407 }
408
409 #[test]
410 fn select_command_requires_a_with_only_graph_name() {
411 let error = "select -p #0 -i default --with-only-graph-names"
412 .parse::<Commands>()
413 .unwrap_err();
414 assert!(error.to_string().contains("--with-only-graph-names"));
415 }
416
417 #[test]
418 fn select_command_parses_filter_options() {
419 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-)'"
420 .parse()
421 .unwrap();
422 match command {
423 Commands::Select(select) => {
424 assert!(select.amplitude_graphs);
425 assert_eq!(
426 select.with_raised_propagator_signatures,
427 vec!["[2]".to_string()]
428 );
429 assert_eq!(
430 select.without_massive_raised_propagator_signatures,
431 vec!["[]".to_string()]
432 );
433 assert_eq!(select.with_cycle_signatures, vec!["[(3,21)]".to_string()]);
434 assert_eq!(
435 select.without_cycle_signatures,
436 vec!["[(ghost)]".to_string()]
437 );
438 assert_eq!(select.with_vertices, vec!["[V_6,V_9]".to_string()]);
439 assert_eq!(select.without_vertices, vec!["[V_36]".to_string()]);
440 assert_eq!(
441 select.with_particles,
442 vec!["[t,b]".to_string(), "[g]".to_string()]
443 );
444 assert_eq!(select.without_particles, vec!["(e+,e-)".to_string()]);
445 }
446 other => panic!("expected select command, got {other:?}"),
447 }
448 }
449
450 #[test]
451 fn select_command_parses_output_targets() {
452 let command: Commands = "select -p #0 -i default --with-graph-names GL04 --output_process selected_proc --output_integrand selected_itg --clear-existing-processes"
453 .parse()
454 .unwrap();
455 match command {
456 Commands::Select(select) => {
457 assert_eq!(select.process, Some(ProcessRef::Id(0)));
458 assert_eq!(select.integrand_name.as_deref(), Some("default"));
459 assert_eq!(select.output_process.as_deref(), Some("selected_proc"));
460 assert_eq!(select.output_integrand.as_deref(), Some("selected_itg"));
461 assert!(select.clear_existing_processes);
462 }
463 other => panic!("expected select command, got {other:?}"),
464 }
465 }
466
467 #[test]
468 fn approach_command_parses_axes_spacing_and_output() {
469 let command: Commands = "approach -p #0 -i default -x 0.5 0.25 --approach-axis 1.0,0.0 --approach-axis 0.0,1.0 --n-points 3 --logarithmic --min-abs-t 1e-4 --n-cores 2 --output-results approach.json"
470 .parse()
471 .unwrap();
472 match command {
473 Commands::Approach(approach) => {
474 assert_eq!(approach.process, Some(ProcessRef::Id(0)));
475 assert_eq!(approach.integrand_name.as_deref(), Some("default"));
476 assert_eq!(approach.point, vec![0.5, 0.25]);
477 assert_eq!(
478 approach.approach_axes,
479 vec!["1.0,0.0".to_string(), "0.0,1.0".to_string()]
480 );
481 assert_eq!(approach.n_points, 3);
482 assert!(approach.logarithmic);
483 assert!(!approach.linear);
484 assert_eq!(approach.min_abs_t, 1.0e-4);
485 assert_eq!(approach.n_cores, Some(2));
486 assert_eq!(
487 approach.output_results.as_deref(),
488 Some("approach.json".as_ref())
489 );
490 }
491 other => panic!("expected approach command, got {other:?}"),
492 }
493 }
494
495 #[test]
496 fn approach_command_parses_momentum_space_selectors() {
497 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"
498 .parse()
499 .unwrap();
500 match command {
501 Commands::Approach(approach) => {
502 assert_eq!(approach.point, vec![1.0, -7.0e-2, 0.0, -0.2, 0.3, 0.4]);
503 assert_eq!(approach.approach_axes, vec!["-1.0e-3,0.0,0.0,0.0,0.0,0.0"]);
504 assert!(approach.momentum_space);
505 assert!(approach.linear);
506 assert_eq!(approach.graph_id, Some(2));
507 assert_eq!(approach.orientation_id, Some(1));
508 assert_eq!(approach.n_cores, Some(1));
509 }
510 other => panic!("expected approach command, got {other:?}"),
511 }
512 }
513
514 #[test]
515 fn generate_rejects_compilation_into_active_state_in_read_only_mode() {
516 let mut state = State::new_test();
517 let mut run_history = RunHistory::default();
518 let mut cli_settings = CLISettings::default();
519 let mut runtime_settings = RuntimeSettings::default();
520 cli_settings.session.read_only_state = true;
521 cli_settings.global.generation.evaluator.compile = true;
522
523 let err = Commands::Generate(Generate {
524 keep_sources: false,
525 mode: Some(GenerateCmd::Existing(ProcessArgs {
526 process: None,
527 integrand_name: None,
528 })),
529 })
530 .run(
531 &mut state,
532 &mut run_history,
533 &mut cli_settings,
534 &mut runtime_settings,
535 )
536 .unwrap_err();
537
538 assert!(format!("{err:?}").contains("--read-only-state"));
539 }
540}