1use crate::asset::{Asset, AssetID, AssetPool};
5use crate::commodity::{BalanceType, CommodityID};
6use crate::model::Model;
7use crate::process::ProcessFlow;
8use crate::region::RegionID;
9use crate::time_slice::{TimeSliceID, TimeSliceInfo};
10use anyhow::{anyhow, Result};
11use highs::{HighsModelStatus, RowProblem as Problem, Sense};
12use indexmap::IndexMap;
13
14mod constraints;
15use constraints::{add_asset_constraints, CapacityConstraintKeys, CommodityBalanceConstraintKeys};
16
17type Variable = highs::Col;
22
23#[derive(Default)]
33pub struct VariableMap(IndexMap<(AssetID, CommodityID, TimeSliceID), Variable>);
34
35impl VariableMap {
36 fn get(
38 &self,
39 asset_id: AssetID,
40 commodity_id: &CommodityID,
41 time_slice: &TimeSliceID,
42 ) -> Variable {
43 let key = (asset_id, commodity_id.clone(), time_slice.clone());
44
45 *self
46 .0
47 .get(&key)
48 .expect("No variable found for given params")
49 }
50}
51
52pub struct Solution<'a> {
54 solution: highs::Solution,
55 variables: VariableMap,
56 time_slice_info: &'a TimeSliceInfo,
57 commodity_balance_constraint_keys: CommodityBalanceConstraintKeys,
58 capacity_constraint_keys: CapacityConstraintKeys,
59}
60
61impl Solution<'_> {
62 pub fn iter_commodity_flows_for_assets(
71 &self,
72 ) -> impl Iterator<Item = (AssetID, &CommodityID, &TimeSliceID, f64)> {
73 self.variables
74 .0
75 .keys()
76 .zip(self.solution.columns().iter().copied())
77 .map(|((asset_id, commodity_id, time_slice), flow)| {
78 (*asset_id, commodity_id, time_slice, flow)
79 })
80 }
81
82 pub fn iter_commodity_balance_duals(
84 &self,
85 ) -> impl Iterator<Item = (&CommodityID, &RegionID, &TimeSliceID, f64)> {
86 self.commodity_balance_constraint_keys
90 .iter()
91 .zip(self.solution.dual_rows())
92 .flat_map(|((commodity_id, region_id, ts_selection), price)| {
93 self.time_slice_info
94 .iter_selection(ts_selection)
95 .map(move |(ts, _)| (commodity_id, region_id, ts, *price))
96 })
97 }
98
99 pub fn iter_capacity_duals(&self) -> impl Iterator<Item = (AssetID, &TimeSliceID, f64)> {
101 self.capacity_constraint_keys
102 .iter()
103 .zip(
104 self.solution.dual_rows()[self.commodity_balance_constraint_keys.len()..]
105 .iter()
106 .copied(),
107 )
108 .map(|((asset_id, time_slice), dual)| (*asset_id, time_slice, dual))
109 }
110}
111
112pub fn perform_dispatch_optimisation<'a>(
128 model: &'a Model,
129 assets: &AssetPool,
130 year: u32,
131) -> Result<Solution<'a>> {
132 let mut problem = Problem::default();
134 let variables = add_variables(&mut problem, model, assets, year);
135
136 let (commodity_balance_constraint_keys, capacity_constraint_keys) =
138 add_asset_constraints(&mut problem, &variables, model, assets, year);
139
140 let mut highs_model = problem.optimise(Sense::Minimise);
142
143 enable_highs_logging(&mut highs_model);
150
151 let solution = highs_model.solve();
153 match solution.status() {
154 HighsModelStatus::Optimal => Ok(Solution {
155 solution: solution.get_solution(),
156 variables,
157 time_slice_info: &model.time_slice_info,
158 commodity_balance_constraint_keys,
159 capacity_constraint_keys,
160 }),
161 status => Err(anyhow!("Could not solve: {status:?}")),
162 }
163}
164
165fn enable_highs_logging(model: &mut highs::Model) {
167 if let Ok(log_level) = std::env::var("MUSE2_LOG_LEVEL") {
169 if log_level.eq_ignore_ascii_case("off") {
170 return;
171 }
172 }
173
174 model.set_option("log_to_console", true);
175 model.set_option("output_flag", true);
176}
177
178fn add_variables(
191 problem: &mut Problem,
192 model: &Model,
193 assets: &AssetPool,
194 year: u32,
195) -> VariableMap {
196 let mut variables = VariableMap::default();
197
198 for asset in assets.iter() {
199 for flow in asset.process.flows.iter() {
200 for time_slice in model.time_slice_info.iter_ids() {
201 let coeff = calculate_cost_coefficient(asset, flow, year, time_slice);
202
203 let var = if flow.flow < 0.0 {
205 problem.add_column(coeff, ..=0.0)
206 } else {
207 problem.add_column(coeff, 0.0..)
208 };
209
210 let key = (asset.id, flow.commodity.id.clone(), time_slice.clone());
211 let existing = variables.0.insert(key, var).is_some();
212 assert!(!existing, "Duplicate entry for var");
213 }
214 }
215 }
216
217 variables
218}
219
220fn calculate_cost_coefficient(
222 asset: &Asset,
223 flow: &ProcessFlow,
224 year: u32,
225 time_slice: &TimeSliceID,
226) -> f64 {
227 let mut coeff = flow.flow_cost;
229
230 if flow.is_pac {
232 coeff += asset
233 .process
234 .parameters
235 .get(&(asset.region_id.clone(), asset.commission_year))
236 .unwrap()
237 .variable_operating_cost
238 }
239
240 if !flow.commodity.costs.is_empty() {
242 let cost = flow
243 .commodity
244 .costs
245 .get(&(asset.region_id.clone(), year, time_slice.clone()))
246 .unwrap();
247 let apply_cost = match cost.balance_type {
248 BalanceType::Net => true,
249 BalanceType::Consumption => flow.flow < 0.0,
250 BalanceType::Production => flow.flow > 0.0,
251 };
252
253 if apply_cost {
254 coeff += cost.value;
255 }
256 }
257
258 if flow.flow > 0.0 {
261 coeff
262 } else {
263 -coeff
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use crate::commodity::{Commodity, CommodityCost, CommodityCostMap, CommodityType, DemandMap};
271 use crate::process::{
272 FlowType, Process, ProcessEnergyLimitsMap, ProcessParameter, ProcessParameterMap,
273 };
274 use crate::time_slice::TimeSliceLevel;
275 use float_cmp::assert_approx_eq;
276 use std::collections::HashSet;
277 use std::rc::Rc;
278
279 fn get_cost_coeff_args(
280 flow: f64,
281 is_pac: bool,
282 costs: CommodityCostMap,
283 ) -> (Asset, ProcessFlow) {
284 let process_param = Rc::new(ProcessParameter {
285 capital_cost: 5.0,
286 fixed_operating_cost: 2.0,
287 variable_operating_cost: 1.0,
288 lifetime: 5,
289 discount_rate: 0.9,
290 capacity_to_activity: 1.0,
291 });
292 let mut process_parameter_map = ProcessParameterMap::new();
293 process_parameter_map.insert(("GBR".into(), 2010), process_param.clone());
294 process_parameter_map.insert(("GBR".into(), 2020), process_param.clone());
295 let commodity = Rc::new(Commodity {
296 id: "commodity1".into(),
297 description: "Some description".into(),
298 kind: CommodityType::InputCommodity,
299 time_slice_level: TimeSliceLevel::Annual,
300 costs,
301 demand: DemandMap::new(),
302 });
303 let flow = ProcessFlow {
304 process_id: "id1".into(),
305 commodity: Rc::clone(&commodity),
306 flow,
307 flow_type: FlowType::Fixed,
308 flow_cost: 1.0,
309 is_pac,
310 };
311 let process = Rc::new(Process {
312 id: "process1".into(),
313 description: "Description".into(),
314 years: 2010..=2020,
315 energy_limits: ProcessEnergyLimitsMap::new(),
316 flows: vec![flow.clone()],
317 parameters: process_parameter_map,
318 regions: HashSet::from([RegionID("GBR".into())]),
319 });
320 let asset = Asset::new(
321 "agent1".into(),
322 Rc::clone(&process),
323 "GBR".into(),
324 1.0,
325 2010,
326 )
327 .unwrap();
328
329 (asset, flow)
330 }
331
332 #[test]
333 fn test_calculate_cost_coefficient() {
334 let time_slice = TimeSliceID {
335 season: "winter".into(),
336 time_of_day: "day".into(),
337 };
338
339 macro_rules! check_coeff {
340 ($flow:expr, $is_pac:expr, $costs:expr, $expected:expr) => {
341 let (asset, flow) = get_cost_coeff_args($flow, $is_pac, $costs);
342 assert_approx_eq!(
343 f64,
344 calculate_cost_coefficient(&asset, &flow, 2010, &time_slice),
345 $expected
346 );
347 };
348 }
349
350 check_coeff!(1.0, false, CommodityCostMap::new(), 1.0);
352 check_coeff!(-1.0, false, CommodityCostMap::new(), -1.0);
353
354 check_coeff!(1.0, true, CommodityCostMap::new(), 2.0);
356 check_coeff!(-1.0, true, CommodityCostMap::new(), -2.0);
357
358 let cost = CommodityCost {
360 balance_type: BalanceType::Production,
361 value: 2.0,
362 };
363 let mut costs = CommodityCostMap::new();
364 costs.insert(("GBR".into(), 2010, time_slice.clone()), cost);
365 check_coeff!(1.0, false, costs.clone(), 3.0);
366 check_coeff!(-1.0, false, costs, -1.0);
367
368 let cost = CommodityCost {
370 balance_type: BalanceType::Net,
371 value: 2.0,
372 };
373 let mut costs = CommodityCostMap::new();
374 costs.insert(("GBR".into(), 2010, time_slice.clone()), cost);
375 check_coeff!(1.0, false, costs.clone(), 3.0);
376 check_coeff!(-1.0, false, costs, -3.0);
377
378 let cost = CommodityCost {
380 balance_type: BalanceType::Consumption,
381 value: 2.0,
382 };
383 let mut costs = CommodityCostMap::new();
384 costs.insert(("GBR".into(), 2010, time_slice.clone()), cost);
385 check_coeff!(1.0, false, costs.clone(), 1.0);
386 check_coeff!(-1.0, false, costs, -3.0);
387
388 let cost = CommodityCost {
390 balance_type: BalanceType::Production,
391 value: 2.0,
392 };
393 let mut costs = CommodityCostMap::new();
394 costs.insert(("GBR".into(), 2010, time_slice.clone()), cost);
395 check_coeff!(1.0, true, costs.clone(), 4.0);
396 check_coeff!(-1.0, true, costs, -2.0);
397 }
398}