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::Commodity;
4use crate::process::{Process, ProcessParameter};
5use crate::region::RegionID;
6use crate::time_slice::TimeSliceID;
7use anyhow::{ensure, Context, Result};
8use std::collections::HashSet;
9use std::ops::RangeInclusive;
10use std::rc::Rc;
11
12/// A unique identifier for an asset
13#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub struct AssetID(u32);
15
16impl AssetID {
17    /// Sentinel value assigned to [`Asset`]s when they are initially created
18    pub const INVALID: AssetID = AssetID(u32::MAX);
19}
20
21/// An asset controlled by an agent.
22#[derive(Clone, Debug, PartialEq)]
23pub struct Asset {
24    /// A unique identifier for the asset
25    pub id: AssetID,
26    /// A unique identifier for the agent
27    pub agent_id: AgentID,
28    /// The [`Process`] that this asset corresponds to
29    pub process: Rc<Process>,
30    /// The [`ProcessParameter`] corresponding to the asset's region and commission year
31    pub process_parameter: Rc<ProcessParameter>,
32    /// The region in which the asset is located
33    pub region_id: RegionID,
34    /// Capacity of asset
35    pub capacity: f64,
36    /// The year the asset comes online
37    pub commission_year: u32,
38}
39
40impl Asset {
41    /// Create a new [`Asset`].
42    ///
43    /// The `id` field is initially set to [`AssetID::INVALID`], but is changed to a unique value
44    /// when the asset is stored in an [`AssetPool`].
45    pub fn new(
46        agent_id: AgentID,
47        process: Rc<Process>,
48        region_id: RegionID,
49        capacity: f64,
50        commission_year: u32,
51    ) -> Result<Self> {
52        ensure!(commission_year > 0, "Commission year must be > 0");
53        ensure!(
54            process.regions.contains(&region_id),
55            "Region {} is not one of the regions in which process {} operates",
56            region_id,
57            process.id
58        );
59
60        let process_parameter = process
61            .parameters
62            .get(&(region_id.clone(), commission_year))
63            .with_context(|| {
64                format!(
65                    "Process {} does not operate in the year {}",
66                    process.id, commission_year
67                )
68            })?
69            .clone();
70
71        Ok(Self {
72            id: AssetID::INVALID,
73            agent_id,
74            process,
75            process_parameter,
76            region_id,
77            capacity,
78            commission_year,
79        })
80    }
81
82    /// The last year in which this asset should be decommissioned
83    pub fn decommission_year(&self) -> u32 {
84        self.commission_year + self.process_parameter.lifetime
85    }
86
87    /// Get the energy limits for this asset in a particular time slice
88    ///
89    /// This is an absolute max and min on the PAC energy produced/consumed in that time slice.
90    pub fn get_energy_limits(&self, time_slice: &TimeSliceID) -> RangeInclusive<f64> {
91        let limits = self
92            .process
93            .energy_limits
94            .get(&(
95                self.region_id.clone(),
96                self.commission_year,
97                time_slice.clone(),
98            ))
99            .unwrap();
100        let max_act = self.maximum_activity();
101
102        // Multiply the fractional capacity in self.process by this asset's actual capacity
103        (max_act * limits.start())..=(max_act * limits.end())
104    }
105
106    /// Maximum activity for this asset (PAC energy produced/consumed per year)
107    pub fn maximum_activity(&self) -> f64 {
108        self.capacity * self.process_parameter.capacity_to_activity
109    }
110}
111
112/// A pool of [`Asset`]s
113pub struct AssetPool {
114    /// The pool of assets, both active and yet to be commissioned.
115    ///
116    /// Sorted in order of commission year.
117    assets: Vec<Asset>,
118    /// Current milestone year.
119    current_year: u32,
120}
121
122impl AssetPool {
123    /// Create a new [`AssetPool`]
124    pub fn new(mut assets: Vec<Asset>) -> Self {
125        // Sort in order of commission year
126        assets.sort_by(|a, b| a.commission_year.cmp(&b.commission_year));
127
128        // Assign each asset a unique ID
129        for (id, asset) in assets.iter_mut().enumerate() {
130            asset.id = AssetID(id as u32);
131        }
132
133        Self {
134            assets,
135            current_year: 0,
136        }
137    }
138
139    /// Commission new assets for the specified milestone year
140    pub fn commission_new(&mut self, year: u32) {
141        assert!(
142            year >= self.current_year,
143            "Assets have already been commissioned for year {year}"
144        );
145        self.current_year = year;
146    }
147
148    /// Decommission old assets for the specified milestone year
149    pub fn decomission_old(&mut self, year: u32) {
150        assert!(
151            year >= self.current_year,
152            "Cannot decommission assets in the past (current year: {})",
153            self.current_year
154        );
155        self.assets.retain(|asset| asset.decommission_year() > year);
156    }
157
158    /// Get an asset with the specified ID.
159    ///
160    /// # Returns
161    ///
162    /// Reference to an [`Asset`] if found, else `None`. The asset may not be found if it has
163    /// already been decommissioned.
164    pub fn get(&self, id: AssetID) -> Option<&Asset> {
165        // The assets in `active` are in order of ID
166        let idx = self
167            .assets
168            .binary_search_by(|asset| asset.id.cmp(&id))
169            .ok()?;
170
171        Some(&self.assets[idx])
172    }
173
174    /// Iterate over active assets
175    pub fn iter(&self) -> impl Iterator<Item = &Asset> {
176        self.assets
177            .iter()
178            .take_while(|asset| asset.commission_year <= self.current_year)
179    }
180
181    /// Iterate over active assets for a particular region
182    pub fn iter_for_region<'a>(
183        &'a self,
184        region_id: &'a RegionID,
185    ) -> impl Iterator<Item = &'a Asset> {
186        self.iter().filter(|asset| asset.region_id == *region_id)
187    }
188
189    /// Iterate over only the active assets in a given region that produce or consume a given
190    /// commodity
191    pub fn iter_for_region_and_commodity<'a>(
192        &'a self,
193        region_id: &'a RegionID,
194        commodity: &'a Rc<Commodity>,
195    ) -> impl Iterator<Item = &'a Asset> {
196        self.iter_for_region(region_id)
197            .filter(|asset| asset.process.contains_commodity_flow(commodity))
198    }
199
200    /// Retain all assets whose IDs are in `assets_to_keep`.
201    ///
202    /// Other assets will be decommissioned. Assets which have not yet been commissioned will not be
203    /// affected.
204    pub fn retain(&mut self, assets_to_keep: &HashSet<AssetID>) {
205        // Sanity check: all IDs should be valid. As this check is slow, only do it for debug
206        // builds.
207        debug_assert!(
208            assets_to_keep.iter().all(|id| self.get(*id).is_some()),
209            "One or more asset IDs were invalid"
210        );
211
212        self.assets.retain(|asset| {
213            assets_to_keep.contains(&asset.id) || asset.commission_year > self.current_year
214        });
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use crate::commodity::{CommodityCostMap, CommodityType, DemandMap};
222    use crate::fixture::{assert_error, process};
223    use crate::process::{
224        FlowType, Process, ProcessEnergyLimitsMap, ProcessFlow, ProcessParameter,
225        ProcessParameterMap,
226    };
227    use crate::time_slice::TimeSliceLevel;
228    use itertools::{assert_equal, Itertools};
229    use rstest::{fixture, rstest};
230    use std::iter;
231    use std::ops::RangeInclusive;
232
233    #[rstest]
234    fn test_asset_new_valid(process: Process) {
235        let agent_id = AgentID("agent1".into());
236        let region_id = RegionID("GBR".into());
237        let asset = Asset::new(agent_id, process.into(), region_id, 1.0, 2015).unwrap();
238        assert!(asset.id == AssetID::INVALID);
239    }
240
241    #[rstest]
242    fn test_asset_new_invalid_commission_year_zero(process: Process) {
243        let agent_id = AgentID("agent1".into());
244        let region_id = RegionID("GBR".into());
245        assert_error!(
246            Asset::new(agent_id, process.into(), region_id, 1.0, 0),
247            "Commission year must be > 0"
248        );
249    }
250
251    #[rstest]
252    fn test_asset_new_invalid_commission_year(process: Process) {
253        let agent_id = AgentID("agent1".into());
254        let region_id = RegionID("GBR".into());
255        assert_error!(
256            Asset::new(agent_id, process.into(), region_id, 1.0, 2009),
257            "Process process1 does not operate in the year 2009"
258        );
259    }
260
261    #[rstest]
262    fn test_asset_new_invalid_region(process: Process) {
263        let agent_id = AgentID("agent1".into());
264        let region_id = RegionID("FRA".into());
265        assert_error!(
266            Asset::new(agent_id, process.into(), region_id, 1.0, 2015),
267            "Region FRA is not one of the regions in which process process1 operates"
268        );
269    }
270
271    #[fixture]
272    fn asset_pool() -> AssetPool {
273        let process_param = Rc::new(ProcessParameter {
274            capital_cost: 5.0,
275            fixed_operating_cost: 2.0,
276            variable_operating_cost: 1.0,
277            lifetime: 5,
278            discount_rate: 0.9,
279            capacity_to_activity: 1.0,
280        });
281        let years = RangeInclusive::new(2010, 2020).collect_vec();
282        let process_parameter_map: ProcessParameterMap = years
283            .iter()
284            .map(|&year| (("GBR".into(), year), process_param.clone()))
285            .collect();
286        let process = Rc::new(Process {
287            id: "process1".into(),
288            description: "Description".into(),
289            years: 2010..=2020,
290            energy_limits: ProcessEnergyLimitsMap::new(),
291            flows: vec![],
292            parameters: process_parameter_map,
293            regions: HashSet::from(["GBR".into()]),
294        });
295        let future = [2020, 2010]
296            .map(|year| {
297                Asset::new(
298                    "agent1".into(),
299                    Rc::clone(&process),
300                    "GBR".into(),
301                    1.0,
302                    year,
303                )
304                .unwrap()
305            })
306            .into_iter()
307            .collect_vec();
308
309        AssetPool::new(future)
310    }
311
312    #[test]
313    fn test_asset_get_energy_limits() {
314        let time_slice = TimeSliceID {
315            season: "winter".into(),
316            time_of_day: "day".into(),
317        };
318        let process_param = Rc::new(ProcessParameter {
319            capital_cost: 5.0,
320            fixed_operating_cost: 2.0,
321            variable_operating_cost: 1.0,
322            lifetime: 5,
323            discount_rate: 0.9,
324            capacity_to_activity: 3.0,
325        });
326        let years = RangeInclusive::new(2010, 2020).collect_vec();
327        let process_parameter_map: ProcessParameterMap = years
328            .iter()
329            .map(|&year| (("GBR".into(), year), process_param.clone()))
330            .collect();
331        let commodity = Rc::new(Commodity {
332            id: "commodity1".into(),
333            description: "Some description".into(),
334            kind: CommodityType::InputCommodity,
335            time_slice_level: TimeSliceLevel::Annual,
336            costs: CommodityCostMap::new(),
337            demand: DemandMap::new(),
338        });
339        let flow = ProcessFlow {
340            process_id: "id1".into(),
341            commodity: Rc::clone(&commodity),
342            flow: 1.0,
343            flow_type: FlowType::Fixed,
344            flow_cost: 1.0,
345            is_pac: true,
346        };
347        let fraction_limits = 1.0..=f64::INFINITY;
348        let mut energy_limits = ProcessEnergyLimitsMap::new();
349        for year in [2010, 2020] {
350            energy_limits.insert(
351                ("GBR".into(), year, time_slice.clone()),
352                fraction_limits.clone(),
353            );
354        }
355        let process = Rc::new(Process {
356            id: "process1".into(),
357            description: "Description".into(),
358            years: 2010..=2020,
359            energy_limits,
360            flows: vec![flow.clone()],
361            parameters: process_parameter_map,
362            regions: HashSet::from(["GBR".into()]),
363        });
364        let asset = Asset::new(
365            "agent1".into(),
366            Rc::clone(&process),
367            "GBR".into(),
368            2.0,
369            2010,
370        )
371        .unwrap();
372
373        assert_eq!(asset.get_energy_limits(&time_slice), 6.0..=f64::INFINITY);
374    }
375
376    #[rstest]
377    fn test_asset_pool_new(asset_pool: AssetPool) {
378        assert!(asset_pool.current_year == 0);
379
380        // Should be in order of commission year
381        assert!(asset_pool.assets.len() == 2);
382        assert!(asset_pool.assets[0].commission_year == 2010);
383        assert!(asset_pool.assets[1].commission_year == 2020);
384    }
385
386    #[rstest]
387    fn test_asset_pool_commission_new1(mut asset_pool: AssetPool) {
388        // Asset to be commissioned in this year
389        asset_pool.commission_new(2010);
390        assert!(asset_pool.current_year == 2010);
391        assert_equal(asset_pool.iter(), iter::once(&asset_pool.assets[0]));
392    }
393
394    #[rstest]
395    fn test_asset_pool_commission_new2(mut asset_pool: AssetPool) {
396        // Commission year has passed
397        asset_pool.commission_new(2011);
398        assert!(asset_pool.current_year == 2011);
399        assert_equal(asset_pool.iter(), iter::once(&asset_pool.assets[0]));
400    }
401
402    #[rstest]
403    fn test_asset_pool_commission_new3(mut asset_pool: AssetPool) {
404        // Nothing to commission for this year
405        asset_pool.commission_new(2000);
406        assert!(asset_pool.current_year == 2000);
407        assert!(asset_pool.iter().next().is_none()); // no active assets
408    }
409
410    #[rstest]
411    fn test_asset_pool_decommission_old(mut asset_pool: AssetPool) {
412        let asset_pool2 = asset_pool.assets.clone();
413
414        asset_pool.commission_new(2020);
415        assert!(asset_pool.assets.len() == 2);
416        asset_pool.decomission_old(2020); // should decommission first asset (lifetime == 5)
417        assert_equal(&asset_pool.assets, iter::once(&asset_pool2[1]));
418        asset_pool.decomission_old(2022); // nothing to decommission
419        assert_equal(&asset_pool.assets, iter::once(&asset_pool2[1]));
420        asset_pool.decomission_old(2025); // should decommission second asset
421        assert!(asset_pool.assets.is_empty());
422    }
423
424    #[rstest]
425    fn test_asset_pool_get(mut asset_pool: AssetPool) {
426        asset_pool.commission_new(2020);
427        assert!(asset_pool.get(AssetID(0)) == Some(&asset_pool.assets[0]));
428        assert!(asset_pool.get(AssetID(1)) == Some(&asset_pool.assets[1]));
429    }
430
431    #[rstest]
432    fn test_asset_pool_retain1(mut asset_pool: AssetPool) {
433        // Even though we are retaining no assets, none have been commissioned so the asset pool
434        // should not be changed
435        asset_pool.retain(&HashSet::new());
436        assert_eq!(asset_pool.assets.len(), 2);
437
438        // Decommission all active assets
439        asset_pool.commission_new(2010); // Commission first asset
440        asset_pool.retain(&HashSet::new());
441        assert_eq!(asset_pool.assets.len(), 1);
442        assert_eq!(asset_pool.assets[0].id, AssetID(1));
443    }
444
445    #[rstest]
446    fn test_asset_pool_retain2(mut asset_pool: AssetPool) {
447        // Decommission single asset
448        asset_pool.commission_new(2020); // Commission all assets
449        asset_pool.retain(&iter::once(AssetID(1)).collect());
450        assert_eq!(asset_pool.assets.len(), 1);
451        assert_eq!(asset_pool.assets[0].id, AssetID(1));
452    }
453}