Skip to content

Commit 6cdd8ec

Browse files
Merge pull request NREL#364 from softwareengineerprogrammer/main
Add 'Discount Initial Year Cashflow' parameter for Excel-style NPV calculation [v3.8.4]
2 parents 7a44315 + 832638e commit 6cdd8ec

File tree

16 files changed

+186
-33
lines changed

16 files changed

+186
-33
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"

CHANGELOG.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ Major, minor, and notable patch versions are documented above.
124124
You may also be interested in viewing the list of all PRs merged into the repository `here <https://github.com/NREL/GEOPHIRES-X/pulls?q=is%3Apr+is%3Amerged+>`__.
125125

126126
Each semantic version has a corresponding tag, the full list of which can be viewed `here <https://github.com/NREL/GEOPHIRES-X/tags>`__.
127-
The latest patch version in this repository and patch versions explicitly mentioned in this changelog are always suitable for public consumption;
127+
The patch version displayed on the package badge in the README and patch versions explicitly mentioned in this changelog are always suitable for public consumption;
128128
but note that not all patch version tags in the list are meant for public consumption
129129
as intermediate internal-only patch versions are sometimes introduced during the development process.
130130
(Improved designation and distribution of releases for public consumption may eventually be addressed by

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: 57 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,
@@ -1739,18 +1778,25 @@ def __init__(self, model: Model):
17391778
PreferredUnits=PercentUnit.PERCENT,
17401779
CurrentUnits=PercentUnit.PERCENT
17411780
)
1781+
1782+
# TODO this is displayed as "Project Net Revenue" in Revenue & Cashflow Profile which is probably not an
1783+
# accurate synonym for annual revenue
17421784
self.TotalRevenue = self.OutputParameterDict[self.TotalRevenue.Name] = OutputParameter(
17431785
Name="Annual Revenue from Project",
17441786
UnitType=Units.CURRENCYFREQUENCY,
17451787
PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR,
17461788
CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR
17471789
)
1790+
1791+
# TODO this is displayed as "Project Net Cashflow" in Revenue & Cashflow Profile which is probably not an
1792+
# accurate synonym for cumulative revenue
17481793
self.TotalCummRevenue = self.OutputParameterDict[self.TotalCummRevenue.Name] = OutputParameter(
17491794
Name="Cumulative Revenue from Project",
17501795
UnitType=Units.CURRENCY,
17511796
PreferredUnits=CurrencyUnit.MDOLLARS,
17521797
CurrentUnits=CurrencyUnit.MDOLLARS
17531798
)
1799+
17541800
self.ProjectNPV = self.OutputParameterDict[self.ProjectNPV.Name] = OutputParameter(
17551801
"Project Net Present Value",
17561802
UnitType=Units.CURRENCY,
@@ -2881,9 +2927,15 @@ def Calculate(self, model: Model) -> None:
28812927

28822928
# Calculate more financial values using numpy financials
28832929
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)
2930+
CalculateFinancialPerformance(
2931+
model.surfaceplant.plant_lifetime.value,
2932+
self.FixedInternalRate.value,
2933+
self.TotalRevenue.value,
2934+
self.TotalCummRevenue.value,
2935+
self.CCap.value,
2936+
self.Coam.value,
2937+
self.discount_initial_year_cashflow.value
2938+
)
28872939

28882940
# Calculate the project payback period
28892941
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))

0 commit comments

Comments
 (0)