Skip to content

Fervo_Project_Cape-4 minor corrections: multilaterals, number of fractures #81

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
c6957ea
Increase max Number of Multilateral SEctions to 1199 (3 laterals per …
softwareengineerprogrammer Jun 20, 2025
efefa76
Add 'Drilling and completion costs per well' as derived output (adds …
softwareengineerprogrammer Jun 20, 2025
6985fe5
Set Number of Multilateral Sections = 0 with comment explaining how v…
softwareengineerprogrammer Jun 20, 2025
8c133ac
Cost adjustment factor so well cost = .96M including 5% indirect costs
softwareengineerprogrammer Jun 20, 2025
cb253b4
Move test to test_fervo_project_cape_4.py
softwareengineerprogrammer Jun 20, 2025
4d39011
Move sig_figs to GeoPHIRESUtils.py
softwareengineerprogrammer Jun 20, 2025
d09824d
test_case_study_documentation
softwareengineerprogrammer Jun 20, 2025
87b8ce4
test that result capex $/kW matches documentation (tangentially relev…
softwareengineerprogrammer Jun 20, 2025
348db7b
Check relevant attributes exist before calculating derivation of dril…
softwareengineerprogrammer Jun 20, 2025
638d943
py38 annotation fix
softwareengineerprogrammer Jun 20, 2025
4a89fb4
Fix apparently-windows-incompatible unicode character in comment (thu…
softwareengineerprogrammer Jun 20, 2025
349eb68
Open Fervo_Project_Cape-4.md with utf-8 encoding by adding open_kw_ar…
softwareengineerprogrammer Jun 20, 2025
05d07fc
Fix number of fractures to actually be 102 per well (previous version…
softwareengineerprogrammer Jun 20, 2025
ce3fd60
Bump version: 3.9.24 → 3.9.25
softwareengineerprogrammer Jun 20, 2025
1733a0a
client - only initialize shared resources if current_process().name =…
softwareengineerprogrammer Jun 20, 2025
be62077
Disable client caching by default - this was effectively the default …
softwareengineerprogrammer Jun 20, 2025
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.9.24
current_version = 3.9.25
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.9.24
version: 3.9.25
version_manager: "bump2version"
website: "https://github.com/NREL"
year_from: "2023"
Expand Down
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,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.9.24.svg
.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.9.25.svg
:alt: Commits since latest release
:target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.24...main
:target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.25...main

.. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat
:target: https://nrel.github.io/GEOPHIRES-X
Expand Down
44 changes: 22 additions & 22 deletions docs/Fervo_Project_Cape-4.md

Large diffs are not rendered by default.

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.9.24'
version = release = '3.9.25'

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.9.24',
version='3.9.25',
license='MIT',
description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.',
long_description='{}\n{}'.format(
Expand Down
26 changes: 23 additions & 3 deletions src/geophires_x/Economics.py
Original file line number Diff line number Diff line change
Expand Up @@ -1633,18 +1633,29 @@ 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',
UnitType=Units.CURRENCY,
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',
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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"
24 changes: 3 additions & 21 deletions src/geophires_x/EconomicsSam.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
13 changes: 13 additions & 0 deletions src/geophires_x/GeoPHIRESUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion src/geophires_x/Outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
14 changes: 12 additions & 2 deletions src/geophires_x/WellBores.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
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.9.24'
__version__ = '3.9.25'
47 changes: 30 additions & 17 deletions src/geophires_x_client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import atexit
import os
import sys
import threading
from multiprocessing import Manager
from pathlib import Path
from multiprocessing import current_process

# noinspection PyPep8Naming
from geophires_x import GEOPHIRESv3 as geophires
Expand All @@ -29,31 +28,37 @@ class GeophiresXClient:
_init_lock = threading.Lock()
"""A standard threading lock to make the one-time initialization thread-safe."""

def __init__(self, enable_caching=True, logger_name=None):
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

# Lazy-initialize shared resources if they haven't been already.
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 manner. It also registers the shutdown hook to ensure
automatic cleanup on application exit.
thread-safe and now process-safe manner. It also registers the
shutdown hook to ensure automatic cleanup on application exit.
"""
with cls._init_lock:
if cls._manager is None:
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)
# 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):
Expand All @@ -65,9 +70,17 @@ def shutdown(cls):
"""
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.
atexit.unregister(cls.shutdown)
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
Expand All @@ -80,22 +93,23 @@ def get_geophires_result(self, input_params: GeophiresInputParameters) -> Geophi
"""
is_immutable = isinstance(input_params, ImmutableGeophiresInputParameters)

if not (self._enable_caching and is_immutable):
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)

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_cwd = Path.cwd()
stash_sys_argv = sys.argv
sys.argv = ['', input_params.as_file_path(), input_params.get_output_file_path()]

Expand All @@ -107,7 +121,6 @@ def _run_simulation(self, input_params: GeophiresInputParameters) -> GeophiresXR
raise RuntimeError('GEOPHIRES exited without giving a reason') from None
finally:
sys.argv = stash_sys_argv
os.chdir(stash_cwd)

self._logger.info(f'GEOPHIRES-X output file: {input_params.get_output_file_path()}')
result = GeophiresXResult(input_params.get_output_file_path())
Expand Down
4 changes: 2 additions & 2 deletions src/geophires_x_schema_generator/geophires-request.json
Original file line number Diff line number Diff line change
Expand Up @@ -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).",
Expand Down
6 changes: 5 additions & 1 deletion src/geophires_x_schema_generator/geophires-result.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {},
Expand Down
4 changes: 2 additions & 2 deletions tests/base_test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading