diff --git a/.bumpversion.cfg b/.bumpversion.cfg index abe21e9a..2f2c0523 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.7.3 +current_version = 3.7.5 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index 8d23c40b..363e0c23 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.7.3 + version: 3.7.5 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 418a4002..494f8355 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,8 @@ GEOPHIRES-X (2023-2025) 3.7.3: Heat to Power Conversion Efficiency output added. +3.7.5: Injection Well Drilling and Completion Capital Cost Adjustment Factor, if not provided, is now automatically set to the provided value of Well Drilling and Completion Capital Cost Adjustment Factor. + 3.6 ^^^ diff --git a/README.rst b/README.rst index 16b643f5..76623fa9 100644 --- a/README.rst +++ b/README.rst @@ -56,9 +56,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.7.3.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.7.5.svg :alt: Commits since latest release - :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.7.3...main + :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.7.5...main .. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat :target: https://nrel.github.io/GEOPHIRES-X diff --git a/docs/conf.py b/docs/conf.py index 1bc78af7..ec55fbf7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ year = '2025' author = 'NREL' copyright = f'{year}, {author}' -version = release = '3.7.3' +version = release = '3.7.5' pygments_style = 'trac' templates_path = ['./templates'] diff --git a/setup.py b/setup.py index a6e3821b..a8be14de 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='geophires-x', - version='3.7.3', + version='3.7.5', 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 7f1da0f9..adffbf6c 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -647,6 +647,10 @@ def __init__(self, model: Model): Valid=False, ToolTipText="Injection Well Drilling and Completion Capital Cost" ) + + # TODO parameterize/document default 5% indirect cost factor that is applied when neither of the well + # drilling/completion capital cost adjustment factors are provided + injection_well_cost_adjustment_factor_name = "Injection Well Drilling and Completion Capital Cost Adjustment Factor" self.production_well_cost_adjustment_factor = self.ParameterDict[self.production_well_cost_adjustment_factor.Name] = floatParameter( "Well Drilling and Completion Capital Cost Adjustment Factor", DefaultValue=1.0, @@ -657,19 +661,23 @@ def __init__(self, model: Model): CurrentUnits=PercentUnit.TENTH, Provided=False, Valid=True, - ToolTipText="Well Drilling and Completion Capital Cost Adjustment Factor" + ToolTipText="Well Drilling and Completion Capital Cost Adjustment Factor. Applies to production wells; " + f"also applies to injection wells unless a value is provided for " + f"{injection_well_cost_adjustment_factor_name}." ) self.injection_well_cost_adjustment_factor = self.ParameterDict[self.injection_well_cost_adjustment_factor.Name] = floatParameter( - "Injection Well Drilling and Completion Capital Cost Adjustment Factor", - DefaultValue=self.production_well_cost_adjustment_factor.value, - Min=0, - Max=10, + injection_well_cost_adjustment_factor_name, + DefaultValue=self.production_well_cost_adjustment_factor.DefaultValue, + Min=self.production_well_cost_adjustment_factor.Min, + Max=self.production_well_cost_adjustment_factor.Max, UnitType=Units.PERCENT, PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH, Provided=False, Valid=True, - ToolTipText="Injection Well Drilling and Completion Capital Cost Adjustment Factor" + ToolTipText="Injection Well Drilling and Completion Capital Cost Adjustment Factor. " + f"If not provided, this value will be set automatically to the same value as " + f"{self.production_well_cost_adjustment_factor.Name}." ) self.oamwellfixed = self.ParameterDict[self.oamwellfixed.Name] = floatParameter( "Wellfield O&M Cost", @@ -1753,7 +1761,8 @@ def __init__(self, model: Model): CurrentUnits=PercentUnit.TENTH ) self.ProjectMOIC = self.OutputParameterDict[self.ProjectMOIC.Name] = OutputParameter( - "Project Multiple of Invested Capital", + "Project MOIC", + ToolTipText="Project Multiple of Invested Capital", UnitType=Units.PERCENT, PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH @@ -1910,35 +1919,30 @@ def read_parameters(self, model: Model) -> None: ParameterToModify.value = 1.0 elif ParameterToModify.Name == "Well Drilling and Completion Capital Cost Adjustment Factor": if self.per_production_well_cost.Valid and ParameterToModify.Valid: - print("Warning: Provided well drilling and completion cost adjustment factor not" + - " considered because valid total well drilling and completion cost provided.") - model.logger.warning("Provided well drilling and completion cost adjustment factor not" + - " considered because valid total well drilling and completion cost provided.") + msg = ('Provided well drilling and completion cost adjustment factor not considered ' + 'because valid total well drilling and completion cost provided.') + print(f'Warning: {msg}') + model.logger.warning(msg) elif not self.per_production_well_cost.Provided and not self.production_well_cost_adjustment_factor.Provided: ParameterToModify.value = 1.0 - print("Warning: No valid well drilling and completion total cost or adjustment" + - " factor provided. GEOPHIRES will assume default built-in well drilling and" + - " completion cost correlation with adjustment factor = 1.") - model.logger.warning( - "No valid well drilling and completion total cost or adjustment factor" + - " provided. GEOPHIRES will assume default built-in well drilling and completion cost" + - " correlation with adjustment factor = 1.") + msg = ("No valid well drilling and completion total cost or adjustment factor provided. " + "GEOPHIRES will assume default built-in well drilling and completion cost " + "correlation with adjustment factor = 1.") + print(f'Warning: {msg}') + model.logger.warning(msg) elif self.per_production_well_cost.Provided and not self.per_production_well_cost.Valid: - print("Warning: Provided well drilling and completion cost outside of range 0-1000." + - " GEOPHIRES will assume default built-in well drilling and completion cost correlation" + - " with adjustment factor = 1.") - model.logger.warning("Provided well drilling and completion cost outside of range 0-1000." + - " GEOPHIRES will assume default built-in well drilling and completion cost correlation with" + - " adjustment factor = 1.") + msg = ("Provided well drilling and completion cost outside of range 0-1000. GEOPHIRES " + "will assume default built-in well drilling and completion cost correlation " + "with adjustment factor = 1.") + print(f'Warning: {msg}') + model.logger.warning(msg) self.production_well_cost_adjustment_factor.value = 1.0 elif not self.per_production_well_cost.Provided and self.production_well_cost_adjustment_factor.Provided and not self.production_well_cost_adjustment_factor.Valid: - print("Warning: Provided well drilling and completion cost adjustment factor outside" + - " of range 0-10. GEOPHIRES will assume default built-in well drilling and completion" + - " cost correlation with adjustment factor = 1.") - model.logger.warning( - "Provided well drilling and completion cost adjustment factor outside" + - " of range 0-10. GEOPHIRES will assume default built-in well drilling and completion" + - " cost correlation with adjustment factor = 1.") + msg = ("Provided well drilling and completion cost adjustment factor outside of range " + "0-10. GEOPHIRES will assume default built-in well drilling and completion cost " + "correlation with adjustment factor = 1.") + print(f'Warning: {msg}') + model.logger.warning(msg) self.production_well_cost_adjustment_factor.value = 1.0 elif ParameterToModify.Name == "Wellfield O&M Cost Adjustment Factor": if self.oamtotalfixed.Valid: @@ -2169,6 +2173,7 @@ def read_parameters(self, model: Model) -> None: coerce_int_params_to_enum_values(self.ParameterDict) self.sync_interest_rate(model) + self.sync_well_drilling_and_completion_capital_cost_adjustment_factor(model) model.logger.info(f'complete {__class__!s}: {sys._getframe().f_code.co_name}') @@ -2198,6 +2203,16 @@ def discount_rate_display() -> str: self.interest_rate.value = self.discountrate.quantity().to(convertible_unit(self.interest_rate.CurrentUnits)).magnitude + def sync_well_drilling_and_completion_capital_cost_adjustment_factor(self, model): + if (self.production_well_cost_adjustment_factor.Provided + and not self.injection_well_cost_adjustment_factor.Provided): + factor = self.production_well_cost_adjustment_factor.value + self.injection_well_cost_adjustment_factor.value = factor + model.logger.info( + f'Set {self.injection_well_cost_adjustment_factor.Name} to {factor} because ' + f'{self.production_well_cost_adjustment_factor.Name} was provided.') + + def Calculate(self, model: Model) -> None: """ The Calculate function is where all the calculations are done. @@ -2281,7 +2296,7 @@ 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 + # 1.05 for 5% indirect costs - see TODO re:parameterizing at src/geophires_x/Economics.py:652 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) diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index 1a5ce3af..ff4e9055 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -1646,7 +1646,7 @@ def PrintOutputs(self, model: Model): f.write(f' Project NPV: {model.economics.ProjectNPV.value:10.2f} ' + model.economics.ProjectNPV.PreferredUnits.value + NL) f.write(f' Project IRR: {model.economics.ProjectIRR.value:10.2f} ' + model.economics.ProjectIRR.PreferredUnits.value + NL) f.write(f' Project VIR=PI=PIR: {model.economics.ProjectVIR.value:10.2f}' + NL) - f.write(f' Project MOIC: {model.economics.ProjectMOIC.value:10.2f}' + NL) + f.write(f' {model.economics.ProjectMOIC.Name}: {model.economics.ProjectMOIC.value:10.2f}' + NL) payback_period_val = model.economics.ProjectPaybackPeriod.value project_payback_period_display = f'{payback_period_val:10.2f} {model.economics.ProjectPaybackPeriod.PreferredUnits.value}' \ diff --git a/src/geophires_x/OutputsCCUS.py b/src/geophires_x/OutputsCCUS.py index e187c5a4..60935240 100644 --- a/src/geophires_x/OutputsCCUS.py +++ b/src/geophires_x/OutputsCCUS.py @@ -36,7 +36,7 @@ def PrintOutputs(self, model): f.write(f" Project NPV (including carbon credit): {model.ccuseconomics.ProjectNPV.value:10.2f} " + model.ccuseconomics.ProjectNPV.PreferredUnits.value + NL) f.write(f" Project IRR (including carbon credit): {model.ccuseconomics.ProjectIRR.value:10.2f} " + model.ccuseconomics.ProjectIRR.PreferredUnits.value + NL) f.write(f" Project VIR=IR=PIR (including carbon credit): {model.ccuseconomics.ProjectVIR.value:10.2f}" + NL) - f.write(f" Project MOIC (including carbon credit): {model.ccuseconomics.ProjectMOIC.value:10.2f}" + NL) + f.write(f" {model.economics.ProjectMOIC.Name} (including carbon credit): {model.ccuseconomics.ProjectMOIC.value:10.2f}" + NL) f.write(NL) f.write(NL) f.write(" ******************" + NL) diff --git a/src/geophires_x/Reservoir.py b/src/geophires_x/Reservoir.py index 8c33b4e7..841e4546 100644 --- a/src/geophires_x/Reservoir.py +++ b/src/geophires_x/Reservoir.py @@ -404,8 +404,8 @@ def __init__(self, model: Model): CurrentUnits=TemperatureUnit.CELSIUS, Required=True, ErrMessage="assume default surface temperature (15 deg.C)", - ToolTipText="Surface temperature used for calculating bottom-hole temperature \ - (with geothermal gradient and reservoir depth)" + ToolTipText="Surface temperature used for calculating bottom-hole temperature " + "(with geothermal gradient and reservoir depth)" ) self.usebuiltintough2model = False diff --git a/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index 87c08592..68a57138 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.7.3' +__version__ = '3.7.5' diff --git a/src/geophires_x_client/geophires_x_result.py b/src/geophires_x_client/geophires_x_result.py index 9b547f9e..21a21195 100644 --- a/src/geophires_x_client/geophires_x_result.py +++ b/src/geophires_x_client/geophires_x_result.py @@ -659,6 +659,9 @@ def _get_data_from_profile_lines(self, profile_lines): return data def _parse_number(self, number_str, field='string') -> int | float: + if number_str == 'N/A': + return None + try: if '.' in number_str: # TODO should probably ideally use decimal.Decimal to preserve precision, diff --git a/src/geophires_x_schema_generator/__init__.py b/src/geophires_x_schema_generator/__init__.py index 12707379..d83b4a63 100644 --- a/src/geophires_x_schema_generator/__init__.py +++ b/src/geophires_x_schema_generator/__init__.py @@ -34,7 +34,8 @@ class GeophiresXSchemaGenerator: def __init__(self): pass - def _get_dummy_model(self): + @staticmethod + def _get_dummy_model(): stash_cwd = Path.cwd() stash_sys_argv = sys.argv sys.argv = [''] @@ -146,9 +147,9 @@ def generate_parameters_reference_rst(self) -> str: input_params_by_category[category][input_param_name] = input_param - def get_input_params_table(category_params, category_name) -> str: + def get_input_params_table(_category_params, category_name) -> str: category_display = category_name if category_name is not None else '' - input_rst = f""" + _input_rst = f""" {category_display} {'-' * len(category_display)} .. list-table:: {category_display}{' ' if len(category_display) > 0 else ''}Parameters @@ -162,7 +163,7 @@ def get_input_params_table(category_params, category_name) -> str: - Min - Max""" - for param_name in category_params: + for param_name in _category_params: param = input_params[param_name] # if param['Required']: @@ -172,7 +173,7 @@ def get_input_params_table(category_params, category_name) -> str: min_val, max_val = _get_min_and_max(param) - input_rst += f"""\n * - {param['Name']} + _input_rst += f"""\n * - {param['Name']} - {_get_key(param, 'ToolTipText')} - {_get_key(param, 'PreferredUnits')} - {_get_key(param, 'json_parameter_type')} @@ -180,7 +181,7 @@ def get_input_params_table(category_params, category_name) -> str: - {min_val} - {max_val}""" - return input_rst + return _input_rst input_rst = '' for category, category_params in input_params_by_category.items(): @@ -204,7 +205,8 @@ def get_input_params_table(category_params, category_name) -> str: return rst - def get_output_params_table_rst(self, output_params_json) -> str: + @staticmethod + def get_output_params_table_rst(output_params_json) -> str: output_params = json.loads(output_params_json) output_rst = """ diff --git a/src/geophires_x_schema_generator/geophires-request.json b/src/geophires_x_schema_generator/geophires-request.json index 395fa8d6..f660ae1f 100644 --- a/src/geophires_x_schema_generator/geophires-request.json +++ b/src/geophires_x_schema_generator/geophires-request.json @@ -396,7 +396,7 @@ "maximum": 0.99 }, "Surface Temperature": { - "description": "Surface temperature used for calculating bottom-hole temperature (with geothermal gradient and reservoir depth)", + "description": "Surface temperature used for calculating bottom-hole temperature (with geothermal gradient and reservoir depth)", "type": "number", "units": "degC", "category": "Reservoir", @@ -1392,7 +1392,7 @@ "maximum": 200 }, "Well Drilling and Completion Capital Cost Adjustment Factor": { - "description": "Well Drilling and Completion Capital Cost Adjustment Factor", + "description": "Well Drilling and Completion Capital Cost Adjustment Factor. Applies to production wells; also applies to injection wells unless a value is provided for Injection Well Drilling and Completion Capital Cost Adjustment Factor.", "type": "number", "units": "", "category": "Economics", @@ -1401,7 +1401,7 @@ "maximum": 10 }, "Injection Well Drilling and Completion Capital Cost Adjustment Factor": { - "description": "Injection Well Drilling and Completion Capital Cost Adjustment Factor", + "description": "Injection Well Drilling and Completion Capital Cost Adjustment Factor. If not provided, this value will be set automatically to the same value as Well Drilling and Completion Capital Cost Adjustment Factor.", "type": "number", "units": "", "category": "Economics", diff --git a/tests/geophires_x_tests/drilling-adjustment-factor.txt b/tests/geophires_x_tests/drilling-adjustment-factor.txt new file mode 100644 index 00000000..80f50240 --- /dev/null +++ b/tests/geophires_x_tests/drilling-adjustment-factor.txt @@ -0,0 +1,62 @@ +Reservoir Model, 1 +Reservoir Volume Option, 1 +Reservoir Density, 2800 +Reservoir Heat Capacity, 790 +Reservoir Thermal Conductivity, 3.05 +Reservoir Porosity, 0.0118 +Reservoir Impedance, 0.001 + +Number of Fractures, 149 +Fracture Shape, 4 +Fracture Height, 2000 +Fracture Width, 10000 +Fracture Separation, 30 + +Number of Segments, 1 + +Production Well Diameter, 7 +Injection Well Diameter, 7 +Well Separation, 365 feet +Injection Temperature, 60 degC +Injection Wellbore Temperature Gain, 3 +Plant Outlet Pressure, 1000 psi +Ramey Production Wellbore Model, 1 +Utilization Factor, .9 +Water Loss Fraction, 0.05 +Maximum Drawdown, 1 +Ambient Temperature, 10 degC +#Surface Temperature, 10 degC +End-Use Option, 1 + +Plant Lifetime, 25 + +Circulation Pump Efficiency, 0.80 + +Economic Model, 3 +Starting Electricity Sale Price, 0.15 +Ending Electricity Sale Price, 1.00 +Electricity Escalation Rate Per Year, 0.004053223 +Electricity Escalation Start Year, 1 +Fraction of Investment in Bonds, .5 +Combined Income Tax Rate, .3 +Gross Revenue Tax Rate, 0 +Inflated Bond Interest Rate, .05 +Inflated Equity Interest Rate, .08 +Inflation Rate, .02 +Investment Tax Credit Rate, .3, -- https://programs.dsireusa.org/system/program/detail/658 +Production Tax Credit Electricity, 0.0275, -- https://programs.dsireusa.org/system/program/detail/734 +Inflation Rate During Construction, 0.05 +Property Tax Rate, 0 +Time steps per year, 10 +Maximum Temperature, 500 + + +Print Output to Console, 0 +Surface Temperature, 12 +Reservoir Depth, 5.4 +Gradient 1, 36.7 +Power Plant Type, 4 + +Number of Injection Wells, 54 +Number of Production Wells, 54 +Production Flow Rate per Well, 80 diff --git a/tests/test_geophires_x.py b/tests/test_geophires_x.py index 75a4ae6a..adec0984 100644 --- a/tests/test_geophires_x.py +++ b/tests/test_geophires_x.py @@ -595,3 +595,56 @@ def test_transmission_pipeline_cost(self): self.assertAlmostEqual( result.result['CAPITAL COSTS (M$)']['Transmission pipeline cost']['value'], 3.75, delta=0.5 ) + + def test_well_drilling_and_completion_capital_cost_adjustment_factor(self): + base_file = self._get_test_file_path('geophires_x_tests/drilling-adjustment-factor.txt') + r_no_adj = GeophiresXClient().get_geophires_result(GeophiresInputParameters(from_file_path=base_file)) + + r_noop_adj = GeophiresXClient().get_geophires_result( + GeophiresInputParameters( + from_file_path=base_file, + params={'Well Drilling and Completion Capital Cost Adjustment Factor': 1.0}, + ) + ) + + r_adj = GeophiresXClient().get_geophires_result( + GeophiresInputParameters( + from_file_path=base_file, + params={'Well Drilling and Completion Capital Cost Adjustment Factor': 1.175}, + ) + ) + + def c_well(r, prod: bool = False, inj: bool = False): + well_type = 'production ' if prod else 'injection ' if inj else '' + try: + c = r.result['CAPITAL COSTS (M$)'][f'Drilling and completion costs per {well_type}well']['value'] + + if not prod and not inj: + # indirect cost is not applied to prod/inj-specific per-well cost; + # see TODO re:parameterizing at src/geophires_x/Economics.py:652 + default_indirect_cost_factor = 1.05 + c = c / default_indirect_cost_factor + + return c + except TypeError: + return None + + self.assertEqual(c_well(r_no_adj), c_well(r_noop_adj)) + + self.assertAlmostEqual(1.175 * c_well(r_no_adj), c_well(r_adj), delta=0.1) + + r_adj_diff_prod_inj = GeophiresXClient().get_geophires_result( + GeophiresInputParameters( + from_file_path=base_file, + params={ + 'Well Drilling and Completion Capital Cost Adjustment Factor': 1.175, + 'Injection Well Drilling and Completion Capital Cost Adjustment Factor': 3, + }, + ) + ) + + c_well_no_adj = c_well(r_no_adj) + c_prod_well_adj = c_well(r_adj_diff_prod_inj, prod=True) + c_inj_well_adj = c_well(r_adj_diff_prod_inj, inj=True) + self.assertAlmostEqual(1.175 * c_well_no_adj, c_prod_well_adj, delta=0.1) + self.assertAlmostEqual(3 * c_well_no_adj, c_inj_well_adj, delta=0.1)