Skip to content

feat: Allow specifying calculation phase in some parameters. - #745

Open
jetuk wants to merge 12 commits into
mainfrom
core-parameter-phase-validation
Open

feat: Allow specifying calculation phase in some parameters.#745
jetuk wants to merge 12 commits into
mainfrom
core-parameter-phase-validation

Conversation

@jetuk

@jetuk jetuk commented Aug 10, 2026

Copy link
Copy Markdown
Member

This folows on from #726 to do the following:

  • pywr-core: Internally allocate space only for before and after parameters that are registered to supply values in those phases. Two new index types are added for this.
  • pywr-core: Validate consumer phasing with parameter return values during network build. This should reject invalid combinations at build time and avoid spurious missing values at runtime.
  • pywr-schema: Allow specifying the phase for Aggregated and AggregatedIndex parameters in the schema.
  • pywr-schema: Correctly construct parameter references for appropriate phase when building metric sets from all parameters.
  • pywr-schema: Add tests models for the issue identified in Runtime bug with DeficitParameter #725 (i.e. calculating and saving total deficit).

What's left to do is implement alternative phase (i.e. after) for some of the other parameter types.

jetuk added 2 commits August 10, 2026 14:12
This folows on from #726 to do the following:
- pywr-core: Internally allocate space only for before and
after parameters that are registered to supply values in those
phases. Two new index types are added for this.
- pywr-core: Validate consumer phasing with parameter return
values during network build. This should reject invalid combinations
at build time and avoid spurious missing values at runtime.
- pywr-schema: Allow specifying the phase for Aggregated
and AggregatedIndex parameters in the schema.
- pywr-schema: Correctly construct parameter references for
appropriate phase when building metric sets from all parameters.
- pywr-schema: Add tests models for the issue identified in #725
(i.e. calculating and saving total deficit).

What's left to do is implement alternative phase (i.e. after) for
some of the other parameter types.

@Batch21 Batch21 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've gone through this in some detail and think the approach is good. A few queries/suggestions below.

let metrics = resolve_metric_u64_vec!(self, &self.metrics, resolution_maps, self.phase, "metrics");

let meta = self.meta;
let agg_func = self.agg_func;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does the builder need to match on phase when processing the metrics like in AggregatedParameterBuilder?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

#[test]
fn simple_before_parameter_can_depend_on_simple_before_value() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do these removed tests need replacing or updating?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added more unit tests.

Comment thread pywr-core/src/metric.rs
Comment on lines +788 to +789
fn resolve_parameter_index_f64_to_metric_f64(
name: &ParameterName,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have some unit tests that check that the correct errors are produced for different combinations of return values and phases.

Also, would it be possible to combined some of the logic from the resolve_parameter_index_* functions into a share function?

Self::Polynomial1D(_) => ParameterPhase::Before,
Self::Threshold(_) => ParameterPhase::Before,
Self::TablesArray(_) => ParameterPhase::Before,
Self::Python(_) => ParameterPhase::Before,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Python parameter could be After or Both which could cause an error when this is used by by metrics sets. Do these phases need to be determined dynamically from the registered parameters instead?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently the parameter (in core) is hardcoded to be "before". I'll fix this along with other parameters in a follow-up.

Comment thread pywr-core/src/metric.rs
match idx {
// Constant and simple can always be resolved to a metric, regardless of the consumer phase
// as long as the parameter return value is "before".
ParameterIndex::Const(idx) => match parameter_return_value {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other resolve_parameter_index_* function do not match on parameter_return_value from constant paramters like this one does.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

// Determine which calculation phase(s) the parameter will be used in
// based on whether the target and actual flow are provided.
// If neither is provided, return an error.
let phase = match (self.target.is_some(), self.actual_flow.is_some()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

won't the phase here vary across the metrics? some will be access in before and some in after?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't that what the match statement is doing? Working out which phases this parameter will be in?

Comment thread pywr-core/src/metric.rs
// initial value is acceptable in the "before" phase, so we can resolve it to a metric provided the
// parameter index contains an "after" index.
match idx.after {
Some(after_idx) => Ok(MetricF64::ParameterAfterMulti {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could result in a runtime error as it will try and look up a value in an empty hashmap?

@jetuk jetuk Aug 17, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's true. I think a follow-up idea would allow parameters to provide initial values.

impl AsymmetricSwitchIndexParameterBuilder {
pub fn new(name: ParameterName, on_parameter: UnresolvedMetricU64, off_parameter: UnresolvedMetricU64) -> Self {
/// Create a new builder for [`AsymmetricSwitchIndexParameter`] that is evaluated in the "before" phase.
pub fn before(name: ParameterName, on_parameter: UnresolvedMetricU64, off_parameter: UnresolvedMetricU64) -> Self {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some parameters, like this one, define a new before constructor method while others (like the max parameter) hardcode a before phase variable in their existing build methods. Is there a reasons for this? Should it be made consistent?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was just a bit of WIP. I was going to do this as the main framework, and then go back through and implement it for other parameters. So, I renamed this one but it doesn't support after at the moment.

@@ -0,0 +1,133 @@
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This model isn't tested anywhere?

Comment thread pywr-core/src/parameters/aggregated.rs Outdated

impl AggregatedParameterBuilder {
pub fn new(name: ParameterName, agg_func: AggFuncF64) -> Self {
/// Create a new builder for [`AggregatedParameter`] that is evaluated in the "after" phase.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"before"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants