From 875f5989f394ede0235b6dd00e506bf4709efec5 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 15 Sep 2025 09:02:56 -0700 Subject: [PATCH 01/32] ignore ai.md --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index b69a931a9..a922924be 100644 --- a/.gitignore +++ b/.gitignore @@ -84,6 +84,8 @@ nosetests.xml .pydevproject .vscode +ai.md + # Complexity output/*.html output/*/index.html From e3b92c95158f889742f1ce9ec668e48a1bdeb36b Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 15 Sep 2025 09:53:48 -0700 Subject: [PATCH 02/32] Initial implementation of royalty rate. WIP - TODO to implement royalty collector revenue/NPV/etc., update documentation, add more unit tests --- src/geophires_x/Economics.py | 10 ++++++++++ src/geophires_x/EconomicsSam.py | 18 +++++++++++++++++- src/geophires_x/Units.py | 3 +++ tests/geophires_x_tests/test_economics_sam.py | 18 ++++++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index a5781a49e..f8457138f 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -967,6 +967,16 @@ def __init__(self, model: Model): "will be automatically set to the same value." ) + self.royalty_rate = self.ParameterDict[self.royalty_rate.Name] = floatParameter( + "Royalty Rate", + DefaultValue=0., + Min=0.0, + Max=1.0, + UnitType=Units.PERCENT, + PreferredUnits=PercentUnit.TENTH, + CurrentUnits=PercentUnit.TENTH, + ToolTipText="Royalty rate used in SAM Economic Models." # FIXME WIP TODO documentation + ) self.discount_initial_year_cashflow = self.ParameterDict[self.discount_initial_year_cashflow.Name] = boolParameter( 'Discount Initial Year Cashflow', diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index e1b14ccca..d2ced510d 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -395,13 +395,29 @@ def _get_single_owner_parameters(model: Model) -> dict[str, Any]: geophires_ptr_tenths = Decimal(econ.PTR.value) ret['property_tax_rate'] = float(geophires_ptr_tenths * Decimal(100)) - ret['ppa_price_input'] = _ppa_pricing_model( + ppa_price_schedule_per_kWh = _ppa_pricing_model( model.surfaceplant.plant_lifetime.value, econ.ElecStartPrice.value, econ.ElecEndPrice.value, econ.ElecEscalationStart.value, econ.ElecEscalationRate.value, ) + ret['ppa_price_input'] = ppa_price_schedule_per_kWh + + if hasattr(econ, 'royalty_rate') and econ.royalty_rate.value > 0.0: + royalty_rate_fraction = econ.royalty_rate.quantity().to(convertible_unit('dimensionless')).magnitude + + # For each year, calculate the royalty as a $/MWh variable cost. + # The royalty is a percentage of revenue (MWh * $/MWh). By setting the + # variable O&M rate to (PPA Price * Royalty Rate), SAM's calculation + # (Rate * MWh) will correctly yield the total royalty payment. + variable_om_schedule_per_MWh = [ + (price_per_kWh * 1000) * royalty_rate_fraction # TODO pint unit conversion (kWh -> MWh) + for price_per_kWh in ppa_price_schedule_per_kWh + ] + + # The PySAM parameter for variable operating cost in $/MWh is 'om_production'. + ret['om_production'] = variable_om_schedule_per_MWh # Debt/equity ratio ('Fraction of Investment in Bonds' parameter) ret['debt_percent'] = _pct(econ.FIB) diff --git a/src/geophires_x/Units.py b/src/geophires_x/Units.py index 50509c735..acda1b94a 100644 --- a/src/geophires_x/Units.py +++ b/src/geophires_x/Units.py @@ -26,6 +26,9 @@ def convertible_unit(unit: Any) -> Any: if unit == Units.PERCENT or unit == PercentUnit.PERCENT or unit == Units.PERCENT.value: return 'percent' + if unit == PercentUnit.TENTH or unit == PercentUnit.TENTH.value: + return 'dimensionless' + return unit diff --git a/tests/geophires_x_tests/test_economics_sam.py b/tests/geophires_x_tests/test_economics_sam.py index 55e76d50c..cb203f70a 100644 --- a/tests/geophires_x_tests/test_economics_sam.py +++ b/tests/geophires_x_tests/test_economics_sam.py @@ -638,6 +638,24 @@ def _assert_capex_line_items_sum_to_total(self, r: GeophiresXResult): self.assertEqual(total_capex, capex_line_item_sum) + def test_royalty_rate(self): + royalty_rate = 0.1 + m: Model = EconomicsSamTestCase._new_model( + self._egs_test_file_path(), additional_params={'Royalty Rate': royalty_rate} + ) + + sam_econ = calculate_sam_economics(m) + cash_flow = sam_econ.sam_cash_flow_profile + + def get_row(name: str): + return EconomicsSamTestCase._get_cash_flow_row(cash_flow, name) + + ppa_revenue_row = get_row('PPA revenue ($)') + expected_royalties = [x * royalty_rate for x in ppa_revenue_row] + + om_prod_based_expense_row = get_row('O&M production-based expense ($)') + self.assertListAlmostEqual(expected_royalties, om_prod_based_expense_row, places=0) + @staticmethod def _new_model(input_file: Path, additional_params: dict[str, Any] | None = None, read_and_calculate=True) -> Model: if additional_params is not None: From 86af5fd655e3f5d1cec3f77fffc9692000865270 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 15 Sep 2025 10:01:09 -0700 Subject: [PATCH 03/32] Add entry in SAM Economic Models docs --- docs/SAM-Economic-Models.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/SAM-Economic-Models.md b/docs/SAM-Economic-Models.md index b54fdddd6..0e79a3a28 100644 --- a/docs/SAM-Economic-Models.md +++ b/docs/SAM-Economic-Models.md @@ -22,6 +22,7 @@ The following table describes how GEOPHIRES parameters are transformed into SAM | `Net Electricity Generation` | AC Degradation | `Annual AC degradation rate` schedule | `Utilityrate5` | `degradation` | Percentage difference of each year's `Net Electricity Generation` from `Maximum Total Electricity Generation` is input as SAM as the degradation rate schedule in order to match SAM's generation profile to GEOPHIRES | | {`Total CAPEX` before inflation} × (1 + `Accrued financing during construction (%)`/100); | Installation Costs | `Total Installed Cost` | `Singleowner` | `total_installed_cost` | `Accrued financing during construction (%)` = (1+`Inflation Rate During Construction`) × 100 if `Inflation Rate During Construction` is provided or ((1+`Inflation Rate`) ^ `Construction Years`) × 100 if not. | | `Total O&M Cost`, `Inflation Rate` | Operating Costs | `Fixed operating cost`, `Escalation rate` set to `Inflation Rate` × -1 | `Singleowner` | `om_fixed`, `om_fixed_escal` | .. N/A | +| `Royalty Rate` | Operating Costs | `Variable operating cost` | `Singleowner` | `om_production` | Royalties are treated as a variable operating cost to the owner | | `Plant Lifetime` | Financial Parameters → Analysis Parameters | `Analysis period` | `CustomGeneration`, `Singleowner` | `CustomGeneration.analysis_period`, `Singleowner.term_tenor` | .. N/A | | `Inflation Rate` | Financial Parameters → Analysis Parameters | `Inflation rate` | `Utilityrate5` | `inflation_rate` | .. N/A | | `Discount Rate` | Financial Parameters → Analysis Parameters | `Real discount rate` | `Singleowner` | `real_discount_rate` | .. N/A | From 1dc6dcb8e14eb4e7c25e6042d1a7c6f448994233 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 15 Sep 2025 10:01:22 -0700 Subject: [PATCH 04/32] Regenerate schema (with Royalty Rate) --- src/geophires_x_schema_generator/geophires-request.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/geophires_x_schema_generator/geophires-request.json b/src/geophires_x_schema_generator/geophires-request.json index d7420d613..bfdd138dc 100644 --- a/src/geophires_x_schema_generator/geophires-request.json +++ b/src/geophires_x_schema_generator/geophires-request.json @@ -1638,6 +1638,15 @@ "minimum": 0.0, "maximum": 1.0 }, + "Royalty Rate": { + "description": "Royalty rate used in SAM Economic Models.", + "type": "number", + "units": "", + "category": "Economics", + "default": 0.0, + "minimum": 0.0, + "maximum": 1.0 + }, "Discount Initial Year Cashflow": { "description": "Whether to discount cashflow in the initial project year when calculating NPV (Net Present Value). The default value of False conforms to NREL's standard convention for NPV calculation (Short W et al, 1995. https://www.nrel.gov/docs/legosti/old/5173.pdf). A value of True will, by contrast, cause NPV calculation to follow the convention used by Excel, Google Sheets, and other common spreadsheet software. Although NREL's NPV convention may typically be considered more technically correct, Excel-style NPV calculation might be preferred for familiarity or compatibility with existing business processes. See https://github.com/NREL/GEOPHIRES-X/discussions/344 for further details.", "type": "boolean", From b5191f23741108cdf2ea1ea582e7ed9365f9ae97 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 15 Sep 2025 10:27:55 -0700 Subject: [PATCH 05/32] Mark TODO remove or clarify project payback period: https://github.com/NREL/GEOPHIRES-X/issues/413 --- src/geophires_x/Economics.py | 2 ++ src/geophires_x/EconomicsSam.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index f8457138f..345442652 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -2517,6 +2517,8 @@ def Calculate(self, model: Model) -> None: self.ProjectMOIC.value = self.sam_economics_calculations.moic.value self.ProjectVIR.value = self.sam_economics_calculations.project_vir.value + + # TODO remove or clarify project payback period: https://github.com/NREL/GEOPHIRES-X/issues/413 self.ProjectPaybackPeriod.value = self.sam_economics_calculations.project_payback_period.value # Calculate the project payback period diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index d2ced510d..139d53ff0 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -71,7 +71,9 @@ class SamEconomicsCalculations: wacc: OutputParameter = field(default_factory=wacc_output_parameter) moic: OutputParameter = field(default_factory=moic_parameter) project_vir: OutputParameter = field(default_factory=project_vir_parameter) + project_payback_period: OutputParameter = field(default_factory=project_payback_period_parameter) + """TODO remove or clarify project payback period: https://github.com/NREL/GEOPHIRES-X/issues/413""" def validate_read_parameters(model: Model): @@ -248,6 +250,7 @@ def _calculate_project_vir(cash_flow: list[list[Any]], model) -> float | None: def _calculate_project_payback_period(cash_flow: list[list[Any]], model) -> float | None: + """TODO remove or clarify project payback period: https://github.com/NREL/GEOPHIRES-X/issues/413""" try: after_tax_cash_flow = _cash_flow_profile_row(cash_flow, 'Total after-tax returns ($)') cumm_cash_flow = np.zeros(len(after_tax_cash_flow)) From 784bccc293ff2b052c1e68886a72be182f39cfa7 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:04:47 -0700 Subject: [PATCH 06/32] Include royalties in opex output/total opex (WIP) --- src/geophires_x/Economics.py | 12 +++++++++-- src/geophires_x/EconomicsSam.py | 12 ++++++++++- src/geophires_x/EconomicsUtils.py | 12 ++++++++++- src/geophires_x/Outputs.py | 4 ++++ src/geophires_x_client/geophires_x_result.py | 1 + tests/geophires_x_tests/test_economics_sam.py | 7 ++++++- tests/test_geophires_x.py | 21 +++++++++++++++++++ 7 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index 345442652..f82e31195 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -13,7 +13,7 @@ from geophires_x.EconomicsUtils import BuildPricingModel, wacc_output_parameter, nominal_discount_rate_parameter, \ real_discount_rate_parameter, after_tax_irr_parameter, moic_parameter, project_vir_parameter, \ project_payback_period_parameter, inflation_cost_during_construction_output_parameter, \ - total_capex_parameter_output_parameter + total_capex_parameter_output_parameter, royalties_opex_parameter_output_parameter from geophires_x.GeoPHIRESUtils import quantity from geophires_x.OptionList import Configuration, WellDrillingCostCorrelation, EconomicModel, EndUseOptions, PlantType, \ _WellDrillingCostCorrelationCitation @@ -1906,6 +1906,7 @@ def __init__(self, model: Model): PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR ) + self.royalties_opex = self.OutputParameterDict[self.royalties_opex.Name] = royalties_opex_parameter_output_parameter() # district heating self.peakingboilercost = self.OutputParameterDict[self.peakingboilercost.Name] = OutputParameter( @@ -2502,10 +2503,17 @@ def Calculate(self, model: Model) -> None: # Setting capex_total distinguishes capex from CCap's display name of 'Total capital costs', # 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) + .to(self.capex_total.CurrentUnits.value).magnitude) self.CCap.value = (self.sam_economics_calculations.capex.quantity() .to(self.CCap.CurrentUnits.value).magnitude) + # FIXME WIP adjust OPEX for royalties + # FIXME WIP unit conversion + average_annual_royalties = np.average(self.sam_economics_calculations.royalties_opex[1:]) # ignore Year 0 + if average_annual_royalties > 0: + self.royalties_opex.value = average_annual_royalties + self.Coam.value += self.royalties_opex.quantity().to(self.Coam.CurrentUnits.value).magnitude + self.wacc.value = self.sam_economics_calculations.wacc.value self.nominal_discount_rate.value = self.sam_economics_calculations.nominal_discount_rate.value self.ProjectNPV.value = self.sam_economics_calculations.project_npv.quantity().to( diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index 139d53ff0..b19311e5f 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -39,11 +39,12 @@ project_payback_period_parameter, inflation_cost_during_construction_output_parameter, total_capex_parameter_output_parameter, + royalties_opex_parameter_output_parameter, ) from geophires_x.GeoPHIRESUtils import is_float, is_int, sig_figs, quantity from geophires_x.OptionList import EconomicModel, EndUseOptions from geophires_x.Parameter import Parameter, OutputParameter, floatParameter -from geophires_x.Units import convertible_unit, EnergyCostUnit, CurrencyUnit, Units +from geophires_x.Units import convertible_unit, EnergyCostUnit, CurrencyUnit, Units, CurrencyFrequencyUnit @dataclass @@ -59,6 +60,8 @@ class SamEconomicsCalculations: capex: OutputParameter = field(default_factory=total_capex_parameter_output_parameter) + royalties_opex: OutputParameter = field(default_factory=royalties_opex_parameter_output_parameter) + project_npv: OutputParameter = field( default_factory=lambda: OutputParameter( UnitType=Units.CURRENCY, @@ -170,6 +173,13 @@ def sf(_v: float, num_sig_figs: int = 5) -> float: sam_economics.project_npv.value = sf(single_owner.Outputs.project_return_aftertax_npv * 1e-6) sam_economics.capex.value = single_owner.Outputs.adjusted_installed_cost * 1e-6 + royalty_rate = model.economics.royalty_rate.quantity().to('dimensionless').magnitude + ppa_revenue_row = _cash_flow_profile_row(cash_flow, 'PPA revenue ($)') + royalties_unit = sam_economics.royalties_opex.CurrentUnits.value.replace('/yr', '') + sam_economics.royalties_opex = [ + quantity(x * royalty_rate, 'USD').to(royalties_unit).magnitude for x in ppa_revenue_row + ] + sam_economics.nominal_discount_rate.value, sam_economics.wacc.value = _calculate_nominal_discount_rate_and_wacc( model, single_owner ) diff --git a/src/geophires_x/EconomicsUtils.py b/src/geophires_x/EconomicsUtils.py index 1f586a145..fed0bb2e1 100644 --- a/src/geophires_x/EconomicsUtils.py +++ b/src/geophires_x/EconomicsUtils.py @@ -1,7 +1,7 @@ from __future__ import annotations from geophires_x.Parameter import OutputParameter -from geophires_x.Units import Units, PercentUnit, TimeUnit, CurrencyUnit +from geophires_x.Units import Units, PercentUnit, TimeUnit, CurrencyUnit, CurrencyFrequencyUnit def BuildPricingModel(plantlifetime: int, StartPrice: float, EndPrice: float, @@ -144,3 +144,13 @@ def total_capex_parameter_output_parameter() -> OutputParameter: 'For SAM Economic models, it also includes any cost escalation from inflation during construction. ' 'It is used as the total installed cost input for SAM Economic Models.' ) + + +def royalties_opex_parameter_output_parameter() -> OutputParameter: + return OutputParameter( + Name='Royalties', + UnitType=Units.CURRENCYFREQUENCY, + PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, + CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, + ToolTipText='Average annual royalties' # TODO WIP clarify + ) diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index 26c082fa9..b6a943b49 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -555,6 +555,10 @@ def PrintOutputs(self, model: Model): aoc_label = Outputs._field_label(model.addeconomics.AddOnOPEXTotalPerYear.display_name, 47) f.write(f' {aoc_label}{model.addeconomics.AddOnOPEXTotalPerYear.value:10.2f} {model.addeconomics.AddOnOPEXTotalPerYear.CurrentUnits.value}\n') + if econ.royalty_rate.value > 0.0: + royalties_label = Outputs._field_label(econ.royalties_opex.display_name, 47) + f.write(f' {royalties_label}{econ.royalties_opex.value:10.2f} {econ.royalties_opex.CurrentUnits.value}\n') + f.write(f' {econ.Coam.display_name}: {(econ.Coam.value + econ.averageannualpumpingcosts.value + econ.averageannualheatpumpelectricitycost.value):10.2f} {econ.Coam.CurrentUnits.value}\n') else: f.write(f' {econ.Coam.display_name}: {econ.Coam.value:10.2f} {econ.Coam.CurrentUnits.value}\n') diff --git a/src/geophires_x_client/geophires_x_result.py b/src/geophires_x_client/geophires_x_result.py index aabb76199..a8049608c 100644 --- a/src/geophires_x_client/geophires_x_result.py +++ b/src/geophires_x_client/geophires_x_result.py @@ -290,6 +290,7 @@ class GeophiresXResult: 'Annual District Heating O&M Cost', 'Average Annual Peaking Fuel Cost', 'Average annual pumping costs', + 'Royalties', # SUTRA 'Average annual auxiliary fuel cost', 'Average annual pumping cost', diff --git a/tests/geophires_x_tests/test_economics_sam.py b/tests/geophires_x_tests/test_economics_sam.py index cb203f70a..6cb025aeb 100644 --- a/tests/geophires_x_tests/test_economics_sam.py +++ b/tests/geophires_x_tests/test_economics_sam.py @@ -20,6 +20,7 @@ get_sam_cash_flow_profile_tabulated_output, _ppa_pricing_model, _get_fed_and_state_tax_rates, + SamEconomicsCalculations, ) from geophires_x.GeoPHIRESUtils import sig_figs, quantity @@ -644,7 +645,7 @@ def test_royalty_rate(self): self._egs_test_file_path(), additional_params={'Royalty Rate': royalty_rate} ) - sam_econ = calculate_sam_economics(m) + sam_econ: SamEconomicsCalculations = calculate_sam_economics(m) cash_flow = sam_econ.sam_cash_flow_profile def get_row(name: str): @@ -653,8 +654,12 @@ def get_row(name: str): ppa_revenue_row = get_row('PPA revenue ($)') expected_royalties = [x * royalty_rate for x in ppa_revenue_row] + self.assertListEqual(expected_royalties, sam_econ.royalties_opex) + om_prod_based_expense_row = get_row('O&M production-based expense ($)') self.assertListAlmostEqual(expected_royalties, om_prod_based_expense_row, places=0) + # Note the above assertion assumes royalties are the only production-based O&M expenses. If this changes, + # the assertion will need to be updated. @staticmethod def _new_model(input_file: Path, additional_params: dict[str, Any] | None = None, read_and_calculate=True) -> Model: diff --git a/tests/test_geophires_x.py b/tests/test_geophires_x.py index 0e50e5d4a..08ae56f8f 100644 --- a/tests/test_geophires_x.py +++ b/tests/test_geophires_x.py @@ -1294,3 +1294,24 @@ def test_redrilling_costs(self): ) / result.result['ECONOMIC PARAMETERS']['Project lifetime']['value'] self.assertAlmostEqual(expected_annual_redrilling_cost, result_opex['Redrilling costs']['value'], places=2) + + def test_royalty_rate(self): + royalties_output_name = 'Royalties' + + for royalty_rate in [0, 0.1]: + result = GeophiresXClient().get_geophires_result( + ImmutableGeophiresInputParameters( + from_file_path=self._get_test_file_path( + 'geophires_x_tests/generic-egs-case-2_sam-single-owner-ppa.txt' + ), + params={'Royalty Rate': royalty_rate}, + ) + ) + opex_result = result.result['OPERATING AND MAINTENANCE COSTS (M$/yr)'] + if royalty_rate > 0.0: + self.assertIsNotNone(opex_result[royalties_output_name]) + self.assertEqual(58.88, opex_result[royalties_output_name]['value']) + self.assertEqual('MUSD/yr', opex_result[royalties_output_name]['unit']) + # FIXME WIP assert total opex includes royalties + else: + self.assertIsNone(opex_result[royalties_output_name]) From 11c33142dc613826876902c249f25233ccd096a9 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:06:08 -0700 Subject: [PATCH 07/32] Regenerate schema with Royalties output param --- src/geophires_x/EconomicsUtils.py | 2 +- src/geophires_x_schema_generator/geophires-result.json | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/geophires_x/EconomicsUtils.py b/src/geophires_x/EconomicsUtils.py index fed0bb2e1..8cc6ab2b2 100644 --- a/src/geophires_x/EconomicsUtils.py +++ b/src/geophires_x/EconomicsUtils.py @@ -152,5 +152,5 @@ def royalties_opex_parameter_output_parameter() -> OutputParameter: UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, - ToolTipText='Average annual royalties' # TODO WIP clarify + ToolTipText='Average annual royalties paid (operating expense)' ) diff --git a/src/geophires_x_schema_generator/geophires-result.json b/src/geophires_x_schema_generator/geophires-result.json index 4edc6f710..d07189a9b 100644 --- a/src/geophires_x_schema_generator/geophires-result.json +++ b/src/geophires_x_schema_generator/geophires-result.json @@ -480,6 +480,11 @@ "units": "MUSD/yr" }, "Average annual pumping costs": {}, + "Royalties": { + "type": "number", + "description": "Average annual royalties paid (operating expense)", + "units": "MUSD/yr" + }, "Average annual auxiliary fuel cost": {}, "Average annual pumping cost": {}, "Redrilling costs": { From 8af6e17e2945c265b81bd4d8fcad91bceafb0258 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:10:58 -0700 Subject: [PATCH 08/32] Assert that opex line items, including royalties, sum up to total --- src/geophires_x/Economics.py | 2 -- tests/test_geophires_x.py | 14 +++++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index f82e31195..a5ebc3d6a 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -2507,8 +2507,6 @@ def Calculate(self, model: Model) -> None: self.CCap.value = (self.sam_economics_calculations.capex.quantity() .to(self.CCap.CurrentUnits.value).magnitude) - # FIXME WIP adjust OPEX for royalties - # FIXME WIP unit conversion average_annual_royalties = np.average(self.sam_economics_calculations.royalties_opex[1:]) # ignore Year 0 if average_annual_royalties > 0: self.royalties_opex.value = average_annual_royalties diff --git a/tests/test_geophires_x.py b/tests/test_geophires_x.py index 08ae56f8f..7b81b9b89 100644 --- a/tests/test_geophires_x.py +++ b/tests/test_geophires_x.py @@ -1312,6 +1312,18 @@ def test_royalty_rate(self): self.assertIsNotNone(opex_result[royalties_output_name]) self.assertEqual(58.88, opex_result[royalties_output_name]['value']) self.assertEqual('MUSD/yr', opex_result[royalties_output_name]['unit']) - # FIXME WIP assert total opex includes royalties + + total_opex_MUSD = opex_result['Total operating and maintenance costs']['value'] + + opex_line_item_sum = 0 + for line_item_names in [ + 'Wellfield maintenance costs', + 'Power plant maintenance costs', + 'Water costs', + royalties_output_name, + ]: + opex_line_item_sum += opex_result[line_item_names]['value'] + + self.assertEqual(opex_line_item_sum, total_opex_MUSD) else: self.assertIsNone(opex_result[royalties_output_name]) From a999d092e9da1337f3e54294fb4a78375990e417 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:22:47 -0700 Subject: [PATCH 09/32] =?UTF-8?q?Bump=20version:=203.9.54=20=E2=86=92=203.?= =?UTF-8?q?9.55?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- .cookiecutterrc | 2 +- README.rst | 4 ++-- docs/conf.py | 2 +- setup.py | 2 +- src/geophires_x/__init__.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 5e89dded1..61879902c 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.9.54 +current_version = 3.9.55 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index 20b327d68..77d07fef2 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -54,7 +54,7 @@ default_context: sphinx_doctest: "no" sphinx_theme: "sphinx-py3doc-enhanced-theme" test_matrix_separate_coverage: "no" - version: 3.9.54 + version: 3.9.55 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" diff --git a/README.rst b/README.rst index 2fbc926f5..1cebc729f 100644 --- a/README.rst +++ b/README.rst @@ -58,9 +58,9 @@ Free software: `MIT 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.9.54.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.9.55.svg :alt: Commits since latest release - :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.54...main + :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.55...main .. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat :target: https://nrel.github.io/GEOPHIRES-X diff --git a/docs/conf.py b/docs/conf.py index 091ee8507..149dfcdf1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ year = '2025' author = 'NREL' copyright = f'{year}, {author}' -version = release = '3.9.54' +version = release = '3.9.55' pygments_style = 'trac' templates_path = ['./templates'] diff --git a/setup.py b/setup.py index 399416965..1d45721df 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='geophires-x', - version='3.9.54', + version='3.9.55', license='MIT', description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.', long_description='{}\n{}'.format( diff --git a/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index ba7552b02..c2fa4fff8 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.9.54' +__version__ = '3.9.55' From 5c989e9ab1b96a6d8b86c3e25a0a7838eb6cfd93 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:33:19 -0700 Subject: [PATCH 10/32] update unit test --- tests/geophires_x_tests/test_economics_sam.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/geophires_x_tests/test_economics_sam.py b/tests/geophires_x_tests/test_economics_sam.py index 6cb025aeb..ee4037cd9 100644 --- a/tests/geophires_x_tests/test_economics_sam.py +++ b/tests/geophires_x_tests/test_economics_sam.py @@ -651,13 +651,14 @@ def test_royalty_rate(self): def get_row(name: str): return EconomicsSamTestCase._get_cash_flow_row(cash_flow, name) - ppa_revenue_row = get_row('PPA revenue ($)') - expected_royalties = [x * royalty_rate for x in ppa_revenue_row] + ppa_revenue_row_USD = get_row('PPA revenue ($)') + expected_royalties_USD = [x * royalty_rate for x in ppa_revenue_row_USD] + expected_royalties_MUSD = [x * 1e-6 for x in expected_royalties_USD] - self.assertListEqual(expected_royalties, sam_econ.royalties_opex) + self.assertListEqual(expected_royalties_MUSD, sam_econ.royalties_opex) om_prod_based_expense_row = get_row('O&M production-based expense ($)') - self.assertListAlmostEqual(expected_royalties, om_prod_based_expense_row, places=0) + self.assertListAlmostEqual(expected_royalties_USD, om_prod_based_expense_row, places=0) # Note the above assertion assumes royalties are the only production-based O&M expenses. If this changes, # the assertion will need to be updated. From 274ca28604a37109b455b49eda95b4f821f8ab2e Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:38:26 -0700 Subject: [PATCH 11/32] Change output param name to 'Average Annual Royalty Cost' --- src/geophires_x/EconomicsUtils.py | 5 +++-- src/geophires_x_client/geophires_x_result.py | 2 +- src/geophires_x_schema_generator/geophires-result.json | 4 ++-- tests/test_geophires_x.py | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/geophires_x/EconomicsUtils.py b/src/geophires_x/EconomicsUtils.py index 8cc6ab2b2..3f7b271c3 100644 --- a/src/geophires_x/EconomicsUtils.py +++ b/src/geophires_x/EconomicsUtils.py @@ -148,9 +148,10 @@ def total_capex_parameter_output_parameter() -> OutputParameter: def royalties_opex_parameter_output_parameter() -> OutputParameter: return OutputParameter( - Name='Royalties', + Name='Average Annual Royalty Cost', UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, - ToolTipText='Average annual royalties paid (operating expense)' + 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.' ) diff --git a/src/geophires_x_client/geophires_x_result.py b/src/geophires_x_client/geophires_x_result.py index a8049608c..04a0b7325 100644 --- a/src/geophires_x_client/geophires_x_result.py +++ b/src/geophires_x_client/geophires_x_result.py @@ -290,7 +290,7 @@ class GeophiresXResult: 'Annual District Heating O&M Cost', 'Average Annual Peaking Fuel Cost', 'Average annual pumping costs', - 'Royalties', + 'Average Annual Royalty Cost', # SUTRA 'Average annual auxiliary fuel cost', 'Average annual pumping cost', diff --git a/src/geophires_x_schema_generator/geophires-result.json b/src/geophires_x_schema_generator/geophires-result.json index d07189a9b..c87095087 100644 --- a/src/geophires_x_schema_generator/geophires-result.json +++ b/src/geophires_x_schema_generator/geophires-result.json @@ -480,9 +480,9 @@ "units": "MUSD/yr" }, "Average annual pumping costs": {}, - "Royalties": { + "Average Annual Royalty Cost": { "type": "number", - "description": "Average annual royalties paid (operating expense)", + "description": "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.", "units": "MUSD/yr" }, "Average annual auxiliary fuel cost": {}, diff --git a/tests/test_geophires_x.py b/tests/test_geophires_x.py index 7b81b9b89..6231f515c 100644 --- a/tests/test_geophires_x.py +++ b/tests/test_geophires_x.py @@ -1296,7 +1296,7 @@ def test_redrilling_costs(self): self.assertAlmostEqual(expected_annual_redrilling_cost, result_opex['Redrilling costs']['value'], places=2) def test_royalty_rate(self): - royalties_output_name = 'Royalties' + royalties_output_name = 'Average Annual Royalty Cost' for royalty_rate in [0, 0.1]: result = GeophiresXClient().get_geophires_result( From ef7ac094d052b0cb8b899917d3d493cd42560b0a Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:52:19 -0700 Subject: [PATCH 12/32] Fix incorrect method of setting royalties output param value --- src/geophires_x/Economics.py | 4 +++- src/geophires_x/EconomicsSam.py | 2 +- tests/geophires_x_tests/test_economics_sam.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index a5ebc3d6a..bd8176ebe 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -2507,7 +2507,9 @@ def Calculate(self, model: Model) -> None: self.CCap.value = (self.sam_economics_calculations.capex.quantity() .to(self.CCap.CurrentUnits.value).magnitude) - average_annual_royalties = np.average(self.sam_economics_calculations.royalties_opex[1:]) # ignore Year 0 + average_annual_royalties = np.average( + self.sam_economics_calculations.royalties_opex.value[1:] # ignore pre-revenue year(s) (Year 0) + ) if average_annual_royalties > 0: self.royalties_opex.value = average_annual_royalties self.Coam.value += self.royalties_opex.quantity().to(self.Coam.CurrentUnits.value).magnitude diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index b19311e5f..b4a59bef7 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -176,7 +176,7 @@ def sf(_v: float, num_sig_figs: int = 5) -> float: royalty_rate = model.economics.royalty_rate.quantity().to('dimensionless').magnitude ppa_revenue_row = _cash_flow_profile_row(cash_flow, 'PPA revenue ($)') royalties_unit = sam_economics.royalties_opex.CurrentUnits.value.replace('/yr', '') - sam_economics.royalties_opex = [ + sam_economics.royalties_opex.value = [ quantity(x * royalty_rate, 'USD').to(royalties_unit).magnitude for x in ppa_revenue_row ] diff --git a/tests/geophires_x_tests/test_economics_sam.py b/tests/geophires_x_tests/test_economics_sam.py index ee4037cd9..e7eb7f4cd 100644 --- a/tests/geophires_x_tests/test_economics_sam.py +++ b/tests/geophires_x_tests/test_economics_sam.py @@ -655,7 +655,7 @@ def get_row(name: str): expected_royalties_USD = [x * royalty_rate for x in ppa_revenue_row_USD] expected_royalties_MUSD = [x * 1e-6 for x in expected_royalties_USD] - self.assertListEqual(expected_royalties_MUSD, sam_econ.royalties_opex) + self.assertListEqual(expected_royalties_MUSD, sam_econ.royalties_opex.value) om_prod_based_expense_row = get_row('O&M production-based expense ($)') self.assertListAlmostEqual(expected_royalties_USD, om_prod_based_expense_row, places=0) From 97203deff118a82d1545d6613877bfdd96433bbe Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:57:08 -0700 Subject: [PATCH 13/32] Internally distinguish between SAM econ royalty cost time series and main econ average annual value --- src/geophires_x/Economics.py | 16 ++++++++++++---- src/geophires_x/EconomicsSam.py | 4 ++-- src/geophires_x/EconomicsUtils.py | 6 +++--- src/geophires_x/Outputs.py | 4 ++-- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index bd8176ebe..11e935ed1 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -13,7 +13,7 @@ from geophires_x.EconomicsUtils import BuildPricingModel, wacc_output_parameter, nominal_discount_rate_parameter, \ real_discount_rate_parameter, after_tax_irr_parameter, moic_parameter, project_vir_parameter, \ project_payback_period_parameter, inflation_cost_during_construction_output_parameter, \ - total_capex_parameter_output_parameter, royalties_opex_parameter_output_parameter + total_capex_parameter_output_parameter from geophires_x.GeoPHIRESUtils import quantity from geophires_x.OptionList import Configuration, WellDrillingCostCorrelation, EconomicModel, EndUseOptions, PlantType, \ _WellDrillingCostCorrelationCitation @@ -1906,7 +1906,15 @@ def __init__(self, model: Model): PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR ) - self.royalties_opex = self.OutputParameterDict[self.royalties_opex.Name] = royalties_opex_parameter_output_parameter() + self.royalties_average_annual_cost = self.OutputParameterDict[self.royalties_average_annual_cost.Name] = OutputParameter( + Name='Average Annual Royalty Cost', + UnitType=Units.CURRENCYFREQUENCY, + PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, + 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.' + ) + # district heating self.peakingboilercost = self.OutputParameterDict[self.peakingboilercost.Name] = OutputParameter( @@ -2511,8 +2519,8 @@ def Calculate(self, model: Model) -> None: self.sam_economics_calculations.royalties_opex.value[1:] # ignore pre-revenue year(s) (Year 0) ) if average_annual_royalties > 0: - self.royalties_opex.value = average_annual_royalties - self.Coam.value += self.royalties_opex.quantity().to(self.Coam.CurrentUnits.value).magnitude + self.royalties_average_annual_cost.value = average_annual_royalties + self.Coam.value += self.royalties_average_annual_cost.quantity().to(self.Coam.CurrentUnits.value).magnitude self.wacc.value = self.sam_economics_calculations.wacc.value self.nominal_discount_rate.value = self.sam_economics_calculations.nominal_discount_rate.value diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index b4a59bef7..22091103b 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -39,7 +39,7 @@ project_payback_period_parameter, inflation_cost_during_construction_output_parameter, total_capex_parameter_output_parameter, - royalties_opex_parameter_output_parameter, + royalty_cost_output_parameter, ) from geophires_x.GeoPHIRESUtils import is_float, is_int, sig_figs, quantity from geophires_x.OptionList import EconomicModel, EndUseOptions @@ -60,7 +60,7 @@ class SamEconomicsCalculations: capex: OutputParameter = field(default_factory=total_capex_parameter_output_parameter) - royalties_opex: OutputParameter = field(default_factory=royalties_opex_parameter_output_parameter) + royalties_opex: OutputParameter = field(default_factory=royalty_cost_output_parameter) project_npv: OutputParameter = field( default_factory=lambda: OutputParameter( diff --git a/src/geophires_x/EconomicsUtils.py b/src/geophires_x/EconomicsUtils.py index 3f7b271c3..782383ecd 100644 --- a/src/geophires_x/EconomicsUtils.py +++ b/src/geophires_x/EconomicsUtils.py @@ -146,12 +146,12 @@ def total_capex_parameter_output_parameter() -> OutputParameter: ) -def royalties_opex_parameter_output_parameter() -> OutputParameter: +def royalty_cost_output_parameter() -> OutputParameter: return OutputParameter( - Name='Average Annual Royalty Cost', + Name='Royalty Cost', UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, - ToolTipText='The average annual cost paid to a royalty holder, calculated as a percentage of the ' + ToolTipText='The annual costs paid to a royalty holder, calculated as a percentage of the ' 'project\'s gross annual revenue. This is modeled as a variable operating expense.' ) diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index b6a943b49..5fa1e2c3d 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -556,8 +556,8 @@ def PrintOutputs(self, model: Model): f.write(f' {aoc_label}{model.addeconomics.AddOnOPEXTotalPerYear.value:10.2f} {model.addeconomics.AddOnOPEXTotalPerYear.CurrentUnits.value}\n') if econ.royalty_rate.value > 0.0: - royalties_label = Outputs._field_label(econ.royalties_opex.display_name, 47) - f.write(f' {royalties_label}{econ.royalties_opex.value:10.2f} {econ.royalties_opex.CurrentUnits.value}\n') + royalties_label = Outputs._field_label(econ.royalties_average_annual_cost.display_name, 47) + f.write(f' {royalties_label}{econ.royalties_average_annual_cost.value:10.2f} {econ.royalties_average_annual_cost.CurrentUnits.value}\n') f.write(f' {econ.Coam.display_name}: {(econ.Coam.value + econ.averageannualpumpingcosts.value + econ.averageannualheatpumpelectricitycost.value):10.2f} {econ.Coam.CurrentUnits.value}\n') else: From a00238114e2d1d0491f3f0bc99b37e2ea65ba795 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 15 Sep 2025 12:26:01 -0700 Subject: [PATCH 14/32] WIP - calculate Royalty Holder NPV --- src/geophires_x/Economics.py | 118 ++++++++++++++----- src/geophires_x/EconomicsSam.py | 2 +- src/geophires_x/Outputs.py | 7 +- src/geophires_x_client/geophires_x_result.py | 1 + tests/test_geophires_x.py | 41 ++++--- 5 files changed, 119 insertions(+), 50 deletions(-) diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index 11e935ed1..4f8e16ff2 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -968,7 +968,7 @@ def __init__(self, model: Model): ) self.royalty_rate = self.ParameterDict[self.royalty_rate.Name] = floatParameter( - "Royalty Rate", + 'Royalty Rate', DefaultValue=0., Min=0.0, Max=1.0, @@ -978,6 +978,18 @@ def __init__(self, model: Model): ToolTipText="Royalty rate used in SAM Economic Models." # FIXME WIP TODO documentation ) + self.royalty_holder_discount_rate = self.ParameterDict[self.royalty_holder_discount_rate.Name] = floatParameter( + 'Royalty Holder Discount Rate', + DefaultValue=0.05, + Min=0.0, + Max=1.0, + UnitType=Units.PERCENT, + PreferredUnits=PercentUnit.TENTH, + CurrentUnits=PercentUnit.TENTH, + ToolTipText="Royalty holder discount rate used in SAM Economic Models." # FIXME WIP TODO documentation + ) + + self.discount_initial_year_cashflow = self.ParameterDict[self.discount_initial_year_cashflow.Name] = boolParameter( 'Discount Initial Year Cashflow', DefaultValue=False, @@ -2134,6 +2146,33 @@ def __init__(self, model: Model): UnitType=Units.NONE, ) + # Results for the Royalty Holder + self.royalty_holder_npv = self.OutputParameterDict[self.royalty_holder_npv.Name] = OutputParameter( + 'Royalty Holder NPV', + UnitType=Units.CURRENCY, + PreferredUnits=CurrencyUnit.MDOLLARS, + CurrentUnits=CurrencyUnit.MDOLLARS, + ToolTipText="Net Present Value (NPV) of the royalty holder's cash flow stream." + ) + self.royalty_holder_annual_revenue = self.OutputParameterDict[ + self.royalty_holder_annual_revenue.Name + ] = OutputParameter( + 'Royalty Holder Annual Revenue', + UnitType=Units.CURRENCYFREQUENCY, + PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, + CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, + ToolTipText="The royalty holder's annual revenue stream from the royalty agreement." + ) + self.royalty_holder_total_revenue = self.OutputParameterDict[ + self.royalty_holder_total_revenue.Name + ] = OutputParameter( + 'Royalty Holder Total Revenue', + UnitType=Units.CURRENCY, + PreferredUnits=CurrencyUnit.MDOLLARS, + CurrentUnits=CurrencyUnit.MDOLLARS, + ToolTipText='The total (undiscounted) revenue received by the royalty holder over the project lifetime.' + ) + model.logger.info(f'Complete {__class__!s}: {sys._getframe().f_code.co_name}') def read_parameters(self, model: Model) -> None: @@ -2504,38 +2543,8 @@ def Calculate(self, model: Model) -> None: self.discount_initial_year_cashflow.value ) - non_calculated_output_placeholder_val = -1 if self.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA: - self.sam_economics_calculations = calculate_sam_economics(model) - - # Setting capex_total distinguishes capex from CCap's display name of 'Total capital costs', - # 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.CCap.value = (self.sam_economics_calculations.capex.quantity() - .to(self.CCap.CurrentUnits.value).magnitude) - - average_annual_royalties = np.average( - self.sam_economics_calculations.royalties_opex.value[1:] # ignore pre-revenue year(s) (Year 0) - ) - if average_annual_royalties > 0: - self.royalties_average_annual_cost.value = average_annual_royalties - self.Coam.value += self.royalties_average_annual_cost.quantity().to(self.Coam.CurrentUnits.value).magnitude - - self.wacc.value = self.sam_economics_calculations.wacc.value - self.nominal_discount_rate.value = self.sam_economics_calculations.nominal_discount_rate.value - self.ProjectNPV.value = self.sam_economics_calculations.project_npv.quantity().to( - convertible_unit(self.ProjectNPV.CurrentUnits)).magnitude - - self.ProjectIRR.value = non_calculated_output_placeholder_val # SAM calculates After-Tax IRR instead - self.after_tax_irr.value = self.sam_economics_calculations.after_tax_irr.quantity().to( - convertible_unit(self.ProjectIRR.CurrentUnits)).magnitude - - self.ProjectMOIC.value = self.sam_economics_calculations.moic.value - self.ProjectVIR.value = self.sam_economics_calculations.project_vir.value - - # TODO remove or clarify project payback period: https://github.com/NREL/GEOPHIRES-X/issues/413 - self.ProjectPaybackPeriod.value = self.sam_economics_calculations.project_payback_period.value + self._calculate_sam_economics(model) # Calculate the project payback period if self.econmodel.value != EconomicModel.SAM_SINGLE_OWNER_PPA: @@ -3267,6 +3276,51 @@ def calculate_cashflow(self, model: Model) -> None: for i in range(1, model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value, 1): self.TotalCummRevenue.value[i] = self.TotalCummRevenue.value[i-1] + self.TotalRevenue.value[i] + def _calculate_sam_economics(self, model: Model) -> None: + non_calculated_output_placeholder_val = -1 + self.sam_economics_calculations = calculate_sam_economics(model) + + # Setting capex_total distinguishes capex from CCap's display name of 'Total capital costs', + # 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.CCap.value = (self.sam_economics_calculations.capex.quantity() + .to(self.CCap.CurrentUnits.value).magnitude) + + + if self.royalty_rate.Provided: + average_annual_royalties = np.average( + self.sam_economics_calculations.royalties_opex.value[1:] # ignore pre-revenue year(s) (Year 0) + ) + + self.royalties_average_annual_cost.value = average_annual_royalties + self.Coam.value += self.royalties_average_annual_cost.quantity().to(self.Coam.CurrentUnits.value).magnitude + + self.royalty_holder_npv.value = calculate_npv( + self.royalty_holder_discount_rate.value, + self.sam_economics_calculations.royalties_opex.value, + self.discount_initial_year_cashflow.value + ) + # FIXME WIP + # self.royalty_holder_annual_revenue + # self.royalty_holder_total_revenue + + + self.wacc.value = self.sam_economics_calculations.wacc.value + self.nominal_discount_rate.value = self.sam_economics_calculations.nominal_discount_rate.value + self.ProjectNPV.value = self.sam_economics_calculations.project_npv.quantity().to( + convertible_unit(self.ProjectNPV.CurrentUnits)).magnitude + + self.ProjectIRR.value = non_calculated_output_placeholder_val # SAM calculates After-Tax IRR instead + self.after_tax_irr.value = self.sam_economics_calculations.after_tax_irr.quantity().to( + convertible_unit(self.ProjectIRR.CurrentUnits)).magnitude + + self.ProjectMOIC.value = self.sam_economics_calculations.moic.value + self.ProjectVIR.value = self.sam_economics_calculations.project_vir.value + + # TODO remove or clarify project payback period: https://github.com/NREL/GEOPHIRES-X/issues/413 + self.ProjectPaybackPeriod.value = self.sam_economics_calculations.project_payback_period.value + # noinspection SpellCheckingInspection def _calculate_derived_outputs(self, model: Model) -> None: """ diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index 22091103b..3ff97dabe 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -417,7 +417,7 @@ def _get_single_owner_parameters(model: Model) -> dict[str, Any]: ) ret['ppa_price_input'] = ppa_price_schedule_per_kWh - if hasattr(econ, 'royalty_rate') and econ.royalty_rate.value > 0.0: + if hasattr(econ, 'royalty_rate') and econ.royalty_rate.Provided: royalty_rate_fraction = econ.royalty_rate.quantity().to(convertible_unit('dimensionless')).magnitude # For each year, calculate the royalty as a $/MWh variable cost. diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index 5fa1e2c3d..a0c651961 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -324,6 +324,11 @@ def PrintOutputs(self, model: Model): if model.surfaceplant.enduse_option.value in [EndUseOptions.ELECTRICITY]: f.write(f' Estimated Jobs Created: {model.economics.jobs_created.value}\n') + if econ.royalty_rate.Provided: + royalty_holder_npv_label = Outputs._field_label(econ.royalty_holder_npv.display_name, 49) + f.write( + f' {royalty_holder_npv_label}{econ.royalty_holder_npv.value:10.2f} {econ.royalty_holder_npv.CurrentUnits.value}\n') + f.write(NL) f.write(' ***ENGINEERING PARAMETERS***\n') @@ -555,7 +560,7 @@ def PrintOutputs(self, model: Model): aoc_label = Outputs._field_label(model.addeconomics.AddOnOPEXTotalPerYear.display_name, 47) f.write(f' {aoc_label}{model.addeconomics.AddOnOPEXTotalPerYear.value:10.2f} {model.addeconomics.AddOnOPEXTotalPerYear.CurrentUnits.value}\n') - if econ.royalty_rate.value > 0.0: + if econ.royalty_rate.Provided: royalties_label = Outputs._field_label(econ.royalties_average_annual_cost.display_name, 47) f.write(f' {royalties_label}{econ.royalties_average_annual_cost.value:10.2f} {econ.royalties_average_annual_cost.CurrentUnits.value}\n') diff --git a/src/geophires_x_client/geophires_x_result.py b/src/geophires_x_client/geophires_x_result.py index 04a0b7325..55aa0540e 100644 --- a/src/geophires_x_client/geophires_x_result.py +++ b/src/geophires_x_client/geophires_x_result.py @@ -93,6 +93,7 @@ class GeophiresXResult: 'Project Payback Period', 'CHP: Percent cost allocation for electrical plant', 'Estimated Jobs Created', + 'Royalty Holder NPV', ], 'EXTENDED ECONOMICS': [ 'Adjusted Project LCOE (after incentives, grants, AddOns,etc)', diff --git a/tests/test_geophires_x.py b/tests/test_geophires_x.py index 6231f515c..ec6b6b6b1 100644 --- a/tests/test_geophires_x.py +++ b/tests/test_geophires_x.py @@ -1025,7 +1025,7 @@ def _get_result( 'Number of Injection Wells': doublets, # offset contingency - 'Reservoir Stimulation Capital Cost Adjustment Factor': 1/default_contingency_factor, + 'Reservoir Stimulation Capital Cost Adjustment Factor': 1 / default_contingency_factor, } ) # fmt:on @@ -1285,6 +1285,7 @@ def test_redrilling_costs(self): capex_field_suffix = ( '' if result_capex.get('Drilling and completion costs') is not None else ' (for redrilling)' ) + # @formatter:off expected_annual_redrilling_cost = ( ( result_capex[f'Drilling and completion costs{capex_field_suffix}']['value'] @@ -1292,6 +1293,7 @@ def test_redrilling_costs(self): ) * result_redrills ) / result.result['ECONOMIC PARAMETERS']['Project lifetime']['value'] + # @formatter:on self.assertAlmostEqual(expected_annual_redrilling_cost, result_opex['Redrilling costs']['value'], places=2) @@ -1308,22 +1310,29 @@ def test_royalty_rate(self): ) ) opex_result = result.result['OPERATING AND MAINTENANCE COSTS (M$/yr)'] - if royalty_rate > 0.0: - self.assertIsNotNone(opex_result[royalties_output_name]) - self.assertEqual(58.88, opex_result[royalties_output_name]['value']) - self.assertEqual('MUSD/yr', opex_result[royalties_output_name]['unit']) - total_opex_MUSD = opex_result['Total operating and maintenance costs']['value'] + self.assertIsNotNone(opex_result[royalties_output_name]) + self.assertEqual('MUSD/yr', opex_result[royalties_output_name]['unit']) - opex_line_item_sum = 0 - for line_item_names in [ - 'Wellfield maintenance costs', - 'Power plant maintenance costs', - 'Water costs', - royalties_output_name, - ]: - opex_line_item_sum += opex_result[line_item_names]['value'] + total_opex_MUSD = opex_result['Total operating and maintenance costs']['value'] - self.assertEqual(opex_line_item_sum, total_opex_MUSD) + opex_line_item_sum = 0 + for line_item_names in [ + 'Wellfield maintenance costs', + 'Power plant maintenance costs', + 'Water costs', + royalties_output_name, + ]: + opex_line_item_sum += opex_result[line_item_names]['value'] + + self.assertEqual(opex_line_item_sum, total_opex_MUSD) + + econ_result = result.result['ECONOMIC PARAMETERS'] + royalty_holder_npv_MUSD = econ_result['Royalty Holder NPV']['value'] + + if royalty_rate > 0.0: + self.assertEqual(58.88, opex_result[royalties_output_name]['value']) + self.assertGreater(royalty_holder_npv_MUSD, 0) # FIXME WIP else: - self.assertIsNone(opex_result[royalties_output_name]) + self.assertEqual(0, opex_result[royalties_output_name]['value']) + self.assertEqual(0, royalty_holder_npv_MUSD) From 092983579cd2959d3485d95c9a34acbcc1021564 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 15 Sep 2025 12:53:07 -0700 Subject: [PATCH 15/32] assert royalty holder NPV --- tests/test_geophires_x.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/tests/test_geophires_x.py b/tests/test_geophires_x.py index ec6b6b6b1..3de33c604 100644 --- a/tests/test_geophires_x.py +++ b/tests/test_geophires_x.py @@ -7,10 +7,16 @@ from pathlib import Path from typing import Any +import numpy as np + +# noinspection PyProtectedMember +from geophires_x.EconomicsSam import _cash_flow_profile_row from geophires_x.OptionList import PlantType from geophires_x.OptionList import WellDrillingCostCorrelation from geophires_x_client import GeophiresXClient from geophires_x_client import GeophiresXResult + +# noinspection PyProtectedMember from geophires_x_client import _get_logger from geophires_x_client.geophires_input_parameters import EndUseOption from geophires_x_client.geophires_input_parameters import GeophiresInputParameters @@ -1306,7 +1312,9 @@ def test_royalty_rate(self): from_file_path=self._get_test_file_path( 'geophires_x_tests/generic-egs-case-2_sam-single-owner-ppa.txt' ), - params={'Royalty Rate': royalty_rate}, + params={ + 'Royalty Rate': royalty_rate, + }, ) ) opex_result = result.result['OPERATING AND MAINTENANCE COSTS (M$/yr)'] @@ -1332,7 +1340,22 @@ def test_royalty_rate(self): if royalty_rate > 0.0: self.assertEqual(58.88, opex_result[royalties_output_name]['value']) - self.assertGreater(royalty_holder_npv_MUSD, 0) # FIXME WIP - else: + self.assertGreater(royalty_holder_npv_MUSD, 0) + + royalties_cash_flow_MUSD = [ + it * 1e-6 + for it in _cash_flow_profile_row( + result.result['SAM CASH FLOW PROFILE'], 'O&M production-based expense ($)' + ) + ] + + self.assertAlmostEqual( + np.average(royalties_cash_flow_MUSD[1:]), opex_result[royalties_output_name]['value'], places=1 + ) + + if royalty_rate == 0.1: + self.assertAlmostEqual(708.07, royalty_holder_npv_MUSD, places=2) + + if royalty_rate == 0.0: self.assertEqual(0, opex_result[royalties_output_name]['value']) self.assertEqual(0, royalty_holder_npv_MUSD) From 4adc2a125ee0ea9b92fc9c0bf067ff3ca7adec2a Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 15 Sep 2025 13:05:04 -0700 Subject: [PATCH 16/32] finish impl of royalty holder outputs --- src/geophires_x/Economics.py | 11 ++++++----- src/geophires_x/Outputs.py | 13 ++++++++++--- src/geophires_x_client/geophires_x_result.py | 2 ++ .../geophires-request.json | 9 +++++++++ .../geophires-result.json | 15 +++++++++++++++ tests/test_geophires_x.py | 3 +++ 6 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index 4f8e16ff2..ac5e13ef2 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -2157,7 +2157,7 @@ def __init__(self, model: Model): self.royalty_holder_annual_revenue = self.OutputParameterDict[ self.royalty_holder_annual_revenue.Name ] = OutputParameter( - 'Royalty Holder Annual Revenue', + 'Royalty Holder Average Annual Revenue', UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, @@ -3289,7 +3289,7 @@ def _calculate_sam_economics(self, model: Model) -> None: if self.royalty_rate.Provided: - average_annual_royalties = np.average( + average_annual_royalties = np.average( # TODO unit conversion self.sam_economics_calculations.royalties_opex.value[1:] # ignore pre-revenue year(s) (Year 0) ) @@ -3301,9 +3301,10 @@ def _calculate_sam_economics(self, model: Model) -> None: self.sam_economics_calculations.royalties_opex.value, self.discount_initial_year_cashflow.value ) - # FIXME WIP - # self.royalty_holder_annual_revenue - # self.royalty_holder_total_revenue + self.royalty_holder_annual_revenue.value = self.royalties_average_annual_cost.value + self.royalty_holder_total_revenue.value = np.sum( # TODO unit conversion + self.sam_economics_calculations.royalties_opex.value[1:] # ignore pre-revenue year(s) (Year 0) + ) self.wacc.value = self.sam_economics_calculations.wacc.value diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index a0c651961..b62d62d4a 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -325,9 +325,16 @@ def PrintOutputs(self, model: Model): f.write(f' Estimated Jobs Created: {model.economics.jobs_created.value}\n') if econ.royalty_rate.Provided: - royalty_holder_npv_label = Outputs._field_label(econ.royalty_holder_npv.display_name, 49) - f.write( - f' {royalty_holder_npv_label}{econ.royalty_holder_npv.value:10.2f} {econ.royalty_holder_npv.CurrentUnits.value}\n') + for royalty_output in [ + econ.royalty_holder_npv, + econ.royalty_holder_annual_revenue, + econ.royalty_holder_total_revenue + ]: + label = Outputs._field_label(royalty_output.display_name, 49) + f.write( + f' {label}{royalty_output.value:10.2f} {royalty_output.CurrentUnits.value}\n') + + f.write(NL) diff --git a/src/geophires_x_client/geophires_x_result.py b/src/geophires_x_client/geophires_x_result.py index 55aa0540e..cd90b777d 100644 --- a/src/geophires_x_client/geophires_x_result.py +++ b/src/geophires_x_client/geophires_x_result.py @@ -94,6 +94,8 @@ class GeophiresXResult: 'CHP: Percent cost allocation for electrical plant', 'Estimated Jobs Created', 'Royalty Holder NPV', + 'Royalty Holder Average Annual Revenue', + 'Royalty Holder Total Revenue', ], 'EXTENDED ECONOMICS': [ 'Adjusted Project LCOE (after incentives, grants, AddOns,etc)', diff --git a/src/geophires_x_schema_generator/geophires-request.json b/src/geophires_x_schema_generator/geophires-request.json index bfdd138dc..46ce9c37f 100644 --- a/src/geophires_x_schema_generator/geophires-request.json +++ b/src/geophires_x_schema_generator/geophires-request.json @@ -1647,6 +1647,15 @@ "minimum": 0.0, "maximum": 1.0 }, + "Royalty Holder Discount Rate": { + "description": "Royalty holder discount rate used in SAM Economic Models.", + "type": "number", + "units": "", + "category": "Economics", + "default": 0.05, + "minimum": 0.0, + "maximum": 1.0 + }, "Discount Initial Year Cashflow": { "description": "Whether to discount cashflow in the initial project year when calculating NPV (Net Present Value). The default value of False conforms to NREL's standard convention for NPV calculation (Short W et al, 1995. https://www.nrel.gov/docs/legosti/old/5173.pdf). A value of True will, by contrast, cause NPV calculation to follow the convention used by Excel, Google Sheets, and other common spreadsheet software. Although NREL's NPV convention may typically be considered more technically correct, Excel-style NPV calculation might be preferred for familiarity or compatibility with existing business processes. See https://github.com/NREL/GEOPHIRES-X/discussions/344 for further details.", "type": "boolean", diff --git a/src/geophires_x_schema_generator/geophires-result.json b/src/geophires_x_schema_generator/geophires-result.json index c87095087..73a906548 100644 --- a/src/geophires_x_schema_generator/geophires-result.json +++ b/src/geophires_x_schema_generator/geophires-result.json @@ -143,6 +143,21 @@ "type": "number", "description": "", "units": null + }, + "Royalty Holder NPV": { + "type": "number", + "description": "Net Present Value (NPV) of the royalty holder's cash flow stream.", + "units": "MUSD" + }, + "Royalty Holder Average Annual Revenue": { + "type": "number", + "description": "The royalty holder's annual revenue stream from the royalty agreement.", + "units": "MUSD/yr" + }, + "Royalty Holder Total Revenue": { + "type": "number", + "description": "The total (undiscounted) revenue received by the royalty holder over the project lifetime.", + "units": "MUSD" } } }, diff --git a/tests/test_geophires_x.py b/tests/test_geophires_x.py index 3de33c604..77c2a55b0 100644 --- a/tests/test_geophires_x.py +++ b/tests/test_geophires_x.py @@ -9,6 +9,7 @@ import numpy as np +# @formatter:off # noinspection PyProtectedMember from geophires_x.EconomicsSam import _cash_flow_profile_row from geophires_x.OptionList import PlantType @@ -22,6 +23,8 @@ from geophires_x_client.geophires_input_parameters import GeophiresInputParameters from geophires_x_client.geophires_input_parameters import ImmutableGeophiresInputParameters from geophires_x_tests.test_options_list import WellDrillingCostCorrelationTestCase + +# @formatter:on from tests.base_test_case import BaseTestCase From 7dd1dbeb793a4447d697dba696aa582f45a82da0 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 15 Sep 2025 13:35:08 -0700 Subject: [PATCH 17/32] Update documentation. Throw exception if royalty rate provided for non-SAM-EM inputs --- src/geophires_x/Economics.py | 13 +++++++++++-- .../geophires-request.json | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index ac5e13ef2..ba0da28d9 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -975,7 +975,9 @@ def __init__(self, model: Model): UnitType=Units.PERCENT, PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH, - ToolTipText="Royalty rate used in SAM Economic Models." # FIXME WIP TODO documentation + ToolTipText="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." ) self.royalty_holder_discount_rate = self.ParameterDict[self.royalty_holder_discount_rate.Name] = floatParameter( @@ -986,7 +988,9 @@ def __init__(self, model: Model): UnitType=Units.PERCENT, PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH, - ToolTipText="Royalty holder discount rate used in SAM Economic Models." # FIXME WIP TODO documentation + ToolTipText="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." ) @@ -2425,6 +2429,11 @@ def _warn(_msg: str) -> None: if self.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA: EconomicsSam.validate_read_parameters(model) + else: + if self.royalty_rate.Provided: + raise NotImplementedError('Royalties are only supported for SAM Economic Models') + + # TODO validate that other SAM-EM-only parameters have not been provided else: model.logger.info("No parameters read because no content provided") diff --git a/src/geophires_x_schema_generator/geophires-request.json b/src/geophires_x_schema_generator/geophires-request.json index 46ce9c37f..580420e28 100644 --- a/src/geophires_x_schema_generator/geophires-request.json +++ b/src/geophires_x_schema_generator/geophires-request.json @@ -1639,7 +1639,7 @@ "maximum": 1.0 }, "Royalty Rate": { - "description": "Royalty rate used in SAM Economic Models.", + "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.", "type": "number", "units": "", "category": "Economics", @@ -1648,7 +1648,7 @@ "maximum": 1.0 }, "Royalty Holder Discount Rate": { - "description": "Royalty holder discount rate used in SAM Economic Models.", + "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.", "type": "number", "units": "", "category": "Economics", From 2a522ccdda7515bed6652e6f0594353d0579be56 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 15 Sep 2025 13:53:37 -0700 Subject: [PATCH 18/32] SAM-EM royalties documentation (parameter mapping + dedicated section) --- docs/SAM-Economic-Models.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/SAM-Economic-Models.md b/docs/SAM-Economic-Models.md index 0e79a3a28..4629a7bf7 100644 --- a/docs/SAM-Economic-Models.md +++ b/docs/SAM-Economic-Models.md @@ -22,7 +22,7 @@ The following table describes how GEOPHIRES parameters are transformed into SAM | `Net Electricity Generation` | AC Degradation | `Annual AC degradation rate` schedule | `Utilityrate5` | `degradation` | Percentage difference of each year's `Net Electricity Generation` from `Maximum Total Electricity Generation` is input as SAM as the degradation rate schedule in order to match SAM's generation profile to GEOPHIRES | | {`Total CAPEX` before inflation} × (1 + `Accrued financing during construction (%)`/100); | Installation Costs | `Total Installed Cost` | `Singleowner` | `total_installed_cost` | `Accrued financing during construction (%)` = (1+`Inflation Rate During Construction`) × 100 if `Inflation Rate During Construction` is provided or ((1+`Inflation Rate`) ^ `Construction Years`) × 100 if not. | | `Total O&M Cost`, `Inflation Rate` | Operating Costs | `Fixed operating cost`, `Escalation rate` set to `Inflation Rate` × -1 | `Singleowner` | `om_fixed`, `om_fixed_escal` | .. N/A | -| `Royalty Rate` | Operating Costs | `Variable operating cost` | `Singleowner` | `om_production` | Royalties are treated as a variable operating cost to the owner | +| `Royalty Rate` | Operating Costs | `Variable operating cost` | `Singleowner` | `om_production` | The royalty is modeled as a tax-deductible variable operating expense. GEOPHIRES calculates a schedule of $/MWh values based on the PPA price and Royalty Rate for each year. This ensures the total annual expense in SAM accurately matches the royalty payment due on gross revenue. | | `Plant Lifetime` | Financial Parameters → Analysis Parameters | `Analysis period` | `CustomGeneration`, `Singleowner` | `CustomGeneration.analysis_period`, `Singleowner.term_tenor` | .. N/A | | `Inflation Rate` | Financial Parameters → Analysis Parameters | `Inflation rate` | `Utilityrate5` | `inflation_rate` | .. N/A | | `Discount Rate` | Financial Parameters → Analysis Parameters | `Real discount rate` | `Singleowner` | `real_discount_rate` | .. N/A | @@ -150,6 +150,27 @@ Total AddOn Profit Gained per year is treated as fixed amount Capacity payment r Add-ons CAPEX, OPEX, and profit are supported. Add-ons with electricity and heat are not currently supported, but may be supported in the future. +## Royalties + +SAM Economic Models can model a royalty agreement where a percentage of the project's gross revenue is paid to a third party (the "royalty holder"). This feature is enabled by providing the `Royalty Rate` parameter. + +The royalty payment is modeled as a tax-deductible variable operating expense from the perspective of the project developer (Single Owner). +This reduces the developer's taxable income and ensures their final after-tax metrics (NPV, IRR, etc.) are calculated accurately. + +This is implemented by having GEOPHIRES create a year-by-year schedule for SAM's Variable operating cost (`om_production`) input. +The value for each year is calculated based on that year's PPA price and the user-provided `Royalty Rate`, ensuring the expense in SAM matches the royalty due on gross revenue. + +Input Parameters: + +1. `Royalty Rate`: The percentage of the project's gross annual revenue paid to the royalty holder. +1. `Royalty Holder Discount Rate` (optional): The discount rate used to calculate the Net Present Value (NPV) of the royalty holder's income stream. This is separate from the project's main discount rate to reflect the different risk profiles of the two parties. + +Output Parameters: + +1. `Average Annual Royalty Cost`: The developer's average annual royalty expense over the project's lifetime after construction is complete (Year 1). The same value is also output as `Royalty Holder Average Annual Revenue`. The individual royalties for each year are included in the cash flow line item `O&M production-based expense ($)`. +1. `Royalty Holder Total Revenue`: The total undiscounted royalty income over the project's lifetime. +1. `Royalty Holder NPV`: The Net Present Value of the royalty holder's income stream, calculated using the `Royalty Holder Discount Rate`. + ## Examples ### SAM Single Owner PPA: 50 MWe From ecab0f7b0cccaefc305ea6b0fa6067327b63fe1a Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 15 Sep 2025 14:10:32 -0700 Subject: [PATCH 19/32] =?UTF-8?q?Bump=20version:=203.9.55=20=E2=86=92=203.?= =?UTF-8?q?9.56?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- .cookiecutterrc | 2 +- README.rst | 4 ++-- docs/conf.py | 2 +- setup.py | 2 +- src/geophires_x/__init__.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 61879902c..2517cc418 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.9.55 +current_version = 3.9.56 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index 77d07fef2..73ce91080 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -54,7 +54,7 @@ default_context: sphinx_doctest: "no" sphinx_theme: "sphinx-py3doc-enhanced-theme" test_matrix_separate_coverage: "no" - version: 3.9.55 + version: 3.9.56 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" diff --git a/README.rst b/README.rst index 1cebc729f..ca05b751d 100644 --- a/README.rst +++ b/README.rst @@ -58,9 +58,9 @@ Free software: `MIT 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.9.55.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.9.56.svg :alt: Commits since latest release - :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.55...main + :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.56...main .. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat :target: https://nrel.github.io/GEOPHIRES-X diff --git a/docs/conf.py b/docs/conf.py index 149dfcdf1..344ab81ec 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ year = '2025' author = 'NREL' copyright = f'{year}, {author}' -version = release = '3.9.55' +version = release = '3.9.56' pygments_style = 'trac' templates_path = ['./templates'] diff --git a/setup.py b/setup.py index 1d45721df..a612e457c 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='geophires-x', - version='3.9.55', + version='3.9.56', license='MIT', description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.', long_description='{}\n{}'.format( diff --git a/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index c2fa4fff8..807095dfc 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.9.55' +__version__ = '3.9.56' From 1847b6524832a1714200488c4b98314068bb15d8 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 16 Sep 2025 07:18:23 -0700 Subject: [PATCH 20/32] Move royalty outputs to EXTENDED ECONOMICS --- src/geophires_x/Outputs.py | 49 ++++++++++++++----- src/geophires_x/OutputsAddOns.py | 6 +-- src/geophires_x_client/geophires_x_result.py | 6 +-- .../geophires-result.json | 32 ++++++------ tests/test_geophires_x.py | 10 ++-- 5 files changed, 62 insertions(+), 41 deletions(-) diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index b62d62d4a..aaaa8c836 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -2,7 +2,9 @@ import math import time import sys +from io import TextIOWrapper from pathlib import Path +from typing import Any # noinspection PyPackageRequirements import numpy as np @@ -324,19 +326,6 @@ def PrintOutputs(self, model: Model): if model.surfaceplant.enduse_option.value in [EndUseOptions.ELECTRICITY]: f.write(f' Estimated Jobs Created: {model.economics.jobs_created.value}\n') - if econ.royalty_rate.Provided: - for royalty_output in [ - econ.royalty_holder_npv, - econ.royalty_holder_annual_revenue, - econ.royalty_holder_total_revenue - ]: - label = Outputs._field_label(royalty_output.display_name, 49) - f.write( - f' {label}{royalty_output.value:10.2f} {royalty_output.CurrentUnits.value}\n') - - - - f.write(NL) f.write(' ***ENGINEERING PARAMETERS***\n') f.write(NL) @@ -803,9 +792,25 @@ def PrintOutputs(self, model: Model): addon_df = pd.DataFrame() addon_results = [] + extended_economics_header_printed = False if model.economics.DoAddOnCalculations.value and not is_sam_econ_model: # SAM econ models incorporate add-on economics into main economics, not as separate extended economics. addon_df, addon_results = model.addoutputs.PrintOutputs(model) + extended_economics_header_printed = True + + if econ.royalty_rate.Provided: + with open(self.output_file, 'a', encoding='UTF-8') as f_: + if not extended_economics_header_printed: + self._print_extended_economics_header(f_) + + for royalty_output in [ + econ.royalty_holder_npv, + econ.royalty_holder_annual_revenue, + econ.royalty_holder_total_revenue + ]: + label = Outputs._field_label(royalty_output.display_name, 49) + f_.write( + f' {label}{royalty_output.value:10.2f} {royalty_output.CurrentUnits.value}\n') sdac_df = pd.DataFrame() sdac_results = [] @@ -909,6 +914,24 @@ def get_sam_cash_flow_profile_output(self, model): return ret + def _print_extended_economics_header(self, f_output_file: TextIOWrapper | None = None) -> None: + """ + Header may be printed by either OutputsAddOns, or parent class if royalties are calculated and add-ons are not. + """ + + close_f = False + if f_output_file is None: + f_output_file = open(self.output_file, 'a', encoding='UTF-8') + close_f = True + + f_output_file.write(NL) + f_output_file.write(NL) + f_output_file.write(" ***EXTENDED ECONOMICS***\n") + f_output_file.write(NL) + + if close_f: + f_output_file.close() + @staticmethod def _field_label(field_name: str, print_width_before_value: int) -> str: return f'{field_name}:{" " * (print_width_before_value - len(field_name) - 1)}' diff --git a/src/geophires_x/OutputsAddOns.py b/src/geophires_x/OutputsAddOns.py index d9b66ab9d..77055e701 100644 --- a/src/geophires_x/OutputsAddOns.py +++ b/src/geophires_x/OutputsAddOns.py @@ -29,10 +29,8 @@ def PrintOutputs(self, model) -> tuple[pd.DataFrame, list]: try: with open(self.output_file, 'a', encoding='UTF-8') as f: addon_results: list[OutputTableItem] = [] - f.write(NL) - f.write(NL) - f.write(" ***EXTENDED ECONOMICS***\n") - f.write(NL) + self._print_extended_economics_header(f) + if model.economics.LCOE.value > -999.0: f.write(f" Adjusted Project LCOE (after incentives, grants, AddOns,etc): {model.economics.LCOE.value:10.2f} " + model.economics.LCOE.PreferredUnits.value + NL) addon_results.append(OutputTableItem('Adjusted Project LCOE (after incentives, grants, AddOns,etc)', '{0:10.2f}'.format(model.economics.LCOE.value), model.economics.LCOE.PreferredUnits.value)) diff --git a/src/geophires_x_client/geophires_x_result.py b/src/geophires_x_client/geophires_x_result.py index cd90b777d..e4d143fa3 100644 --- a/src/geophires_x_client/geophires_x_result.py +++ b/src/geophires_x_client/geophires_x_result.py @@ -93,9 +93,6 @@ class GeophiresXResult: 'Project Payback Period', 'CHP: Percent cost allocation for electrical plant', 'Estimated Jobs Created', - 'Royalty Holder NPV', - 'Royalty Holder Average Annual Revenue', - 'Royalty Holder Total Revenue', ], 'EXTENDED ECONOMICS': [ 'Adjusted Project LCOE (after incentives, grants, AddOns,etc)', @@ -113,6 +110,9 @@ class GeophiresXResult: 'Total Add-on Net Heat', 'Total Add-on Profit', 'AddOns Payback Period', + 'Royalty Holder NPV', + 'Royalty Holder Average Annual Revenue', + 'Royalty Holder Total Revenue', ], 'CCUS ECONOMICS': [ 'Total Avoided Carbon Production', diff --git a/src/geophires_x_schema_generator/geophires-result.json b/src/geophires_x_schema_generator/geophires-result.json index 73a906548..321b08e17 100644 --- a/src/geophires_x_schema_generator/geophires-result.json +++ b/src/geophires_x_schema_generator/geophires-result.json @@ -143,21 +143,6 @@ "type": "number", "description": "", "units": null - }, - "Royalty Holder NPV": { - "type": "number", - "description": "Net Present Value (NPV) of the royalty holder's cash flow stream.", - "units": "MUSD" - }, - "Royalty Holder Average Annual Revenue": { - "type": "number", - "description": "The royalty holder's annual revenue stream from the royalty agreement.", - "units": "MUSD/yr" - }, - "Royalty Holder Total Revenue": { - "type": "number", - "description": "The total (undiscounted) revenue received by the royalty holder over the project lifetime.", - "units": "MUSD" } } }, @@ -186,7 +171,22 @@ "Total Add-on Net Elec": {}, "Total Add-on Net Heat": {}, "Total Add-on Profit": {}, - "AddOns Payback Period": {} + "AddOns Payback Period": {}, + "Royalty Holder NPV": { + "type": "number", + "description": "Net Present Value (NPV) of the royalty holder's cash flow stream.", + "units": "MUSD" + }, + "Royalty Holder Average Annual Revenue": { + "type": "number", + "description": "The royalty holder's annual revenue stream from the royalty agreement.", + "units": "MUSD/yr" + }, + "Royalty Holder Total Revenue": { + "type": "number", + "description": "The total (undiscounted) revenue received by the royalty holder over the project lifetime.", + "units": "MUSD" + } } }, "CCUS ECONOMICS": { diff --git a/tests/test_geophires_x.py b/tests/test_geophires_x.py index 77c2a55b0..79c10fb4e 100644 --- a/tests/test_geophires_x.py +++ b/tests/test_geophires_x.py @@ -9,9 +9,6 @@ import numpy as np -# @formatter:off -# noinspection PyProtectedMember -from geophires_x.EconomicsSam import _cash_flow_profile_row from geophires_x.OptionList import PlantType from geophires_x.OptionList import WellDrillingCostCorrelation from geophires_x_client import GeophiresXClient @@ -24,7 +21,10 @@ from geophires_x_client.geophires_input_parameters import ImmutableGeophiresInputParameters from geophires_x_tests.test_options_list import WellDrillingCostCorrelationTestCase -# @formatter:on +# noinspection PyProtectedMember +# ruff: noqa: I001 # Successful module initialization is dependent on this specific import order. +from geophires_x.EconomicsSam import _cash_flow_profile_row + from tests.base_test_case import BaseTestCase @@ -1338,7 +1338,7 @@ def test_royalty_rate(self): self.assertEqual(opex_line_item_sum, total_opex_MUSD) - econ_result = result.result['ECONOMIC PARAMETERS'] + econ_result = result.result['EXTENDED ECONOMICS'] royalty_holder_npv_MUSD = econ_result['Royalty Holder NPV']['value'] if royalty_rate > 0.0: From 7c978a9840faf888c29870b5580c69202c4aa914 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 16 Sep 2025 07:21:45 -0700 Subject: [PATCH 21/32] Outputs py38 future annotations --- src/geophires_x/Outputs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index aaaa8c836..6f8546cde 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import datetime import math import time import sys from io import TextIOWrapper from pathlib import Path -from typing import Any # noinspection PyPackageRequirements import numpy as np From 367fedddcf3af8b7bcad467e6e69dae653bfb3cc Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 16 Sep 2025 07:37:29 -0700 Subject: [PATCH 22/32] Royalty escalation rate + max (WIP to add unit tests, implement full schedule support) --- src/geophires_x/Economics.py | 25 +++++++++++++- src/geophires_x/EconomicsSam.py | 34 +++++++++++++++---- .../geophires-request.json | 20 ++++++++++- 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index ba0da28d9..ad26ead59 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -975,11 +975,34 @@ def __init__(self, model: Model): UnitType=Units.PERCENT, PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH, - ToolTipText="The percentage of the project's gross annual revenue paid to the royalty holder. " + 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." ) + self.royalty_escalation_rate = self.ParameterDict[self.royalty_escalation_rate.Name] = floatParameter( + 'Royalty Escalation Rate', + DefaultValue=0., + Min=0.0, + Max=1.0, + UnitType=Units.PERCENT, + PreferredUnits=PercentUnit.TENTH, + CurrentUnits=PercentUnit.TENTH, + 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." + ) + + self.maximum_royalty_rate = self.ParameterDict[self.maximum_royalty_rate.Name] = floatParameter( + 'Maximum Royalty Rate', + DefaultValue=1.0, # Default to 100% (no effective cap) + Min=0.0, + Max=1.0, + UnitType=Units.PERCENT, + PreferredUnits=PercentUnit.TENTH, + CurrentUnits=PercentUnit.TENTH, + ToolTipText="The maximum royalty rate after escalation, expressed as a fraction (e.g., 0.06 for a 6% cap)." + ) + self.royalty_holder_discount_rate = self.ParameterDict[self.royalty_holder_discount_rate.Name] = floatParameter( 'Royalty Holder Discount Rate', DefaultValue=0.05, diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index 3ff97dabe..d362ebd20 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -417,20 +417,19 @@ def _get_single_owner_parameters(model: Model) -> dict[str, Any]: ) ret['ppa_price_input'] = ppa_price_schedule_per_kWh - if hasattr(econ, 'royalty_rate') and econ.royalty_rate.Provided: - royalty_rate_fraction = econ.royalty_rate.quantity().to(convertible_unit('dimensionless')).magnitude - + royalty_rate_schedule = _get_royalty_rate_schedule(model) + if model.economics.royalty_rate.Provided: # For each year, calculate the royalty as a $/MWh variable cost. # The royalty is a percentage of revenue (MWh * $/MWh). By setting the # variable O&M rate to (PPA Price * Royalty Rate), SAM's calculation # (Rate * MWh) will correctly yield the total royalty payment. - variable_om_schedule_per_MWh = [ - (price_per_kWh * 1000) * royalty_rate_fraction # TODO pint unit conversion (kWh -> MWh) - for price_per_kWh in ppa_price_schedule_per_kWh + variable_om_schedule_per_mwh = [ + (price_kwh * 1000) * royalty_fraction # TODO use pint unit conversion instead + for price_kwh, royalty_fraction in zip(ppa_price_schedule_per_kWh, royalty_rate_schedule) ] # The PySAM parameter for variable operating cost in $/MWh is 'om_production'. - ret['om_production'] = variable_om_schedule_per_MWh + ret['om_production'] = variable_om_schedule_per_mwh # Debt/equity ratio ('Fraction of Investment in Bonds' parameter) ret['debt_percent'] = _pct(econ.FIB) @@ -481,5 +480,26 @@ def _ppa_pricing_model( ) +def _get_royalty_rate_schedule(model: Model) -> list[float]: + """ + Builds a year-by-year schedule of royalty rates based on escalation and cap. + Returns a list of rates as fractions (e.g., 0.05 for 5%). + """ + + econ = model.economics + plant_lifetime = model.surfaceplant.plant_lifetime.value + + escalation_rate = econ.royalty_escalation_rate.value + max_rate = econ.maximum_royalty_rate.value + + schedule = [] + current_rate = econ.royalty_rate.value + for _ in range(plant_lifetime): + schedule.append(min(current_rate, max_rate)) + current_rate += escalation_rate + + return schedule + + def _get_max_total_generation_kW(model: Model) -> float: return np.max(model.surfaceplant.ElectricityProduced.quantity().to(convertible_unit('kW')).magnitude) diff --git a/src/geophires_x_schema_generator/geophires-request.json b/src/geophires_x_schema_generator/geophires-request.json index 580420e28..70b80da68 100644 --- a/src/geophires_x_schema_generator/geophires-request.json +++ b/src/geophires_x_schema_generator/geophires-request.json @@ -1639,7 +1639,7 @@ "maximum": 1.0 }, "Royalty Rate": { - "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.", + "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.", "type": "number", "units": "", "category": "Economics", @@ -1647,6 +1647,24 @@ "minimum": 0.0, "maximum": 1.0 }, + "Royalty Escalation Rate": { + "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.", + "type": "number", + "units": "", + "category": "Economics", + "default": 0.0, + "minimum": 0.0, + "maximum": 1.0 + }, + "Maximum Royalty Rate": { + "description": "The maximum royalty rate after escalation, expressed as a fraction (e.g., 0.06 for a 6% cap).", + "type": "number", + "units": "", + "category": "Economics", + "default": 1.0, + "minimum": 0.0, + "maximum": 1.0 + }, "Royalty Holder Discount Rate": { "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.", "type": "number", From ad513d366c63fdb79f70b59c2d33336e3f28ecfc Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 16 Sep 2025 07:40:58 -0700 Subject: [PATCH 23/32] Mark TODO support custom royalty rate schedule as a list parameter --- src/geophires_x/Economics.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index ad26ead59..6d6466bdf 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -1003,6 +1003,8 @@ def __init__(self, model: Model): ToolTipText="The maximum royalty rate after escalation, expressed as a fraction (e.g., 0.06 for a 6% cap)." ) + # TODO support custom royalty rate schedule as a list parameter + self.royalty_holder_discount_rate = self.ParameterDict[self.royalty_holder_discount_rate.Name] = floatParameter( 'Royalty Holder Discount Rate', DefaultValue=0.05, From 030c17b3d237de3165c0b76ac19c2b8683068728 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 16 Sep 2025 08:04:49 -0700 Subject: [PATCH 24/32] test_royalty_rate_schedule --- src/geophires_x/Economics.py | 4 +- tests/geophires_x_tests/test_economics_sam.py | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index 6d6466bdf..5f4bd00d4 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -981,7 +981,7 @@ def __init__(self, model: Model): ) self.royalty_escalation_rate = self.ParameterDict[self.royalty_escalation_rate.Name] = floatParameter( - 'Royalty Escalation Rate', + 'Royalty Rate Escalation', DefaultValue=0., Min=0.0, Max=1.0, @@ -993,7 +993,7 @@ def __init__(self, model: Model): ) self.maximum_royalty_rate = self.ParameterDict[self.maximum_royalty_rate.Name] = floatParameter( - 'Maximum Royalty Rate', + 'Royalty Rate Maximum', DefaultValue=1.0, # Default to 100% (no effective cap) Min=0.0, Max=1.0, diff --git a/tests/geophires_x_tests/test_economics_sam.py b/tests/geophires_x_tests/test_economics_sam.py index e7eb7f4cd..997b48482 100644 --- a/tests/geophires_x_tests/test_economics_sam.py +++ b/tests/geophires_x_tests/test_economics_sam.py @@ -21,6 +21,7 @@ _ppa_pricing_model, _get_fed_and_state_tax_rates, SamEconomicsCalculations, + _get_royalty_rate_schedule, ) from geophires_x.GeoPHIRESUtils import sig_figs, quantity @@ -662,6 +663,48 @@ def get_row(name: str): # Note the above assertion assumes royalties are the only production-based O&M expenses. If this changes, # the assertion will need to be updated. + def test_royalty_rate_schedule(self): + royalty_rate = 0.1 + escalation_rate = 0.01 + max_rate = royalty_rate + 5 * escalation_rate + m: Model = EconomicsSamTestCase._new_model( + self._egs_test_file_path(), + additional_params={ + 'Royalty Rate': royalty_rate, + 'Royalty Rate Escalation': escalation_rate, + 'Royalty Rate Maximum': max_rate, + }, + ) + + schedule: list[float] = _get_royalty_rate_schedule(m) + + self.assertListAlmostEqual( + [ + 0.1, + 0.11, + 0.12, + 0.13, + 0.14, + 0.15, + 0.15, + 0.15, + 0.15, + 0.15, + 0.15, + 0.15, + 0.15, + 0.15, + 0.15, + 0.15, + 0.15, + 0.15, + 0.15, + 0.15, + ], + schedule, + places=3, + ) + @staticmethod def _new_model(input_file: Path, additional_params: dict[str, Any] | None = None, read_and_calculate=True) -> Model: if additional_params is not None: From b9a33225fa19b75ffa0ffb5995853a7a6180f5bd Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 16 Sep 2025 08:10:48 -0700 Subject: [PATCH 25/32] move logic to Economics.get_royalty_rate_schedule --- src/geophires_x/Economics.py | 22 ++++++++++++++++++++++ src/geophires_x/EconomicsSam.py | 19 +------------------ 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index 5f4bd00d4..b67f2e329 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -3214,6 +3214,28 @@ def build_price_models(self, model: Model) -> None: self.CarbonEscalationStart.value, self.CarbonEscalationRate.value, self.PTCCarbonPrice) + def get_royalty_rate_schedule(self, model: Model) -> list[float]: + """ + Builds a year-by-year schedule of royalty rates based on escalation and cap. + + :type model: :class:`~geophires_x.Model.Model` + :return: schedule: A list of rates as fractions (e.g., 0.05 for 5%). + """ + + plant_lifetime = model.surfaceplant.plant_lifetime.value + + escalation_rate = self.royalty_escalation_rate.value + max_rate = self.maximum_royalty_rate.value + + schedule = [] + current_rate = self.royalty_rate.value + for _ in range(plant_lifetime): + schedule.append(min(current_rate, max_rate)) + current_rate += escalation_rate + + return schedule + + def calculate_cashflow(self, model: Model) -> None: """ Calculate cashflow and cumulative cash flow diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index d362ebd20..cfbc02364 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -481,24 +481,7 @@ def _ppa_pricing_model( def _get_royalty_rate_schedule(model: Model) -> list[float]: - """ - Builds a year-by-year schedule of royalty rates based on escalation and cap. - Returns a list of rates as fractions (e.g., 0.05 for 5%). - """ - - econ = model.economics - plant_lifetime = model.surfaceplant.plant_lifetime.value - - escalation_rate = econ.royalty_escalation_rate.value - max_rate = econ.maximum_royalty_rate.value - - schedule = [] - current_rate = econ.royalty_rate.value - for _ in range(plant_lifetime): - schedule.append(min(current_rate, max_rate)) - current_rate += escalation_rate - - return schedule + return model.economics.get_royalty_rate_schedule(model) def _get_max_total_generation_kW(model: Model) -> float: From 7a3cd96a157af9981d332d99535b9cfbde6b542c Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 16 Sep 2025 08:48:18 -0700 Subject: [PATCH 26/32] test_royalty_rate_escalation --- src/geophires_x/Economics.py | 12 ++++-- src/geophires_x/EconomicsSam.py | 4 +- tests/test_geophires_x.py | 66 +++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 5 deletions(-) diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index b67f2e329..70832c44f 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -3222,14 +3222,20 @@ def get_royalty_rate_schedule(self, model: Model) -> list[float]: :return: schedule: A list of rates as fractions (e.g., 0.05 for 5%). """ + def r(x: float) -> float: + """Ignore apparent float precision issue""" + _precision = 8 + return round(x, _precision) + plant_lifetime = model.surfaceplant.plant_lifetime.value - escalation_rate = self.royalty_escalation_rate.value - max_rate = self.maximum_royalty_rate.value + escalation_rate = r(self.royalty_escalation_rate.value) + max_rate = r(self.maximum_royalty_rate.value) schedule = [] - current_rate = self.royalty_rate.value + current_rate = r(self.royalty_rate.value) for _ in range(plant_lifetime): + current_rate = r(current_rate) schedule.append(min(current_rate, max_rate)) current_rate += escalation_rate diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index cfbc02364..829a4d8a8 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -423,13 +423,13 @@ def _get_single_owner_parameters(model: Model) -> dict[str, Any]: # The royalty is a percentage of revenue (MWh * $/MWh). By setting the # variable O&M rate to (PPA Price * Royalty Rate), SAM's calculation # (Rate * MWh) will correctly yield the total royalty payment. - variable_om_schedule_per_mwh = [ + variable_om_schedule_per_MWh = [ (price_kwh * 1000) * royalty_fraction # TODO use pint unit conversion instead for price_kwh, royalty_fraction in zip(ppa_price_schedule_per_kWh, royalty_rate_schedule) ] # The PySAM parameter for variable operating cost in $/MWh is 'om_production'. - ret['om_production'] = variable_om_schedule_per_mwh + ret['om_production'] = variable_om_schedule_per_MWh # Debt/equity ratio ('Fraction of Investment in Bonds' parameter) ret['debt_percent'] = _pct(econ.FIB) diff --git a/tests/test_geophires_x.py b/tests/test_geophires_x.py index 79c10fb4e..9545b7ed5 100644 --- a/tests/test_geophires_x.py +++ b/tests/test_geophires_x.py @@ -1309,6 +1309,7 @@ def test_redrilling_costs(self): def test_royalty_rate(self): royalties_output_name = 'Average Annual Royalty Cost' + zero_royalty_npv = None for royalty_rate in [0, 0.1]: result = GeophiresXClient().get_geophires_result( ImmutableGeophiresInputParameters( @@ -1345,6 +1346,9 @@ def test_royalty_rate(self): self.assertEqual(58.88, opex_result[royalties_output_name]['value']) self.assertGreater(royalty_holder_npv_MUSD, 0) + # Owner NPV is lower when royalty rate is non-zero + self.assertGreater(zero_royalty_npv, result.result['ECONOMIC PARAMETERS']['Project NPV']['value']) + royalties_cash_flow_MUSD = [ it * 1e-6 for it in _cash_flow_profile_row( @@ -1362,3 +1366,65 @@ def test_royalty_rate(self): if royalty_rate == 0.0: self.assertEqual(0, opex_result[royalties_output_name]['value']) self.assertEqual(0, royalty_holder_npv_MUSD) + zero_royalty_npv = result.result['ECONOMIC PARAMETERS']['Project NPV']['value'] + + def test_royalty_rate_escalation(self): + royalties_output_name = 'Average Annual Royalty Cost' + + base_royalty_rate = 0.05 + escalation_rate = 0.01 + + for max_rate in [0.08, 1.0]: + result = GeophiresXClient().get_geophires_result( + ImmutableGeophiresInputParameters( + from_file_path=self._get_test_file_path( + 'geophires_x_tests/generic-egs-case-2_sam-single-owner-ppa.txt' + ), + params={ + 'Royalty Rate': base_royalty_rate, + 'Royalty Rate Escalation': escalation_rate, + 'Royalty Rate Maximum': max_rate, + }, + ) + ) + opex_result = result.result['OPERATING AND MAINTENANCE COSTS (M$/yr)'] + + self.assertIsNotNone(opex_result[royalties_output_name]) + self.assertEqual('MUSD/yr', opex_result[royalties_output_name]['unit']) + + total_opex_MUSD = opex_result['Total operating and maintenance costs']['value'] + + opex_line_item_sum = 0 + for line_item_names in [ + 'Wellfield maintenance costs', + 'Power plant maintenance costs', + 'Water costs', + royalties_output_name, + ]: + opex_line_item_sum += opex_result[line_item_names]['value'] + + self.assertAlmostEqual(opex_line_item_sum, total_opex_MUSD, places=4) + + project_lifetime_yrs = result.result['ECONOMIC PARAMETERS']['Project lifetime']['value'] + + royalties_cash_flow_MUSD = [ + it * 1e-6 + for it in _cash_flow_profile_row( + result.result['SAM CASH FLOW PROFILE'], 'O&M production-based expense ($)' + ) + ][1:] + + ppa_revenue_MUSD = [ + it * 1e-6 for it in _cash_flow_profile_row(result.result['SAM CASH FLOW PROFILE'], 'PPA revenue ($)') + ][1:] + + actual_royalty_rate = [None] * len(ppa_revenue_MUSD) + for i in range(len(ppa_revenue_MUSD)): + actual_royalty_rate[i] = royalties_cash_flow_MUSD[i] / ppa_revenue_MUSD[i] + + max_expected_rate = ( + max_rate if max_rate < 1.0 else base_royalty_rate + escalation_rate * (project_lifetime_yrs - 1) + ) + + expected_last_year_revenue = ppa_revenue_MUSD[-1] * max_expected_rate + self.assertAlmostEqual(expected_last_year_revenue, royalties_cash_flow_MUSD[-1], places=3) From 2c70fb1d7bee68f1f0058b79b4e21c164cee42fa Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 16 Sep 2025 08:50:48 -0700 Subject: [PATCH 27/32] Regenerate schema --- src/geophires_x/Economics.py | 1 + src/geophires_x_schema_generator/geophires-request.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index 70832c44f..16dcf9fa1 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -1004,6 +1004,7 @@ def __init__(self, model: Model): ) # TODO support custom royalty rate schedule as a list parameter + # (as an alternative to specifying rate/escalation/max) self.royalty_holder_discount_rate = self.ParameterDict[self.royalty_holder_discount_rate.Name] = floatParameter( 'Royalty Holder Discount Rate', diff --git a/src/geophires_x_schema_generator/geophires-request.json b/src/geophires_x_schema_generator/geophires-request.json index 70b80da68..70a75e79f 100644 --- a/src/geophires_x_schema_generator/geophires-request.json +++ b/src/geophires_x_schema_generator/geophires-request.json @@ -1647,7 +1647,7 @@ "minimum": 0.0, "maximum": 1.0 }, - "Royalty Escalation Rate": { + "Royalty Rate Escalation": { "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.", "type": "number", "units": "", @@ -1656,7 +1656,7 @@ "minimum": 0.0, "maximum": 1.0 }, - "Maximum Royalty Rate": { + "Royalty Rate Maximum": { "description": "The maximum royalty rate after escalation, expressed as a fraction (e.g., 0.06 for a 6% cap).", "type": "number", "units": "", From 4f42e5d9b97798191c5b0bbb0aea93276737d2eb Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 16 Sep 2025 09:00:31 -0700 Subject: [PATCH 28/32] Update SAM-EM Royalties docs --- docs/SAM-Economic-Models.md | 73 +++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 31 deletions(-) diff --git a/docs/SAM-Economic-Models.md b/docs/SAM-Economic-Models.md index 4629a7bf7..8197733f8 100644 --- a/docs/SAM-Economic-Models.md +++ b/docs/SAM-Economic-Models.md @@ -15,28 +15,28 @@ The following table describes how GEOPHIRES parameters are transformed into SAM [EconomicsSam.py](https://github.com/softwareengineerprogrammer/GEOPHIRES/blob/274786e6799d32dad3f42a2a04297818b811f24c/src/geophires_x/EconomicsSam.py#L135-L195). (Note that the source code implementation determines actual behavior in the case of any discrepancies.) -| GEOPHIRES Parameter(s) | SAM Category | SAM Input(s) | SAM Module(s) | SAM Parameter Name(s) | Comment | -|-------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|--------------------------------------------------------------------------------------------------------------|-----------------------------------|--------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `Maximum Total Electricity Generation` | Generation Profile | `Nameplate capacity` | `Singleowner` | `system_capacity` | .. N/A | -| `Utilization Factor` | Generation Profile | `Nominal capacity factor` | `Singleowner` | `user_capacity_factor` | .. N/A | -| `Net Electricity Generation` | AC Degradation | `Annual AC degradation rate` schedule | `Utilityrate5` | `degradation` | Percentage difference of each year's `Net Electricity Generation` from `Maximum Total Electricity Generation` is input as SAM as the degradation rate schedule in order to match SAM's generation profile to GEOPHIRES | -| {`Total CAPEX` before inflation} × (1 + `Accrued financing during construction (%)`/100); | Installation Costs | `Total Installed Cost` | `Singleowner` | `total_installed_cost` | `Accrued financing during construction (%)` = (1+`Inflation Rate During Construction`) × 100 if `Inflation Rate During Construction` is provided or ((1+`Inflation Rate`) ^ `Construction Years`) × 100 if not. | -| `Total O&M Cost`, `Inflation Rate` | Operating Costs | `Fixed operating cost`, `Escalation rate` set to `Inflation Rate` × -1 | `Singleowner` | `om_fixed`, `om_fixed_escal` | .. N/A | -| `Royalty Rate` | Operating Costs | `Variable operating cost` | `Singleowner` | `om_production` | The royalty is modeled as a tax-deductible variable operating expense. GEOPHIRES calculates a schedule of $/MWh values based on the PPA price and Royalty Rate for each year. This ensures the total annual expense in SAM accurately matches the royalty payment due on gross revenue. | -| `Plant Lifetime` | Financial Parameters → Analysis Parameters | `Analysis period` | `CustomGeneration`, `Singleowner` | `CustomGeneration.analysis_period`, `Singleowner.term_tenor` | .. N/A | -| `Inflation Rate` | Financial Parameters → Analysis Parameters | `Inflation rate` | `Utilityrate5` | `inflation_rate` | .. N/A | -| `Discount Rate` | Financial Parameters → Analysis Parameters | `Real discount rate` | `Singleowner` | `real_discount_rate` | .. N/A | -| `Combined Income Tax Rate` | Financial Parameters → Project Tax and Insurance Rates | `Federal income tax rate`\: minimum of {21%, CITR}; and `State income tax rate`: maximum of {0%; CITR - 21%} | `Singleowner` | `federal_tax_rate`, `state_tax_rate` | GEOPHIRES does not have separate parameters for federal and state income tax so the rates are split from the combined rate based on an assumption of a maximum federal tax rate of 21% and the residual amount being the state tax rate. | -| `Property Tax Rate` | Financial Parameters | `Property tax rate` | `Singleowner` | `property_tax_rate` | .. N/A | -| `Fraction of Investment in Bonds` | Financial Parameters → Project Term Debt | `Debt percent` | `Singleowner` | `debt_percent` | .. N/A | -| `Inflated Bond Interest Rate` | Financial Parameters → Project Term Debt | `Annual interest rate` | `Singleowner` | `term_int_rate` | .. N/A | -| `Starting Electricity Sale Price`, `Ending Electricity Sale Price`, `Electricity Escalation Rate Per Year`, `Electricity Escalation Start Year` | Revenue | `PPA price` | `Singleowner` | `ppa_price_input` | GEOPHIRES's pricing model is used to create a PPA price schedule that is passed to SAM. | -| `Total AddOn Profit Gained` | Revenue → Capacity Payments | `Fixed amount`, `Capacity payment amount` | `Singleowner` | `cp_capacity_payment_type = 1`, `cp_capacity_payment_amount` | | -| `Investment Tax Credit Rate` | Incentives → Investment Tax Credit (ITC) | `Federal` → `Percentage (%)` | `Singleowner` | `itc_fed_percent` | Note that unlike the BICYCLE Economic Model's `Total capital costs`, SAM Economic Model's `Total CAPEX` is the total installed cost and does not subtract ITC value (if present). | -| `Production Tax Credit Electricity` | Incentives → Production Tax Credit (PTC) | `Federal` → `Amount ($/kWh)` | `Singleowner` | `ptc_fed_amount` | .. N/A | -| `Production Tax Credit Duration` | Incentives → Production Tax Credit (PTC) | `Federal` → `Term (years)` | `Singleowner` | `ptc_fed_term` | .. N/A | -| `Production Tax Credit Inflation Adjusted`, `Inflation Rate` | Incentives → Production Tax Credit (PTC) | `Federal` → `Escalation (%/yr)` | `Singleowner` | `ptc_fed_escal` | If `Production Tax Credit Inflation Adjusted` = True, GEOPHIRES set's SAM's PTC escalation rate to the inflation rate. SAM applies the escalation rate to years 2 and later of the project cash flow. Note that this produces escalation rates that are similar to inflation-adjusted equivalents, but not exactly equal. | -| `Other Incentives` + `One-time Grants Etc` | Incentives → Investment Based Incentive (IBI) | `Other` → `Amount ($)` | `Singleowner` | `ibi_oth_amount` | .. N/A | +| GEOPHIRES Parameter(s) | SAM Category | SAM Input(s) | SAM Module(s) | SAM Parameter Name(s) | Comment | +|-------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|--------------------------------------------------------------------------------------------------------------|-----------------------------------|--------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Maximum Total Electricity Generation` | Generation Profile | `Nameplate capacity` | `Singleowner` | `system_capacity` | .. N/A | +| `Utilization Factor` | Generation Profile | `Nominal capacity factor` | `Singleowner` | `user_capacity_factor` | .. N/A | +| `Net Electricity Generation` | AC Degradation | `Annual AC degradation rate` schedule | `Utilityrate5` | `degradation` | Percentage difference of each year's `Net Electricity Generation` from `Maximum Total Electricity Generation` is input as SAM as the degradation rate schedule in order to match SAM's generation profile to GEOPHIRES | +| {`Total CAPEX` before inflation} × (1 + `Accrued financing during construction (%)`/100); | Installation Costs | `Total Installed Cost` | `Singleowner` | `total_installed_cost` | `Accrued financing during construction (%)` = (1+`Inflation Rate During Construction`) × 100 if `Inflation Rate During Construction` is provided or ((1+`Inflation Rate`) ^ `Construction Years`) × 100 if not. | +| `Total O&M Cost`, `Inflation Rate` | Operating Costs | `Fixed operating cost`, `Escalation rate` set to `Inflation Rate` × -1 | `Singleowner` | `om_fixed`, `om_fixed_escal` | .. N/A | +| `Royalty Rate`, `Royalty Rate Escalation`, `Royalty Rate Maximum` | Operating Costs | `Variable operating cost` | `Singleowner` | `om_production` | The royalty is modeled as a tax-deductible variable operating expense. GEOPHIRES calculates a schedule of $/MWh values based on the PPA price and Royalty Rate for each year, with optional escalation and cap (maximum). This ensures the total annual expense in SAM accurately matches the royalty payment due on gross revenue. | +| `Plant Lifetime` | Financial Parameters → Analysis Parameters | `Analysis period` | `CustomGeneration`, `Singleowner` | `CustomGeneration.analysis_period`, `Singleowner.term_tenor` | .. N/A | +| `Inflation Rate` | Financial Parameters → Analysis Parameters | `Inflation rate` | `Utilityrate5` | `inflation_rate` | .. N/A | +| `Discount Rate` | Financial Parameters → Analysis Parameters | `Real discount rate` | `Singleowner` | `real_discount_rate` | .. N/A | +| `Combined Income Tax Rate` | Financial Parameters → Project Tax and Insurance Rates | `Federal income tax rate`\: minimum of {21%, CITR}; and `State income tax rate`: maximum of {0%; CITR - 21%} | `Singleowner` | `federal_tax_rate`, `state_tax_rate` | GEOPHIRES does not have separate parameters for federal and state income tax so the rates are split from the combined rate based on an assumption of a maximum federal tax rate of 21% and the residual amount being the state tax rate. | +| `Property Tax Rate` | Financial Parameters | `Property tax rate` | `Singleowner` | `property_tax_rate` | .. N/A | +| `Fraction of Investment in Bonds` | Financial Parameters → Project Term Debt | `Debt percent` | `Singleowner` | `debt_percent` | .. N/A | +| `Inflated Bond Interest Rate` | Financial Parameters → Project Term Debt | `Annual interest rate` | `Singleowner` | `term_int_rate` | .. N/A | +| `Starting Electricity Sale Price`, `Ending Electricity Sale Price`, `Electricity Escalation Rate Per Year`, `Electricity Escalation Start Year` | Revenue | `PPA price` | `Singleowner` | `ppa_price_input` | GEOPHIRES's pricing model is used to create a PPA price schedule that is passed to SAM. | +| `Total AddOn Profit Gained` | Revenue → Capacity Payments | `Fixed amount`, `Capacity payment amount` | `Singleowner` | `cp_capacity_payment_type = 1`, `cp_capacity_payment_amount` | | +| `Investment Tax Credit Rate` | Incentives → Investment Tax Credit (ITC) | `Federal` → `Percentage (%)` | `Singleowner` | `itc_fed_percent` | Note that unlike the BICYCLE Economic Model's `Total capital costs`, SAM Economic Model's `Total CAPEX` is the total installed cost and does not subtract ITC value (if present). | +| `Production Tax Credit Electricity` | Incentives → Production Tax Credit (PTC) | `Federal` → `Amount ($/kWh)` | `Singleowner` | `ptc_fed_amount` | .. N/A | +| `Production Tax Credit Duration` | Incentives → Production Tax Credit (PTC) | `Federal` → `Term (years)` | `Singleowner` | `ptc_fed_term` | .. N/A | +| `Production Tax Credit Inflation Adjusted`, `Inflation Rate` | Incentives → Production Tax Credit (PTC) | `Federal` → `Escalation (%/yr)` | `Singleowner` | `ptc_fed_escal` | If `Production Tax Credit Inflation Adjusted` = True, GEOPHIRES set's SAM's PTC escalation rate to the inflation rate. SAM applies the escalation rate to years 2 and later of the project cash flow. Note that this produces escalation rates that are similar to inflation-adjusted equivalents, but not exactly equal. | +| `Other Incentives` + `One-time Grants Etc` | Incentives → Investment Based Incentive (IBI) | `Other` → `Amount ($)` | `Singleowner` | `ibi_oth_amount` | .. N/A | .. .. Comment entries of ".. N/A" render as blank in the final RST, by design. @@ -152,24 +152,35 @@ Add-ons with electricity and heat are not currently supported, but may be suppor ## Royalties -SAM Economic Models can model a royalty agreement where a percentage of the project's gross revenue is paid to a third party (the "royalty holder"). This feature is enabled by providing the `Royalty Rate` parameter. +SAM Economic Models can model a royalty agreement where a percentage of the project's gross revenue is paid to a third +party (the "royalty holder"). This feature is enabled by providing the `Royalty Rate` parameter. -The royalty payment is modeled as a tax-deductible variable operating expense from the perspective of the project developer (Single Owner). -This reduces the developer's taxable income and ensures their final after-tax metrics (NPV, IRR, etc.) are calculated accurately. +The royalty payment is modeled as a tax-deductible variable operating expense from the perspective of the project +developer (Single Owner). +This reduces the developer's taxable income and ensures their final after-tax metrics (NPV, IRR, etc.) are calculated +accurately. -This is implemented by having GEOPHIRES create a year-by-year schedule for SAM's Variable operating cost (`om_production`) input. -The value for each year is calculated based on that year's PPA price and the user-provided `Royalty Rate`, ensuring the expense in SAM matches the royalty due on gross revenue. +This is implemented by having GEOPHIRES create a year-by-year schedule for SAM's Variable operating cost ( +`om_production`) input. +The value for each year is calculated based on that year's PPA price and the user-provided `Royalty Rate`, ensuring the +expense in SAM matches the royalty due on gross revenue. Input Parameters: -1. `Royalty Rate`: The percentage of the project's gross annual revenue paid to the royalty holder. -1. `Royalty Holder Discount Rate` (optional): The discount rate used to calculate the Net Present Value (NPV) of the royalty holder's income stream. This is separate from the project's main discount rate to reflect the different risk profiles of the two parties. +1. `Royalty Rate`: The percentage of the project's gross annual revenue paid to the royalty holder. It can be optionally + escalated by providing `Royalty Rate Escalation` and capped with `Royalty Rate Maximum`. +1. `Royalty Holder Discount Rate` (optional): The discount rate used to calculate the Net Present Value (NPV) of the + royalty holder's income stream. This is separate from the project's main discount rate to reflect the different risk + profiles of the two parties. Output Parameters: -1. `Average Annual Royalty Cost`: The developer's average annual royalty expense over the project's lifetime after construction is complete (Year 1). The same value is also output as `Royalty Holder Average Annual Revenue`. The individual royalties for each year are included in the cash flow line item `O&M production-based expense ($)`. +1. `Average Annual Royalty Cost`: The developer's average annual royalty expense over the project's lifetime after + construction is complete (Year 1). The same value is also output as `Royalty Holder Average Annual Revenue`. The + individual royalties for each year are included in the cash flow line item `O&M production-based expense ($)`. 1. `Royalty Holder Total Revenue`: The total undiscounted royalty income over the project's lifetime. -1. `Royalty Holder NPV`: The Net Present Value of the royalty holder's income stream, calculated using the `Royalty Holder Discount Rate`. +1. `Royalty Holder NPV`: The Net Present Value of the royalty holder's income stream, calculated using the + `Royalty Holder Discount Rate`. ## Examples From e7dac290bf35b08fa6bba10a77168814421449b9 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 16 Sep 2025 09:10:37 -0700 Subject: [PATCH 29/32] example_SAM-single-owner-PPA-4: 50 MWe with Royalties --- .../example_SAM-single-owner-PPA-4.out | 422 ++++++++++++++++++ .../example_SAM-single-owner-PPA-4.txt | 87 ++++ 2 files changed, 509 insertions(+) create mode 100644 tests/examples/example_SAM-single-owner-PPA-4.out create mode 100644 tests/examples/example_SAM-single-owner-PPA-4.txt diff --git a/tests/examples/example_SAM-single-owner-PPA-4.out b/tests/examples/example_SAM-single-owner-PPA-4.out new file mode 100644 index 000000000..d7d1e526d --- /dev/null +++ b/tests/examples/example_SAM-single-owner-PPA-4.out @@ -0,0 +1,422 @@ + ***************** + ***CASE REPORT*** + ***************** + +Simulation Metadata +---------------------- + GEOPHIRES Version: 3.9.56 + Simulation Date: 2025-09-16 + Simulation Time: 09:04 + Calculation Time: 1.182 sec + + ***SUMMARY OF RESULTS*** + + End-Use Option: Electricity + Average Net Electricity Production: 58.87 MW + Electricity breakeven price: 6.69 cents/kWh + Total CAPEX: 222.97 MUSD + Number of production wells: 6 + Number of injection wells: 6 + Flowrate per production well: 100.0 kg/sec + Well depth: 2.6 kilometer + Geothermal gradient: 74 degC/km + + + ***ECONOMIC PARAMETERS*** + + Economic Model = SAM Single Owner PPA + Real Discount Rate: 8.00 % + Nominal Discount Rate: 10.16 % + WACC: 7.57 % + Accrued financing during construction: 5.00 % + Project lifetime: 20 yr + Capacity factor: 90.0 % + Project NPV: 121.39 MUSD + After-tax IRR: 24.28 % + Project VIR=PI=PIR: 1.91 + Project MOIC: 4.54 + Project Payback Period: 3.84 yr + Estimated Jobs Created: 125 + + ***ENGINEERING PARAMETERS*** + + Number of Production Wells: 6 + Number of Injection Wells: 6 + Well depth: 2.6 kilometer + Water loss rate: 10.0 % + Pump efficiency: 80.0 % + Injection temperature: 56.7 degC + Production Wellbore heat transmission calculated with Ramey's model + Average production well temperature drop: 2.2 degC + Flowrate per production well: 100.0 kg/sec + Injection well casing ID: 9.625 in + Production well casing ID: 9.625 in + Number of times redrilling: 0 + Power plant type: Supercritical ORC + + + ***RESOURCE CHARACTERISTICS*** + + Maximum reservoir temperature: 500.0 degC + Number of segments: 1 + Geothermal gradient: 74 degC/km + + + ***RESERVOIR PARAMETERS*** + + Reservoir Model = Multiple Parallel Fractures Model (Gringarten) + Bottom-hole temperature: 202.40 degC + Fracture model = Square + Well separation: fracture height: 165.00 meter + Fracture area: 27225.00 m**2 + Number of fractures calculated with reservoir volume and fracture separation as input + Number of fractures: 4083 + Fracture separation: 18.00 meter + Reservoir volume: 2000000000 m**3 + Reservoir impedance: 0.0010 GPa.s/m**3 + Reservoir density: 2800.00 kg/m**3 + Reservoir thermal conductivity: 3.05 W/m/K + Reservoir heat capacity: 790.00 J/kg/K + + + ***RESERVOIR SIMULATION RESULTS*** + + Maximum Production Temperature: 200.4 degC + Average Production Temperature: 200.2 degC + Minimum Production Temperature: 198.6 degC + Initial Production Temperature: 198.6 degC + Average Reservoir Heat Extraction: 360.65 MW + Production Wellbore Heat Transmission Model = Ramey Model + Average Production Well Temperature Drop: 2.2 degC + Total Average Pressure Drop: -1320.2 kPa + Average Injection Well Pressure Drop: 483.7 kPa + Average Reservoir Pressure Drop: 630.2 kPa + Average Production Well Pressure Drop: 442.7 kPa + Average Buoyancy Pressure Drop: -2876.7 kPa + + + ***CAPITAL COSTS (M$)*** + + Drilling and completion costs: 49.18 MUSD + Drilling and completion costs per vertical production well: 3.37 MUSD + Drilling and completion costs per vertical injection well: 3.37 MUSD + Drilling and completion costs per non-vertical section: 2.14 MUSD + Stimulation costs: 9.06 MUSD + Surface power plant costs: 144.44 MUSD + Field gathering system costs: 5.80 MUSD + Total surface equipment costs: 150.23 MUSD + Exploration costs: 3.89 MUSD + Inflation costs during construction: 10.62 MUSD + Total CAPEX: 222.97 MUSD + + + ***OPERATING AND MAINTENANCE COSTS (M$/yr)*** + + Wellfield maintenance costs: 1.13 MUSD/yr + Power plant maintenance costs: 3.90 MUSD/yr + Water costs: 1.58 MUSD/yr + Average Annual Royalty Cost: 2.50 MUSD/yr + Total operating and maintenance costs: 9.10 MUSD/yr + + + ***SURFACE EQUIPMENT SIMULATION RESULTS*** + + Initial geofluid availability: 0.19 MW/(kg/s) + Maximum Total Electricity Generation: 59.02 MW + Average Total Electricity Generation: 58.87 MW + Minimum Total Electricity Generation: 57.74 MW + Initial Total Electricity Generation: 57.74 MW + Maximum Net Electricity Generation: 59.02 MW + Average Net Electricity Generation: 58.87 MW + Minimum Net Electricity Generation: 57.74 MW + Initial Net Electricity Generation: 57.74 MW + Average Annual Total Electricity Generation: 464.13 GWh + Average Annual Net Electricity Generation: 464.13 GWh + Average Pumping Power: 0.00 MW + Heat to Power Conversion Efficiency: 16.32 % + + ************************************************************ + * HEATING, COOLING AND/OR ELECTRICITY PRODUCTION PROFILE * + ************************************************************ + YEAR THERMAL GEOFLUID PUMP NET FIRST LAW + DRAWDOWN TEMPERATURE POWER POWER EFFICIENCY + (degC) (MW) (MW) (%) + 1 1.0000 198.64 0.0000 57.7389 16.1868 + 2 1.0055 199.73 0.0000 58.5226 16.2814 + 3 1.0065 199.93 0.0000 58.6666 16.2987 + 4 1.0070 200.03 0.0000 58.7412 16.3077 + 5 1.0074 200.10 0.0000 58.7905 16.3136 + 6 1.0076 200.15 0.0000 58.8268 16.3179 + 7 1.0078 200.19 0.0000 58.8554 16.3213 + 8 1.0080 200.22 0.0000 58.8787 16.3241 + 9 1.0081 200.25 0.0000 58.8984 16.3265 + 10 1.0082 200.27 0.0000 58.9153 16.3285 + 11 1.0083 200.30 0.0000 58.9302 16.3303 + 12 1.0084 200.31 0.0000 58.9434 16.3318 + 13 1.0085 200.33 0.0000 58.9552 16.3332 + 14 1.0086 200.34 0.0000 58.9660 16.3345 + 15 1.0087 200.36 0.0000 58.9758 16.3357 + 16 1.0087 200.37 0.0000 58.9848 16.3368 + 17 1.0088 200.38 0.0000 58.9931 16.3378 + 18 1.0088 200.39 0.0000 59.0009 16.3387 + 19 1.0089 200.40 0.0000 59.0081 16.3396 + 20 1.0089 200.41 0.0000 59.0149 16.3404 + + + ******************************************************************* + * ANNUAL HEATING, COOLING AND/OR ELECTRICITY PRODUCTION PROFILE * + ******************************************************************* + YEAR ELECTRICITY HEAT RESERVOIR PERCENTAGE OF + PROVIDED EXTRACTED HEAT CONTENT TOTAL HEAT MINED + (GWh/year) (GWh/year) (10^15 J) (%) + 1 459.4 2826.8 606.53 1.65 + 2 462.0 2836.1 596.32 3.31 + 3 462.8 2838.9 586.10 4.96 + 4 463.3 2840.6 575.87 6.62 + 5 463.7 2841.7 565.64 8.28 + 6 463.9 2842.6 555.41 9.94 + 7 464.1 2843.3 545.17 11.60 + 8 464.3 2843.9 534.94 13.26 + 9 464.4 2844.4 524.70 14.92 + 10 464.5 2844.9 514.45 16.58 + 11 464.7 2845.2 504.21 18.24 + 12 464.8 2845.6 493.97 19.90 + 13 464.8 2845.9 483.72 21.56 + 14 464.9 2846.2 473.48 23.23 + 15 465.0 2846.4 463.23 24.89 + 16 465.1 2846.7 452.98 26.55 + 17 465.1 2846.9 442.73 28.21 + 18 465.2 2847.1 432.48 29.87 + 19 465.2 2847.3 422.23 31.53 + 20 465.3 2847.5 411.98 33.20 + + *************************** + * SAM CASH FLOW PROFILE * + *************************** +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Year 0 Year 1 Year 2 Year 3 Year 4 Year 5 Year 6 Year 7 Year 8 Year 9 Year 10 Year 11 Year 12 Year 13 Year 14 Year 15 Year 16 Year 17 Year 18 Year 19 Year 20 +ENERGY +Electricity to grid (kWh) 0.0 459,393,200 462,061,296 462,867,882 463,343,815 463,676,568 463,929,931 464,133,155 464,302,013 464,445,938 464,571,006 464,681,345 464,779,886 464,868,777 464,949,642 465,023,726 465,092,011 465,155,313 465,214,308 465,269,475 465,319,040 +Electricity from grid (kWh) 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +Electricity to grid net (kWh) 0.0 459,393,200 462,061,296 462,867,882 463,343,815 463,676,568 463,929,931 464,133,155 464,302,013 464,445,938 464,571,006 464,681,345 464,779,886 464,868,777 464,949,642 465,023,726 465,092,011 465,155,313 465,214,308 465,269,475 465,319,040 + +REVENUE +PPA price (cents/kWh) 0.0 8.0 8.0 8.32 8.64 8.97 9.29 9.61 9.93 10.25 10.58 10.90 11.22 11.54 11.86 12.19 12.51 12.83 13.15 13.47 13.80 +PPA revenue ($) 0 36,751,456 36,964,904 38,519,865 40,051,439 41,573,241 43,089,812 44,603,196 46,114,476 47,624,287 49,133,030 50,640,973 52,148,303 53,655,154 55,161,626 56,667,791 58,173,709 59,679,427 61,184,986 62,690,409 64,195,415 +Curtailment payment revenue ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Capacity payment revenue ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Salvage value ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 111,486,712 +Total revenue ($) 0 36,751,456 36,964,904 38,519,865 40,051,439 41,573,241 43,089,812 44,603,196 46,114,476 47,624,287 49,133,030 50,640,973 52,148,303 53,655,154 55,161,626 56,667,791 58,173,709 59,679,427 61,184,986 62,690,409 175,682,127 + +Property tax net assessed value ($) 0 222,973,424 222,973,424 222,973,424 222,973,424 222,973,424 222,973,424 222,973,424 222,973,424 222,973,424 222,973,424 222,973,424 222,973,424 222,973,424 222,973,424 222,973,424 222,973,424 222,973,424 222,973,424 222,973,424 222,973,424 + +OPERATING EXPENSES +O&M fixed expense ($) 0 6,599,142 6,599,142 6,599,142 6,599,142 6,599,142 6,599,142 6,599,142 6,599,142 6,599,142 6,599,142 6,599,142 6,599,142 6,599,142 6,599,142 6,599,142 6,599,142 6,599,142 6,599,142 6,599,142 6,599,142 +O&M production-based expense ($) 0 1,837,573 2,217,894 2,696,391 3,204,115 3,741,592 4,308,981 4,460,320 4,611,448 4,762,429 4,913,303 5,064,097 5,214,830 5,365,515 5,516,163 5,666,779 5,817,371 5,967,943 6,118,499 6,269,041 6,419,541 +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 +Electricity purchase ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Property tax expense ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Insurance expense ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Total operating expenses ($) 0 8,436,715 8,817,037 9,295,533 9,803,258 10,340,734 10,908,124 11,059,462 11,210,590 11,361,571 11,512,445 11,663,240 11,813,973 11,964,658 12,115,305 12,265,921 12,416,513 12,567,085 12,717,641 12,868,183 13,018,684 + +EBITDA ($) 0 28,314,741 28,147,867 29,224,332 30,248,182 31,232,507 32,181,688 33,543,734 34,903,886 36,262,716 37,620,584 38,977,733 40,334,330 41,690,496 43,046,321 44,401,870 45,757,195 47,112,342 48,467,345 49,822,226 162,663,443 + +OPERATING ACTIVITIES +EBITDA ($) 0 28,314,741 28,147,867 29,224,332 30,248,182 31,232,507 32,181,688 33,543,734 34,903,886 36,262,716 37,620,584 38,977,733 40,334,330 41,690,496 43,046,321 44,401,870 45,757,195 47,112,342 48,467,345 49,822,226 162,663,443 +Interest earned on reserves ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +plus PBI if not available for debt service: +Federal PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Utility PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Other PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Debt interest payment ($) 0 4,459,468 4,324,603 4,182,993 4,034,304 3,878,180 3,714,249 3,542,123 3,361,389 3,171,620 2,972,361 2,763,140 2,543,458 2,312,791 2,070,592 1,816,282 1,549,257 1,268,880 974,485 665,370 340,799 +Cash flow from operating activities ($) 0 23,855,272 23,823,264 25,041,339 26,213,878 27,354,327 28,467,439 30,001,612 31,542,496 33,091,096 34,648,223 36,214,593 37,790,873 39,377,705 40,975,729 42,585,588 44,207,939 45,843,461 47,492,860 49,156,856 162,322,644 + +INVESTING ACTIVITIES +Total installed cost ($) -222,973,424 +Debt closing costs ($) 0 +Debt up-front fee ($) 0 +minus: +Total IBI income ($) 0 +Total CBI income ($) 0 +equals: +Purchase of property ($) -222,973,424 +plus: +Reserve (increase)/decrease debt service ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease working capital ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease receivables ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease major equipment 1 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease major equipment 2 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease major equipment 3 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve capital spending major equipment 1 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve capital spending major equipment 2 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve capital spending major equipment 3 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +equals: +Cash flow from investing activities ($) -222,973,424 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +FINANCING ACTIVITIES +Issuance of equity ($) 133,784,055 +Size of debt ($) 89,189,370 +minus: +Debt principal payment ($) 0 2,697,317 2,832,183 2,973,792 3,122,482 3,278,606 3,442,536 3,614,663 3,795,396 3,985,166 4,184,424 4,393,646 4,613,328 4,843,994 5,086,194 5,340,504 5,607,529 5,887,905 6,182,301 6,491,416 6,815,986 +equals: +Cash flow from financing activities ($) 222,973,424 -2,697,317 -2,832,183 -2,973,792 -3,122,482 -3,278,606 -3,442,536 -3,614,663 -3,795,396 -3,985,166 -4,184,424 -4,393,646 -4,613,328 -4,843,994 -5,086,194 -5,340,504 -5,607,529 -5,887,905 -6,182,301 -6,491,416 -6,815,986 + +PROJECT RETURNS +Pre-tax Cash Flow: +Cash flow from operating activities ($) 0 23,855,272 23,823,264 25,041,339 26,213,878 27,354,327 28,467,439 30,001,612 31,542,496 33,091,096 34,648,223 36,214,593 37,790,873 39,377,705 40,975,729 42,585,588 44,207,939 45,843,461 47,492,860 49,156,856 162,322,644 +Cash flow from investing activities ($) -222,973,424 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Cash flow from financing activities ($) 222,973,424 -2,697,317 -2,832,183 -2,973,792 -3,122,482 -3,278,606 -3,442,536 -3,614,663 -3,795,396 -3,985,166 -4,184,424 -4,393,646 -4,613,328 -4,843,994 -5,086,194 -5,340,504 -5,607,529 -5,887,905 -6,182,301 -6,491,416 -6,815,986 +Total pre-tax cash flow ($) 0 21,157,955 20,991,081 22,067,546 23,091,396 24,075,721 25,024,903 26,386,948 27,747,100 29,105,930 30,463,799 31,820,948 33,177,545 34,533,711 35,889,535 37,245,084 38,600,410 39,955,556 41,310,559 42,665,440 155,506,657 + +Pre-tax Returns: +Issuance of equity ($) 133,784,055 +Total pre-tax cash flow ($) 0 21,157,955 20,991,081 22,067,546 23,091,396 24,075,721 25,024,903 26,386,948 27,747,100 29,105,930 30,463,799 31,820,948 33,177,545 34,533,711 35,889,535 37,245,084 38,600,410 39,955,556 41,310,559 42,665,440 155,506,657 +Total pre-tax returns ($) -133,784,055 21,157,955 20,991,081 22,067,546 23,091,396 24,075,721 25,024,903 26,386,948 27,747,100 29,105,930 30,463,799 31,820,948 33,177,545 34,533,711 35,889,535 37,245,084 38,600,410 39,955,556 41,310,559 42,665,440 155,506,657 + +After-tax Returns: +Total pre-tax returns ($) -133,784,055 21,157,955 20,991,081 22,067,546 23,091,396 24,075,721 25,024,903 26,386,948 27,747,100 29,105,930 30,463,799 31,820,948 33,177,545 34,533,711 35,889,535 37,245,084 38,600,410 39,955,556 41,310,559 42,665,440 155,506,657 +Federal ITC total income ($) 0 66,892,027 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal tax benefit (liability) ($) 0 -3,733,567 -2,801,948 -3,039,838 -3,268,835 -3,491,565 -3,708,956 -4,008,580 -4,309,514 -4,611,956 -4,916,063 -5,221,975 -5,529,822 -5,839,731 -6,151,825 -6,466,230 -6,783,075 -7,102,493 -7,424,620 -7,749,599 -29,850,877 +State ITC total income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State tax benefit (liability) ($) 0 -1,338,196 -1,004,283 -1,089,548 -1,171,626 -1,251,457 -1,329,375 -1,436,767 -1,544,629 -1,653,031 -1,762,030 -1,871,676 -1,982,015 -2,093,093 -2,204,955 -2,317,645 -2,431,210 -2,545,696 -2,661,154 -2,777,634 -10,699,239 +Total after-tax returns ($) -133,784,055 82,978,219 17,184,850 17,938,160 18,650,935 19,332,699 19,986,572 20,941,602 21,892,957 22,840,943 23,785,706 24,727,297 25,665,707 26,600,887 27,532,755 28,461,209 29,386,125 30,307,367 31,224,785 32,138,207 114,956,541 + +After-tax cumulative IRR (%) NaN -37.98 -21.59 -8.12 1.23 7.56 11.90 14.98 17.22 18.87 20.10 21.04 21.76 22.32 22.76 23.10 23.37 23.59 23.76 23.90 24.28 +After-tax cumulative NPV ($) -133,784,055 -58,458,874 -44,297,744 -30,879,178 -18,214,186 -6,297,024 4,886,915 15,524,484 25,619,639 35,180,537 44,218,630 52,747,931 60,784,415 68,345,517 75,449,709 82,116,156 88,364,421 94,214,227 99,685,253 104,796,972 121,394,949 + +AFTER-TAX LCOE AND PPA PRICE +Annual costs ($) -133,784,055 46,226,763 -19,780,053 -20,581,705 -21,400,504 -22,240,542 -23,103,240 -23,661,594 -24,221,519 -24,783,343 -25,347,324 -25,913,676 -26,482,596 -27,054,268 -27,628,870 -28,206,583 -28,787,584 -29,372,060 -29,960,201 -30,552,202 50,761,126 +PPA revenue ($) 0 36,751,456 36,964,904 38,519,865 40,051,439 41,573,241 43,089,812 44,603,196 46,114,476 47,624,287 49,133,030 50,640,973 52,148,303 53,655,154 55,161,626 56,667,791 58,173,709 59,679,427 61,184,986 62,690,409 64,195,415 +Electricity to grid (kWh) 0.0 459,393,200 462,061,296 462,867,882 463,343,815 463,676,568 463,929,931 464,133,155 464,302,013 464,445,938 464,571,006 464,681,345 464,779,886 464,868,777 464,949,642 465,023,726 465,092,011 465,155,313 465,214,308 465,269,475 465,319,040 + +Present value of annual costs ($) 261,106,355 +Present value of annual energy nominal (kWh) 3,903,105,303.0 +LCOE Levelized cost of energy nominal (cents/kWh) 6.69 + +Present value of PPA revenue ($) 382,501,304 +Present value of annual energy nominal (kWh) 3,903,105,303.0 +LPPA Levelized PPA price nominal (cents/kWh) 9.80 + +PROJECT STATE INCOME TAXES +EBITDA ($) 0 28,314,741 28,147,867 29,224,332 30,248,182 31,232,507 32,181,688 33,543,734 34,903,886 36,262,716 37,620,584 38,977,733 40,334,330 41,690,496 43,046,321 44,401,870 45,757,195 47,112,342 48,467,345 49,822,226 162,663,443 +State taxable PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Interest earned on reserves ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State taxable IBI income ($) 0 +State taxable CBI income ($) 0 +minus: +Debt interest payment ($) 0 4,459,468 4,324,603 4,182,993 4,034,304 3,878,180 3,714,249 3,542,123 3,361,389 3,171,620 2,972,361 2,763,140 2,543,458 2,312,791 2,070,592 1,816,282 1,549,257 1,268,880 974,485 665,370 340,799 +Total state tax depreciation ($) 0 4,738,185 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 +equals: +State taxable income ($) 0 19,117,087 14,346,894 15,564,968 16,737,508 17,877,957 18,991,068 20,525,241 22,066,126 23,614,725 25,171,852 26,738,223 28,314,502 29,901,335 31,499,358 33,109,217 34,731,568 36,367,091 38,016,489 39,680,485 152,846,273 + +State income tax rate (frac) 0.0 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 +State tax benefit (liability) ($) 0 -1,338,196 -1,004,283 -1,089,548 -1,171,626 -1,251,457 -1,329,375 -1,436,767 -1,544,629 -1,653,031 -1,762,030 -1,871,676 -1,982,015 -2,093,093 -2,204,955 -2,317,645 -2,431,210 -2,545,696 -2,661,154 -2,777,634 -10,699,239 + +PROJECT FEDERAL INCOME TAXES +EBITDA ($) 0 28,314,741 28,147,867 29,224,332 30,248,182 31,232,507 32,181,688 33,543,734 34,903,886 36,262,716 37,620,584 38,977,733 40,334,330 41,690,496 43,046,321 44,401,870 45,757,195 47,112,342 48,467,345 49,822,226 162,663,443 +Interest earned on reserves ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State tax benefit (liability) ($) 0 -1,338,196 -1,004,283 -1,089,548 -1,171,626 -1,251,457 -1,329,375 -1,436,767 -1,544,629 -1,653,031 -1,762,030 -1,871,676 -1,982,015 -2,093,093 -2,204,955 -2,317,645 -2,431,210 -2,545,696 -2,661,154 -2,777,634 -10,699,239 +State ITC total income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal taxable IBI income ($) 0 +Federal taxable CBI income ($) 0 +Federal taxable PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +minus: +Debt interest payment ($) 0 4,459,468 4,324,603 4,182,993 4,034,304 3,878,180 3,714,249 3,542,123 3,361,389 3,171,620 2,972,361 2,763,140 2,543,458 2,312,791 2,070,592 1,816,282 1,549,257 1,268,880 974,485 665,370 340,799 +Total federal tax depreciation ($) 0 4,738,185 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 9,476,371 +equals: +Federal taxable income ($) 0 17,778,891 13,342,611 14,475,420 15,565,882 16,626,500 17,661,694 19,088,474 20,521,497 21,961,695 23,409,823 24,866,547 26,332,487 27,808,241 29,294,403 30,791,572 32,300,358 33,821,394 35,355,335 36,902,851 142,147,034 + +Federal income tax rate (frac) 0.0 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 +Federal tax benefit (liability) ($) 0 -3,733,567 -2,801,948 -3,039,838 -3,268,835 -3,491,565 -3,708,956 -4,008,580 -4,309,514 -4,611,956 -4,916,063 -5,221,975 -5,529,822 -5,839,731 -6,151,825 -6,466,230 -6,783,075 -7,102,493 -7,424,620 -7,749,599 -29,850,877 + +CASH INCENTIVES +Federal IBI income ($) 0 +State IBI income ($) 0 +Utility IBI income ($) 0 +Other IBI income ($) 0 +Total IBI income ($) 0 + +Federal CBI income ($) 0 +State CBI income ($) 0 +Utility CBI income ($) 0 +Other CBI income ($) 0 +Total CBI income ($) 0 + +Federal PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Utility PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Other PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Total PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +TAX CREDITS +Federal PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Federal ITC amount income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal ITC percent income ($) 0 66,892,027 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal ITC total income ($) 0 66,892,027 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +State ITC amount income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State ITC percent income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State ITC total income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +DEBT REPAYMENT +Debt balance ($) 89,189,370 86,492,052 83,659,869 80,686,077 77,563,595 74,284,989 70,842,453 67,227,790 63,432,393 59,447,227 55,262,803 50,869,157 46,255,829 41,411,835 36,325,641 30,985,137 25,377,608 19,489,703 13,307,402 6,815,986 0 +Debt interest payment ($) 0 4,459,468 4,324,603 4,182,993 4,034,304 3,878,180 3,714,249 3,542,123 3,361,389 3,171,620 2,972,361 2,763,140 2,543,458 2,312,791 2,070,592 1,816,282 1,549,257 1,268,880 974,485 665,370 340,799 +Debt principal payment ($) 0 2,697,317 2,832,183 2,973,792 3,122,482 3,278,606 3,442,536 3,614,663 3,795,396 3,985,166 4,184,424 4,393,646 4,613,328 4,843,994 5,086,194 5,340,504 5,607,529 5,887,905 6,182,301 6,491,416 6,815,986 +Debt total payment ($) 0 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 + +DSCR (DEBT FRACTION) +EBITDA ($) 0 28,314,741 28,147,867 29,224,332 30,248,182 31,232,507 32,181,688 33,543,734 34,903,886 36,262,716 37,620,584 38,977,733 40,334,330 41,690,496 43,046,321 44,401,870 45,757,195 47,112,342 48,467,345 49,822,226 162,663,443 +minus: +Reserves major equipment 1 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 2 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 3 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves receivables funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +equals: +Cash available for debt service (CAFDS) ($) 0 28,314,741 28,147,867 29,224,332 30,248,182 31,232,507 32,181,688 33,543,734 34,903,886 36,262,716 37,620,584 38,977,733 40,334,330 41,690,496 43,046,321 44,401,870 45,757,195 47,112,342 48,467,345 49,822,226 162,663,443 +Debt total payment ($) 0 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 7,156,786 +DSCR (pre-tax) 0.0 3.96 3.93 4.08 4.23 4.36 4.50 4.69 4.88 5.07 5.26 5.45 5.64 5.83 6.01 6.20 6.39 6.58 6.77 6.96 22.73 + +RESERVES +Reserves working capital funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves working capital disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves working capital balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves debt service funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves debt service disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves debt service balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves receivables funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves receivables disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves receivables balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves major equipment 1 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 1 disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 1 balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves major equipment 2 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 2 disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 2 balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves major equipment 3 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 3 disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 3 balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves total reserves balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Interest on reserves (%/year) 1.75 +Interest earned on reserves ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + + ***EXTENDED ECONOMICS*** + + Royalty Holder NPV: 29.63 MUSD + Royalty Holder Average Annual Revenue: 2.50 MUSD/yr + Royalty Holder Total Revenue: 49.93 MUSD diff --git a/tests/examples/example_SAM-single-owner-PPA-4.txt b/tests/examples/example_SAM-single-owner-PPA-4.txt new file mode 100644 index 000000000..872c704c4 --- /dev/null +++ b/tests/examples/example_SAM-single-owner-PPA-4.txt @@ -0,0 +1,87 @@ +# Example: SAM Single Owner PPA Economic Model: 50 MWe with Royalties +# This example models example_SAM-single-owner with royalties that start at 5% and escalate to 10% +# See "SAM Economic Models" in GEOPHIRES documentation: https://nrel.github.io/GEOPHIRES-X/SAM-Economic-Models.html#add-ons + + +# *** ECONOMIC/FINANCIAL PARAMETERS *** +# ************************************* +Economic Model, 5, -- SAM Single Owner PPA + +Royalty Rate, 0.05 +Royalty Rate Escalation, 0.01 +Royalty Rate Maximum, 0.1 + +Starting Electricity Sale Price, 0.08 +Ending Electricity Sale Price, 1.00 +Electricity Escalation Rate Per Year, 0.00322 +Electricity Escalation Start Year, 1 + +Fraction of Investment in Bonds, .4 +Inflated Bond Interest Rate, .05 +Discount Rate, 0.08 +Inflation Rate, .02 +Inflation Rate During Construction, 0.05 + +Combined Income Tax Rate, .28 +Investment Tax Credit Rate, 0.3 +Property Tax Rate, 0 + +Capital Cost for Power Plant for Electricity Generation, 1900 + +# *** SURFACE & SUBSURFACE TECHNICAL PARAMETERS *** +# ************************************************* +End-Use Option, 1, -- Electricity +Power Plant Type, 2, -- Supercritical ORC +Plant Lifetime, 20 + +Reservoir Model, 1 + +Reservoir Volume Option, 2, -- RES_VOL_FRAC_SEP (Specify reservoir volume and fracture separation) +Reservoir Volume, 2000000000, -- m**3 +Fracture Shape, 3, -- Square +Fracture Separation, 18 +Fracture Height, 165 + +Reservoir Density, 2800 +Reservoir Depth, 2.6, -- km +Reservoir Heat Capacity, 790 +Reservoir Thermal Conductivity, 3.05 +Reservoir Porosity, 0.0118 +Reservoir Impedance, 0.001 + +Number of Segments, 1 +Gradient 1, 74 + +Number of Injection Wells, 6 +Number of Production Wells, 6 + +Production Flow Rate per Well, 100 + +Production Well Diameter, 9.625 +Injection Well Diameter, 9.625 + +Well Separation, 365 feet + +Ramey Production Wellbore Model, 1 +Injection Temperature, 60 degC +Injection Wellbore Temperature Gain, 3 +Plant Outlet Pressure, 1000 psi +Production Wellhead Pressure, 325 psi + +Utilization Factor, .9 +Water Loss Fraction, 0.10 +Maximum Drawdown, 0.0066 +Ambient Temperature, 10, -- degC +Surface Temperature, 10, -- degC +Circulation Pump Efficiency, 0.80 + +Well Geometry Configuration, 4 +Has Nonvertical Section, True +Multilaterals Cased, True +Number of Multilateral Sections, 3 +Nonvertical Length per Multilateral Section, 1433, -- meters + +# *** SIMULATION PARAMETERS *** +# ***************************** +Maximum Temperature, 500 +Time steps per year, 12 From e7b2af747107946c860409c11e250bbcb9c43b51 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 16 Sep 2025 09:11:47 -0700 Subject: [PATCH 30/32] =?UTF-8?q?Bump=20version:=203.9.56=20=E2=86=92=203.?= =?UTF-8?q?9.57?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- .cookiecutterrc | 2 +- README.rst | 4 ++-- docs/conf.py | 2 +- setup.py | 2 +- src/geophires_x/__init__.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 2517cc418..3aa89e26b 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.9.56 +current_version = 3.9.57 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index 73ce91080..0ebd31605 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -54,7 +54,7 @@ default_context: sphinx_doctest: "no" sphinx_theme: "sphinx-py3doc-enhanced-theme" test_matrix_separate_coverage: "no" - version: 3.9.56 + version: 3.9.57 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" diff --git a/README.rst b/README.rst index ca05b751d..3939b822f 100644 --- a/README.rst +++ b/README.rst @@ -58,9 +58,9 @@ Free software: `MIT 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.9.56.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.9.57.svg :alt: Commits since latest release - :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.56...main + :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.57...main .. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat :target: https://nrel.github.io/GEOPHIRES-X diff --git a/docs/conf.py b/docs/conf.py index 344ab81ec..cda29f863 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ year = '2025' author = 'NREL' copyright = f'{year}, {author}' -version = release = '3.9.56' +version = release = '3.9.57' pygments_style = 'trac' templates_path = ['./templates'] diff --git a/setup.py b/setup.py index a612e457c..b5f4e9b31 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='geophires-x', - version='3.9.56', + version='3.9.57', license='MIT', description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.', long_description='{}\n{}'.format( diff --git a/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index 807095dfc..42d91d076 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.9.56' +__version__ = '3.9.57' From eccd8386557a18392ab81b1336615b9e8e15ff26 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 16 Sep 2025 09:24:24 -0700 Subject: [PATCH 31/32] Add example_SAM-single-owner-PPA-4 to README examples table --- README.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 3939b822f..8bd8adcdc 100644 --- a/README.rst +++ b/README.rst @@ -308,10 +308,14 @@ Example-specific web interface deeplinks are listed in the Link column. - `example_SAM-single-owner-PPA-2.txt `__ - `.out `__ - `link `__ - * - SAM Single Owner PPA: 50 MWe with Add-on + * - SAM Single Owner PPA: 50 MWe with Add-ons - `example_SAM-single-owner-PPA-3.txt `__ - `.out `__ - `link `__ + * - SAM Single Owner PPA: 50 MWe with Royalties + - `example_SAM-single-owner-PPA-4.txt `__ + - `.out `__ + - `link `__ .. raw:: html From 7a5767ba5369f414e9ddf2cc9fbe803d27e57653 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 16 Sep 2025 09:53:46 -0700 Subject: [PATCH 32/32] SAM-EM doc web interface links --- docs/SAM-Economic-Models.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/SAM-Economic-Models.md b/docs/SAM-Economic-Models.md index 8197733f8..7a328b000 100644 --- a/docs/SAM-Economic-Models.md +++ b/docs/SAM-Economic-Models.md @@ -150,6 +150,8 @@ Total AddOn Profit Gained per year is treated as fixed amount Capacity payment r Add-ons CAPEX, OPEX, and profit are supported. Add-ons with electricity and heat are not currently supported, but may be supported in the future. +[Add-Ons example web interface link](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=example_SAM-single-owner-PPA-3) + ## Royalties SAM Economic Models can model a royalty agreement where a percentage of the project's gross revenue is paid to a third @@ -182,14 +184,22 @@ Output Parameters: 1. `Royalty Holder NPV`: The Net Present Value of the royalty holder's income stream, calculated using the `Royalty Holder Discount Rate`. -## Examples +[Royalties example web interface link](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=example_SAM-single-owner-PPA-4) -### SAM Single Owner PPA: 50 MWe - -[Web interface link](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=example_SAM-single-owner-PPA) +## Examples ### Case Study: 500 MWe EGS Project Modeled on Fervo Cape Station [Web interface link](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=Fervo_Project_Cape-4) See [Case Study: 500 MWe EGS Project Modeled on Fervo Cape Station](Fervo_Project_Cape-4.html). + +### SAM Single Owner PPA: 50 MWe + +1. [SAM Single Owner PPA: 50 MWe](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=example_SAM-single-owner-PPA) +2. [SAM Single Owner PPA: 50 MWe with Add-ons](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=example_SAM-single-owner-PPA-3) +3. [SAM Single Owner PPA: 50 MWe with Royalties](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=example_SAM-single-owner-PPA-4) + +### SAM Single Owner PPA: 400 MWe BICYCLE Comparison + +[Web interface link](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=example_SAM-single-owner-PPA-2)