From 956d50943ef3bbad73879b6809bb0f31202db7ea Mon Sep 17 00:00:00 2001 From: Jonathan Maddock <78556175+jonmaddock@users.noreply.github.com> Date: Thu, 17 Oct 2024 14:31:33 +0100 Subject: [PATCH 1/5] Warn about non-idempotent values in MFILE --- process/caller.py | 85 ++++++++++++++++++++++++++++++++++++----------- setup.py | 1 + 2 files changed, 67 insertions(+), 19 deletions(-) diff --git a/process/caller.py b/process/caller.py index 9dec13cb02..4632db115a 100644 --- a/process/caller.py +++ b/process/caller.py @@ -6,6 +6,8 @@ from process.io.mfile import MFile from process.utilities.f2py_string_patch import f2py_compatible_to_string from typing import Union, Tuple, TYPE_CHECKING +import warnings +from tabulate import tabulate if TYPE_CHECKING: from process.main import Models @@ -41,7 +43,36 @@ def check_agreement( :return: whether values agree or not :rtype: bool """ - return np.allclose(previous, current, rtol=1.0e-6) + # Check for same shape: mfile length can change between iterations + if isinstance(previous, float) or previous.shape == current.shape: + return np.allclose(previous, current, rtol=1.0e-6, equal_nan=True) + else: + return False + + @staticmethod + def check_mfile_agreement( + previous: dict[str, float], current: dict[str, float] + ) -> dict[str, list[float, float]]: + """Compare previous and current mfiles for agreement within a tolerance. + + :param previous: variables and values from previous models evaluation + :type previous: dict[str,float] + :param current: variables and values from current models evaluation + :type current: dict[str,float] + :return: non-converged values + :rtype: dict[str, list[float, float] + """ + nonconverged_vars = {} + for var in previous.keys(): + if np.allclose( + previous[var], current.get(var, np.nan), rtol=1.0e-6, equal_nan=True + ): + continue + else: + # Value has changed between previous and current mfiles + nonconverged_vars[var] = [previous[var], current.get(var, np.nan)] + + return nonconverged_vars def call_models(self, xc: np.ndarray, m: int) -> Tuple[float, np.ndarray]: """Evalutate models until results are idempotent. @@ -113,7 +144,7 @@ def call_models_and_write_output(self, xc: np.ndarray, ifail: int) -> None: """ # TODO The only way to ensure idempotence in all outputs is by comparing # mfiles at this stage - previous_mfile_arr = None + previous_mfile_data = None try: # Evaluate models up to 10 times; any more implies non-converging values @@ -131,24 +162,25 @@ def call_models_and_write_output(self, xc: np.ndarray, ifail: int) -> None: + "IDEM_MFILE.DAT" ) mfile = MFile(mfile_path) - mfile_data = {} - for var in mfile.data.keys(): - mfile_data[var] = mfile.data[var].get_scan(-1) - - # Extract floats from mfile dict into array for straightforward - # comparison: only compare floats - current_mfile_arr = np.array( - [val for val in mfile_data.values() if isinstance(val, float)] - ) - if previous_mfile_arr is None: + # Create mfile dict of float values: only compare floats + mfile_data = { + var: val + for var in mfile.data.keys() + if isinstance(val := mfile.data[var].get_scan(-1), float) + } + + if previous_mfile_data is None: # First run: need another run to compare with logger.debug( "New mfile created: evaluating models again to check idempotence" ) - previous_mfile_arr = np.copy(current_mfile_arr) + previous_mfile_data = mfile_data.copy() continue - if self.check_agreement(previous_mfile_arr, current_mfile_arr): + nonconverged_vars = self.check_mfile_agreement( + previous_mfile_data, mfile_data + ) + if len(nonconverged_vars) == 0: # Previous and current mfiles agree (idempotent) logger.debug("Mfiles idempotent, returning") # Divert OUT.DAT and MFILE.DAT output back to original files @@ -158,17 +190,32 @@ def call_models_and_write_output(self, xc: np.ndarray, ifail: int) -> None: finalise(self.models, ifail) return - # Mfiles not yet idempotent: re-evaluate models + # Mfiles not yet idempotent: need to re-evaluate models logger.debug("Mfiles not idempotent, evaluating models again") - previous_mfile_arr = np.copy(current_mfile_arr) + previous_mfile_data = mfile_data.copy() - raise RuntimeError( + # Values haven't all stabilised after 10 evaluations + # Which variables are still changing? + warnings.warn( "Model evaluations at the current optimisation parameter vector " "don't produce idempotent values in the final output." ) + + print( + tabulate( + [[k, v[0], v[1]] for k, v in nonconverged_vars.items()], + headers=["Variable", "Previous value", "Current value"], + ) + ) + + # Close idempotence files, write final output file and mfile + ft.init_module.close_idempotence_files() + finalise(self.models, ifail) + return + except Exception: - # If exception in model evaluations or idempotence can't be - # achieved, delete intermediate idempotence files to clean up + # If exception in model evaluations delete intermediate idempotence + # files to clean up ft.init_module.close_idempotence_files() raise diff --git a/setup.py b/setup.py index cc84828a9c..d2c7b97a24 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ "CoolProp>=6.4", "matplotlib>=2.1.1", "seaborn>=0.12.2", + "tabulate", ], "extras_require": { "test": ["pytest>=5.4.1", "requests>=2.30", "testbook>=0.4"], From a194300a108e6d3665af8a6cd68b01b02aaa8c4e Mon Sep 17 00:00:00 2001 From: Jonathan Maddock <78556175+jonmaddock@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:33:45 +0000 Subject: [PATCH 2/5] Add warning colour to non-idempotence table --- process/caller.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/process/caller.py b/process/caller.py index 4632db115a..93284bcc51 100644 --- a/process/caller.py +++ b/process/caller.py @@ -197,6 +197,7 @@ def call_models_and_write_output(self, xc: np.ndarray, ifail: int) -> None: # Values haven't all stabilised after 10 evaluations # Which variables are still changing? warnings.warn( + "\033[93m" "Model evaluations at the current optimisation parameter vector " "don't produce idempotent values in the final output." ) @@ -206,6 +207,7 @@ def call_models_and_write_output(self, xc: np.ndarray, ifail: int) -> None: [[k, v[0], v[1]] for k, v in nonconverged_vars.items()], headers=["Variable", "Previous value", "Current value"], ) + + "\033[0m" ) # Close idempotence files, write final output file and mfile From d7df252b0e8a29565f29dc8845c828211b222ede Mon Sep 17 00:00:00 2001 From: Jonathan Maddock <78556175+jonmaddock@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:58:12 +0000 Subject: [PATCH 3/5] Consolidate closely-related check_agreement methods --- process/caller.py | 42 ++++++++++++++---------------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/process/caller.py b/process/caller.py index 93284bcc51..29d72f4c50 100644 --- a/process/caller.py +++ b/process/caller.py @@ -49,31 +49,6 @@ def check_agreement( else: return False - @staticmethod - def check_mfile_agreement( - previous: dict[str, float], current: dict[str, float] - ) -> dict[str, list[float, float]]: - """Compare previous and current mfiles for agreement within a tolerance. - - :param previous: variables and values from previous models evaluation - :type previous: dict[str,float] - :param current: variables and values from current models evaluation - :type current: dict[str,float] - :return: non-converged values - :rtype: dict[str, list[float, float] - """ - nonconverged_vars = {} - for var in previous.keys(): - if np.allclose( - previous[var], current.get(var, np.nan), rtol=1.0e-6, equal_nan=True - ): - continue - else: - # Value has changed between previous and current mfiles - nonconverged_vars[var] = [previous[var], current.get(var, np.nan)] - - return nonconverged_vars - def call_models(self, xc: np.ndarray, m: int) -> Tuple[float, np.ndarray]: """Evalutate models until results are idempotent. @@ -177,9 +152,20 @@ def call_models_and_write_output(self, xc: np.ndarray, ifail: int) -> None: previous_mfile_data = mfile_data.copy() continue - nonconverged_vars = self.check_mfile_agreement( - previous_mfile_data, mfile_data - ) + # Compare previous and current mfiles for agreement + nonconverged_vars = {} + for var in previous_mfile_data.keys(): + previous_value = previous_mfile_data[var] + current_value = mfile_data.get(var, np.nan) + if self.check_agreement(previous_value, current_value): + continue + else: + # Value has changed between previous and current mfiles + nonconverged_vars[var] = [ + previous_value, + current_value, + ] + if len(nonconverged_vars) == 0: # Previous and current mfiles agree (idempotent) logger.debug("Mfiles idempotent, returning") From 55943c3083474addcc4120c6c5df45b22dd8713d Mon Sep 17 00:00:00 2001 From: Jonathan Maddock <78556175+jonmaddock@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:53:26 +0000 Subject: [PATCH 4/5] Write non-idempotence warning to OUT.DAT --- process/caller.py | 22 ++++++++++++---------- process/final.py | 10 +++++++++- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/process/caller.py b/process/caller.py index 29d72f4c50..93c38cecdb 100644 --- a/process/caller.py +++ b/process/caller.py @@ -182,23 +182,25 @@ def call_models_and_write_output(self, xc: np.ndarray, ifail: int) -> None: # Values haven't all stabilised after 10 evaluations # Which variables are still changing? - warnings.warn( - "\033[93m" + non_idempotent_warning = ( "Model evaluations at the current optimisation parameter vector " "don't produce idempotent values in the final output." ) - - print( - tabulate( - [[k, v[0], v[1]] for k, v in nonconverged_vars.items()], - headers=["Variable", "Previous value", "Current value"], - ) - + "\033[0m" + non_idempotent_table = tabulate( + [[k, v[0], v[1]] for k, v in nonconverged_vars.items()], + headers=["Variable", "Previous value", "Current value"], ) + warnings.warn("\033[93m" + non_idempotent_warning) + print(non_idempotent_table + "\033[0m") + # Close idempotence files, write final output file and mfile ft.init_module.close_idempotence_files() - finalise(self.models, ifail) + finalise( + self.models, + ifail, + non_idempotent_msg=non_idempotent_warning + "\n" + non_idempotent_table, + ) return except Exception: diff --git a/process/final.py b/process/final.py index 06a31142fb..c4df6ae9ca 100644 --- a/process/final.py +++ b/process/final.py @@ -3,9 +3,10 @@ from process import fortran as ft from process.fortran import final_module as fm from process import output as op +from process.fortran import process_output as po -def finalise(models, ifail): +def finalise(models, ifail, non_idempotent_msg: None | str = None): """Routine to print out the final point in the scan. Writes to OUT.DAT and MFILE.DAT. @@ -14,6 +15,8 @@ def finalise(models, ifail): :type models: process.main.Models :param ifail: error flag :type ifail: int + :param non_idempotent_msg: warning about non-idempotent variables, defaults to None + :type non_idempotent_msg: None | str, optional """ fm.final_header(ifail) @@ -21,6 +24,11 @@ def finalise(models, ifail): if ft.numerics.ioptimz == -2: fm.no_optimisation() + # Print non-idempotence warning to OUT.DAT only + if non_idempotent_msg: + po.oheadr(ft.constants.nout, "NON-IDEMPOTENT VARIABLES") + po.ocmmnt(ft.constants.nout, non_idempotent_msg) + # Write output to OUT.DAT and MFILE.DAT op.write(models, ft.constants.nout) From 93aeb61289cc4274b336d6e6664f7d156c5cd8cf Mon Sep 17 00:00:00 2001 From: Jonathan Maddock <78556175+jonmaddock@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:54:16 +0000 Subject: [PATCH 5/5] Combine text and table into warning --- process/caller.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/process/caller.py b/process/caller.py index 93c38cecdb..8bbe91018c 100644 --- a/process/caller.py +++ b/process/caller.py @@ -191,8 +191,9 @@ def call_models_and_write_output(self, xc: np.ndarray, ifail: int) -> None: headers=["Variable", "Previous value", "Current value"], ) - warnings.warn("\033[93m" + non_idempotent_warning) - print(non_idempotent_table + "\033[0m") + warnings.warn( + f"\033[93m{non_idempotent_warning}\n{non_idempotent_table}\033[0m" + ) # Close idempotence files, write final output file and mfile ft.init_module.close_idempotence_files()