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 [`MoneyPerFlow`]s, keyed by region ID, year and time slice ID for a specific levy
18pub type CommodityLevyMap = HashMap<(RegionID, u32, TimeSliceID), MoneyPerFlow>;
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, Clone)]
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 pub kind: CommodityType,
35 /// The time slice level for commodity balance
36 pub time_slice_level: TimeSliceLevel,
37 /// Defines the strategy used for calculating commodity prices
38 pub pricing_strategy: PricingStrategy,
39 /// Production levies for this commodity for different combinations of region, year and time slice.
40 ///
41 /// May be empty if there are no production levies for this commodity, otherwise there must be
42 /// entries for every combination of parameters. Note that these values can be negative,
43 /// indicating an incentive.
44 pub levies_prod: CommodityLevyMap,
45 /// Consumption levies for this commodity for different combinations of region, year and time slice.
46 ///
47 /// May be empty if there are no consumption levies for this commodity, otherwise there must be
48 /// entries for every combination of parameters. Note that these values can be negative,
49 /// indicating an incentive.
50 pub levies_cons: CommodityLevyMap,
51 /// Demand as defined in input files. Will be empty for non-service-demand commodities.
52 ///
53 /// The [`TimeSliceSelection`] part of the key is always at the same [`TimeSliceLevel`] as the
54 /// `time_slice_level` field. E.g. if the `time_slice_level` is seasonal, then there will be
55 /// keys representing each season (and not e.g. individual time slices).
56 pub demand: DemandMap,
57 /// Units for this commodity represented as a string e.g Petajoules, Tonnes
58 /// This is only used for validation purposes.
59 pub units: String,
60}
61define_id_getter! {Commodity, CommodityID}
62
63/// Type of balance for application of cost
64#[derive(Eq, PartialEq, Clone, Debug, DeserializeLabeledStringEnum, Hash)]
65pub enum BalanceType {
66 /// Applies to production, with an equal and opposite levy/incentive on consumption
67 #[string = "net"]
68 Net,
69 /// Applies to consumption only
70 #[string = "cons"]
71 Consumption,
72 /// Applies to production only
73 #[string = "prod"]
74 Production,
75}
76
77/// Commodity balance type
78#[derive(PartialEq, Debug, DeserializeLabeledStringEnum, Clone)]
79pub enum CommodityType {
80 /// Supply and demand of this commodity must be balanced
81 #[string = "sed"]
82 SupplyEqualsDemand,
83 /// Specifies a demand (specified in input files) which must be met by the simulation
84 #[string = "svd"]
85 ServiceDemand,
86 /// Either an input or an output to the simulation.
87 ///
88 /// This represents a commodity which can either be produced or consumed, but not both.
89 #[string = "oth"]
90 Other,
91}
92
93/// The strategy used for calculating commodity prices
94#[derive(Debug, PartialEq, Clone, Deserialize, Hash, Eq)]
95pub enum PricingStrategy {
96 /// Take commodity prices directly from the shadow prices
97 #[serde(rename = "shadow")]
98 Shadow,
99 /// Adjust shadow prices for scarcity
100 #[serde(rename = "scarcity")]
101 ScarcityAdjusted,
102 /// Use marginal cost of highest-cost active asset producing the commodity
103 #[serde(rename = "marginal")]
104 MarginalCost,
105 /// Use full cost of highest-cost active asset producing the commodity
106 #[serde(rename = "full")]
107 FullCost,
108 /// Commodities that should not have prices calculated
109 #[serde(rename = "unpriced")]
110 Unpriced,
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::time_slice::TimeSliceSelection;
117
118 #[test]
119 fn demand_map_works() {
120 let ts_selection = TimeSliceSelection::Single(TimeSliceID {
121 season: "all-year".into(),
122 time_of_day: "all-day".into(),
123 });
124 let value = Flow(0.25);
125 let mut map = DemandMap::new();
126 map.insert(("North".into(), 2020, ts_selection.clone()), value);
127
128 assert_eq!(map[&("North".into(), 2020, ts_selection)], value);
129 }
130
131 #[test]
132 fn commodity_levy_map_works() {
133 let ts = TimeSliceID {
134 season: "winter".into(),
135 time_of_day: "day".into(),
136 };
137 let value = MoneyPerFlow(0.5);
138 let mut map = CommodityLevyMap::new();
139 assert!(
140 map.insert(("GBR".into(), 2010, ts.clone()), value)
141 .is_none()
142 );
143 assert_eq!(map[&("GBR".into(), 2010, ts)], value);
144 }
145}