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.total_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.total_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 num_tranches(&self) -> u32 {
851 self.capacity().num_tranches()
852 }
853}
854
855#[allow(clippy::missing_fields_in_debug)]
856impl std::fmt::Debug for Asset {
857 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
858 f.debug_struct("Asset")
859 .field("state", &self.state)
860 .field("process_id", &self.process_id())
861 .field("region_id", &self.region_id)
862 .field("capacity", &self.total_capacity())
863 .field("commission_year", &self.commission_year)
864 .finish()
865 }
866}
867
868pub fn check_region_year_valid_for_process(
870 process: &Process,
871 region_id: &RegionID,
872 year: u32,
873) -> Result<()> {
874 ensure!(
875 process.regions.contains(region_id),
876 "Process {} does not operate in region {}",
877 process.id,
878 region_id
879 );
880 ensure!(
881 process.active_for_year(year),
882 "Process {} does not operate in the year {}",
883 process.id,
884 year
885 );
886 Ok(())
887}
888
889#[derive(Clone, Debug, PartialEq, derive_more::Deref, derive_more::Into)]
891pub struct UserAsset(#[deref(forward)] AssetRef);
892
893impl UserAsset {
894 pub fn new(
896 agent_id: AgentID,
897 process: Arc<Process>,
898 region_id: RegionID,
899 capacity: AssetCapacity,
900 commission_year: u32,
901 max_decommission_year: Option<u32>,
902 ) -> Result<Self> {
903 check_capacity_valid_for_asset(capacity.total_capacity())?;
904 let asset = Asset::new_with_state(
905 AssetState::Ready {
906 agent_id,
907 commission_reason: "user input",
908 },
909 process,
910 region_id,
911 capacity,
912 commission_year,
913 max_decommission_year,
914 )?;
915
916 Ok(Self(asset.into()))
917 }
918}
919
920#[cfg(test)]
921impl From<Asset> for UserAsset {
922 fn from(asset: Asset) -> Self {
923 assert!(
924 matches!(asset.state, AssetState::Ready { .. }),
925 "User assets must be in Ready state"
926 );
927 Self(asset.into())
928 }
929}
930
931pub fn check_capacity_valid_for_asset(capacity: Capacity) -> Result<()> {
933 ensure!(
934 capacity.is_finite() && capacity > Capacity(0.0),
935 "Capacity must be a finite, positive number"
936 );
937 Ok(())
938}
939
940fn log_decommissioning(asset: &Asset, num_tranches: u32, reason: &str) {
942 let (id, agent_id) = match &asset.state {
943 AssetState::Commissioned { id, agent_id, .. } => (*id, agent_id.clone()),
944 _ => panic!("Cannot decommission an asset that hasn't been commissioned"),
945 };
946 debug!(
947 "Decommissioning {}/{} tranches of '{}' asset (ID: {}) for agent '{}' (reason: {})",
948 num_tranches,
949 asset.num_tranches(),
950 asset.process_id(),
951 id,
952 agent_id,
953 reason
954 );
955}
956
957#[derive(Clone, Debug, derive_more::Deref, derive_more::From, derive_more::Into)]
963pub struct AssetRef(#[deref(forward)] Arc<Asset>);
964
965impl AssetRef {
966 pub fn make_mut(&mut self) -> &mut Asset {
968 Arc::make_mut(&mut self.0)
969 }
970
971 fn get_asset_cmp(&self) -> AssetCmp<'_> {
973 if let Some(id) = self.id() {
974 AssetCmp::WithID(id)
975 } else {
976 AssetCmp::WithoutID((
977 self.process_id(),
978 self.region_id(),
979 self.commission_year,
980 self.agent_id(),
981 ))
982 }
983 }
984
985 pub fn with_subset_of_tranches(self, new_num_tranches: u32) -> Self {
995 if new_num_tranches == self.num_tranches() {
996 return self;
997 }
998
999 assert!(
1000 new_num_tranches > 0,
1001 "Cannot make an asset with zero tranches"
1002 );
1003
1004 let max_num_tranches = self.capacity().num_tranches();
1005 let tranche_size = self.capacity().tranche_size();
1006
1007 assert!(
1008 new_num_tranches <= max_num_tranches,
1009 "Cannot make an asset with more tranches than original"
1010 );
1011
1012 let new_num_mothballed =
1017 new_num_tranches.saturating_sub(self.get_num_nonmothballed_tranches());
1018 let mut asset = self.with_mothballed_tranches(new_num_mothballed, None);
1019 asset
1020 .make_mut()
1021 .set_capacity(AssetCapacity::new(new_num_tranches, tranche_size));
1022 asset
1023 }
1024
1025 pub fn as_single_tranche(self) -> Self {
1027 let new_num_tranches = 1;
1028 let tranche_size = self.capacity().tranche_size();
1029 let mut asset = self.with_no_mothballed_tranches();
1030 asset
1031 .make_mut()
1032 .set_capacity(AssetCapacity::new(new_num_tranches, tranche_size));
1033 asset
1034 }
1035
1036 fn decommission(self, reason: &str) {
1038 log_decommissioning(&self, self.num_tranches(), reason);
1039 }
1040
1041 fn with_decommission_mothballed(self, year: u32, mothball_years: u32) -> Option<Self> {
1045 let events = self
1046 .get_mothball_events()
1047 .expect("Can only decommission mothballed tranches in commissioned assets");
1048
1049 let tranches_to_remove: u32 = events
1051 .iter()
1052 .take_while(|event| event.year <= year.saturating_sub(mothball_years))
1053 .map(|event| event.num_tranches)
1054 .sum();
1055 if tranches_to_remove == 0 {
1056 return Some(self);
1058 }
1059
1060 let reason = format!(
1061 "The asset has not been used for the set mothball years ({mothball_years} years)."
1062 );
1063 let new_num_tranches = self.num_tranches() - tranches_to_remove;
1064 if new_num_tranches == 0 {
1065 self.decommission(&reason);
1066 return None;
1067 }
1068
1069 log_decommissioning(&self, tranches_to_remove, &reason);
1072 Some(self.with_subset_of_tranches(new_num_tranches))
1073 }
1074
1075 pub fn with_mothballed_tranches(mut self, num_tranches: u32, year: Option<u32>) -> Self {
1085 if num_tranches == 0 {
1086 return self.with_no_mothballed_tranches();
1088 }
1089
1090 let num_already_mothballed = self.get_num_mothballed_tranches();
1091 if num_tranches == num_already_mothballed {
1092 return self;
1094 }
1095
1096 assert!(
1097 num_tranches <= self.num_tranches(),
1098 "Cannot mothball more tranches than asset represents"
1099 );
1100
1101 let events = self.make_mut().get_mothball_events_mut().expect(
1103 "Cannot change number of mothballed tranches for an asset that hasn't been commissioned",
1104 );
1105 if num_tranches < num_already_mothballed {
1106 let mut remaining = num_already_mothballed - num_tranches;
1109 while remaining > 0
1110 && let Some(event) = events.front_mut()
1111 {
1112 let to_remove = event.num_tranches.min(remaining);
1113 event.num_tranches -= to_remove;
1114 remaining -= to_remove;
1115 if event.num_tranches == 0 {
1116 events.pop_front();
1117 }
1118 }
1119 } else {
1120 let year =
1121 year.expect("Cannot increase number of mothballed tranches without supplying year");
1122
1123 if let Some(event) = events.back() {
1124 assert!(
1127 event.year <= year,
1128 "Attempting to mothball tranches in a year in the past"
1129 );
1130 }
1131
1132 events.push_back(MothballEvent {
1134 year,
1135 num_tranches: num_tranches - num_already_mothballed,
1136 });
1137 }
1138
1139 self
1140 }
1141
1142 pub fn with_no_mothballed_tranches(mut self) -> Self {
1146 if self.has_any_mothballed_tranches() {
1147 self.make_mut().get_mothball_events_mut().unwrap().clear();
1149 }
1150
1151 self
1152 }
1153}
1154
1155impl From<Asset> for AssetRef {
1156 fn from(value: Asset) -> Self {
1157 Self::from(Arc::new(value))
1158 }
1159}
1160
1161impl Eq for AssetRef {}
1162
1163impl PartialEq for AssetRef {
1164 fn eq(&self, other: &Self) -> bool {
1165 self.get_asset_cmp() == other.get_asset_cmp()
1166 }
1167}
1168
1169impl Ord for AssetRef {
1170 fn cmp(&self, other: &Self) -> Ordering {
1171 self.get_asset_cmp().cmp(&other.get_asset_cmp())
1172 }
1173}
1174
1175impl PartialOrd for AssetRef {
1176 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1177 Some(self.cmp(other))
1178 }
1179}
1180
1181impl Hash for AssetRef {
1182 fn hash<H: Hasher>(&self, state: &mut H) {
1183 self.get_asset_cmp().hash(state);
1184 }
1185}
1186
1187#[derive(PartialEq, PartialOrd, Eq, Ord, Hash)]
1193enum AssetCmp<'a> {
1194 WithID(AssetID),
1195 WithoutID((&'a ProcessID, &'a RegionID, u32, Option<&'a AgentID>)),
1196}
1197
1198pub trait AssetIterator<'a>: Iterator<Item = &'a AssetRef> + Sized
1200where
1201 Self: 'a,
1202{
1203 fn filter_agent(self, agent_id: &'a AgentID) -> impl Iterator<Item = &'a AssetRef> + 'a {
1205 self.filter(move |asset| asset.agent_id() == Some(agent_id))
1206 }
1207
1208 fn filter_primary_producers_of(
1210 self,
1211 commodity_id: &'a CommodityID,
1212 ) -> impl Iterator<Item = &'a AssetRef> + 'a {
1213 self.filter(move |asset| {
1214 asset
1215 .primary_output()
1216 .is_some_and(|flow| &flow.commodity.id == commodity_id)
1217 })
1218 }
1219
1220 fn filter_region(self, region_id: &'a RegionID) -> impl Iterator<Item = &'a AssetRef> + 'a {
1222 self.filter(move |asset| asset.region_id == *region_id)
1223 }
1224
1225 fn flows_for_commodity(
1227 self,
1228 commodity_id: &'a CommodityID,
1229 ) -> impl Iterator<Item = (&'a AssetRef, &'a ProcessFlow)> + 'a {
1230 self.filter_map(|asset| Some((asset, asset.get_flow(commodity_id)?)))
1231 }
1232}
1233
1234impl<'a, I> AssetIterator<'a> for I where I: Iterator<Item = &'a AssetRef> + Sized + 'a {}
1235
1236#[cfg(test)]
1237mod tests {
1238 use super::*;
1239 use crate::commodity::Commodity;
1240 use crate::fixture::{
1241 agent_id, assert_error, assert_patched_runs_ok_simple, assert_validate_fails_with_simple,
1242 asset, multi_tranche_asset, process, process_activity_limits_map, process_flows_map,
1243 region_id, svd_commodity, time_slice, time_slice_info,
1244 };
1245 use crate::patch::FilePatch;
1246 use crate::process::{FlowType, Process, ProcessFlow};
1247 use crate::region::RegionID;
1248 use crate::time_slice::{TimeSliceID, TimeSliceInfo};
1249 use crate::units::{
1250 ActivityPerCapacity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity,
1251 MoneyPerFlow,
1252 };
1253 use float_cmp::assert_approx_eq;
1254 use indexmap::indexmap;
1255 use itertools::assert_equal;
1256 use rstest::{fixture, rstest};
1257 use std::sync::Arc;
1258
1259 #[fixture]
1261 fn commissioned_multi_tranche(mut multi_tranche_asset: Asset) -> AssetRef {
1262 multi_tranche_asset.commission(AssetID(0));
1263 assert_eq!(multi_tranche_asset.num_tranches(), 3);
1264 AssetRef::from(multi_tranche_asset)
1265 }
1266
1267 #[rstest]
1268 fn get_input_cost_from_prices_works(
1269 region_id: RegionID,
1270 svd_commodity: Commodity,
1271 mut process: Process,
1272 time_slice: TimeSliceID,
1273 ) {
1274 let commodity_rc = Arc::new(svd_commodity);
1276 let process_flow = ProcessFlow {
1277 commodity: Arc::clone(&commodity_rc),
1278 coeff: FlowPerActivity(-2.0), kind: FlowType::Fixed,
1280 cost: MoneyPerFlow(0.0),
1281 };
1282 let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() };
1283 let process_flows_map = process_flows_map(process.regions.clone(), Arc::new(process_flows));
1284 process.flows = process_flows_map;
1285
1286 let asset = Asset::new_candidate(Arc::new(process), region_id.clone(), Capacity(1.0), 2020)
1288 .unwrap();
1289
1290 let mut input_prices = PriceMap::default();
1292 input_prices.insert(&commodity_rc.id, ®ion_id, &time_slice, MoneyPerFlow(3.0));
1293
1294 let cost = asset.get_input_cost_from_prices(&input_prices, &time_slice);
1296 assert_approx_eq!(MoneyPerActivity, cost, MoneyPerActivity(6.0));
1298 }
1299
1300 #[rstest]
1301 fn dispatch_equivalence_ignores_capacity(asset: Asset) {
1302 let mut other = asset.clone();
1303 other.set_capacity(AssetCapacity::single(Capacity(3.0)));
1304
1305 assert!(asset.is_dispatch_equivalent(&other));
1306 assert_eq!(
1307 asset.dispatch_equivalence_hash(),
1308 other.dispatch_equivalence_hash()
1309 );
1310 }
1311
1312 #[rstest]
1313 fn dispatch_equivalence_fails_for_different_region(asset: Asset) {
1314 let mut other = asset.clone();
1315 other.region_id = "FRA".into();
1316
1317 assert!(!asset.is_dispatch_equivalent(&other));
1318 }
1319
1320 #[rstest]
1321 fn dispatch_equivalence_fails_for_different_variable_operating_cost(asset: Asset) {
1322 let mut other = asset.clone();
1323 Arc::make_mut(&mut other.process_parameter).variable_operating_cost = MoneyPerActivity(1.0);
1324
1325 assert!(!asset.is_dispatch_equivalent(&other));
1326 }
1327
1328 #[rstest]
1329 fn dispatch_equivalence_fails_for_different_activity_limits(
1330 asset: Asset,
1331 asset_with_activity_limits: Asset,
1332 ) {
1333 assert!(!asset.is_dispatch_equivalent(&asset_with_activity_limits));
1334 }
1335
1336 #[rstest]
1337 fn dispatch_equivalence_fails_for_different_flows(asset: Asset, svd_commodity: Commodity) {
1338 let commodity = Arc::new(svd_commodity);
1339 let flow = ProcessFlow {
1340 commodity: Arc::clone(&commodity),
1341 coeff: FlowPerActivity(1.0),
1342 kind: FlowType::Fixed,
1343 cost: MoneyPerFlow(0.0),
1344 };
1345
1346 let mut other = asset.clone();
1347 other.flows = Arc::new(indexmap! { commodity.id.clone() => flow });
1348
1349 assert!(!asset.is_dispatch_equivalent(&other));
1350 }
1351
1352 #[rstest]
1353 fn dispatch_equivalence_handles_separately_allocated_values(asset: Asset) {
1354 let mut other = asset.clone();
1355 other.activity_limits = Arc::new((*asset.activity_limits).clone());
1356 other.flows = Arc::new((*asset.flows).clone());
1357
1358 assert!(asset.is_dispatch_equivalent(&other));
1359 assert_eq!(
1360 asset.dispatch_equivalence_hash(),
1361 other.dispatch_equivalence_hash()
1362 );
1363 }
1364
1365 #[rstest]
1366 fn dispatch_equivalence_ignores_state(asset: Asset) {
1367 let mut other = asset.clone();
1368 other.commission(AssetID(1));
1369
1370 assert!(asset.is_dispatch_equivalent(&other));
1371 assert_eq!(
1372 asset.dispatch_equivalence_hash(),
1373 other.dispatch_equivalence_hash()
1374 );
1375 }
1376
1377 #[rstest]
1378 fn dispatch_equivalence_is_reflexive(asset: Asset) {
1379 assert!(asset.is_dispatch_equivalent(&asset));
1380 assert_eq!(
1381 asset.dispatch_equivalence_hash(),
1382 asset.dispatch_equivalence_hash()
1383 );
1384 }
1385
1386 #[fixture]
1387 fn process_with_activity_limits(
1388 mut process: Process,
1389 time_slice_info: TimeSliceInfo,
1390 time_slice: TimeSliceID,
1391 ) -> Process {
1392 let mut activity_limits = ActivityLimits::new_with_full_availability(&time_slice_info);
1394 activity_limits.add_time_slice_limit(time_slice, Dimensionless(0.1)..=Dimensionless(0.5));
1395 process.activity_limits =
1396 process_activity_limits_map(process.regions.clone(), activity_limits);
1397
1398 process.capacity_to_activity = ActivityPerCapacity(2.0);
1400 process
1401 }
1402
1403 #[fixture]
1404 fn asset_with_activity_limits(process_with_activity_limits: Process) -> Asset {
1405 Asset::new_ready(
1406 "agent1".into(),
1407 Arc::new(process_with_activity_limits),
1408 "GBR".into(),
1409 AssetCapacity::single(Capacity(2.0)),
1410 2010,
1411 )
1412 .unwrap()
1413 }
1414
1415 #[rstest]
1416 fn asset_get_activity_per_capacity_limits(
1417 asset_with_activity_limits: Asset,
1418 time_slice: TimeSliceID,
1419 ) {
1420 assert_eq!(
1422 asset_with_activity_limits.get_activity_per_capacity_limits(&time_slice),
1423 ActivityPerCapacity(0.2)..=ActivityPerCapacity(1.0)
1424 );
1425 }
1426
1427 #[rstest]
1428 #[case(Capacity(0.01))]
1429 #[case(Capacity(0.5))]
1430 #[case(Capacity(1.0))]
1431 #[case(Capacity(100.0))]
1432 fn user_asset_new_valid(
1433 agent_id: AgentID,
1434 process: Process,
1435 region_id: RegionID,
1436 #[case] capacity: Capacity,
1437 ) {
1438 let asset_capacity = AssetCapacity::single(capacity);
1439 let asset = UserAsset::new(
1440 agent_id,
1441 process.into(),
1442 region_id,
1443 asset_capacity,
1444 2015,
1445 None,
1446 )
1447 .unwrap();
1448 assert!(asset.id().is_none());
1449 }
1450
1451 #[rstest]
1452 #[case(Capacity(0.0))]
1453 fn user_asset_new_zero_capacity(
1454 agent_id: AgentID,
1455 process: Process,
1456 region_id: RegionID,
1457 #[case] capacity: Capacity,
1458 ) {
1459 let asset_capacity = AssetCapacity::single(capacity);
1462 assert_error!(
1463 UserAsset::new(
1464 agent_id,
1465 process.into(),
1466 region_id,
1467 asset_capacity,
1468 2015,
1469 None
1470 ),
1471 "Capacity must be a finite, positive number"
1472 );
1473 }
1474
1475 #[rstest]
1476 fn user_asset_new_invalid_commission_year(
1477 agent_id: AgentID,
1478 process: Process,
1479 region_id: RegionID,
1480 ) {
1481 assert_error!(
1482 UserAsset::new(
1483 agent_id,
1484 process.into(),
1485 region_id,
1486 AssetCapacity::single(Capacity(1.0)),
1487 2007,
1488 None
1489 ),
1490 "Process process1 does not operate in the year 2007"
1491 );
1492 }
1493
1494 #[rstest]
1495 fn user_asset_new_invalid_region(agent_id: AgentID, process: Process) {
1496 let region_id = RegionID("FRA".into());
1497 assert_error!(
1498 UserAsset::new(
1499 agent_id,
1500 process.into(),
1501 region_id,
1502 AssetCapacity::single(Capacity(1.0)),
1503 2015,
1504 None
1505 ),
1506 "Process process1 does not operate in region FRA"
1507 );
1508 }
1509
1510 #[rstest]
1511 #[case::subset(2, false)]
1512 #[case::all_tranches(3, true)]
1513 fn with_subset_of_tranches(
1514 multi_tranche_asset: Asset,
1515 #[case] num_tranches: u32,
1516 #[case] expect_same_asset: bool,
1517 ) {
1518 let asset = AssetRef::from(multi_tranche_asset);
1519 let asset_subset = asset.clone().with_subset_of_tranches(num_tranches);
1520
1521 assert_eq!(
1522 asset_subset.capacity(),
1523 AssetCapacity::new(num_tranches, Capacity(4.0))
1524 );
1525 assert_eq!(asset_subset.id(), asset.id());
1526 assert_eq!(asset_subset.agent_id(), asset.agent_id());
1527 assert_eq!(Arc::ptr_eq(&asset_subset.0, &asset.0), expect_same_asset);
1528 assert_eq!(asset.capacity(), AssetCapacity::new(3, Capacity(4.0)));
1529 }
1530
1531 #[rstest]
1532 #[should_panic(expected = "Cannot make an asset with zero tranches")]
1533 fn with_subset_of_tranches_panics_for_zero_tranches(commissioned_multi_tranche: AssetRef) {
1534 commissioned_multi_tranche.with_subset_of_tranches(0);
1535 }
1536
1537 #[rstest]
1538 #[should_panic(expected = "Cannot make an asset with more tranches than original")]
1539 fn with_subset_of_tranches_panics_for_too_many_tranches(commissioned_multi_tranche: AssetRef) {
1540 commissioned_multi_tranche.with_subset_of_tranches(4);
1541 }
1542
1543 #[rstest]
1544 fn asset_commission(process: Process) {
1545 let mut asset = Asset::new_ready(
1547 "agent1".into(),
1548 process.into(),
1549 "GBR".into(),
1550 AssetCapacity::single(Capacity(1.0)),
1551 2020,
1552 )
1553 .unwrap();
1554 asset.commission(AssetID(2));
1555 assert!(asset.is_commissioned());
1556 assert_eq!(asset.id(), Some(AssetID(2)));
1557 }
1558
1559 #[rstest]
1560 #[should_panic(expected = "Assets with state Candidate cannot be commissioned")]
1561 fn commission_wrong_states(process: Process) {
1562 let mut asset =
1563 Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap();
1564 asset.commission(AssetID(1));
1565 }
1566
1567 #[test]
1568 fn commission_year_before_time_horizon() {
1569 let processes_patch = FilePatch::new("processes.csv")
1570 .with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
1571 .with_addition("GASDRV,Dry gas extraction,all,GASPRD,1980,2040,1.0,");
1572
1573 let patches = vec![
1576 processes_patch.clone(),
1577 FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,1980"),
1578 ];
1579 assert_patched_runs_ok_simple!(patches);
1580
1581 let patches = vec![
1583 processes_patch,
1584 FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,1970"),
1585 ];
1586 assert_validate_fails_with_simple!(
1587 patches,
1588 "Agent A0_GEX has asset with commission year 1970, not within process GASDRV commission years: 1980..=2040"
1589 );
1590 }
1591
1592 #[test]
1593 fn commission_year_after_time_horizon() {
1594 let processes_patch = FilePatch::new("processes.csv")
1595 .with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
1596 .with_addition("GASDRV,Dry gas extraction,all,GASPRD,2020,2050,1.0,");
1597
1598 let patches = vec![
1600 processes_patch.clone(),
1601 FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,2050"),
1602 ];
1603 assert_patched_runs_ok_simple!(patches);
1604
1605 let patches = vec![
1607 processes_patch,
1608 FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,2060"),
1609 ];
1610 assert_validate_fails_with_simple!(
1611 patches,
1612 "Agent A0_GEX has asset with commission year 2060, not within process GASDRV commission years: 2020..=2050"
1613 );
1614 }
1615
1616 #[rstest]
1617 #[case::none(0)]
1618 #[case::some(2)]
1619 #[case::all(3)]
1620 fn mothball_tranche_counts(commissioned_multi_tranche: AssetRef, #[case] num_mothballed: u32) {
1621 assert_eq!(commissioned_multi_tranche.num_tranches(), 3);
1622 let asset = commissioned_multi_tranche.with_mothballed_tranches(num_mothballed, Some(2020));
1623 assert_eq!(asset.get_num_mothballed_tranches(), num_mothballed);
1624 assert_eq!(
1625 asset.mothballed_capacity(),
1626 Capacity(4.0 * num_mothballed as f64)
1627 );
1628 assert_eq!(asset.get_num_nonmothballed_tranches(), 3 - num_mothballed);
1629 assert_eq!(asset.has_any_mothballed_tranches(), num_mothballed > 0);
1630 }
1631
1632 #[rstest]
1633 fn mothball_counts_non_commissioned(asset: Asset, process: Process) {
1634 let ready = AssetRef::from(asset);
1636 let candidate = AssetRef::from(
1637 Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap(),
1638 );
1639 for asset in [ready, candidate] {
1640 assert!(!asset.has_any_mothballed_tranches());
1641 assert_eq!(asset.get_num_mothballed_tranches(), 0);
1642 assert_eq!(asset.get_num_nonmothballed_tranches(), asset.num_tranches());
1643 }
1644 }
1645
1646 #[rstest]
1647 fn with_mothballed_tranches_accumulates_events(commissioned_multi_tranche: AssetRef) {
1648 let asset = commissioned_multi_tranche.with_mothballed_tranches(1, Some(2020));
1650 assert_equal(
1651 asset.get_mothball_events().unwrap().iter(),
1652 &[MothballEvent {
1653 year: 2020,
1654 num_tranches: 1,
1655 }],
1656 );
1657
1658 let asset = asset.with_mothballed_tranches(2, Some(2022));
1660 assert_equal(
1661 asset.get_mothball_events().unwrap().iter(),
1662 &[
1663 MothballEvent {
1664 year: 2020,
1665 num_tranches: 1,
1666 },
1667 MothballEvent {
1668 year: 2022,
1669 num_tranches: 1,
1670 },
1671 ],
1672 );
1673 }
1674
1675 #[rstest]
1676 fn with_mothballed_tranches_decrease_removes_oldest_first(
1677 commissioned_multi_tranche: AssetRef,
1678 ) {
1679 let asset = commissioned_multi_tranche
1681 .with_mothballed_tranches(1, Some(2020))
1682 .with_mothballed_tranches(3, Some(2022));
1683 assert_equal(
1684 asset.get_mothball_events().unwrap().iter(),
1685 &[
1686 MothballEvent {
1687 year: 2020,
1688 num_tranches: 1,
1689 },
1690 MothballEvent {
1691 year: 2022,
1692 num_tranches: 2,
1693 },
1694 ],
1695 );
1696
1697 let asset = asset.with_mothballed_tranches(1, None);
1700 assert_eq!(asset.get_num_mothballed_tranches(), 1);
1701 assert_equal(
1702 asset.get_mothball_events().unwrap().iter(),
1703 &[MothballEvent {
1704 year: 2022,
1705 num_tranches: 1,
1706 }],
1707 );
1708 }
1709
1710 #[rstest]
1711 fn with_mothballed_tranches_noop_returns_same_rc(commissioned_multi_tranche: AssetRef) {
1712 let asset = commissioned_multi_tranche.with_mothballed_tranches(2, Some(2020));
1713 let same = asset.clone().with_mothballed_tranches(2, Some(2099));
1715 assert!(Arc::ptr_eq(&asset.0, &same.0));
1716 }
1717
1718 #[rstest]
1719 fn with_mothballed_tranches_zero_unmothballs(commissioned_multi_tranche: AssetRef) {
1720 let asset = commissioned_multi_tranche.with_mothballed_tranches(2, Some(2020));
1721 assert!(asset.has_any_mothballed_tranches());
1722
1723 let asset = asset.with_mothballed_tranches(0, None);
1724 assert!(!asset.has_any_mothballed_tranches());
1725 assert_eq!(asset.get_num_mothballed_tranches(), 0);
1726 }
1727
1728 #[rstest]
1729 #[should_panic(expected = "Cannot mothball more tranches than asset represents")]
1730 fn with_mothballed_tranches_panics_for_too_many_tranches(commissioned_multi_tranche: AssetRef) {
1731 commissioned_multi_tranche.with_mothballed_tranches(4, Some(2020));
1732 }
1733
1734 #[rstest]
1735 #[should_panic(
1736 expected = "Cannot change number of mothballed tranches for an asset that hasn't been commissioned"
1737 )]
1738 fn with_mothballed_tranches_panics_for_non_commissioned(asset: Asset) {
1739 AssetRef::from(asset).with_mothballed_tranches(1, Some(2020));
1740 }
1741
1742 #[rstest]
1743 #[should_panic(
1744 expected = "Cannot increase number of mothballed tranches without supplying year"
1745 )]
1746 fn with_mothballed_tranches_panics_when_increasing_without_year(
1747 commissioned_multi_tranche: AssetRef,
1748 ) {
1749 commissioned_multi_tranche.with_mothballed_tranches(1, None);
1750 }
1751
1752 #[rstest]
1753 #[should_panic(expected = "Attempting to mothball tranches in a year in the past")]
1754 fn with_mothballed_tranches_panics_when_mothballing_in_the_past(
1755 commissioned_multi_tranche: AssetRef,
1756 ) {
1757 commissioned_multi_tranche
1760 .with_mothballed_tranches(1, Some(2020))
1761 .with_mothballed_tranches(2, Some(2019));
1762 }
1763
1764 #[rstest]
1765 fn with_no_mothballed_tranches_clears_events(commissioned_multi_tranche: AssetRef) {
1766 let asset = commissioned_multi_tranche.with_mothballed_tranches(2, Some(2020));
1767 let asset = asset.with_no_mothballed_tranches();
1768 assert!(!asset.has_any_mothballed_tranches());
1769 assert_eq!(asset.get_num_mothballed_tranches(), 0);
1770 }
1771
1772 #[rstest]
1773 fn with_no_mothballed_tranches_noop_returns_same_rc(commissioned_multi_tranche: AssetRef) {
1774 let asset = commissioned_multi_tranche;
1776 let same = asset.clone().with_no_mothballed_tranches();
1777 assert!(Arc::ptr_eq(&asset.0, &same.0));
1778 }
1779
1780 #[rstest]
1781 fn with_subset_of_tranches_caps_mothballed(commissioned_multi_tranche: AssetRef) {
1782 let asset = commissioned_multi_tranche.with_mothballed_tranches(3, Some(2020));
1784 assert_eq!(asset.get_num_mothballed_tranches(), 3);
1785
1786 let subset = asset.with_subset_of_tranches(2);
1788 assert_eq!(subset.num_tranches(), 2);
1789 assert_eq!(subset.get_num_mothballed_tranches(), 2);
1790 }
1791
1792 #[rstest]
1793 fn with_decommission_mothballed_nothing_old_enough(commissioned_multi_tranche: AssetRef) {
1794 let asset = commissioned_multi_tranche.with_mothballed_tranches(1, Some(2020));
1795 let result = asset
1797 .clone()
1798 .with_decommission_mothballed(2025, 20)
1799 .unwrap();
1800 assert!(Arc::ptr_eq(&asset.0, &result.0));
1801 }
1802
1803 #[rstest]
1804 fn with_decommission_mothballed_partial(commissioned_multi_tranche: AssetRef) {
1805 let asset = commissioned_multi_tranche
1807 .with_mothballed_tranches(1, Some(2010))
1808 .with_mothballed_tranches(2, Some(2020));
1809
1810 let result = asset.with_decommission_mothballed(2025, 10).unwrap();
1812 assert_eq!(result.num_tranches(), 2);
1813 assert_eq!(result.get_num_mothballed_tranches(), 1);
1814 assert_equal(
1815 result.get_mothball_events().unwrap().iter(),
1816 &[MothballEvent {
1817 year: 2020,
1818 num_tranches: 1,
1819 }],
1820 );
1821 }
1822
1823 #[rstest]
1824 fn with_decommission_mothballed_all(commissioned_multi_tranche: AssetRef) {
1825 let asset = commissioned_multi_tranche.with_mothballed_tranches(3, Some(2010));
1827 assert!(asset.with_decommission_mothballed(2025, 10).is_none());
1828 }
1829}