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