diff --git a/pywr-book/py-listings/model-conversion/convert.py b/pywr-book/py-listings/model-conversion/convert.py new file mode 100644 index 00000000..c730bd4b --- /dev/null +++ b/pywr-book/py-listings/model-conversion/convert.py @@ -0,0 +1,93 @@ +import json +from pathlib import Path + +# ANCHOR: convert +from pywr import ( + convert_model_from_v1_json_string, + ComponentConversionError, + ConversionError, + Schema, +) + + +def convert(v1_path: Path): + with open(v1_path) as fh: + v1_model_str = fh.read() + # 1. Convert the v1 model to a v2 schema + schema, errors = convert_model_from_v1_json_string(v1_model_str) + + schema_data = json.loads(schema.to_json_string()) + # 2. Handle any conversion errors + for error in errors: + handle_conversion_error(error, schema_data) + + # 3. Apply any other manual changes to the converted JSON. + patch_model(schema_data) + + schema_data_str = json.dumps(schema_data, indent=4) + # 4. Save the converted JSON as a new file (uncomment to save) + # with open(v1_path.parent / "v2-model.json", "w") as fh: + # fh.write(schema_data_str) + print("Conversion complete; running model...") + # 5. Load and run the new JSON file in Pywr v2.x. + schema = Schema.from_json_string(schema_data_str) + model = schema.build(Path(__file__).parent, None) + model.run("clp") + print("Model run complete 🎉") + + +# ANCHOR_END: convert +# ANCHOR: handle_conversion_error +def handle_conversion_error(error: ComponentConversionError, schema_data): + """Handle a schema conversion error. + + Raises a `RuntimeError` if an unhandled error case is found. + """ + match error: + case ComponentConversionError.Parameter(): + match error.error: + case ConversionError.UnrecognisedType() as e: + print( + f"Patching custom parameter of type {e.ty} with name {error.name}" + ) + handle_custom_parameters(schema_data, error.name, e.ty) + case _: + raise RuntimeError(f"Other parameter conversion error: {error}") + case ComponentConversionError.Node(): + raise RuntimeError(f"Failed to convert node `{error.name}`: {error.error}") + case _: + raise RuntimeError(f"Unexpected conversion error: {error}") + + +def handle_custom_parameters(schema_data, name: str, p_type: str): + """Patch the v2 schema to add the custom parameter with `name` and `p_type`.""" + + # Ensure the network parameters is a list + if schema_data["network"]["parameters"] is None: + schema_data["network"]["parameters"] = [] + + schema_data["network"]["parameters"].append( + { + "meta": {"name": name}, + "type": "Python", + "source": {"path": "v2_custom_parameter.py"}, + "object": p_type, # Use the same class name in v1 & v2 + "args": [], + "kwargs": {}, + } + ) + + +# ANCHOR_END: handle_conversion_error +# ANCHOR: patch_model +def patch_model(schema_data): + """Patch the v2 schema to add any additional changes.""" + # Add any additional patches here + schema_data["metadata"]["description"] = "Converted from v1 model" + + +# ANCHOR_END: patch_model + +if __name__ == "__main__": + pth = Path(__file__).parent / "v1-model.json" + convert(pth) diff --git a/pywr-book/py-listings/model-conversion/v1-model.json b/pywr-book/py-listings/model-conversion/v1-model.json new file mode 100644 index 00000000..3199b157 --- /dev/null +++ b/pywr-book/py-listings/model-conversion/v1-model.json @@ -0,0 +1,45 @@ +{ + "metadata": { + "title": "Python include", + "description": "An example of including a Python file to define a custom parameter.", + "minimum_version": "0.1" + }, + "timestepper": { + "start": "2015-01-01", + "end": "2015-12-31", + "timestep": 1 + }, + "nodes": [ + { + "name": "supply1", + "type": "Input", + "max_flow": "supply1_max_flow" + }, + { + "name": "link1", + "type": "Link" + }, + { + "name": "demand1", + "type": "Output", + "max_flow": 10, + "cost": -10 + } + ], + "edges": [ + [ + "supply1", + "link1" + ], + [ + "link1", + "demand1" + ] + ], + "parameters": { + "supply1_max_flow": { + "type": "MyParameter", + "value": 15 + } + } +} diff --git a/pywr-book/py-listings/model-conversion/v1_custom_parameter.py b/pywr-book/py-listings/model-conversion/v1_custom_parameter.py new file mode 100644 index 00000000..7f75a5a3 --- /dev/null +++ b/pywr-book/py-listings/model-conversion/v1_custom_parameter.py @@ -0,0 +1,9 @@ +from pywr.parameters import ConstantParameter + + +class MyParameter(ConstantParameter): + def value(self, *args, **kwargs): + return 42 + + +MyParameter.register() diff --git a/pywr-book/py-listings/model-conversion/v2_custom_parameter.py b/pywr-book/py-listings/model-conversion/v2_custom_parameter.py new file mode 100644 index 00000000..ae8e9f61 --- /dev/null +++ b/pywr-book/py-listings/model-conversion/v2_custom_parameter.py @@ -0,0 +1,3 @@ +class MyParameter: + def calc(self, *args, **kwargs): + return 42 diff --git a/pywr-book/src/getting_started.md b/pywr-book/src/getting_started.md index f3719830..2be6a821 100644 --- a/pywr-book/src/getting_started.md +++ b/pywr-book/src/getting_started.md @@ -9,25 +9,29 @@ TBC ## Python Pywr requires Python 3.9 or later. -It is currently not available on PyPI, but wheels are available from the GitHub [actions](https://github.com/pywr/pywr-next/actions) page. +It is currently not available on PyPI, but wheels are available from the +GitHub [actions](https://github.com/pywr/pywr-next/actions) page. Navigate to the latest successful build, and download the archive and extract the wheel for your platform. ```bash pip install pywr-2.0.0b0-cp312-none-win_amd64.whl ``` + > **Note**: That current Pywr v2.x is in pre-release and may not be suitable for production use. > If you require Pywr v1.x please use `pip install pywr<2`. - # Running a model Pywr is a modelling system for simulating water resources systems. Models are defined using a JSON schema, and can be run using the `pywr` command line tool. Below is an example of a simple model definition `simple1.json`: +[//]: # (@formatter:off) + ```json -{{#include ../../pywr-schema/src/test_models/simple1.json}} +{{#include ../../pywr-schema/tests/simple1.json}} ``` +[//]: # (@formatter:on) To run the model, use the `pywr` command line tool: diff --git a/pywr-book/src/migration_guide.md b/pywr-book/src/migration_guide.md index e69de29b..96f61f9f 100644 --- a/pywr-book/src/migration_guide.md +++ b/pywr-book/src/migration_guide.md @@ -0,0 +1,136 @@ +# Migrating from Pywr v1.x + +This guide is intended to help users of Pywr v1.x migrate to Pywr v2.x. Pywr v2.x is a complete rewrite of Pywr with a +new API and new features. This guide will help you update your models to this new version. + +## Overview of the process + +Pywr v2.x includes a more strict schema for defining models. This schema, along with the +[pywr-v1-schema](https://crates.io/crates/pywr-v1-schema) crate, provide a way to convert models from v1.x to v2.x. +However, this process is not perfect and will more than likely require manual intervention to complete the migration. + +The overall process will follow these steps: + +1. Convert the JSON from v1.x to v2.x using the provided conversion tool. +2. Handle any errors or warnings from the conversion tool. +3. Apply any other manual changes to the converted JSON. +4. (Optional) Save the converted JSON as a new file. +5. Load and run the new JSON file in Pywr v2.x. +6. Compare model outputs to ensure it behaves as expected. If necessary, make further changes to the above process and + repeat. + +## Converting a model + +The example below is a basic script that demonstrates how to convert a v1.x model to v2.x. This process converts +the model at runtime, and does not replace the existing v1.x model with a v2.x definition. + +> **Note**: This example is meant to be a starting point for users to build their own conversion process; +> it is not a complete generic solution. + +The function in the listing below is an example of the overall conversion process. +The function takes a path to a JSON file containing a v1 Pywr model. +The function reads the JSON, and applies the conversion function (`convert_model_from_v1_json_string`). +The conversion function that takes a JSON string and returns a tuple of the converted JSON string and a list of errors. +The function then handles these errors using the `handle_conversion_error` function. +After the errors are handled other arbitrary changes are applied using the `patch_model` function. +Finally, the converted JSON can be saved to a new file and run using Pywr v2.x. + +[//]: # (@formatter:off) + +```python,ignore +{{ #include ../py-listings/model-conversion/convert.py:convert}} +``` + +[//]: # (@formatter:on) + +### Handling conversion errors + +The `convert_model_from_v1_json_string` function returns a list of errors that occurred during the conversion process. +These errors can be handled in a variety of ways, such as modifying the model definition, raising exceptions, or +ignoring them. +It is suggested to implement a function that can handle these errors in a way that is appropriate for your use case. +Begin by matching a few types of errors and then expand the matching as needed. By raising exceptions +for unhandled errors, you can ensure that all errors are eventually accounted, and that new errors are not missed. + +The example handles the `ComponentConversionError` by matching on the error subclass (either `Parameter()` or `Node()`), +and then handling each case separately. +These two classes will contain the name of the component and optionally the attribute that caused the error. +In addition, these types contain an inner error (`ConversionError`) that can be used to provide more detailed +information. +In the example, the `UnrecognisedType()` class is handled for `Parameter()` errors by applying the +`handle_custom_parameters` function. + +This second function adds a Pywr v2.x compatible custom parameter to the model definition using the same name +and type (class name) as the original parameter. + +[//]: # (@formatter:off) + +```python,ignore +{{ #include ../py-listings/model-conversion/convert.py:handle_conversion_error}} +``` + +[//]: # (@formatter:on) + +### Other changes + +The upgrade to v2.x may require other changes to the model. +For example, the conversion process does not currently handle recorders and other model outputs. +These will need to be manually added to the model definition. +Such manual changes can be applied using, for example a `patch_model` function. +This function will make arbitrary changes to the model definition. +The example, below updates the metadata of the model to modify the description. + +[//]: # (@formatter:off) + +```python,ignore +{{ #include ../py-listings/model-conversion/convert.py:patch_model}} +``` + +[//]: # (@formatter:on) + +### Full example + +The complete example below demonstrates the conversion process for a v1.x model to v2.x: + +[//]: # (@formatter:off) + +```python,ignore +{{ #include ../py-listings/model-conversion/convert.py}} +``` + +[//]: # (@formatter:on) + +## Converting custom parameters + +The main changes to custom parameters in Pywr v2.x are as follows: + +1. Custom parameters are no longer required to be a subclass of `Parameter`. They instead can be simple Python classes + that implement a `calc` method. +2. Users are no longer required to handle scenarios within custom parameters. Instead an instance of the custom + parameter is created for each scenario in the simulation. This simplifies writing parameters and removes the risk of + accidentally contaminating state between scenarios. +3. Custom parameters are now added to the model using the "Python" parameter type. I.e. the "type" field in the + parameter definition should be set to "Python" (not the class name of the custom parameter). This parameter type + requires that the user explicitly define which metrics the custom parameter requires. + +### Simple example + +v1.x custom parameter: + +[//]: # (@formatter:off) + +```python,ignore +{{ #include ../py-listings/model-conversion/v1_custom_parameter.py}} +``` + +[//]: # (@formatter:on) + +v2.x custom parameter: + +[//]: # (@formatter:off) + +```python,ignore +{{ #include ../py-listings/model-conversion/v2_custom_parameter.py}} +``` + +[//]: # (@formatter:on) diff --git a/pywr-cli/src/main.rs b/pywr-cli/src/main.rs index a3eed48c..987ec06c 100644 --- a/pywr-cli/src/main.rs +++ b/pywr-cli/src/main.rs @@ -13,7 +13,7 @@ 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, PywrNetwork}; -use pywr_schema::ConversionError; +use pywr_schema::ComponentConversionError; use rand::SeedableRng; use rand_chacha::ChaCha8Rng; use schemars::schema_for; @@ -236,7 +236,7 @@ fn v1_to_v2(in_path: &Path, out_path: &Path, stop_on_error: bool, network_only: Ok(()) } -fn handle_conversion_errors(errors: &[ConversionError], stop_on_error: bool) -> Result<()> { +fn handle_conversion_errors(errors: &[ComponentConversionError], stop_on_error: bool) -> Result<()> { if !errors.is_empty() { info!("File converted with {} errors:", errors.len()); for error in errors { diff --git a/pywr-core/src/parameters/interpolate.rs b/pywr-core/src/parameters/interpolate.rs index 54073c57..79273672 100644 --- a/pywr-core/src/parameters/interpolate.rs +++ b/pywr-core/src/parameters/interpolate.rs @@ -58,14 +58,14 @@ pub fn linear_interpolation( } } - return if error_on_bounds { + if error_on_bounds { Err(InterpolationError::AboveUpperBounds) } else { Ok(points .last() .expect("This should be impossible because fp has been checked for a length of at least 2") .1) - }; + } } #[cfg(test)] diff --git a/pywr-core/src/state.rs b/pywr-core/src/state.rs index 88a076c7..2f99dea4 100644 --- a/pywr-core/src/state.rs +++ b/pywr-core/src/state.rs @@ -416,7 +416,7 @@ pub struct ParameterValuesRef<'a> { multi_values: &'a [MultiValue], } -impl<'a> ParameterValuesRef<'a> { +impl ParameterValuesRef<'_> { fn get_value(&self, idx: usize) -> Option<&f64> { self.values.get(idx) } @@ -435,7 +435,7 @@ pub struct SimpleParameterValues<'a> { simple: ParameterValuesRef<'a>, } -impl<'a> SimpleParameterValues<'a> { +impl SimpleParameterValues<'_> { pub fn get_simple_parameter_f64(&self, idx: SimpleParameterIndex) -> Result { self.simple .get_value(*idx.deref()) @@ -470,7 +470,7 @@ pub struct ConstParameterValues<'a> { constant: ParameterValuesRef<'a>, } -impl<'a> ConstParameterValues<'a> { +impl ConstParameterValues<'_> { pub fn get_const_parameter_f64(&self, idx: ConstParameterIndex) -> Result { self.constant .get_value(*idx.deref()) diff --git a/pywr-python/pywr/__init__.py b/pywr-python/pywr/__init__.py index 8e6b8167..3f08b89b 100644 --- a/pywr-python/pywr/__init__.py +++ b/pywr-python/pywr/__init__.py @@ -1,7 +1,7 @@ from pathlib import Path from typing import Optional -from .pywr import Schema, Model, convert_model_from_v1_json_string, convert_metric_from_v1_json_string # type: ignore +from .pywr import * # type: ignore def run_from_path( diff --git a/pywr-python/src/lib.rs b/pywr-python/src/lib.rs index 86b5ba16..d71cce32 100644 --- a/pywr-python/src/lib.rs +++ b/pywr-python/src/lib.rs @@ -16,7 +16,7 @@ use pywr_core::solvers::{ClpSolver, ClpSolverSettings, ClpSolverSettingsBuilder} #[cfg(feature = "highs")] use pywr_core::solvers::{HighsSolver, HighsSolverSettings, HighsSolverSettingsBuilder}; use pywr_schema::model::DateType; -use pywr_schema::{ConversionData, ConversionError, TryIntoV2}; +use pywr_schema::{ComponentConversionError, ConversionData, ConversionError, TryIntoV2}; use std::fmt; use std::path::PathBuf; use std::str::FromStr; @@ -112,7 +112,7 @@ fn convert_model_from_v1_json_string(py: Python, data: &str) -> PyResult>(); + let py_errors = errors.into_iter().map(|e| e.into_py(py)).collect::>(); let result = PyTuple::new_bound(py, &[py_schema.into_py(py), py_errors.into_py(py)]).into(); Ok(result) @@ -253,5 +253,9 @@ fn pywr(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; + // Error classes + m.add_class::()?; + m.add_class::()?; + Ok(()) } diff --git a/pywr-schema/Cargo.toml b/pywr-schema/Cargo.toml index 45398ba6..ab155e4c 100644 --- a/pywr-schema/Cargo.toml +++ b/pywr-schema/Cargo.toml @@ -16,7 +16,7 @@ categories = ["science", "simulation"] [dependencies] svgbobdoc = { version = "0.3.0", features = ["enable"] } polars = { workspace = true, features = ["csv", "diff", "dtype-datetime", "dtype-date", "dynamic_group_by"], optional = true } -pyo3 = { workspace = true, optional = true } +pyo3 = { workspace = true } pyo3-polars = { workspace = true, optional = true } strum = "0.26" strum_macros = "0.26" @@ -40,7 +40,7 @@ tempfile = "3.14" [features] # Core feature requires additional dependencies -core = ["dep:pywr-core", "dep:hdf5-metno", "dep:csv", "dep:polars", "dep:pyo3", "dep:pyo3-polars", "dep:ndarray", "dep:tracing"] +core = ["dep:pywr-core", "dep:hdf5-metno", "dep:csv", "dep:polars", "dep:pyo3-polars", "dep:ndarray", "dep:tracing"] default = ["core"] cbc = ["pywr-core/cbc"] highs = ["pywr-core/highs"] diff --git a/pywr-schema/src/data_tables/mod.rs b/pywr-schema/src/data_tables/mod.rs index 4bbb8e03..24c8f069 100644 --- a/pywr-schema/src/data_tables/mod.rs +++ b/pywr-schema/src/data_tables/mod.rs @@ -288,11 +288,19 @@ impl TryFrom for TableDataRef { fn try_from(v1: TableDataRefV1) -> Result { let column = match v1.column { None => None, - Some(c) => Some(c.try_into()?), + Some(c) => Some(c.try_into().map_err(|e| ConversionError::TableRef { + attr: "column".to_string(), + name: v1.table.clone(), + error: e, + })?), }; let index = match v1.index { None => None, - Some(i) => Some(i.try_into()?), + Some(i) => Some(i.try_into().map_err(|e| ConversionError::TableRef { + attr: "index".to_string(), + name: v1.table.clone(), + error: e, + })?), }; Ok(Self { table: v1.table, diff --git a/pywr-schema/src/error.rs b/pywr-schema/src/error.rs index a52a3130..6ec0c880 100644 --- a/pywr-schema/src/error.rs +++ b/pywr-schema/src/error.rs @@ -1,12 +1,14 @@ use crate::data_tables::{DataTable, TableDataRef, TableError}; use crate::nodes::NodeAttribute; use crate::timeseries::TimeseriesError; +use pyo3::prelude::*; +use std::path::PathBuf; use thiserror::Error; #[derive(Error, Debug)] pub enum SchemaError { - #[error("IO error: {0}")] - IO(String), + #[error("IO error on path `{path}`: {error}")] + IO { path: PathBuf, error: std::io::Error }, #[error("JSON error: {0}")] Json(#[from] serde_json::Error), #[error("node with name {0} not found")] @@ -70,57 +72,66 @@ pub enum SchemaError { } #[cfg(feature = "core")] -impl From for pyo3::PyErr { - fn from(err: SchemaError) -> pyo3::PyErr { +impl From for PyErr { + fn from(err: SchemaError) -> PyErr { pyo3::exceptions::PyRuntimeError::new_err(err.to_string()) } } -#[derive(Error, Debug, PartialEq, Eq)] -pub enum ConversionError { - #[error("Error converting {attr:?} on node {name:?}")] - NodeAttribute { +#[derive(Error, Debug, PartialEq, Eq, Clone)] +#[pyclass] +pub enum ComponentConversionError { + #[error("Failed to convert `{attr}` on node `{name}`: {error}")] + Node { + attr: String, + name: String, + error: ConversionError, + }, + #[error("Failed to convert `{attr}` on parameter `{name}`: {error}")] + Parameter { attr: String, name: String, - source: Box, + error: ConversionError, }, +} + +#[derive(Error, Debug, PartialEq, Eq, Clone)] +#[pyclass] +pub enum ConversionError { #[error("Constant float value cannot be a parameter reference.")] - ConstantFloatReferencesParameter, + ConstantFloatReferencesParameter {}, #[error("Constant float value cannot be an inline parameter.")] - ConstantFloatInlineParameter, - #[error("Missing one of the following attributes {attrs:?} on parameter {name:?}.")] - MissingAttribute { attrs: Vec, name: String }, - #[error("Unexpected the following attributes {attrs:?} on parameter {name:?}.")] - UnexpectedAttribute { attrs: Vec, name: String }, + ConstantFloatInlineParameter {}, + #[error("Missing one of the following attributes {attrs:?}.")] + MissingAttribute { attrs: Vec }, + #[error("The following attributes are unexpected {attrs:?}.")] + UnexpectedAttribute { attrs: Vec }, + #[error("The following attributes are defined {attrs:?}. Only 1 is allowed.")] + AmbiguousAttributes { attrs: Vec }, #[error("Can not convert a float constant to an index constant.")] - FloatToIndex, - #[error("Attribute {attr:?} on node {name:?} is not allowed .")] - ExtraNodeAttribute { attr: String, name: String }, - #[error("Custom node of type {ty:?} on node {name:?} is not supported .")] - CustomNodeNotSupported { ty: String, name: String }, + FloatToIndex {}, + #[error("Attribute {attr:?} on is not allowed .")] + ExtraAttribute { attr: String }, + #[error("Custom node of type {ty:?} is not supported .")] + CustomTypeNotSupported { ty: String }, #[error("Integer table indices are not supported.")] - IntegerTableIndicesNotSupported, - #[error("Conversion of one of the following attributes {attrs:?} is not supported on parameter {name:?}.")] - UnsupportedAttribute { attrs: Vec, name: String }, - #[error("Conversion of one of the following feature is not supported on parameter {name:?}: {feature}")] - UnsupportedFeature { feature: String, name: String }, - #[error("Parameter {name:?} of type `{ty:?}` are not supported in Pywr v2. {instead:?}")] - DeprecatedParameter { ty: String, name: String, instead: String }, - #[error("Unexpected type for attribute {attr} on parameter {name}. Expected `{expected}`, found `{actual}`")] - UnexpectedType { - attr: String, - name: String, - expected: String, - actual: String, - }, - #[error("'{0}' could not be parsed into a NaiveDate")] - UnparseableDate(String), - #[error("Chrono out of range error: {0}")] - OutOfRange(#[from] chrono::OutOfRange), - #[error("The dataframe parameters '{0}' defines both a column and a scenario attribute. Only 1 is allowed.")] - AmbiguousColumnAndScenario(String), - #[error("The dataframe parameters '{0}' defines both a column and a scenario. Only 1 is allowed.")] - MissingColumnOrScenario(String), - #[error("Unable to create a timeseries for file: '{0}'. No name was found.")] - MissingTimeseriesName(String), + IntegerTableIndicesNotSupported {}, + #[error("Conversion of one of the following attributes {attrs:?} is not supported.")] + UnsupportedAttribute { attrs: Vec }, + #[error("Conversion of the following feature is not supported: {feature}")] + UnsupportedFeature { feature: String }, + #[error("Type `{ty:?}` are not supported in Pywr v2. {instead:?}")] + DeprecatedParameter { ty: String, instead: String }, + #[error("Expected `{expected}`, found `{actual}`")] + UnexpectedType { expected: String, actual: String }, + #[error("'{date}' could not be parsed into a NaiveDate")] + UnparseableDate { date: String }, + #[error("The dataframe parameter `{parameter}` defines both a column and a scenario. Only 1 is allowed.")] + MissingColumnOrScenario { parameter: String }, + #[error("Unable to create a timeseries for file: `{filename}`. No name was found.")] + MissingTimeseriesName { filename: PathBuf }, + #[error("Failed to convert `{attr}` on table `{name}`: {error}")] + TableRef { attr: String, name: String, error: String }, + #[error("Unrecognised type: {ty}")] + UnrecognisedType { ty: String }, } diff --git a/pywr-schema/src/lib.rs b/pywr-schema/src/lib.rs index 40ea44b8..d265ad8a 100644 --- a/pywr-schema/src/lib.rs +++ b/pywr-schema/src/lib.rs @@ -17,7 +17,7 @@ pub mod timeseries; mod v1; mod visit; -pub use error::{ConversionError, SchemaError}; +pub use error::{ComponentConversionError, ConversionError, SchemaError}; pub use model::PywrModel; pub use v1::{ConversionData, TryFromV1, TryIntoV2}; pub use visit::{VisitMetrics, VisitPaths}; diff --git a/pywr-schema/src/metric.rs b/pywr-schema/src/metric.rs index 167279f1..6253109a 100644 --- a/pywr-schema/src/metric.rs +++ b/pywr-schema/src/metric.rs @@ -1,5 +1,6 @@ use crate::data_tables::TableDataRef; use crate::edge::Edge; +use crate::error::ComponentConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; #[cfg(feature = "core")] @@ -200,7 +201,13 @@ impl TryFromV1 for Metric { // Inline parameters are converted to either a parameter or a timeseries // The actual component is extracted into the conversion data leaving a reference // to the component in the metric. - let definition: ParameterOrTimeseriesRef = (*param).try_into_v2(parent_node, conversion_data)?; + let definition: ParameterOrTimeseriesRef = + (*param) + .try_into_v2(parent_node, conversion_data) + .map_err(|e| match e { + ComponentConversionError::Parameter { error, .. } => error, + ComponentConversionError::Node { error, .. } => error, + })?; match definition { ParameterOrTimeseriesRef::Parameter(p) => { let reference = ParameterReference { diff --git a/pywr-schema/src/model.rs b/pywr-schema/src/model.rs index ea6b41af..f43eb1c1 100644 --- a/pywr-schema/src/model.rs +++ b/pywr-schema/src/model.rs @@ -4,7 +4,7 @@ use super::parameters::{Parameter, ParameterOrTimeseriesRef}; use crate::data_tables::DataTable; #[cfg(feature = "core")] use crate::data_tables::LoadedTableCollection; -use crate::error::{ConversionError, SchemaError}; +use crate::error::{ComponentConversionError, SchemaError}; use crate::metric::Metric; use crate::metric_sets::MetricSet; use crate::outputs::Output; @@ -16,6 +16,7 @@ use crate::visit::{VisitMetrics, VisitPaths}; #[cfg(feature = "core")] use chrono::NaiveTime; use chrono::{NaiveDate, NaiveDateTime}; +use pyo3::pyclass; #[cfg(feature = "core")] use pywr_core::{models::ModelDomain, timestep::TimestepDuration, PywrError}; use schemars::JsonSchema; @@ -39,17 +40,15 @@ impl Default for Metadata { } } -impl TryFrom for Metadata { - type Error = ConversionError; - - fn try_from(v1: pywr_v1_schema::model::Metadata) -> Result { - Ok(Self { +impl From for Metadata { + fn from(v1: pywr_v1_schema::model::Metadata) -> Self { + Self { title: v1 .title .unwrap_or("Model converted from Pywr v1.x with no title.".to_string()), description: v1.description, minimum_version: v1.minimum_version, - }) + } } } @@ -153,6 +152,7 @@ pub struct LoadArgs<'a> { } #[derive(serde::Deserialize, serde::Serialize, Clone, Default, JsonSchema)] +#[pyclass] pub struct PywrNetwork { pub nodes: Vec, pub edges: Vec, @@ -252,18 +252,21 @@ impl VisitMetrics for PywrNetwork { impl PywrNetwork { pub fn from_path>(path: P) -> Result { - let data = std::fs::read_to_string(path).map_err(|e| SchemaError::IO(e.to_string()))?; + let data = std::fs::read_to_string(&path).map_err(|error| SchemaError::IO { + path: path.as_ref().to_path_buf(), + error, + })?; Ok(serde_json::from_str(data.as_str())?) } /// Convert a v1 network to a v2 network. /// /// 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. + /// possible and may result in errors. The errors are returned as a vector of [`ComponentConversionError`]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::PywrNetwork) -> (Self, Vec) { + pub fn from_v1(v1: pywr_v1_schema::PywrNetwork) -> (Self, Vec) { let mut errors = Vec::new(); // We will use this to store any timeseries or parameters that are extracted from the v1 nodes let mut conversion_data = ConversionData::default(); @@ -593,7 +596,10 @@ impl PywrModel { } pub fn from_path>(path: P) -> Result { - let data = std::fs::read_to_string(path).map_err(|e| SchemaError::IO(e.to_string()))?; + let data = std::fs::read_to_string(&path).map_err(|error| SchemaError::IO { + path: path.as_ref().to_path_buf(), + error, + })?; Ok(serde_json::from_str(data.as_str())?) } @@ -630,18 +636,14 @@ impl PywrModel { /// 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. + /// possible and may result in errors. The errors are returned as a vector of [`ComponentConversionError`]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) { + 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 metadata = v1.metadata.into(); let timestepper = v1.timestepper.into(); let (network, network_errors) = PywrNetwork::from_v1(v1.network); @@ -661,7 +663,7 @@ impl PywrModel { /// Convert a v1 JSON string to a v2 model. /// /// See [`PywrModel::from_v1`] for more information. - pub fn from_v1_str(v1: &str) -> Result<(Self, Vec), pywr_v1_schema::PywrSchemaError> { + pub fn from_v1_str(v1: &str) -> Result<(Self, Vec), pywr_v1_schema::PywrSchemaError> { let v1_model: pywr_v1_schema::PywrModel = serde_json::from_str(v1)?; Ok(Self::from_v1(v1_model)) @@ -762,7 +764,10 @@ impl FromStr for PywrMultiNetworkModel { impl PywrMultiNetworkModel { pub fn from_path>(path: P) -> Result { - let data = std::fs::read_to_string(path).map_err(|e| SchemaError::IO(e.to_string()))?; + let data = std::fs::read_to_string(&path).map_err(|error| SchemaError::IO { + path: path.as_ref().to_path_buf(), + error, + })?; Ok(serde_json::from_str(data.as_str())?) } diff --git a/pywr-schema/src/nodes/annual_virtual_storage.rs b/pywr-schema/src/nodes/annual_virtual_storage.rs index 6bbe48d5..66556d4f 100644 --- a/pywr-schema/src/nodes/annual_virtual_storage.rs +++ b/pywr-schema/src/nodes/annual_virtual_storage.rs @@ -1,4 +1,4 @@ -use crate::error::ConversionError; +use crate::error::ComponentConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; use crate::metric::{Metric, SimpleNodeReference}; @@ -7,7 +7,7 @@ use crate::model::LoadArgs; use crate::nodes::core::StorageInitialVolume; use crate::nodes::{NodeAttribute, NodeMeta}; use crate::parameters::Parameter; -use crate::v1::{ConversionData, TryFromV1, TryIntoV2}; +use crate::v1::{try_convert_initial_storage, try_convert_node_attr, ConversionData, TryFromV1}; #[cfg(feature = "core")] use pywr_core::{ derived_metric::DerivedMetric, @@ -160,7 +160,7 @@ impl AnnualVirtualStorageNode { } impl TryFromV1 for AnnualVirtualStorageNode { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: AnnualVirtualStorageNodeV1, @@ -169,30 +169,12 @@ impl TryFromV1 for AnnualVirtualStorageNode { ) -> Result { let meta: NodeMeta = v1.meta.into(); - let cost = v1 - .cost - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - let max_volume = v1 - .max_volume - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - - let min_volume = v1 - .min_volume - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - - let initial_volume = if let Some(v) = v1.initial_volume { - StorageInitialVolume::Absolute(v) - } else if let Some(v) = v1.initial_volume_pc { - StorageInitialVolume::Proportional(v) - } else { - return Err(ConversionError::MissingAttribute { - name: meta.name, - attrs: vec!["initial_volume".to_string(), "initial_volume_pc".to_string()], - }); - }; + let cost = try_convert_node_attr(&meta.name, "cost", v1.cost, parent_node, conversion_data)?; + let max_volume = try_convert_node_attr(&meta.name, "max_volume", v1.max_volume, parent_node, conversion_data)?; + let min_volume = try_convert_node_attr(&meta.name, "min_volume", v1.min_volume, parent_node, conversion_data)?; + + let initial_volume = + try_convert_initial_storage(&meta.name, "initial_volume", v1.initial_volume, v1.initial_volume_pc)?; let nodes = v1.nodes.into_iter().map(|n| n.into()).collect(); diff --git a/pywr-schema/src/nodes/core.rs b/pywr-schema/src/nodes/core.rs index 15b53d7a..df3251ca 100644 --- a/pywr-schema/src/nodes/core.rs +++ b/pywr-schema/src/nodes/core.rs @@ -1,4 +1,4 @@ -use crate::error::ConversionError; +use crate::error::ComponentConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; use crate::metric::{Metric, SimpleNodeReference}; @@ -6,7 +6,9 @@ use crate::metric::{Metric, SimpleNodeReference}; use crate::model::LoadArgs; use crate::nodes::{NodeAttribute, NodeMeta}; use crate::parameters::Parameter; -use crate::v1::{ConversionData, TryFromV1, TryIntoV2}; +use crate::v1::{ + try_convert_initial_storage, try_convert_node_attr, try_convert_parameter_attr, ConversionData, TryFromV1, +}; #[cfg(feature = "core")] use pywr_core::{ derived_metric::DerivedMetric, metric::MetricF64, node::StorageInitialVolume as CoreStorageInitialVolume, @@ -108,7 +110,7 @@ impl InputNode { } impl TryFromV1 for InputNode { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: InputNodeV1, @@ -117,19 +119,9 @@ impl TryFromV1 for InputNode { ) -> Result { let meta: NodeMeta = v1.meta.into(); - let max_flow = v1 - .max_flow - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - - let min_flow = v1 - .min_flow - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - let cost = v1 - .cost - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; + let cost = try_convert_node_attr(&meta.name, "cost", v1.cost, parent_node, conversion_data)?; + let max_flow = try_convert_node_attr(&meta.name, "max_flow", v1.max_flow, parent_node, conversion_data)?; + let min_flow = try_convert_node_attr(&meta.name, "min_flow", v1.min_flow, parent_node, conversion_data)?; let n = Self { meta, @@ -562,7 +554,7 @@ impl LinkNode { } impl TryFromV1 for LinkNode { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: LinkNodeV1, @@ -571,18 +563,9 @@ impl TryFromV1 for LinkNode { ) -> Result { let meta: NodeMeta = v1.meta.into(); - let max_flow = v1 - .max_flow - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - let min_flow = v1 - .min_flow - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - let cost = v1 - .cost - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; + let cost = try_convert_node_attr(&meta.name, "cost", v1.cost, parent_node, conversion_data)?; + let max_flow = try_convert_node_attr(&meta.name, "max_flow", v1.max_flow, parent_node, conversion_data)?; + let min_flow = try_convert_node_attr(&meta.name, "min_flow", v1.min_flow, parent_node, conversion_data)?; // not supported in V1 let soft_min = None; let soft_max = None; @@ -695,7 +678,7 @@ impl OutputNode { } impl TryFromV1 for OutputNode { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: OutputNodeV1, @@ -704,18 +687,9 @@ impl TryFromV1 for OutputNode { ) -> Result { let meta: NodeMeta = v1.meta.into(); - let max_flow = v1 - .max_flow - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - let min_flow = v1 - .min_flow - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - let cost = v1 - .cost - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; + let cost = try_convert_node_attr(&meta.name, "cost", v1.cost, parent_node, conversion_data)?; + let max_flow = try_convert_node_attr(&meta.name, "max_flow", v1.max_flow, parent_node, conversion_data)?; + let min_flow = try_convert_node_attr(&meta.name, "min_flow", v1.min_flow, parent_node, conversion_data)?; let n = Self { meta, @@ -849,7 +823,7 @@ impl StorageNode { } impl TryFromV1 for StorageNode { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: StorageNodeV1, @@ -858,46 +832,12 @@ impl TryFromV1 for StorageNode { ) -> Result { let meta: NodeMeta = v1.meta.into(); - let cost = v1 - .cost - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose() - .map_err(|source| ConversionError::NodeAttribute { - attr: "cost".to_string(), - name: meta.name.clone(), - source: Box::new(source), - })?; - - let max_volume = v1 - .max_volume - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose() - .map_err(|source| ConversionError::NodeAttribute { - attr: "max_volume".to_string(), - name: meta.name.clone(), - source: Box::new(source), - })?; - - let min_volume = v1 - .min_volume - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose() - .map_err(|source| ConversionError::NodeAttribute { - attr: "min_volume".to_string(), - name: meta.name.clone(), - source: Box::new(source), - })?; - - let initial_volume = if let Some(v) = v1.initial_volume { - StorageInitialVolume::Absolute(v) - } else if let Some(v) = v1.initial_volume_pc { - StorageInitialVolume::Proportional(v) - } else { - return Err(ConversionError::MissingAttribute { - name: meta.name, - attrs: vec!["initial_volume".to_string(), "initial_volume_pc".to_string()], - }); - }; + let cost = try_convert_node_attr(&meta.name, "cost", v1.cost, parent_node, conversion_data)?; + let max_volume = try_convert_node_attr(&meta.name, "max_volume", v1.max_volume, parent_node, conversion_data)?; + let min_volume = try_convert_node_attr(&meta.name, "min_volume", v1.min_volume, parent_node, conversion_data)?; + + let initial_volume = + try_convert_initial_storage(&meta.name, "initial_volume", v1.initial_volume, v1.initial_volume_pc)?; let n = Self { meta, @@ -912,7 +852,7 @@ impl TryFromV1 for StorageNode { } impl TryFromV1 for StorageNode { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: ReservoirNodeV1, @@ -921,28 +861,12 @@ impl TryFromV1 for StorageNode { ) -> Result { let meta: NodeMeta = v1.meta.into(); - let cost = v1 - .cost - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - - let max_volume = v1 - .max_volume - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - - let min_volume = v1 - .min_volume - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - - let initial_volume = if let Some(v) = v1.initial_volume { - StorageInitialVolume::Absolute(v) - } else if let Some(v) = v1.initial_volume_pc { - StorageInitialVolume::Proportional(v) - } else { - StorageInitialVolume::default() - }; + let cost = try_convert_node_attr(&meta.name, "cost", v1.cost, parent_node, conversion_data)?; + let max_volume = try_convert_node_attr(&meta.name, "max_volume", v1.max_volume, parent_node, conversion_data)?; + let min_volume = try_convert_node_attr(&meta.name, "min_volume", v1.min_volume, parent_node, conversion_data)?; + + let initial_volume = + try_convert_initial_storage(&meta.name, "initial_volume", v1.initial_volume, v1.initial_volume_pc)?; let n = Self { meta, @@ -1053,7 +977,7 @@ impl CatchmentNode { } impl TryFromV1 for CatchmentNode { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: CatchmentNodeV1, @@ -1062,14 +986,8 @@ impl TryFromV1 for CatchmentNode { ) -> Result { let meta: NodeMeta = v1.meta.into(); - let flow = v1 - .flow - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - let cost = v1 - .cost - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; + let cost = try_convert_node_attr(&meta.name, "cost", v1.cost, parent_node, conversion_data)?; + let flow = try_convert_node_attr(&meta.name, "min_flow", v1.flow, parent_node, conversion_data)?; let n = Self { meta, @@ -1240,7 +1158,7 @@ impl AggregatedNode { } impl TryFromV1 for AggregatedNode { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: AggregatedNodeV1, @@ -1253,21 +1171,22 @@ impl TryFromV1 for AggregatedNode { Some(f) => Some(Relationship::Ratio { factors: f .into_iter() - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) + .map(|v| { + try_convert_parameter_attr( + &meta.name, + "factors", + v, + parent_node.or(Some(&meta.name)), + conversion_data, + ) + }) .collect::>()?, }), None => None, }; - let max_flow = v1 - .max_flow - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - - let min_flow = v1 - .min_flow - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; + let max_flow = try_convert_node_attr(&meta.name, "max_flow", v1.max_flow, parent_node, conversion_data)?; + let min_flow = try_convert_node_attr(&meta.name, "min_flow", v1.min_flow, parent_node, conversion_data)?; let nodes = v1.nodes.into_iter().map(|n| n.into()).collect(); diff --git a/pywr-schema/src/nodes/delay.rs b/pywr-schema/src/nodes/delay.rs index 3abccdf0..5ad8e12f 100644 --- a/pywr-schema/src/nodes/delay.rs +++ b/pywr-schema/src/nodes/delay.rs @@ -1,6 +1,6 @@ -use crate::error::ConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; +use crate::error::{ComponentConversionError, ConversionError}; #[cfg(feature = "core")] use crate::model::LoadArgs; use crate::nodes::{NodeAttribute, NodeMeta}; @@ -134,7 +134,7 @@ impl DelayNode { } impl TryFrom for DelayNode { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from(v1: DelayNodeV1) -> Result { let meta: NodeMeta = v1.meta.into(); @@ -145,9 +145,12 @@ impl TryFrom for DelayNode { None => match v1.timesteps { Some(ts) => ts, None => { - return Err(ConversionError::MissingAttribute { + return Err(ComponentConversionError::Node { name: meta.name, - attrs: vec!["days".to_string(), "timesteps".to_string()], + attr: "delay".to_string(), + error: ConversionError::MissingAttribute { + attrs: vec!["days".to_string(), "timesteps".to_string()], + }, }) } }, diff --git a/pywr-schema/src/nodes/loss_link.rs b/pywr-schema/src/nodes/loss_link.rs index 7545a77b..a258e40b 100644 --- a/pywr-schema/src/nodes/loss_link.rs +++ b/pywr-schema/src/nodes/loss_link.rs @@ -1,4 +1,4 @@ -use crate::error::ConversionError; +use crate::error::ComponentConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; use crate::metric::Metric; @@ -6,7 +6,7 @@ use crate::metric::Metric; use crate::model::LoadArgs; use crate::nodes::{NodeAttribute, NodeMeta}; use crate::parameters::Parameter; -use crate::v1::{ConversionData, TryFromV1, TryIntoV2}; +use crate::v1::{try_convert_node_attr, ConversionData, TryFromV1}; #[cfg(feature = "core")] use pywr_core::{aggregated_node::Relationship, metric::MetricF64}; use pywr_schema_macros::PywrVisitAll; @@ -242,7 +242,7 @@ impl LossLinkNode { } impl TryFromV1 for LossLinkNode { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: LossLinkNodeV1, @@ -251,28 +251,13 @@ impl TryFromV1 for LossLinkNode { ) -> Result { let meta: NodeMeta = v1.meta.into(); - let loss_factor = v1 - .loss_factor - .map(|v| { - let factor = v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)?; - Ok::<_, Self::Error>(LossFactor::Net { factor }) - }) - .transpose()?; + let loss_factor: Option = + try_convert_node_attr(&meta.name, "loss_factor", v1.loss_factor, parent_node, conversion_data)?; + let loss_factor = loss_factor.map(|factor| LossFactor::Net { factor }); - let min_net_flow = v1 - .min_flow - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - - let max_net_flow = v1 - .max_flow - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - - let net_cost = v1 - .cost - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; + let net_cost = try_convert_node_attr(&meta.name, "cost", v1.cost, parent_node, conversion_data)?; + let max_net_flow = try_convert_node_attr(&meta.name, "max_flow", v1.max_flow, parent_node, conversion_data)?; + let min_net_flow = try_convert_node_attr(&meta.name, "min_flow", v1.min_flow, parent_node, conversion_data)?; let n = Self { meta, diff --git a/pywr-schema/src/nodes/mod.rs b/pywr-schema/src/nodes/mod.rs index 7f0cbe98..b5018c44 100644 --- a/pywr-schema/src/nodes/mod.rs +++ b/pywr-schema/src/nodes/mod.rs @@ -13,9 +13,9 @@ mod turbine; mod virtual_storage; mod water_treatment_works; -use crate::error::ConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; +use crate::error::{ComponentConversionError, ConversionError}; use crate::metric::Metric; #[cfg(feature = "core")] use crate::model::LoadArgs; @@ -530,7 +530,7 @@ impl Node { } impl TryFromV1 for Node { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: NodeV1, @@ -542,16 +542,17 @@ impl TryFromV1 for Node { let nv2: Node = n.try_into_v2(parent_node, conversion_data)?; Ok(nv2) } - NodeV1::Custom(n) => Err(ConversionError::CustomNodeNotSupported { + NodeV1::Custom(n) => Err(ComponentConversionError::Node { name: n.meta.name, - ty: n.ty, + attr: "".to_string(), + error: ConversionError::CustomTypeNotSupported { ty: n.ty }, }), } } } impl TryFromV1> for Node { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: Box, diff --git a/pywr-schema/src/nodes/monthly_virtual_storage.rs b/pywr-schema/src/nodes/monthly_virtual_storage.rs index 9b7c913c..5d00b01d 100644 --- a/pywr-schema/src/nodes/monthly_virtual_storage.rs +++ b/pywr-schema/src/nodes/monthly_virtual_storage.rs @@ -1,4 +1,4 @@ -use crate::error::ConversionError; +use crate::error::ComponentConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; use crate::metric::{Metric, SimpleNodeReference}; @@ -7,7 +7,7 @@ use crate::model::LoadArgs; use crate::nodes::core::StorageInitialVolume; use crate::nodes::{NodeAttribute, NodeMeta}; use crate::parameters::Parameter; -use crate::v1::{ConversionData, TryFromV1, TryIntoV2}; +use crate::v1::{try_convert_initial_storage, try_convert_node_attr, ConversionData, TryFromV1}; #[cfg(feature = "core")] use pywr_core::{ derived_metric::DerivedMetric, @@ -151,7 +151,7 @@ impl MonthlyVirtualStorageNode { } impl TryFromV1 for MonthlyVirtualStorageNode { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: MonthlyVirtualStorageNodeV1, @@ -160,30 +160,12 @@ impl TryFromV1 for MonthlyVirtualStorageNode { ) -> Result { let meta: NodeMeta = v1.meta.into(); - let cost = v1 - .cost - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - let max_volume = v1 - .max_volume - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - - let min_volume = v1 - .min_volume - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - - let initial_volume = if let Some(v) = v1.initial_volume { - StorageInitialVolume::Absolute(v) - } else if let Some(v) = v1.initial_volume_pc { - StorageInitialVolume::Proportional(v) - } else { - return Err(ConversionError::MissingAttribute { - name: meta.name, - attrs: vec!["initial_volume".to_string(), "initial_volume_pc".to_string()], - }); - }; + let cost = try_convert_node_attr(&meta.name, "cost", v1.cost, parent_node, conversion_data)?; + let max_volume = try_convert_node_attr(&meta.name, "max_volume", v1.max_volume, parent_node, conversion_data)?; + let min_volume = try_convert_node_attr(&meta.name, "min_volume", v1.min_volume, parent_node, conversion_data)?; + + let initial_volume = + try_convert_initial_storage(&meta.name, "initial_volume", v1.initial_volume, v1.initial_volume_pc)?; let nodes = v1.nodes.into_iter().map(|n| n.into()).collect(); diff --git a/pywr-schema/src/nodes/piecewise_link.rs b/pywr-schema/src/nodes/piecewise_link.rs index 6624c4aa..c9735afa 100644 --- a/pywr-schema/src/nodes/piecewise_link.rs +++ b/pywr-schema/src/nodes/piecewise_link.rs @@ -1,4 +1,4 @@ -use crate::error::ConversionError; +use crate::error::ComponentConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; use crate::metric::Metric; @@ -6,7 +6,7 @@ use crate::metric::Metric; use crate::model::LoadArgs; use crate::nodes::{NodeAttribute, NodeMeta}; use crate::parameters::Parameter; -use crate::v1::{ConversionData, TryFromV1, TryIntoV2}; +use crate::v1::{try_convert_node_attr, ConversionData, TryFromV1}; #[cfg(feature = "core")] use pywr_core::metric::MetricF64; use pywr_schema_macros::PywrVisitAll; @@ -164,7 +164,7 @@ impl PiecewiseLinkNode { } impl TryFromV1 for PiecewiseLinkNode { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: PiecewiseLinkNodeV1, @@ -178,8 +178,14 @@ impl TryFromV1 for PiecewiseLinkNode { Some(v1_costs) => v1_costs .into_iter() .map(|v| { - v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data) - .map(Some) + try_convert_node_attr( + &meta.name, + "costs", + v, + parent_node.or(Some(&meta.name)), + conversion_data, + ) + .map(Some) }) .collect::, _>>()?, }; @@ -188,10 +194,7 @@ impl TryFromV1 for PiecewiseLinkNode { None => vec![None; v1.nsteps], Some(v1_max_flows) => v1_max_flows .into_iter() - .map(|v| { - v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data) - .map(Some) - }) + .map(|v| try_convert_node_attr(&meta.name, "max_flows", v, parent_node, conversion_data).map(Some)) .collect::, _>>()?, }; diff --git a/pywr-schema/src/nodes/river.rs b/pywr-schema/src/nodes/river.rs index 60406f22..637d3ca8 100644 --- a/pywr-schema/src/nodes/river.rs +++ b/pywr-schema/src/nodes/river.rs @@ -1,6 +1,6 @@ -use crate::error::ConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; +use crate::error::{ComponentConversionError, ConversionError}; #[cfg(feature = "core")] use crate::model::LoadArgs; use crate::nodes::{LossFactor, NodeAttribute, NodeMeta}; @@ -179,27 +179,36 @@ impl RiverNode { } impl TryFrom for RiverNode { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from(v1: LinkNodeV1) -> Result { let meta: NodeMeta = v1.meta.into(); if v1.max_flow.is_some() { - return Err(ConversionError::ExtraNodeAttribute { + return Err(ComponentConversionError::Node { name: meta.name, attr: "max_flow".to_string(), + error: ConversionError::ExtraAttribute { + attr: "max_flow".to_string(), + }, }); } if v1.min_flow.is_some() { - return Err(ConversionError::ExtraNodeAttribute { + return Err(ComponentConversionError::Node { name: meta.name, attr: "min_flow".to_string(), + error: ConversionError::ExtraAttribute { + attr: "min_flow".to_string(), + }, }); } if v1.cost.is_some() { - return Err(ConversionError::ExtraNodeAttribute { + return Err(ComponentConversionError::Node { name: meta.name, attr: "cost".to_string(), + error: ConversionError::ExtraAttribute { + attr: "cost".to_string(), + }, }); } diff --git a/pywr-schema/src/nodes/river_gauge.rs b/pywr-schema/src/nodes/river_gauge.rs index 8a99bc3f..574cd2dd 100644 --- a/pywr-schema/src/nodes/river_gauge.rs +++ b/pywr-schema/src/nodes/river_gauge.rs @@ -1,4 +1,4 @@ -use crate::error::ConversionError; +use crate::error::ComponentConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; use crate::metric::Metric; @@ -6,7 +6,7 @@ use crate::metric::Metric; use crate::model::LoadArgs; use crate::nodes::{NodeAttribute, NodeMeta}; use crate::parameters::Parameter; -use crate::v1::{ConversionData, TryFromV1, TryIntoV2}; +use crate::v1::{try_convert_node_attr, ConversionData, TryFromV1}; #[cfg(feature = "core")] use pywr_core::metric::MetricF64; use pywr_schema_macros::PywrVisitAll; @@ -146,7 +146,7 @@ impl RiverGaugeNode { } impl TryFromV1 for RiverGaugeNode { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: RiverGaugeNodeV1, @@ -155,20 +155,9 @@ impl TryFromV1 for RiverGaugeNode { ) -> Result { let meta: NodeMeta = v1.meta.into(); - let mrf = v1 - .mrf - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - - let mrf_cost = v1 - .mrf_cost - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - - let bypass_cost = v1 - .cost - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; + let mrf = try_convert_node_attr(&meta.name, "mrf", v1.mrf, parent_node, conversion_data)?; + let mrf_cost = try_convert_node_attr(&meta.name, "mrf_cost", v1.mrf_cost, parent_node, conversion_data)?; + let bypass_cost = try_convert_node_attr(&meta.name, "cost", v1.cost, parent_node, conversion_data)?; let n = Self { meta, diff --git a/pywr-schema/src/nodes/river_split_with_gauge.rs b/pywr-schema/src/nodes/river_split_with_gauge.rs index 21943d19..4b1b2130 100644 --- a/pywr-schema/src/nodes/river_split_with_gauge.rs +++ b/pywr-schema/src/nodes/river_split_with_gauge.rs @@ -1,4 +1,4 @@ -use crate::error::ConversionError; +use crate::error::ComponentConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; use crate::metric::Metric; @@ -6,7 +6,7 @@ use crate::metric::Metric; use crate::model::LoadArgs; use crate::nodes::{NodeAttribute, NodeMeta}; use crate::parameters::Parameter; -use crate::v1::{ConversionData, TryFromV1, TryIntoV2}; +use crate::v1::{try_convert_node_attr, ConversionData, TryFromV1}; #[cfg(feature = "core")] use pywr_core::{aggregated_node::Relationship, metric::MetricF64, node::NodeIndex}; use pywr_schema_macros::PywrVisitAll; @@ -233,7 +233,7 @@ impl RiverSplitWithGaugeNode { } impl TryFromV1 for RiverSplitWithGaugeNode { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: RiverSplitWithGaugeNodeV1, @@ -242,15 +242,8 @@ impl TryFromV1 for RiverSplitWithGaugeNode { ) -> Result { let meta: NodeMeta = v1.meta.into(); - let mrf = v1 - .mrf - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - - let mrf_cost = v1 - .mrf_cost - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; + let mrf = try_convert_node_attr(&meta.name, "mrf", v1.mrf, parent_node, conversion_data)?; + let mrf_cost = try_convert_node_attr(&meta.name, "mrf_cost", v1.mrf_cost, parent_node, conversion_data)?; let splits = v1 .factors @@ -258,10 +251,8 @@ impl TryFromV1 for RiverSplitWithGaugeNode { .skip(1) .zip(v1.slot_names.into_iter().skip(1)) .map(|(f, slot_name)| { - Ok(RiverSplit { - factor: f.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)?, - slot_name, - }) + let factor = try_convert_node_attr(&meta.name, "factors", f, parent_node, conversion_data)?; + Ok(RiverSplit { factor, slot_name }) }) .collect::, Self::Error>>()?; diff --git a/pywr-schema/src/nodes/rolling_virtual_storage.rs b/pywr-schema/src/nodes/rolling_virtual_storage.rs index 8ccbb607..2b86d86a 100644 --- a/pywr-schema/src/nodes/rolling_virtual_storage.rs +++ b/pywr-schema/src/nodes/rolling_virtual_storage.rs @@ -1,12 +1,12 @@ -use crate::error::ConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; +use crate::error::{ComponentConversionError, ConversionError}; use crate::metric::{Metric, SimpleNodeReference}; #[cfg(feature = "core")] use crate::model::LoadArgs; use crate::nodes::{NodeAttribute, NodeMeta, StorageInitialVolume}; use crate::parameters::Parameter; -use crate::v1::{ConversionData, TryFromV1, TryIntoV2}; +use crate::v1::{try_convert_initial_storage, try_convert_node_attr, ConversionData, TryFromV1}; #[cfg(feature = "core")] use pywr_core::{ derived_metric::DerivedMetric, @@ -193,7 +193,7 @@ impl RollingVirtualStorageNode { } impl TryFromV1 for RollingVirtualStorageNode { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: RollingVirtualStorageNodeV1, @@ -202,53 +202,44 @@ impl TryFromV1 for RollingVirtualStorageNode { ) -> Result { let meta: NodeMeta = v1.meta.into(); - let cost = v1 - .cost - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - let max_volume = v1 - .max_volume - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; + let cost = try_convert_node_attr(&meta.name, "cost", v1.cost, parent_node, conversion_data)?; + let max_volume = try_convert_node_attr(&meta.name, "max_volume", v1.max_volume, parent_node, conversion_data)?; + let min_volume = try_convert_node_attr(&meta.name, "min_volume", v1.min_volume, parent_node, conversion_data)?; - let min_volume = v1 - .min_volume - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - - let initial_volume = if let Some(v) = v1.initial_volume { - StorageInitialVolume::Absolute(v) - } else if let Some(v) = v1.initial_volume_pc { - StorageInitialVolume::Proportional(v) - } else { - return Err(ConversionError::MissingAttribute { - name: meta.name, - attrs: vec!["initial_volume".to_string(), "initial_volume_pc".to_string()], - }); - }; + let initial_volume = + try_convert_initial_storage(&meta.name, "initial_volume", v1.initial_volume, v1.initial_volume_pc)?; let window = if let Some(days) = v1.days { if let Some(days) = NonZeroUsize::new(days as usize) { RollingWindow::Days(days) } else { - return Err(ConversionError::UnsupportedFeature { - feature: "Rolling window with zero `days` is not supported".to_string(), + return Err(ComponentConversionError::Node { name: meta.name.clone(), + attr: "window".to_string(), + error: ConversionError::UnsupportedFeature { + feature: "Rolling window with zero `days` is not supported".to_string(), + }, }); } } else if let Some(timesteps) = v1.timesteps { if let Some(timesteps) = NonZeroUsize::new(timesteps as usize) { RollingWindow::Timesteps(timesteps) } else { - return Err(ConversionError::UnsupportedFeature { - feature: "Rolling window with zero `timesteps` is not supported".to_string(), + return Err(ComponentConversionError::Node { name: meta.name.clone(), + attr: "window".to_string(), + error: ConversionError::UnsupportedFeature { + feature: "Rolling window with zero `timesteps` is not supported".to_string(), + }, }); } } else { - return Err(ConversionError::MissingAttribute { - attrs: vec!["days".to_string(), "timesteps".to_string()], + return Err(ComponentConversionError::Node { name: meta.name.clone(), + attr: "window".to_string(), + error: ConversionError::MissingAttribute { + attrs: vec!["days".to_string(), "timesteps".to_string()], + }, }); }; diff --git a/pywr-schema/src/nodes/virtual_storage.rs b/pywr-schema/src/nodes/virtual_storage.rs index 8c8b7753..7681d6e7 100644 --- a/pywr-schema/src/nodes/virtual_storage.rs +++ b/pywr-schema/src/nodes/virtual_storage.rs @@ -1,4 +1,4 @@ -use crate::error::ConversionError; +use crate::error::ComponentConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; use crate::metric::{Metric, SimpleNodeReference}; @@ -7,7 +7,7 @@ use crate::model::LoadArgs; use crate::nodes::core::StorageInitialVolume; use crate::nodes::{NodeAttribute, NodeMeta}; use crate::parameters::Parameter; -use crate::v1::{ConversionData, TryFromV1, TryIntoV2}; +use crate::v1::{try_convert_initial_storage, try_convert_node_attr, ConversionData, TryFromV1}; #[cfg(feature = "core")] use pywr_core::{ derived_metric::DerivedMetric, @@ -136,7 +136,7 @@ impl VirtualStorageNode { } impl TryFromV1 for VirtualStorageNode { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: VirtualStorageNodeV1, @@ -145,31 +145,13 @@ impl TryFromV1 for VirtualStorageNode { ) -> Result { let meta: NodeMeta = v1.meta.into(); - let cost = v1 - .cost - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - - let max_volume = v1 - .max_volume - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - - let min_volume = v1 - .min_volume - .map(|v| v.try_into_v2(parent_node.or(Some(&meta.name)), conversion_data)) - .transpose()?; - - let initial_volume = if let Some(v) = v1.initial_volume { - StorageInitialVolume::Absolute(v) - } else if let Some(v) = v1.initial_volume_pc { - StorageInitialVolume::Proportional(v) - } else { - return Err(ConversionError::MissingAttribute { - name: meta.name, - attrs: vec!["initial_volume".to_string(), "initial_volume_pc".to_string()], - }); - }; + let cost = try_convert_node_attr(&meta.name, "cost", v1.cost, parent_node, conversion_data)?; + let max_volume = try_convert_node_attr(&meta.name, "max_volume", v1.max_volume, parent_node, conversion_data)?; + let min_volume = try_convert_node_attr(&meta.name, "min_volume", v1.min_volume, parent_node, conversion_data)?; + + let initial_volume = + try_convert_initial_storage(&meta.name, "initial_volume", v1.initial_volume, v1.initial_volume_pc)?; + let nodes = v1.nodes.into_iter().map(|v| v.into()).collect(); let n = Self { diff --git a/pywr-schema/src/parameters/aggregated.rs b/pywr-schema/src/parameters/aggregated.rs index f2ea048d..98310161 100644 --- a/pywr-schema/src/parameters/aggregated.rs +++ b/pywr-schema/src/parameters/aggregated.rs @@ -1,11 +1,11 @@ -use crate::error::ConversionError; +use crate::error::ComponentConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; use crate::metric::Metric; #[cfg(feature = "core")] use crate::model::LoadArgs; use crate::parameters::{ConversionData, DynamicIndexValue, ParameterMeta}; -use crate::v1::{IntoV2, TryFromV1, TryIntoV2}; +use crate::v1::{try_convert_parameter_attr, IntoV2, TryFromV1}; #[cfg(feature = "core")] use pywr_core::parameters::ParameterIndex; use pywr_schema_macros::PywrVisitAll; @@ -104,7 +104,7 @@ impl AggregatedParameter { } impl TryFromV1 for AggregatedParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: AggregatedParameterV1, @@ -116,7 +116,7 @@ impl TryFromV1 for AggregatedParameter { let parameters = v1 .parameters .into_iter() - .map(|p| p.try_into_v2(parent_node, conversion_data)) + .map(|p| try_convert_parameter_attr(&meta.name, "parameters", p, parent_node, conversion_data)) .collect::, _>>()?; let p = Self { @@ -215,7 +215,7 @@ impl AggregatedIndexParameter { } impl TryFromV1 for AggregatedIndexParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: AggregatedIndexParameterV1, @@ -227,7 +227,7 @@ impl TryFromV1 for AggregatedIndexParameter { let parameters = v1 .parameters .into_iter() - .map(|p| p.try_into_v2(parent_node, conversion_data)) + .map(|p| try_convert_parameter_attr(&meta.name, "parameters", p, parent_node, conversion_data)) .collect::, _>>()?; let p = Self { diff --git a/pywr-schema/src/parameters/asymmetric_switch.rs b/pywr-schema/src/parameters/asymmetric_switch.rs index e128549c..3658acee 100644 --- a/pywr-schema/src/parameters/asymmetric_switch.rs +++ b/pywr-schema/src/parameters/asymmetric_switch.rs @@ -1,10 +1,10 @@ -use crate::error::ConversionError; +use crate::error::ComponentConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; #[cfg(feature = "core")] use crate::model::LoadArgs; use crate::parameters::{ConversionData, DynamicIndexValue, ParameterMeta}; -use crate::v1::{IntoV2, TryFromV1, TryIntoV2}; +use crate::v1::{try_convert_parameter_attr, IntoV2, TryFromV1}; #[cfg(feature = "core")] use pywr_core::parameters::ParameterIndex; use pywr_schema_macros::PywrVisitAll; @@ -40,7 +40,7 @@ impl AsymmetricSwitchIndexParameter { } impl TryFromV1 for AsymmetricSwitchIndexParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: AsymmetricSwitchIndexParameterV1, @@ -49,8 +49,20 @@ impl TryFromV1 for AsymmetricSwitchIndexParame ) -> Result { let meta: ParameterMeta = v1.meta.into_v2(parent_node, conversion_data); - let on_index_parameter = v1.on_index_parameter.try_into_v2(parent_node, conversion_data)?; - let off_index_parameter = v1.off_index_parameter.try_into_v2(parent_node, conversion_data)?; + let on_index_parameter = try_convert_parameter_attr( + &meta.name, + "on_index_parameter", + v1.on_index_parameter, + parent_node, + conversion_data, + )?; + let off_index_parameter = try_convert_parameter_attr( + &meta.name, + "off_index_parameter", + v1.off_index_parameter, + parent_node, + conversion_data, + )?; let p = Self { meta, diff --git a/pywr-schema/src/parameters/control_curves.rs b/pywr-schema/src/parameters/control_curves.rs index 42e0836d..70dbc598 100644 --- a/pywr-schema/src/parameters/control_curves.rs +++ b/pywr-schema/src/parameters/control_curves.rs @@ -1,12 +1,12 @@ -use crate::error::ConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; +use crate::error::{ComponentConversionError, ConversionError}; use crate::metric::{Metric, NodeReference}; #[cfg(feature = "core")] use crate::model::LoadArgs; use crate::nodes::NodeAttribute; use crate::parameters::{ConversionData, ParameterMeta}; -use crate::v1::{IntoV2, TryFromV1, TryIntoV2}; +use crate::v1::{try_convert_control_curves, try_convert_parameter_attr, IntoV2, TryFromV1}; #[cfg(feature = "core")] use pywr_core::parameters::ParameterIndex; use pywr_schema_macros::PywrVisitAll; @@ -59,7 +59,7 @@ impl ControlCurveInterpolatedParameter { } impl TryFromV1 for ControlCurveInterpolatedParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: ControlCurveInterpolatedParameterV1, @@ -68,38 +68,38 @@ impl TryFromV1 for ControlCurveInterpolated ) -> Result { let meta: ParameterMeta = v1.meta.into_v2(parent_node, conversion_data); - let control_curves = if let Some(control_curves) = v1.control_curves { - control_curves - .into_iter() - .map(|p| p.try_into_v2(parent_node, conversion_data)) - .collect::, _>>()? - } else if let Some(control_curve) = v1.control_curve { - vec![control_curve.try_into_v2(parent_node, conversion_data)?] - } else { - return Err(ConversionError::MissingAttribute { - name: meta.name, - attrs: vec!["control_curves".to_string(), "control_curve".to_string()], - }); - }; + let control_curves = try_convert_control_curves( + &meta.name, + v1.control_curves, + v1.control_curve, + parent_node, + conversion_data, + )?; // Handle the case where neither or both "values" and "parameters" are defined. let values = match (v1.values, v1.parameters) { (None, None) => { - return Err(ConversionError::MissingAttribute { + return Err(ComponentConversionError::Parameter { name: meta.name, - attrs: vec!["values".to_string(), "parameters".to_string()], + attr: "control_curves".to_string(), + error: ConversionError::MissingAttribute { + attrs: vec!["values".to_string(), "parameters".to_string()], + }, }); } (Some(_), Some(_)) => { - return Err(ConversionError::UnexpectedAttribute { + return Err(ComponentConversionError::Parameter { name: meta.name, - attrs: vec!["values".to_string(), "parameters".to_string()], + attr: "control_curves".to_string(), + error: ConversionError::UnexpectedAttribute { + attrs: vec!["values".to_string(), "parameters".to_string()], + }, }); } (Some(values), None) => values.into_iter().map(Metric::from).collect(), (None, Some(parameters)) => parameters .into_iter() - .map(|p| p.try_into_v2(parent_node, conversion_data)) + .map(|p| try_convert_parameter_attr(&meta.name, "parameters", p, parent_node, conversion_data)) .collect::, _>>()?, }; @@ -152,7 +152,7 @@ impl ControlCurveIndexParameter { } impl TryFromV1 for ControlCurveIndexParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: ControlCurveIndexParameterV1, @@ -164,7 +164,7 @@ impl TryFromV1 for ControlCurveIndexParameter { let control_curves = v1 .control_curves .into_iter() - .map(|p| p.try_into_v2(parent_node, conversion_data)) + .map(|p| try_convert_parameter_attr(&meta.name, "control_curves", p, parent_node, conversion_data)) .collect::, _>>()?; // v1 uses proportional volume for control curves @@ -185,7 +185,7 @@ impl TryFromV1 for ControlCurveIndexParameter { /// Pywr v1.x ControlCurveParameter can be an index parameter if it is not given "values" /// or "parameters" keys. impl TryFromV1 for ControlCurveIndexParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: ControlCurveParameterV1, @@ -194,24 +194,21 @@ impl TryFromV1 for ControlCurveIndexParameter { ) -> Result { let meta: ParameterMeta = v1.meta.into_v2(parent_node, conversion_data); - let control_curves = if let Some(control_curves) = v1.control_curves { - control_curves - .into_iter() - .map(|p| p.try_into_v2(parent_node, conversion_data)) - .collect::, _>>()? - } else if let Some(control_curve) = v1.control_curve { - vec![control_curve.try_into_v2(parent_node, conversion_data)?] - } else { - return Err(ConversionError::MissingAttribute { - name: meta.name, - attrs: vec!["control_curves".to_string(), "control_curve".to_string()], - }); - }; + let control_curves = try_convert_control_curves( + &meta.name, + v1.control_curves, + v1.control_curve, + parent_node, + conversion_data, + )?; if v1.values.is_some() || v1.parameters.is_some() { - return Err(ConversionError::UnexpectedAttribute { + return Err(ComponentConversionError::Parameter { name: meta.name, - attrs: vec!["values".to_string(), "parameters".to_string()], + attr: "values".to_string(), + error: ConversionError::UnexpectedAttribute { + attrs: vec!["values".to_string(), "parameters".to_string()], + }, }); }; @@ -271,7 +268,7 @@ impl ControlCurveParameter { } impl TryFromV1 for ControlCurveParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: ControlCurveParameterV1, @@ -280,31 +277,28 @@ impl TryFromV1 for ControlCurveParameter { ) -> Result { let meta: ParameterMeta = v1.meta.into_v2(parent_node, conversion_data); - let control_curves = if let Some(control_curves) = v1.control_curves { - control_curves - .into_iter() - .map(|p| p.try_into_v2(parent_node, conversion_data)) - .collect::, _>>()? - } else if let Some(control_curve) = v1.control_curve { - vec![control_curve.try_into_v2(parent_node, conversion_data)?] - } else { - return Err(ConversionError::MissingAttribute { - name: meta.name, - attrs: vec!["control_curves".to_string(), "control_curve".to_string()], - }); - }; + let control_curves = try_convert_control_curves( + &meta.name, + v1.control_curves, + v1.control_curve, + parent_node, + conversion_data, + )?; let values = if let Some(values) = v1.values { values.into_iter().map(Metric::from).collect() } else if let Some(parameters) = v1.parameters { parameters .into_iter() - .map(|p| p.try_into_v2(parent_node, conversion_data)) + .map(|p| try_convert_parameter_attr(&meta.name, "parameters", p, parent_node, conversion_data)) .collect::, _>>()? } else { - return Err(ConversionError::MissingAttribute { + return Err(ComponentConversionError::Parameter { name: meta.name, - attrs: vec!["values".to_string(), "parameters".to_string()], + attr: "values".to_string(), + error: ConversionError::MissingAttribute { + attrs: vec!["values".to_string(), "parameters".to_string()], + }, }); }; @@ -368,7 +362,7 @@ impl ControlCurvePiecewiseInterpolatedParameter { } impl TryFromV1 for ControlCurvePiecewiseInterpolatedParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: ControlCurvePiecewiseInterpolatedParameterV1, @@ -377,19 +371,13 @@ impl TryFromV1 for ControlCurvePie ) -> Result { let meta: ParameterMeta = v1.meta.into_v2(parent_node, conversion_data); - let control_curves = if let Some(control_curves) = v1.control_curves { - control_curves - .into_iter() - .map(|p| p.try_into_v2(parent_node, conversion_data)) - .collect::, _>>()? - } else if let Some(control_curve) = v1.control_curve { - vec![control_curve.try_into_v2(parent_node, conversion_data)?] - } else { - return Err(ConversionError::MissingAttribute { - name: meta.name, - attrs: vec!["control_curves".to_string(), "control_curve".to_string()], - }); - }; + let control_curves = try_convert_control_curves( + &meta.name, + v1.control_curves, + v1.control_curve, + parent_node, + conversion_data, + )?; // v1 uses proportional volume for control curves let storage_node = NodeReference { diff --git a/pywr-schema/src/parameters/core.rs b/pywr-schema/src/parameters/core.rs index fdb920c6..894b092b 100644 --- a/pywr-schema/src/parameters/core.rs +++ b/pywr-schema/src/parameters/core.rs @@ -1,11 +1,11 @@ -use crate::error::ConversionError; +use crate::error::ComponentConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; use crate::metric::Metric; #[cfg(feature = "core")] use crate::model::LoadArgs; use crate::parameters::{ConstantValue, ConversionData, ParameterMeta}; -use crate::v1::{IntoV2, TryFromV1, TryIntoV2}; +use crate::v1::{try_convert_parameter_attr, IntoV2, TryFromV1}; #[cfg(feature = "core")] use pywr_core::parameters::{ParameterIndex, ParameterName}; use pywr_schema_macros::PywrVisitAll; @@ -177,23 +177,29 @@ impl ConstantParameter { } impl TryFromV1 for ConstantParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: ConstantParameterV1, parent_node: Option<&str>, conversion_data: &mut ConversionData, ) -> Result { + let meta: ParameterMeta = v1.meta.into_v2(parent_node, conversion_data); + let value = if let Some(v) = v1.value { ConstantValue::Literal(v) } else if let Some(tbl) = v1.table { - ConstantValue::Table(tbl.try_into()?) + ConstantValue::Table(tbl.try_into().map_err(|error| ComponentConversionError::Parameter { + name: meta.name.clone(), + attr: "table".to_string(), + error, + })?) } else { ConstantValue::Literal(0.0) }; let p = Self { - meta: v1.meta.into_v2(parent_node, conversion_data), + meta, value, variable: None, // TODO convert variable settings }; @@ -225,7 +231,7 @@ impl MaxParameter { } impl TryFromV1 for MaxParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: MaxParameterV1, @@ -234,7 +240,8 @@ impl TryFromV1 for MaxParameter { ) -> Result { let meta: ParameterMeta = v1.meta.into_v2(parent_node, conversion_data); - let parameter = v1.parameter.try_into_v2(parent_node, conversion_data)?; + let parameter = + try_convert_parameter_attr(&meta.name, "parameter", v1.parameter, parent_node, conversion_data)?; let p = Self { meta, @@ -281,7 +288,7 @@ impl DivisionParameter { } impl TryFromV1 for DivisionParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: DivisionParameterV1, @@ -290,8 +297,10 @@ impl TryFromV1 for DivisionParameter { ) -> Result { let meta: ParameterMeta = v1.meta.into_v2(parent_node, conversion_data); - let numerator = v1.numerator.try_into_v2(parent_node, conversion_data)?; - let denominator = v1.denominator.try_into_v2(parent_node, conversion_data)?; + let numerator = + try_convert_parameter_attr(&meta.name, "numerator", v1.numerator, parent_node, conversion_data)?; + let denominator = + try_convert_parameter_attr(&meta.name, "denominator", v1.denominator, parent_node, conversion_data)?; let p = Self { meta, @@ -338,7 +347,7 @@ impl MinParameter { } impl TryFromV1 for MinParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: MinParameterV1, @@ -347,7 +356,8 @@ impl TryFromV1 for MinParameter { ) -> Result { let meta: ParameterMeta = v1.meta.into_v2(parent_node, conversion_data); - let parameter = v1.parameter.try_into_v2(parent_node, conversion_data)?; + let parameter = + try_convert_parameter_attr(&meta.name, "parameter", v1.parameter, parent_node, conversion_data)?; let p = Self { meta, @@ -380,7 +390,7 @@ impl NegativeParameter { } impl TryFromV1 for NegativeParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: NegativeParameterV1, @@ -389,7 +399,8 @@ impl TryFromV1 for NegativeParameter { ) -> Result { let meta: ParameterMeta = v1.meta.into_v2(parent_node, conversion_data); - let parameter = v1.parameter.try_into_v2(parent_node, conversion_data)?; + let parameter = + try_convert_parameter_attr(&meta.name, "parameter", v1.parameter, parent_node, conversion_data)?; let p = Self { meta, parameter }; Ok(p) @@ -434,7 +445,7 @@ impl NegativeMaxParameter { } impl TryFromV1 for NegativeMaxParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: NegativeMaxParameterV1, @@ -442,7 +453,10 @@ impl TryFromV1 for NegativeMaxParameter { conversion_data: &mut ConversionData, ) -> Result { let meta: ParameterMeta = v1.meta.into_v2(parent_node, conversion_data); - let parameter = v1.parameter.try_into_v2(parent_node, conversion_data)?; + + let parameter = + try_convert_parameter_attr(&meta.name, "parameter", v1.parameter, parent_node, conversion_data)?; + let p = Self { meta, metric: parameter, @@ -490,7 +504,7 @@ impl NegativeMinParameter { } impl TryFromV1 for NegativeMinParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: NegativeMinParameterV1, @@ -498,7 +512,9 @@ impl TryFromV1 for NegativeMinParameter { conversion_data: &mut ConversionData, ) -> Result { let meta: ParameterMeta = v1.meta.into_v2(parent_node, conversion_data); - let parameter = v1.parameter.try_into_v2(parent_node, conversion_data)?; + let parameter = + try_convert_parameter_attr(&meta.name, "parameter", v1.parameter, parent_node, conversion_data)?; + let p = Self { meta, metric: parameter, diff --git a/pywr-schema/src/parameters/discount_factor.rs b/pywr-schema/src/parameters/discount_factor.rs index 3971dc00..c0e72df7 100644 --- a/pywr-schema/src/parameters/discount_factor.rs +++ b/pywr-schema/src/parameters/discount_factor.rs @@ -4,8 +4,7 @@ use crate::metric::Metric; #[cfg(feature = "core")] use crate::model::LoadArgs; use crate::parameters::{ConversionData, ParameterMeta}; -use crate::v1::{IntoV2, TryFromV1}; -use crate::ConversionError; +use crate::v1::{FromV1, IntoV2}; #[cfg(feature = "core")] use pywr_core::parameters::ParameterIndex; use pywr_schema_macros::PywrVisitAll; @@ -38,20 +37,14 @@ impl DiscountFactorParameter { } } -impl TryFromV1 for DiscountFactorParameter { - type Error = ConversionError; - - fn try_from_v1( - v1: DiscountFactorParameterV1, - parent_node: Option<&str>, - conversion_data: &mut ConversionData, - ) -> Result { +impl FromV1 for DiscountFactorParameter { + fn from_v1(v1: DiscountFactorParameterV1, parent_node: Option<&str>, conversion_data: &mut ConversionData) -> Self { let meta: ParameterMeta = v1.meta.into_v2(parent_node, conversion_data); let discount_rate = Metric::from(v1.rate); - Ok(Self { + Self { meta, discount_rate, base_year: v1.base_year as i32, - }) + } } } diff --git a/pywr-schema/src/parameters/hydropower.rs b/pywr-schema/src/parameters/hydropower.rs index 864a5d42..b9a96a03 100644 --- a/pywr-schema/src/parameters/hydropower.rs +++ b/pywr-schema/src/parameters/hydropower.rs @@ -1,9 +1,9 @@ +use crate::error::ComponentConversionError; use crate::metric::Metric; #[cfg(feature = "core")] use crate::model::LoadArgs; use crate::parameters::{ConversionData, ParameterMeta}; -use crate::v1::{IntoV2, TryFromV1, TryIntoV2}; -use crate::ConversionError; +use crate::v1::{try_convert_parameter_attr, IntoV2, TryFromV1}; #[cfg(feature = "core")] use crate::SchemaError; #[cfg(feature = "core")] @@ -119,7 +119,7 @@ impl HydropowerTargetParameter { } impl TryFromV1 for HydropowerTargetParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: HydropowerTargetParameterV1, @@ -127,19 +127,17 @@ impl TryFromV1 for HydropowerTargetParameter { conversion_data: &mut ConversionData, ) -> Result { let meta: ParameterMeta = v1.meta.into_v2(parent_node, conversion_data); - let target = v1.target.try_into_v2(parent_node, conversion_data)?; - let water_elevation = v1 - .water_elevation_parameter - .map(|f| f.try_into_v2(parent_node, conversion_data)) - .transpose()?; - let min_flow = v1 - .min_flow - .map(|f| f.try_into_v2(parent_node, conversion_data)) - .transpose()?; - let max_flow = v1 - .max_flow - .map(|f| f.try_into_v2(parent_node, conversion_data)) - .transpose()?; + let target = try_convert_parameter_attr(&meta.name, "target", v1.target, parent_node, conversion_data)?; + let water_elevation = try_convert_parameter_attr( + &meta.name, + "water_elevation_parameter", + v1.water_elevation_parameter, + parent_node, + conversion_data, + )?; + + let min_flow = try_convert_parameter_attr(&meta.name, "min_flow", v1.min_flow, parent_node, conversion_data)?; + let max_flow = try_convert_parameter_attr(&meta.name, "max_flow", v1.max_flow, parent_node, conversion_data)?; Ok(Self { meta, diff --git a/pywr-schema/src/parameters/indexed_array.rs b/pywr-schema/src/parameters/indexed_array.rs index 153bc8e0..1315d5d2 100644 --- a/pywr-schema/src/parameters/indexed_array.rs +++ b/pywr-schema/src/parameters/indexed_array.rs @@ -1,11 +1,11 @@ -use crate::error::ConversionError; +use crate::error::ComponentConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; use crate::metric::Metric; #[cfg(feature = "core")] use crate::model::LoadArgs; use crate::parameters::{ConversionData, DynamicIndexValue, ParameterMeta}; -use crate::v1::{IntoV2, TryFromV1, TryIntoV2}; +use crate::v1::{try_convert_parameter_attr, IntoV2, TryFromV1}; #[cfg(feature = "core")] use pywr_core::parameters::ParameterIndex; use pywr_schema_macros::PywrVisitAll; @@ -47,7 +47,7 @@ impl IndexedArrayParameter { } impl TryFromV1 for IndexedArrayParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: IndexedArrayParameterV1, @@ -59,10 +59,16 @@ impl TryFromV1 for IndexedArrayParameter { let metrics = v1 .parameters .into_iter() - .map(|p| p.try_into_v2(parent_node, conversion_data)) + .map(|p| try_convert_parameter_attr(&meta.name, "parameters", p, parent_node, conversion_data)) .collect::, _>>()?; - let index_parameter = v1.index_parameter.try_into_v2(parent_node, conversion_data)?; + let index_parameter = try_convert_parameter_attr( + &meta.name, + "index_parameter", + v1.index_parameter, + parent_node, + conversion_data, + )?; let p = Self { meta, diff --git a/pywr-schema/src/parameters/interpolated.rs b/pywr-schema/src/parameters/interpolated.rs index 615a45af..07166a29 100644 --- a/pywr-schema/src/parameters/interpolated.rs +++ b/pywr-schema/src/parameters/interpolated.rs @@ -1,10 +1,11 @@ +use crate::error::ComponentConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; use crate::metric::{Metric, NodeReference}; #[cfg(feature = "core")] use crate::model::LoadArgs; use crate::parameters::{ConversionData, ParameterMeta}; -use crate::v1::{IntoV2, TryFromV1, TryIntoV2}; +use crate::v1::{try_convert_parameter_attr, IntoV2, TryFromV1}; use crate::ConversionError; #[cfg(feature = "core")] use pywr_core::parameters::ParameterIndex; @@ -71,7 +72,7 @@ impl InterpolatedParameter { } impl TryFromV1 for InterpolatedParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: InterpolatedFlowParameterV1, @@ -91,13 +92,13 @@ impl TryFromV1 for InterpolatedParameter { let xp = v1 .flows .into_iter() - .map(|p| p.try_into_v2(parent_node, conversion_data)) + .map(|p| try_convert_parameter_attr(&meta.name, "flows", p, parent_node, conversion_data)) .collect::, _>>()?; let fp = v1 .values .into_iter() - .map(|p| p.try_into_v2(parent_node, conversion_data)) + .map(|p| try_convert_parameter_attr(&meta.name, "values", p, parent_node, conversion_data)) .collect::, _>>()?; // Default values @@ -114,9 +115,12 @@ impl TryFromV1 for InterpolatedParameter { if let Some(kind) = interp_kwargs.get("kind") { if let Some(kind_str) = kind.as_str() { if kind_str != "linear" { - return Err(ConversionError::UnsupportedFeature { - feature: "Interpolation with `kind` other than `linear` is not supported.".to_string(), + return Err(ComponentConversionError::Parameter { name: meta.name.clone(), + attr: "interp_kwargs".to_string(), + error: ConversionError::UnsupportedFeature { + feature: "Interpolation with `kind` other than `linear` is not supported.".to_string(), + }, }); } } @@ -134,7 +138,7 @@ impl TryFromV1 for InterpolatedParameter { } impl TryFromV1 for InterpolatedParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: InterpolatedVolumeParameterV1, @@ -154,13 +158,13 @@ impl TryFromV1 for InterpolatedParameter { let xp = v1 .volumes .into_iter() - .map(|p| p.try_into_v2(parent_node, conversion_data)) + .map(|p| try_convert_parameter_attr(&meta.name, "volumes", p, parent_node, conversion_data)) .collect::, _>>()?; let fp = v1 .values .into_iter() - .map(|p| p.try_into_v2(parent_node, conversion_data)) + .map(|p| try_convert_parameter_attr(&meta.name, "values", p, parent_node, conversion_data)) .collect::, _>>()?; // Default values @@ -177,9 +181,12 @@ impl TryFromV1 for InterpolatedParameter { if let Some(kind) = interp_kwargs.get("kind") { if let Some(kind_str) = kind.as_str() { if kind_str != "linear" { - return Err(ConversionError::UnsupportedFeature { - feature: "Interpolation with `kind` other than `linear` is not supported.".to_string(), + return Err(ComponentConversionError::Parameter { name: meta.name.clone(), + attr: "interp_kwargs".to_string(), + error: ConversionError::UnsupportedFeature { + feature: "Interpolation with `kind` other than `linear` is not supported.".to_string(), + }, }); } } diff --git a/pywr-schema/src/parameters/mod.rs b/pywr-schema/src/parameters/mod.rs index 5f6b4226..86956841 100644 --- a/pywr-schema/src/parameters/mod.rs +++ b/pywr-schema/src/parameters/mod.rs @@ -26,14 +26,14 @@ mod thresholds; #[cfg(feature = "core")] pub use super::data_tables::LoadedTableCollection; pub use super::data_tables::TableDataRef; -use crate::error::ConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; +use crate::error::{ComponentConversionError, ConversionError}; use crate::metric::{Metric, ParameterReference}; #[cfg(feature = "core")] use crate::model::LoadArgs; use crate::timeseries::TimeseriesReference; -use crate::v1::{ConversionData, TryFromV1, TryIntoV2}; +use crate::v1::{ConversionData, IntoV2, TryFromV1, TryIntoV2}; use crate::visit::{VisitMetrics, VisitPaths}; pub use aggregated::{AggFunc, AggregatedIndexParameter, AggregatedParameter, IndexAggFunc}; pub use asymmetric_switch::AsymmetricSwitchIndexParameter; @@ -373,7 +373,7 @@ impl From for ParameterOrTimeseriesRef { } impl TryFromV1 for ParameterOrTimeseriesRef { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: ParameterV1, @@ -415,19 +415,17 @@ impl TryFromV1 for ParameterOrTimeseriesRef { Parameter::MonthlyProfile(p.try_into_v2(parent_node, conversion_data)?).into() } CoreParameter::UniformDrawdownProfile(p) => { - Parameter::UniformDrawdownProfile(p.try_into_v2(parent_node, conversion_data)?).into() + Parameter::UniformDrawdownProfile(p.into_v2(parent_node, conversion_data)).into() } CoreParameter::Max(p) => Parameter::Max(p.try_into_v2(parent_node, conversion_data)?).into(), CoreParameter::Negative(p) => Parameter::Negative(p.try_into_v2(parent_node, conversion_data)?).into(), CoreParameter::Polynomial1D(p) => { - Parameter::Polynomial1D(p.try_into_v2(parent_node, conversion_data)?).into() + Parameter::Polynomial1D(p.into_v2(parent_node, conversion_data)).into() } CoreParameter::ParameterThreshold(p) => { Parameter::Threshold(p.try_into_v2(parent_node, conversion_data)?).into() } - CoreParameter::TablesArray(p) => { - Parameter::TablesArray(p.try_into_v2(parent_node, conversion_data)?).into() - } + CoreParameter::TablesArray(p) => Parameter::TablesArray(p.into_v2(parent_node, conversion_data)).into(), CoreParameter::Min(p) => Parameter::Min(p.try_into_v2(parent_node, conversion_data)?).into(), CoreParameter::Division(p) => Parameter::Division(p.try_into_v2(parent_node, conversion_data)?).into(), CoreParameter::DataFrame(p) => >::try_into_v2( @@ -437,14 +435,17 @@ impl TryFromV1 for ParameterOrTimeseriesRef { )? .into(), CoreParameter::Deficit(p) => { - return Err(ConversionError::DeprecatedParameter { - ty: "DeficitParameter".to_string(), + return Err(ComponentConversionError::Parameter { name: p.meta.and_then(|m| m.name).unwrap_or("unnamed".to_string()), - instead: "Use a derived metric instead.".to_string(), - }) + attr: "".to_string(), + error: ConversionError::DeprecatedParameter { + ty: "DeficitParameter".to_string(), + instead: "Use a derived metric instead.".to_string(), + }, + }); } CoreParameter::DiscountFactor(p) => { - Parameter::DiscountFactor(p.try_into_v2(parent_node, conversion_data)?).into() + Parameter::DiscountFactor(p.into_v2(parent_node, conversion_data)).into() } CoreParameter::InterpolatedVolume(p) => { Parameter::Interpolated(p.try_into_v2(parent_node, conversion_data)?).into() @@ -459,20 +460,26 @@ impl TryFromV1 for ParameterOrTimeseriesRef { Parameter::WeeklyProfile(p.try_into_v2(parent_node, conversion_data)?).into() } CoreParameter::Storage(p) => { - return Err(ConversionError::DeprecatedParameter { - ty: "StorageParameter".to_string(), + return Err(ComponentConversionError::Parameter { name: p.meta.and_then(|m| m.name).unwrap_or("unnamed".to_string()), - instead: "Use a derived metric instead.".to_string(), - }) + attr: "".to_string(), + error: ConversionError::DeprecatedParameter { + ty: "DeficitParameter".to_string(), + instead: "Use a derived metric instead.".to_string(), + }, + }); } CoreParameter::RollingMeanFlowNode(_) => todo!("Implement RollingMeanFlowNodeParameter"), CoreParameter::ScenarioWrapper(_) => todo!("Implement ScenarioWrapperParameter"), CoreParameter::Flow(p) => { - return Err(ConversionError::DeprecatedParameter { - ty: "FlowParameter".to_string(), + return Err(ComponentConversionError::Parameter { name: p.meta.and_then(|m| m.name).unwrap_or("unnamed".to_string()), - instead: "Use a derived metric instead.".to_string(), - }) + attr: "".to_string(), + error: ConversionError::DeprecatedParameter { + ty: "FlowParameter".to_string(), + instead: "Use a derived metric instead.".to_string(), + }, + }); } CoreParameter::RbfProfile(p) => { Parameter::RbfProfile(p.try_into_v2(parent_node, conversion_data)?).into() @@ -485,24 +492,11 @@ impl TryFromV1 for ParameterOrTimeseriesRef { } }, ParameterV1::Custom(p) => { - println!("Custom parameter: {:?} ({})", p.meta.name, p.ty); - // TODO do something better with custom parameters - - let mut comment = format!("V1 CUSTOM PARAMETER ({}) UNCONVERTED!", p.ty); - if let Some(c) = p.meta.comment { - comment.push_str(" ORIGINAL COMMENT: "); - comment.push_str(c.as_str()); - } - - Parameter::Constant(ConstantParameter { - meta: ParameterMeta { - name: p.meta.name.unwrap_or_else(|| "unnamed-custom-parameter".to_string()), - comment: Some(comment), - }, - value: ConstantValue::Literal(0.0), - variable: None, - }) - .into() + return Err(ComponentConversionError::Parameter { + name: p.meta.name.unwrap_or_else(|| "unnamed".to_string()), + attr: "".to_string(), + error: ConversionError::UnrecognisedType { ty: p.ty }, + }); } }; @@ -610,9 +604,9 @@ impl TryFrom for ConstantValue { fn try_from(v1: ParameterValueV1) -> Result { match v1 { ParameterValueV1::Constant(v) => Ok(Self::Literal(v)), - ParameterValueV1::Reference(_) => Err(ConversionError::ConstantFloatReferencesParameter), + ParameterValueV1::Reference(_) => Err(ConversionError::ConstantFloatReferencesParameter {}), ParameterValueV1::Table(tbl) => Ok(Self::Table(tbl.try_into()?)), - ParameterValueV1::Inline(_) => Err(ConversionError::ConstantFloatInlineParameter), + ParameterValueV1::Inline(_) => Err(ConversionError::ConstantFloatInlineParameter {}), } } } @@ -675,14 +669,20 @@ impl TryFromV1 for DynamicIndexValue { let p = match v1 { // There was no such thing as s constant index in Pywr v1 // TODO this could print a warning and do a cast to usize instead. - ParameterValueV1::Constant(_) => return Err(ConversionError::FloatToIndex), + ParameterValueV1::Constant(_) => return Err(ConversionError::FloatToIndex {}), ParameterValueV1::Reference(p_name) => Self::Dynamic(ParameterIndexValue::Reference(p_name)), ParameterValueV1::Table(tbl) => Self::Constant(ConstantValue::Table(tbl.try_into()?)), ParameterValueV1::Inline(param) => { // Inline parameters are converted to either a parameter or a timeseries // The actual component is extracted into the conversion data leaving a reference // to the component in the metric. - let definition: ParameterOrTimeseriesRef = (*param).try_into_v2(parent_node, conversion_data)?; + let definition: ParameterOrTimeseriesRef = + (*param) + .try_into_v2(parent_node, conversion_data) + .map_err(|e| match e { + ComponentConversionError::Node { error, .. } => error, + ComponentConversionError::Parameter { error, .. } => error, + })?; match definition { ParameterOrTimeseriesRef::Parameter(p) => { let reference = ParameterReference { @@ -739,20 +739,20 @@ pub enum TableIndex { } impl TryFrom for TableIndex { - type Error = ConversionError; + type Error = String; fn try_from(v1: TableIndexV1) -> Result { match v1 { TableIndexV1::Single(s) => match s { TableIndexEntryV1::Name(s) => Ok(TableIndex::Single(s)), - TableIndexEntryV1::Index(_) => Err(ConversionError::IntegerTableIndicesNotSupported), + TableIndexEntryV1::Index(_) => Err("Integer table indices not supported".to_string()), }, TableIndexV1::Multi(s) => { let names = s .into_iter() .map(|e| match e { TableIndexEntryV1::Name(s) => Ok(s), - TableIndexEntryV1::Index(_) => Err(ConversionError::IntegerTableIndicesNotSupported), + TableIndexEntryV1::Index(_) => Err("Integer table indices not supported".to_string()), }) .collect::, _>>()?; Ok(Self::Multi(names)) diff --git a/pywr-schema/src/parameters/polynomial.rs b/pywr-schema/src/parameters/polynomial.rs index 5f5d97ee..eb40b01c 100644 --- a/pywr-schema/src/parameters/polynomial.rs +++ b/pywr-schema/src/parameters/polynomial.rs @@ -1,4 +1,3 @@ -use crate::error::ConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; use crate::metric::{Metric, NodeReference}; @@ -6,7 +5,7 @@ use crate::metric::{Metric, NodeReference}; use crate::model::LoadArgs; use crate::nodes::NodeAttribute; use crate::parameters::{ConversionData, ParameterMeta}; -use crate::v1::{IntoV2, TryFromV1}; +use crate::v1::{FromV1, IntoV2}; #[cfg(feature = "core")] use pywr_core::parameters::ParameterIndex; use pywr_schema_macros::PywrVisitAll; @@ -43,14 +42,8 @@ impl Polynomial1DParameter { } } -impl TryFromV1 for Polynomial1DParameter { - type Error = ConversionError; - - fn try_from_v1( - v1: Polynomial1DParameterV1, - parent_node: Option<&str>, - conversion_data: &mut ConversionData, - ) -> Result { +impl FromV1 for Polynomial1DParameter { + fn from_v1(v1: Polynomial1DParameterV1, parent_node: Option<&str>, conversion_data: &mut ConversionData) -> Self { let attribute = match v1.use_proportional_volume.unwrap_or(true) { true => Some(NodeAttribute::ProportionalVolume), false => Some(NodeAttribute::Volume), @@ -61,13 +54,12 @@ impl TryFromV1 for Polynomial1DParameter { attribute, }); - let p = Self { + Self { meta: v1.meta.into_v2(parent_node, conversion_data), metric, coefficients: v1.coefficients, scale: v1.scale, offset: v1.offset, - }; - Ok(p) + } } } diff --git a/pywr-schema/src/parameters/profiles.rs b/pywr-schema/src/parameters/profiles.rs index 0d116506..c7f04375 100644 --- a/pywr-schema/src/parameters/profiles.rs +++ b/pywr-schema/src/parameters/profiles.rs @@ -1,10 +1,10 @@ -use crate::error::ConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; +use crate::error::{ComponentConversionError, ConversionError}; #[cfg(feature = "core")] use crate::model::LoadArgs; use crate::parameters::{ConstantFloatVec, ConstantValue, ConversionData, ParameterMeta}; -use crate::v1::{IntoV2, TryFromV1}; +use crate::v1::{try_convert_values, FromV1, IntoV2, TryFromV1}; #[cfg(feature = "core")] use pywr_core::parameters::{ParameterIndex, WeeklyProfileError, WeeklyProfileValues}; use pywr_schema_macros::PywrVisitAll; @@ -40,7 +40,7 @@ impl DailyProfileParameter { } impl TryFromV1 for DailyProfileParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: DailyProfileParameterV1, @@ -49,22 +49,7 @@ impl TryFromV1 for DailyProfileParameter { ) -> Result { let meta: ParameterMeta = v1.meta.into_v2(parent_node, conversion_data); - let values: ConstantFloatVec = if let Some(values) = v1.values { - ConstantFloatVec::Literal(values) - } else if let Some(_external) = v1.external { - return Err(ConversionError::UnsupportedFeature { - feature: "External data references are not supported in Pywr v2. Please use a table instead." - .to_string(), - name: meta.name, - }); - } else if let Some(table_ref) = v1.table_ref { - ConstantFloatVec::Table(table_ref.try_into()?) - } else { - return Err(ConversionError::MissingAttribute { - name: meta.name, - attrs: vec!["values".to_string(), "table".to_string(), "url".to_string()], - }); - }; + let values = try_convert_values(&meta.name, v1.values, v1.external, v1.table_ref)?; let p = Self { meta, values }; Ok(p) @@ -122,7 +107,7 @@ impl From for MonthlyInterpDay { } impl TryFromV1 for MonthlyProfileParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: MonthlyProfileParameterV1, @@ -132,22 +117,7 @@ impl TryFromV1 for MonthlyProfileParameter { let meta: ParameterMeta = v1.meta.into_v2(parent_node, conversion_data); let interp_day = v1.interp_day.map(|id| id.into()); - let values: ConstantFloatVec = if let Some(values) = v1.values { - ConstantFloatVec::Literal(values.to_vec()) - } else if let Some(_external) = v1.external { - return Err(ConversionError::UnsupportedFeature { - feature: "External data references are not supported in Pywr v2. Please use a table instead." - .to_string(), - name: meta.name, - }); - } else if let Some(table_ref) = v1.table_ref { - ConstantFloatVec::Table(table_ref.try_into()?) - } else { - return Err(ConversionError::MissingAttribute { - name: meta.name, - attrs: vec!["values".to_string(), "table".to_string(), "url".to_string()], - }); - }; + let values = try_convert_values(&meta.name, v1.values.map(|v| v.to_vec()), v1.external, v1.table_ref)?; let p = Self { meta, @@ -197,24 +167,20 @@ impl UniformDrawdownProfileParameter { } } -impl TryFromV1 for UniformDrawdownProfileParameter { - type Error = ConversionError; - - fn try_from_v1( +impl FromV1 for UniformDrawdownProfileParameter { + fn from_v1( v1: UniformDrawdownProfileParameterV1, parent_node: Option<&str>, conversion_data: &mut ConversionData, - ) -> Result { + ) -> Self { let meta: ParameterMeta = v1.meta.into_v2(parent_node, conversion_data); - let p = Self { + Self { meta, reset_day: v1.reset_day.map(|v| ConstantValue::Literal(v as usize)), reset_month: v1.reset_day.map(|v| ConstantValue::Literal(v as usize)), residual_days: v1.reset_day.map(|v| ConstantValue::Literal(v as usize)), - }; - - Ok(p) + } } } @@ -376,7 +342,7 @@ impl RbfProfileParameter { } impl TryFromV1 for RbfProfileParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: RbfProfileParameterV1, @@ -388,16 +354,22 @@ impl TryFromV1 for RbfProfileParameter { let points = v1.days_of_year.into_iter().zip(v1.values).collect(); if v1.rbf_kwargs.contains_key("smooth") { - return Err(ConversionError::UnsupportedFeature { - feature: "The RBF `smooth` keyword argument is not supported.".to_string(), + return Err(ComponentConversionError::Parameter { name: meta.name, + attr: "smooth".to_string(), + error: ConversionError::UnsupportedFeature { + feature: "The RBF `smooth` keyword argument is not supported.".to_string(), + }, }); } if v1.rbf_kwargs.contains_key("norm") { - return Err(ConversionError::UnsupportedFeature { - feature: "The RBF `norm` keyword argument is not supported.".to_string(), + return Err(ComponentConversionError::Parameter { name: meta.name, + attr: "norm".to_string(), + error: ConversionError::UnsupportedFeature { + feature: "The RBF `norm` keyword argument is not supported.".to_string(), + }, }); } @@ -406,11 +378,13 @@ impl TryFromV1 for RbfProfileParameter { if let Some(epsilon_f64) = epsilon_value.as_f64() { Some(epsilon_f64) } else { - return Err(ConversionError::UnexpectedType { - attr: "epsilon".to_string(), + return Err(ComponentConversionError::Parameter { name: meta.name, - expected: "float".to_string(), - actual: format!("{}", epsilon_value), + attr: "epsilon".to_string(), + error: ConversionError::UnexpectedType { + expected: "float".to_string(), + actual: format!("{}", epsilon_value), + }, }); } } else { @@ -428,18 +402,23 @@ impl TryFromV1 for RbfProfileParameter { "cubic" => RadialBasisFunction::Cubic, "thin_plate" => RadialBasisFunction::ThinPlateSpline, _ => { - return Err(ConversionError::UnsupportedFeature { - feature: format!("Radial basis function `{}` not supported.", function_str), - name: meta.name.clone(), - }) + return Err(ComponentConversionError::Parameter { + name: meta.name, + attr: "rbf_kwargs".to_string(), + error: ConversionError::UnsupportedFeature { + feature: format!("Radial basis function `{}` not supported.", function_str), + }, + }); } } } else { - return Err(ConversionError::UnexpectedType { - attr: "function".to_string(), + return Err(ComponentConversionError::Parameter { name: meta.name, - expected: "string".to_string(), - actual: format!("{}", function_value), + attr: "rbf_kwargs".to_string(), + error: ConversionError::UnexpectedType { + expected: "string".to_string(), + actual: format!("{}", function_value), + }, }); } } else { @@ -565,7 +544,7 @@ impl WeeklyProfileParameter { } impl TryFromV1 for WeeklyProfileParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: WeeklyProfileParameterV1, @@ -574,23 +553,7 @@ impl TryFromV1 for WeeklyProfileParameter { ) -> Result { let meta: ParameterMeta = v1.meta.into_v2(parent_node, conversion_data); - let values: ConstantFloatVec = if let Some(values) = v1.values { - // pywr v1 only accept a 52-week profile - ConstantFloatVec::Literal(values) - } else if let Some(_external) = v1.external { - return Err(ConversionError::UnsupportedFeature { - feature: "External data references are not supported in Pywr v2. Please use a table instead." - .to_string(), - name: meta.name, - }); - } else if let Some(table_ref) = v1.table_ref { - ConstantFloatVec::Table(table_ref.try_into()?) - } else { - return Err(ConversionError::MissingAttribute { - name: meta.name, - attrs: vec!["values".to_string(), "table".to_string(), "url".to_string()], - }); - }; + let values = try_convert_values(&meta.name, v1.values, v1.external, v1.table_ref)?; // pywr 1 does not support interpolation let p = Self { diff --git a/pywr-schema/src/parameters/python.rs b/pywr-schema/src/parameters/python.rs index 9983c4c8..3232b891 100644 --- a/pywr-schema/src/parameters/python.rs +++ b/pywr-schema/src/parameters/python.rs @@ -204,7 +204,10 @@ impl PythonParameter { PythonSource::Module(module) => PyModule::import_bound(py, module.as_str()), PythonSource::Path(original_path) => { let path = &make_path(original_path, args.data_path); - let code = std::fs::read_to_string(path).expect("Could not read Python code from path."); + let code = std::fs::read_to_string(path).map_err(|error| SchemaError::IO { + path: path.to_path_buf(), + error, + })?; let file_name = path.file_name().unwrap().to_str().unwrap(); let module_name = path.file_stem().unwrap().to_str().unwrap(); diff --git a/pywr-schema/src/parameters/tables.rs b/pywr-schema/src/parameters/tables.rs index 6919b446..75518613 100644 --- a/pywr-schema/src/parameters/tables.rs +++ b/pywr-schema/src/parameters/tables.rs @@ -1,10 +1,9 @@ -use crate::error::ConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; #[cfg(feature = "core")] use crate::model::LoadArgs; use crate::parameters::{ConversionData, ParameterMeta}; -use crate::v1::{IntoV2, TryFromV1}; +use crate::v1::{FromV1, IntoV2}; #[cfg(feature = "core")] use ndarray::s; #[cfg(feature = "core")] @@ -88,15 +87,9 @@ impl TablesArrayParameter { } } -impl TryFromV1 for TablesArrayParameter { - type Error = ConversionError; - - fn try_from_v1( - v1: TablesArrayParameterV1, - parent_node: Option<&str>, - conversion_data: &mut ConversionData, - ) -> Result { - let p = Self { +impl FromV1 for TablesArrayParameter { + fn from_v1(v1: TablesArrayParameterV1, parent_node: Option<&str>, conversion_data: &mut ConversionData) -> Self { + Self { meta: v1.meta.into_v2(parent_node, conversion_data), node: v1.node, wh: v1.wh, @@ -104,7 +97,6 @@ impl TryFromV1 for TablesArrayParameter { checksum: v1.checksum, url: v1.url, timestep_offset: None, - }; - Ok(p) + } } } diff --git a/pywr-schema/src/parameters/thresholds.rs b/pywr-schema/src/parameters/thresholds.rs index 83eb0d05..8ad8be00 100644 --- a/pywr-schema/src/parameters/thresholds.rs +++ b/pywr-schema/src/parameters/thresholds.rs @@ -1,11 +1,11 @@ -use crate::error::ConversionError; +use crate::error::ComponentConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; use crate::metric::Metric; #[cfg(feature = "core")] use crate::model::LoadArgs; use crate::parameters::{ConversionData, ParameterMeta}; -use crate::v1::{IntoV2, TryFromV1, TryIntoV2}; +use crate::v1::{try_convert_parameter_attr, IntoV2, TryFromV1}; #[cfg(feature = "core")] use pywr_core::parameters::ParameterIndex; use pywr_schema_macros::PywrVisitAll; @@ -86,7 +86,7 @@ impl ThresholdParameter { } impl TryFromV1 for ThresholdParameter { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: ParameterThresholdParameterV1, @@ -95,8 +95,9 @@ impl TryFromV1 for ThresholdParameter { ) -> Result { let meta: ParameterMeta = v1.meta.into_v2(parent_node, conversion_data); - let value = v1.parameter.try_into_v2(parent_node, conversion_data)?; - let threshold = v1.threshold.try_into_v2(parent_node, conversion_data)?; + let value = try_convert_parameter_attr(&meta.name, "parameter", v1.parameter, parent_node, conversion_data)?; + let threshold = + try_convert_parameter_attr(&meta.name, "threshold", v1.threshold, parent_node, conversion_data)?; // TODO warn or something about the lack of using the values here!! diff --git a/pywr-schema/src/timeseries/mod.rs b/pywr-schema/src/timeseries/mod.rs index 244cac13..b1361edb 100644 --- a/pywr-schema/src/timeseries/mod.rs +++ b/pywr-schema/src/timeseries/mod.rs @@ -3,6 +3,7 @@ mod align_and_resample; mod pandas; mod polars_dataset; +use crate::error::ComponentConversionError; use crate::parameters::ParameterMeta; use crate::v1::{ConversionData, IntoV2, TryFromV1}; use crate::visit::VisitPaths; @@ -320,7 +321,7 @@ impl TimeseriesReference { } impl TryFromV1 for TimeseriesReference { - type Error = ConversionError; + type Error = ComponentConversionError; fn try_from_v1( v1: DataFrameParameterV1, @@ -357,9 +358,12 @@ impl TryFromV1 for TimeseriesReference { } else if v1.table.is_some() { // Nothing to do here; The table will be converted separately } else { - return Err(ConversionError::MissingAttribute { - attrs: vec!["url".to_string(), "table".to_string()], + return Err(ComponentConversionError::Parameter { name: meta.name, + attr: "url".to_string(), + error: ConversionError::MissingAttribute { + attrs: vec!["url".to_string(), "table".to_string()], + }, }); }; @@ -367,7 +371,15 @@ impl TryFromV1 for TimeseriesReference { let columns = match (v1.column, v1.scenario) { (Some(col), None) => Some(TimeseriesColumns::Column(col)), (None, Some(scenario)) => Some(TimeseriesColumns::Scenario(scenario)), - (Some(_), Some(_)) => return Err(ConversionError::AmbiguousColumnAndScenario(meta.name)), + (Some(_), Some(_)) => { + return Err(ComponentConversionError::Parameter { + name: meta.name.clone(), + attr: "column".to_string(), + error: ConversionError::AmbiguousAttributes { + attrs: vec!["column".to_string(), "scenario".to_string()], + }, + }) + } (None, None) => None, }; // The reference that is returned diff --git a/pywr-schema/src/v1.rs b/pywr-schema/src/v1.rs index 509b5cf1..12b4baa4 100644 --- a/pywr-schema/src/v1.rs +++ b/pywr-schema/src/v1.rs @@ -10,9 +10,15 @@ //! It also tracks a count of unnamed parameters and timeseries. This is used during conversion //! of meta-data to provide a unique name for unnamed parameters and timeseries. -use crate::parameters::{Parameter, ParameterMeta}; +use crate::error::ComponentConversionError; +use crate::metric::Metric; +use crate::nodes::StorageInitialVolume; +use crate::parameters::{ConstantFloatVec, Parameter, ParameterMeta}; use crate::timeseries::Timeseries; -use pywr_v1_schema::parameters::ParameterMeta as ParameterMetaV1; +use crate::ConversionError; +use pywr_v1_schema::parameters::{ + ExternalDataRef, ParameterMeta as ParameterMetaV1, ParameterValue, ParameterValues, TableDataRef, +}; /// Counters for unnamed parameters and timeseries. #[derive(Default)] @@ -36,6 +42,15 @@ pub trait IntoV2 { fn into_v2(self, parent_node: Option<&str>, conversion_data: &mut ConversionData) -> T; } +impl FromV1> for Option +where + T: FromV1, +{ + fn from_v1(v1: Option, parent_node: Option<&str>, conversion_data: &mut ConversionData) -> Self { + v1.map(|v| v.into_v2(parent_node, conversion_data)) + } +} + // FromV1Parameter implies IntoV2Parameter impl IntoV2 for T where @@ -51,6 +66,19 @@ pub trait TryFromV1: Sized { fn try_from_v1(v1: T, parent_node: Option<&str>, conversion_data: &mut ConversionData) -> Result; } +impl TryFromV1> for Option +where + T: TryFromV1, +{ + type Error = T::Error; + fn try_from_v1( + v1: Option, + parent_node: Option<&str>, + conversion_data: &mut ConversionData, + ) -> Result { + v1.map(|v| v.try_into_v2(parent_node, conversion_data)).transpose() + } +} pub trait TryIntoV2 { type Error; @@ -100,3 +128,140 @@ impl FromV1> for ParameterMeta { } } } + +/// Helper function to convert a node attribute from v1 to v2. +pub fn try_convert_node_attr( + name: &str, + attr: &str, + v1_value: V1, + parent_node: Option<&str>, + conversion_data: &mut ConversionData, +) -> Result +where + V1: TryIntoV2, +{ + v1_value + .try_into_v2(parent_node.or(Some(name)), conversion_data) + .map_err(|error| ComponentConversionError::Node { + attr: attr.to_string(), + name: name.to_string(), + error, + }) +} + +/// Helper function to convert a parameter attribute from v1 to v2. +pub fn try_convert_parameter_attr( + name: &str, + attr: &str, + v1_value: V1, + parent_node: Option<&str>, + conversion_data: &mut ConversionData, +) -> Result +where + V1: TryIntoV2, +{ + v1_value + .try_into_v2(parent_node.or(Some(name)), conversion_data) + .map_err(|error| ComponentConversionError::Parameter { + attr: attr.to_string(), + name: name.to_string(), + error, + }) +} + +/// Helper function to convert initial storage from v1 to v2. +pub fn try_convert_initial_storage( + name: &str, + attr: &str, + v1_initial_volume: Option, + v1_initial_volume_pc: Option, +) -> Result { + let initial_volume = if let Some(v) = v1_initial_volume { + StorageInitialVolume::Absolute(v) + } else if let Some(v) = v1_initial_volume_pc { + StorageInitialVolume::Proportional(v) + } else { + return Err(ComponentConversionError::Node { + attr: attr.to_string(), + name: name.to_string(), + error: ConversionError::MissingAttribute { + attrs: vec!["initial_volume".to_string(), "initial_volume_pc".to_string()], + }, + }); + }; + + Ok(initial_volume) +} + +pub fn try_convert_values( + name: &str, + v1_values: Option>, + v1_external: Option, + v1_table_ref: Option, +) -> Result { + let values: ConstantFloatVec = if let Some(values) = v1_values { + ConstantFloatVec::Literal(values) + } else if let Some(_external) = v1_external { + return Err(ComponentConversionError::Parameter { + name: name.to_string(), + attr: "url".to_string(), + error: ConversionError::UnsupportedFeature { + feature: "External data references are not supported in Pywr v2. Please use a table instead." + .to_string(), + }, + }); + } else if let Some(table_ref) = v1_table_ref { + ConstantFloatVec::Table( + table_ref + .try_into() + .map_err(|error| ComponentConversionError::Parameter { + name: name.to_string(), + attr: "table".to_string(), + error, + })?, + ) + } else { + return Err(ComponentConversionError::Parameter { + name: name.to_string(), + attr: "table".to_string(), + error: ConversionError::MissingAttribute { + attrs: vec!["values".to_string(), "table".to_string(), "url".to_string()], + }, + }); + }; + + Ok(values) +} + +pub fn try_convert_control_curves( + name: &str, + v1_control_curves: Option, + v1_control_curve: Option, + parent_node: Option<&str>, + conversion_data: &mut ConversionData, +) -> Result, ComponentConversionError> { + let control_curves = if let Some(control_curves) = v1_control_curves { + control_curves + .into_iter() + .map(|p| try_convert_parameter_attr(name, "control_curves", p, parent_node, conversion_data)) + .collect::, _>>()? + } else if let Some(control_curve) = v1_control_curve { + vec![try_convert_parameter_attr( + name, + "control_curve", + control_curve, + parent_node, + conversion_data, + )?] + } else { + return Err(ComponentConversionError::Parameter { + name: name.to_string(), + attr: "control_curves".to_string(), + error: ConversionError::MissingAttribute { + attrs: vec!["control_curves".to_string(), "control_curve".to_string()], + }, + }); + }; + + Ok(control_curves) +}