1use super::{input_err_msg, read_csv};
3use crate::ISSUES_URL;
4use crate::commodity::{
5 BalanceType, Commodity, CommodityID, CommodityLevyMap, CommodityMap, CommodityType, DemandMap,
6 PricingStrategy,
7};
8use crate::model::{ALLOW_DANGEROUS_OPTION_NAME, dangerous_model_options_enabled};
9use crate::region::RegionID;
10use crate::time_slice::{TimeSliceInfo, TimeSliceLevel};
11use anyhow::{Context, Ok, Result, ensure};
12use indexmap::{IndexMap, IndexSet};
13use log::warn;
14use serde::Deserialize;
15use std::path::Path;
16
17mod levy;
18use levy::read_commodity_levies;
19mod demand;
20use demand::read_demand;
21mod demand_slicing;
22
23const COMMODITY_FILE_NAME: &str = "commodities.csv";
24
25#[derive(PartialEq, Debug, Deserialize)]
26struct CommodityRaw {
27 pub id: CommodityID,
28 pub description: String,
29 #[serde(rename = "type")] pub kind: CommodityType,
31 pub time_slice_level: TimeSliceLevel,
32 pub pricing_strategy: Option<PricingStrategy>,
33 pub units: String,
34}
35
36pub fn read_commodities(
49 model_dir: &Path,
50 region_ids: &IndexSet<RegionID>,
51 time_slice_info: &TimeSliceInfo,
52 milestone_years: &[u32],
53) -> Result<CommodityMap> {
54 let commodities = read_commodities_file(model_dir)?;
56 let commodity_ids = commodities.keys().cloned().collect();
57
58 let mut costs = read_commodity_levies(
60 model_dir,
61 &commodity_ids,
62 region_ids,
63 time_slice_info,
64 milestone_years,
65 )?;
66
67 let mut demand = read_demand(
69 model_dir,
70 &commodities,
71 region_ids,
72 time_slice_info,
73 milestone_years,
74 )?;
75
76 Ok(commodities
78 .into_iter()
79 .map(|(id, mut commodity)| {
80 if let Some(mut costs) = costs.remove(&id) {
81 if let Some(levies) = costs.remove(&BalanceType::Consumption) {
82 commodity.levies_cons = levies;
83 }
84 if let Some(levies) = costs.remove(&BalanceType::Production) {
85 commodity.levies_prod = levies;
86 }
87 }
88 if let Some(demand) = demand.remove(&id) {
89 commodity.demand = demand;
90 }
91
92 (id, commodity.into())
93 })
94 .collect())
95}
96
97fn read_commodities_file(model_dir: &Path) -> Result<IndexMap<CommodityID, Commodity>> {
98 let file_path = model_dir.join(COMMODITY_FILE_NAME);
99 let commodities_csv = read_csv(&file_path)?;
100 read_commodities_file_from_iter(commodities_csv).with_context(|| input_err_msg(&file_path))
101}
102
103fn read_commodities_file_from_iter<I>(iter: I) -> Result<IndexMap<CommodityID, Commodity>>
104where
105 I: Iterator<Item = CommodityRaw>,
106{
107 let mut commodities = IndexMap::new();
108 for commodity_raw in iter {
109 let pricing_strategy = match commodity_raw.pricing_strategy {
110 Some(strategy) => strategy,
111 None => default_pricing_strategy(&commodity_raw.kind),
112 };
113
114 let commodity = Commodity {
115 id: commodity_raw.id.clone(),
116 description: commodity_raw.description,
117 kind: commodity_raw.kind,
118 time_slice_level: commodity_raw.time_slice_level,
119 pricing_strategy,
120 levies_prod: CommodityLevyMap::default(),
121 levies_cons: CommodityLevyMap::default(),
122 demand: DemandMap::default(),
123 units: commodity_raw.units,
124 };
125
126 validate_commodity(&commodity)?;
127
128 ensure!(
129 commodities.insert(commodity_raw.id, commodity).is_none(),
130 "Duplicate commodity ID"
131 );
132 }
133
134 Ok(commodities)
135}
136
137fn default_pricing_strategy(commodity_kind: &CommodityType) -> PricingStrategy {
139 match commodity_kind {
140 CommodityType::Other => PricingStrategy::Unpriced,
141 CommodityType::SupplyEqualsDemand | CommodityType::ServiceDemand => PricingStrategy::Shadow,
142 }
143}
144
145fn validate_commodity(commodity: &Commodity) -> Result<()> {
146 match commodity.kind {
148 CommodityType::Other => {
149 ensure!(
150 commodity.pricing_strategy == PricingStrategy::Unpriced,
151 "Commodity {} of type Other must be unpriced. \
152 Update its pricing strategy to 'unpriced' or 'default'.",
153 commodity.id
154 );
155 }
156 CommodityType::SupplyEqualsDemand | CommodityType::ServiceDemand => {
157 ensure!(
158 commodity.pricing_strategy != PricingStrategy::Unpriced,
159 "Commodity {} of type {:?} cannot be unpriced. \
160 Update its pricing strategy to a valid option.",
161 commodity.id,
162 commodity.kind
163 );
164 }
165 }
166
167 if commodity.pricing_strategy == PricingStrategy::ScarcityAdjusted {
169 ensure!(
170 dangerous_model_options_enabled(),
171 "The 'scarcity' pricing strategy is currently experimental. \
172 To run anyway, set the {ALLOW_DANGEROUS_OPTION_NAME} option to true."
173 );
174 warn!(
175 "The pricing strategy for {} is set to 'scarcity'. Commodity prices may be \
176 incorrect if assets have more than one output commodity. See: {ISSUES_URL}/677",
177 commodity.id
178 );
179 }
180
181 ensure!(
183 !commodity.units.trim().is_empty(),
184 "Commodity {} requires units to be specified.",
185 commodity.id
186 );
187
188 Ok(())
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use crate::fixture::assert_error;
195 use crate::time_slice::TimeSliceLevel;
196
197 fn make_commodity(kind: CommodityType, pricing_strategy: PricingStrategy) -> Commodity {
198 Commodity {
199 id: "ELC".into(),
200 description: "test".into(),
201 kind,
202 time_slice_level: TimeSliceLevel::Annual,
203 pricing_strategy,
204 levies_prod: CommodityLevyMap::default(),
205 levies_cons: CommodityLevyMap::default(),
206 demand: DemandMap::default(),
207 units: "PJ".into(),
208 }
209 }
210
211 #[test]
212 fn validate_commodity_works() {
213 let commodity = make_commodity(CommodityType::SupplyEqualsDemand, PricingStrategy::Shadow);
214 validate_commodity(&commodity).unwrap();
215 }
216
217 #[test]
218 fn validate_commodity_other_priced() {
219 let commodity = make_commodity(CommodityType::Other, PricingStrategy::MarginalCost);
220 assert_error!(
221 validate_commodity(&commodity),
222 "Commodity ELC of type Other must be unpriced. Update its pricing strategy to 'unpriced' or 'default'."
223 );
224 }
225
226 #[test]
227 fn validate_commodity_sed_unpriced() {
228 let commodity =
229 make_commodity(CommodityType::SupplyEqualsDemand, PricingStrategy::Unpriced);
230 assert_error!(
231 validate_commodity(&commodity),
232 "Commodity ELC of type SupplyEqualsDemand cannot be unpriced. Update its pricing strategy to a valid option."
233 );
234 }
235
236 #[test]
237 fn validate_commodity_remove_units() {
238 let mut commodity = make_commodity(CommodityType::Other, PricingStrategy::Unpriced);
239 commodity.units = " ".into();
240 assert_error!(
241 validate_commodity(&commodity),
242 "Commodity ELC requires units to be specified."
243 );
244 }
245}