diff --git a/pywr-schema/src/metric.rs b/pywr-schema/src/metric.rs index d2fc8ecc..18f64665 100644 --- a/pywr-schema/src/metric.rs +++ b/pywr-schema/src/metric.rs @@ -297,11 +297,11 @@ impl NodeReference { } } -impl From for NodeReference{ - fn from (v: String) -> Self { +impl From for NodeReference { + fn from(v: String) -> Self { NodeReference { name: v, - attribute: None + attribute: None, } } } diff --git a/pywr-schema/src/model.rs b/pywr-schema/src/model.rs index cffdb37b..f509507d 100644 --- a/pywr-schema/src/model.rs +++ b/pywr-schema/src/model.rs @@ -5,7 +5,7 @@ use crate::data_tables::DataTable; #[cfg(feature = "core")] use crate::data_tables::LoadedTableCollection; use crate::error::{ConversionError, SchemaError}; -use crate::metric::Metric; +use crate::metric::{Metric, TimeseriesColumns, TimeseriesReference}; use crate::metric_sets::MetricSet; use crate::nodes::NodeAndTimeseries; use crate::outputs::Output; @@ -283,14 +283,14 @@ impl PywrNetwork { .flatten() .collect::>(); - let nodes = nodes_and_ts.into_iter().map(|n| n.node).collect::>(); + let mut nodes = nodes_and_ts.into_iter().map(|n| n.node).collect::>(); let edges = match v1.edges { Some(edges) => edges.into_iter().map(|e| e.into()).collect(), None => Vec::new(), }; - let parameters = if let Some(v1_parameters) = v1.parameters { + let mut parameters = if let Some(v1_parameters) = v1.parameters { let mut unnamed_count: usize = 0; let (parameters, param_ts_data) = convert_parameter_v1_to_v2(v1_parameters, &mut unnamed_count, &mut errors); @@ -300,6 +300,34 @@ impl PywrNetwork { None }; + // closure to update a parameter ref with a timeseries ref when names match. + let update_to_ts_ref = &mut |m: &mut Metric| { + if let Metric::Parameter(p) = m { + let ts_ref = ts_data.iter().find(|ts| ts.name == Some(p.name.clone())); + if let Some(ts_ref) = ts_ref { + // The timeseries requires a name to be used as a reference + let name = match &ts_ref.name { + Some(n) => n.clone(), + None => return, + }; + + let cols = match (&ts_ref.column, &ts_ref.scenario) { + (Some(col), None) => TimeseriesColumns::Column(col.clone()), + (None, Some(scenario)) => TimeseriesColumns::Scenario(scenario.clone()), + (Some(_), Some(_)) => return, + (None, None) => return, + }; + + *m = Metric::Timeseries(TimeseriesReference::new(name, cols)); + } + } + }; + + nodes.visit_metrics_mut(update_to_ts_ref); + if let Some(p) = parameters.as_mut() { + p.visit_metrics_mut(update_to_ts_ref) + } + let timeseries = if !ts_data.is_empty() { let ts = convert_from_v1_data(ts_data, &v1.tables, &mut errors); Some(ts) @@ -977,6 +1005,26 @@ mod tests { panic!("Expected an error due to missing file: {str}"); } } + + #[test] + fn test_v1_conversion() { + let v1_str = include_str!("./test_models/v1/timeseries.json"); + let v1: pywr_v1_schema::PywrModel = serde_json::from_str(v1_str).unwrap(); + + let (v2, errors) = PywrModel::from_v1(v1); + + assert_eq!(errors.len(), 0); + + std::fs::write("tmp.json", serde_json::to_string_pretty(&v2).unwrap()).unwrap(); + + let v2_converted: serde_json::Value = + serde_json::from_str(&serde_json::to_string_pretty(&v2).unwrap()).unwrap(); + + let v2_expected: serde_json::Value = + serde_json::from_str(include_str!("./test_models/v1/timeseries-converted.json")).unwrap(); + + assert_eq!(v2_converted, v2_expected); + } } #[cfg(test)] @@ -988,6 +1036,7 @@ mod core_tests { use ndarray::{Array1, Array2, Axis}; use pywr_core::{metric::MetricF64, recorders::AssertionRecorder, solvers::ClpSolver, test_utils::run_all_solvers}; use std::path::PathBuf; + use std::str::FromStr; fn model_str() -> &'static str { include_str!("./test_models/simple1.json") diff --git a/pywr-schema/src/nodes/virtual_storage.rs b/pywr-schema/src/nodes/virtual_storage.rs index 2b563b8e..50f7e591 100644 --- a/pywr-schema/src/nodes/virtual_storage.rs +++ b/pywr-schema/src/nodes/virtual_storage.rs @@ -2,11 +2,11 @@ use crate::error::ConversionError; #[cfg(feature = "core")] use crate::error::SchemaError; use crate::metric::Metric; +use crate::metric::NodeReference; #[cfg(feature = "core")] use crate::model::LoadArgs; use crate::nodes::core::StorageInitialVolume; use crate::nodes::{NodeAttribute, NodeMeta}; -use crate::metric::NodeReference; use crate::parameters::TryIntoV2Parameter; #[cfg(feature = "core")] use pywr_core::{ diff --git a/pywr-schema/src/test_models/v1/timeseries-converted.json b/pywr-schema/src/test_models/v1/timeseries-converted.json new file mode 100644 index 00000000..8fae7648 --- /dev/null +++ b/pywr-schema/src/test_models/v1/timeseries-converted.json @@ -0,0 +1,116 @@ +{ + "metadata": { + "title": "Simple timeseries", + "description": null, + "minimum_version": null + }, + "timestepper": { + "start": "2021-01-01", + "end": "2021-01-31", + "timestep": 1 + }, + "scenarios": null, + "network": { + "nodes": [ + { + "type": "Input", + "name": "input1", + "max_flow": { + "type": "Timeseries", + "name": "inflow", + "columns": { + "type": "Column", + "name": "inflow1" + } + }, + "min_flow": null, + "cost": null + }, + { + "type": "Input", + "name": "input2", + "max_flow": { + "type": "Parameter", + "name": "factored_flow", + "key": null + }, + "min_flow": null, + "cost": null + }, + { + "type": "Link", + "name": "link1", + "max_flow": null, + "min_flow": null, + "cost": null + }, + { + "type": "Output", + "name": "output1", + "max_flow": { + "type": "Parameter", + "name": "demand", + "key": null + }, + "min_flow": null, + "cost": { + "type": "Constant", + "value": -10.0 + } + } + ], + "edges": [ + { + "from_node": "input1", + "to_node": "link1" + }, + { + "from_node": "input2", + "to_node": "link1" + }, + { + "from_node": "link1", + "to_node": "output1" + } + ], + "parameters": [ + { + "type": "Constant", + "name": "demand", + "value": 100.0 + }, + { + "type": "Aggregated", + "name": "factored_flow", + "agg_func": "product", + "metrics": [ + { + "type": "Timeseries", + "name": "inflow", + "columns": { + "type": "Column", + "name": "inflow1" + } + }, + { + "type": "Constant", + "value": 0.5 + } + ] + } + ], + "tables": null, + "timeseries": [ + { + "name": "inflow", + "provider": { + "type": "Polars", + "time_col": null, + "url": "inflow.csv" + } + } + ], + "metric_sets": null, + "outputs": null + } +} \ No newline at end of file diff --git a/pywr-schema/src/test_models/v1/timeseries.json b/pywr-schema/src/test_models/v1/timeseries.json new file mode 100644 index 00000000..9eda969d --- /dev/null +++ b/pywr-schema/src/test_models/v1/timeseries.json @@ -0,0 +1,59 @@ +{ + "metadata": { + "title": "Simple timeseries" + }, + "timestepper": { + "start": "2021-01-01", + "end": "2021-01-31", + "timestep": 1 + }, + "nodes": [ + { + "name": "input1", + "type": "Input", + "max_flow": "inflow" + }, + { + "name": "input2", + "type": "Input", + "max_flow": "factored_flow" + }, + { + "name": "link1", + "type": "Link" + }, + { + "name": "output1", + "type": "Output", + "max_flow": "demand", + "cost": -10 + } + ], + "edges": [ + ["input1", "link1"], + ["input2", "link1"], + ["link1", "output1"] + ], + "parameters": { + "demand": { + "type": "constant", + "value": 100.0 + }, + "inflow": { + "type": "dataframe", + "url" : "inflow.csv", + "parse_dates": true, + "dayfirst": true, + "index_col": 0, + "column": "inflow1" + }, + "factored_flow": { + "type": "aggregated", + "agg_func":"product", + "parameters": [ + "inflow", + 0.5 + ] + } + } +}