Skip to content

Commit 2eaaf00

Browse files
Output SAM IRR as 'After-Tax IRR' to avoid potential mixup with 'Project IRR' as unlevered IRR (see https://www.efinancialmodels.com/knowledge-base/financial-metrics/internal-rate-of-revenue-irr/irr-levered-an-internal-rate-of-return-example/)
1 parent f83298a commit 2eaaf00

File tree

7 files changed

+76
-50
lines changed

7 files changed

+76
-50
lines changed

src/geophires_x/Economics.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
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
9+
real_discount_rate_parameter, after_tax_irr_parameter
1010
from geophires_x.OptionList import Configuration, WellDrillingCostCorrelation, EconomicModel, EndUseOptions, PlantType, \
1111
_WellDrillingCostCorrelationCitation
1212
from geophires_x.Parameter import intParameter, floatParameter, OutputParameter, ReadParameter, boolParameter, \
@@ -1784,6 +1784,8 @@ def __init__(self, model: Model):
17841784
CurrentUnits=PercentUnit.PERCENT
17851785
)
17861786

1787+
self.after_tax_irr = self.OutputParameterDict[self.after_tax_irr.Name] = (
1788+
after_tax_irr_parameter())
17871789
self.real_discount_rate = self.OutputParameterDict[self.real_discount_rate.Name] = (
17881790
real_discount_rate_parameter())
17891791
self.nominal_discount_rate = self.OutputParameterDict[self.nominal_discount_rate.Name] = (
@@ -2775,7 +2777,9 @@ def Calculate(self, model: Model) -> None:
27752777
self.nominal_discount_rate.value = self.sam_economics_calculations.nominal_discount_rate.value
27762778
self.ProjectNPV.value = self.sam_economics_calculations.project_npv.quantity().to(
27772779
convertible_unit(self.ProjectNPV.CurrentUnits)).magnitude
2778-
self.ProjectIRR.value = self.sam_economics_calculations.project_irr.quantity().to(
2780+
2781+
self.ProjectIRR.value = non_calculated_output_placeholder_val # SAM calculates After-Tax IRR instead
2782+
self.after_tax_irr.value = self.sam_economics_calculations.after_tax_irr.quantity().to(
27792783
convertible_unit(self.ProjectIRR.CurrentUnits)).magnitude
27802784

27812785
self.ProjectVIR.value = non_calculated_output_placeholder_val # TODO SAM VIR

src/geophires_x/EconomicsSam.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@
2727

2828
from geophires_x import Model as Model
2929
from geophires_x.EconomicsSamCashFlow import _calculate_sam_economics_cash_flow
30-
from geophires_x.EconomicsUtils import BuildPricingModel, wacc_output_parameter, nominal_discount_rate_parameter
30+
from geophires_x.EconomicsUtils import (
31+
BuildPricingModel,
32+
wacc_output_parameter,
33+
nominal_discount_rate_parameter,
34+
after_tax_irr_parameter,
35+
)
3136
from geophires_x.GeoPHIRESUtils import is_float, is_int
3237
from geophires_x.OptionList import EconomicModel, EndUseOptions
3338
from geophires_x.Parameter import Parameter, OutputParameter, floatParameter
@@ -44,24 +49,22 @@ class SamEconomicsCalculations:
4449
CurrentUnits=EnergyCostUnit.CENTSSPERKWH,
4550
)
4651
)
52+
4753
capex: OutputParameter = field(
4854
default_factory=lambda: OutputParameter(
4955
UnitType=Units.CURRENCY,
5056
CurrentUnits=CurrencyUnit.MDOLLARS,
5157
)
5258
)
59+
5360
project_npv: OutputParameter = field(
5461
default_factory=lambda: OutputParameter(
5562
UnitType=Units.CURRENCY,
5663
CurrentUnits=CurrencyUnit.MDOLLARS,
5764
)
5865
)
59-
project_irr: OutputParameter = field(
60-
default_factory=lambda: OutputParameter(
61-
UnitType=Units.PERCENT,
62-
CurrentUnits=PercentUnit.PERCENT,
63-
)
64-
)
66+
67+
after_tax_irr: OutputParameter = field(default_factory=after_tax_irr_parameter)
6568

6669
nominal_discount_rate: OutputParameter = field(default_factory=nominal_discount_rate_parameter)
6770

@@ -157,7 +160,7 @@ def sf(_v: float) -> float:
157160

158161
sam_economics: SamEconomicsCalculations = SamEconomicsCalculations(sam_cash_flow_profile=cash_flow)
159162
sam_economics.lcoe_nominal.value = sf(single_owner.Outputs.lcoe_nom)
160-
sam_economics.project_irr.value = sf(single_owner.Outputs.project_return_aftertax_irr)
163+
sam_economics.after_tax_irr.value = sf(single_owner.Outputs.project_return_aftertax_irr)
161164
sam_economics.project_npv.value = sf(single_owner.Outputs.project_return_aftertax_npv * 1e-6)
162165
sam_economics.capex.value = single_owner.Outputs.adjusted_installed_cost * 1e-6
163166

src/geophires_x/EconomicsUtils.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,27 +37,39 @@ def BuildPricingModel(plantlifetime: int, StartPrice: float, EndPrice: float,
3737
return Price
3838

3939

40+
def after_tax_irr_parameter() -> OutputParameter:
41+
return OutputParameter(
42+
Name='After-Tax IRR',
43+
UnitType=Units.PERCENT,
44+
CurrentUnits=PercentUnit.PERCENT,
45+
PreferredUnits=PercentUnit.PERCENT,
46+
ToolTipText='The After-Tax IRR (internal rate of return) is the nominal discount rate that corresponds to '
47+
'a net present value (NPV) of zero for PPA SAM Economic models. '
48+
'See https://samrepo.nrelcloud.org/help/mtf_irr.html.'
49+
)
50+
51+
4052
def real_discount_rate_parameter() -> OutputParameter:
4153
return OutputParameter(
42-
Name="Real Discount Rate",
43-
UnitType=Units.PERCENT,
44-
CurrentUnits=PercentUnit.PERCENT,
45-
PreferredUnits=PercentUnit.PERCENT,
46-
)
54+
Name="Real Discount Rate",
55+
UnitType=Units.PERCENT,
56+
CurrentUnits=PercentUnit.PERCENT,
57+
PreferredUnits=PercentUnit.PERCENT,
58+
)
4759

4860

4961
def nominal_discount_rate_parameter() -> OutputParameter:
5062
return OutputParameter(
51-
Name="Nominal Discount Rate",
52-
ToolTipText="Nominal Discount Rate is displayed for SAM Economic Models. "
53-
"It is calculated "
54-
"per https://samrepo.nrelcloud.org/help/fin_single_owner.html?q=nominal+discount+rate: "
55-
"Nominal Discount Rate = [ ( 1 + Real Discount Rate ÷ 100 ) "
56-
"× ( 1 + Inflation Rate ÷ 100 ) - 1 ] × 100.",
57-
UnitType=Units.PERCENT,
58-
CurrentUnits=PercentUnit.PERCENT,
59-
PreferredUnits=PercentUnit.PERCENT,
60-
)
63+
Name="Nominal Discount Rate",
64+
ToolTipText="Nominal Discount Rate is displayed for SAM Economic Models. "
65+
"It is calculated "
66+
"per https://samrepo.nrelcloud.org/help/fin_single_owner.html?q=nominal+discount+rate: "
67+
"Nominal Discount Rate = [ ( 1 + Real Discount Rate ÷ 100 ) "
68+
"× ( 1 + Inflation Rate ÷ 100 ) - 1 ] × 100.",
69+
UnitType=Units.PERCENT,
70+
CurrentUnits=PercentUnit.PERCENT,
71+
PreferredUnits=PercentUnit.PERCENT,
72+
)
6173

6274

6375
def wacc_output_parameter() -> OutputParameter:

src/geophires_x/Outputs.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,12 @@ def PrintOutputs(self, model: Model):
275275
# TODO should use CurrentUnits instead of PreferredUnits
276276
f.write(f' {npv_field_label}{e_npv.value:10.2f} {e_npv.PreferredUnits.value}\n')
277277

278-
irr_display_value = f'{econ.ProjectIRR.value:10.2f}' if not math.isnan(econ.ProjectIRR.value) else 'NaN'
279-
f.write(f' {econ.ProjectIRR.display_name}: {irr_display_value} {econ.ProjectIRR.CurrentUnits.value}\n')
278+
irr_output_param: OutputParameter = econ.ProjectIRR \
279+
if econ.econmodel.value != EconomicModel.SAM_SINGLE_OWNER_PPA else econ.after_tax_irr
280+
irr_field_label = Outputs._field_label(irr_output_param.display_name, 49)
281+
irr_display_value = f'{irr_output_param.value:10.2f}' \
282+
if not math.isnan(irr_output_param.value) else 'NaN'
283+
f.write(f' {irr_field_label}{irr_display_value} {irr_output_param.CurrentUnits.value}\n')
280284

281285
if econ.econmodel.value != EconomicModel.SAM_SINGLE_OWNER_PPA:
282286
# VIR, MOIC, and Payback period not currently supported by SAM economic model(s)

tests/examples/Fervo_Project_Cape-4.out

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
Simulation Metadata
66
----------------------
77
GEOPHIRES Version: 3.9.7
8-
Simulation Date: 2025-05-20
9-
Simulation Time: 13:06
10-
Calculation Time: 1.051 sec
8+
Simulation Date: 2025-05-21
9+
Simulation Time: 09:51
10+
Calculation Time: 1.041 sec
1111

1212
***SUMMARY OF RESULTS***
1313

@@ -31,7 +31,7 @@ Simulation Metadata
3131
Project lifetime: 20 yr
3232
Capacity factor: 90.0 %
3333
Project NPV: 1940.10 MUSD
34-
Project IRR: 24.75 %
34+
After-Tax IRR: 24.75 %
3535
Estimated Jobs Created: 1190
3636

3737
***ENGINEERING PARAMETERS***

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
Simulation Metadata
66
----------------------
77
GEOPHIRES Version: 3.9.7
8-
Simulation Date: 2025-05-15
9-
Simulation Time: 10:13
10-
Calculation Time: 0.891 sec
8+
Simulation Date: 2025-05-21
9+
Simulation Time: 09:55
10+
Calculation Time: 0.888 sec
1111

1212
***SUMMARY OF RESULTS***
1313

@@ -31,7 +31,7 @@ Simulation Metadata
3131
Project lifetime: 20 yr
3232
Capacity factor: 90.0 %
3333
Project NPV: 2877.00 MUSD
34-
Project IRR: 59.73 %
34+
After-Tax IRR: 59.73 %
3535
Estimated Jobs Created: 976
3636

3737
***ENGINEERING PARAMETERS***

tests/test_geophires_x.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -195,21 +195,8 @@ def get_output_file_for_example(example_file: str):
195195
del expected_result.result['metadata']
196196
del expected_result.result['Simulation Metadata']
197197

198-
def sanitize_nan(r: GeophiresXResult) -> None:
199-
"""
200-
Workaround for float('nan') != float('nan')
201-
See https://stackoverflow.com/questions/51728427/unittest-how-to-assert-if-the-two-possibly-nan-values-are-equal
202-
203-
TODO generalize beyond Project IRR
204-
"""
205-
try:
206-
if math.isnan(r.result['ECONOMIC PARAMETERS']['Project IRR']['value']):
207-
r.result['ECONOMIC PARAMETERS']['Project IRR']['value'] = 'NaN'
208-
except TypeError:
209-
pass
210-
211-
sanitize_nan(geophires_result)
212-
sanitize_nan(expected_result)
198+
self._sanitize_nan(geophires_result)
199+
self._sanitize_nan(expected_result)
213200

214201
try:
215202
self.assertDictEqual(
@@ -261,6 +248,22 @@ def sanitize_nan(r: GeophiresXResult) -> None:
261248
if len(regenerate_cmds) > 0:
262249
print(f'Command to regenerate {len(regenerate_cmds)} failed examples:\n{" && ".join(regenerate_cmds)}')
263250

251+
# noinspection PyMethodMayBeStatic
252+
def _sanitize_nan(self, r: GeophiresXResult) -> None:
253+
"""
254+
Workaround for float('nan') != float('nan')
255+
See https://stackoverflow.com/questions/51728427/unittest-how-to-assert-if-the-two-possibly-nan-values-are-equal
256+
257+
TODO generalize beyond After-Tax IRR
258+
"""
259+
irr_key = 'After-Tax IRR'
260+
if irr_key in r.result['ECONOMIC PARAMETERS']:
261+
try:
262+
if math.isnan(r.result['ECONOMIC PARAMETERS'][irr_key]['value']):
263+
r.result['ECONOMIC PARAMETERS'][irr_key]['value'] = 'NaN'
264+
except TypeError:
265+
pass
266+
264267
def _get_unequal_dicts_approximate_percent_difference(self, d1: dict, d2: dict) -> Optional[float]:
265268
for i in range(99):
266269
try:

0 commit comments

Comments
 (0)