Skip to main content

muse2/input/commodity/
demand.rs

1//! Code for handling commodity demands. Demands may vary by region, year, and time slice.
2use 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/// Represents a single demand entry in the dataset.
21#[allow(clippy::struct_field_names)]
22#[derive(Debug, Clone, Deserialize, PartialEq)]
23struct Demand {
24    /// The commodity this demand entry refers to
25    commodity_id: String,
26    /// The region of the demand entry
27    region_id: String,
28    /// The year of the demand entry
29    year: u32,
30    /// Annual demand quantity
31    #[serde(deserialize_with = "deserialise_finite_non_negative")]
32    demand: Flow,
33}
34
35/// A map relating commodity, region and year to annual demand
36pub type AnnualDemandMap = HashMap<(CommodityID, RegionID, u32), (TimeSliceLevel, Flow)>;
37
38/// A map containing references to commodities
39pub type BorrowedCommodityMap<'a> = HashMap<CommodityID, &'a Commodity>;
40
41/// Reads demand data from CSV files.
42///
43/// # Arguments
44///
45/// * `model_dir` - Folder containing model configuration files
46/// * `commodity_ids` - All possible commodity IDs
47/// * `region_ids` - Known region identifiers
48/// * `time_slice_info` - Information about seasons and times of day
49/// * `milestone_years` - Milestone years used by the model
50///
51/// # Returns
52///
53/// A `HashMap<CommodityID, DemandMap>` mapping each commodity to its `DemandMap`.
54pub 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    // Demand only applies to SVD commodities
62    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
74/// Read the demand.csv file.
75///
76/// # Arguments
77///
78/// * `model_dir` - Folder containing model configuration files
79/// * `svd_commodities` - Map of service demand commodities
80/// * `region_ids` - All possible IDs for regions
81/// * `milestone_years` - All milestone years
82///
83/// # Returns
84///
85/// An `AnnualDemandMap` mapping `(CommodityID, RegionID, year)` to `(TimeSliceLevel, Flow)`.
86fn 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
98/// Read the demand data from an iterator.
99///
100/// # Arguments
101///
102/// * `iter` - An iterator of [`Demand`]s
103/// * `svd_commodities` - Map of service demand commodities
104/// * `region_ids` - All possible IDs for regions
105/// * `milestone_years` - All milestone years
106///
107/// # Returns
108///
109/// An `AnnualDemandMap` mapping `(CommodityID, RegionID, year)` to `(TimeSliceLevel, Flow)`.
110fn 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    // Check that demand data is specified for all combinations of commodity, region and year
152    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
169/// Calculate the demand for each combination of commodity, region, year and time slice.
170///
171/// # Arguments
172///
173/// * `time_slice_info` - Information about time slices
174/// * `demand` - Total annual demand for combinations of commodity, region and year
175/// * `slices` - How annual demand is shared between time slices
176///
177/// # Returns
178///
179/// A `HashMap<CommodityID, DemandMap>` mapping each commodity to its `DemandMap`, which contains
180/// demand values for combinations of region, year and time slice.
181fn 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            // NB: This has already been checked, so shouldn't fail
196            let demand_fraction = slices[&slice_key];
197
198            // Get or create entry
199            let map = map
200                .entry(commodity_id.clone())
201                .or_insert_with(DemandMap::new);
202
203            // Add a new demand entry
204            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        // Valid
243        read_demand_from_iter(demand.into_iter(), &svd_commodities, &region_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        // Bad commodity ID
252        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, &region_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        // Bad region ID
285        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, &region_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        // Bad year
309        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, &region_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        // Multiple entries for same commodity and region
337        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, &region_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        // Missing entry for a milestone year
370        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            &region_ids,
381            &[2020, 2030],
382        )
383        .unwrap_err();
384    }
385
386    /// Create an example demand file in `dir_path`
387    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, &region_ids, &milestone_years).unwrap();
417        assert_eq!(demand, expected);
418    }
419}