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::simulation::optimisation::solve_optimal;
10use crate::time_slice::{TimeSliceID, TimeSliceInfo};
11use crate::units::{Activity, Capacity, Flow};
12use anyhow::Result;
13use highs::{RowProblem as Problem, Sense};
14use indexmap::IndexMap;
15
16pub type Variable = highs::Col;
21
22struct VariableMap {
24 capacity_var: Variable,
29 activity_vars: IndexMap<TimeSliceID, Variable>,
31 unmet_demand_vars: IndexMap<TimeSliceID, Variable>,
33}
34
35impl VariableMap {
36 fn add_to_problem(
45 problem: &mut Problem,
46 cost_coefficients: &ObjectiveCoefficients,
47 capacity_unit_size: Option<Capacity>,
48 ) -> Self {
49 let capacity_coefficient = cost_coefficients.capacity_coefficient.value();
51 let capacity_var = match capacity_unit_size {
52 Some(unit_size) => {
53 problem.add_integer_column(capacity_coefficient * unit_size.value(), 0.0..)
55 }
56 None => {
57 problem.add_column(capacity_coefficient, 0.0..)
59 }
60 };
61
62 let mut activity_vars = IndexMap::new();
64 for (time_slice, cost) in &cost_coefficients.activity_coefficients {
65 let var = problem.add_column(cost.value(), 0.0..);
66 activity_vars.insert(time_slice.clone(), var);
67 }
68
69 let mut unmet_demand_vars = IndexMap::new();
71 for time_slice in cost_coefficients.activity_coefficients.keys() {
72 let var = problem.add_column(cost_coefficients.unmet_demand_coefficient.value(), 0.0..);
73 unmet_demand_vars.insert(time_slice.clone(), var);
74 }
75
76 Self {
77 capacity_var,
78 activity_vars,
79 unmet_demand_vars,
80 }
81 }
82}
83
84pub struct ResultsMap {
86 pub capacity: AssetCapacity,
88 pub activity: IndexMap<TimeSliceID, Activity>,
90 pub unmet_demand: DemandMap,
92}
93
94fn add_constraints(
96 problem: &mut Problem,
97 asset: &AssetRef,
98 max_capacity: Option<AssetCapacity>,
99 commodity: &Commodity,
100 variables: &VariableMap,
101 demand: &DemandMap,
102 time_slice_info: &TimeSliceInfo,
103) {
104 add_capacity_constraint(problem, asset, max_capacity, variables.capacity_var);
105 add_activity_constraints(
106 problem,
107 asset,
108 variables.capacity_var,
109 &variables.activity_vars,
110 time_slice_info,
111 );
112 add_demand_constraints(
113 problem,
114 asset,
115 commodity,
116 time_slice_info,
117 demand,
118 &variables.activity_vars,
119 &variables.unmet_demand_vars,
120 );
121}
122
123pub fn perform_optimisation(
129 asset: &AssetRef,
130 max_capacity: Option<AssetCapacity>,
131 commodity: &Commodity,
132 coefficients: &ObjectiveCoefficients,
133 demand: &DemandMap,
134 time_slice_info: &TimeSliceInfo,
135 sense: Sense,
136) -> Result<ResultsMap> {
137 let mut problem = Problem::default();
139 let variables = VariableMap::add_to_problem(&mut problem, coefficients, asset.unit_size());
140
141 add_constraints(
143 &mut problem,
144 asset,
145 max_capacity,
146 commodity,
147 &variables,
148 demand,
149 time_slice_info,
150 );
151
152 let solution = solve_optimal(problem.optimise(sense))?.get_solution();
154 let solution_values = solution.columns();
155 Ok(ResultsMap {
156 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
159 capacity: match asset.unit_size() {
160 Some(unit_size) => {
161 AssetCapacity::Discrete(solution_values[0].round() as u32, unit_size)
162 }
163 None => AssetCapacity::Continuous(Capacity::new(solution_values[0])),
164 },
165 activity: variables
169 .activity_vars
170 .keys()
171 .zip(solution_values[1..].iter())
172 .map(|(time_slice, &value)| (time_slice.clone(), Activity::new(value)))
173 .collect(),
174 unmet_demand: variables
175 .unmet_demand_vars
176 .keys()
177 .zip(solution_values[variables.activity_vars.len() + 1..].iter())
178 .map(|(time_slice, &value)| (time_slice.clone(), Flow::new(value)))
179 .collect(),
180 })
181}