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 pub fn make_partial_parent(&self, num_units: u32) -> Self {
1111 assert!(
1112 self.is_parent(),
1113 "Cannot make a partial parent from a non-parent asset"
1114 );
1115 assert!(
1116 num_units > 0,
1117 "Cannot make a partial parent with zero units"
1118 );
1119
1120 let (max_num_units, unit_size) = match self.capacity() {
1121 AssetCapacity::Discrete(max_num_units, unit_size) => (max_num_units, unit_size),
1122 AssetCapacity::Continuous(_) => unreachable!(),
1124 };
1125 match num_units.cmp(&max_num_units) {
1126 Ordering::Less => Self::from(Asset {
1128 capacity: Cell::new(AssetCapacity::Discrete(num_units, unit_size)),
1129 ..Asset::clone(self)
1130 }),
1131 Ordering::Equal => self.clone(),
1133 Ordering::Greater => {
1134 panic!("Cannot make a partial parent with more units than original")
1135 }
1136 }
1137 }
1138}
1139
1140impl From<Rc<Asset>> for AssetRef {
1141 fn from(value: Rc<Asset>) -> Self {
1142 Self(value)
1143 }
1144}
1145
1146impl From<Asset> for AssetRef {
1147 fn from(value: Asset) -> Self {
1148 Self::from(Rc::new(value))
1149 }
1150}
1151
1152impl From<AssetRef> for Rc<Asset> {
1153 fn from(value: AssetRef) -> Self {
1154 value.0
1155 }
1156}
1157
1158impl Deref for AssetRef {
1159 type Target = Asset;
1160
1161 fn deref(&self) -> &Self::Target {
1162 &self.0
1163 }
1164}
1165
1166impl PartialEq for AssetRef {
1167 fn eq(&self, other: &Self) -> bool {
1168 Rc::ptr_eq(&self.0.process, &other.0.process)
1171 && self.0.region_id == other.0.region_id
1172 && self.0.commission_year == other.0.commission_year
1173 && self.0.state == other.0.state
1174 }
1175}
1176
1177impl Eq for AssetRef {}
1178
1179impl Hash for AssetRef {
1180 fn hash<H: Hasher>(&self, state: &mut H) {
1188 match &self.0.state {
1189 AssetState::Commissioned { id, .. } => {
1190 id.hash(state);
1193 }
1194 AssetState::Candidate | AssetState::Selected { .. } | AssetState::Parent { .. } => {
1195 self.0.process.id.hash(state);
1196 self.0.region_id.hash(state);
1197 self.0.commission_year.hash(state);
1198 self.0.agent_id().hash(state);
1199 self.0.group_id().hash(state);
1200 }
1201 state => {
1202 panic!("Cannot hash {state} assets");
1204 }
1205 }
1206 }
1207}
1208
1209impl PartialOrd for AssetRef {
1210 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1211 Some(self.cmp(other))
1212 }
1213}
1214
1215impl Ord for AssetRef {
1216 fn cmp(&self, other: &Self) -> Ordering {
1217 self.id().unwrap().cmp(&other.id().unwrap())
1218 }
1219}
1220
1221pub trait AssetIterator<'a>: Iterator<Item = &'a AssetRef> + Sized
1223where
1224 Self: 'a,
1225{
1226 fn filter_agent(self, agent_id: &'a AgentID) -> impl Iterator<Item = &'a AssetRef> + 'a {
1228 self.filter(move |asset| asset.agent_id() == Some(agent_id))
1229 }
1230
1231 fn filter_primary_producers_of(
1233 self,
1234 commodity_id: &'a CommodityID,
1235 ) -> impl Iterator<Item = &'a AssetRef> + 'a {
1236 self.filter(move |asset| {
1237 asset
1238 .primary_output()
1239 .is_some_and(|flow| &flow.commodity.id == commodity_id)
1240 })
1241 }
1242
1243 fn filter_region(self, region_id: &'a RegionID) -> impl Iterator<Item = &'a AssetRef> + 'a {
1245 self.filter(move |asset| asset.region_id == *region_id)
1246 }
1247
1248 fn flows_for_commodity(
1250 self,
1251 commodity_id: &'a CommodityID,
1252 ) -> impl Iterator<Item = (&'a AssetRef, &'a ProcessFlow)> + 'a {
1253 self.filter_map(|asset| Some((asset, asset.get_flow(commodity_id)?)))
1254 }
1255}
1256
1257impl<'a, I> AssetIterator<'a> for I where I: Iterator<Item = &'a AssetRef> + Sized + 'a {}
1258
1259#[cfg(test)]
1260mod tests {
1261 use super::*;
1262 use crate::commodity::Commodity;
1263 use crate::fixture::{
1264 assert_error, assert_patched_runs_ok_simple, assert_validate_fails_with_simple, asset,
1265 asset_divisible, process, process_activity_limits_map, process_flows_map, region_id,
1266 svd_commodity, time_slice, time_slice_info,
1267 };
1268 use crate::patch::FilePatch;
1269 use crate::process::{FlowType, Process, ProcessFlow};
1270 use crate::region::RegionID;
1271 use crate::time_slice::{TimeSliceID, TimeSliceInfo};
1272 use crate::units::{
1273 ActivityPerCapacity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity,
1274 MoneyPerFlow,
1275 };
1276 use float_cmp::assert_approx_eq;
1277 use indexmap::indexmap;
1278 use rstest::{fixture, rstest};
1279 use std::rc::Rc;
1280
1281 #[rstest]
1282 fn get_input_cost_from_prices_works(
1283 region_id: RegionID,
1284 svd_commodity: Commodity,
1285 mut process: Process,
1286 time_slice: TimeSliceID,
1287 ) {
1288 let commodity_rc = Rc::new(svd_commodity);
1290 let process_flow = ProcessFlow {
1291 commodity: Rc::clone(&commodity_rc),
1292 coeff: FlowPerActivity(-2.0), kind: FlowType::Fixed,
1294 cost: MoneyPerFlow(0.0),
1295 };
1296 let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() };
1297 let process_flows_map = process_flows_map(process.regions.clone(), Rc::new(process_flows));
1298 process.flows = process_flows_map;
1299
1300 let asset =
1302 Asset::new_candidate(Rc::new(process), region_id.clone(), Capacity(1.0), 2020).unwrap();
1303
1304 let mut input_prices = CommodityPrices::default();
1306 input_prices.insert(&commodity_rc.id, ®ion_id, &time_slice, MoneyPerFlow(3.0));
1307
1308 let cost = asset.get_input_cost_from_prices(&input_prices, &time_slice);
1310 assert_approx_eq!(MoneyPerActivity, cost, MoneyPerActivity(6.0));
1312 }
1313
1314 #[rstest]
1315 #[case(Capacity(0.01))]
1316 #[case(Capacity(0.5))]
1317 #[case(Capacity(1.0))]
1318 #[case(Capacity(100.0))]
1319 fn asset_new_valid(process: Process, #[case] capacity: Capacity) {
1320 let agent_id = AgentID("agent1".into());
1321 let region_id = RegionID("GBR".into());
1322 let asset = Asset::new_future(agent_id, process.into(), region_id, capacity, 2015).unwrap();
1323 assert!(asset.id().is_none());
1324 }
1325
1326 #[rstest]
1327 #[case(Capacity(0.0))]
1328 #[case(Capacity(-0.01))]
1329 #[case(Capacity(-1.0))]
1330 #[case(Capacity(f64::NAN))]
1331 #[case(Capacity(f64::INFINITY))]
1332 #[case(Capacity(f64::NEG_INFINITY))]
1333 fn asset_new_invalid_capacity(process: Process, #[case] capacity: Capacity) {
1334 let agent_id = AgentID("agent1".into());
1335 let region_id = RegionID("GBR".into());
1336 assert_error!(
1337 Asset::new_future(agent_id, process.into(), region_id, capacity, 2015),
1338 "Capacity must be a finite, positive number"
1339 );
1340 }
1341
1342 #[rstest]
1343 fn asset_new_invalid_commission_year(process: Process) {
1344 let agent_id = AgentID("agent1".into());
1345 let region_id = RegionID("GBR".into());
1346 assert_error!(
1347 Asset::new_future(agent_id, process.into(), region_id, Capacity(1.0), 2007),
1348 "Process process1 does not operate in the year 2007"
1349 );
1350 }
1351
1352 #[rstest]
1353 fn asset_new_invalid_region(process: Process) {
1354 let agent_id = AgentID("agent1".into());
1355 let region_id = RegionID("FRA".into());
1356 assert_error!(
1357 Asset::new_future(agent_id, process.into(), region_id, Capacity(1.0), 2015),
1358 "Process process1 does not operate in region FRA"
1359 );
1360 }
1361
1362 #[fixture]
1363 fn process_with_activity_limits(
1364 mut process: Process,
1365 time_slice_info: TimeSliceInfo,
1366 time_slice: TimeSliceID,
1367 ) -> Process {
1368 let mut activity_limits = ActivityLimits::new_with_full_availability(&time_slice_info);
1370 activity_limits.add_time_slice_limit(time_slice, Dimensionless(0.1)..=Dimensionless(0.5));
1371 process.activity_limits =
1372 process_activity_limits_map(process.regions.clone(), activity_limits);
1373
1374 process.capacity_to_activity = ActivityPerCapacity(2.0);
1376 process
1377 }
1378
1379 #[fixture]
1380 fn asset_with_activity_limits(process_with_activity_limits: Process) -> Asset {
1381 Asset::new_future(
1382 "agent1".into(),
1383 Rc::new(process_with_activity_limits),
1384 "GBR".into(),
1385 Capacity(2.0),
1386 2010,
1387 )
1388 .unwrap()
1389 }
1390
1391 #[rstest]
1392 fn asset_get_activity_per_capacity_limits(
1393 asset_with_activity_limits: Asset,
1394 time_slice: TimeSliceID,
1395 ) {
1396 assert_eq!(
1398 asset_with_activity_limits.get_activity_per_capacity_limits(&time_slice),
1399 ActivityPerCapacity(0.2)..=ActivityPerCapacity(1.0)
1400 );
1401 }
1402
1403 #[rstest]
1404 #[case::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(
1409 mut process: Process,
1410 #[case] capacity: Capacity,
1411 #[case] unit_size: Capacity,
1412 #[case] n_expected_children: usize,
1413 ) {
1414 process.unit_size = Some(unit_size);
1415 let asset = AssetRef::from(
1416 Asset::new_future(
1417 "agent1".into(),
1418 Rc::new(process),
1419 "GBR".into(),
1420 capacity,
1421 2010,
1422 )
1423 .unwrap(),
1424 );
1425
1426 let mut count = 0;
1427 let mut total_child_capacity = Capacity(0.0);
1428 asset.clone().into_for_each_child(&mut 0, |parent, child| {
1429 assert!(parent.is_some_and(|parent| matches!(parent.state, AssetState::Parent { .. })));
1430
1431 assert_eq!(
1433 child.total_capacity(),
1434 unit_size,
1435 "Child capacity should equal unit_size"
1436 );
1437
1438 total_child_capacity += child.total_capacity();
1439 count += 1;
1440 });
1441 assert_eq!(count, n_expected_children, "Unexpected number of children");
1442
1443 assert!(
1445 total_child_capacity >= asset.total_capacity(),
1446 "Total capacity should be >= parent capacity"
1447 );
1448 }
1449
1450 #[rstest]
1451 fn into_for_each_child_nondivisible(asset: Asset) {
1452 assert!(
1453 asset.process.unit_size.is_none(),
1454 "Asset should be non-divisible"
1455 );
1456
1457 let asset = AssetRef::from(asset);
1458 let mut count = 0;
1459 asset.clone().into_for_each_child(&mut 0, |parent, child| {
1460 assert!(parent.is_none());
1461 assert_eq!(child, asset);
1462 count += 1;
1463 });
1464 assert_eq!(count, 1);
1465 }
1466
1467 #[fixture]
1468 fn parent_asset(asset_divisible: Asset) -> AssetRef {
1469 let asset = AssetRef::from(asset_divisible);
1470 let mut parent = None;
1471
1472 asset.into_for_each_child(&mut 0, |maybe_parent, _| {
1473 if parent.is_none() {
1474 parent = maybe_parent.cloned();
1475 }
1476 });
1477
1478 parent.expect("Divisible asset should create a parent")
1479 }
1480
1481 #[rstest]
1482 #[case::subset_of_children(2, false)]
1483 #[case::all_children(3, true)]
1484 fn make_partial_parent(
1485 parent_asset: AssetRef,
1486 #[case] num_units: u32,
1487 #[case] expect_same_asset: bool,
1488 ) {
1489 let parent = parent_asset;
1490 assert!(parent.is_parent());
1491
1492 let partial_parent = parent.make_partial_parent(num_units);
1493
1494 assert!(partial_parent.is_parent());
1495 assert_eq!(
1496 partial_parent.capacity(),
1497 AssetCapacity::Discrete(num_units, Capacity(4.0))
1498 );
1499 assert_eq!(partial_parent.num_children(), Some(num_units));
1500 assert_eq!(partial_parent.group_id(), parent.group_id());
1501 assert_eq!(partial_parent.agent_id(), parent.agent_id());
1502 assert_eq!(Rc::ptr_eq(&partial_parent.0, &parent.0), expect_same_asset);
1503 assert_eq!(parent.capacity(), AssetCapacity::Discrete(3, Capacity(4.0)));
1504 }
1505
1506 #[rstest]
1507 #[should_panic(expected = "Cannot make a partial parent from a non-parent asset")]
1508 fn make_partial_parent_panics_for_non_parent_asset(asset_divisible: Asset) {
1509 let asset = AssetRef::from(asset_divisible);
1510 asset.make_partial_parent(1);
1511 }
1512
1513 #[rstest]
1514 #[should_panic(expected = "Cannot make a partial parent with zero units")]
1515 fn make_partial_parent_panics_for_zero_units(parent_asset: AssetRef) {
1516 parent_asset.make_partial_parent(0);
1517 }
1518
1519 #[rstest]
1520 #[should_panic(expected = "Cannot make a partial parent with more units than original")]
1521 fn make_partial_parent_panics_for_too_many_units(parent_asset: AssetRef) {
1522 parent_asset.make_partial_parent(4);
1523 }
1524
1525 #[rstest]
1526 fn asset_commission(process: Process) {
1527 let process_rc = Rc::new(process);
1529 let mut asset1 = Asset::new_future(
1530 "agent1".into(),
1531 Rc::clone(&process_rc),
1532 "GBR".into(),
1533 Capacity(1.0),
1534 2020,
1535 )
1536 .unwrap();
1537 asset1.commission(AssetID(1), None, "");
1538 assert!(asset1.is_commissioned());
1539 assert_eq!(asset1.id(), Some(AssetID(1)));
1540
1541 let mut asset2 = Asset::new_selected(
1543 "agent1".into(),
1544 Rc::clone(&process_rc),
1545 "GBR".into(),
1546 Capacity(1.0),
1547 2020,
1548 )
1549 .unwrap();
1550 asset2.commission(AssetID(2), None, "");
1551 assert!(asset2.is_commissioned());
1552 assert_eq!(asset2.id(), Some(AssetID(2)));
1553 }
1554
1555 #[rstest]
1556 #[case::commission_during_process_lifetime(2024, 2024)]
1557 #[case::decommission_after_process_lifetime_ends(2026, 2025)]
1558 fn asset_decommission(
1559 #[case] requested_decommission_year: u32,
1560 #[case] expected_decommission_year: u32,
1561 process: Process,
1562 ) {
1563 let process_rc = Rc::new(process);
1565 let mut asset = Asset::new_future(
1566 "agent1".into(),
1567 Rc::clone(&process_rc),
1568 "GBR".into(),
1569 Capacity(1.0),
1570 2020,
1571 )
1572 .unwrap();
1573 asset.commission(AssetID(1), None, "");
1574 assert!(asset.is_commissioned());
1575 assert_eq!(asset.id(), Some(AssetID(1)));
1576
1577 asset.decommission(requested_decommission_year, "");
1579 assert!(!asset.is_commissioned());
1580 assert_eq!(asset.decommission_year(), Some(expected_decommission_year));
1581 }
1582
1583 #[rstest]
1584 #[case::decommission_after_predefined_max_year(2026, 2025, Some(2025))]
1585 #[case::decommission_before_predefined_max_year(2024, 2024, Some(2025))]
1586 #[case::decommission_during_process_lifetime_end_no_max_year(2024, 2024, None)]
1587 #[case::decommission_after_process_lifetime_end_no_max_year(2026, 2025, None)]
1588 fn asset_decommission_with_max_decommission_year_predefined(
1589 #[case] requested_decommission_year: u32,
1590 #[case] expected_decommission_year: u32,
1591 #[case] max_decommission_year: Option<u32>,
1592 process: Process,
1593 ) {
1594 let process_rc = Rc::new(process);
1596 let mut asset = Asset::new_future_with_max_decommission(
1597 "agent1".into(),
1598 Rc::clone(&process_rc),
1599 "GBR".into(),
1600 Capacity(1.0),
1601 2020,
1602 max_decommission_year,
1603 )
1604 .unwrap();
1605 asset.commission(AssetID(1), None, "");
1606 assert!(asset.is_commissioned());
1607 assert_eq!(asset.id(), Some(AssetID(1)));
1608
1609 asset.decommission(requested_decommission_year, "");
1611 assert!(!asset.is_commissioned());
1612 assert_eq!(asset.decommission_year(), Some(expected_decommission_year));
1613 }
1614
1615 #[rstest]
1616 fn asset_decommission_divisible(asset_divisible: Asset) {
1617 let asset = AssetRef::from(asset_divisible);
1618 let original_capacity = asset.capacity();
1619
1620 let mut children = Vec::new();
1622 let mut next_id = 0;
1623 asset.into_for_each_child(&mut 0, |parent, mut child| {
1624 child
1625 .make_mut()
1626 .commission(AssetID(next_id), parent.cloned(), "");
1627 next_id += 1;
1628 children.push(child);
1629 });
1630
1631 let parent = children[0].parent().unwrap().clone();
1632 assert_eq!(parent.capacity(), original_capacity);
1633 children[0].make_mut().decommission(2020, "");
1634
1635 let AssetCapacity::Discrete(original_units, original_unit_size) = original_capacity else {
1636 panic!("Capacity type should be discrete");
1637 };
1638 assert_eq!(
1639 parent.capacity(),
1640 AssetCapacity::Discrete(original_units - 1, original_unit_size)
1641 );
1642 }
1643
1644 #[rstest]
1645 #[should_panic(expected = "Assets with state Candidate cannot be commissioned")]
1646 fn commission_wrong_states(process: Process) {
1647 let mut asset =
1648 Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap();
1649 asset.commission(AssetID(1), None, "");
1650 }
1651
1652 #[rstest]
1653 #[should_panic(expected = "Cannot decommission an asset that hasn't been commissioned")]
1654 fn decommission_wrong_state(process: Process) {
1655 let mut asset =
1656 Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap();
1657 asset.decommission(2025, "");
1658 }
1659
1660 #[test]
1661 fn commission_year_before_time_horizon() {
1662 let processes_patch = FilePatch::new("processes.csv")
1663 .with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
1664 .with_addition("GASDRV,Dry gas extraction,all,GASPRD,1980,2040,1.0,");
1665
1666 let patches = vec![
1669 processes_patch.clone(),
1670 FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,1980"),
1671 ];
1672 assert_patched_runs_ok_simple!(patches);
1673
1674 let patches = vec![
1676 processes_patch,
1677 FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,1970"),
1678 ];
1679 assert_validate_fails_with_simple!(
1680 patches,
1681 "Agent A0_GEX has asset with commission year 1970, not within process GASDRV commission years: 1980..=2040"
1682 );
1683 }
1684
1685 #[test]
1686 fn commission_year_after_time_horizon() {
1687 let processes_patch = FilePatch::new("processes.csv")
1688 .with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
1689 .with_addition("GASDRV,Dry gas extraction,all,GASPRD,2020,2050,1.0,");
1690
1691 let patches = vec![
1693 processes_patch.clone(),
1694 FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,2050"),
1695 ];
1696 assert_patched_runs_ok_simple!(patches);
1697
1698 let patches = vec![
1700 processes_patch,
1701 FilePatch::new("assets.csv").with_addition("GASDRV,GBR,A0_GEX,4002.26,2060"),
1702 ];
1703 assert_validate_fails_with_simple!(
1704 patches,
1705 "Agent A0_GEX has asset with commission year 2060, not within process GASDRV commission years: 2020..=2050"
1706 );
1707 }
1708
1709 #[rstest]
1710 fn max_installable_capacity(mut process: Process, region_id: RegionID) {
1711 process.investment_constraints.insert(
1713 (region_id.clone(), 2015),
1714 Rc::new(crate::process::ProcessInvestmentConstraint {
1715 addition_limit: Some(Capacity(3.0)),
1716 }),
1717 );
1718 let process_rc = Rc::new(process);
1719
1720 let asset =
1722 Asset::new_candidate(process_rc.clone(), region_id.clone(), Capacity(1.0), 2015)
1723 .unwrap();
1724
1725 let result = asset.max_installable_capacity(Dimensionless(0.5));
1727 assert_eq!(result, Some(AssetCapacity::Continuous(Capacity(1.5))));
1728 }
1729}