From 63e184fd8b6bcd9a1bdf27a2f46d8980f468cd06 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 19 Aug 2026 10:42:13 +0100 Subject: [PATCH 1/4] Rough implementation for total capacity limits --- src/process.rs | 12 +++++++----- src/simulation/investment.rs | 23 ++++++++++++++++++++--- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/process.rs b/src/process.rs index 8cbc38093..6778c8e0d 100644 --- a/src/process.rs +++ b/src/process.rs @@ -505,17 +505,19 @@ pub struct ProcessInvestmentConstraint { /// Addition constraint: Limit an agent can invest in the process, shared according to the /// agent's proportion of the process's primary commodity demand pub addition_limit: Option, + /// Total capacity limit for the process + pub total_limit: Option, } impl ProcessInvestmentConstraint { - /// Calculate the effective addition limit - /// - /// For now, this just returns `addition_limit`, but in the future when we add growth - /// limits and total capacity limits, this will have more complex logic which will depend on the - /// current total capacity. + /// Get the addition limit pub fn get_addition_limit(&self) -> Option { self.addition_limit } + + pub fn get_total_limit(&self) -> Option { + self.total_limit + } } #[cfg(test)] diff --git a/src/simulation/investment.rs b/src/simulation/investment.rs index 3f12bd1f5..f60afa411 100644 --- a/src/simulation/investment.rs +++ b/src/simulation/investment.rs @@ -534,7 +534,8 @@ fn remove_candidates_exceeding_limits( fn update_assets( best_asset: AssetRef, opt_assets: &mut Vec, - remaining_candidate_capacities: &mut HashMap, + remaining_addition_limit: &mut HashMap, + remaining_total_limit: &mut HashMap, remaining_units: &mut HashMap, best_assets: &mut Vec, ) { @@ -543,11 +544,27 @@ fn update_assets( "Invalid asset type" ); + // Remove capacity from the total capacity limit, if applicable. + if let Some(remaining_capacity) = remaining_total_limit.get_mut(&best_asset.process_id()) { + *remaining_capacity -= best_asset.total_capacity(); + + // If there's not enough capacity remaining to install any more units, remove the + // asset from the investment options. + if *remaining_capacity < best_asset.total_capacity() { + let old_idx = opt_assets + .iter() + .position(|asset| *asset == best_asset) + .unwrap(); + opt_assets.swap_remove(old_idx); + remaining_total_limit.remove(&best_asset.process_id()); + } + } + // Update the remaining limits for the selected asset, if applicable, and remove it from the // options if the limit is exhausted. if best_asset.is_candidate() { // Candidate assets: remove capacity from the investment limit, if applicable. - if let Some(remaining_capacity) = remaining_candidate_capacities.get_mut(&best_asset) { + if let Some(remaining_capacity) = remaining_addition_limit.get_mut(&best_asset) { *remaining_capacity -= best_asset.total_capacity(); // If there's not enough capacity remaining to install any more units, remove the @@ -558,7 +575,7 @@ fn update_assets( .position(|asset| *asset == best_asset) .unwrap(); opt_assets.swap_remove(old_idx); - remaining_candidate_capacities.remove(&best_asset); + remaining_addition_limit.remove(&best_asset); } } } else { From ee5f7e96787e364d25b44c4ced1c48914f1e24ab Mon Sep 17 00:00:00 2001 From: Adrian D'Alessandro Date: Wed, 19 Aug 2026 17:21:17 +0100 Subject: [PATCH 2/4] Enforce total capacity limit constraint in investment --- src/asset.rs | 40 +++++++++++++++++++++ src/input/process/investment_constraints.rs | 5 ++- src/process.rs | 5 +-- src/simulation/investment.rs | 20 +++++++---- src/simulation/market.rs | 22 ++++++++++++ 5 files changed, 82 insertions(+), 10 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index 5c1c04ed4..2de3678e4 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -866,6 +866,23 @@ impl Asset { .get(&(self.region_id.clone(), self.commission_year)) .and_then(|c| c.get_addition_limit().map(|l| l * commodity_portion)) } + + /// For all assets, get the maximum capacity permitted to be installed based on the investment + /// constraints for the asset's process. + /// + /// The limit is taken from the process's investment constraints for the asset's region and + /// commission year, and the portion of the commodity demand being considered. + pub fn max_possible_capacity(&self, commodity_portion: Dimensionless) -> Option { + assert!( + commodity_portion >= Dimensionless(0.0) && commodity_portion <= Dimensionless(1.0), + "commodity_portion must be between 0 and 1 inclusive" + ); + + self.process + .investment_constraints + .get(&(self.region_id.clone(), self.commission_year)) + .and_then(|c| c.get_total_limit().map(|l| l * commodity_portion)) + } } #[allow(clippy::missing_fields_in_debug)] @@ -1632,6 +1649,7 @@ mod tests { (region_id.clone(), 2015), Arc::new(crate::process::ProcessInvestmentConstraint { addition_limit: Some(Capacity(3.0)), + total_capacity_limit: Some(Capacity(100.0)), }), ); let process_rc = Arc::new(process); @@ -1646,6 +1664,28 @@ mod tests { assert_eq!(result, Some(Capacity(1.5))); } + #[rstest] + fn max_possible_capacity(mut process: Process, region_id: RegionID) { + // Set a total limit of 100 for (region, year 2015) + process.investment_constraints.insert( + (region_id.clone(), 2015), + Arc::new(crate::process::ProcessInvestmentConstraint { + addition_limit: Some(Capacity(3.0)), + total_capacity_limit: Some(Capacity(100.0)), + }), + ); + let process_rc = Arc::new(process); + + // Create a candidate asset with commission year 2015 + let asset = + Asset::new_candidate(process_rc.clone(), region_id.clone(), Capacity(1.0), 2015) + .unwrap(); + + // commodity_portion = 0.5 -> limit = 100 * 0.5 = 50.0 + let result = asset.max_possible_capacity(Dimensionless(0.5)); + assert_eq!(result, Some(Capacity(50.0))); + } + #[rstest] #[case::none(0)] #[case::some(2)] diff --git a/src/input/process/investment_constraints.rs b/src/input/process/investment_constraints.rs index c2c342fd4..f8054addd 100644 --- a/src/input/process/investment_constraints.rs +++ b/src/input/process/investment_constraints.rs @@ -199,7 +199,10 @@ where .addition_limit .map(|limit| limit * Year(years_since_prev as f64)); - let constraint = Arc::new(ProcessInvestmentConstraint { addition_limit }); + let constraint = Arc::new(ProcessInvestmentConstraint { + addition_limit, + total_capacity_limit: record.total_capacity_limit, + }); try_insert(process_map, &(region.clone(), year), constraint.clone())?; } diff --git a/src/process.rs b/src/process.rs index 6778c8e0d..34806b8d0 100644 --- a/src/process.rs +++ b/src/process.rs @@ -506,7 +506,7 @@ pub struct ProcessInvestmentConstraint { /// agent's proportion of the process's primary commodity demand pub addition_limit: Option, /// Total capacity limit for the process - pub total_limit: Option, + pub total_capacity_limit: Option, } impl ProcessInvestmentConstraint { @@ -515,8 +515,9 @@ impl ProcessInvestmentConstraint { self.addition_limit } + /// Get the total capacity limit allowed pub fn get_total_limit(&self) -> Option { - self.total_limit + self.total_capacity_limit } } diff --git a/src/simulation/investment.rs b/src/simulation/investment.rs index f60afa411..42b12ba74 100644 --- a/src/simulation/investment.rs +++ b/src/simulation/investment.rs @@ -5,6 +5,7 @@ use crate::asset::{Asset, AssetRef}; use crate::commodity::{Commodity, CommodityID, CommodityMap}; use crate::model::Model; use crate::output::DataWriter; +use crate::process::ProcessID; use crate::region::RegionID; use crate::simulation::prices::Prices; use crate::time_slice::{TimeSliceID, TimeSliceInfo, TimeSliceLevel, TimeSliceSelection}; @@ -353,6 +354,7 @@ pub fn select_best_assets( model: &Model, mut opt_assets: Vec, candidate_investment_limits: HashMap, + process_capacity_limits: HashMap, commodity: &Commodity, agent: &Agent, region_id: &RegionID, @@ -364,8 +366,11 @@ pub fn select_best_assets( let objective_type = &agent.objectives[&year]; // Remaining capacity limits for candidate assets - let mut remaining_candidate_capacities = candidate_investment_limits; - remove_candidates_exceeding_limits(&mut opt_assets, &remaining_candidate_capacities); + let mut remaining_addition_limit = candidate_investment_limits; + remove_candidates_exceeding_limits(&mut opt_assets, &remaining_addition_limit); + + // Remaining total capacity for all assets + let mut remaining_total_limit = process_capacity_limits; // Store unit counts for commissioned assets and replace them with single units let mut remaining_units = prepare_commissioned_assets_for_reappraisal(&mut opt_assets); @@ -456,7 +461,8 @@ pub fn select_best_assets( update_assets( best_output.asset, &mut opt_assets, - &mut remaining_candidate_capacities, + &mut remaining_addition_limit, + &mut remaining_total_limit, &mut remaining_units, &mut best_assets, ); @@ -519,7 +525,7 @@ fn remove_candidates_exceeding_limits( /// Add the best asset to the list of selected assets, and update the remaining limits and options /// accordingly. /// -/// If the asset is a candidate, its capacity is subtracted from `remaining_candidate_capacities` +/// If the asset is a candidate, its capacity is subtracted from `remaining_addition_limit` /// (if applicable). This is to ensure that any annual addition limits are not exceeded. If the /// asset is a commissioned asset, one unit is subtracted from `remaining_units`. This is to ensure /// that we do not select more units than are available for retention. @@ -528,7 +534,7 @@ fn remove_candidates_exceeding_limits( /// /// * `best_asset` - The asset that has been selected as the best option in this round /// * `opt_assets` - The list of remaining asset options to be considered in future rounds -/// * `remaining_candidate_capacities` - The remaining investment limits for candidate assets +/// * `remaining_addition_limit` - The remaining investment limits for candidate assets /// * `remaining_units` - The remaining unit counts for commissioned assets /// * `best_assets` - The list of assets that have been selected so far fn update_assets( @@ -545,7 +551,7 @@ fn update_assets( ); // Remove capacity from the total capacity limit, if applicable. - if let Some(remaining_capacity) = remaining_total_limit.get_mut(&best_asset.process_id()) { + if let Some(remaining_capacity) = remaining_total_limit.get_mut(best_asset.process_id()) { *remaining_capacity -= best_asset.total_capacity(); // If there's not enough capacity remaining to install any more units, remove the @@ -556,7 +562,7 @@ fn update_assets( .position(|asset| *asset == best_asset) .unwrap(); opt_assets.swap_remove(old_idx); - remaining_total_limit.remove(&best_asset.process_id()); + remaining_total_limit.remove(best_asset.process_id()); } } diff --git a/src/simulation/market.rs b/src/simulation/market.rs index d44f983f7..708a63349 100644 --- a/src/simulation/market.rs +++ b/src/simulation/market.rs @@ -5,6 +5,7 @@ use crate::asset::{Asset, AssetCapacity, AssetIterator, AssetRef, AssetState}; use crate::commodity::{Commodity, CommodityID}; use crate::model::Model; use crate::output::DataWriter; +use crate::process::ProcessID; use crate::region::RegionID; use crate::simulation::investment::{ AllDemandMap, DemandMap, calculate_candidate_asset_capacity_scale, select_best_assets, @@ -196,11 +197,15 @@ pub fn select_assets_for_single_market( let candidate_investment_limits = collect_investment_limits_for_candidates(&opt_assets, commodity_portion); + // Calculate total capacity limits for this agent + let process_capacity_limits = collect_total_limits(&opt_assets, commodity_portion); + // Choose assets from among existing pool and candidates let best_assets = select_best_assets( model, opt_assets, candidate_investment_limits, + process_capacity_limits, commodity, agent, region_id, @@ -484,6 +489,22 @@ pub fn collect_investment_limits_for_candidates( .collect() } +/// Calculates the total capacity limits for all processes in this list of assets, scaled according +/// to the agent's portion of the commodity demand. +pub fn collect_total_limits( + opt_assets: &[AssetRef], + commodity_portion: Dimensionless, +) -> HashMap { + opt_assets + .iter() + .filter_map(|asset| { + asset + .max_possible_capacity(commodity_portion) + .map(|limit_capacity| (asset.process_id().clone(), limit_capacity)) + }) + .collect() +} + #[cfg(test)] mod tests { use super::*; @@ -536,6 +557,7 @@ mod tests { (region_id.clone(), 2015), Arc::new(ProcessInvestmentConstraint { addition_limit: Some(Capacity(10.0)), + total_capacity_limit: Some(Capacity(100.0)), }), ); From 8d8bb3698345337f636a995651102b903c1c9be8 Mon Sep 17 00:00:00 2001 From: Adrian D'Alessandro Date: Wed, 19 Aug 2026 17:58:05 +0100 Subject: [PATCH 3/4] Add parameter to docstring --- src/simulation/investment.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/simulation/investment.rs b/src/simulation/investment.rs index 42b12ba74..c48c0623e 100644 --- a/src/simulation/investment.rs +++ b/src/simulation/investment.rs @@ -535,6 +535,7 @@ fn remove_candidates_exceeding_limits( /// * `best_asset` - The asset that has been selected as the best option in this round /// * `opt_assets` - The list of remaining asset options to be considered in future rounds /// * `remaining_addition_limit` - The remaining investment limits for candidate assets +/// * `remaining_total_limit` - The remaining capacity for processes /// * `remaining_units` - The remaining unit counts for commissioned assets /// * `best_assets` - The list of assets that have been selected so far fn update_assets( From 98c69b866cf95d73b71fe86450cbbad87966ae21 Mon Sep 17 00:00:00 2001 From: Adrian D'Alessandro Date: Thu, 20 Aug 2026 17:22:47 +0100 Subject: [PATCH 4/4] Use process-first approach for determining agent total limits --- src/asset.rs | 21 +------ src/process.rs | 114 ++++++++++++++++++++++++++++++++--- src/simulation/investment.rs | 14 ++--- src/simulation/market.rs | 88 +++++++++++++++++---------- 4 files changed, 169 insertions(+), 68 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index 0303bf387..9d8a7ba7a 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -9,8 +9,8 @@ use crate::region::RegionID; use crate::simulation::PriceMap; use crate::time_slice::{TimeSliceID, TimeSliceSelection}; use crate::units::{ - Activity, ActivityPerCapacity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity, - MoneyPerCapacity, MoneyPerFlow, UnitType, Year, + Activity, ActivityPerCapacity, Capacity, FlowPerActivity, MoneyPerActivity, MoneyPerCapacity, + MoneyPerFlow, UnitType, Year, }; use anyhow::{Context, Result, ensure}; use indexmap::IndexMap; @@ -845,23 +845,6 @@ impl Asset { pub fn num_units(&self) -> u32 { self.capacity().num_units() } - - /// For all assets, get the maximum capacity permitted to be installed based on the investment - /// constraints for the asset's process. - /// - /// The limit is taken from the process's investment constraints for the asset's region and - /// commission year, and the portion of the commodity demand being considered. - pub fn max_possible_capacity(&self, commodity_portion: Dimensionless) -> Option { - assert!( - commodity_portion >= Dimensionless(0.0) && commodity_portion <= Dimensionless(1.0), - "commodity_portion must be between 0 and 1 inclusive" - ); - - self.process - .investment_constraints - .get(&(self.region_id.clone(), self.commission_year)) - .and_then(|c| c.get_total_limit().map(|l| l * commodity_portion)) - } } #[allow(clippy::missing_fields_in_debug)] diff --git a/src/process.rs b/src/process.rs index 5ffa2e451..33f2fc3bb 100644 --- a/src/process.rs +++ b/src/process.rs @@ -77,13 +77,14 @@ impl Process { self.years.contains(&year) } - /// Calculate an agent's share of the addition limit for this process in a region and year - /// based on commodity portion. - pub fn agent_addition_limit( + /// Calculate an agent's share of any limit for this process in a region and year based on + /// commodity portion. + fn agent_limit( &self, region_id: &RegionID, commission_year: u32, commodity_portion: Dimensionless, + get_limit: fn(&ProcessInvestmentConstraint) -> Option, ) -> Option { assert!( commodity_portion >= Dimensionless(0.0) && commodity_portion <= Dimensionless(1.0), @@ -92,11 +93,39 @@ impl Process { self.investment_constraints .get(&(region_id.clone(), commission_year)) - .and_then(|constraint| { - constraint - .get_addition_limit() - .map(|limit| limit * commodity_portion) - }) + .and_then(|constraint| get_limit(constraint).map(|limit| limit * commodity_portion)) + } + + /// Calculate an agent's share of the addition limit for this process in a region and year + /// based on commodity portion. + pub fn agent_addition_limit( + &self, + region_id: &RegionID, + commission_year: u32, + commodity_portion: Dimensionless, + ) -> Option { + self.agent_limit( + region_id, + commission_year, + commodity_portion, + ProcessInvestmentConstraint::get_addition_limit, + ) + } + + /// Calculate an agent's share of the total limit for this process in a region and year + /// based on commodity portion. + pub fn agent_total_limit( + &self, + region_id: &RegionID, + commission_year: u32, + commodity_portion: Dimensionless, + ) -> Option { + self.agent_limit( + region_id, + commission_year, + commodity_portion, + ProcessInvestmentConstraint::get_total_limit, + ) } } @@ -547,7 +576,7 @@ impl ProcessInvestmentConstraint { mod tests { use super::*; use crate::commodity::{CommodityLevyMap, CommodityType, DemandMap, PricingStrategy}; - use crate::fixture::{assert_error, region_id, time_slice, time_slice_info2}; + use crate::fixture::{assert_error, process, region_id, time_slice, time_slice_info2}; use crate::time_slice::TimeSliceLevel; use crate::time_slice::TimeSliceSelection; use float_cmp::assert_approx_eq; @@ -1159,4 +1188,71 @@ mod tests { "Availability limit for season winter clashes with time slice limits" ); } + + #[rstest] + #[case(Dimensionless(1.1))] + #[case(Dimensionless(-0.1))] + #[should_panic(expected = "commodity_portion must be between 0 and 1 inclusive")] + fn agent_limit_invalid_commodity_portion( + process: Process, + region_id: RegionID, + #[case] commodity_portion: Dimensionless, + ) { + process.agent_limit( + ®ion_id, + 2015, + commodity_portion, + ProcessInvestmentConstraint::get_addition_limit, + ); + } + + #[rstest] + fn agent_addition_limit_no_constraint(process: Process, region_id: RegionID) { + assert!( + process + .agent_addition_limit(®ion_id, 2015, Dimensionless(1.0)) + .is_none() + ); + } + + #[rstest] + fn agent_addition_limit_scaled_by_portion(mut process: Process, region_id: RegionID) { + process.investment_constraints.insert( + (region_id.clone(), 2015), + Arc::new(ProcessInvestmentConstraint { + addition_limit: Some(crate::units::Capacity(10.0)), + total_capacity_limit: Some(crate::units::Capacity(100.0)), + }), + ); + + let result = process + .agent_addition_limit(®ion_id, 2015, Dimensionless(0.5)) + .unwrap(); + assert_eq!(result, Capacity(5.0)); + } + + #[rstest] + fn agent_total_limit_no_constraint(process: Process, region_id: RegionID) { + assert!( + process + .agent_total_limit(®ion_id, 2015, Dimensionless(1.0)) + .is_none() + ); + } + + #[rstest] + fn agent_total_limit_scaled_by_portion(mut process: Process, region_id: RegionID) { + process.investment_constraints.insert( + (region_id.clone(), 2015), + Arc::new(ProcessInvestmentConstraint { + addition_limit: Some(crate::units::Capacity(10.0)), + total_capacity_limit: Some(crate::units::Capacity(100.0)), + }), + ); + + let result = process + .agent_total_limit(®ion_id, 2015, Dimensionless(0.5)) + .unwrap(); + assert_eq!(result, Capacity(50.0)); + } } diff --git a/src/simulation/investment.rs b/src/simulation/investment.rs index f4e5ba8c5..0525f0ed5 100644 --- a/src/simulation/investment.rs +++ b/src/simulation/investment.rs @@ -354,7 +354,7 @@ pub fn select_best_assets( model: &Model, mut opt_assets: Vec, agent_addition_limits: HashMap, - process_capacity_limits: HashMap, + agent_total_limits: HashMap, commodity: &Commodity, agent: &Agent, region_id: &RegionID, @@ -374,7 +374,7 @@ pub fn select_best_assets( ); // Remaining total capacity for all assets - let mut remaining_total_limit = process_capacity_limits; + let mut remaining_agent_total_limit = agent_total_limits; // Store commissioned units available for retention and replace assets with single units let mut available_retention_units = prepare_commissioned_assets_for_retention(&mut opt_assets); @@ -466,7 +466,7 @@ pub fn select_best_assets( best_output.asset, &mut opt_assets, &mut remaining_agent_addition_limits, - &mut remaining_total_limit, + &mut remaining_agent_total_limit, &mut available_retention_units, &mut best_assets, ); @@ -538,14 +538,14 @@ fn remove_candidates_exceeding_agent_addition_limits( /// * `best_asset` - The asset that has been selected as the best option in this round /// * `opt_assets` - The list of remaining asset options to be considered in future rounds /// * `remaining_agent_addition_limits` - The remaining agent addition limits for processes -/// * `remaining_total_limit` - The remaining capacity for processes +/// * `remaining_agent_total_limit` - The remaining capacity for processes /// * `available_retention_units` - The commissioned units available for retention /// * `best_assets` - The list of assets that have been selected so far fn record_asset_selection( best_asset: AssetRef, opt_assets: &mut Vec, remaining_agent_addition_limits: &mut HashMap, - remaining_total_limit: &mut HashMap, + remaining_agent_total_limit: &mut HashMap, available_retention_units: &mut HashMap, best_assets: &mut Vec, ) { @@ -555,7 +555,7 @@ fn record_asset_selection( ); // Remove capacity from the total capacity limit, if applicable. - if let Some(remaining_capacity) = remaining_total_limit.get_mut(best_asset.process_id()) { + if let Some(remaining_capacity) = remaining_agent_total_limit.get_mut(best_asset.process_id()) { *remaining_capacity -= best_asset.total_capacity(); // If there's not enough capacity remaining to install any more units, remove the @@ -566,7 +566,7 @@ fn record_asset_selection( .position(|asset| *asset == best_asset) .unwrap(); opt_assets.swap_remove(old_idx); - remaining_total_limit.remove(best_asset.process_id()); + remaining_agent_total_limit.remove(best_asset.process_id()); } } diff --git a/src/simulation/market.rs b/src/simulation/market.rs index f9da2045b..d76defb85 100644 --- a/src/simulation/market.rs +++ b/src/simulation/market.rs @@ -5,7 +5,7 @@ use crate::asset::{Asset, AssetCapacity, AssetIterator, AssetRef, AssetState}; use crate::commodity::{Commodity, CommodityID}; use crate::model::Model; use crate::output::DataWriter; -use crate::process::ProcessID; +use crate::process::{Process, ProcessID}; use crate::region::RegionID; use crate::simulation::investment::{ AllDemandMap, DemandMap, calculate_candidate_asset_capacity_scale, select_best_assets, @@ -194,18 +194,31 @@ pub fn select_assets_for_single_market( .collect::>(); // Calculate the agent's share of addition limits for candidate processes - let agent_addition_limits = - collect_agent_addition_limits(agent, region_id, &commodity.id, year, commodity_portion); + let agent_addition_limits = collect_agent_limits( + agent, + region_id, + commodity_id, + year, + commodity_portion, + Process::agent_addition_limit, + ); - // Calculate total capacity limits for this agent - let process_capacity_limits = collect_total_limits(&opt_assets, commodity_portion); + // Calculate the agent's share of total capacity limits for all processes + let agent_total_limits = collect_agent_limits( + agent, + region_id, + commodity_id, + year, + commodity_portion, + Process::agent_total_limit, + ); // Choose assets from among existing pool and candidates let best_assets = select_best_assets( model, opt_assets, agent_addition_limits, - process_capacity_limits, + agent_total_limits, commodity, agent, region_id, @@ -472,38 +485,34 @@ fn get_candidate_assets<'a>( }) } -/// Agent addition limits are based on process addition limits that have already been -/// scaled from annual addition limits to the interval since the previous milestone year. -/// The resulting limit is then scaled according to the agent's portion of commodity demand. -pub fn collect_agent_addition_limits( +/// Collects capacity limits for all processes in the agent's search space for a given market. +/// +/// Processes without a defined limit are excluded from the returned map. The limit type is +/// determined by `get_agent_limit`, which should be one of [`Process::agent_addition_limit`] (the +/// agent's share of the annual addition limit) or [`Process::agent_total_limit`] (the agent's share +/// of the maximum total installed capacity). +/// +/// # Arguments +/// +/// * `agent` – Agent whose search space is queried. +/// * `region_id` – Region for which limits are calculated. +/// * `commodity_id` – Commodity for which limits are calculated. +/// * `year` – Milestone year being solved. +/// * `commodity_portion` – Agent's fractional share of commodity demand, used to scale limits. +/// * `get_agent_limit` – Method on [`Process`] that returns the limit value. +fn collect_agent_limits( agent: &Agent, region_id: &RegionID, commodity_id: &CommodityID, year: u32, commodity_portion: Dimensionless, + get_agent_limit: fn(&Process, &RegionID, u32, Dimensionless) -> Option, ) -> HashMap { agent .iter_search_space(region_id, commodity_id, year) .filter_map(|process| { - process - .agent_addition_limit(region_id, year, commodity_portion) - .map(|agent_limit| (process.id.clone(), agent_limit)) - }) - .collect() -} - -/// Calculates the total capacity limits for all processes in this list of assets, scaled according -/// to the agent's portion of the commodity demand. -pub fn collect_total_limits( - opt_assets: &[AssetRef], - commodity_portion: Dimensionless, -) -> HashMap { - opt_assets - .iter() - .filter_map(|asset| { - asset - .max_possible_capacity(commodity_portion) - .map(|limit_capacity| (asset.process_id().clone(), limit_capacity)) + get_agent_limit(process, region_id, year, commodity_portion) + .map(|limit| (process.id.clone(), limit)) }) .collect() } @@ -542,7 +551,7 @@ mod tests { } #[rstest] - fn collect_agent_addition_limits_uses_search_space(mut process: Process, region_id: RegionID) { + fn collect_agent_limits_uses_search_space(mut process: Process, region_id: RegionID) { process.investment_constraints.insert( (region_id.clone(), 2015), Arc::new(ProcessInvestmentConstraint { @@ -554,19 +563,31 @@ mod tests { let process_id = process.id.clone(); let agent = agent_with_process(process, ®ion_id, &commodity_id); - let result = collect_agent_addition_limits( + let result = collect_agent_limits( &agent, ®ion_id, &commodity_id, 2015, Dimensionless(0.5), + Process::agent_addition_limit, ); assert_eq!(result.get(&process_id), Some(&crate::units::Capacity(5.0))); + + let result = collect_agent_limits( + &agent, + ®ion_id, + &commodity_id, + 2015, + Dimensionless(0.5), + Process::agent_total_limit, + ); + + assert_eq!(result.get(&process_id), Some(&crate::units::Capacity(50.0))); } #[rstest] - fn collect_agent_addition_limits_excludes_processes_without_limits( + fn collect_agent_limits_excludes_processes_without_limits( process: Process, region_id: RegionID, ) { @@ -574,12 +595,13 @@ mod tests { let process_id = process.id.clone(); let agent = agent_with_process(process, ®ion_id, &commodity_id); - let result = collect_agent_addition_limits( + let result = collect_agent_limits( &agent, ®ion_id, &commodity_id, 2015, Dimensionless(1.0), + Process::agent_addition_limit, ); assert!(!result.contains_key(&process_id));