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::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 `HashMap` mapping `AgentID` to `AgentObjectiveMap`.
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 check_objective_parameter_single() {
186        // DecisionRule::Single
187        let decision_rule = DecisionRule::Single;
188        let objective = objective!(None, None);
189        check_objective_parameter(&objective, &decision_rule).unwrap();
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 check_objective_parameter_weighted() {
198        // DecisionRule::Weighted
199        let decision_rule = DecisionRule::Weighted;
200        let objective = objective!(Some(Dimensionless(1.0)), None);
201        check_objective_parameter(&objective, &decision_rule).unwrap();
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 check_objective_parameter_lexico() {
210        // DecisionRule::Lexicographical
211        let decision_rule = DecisionRule::Lexicographical { tolerance: 1.0 };
212        let objective = objective!(None, Some(1));
213        check_objective_parameter(&objective, &decision_rule).unwrap();
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 read_agent_objectives_from_iter_valid(agents: AgentMap, objective_raw: AgentObjectiveRaw) {
233        let milestone_years = [2020];
234        let expected = iter::once((
235            "agent1".into(),
236            iter::once((2020, objective_raw.objective_type)).collect(),
237        ))
238        .collect();
239        let actual = read_agent_objectives_from_iter(
240            iter::once(objective_raw.clone()),
241            &agents,
242            &milestone_years,
243        )
244        .unwrap();
245        assert_eq!(actual, expected);
246    }
247
248    #[rstest]
249    fn read_agent_objectives_from_iter_invalid_no_objective_for_agent(agents: AgentMap) {
250        // Missing objective for agent
251        assert_error!(
252            read_agent_objectives_from_iter(iter::empty(), &agents, &[2020]),
253            "Agent agent1 has no objectives"
254        );
255    }
256
257    #[rstest]
258    fn read_agent_objectives_from_iter_invalid_no_objective_for_year(
259        agents: AgentMap,
260        objective_raw: AgentObjectiveRaw,
261    ) {
262        // Missing objective for milestone year
263        assert_error!(
264            read_agent_objectives_from_iter(iter::once(objective_raw), &agents, &[2020, 2030]),
265            "Agent agent1 is missing objectives for the following milestone years: [2030]"
266        );
267    }
268
269    #[rstest]
270    fn read_agent_objectives_from_iter_invalid_bad_param(agents: AgentMap) {
271        // Bad parameter
272        let bad_objective = AgentObjectiveRaw {
273            agent_id: "agent1".into(),
274            years: "2020".into(),
275            objective_type: ObjectiveType::LevelisedCostOfX,
276            decision_weight: Some(Dimensionless(1.0)), // Should only accept None for DecisionRule::Single
277            decision_lexico_order: None,
278        };
279        assert_error!(
280            read_agent_objectives_from_iter([bad_objective].into_iter(), &agents, &[2020]),
281            "Field decision_weight should be empty for this decision rule"
282        );
283    }
284}