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: CommodityLevyMap::new(),
223 demand: DemandMap::new(),
224 }),
225 )]);
226
227 let mut map = AgentCommodityPortionsMap::new();
229 map.insert(("commodity1".into(), 2020), Dimensionless(1.0));
230 let agent_commodity_portions = HashMap::from([("agent1".into(), map)]);
231 assert!(
232 validate_agent_commodity_portions(
233 &agent_commodity_portions,
234 &agents,
235 &commodities,
236 ®ion_ids,
237 &milestone_years
238 )
239 .is_ok()
240 );
241
242 let mut map_v2 = AgentCommodityPortionsMap::new();
244 map_v2.insert(("commodity1".into(), 2020), Dimensionless(0.5));
245 let agent_commodities_v2 = HashMap::from([("agent1".into(), map_v2)]);
246 assert!(
247 validate_agent_commodity_portions(
248 &agent_commodities_v2,
249 &agents,
250 &commodities,
251 ®ion_ids,
252 &milestone_years
253 )
254 .is_err()
255 );
256
257 commodities.insert(
259 CommodityID::new("commodity2"),
260 Rc::new(Commodity {
261 id: "commodity2".into(),
262 description: "Another commodity".into(),
263 kind: CommodityType::SupplyEqualsDemand,
264 time_slice_level: TimeSliceLevel::Annual,
265 levies: CommodityLevyMap::new(),
266 demand: DemandMap::new(),
267 }),
268 );
269 assert!(
270 validate_agent_commodity_portions(
271 &agent_commodity_portions,
272 &agents,
273 &commodities,
274 ®ion_ids,
275 &milestone_years
276 )
277 .is_err()
278 );
279 }
280}