Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/model/prices.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,11 @@ those furthest upstream relative to the rest of the system (i.e., furthest from
downstream of the SCC), and working down. Marginal cost calculations use newly updated prices for
input commodities already evaluated in the sequence, and fall back to the seeded shadow prices for
any not yet evaluated, which may occur due to circular dependencies.
<!-- Currently a hidden option, TBD whether we want to open this up: -->
<!-- 2. **Iterative Refinement:** An iterative loop runs for a fixed number of iterations,
3. **Iterative Refinement:** This process repeats for a fixed number of iterations,
re-evaluating each market in the cycle in reverse order (starting with those
furthest from commodities downstream of the SCC) to propagate the feedback effects through the
circular markets. -->
circular markets. The number of iterations is controlled by the `price_cycle_iterations` parameter
in `model.toml` (default = 1 means just a single pass).

<!-- markdownlint-disable MD024 -->
## Example
Expand Down
2 changes: 2 additions & 0 deletions docs/release_notes/upcoming.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## New features

- Seasonal/annual utilisation penalties are now included in the investment mini-dispatch ([#1548])
- The number of price calculation iterations for cyclically-dependent markets can now be configured
with `price_cycle_iterations` in `model.toml` (default: 1)

## Breaking changes

Expand Down
4 changes: 4 additions & 0 deletions schemas/input/model.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ properties:
type: number
description: The relative tolerance for price convergence in the ironing out loop
default: 1e-6
price_cycle_iterations:
type: integer
description: Number of iterations to perform when calculating prices for cyclically-dependent markets
default: 1
capacity_margin:
type: number
description: Slack proportion for assets selected during cycle balancing to absorb small demand shifts
Expand Down
37 changes: 37 additions & 0 deletions src/model/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ pub struct ModelParameters {
/// The relative tolerance for price convergence in the ironing out loop
#[serde(deserialize_with = "deserialise_finite_non_negative")]
pub price_tolerance: Dimensionless,
/// Number of iterations to perform when calculating prices for cyclically-dependent markets.
pub price_cycle_iterations: u32,
/// Slack applied during cycle balancing, allowing newly selected assets to flex their capacity
/// by this proportion.
///
Expand Down Expand Up @@ -152,6 +154,7 @@ impl Default for ModelParameters {
annual_utilisation_penalty: MoneyPerCapacityPerYear(1e-5),
max_ironing_out_iterations: 1,
price_tolerance: Dimensionless(1e-6),
price_cycle_iterations: 1,
capacity_margin: Dimensionless(0.2),
mothball_years: 0,
remaining_demand_absolute_tolerance: DEFAULT_REMAINING_DEMAND_ABSOLUTE_TOLERANCE,
Expand Down Expand Up @@ -263,6 +266,13 @@ fn check_max_ironing_out_iterations(value: u32) -> Result<()> {
Ok(())
}

/// Check that the number of cycle price iterations is valid.
fn check_price_cycle_iterations(value: u32) -> Result<()> {
ensure!(value > 0, "price_cycle_iterations cannot be zero");

Ok(())
}

/// Check that the `remaining_demand_absolute_tolerance` parameter is valid.
fn check_remaining_demand_absolute_tolerance(
dangerous_options_enabled: bool,
Expand Down Expand Up @@ -355,6 +365,9 @@ impl ModelParameters {

// price_tolerance already validated with deserialise_finite_non_negative

// price_cycle_iterations
check_price_cycle_iterations(self.price_cycle_iterations)?;

// capacity_margin already validated with deserialise_finite_non_negative

// remaining_demand_absolute_tolerance already validated with
Expand Down Expand Up @@ -428,6 +441,15 @@ mod tests {

let model_params = ModelParameters::from_path(dir.path()).unwrap();
assert_eq!(model_params.milestone_years, [2020, 2100]);
assert_eq!(model_params.price_cycle_iterations, 1);
}

#[test]
fn model_params_deserialises_price_cycle_iterations() {
let model_params: ModelParameters =
toml::from_str("milestone_years = [2020, 2100]\nprice_cycle_iterations = 3").unwrap();

assert_eq!(model_params.price_cycle_iterations, 3);
}

#[test]
Expand Down Expand Up @@ -585,6 +607,21 @@ mod tests {
);
}

#[rstest]
#[case(1, true)]
#[case(10, true)]
#[case(0, false)]
fn check_price_cycle_iterations_works(#[case] value: u32, #[case] expected_valid: bool) {
let result = check_price_cycle_iterations(value);

assert_validation_result(
result,
expected_valid,
value,
"price_cycle_iterations cannot be zero",
);
}

#[rstest]
#[case(true, 1e-12, true)] // Valid: default value, dangerous options allowed
#[case(true, 1.0, true)] // Valid: non-default value with dangerous options allowed
Expand Down
5 changes: 1 addition & 4 deletions src/simulation/prices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ use indexmap::IndexMap;
use std::collections::{HashMap, HashSet};
use std::marker::PhantomData;

/// Number of iterations to perform when calculating prices for cyclically-dependent markets.
const N_CYCLE_ITERATIONS: i32 = 1;

/// Weighted average accumulator for `MoneyPerFlow` prices.
#[derive(Clone, Copy, Debug)]
struct WeightedAverageAccumulator<W: UnitType> {
Expand Down Expand Up @@ -359,7 +356,7 @@ fn price_cycle(
}

// Iterate over the markets for a fixed number of iterations, updating prices each time
for _ in 0..N_CYCLE_ITERATIONS {
for _ in 0..model.parameters.price_cycle_iterations {
Comment thread
tsmbland marked this conversation as resolved.
// Price markets in reverse order (i.e. upstream markets first)
for market in markets.iter().rev() {
price_markets(
Expand Down
Loading