Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/simple/agent_search_space.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
agent_id,commodity_id,year,search_space
15 changes: 14 additions & 1 deletion src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AgentSearchSpace>,
/// The decision rule that the agent uses to decide investment.
pub decision_rule: DecisionRule,
/// The maximum capital cost the agent will pay.
Expand All @@ -48,6 +48,19 @@ pub enum SearchSpace {
Some(HashSet<Rc<str>>),
}

/// 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,
Comment on lines +54 to +55
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be tempted to drop this field because we shouldn't need it (though there are some unneeded process IDs lurking around as well: #443)

/// The year the objective is relevant for
pub year: u32,
/// The commodity to apply the search space to
pub commodity: Rc<Commodity>,
/// The agent's search space
pub search_space: SearchSpace,
}

/// The decision rule for a particular objective
#[derive(Debug, Clone, PartialEq)]
pub enum DecisionRule {
Expand Down
83 changes: 23 additions & 60 deletions src/input/agent.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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";

Expand All @@ -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<String>,
/// The decision rule that the agent uses to decide investment.
decision_rule: String,
/// The maximum capital cost the agent will pay.
Expand Down Expand Up @@ -62,15 +61,25 @@ pub fn read_agents(
milestone_years: &[u32],
) -> Result<AgentMap> {
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)
Expand All @@ -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<Rc<str>>,
) -> Result<AgentMap> {
pub fn read_agents_file(model_dir: &Path, commodities: &CommodityMap) -> Result<AgentMap> {
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<I>(
iter: I,
commodities: &CommodityMap,
process_ids: &HashSet<Rc<str>>,
) -> Result<AgentMap>
fn read_agents_file_from_iter<I>(iter: I, commodities: &CommodityMap) -> Result<AgentMap>
where
I: Iterator<Item = AgentRaw>,
{
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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(),
Expand All @@ -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,
Expand All @@ -206,16 +190,15 @@ 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,
regions: RegionSelection::default(),
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
Expand All @@ -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 = [
Expand All @@ -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,
Expand All @@ -264,29 +231,25 @@ 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 {
id: "agent".into(),
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());
}
}
4 changes: 2 additions & 2 deletions src/input/agent/objective.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Loading