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}