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, CommodityType};
4use crate::finance::annual_capital_cost;
5use crate::process::{
6    ActivityLimits, FlowDirection, Process, ProcessFlow, ProcessID, ProcessParameter,
7};
8use crate::region::RegionID;
9use crate::simulation::CommodityPrices;
10use crate::time_slice::{TimeSliceID, TimeSliceSelection};
11use crate::units::{
12    Activity, ActivityPerCapacity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity,
13    MoneyPerCapacity, MoneyPerFlow,
14};
15use anyhow::{Context, Result, ensure};
16use indexmap::IndexMap;
17use itertools::{Itertools, chain};
18use log::{debug, warn};
19use serde::{Deserialize, Serialize};
20use std::cmp::{Ordering, min};
21use std::hash::{Hash, Hasher};
22use std::ops::{Add, Deref, RangeInclusive, Sub};
23use std::rc::Rc;
24use std::slice;
25
26/// A unique identifier for an asset
27#[derive(
28    Clone,
29    Copy,
30    Debug,
31    derive_more::Display,
32    Eq,
33    Hash,
34    Ord,
35    PartialEq,
36    PartialOrd,
37    Deserialize,
38    Serialize,
39)]
40pub struct AssetID(u32);
41
42/// A unique identifier for an asset group
43#[derive(
44    Clone,
45    Copy,
46    Debug,
47    derive_more::Display,
48    Eq,
49    Hash,
50    Ord,
51    PartialEq,
52    PartialOrd,
53    Deserialize,
54    Serialize,
55)]
56pub struct AssetGroupID(u32);
57
58/// The state of an asset
59///
60/// New assets are created as either `Future` or `Candidate` assets. `Future` assets (which are
61/// specified in the input data) have a fixed capacity and capital costs already accounted for,
62/// whereas `Candidate` assets capital costs are not yet accounted for, and their capacity is
63/// determined by the investment algorithm.
64///
65/// `Future` and `Candidate` assets can be converted to `Commissioned` assets by calling
66/// the `commission` method (or via pool operations that commission future/selected assets).
67///
68/// `Commissioned` assets can be decommissioned by calling `decommission`.
69#[derive(Clone, Debug, PartialEq, strum::Display)]
70pub enum AssetState {
71    /// The asset has been commissioned
72    Commissioned {
73        /// The ID of the asset
74        id: AssetID,
75        /// The ID of the agent that owns the asset
76        agent_id: AgentID,
77        /// Year in which the asset was mothballed. None, if it is not mothballed
78        mothballed_year: Option<u32>,
79        /// ID of the asset group, if any. None, if this asset is not resulting from dividing a parent
80        group_id: Option<AssetGroupID>,
81    },
82    /// The asset has been decommissioned
83    Decommissioned {
84        /// The ID of the asset
85        id: AssetID,
86        /// The ID of the agent that owned the asset
87        agent_id: AgentID,
88        /// The year the asset was decommissioned
89        decommission_year: u32,
90    },
91    /// The asset is planned for commissioning in the future
92    Future {
93        /// The ID of the agent that will own the asset
94        agent_id: AgentID,
95    },
96    /// The asset has been selected for investment, but not yet confirmed
97    Selected {
98        /// The ID of the agent that would own the asset
99        agent_id: AgentID,
100    },
101    /// The asset is a candidate for investment but has not yet been selected by an agent
102    Candidate,
103}
104
105/// Capacity of an asset, which may be continuous or a discrete number of indivisible units
106#[derive(Clone, PartialEq, Copy, Debug)]
107pub enum AssetCapacity {
108    /// Continuous capacity
109    Continuous(Capacity),
110    /// Discrete capacity represented by a number of indivisible units
111    /// Stores: (number of units, unit size)
112    Discrete(u32, Capacity),
113}
114
115impl Add for AssetCapacity {
116    type Output = Self;
117
118    // Add two AssetCapacity values together
119    fn add(self, rhs: AssetCapacity) -> Self {
120        match (self, rhs) {
121            (AssetCapacity::Continuous(cap1), AssetCapacity::Continuous(cap2)) => {
122                AssetCapacity::Continuous(cap1 + cap2)
123            }
124            (AssetCapacity::Discrete(units1, size1), AssetCapacity::Discrete(units2, size2)) => {
125                Self::check_same_unit_size(size1, size2);
126                AssetCapacity::Discrete(units1 + units2, size1)
127            }
128            _ => panic!("Cannot add different types of AssetCapacity ({self:?} and {rhs:?})"),
129        }
130    }
131}
132
133impl Sub for AssetCapacity {
134    type Output = Self;
135
136    // Subtract rhs from self, ensuring that the result is non-negative
137    fn sub(self, rhs: AssetCapacity) -> Self {
138        match (self, rhs) {
139            (AssetCapacity::Continuous(cap1), AssetCapacity::Continuous(cap2)) => {
140                AssetCapacity::Continuous((cap1 - cap2).max(Capacity(0.0)))
141            }
142            (AssetCapacity::Discrete(units1, size1), AssetCapacity::Discrete(units2, size2)) => {
143                Self::check_same_unit_size(size1, size2);
144                AssetCapacity::Discrete(units1 - units2.min(units1), size1)
145            }
146            _ => panic!("Cannot subtract different types of AssetCapacity ({self:?} and {rhs:?})"),
147        }
148    }
149}
150
151impl Eq for AssetCapacity {}
152
153impl PartialOrd for AssetCapacity {
154    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
155        Some(self.cmp(other))
156    }
157}
158
159impl Ord for AssetCapacity {
160    fn cmp(&self, other: &Self) -> Ordering {
161        match (self, other) {
162            (AssetCapacity::Continuous(a), AssetCapacity::Continuous(b)) => a.total_cmp(b),
163            (AssetCapacity::Discrete(units1, size1), AssetCapacity::Discrete(units2, size2)) => {
164                Self::check_same_unit_size(*size1, *size2);
165                units1.cmp(units2)
166            }
167            _ => panic!("Cannot compare different types of AssetCapacity ({self:?} and {other:?})"),
168        }
169    }
170}
171
172impl AssetCapacity {
173    /// Validates that two discrete capacities have the same unit size.
174    fn check_same_unit_size(size1: Capacity, size2: Capacity) {
175        assert_eq!(
176            size1, size2,
177            "Can't perform operation on capacities with different unit sizes ({size1} and {size2})",
178        );
179    }
180
181    /// Create an `AssetCapacity` from a total capacity and optional unit size
182    ///
183    /// If a unit size is provided, the capacity is represented as a discrete number of units,
184    /// calculated as the ceiling of (capacity / `unit_size`). If no unit size is provided, the
185    /// capacity is represented as continuous.
186    pub fn from_capacity(capacity: Capacity, unit_size: Option<Capacity>) -> Self {
187        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
188        match unit_size {
189            Some(size) => {
190                let num_units = (capacity / size).value().ceil() as u32;
191                AssetCapacity::Discrete(num_units, size)
192            }
193            None => AssetCapacity::Continuous(capacity),
194        }
195    }
196
197    /// Returns the total capacity represented by this `AssetCapacity`.
198    pub fn total_capacity(&self) -> Capacity {
199        match self {
200            AssetCapacity::Continuous(cap) => *cap,
201            AssetCapacity::Discrete(units, size) => *size * Dimensionless(*units as f64),
202        }
203    }
204
205    /// Returns the number of units if this is a discrete capacity, or `None` if continuous.
206    pub fn n_units(&self) -> Option<u32> {
207        match self {
208            AssetCapacity::Continuous(_) => None,
209            AssetCapacity::Discrete(units, _) => Some(*units),
210        }
211    }
212
213    /// Asserts that both capacities are the same type (both continuous or both discrete).
214    pub fn assert_same_type(&self, other: AssetCapacity) {
215        assert!(
216            matches!(self, AssetCapacity::Continuous(_))
217                == matches!(other, AssetCapacity::Continuous(_)),
218            "Cannot change capacity type"
219        );
220    }
221
222    /// Applies a limit factor to the capacity, scaling it accordingly.
223    ///
224    /// For discrete capacities, the number of units is scaled by the limit factor and rounded up to
225    /// the nearest integer.
226    pub fn apply_limit_factor(self, limit_factor: Dimensionless) -> Self {
227        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
228        match self {
229            AssetCapacity::Continuous(cap) => AssetCapacity::Continuous(cap * limit_factor),
230            AssetCapacity::Discrete(units, size) => {
231                let new_units = (units as f64 * limit_factor.value()).ceil() as u32;
232                AssetCapacity::Discrete(new_units, size)
233            }
234        }
235    }
236}
237
238/// An asset controlled by an agent.
239#[derive(Clone, PartialEq)]
240pub struct Asset {
241    /// The status of the asset
242    state: AssetState,
243    /// The [`Process`] that this asset corresponds to
244    process: Rc<Process>,
245    /// Activity limits for this asset
246    activity_limits: Rc<ActivityLimits>,
247    /// The commodity flows for this asset
248    flows: Rc<IndexMap<CommodityID, ProcessFlow>>,
249    /// The [`ProcessParameter`] corresponding to the asset's region and commission year
250    process_parameter: Rc<ProcessParameter>,
251    /// The region in which the asset is located
252    region_id: RegionID,
253    /// Capacity of asset (for candidates this is a hypothetical capacity which may be altered)
254    capacity: AssetCapacity,
255    /// The year the asset was/will be commissioned
256    commission_year: u32,
257    /// The maximum year that the asset could be decommissioned
258    max_decommission_year: u32,
259}
260
261impl Asset {
262    /// Create a new candidate asset
263    pub fn new_candidate(
264        process: Rc<Process>,
265        region_id: RegionID,
266        capacity: Capacity,
267        commission_year: u32,
268    ) -> Result<Self> {
269        let unit_size = process.unit_size;
270        Self::new_with_state(
271            AssetState::Candidate,
272            process,
273            region_id,
274            AssetCapacity::from_capacity(capacity, unit_size),
275            commission_year,
276            None,
277        )
278    }
279
280    /// Create a new candidate for use in dispatch runs
281    ///
282    /// These candidates will have a single continuous capacity specified by the model parameter
283    /// `candidate_asset_capacity`, regardless of whether the underlying process is divisible or
284    /// not.
285    pub fn new_candidate_for_dispatch(
286        process: Rc<Process>,
287        region_id: RegionID,
288        capacity: Capacity,
289        commission_year: u32,
290    ) -> Result<Self> {
291        Self::new_with_state(
292            AssetState::Candidate,
293            process,
294            region_id,
295            AssetCapacity::Continuous(capacity),
296            commission_year,
297            None,
298        )
299    }
300
301    /// Create a new candidate asset from a commissioned asset
302    pub fn new_candidate_from_commissioned(asset: &Asset) -> Self {
303        assert!(asset.is_commissioned(), "Asset must be commissioned");
304
305        Self {
306            state: AssetState::Candidate,
307            ..asset.clone()
308        }
309    }
310
311    /// Create a new future asset
312    pub fn new_future_with_max_decommission(
313        agent_id: AgentID,
314        process: Rc<Process>,
315        region_id: RegionID,
316        capacity: Capacity,
317        commission_year: u32,
318        max_decommission_year: Option<u32>,
319    ) -> Result<Self> {
320        check_capacity_valid_for_asset(capacity)?;
321        let unit_size = process.unit_size;
322        Self::new_with_state(
323            AssetState::Future { agent_id },
324            process,
325            region_id,
326            AssetCapacity::from_capacity(capacity, unit_size),
327            commission_year,
328            max_decommission_year,
329        )
330    }
331
332    /// Create a new future asset
333    pub fn new_future(
334        agent_id: AgentID,
335        process: Rc<Process>,
336        region_id: RegionID,
337        capacity: Capacity,
338        commission_year: u32,
339    ) -> Result<Self> {
340        Self::new_future_with_max_decommission(
341            agent_id,
342            process,
343            region_id,
344            capacity,
345            commission_year,
346            None,
347        )
348    }
349
350    /// Create a new selected asset
351    ///
352    /// This is only used for testing. In the real program, Selected assets can only be created from
353    /// Candidate assets by calling `select_candidate_for_investment`.
354    #[cfg(test)]
355    fn new_selected(
356        agent_id: AgentID,
357        process: Rc<Process>,
358        region_id: RegionID,
359        capacity: Capacity,
360        commission_year: u32,
361    ) -> Result<Self> {
362        let unit_size = process.unit_size;
363        Self::new_with_state(
364            AssetState::Selected { agent_id },
365            process,
366            region_id,
367            AssetCapacity::from_capacity(capacity, unit_size),
368            commission_year,
369            None,
370        )
371    }
372
373    /// Create a new commissioned asset
374    ///
375    /// This is only used for testing. WARNING: These assets always have an ID of zero, so can
376    /// create hash collisions. Use with care.
377    #[cfg(test)]
378    pub fn new_commissioned(
379        agent_id: AgentID,
380        process: Rc<Process>,
381        region_id: RegionID,
382        capacity: Capacity,
383        commission_year: u32,
384    ) -> Result<Self> {
385        let unit_size = process.unit_size;
386        Self::new_with_state(
387            AssetState::Commissioned {
388                id: AssetID(0),
389                agent_id,
390                mothballed_year: None,
391                group_id: None,
392            },
393            process,
394            region_id,
395            AssetCapacity::from_capacity(capacity, unit_size),
396            commission_year,
397            None,
398        )
399    }
400
401    /// Private helper to create an asset with the given state
402    fn new_with_state(
403        state: AssetState,
404        process: Rc<Process>,
405        region_id: RegionID,
406        capacity: AssetCapacity,
407        commission_year: u32,
408        max_decommission_year: Option<u32>,
409    ) -> Result<Self> {
410        check_region_year_valid_for_process(&process, &region_id, commission_year)?;
411        ensure!(
412            capacity.total_capacity() >= Capacity(0.0),
413            "Capacity must be non-negative"
414        );
415
416        // There should be activity limits, commodity flows and process parameters for all
417        // **milestone** years, but it is possible to have assets that are commissioned before the
418        // simulation start from assets.csv. We check for the presence of the params lazily to
419        // prevent users having to supply them for all the possible valid years before the time
420        // horizon.
421        let key = (region_id.clone(), commission_year);
422        let activity_limits = process
423            .activity_limits
424            .get(&key)
425            .with_context(|| {
426                format!(
427                    "No process availabilities supplied for process {} in region {} in year {}. \
428                    You should update process_availabilities.csv.",
429                    &process.id, region_id, commission_year
430                )
431            })?
432            .clone();
433        let flows = process
434            .flows
435            .get(&key)
436            .with_context(|| {
437                format!(
438                    "No commodity flows supplied for process {} in region {} in year {}. \
439                    You should update process_flows.csv.",
440                    &process.id, region_id, commission_year
441                )
442            })?
443            .clone();
444        let process_parameter = process
445            .parameters
446            .get(&key)
447            .with_context(|| {
448                format!(
449                    "No process parameters supplied for process {} in region {} in year {}. \
450                    You should update process_parameters.csv.",
451                    &process.id, region_id, commission_year
452                )
453            })?
454            .clone();
455
456        let max_decommission_year =
457            max_decommission_year.unwrap_or(commission_year + process_parameter.lifetime);
458        ensure!(
459            max_decommission_year >= commission_year,
460            "Max decommission year must be after/same as commission year"
461        );
462
463        Ok(Self {
464            state,
465            process,
466            activity_limits,
467            flows,
468            process_parameter,
469            region_id,
470            capacity,
471            commission_year,
472            max_decommission_year,
473        })
474    }
475
476    /// Get the state of this asset
477    pub fn state(&self) -> &AssetState {
478        &self.state
479    }
480
481    /// The process parameter for this asset
482    pub fn process_parameter(&self) -> &ProcessParameter {
483        &self.process_parameter
484    }
485
486    /// The last year in which this asset should be decommissioned
487    pub fn max_decommission_year(&self) -> u32 {
488        self.max_decommission_year
489    }
490
491    /// Get the activity limits per unit of capacity for this asset in a particular time slice
492    pub fn get_activity_per_capacity_limits(
493        &self,
494        time_slice: &TimeSliceID,
495    ) -> RangeInclusive<ActivityPerCapacity> {
496        let limits = &self.activity_limits.get_limit_for_time_slice(time_slice);
497        let cap2act = self.process.capacity_to_activity;
498        (cap2act * *limits.start())..=(cap2act * *limits.end())
499    }
500
501    /// Get the activity limits for this asset for a given time slice selection
502    pub fn get_activity_limits_for_selection(
503        &self,
504        time_slice_selection: &TimeSliceSelection,
505    ) -> RangeInclusive<Activity> {
506        let activity_per_capacity_limits = self.activity_limits.get_limit(time_slice_selection);
507        let cap2act = self.process.capacity_to_activity;
508        let max_activity = self.capacity.total_capacity() * cap2act;
509        let lb = max_activity * *activity_per_capacity_limits.start();
510        let ub = max_activity * *activity_per_capacity_limits.end();
511        lb..=ub
512    }
513
514    /// Iterate over activity limits for this asset
515    pub fn iter_activity_limits(
516        &self,
517    ) -> impl Iterator<Item = (TimeSliceSelection, RangeInclusive<Activity>)> + '_ {
518        let max_act = self.max_activity();
519        self.activity_limits
520            .iter_limits()
521            .map(move |(ts_sel, limit)| {
522                (
523                    ts_sel,
524                    (max_act * *limit.start())..=(max_act * *limit.end()),
525                )
526            })
527    }
528
529    /// Iterate over activity per capacity limits for this asset
530    pub fn iter_activity_per_capacity_limits(
531        &self,
532    ) -> impl Iterator<Item = (TimeSliceSelection, RangeInclusive<ActivityPerCapacity>)> + '_ {
533        let cap2act = self.process.capacity_to_activity;
534        self.activity_limits
535            .iter_limits()
536            .map(move |(ts_sel, limit)| {
537                (
538                    ts_sel,
539                    (cap2act * *limit.start())..=(cap2act * *limit.end()),
540                )
541            })
542    }
543
544    /// Gets the total SED/SVD output per unit of activity for this asset
545    ///
546    /// Note: Since we are summing coefficients from different commodities, this ONLY makes sense
547    /// if these commodities have the same units (e.g., all in PJ). Users are currently not made to
548    /// give units for commodities, so we cannot possibly enforce this. Something to potentially
549    /// address in future.
550    pub fn get_total_output_per_activity(&self) -> FlowPerActivity {
551        self.iter_output_flows().map(|flow| flow.coeff).sum()
552    }
553
554    /// Get the operating cost for this asset in a given year and time slice
555    pub fn get_operating_cost(&self, year: u32, time_slice: &TimeSliceID) -> MoneyPerActivity {
556        // The cost for all commodity flows (including levies/incentives)
557        let flows_cost = self
558            .iter_flows()
559            .map(|flow| flow.get_total_cost_per_activity(&self.region_id, year, time_slice))
560            .sum();
561
562        self.process_parameter.variable_operating_cost + flows_cost
563    }
564
565    /// Get the total revenue from all flows for this asset.
566    ///
567    /// If a price is missing, it is assumed to be zero.
568    pub fn get_revenue_from_flows(
569        &self,
570        prices: &CommodityPrices,
571        time_slice: &TimeSliceID,
572    ) -> MoneyPerActivity {
573        self.get_revenue_from_flows_with_filter(prices, time_slice, |_| true)
574    }
575
576    /// Get the total revenue from all flows excluding the primary output.
577    ///
578    /// If a price is missing, it is assumed to be zero.
579    pub fn get_revenue_from_flows_excluding_primary(
580        &self,
581        prices: &CommodityPrices,
582        time_slice: &TimeSliceID,
583    ) -> MoneyPerActivity {
584        let excluded_commodity = self.primary_output().map(|flow| &flow.commodity.id);
585
586        self.get_revenue_from_flows_with_filter(prices, time_slice, |flow| {
587            excluded_commodity.is_none_or(|commodity_id| commodity_id != &flow.commodity.id)
588        })
589    }
590
591    /// Get the total cost of purchasing input commodities per unit of activity for this asset.
592    ///
593    /// If a price is missing, there is assumed to be no cost.
594    pub fn get_input_cost_from_prices(
595        &self,
596        prices: &CommodityPrices,
597        time_slice: &TimeSliceID,
598    ) -> MoneyPerActivity {
599        // Revenues of input flows are negative costs, so we negate the result
600        -self.get_revenue_from_flows_with_filter(prices, time_slice, |x| {
601            x.direction() == FlowDirection::Input
602        })
603    }
604
605    /// Get the total revenue from a subset of flows.
606    ///
607    /// Takes a function as an argument to filter the flows. If a price is missing, it is assumed to
608    /// be zero.
609    fn get_revenue_from_flows_with_filter<F>(
610        &self,
611        prices: &CommodityPrices,
612        time_slice: &TimeSliceID,
613        mut filter_for_flows: F,
614    ) -> MoneyPerActivity
615    where
616        F: FnMut(&ProcessFlow) -> bool,
617    {
618        self.iter_flows()
619            .filter(|flow| filter_for_flows(flow))
620            .map(|flow| {
621                flow.coeff
622                    * prices
623                        .get(&flow.commodity.id, &self.region_id, time_slice)
624                        .unwrap_or_default()
625            })
626            .sum()
627    }
628
629    /// Get the generic activity cost per unit of activity for this asset.
630    ///
631    /// These are all activity-related costs that are not associated with specific SED/SVD outputs.
632    /// Includes levies, flow costs, costs of inputs and variable operating costs
633    fn get_generic_activity_cost(
634        &self,
635        prices: &CommodityPrices,
636        year: u32,
637        time_slice: &TimeSliceID,
638    ) -> MoneyPerActivity {
639        // The cost of purchasing input commodities
640        let cost_of_inputs = self.get_input_cost_from_prices(prices, time_slice);
641
642        // Flow costs/levies for all flows except SED/SVD outputs
643        let excludes_sed_svd_output = |flow: &&ProcessFlow| {
644            !(flow.direction() == FlowDirection::Output
645                && matches!(
646                    flow.commodity.kind,
647                    CommodityType::SupplyEqualsDemand | CommodityType::ServiceDemand
648                ))
649        };
650        let flow_costs = self
651            .iter_flows()
652            .filter(excludes_sed_svd_output)
653            .map(|flow| flow.get_total_cost_per_activity(&self.region_id, year, time_slice))
654            .sum();
655
656        cost_of_inputs + flow_costs + self.process_parameter.variable_operating_cost
657    }
658
659    /// Iterate over marginal costs for a filtered set of SED/SVD output commodities for this asset
660    ///
661    /// For each SED/SVD output commodity, the marginal cost is calculated as the sum of:
662    /// - Generic activity costs (variable operating costs, cost of purchasing inputs, plus all
663    ///   levies and flow costs not associated with specific SED/SVD outputs), which are
664    ///   shared equally over all SED/SVD outputs
665    /// - Production levies and flow costs for the specific SED/SVD output commodity
666    pub fn iter_marginal_costs_with_filter<'a>(
667        &'a self,
668        prices: &'a CommodityPrices,
669        year: u32,
670        time_slice: &'a TimeSliceID,
671        filter: impl Fn(&CommodityID) -> bool + 'a,
672    ) -> Box<dyn Iterator<Item = (CommodityID, MoneyPerFlow)> + 'a> {
673        // Iterator over SED/SVD output flows matching the filter
674        let mut output_flows_iter = self
675            .iter_output_flows()
676            .filter(move |flow| filter(&flow.commodity.id))
677            .peekable();
678
679        // If there are no output flows after filtering, return an empty iterator
680        if output_flows_iter.peek().is_none() {
681            return Box::new(std::iter::empty::<(CommodityID, MoneyPerFlow)>());
682        }
683
684        // Calculate generic activity costs.
685        // This is all activity costs not associated with specific SED/SVD outputs, which will get
686        // shared equally over all SED/SVD outputs. Includes levies, flow costs, costs of inputs and
687        // variable operating costs
688        let generic_activity_cost = self.get_generic_activity_cost(prices, year, time_slice);
689
690        // Share generic activity costs equally over all SED/SVD outputs
691        // We sum the output coefficients of all SED/SVD commodities to get total output, then
692        // divide costs by this total output to get the generic cost per unit of output.
693        // Note: only works if all SED/SVD outputs have the same units - not currently checked!
694        let total_output_per_activity = self.get_total_output_per_activity();
695        assert!(total_output_per_activity > FlowPerActivity::EPSILON); // input checks should guarantee this
696        let generic_cost_per_flow = generic_activity_cost / total_output_per_activity;
697
698        // Iterate over SED/SVD output flows
699        Box::new(output_flows_iter.map(move |flow| {
700            // Get the costs for this specific commodity flow
701            let commodity_specific_costs_per_flow =
702                flow.get_total_cost_per_flow(&self.region_id, year, time_slice);
703
704            // Add these to the generic costs to get total cost for this commodity
705            let marginal_cost = generic_cost_per_flow + commodity_specific_costs_per_flow;
706            (flow.commodity.id.clone(), marginal_cost)
707        }))
708    }
709
710    /// Iterate over marginal costs for all SED/SVD output commodities for this asset
711    ///
712    /// See `iter_marginal_costs_with_filter` for details.
713    pub fn iter_marginal_costs<'a>(
714        &'a self,
715        prices: &'a CommodityPrices,
716        year: u32,
717        time_slice: &'a TimeSliceID,
718    ) -> Box<dyn Iterator<Item = (CommodityID, MoneyPerFlow)> + 'a> {
719        self.iter_marginal_costs_with_filter(prices, year, time_slice, move |_| true)
720    }
721
722    /// Get the annual capital cost per unit of capacity for this asset
723    pub fn get_annual_capital_cost_per_capacity(&self) -> MoneyPerCapacity {
724        let capital_cost = self.process_parameter.capital_cost;
725        let lifetime = self.process_parameter.lifetime;
726        let discount_rate = self.process_parameter.discount_rate;
727        annual_capital_cost(capital_cost, lifetime, discount_rate)
728    }
729
730    /// Get the annual capital cost per unit of activity for this asset
731    ///
732    /// Total capital costs (cost per capacity * capacity) are shared equally over the year in
733    /// accordance with the annual activity.
734    pub fn get_annual_capital_cost_per_activity(
735        &self,
736        annual_activity: Activity,
737    ) -> MoneyPerActivity {
738        let annual_capital_cost_per_capacity = self.get_annual_capital_cost_per_capacity();
739        let total_annual_capital_cost =
740            annual_capital_cost_per_capacity * self.capacity.total_capacity();
741        assert!(
742            annual_activity > Activity::EPSILON,
743            "Cannot calculate annual capital cost per activity for an asset with zero annual activity"
744        );
745        total_annual_capital_cost / annual_activity
746    }
747
748    /// Get the annual capital cost per unit of output flow for this asset
749    ///
750    /// Total capital costs (cost per capacity * capacity) are shared equally across all output
751    /// flows in accordance with the annual activity and total output per unit of activity.
752    pub fn get_annual_capital_cost_per_flow(&self, annual_activity: Activity) -> MoneyPerFlow {
753        let annual_capital_cost_per_activity =
754            self.get_annual_capital_cost_per_activity(annual_activity);
755        let total_output_per_activity = self.get_total_output_per_activity();
756        assert!(total_output_per_activity > FlowPerActivity::EPSILON); // input checks should guarantee this
757        annual_capital_cost_per_activity / total_output_per_activity
758    }
759
760    /// Maximum activity for this asset
761    pub fn max_activity(&self) -> Activity {
762        self.capacity.total_capacity() * self.process.capacity_to_activity
763    }
764
765    /// Get a specific process flow
766    pub fn get_flow(&self, commodity_id: &CommodityID) -> Option<&ProcessFlow> {
767        self.flows.get(commodity_id)
768    }
769
770    /// Iterate over the asset's flows
771    pub fn iter_flows(&self) -> impl Iterator<Item = &ProcessFlow> {
772        self.flows.values()
773    }
774
775    /// Iterate over the asset's output SED/SVD flows
776    pub fn iter_output_flows(&self) -> impl Iterator<Item = &ProcessFlow> {
777        self.flows.values().filter(|flow| {
778            flow.direction() == FlowDirection::Output
779                && matches!(
780                    flow.commodity.kind,
781                    CommodityType::SupplyEqualsDemand | CommodityType::ServiceDemand
782                )
783        })
784    }
785
786    /// Get the primary output flow (if any) for this asset
787    pub fn primary_output(&self) -> Option<&ProcessFlow> {
788        self.process
789            .primary_output
790            .as_ref()
791            .map(|commodity_id| &self.flows[commodity_id])
792    }
793
794    /// Whether this asset has been commissioned
795    pub fn is_commissioned(&self) -> bool {
796        matches!(&self.state, AssetState::Commissioned { .. })
797    }
798
799    /// Get the commission year for this asset
800    pub fn commission_year(&self) -> u32 {
801        self.commission_year
802    }
803
804    /// Get the decommission year for this asset
805    pub fn decommission_year(&self) -> Option<u32> {
806        match &self.state {
807            AssetState::Decommissioned {
808                decommission_year, ..
809            } => Some(*decommission_year),
810            _ => None,
811        }
812    }
813
814    /// Get the region ID for this asset
815    pub fn region_id(&self) -> &RegionID {
816        &self.region_id
817    }
818
819    /// Get the process for this asset
820    pub fn process(&self) -> &Process {
821        &self.process
822    }
823
824    /// Get the process ID for this asset
825    pub fn process_id(&self) -> &ProcessID {
826        &self.process.id
827    }
828
829    /// Get the ID for this asset
830    pub fn id(&self) -> Option<AssetID> {
831        match &self.state {
832            AssetState::Commissioned { id, .. } | AssetState::Decommissioned { id, .. } => {
833                Some(*id)
834            }
835            _ => None,
836        }
837    }
838
839    /// Get the group ID for this asset
840    pub fn group_id(&self) -> Option<AssetGroupID> {
841        match &self.state {
842            AssetState::Commissioned { group_id, .. } => *group_id,
843            _ => None,
844        }
845    }
846
847    /// Get the agent ID for this asset
848    pub fn agent_id(&self) -> Option<&AgentID> {
849        match &self.state {
850            AssetState::Commissioned { agent_id, .. }
851            | AssetState::Decommissioned { agent_id, .. }
852            | AssetState::Future { agent_id }
853            | AssetState::Selected { agent_id } => Some(agent_id),
854            AssetState::Candidate => None,
855        }
856    }
857
858    /// Get the capacity for this asset
859    pub fn capacity(&self) -> AssetCapacity {
860        self.capacity
861    }
862
863    /// Set the capacity for this asset (only for Candidate or Selected assets)
864    pub fn set_capacity(&mut self, capacity: AssetCapacity) {
865        assert!(
866            matches!(
867                self.state,
868                AssetState::Candidate | AssetState::Selected { .. }
869            ),
870            "set_capacity can only be called on Candidate or Selected assets"
871        );
872        assert!(
873            capacity.total_capacity() >= Capacity(0.0),
874            "Capacity must be >= 0"
875        );
876        self.capacity.assert_same_type(capacity);
877        self.capacity = capacity;
878    }
879
880    /// Increase the capacity for this asset (only for Candidate assets)
881    pub fn increase_capacity(&mut self, capacity: AssetCapacity) {
882        assert!(
883            self.state == AssetState::Candidate,
884            "increase_capacity can only be called on Candidate assets"
885        );
886        assert!(
887            capacity.total_capacity() > Capacity(0.0),
888            "Capacity increase must be positive"
889        );
890        self.capacity = self.capacity + capacity;
891    }
892
893    /// Decommission this asset
894    fn decommission(&mut self, decommission_year: u32, reason: &str) {
895        let (id, agent_id) = match &self.state {
896            AssetState::Commissioned { id, agent_id, .. } => (*id, agent_id.clone()),
897            _ => panic!("Cannot decommission an asset that hasn't been commissioned"),
898        };
899        debug!(
900            "Decommissioning '{}' asset (ID: {}) for agent '{}' (reason: {})",
901            self.process_id(),
902            id,
903            agent_id,
904            reason
905        );
906
907        self.state = AssetState::Decommissioned {
908            id,
909            agent_id,
910            decommission_year: decommission_year.min(self.max_decommission_year()),
911        };
912    }
913
914    /// Commission the asset.
915    ///
916    /// Only assets with an [`AssetState`] of `Future` or `Selected` can be commissioned. If the
917    /// asset's state is something else, this function will panic.
918    ///
919    /// # Arguments
920    ///
921    /// * `id` - The ID to give the newly commissioned asset
922    /// * `reason` - The reason for commissioning (included in log)
923    /// * `group_id` - The ID of the group of this asset, if any.
924    fn commission(&mut self, id: AssetID, group_id: Option<AssetGroupID>, reason: &str) {
925        let agent_id = match &self.state {
926            AssetState::Future { agent_id } | AssetState::Selected { agent_id } => agent_id,
927            state => panic!("Assets with state {state} cannot be commissioned"),
928        };
929        debug!(
930            "Commissioning '{}' asset (ID: {}, capacity: {}) for agent '{}' (reason: {})",
931            self.process_id(),
932            id,
933            self.capacity.total_capacity(),
934            agent_id,
935            reason
936        );
937        self.state = AssetState::Commissioned {
938            id,
939            agent_id: agent_id.clone(),
940            mothballed_year: None,
941            group_id,
942        };
943    }
944
945    /// Select a Candidate asset for investment, converting it to a Selected state
946    pub fn select_candidate_for_investment(&mut self, agent_id: AgentID) {
947        assert!(
948            self.state == AssetState::Candidate,
949            "select_candidate_for_investment can only be called on Candidate assets"
950        );
951        check_capacity_valid_for_asset(self.capacity.total_capacity()).unwrap();
952        self.state = AssetState::Selected { agent_id };
953    }
954
955    /// Set the year this asset was mothballed
956    pub fn mothball(&mut self, year: u32) {
957        let (id, agent_id, group_id) = match &self.state {
958            AssetState::Commissioned {
959                id,
960                agent_id,
961                group_id,
962                ..
963            } => (*id, agent_id.clone(), *group_id),
964            _ => panic!("Cannot mothball an asset that hasn't been commissioned"),
965        };
966        self.state = AssetState::Commissioned {
967            id,
968            agent_id: agent_id.clone(),
969            mothballed_year: Some(year),
970            group_id,
971        };
972    }
973
974    /// Remove the mothballed year - presumably because the asset has been used
975    pub fn unmothball(&mut self) {
976        let (id, agent_id, group_id) = match &self.state {
977            AssetState::Commissioned {
978                id,
979                agent_id,
980                group_id,
981                ..
982            } => (*id, agent_id.clone(), *group_id),
983            _ => panic!("Cannot unmothball an asset that hasn't been commissioned"),
984        };
985        self.state = AssetState::Commissioned {
986            id,
987            agent_id: agent_id.clone(),
988            mothballed_year: None,
989            group_id,
990        };
991    }
992
993    /// Get the mothballed year for the asset
994    pub fn get_mothballed_year(&self) -> Option<u32> {
995        let AssetState::Commissioned {
996            mothballed_year, ..
997        } = &self.state
998        else {
999            panic!("Cannot get mothballed year for an asset that hasn't been commissioned")
1000        };
1001        *mothballed_year
1002    }
1003
1004    /// Get the unit size for this asset's capacity (if any)
1005    pub fn unit_size(&self) -> Option<Capacity> {
1006        match self.capacity {
1007            AssetCapacity::Discrete(_, size) => Some(size),
1008            AssetCapacity::Continuous(_) => None,
1009        }
1010    }
1011
1012    /// Checks if the asset corresponds to a process that has a `unit_size` and is therefore divisible.
1013    pub fn is_divisible(&self) -> bool {
1014        self.process.unit_size.is_some()
1015    }
1016
1017    /// Divides an asset if it is divisible and returns a vector of children
1018    ///
1019    /// Assets with capacity of type `AssetCapacity::Discrete` are divided into multiple assets each
1020    /// made up of a single unit of the original asset's unit size. Will panic if the asset does not
1021    /// have a discrete capacity.
1022    ///
1023    /// Only `Future` and `Selected` assets can be divided.
1024    ///
1025    /// TODO: To be deleted
1026    pub fn divide_asset(&self) -> Vec<AssetRef> {
1027        assert!(
1028            matches!(
1029                self.state,
1030                AssetState::Future { .. } | AssetState::Selected { .. }
1031            ),
1032            "Assets with state {} cannot be divided. Only Future or Selected assets can be divided",
1033            self.state
1034        );
1035
1036        // Ensure the asset is discrete
1037        let AssetCapacity::Discrete(n_units, unit_size) = self.capacity else {
1038            panic!("Only discrete assets can be divided")
1039        };
1040
1041        // Divide the asset into `n_units` children of size `unit_size`
1042        let child_asset = Self {
1043            capacity: AssetCapacity::Discrete(1, unit_size),
1044            ..self.clone()
1045        };
1046        let child_asset = AssetRef::from(Rc::new(child_asset));
1047        std::iter::repeat_n(child_asset, n_units as usize).collect()
1048    }
1049}
1050
1051#[allow(clippy::missing_fields_in_debug)]
1052impl std::fmt::Debug for Asset {
1053    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1054        f.debug_struct("Asset")
1055            .field("state", &self.state)
1056            .field("process_id", &self.process_id())
1057            .field("region_id", &self.region_id)
1058            .field("capacity", &self.capacity.total_capacity())
1059            .field("commission_year", &self.commission_year)
1060            .finish()
1061    }
1062}
1063
1064/// Whether the process operates in the specified region and year
1065pub fn check_region_year_valid_for_process(
1066    process: &Process,
1067    region_id: &RegionID,
1068    year: u32,
1069) -> Result<()> {
1070    ensure!(
1071        process.regions.contains(region_id),
1072        "Process {} does not operate in region {}",
1073        process.id,
1074        region_id
1075    );
1076    ensure!(
1077        process.active_for_year(year),
1078        "Process {} does not operate in the year {}",
1079        process.id,
1080        year
1081    );
1082    Ok(())
1083}
1084
1085/// Whether the specified value is a valid capacity for an asset
1086pub fn check_capacity_valid_for_asset(capacity: Capacity) -> Result<()> {
1087    ensure!(
1088        capacity.is_finite() && capacity > Capacity(0.0),
1089        "Capacity must be a finite, positive number"
1090    );
1091    Ok(())
1092}
1093
1094/// A wrapper around [`Asset`] for storing references in maps.
1095///
1096/// If the asset has been commissioned, then comparison and hashing is done based on the asset ID,
1097/// otherwise a combination of other parameters is used.
1098///
1099/// [`Ord`] is implemented for [`AssetRef`], but it will panic for non-commissioned assets.
1100#[derive(Clone, Debug)]
1101pub struct AssetRef(Rc<Asset>);
1102
1103impl AssetRef {
1104    /// Make a mutable reference to the underlying [`Asset`]
1105    pub fn make_mut(&mut self) -> &mut Asset {
1106        Rc::make_mut(&mut self.0)
1107    }
1108}
1109
1110impl From<Rc<Asset>> for AssetRef {
1111    fn from(value: Rc<Asset>) -> Self {
1112        Self(value)
1113    }
1114}
1115
1116impl From<Asset> for AssetRef {
1117    fn from(value: Asset) -> Self {
1118        Self::from(Rc::new(value))
1119    }
1120}
1121
1122impl From<AssetRef> for Rc<Asset> {
1123    fn from(value: AssetRef) -> Self {
1124        value.0
1125    }
1126}
1127
1128impl Deref for AssetRef {
1129    type Target = Asset;
1130
1131    fn deref(&self) -> &Self::Target {
1132        &self.0
1133    }
1134}
1135
1136impl PartialEq for AssetRef {
1137    fn eq(&self, other: &Self) -> bool {
1138        // For assets to be considered equal, they must have the same process, region, commission
1139        // year and state
1140        Rc::ptr_eq(&self.0.process, &other.0.process)
1141            && self.0.region_id == other.0.region_id
1142            && self.0.commission_year == other.0.commission_year
1143            && self.0.state == other.0.state
1144    }
1145}
1146
1147impl Eq for AssetRef {}
1148
1149impl Hash for AssetRef {
1150    /// Hash an asset according to its state:
1151    /// - Commissioned assets are hashed based on their ID alone
1152    /// - Selected assets are hashed based on `process_id`, `region_id`, `commission_year` and `agent_id`
1153    /// - Candidate assets are hashed based on `process_id`, `region_id` and `commission_year`
1154    /// - Future and Decommissioned assets cannot currently be hashed
1155    fn hash<H: Hasher>(&self, state: &mut H) {
1156        match &self.0.state {
1157            AssetState::Commissioned { id, .. } => {
1158                // Hashed based on their ID alone, since this is sufficient to uniquely identify the
1159                // asset
1160                id.hash(state);
1161            }
1162            AssetState::Candidate | AssetState::Selected { .. } => {
1163                // Hashed based on process_id, region_id, commission_year and (for Selected assets)
1164                // agent_id
1165                self.0.process.id.hash(state);
1166                self.0.region_id.hash(state);
1167                self.0.commission_year.hash(state);
1168                self.0.agent_id().hash(state);
1169            }
1170            AssetState::Future { .. } | AssetState::Decommissioned { .. } => {
1171                // We shouldn't currently need to hash Future or Decommissioned assets
1172                panic!("Cannot hash Future or Decommissioned assets");
1173            }
1174        }
1175    }
1176}
1177
1178impl PartialOrd for AssetRef {
1179    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1180        Some(self.cmp(other))
1181    }
1182}
1183
1184impl Ord for AssetRef {
1185    fn cmp(&self, other: &Self) -> Ordering {
1186        self.id().unwrap().cmp(&other.id().unwrap())
1187    }
1188}
1189
1190/// A pool of [`Asset`]s
1191pub struct AssetPool {
1192    /// The pool of active assets, sorted by ID
1193    active: Vec<AssetRef>,
1194    /// Assets that have not yet been commissioned, sorted by commission year
1195    future: Vec<Asset>,
1196    /// Assets that have been decommissioned
1197    decommissioned: Vec<AssetRef>,
1198    /// Next available asset ID number
1199    next_id: u32,
1200    /// Next available group ID number
1201    next_group_id: u32,
1202}
1203
1204impl AssetPool {
1205    /// Create a new [`AssetPool`]
1206    pub fn new(mut assets: Vec<Asset>) -> Self {
1207        // Sort in order of commission year
1208        assets.sort_by(|a, b| a.commission_year.cmp(&b.commission_year));
1209
1210        Self {
1211            active: Vec::new(),
1212            future: assets,
1213            decommissioned: Vec::new(),
1214            next_id: 0,
1215            next_group_id: 0,
1216        }
1217    }
1218
1219    /// Get the active pool as a slice of [`AssetRef`]s
1220    pub fn as_slice(&self) -> &[AssetRef] {
1221        &self.active
1222    }
1223
1224    /// Decommission assets whose lifetime has passed,
1225    /// and commission new assets
1226    pub fn update_for_year(&mut self, year: u32) {
1227        self.decommission_old(year);
1228        self.commission_new(year);
1229    }
1230
1231    /// Commission new assets for the specified milestone year from the input data
1232    fn commission_new(&mut self, year: u32) {
1233        // Count the number of assets to move
1234        let count = self
1235            .future
1236            .iter()
1237            .take_while(|asset| asset.commission_year <= year)
1238            .count();
1239
1240        // Move assets from future to active
1241        for mut asset in self.future.drain(0..count) {
1242            // Ignore assets that have already been decommissioned
1243            if asset.max_decommission_year() <= year {
1244                warn!(
1245                    "Asset '{}' with commission year {} and lifetime {} was decommissioned before \
1246                    the start of the simulation",
1247                    asset.process_id(),
1248                    asset.commission_year,
1249                    asset.process_parameter.lifetime
1250                );
1251                continue;
1252            }
1253
1254            // If it is divisible, we divide and commission all the children
1255            if asset.is_divisible() {
1256                for mut child in asset.divide_asset() {
1257                    child.make_mut().commission(
1258                        AssetID(self.next_id),
1259                        Some(AssetGroupID(self.next_group_id)),
1260                        "user input",
1261                    );
1262                    self.next_id += 1;
1263                    self.active.push(child);
1264                }
1265                self.next_group_id += 1;
1266            }
1267            // If not, we just commission it as a single asset
1268            else {
1269                asset.commission(AssetID(self.next_id), None, "user input");
1270                self.next_id += 1;
1271                self.active.push(asset.into());
1272            }
1273        }
1274    }
1275
1276    /// Decommission old assets for the specified milestone year
1277    fn decommission_old(&mut self, year: u32) {
1278        // Remove assets which are due for decommissioning
1279        let to_decommission = self
1280            .active
1281            .extract_if(.., |asset| asset.max_decommission_year() <= year);
1282
1283        for mut asset in to_decommission {
1284            // Set `decommission_year` and move to `self.decommissioned`
1285            asset.make_mut().decommission(year, "end of life");
1286            self.decommissioned.push(asset);
1287        }
1288    }
1289
1290    /// Decomission mothballed assets if mothballed long enough
1291    pub fn decommission_mothballed(&mut self, year: u32, mothball_years: u32) {
1292        // Remove assets which are due for decommissioning
1293        let to_decommission = self.active.extract_if(.., |asset| {
1294            asset.get_mothballed_year().is_some()
1295                && asset.get_mothballed_year() <= Some(year - min(mothball_years, year))
1296        });
1297
1298        for mut asset in to_decommission {
1299            // Set `decommission_year` and move to `self.decommissioned`
1300            let decommissioned = asset.get_mothballed_year().unwrap() + mothball_years;
1301            asset.make_mut().decommission(
1302                decommissioned,
1303                &format!("The asset has not been used for the set mothball years ({mothball_years} years)."),
1304            );
1305            self.decommissioned.push(asset);
1306        }
1307    }
1308
1309    /// Mothball the specified assets if they are no longer in the active pool and put them back again.
1310    ///
1311    /// # Arguments
1312    ///
1313    /// * `assets` - Assets to possibly mothball
1314    /// * `year` - Mothball year
1315    ///
1316    /// # Panics
1317    ///
1318    /// Panics if any of the provided assets was never commissioned.
1319    pub fn mothball_unretained<I>(&mut self, assets: I, year: u32)
1320    where
1321        I: IntoIterator<Item = AssetRef>,
1322    {
1323        for mut asset in assets {
1324            if match asset.state {
1325                AssetState::Commissioned { .. } => !self.active.contains(&asset),
1326                _ => panic!("Cannot mothball asset that has not been commissioned"),
1327            } {
1328                // If not already set, we set the current year as the mothball year,
1329                // i.e. the first one the asset was not used.
1330                if asset.get_mothballed_year().is_none() {
1331                    asset.make_mut().mothball(year);
1332                }
1333
1334                // And we put it back to the pool, so they can be chosen the next milestone year
1335                // if not decommissioned earlier.
1336                self.active.push(asset);
1337            }
1338        }
1339        self.active.sort();
1340    }
1341
1342    /// Get an asset with the specified ID.
1343    ///
1344    /// # Returns
1345    ///
1346    /// An [`AssetRef`] if found, else `None`. The asset may not be found if it has already been
1347    /// decommissioned.
1348    pub fn get(&self, id: AssetID) -> Option<&AssetRef> {
1349        // The assets in `active` are in order of ID
1350        let idx = self
1351            .active
1352            .binary_search_by(|asset| match &asset.state {
1353                AssetState::Commissioned { id: asset_id, .. } => asset_id.cmp(&id),
1354                _ => panic!("Active pool should only contain commissioned assets"),
1355            })
1356            .ok()?;
1357
1358        Some(&self.active[idx])
1359    }
1360
1361    /// Iterate over active assets
1362    pub fn iter_active(&self) -> slice::Iter<'_, AssetRef> {
1363        self.active.iter()
1364    }
1365
1366    /// Iterate over decommissioned assets
1367    pub fn iter_decommissioned(&self) -> slice::Iter<'_, AssetRef> {
1368        self.decommissioned.iter()
1369    }
1370
1371    /// Iterate over all commissioned and decommissioned assets.
1372    ///
1373    /// NB: Not-yet-commissioned assets are not included.
1374    pub fn iter_all(&self) -> impl Iterator<Item = &AssetRef> {
1375        chain(self.iter_active(), self.iter_decommissioned())
1376    }
1377
1378    /// Return current active pool and clear
1379    pub fn take(&mut self) -> Vec<AssetRef> {
1380        std::mem::take(&mut self.active)
1381    }
1382
1383    /// Extend the active pool with Commissioned or Selected assets
1384    pub fn extend<I>(&mut self, assets: I)
1385    where
1386        I: IntoIterator<Item = AssetRef>,
1387    {
1388        // Check all assets are either Commissioned or Selected, and, if the latter,
1389        // then commission them
1390        for mut asset in assets {
1391            match &asset.state {
1392                AssetState::Commissioned { .. } => {
1393                    asset.make_mut().unmothball();
1394                    self.active.push(asset);
1395                }
1396                AssetState::Selected { .. } => {
1397                    // If it is divisible, we divide and commission all the children
1398                    if asset.is_divisible() {
1399                        for mut child in asset.divide_asset() {
1400                            child.make_mut().commission(
1401                                AssetID(self.next_id),
1402                                Some(AssetGroupID(self.next_group_id)),
1403                                "selected",
1404                            );
1405                            self.next_id += 1;
1406                            self.active.push(child);
1407                        }
1408                        self.next_group_id += 1;
1409                    }
1410                    // If not, we just commission it as a single asset
1411                    else {
1412                        asset
1413                            .make_mut()
1414                            .commission(AssetID(self.next_id), None, "selected");
1415                        self.next_id += 1;
1416                        self.active.push(asset);
1417                    }
1418                }
1419                _ => panic!(
1420                    "Cannot extend asset pool with asset in state {}. Only assets in \
1421                Commissioned or Selected states are allowed.",
1422                    asset.state
1423                ),
1424            }
1425        }
1426
1427        // New assets may not have been sorted, but active needs to be sorted by ID
1428        self.active.sort();
1429
1430        // Sanity check: all assets should be unique
1431        debug_assert_eq!(self.active.iter().unique().count(), self.active.len());
1432    }
1433}
1434
1435/// Additional methods for iterating over assets
1436pub trait AssetIterator<'a>: Iterator<Item = &'a AssetRef> + Sized
1437where
1438    Self: 'a,
1439{
1440    /// Filter assets by the agent that owns them
1441    fn filter_agent(self, agent_id: &'a AgentID) -> impl Iterator<Item = &'a AssetRef> + 'a {
1442        self.filter(move |asset| asset.agent_id() == Some(agent_id))
1443    }
1444
1445    /// Iterate over assets that have the given commodity as a primary output
1446    fn filter_primary_producers_of(
1447        self,
1448        commodity_id: &'a CommodityID,
1449    ) -> impl Iterator<Item = &'a AssetRef> + 'a {
1450        self.filter(move |asset| {
1451            asset
1452                .primary_output()
1453                .is_some_and(|flow| &flow.commodity.id == commodity_id)
1454        })
1455    }
1456
1457    /// Filter the assets by region
1458    fn filter_region(self, region_id: &'a RegionID) -> impl Iterator<Item = &'a AssetRef> + 'a {
1459        self.filter(move |asset| asset.region_id == *region_id)
1460    }
1461
1462    /// Iterate over process flows affecting the given commodity
1463    fn flows_for_commodity(
1464        self,
1465        commodity_id: &'a CommodityID,
1466    ) -> impl Iterator<Item = (&'a AssetRef, &'a ProcessFlow)> + 'a {
1467        self.filter_map(|asset| Some((asset, asset.get_flow(commodity_id)?)))
1468    }
1469}
1470
1471impl<'a, I> AssetIterator<'a> for I where I: Iterator<Item = &'a AssetRef> + Sized + 'a {}
1472
1473#[cfg(test)]
1474mod tests {
1475    use super::*;
1476    use crate::commodity::Commodity;
1477    use crate::fixture::{
1478        assert_error, assert_patched_runs_ok_simple, assert_validate_fails_with_simple, asset,
1479        process, process_activity_limits_map, process_flows_map, process_parameter_map, region_id,
1480        svd_commodity, time_slice, time_slice_info,
1481    };
1482    use crate::patch::FilePatch;
1483    use crate::process::{FlowType, Process, ProcessFlow, ProcessParameter};
1484    use crate::region::RegionID;
1485    use crate::time_slice::{TimeSliceID, TimeSliceInfo};
1486    use crate::units::{
1487        ActivityPerCapacity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity,
1488        MoneyPerCapacity, MoneyPerCapacityPerYear, MoneyPerFlow,
1489    };
1490    use float_cmp::assert_approx_eq;
1491    use indexmap::indexmap;
1492    use itertools::{Itertools, assert_equal};
1493    use rstest::{fixture, rstest};
1494    use std::iter;
1495    use std::rc::Rc;
1496
1497    /// Number of expected children for divisible asset
1498    #[allow(clippy::cast_possible_truncation)]
1499    #[allow(clippy::cast_sign_loss)]
1500    fn expected_children_for_divisible(asset: &Asset) -> usize {
1501        (asset.capacity.total_capacity() / asset.process.unit_size.expect("Asset is not divisible"))
1502            .value()
1503            .ceil() as usize
1504    }
1505
1506    #[rstest]
1507    #[case::exact_multiple(Capacity(12.0), Some(Capacity(4.0)), Some(3), Capacity(12.0))]
1508    #[case::rounded_up(Capacity(11.0), Some(Capacity(4.0)), Some(3), Capacity(12.0))]
1509    #[case::unit_size_greater_than_capacity(
1510        Capacity(3.0),
1511        Some(Capacity(4.0)),
1512        Some(1),
1513        Capacity(4.0)
1514    )]
1515    #[case::continuous(Capacity(5.5), None, None, Capacity(5.5))]
1516    fn from_capacity(
1517        #[case] capacity: Capacity,
1518        #[case] unit_size: Option<Capacity>,
1519        #[case] expected_n: Option<u32>,
1520        #[case] expected_total: Capacity,
1521    ) {
1522        let got = AssetCapacity::from_capacity(capacity, unit_size);
1523        assert_eq!(got.n_units(), expected_n);
1524        assert_eq!(got.total_capacity(), expected_total);
1525    }
1526
1527    #[rstest]
1528    #[case::round_up(3u32, Capacity(4.0), Dimensionless(0.5), 2u32)]
1529    #[case::exact(3u32, Capacity(4.0), Dimensionless(0.33), 1u32)]
1530    fn apply_limit_factor(
1531        #[case] start_units: u32,
1532        #[case] unit_size: Capacity,
1533        #[case] factor: Dimensionless,
1534        #[case] expected_units: u32,
1535    ) {
1536        let orig = AssetCapacity::Discrete(start_units, unit_size);
1537        let got = orig.apply_limit_factor(factor);
1538        assert_eq!(got, AssetCapacity::Discrete(expected_units, unit_size));
1539    }
1540
1541    #[rstest]
1542    fn get_input_cost_from_prices_works(
1543        region_id: RegionID,
1544        svd_commodity: Commodity,
1545        mut process: Process,
1546        time_slice: TimeSliceID,
1547    ) {
1548        // Update the process flows using the existing commodity fixture
1549        let commodity_rc = Rc::new(svd_commodity);
1550        let process_flow = ProcessFlow {
1551            commodity: Rc::clone(&commodity_rc),
1552            coeff: FlowPerActivity(-2.0), // Input
1553            kind: FlowType::Fixed,
1554            cost: MoneyPerFlow(0.0),
1555        };
1556        let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() };
1557        let process_flows_map = process_flows_map(process.regions.clone(), Rc::new(process_flows));
1558        process.flows = process_flows_map;
1559
1560        // Create asset
1561        let asset =
1562            Asset::new_candidate(Rc::new(process), region_id.clone(), Capacity(1.0), 2020).unwrap();
1563
1564        // Set input prices
1565        let mut input_prices = CommodityPrices::default();
1566        input_prices.insert(&commodity_rc.id, &region_id, &time_slice, MoneyPerFlow(3.0));
1567
1568        // Call function
1569        let cost = asset.get_input_cost_from_prices(&input_prices, &time_slice);
1570        // Should be -coeff * price = -(-2.0) * 3.0 = 6.0
1571        assert_approx_eq!(MoneyPerActivity, cost, MoneyPerActivity(6.0));
1572    }
1573
1574    #[rstest]
1575    #[case(Capacity(0.01))]
1576    #[case(Capacity(0.5))]
1577    #[case(Capacity(1.0))]
1578    #[case(Capacity(100.0))]
1579    fn asset_new_valid(process: Process, #[case] capacity: Capacity) {
1580        let agent_id = AgentID("agent1".into());
1581        let region_id = RegionID("GBR".into());
1582        let asset = Asset::new_future(agent_id, process.into(), region_id, capacity, 2015).unwrap();
1583        assert!(asset.id().is_none());
1584    }
1585
1586    #[rstest]
1587    #[case(Capacity(0.0))]
1588    #[case(Capacity(-0.01))]
1589    #[case(Capacity(-1.0))]
1590    #[case(Capacity(f64::NAN))]
1591    #[case(Capacity(f64::INFINITY))]
1592    #[case(Capacity(f64::NEG_INFINITY))]
1593    fn asset_new_invalid_capacity(process: Process, #[case] capacity: Capacity) {
1594        let agent_id = AgentID("agent1".into());
1595        let region_id = RegionID("GBR".into());
1596        assert_error!(
1597            Asset::new_future(agent_id, process.into(), region_id, capacity, 2015),
1598            "Capacity must be a finite, positive number"
1599        );
1600    }
1601
1602    #[rstest]
1603    fn asset_new_invalid_commission_year(process: Process) {
1604        let agent_id = AgentID("agent1".into());
1605        let region_id = RegionID("GBR".into());
1606        assert_error!(
1607            Asset::new_future(agent_id, process.into(), region_id, Capacity(1.0), 2007),
1608            "Process process1 does not operate in the year 2007"
1609        );
1610    }
1611
1612    #[rstest]
1613    fn asset_new_invalid_region(process: Process) {
1614        let agent_id = AgentID("agent1".into());
1615        let region_id = RegionID("FRA".into());
1616        assert_error!(
1617            Asset::new_future(agent_id, process.into(), region_id, Capacity(1.0), 2015),
1618            "Process process1 does not operate in region FRA"
1619        );
1620    }
1621
1622    #[fixture]
1623    fn asset_pool(mut process: Process) -> AssetPool {
1624        // Update process parameters (lifetime = 20 years)
1625        let process_param = ProcessParameter {
1626            capital_cost: MoneyPerCapacity(5.0),
1627            fixed_operating_cost: MoneyPerCapacityPerYear(2.0),
1628            variable_operating_cost: MoneyPerActivity(1.0),
1629            lifetime: 20,
1630            discount_rate: Dimensionless(0.9),
1631        };
1632        let process_parameter_map = process_parameter_map(process.regions.clone(), process_param);
1633        process.parameters = process_parameter_map;
1634
1635        let rc_process = Rc::new(process);
1636        let future = [2020, 2010]
1637            .map(|year| {
1638                Asset::new_future(
1639                    "agent1".into(),
1640                    Rc::clone(&rc_process),
1641                    "GBR".into(),
1642                    Capacity(1.0),
1643                    year,
1644                )
1645                .unwrap()
1646            })
1647            .into_iter()
1648            .collect_vec();
1649
1650        AssetPool::new(future)
1651    }
1652
1653    #[fixture]
1654    fn process_with_activity_limits(
1655        mut process: Process,
1656        time_slice_info: TimeSliceInfo,
1657        time_slice: TimeSliceID,
1658    ) -> Process {
1659        // Add activity limits to the process
1660        let mut activity_limits = ActivityLimits::new_with_full_availability(&time_slice_info);
1661        activity_limits.add_time_slice_limit(time_slice, Dimensionless(0.1)..=Dimensionless(0.5));
1662        process.activity_limits =
1663            process_activity_limits_map(process.regions.clone(), activity_limits);
1664
1665        // Update cap2act
1666        process.capacity_to_activity = ActivityPerCapacity(2.0);
1667        process
1668    }
1669
1670    #[fixture]
1671    fn asset_with_activity_limits(process_with_activity_limits: Process) -> Asset {
1672        Asset::new_future(
1673            "agent1".into(),
1674            Rc::new(process_with_activity_limits),
1675            "GBR".into(),
1676            Capacity(2.0),
1677            2010,
1678        )
1679        .unwrap()
1680    }
1681
1682    #[fixture]
1683    fn asset_divisible(mut process: Process) -> Asset {
1684        process.unit_size = Some(Capacity(4.0));
1685        Asset::new_future(
1686            "agent1".into(),
1687            Rc::new(process),
1688            "GBR".into(),
1689            Capacity(11.0),
1690            2010,
1691        )
1692        .unwrap()
1693    }
1694
1695    #[rstest]
1696    fn asset_get_activity_per_capacity_limits(
1697        asset_with_activity_limits: Asset,
1698        time_slice: TimeSliceID,
1699    ) {
1700        // With cap2act of 2, and activity limits of 0.1..=0.5, should get 0.2..=1.0
1701        assert_eq!(
1702            asset_with_activity_limits.get_activity_per_capacity_limits(&time_slice),
1703            ActivityPerCapacity(0.2)..=ActivityPerCapacity(1.0)
1704        );
1705    }
1706
1707    #[rstest]
1708    #[case::exact_multiple(Capacity(12.0), Capacity(4.0), 3)] // 12 / 4 = 3
1709    #[case::rounded_up(Capacity(11.0), Capacity(4.0), 3)] // 11 / 4 = 2.75 -> 3
1710    #[case::unit_size_equals_capacity(Capacity(4.0), Capacity(4.0), 1)] // 4 / 4 = 1
1711    #[case::unit_size_greater_than_capacity(Capacity(3.0), Capacity(4.0), 1)] // 3 / 4 = 0.75 -> 1
1712    fn divide_asset(
1713        mut process: Process,
1714        #[case] capacity: Capacity,
1715        #[case] unit_size: Capacity,
1716        #[case] n_expected_children: usize,
1717    ) {
1718        process.unit_size = Some(unit_size);
1719        let asset = Asset::new_future(
1720            "agent1".into(),
1721            Rc::new(process),
1722            "GBR".into(),
1723            capacity,
1724            2010,
1725        )
1726        .unwrap();
1727
1728        assert!(asset.is_divisible(), "Asset should be divisible!");
1729
1730        let children = asset.divide_asset();
1731        assert_eq!(
1732            children.len(),
1733            n_expected_children,
1734            "Unexpected number of children"
1735        );
1736
1737        // Check all children have capacity equal to unit_size
1738        for child in children.clone() {
1739            assert_eq!(
1740                child.capacity.total_capacity(),
1741                unit_size,
1742                "Child capacity should equal unit_size"
1743            );
1744        }
1745
1746        // Check total capacity is >= parent capacity
1747        let total_child_capacity: Capacity = children
1748            .iter()
1749            .map(|child| child.capacity.total_capacity())
1750            .sum();
1751        assert!(
1752            total_child_capacity >= asset.capacity.total_capacity(),
1753            "Total capacity should be >= parent capacity"
1754        );
1755    }
1756
1757    #[rstest]
1758    fn asset_pool_new(asset_pool: AssetPool) {
1759        // Should be in order of commission year
1760        assert!(asset_pool.active.is_empty());
1761        assert!(asset_pool.future.len() == 2);
1762        assert!(asset_pool.future[0].commission_year == 2010);
1763        assert!(asset_pool.future[1].commission_year == 2020);
1764    }
1765
1766    #[rstest]
1767    fn asset_pool_commission_new1(mut asset_pool: AssetPool) {
1768        // Asset to be commissioned in this year
1769        asset_pool.commission_new(2010);
1770        assert_equal(asset_pool.iter_active(), iter::once(&asset_pool.active[0]));
1771    }
1772
1773    #[rstest]
1774    fn asset_pool_commission_new2(mut asset_pool: AssetPool) {
1775        // Commission year has passed
1776        asset_pool.commission_new(2011);
1777        assert_equal(asset_pool.iter_active(), iter::once(&asset_pool.active[0]));
1778    }
1779
1780    #[rstest]
1781    fn asset_pool_commission_new3(mut asset_pool: AssetPool) {
1782        // Nothing to commission for this year
1783        asset_pool.commission_new(2000);
1784        assert!(asset_pool.iter_active().next().is_none()); // no active assets
1785    }
1786
1787    #[rstest]
1788    fn asset_pool_commission_new_divisible(asset_divisible: Asset) {
1789        let commision_year = asset_divisible.commission_year;
1790        let expected_children = expected_children_for_divisible(&asset_divisible);
1791        let mut asset_pool = AssetPool::new(vec![asset_divisible.clone()]);
1792        assert!(asset_pool.active.is_empty());
1793        asset_pool.commission_new(commision_year);
1794        assert!(asset_pool.future.is_empty());
1795        assert!(!asset_pool.active.is_empty());
1796        assert_eq!(asset_pool.active.len(), expected_children);
1797        assert_eq!(asset_pool.next_group_id, 1);
1798    }
1799
1800    #[rstest]
1801    fn asset_pool_commission_already_decommissioned(asset: Asset) {
1802        let year = asset.max_decommission_year();
1803        let mut asset_pool = AssetPool::new(vec![asset]);
1804        assert!(asset_pool.active.is_empty());
1805        asset_pool.update_for_year(year);
1806        assert!(asset_pool.active.is_empty());
1807    }
1808
1809    #[rstest]
1810    fn asset_pool_decommission_old(mut asset_pool: AssetPool) {
1811        asset_pool.commission_new(2020);
1812        assert!(asset_pool.future.is_empty());
1813        assert_eq!(asset_pool.active.len(), 2);
1814        asset_pool.decommission_old(2030); // should decommission first asset (lifetime == 5)
1815        assert_eq!(asset_pool.active.len(), 1);
1816        assert_eq!(asset_pool.active[0].commission_year, 2020);
1817        assert_eq!(asset_pool.decommissioned.len(), 1);
1818        assert_eq!(asset_pool.decommissioned[0].commission_year, 2010);
1819        assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2030));
1820        asset_pool.decommission_old(2032); // nothing to decommission
1821        assert_eq!(asset_pool.active.len(), 1);
1822        assert_eq!(asset_pool.active[0].commission_year, 2020);
1823        assert_eq!(asset_pool.decommissioned.len(), 1);
1824        assert_eq!(asset_pool.decommissioned[0].commission_year, 2010);
1825        assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2030));
1826        asset_pool.decommission_old(2040); // should decommission second asset
1827        assert!(asset_pool.active.is_empty());
1828        assert_eq!(asset_pool.decommissioned.len(), 2);
1829        assert_eq!(asset_pool.decommissioned[0].commission_year, 2010);
1830        assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2030));
1831        assert_eq!(asset_pool.decommissioned[1].commission_year, 2020);
1832        assert_eq!(asset_pool.decommissioned[1].decommission_year(), Some(2040));
1833    }
1834
1835    #[rstest]
1836    fn asset_pool_get(mut asset_pool: AssetPool) {
1837        asset_pool.commission_new(2020);
1838        assert_eq!(asset_pool.get(AssetID(0)), Some(&asset_pool.active[0]));
1839        assert_eq!(asset_pool.get(AssetID(1)), Some(&asset_pool.active[1]));
1840    }
1841
1842    #[rstest]
1843    fn asset_pool_extend_empty(mut asset_pool: AssetPool) {
1844        // Start with commissioned assets
1845        asset_pool.commission_new(2020);
1846        let original_count = asset_pool.active.len();
1847
1848        // Extend with empty iterator
1849        asset_pool.extend(Vec::<AssetRef>::new());
1850
1851        assert_eq!(asset_pool.active.len(), original_count);
1852    }
1853
1854    #[rstest]
1855    fn asset_pool_extend_existing_assets(mut asset_pool: AssetPool) {
1856        // Start with some commissioned assets
1857        asset_pool.commission_new(2020);
1858        assert_eq!(asset_pool.active.len(), 2);
1859        let existing_assets = asset_pool.take();
1860
1861        // Extend with the same assets (should maintain their IDs)
1862        asset_pool.extend(existing_assets.clone());
1863
1864        assert_eq!(asset_pool.active.len(), 2);
1865        assert_eq!(asset_pool.active[0].id(), Some(AssetID(0)));
1866        assert_eq!(asset_pool.active[1].id(), Some(AssetID(1)));
1867    }
1868
1869    #[rstest]
1870    fn asset_pool_extend_new_assets(mut asset_pool: AssetPool, process: Process) {
1871        // Start with some commissioned assets
1872        asset_pool.commission_new(2020);
1873        let original_count = asset_pool.active.len();
1874
1875        // Create new non-commissioned assets
1876        let process_rc = Rc::new(process);
1877        let new_assets = vec![
1878            Asset::new_selected(
1879                "agent2".into(),
1880                Rc::clone(&process_rc),
1881                "GBR".into(),
1882                Capacity(1.5),
1883                2015,
1884            )
1885            .unwrap()
1886            .into(),
1887            Asset::new_selected(
1888                "agent3".into(),
1889                Rc::clone(&process_rc),
1890                "GBR".into(),
1891                Capacity(2.5),
1892                2020,
1893            )
1894            .unwrap()
1895            .into(),
1896        ];
1897
1898        asset_pool.extend(new_assets);
1899
1900        assert_eq!(asset_pool.active.len(), original_count + 2);
1901        // New assets should get IDs 2 and 3
1902        assert_eq!(asset_pool.active[original_count].id(), Some(AssetID(2)));
1903        assert_eq!(asset_pool.active[original_count + 1].id(), Some(AssetID(3)));
1904        assert_eq!(
1905            asset_pool.active[original_count].agent_id(),
1906            Some(&"agent2".into())
1907        );
1908        assert_eq!(
1909            asset_pool.active[original_count + 1].agent_id(),
1910            Some(&"agent3".into())
1911        );
1912    }
1913
1914    #[rstest]
1915    fn asset_pool_extend_new_divisible_assets(mut asset_pool: AssetPool, mut process: Process) {
1916        // Start with some commissioned assets
1917        asset_pool.commission_new(2020);
1918        let original_count = asset_pool.active.len();
1919
1920        // Create new non-commissioned assets
1921        process.unit_size = Some(Capacity(4.0));
1922        let process_rc = Rc::new(process);
1923        let new_assets: Vec<AssetRef> = vec![
1924            Asset::new_selected(
1925                "agent2".into(),
1926                Rc::clone(&process_rc),
1927                "GBR".into(),
1928                Capacity(11.0),
1929                2015,
1930            )
1931            .unwrap()
1932            .into(),
1933        ];
1934        let expected_children = expected_children_for_divisible(&new_assets[0]);
1935        asset_pool.extend(new_assets);
1936        assert_eq!(asset_pool.active.len(), original_count + expected_children);
1937    }
1938
1939    #[rstest]
1940    fn asset_pool_extend_mixed_assets(mut asset_pool: AssetPool, process: Process) {
1941        // Start with some commissioned assets
1942        asset_pool.commission_new(2020);
1943
1944        // Create a new non-commissioned asset
1945        let new_asset = Asset::new_selected(
1946            "agent_new".into(),
1947            process.into(),
1948            "GBR".into(),
1949            Capacity(3.0),
1950            2015,
1951        )
1952        .unwrap()
1953        .into();
1954
1955        // Extend with just the new asset (not mixing with existing to avoid duplicates)
1956        asset_pool.extend(vec![new_asset]);
1957
1958        assert_eq!(asset_pool.active.len(), 3);
1959        // Check that we have the original assets plus the new one
1960        assert!(asset_pool.active.iter().any(|a| a.id() == Some(AssetID(0))));
1961        assert!(asset_pool.active.iter().any(|a| a.id() == Some(AssetID(1))));
1962        assert!(asset_pool.active.iter().any(|a| a.id() == Some(AssetID(2))));
1963        // Check that the new asset has the correct agent
1964        assert!(
1965            asset_pool
1966                .active
1967                .iter()
1968                .any(|a| a.agent_id() == Some(&"agent_new".into()))
1969        );
1970    }
1971
1972    #[rstest]
1973    fn asset_pool_extend_maintains_sort_order(mut asset_pool: AssetPool, process: Process) {
1974        // Start with some commissioned assets
1975        asset_pool.commission_new(2020);
1976
1977        // Create new assets that would be out of order if added at the end
1978        let process_rc = Rc::new(process);
1979        let new_assets = vec![
1980            Asset::new_selected(
1981                "agent_high_id".into(),
1982                Rc::clone(&process_rc),
1983                "GBR".into(),
1984                Capacity(1.0),
1985                2010,
1986            )
1987            .unwrap()
1988            .into(),
1989            Asset::new_selected(
1990                "agent_low_id".into(),
1991                Rc::clone(&process_rc),
1992                "GBR".into(),
1993                Capacity(1.0),
1994                2015,
1995            )
1996            .unwrap()
1997            .into(),
1998        ];
1999
2000        asset_pool.extend(new_assets);
2001
2002        // Check that assets are sorted by ID
2003        let ids: Vec<u32> = asset_pool
2004            .iter_active()
2005            .map(|a| a.id().unwrap().0)
2006            .collect();
2007        assert_equal(ids, 0..4);
2008    }
2009
2010    #[rstest]
2011    fn asset_pool_extend_no_duplicates_expected(mut asset_pool: AssetPool) {
2012        // Start with some commissioned assets
2013        asset_pool.commission_new(2020);
2014        let original_count = asset_pool.active.len();
2015
2016        // The extend method expects unique assets - adding duplicates would violate
2017        // the debug assertion, so this test verifies the normal case
2018        asset_pool.extend(Vec::new());
2019
2020        assert_eq!(asset_pool.active.len(), original_count);
2021        // Verify all assets are still unique (this is what the debug_assert checks)
2022        assert_eq!(
2023            asset_pool.active.iter().unique().count(),
2024            asset_pool.active.len()
2025        );
2026    }
2027
2028    #[rstest]
2029    fn asset_pool_extend_increments_next_id(mut asset_pool: AssetPool, process: Process) {
2030        // Start with some commissioned assets
2031        asset_pool.commission_new(2020);
2032        assert_eq!(asset_pool.next_id, 2); // Should be 2 after commissioning 2 assets
2033
2034        // Create new non-commissioned assets
2035        let process_rc = Rc::new(process);
2036        let new_assets = vec![
2037            Asset::new_selected(
2038                "agent1".into(),
2039                Rc::clone(&process_rc),
2040                "GBR".into(),
2041                Capacity(1.0),
2042                2015,
2043            )
2044            .unwrap()
2045            .into(),
2046            Asset::new_selected(
2047                "agent2".into(),
2048                Rc::clone(&process_rc),
2049                "GBR".into(),
2050                Capacity(1.0),
2051                2020,
2052            )
2053            .unwrap()
2054            .into(),
2055        ];
2056
2057        asset_pool.extend(new_assets);
2058
2059        // next_id should have incremented for each new asset
2060        assert_eq!(asset_pool.next_id, 4);
2061        assert_eq!(asset_pool.active[2].id(), Some(AssetID(2)));
2062        assert_eq!(asset_pool.active[3].id(), Some(AssetID(3)));
2063    }
2064
2065    #[rstest]
2066    fn asset_pool_mothball_unretained(mut asset_pool: AssetPool) {
2067        // Commission some assets
2068        asset_pool.commission_new(2020);
2069        assert_eq!(asset_pool.active.len(), 2);
2070
2071        // Remove one asset from the active pool (simulating it being removed elsewhere)
2072        let removed_asset = asset_pool.active.remove(0);
2073        assert_eq!(asset_pool.active.len(), 1);
2074
2075        // Try to mothball both the removed asset (not in active) and an active asset
2076        let assets_to_check = vec![removed_asset.clone(), asset_pool.active[0].clone()];
2077        asset_pool.mothball_unretained(assets_to_check, 2025);
2078
2079        // Only the removed asset should be mothballed (since it's not in active pool)
2080        assert_eq!(asset_pool.active.len(), 2); // And should be back into the pool
2081        assert_eq!(asset_pool.active[0].get_mothballed_year(), Some(2025));
2082    }
2083
2084    #[rstest]
2085    fn asset_pool_decommission_unused(mut asset_pool: AssetPool) {
2086        // Commission some assets
2087        asset_pool.commission_new(2020);
2088        assert_eq!(asset_pool.active.len(), 2);
2089        assert_eq!(asset_pool.decommissioned.len(), 0);
2090
2091        // Make an asset unused for a few years
2092        let mothball_years: u32 = 10;
2093        asset_pool.active[0]
2094            .make_mut()
2095            .mothball(2025 - mothball_years);
2096
2097        assert_eq!(
2098            asset_pool.active[0].get_mothballed_year(),
2099            Some(2025 - mothball_years)
2100        );
2101
2102        // Decomission unused assets
2103        asset_pool.decommission_mothballed(2025, mothball_years);
2104
2105        // Only the removed asset should be decommissioned (since it's not in active pool)
2106        assert_eq!(asset_pool.active.len(), 1); // Active pool unchanged
2107        assert_eq!(asset_pool.decommissioned.len(), 1);
2108        assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2025));
2109    }
2110
2111    #[rstest]
2112    fn asset_pool_decommission_if_not_active_none_active(mut asset_pool: AssetPool) {
2113        // Commission some assets
2114        asset_pool.commission_new(2020);
2115        let all_assets = asset_pool.active.clone();
2116
2117        // Clear the active pool (simulating all assets being removed)
2118        asset_pool.active.clear();
2119
2120        // Try to mothball the assets that are no longer active
2121        asset_pool.mothball_unretained(all_assets.clone(), 2025);
2122
2123        // All assets should be mothballed
2124        assert_eq!(asset_pool.active.len(), 2);
2125        assert_eq!(asset_pool.active[0].id(), all_assets[0].id());
2126        assert_eq!(asset_pool.active[0].get_mothballed_year(), Some(2025));
2127        assert_eq!(asset_pool.active[1].id(), all_assets[1].id());
2128        assert_eq!(asset_pool.active[1].get_mothballed_year(), Some(2025));
2129    }
2130
2131    #[rstest]
2132    #[should_panic(expected = "Cannot mothball asset that has not been commissioned")]
2133    fn asset_pool_decommission_if_not_active_non_commissioned_asset(
2134        mut asset_pool: AssetPool,
2135        process: Process,
2136    ) {
2137        // Create a non-commissioned asset
2138        let non_commissioned_asset = Asset::new_future(
2139            "agent_new".into(),
2140            process.into(),
2141            "GBR".into(),
2142            Capacity(1.0),
2143            2015,
2144        )
2145        .unwrap()
2146        .into();
2147
2148        // This should panic because the asset was never commissioned
2149        asset_pool.mothball_unretained(vec![non_commissioned_asset], 2025);
2150    }
2151
2152    #[rstest]
2153    fn asset_commission(process: Process) {
2154        // Test successful commissioning of Future asset
2155        let process_rc = Rc::new(process);
2156        let mut asset1 = Asset::new_future(
2157            "agent1".into(),
2158            Rc::clone(&process_rc),
2159            "GBR".into(),
2160            Capacity(1.0),
2161            2020,
2162        )
2163        .unwrap();
2164        asset1.commission(AssetID(1), None, "");
2165        assert!(asset1.is_commissioned());
2166        assert_eq!(asset1.id(), Some(AssetID(1)));
2167
2168        // Test successful commissioning of Selected asset
2169        let mut asset2 = Asset::new_selected(
2170            "agent1".into(),
2171            Rc::clone(&process_rc),
2172            "GBR".into(),
2173            Capacity(1.0),
2174            2020,
2175        )
2176        .unwrap();
2177        asset2.commission(AssetID(2), None, "");
2178        assert!(asset2.is_commissioned());
2179        assert_eq!(asset2.id(), Some(AssetID(2)));
2180    }
2181
2182    #[rstest]
2183    #[case::commission_during_process_lifetime(2024, 2024)]
2184    #[case::decommission_after_process_lifetime_ends(2026, 2025)]
2185    fn asset_decommission(
2186        #[case] requested_decommission_year: u32,
2187        #[case] expected_decommission_year: u32,
2188        process: Process,
2189    ) {
2190        // Test successful commissioning of Future asset
2191        let process_rc = Rc::new(process);
2192        let mut asset = Asset::new_future(
2193            "agent1".into(),
2194            Rc::clone(&process_rc),
2195            "GBR".into(),
2196            Capacity(1.0),
2197            2020,
2198        )
2199        .unwrap();
2200        asset.commission(AssetID(1), None, "");
2201        assert!(asset.is_commissioned());
2202        assert_eq!(asset.id(), Some(AssetID(1)));
2203
2204        // Test successful decommissioning
2205        asset.decommission(requested_decommission_year, "");
2206        assert!(!asset.is_commissioned());
2207        assert_eq!(asset.decommission_year(), Some(expected_decommission_year));
2208    }
2209
2210    #[rstest]
2211    #[case::decommission_after_predefined_max_year(2026, 2025, Some(2025))]
2212    #[case::decommission_before_predefined_max_year(2024, 2024, Some(2025))]
2213    #[case::decommission_during_process_lifetime_end_no_max_year(2024, 2024, None)]
2214    #[case::decommission_after_process_lifetime_end_no_max_year(2026, 2025, None)]
2215    fn asset_decommission_with_max_decommission_year_predefined(
2216        #[case] requested_decommission_year: u32,
2217        #[case] expected_decommission_year: u32,
2218        #[case] max_decommission_year: Option<u32>,
2219        process: Process,
2220    ) {
2221        // Test successful commissioning of Future asset
2222        let process_rc = Rc::new(process);
2223        let mut asset = Asset::new_future_with_max_decommission(
2224            "agent1".into(),
2225            Rc::clone(&process_rc),
2226            "GBR".into(),
2227            Capacity(1.0),
2228            2020,
2229            max_decommission_year,
2230        )
2231        .unwrap();
2232        asset.commission(AssetID(1), None, "");
2233        assert!(asset.is_commissioned());
2234        assert_eq!(asset.id(), Some(AssetID(1)));
2235
2236        // Test successful decommissioning
2237        asset.decommission(requested_decommission_year, "");
2238        assert!(!asset.is_commissioned());
2239        assert_eq!(asset.decommission_year(), Some(expected_decommission_year));
2240    }
2241
2242    #[rstest]
2243    #[should_panic(expected = "Assets with state Candidate cannot be commissioned")]
2244    fn commission_wrong_states(process: Process) {
2245        let mut asset =
2246            Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap();
2247        asset.commission(AssetID(1), None, "");
2248    }
2249
2250    #[rstest]
2251    #[should_panic(expected = "Cannot decommission an asset that hasn't been commissioned")]
2252    fn decommission_wrong_state(process: Process) {
2253        let mut asset =
2254            Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap();
2255        asset.decommission(2025, "");
2256    }
2257
2258    #[test]
2259    fn commission_year_before_time_horizon() {
2260        let processes_patch = FilePatch::new("processes.csv")
2261            .with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
2262            .with_addition("GASDRV,Dry gas extraction,all,GASPRD,1980,2040,1.0,");
2263
2264        // Check we can run model with asset commissioned before time horizon (simple starts in
2265        // 2020)
2266        let patches = vec![
2267            processes_patch.clone(),
2268            FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,1980"),
2269        ];
2270        assert_patched_runs_ok_simple!(patches);
2271
2272        // This should fail if it is not one of the years supported by the process, though
2273        let patches = vec![
2274            processes_patch,
2275            FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,1970"),
2276        ];
2277        assert_validate_fails_with_simple!(
2278            patches,
2279            "Agent A0_GEX has asset with commission year 1970, not within process GASDRV commission years: 1980..=2040"
2280        );
2281    }
2282
2283    #[test]
2284    fn commission_year_after_time_horizon() {
2285        let processes_patch = FilePatch::new("processes.csv")
2286            .with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
2287            .with_addition("GASDRV,Dry gas extraction,all,GASPRD,2020,2050,1.0,");
2288
2289        // Check we can run model with asset commissioned after time horizon (simple ends in 2040)
2290        let patches = vec![
2291            processes_patch.clone(),
2292            FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,2050"),
2293        ];
2294        assert_patched_runs_ok_simple!(patches);
2295
2296        // This should fail if it is not one of the years supported by the process, though
2297        let patches = vec![
2298            processes_patch,
2299            FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,2060"),
2300        ];
2301        assert_validate_fails_with_simple!(
2302            patches,
2303            "Agent A0_GEX has asset with commission year 2060, not within process GASDRV commission years: 2020..=2050"
2304        );
2305    }
2306}