From 22757950c85b0dfed5615e64fad9362b2cf4fd77 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Tue, 3 Feb 2026 11:16:35 +0000 Subject: [PATCH 01/12] Revert "AssetPool: Define `update_for_year` method" This reverts commit 637b465d481f2402aa27fedd556fc554539c0a95. --- src/asset.rs | 14 ++++---------- src/fixture.rs | 2 +- src/simulation.rs | 16 +++++++++++----- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index f7fe42873..9e741efaa 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -1221,15 +1221,8 @@ impl AssetPool { &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); - } - /// Commission new assets for the specified milestone year from the input data - fn commission_new(&mut self, year: u32) { + pub fn commission_new(&mut self, year: u32) { // Count the number of assets to move let count = self .future @@ -1274,7 +1267,7 @@ impl AssetPool { } /// Decommission old assets for the specified milestone year - fn decommission_old(&mut self, year: u32) { + pub fn decommission_old(&mut self, year: u32) { // Remove assets which are due for decommissioning let to_decommission = self .active @@ -1802,7 +1795,8 @@ mod tests { 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); + asset_pool.decommission_old(year); + asset_pool.commission_new(year); assert!(asset_pool.active.is_empty()); } diff --git a/src/fixture.rs b/src/fixture.rs index aa43feb6d..2ec0a6935 100644 --- a/src/fixture.rs +++ b/src/fixture.rs @@ -204,7 +204,7 @@ pub fn asset(process: Process) -> Asset { 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); + assets.commission_new(year); assets } diff --git a/src/simulation.rs b/src/simulation.rs index 458d3a674..39e6b9775 100644 --- a/src/simulation.rs +++ b/src/simulation.rs @@ -39,8 +39,11 @@ pub fn run( info!("Milestone year: {year}"); + // There shouldn't be assets already commissioned, but let's do this just in case + assets.decommission_old(year); + // Commission assets for base year - assets.update_for_year(year); + assets.commission_new(year); // Write assets to file writer.write_assets(assets.iter_all())?; @@ -65,10 +68,13 @@ 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. We do this *before* agent investment, to + // prevent agents from selecting assets that are being decommissioned in this milestone + // year. + assets.decommission_old(year); + + // Commission pre-defined assets for this year + assets.commission_new(year); // Take all the active assets as a list of existing assets let existing_assets = assets.take(); From c57ef89bef1284fa19e304a47f3b18f924c755b6 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Tue, 3 Feb 2026 11:12:15 +0000 Subject: [PATCH 02/12] Extract future assets from `AssetPool` --- src/asset.rs | 144 ++++++++++++++++++++++------------------------ src/cli.rs | 4 +- src/fixture.rs | 4 +- src/input.rs | 10 ++-- src/simulation.rs | 15 +++-- 5 files changed, 85 insertions(+), 92 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index 9e741efaa..5bfa5eb5f 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -1188,11 +1188,10 @@ impl Ord for AssetRef { } /// A 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, /// Next available asset ID number @@ -1202,18 +1201,9 @@ 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 @@ -1222,16 +1212,11 @@ impl AssetPool { } /// Commission new assets for the specified milestone year from the input data - pub 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 mut asset in to_commission { // Ignore assets that have already been decommissioned if asset.max_decommission_year() <= year { warn!( @@ -1613,7 +1598,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), @@ -1626,7 +1611,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(), @@ -1638,9 +1623,7 @@ mod tests { .unwrap() }) .into_iter() - .collect_vec(); - - AssetPool::new(future) + .collect_vec() } #[fixture] @@ -1748,32 +1731,31 @@ 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().active.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); + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2010, &mut user_assets); assert_equal(asset_pool.iter_active(), iter::once(&asset_pool.active[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); + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2011, &mut user_assets); assert_equal(asset_pool.iter_active(), iter::once(&asset_pool.active[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); + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2000, &mut user_assets); assert!(asset_pool.iter_active().next().is_none()); // no active assets } @@ -1781,10 +1763,11 @@ mod tests { 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()]); + let mut asset_pool = AssetPool::new(); + let mut user_assets = vec![asset_divisible.clone()]; assert!(asset_pool.active.is_empty()); - asset_pool.commission_new(commision_year); - assert!(asset_pool.future.is_empty()); + asset_pool.commission_new(commision_year, &mut user_assets); + assert!(user_assets.is_empty()); assert!(!asset_pool.active.is_empty()); assert_eq!(asset_pool.active.len(), expected_children); assert_eq!(asset_pool.next_group_id, 1); @@ -1793,17 +1776,18 @@ mod tests { #[rstest] fn asset_pool_commission_already_decommissioned(asset: Asset) { let year = asset.max_decommission_year(); - let mut asset_pool = AssetPool::new(vec![asset]); + let mut asset_pool = AssetPool::new(); assert!(asset_pool.active.is_empty()); asset_pool.decommission_old(year); - asset_pool.commission_new(year); + asset_pool.commission_new(year, &mut vec![asset]); assert!(asset_pool.active.is_empty()); } #[rstest] - fn asset_pool_decommission_old(mut asset_pool: AssetPool) { - asset_pool.commission_new(2020); - assert!(asset_pool.future.is_empty()); + 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.active.len(), 2); asset_pool.decommission_old(2030); // should decommission first asset (lifetime == 5) assert_eq!(asset_pool.active.len(), 1); @@ -1827,16 +1811,18 @@ mod tests { } #[rstest] - fn asset_pool_get(mut asset_pool: AssetPool) { - asset_pool.commission_new(2020); + 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.active[0])); assert_eq!(asset_pool.get(AssetID(1)), Some(&asset_pool.active[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 mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2020, &mut user_assets); let original_count = asset_pool.active.len(); // Extend with empty iterator @@ -1846,9 +1832,10 @@ mod tests { } #[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); + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2020, &mut user_assets); assert_eq!(asset_pool.active.len(), 2); let existing_assets = asset_pool.take(); @@ -1861,9 +1848,10 @@ mod tests { } #[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 mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2020, &mut user_assets); let original_count = asset_pool.active.len(); // Create new non-commissioned assets @@ -1906,9 +1894,10 @@ mod tests { } #[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 mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2020, &mut user_assets); let original_count = asset_pool.active.len(); // Create new non-commissioned assets @@ -1931,9 +1920,10 @@ mod tests { } #[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( @@ -1964,9 +1954,10 @@ mod tests { } #[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); @@ -2002,9 +1993,10 @@ mod tests { } #[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 mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2020, &mut user_assets); let original_count = asset_pool.active.len(); // The extend method expects unique assets - adding duplicates would violate @@ -2020,9 +2012,10 @@ mod tests { } #[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 @@ -2057,9 +2050,10 @@ mod tests { } #[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); + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2020, &mut user_assets); assert_eq!(asset_pool.active.len(), 2); // Remove one asset from the active pool (simulating it being removed elsewhere) @@ -2076,9 +2070,10 @@ mod tests { } #[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); + let mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2020, &mut user_assets); assert_eq!(asset_pool.active.len(), 2); assert_eq!(asset_pool.decommissioned.len(), 0); @@ -2103,9 +2098,10 @@ mod tests { } #[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 mut asset_pool = AssetPool::new(); + asset_pool.commission_new(2020, &mut user_assets); let all_assets = asset_pool.active.clone(); // Clear the active pool (simulating all assets being removed) @@ -2124,10 +2120,7 @@ mod tests { #[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(), @@ -2140,6 +2133,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..2a2f079c1 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, user_assets) = 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, user_assets, output_path, settings.debug_model)?; info!("Simulation complete!"); Ok(()) diff --git a/src/fixture.rs b/src/fixture.rs index 2ec0a6935..35ae1c9e5 100644 --- a/src/fixture.rs +++ b/src/fixture.rs @@ -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.commission_new(year); + let mut assets = AssetPool::new(); + assets.commission_new(year, &mut vec![asset]); assets } diff --git a/src/input.rs b/src/input.rs index 70ae2ef5f..68edb3010 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,5 +1,5 @@ //! Common routines for handling input data. -use crate::asset::AssetPool; +use crate::asset::Asset; 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}; @@ -227,8 +227,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`]) and user-defined assets or an error. +pub fn load_model>(model_dir: P) -> Result<(Model, Vec)> { let model_params = ModelParameters::from_path(&model_dir)?; let time_slice_info = read_time_slice_info(model_dir.as_ref())?; @@ -252,7 +252,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_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); @@ -280,7 +280,7 @@ pub fn load_model>(model_dir: P) -> Result<(Model, AssetPool)> { regions, investment_order, }; - Ok((model, AssetPool::new(assets))) + Ok((model, user_assets)) } /// Load commodity flow graphs for a model. diff --git a/src/simulation.rs b/src/simulation.rs index 39e6b9775..38ab12ef9 100644 --- a/src/simulation.rs +++ b/src/simulation.rs @@ -22,16 +22,17 @@ pub use prices::CommodityPrices; /// # Arguments: /// /// * `model` - The model to run -/// * `assets` - The asset pool +/// * `user_assets` - Assets supplied by user /// * `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, + mut user_assets: Vec, output_path: &Path, debug_model: bool, ) -> Result<()> { let mut writer = DataWriter::create(output_path, &model.model_path, debug_model)?; + let mut assets = AssetPool::new(); // Iterate over milestone years let mut year_iter = model.iter_years().peekable(); @@ -43,7 +44,7 @@ pub fn run( assets.decommission_old(year); // Commission assets for base year - assets.commission_new(year); + assets.commission_new(year, &mut user_assets); // Write assets to file writer.write_assets(assets.iter_all())?; @@ -68,13 +69,11 @@ pub fn run( while let Some(year) = year_iter.next() { info!("Milestone year: {year}"); - // Decommission assets whose lifetime has passed. We do this *before* agent investment, to - // prevent agents from selecting assets that are being decommissioned in this milestone - // year. + // Decommission assets whose lifetime has passed assets.decommission_old(year); - // Commission pre-defined assets for this year - assets.commission_new(year); + // Commission user-defined assets for this year + assets.commission_new(year, &mut user_assets); // Take all the active assets as a list of existing assets let existing_assets = assets.take(); From 22d69908d5234629ee27de7ca63248476c47042d Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Tue, 3 Feb 2026 11:28:02 +0000 Subject: [PATCH 03/12] Make user-defined assets `AssetRef`s --- src/asset.rs | 50 ++++++++++++++++++++++++++-------------------- src/fixture.rs | 2 +- src/input.rs | 4 ++-- src/input/asset.rs | 14 +++++++------ src/simulation.rs | 2 +- 5 files changed, 40 insertions(+), 32 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index 5bfa5eb5f..03b590a39 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -1212,7 +1212,7 @@ impl AssetPool { } /// Commission new assets for the specified milestone year from the input data - pub fn commission_new(&mut self, year: u32, user_assets: &mut Vec) { + 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 @@ -1244,9 +1244,11 @@ impl AssetPool { } // If not, we just commission it as a single asset else { - asset.commission(AssetID(self.next_id), None, "user input"); + asset + .make_mut() + .commission(AssetID(self.next_id), None, "user input"); self.next_id += 1; - self.active.push(asset.into()); + self.active.push(asset); } } } @@ -1598,7 +1600,7 @@ mod tests { } #[fixture] - fn user_assets(mut process: Process) -> Vec { + fn user_assets(mut process: Process) -> Vec { // Update process parameters (lifetime = 20 years) let process_param = ProcessParameter { capital_cost: MoneyPerCapacity(5.0), @@ -1621,6 +1623,7 @@ mod tests { year, ) .unwrap() + .into() }) .into_iter() .collect_vec() @@ -1736,7 +1739,7 @@ mod tests { } #[rstest] - fn asset_pool_commission_new1(mut user_assets: Vec) { + fn asset_pool_commission_new1(mut user_assets: Vec) { // Asset to be commissioned in this year let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2010, &mut user_assets); @@ -1744,7 +1747,7 @@ mod tests { } #[rstest] - fn asset_pool_commission_new2(mut user_assets: Vec) { + fn asset_pool_commission_new2(mut user_assets: Vec) { // Commission year has passed let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2011, &mut user_assets); @@ -1752,7 +1755,7 @@ mod tests { } #[rstest] - fn asset_pool_commission_new3(mut user_assets: Vec) { + fn asset_pool_commission_new3(mut user_assets: Vec) { // Nothing to commission for this year let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2000, &mut user_assets); @@ -1764,7 +1767,7 @@ mod tests { let commision_year = asset_divisible.commission_year; let expected_children = expected_children_for_divisible(&asset_divisible); let mut asset_pool = AssetPool::new(); - let mut user_assets = vec![asset_divisible.clone()]; + let mut user_assets = vec![asset_divisible.into()]; assert!(asset_pool.active.is_empty()); asset_pool.commission_new(commision_year, &mut user_assets); assert!(user_assets.is_empty()); @@ -1779,12 +1782,12 @@ mod tests { let mut asset_pool = AssetPool::new(); assert!(asset_pool.active.is_empty()); asset_pool.decommission_old(year); - asset_pool.commission_new(year, &mut vec![asset]); + asset_pool.commission_new(year, &mut vec![asset.into()]); assert!(asset_pool.active.is_empty()); } #[rstest] - fn asset_pool_decommission_old(mut user_assets: Vec) { + 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()); @@ -1811,7 +1814,7 @@ mod tests { } #[rstest] - fn asset_pool_get(mut user_assets: Vec) { + 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.active[0])); @@ -1819,7 +1822,7 @@ mod tests { } #[rstest] - fn asset_pool_extend_empty(mut user_assets: Vec) { + fn asset_pool_extend_empty(mut user_assets: Vec) { // Start with commissioned assets let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2020, &mut user_assets); @@ -1832,7 +1835,7 @@ mod tests { } #[rstest] - fn asset_pool_extend_existing_assets(mut user_assets: Vec) { + fn asset_pool_extend_existing_assets(mut user_assets: Vec) { // Start with some commissioned assets let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2020, &mut user_assets); @@ -1848,7 +1851,7 @@ mod tests { } #[rstest] - fn asset_pool_extend_new_assets(mut user_assets: Vec, process: Process) { + fn asset_pool_extend_new_assets(mut user_assets: Vec, process: Process) { // Start with some commissioned assets let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2020, &mut user_assets); @@ -1894,7 +1897,10 @@ mod tests { } #[rstest] - fn asset_pool_extend_new_divisible_assets(mut user_assets: Vec, mut process: Process) { + fn asset_pool_extend_new_divisible_assets( + mut user_assets: Vec, + mut process: Process, + ) { // Start with some commissioned assets let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2020, &mut user_assets); @@ -1920,7 +1926,7 @@ mod tests { } #[rstest] - fn asset_pool_extend_mixed_assets(mut user_assets: Vec, process: Process) { + fn asset_pool_extend_mixed_assets(mut user_assets: Vec, process: Process) { // Start with some commissioned assets let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2020, &mut user_assets); @@ -1954,7 +1960,7 @@ mod tests { } #[rstest] - fn asset_pool_extend_maintains_sort_order(mut user_assets: Vec, process: Process) { + fn asset_pool_extend_maintains_sort_order(mut user_assets: Vec, process: Process) { // Start with some commissioned assets let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2020, &mut user_assets); @@ -1993,7 +1999,7 @@ mod tests { } #[rstest] - fn asset_pool_extend_no_duplicates_expected(mut user_assets: Vec) { + fn asset_pool_extend_no_duplicates_expected(mut user_assets: Vec) { // Start with some commissioned assets let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2020, &mut user_assets); @@ -2012,7 +2018,7 @@ mod tests { } #[rstest] - fn asset_pool_extend_increments_next_id(mut user_assets: Vec, process: Process) { + fn asset_pool_extend_increments_next_id(mut user_assets: Vec, process: Process) { // Start with some commissioned assets let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2020, &mut user_assets); @@ -2050,7 +2056,7 @@ mod tests { } #[rstest] - fn asset_pool_mothball_unretained(mut user_assets: Vec) { + fn asset_pool_mothball_unretained(mut user_assets: Vec) { // Commission some assets let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2020, &mut user_assets); @@ -2070,7 +2076,7 @@ mod tests { } #[rstest] - fn asset_pool_decommission_unused(mut user_assets: Vec) { + fn asset_pool_decommission_unused(mut user_assets: Vec) { // Commission some assets let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2020, &mut user_assets); @@ -2098,7 +2104,7 @@ mod tests { } #[rstest] - fn asset_pool_decommission_if_not_active_none_active(mut user_assets: Vec) { + fn asset_pool_decommission_if_not_active_none_active(mut user_assets: Vec) { // Commission some assets let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2020, &mut user_assets); diff --git a/src/fixture.rs b/src/fixture.rs index 35ae1c9e5..9be28c1c9 100644 --- a/src/fixture.rs +++ b/src/fixture.rs @@ -204,7 +204,7 @@ pub fn asset(process: Process) -> Asset { pub fn assets(asset: Asset) -> AssetPool { let year = asset.commission_year(); let mut assets = AssetPool::new(); - assets.commission_new(year, &mut vec![asset]); + assets.commission_new(year, &mut vec![asset.into()]); assets } diff --git a/src/input.rs b/src/input.rs index 68edb3010..61ff464c5 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,5 +1,5 @@ //! Common routines for handling input data. -use crate::asset::Asset; +use crate::asset::AssetRef; 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}; @@ -228,7 +228,7 @@ where /// # Returns /// /// The static model data ([`Model`]) and user-defined assets or an error. -pub fn load_model>(model_dir: P) -> Result<(Model, Vec)> { +pub fn load_model>(model_dir: P) -> Result<(Model, Vec)> { let model_params = ModelParameters::from_path(&model_dir)?; let time_slice_info = read_time_slice_info(model_dir.as_ref())?; diff --git a/src/input/asset.rs b/src/input/asset.rs index 451b706a1..31bef7e37 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; @@ -45,7 +45,7 @@ pub fn read_assets( 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/simulation.rs b/src/simulation.rs index 38ab12ef9..d11f6d49b 100644 --- a/src/simulation.rs +++ b/src/simulation.rs @@ -27,7 +27,7 @@ pub use prices::CommodityPrices; /// * `debug_model` - Whether to write additional information (e.g. duals) to output files pub fn run( model: &Model, - mut user_assets: Vec, + mut user_assets: Vec, output_path: &Path, debug_model: bool, ) -> Result<()> { From 0371da8adbd9bf84bc9b2fdbb0e61e806bd54a43 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Tue, 3 Feb 2026 11:30:04 +0000 Subject: [PATCH 04/12] Make `user_assets` a field of `Model` --- src/cli.rs | 4 ++-- src/fixture.rs | 4 ++-- src/input.rs | 10 +++++----- src/input/asset.rs | 2 +- src/model.rs | 3 +++ src/simulation.rs | 9 ++------- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 2a2f079c1..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, user_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, user_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 9be28c1c9..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(()) })() }}; diff --git a/src/input.rs b/src/input.rs index 61ff464c5..349dd46f3 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,5 +1,4 @@ //! Common routines for handling input data. -use crate::asset::AssetRef; 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; @@ -228,7 +227,7 @@ where /// # Returns /// /// The static model data ([`Model`]) and user-defined assets or an error. -pub fn load_model>(model_dir: P) -> Result<(Model, Vec)> { +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, Vec) years, )?; let agent_ids = agents.keys().cloned().collect(); - let user_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, Vec) processes, time_slice_info, regions, + user_assets, investment_order, }; - Ok((model, user_assets)) + Ok(model) } /// Load commodity flow graphs for a model. diff --git a/src/input/asset.rs b/src/input/asset.rs index 31bef7e37..e4e1b2213 100644 --- a/src/input/asset.rs +++ b/src/input/asset.rs @@ -40,7 +40,7 @@ struct AssetRaw { /// # Returns /// /// A `Vec` of [`Asset`]s or an error. -pub fn read_assets( +pub fn read_user_assets( model_dir: &Path, agent_ids: &IndexSet, processes: &ProcessMap, 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/simulation.rs b/src/simulation.rs index d11f6d49b..e19a7f79e 100644 --- a/src/simulation.rs +++ b/src/simulation.rs @@ -22,16 +22,11 @@ pub use prices::CommodityPrices; /// # Arguments: /// /// * `model` - The model to run -/// * `user_assets` - Assets supplied by user /// * `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 user_assets: Vec, - 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 assets = AssetPool::new(); // Iterate over milestone years From 89409d86384537584b03fbf26d96e22267b997d1 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Tue, 3 Feb 2026 12:03:24 +0000 Subject: [PATCH 05/12] Extract decommissioned assets from `AssetPool` --- src/asset.rs | 96 ++++++++++++++++++++--------------------------- src/simulation.rs | 15 ++++---- 2 files changed, 47 insertions(+), 64 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index 03b590a39..f2f347734 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}; @@ -1192,8 +1192,6 @@ impl Ord for AssetRef { pub struct AssetPool { /// The pool of active assets, sorted by ID active: Vec, - /// Assets that have been decommissioned - decommissioned: Vec, /// Next available asset ID number next_id: u32, /// Next available group ID number @@ -1254,36 +1252,33 @@ impl AssetPool { } /// Decommission old assets for the specified milestone year - pub fn decommission_old(&mut self, year: u32) { - // Remove assets which are due for decommissioning - 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); - } + pub fn decommission_old(&mut self, year: u32) -> impl Iterator { + self.active + .extract_if(.., move |asset| asset.max_decommission_year() <= year) + .map(move |mut asset| { + asset.make_mut().decommission(year, "end of life"); + 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` + pub fn decommission_mothballed( + &mut self, + year: u32, + mothball_years: u32, + ) -> impl Iterator { + self.active.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)."), ); - self.decommissioned.push(asset); - } + asset + }) } /// Mothball the specified assets if they are no longer in the active pool and put them back again. @@ -1343,18 +1338,6 @@ impl AssetPool { 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()) - } - /// Return current active pool and clear pub fn take(&mut self) -> Vec { std::mem::take(&mut self.active) @@ -1781,7 +1764,6 @@ mod tests { let year = asset.max_decommission_year(); let mut asset_pool = AssetPool::new(); assert!(asset_pool.active.is_empty()); - asset_pool.decommission_old(year); asset_pool.commission_new(year, &mut vec![asset.into()]); assert!(asset_pool.active.is_empty()); } @@ -1792,25 +1774,26 @@ mod tests { asset_pool.commission_new(2020, &mut user_assets); assert!(user_assets.is_empty()); assert_eq!(asset_pool.active.len(), 2); - asset_pool.decommission_old(2030); // should decommission first asset (lifetime == 5) + + // should decommission first asset (lifetime == 5) + let decommissioned = asset_pool.decommission_old(2030).collect_vec(); 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!(decommissioned.len(), 1); + assert_eq!(decommissioned[0].commission_year, 2010); + assert_eq!(decommissioned[0].decommission_year(), Some(2030)); + + // nothing to decommission + assert!(asset_pool.decommission_old(2032).next().is_none()); 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 + + // should decommission second asset + let decommissioned = asset_pool.decommission_old(2040).collect_vec(); 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)); + assert_eq!(decommissioned.len(), 1); + assert_eq!(decommissioned[0].commission_year, 2020); + assert_eq!(decommissioned[0].decommission_year(), Some(2040)); } #[rstest] @@ -2081,7 +2064,6 @@ mod tests { let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2020, &mut user_assets); assert_eq!(asset_pool.active.len(), 2); - assert_eq!(asset_pool.decommissioned.len(), 0); // Make an asset unused for a few years let mothball_years: u32 = 10; @@ -2095,12 +2077,14 @@ mod tests { ); // Decomission unused assets - asset_pool.decommission_mothballed(2025, mothball_years); + let decommissioned = asset_pool + .decommission_mothballed(2025, mothball_years) + .collect_vec(); // 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!(decommissioned.len(), 1); + assert_eq!(decommissioned[0].decommission_year(), Some(2025)); } #[rstest] diff --git a/src/simulation.rs b/src/simulation.rs index e19a7f79e..19c6936ef 100644 --- a/src/simulation.rs +++ b/src/simulation.rs @@ -27,7 +27,8 @@ pub use prices::CommodityPrices; 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 assets = AssetPool::new(); + let mut assets = AssetPool::new(); // active assets + let mut decommissioned = Vec::new(); // Iterate over milestone years let mut year_iter = model.iter_years().peekable(); @@ -35,14 +36,11 @@ pub fn run(model: &Model, output_path: &Path, debug_model: bool) -> Result<()> { info!("Milestone year: {year}"); - // There shouldn't be assets already commissioned, but let's do this just in case - assets.decommission_old(year); - // Commission assets for base year assets.commission_new(year, &mut user_assets); // Write assets to file - writer.write_assets(assets.iter_all())?; + writer.write_assets(assets.iter_active())?; // Gather candidates for the next year, if any let next_year = year_iter.peek().copied(); @@ -65,7 +63,7 @@ pub fn run(model: &Model, output_path: &Path, debug_model: bool) -> Result<()> { info!("Milestone year: {year}"); // Decommission assets whose lifetime has passed - assets.decommission_old(year); + decommissioned.extend(assets.decommission_old(year)); // Commission user-defined assets for this year assets.commission_new(year, &mut user_assets); @@ -125,10 +123,11 @@ pub fn run(model: &Model, output_path: &Path, debug_model: bool) -> Result<()> { // Decommission unused assets assets.mothball_unretained(existing_assets, year); - assets.decommission_mothballed(year, model.parameters.mothball_years); + decommissioned + .extend(assets.decommission_mothballed(year, model.parameters.mothball_years)); // Write assets - writer.write_assets(assets.iter_all())?; + writer.write_assets(decommissioned.iter().chain(assets.iter_active()))?; // Gather candidates for the next year, if any let next_year = year_iter.peek().copied(); From 09a2b31e8253e6c72a751268dbef142a86a01c98 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Tue, 3 Feb 2026 12:08:37 +0000 Subject: [PATCH 06/12] Renaming because all assets in pool are now active --- src/asset.rs | 164 +++++++++++++++++++++++----------------------- src/output.rs | 10 +-- src/simulation.rs | 24 +++---- 3 files changed, 98 insertions(+), 100 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index f2f347734..f738483ea 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -1187,11 +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: Vec, /// Next available asset ID number next_id: u32, /// Next available group ID number @@ -1206,7 +1206,7 @@ impl AssetPool { /// Get the active pool as a slice of [`AssetRef`]s pub fn as_slice(&self) -> &[AssetRef] { - &self.active + &self.assets } /// Commission new assets for the specified milestone year from the input data @@ -1236,7 +1236,7 @@ impl AssetPool { "user input", ); self.next_id += 1; - self.active.push(child); + self.assets.push(child); } self.next_group_id += 1; } @@ -1246,14 +1246,14 @@ impl AssetPool { .make_mut() .commission(AssetID(self.next_id), None, "user input"); self.next_id += 1; - self.active.push(asset); + self.assets.push(asset); } } } /// Decommission old assets for the specified milestone year pub fn decommission_old(&mut self, year: u32) -> impl Iterator { - self.active + self.assets .extract_if(.., move |asset| asset.max_decommission_year() <= year) .map(move |mut asset| { asset.make_mut().decommission(year, "end of life"); @@ -1267,7 +1267,7 @@ impl AssetPool { year: u32, mothball_years: u32, ) -> impl Iterator { - self.active.extract_if(.., move |asset| { + self.assets.extract_if(.., move |asset| { asset.get_mothballed_year().is_some_and(|myear|{ myear <= year - min(mothball_years, year)}) }) @@ -1297,7 +1297,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, @@ -1308,10 +1308,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. @@ -1323,24 +1323,25 @@ impl AssetPool { pub fn get(&self, id: AssetID) -> Option<&AssetRef> { // The assets in `active` are in order of 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() + #[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 @@ -1354,7 +1355,7 @@ 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 @@ -1366,7 +1367,7 @@ impl AssetPool { "selected", ); self.next_id += 1; - self.active.push(child); + self.assets.push(child); } self.next_group_id += 1; } @@ -1376,7 +1377,7 @@ impl AssetPool { .make_mut() .commission(AssetID(self.next_id), None, "selected"); self.next_id += 1; - self.active.push(asset); + self.assets.push(asset); } } _ => panic!( @@ -1388,10 +1389,10 @@ impl AssetPool { } // New assets may not have been sorted, but active needs to be sorted by ID - self.active.sort(); + 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()); } } @@ -1718,7 +1719,7 @@ mod tests { #[rstest] fn asset_pool_new() { - assert!(AssetPool::new().active.is_empty()); + assert!(AssetPool::new().assets.is_empty()); } #[rstest] @@ -1726,7 +1727,7 @@ mod tests { // Asset to be commissioned in this year let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2010, &mut user_assets); - assert_equal(asset_pool.iter_active(), iter::once(&asset_pool.active[0])); + assert_equal(asset_pool.iter(), iter::once(&asset_pool.assets[0])); } #[rstest] @@ -1734,7 +1735,7 @@ mod tests { // Commission year has passed let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2011, &mut user_assets); - assert_equal(asset_pool.iter_active(), iter::once(&asset_pool.active[0])); + assert_equal(asset_pool.iter(), iter::once(&asset_pool.assets[0])); } #[rstest] @@ -1742,7 +1743,7 @@ mod tests { // Nothing to commission for this year let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2000, &mut user_assets); - assert!(asset_pool.iter_active().next().is_none()); // no active assets + assert!(asset_pool.iter().next().is_none()); // no active assets } #[rstest] @@ -1751,11 +1752,11 @@ mod tests { let expected_children = expected_children_for_divisible(&asset_divisible); let mut asset_pool = AssetPool::new(); let mut user_assets = vec![asset_divisible.into()]; - assert!(asset_pool.active.is_empty()); + assert!(asset_pool.assets.is_empty()); asset_pool.commission_new(commision_year, &mut user_assets); assert!(user_assets.is_empty()); - assert!(!asset_pool.active.is_empty()); - assert_eq!(asset_pool.active.len(), expected_children); + assert!(!asset_pool.assets.is_empty()); + assert_eq!(asset_pool.assets.len(), expected_children); assert_eq!(asset_pool.next_group_id, 1); } @@ -1763,9 +1764,9 @@ mod tests { fn asset_pool_commission_already_decommissioned(asset: Asset) { let year = asset.max_decommission_year(); let mut asset_pool = AssetPool::new(); - assert!(asset_pool.active.is_empty()); + assert!(asset_pool.assets.is_empty()); asset_pool.commission_new(year, &mut vec![asset.into()]); - assert!(asset_pool.active.is_empty()); + assert!(asset_pool.assets.is_empty()); } #[rstest] @@ -1773,24 +1774,24 @@ mod tests { let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2020, &mut user_assets); assert!(user_assets.is_empty()); - assert_eq!(asset_pool.active.len(), 2); + assert_eq!(asset_pool.assets.len(), 2); // should decommission first asset (lifetime == 5) let decommissioned = asset_pool.decommission_old(2030).collect_vec(); - assert_eq!(asset_pool.active.len(), 1); - assert_eq!(asset_pool.active[0].commission_year, 2020); + 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 assert!(asset_pool.decommission_old(2032).next().is_none()); - assert_eq!(asset_pool.active.len(), 1); - assert_eq!(asset_pool.active[0].commission_year, 2020); + assert_eq!(asset_pool.assets.len(), 1); + assert_eq!(asset_pool.assets[0].commission_year, 2020); // should decommission second asset let decommissioned = asset_pool.decommission_old(2040).collect_vec(); - assert!(asset_pool.active.is_empty()); + 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)); @@ -1800,8 +1801,8 @@ mod tests { 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.active[0])); - assert_eq!(asset_pool.get(AssetID(1)), Some(&asset_pool.active[1])); + 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] @@ -1809,12 +1810,12 @@ mod tests { // Start with commissioned assets let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2020, &mut user_assets); - let original_count = asset_pool.active.len(); + 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] @@ -1822,15 +1823,15 @@ mod tests { // Start with some commissioned assets let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2020, &mut user_assets); - assert_eq!(asset_pool.active.len(), 2); + 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] @@ -1838,7 +1839,7 @@ mod tests { // Start with some commissioned assets let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2020, &mut user_assets); - let original_count = asset_pool.active.len(); + let original_count = asset_pool.assets.len(); // Create new non-commissioned assets let process_rc = Rc::new(process); @@ -1865,16 +1866,16 @@ 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()) ); } @@ -1887,7 +1888,7 @@ mod tests { // Start with some commissioned assets let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2020, &mut user_assets); - let original_count = asset_pool.active.len(); + let original_count = asset_pool.assets.len(); // Create new non-commissioned assets process.unit_size = Some(Capacity(4.0)); @@ -1905,7 +1906,7 @@ 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] @@ -1928,15 +1929,15 @@ 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())) ); @@ -1974,10 +1975,7 @@ 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); } @@ -1986,17 +1984,17 @@ mod tests { // Start with some commissioned assets let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2020, &mut user_assets); - let original_count = asset_pool.active.len(); + 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() ); } @@ -2034,8 +2032,8 @@ 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] @@ -2043,19 +2041,19 @@ mod tests { // Commission some assets let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2020, &mut user_assets); - assert_eq!(asset_pool.active.len(), 2); + 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] @@ -2063,16 +2061,16 @@ mod tests { // Commission some assets let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2020, &mut user_assets); - assert_eq!(asset_pool.active.len(), 2); + 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) ); @@ -2082,7 +2080,7 @@ mod tests { .collect_vec(); // 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.assets.len(), 1); // Active pool unchanged assert_eq!(decommissioned.len(), 1); assert_eq!(decommissioned[0].decommission_year(), Some(2025)); } @@ -2092,20 +2090,20 @@ mod tests { // Commission some assets let mut asset_pool = AssetPool::new(); asset_pool.commission_new(2020, &mut user_assets); - let all_assets = asset_pool.active.clone(); + 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] 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 19c6936ef..36744df98 100644 --- a/src/simulation.rs +++ b/src/simulation.rs @@ -27,7 +27,7 @@ pub use prices::CommodityPrices; 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 assets = AssetPool::new(); // active assets + let mut asset_pool = AssetPool::new(); // active assets let mut decommissioned = Vec::new(); // Iterate over milestone years @@ -37,10 +37,10 @@ pub fn run(model: &Model, output_path: &Path, debug_model: bool) -> Result<()> { info!("Milestone year: {year}"); // Commission assets for base year - assets.commission_new(year, &mut user_assets); + asset_pool.commission_new(year, &mut user_assets); // Write assets to file - writer.write_assets(assets.iter_active())?; + writer.write_assets(asset_pool.iter())?; // Gather candidates for the next year, if any let next_year = year_iter.peek().copied(); @@ -53,7 +53,7 @@ pub fn run(model: &Model, output_path: &Path, debug_model: bool) -> Result<()> { // 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)?; @@ -63,13 +63,13 @@ pub fn run(model: &Model, output_path: &Path, debug_model: bool) -> Result<()> { info!("Milestone year: {year}"); // Decommission assets whose lifetime has passed - decommissioned.extend(assets.decommission_old(year)); + decommissioned.extend(asset_pool.decommission_old(year)); // Commission user-defined assets for this year - assets.commission_new(year, &mut user_assets); + 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; @@ -119,15 +119,15 @@ pub fn run(model: &Model, output_path: &Path, debug_model: bool) -> Result<()> { }; // 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); + asset_pool.mothball_unretained(existing_assets, year); decommissioned - .extend(assets.decommission_mothballed(year, model.parameters.mothball_years)); + .extend(asset_pool.decommission_mothballed(year, model.parameters.mothball_years)); // Write assets - writer.write_assets(decommissioned.iter().chain(assets.iter_active()))?; + writer.write_assets(decommissioned.iter().chain(asset_pool.iter()))?; // Gather candidates for the next year, if any let next_year = year_iter.peek().copied(); @@ -140,7 +140,7 @@ pub fn run(model: &Model, output_path: &Path, debug_model: bool) -> Result<()> { // 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)?; From 7c5644d139ac1e0763090efb09537f55420afaa9 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Tue, 3 Feb 2026 12:12:45 +0000 Subject: [PATCH 07/12] Deduplicate code for commissioning assets or their children --- src/asset.rs | 67 ++++++++++++++++++++-------------------------------- 1 file changed, 26 insertions(+), 41 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index f738483ea..1bf835069 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -1214,7 +1214,7 @@ impl AssetPool { let to_commission = user_assets.extract_if(.., |asset| asset.commission_year <= year); // Move assets from future to active - for mut asset in to_commission { + for asset in to_commission { // Ignore assets that have already been decommissioned if asset.max_decommission_year() <= year { warn!( @@ -1227,27 +1227,32 @@ 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.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, "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.assets.push(asset); + 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); } } @@ -1358,27 +1363,7 @@ impl AssetPool { 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.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, "selected"); - self.next_id += 1; - self.assets.push(asset); - } + self.commission(asset, "selected"); } _ => panic!( "Cannot extend asset pool with asset in state {}. Only assets in \ From e3a7bc84ce1551c70f0203908d2569aef932077e Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Tue, 3 Feb 2026 13:06:41 +0000 Subject: [PATCH 08/12] Apply Copilot's suggestions Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/asset.rs | 9 +++++---- src/input/asset.rs | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index 1bf835069..f9e3cc7c9 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -1266,17 +1266,18 @@ impl AssetPool { }) } - /// Decomission mothballed assets if mothballed long enough + /// Decommission mothballed assets if mothballed long enough pub fn decommission_mothballed( &mut self, year: u32, mothball_years: u32, ) -> impl Iterator { self.assets.extract_if(.., move |asset| { - asset.get_mothballed_year().is_some_and(|myear|{ - myear <= year - min(mothball_years, year)}) + asset.get_mothballed_year().is_some_and(|myear| { + myear <= year - min(mothball_years, year) + }) }) - .map(move |mut asset|{ + .map(move |mut asset| { let decommissioned = asset.get_mothballed_year().unwrap() + mothball_years; asset.make_mut().decommission( decommissioned, diff --git a/src/input/asset.rs b/src/input/asset.rs index e4e1b2213..8f219b3f6 100644 --- a/src/input/asset.rs +++ b/src/input/asset.rs @@ -39,7 +39,7 @@ struct AssetRaw { /// /// # Returns /// -/// A `Vec` of [`Asset`]s or an error. +/// A `Vec` of [`AssetRef`]s or an error. pub fn read_user_assets( model_dir: &Path, agent_ids: &IndexSet, From d748010f9deee23796567310158aa74e30f27aae Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Tue, 3 Feb 2026 13:07:13 +0000 Subject: [PATCH 09/12] Fix doc comment --- src/input.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/input.rs b/src/input.rs index 349dd46f3..7cdfcca23 100644 --- a/src/input.rs +++ b/src/input.rs @@ -226,7 +226,7 @@ where /// /// # Returns /// -/// The static model data ([`Model`]) and user-defined assets or an error. +/// The static model data ([`Model`]) or an error. pub fn load_model>(model_dir: P) -> Result { let model_params = ModelParameters::from_path(&model_dir)?; From 6fb8f7f1fd1a61ba3977a42cd2c526d0a428d52d Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Tue, 3 Feb 2026 15:57:50 +0000 Subject: [PATCH 10/12] Fix stale comments --- src/asset.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index f9e3cc7c9..c79d2c775 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -1213,7 +1213,6 @@ impl AssetPool { 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 asset in to_commission { // Ignore assets that have already been decommissioned if asset.max_decommission_year() <= year { @@ -1327,7 +1326,7 @@ 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 .assets .binary_search_by(|asset| match &asset.state { @@ -1374,7 +1373,7 @@ impl AssetPool { } } - // New assets may not have been sorted, but active needs to be sorted by ID + // New assets may not have been sorted, but we need them sorted by ID self.assets.sort(); // Sanity check: all assets should be unique From 112976b61e47cef13dd27398efb1cba777baaf5d Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Thu, 5 Feb 2026 11:20:52 +0000 Subject: [PATCH 11/12] Fix: Store decommissioned assets directly in `Vec`s Returning an iterator has a subtle gotcha: because iterators are evaluated lazily, if the caller does not comsume the whole thing, only some of the assets that should be decommissioned will be. --- src/asset.rs | 58 +++++++++++++++++++++++++++-------------------- src/simulation.rs | 9 +++++--- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index c79d2c775..710d43092 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -1256,34 +1256,42 @@ impl AssetPool { } /// Decommission old assets for the specified milestone year - pub fn decommission_old(&mut self, year: u32) -> impl Iterator { - self.assets + pub fn decommission_old>(&mut self, year: u32, decommissioned: &mut E) { + let to_decommission = self + .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( + pub fn decommission_mothballed>( &mut self, year: u32, mothball_years: u32, - ) -> impl Iterator { - 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: &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. @@ -1760,9 +1768,10 @@ mod tests { 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) - let decommissioned = asset_pool.decommission_old(2030).collect_vec(); + 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); @@ -1770,12 +1779,14 @@ mod tests { assert_eq!(decommissioned[0].decommission_year(), Some(2030)); // nothing to decommission - assert!(asset_pool.decommission_old(2032).next().is_none()); + 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 - let decommissioned = asset_pool.decommission_old(2040).collect_vec(); + 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); @@ -2060,9 +2071,8 @@ mod tests { ); // Decomission unused assets - let decommissioned = asset_pool - .decommission_mothballed(2025, mothball_years) - .collect_vec(); + 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.assets.len(), 1); // Active pool unchanged diff --git a/src/simulation.rs b/src/simulation.rs index 36744df98..20d4d70c5 100644 --- a/src/simulation.rs +++ b/src/simulation.rs @@ -63,7 +63,7 @@ pub fn run(model: &Model, output_path: &Path, debug_model: bool) -> Result<()> { info!("Milestone year: {year}"); // Decommission assets whose lifetime has passed - decommissioned.extend(asset_pool.decommission_old(year)); + asset_pool.decommission_old(year, &mut decommissioned); // Commission user-defined assets for this year asset_pool.commission_new(year, &mut user_assets); @@ -123,8 +123,11 @@ pub fn run(model: &Model, output_path: &Path, debug_model: bool) -> Result<()> { // Decommission unused assets asset_pool.mothball_unretained(existing_assets, year); - decommissioned - .extend(asset_pool.decommission_mothballed(year, model.parameters.mothball_years)); + asset_pool.decommission_mothballed( + year, + model.parameters.mothball_years, + &mut decommissioned, + ); // Write assets writer.write_assets(decommissioned.iter().chain(asset_pool.iter()))?; From ae12d9326a41c502da7d4441eea23ddf5a4f419c Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Thu, 5 Feb 2026 11:31:15 +0000 Subject: [PATCH 12/12] Fix whitespace Unsure why `cargo fmt` didn't fix this... Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/asset.rs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index 710d43092..92812103e 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -1274,23 +1274,24 @@ impl AssetPool { 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( + 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)."), + years)." + ), ); - asset - }); + asset + }); decommissioned.extend(to_decommission); }