Skip to main content

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