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
68 changes: 64 additions & 4 deletions src/geophires_x/Economics.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
project_payback_period_parameter, inflation_cost_during_construction_output_parameter, \
interest_during_construction_output_parameter, total_capex_parameter_output_parameter, \
overnight_capital_cost_output_parameter, CONSTRUCTION_CAPEX_SCHEDULE_PARAMETER_NAME, \
_YEAR_INDEX_VALUE_EXPLANATION_SNIPPET, investment_tax_credit_output_parameter, lcoh_output_parameter, lcoc_output_parameter
_YEAR_INDEX_VALUE_EXPLANATION_SNIPPET, investment_tax_credit_output_parameter, lcoh_output_parameter, \
lcoc_output_parameter, CALCULATED_PARAMETER_PLACEHOLDER_VALUE
from geophires_x.ParameterUtils import expand_schedule_dsl
from geophires_x.GeoPHIRESUtils import quantity
from geophires_x.OptionList import Configuration, WellDrillingCostCorrelation, EconomicModel, EndUseOptions, PlantType, \
Expand Down Expand Up @@ -652,6 +653,8 @@ def __init__(self, model: Model):
f'For traditional hydrothermal reservoirs, this parameter should be set to $0.'
)

before_stim_modifiers_note = f'before adjustment factor, indirect costs, and contingency'

max_stimulation_cost_per_well_MUSD = 100
self.stimulation_cost_per_injection_well = \
self.ParameterDict[self.stimulation_cost_per_injection_well.Name] = floatParameter(
Expand All @@ -663,7 +666,7 @@ def __init__(self, model: Model):
PreferredUnits=CurrencyUnit.MDOLLARS,
CurrentUnits=CurrencyUnit.MDOLLARS,
Provided=False,
ToolTipText='Reservoir stimulation capital cost per injection well before indirect costs and contingency'
ToolTipText=f'Reservoir stimulation capital cost per injection well {before_stim_modifiers_note}'
)

stimulation_cost_per_production_well_default_value_MUSD = 0
Expand All @@ -674,15 +677,31 @@ def __init__(self, model: Model):
self.ParameterDict[self.stimulation_cost_per_production_well.Name] = floatParameter(
'Reservoir Stimulation Capital Cost per Production Well',
DefaultValue=stimulation_cost_per_production_well_default_value_MUSD,
Min=0,
Min=-1,
Max=max_stimulation_cost_per_well_MUSD,
UnitType=Units.CURRENCY,
PreferredUnits=CurrencyUnit.MDOLLARS,
CurrentUnits=CurrencyUnit.MDOLLARS,
ToolTipText=f'Reservoir stimulation capital cost per production well before indirect costs and contingency'
ToolTipText=f'Reservoir stimulation capital cost per production well {before_stim_modifiers_note}'
f'{stimulation_cost_per_production_well_default_value_note}'
)

self.stimulation_cost_per_fracture_surface_area = \
self.ParameterDict[self.stimulation_cost_per_fracture_surface_area.Name] = floatParameter(
'Reservoir Stimulation Capital Cost per Fracture Surface Area',
Min=0,
Max=1000,
DefaultValue=0.9,
UnitType=Units.COSTPERAREA,
PreferredUnits=CostPerAreaUnit.DOLLARSPERMETERS2,
CurrentUnits=CostPerAreaUnit.DOLLARSPERMETERS2,
# TODO/WIP...
ToolTipText=f'Reservoir stimulation capital cost per fracture surface area {before_stim_modifiers_note}. '
f'Provide {self.stimulation_cost_per_production_well.Name} = '
f'{CALCULATED_PARAMETER_PLACEHOLDER_VALUE} to indicate that production wells are stimulated.'
)

# noinspection SpellCheckingInspection
self.ccstimadjfactor = self.ParameterDict[self.ccstimadjfactor.Name] = floatParameter(
"Reservoir Stimulation Capital Cost Adjustment Factor",
DefaultValue=1.0,
Expand Down Expand Up @@ -2822,6 +2841,8 @@ def _warn(_msg: str) -> None:
if sam_em_only_param.Provided:
raise NotImplementedError(f'{sam_em_only_param.Name} is only supported for SAM Economic Models')

self._validate_read_stimulation_cost_parameters(model)

else:
model.logger.info("No parameters read because no content provided")

Expand Down Expand Up @@ -2865,6 +2886,34 @@ def _set_ratio(frac: float) -> None:

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

def _validate_read_stimulation_cost_parameters(self, model: Model) -> None:
def _raise_mutually_exclusive_error(param1: Parameter, param2: Parameter) -> None:
raise ValueError(f'Cannot provide both {param1.Name} and {param2.Name} parameters. ')

if self.stimulation_cost_per_fracture_surface_area.Provided:
if self.ccstimfixed.Provided:
_raise_mutually_exclusive_error(
self.ccstimfixed,
self.stimulation_cost_per_fracture_surface_area
)
if self.stimulation_cost_per_injection_well.Provided:
_raise_mutually_exclusive_error(
self.stimulation_cost_per_injection_well,
self.stimulation_cost_per_fracture_surface_area
)
if self.stimulation_cost_per_production_well.Provided and \
self.stimulation_cost_per_production_well.value != \
CALCULATED_PARAMETER_PLACEHOLDER_VALUE: # Placeholder indicates production wells are stimulated
_raise_mutually_exclusive_error(
self.stimulation_cost_per_production_well,
self.stimulation_cost_per_fracture_surface_area
)
elif self.stimulation_cost_per_production_well.Provided and self.stimulation_cost_per_production_well.value < 0:
raise ValueError(f'{self.stimulation_cost_per_production_well.Name} must be positive')

# TODO validate fixed stimulation cost param mutual exclusivity with stim cost per well params (warn instead of
# raising error for backwards compatibility where applicable)

def sync_interest_rate(self, model):
def discount_rate_display() -> str:
return str(self.discountrate.quantity()).replace(' dimensionless', '')
Expand Down Expand Up @@ -3099,6 +3148,7 @@ def calculate_wellfield_costs(self, model: Model) -> None:

def calculate_stimulation_costs(self, model: Model) -> PlainQuantity:
production_wells_stimulated: bool = self.stimulation_cost_per_production_well.Provided

if self.ccstimfixed.Valid:
stimulation_costs_cstim_u = self.ccstimfixed.quantity().to(self.Cstim.CurrentUnits).magnitude

Expand All @@ -3122,6 +3172,16 @@ def calculate_stimulation_costs(self, model: Model) -> PlainQuantity:

ret = quantity(stimulation_costs_cstim_u, self.Cstim.CurrentUnits)
else:
if self.stimulation_cost_per_fracture_surface_area.Provided:
total_fracture_surface_area_q = (model.reserv.fracareacalc.quantity() * model.reserv.fracnumbcalc.value).to(
self.stimulation_cost_per_fracture_surface_area.CurrentUnits.get_area_unit_str())
direct_stim_cost_q = (total_fracture_surface_area_q *
self.stimulation_cost_per_fracture_surface_area.quantity())
inj_to_prod_cost_ratio = 1 if not production_wells_stimulated else \
model.wellbores.ninj.value / (model.wellbores.nprod.value + model.wellbores.ninj.value)
self.stimulation_cost_per_injection_well.value = quantity(inj_to_prod_cost_ratio * direct_stim_cost_q / model.wellbores.ninj.value, self.stimulation_cost_per_fracture_surface_area.CurrentUnits.get_currency_unit_str()).to(self.stimulation_cost_per_injection_well.CurrentUnits).magnitude
self.stimulation_cost_per_production_well.value = quantity((1 - inj_to_prod_cost_ratio) * direct_stim_cost_q / model.wellbores.nprod.value, self.stimulation_cost_per_fracture_surface_area.CurrentUnits.get_currency_unit_str()).to(self.stimulation_cost_per_production_well.CurrentUnits).magnitude

direct_stim_cost_per_injection_well_cstim_u = self.stimulation_cost_per_injection_well.quantity().to(
self.Cstim.CurrentUnits).magnitude
direct_stim_cost_per_production_well_cstim_u = self.stimulation_cost_per_production_well.quantity().to(
Expand Down
2 changes: 2 additions & 0 deletions src/geophires_x/EconomicsUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
f'The value is specified as a project year index corresponding to the ' f'Year row in the cash flow profile'
)

CALCULATED_PARAMETER_PLACEHOLDER_VALUE = -1


def BuildPricingModel(
plantlifetime: int,
Expand Down
10 changes: 10 additions & 0 deletions src/geophires_x/Units.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class Units(IntEnum):
DECAY_RATE = auto()
INFLATION_RATE = auto()
DYNAMIC_VISCOSITY = auto()
COSTPERAREA = auto()


class AngleUnit(str, Enum):
Expand Down Expand Up @@ -243,6 +244,15 @@ class CostPerDistanceUnit(str, Enum):
DOLLARSPERM = "USD/m"


class CostPerAreaUnit(str, Enum):
DOLLARSPERMETERS2 = "USD/m**2"

def get_currency_unit_str(self) -> str:
return self.value.split('/')[0]

def get_area_unit_str(self) -> str:
return self.value.split('/')[1]

class PressureUnit(str, Enum):
"""Pressure Units"""
MPASCAL = "MPa"
Expand Down
15 changes: 12 additions & 3 deletions src/geophires_x_schema_generator/geophires-request.json
Original file line number Diff line number Diff line change
Expand Up @@ -1488,7 +1488,7 @@
"maximum": 1000
},
"Reservoir Stimulation Capital Cost per Injection Well": {
"description": "Reservoir stimulation capital cost per injection well before indirect costs and contingency",
"description": "Reservoir stimulation capital cost per injection well before adjustment factor, indirect costs, and contingency",
"type": "number",
"units": "MUSD",
"category": "Economics",
Expand All @@ -1497,14 +1497,23 @@
"maximum": 100
},
"Reservoir Stimulation Capital Cost per Production Well": {
"description": "Reservoir stimulation capital cost per production well before indirect costs and contingency. By default, only the injection wells are assumed to be stimulated unless this parameter is provided.",
"description": "Reservoir stimulation capital cost per production well before adjustment factor, indirect costs, and contingency. By default, only the injection wells are assumed to be stimulated unless this parameter is provided.",
"type": "number",
"units": "MUSD",
"category": "Economics",
"default": 0,
"minimum": 0,
"minimum": -1,
"maximum": 100
},
"Reservoir Stimulation Capital Cost per Fracture Surface Area": {
"description": "Reservoir stimulation capital cost per fracture surface area before adjustment factor, indirect costs, and contingency. Provide Reservoir Stimulation Capital Cost per Production Well = -1 to indicate that production wells are stimulated.",
"type": "number",
"units": "USD/m**2",
"category": "Economics",
"default": 0.9,
"minimum": 0,
"maximum": 1000
},
"Reservoir Stimulation Capital Cost Adjustment Factor": {
"description": "Multiplier for reservoir stimulation capital cost correlation",
"type": "number",
Expand Down
121 changes: 121 additions & 0 deletions tests/geophires_x_tests/generic-egs-case-5_no-stim-costs-specified.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Adapted from https://github.com/softwareengineerprogrammer/GEOPHIRES/blob/90fd881105029ede94fc0275f9dc247d47a7dd26/tests/examples/Fervo_Project_Cape-5.txt

# *** ECONOMIC/FINANCIAL PARAMETERS ***
# *************************************
Economic Model, 5
Inflation Rate, .027

Starting Electricity Sale Price, 0.095
Electricity Escalation Rate Per Year, 0.00057
Ending Electricity Sale Price, 1
Electricity Escalation Start Year, 1

Fraction of Investment in Bonds, .7
Discount Rate, 0.12
Inflated Bond Interest Rate, .07

Inflated Bond Interest Rate During Construction, 0.105
Bond Financing Start Year, -2

Construction Years, 5

# ATB advanced scenario
# Construction CAPEX Schedule

# DOE scenario (alternative)
# Construction CAPEX Schedule

# DOE-ATB hybrid scenario
Construction CAPEX Schedule, 0.014,0.027,0.139,0.431,0.389

Investment Tax Credit Rate, 0.3
Combined Income Tax Rate, .2555
Property Tax Rate, 0.0022

Capital Cost for Power Plant for Electricity Generation, 1900
Exploration Capital Cost, 30

Well Drilling Cost Correlation, 3
Well Drilling and Completion Capital Cost Adjustment Factor, 0.9

Field Gathering System Capital Cost Adjustment Factor, 0.54

Royalty Rate, 0.0175
Royalty Rate Escalation Start Year, 11
Royalty Rate Escalation, 0.0175
Royalty Rate Maximum, 0.035


# *** SURFACE & SUBSURFACE TECHNICAL PARAMETERS ***
# *************************************************
End-Use Option, 1
Power Plant Type, 2
Plant Lifetime, 30

Surface Temperature, 13

Number of Segments, 3
Gradient 1, 74
Thickness 1, 2.5
Gradient 2, 41
Thickness 2, 0.5
Gradient 3, 39.1

Reservoir Depth, 2.68

Reservoir Density, 2800
Reservoir Heat Capacity, 790
Reservoir Thermal Conductivity, 3.05
Reservoir Porosity, 0.0118

Reservoir Model, 1
Reservoir Volume Option, 1

Number of Fractures per Stimulated Well, 150
Fracture Separation, 9.8255

Fracture Shape, 4
Fracture Width, 305
Fracture Height, 100

Water Loss Fraction, 0.01
Water Cost Adjustment Factor, 2

Ambient Temperature, 11.17

Utilization Factor, .913
Plant Outlet Pressure, 2000 psi
Circulation Pump Efficiency, 0.80

# *** Well Bores Parameters ***

Number of Production Wells, 56
Number of Injection Wells per Production Well, 0.666

Nonvertical Length per Multilateral Section, 5000 feet
Number of Multilateral Sections, 0

Production Flow Rate per Well, 107
# The ATB Advanced Scenario models sustained flow rates of 110 kg/s (NREL

Production Well Diameter, 8.535
Injection Well Diameter, 8.535

Production Wellhead Pressure, 303 psi

Injectivity Index, 1.38
Productivity Index, 1.13

Ramey Production Wellbore Model, True
Injection Temperature, 53.6
Injection Wellbore Temperature Gain, 3

Maximum Drawdown, 0.0025

# *** SIMULATION PARAMETERS ***
# *****************************
Maximum Temperature, 500
Time steps per year, 12

Project Latitude, 38.506196
Project Longitude, -112.918155
22 changes: 22 additions & 0 deletions tests/geophires_x_tests/test_economics.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
GeophiresXResult,
GeophiresXClient,
GeophiresInputParameters,
ImmutableGeophiresInputParameters,
)
from tests.base_test_case import BaseTestCase

Expand Down Expand Up @@ -135,6 +136,27 @@ def _lcoh_pbc(r: GeophiresXResult) -> tuple[float, float]:
self.assertLess(lcoh, 13.19)
self.assertEqual(0, peaking_boiler_cost)

def test_stimulation_cost_per_fracture_surface_area(self):
def _get_result() -> GeophiresXResult:
return GeophiresXClient().get_geophires_result(
ImmutableGeophiresInputParameters(
from_file_path=self._get_test_file_path('generic-egs-case-5_no-stim-costs-specified.txt'),
params={
'Reservoir Stimulation Capital Cost per Production Well': -1,
'Reservoir Stimulation Capital Cost per Fracture Surface Area': 0.89773,
'Print Output to Console': True,
},
)
)

r: GeophiresXResult = _get_result()
# FIXME WIP - should match FPC5 more closely
# (probably related to adjustment factor/indirect costs/contingency...)
cap_costs = r.result['CAPITAL COSTS (M$)']
self.assertAlmostEqual(454.02, cap_costs['Stimulation costs']['value'], delta=13)

self.assertAlmostEqual(4.83, cap_costs['Stimulation costs per well']['value'], delta=0.14)

# noinspection PyMethodMayBeStatic
def _new_model(
self, input_file: Path | None = None, additional_params: dict[str, Any] | None = None, read_and_calculate=False
Expand Down
Loading