-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: Add developer guide on adding a new parameter. (#197)
- Loading branch information
Showing
10 changed files
with
293 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
[package] | ||
name = "adding-a-parameter" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
[dependencies] | ||
pywr-core = { path = "../../../pywr-core" } | ||
pywr-schema = { path = "../../../pywr-schema" } | ||
pywr-schema-macros = { path = "../../../pywr-schema-macros" } | ||
serde = { workspace = true } | ||
schemars = { workspace = true } | ||
|
||
[features] | ||
core = ["pywr-schema/core"] | ||
default = ["core"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
#![allow(dead_code)] | ||
use pywr_core::metric::MetricF64; | ||
use pywr_core::network::Network; | ||
use pywr_core::parameters::{GeneralParameter, Parameter, ParameterMeta, ParameterName, ParameterState}; | ||
use pywr_core::scenario::ScenarioIndex; | ||
use pywr_core::state::State; | ||
use pywr_core::timestep::Timestep; | ||
use pywr_core::PywrError; | ||
|
||
// ANCHOR: parameter | ||
pub struct MaxParameter { | ||
meta: ParameterMeta, | ||
metric: MetricF64, | ||
threshold: f64, | ||
} | ||
// ANCHOR_END: parameter | ||
// ANCHOR: impl-new | ||
impl MaxParameter { | ||
pub fn new(name: ParameterName, metric: MetricF64, threshold: f64) -> Self { | ||
Self { | ||
meta: ParameterMeta::new(name), | ||
metric, | ||
threshold, | ||
} | ||
} | ||
} | ||
// ANCHOR_END: impl-new | ||
// ANCHOR: impl-parameter | ||
impl Parameter for MaxParameter { | ||
fn meta(&self) -> &ParameterMeta { | ||
&self.meta | ||
} | ||
} | ||
|
||
impl GeneralParameter<f64> for MaxParameter { | ||
fn compute( | ||
&self, | ||
_timestep: &Timestep, | ||
_scenario_index: &ScenarioIndex, | ||
model: &Network, | ||
state: &State, | ||
_internal_state: &mut Option<Box<dyn ParameterState>>, | ||
) -> Result<f64, PywrError> { | ||
// Current value | ||
let x = self.metric.get_value(model, state)?; | ||
Ok(x.max(self.threshold)) | ||
} | ||
|
||
fn as_parameter(&self) -> &dyn Parameter | ||
where | ||
Self: Sized, | ||
{ | ||
self | ||
} | ||
} | ||
|
||
// ANCHOR_END: impl-parameter | ||
mod schema { | ||
#[cfg(feature = "core")] | ||
use pywr_core::parameters::ParameterIndex; | ||
use pywr_schema::metric::Metric; | ||
use pywr_schema::parameters::ParameterMeta; | ||
#[cfg(feature = "core")] | ||
use pywr_schema::{model::LoadArgs, SchemaError}; | ||
use schemars::JsonSchema; | ||
|
||
// ANCHOR: schema | ||
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, JsonSchema)] | ||
pub struct MaxParameter { | ||
#[serde(flatten)] | ||
pub meta: ParameterMeta, | ||
pub parameter: Metric, | ||
pub threshold: Option<f64>, | ||
} | ||
|
||
// ANCHOR_END: schema | ||
// ANCHOR: schema-impl | ||
#[cfg(feature = "core")] | ||
impl MaxParameter { | ||
pub fn add_to_model( | ||
&self, | ||
network: &mut pywr_core::network::Network, | ||
args: &LoadArgs, | ||
) -> Result<ParameterIndex<f64>, SchemaError> { | ||
let idx = self.parameter.load(network, args)?; | ||
let threshold = self.threshold.unwrap_or(0.0); | ||
|
||
let p = pywr_core::parameters::MaxParameter::new(self.meta.name.as_str().into(), idx, threshold); | ||
Ok(network.add_parameter(Box::new(p))?) | ||
} | ||
} | ||
// ANCHOR_END: schema-impl | ||
} | ||
|
||
fn main() { | ||
println!("Hello, world!"); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# Developers Guide | ||
|
||
This section is intended for developers who want to contribute to Pywr. It covers the following topics: | ||
|
||
- Parameter types and traits | ||
- Adding a new parameter |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
# 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. | ||
|
||
```rust,ignore | ||
{{#rustdoc_include ../../listings/adding-a-parameter/src/main.rs:parameter}} | ||
``` | ||
|
||
To allow the parameter to be used in the model it is helpful to add a `new` function that creates a new instance of the | ||
parameter. This will be used by the schema to create the parameter when it is loaded from a model file. | ||
|
||
```rust,ignore | ||
{{#rustdoc_include ../../listings/adding-a-parameter/src/main.rs:impl-new}} | ||
``` | ||
|
||
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](parameter-traits.md) about parameter traits and return types for more information. | ||
|
||
```rust,ignore | ||
{{#rustdoc_include ../../listings/adding-a-parameter/src/main.rs:impl-parameter}} | ||
``` | ||
|
||
### 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. | ||
```rust,ignore | ||
{{#rustdoc_include ../../listings/adding-a-parameter/src/main.rs:schema}} | ||
``` | ||
|
||
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` using | ||
the `new` method implemented above. | ||
Finally, the method should add the parameter to the network using the `add_parameter` method. | ||
|
||
```rust,ignore | ||
{{#rustdoc_include ../../listings/adding-a-parameter/src/main.rs:schema-impl}} | ||
``` | ||
|
||
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
# Parameter traits and return types | ||
|
||
The `pywr-core` crate defines a number of traits that are used to implement parameters. These traits are used to define | ||
the behaviour of the parameter and how it interacts with the model. Each parameter must implement the `Parameter` trait | ||
and one of the three compute traits: `GeneralParameter<T>`, `SimpleParameter<T>`, or `ConstParameter<T>`. | ||
|
||
## The `Parameter` trait | ||
|
||
The `Parameter` trait is the base trait for all parameters in Pywr. It defines the basic behaviour of the parameter and | ||
how it interacts with the model. The minimum implementation requires returning the metadata for the parameter. | ||
Additional methods can be implemented to provide additional functionality. Please refer to the documentation for | ||
the `Parameter` trait for more information. | ||
|
||
## The `GeneralParameter<T>` trait | ||
|
||
The `GeneralParameter<T>` trait is used for parameters that depend on `MetricF64` values from the model. Because | ||
`MetricF64` values can refer to other parameters, general model state or other information implementing this | ||
traits provides the most flexibility for a parameter. The `compute` method is used to calculate the value of the | ||
parameter at a given timestep and scenario. This method is resolved in order with other model components such | ||
as nodes. | ||
|
||
## The `SimpleParameter<T>` trait | ||
|
||
The `SimpleParameter<T>` trait is used for parameters that depend on `SimpleMetricF64` or `ConstantMetricF64` | ||
values only, or no other values at all. The `compute` method is used to calculate the value of the parameter at a given | ||
timestep and scenario, and therefore `SimpleParameter<T>` can vary with time. This method is resolved in order with | ||
other `SimpleParameter<T>` before `GeneralParameter<T>` and other model components such as nodes. | ||
|
||
## The `ConstParameter<T>` trait | ||
|
||
The `ConstParameter<T>` trait is used for parameters that depend on `ConstantMetricF64` values only and do | ||
not vary with time. The `compute` method is used to calculate the value of the parameter at the start of the simulation | ||
and is not resolved at each timestep. This method is resolved in order with other `ConstParameter<T>`. | ||
|
||
## Implementing multiple traits | ||
|
||
A parameter should implement the "lowest" trait in the hierarchy. For example, if a parameter depends on | ||
a `SimpleParameter<T>` and a `ConstParameter<T>` value, it should implement the `SimpleParameter<T>` trait. | ||
If a parameter depends on a `GeneralParameter<T>` and a `ConstParameter<T>` value, it should implement the | ||
`GeneralParameter<T>` trait. | ||
|
||
For some parameters it can be beneficial to implement multiple traits. For example, a parameter could be generic to the | ||
metric type (e.g. `MetricF64`, `SimpleMetricF64`, or `ConstantMetricF64`) and implement each of the three | ||
compute traits. This would allow the parameter to be used in the most efficient way possible depending on the | ||
model configuration. | ||
|
||
## Return types | ||
|
||
While the compute traits are generic over the type `T`, the return type of the `compute` Pywr currently only supports | ||
`f64`, `usize` and `MultiValue` types. The `MultiValue` type is used to return multiple values from the `compute` | ||
method. This is useful for parameters that return multiple values at a given timestep and scenario. See the | ||
documentation for the `MultiValue` type for more information. Implementations of the compute traits are usually for one | ||
of these concrete types. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters