muse2/simulation/investment/appraisal/
constraints.rs1use super::DemandMap;
3use super::optimisation::Variable;
4use crate::asset::AssetRef;
5use crate::commodity::Commodity;
6use crate::model::Model;
7use crate::time_slice::{Season, TimeSliceID, TimeSliceInfo, TimeSliceSelection};
8use crate::units::{MoneyPerCapacityPerYear, Year};
9use highs::RowProblem as Problem;
10use indexmap::IndexMap;
11
12pub fn add_activity_constraints(
20 problem: &mut Problem,
21 asset: &AssetRef,
22 activity_vars: &IndexMap<TimeSliceID, Variable>,
23 time_slice_info: &TimeSliceInfo,
24) {
25 for (ts_selection, limits) in asset.iter_activity_limits() {
26 let limits = limits.start().value()..=limits.end().value();
27
28 let terms = ts_selection
30 .iter(time_slice_info)
31 .map(|(time_slice, _)| (activity_vars[time_slice], 1.0))
32 .collect::<Vec<_>>();
33
34 problem.add_row(limits, &terms);
36 }
37}
38
39pub fn add_demand_constraints(
45 problem: &mut Problem,
46 asset: &AssetRef,
47 commodity: &Commodity,
48 time_slice_info: &TimeSliceInfo,
49 demand: &DemandMap,
50 activity_vars: &IndexMap<TimeSliceID, Variable>,
51) {
52 let flow_coeff = asset.get_flow(&commodity.id).unwrap().coeff;
53 for ts_selection in time_slice_info.iter_selections_at_level(commodity.time_slice_level) {
54 let demand_for_ts_selection = demand[&ts_selection];
55 let terms: Vec<_> = ts_selection
56 .iter(time_slice_info)
57 .map(|(time_slice, _)| (activity_vars[time_slice], flow_coeff.value()))
58 .collect();
59 problem.add_row(0.0..=demand_for_ts_selection.value(), terms);
60 }
61}
62
63pub fn add_utilisation_peak_constraints(
69 problem: &mut Problem,
70 model: &Model,
71 asset: &AssetRef,
72 activity_vars: &IndexMap<TimeSliceID, Variable>,
73) {
74 let has_seasonal_penalty =
75 model.parameters.seasonal_utilisation_penalty > MoneyPerCapacityPerYear(0.0);
76 let has_annual_penalty =
77 model.parameters.annual_utilisation_penalty > MoneyPerCapacityPerYear(0.0);
78
79 if !has_seasonal_penalty && !has_annual_penalty {
81 return;
82 }
83
84 let seasonal_peak_vars = add_seasonal_peak_variables(problem, model);
86 add_seasonal_peak_constraints(
87 problem,
88 asset,
89 activity_vars,
90 &model.time_slice_info,
91 &seasonal_peak_vars,
92 );
93
94 if has_annual_penalty {
96 let annual_peak_var = add_annual_peak_variable(problem, model);
97 add_annual_peak_constraints(
98 problem,
99 &model.time_slice_info,
100 annual_peak_var,
101 &seasonal_peak_vars,
102 );
103 }
104}
105
106fn add_seasonal_peak_variables(
108 problem: &mut Problem,
109 model: &Model,
110) -> IndexMap<Season, highs::Col> {
111 let mut seasonal_peak_vars = IndexMap::new();
112 for (season, duration) in &model.time_slice_info.seasons {
113 let col_factor = -(model.parameters.seasonal_utilisation_penalty * *duration);
116 let variable = problem.add_column(col_factor.value(), 0.0..);
117 seasonal_peak_vars.insert(season.clone(), variable);
118 }
119 seasonal_peak_vars
120}
121
122fn add_annual_peak_variable(problem: &mut Problem, model: &Model) -> highs::Col {
124 let col_factor = -(model.parameters.annual_utilisation_penalty * Year(1.0));
127 problem.add_column(col_factor.value(), 0.0..)
128}
129
130fn add_seasonal_peak_constraints(
132 problem: &mut Problem,
133 asset: &AssetRef,
134 activity_vars: &IndexMap<TimeSliceID, Variable>,
135 time_slice_info: &TimeSliceInfo,
136 seasonal_peak_vars: &IndexMap<Season, highs::Col>,
137) {
138 let activity_per_capacity = asset.process().capacity_to_activity;
139 for (season, &peak_variable) in seasonal_peak_vars {
140 for (time_slice, ts_length) in
141 TimeSliceSelection::Season(season.clone()).iter(time_slice_info)
142 {
143 let time_slice_fraction = ts_length / Year(1.0);
144 let activity_per_capacity_in_time_slice = activity_per_capacity * time_slice_fraction;
145 let capacity_required_per_activity = 1.0 / activity_per_capacity_in_time_slice.value();
146
147 problem.add_row(
151 0.0..,
152 [
153 (peak_variable, 1.0),
154 (activity_vars[time_slice], -capacity_required_per_activity),
155 ],
156 );
157 }
158 }
159}
160
161fn add_annual_peak_constraints(
163 problem: &mut Problem,
164 time_slice_info: &TimeSliceInfo,
165 annual_peak_var: highs::Col,
166 seasonal_peak_vars: &IndexMap<Season, highs::Col>,
167) {
168 for season in time_slice_info.seasons.keys() {
169 problem.add_row(
170 0.0..,
171 [(annual_peak_var, 1.0), (seasonal_peak_vars[season], -1.0)],
172 );
173 }
174}