Skip to content
Open
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
19 changes: 19 additions & 0 deletions docs/model/dispatch_optimisation.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,25 @@ fraction of the year.
lower and upper availability fractions from `process_activity_limits.csv`, defaulting to
\\( 0 \\) and \\( 1 \\) respectively for any selection not explicitly defined.

### Equal Process Utilisation

Assets representing the same process in the same region are effectively equivalent. To avoid
arbitrarily utilising one over another, the dispatch model adds additional constraints to equalise
the utilisation of equivalent assets. For an asset \\(a\\) in time slice \\(t\\), utilisation is
defined as

\\[
\\mathrm{Utilisation}\_{a,t} =
\\frac{\\mathrm{Activity}\_{a,t}}{\\mathrm{Capacity}_a \\cdot \\mathrm{cap2act}_a}
\\]

For every pair of assets \\( x \\) and \\( y \\) representing the same process in the same
region, and for every time slice \\( t \\), the optimisation model imposes:

\\[
\\mathrm{Utilisation}\_{x,t} = \\mathrm{Utilisation}\_{y,t}
\\]
Comment thread
tsmbland marked this conversation as resolved.

### Commodity Balance Constraints

For each balanced commodity \\( c \in \mathbf{C}^{\mathrm{SED}} \cup \mathbf{C}^{\mathrm{SVD}} \\)
Expand Down
63 changes: 63 additions & 0 deletions src/simulation/optimisation/constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ use super::VariableMap;
use crate::asset::{AssetCapacity, AssetIterator, AssetRef};
use crate::commodity::{CommodityID, CommodityType};
use crate::model::Model;
use crate::process::ProcessID;
use crate::region::RegionID;
use crate::time_slice::{Season, TimeSliceInfo, TimeSliceSelection};
use crate::units::{Flow, MoneyPerCapacityPerYear, UnitType, Year};
use highs::RowProblem as Problem;
use indexmap::IndexMap;
use std::collections::HashSet;

/// Corresponding variables for a constraint along with the row offset in the solution
pub struct KeysWithOffset<T> {
Expand Down Expand Up @@ -106,6 +108,8 @@ where

add_utilisation_peak_constraints(problem, model, assets.clone(), variables);

add_equal_utilisation_constraints(problem, variables, &model.time_slice_info, assets.clone());
Comment thread
tsmbland marked this conversation as resolved.

// Return constraint keys
ConstraintKeys {
commodity_balance_keys,
Expand Down Expand Up @@ -470,6 +474,65 @@ where
ActivityKeys { offset, keys }
}

/// Add constraints requiring assets of the same process in the same region to have equal
/// utilisation in each time slice.
///
/// Flexible-capacity assets are excluded because their maximum activity depends on a decision
/// variable. The constraints added here are not included in [`ConstraintKeys`], as their duals
/// are not currently used.
fn add_equal_utilisation_constraints<'a, I>(
problem: &mut Problem,
variables: &VariableMap,
time_slice_info: &TimeSliceInfo,
assets: I,
) where
I: Iterator<Item = &'a AssetRef> + 'a,
{
// Identify flexible-capacity assets so we can exclude them from the constraints
let flexible_assets: HashSet<_> = variables
.iter_capacity_vars()
.map(|(asset, _)| asset)
.collect();

// Group together assets with the same process and region
let mut assets_by_process: IndexMap<(RegionID, ProcessID), Vec<&AssetRef>> = IndexMap::new();
for asset in assets.filter(|asset| !flexible_assets.contains(asset)) {
assets_by_process
.entry((asset.region_id().clone(), asset.process_id().clone()))
.or_default()
.push(asset);
}

// For each group of assets, add constraints to force equal utilisation in each time slice
// This is done by anchoring each asset to the first asset in the group (-> (n-1) constraints
// for a group of n assets)
for assets in assets_by_process.into_values() {
let Some((reference_asset, others)) = assets.split_first() else {
continue;
};

let reference_max = reference_asset.max_activity().value();

for asset in others {
let asset_max = asset.max_activity().value();

for time_slice in time_slice_info.iter_ids() {
// Constraint: (act_a * max_b) - (act_b * max_a) = 0
problem.add_row(
0.0..=0.0,
[
(variables.get_activity_var(asset, time_slice), reference_max),
(
variables.get_activity_var(reference_asset, time_slice),
-asset_max,
),
],
);
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
6 changes: 3 additions & 3 deletions tests/data/circularity/asset_capacities.csv
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ milestone_year,asset_id,capacity,num_units
2030,17,3680.183145405078,
2030,18,298.6475820926018,
2030,19,2944.1465163240623,
2030,20,163.3092156793555,
2030,20,165.35058087534745,
2040,1,1738.05,
2040,5,3.964844,
2040,6,2.999,
2040,16,1849.2839907535792,
2040,18,298.6475820926018,
2040,20,163.3092156793555,
2040,20,165.35058087534745,
2040,21,912.8939641298446,
2040,22,2162.373384722901,
2040,23,33.51213708228713,
Expand All @@ -43,4 +43,4 @@ milestone_year,asset_id,capacity,num_units
2040,28,972.4856516656134,
2040,29,2349.697251409409,
2040,30,505.4252398153141,
2040,31,2592.9322599984084,
2040,31,2576.6013383651484,
Loading
Loading