diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 5e89dded1..3aa89e26b 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.9.54 +current_version = 3.9.57 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index 20b327d68..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.54 + version: 3.9.57 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" 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 diff --git a/README.rst b/README.rst index 2fbc926f5..8bd8adcdc 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.57.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.57...main .. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat :target: https://nrel.github.io/GEOPHIRES-X @@ -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 diff --git a/docs/SAM-Economic-Models.md b/docs/SAM-Economic-Models.md index b54fdddd6..7a328b000 100644 --- a/docs/SAM-Economic-Models.md +++ b/docs/SAM-Economic-Models.md @@ -15,27 +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 | -| `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. @@ -149,14 +150,56 @@ 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. -## Examples +[Add-Ons example web interface link](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=example_SAM-single-owner-PPA-3) -### SAM Single Owner PPA: 50 MWe +## 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. 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: -[Web interface link](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=example_SAM-single-owner-PPA) +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`. + +[Royalties example web interface link](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=example_SAM-single-owner-PPA-4) + +## 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) diff --git a/docs/conf.py b/docs/conf.py index 091ee8507..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.54' +version = release = '3.9.57' pygments_style = 'trac' templates_path = ['./templates'] diff --git a/setup.py b/setup.py index 399416965..b5f4e9b31 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.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/Economics.py b/src/geophires_x/Economics.py index a5781a49e..16dcf9fa1 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -967,6 +967,58 @@ 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="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 Rate Escalation', + 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( + 'Royalty Rate Maximum', + 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)." + ) + + # 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', + DefaultValue=0.05, + Min=0.0, + Max=1.0, + UnitType=Units.PERCENT, + PreferredUnits=PercentUnit.TENTH, + CurrentUnits=PercentUnit.TENTH, + 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." + ) + self.discount_initial_year_cashflow = self.ParameterDict[self.discount_initial_year_cashflow.Name] = boolParameter( 'Discount Initial Year Cashflow', @@ -1896,6 +1948,15 @@ def __init__(self, model: Model): PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR ) + 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( @@ -2115,6 +2176,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 Average 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: @@ -2367,6 +2455,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") @@ -2485,29 +2578,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) - - 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 - 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: @@ -3143,6 +3215,34 @@ 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%). + """ + + 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 = r(self.royalty_escalation_rate.value) + max_rate = r(self.maximum_royalty_rate.value) + + schedule = [] + 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 + + return schedule + + def calculate_cashflow(self, model: Model) -> None: """ Calculate cashflow and cumulative cash flow @@ -3239,6 +3339,52 @@ 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( # TODO unit conversion + 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 + ) + 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 + 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 e1b14ccca..829a4d8a8 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, + royalty_cost_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=royalty_cost_output_parameter) + project_npv: OutputParameter = field( default_factory=lambda: OutputParameter( UnitType=Units.CURRENCY, @@ -71,7 +74,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): @@ -168,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.value = [ + 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 ) @@ -248,6 +260,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)) @@ -395,13 +408,28 @@ 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 + + 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_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 # Debt/equity ratio ('Fraction of Investment in Bonds' parameter) ret['debt_percent'] = _pct(econ.FIB) @@ -452,5 +480,9 @@ def _ppa_pricing_model( ) +def _get_royalty_rate_schedule(model: Model) -> list[float]: + return model.economics.get_royalty_rate_schedule(model) + + 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/EconomicsUtils.py b/src/geophires_x/EconomicsUtils.py index 1f586a145..782383ecd 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,14 @@ 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 royalty_cost_output_parameter() -> OutputParameter: + return OutputParameter( + Name='Royalty Cost', + UnitType=Units.CURRENCYFREQUENCY, + PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, + CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, + 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 26c082fa9..6f8546cde 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -1,7 +1,10 @@ +from __future__ import annotations + import datetime import math import time import sys +from io import TextIOWrapper from pathlib import Path # noinspection PyPackageRequirements @@ -324,7 +327,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') - f.write(NL) f.write(' ***ENGINEERING PARAMETERS***\n') f.write(NL) @@ -555,6 +557,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.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') + 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') @@ -787,9 +793,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 = [] @@ -893,6 +915,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/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/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index ba7552b02..42d91d076 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.9.54' +__version__ = '3.9.57' diff --git a/src/geophires_x_client/geophires_x_result.py b/src/geophires_x_client/geophires_x_result.py index aabb76199..e4d143fa3 100644 --- a/src/geophires_x_client/geophires_x_result.py +++ b/src/geophires_x_client/geophires_x_result.py @@ -110,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', @@ -290,6 +293,7 @@ class GeophiresXResult: 'Annual District Heating O&M Cost', 'Average Annual Peaking Fuel Cost', 'Average annual pumping costs', + 'Average Annual Royalty Cost', # SUTRA 'Average annual auxiliary fuel cost', 'Average annual pumping cost', diff --git a/src/geophires_x_schema_generator/geophires-request.json b/src/geophires_x_schema_generator/geophires-request.json index d7420d613..70a75e79f 100644 --- a/src/geophires_x_schema_generator/geophires-request.json +++ b/src/geophires_x_schema_generator/geophires-request.json @@ -1638,6 +1638,42 @@ "minimum": 0.0, "maximum": 1.0 }, + "Royalty Rate": { + "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", + "default": 0.0, + "minimum": 0.0, + "maximum": 1.0 + }, + "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": "", + "category": "Economics", + "default": 0.0, + "minimum": 0.0, + "maximum": 1.0 + }, + "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": "", + "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", + "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 4edc6f710..321b08e17 100644 --- a/src/geophires_x_schema_generator/geophires-result.json +++ b/src/geophires_x_schema_generator/geophires-result.json @@ -171,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": { @@ -480,6 +495,11 @@ "units": "MUSD/yr" }, "Average annual pumping costs": {}, + "Average Annual Royalty Cost": { + "type": "number", + "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": {}, "Average annual pumping cost": {}, "Redrilling costs": { 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 diff --git a/tests/geophires_x_tests/test_economics_sam.py b/tests/geophires_x_tests/test_economics_sam.py index 55e76d50c..997b48482 100644 --- a/tests/geophires_x_tests/test_economics_sam.py +++ b/tests/geophires_x_tests/test_economics_sam.py @@ -20,6 +20,8 @@ get_sam_cash_flow_profile_tabulated_output, _ppa_pricing_model, _get_fed_and_state_tax_rates, + SamEconomicsCalculations, + _get_royalty_rate_schedule, ) from geophires_x.GeoPHIRESUtils import sig_figs, quantity @@ -638,6 +640,71 @@ 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: SamEconomicsCalculations = 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_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_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) + # 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: diff --git a/tests/test_geophires_x.py b/tests/test_geophires_x.py index 0e50e5d4a..9545b7ed5 100644 --- a/tests/test_geophires_x.py +++ b/tests/test_geophires_x.py @@ -7,15 +7,24 @@ from pathlib import Path from typing import Any +import numpy as np + 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 from geophires_x_client.geophires_input_parameters import ImmutableGeophiresInputParameters from geophires_x_tests.test_options_list import WellDrillingCostCorrelationTestCase + +# 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 @@ -1025,7 +1034,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 +1294,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,5 +1302,129 @@ 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) + + 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( + 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)'] + + 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.assertEqual(opex_line_item_sum, total_opex_MUSD) + + econ_result = result.result['EXTENDED ECONOMICS'] + 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) + + # 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( + 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) + 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)