1use super::DemandMap;
3use crate::agent::ObjectiveType;
4use crate::asset::{Asset, AssetRef};
5use crate::commodity::Commodity;
6use crate::finance::{lcox, snas};
7use crate::model::Model;
8use crate::time_slice::TimeSliceID;
9use crate::units::{Activity, MoneyPerActivity, MoneyPerCapacity};
10use anyhow::Result;
11use costs::annual_fixed_cost;
12use erased_serde::Serialize as ErasedSerialize;
13use indexmap::IndexMap;
14use optimisation::ResultsMap;
15use serde::Serialize;
16use std::any::Any;
17use std::cmp::Ordering;
18use std::sync::Arc;
19
20pub mod coefficients;
21mod constraints;
22mod costs;
23mod optimisation;
24use coefficients::ObjectiveCoefficients;
25use float_cmp::{ApproxEq, F64Margin};
26use optimisation::perform_optimisation;
27
28fn compare_approx<T>(a: T, b: T) -> Ordering
41where
42 T: Copy + PartialOrd + ApproxEq<Margin = F64Margin>,
43{
44 if a.approx_eq(b, F64Margin::default()) {
45 Ordering::Equal
46 } else {
47 a.partial_cmp(&b).expect("Cannot compare NaN values")
48 }
49}
50
51pub struct AppraisalOutput {
53 pub asset: AssetRef,
55 pub activity: IndexMap<TimeSliceID, Activity>,
57 pub unmet_demand: DemandMap,
59 pub metric: Option<Box<dyn MetricTrait>>,
61 pub coefficients: Arc<ObjectiveCoefficients>,
63}
64
65impl AppraisalOutput {
66 fn new<T: MetricTrait>(
68 asset: AssetRef,
69 results: ResultsMap,
70 metric: Option<T>,
71 coefficients: Arc<ObjectiveCoefficients>,
72 ) -> Self {
73 Self {
74 asset,
75 activity: results.activity,
76 unmet_demand: results.unmet_demand,
77 metric: metric.map(|m| Box::new(m) as Box<dyn MetricTrait>),
78 coefficients,
79 }
80 }
81 pub fn compare_metric(&self, other: &Self) -> Ordering {
90 let (metric1, metric2) = self
91 .metric
92 .as_deref()
93 .zip(other.metric.as_deref())
94 .expect("Cannot compare non-valid outputs");
95
96 metric1.compare(metric2)
98 }
99}
100
101pub trait MetricTrait: ComparableMetric + ErasedSerialize {}
103erased_serde::serialize_trait_object!(MetricTrait);
104
105pub trait ComparableMetric: Any + Send + Sync {
110 fn value(&self) -> f64;
112
113 fn compare(&self, other: &dyn ComparableMetric) -> Ordering;
123
124 fn as_any(&self) -> &dyn Any;
126}
127
128#[derive(Debug, Clone, Serialize)]
133pub struct LCOXMetric {
134 pub cost: MoneyPerActivity,
136}
137
138impl LCOXMetric {
139 pub fn new(cost: MoneyPerActivity) -> Self {
141 Self { cost }
142 }
143}
144
145impl ComparableMetric for LCOXMetric {
146 fn value(&self) -> f64 {
147 self.cost.value()
148 }
149
150 fn compare(&self, other: &dyn ComparableMetric) -> Ordering {
151 let other = other
152 .as_any()
153 .downcast_ref::<Self>()
154 .expect("Cannot compare metrics of different types");
155
156 compare_approx(self.cost, other.cost)
157 }
158
159 fn as_any(&self) -> &dyn Any {
160 self
161 }
162}
163
164impl MetricTrait for LCOXMetric {}
166
167#[derive(Debug, Clone, Serialize)]
172pub struct NPVMetric {
173 pub snas: MoneyPerActivity,
175}
176
177impl NPVMetric {
178 pub fn new(snas: MoneyPerActivity) -> Self {
180 Self { snas }
181 }
182}
183
184impl ComparableMetric for NPVMetric {
185 fn value(&self) -> f64 {
186 self.snas.value()
187 }
188
189 fn compare(&self, other: &dyn ComparableMetric) -> Ordering {
190 let other = other
191 .as_any()
192 .downcast_ref::<Self>()
193 .expect("Cannot compare metrics of different types");
194
195 compare_approx(other.snas, self.snas)
196 }
197
198 fn as_any(&self) -> &dyn Any {
199 self
200 }
201}
202
203impl MetricTrait for NPVMetric {}
205
206fn calculate_lcox(
216 model: &Model,
217 asset: &AssetRef,
218 commodity: &Commodity,
219 coefficients: &Arc<ObjectiveCoefficients>,
220 demand: &DemandMap,
221) -> Result<AppraisalOutput> {
222 let results = perform_optimisation(model, asset, commodity, coefficients, demand)?;
223
224 let cost_index = lcox(
225 asset.total_capacity(),
226 annual_fixed_cost(asset),
227 &results.activity,
228 &coefficients.market_costs,
229 );
230
231 Ok(AppraisalOutput::new(
232 asset.clone(),
233 results,
234 cost_index.map(LCOXMetric::new),
235 coefficients.clone(),
236 ))
237}
238
239fn calculate_npv(
245 model: &Model,
246 asset: &AssetRef,
247 commodity: &Commodity,
248 coefficients: &Arc<ObjectiveCoefficients>,
249 demand: &DemandMap,
250) -> Result<AppraisalOutput> {
251 let results = perform_optimisation(model, asset, commodity, coefficients, demand)?;
252
253 let annual_fixed_cost = annual_fixed_cost(asset);
254 assert!(
255 annual_fixed_cost >= MoneyPerCapacity(0.0),
256 "The current NPV calculation does not support negative annual fixed costs"
257 );
258
259 let snas = snas(
260 asset.total_capacity(),
261 annual_fixed_cost,
262 &results.activity,
263 &coefficients.market_costs,
264 );
265
266 Ok(AppraisalOutput::new(
267 asset.clone(),
268 results,
269 snas.map(NPVMetric::new),
270 coefficients.clone(),
271 ))
272}
273
274pub fn appraise_investment(
281 model: &Model,
282 asset: &AssetRef,
283 commodity: &Commodity,
284 objective_type: &ObjectiveType,
285 coefficients: &Arc<ObjectiveCoefficients>,
286 demand: &DemandMap,
287) -> Result<AppraisalOutput> {
288 let appraisal_method = match objective_type {
289 ObjectiveType::LevelisedCostOfX => calculate_lcox,
290 ObjectiveType::NetPresentValue => calculate_npv,
291 };
292 appraisal_method(model, asset, commodity, coefficients, demand)
293}
294
295fn compare_asset_fallback(asset1: &Asset, asset2: &Asset) -> Ordering {
301 (asset2.is_commissioned(), asset2.commission_year())
302 .cmp(&(asset1.is_commissioned(), asset1.commission_year()))
303}
304
305pub fn sort_and_filter_appraisal_outputs(outputs: &mut Vec<AppraisalOutput>) -> usize {
319 let old_len = outputs.len();
320 outputs.retain(|output| output.metric.is_some());
321 let num_nonfeasible = old_len - outputs.len();
322
323 outputs.sort_by(|output1, output2| match output1.compare_metric(output2) {
324 Ordering::Equal => compare_asset_fallback(&output1.asset, &output2.asset),
326 cmp => cmp,
327 });
328
329 num_nonfeasible
330}
331
332pub fn count_equal_and_best_appraisal_outputs(outputs: &[AppraisalOutput]) -> usize {
335 if outputs.is_empty() {
336 return 0;
337 }
338 outputs[1..]
339 .iter()
340 .take_while(|output| {
341 output.compare_metric(&outputs[0]).is_eq()
342 && compare_asset_fallback(&output.asset, &outputs[0].asset).is_eq()
343 })
344 .count()
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350 use crate::agent::AgentID;
351 use crate::asset::AssetCapacity;
352 use crate::fixture::{agent_id, asset, process, region_id};
353 use crate::process::Process;
354 use crate::region::RegionID;
355 use crate::units::{Capacity, MoneyPerActivity};
356 use float_cmp::assert_approx_eq;
357 use rstest::rstest;
358 use std::sync::Arc;
359
360 #[rstest]
362 #[case(10.0, 10.0, Ordering::Equal, "equal_costs")]
363 #[case(5.0, 10.0, Ordering::Less, "first_lower_cost_is_better")]
364 #[case(10.0, 5.0, Ordering::Greater, "second_lower_cost_is_better")]
365 fn lcox_metric_comparison(
366 #[case] cost1: f64,
367 #[case] cost2: f64,
368 #[case] expected: Ordering,
369 #[case] description: &str,
370 ) {
371 let metric1 = LCOXMetric::new(MoneyPerActivity(cost1));
372 let metric2 = LCOXMetric::new(MoneyPerActivity(cost2));
373
374 assert_eq!(
375 metric1.compare(&metric2),
376 expected,
377 "Failed comparison for case: {description}"
378 );
379 }
380
381 #[rstest]
383 #[case(10.0, 10.0, Ordering::Equal, "equal_costs")]
384 #[case(5.0, 10.0, Ordering::Greater, "second_higher_metric_is_better")]
385 #[case(10.0, 5.0, Ordering::Less, "first_higher_metric_is_better")]
386 fn npv_metric_comparison(
387 #[case] cost1: f64,
388 #[case] cost2: f64,
389 #[case] expected: Ordering,
390 #[case] description: &str,
391 ) {
392 let metric1 = NPVMetric::new(MoneyPerActivity(cost1));
393 let metric2 = NPVMetric::new(MoneyPerActivity(cost2));
394
395 assert_eq!(
396 metric1.compare(&metric2),
397 expected,
398 "Failed comparison for case: {description}"
399 );
400 }
401
402 #[rstest]
403 fn compare_assets_fallback(process: Process, region_id: RegionID, agent_id: AgentID) {
404 let capacity = Capacity(2.0);
405 let process = Arc::new(process);
406 let asset1 = Asset::new_commissioned(
407 agent_id.clone(),
408 process.clone(),
409 region_id.clone(),
410 AssetCapacity::single(capacity),
411 2015,
412 )
413 .unwrap();
414 let asset2 =
415 Asset::new_candidate(process.clone(), region_id.clone(), capacity, 2015).unwrap();
416 let asset3 = Asset::new_commissioned(
417 agent_id,
418 process,
419 region_id.clone(),
420 AssetCapacity::single(capacity),
421 2010,
422 )
423 .unwrap();
424
425 assert!(compare_asset_fallback(&asset1, &asset1).is_eq());
426 assert!(compare_asset_fallback(&asset2, &asset2).is_eq());
427 assert!(compare_asset_fallback(&asset3, &asset3).is_eq());
428 assert!(compare_asset_fallback(&asset1, &asset2).is_lt());
429 assert!(compare_asset_fallback(&asset2, &asset1).is_gt());
430 assert!(compare_asset_fallback(&asset1, &asset3).is_lt());
431 assert!(compare_asset_fallback(&asset3, &asset1).is_gt());
432 assert!(compare_asset_fallback(&asset3, &asset2).is_lt());
433 assert!(compare_asset_fallback(&asset2, &asset3).is_gt());
434 }
435
436 fn objective_coeffs() -> Arc<ObjectiveCoefficients> {
437 Arc::new(ObjectiveCoefficients {
438 activity_coefficients: IndexMap::new(),
439 market_costs: IndexMap::new(),
440 })
441 }
442
443 fn appraisal_outputs(
449 assets: Vec<Asset>,
450 metrics: Vec<Box<dyn MetricTrait>>,
451 ) -> Vec<AppraisalOutput> {
452 assert_eq!(
453 assets.len(),
454 metrics.len(),
455 "assets and metrics must have the same length"
456 );
457
458 assets
459 .into_iter()
460 .zip(metrics)
461 .map(|(asset, metric)| AppraisalOutput {
462 asset: AssetRef::from(asset),
463 coefficients: objective_coeffs(),
464 activity: IndexMap::new(),
465 unmet_demand: IndexMap::new(),
466 metric: Some(metric),
467 })
468 .collect()
469 }
470
471 fn appraisal_outputs_with_investment_priority_invariant_to_assets(
474 metrics: Vec<Box<dyn MetricTrait>>,
475 asset: &Asset,
476 ) -> Vec<AppraisalOutput> {
477 let assets = vec![asset.clone(); metrics.len()];
478 appraisal_outputs(assets, metrics)
479 }
480
481 #[rstest]
483 fn appraisal_sort_by_lcox_metric(asset: Asset) {
484 let metrics: Vec<Box<dyn MetricTrait>> = vec![
485 Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
486 Box::new(LCOXMetric::new(MoneyPerActivity(3.0))),
487 Box::new(LCOXMetric::new(MoneyPerActivity(7.0))),
488 ];
489
490 let mut outputs =
491 appraisal_outputs_with_investment_priority_invariant_to_assets(metrics, &asset);
492 sort_and_filter_appraisal_outputs(&mut outputs);
493
494 assert_approx_eq!(f64, outputs[0].metric.as_ref().unwrap().value(), 3.0); assert_approx_eq!(f64, outputs[1].metric.as_ref().unwrap().value(), 5.0);
496 assert_approx_eq!(f64, outputs[2].metric.as_ref().unwrap().value(), 7.0); }
498
499 #[rstest]
501 fn appraisal_sort_by_npv_metric(asset: Asset) {
502 let metrics: Vec<Box<dyn MetricTrait>> = vec![
503 Box::new(NPVMetric::new(MoneyPerActivity(5.0))),
504 Box::new(NPVMetric::new(MoneyPerActivity(3.0))),
505 Box::new(NPVMetric::new(MoneyPerActivity(7.0))),
506 ];
507
508 let mut outputs =
509 appraisal_outputs_with_investment_priority_invariant_to_assets(metrics, &asset);
510 sort_and_filter_appraisal_outputs(&mut outputs);
511
512 assert_approx_eq!(f64, outputs[0].metric.as_ref().unwrap().value(), 7.0); assert_approx_eq!(f64, outputs[1].metric.as_ref().unwrap().value(), 5.0);
514 assert_approx_eq!(f64, outputs[2].metric.as_ref().unwrap().value(), 3.0); }
516
517 #[rstest]
519 #[should_panic(expected = "Cannot compare metrics of different types")]
520 fn appraisal_sort_by_mixed_metrics_panics(asset: Asset) {
521 let metrics: Vec<Box<dyn MetricTrait>> = vec![
522 Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
523 Box::new(NPVMetric::new(MoneyPerActivity(3.0))),
524 Box::new(LCOXMetric::new(MoneyPerActivity(3.0))),
525 ];
526
527 let mut outputs =
528 appraisal_outputs_with_investment_priority_invariant_to_assets(metrics, &asset);
529 sort_and_filter_appraisal_outputs(&mut outputs);
531 }
532
533 #[rstest]
535 fn appraisal_sort_by_commission_year_when_metrics_equal(
536 process: Process,
537 region_id: RegionID,
538 agent_id: AgentID,
539 ) {
540 let process_rc = Arc::new(process);
541 let capacity = Capacity(10.0);
542 let commission_years = [2015, 2020, 2010];
543
544 let assets: Vec<_> = commission_years
545 .iter()
546 .map(|&year| {
547 Asset::new_commissioned(
548 agent_id.clone(),
549 process_rc.clone(),
550 region_id.clone(),
551 AssetCapacity::single(capacity),
552 year,
553 )
554 .unwrap()
555 })
556 .collect();
557
558 let metrics: Vec<Box<dyn MetricTrait>> = vec![
560 Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
561 Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
562 Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
563 ];
564
565 let mut outputs = appraisal_outputs(assets, metrics);
566 sort_and_filter_appraisal_outputs(&mut outputs);
567
568 assert_eq!(outputs[0].asset.commission_year(), 2020);
570 assert_eq!(outputs[1].asset.commission_year(), 2015);
571 assert_eq!(outputs[2].asset.commission_year(), 2010);
572 }
573
574 #[rstest]
576 fn appraisal_sort_maintains_order_when_all_equal(process: Process, region_id: RegionID) {
577 let process_rc = Arc::new(process);
578 let capacity = AssetCapacity::single(Capacity(10.0));
579 let commission_year = 2015;
580 let agent_ids = ["agent1", "agent2", "agent3"];
581
582 let assets: Vec<_> = agent_ids
583 .iter()
584 .map(|&id| {
585 Asset::new_commissioned(
586 AgentID(id.into()),
587 process_rc.clone(),
588 region_id.clone(),
589 capacity,
590 commission_year,
591 )
592 .unwrap()
593 })
594 .collect();
595
596 let metrics: Vec<Box<dyn MetricTrait>> = vec![
597 Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
598 Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
599 Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
600 ];
601
602 let mut outputs = appraisal_outputs(assets.clone(), metrics);
603 sort_and_filter_appraisal_outputs(&mut outputs);
604
605 for (&expected_id, output) in agent_ids.iter().zip(outputs) {
607 assert_eq!(output.asset.agent_id(), Some(&AgentID(expected_id.into())));
608 }
609 }
610
611 #[rstest]
613 fn appraisal_sort_commissioned_before_uncommissioned_when_metrics_equal(
614 process: Process,
615 region_id: RegionID,
616 agent_id: AgentID,
617 ) {
618 let process_rc = Arc::new(process);
619 let capacity = Capacity(10.0);
620
621 let commissioned_asset_newer = Asset::new_commissioned(
623 agent_id.clone(),
624 process_rc.clone(),
625 region_id.clone(),
626 AssetCapacity::single(capacity),
627 2020,
628 )
629 .unwrap();
630
631 let commissioned_asset_older = Asset::new_commissioned(
632 agent_id.clone(),
633 process_rc.clone(),
634 region_id.clone(),
635 AssetCapacity::single(capacity),
636 2015,
637 )
638 .unwrap();
639
640 let candidate_asset =
641 Asset::new_candidate(process_rc.clone(), region_id.clone(), capacity, 2020).unwrap();
642
643 let assets = vec![
644 candidate_asset.clone(),
645 commissioned_asset_older.clone(),
646 candidate_asset.clone(),
647 commissioned_asset_newer.clone(),
648 ];
649
650 let metrics: Vec<Box<dyn MetricTrait>> = vec![
652 Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
653 Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
654 Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
655 Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
656 ];
657
658 let mut outputs = appraisal_outputs(assets, metrics);
659 sort_and_filter_appraisal_outputs(&mut outputs);
660
661 assert!(outputs[0].asset.is_commissioned());
663 assert_eq!(outputs[0].asset.commission_year(), 2020);
664 assert!(outputs[1].asset.is_commissioned());
665 assert_eq!(outputs[1].asset.commission_year(), 2015);
666
667 assert!(!outputs[2].asset.is_commissioned());
669 assert!(!outputs[3].asset.is_commissioned());
670 }
671
672 #[rstest]
674 fn appraisal_metric_is_prioritised_over_asset_properties(
675 process: Process,
676 region_id: RegionID,
677 agent_id: AgentID,
678 ) {
679 let process_rc = Arc::new(process);
680 let capacity = Capacity(10.0);
681
682 let commissioned_asset_newer = Asset::new_commissioned(
684 agent_id.clone(),
685 process_rc.clone(),
686 region_id.clone(),
687 AssetCapacity::single(capacity),
688 2020,
689 )
690 .unwrap();
691
692 let commissioned_asset_older = Asset::new_commissioned(
693 agent_id.clone(),
694 process_rc.clone(),
695 region_id.clone(),
696 AssetCapacity::single(capacity),
697 2015,
698 )
699 .unwrap();
700
701 let candidate_asset =
702 Asset::new_candidate(process_rc.clone(), region_id.clone(), capacity, 2020).unwrap();
703
704 let assets = vec![
705 candidate_asset.clone(),
706 commissioned_asset_older.clone(),
707 candidate_asset.clone(),
708 commissioned_asset_newer.clone(),
709 ];
710
711 let baseline_metric_value = 5.0;
713 let best_metric_value = baseline_metric_value - 1e-5;
714 let metrics: Vec<Box<dyn MetricTrait>> = vec![
715 Box::new(LCOXMetric::new(MoneyPerActivity(best_metric_value))),
716 Box::new(LCOXMetric::new(MoneyPerActivity(baseline_metric_value))),
717 Box::new(LCOXMetric::new(MoneyPerActivity(baseline_metric_value))),
718 Box::new(LCOXMetric::new(MoneyPerActivity(baseline_metric_value))),
719 ];
720
721 let mut outputs = appraisal_outputs(assets, metrics);
722 sort_and_filter_appraisal_outputs(&mut outputs);
723
724 assert_approx_eq!(
726 f64,
727 outputs[0].metric.as_ref().unwrap().value(),
728 best_metric_value
729 );
730 }
731
732 #[rstest]
734 fn appraisal_sort_filters_invalid_metric(asset: Asset) {
735 let output = AppraisalOutput {
736 asset: AssetRef::from(asset),
737 coefficients: objective_coeffs(),
738 activity: IndexMap::new(),
739 unmet_demand: IndexMap::new(),
740 metric: None,
741 };
742 let mut outputs = vec![output];
743
744 sort_and_filter_appraisal_outputs(&mut outputs);
745
746 assert_eq!(outputs.len(), 0);
748 }
749
750 #[rstest]
753 #[case(vec![5.0], 0, "single_element")]
754 #[case(vec![5.0, 5.0, 5.0], 2, "all_equal_returns_len_minus_one")]
755 #[case(vec![1.0, 2.0, 3.0], 0, "none_equal_to_best")]
756 #[case(vec![5.0, 5.0, 9.0], 1, "partial_equality_stops_at_first_difference")]
757 #[case(vec![5.0, 5.0, 9.0, 5.0], 1, "equality_does_not_resume_after_gap")]
758 fn count_equal_best_lcox_metric(
759 asset: Asset,
760 #[case] metric_values: Vec<f64>,
761 #[case] expected_count: usize,
762 #[case] description: &str,
763 ) {
764 let metrics: Vec<Box<dyn MetricTrait>> = metric_values
765 .into_iter()
766 .map(|v| Box::new(LCOXMetric::new(MoneyPerActivity(v))) as Box<dyn MetricTrait>)
767 .collect();
768
769 let outputs =
770 appraisal_outputs_with_investment_priority_invariant_to_assets(metrics, &asset);
771
772 assert_eq!(
773 count_equal_and_best_appraisal_outputs(&outputs),
774 expected_count,
775 "Failed for case: {description}"
776 );
777 }
778
779 #[test]
781 fn count_equal_best_empty_slice_returns_zero() {
782 let outputs: Vec<AppraisalOutput> = vec![];
783 assert_eq!(count_equal_and_best_appraisal_outputs(&outputs), 0);
784 }
785
786 #[rstest]
789 fn count_equal_best_equal_metric_different_fallback_returns_zero(
790 process: Process,
791 region_id: RegionID,
792 agent_id: AgentID,
793 ) {
794 let process_rc = Arc::new(process);
795 let capacity = Capacity(10.0);
796
797 let commissioned = Asset::new_commissioned(
798 agent_id.clone(),
799 process_rc.clone(),
800 region_id.clone(),
801 AssetCapacity::single(capacity),
802 2020,
803 )
804 .unwrap();
805 let candidate =
806 Asset::new_candidate(process_rc.clone(), region_id.clone(), capacity, 2020).unwrap();
807
808 let metric_value = MoneyPerActivity(5.0);
809 let outputs = appraisal_outputs(
810 vec![commissioned, candidate],
811 vec![
812 Box::new(LCOXMetric::new(metric_value)),
813 Box::new(LCOXMetric::new(metric_value)),
814 ],
815 );
816
817 assert_eq!(count_equal_and_best_appraisal_outputs(&outputs), 0);
818 }
819
820 #[rstest]
823 fn count_equal_best_equal_metric_and_equal_fallback_returns_one(
824 process: Process,
825 region_id: RegionID,
826 agent_id: AgentID,
827 ) {
828 let process_rc = Arc::new(process);
829 let capacity = Capacity(10.0);
830 let year = 2020;
831
832 let asset1 = Asset::new_commissioned(
833 agent_id.clone(),
834 process_rc.clone(),
835 region_id.clone(),
836 AssetCapacity::single(capacity),
837 year,
838 )
839 .unwrap();
840 let asset2 = Asset::new_commissioned(
841 agent_id.clone(),
842 process_rc.clone(),
843 region_id.clone(),
844 AssetCapacity::single(capacity),
845 year,
846 )
847 .unwrap();
848
849 let metric_value = MoneyPerActivity(5.0);
850 let outputs = appraisal_outputs(
851 vec![asset1, asset2],
852 vec![
853 Box::new(LCOXMetric::new(metric_value)),
854 Box::new(LCOXMetric::new(metric_value)),
855 ],
856 );
857
858 assert_eq!(count_equal_and_best_appraisal_outputs(&outputs), 1);
859 }
860}