Skip to content
Draft
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
7 changes: 0 additions & 7 deletions schemas/input/model.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,6 @@ 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:
type: number
description: A factor which constrains the maximum capacity given to candidate assets
default: 0.1
notes: |
This is the proportion of the maximum required capacity across time slices (for a given
asset/commodity etc. combination).
value_of_lost_load:
type: number
description: The cost applied to unmet demand
Expand Down
28 changes: 15 additions & 13 deletions schemas/input/processes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,20 @@ fields:
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.
notes: Must be >=0. Optional (defaults to 1.0).
- name: unit_size
notes: Must be >0. Optional (defaults to 1.0).
- 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 10 / 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 @@ -56,8 +56,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
9 changes: 5 additions & 4 deletions src/fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::agent::{
Agent, AgentCommodityPortionsMap, AgentID, AgentMap, AgentObjectiveMap, AgentSearchSpaceMap,
DecisionRule,
};
use crate::asset::{Asset, AssetCapacity, AssetPool, AssetRef};
use crate::asset::{Asset, AssetPool, AssetRef};
use crate::commodity::{
Commodity, CommodityID, CommodityLevyMap, CommodityType, DemandMap, PricingStrategy,
};
Expand Down 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 Expand Up @@ -406,7 +408,6 @@ pub fn appraisal_output(asset: Asset, time_slice: TimeSliceID) -> AppraisalOutpu
let unmet_demand = indexmap! { time_slice.clone() => Flow(5.0) };
AppraisalOutput {
asset: AssetRef::from(asset),
capacity: AssetCapacity::Continuous(Capacity(42.0)),
coefficients: Rc::new(ObjectiveCoefficients {
activity_coefficients,
market_costs,
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
36 changes: 22 additions & 14 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,8 @@ struct ProcessRaw {
start_year: Option<u32>,
end_year: Option<u32>,
capacity_to_activity: Option<ActivityPerCapacity>,
unit_size: Option<Capacity>,
capacity_granularity: Option<Capacity>,
is_divisible: Option<bool>,
}
define_id_getter! {ProcessRaw, ProcessID}

Expand Down Expand Up @@ -146,19 +147,25 @@ 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",
capacity_to_activity > ActivityPerCapacity(0.0),
"Error in process {}: capacity_to_activity must be > 0",
process_raw.id
);

// is_divisible defaults to false if not specified
let is_divisible = process_raw.is_divisible.unwrap_or(false);

// Capacity granularity defaults to 10 / capacity_to_activity if not specified
let capacity_granularity = process_raw
.capacity_granularity
.unwrap_or_else(|| Activity(10.0) / capacity_to_activity);

// Validate capacity_granularity
ensure!(
capacity_granularity > Capacity(0.0),
"Error in process {}: capacity_granularity must be > 0",
process_raw.id
);

Expand All @@ -173,7 +180,8 @@ where
primary_output,
capacity_to_activity,
investment_constraints: ProcessInvestmentConstraintsMap::new(),
unit_size: process_raw.unit_size,
capacity_granularity,
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
12 changes: 1 addition & 11 deletions src/model/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
//! `model.toml` configuration used by the model. Validation functions ensure sensible numeric
//! ranges and invariants for runtime use.
use crate::asset::check_capacity_valid_for_asset;
use crate::input::{
deserialise_proportion_nonzero, input_err_msg, is_sorted_and_unique, read_toml,
};
use crate::input::{input_err_msg, is_sorted_and_unique, read_toml};
use crate::units::{Capacity, Dimensionless, Flow, MoneyPerFlow};
use anyhow::{Context, Result, ensure};
use itertools::Itertools;
Expand Down Expand Up @@ -77,11 +75,6 @@ pub struct ModelParameters {
///
/// Don't change unless you know what you're doing.
pub candidate_asset_capacity: Capacity,
/// Affects the maximum capacity that can be given to a newly created asset.
///
/// It is the proportion of maximum capacity that could be required across time slices.
#[serde(deserialize_with = "deserialise_proportion_nonzero")]
pub capacity_limit_factor: Dimensionless,
/// The cost applied to unmet demand.
///
/// Currently this only applies to the LCOX appraisal.
Expand Down Expand Up @@ -118,7 +111,6 @@ impl Default for ModelParameters {
// Default values for optional parameters
allow_dangerous_options: false,
candidate_asset_capacity: Capacity(1e-4),
capacity_limit_factor: Dimensionless(0.1),
value_of_lost_load: MoneyPerFlow(1e9),
max_ironing_out_iterations: 1,
price_tolerance: Dimensionless(1e-6),
Expand Down Expand Up @@ -328,8 +320,6 @@ impl ModelParameters {
// milestone_years
check_milestone_years(&self.milestone_years)?;

// capacity_limit_factor already validated with deserialise_proportion_nonzero

// candidate_asset_capacity
check_capacity_valid_for_asset(self.candidate_asset_capacity)
.context("Invalid value for candidate_asset_capacity")?;
Expand Down
4 changes: 2 additions & 2 deletions src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ impl DebugDataWriter {
asset_id: result.asset.id(),
process_id: result.asset.process_id().clone(),
region_id: result.asset.region_id().clone(),
capacity: result.capacity.total_capacity(),
capacity: result.asset.capacity().total_capacity(),
metric: result.metric.as_ref().map(|m| m.value()),
};
self.appraisal_results_writer.serialize(row)?;
Expand Down Expand Up @@ -1183,7 +1183,7 @@ mod tests {
asset_id: None,
process_id: asset.process_id().clone(),
region_id: asset.region_id().clone(),
capacity: Capacity(42.0),
capacity: Capacity(2.0),
metric: Some(4.14),
};
let records: Vec<AppraisalResultsRow> =
Expand Down
19 changes: 14 additions & 5 deletions src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,28 @@ pub struct Process {
pub capacity_to_activity: ActivityPerCapacity,
/// Investment constraints for this process
pub investment_constraints: ProcessInvestmentConstraintsMap,
/// Capacity of the units in which an asset for this process will be divided into when commissioned, if any.
/// The amount of capacity appraised and commissioned at a time.
pub capacity_granularity: Capacity,
/// Whether or not assets are divisible.
///
/// By default, an asset will not be divided when commissioned (`unit_size` will be None), but
/// if this is set, then it will be divided in as many assets as needed to commission the total
/// capacity, each having a `unit_size` capacity or a fraction of it.
pub unit_size: Option<Capacity>,
/// This controls whether this process is commissioned as a set of individual units of size
/// `capacity_granularity` (true) or as a single indivisible unit (false).
pub is_divisible: bool,
}

impl Process {
/// Whether the process can be commissioned in a given year
pub fn active_for_year(&self, year: u32) -> bool {
self.years.contains(&year)
}

/// Capacity of the units making up divisible assets, if applicable.
///
/// For divisible processes, this will be `capacity_granularity`. For indivisible processes,
/// this will be None.
pub fn unit_size(&self) -> Option<Capacity> {
self.is_divisible.then_some(self.capacity_granularity)
}
}

/// Defines the activity limits for a process in a given region and year
Expand Down
Loading
Loading