Skip to content

Commit d551a4b

Browse files
Merge pull request #55 from softwareengineerprogrammer/npv-cashflow
Add 'Discount Initial Year Cashflow' parameter for Excel-style NPV calculation [v3.8.4]
2 parents 288abe9 + 62af12e commit d551a4b

File tree

15 files changed

+178
-32
lines changed

15 files changed

+178
-32
lines changed

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 3.8.0
2+
current_version = 3.8.4
33
commit = True
44
tag = True
55

.cookiecutterrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ default_context:
5454
sphinx_doctest: "no"
5555
sphinx_theme: "sphinx-py3doc-enhanced-theme"
5656
test_matrix_separate_coverage: "no"
57-
version: 3.8.0
57+
version: 3.8.4
5858
version_manager: "bump2version"
5959
website: "https://github.com/NREL"
6060
year_from: "2023"

README.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ Free software: `MIT license <LICENSE>`__
5656
:alt: Supported implementations
5757
:target: https://pypi.org/project/geophires-x
5858

59-
.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.8.0.svg
59+
.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.8.4.svg
6060
:alt: Commits since latest release
61-
:target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.8.0...main
61+
:target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.8.4...main
6262

6363
.. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat
6464
:target: https://nrel.github.io/GEOPHIRES-X

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
year = '2025'
1919
author = 'NREL'
2020
copyright = f'{year}, {author}'
21-
version = release = '3.8.0'
21+
version = release = '3.8.4'
2222

2323
pygments_style = 'trac'
2424
templates_path = ['./templates']

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def read(*names, **kwargs):
1313

1414
setup(
1515
name='geophires-x',
16-
version='3.8.0',
16+
version='3.8.4',
1717
license='MIT',
1818
description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.',
1919
long_description='{}\n{}'.format(

src/geophires_x/Economics.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -304,12 +304,29 @@ def CalculateCarbonRevenue(model, plant_lifetime: int, construction_years: int,
304304
return cash_flow_musd, cumm_cash_flow_musd, carbon_that_would_have_been_produced_annually_lbs, carbon_that_would_have_been_produced_total_lbs
305305

306306

307+
def calculate_npv(
308+
discount_rate_tenths: float,
309+
cashflow_series: list,
310+
discount_initial_year_cashflow: bool
311+
) -> float:
312+
# TODO warn/raise exception if discount rate > 1 (i.e. it's probably not converted from percent to tenths)
313+
314+
npv_cashflow_series = cashflow_series.copy() # Copy to guard against unintentional mutation of consumer field
315+
316+
if discount_initial_year_cashflow:
317+
# Enable Excel-style NPV calculation - see https://github.com/NREL/GEOPHIRES-X/discussions/344
318+
npv_cashflow_series = [0, *npv_cashflow_series]
319+
320+
return npf.npv(discount_rate_tenths, npv_cashflow_series)
321+
322+
307323
def CalculateFinancialPerformance(plantlifetime: int,
308324
FixedInternalRate: float,
309325
TotalRevenue: list,
310326
TotalCummRevenue: list,
311327
CAPEX: float,
312-
OPEX: float):
328+
OPEX: float,
329+
discount_initial_year_cashflow: bool = False):
313330
"""
314331
CalculateFinancialPerformance calculates the financial performance of the project. It is used to calculate the
315332
financial performance of the project. It is used to calculate the revenue stream for the project.
@@ -325,6 +342,9 @@ def CalculateFinancialPerformance(plantlifetime: int,
325342
:type CAPEX: float
326343
:param OPEX: The total annual operating cost of the project in MUSD
327344
:type OPEX: float
345+
:param discount_initial_year_cashflow: Whether to discount the initial year of cashflow used to calculate NPV
346+
:type discount_initial_year_cashflow: bool
347+
328348
:return: NPV: The net present value of the project in MUSD
329349
:rtype: float
330350
:return: IRR: The internal rate of return of the project in %
@@ -336,7 +356,8 @@ def CalculateFinancialPerformance(plantlifetime: int,
336356
:rtype: tuple
337357
"""
338358
# Calculate financial performance values using numpy financials
339-
NPV = npf.npv(FixedInternalRate / 100, TotalRevenue)
359+
360+
NPV = calculate_npv(FixedInternalRate / 100, TotalRevenue.copy(), discount_initial_year_cashflow)
340361
IRR = npf.irr(TotalRevenue)
341362
if math.isnan(IRR):
342363
IRR = 0.0
@@ -859,6 +880,24 @@ def __init__(self, model: Model):
859880
"Discount Rate is synonymous with Fixed Internal Rate. If one is provided, the other's value "
860881
"will be automatically set to the same value."
861882
)
883+
884+
885+
self.discount_initial_year_cashflow = self.ParameterDict[self.discount_initial_year_cashflow.Name] = boolParameter(
886+
'Discount Initial Year Cashflow',
887+
DefaultValue=False,
888+
UnitType=Units.NONE,
889+
ToolTipText='Whether to discount cashflow in the initial project year when calculating NPV '
890+
'(Net Present Value). '
891+
'The default value of False conforms to NREL\'s standard convention for NPV calculation '
892+
'(Short W et al, 1995. https://www.nrel.gov/docs/legosti/old/5173.pdf). '
893+
'A value of True will, by contrast, cause NPV calculation to follow the convention used by '
894+
'Excel, Google Sheets, and other common spreadsheet software. '
895+
'Although NREL\'s NPV convention may typically be considered more technically correct, '
896+
'Excel-style NPV calculation might be preferred for familiarity '
897+
'or compatibility with existing business processes. '
898+
'See https://github.com/NREL/GEOPHIRES-X/discussions/344 for further details.'
899+
)
900+
862901
self.FIB = self.ParameterDict[self.FIB.Name] = floatParameter(
863902
"Fraction of Investment in Bonds",
864903
DefaultValue=0.5,
@@ -2881,9 +2920,15 @@ def Calculate(self, model: Model) -> None:
28812920

28822921
# Calculate more financial values using numpy financials
28832922
self.ProjectNPV.value, self.ProjectIRR.value, self.ProjectVIR.value, self.ProjectMOIC.value = \
2884-
CalculateFinancialPerformance(model.surfaceplant.plant_lifetime.value, self.FixedInternalRate.value,
2885-
self.TotalRevenue.value, self.TotalCummRevenue.value, self.CCap.value,
2886-
self.Coam.value)
2923+
CalculateFinancialPerformance(
2924+
model.surfaceplant.plant_lifetime.value,
2925+
self.FixedInternalRate.value,
2926+
self.TotalRevenue.value,
2927+
self.TotalCummRevenue.value,
2928+
self.CCap.value,
2929+
self.Coam.value,
2930+
self.discount_initial_year_cashflow.value
2931+
)
28872932

28882933
# Calculate the project payback period
28892934
self.ProjectPaybackPeriod.value = 0.0 # start by assuming the project never pays back

src/geophires_x/EconomicsAddOns.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,12 @@ def Calculate(self, model: Model) -> None:
347347

348348
# Now calculate a new "NPV", "IRR", "VIR", "Payback Period", and "MOIC"
349349
# Calculate more financial values using numpy financials
350-
self.ProjectNPV.value = npf.npv(self.FixedInternalRate.value / 100, self.ProjectCashFlow.value)
350+
self.ProjectNPV.value = Economics.calculate_npv(
351+
self.FixedInternalRate.value / 100,
352+
self.ProjectCashFlow.value.copy(),
353+
self.discount_initial_year_cashflow.value
354+
)
355+
351356
self.ProjectIRR.value = npf.irr(self.ProjectCashFlow.value)
352357
if math.isnan(self.ProjectIRR.value):
353358
self.ProjectIRR.value = 0.0

src/geophires_x/Outputs.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1643,7 +1643,12 @@ def PrintOutputs(self, model: Model):
16431643
f.write(f' Accrued financing during construction: {model.economics.inflrateconstruction.value*100:10.2f} ' + model.economics.inflrateconstruction.CurrentUnits.value + NL)
16441644
f.write(f' Project lifetime: {model.surfaceplant.plant_lifetime.value:10.0f} ' + model.surfaceplant.plant_lifetime.CurrentUnits.value + NL)
16451645
f.write(f' Capacity factor: {model.surfaceplant.utilization_factor.value * 100:10.1f} %' + NL)
1646-
f.write(f' Project NPV: {model.economics.ProjectNPV.value:10.2f} ' + model.economics.ProjectNPV.PreferredUnits.value + NL)
1646+
1647+
e_npv = model.economics.ProjectNPV
1648+
npv_field_label = Outputs._field_label('Project NPV', 49)
1649+
# TODO should use CurrentUnits instead of PreferredUnits
1650+
f.write(f' {npv_field_label}{e_npv.value:10.2f} {e_npv.PreferredUnits.value}\n')
1651+
16471652
f.write(f' Project IRR: {model.economics.ProjectIRR.value:10.2f} ' + model.economics.ProjectIRR.PreferredUnits.value + NL)
16481653
f.write(f' Project VIR=PI=PIR: {model.economics.ProjectVIR.value:10.2f}' + NL)
16491654
f.write(f' {model.economics.ProjectMOIC.Name}: {model.economics.ProjectMOIC.value:10.2f}' + NL)

src/geophires_x/OutputsAddOns.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def PrintOutputs(self, model) -> tuple:
4141
addon_results.append(OutputTableItem('Adjusted Project CAPEX (after incentives, grants, AddOns, etc)', '{0:10.2f}'.format(model.addeconomics.AdjustedProjectCAPEX.value), model.addeconomics.AdjustedProjectCAPEX.PreferredUnits.value))
4242
f.write(f" Adjusted Project OPEX (after incentives, grants, AddOns, etc): {model.addeconomics.AdjustedProjectOPEX.value:10.2f} " + model.addeconomics.AdjustedProjectOPEX.PreferredUnits.value + NL)
4343
addon_results.append(OutputTableItem('Adjusted Project OPEX (after incentives, grants, AddOns, etc)', '{0:10.2f}'.format(model.addeconomics.AdjustedProjectOPEX.value), model.addeconomics.AdjustedProjectOPEX.PreferredUnits.value))
44-
f.write(f" Project NPV (including AddOns): {model.addeconomics.ProjectNPV.value:10.2f} " + model.addeconomics.ProjectNPV.PreferredUnits.value + NL)
44+
f.write(f' Project NPV (including AddOns): {model.addeconomics.ProjectNPV.value:10.2f} {model.addeconomics.ProjectNPV.PreferredUnits.value}\n')
4545
addon_results.append(OutputTableItem('Project NPV (including AddOns)', '{0:10.2f}'.format(model.addeconomics.ProjectNPV.value), model.addeconomics.ProjectNPV.PreferredUnits.value))
4646
f.write(f" Project IRR (including AddOns): {model.addeconomics.ProjectIRR.value:10.2f} " + model.addeconomics.ProjectIRR.PreferredUnits.value + NL)
4747
addon_results.append(OutputTableItem('Project IRR (including AddOns)', '{0:10.2f}'.format(model.addeconomics.ProjectIRR.value), model.addeconomics.ProjectIRR.PreferredUnits.value))

src/geophires_x/Parameter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ class boolParameter(Parameter):
144144

145145
def __post_init__(self):
146146
if self.value is None:
147-
self.value:bool = self.DefaultValue
147+
self.value: bool = self.DefaultValue
148148

149149
value: bool = None
150150
DefaultValue: bool = value

0 commit comments

Comments
 (0)