From e4f77a60fc8fe88ef52693314f1c863ebaa37961 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 13 Mar 2025 11:28:55 +0000 Subject: [PATCH 1/9] Add framework for retrieving capacity duals --- src/simulation/optimisation.rs | 81 ++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 8 deletions(-) diff --git a/src/simulation/optimisation.rs b/src/simulation/optimisation.rs index 6e3aa927b..0c31f9564 100644 --- a/src/simulation/optimisation.rs +++ b/src/simulation/optimisation.rs @@ -67,12 +67,16 @@ impl VariableMapKey { /// Indicates the commodity ID and time slice selection covered by each commodity constraint type CommodityConstraintKeys = Vec<(Rc, TimeSliceSelection)>; +/// Indicates the asset ID and time slice covered by each capacity constraint +type CapacityConstraintKeys = Vec<(AssetID, TimeSliceID)>; + /// The solution to the dispatch optimisation problem pub struct Solution<'a> { solution: highs::Solution, variables: VariableMap, time_slice_info: &'a TimeSliceInfo, commodity_constraint_keys: CommodityConstraintKeys, + capacity_constraint_keys: CapacityConstraintKeys, } impl Solution<'_> { @@ -99,6 +103,18 @@ impl Solution<'_> { /// Note that there may only be prices for a subset of the commodities; the rest will need to be /// calculated in another way. pub fn iter_commodity_prices(&self) -> impl Iterator, &TimeSliceID, f64)> { + // Calculate commodity balance duals + let commodity_duals = self.iter_commodity_balance_duals(); + + // Calculate capacity duals + let _capacity_duals = self.iter_capacity_duals(); + + // Calculate prices (for now this is just the commodity duals) + commodity_duals + } + + /// Keys and dual values for commodity balance constraints. + fn iter_commodity_balance_duals(&self) -> impl Iterator, &TimeSliceID, f64)> { // We can get the prices by looking at the dual row values for commodity balance // constraints. Each commodity balance constraint applies to a particular time slice // selection (depending on time slice level), but we want to return prices for each time @@ -112,6 +128,18 @@ impl Solution<'_> { .map(move |(ts, _)| (commodity_id, ts, *price)) }) } + + /// Keys and dual values for capacity constraints. + fn iter_capacity_duals(&self) -> impl Iterator { + self.capacity_constraint_keys + .iter() + .zip( + self.solution.dual_rows()[self.commodity_constraint_keys.len()..] + .iter() + .copied(), + ) + .map(|((asset_id, time_slice), dual)| (*asset_id, time_slice, dual)) + } } /// Perform the dispatch optimisation. @@ -139,8 +167,8 @@ pub fn perform_dispatch_optimisation<'a>( let variables = add_variables(&mut problem, model, assets, year); // Add constraints - let commodity_constraint_keys = - add_asset_contraints(&mut problem, &variables, model, assets, year); + let (commodity_constraint_keys, capacity_constraint_keys) = + add_asset_constraints(&mut problem, &variables, model, assets, year); // Solve problem let mut highs_model = problem.optimise(Sense::Minimise); @@ -161,6 +189,7 @@ pub fn perform_dispatch_optimisation<'a>( variables, time_slice_info: &model.time_slice_info, commodity_constraint_keys, + capacity_constraint_keys, }), status => Err(anyhow!("Could not solve: {status:?}")), } @@ -264,16 +293,40 @@ fn calculate_cost_coefficient( } /// Add asset-level constraints -fn add_asset_contraints( +/// +/// Note: the ordering of constraints is important, as the dual values of the constraints must later +/// be retrieved to calculate commodity prices. +/// +/// # Arguments: +/// +/// * `problem` - The optimisation problem +/// * `variables` - The variables in the problem +/// * `model` - The model +/// * `assets` - The asset pool +/// * `year` - Current milestone year +/// +/// # Returns: +/// +/// * A list of keys for commodity balance constraints +/// * A list of keys for capacity constraints +fn add_asset_constraints( problem: &mut Problem, variables: &VariableMap, model: &Model, assets: &AssetPool, year: u32, -) -> CommodityConstraintKeys { +) -> (CommodityConstraintKeys, CapacityConstraintKeys) { let commodity_constraint_keys = add_commodity_balance_constraints(problem, variables, model, assets, year); + let capacity_constraint_keys = add_asset_capacity_constraints( + problem, + variables, + assets, + &model.time_slice_info, + &commodity_constraint_keys, + ); + // **TODO**: Currently it's safe to assume all process flows are non-flexible, as we enforce // this when reading data in. Once we've added support for flexible process flows, we will // need to add different constraints for assets with flexible and non-flexible flows. @@ -281,9 +334,8 @@ fn add_asset_contraints( // See: https://github.com/EnergySystemsModellingLab/MUSE_2.0/issues/360 add_fixed_asset_constraints(problem, variables, assets, &model.time_slice_info); - add_asset_capacity_constraints(problem, variables, assets, &model.time_slice_info); - - commodity_constraint_keys + // Return constraint keys which are required to calculate commodity prices + (commodity_constraint_keys, capacity_constraint_keys) } /// Add asset-level input-output commodity balances. @@ -418,8 +470,17 @@ fn add_asset_capacity_constraints( variables: &VariableMap, assets: &AssetPool, time_slice_info: &TimeSliceInfo, -) { + commodity_constraint_keys: &CommodityConstraintKeys, +) -> CapacityConstraintKeys { + // Sanity check: we rely on the dual rows corresponding to the capacity constraints being + // immediately after the commodity constraints in `problem`. + assert!( + problem.num_rows() == commodity_constraint_keys.len(), + "Capacity constraints must be added immediately after commodity constraints." + ); + let mut terms = Vec::new(); + let mut keys = CapacityConstraintKeys::new(); for asset in assets.iter() { for time_slice in time_slice_info.iter_ids() { let mut is_input = false; // NB: there will be at least one PAC @@ -438,8 +499,12 @@ fn add_asset_capacity_constraints( } problem.add_row(limits, terms.drain(0..)); + + // Keep track of the order in which constraints were added + keys.push((asset.id, time_slice.clone())); } } + keys } #[cfg(test)] From 05a0b85c97599da91aa9abcf5d341d52e2eb4f8b Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 13 Mar 2025 11:53:20 +0000 Subject: [PATCH 2/9] Remove iter_commodity_prices --- src/simulation/optimisation.rs | 21 ++++----------------- src/simulation/prices.rs | 2 +- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/simulation/optimisation.rs b/src/simulation/optimisation.rs index 0c31f9564..5b9b82e2a 100644 --- a/src/simulation/optimisation.rs +++ b/src/simulation/optimisation.rs @@ -98,23 +98,10 @@ impl Solution<'_> { .map(|(key, flow)| (key.asset_id, &key.commodity_id, &key.time_slice, flow)) } - /// Iterate over the newly calculated commodity prices for each time slice. - /// - /// Note that there may only be prices for a subset of the commodities; the rest will need to be - /// calculated in another way. - pub fn iter_commodity_prices(&self) -> impl Iterator, &TimeSliceID, f64)> { - // Calculate commodity balance duals - let commodity_duals = self.iter_commodity_balance_duals(); - - // Calculate capacity duals - let _capacity_duals = self.iter_capacity_duals(); - - // Calculate prices (for now this is just the commodity duals) - commodity_duals - } - /// Keys and dual values for commodity balance constraints. - fn iter_commodity_balance_duals(&self) -> impl Iterator, &TimeSliceID, f64)> { + pub fn iter_commodity_balance_duals( + &self, + ) -> impl Iterator, &TimeSliceID, f64)> { // We can get the prices by looking at the dual row values for commodity balance // constraints. Each commodity balance constraint applies to a particular time slice // selection (depending on time slice level), but we want to return prices for each time @@ -130,7 +117,7 @@ impl Solution<'_> { } /// Keys and dual values for capacity constraints. - fn iter_capacity_duals(&self) -> impl Iterator { + pub fn iter_capacity_duals(&self) -> impl Iterator { self.capacity_constraint_keys .iter() .zip( diff --git a/src/simulation/prices.rs b/src/simulation/prices.rs index 2c9e7402d..c7ab3e8ff 100644 --- a/src/simulation/prices.rs +++ b/src/simulation/prices.rs @@ -44,7 +44,7 @@ impl CommodityPrices { fn add_from_solution(&mut self, solution: &Solution) -> HashSet> { let mut commodities_updated = HashSet::new(); - for (commodity_id, time_slice, price) in solution.iter_commodity_prices() { + for (commodity_id, time_slice, price) in solution.iter_commodity_balance_duals() { self.insert(commodity_id, time_slice, price); commodities_updated.insert(Rc::clone(commodity_id)); } From bbe989a08bd0c94eed283df0763c4797eb59db08 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 13 Mar 2025 13:51:11 +0000 Subject: [PATCH 3/9] Working (but messy) calculation --- src/simulation.rs | 2 +- src/simulation/prices.rs | 53 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/simulation.rs b/src/simulation.rs index 40f98e638..1e313f065 100644 --- a/src/simulation.rs +++ b/src/simulation.rs @@ -49,7 +49,7 @@ pub fn run(model: Model, mut assets: AssetPool, output_path: &Path) -> Result<() // Dispatch optimisation let solution = perform_dispatch_optimisation(&model, &assets, year)?; - let prices = CommodityPrices::from_model_and_solution(&model, &solution); + let prices = CommodityPrices::from_model_and_solution(&model, &solution, &assets); // Write current commodity prices to CSV write_commodity_prices_to_csv(&mut file, year, &prices)?; diff --git a/src/simulation/prices.rs b/src/simulation/prices.rs index c7ab3e8ff..b7c5e448a 100644 --- a/src/simulation/prices.rs +++ b/src/simulation/prices.rs @@ -1,5 +1,6 @@ //! Code for updating the simulation state. use super::optimisation::Solution; +use crate::agent::AssetPool; use crate::model::Model; use crate::time_slice::{TimeSliceID, TimeSliceInfo}; use indexmap::IndexMap; @@ -18,9 +19,9 @@ impl CommodityPrices { /// Calculate commodity prices based on the result of the dispatch optimisation. /// /// Missing prices will be calculated directly from the input data - pub fn from_model_and_solution(model: &Model, solution: &Solution) -> Self { + pub fn from_model_and_solution(model: &Model, solution: &Solution, assets: &AssetPool) -> Self { let mut prices = CommodityPrices::default(); - let commodities_updated = prices.add_from_solution(solution); + let commodities_updated = prices.add_from_solution(solution, assets); // Find commodities not updated in last step let remaining_commodities = model @@ -41,11 +42,53 @@ impl CommodityPrices { /// # Returns /// /// The set of commodities for which prices were added. - fn add_from_solution(&mut self, solution: &Solution) -> HashSet> { + fn add_from_solution(&mut self, solution: &Solution, assets: &AssetPool) -> HashSet> { let mut commodities_updated = HashSet::new(); - for (commodity_id, time_slice, price) in solution.iter_commodity_balance_duals() { - self.insert(commodity_id, time_slice, price); + // Calculate highest capacity dual for each commodity/timeslice + let mut highest_duals: IndexMap = IndexMap::new(); + for (asset_id, time_slice, dual) in solution.iter_capacity_duals() { + // Get the asset + let asset = assets.get(asset_id).unwrap(); + + // Iterate over process pacs + let process_pacs = asset.process.iter_pacs(); + for pac in process_pacs { + // Get the commodity + let commodity = &pac.commodity; + + // If the commodity flow is positive (produced PAC) + if pac.flow > 0.0 { + let key: CommodityPriceKey = (commodity.id.clone(), time_slice.clone()); + // Update the highest dual for this commodity/timeslice + highest_duals + .entry(key) + .and_modify(|current_dual| { + if dual > *current_dual { + *current_dual = dual; + } + }) + .or_insert(dual); + } + } + } + + // Insert the highest duals into the prices map + for ((commodity_id, time_slice), dual) in highest_duals.iter() { + self.insert(commodity_id, time_slice, *dual); + commodities_updated.insert(Rc::clone(commodity_id)); + } + + // Combine with commodity balance duals + for (commodity_id, time_slice, dual) in solution.iter_commodity_balance_duals() { + let key = (Rc::clone(commodity_id), time_slice.clone()); + let _combined_dual = self + .0 + .entry(key) + .and_modify(|current_dual| { + *current_dual += dual; + }) + .or_insert(dual); commodities_updated.insert(Rc::clone(commodity_id)); } From 21610882dd4fcc7370bd8be94186d18f711f56c7 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 13 Mar 2025 14:25:19 +0000 Subject: [PATCH 4/9] Rename some variables --- src/simulation/optimisation.rs | 43 +++++++++++++++++----------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/simulation/optimisation.rs b/src/simulation/optimisation.rs index 5b9b82e2a..16b548c17 100644 --- a/src/simulation/optimisation.rs +++ b/src/simulation/optimisation.rs @@ -64,8 +64,8 @@ impl VariableMapKey { } } -/// Indicates the commodity ID and time slice selection covered by each commodity constraint -type CommodityConstraintKeys = Vec<(Rc, TimeSliceSelection)>; +/// Indicates the commodity ID and time slice selection covered by each commodity balance constraint +type CommodityBalanceConstraintKeys = Vec<(Rc, TimeSliceSelection)>; /// Indicates the asset ID and time slice covered by each capacity constraint type CapacityConstraintKeys = Vec<(AssetID, TimeSliceID)>; @@ -75,7 +75,7 @@ pub struct Solution<'a> { solution: highs::Solution, variables: VariableMap, time_slice_info: &'a TimeSliceInfo, - commodity_constraint_keys: CommodityConstraintKeys, + commodity_balance_constraint_keys: CommodityBalanceConstraintKeys, capacity_constraint_keys: CapacityConstraintKeys, } @@ -102,11 +102,10 @@ impl Solution<'_> { pub fn iter_commodity_balance_duals( &self, ) -> impl Iterator, &TimeSliceID, f64)> { - // We can get the prices by looking at the dual row values for commodity balance - // constraints. Each commodity balance constraint applies to a particular time slice - // selection (depending on time slice level), but we want to return prices for each time - // slice, so if the selection covers multiple time slices we return the same price for each. - self.commodity_constraint_keys + // Each commodity balance constraint applies to a particular time slice + // selection (depending on time slice level). Where this covers multiple timeslices, + // we return the same dual for each individual timeslice. + self.commodity_balance_constraint_keys .iter() .zip(self.solution.dual_rows()) .flat_map(|((commodity_id, ts_selection), price)| { @@ -121,7 +120,7 @@ impl Solution<'_> { self.capacity_constraint_keys .iter() .zip( - self.solution.dual_rows()[self.commodity_constraint_keys.len()..] + self.solution.dual_rows()[self.commodity_balance_constraint_keys.len()..] .iter() .copied(), ) @@ -154,7 +153,7 @@ pub fn perform_dispatch_optimisation<'a>( let variables = add_variables(&mut problem, model, assets, year); // Add constraints - let (commodity_constraint_keys, capacity_constraint_keys) = + let (commodity_balance_constraint_keys, capacity_constraint_keys) = add_asset_constraints(&mut problem, &variables, model, assets, year); // Solve problem @@ -175,7 +174,7 @@ pub fn perform_dispatch_optimisation<'a>( solution: solution.get_solution(), variables, time_slice_info: &model.time_slice_info, - commodity_constraint_keys, + commodity_balance_constraint_keys, capacity_constraint_keys, }), status => Err(anyhow!("Could not solve: {status:?}")), @@ -294,16 +293,16 @@ fn calculate_cost_coefficient( /// /// # Returns: /// -/// * A list of keys for commodity balance constraints -/// * A list of keys for capacity constraints +/// * A vector of keys for commodity balance constraints +/// * A vector of keys for capacity constraints fn add_asset_constraints( problem: &mut Problem, variables: &VariableMap, model: &Model, assets: &AssetPool, year: u32, -) -> (CommodityConstraintKeys, CapacityConstraintKeys) { - let commodity_constraint_keys = +) -> (CommodityBalanceConstraintKeys, CapacityConstraintKeys) { + let commodity_balance_constraint_keys = add_commodity_balance_constraints(problem, variables, model, assets, year); let capacity_constraint_keys = add_asset_capacity_constraints( @@ -311,7 +310,7 @@ fn add_asset_constraints( variables, assets, &model.time_slice_info, - &commodity_constraint_keys, + &commodity_balance_constraint_keys, ); // **TODO**: Currently it's safe to assume all process flows are non-flexible, as we enforce @@ -322,7 +321,7 @@ fn add_asset_constraints( add_fixed_asset_constraints(problem, variables, assets, &model.time_slice_info); // Return constraint keys which are required to calculate commodity prices - (commodity_constraint_keys, capacity_constraint_keys) + (commodity_balance_constraint_keys, capacity_constraint_keys) } /// Add asset-level input-output commodity balances. @@ -338,7 +337,7 @@ fn add_commodity_balance_constraints( model: &Model, assets: &AssetPool, year: u32, -) -> CommodityConstraintKeys { +) -> CommodityBalanceConstraintKeys { // Sanity check: we rely on the first n values of the dual row values corresponding to the // commodity constraints, so these must be the first rows assert!( @@ -347,7 +346,7 @@ fn add_commodity_balance_constraints( ); let mut terms = Vec::new(); - let mut keys = CommodityConstraintKeys::new(); + let mut keys = CommodityBalanceConstraintKeys::new(); for commodity in model.commodities.values() { if commodity.kind != CommodityType::SupplyEqualsDemand && commodity.kind != CommodityType::ServiceDemand @@ -457,12 +456,12 @@ fn add_asset_capacity_constraints( variables: &VariableMap, assets: &AssetPool, time_slice_info: &TimeSliceInfo, - commodity_constraint_keys: &CommodityConstraintKeys, + commodity_balance_constraint_keys: &CommodityBalanceConstraintKeys, ) -> CapacityConstraintKeys { // Sanity check: we rely on the dual rows corresponding to the capacity constraints being - // immediately after the commodity constraints in `problem`. + // immediately after the commodity balance constraints in `problem`. assert!( - problem.num_rows() == commodity_constraint_keys.len(), + problem.num_rows() == commodity_balance_constraint_keys.len(), "Capacity constraints must be added immediately after commodity constraints." ); From 9bd70de8f87bd1b7ab5fce10ca8fa2f34382471b Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 13 Mar 2025 15:22:34 +0000 Subject: [PATCH 5/9] Clarify imline comments --- src/simulation/prices.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/simulation/prices.rs b/src/simulation/prices.rs index b7c5e448a..b1a64aa11 100644 --- a/src/simulation/prices.rs +++ b/src/simulation/prices.rs @@ -73,13 +73,13 @@ impl CommodityPrices { } } - // Insert the highest duals into the prices map + // Insert the highest capacity duals into the prices map for ((commodity_id, time_slice), dual) in highest_duals.iter() { self.insert(commodity_id, time_slice, *dual); commodities_updated.insert(Rc::clone(commodity_id)); } - // Combine with commodity balance duals + // Sum with the commodity balance duals for (commodity_id, time_slice, dual) in solution.iter_commodity_balance_duals() { let key = (Rc::clone(commodity_id), time_slice.clone()); let _combined_dual = self From c113ae41bce88a9f5bf6625b8da8924fbdd7e3f8 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 14 Mar 2025 09:31:09 +0000 Subject: [PATCH 6/9] Change order of dual insertion --- src/simulation/prices.rs | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/simulation/prices.rs b/src/simulation/prices.rs index b1a64aa11..49b5f044d 100644 --- a/src/simulation/prices.rs +++ b/src/simulation/prices.rs @@ -48,13 +48,11 @@ impl CommodityPrices { // Calculate highest capacity dual for each commodity/timeslice let mut highest_duals: IndexMap = IndexMap::new(); for (asset_id, time_slice, dual) in solution.iter_capacity_duals() { - // Get the asset let asset = assets.get(asset_id).unwrap(); // Iterate over process pacs let process_pacs = asset.process.iter_pacs(); for pac in process_pacs { - // Get the commodity let commodity = &pac.commodity; // If the commodity flow is positive (produced PAC) @@ -73,23 +71,18 @@ impl CommodityPrices { } } - // Insert the highest capacity duals into the prices map - for ((commodity_id, time_slice), dual) in highest_duals.iter() { - self.insert(commodity_id, time_slice, *dual); + // Insert commodity balance duals into the prices map + for (commodity_id, time_slice, price) in solution.iter_commodity_balance_duals() { + self.insert(commodity_id, time_slice, price); commodities_updated.insert(Rc::clone(commodity_id)); } - // Sum with the commodity balance duals - for (commodity_id, time_slice, dual) in solution.iter_commodity_balance_duals() { + // Add the highest capacity dual for each commodity/timeslice + for ((commodity_id, time_slice), dual) in highest_duals.iter() { let key = (Rc::clone(commodity_id), time_slice.clone()); - let _combined_dual = self - .0 - .entry(key) - .and_modify(|current_dual| { - *current_dual += dual; - }) - .or_insert(dual); - commodities_updated.insert(Rc::clone(commodity_id)); + if let Some(current_price) = self.0.get_mut(&key) { + *current_price += dual; + } } commodities_updated From fe0b628a82fc659ab8a4a93d6f0772aa208b01fe Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 14 Mar 2025 15:43:55 +0000 Subject: [PATCH 7/9] Reorder code for readability --- src/simulation/prices.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/simulation/prices.rs b/src/simulation/prices.rs index 49b5f044d..d827fd128 100644 --- a/src/simulation/prices.rs +++ b/src/simulation/prices.rs @@ -35,9 +35,13 @@ impl CommodityPrices { /// Add commodity prices for which there are values in the solution /// + /// Commodity prices are calculated as the sum of the commodity balance duals and the highest + /// capacity dual for each commodity/timeslice. + /// /// # Arguments /// /// * `solution` - The solution to the dispatch optimisation + /// * `assets` - The asset pool /// /// # Returns /// @@ -45,6 +49,12 @@ impl CommodityPrices { fn add_from_solution(&mut self, solution: &Solution, assets: &AssetPool) -> HashSet> { let mut commodities_updated = HashSet::new(); + // Insert commodity balance duals into the prices map + for (commodity_id, time_slice, price) in solution.iter_commodity_balance_duals() { + self.insert(commodity_id, time_slice, price); + commodities_updated.insert(Rc::clone(commodity_id)); + } + // Calculate highest capacity dual for each commodity/timeslice let mut highest_duals: IndexMap = IndexMap::new(); for (asset_id, time_slice, dual) in solution.iter_capacity_duals() { @@ -71,12 +81,6 @@ impl CommodityPrices { } } - // Insert commodity balance duals into the prices map - for (commodity_id, time_slice, price) in solution.iter_commodity_balance_duals() { - self.insert(commodity_id, time_slice, price); - commodities_updated.insert(Rc::clone(commodity_id)); - } - // Add the highest capacity dual for each commodity/timeslice for ((commodity_id, time_slice), dual) in highest_duals.iter() { let key = (Rc::clone(commodity_id), time_slice.clone()); From 38f2a75ce1448689f6548b2add83b6a72c242d7a Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Mon, 17 Mar 2025 10:29:13 +0000 Subject: [PATCH 8/9] Combine loops for price calculation --- src/simulation/prices.rs | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/simulation/prices.rs b/src/simulation/prices.rs index d827fd128..25f97da4a 100644 --- a/src/simulation/prices.rs +++ b/src/simulation/prices.rs @@ -5,7 +5,7 @@ use crate::model::Model; use crate::time_slice::{TimeSliceID, TimeSliceInfo}; use indexmap::IndexMap; use log::warn; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::rc::Rc; /// A combination of commodity ID and time slice @@ -49,14 +49,8 @@ impl CommodityPrices { fn add_from_solution(&mut self, solution: &Solution, assets: &AssetPool) -> HashSet> { let mut commodities_updated = HashSet::new(); - // Insert commodity balance duals into the prices map - for (commodity_id, time_slice, price) in solution.iter_commodity_balance_duals() { - self.insert(commodity_id, time_slice, price); - commodities_updated.insert(Rc::clone(commodity_id)); - } - // Calculate highest capacity dual for each commodity/timeslice - let mut highest_duals: IndexMap = IndexMap::new(); + let mut highest_duals = HashMap::new(); for (asset_id, time_slice, dual) in solution.iter_capacity_duals() { let asset = assets.get(asset_id).unwrap(); @@ -81,12 +75,12 @@ impl CommodityPrices { } } - // Add the highest capacity dual for each commodity/timeslice - for ((commodity_id, time_slice), dual) in highest_duals.iter() { + // Add the highest capacity dual for each commodity/timeslice to each commodity balance dual + for (commodity_id, time_slice, dual) in solution.iter_commodity_balance_duals() { let key = (Rc::clone(commodity_id), time_slice.clone()); - if let Some(current_price) = self.0.get_mut(&key) { - *current_price += dual; - } + let price = dual + highest_duals.get(&key).unwrap_or(&0.0); + self.insert(commodity_id, time_slice, price); + commodities_updated.insert(Rc::clone(commodity_id)); } commodities_updated From cb1d4482bcba60df0e365d2df300b1a6976403c8 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Mon, 17 Mar 2025 12:17:39 +0000 Subject: [PATCH 9/9] Disable link check --- docs/model_description.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/model_description.md b/docs/model_description.md index 420eb25fa..f48433cb3 100644 --- a/docs/model_description.md +++ b/docs/model_description.md @@ -16,6 +16,7 @@ transition, usually in the context of climate change mitigation. ### Model Scope + MUSE is an [Integrated Assessment Modelling](https://unfccc.int/topics/mitigation/workstreams/response-measures/modelling-tools-to-assess-the-impact-of-the-implementation-of-response-measures/integrated-assessment-models-iams-and-energy-environment-economy-e3-models) framework that is designed to enable users to create and apply an agent-based model that simulates a market equilibrium on a set of user-defined commodities, over a user-defined time period, for a