muse2/asset/
pool.rs

1//! Defines a data structure for representing the current active pool of assets.
2use super::{AssetID, AssetRef, AssetState};
3use itertools::Itertools;
4use log::warn;
5use std::cmp::min;
6use std::slice;
7
8/// The active pool of [`super::Asset`]s
9#[derive(Default)]
10pub struct AssetPool {
11    /// The pool of active assets, sorted by ID
12    assets: Vec<AssetRef>,
13    /// Next available asset ID number
14    next_id: u32,
15    /// Next available group ID number
16    next_group_id: u32,
17}
18
19impl AssetPool {
20    /// Create a new empty [`AssetPool`]
21    pub fn new() -> Self {
22        Self::default()
23    }
24
25    /// Get the active pool as a slice of [`AssetRef`]s
26    pub fn as_slice(&self) -> &[AssetRef] {
27        &self.assets
28    }
29
30    /// Commission new assets for the specified milestone year from the input data
31    pub fn commission_new(&mut self, year: u32, user_assets: &mut Vec<AssetRef>) {
32        let to_commission = user_assets.extract_if(.., |asset| asset.commission_year <= year);
33
34        for asset in to_commission {
35            // Ignore assets that have already been decommissioned
36            if asset.max_decommission_year() <= year {
37                warn!(
38                    "Asset '{}' with commission year {} and lifetime {} was decommissioned before \
39                    the start of the simulation",
40                    asset.process_id(),
41                    asset.commission_year,
42                    asset.process_parameter.lifetime
43                );
44                continue;
45            }
46
47            self.commission(asset, "user input");
48        }
49    }
50
51    /// Commission the specified asset or, if divisible, its children
52    fn commission(&mut self, asset: AssetRef, reason: &str) {
53        asset.into_for_each_child(&mut self.next_group_id, |parent, mut child| {
54            child
55                .make_mut()
56                .commission(AssetID(self.next_id), parent.cloned(), reason);
57            self.next_id += 1;
58            self.assets.push(child);
59        });
60    }
61
62    /// Decommission old assets for the specified milestone year
63    pub fn decommission_old<E: Extend<AssetRef>>(&mut self, year: u32, decommissioned: &mut E) {
64        let to_decommission = self
65            .assets
66            .extract_if(.., move |asset| asset.max_decommission_year() <= year)
67            .map(move |mut asset| {
68                asset.make_mut().decommission(year, "end of life");
69                asset
70            });
71        decommissioned.extend(to_decommission);
72    }
73
74    /// Decommission mothballed assets if mothballed long enough
75    pub fn decommission_mothballed<E: Extend<AssetRef>>(
76        &mut self,
77        year: u32,
78        mothball_years: u32,
79        decommissioned: &mut E,
80    ) {
81        let to_decommission = self
82            .assets
83            .extract_if(.., move |asset| {
84                asset
85                    .get_mothballed_year()
86                    .is_some_and(|myear| myear <= year - min(mothball_years, year))
87            })
88            .map(move |mut asset| {
89                let decommissioned = asset.get_mothballed_year().unwrap() + mothball_years;
90                asset.make_mut().decommission(
91                    decommissioned,
92                    &format!(
93                        "The asset has not been used for the set mothball years ({mothball_years} \
94                        years)."
95                    ),
96                );
97                asset
98            });
99        decommissioned.extend(to_decommission);
100    }
101
102    /// Mothball the specified assets if they are no longer in the active pool and put them back
103    /// again.
104    ///
105    /// # Arguments
106    ///
107    /// * `assets` - Assets to possibly mothball
108    /// * `year` - Mothball year
109    ///
110    /// # Panics
111    ///
112    /// Panics if any of the provided assets was never commissioned.
113    pub fn mothball_unretained<I>(&mut self, assets: I, year: u32)
114    where
115        I: IntoIterator<Item = AssetRef>,
116    {
117        for mut asset in assets {
118            let in_pool = match asset.state {
119                AssetState::Commissioned { .. } => !self.assets.contains(&asset),
120                _ => panic!("Cannot mothball asset that has not been commissioned"),
121            };
122
123            if in_pool {
124                // If not already set, we set the current year as the mothball year,
125                // i.e. the first one the asset was not used.
126                if asset.get_mothballed_year().is_none() {
127                    asset.make_mut().mothball(year);
128                }
129
130                // And we put it back to the pool, so they can be chosen the next milestone year
131                // if not decommissioned earlier.
132                self.assets.push(asset);
133            }
134        }
135        self.assets.sort();
136    }
137
138    /// Get an asset with the specified ID.
139    ///
140    /// # Returns
141    ///
142    /// An [`AssetRef`] if found, else `None`. The asset may not be found if it has already been
143    /// decommissioned.
144    pub fn get(&self, id: AssetID) -> Option<&AssetRef> {
145        // Assets are sorted by ID
146        let idx = self
147            .assets
148            .binary_search_by(|asset| match &asset.state {
149                AssetState::Commissioned { id: asset_id, .. } => asset_id.cmp(&id),
150                _ => panic!("Active pool should only contain commissioned assets"),
151            })
152            .ok()?;
153
154        Some(&self.assets[idx])
155    }
156
157    /// Iterate over active assets
158    #[allow(clippy::iter_without_into_iter)]
159    pub fn iter(&self) -> slice::Iter<'_, AssetRef> {
160        self.assets.iter()
161    }
162
163    /// Return current active pool and clear
164    pub fn take(&mut self) -> Vec<AssetRef> {
165        std::mem::take(&mut self.assets)
166    }
167
168    /// Extend the active pool with Commissioned or Selected assets
169    pub fn extend<I>(&mut self, assets: I)
170    where
171        I: IntoIterator<Item = AssetRef>,
172    {
173        // Check all assets are either Commissioned or Selected, and, if the latter,
174        // then commission them
175        for mut asset in assets {
176            match &asset.state {
177                AssetState::Commissioned { .. } => {
178                    asset.make_mut().unmothball();
179                    self.assets.push(asset);
180                }
181                AssetState::Selected { .. } => {
182                    self.commission(asset, "selected");
183                }
184                _ => panic!(
185                    "Cannot extend asset pool with asset in state {}. Only assets in \
186                Commissioned or Selected states are allowed.",
187                    asset.state
188                ),
189            }
190        }
191
192        // New assets may not have been sorted, but we need them sorted by ID
193        self.assets.sort();
194
195        // Sanity check: all assets should be unique
196        debug_assert_eq!(self.assets.iter().unique().count(), self.assets.len());
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::super::Asset;
203    use super::*;
204    use crate::fixture::{asset, asset_divisible, process, process_parameter_map};
205    use crate::process::{Process, ProcessParameter};
206    use crate::units::{
207        Capacity, Dimensionless, MoneyPerActivity, MoneyPerCapacity, MoneyPerCapacityPerYear,
208    };
209    use itertools::{Itertools, assert_equal};
210    use rstest::{fixture, rstest};
211    use std::iter;
212    use std::rc::Rc;
213
214    #[fixture]
215    fn user_assets(mut process: Process) -> Vec<AssetRef> {
216        // Update process parameters (lifetime = 20 years)
217        let process_param = ProcessParameter {
218            capital_cost: MoneyPerCapacity(5.0),
219            fixed_operating_cost: MoneyPerCapacityPerYear(2.0),
220            variable_operating_cost: MoneyPerActivity(1.0),
221            lifetime: 20,
222            discount_rate: Dimensionless(0.9),
223        };
224        let process_parameter_map = process_parameter_map(process.regions.clone(), process_param);
225        process.parameters = process_parameter_map;
226
227        let rc_process = Rc::new(process);
228        [2020, 2010]
229            .map(|year| {
230                Asset::new_future(
231                    "agent1".into(),
232                    Rc::clone(&rc_process),
233                    "GBR".into(),
234                    Capacity(1.0),
235                    year,
236                )
237                .unwrap()
238                .into()
239            })
240            .into_iter()
241            .collect_vec()
242    }
243
244    #[rstest]
245    fn asset_pool_new() {
246        assert!(AssetPool::new().assets.is_empty());
247    }
248
249    #[rstest]
250    fn asset_pool_commission_new1(mut user_assets: Vec<AssetRef>) {
251        // Asset to be commissioned in this year
252        let mut asset_pool = AssetPool::new();
253        asset_pool.commission_new(2010, &mut user_assets);
254        assert_equal(asset_pool.iter(), iter::once(&asset_pool.assets[0]));
255    }
256
257    #[rstest]
258    fn asset_pool_commission_new2(mut user_assets: Vec<AssetRef>) {
259        // Commission year has passed
260        let mut asset_pool = AssetPool::new();
261        asset_pool.commission_new(2011, &mut user_assets);
262        assert_equal(asset_pool.iter(), iter::once(&asset_pool.assets[0]));
263    }
264
265    #[rstest]
266    fn asset_pool_commission_new3(mut user_assets: Vec<AssetRef>) {
267        // Nothing to commission for this year
268        let mut asset_pool = AssetPool::new();
269        asset_pool.commission_new(2000, &mut user_assets);
270        assert!(asset_pool.iter().next().is_none()); // no active assets
271    }
272
273    /// Number of expected children for divisible asset
274    #[allow(clippy::cast_possible_truncation)]
275    #[allow(clippy::cast_sign_loss)]
276    fn expected_children_for_divisible(asset: &Asset) -> usize {
277        (asset.total_capacity() / asset.process.unit_size.expect("Asset is not divisible"))
278            .value()
279            .ceil() as usize
280    }
281
282    #[rstest]
283    fn asset_pool_commission_new_divisible(asset_divisible: Asset) {
284        let commission_year = asset_divisible.commission_year;
285        let expected_children = expected_children_for_divisible(&asset_divisible);
286        let mut asset_pool = AssetPool::new();
287        let mut user_assets = vec![asset_divisible.into()];
288        assert!(asset_pool.assets.is_empty());
289        asset_pool.commission_new(commission_year, &mut user_assets);
290        assert!(user_assets.is_empty());
291        assert!(!asset_pool.assets.is_empty());
292        assert_eq!(asset_pool.assets.len(), expected_children);
293        assert_eq!(asset_pool.next_group_id, 1);
294    }
295
296    #[rstest]
297    fn asset_pool_commission_already_decommissioned(asset: Asset) {
298        let year = asset.max_decommission_year();
299        let mut asset_pool = AssetPool::new();
300        assert!(asset_pool.assets.is_empty());
301        asset_pool.commission_new(year, &mut vec![asset.into()]);
302        assert!(asset_pool.assets.is_empty());
303    }
304
305    #[rstest]
306    fn asset_pool_decommission_old(mut user_assets: Vec<AssetRef>) {
307        let mut asset_pool = AssetPool::new();
308        asset_pool.commission_new(2020, &mut user_assets);
309        assert!(user_assets.is_empty());
310        assert_eq!(asset_pool.assets.len(), 2);
311        let mut decommissioned = Vec::new();
312
313        // should decommission first asset (lifetime == 5)
314        asset_pool.decommission_old(2030, &mut decommissioned);
315        assert_eq!(asset_pool.assets.len(), 1);
316        assert_eq!(asset_pool.assets[0].commission_year, 2020);
317        assert_eq!(decommissioned.len(), 1);
318        assert_eq!(decommissioned[0].commission_year, 2010);
319        assert_eq!(decommissioned[0].decommission_year(), Some(2030));
320
321        // nothing to decommission
322        decommissioned.clear();
323        asset_pool.decommission_old(2032, &mut decommissioned);
324        assert_eq!(asset_pool.assets.len(), 1);
325        assert_eq!(asset_pool.assets[0].commission_year, 2020);
326
327        // should decommission second asset
328        decommissioned.clear();
329        asset_pool.decommission_old(2040, &mut decommissioned);
330        assert!(asset_pool.assets.is_empty());
331        assert_eq!(decommissioned.len(), 1);
332        assert_eq!(decommissioned[0].commission_year, 2020);
333        assert_eq!(decommissioned[0].decommission_year(), Some(2040));
334    }
335
336    #[rstest]
337    fn asset_pool_get(mut user_assets: Vec<AssetRef>) {
338        let mut asset_pool = AssetPool::new();
339        asset_pool.commission_new(2020, &mut user_assets);
340        assert_eq!(asset_pool.get(AssetID(0)), Some(&asset_pool.assets[0]));
341        assert_eq!(asset_pool.get(AssetID(1)), Some(&asset_pool.assets[1]));
342    }
343
344    #[rstest]
345    fn asset_pool_extend_empty(mut user_assets: Vec<AssetRef>) {
346        // Start with commissioned assets
347        let mut asset_pool = AssetPool::new();
348        asset_pool.commission_new(2020, &mut user_assets);
349        let original_count = asset_pool.assets.len();
350
351        // Extend with empty iterator
352        asset_pool.extend(Vec::<AssetRef>::new());
353
354        assert_eq!(asset_pool.assets.len(), original_count);
355    }
356
357    #[rstest]
358    fn asset_pool_extend_existing_assets(mut user_assets: Vec<AssetRef>) {
359        // Start with some commissioned assets
360        let mut asset_pool = AssetPool::new();
361        asset_pool.commission_new(2020, &mut user_assets);
362        assert_eq!(asset_pool.assets.len(), 2);
363        let existing_assets = asset_pool.take();
364
365        // Extend with the same assets (should maintain their IDs)
366        asset_pool.extend(existing_assets.clone());
367
368        assert_eq!(asset_pool.assets.len(), 2);
369        assert_eq!(asset_pool.assets[0].id(), Some(AssetID(0)));
370        assert_eq!(asset_pool.assets[1].id(), Some(AssetID(1)));
371    }
372
373    #[rstest]
374    fn asset_pool_extend_new_assets(mut user_assets: Vec<AssetRef>, process: Process) {
375        // Start with some commissioned assets
376        let mut asset_pool = AssetPool::new();
377        asset_pool.commission_new(2020, &mut user_assets);
378        let original_count = asset_pool.assets.len();
379
380        // Create new non-commissioned assets
381        let process_rc = Rc::new(process);
382        let new_assets = vec![
383            Asset::new_selected(
384                "agent2".into(),
385                Rc::clone(&process_rc),
386                "GBR".into(),
387                Capacity(1.5),
388                2015,
389            )
390            .unwrap()
391            .into(),
392            Asset::new_selected(
393                "agent3".into(),
394                Rc::clone(&process_rc),
395                "GBR".into(),
396                Capacity(2.5),
397                2020,
398            )
399            .unwrap()
400            .into(),
401        ];
402
403        asset_pool.extend(new_assets);
404
405        assert_eq!(asset_pool.assets.len(), original_count + 2);
406        // New assets should get IDs 2 and 3
407        assert_eq!(asset_pool.assets[original_count].id(), Some(AssetID(2)));
408        assert_eq!(asset_pool.assets[original_count + 1].id(), Some(AssetID(3)));
409        assert_eq!(
410            asset_pool.assets[original_count].agent_id(),
411            Some(&"agent2".into())
412        );
413        assert_eq!(
414            asset_pool.assets[original_count + 1].agent_id(),
415            Some(&"agent3".into())
416        );
417    }
418
419    #[rstest]
420    fn asset_pool_extend_new_divisible_assets(
421        mut user_assets: Vec<AssetRef>,
422        mut process: Process,
423    ) {
424        // Start with some commissioned assets
425        let mut asset_pool = AssetPool::new();
426        asset_pool.commission_new(2020, &mut user_assets);
427        let original_count = asset_pool.assets.len();
428
429        // Create new non-commissioned assets
430        process.unit_size = Some(Capacity(4.0));
431        let process_rc = Rc::new(process);
432        let new_assets: Vec<AssetRef> = vec![
433            Asset::new_selected(
434                "agent2".into(),
435                Rc::clone(&process_rc),
436                "GBR".into(),
437                Capacity(11.0),
438                2015,
439            )
440            .unwrap()
441            .into(),
442        ];
443        let expected_children = expected_children_for_divisible(&new_assets[0]);
444        asset_pool.extend(new_assets);
445        assert_eq!(asset_pool.assets.len(), original_count + expected_children);
446    }
447
448    #[rstest]
449    fn asset_pool_extend_mixed_assets(mut user_assets: Vec<AssetRef>, process: Process) {
450        // Start with some commissioned assets
451        let mut asset_pool = AssetPool::new();
452        asset_pool.commission_new(2020, &mut user_assets);
453
454        // Create a new non-commissioned asset
455        let new_asset = Asset::new_selected(
456            "agent_new".into(),
457            process.into(),
458            "GBR".into(),
459            Capacity(3.0),
460            2015,
461        )
462        .unwrap()
463        .into();
464
465        // Extend with just the new asset (not mixing with existing to avoid duplicates)
466        asset_pool.extend(vec![new_asset]);
467
468        assert_eq!(asset_pool.assets.len(), 3);
469        // Check that we have the original assets plus the new one
470        assert!(asset_pool.assets.iter().any(|a| a.id() == Some(AssetID(0))));
471        assert!(asset_pool.assets.iter().any(|a| a.id() == Some(AssetID(1))));
472        assert!(asset_pool.assets.iter().any(|a| a.id() == Some(AssetID(2))));
473        // Check that the new asset has the correct agent
474        assert!(
475            asset_pool
476                .assets
477                .iter()
478                .any(|a| a.agent_id() == Some(&"agent_new".into()))
479        );
480    }
481
482    #[rstest]
483    fn asset_pool_extend_maintains_sort_order(mut user_assets: Vec<AssetRef>, process: Process) {
484        // Start with some commissioned assets
485        let mut asset_pool = AssetPool::new();
486        asset_pool.commission_new(2020, &mut user_assets);
487
488        // Create new assets that would be out of order if added at the end
489        let process_rc = Rc::new(process);
490        let new_assets = vec![
491            Asset::new_selected(
492                "agent_high_id".into(),
493                Rc::clone(&process_rc),
494                "GBR".into(),
495                Capacity(1.0),
496                2010,
497            )
498            .unwrap()
499            .into(),
500            Asset::new_selected(
501                "agent_low_id".into(),
502                Rc::clone(&process_rc),
503                "GBR".into(),
504                Capacity(1.0),
505                2015,
506            )
507            .unwrap()
508            .into(),
509        ];
510
511        asset_pool.extend(new_assets);
512
513        // Check that assets are sorted by ID
514        let ids: Vec<u32> = asset_pool.iter().map(|a| a.id().unwrap().0).collect();
515        assert_equal(ids, 0..4);
516    }
517
518    #[rstest]
519    fn asset_pool_extend_no_duplicates_expected(mut user_assets: Vec<AssetRef>) {
520        // Start with some commissioned assets
521        let mut asset_pool = AssetPool::new();
522        asset_pool.commission_new(2020, &mut user_assets);
523        let original_count = asset_pool.assets.len();
524
525        // The extend method expects unique assets - adding duplicates would violate
526        // the debug assertion, so this test verifies the normal case
527        asset_pool.extend(Vec::new());
528
529        assert_eq!(asset_pool.assets.len(), original_count);
530        // Verify all assets are still unique (this is what the debug_assert checks)
531        assert_eq!(
532            asset_pool.assets.iter().unique().count(),
533            asset_pool.assets.len()
534        );
535    }
536
537    #[rstest]
538    fn asset_pool_extend_increments_next_id(mut user_assets: Vec<AssetRef>, process: Process) {
539        // Start with some commissioned assets
540        let mut asset_pool = AssetPool::new();
541        asset_pool.commission_new(2020, &mut user_assets);
542        assert_eq!(asset_pool.next_id, 2); // Should be 2 after commissioning 2 assets
543
544        // Create new non-commissioned assets
545        let process_rc = Rc::new(process);
546        let new_assets = vec![
547            Asset::new_selected(
548                "agent1".into(),
549                Rc::clone(&process_rc),
550                "GBR".into(),
551                Capacity(1.0),
552                2015,
553            )
554            .unwrap()
555            .into(),
556            Asset::new_selected(
557                "agent2".into(),
558                Rc::clone(&process_rc),
559                "GBR".into(),
560                Capacity(1.0),
561                2020,
562            )
563            .unwrap()
564            .into(),
565        ];
566
567        asset_pool.extend(new_assets);
568
569        // next_id should have incremented for each new asset
570        assert_eq!(asset_pool.next_id, 4);
571        assert_eq!(asset_pool.assets[2].id(), Some(AssetID(2)));
572        assert_eq!(asset_pool.assets[3].id(), Some(AssetID(3)));
573    }
574
575    #[rstest]
576    fn asset_pool_mothball_unretained(mut user_assets: Vec<AssetRef>) {
577        // Commission some assets
578        let mut asset_pool = AssetPool::new();
579        asset_pool.commission_new(2020, &mut user_assets);
580        assert_eq!(asset_pool.assets.len(), 2);
581
582        // Remove one asset from the active pool (simulating it being removed elsewhere)
583        let removed_asset = asset_pool.assets.remove(0);
584        assert_eq!(asset_pool.assets.len(), 1);
585
586        // Try to mothball both the removed asset (not in active) and an active asset
587        let assets_to_check = vec![removed_asset.clone(), asset_pool.assets[0].clone()];
588        asset_pool.mothball_unretained(assets_to_check, 2025);
589
590        // Only the removed asset should be mothballed (since it's not in active pool)
591        assert_eq!(asset_pool.assets.len(), 2); // And should be back into the pool
592        assert_eq!(asset_pool.assets[0].get_mothballed_year(), Some(2025));
593    }
594
595    #[rstest]
596    fn asset_pool_decommission_unused(mut user_assets: Vec<AssetRef>) {
597        // Commission some assets
598        let mut asset_pool = AssetPool::new();
599        asset_pool.commission_new(2020, &mut user_assets);
600        assert_eq!(asset_pool.assets.len(), 2);
601
602        // Make an asset unused for a few years
603        let mothball_years: u32 = 10;
604        asset_pool.assets[0]
605            .make_mut()
606            .mothball(2025 - mothball_years);
607
608        assert_eq!(
609            asset_pool.assets[0].get_mothballed_year(),
610            Some(2025 - mothball_years)
611        );
612
613        // Decommission unused assets
614        let mut decommissioned = Vec::new();
615        asset_pool.decommission_mothballed(2025, mothball_years, &mut decommissioned);
616
617        // Only the removed asset should be decommissioned (since it's not in active pool)
618        assert_eq!(asset_pool.assets.len(), 1); // Active pool unchanged
619        assert_eq!(decommissioned.len(), 1);
620        assert_eq!(decommissioned[0].decommission_year(), Some(2025));
621    }
622
623    #[rstest]
624    fn asset_pool_decommission_if_not_active_none_active(mut user_assets: Vec<AssetRef>) {
625        // Commission some assets
626        let mut asset_pool = AssetPool::new();
627        asset_pool.commission_new(2020, &mut user_assets);
628        let all_assets = asset_pool.assets.clone();
629
630        // Clear the active pool (simulating all assets being removed)
631        asset_pool.assets.clear();
632
633        // Try to mothball the assets that are no longer active
634        asset_pool.mothball_unretained(all_assets.clone(), 2025);
635
636        // All assets should be mothballed
637        assert_eq!(asset_pool.assets.len(), 2);
638        assert_eq!(asset_pool.assets[0].id(), all_assets[0].id());
639        assert_eq!(asset_pool.assets[0].get_mothballed_year(), Some(2025));
640        assert_eq!(asset_pool.assets[1].id(), all_assets[1].id());
641        assert_eq!(asset_pool.assets[1].get_mothballed_year(), Some(2025));
642    }
643
644    #[rstest]
645    #[should_panic(expected = "Cannot mothball asset that has not been commissioned")]
646    fn asset_pool_decommission_if_not_active_non_commissioned_asset(process: Process) {
647        // Create a non-commissioned asset
648        let non_commissioned_asset = Asset::new_future(
649            "agent_new".into(),
650            process.into(),
651            "GBR".into(),
652            Capacity(1.0),
653            2015,
654        )
655        .unwrap()
656        .into();
657
658        // This should panic because the asset was never commissioned
659        let mut asset_pool = AssetPool::new();
660        asset_pool.mothball_unretained(vec![non_commissioned_asset], 2025);
661    }
662}