Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 3.10.18
current_version = 3.10.20
commit = True
tag = True

Expand Down
2 changes: 1 addition & 1 deletion .cookiecutterrc
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ default_context:
sphinx_doctest: "no"
sphinx_theme: "sphinx-py3doc-enhanced-theme"
test_matrix_separate_coverage: "no"
version: 3.10.18
version: 3.10.20
version_manager: "bump2version"
website: "https://github.com/NREL"
year_from: "2023"
Expand Down
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ Free software: `MIT license <LICENSE>`__
:alt: Supported implementations
:target: https://pypi.org/project/geophires-x

.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.10.18.svg
.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.10.20.svg
:alt: Commits since latest release
:target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.10.18...main
:target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.10.20...main

.. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat
:target: https://nrel.github.io/GEOPHIRES-X
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
year = '2025'
author = 'NREL'
copyright = f'{year}, {author}'
version = release = '3.10.18'
version = release = '3.10.20'

pygments_style = 'trac'
templates_path = ['./templates']
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def read(*names, **kwargs):

setup(
name='geophires-x',
version='3.10.18',
version='3.10.20',
license='MIT',
description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.',
long_description='{}\n{}'.format(
Expand Down
22 changes: 17 additions & 5 deletions src/geophires_x/Economics.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
real_discount_rate_parameter, after_tax_irr_parameter, moic_parameter, project_vir_parameter, \
project_payback_period_parameter, inflation_cost_during_construction_output_parameter, \
interest_during_construction_output_parameter, total_capex_parameter_output_parameter, \
overnight_capital_cost_output_parameter, CONSTRUCTION_CAPEX_SCHEDULE_PARAMETER_NAME
overnight_capital_cost_output_parameter, CONSTRUCTION_CAPEX_SCHEDULE_PARAMETER_NAME, \
_YEAR_INDEX_VALUE_EXPLANATION_SNIPPET
from geophires_x.GeoPHIRESUtils import quantity
from geophires_x.OptionList import Configuration, WellDrillingCostCorrelation, EconomicModel, EndUseOptions, PlantType, \
_WellDrillingCostCorrelationCitation
Expand Down Expand Up @@ -1006,6 +1007,17 @@ def __init__(self, model: Model):
"increases a 4% rate (0.04) to 4.1% (0.041) in the next year."
)

self.royalty_escalation_rate_start_year = self.ParameterDict[self.royalty_escalation_rate_start_year.Name] = intParameter(
'Royalty Rate Escalation Start Year',
DefaultValue=1,
AllowableRange=list(range(1, model.surfaceplant.plant_lifetime.AllowableRange[-1], 1)),
UnitType=Units.PERCENT,
PreferredUnits=PercentUnit.TENTH,
CurrentUnits=PercentUnit.TENTH,
ToolTipText=f'The first year that the {self.royalty_escalation_rate.Name} is applied. '
f'{_YEAR_INDEX_VALUE_EXPLANATION_SNIPPET}.'
)

maximum_royalty_rate_default_val = 1.0
self.maximum_royalty_rate = self.ParameterDict[self.maximum_royalty_rate.Name] = floatParameter(
'Royalty Rate Maximum',
Expand Down Expand Up @@ -1203,8 +1215,7 @@ def __init__(self, model: Model):
f'Provide {bond_financing_start_year_name} to delay the '
f'start of bond financing during construction; years prior to {bond_financing_start_year_name} '
f'will be financed with equity only. '
f'The value is specified as a project year index corresponding to the Year row in the cash '
f'flow profile; the first construction year has the year index '
f'{_YEAR_INDEX_VALUE_EXPLANATION_SNIPPET}; the first construction year has the year index '
f'{{({model.surfaceplant.construction_years.Name} - 1) * -1}})'
f' and the final construction year index is 0. '
f'For example, a project with 4 construction years '
Expand Down Expand Up @@ -3402,10 +3413,11 @@ def r(x: float) -> float:

schedule = []
current_rate = r(self.royalty_rate.value)
for _ in range(plant_lifetime):
for year_index in range(plant_lifetime):
current_rate = r(current_rate)
schedule.append(min(current_rate, max_rate))
current_rate += escalation_rate
if year_index >= (model.economics.royalty_escalation_rate_start_year.value - 2):
current_rate += escalation_rate

return schedule

Expand Down
40 changes: 38 additions & 2 deletions src/geophires_x/EconomicsSam.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
from geophires_x.Units import convertible_unit, EnergyCostUnit, CurrencyUnit, Units


ROYALTIES_OPEX_CASH_FLOW_LINE_ITEM_KEY = 'O&M production-based expense ($)'


@dataclass
class SamEconomicsCalculations:
_sam_cash_flow_profile_operational_years: list[list[Any]]
Expand All @@ -73,6 +76,7 @@ class SamEconomicsCalculations:

capex: OutputParameter = field(default_factory=total_capex_parameter_output_parameter)

_royalties_rate_schedule: list[float] | None = None
royalties_opex: OutputParameter = field(default_factory=royalty_cost_output_parameter)

project_npv: OutputParameter = field(
Expand Down Expand Up @@ -104,6 +108,8 @@ def sam_cash_flow_profile(self) -> list[list[Any]]:
ret: list[list[Any]] = self._sam_cash_flow_profile_operational_years.copy()
col_count = len(self._sam_cash_flow_profile_operational_years[0])

# TODO support/insert calendar year line item

pre_revenue_years_to_insert = self._pre_revenue_years_count - 1

construction_rows: list[list[Any]] = [
Expand All @@ -124,7 +130,7 @@ def sam_cash_flow_profile(self) -> list[list[Any]]:
pre_revenue_row.extend([''] * (col_count - len(pre_revenue_row)))
construction_rows.append(pre_revenue_row)

# TODO zero-vectors e.g. Debt principal payment ($)
# TODO zero-vectors for non-construction years e.g. Debt principal payment ($)

adjusted_row = [ret[row_index][0]] + pre_revenue_row_content + ret[row_index][insert_index:]
ret[row_index] = adjusted_row
Expand Down Expand Up @@ -166,6 +172,34 @@ def _get_row(row_name__: str) -> list[Any]:
ret[_get_row_index('After-tax cumulative NPV ($)')] = ['After-tax cumulative NPV ($)'] + npv_usd
ret[_get_row_index('After-tax cumulative IRR (%)')] = ['After-tax cumulative IRR (%)'] + irr_pct

if self._royalties_rate_schedule is not None:
ret = self._insert_royalties_rate_schedule(ret)

return ret

def _insert_royalties_rate_schedule(self, cf_ret: list[list[Any]]) -> list[list[Any]]:
"""
TODO update user-facing documentation to mention this feature
(https://nrel.github.io/GEOPHIRES/SAM-Economic-Models.html#royalties)
"""

ret = cf_ret.copy()

def _get_row_index(row_name_: str) -> list[Any]:
return [it[0] for it in ret].index(row_name_)

ret.insert(
_get_row_index(ROYALTIES_OPEX_CASH_FLOW_LINE_ITEM_KEY),
[
*['Royalty rate (%)'],
*([''] * (self._pre_revenue_years_count)),
*[
quantity(it, 'dimensionless').to(convertible_unit('percent')).magnitude
for it in self._royalties_rate_schedule
],
],
)

return ret

@property
Expand Down Expand Up @@ -319,10 +353,12 @@ def sf(_v: float, num_sig_figs: int = 5) -> float:
*_pre_revenue_years_vector(model),
*[
quantity(it, 'USD / year').to(sam_economics.royalties_opex.CurrentUnits).magnitude
for it in _cash_flow_profile_row(cash_flow_operational_years, 'O&M production-based expense ($)')
for it in _cash_flow_profile_row(cash_flow_operational_years, ROYALTIES_OPEX_CASH_FLOW_LINE_ITEM_KEY)
],
]

sam_economics._royalties_rate_schedule = model.economics.get_royalty_rate_schedule(model)

sam_economics.nominal_discount_rate.value, sam_economics.wacc.value = _calculate_nominal_discount_rate_and_wacc(
model, single_owner
)
Expand Down
4 changes: 4 additions & 0 deletions src/geophires_x/EconomicsUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

CONSTRUCTION_CAPEX_SCHEDULE_PARAMETER_NAME = 'Construction CAPEX Schedule'

_YEAR_INDEX_VALUE_EXPLANATION_SNIPPET = (
f'The value is specified as a project year index corresponding to the ' f'Year row in the cash flow profile'
)


def BuildPricingModel(
plantlifetime: int,
Expand Down
2 changes: 1 addition & 1 deletion src/geophires_x/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '3.10.18'
__version__ = '3.10.20'
9 changes: 9 additions & 0 deletions src/geophires_x_schema_generator/geophires-request.json
Original file line number Diff line number Diff line change
Expand Up @@ -1665,6 +1665,15 @@
"minimum": 0.0,
"maximum": 1.0
},
"Royalty Rate Escalation Start Year": {
"description": "The first year that the Royalty Rate Escalation is applied. The value is specified as a project year index corresponding to the Year row in the cash flow profile.",
"type": "integer",
"units": "",
"category": "Economics",
"default": 1,
"minimum": 1,
"maximum": 99
},
"Royalty Rate Maximum": {
"description": "The maximum royalty rate after escalation, expressed as a fraction (e.g., 0.06 for a 6% cap). Defaults to 100% (no effective cap).",
"type": "number",
Expand Down
9 changes: 5 additions & 4 deletions tests/examples/example_SAM-single-owner-PPA-4.out
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

Simulation Metadata
----------------------
GEOPHIRES Version: 3.10.17
Simulation Date: 2025-12-10
Simulation Time: 11:17
Calculation Time: 1.184 sec
GEOPHIRES Version: 3.10.19
Simulation Date: 2025-12-11
Simulation Time: 09:11
Calculation Time: 1.234 sec

***SUMMARY OF RESULTS***

Expand Down Expand Up @@ -230,6 +230,7 @@ Property tax net assessed value ($) 0 225,808,536 225,

OPERATING EXPENSES
O&M fixed expense ($) 0 6,626,143 6,626,143 6,626,143 6,626,143 6,626,143 6,626,143 6,626,143 6,626,143 6,626,143 6,626,143 6,626,143 6,626,143 6,626,143 6,626,143 6,626,143 6,626,143 6,626,143 6,626,143 6,626,143 6,626,143
Royalty rate (%) 5.0 6.0 7.00 8.0 9.0 10.0 10.0 10.0 10.0 10.0 10.0 10.0 10.0 10.0 10.0 10.0 10.0 10.0 10.0 10.0
O&M production-based expense ($) 0 1,714,220 2,069,952 2,516,875 2,991,037 3,492,966 4,022,825 4,164,255 4,305,473 4,446,544 4,587,507 4,728,390 4,869,211 5,009,984 5,150,718 5,291,422 5,432,101 5,572,760 5,713,403 5,854,032 5,994,619
O&M capacity-based expense ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Fuel expense ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Expand Down
61 changes: 61 additions & 0 deletions tests/geophires_x_tests/test_economics_sam.py
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,67 @@ def test_royalty_rate_schedule(self):
places=3,
)

def test_royalty_rate_escalation_start_year(self) -> None:
construction_years: int = 5
plant_lifetime: int = 20

def _get_result(start_year: int) -> tuple[str, dict[str, Any], GeophiresXResult]:
_input_file_path = self._get_test_file_path('generic-egs-case.txt')
_additional_params = {
'Economic Model': 5,
'Construction Years': construction_years,
'Plant Lifetime': plant_lifetime,
'Royalty Rate': 0.04,
'Royalty Rate Maximum': 0.06,
'Royalty Rate Escalation': 0.02,
'Royalty Rate Escalation Start Year': start_year,
'Print Output to Console': 1,
}
input_params: GeophiresInputParameters = GeophiresInputParameters(
from_file_path=_input_file_path,
params=_additional_params,
)
return _input_file_path, _additional_params, GeophiresXClient().get_geophires_result(input_params)

def __cash_flow_row(r: GeophiresXResult, row_name: str) -> str:
from geophires_x.EconomicsSam import _cash_flow_profile_row

return [it for it in _cash_flow_profile_row(r.result['SAM CASH FLOW PROFILE'], row_name) if is_float(it)]

def _royalty_cash_flow(r: GeophiresXResult) -> list[float]:
return __cash_flow_row(r, 'O&M production-based expense ($)')[1:] # Drop year 0 ($0 revenue)

def _royalty_rates_from_cash_flow(r: GeophiresXResult) -> list[float]:
return __cash_flow_row(r, 'Royalty rate (%)')

input_file_path, additional_params, result_4 = _get_result(4)

expected_royalty_rate_schedule_4 = [*([0.04] * 3), *([0.06] * (plant_lifetime - 3))]
model = EconomicsSamTestCase._new_model(input_file_path, additional_params=additional_params)
econ_royalty_rate_schedule_4 = model.economics.get_royalty_rate_schedule(model)
self.assertListEqual(expected_royalty_rate_schedule_4, econ_royalty_rate_schedule_4)

result_4_royalty_cash_flow_usd = _royalty_cash_flow(result_4)
self.assertEqual(len(expected_royalty_rate_schedule_4), len(result_4_royalty_cash_flow_usd))

econ_royalty_rate_schedule_4_percent = [
quantity(it, 'dimensionless').to(convertible_unit('percent')).magnitude
for it in econ_royalty_rate_schedule_4
]
royalty_rates_from_cash_flow_4 = _royalty_rates_from_cash_flow(result_4)
self.assertListEqual(econ_royalty_rate_schedule_4_percent, royalty_rates_from_cash_flow_4)

expected_royalties_based_on_cash_flow_ppa_revenue = []
cash_flow_ppa_revenue = __cash_flow_row(result_4, 'PPA revenue ($)')[1:]
for i, _rev_usd in enumerate(econ_royalty_rate_schedule_4_percent):
expected_royalties_based_on_cash_flow_ppa_revenue.append(
cash_flow_ppa_revenue[i] * econ_royalty_rate_schedule_4[i]
)

self.assertListAlmostEqual(
expected_royalties_based_on_cash_flow_ppa_revenue, result_4_royalty_cash_flow_usd, percent=0.0001
)

def test_sam_cash_flow_total_after_tax_returns_all_years(self):
input_file = self._egs_test_file_path()
additional_params = {'Construction Years': 2}
Expand Down
Loading