Skip to content

Commit c5fe65b

Browse files
Royalty escalation rate + max (WIP to add unit tests, implement full schedule support)
1 parent 5dd16d2 commit c5fe65b

File tree

3 files changed

+70
-9
lines changed

3 files changed

+70
-9
lines changed

src/geophires_x/Economics.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -975,11 +975,34 @@ def __init__(self, model: Model):
975975
UnitType=Units.PERCENT,
976976
PreferredUnits=PercentUnit.TENTH,
977977
CurrentUnits=PercentUnit.TENTH,
978-
ToolTipText="The percentage of the project's gross annual revenue paid to the royalty holder. "
978+
ToolTipText="The fraction of the project's gross annual revenue paid to the royalty holder. "
979979
"This is modeled as a variable production-based operating expense, reducing the developer's "
980980
"taxable income."
981981
)
982982

983+
self.royalty_escalation_rate = self.ParameterDict[self.royalty_escalation_rate.Name] = floatParameter(
984+
'Royalty Escalation Rate',
985+
DefaultValue=0.,
986+
Min=0.0,
987+
Max=1.0,
988+
UnitType=Units.PERCENT,
989+
PreferredUnits=PercentUnit.TENTH,
990+
CurrentUnits=PercentUnit.TENTH,
991+
ToolTipText="The additive amount the royalty rate increases each year. For example, a value of 0.001 "
992+
"increases a 4% rate (0.04) to 4.1% (0.041) in the next year."
993+
)
994+
995+
self.maximum_royalty_rate = self.ParameterDict[self.maximum_royalty_rate.Name] = floatParameter(
996+
'Maximum Royalty Rate',
997+
DefaultValue=1.0, # Default to 100% (no effective cap)
998+
Min=0.0,
999+
Max=1.0,
1000+
UnitType=Units.PERCENT,
1001+
PreferredUnits=PercentUnit.TENTH,
1002+
CurrentUnits=PercentUnit.TENTH,
1003+
ToolTipText="The maximum royalty rate after escalation, expressed as a fraction (e.g., 0.06 for a 6% cap)."
1004+
)
1005+
9831006
self.royalty_holder_discount_rate = self.ParameterDict[self.royalty_holder_discount_rate.Name] = floatParameter(
9841007
'Royalty Holder Discount Rate',
9851008
DefaultValue=0.05,

src/geophires_x/EconomicsSam.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -417,20 +417,19 @@ def _get_single_owner_parameters(model: Model) -> dict[str, Any]:
417417
)
418418
ret['ppa_price_input'] = ppa_price_schedule_per_kWh
419419

420-
if hasattr(econ, 'royalty_rate') and econ.royalty_rate.Provided:
421-
royalty_rate_fraction = econ.royalty_rate.quantity().to(convertible_unit('dimensionless')).magnitude
422-
420+
royalty_rate_schedule = _get_royalty_rate_schedule(model)
421+
if model.economics.royalty_rate.Provided:
423422
# For each year, calculate the royalty as a $/MWh variable cost.
424423
# The royalty is a percentage of revenue (MWh * $/MWh). By setting the
425424
# variable O&M rate to (PPA Price * Royalty Rate), SAM's calculation
426425
# (Rate * MWh) will correctly yield the total royalty payment.
427-
variable_om_schedule_per_MWh = [
428-
(price_per_kWh * 1000) * royalty_rate_fraction # TODO pint unit conversion (kWh -> MWh)
429-
for price_per_kWh in ppa_price_schedule_per_kWh
426+
variable_om_schedule_per_mwh = [
427+
(price_kwh * 1000) * royalty_fraction # TODO use pint unit conversion instead
428+
for price_kwh, royalty_fraction in zip(ppa_price_schedule_per_kWh, royalty_rate_schedule)
430429
]
431430

432431
# The PySAM parameter for variable operating cost in $/MWh is 'om_production'.
433-
ret['om_production'] = variable_om_schedule_per_MWh
432+
ret['om_production'] = variable_om_schedule_per_mwh
434433

435434
# Debt/equity ratio ('Fraction of Investment in Bonds' parameter)
436435
ret['debt_percent'] = _pct(econ.FIB)
@@ -481,5 +480,26 @@ def _ppa_pricing_model(
481480
)
482481

483482

483+
def _get_royalty_rate_schedule(model: Model) -> list[float]:
484+
"""
485+
Builds a year-by-year schedule of royalty rates based on escalation and cap.
486+
Returns a list of rates as fractions (e.g., 0.05 for 5%).
487+
"""
488+
489+
econ = model.economics
490+
plant_lifetime = model.surfaceplant.plant_lifetime.value
491+
492+
escalation_rate = econ.royalty_escalation_rate.value
493+
max_rate = econ.maximum_royalty_rate.value
494+
495+
schedule = []
496+
current_rate = econ.royalty_rate.value
497+
for _ in range(plant_lifetime):
498+
schedule.append(min(current_rate, max_rate))
499+
current_rate += escalation_rate
500+
501+
return schedule
502+
503+
484504
def _get_max_total_generation_kW(model: Model) -> float:
485505
return np.max(model.surfaceplant.ElectricityProduced.quantity().to(convertible_unit('kW')).magnitude)

src/geophires_x_schema_generator/geophires-request.json

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1639,14 +1639,32 @@
16391639
"maximum": 1.0
16401640
},
16411641
"Royalty Rate": {
1642-
"description": "The percentage 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.",
1642+
"description": "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.",
16431643
"type": "number",
16441644
"units": "",
16451645
"category": "Economics",
16461646
"default": 0.0,
16471647
"minimum": 0.0,
16481648
"maximum": 1.0
16491649
},
1650+
"Royalty Escalation Rate": {
1651+
"description": "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.",
1652+
"type": "number",
1653+
"units": "",
1654+
"category": "Economics",
1655+
"default": 0.0,
1656+
"minimum": 0.0,
1657+
"maximum": 1.0
1658+
},
1659+
"Maximum Royalty Rate": {
1660+
"description": "The maximum royalty rate after escalation, expressed as a fraction (e.g., 0.06 for a 6% cap).",
1661+
"type": "number",
1662+
"units": "",
1663+
"category": "Economics",
1664+
"default": 1.0,
1665+
"minimum": 0.0,
1666+
"maximum": 1.0
1667+
},
16501668
"Royalty Holder Discount Rate": {
16511669
"description": "The discount rate used to calculate the Net Present Value (NPV) of the royalty holder's income stream. This rate should reflect the royalty holder's specific risk profile and is separate from the main project discount rate.",
16521670
"type": "number",

0 commit comments

Comments
 (0)