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