Skip to content

Commit 45c928b

Browse files
Calculate SAM NaN After-tax IRRs using numpy-financial.irr
1 parent 7a197c5 commit 45c928b

File tree

4 files changed

+65
-5
lines changed

4 files changed

+65
-5
lines changed

src/geophires_x/EconomicsSam.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import json
4+
import math
45
import os
56
from dataclasses import dataclass, field
67
from functools import lru_cache
@@ -11,6 +12,7 @@
1112
from decimal import Decimal
1213

1314
import numpy as np
15+
import numpy_financial as npf
1416

1517
# noinspection PyPackageRequirements
1618
from PySAM import CustomGeneration
@@ -155,12 +157,13 @@ def calculate_sam_economics(model: Model) -> SamEconomicsCalculations:
155157

156158
cash_flow = _calculate_sam_economics_cash_flow(model, single_owner)
157159

158-
def sf(_v: float) -> float:
159-
return _sig_figs(_v, 5)
160+
def sf(_v: float, num_sig_figs: int = 5) -> float:
161+
return _sig_figs(_v, num_sig_figs)
160162

161163
sam_economics: SamEconomicsCalculations = SamEconomicsCalculations(sam_cash_flow_profile=cash_flow)
162164
sam_economics.lcoe_nominal.value = sf(single_owner.Outputs.lcoe_nom)
163-
sam_economics.after_tax_irr.value = sf(single_owner.Outputs.project_return_aftertax_irr)
165+
sam_economics.after_tax_irr.value = sf(_get_after_tax_irr_pct(single_owner, cash_flow, model))
166+
164167
sam_economics.project_npv.value = sf(single_owner.Outputs.project_return_aftertax_npv * 1e-6)
165168
sam_economics.capex.value = single_owner.Outputs.adjusted_installed_cost * 1e-6
166169

@@ -171,6 +174,25 @@ def sf(_v: float) -> float:
171174
return sam_economics
172175

173176

177+
def _get_after_tax_irr_pct(single_owner: Singleowner, cash_flow: list[list[Any]], model: Model) -> float:
178+
after_tax_irr_pct = single_owner.Outputs.project_return_aftertax_irr
179+
if math.isnan(after_tax_irr_pct):
180+
try:
181+
182+
def cash_flow_profile_row(row_name: str) -> list[Any]:
183+
return next( # type: ignore[no-any-return]
184+
row for row in cash_flow if len(row) > 0 and row[0] == row_name
185+
)[1:]
186+
187+
after_tax_returns_cash_flow = cash_flow_profile_row('Total after-tax returns ($)')
188+
after_tax_irr_pct = npf.irr(after_tax_returns_cash_flow) * 100.0
189+
model.logger.info(f'After-tax IRR was NaN, calculated with numpy-financial: {after_tax_irr_pct}%')
190+
except Exception as e:
191+
model.logger.warning(f'After-tax IRR was NaN and calculation with numpy-financial failed: {e}')
192+
193+
return after_tax_irr_pct
194+
195+
174196
def _calculate_nominal_discount_rate_and_wacc(model: Model, single_owner: Singleowner) -> tuple[float]:
175197
"""
176198
Calculation per SAM Help -> Financial Parameters -> Commercial -> Commercial Loan Parameters -> WACC

src/geophires_x/EconomicsUtils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ def after_tax_irr_parameter() -> OutputParameter:
4545
PreferredUnits=PercentUnit.PERCENT,
4646
ToolTipText='The After-tax IRR (internal rate of return) is the nominal discount rate that corresponds to '
4747
'a net present value (NPV) of zero for PPA SAM Economic models. '
48-
'See https://samrepo.nrelcloud.org/help/mtf_irr.html.'
48+
'See https://samrepo.nrelcloud.org/help/mtf_irr.html. If SAM calculates After-tax IRR as NaN, '
49+
'numpy-financial.irr (https://numpy.org/numpy-financial/latest/irr.html) '
50+
'is used to calculate the value from SAM\'s total after-tax returns.'
4951
)
5052

5153

src/geophires_x_schema_generator/geophires-result.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"description": "LCOE. For SAM economic models, this is the nominal LCOE value (as opposed to real).",
1818
"units": "cents/kWh"
1919
},
20+
"Total CAPEX": {},
2021
"Average Direct-Use Heat Production": {},
2122
"Direct-Use heat breakeven price": {},
2223
"Direct-Use heat breakeven price (LCOH)": {
@@ -104,7 +105,7 @@
104105
},
105106
"After-tax IRR": {
106107
"type": "number",
107-
"description": "The After-tax IRR (internal rate of return) is the nominal discount rate that corresponds to a net present value (NPV) of zero for PPA SAM Economic models. See https://samrepo.nrelcloud.org/help/mtf_irr.html.",
108+
"description": "The After-tax IRR (internal rate of return) is the nominal discount rate that corresponds to a net present value (NPV) of zero for PPA SAM Economic models. See https://samrepo.nrelcloud.org/help/mtf_irr.html. If SAM calculates After-tax IRR as NaN, numpy-financial.irr (https://numpy.org/numpy-financial/latest/irr.html) is used to calculate the value from SAM's total after-tax returns.",
108109
"units": "%"
109110
},
110111
"Project VIR=PI=PIR": {
@@ -254,6 +255,7 @@
254255
"description": "Calculated Fracture Separation",
255256
"units": "meter"
256257
},
258+
"Reservoir volume calculation note": {},
257259
"Reservoir volume": {},
258260
"Reservoir impedance": {},
259261
"Reservoir hydrostatic pressure": {},

tests/geophires_x_tests/test_economics_sam.py

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

3+
import math
34
import os
45
import sys
56
from pathlib import Path
67
from typing import Any
78

89
import numpy as np
10+
import numpy_financial as npf
911

1012
from base_test_case import BaseTestCase
1113

@@ -466,6 +468,38 @@ def test_get_fed_and_state_tax_rates(self):
466468
self.assertEqual(([21], [9]), _get_fed_and_state_tax_rates(0.3))
467469
self.assertEqual(([10], [0]), _get_fed_and_state_tax_rates(0.1))
468470

471+
def test_nan_after_tax_irr(self):
472+
"""
473+
Verify that After-tax IRRs that would have been calculated as NaN by SAM are instead calculated with
474+
numpy-financial.irr
475+
"""
476+
477+
def _irr(_r: GeophiresXResult) -> float:
478+
return _r.result['ECONOMIC PARAMETERS']['After-tax IRR']['value']
479+
480+
rate_params = {
481+
'Electricity Escalation Rate Per Year': 0.00348993288590604,
482+
'Starting Electricity Sale Price': 0.13,
483+
}
484+
r: GeophiresXResult = self._get_result(rate_params)
485+
after_tax_irr_cash_flow_entries = EconomicsSamTestCase._get_cash_flow_row(
486+
r.result['SAM CASH FLOW PROFILE'], 'After-tax cumulative IRR (%)'
487+
)
488+
sam_after_tax_irr_calc = float(after_tax_irr_cash_flow_entries[-1])
489+
490+
# Test case condition - we expect SAM to have calculated NaN here. If this assertion fails, adjust params passed
491+
# to _get_result such that final year of After-tax cumulative IRR is NaN.
492+
assert math.isnan(sam_after_tax_irr_calc)
493+
494+
after_tax_cash_flow = EconomicsSamTestCase._get_cash_flow_row(
495+
r.result['SAM CASH FLOW PROFILE'], 'Total after-tax returns ($)'
496+
)
497+
npf_irr = npf.irr(after_tax_cash_flow) * 100.0
498+
499+
r_irr = _irr(r)
500+
self.assertFalse(math.isnan(r_irr))
501+
self.assertAlmostEqual(npf_irr, r_irr, places=2)
502+
469503
@staticmethod
470504
def _new_model(input_file: Path, additional_params: dict[str, Any] | None = None, read_and_calculate=True) -> Model:
471505
if additional_params is not None:

0 commit comments

Comments
 (0)