Skip to content

Commit 21742a2

Browse files
Merge pull request #163 from softwareengineerprogrammer/well-integrity-parameterization-2
Well integrity parameterization to trigger redrilling [v3.13.10]
2 parents 1f160d5 + 964752f commit 21742a2

14 files changed

Lines changed: 590 additions & 55 deletions

File tree

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 3.13.9
2+
current_version = 3.13.10
33
commit = True
44
tag = True
55

.cookiecutterrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ default_context:
5454
sphinx_doctest: "no"
5555
sphinx_theme: "sphinx-py3doc-enhanced-theme"
5656
test_matrix_separate_coverage: "no"
57-
version: 3.13.9
57+
version: 3.13.10
5858
version_manager: "bump2version"
5959
website: "https://github.com/NREL"
6060
year_from: "2023"

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ GEOPHIRES v3 (2023-2026)
88
3.13
99
^^^^
1010

11+
3.13.10: `Well integrity parameterization to trigger redrilling <https://github.com/softwareengineerprogrammer/GEOPHIRES/pull/163>`__ | `release <https://github.com/NREL/GEOPHIRES-X/releases/tag/v3.13.10>`__
12+
1113
3.13.9: `Add hip-ra-x-result.json schema and HipRaXResult <https://github.com/NatLabRockies/GEOPHIRES-X/pull/499>`__ | `release <https://github.com/NREL/GEOPHIRES-X/releases/tag/v3.13.9>`__
1214

1315
3.13.8: `Fix drilling cost types output unit conversion issue; Project Location (Latitude & Longitude); SHR Example 3 update <https://github.com/NatLabRockies/GEOPHIRES-X/pull/497>`__ | `release <https://github.com/NREL/GEOPHIRES-X/releases/tag/v3.13.8>`__

README.rst

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ Free software: `MIT license <LICENSE>`__
5858
:alt: Supported implementations
5959
:target: https://pypi.org/project/geophires-x
6060

61-
.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.13.9.svg
61+
.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.13.10.svg
6262
:alt: Commits since latest release
63-
:target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.13.9...main
63+
:target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.13.10...main
6464

6565
.. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat
6666
:target: https://softwareengineerprogrammer.github.io/GEOPHIRES
@@ -236,6 +236,10 @@ Example-specific web interface deeplinks are listed in the Link column.
236236
- `example13.txt <tests/examples/example13.txt>`__
237237
- `.out <tests/examples/example13.out>`__
238238
- `link <https://gtp.scientificwebservices.com/geophires?geophires-example-id=example13>`__
239+
* - Example 13b: Redrilling due to Well Integrity
240+
- `example13.txt <tests/examples/example13b_well-integrity.txt>`__
241+
- `.out <tests/examples/example13b_well-integrity.out>`__
242+
- `link <https://gtp.scientificwebservices.com/geophires?geophires-example-id=example13b_well-integrity>`__
239243
* - Example 14: Data Center
240244
- `example14_data-center.txt <tests/examples/example14_data-center.txt>`__
241245
- `.out <tests/examples/example14_data-center.out>`__

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
year = '2025'
1919
author = 'NREL'
2020
copyright = f'{year}, {author}'
21-
version = release = '3.13.9'
21+
version = release = '3.13.10'
2222

2323
pygments_style = 'trac'
2424
templates_path = ['./templates']

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def read(*names, **kwargs):
1313

1414
setup(
1515
name='geophires-x',
16-
version='3.13.9',
16+
version='3.13.10',
1717
license='MIT',
1818
description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.',
1919
long_description='{}\n{}'.format(

src/geophires_x/WellBores.py

Lines changed: 94 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,9 @@ def __init__(self, model: Model):
950950
ToolTipText="Productivity index defined as ratio of production well flow rate over production well inflow "
951951
"pressure drop (see docs)"
952952
)
953+
954+
well_integrity_max_lifetime_param_name = "Well Integrity Maximum Lifetime"
955+
# noinspection SpellCheckingInspection
953956
self.maxdrawdown = self.ParameterDict[self.maxdrawdown.Name] = floatParameter(
954957
"Maximum Drawdown",
955958
DefaultValue=1.0,
@@ -959,11 +962,33 @@ def __init__(self, model: Model):
959962
PreferredUnits=PercentUnit.TENTH,
960963
CurrentUnits=PercentUnit.TENTH,
961964
ErrMessage="assume default maximum drawdown (1)",
962-
ToolTipText="Maximum allowable thermal drawdown before redrilling of all wells into new reservoir "
963-
"(most applicable to EGS-type reservoirs with heat farming strategies). E.g. a value of 0.2 "
964-
"means that all wells are redrilled after the production temperature (at the wellhead) has "
965-
"dropped by 20% of its initial temperature"
965+
ToolTipText=f"Maximum allowable thermal drawdown before redrilling of all wells into new reservoir "
966+
f"(most applicable to EGS-type reservoirs with heat farming strategies). E.g. a value of 0.2 "
967+
f"means that all wells are redrilled after the production temperature (at the wellhead) has "
968+
f"dropped by 20% of its initial temperature. "
969+
f"Note that redrilling is triggered by whichever occurs first: this thermal drawdown limit or "
970+
f"the chronological limit defined by {well_integrity_max_lifetime_param_name}."
966971
)
972+
self.well_integrity_max_lifetime = self.ParameterDict[self.well_integrity_max_lifetime.Name] = floatParameter(
973+
well_integrity_max_lifetime_param_name,
974+
DefaultValue=-1.0,
975+
Min=0.1,
976+
Max=100.0,
977+
UnitType=Units.TIME,
978+
PreferredUnits=TimeUnit.YEAR,
979+
CurrentUnits=TimeUnit.YEAR,
980+
Required=False,
981+
Provided=False,
982+
ToolTipText=f"Maximum chronological lifetime of the wellbore infrastructure before mechanical/chemical "
983+
f"failure forces a redrilling event, independent of thermal drawdown. Models a deterministic "
984+
f"first-order approximation of well integrity failure (compressive yielding, high-temperature "
985+
f"creep, low-cycle fatigue, cement degradation, sulfide stress cracking, etc.) that is "
986+
f"particularly relevant for Superhot Rock systems where wellbore infrastructure "
987+
f"may typically fail before thermal depletion. Redrilling is triggered at the minimum of the "
988+
f"thermal drawdown index defined by {self.maxdrawdown.Name} and this chronological index. "
989+
f"If not provided, defaults to project lifetime (no mechanical failure)."
990+
)
991+
967992
self.IsAGS = self.ParameterDict[self.IsAGS.Name] = boolParameter(
968993
"Is AGS",
969994
DefaultValue=False,
@@ -1615,22 +1640,71 @@ def Calculate(self, model: Model) -> None:
16151640
model.logger.info(f'complete {self.__class__.__name__}: {__name__}')
16161641

16171642
def calculate_redrilling(self, model: Model) -> None:
1618-
# Redrilling applies to the built-in analytical reservoir models and user-provided profile.
1619-
if model.reserv.resoption.value in \
1620-
[ReservoirModel.MULTIPLE_PARALLEL_FRACTURES, ReservoirModel.LINEAR_HEAT_SWEEP,
1621-
ReservoirModel.SINGLE_FRACTURE, ReservoirModel.ANNUAL_PERCENTAGE, ReservoirModel.USER_PROVIDED_PROFILE]:
1622-
index_first_max_drawdown = np.argmax(
1623-
self.ProducedTemperature.value < (1 - model.wellbores.maxdrawdown.value) *
1624-
self.ProducedTemperature.value[0])
1625-
1626-
if index_first_max_drawdown > 0: # redrilling necessary
1627-
self.redrill.value = int(np.floor(len(self.ProducedTemperature.value) / index_first_max_drawdown))
1628-
ProducedTemperatureRepeated = np.tile(self.ProducedTemperature.value[0:index_first_max_drawdown],
1629-
self.redrill.value + 1)
1630-
self.ProducedTemperature.value = ProducedTemperatureRepeated[0:len(self.ProducedTemperature.value)]
1631-
TResOutputRepeated = np.tile(model.reserv.Tresoutput.value[0:index_first_max_drawdown],
1632-
self.redrill.value + 1)
1633-
model.reserv.Tresoutput.value = TResOutputRepeated[0:len(self.ProducedTemperature.value)]
1643+
"""
1644+
Redrilling applies to the built-in analytical reservoir models and user-provided profile.
1645+
"""
1646+
1647+
if model.reserv.resoption.value not in [
1648+
ReservoirModel.MULTIPLE_PARALLEL_FRACTURES, ReservoirModel.LINEAR_HEAT_SWEEP,
1649+
ReservoirModel.SINGLE_FRACTURE, ReservoirModel.ANNUAL_PERCENTAGE,
1650+
ReservoirModel.USER_PROVIDED_PROFILE,
1651+
]:
1652+
return
1653+
1654+
total_steps = len(self.ProducedTemperature.value)
1655+
project_lifetime_yr = model.surfaceplant.plant_lifetime.value
1656+
1657+
# Thermal drawdown trigger
1658+
index_first_max_drawdown = int(np.argmax(
1659+
self.ProducedTemperature.value
1660+
< (1 - model.wellbores.maxdrawdown.value) * self.ProducedTemperature.value[0]
1661+
))
1662+
1663+
# Well integrity (chronological) trigger
1664+
if self.well_integrity_max_lifetime.Provided and self.well_integrity_max_lifetime.value > 0:
1665+
timesteps_per_year = total_steps / project_lifetime_yr
1666+
integrity_failure_index = int(self.well_integrity_max_lifetime.value * timesteps_per_year)
1667+
# Clamp: a lifetime >= project lifetime means "no mechanical failure"
1668+
if integrity_failure_index >= total_steps:
1669+
integrity_failure_index = 0 # treat as "no trigger" sentinel
1670+
else:
1671+
integrity_failure_index = 0 # no mechanical failure modeled
1672+
1673+
# Competing-risk: take the earliest non-zero trigger
1674+
candidates = [i for i in (index_first_max_drawdown, integrity_failure_index) if i > 0]
1675+
if not candidates:
1676+
return # no redrilling
1677+
redrill_trigger_index = min(candidates)
1678+
1679+
if redrill_trigger_index <= 0 or redrill_trigger_index >= total_steps:
1680+
return
1681+
1682+
self.redrill.value = int(np.floor(total_steps / redrill_trigger_index))
1683+
ProducedTemperatureRepeated = np.tile(
1684+
self.ProducedTemperature.value[0:redrill_trigger_index],
1685+
self.redrill.value + 1,
1686+
)
1687+
self.ProducedTemperature.value = ProducedTemperatureRepeated[0:total_steps]
1688+
TResOutputRepeated = np.tile(
1689+
model.reserv.Tresoutput.value[0:redrill_trigger_index],
1690+
self.redrill.value + 1,
1691+
)
1692+
model.reserv.Tresoutput.value = TResOutputRepeated[0:total_steps]
1693+
1694+
# Log which mechanism dominated
1695+
if (integrity_failure_index > 0
1696+
and (index_first_max_drawdown == 0 or integrity_failure_index < index_first_max_drawdown)):
1697+
model.logger.info(
1698+
f"Redrilling driven by {self.well_integrity_max_lifetime.Name} "
1699+
f"({self.well_integrity_max_lifetime.value} {self.well_integrity_max_lifetime.CurrentUnits}); "
1700+
f"redrill events = {self.redrill.value}."
1701+
)
1702+
else:
1703+
model.logger.info(
1704+
f"Redrilling driven by thermal drawdown ({self.maxdrawdown.Name}; "
1705+
f"{self.maxdrawdown.quantity().to(convertible_unit('percent')).magnitude:.2f}%); "
1706+
f"redrill events = {self.redrill.value}."
1707+
)
16341708

16351709
def _sync_output_params_from_input_params(self) -> None:
16361710
"""

src/geophires_x/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '3.13.9'
1+
__version__ = '3.13.10'

src/geophires_x_schema_generator/geophires-request.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -818,14 +818,23 @@
818818
"maximum": 10000.0
819819
},
820820
"Maximum Drawdown": {
821-
"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",
821+
"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.",
822822
"type": "number",
823823
"units": "",
824824
"category": "Well Bores",
825825
"default": 1.0,
826826
"minimum": 0.0,
827827
"maximum": "1.0"
828828
},
829+
"Well Integrity Maximum Lifetime": {
830+
"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).",
831+
"type": "number",
832+
"units": "yr",
833+
"category": "Well Bores",
834+
"default": -1.0,
835+
"minimum": 0.1,
836+
"maximum": 100.0
837+
},
829838
"Is AGS": {
830839
"description": "Set to true if the model is for an Advanced Geothermal System (AGS)",
831840
"type": "boolean",

tests/base_test_case.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import inspect
4+
import math
45
import numbers
56
import os.path
67
import sys
@@ -11,6 +12,7 @@
1112

1213
from geophires_x.GeoPHIRESUtils import sig_figs
1314
from geophires_x_client import GeophiresInputParameters
15+
from geophires_x_client import GeophiresXResult
1416

1517
# noinspection PyProtectedMember
1618
from geophires_x_client import _get_logger
@@ -29,6 +31,41 @@ def _get_test_file_content(self, test_file_name, **open_kw_args) -> str:
2931
def _list_test_files_dir(self, test_files_dir: str):
3032
return os.listdir(self._get_test_file_path(test_files_dir)) # noqa: PTH208
3133

34+
# noinspection PyMethodMayBeStatic
35+
def _sanitize_nan(self, r: GeophiresXResult) -> GeophiresXResult:
36+
"""
37+
Workaround for float('nan') != float('nan')
38+
See https://stackoverflow.com/questions/51728427/unittest-how-to-assert-if-the-two-possibly-nan-values-are-equal
39+
40+
TODO generalize beyond After-tax IRR
41+
42+
Mutates passed-in result object.
43+
"""
44+
45+
irr_key = 'After-tax IRR'
46+
if irr_key in r.result['ECONOMIC PARAMETERS']:
47+
try:
48+
if math.isnan(r.result['ECONOMIC PARAMETERS'][irr_key]['value']):
49+
r.result['ECONOMIC PARAMETERS'][irr_key]['value'] = 'NaN'
50+
except TypeError:
51+
pass
52+
53+
return r
54+
55+
# noinspection PyMethodMayBeStatic
56+
def _strip_metadata(self, geophires_result: GeophiresXResult) -> GeophiresXResult:
57+
"""
58+
Useful for comparing results from different runs.
59+
60+
Mutates passed-in result object.
61+
"""
62+
63+
for key in ['metadata', 'Simulation Metadata']:
64+
if key in geophires_result.result:
65+
del geophires_result.result[key]
66+
67+
return geophires_result
68+
3269
def assertAlmostEqualWithinPercentage(self, expected, actual, msg: str | None = None, percent=5):
3370
if msg is not None and not isinstance(msg, str):
3471
raise ValueError(f'msg must be a string (you may have meant to pass percent={msg})')

0 commit comments

Comments
 (0)