Skip to content

Commit ef90ffc

Browse files
authored
Merge pull request #1137 from EnergySystemsModellingLab/add-unmet-demand-threshold
add remaining_demand_absolute_tolerance param
2 parents e996669 + f4026b4 commit ef90ffc

3 files changed

Lines changed: 117 additions & 10 deletions

File tree

schemas/input/model.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,12 @@ properties:
5050
type: boolean
5151
description: |
5252
Allows other options that are known to be broken to be used. Please don't ever enable this.
53+
remaining_demand_absolute_tolerance:
54+
type: number
55+
description: |
56+
Absolute tolerance when checking if remaining demand is close enough to zero in the
57+
investment cycle. Changing the value of this parameter is potentially dangerous,
58+
so it requires setting `please_give_me_broken_results` to true.
59+
default: 1e-12
5360

5461
required: [milestone_years]

src/model/parameters.rs

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::asset::check_capacity_valid_for_asset;
77
use crate::input::{
88
deserialise_proportion_nonzero, input_err_msg, is_sorted_and_unique, read_toml,
99
};
10-
use crate::units::{Capacity, Dimensionless, MoneyPerFlow};
10+
use crate::units::{Capacity, Dimensionless, Flow, MoneyPerFlow};
1111
use anyhow::{Context, Result, ensure};
1212
use log::warn;
1313
use serde::Deserialize;
@@ -76,6 +76,7 @@ define_unit_param_default!(default_candidate_asset_capacity, Capacity, 0.0001);
7676
define_unit_param_default!(default_capacity_limit_factor, Dimensionless, 0.1);
7777
define_unit_param_default!(default_value_of_lost_load, MoneyPerFlow, 1e9);
7878
define_unit_param_default!(default_price_tolerance, Dimensionless, 1e-6);
79+
define_unit_param_default!(default_remaining_demand_absolute_tolerance, Flow, 1e-12);
7980
define_param_default!(default_max_ironing_out_iterations, u32, 10);
8081
define_param_default!(default_capacity_margin, f64, 0.2);
8182
define_param_default!(default_mothball_years, u32, 0);
@@ -123,6 +124,9 @@ pub struct ModelParameters {
123124
/// Number of years an asset can remain unused before being decommissioned
124125
#[serde(default = "default_mothball_years")]
125126
pub mothball_years: u32,
127+
/// Absolute tolerance when checking if remaining demand is close enough to zero
128+
#[serde(default = "default_remaining_demand_absolute_tolerance")]
129+
pub remaining_demand_absolute_tolerance: Flow,
126130
}
127131

128132
/// Check that the `milestone_years` parameter is valid
@@ -164,6 +168,29 @@ fn check_price_tolerance(value: Dimensionless) -> Result<()> {
164168
Ok(())
165169
}
166170

171+
fn check_remaining_demand_absolute_tolerance(
172+
allow_broken_options: bool,
173+
value: Flow,
174+
) -> Result<()> {
175+
ensure!(
176+
value.is_finite() && value >= Flow(0.0),
177+
"remaining_demand_absolute_tolerance must be a finite number greater than or equal to zero"
178+
);
179+
180+
let default_value = default_remaining_demand_absolute_tolerance();
181+
if !allow_broken_options {
182+
ensure!(
183+
value == default_value,
184+
"Setting a remaining_demand_absolute_tolerance different from the default value of {:e} \
185+
is potentially dangerous, set please_give_me_broken_results to true \
186+
if you want to allow this.",
187+
default_value.0
188+
);
189+
}
190+
191+
Ok(())
192+
}
193+
167194
/// Check that the `capacity_margin` parameter is valid
168195
fn check_capacity_margin(value: f64) -> Result<()> {
169196
ensure!(
@@ -229,6 +256,12 @@ impl ModelParameters {
229256
// capacity_margin
230257
check_capacity_margin(self.capacity_margin)?;
231258

259+
// remaining_demand_absolute_tolerance
260+
check_remaining_demand_absolute_tolerance(
261+
self.allow_broken_options,
262+
self.remaining_demand_absolute_tolerance,
263+
)?;
264+
232265
Ok(())
233266
}
234267
}
@@ -356,6 +389,57 @@ mod tests {
356389
);
357390
}
358391

392+
#[rstest]
393+
#[case(true, 0.0, true)] // Valid minimum value broken options allowed
394+
#[case(true, 1e-10, true)] // Valid value with broken options allowed
395+
#[case(true, 1e-15, true)] // Valid value with broken options allowed
396+
#[case(false, 1e-12, true)] // Valid value same as default, no broken options needed
397+
#[case(true, 1.0, true)] // Valid larger value with broken options allowed
398+
#[case(true, f64::MAX, true)] // Valid maximum finite value with broken options allowed
399+
#[case(true, -1e-10, false)] // Invalid: negative value
400+
#[case(true, f64::INFINITY, false)] // Invalid: positive infinity
401+
#[case(true, f64::NEG_INFINITY, false)] // Invalid: negative infinity
402+
#[case(true, f64::NAN, false)] // Invalid: NaN
403+
#[case(false, -1e-10, false)] // Invalid: negative value
404+
#[case(false, f64::INFINITY, false)] // Invalid: positive infinity
405+
#[case(false, f64::NEG_INFINITY, false)] // Invalid: negative infinity
406+
#[case(false, f64::NAN, false)] // Invalid: NaN
407+
fn check_remaining_demand_absolute_tolerance_works(
408+
#[case] allow_broken_options: bool,
409+
#[case] value: f64,
410+
#[case] expected_valid: bool,
411+
) {
412+
let flow = Flow::new(value);
413+
let result = check_remaining_demand_absolute_tolerance(allow_broken_options, flow);
414+
415+
assert_validation_result(
416+
result,
417+
expected_valid,
418+
value,
419+
"remaining_demand_absolute_tolerance must be a finite number greater than or equal to zero",
420+
);
421+
}
422+
423+
#[rstest]
424+
#[case(0.0)] // smaller than default
425+
#[case(1e-10)] // Larger than default (1e-12)
426+
#[case(1.0)] // Well above default
427+
#[case(f64::MAX)] // Maximum finite value
428+
fn check_remaining_demand_absolute_tolerance_requires_broken_options_if_non_default(
429+
#[case] value: f64,
430+
) {
431+
let flow = Flow::new(value);
432+
let result = check_remaining_demand_absolute_tolerance(false, flow);
433+
assert_validation_result(
434+
result,
435+
false,
436+
value,
437+
"Setting a remaining_demand_absolute_tolerance different from the default value \
438+
of 1e-12 is potentially dangerous, set \
439+
please_give_me_broken_results to true if you want to allow this.",
440+
);
441+
}
442+
359443
#[rstest]
360444
#[case(0.0, true)] // Valid minimum value
361445
#[case(0.2, true)] // Valid default value

src/simulation/investment.rs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::region::RegionID;
99
use crate::simulation::CommodityPrices;
1010
use crate::time_slice::{TimeSliceID, TimeSliceInfo};
1111
use crate::units::{Capacity, Dimensionless, Flow, FlowPerCapacity};
12-
use anyhow::{Context, Result, ensure};
12+
use anyhow::{Context, Result, bail, ensure};
1313
use indexmap::IndexMap;
1414
use itertools::{Itertools, chain};
1515
use log::debug;
@@ -743,7 +743,10 @@ fn select_best_assets(
743743
// Iteratively select the best asset until demand is met
744744
let mut round = 0;
745745
let mut best_assets: Vec<AssetRef> = Vec::new();
746-
while is_any_remaining_demand(&demand) {
746+
while is_any_remaining_demand(
747+
&demand,
748+
model.parameters.remaining_demand_absolute_tolerance,
749+
) {
747750
ensure!(
748751
!opt_assets.is_empty(),
749752
"Failed to meet demand for commodity '{}' in region '{}' with provided investment \
@@ -805,11 +808,24 @@ fn select_best_assets(
805808
// demand.
806809
// - known issue with the NPV objective
807810
// (see https://github.com/EnergySystemsModellingLab/MUSE2/issues/716).
808-
ensure!(
809-
!outputs_for_opts.is_empty(),
810-
"No feasible investment options for commodity '{}' after appraisal",
811-
&commodity.id
812-
);
811+
if outputs_for_opts.is_empty() {
812+
let remaining_demands: Vec<_> = demand
813+
.iter()
814+
.filter(|(_, flow)| **flow > Flow(0.0))
815+
.map(|(time_slice, flow)| format!("{} : {:e}", time_slice, flow.value()))
816+
.collect();
817+
818+
bail!(
819+
"No feasible investment options left for \
820+
commodity '{}', region '{}', year '{}', agent '{}' after appraisal.\n\
821+
Remaining unmet demand (time_slice : flow):\n{}",
822+
&commodity.id,
823+
region_id,
824+
year,
825+
agent.id,
826+
remaining_demands.join("\n")
827+
);
828+
}
813829

814830
// Warn if there are multiple equally good assets
815831
warn_on_equal_appraisal_outputs(&outputs_for_opts, &agent.id, &commodity.id, region_id);
@@ -851,8 +867,8 @@ fn select_best_assets(
851867
}
852868

853869
/// Check whether there is any remaining demand that is unmet in any time slice
854-
fn is_any_remaining_demand(demand: &DemandMap) -> bool {
855-
demand.values().any(|flow| *flow > Flow(0.0))
870+
fn is_any_remaining_demand(demand: &DemandMap, absolute_tolerance: Flow) -> bool {
871+
demand.values().any(|flow| *flow > absolute_tolerance)
856872
}
857873

858874
/// Update capacity of chosen asset, if needed, and update both asset options and chosen assets

0 commit comments

Comments
 (0)