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