muse2/
cli.rs

1//! The command line interface for the 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, value_name = "BOOL", num_args = 0..=1, default_missing_value = "true")]
41    pub debug_model: Option<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, None),
98            Self::Example { subcommand } => subcommand.execute(),
99            Self::Validate { model_dir } => handle_validate_command(&model_dir, None),
100            Self::SaveGraphs { model_dir, opts } => {
101                handle_save_graphs_command(&model_dir, &opts, None)
102            }
103            Self::Settings { subcommand } => subcommand.execute(),
104        }
105    }
106}
107
108/// Parse CLI arguments and start MUSE2
109pub fn run_cli() -> Result<()> {
110    let cli = Cli::parse();
111
112    // Invoked as: `$ muse2 --markdown-help`
113    if cli.markdown_help {
114        clap_markdown::print_help_markdown::<Cli>();
115        return Ok(());
116    }
117
118    if let Some(command) = cli.command {
119        command.execute()?;
120    } else {
121        // No command provided. Show help.
122        Cli::command().print_long_help()?;
123    }
124
125    Ok(())
126}
127
128/// Handle the `run` command.
129pub fn handle_run_command(
130    model_path: &Path,
131    opts: &RunOpts,
132    settings: Option<Settings>,
133) -> Result<()> {
134    // Load program settings, if not provided
135    let mut settings = if let Some(settings) = settings {
136        settings
137    } else {
138        Settings::load().context("Failed to load settings.")?
139    };
140
141    // These settings can be overridden by command-line arguments
142    if let Some(opt) = opts.debug_model {
143        settings.debug_model = opt;
144    }
145    if opts.overwrite {
146        settings.overwrite = true;
147    }
148
149    // Get path to output folder
150    let pathbuf: PathBuf;
151    let output_path = if let Some(p) = opts.output_dir.as_deref() {
152        p
153    } else {
154        pathbuf = get_output_dir(model_path)?;
155        &pathbuf
156    };
157
158    let overwrite =
159        create_output_directory(output_path, settings.overwrite).with_context(|| {
160            format!(
161                "Failed to create output directory: {}",
162                output_path.display()
163            )
164        })?;
165
166    // Initialise program logger
167    log::init(&settings.log_level, Some(output_path)).context("Failed to initialise logging.")?;
168
169    info!("Starting MUSE2 v{}", env!("CARGO_PKG_VERSION"));
170
171    // Load the model to run
172    let (model, assets) = load_model(model_path).context("Failed to load model.")?;
173    info!("Loaded model from {}", model_path.display());
174    info!("Output folder: {}", output_path.display());
175
176    // NB: We have to wait until the logger is initialised to display this warning
177    if overwrite {
178        warn!("Output folder will be overwritten");
179    }
180
181    // Run the simulation
182    crate::simulation::run(&model, assets, output_path, settings.debug_model)?;
183    info!("Simulation complete!");
184
185    Ok(())
186}
187
188/// Handle the `validate` command.
189pub fn handle_validate_command(model_path: &Path, settings: Option<Settings>) -> Result<()> {
190    // Load program settings, if not provided
191    let settings = if let Some(settings) = settings {
192        settings
193    } else {
194        Settings::load().context("Failed to load settings.")?
195    };
196
197    // Initialise program logger (we won't save log files when running the validate command)
198    log::init(&settings.log_level, None).context("Failed to initialise logging.")?;
199
200    // Load/validate the model
201    load_model(model_path).context("Failed to validate model.")?;
202    info!("Model validation successful!");
203
204    Ok(())
205}
206
207/// Handle the `save-graphs` command.
208pub fn handle_save_graphs_command(
209    model_path: &Path,
210    opts: &GraphOpts,
211    settings: Option<Settings>,
212) -> Result<()> {
213    // Load program settings, if not provided
214    let settings = if let Some(settings) = settings {
215        settings
216    } else {
217        Settings::load().context("Failed to load settings.")?
218    };
219
220    // Get path to output folder
221    let pathbuf: PathBuf;
222    let output_path = if let Some(p) = opts.output_dir.as_deref() {
223        p
224    } else {
225        pathbuf = get_graphs_dir(model_path)?;
226        &pathbuf
227    };
228
229    let overwrite =
230        create_output_directory(output_path, settings.overwrite).with_context(|| {
231            format!(
232                "Failed to create graphs directory: {}",
233                output_path.display()
234            )
235        })?;
236
237    // Initialise program logger (we won't save log files when running this command)
238    log::init(&settings.log_level, None).context("Failed to initialise logging.")?;
239
240    // NB: We have to wait until the logger is initialised to display this warning
241    if overwrite {
242        warn!("Graphs directory will be overwritten");
243    }
244
245    // Load commodity flow graphs and save to file
246    let commodity_graphs = load_commodity_graphs(model_path).context("Failed to build graphs.")?;
247    save_commodity_graphs_for_model(&commodity_graphs, output_path)?;
248    info!("Graphs saved to: {}", output_path.display());
249
250    Ok(())
251}