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