1use crate::agent::AgentID;
3use crate::commodity::CommodityID;
4use crate::process::{Process, ProcessFlow, ProcessID, ProcessParameter};
5use crate::region::RegionID;
6use crate::simulation::CommodityPrices;
7use crate::time_slice::TimeSliceID;
8use crate::units::{Activity, ActivityPerCapacity, Capacity, Dimensionless, MoneyPerActivity};
9use anyhow::{Context, Result, ensure};
10use indexmap::IndexMap;
11use itertools::{Itertools, chain};
12use log::{debug, warn};
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::hash::{Hash, Hasher};
16use std::ops::{Deref, RangeInclusive};
17use std::rc::Rc;
18use std::slice;
19
20#[derive(
22 Clone,
23 Copy,
24 Debug,
25 derive_more::Display,
26 Eq,
27 Hash,
28 Ord,
29 PartialEq,
30 PartialOrd,
31 Deserialize,
32 Serialize,
33)]
34pub struct AssetID(u32);
35
36#[derive(Clone, Debug, PartialEq, strum::Display)]
48pub enum AssetState {
49 Commissioned {
51 id: AssetID,
53 agent_id: AgentID,
55 },
56 Decommissioned {
58 id: AssetID,
60 agent_id: AgentID,
62 decommission_year: u32,
64 },
65 Future {
67 agent_id: AgentID,
69 },
70 Selected {
72 agent_id: AgentID,
74 },
75 Candidate,
77}
78
79#[derive(Clone, PartialEq)]
81pub struct Asset {
82 state: AssetState,
84 process: Rc<Process>,
86 activity_limits: Rc<HashMap<TimeSliceID, RangeInclusive<Dimensionless>>>,
88 flows: Rc<IndexMap<CommodityID, ProcessFlow>>,
90 process_parameter: Rc<ProcessParameter>,
92 region_id: RegionID,
94 capacity: Capacity,
96 commission_year: u32,
98}
99
100impl Asset {
101 pub fn new_candidate(
103 process: Rc<Process>,
104 region_id: RegionID,
105 capacity: Capacity,
106 commission_year: u32,
107 ) -> Result<Self> {
108 Self::new_with_state(
109 AssetState::Candidate,
110 process,
111 region_id,
112 capacity,
113 commission_year,
114 )
115 }
116
117 pub fn new_candidate_from_commissioned(asset: &Asset) -> Self {
119 assert!(asset.is_commissioned(), "Asset must be commissioned");
120
121 Self {
122 state: AssetState::Candidate,
123 ..asset.clone()
124 }
125 }
126
127 pub fn new_future(
129 agent_id: AgentID,
130 process: Rc<Process>,
131 region_id: RegionID,
132 capacity: Capacity,
133 commission_year: u32,
134 ) -> Result<Self> {
135 check_capacity_valid_for_asset(capacity)?;
136 Self::new_with_state(
137 AssetState::Future { agent_id },
138 process,
139 region_id,
140 capacity,
141 commission_year,
142 )
143 }
144
145 #[cfg(test)]
150 fn new_selected(
151 agent_id: AgentID,
152 process: Rc<Process>,
153 region_id: RegionID,
154 capacity: Capacity,
155 commission_year: u32,
156 ) -> Result<Self> {
157 Self::new_with_state(
158 AssetState::Selected { agent_id },
159 process,
160 region_id,
161 capacity,
162 commission_year,
163 )
164 }
165
166 fn new_with_state(
168 state: AssetState,
169 process: Rc<Process>,
170 region_id: RegionID,
171 capacity: Capacity,
172 commission_year: u32,
173 ) -> Result<Self> {
174 check_region_year_valid_for_process(&process, ®ion_id, commission_year)?;
175 ensure!(capacity >= Capacity(0.0), "Capacity must be non-negative");
176
177 let key = (region_id.clone(), commission_year);
183 let activity_limits = process
184 .activity_limits
185 .get(&key)
186 .with_context(|| {
187 format!(
188 "No process availabilities supplied for process {} in region {} in year {}. \
189 You should update process_availabilities.csv.",
190 &process.id, region_id, commission_year
191 )
192 })?
193 .clone();
194 let flows = process
195 .flows
196 .get(&key)
197 .with_context(|| {
198 format!(
199 "No commodity flows supplied for process {} in region {} in year {}. \
200 You should update process_flows.csv.",
201 &process.id, region_id, commission_year
202 )
203 })?
204 .clone();
205 let process_parameter = process
206 .parameters
207 .get(&key)
208 .with_context(|| {
209 format!(
210 "No process parameters supplied for process {} in region {} in year {}. \
211 You should update process_parameters.csv.",
212 &process.id, region_id, commission_year
213 )
214 })?
215 .clone();
216
217 Ok(Self {
218 state,
219 process,
220 activity_limits,
221 flows,
222 process_parameter,
223 region_id,
224 capacity,
225 commission_year,
226 })
227 }
228
229 pub fn state(&self) -> &AssetState {
231 &self.state
232 }
233
234 pub fn process_parameter(&self) -> &ProcessParameter {
236 &self.process_parameter
237 }
238
239 pub fn max_decommission_year(&self) -> u32 {
241 self.commission_year + self.process_parameter.lifetime
242 }
243
244 pub fn get_activity_limits(&self, time_slice: &TimeSliceID) -> RangeInclusive<Activity> {
246 let limits = &self.activity_limits[time_slice];
247 let max_act = self.max_activity();
248
249 (max_act * *limits.start())..=(max_act * *limits.end())
251 }
252
253 pub fn get_activity_per_capacity_limits(
255 &self,
256 time_slice: &TimeSliceID,
257 ) -> RangeInclusive<ActivityPerCapacity> {
258 let limits = &self.activity_limits[time_slice];
259 let cap2act = self.process.capacity_to_activity;
260 (cap2act * *limits.start())..=(cap2act * *limits.end())
261 }
262
263 pub fn get_operating_cost(&self, year: u32, time_slice: &TimeSliceID) -> MoneyPerActivity {
265 let flows_cost: MoneyPerActivity = self
267 .iter_flows()
268 .map(|flow| flow.get_total_cost(&self.region_id, year, time_slice))
269 .sum();
270
271 self.process_parameter.variable_operating_cost + flows_cost
272 }
273
274 pub fn get_revenue_from_flows(
278 &self,
279 prices: &CommodityPrices,
280 time_slice: &TimeSliceID,
281 ) -> MoneyPerActivity {
282 self.get_revenue_from_flows_with_filter(prices, time_slice, |_| true)
283 }
284
285 pub fn get_revenue_from_flows_excluding_primary(
289 &self,
290 prices: &CommodityPrices,
291 time_slice: &TimeSliceID,
292 ) -> MoneyPerActivity {
293 let excluded_commodity = self.primary_output().map(|flow| &flow.commodity.id);
294
295 self.get_revenue_from_flows_with_filter(prices, time_slice, |flow| {
296 excluded_commodity.is_none_or(|commodity_id| commodity_id != &flow.commodity.id)
297 })
298 }
299
300 pub fn get_input_cost_from_prices(
304 &self,
305 input_prices: &CommodityPrices,
306 time_slice: &TimeSliceID,
307 ) -> MoneyPerActivity {
308 -self.get_revenue_from_flows_with_filter(input_prices, time_slice, ProcessFlow::is_input)
309 }
310
311 fn get_revenue_from_flows_with_filter<F>(
316 &self,
317 prices: &CommodityPrices,
318 time_slice: &TimeSliceID,
319 mut filter_for_flows: F,
320 ) -> MoneyPerActivity
321 where
322 F: FnMut(&ProcessFlow) -> bool,
323 {
324 self.iter_flows()
325 .filter(|flow| filter_for_flows(flow))
326 .map(|flow| {
327 flow.coeff
328 * prices
329 .get(&flow.commodity.id, self.region_id(), time_slice)
330 .unwrap_or_default()
331 })
332 .sum()
333 }
334
335 pub fn max_activity(&self) -> Activity {
337 self.capacity * self.process.capacity_to_activity
338 }
339
340 pub fn get_flow(&self, commodity_id: &CommodityID) -> Option<&ProcessFlow> {
342 self.flows.get(commodity_id)
343 }
344
345 pub fn iter_flows(&self) -> impl Iterator<Item = &ProcessFlow> {
347 self.flows.values()
348 }
349
350 pub fn primary_output(&self) -> Option<&ProcessFlow> {
352 self.process
353 .primary_output
354 .as_ref()
355 .map(|commodity_id| &self.flows[commodity_id])
356 }
357
358 pub fn is_commissioned(&self) -> bool {
360 matches!(&self.state, AssetState::Commissioned { .. })
361 }
362
363 pub fn commission_year(&self) -> u32 {
365 self.commission_year
366 }
367
368 pub fn decommission_year(&self) -> Option<u32> {
370 match &self.state {
371 AssetState::Decommissioned {
372 decommission_year, ..
373 } => Some(*decommission_year),
374 _ => None,
375 }
376 }
377
378 pub fn region_id(&self) -> &RegionID {
380 &self.region_id
381 }
382
383 pub fn process(&self) -> &Process {
385 &self.process
386 }
387
388 pub fn process_id(&self) -> &ProcessID {
390 &self.process.id
391 }
392
393 pub fn id(&self) -> Option<AssetID> {
395 match &self.state {
396 AssetState::Commissioned { id, .. } | AssetState::Decommissioned { id, .. } => {
397 Some(*id)
398 }
399 _ => None,
400 }
401 }
402
403 pub fn agent_id(&self) -> Option<&AgentID> {
405 match &self.state {
406 AssetState::Commissioned { agent_id, .. }
407 | AssetState::Decommissioned { agent_id, .. }
408 | AssetState::Future { agent_id }
409 | AssetState::Selected { agent_id } => Some(agent_id),
410 AssetState::Candidate => None,
411 }
412 }
413
414 pub fn capacity(&self) -> Capacity {
416 self.capacity
417 }
418
419 pub fn set_capacity(&mut self, capacity: Capacity) {
421 assert!(
422 self.state == AssetState::Candidate,
423 "set_capacity can only be called on Candidate assets"
424 );
425 assert!(capacity >= Capacity(0.0), "Capacity must be >= 0");
426 self.capacity = capacity;
427 }
428
429 pub fn increase_capacity(&mut self, capacity: Capacity) {
431 assert!(
432 self.state == AssetState::Candidate,
433 "increase_capacity can only be called on Candidate assets"
434 );
435 assert!(capacity >= Capacity(0.0), "Added capacity must be >= 0");
436 self.capacity += capacity;
437 }
438
439 fn decommission(&mut self, decommission_year: u32, reason: &str) {
441 let (id, agent_id) = match &self.state {
442 AssetState::Commissioned { id, agent_id } => (*id, agent_id.clone()),
443 _ => panic!("Cannot decommission an asset that hasn't been commissioned"),
444 };
445 debug!(
446 "Decommissioning '{}' asset (ID: {}) for agent '{}' (reason: {})",
447 self.process_id(),
448 id,
449 agent_id,
450 reason
451 );
452
453 self.state = AssetState::Decommissioned {
454 id,
455 agent_id,
456 decommission_year: decommission_year.min(self.max_decommission_year()),
457 };
458 }
459
460 fn commission(&mut self, id: AssetID, reason: &str) {
470 let agent_id = match &self.state {
471 AssetState::Future { agent_id } | AssetState::Selected { agent_id } => agent_id,
472 state => panic!("Assets with state {state} cannot be commissioned"),
473 };
474 debug!(
475 "Commissioning '{}' asset (ID: {}) for agent '{}' (reason: {})",
476 self.process_id(),
477 id,
478 agent_id,
479 reason
480 );
481 self.state = AssetState::Commissioned {
482 id,
483 agent_id: agent_id.clone(),
484 };
485 }
486
487 pub fn select_candidate_for_investment(&mut self, agent_id: AgentID) {
489 assert!(
490 self.state == AssetState::Candidate,
491 "select_candidate_for_investment can only be called on Candidate assets"
492 );
493 check_capacity_valid_for_asset(self.capacity).unwrap();
494 self.state = AssetState::Selected { agent_id };
495 }
496}
497
498#[allow(clippy::missing_fields_in_debug)]
499impl std::fmt::Debug for Asset {
500 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
501 f.debug_struct("Asset")
502 .field("state", &self.state)
503 .field("process_id", &self.process_id())
504 .field("region_id", &self.region_id)
505 .field("capacity", &self.capacity)
506 .field("commission_year", &self.commission_year)
507 .finish()
508 }
509}
510
511pub fn check_region_year_valid_for_process(
513 process: &Process,
514 region_id: &RegionID,
515 year: u32,
516) -> Result<()> {
517 ensure!(
518 process.regions.contains(region_id),
519 "Process {} does not operate in region {}",
520 process.id,
521 region_id
522 );
523 ensure!(
524 process.active_for_year(year),
525 "Process {} does not operate in the year {}",
526 process.id,
527 year
528 );
529 Ok(())
530}
531
532pub fn check_capacity_valid_for_asset(capacity: Capacity) -> Result<()> {
534 ensure!(
535 capacity.is_finite() && capacity > Capacity(0.0),
536 "Capacity must be a finite, positive number"
537 );
538 Ok(())
539}
540
541#[derive(Clone, Debug)]
548pub struct AssetRef(Rc<Asset>);
549
550impl AssetRef {
551 pub fn make_mut(&mut self) -> &mut Asset {
553 Rc::make_mut(&mut self.0)
554 }
555}
556
557impl From<Rc<Asset>> for AssetRef {
558 fn from(value: Rc<Asset>) -> Self {
559 Self(value)
560 }
561}
562
563impl From<Asset> for AssetRef {
564 fn from(value: Asset) -> Self {
565 Self::from(Rc::new(value))
566 }
567}
568
569impl From<AssetRef> for Rc<Asset> {
570 fn from(value: AssetRef) -> Self {
571 value.0
572 }
573}
574
575impl Deref for AssetRef {
576 type Target = Asset;
577
578 fn deref(&self) -> &Self::Target {
579 &self.0
580 }
581}
582
583impl PartialEq for AssetRef {
584 fn eq(&self, other: &Self) -> bool {
585 Rc::ptr_eq(&self.0.process, &other.0.process)
588 && self.0.region_id == other.0.region_id
589 && self.0.commission_year == other.0.commission_year
590 && self.0.state == other.0.state
591 }
592}
593
594impl Eq for AssetRef {}
595
596impl Hash for AssetRef {
597 fn hash<H: Hasher>(&self, state: &mut H) {
603 match &self.0.state {
604 AssetState::Commissioned { id, .. } => {
605 id.hash(state);
608 }
609 AssetState::Candidate | AssetState::Selected { .. } => {
610 self.0.process.id.hash(state);
613 self.0.region_id.hash(state);
614 self.0.commission_year.hash(state);
615 self.0.agent_id().hash(state);
616 }
617 AssetState::Future { .. } | AssetState::Decommissioned { .. } => {
618 unimplemented!("Cannot hash Future or Decommissioned assets");
620 }
621 }
622 }
623}
624
625impl PartialOrd for AssetRef {
626 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
627 Some(self.cmp(other))
628 }
629}
630
631impl Ord for AssetRef {
632 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
633 self.id().unwrap().cmp(&other.id().unwrap())
634 }
635}
636
637pub struct AssetPool {
639 active: Vec<AssetRef>,
641 future: Vec<Asset>,
643 decommissioned: Vec<AssetRef>,
645 next_id: u32,
647}
648
649impl AssetPool {
650 pub fn new(mut assets: Vec<Asset>) -> Self {
652 assets.sort_by(|a, b| a.commission_year.cmp(&b.commission_year));
654
655 Self {
656 active: Vec::new(),
657 future: assets,
658 decommissioned: Vec::new(),
659 next_id: 0,
660 }
661 }
662
663 pub fn as_slice(&self) -> &[AssetRef] {
665 &self.active
666 }
667
668 pub fn update_for_year(&mut self, year: u32) {
670 self.decommission_old(year);
671 self.commission_new(year);
672 }
673
674 fn commission_new(&mut self, year: u32) {
676 let count = self
678 .future
679 .iter()
680 .take_while(|asset| asset.commission_year <= year)
681 .count();
682
683 for mut asset in self.future.drain(0..count) {
685 if asset.max_decommission_year() <= year {
687 warn!(
688 "Asset '{}' with commission year {} and lifetime {} was decommissioned before \
689 the start of the simulation",
690 asset.process_id(),
691 asset.commission_year,
692 asset.process_parameter().lifetime
693 );
694 continue;
695 }
696
697 asset.commission(AssetID(self.next_id), "user input");
698 self.next_id += 1;
699 self.active.push(asset.into());
700 }
701 }
702
703 fn decommission_old(&mut self, year: u32) {
705 let to_decommission = self
707 .active
708 .extract_if(.., |asset| asset.max_decommission_year() <= year);
709
710 for mut asset in to_decommission {
711 asset.make_mut().decommission(year, "end of life");
713 self.decommissioned.push(asset);
714 }
715 }
716
717 pub fn decommission_if_not_active<I>(&mut self, assets: I, year: u32)
729 where
730 I: IntoIterator<Item = AssetRef>,
731 {
732 let to_decommission = assets.into_iter().filter(|asset| {
733 let AssetState::Commissioned { id, .. } = &asset.state else {
735 panic!("Cannot decommission asset that has not been commissioned")
736 };
737
738 !self.active.iter().any(|a| match &a.state {
740 AssetState::Commissioned { id: active_id, .. } => active_id == id,
741 _ => panic!("Active pool should only contain commissioned assets"),
742 })
743 });
744
745 for mut asset in to_decommission {
746 asset.make_mut().decommission(year, "not selected");
747 self.decommissioned.push(asset);
748 }
749 }
750
751 pub fn get(&self, id: AssetID) -> Option<&AssetRef> {
758 let idx = self
760 .active
761 .binary_search_by(|asset| match &asset.state {
762 AssetState::Commissioned { id: asset_id, .. } => asset_id.cmp(&id),
763 _ => panic!("Active pool should only contain commissioned assets"),
764 })
765 .ok()?;
766
767 Some(&self.active[idx])
768 }
769
770 pub fn iter_active(&self) -> slice::Iter<'_, AssetRef> {
772 self.active.iter()
773 }
774
775 pub fn iter_decommissioned(&self) -> slice::Iter<'_, AssetRef> {
777 self.decommissioned.iter()
778 }
779
780 pub fn iter_all(&self) -> impl Iterator<Item = &AssetRef> {
784 chain(self.iter_active(), self.iter_decommissioned())
785 }
786
787 pub fn take(&mut self) -> Vec<AssetRef> {
789 std::mem::take(&mut self.active)
790 }
791
792 pub fn extend<I>(&mut self, assets: I)
794 where
795 I: IntoIterator<Item = AssetRef>,
796 {
797 let assets = assets.into_iter().map(|mut asset| match &asset.state {
800 AssetState::Commissioned { .. } => asset,
801 AssetState::Selected { .. } => {
802 asset
803 .make_mut()
804 .commission(AssetID(self.next_id), "selected");
805 self.next_id += 1;
806 asset
807 }
808 _ => panic!(
809 "Cannot extend asset pool with asset in state {}. Only assets in \
810 Commissioned or Selected states are allowed.",
811 asset.state
812 ),
813 });
814
815 self.active.extend(assets);
817 self.active.sort();
818
819 debug_assert_eq!(self.active.iter().unique().count(), self.active.len());
821 }
822}
823
824pub trait AssetIterator<'a>: Iterator<Item = &'a AssetRef> + Sized
826where
827 Self: 'a,
828{
829 fn filter_agent(self, agent_id: &'a AgentID) -> impl Iterator<Item = &'a AssetRef> + 'a {
831 self.filter(move |asset| asset.agent_id() == Some(agent_id))
832 }
833
834 fn filter_primary_producers_of(
836 self,
837 commodity_id: &'a CommodityID,
838 ) -> impl Iterator<Item = &'a AssetRef> + 'a {
839 self.filter(move |asset| {
840 asset
841 .primary_output()
842 .is_some_and(|flow| &flow.commodity.id == commodity_id)
843 })
844 }
845
846 fn filter_region(self, region_id: &'a RegionID) -> impl Iterator<Item = &'a AssetRef> + 'a {
848 self.filter(move |asset| asset.region_id == *region_id)
849 }
850
851 fn flows_for_commodity(
853 self,
854 commodity_id: &'a CommodityID,
855 ) -> impl Iterator<Item = (&'a AssetRef, &'a ProcessFlow)> + 'a {
856 self.filter_map(|asset| Some((asset, asset.get_flow(commodity_id)?)))
857 }
858}
859
860impl<'a, I> AssetIterator<'a> for I where I: Iterator<Item = &'a AssetRef> + Sized + 'a {}
861
862#[cfg(test)]
863mod tests {
864 use super::*;
865 use crate::commodity::{Commodity, CommodityID, CommodityType};
866 use crate::fixture::{
867 assert_error, asset, commodity_id, process, process_parameter_map, region_id, time_slice,
868 };
869 use crate::process::{
870 FlowType, Process, ProcessActivityLimitsMap, ProcessFlow, ProcessFlowsMap,
871 ProcessParameter, ProcessParameterMap,
872 };
873 use crate::region::RegionID;
874 use crate::time_slice::{TimeSliceID, TimeSliceLevel};
875 use crate::units::{
876 ActivityPerCapacity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity,
877 MoneyPerCapacity, MoneyPerCapacityPerYear, MoneyPerFlow,
878 };
879 use indexmap::{IndexSet, indexmap, indexset};
880 use itertools::{Itertools, assert_equal};
881 use map_macro::hash_map;
882 use rstest::{fixture, rstest};
883 use std::collections::HashMap;
884 use std::iter;
885 use std::rc::Rc;
886
887 #[rstest]
888 fn test_get_input_cost_from_prices(
889 region_id: RegionID,
890 commodity_id: CommodityID,
891 mut process_parameter_map: ProcessParameterMap,
892 time_slice: TimeSliceID,
893 ) {
894 let commodity = Rc::new(Commodity {
896 id: commodity_id.clone(),
897 description: "Test commodity".to_string(),
898 kind: CommodityType::ServiceDemand,
899 time_slice_level: TimeSliceLevel::Annual,
900 levies: Default::default(),
901 demand: Default::default(),
902 });
903
904 let flow = ProcessFlow {
906 commodity: commodity.clone(),
907 coeff: FlowPerActivity(-2.0), kind: FlowType::Fixed,
909 cost: MoneyPerFlow(0.0),
910 };
911
912 process_parameter_map.insert(
914 (region_id.clone(), 2020),
915 Rc::new(ProcessParameter {
916 capital_cost: Default::default(),
917 fixed_operating_cost: Default::default(),
918 variable_operating_cost: Default::default(),
919 lifetime: 1,
920 discount_rate: Default::default(),
921 }),
922 );
923
924 let flow_map = indexmap! { commodity_id.clone() => flow };
926 let flows = hash_map! {(region_id.clone(), 2020) => flow_map.into()};
927
928 let activity_limits = hash_map! {(region_id.clone(), 2020) => Rc::new(HashMap::new())};
930
931 let process = Rc::new(Process {
932 id: ProcessID::from("PROC1"),
933 description: "Test process".to_string(),
934 flows,
935 parameters: process_parameter_map,
936 regions: indexset! {region_id.clone()},
937 primary_output: Some(commodity_id.clone()),
938 years: vec![2020],
939 activity_limits,
940 capacity_to_activity: ActivityPerCapacity(1.0),
941 });
942
943 let asset = Asset::new_candidate(process, region_id.clone(), Capacity(1.0), 2020).unwrap();
945
946 let mut input_prices = CommodityPrices::default();
948 input_prices.insert(&commodity_id, ®ion_id, &time_slice, MoneyPerFlow(3.0));
949
950 let cost = asset.get_input_cost_from_prices(&input_prices, &time_slice);
952 assert_eq!(cost.0, 6.0);
954 }
955
956 #[rstest]
957 #[case(Capacity(0.01))]
958 #[case(Capacity(0.5))]
959 #[case(Capacity(1.0))]
960 #[case(Capacity(100.0))]
961 fn test_asset_new_valid(process: Process, #[case] capacity: Capacity) {
962 let agent_id = AgentID("agent1".into());
963 let region_id = RegionID("GBR".into());
964 let asset = Asset::new_future(agent_id, process.into(), region_id, capacity, 2015).unwrap();
965 assert!(asset.id().is_none());
966 }
967
968 #[rstest]
969 #[case(Capacity(0.0))]
970 #[case(Capacity(-0.01))]
971 #[case(Capacity(-1.0))]
972 #[case(Capacity(f64::NAN))]
973 #[case(Capacity(f64::INFINITY))]
974 #[case(Capacity(f64::NEG_INFINITY))]
975 fn test_asset_new_invalid_capacity(process: Process, #[case] capacity: Capacity) {
976 let agent_id = AgentID("agent1".into());
977 let region_id = RegionID("GBR".into());
978 assert_error!(
979 Asset::new_future(agent_id, process.into(), region_id, capacity, 2015),
980 "Capacity must be a finite, positive number"
981 );
982 }
983
984 #[rstest]
985 fn test_asset_new_invalid_commission_year(process: Process) {
986 let agent_id = AgentID("agent1".into());
987 let region_id = RegionID("GBR".into());
988 assert_error!(
989 Asset::new_future(agent_id, process.into(), region_id, Capacity(1.0), 2009),
990 "Process process1 does not operate in the year 2009"
991 );
992 }
993
994 #[rstest]
995 fn test_asset_new_invalid_region(process: Process) {
996 let agent_id = AgentID("agent1".into());
997 let region_id = RegionID("FRA".into());
998 assert_error!(
999 Asset::new_future(agent_id, process.into(), region_id, Capacity(1.0), 2015),
1000 "Process process1 does not operate in region FRA"
1001 );
1002 }
1003
1004 #[fixture]
1005 fn asset_pool(region_id: RegionID) -> AssetPool {
1006 let process_param = Rc::new(ProcessParameter {
1007 capital_cost: MoneyPerCapacity(5.0),
1008 fixed_operating_cost: MoneyPerCapacityPerYear(2.0),
1009 variable_operating_cost: MoneyPerActivity(1.0),
1010 lifetime: 20,
1011 discount_rate: Dimensionless(0.9),
1012 });
1013 let years = RangeInclusive::new(2010, 2020).collect_vec();
1014 let process_parameter_map: ProcessParameterMap = years
1015 .iter()
1016 .map(|&year| ((region_id.clone(), year), process_param.clone()))
1017 .collect();
1018 let activity_limits = years
1019 .iter()
1020 .map(|&year| ((region_id.clone(), year), Rc::new(HashMap::new())))
1021 .collect();
1022 let flows = years
1023 .iter()
1024 .map(|&year| ((region_id.clone(), year), Rc::new(IndexMap::new())))
1025 .collect();
1026 let process = Rc::new(Process {
1027 id: "process1".into(),
1028 description: "Description".into(),
1029 years: vec![2010, 2020],
1030 activity_limits,
1031 flows,
1032 parameters: process_parameter_map,
1033 regions: IndexSet::from(["GBR".into()]),
1034 primary_output: None,
1035 capacity_to_activity: ActivityPerCapacity(1.0),
1036 });
1037 let future = [2020, 2010]
1038 .map(|year| {
1039 Asset::new_future(
1040 "agent1".into(),
1041 Rc::clone(&process),
1042 "GBR".into(),
1043 Capacity(1.0),
1044 year,
1045 )
1046 .unwrap()
1047 })
1048 .into_iter()
1049 .collect_vec();
1050
1051 AssetPool::new(future)
1052 }
1053
1054 #[fixture]
1055 fn process_with_activity_limits(region_id: RegionID) -> Process {
1056 let process_param = Rc::new(ProcessParameter {
1057 capital_cost: MoneyPerCapacity(5.0),
1058 fixed_operating_cost: MoneyPerCapacityPerYear(2.0),
1059 variable_operating_cost: MoneyPerActivity(1.0),
1060 lifetime: 5,
1061 discount_rate: Dimensionless(0.9),
1062 });
1063 let years = RangeInclusive::new(2010, 2020).collect_vec();
1064 let process_parameter_map: ProcessParameterMap = years
1065 .iter()
1066 .map(|&year| ((region_id.clone(), year), process_param.clone()))
1067 .collect();
1068 let time_slice = TimeSliceID {
1069 season: "winter".into(),
1070 time_of_day: "day".into(),
1071 };
1072 let fraction_limits = Dimensionless(1.0)..=Dimensionless(2.0);
1073 let mut flows = ProcessFlowsMap::new();
1074 let mut activity_limits = ProcessActivityLimitsMap::new();
1075 let limit_map = Rc::new(hash_map! {time_slice => fraction_limits});
1076 for year in [2010, 2020] {
1077 flows.insert((region_id.clone(), year), Rc::new(IndexMap::new()));
1079 activity_limits.insert((region_id.clone(), year), limit_map.clone());
1080 }
1081 Process {
1082 id: "process1".into(),
1083 description: "Description".into(),
1084 years: vec![2010, 2020],
1085 activity_limits,
1086 flows,
1087 parameters: process_parameter_map,
1088 regions: IndexSet::from([region_id]),
1089 primary_output: None,
1090 capacity_to_activity: ActivityPerCapacity(3.0),
1091 }
1092 }
1093
1094 #[fixture]
1095 fn asset_with_activity_limits(process_with_activity_limits: Process) -> Asset {
1096 Asset::new_future(
1097 "agent1".into(),
1098 Rc::new(process_with_activity_limits),
1099 "GBR".into(),
1100 Capacity(2.0),
1101 2010,
1102 )
1103 .unwrap()
1104 }
1105
1106 #[rstest]
1107 fn test_asset_get_activity_limits(asset_with_activity_limits: Asset, time_slice: TimeSliceID) {
1108 assert_eq!(
1109 asset_with_activity_limits.get_activity_limits(&time_slice),
1110 Activity(6.0)..=Activity(12.0)
1111 );
1112 }
1113
1114 #[rstest]
1115 fn test_asset_get_activity_per_capacity_limits(
1116 asset_with_activity_limits: Asset,
1117 time_slice: TimeSliceID,
1118 ) {
1119 assert_eq!(
1120 asset_with_activity_limits.get_activity_per_capacity_limits(&time_slice),
1121 ActivityPerCapacity(3.0)..=ActivityPerCapacity(6.0)
1122 );
1123 }
1124
1125 #[rstest]
1126 fn test_asset_pool_new(asset_pool: AssetPool) {
1127 assert!(asset_pool.active.is_empty());
1129 assert!(asset_pool.future.len() == 2);
1130 assert!(asset_pool.future[0].commission_year == 2010);
1131 assert!(asset_pool.future[1].commission_year == 2020);
1132 }
1133
1134 #[rstest]
1135 fn test_asset_pool_commission_new1(mut asset_pool: AssetPool) {
1136 asset_pool.commission_new(2010);
1138 assert_equal(asset_pool.iter_active(), iter::once(&asset_pool.active[0]));
1139 }
1140
1141 #[rstest]
1142 fn test_asset_pool_commission_new2(mut asset_pool: AssetPool) {
1143 asset_pool.commission_new(2011);
1145 assert_equal(asset_pool.iter_active(), iter::once(&asset_pool.active[0]));
1146 }
1147
1148 #[rstest]
1149 fn test_asset_pool_commission_new3(mut asset_pool: AssetPool) {
1150 asset_pool.commission_new(2000);
1152 assert!(asset_pool.iter_active().next().is_none()); }
1154
1155 #[rstest]
1156 fn test_asset_pool_commission_already_decommissioned(asset: Asset) {
1157 let year = asset.max_decommission_year();
1158 let mut asset_pool = AssetPool::new(vec![asset]);
1159 assert!(asset_pool.active.is_empty());
1160 asset_pool.update_for_year(year);
1161 assert!(asset_pool.active.is_empty());
1162 }
1163
1164 #[rstest]
1165 fn test_asset_pool_decommission_old(mut asset_pool: AssetPool) {
1166 asset_pool.commission_new(2020);
1167 assert!(asset_pool.future.is_empty());
1168 assert_eq!(asset_pool.active.len(), 2);
1169 asset_pool.decommission_old(2030); assert_eq!(asset_pool.active.len(), 1);
1171 assert_eq!(asset_pool.active[0].commission_year, 2020);
1172 assert_eq!(asset_pool.decommissioned.len(), 1);
1173 assert_eq!(asset_pool.decommissioned[0].commission_year, 2010);
1174 assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2030));
1175 asset_pool.decommission_old(2032); assert_eq!(asset_pool.active.len(), 1);
1177 assert_eq!(asset_pool.active[0].commission_year, 2020);
1178 assert_eq!(asset_pool.decommissioned.len(), 1);
1179 assert_eq!(asset_pool.decommissioned[0].commission_year, 2010);
1180 assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2030));
1181 asset_pool.decommission_old(2040); assert!(asset_pool.active.is_empty());
1183 assert_eq!(asset_pool.decommissioned.len(), 2);
1184 assert_eq!(asset_pool.decommissioned[0].commission_year, 2010);
1185 assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2030));
1186 assert_eq!(asset_pool.decommissioned[1].commission_year, 2020);
1187 assert_eq!(asset_pool.decommissioned[1].decommission_year(), Some(2040));
1188 }
1189
1190 #[rstest]
1191 fn test_asset_pool_get(mut asset_pool: AssetPool) {
1192 asset_pool.commission_new(2020);
1193 assert_eq!(asset_pool.get(AssetID(0)), Some(&asset_pool.active[0]));
1194 assert_eq!(asset_pool.get(AssetID(1)), Some(&asset_pool.active[1]));
1195 }
1196
1197 #[rstest]
1198 fn test_asset_pool_extend_empty(mut asset_pool: AssetPool) {
1199 asset_pool.commission_new(2020);
1201 let original_count = asset_pool.active.len();
1202
1203 asset_pool.extend(Vec::<AssetRef>::new());
1205
1206 assert_eq!(asset_pool.active.len(), original_count);
1207 }
1208
1209 #[rstest]
1210 fn test_asset_pool_extend_existing_assets(mut asset_pool: AssetPool) {
1211 asset_pool.commission_new(2020);
1213 assert_eq!(asset_pool.active.len(), 2);
1214 let existing_assets = asset_pool.take();
1215
1216 asset_pool.extend(existing_assets.clone());
1218
1219 assert_eq!(asset_pool.active.len(), 2);
1220 assert_eq!(asset_pool.active[0].id(), Some(AssetID(0)));
1221 assert_eq!(asset_pool.active[1].id(), Some(AssetID(1)));
1222 }
1223
1224 #[rstest]
1225 fn test_asset_pool_extend_new_assets(mut asset_pool: AssetPool, process: Process) {
1226 asset_pool.commission_new(2020);
1228 let original_count = asset_pool.active.len();
1229
1230 let process_rc = Rc::new(process);
1232 let new_assets = vec![
1233 Asset::new_selected(
1234 "agent2".into(),
1235 Rc::clone(&process_rc),
1236 "GBR".into(),
1237 Capacity(1.5),
1238 2015,
1239 )
1240 .unwrap()
1241 .into(),
1242 Asset::new_selected(
1243 "agent3".into(),
1244 Rc::clone(&process_rc),
1245 "GBR".into(),
1246 Capacity(2.5),
1247 2020,
1248 )
1249 .unwrap()
1250 .into(),
1251 ];
1252
1253 asset_pool.extend(new_assets);
1254
1255 assert_eq!(asset_pool.active.len(), original_count + 2);
1256 assert_eq!(asset_pool.active[original_count].id(), Some(AssetID(2)));
1258 assert_eq!(asset_pool.active[original_count + 1].id(), Some(AssetID(3)));
1259 assert_eq!(
1260 asset_pool.active[original_count].agent_id(),
1261 Some(&"agent2".into())
1262 );
1263 assert_eq!(
1264 asset_pool.active[original_count + 1].agent_id(),
1265 Some(&"agent3".into())
1266 );
1267 }
1268
1269 #[rstest]
1270 fn test_asset_pool_extend_mixed_assets(mut asset_pool: AssetPool, process: Process) {
1271 asset_pool.commission_new(2020);
1273
1274 let new_asset = Asset::new_selected(
1276 "agent_new".into(),
1277 process.into(),
1278 "GBR".into(),
1279 Capacity(3.0),
1280 2015,
1281 )
1282 .unwrap()
1283 .into();
1284
1285 asset_pool.extend(vec![new_asset]);
1287
1288 assert_eq!(asset_pool.active.len(), 3);
1289 assert!(asset_pool.active.iter().any(|a| a.id() == Some(AssetID(0))));
1291 assert!(asset_pool.active.iter().any(|a| a.id() == Some(AssetID(1))));
1292 assert!(asset_pool.active.iter().any(|a| a.id() == Some(AssetID(2))));
1293 assert!(
1295 asset_pool
1296 .active
1297 .iter()
1298 .any(|a| a.agent_id() == Some(&"agent_new".into()))
1299 );
1300 }
1301
1302 #[rstest]
1303 fn test_asset_pool_extend_maintains_sort_order(mut asset_pool: AssetPool, process: Process) {
1304 asset_pool.commission_new(2020);
1306
1307 let process_rc = Rc::new(process);
1309 let new_assets = vec![
1310 Asset::new_selected(
1311 "agent_high_id".into(),
1312 Rc::clone(&process_rc),
1313 "GBR".into(),
1314 Capacity(1.0),
1315 2010,
1316 )
1317 .unwrap()
1318 .into(),
1319 Asset::new_selected(
1320 "agent_low_id".into(),
1321 Rc::clone(&process_rc),
1322 "GBR".into(),
1323 Capacity(1.0),
1324 2015,
1325 )
1326 .unwrap()
1327 .into(),
1328 ];
1329
1330 asset_pool.extend(new_assets);
1331
1332 let ids: Vec<u32> = asset_pool
1334 .iter_active()
1335 .map(|a| a.id().unwrap().0)
1336 .collect();
1337 assert_equal(ids, 0..4);
1338 }
1339
1340 #[rstest]
1341 fn test_asset_pool_extend_no_duplicates_expected(mut asset_pool: AssetPool) {
1342 asset_pool.commission_new(2020);
1344 let original_count = asset_pool.active.len();
1345
1346 asset_pool.extend(Vec::new());
1349
1350 assert_eq!(asset_pool.active.len(), original_count);
1351 assert_eq!(
1353 asset_pool.active.iter().unique().count(),
1354 asset_pool.active.len()
1355 );
1356 }
1357
1358 #[rstest]
1359 fn test_asset_pool_extend_increments_next_id(mut asset_pool: AssetPool, process: Process) {
1360 asset_pool.commission_new(2020);
1362 assert_eq!(asset_pool.next_id, 2); let process_rc = Rc::new(process);
1366 let new_assets = vec![
1367 Asset::new_selected(
1368 "agent1".into(),
1369 Rc::clone(&process_rc),
1370 "GBR".into(),
1371 Capacity(1.0),
1372 2015,
1373 )
1374 .unwrap()
1375 .into(),
1376 Asset::new_selected(
1377 "agent2".into(),
1378 Rc::clone(&process_rc),
1379 "GBR".into(),
1380 Capacity(1.0),
1381 2020,
1382 )
1383 .unwrap()
1384 .into(),
1385 ];
1386
1387 asset_pool.extend(new_assets);
1388
1389 assert_eq!(asset_pool.next_id, 4);
1391 assert_eq!(asset_pool.active[2].id(), Some(AssetID(2)));
1392 assert_eq!(asset_pool.active[3].id(), Some(AssetID(3)));
1393 }
1394
1395 #[rstest]
1396 fn test_asset_pool_decommission_if_not_active(mut asset_pool: AssetPool) {
1397 asset_pool.commission_new(2020);
1399 assert_eq!(asset_pool.active.len(), 2);
1400 assert_eq!(asset_pool.decommissioned.len(), 0);
1401
1402 let removed_asset = asset_pool.active.remove(0);
1404 assert_eq!(asset_pool.active.len(), 1);
1405
1406 let assets_to_check = vec![removed_asset.clone(), asset_pool.active[0].clone()];
1408 asset_pool.decommission_if_not_active(assets_to_check, 2025);
1409
1410 assert_eq!(asset_pool.active.len(), 1); assert_eq!(asset_pool.decommissioned.len(), 1);
1413 assert_eq!(asset_pool.decommissioned[0].id(), removed_asset.id());
1414 assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2025));
1415 }
1416
1417 #[rstest]
1418 fn test_asset_pool_decommission_if_not_active_all_active(mut asset_pool: AssetPool) {
1419 asset_pool.commission_new(2020);
1421 assert_eq!(asset_pool.active.len(), 2);
1422 assert_eq!(asset_pool.decommissioned.len(), 0);
1423
1424 let assets_to_check = asset_pool.active.clone();
1426 asset_pool.decommission_if_not_active(assets_to_check, 2025);
1427
1428 assert_eq!(asset_pool.active.len(), 2);
1430 assert_eq!(asset_pool.decommissioned.len(), 0);
1431 }
1432
1433 #[rstest]
1434 fn test_asset_pool_decommission_if_not_active_none_active(mut asset_pool: AssetPool) {
1435 asset_pool.commission_new(2020);
1437 let all_assets = asset_pool.active.clone();
1438
1439 asset_pool.active.clear();
1441
1442 asset_pool.decommission_if_not_active(all_assets.clone(), 2025);
1444
1445 assert_eq!(asset_pool.active.len(), 0);
1447 assert_eq!(asset_pool.decommissioned.len(), 2);
1448 assert_eq!(asset_pool.decommissioned[0].id(), all_assets[0].id());
1449 assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2025));
1450 assert_eq!(asset_pool.decommissioned[1].id(), all_assets[1].id());
1451 assert_eq!(asset_pool.decommissioned[1].decommission_year(), Some(2025));
1452 }
1453
1454 #[rstest]
1455 #[should_panic(expected = "Cannot decommission asset that has not been commissioned")]
1456 fn test_asset_pool_decommission_if_not_active_non_commissioned_asset(
1457 mut asset_pool: AssetPool,
1458 process: Process,
1459 ) {
1460 let non_commissioned_asset = Asset::new_future(
1462 "agent_new".into(),
1463 process.into(),
1464 "GBR".into(),
1465 Capacity(1.0),
1466 2015,
1467 )
1468 .unwrap()
1469 .into();
1470
1471 asset_pool.decommission_if_not_active(vec![non_commissioned_asset], 2025);
1473 }
1474
1475 #[rstest]
1476 fn test_asset_commission(process: Process) {
1477 let process_rc = Rc::new(process);
1479 let mut asset1 = Asset::new_future(
1480 "agent1".into(),
1481 Rc::clone(&process_rc),
1482 "GBR".into(),
1483 Capacity(1.0),
1484 2020,
1485 )
1486 .unwrap();
1487 asset1.commission(AssetID(1), "");
1488 assert!(asset1.is_commissioned());
1489 assert_eq!(asset1.id(), Some(AssetID(1)));
1490
1491 let mut asset2 = Asset::new_selected(
1493 "agent1".into(),
1494 Rc::clone(&process_rc),
1495 "GBR".into(),
1496 Capacity(1.0),
1497 2020,
1498 )
1499 .unwrap();
1500 asset2.commission(AssetID(2), "");
1501 assert!(asset2.is_commissioned());
1502 assert_eq!(asset2.id(), Some(AssetID(2)));
1503 }
1504
1505 #[rstest]
1506 #[case::early_decommission_within_lifetime(2024, 2024)]
1507 #[case::decommission_at_maximum_year(2026, 2025)]
1508 fn test_asset_decommission(
1509 #[case] requested_decommission_year: u32,
1510 #[case] expected_decommission_year: u32,
1511 process: Process,
1512 ) {
1513 let process_rc = Rc::new(process);
1515 let mut asset = Asset::new_future(
1516 "agent1".into(),
1517 Rc::clone(&process_rc),
1518 "GBR".into(),
1519 Capacity(1.0),
1520 2020,
1521 )
1522 .unwrap();
1523 asset.commission(AssetID(1), "");
1524 assert!(asset.is_commissioned());
1525 assert_eq!(asset.id(), Some(AssetID(1)));
1526
1527 asset.decommission(requested_decommission_year, "");
1529 assert!(!asset.is_commissioned());
1530 assert_eq!(asset.decommission_year(), Some(expected_decommission_year));
1531 }
1532
1533 #[rstest]
1534 #[should_panic(expected = "Assets with state Candidate cannot be commissioned")]
1535 fn test_commission_wrong_states(process: Process) {
1536 let mut asset =
1537 Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap();
1538 asset.commission(AssetID(1), "");
1539 }
1540
1541 #[rstest]
1542 #[should_panic(expected = "Cannot decommission an asset that hasn't been commissioned")]
1543 fn test_decommission_wrong_state(process: Process) {
1544 let mut asset =
1545 Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap();
1546 asset.decommission(2025, "");
1547 }
1548}