Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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-core crate, and
  • Add the schema definition to the pywr-schema crate.

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 PywrVisitAll derive is not shown in the listing as it can not currently be used outside the pywr-schema crate.

#![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.