Skip to main content

gammaloop_api/commands/
mod.rs

1//! Typed command definitions and command-execution results.
2//!
3//! Commands share the CLI's stateful behavior even when executed through
4//! [`crate::session::CliSession`]. Most communicate through state/settings mutations, logs, or
5//! explicitly requested files. [`CommandOutput`] is reserved for results that the embedded Rust
6//! caller should consume directly.
7
8use 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/// Export metadata for Clap settings without public introspection APIs.
61#[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/// Structured value returned by a command, when that command has an embedded result.
96///
97/// Most commands return [`None`](CommandOutput::None); this does not mean that they had no effect.
98/// They may have changed the in-memory state or settings, emitted diagnostics, or written an
99/// explicitly requested file.
100#[derive(Debug, Clone, Default)]
101pub enum CommandOutput {
102    /// No direct Rust value; inspect the mutated state, settings, logs, or requested output file.
103    #[default]
104    None,
105    /// Symbolic result produced by an [`Evaluate`](Commands::Evaluate) command.
106    Evaluate(Atom),
107    /// Estimate, uncertainty, and integration metadata produced by an
108    /// [`Integrate`](Commands::Integrate) command.
109    Integrate(IntegrationOutput),
110}
111
112/// Control-flow request and optional structured value produced by one command.
113///
114/// Callers must inspect `flow` even when they do not need `output`. A break carries the
115/// [`SaveState`] policy requested by save/quit handling; it does not persist the state merely by
116/// being dropped.
117#[derive(Debug, Clone)]
118pub struct CommandExecution {
119    /// Continue the session or return the requested save/quit policy to the embedding caller.
120    pub flow: ControlFlow<SaveState>,
121    /// Embedded result for evaluation/integration, otherwise [`CommandOutput::None`].
122    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    /// Inspect models, processes, integrands, settings, and named session data.
149    #[clap(subcommand)]
150    Display(Display),
151    /// Copy generated data to a new process or integrand name.
152    #[clap(subcommand)]
153    Duplicate(Duplicate),
154    /// Change global, runtime, model, or process settings.
155    #[clap(subcommand)]
156    Set(Set),
157    /// Import models or previously generated graphs into the active state.
158    #[clap(subcommand)]
159    Import(Import),
160    /// Export graph data, standalone evaluators, schemas, or the current state.
161    #[clap(subcommand)]
162    Save(Save),
163
164    /// Execute a stored command block or an inline command list.
165    Run(Run),
166    /// Begin recording subsequent commands under a reusable block name.
167    #[command(name = "start_commands_block")]
168    StartCommandsBlock(StartCommandsBlock),
169    /// Stop recording commands and store the active command block.
170    #[command(name = "finish_commands_block")]
171    FinishCommandsBlock,
172
173    /// Remove generated processes from the active state.
174    #[command(name = "remove")]
175    #[clap(subcommand)]
176    Remove(Remove),
177
178    /// Integrate one or more generated process integrands.
179    Integrate(Integrate),
180
181    Generate(Generate),
182
183    /// Select graph groups and write the selection to a new process or integrand.
184    Select(Select),
185
186    /// End the interactive GammaLoop session without executing further commands.
187    Quit(SaveState),
188    /// Inspect integrand contributions at one phase-space point or momentum configuration.
189    // #[clap(subcommand)]
190    Inspect(Inspect),
191
192    /// Sample an integrand while approaching a phase-space point along selected scaling axes.
193    Approach(Approach),
194
195    /// Evaluate a vacuum amplitude analytically or numerically with Vakint.
196    Evaluate(Evaluate),
197
198    /// Compute and export ultraviolet renormalization contributions for an amplitude.
199    Renormalize(Renormalize),
200
201    /// Measure raw integrand evaluation throughput over random samples and selected worker counts.
202    Bench {
203        /// Number of random samples to evaluate
204        #[arg(short = 's', long, value_name = "SAMPLES")]
205        samples: usize,
206        /// Process reference: `#<id>`, `name:<name>`, or `<id>/<name>`
207        #[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        /// The integrand name to benchmark
216        #[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        /// Number of cores to parallelize over
224        #[arg(short = 'c', long)]
225        n_cores: usize,
226    },
227    /// Profile ultraviolet or infrared scaling limits of a generated integrand.
228    #[clap(subcommand)]
229    Profile(Profile),
230
231    /// Evaluate a process-definition file against an HPC batch input and write named results.
232    Batch {
233        /// Process-definition file used to construct the batch evaluator.
234        #[arg(value_name = "PROCESS_FILE", value_hint = clap::ValueHint::FilePath)]
235        process_file: PathBuf,
236        /// Batch input file containing the evaluation points or jobs to execute.
237        #[arg(value_name = "BATCH_INPUT_FILE", value_hint = clap::ValueHint::FilePath)]
238        batch_input_file: PathBuf,
239        /// Logical batch name used in generated output metadata.
240        #[arg(short = 'n', long, value_name = "NAME")]
241        name: String,
242        /// Base name assigned to the batch result output.
243        #[arg(value_name = "NAME")]
244        output_name: String,
245    },
246    /// Run an operating-system shell command from the interactive GammaLoop session.
247    #[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}