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(
41 model_dir: &Path,
42 agents: &AgentMap,
43 commodities: &CommodityMap,
44 region_ids: &IndexSet<RegionID>,
45 milestone_years: &[u32],
46) -> Result<HashMap<AgentID, AgentCommodityPortionsMap>> {
47 let file_path = model_dir.join(AGENT_COMMODITIES_FILE_NAME);
48 let agent_commodity_portions_csv = read_csv(&file_path)?;
49 read_agent_commodity_portions_from_iter(
50 agent_commodity_portions_csv,
51 agents,
52 commodities,
53 region_ids,
54 milestone_years,
55 )
56 .with_context(|| input_err_msg(&file_path))
57}
58
59fn read_agent_commodity_portions_from_iter<I>(
60 iter: I,
61 agents: &AgentMap,
62 commodities: &CommodityMap,
63 region_ids: &IndexSet<RegionID>,
64 milestone_years: &[u32],
65) -> Result<HashMap<AgentID, AgentCommodityPortionsMap>>
66where
67 I: Iterator<Item = AgentCommodityPortionRaw>,
68{
69 let mut agent_commodity_portions = HashMap::new();
70 for agent_commodity_portion_raw in iter {
71 let agent_id_raw = agent_commodity_portion_raw.agent_id.as_str();
73 let id = agents.get_id(agent_id_raw)?;
74
75 let entry = agent_commodity_portions
77 .entry(id.clone())
78 .or_insert_with(AgentCommodityPortionsMap::new);
79
80 let commodity_id_raw = agent_commodity_portion_raw.commodity_id.as_str();
82 let commodity_id = commodities.get_id(commodity_id_raw)?;
83 let years = parse_year_str(&agent_commodity_portion_raw.years, milestone_years)?;
84 for year in years {
85 try_insert(
86 entry,
87 &(commodity_id.clone(), year),
88 agent_commodity_portion_raw.commodity_portion,
89 )?;
90 }
91 }
92
93 validate_agent_commodity_portions(
94 &agent_commodity_portions,
95 agents,
96 commodities,
97 region_ids,
98 milestone_years,
99 )?;
100
101 Ok(agent_commodity_portions)
102}
103
104fn validate_agent_commodity_portions(
105 agent_commodity_portions: &HashMap<AgentID, AgentCommodityPortionsMap>,
106 agents: &AgentMap,
107 commodities: &CommodityMap,
108 region_ids: &IndexSet<RegionID>,
109 milestone_years: &[u32],
110) -> Result<()> {
111 for (id, portions) in agent_commodity_portions {
113 let commodity_ids: HashSet<_> = portions.keys().map(|(id, _)| id).collect();
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 {id} does not have data for commodity {commodity_id} in year {year}"
122 );
123 }
124 }
125 }
126
127 let mut summed_portions = HashMap::new();
131 for (id, agent_commodity_portions) in agent_commodity_portions {
132 let agent = agents.get(id).context("Invalid agent ID")?;
133 for ((commodity_id, year), portion) in agent_commodity_portions {
134 for region in region_ids {
135 if agent.regions.contains(region) {
136 let key = (commodity_id, year, region);
137 summed_portions
138 .entry(key)
139 .and_modify(|v| *v += *portion)
140 .or_insert(*portion);
141 }
142 }
143 }
144 }
145
146 for (key, portion) in &summed_portions {
148 ensure!(
149 approx_eq!(Dimensionless, *portion, Dimensionless(1.0), epsilon = 1e-5),
150 "Commodity {} in year {} and region {} does not sum to 1.0",
151 key.0,
152 key.1,
153 key.2
154 );
155 }
156
157 let svd_and_sed_commodities = commodities
161 .iter()
162 .filter(|(_, commodity)| {
163 matches!(
164 commodity.kind,
165 CommodityType::SupplyEqualsDemand | CommodityType::ServiceDemand
166 )
167 })
168 .map(|(id, _)| id);
169
170 for commodity_id in svd_and_sed_commodities {
173 for year in milestone_years {
174 for region in region_ids {
175 let key = (commodity_id, year, region);
176 ensure!(
177 summed_portions.contains_key(&key),
178 "Commodity {commodity_id} in year {year} and region {region} is not covered"
179 );
180 }
181 }
182 }
183
184 Ok(())
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 use crate::agent::{
191 Agent, AgentCostLimitsMap, AgentObjectiveMap, AgentSearchSpaceMap, DecisionRule,
192 };
193 use crate::commodity::{Commodity, CommodityID, CommodityLevyMap, CommodityType, DemandMap};
194 use crate::time_slice::TimeSliceLevel;
195 use indexmap::IndexMap;
196 use std::rc::Rc;
197
198 #[test]
199 fn test_validate_agent_commodity_portions() {
200 let region_ids = IndexSet::from([RegionID::new("region1"), RegionID::new("region2")]);
201 let milestone_years = [2020];
202 let agents = IndexMap::from([(
203 AgentID::new("agent1"),
204 Agent {
205 id: "agent1".into(),
206 description: "An agent".into(),
207 commodity_portions: AgentCommodityPortionsMap::new(),
208 search_space: AgentSearchSpaceMap::new(),
209 decision_rule: DecisionRule::Single,
210 cost_limits: AgentCostLimitsMap::new(),
211 regions: region_ids.clone(),
212 objectives: AgentObjectiveMap::new(),
213 },
214 )]);
215 let mut commodities = IndexMap::from([(
216 CommodityID::new("commodity1"),
217 Rc::new(Commodity {
218 id: "commodity1".into(),
219 description: "A commodity".into(),
220 kind: CommodityType::SupplyEqualsDemand,
221 time_slice_level: TimeSliceLevel::Annual,
222 levies_prod: CommodityLevyMap::new(),
223 levies_cons: CommodityLevyMap::new(),
224 demand: DemandMap::new(),
225 }),
226 )]);
227
228 let mut map = AgentCommodityPortionsMap::new();
230 map.insert(("commodity1".into(), 2020), Dimensionless(1.0));
231 let agent_commodity_portions = HashMap::from([("agent1".into(), map)]);
232 assert!(
233 validate_agent_commodity_portions(
234 &agent_commodity_portions,
235 &agents,
236 &commodities,
237 ®ion_ids,
238 &milestone_years
239 )
240 .is_ok()
241 );
242
243 let mut map_v2 = AgentCommodityPortionsMap::new();
245 map_v2.insert(("commodity1".into(), 2020), Dimensionless(0.5));
246 let agent_commodities_v2 = HashMap::from([("agent1".into(), map_v2)]);
247 assert!(
248 validate_agent_commodity_portions(
249 &agent_commodities_v2,
250 &agents,
251 &commodities,
252 ®ion_ids,
253 &milestone_years
254 )
255 .is_err()
256 );
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_prod: CommodityLevyMap::new(),
267 levies_cons: CommodityLevyMap::new(),
268 demand: DemandMap::new(),
269 }),
270 );
271 assert!(
272 validate_agent_commodity_portions(
273 &agent_commodity_portions,
274 &agents,
275 &commodities,
276 ®ion_ids,
277 &milestone_years
278 )
279 .is_err()
280 );
281 }
282}