Skip to main content

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::{copy_input_files, 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    /// Whether to skip copying input files to the output folder
44    #[arg(long)]
45    pub no_copy_input_files: bool,
46}
47
48/// Options for the `graph` command
49#[derive(Args)]
50pub struct GraphOpts {
51    /// Directory for graph files
52    #[arg(short, long)]
53    pub output_dir: Option<PathBuf>,
54    /// Whether to overwrite the output directory if it already exists
55    #[arg(long)]
56    pub overwrite: bool,
57}
58
59/// The available commands.
60#[derive(Subcommand)]
61enum Commands {
62    /// Run a simulation model.
63    Run {
64        /// Path to the model directory.
65        model_dir: PathBuf,
66        /// Other run options
67        #[command(flatten)]
68        opts: RunOpts,
69    },
70    /// Manage example models.
71    Example {
72        /// The available subcommands for managing example models.
73        #[command(subcommand)]
74        subcommand: ExampleSubcommands,
75    },
76    /// Validate a model.
77    Validate {
78        /// The path to the model directory.
79        model_dir: PathBuf,
80    },
81    /// Build and output commodity flow graphs for a model.
82    SaveGraphs {
83        /// The path to the model directory.
84        model_dir: PathBuf,
85        /// Other options
86        #[command(flatten)]
87        opts: GraphOpts,
88    },
89    /// Manage settings file.
90    Settings {
91        /// The subcommands for managing the settings file.
92        #[command(subcommand)]
93        subcommand: SettingsSubcommands,
94    },
95}
96
97impl Commands {
98    /// Execute the supplied CLI command
99    fn execute(self) -> Result<()> {
100        match self {
101            Self::Run { model_dir, opts } => handle_run_command(&model_dir, &opts),
102            Self::Example { subcommand } => subcommand.execute(),
103            Self::Validate { model_dir } => handle_validate_command(&model_dir),
104            Self::SaveGraphs { model_dir, opts } => handle_save_graphs_command(&model_dir, &opts),
105            Self::Settings { subcommand } => subcommand.execute(),
106        }
107    }
108}
109
110/// Parse CLI arguments and run the requested command or show help.
111pub fn run_cli() -> Result<()> {
112    let cli = Cli::parse();
113
114    // Invoked as: `$ muse2 --markdown-help`
115    if cli.markdown_help {
116        clap_markdown::print_help_markdown::<Cli>();
117        return Ok(());
118    }
119
120    if let Some(command) = cli.command {
121        command.execute()?;
122    } else {
123        // No command provided. Show help.
124        Cli::command().print_long_help()?;
125    }
126
127    Ok(())
128}
129
130/// Handle the `run` command.
131pub fn handle_run_command(model_path: &Path, opts: &RunOpts) -> Result<()> {
132    let mut settings = Settings::load_or_default().context("Failed to load settings.")?;
133
134    // These settings can be overridden by command-line arguments
135    if opts.debug_model {
136        settings.debug_model = true;
137    }
138    if opts.overwrite {
139        settings.overwrite = true;
140    }
141    if opts.no_copy_input_files {
142        settings.copy_input_files = false;
143    }
144
145    // Get path to output folder
146    let pathbuf: PathBuf;
147    let output_path = if let Some(p) = opts.output_dir.as_deref() {
148        p
149    } else {
150        pathbuf = get_output_dir(model_path, settings.results_root)?;
151        &pathbuf
152    };
153
154    let overwrite =
155        create_output_directory(output_path, settings.overwrite).with_context(|| {
156            format!(
157                "Failed to create output directory: {}",
158                output_path.display()
159            )
160        })?;
161
162    let model_path = model_path
163        .canonicalize()
164        .context("Failed to resolve model path.")?;
165    let model_name = model_path
166        .file_name()
167        .context("Model cannot be the root directory.")?
168        .to_str()
169        .context("Invalid chars in model directory name")?;
170
171    if settings.copy_input_files {
172        copy_input_files(&model_path, output_path, model_name)
173            .context("Failed to copy input files to output directory.")?;
174    }
175    // Initialise program logger
176    log::init(&settings.log_level, Some(output_path)).context("Failed to initialise logging.")?;
177
178    info!("Starting MUSE2 v{}", env!("CARGO_PKG_VERSION"));
179
180    // Load the model to run
181    let model = load_model(&model_path).context("Failed to load model.")?;
182    info!("Loaded model from {}", model_path.display());
183    info!("Output folder: {}", output_path.display());
184
185    // NB: We have to wait until the logger is initialised to display this warning
186    if overwrite {
187        warn!("Output folder will be overwritten");
188    }
189
190    // Run the simulation
191    crate::simulation::run(&model, output_path, settings.debug_model)?;
192    info!("Simulation complete!");
193
194    Ok(())
195}
196
197/// Handle the `validate` command.
198pub fn handle_validate_command(model_path: &Path) -> Result<()> {
199    let settings = Settings::load_or_default().context("Failed to load settings.")?;
200
201    // Initialise program logger (we won't save log files when running the validate command)
202    log::init(&settings.log_level, None).context("Failed to initialise logging.")?;
203
204    // Load/validate the model
205    load_model(model_path).context("Failed to validate model.")?;
206    info!("Model validation successful!");
207
208    Ok(())
209}
210
211/// Handle the `save-graphs` command.
212pub fn handle_save_graphs_command(model_path: &Path, opts: &GraphOpts) -> Result<()> {
213    let mut settings = Settings::load_or_default().context("Failed to load settings.")?;
214
215    if opts.overwrite {
216        settings.overwrite = true;
217    }
218
219    // Get path to output folder
220    let pathbuf: PathBuf;
221    let output_path = if let Some(p) = opts.output_dir.as_deref() {
222        p
223    } else {
224        pathbuf = get_graphs_dir(model_path, settings.graph_results_root)?;
225        &pathbuf
226    };
227
228    let overwrite =
229        create_output_directory(output_path, settings.overwrite).with_context(|| {
230            format!(
231                "Failed to create graphs directory: {}",
232                output_path.display()
233            )
234        })?;
235
236    // Initialise program logger (we won't save log files when running this command)
237    log::init(&settings.log_level, None).context("Failed to initialise logging.")?;
238
239    // NB: We have to wait until the logger is initialised to display this warning
240    if overwrite {
241        warn!("Graphs directory will be overwritten");
242    }
243
244    // Load commodity flow graphs and save to file
245    let commodity_graphs = load_commodity_graphs(model_path).context("Failed to build graphs.")?;
246    save_commodity_graphs_for_model(&commodity_graphs, output_path)?;
247    info!("Graphs saved to: {}", output_path.display());
248
249    Ok(())
250}