muse2/input/agent/
objective.rs

1//! Code for reading the agent objectives CSV file.
2use super::super::*;
3use crate::agent::{AgentID, AgentMap, AgentObjectiveMap, DecisionRule, ObjectiveType};
4use crate::units::Dimensionless;
5use crate::year::parse_year_str;
6use anyhow::{Context, Result, ensure};
7use log::warn;
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///
35/// # Returns
36///
37/// A map of Agents, with the agent ID as the key
38pub fn read_agent_objectives(
39    model_dir: &Path,
40    agents: &AgentMap,
41    milestone_years: &[u32],
42) -> Result<HashMap<AgentID, AgentObjectiveMap>> {
43    let file_path = model_dir.join(AGENT_OBJECTIVES_FILE_NAME);
44    let agent_objectives_csv = read_csv(&file_path)?;
45    read_agent_objectives_from_iter(agent_objectives_csv, agents, milestone_years)
46        .with_context(|| input_err_msg(&file_path))
47}
48
49fn read_agent_objectives_from_iter<I>(
50    iter: I,
51    agents: &AgentMap,
52    milestone_years: &[u32],
53) -> Result<HashMap<AgentID, AgentObjectiveMap>>
54where
55    I: Iterator<Item = AgentObjectiveRaw>,
56{
57    let mut all_objectives = HashMap::new();
58    for objective in iter {
59        let (id, agent) = agents
60            .get_key_value(&objective.agent_id)
61            .context("Invalid agent ID")?;
62
63        // Check that required parameters are present and others are absent
64        check_objective_parameter(&objective, &agent.decision_rule)?;
65
66        let agent_objectives = all_objectives
67            .entry(id.clone())
68            .or_insert_with(AgentObjectiveMap::new);
69        for year in parse_year_str(&objective.years, milestone_years)? {
70            try_insert(agent_objectives, year, objective.objective_type).with_context(|| {
71                format!("Duplicate agent objective entry for agent {id} and year {year}")
72            })?;
73        }
74    }
75
76    // Check that agents have one objective per milestone year
77    for agent_id in agents.keys() {
78        let agent_objectives = all_objectives
79            .get(agent_id)
80            .with_context(|| format!("Agent {agent_id} has no objectives"))?;
81
82        let missing_years = milestone_years
83            .iter()
84            .copied()
85            .filter(|year| !agent_objectives.contains_key(year))
86            .collect_vec();
87        ensure!(
88            missing_years.is_empty(),
89            "Agent {} is missing objectives for the following milestone years: {:?}",
90            agent_id,
91            missing_years
92        );
93
94        let npv_years = milestone_years
95            .iter()
96            .copied()
97            .filter(|year| agent_objectives[year] == ObjectiveType::NetPresentValue)
98            .collect_vec();
99        if !npv_years.is_empty() {
100            warn!(
101                "Agent {agent_id} is using NPV in years {npv_years:?}. \
102                Support for NPV is currently experimental and may give bad results."
103            );
104        }
105    }
106
107    Ok(all_objectives)
108}
109
110/// Check that required parameters are present and others are absent
111fn check_objective_parameter(
112    objective: &AgentObjectiveRaw,
113    decision_rule: &DecisionRule,
114) -> Result<()> {
115    // Check that the user hasn't supplied a value for a field we're not using
116    macro_rules! check_field_none {
117        ($field:ident) => {
118            ensure!(
119                objective.$field.is_none(),
120                "Field {} should be empty for this decision rule",
121                stringify!($field)
122            )
123        };
124    }
125
126    // Check that required fields are present
127    macro_rules! check_field_some {
128        ($field:ident) => {
129            ensure!(
130                objective.$field.is_some(),
131                "Required field {} is empty",
132                stringify!($field)
133            )
134        };
135    }
136
137    match decision_rule {
138        DecisionRule::Single => {
139            check_field_none!(decision_weight);
140            check_field_none!(decision_lexico_order);
141        }
142        DecisionRule::Weighted => {
143            check_field_some!(decision_weight);
144            check_field_none!(decision_lexico_order);
145        }
146        DecisionRule::Lexicographical { tolerance: _ } => {
147            check_field_none!(decision_weight);
148            check_field_some!(decision_lexico_order);
149        }
150    };
151
152    Ok(())
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::agent::ObjectiveType;
159    use crate::fixture::{agents, assert_error};
160    use rstest::{fixture, rstest};
161    use std::iter;
162
163    macro_rules! objective {
164        ($decision_weight:expr, $decision_lexico_order:expr) => {
165            AgentObjectiveRaw {
166                agent_id: "agent".into(),
167                years: "2020".into(),
168                objective_type: ObjectiveType::LevelisedCostOfX,
169                decision_weight: $decision_weight,
170                decision_lexico_order: $decision_lexico_order,
171            }
172        };
173    }
174
175    #[test]
176    fn test_check_objective_parameter_single() {
177        // DecisionRule::Single
178        let decision_rule = DecisionRule::Single;
179        let objective = objective!(None, None);
180        assert!(check_objective_parameter(&objective, &decision_rule).is_ok());
181        let objective = objective!(Some(Dimensionless(1.0)), 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 test_check_objective_parameter_weighted() {
189        // DecisionRule::Weighted
190        let decision_rule = DecisionRule::Weighted;
191        let objective = objective!(Some(Dimensionless(1.0)), None);
192        assert!(check_objective_parameter(&objective, &decision_rule).is_ok());
193        let objective = objective!(None, None);
194        assert!(check_objective_parameter(&objective, &decision_rule).is_err());
195        let objective = objective!(None, Some(1));
196        assert!(check_objective_parameter(&objective, &decision_rule).is_err());
197    }
198
199    #[test]
200    fn test_check_objective_parameter_lexico() {
201        // DecisionRule::Lexicographical
202        let decision_rule = DecisionRule::Lexicographical { tolerance: 1.0 };
203        let objective = objective!(None, Some(1));
204        assert!(check_objective_parameter(&objective, &decision_rule).is_ok());
205        let objective = objective!(None, None);
206        assert!(check_objective_parameter(&objective, &decision_rule).is_err());
207        let objective = objective!(Some(Dimensionless(1.0)), None);
208        assert!(check_objective_parameter(&objective, &decision_rule).is_err());
209    }
210
211    #[fixture]
212    fn objective_raw() -> AgentObjectiveRaw {
213        AgentObjectiveRaw {
214            agent_id: "agent1".into(),
215            years: "2020".into(),
216            objective_type: ObjectiveType::LevelisedCostOfX,
217            decision_weight: None,
218            decision_lexico_order: None,
219        }
220    }
221
222    #[rstest]
223    fn test_read_agent_objectives_from_iter_valid(
224        agents: AgentMap,
225        objective_raw: AgentObjectiveRaw,
226    ) {
227        let milestone_years = [2020];
228        let expected = iter::once((
229            "agent1".into(),
230            iter::once((2020, objective_raw.objective_type)).collect(),
231        ))
232        .collect();
233        let actual = read_agent_objectives_from_iter(
234            iter::once(objective_raw.clone()),
235            &agents,
236            &milestone_years,
237        )
238        .unwrap();
239        assert_eq!(actual, expected);
240    }
241
242    #[rstest]
243    fn test_read_agent_objectives_from_iter_invalid_no_objective_for_agent(agents: AgentMap) {
244        // Missing objective for agent
245        assert_error!(
246            read_agent_objectives_from_iter(iter::empty(), &agents, &[2020]),
247            "Agent agent1 has no objectives"
248        );
249    }
250
251    #[rstest]
252    fn test_read_agent_objectives_from_iter_invalid_no_objective_for_year(
253        agents: AgentMap,
254        objective_raw: AgentObjectiveRaw,
255    ) {
256        // Missing objective for milestone year
257        assert_error!(
258            read_agent_objectives_from_iter(iter::once(objective_raw), &agents, &[2020, 2030]),
259            "Agent agent1 is missing objectives for the following milestone years: [2030]"
260        );
261    }
262
263    #[rstest]
264    fn test_read_agent_objectives_from_iter_invalid_bad_param(agents: AgentMap) {
265        // Bad parameter
266        let bad_objective = AgentObjectiveRaw {
267            agent_id: "agent1".into(),
268            years: "2020".into(),
269            objective_type: ObjectiveType::LevelisedCostOfX,
270            decision_weight: Some(Dimensionless(1.0)), // Should only accept None for DecisionRule::Single
271            decision_lexico_order: None,
272        };
273        assert_error!(
274            read_agent_objectives_from_iter([bad_objective].into_iter(), &agents, &[2020]),
275            "Field decision_weight should be empty for this decision rule"
276        );
277    }
278}