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::{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 agents.iter_mut() {
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 agents.iter_mut() {
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 {}",
137                    tolerance
138                );
139                DecisionRule::Lexicographical { tolerance }
140            }
141            invalid_rule => bail!("Invalid decision rule: {}", invalid_rule),
142        };
143
144        ensure!(
145            decision_rule == DecisionRule::Single,
146            "Currently only the \"single\" decision rule is supported"
147        );
148
149        let agent = Agent {
150            id: AgentID(agent_raw.id.into()),
151            description: agent_raw.description,
152            commodity_portions: AgentCommodityPortionsMap::new(),
153            search_space: AgentSearchSpaceMap::new(),
154            decision_rule,
155            cost_limits: AgentCostLimitsMap::new(),
156            regions,
157            objectives: AgentObjectiveMap::new(),
158        };
159
160        ensure!(
161            agents.insert(agent.id.clone(), agent).is_none(),
162            "Duplicate agent ID"
163        );
164    }
165
166    Ok(agents)
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use crate::agent::DecisionRule;
173    use std::iter;
174
175    #[test]
176    fn test_read_agents_file_from_iter() {
177        // Valid case
178        let region_ids = IndexSet::from(["GBR".into()]);
179        let agent = AgentRaw {
180            id: "agent".into(),
181            description: "".into(),
182            decision_rule: "single".into(),
183            decision_lexico_tolerance: None,
184            regions: "GBR".into(),
185        };
186        let agent_out = Agent {
187            id: "agent".into(),
188            description: "".into(),
189            commodity_portions: AgentCommodityPortionsMap::new(),
190            search_space: AgentSearchSpaceMap::new(),
191            decision_rule: DecisionRule::Single,
192            cost_limits: AgentCostLimitsMap::new(),
193            regions: IndexSet::from(["GBR".into()]),
194            objectives: AgentObjectiveMap::new(),
195        };
196        let expected = AgentMap::from_iter(iter::once(("agent".into(), agent_out)));
197        let actual = read_agents_file_from_iter(iter::once(agent), &region_ids).unwrap();
198        assert_eq!(actual, expected);
199
200        // Duplicate agent ID
201        let agents = [
202            AgentRaw {
203                id: "agent".into(),
204                description: "".into(),
205                decision_rule: "single".into(),
206                decision_lexico_tolerance: None,
207                regions: "GBR".into(),
208            },
209            AgentRaw {
210                id: "agent".into(),
211                description: "".into(),
212                decision_rule: "single".into(),
213                decision_lexico_tolerance: None,
214                regions: "GBR".into(),
215            },
216        ];
217        assert!(read_agents_file_from_iter(agents.into_iter(), &region_ids).is_err());
218
219        // Lexico tolerance missing for lexico decision rule
220        let agent = AgentRaw {
221            id: "agent".into(),
222            description: "".into(),
223            decision_rule: "lexico".into(),
224            decision_lexico_tolerance: None,
225            regions: "GBR".into(),
226        };
227        assert!(read_agents_file_from_iter(iter::once(agent), &region_ids).is_err());
228    }
229}