1use super::{input_err_msg, read_csv};
3use crate::agent::{
4 Agent, AgentCommodityPortionsMap, AgentID, AgentMap, AgentObjectiveMap, AgentSearchSpaceMap,
5 DecisionRule,
6};
7use crate::commodity::CommodityMap;
8use crate::process::ProcessMap;
9use crate::region::{RegionID, parse_region_str};
10use anyhow::{Context, Result, bail, ensure};
11use indexmap::IndexSet;
12use serde::Deserialize;
13use std::path::Path;
14
15mod objective;
16use objective::read_agent_objectives;
17mod search_space;
18use search_space::read_agent_search_space;
19mod commodity_portion;
20use commodity_portion::read_agent_commodity_portions;
21
22const AGENT_FILE_NAME: &str = "agents.csv";
23
24#[derive(Debug, Deserialize, PartialEq, Clone)]
26struct AgentRaw {
27 id: String,
29 description: String,
31 regions: String,
33 decision_rule: String,
35 decision_lexico_tolerance: Option<f64>,
37}
38
39pub fn read_agents(
53 model_dir: &Path,
54 commodities: &CommodityMap,
55 processes: &ProcessMap,
56 region_ids: &IndexSet<RegionID>,
57 milestone_years: &[u32],
58) -> Result<AgentMap> {
59 let mut agents = read_agents_file(model_dir, region_ids)?;
60
61 let mut agent_commodities = read_agent_commodity_portions(
63 model_dir,
64 &agents,
65 commodities,
66 region_ids,
67 milestone_years,
68 )?;
69 for (id, agent) in &mut agents {
70 agent.commodity_portions = agent_commodities
71 .remove(id)
72 .with_context(|| format!("Missing commodity portions for agent {id}"))?;
73 }
74
75 let mut objectives = read_agent_objectives(model_dir, &agents, milestone_years)?;
76 let commodity_ids = commodities.keys().cloned().collect();
77 let mut search_spaces = read_agent_search_space(
78 model_dir,
79 &agents,
80 processes,
81 &commodity_ids,
82 milestone_years,
83 )?;
84
85 for (id, agent) in &mut agents {
86 agent.objectives = objectives.remove(id).unwrap();
87 agent.search_space = search_spaces.remove(id).unwrap();
88 }
89
90 Ok(agents)
91}
92
93fn read_agents_file(model_dir: &Path, region_ids: &IndexSet<RegionID>) -> Result<AgentMap> {
104 let file_path = model_dir.join(AGENT_FILE_NAME);
105 let agents_csv = read_csv(&file_path)?;
106 read_agents_file_from_iter(agents_csv, region_ids).with_context(|| input_err_msg(&file_path))
107}
108
109fn read_agents_file_from_iter<I>(iter: I, region_ids: &IndexSet<RegionID>) -> Result<AgentMap>
111where
112 I: Iterator<Item = AgentRaw>,
113{
114 let mut agents = AgentMap::new();
115 for agent_raw in iter {
116 let regions = parse_region_str(&agent_raw.regions, region_ids)?;
118
119 let decision_rule = match agent_raw.decision_rule.to_ascii_lowercase().as_str() {
121 "single" => DecisionRule::Single,
122 "weighted" => DecisionRule::Weighted,
123 "lexico" => {
124 let tolerance = agent_raw
125 .decision_lexico_tolerance
126 .with_context(|| "Missing tolerance for lexico decision rule")?;
127 ensure!(
128 tolerance >= 0.0,
129 "Lexico tolerance must be non-negative, got {tolerance}"
130 );
131 DecisionRule::Lexicographical { tolerance }
132 }
133 invalid_rule => bail!("Invalid decision rule: {invalid_rule}"),
134 };
135
136 ensure!(
137 decision_rule == DecisionRule::Single,
138 "Currently only the \"single\" decision rule is supported"
139 );
140
141 let agent = Agent {
142 id: AgentID(agent_raw.id.into()),
143 description: agent_raw.description,
144 commodity_portions: AgentCommodityPortionsMap::new(),
145 search_space: AgentSearchSpaceMap::new(),
146 decision_rule,
147 regions,
148 objectives: AgentObjectiveMap::new(),
149 };
150
151 ensure!(
152 agents.insert(agent.id.clone(), agent).is_none(),
153 "Duplicate agent ID"
154 );
155 }
156
157 Ok(agents)
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::agent::DecisionRule;
164 use std::iter;
165
166 #[test]
167 fn read_agents_file_from_iter_works() {
168 let region_ids = IndexSet::from(["GBR".into()]);
170 let agent = AgentRaw {
171 id: "agent".into(),
172 description: String::new(),
173 decision_rule: "single".into(),
174 decision_lexico_tolerance: None,
175 regions: "GBR".into(),
176 };
177 let agent_out = Agent {
178 id: "agent".into(),
179 description: String::new(),
180 commodity_portions: AgentCommodityPortionsMap::new(),
181 search_space: AgentSearchSpaceMap::new(),
182 decision_rule: DecisionRule::Single,
183 regions: IndexSet::from(["GBR".into()]),
184 objectives: AgentObjectiveMap::new(),
185 };
186 let expected = AgentMap::from_iter(iter::once(("agent".into(), agent_out)));
187 let actual = read_agents_file_from_iter(iter::once(agent), ®ion_ids).unwrap();
188 assert_eq!(actual, expected);
189
190 let agents = [
192 AgentRaw {
193 id: "agent".into(),
194 description: String::new(),
195 decision_rule: "single".into(),
196 decision_lexico_tolerance: None,
197 regions: "GBR".into(),
198 },
199 AgentRaw {
200 id: "agent".into(),
201 description: String::new(),
202 decision_rule: "single".into(),
203 decision_lexico_tolerance: None,
204 regions: "GBR".into(),
205 },
206 ];
207 read_agents_file_from_iter(agents.into_iter(), ®ion_ids).unwrap_err();
208
209 let agent = AgentRaw {
211 id: "agent".into(),
212 description: String::new(),
213 decision_rule: "lexico".into(),
214 decision_lexico_tolerance: None,
215 regions: "GBR".into(),
216 };
217 read_agents_file_from_iter(iter::once(agent), ®ion_ids).unwrap_err();
218 }
219}