muse2/input/agent/
objective.rs

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