muse2/simulation/investment/appraisal/
optimisation.rs

1//! Optimisation problem for investment tools.
2use super::DemandMap;
3use super::coefficients::CoefficientsMap;
4use super::constraints::{
5    add_activity_constraints, add_capacity_constraint, add_demand_constraints,
6};
7use crate::asset::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
16/// A decision variable in the optimisation
17pub type Variable = highs::Col;
18
19/// Map storing variables for the optimisation problem
20struct VariableMap {
21    /// Capacity variable
22    capacity_var: Variable,
23    /// Activity variables in each time slice
24    activity_vars: IndexMap<TimeSliceID, Variable>,
25    // Unmet demand variables
26    unmet_demand_vars: IndexMap<TimeSliceID, Variable>,
27}
28
29/// Map containing optimisation results and coefficients
30pub struct ResultsMap {
31    /// Capacity variable
32    pub capacity: Capacity,
33    /// Activity variables in each time slice
34    pub activity: IndexMap<TimeSliceID, Activity>,
35    /// Unmet demand variables
36    pub unmet_demand: DemandMap,
37}
38
39/// Add variables to the problem based on cost coefficients
40fn add_variables(problem: &mut Problem, cost_coefficients: &CoefficientsMap) -> VariableMap {
41    // Create capacity variable
42    let capacity_var = problem.add_column(cost_coefficients.capacity_coefficient.value(), 0.0..);
43
44    // Create activity variables
45    let mut activity_vars = IndexMap::new();
46    for (time_slice, cost) in cost_coefficients.activity_coefficients.iter() {
47        let var = problem.add_column(cost.value(), 0.0..);
48        activity_vars.insert(time_slice.clone(), var);
49    }
50
51    // Create unmet demand variables
52    // One per time slice, all of which use the same coefficient
53    let mut unmet_demand_vars = IndexMap::new();
54    for time_slice in cost_coefficients.activity_coefficients.keys() {
55        let var = problem.add_column(cost_coefficients.unmet_demand_coefficient.value(), 0.0..);
56        unmet_demand_vars.insert(time_slice.clone(), var);
57    }
58
59    VariableMap {
60        capacity_var,
61        activity_vars,
62        unmet_demand_vars,
63    }
64}
65
66/// Adds constraints to the problem.
67fn add_constraints(
68    problem: &mut Problem,
69    asset: &AssetRef,
70    max_capacity: Option<Capacity>,
71    commodity: &Commodity,
72    variables: &VariableMap,
73    demand: &DemandMap,
74    time_slice_info: &TimeSliceInfo,
75) {
76    add_capacity_constraint(problem, asset, max_capacity, variables.capacity_var);
77    add_activity_constraints(
78        problem,
79        asset,
80        variables.capacity_var,
81        &variables.activity_vars,
82    );
83    add_demand_constraints(
84        problem,
85        asset,
86        commodity,
87        time_slice_info,
88        demand,
89        &variables.activity_vars,
90        &variables.unmet_demand_vars,
91    );
92}
93
94/// Performs optimisation for an asset, given the coefficients and demand.
95///
96/// Will either maximise or minimise the objective function, depending on the `sense` parameter.
97pub fn perform_optimisation(
98    asset: &AssetRef,
99    max_capacity: Option<Capacity>,
100    commodity: &Commodity,
101    coefficients: &CoefficientsMap,
102    demand: &DemandMap,
103    time_slice_info: &TimeSliceInfo,
104    sense: Sense,
105) -> Result<ResultsMap> {
106    // Set up problem
107    let mut problem = Problem::default();
108
109    // Add variables
110    let variables = add_variables(&mut problem, coefficients);
111
112    // Add constraints
113    add_constraints(
114        &mut problem,
115        asset,
116        max_capacity,
117        commodity,
118        &variables,
119        demand,
120        time_slice_info,
121    );
122
123    // Solve model
124    let solution = solve_optimal(problem.optimise(sense))?.get_solution();
125    let solution_values = solution.columns();
126    Ok(ResultsMap {
127        capacity: Capacity::new(solution_values[0]),
128        activity: variables
129            .activity_vars
130            .keys()
131            .zip(solution_values[1..].iter())
132            .map(|(time_slice, &value)| (time_slice.clone(), Activity::new(value)))
133            .collect(),
134        unmet_demand: variables
135            .unmet_demand_vars
136            .keys()
137            .zip(solution_values[variables.activity_vars.len() + 1..].iter())
138            .map(|(time_slice, &value)| (time_slice.clone(), Flow::new(value)))
139            .collect(),
140    })
141}