diff --git a/pywr-cli/src/main.rs b/pywr-cli/src/main.rs index f49c3124..4b4f3642 100644 --- a/pywr-cli/src/main.rs +++ b/pywr-cli/src/main.rs @@ -1,7 +1,8 @@ mod tracing; use crate::tracing::setup_tracing; -use anyhow::{Context, Result}; +use ::tracing::info; +use anyhow::Result; use clap::{Parser, Subcommand, ValueEnum}; #[cfg(feature = "ipm-ocl")] use pywr_core::solvers::{ClIpmF32Solver, ClIpmF64Solver, ClIpmSolverSettings}; @@ -12,7 +13,6 @@ use pywr_core::solvers::{HighsSolver, HighsSolverSettings}; use pywr_core::solvers::{SimdIpmF64Solver, SimdIpmSolverSettings}; use pywr_core::test_utils::make_random_model; use pywr_schema::model::{PywrModel, PywrMultiNetworkModel}; -use pywr_schema::ConversionError; use rand::SeedableRng; use rand_chacha::ChaCha8Rng; use std::fmt::{Display, Formatter}; @@ -50,16 +50,9 @@ impl Display for Solver { #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Cli { - // /// Optional name to operate on - // name: Option, - // - // /// Sets a custom config file - // #[arg(short, long, value_name = "FILE")] - // config: Option, - // - // /// Turn debugging information on - // #[arg(short, long, action = clap::ArgAction::Count)] - // debug: u8, + /// Turn debugging information on + #[arg(long, default_value_t = false)] + debug: bool, #[command(subcommand)] command: Option, } @@ -69,6 +62,9 @@ enum Commands { Convert { /// Path to Pywr v1.x JSON. model: PathBuf, + /// Stop if there is an error converting the model. + #[arg(short, long, default_value_t = false)] + stop_on_error: bool, }, Run { @@ -87,8 +83,6 @@ enum Commands { /// The number of threads to use in parallel simulation. #[arg(short, long, default_value_t = 1)] threads: usize, - #[arg(long, default_value_t = false)] - debug: bool, }, RunMulti { /// Path to Pywr model JSON. @@ -106,8 +100,6 @@ enum Commands { /// The number of threads to use in parallel simulation. #[arg(short, long, default_value_t = 1)] threads: usize, - #[arg(long, default_value_t = false)] - debug: bool, }, RunRandom { num_systems: usize, @@ -121,10 +113,11 @@ enum Commands { fn main() -> Result<()> { let cli = Cli::parse(); + setup_tracing(cli.debug).unwrap(); match &cli.command { Some(command) => match command { - Commands::Convert { model } => convert(model)?, + Commands::Convert { model, stop_on_error } => convert(model, *stop_on_error), Commands::Run { model, solver, @@ -132,8 +125,7 @@ fn main() -> Result<()> { output_path, parallel: _, threads: _, - debug, - } => run(model, solver, data_path.as_deref(), output_path.as_deref(), *debug), + } => run(model, solver, data_path.as_deref(), output_path.as_deref()), Commands::RunMulti { model, solver, @@ -141,8 +133,7 @@ fn main() -> Result<()> { output_path, parallel: _, threads: _, - debug, - } => run_multi(model, solver, data_path.as_deref(), output_path.as_deref(), *debug), + } => run_multi(model, solver, data_path.as_deref(), output_path.as_deref()), Commands::RunRandom { num_systems, density, @@ -156,7 +147,7 @@ fn main() -> Result<()> { Ok(()) } -fn convert(path: &Path) -> Result<()> { +fn convert(path: &Path, stop_on_error: bool) { if path.is_dir() { for entry in path.read_dir().expect("read_dir call failed").flatten() { let path = entry.path(); @@ -164,22 +155,34 @@ fn convert(path: &Path) -> Result<()> { && (path.extension().unwrap() == "json") && (!path.file_stem().unwrap().to_str().unwrap().contains("_v2")) { - v1_to_v2(&path).with_context(|| format!("Could not convert model: `{:?}`", &path))?; + v1_to_v2(&path, stop_on_error); } } } else { - v1_to_v2(path).with_context(|| format!("Could not convert model: `{:?}`", path))?; + v1_to_v2(path, stop_on_error); } - - Ok(()) } -fn v1_to_v2(path: &Path) -> std::result::Result<(), ConversionError> { - println!("Model: {}", path.display()); +fn v1_to_v2(path: &Path, stop_on_error: bool) { + info!("Model: {}", path.display()); let data = std::fs::read_to_string(path).unwrap(); + // Load the v1 schema let schema: pywr_v1_schema::PywrModel = serde_json::from_str(data.as_str()).unwrap(); - let schema_v2: PywrModel = schema.try_into()?; + // Convert to v2 schema and collect any errors + let (schema_v2, errors) = PywrModel::from_v1(schema); + + if !errors.is_empty() { + info!("Model converted with {} errors:", errors.len()); + for error in errors { + info!(" {}", error); + } + if stop_on_error { + return; + } + } else { + info!("Model converted with zero errors!"); + } // There must be a better way to do this!! let mut new_file_name = path.file_stem().unwrap().to_os_string(); @@ -189,13 +192,9 @@ fn v1_to_v2(path: &Path) -> std::result::Result<(), ConversionError> { let new_file_pth = path.parent().unwrap().join(new_file_name); std::fs::write(new_file_pth, serde_json::to_string_pretty(&schema_v2).unwrap()).unwrap(); - - Ok(()) } -fn run(path: &Path, solver: &Solver, data_path: Option<&Path>, output_path: Option<&Path>, debug: bool) { - setup_tracing(debug).unwrap(); - +fn run(path: &Path, solver: &Solver, data_path: Option<&Path>, output_path: Option<&Path>) { let data = std::fs::read_to_string(path).unwrap(); let data_path = data_path.or_else(|| path.parent()); let schema_v2: PywrModel = serde_json::from_str(data.as_str()).unwrap(); @@ -216,9 +215,7 @@ fn run(path: &Path, solver: &Solver, data_path: Option<&Path>, output_path: Opti .unwrap(); } -fn run_multi(path: &Path, solver: &Solver, data_path: Option<&Path>, output_path: Option<&Path>, debug: bool) { - setup_tracing(debug).unwrap(); - +fn run_multi(path: &Path, solver: &Solver, data_path: Option<&Path>, output_path: Option<&Path>) { let data = std::fs::read_to_string(path).unwrap(); let data_path = data_path.or_else(|| path.parent()); diff --git a/pywr-core/src/parameters/py.rs b/pywr-core/src/parameters/py.rs index 9289ce39..5c9b97a1 100644 --- a/pywr-core/src/parameters/py.rs +++ b/pywr-core/src/parameters/py.rs @@ -145,7 +145,6 @@ impl PyParameter { } impl Parameter for PyParameter { - fn meta(&self) -> &ParameterMeta { &self.meta } @@ -182,7 +181,6 @@ impl Parameter for PyParameter { } impl Parameter for PyParameter { - fn meta(&self) -> &ParameterMeta { &self.meta } diff --git a/pywr-schema/src/model.rs b/pywr-schema/src/model.rs index f33b7de9..3f0850d6 100644 --- a/pywr-schema/src/model.rs +++ b/pywr-schema/src/model.rs @@ -20,6 +20,16 @@ pub struct Metadata { pub minimum_version: Option, } +impl Default for Metadata { + fn default() -> Self { + Self { + title: "Untitled model".to_string(), + description: None, + minimum_version: None, + } + } +} + impl TryFrom for Metadata { type Error = ConversionError; @@ -64,6 +74,16 @@ pub struct Timestepper { pub timestep: Timestep, } +impl Default for Timestepper { + fn default() -> Self { + Self { + start: DateType::Date(NaiveDate::from_ymd_opt(2000, 1, 1).expect("Invalid date")), + end: DateType::Date(NaiveDate::from_ymd_opt(2000, 12, 31).expect("Invalid date")), + timestep: Timestep::Days(1), + } + } +} + impl TryFrom for Timestepper { type Error = ConversionError; @@ -371,20 +391,38 @@ impl PywrModel { Ok(model) } -} - -impl TryFrom for PywrModel { - type Error = ConversionError; - fn try_from(v1: pywr_v1_schema::PywrModel) -> Result { - let metadata = v1.metadata.try_into()?; - let timestepper = v1.timestepper.try_into()?; + /// Convert a v1 model to a v2 model. + /// + /// This function is used to convert a v1 model to a v2 model. The conversion is not always + /// possible and may result in errors. The errors are returned as a vector of [`ConversionError`]s. + /// alongside the (partially) converted model. This may result in a model that will not + /// function as expected. The user should check the errors and the converted model to ensure + /// that the conversion has been successful. + pub fn from_v1(v1: pywr_v1_schema::PywrModel) -> (Self, Vec) { + let mut errors = Vec::new(); + + let metadata = v1.metadata.try_into().unwrap_or_else(|e| { + errors.push(e); + Metadata::default() + }); + + let timestepper = v1.timestepper.try_into().unwrap_or_else(|e| { + errors.push(e); + Timestepper::default() + }); let nodes = v1 .nodes .into_iter() - .map(|n| n.try_into()) - .collect::, _>>()?; + .filter_map(|n| match n.try_into() { + Ok(n) => Some(n), + Err(e) => { + errors.push(e); + None + } + }) + .collect::>(); let edges = v1.edges.into_iter().map(|e| e.into()).collect(); @@ -393,8 +431,14 @@ impl TryFrom for PywrModel { Some( v1_parameters .into_iter() - .map(|p| p.try_into_v2_parameter(None, &mut unnamed_count)) - .collect::, _>>()?, + .filter_map(|p| match p.try_into_v2_parameter(None, &mut unnamed_count) { + Ok(p) => Some(p), + Err(e) => { + errors.push(e); + None + } + }) + .collect::>(), ) } else { None @@ -413,12 +457,15 @@ impl TryFrom for PywrModel { outputs, }; - Ok(Self { - metadata, - timestepper, - scenarios: None, - network, - }) + ( + Self { + metadata, + timestepper, + scenarios: None, + network, + }, + errors, + ) } }