muse2/simulation/investment/appraisal/
constraints.rs

1//! Constraints for the optimisation problem.
2use super::DemandMap;
3use super::optimisation::Variable;
4use crate::asset::{AssetRef, AssetState};
5use crate::commodity::Commodity;
6use crate::time_slice::{TimeSliceID, TimeSliceInfo};
7use crate::units::{Capacity, Flow};
8use highs::RowProblem as Problem;
9use indexmap::IndexMap;
10
11/// Adds a capacity constraint to the problem.
12///
13/// The behaviour depends on whether the asset is commissioned or a candidate:
14/// - For a commissioned asset, the capacity is fixed.
15/// - For a candidate asset, the capacity is variable between zero and an upper bound.
16pub fn add_capacity_constraint(
17    problem: &mut Problem,
18    asset: &AssetRef,
19    max_capacity: Option<Capacity>,
20    capacity_var: Variable,
21) {
22    let capacity = max_capacity.unwrap_or(asset.capacity());
23    let bounds = match asset.state() {
24        AssetState::Commissioned { .. } => {
25            // Fixed capacity for commissioned assets
26            capacity.value()..=capacity.value()
27        }
28        AssetState::Candidate => {
29            // Variable capacity between 0 and max for candidate assets
30            0.0..=capacity.value()
31        }
32        _ => panic!(
33            "add_capacity_constraint should only be called with Commissioned or Candidate assets"
34        ),
35    };
36    problem.add_row(bounds, [(capacity_var, 1.0)]);
37}
38
39/// Adds activity constraints to the problem.
40///
41/// Constrains the activity variables to be within the asset's activity limits.
42///
43/// The behaviour depends on whether the asset is commissioned or a candidate:
44/// - For an commissioned asset, the activity limits have fixed bounds based on the asset's (fixed)
45///   capacity.
46/// - For a candidate asset, the activity limits depend on the capacity of the asset, which is
47///   itself variable. The constraints are therefore applied to both the capacity and activity
48///   variables. We need separate constraints for the upper and lower bounds.
49pub fn add_activity_constraints(
50    problem: &mut Problem,
51    asset: &AssetRef,
52    capacity_var: Variable,
53    activity_vars: &IndexMap<TimeSliceID, Variable>,
54) {
55    match asset.state() {
56        AssetState::Commissioned { .. } => {
57            add_activity_constraints_for_existing(problem, asset, activity_vars)
58        }
59        AssetState::Candidate => {
60            add_activity_constraints_for_candidate(problem, asset, capacity_var, activity_vars)
61        }
62        _ => panic!(
63            "add_activity_constraints should only be called with Commissioned or Candidate assets"
64        ),
65    }
66}
67
68fn add_activity_constraints_for_existing(
69    problem: &mut Problem,
70    asset: &AssetRef,
71    activity_vars: &IndexMap<TimeSliceID, Variable>,
72) {
73    for (time_slice, var) in activity_vars.iter() {
74        let limits = asset.get_activity_limits(time_slice);
75        let limits = limits.start().value()..=limits.end().value();
76        problem.add_row(limits, [(*var, 1.0)]);
77    }
78}
79
80fn add_activity_constraints_for_candidate(
81    problem: &mut Problem,
82    asset: &AssetRef,
83    capacity_var: Variable,
84    activity_vars: &IndexMap<TimeSliceID, Variable>,
85) {
86    for (time_slice, activity_var) in activity_vars.iter() {
87        let limits = asset.get_activity_per_capacity_limits(time_slice);
88        let lower_limit = limits.start().value();
89        let upper_limit = limits.end().value();
90
91        // Upper bound: activity ≤ capacity * upper_limit
92        problem.add_row(..=0.0, [(*activity_var, 1.0), (capacity_var, -upper_limit)]);
93
94        // Lower bound: activity ≥ capacity * lower_limit
95        problem.add_row(..=0.0, [(*activity_var, -1.0), (capacity_var, lower_limit)]);
96    }
97}
98
99/// Adds demand constraints to the problem.
100///
101/// Constrains supply to be less than or equal to demand, which adapts based on the commodity's
102/// balance level.
103pub fn add_demand_constraints(
104    problem: &mut Problem,
105    asset: &AssetRef,
106    commodity: &Commodity,
107    time_slice_info: &TimeSliceInfo,
108    demand: &DemandMap,
109    activity_vars: &IndexMap<TimeSliceID, Variable>,
110    unmet_demand_vars: &IndexMap<TimeSliceID, Variable>,
111) {
112    for ts_selection in time_slice_info.iter_selections_at_level(commodity.time_slice_level) {
113        let mut demand_for_ts_selection = Flow(0.0);
114        let mut terms = Vec::new();
115        for (time_slice, _) in ts_selection.iter(time_slice_info) {
116            demand_for_ts_selection += *demand.get(time_slice).unwrap();
117            let flow_coeff = asset.get_flow(&commodity.id).unwrap().coeff;
118            terms.push((*activity_vars.get(time_slice).unwrap(), flow_coeff.value()));
119            terms.push((*unmet_demand_vars.get(time_slice).unwrap(), 1.0));
120        }
121        problem.add_row(
122            demand_for_ts_selection.value()..=demand_for_ts_selection.value(),
123            terms,
124        );
125    }
126}