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, Year,
14};
15use anyhow::{Context, Result, ensure};
16use indexmap::IndexMap;
17use log::debug;
18use serde::{Deserialize, Serialize};
19use std::cell::Cell;
20use std::cmp::Ordering;
21use std::hash::{Hash, Hasher};
22use std::iter;
23use std::ops::{Deref, RangeInclusive};
24use std::rc::Rc;
25
26mod capacity;
27pub use capacity::AssetCapacity;
28mod pool;
29pub use pool::AssetPool;
30
31#[derive(
33 Clone,
34 Copy,
35 Debug,
36 derive_more::Display,
37 Eq,
38 Hash,
39 Ord,
40 PartialEq,
41 PartialOrd,
42 Deserialize,
43 Serialize,
44)]
45pub struct AssetID(u32);
46
47#[derive(
49 Clone,
50 Copy,
51 Debug,
52 derive_more::Display,
53 Eq,
54 Hash,
55 Ord,
56 PartialEq,
57 PartialOrd,
58 Deserialize,
59 Serialize,
60)]
61pub struct AssetGroupID(u32);
62
63#[derive(Clone, Debug, PartialEq, strum::Display)]
75pub enum AssetState {
76 Commissioned {
78 id: AssetID,
80 agent_id: AgentID,
82 mothballed_year: Option<u32>,
84 parent: Option<AssetRef>,
88 },
89 Decommissioned {
91 id: AssetID,
93 agent_id: AgentID,
95 decommission_year: u32,
97 },
98 Future {
100 agent_id: AgentID,
102 },
103 Selected {
105 agent_id: AgentID,
107 },
108 Parent {
113 agent_id: AgentID,
115 group_id: AssetGroupID,
117 },
118 Candidate,
120}
121
122#[derive(Clone, PartialEq)]
124pub struct Asset {
125 state: AssetState,
127 process: Rc<Process>,
129 activity_limits: Rc<ActivityLimits>,
131 flows: Rc<IndexMap<CommodityID, ProcessFlow>>,
133 process_parameter: Rc<ProcessParameter>,
135 region_id: RegionID,
137 capacity: Cell<AssetCapacity>,
139 commission_year: u32,
141 max_decommission_year: u32,
143}
144
145impl Asset {
146 pub fn new_candidate(
148 process: Rc<Process>,
149 region_id: RegionID,
150 capacity: Capacity,
151 commission_year: u32,
152 ) -> Result<Self> {
153 let unit_size = process.unit_size;
154 Self::new_with_state(
155 AssetState::Candidate,
156 process,
157 region_id,
158 AssetCapacity::from_capacity(capacity, unit_size),
159 commission_year,
160 None,
161 )
162 }
163
164 pub fn new_candidate_for_dispatch(
170 process: Rc<Process>,
171 region_id: RegionID,
172 capacity: Capacity,
173 commission_year: u32,
174 ) -> Result<Self> {
175 Self::new_with_state(
176 AssetState::Candidate,
177 process,
178 region_id,
179 AssetCapacity::Continuous(capacity),
180 commission_year,
181 None,
182 )
183 }
184
185 pub fn new_candidate_from_commissioned(asset: &Asset) -> Self {
187 assert!(asset.is_commissioned(), "Asset must be commissioned");
188
189 Self {
190 state: AssetState::Candidate,
191 ..asset.clone()
192 }
193 }
194
195 pub fn new_future_with_max_decommission(
197 agent_id: AgentID,
198 process: Rc<Process>,
199 region_id: RegionID,
200 capacity: Capacity,
201 commission_year: u32,
202 max_decommission_year: Option<u32>,
203 ) -> Result<Self> {
204 check_capacity_valid_for_asset(capacity)?;
205 let unit_size = process.unit_size;
206 Self::new_with_state(
207 AssetState::Future { agent_id },
208 process,
209 region_id,
210 AssetCapacity::from_capacity(capacity, unit_size),
211 commission_year,
212 max_decommission_year,
213 )
214 }
215
216 pub fn new_future(
218 agent_id: AgentID,
219 process: Rc<Process>,
220 region_id: RegionID,
221 capacity: Capacity,
222 commission_year: u32,
223 ) -> Result<Self> {
224 Self::new_future_with_max_decommission(
225 agent_id,
226 process,
227 region_id,
228 capacity,
229 commission_year,
230 None,
231 )
232 }
233
234 #[cfg(test)]
239 fn new_selected(
240 agent_id: AgentID,
241 process: Rc<Process>,
242 region_id: RegionID,
243 capacity: Capacity,
244 commission_year: u32,
245 ) -> Result<Self> {
246 let unit_size = process.unit_size;
247 Self::new_with_state(
248 AssetState::Selected { agent_id },
249 process,
250 region_id,
251 AssetCapacity::from_capacity(capacity, unit_size),
252 commission_year,
253 None,
254 )
255 }
256
257 #[cfg(test)]
262 pub fn new_commissioned(
263 agent_id: AgentID,
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::Commissioned {
272 id: AssetID(0),
273 agent_id,
274 mothballed_year: None,
275 parent: None,
276 },
277 process,
278 region_id,
279 AssetCapacity::from_capacity(capacity, unit_size),
280 commission_year,
281 None,
282 )
283 }
284
285 fn new_with_state(
287 state: AssetState,
288 process: Rc<Process>,
289 region_id: RegionID,
290 capacity: AssetCapacity,
291 commission_year: u32,
292 max_decommission_year: Option<u32>,
293 ) -> Result<Self> {
294 check_region_year_valid_for_process(&process, ®ion_id, commission_year)?;
295 ensure!(
296 capacity.total_capacity() >= Capacity(0.0),
297 "Capacity must be non-negative"
298 );
299
300 let key = (region_id.clone(), commission_year);
306 let activity_limits = process
307 .activity_limits
308 .get(&key)
309 .with_context(|| {
310 format!(
311 "No process availabilities supplied for process {} in region {} in year {}. \
312 You should update process_availabilities.csv.",
313 &process.id, region_id, commission_year
314 )
315 })?
316 .clone();
317 let flows = process
318 .flows
319 .get(&key)
320 .with_context(|| {
321 format!(
322 "No commodity flows supplied for process {} in region {} in year {}. \
323 You should update process_flows.csv.",
324 &process.id, region_id, commission_year
325 )
326 })?
327 .clone();
328 let process_parameter = process
329 .parameters
330 .get(&key)
331 .with_context(|| {
332 format!(
333 "No process parameters supplied for process {} in region {} in year {}. \
334 You should update process_parameters.csv.",
335 &process.id, region_id, commission_year
336 )
337 })?
338 .clone();
339
340 let max_decommission_year =
341 max_decommission_year.unwrap_or(commission_year + process_parameter.lifetime);
342 ensure!(
343 max_decommission_year >= commission_year,
344 "Max decommission year must be after/same as commission year"
345 );
346
347 Ok(Self {
348 state,
349 process,
350 activity_limits,
351 flows,
352 process_parameter,
353 region_id,
354 capacity: Cell::new(capacity),
355 commission_year,
356 max_decommission_year,
357 })
358 }
359
360 pub fn state(&self) -> &AssetState {
362 &self.state
363 }
364
365 pub fn process_parameter(&self) -> &ProcessParameter {
367 &self.process_parameter
368 }
369
370 pub fn max_decommission_year(&self) -> u32 {
372 self.max_decommission_year
373 }
374
375 pub fn get_activity_per_capacity_limits(
377 &self,
378 time_slice: &TimeSliceID,
379 ) -> RangeInclusive<ActivityPerCapacity> {
380 let limits = &self.activity_limits.get_limit_for_time_slice(time_slice);
381 let cap2act = self.process.capacity_to_activity;
382 (cap2act * *limits.start())..=(cap2act * *limits.end())
383 }
384
385 pub fn get_activity_limits_for_selection(
387 &self,
388 time_slice_selection: &TimeSliceSelection,
389 ) -> RangeInclusive<Activity> {
390 let activity_per_capacity_limits = self.activity_limits.get_limit(time_slice_selection);
391 let cap2act = self.process.capacity_to_activity;
392 let max_activity = self.total_capacity() * cap2act;
393 let lb = max_activity * *activity_per_capacity_limits.start();
394 let ub = max_activity * *activity_per_capacity_limits.end();
395 lb..=ub
396 }
397
398 pub fn iter_activity_limits(
400 &self,
401 ) -> impl Iterator<Item = (TimeSliceSelection, RangeInclusive<Activity>)> + '_ {
402 let max_act = self.max_activity();
403 self.activity_limits
404 .iter_limits()
405 .map(move |(ts_sel, limit)| {
406 (
407 ts_sel,
408 (max_act * *limit.start())..=(max_act * *limit.end()),
409 )
410 })
411 }
412
413 pub fn iter_activity_per_capacity_limits(
415 &self,
416 ) -> impl Iterator<Item = (TimeSliceSelection, RangeInclusive<ActivityPerCapacity>)> + '_ {
417 let cap2act = self.process.capacity_to_activity;
418 self.activity_limits
419 .iter_limits()
420 .map(move |(ts_sel, limit)| {
421 (
422 ts_sel,
423 (cap2act * *limit.start())..=(cap2act * *limit.end()),
424 )
425 })
426 }
427
428 pub fn get_total_output_per_activity(&self) -> FlowPerActivity {
435 self.iter_output_flows().map(|flow| flow.coeff).sum()
436 }
437
438 pub fn get_operating_cost(&self, year: u32, time_slice: &TimeSliceID) -> MoneyPerActivity {
440 let flows_cost = self
442 .iter_flows()
443 .map(|flow| flow.get_total_cost_per_activity(&self.region_id, year, time_slice))
444 .sum();
445
446 self.process_parameter.variable_operating_cost + flows_cost
447 }
448
449 pub fn get_revenue_from_flows(
453 &self,
454 prices: &CommodityPrices,
455 time_slice: &TimeSliceID,
456 ) -> MoneyPerActivity {
457 self.get_revenue_from_flows_with_filter(prices, time_slice, |_| true)
458 }
459
460 pub fn get_revenue_from_flows_excluding_primary(
464 &self,
465 prices: &CommodityPrices,
466 time_slice: &TimeSliceID,
467 ) -> MoneyPerActivity {
468 let excluded_commodity = self.primary_output().map(|flow| &flow.commodity.id);
469
470 self.get_revenue_from_flows_with_filter(prices, time_slice, |flow| {
471 excluded_commodity.is_none_or(|commodity_id| commodity_id != &flow.commodity.id)
472 })
473 }
474
475 pub fn get_input_cost_from_prices(
479 &self,
480 prices: &CommodityPrices,
481 time_slice: &TimeSliceID,
482 ) -> MoneyPerActivity {
483 -self.get_revenue_from_flows_with_filter(prices, time_slice, |x| {
485 x.direction() == FlowDirection::Input
486 })
487 }
488
489 fn get_revenue_from_flows_with_filter<F>(
494 &self,
495 prices: &CommodityPrices,
496 time_slice: &TimeSliceID,
497 mut filter_for_flows: F,
498 ) -> MoneyPerActivity
499 where
500 F: FnMut(&ProcessFlow) -> bool,
501 {
502 self.iter_flows()
503 .filter(|flow| filter_for_flows(flow))
504 .map(|flow| {
505 flow.coeff
506 * prices
507 .get(&flow.commodity.id, &self.region_id, time_slice)
508 .unwrap_or(MoneyPerFlow(0.0))
509 })
510 .sum()
511 }
512
513 fn get_generic_activity_cost(
518 &self,
519 prices: &CommodityPrices,
520 year: u32,
521 time_slice: &TimeSliceID,
522 ) -> MoneyPerActivity {
523 let cost_of_inputs = self.get_input_cost_from_prices(prices, time_slice);
525
526 let excludes_sed_svd_output = |flow: &&ProcessFlow| {
528 !(flow.direction() == FlowDirection::Output
529 && matches!(
530 flow.commodity.kind,
531 CommodityType::SupplyEqualsDemand | CommodityType::ServiceDemand
532 ))
533 };
534 let flow_costs = self
535 .iter_flows()
536 .filter(excludes_sed_svd_output)
537 .map(|flow| flow.get_total_cost_per_activity(&self.region_id, year, time_slice))
538 .sum();
539
540 cost_of_inputs + flow_costs + self.process_parameter.variable_operating_cost
541 }
542
543 pub fn iter_marginal_costs_with_filter<'a>(
551 &'a self,
552 prices: &'a CommodityPrices,
553 year: u32,
554 time_slice: &'a TimeSliceID,
555 filter: impl Fn(&CommodityID) -> bool + 'a,
556 ) -> Box<dyn Iterator<Item = (CommodityID, MoneyPerFlow)> + 'a> {
557 let mut output_flows_iter = self
559 .iter_output_flows()
560 .filter(move |flow| filter(&flow.commodity.id))
561 .peekable();
562
563 if output_flows_iter.peek().is_none() {
565 return Box::new(std::iter::empty::<(CommodityID, MoneyPerFlow)>());
566 }
567
568 let generic_activity_cost = self.get_generic_activity_cost(prices, year, time_slice);
573
574 let total_output_per_activity = self.get_total_output_per_activity();
579 assert!(total_output_per_activity > FlowPerActivity::EPSILON); let generic_cost_per_flow = generic_activity_cost / total_output_per_activity;
581
582 Box::new(output_flows_iter.map(move |flow| {
584 let commodity_specific_costs_per_flow =
586 flow.get_total_cost_per_flow(&self.region_id, year, time_slice);
587
588 let marginal_cost = generic_cost_per_flow + commodity_specific_costs_per_flow;
590 (flow.commodity.id.clone(), marginal_cost)
591 }))
592 }
593
594 pub fn iter_marginal_costs<'a>(
598 &'a self,
599 prices: &'a CommodityPrices,
600 year: u32,
601 time_slice: &'a TimeSliceID,
602 ) -> Box<dyn Iterator<Item = (CommodityID, MoneyPerFlow)> + 'a> {
603 self.iter_marginal_costs_with_filter(prices, year, time_slice, move |_| true)
604 }
605
606 pub fn get_annual_capital_cost_per_capacity(&self) -> MoneyPerCapacity {
608 let capital_cost = self.process_parameter.capital_cost;
609 let lifetime = self.process_parameter.lifetime;
610 let discount_rate = self.process_parameter.discount_rate;
611 annual_capital_cost(capital_cost, lifetime, discount_rate)
612 }
613
614 pub fn get_annual_fixed_costs_per_activity(
619 &self,
620 annual_activity: Activity,
621 ) -> MoneyPerActivity {
622 let annual_capital_cost_per_capacity = self.get_annual_capital_cost_per_capacity();
623 let annual_fixed_opex = self.process_parameter.fixed_operating_cost * Year(1.0);
624 let total_annual_fixed_costs =
625 (annual_capital_cost_per_capacity + annual_fixed_opex) * self.total_capacity();
626 assert!(
627 annual_activity > Activity::EPSILON,
628 "Cannot calculate annual fixed costs per activity for an asset with zero annual activity"
629 );
630 total_annual_fixed_costs / annual_activity
631 }
632
633 pub fn get_annual_fixed_costs_per_flow(&self, annual_activity: Activity) -> MoneyPerFlow {
638 let annual_fixed_costs_per_activity =
639 self.get_annual_fixed_costs_per_activity(annual_activity);
640 let total_output_per_activity = self.get_total_output_per_activity();
641 assert!(total_output_per_activity > FlowPerActivity::EPSILON); annual_fixed_costs_per_activity / total_output_per_activity
643 }
644
645 pub fn max_activity(&self) -> Activity {
647 self.total_capacity() * self.process.capacity_to_activity
648 }
649
650 pub fn get_flow(&self, commodity_id: &CommodityID) -> Option<&ProcessFlow> {
652 self.flows.get(commodity_id)
653 }
654
655 pub fn iter_flows(&self) -> impl Iterator<Item = &ProcessFlow> {
657 self.flows.values()
658 }
659
660 pub fn iter_output_flows(&self) -> impl Iterator<Item = &ProcessFlow> {
662 self.flows.values().filter(|flow| {
663 flow.direction() == FlowDirection::Output
664 && matches!(
665 flow.commodity.kind,
666 CommodityType::SupplyEqualsDemand | CommodityType::ServiceDemand
667 )
668 })
669 }
670
671 pub fn primary_output(&self) -> Option<&ProcessFlow> {
673 self.process
674 .primary_output
675 .as_ref()
676 .map(|commodity_id| &self.flows[commodity_id])
677 }
678
679 pub fn is_commissioned(&self) -> bool {
681 matches!(&self.state, AssetState::Commissioned { .. })
682 }
683
684 pub fn commission_year(&self) -> u32 {
686 self.commission_year
687 }
688
689 pub fn decommission_year(&self) -> Option<u32> {
691 match &self.state {
692 AssetState::Decommissioned {
693 decommission_year, ..
694 } => Some(*decommission_year),
695 _ => None,
696 }
697 }
698
699 pub fn region_id(&self) -> &RegionID {
701 &self.region_id
702 }
703
704 pub fn process(&self) -> &Process {
706 &self.process
707 }
708
709 pub fn process_id(&self) -> &ProcessID {
711 &self.process.id
712 }
713
714 pub fn id(&self) -> Option<AssetID> {
716 match &self.state {
717 AssetState::Commissioned { id, .. } | AssetState::Decommissioned { id, .. } => {
718 Some(*id)
719 }
720 _ => None,
721 }
722 }
723
724 pub fn parent(&self) -> Option<&AssetRef> {
726 match &self.state {
727 AssetState::Commissioned { parent, .. } => parent.as_ref(),
728 _ => None,
729 }
730 }
731
732 pub fn is_parent(&self) -> bool {
734 matches!(self.state, AssetState::Parent { .. })
735 }
736
737 pub fn num_children(&self) -> Option<u32> {
741 match &self.state {
742 AssetState::Parent { .. } => Some(self.capacity().n_units().unwrap()),
743 _ => None,
744 }
745 }
746
747 pub fn group_id(&self) -> Option<AssetGroupID> {
749 match &self.state {
750 AssetState::Commissioned { parent, .. } => {
751 parent
753 .as_ref()
754 .map(|parent| parent.group_id().unwrap())
756 }
757 AssetState::Parent { group_id, .. } => Some(*group_id),
758 _ => None,
759 }
760 }
761
762 pub fn agent_id(&self) -> Option<&AgentID> {
764 match &self.state {
765 AssetState::Commissioned { agent_id, .. }
766 | AssetState::Decommissioned { agent_id, .. }
767 | AssetState::Future { agent_id }
768 | AssetState::Selected { agent_id }
769 | AssetState::Parent { agent_id, .. } => Some(agent_id),
770 AssetState::Candidate => None,
771 }
772 }
773
774 pub fn capacity(&self) -> AssetCapacity {
776 self.capacity.get()
777 }
778
779 pub fn total_capacity(&self) -> Capacity {
781 self.capacity().total_capacity()
782 }
783
784 pub fn set_capacity(&mut self, capacity: AssetCapacity) {
786 assert!(
787 matches!(
788 self.state,
789 AssetState::Candidate | AssetState::Selected { .. }
790 ),
791 "set_capacity can only be called on Candidate or Selected assets"
792 );
793 assert!(
794 capacity.total_capacity() >= Capacity(0.0),
795 "Capacity must be >= 0"
796 );
797 self.capacity().assert_same_type(capacity);
798
799 self.capacity.set(capacity);
802 }
803
804 pub fn increase_capacity(&mut self, capacity: AssetCapacity) {
806 assert!(
807 self.state == AssetState::Candidate,
808 "increase_capacity can only be called on Candidate assets"
809 );
810 assert!(
811 capacity.total_capacity() > Capacity(0.0),
812 "Capacity increase must be positive"
813 );
814
815 self.capacity.update(|c| c + capacity);
818 }
819
820 fn decrement_unit_count(&self) {
828 let AssetCapacity::Discrete(n_units, unit_size) = self.capacity() else {
829 panic!("Cannot decrement unit count of non-divisible asset");
830 };
831 assert!(n_units > 0, "Unit count has dropped below zero");
832
833 self.capacity
834 .set(AssetCapacity::Discrete(n_units - 1, unit_size));
835 }
836
837 fn decommission(&mut self, decommission_year: u32, reason: &str) {
839 let (id, agent_id, parent) = match &self.state {
840 AssetState::Commissioned {
841 id,
842 agent_id,
843 parent,
844 ..
845 } => (*id, agent_id.clone(), parent),
846 _ => panic!("Cannot decommission an asset that hasn't been commissioned"),
847 };
848 debug!(
849 "Decommissioning '{}' asset (ID: {}) for agent '{}' (reason: {})",
850 self.process_id(),
851 id,
852 agent_id,
853 reason
854 );
855
856 if let Some(parent) = parent {
858 parent.decrement_unit_count();
859 }
860
861 self.state = AssetState::Decommissioned {
862 id,
863 agent_id,
864 decommission_year: decommission_year.min(self.max_decommission_year()),
865 };
866 }
867
868 fn commission(&mut self, id: AssetID, parent: Option<AssetRef>, reason: &str) {
879 let agent_id = match &self.state {
880 AssetState::Future { agent_id } | AssetState::Selected { agent_id } => agent_id,
881 state => panic!("Assets with state {state} cannot be commissioned"),
882 };
883 debug!(
884 "Commissioning '{}' asset (ID: {}, capacity: {}) for agent '{}' (reason: {})",
885 self.process_id(),
886 id,
887 self.total_capacity(),
888 agent_id,
889 reason
890 );
891 self.state = AssetState::Commissioned {
892 id,
893 agent_id: agent_id.clone(),
894 mothballed_year: None,
895 parent,
896 };
897 }
898
899 pub fn select_candidate_for_investment(&mut self, agent_id: AgentID) {
901 assert!(
902 self.state == AssetState::Candidate,
903 "select_candidate_for_investment can only be called on Candidate assets"
904 );
905 check_capacity_valid_for_asset(self.total_capacity()).unwrap();
906 self.state = AssetState::Selected { agent_id };
907 }
908
909 pub fn mothball(&mut self, year: u32) {
911 let (id, agent_id, parent) = match &self.state {
912 AssetState::Commissioned {
913 id,
914 agent_id,
915 parent,
916 ..
917 } => (*id, agent_id.clone(), parent.clone()),
918 _ => panic!("Cannot mothball an asset that hasn't been commissioned"),
919 };
920 self.state = AssetState::Commissioned {
921 id,
922 agent_id,
923 mothballed_year: Some(year),
924 parent,
925 };
926 }
927
928 pub fn unmothball(&mut self) {
930 let (id, agent_id, parent) = match &self.state {
931 AssetState::Commissioned {
932 id,
933 agent_id,
934 parent,
935 ..
936 } => (*id, agent_id.clone(), parent.clone()),
937 _ => panic!("Cannot unmothball an asset that hasn't been commissioned"),
938 };
939 self.state = AssetState::Commissioned {
940 id,
941 agent_id,
942 mothballed_year: None,
943 parent,
944 };
945 }
946
947 pub fn get_mothballed_year(&self) -> Option<u32> {
949 let AssetState::Commissioned {
950 mothballed_year, ..
951 } = &self.state
952 else {
953 panic!("Cannot get mothballed year for an asset that hasn't been commissioned")
954 };
955 *mothballed_year
956 }
957
958 pub fn unit_size(&self) -> Option<Capacity> {
960 match self.capacity() {
961 AssetCapacity::Discrete(_, size) => Some(size),
962 AssetCapacity::Continuous(_) => None,
963 }
964 }
965
966 pub fn max_installable_capacity(
975 &self,
976 commodity_portion: Dimensionless,
977 ) -> Option<AssetCapacity> {
978 assert!(
979 !self.is_commissioned(),
980 "max_installable_capacity can only be called on uncommissioned assets"
981 );
982 assert!(
983 commodity_portion >= Dimensionless(0.0) && commodity_portion <= Dimensionless(1.0),
984 "commodity_portion must be between 0 and 1 inclusive"
985 );
986
987 self.process
988 .investment_constraints
989 .get(&(self.region_id.clone(), self.commission_year))
990 .and_then(|c| c.get_addition_limit().map(|l| l * commodity_portion))
991 .map(|limit| AssetCapacity::from_capacity_floor(limit, self.unit_size()))
992 }
993}
994
995#[allow(clippy::missing_fields_in_debug)]
996impl std::fmt::Debug for Asset {
997 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
998 f.debug_struct("Asset")
999 .field("state", &self.state)
1000 .field("process_id", &self.process_id())
1001 .field("region_id", &self.region_id)
1002 .field("capacity", &self.total_capacity())
1003 .field("commission_year", &self.commission_year)
1004 .finish()
1005 }
1006}
1007
1008pub fn check_region_year_valid_for_process(
1010 process: &Process,
1011 region_id: &RegionID,
1012 year: u32,
1013) -> Result<()> {
1014 ensure!(
1015 process.regions.contains(region_id),
1016 "Process {} does not operate in region {}",
1017 process.id,
1018 region_id
1019 );
1020 ensure!(
1021 process.active_for_year(year),
1022 "Process {} does not operate in the year {}",
1023 process.id,
1024 year
1025 );
1026 Ok(())
1027}
1028
1029pub fn check_capacity_valid_for_asset(capacity: Capacity) -> Result<()> {
1031 ensure!(
1032 capacity.is_finite() && capacity > Capacity(0.0),
1033 "Capacity must be a finite, positive number"
1034 );
1035 Ok(())
1036}
1037
1038#[derive(Clone, Debug)]
1045pub struct AssetRef(Rc<Asset>);
1046
1047impl AssetRef {
1048 pub fn make_mut(&mut self) -> &mut Asset {
1050 Rc::make_mut(&mut self.0)
1051 }
1052
1053 fn into_for_each_child<F>(mut self, next_group_id: &mut u32, mut f: F)
1066 where
1067 F: FnMut(Option<&AssetRef>, AssetRef),
1068 {
1069 assert!(
1070 matches!(
1071 self.state,
1072 AssetState::Future { .. } | AssetState::Selected { .. }
1073 ),
1074 "Assets with state {} cannot be divided. Only Future or Selected assets can be divided",
1075 self.state
1076 );
1077
1078 let AssetCapacity::Discrete(n_units, unit_size) = self.capacity() else {
1079 f(None, self);
1081 return;
1082 };
1083
1084 let child = AssetRef::from(Asset {
1086 capacity: Cell::new(AssetCapacity::Discrete(1, unit_size)),
1087 ..Asset::clone(&self)
1088 });
1089
1090 let agent_id = self.agent_id().unwrap().clone();
1092 self.make_mut().state = AssetState::Parent {
1093 agent_id,
1094 group_id: AssetGroupID(*next_group_id),
1095 };
1096 *next_group_id += 1;
1097
1098 for child in iter::repeat_n(child, n_units as usize) {
1100 f(Some(&self), child);
1101 }
1102 }
1103}
1104
1105impl From<Rc<Asset>> for AssetRef {
1106 fn from(value: Rc<Asset>) -> Self {
1107 Self(value)
1108 }
1109}
1110
1111impl From<Asset> for AssetRef {
1112 fn from(value: Asset) -> Self {
1113 Self::from(Rc::new(value))
1114 }
1115}
1116
1117impl From<AssetRef> for Rc<Asset> {
1118 fn from(value: AssetRef) -> Self {
1119 value.0
1120 }
1121}
1122
1123impl Deref for AssetRef {
1124 type Target = Asset;
1125
1126 fn deref(&self) -> &Self::Target {
1127 &self.0
1128 }
1129}
1130
1131impl PartialEq for AssetRef {
1132 fn eq(&self, other: &Self) -> bool {
1133 Rc::ptr_eq(&self.0.process, &other.0.process)
1136 && self.0.region_id == other.0.region_id
1137 && self.0.commission_year == other.0.commission_year
1138 && self.0.state == other.0.state
1139 }
1140}
1141
1142impl Eq for AssetRef {}
1143
1144impl Hash for AssetRef {
1145 fn hash<H: Hasher>(&self, state: &mut H) {
1153 match &self.0.state {
1154 AssetState::Commissioned { id, .. } => {
1155 id.hash(state);
1158 }
1159 AssetState::Candidate | AssetState::Selected { .. } | AssetState::Parent { .. } => {
1160 self.0.process.id.hash(state);
1161 self.0.region_id.hash(state);
1162 self.0.commission_year.hash(state);
1163 self.0.agent_id().hash(state);
1164 self.0.group_id().hash(state);
1165 }
1166 state => {
1167 panic!("Cannot hash {state} assets");
1169 }
1170 }
1171 }
1172}
1173
1174impl PartialOrd for AssetRef {
1175 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1176 Some(self.cmp(other))
1177 }
1178}
1179
1180impl Ord for AssetRef {
1181 fn cmp(&self, other: &Self) -> Ordering {
1182 self.id().unwrap().cmp(&other.id().unwrap())
1183 }
1184}
1185
1186pub trait AssetIterator<'a>: Iterator<Item = &'a AssetRef> + Sized
1188where
1189 Self: 'a,
1190{
1191 fn filter_agent(self, agent_id: &'a AgentID) -> impl Iterator<Item = &'a AssetRef> + 'a {
1193 self.filter(move |asset| asset.agent_id() == Some(agent_id))
1194 }
1195
1196 fn filter_primary_producers_of(
1198 self,
1199 commodity_id: &'a CommodityID,
1200 ) -> impl Iterator<Item = &'a AssetRef> + 'a {
1201 self.filter(move |asset| {
1202 asset
1203 .primary_output()
1204 .is_some_and(|flow| &flow.commodity.id == commodity_id)
1205 })
1206 }
1207
1208 fn filter_region(self, region_id: &'a RegionID) -> impl Iterator<Item = &'a AssetRef> + 'a {
1210 self.filter(move |asset| asset.region_id == *region_id)
1211 }
1212
1213 fn flows_for_commodity(
1215 self,
1216 commodity_id: &'a CommodityID,
1217 ) -> impl Iterator<Item = (&'a AssetRef, &'a ProcessFlow)> + 'a {
1218 self.filter_map(|asset| Some((asset, asset.get_flow(commodity_id)?)))
1219 }
1220}
1221
1222impl<'a, I> AssetIterator<'a> for I where I: Iterator<Item = &'a AssetRef> + Sized + 'a {}
1223
1224#[cfg(test)]
1225mod tests {
1226 use super::*;
1227 use crate::commodity::Commodity;
1228 use crate::fixture::{
1229 assert_error, assert_patched_runs_ok_simple, assert_validate_fails_with_simple, asset,
1230 asset_divisible, process, process_activity_limits_map, process_flows_map, region_id,
1231 svd_commodity, time_slice, time_slice_info,
1232 };
1233 use crate::patch::FilePatch;
1234 use crate::process::{FlowType, Process, ProcessFlow};
1235 use crate::region::RegionID;
1236 use crate::time_slice::{TimeSliceID, TimeSliceInfo};
1237 use crate::units::{
1238 ActivityPerCapacity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity,
1239 MoneyPerFlow,
1240 };
1241 use float_cmp::assert_approx_eq;
1242 use indexmap::indexmap;
1243 use rstest::{fixture, rstest};
1244 use std::rc::Rc;
1245
1246 #[rstest]
1247 fn get_input_cost_from_prices_works(
1248 region_id: RegionID,
1249 svd_commodity: Commodity,
1250 mut process: Process,
1251 time_slice: TimeSliceID,
1252 ) {
1253 let commodity_rc = Rc::new(svd_commodity);
1255 let process_flow = ProcessFlow {
1256 commodity: Rc::clone(&commodity_rc),
1257 coeff: FlowPerActivity(-2.0), kind: FlowType::Fixed,
1259 cost: MoneyPerFlow(0.0),
1260 };
1261 let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() };
1262 let process_flows_map = process_flows_map(process.regions.clone(), Rc::new(process_flows));
1263 process.flows = process_flows_map;
1264
1265 let asset =
1267 Asset::new_candidate(Rc::new(process), region_id.clone(), Capacity(1.0), 2020).unwrap();
1268
1269 let mut input_prices = CommodityPrices::default();
1271 input_prices.insert(&commodity_rc.id, ®ion_id, &time_slice, MoneyPerFlow(3.0));
1272
1273 let cost = asset.get_input_cost_from_prices(&input_prices, &time_slice);
1275 assert_approx_eq!(MoneyPerActivity, cost, MoneyPerActivity(6.0));
1277 }
1278
1279 #[rstest]
1280 #[case(Capacity(0.01))]
1281 #[case(Capacity(0.5))]
1282 #[case(Capacity(1.0))]
1283 #[case(Capacity(100.0))]
1284 fn asset_new_valid(process: Process, #[case] capacity: Capacity) {
1285 let agent_id = AgentID("agent1".into());
1286 let region_id = RegionID("GBR".into());
1287 let asset = Asset::new_future(agent_id, process.into(), region_id, capacity, 2015).unwrap();
1288 assert!(asset.id().is_none());
1289 }
1290
1291 #[rstest]
1292 #[case(Capacity(0.0))]
1293 #[case(Capacity(-0.01))]
1294 #[case(Capacity(-1.0))]
1295 #[case(Capacity(f64::NAN))]
1296 #[case(Capacity(f64::INFINITY))]
1297 #[case(Capacity(f64::NEG_INFINITY))]
1298 fn asset_new_invalid_capacity(process: Process, #[case] capacity: Capacity) {
1299 let agent_id = AgentID("agent1".into());
1300 let region_id = RegionID("GBR".into());
1301 assert_error!(
1302 Asset::new_future(agent_id, process.into(), region_id, capacity, 2015),
1303 "Capacity must be a finite, positive number"
1304 );
1305 }
1306
1307 #[rstest]
1308 fn asset_new_invalid_commission_year(process: Process) {
1309 let agent_id = AgentID("agent1".into());
1310 let region_id = RegionID("GBR".into());
1311 assert_error!(
1312 Asset::new_future(agent_id, process.into(), region_id, Capacity(1.0), 2007),
1313 "Process process1 does not operate in the year 2007"
1314 );
1315 }
1316
1317 #[rstest]
1318 fn asset_new_invalid_region(process: Process) {
1319 let agent_id = AgentID("agent1".into());
1320 let region_id = RegionID("FRA".into());
1321 assert_error!(
1322 Asset::new_future(agent_id, process.into(), region_id, Capacity(1.0), 2015),
1323 "Process process1 does not operate in region FRA"
1324 );
1325 }
1326
1327 #[fixture]
1328 fn process_with_activity_limits(
1329 mut process: Process,
1330 time_slice_info: TimeSliceInfo,
1331 time_slice: TimeSliceID,
1332 ) -> Process {
1333 let mut activity_limits = ActivityLimits::new_with_full_availability(&time_slice_info);
1335 activity_limits.add_time_slice_limit(time_slice, Dimensionless(0.1)..=Dimensionless(0.5));
1336 process.activity_limits =
1337 process_activity_limits_map(process.regions.clone(), activity_limits);
1338
1339 process.capacity_to_activity = ActivityPerCapacity(2.0);
1341 process
1342 }
1343
1344 #[fixture]
1345 fn asset_with_activity_limits(process_with_activity_limits: Process) -> Asset {
1346 Asset::new_future(
1347 "agent1".into(),
1348 Rc::new(process_with_activity_limits),
1349 "GBR".into(),
1350 Capacity(2.0),
1351 2010,
1352 )
1353 .unwrap()
1354 }
1355
1356 #[rstest]
1357 fn asset_get_activity_per_capacity_limits(
1358 asset_with_activity_limits: Asset,
1359 time_slice: TimeSliceID,
1360 ) {
1361 assert_eq!(
1363 asset_with_activity_limits.get_activity_per_capacity_limits(&time_slice),
1364 ActivityPerCapacity(0.2)..=ActivityPerCapacity(1.0)
1365 );
1366 }
1367
1368 #[rstest]
1369 #[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 into_for_each_child_divisible(
1374 mut process: Process,
1375 #[case] capacity: Capacity,
1376 #[case] unit_size: Capacity,
1377 #[case] n_expected_children: usize,
1378 ) {
1379 process.unit_size = Some(unit_size);
1380 let asset = AssetRef::from(
1381 Asset::new_future(
1382 "agent1".into(),
1383 Rc::new(process),
1384 "GBR".into(),
1385 capacity,
1386 2010,
1387 )
1388 .unwrap(),
1389 );
1390
1391 let mut count = 0;
1392 let mut total_child_capacity = Capacity(0.0);
1393 asset.clone().into_for_each_child(&mut 0, |parent, child| {
1394 assert!(parent.is_some_and(|parent| matches!(parent.state, AssetState::Parent { .. })));
1395
1396 assert_eq!(
1398 child.total_capacity(),
1399 unit_size,
1400 "Child capacity should equal unit_size"
1401 );
1402
1403 total_child_capacity += child.total_capacity();
1404 count += 1;
1405 });
1406 assert_eq!(count, n_expected_children, "Unexpected number of children");
1407
1408 assert!(
1410 total_child_capacity >= asset.total_capacity(),
1411 "Total capacity should be >= parent capacity"
1412 );
1413 }
1414
1415 #[rstest]
1416 fn into_for_each_child_nondivisible(asset: Asset) {
1417 assert!(
1418 asset.process.unit_size.is_none(),
1419 "Asset should be non-divisible"
1420 );
1421
1422 let asset = AssetRef::from(asset);
1423 let mut count = 0;
1424 asset.clone().into_for_each_child(&mut 0, |parent, child| {
1425 assert!(parent.is_none());
1426 assert_eq!(child, asset);
1427 count += 1;
1428 });
1429 assert_eq!(count, 1);
1430 }
1431
1432 #[rstest]
1433 fn asset_commission(process: Process) {
1434 let process_rc = Rc::new(process);
1436 let mut asset1 = Asset::new_future(
1437 "agent1".into(),
1438 Rc::clone(&process_rc),
1439 "GBR".into(),
1440 Capacity(1.0),
1441 2020,
1442 )
1443 .unwrap();
1444 asset1.commission(AssetID(1), None, "");
1445 assert!(asset1.is_commissioned());
1446 assert_eq!(asset1.id(), Some(AssetID(1)));
1447
1448 let mut asset2 = Asset::new_selected(
1450 "agent1".into(),
1451 Rc::clone(&process_rc),
1452 "GBR".into(),
1453 Capacity(1.0),
1454 2020,
1455 )
1456 .unwrap();
1457 asset2.commission(AssetID(2), None, "");
1458 assert!(asset2.is_commissioned());
1459 assert_eq!(asset2.id(), Some(AssetID(2)));
1460 }
1461
1462 #[rstest]
1463 #[case::commission_during_process_lifetime(2024, 2024)]
1464 #[case::decommission_after_process_lifetime_ends(2026, 2025)]
1465 fn asset_decommission(
1466 #[case] requested_decommission_year: u32,
1467 #[case] expected_decommission_year: u32,
1468 process: Process,
1469 ) {
1470 let process_rc = Rc::new(process);
1472 let mut asset = Asset::new_future(
1473 "agent1".into(),
1474 Rc::clone(&process_rc),
1475 "GBR".into(),
1476 Capacity(1.0),
1477 2020,
1478 )
1479 .unwrap();
1480 asset.commission(AssetID(1), None, "");
1481 assert!(asset.is_commissioned());
1482 assert_eq!(asset.id(), Some(AssetID(1)));
1483
1484 asset.decommission(requested_decommission_year, "");
1486 assert!(!asset.is_commissioned());
1487 assert_eq!(asset.decommission_year(), Some(expected_decommission_year));
1488 }
1489
1490 #[rstest]
1491 #[case::decommission_after_predefined_max_year(2026, 2025, Some(2025))]
1492 #[case::decommission_before_predefined_max_year(2024, 2024, Some(2025))]
1493 #[case::decommission_during_process_lifetime_end_no_max_year(2024, 2024, None)]
1494 #[case::decommission_after_process_lifetime_end_no_max_year(2026, 2025, None)]
1495 fn asset_decommission_with_max_decommission_year_predefined(
1496 #[case] requested_decommission_year: u32,
1497 #[case] expected_decommission_year: u32,
1498 #[case] max_decommission_year: Option<u32>,
1499 process: Process,
1500 ) {
1501 let process_rc = Rc::new(process);
1503 let mut asset = Asset::new_future_with_max_decommission(
1504 "agent1".into(),
1505 Rc::clone(&process_rc),
1506 "GBR".into(),
1507 Capacity(1.0),
1508 2020,
1509 max_decommission_year,
1510 )
1511 .unwrap();
1512 asset.commission(AssetID(1), None, "");
1513 assert!(asset.is_commissioned());
1514 assert_eq!(asset.id(), Some(AssetID(1)));
1515
1516 asset.decommission(requested_decommission_year, "");
1518 assert!(!asset.is_commissioned());
1519 assert_eq!(asset.decommission_year(), Some(expected_decommission_year));
1520 }
1521
1522 #[rstest]
1523 fn asset_decommission_divisible(asset_divisible: Asset) {
1524 let asset = AssetRef::from(asset_divisible);
1525 let original_capacity = asset.capacity();
1526
1527 let mut children = Vec::new();
1529 let mut next_id = 0;
1530 asset.into_for_each_child(&mut 0, |parent, mut child| {
1531 child
1532 .make_mut()
1533 .commission(AssetID(next_id), parent.cloned(), "");
1534 next_id += 1;
1535 children.push(child);
1536 });
1537
1538 let parent = children[0].parent().unwrap().clone();
1539 assert_eq!(parent.capacity(), original_capacity);
1540 children[0].make_mut().decommission(2020, "");
1541
1542 let AssetCapacity::Discrete(original_units, original_unit_size) = original_capacity else {
1543 panic!("Capacity type should be discrete");
1544 };
1545 assert_eq!(
1546 parent.capacity(),
1547 AssetCapacity::Discrete(original_units - 1, original_unit_size)
1548 );
1549 }
1550
1551 #[rstest]
1552 #[should_panic(expected = "Assets with state Candidate cannot be commissioned")]
1553 fn commission_wrong_states(process: Process) {
1554 let mut asset =
1555 Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap();
1556 asset.commission(AssetID(1), None, "");
1557 }
1558
1559 #[rstest]
1560 #[should_panic(expected = "Cannot decommission an asset that hasn't been commissioned")]
1561 fn decommission_wrong_state(process: Process) {
1562 let mut asset =
1563 Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap();
1564 asset.decommission(2025, "");
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 fn max_installable_capacity(mut process: Process, region_id: RegionID) {
1618 process.investment_constraints.insert(
1620 (region_id.clone(), 2015),
1621 Rc::new(crate::process::ProcessInvestmentConstraint {
1622 addition_limit: Some(Capacity(3.0)),
1623 }),
1624 );
1625 let process_rc = Rc::new(process);
1626
1627 let asset =
1629 Asset::new_candidate(process_rc.clone(), region_id.clone(), Capacity(1.0), 2015)
1630 .unwrap();
1631
1632 let result = asset.max_installable_capacity(Dimensionless(0.5));
1634 assert_eq!(result, Some(AssetCapacity::Continuous(Capacity(1.5))));
1635 }
1636}