1use crate::agent::AgentID;
3use crate::commodity::CommodityID;
4use crate::process::{Process, ProcessFlow, ProcessID, ProcessParameter};
5use crate::region::RegionID;
6use crate::time_slice::TimeSliceID;
7use crate::units::{Activity, ActivityPerCapacity, Capacity, MoneyPerActivity, MoneyPerFlow};
8use anyhow::{Context, Result, ensure};
9use indexmap::IndexMap;
10use itertools::{Itertools, chain};
11use log::debug;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::hash::{Hash, Hasher};
15use std::ops::{Deref, RangeInclusive};
16use std::rc::Rc;
17use std::slice;
18
19#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
21pub struct AssetID(u32);
22
23#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
35pub enum AssetState {
36 Commissioned {
38 id: AssetID,
40 agent_id: AgentID,
42 },
43 Decommissioned {
45 id: AssetID,
47 agent_id: AgentID,
49 decommission_year: u32,
51 },
52 Future {
54 agent_id: AgentID,
56 },
57 Selected {
59 agent_id: AgentID,
61 },
62 Candidate,
64}
65
66impl AssetState {
67 pub fn name(&self) -> &str {
69 match self {
70 AssetState::Commissioned { .. } => "Commissioned",
71 AssetState::Decommissioned { .. } => "Decommissioned",
72 AssetState::Future { .. } => "Future",
73 AssetState::Selected { .. } => "Selected",
74 AssetState::Candidate => "Candidate",
75 }
76 }
77}
78
79#[derive(Clone, PartialEq)]
81pub struct Asset {
82 state: AssetState,
84 process: Rc<Process>,
86 process_parameter: Rc<ProcessParameter>,
88 region_id: RegionID,
90 capacity: Capacity,
92 commission_year: u32,
94}
95
96impl Asset {
97 pub fn new_candidate(
99 process: Rc<Process>,
100 region_id: RegionID,
101 capacity: Capacity,
102 commission_year: u32,
103 ) -> Result<Self> {
104 Self::new_with_state(
105 AssetState::Candidate,
106 process,
107 region_id,
108 capacity,
109 commission_year,
110 )
111 }
112
113 pub fn new_future(
115 agent_id: AgentID,
116 process: Rc<Process>,
117 region_id: RegionID,
118 capacity: Capacity,
119 commission_year: u32,
120 ) -> Result<Self> {
121 check_capacity_valid_for_asset(capacity)?;
122 Self::new_with_state(
123 AssetState::Future { agent_id },
124 process,
125 region_id,
126 capacity,
127 commission_year,
128 )
129 }
130
131 #[cfg(test)]
136 fn new_selected(
137 agent_id: AgentID,
138 process: Rc<Process>,
139 region_id: RegionID,
140 capacity: Capacity,
141 commission_year: u32,
142 ) -> Result<Self> {
143 Self::new_with_state(
144 AssetState::Selected { agent_id },
145 process,
146 region_id,
147 capacity,
148 commission_year,
149 )
150 }
151
152 fn new_with_state(
154 state: AssetState,
155 process: Rc<Process>,
156 region_id: RegionID,
157 capacity: Capacity,
158 commission_year: u32,
159 ) -> Result<Self> {
160 check_region_year_valid_for_process(&process, ®ion_id, commission_year)?;
161 ensure!(capacity >= Capacity(0.0), "Capacity must be non-negative");
162
163 let process_parameter = process
168 .parameters
169 .get(&(region_id.clone(), commission_year))
170 .with_context(|| {
171 format!(
172 "No process parameters supplied for process {} in region {} in year {}. \
173 You should update process_parameters.csv.",
174 &process.id, region_id, commission_year
175 )
176 })?
177 .clone();
178
179 Ok(Self {
180 state,
181 process: process.clone(),
182 process_parameter,
183 region_id,
184 capacity,
185 commission_year,
186 })
187 }
188
189 pub fn state(&self) -> &AssetState {
191 &self.state
192 }
193
194 pub fn process_parameter(&self) -> &ProcessParameter {
196 &self.process_parameter
197 }
198
199 pub fn max_decommission_year(&self) -> u32 {
201 self.commission_year + self.process_parameter.lifetime
202 }
203
204 pub fn get_activity_limits(&self, time_slice: &TimeSliceID) -> RangeInclusive<Activity> {
206 let limits = self
207 .process
208 .activity_limits
209 .get(&(
210 self.region_id.clone(),
211 self.commission_year,
212 time_slice.clone(),
213 ))
214 .unwrap();
215 let max_act = self.max_activity();
216
217 (max_act * *limits.start())..=(max_act * *limits.end())
219 }
220
221 pub fn get_activity_per_capacity_limits(
223 &self,
224 time_slice: &TimeSliceID,
225 ) -> RangeInclusive<ActivityPerCapacity> {
226 let limits = self
227 .process
228 .activity_limits
229 .get(&(
230 self.region_id.clone(),
231 self.commission_year,
232 time_slice.clone(),
233 ))
234 .unwrap();
235 let cap2act = self.process_parameter.capacity_to_activity;
236 (cap2act * *limits.start())..=(cap2act * *limits.end())
237 }
238
239 pub fn get_operating_cost(&self, year: u32, time_slice: &TimeSliceID) -> MoneyPerActivity {
241 let flows_cost: MoneyPerActivity = self
243 .iter_flows()
244 .map(|flow| flow.get_total_cost(&self.region_id, year, time_slice))
245 .sum();
246
247 self.process_parameter.variable_operating_cost + flows_cost
248 }
249
250 pub fn get_input_cost_from_prices(
252 &self,
253 input_prices: &HashMap<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>,
254 time_slice: &TimeSliceID,
255 ) -> MoneyPerActivity {
256 self.iter_flows()
257 .filter_map(|flow| {
258 if !flow.is_input() {
259 return None;
260 }
261 let price = *input_prices.get(&(
262 flow.commodity.id.clone(),
263 self.region_id.clone(),
264 time_slice.clone(),
265 ))?;
266 Some(-flow.coeff * price)
267 })
268 .sum()
269 }
270
271 pub fn max_activity(&self) -> Activity {
273 self.capacity * self.process_parameter.capacity_to_activity
274 }
275
276 pub fn get_flow(&self, commodity_id: &CommodityID) -> Option<&ProcessFlow> {
278 self.get_flows_map().get(commodity_id)
279 }
280
281 fn get_flows_map(&self) -> &IndexMap<CommodityID, ProcessFlow> {
283 self.process
284 .flows
285 .get(&(self.region_id.clone(), self.commission_year))
286 .unwrap()
287 }
288
289 pub fn iter_flows(&self) -> impl Iterator<Item = &ProcessFlow> {
291 self.get_flows_map().values()
292 }
293
294 pub fn primary_output(&self) -> Option<&ProcessFlow> {
296 self.process
297 .primary_output
298 .as_ref()
299 .map(|commodity_id| &self.get_flows_map()[commodity_id])
300 }
301
302 pub fn is_commissioned(&self) -> bool {
304 matches!(&self.state, AssetState::Commissioned { .. })
305 }
306
307 pub fn commission_year(&self) -> u32 {
309 self.commission_year
310 }
311
312 pub fn decommission_year(&self) -> Option<u32> {
314 match &self.state {
315 AssetState::Decommissioned {
316 decommission_year, ..
317 } => Some(*decommission_year),
318 _ => None,
319 }
320 }
321
322 pub fn region_id(&self) -> &RegionID {
324 &self.region_id
325 }
326
327 pub fn process(&self) -> &Process {
329 &self.process
330 }
331
332 pub fn process_id(&self) -> &ProcessID {
334 &self.process.id
335 }
336
337 pub fn id(&self) -> Option<AssetID> {
339 match &self.state {
340 AssetState::Commissioned { id, .. } => Some(*id),
341 AssetState::Decommissioned { id, .. } => Some(*id),
342 _ => None,
343 }
344 }
345
346 pub fn agent_id(&self) -> Option<&AgentID> {
348 match &self.state {
349 AssetState::Commissioned { agent_id, .. } => Some(agent_id),
350 AssetState::Decommissioned { agent_id, .. } => Some(agent_id),
351 AssetState::Future { agent_id } => Some(agent_id),
352 AssetState::Selected { agent_id } => Some(agent_id),
353 _ => None,
354 }
355 }
356
357 pub fn capacity(&self) -> Capacity {
359 self.capacity
360 }
361
362 pub fn set_capacity(&mut self, capacity: Capacity) {
364 assert!(
365 self.state == AssetState::Candidate,
366 "set_capacity can only be called on Candidate assets"
367 );
368 assert!(capacity >= Capacity(0.0), "Capacity must be >= 0");
369 self.capacity = capacity;
370 }
371
372 pub fn increase_capacity(&mut self, capacity: Capacity) {
374 assert!(
375 self.state == AssetState::Candidate,
376 "increase_capacity can only be called on Candidate assets"
377 );
378 assert!(capacity >= Capacity(0.0), "Added capacity must be >= 0");
379 self.capacity += capacity;
380 }
381
382 fn decommission(&mut self, decommission_year: u32) {
384 let (id, agent_id) = match &self.state {
385 AssetState::Commissioned { id, agent_id } => (*id, agent_id.clone()),
386 _ => panic!("Cannot decommission an asset that hasn't been commissioned"),
387 };
388 self.state = AssetState::Decommissioned {
389 id,
390 agent_id,
391 decommission_year,
392 };
393 }
394
395 fn commission_future(&mut self, id: AssetID) {
397 let agent_id = match &self.state {
398 AssetState::Future { agent_id } => agent_id.clone(),
399 _ => panic!("commission_future can only be called on Future assets"),
400 };
401 self.state = AssetState::Commissioned { id, agent_id };
402 }
403
404 pub fn select_candidate_for_investment(&mut self, agent_id: AgentID) {
406 assert!(
407 self.state == AssetState::Candidate,
408 "select_candidate_for_investment can only be called on Candidate assets"
409 );
410 self.state = AssetState::Selected { agent_id };
411 }
412
413 fn commission_selected(&mut self, id: AssetID) {
417 check_capacity_valid_for_asset(self.capacity).unwrap();
418 let agent_id = match &self.state {
419 AssetState::Selected { agent_id } => agent_id.clone(),
420 _ => panic!("commission_selected can only be called on Selected assets"),
421 };
422 self.state = AssetState::Commissioned { id, agent_id };
423 }
424
425 pub fn as_candidate(&self, capacity: Option<Capacity>) -> Asset {
429 assert!(
430 matches!(self.state, AssetState::Commissioned { .. }),
431 "as_candidate can only be called on Commissioned assets, not {}",
432 self.state.name()
433 );
434 let mut copy = self.clone();
435 copy.state = AssetState::Candidate;
436 copy.set_capacity(capacity.unwrap_or(copy.capacity));
437 copy
438 }
439}
440
441impl std::fmt::Debug for Asset {
442 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
443 f.debug_struct("Asset")
444 .field("state", &self.state)
445 .field("process_id", &self.process_id())
446 .field("region_id", &self.region_id)
447 .field("capacity", &self.capacity)
448 .field("commission_year", &self.commission_year)
449 .finish()
450 }
451}
452
453pub fn check_region_year_valid_for_process(
455 process: &Process,
456 region_id: &RegionID,
457 year: u32,
458) -> Result<()> {
459 ensure!(
460 process.regions.contains(region_id),
461 "Process {} does not operate in region {}",
462 process.id,
463 region_id
464 );
465 ensure!(
466 process.active_for_year(year),
467 "Process {} does not operate in the year {}",
468 process.id,
469 year
470 );
471 Ok(())
472}
473
474pub fn check_capacity_valid_for_asset(capacity: Capacity) -> Result<()> {
476 ensure!(
477 capacity.is_finite() && capacity > Capacity(0.0),
478 "Capacity must be a finite, positive number"
479 );
480 Ok(())
481}
482
483#[derive(Clone, Debug)]
490pub struct AssetRef(Rc<Asset>);
491
492impl AssetRef {
493 pub fn make_mut(&mut self) -> &mut Asset {
495 Rc::make_mut(&mut self.0)
496 }
497}
498
499impl From<Rc<Asset>> for AssetRef {
500 fn from(value: Rc<Asset>) -> Self {
501 Self(value)
502 }
503}
504
505impl From<Asset> for AssetRef {
506 fn from(value: Asset) -> Self {
507 Self::from(Rc::new(value))
508 }
509}
510
511impl From<AssetRef> for Rc<Asset> {
512 fn from(value: AssetRef) -> Self {
513 value.0
514 }
515}
516
517impl Deref for AssetRef {
518 type Target = Asset;
519
520 fn deref(&self) -> &Self::Target {
521 &self.0
522 }
523}
524
525impl PartialEq for AssetRef {
526 fn eq(&self, other: &Self) -> bool {
527 Rc::ptr_eq(&self.0.process, &other.0.process)
530 && self.0.region_id == other.0.region_id
531 && self.0.commission_year == other.0.commission_year
532 && self.0.state == other.0.state
533 }
534}
535
536impl Eq for AssetRef {}
537
538impl Hash for AssetRef {
539 fn hash<H: Hasher>(&self, state: &mut H) {
545 match &self.0.state {
546 AssetState::Commissioned { id, .. } => {
547 id.hash(state);
550 }
551 AssetState::Candidate | AssetState::Selected { .. } => {
552 self.0.process.id.hash(state);
555 self.0.region_id.hash(state);
556 self.0.commission_year.hash(state);
557 self.0.agent_id().hash(state);
558 }
559 AssetState::Future { .. } | AssetState::Decommissioned { .. } => {
560 unimplemented!("Cannot hash Future or Decommissioned assets");
562 }
563 }
564 }
565}
566
567impl PartialOrd for AssetRef {
568 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
569 Some(self.cmp(other))
570 }
571}
572
573impl Ord for AssetRef {
574 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
575 self.id().unwrap().cmp(&other.id().unwrap())
576 }
577}
578
579fn decommission_assets<'a, I>(
581 assets: I,
582 year: u32,
583 reason: &'a str,
584) -> impl Iterator<Item = AssetRef> + 'a
585where
586 I: IntoIterator<Item = AssetRef> + 'a,
587{
588 assets.into_iter().map(move |mut asset| {
589 debug!(
590 "Decommissioning asset '{}' for agent '{}' (reason: {})",
591 asset.process_id(),
592 asset.agent_id().unwrap(),
593 reason
594 );
595
596 asset.make_mut().decommission(year);
597 asset
598 })
599}
600
601pub struct AssetPool {
603 active: Vec<AssetRef>,
605 future: Vec<Asset>,
607 decommissioned: Vec<AssetRef>,
609 next_id: u32,
611}
612
613impl AssetPool {
614 pub fn new(mut assets: Vec<Asset>) -> Self {
616 assets.sort_by(|a, b| a.commission_year.cmp(&b.commission_year));
618
619 Self {
620 active: Vec::new(),
621 future: assets,
622 decommissioned: Vec::new(),
623 next_id: 0,
624 }
625 }
626
627 pub fn as_slice(&self) -> &[AssetRef] {
629 &self.active
630 }
631
632 pub fn commission_new(&mut self, year: u32) {
634 let count = self
636 .future
637 .iter()
638 .take_while(|asset| asset.commission_year <= year)
639 .count();
640
641 for mut asset in self.future.drain(0..count) {
643 debug!(
644 "Commissioning asset '{}' for agent '{}' (reason: user input)",
645 asset.process_id(),
646 asset.agent_id().unwrap(),
647 );
648 asset.commission_future(AssetID(self.next_id));
649 self.next_id += 1;
650 self.active.push(asset.into());
651 }
652 }
653
654 pub fn decommission_old(&mut self, year: u32) {
656 let to_decommission = self
658 .active
659 .extract_if(.., |asset| asset.max_decommission_year() <= year);
660
661 let decommissioned = decommission_assets(to_decommission, year, "end of life");
663 self.decommissioned.extend(decommissioned);
664 }
665
666 pub fn decommission_if_not_active<I>(&mut self, assets: I, year: u32)
678 where
679 I: IntoIterator<Item = AssetRef>,
680 {
681 let to_decommission = assets.into_iter().filter(|asset| {
682 let AssetState::Commissioned { id, .. } = &asset.state else {
684 panic!("Cannot decommission asset that has not been commissioned")
685 };
686
687 !self.active.iter().any(|a| match &a.state {
689 AssetState::Commissioned { id: active_id, .. } => active_id == id,
690 _ => panic!("Active pool should only contain commissioned assets"),
691 })
692 });
693 let decommissioned = decommission_assets(to_decommission, year, "not selected");
694 self.decommissioned.extend(decommissioned);
695 }
696
697 pub fn get(&self, id: AssetID) -> Option<&AssetRef> {
704 let idx = self
706 .active
707 .binary_search_by(|asset| match &asset.state {
708 AssetState::Commissioned { id: asset_id, .. } => asset_id.cmp(&id),
709 _ => panic!("Active pool should only contain commissioned assets"),
710 })
711 .ok()?;
712
713 Some(&self.active[idx])
714 }
715
716 pub fn iter_active(&self) -> slice::Iter<'_, AssetRef> {
718 self.active.iter()
719 }
720
721 pub fn iter_decommissioned(&self) -> slice::Iter<'_, AssetRef> {
723 self.decommissioned.iter()
724 }
725
726 pub fn iter_all(&self) -> impl Iterator<Item = &AssetRef> {
730 chain(self.iter_active(), self.iter_decommissioned())
731 }
732
733 pub fn take(&mut self) -> Vec<AssetRef> {
735 std::mem::take(&mut self.active)
736 }
737
738 pub fn extend(&mut self, mut assets: Vec<AssetRef>) -> Vec<AssetRef> {
742 for asset in assets.iter_mut() {
743 match &asset.state {
744 AssetState::Commissioned { .. } => {}
745 AssetState::Selected { .. } => {
746 debug!(
747 "Commissioning asset '{}' for agent '{}' (reason: selected)",
748 asset.process_id(),
749 asset.agent_id().unwrap(),
750 );
751 asset.make_mut().commission_selected(AssetID(self.next_id));
752 self.next_id += 1;
753 }
754 _ => panic!(
755 "Cannot extend asset pool with asset in state {:?}",
756 asset.state
757 ),
758 }
759 }
760
761 self.active.extend(assets.iter().cloned());
763 self.active.sort();
764
765 debug_assert_eq!(self.active.iter().unique().count(), self.active.len());
767 assets
768 }
769}
770
771pub trait AssetIterator<'a>: Iterator<Item = &'a AssetRef> + Sized
773where
774 Self: 'a,
775{
776 fn filter_agent(self, agent_id: &'a AgentID) -> impl Iterator<Item = &'a AssetRef> + 'a {
778 self.filter(move |asset| asset.agent_id() == Some(agent_id))
779 }
780
781 fn filter_primary_producers_of(
783 self,
784 commodity_id: &'a CommodityID,
785 ) -> impl Iterator<Item = &'a AssetRef> + 'a {
786 self.filter(move |asset| {
787 asset
788 .primary_output()
789 .is_some_and(|flow| &flow.commodity.id == commodity_id)
790 })
791 }
792
793 fn filter_region(self, region_id: &'a RegionID) -> impl Iterator<Item = &'a AssetRef> + 'a {
795 self.filter(move |asset| asset.region_id == *region_id)
796 }
797
798 fn flows_for_commodity(
800 self,
801 commodity_id: &'a CommodityID,
802 ) -> impl Iterator<Item = (&'a AssetRef, &'a ProcessFlow)> + 'a {
803 self.filter_map(|asset| Some((asset, asset.get_flow(commodity_id)?)))
804 }
805}
806
807impl<'a, I> AssetIterator<'a> for I where I: Iterator<Item = &'a AssetRef> + Sized + 'a {}
808
809#[cfg(test)]
810mod tests {
811 use super::*;
812 use crate::commodity::{Commodity, CommodityID, CommodityType};
813 use crate::fixture::{
814 assert_error, commodity_id, process, process_parameter_map, region_id, time_slice,
815 };
816 use crate::process::{
817 FlowType, Process, ProcessActivityLimitsMap, ProcessFlow, ProcessFlowsMap, ProcessID,
818 ProcessParameter, ProcessParameterMap,
819 };
820 use crate::region::RegionID;
821 use crate::time_slice::{TimeSliceID, TimeSliceLevel};
822 use crate::units::{
823 ActivityPerCapacity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity,
824 MoneyPerCapacity, MoneyPerCapacityPerYear, MoneyPerFlow,
825 };
826 use indexmap::{IndexMap, IndexSet};
827 use itertools::{Itertools, assert_equal};
828 use rstest::{fixture, rstest};
829 use std::collections::HashMap;
830 use std::iter;
831 use std::rc::Rc;
832
833 #[rstest]
834 fn test_get_input_cost_from_prices(
835 region_id: RegionID,
836 commodity_id: CommodityID,
837 mut process_parameter_map: ProcessParameterMap,
838 time_slice: TimeSliceID,
839 ) {
840 let commodity = Rc::new(Commodity {
842 id: commodity_id.clone(),
843 description: "Test commodity".to_string(),
844 kind: CommodityType::ServiceDemand,
845 time_slice_level: TimeSliceLevel::Annual,
846 levies: Default::default(),
847 demand: Default::default(),
848 });
849
850 let flow = ProcessFlow {
852 commodity: commodity.clone(),
853 coeff: FlowPerActivity(-2.0), kind: FlowType::Fixed,
855 cost: MoneyPerFlow(0.0),
856 };
857
858 process_parameter_map.insert(
860 (region_id.clone(), 2020),
861 Rc::new(ProcessParameter {
862 capital_cost: Default::default(),
863 fixed_operating_cost: Default::default(),
864 variable_operating_cost: Default::default(),
865 lifetime: 1,
866 discount_rate: Default::default(),
867 capacity_to_activity: ActivityPerCapacity(1.0),
868 }),
869 );
870
871 let mut flows: HashMap<(RegionID, u32), IndexMap<CommodityID, ProcessFlow>> =
873 Default::default();
874 let mut flow_map: IndexMap<CommodityID, ProcessFlow> = Default::default();
875 flow_map.insert(commodity_id.clone(), flow);
876 flows.insert((region_id.clone(), 2020), flow_map);
877
878 let mut regions = indexmap::IndexSet::new();
879 regions.insert(region_id.clone());
880 let process = Rc::new(Process {
881 id: ProcessID::from("PROC1"),
882 description: "Test process".to_string(),
883 flows,
884 parameters: process_parameter_map,
885 regions,
886 primary_output: Some(commodity_id.clone()),
887 years: vec![2020],
888 activity_limits: Default::default(),
889 });
890
891 let asset = Asset::new_candidate(process, region_id.clone(), Capacity(1.0), 2020).unwrap();
893
894 let mut input_prices = HashMap::new();
896 input_prices.insert(
897 (commodity_id.clone(), region_id.clone(), time_slice.clone()),
898 MoneyPerFlow(3.0),
899 );
900
901 let cost = asset.get_input_cost_from_prices(&input_prices, &time_slice);
903 assert_eq!(cost.0, 6.0);
905 }
906
907 #[rstest]
908 #[case(Capacity(0.01))]
909 #[case(Capacity(0.5))]
910 #[case(Capacity(1.0))]
911 #[case(Capacity(100.0))]
912 fn test_asset_new_valid(process: Process, #[case] capacity: Capacity) {
913 let agent_id = AgentID("agent1".into());
914 let region_id = RegionID("GBR".into());
915 let asset = Asset::new_future(agent_id, process.into(), region_id, capacity, 2015).unwrap();
916 assert!(asset.id().is_none());
917 }
918
919 #[rstest]
920 #[case(Capacity(0.0))]
921 #[case(Capacity(-0.01))]
922 #[case(Capacity(-1.0))]
923 #[case(Capacity(f64::NAN))]
924 #[case(Capacity(f64::INFINITY))]
925 #[case(Capacity(f64::NEG_INFINITY))]
926 fn test_asset_new_invalid_capacity(process: Process, #[case] capacity: Capacity) {
927 let agent_id = AgentID("agent1".into());
928 let region_id = RegionID("GBR".into());
929 assert_error!(
930 Asset::new_future(agent_id, process.into(), region_id, capacity, 2015),
931 "Capacity must be a finite, positive number"
932 );
933 }
934
935 #[rstest]
936 fn test_asset_new_invalid_commission_year(process: Process) {
937 let agent_id = AgentID("agent1".into());
938 let region_id = RegionID("GBR".into());
939 assert_error!(
940 Asset::new_future(agent_id, process.into(), region_id, Capacity(1.0), 2009),
941 "Process process1 does not operate in the year 2009"
942 );
943 }
944
945 #[rstest]
946 fn test_asset_new_invalid_region(process: Process) {
947 let agent_id = AgentID("agent1".into());
948 let region_id = RegionID("FRA".into());
949 assert_error!(
950 Asset::new_future(agent_id, process.into(), region_id, Capacity(1.0), 2015),
951 "Process process1 does not operate in region FRA"
952 );
953 }
954
955 #[fixture]
956 fn asset_pool() -> AssetPool {
957 let process_param = Rc::new(ProcessParameter {
958 capital_cost: MoneyPerCapacity(5.0),
959 fixed_operating_cost: MoneyPerCapacityPerYear(2.0),
960 variable_operating_cost: MoneyPerActivity(1.0),
961 lifetime: 5,
962 discount_rate: Dimensionless(0.9),
963 capacity_to_activity: ActivityPerCapacity(1.0),
964 });
965 let years = RangeInclusive::new(2010, 2020).collect_vec();
966 let process_parameter_map: ProcessParameterMap = years
967 .iter()
968 .map(|&year| (("GBR".into(), year), process_param.clone()))
969 .collect();
970 let process = Rc::new(Process {
971 id: "process1".into(),
972 description: "Description".into(),
973 years: vec![2010, 2020],
974 activity_limits: ProcessActivityLimitsMap::new(),
975 flows: ProcessFlowsMap::new(),
976 parameters: process_parameter_map,
977 regions: IndexSet::from(["GBR".into()]),
978 primary_output: None,
979 });
980 let future = [2020, 2010]
981 .map(|year| {
982 Asset::new_future(
983 "agent1".into(),
984 Rc::clone(&process),
985 "GBR".into(),
986 Capacity(1.0),
987 year,
988 )
989 .unwrap()
990 })
991 .into_iter()
992 .collect_vec();
993
994 AssetPool::new(future)
995 }
996
997 #[fixture]
998 fn process_with_activity_limits() -> Process {
999 let process_param = Rc::new(ProcessParameter {
1000 capital_cost: MoneyPerCapacity(5.0),
1001 fixed_operating_cost: MoneyPerCapacityPerYear(2.0),
1002 variable_operating_cost: MoneyPerActivity(1.0),
1003 lifetime: 5,
1004 discount_rate: Dimensionless(0.9),
1005 capacity_to_activity: ActivityPerCapacity(3.0),
1006 });
1007 let years = RangeInclusive::new(2010, 2020).collect_vec();
1008 let process_parameter_map: ProcessParameterMap = years
1009 .iter()
1010 .map(|&year| (("GBR".into(), year), process_param.clone()))
1011 .collect();
1012 let time_slice = TimeSliceID {
1013 season: "winter".into(),
1014 time_of_day: "day".into(),
1015 };
1016 let fraction_limits = Dimensionless(1.0)..=Dimensionless(2.0);
1017 let mut activity_limits = ProcessActivityLimitsMap::new();
1018 for year in [2010, 2020] {
1019 activity_limits.insert(
1020 ("GBR".into(), year, time_slice.clone()),
1021 fraction_limits.clone(),
1022 );
1023 }
1024 Process {
1025 id: "process1".into(),
1026 description: "Description".into(),
1027 years: vec![2010, 2020],
1028 activity_limits,
1029 flows: ProcessFlowsMap::new(),
1030 parameters: process_parameter_map,
1031 regions: IndexSet::from(["GBR".into()]),
1032 primary_output: None,
1033 }
1034 }
1035
1036 #[fixture]
1037 fn asset_with_activity_limits(process_with_activity_limits: Process) -> Asset {
1038 Asset::new_future(
1039 "agent1".into(),
1040 Rc::new(process_with_activity_limits),
1041 "GBR".into(),
1042 Capacity(2.0),
1043 2010,
1044 )
1045 .unwrap()
1046 }
1047
1048 #[rstest]
1049 fn test_asset_get_activity_limits(asset_with_activity_limits: Asset, time_slice: TimeSliceID) {
1050 assert_eq!(
1051 asset_with_activity_limits.get_activity_limits(&time_slice),
1052 Activity(6.0)..=Activity(12.0)
1053 );
1054 }
1055
1056 #[rstest]
1057 fn test_asset_get_activity_per_capacity_limits(
1058 asset_with_activity_limits: Asset,
1059 time_slice: TimeSliceID,
1060 ) {
1061 assert_eq!(
1062 asset_with_activity_limits.get_activity_per_capacity_limits(&time_slice),
1063 ActivityPerCapacity(3.0)..=ActivityPerCapacity(6.0)
1064 );
1065 }
1066
1067 #[rstest]
1068 fn test_asset_pool_new(asset_pool: AssetPool) {
1069 assert!(asset_pool.active.is_empty());
1071 assert!(asset_pool.future.len() == 2);
1072 assert!(asset_pool.future[0].commission_year == 2010);
1073 assert!(asset_pool.future[1].commission_year == 2020);
1074 }
1075
1076 #[rstest]
1077 fn test_asset_pool_commission_new1(mut asset_pool: AssetPool) {
1078 asset_pool.commission_new(2010);
1080 assert_equal(asset_pool.iter_active(), iter::once(&asset_pool.active[0]));
1081 }
1082
1083 #[rstest]
1084 fn test_asset_pool_commission_new2(mut asset_pool: AssetPool) {
1085 asset_pool.commission_new(2011);
1087 assert_equal(asset_pool.iter_active(), iter::once(&asset_pool.active[0]));
1088 }
1089
1090 #[rstest]
1091 fn test_asset_pool_commission_new3(mut asset_pool: AssetPool) {
1092 asset_pool.commission_new(2000);
1094 assert!(asset_pool.iter_active().next().is_none()); }
1096
1097 #[rstest]
1098 fn test_asset_pool_decommission_old(mut asset_pool: AssetPool) {
1099 asset_pool.commission_new(2020);
1100 assert_eq!(asset_pool.active.len(), 2);
1101 asset_pool.decommission_old(2020); assert_eq!(asset_pool.active.len(), 1);
1103 assert_eq!(asset_pool.active[0].commission_year, 2020);
1104 assert_eq!(asset_pool.decommissioned.len(), 1);
1105 assert_eq!(asset_pool.decommissioned[0].commission_year, 2010);
1106 assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2020));
1107 asset_pool.decommission_old(2022); assert_eq!(asset_pool.active.len(), 1);
1109 assert_eq!(asset_pool.active[0].commission_year, 2020);
1110 assert_eq!(asset_pool.decommissioned.len(), 1);
1111 assert_eq!(asset_pool.decommissioned[0].commission_year, 2010);
1112 assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2020));
1113 asset_pool.decommission_old(2025); assert!(asset_pool.active.is_empty());
1115 assert_eq!(asset_pool.decommissioned.len(), 2);
1116 assert_eq!(asset_pool.decommissioned[0].commission_year, 2010);
1117 assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2020));
1118 assert_eq!(asset_pool.decommissioned[1].commission_year, 2020);
1119 assert_eq!(asset_pool.decommissioned[1].decommission_year(), Some(2025));
1120 }
1121
1122 #[rstest]
1123 fn test_asset_pool_get(mut asset_pool: AssetPool) {
1124 asset_pool.commission_new(2020);
1125 assert_eq!(asset_pool.get(AssetID(0)), Some(&asset_pool.active[0]));
1126 assert_eq!(asset_pool.get(AssetID(1)), Some(&asset_pool.active[1]));
1127 }
1128
1129 #[rstest]
1130 fn test_asset_pool_extend_empty(mut asset_pool: AssetPool) {
1131 asset_pool.commission_new(2020);
1133 let original_count = asset_pool.active.len();
1134
1135 asset_pool.extend(Vec::<AssetRef>::new());
1137
1138 assert_eq!(asset_pool.active.len(), original_count);
1139 }
1140
1141 #[rstest]
1142 fn test_asset_pool_extend_existing_assets(mut asset_pool: AssetPool) {
1143 asset_pool.commission_new(2020);
1145 assert_eq!(asset_pool.active.len(), 2);
1146 let existing_assets = asset_pool.take();
1147
1148 asset_pool.extend(existing_assets.clone());
1150
1151 assert_eq!(asset_pool.active.len(), 2);
1152 assert_eq!(asset_pool.active[0].id(), Some(AssetID(0)));
1153 assert_eq!(asset_pool.active[1].id(), Some(AssetID(1)));
1154 }
1155
1156 #[rstest]
1157 fn test_asset_pool_extend_new_assets(mut asset_pool: AssetPool, process: Process) {
1158 asset_pool.commission_new(2020);
1160 let original_count = asset_pool.active.len();
1161
1162 let process_rc = Rc::new(process);
1164 let new_assets = vec![
1165 Asset::new_selected(
1166 "agent2".into(),
1167 Rc::clone(&process_rc),
1168 "GBR".into(),
1169 Capacity(1.5),
1170 2015,
1171 )
1172 .unwrap()
1173 .into(),
1174 Asset::new_selected(
1175 "agent3".into(),
1176 Rc::clone(&process_rc),
1177 "GBR".into(),
1178 Capacity(2.5),
1179 2018,
1180 )
1181 .unwrap()
1182 .into(),
1183 ];
1184
1185 asset_pool.extend(new_assets);
1186
1187 assert_eq!(asset_pool.active.len(), original_count + 2);
1188 assert_eq!(asset_pool.active[original_count].id(), Some(AssetID(2)));
1190 assert_eq!(asset_pool.active[original_count + 1].id(), Some(AssetID(3)));
1191 assert_eq!(
1192 asset_pool.active[original_count].agent_id(),
1193 Some(&"agent2".into())
1194 );
1195 assert_eq!(
1196 asset_pool.active[original_count + 1].agent_id(),
1197 Some(&"agent3".into())
1198 );
1199 }
1200
1201 #[rstest]
1202 fn test_asset_pool_extend_mixed_assets(mut asset_pool: AssetPool, process: Process) {
1203 asset_pool.commission_new(2020);
1205
1206 let new_asset = Asset::new_selected(
1208 "agent_new".into(),
1209 process.into(),
1210 "GBR".into(),
1211 Capacity(3.0),
1212 2019,
1213 )
1214 .unwrap()
1215 .into();
1216
1217 asset_pool.extend(vec![new_asset]);
1219
1220 assert_eq!(asset_pool.active.len(), 3);
1221 assert!(asset_pool.active.iter().any(|a| a.id() == Some(AssetID(0))));
1223 assert!(asset_pool.active.iter().any(|a| a.id() == Some(AssetID(1))));
1224 assert!(asset_pool.active.iter().any(|a| a.id() == Some(AssetID(2))));
1225 assert!(
1227 asset_pool
1228 .active
1229 .iter()
1230 .any(|a| a.agent_id() == Some(&"agent_new".into()))
1231 );
1232 }
1233
1234 #[rstest]
1235 fn test_asset_pool_extend_maintains_sort_order(mut asset_pool: AssetPool, process: Process) {
1236 asset_pool.commission_new(2020);
1238
1239 let process_rc = Rc::new(process);
1241 let new_assets = vec![
1242 Asset::new_selected(
1243 "agent_high_id".into(),
1244 Rc::clone(&process_rc),
1245 "GBR".into(),
1246 Capacity(1.0),
1247 2016,
1248 )
1249 .unwrap()
1250 .into(),
1251 Asset::new_selected(
1252 "agent_low_id".into(),
1253 Rc::clone(&process_rc),
1254 "GBR".into(),
1255 Capacity(1.0),
1256 2017,
1257 )
1258 .unwrap()
1259 .into(),
1260 ];
1261
1262 asset_pool.extend(new_assets);
1263
1264 let ids: Vec<u32> = asset_pool
1266 .iter_active()
1267 .map(|a| a.id().unwrap().0)
1268 .collect();
1269 assert_equal(ids, 0..4);
1270 }
1271
1272 #[rstest]
1273 fn test_asset_pool_extend_no_duplicates_expected(mut asset_pool: AssetPool) {
1274 asset_pool.commission_new(2020);
1276 let original_count = asset_pool.active.len();
1277
1278 asset_pool.extend(Vec::new());
1281
1282 assert_eq!(asset_pool.active.len(), original_count);
1283 assert_eq!(
1285 asset_pool.active.iter().unique().count(),
1286 asset_pool.active.len()
1287 );
1288 }
1289
1290 #[rstest]
1291 fn test_asset_pool_extend_increments_next_id(mut asset_pool: AssetPool, process: Process) {
1292 asset_pool.commission_new(2020);
1294 assert_eq!(asset_pool.next_id, 2); let process_rc = Rc::new(process);
1298 let new_assets = vec![
1299 Asset::new_selected(
1300 "agent1".into(),
1301 Rc::clone(&process_rc),
1302 "GBR".into(),
1303 Capacity(1.0),
1304 2015,
1305 )
1306 .unwrap()
1307 .into(),
1308 Asset::new_selected(
1309 "agent2".into(),
1310 Rc::clone(&process_rc),
1311 "GBR".into(),
1312 Capacity(1.0),
1313 2016,
1314 )
1315 .unwrap()
1316 .into(),
1317 ];
1318
1319 asset_pool.extend(new_assets);
1320
1321 assert_eq!(asset_pool.next_id, 4);
1323 assert_eq!(asset_pool.active[2].id(), Some(AssetID(2)));
1324 assert_eq!(asset_pool.active[3].id(), Some(AssetID(3)));
1325 }
1326
1327 #[rstest]
1328 fn test_asset_pool_decommission_if_not_active(mut asset_pool: AssetPool) {
1329 asset_pool.commission_new(2020);
1331 assert_eq!(asset_pool.active.len(), 2);
1332 assert_eq!(asset_pool.decommissioned.len(), 0);
1333
1334 let removed_asset = asset_pool.active.remove(0);
1336 assert_eq!(asset_pool.active.len(), 1);
1337
1338 let assets_to_check = vec![removed_asset.clone(), asset_pool.active[0].clone()];
1340 asset_pool.decommission_if_not_active(assets_to_check, 2025);
1341
1342 assert_eq!(asset_pool.active.len(), 1); assert_eq!(asset_pool.decommissioned.len(), 1);
1345 assert_eq!(asset_pool.decommissioned[0].id(), removed_asset.id());
1346 assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2025));
1347 }
1348
1349 #[rstest]
1350 fn test_asset_pool_decommission_if_not_active_all_active(mut asset_pool: AssetPool) {
1351 asset_pool.commission_new(2020);
1353 assert_eq!(asset_pool.active.len(), 2);
1354 assert_eq!(asset_pool.decommissioned.len(), 0);
1355
1356 let assets_to_check = asset_pool.active.clone();
1358 asset_pool.decommission_if_not_active(assets_to_check, 2025);
1359
1360 assert_eq!(asset_pool.active.len(), 2);
1362 assert_eq!(asset_pool.decommissioned.len(), 0);
1363 }
1364
1365 #[rstest]
1366 fn test_asset_pool_decommission_if_not_active_none_active(mut asset_pool: AssetPool) {
1367 asset_pool.commission_new(2020);
1369 let all_assets = asset_pool.active.clone();
1370
1371 asset_pool.active.clear();
1373
1374 asset_pool.decommission_if_not_active(all_assets.clone(), 2025);
1376
1377 assert_eq!(asset_pool.active.len(), 0);
1379 assert_eq!(asset_pool.decommissioned.len(), 2);
1380 assert_eq!(asset_pool.decommissioned[0].id(), all_assets[0].id());
1381 assert_eq!(asset_pool.decommissioned[0].decommission_year(), Some(2025));
1382 assert_eq!(asset_pool.decommissioned[1].id(), all_assets[1].id());
1383 assert_eq!(asset_pool.decommissioned[1].decommission_year(), Some(2025));
1384 }
1385
1386 #[rstest]
1387 #[should_panic(expected = "Cannot decommission asset that has not been commissioned")]
1388 fn test_asset_pool_decommission_if_not_active_non_commissioned_asset(
1389 mut asset_pool: AssetPool,
1390 process: Process,
1391 ) {
1392 let non_commissioned_asset = Asset::new_future(
1394 "agent_new".into(),
1395 process.into(),
1396 "GBR".into(),
1397 Capacity(1.0),
1398 2015,
1399 )
1400 .unwrap()
1401 .into();
1402
1403 asset_pool.decommission_if_not_active(vec![non_commissioned_asset], 2025);
1405 }
1406
1407 #[rstest]
1408 fn test_asset_state_transitions(process: Process) {
1409 let process_rc = Rc::new(process);
1411 let mut asset1 = Asset::new_future(
1412 "agent1".into(),
1413 Rc::clone(&process_rc),
1414 "GBR".into(),
1415 Capacity(1.0),
1416 2020,
1417 )
1418 .unwrap();
1419 asset1.commission_future(AssetID(1));
1420 assert!(asset1.is_commissioned());
1421 assert_eq!(asset1.id(), Some(AssetID(1)));
1422
1423 let mut asset2 = Asset::new_selected(
1425 "agent1".into(),
1426 Rc::clone(&process_rc),
1427 "GBR".into(),
1428 Capacity(1.0),
1429 2020,
1430 )
1431 .unwrap();
1432 asset2.commission_selected(AssetID(2));
1433 assert!(asset2.is_commissioned());
1434 assert_eq!(asset2.id(), Some(AssetID(2)));
1435
1436 asset1.decommission(2025);
1438 assert!(!asset1.is_commissioned());
1439 assert_eq!(asset1.decommission_year(), Some(2025));
1440 }
1441
1442 #[rstest]
1443 #[should_panic(expected = "commission_future can only be called on Future assets")]
1444 fn test_commission_future_wrong_states(process: Process) {
1445 let mut asset =
1446 Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap();
1447 asset.commission_future(AssetID(1));
1448 }
1449
1450 #[rstest]
1451 #[should_panic(expected = "commission_selected can only be called on Selected assets")]
1452 fn test_commission_candidate_wrong_state(process: Process) {
1453 let mut asset = Asset::new_future(
1454 "agent1".into(),
1455 process.into(),
1456 "GBR".into(),
1457 Capacity(1.0),
1458 2020,
1459 )
1460 .unwrap();
1461 asset.commission_selected(AssetID(1));
1462 }
1463
1464 #[rstest]
1465 #[should_panic(expected = "Cannot decommission an asset that hasn't been commissioned")]
1466 fn test_decommission_wrong_state(process: Process) {
1467 let mut asset =
1468 Asset::new_candidate(process.into(), "GBR".into(), Capacity(1.0), 2020).unwrap();
1469 asset.decommission(2025);
1470 }
1471}