Skip to content

Commit 3c60bad

Browse files
authored
Merge pull request #1070 from EnergySystemsModellingLab/child_asset_capacity
Ensure that child assets have equal capacity
2 parents b55fe62 + c4d6f17 commit 3c60bad

File tree

11 files changed

+1399
-61
lines changed

11 files changed

+1399
-61
lines changed

examples/simple/processes.csv

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
id,description,regions,primary_output,start_year,end_year,capacity_to_activity
2-
GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0
3-
GASPRC,Gas processing,all,GASNAT,2020,2040,1.0
4-
WNDFRM,Wind farm,all,ELCTRI,2020,2040,31.54
5-
GASCGT,Gas combined cycle turbine,all,ELCTRI,2020,2040,31.54
6-
RGASBR,Gas boiler,all,RSHEAT,2020,2040,1.0
7-
RELCHP,Heat pump,all,RSHEAT,2020,2040,1.0
1+
id,description,regions,primary_output,start_year,end_year,capacity_to_activity,unit_size
2+
GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,
3+
GASPRC,Gas processing,all,GASNAT,2020,2040,1.0,
4+
WNDFRM,Wind farm,all,ELCTRI,2020,2040,31.54,
5+
GASCGT,Gas combined cycle turbine,all,ELCTRI,2020,2040,31.54,
6+
RGASBR,Gas boiler,all,RSHEAT,2020,2040,1.0,
7+
RELCHP,Heat pump,all,RSHEAT,2020,2040,1.0,

schemas/input/processes.yaml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,18 @@ fields:
2828
notes: Must be >= to `start_year`
2929
- name: capacity_to_activity
3030
type: number
31-
description: Factor relating capacity units (e.g. GW) to activity units (e.g. PJ). It is the maximum activity per year for one unit of capacity.
31+
description: Factor relating capacity units (e.g. GW) to activity units (e.g. PJ). It is the
32+
maximum activity per year for one unit of capacity.
3233
notes: Must be >=0. Optional (defaults to 1.0).
3334
- name: unit_size
3435
type: number
35-
description: Capacity of the units in which an asset for this process will be divided into when commissioned, if any.
36-
notes:
37-
If present, must be >0. Optional (defaults to None). It should be noted that making this number too small with respect the typical
38-
size of an asset might create hundreds or thousands of children assets, with a very negative effect on the performance. Users are advised
39-
to use this feature with care.
36+
description: Capacity of the units in which an asset for this process will be divided into when
37+
commissioned, if any.
38+
notes: If present, must be >0. Optional (defaults to None). Assets with a defined unit size are
39+
divided into n = ceil(C / U) equal units, where C is overall capacity and U is unit_size
40+
(i.e. rounding up the number of units, which may result in a total capacity greater than C, if
41+
C is not an exact multiple of U).
42+
43+
It should be noted that making this number too small with respect to the typical size of an
44+
asset might create hundreds or thousands of children assets, with a very negative effect on
45+
the performance. Users are advised to use this feature with care.

src/asset.rs

Lines changed: 60 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -838,32 +838,40 @@ impl Asset {
838838

839839
/// Divides an asset if it is divisible and returns a vector of children
840840
///
841-
/// The children assets are identical to the parent (including state) but with a capacity defined
842-
/// by the `unit_size`. Only Future or Selected assets can be divided.
841+
/// The child assets are identical to the parent (including state) but with a capacity
842+
/// defined by the `unit_size`. From a parent asset of capacity `C` and unit size `U`,
843+
/// `n = ceil(C / U)` child assets are created, each with capacity `U`. In other words, the
844+
/// total combined capacity of the children may be larger than that of the parent,
845+
/// if `C` is not an exact multiple of `U`.
846+
///
847+
/// Only `Future` and `Selected` assets can be divided.
843848
pub fn divide_asset(&self) -> Vec<AssetRef> {
844849
assert!(
845850
matches!(
846851
self.state,
847852
AssetState::Future { .. } | AssetState::Selected { .. }
848853
),
849-
"Assets with state {0} cannot be divided. Only Future or Selected assets can be divided",
854+
"Assets with state {} cannot be divided. Only Future or Selected assets can be divided",
850855
self.state
851856
);
852857

853-
// Divide the asset into children until all capacity is allocated
854-
let mut capacity = self.capacity;
855858
let unit_size = self.process.unit_size.expect(
856859
"Only assets corresponding to processes with a unit size defined can be divided",
857860
);
858-
let mut children = Vec::new();
859-
while capacity > Capacity(0.0) {
860-
let mut child = self.clone();
861-
child.capacity = unit_size.min(capacity);
862-
capacity -= child.capacity;
863-
children.push(child.into());
864-
}
865861

866-
children
862+
// Calculate the number of units corresponding to the asset's capacity
863+
// Safe because capacity and unit_size are both positive finite numbers, so their ratio
864+
// must also be positive and finite.
865+
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
866+
let n_units = (self.capacity / unit_size).value().ceil() as usize;
867+
868+
// Divide the asset into `n_units` children of size `unit_size`
869+
let child_asset = Self {
870+
capacity: unit_size,
871+
..self.clone()
872+
};
873+
let child_asset = AssetRef::from(Rc::new(child_asset));
874+
std::iter::repeat_n(child_asset, n_units).collect()
867875
}
868876
}
869877

@@ -1072,8 +1080,7 @@ impl AssetPool {
10721080

10731081
// If it is divisible, we divide and commission all the children
10741082
if asset.is_divisible() {
1075-
let children = asset.divide_asset();
1076-
for mut child in children {
1083+
for mut child in asset.divide_asset() {
10771084
child.make_mut().commission(
10781085
AssetID(self.next_id),
10791086
Some(AssetGroupID(self.next_group_id)),
@@ -1490,31 +1497,49 @@ mod tests {
14901497
}
14911498

14921499
#[rstest]
1493-
fn divide_asset_works(asset_divisible: Asset) {
1494-
assert!(
1495-
asset_divisible.is_divisible(),
1496-
"Divisbile asset cannot be divided!"
1497-
);
1500+
#[case::exact_multiple(Capacity(12.0), Capacity(4.0), 3)] // 12 / 4 = 3
1501+
#[case::rounded_up(Capacity(11.0), Capacity(4.0), 3)] // 11 / 4 = 2.75 -> 3
1502+
#[case::unit_size_equals_capacity(Capacity(4.0), Capacity(4.0), 1)] // 4 / 4 = 1
1503+
#[case::unit_size_greater_than_capacity(Capacity(3.0), Capacity(4.0), 1)] // 3 / 4 = 0.75 -> 1
1504+
fn divide_asset(
1505+
mut process: Process,
1506+
#[case] capacity: Capacity,
1507+
#[case] unit_size: Capacity,
1508+
#[case] n_expected_children: usize,
1509+
) {
1510+
process.unit_size = Some(unit_size);
1511+
let asset = Asset::new_future(
1512+
"agent1".into(),
1513+
Rc::new(process),
1514+
"GBR".into(),
1515+
capacity,
1516+
2010,
1517+
)
1518+
.unwrap();
14981519

1499-
// Check number of children
1500-
let children = asset_divisible.divide_asset();
1501-
let expected_children = expected_children_for_divisible(&asset_divisible);
1520+
assert!(asset.is_divisible(), "Asset should be divisible!");
1521+
1522+
let children = asset.divide_asset();
15021523
assert_eq!(
15031524
children.len(),
1504-
expected_children,
1525+
n_expected_children,
15051526
"Unexpected number of children"
15061527
);
15071528

1508-
// Check capacity of the children
1509-
let max_child_capacity = asset_divisible.process.unit_size.unwrap();
1529+
// Check all children have capacity equal to unit_size
15101530
for child in children.clone() {
1511-
assert!(
1512-
child.capacity <= max_child_capacity,
1513-
"Child capacity is too large!"
1531+
assert_eq!(
1532+
child.capacity, unit_size,
1533+
"Child capacity should equal unit_size"
15141534
);
15151535
}
1516-
let children_capacity: Capacity = children.iter().map(|a| a.capacity).sum();
1517-
assert_eq!(asset_divisible.capacity, children_capacity);
1536+
1537+
// Check total capacity is >= parent capacity
1538+
let total_child_capacity: Capacity = children.iter().map(|child| child.capacity).sum();
1539+
assert!(
1540+
total_child_capacity >= asset.capacity,
1541+
"Total capacity should be >= parent capacity"
1542+
);
15181543
}
15191544

15201545
#[rstest]
@@ -1558,9 +1583,6 @@ mod tests {
15581583
assert!(!asset_pool.active.is_empty());
15591584
assert_eq!(asset_pool.active.len(), expected_children);
15601585
assert_eq!(asset_pool.next_group_id, 1);
1561-
1562-
let children_capacity: Capacity = asset_pool.active.iter().map(|a| a.capacity).sum();
1563-
assert_eq!(asset_divisible.capacity, children_capacity);
15641586
}
15651587

15661588
#[rstest]
@@ -2024,8 +2046,8 @@ mod tests {
20242046
#[test]
20252047
fn commission_year_before_time_horizon() {
20262048
let processes_patch = FilePatch::new("processes.csv")
2027-
.with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0")
2028-
.with_addition("GASDRV,Dry gas extraction,all,GASPRD,1980,2040,1.0");
2049+
.with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
2050+
.with_addition("GASDRV,Dry gas extraction,all,GASPRD,1980,2040,1.0,");
20292051

20302052
// Check we can run model with asset commissioned before time horizon (simple starts in
20312053
// 2020)
@@ -2049,8 +2071,8 @@ mod tests {
20492071
#[test]
20502072
fn commission_year_after_time_horizon() {
20512073
let processes_patch = FilePatch::new("processes.csv")
2052-
.with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0")
2053-
.with_addition("GASDRV,Dry gas extraction,all,GASPRD,2020,2050,1.0");
2074+
.with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
2075+
.with_addition("GASDRV,Dry gas extraction,all,GASPRD,2020,2050,1.0,");
20542076

20552077
// Check we can run model with asset commissioned after time horizon (simple ends in 2040)
20562078
let patches = vec![

src/input/asset.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ use crate::process::ProcessMap;
77
use crate::region::RegionID;
88
use crate::units::Capacity;
99
use anyhow::{Context, Result, ensure};
10+
use float_cmp::approx_eq;
1011
use indexmap::IndexSet;
1112
use itertools::Itertools;
13+
use log::warn;
1214
use serde::Deserialize;
1315
use std::path::Path;
1416
use std::rc::Rc;
@@ -103,6 +105,24 @@ where
103105
asset.agent_id,
104106
);
105107

108+
// Check that capacity is approximately a multiple of the process unit size
109+
// If not, raise a warning
110+
if let Some(unit_size) = process.unit_size {
111+
let ratio = (asset.capacity / unit_size).value();
112+
if !approx_eq!(f64, ratio, ratio.ceil()) {
113+
let n_units = ratio.ceil();
114+
warn!(
115+
"Asset capacity {} for process {} is not a multiple of unit size {}. \
116+
Asset will be divided into {} units with combined capacity of {}.",
117+
asset.capacity,
118+
asset.process_id,
119+
unit_size,
120+
n_units,
121+
unit_size.value() * n_units
122+
);
123+
}
124+
}
125+
106126
Asset::new_future_with_max_decommission(
107127
agent_id.clone(),
108128
Rc::clone(process),

src/input/process/flow.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -563,8 +563,8 @@ mod tests {
563563
// non-milestone years.
564564
let patches = vec![
565565
FilePatch::new("processes.csv")
566-
.with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0")
567-
.with_addition("GASDRV,Dry gas extraction,all,GASPRD,1980,2040,1.0"),
566+
.with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
567+
.with_addition("GASDRV,Dry gas extraction,all,GASPRD,1980,2040,1.0,"),
568568
FilePatch::new("process_flows.csv")
569569
.with_deletion("GASPRC,GASPRD,all,all,-1.05,fixed,")
570570
.with_addition("GASPRC,GASPRD,all,2020;2030;2040,-1.05,fixed,"),

src/patch.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ impl ModelPatch {
2626
}
2727

2828
/// Create a new empty `ModelPatch` for an example model
29-
#[cfg(test)]
3029
pub fn from_example(name: &str) -> Self {
3130
let base_model_dir = PathBuf::from("examples").join(name);
3231
ModelPatch::new(base_model_dir)
@@ -62,7 +61,7 @@ impl ModelPatch {
6261
}
6362

6463
/// Build this `ModelPatch` into `out_dir` (creating/overwriting files there).
65-
fn build<O: AsRef<Path>>(&self, out_dir: O) -> Result<()> {
64+
pub fn build<O: AsRef<Path>>(&self, out_dir: O) -> Result<()> {
6665
let base_dir = self.base_model_dir.as_path();
6766
let out_path = out_dir.as_ref();
6867

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
asset_id,process_id,region_id,agent_id,group_id,commission_year,decommission_year,capacity
2+
0,GASDRV,GBR,A0_GEX,,2020,,4002.26
3+
1,GASPRC,GBR,A0_GPR,,2020,,3782.13
4+
2,WNDFRM,GBR,A0_ELC,,2020,2040,3.964844
5+
3,GASCGT,GBR,A0_ELC,,2020,2040,2.43
6+
4,RGASBR,GBR,A0_RES,,2020,2035,1000.0
7+
5,RGASBR,GBR,A0_RES,,2020,2035,1000.0
8+
6,RGASBR,GBR,A0_RES,,2020,2035,1000.0
9+
7,RELCHP,GBR,A0_RES,,2020,2035,399.98
10+
8,RGASBR,GBR,A0_RES,1,2030,,1000.0
11+
9,GASCGT,GBR,A0_ELC,,2030,2040,0.44245235762867363
12+
10,RGASBR,GBR,A0_RES,2,2040,,1000.0
13+
11,RGASBR,GBR,A0_RES,2,2040,,1000.0
14+
12,RGASBR,GBR,A0_RES,2,2040,,1000.0
15+
13,RGASBR,GBR,A0_RES,2,2040,,1000.0

0 commit comments

Comments
 (0)