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