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