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    /// Get an [`AssetRef`] representing a subset of this parent's children.
1105    ///
1106    /// # Panics
1107    ///
1108    /// Panics if this asset is not a parent asset or `num_units` is zero or exceeds the total
1109    /// capacity of this asset.
1110    pub fn make_partial_parent(&self, num_units: u32) -> Self {
1111        assert!(
1112            self.is_parent(),
1113            "Cannot make a partial parent from a non-parent asset"
1114        );
1115        assert!(
1116            num_units > 0,
1117            "Cannot make a partial parent with zero units"
1118        );
1119
1120        let (max_num_units, unit_size) = match self.capacity() {
1121            AssetCapacity::Discrete(max_num_units, unit_size) => (max_num_units, unit_size),
1122            // We know asset capacity type is discrete as this is a parent asset
1123            AssetCapacity::Continuous(_) => unreachable!(),
1124        };
1125        match num_units.cmp(&max_num_units) {
1126            // Make a new Asset with fewer units
1127            Ordering::Less => Self::from(Asset {
1128                capacity: Cell::new(AssetCapacity::Discrete(num_units, unit_size)),
1129                ..Asset::clone(self)
1130            }),
1131            // Same number of units as self
1132            Ordering::Equal => self.clone(),
1133            Ordering::Greater => {
1134                panic!("Cannot make a partial parent with more units than original")
1135            }
1136        }
1137    }
1138}
1139
1140impl From<Rc<Asset>> for AssetRef {
1141    fn from(value: Rc<Asset>) -> Self {
1142        Self(value)
1143    }
1144}
1145
1146impl From<Asset> for AssetRef {
1147    fn from(value: Asset) -> Self {
1148        Self::from(Rc::new(value))
1149    }
1150}
1151
1152impl From<AssetRef> for Rc<Asset> {
1153    fn from(value: AssetRef) -> Self {
1154        value.0
1155    }
1156}
1157
1158impl Deref for AssetRef {
1159    type Target = Asset;
1160
1161    fn deref(&self) -> &Self::Target {
1162        &self.0
1163    }
1164}
1165
1166impl PartialEq for AssetRef {
1167    fn eq(&self, other: &Self) -> bool {
1168        // For assets to be considered equal, they must have the same process, region, commission
1169        // year and state
1170        Rc::ptr_eq(&self.0.process, &other.0.process)
1171            && self.0.region_id == other.0.region_id
1172            && self.0.commission_year == other.0.commission_year
1173            && self.0.state == other.0.state
1174    }
1175}
1176
1177impl Eq for AssetRef {}
1178
1179impl Hash for AssetRef {
1180    /// Hash an asset according to its state:
1181    /// - Commissioned assets are hashed based on their ID alone
1182    /// - Selected assets are hashed based on `process_id`, `region_id`, `commission_year` and
1183    ///   `agent_id`
1184    /// - Candidate assets are hashed based on `process_id`, `region_id` and `commission_year`
1185    /// - Parent assets are hashed based on `agent_id` and `group_id`
1186    /// - Future and Decommissioned assets cannot currently be hashed
1187    fn hash<H: Hasher>(&self, state: &mut H) {
1188        match &self.0.state {
1189            AssetState::Commissioned { id, .. } => {
1190                // Hashed based on their ID alone, since this is sufficient to uniquely identify the
1191                // asset
1192                id.hash(state);
1193            }
1194            AssetState::Candidate | AssetState::Selected { .. } | AssetState::Parent { .. } => {
1195                self.0.process.id.hash(state);
1196                self.0.region_id.hash(state);
1197                self.0.commission_year.hash(state);
1198                self.0.agent_id().hash(state);
1199                self.0.group_id().hash(state);
1200            }
1201            state => {
1202                // We don't need to hash other types of asset
1203                panic!("Cannot hash {state} assets");
1204            }
1205        }
1206    }
1207}
1208
1209impl PartialOrd for AssetRef {
1210    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1211        Some(self.cmp(other))
1212    }
1213}
1214
1215impl Ord for AssetRef {
1216    fn cmp(&self, other: &Self) -> Ordering {
1217        self.id().unwrap().cmp(&other.id().unwrap())
1218    }
1219}
1220
1221/// Additional methods for iterating over assets
1222pub trait AssetIterator<'a>: Iterator<Item = &'a AssetRef> + Sized
1223where
1224    Self: 'a,
1225{
1226    /// Filter assets by the agent that owns them
1227    fn filter_agent(self, agent_id: &'a AgentID) -> impl Iterator<Item = &'a AssetRef> + 'a {
1228        self.filter(move |asset| asset.agent_id() == Some(agent_id))
1229    }
1230
1231    /// Iterate over assets that have the given commodity as a primary output
1232    fn filter_primary_producers_of(
1233        self,
1234        commodity_id: &'a CommodityID,
1235    ) -> impl Iterator<Item = &'a AssetRef> + 'a {
1236        self.filter(move |asset| {
1237            asset
1238                .primary_output()
1239                .is_some_and(|flow| &flow.commodity.id == commodity_id)
1240        })
1241    }
1242
1243    /// Filter the assets by region
1244    fn filter_region(self, region_id: &'a RegionID) -> impl Iterator<Item = &'a AssetRef> + 'a {
1245        self.filter(move |asset| asset.region_id == *region_id)
1246    }
1247
1248    /// Iterate over process flows affecting the given commodity
1249    fn flows_for_commodity(
1250        self,
1251        commodity_id: &'a CommodityID,
1252    ) -> impl Iterator<Item = (&'a AssetRef, &'a ProcessFlow)> + 'a {
1253        self.filter_map(|asset| Some((asset, asset.get_flow(commodity_id)?)))
1254    }
1255}
1256
1257impl<'a, I> AssetIterator<'a> for I where I: Iterator<Item = &'a AssetRef> + Sized + 'a {}
1258
1259#[cfg(test)]
1260mod tests {
1261    use super::*;
1262    use crate::commodity::Commodity;
1263    use crate::fixture::{
1264        assert_error, assert_patched_runs_ok_simple, assert_validate_fails_with_simple, asset,
1265        asset_divisible, process, process_activity_limits_map, process_flows_map, region_id,
1266        svd_commodity, time_slice, time_slice_info,
1267    };
1268    use crate::patch::FilePatch;
1269    use crate::process::{FlowType, Process, ProcessFlow};
1270    use crate::region::RegionID;
1271    use crate::time_slice::{TimeSliceID, TimeSliceInfo};
1272    use crate::units::{
1273        ActivityPerCapacity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity,
1274        MoneyPerFlow,
1275    };
1276    use float_cmp::assert_approx_eq;
1277    use indexmap::indexmap;
1278    use rstest::{fixture, rstest};
1279    use std::rc::Rc;
1280
1281    #[rstest]
1282    fn get_input_cost_from_prices_works(
1283        region_id: RegionID,
1284        svd_commodity: Commodity,
1285        mut process: Process,
1286        time_slice: TimeSliceID,
1287    ) {
1288        // Update the process flows using the existing commodity fixture
1289        let commodity_rc = Rc::new(svd_commodity);
1290        let process_flow = ProcessFlow {
1291            commodity: Rc::clone(&commodity_rc),
1292            coeff: FlowPerActivity(-2.0), // Input
1293            kind: FlowType::Fixed,
1294            cost: MoneyPerFlow(0.0),
1295        };
1296        let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() };
1297        let process_flows_map = process_flows_map(process.regions.clone(), Rc::new(process_flows));
1298        process.flows = process_flows_map;
1299
1300        // Create asset
1301        let asset =
1302            Asset::new_candidate(Rc::new(process), region_id.clone(), Capacity(1.0), 2020).unwrap();
1303
1304        // Set input prices
1305        let mut input_prices = CommodityPrices::default();
1306        input_prices.insert(&commodity_rc.id, &region_id, &time_slice, MoneyPerFlow(3.0));
1307
1308        // Call function
1309        let cost = asset.get_input_cost_from_prices(&input_prices, &time_slice);
1310        // Should be -coeff * price = -(-2.0) * 3.0 = 6.0
1311        assert_approx_eq!(MoneyPerActivity, cost, MoneyPerActivity(6.0));
1312    }
1313
1314    #[rstest]
1315    #[case(Capacity(0.01))]
1316    #[case(Capacity(0.5))]
1317    #[case(Capacity(1.0))]
1318    #[case(Capacity(100.0))]
1319    fn asset_new_valid(process: Process, #[case] capacity: Capacity) {
1320        let agent_id = AgentID("agent1".into());
1321        let region_id = RegionID("GBR".into());
1322        let asset = Asset::new_future(agent_id, process.into(), region_id, capacity, 2015).unwrap();
1323        assert!(asset.id().is_none());
1324    }
1325
1326    #[rstest]
1327    #[case(Capacity(0.0))]
1328    #[case(Capacity(-0.01))]
1329    #[case(Capacity(-1.0))]
1330    #[case(Capacity(f64::NAN))]
1331    #[case(Capacity(f64::INFINITY))]
1332    #[case(Capacity(f64::NEG_INFINITY))]
1333    fn asset_new_invalid_capacity(process: Process, #[case] capacity: Capacity) {
1334        let agent_id = AgentID("agent1".into());
1335        let region_id = RegionID("GBR".into());
1336        assert_error!(
1337            Asset::new_future(agent_id, process.into(), region_id, capacity, 2015),
1338            "Capacity must be a finite, positive number"
1339        );
1340    }
1341
1342    #[rstest]
1343    fn asset_new_invalid_commission_year(process: Process) {
1344        let agent_id = AgentID("agent1".into());
1345        let region_id = RegionID("GBR".into());
1346        assert_error!(
1347            Asset::new_future(agent_id, process.into(), region_id, Capacity(1.0), 2007),
1348            "Process process1 does not operate in the year 2007"
1349        );
1350    }
1351
1352    #[rstest]
1353    fn asset_new_invalid_region(process: Process) {
1354        let agent_id = AgentID("agent1".into());
1355        let region_id = RegionID("FRA".into());
1356        assert_error!(
1357            Asset::new_future(agent_id, process.into(), region_id, Capacity(1.0), 2015),
1358            "Process process1 does not operate in region FRA"
1359        );
1360    }
1361
1362    #[fixture]
1363    fn process_with_activity_limits(
1364        mut process: Process,
1365        time_slice_info: TimeSliceInfo,
1366        time_slice: TimeSliceID,
1367    ) -> Process {
1368        // Add activity limits to the process
1369        let mut activity_limits = ActivityLimits::new_with_full_availability(&time_slice_info);
1370        activity_limits.add_time_slice_limit(time_slice, Dimensionless(0.1)..=Dimensionless(0.5));
1371        process.activity_limits =
1372            process_activity_limits_map(process.regions.clone(), activity_limits);
1373
1374        // Update cap2act
1375        process.capacity_to_activity = ActivityPerCapacity(2.0);
1376        process
1377    }
1378
1379    #[fixture]
1380    fn asset_with_activity_limits(process_with_activity_limits: Process) -> Asset {
1381        Asset::new_future(
1382            "agent1".into(),
1383            Rc::new(process_with_activity_limits),
1384            "GBR".into(),
1385            Capacity(2.0),
1386            2010,
1387        )
1388        .unwrap()
1389    }
1390
1391    #[rstest]
1392    fn asset_get_activity_per_capacity_limits(
1393        asset_with_activity_limits: Asset,
1394        time_slice: TimeSliceID,
1395    ) {
1396        // With cap2act of 2, and activity limits of 0.1..=0.5, should get 0.2..=1.0
1397        assert_eq!(
1398            asset_with_activity_limits.get_activity_per_capacity_limits(&time_slice),
1399            ActivityPerCapacity(0.2)..=ActivityPerCapacity(1.0)
1400        );
1401    }
1402
1403    #[rstest]
1404    #[case::exact_multiple(Capacity(12.0), Capacity(4.0), 3)] // 12 / 4 = 3
1405    #[case::rounded_up(Capacity(11.0), Capacity(4.0), 3)] // 11 / 4 = 2.75 -> 3
1406    #[case::unit_size_equals_capacity(Capacity(4.0), Capacity(4.0), 1)] // 4 / 4 = 1
1407    #[case::unit_size_greater_than_capacity(Capacity(3.0), Capacity(4.0), 1)] // 3 / 4 = 0.75 -> 1
1408    fn into_for_each_child_divisible(
1409        mut process: Process,
1410        #[case] capacity: Capacity,
1411        #[case] unit_size: Capacity,
1412        #[case] n_expected_children: usize,
1413    ) {
1414        process.unit_size = Some(unit_size);
1415        let asset = AssetRef::from(
1416            Asset::new_future(
1417                "agent1".into(),
1418                Rc::new(process),
1419                "GBR".into(),
1420                capacity,
1421                2010,
1422            )
1423            .unwrap(),
1424        );
1425
1426        let mut count = 0;
1427        let mut total_child_capacity = Capacity(0.0);
1428        asset.clone().into_for_each_child(&mut 0, |parent, child| {
1429            assert!(parent.is_some_and(|parent| matches!(parent.state, AssetState::Parent { .. })));
1430
1431            // Check each child has capacity equal to unit_size
1432            assert_eq!(
1433                child.total_capacity(),
1434                unit_size,
1435                "Child capacity should equal unit_size"
1436            );
1437
1438            total_child_capacity += child.total_capacity();
1439            count += 1;
1440        });
1441        assert_eq!(count, n_expected_children, "Unexpected number of children");
1442
1443        // Check total capacity is >= parent capacity
1444        assert!(
1445            total_child_capacity >= asset.total_capacity(),
1446            "Total capacity should be >= parent capacity"
1447        );
1448    }
1449
1450    #[rstest]
1451    fn into_for_each_child_nondivisible(asset: Asset) {
1452        assert!(
1453            asset.process.unit_size.is_none(),
1454            "Asset should be non-divisible"
1455        );
1456
1457        let asset = AssetRef::from(asset);
1458        let mut count = 0;
1459        asset.clone().into_for_each_child(&mut 0, |parent, child| {
1460            assert!(parent.is_none());
1461            assert_eq!(child, asset);
1462            count += 1;
1463        });
1464        assert_eq!(count, 1);
1465    }
1466
1467    #[fixture]
1468    fn parent_asset(asset_divisible: Asset) -> AssetRef {
1469        let asset = AssetRef::from(asset_divisible);
1470        let mut parent = None;
1471
1472        asset.into_for_each_child(&mut 0, |maybe_parent, _| {
1473            if parent.is_none() {
1474                parent = maybe_parent.cloned();
1475            }
1476        });
1477
1478        parent.expect("Divisible asset should create a parent")
1479    }
1480
1481    #[rstest]
1482    #[case::subset_of_children(2, false)]
1483    #[case::all_children(3, true)]
1484    fn make_partial_parent(
1485        parent_asset: AssetRef,
1486        #[case] num_units: u32,
1487        #[case] expect_same_asset: bool,
1488    ) {
1489        let parent = parent_asset;
1490        assert!(parent.is_parent());
1491
1492        let partial_parent = parent.make_partial_parent(num_units);
1493
1494        assert!(partial_parent.is_parent());
1495        assert_eq!(
1496            partial_parent.capacity(),
1497            AssetCapacity::Discrete(num_units, Capacity(4.0))
1498        );
1499        assert_eq!(partial_parent.num_children(), Some(num_units));
1500        assert_eq!(partial_parent.group_id(), parent.group_id());
1501        assert_eq!(partial_parent.agent_id(), parent.agent_id());
1502        assert_eq!(Rc::ptr_eq(&partial_parent.0, &parent.0), expect_same_asset);
1503        assert_eq!(parent.capacity(), AssetCapacity::Discrete(3, Capacity(4.0)));
1504    }
1505
1506    #[rstest]
1507    #[should_panic(expected = "Cannot make a partial parent from a non-parent asset")]
1508    fn make_partial_parent_panics_for_non_parent_asset(asset_divisible: Asset) {
1509        let asset = AssetRef::from(asset_divisible);
1510        asset.make_partial_parent(1);
1511    }
1512
1513    #[rstest]
1514    #[should_panic(expected = "Cannot make a partial parent with zero units")]
1515    fn make_partial_parent_panics_for_zero_units(parent_asset: AssetRef) {
1516        parent_asset.make_partial_parent(0);
1517    }
1518
1519    #[rstest]
1520    #[should_panic(expected = "Cannot make a partial parent with more units than original")]
1521    fn make_partial_parent_panics_for_too_many_units(parent_asset: AssetRef) {
1522        parent_asset.make_partial_parent(4);
1523    }
1524
1525    #[rstest]
1526    fn asset_commission(process: Process) {
1527        // Test successful commissioning of Future asset
1528        let process_rc = Rc::new(process);
1529        let mut asset1 = Asset::new_future(
1530            "agent1".into(),
1531            Rc::clone(&process_rc),
1532            "GBR".into(),
1533            Capacity(1.0),
1534            2020,
1535        )
1536        .unwrap();
1537        asset1.commission(AssetID(1), None, "");
1538        assert!(asset1.is_commissioned());
1539        assert_eq!(asset1.id(), Some(AssetID(1)));
1540
1541        // Test successful commissioning of Selected asset
1542        let mut asset2 = Asset::new_selected(
1543            "agent1".into(),
1544            Rc::clone(&process_rc),
1545            "GBR".into(),
1546            Capacity(1.0),
1547            2020,
1548        )
1549        .unwrap();
1550        asset2.commission(AssetID(2), None, "");
1551        assert!(asset2.is_commissioned());
1552        assert_eq!(asset2.id(), Some(AssetID(2)));
1553    }
1554
1555    #[rstest]
1556    #[case::commission_during_process_lifetime(2024, 2024)]
1557    #[case::decommission_after_process_lifetime_ends(2026, 2025)]
1558    fn asset_decommission(
1559        #[case] requested_decommission_year: u32,
1560        #[case] expected_decommission_year: u32,
1561        process: Process,
1562    ) {
1563        // Test successful commissioning of Future asset
1564        let process_rc = Rc::new(process);
1565        let mut asset = Asset::new_future(
1566            "agent1".into(),
1567            Rc::clone(&process_rc),
1568            "GBR".into(),
1569            Capacity(1.0),
1570            2020,
1571        )
1572        .unwrap();
1573        asset.commission(AssetID(1), None, "");
1574        assert!(asset.is_commissioned());
1575        assert_eq!(asset.id(), Some(AssetID(1)));
1576
1577        // Test successful decommissioning
1578        asset.decommission(requested_decommission_year, "");
1579        assert!(!asset.is_commissioned());
1580        assert_eq!(asset.decommission_year(), Some(expected_decommission_year));
1581    }
1582
1583    #[rstest]
1584    #[case::decommission_after_predefined_max_year(2026, 2025, Some(2025))]
1585    #[case::decommission_before_predefined_max_year(2024, 2024, Some(2025))]
1586    #[case::decommission_during_process_lifetime_end_no_max_year(2024, 2024, None)]
1587    #[case::decommission_after_process_lifetime_end_no_max_year(2026, 2025, None)]
1588    fn asset_decommission_with_max_decommission_year_predefined(
1589        #[case] requested_decommission_year: u32,
1590        #[case] expected_decommission_year: u32,
1591        #[case] max_decommission_year: Option<u32>,
1592        process: Process,
1593    ) {
1594        // Test successful commissioning of Future asset
1595        let process_rc = Rc::new(process);
1596        let mut asset = Asset::new_future_with_max_decommission(
1597            "agent1".into(),
1598            Rc::clone(&process_rc),
1599            "GBR".into(),
1600            Capacity(1.0),
1601            2020,
1602            max_decommission_year,
1603        )
1604        .unwrap();
1605        asset.commission(AssetID(1), None, "");
1606        assert!(asset.is_commissioned());
1607        assert_eq!(asset.id(), Some(AssetID(1)));
1608
1609        // Test successful decommissioning
1610        asset.decommission(requested_decommission_year, "");
1611        assert!(!asset.is_commissioned());
1612        assert_eq!(asset.decommission_year(), Some(expected_decommission_year));
1613    }
1614
1615    #[rstest]
1616    fn asset_decommission_divisible(asset_divisible: Asset) {
1617        let asset = AssetRef::from(asset_divisible);
1618        let original_capacity = asset.capacity();
1619
1620        // Commission children
1621        let mut children = Vec::new();
1622        let mut next_id = 0;
1623        asset.into_for_each_child(&mut 0, |parent, mut child| {
1624            child
1625                .make_mut()
1626                .commission(AssetID(next_id), parent.cloned(), "");
1627            next_id += 1;
1628            children.push(child);
1629        });
1630
1631        let parent = children[0].parent().unwrap().clone();
1632        assert_eq!(parent.capacity(), original_capacity);
1633        children[0].make_mut().decommission(2020, "");
1634
1635        let AssetCapacity::Discrete(original_units, original_unit_size) = original_capacity else {
1636            panic!("Capacity type should be discrete");
1637        };
1638        assert_eq!(
1639            parent.capacity(),
1640            AssetCapacity::Discrete(original_units - 1, original_unit_size)
1641        );
1642    }
1643
1644    #[rstest]
1645    #[should_panic(expected = "Assets with state Candidate cannot be commissioned")]
1646    fn commission_wrong_states(process: Process) {
1647        let mut asset =
1648            Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap();
1649        asset.commission(AssetID(1), None, "");
1650    }
1651
1652    #[rstest]
1653    #[should_panic(expected = "Cannot decommission an asset that hasn't been commissioned")]
1654    fn decommission_wrong_state(process: Process) {
1655        let mut asset =
1656            Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap();
1657        asset.decommission(2025, "");
1658    }
1659
1660    #[test]
1661    fn commission_year_before_time_horizon() {
1662        let processes_patch = FilePatch::new("processes.csv")
1663            .with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
1664            .with_addition("GASDRV,Dry gas extraction,all,GASPRD,1980,2040,1.0,");
1665
1666        // Check we can run model with asset commissioned before time horizon (simple starts in
1667        // 2020)
1668        let patches = vec![
1669            processes_patch.clone(),
1670            FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,1980"),
1671        ];
1672        assert_patched_runs_ok_simple!(patches);
1673
1674        // This should fail if it is not one of the years supported by the process, though
1675        let patches = vec![
1676            processes_patch,
1677            FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,1970"),
1678        ];
1679        assert_validate_fails_with_simple!(
1680            patches,
1681            "Agent A0_GEX has asset with commission year 1970, not within process GASDRV commission years: 1980..=2040"
1682        );
1683    }
1684
1685    #[test]
1686    fn commission_year_after_time_horizon() {
1687        let processes_patch = FilePatch::new("processes.csv")
1688            .with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
1689            .with_addition("GASDRV,Dry gas extraction,all,GASPRD,2020,2050,1.0,");
1690
1691        // Check we can run model with asset commissioned after time horizon (simple ends in 2040)
1692        let patches = vec![
1693            processes_patch.clone(),
1694            FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,2050"),
1695        ];
1696        assert_patched_runs_ok_simple!(patches);
1697
1698        // This should fail if it is not one of the years supported by the process, though
1699        let patches = vec![
1700            processes_patch,
1701            FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,2060"),
1702        ];
1703        assert_validate_fails_with_simple!(
1704            patches,
1705            "Agent A0_GEX has asset with commission year 2060, not within process GASDRV commission years: 2020..=2050"
1706        );
1707    }
1708
1709    #[rstest]
1710    fn max_installable_capacity(mut process: Process, region_id: RegionID) {
1711        // Set an addition limit of 3 for (region, year 2015)
1712        process.investment_constraints.insert(
1713            (region_id.clone(), 2015),
1714            Rc::new(crate::process::ProcessInvestmentConstraint {
1715                addition_limit: Some(Capacity(3.0)),
1716            }),
1717        );
1718        let process_rc = Rc::new(process);
1719
1720        // Create a candidate asset with commission year 2015
1721        let asset =
1722            Asset::new_candidate(process_rc.clone(), region_id.clone(), Capacity(1.0), 2015)
1723                .unwrap();
1724
1725        // commodity_portion = 0.5 -> limit = 3 * 0.5 = 1.5
1726        let result = asset.max_installable_capacity(Dimensionless(0.5));
1727        assert_eq!(result, Some(AssetCapacity::Continuous(Capacity(1.5))));
1728    }
1729}