Skip to content

Commit b36d823

Browse files
authored
Merge pull request #1211 from EnergySystemsModellingLab/fix-parent-capacity-for-dispatch
Fix parent capacity for dispatch
2 parents f4cdad4 + d237e58 commit b36d823

3 files changed

Lines changed: 212 additions & 104 deletions

File tree

src/asset.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,41 @@ impl AssetRef {
11001100
f(Some(&self), child);
11011101
}
11021102
}
1103+
1104+
/// Get an [`AssetRef`] representing a subset of this parent's children.
1105+
///
1106+
/// # Panics
1107+
///
1108+
/// Panics if this asset is not a parent asset or `num_units` is zero or exceeds the total
1109+
/// capacity of this asset.
1110+
pub fn make_partial_parent(&self, num_units: u32) -> Self {
1111+
assert!(
1112+
self.is_parent(),
1113+
"Cannot make a partial parent from a non-parent asset"
1114+
);
1115+
assert!(
1116+
num_units > 0,
1117+
"Cannot make a partial parent with zero units"
1118+
);
1119+
1120+
let (max_num_units, unit_size) = match self.capacity() {
1121+
AssetCapacity::Discrete(max_num_units, unit_size) => (max_num_units, unit_size),
1122+
// We know asset capacity type is discrete as this is a parent asset
1123+
AssetCapacity::Continuous(_) => unreachable!(),
1124+
};
1125+
match num_units.cmp(&max_num_units) {
1126+
// Make a new Asset with fewer units
1127+
Ordering::Less => Self::from(Asset {
1128+
capacity: Cell::new(AssetCapacity::Discrete(num_units, unit_size)),
1129+
..Asset::clone(self)
1130+
}),
1131+
// Same number of units as self
1132+
Ordering::Equal => self.clone(),
1133+
Ordering::Greater => {
1134+
panic!("Cannot make a partial parent with more units than original")
1135+
}
1136+
}
1137+
}
11031138
}
11041139

11051140
impl From<Rc<Asset>> for AssetRef {
@@ -1429,6 +1464,64 @@ mod tests {
14291464
assert_eq!(count, 1);
14301465
}
14311466

1467+
#[fixture]
1468+
fn parent_asset(asset_divisible: Asset) -> AssetRef {
1469+
let asset = AssetRef::from(asset_divisible);
1470+
let mut parent = None;
1471+
1472+
asset.into_for_each_child(&mut 0, |maybe_parent, _| {
1473+
if parent.is_none() {
1474+
parent = maybe_parent.cloned();
1475+
}
1476+
});
1477+
1478+
parent.expect("Divisible asset should create a parent")
1479+
}
1480+
1481+
#[rstest]
1482+
#[case::subset_of_children(2, false)]
1483+
#[case::all_children(3, true)]
1484+
fn make_partial_parent(
1485+
parent_asset: AssetRef,
1486+
#[case] num_units: u32,
1487+
#[case] expect_same_asset: bool,
1488+
) {
1489+
let parent = parent_asset;
1490+
assert!(parent.is_parent());
1491+
1492+
let partial_parent = parent.make_partial_parent(num_units);
1493+
1494+
assert!(partial_parent.is_parent());
1495+
assert_eq!(
1496+
partial_parent.capacity(),
1497+
AssetCapacity::Discrete(num_units, Capacity(4.0))
1498+
);
1499+
assert_eq!(partial_parent.num_children(), Some(num_units));
1500+
assert_eq!(partial_parent.group_id(), parent.group_id());
1501+
assert_eq!(partial_parent.agent_id(), parent.agent_id());
1502+
assert_eq!(Rc::ptr_eq(&partial_parent.0, &parent.0), expect_same_asset);
1503+
assert_eq!(parent.capacity(), AssetCapacity::Discrete(3, Capacity(4.0)));
1504+
}
1505+
1506+
#[rstest]
1507+
#[should_panic(expected = "Cannot make a partial parent from a non-parent asset")]
1508+
fn make_partial_parent_panics_for_non_parent_asset(asset_divisible: Asset) {
1509+
let asset = AssetRef::from(asset_divisible);
1510+
asset.make_partial_parent(1);
1511+
}
1512+
1513+
#[rstest]
1514+
#[should_panic(expected = "Cannot make a partial parent with zero units")]
1515+
fn make_partial_parent_panics_for_zero_units(parent_asset: AssetRef) {
1516+
parent_asset.make_partial_parent(0);
1517+
}
1518+
1519+
#[rstest]
1520+
#[should_panic(expected = "Cannot make a partial parent with more units than original")]
1521+
fn make_partial_parent_panics_for_too_many_units(parent_asset: AssetRef) {
1522+
parent_asset.make_partial_parent(4);
1523+
}
1524+
14321525
#[rstest]
14331526
fn asset_commission(process: Process) {
14341527
// Test successful commissioning of Future asset

src/simulation/optimisation.rs

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ use crate::units::{
1717
use anyhow::{Result, bail, ensure};
1818
use highs::{HighsModelStatus, HighsStatus, RowProblem as Problem, Sense};
1919
use indexmap::{IndexMap, IndexSet};
20-
use itertools::{Itertools, chain, iproduct};
20+
use itertools::{chain, iproduct};
2121
use std::cell::Cell;
22-
use std::collections::{HashMap, HashSet};
22+
use std::collections::HashMap;
2323
use std::error::Error;
2424
use std::fmt;
2525
use std::ops::Range;
@@ -426,15 +426,30 @@ fn filter_input_prices(
426426
///
427427
/// Child assets are converted to their parents and non-divisible assets are returned as is. Each
428428
/// parent asset is returned only once.
429-
fn get_parent_or_self(assets: &[AssetRef]) -> impl Iterator<Item = AssetRef> {
430-
let mut parents = HashSet::new();
431-
assets
432-
.iter()
433-
.filter_map(move |asset| match asset.parent() {
434-
Some(parent) => parents.insert(parent.clone()).then_some(parent),
435-
None => Some(asset),
436-
})
437-
.cloned()
429+
///
430+
/// If only a subset of a parent's children are present in `assets`, a new parent asset representing
431+
/// a portion of the total capacity will be created. This will have the same hash as the original
432+
/// parent.
433+
fn get_parent_or_self(assets: &[AssetRef]) -> Vec<AssetRef> {
434+
let mut child_counts: IndexMap<&AssetRef, u32> = IndexMap::new();
435+
let mut out = Vec::new();
436+
437+
for asset in assets {
438+
if let Some(parent) = asset.parent() {
439+
// For child assets, keep count of number of children per parent
440+
*child_counts.entry(parent).or_default() += 1;
441+
} else {
442+
// Non-divisible assets can be returned as is
443+
out.push(asset.clone());
444+
}
445+
}
446+
447+
for (parent, child_count) in child_counts {
448+
// Convert to an object representing the appropriate portion of the parent's capacity
449+
out.push(parent.make_partial_parent(child_count));
450+
}
451+
452+
out
438453
}
439454

440455
/// Provides the interface for running the dispatch optimisation.
@@ -613,7 +628,7 @@ impl<'model, 'run> DispatchRun<'model, 'run> {
613628
allow_unmet_demand: bool,
614629
input_prices: Option<&CommodityPrices>,
615630
) -> Result<Solution<'model>, ModelError> {
616-
let parent_assets = get_parent_or_self(self.existing_assets).collect_vec();
631+
let parent_assets = get_parent_or_self(self.existing_assets);
617632

618633
// Set up problem
619634
let mut problem = Problem::default();

0 commit comments

Comments
 (0)