diff --git a/examples/simple/agent_search_space.csv b/examples/simple/agent_search_space.csv new file mode 100644 index 000000000..b2942ec7d --- /dev/null +++ b/examples/simple/agent_search_space.csv @@ -0,0 +1 @@ +agent_id,commodity_id,year,search_space diff --git a/src/agent.rs b/src/agent.rs index acf597ad7..a38ae994e 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -26,7 +26,7 @@ pub struct Agent { /// The proportion of the commodity production that the agent is responsible for. pub commodity_portion: f64, /// The processes that the agent will consider investing in. - pub search_space: SearchSpace, + pub search_space: Vec, /// The decision rule that the agent uses to decide investment. pub decision_rule: DecisionRule, /// The maximum capital cost the agent will pay. @@ -48,6 +48,19 @@ pub enum SearchSpace { Some(HashSet>), } +/// 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 + pub commodity: Rc, + /// The agent's search space + pub search_space: SearchSpace, +} + /// The decision rule for a particular objective #[derive(Debug, Clone, PartialEq)] pub enum DecisionRule { diff --git a/src/input/agent.rs b/src/input/agent.rs index e2c045035..984dc5dbb 100644 --- a/src/input/agent.rs +++ b/src/input/agent.rs @@ -1,6 +1,6 @@ //! Code for reading in agent-related data from CSV files. use super::*; -use crate::agent::{Agent, AgentMap, DecisionRule, SearchSpace}; +use crate::agent::{Agent, AgentMap, DecisionRule}; use crate::commodity::CommodityMap; use crate::process::ProcessMap; use crate::region::RegionSelection; @@ -14,6 +14,8 @@ pub mod objective; use objective::read_agent_objectives; pub mod region; use region::read_agent_regions; +pub mod search_space; +use search_space::read_agent_search_space; const AGENT_FILE_NAME: &str = "agents.csv"; @@ -29,9 +31,6 @@ struct AgentRaw { /// The proportion of the commodity production that the agent is responsible for. #[serde(deserialize_with = "deserialise_proportion_nonzero")] commodity_portion: f64, - /// The processes that the agent will consider investing in. Expressed as process IDs separated - /// by semicolons or `None`, meaning all processes. - search_space: Option, /// The decision rule that the agent uses to decide investment. decision_rule: String, /// The maximum capital cost the agent will pay. @@ -62,15 +61,25 @@ pub fn read_agents( milestone_years: &[u32], ) -> Result { let process_ids = processes.keys().cloned().collect(); - let mut agents = read_agents_file(model_dir, commodities, &process_ids)?; + let mut agents = read_agents_file(model_dir, commodities)?; let agent_ids = agents.keys().cloned().collect(); let mut agent_regions = read_agent_regions(model_dir, &agent_ids, region_ids)?; let mut objectives = read_agent_objectives(model_dir, &agents, milestone_years)?; + let mut search_spaces = read_agent_search_space( + model_dir, + &agents, + &process_ids, + commodities, + milestone_years, + )?; for (id, agent) in agents.iter_mut() { agent.regions = agent_regions.remove(id).unwrap(); agent.objectives = objectives.remove(id).unwrap(); + if let Some(search_space) = search_spaces.remove(id) { + agent.search_space = search_space; + } } Ok(agents) @@ -87,23 +96,14 @@ pub fn read_agents( /// # Returns /// /// A map of Agents, with the agent ID as the key -pub fn read_agents_file( - model_dir: &Path, - commodities: &CommodityMap, - process_ids: &HashSet>, -) -> Result { +pub fn read_agents_file(model_dir: &Path, commodities: &CommodityMap) -> Result { let file_path = model_dir.join(AGENT_FILE_NAME); let agents_csv = read_csv(&file_path)?; - read_agents_file_from_iter(agents_csv, commodities, process_ids) - .with_context(|| input_err_msg(&file_path)) + read_agents_file_from_iter(agents_csv, commodities).with_context(|| input_err_msg(&file_path)) } /// Read agents info from an iterator. -fn read_agents_file_from_iter( - iter: I, - commodities: &CommodityMap, - process_ids: &HashSet>, -) -> Result +fn read_agents_file_from_iter(iter: I, commodities: &CommodityMap) -> Result where I: Iterator, { @@ -113,19 +113,6 @@ where .get(agent_raw.commodity_id.as_str()) .context("Invalid commodity ID")?; - // Parse search space string - let search_space = match agent_raw.search_space { - None => SearchSpace::AllProcesses, - Some(processes) => { - let mut set = HashSet::new(); - for id in processes.split(';') { - set.insert(process_ids.get_id(id)?); - } - - SearchSpace::Some(set) - } - }; - // Parse decision rule let decision_rule = match agent_raw.decision_rule.to_ascii_lowercase().as_str() { "single" => DecisionRule::Single, @@ -149,7 +136,7 @@ where description: agent_raw.description, commodity: Rc::clone(commodity), commodity_portion: agent_raw.commodity_portion, - search_space, + search_space: Vec::new(), decision_rule, capex_limit: agent_raw.capex_limit, annual_cost_limit: agent_raw.annual_cost_limit, @@ -177,7 +164,6 @@ mod tests { #[test] fn test_read_agents_file_from_iter() { - let process_ids = ["A".into(), "B".into(), "C".into()].into_iter().collect(); let commodity = Rc::new(Commodity { id: "commodity1".into(), description: "A commodity".into(), @@ -189,13 +175,11 @@ mod tests { let commodities = iter::once(("commodity1".into(), Rc::clone(&commodity))).collect(); // Valid case - let search_space = HashSet::from_iter(["A".into(), "B".into()]); let agent = AgentRaw { id: "agent".into(), description: "".into(), commodity_id: "commodity1".into(), commodity_portion: 1.0, - search_space: Some("A;B".into()), decision_rule: "single".into(), capex_limit: None, annual_cost_limit: None, @@ -206,7 +190,7 @@ mod tests { description: "".into(), commodity, commodity_portion: 1.0, - search_space: SearchSpace::Some(search_space), + search_space: Vec::new(), decision_rule: DecisionRule::Single, capex_limit: None, annual_cost_limit: None, @@ -214,8 +198,7 @@ mod tests { objectives: Vec::new(), }; let expected = AgentMap::from_iter(iter::once(("agent".into(), agent_out))); - let actual = - read_agents_file_from_iter(iter::once(agent), &commodities, &process_ids).unwrap(); + let actual = read_agents_file_from_iter(iter::once(agent), &commodities).unwrap(); assert_eq!(actual, expected); // Invalid commodity ID @@ -224,27 +207,12 @@ mod tests { description: "".into(), commodity_id: "made_up_commodity".into(), commodity_portion: 1.0, - search_space: None, - decision_rule: "single".into(), - capex_limit: None, - annual_cost_limit: None, - decision_lexico_tolerance: None, - }; - assert!(read_agents_file_from_iter(iter::once(agent), &commodities, &process_ids).is_err()); - - // Invalid process ID - let agent = AgentRaw { - id: "agent".into(), - description: "".into(), - commodity_id: "commodity1".into(), - commodity_portion: 1.0, - search_space: Some("A;D".into()), decision_rule: "single".into(), capex_limit: None, annual_cost_limit: None, decision_lexico_tolerance: None, }; - assert!(read_agents_file_from_iter(iter::once(agent), &commodities, &process_ids).is_err()); + assert!(read_agents_file_from_iter(iter::once(agent), &commodities).is_err()); // Duplicate agent ID let agents = [ @@ -253,7 +221,6 @@ mod tests { description: "".into(), commodity_id: "commodity1".into(), commodity_portion: 1.0, - search_space: None, decision_rule: "single".into(), capex_limit: None, annual_cost_limit: None, @@ -264,16 +231,13 @@ mod tests { description: "".into(), commodity_id: "commodity1".into(), commodity_portion: 1.0, - search_space: None, decision_rule: "single".into(), capex_limit: None, annual_cost_limit: None, decision_lexico_tolerance: None, }, ]; - assert!( - read_agents_file_from_iter(agents.into_iter(), &commodities, &process_ids).is_err() - ); + assert!(read_agents_file_from_iter(agents.into_iter(), &commodities).is_err()); // Lexico tolerance missing for lexico decision rule let agent = AgentRaw { @@ -281,12 +245,11 @@ mod tests { description: "".into(), commodity_id: "commodity1".into(), commodity_portion: 1.0, - search_space: None, decision_rule: "lexico".into(), capex_limit: None, annual_cost_limit: None, decision_lexico_tolerance: None, }; - assert!(read_agents_file_from_iter(iter::once(agent), &commodities, &process_ids).is_err()); + assert!(read_agents_file_from_iter(iter::once(agent), &commodities).is_err()); } } diff --git a/src/input/agent/objective.rs b/src/input/agent/objective.rs index daaf12568..a0f160f46 100644 --- a/src/input/agent/objective.rs +++ b/src/input/agent/objective.rs @@ -172,7 +172,7 @@ fn check_agent_objectives( #[cfg(test)] mod tests { use super::*; - use crate::agent::{ObjectiveType, SearchSpace}; + use crate::agent::ObjectiveType; use crate::commodity::{Commodity, CommodityCostMap, CommodityType, DemandMap}; use crate::region::RegionSelection; use crate::time_slice::TimeSliceLevel; @@ -236,7 +236,7 @@ mod tests { description: "".into(), commodity, commodity_portion: 1.0, - search_space: SearchSpace::AllProcesses, + search_space: Vec::new(), decision_rule: DecisionRule::Single, capex_limit: None, annual_cost_limit: None, diff --git a/src/input/agent/search_space.rs b/src/input/agent/search_space.rs new file mode 100644 index 000000000..0f19f8053 --- /dev/null +++ b/src/input/agent/search_space.rs @@ -0,0 +1,171 @@ +//! Code for reading the agent search space CSV file. +use super::super::*; +use crate::agent::{AgentMap, AgentSearchSpace, SearchSpace}; +use crate::commodity::CommodityMap; +use anyhow::{Context, Result}; +use serde::Deserialize; +use std::collections::HashMap; +use std::path::Path; +use std::rc::Rc; + +const AGENT_SEARCH_SPACE_FILE_NAME: &str = "agent_search_space.csv"; + +#[derive(PartialEq, Debug, Deserialize)] +struct AgentSearchSpaceRaw { + /// The agent to apply the search space to. + pub agent_id: String, + /// The commodity to apply the search space to. + pub commodity_id: String, + /// The year to apply the search space to. + pub year: u32, + /// The processes that the agent will consider investing in. Expressed as process IDs separated + /// by semicolons or `None`, meaning all processes. + pub search_space: Option, +} + +impl AgentSearchSpaceRaw { + pub fn to_agent_search_space( + &self, + process_ids: &HashSet>, + commodities: &CommodityMap, + milestone_years: &[u32], + ) -> Result { + // Parse search_space string + let search_space = match &self.search_space { + None => SearchSpace::AllProcesses, + Some(processes) => { + let mut set = HashSet::new(); + for id in processes.split(';') { + set.insert(process_ids.get_id(id)?); + } + SearchSpace::Some(set) + } + }; + + // Get commodity + let commodity = commodities + .get(self.commodity_id.as_str()) + .context("Invalid commodity ID")?; + + // Check that the year is a valid milestone year + ensure!( + milestone_years.binary_search(&self.year).is_ok(), + "Invalid milestone year {}", + self.year + ); + + // Create AgentSearchSpace + Ok(AgentSearchSpace { + agent_id: self.agent_id.clone(), + year: self.year, + commodity: Rc::clone(commodity), + search_space, + }) + } +} + +/// Read agent search space info from the agent_search_space.csv file. +/// +/// # Arguments +/// +/// * `model_dir` - Folder containing model configuration files +/// +/// # Returns +/// +/// A map of Agents, with the agent ID as the key +pub fn read_agent_search_space( + model_dir: &Path, + agents: &AgentMap, + process_ids: &HashSet>, + commodities: &CommodityMap, + milestone_years: &[u32], +) -> Result, Vec>> { + let file_path = model_dir.join(AGENT_SEARCH_SPACE_FILE_NAME); + let iter = read_csv::(&file_path)?; + read_agent_search_space_from_iter(iter, agents, process_ids, commodities, milestone_years) + .with_context(|| input_err_msg(&file_path)) +} + +fn read_agent_search_space_from_iter( + iter: I, + agents: &AgentMap, + process_ids: &HashSet>, + commodities: &CommodityMap, + milestone_years: &[u32], +) -> Result, Vec>> +where + I: Iterator, +{ + let mut search_spaces = HashMap::new(); + for search_space in iter { + let search_space = + search_space.to_agent_search_space(process_ids, commodities, milestone_years)?; + + let (id, _agent) = agents + .get_key_value(search_space.agent_id.as_str()) + .context("Invalid agent ID")?; + + // Append to Vec with the corresponding key or create + search_spaces + .entry(Rc::clone(id)) + .or_insert_with(|| Vec::with_capacity(1)) + .push(search_space); + } + + Ok(search_spaces) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::commodity::{Commodity, CommodityCostMap, CommodityType, DemandMap}; + use crate::time_slice::TimeSliceLevel; + use std::iter; + + #[test] + fn test_search_space_raw_into_search_space() { + let process_ids = ["A".into(), "B".into(), "C".into()].into_iter().collect(); + let commodity = Rc::new(Commodity { + id: "commodity1".into(), + description: "A commodity".into(), + kind: CommodityType::SupplyEqualsDemand, + time_slice_level: TimeSliceLevel::Annual, + costs: CommodityCostMap::new(), + demand: DemandMap::new(), + }); + let commodities = iter::once(("commodity1".into(), Rc::clone(&commodity))).collect(); + + // Valid search space + let raw = AgentSearchSpaceRaw { + agent_id: "agent1".into(), + commodity_id: "commodity1".into(), + year: 2020, + search_space: Some("A;B".into()), + }; + assert!(raw + .to_agent_search_space(&process_ids, &commodities, &[2020]) + .is_ok()); + + // Invalid commodity ID + let raw = AgentSearchSpaceRaw { + agent_id: "agent1".into(), + commodity_id: "invalid_commodity".into(), + year: 2020, + search_space: Some("A;B".into()), + }; + assert!(raw + .to_agent_search_space(&process_ids, &commodities, &[2020]) + .is_err()); + + // Invalid process ID + let raw = AgentSearchSpaceRaw { + agent_id: "agent1".into(), + commodity_id: "commodity1".into(), + year: 2020, + search_space: Some("A;D".into()), + }; + assert!(raw + .to_agent_search_space(&process_ids, &commodities, &[2020]) + .is_err()); + } +}