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