diff --git a/README.rst b/README.rst index ba71f17c5..0e1828e63 100644 --- a/README.rst +++ b/README.rst @@ -288,7 +288,7 @@ Example-specific web interface deeplinks are listed in the Link column. - `Fervo_Project_Cape-3.txt `__ - `.out `__ - `link `__ - * - Case Study: 500 MWe EGS Project Modeled on Fervo Cape Station (`documentation `__) + * - Case Study: 500 MWe EGS Project Modeled on Fervo Cape Station (`documentation `__) - `Fervo_Project_Cape-4.txt `__ - `.out `__ - `link `__ diff --git a/docs/_images/geophires-favicon.png b/docs/_images/geophires-favicon.png new file mode 100644 index 000000000..d69d88104 Binary files /dev/null and b/docs/_images/geophires-favicon.png differ diff --git a/src/geophires_x/MPFReservoir.py b/src/geophires_x/MPFReservoir.py index efc5ba31c..879739983 100644 --- a/src/geophires_x/MPFReservoir.py +++ b/src/geophires_x/MPFReservoir.py @@ -1,8 +1,12 @@ import sys + +from mpmath import mp import numpy as np from mpmath import * import geophires_x.Model as Model +from .Parameter import intParameter from .Reservoir import Reservoir +from .Units import Units class MPFReservoir(Reservoir): @@ -12,6 +16,7 @@ class MPFReservoir(Reservoir): It also has its own methods and attributes that are unique to this class. """ + # noinspection PyUnresolvedReferences,PyProtectedMember def __init__(self, model: Model): """ The __init__ function is called automatically when a class is instantiated. @@ -31,15 +36,30 @@ 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 {str(__class__)}: {sys._getframe().f_code.co_name}') + super().__init__(model) # initialize the parent parameters and variables sclass = str(__class__).replace("", "") - model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) + + max_allowed_precision = 15 + self.gringarten_stehfest_precision = self.ParameterDict[self.gringarten_stehfest_precision.Name] = intParameter( + 'Gringarten-Stehfest Precision', + DefaultValue=15, + AllowableRange=list(range(8, max_allowed_precision + 1)), + UnitType=Units.NONE, + Required=False, + ToolTipText='Sets the numerical precision (decimal places) for the Stehfest ' + 'algorithm used for the inverse Laplace transform.' + ) + + + model.logger.info(f'Complete {str(__class__)}: {sys._getframe().f_code.co_name}') def __str__(self): return "MPFReservoir" + # noinspection PyUnresolvedReferences,PyProtectedMember def read_parameters(self, model: Model) -> None: """ The read_parameters function reads in the parameters from a dictionary created by reading the user-provided file @@ -51,15 +71,16 @@ 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 {str(__class__)}: {sys._getframe().f_code.co_name}') # 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 super().read_parameters(model) # read the parameters for the parent. - model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f'Complete {str(__class__)}: {sys._getframe().f_code.co_name}') + # noinspection SpellCheckingInspection,PyUnresolvedReferences,PyProtectedMember def Calculate(self, model: Model): """ The Calculate function calculates the values of all the parameters that are calculated by this object. @@ -87,13 +108,22 @@ def Calculate(self, model: Model): # calculate non-dimensional temperature array Twnd = [] try: + stash_dps = mp.dps + if self.gringarten_stehfest_precision.Provided: + mp.dps = self.gringarten_stehfest_precision.value + for t in range(1, len(model.reserv.timevector.value)): Twnd = Twnd + [float(invertlaplace(fp, td[t], method='stehfest'))] - except: - msg = ('Error: GEOPHIRES could not execute numerical inverse laplace calculation for reservoir model 1. ' + except Exception as e_: + mp.dps = stash_dps + + msg = (f'Error: GEOPHIRES could not execute numerical inverse laplace calculation for reservoir model 1 ' + f'({self.gringarten_stehfest_precision.Name} = {mpmath.dps}). ' 'Simulation will abort.') print(msg) - raise RuntimeError(msg) + raise RuntimeError(msg) from e_ + + mp.dps = stash_dps Twnd = np.asarray(Twnd) diff --git a/src/geophires_x_client/geophires_x_result.py b/src/geophires_x_client/geophires_x_result.py index e2ad90298..aabb76199 100644 --- a/src/geophires_x_client/geophires_x_result.py +++ b/src/geophires_x_client/geophires_x_result.py @@ -346,7 +346,10 @@ class GeophiresXResult: 'Average Annual Total Heating Production', 'Average Annual Electricity Use for Pumping', ], - 'Simulation Metadata': [_StringValueField('GEOPHIRES Version')], + 'Simulation Metadata': [ + _StringValueField('GEOPHIRES Version'), + 'Calculation Time', + ], } ) diff --git a/src/geophires_x_schema_generator/geophires-request.json b/src/geophires_x_schema_generator/geophires-request.json index 292d7dbd1..151f6fa0a 100644 --- a/src/geophires_x_schema_generator/geophires-request.json +++ b/src/geophires_x_schema_generator/geophires-request.json @@ -412,6 +412,15 @@ "minimum": 0, "maximum": 0.2 }, + "Gringarten-Stehfest Precision": { + "description": "Sets the numerical precision (decimal places) for the Stehfest algorithm used for the inverse Laplace transform.", + "type": "integer", + "units": null, + "category": "Reservoir", + "default": 15, + "minimum": 8, + "maximum": 15 + }, "Cylindrical Reservoir Input Depth": { "description": "Depth of the inflow end of a cylindrical reservoir", "type": "number", diff --git a/src/geophires_x_schema_generator/geophires-result.json b/src/geophires_x_schema_generator/geophires-result.json index fa345748a..4edc6f710 100644 --- a/src/geophires_x_schema_generator/geophires-result.json +++ b/src/geophires_x_schema_generator/geophires-result.json @@ -555,7 +555,8 @@ "Simulation Metadata": { "type": "object", "properties": { - "GEOPHIRES Version": {} + "GEOPHIRES Version": {}, + "Calculation Time": {} } } } diff --git a/tests/example1_addons.csv b/tests/example1_addons.csv index 0599d06cf..b02d3b434 100644 --- a/tests/example1_addons.csv +++ b/tests/example1_addons.csv @@ -98,7 +98,8 @@ SURFACE EQUIPMENT SIMULATION RESULTS,Average Annual Net Electricity Generation,, SURFACE EQUIPMENT SIMULATION RESULTS,Average Pumping Power,,0.2,MW SURFACE EQUIPMENT SIMULATION RESULTS,Initial pumping power/net installed power,,3.82,% SURFACE EQUIPMENT SIMULATION RESULTS,Heat to Power Conversion Efficiency,,10.07,% -Simulation Metadata,GEOPHIRES Version,,3.9.44, +Simulation Metadata,GEOPHIRES Version,,3.9.49, +Simulation Metadata,Calculation Time,,0.852,sec POWER GENERATION PROFILE,THERMAL DRAWDOWN,1,1.0, POWER GENERATION PROFILE,THERMAL DRAWDOWN,2,1.0056, POWER GENERATION PROFILE,THERMAL DRAWDOWN,3,1.0073, diff --git a/tests/examples/example1_addons.out b/tests/examples/example1_addons.out index a29a34fa5..298006df1 100644 --- a/tests/examples/example1_addons.out +++ b/tests/examples/example1_addons.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.9.44 - Simulation Date: 2025-07-29 - Simulation Time: 06:46 - Calculation Time: 0.898 sec + GEOPHIRES Version: 3.9.49 + Simulation Date: 2025-08-21 + Simulation Time: 09:19 + Calculation Time: 0.852 sec ***SUMMARY OF RESULTS*** diff --git a/tests/geophires-result_example-3.csv b/tests/geophires-result_example-3.csv index 471040120..42508b6e1 100644 --- a/tests/geophires-result_example-3.csv +++ b/tests/geophires-result_example-3.csv @@ -82,6 +82,7 @@ SURFACE EQUIPMENT SIMULATION RESULTS,Average Annual Heat Production,,87.2,GWh SURFACE EQUIPMENT SIMULATION RESULTS,Average Pumping Power,,0.18,MW SURFACE EQUIPMENT SIMULATION RESULTS,Initial pumping power/net installed power,,100.0,% Simulation Metadata,GEOPHIRES Version,,3.0, +Simulation Metadata,Calculation Time,,0.057,sec POWER GENERATION PROFILE,THERMAL DRAWDOWN,0,1.0, POWER GENERATION PROFILE,THERMAL DRAWDOWN,1,1.0081, POWER GENERATION PROFILE,THERMAL DRAWDOWN,2,1.0097, diff --git a/tests/geophires_x_tests/test_reservoir.py b/tests/geophires_x_tests/test_reservoir.py index 98985cb6c..52ce8e142 100644 --- a/tests/geophires_x_tests/test_reservoir.py +++ b/tests/geophires_x_tests/test_reservoir.py @@ -12,10 +12,12 @@ from geophires_x_client import GeophiresInputParameters from geophires_x_client import GeophiresXClient from geophires_x_client import GeophiresXResult +from geophires_x_client import ImmutableGeophiresInputParameters from tests.base_test_case import BaseTestCase class ReservoirTestCase(BaseTestCase): + def test_lithostatic_pressure(self): p = static_pressure_MPa(2700, 3000) self.assertEqual(79.433865, p) @@ -32,6 +34,35 @@ def test_reservoir_lithostatic_pressure(self): self.assertAlmostEqual(79.433865, p.magnitude, places=3) self.assertEqual('megapascal', p.units) + def test_gringarten_stehfest_precision(self): + def _log(msg) -> None: + print(f'[DEBUG][test_gringarten_stehfest_precision] {msg}') + + def _get_result(gringarten_stehfest_precision: int) -> GeophiresXResult: + return GeophiresXClient(enable_caching=False).get_geophires_result( + ImmutableGeophiresInputParameters( + from_file_path=self._get_test_file_path('generic-egs-case.txt'), + params={'Gringarten-Stehfest Precision': gringarten_stehfest_precision}, + ) + ) + + _ = _get_result(15) # warm up any caching + result_15 = _get_result(15) + result_8 = _get_result(8) + + def calc_time(r: GeophiresXResult) -> float: + return r.result['Simulation Metadata']['Calculation Time']['value'] + + calc_time_15_sec = calc_time(result_15) + calc_time_8_sec = calc_time(result_8) + + _log(f'calc_time_15_sec={calc_time_15_sec}, calc_time_8_sec={calc_time_8_sec}') + + self.assertLess(calc_time_8_sec, calc_time_15_sec) + + speedup_pct = ((calc_time_15_sec - calc_time_8_sec) / calc_time_15_sec) * 100 + _log(f'Speedup: {speedup_pct:.2f}%') + # noinspection PyMethodMayBeStatic def _new_model(self, input_file=None) -> Model: stash_cwd = Path.cwd() diff --git a/tests/test_geophires_x.py b/tests/test_geophires_x.py index 0ac9abf15..0e50e5d4a 100644 --- a/tests/test_geophires_x.py +++ b/tests/test_geophires_x.py @@ -67,6 +67,9 @@ def test_geophires_x_end_use_direct_use_heat(self): if 'metadata' in result_same_input.result: del result_same_input.result['metadata'] + del result.result['Simulation Metadata']['Calculation Time'] + del result_same_input.result['Simulation Metadata']['Calculation Time'] + self.assertDictEqual(result.result, result_same_input.result) # noinspection PyMethodMayBeStatic @@ -362,6 +365,10 @@ def test_RTES_name(self): self.assertEqual(PlantType.RTES.value, 'Reservoir Thermal Energy Storage') def test_input_unit_conversion(self): + def delete_metadata(r: GeophiresXResult) -> None: + del r.result['metadata'] + del r.result['Simulation Metadata']['Calculation Time'] + client = GeophiresXClient() result_meters_input = client.get_geophires_result( @@ -371,7 +378,7 @@ def test_input_unit_conversion(self): ) ) ) - del result_meters_input.result['metadata'] + delete_metadata(result_meters_input) result_kilometers_input = client.get_geophires_result( GeophiresInputParameters( @@ -380,7 +387,7 @@ def test_input_unit_conversion(self): ) ) ) - del result_kilometers_input.result['metadata'] + delete_metadata(result_kilometers_input) self.assertDictEqual(result_kilometers_input.result, result_meters_input.result) diff --git a/tests/test_geophires_x_client.py b/tests/test_geophires_x_client.py index d9bb8438b..f3f8fa873 100644 --- a/tests/test_geophires_x_client.py +++ b/tests/test_geophires_x_client.py @@ -440,36 +440,44 @@ def test_input_hashing(self): self.assertNotEqual(hash(input1), hash(input3)) def test_input_with_non_default_units(self): + def delete_metadata(r: GeophiresXResult) -> GeophiresXResult: + del r.result['metadata'] + del r.result['Simulation Metadata']['Calculation Time'] + + return r + client = GeophiresXClient() - result_default_units = client.get_geophires_result( - GeophiresInputParameters( - { - 'Print Output to Console': 0, - 'End-Use Option': EndUseOption.DIRECT_USE_HEAT.value, - 'Reservoir Model': 1, - 'Time steps per year': 1, - 'Reservoir Depth': 3, - 'Gradient 1': 50, - 'Maximum Temperature': 250, - } + result_default_units = delete_metadata( + client.get_geophires_result( + GeophiresInputParameters( + { + 'Print Output to Console': 0, + 'End-Use Option': EndUseOption.DIRECT_USE_HEAT.value, + 'Reservoir Model': 1, + 'Time steps per year': 1, + 'Reservoir Depth': 3, + 'Gradient 1': 50, + 'Maximum Temperature': 250, + } + ) ) ).result - del result_default_units['metadata'] - result_non_default_units = client.get_geophires_result( - GeophiresInputParameters( - { - 'Print Output to Console': 0, - 'End-Use Option': EndUseOption.DIRECT_USE_HEAT.value, - 'Reservoir Model': 1, - 'Time steps per year': 1, - 'Reservoir Depth': '3000 meter', - 'Gradient 1': 50, - 'Maximum Temperature': 250, - } + result_non_default_units = delete_metadata( + client.get_geophires_result( + GeophiresInputParameters( + { + 'Print Output to Console': 0, + 'End-Use Option': EndUseOption.DIRECT_USE_HEAT.value, + 'Reservoir Model': 1, + 'Time steps per year': 1, + 'Reservoir Depth': '3000 meter', + 'Gradient 1': 50, + 'Maximum Temperature': 250, + } + ) ) ).result - del result_non_default_units['metadata'] self.assertDictEqual(result_default_units, result_non_default_units)