1use crate::agent::AgentID;
3use crate::commodity::CommodityID;
4use crate::process::{Process, ProcessFlow, ProcessParameter};
5use crate::region::RegionID;
6use crate::time_slice::TimeSliceID;
7use crate::units::{Activity, Capacity};
8use anyhow::{ensure, Context, Result};
9use indexmap::IndexMap;
10use serde::{Deserialize, Serialize};
11use std::hash::{Hash, Hasher};
12use std::ops::{Deref, RangeInclusive};
13use std::rc::Rc;
14
15#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
17pub struct AssetID(u32);
18
19#[derive(Clone, Debug, PartialEq)]
21pub struct Asset {
22 pub id: Option<AssetID>,
24 pub agent_id: Option<AgentID>,
28 pub process: Rc<Process>,
30 pub process_parameter: Rc<ProcessParameter>,
32 pub region_id: RegionID,
34 pub capacity: Capacity,
36 pub commission_year: u32,
38}
39
40impl Asset {
41 pub fn new(
46 agent_id: Option<AgentID>,
47 process: Rc<Process>,
48 region_id: RegionID,
49 capacity: Capacity,
50 commission_year: u32,
51 ) -> Result<Self> {
52 ensure!(
53 process.regions.contains(®ion_id),
54 "Region {} is not one of the regions in which process {} operates",
55 region_id,
56 process.id
57 );
58
59 let process_parameter = process
60 .parameters
61 .get(&(region_id.clone(), commission_year))
62 .with_context(|| {
63 format!(
64 "Process {} does not operate in the year {}",
65 process.id, commission_year
66 )
67 })?
68 .clone();
69
70 check_capacity_valid_for_asset(capacity)?;
71
72 Ok(Self {
73 id: None,
74 agent_id,
75 process,
76 process_parameter,
77 region_id,
78 capacity,
79 commission_year,
80 })
81 }
82
83 pub fn decommission_year(&self) -> u32 {
85 self.commission_year + self.process_parameter.lifetime
86 }
87
88 pub fn get_activity_limits(&self, time_slice: &TimeSliceID) -> RangeInclusive<Activity> {
90 let limits = self
91 .process
92 .activity_limits
93 .get(&(
94 self.region_id.clone(),
95 self.commission_year,
96 time_slice.clone(),
97 ))
98 .unwrap();
99 let max_act = self.maximum_activity();
100
101 (max_act * *limits.start())..=(max_act * *limits.end())
103 }
104
105 pub fn maximum_activity(&self) -> Activity {
107 self.capacity * self.process_parameter.capacity_to_activity
108 }
109
110 pub fn get_flow(&self, commodity_id: &CommodityID) -> Option<&ProcessFlow> {
112 self.get_flows_map().get(commodity_id)
113 }
114
115 fn get_flows_map(&self) -> &IndexMap<CommodityID, ProcessFlow> {
117 self.process
118 .flows
119 .get(&(self.region_id.clone(), self.commission_year))
120 .unwrap()
121 }
122
123 pub fn iter_flows(&self) -> impl Iterator<Item = &ProcessFlow> {
125 self.get_flows_map().values()
126 }
127}
128
129pub fn check_capacity_valid_for_asset(capacity: Capacity) -> Result<()> {
131 ensure!(
132 capacity.is_finite() && capacity > Capacity(0.0),
133 "Capacity must be a finite, positive number"
134 );
135
136 Ok(())
137}
138
139#[derive(Clone, Debug)]
146pub struct AssetRef(Rc<Asset>);
147
148impl From<Rc<Asset>> for AssetRef {
149 fn from(value: Rc<Asset>) -> Self {
150 Self(value)
151 }
152}
153
154impl From<Asset> for AssetRef {
155 fn from(value: Asset) -> Self {
156 Self::from(Rc::new(value))
157 }
158}
159
160impl From<AssetRef> for Rc<Asset> {
161 fn from(value: AssetRef) -> Self {
162 value.0
163 }
164}
165
166impl Deref for AssetRef {
167 type Target = Asset;
168
169 fn deref(&self) -> &Self::Target {
170 &self.0
171 }
172}
173
174impl PartialEq for AssetRef {
175 fn eq(&self, other: &Self) -> bool {
176 if self.0.id.is_some() {
177 self.0.id == other.0.id
178 } else {
179 other.0.id.is_none()
180 && self.0.agent_id == other.0.agent_id
181 && Rc::ptr_eq(&self.0.process, &other.0.process)
182 && self.0.region_id == other.0.region_id
183 && self.0.commission_year == other.0.commission_year
184 }
185 }
186}
187
188impl Eq for AssetRef {}
189
190impl Hash for AssetRef {
191 fn hash<H: Hasher>(&self, state: &mut H) {
193 if let Some(id) = self.0.id {
194 id.hash(state);
195 } else {
196 self.0.agent_id.hash(state);
197 self.0.process.id.hash(state);
198 self.0.region_id.hash(state);
199 self.0.commission_year.hash(state);
200 }
201 }
202}
203
204impl PartialOrd for AssetRef {
205 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
206 Some(self.cmp(other))
207 }
208}
209
210impl Ord for AssetRef {
211 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
212 self.id.unwrap().cmp(&other.id.unwrap())
213 }
214}
215
216pub struct AssetPool {
218 active: Vec<AssetRef>,
220 future: Vec<Asset>,
222 next_id: u32,
224}
225
226impl AssetPool {
227 pub fn new(mut assets: Vec<Asset>) -> Self {
229 assets.sort_by(|a, b| a.commission_year.cmp(&b.commission_year));
231
232 Self {
233 active: Vec::new(),
234 future: assets,
235 next_id: 0,
236 }
237 }
238
239 pub fn as_slice(&self) -> &[AssetRef] {
241 &self.active
242 }
243
244 pub fn commission_new(&mut self, year: u32) {
246 let count = self
248 .future
249 .iter()
250 .take_while(|asset| asset.commission_year <= year)
251 .count();
252
253 for mut asset in self.future.drain(0..count) {
255 asset.id = Some(AssetID(self.next_id));
256 self.next_id += 1;
257 self.active.push(asset.into());
258 }
259 }
260
261 pub fn decommission_old(&mut self, year: u32) {
263 self.active.retain(|asset| asset.decommission_year() > year);
264 }
265
266 pub fn get(&self, id: AssetID) -> Option<&AssetRef> {
273 let idx = self
275 .active
276 .binary_search_by(|asset| asset.id.unwrap().cmp(&id))
277 .ok()?;
278
279 Some(&self.active[idx])
280 }
281
282 pub fn iter(&self) -> std::slice::Iter<AssetRef> {
284 self.active.iter()
285 }
286
287 pub fn replace_active_pool<I>(&mut self, assets: I)
289 where
290 I: IntoIterator<Item = Rc<Asset>>,
291 {
292 let new_pool = assets.into_iter().map(|mut asset| {
293 if asset.id.is_none() {
294 let asset = Rc::make_mut(&mut asset);
296 asset.id = Some(AssetID(self.next_id));
297 self.next_id += 1;
298 }
299
300 asset.into()
301 });
302
303 self.active.clear();
304 self.active.extend(new_pool);
305
306 self.active.sort();
308 }
309}
310
311pub trait AssetIterator<'a> {
313 fn filter_region(self, region_id: &'a RegionID) -> impl Iterator<Item = &'a AssetRef> + 'a;
315
316 fn flows_for_commodity(
318 self,
319 commodity_id: &'a CommodityID,
320 ) -> impl Iterator<Item = (&'a AssetRef, &'a ProcessFlow)> + 'a;
321}
322
323impl<'a, I> AssetIterator<'a> for I
324where
325 I: Iterator<Item = &'a AssetRef> + 'a,
326{
327 fn filter_region(self, region_id: &'a RegionID) -> impl Iterator<Item = &'a AssetRef> + 'a {
328 self.filter(move |asset| asset.region_id == *region_id)
329 }
330
331 fn flows_for_commodity(
332 self,
333 commodity_id: &'a CommodityID,
334 ) -> impl Iterator<Item = (&'a AssetRef, &'a ProcessFlow)> + 'a {
335 self.filter_map(|asset| Some((asset, asset.get_flow(commodity_id)?)))
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342 use crate::fixture::{assert_error, process};
343 use crate::process::{
344 Process, ProcessActivityLimitsMap, ProcessFlowsMap, ProcessParameter, ProcessParameterMap,
345 };
346 use crate::units::{
347 ActivityPerCapacity, Dimensionless, MoneyPerActivity, MoneyPerCapacity,
348 MoneyPerCapacityPerYear,
349 };
350 use indexmap::IndexSet;
351 use itertools::{assert_equal, Itertools};
352 use rstest::{fixture, rstest};
353 use std::iter;
354 use std::ops::RangeInclusive;
355
356 #[rstest]
357 #[case(Capacity(0.01))]
358 #[case(Capacity(0.5))]
359 #[case(Capacity(1.0))]
360 #[case(Capacity(100.0))]
361 fn test_asset_new_valid(process: Process, #[case] capacity: Capacity) {
362 let agent_id = Some(AgentID("agent1".into()));
363 let region_id = RegionID("GBR".into());
364 let asset = Asset::new(agent_id, process.into(), region_id, capacity, 2015).unwrap();
365 assert!(asset.id.is_none());
366 }
367
368 #[rstest]
369 #[case(Capacity(0.0))]
370 #[case(Capacity(-0.01))]
371 #[case(Capacity(-1.0))]
372 #[case(Capacity(f64::NAN))]
373 #[case(Capacity(f64::INFINITY))]
374 #[case(Capacity(f64::NEG_INFINITY))]
375 fn test_asset_new_invalid_capacity(process: Process, #[case] capacity: Capacity) {
376 let agent_id = Some(AgentID("agent1".into()));
377 let region_id = RegionID("GBR".into());
378 assert_error!(
379 Asset::new(agent_id, process.into(), region_id, capacity, 2015),
380 "Capacity must be a finite, positive number"
381 );
382 }
383
384 #[rstest]
385 fn test_asset_new_invalid_commission_year(process: Process) {
386 let agent_id = Some(AgentID("agent1".into()));
387 let region_id = RegionID("GBR".into());
388 assert_error!(
389 Asset::new(agent_id, process.into(), region_id, Capacity(1.0), 2009),
390 "Process process1 does not operate in the year 2009"
391 );
392 }
393
394 #[rstest]
395 fn test_asset_new_invalid_region(process: Process) {
396 let agent_id = Some(AgentID("agent1".into()));
397 let region_id = RegionID("FRA".into());
398 assert_error!(
399 Asset::new(agent_id, process.into(), region_id, Capacity(1.0), 2015),
400 "Region FRA is not one of the regions in which process process1 operates"
401 );
402 }
403
404 #[fixture]
405 fn asset_pool() -> AssetPool {
406 let process_param = Rc::new(ProcessParameter {
407 capital_cost: MoneyPerCapacity(5.0),
408 fixed_operating_cost: MoneyPerCapacityPerYear(2.0),
409 variable_operating_cost: MoneyPerActivity(1.0),
410 lifetime: 5,
411 discount_rate: Dimensionless(0.9),
412 capacity_to_activity: ActivityPerCapacity(1.0),
413 });
414 let years = RangeInclusive::new(2010, 2020).collect_vec();
415 let process_parameter_map: ProcessParameterMap = years
416 .iter()
417 .map(|&year| (("GBR".into(), year), process_param.clone()))
418 .collect();
419 let process = Rc::new(Process {
420 id: "process1".into(),
421 description: "Description".into(),
422 years: vec![2010, 2020],
423 activity_limits: ProcessActivityLimitsMap::new(),
424 flows: ProcessFlowsMap::new(),
425 parameters: process_parameter_map,
426 regions: IndexSet::from(["GBR".into()]),
427 });
428 let future = [2020, 2010]
429 .map(|year| {
430 Asset::new(
431 Some("agent1".into()),
432 Rc::clone(&process),
433 "GBR".into(),
434 Capacity(1.0),
435 year,
436 )
437 .unwrap()
438 })
439 .into_iter()
440 .collect_vec();
441
442 AssetPool::new(future)
443 }
444
445 #[test]
446 fn test_asset_get_activity_limits() {
447 let time_slice = TimeSliceID {
448 season: "winter".into(),
449 time_of_day: "day".into(),
450 };
451 let process_param = Rc::new(ProcessParameter {
452 capital_cost: MoneyPerCapacity(5.0),
453 fixed_operating_cost: MoneyPerCapacityPerYear(2.0),
454 variable_operating_cost: MoneyPerActivity(1.0),
455 lifetime: 5,
456 discount_rate: Dimensionless(0.9),
457 capacity_to_activity: ActivityPerCapacity(3.0),
458 });
459 let years = RangeInclusive::new(2010, 2020).collect_vec();
460 let process_parameter_map: ProcessParameterMap = years
461 .iter()
462 .map(|&year| (("GBR".into(), year), process_param.clone()))
463 .collect();
464 let fraction_limits = Dimensionless(1.0)..=Dimensionless(f64::INFINITY);
465 let mut activity_limits = ProcessActivityLimitsMap::new();
466 for year in [2010, 2020] {
467 activity_limits.insert(
468 ("GBR".into(), year, time_slice.clone()),
469 fraction_limits.clone(),
470 );
471 }
472 let process = Rc::new(Process {
473 id: "process1".into(),
474 description: "Description".into(),
475 years: vec![2010, 2020],
476 activity_limits,
477 flows: ProcessFlowsMap::new(),
478 parameters: process_parameter_map,
479 regions: IndexSet::from(["GBR".into()]),
480 });
481 let asset = Asset::new(
482 Some("agent1".into()),
483 Rc::clone(&process),
484 "GBR".into(),
485 Capacity(2.0),
486 2010,
487 )
488 .unwrap();
489
490 assert_eq!(
491 asset.get_activity_limits(&time_slice),
492 Activity(6.0)..=Activity(f64::INFINITY)
493 );
494 }
495
496 #[rstest]
497 fn test_asset_pool_new(asset_pool: AssetPool) {
498 assert!(asset_pool.active.is_empty());
500 assert!(asset_pool.future.len() == 2);
501 assert!(asset_pool.future[0].commission_year == 2010);
502 assert!(asset_pool.future[1].commission_year == 2020);
503 }
504
505 #[rstest]
506 fn test_asset_pool_commission_new1(mut asset_pool: AssetPool) {
507 asset_pool.commission_new(2010);
509 assert_equal(asset_pool.iter(), iter::once(&asset_pool.active[0]));
510 }
511
512 #[rstest]
513 fn test_asset_pool_commission_new2(mut asset_pool: AssetPool) {
514 asset_pool.commission_new(2011);
516 assert_equal(asset_pool.iter(), iter::once(&asset_pool.active[0]));
517 }
518
519 #[rstest]
520 fn test_asset_pool_commission_new3(mut asset_pool: AssetPool) {
521 asset_pool.commission_new(2000);
523 assert!(asset_pool.iter().next().is_none()); }
525
526 #[rstest]
527 fn test_asset_pool_decommission_old(mut asset_pool: AssetPool) {
528 asset_pool.commission_new(2020);
529 assert_eq!(asset_pool.active.len(), 2);
530 asset_pool.decommission_old(2020); assert_eq!(asset_pool.active.len(), 1);
532 assert_eq!(asset_pool.active[0].commission_year, 2020);
533 asset_pool.decommission_old(2022); assert_eq!(asset_pool.active.len(), 1);
535 assert_eq!(asset_pool.active[0].commission_year, 2020);
536 asset_pool.decommission_old(2025); assert!(asset_pool.active.is_empty());
538 }
539
540 #[rstest]
541 fn test_asset_pool_get(mut asset_pool: AssetPool) {
542 asset_pool.commission_new(2020);
543 assert_eq!(asset_pool.get(AssetID(0)), Some(&asset_pool.active[0]));
544 assert_eq!(asset_pool.get(AssetID(1)), Some(&asset_pool.active[1]));
545 }
546
547 #[rstest]
548 fn test_asset_pool_replace_active_pool_existing(mut asset_pool: AssetPool) {
549 asset_pool.commission_new(2020);
550 assert_eq!(asset_pool.active.len(), 2);
551 asset_pool.replace_active_pool(iter::once(asset_pool.active[1].clone().into()));
552 assert_eq!(asset_pool.active.len(), 1);
553 assert_eq!(asset_pool.active[0].id, Some(AssetID(1)));
554 }
555
556 #[rstest]
557 fn test_asset_pool_replace_active_pool_new_asset(mut asset_pool: AssetPool, process: Process) {
558 let asset = Asset::new(
559 Some("some_other_agent".into()),
560 process.into(),
561 "GBR".into(),
562 Capacity(2.0),
563 2010,
564 )
565 .unwrap();
566
567 asset_pool.commission_new(2020);
568 assert_eq!(asset_pool.active.len(), 2);
569 asset_pool.replace_active_pool(iter::once(asset.into()));
570 assert_eq!(asset_pool.active.len(), 1);
571 assert_eq!(asset_pool.active[0].id, Some(AssetID(2)));
572 assert_eq!(
573 asset_pool.active[0].agent_id,
574 Some("some_other_agent".into())
575 );
576 }
577
578 #[rstest]
579 fn test_asset_pool_replace_active_pool_out_of_order(
580 mut asset_pool: AssetPool,
581 process: Process,
582 ) {
583 let new_asset = Asset::new(
584 Some("some_other_agent".into()),
585 process.into(),
586 "GBR".into(),
587 Capacity(2.0),
588 2010,
589 )
590 .unwrap();
591
592 asset_pool.commission_new(2020);
593 assert_eq!(asset_pool.active.len(), 2);
594 let mut new_pool: Vec<Rc<Asset>> = asset_pool
595 .iter()
596 .map(|asset| asset.clone().into())
597 .collect();
598 new_pool.push(new_asset.into());
599 new_pool.reverse();
600
601 asset_pool.replace_active_pool(new_pool);
602 assert_equal(asset_pool.iter().map(|asset| asset.id.unwrap().0), 0..3);
603 assert_eq!(asset_pool.active[2].id, Some(AssetID(2)));
604 assert_eq!(
605 asset_pool.active[2].agent_id,
606 Some("some_other_agent".into())
607 );
608 }
609}