Skip to content

Commit 5bbd23b

Browse files
Normalize CAPEX schedule sum to 1.0.
1 parent 9fde332 commit 5bbd23b

File tree

4 files changed

+72
-18
lines changed

4 files changed

+72
-18
lines changed

src/geophires_x/Economics.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,8 +1180,8 @@ def __init__(self, model: Model):
11801180
ToolTipText=f'A list of fractions of the total overnight CAPEX spent in each construction year. '
11811181
f'For example, for 3 construction years with 10% in the first year, 40% in the second, '
11821182
f'and 50% in the third, provide {construction_capex_schedule_name} = 0.1,0.4,0.5. '
1183-
f'If the length of the provided schedule does not match the number of construction years, '
1184-
f'it will be interpolated to match the number of construction years.'
1183+
f'The schedule will be automatically interpolated to match the number of construction years '
1184+
f'and normalized to sum to 1.0.'
11851185
)
11861186

11871187
default_bond_financing_start_year = 0

src/geophires_x/EconomicsSam.py

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def _get_row(row_name__: str) -> list[Any]:
160160
return ret
161161

162162

163-
def validate_read_parameters(model: Model):
163+
def validate_read_parameters(model: Model) -> None:
164164
def _inv_msg(param_name: str, invalid_value: Any, supported_description: str) -> str:
165165
return (
166166
f'Invalid {param_name} ({invalid_value}) for '
@@ -193,28 +193,46 @@ def _inv_msg(param_name: str, invalid_value: Any, supported_description: str) ->
193193

194194
econ = model.economics
195195

196-
construction_years = model.surfaceplant.construction_years.value
197-
capex_schedule = econ.construction_capex_schedule.value
198-
capex_schedule_len = len(capex_schedule)
199-
if capex_schedule_len != construction_years:
200-
econ.construction_capex_schedule.value = adjust_phased_schedule_to_new_length(
201-
econ.construction_capex_schedule.value, construction_years
202-
)
203-
msg = (
204-
f'{econ.construction_capex_schedule.Name} ({econ.construction_capex_schedule}) '
205-
f'length ({capex_schedule_len}) '
206-
f'does not match construction years ({construction_years}). '
207-
f'It has been adjusted to: {econ.construction_capex_schedule.value}'
208-
)
209-
model.logger.warning(msg)
196+
econ.construction_capex_schedule.value = _validate_construction_capex_schedule(
197+
econ.construction_capex_schedule,
198+
model.surfaceplant.construction_years.value,
199+
model.logger,
200+
)
210201

202+
construction_years = model.surfaceplant.construction_years.value
211203
if abs(econ.bond_financing_start_year.value) >= construction_years:
212204
raise ValueError(
213205
f'{econ.bond_financing_start_year.Name} ({econ.bond_financing_start_year.value}) may not be earlier than '
214206
f'first {model.surfaceplant.construction_years.Name[:-1]} ({-1*(construction_years-1)}). '
215207
)
216208

217209

210+
def _validate_construction_capex_schedule(
211+
econ_capex_schedule: listParameter, construction_years: int, model_logger
212+
) -> list[float]:
213+
capex_schedule: list[float] = econ_capex_schedule.value.copy()
214+
215+
adjust_schedule_reasons: list[str] = []
216+
if sum(capex_schedule) != 1.0:
217+
adjust_schedule_reasons.append(f'does not sum to 1.0 (sums to {sum(capex_schedule)})')
218+
219+
capex_schedule_len = len(capex_schedule)
220+
if capex_schedule_len != construction_years:
221+
adjust_schedule_reasons.append(
222+
f'length ({capex_schedule_len}) does not match ' f'construction years ({construction_years})'
223+
)
224+
225+
if len(adjust_schedule_reasons) > 0:
226+
capex_schedule = adjust_phased_schedule_to_new_length(econ_capex_schedule.value, construction_years)
227+
msg = f'{econ_capex_schedule.Name} ({econ_capex_schedule}) '
228+
msg += ' and '.join(adjust_schedule_reasons)
229+
msg += f'. It has been adjusted to: {capex_schedule}'
230+
231+
model_logger.warning(msg)
232+
233+
return capex_schedule
234+
235+
218236
@lru_cache(maxsize=12)
219237
def calculate_sam_economics(model: Model) -> SamEconomicsCalculations:
220238
custom_gen = CustomGeneration.new()

src/geophires_x/EconomicsSamPreRevenue.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,11 @@ def adjust_phased_schedule_to_new_length(original_schedule: list[float], new_len
235235

236236
original_len = len(original_schedule)
237237
if original_len == new_length:
238-
return original_schedule
238+
# Even if lengths match, we must normalize to ensure sum is 1.0
239+
total = sum(original_schedule)
240+
if total == 0:
241+
return [1.0 / new_length] * new_length
242+
return [x / total for x in original_schedule]
239243

240244
if original_len == 1:
241245
# Interpolation is not possible with a single value; return a constant schedule

tests/geophires_x_tests/test_economics_sam.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import numpy as np
1111
import numpy_financial as npf
12+
from geophires_x.Parameter import listParameter
1213

1314
from base_test_case import BaseTestCase
1415

@@ -23,6 +24,7 @@
2324
_get_fed_and_state_tax_rates,
2425
SamEconomicsCalculations,
2526
_get_royalty_rate_schedule,
27+
_validate_construction_capex_schedule,
2628
)
2729
from geophires_x.GeoPHIRESUtils import sig_figs, quantity, is_float
2830

@@ -290,6 +292,36 @@ def _sum(cf_row_name: str, abs_val: bool = False) -> float:
290292
_floats(self._get_cash_flow_row(cy4_cf, 'Issuance of equity ($)'))[0],
291293
)
292294

295+
def test_validate_construction_capex_schedule(self):
296+
model_logger = self._new_model(self._egs_test_file_path()).logger
297+
298+
def _sched(sched: list[float]) -> listParameter:
299+
construction_capex_schedule_name = 'Construction CAPEX Schedule'
300+
schedule_param = listParameter(
301+
construction_capex_schedule_name,
302+
DefaultValue=[1.0],
303+
Min=0.0,
304+
Max=1.0,
305+
ToolTipText=construction_capex_schedule_name,
306+
)
307+
schedule_param.value = sched
308+
return schedule_param
309+
310+
half_half = [0.5, 0.5]
311+
self.assertListEqual(half_half, _validate_construction_capex_schedule(_sched(half_half), 2, model_logger))
312+
313+
with self.assertLogs(level='WARNING') as logs:
314+
quarters = [0.25] * 4
315+
self.assertListEqual(half_half, _validate_construction_capex_schedule(_sched(quarters), 2, model_logger))
316+
self.assertHasLogRecordWithMessage(
317+
logs, 'has been adjusted to: [0.5, 0.5]', treat_substring_match_as_match=True
318+
)
319+
320+
double_ones = [1.0, 1.0]
321+
with self.assertLogs(level='WARNING') as logs2:
322+
self.assertListEqual(half_half, _validate_construction_capex_schedule(_sched(double_ones), 2, model_logger))
323+
self.assertHasLogRecordWithMessage(logs2, 'does not sum to 1.0', treat_substring_match_as_match=True)
324+
293325
def assertAlmostEqualWithinSigFigs(self, expected: float | int, actual: float | int, num_sig_figs: int = 3):
294326
"""
295327
TODO move to parent class (BaseTestCase)

0 commit comments

Comments
 (0)