muse2/input/
agent.rs

1//! Code for reading in agent-related data from CSV files.
2use 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/// An agent in the simulation
27#[derive(Debug, Deserialize, PartialEq, Clone)]
28struct AgentRaw {
29    /// A unique identifier for the agent.
30    id: String,
31    /// A text description of the agent.
32    description: String,
33    /// The region(s) in which the agent operates.
34    regions: String,
35    /// The decision rule that the agent uses to decide investment.
36    decision_rule: String,
37    /// The tolerance around the main objective to consider secondary objectives.
38    decision_lexico_tolerance: Option<f64>,
39}
40
41/// Read agents info from various CSV files.
42///
43/// # Arguments
44///
45/// * `model_dir` - Folder containing model configuration files
46/// * `commodities` - Commodities for the model
47/// * `process_ids` - The possible valid process IDs
48/// * `region_ids` - The possible valid region IDs
49///
50/// # Returns
51///
52/// A map of Agents, with the agent ID as the key
53pub 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
97/// Read agents info from the agents.csv file.
98///
99/// # Arguments
100///
101/// * `model_dir` - Folder containing model configuration files
102/// * `commodities` - Commodities for the model
103/// * `process_ids` - The possible valid process IDs
104///
105/// # Returns
106///
107/// A map of Agents, with the agent ID as the key
108fn 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
114/// Read agents info from an iterator.
115fn 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        // Parse region ID
122        let regions = parse_region_str(&agent_raw.regions, region_ids)?;
123
124        // Parse decision rule
125        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        // Valid case
176        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), &region_ids).unwrap();
196        assert_eq!(actual, expected);
197
198        // Duplicate agent ID
199        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(), &region_ids).is_err());
216
217        // Lexico tolerance missing for lexico decision rule
218        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), &region_ids).is_err());
226    }
227}