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 std::collections::HashMap;
9use std::ops::RangeInclusive;
10use std::sync::Arc;
11
12define_id_type! {CommodityID, "commodity ID"}
13
14/// A map of [`Commodity`]s, keyed by commodity ID
15pub type CommodityMap = IndexMap<CommodityID, Arc<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 vectors of [`CommodityConstraint`]s, keyed by year
21pub type CommodityConstraintsMap = HashMap<u32, Vec<CommodityConstraint>>;
22
23/// A map of demand values, keyed by region ID, year and time slice selection
24pub type DemandMap = HashMap<(RegionID, u32, TimeSliceSelection), Flow>;
25
26/// A commodity within the simulation.
27///
28/// Represents a substance (e.g. CO2) or form of energy (e.g. electricity) that can be produced or
29/// consumed by processes.
30#[derive(PartialEq, Debug, Clone)]
31pub struct Commodity {
32 /// Unique identifier for the commodity (e.g. "ELC")
33 pub id: CommodityID,
34 /// Text description of commodity (e.g. "electricity")
35 pub description: String,
36 /// Commodity balance type
37 pub kind: CommodityType,
38 /// The time slice level for commodity balance
39 pub time_slice_level: TimeSliceLevel,
40 /// Defines the strategy used for calculating commodity prices
41 pub pricing_strategy: PricingStrategy,
42 /// Production levies for this commodity for different combinations of region, year and time slice.
43 ///
44 /// May be empty if there are no production levies for this commodity, otherwise there must be
45 /// entries for every combination of parameters. Note that these values can be negative,
46 /// indicating an incentive.
47 pub levies_prod: CommodityLevyMap,
48 /// Consumption levies for this commodity for different combinations of region, year and time slice.
49 ///
50 /// May be empty if there are no consumption levies for this commodity, otherwise there must be
51 /// entries for every combination of parameters. Note that these values can be negative,
52 /// indicating an incentive.
53 pub levies_cons: CommodityLevyMap,
54 /// Demand as defined in input files. Will be empty for non-service-demand commodities.
55 ///
56 /// The [`TimeSliceSelection`] part of the key is always at the same [`TimeSliceLevel`] as the
57 /// `time_slice_level` field. E.g. if the `time_slice_level` is seasonal, then there will be
58 /// keys representing each season (and not e.g. individual time slices).
59 pub demand: DemandMap,
60 /// Constraints for this commodity for different combinations of region, year and time slice.
61 ///
62 /// May be empty if there are no constraints for this commodity, otherwise there must be entries
63 /// for every combination of parameters.
64 pub constraints: CommodityConstraintsMap,
65 /// Units for this commodity represented as a string e.g Petajoules, Tonnes
66 /// This is only used for validation purposes.
67 pub units: String,
68}
69define_id_getter! {Commodity, CommodityID}
70
71/// Type of balance for application of cost
72#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Hash)]
73pub enum BalanceType {
74 /// Applies to production, with an equal and opposite levy/incentive on consumption
75 #[serde(rename = "net")]
76 Net,
77 /// Applies to consumption only
78 #[serde(rename = "cons")]
79 Consumption,
80 /// Applies to production only
81 #[serde(rename = "prod")]
82 Production,
83}
84
85/// Commodity balance type
86#[derive(PartialEq, Debug, Deserialize, Clone)]
87pub enum CommodityType {
88 /// Supply and demand of this commodity must be balanced
89 #[serde(rename = "sed")]
90 SupplyEqualsDemand,
91 /// Specifies a demand (specified in input files) which must be met by the simulation
92 #[serde(rename = "svd")]
93 ServiceDemand,
94 /// Either an input or an output to the simulation.
95 ///
96 /// This represents a commodity which can either be produced or consumed, but not both.
97 #[serde(rename = "oth")]
98 Other,
99}
100
101/// The strategy used for calculating commodity prices
102#[derive(Debug, PartialEq, Clone, Copy, Deserialize, Hash, Eq)]
103pub enum PricingStrategy {
104 /// Take commodity prices directly from the shadow prices
105 #[serde(rename = "shadow")]
106 Shadow,
107 /// Adjust shadow prices for scarcity
108 #[serde(rename = "scarcity")]
109 ScarcityAdjusted,
110 /// Use marginal cost of highest-cost active asset producing the commodity
111 #[serde(rename = "marginal")]
112 MarginalCost,
113 /// Use load-weighted average marginal cost across active assets producing the commodity
114 #[serde(rename = "marginal_average")]
115 MarginalCostAverage,
116 /// Use full cost of highest-cost active asset producing the commodity
117 #[serde(rename = "full")]
118 FullCost,
119 /// Use load-weighted average full cost across active assets producing the commodity
120 #[serde(rename = "full_average")]
121 FullCostAverage,
122 /// Commodities that should not have prices calculated
123 #[serde(rename = "unpriced")]
124 Unpriced,
125}
126
127/// A constraint imposed on commodity values
128#[derive(PartialEq, Debug, Clone)]
129pub struct CommodityConstraint {
130 /// Region to which the commodity constraint applies
131 pub region_id: RegionID,
132 /// The balance type for the commodity constraint
133 pub balance_type: BalanceType,
134 /// The time slice selection for the commodity constraint
135 pub ts_selection: TimeSliceSelection,
136 /// The range of values the commodity is constrained to lie between
137 pub limits: RangeInclusive<Flow>,
138}
139
140impl CommodityConstraint {}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::time_slice::TimeSliceSelection;
146
147 #[test]
148 fn demand_map_works() {
149 let ts_selection = TimeSliceSelection::Single(TimeSliceID {
150 season: "all-year".into(),
151 time_of_day: "all-day".into(),
152 });
153 let value = Flow(0.25);
154 let mut map = DemandMap::new();
155 map.insert(("North".into(), 2020, ts_selection.clone()), value);
156
157 assert_eq!(map[&("North".into(), 2020, ts_selection)], value);
158 }
159
160 #[test]
161 fn commodity_levy_map_works() {
162 let ts = TimeSliceID {
163 season: "winter".into(),
164 time_of_day: "day".into(),
165 };
166 let value = MoneyPerFlow(0.5);
167 let mut map = CommodityLevyMap::new();
168 assert!(
169 map.insert(("GBR".into(), 2010, ts.clone()), value)
170 .is_none()
171 );
172 assert_eq!(map[&("GBR".into(), 2010, ts)], value);
173 }
174}