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, Year,
14};
15use anyhow::{Context, Result, ensure};
16use indexmap::IndexMap;
17use log::debug;
18use serde::{Deserialize, Serialize};
19use std::cell::Cell;
20use std::cmp::Ordering;
21use std::hash::{Hash, Hasher};
22use std::iter;
23use std::ops::{Deref, RangeInclusive};
24use std::rc::Rc;
25
26mod capacity;
27pub use capacity::AssetCapacity;
28mod pool;
29pub use pool::AssetPool;
30
31/// A unique identifier for an asset
32#[derive(
33    Clone,
34    Copy,
35    Debug,
36    derive_more::Display,
37    Eq,
38    Hash,
39    Ord,
40    PartialEq,
41    PartialOrd,
42    Deserialize,
43    Serialize,
44)]
45pub struct AssetID(u32);
46
47/// A unique identifier for an asset group
48#[derive(
49    Clone,
50    Copy,
51    Debug,
52    derive_more::Display,
53    Eq,
54    Hash,
55    Ord,
56    PartialEq,
57    PartialOrd,
58    Deserialize,
59    Serialize,
60)]
61pub struct AssetGroupID(u32);
62
63/// The state of an asset
64///
65/// New assets are created as either `Future` or `Candidate` assets. `Future` assets (which are
66/// specified in the input data) have a fixed capacity and capital costs already accounted for,
67/// whereas `Candidate` assets capital costs are not yet accounted for, and their capacity is
68/// determined by the investment algorithm.
69///
70/// `Future` and `Candidate` assets can be converted to `Commissioned` assets by calling
71/// the `commission` method (or via pool operations that commission future/selected assets).
72///
73/// `Commissioned` assets can be decommissioned by calling `decommission`.
74#[derive(Clone, Debug, PartialEq, strum::Display)]
75pub enum AssetState {
76    /// The asset has been commissioned
77    Commissioned {
78        /// The ID of the asset
79        id: AssetID,
80        /// The ID of the agent that owns the asset
81        agent_id: AgentID,
82        /// Year in which the asset was mothballed. None, if it is not mothballed
83        mothballed_year: Option<u32>,
84        /// Parent asset, if any.
85        ///
86        /// All divided assets have a parent, which tracks the total capacity across the children.
87        parent: Option<AssetRef>,
88    },
89    /// The asset has been decommissioned
90    Decommissioned {
91        /// The ID of the asset
92        id: AssetID,
93        /// The ID of the agent that owned the asset
94        agent_id: AgentID,
95        /// The year the asset was decommissioned
96        decommission_year: u32,
97    },
98    /// The asset is planned for commissioning in the future
99    Future {
100        /// The ID of the agent that will own the asset
101        agent_id: AgentID,
102    },
103    /// The asset has been selected for investment, but not yet confirmed
104    Selected {
105        /// The ID of the agent that would own the asset
106        agent_id: AgentID,
107    },
108    /// The asset is a parent of other assets.
109    ///
110    /// Parents are used for grouping (commissioned) divided assets, which can be used as an
111    /// optimisation.
112    Parent {
113        /// The ID of the agent which owns this asset's children
114        agent_id: AgentID,
115        /// ID of the asset group
116        group_id: AssetGroupID,
117    },
118    /// The asset is a candidate for investment but has not yet been selected by an agent
119    Candidate,
120}
121
122/// An asset controlled by an agent.
123#[derive(Clone, PartialEq)]
124pub struct Asset {
125    /// The status of the asset
126    state: AssetState,
127    /// The [`Process`] that this asset corresponds to
128    process: Rc<Process>,
129    /// Activity limits for this asset
130    activity_limits: Rc<ActivityLimits>,
131    /// The commodity flows for this asset
132    flows: Rc<IndexMap<CommodityID, ProcessFlow>>,
133    /// The [`ProcessParameter`] corresponding to the asset's region and commission year
134    process_parameter: Rc<ProcessParameter>,
135    /// The region in which the asset is located
136    region_id: RegionID,
137    /// Capacity of asset (for candidates this is a hypothetical capacity which may be altered)
138    capacity: Cell<AssetCapacity>,
139    /// The year the asset was/will be commissioned
140    commission_year: u32,
141    /// The maximum year that the asset could be decommissioned
142    max_decommission_year: u32,
143}
144
145impl Asset {
146    /// Create a new candidate asset
147    pub fn new_candidate(
148        process: Rc<Process>,
149        region_id: RegionID,
150        capacity: Capacity,
151        commission_year: u32,
152    ) -> Result<Self> {
153        let unit_size = process.unit_size;
154        Self::new_with_state(
155            AssetState::Candidate,
156            process,
157            region_id,
158            AssetCapacity::from_capacity(capacity, unit_size),
159            commission_year,
160            None,
161        )
162    }
163
164    /// Create a new candidate for use in dispatch runs
165    ///
166    /// These candidates will have a single continuous capacity specified by the model parameter
167    /// `candidate_asset_capacity`, regardless of whether the underlying process is divisible or
168    /// not.
169    pub fn new_candidate_for_dispatch(
170        process: Rc<Process>,
171        region_id: RegionID,
172        capacity: Capacity,
173        commission_year: u32,
174    ) -> Result<Self> {
175        Self::new_with_state(
176            AssetState::Candidate,
177            process,
178            region_id,
179            AssetCapacity::Continuous(capacity),
180            commission_year,
181            None,
182        )
183    }
184
185    /// Create a new candidate asset from a commissioned asset
186    pub fn new_candidate_from_commissioned(asset: &Asset) -> Self {
187        assert!(asset.is_commissioned(), "Asset must be commissioned");
188
189        Self {
190            state: AssetState::Candidate,
191            ..asset.clone()
192        }
193    }
194
195    /// Create a new future asset
196    pub fn new_future_with_max_decommission(
197        agent_id: AgentID,
198        process: Rc<Process>,
199        region_id: RegionID,
200        capacity: Capacity,
201        commission_year: u32,
202        max_decommission_year: Option<u32>,
203    ) -> Result<Self> {
204        check_capacity_valid_for_asset(capacity)?;
205        let unit_size = process.unit_size;
206        Self::new_with_state(
207            AssetState::Future { agent_id },
208            process,
209            region_id,
210            AssetCapacity::from_capacity(capacity, unit_size),
211            commission_year,
212            max_decommission_year,
213        )
214    }
215
216    /// Create a new future asset
217    pub fn new_future(
218        agent_id: AgentID,
219        process: Rc<Process>,
220        region_id: RegionID,
221        capacity: Capacity,
222        commission_year: u32,
223    ) -> Result<Self> {
224        Self::new_future_with_max_decommission(
225            agent_id,
226            process,
227            region_id,
228            capacity,
229            commission_year,
230            None,
231        )
232    }
233
234    /// Create a new selected asset
235    ///
236    /// This is only used for testing. In the real program, Selected assets can only be created from
237    /// Candidate assets by calling `select_candidate_for_investment`.
238    #[cfg(test)]
239    fn new_selected(
240        agent_id: AgentID,
241        process: Rc<Process>,
242        region_id: RegionID,
243        capacity: Capacity,
244        commission_year: u32,
245    ) -> Result<Self> {
246        let unit_size = process.unit_size;
247        Self::new_with_state(
248            AssetState::Selected { agent_id },
249            process,
250            region_id,
251            AssetCapacity::from_capacity(capacity, unit_size),
252            commission_year,
253            None,
254        )
255    }
256
257    /// Create a new commissioned asset
258    ///
259    /// This is only used for testing. WARNING: These assets always have an ID of zero, so can
260    /// create hash collisions. Use with care.
261    #[cfg(test)]
262    pub fn new_commissioned(
263        agent_id: AgentID,
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::Commissioned {
272                id: AssetID(0),
273                agent_id,
274                mothballed_year: None,
275                parent: None,
276            },
277            process,
278            region_id,
279            AssetCapacity::from_capacity(capacity, unit_size),
280            commission_year,
281            None,
282        )
283    }
284
285    /// Private helper to create an asset with the given state
286    fn new_with_state(
287        state: AssetState,
288        process: Rc<Process>,
289        region_id: RegionID,
290        capacity: AssetCapacity,
291        commission_year: u32,
292        max_decommission_year: Option<u32>,
293    ) -> Result<Self> {
294        check_region_year_valid_for_process(&process, &region_id, commission_year)?;
295        ensure!(
296            capacity.total_capacity() >= Capacity(0.0),
297            "Capacity must be non-negative"
298        );
299
300        // There should be activity limits, commodity flows and process parameters for all
301        // **milestone** years, but it is possible to have assets that are commissioned before the
302        // simulation start from assets.csv. We check for the presence of the params lazily to
303        // prevent users having to supply them for all the possible valid years before the time
304        // horizon.
305        let key = (region_id.clone(), commission_year);
306        let activity_limits = process
307            .activity_limits
308            .get(&key)
309            .with_context(|| {
310                format!(
311                    "No process availabilities supplied for process {} in region {} in year {}. \
312                    You should update process_availabilities.csv.",
313                    &process.id, region_id, commission_year
314                )
315            })?
316            .clone();
317        let flows = process
318            .flows
319            .get(&key)
320            .with_context(|| {
321                format!(
322                    "No commodity flows supplied for process {} in region {} in year {}. \
323                    You should update process_flows.csv.",
324                    &process.id, region_id, commission_year
325                )
326            })?
327            .clone();
328        let process_parameter = process
329            .parameters
330            .get(&key)
331            .with_context(|| {
332                format!(
333                    "No process parameters supplied for process {} in region {} in year {}. \
334                    You should update process_parameters.csv.",
335                    &process.id, region_id, commission_year
336                )
337            })?
338            .clone();
339
340        let max_decommission_year =
341            max_decommission_year.unwrap_or(commission_year + process_parameter.lifetime);
342        ensure!(
343            max_decommission_year >= commission_year,
344            "Max decommission year must be after/same as commission year"
345        );
346
347        Ok(Self {
348            state,
349            process,
350            activity_limits,
351            flows,
352            process_parameter,
353            region_id,
354            capacity: Cell::new(capacity),
355            commission_year,
356            max_decommission_year,
357        })
358    }
359
360    /// Get the state of this asset
361    pub fn state(&self) -> &AssetState {
362        &self.state
363    }
364
365    /// The process parameter for this asset
366    pub fn process_parameter(&self) -> &ProcessParameter {
367        &self.process_parameter
368    }
369
370    /// The last year in which this asset should be decommissioned
371    pub fn max_decommission_year(&self) -> u32 {
372        self.max_decommission_year
373    }
374
375    /// Get the activity limits per unit of capacity for this asset in a particular time slice
376    pub fn get_activity_per_capacity_limits(
377        &self,
378        time_slice: &TimeSliceID,
379    ) -> RangeInclusive<ActivityPerCapacity> {
380        let limits = &self.activity_limits.get_limit_for_time_slice(time_slice);
381        let cap2act = self.process.capacity_to_activity;
382        (cap2act * *limits.start())..=(cap2act * *limits.end())
383    }
384
385    /// Get the activity limits for this asset for a given time slice selection
386    pub fn get_activity_limits_for_selection(
387        &self,
388        time_slice_selection: &TimeSliceSelection,
389    ) -> RangeInclusive<Activity> {
390        let activity_per_capacity_limits = self.activity_limits.get_limit(time_slice_selection);
391        let cap2act = self.process.capacity_to_activity;
392        let max_activity = self.total_capacity() * cap2act;
393        let lb = max_activity * *activity_per_capacity_limits.start();
394        let ub = max_activity * *activity_per_capacity_limits.end();
395        lb..=ub
396    }
397
398    /// Iterate over activity limits for this asset
399    pub fn iter_activity_limits(
400        &self,
401    ) -> impl Iterator<Item = (TimeSliceSelection, RangeInclusive<Activity>)> + '_ {
402        let max_act = self.max_activity();
403        self.activity_limits
404            .iter_limits()
405            .map(move |(ts_sel, limit)| {
406                (
407                    ts_sel,
408                    (max_act * *limit.start())..=(max_act * *limit.end()),
409                )
410            })
411    }
412
413    /// Iterate over activity per capacity limits for this asset
414    pub fn iter_activity_per_capacity_limits(
415        &self,
416    ) -> impl Iterator<Item = (TimeSliceSelection, RangeInclusive<ActivityPerCapacity>)> + '_ {
417        let cap2act = self.process.capacity_to_activity;
418        self.activity_limits
419            .iter_limits()
420            .map(move |(ts_sel, limit)| {
421                (
422                    ts_sel,
423                    (cap2act * *limit.start())..=(cap2act * *limit.end()),
424                )
425            })
426    }
427
428    /// Gets the total SED/SVD output per unit of activity for this asset
429    ///
430    /// Note: Since we are summing coefficients from different commodities, this ONLY makes sense
431    /// if these commodities have the same units (e.g., all in PJ). Users are currently not made to
432    /// give units for commodities, so we cannot possibly enforce this. Something to potentially
433    /// address in future.
434    pub fn get_total_output_per_activity(&self) -> FlowPerActivity {
435        self.iter_output_flows().map(|flow| flow.coeff).sum()
436    }
437
438    /// Get the operating cost for this asset in a given year and time slice
439    pub fn get_operating_cost(&self, year: u32, time_slice: &TimeSliceID) -> MoneyPerActivity {
440        // The cost for all commodity flows (including levies/incentives)
441        let flows_cost = self
442            .iter_flows()
443            .map(|flow| flow.get_total_cost_per_activity(&self.region_id, year, time_slice))
444            .sum();
445
446        self.process_parameter.variable_operating_cost + flows_cost
447    }
448
449    /// Get the total revenue from all flows for this asset.
450    ///
451    /// If a price is missing, it is assumed to be zero.
452    pub fn get_revenue_from_flows(
453        &self,
454        prices: &CommodityPrices,
455        time_slice: &TimeSliceID,
456    ) -> MoneyPerActivity {
457        self.get_revenue_from_flows_with_filter(prices, time_slice, |_| true)
458    }
459
460    /// Get the total revenue from all flows excluding the primary output.
461    ///
462    /// If a price is missing, it is assumed to be zero.
463    pub fn get_revenue_from_flows_excluding_primary(
464        &self,
465        prices: &CommodityPrices,
466        time_slice: &TimeSliceID,
467    ) -> MoneyPerActivity {
468        let excluded_commodity = self.primary_output().map(|flow| &flow.commodity.id);
469
470        self.get_revenue_from_flows_with_filter(prices, time_slice, |flow| {
471            excluded_commodity.is_none_or(|commodity_id| commodity_id != &flow.commodity.id)
472        })
473    }
474
475    /// Get the total cost of purchasing input commodities per unit of activity for this asset.
476    ///
477    /// If a price is missing, there is assumed to be no cost.
478    pub fn get_input_cost_from_prices(
479        &self,
480        prices: &CommodityPrices,
481        time_slice: &TimeSliceID,
482    ) -> MoneyPerActivity {
483        // Revenues of input flows are negative costs, so we negate the result
484        -self.get_revenue_from_flows_with_filter(prices, time_slice, |x| {
485            x.direction() == FlowDirection::Input
486        })
487    }
488
489    /// Get the total revenue from a subset of flows.
490    ///
491    /// Takes a function as an argument to filter the flows. If a price is missing, it is assumed to
492    /// be zero.
493    fn get_revenue_from_flows_with_filter<F>(
494        &self,
495        prices: &CommodityPrices,
496        time_slice: &TimeSliceID,
497        mut filter_for_flows: F,
498    ) -> MoneyPerActivity
499    where
500        F: FnMut(&ProcessFlow) -> bool,
501    {
502        self.iter_flows()
503            .filter(|flow| filter_for_flows(flow))
504            .map(|flow| {
505                flow.coeff
506                    * prices
507                        .get(&flow.commodity.id, &self.region_id, time_slice)
508                        .unwrap_or(MoneyPerFlow(0.0))
509            })
510            .sum()
511    }
512
513    /// Get the generic activity cost per unit of activity for this asset.
514    ///
515    /// These are all activity-related costs that are not associated with specific SED/SVD outputs.
516    /// Includes levies, flow costs, costs of inputs and variable operating costs
517    fn get_generic_activity_cost(
518        &self,
519        prices: &CommodityPrices,
520        year: u32,
521        time_slice: &TimeSliceID,
522    ) -> MoneyPerActivity {
523        // The cost of purchasing input commodities
524        let cost_of_inputs = self.get_input_cost_from_prices(prices, time_slice);
525
526        // Flow costs/levies for all flows except SED/SVD outputs
527        let excludes_sed_svd_output = |flow: &&ProcessFlow| {
528            !(flow.direction() == FlowDirection::Output
529                && matches!(
530                    flow.commodity.kind,
531                    CommodityType::SupplyEqualsDemand | CommodityType::ServiceDemand
532                ))
533        };
534        let flow_costs = self
535            .iter_flows()
536            .filter(excludes_sed_svd_output)
537            .map(|flow| flow.get_total_cost_per_activity(&self.region_id, year, time_slice))
538            .sum();
539
540        cost_of_inputs + flow_costs + self.process_parameter.variable_operating_cost
541    }
542
543    /// Iterate over marginal costs for a filtered set of SED/SVD output commodities for this asset
544    ///
545    /// For each SED/SVD output commodity, the marginal cost is calculated as the sum of:
546    /// - Generic activity costs (variable operating costs, cost of purchasing inputs, plus all
547    ///   levies and flow costs not associated with specific SED/SVD outputs), which are
548    ///   shared equally over all SED/SVD outputs
549    /// - Production levies and flow costs for the specific SED/SVD output commodity
550    pub fn iter_marginal_costs_with_filter<'a>(
551        &'a self,
552        prices: &'a CommodityPrices,
553        year: u32,
554        time_slice: &'a TimeSliceID,
555        filter: impl Fn(&CommodityID) -> bool + 'a,
556    ) -> Box<dyn Iterator<Item = (CommodityID, MoneyPerFlow)> + 'a> {
557        // Iterator over SED/SVD output flows matching the filter
558        let mut output_flows_iter = self
559            .iter_output_flows()
560            .filter(move |flow| filter(&flow.commodity.id))
561            .peekable();
562
563        // If there are no output flows after filtering, return an empty iterator
564        if output_flows_iter.peek().is_none() {
565            return Box::new(std::iter::empty::<(CommodityID, MoneyPerFlow)>());
566        }
567
568        // Calculate generic activity costs.
569        // This is all activity costs not associated with specific SED/SVD outputs, which will get
570        // shared equally over all SED/SVD outputs. Includes levies, flow costs, costs of inputs and
571        // variable operating costs
572        let generic_activity_cost = self.get_generic_activity_cost(prices, year, time_slice);
573
574        // Share generic activity costs equally over all SED/SVD outputs
575        // We sum the output coefficients of all SED/SVD commodities to get total output, then
576        // divide costs by this total output to get the generic cost per unit of output.
577        // Note: only works if all SED/SVD outputs have the same units - not currently checked!
578        let total_output_per_activity = self.get_total_output_per_activity();
579        assert!(total_output_per_activity > FlowPerActivity::EPSILON); // input checks should guarantee this
580        let generic_cost_per_flow = generic_activity_cost / total_output_per_activity;
581
582        // Iterate over SED/SVD output flows
583        Box::new(output_flows_iter.map(move |flow| {
584            // Get the costs for this specific commodity flow
585            let commodity_specific_costs_per_flow =
586                flow.get_total_cost_per_flow(&self.region_id, year, time_slice);
587
588            // Add these to the generic costs to get total cost for this commodity
589            let marginal_cost = generic_cost_per_flow + commodity_specific_costs_per_flow;
590            (flow.commodity.id.clone(), marginal_cost)
591        }))
592    }
593
594    /// Iterate over marginal costs for all SED/SVD output commodities for this asset
595    ///
596    /// See `iter_marginal_costs_with_filter` for details.
597    pub fn iter_marginal_costs<'a>(
598        &'a self,
599        prices: &'a CommodityPrices,
600        year: u32,
601        time_slice: &'a TimeSliceID,
602    ) -> Box<dyn Iterator<Item = (CommodityID, MoneyPerFlow)> + 'a> {
603        self.iter_marginal_costs_with_filter(prices, year, time_slice, move |_| true)
604    }
605
606    /// Get the annual capital cost per unit of capacity for this asset
607    pub fn get_annual_capital_cost_per_capacity(&self) -> MoneyPerCapacity {
608        let capital_cost = self.process_parameter.capital_cost;
609        let lifetime = self.process_parameter.lifetime;
610        let discount_rate = self.process_parameter.discount_rate;
611        annual_capital_cost(capital_cost, lifetime, discount_rate)
612    }
613
614    /// Get the annual fixed costs (AFC) per unit of activity for this asset
615    ///
616    /// Total capital costs and fixed opex are shared equally over the year in accordance with the
617    /// annual activity.
618    pub fn get_annual_fixed_costs_per_activity(
619        &self,
620        annual_activity: Activity,
621    ) -> MoneyPerActivity {
622        let annual_capital_cost_per_capacity = self.get_annual_capital_cost_per_capacity();
623        let annual_fixed_opex = self.process_parameter.fixed_operating_cost * Year(1.0);
624        let total_annual_fixed_costs =
625            (annual_capital_cost_per_capacity + annual_fixed_opex) * self.total_capacity();
626        assert!(
627            annual_activity > Activity::EPSILON,
628            "Cannot calculate annual fixed costs per activity for an asset with zero annual activity"
629        );
630        total_annual_fixed_costs / annual_activity
631    }
632
633    /// Get the annual fixed costs (AFC) per unit of output flow for this asset
634    ///
635    /// Total capital costs and fixed opex are shared equally across all output flows in accordance
636    /// with the annual activity and total output per unit of activity.
637    pub fn get_annual_fixed_costs_per_flow(&self, annual_activity: Activity) -> MoneyPerFlow {
638        let annual_fixed_costs_per_activity =
639            self.get_annual_fixed_costs_per_activity(annual_activity);
640        let total_output_per_activity = self.get_total_output_per_activity();
641        assert!(total_output_per_activity > FlowPerActivity::EPSILON); // input checks should guarantee this
642        annual_fixed_costs_per_activity / total_output_per_activity
643    }
644
645    /// Maximum activity for this asset
646    pub fn max_activity(&self) -> Activity {
647        self.total_capacity() * self.process.capacity_to_activity
648    }
649
650    /// Get a specific process flow
651    pub fn get_flow(&self, commodity_id: &CommodityID) -> Option<&ProcessFlow> {
652        self.flows.get(commodity_id)
653    }
654
655    /// Iterate over the asset's flows
656    pub fn iter_flows(&self) -> impl Iterator<Item = &ProcessFlow> {
657        self.flows.values()
658    }
659
660    /// Iterate over the asset's output SED/SVD flows
661    pub fn iter_output_flows(&self) -> impl Iterator<Item = &ProcessFlow> {
662        self.flows.values().filter(|flow| {
663            flow.direction() == FlowDirection::Output
664                && matches!(
665                    flow.commodity.kind,
666                    CommodityType::SupplyEqualsDemand | CommodityType::ServiceDemand
667                )
668        })
669    }
670
671    /// Get the primary output flow (if any) for this asset
672    pub fn primary_output(&self) -> Option<&ProcessFlow> {
673        self.process
674            .primary_output
675            .as_ref()
676            .map(|commodity_id| &self.flows[commodity_id])
677    }
678
679    /// Whether this asset has been commissioned
680    pub fn is_commissioned(&self) -> bool {
681        matches!(&self.state, AssetState::Commissioned { .. })
682    }
683
684    /// Get the commission year for this asset
685    pub fn commission_year(&self) -> u32 {
686        self.commission_year
687    }
688
689    /// Get the decommission year for this asset
690    pub fn decommission_year(&self) -> Option<u32> {
691        match &self.state {
692            AssetState::Decommissioned {
693                decommission_year, ..
694            } => Some(*decommission_year),
695            _ => None,
696        }
697    }
698
699    /// Get the region ID for this asset
700    pub fn region_id(&self) -> &RegionID {
701        &self.region_id
702    }
703
704    /// Get the process for this asset
705    pub fn process(&self) -> &Process {
706        &self.process
707    }
708
709    /// Get the process ID for this asset
710    pub fn process_id(&self) -> &ProcessID {
711        &self.process.id
712    }
713
714    /// Get the ID for this asset
715    pub fn id(&self) -> Option<AssetID> {
716        match &self.state {
717            AssetState::Commissioned { id, .. } | AssetState::Decommissioned { id, .. } => {
718                Some(*id)
719            }
720            _ => None,
721        }
722    }
723
724    /// Get the parent asset of this asset, if any
725    pub fn parent(&self) -> Option<&AssetRef> {
726        match &self.state {
727            AssetState::Commissioned { parent, .. } => parent.as_ref(),
728            _ => None,
729        }
730    }
731
732    /// Whether this asset is a parent of divided assets
733    pub fn is_parent(&self) -> bool {
734        matches!(self.state, AssetState::Parent { .. })
735    }
736
737    /// Get the number of children this asset has.
738    ///
739    /// If this asset is not a parent, then `None` is returned.
740    pub fn num_children(&self) -> Option<u32> {
741        match &self.state {
742            AssetState::Parent { .. } => Some(self.capacity().n_units().unwrap()),
743            _ => None,
744        }
745    }
746
747    /// Get the group ID for this asset, if any
748    pub fn group_id(&self) -> Option<AssetGroupID> {
749        match &self.state {
750            AssetState::Commissioned { parent, .. } => {
751                // Get group ID from parent
752                parent
753                    .as_ref()
754                    // Safe because parents always have state `Parent`
755                    .map(|parent| parent.group_id().unwrap())
756            }
757            AssetState::Parent { group_id, .. } => Some(*group_id),
758            _ => None,
759        }
760    }
761
762    /// Get the agent ID for this asset, if any
763    pub fn agent_id(&self) -> Option<&AgentID> {
764        match &self.state {
765            AssetState::Commissioned { agent_id, .. }
766            | AssetState::Decommissioned { agent_id, .. }
767            | AssetState::Future { agent_id }
768            | AssetState::Selected { agent_id }
769            | AssetState::Parent { agent_id, .. } => Some(agent_id),
770            AssetState::Candidate => None,
771        }
772    }
773
774    /// Get the capacity for this asset
775    pub fn capacity(&self) -> AssetCapacity {
776        self.capacity.get()
777    }
778
779    /// Get the total capacity for this asset
780    pub fn total_capacity(&self) -> Capacity {
781        self.capacity().total_capacity()
782    }
783
784    /// Set the capacity for this asset (only for Candidate or Selected assets)
785    pub fn set_capacity(&mut self, capacity: AssetCapacity) {
786        assert!(
787            matches!(
788                self.state,
789                AssetState::Candidate | AssetState::Selected { .. }
790            ),
791            "set_capacity can only be called on Candidate or Selected assets"
792        );
793        assert!(
794            capacity.total_capacity() >= Capacity(0.0),
795            "Capacity must be >= 0"
796        );
797        self.capacity().assert_same_type(capacity);
798
799        // As `capacity` is a `Cell`, we don't actually need a `mut` ref to `self`, but allowing for
800        // changing the capacity of immutable refs would be potentially dangerous
801        self.capacity.set(capacity);
802    }
803
804    /// Increase the capacity for this asset (only for Candidate assets)
805    pub fn increase_capacity(&mut self, capacity: AssetCapacity) {
806        assert!(
807            self.state == AssetState::Candidate,
808            "increase_capacity can only be called on Candidate assets"
809        );
810        assert!(
811            capacity.total_capacity() > Capacity(0.0),
812            "Capacity increase must be positive"
813        );
814
815        // As `capacity` is a `Cell`, we don't actually need a `mut` ref to `self`, but allowing for
816        // changing the capacity of immutable refs would be potentially dangerous
817        self.capacity.update(|c| c + capacity);
818    }
819
820    /// Decrease the unit count (number of units) of this asset by one.
821    ///
822    /// Note that this method uses interior mutability so that we can operate on an immutable ref to
823    /// `self`. Accordingly, calling this method will result in a change in the capacity for all
824    /// `Rc` copies of the asset, which is potentially dangerous. This method is therefore private
825    /// and should **only** be used for the case where we want to decrease the unit count for parent
826    /// assets.
827    fn decrement_unit_count(&self) {
828        let AssetCapacity::Discrete(n_units, unit_size) = self.capacity() else {
829            panic!("Cannot decrement unit count of non-divisible asset");
830        };
831        assert!(n_units > 0, "Unit count has dropped below zero");
832
833        self.capacity
834            .set(AssetCapacity::Discrete(n_units - 1, unit_size));
835    }
836
837    /// Decommission this asset
838    fn decommission(&mut self, decommission_year: u32, reason: &str) {
839        let (id, agent_id, parent) = match &self.state {
840            AssetState::Commissioned {
841                id,
842                agent_id,
843                parent,
844                ..
845            } => (*id, agent_id.clone(), parent),
846            _ => panic!("Cannot decommission an asset that hasn't been commissioned"),
847        };
848        debug!(
849            "Decommissioning '{}' asset (ID: {}) for agent '{}' (reason: {})",
850            self.process_id(),
851            id,
852            agent_id,
853            reason
854        );
855
856        // If this is a child asset, we need to decrease the parent's capacity appropriately
857        if let Some(parent) = parent {
858            parent.decrement_unit_count();
859        }
860
861        self.state = AssetState::Decommissioned {
862            id,
863            agent_id,
864            decommission_year: decommission_year.min(self.max_decommission_year()),
865        };
866    }
867
868    /// Commission the asset.
869    ///
870    /// Only assets with an [`AssetState`] of `Future` or `Selected` can be commissioned. If the
871    /// asset's state is something else, this function will panic.
872    ///
873    /// # Arguments
874    ///
875    /// * `id` - The ID to give the newly commissioned asset
876    /// * `reason` - The reason for commissioning (included in log)
877    /// * `parent` - The parent asset, if this is a child asset
878    fn commission(&mut self, id: AssetID, parent: Option<AssetRef>, reason: &str) {
879        let agent_id = match &self.state {
880            AssetState::Future { agent_id } | AssetState::Selected { agent_id } => agent_id,
881            state => panic!("Assets with state {state} cannot be commissioned"),
882        };
883        debug!(
884            "Commissioning '{}' asset (ID: {}, capacity: {}) for agent '{}' (reason: {})",
885            self.process_id(),
886            id,
887            self.total_capacity(),
888            agent_id,
889            reason
890        );
891        self.state = AssetState::Commissioned {
892            id,
893            agent_id: agent_id.clone(),
894            mothballed_year: None,
895            parent,
896        };
897    }
898
899    /// Select a Candidate asset for investment, converting it to a Selected state
900    pub fn select_candidate_for_investment(&mut self, agent_id: AgentID) {
901        assert!(
902            self.state == AssetState::Candidate,
903            "select_candidate_for_investment can only be called on Candidate assets"
904        );
905        check_capacity_valid_for_asset(self.total_capacity()).unwrap();
906        self.state = AssetState::Selected { agent_id };
907    }
908
909    /// Set the year this asset was mothballed
910    pub fn mothball(&mut self, year: u32) {
911        let (id, agent_id, parent) = match &self.state {
912            AssetState::Commissioned {
913                id,
914                agent_id,
915                parent,
916                ..
917            } => (*id, agent_id.clone(), parent.clone()),
918            _ => panic!("Cannot mothball an asset that hasn't been commissioned"),
919        };
920        self.state = AssetState::Commissioned {
921            id,
922            agent_id,
923            mothballed_year: Some(year),
924            parent,
925        };
926    }
927
928    /// Remove the mothballed year - presumably because the asset has been used
929    pub fn unmothball(&mut self) {
930        let (id, agent_id, parent) = match &self.state {
931            AssetState::Commissioned {
932                id,
933                agent_id,
934                parent,
935                ..
936            } => (*id, agent_id.clone(), parent.clone()),
937            _ => panic!("Cannot unmothball an asset that hasn't been commissioned"),
938        };
939        self.state = AssetState::Commissioned {
940            id,
941            agent_id,
942            mothballed_year: None,
943            parent,
944        };
945    }
946
947    /// Get the mothballed year for the asset
948    pub fn get_mothballed_year(&self) -> Option<u32> {
949        let AssetState::Commissioned {
950            mothballed_year, ..
951        } = &self.state
952        else {
953            panic!("Cannot get mothballed year for an asset that hasn't been commissioned")
954        };
955        *mothballed_year
956    }
957
958    /// Get the unit size for this asset's capacity (if any)
959    pub fn unit_size(&self) -> Option<Capacity> {
960        match self.capacity() {
961            AssetCapacity::Discrete(_, size) => Some(size),
962            AssetCapacity::Continuous(_) => None,
963        }
964    }
965
966    /// For non-commissioned assets, get the maximum capacity permitted to be installed based on the
967    /// investment constraints for the asset's process.
968    ///
969    /// The limit is taken from the process's investment constraints for the asset's region and
970    /// commission year, and the portion of the commodity demand being considered.
971    ///
972    /// For divisible assets, the returned capacity will be rounded down to the nearest multiple of
973    /// the asset's unit size.
974    pub fn max_installable_capacity(
975        &self,
976        commodity_portion: Dimensionless,
977    ) -> Option<AssetCapacity> {
978        assert!(
979            !self.is_commissioned(),
980            "max_installable_capacity can only be called on uncommissioned assets"
981        );
982        assert!(
983            commodity_portion >= Dimensionless(0.0) && commodity_portion <= Dimensionless(1.0),
984            "commodity_portion must be between 0 and 1 inclusive"
985        );
986
987        self.process
988            .investment_constraints
989            .get(&(self.region_id.clone(), self.commission_year))
990            .and_then(|c| c.get_addition_limit().map(|l| l * commodity_portion))
991            .map(|limit| AssetCapacity::from_capacity_floor(limit, self.unit_size()))
992    }
993}
994
995#[allow(clippy::missing_fields_in_debug)]
996impl std::fmt::Debug for Asset {
997    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
998        f.debug_struct("Asset")
999            .field("state", &self.state)
1000            .field("process_id", &self.process_id())
1001            .field("region_id", &self.region_id)
1002            .field("capacity", &self.total_capacity())
1003            .field("commission_year", &self.commission_year)
1004            .finish()
1005    }
1006}
1007
1008/// Whether the process operates in the specified region and year
1009pub fn check_region_year_valid_for_process(
1010    process: &Process,
1011    region_id: &RegionID,
1012    year: u32,
1013) -> Result<()> {
1014    ensure!(
1015        process.regions.contains(region_id),
1016        "Process {} does not operate in region {}",
1017        process.id,
1018        region_id
1019    );
1020    ensure!(
1021        process.active_for_year(year),
1022        "Process {} does not operate in the year {}",
1023        process.id,
1024        year
1025    );
1026    Ok(())
1027}
1028
1029/// Whether the specified value is a valid capacity for an asset
1030pub fn check_capacity_valid_for_asset(capacity: Capacity) -> Result<()> {
1031    ensure!(
1032        capacity.is_finite() && capacity > Capacity(0.0),
1033        "Capacity must be a finite, positive number"
1034    );
1035    Ok(())
1036}
1037
1038/// A wrapper around [`Asset`] for storing references in maps.
1039///
1040/// If the asset has been commissioned, then comparison and hashing is done based on the asset ID,
1041/// otherwise a combination of other parameters is used.
1042///
1043/// [`Ord`] is implemented for [`AssetRef`], but it will panic for non-commissioned assets.
1044#[derive(Clone, Debug)]
1045pub struct AssetRef(Rc<Asset>);
1046
1047impl AssetRef {
1048    /// Make a mutable reference to the underlying [`Asset`]
1049    pub fn make_mut(&mut self) -> &mut Asset {
1050        Rc::make_mut(&mut self.0)
1051    }
1052
1053    /// Apply a function to each of this asset's children, consuming the asset in the process.
1054    ///
1055    /// If this asset is divisible, the first argument to `f` will be this asset after it has been
1056    /// converted to a parent and the second will be each child.
1057    ///
1058    /// If this asset is non-divisible (i.e. does not have a discrete capacity), then `f` will be
1059    /// called with the first argument set to `None` and the second will be `self`.
1060    ///
1061    /// When the asset has a discrete capacity, each of the children will be made up of a single
1062    /// unit of the original asset's unit size.
1063    ///
1064    /// Panics if this asset's state is not `Future` or `Selected`.
1065    fn into_for_each_child<F>(mut self, next_group_id: &mut u32, mut f: F)
1066    where
1067        F: FnMut(Option<&AssetRef>, AssetRef),
1068    {
1069        assert!(
1070            matches!(
1071                self.state,
1072                AssetState::Future { .. } | AssetState::Selected { .. }
1073            ),
1074            "Assets with state {} cannot be divided. Only Future or Selected assets can be divided",
1075            self.state
1076        );
1077
1078        let AssetCapacity::Discrete(n_units, unit_size) = self.capacity() else {
1079            // Asset is non-divisible
1080            f(None, self);
1081            return;
1082        };
1083
1084        // Create a child of size `unit_size`
1085        let child = AssetRef::from(Asset {
1086            capacity: Cell::new(AssetCapacity::Discrete(1, unit_size)),
1087            ..Asset::clone(&self)
1088        });
1089
1090        // Turn this asset into a parent
1091        let agent_id = self.agent_id().unwrap().clone();
1092        self.make_mut().state = AssetState::Parent {
1093            agent_id,
1094            group_id: AssetGroupID(*next_group_id),
1095        };
1096        *next_group_id += 1;
1097
1098        // Run `f` over each child
1099        for child in iter::repeat_n(child, n_units as usize) {
1100            f(Some(&self), child);
1101        }
1102    }
1103}
1104
1105impl From<Rc<Asset>> for AssetRef {
1106    fn from(value: Rc<Asset>) -> Self {
1107        Self(value)
1108    }
1109}
1110
1111impl From<Asset> for AssetRef {
1112    fn from(value: Asset) -> Self {
1113        Self::from(Rc::new(value))
1114    }
1115}
1116
1117impl From<AssetRef> for Rc<Asset> {
1118    fn from(value: AssetRef) -> Self {
1119        value.0
1120    }
1121}
1122
1123impl Deref for AssetRef {
1124    type Target = Asset;
1125
1126    fn deref(&self) -> &Self::Target {
1127        &self.0
1128    }
1129}
1130
1131impl PartialEq for AssetRef {
1132    fn eq(&self, other: &Self) -> bool {
1133        // For assets to be considered equal, they must have the same process, region, commission
1134        // year and state
1135        Rc::ptr_eq(&self.0.process, &other.0.process)
1136            && self.0.region_id == other.0.region_id
1137            && self.0.commission_year == other.0.commission_year
1138            && self.0.state == other.0.state
1139    }
1140}
1141
1142impl Eq for AssetRef {}
1143
1144impl Hash for AssetRef {
1145    /// Hash an asset according to its state:
1146    /// - Commissioned assets are hashed based on their ID alone
1147    /// - Selected assets are hashed based on `process_id`, `region_id`, `commission_year` and
1148    ///   `agent_id`
1149    /// - Candidate assets are hashed based on `process_id`, `region_id` and `commission_year`
1150    /// - Parent assets are hashed based on `agent_id` and `group_id`
1151    /// - Future and Decommissioned assets cannot currently be hashed
1152    fn hash<H: Hasher>(&self, state: &mut H) {
1153        match &self.0.state {
1154            AssetState::Commissioned { id, .. } => {
1155                // Hashed based on their ID alone, since this is sufficient to uniquely identify the
1156                // asset
1157                id.hash(state);
1158            }
1159            AssetState::Candidate | AssetState::Selected { .. } | AssetState::Parent { .. } => {
1160                self.0.process.id.hash(state);
1161                self.0.region_id.hash(state);
1162                self.0.commission_year.hash(state);
1163                self.0.agent_id().hash(state);
1164                self.0.group_id().hash(state);
1165            }
1166            state => {
1167                // We don't need to hash other types of asset
1168                panic!("Cannot hash {state} assets");
1169            }
1170        }
1171    }
1172}
1173
1174impl PartialOrd for AssetRef {
1175    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1176        Some(self.cmp(other))
1177    }
1178}
1179
1180impl Ord for AssetRef {
1181    fn cmp(&self, other: &Self) -> Ordering {
1182        self.id().unwrap().cmp(&other.id().unwrap())
1183    }
1184}
1185
1186/// Additional methods for iterating over assets
1187pub trait AssetIterator<'a>: Iterator<Item = &'a AssetRef> + Sized
1188where
1189    Self: 'a,
1190{
1191    /// Filter assets by the agent that owns them
1192    fn filter_agent(self, agent_id: &'a AgentID) -> impl Iterator<Item = &'a AssetRef> + 'a {
1193        self.filter(move |asset| asset.agent_id() == Some(agent_id))
1194    }
1195
1196    /// Iterate over assets that have the given commodity as a primary output
1197    fn filter_primary_producers_of(
1198        self,
1199        commodity_id: &'a CommodityID,
1200    ) -> impl Iterator<Item = &'a AssetRef> + 'a {
1201        self.filter(move |asset| {
1202            asset
1203                .primary_output()
1204                .is_some_and(|flow| &flow.commodity.id == commodity_id)
1205        })
1206    }
1207
1208    /// Filter the assets by region
1209    fn filter_region(self, region_id: &'a RegionID) -> impl Iterator<Item = &'a AssetRef> + 'a {
1210        self.filter(move |asset| asset.region_id == *region_id)
1211    }
1212
1213    /// Iterate over process flows affecting the given commodity
1214    fn flows_for_commodity(
1215        self,
1216        commodity_id: &'a CommodityID,
1217    ) -> impl Iterator<Item = (&'a AssetRef, &'a ProcessFlow)> + 'a {
1218        self.filter_map(|asset| Some((asset, asset.get_flow(commodity_id)?)))
1219    }
1220}
1221
1222impl<'a, I> AssetIterator<'a> for I where I: Iterator<Item = &'a AssetRef> + Sized + 'a {}
1223
1224#[cfg(test)]
1225mod tests {
1226    use super::*;
1227    use crate::commodity::Commodity;
1228    use crate::fixture::{
1229        assert_error, assert_patched_runs_ok_simple, assert_validate_fails_with_simple, asset,
1230        asset_divisible, process, process_activity_limits_map, process_flows_map, region_id,
1231        svd_commodity, time_slice, time_slice_info,
1232    };
1233    use crate::patch::FilePatch;
1234    use crate::process::{FlowType, Process, ProcessFlow};
1235    use crate::region::RegionID;
1236    use crate::time_slice::{TimeSliceID, TimeSliceInfo};
1237    use crate::units::{
1238        ActivityPerCapacity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity,
1239        MoneyPerFlow,
1240    };
1241    use float_cmp::assert_approx_eq;
1242    use indexmap::indexmap;
1243    use rstest::{fixture, rstest};
1244    use std::rc::Rc;
1245
1246    #[rstest]
1247    fn get_input_cost_from_prices_works(
1248        region_id: RegionID,
1249        svd_commodity: Commodity,
1250        mut process: Process,
1251        time_slice: TimeSliceID,
1252    ) {
1253        // Update the process flows using the existing commodity fixture
1254        let commodity_rc = Rc::new(svd_commodity);
1255        let process_flow = ProcessFlow {
1256            commodity: Rc::clone(&commodity_rc),
1257            coeff: FlowPerActivity(-2.0), // Input
1258            kind: FlowType::Fixed,
1259            cost: MoneyPerFlow(0.0),
1260        };
1261        let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() };
1262        let process_flows_map = process_flows_map(process.regions.clone(), Rc::new(process_flows));
1263        process.flows = process_flows_map;
1264
1265        // Create asset
1266        let asset =
1267            Asset::new_candidate(Rc::new(process), region_id.clone(), Capacity(1.0), 2020).unwrap();
1268
1269        // Set input prices
1270        let mut input_prices = CommodityPrices::default();
1271        input_prices.insert(&commodity_rc.id, &region_id, &time_slice, MoneyPerFlow(3.0));
1272
1273        // Call function
1274        let cost = asset.get_input_cost_from_prices(&input_prices, &time_slice);
1275        // Should be -coeff * price = -(-2.0) * 3.0 = 6.0
1276        assert_approx_eq!(MoneyPerActivity, cost, MoneyPerActivity(6.0));
1277    }
1278
1279    #[rstest]
1280    #[case(Capacity(0.01))]
1281    #[case(Capacity(0.5))]
1282    #[case(Capacity(1.0))]
1283    #[case(Capacity(100.0))]
1284    fn asset_new_valid(process: Process, #[case] capacity: Capacity) {
1285        let agent_id = AgentID("agent1".into());
1286        let region_id = RegionID("GBR".into());
1287        let asset = Asset::new_future(agent_id, process.into(), region_id, capacity, 2015).unwrap();
1288        assert!(asset.id().is_none());
1289    }
1290
1291    #[rstest]
1292    #[case(Capacity(0.0))]
1293    #[case(Capacity(-0.01))]
1294    #[case(Capacity(-1.0))]
1295    #[case(Capacity(f64::NAN))]
1296    #[case(Capacity(f64::INFINITY))]
1297    #[case(Capacity(f64::NEG_INFINITY))]
1298    fn asset_new_invalid_capacity(process: Process, #[case] capacity: Capacity) {
1299        let agent_id = AgentID("agent1".into());
1300        let region_id = RegionID("GBR".into());
1301        assert_error!(
1302            Asset::new_future(agent_id, process.into(), region_id, capacity, 2015),
1303            "Capacity must be a finite, positive number"
1304        );
1305    }
1306
1307    #[rstest]
1308    fn asset_new_invalid_commission_year(process: Process) {
1309        let agent_id = AgentID("agent1".into());
1310        let region_id = RegionID("GBR".into());
1311        assert_error!(
1312            Asset::new_future(agent_id, process.into(), region_id, Capacity(1.0), 2007),
1313            "Process process1 does not operate in the year 2007"
1314        );
1315    }
1316
1317    #[rstest]
1318    fn asset_new_invalid_region(process: Process) {
1319        let agent_id = AgentID("agent1".into());
1320        let region_id = RegionID("FRA".into());
1321        assert_error!(
1322            Asset::new_future(agent_id, process.into(), region_id, Capacity(1.0), 2015),
1323            "Process process1 does not operate in region FRA"
1324        );
1325    }
1326
1327    #[fixture]
1328    fn process_with_activity_limits(
1329        mut process: Process,
1330        time_slice_info: TimeSliceInfo,
1331        time_slice: TimeSliceID,
1332    ) -> Process {
1333        // Add activity limits to the process
1334        let mut activity_limits = ActivityLimits::new_with_full_availability(&time_slice_info);
1335        activity_limits.add_time_slice_limit(time_slice, Dimensionless(0.1)..=Dimensionless(0.5));
1336        process.activity_limits =
1337            process_activity_limits_map(process.regions.clone(), activity_limits);
1338
1339        // Update cap2act
1340        process.capacity_to_activity = ActivityPerCapacity(2.0);
1341        process
1342    }
1343
1344    #[fixture]
1345    fn asset_with_activity_limits(process_with_activity_limits: Process) -> Asset {
1346        Asset::new_future(
1347            "agent1".into(),
1348            Rc::new(process_with_activity_limits),
1349            "GBR".into(),
1350            Capacity(2.0),
1351            2010,
1352        )
1353        .unwrap()
1354    }
1355
1356    #[rstest]
1357    fn asset_get_activity_per_capacity_limits(
1358        asset_with_activity_limits: Asset,
1359        time_slice: TimeSliceID,
1360    ) {
1361        // With cap2act of 2, and activity limits of 0.1..=0.5, should get 0.2..=1.0
1362        assert_eq!(
1363            asset_with_activity_limits.get_activity_per_capacity_limits(&time_slice),
1364            ActivityPerCapacity(0.2)..=ActivityPerCapacity(1.0)
1365        );
1366    }
1367
1368    #[rstest]
1369    #[case::exact_multiple(Capacity(12.0), Capacity(4.0), 3)] // 12 / 4 = 3
1370    #[case::rounded_up(Capacity(11.0), Capacity(4.0), 3)] // 11 / 4 = 2.75 -> 3
1371    #[case::unit_size_equals_capacity(Capacity(4.0), Capacity(4.0), 1)] // 4 / 4 = 1
1372    #[case::unit_size_greater_than_capacity(Capacity(3.0), Capacity(4.0), 1)] // 3 / 4 = 0.75 -> 1
1373    fn into_for_each_child_divisible(
1374        mut process: Process,
1375        #[case] capacity: Capacity,
1376        #[case] unit_size: Capacity,
1377        #[case] n_expected_children: usize,
1378    ) {
1379        process.unit_size = Some(unit_size);
1380        let asset = AssetRef::from(
1381            Asset::new_future(
1382                "agent1".into(),
1383                Rc::new(process),
1384                "GBR".into(),
1385                capacity,
1386                2010,
1387            )
1388            .unwrap(),
1389        );
1390
1391        let mut count = 0;
1392        let mut total_child_capacity = Capacity(0.0);
1393        asset.clone().into_for_each_child(&mut 0, |parent, child| {
1394            assert!(parent.is_some_and(|parent| matches!(parent.state, AssetState::Parent { .. })));
1395
1396            // Check each child has capacity equal to unit_size
1397            assert_eq!(
1398                child.total_capacity(),
1399                unit_size,
1400                "Child capacity should equal unit_size"
1401            );
1402
1403            total_child_capacity += child.total_capacity();
1404            count += 1;
1405        });
1406        assert_eq!(count, n_expected_children, "Unexpected number of children");
1407
1408        // Check total capacity is >= parent capacity
1409        assert!(
1410            total_child_capacity >= asset.total_capacity(),
1411            "Total capacity should be >= parent capacity"
1412        );
1413    }
1414
1415    #[rstest]
1416    fn into_for_each_child_nondivisible(asset: Asset) {
1417        assert!(
1418            asset.process.unit_size.is_none(),
1419            "Asset should be non-divisible"
1420        );
1421
1422        let asset = AssetRef::from(asset);
1423        let mut count = 0;
1424        asset.clone().into_for_each_child(&mut 0, |parent, child| {
1425            assert!(parent.is_none());
1426            assert_eq!(child, asset);
1427            count += 1;
1428        });
1429        assert_eq!(count, 1);
1430    }
1431
1432    #[rstest]
1433    fn asset_commission(process: Process) {
1434        // Test successful commissioning of Future asset
1435        let process_rc = Rc::new(process);
1436        let mut asset1 = Asset::new_future(
1437            "agent1".into(),
1438            Rc::clone(&process_rc),
1439            "GBR".into(),
1440            Capacity(1.0),
1441            2020,
1442        )
1443        .unwrap();
1444        asset1.commission(AssetID(1), None, "");
1445        assert!(asset1.is_commissioned());
1446        assert_eq!(asset1.id(), Some(AssetID(1)));
1447
1448        // Test successful commissioning of Selected asset
1449        let mut asset2 = Asset::new_selected(
1450            "agent1".into(),
1451            Rc::clone(&process_rc),
1452            "GBR".into(),
1453            Capacity(1.0),
1454            2020,
1455        )
1456        .unwrap();
1457        asset2.commission(AssetID(2), None, "");
1458        assert!(asset2.is_commissioned());
1459        assert_eq!(asset2.id(), Some(AssetID(2)));
1460    }
1461
1462    #[rstest]
1463    #[case::commission_during_process_lifetime(2024, 2024)]
1464    #[case::decommission_after_process_lifetime_ends(2026, 2025)]
1465    fn asset_decommission(
1466        #[case] requested_decommission_year: u32,
1467        #[case] expected_decommission_year: u32,
1468        process: Process,
1469    ) {
1470        // Test successful commissioning of Future asset
1471        let process_rc = Rc::new(process);
1472        let mut asset = Asset::new_future(
1473            "agent1".into(),
1474            Rc::clone(&process_rc),
1475            "GBR".into(),
1476            Capacity(1.0),
1477            2020,
1478        )
1479        .unwrap();
1480        asset.commission(AssetID(1), None, "");
1481        assert!(asset.is_commissioned());
1482        assert_eq!(asset.id(), Some(AssetID(1)));
1483
1484        // Test successful decommissioning
1485        asset.decommission(requested_decommission_year, "");
1486        assert!(!asset.is_commissioned());
1487        assert_eq!(asset.decommission_year(), Some(expected_decommission_year));
1488    }
1489
1490    #[rstest]
1491    #[case::decommission_after_predefined_max_year(2026, 2025, Some(2025))]
1492    #[case::decommission_before_predefined_max_year(2024, 2024, Some(2025))]
1493    #[case::decommission_during_process_lifetime_end_no_max_year(2024, 2024, None)]
1494    #[case::decommission_after_process_lifetime_end_no_max_year(2026, 2025, None)]
1495    fn asset_decommission_with_max_decommission_year_predefined(
1496        #[case] requested_decommission_year: u32,
1497        #[case] expected_decommission_year: u32,
1498        #[case] max_decommission_year: Option<u32>,
1499        process: Process,
1500    ) {
1501        // Test successful commissioning of Future asset
1502        let process_rc = Rc::new(process);
1503        let mut asset = Asset::new_future_with_max_decommission(
1504            "agent1".into(),
1505            Rc::clone(&process_rc),
1506            "GBR".into(),
1507            Capacity(1.0),
1508            2020,
1509            max_decommission_year,
1510        )
1511        .unwrap();
1512        asset.commission(AssetID(1), None, "");
1513        assert!(asset.is_commissioned());
1514        assert_eq!(asset.id(), Some(AssetID(1)));
1515
1516        // Test successful decommissioning
1517        asset.decommission(requested_decommission_year, "");
1518        assert!(!asset.is_commissioned());
1519        assert_eq!(asset.decommission_year(), Some(expected_decommission_year));
1520    }
1521
1522    #[rstest]
1523    fn asset_decommission_divisible(asset_divisible: Asset) {
1524        let asset = AssetRef::from(asset_divisible);
1525        let original_capacity = asset.capacity();
1526
1527        // Commission children
1528        let mut children = Vec::new();
1529        let mut next_id = 0;
1530        asset.into_for_each_child(&mut 0, |parent, mut child| {
1531            child
1532                .make_mut()
1533                .commission(AssetID(next_id), parent.cloned(), "");
1534            next_id += 1;
1535            children.push(child);
1536        });
1537
1538        let parent = children[0].parent().unwrap().clone();
1539        assert_eq!(parent.capacity(), original_capacity);
1540        children[0].make_mut().decommission(2020, "");
1541
1542        let AssetCapacity::Discrete(original_units, original_unit_size) = original_capacity else {
1543            panic!("Capacity type should be discrete");
1544        };
1545        assert_eq!(
1546            parent.capacity(),
1547            AssetCapacity::Discrete(original_units - 1, original_unit_size)
1548        );
1549    }
1550
1551    #[rstest]
1552    #[should_panic(expected = "Assets with state Candidate cannot be commissioned")]
1553    fn commission_wrong_states(process: Process) {
1554        let mut asset =
1555            Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap();
1556        asset.commission(AssetID(1), None, "");
1557    }
1558
1559    #[rstest]
1560    #[should_panic(expected = "Cannot decommission an asset that hasn't been commissioned")]
1561    fn decommission_wrong_state(process: Process) {
1562        let mut asset =
1563            Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap();
1564        asset.decommission(2025, "");
1565    }
1566
1567    #[test]
1568    fn commission_year_before_time_horizon() {
1569        let processes_patch = FilePatch::new("processes.csv")
1570            .with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
1571            .with_addition("GASDRV,Dry gas extraction,all,GASPRD,1980,2040,1.0,");
1572
1573        // Check we can run model with asset commissioned before time horizon (simple starts in
1574        // 2020)
1575        let patches = vec![
1576            processes_patch.clone(),
1577            FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,1980"),
1578        ];
1579        assert_patched_runs_ok_simple!(patches);
1580
1581        // This should fail if it is not one of the years supported by the process, though
1582        let patches = vec![
1583            processes_patch,
1584            FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,1970"),
1585        ];
1586        assert_validate_fails_with_simple!(
1587            patches,
1588            "Agent A0_GEX has asset with commission year 1970, not within process GASDRV commission years: 1980..=2040"
1589        );
1590    }
1591
1592    #[test]
1593    fn commission_year_after_time_horizon() {
1594        let processes_patch = FilePatch::new("processes.csv")
1595            .with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
1596            .with_addition("GASDRV,Dry gas extraction,all,GASPRD,2020,2050,1.0,");
1597
1598        // Check we can run model with asset commissioned after time horizon (simple ends in 2040)
1599        let patches = vec![
1600            processes_patch.clone(),
1601            FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,2050"),
1602        ];
1603        assert_patched_runs_ok_simple!(patches);
1604
1605        // This should fail if it is not one of the years supported by the process, though
1606        let patches = vec![
1607            processes_patch,
1608            FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,2060"),
1609        ];
1610        assert_validate_fails_with_simple!(
1611            patches,
1612            "Agent A0_GEX has asset with commission year 2060, not within process GASDRV commission years: 2020..=2050"
1613        );
1614    }
1615
1616    #[rstest]
1617    fn max_installable_capacity(mut process: Process, region_id: RegionID) {
1618        // Set an addition limit of 3 for (region, year 2015)
1619        process.investment_constraints.insert(
1620            (region_id.clone(), 2015),
1621            Rc::new(crate::process::ProcessInvestmentConstraint {
1622                addition_limit: Some(Capacity(3.0)),
1623            }),
1624        );
1625        let process_rc = Rc::new(process);
1626
1627        // Create a candidate asset with commission year 2015
1628        let asset =
1629            Asset::new_candidate(process_rc.clone(), region_id.clone(), Capacity(1.0), 2015)
1630                .unwrap();
1631
1632        // commodity_portion = 0.5 -> limit = 3 * 0.5 = 1.5
1633        let result = asset.max_installable_capacity(Dimensionless(0.5));
1634        assert_eq!(result, Some(AssetCapacity::Continuous(Capacity(1.5))));
1635    }
1636}