muse2/simulation/optimisation/constraints.rs
1//! Code for adding constraints to the dispatch optimisation problem.
2use crate::asset::{AssetID, AssetPool};
3use crate::commodity::{CommodityID, CommodityType};
4use crate::model::Model;
5use crate::region::RegionID;
6use crate::time_slice::{TimeSliceID, TimeSliceInfo, TimeSliceSelection};
7use highs::RowProblem as Problem;
8use std::rc::Rc;
9
10use super::VariableMap;
11
12/// Indicates the commodity ID and time slice selection covered by each commodity balance constraint
13pub type CommodityBalanceConstraintKeys = Vec<(CommodityID, RegionID, TimeSliceSelection)>;
14
15/// Indicates the asset ID and time slice covered by each capacity constraint
16pub type CapacityConstraintKeys = Vec<(AssetID, TimeSliceID)>;
17
18/// Add asset-level constraints
19///
20/// Note: the ordering of constraints is important, as the dual values of the constraints must later
21/// be retrieved to calculate commodity prices.
22///
23/// # Arguments:
24///
25/// * `problem` - The optimisation problem
26/// * `variables` - The variables in the problem
27/// * `model` - The model
28/// * `assets` - The asset pool
29/// * `year` - Current milestone year
30///
31/// # Returns:
32///
33/// * A vector of keys for commodity balance constraints
34/// * A vector of keys for capacity constraints
35pub fn add_asset_constraints(
36 problem: &mut Problem,
37 variables: &VariableMap,
38 model: &Model,
39 assets: &AssetPool,
40 year: u32,
41) -> (CommodityBalanceConstraintKeys, CapacityConstraintKeys) {
42 let commodity_balance_constraint_keys =
43 add_commodity_balance_constraints(problem, variables, model, assets, year);
44
45 let capacity_constraint_keys = add_asset_capacity_constraints(
46 problem,
47 variables,
48 assets,
49 &model.time_slice_info,
50 &commodity_balance_constraint_keys,
51 );
52
53 // **TODO**: Currently it's safe to assume all process flows are non-flexible, as we enforce
54 // this when reading data in. Once we've added support for flexible process flows, we will
55 // need to add different constraints for assets with flexible and non-flexible flows.
56 //
57 // See: https://github.com/EnergySystemsModellingLab/MUSE_2.0/issues/360
58 add_fixed_asset_constraints(problem, variables, assets, &model.time_slice_info);
59
60 // Return constraint keys which are required to calculate commodity prices
61 (commodity_balance_constraint_keys, capacity_constraint_keys)
62}
63
64/// Add asset-level input-output commodity balances.
65///
66/// These constraints fix the supply-demand balance for the whole system.
67///
68/// See description in [the dispatch optimisation documentation][1].
69///
70/// [1]: https://energysystemsmodellinglab.github.io/MUSE_2.0/dispatch_optimisation.html#commodity-balance-constraints
71fn add_commodity_balance_constraints(
72 problem: &mut Problem,
73 variables: &VariableMap,
74 model: &Model,
75 assets: &AssetPool,
76 year: u32,
77) -> CommodityBalanceConstraintKeys {
78 // Sanity check: we rely on the first n values of the dual row values corresponding to the
79 // commodity constraints, so these must be the first rows
80 assert!(
81 problem.num_rows() == 0,
82 "Commodity balance constraints must be added before other constraints"
83 );
84
85 let mut terms = Vec::new();
86 let mut keys = CommodityBalanceConstraintKeys::new();
87 for commodity in model.commodities.values() {
88 if commodity.kind != CommodityType::SupplyEqualsDemand
89 && commodity.kind != CommodityType::ServiceDemand
90 {
91 continue;
92 }
93
94 for region_id in model.iter_regions() {
95 for ts_selection in model
96 .time_slice_info
97 .iter_selections_for_level(commodity.time_slice_level)
98 {
99 // Note about performance: this loop **may** prove to be a bottleneck as
100 // `time_slice_info.iter_selection` returns a `Box` and so requires a heap
101 // allocation each time. For commodities with a `TimeSliceLevel` of `TimeSlice` (the
102 // worst case), this means the number of additional heap allocations will equal the
103 // number of time slices, which for this function could be in the
104 // hundreds/thousands.
105 for (time_slice, _) in model.time_slice_info.iter_selection(&ts_selection) {
106 // Add terms for this asset + commodity at this time slice. The coefficient for
107 // each variable is one.
108 terms.extend(
109 assets
110 .iter_for_region_and_commodity(region_id, commodity)
111 .map(|asset| (variables.get(asset.id, &commodity.id, time_slice), 1.0)),
112 );
113 }
114
115 // Get the RHS of the equation for a commodity balance constraint. For SED
116 // commodities, the RHS will be zero and for SVD commodities it will be equal to the
117 // demand for the given time slice selection.
118 let rhs = match commodity.kind {
119 CommodityType::SupplyEqualsDemand => 0.0,
120 CommodityType::ServiceDemand => {
121 match ts_selection {
122 TimeSliceSelection::Single(ref ts) => *commodity
123 .demand
124 .get(&(region_id.clone(), year, ts.clone()))
125 .unwrap(),
126 // We currently only support specifying demand at the time slice level:
127 // https://github.com/EnergySystemsModellingLab/MUSE_2.0/issues/391
128 _ => panic!(
129 "Currently SVD commodities must have a time slice level of time slice"
130 ),
131 }
132 }
133 _ => unreachable!(),
134 };
135
136 // Add constraint (sum of terms must equal rhs)
137 problem.add_row(rhs..=rhs, terms.drain(0..));
138
139 // Keep track of the order in which constraints were added
140 keys.push((commodity.id.clone(), region_id.clone(), ts_selection));
141 }
142 }
143 }
144
145 keys
146}
147
148/// Add constraints for non-flexible assets.
149///
150/// Non-flexible assets are those which have a fixed ratio between inputs and outputs.
151///
152/// See description in [the dispatch optimisation documentation][1].
153///
154/// [1]: https://energysystemsmodellinglab.github.io/MUSE_2.0/dispatch_optimisation.html#non-flexible-assets
155fn add_fixed_asset_constraints(
156 problem: &mut Problem,
157 variables: &VariableMap,
158 assets: &AssetPool,
159 time_slice_info: &TimeSliceInfo,
160) {
161 for asset in assets.iter() {
162 // Get first PAC. unwrap is safe because all processes have at least one PAC.
163 let pac1 = asset.process.iter_pacs().next().unwrap();
164
165 for time_slice in time_slice_info.iter_ids() {
166 let pac_var = variables.get(asset.id, &pac1.commodity.id, time_slice);
167 let pac_term = (pac_var, -1.0 / pac1.flow);
168
169 for flow in asset.process.flows.iter() {
170 // Don't add a constraint for the PAC itself
171 if Rc::ptr_eq(&flow.commodity, &pac1.commodity) {
172 continue;
173 }
174
175 // We are enforcing that (var / flow) - (pac_var / pac_flow) = 0
176 let var = variables.get(asset.id, &flow.commodity.id, time_slice);
177 problem.add_row(0.0..=0.0, [(var, 1.0 / flow.flow), pac_term]);
178 }
179 }
180 }
181}
182
183/// Add asset-level capacity and availability constraints.
184///
185/// For every asset at every time slice, the sum of the commodity flows for PACs must not exceed the
186/// capacity limits, which are a product of the annual capacity, time slice length and process
187/// availability.
188///
189/// See description in [the dispatch optimisation documentation][1].
190///
191/// [1]: https://energysystemsmodellinglab.github.io/MUSE_2.0/dispatch_optimisation.html#asset-level-capacity-and-availability-constraints
192fn add_asset_capacity_constraints(
193 problem: &mut Problem,
194 variables: &VariableMap,
195 assets: &AssetPool,
196 time_slice_info: &TimeSliceInfo,
197 commodity_balance_constraint_keys: &CommodityBalanceConstraintKeys,
198) -> CapacityConstraintKeys {
199 // Sanity check: we rely on the dual rows corresponding to the capacity constraints being
200 // immediately after the commodity balance constraints in `problem`.
201 assert!(
202 problem.num_rows() == commodity_balance_constraint_keys.len(),
203 "Capacity constraints must be added immediately after commodity constraints."
204 );
205
206 let mut terms = Vec::new();
207 let mut keys = CapacityConstraintKeys::new();
208 for asset in assets.iter() {
209 for time_slice in time_slice_info.iter_ids() {
210 let mut is_input = false; // NB: there will be at least one PAC
211 for flow in asset.process.iter_pacs() {
212 is_input = flow.flow < 0.0; // NB: PACs will be all inputs or all outputs
213
214 let var = variables.get(asset.id, &flow.commodity.id, time_slice);
215 terms.push((var, 1.0));
216 }
217
218 let mut limits = asset.get_energy_limits(time_slice);
219
220 // If it's an input flow, the q's will be negative, so we need to invert the limits
221 if is_input {
222 limits = -limits.end()..=-limits.start();
223 }
224
225 problem.add_row(limits, terms.drain(0..));
226
227 // Keep track of the order in which constraints were added
228 keys.push((asset.id, time_slice.clone()));
229 }
230 }
231 keys
232}