1use super::super::*;
3use crate::agent::{AgentCommodityPortionsMap, AgentID, AgentMap};
4use crate::commodity::{CommodityID, CommodityMap, CommodityType};
5use crate::region::RegionID;
6use crate::year::parse_year_str;
7use anyhow::{ensure, Context, Result};
8use serde::Deserialize;
9use std::collections::HashMap;
10use std::path::Path;
11
12const AGENT_COMMODITIES_FILE_NAME: &str = "agent_commodity_portions.csv";
13
14#[derive(PartialEq, Debug, Deserialize)]
15struct AgentCommodityPortionRaw {
16 agent_id: String,
18 commodity_id: String,
20 year: String,
22 #[serde(deserialize_with = "deserialise_proportion_nonzero")]
24 commodity_portion: f64,
25}
26
27pub fn read_agent_commodity_portions(
37 model_dir: &Path,
38 agents: &AgentMap,
39 commodities: &CommodityMap,
40 region_ids: &HashSet<RegionID>,
41 milestone_years: &[u32],
42) -> Result<HashMap<AgentID, AgentCommodityPortionsMap>> {
43 let file_path = model_dir.join(AGENT_COMMODITIES_FILE_NAME);
44 let agent_commodity_portions_csv = read_csv(&file_path)?;
45 read_agent_commodity_portions_from_iter(
46 agent_commodity_portions_csv,
47 agents,
48 commodities,
49 region_ids,
50 milestone_years,
51 )
52 .with_context(|| input_err_msg(&file_path))
53}
54
55fn read_agent_commodity_portions_from_iter<I>(
56 iter: I,
57 agents: &AgentMap,
58 commodities: &CommodityMap,
59 region_ids: &HashSet<RegionID>,
60 milestone_years: &[u32],
61) -> Result<HashMap<AgentID, AgentCommodityPortionsMap>>
62where
63 I: Iterator<Item = AgentCommodityPortionRaw>,
64{
65 let mut agent_commodity_portions = HashMap::new();
66 for agent_commodity_portion_raw in iter {
67 let agent_id_raw = agent_commodity_portion_raw.agent_id.as_str();
69 let (id, _agent) = agents
70 .get_key_value(agent_id_raw)
71 .with_context(|| format!("Invalid agent ID {agent_id_raw}"))?;
72
73 let entry = agent_commodity_portions
75 .entry(id.clone())
76 .or_insert_with(AgentCommodityPortionsMap::new);
77
78 let commodity_id_raw = agent_commodity_portion_raw.commodity_id.as_str();
80 let (commodity_id, _commodity) = commodities
81 .get_key_value(commodity_id_raw)
82 .with_context(|| format!("Invalid commodity ID {commodity_id_raw}"))?;
83 let years = parse_year_str(&agent_commodity_portion_raw.year, 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: &HashSet<RegionID>,
109 milestone_years: &[u32],
110) -> Result<()> {
111 for (id, portions) in agent_commodity_portions {
113 let commodity_ids: HashSet<CommodityID> =
115 HashSet::from_iter(portions.keys().map(|(id, _)| id.clone()));
116
117 for commodity_id in commodity_ids {
119 for year in milestone_years {
120 ensure!(
121 portions.contains_key(&(commodity_id.clone(), *year)),
122 "Agent {} does not have data for commodity {} in year {}",
123 id,
124 commodity_id,
125 year
126 );
127 }
128 }
129 }
130
131 let mut summed_portions = HashMap::new();
135 for (id, agent_commodity_portions) in agent_commodity_portions.iter() {
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.iter() {
152 ensure!(
153 approx_eq!(f64, *portion, 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.clone());
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 {} in year {} and region {} is not covered",
183 commodity_id,
184 year,
185 region
186 );
187 }
188 }
189 }
190
191 Ok(())
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use crate::agent::{Agent, AgentCostLimitsMap, DecisionRule};
198 use crate::commodity::{Commodity, CommodityCostMap, CommodityID, CommodityType, DemandMap};
199 use crate::time_slice::TimeSliceLevel;
200 use std::rc::Rc;
201
202 #[test]
203 fn test_validate_agent_commodity_portions() {
204 let region_ids = HashSet::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: Vec::new(),
213 decision_rule: DecisionRule::Single,
214 cost_limits: AgentCostLimitsMap::new(),
215 regions: region_ids.clone(),
216 objectives: Vec::new(),
217 },
218 )]);
219 let mut commodities = IndexMap::from([(
220 CommodityID::new("commodity1"),
221 Rc::new(Commodity {
222 id: "commodity1".into(),
223 description: "A commodity".into(),
224 kind: CommodityType::SupplyEqualsDemand,
225 time_slice_level: TimeSliceLevel::Annual,
226 costs: CommodityCostMap::new(),
227 demand: DemandMap::new(),
228 }),
229 )]);
230
231 let mut map = AgentCommodityPortionsMap::new();
233 map.insert(("commodity1".into(), 2020), 1.0);
234 let agent_commodity_portions = HashMap::from([("agent1".into(), map)]);
235 assert!(validate_agent_commodity_portions(
236 &agent_commodity_portions,
237 &agents,
238 &commodities,
239 ®ion_ids,
240 &milestone_years
241 )
242 .is_ok());
243
244 let mut map_v2 = AgentCommodityPortionsMap::new();
246 map_v2.insert(("commodity1".into(), 2020), 0.5);
247 let agent_commodities_v2 = HashMap::from([("agent1".into(), map_v2)]);
248 assert!(validate_agent_commodity_portions(
249 &agent_commodities_v2,
250 &agents,
251 &commodities,
252 ®ion_ids,
253 &milestone_years
254 )
255 .is_err());
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 costs: CommodityCostMap::new(),
266 demand: DemandMap::new(),
267 }),
268 );
269 assert!(validate_agent_commodity_portions(
270 &agent_commodity_portions,
271 &agents,
272 &commodities,
273 ®ion_ids,
274 &milestone_years
275 )
276 .is_err());
277 }
278}