Skip to content

Commit cabdbf1

Browse files
authored
Merge pull request #1124 from EnergySystemsModellingLab/parent-assets-for-dispatch
Use parent assets rather than children for dispatch
2 parents 0150cee + f96b5ff commit cabdbf1

File tree

3 files changed

+562
-484
lines changed

3 files changed

+562
-484
lines changed

src/asset.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,21 @@ impl Asset {
872872
}
873873
}
874874

875+
/// Whether this asset is a parent of divided assets
876+
pub fn is_parent(&self) -> bool {
877+
matches!(self.state, AssetState::Parent { .. })
878+
}
879+
880+
/// Get the number of children this asset has.
881+
///
882+
/// If this asset is not a parent, then `None` is returned.
883+
pub fn num_children(&self) -> Option<u32> {
884+
match &self.state {
885+
AssetState::Parent { .. } => Some(self.capacity().n_units().unwrap()),
886+
_ => None,
887+
}
888+
}
889+
875890
/// Get the group ID for this asset, if any
876891
pub fn group_id(&self) -> Option<AssetGroupID> {
877892
match &self.state {

src/simulation/optimisation.rs

Lines changed: 83 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ use crate::region::RegionID;
1111
use crate::simulation::CommodityPrices;
1212
use crate::time_slice::{TimeSliceID, TimeSliceInfo, TimeSliceSelection};
1313
use crate::units::{
14-
Activity, Capacity, Flow, Money, MoneyPerActivity, MoneyPerCapacity, MoneyPerFlow, Year,
14+
Activity, Capacity, Dimensionless, Flow, Money, MoneyPerActivity, MoneyPerCapacity,
15+
MoneyPerFlow, Year,
1516
};
1617
use anyhow::{Result, bail, ensure};
1718
use highs::{HighsModelStatus, HighsStatus, RowProblem as Problem, Sense};
1819
use indexmap::{IndexMap, IndexSet};
19-
use itertools::{chain, iproduct};
20-
use std::collections::HashMap;
20+
use itertools::{Itertools, chain, iproduct};
21+
use std::cell::Cell;
22+
use std::collections::{HashMap, HashSet};
2123
use std::error::Error;
2224
use std::fmt;
2325
use std::ops::Range;
@@ -179,13 +181,56 @@ impl VariableMap {
179181
}
180182
}
181183

184+
/// Create a map of commodity flows for each asset's coeffs at every time slice.
185+
///
186+
/// Note that this only includes commodity flows which relate to existing assets, so not every
187+
/// commodity in the simulation will necessarily be represented.
188+
fn create_flow_map<'a>(
189+
existing_assets: &[AssetRef],
190+
time_slice_info: &TimeSliceInfo,
191+
activity: impl IntoIterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
192+
) -> FlowMap {
193+
// The decision variables represent assets' activity levels, not commodity flows. We
194+
// multiply this value by the flow coeffs to get commodity flows.
195+
let mut flows = FlowMap::new();
196+
for (asset, time_slice, activity) in activity {
197+
let n_units = Dimensionless(asset.num_children().unwrap_or(1) as f64);
198+
for flow in asset.iter_flows() {
199+
let flow_key = (asset.clone(), flow.commodity.id.clone(), time_slice.clone());
200+
let flow_value = activity * flow.coeff / n_units;
201+
flows.insert(flow_key, flow_value);
202+
}
203+
}
204+
205+
// Copy flows for each child asset
206+
for asset in existing_assets {
207+
if let Some(parent) = asset.parent() {
208+
for commodity_id in asset.iter_flows().map(|flow| &flow.commodity.id) {
209+
for time_slice in time_slice_info.iter_ids() {
210+
let flow = flows[&(parent.clone(), commodity_id.clone(), time_slice.clone())];
211+
flows.insert(
212+
(asset.clone(), commodity_id.clone(), time_slice.clone()),
213+
flow,
214+
);
215+
}
216+
}
217+
}
218+
}
219+
220+
// Remove all the parent assets
221+
flows.retain(|(asset, _, _), _| !asset.is_parent());
222+
223+
flows
224+
}
225+
182226
/// The solution to the dispatch optimisation problem
183227
#[allow(clippy::struct_field_names)]
184228
pub struct Solution<'a> {
185229
solution: highs::Solution,
186230
variables: VariableMap,
187231
time_slice_info: &'a TimeSliceInfo,
188232
constraint_keys: ConstraintKeys,
233+
flow_map: Cell<Option<FlowMap>>,
189234
/// The objective value for the solution
190235
pub objective_value: Money,
191236
}
@@ -195,19 +240,13 @@ impl Solution<'_> {
195240
///
196241
/// Note that this only includes commodity flows which relate to existing assets, so not every
197242
/// commodity in the simulation will necessarily be represented.
243+
///
244+
/// Note: The flow map is actually already created and is taken from `self` when this method is
245+
/// called (hence it can only be called once). The reason for this is because we need to convert
246+
/// back from parent assets to child assets. We can remove this hack once we have updated all
247+
/// the users of this interface to be able to handle parent assets correctly.
198248
pub fn create_flow_map(&self) -> FlowMap {
199-
// The decision variables represent assets' activity levels, not commodity flows. We
200-
// multiply this value by the flow coeffs to get commodity flows.
201-
let mut flows = FlowMap::new();
202-
for (asset, time_slice, activity) in self.iter_activity_for_existing() {
203-
for flow in asset.iter_flows() {
204-
let flow_key = (asset.clone(), flow.commodity.id.clone(), time_slice.clone());
205-
let flow_value = activity * flow.coeff;
206-
flows.insert(flow_key, flow_value);
207-
}
208-
}
209-
210-
flows
249+
self.flow_map.take().expect("Flow map already created")
211250
}
212251

213252
/// Activity for all assets (existing and candidate, if present)
@@ -381,6 +420,21 @@ fn filter_input_prices(
381420
.collect()
382421
}
383422

423+
/// Get the parent for each asset.
424+
///
425+
/// Child assets are converted to their parents and non-divisible assets are returned as is. Each
426+
/// parent asset is returned only once.
427+
fn convert_assets_to_parents(assets: &[AssetRef]) -> impl Iterator<Item = AssetRef> {
428+
let mut parents = HashSet::new();
429+
assets
430+
.iter()
431+
.filter_map(move |asset| match asset.parent() {
432+
Some(parent) => parents.insert(parent.clone()).then_some(parent),
433+
None => Some(asset),
434+
})
435+
.cloned()
436+
}
437+
384438
/// Provides the interface for running the dispatch optimisation.
385439
///
386440
/// The run will attempt to meet unmet demand: if the solver reports infeasibility
@@ -557,13 +611,15 @@ impl<'model, 'run> DispatchRun<'model, 'run> {
557611
allow_unmet_demand: bool,
558612
input_prices: Option<&CommodityPrices>,
559613
) -> Result<Solution<'model>, ModelError> {
614+
let parent_assets = convert_assets_to_parents(self.existing_assets).collect_vec();
615+
560616
// Set up problem
561617
let mut problem = Problem::default();
562618
let mut variables = VariableMap::new_with_activity_vars(
563619
&mut problem,
564620
self.model,
565621
input_prices,
566-
self.existing_assets,
622+
&parent_assets,
567623
self.candidate_assets,
568624
self.year,
569625
);
@@ -577,7 +633,7 @@ impl<'model, 'run> DispatchRun<'model, 'run> {
577633
// Check flexible capacity assets is a subset of existing assets
578634
for asset in self.flexible_capacity_assets {
579635
assert!(
580-
self.existing_assets.contains(asset),
636+
parent_assets.contains(asset),
581637
"Flexible capacity assets must be a subset of existing assets. Offending asset: {asset:?}"
582638
);
583639
}
@@ -594,7 +650,7 @@ impl<'model, 'run> DispatchRun<'model, 'run> {
594650
}
595651

596652
// Add constraints
597-
let all_assets = chain(self.existing_assets.iter(), self.candidate_assets.iter());
653+
let all_assets = chain(parent_assets.iter(), self.candidate_assets.iter());
598654
let constraint_keys = add_model_constraints(
599655
&mut problem,
600656
&variables,
@@ -607,13 +663,20 @@ impl<'model, 'run> DispatchRun<'model, 'run> {
607663
// Solve model
608664
let solution = solve_optimal(problem.optimise(Sense::Minimise))?;
609665

610-
Ok(Solution {
666+
let solution = Solution {
611667
solution: solution.get_solution(),
612668
variables,
613669
time_slice_info: &self.model.time_slice_info,
614670
constraint_keys,
671+
flow_map: Cell::default(),
615672
objective_value: Money(solution.objective_value()),
616-
})
673+
};
674+
solution.flow_map.set(Some(create_flow_map(
675+
self.existing_assets,
676+
&self.model.time_slice_info,
677+
solution.iter_activity(),
678+
)));
679+
Ok(solution)
617680
}
618681
}
619682

0 commit comments

Comments
 (0)