Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1137 +/- ##
==========================================
- Coverage 87.52% 87.46% -0.07%
==========================================
Files 55 55
Lines 7626 7652 +26
Branches 7626 7652 +26
==========================================
+ Hits 6675 6693 +18
- Misses 649 657 +8
Partials 302 302 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR adds a new remaining_demand_absolute_tolerance parameter to address issue #1115, where the investment appraisal would fail with NaN errors when service demand remained constant across consecutive milestone years. The tolerance allows the model to treat negligibly small remaining demand as zero, preventing spurious failures.
Changes:
- Added
remaining_demand_absolute_toleranceparameter with default value of 1e-12 - Modified
is_any_remaining_demandfunction to use the tolerance when checking for unmet demand - Added validation function and comprehensive unit tests for the new parameter
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/model/parameters.rs | Defines the new remaining_demand_absolute_tolerance parameter with default value, validation function, and comprehensive unit tests |
| src/simulation/investment.rs | Updates is_any_remaining_demand to use the new tolerance parameter when checking if demand is met |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/model/parameters.rs
Outdated
| /// Absolute tolerance when checking if remaining demand is close enough to zero | ||
| #[serde(default = "default_remaining_demand_absolute_tolerance")] | ||
| pub remaining_demand_absolute_tolerance: Dimensionless, |
There was a problem hiding this comment.
The schemas/input/model.yaml file needs to be updated to include the new remaining_demand_absolute_tolerance parameter. As noted in the comment at line 90-91 of this file, when adding or changing a field in ModelParameters, the schema must also be updated. This parameter should be documented similar to other parameters like price_tolerance in the schema file.
src/simulation/investment.rs
Outdated
| fn is_any_remaining_demand(demand: &DemandMap, absolute_tolerance: f64) -> bool { | ||
| demand.values().any(|flow| *flow > Flow(absolute_tolerance)) | ||
| } |
There was a problem hiding this comment.
The modified is_any_remaining_demand function lacks test coverage. A test should be added to verify the behavior with different tolerance values, including edge cases like:
- Demand exactly equal to tolerance (should return false)
- Demand slightly above tolerance (should return true)
- Demand slightly below tolerance (should return false)
- Demand with zero tolerance
This is important because this function is central to fixing the bug described in issue Investment appraisal fails when demand is equal in 2 consecutive years in (a slight adaptation of) the simple model #1115.
src/model/parameters.rs
Outdated
| #[rstest] | ||
| #[case(0.0, true)] // Valid minimum value (exactly zero) | ||
| #[case(1e-10, true)] // Valid very small positive value | ||
| #[case(1e-6, true)] // Valid default value |
There was a problem hiding this comment.
The comment says "Valid default value" but uses 1e-6 instead of the actual default value 1e-12 defined at line 82. Update this test case to use the correct default value of 1e-12.
alexdewar
left a comment
There was a problem hiding this comment.
Thanks for this!
A couple of changes:
- I'd make the parameter a
Flowvalue, because that's what it actually represents - I'd gate this option behind the
please_give_me_broken_resultsoption, because it's potentially pretty dangerous. As it stands, the user could set it to 100 or something, which would be nonsensical. We could give a warning/error if the user sets it to something unreasonably high, but that does require making a judgment about what values are appropriate. So I'd just assume that this tolerance is good enough for the foreseeable future and if it turns out that it isn't for a (reasonable) model someone is using, then we can consider changing the default. This is basically an expert-only option, I think.
src/model/parameters.rs
Outdated
| define_unit_param_default!(default_price_tolerance, Dimensionless, 1e-6); | ||
| define_unit_param_default!( | ||
| default_remaining_demand_absolute_tolerance, | ||
| Dimensionless, |
There was a problem hiding this comment.
I think this should actually be Flow. (Yes, it is a bit weird that we are using one number for any commodity flow type, but that seems to be what's needed!)
src/simulation/investment.rs
Outdated
| /// Check whether there is any remaining demand that is unmet in any time slice | ||
| fn is_any_remaining_demand(demand: &DemandMap) -> bool { | ||
| demand.values().any(|flow| *flow > Flow(0.0)) | ||
| fn is_any_remaining_demand(demand: &DemandMap, absolute_tolerance: f64) -> bool { |
There was a problem hiding this comment.
As above, this should be of type Flow.
tsmbland
left a comment
There was a problem hiding this comment.
Agree with @alexdewar's comments, but otherwise looks good.
Surprised we got away without this for so long, to be honest.
FWIW we have a similar parameter in MUSE1 (tolerance_unmet_demand) which is also an absolute tolerance. This is freely accessible to users and set by default at a much higher value of 0.1. The difference is that in MUSE1 this tolerance check happens after investments are complete, whereas here we're doing checks while investments are ongoing. Therefore, a large tolerance is dangerous not just because supply might end up lower than demand (as is the case in MUSE1), but that it most likely will end up lower than demand because investments will terminate early. Because of this, I agree with @alexdewar that this is not something that users should customise and we should guard it behind please_give_me_broken_results.
…oken_options set units of remaining_demand_absolute_tolerance to Flow
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| description: | | ||
| Absolute tolerance when checking if remaining demand is close enough to zero in the | ||
| investment cycle. | ||
| default: 1e-12 |
There was a problem hiding this comment.
The validation currently rejects remaining_demand_absolute_tolerance values above the default unless please_give_me_broken_results is enabled, but the schema text doesn't mention this constraint. Either document this requirement in the schema description/notes (and other docs) or relax the validation to allow safe increases without forcing the broken-results flag.
| default: 1e-12 | |
| default: 1e-12 | |
| notes: | | |
| This is a very strict tolerance; the default value is recommended for most cases. | |
| The validation currently rejects values larger than the default unless | |
| `please_give_me_broken_results` is set to true. Increasing this tolerance may | |
| lead to incorrect or misleading investment results and should only be done with | |
| a full understanding of the implications. |
src/model/parameters.rs
Outdated
| "remaining_demand_absolute_tolerance must be a finite number greater than or equal to zero" | ||
| ); | ||
|
|
||
| // we already checked value is positive, but if they are |
There was a problem hiding this comment.
This comment says the value was checked to be "positive", but the validation allows Flow(0.0) (non-negative). Consider updating the wording to avoid implying that zero is invalid.
| // we already checked value is positive, but if they are | |
| // we already checked value is non-negative, but if they are |
src/model/parameters.rs
Outdated
| "Setting a remaining_demand_absolute_tolerance higher than the default value \ | ||
| of 1e-12 is potentially dangerous, set \ | ||
| please_give_me_broken_results to true if you want to allow this.", |
There was a problem hiding this comment.
The expected error fragment in this test is whitespace-sensitive due to \ line continuations and indentation. The actual ensure! message inserts indentation spaces after {:e} (before "is potentially"), while this fragment inserts indentation spaces before "of 1e-12"; as a result, error_message.contains(...) can fail even when the correct error is produced. Consider asserting on a shorter, whitespace-robust fragment (e.g., containing "higher than the default value" and "please_give_me_broken_results") or normalizing whitespace before comparison.
| "Setting a remaining_demand_absolute_tolerance higher than the default value \ | |
| of 1e-12 is potentially dangerous, set \ | |
| please_give_me_broken_results to true if you want to allow this.", | |
| "please_give_me_broken_results", |
| /// Absolute tolerance when checking if remaining demand is close enough to zero | ||
| #[serde(default = "default_remaining_demand_absolute_tolerance")] | ||
| pub remaining_demand_absolute_tolerance: Flow, |
There was a problem hiding this comment.
The PR description mentions adding a configurable parameter demand_tolerance, but the implemented/configured parameter name is remaining_demand_absolute_tolerance (Rust + schema). Please align the PR description (and any user-facing docs/changelog, if applicable) with the actual parameter name to avoid confusion for model authors.
src/model/parameters.rs
Outdated
| let default_value = default_remaining_demand_absolute_tolerance(); | ||
| if !allow_broken_options { | ||
| ensure!( | ||
| value <= default_value, |
There was a problem hiding this comment.
I'd change this to a == because it's probably not advisable for users to decrease it below the default either
There was a problem hiding this comment.
ah ok I commented about this below, I'll update it to checking for equality
There was a problem hiding this comment.
I think longer run we need a distinction between "broken options" and "developer options". I think this parameter is something that, for now, only developers should be allowed to change, but it isn't necessarily "broken" (especially, as you say, if reducing it below the default).
|
Thanks both, I've added @alexdewar suggested changes. Although one thing I thought as I was making the changes is that it makes sense to me to let them change the parameter freely without please_give_me_broken_results as long as they are making it smaller than the default i.e. any value 0 <= remaining_demand_absolute_tolerance <= 1e-12 (default). Can change this though if you think it's better to block any non-default value. I'l also wondering if we should add more detail for the user in this error so it's clear what's caused the lack of feasible investments without looking at the code. Not sure the best way, maybe we could output all the non-zero unmet demands? On the other hand I suppose you can get the information you need --debug-model option. |
I think this is a good idea. We should also think about all the scenarios in which this might happen. With NPV now supposedly fixed (we should update the comment to remove this), I think that too restrictive investment constraints and the point in the comment about activity limits are the only ways this can happen (other issues should be flagged at the validation stage, in theory...) |
force all non-default remaining_demand_absolute_tolerance parameter changes to require borken results enabled
|
I've added more details when no feasible investment options, so for example, with the original issue #1115, the error would now look like the below instead: |
Description
This adds a new configurable model parameter demand_tolerance so that model won't fail if the remaining demand is negligible
Fixes #1115
Type of change
Key checklist
$ cargo test$ cargo docpresent in the previous release
Further checks