Skip to content
Merged
494 changes: 233 additions & 261 deletions src/asset.rs

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand All @@ -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(())
Expand Down
8 changes: 4 additions & 4 deletions src/fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
})()
}};
Expand Down Expand Up @@ -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
}

Expand Down
12 changes: 6 additions & 6 deletions src/input.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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;
Expand Down Expand Up @@ -227,8 +226,8 @@ where
///
/// # Returns
///
/// The static model data ([`Model`]) and an [`AssetPool`] struct or an error.
pub fn load_model<P: AsRef<Path>>(model_dir: P) -> Result<(Model, AssetPool)> {
/// The static model data ([`Model`]) or an error.
pub fn load_model<P: AsRef<Path>>(model_dir: P) -> Result<Model> {
let model_params = ModelParameters::from_path(&model_dir)?;

let time_slice_info = read_time_slice_info(model_dir.as_ref())?;
Expand All @@ -252,7 +251,7 @@ pub fn load_model<P: AsRef<Path>>(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, &region_ids)?;
let user_assets = read_user_assets(model_dir.as_ref(), &agent_ids, &processes, &region_ids)?;

// Build and validate commodity graphs for all regions and years
let commodity_graphs = build_commodity_graphs_for_model(&processes, &region_ids, years);
Expand All @@ -278,9 +277,10 @@ pub fn load_model<P: AsRef<Path>>(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.
Expand Down
18 changes: 10 additions & 8 deletions src/input/asset.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<AgentID>,
processes: &ProcessMap,
region_ids: &IndexSet<RegionID>,
) -> Result<Vec<Asset>> {
) -> Result<Vec<AssetRef>> {
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)
Expand All @@ -69,7 +69,7 @@ fn read_assets_from_iter<I>(
agent_ids: &IndexSet<AgentID>,
processes: &ProcessMap,
region_ids: &IndexSet<RegionID>,
) -> Result<Vec<Asset>>
) -> Result<Vec<AssetRef>>
where
I: Iterator<Item = AssetRaw>,
{
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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, &region_ids)
.unwrap(),
Expand Down
3 changes: 3 additions & 0 deletions src/model.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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<AssetRef>,
/// Commodity ordering for each milestone year
pub investment_order: HashMap<u32, Vec<InvestmentSet>>,
}
Expand Down
10 changes: 5 additions & 5 deletions src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AssetRow> = csv::Reader::from_path(dir.path().join(ASSETS_FILE_NAME))
.unwrap()
Expand All @@ -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)
};
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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
{
Expand Down
42 changes: 22 additions & 20 deletions src/simulation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The decommissioned Vec is never cleared between milestone years, causing decommissioned assets to accumulate across all years. When assets are written at line 133, all previously decommissioned assets from earlier years will be written again, creating duplicate entries in the output. The Vec should be cleared after writing assets to file (after line 133) to ensure each year only writes the assets that were decommissioned in that specific year.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, but that's by design! We write all decommissioned assets to file.


// Iterate over milestone years
let mut year_iter = model.iter_years().peekable();
Expand All @@ -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())?;
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

In the base year, only active assets are written to the output file, but decommissioned assets should also be included for consistency. If any user-defined assets have already reached their max_decommission_year by the base year, they would be skipped during commissioning (with a warning) but never written to the output file. This creates an inconsistency with subsequent years (line 133) where both decommissioned and active assets are written. Consider changing this line to: writer.write_assets(decommissioned.iter().chain(asset_pool.iter()))?; to match the pattern used in subsequent years.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This isn't right... assets that are "decommissioned" before the time horizon are simply discarded, so they never end up in the decommissioned Vec. So there are no decommissioned assets to be written to file here.


// Gather candidates for the next year, if any
let next_year = year_iter.peek().copied();
Expand All @@ -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)?;
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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)?;
Expand Down