1use 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#[derive(Debug, Clone, Deserialize, PartialEq)]
19struct AgentObjectiveRaw {
20 agent_id: AgentID,
22 years: String,
24 objective_type: ObjectiveType,
26 decision_weight: Option<Dimensionless>,
28 decision_lexico_order: Option<u32>,
30}
31
32pub 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_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 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
119fn check_objective_parameter(
121 objective: &AgentObjectiveRaw,
122 decision_rule: &DecisionRule,
123) -> Result<()> {
124 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 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 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 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 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 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 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 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)), 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}