Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
51 changes: 51 additions & 0 deletions docs/model/dispatch_optimisation.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,57 @@ satisfying an additional unit of demand for that commodity in region \\( r \\) d

---

## Secondary Activity Equalisation Solve

In some dispatch runs, multiple activity patterns have identical (or near-identical) total system
cost. For LP solvers, this is a degenerate optimum: the algorithm returns an extreme-point
solution, which can concentrate activity in a small subset of assets or time slices even when many
alternatives have the same primary cost. This can look arbitrary and can vary between otherwise
similar runs.

To make dispatch patterns more stable and interpretable, MUSE2 can optionally run a second
lexicographic solve when `dispatch_activity_equalisation` is enabled:

1. Primary solve: minimise total dispatch cost, giving optimal value \\( Z^* \\).
2. Secondary solve: minimise activity spread, while constraining cost to remain near-optimal:
\\[
\sum\_{a \in \mathbf{A}} \sum\_{t \in \mathbf{T}}
\mathrm{Activity}\_{a,t} \cdot \mathrm{Cost}\_{\mathrm{Activity},a,t}
\le Z^*(1+\tau)
\\]
where \\( \tau \\) is the `dispatch_activity_equalisation_tolerance` model parameter.

The secondary objective minimises pairwise \\( L_1 \\) differences in utilisation rates \\( u \\)
(activity as a fraction of the maximum), using auxiliary variables. For each pair
\\( (i,j) \\) in an equalisation group:
\\[
d_{ij} \ge u_i - u_j,\qquad d_{ij} \ge u_j - u_i
\\]
so \\( d_{ij} \\) represents \\( |u_i-u_j| \\). The secondary objective is:
\\[
\mathrm{Minimise } \sum_{(i,j)} d_{ij}
\\]

Equalisation is applied across two group types:

- Asset groups by commodity, region, and time slice, to spread utilisation across assets with the
same primary output commodity in the same market.
- Time-slice groups within each asset (at annual or seasonal balancing levels), to spread activity
across time slices that are not independently balanced.

Assets balanced at day-night level are excluded from the time-slice equalisation groups, since
their activity is already constrained at that finer resolution.

This secondary solve does not replace the economic objective. Reported dispatch objective values
remain the primary cost objective \\( Z^* \\); the equalisation objective is only a tie-breaker among
near-optimal cost solutions.

There is a performance cost. Enabling equalisation adds a second full LP solve plus extra
auxiliary variables and constraints (pairwise terms grow approximately quadratically with group
size), which can increase runtime and memory use for large dispatch problems.

---

## Candidate Dispatch Run

After the primary dispatch run, MUSE2 performs a second dispatch run that includes
Expand Down
15 changes: 15 additions & 0 deletions schemas/input/model.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ properties:
default: 1e-6
notes: |
The default value should work. Do not change this value unless you know what you're doing!
dispatch_activity_equalisation:
type: boolean
description: Whether to run a second lexicographic solve to encourage even dispatch across assets
default: true
notes: |
When enabled, a second LP solve minimises the L1 spread of utilisation fractions subject
to total cost remaining within dispatch_activity_equalisation_tolerance of the optimal.
Discourages corner solutions where all activity is concentrated on a single asset.
dispatch_activity_equalisation_tolerance:
type: number
description: Fractional cost tolerance for the lexicographic second solve
default: 1e-6
notes: |
The second solve constrains total cost to at most Z* * (1 + tolerance) where Z* is the
optimal cost from the first solve.
Comment thread
tsmbland marked this conversation as resolved.
capacity_limit_factor:
type: number
description: Scales the capacity assigned to candidate assets
Expand Down
12 changes: 12 additions & 0 deletions src/model/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ pub struct ModelParameters {
/// Don't change unless you know what you're doing.
#[serde(deserialize_with = "deserialise_finite_non_negative")]
pub commodity_balance_epsilon: Flow,
/// Whether to run a second lexicographic solve to encourage even dispatch across assets.
pub dispatch_activity_equalisation: bool,
/// Fractional tolerance on the primary cost for the lexicographic second solve.
///
/// The second solve constrains total cost to at most `Z* * (1 + tolerance)`, where `Z*` is
/// the optimal cost from the first solve.
#[serde(deserialize_with = "deserialise_finite_non_negative")]
pub dispatch_activity_equalisation_tolerance: Dimensionless,
Comment thread
tsmbland marked this conversation as resolved.
/// Affects the maximum capacity that can be given to a newly created asset.
///
/// It is the proportion of maximum capacity that could be required across time slices.
Expand Down Expand Up @@ -135,6 +143,8 @@ impl Default for ModelParameters {
allow_dangerous_options: false,
candidate_asset_capacity: Capacity(1e-4),
commodity_balance_epsilon: Flow(1e-6),
dispatch_activity_equalisation: true,
dispatch_activity_equalisation_tolerance: Dimensionless(1e-6),
capacity_limit_factor: Dimensionless(0.05),
fallback_pricing_strategy: PricingStrategy::FullCostAverage,
value_of_lost_load: MoneyPerFlow(1e9),
Expand Down Expand Up @@ -332,6 +342,8 @@ impl ModelParameters {

// commodity_balance_epsilon already validated with deserialise_finite_non_negative

// dispatch_activity_equalisation_tolerance already validated with deserialise_finite_non_negative

// value_of_lost_load
check_value_of_lost_load(self.value_of_lost_load)?;

Expand Down
Loading
Loading