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