1use crate::agent::AgentID;
3use crate::commodity::{CommodityID, CommodityType};
4use crate::finance::annual_capital_cost;
5use crate::process::{
6 ActivityLimits, FlowDirection, Process, ProcessFlow, ProcessID, ProcessParameter,
7};
8use crate::region::RegionID;
9use crate::simulation::CommodityPrices;
10use crate::time_slice::{TimeSliceID, TimeSliceSelection};
11use crate::units::{
12 Activity, ActivityPerCapacity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity,
13 MoneyPerCapacity, MoneyPerFlow,
14};
15use anyhow::{Context, Result, ensure};
16use indexmap::IndexMap;
17use itertools::{Itertools, chain};
18use log::{debug, warn};
19use serde::{Deserialize, Serialize};
20use std::cmp::{Ordering, min};
21use std::hash::{Hash, Hasher};
22use std::ops::{Add, Deref, RangeInclusive, Sub};
23use std::rc::Rc;
24use std::slice;
25
26#[derive(
28 Clone,
29 Copy,
30 Debug,
31 derive_more::Display,
32 Eq,
33 Hash,
34 Ord,
35 PartialEq,
36 PartialOrd,
37 Deserialize,
38 Serialize,
39)]
40pub struct AssetID(u32);
41
42#[derive(
44 Clone,
45 Copy,
46 Debug,
47 derive_more::Display,
48 Eq,
49 Hash,
50 Ord,
51 PartialEq,
52 PartialOrd,
53 Deserialize,
54 Serialize,
55)]
56pub struct AssetGroupID(u32);
57
58#[derive(Clone, Debug, PartialEq, strum::Display)]
70pub enum AssetState {
71 Commissioned {
73 id: AssetID,
75 agent_id: AgentID,
77 mothballed_year: Option<u32>,
79 group_id: Option<AssetGroupID>,
81 },
82 Decommissioned {
84 id: AssetID,
86 agent_id: AgentID,
88 decommission_year: u32,
90 },
91 Future {
93 agent_id: AgentID,
95 },
96 Selected {
98 agent_id: AgentID,
100 },
101 Candidate,
103}
104
105#[derive(Clone, PartialEq, Copy, Debug)]
107pub enum AssetCapacity {
108 Continuous(Capacity),
110 Discrete(u32, Capacity),
113}
114
115impl Add for AssetCapacity {
116 type Output = Self;
117
118 fn add(self, rhs: AssetCapacity) -> Self {
120 match (self, rhs) {
121 (AssetCapacity::Continuous(cap1), AssetCapacity::Continuous(cap2)) => {
122 AssetCapacity::Continuous(cap1 + cap2)
123 }
124 (AssetCapacity::Discrete(units1, size1), AssetCapacity::Discrete(units2, size2)) => {
125 Self::check_same_unit_size(size1, size2);
126 AssetCapacity::Discrete(units1 + units2, size1)
127 }
128 _ => panic!("Cannot add different types of AssetCapacity ({self:?} and {rhs:?})"),
129 }
130 }
131}
132
133impl Sub for AssetCapacity {
134 type Output = Self;
135
136 fn sub(self, rhs: AssetCapacity) -> Self {
138 match (self, rhs) {
139 (AssetCapacity::Continuous(cap1), AssetCapacity::Continuous(cap2)) => {
140 AssetCapacity::Continuous((cap1 - cap2).max(Capacity(0.0)))
141 }
142 (AssetCapacity::Discrete(units1, size1), AssetCapacity::Discrete(units2, size2)) => {
143 Self::check_same_unit_size(size1, size2);
144 AssetCapacity::Discrete(units1 - units2.min(units1), size1)
145 }
146 _ => panic!("Cannot subtract different types of AssetCapacity ({self:?} and {rhs:?})"),
147 }
148 }
149}
150
151impl Eq for AssetCapacity {}
152
153impl PartialOrd for AssetCapacity {
154 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
155 Some(self.cmp(other))
156 }
157}
158
159impl Ord for AssetCapacity {
160 fn cmp(&self, other: &Self) -> Ordering {
161 match (self, other) {
162 (AssetCapacity::Continuous(a), AssetCapacity::Continuous(b)) => a.total_cmp(b),
163 (AssetCapacity::Discrete(units1, size1), AssetCapacity::Discrete(units2, size2)) => {
164 Self::check_same_unit_size(*size1, *size2);
165 units1.cmp(units2)
166 }
167 _ => panic!("Cannot compare different types of AssetCapacity ({self:?} and {other:?})"),
168 }
169 }
170}
171
172impl AssetCapacity {
173 fn check_same_unit_size(size1: Capacity, size2: Capacity) {
175 assert_eq!(
176 size1, size2,
177 "Can't perform operation on capacities with different unit sizes ({size1} and {size2})",
178 );
179 }
180
181 pub fn from_capacity(capacity: Capacity, unit_size: Option<Capacity>) -> Self {
187 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
188 match unit_size {
189 Some(size) => {
190 let num_units = (capacity / size).value().ceil() as u32;
191 AssetCapacity::Discrete(num_units, size)
192 }
193 None => AssetCapacity::Continuous(capacity),
194 }
195 }
196
197 pub fn total_capacity(&self) -> Capacity {
199 match self {
200 AssetCapacity::Continuous(cap) => *cap,
201 AssetCapacity::Discrete(units, size) => *size * Dimensionless(*units as f64),
202 }
203 }
204
205 pub fn n_units(&self) -> Option<u32> {
207 match self {
208 AssetCapacity::Continuous(_) => None,
209 AssetCapacity::Discrete(units, _) => Some(*units),
210 }
211 }
212
213 pub fn assert_same_type(&self, other: AssetCapacity) {
215 assert!(
216 matches!(self, AssetCapacity::Continuous(_))
217 == matches!(other, AssetCapacity::Continuous(_)),
218 "Cannot change capacity type"
219 );
220 }
221
222 pub fn apply_limit_factor(self, limit_factor: Dimensionless) -> Self {
227 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
228 match self {
229 AssetCapacity::Continuous(cap) => AssetCapacity::Continuous(cap * limit_factor),
230 AssetCapacity::Discrete(units, size) => {
231 let new_units = (units as f64 * limit_factor.value()).ceil() as u32;
232 AssetCapacity::Discrete(new_units, size)
233 }
234 }
235 }
236}
237
238#[derive(Clone, PartialEq)]
240pub struct Asset {
241 state: AssetState,
243 process: Rc<Process>,
245 activity_limits: Rc<ActivityLimits>,
247 flows: Rc<IndexMap<CommodityID, ProcessFlow>>,
249 process_parameter: Rc<ProcessParameter>,
251 region_id: RegionID,
253 capacity: AssetCapacity,
255 commission_year: u32,
257 max_decommission_year: u32,
259}
260
261impl Asset {
262 pub fn new_candidate(
264 process: Rc<Process>,
265 region_id: RegionID,
266 capacity: Capacity,
267 commission_year: u32,
268 ) -> Result<Self> {
269 let unit_size = process.unit_size;
270 Self::new_with_state(
271 AssetState::Candidate,
272 process,
273 region_id,
274 AssetCapacity::from_capacity(capacity, unit_size),
275 commission_year,
276 None,
277 )
278 }
279
280 pub fn new_candidate_for_dispatch(
286 process: Rc<Process>,
287 region_id: RegionID,
288 capacity: Capacity,
289 commission_year: u32,
290 ) -> Result<Self> {
291 Self::new_with_state(
292 AssetState::Candidate,
293 process,
294 region_id,
295 AssetCapacity::Continuous(capacity),
296 commission_year,
297 None,
298 )
299 }
300
301 pub fn new_candidate_from_commissioned(asset: &Asset) -> Self {
303 assert!(asset.is_commissioned(), "Asset must be commissioned");
304
305 Self {
306 state: AssetState::Candidate,
307 ..asset.clone()
308 }
309 }
310
311 pub fn new_future_with_max_decommission(
313 agent_id: AgentID,
314 process: Rc<Process>,
315 region_id: RegionID,
316 capacity: Capacity,
317 commission_year: u32,
318 max_decommission_year: Option<u32>,
319 ) -> Result<Self> {
320 check_capacity_valid_for_asset(capacity)?;
321 let unit_size = process.unit_size;
322 Self::new_with_state(
323 AssetState::Future { agent_id },
324 process,
325 region_id,
326 AssetCapacity::from_capacity(capacity, unit_size),
327 commission_year,
328 max_decommission_year,
329 )
330 }
331
332 pub fn new_future(
334 agent_id: AgentID,
335 process: Rc<Process>,
336 region_id: RegionID,
337 capacity: Capacity,
338 commission_year: u32,
339 ) -> Result<Self> {
340 Self::new_future_with_max_decommission(
341 agent_id,
342 process,
343 region_id,
344 capacity,
345 commission_year,
346 None,
347 )
348 }
349
350 #[cfg(test)]
355 fn new_selected(
356 agent_id: AgentID,
357 process: Rc<Process>,
358 region_id: RegionID,
359 capacity: Capacity,
360 commission_year: u32,
361 ) -> Result<Self> {
362 let unit_size = process.unit_size;
363 Self::new_with_state(
364 AssetState::Selected { agent_id },
365 process,
366 region_id,
367 AssetCapacity::from_capacity(capacity, unit_size),
368 commission_year,
369 None,
370 )
371 }
372
373 #[cfg(test)]
378 pub fn new_commissioned(
379 agent_id: AgentID,
380 process: Rc<Process>,
381 region_id: RegionID,
382 capacity: Capacity,
383 commission_year: u32,
384 ) -> Result<Self> {
385 let unit_size = process.unit_size;
386 Self::new_with_state(
387 AssetState::Commissioned {
388 id: AssetID(0),
389 agent_id,
390 mothballed_year: None,
391 group_id: None,
392 },
393 process,
394 region_id,
395 AssetCapacity::from_capacity(capacity, unit_size),
396 commission_year,
397 None,
398 )
399 }
400
401 fn new_with_state(
403 state: AssetState,
404 process: Rc<Process>,
405 region_id: RegionID,
406 capacity: AssetCapacity,
407 commission_year: u32,
408 max_decommission_year: Option<u32>,
409 ) -> Result<Self> {
410 check_region_year_valid_for_process(&process, ®ion_id, commission_year)?;
411 ensure!(
412 capacity.total_capacity() >= Capacity(0.0),
413 "Capacity must be non-negative"
414 );
415
416 let key = (region_id.clone(), commission_year);
422 let activity_limits = process
423 .activity_limits
424 .get(&key)
425 .with_context(|| {
426 format!(
427 "No process availabilities supplied for process {} in region {} in year {}. \
428 You should update process_availabilities.csv.",
429 &process.id, region_id, commission_year
430 )
431 })?
432 .clone();
433 let flows = process
434 .flows
435 .get(&key)
436 .with_context(|| {
437 format!(
438 "No commodity flows supplied for process {} in region {} in year {}. \
439 You should update process_flows.csv.",
440 &process.id, region_id, commission_year
441 )
442 })?
443 .clone();
444 let process_parameter = process
445 .parameters
446 .get(&key)
447 .with_context(|| {
448 format!(
449 "No process parameters supplied for process {} in region {} in year {}. \
450 You should update process_parameters.csv.",
451 &process.id, region_id, commission_year
452 )
453 })?
454 .clone();
455
456 let max_decommission_year =
457 max_decommission_year.unwrap_or(commission_year + process_parameter.lifetime);
458 ensure!(
459 max_decommission_year >= commission_year,
460 "Max decommission year must be after/same as commission year"
461 );
462
463 Ok(Self {
464 state,
465 process,
466 activity_limits,
467 flows,
468 process_parameter,
469 region_id,
470 capacity,
471 commission_year,
472 max_decommission_year,
473 })
474 }
475
476 pub fn state(&self) -> &AssetState {
478 &self.state
479 }
480
481 pub fn process_parameter(&self) -> &ProcessParameter {
483 &self.process_parameter
484 }
485
486 pub fn max_decommission_year(&self) -> u32 {
488 self.max_decommission_year
489 }
490
491 pub fn get_activity_per_capacity_limits(
493 &self,
494 time_slice: &TimeSliceID,
495 ) -> RangeInclusive<ActivityPerCapacity> {
496 let limits = &self.activity_limits.get_limit_for_time_slice(time_slice);
497 let cap2act = self.process.capacity_to_activity;
498 (cap2act * *limits.start())..=(cap2act * *limits.end())
499 }
500
501 pub fn get_activity_limits_for_selection(
503 &self,
504 time_slice_selection: &TimeSliceSelection,
505 ) -> RangeInclusive<Activity> {
506 let activity_per_capacity_limits = self.activity_limits.get_limit(time_slice_selection);
507 let cap2act = self.process.capacity_to_activity;
508 let max_activity = self.capacity.total_capacity() * cap2act;
509 let lb = max_activity * *activity_per_capacity_limits.start();
510 let ub = max_activity * *activity_per_capacity_limits.end();
511 lb..=ub
512 }
513
514 pub fn iter_activity_limits(
516 &self,
517 ) -> impl Iterator<Item = (TimeSliceSelection, RangeInclusive<Activity>)> + '_ {
518 let max_act = self.max_activity();
519 self.activity_limits
520 .iter_limits()
521 .map(move |(ts_sel, limit)| {
522 (
523 ts_sel,
524 (max_act * *limit.start())..=(max_act * *limit.end()),
525 )
526 })
527 }
528
529 pub fn iter_activity_per_capacity_limits(
531 &self,
532 ) -> impl Iterator<Item = (TimeSliceSelection, RangeInclusive<ActivityPerCapacity>)> + '_ {
533 let cap2act = self.process.capacity_to_activity;
534 self.activity_limits
535 .iter_limits()
536 .map(move |(ts_sel, limit)| {
537 (
538 ts_sel,
539 (cap2act * *limit.start())..=(cap2act * *limit.end()),
540 )
541 })
542 }
543
544 pub fn get_total_output_per_activity(&self) -> FlowPerActivity {
551 self.iter_output_flows().map(|flow| flow.coeff).sum()
552 }
553
554 pub fn get_operating_cost(&self, year: u32, time_slice: &TimeSliceID) -> MoneyPerActivity {
556 let flows_cost = self
558 .iter_flows()
559 .map(|flow| flow.get_total_cost_per_activity(&self.region_id, year, time_slice))
560 .sum();
561
562 self.process_parameter.variable_operating_cost + flows_cost
563 }
564
565 pub fn get_revenue_from_flows(
569 &self,
570 prices: &CommodityPrices,
571 time_slice: &TimeSliceID,
572 ) -> MoneyPerActivity {
573 self.get_revenue_from_flows_with_filter(prices, time_slice, |_| true)
574 }
575
576 pub fn get_revenue_from_flows_excluding_primary(
580 &self,
581 prices: &CommodityPrices,
582 time_slice: &TimeSliceID,
583 ) -> MoneyPerActivity {
584 let excluded_commodity = self.primary_output().map(|flow| &flow.commodity.id);
585
586 self.get_revenue_from_flows_with_filter(prices, time_slice, |flow| {
587 excluded_commodity.is_none_or(|commodity_id| commodity_id != &flow.commodity.id)
588 })
589 }
590
591 pub fn get_input_cost_from_prices(
595 &self,
596 prices: &CommodityPrices,
597 time_slice: &TimeSliceID,
598 ) -> MoneyPerActivity {
599 -self.get_revenue_from_flows_with_filter(prices, time_slice, |x| {
601 x.direction() == FlowDirection::Input
602 })
603 }
604
605 fn get_revenue_from_flows_with_filter<F>(
610 &self,
611 prices: &CommodityPrices,
612 time_slice: &TimeSliceID,
613 mut filter_for_flows: F,
614 ) -> MoneyPerActivity
615 where
616 F: FnMut(&ProcessFlow) -> bool,
617 {
618 self.iter_flows()
619 .filter(|flow| filter_for_flows(flow))
620 .map(|flow| {
621 flow.coeff
622 * prices
623 .get(&flow.commodity.id, &self.region_id, time_slice)
624 .unwrap_or_default()
625 })
626 .sum()
627 }
628
629 fn get_generic_activity_cost(
634 &self,
635 prices: &CommodityPrices,
636 year: u32,
637 time_slice: &TimeSliceID,
638 ) -> MoneyPerActivity {
639 let cost_of_inputs = self.get_input_cost_from_prices(prices, time_slice);
641
642 let excludes_sed_svd_output = |flow: &&ProcessFlow| {
644 !(flow.direction() == FlowDirection::Output
645 && matches!(
646 flow.commodity.kind,
647 CommodityType::SupplyEqualsDemand | CommodityType::ServiceDemand
648 ))
649 };
650 let flow_costs = self
651 .iter_flows()
652 .filter(excludes_sed_svd_output)
653 .map(|flow| flow.get_total_cost_per_activity(&self.region_id, year, time_slice))
654 .sum();
655
656 cost_of_inputs + flow_costs + self.process_parameter.variable_operating_cost
657 }
658
659 pub fn iter_marginal_costs_with_filter<'a>(
667 &'a self,
668 prices: &'a CommodityPrices,
669 year: u32,
670 time_slice: &'a TimeSliceID,
671 filter: impl Fn(&CommodityID) -> bool + 'a,
672 ) -> Box<dyn Iterator<Item = (CommodityID, MoneyPerFlow)> + 'a> {
673 let mut output_flows_iter = self
675 .iter_output_flows()
676 .filter(move |flow| filter(&flow.commodity.id))
677 .peekable();
678
679 if output_flows_iter.peek().is_none() {
681 return Box::new(std::iter::empty::<(CommodityID, MoneyPerFlow)>());
682 }
683
684 let generic_activity_cost = self.get_generic_activity_cost(prices, year, time_slice);
689
690 let total_output_per_activity = self.get_total_output_per_activity();
695 assert!(total_output_per_activity > FlowPerActivity::EPSILON); let generic_cost_per_flow = generic_activity_cost / total_output_per_activity;
697
698 Box::new(output_flows_iter.map(move |flow| {
700 let commodity_specific_costs_per_flow =
702 flow.get_total_cost_per_flow(&self.region_id, year, time_slice);
703
704 let marginal_cost = generic_cost_per_flow + commodity_specific_costs_per_flow;
706 (flow.commodity.id.clone(), marginal_cost)
707 }))
708 }
709
710 pub fn iter_marginal_costs<'a>(
714 &'a self,
715 prices: &'a CommodityPrices,
716 year: u32,
717 time_slice: &'a TimeSliceID,
718 ) -> Box<dyn Iterator<Item = (CommodityID, MoneyPerFlow)> + 'a> {
719 self.iter_marginal_costs_with_filter(prices, year, time_slice, move |_| true)
720 }
721
722 pub fn get_annual_capital_cost_per_capacity(&self) -> MoneyPerCapacity {
724 let capital_cost = self.process_parameter.capital_cost;
725 let lifetime = self.process_parameter.lifetime;
726 let discount_rate = self.process_parameter.discount_rate;
727 annual_capital_cost(capital_cost, lifetime, discount_rate)
728 }
729
730 pub fn get_annual_capital_cost_per_activity(
735 &self,
736 annual_activity: Activity,
737 ) -> MoneyPerActivity {
738 let annual_capital_cost_per_capacity = self.get_annual_capital_cost_per_capacity();
739 let total_annual_capital_cost =
740 annual_capital_cost_per_capacity * self.capacity.total_capacity();
741 assert!(
742 annual_activity > Activity::EPSILON,
743 "Cannot calculate annual capital cost per activity for an asset with zero annual activity"
744 );
745 total_annual_capital_cost / annual_activity
746 }
747
748 pub fn get_annual_capital_cost_per_flow(&self, annual_activity: Activity) -> MoneyPerFlow {
753 let annual_capital_cost_per_activity =
754 self.get_annual_capital_cost_per_activity(annual_activity);
755 let total_output_per_activity = self.get_total_output_per_activity();
756 assert!(total_output_per_activity > FlowPerActivity::EPSILON); annual_capital_cost_per_activity / total_output_per_activity
758 }
759
760 pub fn max_activity(&self) -> Activity {
762 self.capacity.total_capacity() * self.process.capacity_to_activity
763 }
764
765 pub fn get_flow(&self, commodity_id: &CommodityID) -> Option<&ProcessFlow> {
767 self.flows.get(commodity_id)
768 }
769
770 pub fn iter_flows(&self) -> impl Iterator<Item = &ProcessFlow> {
772 self.flows.values()
773 }
774
775 pub fn iter_output_flows(&self) -> impl Iterator<Item = &ProcessFlow> {
777 self.flows.values().filter(|flow| {
778 flow.direction() == FlowDirection::Output
779 && matches!(
780 flow.commodity.kind,
781 CommodityType::SupplyEqualsDemand | CommodityType::ServiceDemand
782 )
783 })
784 }
785
786 pub fn primary_output(&self) -> Option<&ProcessFlow> {
788 self.process
789 .primary_output
790 .as_ref()
791 .map(|commodity_id| &self.flows[commodity_id])
792 }
793
794 pub fn is_commissioned(&self) -> bool {
796 matches!(&self.state, AssetState::Commissioned { .. })
797 }
798
799 pub fn commission_year(&self) -> u32 {
801 self.commission_year
802 }
803
804 pub fn decommission_year(&self) -> Option<u32> {
806 match &self.state {
807 AssetState::Decommissioned {
808 decommission_year, ..
809 } => Some(*decommission_year),
810 _ => None,
811 }
812 }
813
814 pub fn region_id(&self) -> &RegionID {
816 &self.region_id
817 }
818
819 pub fn process(&self) -> &Process {
821 &self.process
822 }
823
824 pub fn process_id(&self) -> &ProcessID {
826 &self.process.id
827 }
828
829 pub fn id(&self) -> Option<AssetID> {
831 match &self.state {
832 AssetState::Commissioned { id, .. } | AssetState::Decommissioned { id, .. } => {
833 Some(*id)
834 }
835 _ => None,
836 }
837 }
838
839 pub fn group_id(&self) -> Option<AssetGroupID> {
841 match &self.state {
842 AssetState::Commissioned { group_id, .. } => *group_id,
843 _ => None,
844 }
845 }
846
847 pub fn agent_id(&self) -> Option<&AgentID> {
849 match &self.state {
850 AssetState::Commissioned { agent_id, .. }
851 | AssetState::Decommissioned { agent_id, .. }
852 | AssetState::Future { agent_id }
853 | AssetState::Selected { agent_id } => Some(agent_id),
854 AssetState::Candidate => None,
855 }
856 }
857
858 pub fn capacity(&self) -> AssetCapacity {
860 self.capacity
861 }
862
863 pub fn set_capacity(&mut self, capacity: AssetCapacity) {
865 assert!(
866 matches!(
867 self.state,
868 AssetState::Candidate | AssetState::Selected { .. }
869 ),
870 "set_capacity can only be called on Candidate or Selected assets"
871 );
872 assert!(
873 capacity.total_capacity() >= Capacity(0.0),
874 "Capacity must be >= 0"
875 );
876 self.capacity.assert_same_type(capacity);
877 self.capacity = capacity;
878 }
879
880 pub fn increase_capacity(&mut self, capacity: AssetCapacity) {
882 assert!(
883 self.state == AssetState::Candidate,
884 "increase_capacity can only be called on Candidate assets"
885 );
886 assert!(
887 capacity.total_capacity() > Capacity(0.0),
888 "Capacity increase must be positive"
889 );
890 self.capacity = self.capacity + capacity;
891 }
892
893 fn decommission(&mut self, decommission_year: u32, reason: &str) {
895 let (id, agent_id) = match &self.state {
896 AssetState::Commissioned { id, agent_id, .. } => (*id, agent_id.clone()),
897 _ => panic!("Cannot decommission an asset that hasn't been commissioned"),
898 };
899 debug!(
900 "Decommissioning '{}' asset (ID: {}) for agent '{}' (reason: {})",
901 self.process_id(),
902 id,
903 agent_id,
904 reason
905 );
906
907 self.state = AssetState::Decommissioned {
908 id,
909 agent_id,
910 decommission_year: decommission_year.min(self.max_decommission_year()),
911 };
912 }
913
914 fn commission(&mut self, id: AssetID, group_id: Option<AssetGroupID>, reason: &str) {
925 let agent_id = match &self.state {
926 AssetState::Future { agent_id } | AssetState::Selected { agent_id } => agent_id,
927 state => panic!("Assets with state {state} cannot be commissioned"),
928 };
929 debug!(
930 "Commissioning '{}' asset (ID: {}, capacity: {}) for agent '{}' (reason: {})",
931 self.process_id(),
932 id,
933 self.capacity.total_capacity(),
934 agent_id,
935 reason
936 );
937 self.state = AssetState::Commissioned {
938 id,
939 agent_id: agent_id.clone(),
940 mothballed_year: None,
941 group_id,
942 };
943 }
944
945 pub fn select_candidate_for_investment(&mut self, agent_id: AgentID) {
947 assert!(
948 self.state == AssetState::Candidate,
949 "select_candidate_for_investment can only be called on Candidate assets"
950 );
951 check_capacity_valid_for_asset(self.capacity.total_capacity()).unwrap();
952 self.state = AssetState::Selected { agent_id };
953 }
954
955 pub fn mothball(&mut self, year: u32) {
957 let (id, agent_id, group_id) = match &self.state {
958 AssetState::Commissioned {
959 id,
960 agent_id,
961 group_id,
962 ..
963 } => (*id, agent_id.clone(), *group_id),
964 _ => panic!("Cannot mothball an asset that hasn't been commissioned"),
965 };
966 self.state = AssetState::Commissioned {
967 id,
968 agent_id: agent_id.clone(),
969 mothballed_year: Some(year),
970 group_id,
971 };
972 }
973
974 pub fn unmothball(&mut self) {
976 let (id, agent_id, group_id) = match &self.state {
977 AssetState::Commissioned {
978 id,
979 agent_id,
980 group_id,
981 ..
982 } => (*id, agent_id.clone(), *group_id),
983 _ => panic!("Cannot unmothball an asset that hasn't been commissioned"),
984 };
985 self.state = AssetState::Commissioned {
986 id,
987 agent_id: agent_id.clone(),
988 mothballed_year: None,
989 group_id,
990 };
991 }
992
993 pub fn get_mothballed_year(&self) -> Option<u32> {
995 let AssetState::Commissioned {
996 mothballed_year, ..
997 } = &self.state
998 else {
999 panic!("Cannot get mothballed year for an asset that hasn't been commissioned")
1000 };
1001 *mothballed_year
1002 }
1003
1004 pub fn unit_size(&self) -> Option<Capacity> {
1006 match self.capacity {
1007 AssetCapacity::Discrete(_, size) => Some(size),
1008 AssetCapacity::Continuous(_) => None,
1009 }
1010 }
1011
1012 pub fn is_divisible(&self) -> bool {
1014 self.process.unit_size.is_some()
1015 }
1016
1017 pub fn divide_asset(&self) -> Vec<AssetRef> {
1027 assert!(
1028 matches!(
1029 self.state,
1030 AssetState::Future { .. } | AssetState::Selected { .. }
1031 ),
1032 "Assets with state {} cannot be divided. Only Future or Selected assets can be divided",
1033 self.state
1034 );
1035
1036 let AssetCapacity::Discrete(n_units, unit_size) = self.capacity else {
1038 panic!("Only discrete assets can be divided")
1039 };
1040
1041 let child_asset = Self {
1043 capacity: AssetCapacity::Discrete(1, unit_size),
1044 ..self.clone()
1045 };
1046 let child_asset = AssetRef::from(Rc::new(child_asset));
1047 std::iter::repeat_n(child_asset, n_units as usize).collect()
1048 }
1049}
1050
1051#[allow(clippy::missing_fields_in_debug)]
1052impl std::fmt::Debug for Asset {
1053 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1054 f.debug_struct("Asset")
1055 .field("state", &self.state)
1056 .field("process_id", &self.process_id())
1057 .field("region_id", &self.region_id)
1058 .field("capacity", &self.capacity.total_capacity())
1059 .field("commission_year", &self.commission_year)
1060 .finish()
1061 }
1062}
1063
1064pub fn check_region_year_valid_for_process(
1066 process: &Process,
1067 region_id: &RegionID,
1068 year: u32,
1069) -> Result<()> {
1070 ensure!(
1071 process.regions.contains(region_id),
1072 "Process {} does not operate in region {}",
1073 process.id,
1074 region_id
1075 );
1076 ensure!(
1077 process.active_for_year(year),
1078 "Process {} does not operate in the year {}",
1079 process.id,
1080 year
1081 );
1082 Ok(())
1083}
1084
1085pub fn check_capacity_valid_for_asset(capacity: Capacity) -> Result<()> {
1087 ensure!(
1088 capacity.is_finite() && capacity > Capacity(0.0),
1089 "Capacity must be a finite, positive number"
1090 );
1091 Ok(())
1092}
1093
1094#[derive(Clone, Debug)]
1101pub struct AssetRef(Rc<Asset>);
1102
1103impl AssetRef {
1104 pub fn make_mut(&mut self) -> &mut Asset {
1106 Rc::make_mut(&mut self.0)
1107 }
1108}
1109
1110impl From<Rc<Asset>> for AssetRef {
1111 fn from(value: Rc<Asset>) -> Self {
1112 Self(value)
1113 }
1114}
1115
1116impl From<Asset> for AssetRef {
1117 fn from(value: Asset) -> Self {
1118 Self::from(Rc::new(value))
1119 }
1120}
1121
1122impl From<AssetRef> for Rc<Asset> {
1123 fn from(value: AssetRef) -> Self {
1124 value.0
1125 }
1126}
1127
1128impl Deref for AssetRef {
1129 type Target = Asset;
1130
1131 fn deref(&self) -> &Self::Target {
1132 &self.0
1133 }
1134}
1135
1136impl PartialEq for AssetRef {
1137 fn eq(&self, other: &Self) -> bool {
1138 Rc::ptr_eq(&self.0.process, &other.0.process)
1141 && self.0.region_id == other.0.region_id
1142 && self.0.commission_year == other.0.commission_year
1143 && self.0.state == other.0.state
1144 }
1145}
1146
1147impl Eq for AssetRef {}
1148
1149impl Hash for AssetRef {
1150 fn hash<H: Hasher>(&self, state: &mut H) {
1156 match &self.0.state {
1157 AssetState::Commissioned { id, .. } => {
1158 id.hash(state);
1161 }
1162 AssetState::Candidate | AssetState::Selected { .. } => {
1163 self.0.process.id.hash(state);
1166 self.0.region_id.hash(state);
1167 self.0.commission_year.hash(state);
1168 self.0.agent_id().hash(state);
1169 }
1170 AssetState::Future { .. } | AssetState::Decommissioned { .. } => {
1171 panic!("Cannot hash Future or Decommissioned assets");
1173 }
1174 }
1175 }
1176}
1177
1178impl PartialOrd for AssetRef {
1179 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1180 Some(self.cmp(other))
1181 }
1182}
1183
1184impl Ord for AssetRef {
1185 fn cmp(&self, other: &Self) -> Ordering {
1186 self.id().unwrap().cmp(&other.id().unwrap())
1187 }
1188}
1189
1190pub struct AssetPool {
1192 active: Vec<AssetRef>,
1194 future: Vec<Asset>,
1196 decommissioned: Vec<AssetRef>,
1198 next_id: u32,
1200 next_group_id: u32,
1202}
1203
1204impl AssetPool {
1205 pub fn new(mut assets: Vec<Asset>) -> Self {
1207 assets.sort_by(|a, b| a.commission_year.cmp(&b.commission_year));
1209
1210 Self {
1211 active: Vec::new(),
1212 future: assets,
1213 decommissioned: Vec::new(),
1214 next_id: 0,
1215 next_group_id: 0,
1216 }
1217 }
1218
1219 pub fn as_slice(&self) -> &[AssetRef] {
1221 &self.active
1222 }
1223
1224 pub fn update_for_year(&mut self, year: u32) {
1227 self.decommission_old(year);
1228 self.commission_new(year);
1229 }
1230
1231 fn commission_new(&mut self, year: u32) {
1233 let count = self
1235 .future
1236 .iter()
1237 .take_while(|asset| asset.commission_year <= year)
1238 .count();
1239
1240 for mut asset in self.future.drain(0..count) {
1242 if asset.max_decommission_year() <= year {
1244 warn!(
1245 "Asset '{}' with commission year {} and lifetime {} was decommissioned before \
1246 the start of the simulation",
1247 asset.process_id(),
1248 asset.commission_year,
1249 asset.process_parameter.lifetime
1250 );
1251 continue;
1252 }
1253
1254 if asset.is_divisible() {
1256 for mut child in asset.divide_asset() {
1257 child.make_mut().commission(
1258 AssetID(self.next_id),
1259 Some(AssetGroupID(self.next_group_id)),
1260 "user input",
1261 );
1262 self.next_id += 1;
1263 self.active.push(child);
1264 }
1265 self.next_group_id += 1;
1266 }
1267 else {
1269 asset.commission(AssetID(self.next_id), None, "user input");
1270 self.next_id += 1;
1271 self.active.push(asset.into());
1272 }
1273 }
1274 }
1275
1276 fn decommission_old(&mut self, year: u32) {
1278 let to_decommission = self
1280 .active
1281 .extract_if(.., |asset| asset.max_decommission_year() <= year);
1282
1283 for mut asset in to_decommission {
1284 asset.make_mut().decommission(year, "end of life");
1286 self.decommissioned.push(asset);
1287 }
1288 }
1289
1290 pub fn decommission_mothballed(&mut self, year: u32, mothball_years: u32) {
1292 let to_decommission = self.active.extract_if(.., |asset| {
1294 asset.get_mothballed_year().is_some()
1295 && asset.get_mothballed_year() <= Some(year - min(mothball_years, year))
1296 });
1297
1298 for mut asset in to_decommission {
1299 let decommissioned = asset.get_mothballed_year().unwrap() + mothball_years;
1301 asset.make_mut().decommission(
1302 decommissioned,
1303 &format!("The asset has not been used for the set mothball years ({mothball_years} years)."),
1304 );
1305 self.decommissioned.push(asset);
1306 }
1307 }
1308
1309 pub fn mothball_unretained<I>(&mut self, assets: I, year: u32)
1320 where
1321 I: IntoIterator<Item = AssetRef>,
1322 {
1323 for mut asset in assets {
1324 if match asset.state {
1325 AssetState::Commissioned { .. } => !self.active.contains(&asset),
1326 _ => panic!("Cannot mothball asset that has not been commissioned"),
1327 } {
1328 if asset.get_mothballed_year().is_none() {
1331 asset.make_mut().mothball(year);
1332 }
1333
1334 self.active.push(asset);
1337 }
1338 }
1339 self.active.sort();
1340 }
1341
1342 pub fn get(&self, id: AssetID) -> Option<&AssetRef> {
1349 let idx = self
1351 .active
1352 .binary_search_by(|asset| match &asset.state {
1353 AssetState::Commissioned { id: asset_id, .. } => asset_id.cmp(&id),
1354 _ => panic!("Active pool should only contain commissioned assets"),
1355 })
1356 .ok()?;
1357
1358 Some(&self.active[idx])
1359 }
1360
1361 pub fn iter_active(&self) -> slice::Iter<'_, AssetRef> {
1363 self.active.iter()
1364 }
1365
1366 pub fn iter_decommissioned(&self) -> slice::Iter<'_, AssetRef> {
1368 self.decommissioned.iter()
1369 }
1370
1371 pub fn iter_all(&self) -> impl Iterator<Item = &AssetRef> {
1375 chain(self.iter_active(), self.iter_decommissioned())
1376 }
1377
1378 pub fn take(&mut self) -> Vec<AssetRef> {
1380 std::mem::take(&mut self.active)
1381 }
1382
1383 pub fn extend<I>(&mut self, assets: I)
1385 where
1386 I: IntoIterator<Item = AssetRef>,
1387 {
1388 for mut asset in assets {
1391 match &asset.state {
1392 AssetState::Commissioned { .. } => {
1393 asset.make_mut().unmothball();
1394 self.active.push(asset);
1395 }
1396 AssetState::Selected { .. } => {
1397 if asset.is_divisible() {
1399 for mut child in asset.divide_asset() {
1400 child.make_mut().commission(
1401 AssetID(self.next_id),
1402 Some(AssetGroupID(self.next_group_id)),
1403 "selected",
1404 );
1405 self.next_id += 1;
1406 self.active.push(child);
1407 }
1408 self.next_group_id += 1;
1409 }
1410 else {
1412 asset
1413 .make_mut()
1414 .commission(AssetID(self.next_id), None, "selected");
1415 self.next_id += 1;
1416 self.active.push(asset);
1417 }
1418 }
1419 _ => panic!(
1420 "Cannot extend asset pool with asset in state {}. Only assets in \
1421 Commissioned or Selected states are allowed.",
1422 asset.state
1423 ),
1424 }
1425 }
1426
1427 self.active.sort();
1429
1430 debug_assert_eq!(self.active.iter().unique().count(), self.active.len());
1432 }
1433}
1434
1435pub trait AssetIterator<'a>: Iterator<Item = &'a AssetRef> + Sized
1437where
1438 Self: 'a,
1439{
1440 fn filter_agent(self, agent_id: &'a AgentID) -> impl Iterator<Item = &'a AssetRef> + 'a {
1442 self.filter(move |asset| asset.agent_id() == Some(agent_id))
1443 }
1444
1445 fn filter_primary_producers_of(
1447 self,
1448 commodity_id: &'a CommodityID,
1449 ) -> impl Iterator<Item = &'a AssetRef> + 'a {
1450 self.filter(move |asset| {
1451 asset
1452 .primary_output()
1453 .is_some_and(|flow| &flow.commodity.id == commodity_id)
1454 })
1455 }
1456
1457 fn filter_region(self, region_id: &'a RegionID) -> impl Iterator<Item = &'a AssetRef> + 'a {
1459 self.filter(move |asset| asset.region_id == *region_id)
1460 }
1461
1462 fn flows_for_commodity(
1464 self,
1465 commodity_id: &'a CommodityID,
1466 ) -> impl Iterator<Item = (&'a AssetRef, &'a ProcessFlow)> + 'a {
1467 self.filter_map(|asset| Some((asset, asset.get_flow(commodity_id)?)))
1468 }
1469}
1470
1471impl<'a, I> AssetIterator<'a> for I where I: Iterator<Item = &'a AssetRef> + Sized + 'a {}
1472
1473#[cfg(test)]
1474mod tests {
1475 use super::*;
1476 use crate::commodity::Commodity;
1477 use crate::fixture::{
1478 assert_error, assert_patched_runs_ok_simple, assert_validate_fails_with_simple, asset,
1479 process, process_activity_limits_map, process_flows_map, process_parameter_map, region_id,
1480 svd_commodity, time_slice, time_slice_info,
1481 };
1482 use crate::patch::FilePatch;
1483 use crate::process::{FlowType, Process, ProcessFlow, ProcessParameter};
1484 use crate::region::RegionID;
1485 use crate::time_slice::{TimeSliceID, TimeSliceInfo};
1486 use crate::units::{
1487 ActivityPerCapacity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity,
1488 MoneyPerCapacity, MoneyPerCapacityPerYear, MoneyPerFlow,
1489 };
1490 use float_cmp::assert_approx_eq;
1491 use indexmap::indexmap;
1492 use itertools::{Itertools, assert_equal};
1493 use rstest::{fixture, rstest};
1494 use std::iter;
1495 use std::rc::Rc;
1496
1497 #[allow(clippy::cast_possible_truncation)]
1499 #[allow(clippy::cast_sign_loss)]
1500 fn expected_children_for_divisible(asset: &Asset) -> usize {
1501 (asset.capacity.total_capacity() / asset.process.unit_size.expect("Asset is not divisible"))
1502 .value()
1503 .ceil() as usize
1504 }
1505
1506 #[rstest]
1507 #[case::exact_multiple(Capacity(12.0), Some(Capacity(4.0)), Some(3), Capacity(12.0))]
1508 #[case::rounded_up(Capacity(11.0), Some(Capacity(4.0)), Some(3), Capacity(12.0))]
1509 #[case::unit_size_greater_than_capacity(
1510 Capacity(3.0),
1511 Some(Capacity(4.0)),
1512 Some(1),
1513 Capacity(4.0)
1514 )]
1515 #[case::continuous(Capacity(5.5), None, None, Capacity(5.5))]
1516 fn from_capacity(
1517 #[case] capacity: Capacity,
1518 #[case] unit_size: Option<Capacity>,
1519 #[case] expected_n: Option<u32>,
1520 #[case] expected_total: Capacity,
1521 ) {
1522 let got = AssetCapacity::from_capacity(capacity, unit_size);
1523 assert_eq!(got.n_units(), expected_n);
1524 assert_eq!(got.total_capacity(), expected_total);
1525 }
1526
1527 #[rstest]
1528 #[case::round_up(3u32, Capacity(4.0), Dimensionless(0.5), 2u32)]
1529 #[case::exact(3u32, Capacity(4.0), Dimensionless(0.33), 1u32)]
1530 fn apply_limit_factor(
1531 #[case] start_units: u32,
1532 #[case] unit_size: Capacity,
1533 #[case] factor: Dimensionless,
1534 #[case] expected_units: u32,
1535 ) {
1536 let orig = AssetCapacity::Discrete(start_units, unit_size);
1537 let got = orig.apply_limit_factor(factor);
1538 assert_eq!(got, AssetCapacity::Discrete(expected_units, unit_size));
1539 }
1540
1541 #[rstest]
1542 fn get_input_cost_from_prices_works(
1543 region_id: RegionID,
1544 svd_commodity: Commodity,
1545 mut process: Process,
1546 time_slice: TimeSliceID,
1547 ) {
1548 let commodity_rc = Rc::new(svd_commodity);
1550 let process_flow = ProcessFlow {
1551 commodity: Rc::clone(&commodity_rc),
1552 coeff: FlowPerActivity(-2.0), kind: FlowType::Fixed,
1554 cost: MoneyPerFlow(0.0),
1555 };
1556 let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() };
1557 let process_flows_map = process_flows_map(process.regions.clone(), Rc::new(process_flows));
1558 process.flows = process_flows_map;
1559
1560 let asset =
1562 Asset::new_candidate(Rc::new(process), region_id.clone(), Capacity(1.0), 2020).unwrap();
1563
1564 let mut input_prices = CommodityPrices::default();
1566 input_prices.insert(&commodity_rc.id, ®ion_id, &time_slice, MoneyPerFlow(3.0));
1567
1568 let cost = asset.get_input_cost_from_prices(&input_prices, &time_slice);
1570 assert_approx_eq!(MoneyPerActivity, cost, MoneyPerActivity(6.0));
1572 }
1573
1574 #[rstest]
1575 #[case(Capacity(0.01))]
1576 #[case(Capacity(0.5))]
1577 #[case(Capacity(1.0))]
1578 #[case(Capacity(100.0))]
1579 fn asset_new_valid(process: Process, #[case] capacity: Capacity) {
1580 let agent_id = AgentID("agent1".into());
1581 let region_id = RegionID("GBR".into());
1582 let asset = Asset::new_future(agent_id, process.into(), region_id, capacity, 2015).unwrap();
1583 assert!(asset.id().is_none());
1584 }
1585
1586 #[rstest]
1587 #[case(Capacity(0.0))]
1588 #[case(Capacity(-0.01))]
1589 #[case(Capacity(-1.0))]
1590 #[case(Capacity(f64::NAN))]
1591 #[case(Capacity(f64::INFINITY))]
1592 #[case(Capacity(f64::NEG_INFINITY))]
1593 fn asset_new_invalid_capacity(process: Process, #[case] capacity: Capacity) {
1594 let agent_id = AgentID("agent1".into());
1595 let region_id = RegionID("GBR".into());
1596 assert_error!(
1597 Asset::new_future(agent_id, process.into(), region_id, capacity, 2015),
1598 "Capacity must be a finite, positive number"
1599 );
1600 }
1601
1602 #[rstest]
1603 fn asset_new_invalid_commission_year(process: Process) {
1604 let agent_id = AgentID("agent1".into());
1605 let region_id = RegionID("GBR".into());
1606 assert_error!(
1607 Asset::new_future(agent_id, process.into(), region_id, Capacity(1.0), 2007),
1608 "Process process1 does not operate in the year 2007"
1609 );
1610 }
1611
1612 #[rstest]
1613 fn asset_new_invalid_region(process: Process) {
1614 let agent_id = AgentID("agent1".into());
1615 let region_id = RegionID("FRA".into());
1616 assert_error!(
1617 Asset::new_future(agent_id, process.into(), region_id, Capacity(1.0), 2015),
1618 "Process process1 does not operate in region FRA"
1619 );
1620 }
1621
1622 #[fixture]
1623 fn asset_pool(mut process: Process) -> AssetPool {
1624 let process_param = ProcessParameter {
1626 capital_cost: MoneyPerCapacity(5.0),
1627 fixed_operating_cost: MoneyPerCapacityPerYear(2.0),
1628 variable_operating_cost: MoneyPerActivity(1.0),
1629 lifetime: 20,
1630 discount_rate: Dimensionless(0.9),
1631 };
1632 let process_parameter_map = process_parameter_map(process.regions.clone(), process_param);
1633 process.parameters = process_parameter_map;
1634
1635 let rc_process = Rc::new(process);
1636 let future = [2020, 2010]
1637 .map(|year| {
1638 Asset::new_future(
1639 "agent1".into(),
1640 Rc::clone(&rc_process),
1641 "GBR".into(),
1642 Capacity(1.0),
1643 year,
1644 )
1645 .unwrap()
1646 })
1647 .into_iter()
1648 .collect_vec();
1649
1650 AssetPool::new(future)
1651 }
1652
1653 #[fixture]
1654 fn process_with_activity_limits(
1655 mut process: Process,
1656 time_slice_info: TimeSliceInfo,
1657 time_slice: TimeSliceID,
1658 ) -> Process {
1659 let mut activity_limits = ActivityLimits::new_with_full_availability(&time_slice_info);
1661 activity_limits.add_time_slice_limit(time_slice, Dimensionless(0.1)..=Dimensionless(0.5));
1662 process.activity_limits =
1663 process_activity_limits_map(process.regions.clone(), activity_limits);
1664
1665 process.capacity_to_activity = ActivityPerCapacity(2.0);
1667 process
1668 }
1669
1670 #[fixture]
1671 fn asset_with_activity_limits(process_with_activity_limits: Process) -> Asset {
1672 Asset::new_future(
1673 "agent1".into(),
1674 Rc::new(process_with_activity_limits),
1675 "GBR".into(),
1676 Capacity(2.0),
1677 2010,
1678 )
1679 .unwrap()
1680 }
1681
1682 #[fixture]
1683 fn asset_divisible(mut process: Process) -> Asset {
1684 process.unit_size = Some(Capacity(4.0));
1685 Asset::new_future(
1686 "agent1".into(),
1687 Rc::new(process),
1688 "GBR".into(),
1689 Capacity(11.0),
1690 2010,
1691 )
1692 .unwrap()
1693 }
1694
1695 #[rstest]
1696 fn asset_get_activity_per_capacity_limits(
1697 asset_with_activity_limits: Asset,
1698 time_slice: TimeSliceID,
1699 ) {
1700 assert_eq!(
1702 asset_with_activity_limits.get_activity_per_capacity_limits(&time_slice),
1703 ActivityPerCapacity(0.2)..=ActivityPerCapacity(1.0)
1704 );
1705 }
1706
1707 #[rstest]
1708 #[case::exact_multiple(Capacity(12.0), Capacity(4.0), 3)] #[case::rounded_up(Capacity(11.0), Capacity(4.0), 3)] #[case::unit_size_equals_capacity(Capacity(4.0), Capacity(4.0), 1)] #[case::unit_size_greater_than_capacity(Capacity(3.0), Capacity(4.0), 1)] fn divide_asset(
1713 mut process: Process,
1714 #[case] capacity: Capacity,
1715 #[case] unit_size: Capacity,
1716 #[case] n_expected_children: usize,
1717 ) {
1718 process.unit_size = Some(unit_size);
1719 let asset = Asset::new_future(
1720 "agent1".into(),
1721 Rc::new(process),
1722 "GBR".into(),
1723 capacity,
1724 2010,
1725 )
1726 .unwrap();
1727
1728 assert!(asset.is_divisible(), "Asset should be divisible!");
1729
1730 let children = asset.divide_asset();
1731 assert_eq!(
1732 children.len(),
1733 n_expected_children,
1734 "Unexpected number of children"
1735 );
1736
1737 for child in children.clone() {
1739 assert_eq!(
1740 child.capacity.total_capacity(),
1741 unit_size,
1742 "Child capacity should equal unit_size"
1743 );
1744 }
1745
1746 let total_child_capacity: Capacity = children
1748 .iter()
1749 .map(|child| child.capacity.total_capacity())
1750 .sum();
1751 assert!(
1752 total_child_capacity >= asset.capacity.total_capacity(),
1753 "Total capacity should be >= parent capacity"
1754 );
1755 }
1756
1757 #[rstest]
1758 fn asset_pool_new(asset_pool: AssetPool) {
1759 assert!(asset_pool.active.is_empty());
1761 assert!(asset_pool.future.len() == 2);
1762 assert!(asset_pool.future[0].commission_year == 2010);
1763 assert!(asset_pool.future[1].commission_year == 2020);
1764 }
1765
1766 #[rstest]
1767 fn asset_pool_commission_new1(mut asset_pool: AssetPool) {
1768 asset_pool.commission_new(2010);
1770 assert_equal(asset_pool.iter_active(), iter::once(&asset_pool.active[0]));
1771 }
1772
1773 #[rstest]
1774 fn asset_pool_commission_new2(mut asset_pool: AssetPool) {
1775 asset_pool.commission_new(2011);
1777 assert_equal(asset_pool.iter_active(), iter::once(&asset_pool.active[0]));
1778 }
1779
1780 #[rstest]
1781 fn asset_pool_commission_new3(mut asset_pool: AssetPool) {
1782 asset_pool.commission_new(2000);
1784 assert!(asset_pool.iter_active().next().is_none()); }
1786
1787 #[rstest]
1788 fn asset_pool_commission_new_divisible(asset_divisible: Asset) {
1789 let commision_year = asset_divisible.commission_year;
1790 let expected_children = expected_children_for_divisible(&asset_divisible);
1791 let mut asset_pool = AssetPool::new(vec![asset_divisible.clone()]);
1792 assert!(asset_pool.active.is_empty());
1793 asset_pool.commission_new(commision_year);
1794 assert!(asset_pool.future.is_empty());
1795 assert!(!asset_pool.active.is_empty());
1796 assert_eq!(asset_pool.active.len(), expected_children);
1797 assert_eq!(asset_pool.next_group_id, 1);
1798 }
1799
1800 #[rstest]
1801 fn asset_pool_commission_already_decommissioned(asset: Asset) {
1802 let year = asset.max_decommission_year();
1803 let mut asset_pool = AssetPool::new(vec![asset]);
1804 assert!(asset_pool.active.is_empty());
1805 asset_pool.update_for_year(year);
1806 assert!(asset_pool.active.is_empty());
1807 }
1808
1809 #[rstest]
1810 fn asset_pool_decommission_old(mut asset_pool: AssetPool) {
1811 asset_pool.commission_new(2020);
1812 assert!(asset_pool.future.is_empty());
1813 assert_eq!(asset_pool.active.len(), 2);
1814 asset_pool.decommission_old(2030); assert_eq!(asset_pool.active.len(), 1);
1816 assert_eq!(asset_pool.active[0].commission_year, 2020);
1817 assert_eq!(asset_pool.decommissioned.len(), 1);
1818 assert_eq!(asset_pool.decommissioned[0].commission_year, 2010);
1819 assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2030));
1820 asset_pool.decommission_old(2032); assert_eq!(asset_pool.active.len(), 1);
1822 assert_eq!(asset_pool.active[0].commission_year, 2020);
1823 assert_eq!(asset_pool.decommissioned.len(), 1);
1824 assert_eq!(asset_pool.decommissioned[0].commission_year, 2010);
1825 assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2030));
1826 asset_pool.decommission_old(2040); assert!(asset_pool.active.is_empty());
1828 assert_eq!(asset_pool.decommissioned.len(), 2);
1829 assert_eq!(asset_pool.decommissioned[0].commission_year, 2010);
1830 assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2030));
1831 assert_eq!(asset_pool.decommissioned[1].commission_year, 2020);
1832 assert_eq!(asset_pool.decommissioned[1].decommission_year(), Some(2040));
1833 }
1834
1835 #[rstest]
1836 fn asset_pool_get(mut asset_pool: AssetPool) {
1837 asset_pool.commission_new(2020);
1838 assert_eq!(asset_pool.get(AssetID(0)), Some(&asset_pool.active[0]));
1839 assert_eq!(asset_pool.get(AssetID(1)), Some(&asset_pool.active[1]));
1840 }
1841
1842 #[rstest]
1843 fn asset_pool_extend_empty(mut asset_pool: AssetPool) {
1844 asset_pool.commission_new(2020);
1846 let original_count = asset_pool.active.len();
1847
1848 asset_pool.extend(Vec::<AssetRef>::new());
1850
1851 assert_eq!(asset_pool.active.len(), original_count);
1852 }
1853
1854 #[rstest]
1855 fn asset_pool_extend_existing_assets(mut asset_pool: AssetPool) {
1856 asset_pool.commission_new(2020);
1858 assert_eq!(asset_pool.active.len(), 2);
1859 let existing_assets = asset_pool.take();
1860
1861 asset_pool.extend(existing_assets.clone());
1863
1864 assert_eq!(asset_pool.active.len(), 2);
1865 assert_eq!(asset_pool.active[0].id(), Some(AssetID(0)));
1866 assert_eq!(asset_pool.active[1].id(), Some(AssetID(1)));
1867 }
1868
1869 #[rstest]
1870 fn asset_pool_extend_new_assets(mut asset_pool: AssetPool, process: Process) {
1871 asset_pool.commission_new(2020);
1873 let original_count = asset_pool.active.len();
1874
1875 let process_rc = Rc::new(process);
1877 let new_assets = vec![
1878 Asset::new_selected(
1879 "agent2".into(),
1880 Rc::clone(&process_rc),
1881 "GBR".into(),
1882 Capacity(1.5),
1883 2015,
1884 )
1885 .unwrap()
1886 .into(),
1887 Asset::new_selected(
1888 "agent3".into(),
1889 Rc::clone(&process_rc),
1890 "GBR".into(),
1891 Capacity(2.5),
1892 2020,
1893 )
1894 .unwrap()
1895 .into(),
1896 ];
1897
1898 asset_pool.extend(new_assets);
1899
1900 assert_eq!(asset_pool.active.len(), original_count + 2);
1901 assert_eq!(asset_pool.active[original_count].id(), Some(AssetID(2)));
1903 assert_eq!(asset_pool.active[original_count + 1].id(), Some(AssetID(3)));
1904 assert_eq!(
1905 asset_pool.active[original_count].agent_id(),
1906 Some(&"agent2".into())
1907 );
1908 assert_eq!(
1909 asset_pool.active[original_count + 1].agent_id(),
1910 Some(&"agent3".into())
1911 );
1912 }
1913
1914 #[rstest]
1915 fn asset_pool_extend_new_divisible_assets(mut asset_pool: AssetPool, mut process: Process) {
1916 asset_pool.commission_new(2020);
1918 let original_count = asset_pool.active.len();
1919
1920 process.unit_size = Some(Capacity(4.0));
1922 let process_rc = Rc::new(process);
1923 let new_assets: Vec<AssetRef> = vec![
1924 Asset::new_selected(
1925 "agent2".into(),
1926 Rc::clone(&process_rc),
1927 "GBR".into(),
1928 Capacity(11.0),
1929 2015,
1930 )
1931 .unwrap()
1932 .into(),
1933 ];
1934 let expected_children = expected_children_for_divisible(&new_assets[0]);
1935 asset_pool.extend(new_assets);
1936 assert_eq!(asset_pool.active.len(), original_count + expected_children);
1937 }
1938
1939 #[rstest]
1940 fn asset_pool_extend_mixed_assets(mut asset_pool: AssetPool, process: Process) {
1941 asset_pool.commission_new(2020);
1943
1944 let new_asset = Asset::new_selected(
1946 "agent_new".into(),
1947 process.into(),
1948 "GBR".into(),
1949 Capacity(3.0),
1950 2015,
1951 )
1952 .unwrap()
1953 .into();
1954
1955 asset_pool.extend(vec![new_asset]);
1957
1958 assert_eq!(asset_pool.active.len(), 3);
1959 assert!(asset_pool.active.iter().any(|a| a.id() == Some(AssetID(0))));
1961 assert!(asset_pool.active.iter().any(|a| a.id() == Some(AssetID(1))));
1962 assert!(asset_pool.active.iter().any(|a| a.id() == Some(AssetID(2))));
1963 assert!(
1965 asset_pool
1966 .active
1967 .iter()
1968 .any(|a| a.agent_id() == Some(&"agent_new".into()))
1969 );
1970 }
1971
1972 #[rstest]
1973 fn asset_pool_extend_maintains_sort_order(mut asset_pool: AssetPool, process: Process) {
1974 asset_pool.commission_new(2020);
1976
1977 let process_rc = Rc::new(process);
1979 let new_assets = vec![
1980 Asset::new_selected(
1981 "agent_high_id".into(),
1982 Rc::clone(&process_rc),
1983 "GBR".into(),
1984 Capacity(1.0),
1985 2010,
1986 )
1987 .unwrap()
1988 .into(),
1989 Asset::new_selected(
1990 "agent_low_id".into(),
1991 Rc::clone(&process_rc),
1992 "GBR".into(),
1993 Capacity(1.0),
1994 2015,
1995 )
1996 .unwrap()
1997 .into(),
1998 ];
1999
2000 asset_pool.extend(new_assets);
2001
2002 let ids: Vec<u32> = asset_pool
2004 .iter_active()
2005 .map(|a| a.id().unwrap().0)
2006 .collect();
2007 assert_equal(ids, 0..4);
2008 }
2009
2010 #[rstest]
2011 fn asset_pool_extend_no_duplicates_expected(mut asset_pool: AssetPool) {
2012 asset_pool.commission_new(2020);
2014 let original_count = asset_pool.active.len();
2015
2016 asset_pool.extend(Vec::new());
2019
2020 assert_eq!(asset_pool.active.len(), original_count);
2021 assert_eq!(
2023 asset_pool.active.iter().unique().count(),
2024 asset_pool.active.len()
2025 );
2026 }
2027
2028 #[rstest]
2029 fn asset_pool_extend_increments_next_id(mut asset_pool: AssetPool, process: Process) {
2030 asset_pool.commission_new(2020);
2032 assert_eq!(asset_pool.next_id, 2); let process_rc = Rc::new(process);
2036 let new_assets = vec![
2037 Asset::new_selected(
2038 "agent1".into(),
2039 Rc::clone(&process_rc),
2040 "GBR".into(),
2041 Capacity(1.0),
2042 2015,
2043 )
2044 .unwrap()
2045 .into(),
2046 Asset::new_selected(
2047 "agent2".into(),
2048 Rc::clone(&process_rc),
2049 "GBR".into(),
2050 Capacity(1.0),
2051 2020,
2052 )
2053 .unwrap()
2054 .into(),
2055 ];
2056
2057 asset_pool.extend(new_assets);
2058
2059 assert_eq!(asset_pool.next_id, 4);
2061 assert_eq!(asset_pool.active[2].id(), Some(AssetID(2)));
2062 assert_eq!(asset_pool.active[3].id(), Some(AssetID(3)));
2063 }
2064
2065 #[rstest]
2066 fn asset_pool_mothball_unretained(mut asset_pool: AssetPool) {
2067 asset_pool.commission_new(2020);
2069 assert_eq!(asset_pool.active.len(), 2);
2070
2071 let removed_asset = asset_pool.active.remove(0);
2073 assert_eq!(asset_pool.active.len(), 1);
2074
2075 let assets_to_check = vec![removed_asset.clone(), asset_pool.active[0].clone()];
2077 asset_pool.mothball_unretained(assets_to_check, 2025);
2078
2079 assert_eq!(asset_pool.active.len(), 2); assert_eq!(asset_pool.active[0].get_mothballed_year(), Some(2025));
2082 }
2083
2084 #[rstest]
2085 fn asset_pool_decommission_unused(mut asset_pool: AssetPool) {
2086 asset_pool.commission_new(2020);
2088 assert_eq!(asset_pool.active.len(), 2);
2089 assert_eq!(asset_pool.decommissioned.len(), 0);
2090
2091 let mothball_years: u32 = 10;
2093 asset_pool.active[0]
2094 .make_mut()
2095 .mothball(2025 - mothball_years);
2096
2097 assert_eq!(
2098 asset_pool.active[0].get_mothballed_year(),
2099 Some(2025 - mothball_years)
2100 );
2101
2102 asset_pool.decommission_mothballed(2025, mothball_years);
2104
2105 assert_eq!(asset_pool.active.len(), 1); assert_eq!(asset_pool.decommissioned.len(), 1);
2108 assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2025));
2109 }
2110
2111 #[rstest]
2112 fn asset_pool_decommission_if_not_active_none_active(mut asset_pool: AssetPool) {
2113 asset_pool.commission_new(2020);
2115 let all_assets = asset_pool.active.clone();
2116
2117 asset_pool.active.clear();
2119
2120 asset_pool.mothball_unretained(all_assets.clone(), 2025);
2122
2123 assert_eq!(asset_pool.active.len(), 2);
2125 assert_eq!(asset_pool.active[0].id(), all_assets[0].id());
2126 assert_eq!(asset_pool.active[0].get_mothballed_year(), Some(2025));
2127 assert_eq!(asset_pool.active[1].id(), all_assets[1].id());
2128 assert_eq!(asset_pool.active[1].get_mothballed_year(), Some(2025));
2129 }
2130
2131 #[rstest]
2132 #[should_panic(expected = "Cannot mothball asset that has not been commissioned")]
2133 fn asset_pool_decommission_if_not_active_non_commissioned_asset(
2134 mut asset_pool: AssetPool,
2135 process: Process,
2136 ) {
2137 let non_commissioned_asset = Asset::new_future(
2139 "agent_new".into(),
2140 process.into(),
2141 "GBR".into(),
2142 Capacity(1.0),
2143 2015,
2144 )
2145 .unwrap()
2146 .into();
2147
2148 asset_pool.mothball_unretained(vec![non_commissioned_asset], 2025);
2150 }
2151
2152 #[rstest]
2153 fn asset_commission(process: Process) {
2154 let process_rc = Rc::new(process);
2156 let mut asset1 = Asset::new_future(
2157 "agent1".into(),
2158 Rc::clone(&process_rc),
2159 "GBR".into(),
2160 Capacity(1.0),
2161 2020,
2162 )
2163 .unwrap();
2164 asset1.commission(AssetID(1), None, "");
2165 assert!(asset1.is_commissioned());
2166 assert_eq!(asset1.id(), Some(AssetID(1)));
2167
2168 let mut asset2 = Asset::new_selected(
2170 "agent1".into(),
2171 Rc::clone(&process_rc),
2172 "GBR".into(),
2173 Capacity(1.0),
2174 2020,
2175 )
2176 .unwrap();
2177 asset2.commission(AssetID(2), None, "");
2178 assert!(asset2.is_commissioned());
2179 assert_eq!(asset2.id(), Some(AssetID(2)));
2180 }
2181
2182 #[rstest]
2183 #[case::commission_during_process_lifetime(2024, 2024)]
2184 #[case::decommission_after_process_lifetime_ends(2026, 2025)]
2185 fn asset_decommission(
2186 #[case] requested_decommission_year: u32,
2187 #[case] expected_decommission_year: u32,
2188 process: Process,
2189 ) {
2190 let process_rc = Rc::new(process);
2192 let mut asset = Asset::new_future(
2193 "agent1".into(),
2194 Rc::clone(&process_rc),
2195 "GBR".into(),
2196 Capacity(1.0),
2197 2020,
2198 )
2199 .unwrap();
2200 asset.commission(AssetID(1), None, "");
2201 assert!(asset.is_commissioned());
2202 assert_eq!(asset.id(), Some(AssetID(1)));
2203
2204 asset.decommission(requested_decommission_year, "");
2206 assert!(!asset.is_commissioned());
2207 assert_eq!(asset.decommission_year(), Some(expected_decommission_year));
2208 }
2209
2210 #[rstest]
2211 #[case::decommission_after_predefined_max_year(2026, 2025, Some(2025))]
2212 #[case::decommission_before_predefined_max_year(2024, 2024, Some(2025))]
2213 #[case::decommission_during_process_lifetime_end_no_max_year(2024, 2024, None)]
2214 #[case::decommission_after_process_lifetime_end_no_max_year(2026, 2025, None)]
2215 fn asset_decommission_with_max_decommission_year_predefined(
2216 #[case] requested_decommission_year: u32,
2217 #[case] expected_decommission_year: u32,
2218 #[case] max_decommission_year: Option<u32>,
2219 process: Process,
2220 ) {
2221 let process_rc = Rc::new(process);
2223 let mut asset = Asset::new_future_with_max_decommission(
2224 "agent1".into(),
2225 Rc::clone(&process_rc),
2226 "GBR".into(),
2227 Capacity(1.0),
2228 2020,
2229 max_decommission_year,
2230 )
2231 .unwrap();
2232 asset.commission(AssetID(1), None, "");
2233 assert!(asset.is_commissioned());
2234 assert_eq!(asset.id(), Some(AssetID(1)));
2235
2236 asset.decommission(requested_decommission_year, "");
2238 assert!(!asset.is_commissioned());
2239 assert_eq!(asset.decommission_year(), Some(expected_decommission_year));
2240 }
2241
2242 #[rstest]
2243 #[should_panic(expected = "Assets with state Candidate cannot be commissioned")]
2244 fn commission_wrong_states(process: Process) {
2245 let mut asset =
2246 Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap();
2247 asset.commission(AssetID(1), None, "");
2248 }
2249
2250 #[rstest]
2251 #[should_panic(expected = "Cannot decommission an asset that hasn't been commissioned")]
2252 fn decommission_wrong_state(process: Process) {
2253 let mut asset =
2254 Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap();
2255 asset.decommission(2025, "");
2256 }
2257
2258 #[test]
2259 fn commission_year_before_time_horizon() {
2260 let processes_patch = FilePatch::new("processes.csv")
2261 .with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
2262 .with_addition("GASDRV,Dry gas extraction,all,GASPRD,1980,2040,1.0,");
2263
2264 let patches = vec![
2267 processes_patch.clone(),
2268 FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,1980"),
2269 ];
2270 assert_patched_runs_ok_simple!(patches);
2271
2272 let patches = vec![
2274 processes_patch,
2275 FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,1970"),
2276 ];
2277 assert_validate_fails_with_simple!(
2278 patches,
2279 "Agent A0_GEX has asset with commission year 1970, not within process GASDRV commission years: 1980..=2040"
2280 );
2281 }
2282
2283 #[test]
2284 fn commission_year_after_time_horizon() {
2285 let processes_patch = FilePatch::new("processes.csv")
2286 .with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
2287 .with_addition("GASDRV,Dry gas extraction,all,GASPRD,2020,2050,1.0,");
2288
2289 let patches = vec![
2291 processes_patch.clone(),
2292 FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,2050"),
2293 ];
2294 assert_patched_runs_ok_simple!(patches);
2295
2296 let patches = vec![
2298 processes_patch,
2299 FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,2060"),
2300 ];
2301 assert_validate_fails_with_simple!(
2302 patches,
2303 "Agent A0_GEX has asset with commission year 2060, not within process GASDRV commission years: 2020..=2050"
2304 );
2305 }
2306}