diff --git a/src/asset.rs b/src/asset.rs index f7fe42873..92812103e 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -14,7 +14,7 @@ use crate::units::{ }; use anyhow::{Context, Result, ensure}; use indexmap::IndexMap; -use itertools::{Itertools, chain}; +use itertools::Itertools; use log::{debug, warn}; use serde::{Deserialize, Serialize}; use std::cmp::{Ordering, min}; @@ -1187,14 +1187,11 @@ impl Ord for AssetRef { } } -/// A pool of [`Asset`]s +/// The active pool of [`Asset`]s +#[derive(Default)] pub struct AssetPool { /// The pool of active assets, sorted by ID - active: Vec, - /// Assets that have not yet been commissioned, sorted by commission year - future: Vec, - /// Assets that have been decommissioned - decommissioned: Vec, + assets: Vec, /// Next available asset ID number next_id: u32, /// Next available group ID number @@ -1202,43 +1199,21 @@ pub struct AssetPool { } impl AssetPool { - /// Create a new [`AssetPool`] - pub fn new(mut assets: Vec) -> Self { - // Sort in order of commission year - assets.sort_by(|a, b| a.commission_year.cmp(&b.commission_year)); - - Self { - active: Vec::new(), - future: assets, - decommissioned: Vec::new(), - next_id: 0, - next_group_id: 0, - } + /// Create a new empty [`AssetPool`] + pub fn new() -> Self { + Self::default() } /// Get the active pool as a slice of [`AssetRef`]s pub fn as_slice(&self) -> &[AssetRef] { - &self.active - } - - /// Decommission assets whose lifetime has passed, - /// and commission new assets - pub fn update_for_year(&mut self, year: u32) { - self.decommission_old(year); - self.commission_new(year); + &self.assets } /// Commission new assets for the specified milestone year from the input data - fn commission_new(&mut self, year: u32) { - // Count the number of assets to move - let count = self - .future - .iter() - .take_while(|asset| asset.commission_year <= year) - .count(); + pub fn commission_new(&mut self, year: u32, user_assets: &mut Vec) { + let to_commission = user_assets.extract_if(.., |asset| asset.commission_year <= year); - // Move assets from future to active - for mut asset in self.future.drain(0..count) { + for asset in to_commission { // Ignore assets that have already been decommissioned if asset.max_decommission_year() <= year { warn!( @@ -1251,59 +1226,73 @@ impl AssetPool { continue; } - // If it is divisible, we divide and commission all the children - if asset.is_divisible() { - for mut child in asset.divide_asset() { - child.make_mut().commission( - AssetID(self.next_id), - Some(AssetGroupID(self.next_group_id)), - "user input", - ); - self.next_id += 1; - self.active.push(child); - } - self.next_group_id += 1; - } - // If not, we just commission it as a single asset - else { - asset.commission(AssetID(self.next_id), None, "user input"); + self.commission(asset, "user input"); + } + } + + /// Commission the specified asset or, if divisible, its children + fn commission(&mut self, mut asset: AssetRef, reason: &str) { + // If it is divisible, we divide and commission all the children + if asset.is_divisible() { + for mut child in asset.divide_asset() { + child.make_mut().commission( + AssetID(self.next_id), + Some(AssetGroupID(self.next_group_id)), + reason, + ); self.next_id += 1; - self.active.push(asset.into()); + self.assets.push(child); } + self.next_group_id += 1; + } + // If not, we just commission it as a single asset + else { + asset + .make_mut() + .commission(AssetID(self.next_id), None, reason); + self.next_id += 1; + self.assets.push(asset); } } /// Decommission old assets for the specified milestone year - fn decommission_old(&mut self, year: u32) { - // Remove assets which are due for decommissioning + pub fn decommission_old>(&mut self, year: u32, decommissioned: &mut E) { let to_decommission = self - .active - .extract_if(.., |asset| asset.max_decommission_year() <= year); - - for mut asset in to_decommission { - // Set `decommission_year` and move to `self.decommissioned` - asset.make_mut().decommission(year, "end of life"); - self.decommissioned.push(asset); - } - } - - /// Decomission mothballed assets if mothballed long enough - pub fn decommission_mothballed(&mut self, year: u32, mothball_years: u32) { - // Remove assets which are due for decommissioning - let to_decommission = self.active.extract_if(.., |asset| { - asset.get_mothballed_year().is_some() - && asset.get_mothballed_year() <= Some(year - min(mothball_years, year)) - }); - - for mut asset in to_decommission { - // Set `decommission_year` and move to `self.decommissioned` - let decommissioned = asset.get_mothballed_year().unwrap() + mothball_years; - asset.make_mut().decommission( - decommissioned, - &format!("The asset has not been used for the set mothball years ({mothball_years} years)."), - ); - self.decommissioned.push(asset); - } + .assets + .extract_if(.., move |asset| asset.max_decommission_year() <= year) + .map(move |mut asset| { + asset.make_mut().decommission(year, "end of life"); + asset + }); + decommissioned.extend(to_decommission); + } + + /// Decommission mothballed assets if mothballed long enough + pub fn decommission_mothballed>( + &mut self, + year: u32, + mothball_years: u32, + decommissioned: &mut E, + ) { + let to_decommission = self + .assets + .extract_if(.., move |asset| { + asset + .get_mothballed_year() + .is_some_and(|myear| myear <= year - min(mothball_years, year)) + }) + .map(move |mut asset| { + let decommissioned = asset.get_mothballed_year().unwrap() + mothball_years; + asset.make_mut().decommission( + decommissioned, + &format!( + "The asset has not been used for the set mothball years ({mothball_years} \ + years)." + ), + ); + asset + }); + decommissioned.extend(to_decommission); } /// Mothball the specified assets if they are no longer in the active pool and put them back again. @@ -1322,7 +1311,7 @@ impl AssetPool { { for mut asset in assets { if match asset.state { - AssetState::Commissioned { .. } => !self.active.contains(&asset), + AssetState::Commissioned { .. } => !self.assets.contains(&asset), _ => panic!("Cannot mothball asset that has not been commissioned"), } { // If not already set, we set the current year as the mothball year, @@ -1333,10 +1322,10 @@ impl AssetPool { // And we put it back to the pool, so they can be chosen the next milestone year // if not decommissioned earlier. - self.active.push(asset); + self.assets.push(asset); } } - self.active.sort(); + self.assets.sort(); } /// Get an asset with the specified ID. @@ -1346,38 +1335,27 @@ impl AssetPool { /// An [`AssetRef`] if found, else `None`. The asset may not be found if it has already been /// decommissioned. pub fn get(&self, id: AssetID) -> Option<&AssetRef> { - // The assets in `active` are in order of ID + // Assets are sorted by ID let idx = self - .active + .assets .binary_search_by(|asset| match &asset.state { AssetState::Commissioned { id: asset_id, .. } => asset_id.cmp(&id), _ => panic!("Active pool should only contain commissioned assets"), }) .ok()?; - Some(&self.active[idx]) + Some(&self.assets[idx]) } /// Iterate over active assets - pub fn iter_active(&self) -> slice::Iter<'_, AssetRef> { - self.active.iter() - } - - /// Iterate over decommissioned assets - pub fn iter_decommissioned(&self) -> slice::Iter<'_, AssetRef> { - self.decommissioned.iter() - } - - /// Iterate over all commissioned and decommissioned assets. - /// - /// NB: Not-yet-commissioned assets are not included. - pub fn iter_all(&self) -> impl Iterator { - chain(self.iter_active(), self.iter_decommissioned()) + #[allow(clippy::iter_without_into_iter)] + pub fn iter(&self) -> slice::Iter<'_, AssetRef> { + self.assets.iter() } /// Return current active pool and clear pub fn take(&mut self) -> Vec { - std::mem::take(&mut self.active) + std::mem::take(&mut self.assets) } /// Extend the active pool with Commissioned or Selected assets @@ -1391,30 +1369,10 @@ impl AssetPool { match &asset.state { AssetState::Commissioned { .. } => { asset.make_mut().unmothball(); - self.active.push(asset); + self.assets.push(asset); } AssetState::Selected { .. } => { - // If it is divisible, we divide and commission all the children - if asset.is_divisible() { - for mut child in asset.divide_asset() { - child.make_mut().commission( - AssetID(self.next_id), - Some(AssetGroupID(self.next_group_id)), - "selected", - ); - self.next_id += 1; - self.active.push(child); - } - self.next_group_id += 1; - } - // If not, we just commission it as a single asset - else { - asset - .make_mut() - .commission(AssetID(self.next_id), None, "selected"); - self.next_id += 1; - self.active.push(asset); - } + self.commission(asset, "selected"); } _ => panic!( "Cannot extend asset pool with asset in state {}. Only assets in \ @@ -1424,11 +1382,11 @@ impl AssetPool { } } - // New assets may not have been sorted, but active needs to be sorted by ID - self.active.sort(); + // New assets may not have been sorted, but we need them sorted by ID + self.assets.sort(); // Sanity check: all assets should be unique - debug_assert_eq!(self.active.iter().unique().count(), self.active.len()); + debug_assert_eq!(self.assets.iter().unique().count(), self.assets.len()); } } @@ -1620,7 +1578,7 @@ mod tests { } #[fixture] - fn asset_pool(mut process: Process) -> AssetPool { + fn user_assets(mut process: Process) -> Vec { // Update process parameters (lifetime = 20 years) let process_param = ProcessParameter { capital_cost: MoneyPerCapacity(5.0), @@ -1633,7 +1591,7 @@ mod tests { process.parameters = process_parameter_map; let rc_process = Rc::new(process); - let future = [2020, 2010] + [2020, 2010] .map(|year| { Asset::new_future( "agent1".into(), @@ -1643,11 +1601,10 @@ mod tests { year, ) .unwrap() + .into() }) .into_iter() - .collect_vec(); - - AssetPool::new(future) + .collect_vec() } #[fixture] @@ -1755,122 +1712,131 @@ mod tests { } #[rstest] - fn asset_pool_new(asset_pool: AssetPool) { - // Should be in order of commission year - assert!(asset_pool.active.is_empty()); - assert!(asset_pool.future.len() == 2); - assert!(asset_pool.future[0].commission_year == 2010); - assert!(asset_pool.future[1].commission_year == 2020); + fn asset_pool_new() { + assert!(AssetPool::new().assets.is_empty()); } #[rstest] - fn asset_pool_commission_new1(mut asset_pool: AssetPool) { + fn asset_pool_commission_new1(mut user_assets: Vec) { // Asset to be commissioned in this year - asset_pool.commission_new(2010); - assert_equal(asset_pool.iter_active(), iter::once(&asset_pool.active[0])); + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2010, &mut user_assets); + assert_equal(asset_pool.iter(), iter::once(&asset_pool.assets[0])); } #[rstest] - fn asset_pool_commission_new2(mut asset_pool: AssetPool) { + fn asset_pool_commission_new2(mut user_assets: Vec) { // Commission year has passed - asset_pool.commission_new(2011); - assert_equal(asset_pool.iter_active(), iter::once(&asset_pool.active[0])); + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2011, &mut user_assets); + assert_equal(asset_pool.iter(), iter::once(&asset_pool.assets[0])); } #[rstest] - fn asset_pool_commission_new3(mut asset_pool: AssetPool) { + fn asset_pool_commission_new3(mut user_assets: Vec) { // Nothing to commission for this year - asset_pool.commission_new(2000); - assert!(asset_pool.iter_active().next().is_none()); // no active assets + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2000, &mut user_assets); + assert!(asset_pool.iter().next().is_none()); // no active assets } #[rstest] fn asset_pool_commission_new_divisible(asset_divisible: Asset) { let commision_year = asset_divisible.commission_year; let expected_children = expected_children_for_divisible(&asset_divisible); - let mut asset_pool = AssetPool::new(vec![asset_divisible.clone()]); - assert!(asset_pool.active.is_empty()); - asset_pool.commission_new(commision_year); - assert!(asset_pool.future.is_empty()); - assert!(!asset_pool.active.is_empty()); - assert_eq!(asset_pool.active.len(), expected_children); + let mut asset_pool = AssetPool::new(); + let mut user_assets = vec![asset_divisible.into()]; + assert!(asset_pool.assets.is_empty()); + asset_pool.commission_new(commision_year, &mut user_assets); + assert!(user_assets.is_empty()); + assert!(!asset_pool.assets.is_empty()); + assert_eq!(asset_pool.assets.len(), expected_children); assert_eq!(asset_pool.next_group_id, 1); } #[rstest] fn asset_pool_commission_already_decommissioned(asset: Asset) { let year = asset.max_decommission_year(); - let mut asset_pool = AssetPool::new(vec![asset]); - assert!(asset_pool.active.is_empty()); - asset_pool.update_for_year(year); - assert!(asset_pool.active.is_empty()); + let mut asset_pool = AssetPool::new(); + assert!(asset_pool.assets.is_empty()); + asset_pool.commission_new(year, &mut vec![asset.into()]); + assert!(asset_pool.assets.is_empty()); } #[rstest] - fn asset_pool_decommission_old(mut asset_pool: AssetPool) { - asset_pool.commission_new(2020); - assert!(asset_pool.future.is_empty()); - assert_eq!(asset_pool.active.len(), 2); - asset_pool.decommission_old(2030); // should decommission first asset (lifetime == 5) - assert_eq!(asset_pool.active.len(), 1); - assert_eq!(asset_pool.active[0].commission_year, 2020); - assert_eq!(asset_pool.decommissioned.len(), 1); - assert_eq!(asset_pool.decommissioned[0].commission_year, 2010); - assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2030)); - asset_pool.decommission_old(2032); // nothing to decommission - assert_eq!(asset_pool.active.len(), 1); - assert_eq!(asset_pool.active[0].commission_year, 2020); - assert_eq!(asset_pool.decommissioned.len(), 1); - assert_eq!(asset_pool.decommissioned[0].commission_year, 2010); - assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2030)); - asset_pool.decommission_old(2040); // should decommission second asset - assert!(asset_pool.active.is_empty()); - assert_eq!(asset_pool.decommissioned.len(), 2); - assert_eq!(asset_pool.decommissioned[0].commission_year, 2010); - assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2030)); - assert_eq!(asset_pool.decommissioned[1].commission_year, 2020); - assert_eq!(asset_pool.decommissioned[1].decommission_year(), Some(2040)); + fn asset_pool_decommission_old(mut user_assets: Vec) { + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2020, &mut user_assets); + assert!(user_assets.is_empty()); + assert_eq!(asset_pool.assets.len(), 2); + let mut decommissioned = Vec::new(); + + // should decommission first asset (lifetime == 5) + asset_pool.decommission_old(2030, &mut decommissioned); + assert_eq!(asset_pool.assets.len(), 1); + assert_eq!(asset_pool.assets[0].commission_year, 2020); + assert_eq!(decommissioned.len(), 1); + assert_eq!(decommissioned[0].commission_year, 2010); + assert_eq!(decommissioned[0].decommission_year(), Some(2030)); + + // nothing to decommission + decommissioned.clear(); + asset_pool.decommission_old(2032, &mut decommissioned); + assert_eq!(asset_pool.assets.len(), 1); + assert_eq!(asset_pool.assets[0].commission_year, 2020); + + // should decommission second asset + decommissioned.clear(); + asset_pool.decommission_old(2040, &mut decommissioned); + assert!(asset_pool.assets.is_empty()); + assert_eq!(decommissioned.len(), 1); + assert_eq!(decommissioned[0].commission_year, 2020); + assert_eq!(decommissioned[0].decommission_year(), Some(2040)); } #[rstest] - fn asset_pool_get(mut asset_pool: AssetPool) { - asset_pool.commission_new(2020); - assert_eq!(asset_pool.get(AssetID(0)), Some(&asset_pool.active[0])); - assert_eq!(asset_pool.get(AssetID(1)), Some(&asset_pool.active[1])); + fn asset_pool_get(mut user_assets: Vec) { + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2020, &mut user_assets); + assert_eq!(asset_pool.get(AssetID(0)), Some(&asset_pool.assets[0])); + assert_eq!(asset_pool.get(AssetID(1)), Some(&asset_pool.assets[1])); } #[rstest] - fn asset_pool_extend_empty(mut asset_pool: AssetPool) { + fn asset_pool_extend_empty(mut user_assets: Vec) { // Start with commissioned assets - asset_pool.commission_new(2020); - let original_count = asset_pool.active.len(); + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2020, &mut user_assets); + let original_count = asset_pool.assets.len(); // Extend with empty iterator asset_pool.extend(Vec::::new()); - assert_eq!(asset_pool.active.len(), original_count); + assert_eq!(asset_pool.assets.len(), original_count); } #[rstest] - fn asset_pool_extend_existing_assets(mut asset_pool: AssetPool) { + fn asset_pool_extend_existing_assets(mut user_assets: Vec) { // Start with some commissioned assets - asset_pool.commission_new(2020); - assert_eq!(asset_pool.active.len(), 2); + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2020, &mut user_assets); + assert_eq!(asset_pool.assets.len(), 2); let existing_assets = asset_pool.take(); // Extend with the same assets (should maintain their IDs) asset_pool.extend(existing_assets.clone()); - assert_eq!(asset_pool.active.len(), 2); - assert_eq!(asset_pool.active[0].id(), Some(AssetID(0))); - assert_eq!(asset_pool.active[1].id(), Some(AssetID(1))); + assert_eq!(asset_pool.assets.len(), 2); + assert_eq!(asset_pool.assets[0].id(), Some(AssetID(0))); + assert_eq!(asset_pool.assets[1].id(), Some(AssetID(1))); } #[rstest] - fn asset_pool_extend_new_assets(mut asset_pool: AssetPool, process: Process) { + fn asset_pool_extend_new_assets(mut user_assets: Vec, process: Process) { // Start with some commissioned assets - asset_pool.commission_new(2020); - let original_count = asset_pool.active.len(); + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2020, &mut user_assets); + let original_count = asset_pool.assets.len(); // Create new non-commissioned assets let process_rc = Rc::new(process); @@ -1897,25 +1863,29 @@ mod tests { asset_pool.extend(new_assets); - assert_eq!(asset_pool.active.len(), original_count + 2); + assert_eq!(asset_pool.assets.len(), original_count + 2); // New assets should get IDs 2 and 3 - assert_eq!(asset_pool.active[original_count].id(), Some(AssetID(2))); - assert_eq!(asset_pool.active[original_count + 1].id(), Some(AssetID(3))); + assert_eq!(asset_pool.assets[original_count].id(), Some(AssetID(2))); + assert_eq!(asset_pool.assets[original_count + 1].id(), Some(AssetID(3))); assert_eq!( - asset_pool.active[original_count].agent_id(), + asset_pool.assets[original_count].agent_id(), Some(&"agent2".into()) ); assert_eq!( - asset_pool.active[original_count + 1].agent_id(), + asset_pool.assets[original_count + 1].agent_id(), Some(&"agent3".into()) ); } #[rstest] - fn asset_pool_extend_new_divisible_assets(mut asset_pool: AssetPool, mut process: Process) { + fn asset_pool_extend_new_divisible_assets( + mut user_assets: Vec, + mut process: Process, + ) { // Start with some commissioned assets - asset_pool.commission_new(2020); - let original_count = asset_pool.active.len(); + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2020, &mut user_assets); + let original_count = asset_pool.assets.len(); // Create new non-commissioned assets process.unit_size = Some(Capacity(4.0)); @@ -1933,13 +1903,14 @@ mod tests { ]; let expected_children = expected_children_for_divisible(&new_assets[0]); asset_pool.extend(new_assets); - assert_eq!(asset_pool.active.len(), original_count + expected_children); + assert_eq!(asset_pool.assets.len(), original_count + expected_children); } #[rstest] - fn asset_pool_extend_mixed_assets(mut asset_pool: AssetPool, process: Process) { + fn asset_pool_extend_mixed_assets(mut user_assets: Vec, process: Process) { // Start with some commissioned assets - asset_pool.commission_new(2020); + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2020, &mut user_assets); // Create a new non-commissioned asset let new_asset = Asset::new_selected( @@ -1955,24 +1926,25 @@ mod tests { // Extend with just the new asset (not mixing with existing to avoid duplicates) asset_pool.extend(vec![new_asset]); - assert_eq!(asset_pool.active.len(), 3); + assert_eq!(asset_pool.assets.len(), 3); // Check that we have the original assets plus the new one - assert!(asset_pool.active.iter().any(|a| a.id() == Some(AssetID(0)))); - assert!(asset_pool.active.iter().any(|a| a.id() == Some(AssetID(1)))); - assert!(asset_pool.active.iter().any(|a| a.id() == Some(AssetID(2)))); + assert!(asset_pool.assets.iter().any(|a| a.id() == Some(AssetID(0)))); + assert!(asset_pool.assets.iter().any(|a| a.id() == Some(AssetID(1)))); + assert!(asset_pool.assets.iter().any(|a| a.id() == Some(AssetID(2)))); // Check that the new asset has the correct agent assert!( asset_pool - .active + .assets .iter() .any(|a| a.agent_id() == Some(&"agent_new".into())) ); } #[rstest] - fn asset_pool_extend_maintains_sort_order(mut asset_pool: AssetPool, process: Process) { + fn asset_pool_extend_maintains_sort_order(mut user_assets: Vec, process: Process) { // Start with some commissioned assets - asset_pool.commission_new(2020); + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2020, &mut user_assets); // Create new assets that would be out of order if added at the end let process_rc = Rc::new(process); @@ -2000,35 +1972,34 @@ mod tests { asset_pool.extend(new_assets); // Check that assets are sorted by ID - let ids: Vec = asset_pool - .iter_active() - .map(|a| a.id().unwrap().0) - .collect(); + let ids: Vec = asset_pool.iter().map(|a| a.id().unwrap().0).collect(); assert_equal(ids, 0..4); } #[rstest] - fn asset_pool_extend_no_duplicates_expected(mut asset_pool: AssetPool) { + fn asset_pool_extend_no_duplicates_expected(mut user_assets: Vec) { // Start with some commissioned assets - asset_pool.commission_new(2020); - let original_count = asset_pool.active.len(); + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2020, &mut user_assets); + let original_count = asset_pool.assets.len(); // The extend method expects unique assets - adding duplicates would violate // the debug assertion, so this test verifies the normal case asset_pool.extend(Vec::new()); - assert_eq!(asset_pool.active.len(), original_count); + assert_eq!(asset_pool.assets.len(), original_count); // Verify all assets are still unique (this is what the debug_assert checks) assert_eq!( - asset_pool.active.iter().unique().count(), - asset_pool.active.len() + asset_pool.assets.iter().unique().count(), + asset_pool.assets.len() ); } #[rstest] - fn asset_pool_extend_increments_next_id(mut asset_pool: AssetPool, process: Process) { + fn asset_pool_extend_increments_next_id(mut user_assets: Vec, process: Process) { // Start with some commissioned assets - asset_pool.commission_new(2020); + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2020, &mut user_assets); assert_eq!(asset_pool.next_id, 2); // Should be 2 after commissioning 2 assets // Create new non-commissioned assets @@ -2058,82 +2029,82 @@ mod tests { // next_id should have incremented for each new asset assert_eq!(asset_pool.next_id, 4); - assert_eq!(asset_pool.active[2].id(), Some(AssetID(2))); - assert_eq!(asset_pool.active[3].id(), Some(AssetID(3))); + assert_eq!(asset_pool.assets[2].id(), Some(AssetID(2))); + assert_eq!(asset_pool.assets[3].id(), Some(AssetID(3))); } #[rstest] - fn asset_pool_mothball_unretained(mut asset_pool: AssetPool) { + fn asset_pool_mothball_unretained(mut user_assets: Vec) { // Commission some assets - asset_pool.commission_new(2020); - assert_eq!(asset_pool.active.len(), 2); + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2020, &mut user_assets); + assert_eq!(asset_pool.assets.len(), 2); // Remove one asset from the active pool (simulating it being removed elsewhere) - let removed_asset = asset_pool.active.remove(0); - assert_eq!(asset_pool.active.len(), 1); + let removed_asset = asset_pool.assets.remove(0); + assert_eq!(asset_pool.assets.len(), 1); // Try to mothball both the removed asset (not in active) and an active asset - let assets_to_check = vec![removed_asset.clone(), asset_pool.active[0].clone()]; + let assets_to_check = vec![removed_asset.clone(), asset_pool.assets[0].clone()]; asset_pool.mothball_unretained(assets_to_check, 2025); // Only the removed asset should be mothballed (since it's not in active pool) - assert_eq!(asset_pool.active.len(), 2); // And should be back into the pool - assert_eq!(asset_pool.active[0].get_mothballed_year(), Some(2025)); + assert_eq!(asset_pool.assets.len(), 2); // And should be back into the pool + assert_eq!(asset_pool.assets[0].get_mothballed_year(), Some(2025)); } #[rstest] - fn asset_pool_decommission_unused(mut asset_pool: AssetPool) { + fn asset_pool_decommission_unused(mut user_assets: Vec) { // Commission some assets - asset_pool.commission_new(2020); - assert_eq!(asset_pool.active.len(), 2); - assert_eq!(asset_pool.decommissioned.len(), 0); + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2020, &mut user_assets); + assert_eq!(asset_pool.assets.len(), 2); // Make an asset unused for a few years let mothball_years: u32 = 10; - asset_pool.active[0] + asset_pool.assets[0] .make_mut() .mothball(2025 - mothball_years); assert_eq!( - asset_pool.active[0].get_mothballed_year(), + asset_pool.assets[0].get_mothballed_year(), Some(2025 - mothball_years) ); // Decomission unused assets - asset_pool.decommission_mothballed(2025, mothball_years); + let mut decommissioned = Vec::new(); + asset_pool.decommission_mothballed(2025, mothball_years, &mut decommissioned); // Only the removed asset should be decommissioned (since it's not in active pool) - assert_eq!(asset_pool.active.len(), 1); // Active pool unchanged - assert_eq!(asset_pool.decommissioned.len(), 1); - assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2025)); + assert_eq!(asset_pool.assets.len(), 1); // Active pool unchanged + assert_eq!(decommissioned.len(), 1); + assert_eq!(decommissioned[0].decommission_year(), Some(2025)); } #[rstest] - fn asset_pool_decommission_if_not_active_none_active(mut asset_pool: AssetPool) { + fn asset_pool_decommission_if_not_active_none_active(mut user_assets: Vec) { // Commission some assets - asset_pool.commission_new(2020); - let all_assets = asset_pool.active.clone(); + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2020, &mut user_assets); + let all_assets = asset_pool.assets.clone(); // Clear the active pool (simulating all assets being removed) - asset_pool.active.clear(); + asset_pool.assets.clear(); // Try to mothball the assets that are no longer active asset_pool.mothball_unretained(all_assets.clone(), 2025); // All assets should be mothballed - assert_eq!(asset_pool.active.len(), 2); - assert_eq!(asset_pool.active[0].id(), all_assets[0].id()); - assert_eq!(asset_pool.active[0].get_mothballed_year(), Some(2025)); - assert_eq!(asset_pool.active[1].id(), all_assets[1].id()); - assert_eq!(asset_pool.active[1].get_mothballed_year(), Some(2025)); + assert_eq!(asset_pool.assets.len(), 2); + assert_eq!(asset_pool.assets[0].id(), all_assets[0].id()); + assert_eq!(asset_pool.assets[0].get_mothballed_year(), Some(2025)); + assert_eq!(asset_pool.assets[1].id(), all_assets[1].id()); + assert_eq!(asset_pool.assets[1].get_mothballed_year(), Some(2025)); } #[rstest] #[should_panic(expected = "Cannot mothball asset that has not been commissioned")] - fn asset_pool_decommission_if_not_active_non_commissioned_asset( - mut asset_pool: AssetPool, - process: Process, - ) { + fn asset_pool_decommission_if_not_active_non_commissioned_asset(process: Process) { // Create a non-commissioned asset let non_commissioned_asset = Asset::new_future( "agent_new".into(), @@ -2146,6 +2117,7 @@ mod tests { .into(); // This should panic because the asset was never commissioned + let mut asset_pool = AssetPool::new(); asset_pool.mothball_unretained(vec![non_commissioned_asset], 2025); } diff --git a/src/cli.rs b/src/cli.rs index a5e30b744..f63d637cf 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -158,7 +158,7 @@ pub fn handle_run_command(model_path: &Path, opts: &RunOpts) -> Result<()> { info!("Starting MUSE2 v{}", env!("CARGO_PKG_VERSION")); // Load the model to run - let (model, assets) = load_model(model_path).context("Failed to load model.")?; + let model = load_model(model_path).context("Failed to load model.")?; info!("Loaded model from {}", model_path.display()); info!("Output folder: {}", output_path.display()); @@ -168,7 +168,7 @@ pub fn handle_run_command(model_path: &Path, opts: &RunOpts) -> Result<()> { } // Run the simulation - crate::simulation::run(&model, assets, output_path, settings.debug_model)?; + crate::simulation::run(&model, output_path, settings.debug_model)?; info!("Simulation complete!"); Ok(()) diff --git a/src/fixture.rs b/src/fixture.rs index aa43feb6d..120f6e092 100644 --- a/src/fixture.rs +++ b/src/fixture.rs @@ -95,11 +95,11 @@ macro_rules! patch_and_run_simple { ($file_patches:expr) => {{ (|| -> Result<()> { let tmp = crate::fixture::build_patched_simple_tempdir($file_patches); - let (model, assets) = crate::input::load_model(tmp.path())?; + let model = crate::input::load_model(tmp.path())?; let output_path = tmp.path().join("output"); std::fs::create_dir_all(&output_path)?; - crate::simulation::run(&model, assets, &output_path, false)?; + crate::simulation::run(&model, &output_path, false)?; Ok(()) })() }}; @@ -203,8 +203,8 @@ pub fn asset(process: Process) -> Asset { #[fixture] pub fn assets(asset: Asset) -> AssetPool { let year = asset.commission_year(); - let mut assets = AssetPool::new(iter::once(asset).collect()); - assets.update_for_year(year); + let mut assets = AssetPool::new(); + assets.commission_new(year, &mut vec![asset.into()]); assets } diff --git a/src/input.rs b/src/input.rs index 70ae2ef5f..7cdfcca23 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,5 +1,4 @@ //! Common routines for handling input data. -use crate::asset::AssetPool; use crate::graph::investment::solve_investment_order_for_model; use crate::graph::validate::validate_commodity_graphs_for_model; use crate::graph::{CommoditiesGraph, build_commodity_graphs_for_model}; @@ -21,7 +20,7 @@ use std::path::Path; mod agent; use agent::read_agents; mod asset; -use asset::read_assets; +use asset::read_user_assets; mod commodity; use commodity::read_commodities; mod process; @@ -227,8 +226,8 @@ where /// /// # Returns /// -/// The static model data ([`Model`]) and an [`AssetPool`] struct or an error. -pub fn load_model>(model_dir: P) -> Result<(Model, AssetPool)> { +/// The static model data ([`Model`]) or an error. +pub fn load_model>(model_dir: P) -> Result { let model_params = ModelParameters::from_path(&model_dir)?; let time_slice_info = read_time_slice_info(model_dir.as_ref())?; @@ -252,7 +251,7 @@ pub fn load_model>(model_dir: P) -> Result<(Model, AssetPool)> { years, )?; let agent_ids = agents.keys().cloned().collect(); - let assets = read_assets(model_dir.as_ref(), &agent_ids, &processes, ®ion_ids)?; + let user_assets = read_user_assets(model_dir.as_ref(), &agent_ids, &processes, ®ion_ids)?; // Build and validate commodity graphs for all regions and years let commodity_graphs = build_commodity_graphs_for_model(&processes, ®ion_ids, years); @@ -278,9 +277,10 @@ pub fn load_model>(model_dir: P) -> Result<(Model, AssetPool)> { processes, time_slice_info, regions, + user_assets, investment_order, }; - Ok((model, AssetPool::new(assets))) + Ok(model) } /// Load commodity flow graphs for a model. diff --git a/src/input/asset.rs b/src/input/asset.rs index 451b706a1..8f219b3f6 100644 --- a/src/input/asset.rs +++ b/src/input/asset.rs @@ -1,7 +1,7 @@ //! Code for reading [`Asset`]s from a CSV file. use super::{input_err_msg, read_csv_optional}; use crate::agent::AgentID; -use crate::asset::Asset; +use crate::asset::{Asset, AssetRef}; use crate::id::IDCollection; use crate::process::ProcessMap; use crate::region::RegionID; @@ -39,13 +39,13 @@ struct AssetRaw { /// /// # Returns /// -/// A `Vec` of [`Asset`]s or an error. -pub fn read_assets( +/// A `Vec` of [`AssetRef`]s or an error. +pub fn read_user_assets( model_dir: &Path, agent_ids: &IndexSet, processes: &ProcessMap, region_ids: &IndexSet, -) -> Result> { +) -> Result> { let file_path = model_dir.join(ASSETS_FILE_NAME); let assets_csv = read_csv_optional(&file_path)?; read_assets_from_iter(assets_csv, agent_ids, processes, region_ids) @@ -69,7 +69,7 @@ fn read_assets_from_iter( agent_ids: &IndexSet, processes: &ProcessMap, region_ids: &IndexSet, -) -> Result> +) -> Result> where I: Iterator, { @@ -123,14 +123,15 @@ where } } - Asset::new_future_with_max_decommission( + let asset = Asset::new_future_with_max_decommission( agent_id.clone(), Rc::clone(process), region_id.clone(), asset.capacity, asset.commission_year, asset.max_decommission_year, - ) + )?; + Ok(asset.into()) }) .try_collect() } @@ -174,7 +175,8 @@ mod tests { 2010, max_decommission_year, ) - .unwrap(); + .unwrap() + .into(); assert_equal( read_assets_from_iter(iter::once(asset_in), &agent_ids, &processes, ®ion_ids) .unwrap(), diff --git a/src/model.rs b/src/model.rs index bbc9afa28..789da3c89 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,5 +1,6 @@ //! The model represents the static input data provided by the user. use crate::agent::AgentMap; +use crate::asset::AssetRef; use crate::commodity::{CommodityID, CommodityMap}; use crate::process::ProcessMap; use crate::region::{Region, RegionID, RegionMap}; @@ -27,6 +28,8 @@ pub struct Model { pub time_slice_info: TimeSliceInfo, /// Regions for the simulation pub regions: RegionMap, + /// User-defined assets + pub user_assets: Vec, /// Commodity ordering for each milestone year pub investment_order: HashMap>, } diff --git a/src/output.rs b/src/output.rs index b31a3fe64..a60338711 100644 --- a/src/output.rs +++ b/src/output.rs @@ -680,12 +680,12 @@ mod tests { // Write an asset { let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap(); - writer.write_assets(assets.iter_active()).unwrap(); + writer.write_assets(assets.iter()).unwrap(); writer.flush().unwrap(); } // Read back and compare - let asset = assets.iter_active().next().unwrap(); + let asset = assets.iter().next().unwrap(); let expected = AssetRow::new(asset); let records: Vec = csv::Reader::from_path(dir.path().join(ASSETS_FILE_NAME)) .unwrap() @@ -698,7 +698,7 @@ mod tests { #[rstest] fn write_flows(assets: AssetPool, commodity_id: CommodityID, time_slice: TimeSliceID) { let milestone_year = 2020; - let asset = assets.iter_active().next().unwrap(); + let asset = assets.iter().next().unwrap(); let flow_map = indexmap! { (asset.clone(), commodity_id.clone(), time_slice.clone()) => Flow(42.0) }; @@ -849,7 +849,7 @@ mod tests { let activity_dual = MoneyPerActivity(-1.5); let column_dual = MoneyPerActivity(5.0); let dir = tempdir().unwrap(); - let asset = assets.iter_active().next().unwrap(); + let asset = assets.iter().next().unwrap(); // Write activity { @@ -893,7 +893,7 @@ mod tests { let run_description = "test_run".to_string(); let activity = Activity(100.5); let dir = tempdir().unwrap(); - let asset = assets.iter_active().next().unwrap(); + let asset = assets.iter().next().unwrap(); // Write activity { diff --git a/src/simulation.rs b/src/simulation.rs index 458d3a674..20d4d70c5 100644 --- a/src/simulation.rs +++ b/src/simulation.rs @@ -22,16 +22,13 @@ pub use prices::CommodityPrices; /// # Arguments: /// /// * `model` - The model to run -/// * `assets` - The asset pool /// * `output_path` - The folder to which output files will be written /// * `debug_model` - Whether to write additional information (e.g. duals) to output files -pub fn run( - model: &Model, - mut assets: AssetPool, - output_path: &Path, - debug_model: bool, -) -> Result<()> { +pub fn run(model: &Model, output_path: &Path, debug_model: bool) -> Result<()> { let mut writer = DataWriter::create(output_path, &model.model_path, debug_model)?; + let mut user_assets = model.user_assets.clone(); + let mut asset_pool = AssetPool::new(); // active assets + let mut decommissioned = Vec::new(); // Iterate over milestone years let mut year_iter = model.iter_years().peekable(); @@ -40,10 +37,10 @@ pub fn run( info!("Milestone year: {year}"); // Commission assets for base year - assets.update_for_year(year); + asset_pool.commission_new(year, &mut user_assets); // Write assets to file - writer.write_assets(assets.iter_all())?; + writer.write_assets(asset_pool.iter())?; // Gather candidates for the next year, if any let next_year = year_iter.peek().copied(); @@ -56,7 +53,7 @@ pub fn run( // Run dispatch optimisation info!("Running dispatch optimisation..."); let (flow_map, mut prices) = - run_dispatch_for_year(model, assets.as_slice(), &candidates, year, &mut writer)?; + run_dispatch_for_year(model, asset_pool.as_slice(), &candidates, year, &mut writer)?; // Write results of dispatch optimisation to file writer.write_flows(year, &flow_map)?; @@ -65,13 +62,14 @@ pub fn run( while let Some(year) = year_iter.next() { info!("Milestone year: {year}"); - // Commission new assets and decommission those whose lifetime has passed. We do this - // *before* agent investment, to prevent agents from selecting assets that are being - // decommissioned in this milestone year. - assets.update_for_year(year); + // Decommission assets whose lifetime has passed + asset_pool.decommission_old(year, &mut decommissioned); + + // Commission user-defined assets for this year + asset_pool.commission_new(year, &mut user_assets); // Take all the active assets as a list of existing assets - let existing_assets = assets.take(); + let existing_assets = asset_pool.take(); // Iterative loop to "iron out" prices via repeated investment and dispatch let mut ironing_out_iter = 0; @@ -121,14 +119,18 @@ pub fn run( }; // Add selected_assets to the active pool - assets.extend(selected_assets); + asset_pool.extend(selected_assets); // Decommission unused assets - assets.mothball_unretained(existing_assets, year); - assets.decommission_mothballed(year, model.parameters.mothball_years); + asset_pool.mothball_unretained(existing_assets, year); + asset_pool.decommission_mothballed( + year, + model.parameters.mothball_years, + &mut decommissioned, + ); // Write assets - writer.write_assets(assets.iter_all())?; + writer.write_assets(decommissioned.iter().chain(asset_pool.iter()))?; // Gather candidates for the next year, if any let next_year = year_iter.peek().copied(); @@ -141,7 +143,7 @@ pub fn run( // Run dispatch optimisation info!("Running final dispatch optimisation for year {year}..."); let (flow_map, new_prices) = - run_dispatch_for_year(model, assets.as_slice(), &candidates, year, &mut writer)?; + run_dispatch_for_year(model, asset_pool.as_slice(), &candidates, year, &mut writer)?; // Write results of dispatch optimisation to file writer.write_flows(year, &flow_map)?;