From 76a9b018140f83a7550234e4a65de09d561fb33c Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Sun, 31 May 2026 11:01:01 -0700 Subject: [PATCH 1/3] Support redrilling based on well integrity parameterization (Well Integrity Maximum Lifetime) --- src/geophires_x/WellBores.py | 114 ++++++-- .../geophires-request.json | 11 +- tests/base_test_case.py | 37 +++ tests/examples/example13b_well-integrity.out | 252 ++++++++++++++++++ tests/examples/example13b_well-integrity.txt | 73 +++++ tests/geophires_x_tests/test_well_bores.py | 105 ++++++++ tests/test_geophires_x.py | 33 +-- 7 files changed, 577 insertions(+), 48 deletions(-) create mode 100644 tests/examples/example13b_well-integrity.out create mode 100644 tests/examples/example13b_well-integrity.txt diff --git a/src/geophires_x/WellBores.py b/src/geophires_x/WellBores.py index b7a9b4bdf..91155e9bd 100644 --- a/src/geophires_x/WellBores.py +++ b/src/geophires_x/WellBores.py @@ -950,6 +950,9 @@ def __init__(self, model: Model): ToolTipText="Productivity index defined as ratio of production well flow rate over production well inflow " "pressure drop (see docs)" ) + + well_integrity_max_lifetime_param_name = "Well Integrity Maximum Lifetime" + # noinspection SpellCheckingInspection self.maxdrawdown = self.ParameterDict[self.maxdrawdown.Name] = floatParameter( "Maximum Drawdown", DefaultValue=1.0, @@ -959,11 +962,33 @@ def __init__(self, model: Model): PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH, ErrMessage="assume default maximum drawdown (1)", - ToolTipText="Maximum allowable thermal drawdown before redrilling of all wells into new reservoir " - "(most applicable to EGS-type reservoirs with heat farming strategies). E.g. a value of 0.2 " - "means that all wells are redrilled after the production temperature (at the wellhead) has " - "dropped by 20% of its initial temperature" + ToolTipText=f"Maximum allowable thermal drawdown before redrilling of all wells into new reservoir " + f"(most applicable to EGS-type reservoirs with heat farming strategies). E.g. a value of 0.2 " + f"means that all wells are redrilled after the production temperature (at the wellhead) has " + f"dropped by 20% of its initial temperature. " + f"Note that redrilling is triggered by whichever occurs first: this thermal drawdown limit or " + f"the chronological limit defined by {well_integrity_max_lifetime_param_name}." ) + self.well_integrity_max_lifetime = self.ParameterDict[self.well_integrity_max_lifetime.Name] = floatParameter( + well_integrity_max_lifetime_param_name, + DefaultValue=-1.0, + Min=0.1, + Max=100.0, + UnitType=Units.TIME, + PreferredUnits=TimeUnit.YEAR, + CurrentUnits=TimeUnit.YEAR, + Required=False, + Provided=False, + ToolTipText=f"Maximum chronological lifetime of the wellbore infrastructure before mechanical/chemical " + f"failure forces a redrilling event, independent of thermal drawdown. Models a deterministic " + f"first-order approximation of well integrity failure (compressive yielding, high-temperature " + f"creep, low-cycle fatigue, cement degradation, sulfide stress cracking, etc.) that is " + f"particularly relevant for Superhot Rock systems where wellbore infrastructure " + f"may typically fail before thermal depletion. Redrilling is triggered at the minimum of the " + f"thermal drawdown index defined by {self.maxdrawdown.Name} and this chronological index. " + f"If not provided, defaults to project lifetime (no mechanical failure)." + ) + self.IsAGS = self.ParameterDict[self.IsAGS.Name] = boolParameter( "Is AGS", DefaultValue=False, @@ -1615,22 +1640,71 @@ def Calculate(self, model: Model) -> None: model.logger.info(f'complete {self.__class__.__name__}: {__name__}') def calculate_redrilling(self, model: Model) -> None: - # Redrilling applies to the built-in analytical reservoir models and user-provided profile. - if model.reserv.resoption.value in \ - [ReservoirModel.MULTIPLE_PARALLEL_FRACTURES, ReservoirModel.LINEAR_HEAT_SWEEP, - ReservoirModel.SINGLE_FRACTURE, ReservoirModel.ANNUAL_PERCENTAGE, ReservoirModel.USER_PROVIDED_PROFILE]: - index_first_max_drawdown = np.argmax( - self.ProducedTemperature.value < (1 - model.wellbores.maxdrawdown.value) * - self.ProducedTemperature.value[0]) - - if index_first_max_drawdown > 0: # redrilling necessary - self.redrill.value = int(np.floor(len(self.ProducedTemperature.value) / index_first_max_drawdown)) - ProducedTemperatureRepeated = np.tile(self.ProducedTemperature.value[0:index_first_max_drawdown], - self.redrill.value + 1) - self.ProducedTemperature.value = ProducedTemperatureRepeated[0:len(self.ProducedTemperature.value)] - TResOutputRepeated = np.tile(model.reserv.Tresoutput.value[0:index_first_max_drawdown], - self.redrill.value + 1) - model.reserv.Tresoutput.value = TResOutputRepeated[0:len(self.ProducedTemperature.value)] + """ + Redrilling applies to the built-in analytical reservoir models and user-provided profile. + """ + + if model.reserv.resoption.value not in [ + ReservoirModel.MULTIPLE_PARALLEL_FRACTURES, ReservoirModel.LINEAR_HEAT_SWEEP, + ReservoirModel.SINGLE_FRACTURE, ReservoirModel.ANNUAL_PERCENTAGE, + ReservoirModel.USER_PROVIDED_PROFILE, + ]: + return + + total_steps = len(self.ProducedTemperature.value) + project_lifetime_yr = model.surfaceplant.plant_lifetime.value + + # Thermal drawdown trigger + index_first_max_drawdown = int(np.argmax( + self.ProducedTemperature.value + < (1 - model.wellbores.maxdrawdown.value) * self.ProducedTemperature.value[0] + )) + + # Well integrity (chronological) trigger + if self.well_integrity_max_lifetime.Provided and self.well_integrity_max_lifetime.value > 0: + timesteps_per_year = total_steps / project_lifetime_yr + integrity_failure_index = int(self.well_integrity_max_lifetime.value * timesteps_per_year) + # Clamp: a lifetime >= project lifetime means "no mechanical failure" + if integrity_failure_index >= total_steps: + integrity_failure_index = 0 # treat as "no trigger" sentinel + else: + integrity_failure_index = 0 # no mechanical failure modeled + + # Competing-risk: take the earliest non-zero trigger + candidates = [i for i in (index_first_max_drawdown, integrity_failure_index) if i > 0] + if not candidates: + return # no redrilling + redrill_trigger_index = min(candidates) + + if redrill_trigger_index <= 0 or redrill_trigger_index >= total_steps: + return + + self.redrill.value = int(np.floor(total_steps / redrill_trigger_index)) + ProducedTemperatureRepeated = np.tile( + self.ProducedTemperature.value[0:redrill_trigger_index], + self.redrill.value + 1, + ) + self.ProducedTemperature.value = ProducedTemperatureRepeated[0:total_steps] + TResOutputRepeated = np.tile( + model.reserv.Tresoutput.value[0:redrill_trigger_index], + self.redrill.value + 1, + ) + model.reserv.Tresoutput.value = TResOutputRepeated[0:total_steps] + + # Log which mechanism dominated + if (integrity_failure_index > 0 + and (index_first_max_drawdown == 0 or integrity_failure_index < index_first_max_drawdown)): + model.logger.info( + f"Redrilling driven by {self.well_integrity_max_lifetime.Name} " + f"({self.well_integrity_max_lifetime.value} {self.well_integrity_max_lifetime.CurrentUnits}); " + f"redrill events = {self.redrill.value}." + ) + else: + model.logger.info( + f"Redrilling driven by thermal drawdown ({self.maxdrawdown.Name}; " + f"{self.maxdrawdown.quantity().to(convertible_unit('percent')).magnitude:.2f}%); " + f"redrill events = {self.redrill.value}." + ) def _sync_output_params_from_input_params(self) -> None: """ diff --git a/src/geophires_x_schema_generator/geophires-request.json b/src/geophires_x_schema_generator/geophires-request.json index 3f7a887b8..2c4284790 100644 --- a/src/geophires_x_schema_generator/geophires-request.json +++ b/src/geophires_x_schema_generator/geophires-request.json @@ -818,7 +818,7 @@ "maximum": 10000.0 }, "Maximum Drawdown": { - "description": "Maximum allowable thermal drawdown before redrilling of all wells into new reservoir (most applicable to EGS-type reservoirs with heat farming strategies). E.g. a value of 0.2 means that all wells are redrilled after the production temperature (at the wellhead) has dropped by 20% of its initial temperature", + "description": "Maximum allowable thermal drawdown before redrilling of all wells into new reservoir (most applicable to EGS-type reservoirs with heat farming strategies). E.g. a value of 0.2 means that all wells are redrilled after the production temperature (at the wellhead) has dropped by 20% of its initial temperature. Note that redrilling is triggered by whichever occurs first: this thermal drawdown limit or the chronological limit defined by Well Integrity Maximum Lifetime.", "type": "number", "units": "", "category": "Well Bores", @@ -826,6 +826,15 @@ "minimum": 0.0, "maximum": "1.0" }, + "Well Integrity Maximum Lifetime": { + "description": "Maximum chronological lifetime of the wellbore infrastructure before mechanical/chemical failure forces a redrilling event, independent of thermal drawdown. Models a deterministic first-order approximation of well integrity failure (compressive yielding, high-temperature creep, low-cycle fatigue, cement degradation, sulfide stress cracking, etc.) that is particularly relevant for Superhot Rock systems where wellbore infrastructure may typically fail before thermal depletion. Redrilling is triggered at the minimum of the thermal drawdown index defined by Maximum Drawdown and this chronological index. If not provided, defaults to project lifetime (no mechanical failure).", + "type": "number", + "units": "yr", + "category": "Well Bores", + "default": -1.0, + "minimum": 0.1, + "maximum": 100.0 + }, "Is AGS": { "description": "Set to true if the model is for an Advanced Geothermal System (AGS)", "type": "boolean", diff --git a/tests/base_test_case.py b/tests/base_test_case.py index d46335050..acf2e9b6b 100644 --- a/tests/base_test_case.py +++ b/tests/base_test_case.py @@ -1,6 +1,7 @@ from __future__ import annotations import inspect +import math import numbers import os.path import sys @@ -11,6 +12,7 @@ from geophires_x.GeoPHIRESUtils import sig_figs from geophires_x_client import GeophiresInputParameters +from geophires_x_client import GeophiresXResult # noinspection PyProtectedMember from geophires_x_client import _get_logger @@ -29,6 +31,41 @@ def _get_test_file_content(self, test_file_name, **open_kw_args) -> str: def _list_test_files_dir(self, test_files_dir: str): return os.listdir(self._get_test_file_path(test_files_dir)) # noqa: PTH208 + # noinspection PyMethodMayBeStatic + def _sanitize_nan(self, r: GeophiresXResult) -> GeophiresXResult: + """ + Workaround for float('nan') != float('nan') + See https://stackoverflow.com/questions/51728427/unittest-how-to-assert-if-the-two-possibly-nan-values-are-equal + + TODO generalize beyond After-tax IRR + + Mutates passed-in result object. + """ + + irr_key = 'After-tax IRR' + if irr_key in r.result['ECONOMIC PARAMETERS']: + try: + if math.isnan(r.result['ECONOMIC PARAMETERS'][irr_key]['value']): + r.result['ECONOMIC PARAMETERS'][irr_key]['value'] = 'NaN' + except TypeError: + pass + + return r + + # noinspection PyMethodMayBeStatic + def _strip_metadata(self, geophires_result: GeophiresXResult) -> GeophiresXResult: + """ + Useful for comparing results from different runs. + + Mutates passed-in result object. + """ + + for key in ['metadata', 'Simulation Metadata']: + if key in geophires_result.result: + del geophires_result.result[key] + + return geophires_result + def assertAlmostEqualWithinPercentage(self, expected, actual, msg: str | None = None, percent=5): if msg is not None and not isinstance(msg, str): raise ValueError(f'msg must be a string (you may have meant to pass percent={msg})') diff --git a/tests/examples/example13b_well-integrity.out b/tests/examples/example13b_well-integrity.out new file mode 100644 index 000000000..715fc06ad --- /dev/null +++ b/tests/examples/example13b_well-integrity.out @@ -0,0 +1,252 @@ + ***************** + ***CASE REPORT*** + ***************** + +Simulation Metadata +---------------------- + GEOPHIRES Version: 3.13.9 + Simulation Date: 2026-05-31 + Simulation Time: 10:01 + Calculation Time: 0.047 sec + + ***SUMMARY OF RESULTS*** + + End-Use Option: Cogeneration Bottoming Cycle, Electricity sales considered as extra income + Average Net Electricity Production: 3.15 MW + Average Direct-Use Heat Production: 15.63 MW + Electricity breakeven price: 16.12 cents/kWh + Direct-Use heat breakeven price (LCOH): 6.44 USD/MMBTU + Number of production wells: 2 + Number of injection wells: 2 + Flowrate per production well: 50.0 kg/sec + Well depth: 4.0 kilometer + Geothermal gradient: 50 degC/km + + + ***ECONOMIC PARAMETERS*** + + Economic Model = Standard Levelized Cost + Interest Rate: 5.00 % + Accrued financing during construction: 0.00 % + Project lifetime: 30 yr + Capacity factor: 80.0 % + Project NPV: -31.42 MUSD + Project IRR: -2.67 % + Project VIR=PI=PIR: 0.37 + Project MOIC: -0.12 + Project Payback Period: N/A + CHP: Percent cost allocation for electrical plant: 60.28 % + + ***ENGINEERING PARAMETERS*** + + Number of Production Wells: 2 + Number of Injection Wells: 2 + Well depth: 4.0 kilometer + Water loss rate: 0.0 % + Pump efficiency: 80.0 % + Injection temperature: 50.0 degC + User-provided production well temperature drop + Constant production well temperature drop: 3.0 degC + Flowrate per production well: 50.0 kg/sec + Injection well casing ID: 6.000 in + Production well casing ID: 6.000 in + Number of times redrilling: 1 + 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: 1.000 1/year + Bottom-hole temperature: 212.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 provided as input + Reservoir volume: 125000000 m**3 + Reservoir hydrostatic pressure: 38004.61 kPa + Plant outlet pressure: 2260.87 kPa + Production wellhead pressure: 2329.82 kPa + Productivity Index: 10.00 kg/sec/bar + Injectivity Index: 10.00 kg/sec/bar + Reservoir density: 2700.00 kg/m**3 + Reservoir heat capacity: 1000.00 J/kg/K + + + ***RESERVOIR SIMULATION RESULTS*** + + Maximum Production Temperature: 209.0 degC + Average Production Temperature: 197.0 degC + Minimum Production Temperature: 184.4 degC + Initial Production Temperature: 209.0 degC + Average Reservoir Heat Extraction: 61.13 MW + Wellbore Heat Transmission Model = Constant Temperature Drop: 3.0 degC + Average Injection Well Pump Pressure Drop: -1371.4 kPa + Average Production Well Pump Pressure Drop: 1776.4 kPa + + + ***CAPITAL COSTS (M$)*** + + Exploration costs: 0.00 MUSD + Drilling and completion costs: 26.49 MUSD + Drilling and completion costs per well: 6.62 MUSD + Stimulation costs: 1.00 MUSD + Surface power plant costs: 19.89 MUSD + of which Electrical Plant Cost: 11.99 MUSD + of which Heat Plant Cost: 7.90 MUSD + Field gathering system costs: 2.32 MUSD + Total surface equipment costs: 22.21 MUSD + Total capital costs: 49.70 MUSD + + + ***OPERATING AND MAINTENANCE COSTS (M$/yr)*** + + Wellfield maintenance costs: 0.62 MUSD/yr + Power plant maintenance costs: 1.30 MUSD/yr + Water costs: 0.00 MUSD/yr + Redrilling costs: 0.92 MUSD/yr + Total operating and maintenance costs: 2.84 MUSD/yr + + + ***SURFACE EQUIPMENT SIMULATION RESULTS*** + Initial geofluid availability: 0.11 MW/(kg/s) + Maximum Total Electricity Generation: 3.40 MW + Average Total Electricity Generation: 3.40 MW + Minimum Total Electricity Generation: 3.40 MW + Initial Total Electricity Generation: 3.40 MW + Maximum Net Electricity Generation: 3.22 MW + Average Net Electricity Generation: 3.15 MW + Minimum Net Electricity Generation: 3.09 MW + Initial Net Electricity Generation: 3.22 MW + Average Annual Total Electricity Generation: 23.84 GWh + Average Annual Net Electricity Generation: 22.09 GWh + Initial pumping power/net installed power: 5.80 % + Maximum Net Heat Production: 19.63 MW + Average Net Heat Production: 15.63 MW + Minimum Net Heat Production: 11.45 MW + Initial Net Heat Production: 19.63 MW + Average Annual Heat Production: 109.25 GWh + Average Pumping Power: 0.25 MW + Heat to Power Conversion Efficiency: 7.58 % + + ************************************************************ + * HEATING, COOLING AND/OR ELECTRICITY PRODUCTION PROFILE * + ************************************************************ + YEAR THERMAL GEOFLUID PUMP NET NET FIRST LAW + DRAWDOWN TEMPERATURE POWER POWER HEAT EFFICIENCY + (deg C) (MW) (MW) (MW) (%) + 0 1.0000 209.00 0.1866 3.2152 19.6291 7.7312 + 1 0.9922 207.36 0.1954 3.2063 19.0841 7.7099 + 2 0.9843 205.72 0.2042 3.1975 18.5391 7.6888 + 3 0.9765 204.09 0.2129 3.1888 17.9941 7.6679 + 4 0.9686 202.45 0.2215 3.1802 17.4490 7.6472 + 5 0.9608 200.81 0.2300 3.1717 16.9040 7.6267 + 6 0.9530 199.17 0.2385 3.1633 16.3590 7.6064 + 7 0.9451 197.53 0.2468 3.1549 15.8140 7.5863 + 8 0.9373 195.89 0.2551 3.1466 15.2689 7.5663 + 9 0.9295 194.26 0.2633 3.1384 14.7239 7.5466 + 10 0.9216 192.62 0.2714 3.1303 14.1789 7.5271 + 11 0.9138 190.98 0.2795 3.1223 13.6339 7.5077 + 12 0.9059 189.34 0.2875 3.1143 13.0888 7.4886 + 13 0.8981 187.70 0.2954 3.1064 12.5438 7.4696 + 14 0.8903 186.07 0.3032 3.0986 11.9988 7.4508 + 15 0.8824 184.43 0.3109 3.0908 11.4538 7.4321 + 16 0.9948 207.91 0.1925 3.2093 19.2658 7.7169 + 17 0.9869 206.27 0.2013 3.2005 18.7208 7.6958 + 18 0.9791 204.63 0.2100 3.1917 18.1757 7.6748 + 19 0.9713 202.99 0.2186 3.1831 17.6307 7.6540 + 20 0.9634 201.36 0.2272 3.1745 17.0857 7.6335 + 21 0.9556 199.72 0.2357 3.1661 16.5407 7.6131 + 22 0.9477 198.08 0.2441 3.1577 15.9956 7.5929 + 23 0.9399 196.44 0.2524 3.1494 15.4506 7.5730 + 24 0.9321 194.80 0.2606 3.1412 14.9056 7.5532 + 25 0.9242 193.16 0.2687 3.1330 14.3606 7.5336 + 26 0.9164 191.53 0.2768 3.1249 13.8155 7.5142 + 27 0.9086 189.89 0.2848 3.1169 13.2705 7.4949 + 28 0.9007 188.25 0.2927 3.1090 12.7255 7.4759 + 29 0.8929 186.61 0.3006 3.1012 12.1805 7.4570 + + + ******************************************************************* + * ANNUAL HEATING, COOLING AND/OR ELECTRICITY PRODUCTION PROFILE * + ******************************************************************* + YEAR HEAT ELECTRICITY HEAT RESERVOIR PERCENTAGE OF + PROVIDED PROVIDED EXTRACTED HEAT CONTENT TOTAL HEAT MINED + (GWh/year) (GWh/year) (GWh/year) (10^15 J) (%) + 1 135.7 22.5 461.01 53.02 3.04 + 2 131.8 22.4 456.23 51.37 6.04 + 3 128.0 22.4 451.46 49.75 9.01 + 4 124.2 22.3 446.68 48.14 11.95 + 5 120.4 22.3 441.91 46.55 14.86 + 6 116.6 22.2 437.13 44.98 17.74 + 7 112.7 22.1 432.36 43.42 20.59 + 8 108.9 22.1 427.59 41.88 23.40 + 9 105.1 22.0 422.81 40.36 26.19 + 10 101.3 22.0 418.04 38.85 28.94 + 11 97.5 21.9 413.26 37.36 31.66 + 12 93.6 21.9 408.49 35.89 34.35 + 13 89.8 21.8 403.71 34.44 37.01 + 14 86.0 21.7 398.94 33.00 39.64 + 15 82.2 21.7 394.17 31.59 42.23 + 16 127.2 22.4 450.40 29.96 45.20 + 17 133.1 22.5 457.82 28.32 48.21 + 18 129.3 22.4 453.05 26.68 51.19 + 19 125.5 22.3 448.28 25.07 54.15 + 20 121.6 22.3 443.50 23.47 57.07 + 21 117.8 22.2 438.73 21.89 59.95 + 22 114.0 22.2 433.95 20.33 62.81 + 23 110.2 22.1 429.18 18.79 65.64 + 24 106.4 22.0 424.40 17.26 68.43 + 25 102.5 22.0 419.63 15.75 71.19 + 26 98.7 21.9 414.85 14.26 73.93 + 27 94.9 21.9 410.08 12.78 76.63 + 28 91.1 21.8 405.31 11.32 79.30 + 29 87.3 21.8 400.53 9.88 81.93 + 30 84.1 21.7 396.55 8.45 84.54 + + + ******************************** + * 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 -49.70 -49.70 + 1 5.50 1.24 1.24 | 2.50 3.39 3.39 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 1.79 -47.91 + 2 5.50 1.23 2.47 | 2.50 3.30 6.69 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 1.69 -46.22 + 3 5.50 1.23 3.70 | 2.50 3.20 9.89 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 1.59 -44.63 + 4 5.50 1.23 4.93 | 2.50 3.10 12.99 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 1.49 -43.14 + 5 5.50 1.22 6.15 | 2.50 3.01 16.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 1.39 -41.75 + 6 5.50 1.22 7.37 | 2.50 2.91 18.92 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 1.29 -40.45 + 7 5.50 1.22 8.59 | 2.50 2.82 21.73 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 1.19 -39.26 + 8 5.50 1.21 9.81 | 2.50 2.72 24.46 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 1.10 -38.16 + 9 5.50 1.21 11.02 | 2.50 2.63 27.08 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 1.00 -37.16 + 10 5.50 1.21 12.23 | 2.50 2.53 29.62 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 0.90 -36.26 + 11 5.50 1.20 13.43 | 2.50 2.44 32.05 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 0.80 -35.46 + 12 5.50 1.20 14.63 | 2.50 2.34 34.39 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 0.70 -34.76 + 13 5.50 1.20 15.83 | 2.50 2.25 36.64 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 0.60 -34.16 + 14 5.50 1.20 17.03 | 2.50 2.15 38.79 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 0.50 -33.66 + 15 5.50 1.19 18.22 | 2.50 2.05 40.84 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 0.41 -33.25 + 16 5.50 1.23 19.45 | 2.50 3.18 44.02 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 1.57 -31.68 + 17 5.50 1.24 20.69 | 2.50 3.33 47.35 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 1.72 -29.96 + 18 5.50 1.23 21.92 | 2.50 3.23 50.58 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 1.62 -28.34 + 19 5.50 1.23 23.15 | 2.50 3.14 53.72 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 1.52 -26.81 + 20 5.50 1.23 24.37 | 2.50 3.04 56.76 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 1.43 -25.39 + 21 5.50 1.22 25.59 | 2.50 2.95 59.71 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 1.33 -24.06 + 22 5.50 1.22 26.81 | 2.50 2.85 62.56 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 1.23 -22.83 + 23 5.50 1.22 28.03 | 2.50 2.75 65.31 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 1.13 -21.70 + 24 5.50 1.21 29.24 | 2.50 2.66 67.97 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 1.03 -20.67 + 25 5.50 1.21 30.45 | 2.50 2.56 70.53 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 0.93 -19.74 + 26 5.50 1.21 31.66 | 2.50 2.47 73.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 0.83 -18.91 + 27 5.50 1.20 32.86 | 2.50 2.37 75.37 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 0.73 -18.17 + 28 5.50 1.20 34.06 | 2.50 2.28 77.65 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 0.64 -17.54 + 29 5.50 1.20 35.26 | 2.50 2.18 79.83 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 0.54 -17.00 + 30 5.50 1.19 36.45 | 2.50 2.10 81.94 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 2.84 0.46 -16.55 diff --git a/tests/examples/example13b_well-integrity.txt b/tests/examples/example13b_well-integrity.txt new file mode 100644 index 000000000..491847d41 --- /dev/null +++ b/tests/examples/example13b_well-integrity.txt @@ -0,0 +1,73 @@ +# Example 13b: This example file demonstrates redrilling triggered by Well Integrity Maximum Lifetime + +# *** Subsurface technical parameters *** +# **************************************** +Reservoir Model,4, --- Percentage thermal drawdown model +Drawdown Parameter,0.01, --- In [1/year]. +Reservoir Depth,4, --- [km] +Number of Segments,1, --- [-] +Gradient 1,50, --- [deg.C/km] +Number of Production Wells,2, --- [-] +Number of Injection Wells,2, --- [-] +Production Well Diameter,6, --- [inch] +Injection Well Diameter,6, --- [inch] +Ramey Production Wellbore Model,0, --- Should be 0 (disable) or 1 (enable) +Production Wellbore Temperature Drop,3, --- [deg.C] +Injection Wellbore Temperature Gain,0, --- [deg.C] +Production Flow Rate per Well,50, --- [kg/s] +Maximum Temperature,400, --- [deg.C] +Reservoir Volume Option,4, --- Should be 1, 2, 3, or 4. See manual for details. +Reservoir Volume,1.25e8, --- [m3] (required for reservoir volume option 3 and 4 +Water Loss Fraction,0.0, --- [-] (total geofluid lost)/(total geofluid produced) +Injectivity Index,10, --- [kg/s/bar] +Productivity Index,10, --- [kg/s/bar] +Injection Temperature,50, --- [deg.C] + +Maximum Drawdown, 1 +Well Integrity Maximum Lifetime, 15.5 + +Reservoir Heat Capacity,1000, --- [J/kg/K] +Reservoir Density,2700, --- [kg/m3] +Reservoir Thermal Conductivity,3, --- [W/m/K] + + +# *** Surface technical parameters *** +# ************************************ +End-Use Option,42, --- [-] Co-gen bottoming cycle with heat the main product +Circulation Pump Efficiency,0.8, --- [-] +Utilization Factor,0.8, --- [-] +Surface Temperature,12, --- [deg.C] +Ambient Temperature,12, --- [deg.C] +End-Use Efficiency Factor,0.8, --- [-] + +District Heating Demand Option,1, --- Should be 1 or 2. See manual or below for option details +District Heating Demand File Name,Examples/cornell_heat_demand.csv, --- hourly MW thermal demand in a CSV file +District Heating Demand Data Time Resolution,1, --- 1 for hourly, 2 for daily +District Heating Demand Data Column Number,2, --- + +Peaking Fuel Cost Rate,0.0273, --- [$/kWh] Cost of natural gas for peak boiler use +Peaking Boiler Efficiency,0.85, ` --- Should be between 0 and 1, defaults to 0.85 +District Heating Piping Cost Rate,1200, --- [$/m] used for calculating surface piping capital cost for district heat +District Heating Road Length,3, ---[km] supersedes model option 2 if any value is entered + + +# *** Economic/Financial Parameters *** +# ************************************* +Plant Lifetime,30, --- [years] +Economic Model,2, --- Should be 1 (FCR model), 2 (Standard LCOE/LCOH model), or 3 (Bicycle model). +Discount Rate,0.05, +Inflation Rate During Construction,0, --- [-] +Well Drilling and Completion Capital Cost Adjustment Factor,1, --- [-] Use built-in well cost correlation as is +Well Drilling Cost Correlation,1, --- [-] Use built-in well drilling cost correlation #1 +Reservoir Stimulation Capital Cost,1, --- [M$/injection well] Reservoir stimulation capital cost per injection well +Surface Plant Capital Cost Adjustment Factor,1, --- [-] Use built-in surface plant cost correlation as is +Field Gathering System Capital Cost Adjustment Factor,1, --- [-] Use built-in pipeline distribution cost correlation as is +Exploration Capital Cost Adjustment Factor,0, --- [-] Use built-in exploration cost correlation as is +Wellfield O&M Cost Adjustment Factor,1, --- [-] Use built-in wellfield O&M cost correlation as is +Water Cost Adjustment Factor,1, --- [-] Use built-in water cost correlation as is +Surface Plant O&M Cost Adjustment Factor,1, --- [-] Use built-in surface plant O&M cost correlation as is + + +# *** Simulation Parameters *** +Print Output to Console,1, --- [-] Should be 0 (don't print results to console) or 1 (print results to console) +Time steps per year,3, --- [1/year] diff --git a/tests/geophires_x_tests/test_well_bores.py b/tests/geophires_x_tests/test_well_bores.py index 30b2ebace..5de1f3c94 100644 --- a/tests/geophires_x_tests/test_well_bores.py +++ b/tests/geophires_x_tests/test_well_bores.py @@ -126,6 +126,111 @@ def test_number_of_injection_wells_per_production_well(self): } ) + def test_redrilling_thermal_drawdown_dominates(self): + """ + Verify that if thermal drawdown triggers before well integrity failure, + the redrilling count reflects the thermal limit. + """ + max_drawdown = 0.02 + result = self._get_result( + { + 'Reservoir Model': 4, + 'Drawdown Parameter': 0.01, + 'Plant Lifetime': 30, + 'Maximum Drawdown': max_drawdown, + 'Well Integrity Maximum Lifetime': 50.0, + } + ) + + redrill_events = int(result.result['ENGINEERING PARAMETERS']['Number of times redrilling']['value']) + self.assertGreater(redrill_events, 2) + + profile = result.power_generation_profile + header = profile[0] + drawdown_idx = next(i for i, h in enumerate(header) if 'THERMAL DRAWDOWN' in str(h).upper()) + drawdowns = [float(row[drawdown_idx]) for row in profile[1:]] + + # Verify that drawdown hits the minimum defined by max drawdown param the same number of times as redrilling. + # Each reset (value jumps back up to ~1.0) indicates a redrill event triggered by hitting the threshold. + # We use a difference > 0.005 to filter out minor natural year-over-year increases (~0.001) that can occur + # early in analytical thermal profiles. + resets = sum(1 for i in range(len(drawdowns) - 1) if drawdowns[i + 1] - drawdowns[i] > 0.005) + self.assertEqual(redrill_events, resets) + + def test_redrilling_well_integrity_dominates(self): + """ + Verify that if well integrity failure triggers before thermal drawdown, + the redrilling count reflects the chronological integrity limit. + """ + max_drawdown = 0.90 + result = self._get_result( + { + 'Reservoir Model': 4, + 'Drawdown Parameter': 0.01, + 'Plant Lifetime': 30, + 'Maximum Drawdown': max_drawdown, + 'Well Integrity Maximum Lifetime': 7.0, + } + ) + + redrill_events = int(result.result['ENGINEERING PARAMETERS']['Number of times redrilling']['value']) + self.assertEqual(redrill_events, 4) + + profile = result.power_generation_profile + header = profile[0] + drawdown_idx = next(i for i, h in enumerate(header) if 'THERMAL DRAWDOWN' in str(h).upper()) + drawdowns = [float(row[drawdown_idx]) for row in profile[1:]] + + # Verify that drawdown stays above the maximum drawdown value throughout project lifetime + threshold = 1.0 - max_drawdown + min_drawdown = min(drawdowns) + self.assertGreater(min_drawdown, threshold) + + # Verify the chronological resets occurred the correct number of times. + # We use a difference > 0.005 to filter out minor natural year-over-year increases. + resets = sum(1 for i in range(len(drawdowns) - 1) if drawdowns[i + 1] - drawdowns[i] > 0.005) + self.assertEqual(redrill_events, resets) + + def test_redrilling_no_triggers(self): + """ + Verify that if neither threshold is reached before project end, + redrilling is 0 (or not triggered). + """ + result = self._get_result( + { + 'Reservoir Model': 4, + 'Drawdown Parameter': 0.01, + 'Plant Lifetime': 30, + 'Maximum Drawdown': 0.90, + 'Well Integrity Maximum Lifetime': 40.0, + } + ) + + summary = result.result.get('ENGINEERING PARAMETERS', {}) + if 'Number of times redrilling' in summary: + redrill_events = int(summary['Number of times redrilling']['value']) + self.assertEqual(0, redrill_events) + + def test_redrilling_examples_equivalence(self) -> None: + """ + example13 and example13b_well-integrity are expected to have identical results + with the only difference being whether redrilling is triggered by drawdown or well integrity. + """ + + def _san(r: GeophiresXResult) -> tuple[int, int, float, float]: + return self._sanitize_nan(self._strip_metadata(r)) + + drawdown_example: GeophiresXResult = _san( + GeophiresXResult(self._get_test_file_path('../examples/example13.out')) + ) + well_integrity_example: GeophiresXResult = _san( + GeophiresXResult(self._get_test_file_path('../examples/example13b_well-integrity.out')) + ) + self.assertDictEqual( + drawdown_example.result, + well_integrity_example.result, + ) + # noinspection PyMethodMayBeStatic def _get_result(self, _params) -> GeophiresXResult: params = GeophiresInputParameters( diff --git a/tests/test_geophires_x.py b/tests/test_geophires_x.py index 6928bba49..f90b5968c 100644 --- a/tests/test_geophires_x.py +++ b/tests/test_geophires_x.py @@ -1,6 +1,5 @@ from __future__ import annotations -import math import os import tempfile import uuid @@ -214,16 +213,12 @@ def get_output_file_for_example(example_file: str): input_params = GeophiresInputParameters( from_file_path=self._get_test_file_path(Path('examples', example_file_path)) ) - geophires_result: GeophiresXResult = client.get_geophires_result(input_params) - del geophires_result.result['metadata'] - del geophires_result.result['Simulation Metadata'] - - expected_result: GeophiresXResult = GeophiresXResult(get_output_file_for_example(example_file_path)) - del expected_result.result['metadata'] - del expected_result.result['Simulation Metadata'] - - self._sanitize_nan(geophires_result) - self._sanitize_nan(expected_result) + geophires_result: GeophiresXResult = self._sanitize_nan( + self._strip_metadata(client.get_geophires_result(input_params)) + ) + expected_result: GeophiresXResult = self._sanitize_nan( + self._strip_metadata(GeophiresXResult(get_output_file_for_example(example_file_path))) + ) try: self.assertDictEqual( @@ -276,22 +271,6 @@ def get_output_file_for_example(example_file: str): if len(regenerate_cmds) > 0: print(f'Command to regenerate {len(regenerate_cmds)} failed examples:\n{" && ".join(regenerate_cmds)}') - # noinspection PyMethodMayBeStatic - def _sanitize_nan(self, r: GeophiresXResult) -> None: - """ - Workaround for float('nan') != float('nan') - See https://stackoverflow.com/questions/51728427/unittest-how-to-assert-if-the-two-possibly-nan-values-are-equal - - TODO generalize beyond After-tax IRR - """ - irr_key = 'After-tax IRR' - if irr_key in r.result['ECONOMIC PARAMETERS']: - try: - if math.isnan(r.result['ECONOMIC PARAMETERS'][irr_key]['value']): - r.result['ECONOMIC PARAMETERS'][irr_key]['value'] = 'NaN' - except TypeError: - pass - def _get_unequal_dicts_approximate_percent_difference(self, d1: dict, d2: dict) -> float | None: for i in range(99): try: From 6a21943bb58d6a4e073de63e17184efa09a9ae55 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Sun, 31 May 2026 11:06:41 -0700 Subject: [PATCH 2/3] =?UTF-8?q?Bump=20version:=203.13.9=20=E2=86=92=203.13?= =?UTF-8?q?.10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- .cookiecutterrc | 2 +- README.rst | 4 ++-- docs/conf.py | 2 +- setup.py | 2 +- src/geophires_x/__init__.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index bb40ceb7e..6afd1847f 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.13.9 +current_version = 3.13.10 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index bd9ff8e45..da07fd0c0 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.13.9 + version: 3.13.10 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" diff --git a/README.rst b/README.rst index d613d77a2..06e528b77 100644 --- a/README.rst +++ b/README.rst @@ -58,9 +58,9 @@ Free software: `MIT license `__ :alt: Supported implementations :target: https://pypi.org/project/geophires-x -.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.13.9.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.13.10.svg :alt: Commits since latest release - :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.13.9...main + :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.13.10...main .. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat :target: https://softwareengineerprogrammer.github.io/GEOPHIRES diff --git a/docs/conf.py b/docs/conf.py index d6e2f6833..fa1f3ed83 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ year = '2025' author = 'NREL' copyright = f'{year}, {author}' -version = release = '3.13.9' +version = release = '3.13.10' pygments_style = 'trac' templates_path = ['./templates'] diff --git a/setup.py b/setup.py index dd6d50a40..34b5ad8eb 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='geophires-x', - version='3.13.9', + version='3.13.10', license='MIT', description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.', long_description='{}\n{}'.format( diff --git a/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index 55b4bfdbb..4b27ac35f 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.13.9' +__version__ = '3.13.10' From 964752f9f0e048add07a97b7de81c8d4e8f8919f Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Sun, 31 May 2026 11:29:17 -0700 Subject: [PATCH 3/3] v3.13.10 CHANGELOG entry + example13b_well-integrity README example list entry --- CHANGELOG.rst | 2 ++ README.rst | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4dfdd4e8d..ca8405975 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,8 @@ GEOPHIRES v3 (2023-2026) 3.13 ^^^^ +3.13.10: `Well integrity parameterization to trigger redrilling `__ | `release `__ + 3.13.9: `Add hip-ra-x-result.json schema and HipRaXResult `__ | `release `__ 3.13.8: `Fix drilling cost types output unit conversion issue; Project Location (Latitude & Longitude); SHR Example 3 update `__ | `release `__ diff --git a/README.rst b/README.rst index 06e528b77..ace01e371 100644 --- a/README.rst +++ b/README.rst @@ -236,6 +236,10 @@ Example-specific web interface deeplinks are listed in the Link column. - `example13.txt `__ - `.out `__ - `link `__ + * - Example 13b: Redrilling due to Well Integrity + - `example13.txt `__ + - `.out `__ + - `link `__ * - Example 14: Data Center - `example14_data-center.txt `__ - `.out `__