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