muse2/input/
agent.rs

1//! Code for reading in agent-related data from CSV files.
2use super::{input_err_msg, read_csv};
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/// 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    // We read commodity portions first as they are required by `read_agent_search_space`
64    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 &mut agents {
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 &mut agents {
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
99/// Read agents info from the agents.csv file.
100///
101/// # Arguments
102///
103/// * `model_dir` - Folder containing model configuration files
104/// * `commodities` - Commodities for the model
105/// * `process_ids` - The possible valid process IDs
106///
107/// # Returns
108///
109/// A map of Agents, with the agent ID as the key
110fn 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
116/// Read agents info from an iterator.
117fn 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        // Parse region ID
124        let regions = parse_region_str(&agent_raw.regions, region_ids)?;
125
126        // Parse decision rule
127        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 {tolerance}"
137                );
138                DecisionRule::Lexicographical { tolerance }
139            }
140            invalid_rule => bail!("Invalid decision rule: {invalid_rule}"),
141        };
142
143        ensure!(
144            decision_rule == DecisionRule::Single,
145            "Currently only the \"single\" decision rule is supported"
146        );
147
148        let agent = Agent {
149            id: AgentID(agent_raw.id.into()),
150            description: agent_raw.description,
151            commodity_portions: AgentCommodityPortionsMap::new(),
152            search_space: AgentSearchSpaceMap::new(),
153            decision_rule,
154            cost_limits: AgentCostLimitsMap::new(),
155            regions,
156            objectives: AgentObjectiveMap::new(),
157        };
158
159        ensure!(
160            agents.insert(agent.id.clone(), agent).is_none(),
161            "Duplicate agent ID"
162        );
163    }
164
165    Ok(agents)
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use crate::agent::DecisionRule;
172    use std::iter;
173
174    #[test]
175    fn test_read_agents_file_from_iter() {
176        // Valid case
177        let region_ids = IndexSet::from(["GBR".into()]);
178        let agent = AgentRaw {
179            id: "agent".into(),
180            description: "".into(),
181            decision_rule: "single".into(),
182            decision_lexico_tolerance: None,
183            regions: "GBR".into(),
184        };
185        let agent_out = Agent {
186            id: "agent".into(),
187            description: "".into(),
188            commodity_portions: AgentCommodityPortionsMap::new(),
189            search_space: AgentSearchSpaceMap::new(),
190            decision_rule: DecisionRule::Single,
191            cost_limits: AgentCostLimitsMap::new(),
192            regions: IndexSet::from(["GBR".into()]),
193            objectives: AgentObjectiveMap::new(),
194        };
195        let expected = AgentMap::from_iter(iter::once(("agent".into(), agent_out)));
196        let actual = read_agents_file_from_iter(iter::once(agent), &region_ids).unwrap();
197        assert_eq!(actual, expected);
198
199        // Duplicate agent ID
200        let agents = [
201            AgentRaw {
202                id: "agent".into(),
203                description: "".into(),
204                decision_rule: "single".into(),
205                decision_lexico_tolerance: None,
206                regions: "GBR".into(),
207            },
208            AgentRaw {
209                id: "agent".into(),
210                description: "".into(),
211                decision_rule: "single".into(),
212                decision_lexico_tolerance: None,
213                regions: "GBR".into(),
214            },
215        ];
216        assert!(read_agents_file_from_iter(agents.into_iter(), &region_ids).is_err());
217
218        // Lexico tolerance missing for lexico decision rule
219        let agent = AgentRaw {
220            id: "agent".into(),
221            description: "".into(),
222            decision_rule: "lexico".into(),
223            decision_lexico_tolerance: None,
224            regions: "GBR".into(),
225        };
226        assert!(read_agents_file_from_iter(iter::once(agent), &region_ids).is_err());
227    }
228}