muse2/
asset.rs

1//! Assets are instances of a process which are owned and invested in by agents.
2use 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/// A unique identifier for an asset
16#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
17pub struct AssetID(u32);
18
19/// An asset controlled by an agent.
20#[derive(Clone, Debug, PartialEq)]
21pub struct Asset {
22    /// A unique identifier for the asset
23    pub id: Option<AssetID>,
24    /// A unique identifier for the agent
25    pub agent_id: AgentID,
26    /// The [`Process`] that this asset corresponds to
27    pub process: Rc<Process>,
28    /// The [`ProcessParameter`] corresponding to the asset's region and commission year
29    pub process_parameter: Rc<ProcessParameter>,
30    /// The region in which the asset is located
31    pub region_id: RegionID,
32    /// Capacity of asset
33    pub capacity: Capacity,
34    /// The year the asset comes online
35    pub commission_year: u32,
36}
37
38impl Asset {
39    /// Create a new [`Asset`].
40    ///
41    /// The `id` field is initially set to `None`, but is changed to a unique value when the asset
42    /// is stored in an [`AssetPool`].
43    pub fn new(
44        agent_id: AgentID,
45        process: Rc<Process>,
46        region_id: RegionID,
47        capacity: Capacity,
48        commission_year: u32,
49    ) -> Result<Self> {
50        ensure!(
51            process.regions.contains(&region_id),
52            "Region {} is not one of the regions in which process {} operates",
53            region_id,
54            process.id
55        );
56
57        let process_parameter = process
58            .parameters
59            .get(&(region_id.clone(), commission_year))
60            .with_context(|| {
61                format!(
62                    "Process {} does not operate in the year {}",
63                    process.id, commission_year
64                )
65            })?
66            .clone();
67
68        ensure!(
69            capacity.is_finite() && capacity > Capacity(0.0),
70            "Capacity must be a finite, positive number"
71        );
72
73        Ok(Self {
74            id: None,
75            agent_id,
76            process,
77            process_parameter,
78            region_id,
79            capacity,
80            commission_year,
81        })
82    }
83
84    /// The last year in which this asset should be decommissioned
85    pub fn decommission_year(&self) -> u32 {
86        self.commission_year + self.process_parameter.lifetime
87    }
88
89    /// Get the activity limits for this asset in a particular time slice
90    pub fn get_activity_limits(&self, time_slice: &TimeSliceID) -> RangeInclusive<Activity> {
91        let limits = self
92            .process
93            .activity_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        // limits in real units (which are user defined)
103        (max_act * *limits.start())..=(max_act * *limits.end())
104    }
105
106    /// Maximum activity for this asset
107    pub fn maximum_activity(&self) -> Activity {
108        self.capacity * self.process_parameter.capacity_to_activity
109    }
110
111    /// Get a specific process flow
112    pub fn get_flow(&self, commodity_id: &CommodityID) -> Option<&ProcessFlow> {
113        self.get_flows_map().get(commodity_id)
114    }
115
116    /// Get the process flows map for this asset
117    fn get_flows_map(&self) -> &IndexMap<CommodityID, ProcessFlow> {
118        self.process
119            .flows
120            .get(&(self.region_id.clone(), self.commission_year))
121            .unwrap()
122    }
123
124    /// Iterate over the asset's flows
125    pub fn iter_flows(&self) -> impl Iterator<Item = &ProcessFlow> {
126        self.get_flows_map().values()
127    }
128}
129
130/// A wrapper around [`Asset`] for storing references in maps.
131///
132/// If the asset has been commissioned, then comparison and hashing is done based on the asset ID,
133/// otherwise a combination of other parameters is used.
134///
135/// [`Ord`] is implemented for [`AssetRef`], but it will panic for non-commissioned assets.
136#[derive(Clone, Debug)]
137pub struct AssetRef(Rc<Asset>);
138
139impl From<Rc<Asset>> for AssetRef {
140    fn from(value: Rc<Asset>) -> Self {
141        Self(value)
142    }
143}
144
145impl From<Asset> for AssetRef {
146    fn from(value: Asset) -> Self {
147        Self::from(Rc::new(value))
148    }
149}
150
151impl From<AssetRef> for Rc<Asset> {
152    fn from(value: AssetRef) -> Self {
153        value.0
154    }
155}
156
157impl Deref for AssetRef {
158    type Target = Asset;
159
160    fn deref(&self) -> &Self::Target {
161        &self.0
162    }
163}
164
165impl PartialEq for AssetRef {
166    fn eq(&self, other: &Self) -> bool {
167        if self.0.id.is_some() {
168            self.0.id == other.0.id
169        } else {
170            other.0.id.is_none()
171                && self.0.agent_id == other.0.agent_id
172                && Rc::ptr_eq(&self.0.process, &other.0.process)
173                && self.0.region_id == other.0.region_id
174                && self.0.commission_year == other.0.commission_year
175        }
176    }
177}
178
179impl Eq for AssetRef {}
180
181impl Hash for AssetRef {
182    /// Hash asset based purely on its ID
183    fn hash<H: Hasher>(&self, state: &mut H) {
184        if let Some(id) = self.0.id {
185            id.hash(state);
186        } else {
187            self.0.agent_id.hash(state);
188            self.0.process.id.hash(state);
189            self.0.region_id.hash(state);
190            self.0.commission_year.hash(state);
191        }
192    }
193}
194
195impl PartialOrd for AssetRef {
196    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
197        Some(self.cmp(other))
198    }
199}
200
201impl Ord for AssetRef {
202    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
203        self.id.unwrap().cmp(&other.id.unwrap())
204    }
205}
206
207/// A pool of [`Asset`]s
208pub struct AssetPool {
209    /// The pool of active assets
210    active: Vec<AssetRef>,
211    /// Assets that have not yet been commissioned, sorted by commission year
212    future: Vec<Asset>,
213    /// Next available asset ID number
214    next_id: u32,
215}
216
217impl AssetPool {
218    /// Create a new [`AssetPool`]
219    pub fn new(mut assets: Vec<Asset>) -> Self {
220        // Sort in order of commission year
221        assets.sort_by(|a, b| a.commission_year.cmp(&b.commission_year));
222
223        Self {
224            active: Vec::new(),
225            future: assets,
226            next_id: 0,
227        }
228    }
229
230    /// Get the active pool as a slice of [`AssetRef`]s
231    pub fn as_slice(&self) -> &[AssetRef] {
232        &self.active
233    }
234
235    /// Commission new assets for the specified milestone year from the input data
236    pub fn commission_new(&mut self, year: u32) {
237        // Count the number of assets to move
238        let count = self
239            .future
240            .iter()
241            .take_while(|asset| asset.commission_year <= year)
242            .count();
243
244        // Move assets from future to active
245        for mut asset in self.future.drain(0..count) {
246            asset.id = Some(AssetID(self.next_id));
247            self.next_id += 1;
248            self.active.push(asset.into());
249        }
250    }
251
252    /// Decommission old assets for the specified milestone year
253    pub fn decommission_old(&mut self, year: u32) {
254        self.active.retain(|asset| asset.decommission_year() > year);
255    }
256
257    /// Get an asset with the specified ID.
258    ///
259    /// # Returns
260    ///
261    /// An [`AssetRef`] if found, else `None`. The asset may not be found if it has already been
262    /// decommissioned.
263    pub fn get(&self, id: AssetID) -> Option<&AssetRef> {
264        // The assets in `active` are in order of ID
265        let idx = self
266            .active
267            .binary_search_by(|asset| asset.id.unwrap().cmp(&id))
268            .ok()?;
269
270        Some(&self.active[idx])
271    }
272
273    /// Iterate over active assets
274    pub fn iter(&self) -> std::slice::Iter<AssetRef> {
275        self.active.iter()
276    }
277
278    /// Replace the active pool with new and/or already commissioned assets
279    pub fn replace_active_pool<I>(&mut self, assets: I)
280    where
281        I: IntoIterator<Item = Rc<Asset>>,
282    {
283        let new_pool = assets.into_iter().map(|mut asset| {
284            if asset.id.is_none() {
285                // Asset is newly created from process so we need to assign an ID
286                let asset = Rc::make_mut(&mut asset);
287                asset.id = Some(AssetID(self.next_id));
288                self.next_id += 1;
289            }
290
291            asset.into()
292        });
293
294        self.active.clear();
295        self.active.extend(new_pool);
296
297        // New pool may not have been sorted, but active needs to be sorted by ID
298        self.active.sort();
299    }
300}
301
302/// Additional methods for iterating over assets
303pub trait AssetIterator<'a> {
304    /// Filter the assets by region
305    fn filter_region(self, region_id: &'a RegionID) -> impl Iterator<Item = &'a AssetRef> + 'a;
306
307    /// Iterate over process flows affecting the given commodity
308    fn flows_for_commodity(
309        self,
310        commodity_id: &'a CommodityID,
311    ) -> impl Iterator<Item = (&'a AssetRef, &'a ProcessFlow)> + 'a;
312}
313
314impl<'a, I> AssetIterator<'a> for I
315where
316    I: Iterator<Item = &'a AssetRef> + 'a,
317{
318    fn filter_region(self, region_id: &'a RegionID) -> impl Iterator<Item = &'a AssetRef> + 'a {
319        self.filter(move |asset| asset.region_id == *region_id)
320    }
321
322    fn flows_for_commodity(
323        self,
324        commodity_id: &'a CommodityID,
325    ) -> impl Iterator<Item = (&'a AssetRef, &'a ProcessFlow)> + 'a {
326        self.filter_map(|asset| Some((asset, asset.get_flow(commodity_id)?)))
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333    use crate::fixture::{assert_error, process};
334    use crate::process::{
335        Process, ProcessActivityLimitsMap, ProcessFlowsMap, ProcessParameter, ProcessParameterMap,
336    };
337    use crate::units::{
338        ActivityPerCapacity, Dimensionless, MoneyPerActivity, MoneyPerCapacity,
339        MoneyPerCapacityPerYear,
340    };
341    use indexmap::IndexSet;
342    use itertools::{assert_equal, Itertools};
343    use rstest::{fixture, rstest};
344    use std::iter;
345    use std::ops::RangeInclusive;
346
347    #[rstest]
348    #[case(Capacity(0.01))]
349    #[case(Capacity(0.5))]
350    #[case(Capacity(1.0))]
351    #[case(Capacity(100.0))]
352    fn test_asset_new_valid(process: Process, #[case] capacity: Capacity) {
353        let agent_id = AgentID("agent1".into());
354        let region_id = RegionID("GBR".into());
355        let asset = Asset::new(agent_id, process.into(), region_id, capacity, 2015).unwrap();
356        assert!(asset.id.is_none());
357    }
358
359    #[rstest]
360    #[case(Capacity(0.0))]
361    #[case(Capacity(-0.01))]
362    #[case(Capacity(-1.0))]
363    #[case(Capacity(f64::NAN))]
364    #[case(Capacity(f64::INFINITY))]
365    #[case(Capacity(f64::NEG_INFINITY))]
366    fn test_asset_new_invalid_capacity(process: Process, #[case] capacity: Capacity) {
367        let agent_id = AgentID("agent1".into());
368        let region_id = RegionID("GBR".into());
369        assert_error!(
370            Asset::new(agent_id, process.into(), region_id, capacity, 2015),
371            "Capacity must be a finite, positive number"
372        );
373    }
374
375    #[rstest]
376    fn test_asset_new_invalid_commission_year(process: Process) {
377        let agent_id = AgentID("agent1".into());
378        let region_id = RegionID("GBR".into());
379        assert_error!(
380            Asset::new(agent_id, process.into(), region_id, Capacity(1.0), 2009),
381            "Process process1 does not operate in the year 2009"
382        );
383    }
384
385    #[rstest]
386    fn test_asset_new_invalid_region(process: Process) {
387        let agent_id = AgentID("agent1".into());
388        let region_id = RegionID("FRA".into());
389        assert_error!(
390            Asset::new(agent_id, process.into(), region_id, Capacity(1.0), 2015),
391            "Region FRA is not one of the regions in which process process1 operates"
392        );
393    }
394
395    #[fixture]
396    fn asset_pool() -> AssetPool {
397        let process_param = Rc::new(ProcessParameter {
398            capital_cost: MoneyPerCapacity(5.0),
399            fixed_operating_cost: MoneyPerCapacityPerYear(2.0),
400            variable_operating_cost: MoneyPerActivity(1.0),
401            lifetime: 5,
402            discount_rate: Dimensionless(0.9),
403            capacity_to_activity: ActivityPerCapacity(1.0),
404        });
405        let years = RangeInclusive::new(2010, 2020).collect_vec();
406        let process_parameter_map: ProcessParameterMap = years
407            .iter()
408            .map(|&year| (("GBR".into(), year), process_param.clone()))
409            .collect();
410        let process = Rc::new(Process {
411            id: "process1".into(),
412            description: "Description".into(),
413            years: vec![2010, 2020],
414            activity_limits: ProcessActivityLimitsMap::new(),
415            flows: ProcessFlowsMap::new(),
416            parameters: process_parameter_map,
417            regions: IndexSet::from(["GBR".into()]),
418        });
419        let future = [2020, 2010]
420            .map(|year| {
421                Asset::new(
422                    "agent1".into(),
423                    Rc::clone(&process),
424                    "GBR".into(),
425                    Capacity(1.0),
426                    year,
427                )
428                .unwrap()
429            })
430            .into_iter()
431            .collect_vec();
432
433        AssetPool::new(future)
434    }
435
436    #[test]
437    fn test_asset_get_activity_limits() {
438        let time_slice = TimeSliceID {
439            season: "winter".into(),
440            time_of_day: "day".into(),
441        };
442        let process_param = Rc::new(ProcessParameter {
443            capital_cost: MoneyPerCapacity(5.0),
444            fixed_operating_cost: MoneyPerCapacityPerYear(2.0),
445            variable_operating_cost: MoneyPerActivity(1.0),
446            lifetime: 5,
447            discount_rate: Dimensionless(0.9),
448            capacity_to_activity: ActivityPerCapacity(3.0),
449        });
450        let years = RangeInclusive::new(2010, 2020).collect_vec();
451        let process_parameter_map: ProcessParameterMap = years
452            .iter()
453            .map(|&year| (("GBR".into(), year), process_param.clone()))
454            .collect();
455        let fraction_limits = Dimensionless(1.0)..=Dimensionless(f64::INFINITY);
456        let mut activity_limits = ProcessActivityLimitsMap::new();
457        for year in [2010, 2020] {
458            activity_limits.insert(
459                ("GBR".into(), year, time_slice.clone()),
460                fraction_limits.clone(),
461            );
462        }
463        let process = Rc::new(Process {
464            id: "process1".into(),
465            description: "Description".into(),
466            years: vec![2010, 2020],
467            activity_limits,
468            flows: ProcessFlowsMap::new(),
469            parameters: process_parameter_map,
470            regions: IndexSet::from(["GBR".into()]),
471        });
472        let asset = Asset::new(
473            "agent1".into(),
474            Rc::clone(&process),
475            "GBR".into(),
476            Capacity(2.0),
477            2010,
478        )
479        .unwrap();
480
481        assert_eq!(
482            asset.get_activity_limits(&time_slice),
483            Activity(6.0)..=Activity(f64::INFINITY)
484        );
485    }
486
487    #[rstest]
488    fn test_asset_pool_new(asset_pool: AssetPool) {
489        // Should be in order of commission year
490        assert!(asset_pool.active.is_empty());
491        assert!(asset_pool.future.len() == 2);
492        assert!(asset_pool.future[0].commission_year == 2010);
493        assert!(asset_pool.future[1].commission_year == 2020);
494    }
495
496    #[rstest]
497    fn test_asset_pool_commission_new1(mut asset_pool: AssetPool) {
498        // Asset to be commissioned in this year
499        asset_pool.commission_new(2010);
500        assert_equal(asset_pool.iter(), iter::once(&asset_pool.active[0]));
501    }
502
503    #[rstest]
504    fn test_asset_pool_commission_new2(mut asset_pool: AssetPool) {
505        // Commission year has passed
506        asset_pool.commission_new(2011);
507        assert_equal(asset_pool.iter(), iter::once(&asset_pool.active[0]));
508    }
509
510    #[rstest]
511    fn test_asset_pool_commission_new3(mut asset_pool: AssetPool) {
512        // Nothing to commission for this year
513        asset_pool.commission_new(2000);
514        assert!(asset_pool.iter().next().is_none()); // no active assets
515    }
516
517    #[rstest]
518    fn test_asset_pool_decommission_old(mut asset_pool: AssetPool) {
519        asset_pool.commission_new(2020);
520        assert_eq!(asset_pool.active.len(), 2);
521        asset_pool.decommission_old(2020); // should decommission first asset (lifetime == 5)
522        assert_eq!(asset_pool.active.len(), 1);
523        assert_eq!(asset_pool.active[0].commission_year, 2020);
524        asset_pool.decommission_old(2022); // nothing to decommission
525        assert_eq!(asset_pool.active.len(), 1);
526        assert_eq!(asset_pool.active[0].commission_year, 2020);
527        asset_pool.decommission_old(2025); // should decommission second asset
528        assert!(asset_pool.active.is_empty());
529    }
530
531    #[rstest]
532    fn test_asset_pool_get(mut asset_pool: AssetPool) {
533        asset_pool.commission_new(2020);
534        assert_eq!(asset_pool.get(AssetID(0)), Some(&asset_pool.active[0]));
535        assert_eq!(asset_pool.get(AssetID(1)), Some(&asset_pool.active[1]));
536    }
537
538    #[rstest]
539    fn test_asset_pool_replace_active_pool_existing(mut asset_pool: AssetPool) {
540        asset_pool.commission_new(2020);
541        assert_eq!(asset_pool.active.len(), 2);
542        asset_pool.replace_active_pool(iter::once(asset_pool.active[1].clone().into()));
543        assert_eq!(asset_pool.active.len(), 1);
544        assert_eq!(asset_pool.active[0].id, Some(AssetID(1)));
545    }
546
547    #[rstest]
548    fn test_asset_pool_replace_active_pool_new_asset(mut asset_pool: AssetPool, process: Process) {
549        let asset = Asset::new(
550            "some_other_agent".into(),
551            process.into(),
552            "GBR".into(),
553            Capacity(2.0),
554            2010,
555        )
556        .unwrap();
557
558        asset_pool.commission_new(2020);
559        assert_eq!(asset_pool.active.len(), 2);
560        asset_pool.replace_active_pool(iter::once(asset.into()));
561        assert_eq!(asset_pool.active.len(), 1);
562        assert_eq!(asset_pool.active[0].id, Some(AssetID(2)));
563        assert_eq!(asset_pool.active[0].agent_id, "some_other_agent".into());
564    }
565
566    #[rstest]
567    fn test_asset_pool_replace_active_pool_out_of_order(
568        mut asset_pool: AssetPool,
569        process: Process,
570    ) {
571        let new_asset = Asset::new(
572            "some_other_agent".into(),
573            process.into(),
574            "GBR".into(),
575            Capacity(2.0),
576            2010,
577        )
578        .unwrap();
579
580        asset_pool.commission_new(2020);
581        assert_eq!(asset_pool.active.len(), 2);
582        let mut new_pool: Vec<Rc<Asset>> = asset_pool
583            .iter()
584            .map(|asset| asset.clone().into())
585            .collect();
586        new_pool.push(new_asset.into());
587        new_pool.reverse();
588
589        asset_pool.replace_active_pool(new_pool);
590        assert_equal(asset_pool.iter().map(|asset| asset.id.unwrap().0), 0..3);
591        assert_eq!(asset_pool.active[2].id, Some(AssetID(2)));
592        assert_eq!(asset_pool.active[2].agent_id, "some_other_agent".into());
593    }
594}