Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 3.13.9
current_version = 3.13.10
commit = True
tag = True

Expand Down
2 changes: 1 addition & 1 deletion .cookiecutterrc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ GEOPHIRES v3 (2023-2026)
3.13
^^^^

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>`__

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>`__

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>`__
Expand Down
8 changes: 6 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ Free software: `MIT license <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
Expand Down Expand Up @@ -236,6 +236,10 @@ Example-specific web interface deeplinks are listed in the Link column.
- `example13.txt <tests/examples/example13.txt>`__
- `.out <tests/examples/example13.out>`__
- `link <https://gtp.scientificwebservices.com/geophires?geophires-example-id=example13>`__
* - Example 13b: Redrilling due to Well Integrity
- `example13.txt <tests/examples/example13b_well-integrity.txt>`__
- `.out <tests/examples/example13b_well-integrity.out>`__
- `link <https://gtp.scientificwebservices.com/geophires?geophires-example-id=example13b_well-integrity>`__
* - Example 14: Data Center
- `example14_data-center.txt <tests/examples/example14_data-center.txt>`__
- `.out <tests/examples/example14_data-center.out>`__
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
114 changes: 94 additions & 20 deletions src/geophires_x/WellBores.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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:
"""
Expand Down
2 changes: 1 addition & 1 deletion src/geophires_x/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '3.13.9'
__version__ = '3.13.10'
11 changes: 10 additions & 1 deletion src/geophires_x_schema_generator/geophires-request.json
Original file line number Diff line number Diff line change
Expand Up @@ -818,14 +818,23 @@
"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",
"default": 1.0,
"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",
Expand Down
37 changes: 37 additions & 0 deletions tests/base_test_case.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import inspect
import math
import numbers
import os.path
import sys
Expand All @@ -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
Expand All @@ -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})')
Expand Down
Loading
Loading