1use crate::agent::AgentID;
3use crate::commodity::Commodity;
4use crate::process::{Process, ProcessParameter};
5use crate::region::RegionID;
6use crate::time_slice::TimeSliceID;
7use anyhow::{ensure, Context, Result};
8use std::collections::HashSet;
9use std::ops::RangeInclusive;
10use std::rc::Rc;
11
12#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub struct AssetID(u32);
15
16impl AssetID {
17 pub const INVALID: AssetID = AssetID(u32::MAX);
19}
20
21#[derive(Clone, Debug, PartialEq)]
23pub struct Asset {
24 pub id: AssetID,
26 pub agent_id: AgentID,
28 pub process: Rc<Process>,
30 pub process_parameter: Rc<ProcessParameter>,
32 pub region_id: RegionID,
34 pub capacity: f64,
36 pub commission_year: u32,
38}
39
40impl Asset {
41 pub fn new(
46 agent_id: AgentID,
47 process: Rc<Process>,
48 region_id: RegionID,
49 capacity: f64,
50 commission_year: u32,
51 ) -> Result<Self> {
52 ensure!(commission_year > 0, "Commission year must be > 0");
53 ensure!(
54 process.regions.contains(®ion_id),
55 "Region {} is not one of the regions in which process {} operates",
56 region_id,
57 process.id
58 );
59
60 let process_parameter = process
61 .parameters
62 .get(&(region_id.clone(), commission_year))
63 .with_context(|| {
64 format!(
65 "Process {} does not operate in the year {}",
66 process.id, commission_year
67 )
68 })?
69 .clone();
70
71 Ok(Self {
72 id: AssetID::INVALID,
73 agent_id,
74 process,
75 process_parameter,
76 region_id,
77 capacity,
78 commission_year,
79 })
80 }
81
82 pub fn decommission_year(&self) -> u32 {
84 self.commission_year + self.process_parameter.lifetime
85 }
86
87 pub fn get_energy_limits(&self, time_slice: &TimeSliceID) -> RangeInclusive<f64> {
91 let limits = self
92 .process
93 .energy_limits
94 .get(&(
95 self.region_id.clone(),
96 self.commission_year,
97 time_slice.clone(),
98 ))
99 .unwrap();
100 let max_act = self.maximum_activity();
101
102 (max_act * limits.start())..=(max_act * limits.end())
104 }
105
106 pub fn maximum_activity(&self) -> f64 {
108 self.capacity * self.process_parameter.capacity_to_activity
109 }
110}
111
112pub struct AssetPool {
114 assets: Vec<Asset>,
118 current_year: u32,
120}
121
122impl AssetPool {
123 pub fn new(mut assets: Vec<Asset>) -> Self {
125 assets.sort_by(|a, b| a.commission_year.cmp(&b.commission_year));
127
128 for (id, asset) in assets.iter_mut().enumerate() {
130 asset.id = AssetID(id as u32);
131 }
132
133 Self {
134 assets,
135 current_year: 0,
136 }
137 }
138
139 pub fn commission_new(&mut self, year: u32) {
141 assert!(
142 year >= self.current_year,
143 "Assets have already been commissioned for year {year}"
144 );
145 self.current_year = year;
146 }
147
148 pub fn decomission_old(&mut self, year: u32) {
150 assert!(
151 year >= self.current_year,
152 "Cannot decommission assets in the past (current year: {})",
153 self.current_year
154 );
155 self.assets.retain(|asset| asset.decommission_year() > year);
156 }
157
158 pub fn get(&self, id: AssetID) -> Option<&Asset> {
165 let idx = self
167 .assets
168 .binary_search_by(|asset| asset.id.cmp(&id))
169 .ok()?;
170
171 Some(&self.assets[idx])
172 }
173
174 pub fn iter(&self) -> impl Iterator<Item = &Asset> {
176 self.assets
177 .iter()
178 .take_while(|asset| asset.commission_year <= self.current_year)
179 }
180
181 pub fn iter_for_region<'a>(
183 &'a self,
184 region_id: &'a RegionID,
185 ) -> impl Iterator<Item = &'a Asset> {
186 self.iter().filter(|asset| asset.region_id == *region_id)
187 }
188
189 pub fn iter_for_region_and_commodity<'a>(
192 &'a self,
193 region_id: &'a RegionID,
194 commodity: &'a Rc<Commodity>,
195 ) -> impl Iterator<Item = &'a Asset> {
196 self.iter_for_region(region_id)
197 .filter(|asset| asset.process.contains_commodity_flow(commodity))
198 }
199
200 pub fn retain(&mut self, assets_to_keep: &HashSet<AssetID>) {
205 debug_assert!(
208 assets_to_keep.iter().all(|id| self.get(*id).is_some()),
209 "One or more asset IDs were invalid"
210 );
211
212 self.assets.retain(|asset| {
213 assets_to_keep.contains(&asset.id) || asset.commission_year > self.current_year
214 });
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use crate::commodity::{CommodityCostMap, CommodityType, DemandMap};
222 use crate::fixture::{assert_error, process};
223 use crate::process::{
224 FlowType, Process, ProcessEnergyLimitsMap, ProcessFlow, ProcessParameter,
225 ProcessParameterMap,
226 };
227 use crate::time_slice::TimeSliceLevel;
228 use itertools::{assert_equal, Itertools};
229 use rstest::{fixture, rstest};
230 use std::iter;
231 use std::ops::RangeInclusive;
232
233 #[rstest]
234 fn test_asset_new_valid(process: Process) {
235 let agent_id = AgentID("agent1".into());
236 let region_id = RegionID("GBR".into());
237 let asset = Asset::new(agent_id, process.into(), region_id, 1.0, 2015).unwrap();
238 assert!(asset.id == AssetID::INVALID);
239 }
240
241 #[rstest]
242 fn test_asset_new_invalid_commission_year_zero(process: Process) {
243 let agent_id = AgentID("agent1".into());
244 let region_id = RegionID("GBR".into());
245 assert_error!(
246 Asset::new(agent_id, process.into(), region_id, 1.0, 0),
247 "Commission year must be > 0"
248 );
249 }
250
251 #[rstest]
252 fn test_asset_new_invalid_commission_year(process: Process) {
253 let agent_id = AgentID("agent1".into());
254 let region_id = RegionID("GBR".into());
255 assert_error!(
256 Asset::new(agent_id, process.into(), region_id, 1.0, 2009),
257 "Process process1 does not operate in the year 2009"
258 );
259 }
260
261 #[rstest]
262 fn test_asset_new_invalid_region(process: Process) {
263 let agent_id = AgentID("agent1".into());
264 let region_id = RegionID("FRA".into());
265 assert_error!(
266 Asset::new(agent_id, process.into(), region_id, 1.0, 2015),
267 "Region FRA is not one of the regions in which process process1 operates"
268 );
269 }
270
271 #[fixture]
272 fn asset_pool() -> AssetPool {
273 let process_param = Rc::new(ProcessParameter {
274 capital_cost: 5.0,
275 fixed_operating_cost: 2.0,
276 variable_operating_cost: 1.0,
277 lifetime: 5,
278 discount_rate: 0.9,
279 capacity_to_activity: 1.0,
280 });
281 let years = RangeInclusive::new(2010, 2020).collect_vec();
282 let process_parameter_map: ProcessParameterMap = years
283 .iter()
284 .map(|&year| (("GBR".into(), year), process_param.clone()))
285 .collect();
286 let process = Rc::new(Process {
287 id: "process1".into(),
288 description: "Description".into(),
289 years: 2010..=2020,
290 energy_limits: ProcessEnergyLimitsMap::new(),
291 flows: vec![],
292 parameters: process_parameter_map,
293 regions: HashSet::from(["GBR".into()]),
294 });
295 let future = [2020, 2010]
296 .map(|year| {
297 Asset::new(
298 "agent1".into(),
299 Rc::clone(&process),
300 "GBR".into(),
301 1.0,
302 year,
303 )
304 .unwrap()
305 })
306 .into_iter()
307 .collect_vec();
308
309 AssetPool::new(future)
310 }
311
312 #[test]
313 fn test_asset_get_energy_limits() {
314 let time_slice = TimeSliceID {
315 season: "winter".into(),
316 time_of_day: "day".into(),
317 };
318 let process_param = Rc::new(ProcessParameter {
319 capital_cost: 5.0,
320 fixed_operating_cost: 2.0,
321 variable_operating_cost: 1.0,
322 lifetime: 5,
323 discount_rate: 0.9,
324 capacity_to_activity: 3.0,
325 });
326 let years = RangeInclusive::new(2010, 2020).collect_vec();
327 let process_parameter_map: ProcessParameterMap = years
328 .iter()
329 .map(|&year| (("GBR".into(), year), process_param.clone()))
330 .collect();
331 let commodity = Rc::new(Commodity {
332 id: "commodity1".into(),
333 description: "Some description".into(),
334 kind: CommodityType::InputCommodity,
335 time_slice_level: TimeSliceLevel::Annual,
336 costs: CommodityCostMap::new(),
337 demand: DemandMap::new(),
338 });
339 let flow = ProcessFlow {
340 process_id: "id1".into(),
341 commodity: Rc::clone(&commodity),
342 flow: 1.0,
343 flow_type: FlowType::Fixed,
344 flow_cost: 1.0,
345 is_pac: true,
346 };
347 let fraction_limits = 1.0..=f64::INFINITY;
348 let mut energy_limits = ProcessEnergyLimitsMap::new();
349 for year in [2010, 2020] {
350 energy_limits.insert(
351 ("GBR".into(), year, time_slice.clone()),
352 fraction_limits.clone(),
353 );
354 }
355 let process = Rc::new(Process {
356 id: "process1".into(),
357 description: "Description".into(),
358 years: 2010..=2020,
359 energy_limits,
360 flows: vec![flow.clone()],
361 parameters: process_parameter_map,
362 regions: HashSet::from(["GBR".into()]),
363 });
364 let asset = Asset::new(
365 "agent1".into(),
366 Rc::clone(&process),
367 "GBR".into(),
368 2.0,
369 2010,
370 )
371 .unwrap();
372
373 assert_eq!(asset.get_energy_limits(&time_slice), 6.0..=f64::INFINITY);
374 }
375
376 #[rstest]
377 fn test_asset_pool_new(asset_pool: AssetPool) {
378 assert!(asset_pool.current_year == 0);
379
380 assert!(asset_pool.assets.len() == 2);
382 assert!(asset_pool.assets[0].commission_year == 2010);
383 assert!(asset_pool.assets[1].commission_year == 2020);
384 }
385
386 #[rstest]
387 fn test_asset_pool_commission_new1(mut asset_pool: AssetPool) {
388 asset_pool.commission_new(2010);
390 assert!(asset_pool.current_year == 2010);
391 assert_equal(asset_pool.iter(), iter::once(&asset_pool.assets[0]));
392 }
393
394 #[rstest]
395 fn test_asset_pool_commission_new2(mut asset_pool: AssetPool) {
396 asset_pool.commission_new(2011);
398 assert!(asset_pool.current_year == 2011);
399 assert_equal(asset_pool.iter(), iter::once(&asset_pool.assets[0]));
400 }
401
402 #[rstest]
403 fn test_asset_pool_commission_new3(mut asset_pool: AssetPool) {
404 asset_pool.commission_new(2000);
406 assert!(asset_pool.current_year == 2000);
407 assert!(asset_pool.iter().next().is_none()); }
409
410 #[rstest]
411 fn test_asset_pool_decommission_old(mut asset_pool: AssetPool) {
412 let asset_pool2 = asset_pool.assets.clone();
413
414 asset_pool.commission_new(2020);
415 assert!(asset_pool.assets.len() == 2);
416 asset_pool.decomission_old(2020); assert_equal(&asset_pool.assets, iter::once(&asset_pool2[1]));
418 asset_pool.decomission_old(2022); assert_equal(&asset_pool.assets, iter::once(&asset_pool2[1]));
420 asset_pool.decomission_old(2025); assert!(asset_pool.assets.is_empty());
422 }
423
424 #[rstest]
425 fn test_asset_pool_get(mut asset_pool: AssetPool) {
426 asset_pool.commission_new(2020);
427 assert!(asset_pool.get(AssetID(0)) == Some(&asset_pool.assets[0]));
428 assert!(asset_pool.get(AssetID(1)) == Some(&asset_pool.assets[1]));
429 }
430
431 #[rstest]
432 fn test_asset_pool_retain1(mut asset_pool: AssetPool) {
433 asset_pool.retain(&HashSet::new());
436 assert_eq!(asset_pool.assets.len(), 2);
437
438 asset_pool.commission_new(2010); asset_pool.retain(&HashSet::new());
441 assert_eq!(asset_pool.assets.len(), 1);
442 assert_eq!(asset_pool.assets[0].id, AssetID(1));
443 }
444
445 #[rstest]
446 fn test_asset_pool_retain2(mut asset_pool: AssetPool) {
447 asset_pool.commission_new(2020); asset_pool.retain(&iter::once(AssetID(1)).collect());
450 assert_eq!(asset_pool.assets.len(), 1);
451 assert_eq!(asset_pool.assets[0].id, AssetID(1));
452 }
453}