muse2/
cli.rs

1//! Command-line interface for the MUSE2 simulation.
2use crate::graph::save_commodity_graphs_for_model;
3use crate::input::{load_commodity_graphs, load_model};
4use crate::log;
5use crate::output::{create_output_directory, get_graphs_dir, get_output_dir};
6use crate::settings::Settings;
7use ::log::{info, warn};
8use anyhow::{Context, Result};
9use clap::{Args, CommandFactory, Parser, Subcommand};
10use std::path::{Path, PathBuf};
11
12pub mod example;
13use example::ExampleSubcommands;
14
15pub mod settings;
16use settings::SettingsSubcommands;
17
18/// The command line interface for the simulation.
19#[derive(Parser)]
20#[command(version, about)]
21struct Cli {
22    /// The available commands.
23    #[command(subcommand)]
24    command: Option<Commands>,
25    /// Flag to provide the CLI docs as markdown
26    #[arg(long, hide = true)]
27    markdown_help: bool,
28}
29
30/// Options for the `run` command
31#[derive(Args)]
32pub struct RunOpts {
33    /// Directory for output files
34    #[arg(short, long)]
35    pub output_dir: Option<PathBuf>,
36    /// Whether to overwrite the output directory if it already exists
37    #[arg(long)]
38    pub overwrite: bool,
39    /// Whether to write additional information to CSV files
40    #[arg(long)]
41    pub debug_model: bool,
42}
43
44/// Options for the `graph` command
45#[derive(Args)]
46pub struct GraphOpts {
47    /// Directory for graph files
48    #[arg(short, long)]
49    pub output_dir: Option<PathBuf>,
50    /// Whether to overwrite the output directory if it already exists
51    #[arg(long)]
52    pub overwrite: bool,
53}
54
55/// The available commands.
56#[derive(Subcommand)]
57enum Commands {
58    /// Run a simulation model.
59    Run {
60        /// Path to the model directory.
61        model_dir: PathBuf,
62        /// Other run options
63        #[command(flatten)]
64        opts: RunOpts,
65    },
66    /// Manage example models.
67    Example {
68        /// The available subcommands for managing example models.
69        #[command(subcommand)]
70        subcommand: ExampleSubcommands,
71    },
72    /// Validate a model.
73    Validate {
74        /// The path to the model directory.
75        model_dir: PathBuf,
76    },
77    /// Build and output commodity flow graphs for a model.
78    SaveGraphs {
79        /// The path to the model directory.
80        model_dir: PathBuf,
81        /// Other options
82        #[command(flatten)]
83        opts: GraphOpts,
84    },
85    /// Manage settings file.
86    Settings {
87        /// The subcommands for managing the settings file.
88        #[command(subcommand)]
89        subcommand: SettingsSubcommands,
90    },
91}
92
93impl Commands {
94    /// Execute the supplied CLI command
95    fn execute(self) -> Result<()> {
96        match self {
97            Self::Run { model_dir, opts } => handle_run_command(&model_dir, &opts),
98            Self::Example { subcommand } => subcommand.execute(),
99            Self::Validate { model_dir } => handle_validate_command(&model_dir),
100            Self::SaveGraphs { model_dir, opts } => handle_save_graphs_command(&model_dir, &opts),
101            Self::Settings { subcommand } => subcommand.execute(),
102        }
103    }
104}
105
106/// Parse CLI arguments and run the requested command or show help.
107pub fn run_cli() -> Result<()> {
108    let cli = Cli::parse();
109
110    // Invoked as: `$ muse2 --markdown-help`
111    if cli.markdown_help {
112        clap_markdown::print_help_markdown::<Cli>();
113        return Ok(());
114    }
115
116    if let Some(command) = cli.command {
117        command.execute()?;
118    } else {
119        // No command provided. Show help.
120        Cli::command().print_long_help()?;
121    }
122
123    Ok(())
124}
125
126/// Handle the `run` command.
127pub fn handle_run_command(model_path: &Path, opts: &RunOpts) -> Result<()> {
128    let mut settings = Settings::load_or_default().context("Failed to load settings.")?;
129
130    // These settings can be overridden by command-line arguments
131    if opts.debug_model {
132        settings.debug_model = true;
133    }
134    if opts.overwrite {
135        settings.overwrite = true;
136    }
137
138    // Get path to output folder
139    let pathbuf: PathBuf;
140    let output_path = if let Some(p) = opts.output_dir.as_deref() {
141        p
142    } else {
143        pathbuf = get_output_dir(model_path, settings.results_root)?;
144        &pathbuf
145    };
146
147    let overwrite =
148        create_output_directory(output_path, settings.overwrite).with_context(|| {
149            format!(
150                "Failed to create output directory: {}",
151                output_path.display()
152            )
153        })?;
154
155    // Initialise program logger
156    log::init(&settings.log_level, Some(output_path)).context("Failed to initialise logging.")?;
157
158    info!("Starting MUSE2 v{}", env!("CARGO_PKG_VERSION"));
159
160    // Load the model to run
161    let (model, assets) = load_model(model_path).context("Failed to load model.")?;
162    info!("Loaded model from {}", model_path.display());
163    info!("Output folder: {}", output_path.display());
164
165    // NB: We have to wait until the logger is initialised to display this warning
166    if overwrite {
167        warn!("Output folder will be overwritten");
168    }
169
170    // Run the simulation
171    crate::simulation::run(&model, assets, output_path, settings.debug_model)?;
172    info!("Simulation complete!");
173
174    Ok(())
175}
176
177/// Handle the `validate` command.
178pub fn handle_validate_command(model_path: &Path) -> Result<()> {
179    let settings = Settings::load_or_default().context("Failed to load settings.")?;
180
181    // Initialise program logger (we won't save log files when running the validate command)
182    log::init(&settings.log_level, None).context("Failed to initialise logging.")?;
183
184    // Load/validate the model
185    load_model(model_path).context("Failed to validate model.")?;
186    info!("Model validation successful!");
187
188    Ok(())
189}
190
191/// Handle the `save-graphs` command.
192pub fn handle_save_graphs_command(model_path: &Path, opts: &GraphOpts) -> Result<()> {
193    let mut settings = Settings::load_or_default().context("Failed to load settings.")?;
194
195    if opts.overwrite {
196        settings.overwrite = true;
197    }
198
199    // Get path to output folder
200    let pathbuf: PathBuf;
201    let output_path = if let Some(p) = opts.output_dir.as_deref() {
202        p
203    } else {
204        pathbuf = get_graphs_dir(model_path, settings.graph_results_root)?;
205        &pathbuf
206    };
207
208    let overwrite =
209        create_output_directory(output_path, settings.overwrite).with_context(|| {
210            format!(
211                "Failed to create graphs directory: {}",
212                output_path.display()
213            )
214        })?;
215
216    // Initialise program logger (we won't save log files when running this command)
217    log::init(&settings.log_level, None).context("Failed to initialise logging.")?;
218
219    // NB: We have to wait until the logger is initialised to display this warning
220    if overwrite {
221        warn!("Graphs directory will be overwritten");
222    }
223
224    // Load commodity flow graphs and save to file
225    let commodity_graphs = load_commodity_graphs(model_path).context("Failed to build graphs.")?;
226    save_commodity_graphs_for_model(&commodity_graphs, output_path)?;
227    info!("Graphs saved to: {}", output_path.display());
228
229    Ok(())
230}