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    ///
26    /// Candidate assets may not have an agent ID.
27    pub agent_id: Option<AgentID>,
28    /// The [`Process`] that this asset corresponds to
29    pub process: Rc<Process>,
30    /// The [`ProcessParameter`] corresponding to the asset's region and commission year
31    pub process_parameter: Rc<ProcessParameter>,
32    /// The region in which the asset is located
33    pub region_id: RegionID,
34    /// Capacity of asset
35    pub capacity: Capacity,
36    /// The year the asset comes online
37    pub commission_year: u32,
38}
39
40impl Asset {
41    /// Create a new [`Asset`].
42    ///
43    /// The `id` field is initially set to `None`, but is changed to a unique value when the asset
44    /// is stored in an [`AssetPool`].
45    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(&region_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    /// The last year in which this asset should be decommissioned
84    pub fn decommission_year(&self) -> u32 {
85        self.commission_year + self.process_parameter.lifetime
86    }
87
88    /// Get the activity limits for this asset in a particular time slice
89    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        // limits in real units (which are user defined)
102        (max_act * *limits.start())..=(max_act * *limits.end())
103    }
104
105    /// Maximum activity for this asset
106    pub fn maximum_activity(&self) -> Activity {
107        self.capacity * self.process_parameter.capacity_to_activity
108    }
109
110    /// Get a specific process flow
111    pub fn get_flow(&self, commodity_id: &CommodityID) -> Option<&ProcessFlow> {
112        self.get_flows_map().get(commodity_id)
113    }
114
115    /// Get the process flows map for this asset
116    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    /// Iterate over the asset's flows
124    pub fn iter_flows(&self) -> impl Iterator<Item = &ProcessFlow> {
125        self.get_flows_map().values()
126    }
127}
128
129/// Whether the specified value is a valid capacity for an asset
130pub 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/// A wrapper around [`Asset`] for storing references in maps.
140///
141/// If the asset has been commissioned, then comparison and hashing is done based on the asset ID,
142/// otherwise a combination of other parameters is used.
143///
144/// [`Ord`] is implemented for [`AssetRef`], but it will panic for non-commissioned assets.
145#[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    /// Hash asset based purely on its ID
192    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
216/// A pool of [`Asset`]s
217pub struct AssetPool {
218    /// The pool of active assets
219    active: Vec<AssetRef>,
220    /// Assets that have not yet been commissioned, sorted by commission year
221    future: Vec<Asset>,
222    /// Next available asset ID number
223    next_id: u32,
224}
225
226impl AssetPool {
227    /// Create a new [`AssetPool`]
228    pub fn new(mut assets: Vec<Asset>) -> Self {
229        // Sort in order of commission year
230        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    /// Get the active pool as a slice of [`AssetRef`]s
240    pub fn as_slice(&self) -> &[AssetRef] {
241        &self.active
242    }
243
244    /// Commission new assets for the specified milestone year from the input data
245    pub fn commission_new(&mut self, year: u32) {
246        // Count the number of assets to move
247        let count = self
248            .future
249            .iter()
250            .take_while(|asset| asset.commission_year <= year)
251            .count();
252
253        // Move assets from future to active
254        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    /// Decommission old assets for the specified milestone year
262    pub fn decommission_old(&mut self, year: u32) {
263        self.active.retain(|asset| asset.decommission_year() > year);
264    }
265
266    /// Get an asset with the specified ID.
267    ///
268    /// # Returns
269    ///
270    /// An [`AssetRef`] if found, else `None`. The asset may not be found if it has already been
271    /// decommissioned.
272    pub fn get(&self, id: AssetID) -> Option<&AssetRef> {
273        // The assets in `active` are in order of ID
274        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    /// Iterate over active assets
283    pub fn iter(&self) -> std::slice::Iter<AssetRef> {
284        self.active.iter()
285    }
286
287    /// Replace the active pool with new and/or already commissioned assets
288    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                // Asset is newly created from process so we need to assign an ID
295                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        // New pool may not have been sorted, but active needs to be sorted by ID
307        self.active.sort();
308    }
309}
310
311/// Additional methods for iterating over assets
312pub trait AssetIterator<'a> {
313    /// Filter the assets by region
314    fn filter_region(self, region_id: &'a RegionID) -> impl Iterator<Item = &'a AssetRef> + 'a;
315
316    /// Iterate over process flows affecting the given commodity
317    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        // Should be in order of commission year
499        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 to be commissioned in this year
508        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        // Commission year has passed
515        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        // Nothing to commission for this year
522        asset_pool.commission_new(2000);
523        assert!(asset_pool.iter().next().is_none()); // no active assets
524    }
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); // should decommission first asset (lifetime == 5)
531        assert_eq!(asset_pool.active.len(), 1);
532        assert_eq!(asset_pool.active[0].commission_year, 2020);
533        asset_pool.decommission_old(2022); // nothing to decommission
534        assert_eq!(asset_pool.active.len(), 1);
535        assert_eq!(asset_pool.active[0].commission_year, 2020);
536        asset_pool.decommission_old(2025); // should decommission second asset
537        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}