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
9 changes: 9 additions & 0 deletions examples/simple/agent_commodities.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
agent_id,commodity_id,year,commodity_portion
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.

After thinking about this, it seems kinda weird to me that we don't also include a region field for the agent commodities stuff (we didn't when it was just a single commodity either so this isn't really about your PR). But how can it make sense for an agent to be responsible for e.g. 50% of the electricity in every region it operates in? Surely it would be different proportions for e.g. the UK and France if the agent operated in both?

Or have I missed something?

This is fine as a simplification for now. But it might be a limitation when it comes to modelling the real world.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good point and I agree. To be honest I still don't really have an intuitive understanding what it means for an agent to operate in multiple regions. Is it just a shortcut way of saying that we want identical agents in multiple regions, but these are still separate agents for each region (e.g. household agents in UK and France may have approximately similar demand profiles, so we simplify things by using a shared parameter set)? Or is it really a multi-region agent which receives demand from multiple regions and trades commodities between regions (e.g. an oil company)?

I discussed this with Adam last time we spoke and I think there are still open questions here which we/he need to think about. We also discussed scenarios where agents might have no fixed demand share and instead compete against each other to maximize demand share, which is something else that can't really be captured with this table.

Anyway, stuff to think about and bring up when we make plans for the next engagement, but I'd say it's fine to leave it for now

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.

Anyway, stuff to think about and bring up when we make plans for the next engagement, but I'd say it's fine to leave it for now

Agreed 👍

A0_GEX,GASPRD,2020,1
A0_GEX,GASPRD,2030,1
A0_GPR,GASNAT,2020,1
A0_GPR,GASNAT,2030,1
A0_ELC,ELCTRI,2020,1
A0_ELC,ELCTRI,2030,1
A0_RES,RSHEAT,2020,1
A0_RES,RSHEAT,2030,1
10 changes: 5 additions & 5 deletions examples/simple/agents.csv
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
id,description,commodity_id,commodity_portion,search_space,decision_rule,capex_limit,annual_cost_limit,decision_lexico_tolerance
A0_GEX,Gas extractors,GASPRD,1,,single,,,
A0_GPR,Gas processors,GASNAT,1,,single,,,
A0_ELC,Electricity generators,ELCTRI,1,,single,,,
A0_RES,Residential consumer,RSHEAT,1,,single,,,
id,description,decision_rule,capex_limit,annual_cost_limit,decision_lexico_tolerance
A0_GEX,Gas extractors,single,,,
A0_GPR,Gas processors,single,,,
A0_ELC,Electricity generators,single,,,
A0_RES,Residential consumer,single,,,
17 changes: 13 additions & 4 deletions src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@ pub struct Agent {
pub id: Rc<str>,
/// A text description of the agent.
pub description: String,
/// The commodity that the agent produces (could be a service demand too).
pub commodity: Rc<Commodity>,
/// The proportion of the commodity production that the agent is responsible for.
pub commodity_portion: f64,
/// The commodities that the agent is responsible for servicing.
pub commodities: Vec<AgentCommodity>,
/// The processes that the agent will consider investing in.
pub search_space: Vec<AgentSearchSpace>,
/// The decision rule that the agent uses to decide investment.
Expand Down Expand Up @@ -87,6 +85,17 @@ pub struct AgentObjective {
pub decision_lexico_order: Option<u32>,
}

/// A commodity that the agent is responsible for servicing, with associated commodity portion
#[derive(Debug, Clone, Deserialize, PartialEq)]
pub struct AgentCommodity {
/// The year the commodity portion applies to.
pub year: u32,
/// The commodity that the agent is responsible for servicing.
pub commodity: Rc<Commodity>,
/// The proportion of the commodity production that the agent is responsible for.
pub commodity_portion: f64,
}

/// The type of objective for the agent
///
/// **TODO** Add more objective types
Expand Down
67 changes: 14 additions & 53 deletions src/input/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub mod region;
use region::read_agent_regions;
pub mod search_space;
use search_space::read_agent_search_space;
pub mod commodity;
use commodity::read_agent_commodities;

const AGENT_FILE_NAME: &str = "agents.csv";

Expand All @@ -26,11 +28,6 @@ struct AgentRaw {
id: Rc<str>,
/// A text description of the agent.
description: String,
/// The commodity that the agent produces (could be a service demand too).
commodity_id: String,
/// The proportion of the commodity production that the agent is responsible for.
#[serde(deserialize_with = "deserialise_proportion_nonzero")]
commodity_portion: f64,
/// 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 @@ -61,7 +58,7 @@ 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)?;
let mut agents = read_agents_file(model_dir)?;
let agent_ids = agents.keys().cloned().collect();

let mut agent_regions = read_agent_regions(model_dir, &agent_ids, region_ids)?;
Expand All @@ -73,13 +70,16 @@ pub fn read_agents(
commodities,
milestone_years,
)?;
let mut agent_commodities =
read_agent_commodities(model_dir, &agents, commodities, region_ids, 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;
}
agent.commodities = agent_commodities.remove(id).unwrap();
}

Ok(agents)
Expand All @@ -96,23 +96,19 @@ 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) -> Result<AgentMap> {
pub fn read_agents_file(model_dir: &Path) -> 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).with_context(|| input_err_msg(&file_path))
read_agents_file_from_iter(agents_csv).with_context(|| input_err_msg(&file_path))
}

/// Read agents info from an iterator.
fn read_agents_file_from_iter<I>(iter: I, commodities: &CommodityMap) -> Result<AgentMap>
fn read_agents_file_from_iter<I>(iter: I) -> Result<AgentMap>
where
I: Iterator<Item = AgentRaw>,
{
let mut agents = AgentMap::new();
for agent_raw in iter {
let commodity = commodities
.get(agent_raw.commodity_id.as_str())
.context("Invalid commodity ID")?;

// Parse decision rule
let decision_rule = match agent_raw.decision_rule.to_ascii_lowercase().as_str() {
"single" => DecisionRule::Single,
Expand All @@ -134,8 +130,7 @@ where
let agent = Agent {
id: Rc::clone(&agent_raw.id),
description: agent_raw.description,
commodity: Rc::clone(commodity),
commodity_portion: agent_raw.commodity_portion,
commodities: Vec::new(),
search_space: Vec::new(),
decision_rule,
capex_limit: agent_raw.capex_limit,
Expand All @@ -157,29 +152,15 @@ where
mod tests {
use super::*;
use crate::agent::DecisionRule;
use crate::commodity::{Commodity, CommodityCostMap, CommodityType, DemandMap};
use crate::region::RegionSelection;
use crate::time_slice::TimeSliceLevel;
use std::iter;

#[test]
fn test_read_agents_file_from_iter() {
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 case
let agent = AgentRaw {
id: "agent".into(),
description: "".into(),
commodity_id: "commodity1".into(),
commodity_portion: 1.0,
decision_rule: "single".into(),
capex_limit: None,
annual_cost_limit: None,
Expand All @@ -188,8 +169,7 @@ mod tests {
let agent_out = Agent {
id: "agent".into(),
description: "".into(),
commodity,
commodity_portion: 1.0,
commodities: Vec::new(),
search_space: Vec::new(),
decision_rule: DecisionRule::Single,
capex_limit: None,
Expand All @@ -198,29 +178,14 @@ 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).unwrap();
let actual = read_agents_file_from_iter(iter::once(agent)).unwrap();
assert_eq!(actual, expected);

// Invalid commodity ID
let agent = AgentRaw {
id: "agent".into(),
description: "".into(),
commodity_id: "made_up_commodity".into(),
commodity_portion: 1.0,
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).is_err());

// Duplicate agent ID
let agents = [
AgentRaw {
id: "agent".into(),
description: "".into(),
commodity_id: "commodity1".into(),
commodity_portion: 1.0,
decision_rule: "single".into(),
capex_limit: None,
annual_cost_limit: None,
Expand All @@ -229,27 +194,23 @@ mod tests {
AgentRaw {
id: "agent".into(),
description: "".into(),
commodity_id: "commodity1".into(),
commodity_portion: 1.0,
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).is_err());
assert!(read_agents_file_from_iter(agents.into_iter()).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,
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).is_err());
assert!(read_agents_file_from_iter(iter::once(agent)).is_err());
}
}
Loading