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)]
73pub enum AssetState {
74 Commissioned {
76 id: AssetID,
78 agent_id: AgentID,
80 mothballed_year: Option<u32>,
82 parent: Option<AssetRef>,
86 },
87 Future {
89 agent_id: AgentID,
91 },
92 Selected {
94 agent_id: AgentID,
96 },
97 Parent {
102 agent_id: AgentID,
104 group_id: AssetGroupID,
106 },
107 Candidate,
109}
110
111#[derive(Clone)]
113pub struct Asset {
114 state: AssetState,
116 process: Rc<Process>,
118 activity_limits: Rc<ActivityLimits>,
120 flows: Rc<IndexMap<CommodityID, ProcessFlow>>,
122 process_parameter: Rc<ProcessParameter>,
124 region_id: RegionID,
126 capacity: Cell<AssetCapacity>,
128 commission_year: u32,
130 max_decommission_year: u32,
132}
133
134impl Asset {
135 pub fn new_candidate(
137 process: Rc<Process>,
138 region_id: RegionID,
139 capacity: Capacity,
140 commission_year: u32,
141 ) -> Result<Self> {
142 let unit_size = process.unit_size;
143 Self::new_with_state(
144 AssetState::Candidate,
145 process,
146 region_id,
147 AssetCapacity::from_capacity(capacity, unit_size),
148 commission_year,
149 None,
150 )
151 }
152
153 pub fn new_candidate_for_dispatch(
159 process: Rc<Process>,
160 region_id: RegionID,
161 capacity: Capacity,
162 commission_year: u32,
163 ) -> Result<Self> {
164 Self::new_with_state(
165 AssetState::Candidate,
166 process,
167 region_id,
168 AssetCapacity::Continuous(capacity),
169 commission_year,
170 None,
171 )
172 }
173
174 pub fn new_candidate_from_commissioned(asset: &Asset) -> Self {
176 assert!(asset.is_commissioned(), "Asset must be commissioned");
177
178 Self {
179 state: AssetState::Candidate,
180 ..asset.clone()
181 }
182 }
183
184 pub fn new_future_with_max_decommission(
186 agent_id: AgentID,
187 process: Rc<Process>,
188 region_id: RegionID,
189 capacity: Capacity,
190 commission_year: u32,
191 max_decommission_year: Option<u32>,
192 ) -> Result<Self> {
193 check_capacity_valid_for_asset(capacity)?;
194 let unit_size = process.unit_size;
195 Self::new_with_state(
196 AssetState::Future { agent_id },
197 process,
198 region_id,
199 AssetCapacity::from_capacity(capacity, unit_size),
200 commission_year,
201 max_decommission_year,
202 )
203 }
204
205 pub fn new_future(
207 agent_id: AgentID,
208 process: Rc<Process>,
209 region_id: RegionID,
210 capacity: Capacity,
211 commission_year: u32,
212 ) -> Result<Self> {
213 Self::new_future_with_max_decommission(
214 agent_id,
215 process,
216 region_id,
217 capacity,
218 commission_year,
219 None,
220 )
221 }
222
223 #[cfg(test)]
228 fn new_selected(
229 agent_id: AgentID,
230 process: Rc<Process>,
231 region_id: RegionID,
232 capacity: Capacity,
233 commission_year: u32,
234 ) -> Result<Self> {
235 let unit_size = process.unit_size;
236 Self::new_with_state(
237 AssetState::Selected { agent_id },
238 process,
239 region_id,
240 AssetCapacity::from_capacity(capacity, unit_size),
241 commission_year,
242 None,
243 )
244 }
245
246 #[cfg(test)]
251 pub fn new_commissioned(
252 agent_id: AgentID,
253 process: Rc<Process>,
254 region_id: RegionID,
255 capacity: Capacity,
256 commission_year: u32,
257 ) -> Result<Self> {
258 let unit_size = process.unit_size;
259 Self::new_with_state(
260 AssetState::Commissioned {
261 id: AssetID(0),
262 agent_id,
263 mothballed_year: None,
264 parent: None,
265 },
266 process,
267 region_id,
268 AssetCapacity::from_capacity(capacity, unit_size),
269 commission_year,
270 None,
271 )
272 }
273
274 fn new_with_state(
276 state: AssetState,
277 process: Rc<Process>,
278 region_id: RegionID,
279 capacity: AssetCapacity,
280 commission_year: u32,
281 max_decommission_year: Option<u32>,
282 ) -> Result<Self> {
283 check_region_year_valid_for_process(&process, ®ion_id, commission_year)?;
284 ensure!(
285 capacity.total_capacity() >= Capacity(0.0),
286 "Capacity must be non-negative"
287 );
288
289 let key = (region_id.clone(), commission_year);
295 let activity_limits = process
296 .activity_limits
297 .get(&key)
298 .with_context(|| {
299 format!(
300 "No process availabilities supplied for process {} in region {} in year {}. \
301 You should update process_availabilities.csv.",
302 &process.id, region_id, commission_year
303 )
304 })?
305 .clone();
306 let flows = process
307 .flows
308 .get(&key)
309 .with_context(|| {
310 format!(
311 "No commodity flows supplied for process {} in region {} in year {}. \
312 You should update process_flows.csv.",
313 &process.id, region_id, commission_year
314 )
315 })?
316 .clone();
317 let process_parameter = process
318 .parameters
319 .get(&key)
320 .with_context(|| {
321 format!(
322 "No process parameters supplied for process {} in region {} in year {}. \
323 You should update process_parameters.csv.",
324 &process.id, region_id, commission_year
325 )
326 })?
327 .clone();
328
329 let max_decommission_year =
330 max_decommission_year.unwrap_or(commission_year + process_parameter.lifetime);
331 ensure!(
332 max_decommission_year > commission_year,
333 "Max decommission year must be greater than commission year"
334 );
335
336 Ok(Self {
337 state,
338 process,
339 activity_limits,
340 flows,
341 process_parameter,
342 region_id,
343 capacity: Cell::new(capacity),
344 commission_year,
345 max_decommission_year,
346 })
347 }
348
349 pub fn state(&self) -> &AssetState {
351 &self.state
352 }
353
354 pub fn process_parameter(&self) -> &ProcessParameter {
356 &self.process_parameter
357 }
358
359 pub fn max_decommission_year(&self) -> u32 {
361 self.max_decommission_year
362 }
363
364 pub fn get_activity_per_capacity_limits(
366 &self,
367 time_slice: &TimeSliceID,
368 ) -> RangeInclusive<ActivityPerCapacity> {
369 let limits = &self.activity_limits.get_limit_for_time_slice(time_slice);
370 let cap2act = self.process.capacity_to_activity;
371 (cap2act * *limits.start())..=(cap2act * *limits.end())
372 }
373
374 pub fn get_activity_limits_for_selection(
376 &self,
377 time_slice_selection: &TimeSliceSelection,
378 ) -> RangeInclusive<Activity> {
379 let activity_per_capacity_limits = self.activity_limits.get_limit(time_slice_selection);
380 let cap2act = self.process.capacity_to_activity;
381 let max_activity = self.total_capacity() * cap2act;
382 let lb = max_activity * *activity_per_capacity_limits.start();
383 let ub = max_activity * *activity_per_capacity_limits.end();
384 lb..=ub
385 }
386
387 pub fn iter_activity_limits(
389 &self,
390 ) -> impl Iterator<Item = (TimeSliceSelection, RangeInclusive<Activity>)> + '_ {
391 let max_act = self.max_activity();
392 self.activity_limits
393 .iter_limits()
394 .map(move |(ts_sel, limit)| {
395 (
396 ts_sel,
397 (max_act * *limit.start())..=(max_act * *limit.end()),
398 )
399 })
400 }
401
402 pub fn iter_activity_per_capacity_limits(
404 &self,
405 ) -> impl Iterator<Item = (TimeSliceSelection, RangeInclusive<ActivityPerCapacity>)> + '_ {
406 let cap2act = self.process.capacity_to_activity;
407 self.activity_limits
408 .iter_limits()
409 .map(move |(ts_sel, limit)| {
410 (
411 ts_sel,
412 (cap2act * *limit.start())..=(cap2act * *limit.end()),
413 )
414 })
415 }
416
417 pub fn get_total_output_per_activity(&self) -> FlowPerActivity {
424 self.iter_output_flows().map(|flow| flow.coeff).sum()
425 }
426
427 pub fn get_operating_cost(&self, year: u32, time_slice: &TimeSliceID) -> MoneyPerActivity {
429 let flows_cost = self
431 .iter_flows()
432 .map(|flow| flow.get_total_cost_per_activity(&self.region_id, year, time_slice))
433 .sum();
434
435 self.process_parameter.variable_operating_cost + flows_cost
436 }
437
438 pub fn get_revenue_from_flows(
442 &self,
443 prices: &CommodityPrices,
444 time_slice: &TimeSliceID,
445 ) -> MoneyPerActivity {
446 self.get_revenue_from_flows_with_filter(prices, time_slice, |_| true)
447 }
448
449 pub fn get_revenue_from_flows_excluding_primary(
453 &self,
454 prices: &CommodityPrices,
455 time_slice: &TimeSliceID,
456 ) -> MoneyPerActivity {
457 let excluded_commodity = self.primary_output().map(|flow| &flow.commodity.id);
458
459 self.get_revenue_from_flows_with_filter(prices, time_slice, |flow| {
460 excluded_commodity.is_none_or(|commodity_id| commodity_id != &flow.commodity.id)
461 })
462 }
463
464 pub fn get_input_cost_from_prices(
468 &self,
469 prices: &CommodityPrices,
470 time_slice: &TimeSliceID,
471 ) -> MoneyPerActivity {
472 -self.get_revenue_from_flows_with_filter(prices, time_slice, |x| {
474 x.direction() == FlowDirection::Input
475 })
476 }
477
478 fn get_revenue_from_flows_with_filter<F>(
483 &self,
484 prices: &CommodityPrices,
485 time_slice: &TimeSliceID,
486 mut filter_for_flows: F,
487 ) -> MoneyPerActivity
488 where
489 F: FnMut(&ProcessFlow) -> bool,
490 {
491 self.iter_flows()
492 .filter(|flow| filter_for_flows(flow))
493 .map(|flow| {
494 flow.coeff
495 * prices
496 .get(&flow.commodity.id, &self.region_id, time_slice)
497 .unwrap_or(MoneyPerFlow(0.0))
498 })
499 .sum()
500 }
501
502 fn get_generic_activity_cost(
507 &self,
508 prices: &CommodityPrices,
509 year: u32,
510 time_slice: &TimeSliceID,
511 ) -> MoneyPerActivity {
512 let cost_of_inputs = self.get_input_cost_from_prices(prices, time_slice);
514
515 let excludes_sed_svd_output = |flow: &&ProcessFlow| {
517 !(flow.direction() == FlowDirection::Output
518 && matches!(
519 flow.commodity.kind,
520 CommodityType::SupplyEqualsDemand | CommodityType::ServiceDemand
521 ))
522 };
523 let flow_costs = self
524 .iter_flows()
525 .filter(excludes_sed_svd_output)
526 .map(|flow| flow.get_total_cost_per_activity(&self.region_id, year, time_slice))
527 .sum();
528
529 cost_of_inputs + flow_costs + self.process_parameter.variable_operating_cost
530 }
531
532 pub fn iter_marginal_costs_with_filter<'a>(
540 &'a self,
541 prices: &'a CommodityPrices,
542 year: u32,
543 time_slice: &'a TimeSliceID,
544 filter: impl Fn(&CommodityID) -> bool + 'a,
545 ) -> Box<dyn Iterator<Item = (CommodityID, MoneyPerFlow)> + 'a> {
546 let mut output_flows_iter = self
548 .iter_output_flows()
549 .filter(move |flow| filter(&flow.commodity.id))
550 .peekable();
551
552 if output_flows_iter.peek().is_none() {
554 return Box::new(std::iter::empty::<(CommodityID, MoneyPerFlow)>());
555 }
556
557 let generic_activity_cost = self.get_generic_activity_cost(prices, year, time_slice);
562
563 let total_output_per_activity = self.get_total_output_per_activity();
568 assert!(total_output_per_activity > FlowPerActivity::EPSILON); let generic_cost_per_flow = generic_activity_cost / total_output_per_activity;
570
571 Box::new(output_flows_iter.map(move |flow| {
573 let commodity_specific_costs_per_flow =
575 flow.get_total_cost_per_flow(&self.region_id, year, time_slice);
576
577 let marginal_cost = generic_cost_per_flow + commodity_specific_costs_per_flow;
579 (flow.commodity.id.clone(), marginal_cost)
580 }))
581 }
582
583 pub fn iter_marginal_costs<'a>(
587 &'a self,
588 prices: &'a CommodityPrices,
589 year: u32,
590 time_slice: &'a TimeSliceID,
591 ) -> Box<dyn Iterator<Item = (CommodityID, MoneyPerFlow)> + 'a> {
592 self.iter_marginal_costs_with_filter(prices, year, time_slice, move |_| true)
593 }
594
595 pub fn get_annual_capital_cost_per_capacity(&self) -> MoneyPerCapacity {
597 let capital_cost = self.process_parameter.capital_cost;
598 let lifetime = self.process_parameter.lifetime;
599 let discount_rate = self.process_parameter.discount_rate;
600 annual_capital_cost(capital_cost, lifetime, discount_rate)
601 }
602
603 pub fn get_annual_fixed_costs_per_activity(
608 &self,
609 annual_activity: Activity,
610 ) -> MoneyPerActivity {
611 let annual_capital_cost_per_capacity = self.get_annual_capital_cost_per_capacity();
612 let annual_fixed_opex = self.process_parameter.fixed_operating_cost * Year(1.0);
613 let total_annual_fixed_costs =
614 (annual_capital_cost_per_capacity + annual_fixed_opex) * self.total_capacity();
615 assert!(
616 annual_activity > Activity::EPSILON,
617 "Cannot calculate annual fixed costs per activity for an asset with zero annual activity"
618 );
619 total_annual_fixed_costs / annual_activity
620 }
621
622 pub fn get_annual_fixed_costs_per_flow(&self, annual_activity: Activity) -> MoneyPerFlow {
627 let annual_fixed_costs_per_activity =
628 self.get_annual_fixed_costs_per_activity(annual_activity);
629 let total_output_per_activity = self.get_total_output_per_activity();
630 assert!(total_output_per_activity > FlowPerActivity::EPSILON); annual_fixed_costs_per_activity / total_output_per_activity
632 }
633
634 pub fn max_activity(&self) -> Activity {
636 self.total_capacity() * self.process.capacity_to_activity
637 }
638
639 pub fn get_flow(&self, commodity_id: &CommodityID) -> Option<&ProcessFlow> {
641 self.flows.get(commodity_id)
642 }
643
644 pub fn iter_flows(&self) -> impl Iterator<Item = &ProcessFlow> {
646 self.flows.values()
647 }
648
649 pub fn iter_output_flows(&self) -> impl Iterator<Item = &ProcessFlow> {
651 self.flows.values().filter(|flow| {
652 flow.direction() == FlowDirection::Output
653 && matches!(
654 flow.commodity.kind,
655 CommodityType::SupplyEqualsDemand | CommodityType::ServiceDemand
656 )
657 })
658 }
659
660 pub fn primary_output(&self) -> Option<&ProcessFlow> {
662 self.process
663 .primary_output
664 .as_ref()
665 .map(|commodity_id| &self.flows[commodity_id])
666 }
667
668 pub fn primary_output_commodity(&self) -> Option<&CommodityID> {
670 self.process.primary_output.as_ref()
671 }
672
673 pub fn is_commissioned(&self) -> bool {
675 matches!(&self.state, AssetState::Commissioned { .. })
676 }
677
678 pub fn commission_year(&self) -> u32 {
680 self.commission_year
681 }
682
683 pub fn region_id(&self) -> &RegionID {
685 &self.region_id
686 }
687
688 pub fn process(&self) -> &Process {
690 &self.process
691 }
692
693 pub fn process_id(&self) -> &ProcessID {
695 &self.process.id
696 }
697
698 pub fn id(&self) -> Option<AssetID> {
700 match &self.state {
701 AssetState::Commissioned { id, .. } => Some(*id),
702 _ => None,
703 }
704 }
705
706 pub fn parent(&self) -> Option<&AssetRef> {
708 match &self.state {
709 AssetState::Commissioned { parent, .. } => parent.as_ref(),
710 _ => None,
711 }
712 }
713
714 pub fn is_parent(&self) -> bool {
716 matches!(self.state, AssetState::Parent { .. })
717 }
718
719 pub fn num_children(&self) -> Option<u32> {
723 match &self.state {
724 AssetState::Parent { .. } => Some(self.capacity().n_units().unwrap()),
725 _ => None,
726 }
727 }
728
729 pub fn group_id(&self) -> Option<AssetGroupID> {
731 match &self.state {
732 AssetState::Commissioned { parent, .. } => {
733 parent
735 .as_ref()
736 .map(|parent| parent.group_id().unwrap())
738 }
739 AssetState::Parent { group_id, .. } => Some(*group_id),
740 _ => None,
741 }
742 }
743
744 pub fn agent_id(&self) -> Option<&AgentID> {
746 match &self.state {
747 AssetState::Commissioned { agent_id, .. }
748 | AssetState::Future { agent_id }
749 | AssetState::Selected { agent_id }
750 | AssetState::Parent { agent_id, .. } => Some(agent_id),
751 AssetState::Candidate => None,
752 }
753 }
754
755 pub fn capacity(&self) -> AssetCapacity {
757 self.capacity.get()
758 }
759
760 pub fn total_capacity(&self) -> Capacity {
762 self.capacity().total_capacity()
763 }
764
765 pub fn set_capacity(&mut self, capacity: AssetCapacity) {
767 assert!(
768 matches!(
769 self.state,
770 AssetState::Candidate | AssetState::Selected { .. }
771 ),
772 "set_capacity can only be called on Candidate or Selected assets"
773 );
774 assert!(
775 capacity.total_capacity() >= Capacity(0.0),
776 "Capacity must be >= 0"
777 );
778 self.capacity().assert_same_type(capacity);
779
780 self.capacity.set(capacity);
783 }
784
785 pub fn increase_capacity(&mut self, capacity: AssetCapacity) {
787 assert!(
788 self.state == AssetState::Candidate,
789 "increase_capacity can only be called on Candidate assets"
790 );
791 assert!(
792 capacity.total_capacity() > Capacity(0.0),
793 "Capacity increase must be positive"
794 );
795
796 self.capacity.update(|c| c + capacity);
799 }
800
801 fn decrement_unit_count(&self) {
809 let AssetCapacity::Discrete(n_units, unit_size) = self.capacity() else {
810 panic!("Cannot decrement unit count of non-divisible asset");
811 };
812 assert!(n_units > 0, "Unit count has dropped below zero");
813
814 self.capacity
815 .set(AssetCapacity::Discrete(n_units - 1, unit_size));
816 }
817
818 fn decommission(&mut self, reason: &str) {
820 let (id, agent_id, parent) = match &self.state {
821 AssetState::Commissioned {
822 id,
823 agent_id,
824 parent,
825 ..
826 } => (*id, agent_id.clone(), parent),
827 _ => panic!("Cannot decommission an asset that hasn't been commissioned"),
828 };
829 debug!(
830 "Decommissioning '{}' asset (ID: {}) for agent '{}' (reason: {})",
831 self.process_id(),
832 id,
833 agent_id,
834 reason
835 );
836
837 if let Some(parent) = parent {
839 parent.decrement_unit_count();
840 }
841 }
842
843 fn commission(&mut self, id: AssetID, parent: Option<AssetRef>, reason: &str) {
854 let agent_id = match &self.state {
855 AssetState::Future { agent_id } | AssetState::Selected { agent_id } => agent_id,
856 state => panic!("Assets with state {state} cannot be commissioned"),
857 };
858 debug!(
859 "Commissioning '{}' asset (ID: {}, capacity: {}) for agent '{}' (reason: {})",
860 self.process_id(),
861 id,
862 self.total_capacity(),
863 agent_id,
864 reason
865 );
866 self.state = AssetState::Commissioned {
867 id,
868 agent_id: agent_id.clone(),
869 mothballed_year: None,
870 parent,
871 };
872 }
873
874 pub fn select_candidate_for_investment(&mut self, agent_id: AgentID) {
876 assert!(
877 self.state == AssetState::Candidate,
878 "select_candidate_for_investment can only be called on Candidate assets"
879 );
880 check_capacity_valid_for_asset(self.total_capacity()).unwrap();
881 self.state = AssetState::Selected { agent_id };
882 }
883
884 pub fn mothball(&mut self, year: u32) {
886 let (id, agent_id, parent) = match &self.state {
887 AssetState::Commissioned {
888 id,
889 agent_id,
890 parent,
891 ..
892 } => (*id, agent_id.clone(), parent.clone()),
893 _ => panic!("Cannot mothball an asset that hasn't been commissioned"),
894 };
895 self.state = AssetState::Commissioned {
896 id,
897 agent_id,
898 mothballed_year: Some(year),
899 parent,
900 };
901 }
902
903 pub fn unmothball(&mut self) {
905 let (id, agent_id, parent) = match &self.state {
906 AssetState::Commissioned {
907 id,
908 agent_id,
909 parent,
910 ..
911 } => (*id, agent_id.clone(), parent.clone()),
912 _ => panic!("Cannot unmothball an asset that hasn't been commissioned"),
913 };
914 self.state = AssetState::Commissioned {
915 id,
916 agent_id,
917 mothballed_year: None,
918 parent,
919 };
920 }
921
922 pub fn get_mothballed_year(&self) -> Option<u32> {
924 let AssetState::Commissioned {
925 mothballed_year, ..
926 } = &self.state
927 else {
928 panic!("Cannot get mothballed year for an asset that hasn't been commissioned")
929 };
930 *mothballed_year
931 }
932
933 pub fn unit_size(&self) -> Option<Capacity> {
935 match self.capacity() {
936 AssetCapacity::Discrete(_, size) => Some(size),
937 AssetCapacity::Continuous(_) => None,
938 }
939 }
940
941 pub fn max_installable_capacity(
950 &self,
951 commodity_portion: Dimensionless,
952 ) -> Option<AssetCapacity> {
953 assert!(
954 !self.is_commissioned(),
955 "max_installable_capacity can only be called on uncommissioned assets"
956 );
957 assert!(
958 commodity_portion >= Dimensionless(0.0) && commodity_portion <= Dimensionless(1.0),
959 "commodity_portion must be between 0 and 1 inclusive"
960 );
961
962 self.process
963 .investment_constraints
964 .get(&(self.region_id.clone(), self.commission_year))
965 .and_then(|c| c.get_addition_limit().map(|l| l * commodity_portion))
966 .map(|limit| AssetCapacity::from_capacity_floor(limit, self.unit_size()))
967 }
968}
969
970#[allow(clippy::missing_fields_in_debug)]
971impl std::fmt::Debug for Asset {
972 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
973 f.debug_struct("Asset")
974 .field("state", &self.state)
975 .field("process_id", &self.process_id())
976 .field("region_id", &self.region_id)
977 .field("capacity", &self.total_capacity())
978 .field("commission_year", &self.commission_year)
979 .finish()
980 }
981}
982
983pub fn check_region_year_valid_for_process(
985 process: &Process,
986 region_id: &RegionID,
987 year: u32,
988) -> Result<()> {
989 ensure!(
990 process.regions.contains(region_id),
991 "Process {} does not operate in region {}",
992 process.id,
993 region_id
994 );
995 ensure!(
996 process.active_for_year(year),
997 "Process {} does not operate in the year {}",
998 process.id,
999 year
1000 );
1001 Ok(())
1002}
1003
1004pub fn check_capacity_valid_for_asset(capacity: Capacity) -> Result<()> {
1006 ensure!(
1007 capacity.is_finite() && capacity > Capacity(0.0),
1008 "Capacity must be a finite, positive number"
1009 );
1010 Ok(())
1011}
1012
1013#[derive(Clone, Debug)]
1019pub struct AssetRef(Rc<Asset>);
1020
1021impl AssetRef {
1022 pub fn make_mut(&mut self) -> &mut Asset {
1024 Rc::make_mut(&mut self.0)
1025 }
1026
1027 fn get_asset_cmp(&self) -> AssetCmp<'_> {
1029 if let Some(id) = self.id() {
1030 AssetCmp::WithID(id)
1031 } else {
1032 AssetCmp::WithoutID((
1033 self.process_id(),
1034 self.region_id(),
1035 self.commission_year,
1036 self.agent_id(),
1037 self.group_id(),
1038 ))
1039 }
1040 }
1041
1042 fn into_for_each_child<F>(mut self, next_group_id: &mut u32, mut f: F)
1055 where
1056 F: FnMut(Option<&AssetRef>, AssetRef),
1057 {
1058 assert!(
1059 matches!(
1060 self.state,
1061 AssetState::Future { .. } | AssetState::Selected { .. }
1062 ),
1063 "Assets with state {} cannot be divided. Only Future or Selected assets can be divided",
1064 self.state
1065 );
1066
1067 let AssetCapacity::Discrete(n_units, unit_size) = self.capacity() else {
1068 f(None, self);
1070 return;
1071 };
1072
1073 let child = AssetRef::from(Asset {
1075 capacity: Cell::new(AssetCapacity::Discrete(1, unit_size)),
1076 ..Asset::clone(&self)
1077 });
1078
1079 let agent_id = self.agent_id().unwrap().clone();
1081 self.make_mut().state = AssetState::Parent {
1082 agent_id,
1083 group_id: AssetGroupID(*next_group_id),
1084 };
1085 *next_group_id += 1;
1086
1087 for child in iter::repeat_n(child, n_units as usize) {
1089 f(Some(&self), child);
1090 }
1091 }
1092
1093 pub fn make_partial_parent(&self, num_units: u32) -> Self {
1100 assert!(
1101 self.is_parent(),
1102 "Cannot make a partial parent from a non-parent asset"
1103 );
1104 assert!(
1105 num_units > 0,
1106 "Cannot make a partial parent with zero units"
1107 );
1108
1109 let (max_num_units, unit_size) = match self.capacity() {
1110 AssetCapacity::Discrete(max_num_units, unit_size) => (max_num_units, unit_size),
1111 AssetCapacity::Continuous(_) => unreachable!(),
1113 };
1114 match num_units.cmp(&max_num_units) {
1115 Ordering::Less => Self::from(Asset {
1117 capacity: Cell::new(AssetCapacity::Discrete(num_units, unit_size)),
1118 ..Asset::clone(self)
1119 }),
1120 Ordering::Equal => self.clone(),
1122 Ordering::Greater => {
1123 panic!("Cannot make a partial parent with more units than original")
1124 }
1125 }
1126 }
1127}
1128
1129impl From<Rc<Asset>> for AssetRef {
1130 fn from(value: Rc<Asset>) -> Self {
1131 Self(value)
1132 }
1133}
1134
1135impl From<Asset> for AssetRef {
1136 fn from(value: Asset) -> Self {
1137 Self::from(Rc::new(value))
1138 }
1139}
1140
1141impl From<AssetRef> for Rc<Asset> {
1142 fn from(value: AssetRef) -> Self {
1143 value.0
1144 }
1145}
1146
1147impl Deref for AssetRef {
1148 type Target = Asset;
1149
1150 fn deref(&self) -> &Self::Target {
1151 &self.0
1152 }
1153}
1154
1155impl Eq for AssetRef {}
1156
1157impl PartialEq for AssetRef {
1158 fn eq(&self, other: &Self) -> bool {
1159 self.get_asset_cmp() == other.get_asset_cmp()
1160 }
1161}
1162
1163impl Ord for AssetRef {
1164 fn cmp(&self, other: &Self) -> Ordering {
1165 self.get_asset_cmp().cmp(&other.get_asset_cmp())
1166 }
1167}
1168
1169impl PartialOrd for AssetRef {
1170 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1171 Some(self.cmp(other))
1172 }
1173}
1174
1175impl Hash for AssetRef {
1176 fn hash<H: Hasher>(&self, state: &mut H) {
1177 self.get_asset_cmp().hash(state);
1178 }
1179}
1180
1181#[derive(PartialEq, PartialOrd, Eq, Ord, Hash)]
1187enum AssetCmp<'a> {
1188 WithID(AssetID),
1189 WithoutID(
1190 (
1191 &'a ProcessID,
1192 &'a RegionID,
1193 u32,
1194 Option<&'a AgentID>,
1195 Option<AssetGroupID>,
1196 ),
1197 ),
1198}
1199
1200pub trait AssetIterator<'a>: Iterator<Item = &'a AssetRef> + Sized
1202where
1203 Self: 'a,
1204{
1205 fn filter_agent(self, agent_id: &'a AgentID) -> impl Iterator<Item = &'a AssetRef> + 'a {
1207 self.filter(move |asset| asset.agent_id() == Some(agent_id))
1208 }
1209
1210 fn filter_primary_producers_of(
1212 self,
1213 commodity_id: &'a CommodityID,
1214 ) -> impl Iterator<Item = &'a AssetRef> + 'a {
1215 self.filter(move |asset| {
1216 asset
1217 .primary_output()
1218 .is_some_and(|flow| &flow.commodity.id == commodity_id)
1219 })
1220 }
1221
1222 fn filter_region(self, region_id: &'a RegionID) -> impl Iterator<Item = &'a AssetRef> + 'a {
1224 self.filter(move |asset| asset.region_id == *region_id)
1225 }
1226
1227 fn flows_for_commodity(
1229 self,
1230 commodity_id: &'a CommodityID,
1231 ) -> impl Iterator<Item = (&'a AssetRef, &'a ProcessFlow)> + 'a {
1232 self.filter_map(|asset| Some((asset, asset.get_flow(commodity_id)?)))
1233 }
1234}
1235
1236impl<'a, I> AssetIterator<'a> for I where I: Iterator<Item = &'a AssetRef> + Sized + 'a {}
1237
1238#[cfg(test)]
1239mod tests {
1240 use super::*;
1241 use crate::commodity::Commodity;
1242 use crate::fixture::{
1243 assert_error, assert_patched_runs_ok_simple, assert_validate_fails_with_simple, asset,
1244 asset_divisible, process, process_activity_limits_map, process_flows_map, region_id,
1245 svd_commodity, time_slice, time_slice_info,
1246 };
1247 use crate::patch::FilePatch;
1248 use crate::process::{FlowType, Process, ProcessFlow};
1249 use crate::region::RegionID;
1250 use crate::time_slice::{TimeSliceID, TimeSliceInfo};
1251 use crate::units::{
1252 ActivityPerCapacity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity,
1253 MoneyPerFlow,
1254 };
1255 use float_cmp::assert_approx_eq;
1256 use indexmap::indexmap;
1257 use rstest::{fixture, rstest};
1258 use std::rc::Rc;
1259
1260 #[rstest]
1261 fn get_input_cost_from_prices_works(
1262 region_id: RegionID,
1263 svd_commodity: Commodity,
1264 mut process: Process,
1265 time_slice: TimeSliceID,
1266 ) {
1267 let commodity_rc = Rc::new(svd_commodity);
1269 let process_flow = ProcessFlow {
1270 commodity: Rc::clone(&commodity_rc),
1271 coeff: FlowPerActivity(-2.0), kind: FlowType::Fixed,
1273 cost: MoneyPerFlow(0.0),
1274 };
1275 let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() };
1276 let process_flows_map = process_flows_map(process.regions.clone(), Rc::new(process_flows));
1277 process.flows = process_flows_map;
1278
1279 let asset =
1281 Asset::new_candidate(Rc::new(process), region_id.clone(), Capacity(1.0), 2020).unwrap();
1282
1283 let mut input_prices = CommodityPrices::default();
1285 input_prices.insert(&commodity_rc.id, ®ion_id, &time_slice, MoneyPerFlow(3.0));
1286
1287 let cost = asset.get_input_cost_from_prices(&input_prices, &time_slice);
1289 assert_approx_eq!(MoneyPerActivity, cost, MoneyPerActivity(6.0));
1291 }
1292
1293 #[rstest]
1294 #[case(Capacity(0.01))]
1295 #[case(Capacity(0.5))]
1296 #[case(Capacity(1.0))]
1297 #[case(Capacity(100.0))]
1298 fn asset_new_valid(process: Process, #[case] capacity: Capacity) {
1299 let agent_id = AgentID("agent1".into());
1300 let region_id = RegionID("GBR".into());
1301 let asset = Asset::new_future(agent_id, process.into(), region_id, capacity, 2015).unwrap();
1302 assert!(asset.id().is_none());
1303 }
1304
1305 #[rstest]
1306 #[case(Capacity(0.0))]
1307 #[case(Capacity(-0.01))]
1308 #[case(Capacity(-1.0))]
1309 #[case(Capacity(f64::NAN))]
1310 #[case(Capacity(f64::INFINITY))]
1311 #[case(Capacity(f64::NEG_INFINITY))]
1312 fn asset_new_invalid_capacity(process: Process, #[case] capacity: Capacity) {
1313 let agent_id = AgentID("agent1".into());
1314 let region_id = RegionID("GBR".into());
1315 assert_error!(
1316 Asset::new_future(agent_id, process.into(), region_id, capacity, 2015),
1317 "Capacity must be a finite, positive number"
1318 );
1319 }
1320
1321 #[rstest]
1322 fn asset_new_invalid_commission_year(process: Process) {
1323 let agent_id = AgentID("agent1".into());
1324 let region_id = RegionID("GBR".into());
1325 assert_error!(
1326 Asset::new_future(agent_id, process.into(), region_id, Capacity(1.0), 2007),
1327 "Process process1 does not operate in the year 2007"
1328 );
1329 }
1330
1331 #[rstest]
1332 fn asset_new_invalid_region(process: Process) {
1333 let agent_id = AgentID("agent1".into());
1334 let region_id = RegionID("FRA".into());
1335 assert_error!(
1336 Asset::new_future(agent_id, process.into(), region_id, Capacity(1.0), 2015),
1337 "Process process1 does not operate in region FRA"
1338 );
1339 }
1340
1341 #[fixture]
1342 fn process_with_activity_limits(
1343 mut process: Process,
1344 time_slice_info: TimeSliceInfo,
1345 time_slice: TimeSliceID,
1346 ) -> Process {
1347 let mut activity_limits = ActivityLimits::new_with_full_availability(&time_slice_info);
1349 activity_limits.add_time_slice_limit(time_slice, Dimensionless(0.1)..=Dimensionless(0.5));
1350 process.activity_limits =
1351 process_activity_limits_map(process.regions.clone(), activity_limits);
1352
1353 process.capacity_to_activity = ActivityPerCapacity(2.0);
1355 process
1356 }
1357
1358 #[fixture]
1359 fn asset_with_activity_limits(process_with_activity_limits: Process) -> Asset {
1360 Asset::new_future(
1361 "agent1".into(),
1362 Rc::new(process_with_activity_limits),
1363 "GBR".into(),
1364 Capacity(2.0),
1365 2010,
1366 )
1367 .unwrap()
1368 }
1369
1370 #[rstest]
1371 fn asset_get_activity_per_capacity_limits(
1372 asset_with_activity_limits: Asset,
1373 time_slice: TimeSliceID,
1374 ) {
1375 assert_eq!(
1377 asset_with_activity_limits.get_activity_per_capacity_limits(&time_slice),
1378 ActivityPerCapacity(0.2)..=ActivityPerCapacity(1.0)
1379 );
1380 }
1381
1382 #[rstest]
1383 #[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(
1388 mut process: Process,
1389 #[case] capacity: Capacity,
1390 #[case] unit_size: Capacity,
1391 #[case] n_expected_children: usize,
1392 ) {
1393 process.unit_size = Some(unit_size);
1394 let asset = AssetRef::from(
1395 Asset::new_future(
1396 "agent1".into(),
1397 Rc::new(process),
1398 "GBR".into(),
1399 capacity,
1400 2010,
1401 )
1402 .unwrap(),
1403 );
1404
1405 let mut count = 0;
1406 let mut total_child_capacity = Capacity(0.0);
1407 asset.clone().into_for_each_child(&mut 0, |parent, child| {
1408 assert!(parent.is_some_and(|parent| matches!(parent.state, AssetState::Parent { .. })));
1409
1410 assert_eq!(
1412 child.total_capacity(),
1413 unit_size,
1414 "Child capacity should equal unit_size"
1415 );
1416
1417 total_child_capacity += child.total_capacity();
1418 count += 1;
1419 });
1420 assert_eq!(count, n_expected_children, "Unexpected number of children");
1421
1422 assert!(
1424 total_child_capacity >= asset.total_capacity(),
1425 "Total capacity should be >= parent capacity"
1426 );
1427 }
1428
1429 #[rstest]
1430 fn into_for_each_child_nondivisible(asset: Asset) {
1431 assert!(
1432 asset.process.unit_size.is_none(),
1433 "Asset should be non-divisible"
1434 );
1435
1436 let asset = AssetRef::from(asset);
1437 let mut count = 0;
1438 asset.clone().into_for_each_child(&mut 0, |parent, child| {
1439 assert!(parent.is_none());
1440 assert_eq!(child, asset);
1441 count += 1;
1442 });
1443 assert_eq!(count, 1);
1444 }
1445
1446 #[fixture]
1447 fn parent_asset(asset_divisible: Asset) -> AssetRef {
1448 let asset = AssetRef::from(asset_divisible);
1449 let mut parent = None;
1450
1451 asset.into_for_each_child(&mut 0, |maybe_parent, _| {
1452 if parent.is_none() {
1453 parent = maybe_parent.cloned();
1454 }
1455 });
1456
1457 parent.expect("Divisible asset should create a parent")
1458 }
1459
1460 #[rstest]
1461 #[case::subset_of_children(2, false)]
1462 #[case::all_children(3, true)]
1463 fn make_partial_parent(
1464 parent_asset: AssetRef,
1465 #[case] num_units: u32,
1466 #[case] expect_same_asset: bool,
1467 ) {
1468 let parent = parent_asset;
1469 assert!(parent.is_parent());
1470
1471 let partial_parent = parent.make_partial_parent(num_units);
1472
1473 assert!(partial_parent.is_parent());
1474 assert_eq!(
1475 partial_parent.capacity(),
1476 AssetCapacity::Discrete(num_units, Capacity(4.0))
1477 );
1478 assert_eq!(partial_parent.num_children(), Some(num_units));
1479 assert_eq!(partial_parent.group_id(), parent.group_id());
1480 assert_eq!(partial_parent.agent_id(), parent.agent_id());
1481 assert_eq!(Rc::ptr_eq(&partial_parent.0, &parent.0), expect_same_asset);
1482 assert_eq!(parent.capacity(), AssetCapacity::Discrete(3, Capacity(4.0)));
1483 }
1484
1485 #[rstest]
1486 #[should_panic(expected = "Cannot make a partial parent from a non-parent asset")]
1487 fn make_partial_parent_panics_for_non_parent_asset(asset_divisible: Asset) {
1488 let asset = AssetRef::from(asset_divisible);
1489 asset.make_partial_parent(1);
1490 }
1491
1492 #[rstest]
1493 #[should_panic(expected = "Cannot make a partial parent with zero units")]
1494 fn make_partial_parent_panics_for_zero_units(parent_asset: AssetRef) {
1495 parent_asset.make_partial_parent(0);
1496 }
1497
1498 #[rstest]
1499 #[should_panic(expected = "Cannot make a partial parent with more units than original")]
1500 fn make_partial_parent_panics_for_too_many_units(parent_asset: AssetRef) {
1501 parent_asset.make_partial_parent(4);
1502 }
1503
1504 #[rstest]
1505 fn asset_commission(process: Process) {
1506 let process_rc = Rc::new(process);
1508 let mut asset1 = Asset::new_future(
1509 "agent1".into(),
1510 Rc::clone(&process_rc),
1511 "GBR".into(),
1512 Capacity(1.0),
1513 2020,
1514 )
1515 .unwrap();
1516 asset1.commission(AssetID(1), None, "");
1517 assert!(asset1.is_commissioned());
1518 assert_eq!(asset1.id(), Some(AssetID(1)));
1519
1520 let mut asset2 = Asset::new_selected(
1522 "agent1".into(),
1523 Rc::clone(&process_rc),
1524 "GBR".into(),
1525 Capacity(1.0),
1526 2020,
1527 )
1528 .unwrap();
1529 asset2.commission(AssetID(2), None, "");
1530 assert!(asset2.is_commissioned());
1531 assert_eq!(asset2.id(), Some(AssetID(2)));
1532 }
1533
1534 #[rstest]
1535 #[should_panic(expected = "Assets with state Candidate cannot be commissioned")]
1536 fn commission_wrong_states(process: Process) {
1537 let mut asset =
1538 Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap();
1539 asset.commission(AssetID(1), None, "");
1540 }
1541
1542 #[test]
1543 fn commission_year_before_time_horizon() {
1544 let processes_patch = FilePatch::new("processes.csv")
1545 .with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
1546 .with_addition("GASDRV,Dry gas extraction,all,GASPRD,1980,2040,1.0,");
1547
1548 let patches = vec![
1551 processes_patch.clone(),
1552 FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,1980"),
1553 ];
1554 assert_patched_runs_ok_simple!(patches);
1555
1556 let patches = vec![
1558 processes_patch,
1559 FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,1970"),
1560 ];
1561 assert_validate_fails_with_simple!(
1562 patches,
1563 "Agent A0_GEX has asset with commission year 1970, not within process GASDRV commission years: 1980..=2040"
1564 );
1565 }
1566
1567 #[test]
1568 fn commission_year_after_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,2020,2050,1.0,");
1572
1573 let patches = vec![
1575 processes_patch.clone(),
1576 FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,2050"),
1577 ];
1578 assert_patched_runs_ok_simple!(patches);
1579
1580 let patches = vec![
1582 processes_patch,
1583 FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,2060"),
1584 ];
1585 assert_validate_fails_with_simple!(
1586 patches,
1587 "Agent A0_GEX has asset with commission year 2060, not within process GASDRV commission years: 2020..=2050"
1588 );
1589 }
1590
1591 #[rstest]
1592 fn max_installable_capacity(mut process: Process, region_id: RegionID) {
1593 process.investment_constraints.insert(
1595 (region_id.clone(), 2015),
1596 Rc::new(crate::process::ProcessInvestmentConstraint {
1597 addition_limit: Some(Capacity(3.0)),
1598 }),
1599 );
1600 let process_rc = Rc::new(process);
1601
1602 let asset =
1604 Asset::new_candidate(process_rc.clone(), region_id.clone(), Capacity(1.0), 2015)
1605 .unwrap();
1606
1607 let result = asset.max_installable_capacity(Dimensionless(0.5));
1609 assert_eq!(result, Some(AssetCapacity::Continuous(Capacity(1.5))));
1610 }
1611}