pywr / pywr-next

An experimental repository exploring ideas for a major revision to Pywr using Rust as a backend.
6 stars 4 forks source link

Implement a system of constant parameters. #62

Closed jetuk closed 2 months ago

jetuk commented 11 months ago

The current parameter system supports values that change during a simulation (i.e. over the time and scenario domains). Several parameter types do not vary in either or both of these domains (e.g. ConstantParameter). There's an efficiency to be gained from only "calculating" such parameters once at the beginning of a simulation.

However, it might also be useful to have constants derived from other constants. For example, where a ConstantParameter is used to optimise (i.e. by running the model with many different values) the size of a reservoir an associated cost function might depend on that constant value. It would need to be re-calculated if the constant were to change before each simulation.

This implementation could follow the same pattern as the regular parameter implementation. However, it would only depend on a subset of the state - the constant values. This subset would then be immutable for the remainder of the simulation, but could be used by regular parameters as required.

jetuk commented 4 months ago

Current Parameter calculation method signature:

pub trait Parameter<T>: Send + Sync {
    fn compute(
        &self,
        timestep: &Timestep,
        scenario_index: &ScenarioIndex,
        model: &Network,
        state: &State,
        internal_state: &mut Option<Box<dyn ParameterState>>,
    ) -> Result<T, PywrError>;
}

Alternative signatures could depend on less data:

/// Parameter who's value does not change but could be dependent on another `ConstantParameter`
/// This can be evaluated once at the start of a model run.
pub trait ConstantParameter<T>: Send + Sync {
    /// The output of this would be stored in `ConstantParameterValues`
    fn compute(
        &self,
        scenario_index: &ScenarioIndex,
        state: &ConstantParameterValues
    ) -> Result<T, PywrError>;
}

/// Parameter who's value can change in time (could be called something else)
/// This can be first at the start of a time-step
pub trait SimpleParameter<T>: Send + Sync {
    /// The output of this would be stored in `SimpleParameterValues`
    fn compute(
        &self,
        timestep: &Timestep,
        state: &SimpleParameterValues
    ) -> Result<T, PywrError>;
}
jetuk commented 4 months ago

The idea here is that the algorithm would change to something like:

calculate constant parameters
for t in timesteps:
    calculate simple parameters (can only depend on constant or other simple parameters)
    update nodes (`.before()` can safely use a simple or constant parameter for max volume in storage nodes)
    calculate general parameters (can depend on node (including proportional volume that was just updated) and any other parameters)
    update lp
    solve lp

The middle part is more like Pywr v1.x with nodes updated before parameters. The current implementation here tries to mix them together to have dependency tree resolve in the correct order. Which is why you have to load max volume before creating the storage node (see also #192).

In this method that would no longer be needed as there would be a restriction on max volume to only allow simple or constant parameters. This feels like it models the invariant (i.e. max volume can't be dependent on the node) better with the type system.

jetuk commented 2 months ago

Done in #194