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::PriceMap;
10use crate::time_slice::{TimeSliceID, TimeSliceSelection};
11use crate::units::{
12 Activity, ActivityPerCapacity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity,
13 MoneyPerCapacity, MoneyPerFlow, UnitType, Year,
14};
15use anyhow::{Context, Result, ensure};
16use indexmap::IndexMap;
17use log::debug;
18use map_macro::vec_deque;
19use serde::{Deserialize, Serialize};
20use std::cmp::Ordering;
21use std::collections::VecDeque;
22use std::collections::hash_map::DefaultHasher;
23use std::hash::{Hash, Hasher};
24use std::ops::RangeInclusive;
25use std::sync::Arc;
26
27mod capacity;
28pub use capacity::AssetCapacity;
29mod pool;
30pub use pool::AssetPool;
31
32#[derive(
34 Clone,
35 Copy,
36 Debug,
37 derive_more::Display,
38 Eq,
39 Hash,
40 Ord,
41 PartialEq,
42 PartialOrd,
43 Deserialize,
44 Serialize,
45)]
46pub struct AssetID(u32);
47
48#[derive(PartialEq, Debug, Clone)]
50pub struct MothballEvent {
51 year: u32,
52 num_tranches: u32,
53}
54
55#[derive(Clone, Debug, PartialEq, strum::Display)]
65pub enum AssetState {
66 Commissioned {
68 id: AssetID,
70 agent_id: AgentID,
72 mothball_events: VecDeque<MothballEvent>,
77 },
78 Ready {
80 agent_id: AgentID,
82 commission_reason: &'static str,
84 },
85 Candidate,
87}
88
89#[derive(Clone)]
91pub struct Asset {
92 state: AssetState,
94 process: Arc<Process>,
96 activity_limits: Arc<ActivityLimits>,
98 flows: Arc<IndexMap<CommodityID, ProcessFlow>>,
100 process_parameter: Arc<ProcessParameter>,
102 region_id: RegionID,
104 capacity: AssetCapacity,
106 commission_year: u32,
108 max_decommission_year: u32,
110}
111
112fn hash_unit<U: UnitType>(value: U) -> u64 {
114 let value = value.value();
115 if value == 0.0 { 0 } else { value.to_bits() }
116}
117
118impl Asset {
119 pub fn new_candidate(
123 process: Arc<Process>,
124 region_id: RegionID,
125 tranche_size: Capacity,
126 commission_year: u32,
127 ) -> Result<Self> {
128 Self::new_with_state(
129 AssetState::Candidate,
130 process,
131 region_id,
132 AssetCapacity::single(tranche_size),
133 commission_year,
134 None,
135 )
136 }
137
138 #[cfg(test)]
143 pub fn new_ready(
144 agent_id: AgentID,
145 process: Arc<Process>,
146 region_id: RegionID,
147 capacity: AssetCapacity,
148 commission_year: u32,
149 ) -> Result<Self> {
150 Self::new_with_state(
151 AssetState::Ready {
152 agent_id,
153 commission_reason: "selected",
154 },
155 process,
156 region_id,
157 capacity,
158 commission_year,
159 None,
160 )
161 }
162
163 #[cfg(test)]
168 pub fn new_commissioned(
169 agent_id: AgentID,
170 process: Arc<Process>,
171 region_id: RegionID,
172 capacity: AssetCapacity,
173 commission_year: u32,
174 ) -> Result<Self> {
175 Self::new_with_state(
176 AssetState::Commissioned {
177 id: AssetID(0),
178 agent_id,
179 mothball_events: vec_deque![],
180 },
181 process,
182 region_id,
183 capacity,
184 commission_year,
185 None,
186 )
187 }
188
189 fn new_with_state(
191 state: AssetState,
192 process: Arc<Process>,
193 region_id: RegionID,
194 capacity: AssetCapacity,
195 commission_year: u32,
196 max_decommission_year: Option<u32>,
197 ) -> Result<Self> {
198 check_region_year_valid_for_process(&process, ®ion_id, commission_year)?;
199 ensure!(
200 capacity.total_capacity() >= Capacity(0.0),
201 "Capacity must be non-negative"
202 );
203
204 let key = (region_id.clone(), commission_year);
210 let activity_limits = process
211 .activity_limits
212 .get(&key)
213 .with_context(|| {
214 format!(
215 "No process availabilities supplied for process {} in region {} in year {}. \
216 You should update process_availabilities.csv.",
217 process.id, region_id, commission_year
218 )
219 })?
220 .clone();
221 let flows = process
222 .flows
223 .get(&key)
224 .with_context(|| {
225 format!(
226 "No commodity flows supplied for process {} in region {} in year {}. \
227 You should update process_flows.csv.",
228 process.id, region_id, commission_year
229 )
230 })?
231 .clone();
232 let process_parameter = process
233 .parameters
234 .get(&key)
235 .with_context(|| {
236 format!(
237 "No process parameters supplied for process {} in region {} in year {}. \
238 You should update process_parameters.csv.",
239 process.id, region_id, commission_year
240 )
241 })?
242 .clone();
243
244 let max_decommission_year =
245 max_decommission_year.unwrap_or(commission_year + process_parameter.lifetime);
246 ensure!(
247 max_decommission_year > commission_year,
248 "Max decommission year must be greater than commission year"
249 );
250 Ok(Self {
251 state,
252 process,
253 activity_limits,
254 flows,
255 process_parameter,
256 region_id,
257 capacity,
258 commission_year,
259 max_decommission_year,
260 })
261 }
262
263 pub fn state(&self) -> &AssetState {
265 &self.state
266 }
267
268 pub fn process_parameter(&self) -> &ProcessParameter {
270 &self.process_parameter
271 }
272
273 pub fn max_decommission_year(&self) -> u32 {
275 self.max_decommission_year
276 }
277
278 pub fn get_activity_per_capacity_limits(
280 &self,
281 time_slice: &TimeSliceID,
282 ) -> RangeInclusive<ActivityPerCapacity> {
283 let limits = &self.activity_limits.get_limit_for_time_slice(time_slice);
284 let cap2act = self.process.capacity_to_activity;
285 (cap2act * *limits.start())..=(cap2act * *limits.end())
286 }
287
288 pub fn get_activity_limits_for_selection(
290 &self,
291 time_slice_selection: &TimeSliceSelection,
292 ) -> RangeInclusive<Activity> {
293 let activity_per_capacity_limits = self.activity_limits.get_limit(time_slice_selection);
294 let cap2act = self.process.capacity_to_activity;
295 let max_activity = self.active_capacity() * cap2act;
296 let lb = max_activity * *activity_per_capacity_limits.start();
297 let ub = max_activity * *activity_per_capacity_limits.end();
298 lb..=ub
299 }
300
301 pub fn get_activity_per_capacity_limits_for_selection(
303 &self,
304 time_slice_selection: &TimeSliceSelection,
305 ) -> RangeInclusive<ActivityPerCapacity> {
306 let limits = self.activity_limits.get_limit(time_slice_selection);
307 let cap2act = self.process.capacity_to_activity;
308 (cap2act * *limits.start())..=(cap2act * *limits.end())
309 }
310
311 pub fn iter_activity_limits(
313 &self,
314 ) -> impl Iterator<Item = (TimeSliceSelection, RangeInclusive<Activity>)> + '_ {
315 let max_act = self.max_activity();
316 self.activity_limits
317 .iter_limits()
318 .map(move |(ts_sel, limit)| {
319 (
320 ts_sel,
321 (max_act * *limit.start())..=(max_act * *limit.end()),
322 )
323 })
324 }
325
326 pub fn iter_activity_per_capacity_limits(
328 &self,
329 ) -> impl Iterator<Item = (TimeSliceSelection, RangeInclusive<ActivityPerCapacity>)> + '_ {
330 let cap2act = self.process.capacity_to_activity;
331 self.activity_limits
332 .iter_limits()
333 .map(move |(ts_sel, limit)| {
334 (
335 ts_sel,
336 (cap2act * *limit.start())..=(cap2act * *limit.end()),
337 )
338 })
339 }
340
341 pub fn get_total_output_per_activity(&self) -> FlowPerActivity {
348 self.iter_output_flows().map(|flow| flow.coeff).sum()
349 }
350
351 pub fn get_operating_cost(&self, year: u32, time_slice: &TimeSliceID) -> MoneyPerActivity {
353 let flows_cost = self
355 .iter_flows()
356 .map(|flow| flow.get_total_cost_per_activity(&self.region_id, year, time_slice))
357 .sum();
358
359 self.process_parameter.variable_operating_cost + flows_cost
360 }
361
362 pub fn get_revenue_from_flows(
366 &self,
367 prices: &PriceMap,
368 time_slice: &TimeSliceID,
369 ) -> MoneyPerActivity {
370 self.get_revenue_from_flows_with_filter(prices, time_slice, |_| true)
371 }
372
373 pub fn get_revenue_from_flows_excluding_primary(
377 &self,
378 prices: &PriceMap,
379 time_slice: &TimeSliceID,
380 ) -> MoneyPerActivity {
381 let excluded_commodity = self.primary_output().map(|flow| &flow.commodity.id);
382
383 self.get_revenue_from_flows_with_filter(prices, time_slice, |flow| {
384 excluded_commodity.is_none_or(|commodity_id| commodity_id != &flow.commodity.id)
385 })
386 }
387
388 pub fn get_input_cost_from_prices(
392 &self,
393 prices: &PriceMap,
394 time_slice: &TimeSliceID,
395 ) -> MoneyPerActivity {
396 -self.get_revenue_from_flows_with_filter(prices, time_slice, |x| {
398 x.direction() == FlowDirection::Input
399 })
400 }
401
402 fn get_revenue_from_flows_with_filter<F>(
407 &self,
408 prices: &PriceMap,
409 time_slice: &TimeSliceID,
410 mut filter_for_flows: F,
411 ) -> MoneyPerActivity
412 where
413 F: FnMut(&ProcessFlow) -> bool,
414 {
415 self.iter_flows()
416 .filter(|flow| filter_for_flows(flow))
417 .map(|flow| {
418 flow.coeff
419 * prices
420 .get(&flow.commodity.id, &self.region_id, time_slice)
421 .unwrap_or(MoneyPerFlow(0.0))
422 })
423 .sum()
424 }
425
426 fn get_generic_activity_cost(
431 &self,
432 prices: &PriceMap,
433 year: u32,
434 time_slice: &TimeSliceID,
435 ) -> MoneyPerActivity {
436 let cost_of_inputs = self.get_input_cost_from_prices(prices, time_slice);
438
439 let excludes_sed_svd_output = |flow: &&ProcessFlow| {
441 !(flow.direction() == FlowDirection::Output
442 && matches!(
443 flow.commodity.kind,
444 CommodityType::SupplyEqualsDemand | CommodityType::ServiceDemand
445 ))
446 };
447 let flow_costs = self
448 .iter_flows()
449 .filter(excludes_sed_svd_output)
450 .map(|flow| flow.get_total_cost_per_activity(&self.region_id, year, time_slice))
451 .sum();
452
453 cost_of_inputs + flow_costs + self.process_parameter.variable_operating_cost
454 }
455
456 pub fn iter_marginal_costs_with_filter<'a>(
464 &'a self,
465 prices: &'a PriceMap,
466 year: u32,
467 time_slice: &'a TimeSliceID,
468 filter: impl Fn(&CommodityID) -> bool + 'a,
469 ) -> Box<dyn Iterator<Item = (CommodityID, MoneyPerFlow)> + 'a> {
470 let mut output_flows_iter = self
472 .iter_output_flows()
473 .filter(move |flow| filter(&flow.commodity.id))
474 .peekable();
475
476 if output_flows_iter.peek().is_none() {
478 return Box::new(std::iter::empty::<(CommodityID, MoneyPerFlow)>());
479 }
480
481 let generic_activity_cost = self.get_generic_activity_cost(prices, year, time_slice);
486
487 let total_output_per_activity = self.get_total_output_per_activity();
492 assert!(total_output_per_activity > FlowPerActivity::EPSILON); let generic_cost_per_flow = generic_activity_cost / total_output_per_activity;
494
495 Box::new(output_flows_iter.map(move |flow| {
497 let commodity_specific_costs_per_flow =
499 flow.get_total_cost_per_flow(&self.region_id, year, time_slice);
500
501 let marginal_cost = generic_cost_per_flow + commodity_specific_costs_per_flow;
503 (flow.commodity.id.clone(), marginal_cost)
504 }))
505 }
506
507 pub fn iter_marginal_costs<'a>(
511 &'a self,
512 prices: &'a PriceMap,
513 year: u32,
514 time_slice: &'a TimeSliceID,
515 ) -> Box<dyn Iterator<Item = (CommodityID, MoneyPerFlow)> + 'a> {
516 self.iter_marginal_costs_with_filter(prices, year, time_slice, move |_| true)
517 }
518
519 pub fn get_annual_capital_cost_per_capacity(&self) -> MoneyPerCapacity {
521 let capital_cost = self.process_parameter.capital_cost;
522 let lifetime = self.process_parameter.lifetime;
523 let discount_rate = self.process_parameter.discount_rate;
524 annual_capital_cost(capital_cost, lifetime, discount_rate)
525 }
526
527 pub fn get_annual_fixed_costs_per_activity(
532 &self,
533 annual_activity: Activity,
534 ) -> MoneyPerActivity {
535 let annual_capital_cost_per_capacity = self.get_annual_capital_cost_per_capacity();
536 let annual_fixed_opex = self.process_parameter.fixed_operating_cost * Year(1.0);
537 let total_annual_fixed_costs =
538 (annual_capital_cost_per_capacity + annual_fixed_opex) * self.total_capacity();
539 assert!(
540 annual_activity > Activity::EPSILON,
541 "Cannot calculate annual fixed costs per activity for an asset with zero annual activity"
542 );
543 total_annual_fixed_costs / annual_activity
544 }
545
546 pub fn get_annual_fixed_costs_per_flow(&self, annual_activity: Activity) -> MoneyPerFlow {
551 let annual_fixed_costs_per_activity =
552 self.get_annual_fixed_costs_per_activity(annual_activity);
553 let total_output_per_activity = self.get_total_output_per_activity();
554 assert!(total_output_per_activity > FlowPerActivity::EPSILON); annual_fixed_costs_per_activity / total_output_per_activity
556 }
557
558 pub fn max_activity(&self) -> Activity {
560 self.active_capacity() * self.process.capacity_to_activity
561 }
562
563 pub fn get_flow(&self, commodity_id: &CommodityID) -> Option<&ProcessFlow> {
565 self.flows.get(commodity_id)
566 }
567
568 pub fn iter_flows(&self) -> impl Iterator<Item = &ProcessFlow> {
570 self.flows.values()
571 }
572
573 pub fn iter_output_flows(&self) -> impl Iterator<Item = &ProcessFlow> {
575 self.flows.values().filter(|flow| {
576 flow.direction() == FlowDirection::Output
577 && matches!(
578 flow.commodity.kind,
579 CommodityType::SupplyEqualsDemand | CommodityType::ServiceDemand
580 )
581 })
582 }
583
584 pub fn primary_output(&self) -> Option<&ProcessFlow> {
586 self.process
587 .primary_output
588 .as_ref()
589 .map(|commodity_id| &self.flows[commodity_id])
590 }
591
592 pub fn primary_output_commodity(&self) -> Option<&CommodityID> {
594 self.process.primary_output.as_ref()
595 }
596
597 pub fn is_commissioned(&self) -> bool {
599 matches!(&self.state, AssetState::Commissioned { .. })
600 }
601
602 pub fn is_candidate(&self) -> bool {
604 matches!(&self.state, AssetState::Candidate)
605 }
606
607 pub fn commission_year(&self) -> u32 {
609 self.commission_year
610 }
611
612 pub fn region_id(&self) -> &RegionID {
614 &self.region_id
615 }
616
617 pub fn process(&self) -> &Process {
619 &self.process
620 }
621
622 pub fn process_id(&self) -> &ProcessID {
624 &self.process.id
625 }
626
627 pub fn is_dispatch_equivalent(&self, other: &Self) -> bool {
637 self.region_id == other.region_id
638 && self.process_parameter.variable_operating_cost
639 == other.process_parameter.variable_operating_cost
640 && (Arc::ptr_eq(&self.activity_limits, &other.activity_limits)
644 || self.activity_limits == other.activity_limits)
645 && (Arc::ptr_eq(&self.flows, &other.flows) || self.flows == other.flows)
646 }
647
648 pub(crate) fn dispatch_equivalence_hash(&self) -> u64 {
662 let mut hasher = DefaultHasher::new();
663
664 self.region_id.hash(&mut hasher);
666
667 hash_unit(self.process_parameter.variable_operating_cost).hash(&mut hasher);
669
670 let mut activity_limit_hashes = self
672 .activity_limits
673 .iter_limits()
674 .map(|(selection, limits)| {
675 let mut hasher = DefaultHasher::new();
676 selection.hash(&mut hasher);
677 hash_unit(*limits.start()).hash(&mut hasher);
678 hash_unit(*limits.end()).hash(&mut hasher);
679 hasher.finish()
680 })
681 .collect::<Vec<_>>();
682 activity_limit_hashes.sort_unstable();
683 activity_limit_hashes.hash(&mut hasher);
684
685 let mut flow_hashes = self
687 .flows
688 .values()
689 .map(|flow| {
690 let mut hasher = DefaultHasher::new();
691 flow.commodity.id.hash(&mut hasher);
692 hash_unit(flow.coeff).hash(&mut hasher);
693 std::mem::discriminant(&flow.kind).hash(&mut hasher);
694 hash_unit(flow.cost).hash(&mut hasher);
695 hasher.finish()
696 })
697 .collect::<Vec<_>>();
698 flow_hashes.sort_unstable();
699 flow_hashes.hash(&mut hasher);
700
701 hasher.finish()
702 }
703
704 pub fn id(&self) -> Option<AssetID> {
706 match &self.state {
707 AssetState::Commissioned { id, .. } => Some(*id),
708 _ => None,
709 }
710 }
711
712 pub fn agent_id(&self) -> Option<&AgentID> {
714 match &self.state {
715 AssetState::Commissioned { agent_id, .. } | AssetState::Ready { agent_id, .. } => {
716 Some(agent_id)
717 }
718 AssetState::Candidate => None,
719 }
720 }
721
722 pub fn capacity(&self) -> AssetCapacity {
724 self.capacity
725 }
726
727 pub fn total_capacity(&self) -> Capacity {
729 self.capacity().total_capacity()
730 }
731
732 pub fn set_capacity(&mut self, capacity: AssetCapacity) {
736 assert!(
737 capacity.total_capacity() >= Capacity(0.0),
738 "Capacity must be >= 0"
739 );
740 assert!(
741 self.get_num_mothballed_tranches() <= capacity.num_tranches(),
742 "Cannot set capacity to a smaller number of tranches than are currently mothballed"
743 );
744 self.capacity = capacity;
745 }
746
747 pub fn increase_capacity(&mut self, capacity: AssetCapacity) {
749 assert!(
750 capacity.total_capacity() > Capacity(0.0),
751 "Capacity increase must be positive"
752 );
753
754 self.capacity = self.capacity + capacity;
755 }
756
757 fn commission(&mut self, id: AssetID) {
766 let (agent_id, reason) = match &self.state {
767 AssetState::Ready {
768 agent_id,
769 commission_reason,
770 } => (agent_id, commission_reason),
771 state => panic!("Assets with state {state} cannot be commissioned"),
772 };
773 debug!(
774 "Commissioning '{}' asset (ID: {}, capacity: {}) for agent '{}' (reason: {})",
775 self.process_id(),
776 id,
777 self.total_capacity(),
778 agent_id,
779 reason
780 );
781 self.state = AssetState::Commissioned {
782 id,
783 agent_id: agent_id.clone(),
784 mothball_events: vec_deque![],
785 };
786 }
787
788 pub fn select_candidate_for_investment(&mut self, agent_id: AgentID) {
790 assert!(
791 self.is_candidate(),
792 "select_candidate_for_investment can only be called on Candidate assets"
793 );
794 self.state = AssetState::Ready {
795 agent_id,
796 commission_reason: "selected",
797 };
798 }
799
800 fn get_mothball_events(&self) -> Option<&VecDeque<MothballEvent>> {
802 match &self.state {
803 AssetState::Commissioned {
804 mothball_events, ..
805 } => Some(mothball_events),
806 _ => None,
807 }
808 }
809
810 fn get_mothball_events_mut(&mut self) -> Option<&mut VecDeque<MothballEvent>> {
812 match &mut self.state {
813 AssetState::Commissioned {
814 mothball_events, ..
815 } => Some(mothball_events),
816 _ => None,
817 }
818 }
819
820 pub fn has_any_mothballed_tranches(&self) -> bool {
822 self.get_mothball_events()
823 .is_some_and(|events| !events.is_empty())
824 }
825
826 pub fn get_num_mothballed_tranches(&self) -> u32 {
830 let Some(events) = self.get_mothball_events() else {
831 return 0;
832 };
833
834 events.iter().map(|event| event.num_tranches).sum()
835 }
836
837 pub fn mothballed_capacity(&self) -> Capacity {
839 self.capacity().tranche_size() * Dimensionless(self.get_num_mothballed_tranches() as f64)
840 }
841
842 pub fn get_num_nonmothballed_tranches(&self) -> u32 {
846 self.num_tranches() - self.get_num_mothballed_tranches()
847 }
848
849 pub fn active_capacity(&self) -> Capacity {
851 self.capacity().tranche_size() * Dimensionless(self.get_num_nonmothballed_tranches() as f64)
852 }
853
854 pub fn num_tranches(&self) -> u32 {
856 self.capacity().num_tranches()
857 }
858}
859
860#[allow(clippy::missing_fields_in_debug)]
861impl std::fmt::Debug for Asset {
862 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
863 f.debug_struct("Asset")
864 .field("state", &self.state)
865 .field("process_id", &self.process_id())
866 .field("region_id", &self.region_id)
867 .field("capacity", &self.total_capacity())
868 .field("commission_year", &self.commission_year)
869 .finish()
870 }
871}
872
873pub fn check_region_year_valid_for_process(
875 process: &Process,
876 region_id: &RegionID,
877 year: u32,
878) -> Result<()> {
879 ensure!(
880 process.regions.contains(region_id),
881 "Process {} does not operate in region {}",
882 process.id,
883 region_id
884 );
885 ensure!(
886 process.active_for_year(year),
887 "Process {} does not operate in the year {}",
888 process.id,
889 year
890 );
891 Ok(())
892}
893
894#[derive(Clone, Debug, PartialEq, derive_more::Deref, derive_more::Into)]
896pub struct UserAsset(#[deref(forward)] AssetRef);
897
898impl UserAsset {
899 pub fn new(
901 agent_id: AgentID,
902 process: Arc<Process>,
903 region_id: RegionID,
904 capacity: AssetCapacity,
905 commission_year: u32,
906 max_decommission_year: Option<u32>,
907 ) -> Result<Self> {
908 check_capacity_valid_for_asset(capacity.total_capacity())?;
909 let asset = Asset::new_with_state(
910 AssetState::Ready {
911 agent_id,
912 commission_reason: "user input",
913 },
914 process,
915 region_id,
916 capacity,
917 commission_year,
918 max_decommission_year,
919 )?;
920
921 Ok(Self(asset.into()))
922 }
923}
924
925#[cfg(test)]
926impl From<Asset> for UserAsset {
927 fn from(asset: Asset) -> Self {
928 assert!(
929 matches!(asset.state, AssetState::Ready { .. }),
930 "User assets must be in Ready state"
931 );
932 Self(asset.into())
933 }
934}
935
936pub fn check_capacity_valid_for_asset(capacity: Capacity) -> Result<()> {
938 ensure!(
939 capacity.is_finite() && capacity > Capacity(0.0),
940 "Capacity must be a finite, positive number"
941 );
942 Ok(())
943}
944
945fn log_decommissioning(asset: &Asset, num_tranches: u32, reason: &str) {
947 let (id, agent_id) = match &asset.state {
948 AssetState::Commissioned { id, agent_id, .. } => (*id, agent_id.clone()),
949 _ => panic!("Cannot decommission an asset that hasn't been commissioned"),
950 };
951 debug!(
952 "Decommissioning {}/{} tranches of '{}' asset (ID: {}) for agent '{}' (reason: {})",
953 num_tranches,
954 asset.num_tranches(),
955 asset.process_id(),
956 id,
957 agent_id,
958 reason
959 );
960}
961
962#[derive(Clone, Debug, derive_more::Deref, derive_more::From, derive_more::Into)]
968pub struct AssetRef(#[deref(forward)] Arc<Asset>);
969
970impl AssetRef {
971 pub fn make_mut(&mut self) -> &mut Asset {
973 Arc::make_mut(&mut self.0)
974 }
975
976 fn get_asset_cmp(&self) -> AssetCmp<'_> {
978 if let Some(id) = self.id() {
979 AssetCmp::WithID(id)
980 } else {
981 AssetCmp::WithoutID((
982 self.process_id(),
983 self.region_id(),
984 self.commission_year,
985 self.agent_id(),
986 ))
987 }
988 }
989
990 pub fn as_single_tranche(self) -> Self {
992 assert!(
993 self.num_tranches() > 0,
994 "Cannot convert an asset with zero tranches to a single tranche"
995 );
996 let tranche_size = self.capacity().tranche_size();
997 let mut asset = self.with_no_mothballed_tranches();
998 asset
999 .make_mut()
1000 .set_capacity(AssetCapacity::new(1, tranche_size));
1001 asset
1002 }
1003
1004 fn decommission(self, reason: &str) {
1006 log_decommissioning(&self, self.num_tranches(), reason);
1007 }
1008
1009 fn with_decommission_mothballed(self, year: u32, mothball_years: u32) -> Option<Self> {
1013 let events = self
1014 .get_mothball_events()
1015 .expect("Can only decommission mothballed tranches in commissioned assets");
1016
1017 let tranches_to_remove: u32 = events
1019 .iter()
1020 .take_while(|event| event.year <= year.saturating_sub(mothball_years))
1021 .map(|event| event.num_tranches)
1022 .sum();
1023 if tranches_to_remove == 0 {
1024 return Some(self);
1026 }
1027
1028 let reason = format!(
1029 "The asset has not been used for the set mothball years ({mothball_years} years)."
1030 );
1031 let new_num_tranches = self.num_tranches() - tranches_to_remove;
1032 if new_num_tranches == 0 {
1033 self.decommission(&reason);
1034 return None;
1035 }
1036
1037 log_decommissioning(&self, tranches_to_remove, &reason);
1038
1039 let tranche_size = self.capacity().tranche_size();
1042 let new_num_mothballed =
1043 new_num_tranches.saturating_sub(self.get_num_nonmothballed_tranches());
1044 let mut asset = self.with_mothballed_tranches(new_num_mothballed, None);
1045 asset
1046 .make_mut()
1047 .set_capacity(AssetCapacity::new(new_num_tranches, tranche_size));
1048 Some(asset)
1049 }
1050
1051 pub fn with_mothballed_tranches(mut self, num_tranches: u32, year: Option<u32>) -> Self {
1061 if num_tranches == 0 {
1062 return self.with_no_mothballed_tranches();
1064 }
1065
1066 let num_already_mothballed = self.get_num_mothballed_tranches();
1067 if num_tranches == num_already_mothballed {
1068 return self;
1070 }
1071
1072 assert!(
1073 num_tranches <= self.num_tranches(),
1074 "Cannot mothball more tranches than asset represents"
1075 );
1076
1077 let events = self.make_mut().get_mothball_events_mut().expect(
1079 "Cannot change number of mothballed tranches for an asset that hasn't been commissioned",
1080 );
1081 if num_tranches < num_already_mothballed {
1082 let mut remaining = num_already_mothballed - num_tranches;
1085 while remaining > 0
1086 && let Some(event) = events.front_mut()
1087 {
1088 let to_remove = event.num_tranches.min(remaining);
1089 event.num_tranches -= to_remove;
1090 remaining -= to_remove;
1091 if event.num_tranches == 0 {
1092 events.pop_front();
1093 }
1094 }
1095 } else {
1096 let year =
1097 year.expect("Cannot increase number of mothballed tranches without supplying year");
1098
1099 if let Some(event) = events.back() {
1100 assert!(
1103 event.year <= year,
1104 "Attempting to mothball tranches in a year in the past"
1105 );
1106 }
1107
1108 events.push_back(MothballEvent {
1110 year,
1111 num_tranches: num_tranches - num_already_mothballed,
1112 });
1113 }
1114
1115 self
1116 }
1117
1118 pub fn with_no_mothballed_tranches(mut self) -> Self {
1122 if self.has_any_mothballed_tranches() {
1123 self.make_mut().get_mothball_events_mut().unwrap().clear();
1125 }
1126
1127 self
1128 }
1129}
1130
1131impl From<Asset> for AssetRef {
1132 fn from(value: Asset) -> Self {
1133 Self::from(Arc::new(value))
1134 }
1135}
1136
1137impl Eq for AssetRef {}
1138
1139impl PartialEq for AssetRef {
1140 fn eq(&self, other: &Self) -> bool {
1141 self.get_asset_cmp() == other.get_asset_cmp()
1142 }
1143}
1144
1145impl Ord for AssetRef {
1146 fn cmp(&self, other: &Self) -> Ordering {
1147 self.get_asset_cmp().cmp(&other.get_asset_cmp())
1148 }
1149}
1150
1151impl PartialOrd for AssetRef {
1152 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1153 Some(self.cmp(other))
1154 }
1155}
1156
1157impl Hash for AssetRef {
1158 fn hash<H: Hasher>(&self, state: &mut H) {
1159 self.get_asset_cmp().hash(state);
1160 }
1161}
1162
1163#[derive(PartialEq, PartialOrd, Eq, Ord, Hash)]
1169enum AssetCmp<'a> {
1170 WithID(AssetID),
1171 WithoutID((&'a ProcessID, &'a RegionID, u32, Option<&'a AgentID>)),
1172}
1173
1174pub trait AssetIterator<'a>: Iterator<Item = &'a AssetRef> + Sized
1176where
1177 Self: 'a,
1178{
1179 fn filter_agent(self, agent_id: &'a AgentID) -> impl Iterator<Item = &'a AssetRef> + 'a {
1181 self.filter(move |asset| asset.agent_id() == Some(agent_id))
1182 }
1183
1184 fn filter_primary_producers_of(
1186 self,
1187 commodity_id: &'a CommodityID,
1188 ) -> impl Iterator<Item = &'a AssetRef> + 'a {
1189 self.filter(move |asset| {
1190 asset
1191 .primary_output()
1192 .is_some_and(|flow| &flow.commodity.id == commodity_id)
1193 })
1194 }
1195
1196 fn filter_region(self, region_id: &'a RegionID) -> impl Iterator<Item = &'a AssetRef> + 'a {
1198 self.filter(move |asset| asset.region_id == *region_id)
1199 }
1200
1201 fn flows_for_commodity(
1203 self,
1204 commodity_id: &'a CommodityID,
1205 ) -> impl Iterator<Item = (&'a AssetRef, &'a ProcessFlow)> + 'a {
1206 self.filter_map(|asset| Some((asset, asset.get_flow(commodity_id)?)))
1207 }
1208}
1209
1210impl<'a, I> AssetIterator<'a> for I where I: Iterator<Item = &'a AssetRef> + Sized + 'a {}
1211
1212#[cfg(test)]
1213mod tests {
1214 use super::*;
1215 use crate::commodity::Commodity;
1216 use crate::fixture::{
1217 agent_id, assert_error, assert_patched_runs_ok_simple, assert_validate_fails_with_simple,
1218 asset, multi_tranche_asset, process, process_activity_limits_map, process_flows_map,
1219 region_id, svd_commodity, time_slice, time_slice_info,
1220 };
1221 use crate::patch::FilePatch;
1222 use crate::process::{FlowType, Process, ProcessFlow};
1223 use crate::region::RegionID;
1224 use crate::time_slice::{TimeSliceID, TimeSliceInfo};
1225 use crate::units::{
1226 ActivityPerCapacity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity,
1227 MoneyPerFlow,
1228 };
1229 use float_cmp::assert_approx_eq;
1230 use indexmap::indexmap;
1231 use itertools::assert_equal;
1232 use rstest::{fixture, rstest};
1233 use std::sync::Arc;
1234
1235 #[fixture]
1237 fn commissioned_multi_tranche(mut multi_tranche_asset: Asset) -> AssetRef {
1238 multi_tranche_asset.commission(AssetID(0));
1239 assert_eq!(multi_tranche_asset.num_tranches(), 3);
1240 AssetRef::from(multi_tranche_asset)
1241 }
1242
1243 #[rstest]
1244 fn get_input_cost_from_prices_works(
1245 region_id: RegionID,
1246 svd_commodity: Commodity,
1247 mut process: Process,
1248 time_slice: TimeSliceID,
1249 ) {
1250 let commodity_rc = Arc::new(svd_commodity);
1252 let process_flow = ProcessFlow {
1253 commodity: Arc::clone(&commodity_rc),
1254 coeff: FlowPerActivity(-2.0), kind: FlowType::Fixed,
1256 cost: MoneyPerFlow(0.0),
1257 };
1258 let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() };
1259 let process_flows_map = process_flows_map(process.regions.clone(), Arc::new(process_flows));
1260 process.flows = process_flows_map;
1261
1262 let asset = Asset::new_candidate(Arc::new(process), region_id.clone(), Capacity(1.0), 2020)
1264 .unwrap();
1265
1266 let mut input_prices = PriceMap::default();
1268 input_prices.insert(&commodity_rc.id, ®ion_id, &time_slice, MoneyPerFlow(3.0));
1269
1270 let cost = asset.get_input_cost_from_prices(&input_prices, &time_slice);
1272 assert_approx_eq!(MoneyPerActivity, cost, MoneyPerActivity(6.0));
1274 }
1275
1276 #[rstest]
1277 fn dispatch_equivalence_ignores_capacity(asset: Asset) {
1278 let mut other = asset.clone();
1279 other.set_capacity(AssetCapacity::single(Capacity(3.0)));
1280
1281 assert!(asset.is_dispatch_equivalent(&other));
1282 assert_eq!(
1283 asset.dispatch_equivalence_hash(),
1284 other.dispatch_equivalence_hash()
1285 );
1286 }
1287
1288 #[rstest]
1289 fn dispatch_equivalence_fails_for_different_region(asset: Asset) {
1290 let mut other = asset.clone();
1291 other.region_id = "FRA".into();
1292
1293 assert!(!asset.is_dispatch_equivalent(&other));
1294 }
1295
1296 #[rstest]
1297 fn dispatch_equivalence_fails_for_different_variable_operating_cost(asset: Asset) {
1298 let mut other = asset.clone();
1299 Arc::make_mut(&mut other.process_parameter).variable_operating_cost = MoneyPerActivity(1.0);
1300
1301 assert!(!asset.is_dispatch_equivalent(&other));
1302 }
1303
1304 #[rstest]
1305 fn dispatch_equivalence_fails_for_different_activity_limits(
1306 asset: Asset,
1307 asset_with_activity_limits: Asset,
1308 ) {
1309 assert!(!asset.is_dispatch_equivalent(&asset_with_activity_limits));
1310 }
1311
1312 #[rstest]
1313 fn dispatch_equivalence_fails_for_different_flows(asset: Asset, svd_commodity: Commodity) {
1314 let commodity = Arc::new(svd_commodity);
1315 let flow = ProcessFlow {
1316 commodity: Arc::clone(&commodity),
1317 coeff: FlowPerActivity(1.0),
1318 kind: FlowType::Fixed,
1319 cost: MoneyPerFlow(0.0),
1320 };
1321
1322 let mut other = asset.clone();
1323 other.flows = Arc::new(indexmap! { commodity.id.clone() => flow });
1324
1325 assert!(!asset.is_dispatch_equivalent(&other));
1326 }
1327
1328 #[rstest]
1329 fn dispatch_equivalence_handles_separately_allocated_values(asset: Asset) {
1330 let mut other = asset.clone();
1331 other.activity_limits = Arc::new((*asset.activity_limits).clone());
1332 other.flows = Arc::new((*asset.flows).clone());
1333
1334 assert!(asset.is_dispatch_equivalent(&other));
1335 assert_eq!(
1336 asset.dispatch_equivalence_hash(),
1337 other.dispatch_equivalence_hash()
1338 );
1339 }
1340
1341 #[rstest]
1342 fn dispatch_equivalence_ignores_state(asset: Asset) {
1343 let mut other = asset.clone();
1344 other.commission(AssetID(1));
1345
1346 assert!(asset.is_dispatch_equivalent(&other));
1347 assert_eq!(
1348 asset.dispatch_equivalence_hash(),
1349 other.dispatch_equivalence_hash()
1350 );
1351 }
1352
1353 #[rstest]
1354 fn dispatch_equivalence_is_reflexive(asset: Asset) {
1355 assert!(asset.is_dispatch_equivalent(&asset));
1356 assert_eq!(
1357 asset.dispatch_equivalence_hash(),
1358 asset.dispatch_equivalence_hash()
1359 );
1360 }
1361
1362 #[fixture]
1363 fn process_with_activity_limits(
1364 mut process: Process,
1365 time_slice_info: TimeSliceInfo,
1366 time_slice: TimeSliceID,
1367 ) -> Process {
1368 let mut activity_limits = ActivityLimits::new_with_full_availability(&time_slice_info);
1370 activity_limits.add_time_slice_limit(time_slice, Dimensionless(0.1)..=Dimensionless(0.5));
1371 process.activity_limits =
1372 process_activity_limits_map(process.regions.clone(), activity_limits);
1373
1374 process.capacity_to_activity = ActivityPerCapacity(2.0);
1376 process
1377 }
1378
1379 #[fixture]
1380 fn asset_with_activity_limits(process_with_activity_limits: Process) -> Asset {
1381 Asset::new_ready(
1382 "agent1".into(),
1383 Arc::new(process_with_activity_limits),
1384 "GBR".into(),
1385 AssetCapacity::single(Capacity(2.0)),
1386 2010,
1387 )
1388 .unwrap()
1389 }
1390
1391 #[rstest]
1392 fn asset_get_activity_per_capacity_limits(
1393 asset_with_activity_limits: Asset,
1394 time_slice: TimeSliceID,
1395 ) {
1396 assert_eq!(
1398 asset_with_activity_limits.get_activity_per_capacity_limits(&time_slice),
1399 ActivityPerCapacity(0.2)..=ActivityPerCapacity(1.0)
1400 );
1401 }
1402
1403 #[rstest]
1404 #[case(Capacity(0.01))]
1405 #[case(Capacity(0.5))]
1406 #[case(Capacity(1.0))]
1407 #[case(Capacity(100.0))]
1408 fn user_asset_new_valid(
1409 agent_id: AgentID,
1410 process: Process,
1411 region_id: RegionID,
1412 #[case] capacity: Capacity,
1413 ) {
1414 let asset_capacity = AssetCapacity::single(capacity);
1415 let asset = UserAsset::new(
1416 agent_id,
1417 process.into(),
1418 region_id,
1419 asset_capacity,
1420 2015,
1421 None,
1422 )
1423 .unwrap();
1424 assert!(asset.id().is_none());
1425 }
1426
1427 #[rstest]
1428 #[case(Capacity(0.0))]
1429 fn user_asset_new_zero_capacity(
1430 agent_id: AgentID,
1431 process: Process,
1432 region_id: RegionID,
1433 #[case] capacity: Capacity,
1434 ) {
1435 let asset_capacity = AssetCapacity::single(capacity);
1438 assert_error!(
1439 UserAsset::new(
1440 agent_id,
1441 process.into(),
1442 region_id,
1443 asset_capacity,
1444 2015,
1445 None
1446 ),
1447 "Capacity must be a finite, positive number"
1448 );
1449 }
1450
1451 #[rstest]
1452 fn user_asset_new_invalid_commission_year(
1453 agent_id: AgentID,
1454 process: Process,
1455 region_id: RegionID,
1456 ) {
1457 assert_error!(
1458 UserAsset::new(
1459 agent_id,
1460 process.into(),
1461 region_id,
1462 AssetCapacity::single(Capacity(1.0)),
1463 2007,
1464 None
1465 ),
1466 "Process process1 does not operate in the year 2007"
1467 );
1468 }
1469
1470 #[rstest]
1471 fn user_asset_new_invalid_region(agent_id: AgentID, process: Process) {
1472 let region_id = RegionID("FRA".into());
1473 assert_error!(
1474 UserAsset::new(
1475 agent_id,
1476 process.into(),
1477 region_id,
1478 AssetCapacity::single(Capacity(1.0)),
1479 2015,
1480 None
1481 ),
1482 "Process process1 does not operate in region FRA"
1483 );
1484 }
1485
1486 #[rstest]
1487 fn asset_commission(process: Process) {
1488 let mut asset = Asset::new_ready(
1490 "agent1".into(),
1491 process.into(),
1492 "GBR".into(),
1493 AssetCapacity::single(Capacity(1.0)),
1494 2020,
1495 )
1496 .unwrap();
1497 asset.commission(AssetID(2));
1498 assert!(asset.is_commissioned());
1499 assert_eq!(asset.id(), Some(AssetID(2)));
1500 }
1501
1502 #[rstest]
1503 #[should_panic(expected = "Assets with state Candidate cannot be commissioned")]
1504 fn commission_wrong_states(process: Process) {
1505 let mut asset =
1506 Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap();
1507 asset.commission(AssetID(1));
1508 }
1509
1510 #[test]
1511 fn commission_year_before_time_horizon() {
1512 let processes_patch = FilePatch::new("processes.csv")
1513 .with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
1514 .with_addition("GASDRV,Dry gas extraction,all,GASPRD,1980,2040,1.0,");
1515
1516 let patches = vec![
1519 processes_patch.clone(),
1520 FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,1980"),
1521 ];
1522 assert_patched_runs_ok_simple!(patches);
1523
1524 let patches = vec![
1526 processes_patch,
1527 FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,1970"),
1528 ];
1529 assert_validate_fails_with_simple!(
1530 patches,
1531 "Agent A0_GEX has asset with commission year 1970, not within process GASDRV commission years: 1980..=2040"
1532 );
1533 }
1534
1535 #[test]
1536 fn commission_year_after_time_horizon() {
1537 let processes_patch = FilePatch::new("processes.csv")
1538 .with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
1539 .with_addition("GASDRV,Dry gas extraction,all,GASPRD,2020,2050,1.0,");
1540
1541 let patches = vec![
1543 processes_patch.clone(),
1544 FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,2050"),
1545 ];
1546 assert_patched_runs_ok_simple!(patches);
1547
1548 let patches = vec![
1550 processes_patch,
1551 FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,2060"),
1552 ];
1553 assert_validate_fails_with_simple!(
1554 patches,
1555 "Agent A0_GEX has asset with commission year 2060, not within process GASDRV commission years: 2020..=2050"
1556 );
1557 }
1558
1559 #[rstest]
1560 #[case::none(0)]
1561 #[case::some(2)]
1562 #[case::all(3)]
1563 fn mothball_tranche_counts(commissioned_multi_tranche: AssetRef, #[case] num_mothballed: u32) {
1564 assert_eq!(commissioned_multi_tranche.num_tranches(), 3);
1565 let asset = commissioned_multi_tranche.with_mothballed_tranches(num_mothballed, Some(2020));
1566 assert_eq!(asset.get_num_mothballed_tranches(), num_mothballed);
1567 assert_eq!(
1568 asset.mothballed_capacity(),
1569 Capacity(4.0 * num_mothballed as f64)
1570 );
1571 assert_eq!(asset.get_num_nonmothballed_tranches(), 3 - num_mothballed);
1572 assert_eq!(asset.has_any_mothballed_tranches(), num_mothballed > 0);
1573 }
1574
1575 #[rstest]
1576 fn mothball_counts_non_commissioned(asset: Asset, process: Process) {
1577 let ready = AssetRef::from(asset);
1579 let candidate = AssetRef::from(
1580 Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap(),
1581 );
1582 for asset in [ready, candidate] {
1583 assert!(!asset.has_any_mothballed_tranches());
1584 assert_eq!(asset.get_num_mothballed_tranches(), 0);
1585 assert_eq!(asset.get_num_nonmothballed_tranches(), asset.num_tranches());
1586 }
1587 }
1588
1589 #[rstest]
1590 fn with_mothballed_tranches_accumulates_events(commissioned_multi_tranche: AssetRef) {
1591 let asset = commissioned_multi_tranche.with_mothballed_tranches(1, Some(2020));
1593 assert_equal(
1594 asset.get_mothball_events().unwrap().iter(),
1595 &[MothballEvent {
1596 year: 2020,
1597 num_tranches: 1,
1598 }],
1599 );
1600
1601 let asset = asset.with_mothballed_tranches(2, Some(2022));
1603 assert_equal(
1604 asset.get_mothball_events().unwrap().iter(),
1605 &[
1606 MothballEvent {
1607 year: 2020,
1608 num_tranches: 1,
1609 },
1610 MothballEvent {
1611 year: 2022,
1612 num_tranches: 1,
1613 },
1614 ],
1615 );
1616 }
1617
1618 #[rstest]
1619 fn with_mothballed_tranches_decrease_removes_oldest_first(
1620 commissioned_multi_tranche: AssetRef,
1621 ) {
1622 let asset = commissioned_multi_tranche
1624 .with_mothballed_tranches(1, Some(2020))
1625 .with_mothballed_tranches(3, Some(2022));
1626 assert_equal(
1627 asset.get_mothball_events().unwrap().iter(),
1628 &[
1629 MothballEvent {
1630 year: 2020,
1631 num_tranches: 1,
1632 },
1633 MothballEvent {
1634 year: 2022,
1635 num_tranches: 2,
1636 },
1637 ],
1638 );
1639
1640 let asset = asset.with_mothballed_tranches(1, None);
1643 assert_eq!(asset.get_num_mothballed_tranches(), 1);
1644 assert_equal(
1645 asset.get_mothball_events().unwrap().iter(),
1646 &[MothballEvent {
1647 year: 2022,
1648 num_tranches: 1,
1649 }],
1650 );
1651 }
1652
1653 #[rstest]
1654 fn with_mothballed_tranches_noop_returns_same_rc(commissioned_multi_tranche: AssetRef) {
1655 let asset = commissioned_multi_tranche.with_mothballed_tranches(2, Some(2020));
1656 let same = asset.clone().with_mothballed_tranches(2, Some(2099));
1658 assert!(Arc::ptr_eq(&asset.0, &same.0));
1659 }
1660
1661 #[rstest]
1662 fn with_mothballed_tranches_zero_unmothballs(commissioned_multi_tranche: AssetRef) {
1663 let asset = commissioned_multi_tranche.with_mothballed_tranches(2, Some(2020));
1664 assert!(asset.has_any_mothballed_tranches());
1665
1666 let asset = asset.with_mothballed_tranches(0, None);
1667 assert!(!asset.has_any_mothballed_tranches());
1668 assert_eq!(asset.get_num_mothballed_tranches(), 0);
1669 }
1670
1671 #[rstest]
1672 #[should_panic(expected = "Cannot mothball more tranches than asset represents")]
1673 fn with_mothballed_tranches_panics_for_too_many_tranches(commissioned_multi_tranche: AssetRef) {
1674 commissioned_multi_tranche.with_mothballed_tranches(4, Some(2020));
1675 }
1676
1677 #[rstest]
1678 #[should_panic(
1679 expected = "Cannot change number of mothballed tranches for an asset that hasn't been commissioned"
1680 )]
1681 fn with_mothballed_tranches_panics_for_non_commissioned(asset: Asset) {
1682 AssetRef::from(asset).with_mothballed_tranches(1, Some(2020));
1683 }
1684
1685 #[rstest]
1686 #[should_panic(
1687 expected = "Cannot increase number of mothballed tranches without supplying year"
1688 )]
1689 fn with_mothballed_tranches_panics_when_increasing_without_year(
1690 commissioned_multi_tranche: AssetRef,
1691 ) {
1692 commissioned_multi_tranche.with_mothballed_tranches(1, None);
1693 }
1694
1695 #[rstest]
1696 #[should_panic(expected = "Attempting to mothball tranches in a year in the past")]
1697 fn with_mothballed_tranches_panics_when_mothballing_in_the_past(
1698 commissioned_multi_tranche: AssetRef,
1699 ) {
1700 commissioned_multi_tranche
1703 .with_mothballed_tranches(1, Some(2020))
1704 .with_mothballed_tranches(2, Some(2019));
1705 }
1706
1707 #[rstest]
1708 fn with_no_mothballed_tranches_clears_events(commissioned_multi_tranche: AssetRef) {
1709 let asset = commissioned_multi_tranche.with_mothballed_tranches(2, Some(2020));
1710 let asset = asset.with_no_mothballed_tranches();
1711 assert!(!asset.has_any_mothballed_tranches());
1712 assert_eq!(asset.get_num_mothballed_tranches(), 0);
1713 }
1714
1715 #[rstest]
1716 fn with_no_mothballed_tranches_noop_returns_same_rc(commissioned_multi_tranche: AssetRef) {
1717 let asset = commissioned_multi_tranche;
1719 let same = asset.clone().with_no_mothballed_tranches();
1720 assert!(Arc::ptr_eq(&asset.0, &same.0));
1721 }
1722
1723 #[rstest]
1724 fn with_decommission_mothballed_nothing_old_enough(commissioned_multi_tranche: AssetRef) {
1725 let asset = commissioned_multi_tranche.with_mothballed_tranches(1, Some(2020));
1726 let result = asset
1728 .clone()
1729 .with_decommission_mothballed(2025, 20)
1730 .unwrap();
1731 assert!(Arc::ptr_eq(&asset.0, &result.0));
1732 }
1733
1734 #[rstest]
1735 fn with_decommission_mothballed_partial(commissioned_multi_tranche: AssetRef) {
1736 let asset = commissioned_multi_tranche
1738 .with_mothballed_tranches(1, Some(2010))
1739 .with_mothballed_tranches(2, Some(2020));
1740
1741 let result = asset
1743 .clone()
1744 .with_decommission_mothballed(2025, 10)
1745 .unwrap();
1746 assert_eq!(result.num_tranches(), 2);
1747 assert_eq!(result.capacity(), AssetCapacity::new(2, Capacity(4.0)));
1748 assert_eq!(result.id(), asset.id());
1749 assert_eq!(result.agent_id(), asset.agent_id());
1750 assert_eq!(result.get_num_mothballed_tranches(), 1);
1751 assert_equal(
1752 result.get_mothball_events().unwrap().iter(),
1753 &[MothballEvent {
1754 year: 2020,
1755 num_tranches: 1,
1756 }],
1757 );
1758 }
1759
1760 #[rstest]
1761 fn with_decommission_mothballed_all(commissioned_multi_tranche: AssetRef) {
1762 let asset = commissioned_multi_tranche.with_mothballed_tranches(3, Some(2010));
1764 assert!(asset.with_decommission_mothballed(2025, 10).is_none());
1765 }
1766}