diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 6cf7bacd..a0f79492 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.9.36 +current_version = 3.9.40 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index 1cbbc98b..00ae6670 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.9.36 + version: 3.9.40 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" diff --git a/README.rst b/README.rst index 1f10631e..2eb8ba44 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.9.36.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.9.40.svg :alt: Commits since latest release - :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.36...main + :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.40...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 3d8e4309..b2fdba14 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ year = '2025' author = 'NREL' copyright = f'{year}, {author}' -version = release = '3.9.36' +version = release = '3.9.40' pygments_style = 'trac' templates_path = ['./templates'] diff --git a/setup.py b/setup.py index e7f55c66..1e04355c 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='geophires-x', - version='3.9.36', + version='3.9.40', 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 15b92426..afa2acfe 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -868,7 +868,8 @@ def __init__(self, model: Model): "Total Capital Cost", DefaultValue=-1.0, Min=0, - Max=1000, + # pint treats GUSD as billions of dollars (G for giga) + Max=quantity(100, 'GUSD').to('MUSD').magnitude, UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS, @@ -1775,6 +1776,18 @@ def __init__(self, model: Model): UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR + # TODO TooltipText to document how this is calculated + ) + self.redrilling_annual_cost = self.OutputParameterDict[self.redrilling_annual_cost.Name] = OutputParameter( + Name="Redrilling costs", + UnitType=Units.CURRENCYFREQUENCY, + PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, + CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, + ToolTipText=f'Total redrilling costs over the {model.surfaceplant.plant_lifetime.Name} are calculated as ' + f'({self.Cwell.display_name} + {self.Cstim.display_name}) ' + f'× {model.wellbores.redrill.display_name}. ' + f'The total is then divided over {model.surfaceplant.plant_lifetime.Name} years to calculate ' + f'Redrilling costs per year.' ) self.Cplant = self.OutputParameterDict[self.Cplant.Name] = OutputParameter( Name="Surface Plant cost", @@ -1809,7 +1822,7 @@ def __init__(self, model: Model): UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, - ToolTipText='Assumes $3.5/1,000 gallons of water' + ToolTipText='Assumes $3.5/1,000 gallons of water' # TODO parameterize ) self.CCap = self.OutputParameterDict[self.CCap.Name] = OutputParameter( Name="Total Capital Cost", @@ -2521,8 +2534,10 @@ def Calculate(self, model: Model) -> None: if model.wellbores.redrill.value > 0: # account for well redrilling - self.Coam.value = self.Coam.value + \ - (self.Cwell.value + self.Cstim.value) * model.wellbores.redrill.value / model.surfaceplant.plant_lifetime.value + redrilling_costs: PlainQuantity = self.calculate_redrilling_costs(model) + self.redrilling_annual_cost.value = redrilling_costs.to(self.redrilling_annual_cost.CurrentUnits).magnitude + self.Coam.value += redrilling_costs.to(self.Coam.CurrentUnits).magnitude + # Add in the AnnualLicenseEtc and TaxRelief self.Coam.value = self.Coam.value + self.AnnualLicenseEtc.value - self.TaxRelief.value @@ -2751,6 +2766,11 @@ def calculate_stimulation_costs(self, model: Model) -> PlainQuantity: return quantity(stimulation_costs, self.Cstim.CurrentUnits) + def calculate_redrilling_costs(self, model) -> PlainQuantity: + return ((self.Cwell.quantity() + self.Cstim.quantity()) + * model.wellbores.redrill.quantity() + / model.surfaceplant.plant_lifetime.quantity()) + def calculate_field_gathering_costs(self, model: Model) -> None: if self.ccgathfixed.Valid: self.Cgath.value = self.ccgathfixed.value @@ -3164,5 +3184,7 @@ def _calculate_derived_outputs(self, model: Model) -> None: (model.wellbores.nprod.value + model.wellbores.ninj.value) ) + + def __str__(self): return "Economics" diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index d1c45300..f6bc8819 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -325,7 +325,7 @@ def PrintOutputs(self, model: Model): f.write(f' Flowrate per production well: {model.wellbores.prodwellflowrate.value:10.1f} ' + model.wellbores.prodwellflowrate.CurrentUnits.value + NL) f.write(f' Injection well casing ID: {model.wellbores.injwelldiam.value:10.3f} ' + model.wellbores.injwelldiam.CurrentUnits.value + NL) f.write(f' Production well casing ID: {model.wellbores.prodwelldiam.value:10.3f} ' + model.wellbores.prodwelldiam.CurrentUnits.value + NL) - f.write(f' Number of times redrilling: {model.wellbores.redrill.value:10.0f}'+NL) + f.write(f' {model.wellbores.redrill.display_name}: {model.wellbores.redrill.value:10.0f}\n') if model.surfaceplant.enduse_option.value in [EndUseOptions.ELECTRICITY, EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: f.write(' Power plant type: ' + str(model.surfaceplant.plant_type.value.value) + NL) f.write(NL) @@ -475,12 +475,23 @@ def PrintOutputs(self, model: Model): f.write(f' District Heating System Cost: {model.economics.dhdistrictcost.value:10.2f} {model.economics.dhdistrictcost.CurrentUnits.value}\n') f.write(f' Total surface equipment costs: {(model.economics.Cplant.value+model.economics.Cgath.value):10.2f} ' + model.economics.Cplant.CurrentUnits.value + NL) f.write(f' {model.economics.Cexpl.display_name}: {model.economics.Cexpl.value:10.2f} {model.economics.Cexpl.CurrentUnits.value}\n') + if model.economics.totalcapcost.Valid and model.wellbores.redrill.value > 0: - f.write(f' Drilling and completion costs (for redrilling):{model.economics.Cwell.value:10.2f} ' + model.economics.Cwell.CurrentUnits.value + NL) - f.write(f' Drilling and completion costs per redrilled well: {(model.economics.Cwell.value/(model.wellbores.nprod.value+model.wellbores.ninj.value)):10.2f} ' + model.economics.Cwell.CurrentUnits.value + NL) + f.write(f' Drilling and completion costs (for redrilling):{econ.Cwell.value:10.2f} {econ.Cwell.CurrentUnits.value}\n') + f.write(f' Drilling and completion costs per redrilled well: {(econ.Cwell.value/(model.wellbores.nprod.value+model.wellbores.ninj.value)):10.2f} {econ.Cwell.CurrentUnits.value}\n') f.write(f' Stimulation costs (for redrilling): {econ.Cstim.value:10.2f} {econ.Cstim.CurrentUnits.value}\n') + if model.economics.RITCValue.value: - f.write(f' {model.economics.RITCValue.display_name}: {-1*model.economics.RITCValue.value:10.2f} {model.economics.RITCValue.CurrentUnits.value}\n') + if model.economics.econmodel.value != EconomicModel.SAM_SINGLE_OWNER_PPA: + f.write(f' {model.economics.RITCValue.display_name}: {-1*model.economics.RITCValue.value:10.2f} {model.economics.RITCValue.CurrentUnits.value}\n') + else: + # TODO Extract value from SAM Cash Flow Profile per + # https://github.com/NREL/GEOPHIRES-X/issues/404. + # For now we skip displaying the value because it can be/probably is usually mathematically + # inaccurate, and even if it's not, it's redundant with the cash flow profile and also + # misleading/confusing/wrong to display it as a capital cost since it is not a capital + # expenditure. + pass capex_label = Outputs._field_label(econ.CCap.display_name, 50) f.write(f' {capex_label}{econ.CCap.value:10.2f} {econ.CCap.CurrentUnits.value}\n') @@ -506,6 +517,10 @@ def PrintOutputs(self, model: Model): f.write(f' Annual District Heating O&M Cost: {model.economics.dhdistrictoandmcost.value:10.2f} {model.economics.dhdistrictoandmcost.CurrentUnits.value}\n') f.write(f' Average Annual Peaking Fuel Cost: {model.economics.averageannualngcost.value:10.2f} {model.economics.averageannualngcost.CurrentUnits.value}\n') + if model.wellbores.redrill.value > 0: + redrill_label = Outputs._field_label(econ.redrilling_annual_cost.display_name, 47) + f.write(f' {redrill_label}{econ.redrilling_annual_cost.value:10.2f} {econ.redrilling_annual_cost.CurrentUnits.value}\n') + f.write(f' {econ.Coam.display_name}: {(econ.Coam.value + econ.averageannualpumpingcosts.value + econ.averageannualheatpumpelectricitycost.value):10.2f} {econ.Coam.CurrentUnits.value}\n') else: f.write(f' {econ.Coam.display_name}: {econ.Coam.value:10.2f} {econ.Coam.CurrentUnits.value}\n') diff --git a/src/geophires_x/WellBores.py b/src/geophires_x/WellBores.py index af33257b..dca1eb52 100644 --- a/src/geophires_x/WellBores.py +++ b/src/geophires_x/WellBores.py @@ -1120,6 +1120,7 @@ def __init__(self, model: Model): ) self.redrill = self.OutputParameterDict[self.redrill.Name] = OutputParameter( Name="redrill", + display_name='Number of times redrilling', UnitType=Units.NONE ) self.PumpingPowerProd = self.OutputParameterDict[self.PumpingPowerProd.Name] = OutputParameter( diff --git a/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index 0c4a7494..ac839cc1 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.9.36' +__version__ = '3.9.40' diff --git a/src/geophires_x_client/geophires_x_result.py b/src/geophires_x_client/geophires_x_result.py index 7b34f3dc..eb367580 100644 --- a/src/geophires_x_client/geophires_x_result.py +++ b/src/geophires_x_client/geophires_x_result.py @@ -284,13 +284,14 @@ class GeophiresXResult: 'Annual District Heating O&M Cost', 'Average Annual Peaking Fuel Cost', 'Average annual pumping costs', - 'Total operating and maintenance costs', - # AGS/CLGS - 'OPEX', # SUTRA 'Average annual auxiliary fuel cost', 'Average annual pumping cost', + 'Redrilling costs', 'Total average annual O&M costs', + 'Total operating and maintenance costs', + # AGS/CLGS + 'OPEX', ], 'SURFACE EQUIPMENT SIMULATION RESULTS': [ 'Initial geofluid availability', diff --git a/src/geophires_x_schema_generator/geophires-request.json b/src/geophires_x_schema_generator/geophires-request.json index 8def2347..90967ba1 100644 --- a/src/geophires_x_schema_generator/geophires-request.json +++ b/src/geophires_x_schema_generator/geophires-request.json @@ -1591,7 +1591,7 @@ "category": "Economics", "default": -1.0, "minimum": 0, - "maximum": 1000 + "maximum": 100000.0 }, "Total O&M Cost": { "description": "Total initial O&M cost.", diff --git a/src/geophires_x_schema_generator/geophires-result.json b/src/geophires_x_schema_generator/geophires-result.json index 9cba6618..a2fe3a18 100644 --- a/src/geophires_x_schema_generator/geophires-result.json +++ b/src/geophires_x_schema_generator/geophires-result.json @@ -197,7 +197,11 @@ "Flowrate per production well": {}, "Injection well casing ID": {}, "Production well casing ID": {}, - "Number of times redrilling": {}, + "Number of times redrilling": { + "type": "number", + "description": "redrill", + "units": null + }, "Power plant type": {}, "Fluid": {}, "Design": {}, @@ -432,15 +436,20 @@ "units": "MUSD/yr" }, "Average annual pumping costs": {}, + "Average annual auxiliary fuel cost": {}, + "Average annual pumping cost": {}, + "Redrilling costs": { + "type": "number", + "description": "Total redrilling costs over the Plant Lifetime are calculated as (Drilling and completion costs + Stimulation costs) \u00d7 Number of times redrilling. The total is then divided over Plant Lifetime years to calculate Redrilling costs per year.", + "units": "MUSD/yr" + }, + "Total average annual O&M costs": {}, "Total operating and maintenance costs": { "type": "number", "description": "Total O&M Cost", "units": "MUSD/yr" }, - "OPEX": {}, - "Average annual auxiliary fuel cost": {}, - "Average annual pumping cost": {}, - "Total average annual O&M costs": {} + "OPEX": {} } }, "SURFACE EQUIPMENT SIMULATION RESULTS": { diff --git a/tests/examples/Fervo_Project_Cape-4.out b/tests/examples/Fervo_Project_Cape-4.out index 377cd823..e461faed 100644 --- a/tests/examples/Fervo_Project_Cape-4.out +++ b/tests/examples/Fervo_Project_Cape-4.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.9.28 - Simulation Date: 2025-07-02 - Simulation Time: 12:19 - Calculation Time: 1.773 sec + GEOPHIRES Version: 3.9.39 + Simulation Date: 2025-07-25 + Simulation Time: 14:32 + Calculation Time: 1.794 sec ***SUMMARY OF RESULTS*** @@ -104,7 +104,6 @@ Simulation Metadata Field gathering system costs: 56.44 MUSD Total surface equipment costs: 1560.49 MUSD Exploration costs: 30.00 MUSD - Investment Tax Credit: -688.54 MUSD Total CAPEX: 2639.39 MUSD @@ -113,6 +112,7 @@ Simulation Metadata Wellfield maintenance costs: 6.20 MUSD/yr Power plant maintenance costs: 25.43 MUSD/yr Water costs: 24.86 MUSD/yr + Redrilling costs: 70.46 MUSD/yr Total operating and maintenance costs: 126.95 MUSD/yr diff --git a/tests/examples/example13.out b/tests/examples/example13.out index b903fc30..006909f4 100644 --- a/tests/examples/example13.out +++ b/tests/examples/example13.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.9.7 - Simulation Date: 2025-05-15 - Simulation Time: 10:12 - Calculation Time: 0.032 sec + GEOPHIRES Version: 3.9.36 + Simulation Date: 2025-07-25 + Simulation Time: 11:30 + Calculation Time: 0.037 sec ***SUMMARY OF RESULTS*** @@ -109,6 +109,7 @@ Simulation Metadata Wellfield maintenance costs: 0.62 MUSD/yr Power plant maintenance costs: 1.30 MUSD/yr Water costs: 0.00 MUSD/yr + Redrilling costs: 0.92 MUSD/yr Total operating and maintenance costs: 2.84 MUSD/yr diff --git a/tests/examples/example_SAM-single-owner-PPA-2.out b/tests/examples/example_SAM-single-owner-PPA-2.out index 9a0af2ae..bcd63c41 100644 --- a/tests/examples/example_SAM-single-owner-PPA-2.out +++ b/tests/examples/example_SAM-single-owner-PPA-2.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.9.28 - Simulation Date: 2025-07-02 - Simulation Time: 12:19 - Calculation Time: 0.975 sec + GEOPHIRES Version: 3.9.39 + Simulation Date: 2025-07-25 + Simulation Time: 14:32 + Calculation Time: 0.983 sec ***SUMMARY OF RESULTS*** @@ -105,7 +105,6 @@ Simulation Metadata Field gathering system costs: 70.43 MUSD Total surface equipment costs: 969.26 MUSD Exploration costs: 30.00 MUSD - Investment Tax Credit: -459.83 MUSD Total CAPEX: 1609.42 MUSD diff --git a/tests/examples/example_SAM-single-owner-PPA.out b/tests/examples/example_SAM-single-owner-PPA.out index 07be3655..e71832f9 100644 --- a/tests/examples/example_SAM-single-owner-PPA.out +++ b/tests/examples/example_SAM-single-owner-PPA.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.9.28 - Simulation Date: 2025-07-02 - Simulation Time: 12:19 - Calculation Time: 1.163 sec + GEOPHIRES Version: 3.9.39 + Simulation Date: 2025-07-25 + Simulation Time: 14:32 + Calculation Time: 1.177 sec ***SUMMARY OF RESULTS*** @@ -106,7 +106,6 @@ Simulation Metadata Field gathering system costs: 5.80 MUSD Total surface equipment costs: 150.23 MUSD Exploration costs: 3.89 MUSD - Investment Tax Credit: -63.71 MUSD Total CAPEX: 222.97 MUSD diff --git a/tests/test_geophires_x.py b/tests/test_geophires_x.py index 58119d40..77352278 100644 --- a/tests/test_geophires_x.py +++ b/tests/test_geophires_x.py @@ -1219,3 +1219,45 @@ def test_exploration_cost(self): self.assertEqual(exploration_cost_MUSD, result.result['CAPITAL COSTS (M$)']['Exploration costs']['value']) self.assertEqual('MUSD', result.result['CAPITAL COSTS (M$)']['Exploration costs']['unit']) + + def test_redrilling_costs(self): + total_capex_specified_result = GeophiresXClient().get_geophires_result( + ImmutableGeophiresInputParameters( + from_file_path=self._get_test_file_path('examples/Fervo_Project_Cape-4.txt'), + params={'Total Capital Cost': 2500}, + ) + ) + + for result in [ + GeophiresXResult(self._get_test_file_path('examples/Fervo_Project_Cape-4.out')), + total_capex_specified_result, + ]: + result_redrills = result.result['ENGINEERING PARAMETERS']['Number of times redrilling']['value'] + self.assertGreater(result_redrills, 0) + + result_opex = result.result['OPERATING AND MAINTENANCE COSTS (M$/yr)'] + opex_sum = 0 + expected_opex_line_items = [ + 'Wellfield maintenance costs', + 'Power plant maintenance costs', + 'Water costs', + 'Redrilling costs', + ] + for opex_line_item in expected_opex_line_items: + opex_sum += result_opex[opex_line_item]['value'] + + self.assertAlmostEqual(result_opex['Total operating and maintenance costs']['value'], opex_sum, places=1) + + result_capex = result.result['CAPITAL COSTS (M$)'] + capex_field_suffix = ( + '' if result_capex.get('Drilling and completion costs') is not None else ' (for redrilling)' + ) + expected_annual_redrilling_cost = ( + ( + result_capex[f'Drilling and completion costs{capex_field_suffix}']['value'] + + result_capex[f'Stimulation costs{capex_field_suffix}']['value'] + ) + * result_redrills + ) / result.result['ECONOMIC PARAMETERS']['Project lifetime']['value'] + + self.assertAlmostEqual(expected_annual_redrilling_cost, result_opex['Redrilling costs']['value'], places=2)