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::{GetIDValue, IDCollection};
6use crate::input::parse_year_str;
7use crate::process::{Process, ProcessMap};
8use crate::region::RegionID;
9use anyhow::{Context, Result, ensure};
10use itertools::{Itertools, iproduct};
11use serde::Deserialize;
12use std::collections::{HashMap, HashSet};
13use std::path::Path;
14use std::rc::Rc;
15
16const AGENT_SEARCH_SPACES_FILE_NAME: &str = "agent_search_spaces.csv";
17
18type ProducersMap = HashMap<(CommodityID, RegionID, u32), Rc<Vec<Rc<Process>>>>;
19
20#[derive(PartialEq, Debug, Deserialize)]
21struct SearchSpaceEntry {
22 agent_id: String,
24 commodity_id: String,
26 years: String,
28 search_space: String,
32}
33
34fn add_entry_to_search_space_map(
41 entry: &SearchSpaceEntry,
42 agents: &AgentMap,
43 processes: &ProcessMap,
44 commodity_ids: &HashSet<CommodityID>,
45 milestone_years: &[u32],
46 producers: &ProducersMap,
47 map: &mut HashMap<AgentID, AgentSearchSpaceMap>,
48) -> Result<()> {
49 let (agent_id, agent) = agents.get_id_value(&entry.agent_id)?;
50 let commodity_id = commodity_ids.get_id(&entry.commodity_id)?;
51 let years = parse_year_str(&entry.years, milestone_years)?;
52 ensure!(
53 years.iter().all(|year| agent
54 .commodity_portions
55 .contains_key(&(commodity_id.clone(), *year))),
56 "Agent '{agent_id}' is not responsible for commodity '{commodity_id}' in at least some of \
57 the specified years: {years:?}",
58 );
59
60 let map = map.entry(agent.id.clone()).or_default();
61 for_each_year_in_search_space(
62 &entry.search_space,
63 agent,
64 commodity_id,
65 &years,
66 processes,
67 producers,
68 |commodity_id, region_id, year, search_space| {
69 try_insert(map, &(commodity_id, region_id, year), search_space)
70 .context("Overlapping entries in search space file")
71 },
72 )?;
73
74 Ok(())
75}
76
77fn for_each_year_in_search_space<F>(
79 search_space: &str,
80 agent: &Agent,
81 commodity_id: &CommodityID,
82 years: &[u32],
83 processes: &ProcessMap,
84 producers: &ProducersMap,
85 mut f: F,
86) -> Result<()>
87where
88 F: FnMut(CommodityID, RegionID, u32, Rc<Vec<Rc<Process>>>) -> Result<()>,
89{
90 ensure!(!search_space.is_empty(), "No processes provided");
91
92 let regions_and_years = iproduct!(agent.regions.iter(), years.iter().copied());
93 if search_space.eq_ignore_ascii_case("all") {
94 for (region_id, year) in regions_and_years {
96 let search_space = &producers[&(commodity_id.clone(), region_id.clone(), year)];
97 f(
98 commodity_id.clone(),
99 region_id.clone(),
100 year,
101 search_space.clone(),
102 )?;
103 }
104 } else {
105 let search_space: Rc<Vec<_>> = Rc::new(
107 search_space
108 .split(';')
109 .map(|process_id_str| {
110 let (_, process) = processes.get_id_value(process_id_str.trim())?;
111
112 for (region_id, year) in regions_and_years.clone() {
115 let producers =
116 &producers[&(commodity_id.clone(), region_id.clone(), year)];
117 ensure!(
118 producers.iter().any(|producer| producer.id == process.id),
119 "Process '{}' does not produce commodity '{commodity_id}' in region \
120 '{region_id}' in year {year}",
121 &process.id
122 );
123 }
124
125 Ok(process.clone())
126 })
127 .try_collect()?,
128 );
129
130 for (region_id, year) in regions_and_years {
131 f(
132 commodity_id.clone(),
133 region_id.clone(),
134 year,
135 search_space.clone(),
136 )?;
137 }
138 }
139
140 Ok(())
141}
142
143pub fn read_agent_search_spaces(
157 model_dir: &Path,
158 agents: &AgentMap,
159 processes: &ProcessMap,
160 commodity_ids: &HashSet<CommodityID>,
161 milestone_years: &[u32],
162) -> Result<HashMap<AgentID, AgentSearchSpaceMap>> {
163 let file_path = model_dir.join(AGENT_SEARCH_SPACES_FILE_NAME);
164 let iter = read_csv_optional::<SearchSpaceEntry>(&file_path)?;
165 read_agent_search_spaces_from_iter(iter, agents, processes, commodity_ids, milestone_years)
166 .with_context(|| input_err_msg(&file_path))
167}
168
169fn read_agent_search_spaces_from_iter<I>(
170 iter: I,
171 agents: &AgentMap,
172 processes: &ProcessMap,
173 commodity_ids: &HashSet<CommodityID>,
174 milestone_years: &[u32],
175) -> Result<HashMap<AgentID, AgentSearchSpaceMap>>
176where
177 I: Iterator<Item = SearchSpaceEntry>,
178{
179 let producers = get_producers_map(agents, processes);
180 let mut search_spaces = HashMap::new();
181 for entry in iter {
182 add_entry_to_search_space_map(
183 &entry,
184 agents,
185 processes,
186 commodity_ids,
187 milestone_years,
188 &producers,
189 &mut search_spaces,
190 )?;
191 }
192
193 for (agent_id, agent) in agents {
194 let search_space = search_spaces
196 .entry(agent_id.clone())
197 .or_insert_with(AgentSearchSpaceMap::new);
198
199 fill_missing_search_space_entries(agent, &producers, search_space);
201 }
202
203 Ok(search_spaces)
204}
205
206fn fill_missing_search_space_entries(
211 agent: &Agent,
212 producers: &ProducersMap,
213 search_space: &mut AgentSearchSpaceMap,
214) {
215 assert!(!agent.commodity_portions.is_empty());
217
218 for (commodity_id, year) in agent.commodity_portions.keys() {
219 for region_id in &agent.regions {
220 let key = (commodity_id.clone(), region_id.clone(), *year);
221 search_space
222 .entry(key.clone())
223 .or_insert_with(|| producers[&key].clone());
224 }
225 }
226}
227
228fn get_producers_map(agents: &AgentMap, processes: &ProcessMap) -> ProducersMap {
230 let mut map = ProducersMap::new();
233 for agent in agents.values() {
234 for (commodity_id, year) in agent.commodity_portions.keys() {
235 for region_id in &agent.regions {
236 map.entry((commodity_id.clone(), region_id.clone(), *year))
237 .or_default();
238 }
239 }
240 }
241
242 for ((commodity_id, region_id, year), vec) in &mut map {
244 let producers = processes
245 .values()
246 .filter(move |process| {
247 process.active_for_year(*year)
248 && process.primary_output.as_ref() == Some(commodity_id)
249 && process.regions.contains(region_id)
250 })
251 .cloned();
252 Rc::get_mut(vec).unwrap().extend(producers);
253 }
254
255 map
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use crate::{
262 agent::{AgentCommodityPortionsMap, AgentObjectiveMap, DecisionRule},
263 fixture::{
264 agent_id, assert_error, assert_patched_runs_ok_simple,
265 assert_validate_fails_with_simple, commodity_id, process, processes, region_id,
266 },
267 patch::FilePatch,
268 };
269 use indexmap::indexmap;
270 use map_macro::hash_map;
271 use rstest::{fixture, rstest};
272 use std::iter;
273
274 #[fixture]
275 fn process1(process: Process) -> Rc<Process> {
276 Rc::new(process)
277 }
278
279 #[fixture]
280 fn process2(process: Process) -> Rc<Process> {
281 Rc::new(Process {
282 id: "process2".into(),
283 ..process
284 })
285 }
286
287 #[fixture]
288 fn agent(agent_id: AgentID, region_id: RegionID) -> Agent {
289 Agent {
290 id: agent_id,
291 description: String::new(),
292 commodity_portions: AgentCommodityPortionsMap::new(),
293 search_space: AgentSearchSpaceMap::new(),
294 decision_rule: DecisionRule::Single,
295 regions: iter::once(region_id).collect(),
296 objectives: AgentObjectiveMap::new(),
297 }
298 }
299
300 #[rstest]
301 fn empty_search_space_returns_error(agent: Agent, commodity_id: CommodityID) {
302 let result = for_each_year_in_search_space(
303 "",
304 &agent,
305 &commodity_id,
306 &[2020],
307 &ProcessMap::new(),
308 &ProducersMap::new(),
309 |_, _, _, _| Ok(()),
310 );
311 assert_error!(result, "No processes provided");
312 }
313
314 #[rstest]
315 fn all_calls_f_for_each_year(
316 agent: Agent,
317 commodity_id: CommodityID,
318 region_id: RegionID,
319 process1: Rc<Process>,
320 process2: Rc<Process>,
321 ) {
322 let producers = hash_map! {
323 (commodity_id.clone(), region_id.clone(), 2020) => Rc::new(vec![process1.clone()]),
324 (commodity_id.clone(), region_id.clone(), 2030) => Rc::new(vec![process2.clone()])
325 };
326 let mut calls: Vec<(u32, Rc<Vec<Rc<Process>>>)> = Vec::new();
327 for_each_year_in_search_space(
328 "all",
329 &agent,
330 &commodity_id,
331 &[2020, 2030],
332 &ProcessMap::new(),
333 &producers,
334 |_, _, year, search_space| {
335 calls.push((year, search_space));
336 Ok(())
337 },
338 )
339 .unwrap();
340 assert_eq!(calls.len(), 2);
341 assert_eq!(calls[0].0, 2020);
342 assert_eq!(calls[0].1.len(), 1);
343 assert_eq!(calls[0].1[0].id, process1.id);
344 assert_eq!(calls[1].0, 2030);
345 assert_eq!(calls[1].1.len(), 1);
346 assert_eq!(calls[1].1[0].id, process2.id);
347 }
348
349 #[rstest]
350 fn specific_process_calls_f_for_each_year(
351 agent: Agent,
352 commodity_id: CommodityID,
353 region_id: RegionID,
354 processes: ProcessMap,
355 ) {
356 let process = processes.values().next().unwrap().clone();
357 let value = Rc::new(vec![process.clone()]);
358 let producers = hash_map! {
359 (commodity_id.clone(), region_id.clone(), 2020) => value.clone(),
360 (commodity_id.clone(), region_id.clone(), 2030) => value
361 };
362 let mut calls: Vec<(u32, Rc<Vec<Rc<Process>>>)> = Vec::new();
363 for_each_year_in_search_space(
364 "process1",
365 &agent,
366 &commodity_id,
367 &[2020, 2030],
368 &processes,
369 &producers,
370 |_, _, year, search_space| {
371 calls.push((year, search_space));
372 Ok(())
373 },
374 )
375 .unwrap();
376 assert_eq!(calls.len(), 2);
377 assert_eq!(calls[0].0, 2020);
378 assert_eq!(calls[1].0, 2030);
379 assert_eq!(calls[0].1.len(), 1);
380 assert_eq!(calls[0].1[0].id, process.id);
381 assert!(Rc::ptr_eq(&calls[0].1, &calls[1].1));
383 }
384
385 #[rstest]
386 fn multiple_process_ids_calls_f_with_all_processes(
387 agent: Agent,
388 commodity_id: CommodityID,
389 region_id: RegionID,
390 process1: Rc<Process>,
391 process2: Rc<Process>,
392 ) {
393 let producers = hash_map! {
394 (commodity_id.clone(), region_id.clone(), 2020) => Rc::new(vec![process1.clone(), process2.clone()])
395 };
396 let processes: ProcessMap = indexmap! {
397 process1.id.clone() => process1.clone(),
398 process2.id.clone() => process2.clone(),
399 };
400 let mut calls: Vec<(u32, Rc<Vec<Rc<Process>>>)> = Vec::new();
401 for_each_year_in_search_space(
402 "process1;process2",
403 &agent,
404 &commodity_id,
405 &[2020],
406 &processes,
407 &producers,
408 |_, _, year, search_space| {
409 calls.push((year, search_space));
410 Ok(())
411 },
412 )
413 .unwrap();
414 assert_eq!(calls.len(), 1);
415 assert_eq!(calls[0].1.len(), 2);
416 assert_eq!(calls[0].1[0].id, process1.id);
417 assert_eq!(calls[0].1[1].id, process2.id);
418 }
419
420 #[test]
421 fn model_runs_with_search_space_file1() {
422 assert_patched_runs_ok_simple!(vec![
424 FilePatch::new(AGENT_SEARCH_SPACES_FILE_NAME).with_replacement(&[
425 "agent_id,commodity_id,years,search_space",
426 "A0_GEX,GASPRD,all,all",
427 "A0_GPR,GASNAT,all,all",
428 "A0_ELC,ELCTRI,all,all",
429 "A0_RES,RSHEAT,all,all",
430 ])
431 ]);
432 }
433
434 #[test]
435 fn model_runs_with_search_space_file2() {
436 assert_patched_runs_ok_simple!(vec![
438 FilePatch::new(AGENT_SEARCH_SPACES_FILE_NAME).with_replacement(&[
439 "agent_id,commodity_id,years,search_space",
440 "A0_GEX,GASPRD,all,GASDRV",
441 "A0_GPR,GASNAT,2020,all",
442 "A0_GPR,GASNAT,2030,GASPRC",
443 "A0_GPR,GASNAT,2040,all",
444 "A0_ELC,ELCTRI,all,all",
445 "A0_RES,RSHEAT,2020,RGASBR;RELCHP",
446 "A0_RES,RSHEAT,2030,RGASBR ; RELCHP",
447 "A0_RES,RSHEAT,2040,RGASBR;RELCHP",
448 ])
449 ]);
450 }
451
452 #[test]
453 fn not_responsible_for_commodity() {
454 assert_validate_fails_with_simple!(
455 vec![
456 FilePatch::new(AGENT_SEARCH_SPACES_FILE_NAME).with_replacement(&[
457 "agent_id,commodity_id,years,search_space",
458 "A0_GEX,ELCTRI,all,all",
459 ]),
460 ],
461 "Agent 'A0_GEX' is not responsible for commodity 'ELCTRI' in at least some of the \
462 specified years: [2020, 2030, 2040]"
463 );
464 }
465
466 #[test]
467 fn unknown_agent_id_fails() {
468 assert_validate_fails_with_simple!(
469 vec![
470 FilePatch::new(AGENT_SEARCH_SPACES_FILE_NAME).with_replacement(&[
471 "agent_id,commodity_id,years,search_space",
472 "UNKNOWN_AGENT,GASPRD,all,all",
473 ])
474 ],
475 "Unknown agent ID 'UNKNOWN_AGENT'"
476 );
477 }
478
479 #[test]
480 fn unknown_commodity_id_fails() {
481 assert_validate_fails_with_simple!(
482 vec![
483 FilePatch::new(AGENT_SEARCH_SPACES_FILE_NAME).with_replacement(&[
484 "agent_id,commodity_id,years,search_space",
485 "A0_GEX,UNKNOWN_COMMODITY,all,all",
486 ])
487 ],
488 "Unknown commodity ID 'UNKNOWN_COMMODITY'"
489 );
490 }
491
492 #[test]
493 fn invalid_year_fails() {
494 assert_validate_fails_with_simple!(
495 vec![
496 FilePatch::new(AGENT_SEARCH_SPACES_FILE_NAME).with_replacement(&[
497 "agent_id,commodity_id,years,search_space",
498 "A0_GEX,GASPRD,9999,all",
499 ])
500 ],
501 "Invalid year: 9999"
502 );
503 }
504
505 #[test]
506 fn overlapping_entries_fails() {
507 assert_validate_fails_with_simple!(
508 vec![
509 FilePatch::new(AGENT_SEARCH_SPACES_FILE_NAME).with_replacement(&[
510 "agent_id,commodity_id,years,search_space",
511 "A0_GEX,GASPRD,2020,all",
512 "A0_GEX,GASPRD,2020,GASDRV",
513 ])
514 ],
515 "Overlapping entries in search space file"
516 );
517 }
518
519 #[test]
520 fn invalid_search_space_fails() {
521 assert_validate_fails_with_simple!(
522 vec![
523 FilePatch::new(AGENT_SEARCH_SPACES_FILE_NAME).with_replacement(&[
524 "agent_id,commodity_id,years,search_space",
525 "A0_GEX,GASPRD,all,NONEXISTENT_PROCESS",
526 ])
527 ],
528 "Unknown process ID 'NONEXISTENT_PROCESS'"
529 );
530 }
531
532 #[test]
533 fn process_not_valid_producer_fails() {
534 assert_validate_fails_with_simple!(
535 vec![
536 FilePatch::new(AGENT_SEARCH_SPACES_FILE_NAME).with_replacement(&[
537 "agent_id,commodity_id,years,search_space",
538 "A0_GEX,GASPRD,all,GASPRC",
539 ])
540 ],
541 "Process 'GASPRC' does not produce commodity 'GASPRD' in region 'GBR' in year 2020"
542 );
543 }
544}