diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 6afd1847f..d6c7fdf11 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.13.10 +current_version = 3.13.12 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index da07fd0c0..78bb6fa68 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -54,7 +54,7 @@ default_context: sphinx_doctest: "no" sphinx_theme: "sphinx-py3doc-enhanced-theme" test_matrix_separate_coverage: "no" - version: 3.13.10 + version: 3.13.12 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5dff8e903..39a20913e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # pre-commit install --install-hooks # To update the versions: # pre-commit autoupdate -exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg|src/geophires_x(?!/(GEOPHIRESv3|EconomicsSam|EconomicsSamCashFlow|EconomicsUtils|EconomicsSamPreRevenue|EconomicsSamCalculations|SurfacePlantUtils|NumpyUtils|UPPReservoir)\.py))(/|$)' +exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg|src/geophires_x(?!/(GEOPHIRESv3|EconomicsSam|EconomicsSamCashFlow|EconomicsUtils|EconomicsSamPreRevenue|EconomicsSamCalculations||ParameterUtils|SurfacePlantUtils|NumpyUtils|UPPReservoir)\.py))(/|$)' # Note the order is intentional to avoid multiple passes of the hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit diff --git a/README.rst b/README.rst index ace01e371..6955b0c4f 100644 --- a/README.rst +++ b/README.rst @@ -58,9 +58,9 @@ Free software: `MIT 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.10.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.13.12.svg :alt: Commits since latest release - :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.13.10...main + :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.13.12...main .. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat :target: https://softwareengineerprogrammer.github.io/GEOPHIRES diff --git a/docs/conf.py b/docs/conf.py index fa1f3ed83..74001604b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ year = '2025' author = 'NREL' copyright = f'{year}, {author}' -version = release = '3.13.10' +version = release = '3.13.12' pygments_style = 'trac' templates_path = ['./templates'] diff --git a/setup.py b/setup.py index 34b5ad8eb..72bae6292 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='geophires-x', - version='3.13.10', + version='3.13.12', license='MIT', description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.', long_description='{}\n{}'.format( diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index 071733c43..12de526db 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -16,8 +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, expand_schedule_dsl, \ - lcoh_output_parameter, lcoc_output_parameter + _YEAR_INDEX_VALUE_EXPLANATION_SNIPPET, investment_tax_credit_output_parameter, lcoh_output_parameter, lcoc_output_parameter +from geophires_x.ParameterUtils import expand_schedule_dsl from geophires_x.GeoPHIRESUtils import quantity from geophires_x.OptionList import Configuration, WellDrillingCostCorrelation, EconomicModel, EndUseOptions, PlantType, \ _WellDrillingCostCorrelationCitation diff --git a/src/geophires_x/EconomicsUtils.py b/src/geophires_x/EconomicsUtils.py index db0410979..70a7c7598 100644 --- a/src/geophires_x/EconomicsUtils.py +++ b/src/geophires_x/EconomicsUtils.py @@ -1,7 +1,6 @@ from __future__ import annotations -from geophires_x.GeoPHIRESUtils import is_float, is_int -from geophires_x.Parameter import OutputParameter, SCHEDULE_DSL_MULTIPLIER_SYMBOL +from geophires_x.Parameter import OutputParameter from geophires_x.Units import Units, PercentUnit, TimeUnit, CurrencyUnit, CurrencyFrequencyUnit, EnergyCostUnit CONSTRUCTION_CAPEX_SCHEDULE_PARAMETER_NAME = 'Construction CAPEX Schedule' @@ -248,90 +247,9 @@ def investment_tax_credit_output_parameter() -> OutputParameter: def expand_schedule_dsl(schedule_strings: list[str | float], total_years: int) -> list[float]: """ - Parse a duration-based scheduling DSL and expand it into a fixed-length time-series array. - - Syntax: `[Value] * [Years], [Value] * [Years], ..., [Terminal Value]` - - The terminal (last) value is repeated to fill `total_years`. A bare scalar - (e.g. `['2.5']`) is treated as a terminal value and broadcast across all years. - - Examples:: - - expand_schedule_dsl(['1.0 * 3', '0.1'], total_years=6) - # => [1.0, 1.0, 1.0, 0.1, 0.1, 0.1] - - expand_schedule_dsl(['2.5'], total_years=4) - # => [2.5, 2.5, 2.5, 2.5] - - :param schedule_strings: list of DSL segment strings. Each element is either - `" * "` (a run-length segment) or `""` (a scalar, - which becomes the terminal value when it is the last element, or a 1-year - segment otherwise). - :param total_years: The total number of years the expanded array must span - (typically `construction_years + plant_lifetime`). - :returns: A `list[float]` of length `total_years`. - :raises ValueError: On malformed DSL strings or when explicit segments exceed - `total_years`. + Deprecated, call ParameterUtils.expand_schedule_dsl """ - if total_years <= 0: - return [] - - if not schedule_strings: - return [0.0] * total_years - - segments: list[tuple[float, int | None]] = [] - for raw in schedule_strings: - raw = str(raw).strip() - if SCHEDULE_DSL_MULTIPLIER_SYMBOL in raw: - parts = raw.split(SCHEDULE_DSL_MULTIPLIER_SYMBOL) - if len(parts) != 2: - raise ValueError(f'Invalid schedule segment "{raw}": expected " * ".') - - val_raw = parts[0].strip() - if not is_float(val_raw): - raise ValueError(f'Invalid schedule segment "{raw}": "{val_raw}" is not a float.') - value = float(val_raw) - if value < 0: - raise ValueError(f'Invalid schedule segment "{raw}": {val_raw} is negative.') - - years_raw = parts[1].strip() - if not is_int(years_raw): - raise ValueError(f'Invalid schedule segment "{raw}": "{years_raw}" is not an int.') - - years = int(years_raw) - if years < 0: - raise ValueError(f'Invalid schedule segment "{raw}": year count must be non-negative.') - segments.append((value, years)) - else: - if not is_float(raw): - raise ValueError(f'Invalid schedule segment "{raw}": "{raw}" is not a float.') - - value = float(raw) - segments.append((value, None)) - - result: list[float] = [] - terminal_value = 0.0 - - for idx, (value, years) in enumerate(segments): - is_last = idx == len(segments) - 1 - if years is not None: - result.extend([value] * years) - terminal_value = value - else: - if is_last: - terminal_value = value - else: - result.append(value) - terminal_value = value - - if len(result) > total_years: - raise ValueError( - f'Invalid schedule: Schedule expands to {len(result)} years ' f'which exceeds total_years={total_years}.' - ) - - remaining = total_years - len(result) - if remaining > 0: - result.extend([terminal_value] * remaining) - - return result + from geophires_x.ParameterUtils import expand_schedule_dsl + + return expand_schedule_dsl(schedule_strings, total_years) diff --git a/src/geophires_x/ParameterUtils.py b/src/geophires_x/ParameterUtils.py new file mode 100644 index 000000000..6f70caa5b --- /dev/null +++ b/src/geophires_x/ParameterUtils.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +import copy +import logging + +from geophires_x.GeoPHIRESUtils import is_float, is_int +from geophires_x.Parameter import SCHEDULE_DSL_MULTIPLIER_SYMBOL + + +_log = logging.getLogger(__name__) + + +def expand_schedule_dsl( + schedule_strings: list[str | float], total_years: int, allow_schedule_length_to_exceed_total_years: bool = False +) -> list[float]: + """ + Parse a duration-based scheduling DSL and expand it into a fixed-length time-series array. + + Syntax: `[Value] * [Years], [Value] * [Years], ..., [Terminal Value]` + + The terminal (last) value is repeated to fill `total_years`. A bare scalar + (e.g. `['2.5']`) is treated as a terminal value and broadcast across all years. + + Examples:: + + expand_schedule_dsl(['1.0 * 3', '0.1'], total_years=6) + # => [1.0, 1.0, 1.0, 0.1, 0.1, 0.1] + + expand_schedule_dsl(['2.5'], total_years=4) + # => [2.5, 2.5, 2.5, 2.5] + + :param schedule_strings: list of DSL segment strings. Each element is either + `" * "` (a run-length segment) or `""` (a scalar, + which becomes the terminal value when it is the last element, or a 1-year + segment otherwise). + :param total_years: The total number of years the expanded array must span + (typically `construction_years + plant_lifetime`). + :returns: A `list[float]` of length `total_years`. + :raises ValueError: On malformed DSL strings or when explicit segments exceed + `total_years`. + """ + + if total_years <= 0: + return [] + + if not schedule_strings: + return [0.0] * total_years + + segments: list[tuple[float, int | None]] = [] + for raw in schedule_strings: + raw = str(raw).strip() + if SCHEDULE_DSL_MULTIPLIER_SYMBOL in raw: + parts = raw.split(SCHEDULE_DSL_MULTIPLIER_SYMBOL) + if len(parts) != 2: + raise ValueError(f'Invalid schedule segment "{raw}": expected " * ".') + + val_raw = parts[0].strip() + if not is_float(val_raw): + raise ValueError(f'Invalid schedule segment "{raw}": "{val_raw}" is not a float.') + value = float(val_raw) + if value < 0: + raise ValueError(f'Invalid schedule segment "{raw}": {val_raw} is negative.') + + years_raw = parts[1].strip() + if not is_int(years_raw): + raise ValueError(f'Invalid schedule segment "{raw}": "{years_raw}" is not an int.') + + years = int(years_raw) + if years < 0: + raise ValueError(f'Invalid schedule segment "{raw}": year count must be non-negative.') + segments.append((value, years)) + else: + if not is_float(raw): + raise ValueError(f'Invalid schedule segment "{raw}": "{raw}" is not a float.') + + value = float(raw) + segments.append((value, None)) + + result: list[float] = [] + terminal_value = 0.0 + + for idx, (value, years) in enumerate(segments): + is_last = idx == len(segments) - 1 + if years is not None: + result.extend([value] * years) + terminal_value = value + else: + if is_last: + terminal_value = value + else: + result.append(value) + terminal_value = value + + remaining = total_years - len(result) + if remaining > 0: + result.extend([terminal_value] * remaining) + + if len(result) > total_years: + if not allow_schedule_length_to_exceed_total_years: + raise ValueError( + f'Invalid schedule: Schedule expands to {len(result)} years ' + f'which exceeds total_years={total_years}.' + ) + else: + pre_truncation_result = copy.copy(result) + result = result[:total_years] + _log.warning( + f'Schedule expands to {len(pre_truncation_result)} years, which exceeds total_years={total_years}. ' + f'Schedule has been truncated to {total_years} years ({result}; from {pre_truncation_result}).' + ) + + return result diff --git a/src/geophires_x/SFReservoir.py b/src/geophires_x/SFReservoir.py index ebd7940ec..db75831c1 100644 --- a/src/geophires_x/SFReservoir.py +++ b/src/geophires_x/SFReservoir.py @@ -10,7 +10,7 @@ class SFReservoir(Reservoir): """ This class models the Single Fracture Reservoir. """ - def __init__(self, model:Model): + def __init__(self, model: Model): """ The __init__ function is called automatically when a class is instantiated. It initializes the attributes of an object, and sets default values for certain arguments that can be @@ -19,7 +19,7 @@ def __init__(self, model:Model): :type model: :class:`~geophires_x.Model.Model` :return: None """ - model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f'Init {__class__}: {sys._getframe().f_code.co_name}') super().__init__(model) # initialize the parent parameters and variables sclass = str(__class__).replace("", "") @@ -34,6 +34,8 @@ def __init__(self, model:Model): # If you choose to subclass this master class, you can do so before or after you create your own parameters. # If you do, you can also choose to call this method from you class, which will effectively add # and set all these parameters to your class. + + # noinspection SpellCheckingInspection self.drawdp = self.ParameterDict[self.drawdp.Name] = floatParameter( "Drawdown Parameter", DefaultValue=0.005, @@ -46,7 +48,7 @@ def __init__(self, model:Model): ToolTipText="specify the thermal drawdown for reservoir model 3 and 4" ) - model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f'Complete {__class__}: {sys._getframe().f_code.co_name}') def __str__(self): return 'SFReservoir' @@ -62,15 +64,15 @@ def read_parameters(self, model: Model) -> None: :type model: :class:`~geophires_x.Model.Model` :return: None """ - model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f'Init {__class__}: {sys._getframe().f_code.co_name}') super().read_parameters(model) # read the parameters for the parent. # if we call super, we don't need to deal with setting the parameters here, # just deal with the special cases for the variables in this class # because the call to the super.readparameters will set all the variables, # including the ones that are specific to this class - model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f'Complete {__class__}: {sys._getframe().f_code.co_name}') - def Calculate(self, model:Model): + def Calculate(self, model: Model): """ The Calculate function calculates the values of all the parameters that are calculated by this object. It calls the Calculate function of the parent object to calculate the values of the parameters that are @@ -80,7 +82,7 @@ def Calculate(self, model:Model): :type model: :class:`~geophires_x.Model.Model` :return: None """ - model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f'Init {__class__}: {sys._getframe().f_code.co_name}') super().Calculate(model) # run calculation for the parent. model.reserv.Tresoutput.value[0] = model.reserv.Trock.value @@ -92,4 +94,4 @@ def Calculate(self, model:Model): (model.reserv.Trock.value - model.wellbores.Tinj.value) +\ model.wellbores.Tinj.value - model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f'Complete {__class__}: {sys._getframe().f_code.co_name}') diff --git a/src/geophires_x/TDPReservoir.py b/src/geophires_x/TDPReservoir.py index 876a64675..69cc24ef0 100644 --- a/src/geophires_x/TDPReservoir.py +++ b/src/geophires_x/TDPReservoir.py @@ -1,9 +1,12 @@ import sys +import math +import numpy as np -from geophires_x.Parameter import floatParameter +from geophires_x.Parameter import floatParameter, listParameter import geophires_x.Model as Model from geophires_x.Reservoir import Reservoir from geophires_x.Units import DrawdownUnit, Units +from geophires_x.ParameterUtils import expand_schedule_dsl class TDPReservoir(Reservoir): @@ -35,11 +38,17 @@ def __init__(self, model: Model): # If you choose to subclass this master class, you can do so before or after you create your own parameters. # If you do, you can also choose to call this method from you class, which will effectively add # set all these parameters to your class. + + default_drawdown_parameter_val = 0.005 + drawdown_param_min = 0. + drawdown_param_max = 0.2 + + # noinspection SpellCheckingInspection self.drawdp = self.ParameterDict[self.drawdp.Name] = floatParameter( "Drawdown Parameter", - DefaultValue=0.005, - Min=0, - Max=0.2, + DefaultValue=default_drawdown_parameter_val, + Min=drawdown_param_min, + Max=drawdown_param_max, UnitType=Units.DRAWDOWN, PreferredUnits=DrawdownUnit.PERYEAR, CurrentUnits=DrawdownUnit.PERYEAR, @@ -47,6 +56,18 @@ def __init__(self, model: Model): ToolTipText="specify the thermal drawdown for reservoir model 3 and 4" ) + self.drawdown_parameter_schedule = self.ParameterDict['Drawdown Parameter Schedule'] = listParameter( + 'Drawdown Parameter Schedule', + DefaultValue=[default_drawdown_parameter_val], + Min=drawdown_param_min, + Max=drawdown_param_max, + UnitType=Units.DRAWDOWN, + PreferredUnits=DrawdownUnit.PERYEAR, + CurrentUnits=DrawdownUnit.PERYEAR, + auto_raise_exception_on_invalid_read=True, + ToolTipText='Thermal drawdown schedule for reservoir model 3 and 4 using DSL syntax', + ) + model.logger.info(f'Complete {__class__!s}: {sys._getframe().f_code.co_name}') def __str__(self): @@ -70,6 +91,14 @@ def read_parameters(self, model: Model) -> None: # because the call to the super.readparameters will set all the variables, # including the ones that are specific to this class + if self.drawdp.Provided and self.drawdown_parameter_schedule.Provided: + raise ValueError(f'Only one of {self.drawdp.Name} and ' + f'{self.drawdown_parameter_schedule.Name} may be provided.') + + if self.drawdown_parameter_schedule.Provided: + if len(self.drawdown_parameter_schedule.value) < 1: + raise ValueError(f'{self.drawdown_parameter_schedule.Name} must have at least one value.') + model.logger.info(f'Complete {__class__!s}: {sys._getframe().f_code.co_name}') def Calculate(self, model: Model): @@ -82,8 +111,24 @@ def Calculate(self, model: Model): model.logger.info(f'Init {__class__!s}: {sys._getframe().f_code.co_name}') super().Calculate(model) # run calculation for the parent. - model.reserv.Tresoutput.value = (1 - model.reserv.drawdp.value * model.reserv.timevector.value) * \ + max_time = max(model.reserv.timevector.value) if len(model.reserv.timevector.value) > 0 else 0.0 + max_years = max(1, math.ceil(max_time) + 1) + + if self.drawdown_parameter_schedule.Provided: + drawdp_schedule_expanded = expand_schedule_dsl( + model.reserv.drawdown_parameter_schedule.value, + max_years, + allow_schedule_length_to_exceed_total_years=True + ) + drawdp_vec = np.array([ + drawdp_schedule_expanded[min(math.floor(t), len(drawdp_schedule_expanded) - 1)] + for t in model.reserv.timevector.value + ]) + else: + drawdp_vec = model.reserv.drawdp.value + + model.reserv.Tresoutput.value = (1 - drawdp_vec * model.reserv.timevector.value) * \ (model.reserv.Trock.value - model.wellbores.Tinj.value) + \ model.wellbores.Tinj.value # this is no longer as in thesis (equation 4.16) - model.logger.info(f'Complete {__class__!s}: {sys._getframe().f_code.co_name}') + model.logger.info(f'Complete {__class__!s}: {sys._getframe().f_code.co_name}') \ No newline at end of file diff --git a/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index 4b27ac35f..4ecf7b4ea 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.13.10' +__version__ = '3.13.12' diff --git a/src/geophires_x_schema_generator/geophires-request.json b/src/geophires_x_schema_generator/geophires-request.json index 2c4284790..fbb4bee7c 100644 --- a/src/geophires_x_schema_generator/geophires-request.json +++ b/src/geophires_x_schema_generator/geophires-request.json @@ -421,6 +421,17 @@ "minimum": 0, "maximum": 0.2 }, + "Drawdown Parameter Schedule": { + "description": "Thermal drawdown schedule for reservoir model 3 and 4 using DSL syntax", + "type": "array", + "units": "1/year", + "category": "Reservoir", + "default": [ + 0.005 + ], + "minimum": 0.0, + "maximum": 0.2 + }, "Gringarten-Stehfest Precision": { "description": "Sets the numerical precision (decimal places) for the inverse Laplace transform (Stehfest algorithm) used in the Gringarten calculation for the Multiple Parallel Fractures Reservoir Model. The default value provides maximum result stability; lower values calculate faster but may reduce consistency.", "type": "integer", diff --git a/tests/examples/example4b_drawdown-schedule.out b/tests/examples/example4b_drawdown-schedule.out new file mode 100644 index 000000000..dd2c710bc --- /dev/null +++ b/tests/examples/example4b_drawdown-schedule.out @@ -0,0 +1,241 @@ + ***************** + ***CASE REPORT*** + ***************** + +Simulation Metadata +---------------------- + GEOPHIRES Version: 3.13.10 + Simulation Date: 2026-06-03 + Simulation Time: 07:26 + Calculation Time: 0.058 sec + + ***SUMMARY OF RESULTS*** + + End-Use Option: Electricity + Average Net Electricity Production: 7.13 MW + Electricity breakeven price: 10.35 cents/kWh + Number of production wells: 3 + Number of injection wells: 2 + Flowrate per production well: 110.0 kg/sec + Well depth: 2.0 kilometer + Geothermal gradient: 65 degC/km + + + ***ECONOMIC PARAMETERS*** + + Economic Model = BICYCLE + Accrued financing during construction: 0.00 % + Project lifetime: 30 yr + Capacity factor: 90.0 % + Project NPV: -32.73 MUSD + Project IRR: -1.85 % + Project VIR=PI=PIR: 0.36 + Project MOIC: -0.11 + Project Payback Period: N/A + Estimated Jobs Created: 18 + + ***ENGINEERING PARAMETERS*** + + Number of Production Wells: 3 + Number of Injection Wells: 2 + Well depth: 2.0 kilometer + Water loss rate: 0.0 % + Pump efficiency: 80.0 % + Injection temperature: 70.0 degC + User-provided production well temperature drop + Constant production well temperature drop: 0.0 degC + Flowrate per production well: 110.0 kg/sec + Injection well casing ID: 9.625 in + Production well casing ID: 9.625 in + Number of times redrilling: 0 + Power plant type: Subcritical ORC + + + ***RESOURCE CHARACTERISTICS*** + + Maximum reservoir temperature: 375.0 degC + Number of segments: 1 + Geothermal gradient: 65 degC/km + + + ***RESERVOIR PARAMETERS*** + + Reservoir Model = Annual Percentage Thermal Drawdown Model + Annual Thermal Drawdown: 0.500 1/year + Bottom-hole temperature: 145.00 degC + Warning: the reservoir dimensions and thermo-physical properties + listed below are default values if not provided by the user. + They are only used for calculating remaining heat content. + Reservoir volume provided as input + Reservoir volume: 1000000000 m**3 + Reservoir hydrostatic pressure: 19277.18 kPa + Plant outlet pressure: 691.43 kPa + Production wellhead pressure: 760.38 kPa + Productivity Index: 10.00 kg/sec/bar + Injectivity Index: 10.00 kg/sec/bar + Reservoir density: 2700.00 kg/m**3 + Reservoir heat capacity: 1050.00 J/kg/K + + + ***RESERVOIR SIMULATION RESULTS*** + + Maximum Production Temperature: 145.0 degC + Average Production Temperature: 140.0 degC + Minimum Production Temperature: 133.8 degC + Initial Production Temperature: 145.0 degC + Average Reservoir Heat Extraction: 96.47 MW + Wellbore Heat Transmission Model = Constant Temperature Drop: 0.0 degC + Average Injection Well Pump Pressure Drop: 1720.1 kPa + Average Production Well Pump Pressure Drop: 1339.9 kPa + + + ***CAPITAL COSTS (M$)*** + + Exploration costs: 3.21 MUSD + Drilling and completion costs: 13.06 MUSD + Drilling and completion costs per well: 2.61 MUSD + Stimulation costs: 0.00 MUSD + Surface power plant costs: 31.58 MUSD + Field gathering system costs: 3.69 MUSD + Total surface equipment costs: 35.26 MUSD + Total capital costs: 51.54 MUSD + + + ***OPERATING AND MAINTENANCE COSTS (M$/yr)*** + + Wellfield maintenance costs: 0.45 MUSD/yr + Power plant maintenance costs: 1.31 MUSD/yr + Water costs: 0.00 MUSD/yr + Total operating and maintenance costs: 1.76 MUSD/yr + + + ***SURFACE EQUIPMENT SIMULATION RESULTS*** + Initial geofluid availability: 0.10 MW/(kg/s) + Maximum Total Electricity Generation: 9.46 MW + Average Total Electricity Generation: 8.44 MW + Minimum Total Electricity Generation: 7.23 MW + Initial Total Electricity Generation: 9.46 MW + Maximum Net Electricity Generation: 8.18 MW + Average Net Electricity Generation: 7.13 MW + Minimum Net Electricity Generation: 5.87 MW + Initial Net Electricity Generation: 8.18 MW + Average Annual Total Electricity Generation: 66.47 GWh + Average Annual Net Electricity Generation: 56.14 GWh + Initial pumping power/net installed power: 15.60 % + Average Pumping Power: 1.31 MW + Heat to Power Conversion Efficiency: 7.37 % + + ************************************************************ + * HEATING, COOLING AND/OR ELECTRICITY PRODUCTION PROFILE * + ************************************************************ + YEAR THERMAL GEOFLUID PUMP NET FIRST LAW + DRAWDOWN TEMPERATURE POWER POWER EFFICIENCY + (degC) (MW) (MW) (%) + 1 1.0000 145.00 1.2759 8.1813 7.9137 + 2 1.0000 145.00 1.2759 8.1813 7.9137 + 3 1.0000 145.00 1.2759 8.1813 7.9137 + 4 1.0000 145.00 1.2759 8.1813 7.9137 + 5 1.0000 145.00 1.2759 8.1813 7.9137 + 6 1.0000 145.00 1.2759 8.1813 7.9137 + 7 1.0000 145.00 1.2759 8.1813 7.9137 + 8 1.0000 145.00 1.2759 8.1813 7.9137 + 9 1.0000 145.00 1.2759 8.1813 7.9137 + 10 1.0000 145.00 1.2759 8.1813 7.9137 + 11 0.9739 141.22 1.3018 7.3625 7.4998 + 12 0.9713 140.84 1.3044 7.2831 7.4585 + 13 0.9687 140.46 1.3069 7.2041 7.4172 + 14 0.9661 140.08 1.3094 7.1255 7.3759 + 15 0.9635 139.71 1.3120 7.0474 7.3347 + 16 0.9609 139.33 1.3145 6.9698 7.2934 + 17 0.9583 138.95 1.3170 6.8925 7.2522 + 18 0.9557 138.57 1.3195 6.8158 7.2109 + 19 0.9531 138.19 1.3221 6.7394 7.1697 + 20 0.9504 137.82 1.3246 6.6635 7.1284 + 21 0.9478 137.44 1.3271 6.5880 7.0872 + 22 0.9452 137.06 1.3295 6.5129 7.0460 + 23 0.9426 136.68 1.3320 6.4383 7.0047 + 24 0.9400 136.30 1.3345 6.3641 6.9635 + 25 0.9374 135.92 1.3370 6.2904 6.9223 + 26 0.9348 135.55 1.3395 6.2170 6.8810 + 27 0.9322 135.17 1.3419 6.1441 6.8398 + 28 0.9296 134.79 1.3444 6.0716 6.7986 + 29 0.9270 134.41 1.3468 5.9996 6.7573 + 30 0.9244 134.03 1.3493 5.9279 6.7161 + + + ******************************************************************* + * ANNUAL HEATING, COOLING AND/OR ELECTRICITY PRODUCTION PROFILE * + ******************************************************************* + YEAR ELECTRICITY HEAT RESERVOIR PERCENTAGE OF + PROVIDED EXTRACTED HEAT CONTENT TOTAL HEAT MINED + (GWh/year) (GWh/year) (10^15 J) (%) + 1 64.5 815.1 209.69 1.38 + 2 64.5 815.1 206.76 2.76 + 3 64.5 815.1 203.82 4.14 + 4 64.5 815.1 200.89 5.52 + 5 64.5 815.1 197.95 6.90 + 6 64.5 815.1 195.02 8.28 + 7 64.5 815.1 192.09 9.66 + 8 64.5 815.1 189.15 11.04 + 9 64.5 815.1 186.22 12.42 + 10 63.7 809.9 183.30 13.79 + 11 57.7 771.9 180.52 15.10 + 12 57.1 767.8 177.76 16.40 + 13 56.5 763.7 175.01 17.69 + 14 55.9 759.6 172.27 18.98 + 15 55.3 755.5 169.55 20.26 + 16 54.6 751.4 166.85 21.53 + 17 54.0 747.3 164.16 22.79 + 18 53.4 743.1 161.48 24.05 + 19 52.8 739.0 158.82 25.30 + 20 52.2 734.9 156.18 26.55 + 21 51.6 730.8 153.55 27.78 + 22 51.1 726.7 150.93 29.02 + 23 50.5 722.6 148.33 30.24 + 24 49.9 718.5 145.74 31.46 + 25 49.3 714.4 143.17 32.66 + 26 48.7 710.3 140.61 33.87 + 27 48.2 706.2 138.07 35.06 + 28 47.6 702.0 135.55 36.25 + 29 47.0 697.9 133.03 37.43 + 30 46.5 694.3 130.53 38.61 + + + ******************************** + * REVENUE & CASHFLOW PROFILE * + ******************************** +Year Electricity | Heat | Cooling | Carbon | Project +Since Price Ann. Rev. Cumm. Rev. | Price Ann. Rev. Cumm. Rev. | Price Ann. Rev. Cumm. Rev. | Price Ann. Rev. Cumm. Rev. | OPEX Net Rev. Net Cashflow +Start (cents/kWh)(MUSD/yr) (MUSD) |(cents/kWh) (MUSD/yr) (MUSD) |(cents/kWh) (MUSD/yr) (MUSD) |(USD/lb) (MUSD/yr) (MUSD) |(MUSD/yr) (MUSD/yr) (MUSD) +________________________________________________________________________________________________________________________________________________________________________________________ + 0 0.00 0.00 0.00 | 0.00 0.00 0.00 | 0.00 0.00 0.00 | 0.00 0.00 0.00 | 0.00 -51.54 -51.54 + 1 5.50 3.55 3.55 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.79 -49.75 + 2 5.50 3.55 7.10 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.79 -47.97 + 3 5.50 3.55 10.64 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.79 -46.18 + 4 5.50 3.55 14.19 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.79 -44.40 + 5 5.50 3.55 17.74 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.79 -42.61 + 6 5.50 3.55 21.29 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.79 -40.83 + 7 5.50 3.55 24.83 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.79 -39.04 + 8 5.50 3.55 28.38 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.79 -37.26 + 9 5.50 3.55 31.93 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.79 -35.47 + 10 5.50 3.50 35.43 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.74 -33.73 + 11 5.50 3.18 38.61 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.41 -32.32 + 12 5.50 3.14 41.75 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.38 -30.94 + 13 5.50 3.11 44.85 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.34 -29.60 + 14 5.50 3.07 47.93 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.31 -28.29 + 15 5.50 3.04 50.97 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.28 -27.01 + 16 5.50 3.01 53.97 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.24 -25.77 + 17 5.50 2.97 56.94 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.21 -24.56 + 18 5.50 2.94 59.88 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.18 -23.38 + 19 5.50 2.91 62.79 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.14 -22.24 + 20 5.50 2.87 65.66 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.11 -21.13 + 21 5.50 2.84 68.50 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.08 -20.05 + 22 5.50 2.81 71.31 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.05 -19.00 + 23 5.50 2.78 74.09 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 1.01 -17.99 + 24 5.50 2.74 76.83 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 0.98 -17.01 + 25 5.50 2.71 79.54 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 0.95 -16.06 + 26 5.50 2.68 82.22 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 0.92 -15.14 + 27 5.50 2.65 84.87 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 0.89 -14.25 + 28 5.50 2.62 87.49 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 0.85 -13.40 + 29 5.50 2.59 90.07 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 0.82 -12.58 + 30 5.50 2.56 92.63 | 2.50 0.00 0.00 | 2.50 0.00 0.00 | 0.00 0.00 0.00 | 1.76 0.80 -11.78 diff --git a/tests/examples/example4b_drawdown-schedule.txt b/tests/examples/example4b_drawdown-schedule.txt new file mode 100644 index 000000000..55dfb3986 --- /dev/null +++ b/tests/examples/example4b_drawdown-schedule.txt @@ -0,0 +1,69 @@ +# Example 4b: Geothermal Electricity Example Problem using Percentage Thermal Drawdown Schedule Model +# This example problem considers a simple hydrothermal reservoir +# at 2 km depth with an initial production temperature of 145deg.C. The thermal drawdown +# is assumed linear at 0.5%/year. The heat is converted to electricity with a subcritical ORC. + +# *** Subsurface technical parameters *** +# **************************************** +Reservoir Model,4, --- Percentage thermal drawdown model +Drawdown Parameter Schedule, 0*10,0.005, -- No drawdown for first 10 years, then 0.005/year +Reservoir Depth,2, --- [km] +Number of Segments,1, --- [-] +Gradient 1,65, --- [deg.C/km] +Number of Production Wells,3, --- [-] +Number of Injection Wells,2, --- [-] +Production Well Diameter,9.625, --- [inch] +Injection Well Diameter,9.625, --- [inch] +Ramey Production Wellbore Model,0, --- Should be 0 (disable) or 1 (enable) +Production Wellbore Temperature Drop,0, --- [deg.C] +Injection Wellbore Temperature Gain,0, --- [deg.C] +Production Flow Rate per Well,110, --- [kg/s] +Maximum Temperature,375, --- [deg.C] +Reservoir Volume Option,4, --- Should be 1 2 3 or 4. See manual for details. +Reservoir Volume,1e9, --- [m3] (required for reservoir volume option 3 and 4 +Water Loss Fraction,0.0, --- [-] (total geofluid lost)/(total geofluid produced) +Injectivity Index,10, --- [kg/s/bar] +Productivity Index,10, --- [kg/s/bar] +Injection Temperature,70, --- [deg.C] +Maximum Drawdown,1, --- [-] Maximum allowable drawdown before redrilling (a value of 0.1 means redrilling of 10% drawdown) +Reservoir Heat Capacity,1050, --- [J/kg/K] +Reservoir Density,2700, --- [kg/m3] +Reservoir Thermal Conductivity,3, --- [W/m/K] + + +# *** Surface technical parameters *** +# ************************************ +End-Use Option,1, --- [-] Electricity +Power Plant Type,1, --- [1] Subcritical ORC +Circulation Pump Efficiency,0.8, --- [-] +Utilization Factor,0.9, --- [-] +Surface Temperature,15, --- [deg.C] +Ambient Temperature,15, --- [deg.C] + +# *** Economic/Financial Parameters *** +# ************************************* +Plant Lifetime,30, --- [years] +Economic Model,3, --- Should be 1 (FCR model) 2 (Standard LCOE/LCOH model) or 3 (Bicycle model). +Fraction of Investment in Bonds,0.65, --- [-] Required if Bicycle model is selected. See manual for details. +Inflated Bond Interest Rate,0.07, --- [-] Required if Bicycle model is selected. See manual for details. +Inflated Equity Interest Rate,0.12, --- [-] Required if Bicycle model is selected. See manual for details. +Inflation Rate,0.025, --- [-] Required if Bicycle model is selected. See manual for details. +Combined Income Tax Rate,0.392, --- [-] Required if Bicycle model is selected. See manual for details. +Gross Revenue Tax Rate,0, --- [-] Required if Bicycle model is selected. See manual for details. +Investment Tax Credit Rate,0, --- [-] Required if Bicycle model is selected. See manual for details. +Property Tax Rate,0, --- [-] Required if Bicycle model is selected. See manual for details. +Inflation Rate During Construction,0, --- [-] +Well Drilling and Completion Capital Cost Adjustment Factor,1, --- [-] Use built-in well cost correlation as is +Well Drilling Cost Correlation,1, --- [-] Use built-in well drilling cost correlation #1 +Reservoir Stimulation Capital Cost,0, --- [M$/injection well] Reservoir stimulation capital cost per injection well +Surface Plant Capital Cost Adjustment Factor,1, --- [-] Use built-in surface plant cost correlation as is +Field Gathering System Capital Cost Adjustment Factor,1, --- [-] Use built-in pipeline distribution cost correlation as is +Exploration Capital Cost Adjustment Factor,1, --- [-] Use built-in exploration cost correlation as is +Wellfield O&M Cost Adjustment Factor,1, --- [-] Use built-in wellfield O&M cost correlation as is +Water Cost Adjustment Factor,1, --- [-] Use built-in water cost correlation as is +Surface Plant O&M Cost Adjustment Factor,1, --- [-] Use built-in surface plant O&M cost correlation as is + + +# *** Simulation Parameters *** +Print Output to Console,1, --- [-] Should be 0 (don't print results to console) or 1 (print results to console) +Time steps per year,4, --- [1/year] diff --git a/tests/geophires_x_tests/test_economics_utils.py b/tests/geophires_x_tests/test_parameter_utils.py similarity index 58% rename from tests/geophires_x_tests/test_economics_utils.py rename to tests/geophires_x_tests/test_parameter_utils.py index 0e4931292..bdbb8cf5c 100644 --- a/tests/geophires_x_tests/test_economics_utils.py +++ b/tests/geophires_x_tests/test_parameter_utils.py @@ -1,10 +1,10 @@ from __future__ import annotations -from geophires_x.EconomicsUtils import expand_schedule_dsl +from geophires_x.ParameterUtils import expand_schedule_dsl from tests.base_test_case import BaseTestCase -class EconomicsUtilsTestCase(BaseTestCase): +class ParameterUtilsTestCase(BaseTestCase): def test_expand_schedule_dsl(self): total_years = 25 @@ -17,3 +17,13 @@ def test_expand_schedule_dsl(self): with self.assertRaises(ValueError) as ve: expand_schedule_dsl(invalid_case, total_years) self.assertIn('Invalid', str(ve)) + + with self.assertRaises(ValueError) as ve: + expand_schedule_dsl(['0.03', '0.03 * 9', '0.04 * 10', '0.05'], 3) + + self.assertEqual( + [0.03] * 3, + expand_schedule_dsl( + ['0.03', '0.03 * 9', '0.04 * 10', '0.05'], 3, allow_schedule_length_to_exceed_total_years=True + ), + ) diff --git a/tests/geophires_x_tests/test_reservoir.py b/tests/geophires_x_tests/test_reservoir.py index a5923fa81..8b0c50e46 100644 --- a/tests/geophires_x_tests/test_reservoir.py +++ b/tests/geophires_x_tests/test_reservoir.py @@ -357,3 +357,15 @@ def _del_metadata(r: GeophiresXResult) -> GeophiresXResult: ) except AssertionError as ae: self._handle_assert_logs_failure(ae) + + def test_drawdown_parameter_schedule(self) -> None: + default_result: GeophiresXResult = GeophiresXResult(self._get_test_file_path('../examples/example4.out')) + schedule_result: GeophiresXResult = GeophiresXResult( + self._get_test_file_path('../examples/example4b_drawdown-schedule.out') + ) + + def _net_prod(r: GeophiresXResult) -> float: + return r.result['SUMMARY OF RESULTS']['Average Net Electricity Production']['value'] + + # Schedule version has no drawdown for 10 years + self.assertGreater(_net_prod(schedule_result), _net_prod(default_result))