diff --git a/.bumpversion.cfg b/.bumpversion.cfg index c1ce4bc1..4915a551 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.9.43 +current_version = 3.9.47 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index d4a01198..8cef84ab 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.43 + version: 3.9.47 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 53739884..86d83382 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,8 +10,9 @@ GEOPHIRES v3 (2023-2025) `release `__ -v3.9 adds the `SAM Single Owner PPA Economic Model `__ +v3.9.47 adds `Add-Ons support for SAM Economic Models `__ +v3.9 adds the `SAM Single Owner PPA Economic Model `__ 3.8 ^^^ diff --git a/README.rst b/README.rst index a0ea909a..4569b460 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.43.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.9.47.svg :alt: Commits since latest release - :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.43...main + :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.47...main .. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat :target: https://nrel.github.io/GEOPHIRES-X @@ -308,7 +308,10 @@ 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 + - `example_SAM-single-owner-PPA-3.txt `__ + - `.out `__ + - `link `__ .. raw:: html diff --git a/docs/SAM-Economic-Models.md b/docs/SAM-Economic-Models.md index cac5b78e..64c0bef6 100644 --- a/docs/SAM-Economic-Models.md +++ b/docs/SAM-Economic-Models.md @@ -30,6 +30,7 @@ The following table describes how GEOPHIRES parameters are transformed into SAM | `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 | @@ -48,7 +49,9 @@ The following table describes how GEOPHIRES parameters are transformed into SAM ### Limitations 1. Only Electricity end-use is supported -2. Only 1 construction year is supported. Note that the `Inflation Rate During Construction` parameter can be used to partially account for longer construction periods. +2. Only 1 construction year is supported. Note that the `Inflation Rate During Construction` parameter can be used to + partially account for longer construction periods. +3. Add-ons with electricity and heat are not currently supported. (Add-ons CAPEX, OPEX, and profit are supported.) ## Using SAM Economic Models with Existing GEOPHIRES Inputs @@ -135,6 +138,17 @@ You can then manually enter the parameters from the logged mapping into the SAM ![](sam-desktop-app-manually-enter-system-capacity-from-geophires-log.png) +## Add-Ons + +SAM Economic Models incorporate add-ons directly, unlike other GEOPHIRES economic models, which calculate separate +extended economics. +Total Add-on CAPEX is added to Total CAPEX. +Total Add-on OPEX is added to Total operating and maintenance costs. +Total AddOn Profit Gained per year is treated as fixed amount Capacity payment revenue. + +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 ### SAM Single Owner PPA: 50 MWe diff --git a/docs/conf.py b/docs/conf.py index 8dbb519f..cde53599 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.43' +version = release = '3.9.47' pygments_style = 'trac' templates_path = ['./templates'] diff --git a/setup.py b/setup.py index ed100cc3..5b79185b 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='geophires-x', - version='3.9.43', + version='3.9.47', 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 c7c6f39e..890d8965 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -2334,7 +2334,7 @@ def _warn(_msg: str) -> None: # we can determine on-the-fly if Addons, CCUS, or S-DAC-GT are being used in the user input file for key in model.InputParameters.keys(): - if key.startswith("AddOn"): + if key.startswith("AddOn") and not self.DoAddOnCalculations.Provided: self.DoAddOnCalculations.value = True break diff --git a/src/geophires_x/EconomicsAddOns.py b/src/geophires_x/EconomicsAddOns.py index 6a047f55..17f6fa62 100644 --- a/src/geophires_x/EconomicsAddOns.py +++ b/src/geophires_x/EconomicsAddOns.py @@ -5,7 +5,7 @@ import numpy_financial as npf import geophires_x.Economics as Economics import geophires_x.Model as Model -from geophires_x.OptionList import EndUseOptions +from geophires_x.OptionList import EndUseOptions, EconomicModel from geophires_x.Parameter import listParameter, OutputParameter from geophires_x.Units import * @@ -108,12 +108,14 @@ def multi_addon_tooltip_text(param_name: str) -> str: # results self.AddOnCAPEXTotal = self.OutputParameterDict[self.AddOnCAPEXTotal.Name] = OutputParameter( "AddOn CAPEX Total", + display_name='Total Add-on CAPEX', UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS, ) self.AddOnOPEXTotalPerYear = self.OutputParameterDict[self.AddOnOPEXTotalPerYear.Name] = OutputParameter( "AddOn OPEX Total Per Year", + display_name='Total Add-on OPEX', UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR @@ -222,6 +224,8 @@ def read_parameters(self, model: Model) -> None: model.logger.info(f'Init {str(__class__)}: {sys._getframe().f_code.co_name}') super().read_parameters(model) # read the parameters for the parent. + is_sam_econ_model = model.economics.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA + # Deal with all the parameter values that the user has provided that relate to this extension. # super.read_parameter will have already dealt with all the regular values, but anything unusual # may not be dealt with, so check. @@ -244,12 +248,21 @@ def read_parameters(self, model: Model) -> None: if key.startswith("AddOn OPEX"): val = float(model.InputParameters[key].sValue) self.AddOnOPEXPerYear.value.append(val) # this assumes they put the values in the file in consecutive fashion + if key.startswith("AddOn Electricity Gained"): + if is_sam_econ_model: + raise NotImplementedError('AddOn Electricity is not supported for SAM Economic Models') + val = float(model.InputParameters[key].sValue) self.AddOnElecGainedPerYear.value.append(val) # this assumes they put the values in the file in consecutive fashion + if key.startswith("AddOn Heat Gained"): + if is_sam_econ_model: + raise NotImplementedError('AddOn Heat is not supported for SAM Economic Models') + val = float(model.InputParameters[key].sValue) self.AddOnHeatGainedPerYear.value.append(val) # this assumes they put the values in the file in consecutive fashion + if key.startswith("AddOn Profit Gained"): val = float(model.InputParameters[key].sValue) self.AddOnProfitGainedPerYear.value.append(val) # this assumes they put the values in the file in consecutive fashion @@ -271,7 +284,9 @@ def Calculate(self, model: Model) -> None: :type model: :class:`~geophires_x.Model.Model` :return: Nothing, but it does make calculations and set values in the model """ - model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f"Init {str(__class__)}: {sys._getframe().f_code.co_name}") + + is_sam_em = model.economics.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA # sum all the AddOn values together, so we can treat all AddOns together. If an AddOn slot is not used, # it has zeros for the values, so this won't create problems @@ -302,6 +317,12 @@ def Calculate(self, model: Model) -> None: # Calculate the adjusted OPEX and CAPEX self.AdjustedProjectCAPEX.value = model.economics.CCap.value + self.AddOnCAPEXTotal.value self.AdjustedProjectOPEX.value = model.economics.Coam.value + self.AddOnOPEXTotalPerYear.value + + if is_sam_em: + # SAM econ models incorporate add-ons into main economics, not as separate extended economics + model.economics.CCap.value = self.AdjustedProjectCAPEX.value + model.economics.Coam.value = self.AdjustedProjectOPEX.value + AddOnCapCostPerYear = self.AddOnCAPEXTotal.value / model.surfaceplant.construction_years.value ProjectCapCostPerYear = self.AdjustedProjectCAPEX.value / model.surfaceplant.construction_years.value @@ -386,8 +407,9 @@ def Calculate(self, model: Model) -> None: self.AdjustedProjectCAPEX.value + ( self.AdjustedProjectOPEX.value * model.surfaceplant.plant_lifetime.value)) - # recalculate LCOE/LCOH - self.LCOE.value, self.LCOH.value, LCOC = Economics.CalculateLCOELCOHLCOC(self, model) + if not is_sam_em: + # recalculate LCOE/LCOH + self.LCOE.value, self.LCOH.value, LCOC = Economics.CalculateLCOELCOHLCOC(self, model) self._calculate_derived_outputs(model) model.logger.info(f'complete {str(__class__)}: {sys._getframe().f_code.co_name}') diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index 78ef3a89..e1b14ccc 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -415,6 +415,12 @@ def _get_single_owner_parameters(model: Model) -> dict[str, Any]: ret['ibi_oth_amount'] = (econ.OtherIncentives.quantity() + econ.TotalGrant.quantity()).to('USD').magnitude + if model.economics.DoAddOnCalculations.value: + add_on_profit_per_year = np.sum(model.addeconomics.AddOnProfitGainedPerYear.quantity().to('USD/yr').magnitude) + add_on_profit_series = [add_on_profit_per_year] + ret['cp_capacity_payment_amount'] = add_on_profit_series + ret['cp_capacity_payment_type'] = 1 + return ret diff --git a/src/geophires_x/Model.py b/src/geophires_x/Model.py index 63a48dd9..d7adfb0f 100644 --- a/src/geophires_x/Model.py +++ b/src/geophires_x/Model.py @@ -1,5 +1,4 @@ import sys -from email.policy import default from pathlib import Path import logging import time diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index 486bae3a..51bcdee5 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -6,6 +6,7 @@ # noinspection PyPackageRequirements import numpy as np +import pandas as pd import geophires_x import geophires_x.Model as Model @@ -175,6 +176,7 @@ def PrintOutputs(self, model: Model): with open(self.output_file, 'w', encoding='UTF-8') as f: econ: Economics = model.economics + is_sam_econ_model = econ.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA f.write(' *****************\n') f.write(' ***CASE REPORT***\n') @@ -221,7 +223,7 @@ def PrintOutputs(self, model: Model): f.write(f' {model.economics.LCOE.display_name}: {model.economics.LCOE.value:10.2f} {model.economics.LCOE.CurrentUnits.value}\n') f.write(f' {model.economics.LCOH.display_name}: {model.economics.LCOH.value:10.2f} {model.economics.LCOH.CurrentUnits.value}\n') - if econ.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA: + if is_sam_econ_model: f.write(f' {Outputs._field_label(econ.capex_total.display_name, 50)}{econ.capex_total.value:10.2f} {econ.capex_total.CurrentUnits.value}\n') f.write(f' Number of production wells: {model.wellbores.nprod.value:10.0f}'+NL) @@ -254,10 +256,10 @@ def PrintOutputs(self, model: Model): # https://github.com/softwareengineerprogrammer/GEOPHIRES/commit/535c02d4adbeeeca553b61e9b996fccf00016529 f.write(f' {model.economics.interest_rate.Name}: {model.economics.interest_rate.value:10.2f} {model.economics.interest_rate.CurrentUnits.value}\n') - elif model.economics.econmodel.value in (EconomicModel.BICYCLE, EconomicModel.SAM_SINGLE_OWNER_PPA): + elif is_sam_econ_model or model.economics.econmodel.value == EconomicModel.BICYCLE: f.write(f' Economic Model = {model.economics.econmodel.value.value}\n') - if model.economics.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA: + if is_sam_econ_model: sam_econ_fields: list[OutputParameter] = [ econ.real_discount_rate, econ.nominal_discount_rate, @@ -281,7 +283,7 @@ def PrintOutputs(self, model: Model): f.write(f' {npv_field_label}{e_npv.value:10.2f} {e_npv.PreferredUnits.value}\n') irr_output_param: OutputParameter = econ.ProjectIRR \ - if econ.econmodel.value != EconomicModel.SAM_SINGLE_OWNER_PPA else econ.after_tax_irr + if not is_sam_econ_model else econ.after_tax_irr irr_field_label = Outputs._field_label(irr_output_param.display_name, 49) irr_display_value = f'{irr_output_param.value:10.2f}' \ if not math.isnan(irr_output_param.value) else 'NaN' @@ -484,7 +486,7 @@ def PrintOutputs(self, model: Model): f.write(f' Stimulation costs (for redrilling): {econ.Cstim.value:10.2f} {econ.Cstim.CurrentUnits.value}\n') if model.economics.RITCValue.value: - if model.economics.econmodel.value != EconomicModel.SAM_SINGLE_OWNER_PPA: + if not is_sam_econ_model: f.write(f' {model.economics.RITCValue.display_name}: {-1*model.economics.RITCValue.value:10.2f} {model.economics.RITCValue.CurrentUnits.value}\n') else: # TODO Extract value from SAM Cash Flow Profile per @@ -495,12 +497,17 @@ def PrintOutputs(self, model: Model): # expenditure. pass - if model.economics.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA: + if is_sam_econ_model: # TODO calculate & display for other economic models icc_label = Outputs._field_label(econ.inflation_cost_during_construction.display_name, 47) f.write(f' {icc_label}{econ.inflation_cost_during_construction.value:10.2f} {econ.inflation_cost_during_construction.CurrentUnits.value}\n') - capex_param = econ.CCap if econ.econmodel.value != EconomicModel.SAM_SINGLE_OWNER_PPA else econ.capex_total + if econ.DoAddOnCalculations.value: + # Non-SAM econ models print this in Extended Economics profile + aoc_label = Outputs._field_label(model.addeconomics.AddOnCAPEXTotal.display_name, 47) + f.write(f' {aoc_label}{model.addeconomics.AddOnCAPEXTotal.value:10.2f} {model.addeconomics.AddOnCAPEXTotal.CurrentUnits.value}\n') + + capex_param = econ.CCap if not is_sam_econ_model else econ.capex_total capex_label = Outputs._field_label(capex_param.display_name, 50) f.write(f' {capex_label}{capex_param.value:10.2f} {capex_param.CurrentUnits.value}\n') @@ -529,6 +536,11 @@ def PrintOutputs(self, model: Model): redrill_label = Outputs._field_label(econ.redrilling_annual_cost.display_name, 47) f.write(f' {redrill_label}{econ.redrilling_annual_cost.value:10.2f} {econ.redrilling_annual_cost.CurrentUnits.value}\n') + if econ.DoAddOnCalculations.value and is_sam_econ_model: + # Non-SAM econ models print this in Extended Economics profile + 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') + 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') @@ -732,10 +744,10 @@ def PrintOutputs(self, model: Model): model.surfaceplant.RemainingReservoirHeatContent.value[i], (model.reserv.InitialReservoirHeatContent.value-model.surfaceplant.RemainingReservoirHeatContent.value[i])*100/model.reserv.InitialReservoirHeatContent.value)+NL) - if econ.econmodel.value != EconomicModel.SAM_SINGLE_OWNER_PPA: + if not is_sam_econ_model: self.write_revenue_and_cashflow_profile_output(model, f) - if econ.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA: + if is_sam_econ_model: f.write(self.get_sam_cash_flow_profile_output(model)) # if we are dealing with overpressure and two different reservoirs, show a table reporting the values @@ -759,6 +771,16 @@ def PrintOutputs(self, model: Model): f.write(NL) f.write(NL) + addon_df = pd.DataFrame() + addon_results = [] + 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) + + sdac_df = pd.DataFrame() + sdac_results = [] + if model.economics.DoSDACGTCalculations.value: + sdac_df, sdac_results = model.sdacgtoutputs.PrintOutputs(model) except BaseException as ex: tb = sys.exc_info()[2] @@ -769,7 +791,15 @@ def PrintOutputs(self, model: Model): model.logger.critical(msg) raise RuntimeError(msg) from ex - print_outputs_rich(self.output_file, self.text_output_file, self.html_output_file, model) + print_outputs_rich( + self.text_output_file, + self.html_output_file, + model, + sdac_results, + addon_results, + sdac_df, + addon_df + ) model.logger.info(f'Complete {__class__!s}: {sys._getframe().f_code.co_name}') diff --git a/src/geophires_x/OutputsAddOns.py b/src/geophires_x/OutputsAddOns.py index 1ee2caff..d9b66ab9 100644 --- a/src/geophires_x/OutputsAddOns.py +++ b/src/geophires_x/OutputsAddOns.py @@ -1,7 +1,9 @@ +from __future__ import annotations + import sys import pandas as pd from geophires_x.Outputs import Outputs -from geophires_x.OutputsRich import OutputTableItem +from geophires_x.OutputsUtils import OutputTableItem NL = "\n" @@ -10,13 +12,13 @@ class OutputsAddOns(Outputs): """ Class to handle output of the AddOns values """ - def PrintOutputs(self, model) -> tuple: + def PrintOutputs(self, model) -> tuple[pd.DataFrame, list]: """ The PrintOutputs function prints the results of the AddOns to a text file and to the screen. :param model: Model: The container class of the application, giving access to everything else, including the logger :type model: :class:`~geophires_x.Model.Model` - :return: None + :return: tuple of addon_df, addon_results """ model.logger.info(f'Init {str(__class__)}: {__name__}') @@ -114,7 +116,7 @@ def PrintOutputs(self, model) -> tuple: print(err_msg) model.logger.critical(str(ex)) model.logger.critical(err_msg) - sys.exit() + raise RuntimeError(err_msg) from ex model.logger.info(f'Complete {str(__class__)}: {__name__}') diff --git a/src/geophires_x/OutputsRich.py b/src/geophires_x/OutputsRich.py index d4e2b6ec..cc9e45bc 100644 --- a/src/geophires_x/OutputsRich.py +++ b/src/geophires_x/OutputsRich.py @@ -1,4 +1,3 @@ -import dataclasses import datetime import string import time @@ -20,6 +19,7 @@ from geophires_x.MatplotlibUtils import plt_subplots from geophires_x.OptionList import EndUseOptions, PlantType, EconomicModel, ReservoirModel, FractureShape, \ ReservoirVolume +from geophires_x.OutputsUtils import OutputTableItem from geophires_x.Parameter import intParameter, strParameter @@ -30,21 +30,14 @@ VERTICAL_WELL_DEPTH_OUTPUT_NAME = 'Well depth' -@dataclasses.dataclass -class OutputTableItem: - parameter: str = '' - value: str = '' - units: str = '' - - def __init__(self, parameter: str, value: str = '', units: str = ''): - self.parameter = parameter - self.value = value - self.units = units - if self.units: - self.units = UpgradeSymbologyOfUnits(self.units) - - -def print_outputs_rich(output_file: str, text_output_file: strParameter, html_output_file: strParameter, model: Model): +def print_outputs_rich( + text_output_file: strParameter, + html_output_file: strParameter, + model: Model, + sdac_results: list, + addon_results: list, + sdac_df: pd.DataFrame, + addon_df: pd.DataFrame): """ TODO Implementation of rich output in this method/file is duplicative of Outputs.PrintOutputs. This adds undue code complexity, maintenance overhead, inconsistency, and potential for bugs. Rich output should instead be @@ -63,9 +56,7 @@ def print_outputs_rich(output_file: str, text_output_file: strParameter, html_ou CAPEX = [] OPEX = [] surface_equipment_results = [] - addon_results = [] - sdac_resa_results = [] - pumping_power_results = [] + # addon_results = [] simulation_metadata.append(OutputTableItem('GEOPHIRES Version', geophires_x.__version__)) simulation_metadata.append(OutputTableItem('Simulation Date', datetime.datetime.now().strftime('%Y-%m-%d'))) @@ -892,18 +883,8 @@ def print_outputs_rich(output_file: str, text_output_file: strParameter, html_ou pumping_power_profiles = pumping_power_profiles.reset_index() - addon_df = pd.DataFrame() - sdac_df = pd.DataFrame() - addon_results: list[OutputTableItem] = [] - sdac_results: list[OutputTableItem] = [] - - if model.economics.DoAddOnCalculations.value: - addon_df, addon_results = model.addoutputs.PrintOutputs(model) - if model.economics.DoSDACGTCalculations.value: - sdac_df, sdac_results = model.sdacgtoutputs.PrintOutputs(model) - if text_output_file.Provided: - Write_Text_Output(output_file, simulation_metadata, summary, economic_parameters, + Write_Text_Output(text_output_file.value, simulation_metadata, summary, economic_parameters, engineering_parameters, resource_characteristics, reservoir_parameters, reservoir_stimulation_results, CAPEX, OPEX, @@ -911,7 +892,7 @@ def print_outputs_rich(output_file: str, text_output_file: strParameter, html_ou pumping_power_profiles, sdac_df, addon_df) # Get rid of any trailing spaces in that output file - they are confusing the testing code - with open(output_file, 'r+') as fp: + with open(text_output_file.value, 'r+') as fp: lines = fp.readlines() fp.seek(0) fp.truncate() diff --git a/src/geophires_x/OutputsS_DAC_GT.py b/src/geophires_x/OutputsS_DAC_GT.py index 7bdc59df..ba283858 100644 --- a/src/geophires_x/OutputsS_DAC_GT.py +++ b/src/geophires_x/OutputsS_DAC_GT.py @@ -1,7 +1,7 @@ import sys import pandas as pd from geophires_x.Outputs import Outputs -from geophires_x.OutputsRich import OutputTableItem +from geophires_x.OutputsUtils import OutputTableItem NL = "\n" diff --git a/src/geophires_x/OutputsUtils.py b/src/geophires_x/OutputsUtils.py new file mode 100644 index 00000000..1a920f56 --- /dev/null +++ b/src/geophires_x/OutputsUtils.py @@ -0,0 +1,17 @@ +import dataclasses + +from geophires_x.GeoPHIRESUtils import UpgradeSymbologyOfUnits + + +@dataclasses.dataclass +class OutputTableItem: + parameter: str = '' + value: str = '' + units: str = '' + + def __init__(self, parameter: str, value: str = '', units: str = ''): + self.parameter = parameter + self.value = value + self.units = units + if self.units: + self.units = UpgradeSymbologyOfUnits(self.units) diff --git a/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index 2037d6cd..eaf2a48e 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.9.43' +__version__ = '3.9.47' diff --git a/src/geophires_x_client/geophires_x_result.py b/src/geophires_x_client/geophires_x_result.py index ea65817b..c44e3b2f 100644 --- a/src/geophires_x_client/geophires_x_result.py +++ b/src/geophires_x_client/geophires_x_result.py @@ -40,6 +40,7 @@ class GeophiresXResult: _StringValueField('End-Use Option'), _StringValueField('End-Use'), _StringValueField('Surface Application'), + _EqualSignDelimitedField('Reservoir Model'), # SUTRA only 'Average Net Electricity Production', 'Electricity breakeven price', 'Total CAPEX', @@ -261,6 +262,7 @@ class GeophiresXResult: 'Exploration costs', 'Investment Tax Credit', 'Inflation costs during construction', + 'Total Add-on CAPEX', 'Total capital costs', 'Annualized capital costs', # AGS/CLGS @@ -289,6 +291,7 @@ class GeophiresXResult: 'Average annual auxiliary fuel cost', 'Average annual pumping cost', 'Redrilling costs', + 'Total Add-on OPEX', 'Total average annual O&M costs', 'Total operating and maintenance costs', # AGS/CLGS @@ -379,9 +382,12 @@ def __init__(self, output_file_path, logger_name=None): self._logger = _get_logger(logger_name) self.output_file_path = output_file_path - f = open(self.output_file_path) - self._lines = list(f.readlines()) - f.close() + with open(self.output_file_path, encoding='utf-8') as f: + self._lines = list(f.readlines()) + + self.is_ags_clgs_style_output = '***AGS/CLGS STYLE OUTPUT***' in [_l.strip() for _l in self._lines] + + self._lines_by_category = self._get_lines_by_category() # TODO generic-er result value map @@ -391,19 +397,27 @@ def __init__(self, output_file_path, logger_name=None): fields = category_fields[1] self.result[category] = {} + category_lines = self._lines_by_category.get(category, []) + search_lines = category_lines if not self.is_ags_clgs_style_output else self._lines + for field in fields: if isinstance(field, _EqualSignDelimitedField): - self.result[category][field.field_name] = self._get_equal_sign_delimited_field(field.field_name) + self.result[category][field.field_name] = self._get_equal_sign_delimited_field( + field.field_name, search_lines=search_lines + ) elif isinstance(field, _UnlabeledStringField): self.result[category][field.field_name] = self._get_unlabeled_string_field( - field.field_name, field.marker_prefixes + field.field_name, field.marker_prefixes, search_lines=search_lines ) else: is_string_field = isinstance(field, _StringValueField) field_name = field.field_name if is_string_field else field indent = 4 if category != 'Simulation Metadata' else 1 self.result[category][field_name] = self._get_result_field( - field_name, is_string_value_field=is_string_field, min_indentation_spaces=indent + field_name, + is_string_value_field=is_string_field, + min_indentation_spaces=indent, + search_lines=search_lines, ) try: @@ -444,6 +458,41 @@ def __init__(self, output_file_path, logger_name=None): if self._get_end_use_option() is not None: self.result['metadata']['End-Use Option'] = self._get_end_use_option().name + def _get_lines_by_category(self) -> dict[str, list[str]]: + """ + Parses the raw output file lines into a dictionary where keys are + category headers and values are the lines belonging to that category. + """ + lines_by_category = {} + current_category = None + known_headers = list(self._RESULT_FIELDS_BY_CATEGORY.keys()) + + for line in self._lines: + + def get_header_content(h_: str) -> str: + """ + TODO adjust this to also work with AGS/CLGS-style headers like '### Cost Results ###' + For now, AGS-style results are parsed from all lines according to the categories defined in + _RESULT_FIELDS_BY_CATEGORY. + """ + + if h_ == 'Simulation Metadata': + return h_ + return f'***{h_}***' + + # Check if the line is a category header + found_header = next((h for h in known_headers if get_header_content(h) == line.strip()), None) + + if found_header: + current_category = found_header + if current_category not in lines_by_category: + lines_by_category[current_category] = [] + elif current_category: + # Append the line to the current category if one has been found + lines_by_category[current_category].append(line) + + return lines_by_category + @property def direct_use_heat_breakeven_price_USD_per_MMBTU(self): summary = self.result['SUMMARY OF RESULTS'] @@ -550,9 +599,18 @@ def _json_fields(self) -> MappingProxyType: except FileNotFoundError: return {} - def _get_result_field(self, field_name: str, is_string_value_field: bool = False, min_indentation_spaces: int = 4): + def _get_result_field( + self, + field_name: str, + is_string_value_field: bool = False, + min_indentation_spaces: int = 4, + search_lines: list[str] | None = None, + ): + if search_lines is None: + search_lines = self._lines + # TODO make this less fragile with proper regex - matching_lines = set(filter(lambda line: f'{min_indentation_spaces * " "}{field_name}: ' in line, self._lines)) + matching_lines = set(filter(lambda line: f'{min_indentation_spaces * " "}{field_name}: ' in line, search_lines)) if len(matching_lines) == 0: self._logger.debug(f'Field not found: {field_name}') @@ -590,14 +648,17 @@ def normalize_spaces(matched_line): return {'value': self._parse_number(str_val, field=f'field "{field_name}"'), 'unit': unit} - def _get_equal_sign_delimited_field(self, field_name): + def _get_equal_sign_delimited_field(self, field_name, search_lines: list[str] | None = None): + if search_lines is None: + search_lines = self._lines + metadata_markers = ( f' {field_name} = ', # Previous versions of GEOPHIRES erroneously included an extra space after the field name so we include # the pattern for it for backwards compatibility with existing .out files. f' {field_name} = ', ) - matching_lines = set(filter(lambda line: any(m in line for m in metadata_markers), self._lines)) + matching_lines = set(filter(lambda line: any(m in line for m in metadata_markers), search_lines)) if len(matching_lines) == 0: self._logger.debug(f'Equal sign-delimited field not found: {field_name}') @@ -617,8 +678,13 @@ def _get_equal_sign_delimited_field(self, field_name): self._logger.error(f'Unexpected error extracting equal sign-delimited field {field_name}') # Shouldn't happen return None - def _get_unlabeled_string_field(self, field_name: str, marker_prefixes: list[str]): - matching_lines = set(filter(lambda line: any(m in line for m in marker_prefixes), self._lines)) + def _get_unlabeled_string_field( + self, field_name: str, marker_prefixes: list[str], search_lines: list[str] | None = None + ): + if search_lines is None: + search_lines = self._lines + + matching_lines = set(filter(lambda line: any(m in line for m in marker_prefixes), search_lines)) if len(matching_lines) == 0: self._logger.debug(f'Unlabeled string field not found: {field_name}') diff --git a/src/geophires_x_schema_generator/geophires-result.json b/src/geophires_x_schema_generator/geophires-result.json index c8615020..9b2251cc 100644 --- a/src/geophires_x_schema_generator/geophires-result.json +++ b/src/geophires_x_schema_generator/geophires-result.json @@ -11,6 +11,7 @@ "End-Use Option": {}, "End-Use": {}, "Surface Application": {}, + "Reservoir Model": {}, "Average Net Electricity Production": {}, "Electricity breakeven price": { "type": "number", @@ -152,8 +153,16 @@ "Project VIR=PI=PIR (including AddOns)": {}, "Project MOIC (including AddOns)": {}, "Project Payback Period (including AddOns)": {}, - "Total Add-on CAPEX": {}, - "Total Add-on OPEX": {}, + "Total Add-on CAPEX": { + "type": "number", + "description": "AddOn CAPEX Total", + "units": "MUSD" + }, + "Total Add-on OPEX": { + "type": "number", + "description": "AddOn OPEX Total Per Year", + "units": "MUSD/yr" + }, "Total Add-on Net Elec": {}, "Total Add-on Net Heat": {}, "Total Add-on Profit": {}, @@ -402,6 +411,11 @@ "description": "The calculated amount of cost escalation due to inflation over the construction period.", "units": "MUSD" }, + "Total Add-on CAPEX": { + "type": "number", + "description": "AddOn CAPEX Total", + "units": "MUSD" + }, "Total capital costs": { "type": "number", "description": "Total Capital Cost", @@ -460,6 +474,11 @@ "description": "Total redrilling costs over the Plant Lifetime are calculated as (Drilling and completion costs + Stimulation costs) \u00d7 Number of times redrilling. The total is then divided over Plant Lifetime years to calculate Redrilling costs per year.", "units": "MUSD/yr" }, + "Total Add-on OPEX": { + "type": "number", + "description": "AddOn OPEX Total Per Year", + "units": "MUSD/yr" + }, "Total average annual O&M costs": {}, "Total operating and maintenance costs": { "type": "number", diff --git a/tests/example1_addons.csv b/tests/example1_addons.csv index 34b82892..0599d06c 100644 --- a/tests/example1_addons.csv +++ b/tests/example1_addons.csv @@ -71,7 +71,6 @@ RESERVOIR SIMULATION RESULTS,Production Wellbore Heat Transmission Model,,Ramey RESERVOIR SIMULATION RESULTS,Average Production Well Temperature Drop,,3.0,degC RESERVOIR SIMULATION RESULTS,Average Injection Well Pump Pressure Drop,,217.9,kPa RESERVOIR SIMULATION RESULTS,Average Production Well Pump Pressure Drop,,1112.0,kPa -RESERVOIR SIMULATION RESULTS,Average Net Electricity Production,,5.39,MW CAPITAL COSTS (M$),Drilling and completion costs,,17.38,MUSD CAPITAL COSTS (M$),Drilling and completion costs per well,,4.35,MUSD CAPITAL COSTS (M$),Stimulation costs,,3.02,MUSD @@ -99,7 +98,7 @@ SURFACE EQUIPMENT SIMULATION RESULTS,Average Annual Net Electricity Generation,, SURFACE EQUIPMENT SIMULATION RESULTS,Average Pumping Power,,0.2,MW SURFACE EQUIPMENT SIMULATION RESULTS,Initial pumping power/net installed power,,3.82,% SURFACE EQUIPMENT SIMULATION RESULTS,Heat to Power Conversion Efficiency,,10.07,% -Simulation Metadata,GEOPHIRES Version,,3.9.28, +Simulation Metadata,GEOPHIRES Version,,3.9.44, POWER GENERATION PROFILE,THERMAL DRAWDOWN,1,1.0, POWER GENERATION PROFILE,THERMAL DRAWDOWN,2,1.0056, POWER GENERATION PROFILE,THERMAL DRAWDOWN,3,1.0073, diff --git a/tests/examples/example1_addons.out b/tests/examples/example1_addons.out index 6cadf1d9..a29a34fa 100644 --- a/tests/examples/example1_addons.out +++ b/tests/examples/example1_addons.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.9.28 - Simulation Date: 2025-07-02 - Simulation Time: 12:18 - Calculation Time: 0.882 sec + GEOPHIRES Version: 3.9.44 + Simulation Date: 2025-07-29 + Simulation Time: 06:46 + Calculation Time: 0.898 sec ***SUMMARY OF RESULTS*** @@ -26,7 +26,7 @@ Simulation Metadata Economic Model = Fixed Charge Rate (FCR) Fixed Charge Rate (FCR): 5.00 - Accrued financing during construction: 0.00 % + Accrued financing during construction: 0.00 % Project lifetime: 30 yr Capacity factor: 90.0 % Project NPV: 72.08 MUSD diff --git a/tests/examples/example_SAM-single-owner-PPA-3.out b/tests/examples/example_SAM-single-owner-PPA-3.out new file mode 100644 index 00000000..50a70f80 --- /dev/null +++ b/tests/examples/example_SAM-single-owner-PPA-3.out @@ -0,0 +1,415 @@ + ***************** + ***CASE REPORT*** + ***************** + +Simulation Metadata +---------------------- + GEOPHIRES Version: 3.9.43 + Simulation Date: 2025-07-28 + Simulation Time: 13:40 + Calculation Time: 1.157 sec + + ***SUMMARY OF RESULTS*** + + End-Use Option: Electricity + Average Net Electricity Production: 58.87 MW + Electricity breakeven price: 7.64 cents/kWh + Total CAPEX: 275.47 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: 210.63 MUSD + After-tax IRR: 30.00 % + Project VIR=PI=PIR: 2.27 + Project MOIC: 5.70 + Project Payback Period: 2.94 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: 13.12 MUSD + Total Add-on CAPEX: 50.00 MUSD + Total CAPEX: 275.47 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 + Total Add-on OPEX: 1.00 MUSD/yr + Total operating and maintenance costs: 7.60 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 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 +Salvage value ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 137,736,712 +Total revenue ($) 0 51,751,456 51,964,904 53,519,865 55,051,439 56,573,241 58,089,812 59,603,196 61,114,476 62,624,287 64,133,030 65,640,973 67,148,303 68,655,154 70,161,626 71,667,791 73,173,709 74,679,427 76,184,986 77,690,409 216,932,127 + +Property tax net assessed value ($) 0 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 + +OPERATING EXPENSES +O&M fixed expense ($) 0 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 +O&M production-based expense ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +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 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 + +EBITDA ($) 0 44,152,314 44,365,761 45,920,723 47,452,297 48,974,099 50,490,670 52,004,054 53,515,334 55,025,144 56,533,887 58,041,831 59,549,161 61,056,012 62,562,483 64,068,649 65,574,566 67,080,284 68,585,843 70,091,267 209,332,985 + +OPERATING ACTIVITIES +EBITDA ($) 0 44,152,314 44,365,761 45,920,723 47,452,297 48,974,099 50,490,670 52,004,054 53,515,334 55,025,144 56,533,887 58,041,831 59,549,161 61,056,012 62,562,483 64,068,649 65,574,566 67,080,284 68,585,843 70,091,267 209,332,985 +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 5,509,468 5,342,848 5,167,896 4,984,197 4,791,313 4,588,785 4,376,130 4,152,842 3,918,390 3,672,216 3,413,733 3,142,325 2,857,348 2,558,121 2,243,933 1,914,036 1,567,643 1,203,932 822,034 421,042 +Cash flow from operating activities ($) 0 38,642,845 39,022,913 40,752,827 42,468,100 44,182,786 45,901,885 47,627,924 49,362,491 51,106,754 52,861,671 54,628,098 56,406,835 58,198,664 60,004,362 61,824,716 63,660,531 65,512,641 67,381,912 69,269,232 208,911,943 + +INVESTING ACTIVITIES +Total installed cost ($) -275,473,424 +Debt closing costs ($) 0 +Debt up-front fee ($) 0 +minus: +Total IBI income ($) 0 +Total CBI income ($) 0 +equals: +Purchase of property ($) -275,473,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 ($) -275,473,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 ($) 165,284,055 +Size of debt ($) 110,189,370 +minus: +Debt principal payment ($) 0 3,332,412 3,499,032 3,673,984 3,857,683 4,050,567 4,253,096 4,465,750 4,689,038 4,923,490 5,169,664 5,428,147 5,699,555 5,984,532 6,283,759 6,597,947 6,927,844 7,274,237 7,637,948 8,019,846 8,420,838 +equals: +Cash flow from financing activities ($) 275,473,424 -3,332,412 -3,499,032 -3,673,984 -3,857,683 -4,050,567 -4,253,096 -4,465,750 -4,689,038 -4,923,490 -5,169,664 -5,428,147 -5,699,555 -5,984,532 -6,283,759 -6,597,947 -6,927,844 -7,274,237 -7,637,948 -8,019,846 -8,420,838 + +PROJECT RETURNS +Pre-tax Cash Flow: +Cash flow from operating activities ($) 0 38,642,845 39,022,913 40,752,827 42,468,100 44,182,786 45,901,885 47,627,924 49,362,491 51,106,754 52,861,671 54,628,098 56,406,835 58,198,664 60,004,362 61,824,716 63,660,531 65,512,641 67,381,912 69,269,232 208,911,943 +Cash flow from investing activities ($) -275,473,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 ($) 275,473,424 -3,332,412 -3,499,032 -3,673,984 -3,857,683 -4,050,567 -4,253,096 -4,465,750 -4,689,038 -4,923,490 -5,169,664 -5,428,147 -5,699,555 -5,984,532 -6,283,759 -6,597,947 -6,927,844 -7,274,237 -7,637,948 -8,019,846 -8,420,838 +Total pre-tax cash flow ($) 0 35,310,434 35,523,881 37,078,843 38,610,417 40,132,219 41,648,789 43,162,174 44,673,453 46,183,264 47,692,007 49,199,951 50,707,281 52,214,132 53,720,603 55,226,769 56,732,686 58,238,404 59,743,963 61,249,387 200,491,104 + +Pre-tax Returns: +Issuance of equity ($) 165,284,055 +Total pre-tax cash flow ($) 0 35,310,434 35,523,881 37,078,843 38,610,417 40,132,219 41,648,789 43,162,174 44,673,453 46,183,264 47,692,007 49,199,951 50,707,281 52,214,132 53,720,603 55,226,769 56,732,686 58,238,404 59,743,963 61,249,387 200,491,104 +Total pre-tax returns ($) -165,284,055 35,310,434 35,523,881 37,078,843 38,610,417 40,132,219 41,648,789 43,162,174 44,673,453 46,183,264 47,692,007 49,199,951 50,707,281 52,214,132 53,720,603 55,226,769 56,732,686 58,238,404 59,743,963 61,249,387 200,491,104 + +After-tax Returns: +Total pre-tax returns ($) -165,284,055 35,310,434 35,523,881 37,078,843 38,610,417 40,132,219 41,648,789 43,162,174 44,673,453 46,183,264 47,692,007 49,199,951 50,707,281 52,214,132 53,720,603 55,226,769 56,732,686 58,238,404 59,743,963 61,249,387 200,491,104 +Federal ITC total income ($) 0 82,642,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 -6,403,699 -5,334,677 -5,672,529 -6,007,522 -6,342,400 -6,678,140 -7,015,235 -7,353,996 -7,694,651 -8,037,386 -8,382,369 -8,729,757 -9,079,701 -9,432,354 -9,787,869 -10,146,403 -10,508,120 -10,873,189 -11,241,783 -38,514,004 +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 -2,295,232 -1,912,071 -2,033,164 -2,153,234 -2,273,262 -2,393,599 -2,514,421 -2,635,841 -2,757,939 -2,880,784 -3,004,433 -3,128,945 -3,254,373 -3,380,772 -3,508,197 -3,636,704 -3,766,351 -3,897,200 -4,029,313 -13,804,303 +Total after-tax returns ($) -165,284,055 109,253,530 28,277,134 29,373,150 30,449,662 31,516,557 32,577,051 33,632,517 34,683,616 35,730,674 36,773,837 37,813,148 38,848,579 39,880,058 40,907,478 41,930,703 42,949,579 43,963,932 44,973,574 45,978,291 148,172,798 + +After-tax cumulative IRR (%) NaN -33.90 -14.01 0.64 10.10 16.19 20.21 22.94 24.84 26.19 27.16 27.87 28.40 28.80 29.10 29.32 29.50 29.63 29.73 29.81 30.0 +After-tax cumulative NPV ($) -165,284,055 -66,106,921 -42,805,225 -20,832,763 -155,799 19,271,800 37,501,025 54,585,116 70,578,228 85,534,586 99,507,908 112,550,973 124,715,298 136,050,903 146,606,134 156,427,530 165,559,744 174,045,485 181,925,494 189,238,538 210,632,437 + +AFTER-TAX LCOE AND PPA PRICE +Annual costs ($) -165,284,055 57,502,074 -23,687,770 -24,146,716 -24,601,778 -25,056,684 -25,512,761 -25,970,679 -26,430,860 -26,893,613 -27,359,192 -27,827,825 -28,299,724 -28,775,096 -29,254,148 -29,737,088 -30,224,130 -30,715,494 -31,211,412 -31,712,118 68,977,383 +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 ($) 298,190,010 +Present value of annual energy nominal (kWh) 3,903,105,303.0 +LCOE Levelized cost of energy nominal (cents/kWh) 7.64 + +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 44,152,314 44,365,761 45,920,723 47,452,297 48,974,099 50,490,670 52,004,054 53,515,334 55,025,144 56,533,887 58,041,831 59,549,161 61,056,012 62,562,483 64,068,649 65,574,566 67,080,284 68,585,843 70,091,267 209,332,985 +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 5,509,468 5,342,848 5,167,896 4,984,197 4,791,313 4,588,785 4,376,130 4,152,842 3,918,390 3,672,216 3,413,733 3,142,325 2,857,348 2,558,121 2,243,933 1,914,036 1,567,643 1,203,932 822,034 421,042 +Total state tax depreciation ($) 0 5,853,810 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 +equals: +State taxable income ($) 0 32,789,035 27,315,293 29,045,206 30,760,479 32,475,165 34,194,264 35,920,304 37,654,871 39,399,133 41,154,051 42,920,477 44,699,215 46,491,044 48,296,742 50,117,095 51,952,910 53,805,020 55,674,291 57,561,612 197,204,322 + +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 -2,295,232 -1,912,071 -2,033,164 -2,153,234 -2,273,262 -2,393,599 -2,514,421 -2,635,841 -2,757,939 -2,880,784 -3,004,433 -3,128,945 -3,254,373 -3,380,772 -3,508,197 -3,636,704 -3,766,351 -3,897,200 -4,029,313 -13,804,303 + +PROJECT FEDERAL INCOME TAXES +EBITDA ($) 0 44,152,314 44,365,761 45,920,723 47,452,297 48,974,099 50,490,670 52,004,054 53,515,334 55,025,144 56,533,887 58,041,831 59,549,161 61,056,012 62,562,483 64,068,649 65,574,566 67,080,284 68,585,843 70,091,267 209,332,985 +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 -2,295,232 -1,912,071 -2,033,164 -2,153,234 -2,273,262 -2,393,599 -2,514,421 -2,635,841 -2,757,939 -2,880,784 -3,004,433 -3,128,945 -3,254,373 -3,380,772 -3,508,197 -3,636,704 -3,766,351 -3,897,200 -4,029,313 -13,804,303 +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 5,509,468 5,342,848 5,167,896 4,984,197 4,791,313 4,588,785 4,376,130 4,152,842 3,918,390 3,672,216 3,413,733 3,142,325 2,857,348 2,558,121 2,243,933 1,914,036 1,567,643 1,203,932 822,034 421,042 +Total federal tax depreciation ($) 0 5,853,810 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 +equals: +Federal taxable income ($) 0 30,493,802 25,403,222 27,012,042 28,607,246 30,201,904 31,800,666 33,405,882 35,019,030 36,641,194 38,273,267 39,916,044 41,570,270 43,236,671 44,915,970 46,608,899 48,316,206 50,038,669 51,777,091 53,532,299 183,400,020 + +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 -6,403,699 -5,334,677 -5,672,529 -6,007,522 -6,342,400 -6,678,140 -7,015,235 -7,353,996 -7,694,651 -8,037,386 -8,382,369 -8,729,757 -9,079,701 -9,432,354 -9,787,869 -10,146,403 -10,508,120 -10,873,189 -11,241,783 -38,514,004 + +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 82,642,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 82,642,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 ($) 110,189,370 106,856,958 103,357,926 99,683,942 95,826,259 91,775,692 87,522,596 83,056,846 78,367,808 73,444,319 68,274,654 62,846,507 57,146,952 51,162,420 44,878,661 38,280,714 31,352,869 24,078,633 16,440,684 8,420,838 0 +Debt interest payment ($) 0 5,509,468 5,342,848 5,167,896 4,984,197 4,791,313 4,588,785 4,376,130 4,152,842 3,918,390 3,672,216 3,413,733 3,142,325 2,857,348 2,558,121 2,243,933 1,914,036 1,567,643 1,203,932 822,034 421,042 +Debt principal payment ($) 0 3,332,412 3,499,032 3,673,984 3,857,683 4,050,567 4,253,096 4,465,750 4,689,038 4,923,490 5,169,664 5,428,147 5,699,555 5,984,532 6,283,759 6,597,947 6,927,844 7,274,237 7,637,948 8,019,846 8,420,838 +Debt total payment ($) 0 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 + +DSCR (DEBT FRACTION) +EBITDA ($) 0 44,152,314 44,365,761 45,920,723 47,452,297 48,974,099 50,490,670 52,004,054 53,515,334 55,025,144 56,533,887 58,041,831 59,549,161 61,056,012 62,562,483 64,068,649 65,574,566 67,080,284 68,585,843 70,091,267 209,332,985 +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 44,152,314 44,365,761 45,920,723 47,452,297 48,974,099 50,490,670 52,004,054 53,515,334 55,025,144 56,533,887 58,041,831 59,549,161 61,056,012 62,562,483 64,068,649 65,574,566 67,080,284 68,585,843 70,091,267 209,332,985 +Debt total payment ($) 0 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 +DSCR (pre-tax) 0.0 4.99 5.02 5.19 5.37 5.54 5.71 5.88 6.05 6.22 6.39 6.56 6.73 6.91 7.08 7.25 7.42 7.59 7.76 7.93 23.68 + +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 +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/tests/examples/example_SAM-single-owner-PPA-3.txt b/tests/examples/example_SAM-single-owner-PPA-3.txt new file mode 100644 index 00000000..792e315a --- /dev/null +++ b/tests/examples/example_SAM-single-owner-PPA-3.txt @@ -0,0 +1,90 @@ +# Example: SAM Single Owner PPA Economic Model: 50 MWe with Add-Ons +# This example models example_SAM-single-owner with a Waste Heat Absorption Chiller Add-on +# See "SAM Economic Models" in GEOPHIRES documentation: https://nrel.github.io/GEOPHIRES-X/SAM-Economic-Models.html#add-ons + +# *** ADD-ONS *** +# *************** +AddOn Nickname 1, Waste Heat Absorption Chiller +AddOn CAPEX 1, 50 +AddOn OPEX 1, 1 +AddOn Profit Gained 1, 15 + + +# *** ECONOMIC/FINANCIAL PARAMETERS *** +# ************************************* +Economic Model, 5, -- SAM Single Owner PPA +Capital Cost for Power Plant for Electricity Generation, 1900 + +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 + + +# *** 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-result_example-3.csv b/tests/geophires-result_example-3.csv index 59715605..47104012 100644 --- a/tests/geophires-result_example-3.csv +++ b/tests/geophires-result_example-3.csv @@ -51,7 +51,6 @@ RESERVOIR SIMULATION RESULTS,Average Reservoir Heat Extraction,,133.3,MW RESERVOIR SIMULATION RESULTS,Production Wellbore Heat Transmission Model,,Ramey Model, RESERVOIR SIMULATION RESULTS,Average Production Well Temperature Drop,,3.6,degC RESERVOIR SIMULATION RESULTS,Average Injection Well Pump Pressure Drop,,650.2,kPa -RESERVOIR SIMULATION RESULTS,Average Net Electricity Production,,19.7,MW CAPITAL COSTS (M$),Drilling and completion costs,,34.45,MUSD CAPITAL COSTS (M$),Drilling and completion costs per well,,5.74,MUSD CAPITAL COSTS (M$),Stimulation costs,,4.53,MUSD diff --git a/tests/geophires_x_client_tests/test_geophires_x_result.py b/tests/geophires_x_client_tests/test_geophires_x_result.py index 9185ac47..0bf5fab2 100644 --- a/tests/geophires_x_client_tests/test_geophires_x_result.py +++ b/tests/geophires_x_client_tests/test_geophires_x_result.py @@ -16,6 +16,12 @@ def test_get_sam_cash_flow_row_name_unit_split(self) -> None: actual = GeophiresXResult._get_sam_cash_flow_row_name_unit_split(case[0]) self.assertListEqual(actual, case[1]) + def test_get_lines_by_category(self) -> None: + r: GeophiresXResult = GeophiresXResult(self._get_test_file_path('../examples/example2.out')) + lines_by_cat = r._get_lines_by_category() + res_params_lines = lines_by_cat['RESERVOIR PARAMETERS'] + self.assertGreater(len(res_params_lines), 0) + def test_reservoir_volume_calculation_note(self) -> None: r: GeophiresXResult = GeophiresXResult(self._get_test_file_path('../examples/example2.out')) field_name = 'Reservoir volume calculation note' @@ -37,3 +43,27 @@ def test_sam_economic_model_result_csv(self) -> None: r: GeophiresXResult = GeophiresXResult(self._get_test_file_path('sam-em-csv-test.out')) as_csv = r.as_csv() self.assertIsNotNone(as_csv) + + def test_multicategory_fields_only_in_case_report_category(self) -> None: + r: GeophiresXResult = GeophiresXResult( + self._get_test_file_path('../examples/example_SAM-single-owner-PPA-3.out') + ) + self.assertIsNone(r.result['EXTENDED ECONOMICS']['Total Add-on CAPEX']) + self.assertIsNone(r.result['EXTENDED ECONOMICS']['Total Add-on OPEX']) + + self.assertIn('Total Add-on CAPEX', r.result['CAPITAL COSTS (M$)']) + self.assertIn('Total Add-on OPEX', r.result['OPERATING AND MAINTENANCE COSTS (M$/yr)']) + + self.assertIsNone(r.result['RESERVOIR SIMULATION RESULTS']['Average Net Electricity Production']) + self.assertIsNotNone(r.result['SUMMARY OF RESULTS']['Average Net Electricity Production']) + self.assertIsNotNone(r.result['SURFACE EQUIPMENT SIMULATION RESULTS']['Average Net Electricity Generation']) + + def test_ags_clgs_style_output(self) -> None: + r: GeophiresXResult = GeophiresXResult( + self._get_test_file_path('../examples/Beckers_et_al_2023_Tabulated_Database_Uloop_sCO2_elec.out') + ) + self.assertIsNotNone(r.result['SUMMARY OF RESULTS']['LCOE']) + + def test_sutra_reservoir_model_in_summary(self) -> None: + r: GeophiresXResult = GeophiresXResult(self._get_test_file_path('../examples/SUTRAExample1.out')) + self.assertEqual('SUTRA Model', r.result['SUMMARY OF RESULTS']['Reservoir Model']) diff --git a/tests/geophires_x_tests/egs-sam-em-add-ons.txt b/tests/geophires_x_tests/egs-sam-em-add-ons.txt new file mode 100644 index 00000000..e5d2c0a4 --- /dev/null +++ b/tests/geophires_x_tests/egs-sam-em-add-ons.txt @@ -0,0 +1,66 @@ + Reservoir Model, 1 +Reservoir Volume Option, 1 +Reservoir Density, 2800 +Reservoir Heat Capacity, 790 +Reservoir Thermal Conductivity, 3.05 +Reservoir Porosity, 0.0118 +Reservoir Impedance, 0.001 + +Number of Fractures, 149 +Fracture Shape, 4 +Fracture Height, 2000 +Fracture Width, 10000 +Fracture Separation, 30 + +Number of Segments, 1 + +Production Well Diameter, 7 +Injection Well Diameter, 7 +Well Separation, 365 feet +Injection Temperature, 60 degC +Injection Wellbore Temperature Gain, 3 +Plant Outlet Pressure, 1000 psi +Ramey Production Wellbore Model, 1 +Utilization Factor, .9 +Water Loss Fraction, 0.05 +Maximum Drawdown, 1 +Ambient Temperature, 10 degC +#Surface Temperature, 10 degC +End-Use Option, 1 + +Plant Lifetime, 25 + +Circulation Pump Efficiency, 0.80 + +Economic Model, 5 +Starting Electricity Sale Price, 0.15 +Ending Electricity Sale Price, 1.00 +Electricity Escalation Rate Per Year, 0.004053223 +Electricity Escalation Start Year, 1 +Fraction of Investment in Bonds, .5 +Combined Income Tax Rate, .3 +Inflated Bond Interest Rate, .05 +Inflation Rate, .02 +Investment Tax Credit Rate, .3, -- https://programs.dsireusa.org/system/program/detail/658 +Production Tax Credit Electricity, 0.0275, -- https://programs.dsireusa.org/system/program/detail/734 +Property Tax Rate, 0 +Time steps per year, 10 +Maximum Temperature, 500 + + +Print Output to Console, 0 +Surface Temperature, 12 +Reservoir Depth, 5.4 +Gradient 1, 36.7 +Power Plant Type, 4 + +Number of Injection Wells, 54 +Number of Production Wells, 54 +Production Flow Rate per Well, 80 + + +# AddOns +AddOn Nickname 1, Desalinization +AddOn CAPEX 1, 562 +AddOn OPEX 1, 0.1 +AddOn Profit Gained 1, 107 diff --git a/tests/geophires_x_tests/test_economics_sam.py b/tests/geophires_x_tests/test_economics_sam.py index fd0abccd..cbb01772 100644 --- a/tests/geophires_x_tests/test_economics_sam.py +++ b/tests/geophires_x_tests/test_economics_sam.py @@ -572,6 +572,47 @@ def _accrued_financing(_r: GeophiresXResult) -> float: # ) # self.assertEqual(15.0, _accrued_financing(r4)) + def test_add_ons(self): + add_ons_result = self._get_result( + {'Do AddOn Calculations': True}, file_path=self._get_test_file_path('egs-sam-em-add-ons.txt') + ) + no_add_ons_result = self._get_result( + {'Do AddOn Calculations': False}, file_path=self._get_test_file_path('egs-sam-em-add-ons.txt') + ) + self.assertIsNotNone(add_ons_result) + + self.assertGreater( + add_ons_result.result['SUMMARY OF RESULTS']['Total CAPEX']['value'], + no_add_ons_result.result['SUMMARY OF RESULTS']['Total CAPEX']['value'], + ) + + self.assertGreater(add_ons_result.result['CAPITAL COSTS (M$)']['Total Add-on CAPEX']['value'], 0) + + self.assertGreater( + add_ons_result.result['OPERATING AND MAINTENANCE COSTS (M$/yr)']['Total operating and maintenance costs'][ + 'value' + ], + no_add_ons_result.result['OPERATING AND MAINTENANCE COSTS (M$/yr)'][ + 'Total operating and maintenance costs' + ]['value'], + ) + + self.assertGreater( + add_ons_result.result['OPERATING AND MAINTENANCE COSTS (M$/yr)']['Total Add-on OPEX']['value'], 0 + ) + + with self.assertRaises(RuntimeError, msg='AddOn Electricity is not supported for SAM Economic Models'): + self._get_result( + {'Do AddOn Calculations': True, 'AddOn Electricity Gained 1': 100}, + file_path=self._get_test_file_path('egs-sam-em-add-ons.txt'), + ) + + with self.assertRaises(RuntimeError, msg='AddOn Heat is not supported for SAM Economic Models'): + self._get_result( + {'Do AddOn Calculations': True, 'AddOn Heat Gained 1': 100}, + file_path=self._get_test_file_path('egs-sam-em-add-ons.txt'), + ) + @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/regenerate-example-result.sh b/tests/regenerate-example-result.sh index be7624a7..faef5cee 100755 --- a/tests/regenerate-example-result.sh +++ b/tests/regenerate-example-result.sh @@ -16,5 +16,5 @@ rm examples/$1.json if [[ $1 == "example1_addons" ]] then echo "Updating CSV..." - python regenerate_example_result_csv.py + python regenerate_example_result_csv.py example1_addons fi diff --git a/tests/regenerate_example_result_csv.py b/tests/regenerate_example_result_csv.py index 0c8319a9..30a86306 100644 --- a/tests/regenerate_example_result_csv.py +++ b/tests/regenerate_example_result_csv.py @@ -1,3 +1,4 @@ +import argparse import os from geophires_x_client import GeophiresXResult @@ -8,5 +9,18 @@ def _get_file_path(file_name: str) -> str: if __name__ == '__main__': - with open(_get_file_path('example1_addons.csv'), 'w', encoding='utf-8') as csvfile: - csvfile.write(GeophiresXResult(_get_file_path('examples/example1_addons.out')).as_csv()) + parser = argparse.ArgumentParser(description='Regenerate a CSV result file from a GEOPHIRES-X example .out file.') + parser.add_argument( + 'example_name', + type=str, + nargs='?', # Makes the argument optional + default='example1_addons', + help='The base name of the example file (e.g., "example1_addons"). Defaults to "example1_addons".', + ) + args = parser.parse_args() + + example_name = args.example_name + example_relative_path = f'{"examples/" if example_name.startswith("example") else ""}{example_name}.out' + + with open(_get_file_path(f'{example_name}.csv'), 'w', encoding='utf-8') as csvfile: + csvfile.write(GeophiresXResult(_get_file_path(example_relative_path)).as_csv())