Skip to content

Commit a002381

Browse files
WIP - calculate Royalty Holder NPV
1 parent 97203de commit a002381

File tree

5 files changed

+119
-50
lines changed

5 files changed

+119
-50
lines changed

src/geophires_x/Economics.py

Lines changed: 86 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -968,7 +968,7 @@ def __init__(self, model: Model):
968968
)
969969

970970
self.royalty_rate = self.ParameterDict[self.royalty_rate.Name] = floatParameter(
971-
"Royalty Rate",
971+
'Royalty Rate',
972972
DefaultValue=0.,
973973
Min=0.0,
974974
Max=1.0,
@@ -978,6 +978,18 @@ def __init__(self, model: Model):
978978
ToolTipText="Royalty rate used in SAM Economic Models." # FIXME WIP TODO documentation
979979
)
980980

981+
self.royalty_holder_discount_rate = self.ParameterDict[self.royalty_holder_discount_rate.Name] = floatParameter(
982+
'Royalty Holder Discount Rate',
983+
DefaultValue=0.05,
984+
Min=0.0,
985+
Max=1.0,
986+
UnitType=Units.PERCENT,
987+
PreferredUnits=PercentUnit.TENTH,
988+
CurrentUnits=PercentUnit.TENTH,
989+
ToolTipText="Royalty holder discount rate used in SAM Economic Models." # FIXME WIP TODO documentation
990+
)
991+
992+
981993
self.discount_initial_year_cashflow = self.ParameterDict[self.discount_initial_year_cashflow.Name] = boolParameter(
982994
'Discount Initial Year Cashflow',
983995
DefaultValue=False,
@@ -2134,6 +2146,33 @@ def __init__(self, model: Model):
21342146
UnitType=Units.NONE,
21352147
)
21362148

2149+
# Results for the Royalty Holder
2150+
self.royalty_holder_npv = self.OutputParameterDict[self.royalty_holder_npv.Name] = OutputParameter(
2151+
'Royalty Holder NPV',
2152+
UnitType=Units.CURRENCY,
2153+
PreferredUnits=CurrencyUnit.MDOLLARS,
2154+
CurrentUnits=CurrencyUnit.MDOLLARS,
2155+
ToolTipText="Net Present Value (NPV) of the royalty holder's cash flow stream."
2156+
)
2157+
self.royalty_holder_annual_revenue = self.OutputParameterDict[
2158+
self.royalty_holder_annual_revenue.Name
2159+
] = OutputParameter(
2160+
'Royalty Holder Annual Revenue',
2161+
UnitType=Units.CURRENCYFREQUENCY,
2162+
PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR,
2163+
CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR,
2164+
ToolTipText="The royalty holder's annual revenue stream from the royalty agreement."
2165+
)
2166+
self.royalty_holder_total_revenue = self.OutputParameterDict[
2167+
self.royalty_holder_total_revenue.Name
2168+
] = OutputParameter(
2169+
'Royalty Holder Total Revenue',
2170+
UnitType=Units.CURRENCY,
2171+
PreferredUnits=CurrencyUnit.MDOLLARS,
2172+
CurrentUnits=CurrencyUnit.MDOLLARS,
2173+
ToolTipText='The total (undiscounted) revenue received by the royalty holder over the project lifetime.'
2174+
)
2175+
21372176
model.logger.info(f'Complete {__class__!s}: {sys._getframe().f_code.co_name}')
21382177

21392178
def read_parameters(self, model: Model) -> None:
@@ -2504,38 +2543,8 @@ def Calculate(self, model: Model) -> None:
25042543
self.discount_initial_year_cashflow.value
25052544
)
25062545

2507-
non_calculated_output_placeholder_val = -1
25082546
if self.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA:
2509-
self.sam_economics_calculations = calculate_sam_economics(model)
2510-
2511-
# Setting capex_total distinguishes capex from CCap's display name of 'Total capital costs',
2512-
# since SAM Economic Model doesn't subtract ITC from this value.
2513-
self.capex_total.value = (self.sam_economics_calculations.capex.quantity()
2514-
.to(self.capex_total.CurrentUnits.value).magnitude)
2515-
self.CCap.value = (self.sam_economics_calculations.capex.quantity()
2516-
.to(self.CCap.CurrentUnits.value).magnitude)
2517-
2518-
average_annual_royalties = np.average(
2519-
self.sam_economics_calculations.royalties_opex.value[1:] # ignore pre-revenue year(s) (Year 0)
2520-
)
2521-
if average_annual_royalties > 0:
2522-
self.royalties_average_annual_cost.value = average_annual_royalties
2523-
self.Coam.value += self.royalties_average_annual_cost.quantity().to(self.Coam.CurrentUnits.value).magnitude
2524-
2525-
self.wacc.value = self.sam_economics_calculations.wacc.value
2526-
self.nominal_discount_rate.value = self.sam_economics_calculations.nominal_discount_rate.value
2527-
self.ProjectNPV.value = self.sam_economics_calculations.project_npv.quantity().to(
2528-
convertible_unit(self.ProjectNPV.CurrentUnits)).magnitude
2529-
2530-
self.ProjectIRR.value = non_calculated_output_placeholder_val # SAM calculates After-Tax IRR instead
2531-
self.after_tax_irr.value = self.sam_economics_calculations.after_tax_irr.quantity().to(
2532-
convertible_unit(self.ProjectIRR.CurrentUnits)).magnitude
2533-
2534-
self.ProjectMOIC.value = self.sam_economics_calculations.moic.value
2535-
self.ProjectVIR.value = self.sam_economics_calculations.project_vir.value
2536-
2537-
# TODO remove or clarify project payback period: https://github.com/NREL/GEOPHIRES-X/issues/413
2538-
self.ProjectPaybackPeriod.value = self.sam_economics_calculations.project_payback_period.value
2547+
self._calculate_sam_economics(model)
25392548

25402549
# Calculate the project payback period
25412550
if self.econmodel.value != EconomicModel.SAM_SINGLE_OWNER_PPA:
@@ -3267,6 +3276,51 @@ def calculate_cashflow(self, model: Model) -> None:
32673276
for i in range(1, model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value, 1):
32683277
self.TotalCummRevenue.value[i] = self.TotalCummRevenue.value[i-1] + self.TotalRevenue.value[i]
32693278

3279+
def _calculate_sam_economics(self, model: Model) -> None:
3280+
non_calculated_output_placeholder_val = -1
3281+
self.sam_economics_calculations = calculate_sam_economics(model)
3282+
3283+
# Setting capex_total distinguishes capex from CCap's display name of 'Total capital costs',
3284+
# since SAM Economic Model doesn't subtract ITC from this value.
3285+
self.capex_total.value = (self.sam_economics_calculations.capex.quantity()
3286+
.to(self.capex_total.CurrentUnits.value).magnitude)
3287+
self.CCap.value = (self.sam_economics_calculations.capex.quantity()
3288+
.to(self.CCap.CurrentUnits.value).magnitude)
3289+
3290+
3291+
if self.royalty_rate.Provided:
3292+
average_annual_royalties = np.average(
3293+
self.sam_economics_calculations.royalties_opex.value[1:] # ignore pre-revenue year(s) (Year 0)
3294+
)
3295+
3296+
self.royalties_average_annual_cost.value = average_annual_royalties
3297+
self.Coam.value += self.royalties_average_annual_cost.quantity().to(self.Coam.CurrentUnits.value).magnitude
3298+
3299+
self.royalty_holder_npv.value = calculate_npv(
3300+
self.royalty_holder_discount_rate.value,
3301+
self.sam_economics_calculations.royalties_opex.value,
3302+
self.discount_initial_year_cashflow.value
3303+
)
3304+
# FIXME WIP
3305+
# self.royalty_holder_annual_revenue
3306+
# self.royalty_holder_total_revenue
3307+
3308+
3309+
self.wacc.value = self.sam_economics_calculations.wacc.value
3310+
self.nominal_discount_rate.value = self.sam_economics_calculations.nominal_discount_rate.value
3311+
self.ProjectNPV.value = self.sam_economics_calculations.project_npv.quantity().to(
3312+
convertible_unit(self.ProjectNPV.CurrentUnits)).magnitude
3313+
3314+
self.ProjectIRR.value = non_calculated_output_placeholder_val # SAM calculates After-Tax IRR instead
3315+
self.after_tax_irr.value = self.sam_economics_calculations.after_tax_irr.quantity().to(
3316+
convertible_unit(self.ProjectIRR.CurrentUnits)).magnitude
3317+
3318+
self.ProjectMOIC.value = self.sam_economics_calculations.moic.value
3319+
self.ProjectVIR.value = self.sam_economics_calculations.project_vir.value
3320+
3321+
# TODO remove or clarify project payback period: https://github.com/NREL/GEOPHIRES-X/issues/413
3322+
self.ProjectPaybackPeriod.value = self.sam_economics_calculations.project_payback_period.value
3323+
32703324
# noinspection SpellCheckingInspection
32713325
def _calculate_derived_outputs(self, model: Model) -> None:
32723326
"""

src/geophires_x/EconomicsSam.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ 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.value > 0.0:
420+
if hasattr(econ, 'royalty_rate') and econ.royalty_rate.Provided:
421421
royalty_rate_fraction = econ.royalty_rate.quantity().to(convertible_unit('dimensionless')).magnitude
422422

423423
# For each year, calculate the royalty as a $/MWh variable cost.

src/geophires_x/Outputs.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,11 @@ def PrintOutputs(self, model: Model):
324324
if model.surfaceplant.enduse_option.value in [EndUseOptions.ELECTRICITY]:
325325
f.write(f' Estimated Jobs Created: {model.economics.jobs_created.value}\n')
326326

327+
if econ.royalty_rate.Provided:
328+
royalty_holder_npv_label = Outputs._field_label(econ.royalty_holder_npv.display_name, 49)
329+
f.write(
330+
f' {royalty_holder_npv_label}{econ.royalty_holder_npv.value:10.2f} {econ.royalty_holder_npv.CurrentUnits.value}\n')
331+
327332

328333
f.write(NL)
329334
f.write(' ***ENGINEERING PARAMETERS***\n')
@@ -555,7 +560,7 @@ def PrintOutputs(self, model: Model):
555560
aoc_label = Outputs._field_label(model.addeconomics.AddOnOPEXTotalPerYear.display_name, 47)
556561
f.write(f' {aoc_label}{model.addeconomics.AddOnOPEXTotalPerYear.value:10.2f} {model.addeconomics.AddOnOPEXTotalPerYear.CurrentUnits.value}\n')
557562

558-
if econ.royalty_rate.value > 0.0:
563+
if econ.royalty_rate.Provided:
559564
royalties_label = Outputs._field_label(econ.royalties_average_annual_cost.display_name, 47)
560565
f.write(f' {royalties_label}{econ.royalties_average_annual_cost.value:10.2f} {econ.royalties_average_annual_cost.CurrentUnits.value}\n')
561566

src/geophires_x_client/geophires_x_result.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ class GeophiresXResult:
9393
'Project Payback Period',
9494
'CHP: Percent cost allocation for electrical plant',
9595
'Estimated Jobs Created',
96+
'Royalty Holder NPV',
9697
],
9798
'EXTENDED ECONOMICS': [
9899
'Adjusted Project LCOE (after incentives, grants, AddOns,etc)',

tests/test_geophires_x.py

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,7 +1025,7 @@ def _get_result(
10251025
'Number of Injection Wells': doublets,
10261026

10271027
# offset contingency
1028-
'Reservoir Stimulation Capital Cost Adjustment Factor': 1/default_contingency_factor,
1028+
'Reservoir Stimulation Capital Cost Adjustment Factor': 1 / default_contingency_factor,
10291029
}
10301030
)
10311031
# fmt:on
@@ -1285,13 +1285,15 @@ def test_redrilling_costs(self):
12851285
capex_field_suffix = (
12861286
'' if result_capex.get('Drilling and completion costs') is not None else ' (for redrilling)'
12871287
)
1288+
# @formatter:off
12881289
expected_annual_redrilling_cost = (
12891290
(
12901291
result_capex[f'Drilling and completion costs{capex_field_suffix}']['value']
12911292
+ result_capex[f'Stimulation costs{capex_field_suffix}']['value']
12921293
)
12931294
* result_redrills
12941295
) / result.result['ECONOMIC PARAMETERS']['Project lifetime']['value']
1296+
# @formatter:on
12951297

12961298
self.assertAlmostEqual(expected_annual_redrilling_cost, result_opex['Redrilling costs']['value'], places=2)
12971299

@@ -1308,22 +1310,29 @@ def test_royalty_rate(self):
13081310
)
13091311
)
13101312
opex_result = result.result['OPERATING AND MAINTENANCE COSTS (M$/yr)']
1311-
if royalty_rate > 0.0:
1312-
self.assertIsNotNone(opex_result[royalties_output_name])
1313-
self.assertEqual(58.88, opex_result[royalties_output_name]['value'])
1314-
self.assertEqual('MUSD/yr', opex_result[royalties_output_name]['unit'])
13151313

1316-
total_opex_MUSD = opex_result['Total operating and maintenance costs']['value']
1314+
self.assertIsNotNone(opex_result[royalties_output_name])
1315+
self.assertEqual('MUSD/yr', opex_result[royalties_output_name]['unit'])
13171316

1318-
opex_line_item_sum = 0
1319-
for line_item_names in [
1320-
'Wellfield maintenance costs',
1321-
'Power plant maintenance costs',
1322-
'Water costs',
1323-
royalties_output_name,
1324-
]:
1325-
opex_line_item_sum += opex_result[line_item_names]['value']
1317+
total_opex_MUSD = opex_result['Total operating and maintenance costs']['value']
13261318

1327-
self.assertEqual(opex_line_item_sum, total_opex_MUSD)
1319+
opex_line_item_sum = 0
1320+
for line_item_names in [
1321+
'Wellfield maintenance costs',
1322+
'Power plant maintenance costs',
1323+
'Water costs',
1324+
royalties_output_name,
1325+
]:
1326+
opex_line_item_sum += opex_result[line_item_names]['value']
1327+
1328+
self.assertEqual(opex_line_item_sum, total_opex_MUSD)
1329+
1330+
econ_result = result.result['ECONOMIC PARAMETERS']
1331+
royalty_holder_npv_MUSD = econ_result['Royalty Holder NPV']['value']
1332+
1333+
if royalty_rate > 0.0:
1334+
self.assertEqual(58.88, opex_result[royalties_output_name]['value'])
1335+
self.assertGreater(royalty_holder_npv_MUSD, 0) # FIXME WIP
13281336
else:
1329-
self.assertIsNone(opex_result[royalties_output_name])
1337+
self.assertEqual(0, opex_result[royalties_output_name]['value'])
1338+
self.assertEqual(0, royalty_holder_npv_MUSD)

0 commit comments

Comments
 (0)