Skip to content

Commit 8ed0d46

Browse files
Merge pull request #175 from softwareengineerprogrammer/stimulation-cost-per-fracture-surface-area
Stimulation cost per fracture surface area [v3.15.2]
2 parents 90fd881 + a3a239c commit 8ed0d46

13 files changed

Lines changed: 304 additions & 16 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.15.0
2+
current_version = 3.15.2
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.15.0
57+
version: 3.15.2
5858
version_manager: "bump2version"
5959
website: "https://github.com/NREL"
6060
year_from: "2023"

CHANGELOG.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ GEOPHIRES v3 (2023-2026)
88
3.15
99
^^^^
1010

11-
3.15: `Calculate S-DAC carbon revenue (BICYCLE) <https://github.com/NREL/GEOPHIRES-X/pull/510>`__ | `release <https://github.com/NREL/GEOPHIRES-X/releases/tag/v3.15.0>`__ | **Changed:** S-DAC carbon revenue is now calculated. See `the tracking issue <https://github.com/NatLabRockies/GEOPHIRES-X/issues/341>`__ for details.
11+
3.15: `Calculate S-DAC carbon revenue (BICYCLE); Reservoir Stimulation Capital Cost per Fracture Surface Area <https://github.com/NREL/GEOPHIRES-X/pull/510>`__ | `release <https://github.com/NREL/GEOPHIRES-X/releases/tag/v3.15.2>`__ | **Changed:** S-DAC carbon revenue is now calculated. See `the tracking issue <https://github.com/NatLabRockies/GEOPHIRES-X/issues/341>`__ for details.
1212

1313
3.14
1414
^^^^

README.rst

Lines changed: 2 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.15.0.svg
61+
.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.15.2.svg
6262
:alt: Commits since latest release
63-
:target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.15.0...main
63+
:target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.15.2...main
6464

6565
.. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat
6666
:target: https://softwareengineerprogrammer.github.io/GEOPHIRES

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.15.0'
21+
version = release = '3.15.2'
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.15.0',
16+
version='3.15.2',
1717
license='MIT',
1818
description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.',
1919
long_description='{}\n{}'.format(

src/geophires_x/Economics.py

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
project_payback_period_parameter, inflation_cost_during_construction_output_parameter, \
1717
interest_during_construction_output_parameter, total_capex_parameter_output_parameter, \
1818
overnight_capital_cost_output_parameter, CONSTRUCTION_CAPEX_SCHEDULE_PARAMETER_NAME, \
19-
_YEAR_INDEX_VALUE_EXPLANATION_SNIPPET, investment_tax_credit_output_parameter, lcoh_output_parameter, lcoc_output_parameter
20-
from geophires_x.ParameterUtils import expand_schedule_dsl
19+
_YEAR_INDEX_VALUE_EXPLANATION_SNIPPET, investment_tax_credit_output_parameter, lcoh_output_parameter, \
20+
lcoc_output_parameter
21+
from geophires_x.ParameterUtils import expand_schedule_dsl, CALCULATED_PARAMETER_PLACEHOLDER_VALUE
2122
from geophires_x.GeoPHIRESUtils import quantity
2223
from geophires_x.OptionList import Configuration, WellDrillingCostCorrelation, EconomicModel, EndUseOptions, PlantType, \
2324
_WellDrillingCostCorrelationCitation
@@ -652,6 +653,8 @@ def __init__(self, model: Model):
652653
f'For traditional hydrothermal reservoirs, this parameter should be set to $0.'
653654
)
654655

656+
before_stim_modifiers_note = f'before adjustment factor, indirect costs, and contingency'
657+
655658
max_stimulation_cost_per_well_MUSD = 100
656659
self.stimulation_cost_per_injection_well = \
657660
self.ParameterDict[self.stimulation_cost_per_injection_well.Name] = floatParameter(
@@ -663,7 +666,7 @@ def __init__(self, model: Model):
663666
PreferredUnits=CurrencyUnit.MDOLLARS,
664667
CurrentUnits=CurrencyUnit.MDOLLARS,
665668
Provided=False,
666-
ToolTipText='Reservoir stimulation capital cost per injection well before indirect costs and contingency'
669+
ToolTipText=f'Reservoir stimulation capital cost per injection well {before_stim_modifiers_note}'
667670
)
668671

669672
stimulation_cost_per_production_well_default_value_MUSD = 0
@@ -674,15 +677,30 @@ def __init__(self, model: Model):
674677
self.ParameterDict[self.stimulation_cost_per_production_well.Name] = floatParameter(
675678
'Reservoir Stimulation Capital Cost per Production Well',
676679
DefaultValue=stimulation_cost_per_production_well_default_value_MUSD,
677-
Min=0,
680+
Min=-1,
678681
Max=max_stimulation_cost_per_well_MUSD,
679682
UnitType=Units.CURRENCY,
680683
PreferredUnits=CurrencyUnit.MDOLLARS,
681684
CurrentUnits=CurrencyUnit.MDOLLARS,
682-
ToolTipText=f'Reservoir stimulation capital cost per production well before indirect costs and contingency'
685+
ToolTipText=f'Reservoir stimulation capital cost per production well {before_stim_modifiers_note}'
683686
f'{stimulation_cost_per_production_well_default_value_note}'
684687
)
685688

689+
self.stimulation_cost_per_fracture_surface_area = \
690+
self.ParameterDict[self.stimulation_cost_per_fracture_surface_area.Name] = floatParameter(
691+
'Reservoir Stimulation Capital Cost per Fracture Surface Area',
692+
Min=0,
693+
Max=1000,
694+
DefaultValue=0.9,
695+
UnitType=Units.COSTPERAREA,
696+
PreferredUnits=CostPerAreaUnit.DOLLARSPERMETERS2,
697+
CurrentUnits=CostPerAreaUnit.DOLLARSPERMETERS2,
698+
ToolTipText=f'Direct reservoir stimulation cost per fracture surface area {before_stim_modifiers_note}. '
699+
f'By default, this applies only to injection wells. To include production wells, set '
700+
f'{self.stimulation_cost_per_production_well.Name} = {CALCULATED_PARAMETER_PLACEHOLDER_VALUE}.'
701+
)
702+
703+
# noinspection SpellCheckingInspection
686704
self.ccstimadjfactor = self.ParameterDict[self.ccstimadjfactor.Name] = floatParameter(
687705
"Reservoir Stimulation Capital Cost Adjustment Factor",
688706
DefaultValue=1.0,
@@ -2822,6 +2840,8 @@ def _warn(_msg: str) -> None:
28222840
if sam_em_only_param.Provided:
28232841
raise NotImplementedError(f'{sam_em_only_param.Name} is only supported for SAM Economic Models')
28242842

2843+
self._validate_read_stimulation_cost_parameters(model)
2844+
28252845
else:
28262846
model.logger.info("No parameters read because no content provided")
28272847

@@ -2865,6 +2885,34 @@ def _set_ratio(frac: float) -> None:
28652885

28662886
model.logger.info(f'complete {__class__!s}: {sys._getframe().f_code.co_name}')
28672887

2888+
def _validate_read_stimulation_cost_parameters(self, _model: Model) -> None:
2889+
def _raise_mutually_exclusive_error(param1: Parameter, param2: Parameter) -> None:
2890+
raise ValueError(f'Cannot provide both {param1.Name} and {param2.Name} parameters. ')
2891+
2892+
if self.stimulation_cost_per_fracture_surface_area.Provided:
2893+
if self.ccstimfixed.Provided:
2894+
_raise_mutually_exclusive_error(
2895+
self.ccstimfixed,
2896+
self.stimulation_cost_per_fracture_surface_area
2897+
)
2898+
if self.stimulation_cost_per_injection_well.Provided:
2899+
_raise_mutually_exclusive_error(
2900+
self.stimulation_cost_per_injection_well,
2901+
self.stimulation_cost_per_fracture_surface_area
2902+
)
2903+
if self.stimulation_cost_per_production_well.Provided and \
2904+
self.stimulation_cost_per_production_well.value != \
2905+
CALCULATED_PARAMETER_PLACEHOLDER_VALUE: # Placeholder indicates production wells are stimulated
2906+
_raise_mutually_exclusive_error(
2907+
self.stimulation_cost_per_production_well,
2908+
self.stimulation_cost_per_fracture_surface_area
2909+
)
2910+
elif self.stimulation_cost_per_production_well.Provided and self.stimulation_cost_per_production_well.value < 0:
2911+
raise ValueError(f'{self.stimulation_cost_per_production_well.Name} must be positive')
2912+
2913+
# TODO validate fixed stimulation cost param mutual exclusivity with stim cost per well params (warn instead of
2914+
# raising error for backwards compatibility where applicable)
2915+
28682916
def sync_interest_rate(self, model):
28692917
def discount_rate_display() -> str:
28702918
return str(self.discountrate.quantity()).replace(' dimensionless', '')
@@ -3099,6 +3147,7 @@ def calculate_wellfield_costs(self, model: Model) -> None:
30993147

31003148
def calculate_stimulation_costs(self, model: Model) -> PlainQuantity:
31013149
production_wells_stimulated: bool = self.stimulation_cost_per_production_well.Provided
3150+
31023151
if self.ccstimfixed.Valid:
31033152
stimulation_costs_cstim_u = self.ccstimfixed.quantity().to(self.Cstim.CurrentUnits).magnitude
31043153

@@ -3122,6 +3171,39 @@ def calculate_stimulation_costs(self, model: Model) -> PlainQuantity:
31223171

31233172
ret = quantity(stimulation_costs_cstim_u, self.Cstim.CurrentUnits)
31243173
else:
3174+
if self.stimulation_cost_per_fracture_surface_area.Provided:
3175+
total_fracture_surface_area_q = (model.reserv.fracareacalc.quantity() * model.reserv.fracnumbcalc.value).to(
3176+
self.stimulation_cost_per_fracture_surface_area.CurrentUnits.get_area_unit_str())
3177+
direct_stim_cost_q = (total_fracture_surface_area_q *
3178+
self.stimulation_cost_per_fracture_surface_area.quantity())
3179+
inj_to_prod_cost_ratio = 1 if not production_wells_stimulated else \
3180+
model.wellbores.ninj.value / (model.wellbores.nprod.value + model.wellbores.ninj.value)
3181+
3182+
# Coerces equal injection and production well costs required for stimulation cost per well output
3183+
# display heuristic
3184+
per_well_cost_precision = 3
3185+
3186+
self.stimulation_cost_per_injection_well.value = round(
3187+
quantity(
3188+
inj_to_prod_cost_ratio * direct_stim_cost_q / model.wellbores.ninj.value,
3189+
self.stimulation_cost_per_fracture_surface_area.CurrentUnits.get_currency_unit_str()
3190+
).to(self.stimulation_cost_per_injection_well.CurrentUnits).magnitude,
3191+
per_well_cost_precision
3192+
)
3193+
3194+
self.stimulation_cost_per_production_well.value = round(
3195+
quantity(
3196+
(1 - inj_to_prod_cost_ratio) * direct_stim_cost_q / model.wellbores.nprod.value,
3197+
self.stimulation_cost_per_fracture_surface_area.CurrentUnits.get_currency_unit_str()
3198+
).to(self.stimulation_cost_per_production_well.CurrentUnits).magnitude,
3199+
per_well_cost_precision
3200+
)
3201+
3202+
# In an ideal world we might use separate calculated values instead of mutating
3203+
# stimulation_cost_per_injection_well and stimulation_cost_per_production_well above. But the current
3204+
# pattern is OK for now and nominally captures the fact that the value is mutated internally by
3205+
# virtue of the Provided attribute remaining False.
3206+
31253207
direct_stim_cost_per_injection_well_cstim_u = self.stimulation_cost_per_injection_well.quantity().to(
31263208
self.Cstim.CurrentUnits).magnitude
31273209
direct_stim_cost_per_production_well_cstim_u = self.stimulation_cost_per_production_well.quantity().to(

src/geophires_x/ParameterUtils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
COMMENT_PARAMETER_NAME_PREFIX: str = '_COMMENT-'
1313

14+
CALCULATED_PARAMETER_PLACEHOLDER_VALUE = -1
15+
1416

1517
def expand_schedule_dsl(
1618
schedule_strings: list[str | float], total_years: int, allow_schedule_length_to_exceed_total_years: bool = False

src/geophires_x/Units.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class Units(IntEnum):
8888
DECAY_RATE = auto()
8989
INFLATION_RATE = auto()
9090
DYNAMIC_VISCOSITY = auto()
91+
COSTPERAREA = auto()
9192

9293

9394
class AngleUnit(str, Enum):
@@ -243,6 +244,15 @@ class CostPerDistanceUnit(str, Enum):
243244
DOLLARSPERM = "USD/m"
244245

245246

247+
class CostPerAreaUnit(str, Enum):
248+
DOLLARSPERMETERS2 = "USD/m**2"
249+
250+
def get_currency_unit_str(self) -> str:
251+
return self.value.split('/')[0]
252+
253+
def get_area_unit_str(self) -> str:
254+
return self.value.split('/')[1]
255+
246256
class PressureUnit(str, Enum):
247257
"""Pressure Units"""
248258
MPASCAL = "MPa"

src/geophires_x/__init__.py

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

0 commit comments

Comments
 (0)