Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
dca152f
initial royalty schedule impl (https://github.com/NatLabRockies/GEOPH…
softwareengineerprogrammer Feb 24, 2026
ee195a8
Bump version: 3.11.17 → 3.11.18
softwareengineerprogrammer Feb 27, 2026
e4e82dc
Royalty Supplemental Payments parameter definition
softwareengineerprogrammer Feb 27, 2026
b7adc72
calculate royalty supplemental payments during construction, includin…
softwareengineerprogrammer Feb 27, 2026
898f8df
include royalty supplementary payments in opex
softwareengineerprogrammer Feb 27, 2026
ec303b8
include royalty supplemental payments in royalty opex
softwareengineerprogrammer Feb 27, 2026
17b4cf9
regen schema
softwareengineerprogrammer Feb 27, 2026
a258dea
account for supplementary payments during construction years in royal…
softwareengineerprogrammer Feb 27, 2026
17f29bb
test_royalty_supplemental_payments
softwareengineerprogrammer Feb 27, 2026
57ce8e3
swap order of inflation and interest during construction to match cas…
softwareengineerprogrammer Feb 27, 2026
b590d1f
include Royalty supplemental payments during construction in capital …
softwareengineerprogrammer Feb 27, 2026
461c46c
regenerate SAM-EM examples with royalties - average annual royalty ho…
softwareengineerprogrammer Feb 27, 2026
0061b4d
remove extraneously-added space (https://github.com/softwareengineerp…
softwareengineerprogrammer Feb 28, 2026
2d2de90
address TODO to validate all royalty params are SAM-EM-only
softwareengineerprogrammer Feb 28, 2026
0cc9a55
validate that only one of royalty rate and royalty schedule are provided
softwareengineerprogrammer Feb 28, 2026
e133b0b
mark more tooltip text TODOs
softwareengineerprogrammer Feb 28, 2026
e70e341
remove extraneous commented code per https://github.com/NatLabRockies…
softwareengineerprogrammer Feb 28, 2026
d197f7d
Bump version: 3.11.18 → 3.11.19
softwareengineerprogrammer Feb 28, 2026
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.11.17
current_version = 3.11.19
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.11.17
version: 3.11.19
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.11.17.svg
.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.11.19.svg
:alt: Commits since latest release
:target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.11.17...main
:target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.11.19...main

.. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat
:target: https://softwareengineerprogrammer.github.io/GEOPHIRES
Expand Down
Binary file modified docs/_images/fervo_project_cape-5-power-production.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/_images/fervo_project_cape-5-production-temperature.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
206 changes: 103 additions & 103 deletions docs/_images/fervo_project_cape-5-sensitivity-analysis-irr.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
200 changes: 100 additions & 100 deletions docs/_images/fervo_project_cape-5-sensitivity-analysis-lcoe.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
210 changes: 105 additions & 105 deletions docs/_images/fervo_project_cape-5-sensitivity-analysis-project_npv.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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.11.17'
version = release = '3.11.19'

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.11.17',
version='3.11.19',
license='MIT',
description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.',
long_description='{}\n{}'.format(
Expand Down
122 changes: 107 additions & 15 deletions src/geophires_x/Economics.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
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, \
_YEAR_INDEX_VALUE_EXPLANATION_SNIPPET, investment_tax_credit_output_parameter
_YEAR_INDEX_VALUE_EXPLANATION_SNIPPET, investment_tax_credit_output_parameter, expand_schedule
from geophires_x.GeoPHIRESUtils import quantity
from geophires_x.OptionList import Configuration, WellDrillingCostCorrelation, EconomicModel, EndUseOptions, PlantType, \
_WellDrillingCostCorrelationCitation
Expand Down Expand Up @@ -991,6 +991,8 @@ def __init__(self, model: Model):
UnitType=Units.PERCENT,
PreferredUnits=PercentUnit.TENTH,
CurrentUnits=PercentUnit.TENTH,
# TODO clarify relation to supplemental payments
# TODO document mutual incompatibility with Royalty Rate Schedule
ToolTipText="The fraction of the project's gross annual revenue paid to the royalty holder. "
"This is modeled as a variable production-based operating expense, reducing the developer's "
"taxable income."
Expand All @@ -1004,6 +1006,7 @@ def __init__(self, model: Model):
UnitType=Units.PERCENT,
PreferredUnits=PercentUnit.TENTH,
CurrentUnits=PercentUnit.TENTH,
# TODO clarify applies to Royalty Rate and not schedule
ToolTipText="The additive amount the royalty rate increases each year. For example, a value of 0.001 "
"increases a 4% rate (0.04) to 4.1% (0.041) in the next year."
)
Expand All @@ -1015,6 +1018,7 @@ def __init__(self, model: Model):
UnitType=Units.NONE,
PreferredUnits=TimeUnit.YEAR,
CurrentUnits=TimeUnit.YEAR,
# TODO clarify applies to Royalty Rate and not schedule
ToolTipText=f'The first year that the {self.royalty_escalation_rate.Name} is applied. '
f'{_YEAR_INDEX_VALUE_EXPLANATION_SNIPPET}.'
)
Expand All @@ -1028,12 +1032,40 @@ def __init__(self, model: Model):
UnitType=Units.PERCENT,
PreferredUnits=PercentUnit.TENTH,
CurrentUnits=PercentUnit.TENTH,
# TODO clarify applies to Royalty Rate and not schedule
ToolTipText=f"The maximum royalty rate after escalation, expressed as a fraction (e.g., 0.06 for a 6% cap)."
f"{' Defaults to 100% (no effective cap).' if maximum_royalty_rate_default_val == 1.0 else ''}"
)

# TODO support custom royalty rate schedule as a list parameter
# (as an alternative to specifying rate/escalation/max)
self.royalty_rate_schedule = self.ParameterDict[self.royalty_rate_schedule.Name] = listParameter(
'Royalty Rate Schedule',
Min=0.0,
Max=1.0,
UnitType=Units.PERCENT,
PreferredUnits=PercentUnit.TENTH,
CurrentUnits=PercentUnit.TENTH,
ToolTipText='A schedule DSL string defining the royalty rate for each year of the project, '
'starting at Year 1. ' # TODO clarify this means operational phase/COD
'Syntax: "<rate> * <years>, <rate> * <years>, ..., <terminal_rate>". '
'For example "0.0175 * 10, 0.035" means 1.75% for 10 years then 3.5% thereafter. '
# TODO document mutual exclusivity with Royalty Rate
# 'If provided, this overrides Royalty Rate, Royalty Rate Escalation, '
# 'and Royalty Rate Maximum.'
)

self.royalty_supplemental_payments = self.ParameterDict[self.royalty_supplemental_payments.Name] = listParameter(
'Royalty Supplemental Payments',
Min=0.0,
Max=1.0,
UnitType=Units.CURRENCYFREQUENCY,
PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR,
CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR,
# TODO improve phrasing, contrast with Royalty Rate Schedule beginning at Year 1
ToolTipText='A schedule DSL string defining the royalty supplemental payments for each year of the '
'project, starting at the first construction year. '
'Syntax: "<amount> * <years>, <amount> * <years>, ..., <terminal_amount_per_year>". '
'For example "1 * 2, 0.25" means $1M for 2 years then $250k/year thereafter. '
)

self.royalty_holder_discount_rate = self.ParameterDict[self.royalty_holder_discount_rate.Name] = floatParameter(
'Royalty Holder Discount Rate',
Expand Down Expand Up @@ -1265,8 +1297,9 @@ def __init__(self, model: Model):
DefaultValue=False,
UnitType=Units.NONE,
Required=False,
ErrMessage="assume default: no economics calculations",
ToolTipText="Set to true if you want the add-on economics calculations to be made"
ToolTipText="By default, add-on calculations are automatically enabled if add-ons parameters are provided. "
"Set this value to false to disable add-on economics calculations. "
"(If, for example, you wish to quickly compare between economics with and without add-ons.)"
)
self.DoCarbonCalculations = self.ParameterDict[self.DoCarbonCalculations.Name] = boolParameter(
"Do Carbon Price Calculations",
Expand Down Expand Up @@ -2108,7 +2141,8 @@ def __init__(self, model: Model):
PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR,
CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR,
ToolTipText=f'GEOPHIRES estimates the annual O&M costs as the sum of the annual surface plant, wellfield, '
f'make-up water, and pumping O&M costs.'
f'make-up water, pumping O&M costs, '
f'and average royalty costs (both production-based and supplemental payments).'
)
self.averageannualpumpingcosts = OutputParameter(
Name="Average Annual Pumping Costs",
Expand All @@ -2131,6 +2165,7 @@ def __init__(self, model: Model):
CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR,
ToolTipText='The average annual cost paid to a royalty holder, calculated as a percentage of the '
'project\'s gross annual revenue. This is modeled as a variable operating expense.'
# TODO adjust for Royalty Supplemental Payments, including explaining construction vs. operational years
)


Expand Down Expand Up @@ -2262,6 +2297,15 @@ def __init__(self, model: Model):
self.inflation_cost_during_construction = self.OutputParameterDict[
self.inflation_cost_during_construction.Name] = inflation_cost_during_construction_output_parameter()

self.royalty_supplemental_payments_cost_during_construction = self.OutputParameterDict[
self.royalty_supplemental_payments_cost_during_construction.Name] = OutputParameter(
Name='Royalty supplemental payments during construction',
UnitType=Units.CURRENCY,
PreferredUnits=CurrencyUnit.MDOLLARS,
CurrentUnits=CurrencyUnit.MDOLLARS,
ToolTipText='The sum of royalty supplemental payments during the construction period.',
)

self.interest_during_construction = self.OutputParameterDict[
self.interest_during_construction.Name] = interest_during_construction_output_parameter()

Expand Down Expand Up @@ -2368,6 +2412,7 @@ def __init__(self, model: Model):
UnitType=Units.CURRENCYFREQUENCY,
PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR,
CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR,
# TODO clarify that this includes construction years (production-based vs. supplemental payments)
ToolTipText="The royalty holder's gross (pre-tax) annual revenue stream from the royalty agreement."
)
self.royalty_holder_total_revenue = self.OutputParameterDict[
Expand Down Expand Up @@ -2636,7 +2681,11 @@ def _warn(_msg: str) -> None:
else:
sam_em_only_params: list[Parameter] = [
self.royalty_rate,
# TODO other royalty params
self.royalty_escalation_rate,
self.royalty_escalation_rate_start_year,
self.maximum_royalty_rate,
self.royalty_rate_schedule,
self.royalty_supplemental_payments,
self.construction_capex_schedule,
self.bond_financing_start_year
]
Expand Down Expand Up @@ -3401,11 +3450,20 @@ def build_price_models(self, model: Model) -> None:

def get_royalty_rate_schedule(self, model: Model) -> list[float]:
"""
Builds a year-by-year schedule of royalty rates based on escalation and cap.
Build the royalty rate schedule for each operational year.

:type model: :class:`~geophires_x.Model.Model`
:return: schedule: A list of rates as fractions (e.g., 0.05 for 5%).
If ``royalty_rate_schedule`` was provided via the DSL, it is expanded using
:func:`expand_schedule` and takes precedence. Otherwise the legacy
``royalty_rate`` + ``royalty_escalation_rate`` + ``maximum_royalty_rate`` logic
is used.

:returns: A list of royalty rates (fractional, e.g. 0.035 for 3.5%) with
one entry per operational year (length == ``plant_lifetime``).
"""
plant_lifetime: int = model.surfaceplant.plant_lifetime.value

if self.royalty_rate_schedule.Provided and self.royalty_rate_schedule.value:
return expand_schedule(self.royalty_rate_schedule.value, plant_lifetime)

def r(x: float) -> float:
"""Ignore apparent float precision issue"""
Expand All @@ -3427,6 +3485,20 @@ def r(x: float) -> float:

return schedule

def get_royalty_supplemental_payments_schedule_usd(self, model: Model) -> list[float]:
construction_years: int = model.surfaceplant.construction_years.value
operational_years: int = model.surfaceplant.plant_lifetime.value

royalty_supplemental_payments_schedule_expanded = expand_schedule(
self.royalty_supplemental_payments.value, construction_years + operational_years)

royalty_supplemental_payments_schedule_usd = [
PlainQuantity(it, self.royalty_supplemental_payments.CurrentUnits).to('USD/yr').magnitude
for it in royalty_supplemental_payments_schedule_expanded
]

return royalty_supplemental_payments_schedule_usd


def calculate_cashflow(self, model: Model) -> None:
"""
Expand Down Expand Up @@ -3532,7 +3604,6 @@ def _calculate_sam_economics(self, model: Model) -> None:
# since SAM Economic Model doesn't subtract ITC from this value.
self.capex_total.value = (self.sam_economics_calculations.capex.quantity()
.to(self.capex_total.CurrentUnits.value).magnitude)
# self.capex_total_per_kw.value = PlainQuantity(self.capex_total.value, f'{self.capex_total.CurrentUnits}'

# TODO define this as an output of SurfacePlant rather than calculating it on-demand here and elsewhere
max_net_electricity_generation_kw = quantity(
Expand All @@ -3554,7 +3625,7 @@ def _calculate_sam_economics(self, model: Model) -> None:
).to(self.interest_during_construction.CurrentUnits.value).magnitude


if self.royalty_rate.Provided:
if self.has_royalties:
# ignore pre-revenue year(s) (e.g. Year 0)
pre_revenue_years_slice_index = model.surfaceplant.construction_years.value

Expand All @@ -3569,25 +3640,39 @@ def _calculate_sam_economics(self, model: Model) -> None:

self.Coam.value += (self.royalties_average_annual_cost.quantity()
.to(self.Coam.CurrentUnits.value).magnitude)
# Note that updating Coam's value here does not affect already-calculated cash flow/result OPEX

self.royalty_holder_npv.value = quantity(
calculate_npv(
self.royalty_holder_discount_rate.value,
self.sam_economics_calculations.royalties_opex.value,
self.sam_economics_calculations.royalties_opex.value, # Includes construction years
self.discount_initial_year_cashflow.value
),
self.sam_economics_calculations.royalties_opex.CurrentUnits.get_currency_unit_str()
).to(self.royalty_holder_npv.CurrentUnits).magnitude

self.royalty_holder_annual_revenue.value = self.royalties_average_annual_cost.value

self.royalty_holder_annual_revenue.value = (quantity(
np.average(
self.sam_economics_calculations.royalties_opex.value # Includes construction years
),
self.sam_economics_calculations.royalties_opex.CurrentUnits
).to(self.royalty_holder_annual_revenue.CurrentUnits).magnitude)

self.royalty_holder_total_revenue.value = quantity(
np.sum(
self.sam_economics_calculations.royalties_opex.value[pre_revenue_years_slice_index:]
self.sam_economics_calculations.royalties_opex.value # Includes construction years
),
self.sam_economics_calculations.royalties_opex.CurrentUnits.get_currency_unit_str()
).to(self.royalty_holder_total_revenue.CurrentUnits).magnitude

self.royalty_supplemental_payments_cost_during_construction.value = quantity(
np.sum(
self.sam_economics_calculations.royalties_opex.value[:pre_revenue_years_slice_index]
),
self.sam_economics_calculations.royalties_opex.CurrentUnits.get_currency_unit_str()
).to(self.royalty_supplemental_payments_cost_during_construction.CurrentUnits).magnitude


self.wacc.value = self.sam_economics_calculations.wacc.value
self.nominal_discount_rate.value = self.sam_economics_calculations.nominal_discount_rate.value
Expand Down Expand Up @@ -3629,6 +3714,13 @@ def _calculate_derived_outputs(self, model: Model) -> None:
(model.wellbores.nprod.value + model.wellbores.ninj.value)
)

@property
def has_production_based_royalties(self):
return self.royalty_rate.Provided or self.royalty_rate_schedule.Provided

@property
def has_royalties(self):
return self.has_production_based_royalties or self.royalty_supplemental_payments.Provided


def __str__(self):
Expand Down
Loading