1use 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#[derive(Debug, Clone, Deserialize, PartialEq)]
16struct AgentObjectiveRaw {
17 agent_id: AgentID,
19 years: String,
21 objective_type: ObjectiveType,
23 decision_weight: Option<Dimensionless>,
25 decision_lexico_order: Option<u32>,
27}
28
29pub 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_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 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
110fn check_objective_parameter(
112 objective: &AgentObjectiveRaw,
113 decision_rule: &DecisionRule,
114) -> Result<()> {
115 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 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 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 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 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 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 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 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)), 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}