Skip to main content

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