1use super::super::{format_items_with_cap, input_err_msg, read_csv};
4use super::demand_slicing::{DemandSliceMap, read_demand_slices};
5use crate::commodity::{Commodity, CommodityID, CommodityType, DemandMap};
6use crate::id::IDCollection;
7use crate::region::RegionID;
8use crate::time_slice::{TimeSliceInfo, TimeSliceLevel};
9use crate::units::Flow;
10use anyhow::{Context, Result, ensure};
11use indexmap::{IndexMap, IndexSet};
12use itertools::iproduct;
13use serde::Deserialize;
14use std::collections::HashMap;
15use std::path::Path;
16
17const DEMAND_FILE_NAME: &str = "demand.csv";
18
19#[allow(clippy::struct_field_names)]
21#[derive(Debug, Clone, Deserialize, PartialEq)]
22struct Demand {
23 commodity_id: String,
25 region_id: String,
27 year: u32,
29 demand: Flow,
31}
32
33pub type AnnualDemandMap = HashMap<(CommodityID, RegionID, u32), (TimeSliceLevel, Flow)>;
35
36pub type BorrowedCommodityMap<'a> = HashMap<CommodityID, &'a Commodity>;
38
39pub fn read_demand(
53 model_dir: &Path,
54 commodities: &IndexMap<CommodityID, Commodity>,
55 region_ids: &IndexSet<RegionID>,
56 time_slice_info: &TimeSliceInfo,
57 milestone_years: &[u32],
58) -> Result<HashMap<CommodityID, DemandMap>> {
59 let svd_commodities = commodities
61 .iter()
62 .filter(|(_, commodity)| commodity.kind == CommodityType::ServiceDemand)
63 .map(|(id, commodity)| (id.clone(), commodity))
64 .collect();
65
66 let demand = read_demand_file(model_dir, &svd_commodities, region_ids, milestone_years)?;
67 let slices = read_demand_slices(model_dir, &svd_commodities, region_ids, time_slice_info)?;
68
69 Ok(compute_demand_maps(time_slice_info, &demand, &slices))
70}
71
72fn read_demand_file(
85 model_dir: &Path,
86 svd_commodities: &BorrowedCommodityMap,
87 region_ids: &IndexSet<RegionID>,
88 milestone_years: &[u32],
89) -> Result<AnnualDemandMap> {
90 let file_path = model_dir.join(DEMAND_FILE_NAME);
91 let iter = read_csv(&file_path)?;
92 read_demand_from_iter(iter, svd_commodities, region_ids, milestone_years)
93 .with_context(|| input_err_msg(file_path))
94}
95
96fn read_demand_from_iter<I>(
110 iter: I,
111 svd_commodities: &BorrowedCommodityMap,
112 region_ids: &IndexSet<RegionID>,
113 milestone_years: &[u32],
114) -> Result<AnnualDemandMap>
115where
116 I: Iterator<Item = Demand>,
117{
118 let mut map = AnnualDemandMap::new();
119 for demand in iter {
120 let commodity = svd_commodities
121 .get(demand.commodity_id.as_str())
122 .with_context(|| {
123 format!(
124 "Can only provide demand data for SVD commodities. Found entry for '{}'",
125 demand.commodity_id
126 )
127 })?;
128 let region_id = region_ids.get_id(&demand.region_id)?;
129
130 ensure!(
131 milestone_years.binary_search(&demand.year).is_ok(),
132 "Year {} is not a milestone year. \
133 Input of non-milestone years is currently not supported.",
134 demand.year
135 );
136
137 ensure!(
138 demand.demand.is_finite() && demand.demand >= Flow(0.0),
139 "Demand must be a finite number greater than or equal to zero"
140 );
141
142 ensure!(
143 map.insert(
144 (commodity.id.clone(), region_id.clone(), demand.year),
145 (commodity.time_slice_level, demand.demand)
146 )
147 .is_none(),
148 "Duplicate demand entries (commodity: {}, region: {}, year: {})",
149 commodity.id,
150 region_id,
151 demand.year
152 );
153 }
154
155 for commodity_id in svd_commodities.keys() {
157 let mut missing_keys = Vec::new();
158 for (region_id, year) in iproduct!(region_ids, milestone_years) {
159 if !map.contains_key(&(commodity_id.clone(), region_id.clone(), *year)) {
160 missing_keys.push((region_id.clone(), *year));
161 }
162 }
163 ensure!(
164 missing_keys.is_empty(),
165 "Commodity {commodity_id} is missing demand data for {}",
166 format_items_with_cap(&missing_keys)
167 );
168 }
169
170 Ok(map)
171}
172
173fn compute_demand_maps(
186 time_slice_info: &TimeSliceInfo,
187 demand: &AnnualDemandMap,
188 slices: &DemandSliceMap,
189) -> HashMap<CommodityID, DemandMap> {
190 let mut map = HashMap::new();
191 for ((commodity_id, region_id, year), (level, annual_demand)) in demand {
192 for ts_selection in time_slice_info.iter_selections_at_level(*level) {
193 let slice_key = (
194 commodity_id.clone(),
195 region_id.clone(),
196 ts_selection.clone(),
197 );
198
199 let demand_fraction = slices[&slice_key];
201
202 let map = map
204 .entry(commodity_id.clone())
205 .or_insert_with(DemandMap::new);
206
207 map.insert(
209 (region_id.clone(), *year, ts_selection.clone()),
210 *annual_demand * demand_fraction,
211 );
212 }
213 }
214
215 map
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use crate::fixture::{assert_error, get_svd_map, region_ids, svd_commodity};
222 use rstest::rstest;
223 use std::fs::File;
224 use std::io::Write;
225 use std::path::Path;
226 use tempfile::tempdir;
227
228 #[rstest]
229 fn test_read_demand_from_iter(svd_commodity: Commodity, region_ids: IndexSet<RegionID>) {
230 let svd_commodities = get_svd_map(&svd_commodity);
231 let demand = [
232 Demand {
233 year: 2020,
234 region_id: "GBR".to_string(),
235 commodity_id: "commodity1".to_string(),
236 demand: Flow(10.0),
237 },
238 Demand {
239 year: 2020,
240 region_id: "USA".to_string(),
241 commodity_id: "commodity1".to_string(),
242 demand: Flow(11.0),
243 },
244 ];
245
246 assert!(
248 read_demand_from_iter(demand.into_iter(), &svd_commodities, ®ion_ids, &[2020])
249 .is_ok()
250 );
251 }
252
253 #[rstest]
254 fn test_read_demand_from_iter_bad_commodity_id(
255 svd_commodity: Commodity,
256 region_ids: IndexSet<RegionID>,
257 ) {
258 let svd_commodities = get_svd_map(&svd_commodity);
260 let demand = [
261 Demand {
262 year: 2020,
263 region_id: "GBR".to_string(),
264 commodity_id: "commodity2".to_string(),
265 demand: Flow(10.0),
266 },
267 Demand {
268 year: 2020,
269 region_id: "USA".to_string(),
270 commodity_id: "commodity1".to_string(),
271 demand: Flow(11.0),
272 },
273 Demand {
274 year: 2020,
275 region_id: "Spain".to_string(),
276 commodity_id: "commodity3".to_string(),
277 demand: Flow(0.0),
278 },
279 ];
280 assert_error!(
281 read_demand_from_iter(demand.into_iter(), &svd_commodities, ®ion_ids, &[2020]),
282 "Can only provide demand data for SVD commodities. Found entry for 'commodity2'"
283 );
284 }
285
286 #[rstest]
287 fn test_read_demand_from_iter_bad_region_id(
288 svd_commodity: Commodity,
289 region_ids: IndexSet<RegionID>,
290 ) {
291 let svd_commodities = get_svd_map(&svd_commodity);
293 let demand = [
294 Demand {
295 year: 2020,
296 region_id: "FRA".to_string(),
297 commodity_id: "commodity1".to_string(),
298 demand: Flow(10.0),
299 },
300 Demand {
301 year: 2020,
302 region_id: "USA".to_string(),
303 commodity_id: "commodity1".to_string(),
304 demand: Flow(11.0),
305 },
306 ];
307 assert_error!(
308 read_demand_from_iter(demand.into_iter(), &svd_commodities, ®ion_ids, &[2020]),
309 "Unknown ID FRA found"
310 );
311 }
312
313 #[rstest]
314 fn test_read_demand_from_iter_bad_year(
315 svd_commodity: Commodity,
316 region_ids: IndexSet<RegionID>,
317 ) {
318 let svd_commodities = get_svd_map(&svd_commodity);
320 let demand = [
321 Demand {
322 year: 2010,
323 region_id: "GBR".to_string(),
324 commodity_id: "commodity1".to_string(),
325 demand: Flow(10.0),
326 },
327 Demand {
328 year: 2020,
329 region_id: "USA".to_string(),
330 commodity_id: "commodity1".to_string(),
331 demand: Flow(11.0),
332 },
333 ];
334 assert_error!(
335 read_demand_from_iter(demand.into_iter(), &svd_commodities, ®ion_ids, &[2020]),
336 "Year 2010 is not a milestone year. \
337 Input of non-milestone years is currently not supported."
338 );
339 }
340
341 #[rstest]
342 #[case(-1.0)]
343 #[case(f64::NAN)]
344 #[case(f64::NEG_INFINITY)]
345 #[case(f64::INFINITY)]
346 fn test_read_demand_from_iter_bad_demand(
347 svd_commodity: Commodity,
348 region_ids: IndexSet<RegionID>,
349 #[case] quantity: f64,
350 ) {
351 let svd_commodities = get_svd_map(&svd_commodity);
353 let demand = [Demand {
354 year: 2020,
355 region_id: "GBR".to_string(),
356 commodity_id: "commodity1".to_string(),
357 demand: Flow(quantity),
358 }];
359 assert_error!(
360 read_demand_from_iter(demand.into_iter(), &svd_commodities, ®ion_ids, &[2020],),
361 "Demand must be a finite number greater than or equal to zero"
362 );
363 }
364
365 #[rstest]
366 fn test_read_demand_from_iter_multiple_entries(
367 svd_commodity: Commodity,
368 region_ids: IndexSet<RegionID>,
369 ) {
370 let svd_commodities = get_svd_map(&svd_commodity);
372 let demand = [
373 Demand {
374 year: 2020,
375 region_id: "GBR".to_string(),
376 commodity_id: "commodity1".to_string(),
377 demand: Flow(10.0),
378 },
379 Demand {
380 year: 2020,
381 region_id: "GBR".to_string(),
382 commodity_id: "commodity1".to_string(),
383 demand: Flow(10.0),
384 },
385 Demand {
386 year: 2020,
387 region_id: "USA".to_string(),
388 commodity_id: "commodity1".to_string(),
389 demand: Flow(11.0),
390 },
391 ];
392 assert_error!(
393 read_demand_from_iter(demand.into_iter(), &svd_commodities, ®ion_ids, &[2020]),
394 "Duplicate demand entries (commodity: commodity1, region: GBR, year: 2020)"
395 );
396 }
397
398 #[rstest]
399 fn test_read_demand_from_iter_missing_year(
400 svd_commodity: Commodity,
401 region_ids: IndexSet<RegionID>,
402 ) {
403 let svd_commodities = get_svd_map(&svd_commodity);
405 let demand = Demand {
406 year: 2020,
407 region_id: "GBR".to_string(),
408 commodity_id: "commodity1".to_string(),
409 demand: Flow(10.0),
410 };
411 assert!(
412 read_demand_from_iter(
413 std::iter::once(demand),
414 &svd_commodities,
415 ®ion_ids,
416 &[2020, 2030]
417 )
418 .is_err()
419 );
420 }
421
422 fn create_demand_file(dir_path: &Path) {
424 let file_path = dir_path.join(DEMAND_FILE_NAME);
425 let mut file = File::create(file_path).unwrap();
426 writeln!(
427 file,
428 "commodity_id,region_id,year,demand\n\
429 commodity1,GBR,2020,10\n\
430 commodity1,USA,2020,11\n"
431 )
432 .unwrap();
433 }
434
435 #[rstest]
436 fn test_read_demand_file(svd_commodity: Commodity, region_ids: IndexSet<RegionID>) {
437 let svd_commodities = get_svd_map(&svd_commodity);
438 let dir = tempdir().unwrap();
439 create_demand_file(dir.path());
440 let milestone_years = [2020];
441 let expected = AnnualDemandMap::from_iter([
442 (
443 ("commodity1".into(), "GBR".into(), 2020),
444 (TimeSliceLevel::DayNight, Flow(10.0)),
445 ),
446 (
447 ("commodity1".into(), "USA".into(), 2020),
448 (TimeSliceLevel::DayNight, Flow(11.0)),
449 ),
450 ]);
451 let demand =
452 read_demand_file(dir.path(), &svd_commodities, ®ion_ids, &milestone_years).unwrap();
453 assert_eq!(demand, expected);
454 }
455}