Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Initial commit of DerivedMetric #68

Merged
merged 1 commit into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions pywr-core/src/derived_metric.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use crate::aggregated_storage_node::AggregatedStorageNodeIndex;
use crate::model::Model;
use crate::node::NodeIndex;
use crate::state::State;
use crate::timestep::Timestep;
use crate::virtual_storage::VirtualStorageIndex;
use crate::PywrError;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::ops::Deref;

#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug)]
pub struct DerivedMetricIndex(usize);

impl Deref for DerivedMetricIndex {
type Target = usize;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl DerivedMetricIndex {
pub fn new(idx: usize) -> Self {
Self(idx)
}
}

impl Display for DerivedMetricIndex {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}

/// Derived metrics are updated after the model is solved.
///
/// These metrics are "derived" from node states (e.g. volume, flow) and must be updated
/// after those states have been updated. This should happen after the model is solved. The values
/// are then available in this state for the next time-step.
#[derive(Clone, Debug, PartialEq)]
pub enum DerivedMetric {
NodeInFlowDeficit(NodeIndex),
NodeProportionalVolume(NodeIndex),
AggregatedNodeProportionalVolume(AggregatedStorageNodeIndex),
VirtualStorageProportionalVolume(VirtualStorageIndex),
}

impl DerivedMetric {
pub fn before(&self, timestep: &Timestep, model: &Model, state: &State) -> Result<Option<f64>, PywrError> {
// On the first time-step set the initial value
if timestep.is_first() {
self.compute(model, state).map(|v| Some(v))
} else {
Ok(None)
}
}

pub fn compute(&self, model: &Model, state: &State) -> Result<f64, PywrError> {
match self {
Self::NodeProportionalVolume(idx) => {
let max_volume = model.get_node(idx)?.get_current_max_volume(model, state)?;
Ok(state
.get_network_state()
.get_node_proportional_volume(idx, max_volume)?)
}
Self::VirtualStorageProportionalVolume(idx) => {
let max_volume = model.get_virtual_storage_node(idx)?.get_max_volume(model, state)?;
Ok(state
.get_network_state()
.get_virtual_storage_proportional_volume(idx, max_volume)?)
}
Self::AggregatedNodeProportionalVolume(idx) => {
let node = model.get_aggregated_storage_node(idx)?;
let volume: f64 = node
.nodes
.iter()
.map(|idx| state.get_network_state().get_node_volume(idx))
.sum::<Result<_, _>>()?;

let max_volume: f64 = node
.nodes
.iter()
.map(|idx| model.get_node(idx)?.get_current_max_volume(model, state))
.sum::<Result<_, _>>()?;
// TODO handle divide by zero
Ok(volume / max_volume)
}
Self::NodeInFlowDeficit(idx) => {
let node = model.get_node(idx)?;
let flow = state.get_network_state().get_node_in_flow(idx)?;
let max_flow = node.get_current_max_flow(model, state)?;
Ok(max_flow - flow)
}
}
}
}
8 changes: 8 additions & 0 deletions pywr-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

extern crate core;

use crate::derived_metric::DerivedMetricIndex;
use crate::node::NodeIndex;
use crate::parameters::{IndexParameterIndex, MultiValueParameterIndex, ParameterIndex};
use crate::recorders::RecorderIndex;
Expand All @@ -11,6 +12,7 @@ use thiserror::Error;

pub mod aggregated_node;
mod aggregated_storage_node;
pub mod derived_metric;
pub mod edge;
pub mod metric;
pub mod model;
Expand Down Expand Up @@ -55,6 +57,10 @@ pub enum PywrError {
RecorderIndexNotFound,
#[error("recorder not found")]
RecorderNotFound,
#[error("derived metric not found")]
DerivedMetricNotFound,
#[error("derived metric index {0} not found")]
DerivedMetricIndexNotFound(DerivedMetricIndex),
#[error("node name `{0}` already exists")]
NodeNameAlreadyExists(String),
#[error("parameter name `{0}` already exists at index {1}")]
Expand Down Expand Up @@ -127,6 +133,8 @@ pub enum PywrError {
ParameterVariableValuesIncorrectLength,
#[error("missing solver features")]
MissingSolverFeatures,
#[error("parameters do not provide an initial value")]
ParameterNoInitialValue,
}

// Python errors
Expand Down
89 changes: 19 additions & 70 deletions pywr-core/src/metric.rs
Original file line number Diff line number Diff line change
@@ -1,54 +1,33 @@
use crate::aggregated_node::AggregatedNodeIndex;
use crate::aggregated_storage_node::AggregatedStorageNodeIndex;
use crate::derived_metric::DerivedMetricIndex;
use crate::edge::EdgeIndex;
use crate::model::Model;
use crate::node::NodeIndex;
use crate::parameters::{MultiValueParameterIndex, ParameterIndex};
use crate::parameters::{IndexParameterIndex, MultiValueParameterIndex, ParameterIndex};
use crate::state::State;
use crate::virtual_storage::VirtualStorageIndex;
use crate::PywrError;

#[derive(Clone, Debug, PartialEq)]
pub struct VolumeBetweenControlCurves {
max_volume: Box<Metric>,
upper: Option<Box<Metric>>,
lower: Option<Box<Metric>>,
}

impl VolumeBetweenControlCurves {
pub fn new(max_volume: Metric, upper: Option<Metric>, lower: Option<Metric>) -> Self {
Self {
max_volume: Box::new(max_volume),
upper: upper.map(Box::new),
lower: lower.map(Box::new),
}
}
}

#[derive(Clone, Debug, PartialEq)]
pub enum Metric {
NodeInFlow(NodeIndex),
NodeOutFlow(NodeIndex),
NodeVolume(NodeIndex),
NodeInFlowDeficit(NodeIndex),
NodeProportionalVolume(NodeIndex),
AggregatedNodeInFlow(AggregatedNodeIndex),
AggregatedNodeOutFlow(AggregatedNodeIndex),
AggregatedNodeVolume(AggregatedStorageNodeIndex),
AggregatedNodeProportionalVolume(AggregatedStorageNodeIndex),
EdgeFlow(EdgeIndex),
ParameterValue(ParameterIndex),
MultiParameterValue((MultiValueParameterIndex, String)),
VirtualStorageVolume(VirtualStorageIndex),
VirtualStorageProportionalVolume(VirtualStorageIndex),
VolumeBetweenControlCurves(VolumeBetweenControlCurves),
MultiNodeInFlow {
indices: Vec<NodeIndex>,
name: String,
sub_name: Option<String>,
},
// TODO implement other MultiNodeXXX variants
Constant(f64),
DerivedMetric(DerivedMetricIndex),
}

impl Metric {
Expand All @@ -73,22 +52,12 @@ impl Metric {
.map(|idx| state.get_network_state().get_node_out_flow(idx))
.sum::<Result<_, _>>()
}
Metric::NodeProportionalVolume(idx) => {
let max_volume = model.get_node(idx)?.get_current_max_volume(model, state)?;
Ok(state
.get_network_state()
.get_node_proportional_volume(idx, max_volume)?)
}

Metric::EdgeFlow(idx) => Ok(state.get_network_state().get_edge_flow(idx)?),
Metric::ParameterValue(idx) => Ok(state.get_parameter_value(*idx)?),
Metric::MultiParameterValue((idx, key)) => Ok(state.get_multi_parameter_value(*idx, key)?),
Metric::VirtualStorageVolume(idx) => Ok(state.get_network_state().get_virtual_storage_volume(idx)?),
Metric::VirtualStorageProportionalVolume(idx) => {
let max_volume = model.get_virtual_storage_node(idx)?.get_max_volume(model, state)?;
Ok(state
.get_network_state()
.get_virtual_storage_proportional_volume(idx, max_volume)?)
}
Metric::DerivedMetric(idx) => state.get_derived_metric_value(*idx),
Metric::Constant(v) => Ok(*v),
Metric::AggregatedNodeVolume(idx) => {
let node = model.get_aggregated_storage_node(idx)?;
Expand All @@ -97,49 +66,29 @@ impl Metric {
.map(|idx| state.get_network_state().get_node_volume(idx))
.sum::<Result<_, _>>()
}
Metric::AggregatedNodeProportionalVolume(idx) => {
let node = model.get_aggregated_storage_node(idx)?;
let volume: f64 = node
.nodes
.iter()
.map(|idx| state.get_network_state().get_node_volume(idx))
.sum::<Result<_, _>>()?;

let max_volume: f64 = node
.nodes
.iter()
.map(|idx| model.get_node(idx)?.get_current_max_volume(model, state))
.sum::<Result<_, _>>()?;
// TODO handle divide by zero
Ok(volume / max_volume)
}
Metric::MultiNodeInFlow { indices, .. } => {
let flow = indices
.iter()
.map(|idx| state.get_network_state().get_node_in_flow(idx))
.sum::<Result<_, _>>()?;
Ok(flow)
}
Metric::NodeInFlowDeficit(idx) => {
let node = model.get_node(idx)?;
let flow = state.get_network_state().get_node_in_flow(idx)?;
let max_flow = node.get_current_max_flow(model, state)?;
Ok(max_flow - flow)
}
Metric::VolumeBetweenControlCurves(vol) => {
let max_volume = vol.max_volume.get_value(model, state)?;
let lower = vol
.lower
.as_ref()
.map_or(Ok(0.0), |metric| metric.get_value(model, state))?;
let upper = vol
.upper
.as_ref()
.map_or(Ok(1.0), |metric| metric.get_value(model, state))?;
}
}
}

// TODO handle invalid bounds
Ok(max_volume * (upper - lower))
}
#[derive(Clone, Debug, PartialEq)]
pub enum IndexMetric {
IndexParameterValue(IndexParameterIndex),
Constant(usize),
}

impl IndexMetric {
pub fn get_value(&self, model: &Model, state: &State) -> Result<usize, PywrError> {
match self {
Self::IndexParameterValue(idx) => state.get_parameter_index(*idx),
Self::Constant(i) => Ok(*i),
}
}
}
Loading