diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 10a7975f..619fde4a 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.9.19 +current_version = 3.9.26 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index 4dc02392..f34c1067 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.19 + version: 3.9.26 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" diff --git a/README.rst b/README.rst index 3e19cc79..e1728800 100644 --- a/README.rst +++ b/README.rst @@ -56,9 +56,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.19.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.9.26.svg :alt: Commits since latest release - :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.19...main + :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.26...main .. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat :target: https://nrel.github.io/GEOPHIRES-X diff --git a/docs/Fervo_Project_Cape-4.md b/docs/Fervo_Project_Cape-4.md index b6d0d5a2..06354385 100644 --- a/docs/Fervo_Project_Cape-4.md +++ b/docs/Fervo_Project_Cape-4.md @@ -8,7 +8,7 @@ Financial results are calculated using the [SAM Single Owner PPA Economic Model](https://softwareengineerprogrammer.github.io/GEOPHIRES/SAM-Economic-Models.html#sam-single-owner-ppa). -Key case study results include LCOE = $76.5/MWh and CAPEX = $4350/kW. +Key case study results include LCOE = $75.5/MWh and CAPEX = $4290/kW. [Click here](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=Fervo_Project_Cape-4) to interactively explore the case study in the GEOPHIRES web interface. @@ -41,20 +41,20 @@ in source code for the full set of inputs. ### Economic Parameters -| Parameter | Input Value(s) | Source | -|-----------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Economic Model | SAM Single Owner PPA | The SAM Single Owner PPA economic model is used to calculate financial results including LCOE, NPV, IRR, and pro-forma cash flow analysis. See [GEOPHIRES documentation of SAM Economic Models](https://softwareengineerprogrammer.github.io/GEOPHIRES/SAM-Economic-Models.html) for details on how System Advisor Model financial models are integrated into GEOPHIRES. | -| Inflation Rate | 2.3% | US inflation rate as of April 2025 | -| PPA Price | Starting at 9.5 cents/kWh, escalating to 10 cents/kWh by project year 11 | Upper end of ranges given in 2024 NREL ATB (NREL, 2024). Both PPAs 'firm for 10 years at less than $100/MWh' estimate given in a podcast. | -| Well Drilling Cost Correlation & Adjustment Factor | Vertical large baseline correlation + adjustment factor = 0.84 to align with Fervo claimed drilling costs of <$4M/well | Akindipe & Witter, 2025; Latimer, 2025. | -| Reservoir Stimulation Capital Cost Adjustment Factor | 2.66 | Estimated cost of ~$2M per well. Typical range for Nth-of-kind projects may be $0.5–2M. | -| Capital Cost for Power Plant for Electricity Generation | $1900/kW | US DOE, 2021. | -| Discount Rate | 12% | Typical discount rates for high-risk projects may be 12–15% | -| Inflated Bond Interest Rate | 5.6% | Typical debt annual interest rate | -| Fraction of Investment in Bonds (percent debt vs. equity) | 60% | Approximate remaining percentage of CAPEX with $1 billion sponsor equity per Matson, 2024. Note that this source says that Fervo ultimately wants to target "15% sponsor equity, 15% bridge loan, and 70% construction to term loans", but this case study does not attempt to model that capital structure. | -| Exploration Capital Cost | $30M | Estimate significantly higher exploration costs than default correlation in consideration of potential risks associated with second/third/fourth-of-a-kind EGS projects | -| Investment Tax Credit Rate (ITC) | 30% | Same as 400 MWe case study (Fervo_Project_Cape-3) | -| Inflation Rate During Construction (additional indirect capital cost) | 15% | Estimate high indirect capital costs in consideration of potential risks associated with unforeseen engineering challenges or construction delays | +| Parameter | Input Value(s) | Source | +|-----------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Economic Model | SAM Single Owner PPA | The SAM Single Owner PPA economic model is used to calculate financial results including LCOE, NPV, IRR, and pro-forma cash flow analysis. See [GEOPHIRES documentation of SAM Economic Models](https://softwareengineerprogrammer.github.io/GEOPHIRES/SAM-Economic-Models.html) for details on how System Advisor Model financial models are integrated into GEOPHIRES. | +| Inflation Rate | 2.3% | US inflation rate as of April 2025 | +| PPA Price | Starting at 9.5 cents/kWh, escalating to 10 cents/kWh by project year 11 | Upper end of ranges given in 2024 NREL ATB (NREL, 2024). Both PPAs 'firm for 10 years at less than $100/MWh' estimate given in a podcast. | +| Well Drilling Cost Correlation & Adjustment Factor | Vertical large baseline correlation + adjustment factor = 0.8 to align with Fervo claimed drilling costs of <$4M/well | Akindipe & Witter, 2025; Latimer, 2025. | +| Reservoir Stimulation Capital Cost Adjustment Factor | 2.66 | Estimated cost of ~$2M per well. Typical range for Nth-of-kind projects may be $0.5–2M. | +| Capital Cost for Power Plant for Electricity Generation | $1900/kW | US DOE, 2021. | +| Discount Rate | 12% | Typical discount rates for high-risk projects may be 12–15% | +| Inflated Bond Interest Rate | 5.6% | Typical debt annual interest rate | +| Fraction of Investment in Bonds (percent debt vs. equity) | 60% | Approximate remaining percentage of CAPEX with $1 billion sponsor equity per Matson, 2024. Note that this source says that Fervo ultimately wants to target "15% sponsor equity, 15% bridge loan, and 70% construction to term loans", but this case study does not attempt to model that capital structure. | +| Exploration Capital Cost | $30M | Estimate significantly higher exploration costs than default correlation in consideration of potential risks associated with second/third/fourth-of-a-kind EGS projects | +| Investment Tax Credit Rate (ITC) | 30% | Same as 400 MWe case study (Fervo_Project_Cape-3) | +| Inflation Rate During Construction (additional indirect capital cost) | 15% | Estimate high indirect capital costs in consideration of potential risks associated with unforeseen engineering challenges or construction delays | ### Technical & Engineering Parameters @@ -67,7 +67,7 @@ in source code for the full set of inputs. | Number of Fractures per well | 102 | Estimate. (Note this is not a direct GEOPHIRES input parameter but was used to calculate other case study GEOPHIRES input parameters such as reservoir volume.) | | Fracture Separation | 18 m | Per Norbeck et al, 2024: lateral length is 4700 ft = 1432 m. Dividing 1432 by 80 = ~18 m fracture spacing. | | Fracture Geometry | 165.3 m × 165.3 m (Square) | Extrapolated from 30 million ft² fracture surface area per well per Fercho et al, 2025. | -| Reservoir Volume | 5,418,039,158 m³ | Calculated from fracture area (27,324.09 m²) × fracture separation (18 m) × targeted number of fractures per well (102) | +| Reservoir Volume | 5,919,217,617 m³ | Calculated from fracture area (27,324.09 m²) × fracture separation (18 m) × targeted number of fractures per well (102) | | Water Loss Rate | 15% | Water loss rate is conservatively estimated to be between 10 and 20%. Other estimates and some simulations may suggest a significantly lower water loss rate than this conservative estimate. See [Geothermal Mythbusting: Water Use and Impacts](https://fervoenergy.com/geothermal-mythbusting-water-use-and-impacts/) (Fervo Energy, 2025). | | Maximum Drawdown | 0.0153 | Tuned to keep minimum net electricity generation ≥ 500 MWe and thermal breakthrough requiring redrilling occurring every 5–10 years | | Reservoir Impedance | 0.001565 GPa.s/m³ | Yields ~15% initial pumping power/net installed power | @@ -82,18 +82,18 @@ in source code for the complete results. | Metric | Result Value | Reference Value(s) | Reference Source | |------------------------------------|----------------------------------------------------------|--------------------------|---------------------------------------------| -| LCOE | $76.5/MWh | $80/MWh | Horne et al, 2025 | -| Project capital costs: Total CAPEX | $2.67B | | | -| Project capital costs: $/kW | $4350/kW (based on maximum total electricity generation) | $4500/kW, $3000–$6000/kW | Horne et al, 2025; Latimer, 2025. | -| Well Drilling and Completion Cost | $3.96M/well | $<4M/well | Latimer, 2025. | +| LCOE | $75.5/MWh | $80/MWh | Horne et al, 2025 | +| Project capital costs: Total CAPEX | $2.64B | | | +| Project capital costs: $/kW | $4290/kW (based on maximum total electricity generation) | $4500/kW, $3000–$6000/kW | Horne et al, 2025; Latimer, 2025. | +| Well Drilling and Completion Cost | $3.96M/well (including 5% indirect costs) | $<4M/well | Latimer, 2025. | | WACC | 8.3% | 8.3% | Matson, 2024. | -| After-tax IRR | 30.7% | 15–25% | Typical levered returns for energy projects | +| After-tax IRR | 31.5% | 15–25% | Typical levered returns for energy projects | ### Technical & Engineering Results | Metric | Result Value | Reference Value(s) | Reference Source | |-------------------------------------------------|--------------|----------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Minimum Net Electricity Generation | 503 MW | 500 MW | Fervo Energy, 2025. The 500 MW PPA is interpreted to mean that Cape Station's net electricity generation must never fall below 500 MWe. | +| Minimum Net Electricity Generation | 504 MW | 500 MW | Fervo Energy, 2025. The 500 MW PPA is interpreted to mean that Cape Station's net electricity generation must never fall below 500 MWe. | | Maximum Total Electricity Generation | 615 MW | | Actual maximum total generation may be bounded or constrained by modular power plant design not modeled in this case study. For example, a modular design with 50MW units may constrain maximum total generation to 600 MW. | | Number of times redrilling | 3 | 3–6 | Redrilling expected to be required within 5–10 years of project start | | Average Production Temperature | 199℃ | 204℃, 190.6–198.6℃ (optimal plant operating range) | Trent, 2024; Norbeck et al, 2024. | diff --git a/docs/conf.py b/docs/conf.py index 83c14c14..edf43a60 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.19' +version = release = '3.9.26' pygments_style = 'trac' templates_path = ['./templates'] diff --git a/setup.py b/setup.py index 3f4d32a6..ebb2065e 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='geophires-x', - version='3.9.19', + version='3.9.26', 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 660ff1ea..5cf3f02a 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -1125,17 +1125,17 @@ def __init__(self, model: Model): ErrMessage="assume default peaking boiler efficiency (85%)", ToolTipText="Peaking boiler efficiency" ) - self._default_peaking_boiler_cost_USD_per_kw = 65 - self.peaking_boiler_cost_per_kw = self.ParameterDict[self.peaking_boiler_cost_per_kw.Name] = floatParameter( - "Peaking Boiler Cost per KW", - DefaultValue=self._default_peaking_boiler_cost_USD_per_kw, + self._default_peaking_boiler_cost_USD_per_kW = 65 + self.peaking_boiler_cost_per_kW = self.ParameterDict[self.peaking_boiler_cost_per_kW.Name] = floatParameter( + "Peaking Boiler Cost per kW", + DefaultValue=self._default_peaking_boiler_cost_USD_per_kW, Min=0, Max=1000, UnitType=Units.ENERGYCOST, PreferredUnits=EnergyCostUnit.DOLLARSPERKW, CurrentUnits=EnergyCostUnit.DOLLARSPERKW, Required=False, - ToolTipText="Peaking boiler cost per KW of maximum peaking boiler demand" + ToolTipText="Peaking boiler cost per kW of maximum peaking boiler demand" ) self.dhpipingcostrate = self.ParameterDict[self.dhpipingcostrate.Name] = floatParameter( "District Heating Piping Cost Rate", @@ -1633,6 +1633,7 @@ def __init__(self, model: Model): f'Provide {self.ccexplfixed.Name} to override the default correlation and set your own cost.' ) + # noinspection SpellCheckingInspection self.Cwell = self.OutputParameterDict[self.Cwell.Name] = OutputParameter( Name="Wellfield cost", display_name='Drilling and completion costs', @@ -1640,11 +1641,21 @@ def __init__(self, model: Model): PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS, - # See TODO re:parameterizing indirect costs at src/geophires_x/Economics.py:652 - # (https://github.com/NREL/GEOPHIRES-X/issues/383) + # TODO https://github.com/NREL/GEOPHIRES-X/issues/383?title=Parameterize+indirect+cost+factor ToolTipText="Includes total drilling and completion cost of all injection and production wells and " "laterals, plus 5% indirect costs." ) + self.drilling_and_completion_costs_per_well = self.OutputParameterDict[ + self.drilling_and_completion_costs_per_well.Name] = OutputParameter( + Name='Drilling and completion costs per well', + UnitType=Units.CURRENCY, + PreferredUnits=CurrencyUnit.MDOLLARS, + CurrentUnits=CurrencyUnit.MDOLLARS, + + # TODO https://github.com/NREL/GEOPHIRES-X/issues/383?title=Parameterize+indirect+cost+factor + ToolTipText='Includes total drilling and completion cost per well, ' + 'including injection and production wells and laterals, plus 5% indirect costs.' + ) self.Coamwell = self.OutputParameterDict[self.Coamwell.Name] = OutputParameter( Name="O&M Wellfield cost", display_name='Wellfield maintenance costs', @@ -1722,9 +1733,9 @@ def __init__(self, model: Model): UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS, - ToolTipText=f'Default cost: ${self._default_peaking_boiler_cost_USD_per_kw}/KW ' + ToolTipText=f'Default cost: ${self._default_peaking_boiler_cost_USD_per_kW}/KW ' f'of maximum peaking boiler demand. ' - f'Provide {self.peaking_boiler_cost_per_kw.Name} override the default.' + f'Provide {self.peaking_boiler_cost_per_kW.Name} override the default.' ) self.dhdistrictcost = self.OutputParameterDict[self.dhdistrictcost.Name] = OutputParameter( @@ -2313,7 +2324,9 @@ def Calculate(self, model: Model) -> None: else: self.cost_lateral_section.value = 0.0 # cost of the well field - # 1.05 for 5% indirect costs - see TODO re:parameterizing at src/geophires_x/Economics.py:652 + + # 1.05 for 5% indirect costs + # TODO https://github.com/NREL/GEOPHIRES-X/issues/383?title=Parameterize+indirect+cost+factor self.Cwell.value = 1.05 * ((self.cost_one_production_well.value * model.wellbores.nprod.value) + (self.cost_one_injection_well.value * model.wellbores.ninj.value) + self.cost_lateral_section.value) @@ -2685,7 +2698,7 @@ def calculate_plant_costs(self, model:Model) -> None: model.surfaceplant.HeatExtracted.value) * 1000. # add 65$/KW for peaking boiler - self.peakingboilercost.value = (self.peaking_boiler_cost_per_kw.quantity() + self.peakingboilercost.value = (self.peaking_boiler_cost_per_kW.quantity() .to('USD / kilowatt').magnitude * model.surfaceplant.max_peaking_boiler_demand.value / 1000) @@ -2972,6 +2985,7 @@ 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] + # noinspection SpellCheckingInspection def _calculate_derived_outputs(self, model: Model) -> None: """ Subclasses should call _calculate_derived_outputs at the end of their Calculate methods to populate output @@ -2988,5 +3002,11 @@ def _calculate_derived_outputs(self, model: Model) -> None: self.real_discount_rate.value = self.discountrate.quantity().to(convertible_unit( self.real_discount_rate.CurrentUnits)).magnitude + if hasattr(self, 'Cwell') and hasattr(model.wellbores, 'nprod') and hasattr(model.wellbores, 'ninj'): + self.drilling_and_completion_costs_per_well.value = ( + self.Cwell.value / + (model.wellbores.nprod.value + model.wellbores.ninj.value) + ) + def __str__(self): return "Economics" diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index 4b93b98d..eedff5a8 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -38,10 +38,10 @@ project_vir_parameter, project_payback_period_parameter, ) -from geophires_x.GeoPHIRESUtils import is_float, is_int +from geophires_x.GeoPHIRESUtils import is_float, is_int, sig_figs 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, PercentUnit +from geophires_x.Units import convertible_unit, EnergyCostUnit, CurrencyUnit, Units @dataclass @@ -162,7 +162,7 @@ def calculate_sam_economics(model: Model) -> SamEconomicsCalculations: cash_flow = _calculate_sam_economics_cash_flow(model, single_owner) def sf(_v: float, num_sig_figs: int = 5) -> float: - return _sig_figs(_v, num_sig_figs) + return sig_figs(_v, num_sig_figs) sam_economics: SamEconomicsCalculations = SamEconomicsCalculations(sam_cash_flow_profile=cash_flow) sam_economics.lcoe_nominal.value = sf(single_owner.Outputs.lcoe_nom) @@ -435,21 +435,3 @@ def _ppa_pricing_model( def _get_max_total_generation_kW(model: Model) -> float: return np.max(model.surfaceplant.ElectricityProduced.quantity().to(convertible_unit('kW')).magnitude) - - -def _sig_figs(val: float | list | tuple, num_sig_figs: int) -> float: - """ - TODO move to utilities, probably - """ - - if val is None: - return None - - if isinstance(val, list) or isinstance(val, tuple): - return [_sig_figs(v, num_sig_figs) for v in val] - - try: - return float('%s' % float(f'%.{num_sig_figs}g' % val)) # pylint: disable=consider-using-f-string - except TypeError: - # TODO warn - return val diff --git a/src/geophires_x/GeoPHIRESUtils.py b/src/geophires_x/GeoPHIRESUtils.py index e8328d8a..1a7594aa 100644 --- a/src/geophires_x/GeoPHIRESUtils.py +++ b/src/geophires_x/GeoPHIRESUtils.py @@ -642,3 +642,16 @@ def is_float(o: Any) -> bool: else: return True + +def sig_figs(val: float | list | tuple, num_sig_figs: int) -> float: + if val is None: + return None + + if isinstance(val, list) or isinstance(val, tuple): + return [sig_figs(v, num_sig_figs) for v in val] + + try: + return float('%s' % float(f'%.{num_sig_figs}g' % val)) # pylint: disable=consider-using-f-string + except TypeError: + # TODO warn + return val diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index ac9f7a2c..6ae0fb7a 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -458,7 +458,8 @@ def PrintOutputs(self, model: Model): f.write(f' Drilling and completion costs per production well: {econ.cost_one_production_well.value:10.2f} ' + econ.cost_one_production_well.CurrentUnits.value + NL) f.write(f' Drilling and completion costs per injection well: {econ.cost_one_injection_well.value:10.2f} ' + econ.cost_one_injection_well.CurrentUnits.value + NL) else: - f.write(f' Drilling and completion costs per well: {model.economics.Cwell.value/(model.wellbores.nprod.value+model.wellbores.ninj.value):10.2f} ' + model.economics.Cwell.CurrentUnits.value + NL) + cpw_label = Outputs._field_label(econ.drilling_and_completion_costs_per_well.display_name, 47) + f.write(f' {cpw_label}{econ.drilling_and_completion_costs_per_well.value:10.2f} {econ.Cwell.CurrentUnits.value}\n') f.write(f' {econ.Cstim.display_name}: {econ.Cstim.value:10.2f} {econ.Cstim.CurrentUnits.value}\n') f.write(f' Surface power plant costs: {model.economics.Cplant.value:10.2f} ' + model.economics.Cplant.CurrentUnits.value + NL) if model.surfaceplant.plant_type.value == PlantType.ABSORPTION_CHILLER: diff --git a/src/geophires_x/WellBores.py b/src/geophires_x/WellBores.py index d4f6e51d..ec25bf92 100644 --- a/src/geophires_x/WellBores.py +++ b/src/geophires_x/WellBores.py @@ -1035,14 +1035,24 @@ def __init__(self, model: Model): ErrMessage="assume default for Non-vertical Wellbore Diameter (0.156 m)", ToolTipText="Non-vertical Wellbore Diameter" ) + + max_allowed_total_wells = max(self.nprod.AllowableRange) + max(self.ninj.AllowableRange) + max_allowed_laterals_per_well_when_max_wells = 3 + """Arbitrary upper limit, could be increased in future if needed""" + + # noinspection SpellCheckingInspection self.numnonverticalsections = self.ParameterDict[self.numnonverticalsections.Name] = intParameter( "Number of Multilateral Sections", DefaultValue=0, - AllowableRange=list(range(0, 101, 1)), + AllowableRange=list(range(0, max_allowed_total_wells * max_allowed_laterals_per_well_when_max_wells, 1)), UnitType=Units.NONE, ErrMessage="assume default for Number of Nonvertical Wellbore Sections (0)", - ToolTipText="Number of Nonvertical Wellbore Sections" + ToolTipText='Number of Nonvertical Wellbore Sections, aka laterals or horizontals. ' + 'Note that this is the total number of sections for the entire project and not the number of ' + 'sections per well. For example, a project with 2 injectors and 2 producers with 3 laterals ' + 'per well should set Number of Multilateral Sections = 2 * 2 * 3 = 12.' ) + self.NonverticalsCased = self.ParameterDict[self.NonverticalsCased.Name] = boolParameter( "Multilaterals Cased", DefaultValue=False, diff --git a/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index 6968830a..18b11f42 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.9.19' +__version__ = '3.9.26' diff --git a/src/geophires_x_client/__init__.py b/src/geophires_x_client/__init__.py index 9e4e0864..954e1fac 100644 --- a/src/geophires_x_client/__init__.py +++ b/src/geophires_x_client/__init__.py @@ -1,71 +1,127 @@ -import json -import os +import atexit import sys -from pathlib import Path +import threading +from multiprocessing import Manager +from multiprocessing import current_process +# noinspection PyPep8Naming from geophires_x import GEOPHIRESv3 as geophires from .common import _get_logger -from .geophires_input_parameters import EndUseOption from .geophires_input_parameters import GeophiresInputParameters +from .geophires_input_parameters import ImmutableGeophiresInputParameters from .geophires_x_result import GeophiresXResult class GeophiresXClient: - def __init__(self, enable_caching=True, logger_name=None): + """ + A thread-safe and process-safe client for running GEOPHIRES simulations. + It automatically manages a background process via atexit and provides an + explicit shutdown() method for advanced use cases like testing. + """ + + # --- Class-level shared resources --- + _manager = None + _cache = None + _lock = None + + _init_lock = threading.Lock() + """A standard threading lock to make the one-time initialization thread-safe.""" + + def __init__(self, enable_caching=False, logger_name=None): if logger_name is None: logger_name = __name__ self._logger = _get_logger(logger_name=logger_name) self._enable_caching = enable_caching - self._cache = {} + + if enable_caching and GeophiresXClient._manager is None: + # Lazy-initialize shared resources if they haven't been already. + self._initialize_shared_resources() + + @classmethod + def _initialize_shared_resources(cls): + """ + Initializes the multiprocessing Manager and shared resources in a + thread-safe and now process-safe manner. It also registers the + shutdown hook to ensure automatic cleanup on application exit. + """ + # Ensure that only the top-level user process can create the manager. + # A spawned child process, which re-imports this script, will have a different name + # (e.g., 'Spawn-1') and will skip this entire block, preventing a recursive crash. + if current_process().name == 'MainProcess': + with cls._init_lock: + if cls._manager is None: + cls._logger = _get_logger(__name__) # Add a logger for this class method + cls._logger.debug('MainProcess is creating the shared multiprocessing manager...') + cls._manager = Manager() + cls._cache = cls._manager.dict() + cls._lock = cls._manager.RLock() + # Register the shutdown method to be called automatically on exit. + atexit.register(cls.shutdown) + + @classmethod + def shutdown(cls): + """ + Explicitly shuts down the background manager process and de-registers + the atexit hook to prevent errors if called multiple times. + This is useful for test suites or applications that need to precisely + control the resource lifecycle. + """ + with cls._init_lock: + if cls._manager is not None: + cls._logger = _get_logger(__name__) + cls._logger.debug('Shutting down the shared multiprocessing manager...') + cls._manager.shutdown() + # De-register the hook to avoid trying to shut down twice. + try: + atexit.unregister(cls.shutdown) + except Exception as e: + # Fails in some environments (e.g. pytest), but is not critical + cls._logger.debug( + f'Encountered exception shutting down the shared multiprocessing manager (OK): ' f'{e!s}' + ) + cls._manager = None + cls._cache = None + cls._lock = None def get_geophires_result(self, input_params: GeophiresInputParameters) -> GeophiresXResult: + """ + Calculates a GEOPHIRES result, using a cross-process cache to avoid + re-computing results for the same inputs. Caching is only effective + when providing an instance of ImmutableGeophiresInputParameters. + """ + is_immutable = isinstance(input_params, ImmutableGeophiresInputParameters) + + if not (self._enable_caching and is_immutable and GeophiresXClient._manager is not None): + return self._run_simulation(input_params) + cache_key = hash(input_params) - if self._enable_caching and cache_key in self._cache: - return self._cache[cache_key] - stash_cwd = Path.cwd() - stash_sys_argv = sys.argv + with GeophiresXClient._lock: + if cache_key in GeophiresXClient._cache: + # self._logger.debug(f'Cache hit for inputs: {input_params}') + return GeophiresXClient._cache[cache_key] + + # Cache miss + result = self._run_simulation(input_params) + GeophiresXClient._cache[cache_key] = result + return result + def _run_simulation(self, input_params: GeophiresInputParameters) -> GeophiresXResult: + """Helper method to encapsulate the actual GEOPHIRES run.""" + stash_sys_argv = sys.argv sys.argv = ['', input_params.as_file_path(), input_params.get_output_file_path()] + try: geophires.main(enable_geophires_logging_config=False) except Exception as e: raise RuntimeError(f'GEOPHIRES encountered an exception: {e!s}') from e except SystemExit: raise RuntimeError('GEOPHIRES exited without giving a reason') from None - - # Undo Geophires internal global settings changes - sys.argv = stash_sys_argv - os.chdir(stash_cwd) + finally: + sys.argv = stash_sys_argv self._logger.info(f'GEOPHIRES-X output file: {input_params.get_output_file_path()}') - result = GeophiresXResult(input_params.get_output_file_path()) - if self._enable_caching: - self._cache[cache_key] = result - return result - - -if __name__ == '__main__': - client = GeophiresXClient() - log = _get_logger() - - # noinspection PyTypeChecker - params = GeophiresInputParameters( - { - 'Print Output to Console': 0, - 'End-Use Option': EndUseOption.DIRECT_USE_HEAT.value, - 'Reservoir Model': 1, - 'Time steps per year': 1, - 'Reservoir Depth': 3, - 'Gradient 1': 50, - 'Maximum Temperature': 250, - } - ) - - result = client.get_geophires_result(params) - log.info(f'Breakeven price: ${result.direct_use_heat_breakeven_price_USD_per_MMBTU}/MMBTU') - log.info(json.dumps(result.result, indent=2)) diff --git a/src/geophires_x_client/geophires_input_parameters.py b/src/geophires_x_client/geophires_input_parameters.py index 057c0101..24921f05 100644 --- a/src/geophires_x_client/geophires_input_parameters.py +++ b/src/geophires_x_client/geophires_input_parameters.py @@ -1,9 +1,16 @@ import tempfile import uuid +from dataclasses import dataclass +from dataclasses import field from enum import Enum from pathlib import Path from types import MappingProxyType +from typing import Any +from typing import Mapping from typing import Optional +from typing import Union + +from typing_extensions import override class EndUseOption(Enum): @@ -35,6 +42,13 @@ class PowerPlantType(Enum): class GeophiresInputParameters: + """ + .. deprecated:: v3.9.21 + Use :class:`~geophires_x_client.geophires_input_parameters.ImmutableGeophiresInputParameters` instead for + better performance and guardrails against erroneous usage. + This class is kept for backwards compatibility, but does not work with GeophiresXClient caching and is more + susceptible to potential bugs due to its mutability. + """ def __init__(self, params: Optional[MappingProxyType] = None, from_file_path: Optional[Path] = None): """ @@ -48,6 +62,7 @@ def __init__(self, params: Optional[MappingProxyType] = None, from_file_path: Op self._file_path = Path(tempfile.gettempdir(), f'geophires-input-params_{uuid.uuid4()!s}.txt') if from_file_path is not None: + # Note: This has a potential race condition if the file doesn't exist at the time of 'a', with open(from_file_path, encoding='UTF-8') as base_file: with open(self._file_path, 'a', encoding='UTF-8') as f: f.writelines(base_file.readlines()) @@ -74,5 +89,131 @@ def as_text(self): return f.read() def __hash__(self): - """TODO make hashes for equivalent parameters equal""" + """ + Note hashes for equivalent parameters may not be equal. + Use ImmutableGeophiresInputParameters instead. + """ + return self._id + + +@dataclass(frozen=True) +class ImmutableGeophiresInputParameters(GeophiresInputParameters): + """ + An immutable, self-contained, and content-hashable set of GEOPHIRES + input parameters. + + This class is hashable based on its logical content, making it safe for + caching. It generates its file representation on-demand and is designed + for use cases where parameter sets must be treated as immutable values. + """ + + params: Mapping[str, Any] = field(default_factory=lambda: MappingProxyType({})) + from_file_path: Union[Path, str, None] = None + + # A unique ID for this instance, used for file I/O but not for hashing or equality. + _instance_id: uuid.UUID = field(default_factory=uuid.uuid4, init=False, repr=False, compare=False) + + def __post_init__(self): + """ + Validates input and normalizes field types for immutability and consistency. + - Ensures from_file_path is a Path object if provided as a string. + - Ensures the params dictionary is an immutable mapping proxy. + """ + # Normalize from_file_path to a Path object. object.__setattr__ is required + # because the dataclass is frozen. + if self.from_file_path and isinstance(self.from_file_path, str): + object.__setattr__(self, 'from_file_path', Path(self.from_file_path)) + + # Ensure params is an immutable proxy + if not isinstance(self.params, MappingProxyType): + object.__setattr__(self, 'params', MappingProxyType(self.params)) + + @override + def __hash__(self) -> int: + """ + Computes a hash based on the content of the parameters. + If a base file is used, its content is read and hashed to ensure + the hash reflects a true snapshot of all inputs. + """ + param_hash = hash(frozenset(self.params.items())) + + file_content_hash = None + # self.from_file_path is now guaranteed to be a Path object or None + if self.from_file_path and self.from_file_path.exists(): + file_content_hash = hash(self.from_file_path.read_bytes()) + else: + # Hash the path itself if it's None or doesn't exist. + file_content_hash = hash(self.from_file_path) + + return hash((param_hash, file_content_hash)) + + def __deepcopy__(self, memo): + """ + Return the instance itself for deepcopy, as the object is immutable. + + This implementation prevents a TypeError when `copy.deepcopy` is used + on an object containing an instance of this class, as it avoids trying + to pickle the internal `mappingproxy` object. + + Args: + memo: The memoization dictionary used by `copy.deepcopy` to prevent + infinite recursion in case of circular references. + """ + # Add self to the memoization dictionary to handle circular references correctly. + memo[id(self)] = self + return self + + def __getstate__(self) -> dict: + """ + Prepare the object's state for pickling. + Converts the mappingproxy to a regular dict, which is pickleable. + """ + state = self.__dict__.copy() + # Convert mappingproxy to dict for serialization + state['params'] = dict(self.params) + return state + + def __setstate__(self, state: dict): + """ + Restore the object's state after unpickling. + Converts the dict back to a mappingproxy to maintain immutability. + """ + # Convert dict back to mappingproxy + state['params'] = MappingProxyType(state['params']) + # Restore the instance's dictionary + self.__dict__.update(state) + + @override + def as_file_path(self) -> Path: + """ + Creates a temporary file representation of the parameters on demand. + The resulting file path is cached on the instance for efficiency. + """ + # Use hasattr to check for the cached attribute on the frozen instance + if hasattr(self, '_cached_file_path'): + return self._cached_file_path + + file_path = Path(tempfile.gettempdir(), f'geophires-input-params_{self._instance_id!s}.txt') + + with open(file_path, 'w+', encoding='UTF-8') as f: + if self.from_file_path: + with open(self.from_file_path, encoding='UTF-8') as base_file: + f.write(base_file.read()) + + if self.params: + # Ensure there is a newline between the base file content and appended params. + if self.from_file_path and f.tell() > 0: + f.seek(f.tell() - 1) + if f.read(1) != '\n': + f.write('\n') + f.writelines([f'{key}, {value}\n' for key, value in self.params.items()]) + + # Cache the path on the instance after creation. + object.__setattr__(self, '_cached_file_path', file_path) + return file_path + + @override + def get_output_file_path(self) -> Path: + """Returns a unique path for the GEOPHIRES output file.""" + return Path(tempfile.gettempdir(), f'geophires-result_{self._instance_id!s}.out') diff --git a/src/geophires_x_schema_generator/geophires-request.json b/src/geophires_x_schema_generator/geophires-request.json index 34ebf881..1928dfd5 100644 --- a/src/geophires_x_schema_generator/geophires-request.json +++ b/src/geophires_x_schema_generator/geophires-request.json @@ -947,13 +947,13 @@ "maximum": 100.0 }, "Number of Multilateral Sections": { - "description": "Number of Nonvertical Wellbore Sections", + "description": "Number of Nonvertical Wellbore Sections, aka laterals or horizontals. Note that this is the total number of sections for the entire project and not the number of sections per well. For example, a project with 2 injectors and 2 producers with 3 laterals per well should set Number of Multilateral Sections = 2 * 2 * 3 = 12.", "type": "integer", "units": null, "category": "Well Bores", "default": 0, "minimum": 0, - "maximum": 100 + "maximum": 1199 }, "Multilaterals Cased": { "description": "If set to True, casing & cementing are assumed to comprise 50% of drilling costs (doubling cost compared to uncased).", @@ -1869,8 +1869,8 @@ "minimum": 0, "maximum": 1 }, - "Peaking Boiler Cost per KW": { - "description": "Peaking boiler cost per KW of maximum peaking boiler demand", + "Peaking Boiler Cost per kW": { + "description": "Peaking boiler cost per kW of maximum peaking boiler demand", "type": "number", "units": "USD/kW", "category": "Economics", diff --git a/src/geophires_x_schema_generator/geophires-result.json b/src/geophires_x_schema_generator/geophires-result.json index 5294e9b9..8fadbe38 100644 --- a/src/geophires_x_schema_generator/geophires-result.json +++ b/src/geophires_x_schema_generator/geophires-result.json @@ -333,7 +333,11 @@ "description": "Wellfield cost. Includes total drilling and completion cost of all injection and production wells and laterals, plus 5% indirect costs.", "units": "MUSD" }, - "Drilling and completion costs per well": {}, + "Drilling and completion costs per well": { + "type": "number", + "description": "Includes total drilling and completion cost per well, including injection and production wells and laterals, plus 5% indirect costs.", + "units": "MUSD" + }, "Drilling and completion costs per production well": {}, "Drilling and completion costs per injection well": {}, "Drilling and completion costs per vertical production well": {}, diff --git a/tests/base_test_case.py b/tests/base_test_case.py index 1e36812e..36bf53ba 100644 --- a/tests/base_test_case.py +++ b/tests/base_test_case.py @@ -15,8 +15,8 @@ class BaseTestCase(unittest.TestCase): def _get_test_file_path(self, test_file_name) -> str: return os.path.join(os.path.abspath(os.path.dirname(inspect.getfile(self.__class__))), test_file_name) - def _get_test_file_content(self, test_file_name): - with open(self._get_test_file_path(test_file_name)) as f: + def _get_test_file_content(self, test_file_name, **open_kw_args) -> str: + with open(self._get_test_file_path(test_file_name), **open_kw_args) as f: return f.readlines() def _list_test_files_dir(self, test_files_dir: str): diff --git a/tests/examples/Fervo_Project_Cape-4.out b/tests/examples/Fervo_Project_Cape-4.out index dcdb16f0..55113f51 100644 --- a/tests/examples/Fervo_Project_Cape-4.out +++ b/tests/examples/Fervo_Project_Cape-4.out @@ -4,17 +4,17 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.9.18 - Simulation Date: 2025-06-18 - Simulation Time: 13:23 - Calculation Time: 1.554 sec + GEOPHIRES Version: 3.9.24 + Simulation Date: 2025-06-20 + Simulation Time: 14:04 + Calculation Time: 1.575 sec ***SUMMARY OF RESULTS*** End-Use Option: Electricity - Average Net Electricity Production: 531.71 MW - Electricity breakeven price: 7.65 cents/kWh - Total CAPEX: 2673.11 MUSD + Average Net Electricity Production: 532.53 MW + Electricity breakeven price: 7.55 cents/kWh + Total CAPEX: 2639.39 MUSD Number of production wells: 59 Number of injection wells: 59 Flowrate per production well: 107.0 kg/sec @@ -31,12 +31,12 @@ Simulation Metadata Accrued financing during construction: 15.00 % Project lifetime: 30 yr Capacity factor: 90.0 % - Project NPV: 612.72 MUSD - After-tax IRR: 30.66 % - Project VIR=PI=PIR: 1.57 - Project MOIC: 5.55 - Project Payback Period: 2.01 yr - Estimated Jobs Created: 1298 + Project NPV: 641.24 MUSD + After-tax IRR: 31.51 % + Project VIR=PI=PIR: 1.61 + Project MOIC: 5.77 + Project Payback Period: 1.95 yr + Estimated Jobs Created: 1300 ***ENGINEERING PARAMETERS*** @@ -47,7 +47,7 @@ Simulation Metadata Pump efficiency: 80.0 % Injection temperature: 56.6 degC Production Wellbore heat transmission calculated with Ramey's model - Average production well temperature drop: 0.5 degC + Average production well temperature drop: 0.6 degC Flowrate per production well: 107.0 kg/sec Injection well casing ID: 9.625 in Production well casing ID: 9.625 in @@ -69,10 +69,10 @@ Simulation Metadata Fracture model = Square Well separation: fracture height: 165.30 meter Fracture area: 27324.09 m**2 - Number of fractures calculated with reservoir volume and fracture separation as input - Number of fractures: 11018 + Reservoir volume calculated with fracture separation and number of fractures as input + Number of fractures: 12036 Fracture separation: 18.00 meter - Reservoir volume: 5418039158 m**3 + Reservoir volume: 5919217617 m**3 Reservoir impedance: 0.0016 GPa.s/m**3 Reservoir density: 2800.00 kg/m**3 Reservoir thermal conductivity: 3.05 W/m/K @@ -81,59 +81,57 @@ Simulation Metadata ***RESERVOIR SIMULATION RESULTS*** - Maximum Production Temperature: 199.5 degC - Average Production Temperature: 198.9 degC - Minimum Production Temperature: 195.2 degC + Maximum Production Temperature: 199.6 degC + Average Production Temperature: 199.0 degC + Minimum Production Temperature: 195.4 degC Initial Production Temperature: 198.2 degC - Average Reservoir Heat Extraction: 3758.70 MW + Average Reservoir Heat Extraction: 3761.51 MW Production Wellbore Heat Transmission Model = Ramey Model - Average Production Well Temperature Drop: 0.5 degC - Total Average Pressure Drop: 8523.8 kPa + Average Production Well Temperature Drop: 0.6 degC + Total Average Pressure Drop: 8521.8 kPa Average Injection Well Pressure Drop: 600.9 kPa Average Reservoir Pressure Drop: 10344.9 kPa Average Production Well Pressure Drop: 504.2 kPa - Average Buoyancy Pressure Drop: -2926.2 kPa + Average Buoyancy Pressure Drop: -2928.2 kPa ***CAPITAL COSTS (M$)*** - Drilling and completion costs: 497.69 MUSD - Drilling and completion costs per vertical production well: 3.96 MUSD - Drilling and completion costs per vertical injection well: 3.96 MUSD - Drilling and completion costs per non-vertical section: 2.08 MUSD + Drilling and completion costs: 467.75 MUSD + Drilling and completion costs per well: 3.96 MUSD Stimulation costs: 236.88 MUSD - Surface power plant costs: 1503.42 MUSD - Field gathering system costs: 56.45 MUSD - Total surface equipment costs: 1559.87 MUSD + Surface power plant costs: 1504.05 MUSD + Field gathering system costs: 56.44 MUSD + Total surface equipment costs: 1560.49 MUSD Exploration costs: 30.00 MUSD - Investment Tax Credit: -697.33 MUSD - Total CAPEX: 2673.11 MUSD + Investment Tax Credit: -688.54 MUSD + Total CAPEX: 2639.39 MUSD ***OPERATING AND MAINTENANCE COSTS (M$/yr)*** - Wellfield maintenance costs: 6.50 MUSD/yr - Power plant maintenance costs: 25.42 MUSD/yr + Wellfield maintenance costs: 6.20 MUSD/yr + Power plant maintenance costs: 25.43 MUSD/yr Water costs: 24.86 MUSD/yr - Total operating and maintenance costs: 130.24 MUSD/yr + Total operating and maintenance costs: 126.95 MUSD/yr ***SURFACE EQUIPMENT SIMULATION RESULTS*** Initial geofluid availability: 0.19 MW/(kg/s) - Maximum Total Electricity Generation: 614.34 MW - Average Total Electricity Generation: 609.40 MW - Minimum Total Electricity Generation: 581.66 MW + Maximum Total Electricity Generation: 614.60 MW + Average Total Electricity Generation: 610.21 MW + Minimum Total Electricity Generation: 583.15 MW Initial Total Electricity Generation: 604.35 MW - Maximum Net Electricity Generation: 536.88 MW - Average Net Electricity Generation: 531.71 MW - Minimum Net Electricity Generation: 502.90 MW + Maximum Net Electricity Generation: 537.14 MW + Average Net Electricity Generation: 532.53 MW + Minimum Net Electricity Generation: 504.44 MW Initial Net Electricity Generation: 526.63 MW - Average Annual Total Electricity Generation: 4804.56 GWh - Average Annual Net Electricity Generation: 4192.05 GWh + Average Annual Total Electricity Generation: 4810.97 GWh + Average Annual Net Electricity Generation: 4198.60 GWh Initial pumping power/net installed power: 14.76 % - Average Pumping Power: 77.69 MW - Heat to Power Conversion Efficiency: 14.15 % + Average Pumping Power: 77.67 MW + Heat to Power Conversion Efficiency: 14.16 % ************************************************************ * HEATING, COOLING AND/OR ELECTRICITY PRODUCTION PROFILE * @@ -143,34 +141,34 @@ Simulation Metadata (degC) (MW) (MW) (%) 1 1.0000 198.22 77.7286 526.6263 14.0769 2 1.0051 199.24 77.6662 534.3539 14.1818 - 3 1.0061 199.42 77.6548 535.7720 14.2009 - 4 1.0065 199.52 77.6495 536.4851 14.2105 - 5 1.0066 199.54 77.6560 536.6451 14.2126 - 6 1.0057 199.35 77.7115 535.1783 14.1922 - 7 1.0019 198.61 77.9043 529.3559 14.1115 - 8 0.9926 196.75 78.3717 514.8879 13.9080 - 9 1.0037 198.95 77.6277 532.2546 14.1541 - 10 1.0056 199.33 77.6188 535.1354 14.1929 - 11 1.0063 199.47 77.6071 536.1662 14.2068 - 12 1.0066 199.54 77.5953 536.7025 14.2141 - 13 1.0065 199.50 77.6023 536.4142 14.2102 - 14 1.0046 199.14 77.6869 533.5811 14.1712 - 15 0.9989 198.00 77.9623 524.7524 14.0484 - 16 0.9862 195.49 78.5792 505.2627 13.7716 - 17 1.0048 199.18 77.5103 534.1012 14.1805 - 18 1.0059 199.40 77.4976 535.7642 14.2029 - 19 1.0065 199.50 77.4875 536.5580 14.2136 - 20 1.0067 199.54 77.4864 536.8507 14.2175 - 21 1.0060 199.41 77.5261 535.7989 14.2030 - 22 1.0029 198.79 77.6864 530.9677 14.1361 - 23 0.9947 197.16 78.0990 518.2759 13.9582 - 24 1.0027 198.76 77.4615 530.9336 14.1386 - 25 1.0054 199.30 77.4599 535.0372 14.1937 - 26 1.0062 199.45 77.4589 536.1857 14.2090 - 27 1.0066 199.53 77.4598 536.7844 14.2170 - 28 1.0066 199.52 77.4767 536.6927 14.2156 - 29 1.0051 199.24 77.5592 534.4598 14.1846 - 30 1.0003 198.27 77.8106 526.9180 14.0797 + 3 1.0061 199.42 77.6547 535.7721 14.2009 + 4 1.0065 199.52 77.6489 536.5044 14.2108 + 5 1.0068 199.57 77.6474 536.9145 14.2163 + 6 1.0067 199.55 77.6626 536.7091 14.2133 + 7 1.0054 199.29 77.7347 534.6638 14.1850 + 8 1.0012 198.47 77.9449 528.2545 14.0960 + 9 0.9919 196.61 78.4108 513.7990 13.8924 + 10 1.0040 199.02 77.6265 532.7400 14.1607 + 11 1.0057 199.35 77.6184 535.2509 14.1944 + 12 1.0063 199.48 77.6077 536.2282 14.2076 + 13 1.0067 199.55 77.5951 536.8041 14.2155 + 14 1.0068 199.58 77.5874 537.0016 14.2182 + 15 1.0063 199.46 77.6101 536.1193 14.2061 + 16 1.0038 198.97 77.7249 532.3053 14.1535 + 17 0.9974 197.70 78.0349 522.3915 14.0153 + 18 1.0000 198.22 77.5160 526.8389 14.0826 + 19 1.0051 199.24 77.5030 534.5170 14.1861 + 20 1.0061 199.42 77.4920 535.9348 14.2052 + 21 1.0065 199.52 77.4831 536.6702 14.2152 + 22 1.0068 199.57 77.4784 537.0835 14.2208 + 23 1.0067 199.55 77.4911 536.8806 14.2179 + 24 1.0054 199.29 77.5620 534.8365 14.1896 + 25 1.0012 198.47 77.7729 528.4265 14.1006 + 26 0.9919 196.61 78.2420 513.9678 13.8970 + 27 1.0040 199.02 77.4599 532.9067 14.1651 + 28 1.0057 199.35 77.4589 535.4103 14.1987 + 29 1.0063 199.48 77.4584 536.3775 14.2116 + 30 1.0067 199.55 77.4587 536.9405 14.2191 ******************************************************************* @@ -179,36 +177,36 @@ Simulation Metadata YEAR ELECTRICITY HEAT RESERVOIR PERCENTAGE OF PROVIDED EXTRACTED HEAT CONTENT TOTAL HEAT MINED (GWh/year) (GWh/year) (10^15 J) (%) - 1 4192.9 29636.9 1632.52 6.13 - 2 4219.2 29728.1 1525.50 12.29 - 3 4227.1 29755.4 1418.38 18.45 - 4 4230.8 29768.3 1311.21 24.61 - 5 4226.9 29755.2 1204.09 30.77 - 6 4200.5 29666.3 1097.29 36.91 - 7 4123.6 29405.9 991.43 43.00 - 8 4085.2 29271.2 886.06 49.05 - 9 4210.4 29696.4 779.15 55.20 - 10 4223.5 29741.8 672.08 61.36 - 11 4229.5 29762.5 564.93 67.52 - 12 4231.1 29767.9 457.77 73.68 - 13 4220.5 29731.9 350.73 79.83 - 14 4177.3 29585.5 244.23 85.96 - 15 4068.9 29216.8 139.05 92.01 - 16 4161.7 29526.4 32.75 98.12 - 17 4218.4 29721.1 -74.24 104.27 - 18 4227.4 29752.0 -181.35 110.43 - 19 4231.8 29767.3 -288.51 116.59 - 20 4229.8 29760.8 -395.65 122.75 - 21 4208.8 29689.9 -502.54 128.89 - 22 4142.8 29466.4 -608.62 134.99 - 23 4066.8 29205.0 -713.75 141.04 - 24 4207.2 29680.6 -820.60 147.18 - 25 4223.3 29736.8 -927.66 153.34 - 26 4229.9 29759.9 -1034.79 159.50 - 27 4232.4 29768.7 -1141.96 165.66 - 28 4224.7 29743.1 -1249.03 171.82 - 29 4188.8 29621.9 -1355.67 177.95 - 30 4099.9 29320.6 -1461.23 184.02 + 1 4192.9 29636.9 1793.40 5.62 + 2 4219.2 29728.1 1686.38 11.25 + 3 4227.2 29755.6 1579.26 16.89 + 4 4231.6 29771.1 1472.08 22.53 + 5 4232.9 29775.6 1364.89 28.17 + 6 4225.2 29749.9 1257.79 33.80 + 7 4194.0 29644.4 1151.07 39.42 + 8 4114.4 29374.5 1045.32 44.99 + 9 4101.4 29325.8 939.75 50.54 + 10 4212.2 29702.5 832.82 56.17 + 11 4224.2 29744.1 725.74 61.80 + 12 4230.1 29764.4 618.59 67.44 + 13 4233.3 29775.3 511.40 73.09 + 14 4231.4 29768.8 404.23 78.73 + 15 4214.5 29711.6 297.27 84.36 + 16 4162.9 29536.5 190.94 89.95 + 17 4055.6 29171.2 85.92 95.48 + 18 4194.3 29636.9 -20.77 101.09 + 19 4220.5 29728.1 -127.79 106.73 + 20 4228.5 29755.6 -234.91 112.36 + 21 4233.0 29771.1 -342.09 118.00 + 22 4234.2 29775.6 -449.28 123.65 + 23 4226.6 29749.9 -556.38 129.28 + 24 4195.3 29644.4 -663.10 134.90 + 25 4115.7 29374.5 -768.85 140.46 + 26 4102.7 29325.8 -874.42 146.02 + 27 4213.5 29702.5 -981.35 151.65 + 28 4225.4 29744.1 -1088.43 157.28 + 29 4231.2 29764.4 -1195.58 162.92 + 30 4234.3 29775.2 -1302.77 168.56 *************************** * SAM CASH FLOW PROFILE * @@ -216,52 +214,52 @@ Simulation Metadata ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 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 Year 21 Year 22 Year 23 Year 24 Year 25 Year 26 Year 27 Year 28 Year 29 Year 30 ENERGY -Electricity to grid (kWh) 0.0 4,193,226,013 4,219,526,008 4,227,429,703 4,231,130,578 4,227,174,470 4,200,800,078 4,123,944,075 4,085,524,292 4,210,727,110 4,223,816,595 4,229,858,515 4,231,437,288 4,220,815,353 4,177,617,023 4,069,152,556 4,162,009,516 4,218,752,453 4,227,707,328 4,232,132,093 4,230,152,834 4,209,135,409 4,143,119,490 4,067,069,431 4,207,503,902 4,223,608,553 4,230,238,414 4,232,692,624 4,225,013,608 4,189,101,096 4,100,244,505 +Electricity to grid (kWh) 0.0 4,193,273,525 4,219,573,970 4,227,516,388 4,232,001,035 4,233,245,126 4,225,570,913 4,194,325,606 4,114,710,394 4,101,737,992 4,212,547,398 4,224,519,385 4,230,441,060 4,233,667,186 4,231,750,368 4,214,888,525 4,163,207,043 4,055,985,743 4,194,671,056 4,220,853,770 4,228,811,003 4,233,321,393 4,234,588,146 4,226,928,684 4,195,686,141 4,116,056,232 4,103,061,353 4,213,834,812 4,225,738,401 4,231,569,184 4,234,649,495 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 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 4,193,226,013 4,219,526,008 4,227,429,703 4,231,130,578 4,227,174,470 4,200,800,078 4,123,944,075 4,085,524,292 4,210,727,110 4,223,816,595 4,229,858,515 4,231,437,288 4,220,815,353 4,177,617,023 4,069,152,556 4,162,009,516 4,218,752,453 4,227,707,328 4,232,132,093 4,230,152,834 4,209,135,409 4,143,119,490 4,067,069,431 4,207,503,902 4,223,608,553 4,230,238,414 4,232,692,624 4,225,013,608 4,189,101,096 4,100,244,505 +Electricity to grid net (kWh) 0.0 4,193,273,525 4,219,573,970 4,227,516,388 4,232,001,035 4,233,245,126 4,225,570,913 4,194,325,606 4,114,710,394 4,101,737,992 4,212,547,398 4,224,519,385 4,230,441,060 4,233,667,186 4,231,750,368 4,214,888,525 4,163,207,043 4,055,985,743 4,194,671,056 4,220,853,770 4,228,811,003 4,233,321,393 4,234,588,146 4,226,928,684 4,195,686,141 4,116,056,232 4,103,061,353 4,213,834,812 4,225,738,401 4,231,569,184 4,234,649,495 REVENUE PPA price (cents/kWh) 0.0 9.50 9.50 9.56 9.61 9.67 9.73 9.79 9.84 9.90 9.96 10.01 10.07 10.13 10.18 10.24 10.30 10.36 10.41 10.47 10.53 10.58 10.64 10.70 10.75 10.81 10.87 10.93 10.98 11.04 11.10 -PPA revenue ($) 0 398,356,471 400,854,971 404,015,457 406,780,894 408,810,043 408,653,832 403,527,928 402,097,301 416,819,877 420,523,180 423,535,733 426,105,735 427,441,971 425,448,518 416,721,913 428,603,740 436,851,817 440,188,887 443,061,909 445,265,887 445,452,800 440,827,914 435,054,417 452,474,970 456,614,321 459,742,311 462,421,669 463,990,994 462,434,870 454,963,130 +PPA revenue ($) 0 398,360,985 400,859,527 404,023,741 406,864,580 409,397,136 411,063,538 410,414,761 404,969,797 406,031,044 419,401,219 423,001,126 426,005,415 428,743,476 430,961,457 431,646,734 428,727,061 419,997,324 436,749,150 441,881,181 445,124,646 448,012,403 450,560,179 452,154,561 451,204,088 444,986,839 445,920,708 460,361,453 464,070,591 467,122,922 469,876,708 Curtailment payment revenue ($) 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 Capacity payment revenue ($) 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 -Salvage value ($) 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 1,336,555,047 -Total revenue ($) 0 398,356,471 400,854,971 404,015,457 406,780,894 408,810,043 408,653,832 403,527,928 402,097,301 416,819,877 420,523,180 423,535,733 426,105,735 427,441,971 425,448,518 416,721,913 428,603,740 436,851,817 440,188,887 443,061,909 445,265,887 445,452,800 440,827,914 435,054,417 452,474,970 456,614,321 459,742,311 462,421,669 463,990,994 462,434,870 1,791,518,178 +Salvage value ($) 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 1,319,696,135 +Total revenue ($) 0 398,360,985 400,859,527 404,023,741 406,864,580 409,397,136 411,063,538 410,414,761 404,969,797 406,031,044 419,401,219 423,001,126 426,005,415 428,743,476 430,961,457 431,646,734 428,727,061 419,997,324 436,749,150 441,881,181 445,124,646 448,012,403 450,560,179 452,154,561 451,204,088 444,986,839 445,920,708 460,361,453 464,070,591 467,122,922 1,789,572,843 -Property tax net assessed value ($) 0 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 2,673,110,095 +Property tax net assessed value ($) 0 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 2,639,392,270 OPERATING EXPENSES -O&M fixed expense ($) 0 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 +O&M fixed expense ($) 0 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 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 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 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 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 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 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 0 0 0 0 0 0 0 0 0 0 -Total operating expenses ($) 0 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 130,236,463 +Total operating expenses ($) 0 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 126,952,474 -EBITDA ($) 0 268,120,008 270,618,508 273,778,994 276,544,431 278,573,580 278,417,368 273,291,465 271,860,838 286,583,414 290,286,717 293,299,270 295,869,272 297,205,508 295,212,055 286,485,450 298,367,277 306,615,353 309,952,424 312,825,446 315,029,424 315,216,337 310,591,451 304,817,954 322,238,507 326,377,858 329,505,848 332,185,206 333,754,531 332,198,407 1,661,281,714 +EBITDA ($) 0 271,408,511 273,907,053 277,071,267 279,912,106 282,444,662 284,111,065 283,462,287 278,017,323 279,078,570 292,448,745 296,048,652 299,052,941 301,791,002 304,008,984 304,694,260 301,774,587 293,044,850 309,796,677 314,928,707 318,172,172 321,059,929 323,607,705 325,202,088 324,251,614 318,034,365 318,968,234 333,408,979 337,118,117 340,170,448 1,662,620,369 OPERATING ACTIVITIES -EBITDA ($) 0 268,120,008 270,618,508 273,778,994 276,544,431 278,573,580 278,417,368 273,291,465 271,860,838 286,583,414 290,286,717 293,299,270 295,869,272 297,205,508 295,212,055 286,485,450 298,367,277 306,615,353 309,952,424 312,825,446 315,029,424 315,216,337 310,591,451 304,817,954 322,238,507 326,377,858 329,505,848 332,185,206 333,754,531 332,198,407 1,661,281,714 +EBITDA ($) 0 271,408,511 273,907,053 277,071,267 279,912,106 282,444,662 284,111,065 283,462,287 278,017,323 279,078,570 292,448,745 296,048,652 299,052,941 301,791,002 304,008,984 304,694,260 301,774,587 293,044,850 309,796,677 314,928,707 318,172,172 321,059,929 323,607,705 325,202,088 324,251,614 318,034,365 318,968,234 333,408,979 337,118,117 340,170,448 1,662,620,369 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 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 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 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 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 0 0 0 0 0 0 0 0 0 0 -Debt interest payment ($) 0 89,816,499 88,597,952 87,311,166 85,952,321 84,517,380 83,002,082 81,401,927 79,712,164 77,927,774 76,043,459 74,053,621 71,952,353 69,733,414 67,390,214 64,915,795 62,302,808 59,543,495 56,629,659 53,552,649 50,303,327 46,872,042 43,248,605 39,422,256 35,381,631 31,114,732 26,608,886 21,850,712 16,826,081 11,520,070 5,916,923 -Cash flow from operating activities ($) 0 178,303,509 182,020,556 186,467,827 190,592,110 194,056,200 195,415,287 191,889,537 192,148,674 208,655,639 214,243,258 219,245,649 223,916,919 227,472,094 227,821,841 221,569,655 236,064,468 247,071,859 253,322,765 259,272,797 264,726,098 268,344,295 267,342,846 265,395,698 286,856,875 295,263,126 302,896,962 310,334,494 316,928,450 320,678,336 1,655,364,791 +Debt interest payment ($) 0 88,683,580 87,480,404 86,209,849 84,868,143 83,451,302 81,955,118 80,375,147 78,706,698 76,944,816 75,084,269 73,119,531 71,044,767 68,853,817 66,540,174 64,096,966 61,516,939 58,792,431 55,915,350 52,877,152 49,668,815 46,280,812 42,703,080 38,924,995 34,935,338 30,722,260 26,273,249 21,575,094 16,613,842 11,374,760 5,842,289 +Cash flow from operating activities ($) 0 182,724,931 186,426,650 190,861,418 195,043,962 198,993,360 202,155,947 203,087,139 199,310,625 202,133,754 217,364,476 222,929,121 228,008,174 232,937,185 237,468,810 240,597,294 240,257,648 234,252,419 253,881,327 262,051,556 268,503,357 274,779,118 280,904,625 286,277,092 289,316,276 287,312,106 292,694,985 311,833,886 320,504,276 328,795,689 1,656,778,080 INVESTING ACTIVITIES -Total installed cost ($) -2,673,110,095 +Total installed cost ($) -2,639,392,270 Debt closing costs ($) 0 Debt up-front fee ($) 0 minus: Total IBI income ($) 0 Total CBI income ($) 0 equals: -Purchase of property ($) -2,673,110,095 +Purchase of property ($) -2,639,392,270 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 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 0 0 0 0 0 0 0 0 0 0 @@ -273,86 +271,86 @@ Reserve capital spending major equipment 1 ($) 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 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 0 0 0 0 0 0 0 0 0 0 equals: -Cash flow from investing activities ($) -2,673,110,095 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 +Cash flow from investing activities ($) -2,639,392,270 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 FINANCING ACTIVITIES -Issuance of equity ($) 1,069,244,038 -Size of debt ($) 1,603,866,057 +Issuance of equity ($) 1,055,756,908 +Size of debt ($) 1,583,635,362 minus: -Debt principal payment ($) 0 21,759,769 22,978,316 24,265,102 25,623,948 27,058,889 28,574,187 30,174,341 31,864,104 33,648,494 35,532,810 37,522,647 39,623,915 41,842,855 44,186,054 46,660,474 49,273,460 52,032,774 54,946,609 58,023,619 61,272,942 64,704,227 68,327,663 72,154,013 76,194,637 80,461,537 84,967,383 89,725,556 94,750,188 100,056,198 105,659,345 +Debt principal payment ($) 0 21,485,298 22,688,475 23,959,029 25,300,735 26,717,576 28,213,760 29,793,731 31,462,180 33,224,062 35,084,609 37,049,347 39,124,111 41,315,061 43,628,705 46,071,912 48,651,939 51,376,448 54,253,529 57,291,726 60,500,063 63,888,067 67,465,798 71,243,883 75,233,540 79,446,619 83,895,629 88,593,785 93,555,037 98,794,119 104,326,589 equals: -Cash flow from financing activities ($) 2,673,110,095 -21,759,769 -22,978,316 -24,265,102 -25,623,948 -27,058,889 -28,574,187 -30,174,341 -31,864,104 -33,648,494 -35,532,810 -37,522,647 -39,623,915 -41,842,855 -44,186,054 -46,660,474 -49,273,460 -52,032,774 -54,946,609 -58,023,619 -61,272,942 -64,704,227 -68,327,663 -72,154,013 -76,194,637 -80,461,537 -84,967,383 -89,725,556 -94,750,188 -100,056,198 -105,659,345 +Cash flow from financing activities ($) 2,639,392,270 -21,485,298 -22,688,475 -23,959,029 -25,300,735 -26,717,576 -28,213,760 -29,793,731 -31,462,180 -33,224,062 -35,084,609 -37,049,347 -39,124,111 -41,315,061 -43,628,705 -46,071,912 -48,651,939 -51,376,448 -54,253,529 -57,291,726 -60,500,063 -63,888,067 -67,465,798 -71,243,883 -75,233,540 -79,446,619 -83,895,629 -88,593,785 -93,555,037 -98,794,119 -104,326,589 PROJECT RETURNS Pre-tax Cash Flow: -Cash flow from operating activities ($) 0 178,303,509 182,020,556 186,467,827 190,592,110 194,056,200 195,415,287 191,889,537 192,148,674 208,655,639 214,243,258 219,245,649 223,916,919 227,472,094 227,821,841 221,569,655 236,064,468 247,071,859 253,322,765 259,272,797 264,726,098 268,344,295 267,342,846 265,395,698 286,856,875 295,263,126 302,896,962 310,334,494 316,928,450 320,678,336 1,655,364,791 -Cash flow from investing activities ($) -2,673,110,095 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 -Cash flow from financing activities ($) 2,673,110,095 -21,759,769 -22,978,316 -24,265,102 -25,623,948 -27,058,889 -28,574,187 -30,174,341 -31,864,104 -33,648,494 -35,532,810 -37,522,647 -39,623,915 -41,842,855 -44,186,054 -46,660,474 -49,273,460 -52,032,774 -54,946,609 -58,023,619 -61,272,942 -64,704,227 -68,327,663 -72,154,013 -76,194,637 -80,461,537 -84,967,383 -89,725,556 -94,750,188 -100,056,198 -105,659,345 -Total pre-tax cash flow ($) 0 156,543,740 159,042,239 162,202,725 164,968,162 166,997,311 166,841,100 161,715,196 160,284,569 175,007,145 178,710,449 181,723,002 184,293,003 185,629,239 183,635,786 174,909,182 186,791,008 195,039,085 198,376,155 201,249,177 203,453,156 203,640,069 199,015,182 193,241,685 210,662,238 214,801,589 217,929,579 220,608,938 222,178,263 220,622,138 1,549,705,446 +Cash flow from operating activities ($) 0 182,724,931 186,426,650 190,861,418 195,043,962 198,993,360 202,155,947 203,087,139 199,310,625 202,133,754 217,364,476 222,929,121 228,008,174 232,937,185 237,468,810 240,597,294 240,257,648 234,252,419 253,881,327 262,051,556 268,503,357 274,779,118 280,904,625 286,277,092 289,316,276 287,312,106 292,694,985 311,833,886 320,504,276 328,795,689 1,656,778,080 +Cash flow from investing activities ($) -2,639,392,270 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 +Cash flow from financing activities ($) 2,639,392,270 -21,485,298 -22,688,475 -23,959,029 -25,300,735 -26,717,576 -28,213,760 -29,793,731 -31,462,180 -33,224,062 -35,084,609 -37,049,347 -39,124,111 -41,315,061 -43,628,705 -46,071,912 -48,651,939 -51,376,448 -54,253,529 -57,291,726 -60,500,063 -63,888,067 -67,465,798 -71,243,883 -75,233,540 -79,446,619 -83,895,629 -88,593,785 -93,555,037 -98,794,119 -104,326,589 +Total pre-tax cash flow ($) 0 161,239,633 163,738,175 166,902,389 169,743,227 172,275,784 173,942,186 173,293,409 167,848,445 168,909,692 182,279,867 185,879,774 188,884,063 191,622,124 193,840,105 194,525,382 191,605,709 182,875,972 199,627,798 204,759,829 208,003,294 210,891,051 213,438,827 215,033,209 214,082,736 207,865,487 208,799,356 223,240,101 226,949,239 230,001,570 1,552,451,491 Pre-tax Returns: -Issuance of equity ($) 1,069,244,038 -Total pre-tax cash flow ($) 0 156,543,740 159,042,239 162,202,725 164,968,162 166,997,311 166,841,100 161,715,196 160,284,569 175,007,145 178,710,449 181,723,002 184,293,003 185,629,239 183,635,786 174,909,182 186,791,008 195,039,085 198,376,155 201,249,177 203,453,156 203,640,069 199,015,182 193,241,685 210,662,238 214,801,589 217,929,579 220,608,938 222,178,263 220,622,138 1,549,705,446 -Total pre-tax returns ($) -1,069,244,038 156,543,740 159,042,239 162,202,725 164,968,162 166,997,311 166,841,100 161,715,196 160,284,569 175,007,145 178,710,449 181,723,002 184,293,003 185,629,239 183,635,786 174,909,182 186,791,008 195,039,085 198,376,155 201,249,177 203,453,156 203,640,069 199,015,182 193,241,685 210,662,238 214,801,589 217,929,579 220,608,938 222,178,263 220,622,138 1,549,705,446 +Issuance of equity ($) 1,055,756,908 +Total pre-tax cash flow ($) 0 161,239,633 163,738,175 166,902,389 169,743,227 172,275,784 173,942,186 173,293,409 167,848,445 168,909,692 182,279,867 185,879,774 188,884,063 191,622,124 193,840,105 194,525,382 191,605,709 182,875,972 199,627,798 204,759,829 208,003,294 210,891,051 213,438,827 215,033,209 214,082,736 207,865,487 208,799,356 223,240,101 226,949,239 230,001,570 1,552,451,491 +Total pre-tax returns ($) -1,055,756,908 161,239,633 163,738,175 166,902,389 169,743,227 172,275,784 173,942,186 173,293,409 167,848,445 168,909,692 182,279,867 185,879,774 188,884,063 191,622,124 193,840,105 194,525,382 191,605,709 182,875,972 199,627,798 204,759,829 208,003,294 210,891,051 213,438,827 215,033,209 214,082,736 207,865,487 208,799,356 223,240,101 226,949,239 230,001,570 1,552,451,491 After-tax Returns: -Total pre-tax returns ($) -1,069,244,038 156,543,740 159,042,239 162,202,725 164,968,162 166,997,311 166,841,100 161,715,196 160,284,569 175,007,145 178,710,449 181,723,002 184,293,003 185,629,239 183,635,786 174,909,182 186,791,008 195,039,085 198,376,155 201,249,177 203,453,156 203,640,069 199,015,182 193,241,685 210,662,238 214,801,589 217,929,579 220,608,938 222,178,263 220,622,138 1,549,705,446 -Federal ITC total income ($) 0 801,933,028 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 +Total pre-tax returns ($) -1,055,756,908 161,239,633 163,738,175 166,902,389 169,743,227 172,275,784 173,942,186 173,293,409 167,848,445 168,909,692 182,279,867 185,879,774 188,884,063 191,622,124 193,840,105 194,525,382 191,605,709 182,875,972 199,627,798 204,759,829 208,003,294 210,891,051 213,438,827 215,033,209 214,082,736 207,865,487 208,799,356 223,240,101 226,949,239 230,001,570 1,552,451,491 +Federal ITC total income ($) 0 791,817,681 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 Federal PTC income ($) 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 -Federal tax benefit (liability) ($) 0 -23,728,934 -13,361,132 -14,229,685 -15,035,157 -15,711,694 -15,977,123 -15,288,545 -15,339,154 -18,562,964 -19,654,226 -20,631,193 -21,543,492 -22,237,818 -22,306,123 -21,085,072 -23,915,909 -26,065,652 -27,286,454 -28,448,495 -29,513,525 -41,313,900 -52,212,058 -51,831,780 -56,023,148 -57,664,889 -59,155,777 -60,608,327 -61,896,126 -62,628,479 -323,292,744 +Federal tax benefit (liability) ($) 0 -24,732,371 -14,501,509 -15,367,619 -16,184,470 -16,955,788 -17,573,441 -17,755,303 -17,017,749 -17,569,106 -20,543,667 -21,630,442 -22,622,381 -23,585,017 -24,470,043 -25,081,036 -25,014,703 -23,841,882 -27,675,407 -29,271,053 -30,531,090 -42,710,554 -54,860,673 -55,909,916 -56,503,469 -56,112,054 -57,163,331 -60,901,158 -62,594,485 -64,213,798 -323,568,759 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 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 0 0 0 0 0 0 0 0 0 0 -State tax benefit (liability) ($) 0 -8,504,994 -4,788,936 -5,100,245 -5,388,945 -5,631,431 -5,726,568 -5,479,765 -5,497,905 -6,653,392 -7,044,526 -7,394,693 -7,721,682 -7,970,544 -7,995,026 -7,557,373 -8,572,010 -9,342,528 -9,780,091 -10,196,593 -10,578,324 -14,807,849 -18,713,999 -18,577,699 -20,079,981 -20,668,419 -21,202,787 -21,723,415 -22,184,992 -22,447,484 -115,875,535 -Total after-tax returns ($) -1,069,244,038 926,242,839 140,892,170 142,872,795 144,544,060 145,654,186 145,137,409 140,946,887 139,447,511 149,790,789 152,011,697 153,697,116 155,027,829 155,420,877 153,334,636 146,266,737 154,303,090 159,630,905 161,309,611 162,604,089 163,361,307 147,518,320 128,089,125 122,832,207 134,559,109 136,468,282 137,571,015 138,277,196 138,097,145 135,546,176 1,110,537,167 +State tax benefit (liability) ($) 0 -8,864,649 -5,197,673 -5,508,107 -5,800,885 -6,077,343 -6,298,724 -6,363,908 -6,099,552 -6,297,171 -7,363,321 -7,752,846 -8,108,380 -8,453,411 -8,770,625 -8,989,619 -8,965,843 -8,545,477 -9,919,501 -10,491,417 -10,943,043 -15,308,442 -19,663,324 -20,039,396 -20,252,139 -20,111,847 -20,488,649 -21,828,372 -22,435,299 -23,015,698 -115,974,466 +Total after-tax returns ($) -1,055,756,908 919,460,294 144,038,993 146,026,663 147,757,872 149,242,653 150,070,021 149,174,198 144,731,144 145,043,415 154,372,879 156,496,486 158,153,302 159,583,696 160,599,438 160,454,727 157,625,163 150,488,612 162,032,890 164,997,359 166,529,161 152,872,055 138,914,830 139,083,897 137,327,127 131,641,585 131,147,376 140,510,571 141,919,455 142,772,074 1,112,908,266 -After-tax cumulative IRR (%) NaN -13.37 -0.17 9.73 16.39 20.77 23.68 25.59 26.91 27.91 28.63 29.15 29.54 29.82 30.03 30.17 30.29 30.38 30.45 30.50 30.54 30.57 30.59 30.60 30.61 30.62 30.63 30.63 30.64 30.64 30.66 -After-tax cumulative NPV ($) -1,069,244,038 -260,834,913 -153,510,246 -58,522,284 25,351,382 99,117,119 163,270,187 217,645,265 264,598,067 308,617,272 347,606,124 382,012,228 412,301,287 438,804,091 461,624,797 480,624,234 498,117,711 513,912,899 527,843,647 540,099,738 550,846,462 559,316,379 565,735,149 571,107,423 576,243,900 580,790,538 584,790,831 588,300,142 591,359,021 593,979,443 612,717,451 +After-tax cumulative IRR (%) NaN -12.91 0.65 10.70 17.40 21.79 24.70 26.65 27.96 28.88 29.57 30.08 30.45 30.72 30.92 31.07 31.18 31.26 31.32 31.37 31.41 31.43 31.45 31.47 31.48 31.48 31.49 31.49 31.50 31.50 31.51 +After-tax cumulative NPV ($) -1,055,756,908 -253,267,474 -143,545,715 -46,460,926 39,277,598 114,860,694 181,194,056 238,743,100 287,474,933 330,099,021 369,693,483 404,726,244 435,625,953 462,838,612 486,740,535 507,582,932 525,453,035 540,343,610 554,336,820 566,773,302 577,728,422 586,505,730 593,466,995 599,550,064 604,792,204 609,178,033 612,991,540 616,557,531 619,701,075 622,461,191 641,239,205 AFTER-TAX LCOE AND PPA PRICE -Annual costs ($) -1,069,244,038 527,886,368 -259,962,800 -261,142,662 -262,236,834 -263,155,857 -263,516,423 -262,581,041 -262,649,790 -267,029,088 -268,511,483 -269,838,618 -271,077,906 -272,021,093 -272,113,881 -270,455,176 -274,300,650 -277,220,911 -278,879,276 -280,457,820 -281,904,581 -297,934,481 -312,738,788 -312,222,210 -317,915,861 -320,146,039 -322,171,296 -324,144,473 -325,893,849 -326,888,694 655,574,037 -PPA revenue ($) 0 398,356,471 400,854,971 404,015,457 406,780,894 408,810,043 408,653,832 403,527,928 402,097,301 416,819,877 420,523,180 423,535,733 426,105,735 427,441,971 425,448,518 416,721,913 428,603,740 436,851,817 440,188,887 443,061,909 445,265,887 445,452,800 440,827,914 435,054,417 452,474,970 456,614,321 459,742,311 462,421,669 463,990,994 462,434,870 454,963,130 -Electricity to grid (kWh) 0.0 4,193,226,013 4,219,526,008 4,227,429,703 4,231,130,578 4,227,174,470 4,200,800,078 4,123,944,075 4,085,524,292 4,210,727,110 4,223,816,595 4,229,858,515 4,231,437,288 4,220,815,353 4,177,617,023 4,069,152,556 4,162,009,516 4,218,752,453 4,227,707,328 4,232,132,093 4,230,152,834 4,209,135,409 4,143,119,490 4,067,069,431 4,207,503,902 4,223,608,553 4,230,238,414 4,232,692,624 4,225,013,608 4,189,101,096 4,100,244,505 +Annual costs ($) -1,055,756,908 521,099,309 -256,820,535 -257,997,079 -259,106,708 -260,154,483 -260,993,517 -261,240,562 -260,238,653 -260,987,629 -265,028,340 -266,504,640 -267,852,113 -269,159,780 -270,362,020 -271,192,006 -271,101,898 -269,508,711 -274,716,260 -276,883,822 -278,595,485 -295,140,348 -311,645,349 -313,070,665 -313,876,960 -313,345,254 -314,773,332 -319,850,882 -322,151,136 -324,350,848 643,031,558 +PPA revenue ($) 0 398,360,985 400,859,527 404,023,741 406,864,580 409,397,136 411,063,538 410,414,761 404,969,797 406,031,044 419,401,219 423,001,126 426,005,415 428,743,476 430,961,457 431,646,734 428,727,061 419,997,324 436,749,150 441,881,181 445,124,646 448,012,403 450,560,179 452,154,561 451,204,088 444,986,839 445,920,708 460,361,453 464,070,591 467,122,922 469,876,708 +Electricity to grid (kWh) 0.0 4,193,273,525 4,219,573,970 4,227,516,388 4,232,001,035 4,233,245,126 4,225,570,913 4,194,325,606 4,114,710,394 4,101,737,992 4,212,547,398 4,224,519,385 4,230,441,060 4,233,667,186 4,231,750,368 4,214,888,525 4,163,207,043 4,055,985,743 4,194,671,056 4,220,853,770 4,228,811,003 4,233,321,393 4,234,588,146 4,226,928,684 4,195,686,141 4,116,056,232 4,103,061,353 4,213,834,812 4,225,738,401 4,231,569,184 4,234,649,495 -Present value of annual costs ($) 2,166,141,497 -Present value of annual energy nominal (kWh) 28,322,755,320 -LCOE Levelized cost of energy nominal (cents/kWh) 7.65 +Present value of annual costs ($) 2,140,838,644 +Present value of annual energy nominal (kWh) 28,355,365,264 +LCOE Levelized cost of energy nominal (cents/kWh) 7.55 -Present value of PPA revenue ($) 2,778,858,948 -Present value of annual energy nominal (kWh) 28,322,755,320 +Present value of PPA revenue ($) 2,782,077,849 +Present value of annual energy nominal (kWh) 28,355,365,264 LPPA Levelized PPA price nominal (cents/kWh) 9.81 PROJECT STATE INCOME TAXES -EBITDA ($) 0 268,120,008 270,618,508 273,778,994 276,544,431 278,573,580 278,417,368 273,291,465 271,860,838 286,583,414 290,286,717 293,299,270 295,869,272 297,205,508 295,212,055 286,485,450 298,367,277 306,615,353 309,952,424 312,825,446 315,029,424 315,216,337 310,591,451 304,817,954 322,238,507 326,377,858 329,505,848 332,185,206 333,754,531 332,198,407 1,661,281,714 +EBITDA ($) 0 271,408,511 273,907,053 277,071,267 279,912,106 282,444,662 284,111,065 283,462,287 278,017,323 279,078,570 292,448,745 296,048,652 299,052,941 301,791,002 304,008,984 304,694,260 301,774,587 293,044,850 309,796,677 314,928,707 318,172,172 321,059,929 323,607,705 325,202,088 324,251,614 318,034,365 318,968,234 333,408,979 337,118,117 340,170,448 1,662,620,369 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 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 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 89,816,499 88,597,952 87,311,166 85,952,321 84,517,380 83,002,082 81,401,927 79,712,164 77,927,774 76,043,459 74,053,621 71,952,353 69,733,414 67,390,214 64,915,795 62,302,808 59,543,495 56,629,659 53,552,649 50,303,327 46,872,042 43,248,605 39,422,256 35,381,631 31,114,732 26,608,886 21,850,712 16,826,081 11,520,070 5,916,923 -Total state tax depreciation ($) 0 56,803,590 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 56,803,590 0 0 0 0 0 0 0 0 0 +Debt interest payment ($) 0 88,683,580 87,480,404 86,209,849 84,868,143 83,451,302 81,955,118 80,375,147 78,706,698 76,944,816 75,084,269 73,119,531 71,044,767 68,853,817 66,540,174 64,096,966 61,516,939 58,792,431 55,915,350 52,877,152 49,668,815 46,280,812 42,703,080 38,924,995 34,935,338 30,722,260 26,273,249 21,575,094 16,613,842 11,374,760 5,842,289 +Total state tax depreciation ($) 0 56,087,086 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 56,087,086 0 0 0 0 0 0 0 0 0 equals: -State taxable income ($) 0 121,499,919 68,413,377 72,860,648 76,984,931 80,449,021 81,808,108 78,282,358 78,541,495 95,048,460 100,636,079 105,638,470 110,309,740 113,864,915 114,214,662 107,962,476 122,457,289 133,464,680 139,715,586 145,665,618 151,118,919 211,540,706 267,342,846 265,395,698 286,856,875 295,263,126 302,896,962 310,334,494 316,928,450 320,678,336 1,655,364,791 +State taxable income ($) 0 126,637,845 74,252,478 78,687,247 82,869,791 86,819,189 89,981,775 90,912,968 87,136,453 89,959,582 105,190,305 110,754,950 115,834,002 120,763,014 125,294,638 128,423,122 128,083,477 122,078,248 141,707,156 149,877,384 156,329,186 218,692,032 280,904,625 286,277,092 289,316,276 287,312,106 292,694,985 311,833,886 320,504,276 328,795,689 1,656,778,080 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 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 -8,504,994 -4,788,936 -5,100,245 -5,388,945 -5,631,431 -5,726,568 -5,479,765 -5,497,905 -6,653,392 -7,044,526 -7,394,693 -7,721,682 -7,970,544 -7,995,026 -7,557,373 -8,572,010 -9,342,528 -9,780,091 -10,196,593 -10,578,324 -14,807,849 -18,713,999 -18,577,699 -20,079,981 -20,668,419 -21,202,787 -21,723,415 -22,184,992 -22,447,484 -115,875,535 +State tax benefit (liability) ($) 0 -8,864,649 -5,197,673 -5,508,107 -5,800,885 -6,077,343 -6,298,724 -6,363,908 -6,099,552 -6,297,171 -7,363,321 -7,752,846 -8,108,380 -8,453,411 -8,770,625 -8,989,619 -8,965,843 -8,545,477 -9,919,501 -10,491,417 -10,943,043 -15,308,442 -19,663,324 -20,039,396 -20,252,139 -20,111,847 -20,488,649 -21,828,372 -22,435,299 -23,015,698 -115,974,466 PROJECT FEDERAL INCOME TAXES -EBITDA ($) 0 268,120,008 270,618,508 273,778,994 276,544,431 278,573,580 278,417,368 273,291,465 271,860,838 286,583,414 290,286,717 293,299,270 295,869,272 297,205,508 295,212,055 286,485,450 298,367,277 306,615,353 309,952,424 312,825,446 315,029,424 315,216,337 310,591,451 304,817,954 322,238,507 326,377,858 329,505,848 332,185,206 333,754,531 332,198,407 1,661,281,714 +EBITDA ($) 0 271,408,511 273,907,053 277,071,267 279,912,106 282,444,662 284,111,065 283,462,287 278,017,323 279,078,570 292,448,745 296,048,652 299,052,941 301,791,002 304,008,984 304,694,260 301,774,587 293,044,850 309,796,677 314,928,707 318,172,172 321,059,929 323,607,705 325,202,088 324,251,614 318,034,365 318,968,234 333,408,979 337,118,117 340,170,448 1,662,620,369 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 0 0 0 0 0 0 0 0 0 0 -State tax benefit (liability) ($) 0 -8,504,994 -4,788,936 -5,100,245 -5,388,945 -5,631,431 -5,726,568 -5,479,765 -5,497,905 -6,653,392 -7,044,526 -7,394,693 -7,721,682 -7,970,544 -7,995,026 -7,557,373 -8,572,010 -9,342,528 -9,780,091 -10,196,593 -10,578,324 -14,807,849 -18,713,999 -18,577,699 -20,079,981 -20,668,419 -21,202,787 -21,723,415 -22,184,992 -22,447,484 -115,875,535 +State tax benefit (liability) ($) 0 -8,864,649 -5,197,673 -5,508,107 -5,800,885 -6,077,343 -6,298,724 -6,363,908 -6,099,552 -6,297,171 -7,363,321 -7,752,846 -8,108,380 -8,453,411 -8,770,625 -8,989,619 -8,965,843 -8,545,477 -9,919,501 -10,491,417 -10,943,043 -15,308,442 -19,663,324 -20,039,396 -20,252,139 -20,111,847 -20,488,649 -21,828,372 -22,435,299 -23,015,698 -115,974,466 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 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 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 0 0 0 0 0 0 0 0 0 0 minus: -Debt interest payment ($) 0 89,816,499 88,597,952 87,311,166 85,952,321 84,517,380 83,002,082 81,401,927 79,712,164 77,927,774 76,043,459 74,053,621 71,952,353 69,733,414 67,390,214 64,915,795 62,302,808 59,543,495 56,629,659 53,552,649 50,303,327 46,872,042 43,248,605 39,422,256 35,381,631 31,114,732 26,608,886 21,850,712 16,826,081 11,520,070 5,916,923 -Total federal tax depreciation ($) 0 56,803,590 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 113,607,179 56,803,590 0 0 0 0 0 0 0 0 0 +Debt interest payment ($) 0 88,683,580 87,480,404 86,209,849 84,868,143 83,451,302 81,955,118 80,375,147 78,706,698 76,944,816 75,084,269 73,119,531 71,044,767 68,853,817 66,540,174 64,096,966 61,516,939 58,792,431 55,915,350 52,877,152 49,668,815 46,280,812 42,703,080 38,924,995 34,935,338 30,722,260 26,273,249 21,575,094 16,613,842 11,374,760 5,842,289 +Total federal tax depreciation ($) 0 56,087,086 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 112,174,171 56,087,086 0 0 0 0 0 0 0 0 0 equals: -Federal taxable income ($) 0 112,994,925 63,624,440 67,760,403 71,595,986 74,817,590 76,081,540 72,802,593 73,043,590 88,395,068 93,591,554 98,243,777 102,588,058 105,894,371 106,219,635 100,405,103 113,885,279 124,122,152 129,935,495 135,469,024 140,540,594 196,732,857 248,628,846 246,817,999 266,776,894 274,594,707 281,694,175 288,611,079 294,743,459 298,230,853 1,539,489,256 +Federal taxable income ($) 0 117,773,196 69,054,805 73,179,140 77,068,905 80,741,845 83,683,051 84,549,060 81,036,901 83,662,411 97,826,983 103,002,103 107,725,622 112,309,603 116,524,014 119,433,504 119,117,633 113,532,771 131,787,655 139,385,967 145,386,143 203,383,590 261,241,301 266,237,696 269,064,137 267,200,258 272,206,336 290,005,514 298,068,976 305,779,991 1,540,803,615 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 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 -23,728,934 -13,361,132 -14,229,685 -15,035,157 -15,711,694 -15,977,123 -15,288,545 -15,339,154 -18,562,964 -19,654,226 -20,631,193 -21,543,492 -22,237,818 -22,306,123 -21,085,072 -23,915,909 -26,065,652 -27,286,454 -28,448,495 -29,513,525 -41,313,900 -52,212,058 -51,831,780 -56,023,148 -57,664,889 -59,155,777 -60,608,327 -61,896,126 -62,628,479 -323,292,744 +Federal tax benefit (liability) ($) 0 -24,732,371 -14,501,509 -15,367,619 -16,184,470 -16,955,788 -17,573,441 -17,755,303 -17,017,749 -17,569,106 -20,543,667 -21,630,442 -22,622,381 -23,585,017 -24,470,043 -25,081,036 -25,014,703 -23,841,882 -27,675,407 -29,271,053 -30,531,090 -42,710,554 -54,860,673 -55,909,916 -56,503,469 -56,112,054 -57,163,331 -60,901,158 -62,594,485 -64,213,798 -323,568,759 CASH INCENTIVES Federal IBI income ($) 0 @@ -378,30 +376,30 @@ Federal PTC income ($) 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 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 0 0 0 0 0 0 0 0 0 0 -Federal ITC percent income ($) 0 801,933,028 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 -Federal ITC total income ($) 0 801,933,028 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 +Federal ITC percent income ($) 0 791,817,681 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 +Federal ITC total income ($) 0 791,817,681 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 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 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 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 0 0 0 0 0 0 0 0 0 0 DEBT REPAYMENT -Debt balance ($) 1,603,866,057 1,582,106,287 1,559,127,971 1,534,862,869 1,509,238,921 1,482,180,032 1,453,605,845 1,423,431,504 1,391,567,400 1,357,918,906 1,322,386,096 1,284,863,449 1,245,239,534 1,203,396,679 1,159,210,625 1,112,550,151 1,063,276,691 1,011,243,917 956,297,308 898,273,689 837,000,747 772,296,520 703,968,857 631,814,844 555,620,207 475,158,670 390,191,287 300,465,731 205,715,543 105,659,345 0 -Debt interest payment ($) 0 89,816,499 88,597,952 87,311,166 85,952,321 84,517,380 83,002,082 81,401,927 79,712,164 77,927,774 76,043,459 74,053,621 71,952,353 69,733,414 67,390,214 64,915,795 62,302,808 59,543,495 56,629,659 53,552,649 50,303,327 46,872,042 43,248,605 39,422,256 35,381,631 31,114,732 26,608,886 21,850,712 16,826,081 11,520,070 5,916,923 -Debt principal payment ($) 0 21,759,769 22,978,316 24,265,102 25,623,948 27,058,889 28,574,187 30,174,341 31,864,104 33,648,494 35,532,810 37,522,647 39,623,915 41,842,855 44,186,054 46,660,474 49,273,460 52,032,774 54,946,609 58,023,619 61,272,942 64,704,227 68,327,663 72,154,013 76,194,637 80,461,537 84,967,383 89,725,556 94,750,188 100,056,198 105,659,345 -Debt total payment ($) 0 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 +Debt balance ($) 1,583,635,362 1,562,150,064 1,539,461,590 1,515,502,560 1,490,201,825 1,463,484,249 1,435,270,489 1,405,476,758 1,374,014,578 1,340,790,516 1,305,705,907 1,268,656,560 1,229,532,449 1,188,217,387 1,144,588,683 1,098,516,771 1,049,864,832 998,488,384 944,234,855 886,943,129 826,443,066 762,554,999 695,089,201 623,845,318 548,611,777 469,165,159 385,269,529 296,675,745 203,120,708 104,326,589 0 +Debt interest payment ($) 0 88,683,580 87,480,404 86,209,849 84,868,143 83,451,302 81,955,118 80,375,147 78,706,698 76,944,816 75,084,269 73,119,531 71,044,767 68,853,817 66,540,174 64,096,966 61,516,939 58,792,431 55,915,350 52,877,152 49,668,815 46,280,812 42,703,080 38,924,995 34,935,338 30,722,260 26,273,249 21,575,094 16,613,842 11,374,760 5,842,289 +Debt principal payment ($) 0 21,485,298 22,688,475 23,959,029 25,300,735 26,717,576 28,213,760 29,793,731 31,462,180 33,224,062 35,084,609 37,049,347 39,124,111 41,315,061 43,628,705 46,071,912 48,651,939 51,376,448 54,253,529 57,291,726 60,500,063 63,888,067 67,465,798 71,243,883 75,233,540 79,446,619 83,895,629 88,593,785 93,555,037 98,794,119 104,326,589 +Debt total payment ($) 0 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 DSCR (DEBT FRACTION) -EBITDA ($) 0 268,120,008 270,618,508 273,778,994 276,544,431 278,573,580 278,417,368 273,291,465 271,860,838 286,583,414 290,286,717 293,299,270 295,869,272 297,205,508 295,212,055 286,485,450 298,367,277 306,615,353 309,952,424 312,825,446 315,029,424 315,216,337 310,591,451 304,817,954 322,238,507 326,377,858 329,505,848 332,185,206 333,754,531 332,198,407 1,661,281,714 +EBITDA ($) 0 271,408,511 273,907,053 277,071,267 279,912,106 282,444,662 284,111,065 283,462,287 278,017,323 279,078,570 292,448,745 296,048,652 299,052,941 301,791,002 304,008,984 304,694,260 301,774,587 293,044,850 309,796,677 314,928,707 318,172,172 321,059,929 323,607,705 325,202,088 324,251,614 318,034,365 318,968,234 333,408,979 337,118,117 340,170,448 1,662,620,369 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 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 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 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 0 0 0 0 0 0 0 0 0 0 equals: -Cash available for debt service (CAFDS) ($) 0 268,120,008 270,618,508 273,778,994 276,544,431 278,573,580 278,417,368 273,291,465 271,860,838 286,583,414 290,286,717 293,299,270 295,869,272 297,205,508 295,212,055 286,485,450 298,367,277 306,615,353 309,952,424 312,825,446 315,029,424 315,216,337 310,591,451 304,817,954 322,238,507 326,377,858 329,505,848 332,185,206 333,754,531 332,198,407 1,661,281,714 -Debt total payment ($) 0 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 111,576,269 -DSCR (pre-tax) 0.0 2.40 2.43 2.45 2.48 2.50 2.50 2.45 2.44 2.57 2.60 2.63 2.65 2.66 2.65 2.57 2.67 2.75 2.78 2.80 2.82 2.83 2.78 2.73 2.89 2.93 2.95 2.98 2.99 2.98 14.89 +Cash available for debt service (CAFDS) ($) 0 271,408,511 273,907,053 277,071,267 279,912,106 282,444,662 284,111,065 283,462,287 278,017,323 279,078,570 292,448,745 296,048,652 299,052,941 301,791,002 304,008,984 304,694,260 301,774,587 293,044,850 309,796,677 314,928,707 318,172,172 321,059,929 323,607,705 325,202,088 324,251,614 318,034,365 318,968,234 333,408,979 337,118,117 340,170,448 1,662,620,369 +Debt total payment ($) 0 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 110,168,878 +DSCR (pre-tax) 0.0 2.46 2.49 2.51 2.54 2.56 2.58 2.57 2.52 2.53 2.65 2.69 2.71 2.74 2.76 2.77 2.74 2.66 2.81 2.86 2.89 2.91 2.94 2.95 2.94 2.89 2.90 3.03 3.06 3.09 15.09 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 0 0 0 0 0 0 0 0 0 0 diff --git a/tests/examples/Fervo_Project_Cape-4.txt b/tests/examples/Fervo_Project_Cape-4.txt index 9bfbb44d..f07c8196 100644 --- a/tests/examples/Fervo_Project_Cape-4.txt +++ b/tests/examples/Fervo_Project_Cape-4.txt @@ -27,7 +27,7 @@ Capital Cost for Power Plant for Electricity Generation, 1900, -- https://better Exploration Capital Cost, 30, -- Estimate significantly higher exploration costs than default correlation in consideration of potential risks associated with second/third/fourth-of-a-kind EGS projects Well Drilling Cost Correlation, 3, -- VERTICAL_LARGE (2025 NREL Geothermal Drilling Cost Curve Update) -Well Drilling and Completion Capital Cost Adjustment Factor, 0.84, -- Adjust correlation-calculated value of $4.72M/well to $3.96M/well per Tim Latimer on 2025-02-12 Volts podcast: less than $4M/well +Well Drilling and Completion Capital Cost Adjustment Factor, 0.8, -- Adjust correlation-calculated value of $4.72M/well to $3.96M/well per Tim Latimer on 2025-02-12 Volts podcast: less than $4M/well Reservoir Stimulation Capital Cost Adjustment Factor, 2.66, -- Estimated cost of ~$2M per well. Typical range for Nth-of-kind projects may be $0.5–2M. Field Gathering System Capital Cost Adjustment Factor, 0.54, -- Gathering costs represent 2% of facilities CAPEX per https://www.linkedin.com/pulse/fervo-energy-technology-day-2024-entering-geothermal-decade-matson-n4stc/ @@ -50,8 +50,8 @@ Reservoir Thermal Conductivity, 3.05 Reservoir Porosity, 0.0118 Reservoir Impedance, 0.001565 -Reservoir Volume Option, 2, -- RES_VOL_FRAC_SEP (Specify reservoir volume and fracture separation) -Reservoir Volume, 5418039158, -- Based on 102 fractures per well +Reservoir Volume Option, 1, -- FRAC_NUM_SEP: Reservoir volume calculated with fracture separation and number of fractures as input +Number of Fractures, 12036, -- 102 fractures per well Fracture Separation, 18, -- Per https://eartharxiv.org/repository/view/7665/, lateral length is 4700 ft = 1432 m. Dividing 1432 by 80 = ~18 m fracture spacing. Fracture Shape, 3, -- Square Fracture Height, 165.3, -- Based on total fracture surface area of 30 million ft^2 per well https://pangea.stanford.edu/ERE/pdf/IGAstandard/SGW/2025/Fercho.pdf @@ -74,9 +74,10 @@ Utilization Factor, .9 Plant Outlet Pressure, 1000 psi, -- https://doi.org/10.31223/X5VH8C Production Wellhead Pressure, 325 psi, -- https://doi.org/10.31223/X5VH8C Circulation Pump Efficiency, 0.80 -Well Geometry Configuration, 4 -Number of Multilateral Sections, 3 -Nonvertical Length per Multilateral Section, 4700 feet + +Well Geometry Configuration, 4, -- L +Number of Multilateral Sections, 0, -- This parameter is set to 0 because, for this case study, the cost of horizontal drilling (which would otherwise account for approximately 118 multilateral sections) is included within the 'vertical drilling cost.' This approach allows us to more directly convey the overall well drilling and completion cost, which is under $4 million. +Nonvertical Length per Multilateral Section, 4700 feet, -- Deployment of Enhanced Geothermal System Technology Leads to Rapid Cost Reductions and Performance Improvements. p. 3. https://doi.org/10.31223/X5VH8C Multilaterals Cased, True # *** SIMULATION PARAMETERS *** diff --git a/tests/geophires_x_client_tests/caching-test-result.out b/tests/geophires_x_client_tests/caching-test-result.out new file mode 100644 index 00000000..4d2d6d74 --- /dev/null +++ b/tests/geophires_x_client_tests/caching-test-result.out @@ -0,0 +1,489 @@ + ***************** + ***CASE REPORT*** + ***************** + +Simulation Metadata +---------------------- + GEOPHIRES Version: 3.9.16 + Simulation Date: 2025-06-19 + Simulation Time: 09:30 + Calculation Time: 0.041 sec + + ***SUMMARY OF RESULTS*** + + End-Use Option: Electricity + Average Net Electricity Production: 3.51 MW + Electricity breakeven price: 13.60 cents/kWh + Number of production wells: 2 + Number of injection wells: 2 + Flowrate per production well: 50.0 kg/sec + Well depth: 3.0 kilometer + Geothermal gradient: 50 degC/km + + + ***ECONOMIC PARAMETERS*** + + Economic Model = Standard Levelized Cost + Interest Rate: 7.00 % + Accrued financing during construction: 0.00 % + Project lifetime: 30 yr + Capacity factor: 90.0 % + Project NPV: -31.59 MUSD + Project IRR: -5.22 % + Project VIR=PI=PIR: 0.20 + Project MOIC: -0.33 + Project Payback Period: N/A + Estimated Jobs Created: 8 + + ***ENGINEERING PARAMETERS*** + + Number of Production Wells: 2 + Number of Injection Wells: 2 + Well depth: 3.0 kilometer + Water loss rate: 0.0 % + Pump efficiency: 75.0 % + Injection temperature: 70.0 degC + Production Wellbore heat transmission calculated with Ramey's model + Average production well temperature drop: 3.3 degC + Flowrate per production well: 50.0 kg/sec + Injection well casing ID: 8.000 in + Production well casing ID: 8.000 in + Number of times redrilling: 0 + Power plant type: Subcritical ORC + + + ***RESOURCE CHARACTERISTICS*** + + Maximum reservoir temperature: 400.0 degC + Number of segments: 1 + Geothermal gradient: 50 degC/km + + + ***RESERVOIR PARAMETERS*** + + Reservoir Model = Annual Percentage Thermal Drawdown Model + Annual Thermal Drawdown: 0.500 1/year + Bottom-hole temperature: 165.00 degC + Warning: the reservoir dimensions and thermo-physical properties + listed below are default values if not provided by the user. + They are only used for calculating remaining heat content. + Reservoir volume: 125000000 m**3 + Reservoir hydrostatic pressure: 28892.26 kPa + Plant outlet pressure: 976.68 kPa + Production wellhead pressure: 1045.63 kPa + Productivity Index: 10.00 kg/sec/bar + Injectivity Index: 10.00 kg/sec/bar + Reservoir density: 2700.00 kg/m**3 + Reservoir thermal conductivity: 3.00 W/m/K + Reservoir heat capacity: 1000.00 J/kg/K + + + ***RESERVOIR SIMULATION RESULTS*** + + Maximum Production Temperature: 159.9 degC + Average Production Temperature: 154.5 degC + Minimum Production Temperature: 148.0 degC + Initial Production Temperature: 159.5 degC + Average Reservoir Heat Extraction: 35.23 MW + Production Wellbore Heat Transmission Model = Ramey Model + Average Production Well Temperature Drop: 3.3 degC + Average Injection Well Pump Pressure Drop: -421.5 kPa + Average Production Well Pump Pressure Drop: 235.6 kPa + + + ***CAPITAL COSTS (M$)*** + + Drilling and completion costs: 16.42 MUSD + Drilling and completion costs per well: 4.11 MUSD + Stimulation costs: 3.02 MUSD + Surface power plant costs: 13.68 MUSD + Field gathering system costs: 2.11 MUSD + Total surface equipment costs: 15.79 MUSD + Exploration costs: 4.31 MUSD + Total capital costs: 39.54 MUSD + + + ***OPERATING AND MAINTENANCE COSTS (M$/yr)*** + + Wellfield maintenance costs: 0.32 MUSD/yr + Power plant maintenance costs: 0.62 MUSD/yr + Water costs: 0.00 MUSD/yr + Total operating and maintenance costs: 0.95 MUSD/yr + + + ***SURFACE EQUIPMENT SIMULATION RESULTS*** + + Initial geofluid availability: 0.11 MW/(kg/s) + Maximum Total Electricity Generation: 3.95 MW + Average Total Electricity Generation: 3.54 MW + Minimum Total Electricity Generation: 3.07 MW + Initial Total Electricity Generation: 3.92 MW + Maximum Net Electricity Generation: 3.94 MW + Average Net Electricity Generation: 3.51 MW + Minimum Net Electricity Generation: 3.01 MW + Initial Net Electricity Generation: 3.91 MW + Average Annual Total Electricity Generation: 27.88 GWh + Average Annual Net Electricity Generation: 27.61 GWh + Initial pumping power/net installed power: 0.23 % + Average Pumping Power: 0.03 MW + Heat to Power Conversion Efficiency: 9.94 % + + ************************************************************ + * 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 159.53 0.0089 3.9099 10.4810 + 2 1.0026 159.94 0.0099 3.9416 10.5180 + 3 1.0018 159.82 0.0115 3.9306 10.5025 + 4 1.0001 159.54 0.0131 3.9065 10.4708 + 5 0.9979 159.20 0.0148 3.8777 10.4331 + 6 0.9956 158.83 0.0166 3.8465 10.3925 + 7 0.9932 158.44 0.0183 3.8140 10.3501 + 8 0.9906 158.04 0.0201 3.7806 10.3065 + 9 0.9881 157.63 0.0218 3.7468 10.2621 + 10 0.9854 157.21 0.0236 3.7126 10.2172 + 11 0.9828 156.78 0.0253 3.6782 10.1718 + 12 0.9801 156.36 0.0271 3.6436 10.1261 + 13 0.9774 155.93 0.0289 3.6090 10.0802 + 14 0.9747 155.49 0.0306 3.5744 10.0340 + 15 0.9720 155.06 0.0324 3.5398 9.9878 + 16 0.9692 154.62 0.0341 3.5053 9.9414 + 17 0.9665 154.18 0.0359 3.4708 9.8949 + 18 0.9637 153.74 0.0376 3.4364 9.8484 + 19 0.9609 153.30 0.0394 3.4021 9.8019 + 20 0.9582 152.86 0.0411 3.3679 9.7553 + 21 0.9554 152.41 0.0428 3.3339 9.7087 + 22 0.9526 151.97 0.0446 3.3000 9.6621 + 23 0.9498 151.52 0.0463 3.2662 9.6155 + 24 0.9470 151.08 0.0480 3.2326 9.5690 + 25 0.9442 150.63 0.0497 3.1991 9.5224 + 26 0.9414 150.18 0.0514 3.1658 9.4759 + 27 0.9386 149.73 0.0531 3.1327 9.4294 + 28 0.9358 149.28 0.0548 3.0997 9.3829 + 29 0.9330 148.83 0.0565 3.0669 9.3365 + 30 0.9301 148.38 0.0582 3.0342 9.2901 + + + ******************************************************************* + * 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 30.9 294.7 31.00 3.31 + 2 31.0 295.3 29.94 6.62 + 3 30.9 294.6 28.88 9.93 + 4 30.7 293.6 27.82 13.23 + 5 30.4 292.4 26.77 16.51 + 6 30.2 291.2 25.72 19.78 + 7 29.9 289.9 24.68 23.04 + 8 29.7 288.5 23.64 26.28 + 9 29.4 287.2 22.60 29.50 + 10 29.1 285.8 21.58 32.71 + 11 28.9 284.4 20.55 35.90 + 12 28.6 283.0 19.53 39.08 + 13 28.3 281.6 18.52 42.24 + 14 28.0 280.1 17.51 45.39 + 15 27.8 278.7 16.51 48.52 + 16 27.5 277.3 15.51 51.63 + 17 27.2 275.8 14.52 54.73 + 18 27.0 274.4 13.53 57.81 + 19 26.7 272.9 12.55 60.87 + 20 26.4 271.5 11.57 63.92 + 21 26.2 270.0 10.60 66.95 + 22 25.9 268.5 9.63 69.97 + 23 25.6 267.1 8.67 72.96 + 24 25.4 265.6 7.71 75.95 + 25 25.1 264.1 6.76 78.91 + 26 24.8 262.7 5.82 81.86 + 27 24.6 261.2 4.88 84.79 + 28 24.3 259.7 3.94 87.71 + 29 24.1 258.2 3.01 90.61 + 30 23.8 256.9 2.09 93.49 + + + ******************************** + * REVENUE & CASHFLOW PROFILE * + ******************************** +Year Electricity | Heat | Cooling | Carbon | Project +Since Price Ann. Rev. Cumm. Rev. | Price Ann. Rev. Cumm. Rev. | Price Ann. Rev. Cumm. Rev. | Price Ann. Rev. Cumm. Rev. | OPEX Net Rev. Net Cashflow +Start (cents/kWh)(MUSD/yr) (MUSD) |(cents/kWh) (MUSD/yr) (MUSD) |(cents/kWh) (MUSD/yr) (MUSD) |(USD/lb) (MUSD/yr) (MUSD) |(MUSD/yr) (MUSD/yr) (MUSD) +________________________________________________________________________________________________________________________________________________________________________________________ + 0 0.00 0.00 0.00 | 0.00 0.00 0.00 | 0.00 0.00 0.00 | 0.00 0.00 0.00 | 0.00 -39.54 -39.54 + 1 5.50 1.70 1.70 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.75 -38.78 + 2 5.50 1.71 3.41 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.76 -38.02 + 3 5.50 1.70 5.11 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.75 -37.27 + 4 5.50 1.69 6.80 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.74 -36.53 + 5 5.50 1.67 8.47 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.73 -35.80 + 6 5.50 1.66 10.13 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.71 -35.08 + 7 5.50 1.65 11.78 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.70 -34.38 + 8 5.50 1.63 13.41 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.69 -33.70 + 9 5.50 1.62 15.03 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.67 -33.03 + 10 5.50 1.60 16.63 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.66 -32.37 + 11 5.50 1.59 18.22 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.64 -31.73 + 12 5.50 1.57 19.79 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.63 -31.10 + 13 5.50 1.56 21.35 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.61 -30.49 + 14 5.50 1.54 22.89 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.60 -29.90 + 15 5.50 1.53 24.42 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.58 -29.32 + 16 5.50 1.51 25.93 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.57 -28.75 + 17 5.50 1.50 27.43 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.55 -28.20 + 18 5.50 1.48 28.91 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.54 -27.66 + 19 5.50 1.47 30.38 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.52 -27.14 + 20 5.50 1.45 31.83 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.51 -26.63 + 21 5.50 1.44 33.27 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.49 -26.14 + 22 5.50 1.42 34.69 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.48 -25.66 + 23 5.50 1.41 36.10 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.46 -25.20 + 24 5.50 1.39 37.50 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.45 -24.75 + 25 5.50 1.38 38.88 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.43 -24.32 + 26 5.50 1.37 40.24 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.42 -23.90 + 27 5.50 1.35 41.59 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.40 -23.50 + 28 5.50 1.34 42.93 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.39 -23.11 + 29 5.50 1.32 44.25 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.38 -22.73 + 30 5.50 1.31 45.56 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.36 -22.37 + + ***************** + ***CASE REPORT*** + ***************** + +Simulation Metadata +---------------------- + GEOPHIRES Version: 3.9.16 + Simulation Date: 2025-06-19 + Simulation Time: 09:30 + Calculation Time: 0.041 sec + + ***SUMMARY OF RESULTS*** + + End-Use Option: Electricity + Average Net Electricity Production: 3.51 MW + Electricity breakeven price: 13.60 cents/kWh + Number of production wells: 2 + Number of injection wells: 2 + Flowrate per production well: 50.0 kg/sec + Well depth: 3.0 kilometer + Geothermal gradient: 50 degC/km + + + ***ECONOMIC PARAMETERS*** + + Economic Model = Standard Levelized Cost + Interest Rate: 7.00 % + Accrued financing during construction: 0.00 % + Project lifetime: 30 yr + Capacity factor: 90.0 % + Project NPV: -31.59 MUSD + Project IRR: -5.22 % + Project VIR=PI=PIR: 0.20 + Project MOIC: -0.33 + Project Payback Period: N/A + Estimated Jobs Created: 8 + + ***ENGINEERING PARAMETERS*** + + Number of Production Wells: 2 + Number of Injection Wells: 2 + Well depth: 3.0 kilometer + Water loss rate: 0.0 % + Pump efficiency: 75.0 % + Injection temperature: 70.0 degC + Production Wellbore heat transmission calculated with Ramey's model + Average production well temperature drop: 3.3 degC + Flowrate per production well: 50.0 kg/sec + Injection well casing ID: 8.000 in + Production well casing ID: 8.000 in + Number of times redrilling: 0 + Power plant type: Subcritical ORC + + + ***RESOURCE CHARACTERISTICS*** + + Maximum reservoir temperature: 400.0 degC + Number of segments: 1 + Geothermal gradient: 50 degC/km + + + ***RESERVOIR PARAMETERS*** + + Reservoir Model = Annual Percentage Thermal Drawdown Model + Annual Thermal Drawdown: 0.500 1/year + Bottom-hole temperature: 165.00 degC + Warning: the reservoir dimensions and thermo-physical properties + listed below are default values if not provided by the user. + They are only used for calculating remaining heat content. + Reservoir volume: 125000000 m**3 + Reservoir hydrostatic pressure: 28892.26 kPa + Plant outlet pressure: 976.68 kPa + Production wellhead pressure: 1045.63 kPa + Productivity Index: 10.00 kg/sec/bar + Injectivity Index: 10.00 kg/sec/bar + Reservoir density: 2700.00 kg/m**3 + Reservoir thermal conductivity: 3.00 W/m/K + Reservoir heat capacity: 1000.00 J/kg/K + + + ***RESERVOIR SIMULATION RESULTS*** + + Maximum Production Temperature: 159.9 degC + Average Production Temperature: 154.5 degC + Minimum Production Temperature: 148.0 degC + Initial Production Temperature: 159.5 degC + Average Reservoir Heat Extraction: 35.23 MW + Production Wellbore Heat Transmission Model = Ramey Model + Average Production Well Temperature Drop: 3.3 degC + Average Injection Well Pump Pressure Drop: -421.5 kPa + Average Production Well Pump Pressure Drop: 235.6 kPa + + + ***CAPITAL COSTS (M$)*** + + Drilling and completion costs: 16.42 MUSD + Drilling and completion costs per well: 4.11 MUSD + Stimulation costs: 3.02 MUSD + Surface power plant costs: 13.68 MUSD + Field gathering system costs: 2.11 MUSD + Total surface equipment costs: 15.79 MUSD + Exploration costs: 4.31 MUSD + Total capital costs: 39.54 MUSD + + + ***OPERATING AND MAINTENANCE COSTS (M$/yr)*** + + Wellfield maintenance costs: 0.32 MUSD/yr + Power plant maintenance costs: 0.62 MUSD/yr + Water costs: 0.00 MUSD/yr + Total operating and maintenance costs: 0.95 MUSD/yr + + + ***SURFACE EQUIPMENT SIMULATION RESULTS*** + + Initial geofluid availability: 0.11 MW/(kg/s) + Maximum Total Electricity Generation: 3.95 MW + Average Total Electricity Generation: 3.54 MW + Minimum Total Electricity Generation: 3.07 MW + Initial Total Electricity Generation: 3.92 MW + Maximum Net Electricity Generation: 3.94 MW + Average Net Electricity Generation: 3.51 MW + Minimum Net Electricity Generation: 3.01 MW + Initial Net Electricity Generation: 3.91 MW + Average Annual Total Electricity Generation: 27.88 GWh + Average Annual Net Electricity Generation: 27.61 GWh + Initial pumping power/net installed power: 0.23 % + Average Pumping Power: 0.03 MW + Heat to Power Conversion Efficiency: 9.94 % + + ************************************************************ + * 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 159.53 0.0089 3.9099 10.4810 + 2 1.0026 159.94 0.0099 3.9416 10.5180 + 3 1.0018 159.82 0.0115 3.9306 10.5025 + 4 1.0001 159.54 0.0131 3.9065 10.4708 + 5 0.9979 159.20 0.0148 3.8777 10.4331 + 6 0.9956 158.83 0.0166 3.8465 10.3925 + 7 0.9932 158.44 0.0183 3.8140 10.3501 + 8 0.9906 158.04 0.0201 3.7806 10.3065 + 9 0.9881 157.63 0.0218 3.7468 10.2621 + 10 0.9854 157.21 0.0236 3.7126 10.2172 + 11 0.9828 156.78 0.0253 3.6782 10.1718 + 12 0.9801 156.36 0.0271 3.6436 10.1261 + 13 0.9774 155.93 0.0289 3.6090 10.0802 + 14 0.9747 155.49 0.0306 3.5744 10.0340 + 15 0.9720 155.06 0.0324 3.5398 9.9878 + 16 0.9692 154.62 0.0341 3.5053 9.9414 + 17 0.9665 154.18 0.0359 3.4708 9.8949 + 18 0.9637 153.74 0.0376 3.4364 9.8484 + 19 0.9609 153.30 0.0394 3.4021 9.8019 + 20 0.9582 152.86 0.0411 3.3679 9.7553 + 21 0.9554 152.41 0.0428 3.3339 9.7087 + 22 0.9526 151.97 0.0446 3.3000 9.6621 + 23 0.9498 151.52 0.0463 3.2662 9.6155 + 24 0.9470 151.08 0.0480 3.2326 9.5690 + 25 0.9442 150.63 0.0497 3.1991 9.5224 + 26 0.9414 150.18 0.0514 3.1658 9.4759 + 27 0.9386 149.73 0.0531 3.1327 9.4294 + 28 0.9358 149.28 0.0548 3.0997 9.3829 + 29 0.9330 148.83 0.0565 3.0669 9.3365 + 30 0.9301 148.38 0.0582 3.0342 9.2901 + + + ******************************************************************* + * 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 30.9 294.7 31.00 3.31 + 2 31.0 295.3 29.94 6.62 + 3 30.9 294.6 28.88 9.93 + 4 30.7 293.6 27.82 13.23 + 5 30.4 292.4 26.77 16.51 + 6 30.2 291.2 25.72 19.78 + 7 29.9 289.9 24.68 23.04 + 8 29.7 288.5 23.64 26.28 + 9 29.4 287.2 22.60 29.50 + 10 29.1 285.8 21.58 32.71 + 11 28.9 284.4 20.55 35.90 + 12 28.6 283.0 19.53 39.08 + 13 28.3 281.6 18.52 42.24 + 14 28.0 280.1 17.51 45.39 + 15 27.8 278.7 16.51 48.52 + 16 27.5 277.3 15.51 51.63 + 17 27.2 275.8 14.52 54.73 + 18 27.0 274.4 13.53 57.81 + 19 26.7 272.9 12.55 60.87 + 20 26.4 271.5 11.57 63.92 + 21 26.2 270.0 10.60 66.95 + 22 25.9 268.5 9.63 69.97 + 23 25.6 267.1 8.67 72.96 + 24 25.4 265.6 7.71 75.95 + 25 25.1 264.1 6.76 78.91 + 26 24.8 262.7 5.82 81.86 + 27 24.6 261.2 4.88 84.79 + 28 24.3 259.7 3.94 87.71 + 29 24.1 258.2 3.01 90.61 + 30 23.8 256.9 2.09 93.49 + + + ******************************** + * REVENUE & CASHFLOW PROFILE * + ******************************** +Year Electricity | Heat | Cooling | Carbon | Project +Since Price Ann. Rev. Cumm. Rev. | Price Ann. Rev. Cumm. Rev. | Price Ann. Rev. Cumm. Rev. | Price Ann. Rev. Cumm. Rev. | OPEX Net Rev. Net Cashflow +Start (cents/kWh)(MUSD/yr) (MUSD) |(cents/kWh) (MUSD/yr) (MUSD) |(cents/kWh) (MUSD/yr) (MUSD) |(USD/lb) (MUSD/yr) (MUSD) |(MUSD/yr) (MUSD/yr) (MUSD) +________________________________________________________________________________________________________________________________________________________________________________________ + 0 0.00 0.00 0.00 | 0.00 0.00 0.00 | 0.00 0.00 0.00 | 0.00 0.00 0.00 | 0.00 -39.54 -39.54 + 1 5.50 1.70 1.70 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.75 -38.78 + 2 5.50 1.71 3.41 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.76 -38.02 + 3 5.50 1.70 5.11 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.75 -37.27 + 4 5.50 1.69 6.80 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.74 -36.53 + 5 5.50 1.67 8.47 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.73 -35.80 + 6 5.50 1.66 10.13 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.71 -35.08 + 7 5.50 1.65 11.78 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.70 -34.38 + 8 5.50 1.63 13.41 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.69 -33.70 + 9 5.50 1.62 15.03 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.67 -33.03 + 10 5.50 1.60 16.63 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.66 -32.37 + 11 5.50 1.59 18.22 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.64 -31.73 + 12 5.50 1.57 19.79 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.63 -31.10 + 13 5.50 1.56 21.35 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.61 -30.49 + 14 5.50 1.54 22.89 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.60 -29.90 + 15 5.50 1.53 24.42 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.58 -29.32 + 16 5.50 1.51 25.93 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.57 -28.75 + 17 5.50 1.50 27.43 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.55 -28.20 + 18 5.50 1.48 28.91 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.54 -27.66 + 19 5.50 1.47 30.38 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.52 -27.14 + 20 5.50 1.45 31.83 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.51 -26.63 + 21 5.50 1.44 33.27 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.49 -26.14 + 22 5.50 1.42 34.69 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.48 -25.66 + 23 5.50 1.41 36.10 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.46 -25.20 + 24 5.50 1.39 37.50 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.45 -24.75 + 25 5.50 1.38 38.88 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.43 -24.32 + 26 5.50 1.37 40.24 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.42 -23.90 + 27 5.50 1.35 41.59 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.40 -23.50 + 28 5.50 1.34 42.93 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.39 -23.11 + 29 5.50 1.32 44.25 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.38 -22.73 + 30 5.50 1.31 45.56 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 0.95 0.36 -22.37 diff --git a/tests/geophires_x_client_tests/test_geophires_client_caching.py b/tests/geophires_x_client_tests/test_geophires_client_caching.py new file mode 100644 index 00000000..95cc44d6 --- /dev/null +++ b/tests/geophires_x_client_tests/test_geophires_client_caching.py @@ -0,0 +1,112 @@ +import sys +import unittest +from unittest.mock import patch + +from geophires_x_client import GeophiresXClient +from geophires_x_client.geophires_input_parameters import ImmutableGeophiresInputParameters +from tests.base_test_case import BaseTestCase + + +class GeophiresClientCachingTestCase(BaseTestCase): + """ + Tests the caching functionality of the GeophiresXClient, especially + in conjunction with the content-addressable ImmutableGeophiresInputParameters. + """ + + def _create_mock_output_file(self, *args, **kwargs): + """ + A helper function to be used as a side_effect for mocking geophires.main. + It simulates the behavior of GEOPHIRES by creating an output file based on + the arguments it receives via sys.argv. + """ + # The client sets sys.argv to ['', input_path, output_path] before calling main. + # We read from sys.argv directly to correctly simulate the real process. + output_path_arg = sys.argv[2] + with open(output_path_arg, 'w') as f: + with open(self._get_test_file_path('caching-test-result.out'), encoding='utf-8') as fr: + f.write(fr.read()) + return 0 # Simulate a successful run + + @patch('geophires_x_client.geophires.main') + def test_caching_with_identical_immutable_params(self, mock_geophires_main: unittest.mock.MagicMock): + """ + Verify that when two different ImmutableGeophiresInputParameters objects + have the same content, the GeophiresXClient's caching mechanism is + triggered and the expensive geophires.main function is only called once. + """ + # Arrange + mock_geophires_main.side_effect = self._create_mock_output_file + + client = GeophiresXClient(enable_caching=True) + + # Create two distinct parameter objects with identical content. + params1 = ImmutableGeophiresInputParameters(params={'Reservoir Depth': 3, 'Gradient 1': 50}) + params2 = ImmutableGeophiresInputParameters(params={'Reservoir Depth': 3, 'Gradient 1': 50}) + + # Pre-condition check: Although they are different objects in memory, + # their content-based hashes must be identical for caching to work. + self.assertIsNot(params1, params2, 'Test setup failed: params1 and params2 should be different objects.') + self.assertEqual(hash(params1), hash(params2), 'Hashes of identical-content objects should be equal.') + + # Act + result1 = client.get_geophires_result(params1) + result2 = client.get_geophires_result(params2) + + # Assert + # The core assertion: was the expensive simulation function only called once? + mock_geophires_main.assert_called_once() + + self.assertDictEqual(result1.result, result2.result) + + # TODO The results should probably not only be equivalent but also the *same object*... + # For now they not, but we probably don't care about this since the important part is performance/cache hit - + # manually verified the cache hit in debugger during development. + # self.assertIs(result1, result2, 'The second result should be the cached object instance.') + + @patch('geophires_x_client.geophires.main') + def test_no_caching_with_different_immutable_params(self, mock_geophires_main: unittest.mock.MagicMock): + """ + Verify that when two ImmutableGeophiresInputParameters objects have + different content, the cache is not used and geophires.main is called for each. + """ + # Arrange + mock_geophires_main.side_effect = self._create_mock_output_file + + client = GeophiresXClient(enable_caching=True) + params1 = ImmutableGeophiresInputParameters(params={'Reservoir Depth': 3}) + params2 = ImmutableGeophiresInputParameters(params={'Reservoir Depth': 4}) + + self.assertNotEqual(hash(params1), hash(params2), 'Hashes of different-content objects should not be equal.') + + # Act + client.get_geophires_result(params1) + client.get_geophires_result(params2) + + # Assert + self.assertEqual( + mock_geophires_main.call_count, 2, 'geophires.main should be called for each unique set of parameters.' + ) + + @patch('geophires_x_client.geophires.main') + def test_no_caching_when_disabled(self, mock_geophires_main: unittest.mock.MagicMock): + """ + Verify that even with identical parameters, geophires.main is called + multiple times if the client has caching disabled. + """ + # Arrange + mock_geophires_main.side_effect = self._create_mock_output_file + + client = GeophiresXClient(enable_caching=False) # Caching is explicitly disabled + params1 = ImmutableGeophiresInputParameters(params={'Reservoir Depth': 3}) + params2 = ImmutableGeophiresInputParameters(params={'Reservoir Depth': 3}) + + self.assertEqual(hash(params1), hash(params2)) + + # Act + client.get_geophires_result(params1) + client.get_geophires_result(params2) + + # Assert + self.assertEqual( + mock_geophires_main.call_count, 2, 'geophires.main should be called twice when caching is disabled.' + ) diff --git a/tests/geophires_x_client_tests/test_geophires_input_parameters.py b/tests/geophires_x_client_tests/test_geophires_input_parameters.py index f105f11a..cde46779 100644 --- a/tests/geophires_x_client_tests/test_geophires_input_parameters.py +++ b/tests/geophires_x_client_tests/test_geophires_input_parameters.py @@ -1,18 +1,22 @@ +import copy import tempfile import uuid from pathlib import Path +from types import MappingProxyType from geophires_x_client import GeophiresInputParameters from geophires_x_client import GeophiresXClient +from geophires_x_client.geophires_input_parameters import ImmutableGeophiresInputParameters from tests.base_test_case import BaseTestCase class GeophiresInputParametersTestCase(BaseTestCase): - def test_id(self): + def test_internal_id_and_hash(self): input_1 = GeophiresInputParameters(from_file_path=self._get_test_file_path('client_test_input_1.txt')) input_2 = GeophiresInputParameters(from_file_path=self._get_test_file_path('client_test_input_2.txt')) self.assertIsNot(input_1._id, input_2._id) + self.assertNotEqual(hash(input_1), hash(input_2)) def test_init_with_input_file(self): file_path = self._get_test_file_path('client_test_input_1.txt') @@ -51,3 +55,78 @@ def test_input_file_comments(self): GeophiresInputParameters(from_file_path=self._get_test_file_path('input_comments.txt')) ) self.assertIsNotNone(result) + + +class ImmutableGeophiresInputParametersTestCase(BaseTestCase): + def test_init_with_file_path_as_string(self): + """Verify that the class can be initialized with a string path without raising an AttributeError.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as tmp_file: + tmp_file_path = tmp_file.name + tmp_file.write('key,value\n') + + # This should not raise an AttributeError + params = ImmutableGeophiresInputParameters(from_file_path=tmp_file_path) + + # Verify the path was correctly converted and can be used + self.assertTrue(params.as_file_path().exists()) + self.assertIsInstance(params.from_file_path, Path) + + # Clean up the temporary file + Path(tmp_file_path).unlink() + + def test_hash_equality(self): + """Verify that two objects with the same content have the same hash.""" + params = {'Reservoir Depth': 3, 'Gradient 1': 50} + p1 = ImmutableGeophiresInputParameters(params=params) + p2 = ImmutableGeophiresInputParameters(params=params) + + self.assertIsNot(p1, p2) + self.assertEqual(hash(p1), hash(p2)) + + def test_hash_inequality(self): + """Verify that two objects with different content have different hashes.""" + p1 = ImmutableGeophiresInputParameters(params={'Reservoir Depth': 3}) + p2 = ImmutableGeophiresInputParameters(params={'Reservoir Depth': 4}) + self.assertNotEqual(hash(p1), hash(p2)) + + def test_immutability_of_params(self): + """Verify that the params dictionary is an immutable mapping proxy.""" + p1 = ImmutableGeophiresInputParameters(params={'Reservoir Depth': 3}) + self.assertIsInstance(p1.params, MappingProxyType) + + with self.assertRaises(TypeError): + # This should fail because MappingProxyType is read-only + p1.params['Reservoir Depth'] = 4 + + def test_combining_file_and_params_with_no_trailing_newline(self): + """Verify that combining a base file and params works correctly when the base file lacks a trailing newline.""" + # Arrange + base_content = 'base_key,base_value' # Note: no trailing newline + with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False, newline='') as tmp_file: + base_file_path = Path(tmp_file.name) + tmp_file.write(base_content) + + # Act + params = ImmutableGeophiresInputParameters(from_file_path=base_file_path, params={'new_key': 'new_value'}) + combined_file_path = params.as_file_path() + combined_content = combined_file_path.read_text() + + # Assert + expected_content = 'base_key,base_value\nnew_key, new_value\n' + self.assertEqual(expected_content, combined_content) + + # Clean up the temporary file + base_file_path.unlink() + + def test_deepcopy(self): + """Verify that copy.deepcopy works on an instance without raising a TypeError.""" + p1 = ImmutableGeophiresInputParameters(params={'Reservoir Depth': 3}) + p2 = None + + try: + p2 = copy.deepcopy(p1) + except TypeError: + self.fail('copy.deepcopy(ImmutableGeophiresInputParameters) raised TypeError unexpectedly!') + + # For an immutable object, deepcopy should ideally return the same instance. + self.assertIs(p1, p2) diff --git a/tests/geophires_x_client_tests/test_imperative_instantiation_in_subprocess.py b/tests/geophires_x_client_tests/test_imperative_instantiation_in_subprocess.py new file mode 100644 index 00000000..e45810cc --- /dev/null +++ b/tests/geophires_x_client_tests/test_imperative_instantiation_in_subprocess.py @@ -0,0 +1,71 @@ +# ruff: noqa: S603 + +import subprocess +import sys +import tempfile +from pathlib import Path + +from base_test_case import BaseTestCase + + +class GeophiresClientImperativeInstantiationTestCase(BaseTestCase): + + # noinspection PyMethodMayBeStatic + def test_imperative_instantiation_in_subprocess(self): + """ + Verifies that GeophiresXClient can be instantiated at the global scope + in a script without causing a multiprocessing-related RuntimeError. + + This test directly simulates the failure condition by writing and executing + a separate Python script as a subprocess. This ensures that the fix + (checking for 'MainProcess') is working correctly on systems that use + the 'spawn' start method for multiprocessing (like macOS and Windows). + """ + project_root = Path(__file__).parent.parent.resolve() + + script_content = f""" +import sys +# We must add the project root to the path for the import to work. +sys.path.insert(0, r'{project_root}') + +from geophires_x_client import GeophiresXClient + +print("Attempting to instantiate GeophiresXClient at the global scope...") + +# This is the line that would have previously crashed with a RuntimeError. +client = GeophiresXClient() + +print("Instantiation successful.") + +# It is critical to shut down the client to release the manager process, +# otherwise it can linger and interfere with other tests in the suite. +GeophiresXClient.shutdown() + +print("Shutdown successful.") + +# A final message to confirm the script completed without errors. +print("SUCCESS") +""" + + with tempfile.TemporaryDirectory() as tmpdir: + test_script_path = Path(tmpdir) / 'run_client_test.py' + test_script_path.write_text(script_content) + + # fmt:off + result = subprocess.run( + [sys.executable, str(test_script_path)], + capture_output=True, + text=True, + timeout=60 + ) + # fmt:on + + assert result.returncode == 0, ( + f'Subprocess failed with exit code {result.returncode}. This indicates a crash.\\n' + f'--- STDOUT ---\\n{result.stdout}\\n' + f'--- STDERR ---\\n{result.stderr}' + ) + + assert 'SUCCESS' in result.stdout, ( + "Subprocess completed but did not print the final 'SUCCESS' message.\\n" f"--- STDOUT ---\\n{result.stdout}" + ) diff --git a/tests/geophires_x_client_tests/test_multiprocessing_safety.py b/tests/geophires_x_client_tests/test_multiprocessing_safety.py new file mode 100644 index 00000000..aa2d768d --- /dev/null +++ b/tests/geophires_x_client_tests/test_multiprocessing_safety.py @@ -0,0 +1,133 @@ +import logging +import multiprocessing +import sys +import time +import unittest +from logging.handlers import QueueHandler +from queue import Empty + +# Important: We must be able to import the client and all parameter classes +from geophires_x_client import GeophiresXClient +from geophires_x_client.geophires_input_parameters import EndUseOption +from geophires_x_client.geophires_input_parameters import ImmutableGeophiresInputParameters + + +def run_client_in_process(params_dict: dict, log_queue: multiprocessing.Queue, result_queue: multiprocessing.Queue): + """ + This is the function that each worker process will execute. + It must be a top-level function to be picklable by multiprocessing. + """ + # Configure logging for this worker process to send messages to the shared queue. + root_logger = logging.getLogger() + root_logger.setLevel(logging.INFO) + root_logger.handlers = [QueueHandler(log_queue)] + + try: + # The client will use the Manager that was injected by the test's main process. + client = GeophiresXClient(enable_caching=True) + params = ImmutableGeophiresInputParameters(params_dict) + result = client.get_geophires_result(params) + result_queue.put(result.direct_use_heat_breakeven_price_USD_per_MMBTU) + except Exception as e: + result_queue.put(e) + + +class TestMultiprocessingSafety(unittest.TestCase): + def setUp(self): + """Set up a unique set of parameters for each test.""" + self.params_dict = { + 'Print Output to Console': 0, + 'End-Use Option': EndUseOption.DIRECT_USE_HEAT.value, + 'Reservoir Model': 1, + 'Time steps per year': 1, + 'Reservoir Depth': 4 + time.time_ns() / 1e19, + 'Gradient 1': 50, + 'Maximum Temperature': 550, + } + + def test_client_runs_real_geophires_and_caches_across_processes(self): + """ + Tests that GeophiresXClient can run the real geophires.main in multiple + processes and that the cache is shared between them. This test is now + fully self-contained to prevent resource conflicts with the test runner. + """ + if sys.platform == 'win32': + self.skipTest("The 'fork' multiprocessing context is not available on Windows.") + + ctx = multiprocessing.get_context('fork') + # Use the Manager as a context manager. This is the key to ensuring + # all resources it creates (queues, etc.) are properly shut down + # at the end of the block, preventing deadlocks. + with ctx.Manager() as manager: + # For this test to work, we MUST inject the test-specific manager + # into the client's class-level singleton attributes. + GeophiresXClient._manager = manager + GeophiresXClient._cache = manager.dict() + GeophiresXClient._lock = manager.RLock() + + log_queue = manager.Queue() + result_queue = manager.Queue() + + num_processes = 4 + process_timeout_seconds = 15 + + processes = [ + ctx.Process(target=run_client_in_process, args=(self.params_dict, log_queue, result_queue)) + for _ in range(num_processes) + ] + + for p in processes: + p.start() + + # --- Robust Result Collection --- + results = [] + for i in range(num_processes): + try: + result = result_queue.get(timeout=process_timeout_seconds) + results.append(result) + except Empty: + for p_cleanup in processes: + if p_cleanup.is_alive(): + p_cleanup.terminate() + self.fail( + f'Test timed out waiting for result #{i + 1}. A worker process likely crashed or is stuck.' + ) + + # --- Process Cleanup --- + for p in processes: + p.join(timeout=process_timeout_seconds) + if p.is_alive(): + p.terminate() + self.fail(f'Process {p.pid} failed to terminate cleanly.') + + # --- Assertions --- + for r in results: + self.assertNotIsInstance(r, Exception, f'A process failed with an exception: {r}') + self.assertIsNotNone(r) + self.assertIsInstance(r, float) + + log_records = [] + while not log_queue.empty(): + log_records.append(log_queue.get().getMessage()) + + cache_indicator_log = 'GEOPHIRES-X output file:' + successful_runs = sum(1 for record in log_records if cache_indicator_log in record) + + self.assertEqual( + successful_runs, + 1, + f'FAIL: GEOPHIRES was run {successful_runs} times instead of once, indicating the cache failed.', + ) + + print( + f'\nTest passed: Detected {successful_runs} non-cached GEOPHIRES run(s) for {num_processes} requests.' + ) + + # CRITICAL: Reset the client's singleton state after the test to not interfere with other tests. + GeophiresXClient._manager = None + GeophiresXClient._cache = None + GeophiresXClient._lock = None + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/geophires_x_tests/test_economics.py b/tests/geophires_x_tests/test_economics.py index 79689736..f5d8121a 100644 --- a/tests/geophires_x_tests/test_economics.py +++ b/tests/geophires_x_tests/test_economics.py @@ -125,7 +125,7 @@ def _get_result(peaking_boiler_cost_: int) -> GeophiresXResult: GeophiresInputParameters( from_file_path=self._get_test_file_path('../examples/example12_DH.txt'), params={ - 'Peaking Boiler Cost per KW': peaking_boiler_cost_, + 'Peaking Boiler Cost per kW': peaking_boiler_cost_, }, ) ) diff --git a/tests/geophires_x_tests/test_economics_sam.py b/tests/geophires_x_tests/test_economics_sam.py index 54eec89d..7d3bc1f0 100644 --- a/tests/geophires_x_tests/test_economics_sam.py +++ b/tests/geophires_x_tests/test_economics_sam.py @@ -17,11 +17,11 @@ # noinspection PyProtectedMember from geophires_x.EconomicsSam import ( calculate_sam_economics, - _sig_figs, get_sam_cash_flow_profile_tabulated_output, _ppa_pricing_model, _get_fed_and_state_tax_rates, ) +from geophires_x.GeoPHIRESUtils import sig_figs # noinspection PyProtectedMember from geophires_x.EconomicsSamCashFlow import _clean_profile, _is_category_row_label, _is_designator_row_label @@ -459,8 +459,8 @@ def test_is_designator_row_label(self): self.assertTrue(_is_designator_row_label('plus PBI if not available for debt service:')) def test_sig_figs(self): - self.assertListEqual(_sig_figs([1.14, 2.24], 2), [1.1, 2.2]) - self.assertListEqual(_sig_figs((1.14, 2.24), 2), [1.1, 2.2]) + self.assertListEqual(sig_figs([1.14, 2.24], 2), [1.1, 2.2]) + self.assertListEqual(sig_figs((1.14, 2.24), 2), [1.1, 2.2]) def test_get_fed_and_state_tax_rates(self): self.assertEqual(([21], [7]), _get_fed_and_state_tax_rates(0.28)) diff --git a/tests/geophires_x_tests/test_fervo_project_cape_4.py b/tests/geophires_x_tests/test_fervo_project_cape_4.py new file mode 100644 index 00000000..542114c1 --- /dev/null +++ b/tests/geophires_x_tests/test_fervo_project_cape_4.py @@ -0,0 +1,305 @@ +from __future__ import annotations + +import re +from typing import Any + +from base_test_case import BaseTestCase +from geophires_x.GeoPHIRESUtils import sig_figs +from geophires_x.Parameter import HasQuantity +from geophires_x_client import GeophiresInputParameters +from geophires_x_client import GeophiresXClient +from geophires_x_client import GeophiresXResult + + +class FervoProjectCape4TestCase(BaseTestCase): + + def test_fervo_project_cape_4_results_against_reference_values(self): + """ + Asserts that results conform to some of the key reference values claimed in docs/Fervo_Project_Cape-4.md. + """ + + r = GeophiresXClient().get_geophires_result( + GeophiresInputParameters(from_file_path=self._get_test_file_path('../examples/Fervo_Project_Cape-4.txt')) + ) + + min_net_gen = r.result['SURFACE EQUIPMENT SIMULATION RESULTS']['Minimum Net Electricity Generation']['value'] + self.assertGreater(min_net_gen, 500) + self.assertLess(min_net_gen, 505) + + max_total_gen = r.result['SURFACE EQUIPMENT SIMULATION RESULTS']['Maximum Total Electricity Generation'][ + 'value' + ] + self.assertGreater(max_total_gen, 600) + self.assertLess(max_total_gen, 650) + + lcoe = r.result['SUMMARY OF RESULTS']['Electricity breakeven price']['value'] + self.assertGreater(lcoe, 7.5) + self.assertLess(lcoe, 8.5) + + redrills = r.result['ENGINEERING PARAMETERS']['Number of times redrilling']['value'] + self.assertGreater(redrills, 2) + self.assertLess(redrills, 7) + + well_cost = r.result['CAPITAL COSTS (M$)']['Drilling and completion costs per well']['value'] + self.assertLess(well_cost, 4.0) + self.assertGreater(well_cost, 3.0) + + pumping_power_pct = r.result['SURFACE EQUIPMENT SIMULATION RESULTS'][ + 'Initial pumping power/net installed power' + ]['value'] + self.assertGreater(pumping_power_pct, 13) + self.assertLess(pumping_power_pct, 17) + + self.assertEqual( + r.result['SUMMARY OF RESULTS']['Number of production wells']['value'], + r.result['SUMMARY OF RESULTS']['Number of injection wells']['value'], + ) + + def test_case_study_documentation(self): + """ + Parses result values from case study documentation markdown and checks that they match the actual result. + Useful for catching when minor updates are made to the case study which need to be manually synced to the + documentation. + + Note: for future case studies, generate the documentation markdown from the input/result rather than writing + (entirely) by hand so that they are guaranteed to be in sync and don't need to be tested like this, + which has proved messy. + """ + + documentation_file_content = '\n'.join( + self._get_test_file_content('../../docs/Fervo_Project_Cape-4.md', encoding='utf-8') + ) + inputs_in_markdown = self.parse_markdown_inputs_structured(documentation_file_content) + results_in_markdown = self.parse_markdown_results_structured(documentation_file_content) + + self.assertEqual(3.96, results_in_markdown['Well Drilling and Completion Cost']['value']) + self.assertEqual('MUSD/well', results_in_markdown['Well Drilling and Completion Cost']['unit']) + + class _Q(HasQuantity): + def __init__(self, vu: dict[str, Any]): + self.value = vu['value'] + + # https://stackoverflow.com/questions/2280334/shortest-way-of-creating-an-object-with-arbitrary-attributes-in-python + self.CurrentUnits = type('', (), {})() + + self.CurrentUnits.value = vu['unit'] + + capex_q = _Q(results_in_markdown['Project capital costs: Total CAPEX']).quantity() + markdown_capex_USD_per_kW = ( + capex_q.to('USD').magnitude + / _Q(results_in_markdown['Maximum Total Electricity Generation']).quantity().to('kW').magnitude + ) + self.assertAlmostEqual( + sig_figs(markdown_capex_USD_per_kW, 3), results_in_markdown['Project capital costs: $/kW']['value'] + ) + + field_mapping = { + 'LCOE': 'Electricity breakeven price', + 'Project capital costs: Total CAPEX': 'Total CAPEX', + 'Well Drilling and Completion Cost': 'Drilling and completion costs per well', + } + + ignore_keys = ['Project capital costs: $/kW', 'Total fracture surface area per production well'] + + example_result = GeophiresXResult(self._get_test_file_path('../examples/Fervo_Project_Cape-4.out')) + example_result_values = {} + for key, _ in results_in_markdown.items(): + if key not in ignore_keys: + mapped_key = field_mapping.get(key) if key in field_mapping else key + entry = example_result._get_result_field(mapped_key) + if entry is not None and 'value' in entry: + entry['value'] = sig_figs(entry['value'], 3) + + example_result_values[key] = entry + + for ignore_key in ignore_keys: + if ignore_key in results_in_markdown: + del results_in_markdown[ignore_key] + + results_in_markdown['Well Drilling and Completion Cost']['unit'] = results_in_markdown[ + 'Well Drilling and Completion Cost' + ]['unit'].replace('/well', '') + self.assertDictAlmostEqual(example_result_values, results_in_markdown, places=3) + + result_capex_USD_per_kW = ( + _Q(example_result._get_result_field('Total CAPEX')).quantity().to('USD').magnitude + / _Q(example_result._get_result_field('Maximum Total Electricity Generation')).quantity().to('kW').magnitude + ) + self.assertAlmostEqual(sig_figs(result_capex_USD_per_kW, 3), sig_figs(markdown_capex_USD_per_kW, 3)) + + num_doublets = inputs_in_markdown['Number of Doublets']['value'] + self.assertEqual( + example_result.result['SUMMARY OF RESULTS']['Number of production wells']['value'], num_doublets + ) + + num_fracs_per_well = inputs_in_markdown['Number of Fractures per well']['value'] + expected_total_fracs = num_doublets * 2 * num_fracs_per_well + self.assertEqual( + expected_total_fracs, example_result.result['RESERVOIR PARAMETERS']['Number of fractures']['value'] + ) + + self.assertEqual( + example_result.result['RESERVOIR PARAMETERS']['Reservoir volume']['value'], + inputs_in_markdown['Reservoir Volume']['value'], + ) + + def parse_markdown_results_structured(self, markdown_text: str) -> dict: + """ + Parses result values from markdown into a structured dictionary with values and units. + """ + raw_results = {} + table_pattern = re.compile(r'^\s*\|\s*(?!-)([^|]+?)\s*\|\s*([^|]+?)\s*\|', re.MULTILINE) + + try: + results_start_index = markdown_text.index('## Results') + search_area = markdown_text[results_start_index:] + + matches = table_pattern.findall(search_area) + + # Use key_ and value_ to avoid shadowing + for match in matches: + key_ = match[0].strip() + value_ = match[1].strip() + if key_.lower() not in ('metric', 'parameter'): + raw_results[key_] = value_ + except ValueError: + print("Warning: '## Results' section not found.") + return {} + + # Consistency check + special_case_pattern = re.compile(r'LCOE\s*=\s*(\S+)\s*and\s*CAPEX\s*=\s*(\S+)') + special_case_match = special_case_pattern.search(markdown_text) + if special_case_match: + lcoe_text = special_case_match.group(1).rstrip('.,;') + lcoe_table_base = raw_results.get('LCOE', '').split('(')[0].strip() + if lcoe_text != lcoe_table_base: + raise ValueError( + f'LCOE mismatch: Text value ({lcoe_text}) does not match table value ({lcoe_table_base}).' + ) + + # Now, process the raw results into the structured format + structured_results = {} + # Use key_ and value_ to avoid shadowing + for key_, value_ in raw_results.items(): + if key_ in [ + 'After-tax IRR', + 'Average Production Temperature', + 'LCOE', + 'Maximum Total Electricity Generation', + 'Minimum Net Electricity Generation', + 'Number of times redrilling', + 'Project capital costs: Total CAPEX', + 'Project capital costs: $/kW', + 'WACC', + 'Well Drilling and Completion Cost', + ]: + structured_results[key_] = self._parse_value_unit(value_) + + return structured_results + + def parse_markdown_inputs_structured(self, markdown_text: str) -> dict: + """ + Parses all input values from all tables under the '## Inputs' section + of a markdown file into a structured dictionary. + """ + try: + # Isolate the content from "## Inputs" to the next "## " header + sections = re.split(r'(^##\s.*)', markdown_text, flags=re.MULTILINE) + inputs_header_index = next(i for i, s in enumerate(sections) if s.startswith('## Inputs')) + inputs_content = sections[inputs_header_index + 1] + except (StopIteration, IndexError): + print("Warning: '## Inputs' section not found or is empty.") + return {} + + raw_inputs = {} + table_pattern = re.compile(r'^\s*\|\s*(?!-)([^|]+?)\s*\|\s*([^|]+?)\s*\|', re.MULTILINE) + matches = table_pattern.findall(inputs_content) + + for match in matches: + key_ = match[0].strip() + value_ = match[1].strip() + if key_.lower() not in ('parameter', 'metric'): + raw_inputs[key_] = value_ + + structured_inputs = {} + for key_, value_ in raw_inputs.items(): + structured_inputs[key_] = self._parse_value_unit(value_) + + return structured_inputs + + # noinspection PyMethodMayBeStatic + def _parse_value_unit(self, raw_string: str) -> dict: + """ + A helper function to parse a string and extract a numerical value and its unit. + It handles various formats like currency, percentages, text, and scientific notation. + """ + clean_str = re.split(r'\s*\(|,(?!\s*\d)', raw_string)[0].strip() + + # Case 1: LCOE format ($X.X/MWh -> cents/kWh) + match = re.match(r'^\$(\d+\.?\d*)/MWh$', clean_str) + if match: + value = float(match.group(1)) + return {'value': round(value / 10, 2), 'unit': 'cents/kWh'} + + # Case 2: Billion dollar format ($X.XB -> MUSD) + match = re.match(r'^\$(\d+\.?\d*)B$', clean_str) + if match: + value = float(match.group(1)) + return {'value': value * 1000, 'unit': 'MUSD'} + + # Case 3: Million dollar format ($X.XM or $X.XM/unit) + match = re.match(r'^\$(\d+\.?\d*)M(\/.*)?$', clean_str) + if match: + value = float(match.group(1)) + unit_suffix = match.group(2) + unit = 'MUSD' + if unit_suffix: + unit = f'MUSD{unit_suffix}' + return {'value': value, 'unit': unit} + + # Case 4: Dollar per kW format ($X/kW -> USD/kW) + match = re.match(r'^\$(\d+\.?\d*)/kW$', clean_str) + if match: + value = float(match.group(1)) + return {'value': value, 'unit': 'USD/kW'} + + # Case 5: Percentage format (X.X%) + match = re.search(r'(\d+\.?\d*)%$', clean_str) + if match: + value = float(match.group(1)) + return {'value': value, 'unit': '%'} + + # Case 6: Temperature format (X℃ -> degC) + match = re.search(r'(\d+\.?\d*)\s*℃$', clean_str) + if match: + value = float(match.group(1)) + return {'value': value, 'unit': 'degC'} + + # Case 7: Scientific notation format (X.X*10⁶ Y) + match = re.match(r'^(\d+\.?\d*)\s*[×xX]\s*10[⁶6]\s*(.*)$', clean_str) + if match: + base_value = float(match.group(1)) + unit = match.group(2).strip() + return {'value': base_value * 1e6, 'unit': unit} + + # Case 8: Generic number and unit parser + if clean_str.startswith('9⅝'): + parts = clean_str.split(' ') + value = 9.0 + 5.0 / 8.0 + unit = parts[1] if len(parts) > 1 else 'unknown' + return {'value': value, 'unit': unit} + + match = re.search(r'([\d\.,]+)\s*(.*)', clean_str) + if match: + value_str = match.group(1).replace(',', '').replace(' ', '') + unit = match.group(2).strip() + + if '.' in value_str: + value = float(value_str) + else: + value = int(value_str) + + return {'value': value, 'unit': unit if unit else 'count'} + + # Fallback for text-only values + return {'value': clean_str, 'unit': 'text'} diff --git a/tests/test_geophires_x.py b/tests/test_geophires_x.py index 2d47bffd..5ae240b1 100644 --- a/tests/test_geophires_x.py +++ b/tests/test_geophires_x.py @@ -59,14 +59,12 @@ def test_geophires_x_end_use_direct_use_heat(self): ) del result.result['metadata'] - del result_same_input.result['metadata'] - self.assertDictEqual(result.result, result_same_input.result) + if 'metadata' in result_same_input.result: + del result_same_input.result['metadata'] - # See TODO in geophires_x_client.geophires_input_parameters.GeophiresInputParameters.__hash__ - if/when hashes - # of equivalent sets of parameters are made equal, the commented assertion below will test that caching is - # working as expected. - # assert result == result_same_input + self.assertDictEqual(result.result, result_same_input.result) + # noinspection PyMethodMayBeStatic def test_geophires_x_end_use_electricity(self): client = GeophiresXClient() result = client.get_geophires_result( @@ -941,42 +939,3 @@ def test_sbt_coaxial_raises_error(self): ) client.get_geophires_result(params) self.assertIn('SBT with coaxial configuration is not implemented', str(e.exception)) - - def test_fervo_project_cape_4_results_against_reference_values(self): - """ - Asserts that results conform to some of the key reference values claimed in docs/Fervo_Project_Cape-4.md. - """ - - r = GeophiresXClient().get_geophires_result( - GeophiresInputParameters(from_file_path=self._get_test_file_path('examples/Fervo_Project_Cape-4.txt')) - ) - - min_net_gen = r.result['SURFACE EQUIPMENT SIMULATION RESULTS']['Minimum Net Electricity Generation']['value'] - self.assertGreater(min_net_gen, 500) - self.assertLess(min_net_gen, 505) - - max_total_gen = r.result['SURFACE EQUIPMENT SIMULATION RESULTS']['Maximum Total Electricity Generation'][ - 'value' - ] - self.assertGreater(max_total_gen, 600) - self.assertLess(max_total_gen, 650) - - lcoe = r.result['SUMMARY OF RESULTS']['Electricity breakeven price']['value'] - self.assertGreater(lcoe, 7.5) - self.assertLess(lcoe, 8.5) - - redrills = r.result['ENGINEERING PARAMETERS']['Number of times redrilling']['value'] - self.assertGreater(redrills, 2) - self.assertLess(redrills, 7) - - well_cost = r.result['CAPITAL COSTS (M$)']['Drilling and completion costs per vertical production well'][ - 'value' - ] - self.assertLess(well_cost, 4.0) - self.assertGreater(well_cost, 3.0) - - pumping_power_pct = r.result['SURFACE EQUIPMENT SIMULATION RESULTS'][ - 'Initial pumping power/net installed power' - ]['value'] - self.assertGreater(pumping_power_pct, 13) - self.assertLess(pumping_power_pct, 17)