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 profitability_index(
33 capacity: Capacity,
34 annual_fixed_cost: MoneyPerCapacity,
35 activity: &IndexMap<TimeSliceID, Activity>,
36 activity_surpluses: &IndexMap<TimeSliceID, MoneyPerActivity>,
37) -> Dimensionless {
38 let annualised_fixed_cost = annual_fixed_cost * capacity;
40
41 let mut total_annualised_surplus = Money(0.0);
43 for (time_slice, activity) in activity {
44 let activity_surplus = activity_surpluses[time_slice];
45 total_annualised_surplus += activity_surplus * *activity;
46 }
47
48 total_annualised_surplus / annualised_fixed_cost
49}
50
51pub fn lcox(
53 capacity: Capacity,
54 annual_fixed_cost: MoneyPerCapacity,
55 activity: &IndexMap<TimeSliceID, Activity>,
56 activity_costs: &IndexMap<TimeSliceID, MoneyPerActivity>,
57) -> MoneyPerActivity {
58 let annualised_fixed_cost = annual_fixed_cost * capacity;
60
61 let mut total_activity_costs = Money(0.0);
63 let mut total_activity = Activity(0.0);
64 for (time_slice, activity) in activity {
65 let activity_cost = activity_costs[time_slice];
66 total_activity += *activity;
67 total_activity_costs += activity_cost * *activity;
68 }
69
70 (annualised_fixed_cost + total_activity_costs) / total_activity
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use crate::time_slice::TimeSliceID;
77 use float_cmp::assert_approx_eq;
78 use indexmap::indexmap;
79 use rstest::rstest;
80
81 #[rstest]
82 #[case(0, 0.05, 0.0)] #[case(10, 0.0, 0.1)] #[case(10, 0.05, 0.1295045749654567)]
85 #[case(5, 0.03, 0.2183545714005762)]
86 fn test_capital_recovery_factor(
87 #[case] lifetime: u32,
88 #[case] discount_rate: f64,
89 #[case] expected: f64,
90 ) {
91 let result = capital_recovery_factor(lifetime, Dimensionless(discount_rate));
92 assert_approx_eq!(f64, result.0, expected, epsilon = 1e-10);
93 }
94
95 #[rstest]
96 #[case(1000.0, 10, 0.05, 129.5045749654567)]
97 #[case(500.0, 5, 0.03, 109.17728570028798)]
98 #[case(1000.0, 0, 0.05, 0.0)] #[case(2000.0, 20, 0.0, 100.0)] fn test_annual_capital_cost(
101 #[case] capital_cost: f64,
102 #[case] lifetime: u32,
103 #[case] discount_rate: f64,
104 #[case] expected: f64,
105 ) {
106 let expected = MoneyPerCapacity(expected);
107 let result = annual_capital_cost(
108 MoneyPerCapacity(capital_cost),
109 lifetime,
110 Dimensionless(discount_rate),
111 );
112 assert_approx_eq!(MoneyPerCapacity, result, expected, epsilon = 1e-8);
113 }
114
115 #[rstest]
116 #[case(
117 100.0, 50.0,
118 vec![("winter", "day", 10.0), ("summer", "night", 15.0)],
119 vec![("winter", "day", 30.0), ("summer", "night", 20.0)],
120 0.12 )]
122 #[case(
123 50.0, 100.0,
124 vec![("q1", "peak", 5.0)],
125 vec![("q1", "peak", 40.0)],
126 0.04 )]
128 #[case(
129 0.0, 100.0,
130 vec![("winter", "day", 10.0)],
131 vec![("winter", "day", 50.0)],
132 f64::INFINITY )]
134 fn test_profitability_index(
135 #[case] capacity: f64,
136 #[case] annual_fixed_cost: f64,
137 #[case] activity_data: Vec<(&str, &str, f64)>,
138 #[case] surplus_data: Vec<(&str, &str, f64)>,
139 #[case] expected: f64,
140 ) {
141 let activity = activity_data
142 .into_iter()
143 .map(|(season, time_of_day, value)| {
144 (
145 TimeSliceID {
146 season: season.into(),
147 time_of_day: time_of_day.into(),
148 },
149 Activity(value),
150 )
151 })
152 .collect();
153
154 let activity_surpluses = surplus_data
155 .into_iter()
156 .map(|(season, time_of_day, value)| {
157 (
158 TimeSliceID {
159 season: season.into(),
160 time_of_day: time_of_day.into(),
161 },
162 MoneyPerActivity(value),
163 )
164 })
165 .collect();
166
167 let result = profitability_index(
168 Capacity(capacity),
169 MoneyPerCapacity(annual_fixed_cost),
170 &activity,
171 &activity_surpluses,
172 );
173
174 assert_approx_eq!(Dimensionless, result, Dimensionless(expected));
175 }
176
177 #[test]
178 fn test_profitability_index_zero_activity() {
179 let capacity = Capacity(100.0);
180 let annual_fixed_cost = MoneyPerCapacity(50.0);
181 let activity = indexmap! {};
182 let activity_surpluses = indexmap! {};
183
184 let result =
185 profitability_index(capacity, annual_fixed_cost, &activity, &activity_surpluses);
186 assert_eq!(result, Dimensionless(0.0));
187 }
188
189 #[rstest]
190 #[case(
191 100.0, 50.0,
192 vec![("winter", "day", 10.0), ("summer", "night", 20.0)],
193 vec![("winter", "day", 5.0), ("summer", "night", 3.0)],
194 170.33333333333334 )]
196 #[case(
197 50.0, 100.0,
198 vec![("winter", "day", 25.0)],
199 vec![("winter", "day", 0.0)],
200 200.0 )]
202 fn test_lcox(
203 #[case] capacity: f64,
204 #[case] annual_fixed_cost: f64,
205 #[case] activity_data: Vec<(&str, &str, f64)>,
206 #[case] cost_data: Vec<(&str, &str, f64)>,
207 #[case] expected: f64,
208 ) {
209 let activity = activity_data
210 .into_iter()
211 .map(|(season, time_of_day, value)| {
212 (
213 TimeSliceID {
214 season: season.into(),
215 time_of_day: time_of_day.into(),
216 },
217 Activity(value),
218 )
219 })
220 .collect();
221
222 let activity_costs = cost_data
223 .into_iter()
224 .map(|(season, time_of_day, value)| {
225 (
226 TimeSliceID {
227 season: season.into(),
228 time_of_day: time_of_day.into(),
229 },
230 MoneyPerActivity(value),
231 )
232 })
233 .collect();
234
235 let result = lcox(
236 Capacity(capacity),
237 MoneyPerCapacity(annual_fixed_cost),
238 &activity,
239 &activity_costs,
240 );
241
242 let expected = MoneyPerActivity(expected);
243 assert_approx_eq!(MoneyPerActivity, result, expected);
244 }
245}