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_BROKEN_OPTION_NAME, broken_model_options_allowed};
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}
34
35pub fn read_commodities(
48 model_dir: &Path,
49 region_ids: &IndexSet<RegionID>,
50 time_slice_info: &TimeSliceInfo,
51 milestone_years: &[u32],
52) -> Result<CommodityMap> {
53 let commodities = read_commodities_file(model_dir)?;
55 let commodity_ids = commodities.keys().cloned().collect();
56
57 let mut costs = read_commodity_levies(
59 model_dir,
60 &commodity_ids,
61 region_ids,
62 time_slice_info,
63 milestone_years,
64 )?;
65
66 let mut demand = read_demand(
68 model_dir,
69 &commodities,
70 region_ids,
71 time_slice_info,
72 milestone_years,
73 )?;
74
75 Ok(commodities
77 .into_iter()
78 .map(|(id, mut commodity)| {
79 if let Some(mut costs) = costs.remove(&id) {
80 if let Some(levies) = costs.remove(&BalanceType::Consumption) {
81 commodity.levies_cons = levies;
82 }
83 if let Some(levies) = costs.remove(&BalanceType::Production) {
84 commodity.levies_prod = levies;
85 }
86 }
87 if let Some(demand) = demand.remove(&id) {
88 commodity.demand = demand;
89 }
90
91 (id, commodity.into())
92 })
93 .collect())
94}
95
96fn read_commodities_file(model_dir: &Path) -> Result<IndexMap<CommodityID, Commodity>> {
97 let file_path = model_dir.join(COMMODITY_FILE_NAME);
98 let commodities_csv = read_csv(&file_path)?;
99 read_commodities_file_from_iter(commodities_csv).with_context(|| input_err_msg(&file_path))
100}
101
102fn read_commodities_file_from_iter<I>(iter: I) -> Result<IndexMap<CommodityID, Commodity>>
103where
104 I: Iterator<Item = CommodityRaw>,
105{
106 let mut commodities = IndexMap::new();
107 for commodity_raw in iter {
108 let pricing_strategy = match commodity_raw.pricing_strategy {
109 Some(strategy) => strategy,
110 None => default_pricing_strategy(&commodity_raw.kind),
111 };
112
113 let commodity = Commodity {
114 id: commodity_raw.id.clone(),
115 description: commodity_raw.description,
116 kind: commodity_raw.kind,
117 time_slice_level: commodity_raw.time_slice_level,
118 pricing_strategy,
119 levies_prod: CommodityLevyMap::default(),
120 levies_cons: CommodityLevyMap::default(),
121 demand: DemandMap::default(),
122 };
123
124 validate_commodity(&commodity)?;
125
126 ensure!(
127 commodities.insert(commodity_raw.id, commodity).is_none(),
128 "Duplicate commodity ID"
129 );
130 }
131
132 Ok(commodities)
133}
134
135fn default_pricing_strategy(commodity_kind: &CommodityType) -> PricingStrategy {
137 match commodity_kind {
138 CommodityType::Other => PricingStrategy::Unpriced,
139 CommodityType::SupplyEqualsDemand | CommodityType::ServiceDemand => PricingStrategy::Shadow,
140 }
141}
142
143fn validate_commodity(commodity: &Commodity) -> Result<()> {
144 match commodity.kind {
146 CommodityType::Other => {
147 ensure!(
148 commodity.pricing_strategy == PricingStrategy::Unpriced,
149 "Commodity {} of type Other must be unpriced. \
150 Update its pricing strategy to 'unpriced' or 'default'.",
151 commodity.id
152 );
153 }
154 CommodityType::SupplyEqualsDemand | CommodityType::ServiceDemand => {
155 ensure!(
156 commodity.pricing_strategy != PricingStrategy::Unpriced,
157 "Commodity {} of type {:?} cannot be unpriced. \
158 Update its pricing strategy to a valid option.",
159 commodity.id,
160 commodity.kind
161 );
162 }
163 }
164
165 if !matches!(
167 commodity.pricing_strategy,
168 PricingStrategy::Shadow | PricingStrategy::Unpriced
169 ) {
170 ensure!(
171 broken_model_options_allowed(),
172 "Price strategies other than 'shadow' and 'unpriced' are currently experimental. \
173 To run anyway, set the {ALLOW_BROKEN_OPTION_NAME} option to true."
174 );
175 }
176 if commodity.pricing_strategy == PricingStrategy::ScarcityAdjusted {
177 warn!(
178 "The pricing strategy for {} is set to 'scarcity'. Commodity prices may be \
179 incorrect if assets have more than one output commodity. See: {ISSUES_URL}/677",
180 commodity.id
181 );
182 }
183
184 Ok(())
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 use crate::fixture::assert_error;
191 use crate::time_slice::TimeSliceLevel;
192
193 fn make_commodity(kind: CommodityType, pricing_strategy: PricingStrategy) -> Commodity {
194 Commodity {
195 id: "ELC".into(),
196 description: "test".into(),
197 kind,
198 time_slice_level: TimeSliceLevel::Annual,
199 pricing_strategy,
200 levies_prod: CommodityLevyMap::default(),
201 levies_cons: CommodityLevyMap::default(),
202 demand: DemandMap::default(),
203 }
204 }
205
206 #[test]
207 fn validate_commodity_works() {
208 let commodity = make_commodity(CommodityType::SupplyEqualsDemand, PricingStrategy::Shadow);
209 validate_commodity(&commodity).unwrap();
210 }
211
212 #[test]
213 fn validate_commodity_other_priced() {
214 let commodity = make_commodity(CommodityType::Other, PricingStrategy::MarginalCost);
215 assert_error!(
216 validate_commodity(&commodity),
217 "Commodity ELC of type Other must be unpriced. Update its pricing strategy to 'unpriced' or 'default'."
218 );
219 }
220
221 #[test]
222 fn validate_commodity_sed_unpriced() {
223 let commodity =
224 make_commodity(CommodityType::SupplyEqualsDemand, PricingStrategy::Unpriced);
225 assert_error!(
226 validate_commodity(&commodity),
227 "Commodity ELC of type SupplyEqualsDemand cannot be unpriced. Update its pricing strategy to a valid option."
228 );
229 }
230}