1use super::super::*;
4use super::demand_slicing::{read_demand_slices, DemandSliceMap};
5use crate::commodity::{Commodity, CommodityID, CommodityType, DemandMap};
6use crate::id::IDCollection;
7use crate::region::RegionID;
8use crate::time_slice::TimeSliceInfo;
9use anyhow::{ensure, Result};
10use serde::Deserialize;
11use std::collections::{HashMap, HashSet};
12use std::path::Path;
13
14const DEMAND_FILE_NAME: &str = "demand.csv";
15
16#[derive(Debug, Clone, Deserialize, PartialEq)]
18struct Demand {
19 commodity_id: String,
21 region_id: String,
23 year: u32,
25 demand: f64,
27}
28
29pub type AnnualDemandMap = HashMap<(CommodityID, RegionID, u32), f64>;
31
32pub fn read_demand(
46 model_dir: &Path,
47 commodities: &IndexMap<CommodityID, Commodity>,
48 region_ids: &HashSet<RegionID>,
49 time_slice_info: &TimeSliceInfo,
50 milestone_years: &[u32],
51) -> Result<HashMap<CommodityID, DemandMap>> {
52 let svd_commodity_ids: HashSet<CommodityID> = commodities
54 .iter()
55 .filter(|(_, commodity)| commodity.kind == CommodityType::ServiceDemand)
56 .map(|(id, _)| id.clone())
57 .collect();
58
59 let demand = read_demand_file(model_dir, &svd_commodity_ids, region_ids, milestone_years)?;
60 let slices = read_demand_slices(model_dir, &svd_commodity_ids, region_ids, time_slice_info)?;
61
62 Ok(compute_demand_maps(&demand, &slices, time_slice_info))
63}
64
65fn read_demand_file(
78 model_dir: &Path,
79 svd_commodity_ids: &HashSet<CommodityID>,
80 region_ids: &HashSet<RegionID>,
81 milestone_years: &[u32],
82) -> Result<AnnualDemandMap> {
83 let file_path = model_dir.join(DEMAND_FILE_NAME);
84 let iter = read_csv(&file_path)?;
85 read_demand_from_iter(iter, svd_commodity_ids, region_ids, milestone_years)
86}
87
88fn read_demand_from_iter<I>(
102 iter: I,
103 svd_commodity_ids: &HashSet<CommodityID>,
104 region_ids: &HashSet<RegionID>,
105 milestone_years: &[u32],
106) -> Result<AnnualDemandMap>
107where
108 I: Iterator<Item = Demand>,
109{
110 let mut map = AnnualDemandMap::new();
111 for demand in iter {
112 let commodity_id = svd_commodity_ids
113 .get_id_by_str(&demand.commodity_id)
114 .with_context(|| {
115 format!(
116 "Can only provide demand data for SVD commodities. Found entry for '{}'",
117 demand.commodity_id
118 )
119 })?;
120 let region_id = region_ids.get_id_by_str(&demand.region_id)?;
121
122 ensure!(
123 milestone_years.binary_search(&demand.year).is_ok(),
124 "Year {} is not a milestone year. \
125 Input of non-milestone years is currently not supported.",
126 demand.year
127 );
128
129 ensure!(
130 demand.demand.is_normal() && demand.demand > 0.0,
131 "Demand must be a valid number greater than zero"
132 );
133
134 ensure!(
135 map.insert(
136 (commodity_id.clone(), region_id.clone(), demand.year),
137 demand.demand
138 )
139 .is_none(),
140 "Duplicate demand entries (commodity: {}, region: {}, year: {})",
141 commodity_id,
142 region_id,
143 demand.year
144 );
145 }
146
147 for commodity_id in svd_commodity_ids {
149 let mut missing_keys = Vec::new();
150 for region_id in region_ids {
151 for year in milestone_years {
152 if !map.contains_key(&(commodity_id.clone(), region_id.clone(), *year)) {
153 missing_keys.push((region_id.clone(), *year));
154 }
155 }
156 }
157 ensure!(
158 missing_keys.is_empty(),
159 "Commodity {} is missing demand data for {:?}",
160 commodity_id,
161 missing_keys
162 );
163 }
164
165 Ok(map)
166}
167
168fn compute_demand_maps(
181 demand: &AnnualDemandMap,
182 slices: &DemandSliceMap,
183 time_slice_info: &TimeSliceInfo,
184) -> HashMap<CommodityID, DemandMap> {
185 let mut map = HashMap::new();
186 for ((commodity_id, region_id, year), annual_demand) in demand.iter() {
187 for time_slice in time_slice_info.iter_ids() {
188 let slice_key = (commodity_id.clone(), region_id.clone(), time_slice.clone());
189
190 let demand_fraction = slices.get(&slice_key).unwrap();
192
193 let map = map
195 .entry(commodity_id.clone())
196 .or_insert_with(DemandMap::new);
197
198 map.insert(
200 (region_id.clone(), *year, time_slice.clone()),
201 annual_demand * demand_fraction,
202 );
203 }
204 }
205
206 map
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use std::fs::File;
213 use std::io::Write;
214 use std::iter;
215 use std::path::Path;
216 use tempfile::tempdir;
217
218 fn create_demand_file(dir_path: &Path) {
220 let file_path = dir_path.join(DEMAND_FILE_NAME);
221 let mut file = File::create(file_path).unwrap();
222 writeln!(
223 file,
224 "commodity_id,region_id,year,demand
225COM1,North,2020,10
226COM1,South,2020,11
227COM1,East,2020,12
228COM1,West,2020,13"
229 )
230 .unwrap();
231 }
232
233 #[test]
234 fn test_read_demand_from_iter() {
235 let commodity_ids = ["COM1".into()].into_iter().collect();
236 let region_ids = ["North".into(), "South".into()].into_iter().collect();
237 let milestone_years = [2020];
238
239 let demand = [
241 Demand {
242 year: 2020,
243 region_id: "North".to_string(),
244 commodity_id: "COM1".to_string(),
245 demand: 10.0,
246 },
247 Demand {
248 year: 2020,
249 region_id: "South".to_string(),
250 commodity_id: "COM1".to_string(),
251 demand: 11.0,
252 },
253 ];
254 assert!(read_demand_from_iter(
255 demand.into_iter(),
256 &commodity_ids,
257 ®ion_ids,
258 &milestone_years
259 )
260 .is_ok());
261
262 let demand = [
264 Demand {
265 year: 2020,
266 region_id: "North".to_string(),
267 commodity_id: "COM2".to_string(),
268 demand: 10.0,
269 },
270 Demand {
271 year: 2020,
272 region_id: "South".to_string(),
273 commodity_id: "COM1".to_string(),
274 demand: 11.0,
275 },
276 ];
277 assert!(read_demand_from_iter(
278 demand.into_iter(),
279 &commodity_ids,
280 ®ion_ids,
281 &milestone_years
282 )
283 .is_err());
284
285 let demand = [
287 Demand {
288 year: 2020,
289 region_id: "East".to_string(),
290 commodity_id: "COM1".to_string(),
291 demand: 10.0,
292 },
293 Demand {
294 year: 2020,
295 region_id: "South".to_string(),
296 commodity_id: "COM1".to_string(),
297 demand: 11.0,
298 },
299 ];
300 assert!(read_demand_from_iter(
301 demand.into_iter(),
302 &commodity_ids,
303 ®ion_ids,
304 &milestone_years
305 )
306 .is_err());
307
308 let demand = [
310 Demand {
311 year: 2010,
312 region_id: "North".to_string(),
313 commodity_id: "COM1".to_string(),
314 demand: 10.0,
315 },
316 Demand {
317 year: 2020,
318 region_id: "South".to_string(),
319 commodity_id: "COM1".to_string(),
320 demand: 11.0,
321 },
322 ];
323 assert!(read_demand_from_iter(
324 demand.into_iter(),
325 &commodity_ids,
326 ®ion_ids,
327 &milestone_years
328 )
329 .is_err());
330
331 macro_rules! test_quantity {
333 ($quantity: expr) => {
334 let demand = [Demand {
335 year: 2020,
336 region_id: "North".to_string(),
337 commodity_id: "COM1".to_string(),
338 demand: $quantity,
339 }];
340 assert!(read_demand_from_iter(
341 demand.into_iter(),
342 &commodity_ids,
343 ®ion_ids,
344 &milestone_years,
345 )
346 .is_err());
347 };
348 }
349 test_quantity!(-1.0);
350 test_quantity!(0.0);
351 test_quantity!(f64::NAN);
352 test_quantity!(f64::NEG_INFINITY);
353 test_quantity!(f64::INFINITY);
354
355 let demand = [
357 Demand {
358 year: 2020,
359 region_id: "North".to_string(),
360 commodity_id: "COM1".to_string(),
361 demand: 10.0,
362 },
363 Demand {
364 year: 2020,
365 region_id: "North".to_string(),
366 commodity_id: "COM1".to_string(),
367 demand: 10.0,
368 },
369 Demand {
370 year: 2020,
371 region_id: "South".to_string(),
372 commodity_id: "COM1".to_string(),
373 demand: 11.0,
374 },
375 ];
376 assert!(read_demand_from_iter(
377 demand.into_iter(),
378 &commodity_ids,
379 ®ion_ids,
380 &milestone_years
381 )
382 .is_err());
383
384 let demand = Demand {
386 year: 2020,
387 region_id: "North".to_string(),
388 commodity_id: "COM1".to_string(),
389 demand: 10.0,
390 };
391 assert!(read_demand_from_iter(
392 iter::once(demand),
393 &commodity_ids,
394 ®ion_ids,
395 &[2020, 2030]
396 )
397 .is_err());
398 }
399
400 #[test]
401 fn test_read_demand_file() {
402 let dir = tempdir().unwrap();
403 create_demand_file(dir.path());
404 let commodity_ids = HashSet::from_iter(iter::once("COM1".into()));
405 let region_ids =
406 HashSet::from_iter(["North".into(), "South".into(), "East".into(), "West".into()]);
407 let milestone_years = [2020];
408 let expected = AnnualDemandMap::from_iter([
409 (("COM1".into(), "North".into(), 2020), 10.0),
410 (("COM1".into(), "South".into(), 2020), 11.0),
411 (("COM1".into(), "East".into(), 2020), 12.0),
412 (("COM1".into(), "West".into(), 2020), 13.0),
413 ]);
414 let demand =
415 read_demand_file(dir.path(), &commodity_ids, ®ion_ids, &milestone_years).unwrap();
416 assert_eq!(demand, expected);
417 }
418}