Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions examples/simple/processes.csv
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
id,description,regions,primary_output,start_year,end_year,capacity_to_activity,unit_size
GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,
GASPRC,Gas processing,all,GASNAT,2020,2040,1.0,
WNDFRM,Wind farm,all,ELCTRI,2020,2040,31.54,
GASCGT,Gas combined cycle turbine,all,ELCTRI,2020,2040,31.54,
RGASBR,Gas boiler,all,RSHEAT,2020,2040,1.0,
RELCHP,Heat pump,all,RSHEAT,2020,2040,1.0,
id,description,regions,primary_output,start_year,end_year,capacity_to_activity,is_divisible
GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,false
GASPRC,Gas processing,all,GASNAT,2020,2040,1.0,false
WNDFRM,Wind farm,all,ELCTRI,2020,2040,31.54,false
GASCGT,Gas combined cycle turbine,all,ELCTRI,2020,2040,31.54,false
RGASBR,Gas boiler,all,RSHEAT,2020,2040,1.0,false
RELCHP,Heat pump,all,RSHEAT,2020,2040,1.0,false
10 changes: 5 additions & 5 deletions schemas/input/model.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ properties:
default: 0.0001
notes: |
The default value should work. Do not change this value unless you know what you're doing!
capacity_limit_factor:
default_capacity_granularity_factor:
type: number
description: A factor which constrains the maximum capacity given to candidate assets
default: 0.1
description: Factor used to define the default `capacity_granularity`
default: 100
notes: |
This is the proportion of the maximum required capacity across time slices (for a given
asset/commodity etc. combination).
For processes where `capacity_granularity` is unspecified, the default value is calculated as
`default_capacity_granularity_factor / capacity_to_activity`.
value_of_lost_load:
type: number
description: The cost applied to unmet demand
Expand Down
27 changes: 15 additions & 12 deletions schemas/input/processes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,20 @@ fields:
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.
notes: Must be >0. Optional (defaults to 1.0).
- name: unit_size
- name: capacity_granularity
type: number
description:
Capacity of the units in which an asset for this process will be divided into when
commissioned, if any.
notes:
If present, must be >0. Optional (defaults to None). Assets with a defined unit size are
divided into n = ceil(C / U) equal units, where C is overall capacity and U is unit_size
(i.e. rounding up the number of units, which may result in a total capacity greater than C, if
C is not an exact multiple of U).

It should be noted that making this number too small with respect to the typical size of an
asset might create hundreds or thousands of children assets, with a very negative effect on
the performance. Users are advised to use this feature with care.
The minimum capacity increment that can be invested in for this process. Reducing this value
allows for more precise capacities, but can slow down the simulation.
notes: Must be >0. Optional (defaults to
default_capacity_granularity_factor / capacity_to_activity).
- name: is_divisible
type: boolean
description:
Whether the process is commissioned as a set of individual units of size
`capacity_granularity` (true), which can be decommissioned individually, or as a single
indivisible unit (false) which must be decommissioned in one go. If true, assets are
divided into n = ceil(C / G) equal units, where C is overall capacity and G is
`capacity_granularity` (i.e. rounding up the number of units, which may result in a total
capacity greater than C, if C is not an exact multiple of G).
notes: Optional (defaults to false).
24 changes: 11 additions & 13 deletions src/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ impl Asset {
capacity: Capacity,
commission_year: u32,
) -> Result<Self> {
let unit_size = process.unit_size;
let unit_size = process.unit_size();
Self::new_with_state(
AssetState::Candidate,
process,
Expand Down Expand Up @@ -190,7 +190,7 @@ impl Asset {
capacity: Capacity,
commission_year: u32,
) -> Result<Self> {
let unit_size = process.unit_size;
let unit_size = process.unit_size();
Self::new_with_state(
AssetState::Ready {
agent_id,
Expand All @@ -216,7 +216,7 @@ impl Asset {
capacity: Capacity,
commission_year: u32,
) -> Result<Self> {
let unit_size = process.unit_size;
let unit_size = process.unit_size();
Self::new_with_state(
AssetState::Commissioned {
id: AssetID(0),
Expand Down Expand Up @@ -988,7 +988,7 @@ impl UserAsset {
max_decommission_year: Option<u32>,
) -> Result<Self> {
check_capacity_valid_for_asset(capacity)?;
let unit_size = process.unit_size;
let unit_size = process.unit_size();
let asset = Asset::new_with_state(
AssetState::Ready {
agent_id,
Expand Down Expand Up @@ -1404,7 +1404,8 @@ mod tests {
#[case] unit_size: Capacity,
#[case] n_expected_children: usize,
) {
process.unit_size = Some(unit_size);
process.capacity_granularity = unit_size;
process.is_divisible = true;
let asset = AssetRef::from(
Asset::new_ready(
"agent1".into(),
Expand Down Expand Up @@ -1442,10 +1443,7 @@ mod tests {

#[rstest]
fn into_for_each_child_nondivisible(asset: Asset) {
assert!(
asset.process.unit_size.is_none(),
"Asset should be non-divisible"
);
assert!(!asset.process.is_divisible, "Asset should be non-divisible");

let asset = AssetRef::from(asset);
let mut count = 0;
Expand Down Expand Up @@ -1542,8 +1540,8 @@ mod tests {
#[test]
fn commission_year_before_time_horizon() {
let processes_patch = FilePatch::new("processes.csv")
.with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
.with_addition("GASDRV,Dry gas extraction,all,GASPRD,1980,2040,1.0,");
.with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,false")
.with_addition("GASDRV,Dry gas extraction,all,GASPRD,1980,2040,1.0,false");

// Check we can run model with asset commissioned before time horizon (simple starts in
// 2020)
Expand All @@ -1567,8 +1565,8 @@ mod tests {
#[test]
fn commission_year_after_time_horizon() {
let processes_patch = FilePatch::new("processes.csv")
.with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
.with_addition("GASDRV,Dry gas extraction,all,GASPRD,2020,2050,1.0,");
.with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,false")
.with_addition("GASDRV,Dry gas extraction,all,GASPRD,2020,2050,1.0,false");

// Check we can run model with asset commissioned after time horizon (simple ends in 2040)
let patches = vec![
Expand Down
8 changes: 5 additions & 3 deletions src/asset/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ mod tests {
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
fn expected_children_for_divisible(asset: &Asset) -> usize {
(asset.total_capacity() / asset.process.unit_size.expect("Asset is not divisible"))
(asset.total_capacity() / asset.process.unit_size().expect("Asset is not divisible"))
.value()
.ceil() as usize
}
Expand Down Expand Up @@ -442,7 +442,8 @@ mod tests {
let original_count = asset_pool.assets.len();

// Create new non-commissioned assets
process.unit_size = Some(Capacity(4.0));
process.capacity_granularity = Capacity(4.0);
process.is_divisible = true;
let process_rc = Rc::new(process);
let new_assets: Vec<AssetRef> = vec![
Asset::new_ready(
Expand Down Expand Up @@ -474,7 +475,8 @@ mod tests {
let existing_assets = asset_pool.take();

// Add one ready divisible asset so extend() commissions multiple new children
process.unit_size = Some(Capacity(4.0));
process.capacity_granularity = Capacity(4.0);
process.is_divisible = true;
let process_rc = Rc::new(process);
let ready_divisible: AssetRef = Asset::new_ready(
"agent_selected".into(),
Expand Down
4 changes: 2 additions & 2 deletions src/example/patches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ fn get_all_patches() -> PatchMap {
"simple",
vec![
FilePatch::new("processes.csv")
.with_deletion("RGASBR,Gas boiler,all,RSHEAT,2020,2040,1.0,")
.with_addition("RGASBR,Gas boiler,all,RSHEAT,2020,2040,1.0,1000"),
.with_deletion("RGASBR,Gas boiler,all,RSHEAT,2020,2040,1.0,false")
.with_addition("RGASBR,Gas boiler,all,RSHEAT,2020,2040,1.0,true"),
],
None,
),
Expand Down
6 changes: 4 additions & 2 deletions src/fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,8 @@ pub fn asset(process: Process) -> Asset {

#[fixture]
pub fn asset_divisible(mut process: Process) -> Asset {
process.unit_size = Some(Capacity(4.0));
process.capacity_granularity = Capacity(4.0);
process.is_divisible = true;
Asset::new_ready(
"agent1".into(),
Rc::new(process),
Expand Down Expand Up @@ -321,7 +322,8 @@ pub fn process(
primary_output: None,
capacity_to_activity: ActivityPerCapacity(1.0),
investment_constraints: process_investment_constraints,
unit_size: None,
capacity_granularity: Capacity(1.0),
is_divisible: false,
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ pub fn load_model<P: AsRef<Path>>(model_dir: P) -> Result<Model> {
&region_ids,
&time_slice_info,
years,
model_params.default_capacity_granularity_factor,
)?;
let agents = read_agents(
model_dir.as_ref(),
Expand Down Expand Up @@ -324,6 +325,7 @@ pub fn load_commodity_graphs<P: AsRef<Path>>(
&region_ids,
&time_slice_info,
years,
model_params.default_capacity_granularity_factor,
)?;

let commodity_graphs = build_commodity_graphs_for_model(&processes, &region_ids, years);
Expand Down
2 changes: 1 addition & 1 deletion src/input/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ where

// Check that capacity is approximately a multiple of the process unit size
// If not, raise a warning
if let Some(unit_size) = process.unit_size {
if let Some(unit_size) = process.unit_size() {
let ratio = (asset.capacity / unit_size).value();
if !approx_eq!(f64, ratio, ratio.ceil()) {
let n_units = ratio.ceil();
Expand Down
52 changes: 37 additions & 15 deletions src/input/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::process::{
};
use crate::region::{RegionID, parse_region_str};
use crate::time_slice::TimeSliceInfo;
use crate::units::{ActivityPerCapacity, Capacity};
use crate::units::{Activity, ActivityPerCapacity, Capacity};
use anyhow::{Context, Ok, Result, ensure};
use indexmap::IndexSet;
use log::warn;
Expand Down Expand Up @@ -37,7 +37,9 @@ struct ProcessRaw {
start_year: Option<u32>,
end_year: Option<u32>,
capacity_to_activity: Option<ActivityPerCapacity>,
unit_size: Option<Capacity>,
capacity_granularity: Option<Capacity>,
#[serde(default)]
is_divisible: bool, // defaults to false if not specified
}
define_id_getter! {ProcessRaw, ProcessID}

Expand All @@ -60,8 +62,15 @@ pub fn read_processes(
region_ids: &IndexSet<RegionID>,
time_slice_info: &TimeSliceInfo,
milestone_years: &[u32],
default_capacity_granularity_factor: Activity,
) -> Result<ProcessMap> {
let mut processes = read_processes_file(model_dir, milestone_years, region_ids, commodities)?;
let mut processes = read_processes_file(
model_dir,
milestone_years,
region_ids,
commodities,
default_capacity_granularity_factor,
)?;
let mut activity_limits = read_process_availabilities(model_dir, &processes, time_slice_info)?;
let mut flows = read_process_flows(model_dir, &mut processes, commodities, milestone_years)?;
let mut parameters = read_process_parameters(model_dir, &processes, milestone_years)?;
Expand All @@ -88,18 +97,26 @@ fn read_processes_file(
milestone_years: &[u32],
region_ids: &IndexSet<RegionID>,
commodities: &CommodityMap,
default_capacity_granularity_factor: Activity,
) -> Result<ProcessMap> {
let file_path = model_dir.join(PROCESSES_FILE_NAME);
let processes_csv = read_csv(&file_path)?;
read_processes_file_from_iter(processes_csv, milestone_years, region_ids, commodities)
.with_context(|| input_err_msg(&file_path))
read_processes_file_from_iter(
processes_csv,
milestone_years,
region_ids,
commodities,
default_capacity_granularity_factor,
)
.with_context(|| input_err_msg(&file_path))
}

fn read_processes_file_from_iter<I>(
iter: I,
milestone_years: &[u32],
region_ids: &IndexSet<RegionID>,
commodities: &CommodityMap,
default_capacity_granularity_factor: Activity,
) -> Result<ProcessMap>
where
I: Iterator<Item = ProcessRaw>,
Expand Down Expand Up @@ -146,22 +163,26 @@ where
.capacity_to_activity
.unwrap_or(ActivityPerCapacity(1.0));

// Validate unit_size
if process_raw.unit_size.is_some() {
ensure!(
process_raw.unit_size > Some(Capacity(0.0)),
"Error in process {}: unit_size must be > 0 or None",
process_raw.id
);
}

// Validate capacity_to_activity
ensure!(
capacity_to_activity > ActivityPerCapacity(0.0),
"Error in process {}: capacity_to_activity must be > 0",
process_raw.id
);
Comment thread
tsmbland marked this conversation as resolved.

// Capacity granularity defaults to
// `default_capacity_granularity_factor / capacity_to_activity` if not specified
let capacity_granularity = process_raw
.capacity_granularity
.unwrap_or_else(|| default_capacity_granularity_factor / capacity_to_activity);

// Validate capacity_granularity
ensure!(
capacity_granularity > Capacity(0.0),
"Error in process {}: capacity_granularity must be > 0",
process_raw.id
);
Comment on lines +180 to +184

let process = Process {
id: process_raw.id.clone(),
description: process_raw.description,
Expand All @@ -173,7 +194,8 @@ where
primary_output,
capacity_to_activity,
investment_constraints: ProcessInvestmentConstraintsMap::new(),
unit_size: process_raw.unit_size,
capacity_granularity,
is_divisible: process_raw.is_divisible,
};

ensure!(
Expand Down
4 changes: 2 additions & 2 deletions src/input/process/flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -796,8 +796,8 @@ mod tests {
// non-milestone years.
let patches = vec![
FilePatch::new("processes.csv")
.with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
.with_addition("GASDRV,Dry gas extraction,all,GASPRD,1980,2040,1.0,"),
.with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,false")
.with_addition("GASDRV,Dry gas extraction,all,GASPRD,1980,2040,1.0,false"),
FilePatch::new("process_flows.csv")
.with_deletion("GASPRC,GASPRD,all,all,-1.05,fixed,")
.with_addition("GASPRC,GASPRD,all,2020;2030;2040,-1.05,fixed,"),
Expand Down
Loading
Loading