1use super::super::{deserialise_proportion_nonzero, input_err_msg, read_csv, try_insert};
3use crate::agent::{AgentCommodityPortionsMap, AgentID, AgentMap};
4use crate::commodity::{CommodityMap, CommodityType};
5use crate::id::IDCollection;
6use crate::input::parse_year_str;
7use crate::region::RegionID;
8use crate::units::Dimensionless;
9use anyhow::{Context, Result, ensure};
10use float_cmp::approx_eq;
11use indexmap::{IndexMap, IndexSet};
12use serde::Deserialize;
13use std::collections::HashMap;
14use std::path::Path;
15
16const AGENT_COMMODITIES_FILE_NAME: &str = "agent_commodity_portions.csv";
17
18#[derive(PartialEq, Debug, Deserialize)]
19struct AgentCommodityPortionRaw {
20 agent_id: String,
22 commodity_id: String,
24 years: String,
26 #[serde(deserialize_with = "deserialise_proportion_nonzero")]
28 commodity_portion: Dimensionless,
29}
30
31pub fn read_agent_commodity_portions(
45 model_dir: &Path,
46 agents: &AgentMap,
47 commodities: &CommodityMap,
48 region_ids: &IndexSet<RegionID>,
49 milestone_years: &[u32],
50) -> Result<HashMap<AgentID, AgentCommodityPortionsMap>> {
51 let file_path = model_dir.join(AGENT_COMMODITIES_FILE_NAME);
52 let agent_commodity_portions_csv = read_csv(&file_path)?;
53 read_agent_commodity_portions_from_iter(
54 agent_commodity_portions_csv,
55 agents,
56 commodities,
57 region_ids,
58 milestone_years,
59 )
60 .with_context(|| input_err_msg(&file_path))
61}
62
63fn read_agent_commodity_portions_from_iter<I>(
64 iter: I,
65 agents: &AgentMap,
66 commodities: &CommodityMap,
67 region_ids: &IndexSet<RegionID>,
68 milestone_years: &[u32],
69) -> Result<HashMap<AgentID, AgentCommodityPortionsMap>>
70where
71 I: Iterator<Item = AgentCommodityPortionRaw>,
72{
73 let mut agent_commodity_portions = HashMap::new();
74 for agent_commodity_portion_raw in iter {
75 let agent_id_raw = agent_commodity_portion_raw.agent_id.as_str();
77 let id = agents.get_id(agent_id_raw)?;
78
79 let entry = agent_commodity_portions
81 .entry(id.clone())
82 .or_insert_with(AgentCommodityPortionsMap::new);
83
84 let commodity_id_raw = agent_commodity_portion_raw.commodity_id.as_str();
86 let commodity_id = commodities.get_id(commodity_id_raw)?;
87 let years = parse_year_str(&agent_commodity_portion_raw.years, milestone_years)?;
88 for year in years {
89 try_insert(
90 entry,
91 &(commodity_id.clone(), year),
92 agent_commodity_portion_raw.commodity_portion,
93 )?;
94 }
95 }
96
97 validate_agent_commodity_portions(
98 &agent_commodity_portions,
99 agents,
100 commodities,
101 region_ids,
102 milestone_years,
103 )?;
104
105 Ok(agent_commodity_portions)
106}
107
108fn validate_agent_commodity_portions(
109 agent_commodity_portions: &HashMap<AgentID, AgentCommodityPortionsMap>,
110 agents: &AgentMap,
111 commodities: &CommodityMap,
112 region_ids: &IndexSet<RegionID>,
113 milestone_years: &[u32],
114) -> Result<()> {
115 for (id, portions) in agent_commodity_portions {
117 let commodity_ids: IndexSet<_> = portions.keys().map(|(id, _)| id).collect();
119
120 for commodity_id in commodity_ids {
122 ensure!(
123 commodities[commodity_id].kind != CommodityType::Other,
124 "Agent {id} contains portion for commodity {commodity_id} which is of type OTH"
125 );
126 }
127 }
128
129 for (id, portions) in agent_commodity_portions {
131 let commodity_ids: IndexSet<_> = portions.keys().map(|(id, _)| id).collect();
133
134 for commodity_id in commodity_ids {
136 for year in milestone_years {
137 ensure!(
138 portions.contains_key(&(commodity_id.clone(), *year)),
139 "Agent {id} does not have data for commodity {commodity_id} in year {year}"
140 );
141 }
142 }
143 }
144
145 let mut summed_portions = IndexMap::new();
149 for (id, agent_commodity_portions) in agent_commodity_portions {
150 let agent = &agents[id];
151 for ((commodity_id, year), portion) in agent_commodity_portions {
152 for region in region_ids {
153 if agent.regions.contains(region) {
154 let key = (commodity_id, year, region);
155 summed_portions
156 .entry(key)
157 .and_modify(|v| *v += *portion)
158 .or_insert(*portion);
159 }
160 }
161 }
162 }
163
164 for (key, portion) in &summed_portions {
166 ensure!(
167 approx_eq!(Dimensionless, *portion, Dimensionless(1.0), epsilon = 1e-5),
168 "Commodity {} in year {} and region {} does not sum to 1.0",
169 key.0,
170 key.1,
171 key.2
172 );
173 }
174
175 let svd_and_sed_commodities = commodities
179 .iter()
180 .filter(|(_, commodity)| {
181 matches!(
182 commodity.kind,
183 CommodityType::SupplyEqualsDemand | CommodityType::ServiceDemand
184 )
185 })
186 .map(|(id, _)| id);
187
188 for commodity_id in svd_and_sed_commodities {
191 for year in milestone_years {
192 for region in region_ids {
193 let key = (commodity_id, year, region);
194 ensure!(
195 summed_portions.contains_key(&key),
196 "Commodity {commodity_id} in year {year} and region {region} is not covered"
197 );
198 }
199 }
200 }
201
202 Ok(())
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::commodity::{Commodity, CommodityID};
209 use crate::fixture::{
210 agents, assert_error, other_commodity, region_ids, sed_commodity, svd_commodity,
211 };
212 use indexmap::IndexMap;
213 use rstest::{fixture, rstest};
214 use std::rc::Rc;
215
216 #[fixture]
217 fn milestone_years() -> [u32; 1] {
218 [2020]
219 }
220
221 #[fixture]
222 fn commodities(svd_commodity: Commodity, other_commodity: Commodity) -> CommodityMap {
223 IndexMap::from([
224 ("commodity1".into(), Rc::new(svd_commodity)),
225 ("other_commodity".into(), Rc::new(other_commodity)),
226 ])
227 }
228
229 #[fixture]
230 fn agent_commodity_portions() -> HashMap<AgentID, AgentCommodityPortionsMap> {
231 let mut map = AgentCommodityPortionsMap::new();
232 map.insert(("commodity1".into(), 2020), Dimensionless(1.0));
233 HashMap::from([("agent1".into(), map)])
234 }
235
236 #[rstest]
237 fn validate_agent_commodity_portions_valid_case(
238 agent_commodity_portions: HashMap<AgentID, AgentCommodityPortionsMap>,
239 agents: AgentMap,
240 commodities: CommodityMap,
241 region_ids: IndexSet<RegionID>,
242 milestone_years: [u32; 1],
243 ) {
244 validate_agent_commodity_portions(
245 &agent_commodity_portions,
246 &agents,
247 &commodities,
248 ®ion_ids,
249 &milestone_years,
250 )
251 .unwrap();
252 }
253
254 #[rstest]
255 fn validate_agent_commodity_portions_other_type_commodity(
256 mut agent_commodity_portions: HashMap<AgentID, AgentCommodityPortionsMap>,
257 agents: AgentMap,
258 commodities: CommodityMap,
259 region_ids: IndexSet<RegionID>,
260 milestone_years: [u32; 1],
261 ) {
262 agent_commodity_portions
264 .get_mut(&AgentID::new("agent1"))
265 .unwrap()
266 .insert(("other_commodity".into(), 2020), Dimensionless(0.5));
267 assert_error!(
268 validate_agent_commodity_portions(
269 &agent_commodity_portions,
270 &agents,
271 &commodities,
272 ®ion_ids,
273 &milestone_years
274 ),
275 "Agent agent1 contains portion for commodity other_commodity which is of type OTH"
276 );
277 }
278
279 #[rstest]
280 fn validate_agent_commodity_portions_missing_year(
281 agent_commodity_portions: HashMap<AgentID, AgentCommodityPortionsMap>,
282 agents: AgentMap,
283 commodities: CommodityMap,
284 region_ids: IndexSet<RegionID>,
285 ) {
286 let milestone_years = [2020, 2030];
287 assert_error!(
288 validate_agent_commodity_portions(
289 &agent_commodity_portions,
290 &agents,
291 &commodities,
292 ®ion_ids,
293 &milestone_years,
294 ),
295 "Agent agent1 does not have data for commodity commodity1 in year 2030"
296 );
297 }
298
299 #[rstest]
300 fn validate_agent_commodity_portions_dont_sum_to_one(
301 mut agent_commodity_portions: HashMap<AgentID, AgentCommodityPortionsMap>,
302 agents: AgentMap,
303 commodities: CommodityMap,
304 region_ids: IndexSet<RegionID>,
305 milestone_years: [u32; 1],
306 ) {
307 agent_commodity_portions
309 .get_mut(&AgentID::new("agent1"))
310 .unwrap()
311 .insert(("commodity1".into(), 2020), Dimensionless(0.5));
312 assert_error!(
313 validate_agent_commodity_portions(
314 &agent_commodity_portions,
315 &agents,
316 &commodities,
317 ®ion_ids,
318 &milestone_years,
319 ),
320 "Commodity commodity1 in year 2020 and region GBR does not sum to 1.0"
321 );
322 }
323
324 #[rstest]
325 fn validate_agent_commodity_portions_missing_portions(
326 agent_commodity_portions: HashMap<AgentID, AgentCommodityPortionsMap>,
327 agents: AgentMap,
328 mut commodities: CommodityMap,
329 region_ids: IndexSet<RegionID>,
330 milestone_years: [u32; 1],
331 sed_commodity: Commodity,
332 ) {
333 commodities.insert(CommodityID::new("sed_commodity"), Rc::new(sed_commodity));
335 assert_error!(
336 validate_agent_commodity_portions(
337 &agent_commodity_portions,
338 &agents,
339 &commodities,
340 ®ion_ids,
341 &milestone_years
342 ),
343 "Commodity sed_commodity in year 2020 and region GBR is not covered"
344 );
345 }
346}