muse2/simulation/investment/appraisal/
optimisation.rs1use super::DemandMap;
3use super::ObjectiveCoefficients;
4use super::constraints::{
5 add_activity_constraints, add_capacity_constraint, add_demand_constraints,
6};
7use crate::asset::{AssetCapacity, AssetRef};
8use crate::commodity::Commodity;
9use crate::model::Model;
10use crate::simulation::optimisation::ModelError;
11use crate::simulation::optimisation::apply_highs_options_from_toml;
12use crate::simulation::optimisation::solve_optimal;
13use crate::time_slice::{TimeSliceID, TimeSliceInfo};
14use crate::units::{Activity, Capacity, Flow};
15use anyhow::{Context, Result};
16use highs::{RowProblem as Problem, Sense};
17use indexmap::IndexMap;
18
19pub type Variable = highs::Col;
24
25struct VariableMap {
27 capacity_var: Variable,
32 activity_vars: IndexMap<TimeSliceID, Variable>,
34 unmet_demand_vars: IndexMap<TimeSliceID, Variable>,
36}
37
38impl VariableMap {
39 fn add_to_problem(
48 problem: &mut Problem,
49 cost_coefficients: &ObjectiveCoefficients,
50 capacity_unit_size: Option<Capacity>,
51 ) -> Self {
52 let capacity_coefficient = cost_coefficients.capacity_coefficient.value();
54 let capacity_var = match capacity_unit_size {
55 Some(unit_size) => {
56 problem.add_integer_column(capacity_coefficient * unit_size.value(), 0.0..)
58 }
59 None => {
60 problem.add_column(capacity_coefficient, 0.0..)
62 }
63 };
64
65 let mut activity_vars = IndexMap::new();
67 for (time_slice, cost) in &cost_coefficients.activity_coefficients {
68 let var = problem.add_column(cost.value(), 0.0..);
69 activity_vars.insert(time_slice.clone(), var);
70 }
71
72 let mut unmet_demand_vars = IndexMap::new();
74 for time_slice in cost_coefficients.activity_coefficients.keys() {
75 let var = problem.add_column(cost_coefficients.unmet_demand_coefficient.value(), 0.0..);
76 unmet_demand_vars.insert(time_slice.clone(), var);
77 }
78
79 Self {
80 capacity_var,
81 activity_vars,
82 unmet_demand_vars,
83 }
84 }
85}
86
87pub struct ResultsMap {
89 pub capacity: AssetCapacity,
91 pub activity: IndexMap<TimeSliceID, Activity>,
93 pub unmet_demand: DemandMap,
95}
96
97fn add_constraints(
99 problem: &mut Problem,
100 asset: &AssetRef,
101 max_capacity: AssetCapacity,
102 commodity: &Commodity,
103 variables: &VariableMap,
104 demand: &DemandMap,
105 time_slice_info: &TimeSliceInfo,
106) {
107 add_capacity_constraint(problem, asset, max_capacity, variables.capacity_var);
108 add_activity_constraints(
109 problem,
110 asset,
111 variables.capacity_var,
112 &variables.activity_vars,
113 time_slice_info,
114 );
115 add_demand_constraints(
116 problem,
117 asset,
118 commodity,
119 time_slice_info,
120 demand,
121 &variables.activity_vars,
122 &variables.unmet_demand_vars,
123 );
124}
125
126pub fn perform_optimisation(
132 model: &Model,
133 asset: &AssetRef,
134 max_capacity: AssetCapacity,
135 commodity: &Commodity,
136 coefficients: &ObjectiveCoefficients,
137 demand: &DemandMap,
138 sense: Sense,
139) -> Result<ResultsMap> {
140 let mut problem = Problem::default();
142 let variables = VariableMap::add_to_problem(&mut problem, coefficients, asset.unit_size());
143
144 add_constraints(
146 &mut problem,
147 asset,
148 max_capacity,
149 commodity,
150 &variables,
151 demand,
152 &model.time_slice_info,
153 );
154
155 let mut highs_model = problem.optimise(sense);
157 apply_highs_options_from_toml(&mut highs_model, &model.parameters.highs.appraisal_options)
158 .context("Failed to apply custom HiGHS options to appraisal optimisation")?;
159 let solution = solve_optimal(highs_model)
160 .map_err(ModelError::into_anyhow)?
161 .get_solution();
162 let solution_values = solution.columns();
163 Ok(ResultsMap {
164 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
167 capacity: match asset.unit_size() {
168 Some(unit_size) => {
169 AssetCapacity::Discrete(solution_values[0].round() as u32, unit_size)
170 }
171 None => AssetCapacity::Continuous(Capacity::new(solution_values[0])),
172 },
173 activity: variables
177 .activity_vars
178 .keys()
179 .zip(solution_values[1..].iter())
180 .map(|(time_slice, &value)| (time_slice.clone(), Activity::new(value)))
181 .collect(),
182 unmet_demand: variables
183 .unmet_demand_vars
184 .keys()
185 .zip(solution_values[variables.activity_vars.len() + 1..].iter())
186 .map(|(time_slice, &value)| (time_slice.clone(), Flow::new(value)))
187 .collect(),
188 })
189}