1use super::super::{input_err_msg, read_csv_optional, try_insert};
3use crate::agent::{Agent, AgentID, AgentMap, AgentSearchSpaceMap};
4use crate::commodity::CommodityID;
5use crate::id::IDCollection;
6use crate::process::{Process, ProcessMap};
7use crate::year::parse_year_str;
8use anyhow::{Context, Result};
9use itertools::Itertools;
10use serde::Deserialize;
11use std::collections::{HashMap, HashSet};
12use std::path::Path;
13use std::rc::Rc;
14
15const AGENT_SEARCH_SPACE_FILE_NAME: &str = "agent_search_space.csv";
16
17#[derive(PartialEq, Debug, Deserialize)]
18struct AgentSearchSpaceRaw {
19 agent_id: String,
21 commodity_id: String,
23 years: String,
25 search_space: String,
28}
29
30#[derive(Debug)]
32struct AgentSearchSpace {
33 agent_id: AgentID,
35 commodity_id: CommodityID,
37 years: Vec<u32>,
39 search_space: Rc<Vec<Rc<Process>>>,
41}
42
43impl AgentSearchSpaceRaw {
44 fn into_agent_search_space(
45 self,
46 agents: &AgentMap,
47 processes: &ProcessMap,
48 commodity_ids: &HashSet<CommodityID>,
49 milestone_years: &[u32],
50 ) -> Result<AgentSearchSpace> {
51 let search_space = Rc::new(parse_search_space_str(&self.search_space, processes)?);
53
54 let commodity_id = commodity_ids.get_id(&self.commodity_id)?;
56
57 let year = parse_year_str(&self.years, milestone_years)?;
59
60 let agent_id = agents.get_id(&self.agent_id)?;
61
62 Ok(AgentSearchSpace {
63 agent_id: agent_id.clone(),
64 commodity_id: commodity_id.clone(),
65 years: year,
66 search_space,
67 })
68 }
69}
70
71fn parse_search_space_str(search_space: &str, processes: &ProcessMap) -> Result<Vec<Rc<Process>>> {
78 let search_space = search_space.trim();
79 if search_space.is_empty() || search_space.eq_ignore_ascii_case("all") {
80 Ok(processes.values().cloned().collect())
81 } else {
82 search_space
83 .split(';')
84 .map(|id| {
85 let process = processes
86 .get(id.trim())
87 .with_context(|| format!("Invalid process '{id}'"))?;
88 Ok(process.clone())
89 })
90 .try_collect()
91 }
92}
93
94pub fn read_agent_search_space(
108 model_dir: &Path,
109 agents: &AgentMap,
110 processes: &ProcessMap,
111 commodity_ids: &HashSet<CommodityID>,
112 milestone_years: &[u32],
113) -> Result<HashMap<AgentID, AgentSearchSpaceMap>> {
114 let file_path = model_dir.join(AGENT_SEARCH_SPACE_FILE_NAME);
115 let iter = read_csv_optional::<AgentSearchSpaceRaw>(&file_path)?;
116 read_agent_search_space_from_iter(iter, agents, processes, commodity_ids, milestone_years)
117 .with_context(|| input_err_msg(&file_path))
118}
119
120fn read_agent_search_space_from_iter<I>(
121 iter: I,
122 agents: &AgentMap,
123 processes: &ProcessMap,
124 commodity_ids: &HashSet<CommodityID>,
125 milestone_years: &[u32],
126) -> Result<HashMap<AgentID, AgentSearchSpaceMap>>
127where
128 I: Iterator<Item = AgentSearchSpaceRaw>,
129{
130 let mut search_spaces = HashMap::new();
131 for search_space_raw in iter {
132 let search_space = search_space_raw.into_agent_search_space(
133 agents,
134 processes,
135 commodity_ids,
136 milestone_years,
137 )?;
138
139 let map = search_spaces
141 .entry(search_space.agent_id)
142 .or_insert_with(AgentSearchSpaceMap::new);
143
144 for year in search_space.years {
146 try_insert(
147 map,
148 &(search_space.commodity_id.clone(), year),
149 search_space.search_space.clone(),
150 )?;
151 }
152 }
153
154 for (agent_id, agent) in agents {
155 let search_space = search_spaces
157 .entry(agent_id.clone())
158 .or_insert_with(AgentSearchSpaceMap::new);
159
160 fill_missing_search_space_entries(agent, processes, search_space);
162 }
163
164 Ok(search_spaces)
165}
166
167fn fill_missing_search_space_entries(
172 agent: &Agent,
173 processes: &ProcessMap,
174 search_space: &mut AgentSearchSpaceMap,
175) {
176 assert!(!agent.commodity_portions.is_empty());
178
179 for (commodity_id, year) in agent.commodity_portions.keys() {
180 let key = (commodity_id.clone(), *year);
181 search_space.entry(key).or_insert_with(|| {
182 Rc::new(get_all_producers(processes, commodity_id, *year).collect())
183 });
184 }
185}
186
187fn get_all_producers<'a>(
189 processes: &'a ProcessMap,
190 commodity_id: &'a CommodityID,
191 year: u32,
192) -> impl Iterator<Item = Rc<Process>> + 'a {
193 processes
194 .values()
195 .filter(move |process| {
196 process.active_for_year(year) && process.primary_output.as_ref() == Some(commodity_id)
197 })
198 .cloned()
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use crate::fixture::{agents, assert_error, region_ids};
205 use crate::process::{
206 ProcessActivityLimitsMap, ProcessFlowsMap, ProcessID, ProcessInvestmentConstraintsMap,
207 ProcessParameterMap,
208 };
209 use crate::region::RegionID;
210 use crate::units::ActivityPerCapacity;
211 use indexmap::IndexSet;
212 use rstest::{fixture, rstest};
213 use std::iter;
214
215 #[fixture]
216 pub fn processes(region_ids: IndexSet<RegionID>) -> ProcessMap {
217 ["A", "B", "C"]
218 .map(|id| {
219 let id: ProcessID = id.into();
220 let process = Process {
221 id: id.clone(),
222 description: "Description".into(),
223 years: 2010..=2020,
224 activity_limits: ProcessActivityLimitsMap::new(),
225 flows: ProcessFlowsMap::new(),
226 parameters: ProcessParameterMap::new(),
227 regions: region_ids.clone(),
228 primary_output: None,
229 capacity_to_activity: ActivityPerCapacity(1.0),
230 investment_constraints: ProcessInvestmentConstraintsMap::new(),
231 };
232 (id, process.into())
233 })
234 .into_iter()
235 .collect()
236 }
237
238 #[fixture]
239 fn commodity_ids() -> HashSet<CommodityID> {
240 iter::once("commodity1".into()).collect()
241 }
242
243 #[rstest]
244 fn test_search_space_raw_into_search_space_valid(
245 agents: AgentMap,
246 processes: ProcessMap,
247 commodity_ids: HashSet<CommodityID>,
248 ) {
249 let raw = AgentSearchSpaceRaw {
251 agent_id: "agent1".into(),
252 commodity_id: "commodity1".into(),
253 years: "2020".into(),
254 search_space: "A;B".into(),
255 };
256 assert!(
257 raw.into_agent_search_space(&agents, &processes, &commodity_ids, &[2020])
258 .is_ok()
259 );
260 }
261
262 #[rstest]
263 fn test_search_space_raw_into_search_space_invalid_commodity_id(
264 agents: AgentMap,
265 processes: ProcessMap,
266 commodity_ids: HashSet<CommodityID>,
267 ) {
268 let raw = AgentSearchSpaceRaw {
270 agent_id: "agent1".into(),
271 commodity_id: "invalid_commodity".into(),
272 years: "2020".into(),
273 search_space: "A;B".into(),
274 };
275 assert_error!(
276 raw.into_agent_search_space(&agents, &processes, &commodity_ids, &[2020]),
277 "Unknown ID invalid_commodity found"
278 );
279 }
280
281 #[rstest]
282 fn test_search_space_raw_into_search_space_invalid_process_id(
283 agents: AgentMap,
284 processes: ProcessMap,
285 commodity_ids: HashSet<CommodityID>,
286 ) {
287 let raw = AgentSearchSpaceRaw {
289 agent_id: "agent1".into(),
290 commodity_id: "commodity1".into(),
291 years: "2020".into(),
292 search_space: "A;D".into(),
293 };
294 assert_error!(
295 raw.into_agent_search_space(&agents, &processes, &commodity_ids, &[2020]),
296 "Invalid process 'D'"
297 );
298 }
299}