Skip to main content

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