muse2/input/agent/
objective.rs

1//! Code for reading agent objectives from a CSV file.
2use super::super::{input_err_msg, read_csv, try_insert};
3use crate::agent::{AgentID, AgentMap, AgentObjectiveMap, DecisionRule, ObjectiveType};
4use crate::units::Dimensionless;
5use crate::year::parse_year_str;
6use anyhow::{Context, Result, ensure};
7use itertools::Itertools;
8use serde::Deserialize;
9use std::collections::HashMap;
10use std::path::Path;
11
12const AGENT_OBJECTIVES_FILE_NAME: &str = "agent_objectives.csv";
13
14/// An objective for an agent with associated parameters
15#[derive(Debug, Clone, Deserialize, PartialEq)]
16struct AgentObjectiveRaw {
17    /// Unique agent id identifying the agent this objective belongs to.
18    agent_id: AgentID,
19    /// The year(s) the objective is relevant for
20    years: String,
21    /// Acronym identifying the objective (e.g. LCOX).
22    objective_type: ObjectiveType,
23    /// For the weighted sum decision rule, the set of weights to apply to each objective.
24    decision_weight: Option<Dimensionless>,
25    /// For the lexico decision rule, the order in which to consider objectives.
26    decision_lexico_order: Option<u32>,
27}
28
29/// Read agent objective info from the `agent_objectives.csv` file.
30///
31/// # Arguments
32///
33/// * `model_dir` - Folder containing model configuration files
34/// * `agents` - Map of agents
35/// * `milestone_years` - Milestone years for the simulation
36///
37/// # Returns
38///
39/// A `HashMap` mapping `AgentID` to `AgentObjectiveMap`.
40pub fn read_agent_objectives(
41    model_dir: &Path,
42    agents: &AgentMap,
43    milestone_years: &[u32],
44) -> Result<HashMap<AgentID, AgentObjectiveMap>> {
45    let file_path = model_dir.join(AGENT_OBJECTIVES_FILE_NAME);
46    let agent_objectives_csv = read_csv(&file_path)?;
47    read_agent_objectives_from_iter(agent_objectives_csv, agents, milestone_years)
48        .with_context(|| input_err_msg(&file_path))
49}
50
51fn read_agent_objectives_from_iter<I>(
52    iter: I,
53    agents: &AgentMap,
54    milestone_years: &[u32],
55) -> Result<HashMap<AgentID, AgentObjectiveMap>>
56where
57    I: Iterator<Item = AgentObjectiveRaw>,
58{
59    let mut all_objectives = HashMap::new();
60    for objective in iter {
61        let (id, agent) = agents
62            .get_key_value(&objective.agent_id)
63            .context("Invalid agent ID")?;
64
65        // Check that required parameters are present and others are absent
66        check_objective_parameter(&objective, &agent.decision_rule)?;
67
68        let agent_objectives = all_objectives
69            .entry(id.clone())
70            .or_insert_with(AgentObjectiveMap::new);
71        for year in parse_year_str(&objective.years, milestone_years)? {
72            try_insert(agent_objectives, &year, objective.objective_type).with_context(|| {
73                format!("Duplicate agent objective entry for agent {id} and year {year}")
74            })?;
75        }
76    }
77
78    // Check that agents have one objective per milestone year
79    for agent_id in agents.keys() {
80        let agent_objectives = all_objectives
81            .get(agent_id)
82            .with_context(|| format!("Agent {agent_id} has no objectives"))?;
83
84        let missing_years = milestone_years
85            .iter()
86            .copied()
87            .filter(|year| !agent_objectives.contains_key(year))
88            .collect_vec();
89        ensure!(
90            missing_years.is_empty(),
91            "Agent {agent_id} is missing objectives for the following milestone years: {missing_years:?}"
92        );
93    }
94
95    Ok(all_objectives)
96}
97
98/// Check that required parameters are present and others are absent
99fn check_objective_parameter(
100    objective: &AgentObjectiveRaw,
101    decision_rule: &DecisionRule,
102) -> Result<()> {
103    // Check that the user hasn't supplied a value for a field we're not using
104    macro_rules! check_field_none {
105        ($field:ident) => {
106            ensure!(
107                objective.$field.is_none(),
108                "Field {} should be empty for this decision rule",
109                stringify!($field)
110            )
111        };
112    }
113
114    // Check that required fields are present
115    macro_rules! check_field_some {
116        ($field:ident) => {
117            ensure!(
118                objective.$field.is_some(),
119                "Required field {} is empty",
120                stringify!($field)
121            )
122        };
123    }
124
125    match decision_rule {
126        DecisionRule::Single => {
127            check_field_none!(decision_weight);
128            check_field_none!(decision_lexico_order);
129        }
130        DecisionRule::Weighted => {
131            check_field_some!(decision_weight);
132            check_field_none!(decision_lexico_order);
133        }
134        DecisionRule::Lexicographical { tolerance: _ } => {
135            check_field_none!(decision_weight);
136            check_field_some!(decision_lexico_order);
137        }
138    }
139
140    Ok(())
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::agent::ObjectiveType;
147    use crate::fixture::{agents, assert_error};
148    use rstest::{fixture, rstest};
149    use std::iter;
150
151    macro_rules! objective {
152        ($decision_weight:expr, $decision_lexico_order:expr) => {
153            AgentObjectiveRaw {
154                agent_id: "agent".into(),
155                years: "2020".into(),
156                objective_type: ObjectiveType::LevelisedCostOfX,
157                decision_weight: $decision_weight,
158                decision_lexico_order: $decision_lexico_order,
159            }
160        };
161    }
162
163    #[test]
164    fn check_objective_parameter_single() {
165        // DecisionRule::Single
166        let decision_rule = DecisionRule::Single;
167        let objective = objective!(None, None);
168        check_objective_parameter(&objective, &decision_rule).unwrap();
169        let objective = objective!(Some(Dimensionless(1.0)), None);
170        assert!(check_objective_parameter(&objective, &decision_rule).is_err());
171        let objective = objective!(None, Some(1));
172        assert!(check_objective_parameter(&objective, &decision_rule).is_err());
173    }
174
175    #[test]
176    fn check_objective_parameter_weighted() {
177        // DecisionRule::Weighted
178        let decision_rule = DecisionRule::Weighted;
179        let objective = objective!(Some(Dimensionless(1.0)), None);
180        check_objective_parameter(&objective, &decision_rule).unwrap();
181        let objective = objective!(None, None);
182        assert!(check_objective_parameter(&objective, &decision_rule).is_err());
183        let objective = objective!(None, Some(1));
184        assert!(check_objective_parameter(&objective, &decision_rule).is_err());
185    }
186
187    #[test]
188    fn check_objective_parameter_lexico() {
189        // DecisionRule::Lexicographical
190        let decision_rule = DecisionRule::Lexicographical { tolerance: 1.0 };
191        let objective = objective!(None, Some(1));
192        check_objective_parameter(&objective, &decision_rule).unwrap();
193        let objective = objective!(None, None);
194        assert!(check_objective_parameter(&objective, &decision_rule).is_err());
195        let objective = objective!(Some(Dimensionless(1.0)), None);
196        assert!(check_objective_parameter(&objective, &decision_rule).is_err());
197    }
198
199    #[fixture]
200    fn objective_raw() -> AgentObjectiveRaw {
201        AgentObjectiveRaw {
202            agent_id: "agent1".into(),
203            years: "2020".into(),
204            objective_type: ObjectiveType::LevelisedCostOfX,
205            decision_weight: None,
206            decision_lexico_order: None,
207        }
208    }
209
210    #[rstest]
211    fn read_agent_objectives_from_iter_valid(agents: AgentMap, objective_raw: AgentObjectiveRaw) {
212        let milestone_years = [2020];
213        let expected = iter::once((
214            "agent1".into(),
215            iter::once((2020, objective_raw.objective_type)).collect(),
216        ))
217        .collect();
218        let actual = read_agent_objectives_from_iter(
219            iter::once(objective_raw.clone()),
220            &agents,
221            &milestone_years,
222        )
223        .unwrap();
224        assert_eq!(actual, expected);
225    }
226
227    #[rstest]
228    fn read_agent_objectives_from_iter_invalid_no_objective_for_agent(agents: AgentMap) {
229        // Missing objective for agent
230        assert_error!(
231            read_agent_objectives_from_iter(iter::empty(), &agents, &[2020]),
232            "Agent agent1 has no objectives"
233        );
234    }
235
236    #[rstest]
237    fn read_agent_objectives_from_iter_invalid_no_objective_for_year(
238        agents: AgentMap,
239        objective_raw: AgentObjectiveRaw,
240    ) {
241        // Missing objective for milestone year
242        assert_error!(
243            read_agent_objectives_from_iter(iter::once(objective_raw), &agents, &[2020, 2030]),
244            "Agent agent1 is missing objectives for the following milestone years: [2030]"
245        );
246    }
247
248    #[rstest]
249    fn read_agent_objectives_from_iter_invalid_bad_param(agents: AgentMap) {
250        // Bad parameter
251        let bad_objective = AgentObjectiveRaw {
252            agent_id: "agent1".into(),
253            years: "2020".into(),
254            objective_type: ObjectiveType::LevelisedCostOfX,
255            decision_weight: Some(Dimensionless(1.0)), // Should only accept None for DecisionRule::Single
256            decision_lexico_order: None,
257        };
258        assert_error!(
259            read_agent_objectives_from_iter([bad_objective].into_iter(), &agents, &[2020]),
260            "Field decision_weight should be empty for this decision rule"
261        );
262    }
263}