diff --git a/src/agent.rs b/src/agent.rs index 55766e04f..a69570ea1 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -1,6 +1,7 @@ //! Agents drive the economy of the MUSE 2.0 simulation, through relative investment in different //! assets. use crate::commodity::Commodity; +use crate::id::define_id_getter; use crate::region::RegionSelection; use indexmap::IndexMap; use serde::Deserialize; @@ -33,6 +34,7 @@ pub struct Agent { /// The agent's objectives. pub objectives: Vec, } +define_id_getter! {Agent} /// Which processes apply to this agent #[derive(Debug, Clone, PartialEq)] @@ -46,8 +48,6 @@ pub enum SearchSpace { /// Search space for an agent #[derive(Debug, Clone, PartialEq)] pub struct AgentSearchSpace { - /// Unique agent id identifying the agent this search space belongs to - pub agent_id: String, /// The year the objective is relevant for pub year: u32, /// The commodity to apply the search space to diff --git a/src/asset.rs b/src/asset.rs index 93bc9cad8..5f13dc831 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -197,7 +197,6 @@ mod tests { time_of_day: "day".into(), }; let process_param = ProcessParameter { - process_id: "process1".into(), years: 2010..=2020, capital_cost: 5.0, fixed_operating_cost: 2.0, @@ -246,7 +245,6 @@ mod tests { fn create_asset_pool() -> AssetPool { let process_param = ProcessParameter { - process_id: "process1".into(), years: 2010..=2020, capital_cost: 5.0, fixed_operating_cost: 2.0, diff --git a/src/commodity.rs b/src/commodity.rs index 4d4050847..f1eb9d4a4 100644 --- a/src/commodity.rs +++ b/src/commodity.rs @@ -1,5 +1,5 @@ #![allow(missing_docs)] -use crate::input::{define_id_getter, HasID}; +use crate::id::define_id_getter; use crate::time_slice::{TimeSliceID, TimeSliceLevel}; use indexmap::IndexMap; use serde::Deserialize; diff --git a/src/id.rs b/src/id.rs new file mode 100644 index 000000000..8a29c955b --- /dev/null +++ b/src/id.rs @@ -0,0 +1,63 @@ +//! Code for handing IDs +use anyhow::{Context, Result}; +use std::collections::HashSet; +use std::rc::Rc; + +/// Indicates that the struct has an ID field +pub trait HasID { + /// Get a string representation of the struct's ID + fn get_id(&self) -> &str; +} + +/// An object which is associated with a single region +pub trait HasRegionID { + /// Get the associated region ID + fn get_region_id(&self) -> &str; +} + +/// Implement the `HasID` trait for the given type, assuming it has a field called `id` +macro_rules! define_id_getter { + ($t:ty) => { + impl crate::id::HasID for $t { + fn get_id(&self) -> &str { + &self.id + } + } + }; +} +pub(crate) use define_id_getter; + +/// Implement the `HasRegionID` trait for the given type, assuming it has a field called `region_id` +macro_rules! define_region_id_getter { + ($t:ty) => { + impl crate::id::HasRegionID for $t { + fn get_region_id(&self) -> &str { + &self.region_id + } + } + }; +} +pub(crate) use define_region_id_getter; + +/// A data structure containing a set of IDs +pub trait IDCollection { + /// Get the ID after checking that it exists this collection. + /// + /// # Arguments + /// + /// * `id` - The ID to look up + /// + /// # Returns + /// + /// A copy of the `Rc` in `self` or an error if not found. + fn get_id(&self, id: &str) -> Result>; +} + +impl IDCollection for HashSet> { + fn get_id(&self, id: &str) -> Result> { + let id = self + .get(id) + .with_context(|| format!("Unknown ID {id} found"))?; + Ok(Rc::clone(id)) + } +} diff --git a/src/input.rs b/src/input.rs index 7020b4a95..608d7fcb2 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,12 +1,13 @@ //! Common routines for handling input data. use crate::asset::AssetPool; +use crate::id::HasID; use crate::model::{Model, ModelFile}; use anyhow::{bail, ensure, Context, Result}; use float_cmp::approx_eq; use indexmap::IndexMap; use itertools::Itertools; use serde::de::{Deserialize, DeserializeOwned, Deserializer}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::fs; use std::path::Path; use std::rc::Rc; @@ -96,48 +97,6 @@ pub fn input_err_msg>(file_path: P) -> String { format!("Error reading {}", file_path.as_ref().display()) } -/// Indicates that the struct has an ID field -pub trait HasID { - /// Get a string representation of the struct's ID - fn get_id(&self) -> &str; -} - -/// Implement the `HasID` trait for the given type, assuming it has a field called `id` -macro_rules! define_id_getter { - ($t:ty) => { - impl HasID for $t { - fn get_id(&self) -> &str { - &self.id - } - } - }; -} - -pub(crate) use define_id_getter; - -/// A data structure containing a set of IDs -pub trait IDCollection { - /// Get the ID after checking that it exists this collection. - /// - /// # Arguments - /// - /// * `id` - The ID to look up - /// - /// # Returns - /// - /// A copy of the `Rc` in `self` or an error if not found. - fn get_id(&self, id: &str) -> Result>; -} - -impl IDCollection for HashSet> { - fn get_id(&self, id: &str) -> Result> { - let id = self - .get(id) - .with_context(|| format!("Unknown ID {id} found"))?; - Ok(Rc::clone(id)) - } -} - /// Read a CSV file of items with IDs. /// /// As this function is only ever used for top-level CSV files (i.e. the ones which actually define @@ -166,36 +125,6 @@ where fill_and_validate_map(file_path).with_context(|| input_err_msg(file_path)) } -/// Trait for converting an iterator into a [`HashMap`] grouped by IDs. -pub trait IntoIDMap { - /// Convert into a [`HashMap`] grouped by IDs. - fn into_id_map(self, ids: &HashSet>) -> Result, Vec>>; -} - -impl IntoIDMap for I -where - T: HasID, - I: Iterator, -{ - /// Convert the specified iterator into a `HashMap` of the items grouped by ID. - /// - /// # Arguments - /// - /// `ids` - The set of valid IDs to check against. - fn into_id_map(self, ids: &HashSet>) -> Result, Vec>> { - let map = self - .map(|item| -> Result<_> { - let id = ids.get_id(item.get_id())?; - Ok((id, item)) - }) - .process_results(|iter| iter.into_group_map())?; - - ensure!(!map.is_empty(), "CSV file is empty"); - - Ok(map) - } -} - /// Check that fractions sum to (approximately) one fn check_fractions_sum_to_one(fractions: I) -> Result<()> where diff --git a/src/input/agent/objective.rs b/src/input/agent/objective.rs index 8a071226c..26c263381 100644 --- a/src/input/agent/objective.rs +++ b/src/input/agent/objective.rs @@ -1,6 +1,6 @@ //! Code for reading the agent objectives CSV file. use super::super::*; -use crate::agent::{Agent, AgentMap, AgentObjective, DecisionRule}; +use crate::agent::{AgentMap, AgentObjective, DecisionRule}; use anyhow::{ensure, Context, Result}; use std::collections::HashMap; use std::path::Path; @@ -8,8 +8,6 @@ use std::rc::Rc; const AGENT_OBJECTIVES_FILE_NAME: &str = "agent_objectives.csv"; -define_id_getter! {Agent} - /// Read agent objective info from the agent_objectives.csv file. /// /// # Arguments @@ -172,6 +170,7 @@ fn check_agent_objectives( #[cfg(test)] mod tests { use super::*; + use crate::agent::Agent; use crate::agent::ObjectiveType; use crate::region::RegionSelection; diff --git a/src/input/agent/region.rs b/src/input/agent/region.rs index 10792325c..b75650ed3 100644 --- a/src/input/agent/region.rs +++ b/src/input/agent/region.rs @@ -1,6 +1,6 @@ //! Code for loading the agent regions CSV file. -use super::super::region::{define_region_id_getter, read_regions_for_entity}; -use super::super::HasID; +use super::super::region::read_regions_for_entity; +use crate::id::{define_region_id_getter, HasID}; use crate::region::RegionSelection; use anyhow::Result; use serde::Deserialize; diff --git a/src/input/agent/search_space.rs b/src/input/agent/search_space.rs index ed0743a0f..f619112f2 100644 --- a/src/input/agent/search_space.rs +++ b/src/input/agent/search_space.rs @@ -2,6 +2,7 @@ use super::super::*; use crate::agent::{AgentMap, AgentSearchSpace, SearchSpace}; use crate::commodity::CommodityMap; +use crate::id::IDCollection; use anyhow::{Context, Result}; use serde::Deserialize; use std::collections::HashMap; @@ -56,7 +57,6 @@ impl AgentSearchSpaceRaw { // Create AgentSearchSpace Ok(AgentSearchSpace { - agent_id: self.agent_id.clone(), year: self.year, commodity: Rc::clone(commodity), search_space, @@ -97,12 +97,12 @@ where I: Iterator, { let mut search_spaces = HashMap::new(); - for search_space in iter { + for search_space_raw in iter { let search_space = - search_space.to_agent_search_space(process_ids, commodities, milestone_years)?; + search_space_raw.to_agent_search_space(process_ids, commodities, milestone_years)?; let (id, _agent) = agents - .get_key_value(search_space.agent_id.as_str()) + .get_key_value(search_space_raw.agent_id.as_str()) .context("Invalid agent ID")?; // Append to Vec with the corresponding key or create diff --git a/src/input/asset.rs b/src/input/asset.rs index 1a5497532..03002f029 100644 --- a/src/input/asset.rs +++ b/src/input/asset.rs @@ -1,6 +1,7 @@ //! Code for reading [Asset]s from a CSV file. use super::*; use crate::asset::Asset; +use crate::id::IDCollection; use crate::process::ProcessMap; use anyhow::{ensure, Context, Result}; use itertools::Itertools; @@ -100,7 +101,6 @@ mod tests { #[test] fn test_read_assets_from_iter() { let process_param = ProcessParameter { - process_id: "process1".into(), years: 2010..=2020, capital_cost: 5.0, fixed_operating_cost: 2.0, diff --git a/src/input/commodity/cost.rs b/src/input/commodity/cost.rs index b3012146c..afd80e705 100644 --- a/src/input/commodity/cost.rs +++ b/src/input/commodity/cost.rs @@ -1,6 +1,7 @@ //! Code for reading in the commodity cost CSV file. use super::super::*; use crate::commodity::{BalanceType, CommodityCost, CommodityCostMap}; +use crate::id::IDCollection; use crate::time_slice::TimeSliceInfo; use anyhow::{ensure, Context, Result}; use serde::Deserialize; diff --git a/src/input/commodity/demand.rs b/src/input/commodity/demand.rs index 59f7633ac..46c42997a 100644 --- a/src/input/commodity/demand.rs +++ b/src/input/commodity/demand.rs @@ -3,6 +3,7 @@ use super::super::*; use super::demand_slicing::{read_demand_slices, DemandSliceMap, DemandSliceMapKey}; use crate::commodity::DemandMap; +use crate::id::IDCollection; use crate::time_slice::TimeSliceInfo; use anyhow::{ensure, Result}; use serde::Deserialize; diff --git a/src/input/commodity/demand_slicing.rs b/src/input/commodity/demand_slicing.rs index d954f6ab1..513154fab 100644 --- a/src/input/commodity/demand_slicing.rs +++ b/src/input/commodity/demand_slicing.rs @@ -1,6 +1,7 @@ //! Demand slicing determines how annual demand is distributed across the year. use super::super::*; use super::demand::*; +use crate::id::IDCollection; use crate::time_slice::{TimeSliceID, TimeSliceInfo}; use anyhow::{ensure, Context, Result}; use itertools::Itertools; diff --git a/src/input/process.rs b/src/input/process.rs index e57add416..589aab1e3 100644 --- a/src/input/process.rs +++ b/src/input/process.rs @@ -17,21 +17,11 @@ use flow::read_process_flows; mod parameter; use parameter::read_process_parameters; mod region; +use crate::id::define_id_getter; use region::read_process_regions; const PROCESSES_FILE_NAME: &str = "processes.csv"; -macro_rules! define_process_id_getter { - ($t:ty) => { - impl HasID for $t { - fn get_id(&self) -> &str { - &self.process_id - } - } - }; -} -use define_process_id_getter; - #[derive(PartialEq, Debug, Deserialize)] struct ProcessDescription { id: Rc, @@ -307,7 +297,6 @@ mod tests { .into_iter() .map(|id| { let parameter = ProcessParameter { - process_id: id.to_string(), years: 2010..=2020, capital_cost: 0.0, fixed_operating_cost: 0.0, diff --git a/src/input/process/availability.rs b/src/input/process/availability.rs index ea71dbc1b..87502181a 100644 --- a/src/input/process/availability.rs +++ b/src/input/process/availability.rs @@ -1,5 +1,6 @@ //! Code for reading process availabilities CSV file use super::super::*; +use crate::id::IDCollection; use crate::process::ActivityLimitsMap; use crate::time_slice::TimeSliceInfo; use anyhow::{Context, Result}; diff --git a/src/input/process/flow.rs b/src/input/process/flow.rs index fd3b0037a..92ec927e7 100644 --- a/src/input/process/flow.rs +++ b/src/input/process/flow.rs @@ -1,10 +1,9 @@ //! Code for reading process flows file use super::super::*; -use super::define_process_id_getter; use crate::commodity::CommodityMap; +use crate::id::IDCollection; use crate::process::{FlowType, ProcessFlow}; use anyhow::{ensure, Context, Result}; -use itertools::Itertools; use serde::Deserialize; use std::collections::{HashMap, HashSet}; use std::path::Path; @@ -12,8 +11,6 @@ use std::rc::Rc; const PROCESS_FLOWS_FILE_NAME: &str = "process_flows.csv"; -define_process_id_getter! {ProcessFlow} - #[derive(PartialEq, Debug, Deserialize)] struct ProcessFlowRaw { process_id: String, @@ -24,7 +21,6 @@ struct ProcessFlowRaw { flow_cost: Option, is_pac: bool, } -define_process_id_getter! {ProcessFlowRaw} /// Read process flows from a CSV file pub fn read_process_flows( @@ -47,44 +43,51 @@ fn read_process_flows_from_iter( where I: Iterator, { - let flows = iter - .map(|flow| -> Result { - let commodity = commodities - .get(flow.commodity_id.as_str()) - .with_context(|| format!("{} is not a valid commodity ID", &flow.commodity_id))?; + let mut flows = HashMap::new(); + for flow in iter { + let commodity = commodities + .get(flow.commodity_id.as_str()) + .with_context(|| format!("{} is not a valid commodity ID", &flow.commodity_id))?; - ensure!(flow.flow != 0.0, "Flow cannot be zero"); + ensure!(flow.flow != 0.0, "Flow cannot be zero"); - // Check that flow is not infinity, nan, etc. - ensure!( - flow.flow.is_normal(), - "Invalid value for flow ({})", - flow.flow - ); + // Check that flow is not infinity, nan, etc. + ensure!( + flow.flow.is_normal(), + "Invalid value for flow ({})", + flow.flow + ); - // **TODO**: https://github.com/EnergySystemsModellingLab/MUSE_2.0/issues/300 - ensure!( - flow.flow_type == FlowType::Fixed, - "Commodity flexible assets are not currently supported" - ); + // **TODO**: https://github.com/EnergySystemsModellingLab/MUSE_2.0/issues/300 + ensure!( + flow.flow_type == FlowType::Fixed, + "Commodity flexible assets are not currently supported" + ); - if let Some(flow_cost) = flow.flow_cost { - ensure!( - (0.0..f64::INFINITY).contains(&flow_cost), - "Invalid value for flow cost ({flow_cost}). Must be >=0." - ) - } + if let Some(flow_cost) = flow.flow_cost { + ensure!( + (0.0..f64::INFINITY).contains(&flow_cost), + "Invalid value for flow cost ({flow_cost}). Must be >=0." + ) + } - Ok(ProcessFlow { - process_id: flow.process_id, - commodity: Rc::clone(commodity), - flow: flow.flow, - flow_type: flow.flow_type, - flow_cost: flow.flow_cost.unwrap_or(0.0), - is_pac: flow.is_pac, - }) - }) - .process_results(|iter| iter.into_id_map(process_ids))??; + // Create ProcessFlow object + let process_id = process_ids.get_id(&flow.process_id)?; + let process_flow = ProcessFlow { + process_id: flow.process_id, + commodity: Rc::clone(commodity), + flow: flow.flow, + flow_type: flow.flow_type, + flow_cost: flow.flow_cost.unwrap_or(0.0), + is_pac: flow.is_pac, + }; + + // Insert into the map + flows + .entry(process_id) + .or_insert_with(Vec::new) + .push(process_flow); + } validate_flows(&flows)?; validate_pac_flows(&flows)?; diff --git a/src/input/process/parameter.rs b/src/input/process/parameter.rs index 40d6d4d0d..37d4c03ae 100644 --- a/src/input/process/parameter.rs +++ b/src/input/process/parameter.rs @@ -1,6 +1,6 @@ //! Code for reading process parameters CSV file use super::super::*; -use super::define_process_id_getter; +use crate::id::IDCollection; use crate::process::ProcessParameter; use ::log::warn; use anyhow::{ensure, Context, Result}; @@ -24,7 +24,6 @@ struct ProcessParameterRaw { discount_rate: Option, capacity_to_activity: Option, } -define_process_id_getter! {ProcessParameterRaw} impl ProcessParameterRaw { fn into_parameter(self, year_range: &RangeInclusive) -> Result { @@ -41,7 +40,6 @@ impl ProcessParameterRaw { self.validate()?; Ok(ProcessParameter { - process_id: self.process_id, years: start_year..=end_year, capital_cost: self.capital_cost, fixed_operating_cost: self.fixed_operating_cost, @@ -126,9 +124,9 @@ where I: Iterator, { let mut params = HashMap::new(); - for param in iter { - let param = param.into_parameter(year_range)?; - let id = process_ids.get_id(¶m.process_id)?; + for param_raw in iter { + let id = process_ids.get_id(¶m_raw.process_id)?; + let param = param_raw.into_parameter(year_range)?; ensure!( params.insert(Rc::clone(&id), param).is_none(), "More than one parameter provided for process {id}" @@ -167,7 +165,6 @@ mod tests { capacity_to_activity: f64, ) -> ProcessParameter { ProcessParameter { - process_id: "id".to_string(), years, capital_cost: 0.0, fixed_operating_cost: 0.0, @@ -314,7 +311,6 @@ mod tests { ( "A".into(), ProcessParameter { - process_id: "A".into(), years: 2010..=2020, capital_cost: 1.0, fixed_operating_cost: 1.0, @@ -327,7 +323,6 @@ mod tests { ( "B".into(), ProcessParameter { - process_id: "B".into(), years: 2015..=2020, capital_cost: 1.0, fixed_operating_cost: 1.0, diff --git a/src/input/process/region.rs b/src/input/process/region.rs index 4c5607828..9ce39ee73 100644 --- a/src/input/process/region.rs +++ b/src/input/process/region.rs @@ -1,7 +1,6 @@ //! Code for reading the process region CSV file -use super::super::region::{define_region_id_getter, read_regions_for_entity}; -use super::super::*; -use super::define_process_id_getter; +use super::super::region::read_regions_for_entity; +use crate::id::{define_region_id_getter, HasID}; use crate::region::RegionSelection; use anyhow::Result; use serde::Deserialize; @@ -16,9 +15,14 @@ struct ProcessRegion { process_id: String, region_id: String, } -define_process_id_getter! {ProcessRegion} define_region_id_getter! {ProcessRegion} +impl HasID for ProcessRegion { + fn get_id(&self) -> &str { + &self.process_id + } +} + /// Read the process regions file. /// /// # Arguments diff --git a/src/input/region.rs b/src/input/region.rs index 0944a1cbf..d697c32f3 100644 --- a/src/input/region.rs +++ b/src/input/region.rs @@ -1,6 +1,7 @@ //! Code for reading region-related information from CSV files. use super::*; -use crate::region::{Region, RegionMap, RegionSelection}; +use crate::id::{HasID, HasRegionID, IDCollection}; +use crate::region::{RegionMap, RegionSelection}; use anyhow::{anyhow, ensure, Context, Result}; use serde::de::DeserializeOwned; use std::collections::{HashMap, HashSet}; @@ -9,25 +10,6 @@ use std::rc::Rc; const REGIONS_FILE_NAME: &str = "regions.csv"; -define_id_getter! {Region} - -/// An object which is associated with a single region -pub trait HasRegionID { - /// Get the associated region ID - fn get_region_id(&self) -> &str; -} - -macro_rules! define_region_id_getter { - ($t:ty) => { - impl crate::input::region::HasRegionID for $t { - fn get_region_id(&self) -> &str { - &self.region_id - } - } - }; -} -pub(crate) use define_region_id_getter; - /// Reads regions from a CSV file. /// /// # Arguments @@ -137,6 +119,8 @@ fn try_insert_region( #[cfg(test)] mod tests { use super::*; + use crate::id::{define_id_getter, define_region_id_getter}; + use crate::region::Region; use serde::Deserialize; use std::fs::File; use std::io::Write; diff --git a/src/lib.rs b/src/lib.rs index b0aa62be8..2290ab0d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod agent; pub mod asset; pub mod commands; pub mod commodity; +pub mod id; pub mod input; pub mod log; pub mod model; diff --git a/src/output.rs b/src/output.rs index 92a24fcc2..c0c7460ea 100644 --- a/src/output.rs +++ b/src/output.rs @@ -189,7 +189,6 @@ mod tests { let agent_id = "agent1".into(); let commission_year = 2015; let process_param = ProcessParameter { - process_id: "process1".to_string(), years: 2010..=2020, capital_cost: 5.0, fixed_operating_cost: 2.0, diff --git a/src/process.rs b/src/process.rs index be53ff9b2..ac9e20567 100644 --- a/src/process.rs +++ b/src/process.rs @@ -93,8 +93,6 @@ pub enum FlowType { /// Additional parameters for a process #[derive(PartialEq, Clone, Debug, Deserialize)] pub struct ProcessParameter { - /// A unique identifier for the process - pub process_id: String, /// The years in which this process is available for investment pub years: RangeInclusive, /// Overnight capital cost per unit capacity diff --git a/src/region.rs b/src/region.rs index 609c5369b..a9d188cd2 100644 --- a/src/region.rs +++ b/src/region.rs @@ -1,4 +1,5 @@ //! Regions represent different geographical areas in which agents, processes, etc. are active. +use crate::id::define_id_getter; use indexmap::IndexMap; use itertools::Itertools; use serde::Deserialize; @@ -17,6 +18,7 @@ pub struct Region { /// A text description of the region (e.g. "United Kingdom"). pub description: String, } +define_id_getter! {Region} /// Represents multiple regions #[derive(PartialEq, Debug, Clone, Default)] diff --git a/src/simulation/optimisation.rs b/src/simulation/optimisation.rs index 29b86645b..491f6d128 100644 --- a/src/simulation/optimisation.rs +++ b/src/simulation/optimisation.rs @@ -291,7 +291,6 @@ mod tests { costs: CommodityCostMap, ) -> (Asset, ProcessFlow) { let process_param = ProcessParameter { - process_id: "process1".into(), years: 2010..=2020, capital_cost: 5.0, fixed_operating_cost: 2.0,