Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add data structure for asset pool #388

Merged
merged 10 commits into from
Feb 17, 2025
2 changes: 1 addition & 1 deletion examples/simple/commodity_costs.csv
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
commodity_id,region_id,balance_type,year,time_slice,value
CO2EMT,GBR,net,2020,annual,0.04
CO2EMT,GBR,net,2100,annual,0.04
CO2EMT,GBR,net,2030,annual,0.04
2 changes: 1 addition & 1 deletion examples/simple/demand.csv
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
commodity_id,region_id,year,demand
RSHEAT,GBR,2020,927.38
RSHEAT,GBR,2100,927.38
RSHEAT,GBR,2030,927.38
2 changes: 1 addition & 1 deletion examples/simple/model.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[milestone_years]
years = [2020, 2100]
years = [2020, 2030]
12 changes: 6 additions & 6 deletions examples/simple/process_parameters.csv
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
process_id,start_year,end_year,capital_cost,fixed_operating_cost,variable_operating_cost,lifetime,discount_rate,cap2act
GASDRV,2020,2100,10.0,0.3,2.0,25,0.1,1.0
GASPRC,2020,2100,7.0,0.21,0.5,25,0.1,1.0
WNDFRM,2020,2100,1000.0,30.0,0.4,25,0.1,31.54
GASCGT,2020,2100,700.0,21.0,0.55,30,0.1,31.54
RGASBR,2020,2100,55.56,1.6668,0.16,15,0.1,1.0
RELCHP,2020,2100,138.9,4.167,0.17,15,0.1,1.0
GASDRV,2020,2030,10.0,0.3,2.0,25,0.1,1.0
GASPRC,2020,2030,7.0,0.21,0.5,25,0.1,1.0
WNDFRM,2020,2030,1000.0,30.0,0.4,25,0.1,31.54
GASCGT,2020,2030,700.0,21.0,0.55,30,0.1,31.54
RGASBR,2020,2030,55.56,1.6668,0.16,15,0.1,1.0
RELCHP,2020,2030,138.9,4.167,0.17,15,0.1,1.0
200 changes: 196 additions & 4 deletions src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,20 @@ pub enum ObjectiveType {
EquivalentAnnualCost,
}

/// A unique identifier for an asset
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct AssetID(u32);

impl AssetID {
/// Sentinel value assigned to [`Asset`]s when they are initially created
pub const INVALID: AssetID = AssetID(u32::MAX);
}

/// An asset controlled by an agent.
#[derive(Clone, Debug, PartialEq)]
pub struct Asset {
/// A unique identifier for the asset
pub id: u32,
pub id: AssetID,
/// A unique identifier for the agent
pub agent_id: Rc<str>,
/// The [`Process`] that this asset corresponds to
Expand All @@ -102,6 +111,32 @@ pub struct Asset {
}

impl Asset {
/// Create a new [`Asset`].
///
/// The `id` field is initially set to [`AssetID::INVALID`], but is changed to a unique value
/// when the asset is commissioned.
pub fn new(
agent_id: Rc<str>,
process: Rc<Process>,
region_id: Rc<str>,
capacity: f64,
commission_year: u32,
) -> Self {
Self {
id: AssetID::INVALID,
agent_id,
process,
region_id,
capacity,
commission_year,
}
}

/// The last year in which this asset should be decommissioned
pub fn decommission_year(&self) -> u32 {
self.commission_year + self.process.parameter.lifetime
}

/// Get the activity limits for this asset in a particular time slice
pub fn get_activity_limits(&self, time_slice: &TimeSliceID) -> RangeInclusive<f64> {
let limits = self.process.capacity_fractions.get(time_slice).unwrap();
Expand All @@ -113,14 +148,81 @@ impl Asset {
}

/// A pool of [`Asset`]s
pub type AssetPool = Vec<Asset>;
pub struct AssetPool {
/// The pool of assets, both active and yet to be commissioned.
///
/// Sorted in order of commission year.
assets: Vec<Asset>,
/// Current milestone year.
current_year: u32,
}

impl AssetPool {
/// Create a new [`AssetPool`]
pub fn new(mut assets: Vec<Asset>) -> Self {
// Sort in order of commission year
assets.sort_by(|a, b| a.commission_year.cmp(&b.commission_year));

// Assign each asset a unique ID
for (id, asset) in assets.iter_mut().enumerate() {
asset.id = AssetID(id as u32);
}

Self {
assets,
current_year: 0,
}
}

/// Commission new assets for the specified milestone year
pub fn commission_new(&mut self, year: u32) {
assert!(
year >= self.current_year,
"Assets have already been commissioned for year {year}"
);
self.current_year = year;
}

/// Decommission old assets for the specified milestone year
pub fn decomission_old(&mut self, year: u32) {
assert!(
year >= self.current_year,
"Cannot decommission assets in the past (current year: {})",
self.current_year
);
self.assets.retain(|asset| asset.decommission_year() > year);
}
Comment on lines +178 to +194
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just so I understand; So all commission_new() is doing is updating the current_year for the AssetPool. decommission_old is doing the important bit by making sure that only assets whose decommission year is in the future is kept in AssetPool.

Do we not want to set the id field of the Asset to AssetID::INVALID here also? Or is that not the intended use of that value? Does "decommissioned" have a different meaning to "inactive"?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just so I understand; So all commission_new() is doing is updating the current_year for the AssetPool. decommission_old is doing the important bit by making sure that only assets whose decommission year is in the future is kept in AssetPool.

Yep, exactly. Because the assets are sorted by commission year, to get the active assets we can then just iterate over the Vec until we find a commission year > current_year.

Decommissioned assets are just deleted from the Vec for now, though we could potentially just mark them as inactive in some way instead (maybe replace them with None?), which would have the advantage that we wouldn't have to move memory around every time we decommission things. But I just went for deleting them for now because that seemed easier.

Do we not want to set the id field of the Asset to AssetID::INVALID here also? Or is that not the intended use of that value? Does "decommissioned" have a different meaning to "inactive"?

I've noticed that the doc comments are a bit confusing here. AssetID::INVALID is just the ID that every Asset is given when it's created. When we move the assets into the AssetPool, then at that point they're all given a unique ID. The reason for doing it at this point is so that we can sort the assets by commission year and then give them IDs in that same order, which will make it faster to look up assets by ID. We could also just sort the assets when we load them in, but it seemed a bit fragile for AssetPool to rely on this (we might refactor the input code and subtly break it).


/// Get an asset with the specified ID
///
/// # Panics
///
/// Panics if `id` is not in pool.
pub fn get(&self, id: AssetID) -> &Asset {
// The assets in `active` are in order of ID
let idx = self
.assets
.binary_search_by(|asset| asset.id.cmp(&id))
.expect("id not found");

&self.assets[idx]
}

/// Iterate over active assets
pub fn iter(&self) -> impl Iterator<Item = &Asset> {
self.assets
.iter()
.take_while(|asset| asset.commission_year <= self.current_year)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::commodity::{CommodityCostMap, CommodityType, DemandMap};
use crate::process::{FlowType, ProcessFlow, ProcessParameter};
use crate::process::{FlowType, Process, ProcessCapacityMap, ProcessFlow, ProcessParameter};
use crate::time_slice::TimeSliceLevel;
use itertools::{assert_equal, Itertools};
use std::iter;

#[test]
Expand Down Expand Up @@ -166,7 +268,7 @@ mod tests {
regions: RegionSelection::All,
});
let asset = Asset {
id: 0,
id: AssetID(0),
agent_id: "agent1".into(),
process: Rc::clone(&process),
region_id: "GBR".into(),
Expand All @@ -176,4 +278,94 @@ mod tests {

assert_eq!(asset.get_activity_limits(&time_slice), 6.0..=f64::INFINITY);
}

fn create_asset_pool() -> AssetPool {
let process_param = ProcessParameter {
process_id: "process1".into(),
years: 2010..=2020,
capital_cost: 5.0,
fixed_operating_cost: 2.0,
variable_operating_cost: 1.0,
lifetime: 5,
discount_rate: 0.9,
cap2act: 1.0,
};
let process = Rc::new(Process {
id: "process1".into(),
description: "Description".into(),
capacity_fractions: ProcessCapacityMap::new(),
flows: vec![],
parameter: process_param.clone(),
regions: RegionSelection::All,
});
let future = [2020, 2010]
.map(|year| {
Asset::new(
"agent1".into(),
Rc::clone(&process),
"GBR".into(),
1.0,
year,
)
})
.into_iter()
.collect_vec();

AssetPool::new(future)
}

#[test]
fn test_asset_pool_new() {
let assets = create_asset_pool();
assert!(assets.current_year == 0);

// Should be in order of commission year
assert!(assets.assets.len() == 2);
assert!(assets.assets[0].commission_year == 2010);
assert!(assets.assets[1].commission_year == 2020);
}

#[test]
fn test_asset_pool_commission_new() {
// Asset to be commissioned in this year
let mut assets = create_asset_pool();
assets.commission_new(2010);
assert!(assets.current_year == 2010);
assert_equal(assets.iter(), iter::once(&assets.assets[0]));

// Commission year has passed
let mut assets = create_asset_pool();
assets.commission_new(2011);
assert!(assets.current_year == 2011);
assert_equal(assets.iter(), iter::once(&assets.assets[0]));

// Nothing to commission for this year
let mut assets = create_asset_pool();
assets.commission_new(2000);
assert!(assets.current_year == 2000);
assert!(assets.iter().next().is_none()); // no active assets
}

#[test]
fn test_asset_pool_decommission_old() {
let mut assets = create_asset_pool();
let assets2 = assets.assets.clone();

assets.commission_new(2020);
assert!(assets.assets.len() == 2);
assets.decomission_old(2020); // should decommission first asset (lifetime == 5)
assert_equal(&assets.assets, iter::once(&assets2[1]));
assets.decomission_old(2022); // nothing to decommission
assert_equal(&assets.assets, iter::once(&assets2[1]));
assets.decomission_old(2025); // should decommission second asset
assert!(assets.assets.is_empty());
}

#[test]
fn test_asset_pool_get() {
let mut assets = create_asset_pool();
assets.commission_new(2020);
assert!(*assets.get(AssetID(0)) == assets.assets[0]);
assert!(*assets.get(AssetID(1)) == assets.assets[1]);
}
}
2 changes: 1 addition & 1 deletion src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ pub fn load_model<P: AsRef<Path>>(model_dir: P) -> Result<(Model, AssetPool)> {
time_slice_info,
regions,
};
Ok((model, assets))
Ok((model, AssetPool::new(assets)))
}

#[cfg(test)]
Expand Down
33 changes: 12 additions & 21 deletions src/input/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,6 @@ fn read_assets_from_iter<I>(
where
I: Iterator<Item = AssetRaw>,
{
let mut id = 0u32;

iter.map(|asset| -> Result<_> {
let agent_id = agent_ids.get_id(&asset.agent_id)?;
let process = processes
Expand All @@ -80,19 +78,13 @@ where
process.id
);

let asset = Asset {
id,
Ok(Asset::new(
agent_id,
process: Rc::clone(process),
Rc::clone(process),
region_id,
capacity: asset.capacity,
commission_year: asset.commission_year,
};

// Increment ID for next asset
id += 1;

Ok(asset)
asset.capacity,
asset.commission_year,
))
})
.try_collect()
}
Expand Down Expand Up @@ -139,14 +131,13 @@ mod tests {
capacity: 1.0,
commission_year: 2010,
};
let asset_out = Asset {
id: 0,
agent_id: "agent1".into(),
process: Rc::clone(&process),
region_id: "GBR".into(),
capacity: 1.0,
commission_year: 2010,
};
let asset_out = Asset::new(
"agent1".into(),
Rc::clone(&process),
"GBR".into(),
1.0,
2010,
);
assert_equal(
read_assets_from_iter([asset_in].into_iter(), &agent_ids, &processes, &region_ids)
.unwrap(),
Expand Down
13 changes: 5 additions & 8 deletions src/simulation.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! Functionality for running the MUSE 2.0 simulation.
use crate::agent::{Asset, AssetPool};
use crate::agent::AssetPool;
use crate::model::Model;
use crate::time_slice::TimeSliceID;
use log::info;
Expand Down Expand Up @@ -60,6 +60,10 @@ pub fn run(model: Model, mut assets: AssetPool) {
for year in model.iter_years() {
info!("Milestone year: {year}");

// Commission and decommission assets for this milestone year
assets.decomission_old(year);
assets.commission_new(year);

// Dispatch optimisation
let solution = perform_dispatch_optimisation(&model, &assets, year);
update_commodity_flows(&solution, &mut assets);
Expand All @@ -69,10 +73,3 @@ pub fn run(model: Model, mut assets: AssetPool) {
perform_agent_investment(&model, &mut assets);
}
}

/// Get an iterator of active [`Asset`]s for the specified milestone year.
pub fn filter_assets(assets: &AssetPool, year: u32) -> impl Iterator<Item = &Asset> {
assets
.iter()
.filter(move |asset| asset.commission_year <= year)
}
Loading