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, ProcessID, ProcessParameter};
5use crate::region::RegionID;
6use crate::simulation::CommodityPrices;
7use crate::time_slice::TimeSliceID;
8use crate::units::{Activity, ActivityPerCapacity, Capacity, Dimensionless, MoneyPerActivity};
9use anyhow::{Context, Result, ensure};
10use indexmap::IndexMap;
11use itertools::{Itertools, chain};
12use log::{debug, warn};
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::hash::{Hash, Hasher};
16use std::ops::{Deref, RangeInclusive};
17use std::rc::Rc;
18use std::slice;
19
20/// A unique identifier for an asset
21#[derive(
22    Clone,
23    Copy,
24    Debug,
25    derive_more::Display,
26    Eq,
27    Hash,
28    Ord,
29    PartialEq,
30    PartialOrd,
31    Deserialize,
32    Serialize,
33)]
34pub struct AssetID(u32);
35
36/// The state of an asset
37///
38/// New assets are created as either `Future` or `Candidate` assets. `Future` assets (which are
39/// specified in the input data) have a fixed capacity and capital costs already accounted for,
40/// whereas `Candidate` assets capital costs are not yet accounted for, and their capacity is
41/// determined by the investment algorithm.
42///
43/// `Future` and `Candidate` assets can be converted to `Commissioned` assets by calling
44/// `commission_future` or `commission_candidate` respectively.
45///
46/// `Commissioned` assets can be decommissioned by calling `decommission`.
47#[derive(Clone, Debug, PartialEq, strum::Display)]
48pub enum AssetState {
49    /// The asset has been commissioned
50    Commissioned {
51        /// The ID of the asset
52        id: AssetID,
53        /// The ID of the agent that owns the asset
54        agent_id: AgentID,
55    },
56    /// The asset has been decommissioned
57    Decommissioned {
58        /// The ID of the asset
59        id: AssetID,
60        /// The ID of the agent that owned the asset
61        agent_id: AgentID,
62        /// The year the asset was decommissioned
63        decommission_year: u32,
64    },
65    /// The asset is planned for commissioning in the future
66    Future {
67        /// The ID of the agent that will own the asset
68        agent_id: AgentID,
69    },
70    /// The asset has been selected for investment, but not yet confirmed
71    Selected {
72        /// The ID of the agent that would own the asset
73        agent_id: AgentID,
74    },
75    /// The asset is a candidate for investment but has not yet been selected by an agent
76    Candidate,
77}
78
79/// An asset controlled by an agent.
80#[derive(Clone, PartialEq)]
81pub struct Asset {
82    /// The status of the asset
83    state: AssetState,
84    /// The [`Process`] that this asset corresponds to
85    process: Rc<Process>,
86    /// Activity limits for this asset
87    activity_limits: Rc<HashMap<TimeSliceID, RangeInclusive<Dimensionless>>>,
88    /// The commodity flows for this asset
89    flows: Rc<IndexMap<CommodityID, ProcessFlow>>,
90    /// The [`ProcessParameter`] corresponding to the asset's region and commission year
91    process_parameter: Rc<ProcessParameter>,
92    /// The region in which the asset is located
93    region_id: RegionID,
94    /// Capacity of asset (for candidates this is a hypothetical capacity which may be altered)
95    capacity: Capacity,
96    /// The year the asset was/will be commissioned
97    commission_year: u32,
98}
99
100impl Asset {
101    /// Create a new candidate asset
102    pub fn new_candidate(
103        process: Rc<Process>,
104        region_id: RegionID,
105        capacity: Capacity,
106        commission_year: u32,
107    ) -> Result<Self> {
108        Self::new_with_state(
109            AssetState::Candidate,
110            process,
111            region_id,
112            capacity,
113            commission_year,
114        )
115    }
116
117    /// Create a new candidate asset from a commissioned asset
118    pub fn new_candidate_from_commissioned(asset: &Asset) -> Self {
119        assert!(asset.is_commissioned(), "Asset must be commissioned");
120
121        Self {
122            state: AssetState::Candidate,
123            ..asset.clone()
124        }
125    }
126
127    /// Create a new future asset
128    pub fn new_future(
129        agent_id: AgentID,
130        process: Rc<Process>,
131        region_id: RegionID,
132        capacity: Capacity,
133        commission_year: u32,
134    ) -> Result<Self> {
135        check_capacity_valid_for_asset(capacity)?;
136        Self::new_with_state(
137            AssetState::Future { agent_id },
138            process,
139            region_id,
140            capacity,
141            commission_year,
142        )
143    }
144
145    /// Create a new selected asset
146    ///
147    /// This is only used for testing. In the real program, Selected assets can only be created from
148    /// Candidate assets by calling `select_candidate_for_investment`.
149    #[cfg(test)]
150    fn new_selected(
151        agent_id: AgentID,
152        process: Rc<Process>,
153        region_id: RegionID,
154        capacity: Capacity,
155        commission_year: u32,
156    ) -> Result<Self> {
157        Self::new_with_state(
158            AssetState::Selected { agent_id },
159            process,
160            region_id,
161            capacity,
162            commission_year,
163        )
164    }
165
166    /// Private helper to create an asset with the given state
167    fn new_with_state(
168        state: AssetState,
169        process: Rc<Process>,
170        region_id: RegionID,
171        capacity: Capacity,
172        commission_year: u32,
173    ) -> Result<Self> {
174        check_region_year_valid_for_process(&process, &region_id, commission_year)?;
175        ensure!(capacity >= Capacity(0.0), "Capacity must be non-negative");
176
177        // There should be activity limits, commodity flows and process parameters for all
178        // **milestone** years, but it is possible to have assets that are commissioned before the
179        // simulation start from assets.csv. We check for the presence of the params lazily to
180        // prevent users having to supply them for all the possible valid years before the time
181        // horizon.
182        let key = (region_id.clone(), commission_year);
183        let activity_limits = process
184            .activity_limits
185            .get(&key)
186            .with_context(|| {
187                format!(
188                    "No process availabilities supplied for process {} in region {} in year {}. \
189                    You should update process_availabilities.csv.",
190                    &process.id, region_id, commission_year
191                )
192            })?
193            .clone();
194        let flows = process
195            .flows
196            .get(&key)
197            .with_context(|| {
198                format!(
199                    "No commodity flows supplied for process {} in region {} in year {}. \
200                    You should update process_flows.csv.",
201                    &process.id, region_id, commission_year
202                )
203            })?
204            .clone();
205        let process_parameter = process
206            .parameters
207            .get(&key)
208            .with_context(|| {
209                format!(
210                    "No process parameters supplied for process {} in region {} in year {}. \
211                    You should update process_parameters.csv.",
212                    &process.id, region_id, commission_year
213                )
214            })?
215            .clone();
216
217        Ok(Self {
218            state,
219            process,
220            activity_limits,
221            flows,
222            process_parameter,
223            region_id,
224            capacity,
225            commission_year,
226        })
227    }
228
229    /// Get the state of this asset
230    pub fn state(&self) -> &AssetState {
231        &self.state
232    }
233
234    /// The process parameter for this asset
235    pub fn process_parameter(&self) -> &ProcessParameter {
236        &self.process_parameter
237    }
238
239    /// The last year in which this asset should be decommissioned
240    pub fn max_decommission_year(&self) -> u32 {
241        self.commission_year + self.process_parameter.lifetime
242    }
243
244    /// Get the activity limits for this asset in a particular time slice
245    pub fn get_activity_limits(&self, time_slice: &TimeSliceID) -> RangeInclusive<Activity> {
246        let limits = &self.activity_limits[time_slice];
247        let max_act = self.max_activity();
248
249        // limits in real units (which are user defined)
250        (max_act * *limits.start())..=(max_act * *limits.end())
251    }
252
253    /// Get the activity limits per unit of capacity for this asset in a particular time slice
254    pub fn get_activity_per_capacity_limits(
255        &self,
256        time_slice: &TimeSliceID,
257    ) -> RangeInclusive<ActivityPerCapacity> {
258        let limits = &self.activity_limits[time_slice];
259        let cap2act = self.process.capacity_to_activity;
260        (cap2act * *limits.start())..=(cap2act * *limits.end())
261    }
262
263    /// Get the operating cost for this asset in a given year and time slice
264    pub fn get_operating_cost(&self, year: u32, time_slice: &TimeSliceID) -> MoneyPerActivity {
265        // The cost for all commodity flows (including levies/incentives)
266        let flows_cost: MoneyPerActivity = self
267            .iter_flows()
268            .map(|flow| flow.get_total_cost(&self.region_id, year, time_slice))
269            .sum();
270
271        self.process_parameter.variable_operating_cost + flows_cost
272    }
273
274    /// Get the total revenue from all flows for this asset.
275    ///
276    /// If a price is missing, it is assumed to be zero.
277    pub fn get_revenue_from_flows(
278        &self,
279        prices: &CommodityPrices,
280        time_slice: &TimeSliceID,
281    ) -> MoneyPerActivity {
282        self.get_revenue_from_flows_with_filter(prices, time_slice, |_| true)
283    }
284
285    /// Get the total revenue from all flows excluding the primary output.
286    ///
287    /// If a price is missing, it is assumed to be zero.
288    pub fn get_revenue_from_flows_excluding_primary(
289        &self,
290        prices: &CommodityPrices,
291        time_slice: &TimeSliceID,
292    ) -> MoneyPerActivity {
293        let excluded_commodity = self.primary_output().map(|flow| &flow.commodity.id);
294
295        self.get_revenue_from_flows_with_filter(prices, time_slice, |flow| {
296            excluded_commodity.is_none_or(|commodity_id| commodity_id != &flow.commodity.id)
297        })
298    }
299
300    /// Get the cost of input flows using the commodity prices in `input_prices`.
301    ///
302    /// If a price is missing, there is assumed to be no cost.
303    pub fn get_input_cost_from_prices(
304        &self,
305        input_prices: &CommodityPrices,
306        time_slice: &TimeSliceID,
307    ) -> MoneyPerActivity {
308        -self.get_revenue_from_flows_with_filter(input_prices, time_slice, ProcessFlow::is_input)
309    }
310
311    /// Get the total revenue from a subset of flows.
312    ///
313    /// Takes a function as an argument to filter the flows. If a price is missing, it is assumed to
314    /// be zero.
315    fn get_revenue_from_flows_with_filter<F>(
316        &self,
317        prices: &CommodityPrices,
318        time_slice: &TimeSliceID,
319        mut filter_for_flows: F,
320    ) -> MoneyPerActivity
321    where
322        F: FnMut(&ProcessFlow) -> bool,
323    {
324        self.iter_flows()
325            .filter(|flow| filter_for_flows(flow))
326            .map(|flow| {
327                flow.coeff
328                    * prices
329                        .get(&flow.commodity.id, self.region_id(), time_slice)
330                        .unwrap_or_default()
331            })
332            .sum()
333    }
334
335    /// Maximum activity for this asset
336    pub fn max_activity(&self) -> Activity {
337        self.capacity * self.process.capacity_to_activity
338    }
339
340    /// Get a specific process flow
341    pub fn get_flow(&self, commodity_id: &CommodityID) -> Option<&ProcessFlow> {
342        self.flows.get(commodity_id)
343    }
344
345    /// Iterate over the asset's flows
346    pub fn iter_flows(&self) -> impl Iterator<Item = &ProcessFlow> {
347        self.flows.values()
348    }
349
350    /// Get the primary output flow (if any) for this asset
351    pub fn primary_output(&self) -> Option<&ProcessFlow> {
352        self.process
353            .primary_output
354            .as_ref()
355            .map(|commodity_id| &self.flows[commodity_id])
356    }
357
358    /// Whether this asset has been commissioned
359    pub fn is_commissioned(&self) -> bool {
360        matches!(&self.state, AssetState::Commissioned { .. })
361    }
362
363    /// Get the commission year for this asset
364    pub fn commission_year(&self) -> u32 {
365        self.commission_year
366    }
367
368    /// Get the decommission year for this asset
369    pub fn decommission_year(&self) -> Option<u32> {
370        match &self.state {
371            AssetState::Decommissioned {
372                decommission_year, ..
373            } => Some(*decommission_year),
374            _ => None,
375        }
376    }
377
378    /// Get the region ID for this asset
379    pub fn region_id(&self) -> &RegionID {
380        &self.region_id
381    }
382
383    /// Get the process for this asset
384    pub fn process(&self) -> &Process {
385        &self.process
386    }
387
388    /// Get the process ID for this asset
389    pub fn process_id(&self) -> &ProcessID {
390        &self.process.id
391    }
392
393    /// Get the ID for this asset
394    pub fn id(&self) -> Option<AssetID> {
395        match &self.state {
396            AssetState::Commissioned { id, .. } | AssetState::Decommissioned { id, .. } => {
397                Some(*id)
398            }
399            _ => None,
400        }
401    }
402
403    /// Get the agent ID for this asset
404    pub fn agent_id(&self) -> Option<&AgentID> {
405        match &self.state {
406            AssetState::Commissioned { agent_id, .. }
407            | AssetState::Decommissioned { agent_id, .. }
408            | AssetState::Future { agent_id }
409            | AssetState::Selected { agent_id } => Some(agent_id),
410            AssetState::Candidate => None,
411        }
412    }
413
414    /// Get the capacity for this asset
415    pub fn capacity(&self) -> Capacity {
416        self.capacity
417    }
418
419    /// Set the capacity for this asset (only for Candidate assets)
420    pub fn set_capacity(&mut self, capacity: Capacity) {
421        assert!(
422            self.state == AssetState::Candidate,
423            "set_capacity can only be called on Candidate assets"
424        );
425        assert!(capacity >= Capacity(0.0), "Capacity must be >= 0");
426        self.capacity = capacity;
427    }
428
429    /// Increase the capacity for this asset (only for Candidate assets)
430    pub fn increase_capacity(&mut self, capacity: Capacity) {
431        assert!(
432            self.state == AssetState::Candidate,
433            "increase_capacity can only be called on Candidate assets"
434        );
435        assert!(capacity >= Capacity(0.0), "Added capacity must be >= 0");
436        self.capacity += capacity;
437    }
438
439    /// Decommission this asset
440    fn decommission(&mut self, decommission_year: u32, reason: &str) {
441        let (id, agent_id) = match &self.state {
442            AssetState::Commissioned { id, agent_id } => (*id, agent_id.clone()),
443            _ => panic!("Cannot decommission an asset that hasn't been commissioned"),
444        };
445        debug!(
446            "Decommissioning '{}' asset (ID: {}) for agent '{}' (reason: {})",
447            self.process_id(),
448            id,
449            agent_id,
450            reason
451        );
452
453        self.state = AssetState::Decommissioned {
454            id,
455            agent_id,
456            decommission_year: decommission_year.min(self.max_decommission_year()),
457        };
458    }
459
460    /// Commission the asset.
461    ///
462    /// Only assets with an [`AssetState`] of `Future` or `Selected` can be commissioned. If the
463    /// asset's state is something else, this function will panic.
464    ///
465    /// # Arguments
466    ///
467    /// * `id` - The ID to give the newly commissioned asset
468    /// * `reason` - The reason for commissioning (included in log)
469    fn commission(&mut self, id: AssetID, reason: &str) {
470        let agent_id = match &self.state {
471            AssetState::Future { agent_id } | AssetState::Selected { agent_id } => agent_id,
472            state => panic!("Assets with state {state} cannot be commissioned"),
473        };
474        debug!(
475            "Commissioning '{}' asset (ID: {}) for agent '{}' (reason: {})",
476            self.process_id(),
477            id,
478            agent_id,
479            reason
480        );
481        self.state = AssetState::Commissioned {
482            id,
483            agent_id: agent_id.clone(),
484        };
485    }
486
487    /// Select a Candidate asset for investment, converting it to a Selected state
488    pub fn select_candidate_for_investment(&mut self, agent_id: AgentID) {
489        assert!(
490            self.state == AssetState::Candidate,
491            "select_candidate_for_investment can only be called on Candidate assets"
492        );
493        check_capacity_valid_for_asset(self.capacity).unwrap();
494        self.state = AssetState::Selected { agent_id };
495    }
496}
497
498#[allow(clippy::missing_fields_in_debug)]
499impl std::fmt::Debug for Asset {
500    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
501        f.debug_struct("Asset")
502            .field("state", &self.state)
503            .field("process_id", &self.process_id())
504            .field("region_id", &self.region_id)
505            .field("capacity", &self.capacity)
506            .field("commission_year", &self.commission_year)
507            .finish()
508    }
509}
510
511/// Whether the process operates in the specified region and year
512pub fn check_region_year_valid_for_process(
513    process: &Process,
514    region_id: &RegionID,
515    year: u32,
516) -> Result<()> {
517    ensure!(
518        process.regions.contains(region_id),
519        "Process {} does not operate in region {}",
520        process.id,
521        region_id
522    );
523    ensure!(
524        process.active_for_year(year),
525        "Process {} does not operate in the year {}",
526        process.id,
527        year
528    );
529    Ok(())
530}
531
532/// Whether the specified value is a valid capacity for an asset
533pub fn check_capacity_valid_for_asset(capacity: Capacity) -> Result<()> {
534    ensure!(
535        capacity.is_finite() && capacity > Capacity(0.0),
536        "Capacity must be a finite, positive number"
537    );
538    Ok(())
539}
540
541/// A wrapper around [`Asset`] for storing references in maps.
542///
543/// If the asset has been commissioned, then comparison and hashing is done based on the asset ID,
544/// otherwise a combination of other parameters is used.
545///
546/// [`Ord`] is implemented for [`AssetRef`], but it will panic for non-commissioned assets.
547#[derive(Clone, Debug)]
548pub struct AssetRef(Rc<Asset>);
549
550impl AssetRef {
551    /// Make a mutable reference to the underlying [`Asset`]
552    pub fn make_mut(&mut self) -> &mut Asset {
553        Rc::make_mut(&mut self.0)
554    }
555}
556
557impl From<Rc<Asset>> for AssetRef {
558    fn from(value: Rc<Asset>) -> Self {
559        Self(value)
560    }
561}
562
563impl From<Asset> for AssetRef {
564    fn from(value: Asset) -> Self {
565        Self::from(Rc::new(value))
566    }
567}
568
569impl From<AssetRef> for Rc<Asset> {
570    fn from(value: AssetRef) -> Self {
571        value.0
572    }
573}
574
575impl Deref for AssetRef {
576    type Target = Asset;
577
578    fn deref(&self) -> &Self::Target {
579        &self.0
580    }
581}
582
583impl PartialEq for AssetRef {
584    fn eq(&self, other: &Self) -> bool {
585        // For assets to be considered equal, they must have the same process, region, commission
586        // year and state
587        Rc::ptr_eq(&self.0.process, &other.0.process)
588            && self.0.region_id == other.0.region_id
589            && self.0.commission_year == other.0.commission_year
590            && self.0.state == other.0.state
591    }
592}
593
594impl Eq for AssetRef {}
595
596impl Hash for AssetRef {
597    /// Hash an asset according to its state:
598    /// - Commissioned assets are hashed based on their ID alone
599    /// - Selected assets are hashed based on `process_id`, `region_id`, `commission_year` and `agent_id`
600    /// - Candidate assets are hashed based on `process_id`, `region_id` and `commission_year`
601    /// - Future and Decommissioned assets cannot currently be hashed
602    fn hash<H: Hasher>(&self, state: &mut H) {
603        match &self.0.state {
604            AssetState::Commissioned { id, .. } => {
605                // Hashed based on their ID alone, since this is sufficient to uniquely identify the
606                // asset
607                id.hash(state);
608            }
609            AssetState::Candidate | AssetState::Selected { .. } => {
610                // Hashed based on process_id, region_id, commission_year and (for Selected assets)
611                // agent_id
612                self.0.process.id.hash(state);
613                self.0.region_id.hash(state);
614                self.0.commission_year.hash(state);
615                self.0.agent_id().hash(state);
616            }
617            AssetState::Future { .. } | AssetState::Decommissioned { .. } => {
618                // We shouldn't currently need to hash Future or Decommissioned assets
619                unimplemented!("Cannot hash Future or Decommissioned assets");
620            }
621        }
622    }
623}
624
625impl PartialOrd for AssetRef {
626    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
627        Some(self.cmp(other))
628    }
629}
630
631impl Ord for AssetRef {
632    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
633        self.id().unwrap().cmp(&other.id().unwrap())
634    }
635}
636
637/// A pool of [`Asset`]s
638pub struct AssetPool {
639    /// The pool of active assets, sorted by ID
640    active: Vec<AssetRef>,
641    /// Assets that have not yet been commissioned, sorted by commission year
642    future: Vec<Asset>,
643    /// Assets that have been decommissioned
644    decommissioned: Vec<AssetRef>,
645    /// Next available asset ID number
646    next_id: u32,
647}
648
649impl AssetPool {
650    /// Create a new [`AssetPool`]
651    pub fn new(mut assets: Vec<Asset>) -> Self {
652        // Sort in order of commission year
653        assets.sort_by(|a, b| a.commission_year.cmp(&b.commission_year));
654
655        Self {
656            active: Vec::new(),
657            future: assets,
658            decommissioned: Vec::new(),
659            next_id: 0,
660        }
661    }
662
663    /// Get the active pool as a slice of [`AssetRef`]s
664    pub fn as_slice(&self) -> &[AssetRef] {
665        &self.active
666    }
667
668    /// Decommission assets whose lifetime has passed and commission new assets
669    pub fn update_for_year(&mut self, year: u32) {
670        self.decommission_old(year);
671        self.commission_new(year);
672    }
673
674    /// Commission new assets for the specified milestone year from the input data
675    fn commission_new(&mut self, year: u32) {
676        // Count the number of assets to move
677        let count = self
678            .future
679            .iter()
680            .take_while(|asset| asset.commission_year <= year)
681            .count();
682
683        // Move assets from future to active
684        for mut asset in self.future.drain(0..count) {
685            // Ignore assets that have already been decommissioned
686            if asset.max_decommission_year() <= year {
687                warn!(
688                    "Asset '{}' with commission year {} and lifetime {} was decommissioned before \
689                    the start of the simulation",
690                    asset.process_id(),
691                    asset.commission_year,
692                    asset.process_parameter().lifetime
693                );
694                continue;
695            }
696
697            asset.commission(AssetID(self.next_id), "user input");
698            self.next_id += 1;
699            self.active.push(asset.into());
700        }
701    }
702
703    /// Decommission old assets for the specified milestone year
704    fn decommission_old(&mut self, year: u32) {
705        // Remove assets which are due for decommissioning
706        let to_decommission = self
707            .active
708            .extract_if(.., |asset| asset.max_decommission_year() <= year);
709
710        for mut asset in to_decommission {
711            // Set `decommission_year` and move to `self.decommissioned`
712            asset.make_mut().decommission(year, "end of life");
713            self.decommissioned.push(asset);
714        }
715    }
716
717    /// Decommission the specified assets if they are no longer in the active pool.
718    ///
719    /// # Arguments
720    ///
721    /// * `assets` - Assets to possibly decommission
722    /// * `year` - Decommissioning year
723    ///
724    /// # Panics
725    ///
726    /// Panics if any of the provided assets was never commissioned or has already been
727    /// decommissioned.
728    pub fn decommission_if_not_active<I>(&mut self, assets: I, year: u32)
729    where
730        I: IntoIterator<Item = AssetRef>,
731    {
732        let to_decommission = assets.into_iter().filter(|asset| {
733            // Get ID of the asset
734            let AssetState::Commissioned { id, .. } = &asset.state else {
735                panic!("Cannot decommission asset that has not been commissioned")
736            };
737
738            // Return true if asset **not** in active pool
739            !self.active.iter().any(|a| match &a.state {
740                AssetState::Commissioned { id: active_id, .. } => active_id == id,
741                _ => panic!("Active pool should only contain commissioned assets"),
742            })
743        });
744
745        for mut asset in to_decommission {
746            asset.make_mut().decommission(year, "not selected");
747            self.decommissioned.push(asset);
748        }
749    }
750
751    /// Get an asset with the specified ID.
752    ///
753    /// # Returns
754    ///
755    /// An [`AssetRef`] if found, else `None`. The asset may not be found if it has already been
756    /// decommissioned.
757    pub fn get(&self, id: AssetID) -> Option<&AssetRef> {
758        // The assets in `active` are in order of ID
759        let idx = self
760            .active
761            .binary_search_by(|asset| match &asset.state {
762                AssetState::Commissioned { id: asset_id, .. } => asset_id.cmp(&id),
763                _ => panic!("Active pool should only contain commissioned assets"),
764            })
765            .ok()?;
766
767        Some(&self.active[idx])
768    }
769
770    /// Iterate over active assets
771    pub fn iter_active(&self) -> slice::Iter<'_, AssetRef> {
772        self.active.iter()
773    }
774
775    /// Iterate over decommissioned assets
776    pub fn iter_decommissioned(&self) -> slice::Iter<'_, AssetRef> {
777        self.decommissioned.iter()
778    }
779
780    /// Iterate over all commissioned and decommissioned assets.
781    ///
782    /// NB: Not-yet-commissioned assets are not included.
783    pub fn iter_all(&self) -> impl Iterator<Item = &AssetRef> {
784        chain(self.iter_active(), self.iter_decommissioned())
785    }
786
787    /// Return current active pool and clear
788    pub fn take(&mut self) -> Vec<AssetRef> {
789        std::mem::take(&mut self.active)
790    }
791
792    /// Extend the active pool with Commissioned or Selected assets
793    pub fn extend<I>(&mut self, assets: I)
794    where
795        I: IntoIterator<Item = AssetRef>,
796    {
797        // Check all assets are either Commissioned or Selected, and, if the latter,
798        // then commission them
799        let assets = assets.into_iter().map(|mut asset| match &asset.state {
800            AssetState::Commissioned { .. } => asset,
801            AssetState::Selected { .. } => {
802                asset
803                    .make_mut()
804                    .commission(AssetID(self.next_id), "selected");
805                self.next_id += 1;
806                asset
807            }
808            _ => panic!(
809                "Cannot extend asset pool with asset in state {}. Only assets in \
810                Commissioned or Selected states are allowed.",
811                asset.state
812            ),
813        });
814
815        // New assets may not have been sorted, but active needs to be sorted by ID
816        self.active.extend(assets);
817        self.active.sort();
818
819        // Sanity check: all assets should be unique
820        debug_assert_eq!(self.active.iter().unique().count(), self.active.len());
821    }
822}
823
824/// Additional methods for iterating over assets
825pub trait AssetIterator<'a>: Iterator<Item = &'a AssetRef> + Sized
826where
827    Self: 'a,
828{
829    /// Filter assets by the agent that owns them
830    fn filter_agent(self, agent_id: &'a AgentID) -> impl Iterator<Item = &'a AssetRef> + 'a {
831        self.filter(move |asset| asset.agent_id() == Some(agent_id))
832    }
833
834    /// Iterate over assets that have the given commodity as a primary output
835    fn filter_primary_producers_of(
836        self,
837        commodity_id: &'a CommodityID,
838    ) -> impl Iterator<Item = &'a AssetRef> + 'a {
839        self.filter(move |asset| {
840            asset
841                .primary_output()
842                .is_some_and(|flow| &flow.commodity.id == commodity_id)
843        })
844    }
845
846    /// Filter the assets by region
847    fn filter_region(self, region_id: &'a RegionID) -> impl Iterator<Item = &'a AssetRef> + 'a {
848        self.filter(move |asset| asset.region_id == *region_id)
849    }
850
851    /// Iterate over process flows affecting the given commodity
852    fn flows_for_commodity(
853        self,
854        commodity_id: &'a CommodityID,
855    ) -> impl Iterator<Item = (&'a AssetRef, &'a ProcessFlow)> + 'a {
856        self.filter_map(|asset| Some((asset, asset.get_flow(commodity_id)?)))
857    }
858}
859
860impl<'a, I> AssetIterator<'a> for I where I: Iterator<Item = &'a AssetRef> + Sized + 'a {}
861
862#[cfg(test)]
863mod tests {
864    use super::*;
865    use crate::commodity::{Commodity, CommodityID, CommodityType};
866    use crate::fixture::{
867        assert_error, asset, commodity_id, process, process_parameter_map, region_id, time_slice,
868    };
869    use crate::process::{
870        FlowType, Process, ProcessActivityLimitsMap, ProcessFlow, ProcessFlowsMap,
871        ProcessParameter, ProcessParameterMap,
872    };
873    use crate::region::RegionID;
874    use crate::time_slice::{TimeSliceID, TimeSliceLevel};
875    use crate::units::{
876        ActivityPerCapacity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity,
877        MoneyPerCapacity, MoneyPerCapacityPerYear, MoneyPerFlow,
878    };
879    use indexmap::{IndexSet, indexmap, indexset};
880    use itertools::{Itertools, assert_equal};
881    use map_macro::hash_map;
882    use rstest::{fixture, rstest};
883    use std::collections::HashMap;
884    use std::iter;
885    use std::rc::Rc;
886
887    #[rstest]
888    fn test_get_input_cost_from_prices(
889        region_id: RegionID,
890        commodity_id: CommodityID,
891        mut process_parameter_map: ProcessParameterMap,
892        time_slice: TimeSliceID,
893    ) {
894        // Create a commodity
895        let commodity = Rc::new(Commodity {
896            id: commodity_id.clone(),
897            description: "Test commodity".to_string(),
898            kind: CommodityType::ServiceDemand,
899            time_slice_level: TimeSliceLevel::Annual,
900            levies: Default::default(),
901            demand: Default::default(),
902        });
903
904        // Create a process flow (input)
905        let flow = ProcessFlow {
906            commodity: commodity.clone(),
907            coeff: FlowPerActivity(-2.0), // input
908            kind: FlowType::Fixed,
909            cost: MoneyPerFlow(0.0),
910        };
911
912        // Insert process parameter for year 2020
913        process_parameter_map.insert(
914            (region_id.clone(), 2020),
915            Rc::new(ProcessParameter {
916                capital_cost: Default::default(),
917                fixed_operating_cost: Default::default(),
918                variable_operating_cost: Default::default(),
919                lifetime: 1,
920                discount_rate: Default::default(),
921            }),
922        );
923
924        // Create flows map
925        let flow_map = indexmap! { commodity_id.clone() => flow };
926        let flows = hash_map! {(region_id.clone(), 2020) => flow_map.into()};
927
928        // Create empty activity limits map
929        let activity_limits = hash_map! {(region_id.clone(), 2020) => Rc::new(HashMap::new())};
930
931        let process = Rc::new(Process {
932            id: ProcessID::from("PROC1"),
933            description: "Test process".to_string(),
934            flows,
935            parameters: process_parameter_map,
936            regions: indexset! {region_id.clone()},
937            primary_output: Some(commodity_id.clone()),
938            years: vec![2020],
939            activity_limits,
940            capacity_to_activity: ActivityPerCapacity(1.0),
941        });
942
943        // Create asset
944        let asset = Asset::new_candidate(process, region_id.clone(), Capacity(1.0), 2020).unwrap();
945
946        // Set input prices
947        let mut input_prices = CommodityPrices::default();
948        input_prices.insert(&commodity_id, &region_id, &time_slice, MoneyPerFlow(3.0));
949
950        // Call function
951        let cost = asset.get_input_cost_from_prices(&input_prices, &time_slice);
952        // Should be -coeff * price = -(-2.0) * 3.0 = 6.0
953        assert_eq!(cost.0, 6.0);
954    }
955
956    #[rstest]
957    #[case(Capacity(0.01))]
958    #[case(Capacity(0.5))]
959    #[case(Capacity(1.0))]
960    #[case(Capacity(100.0))]
961    fn test_asset_new_valid(process: Process, #[case] capacity: Capacity) {
962        let agent_id = AgentID("agent1".into());
963        let region_id = RegionID("GBR".into());
964        let asset = Asset::new_future(agent_id, process.into(), region_id, capacity, 2015).unwrap();
965        assert!(asset.id().is_none());
966    }
967
968    #[rstest]
969    #[case(Capacity(0.0))]
970    #[case(Capacity(-0.01))]
971    #[case(Capacity(-1.0))]
972    #[case(Capacity(f64::NAN))]
973    #[case(Capacity(f64::INFINITY))]
974    #[case(Capacity(f64::NEG_INFINITY))]
975    fn test_asset_new_invalid_capacity(process: Process, #[case] capacity: Capacity) {
976        let agent_id = AgentID("agent1".into());
977        let region_id = RegionID("GBR".into());
978        assert_error!(
979            Asset::new_future(agent_id, process.into(), region_id, capacity, 2015),
980            "Capacity must be a finite, positive number"
981        );
982    }
983
984    #[rstest]
985    fn test_asset_new_invalid_commission_year(process: Process) {
986        let agent_id = AgentID("agent1".into());
987        let region_id = RegionID("GBR".into());
988        assert_error!(
989            Asset::new_future(agent_id, process.into(), region_id, Capacity(1.0), 2009),
990            "Process process1 does not operate in the year 2009"
991        );
992    }
993
994    #[rstest]
995    fn test_asset_new_invalid_region(process: Process) {
996        let agent_id = AgentID("agent1".into());
997        let region_id = RegionID("FRA".into());
998        assert_error!(
999            Asset::new_future(agent_id, process.into(), region_id, Capacity(1.0), 2015),
1000            "Process process1 does not operate in region FRA"
1001        );
1002    }
1003
1004    #[fixture]
1005    fn asset_pool(region_id: RegionID) -> AssetPool {
1006        let process_param = Rc::new(ProcessParameter {
1007            capital_cost: MoneyPerCapacity(5.0),
1008            fixed_operating_cost: MoneyPerCapacityPerYear(2.0),
1009            variable_operating_cost: MoneyPerActivity(1.0),
1010            lifetime: 20,
1011            discount_rate: Dimensionless(0.9),
1012        });
1013        let years = RangeInclusive::new(2010, 2020).collect_vec();
1014        let process_parameter_map: ProcessParameterMap = years
1015            .iter()
1016            .map(|&year| ((region_id.clone(), year), process_param.clone()))
1017            .collect();
1018        let activity_limits = years
1019            .iter()
1020            .map(|&year| ((region_id.clone(), year), Rc::new(HashMap::new())))
1021            .collect();
1022        let flows = years
1023            .iter()
1024            .map(|&year| ((region_id.clone(), year), Rc::new(IndexMap::new())))
1025            .collect();
1026        let process = Rc::new(Process {
1027            id: "process1".into(),
1028            description: "Description".into(),
1029            years: vec![2010, 2020],
1030            activity_limits,
1031            flows,
1032            parameters: process_parameter_map,
1033            regions: IndexSet::from(["GBR".into()]),
1034            primary_output: None,
1035            capacity_to_activity: ActivityPerCapacity(1.0),
1036        });
1037        let future = [2020, 2010]
1038            .map(|year| {
1039                Asset::new_future(
1040                    "agent1".into(),
1041                    Rc::clone(&process),
1042                    "GBR".into(),
1043                    Capacity(1.0),
1044                    year,
1045                )
1046                .unwrap()
1047            })
1048            .into_iter()
1049            .collect_vec();
1050
1051        AssetPool::new(future)
1052    }
1053
1054    #[fixture]
1055    fn process_with_activity_limits(region_id: RegionID) -> Process {
1056        let process_param = Rc::new(ProcessParameter {
1057            capital_cost: MoneyPerCapacity(5.0),
1058            fixed_operating_cost: MoneyPerCapacityPerYear(2.0),
1059            variable_operating_cost: MoneyPerActivity(1.0),
1060            lifetime: 5,
1061            discount_rate: Dimensionless(0.9),
1062        });
1063        let years = RangeInclusive::new(2010, 2020).collect_vec();
1064        let process_parameter_map: ProcessParameterMap = years
1065            .iter()
1066            .map(|&year| ((region_id.clone(), year), process_param.clone()))
1067            .collect();
1068        let time_slice = TimeSliceID {
1069            season: "winter".into(),
1070            time_of_day: "day".into(),
1071        };
1072        let fraction_limits = Dimensionless(1.0)..=Dimensionless(2.0);
1073        let mut flows = ProcessFlowsMap::new();
1074        let mut activity_limits = ProcessActivityLimitsMap::new();
1075        let limit_map = Rc::new(hash_map! {time_slice => fraction_limits});
1076        for year in [2010, 2020] {
1077            // empty flows map, but this is fine for our purposes
1078            flows.insert((region_id.clone(), year), Rc::new(IndexMap::new()));
1079            activity_limits.insert((region_id.clone(), year), limit_map.clone());
1080        }
1081        Process {
1082            id: "process1".into(),
1083            description: "Description".into(),
1084            years: vec![2010, 2020],
1085            activity_limits,
1086            flows,
1087            parameters: process_parameter_map,
1088            regions: IndexSet::from([region_id]),
1089            primary_output: None,
1090            capacity_to_activity: ActivityPerCapacity(3.0),
1091        }
1092    }
1093
1094    #[fixture]
1095    fn asset_with_activity_limits(process_with_activity_limits: Process) -> Asset {
1096        Asset::new_future(
1097            "agent1".into(),
1098            Rc::new(process_with_activity_limits),
1099            "GBR".into(),
1100            Capacity(2.0),
1101            2010,
1102        )
1103        .unwrap()
1104    }
1105
1106    #[rstest]
1107    fn test_asset_get_activity_limits(asset_with_activity_limits: Asset, time_slice: TimeSliceID) {
1108        assert_eq!(
1109            asset_with_activity_limits.get_activity_limits(&time_slice),
1110            Activity(6.0)..=Activity(12.0)
1111        );
1112    }
1113
1114    #[rstest]
1115    fn test_asset_get_activity_per_capacity_limits(
1116        asset_with_activity_limits: Asset,
1117        time_slice: TimeSliceID,
1118    ) {
1119        assert_eq!(
1120            asset_with_activity_limits.get_activity_per_capacity_limits(&time_slice),
1121            ActivityPerCapacity(3.0)..=ActivityPerCapacity(6.0)
1122        );
1123    }
1124
1125    #[rstest]
1126    fn test_asset_pool_new(asset_pool: AssetPool) {
1127        // Should be in order of commission year
1128        assert!(asset_pool.active.is_empty());
1129        assert!(asset_pool.future.len() == 2);
1130        assert!(asset_pool.future[0].commission_year == 2010);
1131        assert!(asset_pool.future[1].commission_year == 2020);
1132    }
1133
1134    #[rstest]
1135    fn test_asset_pool_commission_new1(mut asset_pool: AssetPool) {
1136        // Asset to be commissioned in this year
1137        asset_pool.commission_new(2010);
1138        assert_equal(asset_pool.iter_active(), iter::once(&asset_pool.active[0]));
1139    }
1140
1141    #[rstest]
1142    fn test_asset_pool_commission_new2(mut asset_pool: AssetPool) {
1143        // Commission year has passed
1144        asset_pool.commission_new(2011);
1145        assert_equal(asset_pool.iter_active(), iter::once(&asset_pool.active[0]));
1146    }
1147
1148    #[rstest]
1149    fn test_asset_pool_commission_new3(mut asset_pool: AssetPool) {
1150        // Nothing to commission for this year
1151        asset_pool.commission_new(2000);
1152        assert!(asset_pool.iter_active().next().is_none()); // no active assets
1153    }
1154
1155    #[rstest]
1156    fn test_asset_pool_commission_already_decommissioned(asset: Asset) {
1157        let year = asset.max_decommission_year();
1158        let mut asset_pool = AssetPool::new(vec![asset]);
1159        assert!(asset_pool.active.is_empty());
1160        asset_pool.update_for_year(year);
1161        assert!(asset_pool.active.is_empty());
1162    }
1163
1164    #[rstest]
1165    fn test_asset_pool_decommission_old(mut asset_pool: AssetPool) {
1166        asset_pool.commission_new(2020);
1167        assert!(asset_pool.future.is_empty());
1168        assert_eq!(asset_pool.active.len(), 2);
1169        asset_pool.decommission_old(2030); // should decommission first asset (lifetime == 5)
1170        assert_eq!(asset_pool.active.len(), 1);
1171        assert_eq!(asset_pool.active[0].commission_year, 2020);
1172        assert_eq!(asset_pool.decommissioned.len(), 1);
1173        assert_eq!(asset_pool.decommissioned[0].commission_year, 2010);
1174        assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2030));
1175        asset_pool.decommission_old(2032); // nothing to decommission
1176        assert_eq!(asset_pool.active.len(), 1);
1177        assert_eq!(asset_pool.active[0].commission_year, 2020);
1178        assert_eq!(asset_pool.decommissioned.len(), 1);
1179        assert_eq!(asset_pool.decommissioned[0].commission_year, 2010);
1180        assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2030));
1181        asset_pool.decommission_old(2040); // should decommission second asset
1182        assert!(asset_pool.active.is_empty());
1183        assert_eq!(asset_pool.decommissioned.len(), 2);
1184        assert_eq!(asset_pool.decommissioned[0].commission_year, 2010);
1185        assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2030));
1186        assert_eq!(asset_pool.decommissioned[1].commission_year, 2020);
1187        assert_eq!(asset_pool.decommissioned[1].decommission_year(), Some(2040));
1188    }
1189
1190    #[rstest]
1191    fn test_asset_pool_get(mut asset_pool: AssetPool) {
1192        asset_pool.commission_new(2020);
1193        assert_eq!(asset_pool.get(AssetID(0)), Some(&asset_pool.active[0]));
1194        assert_eq!(asset_pool.get(AssetID(1)), Some(&asset_pool.active[1]));
1195    }
1196
1197    #[rstest]
1198    fn test_asset_pool_extend_empty(mut asset_pool: AssetPool) {
1199        // Start with commissioned assets
1200        asset_pool.commission_new(2020);
1201        let original_count = asset_pool.active.len();
1202
1203        // Extend with empty iterator
1204        asset_pool.extend(Vec::<AssetRef>::new());
1205
1206        assert_eq!(asset_pool.active.len(), original_count);
1207    }
1208
1209    #[rstest]
1210    fn test_asset_pool_extend_existing_assets(mut asset_pool: AssetPool) {
1211        // Start with some commissioned assets
1212        asset_pool.commission_new(2020);
1213        assert_eq!(asset_pool.active.len(), 2);
1214        let existing_assets = asset_pool.take();
1215
1216        // Extend with the same assets (should maintain their IDs)
1217        asset_pool.extend(existing_assets.clone());
1218
1219        assert_eq!(asset_pool.active.len(), 2);
1220        assert_eq!(asset_pool.active[0].id(), Some(AssetID(0)));
1221        assert_eq!(asset_pool.active[1].id(), Some(AssetID(1)));
1222    }
1223
1224    #[rstest]
1225    fn test_asset_pool_extend_new_assets(mut asset_pool: AssetPool, process: Process) {
1226        // Start with some commissioned assets
1227        asset_pool.commission_new(2020);
1228        let original_count = asset_pool.active.len();
1229
1230        // Create new non-commissioned assets
1231        let process_rc = Rc::new(process);
1232        let new_assets = vec![
1233            Asset::new_selected(
1234                "agent2".into(),
1235                Rc::clone(&process_rc),
1236                "GBR".into(),
1237                Capacity(1.5),
1238                2015,
1239            )
1240            .unwrap()
1241            .into(),
1242            Asset::new_selected(
1243                "agent3".into(),
1244                Rc::clone(&process_rc),
1245                "GBR".into(),
1246                Capacity(2.5),
1247                2020,
1248            )
1249            .unwrap()
1250            .into(),
1251        ];
1252
1253        asset_pool.extend(new_assets);
1254
1255        assert_eq!(asset_pool.active.len(), original_count + 2);
1256        // New assets should get IDs 2 and 3
1257        assert_eq!(asset_pool.active[original_count].id(), Some(AssetID(2)));
1258        assert_eq!(asset_pool.active[original_count + 1].id(), Some(AssetID(3)));
1259        assert_eq!(
1260            asset_pool.active[original_count].agent_id(),
1261            Some(&"agent2".into())
1262        );
1263        assert_eq!(
1264            asset_pool.active[original_count + 1].agent_id(),
1265            Some(&"agent3".into())
1266        );
1267    }
1268
1269    #[rstest]
1270    fn test_asset_pool_extend_mixed_assets(mut asset_pool: AssetPool, process: Process) {
1271        // Start with some commissioned assets
1272        asset_pool.commission_new(2020);
1273
1274        // Create a new non-commissioned asset
1275        let new_asset = Asset::new_selected(
1276            "agent_new".into(),
1277            process.into(),
1278            "GBR".into(),
1279            Capacity(3.0),
1280            2015,
1281        )
1282        .unwrap()
1283        .into();
1284
1285        // Extend with just the new asset (not mixing with existing to avoid duplicates)
1286        asset_pool.extend(vec![new_asset]);
1287
1288        assert_eq!(asset_pool.active.len(), 3);
1289        // Check that we have the original assets plus the new one
1290        assert!(asset_pool.active.iter().any(|a| a.id() == Some(AssetID(0))));
1291        assert!(asset_pool.active.iter().any(|a| a.id() == Some(AssetID(1))));
1292        assert!(asset_pool.active.iter().any(|a| a.id() == Some(AssetID(2))));
1293        // Check that the new asset has the correct agent
1294        assert!(
1295            asset_pool
1296                .active
1297                .iter()
1298                .any(|a| a.agent_id() == Some(&"agent_new".into()))
1299        );
1300    }
1301
1302    #[rstest]
1303    fn test_asset_pool_extend_maintains_sort_order(mut asset_pool: AssetPool, process: Process) {
1304        // Start with some commissioned assets
1305        asset_pool.commission_new(2020);
1306
1307        // Create new assets that would be out of order if added at the end
1308        let process_rc = Rc::new(process);
1309        let new_assets = vec![
1310            Asset::new_selected(
1311                "agent_high_id".into(),
1312                Rc::clone(&process_rc),
1313                "GBR".into(),
1314                Capacity(1.0),
1315                2010,
1316            )
1317            .unwrap()
1318            .into(),
1319            Asset::new_selected(
1320                "agent_low_id".into(),
1321                Rc::clone(&process_rc),
1322                "GBR".into(),
1323                Capacity(1.0),
1324                2015,
1325            )
1326            .unwrap()
1327            .into(),
1328        ];
1329
1330        asset_pool.extend(new_assets);
1331
1332        // Check that assets are sorted by ID
1333        let ids: Vec<u32> = asset_pool
1334            .iter_active()
1335            .map(|a| a.id().unwrap().0)
1336            .collect();
1337        assert_equal(ids, 0..4);
1338    }
1339
1340    #[rstest]
1341    fn test_asset_pool_extend_no_duplicates_expected(mut asset_pool: AssetPool) {
1342        // Start with some commissioned assets
1343        asset_pool.commission_new(2020);
1344        let original_count = asset_pool.active.len();
1345
1346        // The extend method expects unique assets - adding duplicates would violate
1347        // the debug assertion, so this test verifies the normal case
1348        asset_pool.extend(Vec::new());
1349
1350        assert_eq!(asset_pool.active.len(), original_count);
1351        // Verify all assets are still unique (this is what the debug_assert checks)
1352        assert_eq!(
1353            asset_pool.active.iter().unique().count(),
1354            asset_pool.active.len()
1355        );
1356    }
1357
1358    #[rstest]
1359    fn test_asset_pool_extend_increments_next_id(mut asset_pool: AssetPool, process: Process) {
1360        // Start with some commissioned assets
1361        asset_pool.commission_new(2020);
1362        assert_eq!(asset_pool.next_id, 2); // Should be 2 after commissioning 2 assets
1363
1364        // Create new non-commissioned assets
1365        let process_rc = Rc::new(process);
1366        let new_assets = vec![
1367            Asset::new_selected(
1368                "agent1".into(),
1369                Rc::clone(&process_rc),
1370                "GBR".into(),
1371                Capacity(1.0),
1372                2015,
1373            )
1374            .unwrap()
1375            .into(),
1376            Asset::new_selected(
1377                "agent2".into(),
1378                Rc::clone(&process_rc),
1379                "GBR".into(),
1380                Capacity(1.0),
1381                2020,
1382            )
1383            .unwrap()
1384            .into(),
1385        ];
1386
1387        asset_pool.extend(new_assets);
1388
1389        // next_id should have incremented for each new asset
1390        assert_eq!(asset_pool.next_id, 4);
1391        assert_eq!(asset_pool.active[2].id(), Some(AssetID(2)));
1392        assert_eq!(asset_pool.active[3].id(), Some(AssetID(3)));
1393    }
1394
1395    #[rstest]
1396    fn test_asset_pool_decommission_if_not_active(mut asset_pool: AssetPool) {
1397        // Commission some assets
1398        asset_pool.commission_new(2020);
1399        assert_eq!(asset_pool.active.len(), 2);
1400        assert_eq!(asset_pool.decommissioned.len(), 0);
1401
1402        // Remove one asset from the active pool (simulating it being removed elsewhere)
1403        let removed_asset = asset_pool.active.remove(0);
1404        assert_eq!(asset_pool.active.len(), 1);
1405
1406        // Try to decommission both the removed asset (not in active) and an active asset
1407        let assets_to_check = vec![removed_asset.clone(), asset_pool.active[0].clone()];
1408        asset_pool.decommission_if_not_active(assets_to_check, 2025);
1409
1410        // Only the removed asset should be decommissioned (since it's not in active pool)
1411        assert_eq!(asset_pool.active.len(), 1); // Active pool unchanged
1412        assert_eq!(asset_pool.decommissioned.len(), 1);
1413        assert_eq!(asset_pool.decommissioned[0].id(), removed_asset.id());
1414        assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2025));
1415    }
1416
1417    #[rstest]
1418    fn test_asset_pool_decommission_if_not_active_all_active(mut asset_pool: AssetPool) {
1419        // Commission some assets
1420        asset_pool.commission_new(2020);
1421        assert_eq!(asset_pool.active.len(), 2);
1422        assert_eq!(asset_pool.decommissioned.len(), 0);
1423
1424        // Try to decommission assets that are all still in the active pool
1425        let assets_to_check = asset_pool.active.clone();
1426        asset_pool.decommission_if_not_active(assets_to_check, 2025);
1427
1428        // Nothing should be decommissioned since all assets are still active
1429        assert_eq!(asset_pool.active.len(), 2);
1430        assert_eq!(asset_pool.decommissioned.len(), 0);
1431    }
1432
1433    #[rstest]
1434    fn test_asset_pool_decommission_if_not_active_none_active(mut asset_pool: AssetPool) {
1435        // Commission some assets
1436        asset_pool.commission_new(2020);
1437        let all_assets = asset_pool.active.clone();
1438
1439        // Clear the active pool (simulating all assets being removed)
1440        asset_pool.active.clear();
1441
1442        // Try to decommission the assets that are no longer active
1443        asset_pool.decommission_if_not_active(all_assets.clone(), 2025);
1444
1445        // All assets should be decommissioned since none are in active pool
1446        assert_eq!(asset_pool.active.len(), 0);
1447        assert_eq!(asset_pool.decommissioned.len(), 2);
1448        assert_eq!(asset_pool.decommissioned[0].id(), all_assets[0].id());
1449        assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2025));
1450        assert_eq!(asset_pool.decommissioned[1].id(), all_assets[1].id());
1451        assert_eq!(asset_pool.decommissioned[1].decommission_year(), Some(2025));
1452    }
1453
1454    #[rstest]
1455    #[should_panic(expected = "Cannot decommission asset that has not been commissioned")]
1456    fn test_asset_pool_decommission_if_not_active_non_commissioned_asset(
1457        mut asset_pool: AssetPool,
1458        process: Process,
1459    ) {
1460        // Create a non-commissioned asset
1461        let non_commissioned_asset = Asset::new_future(
1462            "agent_new".into(),
1463            process.into(),
1464            "GBR".into(),
1465            Capacity(1.0),
1466            2015,
1467        )
1468        .unwrap()
1469        .into();
1470
1471        // This should panic because the asset was never commissioned
1472        asset_pool.decommission_if_not_active(vec![non_commissioned_asset], 2025);
1473    }
1474
1475    #[rstest]
1476    fn test_asset_commission(process: Process) {
1477        // Test successful commissioning of Future asset
1478        let process_rc = Rc::new(process);
1479        let mut asset1 = Asset::new_future(
1480            "agent1".into(),
1481            Rc::clone(&process_rc),
1482            "GBR".into(),
1483            Capacity(1.0),
1484            2020,
1485        )
1486        .unwrap();
1487        asset1.commission(AssetID(1), "");
1488        assert!(asset1.is_commissioned());
1489        assert_eq!(asset1.id(), Some(AssetID(1)));
1490
1491        // Test successful commissioning of Selected asset
1492        let mut asset2 = Asset::new_selected(
1493            "agent1".into(),
1494            Rc::clone(&process_rc),
1495            "GBR".into(),
1496            Capacity(1.0),
1497            2020,
1498        )
1499        .unwrap();
1500        asset2.commission(AssetID(2), "");
1501        assert!(asset2.is_commissioned());
1502        assert_eq!(asset2.id(), Some(AssetID(2)));
1503    }
1504
1505    #[rstest]
1506    #[case::early_decommission_within_lifetime(2024, 2024)]
1507    #[case::decommission_at_maximum_year(2026, 2025)]
1508    fn test_asset_decommission(
1509        #[case] requested_decommission_year: u32,
1510        #[case] expected_decommission_year: u32,
1511        process: Process,
1512    ) {
1513        // Test successful commissioning of Future asset
1514        let process_rc = Rc::new(process);
1515        let mut asset = Asset::new_future(
1516            "agent1".into(),
1517            Rc::clone(&process_rc),
1518            "GBR".into(),
1519            Capacity(1.0),
1520            2020,
1521        )
1522        .unwrap();
1523        asset.commission(AssetID(1), "");
1524        assert!(asset.is_commissioned());
1525        assert_eq!(asset.id(), Some(AssetID(1)));
1526
1527        // Test successful decommissioning
1528        asset.decommission(requested_decommission_year, "");
1529        assert!(!asset.is_commissioned());
1530        assert_eq!(asset.decommission_year(), Some(expected_decommission_year));
1531    }
1532
1533    #[rstest]
1534    #[should_panic(expected = "Assets with state Candidate cannot be commissioned")]
1535    fn test_commission_wrong_states(process: Process) {
1536        let mut asset =
1537            Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap();
1538        asset.commission(AssetID(1), "");
1539    }
1540
1541    #[rstest]
1542    #[should_panic(expected = "Cannot decommission an asset that hasn't been commissioned")]
1543    fn test_decommission_wrong_state(process: Process) {
1544        let mut asset =
1545            Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap();
1546        asset.decommission(2025, "");
1547    }
1548}