muse2/input/commodity/
demand.rs

1//! Code for handling commodity demands. Demands may vary by region, year, and time slice.
2use 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/// Represents a single demand entry in the dataset.
19#[allow(clippy::struct_field_names)]
20#[derive(Debug, Clone, Deserialize, PartialEq)]
21struct Demand {
22    /// The commodity this demand entry refers to
23    commodity_id: String,
24    /// The region of the demand entry
25    region_id: String,
26    /// The year of the demand entry
27    year: u32,
28    /// Annual demand quantity
29    demand: Flow,
30}
31
32/// A map relating commodity, region and year to annual demand
33pub type AnnualDemandMap = HashMap<(CommodityID, RegionID, u32), (TimeSliceLevel, Flow)>;
34
35/// A map containing references to commodities
36pub type BorrowedCommodityMap<'a> = HashMap<CommodityID, &'a Commodity>;
37
38/// Reads demand data from CSV files.
39///
40/// # Arguments
41///
42/// * `model_dir` - Folder containing model configuration files
43/// * `commodity_ids` - All possible commodity IDs
44/// * `region_ids` - Known region identifiers
45/// * `time_slice_info` - Information about seasons and times of day
46/// * `milestone_years` - Milestone years used by the model
47///
48/// # Returns
49///
50/// A `HashMap<CommodityID, DemandMap>` mapping each commodity to its `DemandMap`.
51pub 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    // Demand only applies to SVD commodities
59    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
71/// Read the demand.csv file.
72///
73/// # Arguments
74///
75/// * `model_dir` - Folder containing model configuration files
76/// * `svd_commodities` - Map of service demand commodities
77/// * `region_ids` - All possible IDs for regions
78/// * `milestone_years` - All milestone years
79///
80/// # Returns
81///
82/// An `AnnualDemandMap` mapping `(CommodityID, RegionID, year)` to `(TimeSliceLevel, Flow)`.
83fn 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
95/// Read the demand data from an iterator.
96///
97/// # Arguments
98///
99/// * `iter` - An iterator of [`Demand`]s
100/// * `svd_commodities` - Map of service demand commodities
101/// * `region_ids` - All possible IDs for regions
102/// * `milestone_years` - All milestone years
103///
104/// # Returns
105///
106/// An `AnnualDemandMap` mapping `(CommodityID, RegionID, year)` to `(TimeSliceLevel, Flow)`.
107fn 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    // Check that demand data is specified for all combinations of commodity, region and year
154    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
171/// Calculate the demand for each combination of commodity, region, year and time slice.
172///
173/// # Arguments
174///
175/// * `time_slice_info` - Information about time slices
176/// * `demand` - Total annual demand for combinations of commodity, region and year
177/// * `slices` - How annual demand is shared between time slices
178///
179/// # Returns
180///
181/// A `HashMap<CommodityID, DemandMap>` mapping each commodity to its `DemandMap`, which contains
182/// demand values for combinations of region, year and time slice.
183fn 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            // NB: This has already been checked, so shouldn't fail
198            let demand_fraction = slices[&slice_key];
199
200            // Get or create entry
201            let map = map
202                .entry(commodity_id.clone())
203                .or_insert_with(DemandMap::new);
204
205            // Add a new demand entry
206            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        // Valid
245        read_demand_from_iter(demand.into_iter(), &svd_commodities, &region_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        // Bad commodity ID
254        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, &region_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        // Bad region ID
287        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, &region_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        // Bad year
311        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, &region_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        // Bad demand quantity
344        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, &region_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        // Multiple entries for same commodity and region
363        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, &region_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        // Missing entry for a milestone year
396        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            &region_ids,
407            &[2020, 2030],
408        )
409        .unwrap_err();
410    }
411
412    /// Create an example demand file in `dir_path`
413    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, &region_ids, &milestone_years).unwrap();
443        assert_eq!(demand, expected);
444    }
445}