1use crate::time_slice::TimeSliceID;
3use crate::units::{Activity, Capacity, Dimensionless, Money, MoneyPerActivity, MoneyPerCapacity};
4use indexmap::IndexMap;
5
6pub fn capital_recovery_factor(lifetime: u32, discount_rate: Dimensionless) -> Dimensionless {
10 if lifetime == 0 {
11 return Dimensionless(0.0);
12 }
13 if discount_rate == Dimensionless(0.0) {
14 return Dimensionless(1.0) / Dimensionless(lifetime as f64);
15 }
16
17 let factor = (Dimensionless(1.0) + discount_rate).powi(lifetime.try_into().unwrap());
18 (discount_rate * factor) / (factor - Dimensionless(1.0))
19}
20
21pub fn annual_capital_cost(
23 capital_cost: MoneyPerCapacity,
24 lifetime: u32,
25 discount_rate: Dimensionless,
26) -> MoneyPerCapacity {
27 let crf = capital_recovery_factor(lifetime, discount_rate);
28 capital_cost * crf
29}
30
31pub fn snas(
36 capacity: Capacity,
37 annual_fixed_cost: MoneyPerCapacity,
38 activity: &IndexMap<TimeSliceID, Activity>,
39 activity_costs: &IndexMap<TimeSliceID, MoneyPerActivity>,
40) -> Option<MoneyPerActivity> {
41 lcox(capacity, annual_fixed_cost, activity, activity_costs).map(|lcox| -lcox)
42}
43
44pub fn lcox(
49 capacity: Capacity,
50 annual_fixed_cost: MoneyPerCapacity,
51 activity: &IndexMap<TimeSliceID, Activity>,
52 activity_costs: &IndexMap<TimeSliceID, MoneyPerActivity>,
53) -> Option<MoneyPerActivity> {
54 let annualised_fixed_cost = annual_fixed_cost * capacity;
56
57 let mut total_activity_costs = Money(0.0);
59 let mut total_activity = Activity(0.0);
60 for (time_slice, activity) in activity {
61 let activity_cost = activity_costs[time_slice];
62 total_activity += *activity;
63 total_activity_costs += activity_cost * *activity;
64 }
65
66 (total_activity > Activity(0.0))
67 .then(|| (annualised_fixed_cost + total_activity_costs) / total_activity)
68}
69
70#[cfg(test)]
71#[allow(clippy::unreadable_literal)]
72mod tests {
73 use super::*;
74 use crate::time_slice::TimeSliceID;
75 use float_cmp::assert_approx_eq;
76 use rstest::rstest;
77
78 #[rstest]
79 #[case(0, 0.05, 0.0)] #[case(10, 0.0, 0.1)] #[case(10, 0.05, 0.1295045749654567)]
82 #[case(5, 0.03, 0.2183545714005762)]
83 fn capital_recovery_factor_works(
84 #[case] lifetime: u32,
85 #[case] discount_rate: f64,
86 #[case] expected: f64,
87 ) {
88 let result = capital_recovery_factor(lifetime, Dimensionless(discount_rate));
89 assert_approx_eq!(f64, result.0, expected, epsilon = 1e-10);
90 }
91
92 #[rstest]
93 #[case(1000.0, 10, 0.05, 129.5045749654567)]
94 #[case(500.0, 5, 0.03, 109.17728570028798)]
95 #[case(1000.0, 0, 0.05, 0.0)] #[case(2000.0, 20, 0.0, 100.0)] fn annual_capital_cost_works(
98 #[case] capital_cost: f64,
99 #[case] lifetime: u32,
100 #[case] discount_rate: f64,
101 #[case] expected: f64,
102 ) {
103 let expected = MoneyPerCapacity(expected);
104 let result = annual_capital_cost(
105 MoneyPerCapacity(capital_cost),
106 lifetime,
107 Dimensionless(discount_rate),
108 );
109 assert_approx_eq!(MoneyPerCapacity, result, expected, epsilon = 1e-8);
110 }
111
112 #[rstest]
113 #[case(
114 100.0, 50.0,
115 vec![("winter", "day", 10.0), ("summer", "night", 20.0)],
116 vec![("winter", "day", 5.0), ("summer", "night", 3.0)],
117 Some(170.33333333333334) )]
119 #[case(
120 50.0, 100.0,
121 vec![("winter", "day", 25.0)],
122 vec![("winter", "day", 0.0)],
123 Some(200.0) )]
125 #[case(
126 50.0, 100.0,
127 vec![("winter", "day", 0.0)],
128 vec![("winter", "day", 0.0)],
129 None )]
131 fn lcox_works(
132 #[case] capacity: f64,
133 #[case] annual_fixed_cost: f64,
134 #[case] activity_data: Vec<(&str, &str, f64)>,
135 #[case] cost_data: Vec<(&str, &str, f64)>,
136 #[case] expected: Option<f64>,
137 ) {
138 let activity = activity_data
139 .into_iter()
140 .map(|(season, time_of_day, value)| {
141 (
142 TimeSliceID {
143 season: season.into(),
144 time_of_day: time_of_day.into(),
145 },
146 Activity(value),
147 )
148 })
149 .collect();
150
151 let activity_costs = cost_data
152 .into_iter()
153 .map(|(season, time_of_day, value)| {
154 (
155 TimeSliceID {
156 season: season.into(),
157 time_of_day: time_of_day.into(),
158 },
159 MoneyPerActivity(value),
160 )
161 })
162 .collect();
163
164 let result = lcox(
165 Capacity(capacity),
166 MoneyPerCapacity(annual_fixed_cost),
167 &activity,
168 &activity_costs,
169 );
170
171 let expected = expected.map(MoneyPerActivity);
172 assert_approx_eq!(Option<MoneyPerActivity>, result, expected);
173 }
174}