From 4d618df27b3029b8a7de06cfc1de7fc000335e15 Mon Sep 17 00:00:00 2001 From: James Tomlinson Date: Thu, 21 Mar 2024 12:14:21 +0000 Subject: [PATCH] feat(core): Add methods for name, sub_name and attribute to Metrics. Move some replicated code from the CSV and HDF recorders to the metric definition. This removes a lot of TODOs and unimplemented metrics for both outputs. --- pywr-core/src/derived_metric.rs | 29 +++++++++ pywr-core/src/metric.rs | 86 +++++++++++++++++++++++++++ pywr-core/src/network.rs | 29 ++++++++- pywr-core/src/recorders/csv.rs | 75 ++--------------------- pywr-core/src/recorders/hdf.rs | 63 +++----------------- pywr-core/src/recorders/metric_set.rs | 4 +- 6 files changed, 156 insertions(+), 130 deletions(-) diff --git a/pywr-core/src/derived_metric.rs b/pywr-core/src/derived_metric.rs index 767d609f..e0034aa7 100644 --- a/pywr-core/src/derived_metric.rs +++ b/pywr-core/src/derived_metric.rs @@ -93,4 +93,33 @@ impl DerivedMetric { } } } + + pub fn name<'a>(&self, network: &'a Network) -> Result<&'a str, PywrError> { + match self { + Self::NodeInFlowDeficit(idx) | Self::NodeProportionalVolume(idx) => network.get_node(idx).map(|n| n.name()), + Self::AggregatedNodeProportionalVolume(idx) => network.get_aggregated_storage_node(idx).map(|n| n.name()), + Self::VirtualStorageProportionalVolume(idx) => network.get_virtual_storage_node(idx).map(|v| v.name()), + } + } + + pub fn sub_name<'a>(&self, network: &'a Network) -> Result, PywrError> { + match self { + Self::NodeInFlowDeficit(idx) | Self::NodeProportionalVolume(idx) => { + network.get_node(idx).map(|n| n.sub_name()) + } + Self::AggregatedNodeProportionalVolume(idx) => { + network.get_aggregated_storage_node(idx).map(|n| n.sub_name()) + } + Self::VirtualStorageProportionalVolume(idx) => network.get_virtual_storage_node(idx).map(|v| v.sub_name()), + } + } + + pub fn attribute(&self) -> &str { + match self { + Self::NodeInFlowDeficit(_) => "in_flow_deficit", + Self::NodeProportionalVolume(_) => "proportional_volume", + Self::AggregatedNodeProportionalVolume(_) => "proportional_volume", + Self::VirtualStorageProportionalVolume(_) => "proportional_volume", + } + } } diff --git a/pywr-core/src/metric.rs b/pywr-core/src/metric.rs index d140ecae..ea59b715 100644 --- a/pywr-core/src/metric.rs +++ b/pywr-core/src/metric.rs @@ -9,6 +9,7 @@ use crate::parameters::ParameterIndex; use crate::state::{MultiValue, State}; use crate::virtual_storage::VirtualStorageIndex; use crate::PywrError; + #[derive(Clone, Debug, PartialEq)] pub enum MetricF64 { NodeInFlow(NodeIndex), @@ -83,6 +84,70 @@ impl MetricF64 { MetricF64::InterNetworkTransfer(idx) => state.get_inter_network_transfer_value(*idx), } } + pub fn name<'a>(&'a self, network: &'a Network) -> Result<&'a str, PywrError> { + match self { + Self::NodeInFlow(idx) | Self::NodeOutFlow(idx) | Self::NodeVolume(idx) => { + network.get_node(idx).map(|n| n.name()) + } + Self::AggregatedNodeInFlow(idx) | Self::AggregatedNodeOutFlow(idx) => { + network.get_aggregated_node(idx).map(|n| n.name()) + } + Self::AggregatedNodeVolume(idx) => network.get_aggregated_storage_node(idx).map(|n| n.name()), + Self::EdgeFlow(idx) => { + let edge = network.get_edge(idx)?; + network.get_node(&edge.from_node_index).map(|n| n.name()) + } + Self::ParameterValue(idx) => network.get_parameter(idx).map(|p| p.name()), + Self::MultiParameterValue((idx, _)) => network.get_multi_valued_parameter(idx).map(|p| p.name()), + Self::VirtualStorageVolume(idx) => network.get_virtual_storage_node(idx).map(|v| v.name()), + Self::MultiNodeInFlow { name, .. } | Self::MultiNodeOutFlow { name, .. } => Ok(name), + Self::Constant(_) => Ok(""), + Self::DerivedMetric(idx) => network.get_derived_metric(idx)?.name(network), + Self::InterNetworkTransfer(_) => todo!("InterNetworkTransfer name is not implemented"), + } + } + + pub fn sub_name<'a>(&'a self, network: &'a Network) -> Result, PywrError> { + match self { + Self::NodeInFlow(idx) | Self::NodeOutFlow(idx) | Self::NodeVolume(idx) => { + network.get_node(idx).map(|n| n.sub_name()) + } + Self::AggregatedNodeInFlow(idx) | Self::AggregatedNodeOutFlow(idx) => { + network.get_aggregated_node(idx).map(|n| n.sub_name()) + } + Self::AggregatedNodeVolume(idx) => network.get_aggregated_storage_node(idx).map(|n| n.sub_name()), + Self::EdgeFlow(idx) => { + let edge = network.get_edge(idx)?; + network.get_node(&edge.to_node_index).map(|n| Some(n.name())) + } + Self::ParameterValue(_) | Self::MultiParameterValue(_) => Ok(None), + Self::VirtualStorageVolume(idx) => network.get_virtual_storage_node(idx).map(|v| v.sub_name()), + Self::MultiNodeInFlow { .. } | Self::MultiNodeOutFlow { .. } => Ok(None), + Self::Constant(_) => Ok(None), + Self::DerivedMetric(idx) => network.get_derived_metric(idx)?.sub_name(network), + Self::InterNetworkTransfer(_) => todo!("InterNetworkTransfer sub_name is not implemented"), + } + } + + pub fn attribute(&self) -> &str { + match self { + Self::NodeInFlow(_) => "inflow", + Self::NodeOutFlow(_) => "outflow", + Self::NodeVolume(_) => "volume", + Self::AggregatedNodeInFlow(_) => "inflow", + Self::AggregatedNodeOutFlow(_) => "outflow", + Self::AggregatedNodeVolume(_) => "volume", + Self::EdgeFlow(_) => "edge_flow", + Self::ParameterValue(_) => "value", + Self::MultiParameterValue(_) => "value", + Self::VirtualStorageVolume(_) => "volume", + Self::MultiNodeInFlow { .. } => "inflow", + Self::MultiNodeOutFlow { .. } => "outflow", + Self::Constant(_) => "value", + Self::DerivedMetric(_) => "value", + Self::InterNetworkTransfer(_) => "value", + } + } } #[derive(Clone, Debug, PartialEq)] @@ -98,4 +163,25 @@ impl MetricUsize { Self::Constant(i) => Ok(*i), } } + + pub fn name<'a>(&'a self, network: &'a Network) -> Result<&'a str, PywrError> { + match self { + Self::IndexParameterValue(idx) => network.get_index_parameter(idx).map(|p| p.name()), + Self::Constant(_) => Ok(""), + } + } + + pub fn sub_name<'a>(&'a self, _network: &'a Network) -> Result, PywrError> { + match self { + Self::IndexParameterValue(_) => Ok(None), + Self::Constant(_) => Ok(None), + } + } + + pub fn attribute(&self) -> &str { + match self { + Self::IndexParameterValue(_) => "value", + Self::Constant(_) => "value", + } + } } diff --git a/pywr-core/src/network.rs b/pywr-core/src/network.rs index c9b66090..d32e24e2 100644 --- a/pywr-core/src/network.rs +++ b/pywr-core/src/network.rs @@ -1,7 +1,7 @@ use crate::aggregated_node::{AggregatedNode, AggregatedNodeIndex, AggregatedNodeVec, Factors}; use crate::aggregated_storage_node::{AggregatedStorageNode, AggregatedStorageNodeIndex, AggregatedStorageNodeVec}; use crate::derived_metric::{DerivedMetric, DerivedMetricIndex}; -use crate::edge::{EdgeIndex, EdgeVec}; +use crate::edge::{Edge, EdgeIndex, EdgeVec}; use crate::metric::MetricF64; use crate::models::ModelDomain; use crate::node::{ConstraintValue, Node, NodeVec, StorageInitialVolume}; @@ -794,6 +794,11 @@ impl Network { Ok(()) } + /// Get an [`Edge`] from an edge's index + pub fn get_edge(&self, index: &EdgeIndex) -> Result<&Edge, PywrError> { + self.edges.get(index) + } + /// Get a Node from a node's name pub fn get_node_index_by_name(&self, name: &str, sub_name: Option<&str>) -> Result { Ok(self.get_node_by_name(name, sub_name)?.index()) @@ -1123,6 +1128,17 @@ impl Network { } } + /// Get a [`Parameter`] from its index. + pub fn get_index_parameter( + &self, + index: &ParameterIndex, + ) -> Result<&dyn parameters::Parameter, PywrError> { + match self.index_parameters.get(*index.deref()) { + Some(p) => Ok(p.as_ref()), + None => Err(PywrError::IndexParameterIndexNotFound(*index)), + } + } + /// Get a `IndexParameter` from a parameter's name pub fn get_index_parameter_by_name(&self, name: &str) -> Result<&dyn parameters::Parameter, PywrError> { match self.index_parameters.iter().find(|p| p.name() == name) { @@ -1139,6 +1155,17 @@ impl Network { } } + /// Get a `MultiValueParameterIndex` from a parameter's name + pub fn get_multi_valued_parameter( + &self, + index: &ParameterIndex, + ) -> Result<&dyn parameters::Parameter, PywrError> { + match self.multi_parameters.get(*index.deref()) { + Some(p) => Ok(p.as_ref()), + None => Err(PywrError::MultiValueParameterIndexNotFound(*index)), + } + } + /// Get a `MultiValueParameterIndex` from a parameter's name pub fn get_multi_valued_parameter_index_by_name( &self, diff --git a/pywr-core/src/recorders/csv.rs b/pywr-core/src/recorders/csv.rs index 99b709b0..bdd357e2 100644 --- a/pywr-core/src/recorders/csv.rs +++ b/pywr-core/src/recorders/csv.rs @@ -1,5 +1,4 @@ use super::{MetricSetState, PywrError, Recorder, RecorderMeta, Timestep}; -use crate::metric::MetricF64; use crate::models::ModelDomain; use crate::network::Network; use crate::recorders::metric_set::MetricSetIndex; @@ -46,75 +45,11 @@ impl Recorder for CSVRecorder { let metric_set = network.get_metric_set(self.metric_set_idx)?; for metric in metric_set.iter_metrics() { - let (name, sub_name, attribute) = match metric { - MetricF64::NodeInFlow(idx) => { - let node = network.get_node(idx)?; - let (name, sub_name) = node.full_name(); - let sub_name = sub_name.map_or("".to_string(), |sn| sn.to_string()); - - (name.to_string(), sub_name, "inflow".to_string()) - } - MetricF64::NodeOutFlow(idx) => { - let node = network.get_node(idx)?; - let (name, sub_name) = node.full_name(); - let sub_name = sub_name.map_or("".to_string(), |sn| sn.to_string()); - - (name.to_string(), sub_name, "outflow".to_string()) - } - MetricF64::NodeVolume(idx) => { - let node = network.get_node(idx)?; - let (name, sub_name) = node.full_name(); - let sub_name = sub_name.map_or("".to_string(), |sn| sn.to_string()); - - (name.to_string(), sub_name, "volume".to_string()) - } - MetricF64::DerivedMetric(_idx) => { - todo!("Derived metrics are not yet supported in CSV recorders"); - } - MetricF64::AggregatedNodeVolume(idx) => { - let node = network.get_aggregated_storage_node(idx)?; - let (name, sub_name) = node.full_name(); - let sub_name = sub_name.map_or("".to_string(), |sn| sn.to_string()); - - (name.to_string(), sub_name, "volume".to_string()) - } - MetricF64::EdgeFlow(_) => { - continue; // TODO - } - MetricF64::ParameterValue(idx) => { - let parameter = network.get_parameter(idx)?; - let name = parameter.name(); - (name.to_string(), "".to_string(), "parameter".to_string()) - } - MetricF64::VirtualStorageVolume(_) => { - continue; // TODO - } - MetricF64::Constant(_) => { - continue; // TODO - } - MetricF64::MultiParameterValue(_) => { - continue; // TODO - } - MetricF64::AggregatedNodeInFlow(idx) => { - let node = network.get_aggregated_node(idx)?; - let (name, sub_name) = node.full_name(); - let sub_name = sub_name.map_or("".to_string(), |sn| sn.to_string()); - - (name.to_string(), sub_name, "inflow".to_string()) - } - MetricF64::AggregatedNodeOutFlow(idx) => { - let node = network.get_aggregated_node(idx)?; - let (name, sub_name) = node.full_name(); - let sub_name = sub_name.map_or("".to_string(), |sn| sn.to_string()); - - (name.to_string(), sub_name, "outflow".to_string()) - } - MetricF64::MultiNodeInFlow { name, .. } => (name.to_string(), "".to_string(), "inflow".to_string()), - MetricF64::MultiNodeOutFlow { name, .. } => (name.to_string(), "".to_string(), "outflow".to_string()), - MetricF64::InterNetworkTransfer(_) => { - continue; // TODO - } - }; + let name = metric.name(network)?.to_string(); + let sub_name = metric + .sub_name(network)? + .map_or_else(|| "".to_string(), |s| s.to_string()); + let attribute = metric.attribute().to_string(); // Add entries for each scenario names.push(name); diff --git a/pywr-core/src/recorders/hdf.rs b/pywr-core/src/recorders/hdf.rs index 2c05e247..ccb6dbe3 100644 --- a/pywr-core/src/recorders/hdf.rs +++ b/pywr-core/src/recorders/hdf.rs @@ -1,5 +1,4 @@ use super::{MetricSetState, PywrError, Recorder, RecorderMeta, Timestep}; -use crate::metric::MetricF64; use crate::models::ModelDomain; use crate::network::Network; use crate::recorders::MetricSetIndex; @@ -65,7 +64,7 @@ impl Recorder for HDF5Recorder { fn meta(&self) -> &RecorderMeta { &self.meta } - fn setup(&self, domain: &ModelDomain, model: &Network) -> Result>, PywrError> { + fn setup(&self, domain: &ModelDomain, network: &Network) -> Result>, PywrError> { let file = match hdf5::File::create(&self.filename) { Ok(f) => f, Err(e) => return Err(PywrError::HDF5Error(e.to_string())), @@ -82,62 +81,14 @@ impl Recorder for HDF5Recorder { let root_grp = file.deref(); - let metric_set = model.get_metric_set(self.metric_set_idx)?; + let metric_set = network.get_metric_set(self.metric_set_idx)?; for metric in metric_set.iter_metrics() { - let ds = match metric { - MetricF64::NodeInFlow(idx) => { - let node = model.get_node(idx)?; - require_node_dataset(root_grp, shape, node.name(), node.sub_name(), "inflow")? - } - MetricF64::NodeOutFlow(idx) => { - let node = model.get_node(idx)?; - require_node_dataset(root_grp, shape, node.name(), node.sub_name(), "outflow")? - } - MetricF64::NodeVolume(idx) => { - let node = model.get_node(idx)?; - require_node_dataset(root_grp, shape, node.name(), node.sub_name(), "volume")? - } - MetricF64::DerivedMetric(_idx) => { - todo!("Derived metrics are not yet supported in HDF recorders"); - } - MetricF64::AggregatedNodeVolume(idx) => { - let node = model.get_aggregated_storage_node(idx)?; - require_node_dataset(root_grp, shape, node.name(), node.sub_name(), "volume")? - } - MetricF64::EdgeFlow(_) => { - continue; // TODO - } - MetricF64::ParameterValue(idx) => { - let parameter = model.get_parameter(idx)?; - let parameter_group = require_group(root_grp, "parameters")?; - require_dataset(¶meter_group, shape, parameter.name())? - } - MetricF64::VirtualStorageVolume(_) => { - continue; // TODO - } - MetricF64::Constant(_) => { - continue; // TODO - } - MetricF64::MultiParameterValue(_) => { - continue; // TODO - } - MetricF64::AggregatedNodeInFlow(idx) => { - let node = model.get_aggregated_node(idx)?; - require_node_dataset(root_grp, shape, node.name(), node.sub_name(), "inflow")? - } - MetricF64::AggregatedNodeOutFlow(idx) => { - let node = model.get_aggregated_node(idx)?; - require_node_dataset(root_grp, shape, node.name(), node.sub_name(), "outflow")? - } - MetricF64::MultiNodeInFlow { name, .. } => require_node_dataset(root_grp, shape, name, None, "inflow")?, - MetricF64::MultiNodeOutFlow { name, .. } => { - require_node_dataset(root_grp, shape, name, None, "outflow")? - } - MetricF64::InterNetworkTransfer(_) => { - continue; // TODO - } - }; + let name = metric.name(network)?; + let sub_name = metric.sub_name(network)?; + let attribute = metric.attribute(); + + let ds = require_node_dataset(root_grp, shape, name, sub_name, attribute)?; datasets.push(ds); } diff --git a/pywr-core/src/recorders/metric_set.rs b/pywr-core/src/recorders/metric_set.rs index 2b2fb81e..aeee6b07 100644 --- a/pywr-core/src/recorders/metric_set.rs +++ b/pywr-core/src/recorders/metric_set.rs @@ -5,11 +5,9 @@ use crate::scenario::ScenarioIndex; use crate::state::State; use crate::timestep::Timestep; use crate::PywrError; -use core::f64; use std::fmt; use std::fmt::{Display, Formatter}; use std::ops::Deref; -use std::slice::Iter; #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] pub struct MetricSetIndex(usize); @@ -69,7 +67,7 @@ impl MetricSet { pub fn name(&self) -> &str { &self.name } - pub fn iter_metrics(&self) -> Iter<'_, MetricF64> { + pub fn iter_metrics(&self) -> impl Iterator + '_ { self.metrics.iter() }