Skip to content

Commit 6069d18

Browse files
SAM-EM Project Payback Period NREL#390
1 parent 5248e32 commit 6069d18

File tree

10 files changed

+72
-29
lines changed

10 files changed

+72
-29
lines changed

docs/SAM-Economic-Models.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,7 @@ The following table describes how GEOPHIRES parameters are transformed into SAM
4848
### Limitations
4949

5050
1. Only Electricity end-use is supported
51-
2. Only 1 construction year is supported
52-
3. The following economic outputs are not calculated:
53-
1. Project Payback Period
51+
2. Only 1 construction year is supported.
5452

5553
## Using SAM Economic Models with Existing GEOPHIRES Inputs
5654

src/geophires_x/Economics.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from geophires_x import EconomicsSam
77
from geophires_x.EconomicsSam import calculate_sam_economics, SamEconomicsCalculations
88
from geophires_x.EconomicsUtils import BuildPricingModel, wacc_output_parameter, nominal_discount_rate_parameter, \
9-
real_discount_rate_parameter, after_tax_irr_parameter, moic_parameter, project_vir_parameter
9+
real_discount_rate_parameter, after_tax_irr_parameter, moic_parameter, project_vir_parameter, \
10+
project_payback_period_parameter
1011
from geophires_x.OptionList import Configuration, WellDrillingCostCorrelation, EconomicModel, EndUseOptions, PlantType, \
1112
_WellDrillingCostCorrelationCitation
1213
from geophires_x.Parameter import intParameter, floatParameter, OutputParameter, ReadParameter, boolParameter, \
@@ -1841,12 +1842,8 @@ def __init__(self, model: Model):
18411842
)
18421843
self.ProjectVIR = self.OutputParameterDict[self.ProjectVIR.Name] = project_vir_parameter()
18431844
self.ProjectMOIC = self.OutputParameterDict[self.ProjectMOIC.Name] = moic_parameter()
1844-
self.ProjectPaybackPeriod = self.OutputParameterDict[self.ProjectPaybackPeriod.Name] = OutputParameter(
1845-
"Project Payback Period",
1846-
UnitType=Units.TIME,
1847-
PreferredUnits=TimeUnit.YEAR,
1848-
CurrentUnits=TimeUnit.YEAR
1849-
)
1845+
self.ProjectPaybackPeriod = self.OutputParameterDict[self.ProjectPaybackPeriod.Name] = (
1846+
project_payback_period_parameter())
18501847
self.RITCValue = self.OutputParameterDict[self.RITCValue.Name] = OutputParameter(
18511848
Name="Investment Tax Credit Value",
18521849
display_name='Investment Tax Credit',
@@ -2794,12 +2791,10 @@ def Calculate(self, model: Model) -> None:
27942791

27952792
self.ProjectMOIC.value = self.sam_economics_calculations.moic.value
27962793
self.ProjectVIR.value = self.sam_economics_calculations.project_vir.value
2794+
self.ProjectPaybackPeriod.value = self.sam_economics_calculations.project_payback_period.value
27972795

27982796
# Calculate the project payback period
2799-
if self.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA:
2800-
# TODO TODO SAM economic models Payback period https://github.com/NREL/GEOPHIRES-X/issues/390
2801-
self.ProjectPaybackPeriod.value = non_calculated_output_placeholder_val
2802-
else:
2797+
if self.econmodel.value != EconomicModel.SAM_SINGLE_OWNER_PPA:
28032798
self.ProjectPaybackPeriod.value = 0.0 # start by assuming the project never pays back
28042799
for i in range(0, len(self.TotalCummRevenue.value), 1):
28052800
# find out when the cumm cashflow goes from negative to positive

src/geophires_x/EconomicsSam.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
after_tax_irr_parameter,
3737
moic_parameter,
3838
project_vir_parameter,
39+
project_payback_period_parameter,
3940
)
4041
from geophires_x.GeoPHIRESUtils import is_float, is_int
4142
from geophires_x.OptionList import EconomicModel, EndUseOptions
@@ -73,6 +74,7 @@ class SamEconomicsCalculations:
7374
wacc: OutputParameter = field(default_factory=wacc_output_parameter)
7475
moic: OutputParameter = field(default_factory=moic_parameter)
7576
project_vir: OutputParameter = field(default_factory=project_vir_parameter)
77+
project_payback_period: OutputParameter = field(default_factory=project_payback_period_parameter)
7678

7779

7880
def validate_read_parameters(model: Model):
@@ -174,6 +176,7 @@ def sf(_v: float, num_sig_figs: int = 5) -> float:
174176
)
175177
sam_economics.moic.value = _calculate_moic(cash_flow, model)
176178
sam_economics.project_vir.value = _calculate_project_vir(cash_flow, model)
179+
sam_economics.project_payback_period.value = _calculate_project_payback_period(cash_flow, model)
177180

178181
return sam_economics
179182

@@ -247,6 +250,28 @@ def _calculate_project_vir(cash_flow: list[list[Any]], model) -> float | None:
247250
return None
248251

249252

253+
def _calculate_project_payback_period(cash_flow: list[list[Any]], model) -> float | None:
254+
try:
255+
after_tax_cash_flow = _cash_flow_profile_row(cash_flow, 'Total after-tax returns ($)')
256+
cumm_cash_flow = np.zeros(len(after_tax_cash_flow))
257+
cumm_cash_flow[0] = after_tax_cash_flow[0]
258+
for year in range(1, len(after_tax_cash_flow)):
259+
cumm_cash_flow[year] = cumm_cash_flow[year - 1] + after_tax_cash_flow[year]
260+
if cumm_cash_flow[year] >= 0:
261+
year_before_full_recovery = year - 1
262+
payback_period = (
263+
year_before_full_recovery
264+
+ abs(cumm_cash_flow[year_before_full_recovery]) / after_tax_cash_flow[year]
265+
)
266+
267+
return float(payback_period)
268+
269+
return float('nan') # never pays back
270+
except Exception as e:
271+
model.logger.error(f'Encountered exception calculating Project Payback Period: {e}')
272+
return None
273+
274+
250275
def get_sam_cash_flow_profile_tabulated_output(model: Model, **tabulate_kw_args) -> str:
251276
"""
252277
Note model must have already calculated economics for this to work (used in Outputs)

src/geophires_x/EconomicsUtils.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from geophires_x.Parameter import OutputParameter
4-
from geophires_x.Units import Units, PercentUnit
4+
from geophires_x.Units import Units, PercentUnit, TimeUnit
55

66

77
def BuildPricingModel(plantlifetime: int, StartPrice: float, EndPrice: float,
@@ -59,6 +59,18 @@ def project_vir_parameter() -> OutputParameter:
5959
)
6060

6161

62+
def project_payback_period_parameter() -> OutputParameter:
63+
return OutputParameter(
64+
"Project Payback Period",
65+
UnitType=Units.TIME,
66+
PreferredUnits=TimeUnit.YEAR,
67+
CurrentUnits=TimeUnit.YEAR,
68+
ToolTipText='The time at which cumulative cash flow reaches zero. '
69+
'For projects that never pay back, the calculated value will be "N/A". '
70+
'For SAM Economic Models, total after-tax returns are used to calculate cumulative cash flow.',
71+
)
72+
73+
6274
def after_tax_irr_parameter() -> OutputParameter:
6375
return OutputParameter(
6476
Name='After-tax IRR',

src/geophires_x/Outputs.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -288,13 +288,12 @@ def PrintOutputs(self, model: Model):
288288
f.write(f' {econ.ProjectVIR.display_name}: {econ.ProjectVIR.value:10.2f}\n')
289289
f.write(f' {econ.ProjectMOIC.display_name}: {econ.ProjectMOIC.value:10.2f}\n')
290290

291-
if econ.econmodel.value != EconomicModel.SAM_SINGLE_OWNER_PPA:
292-
# TODO TODO SAM economic models Payback period https://github.com/NREL/GEOPHIRES-X/issues/390
293-
payback_period_val = model.economics.ProjectPaybackPeriod.value
294-
project_payback_period_display = f'{payback_period_val:10.2f} {econ.ProjectPaybackPeriod.PreferredUnits.value}' \
295-
if payback_period_val > 0.0 else 'N/A'
296-
project_payback_period_label = Outputs._field_label(model.economics.ProjectPaybackPeriod.display_name, 56)
297-
f.write(f' {project_payback_period_label}{project_payback_period_display}\n')
291+
payback_period_val = model.economics.ProjectPaybackPeriod.value
292+
project_payback_period_display = (f'{payback_period_val:10.2f} '
293+
f'{econ.ProjectPaybackPeriod.PreferredUnits.value}') \
294+
if payback_period_val > 0.0 else 'N/A'
295+
project_payback_period_label = Outputs._field_label(model.economics.ProjectPaybackPeriod.display_name, 56)
296+
f.write(f' {project_payback_period_label}{project_payback_period_display}\n')
298297

299298
if model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT,
300299
EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT,

src/geophires_x_schema_generator/geophires-result.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@
121121
"Fixed Charge Rate (FCR)": {},
122122
"Project Payback Period": {
123123
"type": "number",
124-
"description": "",
124+
"description": "The time at which cumulative cash flow reaches zero. For projects that never pay back, the calculated value will be \"N/A\". For SAM Economic Models, total after-tax returns are used to calculate cumulative cash flow.",
125125
"units": "yr"
126126
},
127127
"CHP: Percent cost allocation for electrical plant": {},

tests/examples/Fervo_Project_Cape-4.out

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ Simulation Metadata
66
----------------------
77
GEOPHIRES Version: 3.9.15
88
Simulation Date: 2025-06-04
9-
Simulation Time: 11:49
10-
Calculation Time: 1.057 sec
9+
Simulation Time: 12:20
10+
Calculation Time: 1.066 sec
1111

1212
***SUMMARY OF RESULTS***
1313

@@ -35,6 +35,7 @@ Simulation Metadata
3535
After-tax IRR: 25.85 %
3636
Project VIR=PI=PIR: 1.39
3737
Project MOIC: 2.98
38+
Project Payback Period: 2.53 yr
3839
Estimated Jobs Created: 1299
3940

4041
***ENGINEERING PARAMETERS***

tests/examples/example_SAM-single-owner-PPA-2.out

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ Simulation Metadata
66
----------------------
77
GEOPHIRES Version: 3.9.15
88
Simulation Date: 2025-06-04
9-
Simulation Time: 11:49
10-
Calculation Time: 0.878 sec
9+
Simulation Time: 12:20
10+
Calculation Time: 0.882 sec
1111

1212
***SUMMARY OF RESULTS***
1313

@@ -35,6 +35,7 @@ Simulation Metadata
3535
After-tax IRR: 59.73 %
3636
Project VIR=PI=PIR: 4.58
3737
Project MOIC: 12.36
38+
Project Payback Period: 1.13 yr
3839
Estimated Jobs Created: 976
3940

4041
***ENGINEERING PARAMETERS***

tests/examples/example_SAM-single-owner-PPA.out

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ Simulation Metadata
66
----------------------
77
GEOPHIRES Version: 3.9.15
88
Simulation Date: 2025-06-04
9-
Simulation Time: 11:49
10-
Calculation Time: 1.042 sec
9+
Simulation Time: 12:20
10+
Calculation Time: 1.061 sec
1111

1212
***SUMMARY OF RESULTS***
1313

@@ -35,6 +35,7 @@ Simulation Metadata
3535
After-tax IRR: 22.33 %
3636
Project VIR=PI=PIR: 1.79
3737
Project MOIC: 4.15
38+
Project Payback Period: 4.32 yr
3839
Estimated Jobs Created: 125
3940

4041
***ENGINEERING PARAMETERS***

tests/geophires_x_tests/test_economics_sam.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,17 @@ def _irr(_r: GeophiresXResult) -> float:
500500
self.assertFalse(math.isnan(r_irr))
501501
self.assertAlmostEqual(npf_irr, r_irr, places=2)
502502

503+
def test_nan_project_payback_period(self):
504+
def _payback_period(_r: GeophiresXResult) -> float:
505+
return _r.result['ECONOMIC PARAMETERS']['Project Payback Period']['value']
506+
507+
never_pays_back_params = {
508+
'Starting Electricity Sale Price': 0.00001,
509+
'Ending Electricity Sale Price': 0.00002,
510+
}
511+
r: GeophiresXResult = self._get_result(never_pays_back_params)
512+
self.assertIsNone(_payback_period(r))
513+
503514
@staticmethod
504515
def _new_model(input_file: Path, additional_params: dict[str, Any] | None = None, read_and_calculate=True) -> Model:
505516
if additional_params is not None:

0 commit comments

Comments
 (0)