Skip to content

Commit 7a3cd96

Browse files
test_royalty_rate_escalation
1 parent b9a3322 commit 7a3cd96

File tree

3 files changed

+77
-5
lines changed

3 files changed

+77
-5
lines changed

src/geophires_x/Economics.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3222,14 +3222,20 @@ def get_royalty_rate_schedule(self, model: Model) -> list[float]:
32223222
:return: schedule: A list of rates as fractions (e.g., 0.05 for 5%).
32233223
"""
32243224

3225+
def r(x: float) -> float:
3226+
"""Ignore apparent float precision issue"""
3227+
_precision = 8
3228+
return round(x, _precision)
3229+
32253230
plant_lifetime = model.surfaceplant.plant_lifetime.value
32263231

3227-
escalation_rate = self.royalty_escalation_rate.value
3228-
max_rate = self.maximum_royalty_rate.value
3232+
escalation_rate = r(self.royalty_escalation_rate.value)
3233+
max_rate = r(self.maximum_royalty_rate.value)
32293234

32303235
schedule = []
3231-
current_rate = self.royalty_rate.value
3236+
current_rate = r(self.royalty_rate.value)
32323237
for _ in range(plant_lifetime):
3238+
current_rate = r(current_rate)
32333239
schedule.append(min(current_rate, max_rate))
32343240
current_rate += escalation_rate
32353241

src/geophires_x/EconomicsSam.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -423,13 +423,13 @@ def _get_single_owner_parameters(model: Model) -> dict[str, Any]:
423423
# The royalty is a percentage of revenue (MWh * $/MWh). By setting the
424424
# variable O&M rate to (PPA Price * Royalty Rate), SAM's calculation
425425
# (Rate * MWh) will correctly yield the total royalty payment.
426-
variable_om_schedule_per_mwh = [
426+
variable_om_schedule_per_MWh = [
427427
(price_kwh * 1000) * royalty_fraction # TODO use pint unit conversion instead
428428
for price_kwh, royalty_fraction in zip(ppa_price_schedule_per_kWh, royalty_rate_schedule)
429429
]
430430

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

434434
# Debt/equity ratio ('Fraction of Investment in Bonds' parameter)
435435
ret['debt_percent'] = _pct(econ.FIB)

tests/test_geophires_x.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1309,6 +1309,7 @@ def test_redrilling_costs(self):
13091309
def test_royalty_rate(self):
13101310
royalties_output_name = 'Average Annual Royalty Cost'
13111311

1312+
zero_royalty_npv = None
13121313
for royalty_rate in [0, 0.1]:
13131314
result = GeophiresXClient().get_geophires_result(
13141315
ImmutableGeophiresInputParameters(
@@ -1345,6 +1346,9 @@ def test_royalty_rate(self):
13451346
self.assertEqual(58.88, opex_result[royalties_output_name]['value'])
13461347
self.assertGreater(royalty_holder_npv_MUSD, 0)
13471348

1349+
# Owner NPV is lower when royalty rate is non-zero
1350+
self.assertGreater(zero_royalty_npv, result.result['ECONOMIC PARAMETERS']['Project NPV']['value'])
1351+
13481352
royalties_cash_flow_MUSD = [
13491353
it * 1e-6
13501354
for it in _cash_flow_profile_row(
@@ -1362,3 +1366,65 @@ def test_royalty_rate(self):
13621366
if royalty_rate == 0.0:
13631367
self.assertEqual(0, opex_result[royalties_output_name]['value'])
13641368
self.assertEqual(0, royalty_holder_npv_MUSD)
1369+
zero_royalty_npv = result.result['ECONOMIC PARAMETERS']['Project NPV']['value']
1370+
1371+
def test_royalty_rate_escalation(self):
1372+
royalties_output_name = 'Average Annual Royalty Cost'
1373+
1374+
base_royalty_rate = 0.05
1375+
escalation_rate = 0.01
1376+
1377+
for max_rate in [0.08, 1.0]:
1378+
result = GeophiresXClient().get_geophires_result(
1379+
ImmutableGeophiresInputParameters(
1380+
from_file_path=self._get_test_file_path(
1381+
'geophires_x_tests/generic-egs-case-2_sam-single-owner-ppa.txt'
1382+
),
1383+
params={
1384+
'Royalty Rate': base_royalty_rate,
1385+
'Royalty Rate Escalation': escalation_rate,
1386+
'Royalty Rate Maximum': max_rate,
1387+
},
1388+
)
1389+
)
1390+
opex_result = result.result['OPERATING AND MAINTENANCE COSTS (M$/yr)']
1391+
1392+
self.assertIsNotNone(opex_result[royalties_output_name])
1393+
self.assertEqual('MUSD/yr', opex_result[royalties_output_name]['unit'])
1394+
1395+
total_opex_MUSD = opex_result['Total operating and maintenance costs']['value']
1396+
1397+
opex_line_item_sum = 0
1398+
for line_item_names in [
1399+
'Wellfield maintenance costs',
1400+
'Power plant maintenance costs',
1401+
'Water costs',
1402+
royalties_output_name,
1403+
]:
1404+
opex_line_item_sum += opex_result[line_item_names]['value']
1405+
1406+
self.assertAlmostEqual(opex_line_item_sum, total_opex_MUSD, places=4)
1407+
1408+
project_lifetime_yrs = result.result['ECONOMIC PARAMETERS']['Project lifetime']['value']
1409+
1410+
royalties_cash_flow_MUSD = [
1411+
it * 1e-6
1412+
for it in _cash_flow_profile_row(
1413+
result.result['SAM CASH FLOW PROFILE'], 'O&M production-based expense ($)'
1414+
)
1415+
][1:]
1416+
1417+
ppa_revenue_MUSD = [
1418+
it * 1e-6 for it in _cash_flow_profile_row(result.result['SAM CASH FLOW PROFILE'], 'PPA revenue ($)')
1419+
][1:]
1420+
1421+
actual_royalty_rate = [None] * len(ppa_revenue_MUSD)
1422+
for i in range(len(ppa_revenue_MUSD)):
1423+
actual_royalty_rate[i] = royalties_cash_flow_MUSD[i] / ppa_revenue_MUSD[i]
1424+
1425+
max_expected_rate = (
1426+
max_rate if max_rate < 1.0 else base_royalty_rate + escalation_rate * (project_lifetime_yrs - 1)
1427+
)
1428+
1429+
expected_last_year_revenue = ppa_revenue_MUSD[-1] * max_expected_rate
1430+
self.assertAlmostEqual(expected_last_year_revenue, royalties_cash_flow_MUSD[-1], places=3)

0 commit comments

Comments
 (0)