diff --git a/Cargo.lock b/Cargo.lock index 86c9c1025..bc8bd11ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1082,6 +1082,7 @@ dependencies = [ "ordered-float", "petgraph", "platform-info", + "rayon", "rstest", "serde", "similar", diff --git a/Cargo.toml b/Cargo.toml index 51cd9a5b3..eb410248f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ edit = "0.1.5" erased-serde = "0.4.10" context_manager = "0.1.3" map-macro = "0.3.0" +rayon = "1.12.0" [dev-dependencies] assert_cmd = "2.2.2" diff --git a/src/agent.rs b/src/agent.rs index 5d5c42fac..db5e6d851 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -8,7 +8,7 @@ use crate::units::Dimensionless; use indexmap::{IndexMap, IndexSet}; use serde::Deserialize; use std::collections::HashMap; -use std::rc::Rc; +use std::sync::Arc; define_id_type! {AgentID, "agent ID"} @@ -19,7 +19,7 @@ pub type AgentMap = IndexMap; pub type AgentCommodityPortionsMap = HashMap<(CommodityID, u32), Dimensionless>; /// A map for the agent's search space, keyed by commodity, region, and year -pub type AgentSearchSpaceMap = HashMap<(CommodityID, RegionID, u32), Rc>>>; +pub type AgentSearchSpaceMap = HashMap<(CommodityID, RegionID, u32), Arc>>>; /// A map of objectives for an agent, keyed by year. /// @@ -59,7 +59,7 @@ impl Agent { region_id: &RegionID, commodity_id: &CommodityID, year: u32, - ) -> impl Iterator> { + ) -> impl Iterator> { self.search_space[&(commodity_id.clone(), region_id.clone(), year)].iter() } } diff --git a/src/asset.rs b/src/asset.rs index df238c4fb..821613de3 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -21,7 +21,7 @@ use std::cmp::Ordering; use std::collections::VecDeque; use std::hash::{Hash, Hasher}; use std::ops::RangeInclusive; -use std::rc::Rc; +use std::sync::Arc; mod capacity; pub use capacity::AssetCapacity; @@ -91,13 +91,13 @@ pub struct Asset { /// The status of the asset state: AssetState, /// The [`Process`] that this asset corresponds to - process: Rc, + process: Arc, /// Activity limits for this asset - activity_limits: Rc, + activity_limits: Arc, /// The commodity flows for this asset - flows: Rc>, + flows: Arc>, /// The [`ProcessParameter`] corresponding to the asset's region and commission year - process_parameter: Rc, + process_parameter: Arc, /// The region in which the asset is located region_id: RegionID, /// Capacity of asset (for candidates this is a hypothetical capacity which may be altered) @@ -111,7 +111,7 @@ pub struct Asset { impl Asset { /// Create a new candidate asset pub fn new_candidate( - process: Rc, + process: Arc, region_id: RegionID, capacity: Capacity, commission_year: u32, @@ -133,7 +133,7 @@ impl Asset { /// `candidate_asset_capacity`, regardless of whether the underlying process is divisible or /// not. pub fn new_candidate_for_dispatch( - process: Rc, + process: Arc, region_id: RegionID, capacity: Capacity, commission_year: u32, @@ -165,7 +165,7 @@ impl Asset { #[cfg(test)] pub fn new_ready( agent_id: AgentID, - process: Rc, + process: Arc, region_id: RegionID, capacity: Capacity, commission_year: u32, @@ -191,7 +191,7 @@ impl Asset { #[cfg(test)] pub fn new_commissioned( agent_id: AgentID, - process: Rc, + process: Arc, region_id: RegionID, capacity: Capacity, commission_year: u32, @@ -214,7 +214,7 @@ impl Asset { /// Private helper to create an asset with the given state fn new_with_state( state: AssetState, - process: Rc, + process: Arc, region_id: RegionID, capacity: AssetCapacity, commission_year: u32, @@ -884,7 +884,7 @@ impl UserAsset { /// Create a new [`UserAsset`] pub fn new( agent_id: AgentID, - process: Rc, + process: Arc, region_id: RegionID, capacity: Capacity, commission_year: u32, @@ -951,12 +951,12 @@ fn log_decommissioning(asset: &Asset, num_units: u32, reason: &str) { /// otherwise using a combination of other fields which should be unique at all the relevant points /// in the simulation. #[derive(Clone, Debug, derive_more::Deref, derive_more::From, derive_more::Into)] -pub struct AssetRef(#[deref(forward)] Rc); +pub struct AssetRef(#[deref(forward)] Arc); impl AssetRef { /// Make a mutable reference to the underlying [`Asset`] pub fn make_mut(&mut self) -> &mut Asset { - Rc::make_mut(&mut self.0) + Arc::make_mut(&mut self.0) } /// Get a representation of this [`AssetRef`] that can be used for comparisons @@ -1135,7 +1135,7 @@ impl AssetRef { impl From for AssetRef { fn from(value: Asset) -> Self { - Self::from(Rc::new(value)) + Self::from(Arc::new(value)) } } @@ -1235,7 +1235,7 @@ mod tests { use indexmap::indexmap; use itertools::assert_equal; use rstest::{fixture, rstest}; - use std::rc::Rc; + use std::sync::Arc; /// A commissioned divisible asset with three units. #[fixture] @@ -1253,20 +1253,20 @@ mod tests { time_slice: TimeSliceID, ) { // Update the process flows using the existing commodity fixture - let commodity_rc = Rc::new(svd_commodity); + let commodity_rc = Arc::new(svd_commodity); let process_flow = ProcessFlow { - commodity: Rc::clone(&commodity_rc), + commodity: Arc::clone(&commodity_rc), coeff: FlowPerActivity(-2.0), // Input kind: FlowType::Fixed, cost: MoneyPerFlow(0.0), }; let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() }; - let process_flows_map = process_flows_map(process.regions.clone(), Rc::new(process_flows)); + let process_flows_map = process_flows_map(process.regions.clone(), Arc::new(process_flows)); process.flows = process_flows_map; // Create asset - let asset = - Asset::new_candidate(Rc::new(process), region_id.clone(), Capacity(1.0), 2020).unwrap(); + let asset = Asset::new_candidate(Arc::new(process), region_id.clone(), Capacity(1.0), 2020) + .unwrap(); // Set input prices let mut input_prices = PriceMap::default(); @@ -1299,7 +1299,7 @@ mod tests { fn asset_with_activity_limits(process_with_activity_limits: Process) -> Asset { Asset::new_ready( "agent1".into(), - Rc::new(process_with_activity_limits), + Arc::new(process_with_activity_limits), "GBR".into(), Capacity(2.0), 2010, @@ -1407,14 +1407,14 @@ mod tests { assert_eq!(asset_subset.capacity().n_units(), Some(num_units)); assert_eq!(asset_subset.id(), asset.id()); assert_eq!(asset_subset.agent_id(), asset.agent_id()); - assert_eq!(Rc::ptr_eq(&asset_subset.0, &asset.0), expect_same_asset); + assert_eq!(Arc::ptr_eq(&asset_subset.0, &asset.0), expect_same_asset); assert_eq!(asset.capacity(), AssetCapacity::Discrete(3, Capacity(4.0))); } #[rstest] fn with_subset_of_units_non_divisible_asset(asset: Asset) { let asset = AssetRef::from(asset); - assert!(Rc::ptr_eq( + assert!(Arc::ptr_eq( &asset.0, &asset.clone().with_subset_of_units(1).0 )); @@ -1516,11 +1516,11 @@ mod tests { // Set an addition limit of 3 for (region, year 2015) process.investment_constraints.insert( (region_id.clone(), 2015), - Rc::new(crate::process::ProcessInvestmentConstraint { + Arc::new(crate::process::ProcessInvestmentConstraint { addition_limit: Some(Capacity(3.0)), }), ); - let process_rc = Rc::new(process); + let process_rc = Arc::new(process); // Create a candidate asset with commission year 2015 let asset = @@ -1625,7 +1625,7 @@ mod tests { let asset = commissioned_divisible.with_mothballed_units(2, Some(2020)); // Requesting the same number of mothballed units is a no-op (the year is ignored) let same = asset.clone().with_mothballed_units(2, Some(2099)); - assert!(Rc::ptr_eq(&asset.0, &same.0)); + assert!(Arc::ptr_eq(&asset.0, &same.0)); } #[rstest] @@ -1681,7 +1681,7 @@ mod tests { // `asset_divisble` has no mothballed units, so the original Rc is returned unchanged let asset = commissioned_divisible; let same = asset.clone().with_no_mothballed_units(); - assert!(Rc::ptr_eq(&asset.0, &same.0)); + assert!(Arc::ptr_eq(&asset.0, &same.0)); } #[rstest] @@ -1704,7 +1704,7 @@ mod tests { .clone() .with_decommission_mothballed(2025, 20) .unwrap(); - assert!(Rc::ptr_eq(&asset.0, &result.0)); + assert!(Arc::ptr_eq(&asset.0, &result.0)); } #[rstest] diff --git a/src/asset/pool.rs b/src/asset/pool.rs index bff188b28..f1c221c83 100644 --- a/src/asset/pool.rs +++ b/src/asset/pool.rs @@ -197,7 +197,7 @@ mod tests { use itertools::{Itertools, assert_equal}; use rstest::{fixture, rstest}; use std::iter; - use std::rc::Rc; + use std::sync::Arc; #[fixture] fn user_assets(mut process: Process) -> Vec { @@ -212,12 +212,12 @@ mod tests { let process_parameter_map = process_parameter_map(process.regions.clone(), process_param); process.parameters = process_parameter_map; - let rc_process = Rc::new(process); + let rc_process = Arc::new(process); [2020, 2010] .map(|year| { UserAsset::new( "agent1".into(), - Rc::clone(&rc_process), + Arc::clone(&rc_process), "GBR".into(), Capacity(1.0), year, @@ -346,11 +346,11 @@ mod tests { let original_count = asset_pool.assets.len(); // Create new non-commissioned assets - let process_rc = Rc::new(process); + let process_rc = Arc::new(process); let new_assets = vec![ Asset::new_ready( "agent2".into(), - Rc::clone(&process_rc), + Arc::clone(&process_rc), "GBR".into(), Capacity(1.5), 2015, @@ -359,7 +359,7 @@ mod tests { .into(), Asset::new_ready( "agent3".into(), - Rc::clone(&process_rc), + Arc::clone(&process_rc), "GBR".into(), Capacity(2.5), 2020, @@ -425,11 +425,11 @@ mod tests { 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); + let process_rc = Arc::new(process); let new_assets = vec![ Asset::new_ready( "agent_high_id".into(), - Rc::clone(&process_rc), + Arc::clone(&process_rc), "GBR".into(), Capacity(1.0), 2010, @@ -438,7 +438,7 @@ mod tests { .into(), Asset::new_ready( "agent_low_id".into(), - Rc::clone(&process_rc), + Arc::clone(&process_rc), "GBR".into(), Capacity(1.0), 2015, @@ -481,11 +481,11 @@ mod tests { assert_eq!(asset_pool.next_id, 2); // Should be 2 after commissioning 2 assets // Create new non-commissioned assets - let process_rc = Rc::new(process); + let process_rc = Arc::new(process); let new_assets = vec![ Asset::new_ready( "agent1".into(), - Rc::clone(&process_rc), + Arc::clone(&process_rc), "GBR".into(), Capacity(1.0), 2015, @@ -494,7 +494,7 @@ mod tests { .into(), Asset::new_ready( "agent2".into(), - Rc::clone(&process_rc), + Arc::clone(&process_rc), "GBR".into(), Capacity(1.0), 2020, diff --git a/src/commodity.rs b/src/commodity.rs index 80d8cb1a7..a7fa92edf 100644 --- a/src/commodity.rs +++ b/src/commodity.rs @@ -6,12 +6,12 @@ use crate::units::{Flow, MoneyPerFlow}; use indexmap::IndexMap; use serde::Deserialize; use std::collections::HashMap; -use std::rc::Rc; +use std::sync::Arc; define_id_type! {CommodityID, "commodity ID"} /// A map of [`Commodity`]s, keyed by commodity ID -pub type CommodityMap = IndexMap>; +pub type CommodityMap = IndexMap>; /// A map of [`MoneyPerFlow`]s, keyed by region ID, year and time slice ID for a specific levy pub type CommodityLevyMap = HashMap<(RegionID, u32, TimeSliceID), MoneyPerFlow>; diff --git a/src/fixture.rs b/src/fixture.rs index 7363820cf..02b98c8f2 100644 --- a/src/fixture.rs +++ b/src/fixture.rs @@ -30,7 +30,7 @@ use itertools::Itertools; use rstest::fixture; use std::collections::HashMap; use std::iter; -use std::rc::Rc; +use std::sync::Arc; /// Assert that an error with the given message occurs macro_rules! assert_error { @@ -215,7 +215,7 @@ pub fn asset_divisible(mut process: Process) -> Asset { process.unit_size = Some(Capacity(4.0)); Asset::new_ready( "agent1".into(), - Rc::new(process), + Arc::new(process), "GBR".into(), Capacity(11.0), 2010, @@ -248,7 +248,7 @@ pub fn process_parameter_map( region_ids: IndexSet, process_parameter: ProcessParameter, ) -> ProcessParameterMap { - let parameter = Rc::new(process_parameter); + let parameter = Arc::new(process_parameter); region_ids .into_iter() .cartesian_product(2010..=2020) @@ -271,28 +271,28 @@ pub fn process_activity_limits_map( region_ids .into_iter() .cartesian_product(2010..=2020) - .map(|(region_id, year)| ((region_id, year), Rc::new(process_activity_limits.clone()))) + .map(|(region_id, year)| ((region_id, year), Arc::new(process_activity_limits.clone()))) .collect() } #[fixture] /// Create an empty set of `ProcessInvestmentConstraints` for a given region/year -/// Returns a `HashMap` keyed by (`RegionID`, year) with empty Rc +/// Returns a `HashMap` keyed by (`RegionID`, year) with empty Arc pub fn process_investment_constraints() -> ProcessInvestmentConstraintsMap { HashMap::new() } #[fixture] /// Create an empty set of `ProcessFlows` for a given region/year -pub fn process_flows() -> Rc> { - Rc::new(IndexMap::new()) +pub fn process_flows() -> Arc> { + Arc::new(IndexMap::new()) } #[fixture] /// Create a `ProcessFlowsMap` with the provided flows for each region/year pub fn process_flows_map( region_ids: IndexSet, - process_flows: Rc>, + process_flows: Arc>, ) -> ProcessFlowsMap { region_ids .into_iter() @@ -406,7 +406,7 @@ pub fn appraisal_output(asset: Asset, time_slice: TimeSliceID) -> AppraisalOutpu let unmet_demand = indexmap! { time_slice.clone() => Flow(5.0) }; AppraisalOutput { asset: AssetRef::from(asset), - coefficients: Rc::new(ObjectiveCoefficients { + coefficients: Arc::new(ObjectiveCoefficients { activity_coefficients, market_costs, }), diff --git a/src/graph.rs b/src/graph.rs index 1f166ac77..def0ddb06 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -12,7 +12,7 @@ use std::collections::HashMap; use std::fs::File; use std::io::Write as IoWrite; use std::path::Path; -use std::rc::Rc; +use std::sync::Arc; pub mod investment; pub mod validate; @@ -61,7 +61,7 @@ pub enum GraphEdge { fn get_flow_for_year( process: &Process, target: (RegionID, u32), -) -> Option>> { +) -> Option>> { // If its already in the map, we return it if process.flows.contains_key(&target) { return process.flows.get(&target).cloned(); diff --git a/src/graph/investment.rs b/src/graph/investment.rs index 5d6ade42d..5f30ac1ec 100644 --- a/src/graph/investment.rs +++ b/src/graph/investment.rs @@ -504,7 +504,7 @@ mod tests { use crate::fixture::{sed_commodity, svd_commodity}; use petgraph::graph::Graph; use rstest::rstest; - use std::rc::Rc; + use std::sync::Arc; #[test] fn order_sccs_simple_cycle() { @@ -561,9 +561,9 @@ mod tests { // Create commodities map using fixtures let mut commodities = CommodityMap::new(); - commodities.insert("A".into(), Rc::new(sed_commodity.clone())); - commodities.insert("B".into(), Rc::new(sed_commodity)); - commodities.insert("C".into(), Rc::new(svd_commodity)); + commodities.insert("A".into(), Arc::new(sed_commodity.clone())); + commodities.insert("B".into(), Arc::new(sed_commodity)); + commodities.insert("C".into(), Arc::new(svd_commodity)); let graphs = IndexMap::from([(("GBR".into(), 2020), graph)]); let result = solve_investment_order_for_year(&graphs, &commodities, 2020); @@ -590,8 +590,8 @@ mod tests { // Create commodities map using fixtures let mut commodities = CommodityMap::new(); - commodities.insert("A".into(), Rc::new(sed_commodity.clone())); - commodities.insert("B".into(), Rc::new(sed_commodity)); + commodities.insert("A".into(), Arc::new(sed_commodity.clone())); + commodities.insert("B".into(), Arc::new(sed_commodity)); let graphs = IndexMap::from([(("GBR".into(), 2020), graph)]); let result = solve_investment_order_for_year(&graphs, &commodities, 2020); @@ -627,10 +627,10 @@ mod tests { // Create commodities map using fixtures let mut commodities = CommodityMap::new(); - commodities.insert("A".into(), Rc::new(sed_commodity.clone())); - commodities.insert("B".into(), Rc::new(sed_commodity.clone())); - commodities.insert("C".into(), Rc::new(sed_commodity)); - commodities.insert("D".into(), Rc::new(svd_commodity)); + commodities.insert("A".into(), Arc::new(sed_commodity.clone())); + commodities.insert("B".into(), Arc::new(sed_commodity.clone())); + commodities.insert("C".into(), Arc::new(sed_commodity)); + commodities.insert("D".into(), Arc::new(svd_commodity)); let graphs = IndexMap::from([(("GBR".into(), 2020), graph)]); let result = solve_investment_order_for_year(&graphs, &commodities, 2020); @@ -663,9 +663,9 @@ mod tests { // Create commodities map using fixtures let mut commodities = CommodityMap::new(); - commodities.insert("A".into(), Rc::new(sed_commodity.clone())); - commodities.insert("B".into(), Rc::new(sed_commodity)); - commodities.insert("C".into(), Rc::new(svd_commodity)); + commodities.insert("A".into(), Arc::new(sed_commodity.clone())); + commodities.insert("B".into(), Arc::new(sed_commodity)); + commodities.insert("C".into(), Arc::new(svd_commodity)); // Duplicate the graph over two regions let graphs = IndexMap::from([ diff --git a/src/graph/validate.rs b/src/graph/validate.rs index fd5f01c00..b750ae9db 100644 --- a/src/graph/validate.rs +++ b/src/graph/validate.rs @@ -237,7 +237,7 @@ mod tests { use crate::fixture::{assert_error, other_commodity, sed_commodity, svd_commodity}; use petgraph::graph::Graph; use rstest::rstest; - use std::rc::Rc; + use std::sync::Arc; #[rstest] fn validate_commodities_graph_works( @@ -249,9 +249,9 @@ mod tests { let mut commodities = CommodityMap::new(); // Add test commodities (all have DayNight time slice level) - commodities.insert("A".into(), Rc::new(other_commodity)); - commodities.insert("B".into(), Rc::new(sed_commodity)); - commodities.insert("C".into(), Rc::new(svd_commodity)); + commodities.insert("A".into(), Arc::new(other_commodity)); + commodities.insert("B".into(), Arc::new(sed_commodity)); + commodities.insert("C".into(), Arc::new(svd_commodity)); // Build valid graph: A(OTH) -> B(SED) -> C(SVD) ->D(DEMAND) let node_a = graph.add_node(GraphNode::Commodity("A".into())); @@ -276,9 +276,9 @@ mod tests { let mut commodities = CommodityMap::new(); // Add test commodities (all have DayNight time slice level) - commodities.insert("A".into(), Rc::new(svd_commodity)); - commodities.insert("B".into(), Rc::new(sed_commodity)); - commodities.insert("C".into(), Rc::new(other_commodity)); + commodities.insert("A".into(), Arc::new(svd_commodity)); + commodities.insert("B".into(), Arc::new(sed_commodity)); + commodities.insert("C".into(), Arc::new(other_commodity)); // Build invalid graph: C(OTH) -> A(SVD) -> B(SED) - SVD cannot be consumed let node_c = graph.add_node(GraphNode::Commodity("C".into())); @@ -300,7 +300,7 @@ mod tests { let mut commodities = CommodityMap::new(); // Add test commodities (all have DayNight time slice level) - commodities.insert("A".into(), Rc::new(svd_commodity)); + commodities.insert("A".into(), Arc::new(svd_commodity)); // Build invalid graph: A(SVD) -> B(DEMAND) - SVD must be produced let node_a = graph.add_node(GraphNode::Commodity("A".into())); @@ -320,8 +320,8 @@ mod tests { let mut commodities = CommodityMap::new(); // Add test commodities (all have DayNight time slice level) - commodities.insert("A".into(), Rc::new(sed_commodity.clone())); - commodities.insert("B".into(), Rc::new(sed_commodity)); + commodities.insert("A".into(), Arc::new(sed_commodity.clone())); + commodities.insert("B".into(), Arc::new(sed_commodity)); // Build invalid graph: B(SED) -> A(SED) let node_a = graph.add_node(GraphNode::Commodity("A".into())); @@ -344,9 +344,9 @@ mod tests { let mut commodities = CommodityMap::new(); // Add test commodities (all have DayNight time slice level) - commodities.insert("A".into(), Rc::new(other_commodity)); - commodities.insert("B".into(), Rc::new(sed_commodity.clone())); - commodities.insert("C".into(), Rc::new(sed_commodity)); + commodities.insert("A".into(), Arc::new(other_commodity)); + commodities.insert("B".into(), Arc::new(sed_commodity.clone())); + commodities.insert("C".into(), Arc::new(sed_commodity)); // Build invalid graph: B(SED) -> A(OTH) -> C(SED) let node_a = graph.add_node(GraphNode::Commodity("A".into())); diff --git a/src/id.rs b/src/id.rs index 52eaed069..9c0e06501 100644 --- a/src/id.rs +++ b/src/id.rs @@ -30,7 +30,7 @@ macro_rules! define_id_type { )] /// An ID type (e.g. `AgentID`, `CommodityID`, etc.) #[from(forward)] - pub struct $name(pub std::rc::Rc); + pub struct $name(pub std::sync::Arc); impl std::borrow::Borrow for $name { fn borrow(&self) -> &str { @@ -71,7 +71,7 @@ macro_rules! define_id_type { impl $name { /// Create a new ID from a string slice pub fn new(id: &str) -> Self { - $name(std::rc::Rc::from(id)) + $name(std::sync::Arc::from(id)) } } }; diff --git a/src/input/agent/commodity_portion.rs b/src/input/agent/commodity_portion.rs index 0dc59694e..ba42f8e1c 100644 --- a/src/input/agent/commodity_portion.rs +++ b/src/input/agent/commodity_portion.rs @@ -211,7 +211,7 @@ mod tests { }; use indexmap::IndexMap; use rstest::{fixture, rstest}; - use std::rc::Rc; + use std::sync::Arc; #[fixture] fn milestone_years() -> [u32; 1] { @@ -221,8 +221,8 @@ mod tests { #[fixture] fn commodities(svd_commodity: Commodity, other_commodity: Commodity) -> CommodityMap { IndexMap::from([ - ("commodity1".into(), Rc::new(svd_commodity)), - ("other_commodity".into(), Rc::new(other_commodity)), + ("commodity1".into(), Arc::new(svd_commodity)), + ("other_commodity".into(), Arc::new(other_commodity)), ]) } @@ -331,7 +331,7 @@ mod tests { sed_commodity: Commodity, ) { // Invalid case: SED commodity without associated commodity portions - commodities.insert(CommodityID::new("sed_commodity"), Rc::new(sed_commodity)); + commodities.insert(CommodityID::new("sed_commodity"), Arc::new(sed_commodity)); assert_error!( validate_agent_commodity_portions( &agent_commodity_portions, diff --git a/src/input/agent/search_space.rs b/src/input/agent/search_space.rs index 44236266d..6216b0072 100644 --- a/src/input/agent/search_space.rs +++ b/src/input/agent/search_space.rs @@ -11,11 +11,11 @@ use itertools::{Itertools, iproduct}; use serde::Deserialize; use std::collections::{HashMap, HashSet}; use std::path::Path; -use std::rc::Rc; +use std::sync::Arc; const AGENT_SEARCH_SPACES_FILE_NAME: &str = "agent_search_spaces.csv"; -type ProducersMap = HashMap<(CommodityID, RegionID, u32), Rc>>>; +type ProducersMap = HashMap<(CommodityID, RegionID, u32), Arc>>>; #[derive(PartialEq, Debug, Deserialize)] struct SearchSpaceEntry { @@ -85,7 +85,7 @@ fn for_each_year_in_search_space( mut f: F, ) -> Result<()> where - F: FnMut(CommodityID, RegionID, u32, Rc>>) -> Result<()>, + F: FnMut(CommodityID, RegionID, u32, Arc>>) -> Result<()>, { ensure!(!search_space.is_empty(), "No processes provided"); @@ -103,7 +103,7 @@ where } } else { // Check each process ID in turn - let search_space: Rc> = Rc::new( + let search_space: Arc> = Arc::new( search_space .split(';') .map(|process_id_str| { @@ -249,7 +249,7 @@ fn get_producers_map(agents: &AgentMap, processes: &ProcessMap) -> ProducersMap && process.regions.contains(region_id) }) .cloned(); - Rc::get_mut(vec).unwrap().extend(producers); + Arc::get_mut(vec).unwrap().extend(producers); } map @@ -272,13 +272,13 @@ mod tests { use std::iter; #[fixture] - fn process1(process: Process) -> Rc { - Rc::new(process) + fn process1(process: Process) -> Arc { + Arc::new(process) } #[fixture] - fn process2(process: Process) -> Rc { - Rc::new(Process { + fn process2(process: Process) -> Arc { + Arc::new(Process { id: "process2".into(), ..process }) @@ -316,14 +316,14 @@ mod tests { agent: Agent, commodity_id: CommodityID, region_id: RegionID, - process1: Rc, - process2: Rc, + process1: Arc, + process2: Arc, ) { let producers = hash_map! { - (commodity_id.clone(), region_id.clone(), 2020) => Rc::new(vec![process1.clone()]), - (commodity_id.clone(), region_id.clone(), 2030) => Rc::new(vec![process2.clone()]) + (commodity_id.clone(), region_id.clone(), 2020) => Arc::new(vec![process1.clone()]), + (commodity_id.clone(), region_id.clone(), 2030) => Arc::new(vec![process2.clone()]) }; - let mut calls: Vec<(u32, Rc>>)> = Vec::new(); + let mut calls: Vec<(u32, Arc>>)> = Vec::new(); for_each_year_in_search_space( "all", &agent, @@ -354,12 +354,12 @@ mod tests { processes: ProcessMap, ) { let process = processes.values().next().unwrap().clone(); - let value = Rc::new(vec![process.clone()]); + let value = Arc::new(vec![process.clone()]); let producers = hash_map! { (commodity_id.clone(), region_id.clone(), 2020) => value.clone(), (commodity_id.clone(), region_id.clone(), 2030) => value }; - let mut calls: Vec<(u32, Rc>>)> = Vec::new(); + let mut calls: Vec<(u32, Arc>>)> = Vec::new(); for_each_year_in_search_space( "process1", &agent, @@ -379,7 +379,7 @@ mod tests { assert_eq!(calls[0].1.len(), 1); assert_eq!(calls[0].1[0].id, process.id); // Both years receive the same Rc-wrapped search space - assert!(Rc::ptr_eq(&calls[0].1, &calls[1].1)); + assert!(Arc::ptr_eq(&calls[0].1, &calls[1].1)); } #[rstest] @@ -387,17 +387,17 @@ mod tests { agent: Agent, commodity_id: CommodityID, region_id: RegionID, - process1: Rc, - process2: Rc, + process1: Arc, + process2: Arc, ) { let producers = hash_map! { - (commodity_id.clone(), region_id.clone(), 2020) => Rc::new(vec![process1.clone(), process2.clone()]) + (commodity_id.clone(), region_id.clone(), 2020) => Arc::new(vec![process1.clone(), process2.clone()]) }; let processes: ProcessMap = indexmap! { process1.id.clone() => process1.clone(), process2.id.clone() => process2.clone(), }; - let mut calls: Vec<(u32, Rc>>)> = Vec::new(); + let mut calls: Vec<(u32, Arc>>)> = Vec::new(); for_each_year_in_search_space( "process1;process2", &agent, diff --git a/src/input/asset.rs b/src/input/asset.rs index f38deb98d..da8b27798 100644 --- a/src/input/asset.rs +++ b/src/input/asset.rs @@ -13,7 +13,7 @@ use itertools::Itertools; use log::warn; use serde::Deserialize; use std::path::Path; -use std::rc::Rc; +use std::sync::Arc; const ASSETS_FILE_NAME: &str = "assets.csv"; @@ -124,7 +124,7 @@ where UserAsset::new( agent_id.clone(), - Rc::clone(process), + Arc::clone(process), region_id.clone(), asset.capacity, asset.commission_year, @@ -167,7 +167,7 @@ mod tests { }; let asset_out = UserAsset::new( "agent1".into(), - Rc::clone(processes.values().next().unwrap()), + Arc::clone(processes.values().next().unwrap()), "GBR".into(), Capacity(1.0), 2010, diff --git a/src/input/process.rs b/src/input/process.rs index 895f5f3a8..d86063854 100644 --- a/src/input/process.rs +++ b/src/input/process.rs @@ -14,7 +14,7 @@ use indexmap::IndexSet; use log::warn; use serde::Deserialize; use std::path::Path; -use std::rc::Rc; +use std::sync::Arc; mod availability; use availability::read_process_availabilities; @@ -71,7 +71,7 @@ pub fn read_processes( // Add data to Process objects for (id, process) in &mut processes { // This will always succeed as we know there will only be one reference to the process here - let process = Rc::get_mut(process).unwrap(); + let process = Arc::get_mut(process).unwrap(); // We have already checked that there are maps for every process so this will succeed process.activity_limits = activity_limits.remove(id).unwrap(); diff --git a/src/input/process/availability.rs b/src/input/process/availability.rs index cf10ceb2b..dadf88eb1 100644 --- a/src/input/process/availability.rs +++ b/src/input/process/availability.rs @@ -12,7 +12,7 @@ use serde::Deserialize; use std::collections::HashMap; use std::ops::RangeInclusive; use std::path::Path; -use std::rc::Rc; +use std::sync::Arc; const PROCESS_AVAILABILITIES_FILE_NAME: &str = "process_availabilities.csv"; @@ -150,7 +150,7 @@ where .with_context(|| { format!("Error creating activity limits for process {process_id}") })?; - inner_map.insert((region_id.clone(), year), Rc::new(availabilities)); + inner_map.insert((region_id.clone(), year), Arc::new(availabilities)); } map.insert(process_id.clone(), inner_map); } diff --git a/src/input/process/flow.rs b/src/input/process/flow.rs index 66c96a494..69a323308 100644 --- a/src/input/process/flow.rs +++ b/src/input/process/flow.rs @@ -14,7 +14,7 @@ use itertools::iproduct; use serde::Deserialize; use std::collections::HashMap; use std::path::Path; -use std::rc::Rc; +use std::sync::Arc; const PROCESS_FLOWS_FILE_NAME: &str = "process_flows.csv"; @@ -183,7 +183,7 @@ where // Create ProcessFlow object let process_flow = ProcessFlow { - commodity: Rc::clone(commodity), + commodity: Arc::clone(commodity), coeff: record.coeff, kind: FlowType::Fixed, cost: record.cost.unwrap_or(MoneyPerFlow(0.0)), @@ -195,7 +195,7 @@ where let flows_map = region_year_map .entry((region_id.clone(), year)) .or_default(); - let existing = Rc::get_mut(flows_map) + let existing = Arc::get_mut(flows_map) .unwrap() // safe: there will only be one copy .insert(commodity.id.clone(), process_flow.clone()) .is_some(); @@ -264,7 +264,7 @@ fn validate_flows_and_update_primary_output( // Update primary output if needed if process.primary_output != primary_output { // Safe: There should only be one ref to process - Rc::get_mut(process).unwrap().primary_output = primary_output; + Arc::get_mut(process).unwrap().primary_output = primary_output; } } @@ -404,9 +404,9 @@ mod tests { use map_macro::hash_map; use rstest::{fixture, rstest}; use std::iter; - use std::rc::Rc; + use std::sync::Arc; - fn flow(commodity: Rc, coeff: f64) -> ProcessFlow { + fn flow(commodity: Arc, coeff: f64) -> ProcessFlow { ProcessFlow { commodity, coeff: FlowPerActivity(coeff), @@ -424,7 +424,7 @@ mod tests { I: Clone + Iterator, { let years = years.unwrap_or(process.years.clone().collect()); - let map: Rc> = Rc::new(flows.collect()); + let map: Arc> = Arc::new(flows.collect()); let flows_inner = iproduct!(&process.regions, years) .map(|(region_id, year)| ((region_id.clone(), year), map.clone())) .collect(); @@ -473,8 +473,8 @@ mod tests { // Both commodities have the same units assert_eq!(svd_commodity.units, sed_commodity.units); - let commodity1 = Rc::new(svd_commodity); - let commodity2 = Rc::new(sed_commodity); + let commodity1 = Arc::new(svd_commodity); + let commodity2 = Arc::new(sed_commodity); let (_, flows_map) = build_maps( process, [ @@ -498,8 +498,8 @@ mod tests { // Ensure the two commodities have different units assert_ne!(sed_commodity_pj.units, sed_commodity_tonnes.units); - let commodity1 = Rc::new(sed_commodity_pj); - let commodity2 = Rc::new(sed_commodity_tonnes); + let commodity1 = Arc::new(sed_commodity_pj); + let commodity2 = Arc::new(sed_commodity_tonnes); let (_, flows_map) = build_maps( process, [ @@ -531,8 +531,8 @@ mod tests { other_commodity.units = "tonnes".into(); assert_ne!(sed_commodity_pj.units, other_commodity.units); - let sed_commodity = Rc::new(sed_commodity_pj); - let oth_commodity = Rc::new(other_commodity); + let sed_commodity = Arc::new(sed_commodity_pj); + let oth_commodity = Arc::new(other_commodity); let (_, flows_map) = build_maps( process, @@ -550,7 +550,7 @@ mod tests { #[rstest] fn single_sed_svd_output(svd_commodity: Commodity, process: Process) { - let commodity = Rc::new(svd_commodity); + let commodity = Arc::new(svd_commodity); let (_, flows_map) = build_maps( process, std::iter::once((commodity.id.clone(), flow(commodity.clone(), 1.0))), @@ -563,8 +563,8 @@ mod tests { #[rstest] fn no_sed_svd_outputs(other_commodity: Commodity, process: Process) { - let oth_commodity_1 = Rc::new(other_commodity.clone()); - let oth_commodity_2 = Rc::new(other_commodity.clone()); + let oth_commodity_1 = Arc::new(other_commodity.clone()); + let oth_commodity_2 = Arc::new(other_commodity.clone()); let (_, flows_map) = build_maps( process, [ @@ -592,9 +592,9 @@ mod tests { // Output commodity shares units with one input assert_eq!(svd_commodity.units, sed_commodity_pj.units); - let input1 = Rc::new(sed_commodity_pj); - let input2 = Rc::new(sed_commodity_tonnes); - let output = Rc::new(svd_commodity); + let input1 = Arc::new(sed_commodity_pj); + let input2 = Arc::new(sed_commodity_tonnes); + let output = Arc::new(svd_commodity); let (_, flows_map) = build_maps( process, @@ -616,7 +616,7 @@ mod tests { #[rstest] fn single_output_infer_primary(#[from(svd_commodity)] commodity: Commodity, process: Process) { let milestone_years = vec![2010, 2020]; - let commodity = Rc::new(commodity); + let commodity = Arc::new(commodity); let (mut processes, flows_map) = build_maps( process, std::iter::once((commodity.id.clone(), flow(commodity.clone(), 1.0))), @@ -637,8 +637,8 @@ mod tests { process: Process, ) { let milestone_years: Vec = vec![2010, 2020]; - let commodity1 = Rc::new(commodity1); - let commodity2 = Rc::new(commodity2); + let commodity1 = Arc::new(commodity1); + let commodity2 = Arc::new(commodity2); let (mut processes, flows_map) = build_maps( process, [ @@ -660,8 +660,8 @@ mod tests { process: Process, ) { let milestone_years = vec![2010, 2020]; - let commodity1 = Rc::new(commodity1); - let commodity2 = Rc::new(commodity2); + let commodity1 = Arc::new(commodity1); + let commodity2 = Arc::new(commodity2); let mut process = process; process.primary_output = Some(commodity2.id.clone()); let (mut processes, flows_map) = build_maps( @@ -688,8 +688,8 @@ mod tests { process: Process, ) { let milestone_years = vec![2010, 2020]; - let commodity1 = Rc::new(commodity1); - let commodity2 = Rc::new(commodity2); + let commodity1 = Arc::new(commodity1); + let commodity2 = Arc::new(commodity2); let (mut processes, flows_map) = build_maps( process, [ @@ -715,8 +715,8 @@ mod tests { ) { let milestone_years = vec![2010, 2015, 2020]; let flow_years = vec![2010, 2020]; - let commodity1 = Rc::new(commodity1); - let commodity2 = Rc::new(commodity2); + let commodity1 = Arc::new(commodity1); + let commodity2 = Arc::new(commodity2); let (mut processes, flows_map) = build_maps( process, [ @@ -741,8 +741,8 @@ mod tests { process: Process, ) { let milestone_years = vec![2010, 2015, 2020]; - let commodity1 = Rc::new(commodity1); - let commodity2 = Rc::new(commodity2); + let commodity1 = Arc::new(commodity1); + let commodity2 = Arc::new(commodity2); let (mut processes, flows_map) = build_maps( process, [ diff --git a/src/input/process/investment_constraints.rs b/src/input/process/investment_constraints.rs index 14788016c..c2c342fd4 100644 --- a/src/input/process/investment_constraints.rs +++ b/src/input/process/investment_constraints.rs @@ -14,7 +14,7 @@ use log::warn; use serde::Deserialize; use std::collections::HashMap; use std::path::Path; -use std::rc::Rc; +use std::sync::Arc; const PROCESS_INVESTMENT_CONSTRAINTS_FILE_NAME: &str = "process_investment_constraints.csv"; @@ -199,7 +199,7 @@ where .addition_limit .map(|limit| limit * Year(years_since_prev as f64)); - let constraint = Rc::new(ProcessInvestmentConstraint { addition_limit }); + let constraint = Arc::new(ProcessInvestmentConstraint { addition_limit }); try_insert(process_map, &(region.clone(), year), constraint.clone())?; } diff --git a/src/input/process/parameter.rs b/src/input/process/parameter.rs index 080e9d2b7..8b860ec91 100644 --- a/src/input/process/parameter.rs +++ b/src/input/process/parameter.rs @@ -10,7 +10,7 @@ use anyhow::{Context, Result, ensure}; use serde::Deserialize; use std::collections::HashMap; use std::path::Path; -use std::rc::Rc; +use std::sync::Arc; const PROCESS_PARAMETERS_FILE_NAME: &str = "process_parameters.csv"; @@ -123,7 +123,7 @@ where })?; // Insert parameter into the map - let param = Rc::new(param_raw.into_parameter()?); + let param = Arc::new(param_raw.into_parameter()?); let entry = map.entry(id.clone()).or_default(); for year in parameter_years { for region in parameter_regions.clone() { diff --git a/src/process.rs b/src/process.rs index 9495e50ff..8cbc38093 100644 --- a/src/process.rs +++ b/src/process.rs @@ -14,27 +14,27 @@ use itertools::Itertools; use serde::Deserialize; use std::collections::HashMap; use std::ops::RangeInclusive; -use std::rc::Rc; +use std::sync::Arc; define_id_type! {ProcessID, "process ID"} /// A map of [`Process`]es, keyed by process ID -pub type ProcessMap = IndexMap>; +pub type ProcessMap = IndexMap>; /// A map indicating activity limits for a [`Process`] throughout the year. -pub type ProcessActivityLimitsMap = HashMap<(RegionID, u32), Rc>; +pub type ProcessActivityLimitsMap = HashMap<(RegionID, u32), Arc>; /// A map of [`ProcessParameter`]s, keyed by region and year -pub type ProcessParameterMap = HashMap<(RegionID, u32), Rc>; +pub type ProcessParameterMap = HashMap<(RegionID, u32), Arc>; /// A map of process flows, keyed by region and year. /// /// The value is actually a map itself, keyed by commodity ID. -pub type ProcessFlowsMap = HashMap<(RegionID, u32), Rc>>; +pub type ProcessFlowsMap = HashMap<(RegionID, u32), Arc>>; /// Map of process investment constraints, keyed by region and year pub type ProcessInvestmentConstraintsMap = - HashMap<(RegionID, u32), Rc>; + HashMap<(RegionID, u32), Arc>; /// Represents a process within the simulation #[derive(PartialEq, Debug)] @@ -390,7 +390,7 @@ impl ActivityLimits { #[derive(PartialEq, Debug, Clone)] pub struct ProcessFlow { /// The commodity produced or consumed by this flow - pub commodity: Rc, + pub commodity: Arc, /// Maximum annual commodity flow quantity relative to other commodity flows. /// /// Positive value indicates flow out and negative value indicates flow in. @@ -528,10 +528,10 @@ mod tests { use float_cmp::assert_approx_eq; use rstest::{fixture, rstest}; use std::collections::HashMap; - use std::rc::Rc; + use std::sync::Arc; #[fixture] - fn commodity_with_levy(region_id: RegionID, time_slice: TimeSliceID) -> Rc { + fn commodity_with_levy(region_id: RegionID, time_slice: TimeSliceID) -> Arc { let mut levies_prod = CommodityLevyMap::new(); let mut levies_cons = CommodityLevyMap::new(); @@ -580,7 +580,7 @@ mod tests { MoneyPerFlow(-3.0), ); - Rc::new(Commodity { + Arc::new(Commodity { id: "test_commodity".into(), description: "Test commodity".into(), kind: CommodityType::ServiceDemand, @@ -597,11 +597,11 @@ mod tests { fn commodity_with_consumption_levy( region_id: RegionID, time_slice: TimeSliceID, - ) -> Rc { + ) -> Arc { let mut levies = CommodityLevyMap::new(); levies.insert((region_id, 2020, time_slice), MoneyPerFlow(10.0)); - Rc::new(Commodity { + Arc::new(Commodity { id: "test_commodity".into(), description: "Test commodity".into(), kind: CommodityType::ServiceDemand, @@ -618,11 +618,11 @@ mod tests { fn commodity_with_production_levy( region_id: RegionID, time_slice: TimeSliceID, - ) -> Rc { + ) -> Arc { let mut levies = CommodityLevyMap::new(); levies.insert((region_id, 2020, time_slice), MoneyPerFlow(10.0)); - Rc::new(Commodity { + Arc::new(Commodity { id: "test_commodity".into(), description: "Test commodity".into(), kind: CommodityType::ServiceDemand, @@ -636,7 +636,7 @@ mod tests { } #[fixture] - fn commodity_with_incentive(region_id: RegionID, time_slice: TimeSliceID) -> Rc { + fn commodity_with_incentive(region_id: RegionID, time_slice: TimeSliceID) -> Arc { let mut levies_prod = CommodityLevyMap::new(); levies_prod.insert( (region_id.clone(), 2020, time_slice.clone()), @@ -645,7 +645,7 @@ mod tests { let mut levies_cons = CommodityLevyMap::new(); levies_cons.insert((region_id, 2020, time_slice), MoneyPerFlow(5.0)); - Rc::new(Commodity { + Arc::new(Commodity { id: "test_commodity".into(), description: "Test commodity".into(), kind: CommodityType::ServiceDemand, @@ -659,8 +659,8 @@ mod tests { } #[fixture] - fn commodity_no_levies() -> Rc { - Rc::new(Commodity { + fn commodity_no_levies() -> Arc { + Arc::new(Commodity { id: "test_commodity".into(), description: "Test commodity".into(), kind: CommodityType::ServiceDemand, @@ -676,7 +676,7 @@ mod tests { #[fixture] fn flow_with_cost() -> ProcessFlow { ProcessFlow { - commodity: Rc::new(Commodity { + commodity: Arc::new(Commodity { id: "test_commodity".into(), description: "Test commodity".into(), kind: CommodityType::ServiceDemand, @@ -699,7 +699,7 @@ mod tests { levies.insert((region_id, 2020, time_slice), MoneyPerFlow(10.0)); ProcessFlow { - commodity: Rc::new(Commodity { + commodity: Arc::new(Commodity { id: "test_commodity".into(), description: "Test commodity".into(), kind: CommodityType::ServiceDemand, @@ -722,7 +722,7 @@ mod tests { levies.insert((region_id, 2020, time_slice), MoneyPerFlow(-3.0)); ProcessFlow { - commodity: Rc::new(Commodity { + commodity: Arc::new(Commodity { id: "test_commodity".into(), description: "Test commodity".into(), kind: CommodityType::ServiceDemand, @@ -741,7 +741,7 @@ mod tests { #[rstest] fn get_levy_no_levies( - commodity_no_levies: Rc, + commodity_no_levies: Arc, region_id: RegionID, time_slice: TimeSliceID, ) { @@ -760,7 +760,7 @@ mod tests { #[rstest] fn get_levy_with_levy( - commodity_with_levy: Rc, + commodity_with_levy: Arc, region_id: RegionID, time_slice: TimeSliceID, ) { @@ -779,7 +779,7 @@ mod tests { #[rstest] fn get_levy_with_incentive( - commodity_with_incentive: Rc, + commodity_with_incentive: Arc, region_id: RegionID, time_slice: TimeSliceID, ) { @@ -797,7 +797,7 @@ mod tests { } #[rstest] - fn get_levy_different_region(commodity_with_levy: Rc, time_slice: TimeSliceID) { + fn get_levy_different_region(commodity_with_levy: Arc, time_slice: TimeSliceID) { let flow = ProcessFlow { commodity: commodity_with_levy, coeff: FlowPerActivity(1.0), @@ -813,7 +813,7 @@ mod tests { #[rstest] fn get_levy_different_year( - commodity_with_levy: Rc, + commodity_with_levy: Arc, region_id: RegionID, time_slice: TimeSliceID, ) { @@ -831,7 +831,7 @@ mod tests { } #[rstest] - fn get_levy_different_time_slice(commodity_with_levy: Rc, region_id: RegionID) { + fn get_levy_different_time_slice(commodity_with_levy: Arc, region_id: RegionID) { let flow = ProcessFlow { commodity: commodity_with_levy, coeff: FlowPerActivity(1.0), @@ -852,7 +852,7 @@ mod tests { #[rstest] fn get_levy_consumption_positive_coeff( - commodity_with_consumption_levy: Rc, + commodity_with_consumption_levy: Arc, region_id: RegionID, time_slice: TimeSliceID, ) { @@ -871,7 +871,7 @@ mod tests { #[rstest] fn get_levy_consumption_negative_coeff( - commodity_with_consumption_levy: Rc, + commodity_with_consumption_levy: Arc, region_id: RegionID, time_slice: TimeSliceID, ) { @@ -890,7 +890,7 @@ mod tests { #[rstest] fn get_levy_production_positive_coeff( - commodity_with_production_levy: Rc, + commodity_with_production_levy: Arc, region_id: RegionID, time_slice: TimeSliceID, ) { @@ -909,7 +909,7 @@ mod tests { #[rstest] fn get_levy_production_negative_coeff( - commodity_with_production_levy: Rc, + commodity_with_production_levy: Arc, region_id: RegionID, time_slice: TimeSliceID, ) { @@ -990,7 +990,7 @@ mod tests { #[test] fn is_input_and_is_output() { - let commodity = Rc::new(Commodity { + let commodity = Arc::new(Commodity { id: "test_commodity".into(), description: "Test commodity".into(), kind: CommodityType::ServiceDemand, @@ -1003,19 +1003,19 @@ mod tests { }); let flow_in = ProcessFlow { - commodity: Rc::clone(&commodity), + commodity: Arc::clone(&commodity), coeff: FlowPerActivity(-1.0), kind: FlowType::Fixed, cost: MoneyPerFlow(0.0), }; let flow_out = ProcessFlow { - commodity: Rc::clone(&commodity), + commodity: Arc::clone(&commodity), coeff: FlowPerActivity(1.0), kind: FlowType::Fixed, cost: MoneyPerFlow(0.0), }; let flow_zero = ProcessFlow { - commodity: Rc::clone(&commodity), + commodity: Arc::clone(&commodity), coeff: FlowPerActivity(0.0), kind: FlowType::Fixed, cost: MoneyPerFlow(0.0), diff --git a/src/simulation.rs b/src/simulation.rs index 5ad8acfdf..030c7d3ad 100644 --- a/src/simulation.rs +++ b/src/simulation.rs @@ -10,7 +10,7 @@ use anyhow::{Context, Result}; use context_manager; use log::info; use std::path::Path; -use std::rc::Rc; +use std::sync::Arc; pub mod optimisation; use optimisation::{DispatchRun, FlowMap}; @@ -257,7 +257,7 @@ fn candidate_assets_for_next_year( for region_id in &process.regions { candidates.push( Asset::new_candidate_for_dispatch( - Rc::clone(process), + Arc::clone(process), region_id.clone(), candidate_asset_capacity, next_year, diff --git a/src/simulation/investment.rs b/src/simulation/investment.rs index 9b0059b5b..579c7cf88 100644 --- a/src/simulation/investment.rs +++ b/src/simulation/investment.rs @@ -15,6 +15,7 @@ use context_manager; use indexmap::IndexMap; use itertools::Itertools; use log::{debug, warn}; +use rayon::prelude::*; use std::collections::HashMap; use strum::IntoEnumIterator; @@ -387,45 +388,54 @@ pub fn select_best_assets( region_id ); - // Appraise all options - let mut outputs = Vec::new(); - for asset in &opt_assets { - // For candidates, cap the asset's capacity by the current demand-limiting capacity - // and, where an addition constraint exists, the remaining installable capacity. - let mut asset = asset.clone(); - if asset.is_candidate() { - let dlc = AssetCapacity::from_capacity( - get_demand_limiting_capacity( - &model.time_slice_info, - &asset, - commodity, - &demand, - ), - asset.unit_size(), - ); - let cap = asset.capacity().min(dlc); - let max_capacity = remaining_capacities - .get(&asset) - .copied() - .map_or(cap, |remaining| cap.min(remaining)); - asset.make_mut().set_capacity(max_capacity); - } - - // Skip assets with zero capacity - if asset.capacity().total_capacity() <= Capacity(0.0) { - continue; - } - - let output = appraise_investment( - model, - &asset, - commodity, - objective_type, - &coefficients[&asset], - &demand, - )?; - outputs.push(output); - } + // Appraise all options in parallel: each asset's appraisal is independent (all shared + // state is read-only within this block), so we can safely use Rayon here. + // Each HiGHS solve inside `appraise_investment` is configured to use only one thread + // (via `parallel="off"`) to avoid over-subscription. + let mut outputs: Vec = opt_assets + .par_iter() + .map(|asset| -> Result> { + // For candidates, cap the asset's capacity by the current demand-limiting + // capacity and, where an addition constraint exists, the remaining installable + // capacity. `make_mut` creates a new Arc allocation for the modified clone so + // there is no shared mutable state between iterations. + let mut asset = asset.clone(); + if asset.is_candidate() { + let dlc = AssetCapacity::from_capacity( + get_demand_limiting_capacity( + &model.time_slice_info, + &asset, + commodity, + &demand, + ), + asset.unit_size(), + ); + let cap = asset.capacity().min(dlc); + let max_capacity = remaining_capacities + .get(&asset) + .copied() + .map_or(cap, |remaining| cap.min(remaining)); + asset.make_mut().set_capacity(max_capacity); + } + + // Skip assets with zero capacity + if asset.capacity().total_capacity() <= Capacity(0.0) { + return Ok(None); + } + + Ok(Some(appraise_investment( + model, + &asset, + commodity, + objective_type, + &coefficients[&asset], + &demand, + )?)) + }) + .collect::>>()? // propagate any solver error + .into_iter() + .flatten() + .collect(); // Save appraisal results writer.write_appraisal_debug_info( @@ -578,7 +588,7 @@ mod tests { use crate::units::{Flow, FlowPerActivity, MoneyPerFlow}; use indexmap::indexmap; use rstest::rstest; - use std::rc::Rc; + use std::sync::Arc; #[rstest] fn get_demand_limiting_capacity_works( @@ -588,15 +598,15 @@ mod tests { mut process: Process, ) { // Add flows for the process using the existing commodity fixture - let commodity_rc = Rc::new(svd_commodity); + let commodity_rc = Arc::new(svd_commodity); let process_flow = ProcessFlow { - commodity: Rc::clone(&commodity_rc), + commodity: Arc::clone(&commodity_rc), coeff: FlowPerActivity(2.0), // 2 units of flow per unit of activity kind: FlowType::Fixed, cost: MoneyPerFlow(0.0), }; let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() }; - let process_flows_map = process_flows_map(process.regions.clone(), Rc::new(process_flows)); + let process_flows_map = process_flows_map(process.regions.clone(), Arc::new(process_flows)); process.flows = process_flows_map; // Create asset with the configured process @@ -624,15 +634,15 @@ mod tests { time_slice_info2.time_slices.keys().collect_tuple().unwrap(); // Add flows for the process using the existing commodity fixture - let commodity_rc = Rc::new(svd_commodity); + let commodity_rc = Arc::new(svd_commodity); let process_flow = ProcessFlow { - commodity: Rc::clone(&commodity_rc), + commodity: Arc::clone(&commodity_rc), coeff: FlowPerActivity(1.0), // 1 unit of flow per unit of activity kind: FlowType::Fixed, cost: MoneyPerFlow(0.0), }; let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() }; - let process_flows_map = process_flows_map(process.regions.clone(), Rc::new(process_flows)); + let process_flows_map = process_flows_map(process.regions.clone(), Arc::new(process_flows)); process.flows = process_flows_map; // Add activity limits for the process @@ -672,16 +682,16 @@ mod tests { time_slice_info2.time_slices.keys().collect_tuple().unwrap(); // Configure a 1:1 activity-to-flow relationship. - let commodity_rc = Rc::new(svd_commodity); + let commodity_rc = Arc::new(svd_commodity); let process_flow = ProcessFlow { - commodity: Rc::clone(&commodity_rc), + commodity: Arc::clone(&commodity_rc), coeff: FlowPerActivity(1.0), kind: FlowType::Fixed, cost: MoneyPerFlow(0.0), }; let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() }; - process.flows = process_flows_map(process.regions.clone(), Rc::new(process_flows)); + process.flows = process_flows_map(process.regions.clone(), Arc::new(process_flows)); // Fine-grained limits imply a capacity requirement of 5: // TS1: 5 / 1 = 5 @@ -738,16 +748,16 @@ mod tests { #[case] activity_limit: Dimensionless, #[case] expected: Capacity, ) { - let commodity_rc = Rc::new(svd_commodity); + let commodity_rc = Arc::new(svd_commodity); let process_flow = ProcessFlow { - commodity: Rc::clone(&commodity_rc), + commodity: Arc::clone(&commodity_rc), coeff: FlowPerActivity(1.0), kind: FlowType::Fixed, cost: MoneyPerFlow(0.0), }; process.flows = process_flows_map( process.regions.clone(), - Rc::new(indexmap! { commodity_rc.id.clone() => process_flow }), + Arc::new(indexmap! { commodity_rc.id.clone() => process_flow }), ); let mut limits = ActivityLimits::new_with_full_availability(&time_slice_info); diff --git a/src/simulation/investment/appraisal.rs b/src/simulation/investment/appraisal.rs index 593a03228..56dae5e7d 100644 --- a/src/simulation/investment/appraisal.rs +++ b/src/simulation/investment/appraisal.rs @@ -15,7 +15,7 @@ use optimisation::ResultsMap; use serde::Serialize; use std::any::Any; use std::cmp::Ordering; -use std::rc::Rc; +use std::sync::Arc; pub mod coefficients; mod constraints; @@ -59,7 +59,7 @@ pub struct AppraisalOutput { /// The comparison metric to compare investment decisions pub metric: Option>, /// Activity coefficients and market costs used in the appraisal - pub coefficients: Rc, + pub coefficients: Arc, } impl AppraisalOutput { @@ -68,7 +68,7 @@ impl AppraisalOutput { asset: AssetRef, results: ResultsMap, metric: Option, - coefficients: Rc, + coefficients: Arc, ) -> Self { Self { asset, @@ -216,7 +216,7 @@ fn calculate_lcox( model: &Model, asset: &AssetRef, commodity: &Commodity, - coefficients: &Rc, + coefficients: &Arc, demand: &DemandMap, ) -> Result { let results = perform_optimisation(model, asset, commodity, coefficients, demand)?; @@ -245,7 +245,7 @@ fn calculate_npv( model: &Model, asset: &AssetRef, commodity: &Commodity, - coefficients: &Rc, + coefficients: &Arc, demand: &DemandMap, ) -> Result { let results = perform_optimisation(model, asset, commodity, coefficients, demand)?; @@ -282,7 +282,7 @@ pub fn appraise_investment( asset: &AssetRef, commodity: &Commodity, objective_type: &ObjectiveType, - coefficients: &Rc, + coefficients: &Arc, demand: &DemandMap, ) -> Result { let appraisal_method = match objective_type { @@ -354,7 +354,7 @@ mod tests { use crate::units::{Capacity, MoneyPerActivity}; use float_cmp::assert_approx_eq; use rstest::rstest; - use std::rc::Rc; + use std::sync::Arc; /// Parametrised tests for LCOX metric comparison. #[rstest] @@ -400,7 +400,7 @@ mod tests { #[rstest] fn compare_assets_fallback(process: Process, region_id: RegionID, agent_id: AgentID) { - let process = Rc::new(process); + let process = Arc::new(process); let capacity = Capacity(2.0); let asset1 = Asset::new_commissioned( agent_id.clone(), @@ -426,8 +426,8 @@ mod tests { assert!(compare_asset_fallback(&asset2, &asset3).is_gt()); } - fn objective_coeffs() -> Rc { - Rc::new(ObjectiveCoefficients { + fn objective_coeffs() -> Arc { + Arc::new(ObjectiveCoefficients { activity_coefficients: IndexMap::new(), market_costs: IndexMap::new(), }) @@ -530,7 +530,7 @@ mod tests { region_id: RegionID, agent_id: AgentID, ) { - let process_rc = Rc::new(process); + let process_rc = Arc::new(process); let capacity = Capacity(10.0); let commission_years = [2015, 2020, 2010]; @@ -567,7 +567,7 @@ mod tests { /// Test that when metrics and commission years are equal, the original order is preserved #[rstest] fn appraisal_sort_maintains_order_when_all_equal(process: Process, region_id: RegionID) { - let process_rc = Rc::new(process); + let process_rc = Arc::new(process); let capacity = Capacity(10.0); let commission_year = 2015; let agent_ids = ["agent1", "agent2", "agent3"]; @@ -608,7 +608,7 @@ mod tests { region_id: RegionID, agent_id: AgentID, ) { - let process_rc = Rc::new(process); + let process_rc = Arc::new(process); let capacity = Capacity(10.0); // Create a mix of commissioned and candidate (non-commissioned) assets @@ -669,7 +669,7 @@ mod tests { region_id: RegionID, agent_id: AgentID, ) { - let process_rc = Rc::new(process); + let process_rc = Arc::new(process); let capacity = Capacity(10.0); // Create a mix of commissioned and candidate (non-commissioned) assets @@ -784,7 +784,7 @@ mod tests { region_id: RegionID, agent_id: AgentID, ) { - let process_rc = Rc::new(process); + let process_rc = Arc::new(process); let capacity = Capacity(10.0); let commissioned = Asset::new_commissioned( @@ -818,7 +818,7 @@ mod tests { region_id: RegionID, agent_id: AgentID, ) { - let process_rc = Rc::new(process); + let process_rc = Arc::new(process); let capacity = Capacity(10.0); let year = 2020; diff --git a/src/simulation/investment/appraisal/coefficients.rs b/src/simulation/investment/appraisal/coefficients.rs index 59c3c4a45..7911a9ec8 100644 --- a/src/simulation/investment/appraisal/coefficients.rs +++ b/src/simulation/investment/appraisal/coefficients.rs @@ -8,7 +8,7 @@ use crate::time_slice::{TimeSliceID, TimeSliceInfo}; use crate::units::{MoneyPerActivity, MoneyPerFlow}; use indexmap::IndexMap; use std::collections::HashMap; -use std::rc::Rc; +use std::sync::Arc; /// Per-time-slice cost coefficients for an asset. /// @@ -43,7 +43,7 @@ pub fn calculate_coefficients_for_assets( assets: &[AssetRef], prices: &Prices, year: u32, -) -> HashMap> { +) -> HashMap> { assets .iter() .map(|asset| { @@ -54,7 +54,7 @@ pub fn calculate_coefficients_for_assets( prices, year, ); - (asset.clone(), Rc::new(coefficient)) + (asset.clone(), Arc::new(coefficient)) }) .collect() } diff --git a/src/simulation/investment/appraisal/optimisation.rs b/src/simulation/investment/appraisal/optimisation.rs index a57827459..448abb093 100644 --- a/src/simulation/investment/appraisal/optimisation.rs +++ b/src/simulation/investment/appraisal/optimisation.rs @@ -127,6 +127,12 @@ pub fn perform_optimisation( let mut highs_model = problem.optimise(Sense::Maximise); apply_highs_options_from_toml(&mut highs_model, &model.parameters.highs.appraisal_options) .context("Failed to apply custom HiGHS options to appraisal optimisation")?; + // Enforce single-threaded solving: when appraisals run in parallel via Rayon each HiGHS + // instance must not spawn additional worker threads. Setting `parallel="off"` disables + // HiGHS's concurrent simplex strategy without touching the global thread-pool scheduler + // (setting `threads=N` would fail if the scheduler was already initialised on this thread + // by a previous solve with a different count). + highs_model.set_option("parallel", "off"); let solution = solve_optimal(highs_model) .map_err(ModelError::into_anyhow)? .get_solution(); diff --git a/src/simulation/market.rs b/src/simulation/market.rs index 3d3c5fc48..3a093dfd6 100644 --- a/src/simulation/market.rs +++ b/src/simulation/market.rs @@ -497,8 +497,8 @@ mod tests { use crate::units::{ActivityPerCapacity, Capacity}; use indexmap::IndexSet; use rstest::{fixture, rstest}; - use std::rc::Rc; use std::slice::from_ref; + use std::sync::Arc; #[rstest] fn collect_investment_limits_for_candidates_empty_list() { @@ -513,7 +513,7 @@ mod tests { #[fixture] fn uncommissioned_asset_without_limit(process: Process, region_id: RegionID) -> AssetRef { - Asset::new_candidate(Rc::new(process), region_id, Capacity(10.0), 2015) + Asset::new_candidate(Arc::new(process), region_id, Capacity(10.0), 2015) .unwrap() .into() } @@ -531,7 +531,7 @@ mod tests { constraints.insert( (region_id.clone(), 2015), - Rc::new(ProcessInvestmentConstraint { + Arc::new(ProcessInvestmentConstraint { addition_limit: Some(Capacity(10.0)), }), ); @@ -550,7 +550,7 @@ mod tests { unit_size: None, }; - Asset::new_candidate(Rc::new(process), region_id, Capacity(15.0), 2015) + Asset::new_candidate(Arc::new(process), region_id, Capacity(15.0), 2015) .unwrap() .into() } diff --git a/src/simulation/optimisation/constraints.rs b/src/simulation/optimisation/constraints.rs index e7b7d3cf4..d04d11bd3 100644 --- a/src/simulation/optimisation/constraints.rs +++ b/src/simulation/optimisation/constraints.rs @@ -341,7 +341,7 @@ mod tests { use crate::units::{FlowPerActivity, MoneyPerFlow}; use indexmap::indexmap; use rstest::rstest; - use std::rc::Rc; + use std::sync::Arc; #[rstest] // Max candidate output (2.0) < epsilon (10.0) → zero (guard prevents infeasibility) @@ -354,19 +354,19 @@ mod tests { svd_commodity: Commodity, mut process: Process, ) { - let commodity_rc = Rc::new(svd_commodity); + let commodity_rc = Arc::new(svd_commodity); // Add an output flow for the commodity to the process. With capacity 2.0, cap2act 1.0, // and full availability over a single annual time slice, max_candidate_output = 2.0. let flow = ProcessFlow { - commodity: Rc::clone(&commodity_rc), + commodity: Arc::clone(&commodity_rc), coeff: FlowPerActivity(1.0), kind: FlowType::Fixed, cost: MoneyPerFlow(0.0), }; process.flows = process_flows_map( process.regions.clone(), - Rc::new(indexmap! { commodity_rc.id.clone() => flow }), + Arc::new(indexmap! { commodity_rc.id.clone() => flow }), ); let result = candidate_balance_epsilon( diff --git a/src/simulation/prices.rs b/src/simulation/prices.rs index 249720a0c..7447973c2 100644 --- a/src/simulation/prices.rs +++ b/src/simulation/prices.rs @@ -1355,11 +1355,11 @@ mod tests { use indexmap::{IndexMap, IndexSet}; use rstest::rstest; use std::collections::{HashMap, HashSet}; - use std::rc::Rc; + use std::sync::Arc; fn build_process_flow(commodity: &Commodity, coeff: f64, cost: MoneyPerFlow) -> ProcessFlow { ProcessFlow { - commodity: Rc::new(commodity.clone()), + commodity: Arc::new(commodity.clone()), coeff: FlowPerActivity(coeff), kind: FlowType::Fixed, cost, @@ -1379,7 +1379,7 @@ mod tests { discount_rate: Dimensionless, ) -> Process { let mut process_flows_map = HashMap::new(); - process_flows_map.insert((region_id.clone(), year), Rc::new(flows)); + process_flows_map.insert((region_id.clone(), year), Arc::new(flows)); let mut process_parameter_map = HashMap::new(); let proc_param = ProcessParameter { @@ -1389,12 +1389,12 @@ mod tests { lifetime, discount_rate, }; - process_parameter_map.insert((region_id.clone(), year), Rc::new(proc_param)); + process_parameter_map.insert((region_id.clone(), year), Arc::new(proc_param)); let mut activity_limits_map = HashMap::new(); activity_limits_map.insert( (region_id.clone(), year), - Rc::new(ActivityLimits::new_with_full_availability(time_slice_info)), + Arc::new(ActivityLimits::new_with_full_availability(time_slice_info)), ); let regions: IndexSet = IndexSet::from([region_id.clone()]); @@ -1517,7 +1517,7 @@ mod tests { ); let asset = - Asset::new_candidate(Rc::new(process), region_id.clone(), Capacity(1.0), 2015u32) + Asset::new_candidate(Arc::new(process), region_id.clone(), Capacity(1.0), 2015u32) .unwrap(); let asset_ref = AssetRef::from(asset); let mut prices = @@ -1527,8 +1527,8 @@ mod tests { markets.insert((c.id.clone(), region_id.clone())); let mut commodities = CommodityMap::new(); - commodities.insert(b.id.clone(), Rc::new(b.clone())); - commodities.insert(c.id.clone(), Rc::new(c.clone())); + commodities.insert(b.id.clone(), Arc::new(b.clone())); + commodities.insert(c.id.clone(), Arc::new(c.clone())); let existing = vec![(&asset_ref, &time_slice, Activity(1.0))]; let candidates = Vec::new(); @@ -1599,7 +1599,7 @@ mod tests { ); let asset = - Asset::new_candidate(Rc::new(process), region_id.clone(), Capacity(4.0), 2015u32) + Asset::new_candidate(Arc::new(process), region_id.clone(), Capacity(4.0), 2015u32) .unwrap(); let asset_ref = AssetRef::from(asset); let mut prices = @@ -1609,8 +1609,8 @@ mod tests { markets.insert((c.id.clone(), region_id.clone())); let mut commodities = CommodityMap::new(); - commodities.insert(b.id.clone(), Rc::new(b.clone())); - commodities.insert(c.id.clone(), Rc::new(c.clone())); + commodities.insert(b.id.clone(), Arc::new(b.clone())); + commodities.insert(c.id.clone(), Arc::new(c.clone())); let existing = vec![(&asset_ref, &time_slice, Activity(2.0))]; let candidates = Vec::new();