muse2/input/commodity/
demand.rs

1//! Code for working with demand for a given commodity. Demand can vary by region, year and time
2//! slice.
3use 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/// Represents a single demand entry in the dataset.
20#[allow(clippy::struct_field_names)]
21#[derive(Debug, Clone, Deserialize, PartialEq)]
22struct Demand {
23    /// The commodity this demand entry refers to
24    commodity_id: String,
25    /// The region of the demand entry
26    region_id: String,
27    /// The year of the demand entry
28    year: u32,
29    /// Annual demand quantity
30    demand: Flow,
31}
32
33/// A map relating commodity, region and year to annual demand
34pub type AnnualDemandMap = HashMap<(CommodityID, RegionID, u32), (TimeSliceLevel, Flow)>;
35
36/// A map containing a references to commodities
37pub type BorrowedCommodityMap<'a> = HashMap<CommodityID, &'a Commodity>;
38
39/// Reads demand data from CSV files.
40///
41/// # Arguments
42///
43/// * `model_dir` - Folder containing model configuration files
44/// * `commodity_ids` - All possible IDs of commodities
45/// * `region_ids` - All possible IDs for regions
46/// * `time_slice_info` - Information about seasons and times of day
47/// * `milestone_years` - All milestone years
48///
49/// # Returns
50///
51/// This function returns [`DemandMap`]s grouped by commodity ID.
52pub 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    // Demand only applies to SVD commodities
60    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
72/// Read the demand.csv file.
73///
74/// # Arguments
75///
76/// * `model_dir` - Folder containing model configuration files
77/// * `svd_commodities` - Map of service demand commodities
78/// * `region_ids` - All possible IDs for regions
79/// * `milestone_years` - All milestone years
80///
81/// # Returns
82///
83/// Annual demand data, grouped by commodity, region and milestone year.
84fn 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
96/// Read the demand data from an iterator.
97///
98/// # Arguments
99///
100/// * `iter` - An iterator of [`Demand`]s
101/// * `svd_commodities` - Map of service demand commodities
102/// * `region_ids` - All possible IDs for regions
103/// * `milestone_years` - All milestone years
104///
105/// # Returns
106///
107/// A map of demand and time slice level for every combination of commodity, region and milestone
108/// year.
109fn 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_normal() && demand.demand > Flow(0.0),
139            "Demand must be a valid number greater than 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    // Check that demand data is specified for all combinations of commodity, region and year
156    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
173/// Calculate the demand for each combination of commodity, region, year and time slice.
174///
175/// # Arguments
176///
177/// * `time_slice_info` - Information about time slices
178/// * `demand` - Total annual demand for combinations of commodity, region and year
179/// * `slices` - How annual demand is shared between time slices
180///
181/// # Returns
182///
183/// [`DemandMap`]s for combinations of region, year and time slice, grouped by the commodity to
184/// which the demand applies.
185fn 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            // NB: This has already been checked, so shouldn't fail
200            let demand_fraction = slices[&slice_key];
201
202            // Get or create entry
203            let map = map
204                .entry(commodity_id.clone())
205                .or_insert_with(DemandMap::new);
206
207            // Add a new demand entry
208            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        // Valid
247        assert!(
248            read_demand_from_iter(demand.into_iter(), &svd_commodities, &region_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        // Bad commodity ID
259        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        ];
274        assert_error!(
275            read_demand_from_iter(demand.into_iter(), &svd_commodities, &region_ids, &[2020]),
276            "Can only provide demand data for SVD commodities. Found entry for 'commodity2'"
277        );
278    }
279
280    #[rstest]
281    fn test_read_demand_from_iter_bad_region_id(
282        svd_commodity: Commodity,
283        region_ids: IndexSet<RegionID>,
284    ) {
285        // Bad region ID
286        let svd_commodities = get_svd_map(&svd_commodity);
287        let demand = [
288            Demand {
289                year: 2020,
290                region_id: "FRA".to_string(),
291                commodity_id: "commodity1".to_string(),
292                demand: Flow(10.0),
293            },
294            Demand {
295                year: 2020,
296                region_id: "USA".to_string(),
297                commodity_id: "commodity1".to_string(),
298                demand: Flow(11.0),
299            },
300        ];
301        assert_error!(
302            read_demand_from_iter(demand.into_iter(), &svd_commodities, &region_ids, &[2020]),
303            "Unknown ID FRA found"
304        );
305    }
306
307    #[rstest]
308    fn test_read_demand_from_iter_bad_year(
309        svd_commodity: Commodity,
310        region_ids: IndexSet<RegionID>,
311    ) {
312        // Bad year
313        let svd_commodities = get_svd_map(&svd_commodity);
314        let demand = [
315            Demand {
316                year: 2010,
317                region_id: "GBR".to_string(),
318                commodity_id: "commodity1".to_string(),
319                demand: Flow(10.0),
320            },
321            Demand {
322                year: 2020,
323                region_id: "USA".to_string(),
324                commodity_id: "commodity1".to_string(),
325                demand: Flow(11.0),
326            },
327        ];
328        assert_error!(
329            read_demand_from_iter(demand.into_iter(), &svd_commodities, &region_ids, &[2020]),
330            "Year 2010 is not a milestone year. \
331            Input of non-milestone years is currently not supported."
332        );
333    }
334
335    #[rstest]
336    #[case(-1.0)]
337    #[case(0.0)]
338    #[case(f64::NAN)]
339    #[case(f64::NEG_INFINITY)]
340    #[case(f64::INFINITY)]
341    fn test_read_demand_from_iter_bad_demand(
342        svd_commodity: Commodity,
343        region_ids: IndexSet<RegionID>,
344        #[case] quantity: f64,
345    ) {
346        // Bad demand quantity
347        let svd_commodities = get_svd_map(&svd_commodity);
348        let demand = [Demand {
349            year: 2020,
350            region_id: "GBR".to_string(),
351            commodity_id: "commodity1".to_string(),
352            demand: Flow(quantity),
353        }];
354        assert_error!(
355            read_demand_from_iter(demand.into_iter(), &svd_commodities, &region_ids, &[2020],),
356            "Demand must be a valid number greater than zero"
357        );
358    }
359
360    #[rstest]
361    fn test_read_demand_from_iter_multiple_entries(
362        svd_commodity: Commodity,
363        region_ids: IndexSet<RegionID>,
364    ) {
365        // Multiple entries for same commodity and region
366        let svd_commodities = get_svd_map(&svd_commodity);
367        let demand = [
368            Demand {
369                year: 2020,
370                region_id: "GBR".to_string(),
371                commodity_id: "commodity1".to_string(),
372                demand: Flow(10.0),
373            },
374            Demand {
375                year: 2020,
376                region_id: "GBR".to_string(),
377                commodity_id: "commodity1".to_string(),
378                demand: Flow(10.0),
379            },
380            Demand {
381                year: 2020,
382                region_id: "USA".to_string(),
383                commodity_id: "commodity1".to_string(),
384                demand: Flow(11.0),
385            },
386        ];
387        assert_error!(
388            read_demand_from_iter(demand.into_iter(), &svd_commodities, &region_ids, &[2020]),
389            "Duplicate demand entries (commodity: commodity1, region: GBR, year: 2020)"
390        );
391    }
392
393    #[rstest]
394    fn test_read_demand_from_iter_missing_year(
395        svd_commodity: Commodity,
396        region_ids: IndexSet<RegionID>,
397    ) {
398        // Missing entry for a milestone year
399        let svd_commodities = get_svd_map(&svd_commodity);
400        let demand = Demand {
401            year: 2020,
402            region_id: "GBR".to_string(),
403            commodity_id: "commodity1".to_string(),
404            demand: Flow(10.0),
405        };
406        assert!(
407            read_demand_from_iter(
408                std::iter::once(demand),
409                &svd_commodities,
410                &region_ids,
411                &[2020, 2030]
412            )
413            .is_err()
414        );
415    }
416
417    /// Create an example demand file in dir_path
418    fn create_demand_file(dir_path: &Path) {
419        let file_path = dir_path.join(DEMAND_FILE_NAME);
420        let mut file = File::create(file_path).unwrap();
421        writeln!(
422            file,
423            "commodity_id,region_id,year,demand\n\
424            commodity1,GBR,2020,10\n\
425            commodity1,USA,2020,11\n"
426        )
427        .unwrap();
428    }
429
430    #[rstest]
431    fn test_read_demand_file(svd_commodity: Commodity, region_ids: IndexSet<RegionID>) {
432        let svd_commodities = get_svd_map(&svd_commodity);
433        let dir = tempdir().unwrap();
434        create_demand_file(dir.path());
435        let milestone_years = [2020];
436        let expected = AnnualDemandMap::from_iter([
437            (
438                ("commodity1".into(), "GBR".into(), 2020),
439                (TimeSliceLevel::DayNight, Flow(10.0)),
440            ),
441            (
442                ("commodity1".into(), "USA".into(), 2020),
443                (TimeSliceLevel::DayNight, Flow(11.0)),
444            ),
445        ]);
446        let demand =
447            read_demand_file(dir.path(), &svd_commodities, &region_ids, &milestone_years).unwrap();
448        assert_eq!(demand, expected);
449    }
450}