1use super::super::{deserialise_proportion_nonzero, input_err_msg, read_csv, try_insert};
3use crate::agent::{AgentCommodityPortionsMap, AgentID, AgentMap};
4use crate::commodity::{CommodityMap, CommodityType};
5use crate::id::IDCollection;
6use crate::region::RegionID;
7use crate::units::Dimensionless;
8use crate::year::parse_year_str;
9use anyhow::{Context, Result, ensure};
10use float_cmp::approx_eq;
11use indexmap::IndexSet;
12use serde::Deserialize;
13use std::collections::{HashMap, HashSet};
14use std::path::Path;
15
16const AGENT_COMMODITIES_FILE_NAME: &str = "agent_commodity_portions.csv";
17
18#[derive(PartialEq, Debug, Deserialize)]
19struct AgentCommodityPortionRaw {
20 agent_id: String,
22 commodity_id: String,
24 years: String,
26 #[serde(deserialize_with = "deserialise_proportion_nonzero")]
28 commodity_portion: Dimensionless,
29}
30
31pub fn read_agent_commodity_portions(
45 model_dir: &Path,
46 agents: &AgentMap,
47 commodities: &CommodityMap,
48 region_ids: &IndexSet<RegionID>,
49 milestone_years: &[u32],
50) -> Result<HashMap<AgentID, AgentCommodityPortionsMap>> {
51 let file_path = model_dir.join(AGENT_COMMODITIES_FILE_NAME);
52 let agent_commodity_portions_csv = read_csv(&file_path)?;
53 read_agent_commodity_portions_from_iter(
54 agent_commodity_portions_csv,
55 agents,
56 commodities,
57 region_ids,
58 milestone_years,
59 )
60 .with_context(|| input_err_msg(&file_path))
61}
62
63fn read_agent_commodity_portions_from_iter<I>(
64 iter: I,
65 agents: &AgentMap,
66 commodities: &CommodityMap,
67 region_ids: &IndexSet<RegionID>,
68 milestone_years: &[u32],
69) -> Result<HashMap<AgentID, AgentCommodityPortionsMap>>
70where
71 I: Iterator<Item = AgentCommodityPortionRaw>,
72{
73 let mut agent_commodity_portions = HashMap::new();
74 for agent_commodity_portion_raw in iter {
75 let agent_id_raw = agent_commodity_portion_raw.agent_id.as_str();
77 let id = agents.get_id(agent_id_raw)?;
78
79 let entry = agent_commodity_portions
81 .entry(id.clone())
82 .or_insert_with(AgentCommodityPortionsMap::new);
83
84 let commodity_id_raw = agent_commodity_portion_raw.commodity_id.as_str();
86 let commodity_id = commodities.get_id(commodity_id_raw)?;
87 let years = parse_year_str(&agent_commodity_portion_raw.years, milestone_years)?;
88 for year in years {
89 try_insert(
90 entry,
91 &(commodity_id.clone(), year),
92 agent_commodity_portion_raw.commodity_portion,
93 )?;
94 }
95 }
96
97 validate_agent_commodity_portions(
98 &agent_commodity_portions,
99 agents,
100 commodities,
101 region_ids,
102 milestone_years,
103 )?;
104
105 Ok(agent_commodity_portions)
106}
107
108fn validate_agent_commodity_portions(
109 agent_commodity_portions: &HashMap<AgentID, AgentCommodityPortionsMap>,
110 agents: &AgentMap,
111 commodities: &CommodityMap,
112 region_ids: &IndexSet<RegionID>,
113 milestone_years: &[u32],
114) -> Result<()> {
115 for (id, portions) in agent_commodity_portions {
117 let commodity_ids: HashSet<_> = portions.keys().map(|(id, _)| id).collect();
119
120 for commodity_id in commodity_ids {
122 for year in milestone_years {
123 ensure!(
124 portions.contains_key(&(commodity_id.clone(), *year)),
125 "Agent {id} does not have data for commodity {commodity_id} in year {year}"
126 );
127 }
128 }
129 }
130
131 let mut summed_portions = HashMap::new();
135 for (id, agent_commodity_portions) in agent_commodity_portions {
136 let agent = agents.get(id).context("Invalid agent ID")?;
137 for ((commodity_id, year), portion) in agent_commodity_portions {
138 for region in region_ids {
139 if agent.regions.contains(region) {
140 let key = (commodity_id, year, region);
141 summed_portions
142 .entry(key)
143 .and_modify(|v| *v += *portion)
144 .or_insert(*portion);
145 }
146 }
147 }
148 }
149
150 for (key, portion) in &summed_portions {
152 ensure!(
153 approx_eq!(Dimensionless, *portion, Dimensionless(1.0), epsilon = 1e-5),
154 "Commodity {} in year {} and region {} does not sum to 1.0",
155 key.0,
156 key.1,
157 key.2
158 );
159 }
160
161 let svd_and_sed_commodities = commodities
165 .iter()
166 .filter(|(_, commodity)| {
167 matches!(
168 commodity.kind,
169 CommodityType::SupplyEqualsDemand | CommodityType::ServiceDemand
170 )
171 })
172 .map(|(id, _)| id);
173
174 for commodity_id in svd_and_sed_commodities {
177 for year in milestone_years {
178 for region in region_ids {
179 let key = (commodity_id, year, region);
180 ensure!(
181 summed_portions.contains_key(&key),
182 "Commodity {commodity_id} in year {year} and region {region} is not covered"
183 );
184 }
185 }
186 }
187
188 Ok(())
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use crate::agent::{Agent, AgentObjectiveMap, AgentSearchSpaceMap, DecisionRule};
195 use crate::commodity::{
196 Commodity, CommodityID, CommodityLevyMap, CommodityType, DemandMap, PricingStrategy,
197 };
198 use crate::time_slice::TimeSliceLevel;
199 use indexmap::IndexMap;
200 use std::rc::Rc;
201
202 #[test]
203 fn validate_agent_commodity_portions_works() {
204 let region_ids = IndexSet::from([RegionID::new("region1"), RegionID::new("region2")]);
205 let milestone_years = [2020];
206 let agents = IndexMap::from([(
207 AgentID::new("agent1"),
208 Agent {
209 id: "agent1".into(),
210 description: "An agent".into(),
211 commodity_portions: AgentCommodityPortionsMap::new(),
212 search_space: AgentSearchSpaceMap::new(),
213 decision_rule: DecisionRule::Single,
214 regions: region_ids.clone(),
215 objectives: AgentObjectiveMap::new(),
216 },
217 )]);
218 let mut commodities = IndexMap::from([(
219 CommodityID::new("commodity1"),
220 Rc::new(Commodity {
221 id: "commodity1".into(),
222 description: "A commodity".into(),
223 kind: CommodityType::SupplyEqualsDemand,
224 time_slice_level: TimeSliceLevel::Annual,
225 pricing_strategy: PricingStrategy::Shadow,
226 levies_prod: CommodityLevyMap::new(),
227 levies_cons: CommodityLevyMap::new(),
228 demand: DemandMap::new(),
229 }),
230 )]);
231
232 let mut map = AgentCommodityPortionsMap::new();
234 map.insert(("commodity1".into(), 2020), Dimensionless(1.0));
235 let agent_commodity_portions = HashMap::from([("agent1".into(), map)]);
236 validate_agent_commodity_portions(
237 &agent_commodity_portions,
238 &agents,
239 &commodities,
240 ®ion_ids,
241 &milestone_years,
242 )
243 .unwrap();
244
245 let mut map_v2 = AgentCommodityPortionsMap::new();
247 map_v2.insert(("commodity1".into(), 2020), Dimensionless(0.5));
248 let agent_commodities_v2 = HashMap::from([("agent1".into(), map_v2)]);
249 assert!(
250 validate_agent_commodity_portions(
251 &agent_commodities_v2,
252 &agents,
253 &commodities,
254 ®ion_ids,
255 &milestone_years
256 )
257 .is_err()
258 );
259
260 commodities.insert(
262 CommodityID::new("commodity2"),
263 Rc::new(Commodity {
264 id: "commodity2".into(),
265 description: "Another commodity".into(),
266 kind: CommodityType::SupplyEqualsDemand,
267 time_slice_level: TimeSliceLevel::Annual,
268 pricing_strategy: PricingStrategy::Shadow,
269 levies_prod: CommodityLevyMap::new(),
270 levies_cons: CommodityLevyMap::new(),
271 demand: DemandMap::new(),
272 }),
273 );
274 assert!(
275 validate_agent_commodity_portions(
276 &agent_commodity_portions,
277 &agents,
278 &commodities,
279 ®ion_ids,
280 &milestone_years
281 )
282 .is_err()
283 );
284 }
285}