Adding a new parameter to Pywr.
This guide explains how to add a new parameter to Pywr.
When to add a new parameter?
New parameters can be added to complement the existing parameters in Pywr. These parameters should be generic and reusable across a wide range of models. By adding them to Pywr itself other users are able to use them in their models without having to implement them themselves. They are also typically implemented in Rust, which means they are fast and efficient.
If the parameter is specific to a particular model or data set, it is better to implement it in the model itself
using a custom parameter.
Custom parameters can be added using, for example, the PythonParameter.
Adding a new parameter
To add new parameter to Pywr you need to do two things:
- Add the implementation to the
pywr-corecrate, and - Add the schema definition to the
pywr-schemacrate.
Adding the implementation to pywr-core
The implementation of the parameter should be added to the pywr-core crate.
This is typically done by adding a new module to the parameters module in the src directory.
It is a good idea to follow the existing structure of the parameters module by making a new module for the new
parameter.
Developers can follow the existing parameters as examples.
In this example, we will add a new parameter called MaxParameter that calculates the maximum value of a metric.
Parameters can depend on other parameters or values from the model via the MetricF64 type.
In this case the metric field stores a MetricF64 that will be compared with the threshold field
to calculate the maximum value.
The threshold is a constant value that is set when the parameter is created.
Finally, the meta field stores the metadata for the parameter.
The ParameterMeta struct is used to store the metadata for all parameters and can be reused.
#![allow(dead_code)]
use pywr_core::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64};
use pywr_core::network::ResolutionMaps;
use pywr_core::parameters::{BuiltParameter, GeneralBeforeParameter, GeneralAfterParameter, GeneralCalculationError, GeneralParameter, GeneralParameterContext, GeneralParameterEntry, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, ParameterMeta, ParameterName, ParameterState};
use pywr_core::resolve_metric_f64;
#[derive(Debug)]
pub struct MaxParameter {
meta: ParameterMeta,
metric: MetricF64,
threshold: f64,
}
impl Parameter for MaxParameter {
fn meta(&self) -> &ParameterMeta {
&self.meta
}
}
impl GeneralParameter for MaxParameter {
fn as_parameter(&self) -> &dyn Parameter
where
Self: Sized,
{
self
}
}
impl GeneralBeforeParameter<f64> for MaxParameter {
fn before(
&self,
ctx: GeneralParameterContext<'_>,
_internal_state: &mut Option<Box<dyn ParameterState>>,
) -> Result<f64, GeneralCalculationError> {
// Current value
let x = self.metric.get_value(ctx.network, ctx.state)?;
Ok(x.max(self.threshold))
}
}
impl GeneralAfterParameter<f64> for MaxParameter {
fn after(
&self,
ctx: GeneralParameterContext<'_>,
_internal_state: &mut Option<Box<dyn ParameterState>>,
) -> Result<f64, GeneralCalculationError> {
// Current value
let x = self.metric.get_value(ctx.network, ctx.state)?;
Ok(x.max(self.threshold))
}
}
#[derive(Debug)]
pub struct MaxParameterBuilder {
meta: ParameterMeta,
metric: UnresolvedMetricF64,
threshold: f64,
phase: MetricConsumerPhase,
}
impl MaxParameterBuilder {
/// Create a new builder for [`MaxParameter`] that is evaluated in the "before" phase.
pub fn before(name: ParameterName, metric: UnresolvedMetricF64, threshold: f64) -> Self {
Self {
meta: ParameterMeta::new(name),
metric,
threshold,
phase: MetricConsumerPhase::Before,
}
}
/// Create a new builder for [`MaxParameter`] that is evaluated in the "after" phase.
pub fn after(name: ParameterName, metric: UnresolvedMetricF64, threshold: f64) -> Self {
Self {
meta: ParameterMeta::new(name),
metric,
threshold,
phase: MetricConsumerPhase::After,
}
}
/// Create a new builder for [`MaxParameter`] that is evaluated in both "before" and "after" phases.
pub fn both(name: ParameterName, metric: UnresolvedMetricF64, threshold: f64) -> Self {
Self {
meta: ParameterMeta::new(name),
metric,
threshold,
phase: MetricConsumerPhase::Both,
}
}
}
impl ParameterBuilder<f64> for MaxParameterBuilder {
fn name(&self) -> &ParameterName {
&self.meta.name
}
fn build(
self: Box<Self>,
resolution_maps: &ResolutionMaps,
) -> Result<MaybeBuiltParameter<f64>, ParameterBuildError> {
let metric = resolve_metric_f64!(self, self.metric, resolution_maps, self.phase, "metric");
let p = MaxParameter {
meta: self.meta,
metric,
threshold: self.threshold,
};
let built = match self.phase {
MetricConsumerPhase::Before => {
BuiltParameter::General(GeneralParameterEntry::before(p))
},
MetricConsumerPhase::After => {
BuiltParameter::General(GeneralParameterEntry::after(p))
},
MetricConsumerPhase::Both => {
BuiltParameter::General(GeneralParameterEntry::both(p))
},
};
Ok(built.into())
}
}
mod schema {
#[cfg(feature = "core")]
use pywr_core::parameters::ParameterName;
use pywr_schema::metric::Metric;
use pywr_schema::parameters::{ParameterMeta, ParameterPhase};
#[cfg(feature = "core")]
use pywr_schema::{LoadArgs, SchemaError};
use schemars::JsonSchema;
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, JsonSchema)]
pub struct MaxParameter {
#[serde(flatten)]
pub meta: ParameterMeta,
pub phase: ParameterPhase,
pub parameter: Metric,
pub threshold: Option<f64>,
}
#[cfg(feature = "core")]
impl MaxParameter {
pub fn add_to_network(
&self,
network: &mut pywr_core::network::NetworkBuilder,
args: &LoadArgs,
parent: Option<&str>,
) -> Result<(), SchemaError> {
let idx = self.parameter.load(network, args, None)?;
let threshold = self.threshold.unwrap_or(0.0);
let name = ParameterName::new(&self.meta.name, parent);
let p = match self.phase {
ParameterPhase::Before => pywr_core::parameters::MaxParameterBuilder::before(name, idx, threshold),
ParameterPhase::After => pywr_core::parameters::MaxParameterBuilder::after(name, idx, threshold),
ParameterPhase::Both => pywr_core::parameters::MaxParameterBuilder::both(name, idx, threshold),
};
network.parameters().f64(Box::new(p));
Ok(())
}
}
}
fn main() {
println!("Hello, world!");
}
To allow the parameter to be used in the model a "builder" is required. This struct must implement the
ParameterBuilder<T> trait. This builder will be used by the schema to create the parameter when it is loaded from a model
file. Builders will typically have the same fields as the parameter itself, but will use "unresolved" types
(e.g. UnresolvedMetricF64 or UnresolvedNode). This allows the builder to be created without first resolving the
dependencies of the parameter. The build function is then used to resolve the dependencies and create the parameter
used in the model.
The builder may also have a phase field which allows the parameter to be evaluated
in either the before, after or both phase. This determines whether the calculation is evaluated at the beginning or end
of the timestep. Parameters evaluated in the before phase will be available to other parameters and calculations in the same
timestep often using values from the end of the previous timestep.
#![allow(dead_code)]
use pywr_core::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64};
use pywr_core::network::ResolutionMaps;
use pywr_core::parameters::{BuiltParameter, GeneralBeforeParameter, GeneralAfterParameter, GeneralCalculationError, GeneralParameter, GeneralParameterContext, GeneralParameterEntry, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, ParameterMeta, ParameterName, ParameterState};
use pywr_core::resolve_metric_f64;
#[derive(Debug)]
pub struct MaxParameter {
meta: ParameterMeta,
metric: MetricF64,
threshold: f64,
}
impl Parameter for MaxParameter {
fn meta(&self) -> &ParameterMeta {
&self.meta
}
}
impl GeneralParameter for MaxParameter {
fn as_parameter(&self) -> &dyn Parameter
where
Self: Sized,
{
self
}
}
impl GeneralBeforeParameter<f64> for MaxParameter {
fn before(
&self,
ctx: GeneralParameterContext<'_>,
_internal_state: &mut Option<Box<dyn ParameterState>>,
) -> Result<f64, GeneralCalculationError> {
// Current value
let x = self.metric.get_value(ctx.network, ctx.state)?;
Ok(x.max(self.threshold))
}
}
impl GeneralAfterParameter<f64> for MaxParameter {
fn after(
&self,
ctx: GeneralParameterContext<'_>,
_internal_state: &mut Option<Box<dyn ParameterState>>,
) -> Result<f64, GeneralCalculationError> {
// Current value
let x = self.metric.get_value(ctx.network, ctx.state)?;
Ok(x.max(self.threshold))
}
}
#[derive(Debug)]
pub struct MaxParameterBuilder {
meta: ParameterMeta,
metric: UnresolvedMetricF64,
threshold: f64,
phase: MetricConsumerPhase,
}
impl MaxParameterBuilder {
/// Create a new builder for [`MaxParameter`] that is evaluated in the "before" phase.
pub fn before(name: ParameterName, metric: UnresolvedMetricF64, threshold: f64) -> Self {
Self {
meta: ParameterMeta::new(name),
metric,
threshold,
phase: MetricConsumerPhase::Before,
}
}
/// Create a new builder for [`MaxParameter`] that is evaluated in the "after" phase.
pub fn after(name: ParameterName, metric: UnresolvedMetricF64, threshold: f64) -> Self {
Self {
meta: ParameterMeta::new(name),
metric,
threshold,
phase: MetricConsumerPhase::After,
}
}
/// Create a new builder for [`MaxParameter`] that is evaluated in both "before" and "after" phases.
pub fn both(name: ParameterName, metric: UnresolvedMetricF64, threshold: f64) -> Self {
Self {
meta: ParameterMeta::new(name),
metric,
threshold,
phase: MetricConsumerPhase::Both,
}
}
}
impl ParameterBuilder<f64> for MaxParameterBuilder {
fn name(&self) -> &ParameterName {
&self.meta.name
}
fn build(
self: Box<Self>,
resolution_maps: &ResolutionMaps,
) -> Result<MaybeBuiltParameter<f64>, ParameterBuildError> {
let metric = resolve_metric_f64!(self, self.metric, resolution_maps, self.phase, "metric");
let p = MaxParameter {
meta: self.meta,
metric,
threshold: self.threshold,
};
let built = match self.phase {
MetricConsumerPhase::Before => {
BuiltParameter::General(GeneralParameterEntry::before(p))
},
MetricConsumerPhase::After => {
BuiltParameter::General(GeneralParameterEntry::after(p))
},
MetricConsumerPhase::Both => {
BuiltParameter::General(GeneralParameterEntry::both(p))
},
};
Ok(built.into())
}
}
mod schema {
#[cfg(feature = "core")]
use pywr_core::parameters::ParameterName;
use pywr_schema::metric::Metric;
use pywr_schema::parameters::{ParameterMeta, ParameterPhase};
#[cfg(feature = "core")]
use pywr_schema::{LoadArgs, SchemaError};
use schemars::JsonSchema;
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, JsonSchema)]
pub struct MaxParameter {
#[serde(flatten)]
pub meta: ParameterMeta,
pub phase: ParameterPhase,
pub parameter: Metric,
pub threshold: Option<f64>,
}
#[cfg(feature = "core")]
impl MaxParameter {
pub fn add_to_network(
&self,
network: &mut pywr_core::network::NetworkBuilder,
args: &LoadArgs,
parent: Option<&str>,
) -> Result<(), SchemaError> {
let idx = self.parameter.load(network, args, None)?;
let threshold = self.threshold.unwrap_or(0.0);
let name = ParameterName::new(&self.meta.name, parent);
let p = match self.phase {
ParameterPhase::Before => pywr_core::parameters::MaxParameterBuilder::before(name, idx, threshold),
ParameterPhase::After => pywr_core::parameters::MaxParameterBuilder::after(name, idx, threshold),
ParameterPhase::Both => pywr_core::parameters::MaxParameterBuilder::both(name, idx, threshold),
};
network.parameters().f64(Box::new(p));
Ok(())
}
}
}
fn main() {
println!("Hello, world!");
}
Finally, the minimum implementation of the Parameter and one of the three types of parameter compute traits should be
added for MaxParameter. These traits require the meta function to return the metadata for the parameter, and
the compute function to calculate the value of the parameter at a given timestep and scenario.
In this case the compute function calculates the maximum value of the metric and the threshold.
The value of the metric is obtained from the model using the get_value function.
See the documentation about parameter traits and return types for more information.
#![allow(dead_code)]
use pywr_core::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64};
use pywr_core::network::ResolutionMaps;
use pywr_core::parameters::{BuiltParameter, GeneralBeforeParameter, GeneralAfterParameter, GeneralCalculationError, GeneralParameter, GeneralParameterContext, GeneralParameterEntry, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, ParameterMeta, ParameterName, ParameterState};
use pywr_core::resolve_metric_f64;
#[derive(Debug)]
pub struct MaxParameter {
meta: ParameterMeta,
metric: MetricF64,
threshold: f64,
}
impl Parameter for MaxParameter {
fn meta(&self) -> &ParameterMeta {
&self.meta
}
}
impl GeneralParameter for MaxParameter {
fn as_parameter(&self) -> &dyn Parameter
where
Self: Sized,
{
self
}
}
impl GeneralBeforeParameter<f64> for MaxParameter {
fn before(
&self,
ctx: GeneralParameterContext<'_>,
_internal_state: &mut Option<Box<dyn ParameterState>>,
) -> Result<f64, GeneralCalculationError> {
// Current value
let x = self.metric.get_value(ctx.network, ctx.state)?;
Ok(x.max(self.threshold))
}
}
impl GeneralAfterParameter<f64> for MaxParameter {
fn after(
&self,
ctx: GeneralParameterContext<'_>,
_internal_state: &mut Option<Box<dyn ParameterState>>,
) -> Result<f64, GeneralCalculationError> {
// Current value
let x = self.metric.get_value(ctx.network, ctx.state)?;
Ok(x.max(self.threshold))
}
}
#[derive(Debug)]
pub struct MaxParameterBuilder {
meta: ParameterMeta,
metric: UnresolvedMetricF64,
threshold: f64,
phase: MetricConsumerPhase,
}
impl MaxParameterBuilder {
/// Create a new builder for [`MaxParameter`] that is evaluated in the "before" phase.
pub fn before(name: ParameterName, metric: UnresolvedMetricF64, threshold: f64) -> Self {
Self {
meta: ParameterMeta::new(name),
metric,
threshold,
phase: MetricConsumerPhase::Before,
}
}
/// Create a new builder for [`MaxParameter`] that is evaluated in the "after" phase.
pub fn after(name: ParameterName, metric: UnresolvedMetricF64, threshold: f64) -> Self {
Self {
meta: ParameterMeta::new(name),
metric,
threshold,
phase: MetricConsumerPhase::After,
}
}
/// Create a new builder for [`MaxParameter`] that is evaluated in both "before" and "after" phases.
pub fn both(name: ParameterName, metric: UnresolvedMetricF64, threshold: f64) -> Self {
Self {
meta: ParameterMeta::new(name),
metric,
threshold,
phase: MetricConsumerPhase::Both,
}
}
}
impl ParameterBuilder<f64> for MaxParameterBuilder {
fn name(&self) -> &ParameterName {
&self.meta.name
}
fn build(
self: Box<Self>,
resolution_maps: &ResolutionMaps,
) -> Result<MaybeBuiltParameter<f64>, ParameterBuildError> {
let metric = resolve_metric_f64!(self, self.metric, resolution_maps, self.phase, "metric");
let p = MaxParameter {
meta: self.meta,
metric,
threshold: self.threshold,
};
let built = match self.phase {
MetricConsumerPhase::Before => {
BuiltParameter::General(GeneralParameterEntry::before(p))
},
MetricConsumerPhase::After => {
BuiltParameter::General(GeneralParameterEntry::after(p))
},
MetricConsumerPhase::Both => {
BuiltParameter::General(GeneralParameterEntry::both(p))
},
};
Ok(built.into())
}
}
mod schema {
#[cfg(feature = "core")]
use pywr_core::parameters::ParameterName;
use pywr_schema::metric::Metric;
use pywr_schema::parameters::{ParameterMeta, ParameterPhase};
#[cfg(feature = "core")]
use pywr_schema::{LoadArgs, SchemaError};
use schemars::JsonSchema;
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, JsonSchema)]
pub struct MaxParameter {
#[serde(flatten)]
pub meta: ParameterMeta,
pub phase: ParameterPhase,
pub parameter: Metric,
pub threshold: Option<f64>,
}
#[cfg(feature = "core")]
impl MaxParameter {
pub fn add_to_network(
&self,
network: &mut pywr_core::network::NetworkBuilder,
args: &LoadArgs,
parent: Option<&str>,
) -> Result<(), SchemaError> {
let idx = self.parameter.load(network, args, None)?;
let threshold = self.threshold.unwrap_or(0.0);
let name = ParameterName::new(&self.meta.name, parent);
let p = match self.phase {
ParameterPhase::Before => pywr_core::parameters::MaxParameterBuilder::before(name, idx, threshold),
ParameterPhase::After => pywr_core::parameters::MaxParameterBuilder::after(name, idx, threshold),
ParameterPhase::Both => pywr_core::parameters::MaxParameterBuilder::both(name, idx, threshold),
};
network.parameters().f64(Box::new(p));
Ok(())
}
}
}
fn main() {
println!("Hello, world!");
}
Adding the schema definition to pywr-schema
The schema definition for the new parameter should be added to the pywr-schema crate.
Again, it is a good idea to follow the existing structure of the schema by making a new module for the new parameter.
Developers can also follow the existing parameters as examples.
As with the pywr-core implementation, the meta field is used to store the metadata for the parameter and can
use the ParameterMeta struct (NB this is from pywr-schema crate).
The rest of the struct looks very similar to the pywr-core implementation, but uses pywr-schema
types for the fields.
The struct should also derive serde::Deserialize, serde::Serialize, Debug, Clone, JsonSchema,
and PywrVisitAll to be compatible with the rest of Pywr.
Note: The
PywrVisitAllderive is not shown in the listing as it can not currently be used outside thepywr-schemacrate.
#![allow(dead_code)]
use pywr_core::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64};
use pywr_core::network::ResolutionMaps;
use pywr_core::parameters::{BuiltParameter, GeneralBeforeParameter, GeneralAfterParameter, GeneralCalculationError, GeneralParameter, GeneralParameterContext, GeneralParameterEntry, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, ParameterMeta, ParameterName, ParameterState};
use pywr_core::resolve_metric_f64;
#[derive(Debug)]
pub struct MaxParameter {
meta: ParameterMeta,
metric: MetricF64,
threshold: f64,
}
impl Parameter for MaxParameter {
fn meta(&self) -> &ParameterMeta {
&self.meta
}
}
impl GeneralParameter for MaxParameter {
fn as_parameter(&self) -> &dyn Parameter
where
Self: Sized,
{
self
}
}
impl GeneralBeforeParameter<f64> for MaxParameter {
fn before(
&self,
ctx: GeneralParameterContext<'_>,
_internal_state: &mut Option<Box<dyn ParameterState>>,
) -> Result<f64, GeneralCalculationError> {
// Current value
let x = self.metric.get_value(ctx.network, ctx.state)?;
Ok(x.max(self.threshold))
}
}
impl GeneralAfterParameter<f64> for MaxParameter {
fn after(
&self,
ctx: GeneralParameterContext<'_>,
_internal_state: &mut Option<Box<dyn ParameterState>>,
) -> Result<f64, GeneralCalculationError> {
// Current value
let x = self.metric.get_value(ctx.network, ctx.state)?;
Ok(x.max(self.threshold))
}
}
#[derive(Debug)]
pub struct MaxParameterBuilder {
meta: ParameterMeta,
metric: UnresolvedMetricF64,
threshold: f64,
phase: MetricConsumerPhase,
}
impl MaxParameterBuilder {
/// Create a new builder for [`MaxParameter`] that is evaluated in the "before" phase.
pub fn before(name: ParameterName, metric: UnresolvedMetricF64, threshold: f64) -> Self {
Self {
meta: ParameterMeta::new(name),
metric,
threshold,
phase: MetricConsumerPhase::Before,
}
}
/// Create a new builder for [`MaxParameter`] that is evaluated in the "after" phase.
pub fn after(name: ParameterName, metric: UnresolvedMetricF64, threshold: f64) -> Self {
Self {
meta: ParameterMeta::new(name),
metric,
threshold,
phase: MetricConsumerPhase::After,
}
}
/// Create a new builder for [`MaxParameter`] that is evaluated in both "before" and "after" phases.
pub fn both(name: ParameterName, metric: UnresolvedMetricF64, threshold: f64) -> Self {
Self {
meta: ParameterMeta::new(name),
metric,
threshold,
phase: MetricConsumerPhase::Both,
}
}
}
impl ParameterBuilder<f64> for MaxParameterBuilder {
fn name(&self) -> &ParameterName {
&self.meta.name
}
fn build(
self: Box<Self>,
resolution_maps: &ResolutionMaps,
) -> Result<MaybeBuiltParameter<f64>, ParameterBuildError> {
let metric = resolve_metric_f64!(self, self.metric, resolution_maps, self.phase, "metric");
let p = MaxParameter {
meta: self.meta,
metric,
threshold: self.threshold,
};
let built = match self.phase {
MetricConsumerPhase::Before => {
BuiltParameter::General(GeneralParameterEntry::before(p))
},
MetricConsumerPhase::After => {
BuiltParameter::General(GeneralParameterEntry::after(p))
},
MetricConsumerPhase::Both => {
BuiltParameter::General(GeneralParameterEntry::both(p))
},
};
Ok(built.into())
}
}
mod schema {
#[cfg(feature = "core")]
use pywr_core::parameters::ParameterName;
use pywr_schema::metric::Metric;
use pywr_schema::parameters::{ParameterMeta, ParameterPhase};
#[cfg(feature = "core")]
use pywr_schema::{LoadArgs, SchemaError};
use schemars::JsonSchema;
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, JsonSchema)]
pub struct MaxParameter {
#[serde(flatten)]
pub meta: ParameterMeta,
pub phase: ParameterPhase,
pub parameter: Metric,
pub threshold: Option<f64>,
}
#[cfg(feature = "core")]
impl MaxParameter {
pub fn add_to_network(
&self,
network: &mut pywr_core::network::NetworkBuilder,
args: &LoadArgs,
parent: Option<&str>,
) -> Result<(), SchemaError> {
let idx = self.parameter.load(network, args, None)?;
let threshold = self.threshold.unwrap_or(0.0);
let name = ParameterName::new(&self.meta.name, parent);
let p = match self.phase {
ParameterPhase::Before => pywr_core::parameters::MaxParameterBuilder::before(name, idx, threshold),
ParameterPhase::After => pywr_core::parameters::MaxParameterBuilder::after(name, idx, threshold),
ParameterPhase::Both => pywr_core::parameters::MaxParameterBuilder::both(name, idx, threshold),
};
network.parameters().f64(Box::new(p));
Ok(())
}
}
}
fn main() {
println!("Hello, world!");
}
Next, the parameter needs a method to add itself to a network.
This is typically done by implementing a add_to_model method for the parameter.
This method should be feature-gated with the core feature to ensure it is only available when the core feature is
enabled.
The method should take a mutable reference to the network and a reference to the LoadArgs struct.
The method should load the metric from the model using the load method, and then create a new MaxParameter by matching to
the given phase and using one of the before, after or both methods implemented above.
Finally, the method should add the parameter to the network using the add_parameter method.
#![allow(dead_code)]
use pywr_core::metric::{MetricConsumerPhase, MetricF64, UnresolvedMetricF64};
use pywr_core::network::ResolutionMaps;
use pywr_core::parameters::{BuiltParameter, GeneralBeforeParameter, GeneralAfterParameter, GeneralCalculationError, GeneralParameter, GeneralParameterContext, GeneralParameterEntry, MaybeBuiltParameter, Parameter, ParameterBuildError, ParameterBuilder, ParameterMeta, ParameterName, ParameterState};
use pywr_core::resolve_metric_f64;
#[derive(Debug)]
pub struct MaxParameter {
meta: ParameterMeta,
metric: MetricF64,
threshold: f64,
}
impl Parameter for MaxParameter {
fn meta(&self) -> &ParameterMeta {
&self.meta
}
}
impl GeneralParameter for MaxParameter {
fn as_parameter(&self) -> &dyn Parameter
where
Self: Sized,
{
self
}
}
impl GeneralBeforeParameter<f64> for MaxParameter {
fn before(
&self,
ctx: GeneralParameterContext<'_>,
_internal_state: &mut Option<Box<dyn ParameterState>>,
) -> Result<f64, GeneralCalculationError> {
// Current value
let x = self.metric.get_value(ctx.network, ctx.state)?;
Ok(x.max(self.threshold))
}
}
impl GeneralAfterParameter<f64> for MaxParameter {
fn after(
&self,
ctx: GeneralParameterContext<'_>,
_internal_state: &mut Option<Box<dyn ParameterState>>,
) -> Result<f64, GeneralCalculationError> {
// Current value
let x = self.metric.get_value(ctx.network, ctx.state)?;
Ok(x.max(self.threshold))
}
}
#[derive(Debug)]
pub struct MaxParameterBuilder {
meta: ParameterMeta,
metric: UnresolvedMetricF64,
threshold: f64,
phase: MetricConsumerPhase,
}
impl MaxParameterBuilder {
/// Create a new builder for [`MaxParameter`] that is evaluated in the "before" phase.
pub fn before(name: ParameterName, metric: UnresolvedMetricF64, threshold: f64) -> Self {
Self {
meta: ParameterMeta::new(name),
metric,
threshold,
phase: MetricConsumerPhase::Before,
}
}
/// Create a new builder for [`MaxParameter`] that is evaluated in the "after" phase.
pub fn after(name: ParameterName, metric: UnresolvedMetricF64, threshold: f64) -> Self {
Self {
meta: ParameterMeta::new(name),
metric,
threshold,
phase: MetricConsumerPhase::After,
}
}
/// Create a new builder for [`MaxParameter`] that is evaluated in both "before" and "after" phases.
pub fn both(name: ParameterName, metric: UnresolvedMetricF64, threshold: f64) -> Self {
Self {
meta: ParameterMeta::new(name),
metric,
threshold,
phase: MetricConsumerPhase::Both,
}
}
}
impl ParameterBuilder<f64> for MaxParameterBuilder {
fn name(&self) -> &ParameterName {
&self.meta.name
}
fn build(
self: Box<Self>,
resolution_maps: &ResolutionMaps,
) -> Result<MaybeBuiltParameter<f64>, ParameterBuildError> {
let metric = resolve_metric_f64!(self, self.metric, resolution_maps, self.phase, "metric");
let p = MaxParameter {
meta: self.meta,
metric,
threshold: self.threshold,
};
let built = match self.phase {
MetricConsumerPhase::Before => {
BuiltParameter::General(GeneralParameterEntry::before(p))
},
MetricConsumerPhase::After => {
BuiltParameter::General(GeneralParameterEntry::after(p))
},
MetricConsumerPhase::Both => {
BuiltParameter::General(GeneralParameterEntry::both(p))
},
};
Ok(built.into())
}
}
mod schema {
#[cfg(feature = "core")]
use pywr_core::parameters::ParameterName;
use pywr_schema::metric::Metric;
use pywr_schema::parameters::{ParameterMeta, ParameterPhase};
#[cfg(feature = "core")]
use pywr_schema::{LoadArgs, SchemaError};
use schemars::JsonSchema;
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, JsonSchema)]
pub struct MaxParameter {
#[serde(flatten)]
pub meta: ParameterMeta,
pub phase: ParameterPhase,
pub parameter: Metric,
pub threshold: Option<f64>,
}
#[cfg(feature = "core")]
impl MaxParameter {
pub fn add_to_network(
&self,
network: &mut pywr_core::network::NetworkBuilder,
args: &LoadArgs,
parent: Option<&str>,
) -> Result<(), SchemaError> {
let idx = self.parameter.load(network, args, None)?;
let threshold = self.threshold.unwrap_or(0.0);
let name = ParameterName::new(&self.meta.name, parent);
let p = match self.phase {
ParameterPhase::Before => pywr_core::parameters::MaxParameterBuilder::before(name, idx, threshold),
ParameterPhase::After => pywr_core::parameters::MaxParameterBuilder::after(name, idx, threshold),
ParameterPhase::Both => pywr_core::parameters::MaxParameterBuilder::both(name, idx, threshold),
};
network.parameters().f64(Box::new(p));
Ok(())
}
}
}
fn main() {
println!("Hello, world!");
}
Finally, the schema definition should be added to the Parameter enum in the parameters module.
This will require ensuring the new variant is added to all places where that enum is used.
The borrow checker can be helpful in ensuring all places are updated.