muse2/
commodity.rs

1//! Commodities are substances or forms of energy that can be produced and consumed by processes.
2use crate::id::{define_id_getter, define_id_type};
3use crate::region::RegionID;
4use crate::time_slice::{TimeSliceID, TimeSliceLevel, TimeSliceSelection};
5use crate::units::{Flow, MoneyPerFlow};
6use indexmap::IndexMap;
7use serde::Deserialize;
8use serde_string_enum::DeserializeLabeledStringEnum;
9use std::collections::HashMap;
10use std::rc::Rc;
11
12define_id_type! {CommodityID}
13
14/// A map of [`Commodity`]s, keyed by commodity ID
15pub type CommodityMap = IndexMap<CommodityID, Rc<Commodity>>;
16
17/// A map of [`CommodityLevy`]s, keyed by region ID, year and time slice ID
18pub type CommodityLevyMap = HashMap<(RegionID, u32, TimeSliceID), CommodityLevy>;
19
20/// A map of demand values, keyed by region ID, year and time slice selection
21pub type DemandMap = HashMap<(RegionID, u32, TimeSliceSelection), Flow>;
22
23/// A commodity within the simulation.
24///
25/// Represents a substance (e.g. CO2) or form of energy (e.g. electricity) that can be produced or
26/// consumed by processes.
27#[derive(PartialEq, Debug, Deserialize)]
28pub struct Commodity {
29    /// Unique identifier for the commodity (e.g. "ELC")
30    pub id: CommodityID,
31    /// Text description of commodity (e.g. "electricity")
32    pub description: String,
33    /// Commodity balance type
34    #[serde(rename = "type")] // NB: we can't name a field type as it's a reserved keyword
35    pub kind: CommodityType,
36    /// The time slice level for commodity balance
37    pub time_slice_level: TimeSliceLevel,
38    /// Levies for this commodity for different combinations of region, year and time slice.
39    ///
40    /// May be empty if there are no levies for this commodity, otherwise there must be entries for
41    /// every combination of parameters. Note that these values can be negative, indicating an
42    /// incentive.
43    #[serde(skip)]
44    pub levies: CommodityLevyMap,
45    /// Demand as defined in input files. Will be empty for non-service-demand commodities.
46    ///
47    /// The [`TimeSliceSelection`] part of the key is always at the same [`TimeSliceLevel`] as the
48    /// `time_slice_level` field. E.g. if the `time_slice_level` is seasonal, then there will be
49    /// keys representing each season (and not e.g. individual time slices).
50    #[serde(skip)]
51    pub demand: DemandMap,
52}
53define_id_getter! {Commodity, CommodityID}
54
55/// Type of balance for application of cost
56#[derive(PartialEq, Clone, Debug, DeserializeLabeledStringEnum)]
57pub enum BalanceType {
58    /// Applies to both consumption and production
59    #[string = "net"]
60    Net,
61    /// Applies to consumption only
62    #[string = "cons"]
63    Consumption,
64    /// Applies to production only
65    #[string = "prod"]
66    Production,
67}
68
69/// Represents a tax or other external cost on a commodity, as specified in input data.
70///
71/// For example, a CO2 price could be specified in input data to be applied to net CO2. Note that
72/// the value can also be negative, indicating an incentive.
73#[derive(PartialEq, Clone, Debug)]
74pub struct CommodityLevy {
75    /// Type of balance for application of cost
76    pub balance_type: BalanceType,
77    /// Cost per unit commodity
78    pub value: MoneyPerFlow,
79}
80
81/// Commodity balance type
82#[derive(PartialEq, Debug, DeserializeLabeledStringEnum)]
83pub enum CommodityType {
84    /// Supply and demand of this commodity must be balanced
85    #[string = "sed"]
86    SupplyEqualsDemand,
87    /// Specifies a demand (specified in input files) which must be met by the simulation
88    #[string = "svd"]
89    ServiceDemand,
90    /// Either an input or an output to the simulation.
91    ///
92    /// This represents a commodity which can either be produced or consumed, but not both.
93    #[string = "oth"]
94    Other,
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::time_slice::TimeSliceSelection;
101
102    #[test]
103    fn test_demand_map() {
104        let ts_selection = TimeSliceSelection::Single(TimeSliceID {
105            season: "all-year".into(),
106            time_of_day: "all-day".into(),
107        });
108        let value = Flow(0.25);
109        let mut map = DemandMap::new();
110        map.insert(("North".into(), 2020, ts_selection.clone()), value);
111
112        assert_eq!(
113            map.get(&("North".into(), 2020, ts_selection)).unwrap(),
114            &value
115        )
116    }
117
118    #[test]
119    fn test_commodity_levy_map() {
120        let ts = TimeSliceID {
121            season: "winter".into(),
122            time_of_day: "day".into(),
123        };
124        let value = CommodityLevy {
125            balance_type: BalanceType::Consumption,
126            value: MoneyPerFlow(0.5),
127        };
128        let mut map = CommodityLevyMap::new();
129        assert!(map
130            .insert(("GBR".into(), 2010, ts.clone()), value.clone())
131            .is_none());
132        assert_eq!(map.get(&("GBR".into(), 2010, ts)).unwrap(), &value);
133    }
134}