From b3798e0dd5fbf3c5bd822033f116a08232623943 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Mon, 1 Dec 2025 10:00:42 +0100 Subject: [PATCH 01/41] report and observations in check --- util/dataframe_ops.py | 61 +++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/util/dataframe_ops.py b/util/dataframe_ops.py index 0120b70e..faf4084d 100644 --- a/util/dataframe_ops.py +++ b/util/dataframe_ops.py @@ -15,7 +15,7 @@ from util.constants import CHECK_THRESHOLD, compute_statistics from util.file_system import file_names_from_pattern -from util.fof_utils import split_feedback_dataset +from util.fof_utils import split_feedback_dataset, compare_var_and_attr_ds from util.log_handler import logger from util.model_output_parser import model_output_parser from util.utils import FileType @@ -71,9 +71,12 @@ def parse_probtest_stats(path, index_col=None): def parse_probtest_fof(path): ds = xr.open_dataset(path) - _, _, ds_veri = split_feedback_dataset(ds) + ds_report, _, ds_veri = split_feedback_dataset(ds) + df_report = ds_report.to_dataframe().reset_index() + df_report = pd.DataFrame(df_report) df_veri = ds_veri.to_dataframe().reset_index() - return pd.DataFrame(df_veri) + df_veri = pd.DataFrame(df_veri) + return df_report, df_veri def read_input_file(label, file_name, specification): @@ -320,21 +323,29 @@ def check_file_with_tolerances( ds_tol = pd.read_csv(tolerance_file_name, index_col=0) df_tol = ds_tol * factor - df_ref = parse_probtest_fof(input_file_ref.path) + df_ref_rep, df_ref_veri = parse_probtest_fof(input_file_ref.path) - df_cur = parse_probtest_fof(input_file_cur.path) - if rules != "": + df_cur_rep, df_cur_veri = parse_probtest_fof(input_file_cur.path) - errors = multiple_solutions_from_dict(df_ref, df_cur, rules) + df_ref = {"rep": df_ref_rep, "veri": df_ref_veri} + df_cur = {"rep": df_cur_rep, "veri": df_cur_veri} - if errors: - logger.error("RESULT: check FAILED") - sys.exit(1) + errors = multiple_solutions_from_dict(df_ref, df_cur, rules) + + if errors: + logger.error("RESULT: check FAILED") + sys.exit(1) else: df_tol, df_ref, df_cur = parse_check( tolerance_file_name, input_file_ref.path, input_file_cur.path, factor ) + # check if variables are available in reference file + skip_test, df_ref, df_cur = check_intersection(df_ref, df_cur) + + if skip_test: # No intersection + logger.error("RESULT: check FAILED") + sys.exit(1) logger.info("applying a factor of %s to the spread", factor) logger.info( @@ -343,16 +354,10 @@ def check_file_with_tolerances( input_file_ref.path, tolerance_file_name, ) - # check if variables are available in reference file - skip_test, df_ref, df_cur = check_intersection(df_ref, df_cur) - - if skip_test: # No intersection - logger.error("RESULT: check FAILED") - sys.exit(1) if input_file_ref.file_type == FileType.FOF: - df_ref = df_ref["veri_data"] - df_cur = df_cur["veri_data"] + df_ref = df_ref["veri"]["veri_data"] + df_cur = df_cur["veri"]["veri_data"] df_tol.columns = ["veri_data"] # compute relative difference @@ -393,9 +398,12 @@ def multiple_solutions_from_dict(df_ref, df_cur, rules): """ if isinstance(rules, str): - rules_dict = ast.literal_eval(rules) - else: + rules = rules.strip() + rules_dict = ast.literal_eval(rules) if rules else {} + elif isinstance(rules, dict): rules_dict = rules + else: + rules_dict = {} cols_present = [ col @@ -404,6 +412,19 @@ def multiple_solutions_from_dict(df_ref, df_cur, rules): ] errors = [] + errors_found = False + + for key in df_ref.keys(): + ref_df = df_ref[key] + cur_df = df_cur[key] + + if not ref_df.equals(cur_df): + errors_found = True + print(f"DataFrames different in section '{key}':") + print(ref_df.compare(cur_df)) + + return errors_found + if cols_present: for i in range(len(df_ref)): row1 = df_ref.iloc[i] From b255f0e80e0d133313691d31fde758f536d346c7 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Mon, 1 Dec 2025 10:57:55 +0100 Subject: [PATCH 02/41] improved version of extended check --- engine/fof_compare.py | 4 ++-- tests/util/test_dataframe_ops.py | 16 ++++++++-------- util/dataframe_ops.py | 22 +++++++++------------- util/fof_utils.py | 7 +++---- 4 files changed, 22 insertions(+), 27 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index 238234ca..ae47e99c 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -57,8 +57,8 @@ def fof_compare( ds1 = xr.open_dataset(file1) ds2 = xr.open_dataset(file2) - ds_reports1_sorted, ds_obs1_sorted, _ = split_feedback_dataset(ds1) - ds_reports2_sorted, ds_obs2_sorted, _ = split_feedback_dataset(ds2) + ds_reports1_sorted, ds_obs1_sorted = split_feedback_dataset(ds1) + ds_reports2_sorted, ds_obs2_sorted = split_feedback_dataset(ds2) total_elements_all, equal_elements_all = 0, 0 diff --git a/tests/util/test_dataframe_ops.py b/tests/util/test_dataframe_ops.py index f3d55ab9..c78b60f3 100644 --- a/tests/util/test_dataframe_ops.py +++ b/tests/util/test_dataframe_ops.py @@ -419,7 +419,7 @@ def test(sample_dataset_fof, tmp_path, sample_df_with_obs): fake_path = tmp_path / "sample_dataset_fof.nc" sample_dataset_fof.to_netcdf(fake_path) - f = parse_probtest_fof(fake_path) + _, f = parse_probtest_fof(fake_path) assert f.equals(sample_df_with_obs) @@ -669,8 +669,8 @@ def test_check_stats(stats_dataframes): def test_check_fof(fof_datasets): ds1, ds2, tol_large, tol_small = fof_datasets - _, _, ds_veri1 = split_feedback_dataset(ds1) - _, _, ds_veri2 = split_feedback_dataset(ds2) + _, ds_veri1 = split_feedback_dataset(ds1) + _, ds_veri2 = split_feedback_dataset(ds2) df_veri1 = ds_veri1.to_dataframe().reset_index() df_veri2 = ds_veri2.to_dataframe().reset_index() _check( @@ -712,8 +712,8 @@ def test_check_one_zero_fof(fof_datasets): ds1["veri_data"][2] = 0 ds2_copy["veri_data"][2] = CHECK_THRESHOLD / 2 - _, _, ds_veri1 = split_feedback_dataset(ds1) - _, _, ds_veri2 = split_feedback_dataset(ds2) + _, ds_veri1 = split_feedback_dataset(ds1) + _, ds_veri2 = split_feedback_dataset(ds2) df_veri1 = ds_veri1.to_dataframe().reset_index() df_veri2 = ds_veri2.to_dataframe().reset_index() @@ -724,7 +724,7 @@ def test_check_one_zero_fof(fof_datasets): assert not out, f"Check with 0-value reference validated incorrectly:\n{err}" - _, _, ds_veri2_copy = split_feedback_dataset(ds2_copy) + _, ds_veri2_copy = split_feedback_dataset(ds2_copy) ds_veri2_copy = ds_veri2_copy.copy(deep=True) ds_veri2_copy["veri_data"][2] = CHECK_THRESHOLD / 2 _check( @@ -765,8 +765,8 @@ def test_check_smalls_fof(fof_datasets): ds1["veri_data"][2] = CHECK_THRESHOLD * 1e-5 ds2["veri_data"][2] = -CHECK_THRESHOLD / 2 - _, _, ds_veri1 = split_feedback_dataset(ds1) - _, _, ds_veri2 = split_feedback_dataset(ds2) + _, ds_veri1 = split_feedback_dataset(ds1) + _, ds_veri2 = split_feedback_dataset(ds2) df_veri1 = ds_veri1.to_dataframe().reset_index() df_veri2 = ds_veri2.to_dataframe().reset_index() diff --git a/util/dataframe_ops.py b/util/dataframe_ops.py index faf4084d..a01ecd61 100644 --- a/util/dataframe_ops.py +++ b/util/dataframe_ops.py @@ -71,11 +71,9 @@ def parse_probtest_stats(path, index_col=None): def parse_probtest_fof(path): ds = xr.open_dataset(path) - ds_report, _, ds_veri = split_feedback_dataset(ds) - df_report = ds_report.to_dataframe().reset_index() - df_report = pd.DataFrame(df_report) - df_veri = ds_veri.to_dataframe().reset_index() - df_veri = pd.DataFrame(df_veri) + ds_report, ds_veri = split_feedback_dataset(ds) + df_report, df_veri = (pd.DataFrame(d.to_dataframe().reset_index()) for d in (ds_report, ds_veri)) + return df_report, df_veri @@ -384,7 +382,7 @@ def has_enough_data(dfs): file_name_parser = { - FileType.FOF: parse_probtest_fof, + FileType.FOF: lambda path: parse_probtest_fof(path)[1], FileType.STATS: parse_probtest_stats, } @@ -412,18 +410,16 @@ def multiple_solutions_from_dict(df_ref, df_cur, rules): ] errors = [] - errors_found = False - for key in df_ref.keys(): ref_df = df_ref[key] cur_df = df_cur[key] - if not ref_df.equals(cur_df): - errors_found = True - print(f"DataFrames different in section '{key}':") - print(ref_df.compare(cur_df)) + ref_df = ref_df.to_xarray() + cur_df = cur_df.to_xarray() - return errors_found + t, e = compare_var_and_attr_ds(ref_df, cur_df, nl=5, output=False, location=None) + if t != e: + return errors == 1 if cols_present: for i in range(len(df_ref)): diff --git a/util/fof_utils.py b/util/fof_utils.py index 94a78256..ed75b36a 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -60,15 +60,14 @@ def split_feedback_dataset(ds): ds = ds.assign_coords(dict(zip(ds.coords, [ds[coord] for coord in ds.coords]))) observation_variables = get_observation_variables(ds) + observation_variables.append("veri_data") ds_obs = ds[observation_variables] sort_keys_obs = ["lat", "lon", "statid", "varno", "level", "time_nomi"] - observation_variables.append("veri_data") - ds_veri = ds[observation_variables] ds_obs_sorted = ds_obs.sortby(sort_keys_obs) - ds_veri_sorted = ds_veri.sortby(sort_keys_obs) - return ds_report_sorted, ds_obs_sorted, ds_veri_sorted + + return ds_report_sorted, ds_obs_sorted def compare_arrays(arr1, arr2, var_name): From aac573d1decfd4f780c11bed1006d251a344686b Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Wed, 3 Dec 2025 14:09:14 +0100 Subject: [PATCH 03/41] first version of including veri_data in fof-compare --- engine/fof_compare.py | 8 ++++++-- util/dataframe_ops.py | 8 ++++---- util/fof_utils.py | 23 +++++++++++++++++++---- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index ae47e99c..0b12a5fc 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -46,8 +46,12 @@ default=None, help="If specified, location where to save the CSV file with the differences.", ) +@click.option( + "--tol", + default=10e-12, +) def fof_compare( - file1, file2, print_lines, lines, output, location + file1, file2, print_lines, lines, output, location, tol ): # pylint: disable=too-many-positional-arguments if not primary_check(file1, file2): @@ -71,7 +75,7 @@ def fof_compare( (ds_reports1_sorted, ds_reports2_sorted), (ds_obs1_sorted, ds_obs2_sorted), ]: - t, e = compare_var_and_attr_ds(ds1, ds2, nl, output, location) + t, e = compare_var_and_attr_ds(ds1, ds2, nl, output, location, tol) total_elements_all += t equal_elements_all += e diff --git a/util/dataframe_ops.py b/util/dataframe_ops.py index a01ecd61..e740d57d 100644 --- a/util/dataframe_ops.py +++ b/util/dataframe_ops.py @@ -325,8 +325,8 @@ def check_file_with_tolerances( df_cur_rep, df_cur_veri = parse_probtest_fof(input_file_cur.path) - df_ref = {"rep": df_ref_rep, "veri": df_ref_veri} - df_cur = {"rep": df_cur_rep, "veri": df_cur_veri} + df_ref = {"reports": df_ref_rep, "observation": df_ref_veri} + df_cur = {"reports": df_cur_rep, "observation": df_cur_veri} errors = multiple_solutions_from_dict(df_ref, df_cur, rules) @@ -354,8 +354,8 @@ def check_file_with_tolerances( ) if input_file_ref.file_type == FileType.FOF: - df_ref = df_ref["veri"]["veri_data"] - df_cur = df_cur["veri"]["veri_data"] + df_ref = df_ref["observation"]["veri_data"] + df_cur = df_cur["observation"]["veri_data"] df_tol.columns = ["veri_data"] # compute relative difference diff --git a/util/fof_utils.py b/util/fof_utils.py index ed75b36a..befca436 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -10,6 +10,8 @@ import xarray as xr + + def get_report_variables(ds): """ Get variable names of reports. @@ -70,13 +72,26 @@ def split_feedback_dataset(ds): return ds_report_sorted, ds_obs_sorted -def compare_arrays(arr1, arr2, var_name): +def compare_arrays(arr1, arr2, var_name, tol): """ Comparison of two arrays containing the values of the same variable. If not the same, it tells you in percentage terms how different they are. """ total = arr1.size + if var_name == "veri_data": + from util.dataframe_ops import compute_rel_diff_dataframe, check_variable + diff = compute_rel_diff_dataframe(pd.DataFrame(arr1), pd.DataFrame(arr2)) + df_new = pd.DataFrame(pd.DataFrame({'tol':[tol]*diff.size})) + + out, err, tol = check_variable(diff, df_new) + if out: + return total,total, 0 + else: + equal = total - len(err) + return total, equal, err + + if np.array_equal(arr1, arr2): equal = total diff = np.array([]) @@ -206,7 +221,7 @@ def write_different_size(output, nl, path_name, var, sizes): ) -def compare_var_and_attr_ds(ds1, ds2, nl, output, location): +def compare_var_and_attr_ds(ds1, ds2, nl, output, location, tol): """ Variable by variable and attribute by attribute, comparison of the two files. @@ -232,7 +247,7 @@ def compare_var_and_attr_ds(ds1, ds2, nl, output, location): arr2 = fill_nans_for_float32(ds2[var].values) if arr1.size == arr2.size: - t, e, diff = compare_arrays(arr1, arr2, var) + t, e, diff = compare_arrays(arr1, arr2, var, tol) if output: write_lines(ds1, ds2, diff, path_name) @@ -252,7 +267,7 @@ def compare_var_and_attr_ds(ds1, ds2, nl, output, location): arr1 = np.array(ds1.attrs[var], dtype=object) arr2 = np.array(ds2.attrs[var], dtype=object) if arr1.size == arr2.size: - t, e, diff = compare_arrays(arr1, arr2, var) + t, e, diff = compare_arrays(arr1, arr2, var, tol) if output: write_lines(ds1, ds2, diff, path_name) From a08b99b578db60b8c9ce89b6ab8e373f0b55e27a Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Fri, 12 Dec 2025 10:22:45 +0100 Subject: [PATCH 04/41] adapt probtest to ekf --- engine/fof_compare.py | 1 + 1 file changed, 1 insertion(+) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index 0b12a5fc..c8eab1fa 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -78,6 +78,7 @@ def fof_compare( t, e = compare_var_and_attr_ds(ds1, ds2, nl, output, location, tol) total_elements_all += t equal_elements_all += e + hh if total_elements_all > 0: percent_equal_all = (equal_elements_all / total_elements_all) * 100 From 357534d1ce3f1340ce3e1b10ee57cfd4ebc545ea Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Fri, 12 Dec 2025 17:51:03 +0100 Subject: [PATCH 05/41] first complete version of this PR --- engine/fof_compare.py | 1 - tests/engine/test_check.py | 7 +----- tests/util/test_fof_utils.py | 20 +++++++++------- util/dataframe_ops.py | 46 +++++++++++++++++++++++++----------- util/fof_utils.py | 31 +++++++++++++----------- 5 files changed, 62 insertions(+), 43 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index c8eab1fa..0b12a5fc 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -78,7 +78,6 @@ def fof_compare( t, e = compare_var_and_attr_ds(ds1, ds2, nl, output, location, tol) total_elements_all += t equal_elements_all += e - hh if total_elements_all > 0: percent_equal_all = (equal_elements_all / total_elements_all) * 100 diff --git a/tests/engine/test_check.py b/tests/engine/test_check.py index f323645b..311f9a02 100644 --- a/tests/engine/test_check.py +++ b/tests/engine/test_check.py @@ -148,12 +148,7 @@ def test_check_cli_fof(fof_datasets): df1, df2, tol_large, tol_small = fof_datasets - rules = { - "check": [13, 18, 32], - "state": [1, 5, 7, 9], - "r_check": [13, 18, 32], - "r_state": [1, 5, 7, 9], - } + rules = "" runner = CliRunner() result = runner.invoke( diff --git a/tests/util/test_fof_utils.py b/tests/util/test_fof_utils.py index 87d77b42..7620fdb4 100644 --- a/tests/util/test_fof_utils.py +++ b/tests/util/test_fof_utils.py @@ -123,14 +123,14 @@ def fixture_sample_dataset_veri(sample_dataset_fof): return ds_veri -def test_split_report(ds1, ds_report, ds_obs, ds_veri): +def test_split_report(ds1, ds_report, ds_obs): """ Test that the dataset is correctly split into reports, observations and veri data according to their dimensions. """ - reports, observations, veri_data = split_feedback_dataset(ds1) + reports, observations = split_feedback_dataset(ds1) - assert reports == ds_report and observations == ds_obs and veri_data == ds_veri + assert reports == ds_report and observations == ds_obs @pytest.fixture(name="arr1", scope="function") @@ -164,8 +164,10 @@ def test_compare_array_equal(arr1, arr2, arr1_nan, arr2_nan): - they have the same content - they have nan values in the same positions """ - total, equal, diff = compare_arrays(arr1, arr2, "var_name") - total_nan, equal_nan, diff_nan = compare_arrays(arr1_nan, arr2_nan, "var_name") + total, equal, diff = compare_arrays(arr1, arr2, "var_name", tol=1e-12) + total_nan, equal_nan, diff_nan = compare_arrays( + arr1_nan, arr2_nan, "var_name", tol=1e-12 + ) assert (total, equal, total_nan, equal_nan, diff.size, diff_nan.size) == ( 5, @@ -181,7 +183,7 @@ def test_compare_array_diff(arr1, arr3): """ Test that if I compare two different arrays I get the number of total and equal vales and the number of the position where values are different.""" - total, equal, diff = compare_arrays(arr1, arr3, "var_name") + total, equal, diff = compare_arrays(arr1, arr3, "var_name", tol=1e-12) assert (total, equal, diff.tolist()) == (5, 3, [0, 3]) @@ -320,9 +322,11 @@ def test_compare_var_and_attr_ds(ds1, ds2, tmp_path): file_path = tmp_path / "differences.csv" total1, equal1 = compare_var_and_attr_ds( - ds1, ds2, nl=0, output=True, location=file_path + ds1, ds2, nl=0, output=True, location=file_path, tol=1e-12 + ) + total2, equal2 = compare_var_and_attr_ds( + ds1, ds2, nl=4, output=True, location=None, tol=1e-12 ) - total2, equal2 = compare_var_and_attr_ds(ds1, ds2, nl=4, output=True, location=None) assert (total1, equal1) == (104, 103) assert (total2, equal2) == (104, 103) diff --git a/util/dataframe_ops.py b/util/dataframe_ops.py index e740d57d..682ff03c 100644 --- a/util/dataframe_ops.py +++ b/util/dataframe_ops.py @@ -15,7 +15,7 @@ from util.constants import CHECK_THRESHOLD, compute_statistics from util.file_system import file_names_from_pattern -from util.fof_utils import split_feedback_dataset, compare_var_and_attr_ds +from util.fof_utils import compare_var_and_attr_ds, split_feedback_dataset from util.log_handler import logger from util.model_output_parser import model_output_parser from util.utils import FileType @@ -72,7 +72,9 @@ def parse_probtest_stats(path, index_col=None): def parse_probtest_fof(path): ds = xr.open_dataset(path) ds_report, ds_veri = split_feedback_dataset(ds) - df_report, df_veri = (pd.DataFrame(d.to_dataframe().reset_index()) for d in (ds_report, ds_veri)) + df_report, df_veri = ( + pd.DataFrame(d.to_dataframe().reset_index()) for d in (ds_report, ds_veri) + ) return df_report, df_veri @@ -331,7 +333,13 @@ def check_file_with_tolerances( errors = multiple_solutions_from_dict(df_ref, df_cur, rules) if errors: - logger.error("RESULT: check FAILED") + logger.error("RESULT: check FAILED due to rules") + sys.exit(1) + + errors = check_reports_observations(df_ref, df_cur) + + if errors: + logger.error("RESULT: check FAILED due to reports or observations") sys.exit(1) else: @@ -410,17 +418,6 @@ def multiple_solutions_from_dict(df_ref, df_cur, rules): ] errors = [] - for key in df_ref.keys(): - ref_df = df_ref[key] - cur_df = df_cur[key] - - ref_df = ref_df.to_xarray() - cur_df = cur_df.to_xarray() - - t, e = compare_var_and_attr_ds(ref_df, cur_df, nl=5, output=False, location=None) - if t != e: - return errors == 1 - if cols_present: for i in range(len(df_ref)): row1 = df_ref.iloc[i] @@ -456,3 +453,24 @@ def multiple_solutions_from_dict(df_ref, df_cur, rules): return errors return [] + + +def check_reports_observations(df_ref, df_cur): + """ + This function compares two DataFrames row by row and column by column + and check that the reports and observations are the same. + """ + + errors = [] + for key in df_ref.keys(): + ref_df = df_ref[key] + cur_df = df_cur[key] + + ref_df = ref_df.to_xarray() + cur_df = cur_df.to_xarray() + + t, e = compare_var_and_attr_ds( + ref_df, cur_df, nl=0, output=False, location=None, tol=0 + ) + if t != e: + return errors == 1 diff --git a/util/fof_utils.py b/util/fof_utils.py index befca436..61281532 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -9,7 +9,7 @@ import pandas as pd import xarray as xr - +from util.constants import CHECK_THRESHOLD def get_report_variables(ds): @@ -68,7 +68,6 @@ def split_feedback_dataset(ds): sort_keys_obs = ["lat", "lon", "statid", "varno", "level", "time_nomi"] ds_obs_sorted = ds_obs.sortby(sort_keys_obs) - return ds_report_sorted, ds_obs_sorted @@ -80,17 +79,20 @@ def compare_arrays(arr1, arr2, var_name, tol): total = arr1.size if var_name == "veri_data": - from util.dataframe_ops import compute_rel_diff_dataframe, check_variable - diff = compute_rel_diff_dataframe(pd.DataFrame(arr1), pd.DataFrame(arr2)) - df_new = pd.DataFrame(pd.DataFrame({'tol':[tol]*diff.size})) + diff_rel = np.abs((arr1 - arr2) / (1.0 + np.abs(arr1))) + diff_rel_df = pd.DataFrame(diff_rel) - out, err, tol = check_variable(diff, df_new) - if out: - return total,total, 0 - else: - equal = total - len(err) - return total, equal, err + diff = diff_rel_df - tol + selector = (diff > CHECK_THRESHOLD).any(axis=1) + + out = (~selector).all() + diff_err = diff.index[selector].to_numpy() + + if out: + return total, total, np.array([]) + equal = total - len(diff_err) + return total, equal, diff_err if np.array_equal(arr1, arr2): equal = total @@ -112,8 +114,7 @@ def compare_arrays(arr1, arr2, var_name, tol): f"Differences in '{var_name}': {percent:.2f}% equal. " f"{total} total entries for this variable" ) - diff_idx = np.where(~mask_equal.ravel())[0] - diff = diff_idx + diff = np.where(~mask_equal.ravel())[0] return total, equal, diff @@ -221,7 +222,9 @@ def write_different_size(output, nl, path_name, var, sizes): ) -def compare_var_and_attr_ds(ds1, ds2, nl, output, location, tol): +def compare_var_and_attr_ds( + ds1, ds2, nl, output, location, tol +): # pylint: disable=too-many-positional-arguments """ Variable by variable and attribute by attribute, comparison of the two files. From 43344d32918e69da47b971483c5754f114e4dab0 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Thu, 8 Jan 2026 11:50:55 +0100 Subject: [PATCH 06/41] solve tests failing --- util/dataframe_ops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/dataframe_ops.py b/util/dataframe_ops.py index 85e46edb..b5a0fb12 100644 --- a/util/dataframe_ops.py +++ b/util/dataframe_ops.py @@ -471,7 +471,7 @@ def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules): cur_df_xr = cur_df[cols_other].to_xarray() t, e = compare_var_and_attr_ds( - ref_df_xr, cur_df_xr, nl=5, output=False, location=None + ref_df_xr, cur_df_xr, nl=5, output=False, location=None, tol=0 ) if t != e: return errors == 1 From e4faee0136e826df9f4f46ed8471e2be5d03a3b2 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Mon, 12 Jan 2026 09:19:44 +0100 Subject: [PATCH 07/41] integration first part of comments --- util/fof_utils.py | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/util/fof_utils.py b/util/fof_utils.py index 61281532..180ff204 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -4,6 +4,7 @@ import os import shutil +import re import numpy as np import pandas as pd @@ -209,17 +210,23 @@ def write_lines(ds1, ds2, diff, path_name): def write_different_size(output, nl, path_name, var, sizes): + """ + This function appends a message to a file (and optionally prints it) warning + that a given variable cannot be compared because two datasets have different + lengths. The message is written only if output is enabled, and printed to the + console if nl is not zero. + """ if output: with open(path_name, "a", encoding="utf-8") as f: f.write( f"variable : {var} -> datasets have different lengths " f"({sizes[0]} vs. {sizes[1]} ), comparison not possible" + "\n" ) - if nl != 0: - print( - f"\033[1mvar\033[0m : {var} -> datasets have different lengths " - f"({sizes[0]} vs. {sizes[1]} ), comparison not possible" - ) + else: + print( + f"\033[1mvar\033[0m : {var} -> datasets have different lengths " + f"({sizes[0]} vs. {sizes[1]} ), comparison not possible" + ) def compare_var_and_attr_ds( @@ -291,12 +298,16 @@ def compare_var_and_attr_ds( def primary_check(file1, file2): """ - Test that the two files are of the same type. + Check if two files are of the observation type, ignoring timestamp differences. + The check includes the prefix, the observation type and the ensemble suffix if + present. """ - name1 = os.path.basename(file1) - name2 = os.path.basename(file2) - - name1_core = name1.replace("fof", "").replace(".nc", "") - name2_core = name2.replace("fof", "").replace(".nc", "") - - return name1_core == name2_core + def core_name(path): + # Filename without directory + name = os.path.basename(path) + # Remove extension + name = os.path.splitext(name)[0] + # Remove timestamp + return re.sub(r'_(\d{14})(?=(_ens\d+)?$)', '', name) + + return core_name(file1) == core_name(file2) From 17f49acaa3c55fd16750e9884df4073dffef206c Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Mon, 12 Jan 2026 09:45:34 +0100 Subject: [PATCH 08/41] improve write_differences function --- util/fof_utils.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/util/fof_utils.py b/util/fof_utils.py index 180ff204..f8a7ce19 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -209,13 +209,15 @@ def write_lines(ds1, ds2, diff, path_name): f.write(f"diff : {row_diff}" + "\n") -def write_different_size(output, nl, path_name, var, sizes): +def write_different_size(output, nl, var, sizes, path_name=None): """ This function appends a message to a file (and optionally prints it) warning that a given variable cannot be compared because two datasets have different lengths. The message is written only if output is enabled, and printed to the console if nl is not zero. """ + #print(sizes) + print(var) if output: with open(path_name, "a", encoding="utf-8") as f: f.write( @@ -239,6 +241,7 @@ def compare_var_and_attr_ds( total_all, equal_all = 0, 0 list_to_skip = ["source", "i_body", "l_body"] + path_name = "" if output: if location: @@ -268,7 +271,7 @@ def compare_var_and_attr_ds( else: t, e = max(arr1.size, arr2.size), 0 - write_different_size(output, nl, path_name, var, [arr1.size, arr2.size]) + write_different_size(output, nl, var, [arr1.size, arr2.size], path_name) total_all += t equal_all += e @@ -288,7 +291,7 @@ def compare_var_and_attr_ds( else: t, e = max(arr1.size, arr2.size), 0 - write_different_size(output, nl, path_name, var, [arr1.size, arr2.size]) + write_different_size(output, nl, var, [arr1.size, arr2.size], path_name) total_all += t equal_all += e From 9560e94233c805b24f9ba2ebb47ed08bfff92330 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Mon, 12 Jan 2026 10:00:02 +0100 Subject: [PATCH 09/41] solve pylint --- engine/fof_compare.py | 2 +- util/fof_utils.py | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index 0b12a5fc..73e39fe5 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -48,7 +48,7 @@ ) @click.option( "--tol", - default=10e-12, + default=1e-12, ) def fof_compare( file1, file2, print_lines, lines, output, location, tol diff --git a/util/fof_utils.py b/util/fof_utils.py index f8a7ce19..1d4ab28c 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -3,8 +3,8 @@ """ import os -import shutil import re +import shutil import numpy as np import pandas as pd @@ -209,14 +209,14 @@ def write_lines(ds1, ds2, diff, path_name): f.write(f"diff : {row_diff}" + "\n") -def write_different_size(output, nl, var, sizes, path_name=None): +def write_different_size(output, var, sizes, path_name=None): """ This function appends a message to a file (and optionally prints it) warning that a given variable cannot be compared because two datasets have different lengths. The message is written only if output is enabled, and printed to the console if nl is not zero. """ - #print(sizes) + # print(sizes) print(var) if output: with open(path_name, "a", encoding="utf-8") as f: @@ -271,7 +271,7 @@ def compare_var_and_attr_ds( else: t, e = max(arr1.size, arr2.size), 0 - write_different_size(output, nl, var, [arr1.size, arr2.size], path_name) + write_different_size(output, var, [arr1.size, arr2.size], path_name) total_all += t equal_all += e @@ -291,7 +291,7 @@ def compare_var_and_attr_ds( else: t, e = max(arr1.size, arr2.size), 0 - write_different_size(output, nl, var, [arr1.size, arr2.size], path_name) + write_different_size(output, var, [arr1.size, arr2.size], path_name) total_all += t equal_all += e @@ -305,12 +305,13 @@ def primary_check(file1, file2): The check includes the prefix, the observation type and the ensemble suffix if present. """ + def core_name(path): # Filename without directory name = os.path.basename(path) # Remove extension name = os.path.splitext(name)[0] # Remove timestamp - return re.sub(r'_(\d{14})(?=(_ens\d+)?$)', '', name) + return re.sub(r"_(\d{14})(?=(_ens\d+)?$)", "", name) return core_name(file1) == core_name(file2) From 33bf2656b99b03cc7223733516ffd92cf6aad52e Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Mon, 19 Jan 2026 09:18:44 +0100 Subject: [PATCH 10/41] first draft new version fof-compare --- engine/fof_compare.py | 45 +++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index 73e39fe5..73b051aa 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -8,12 +8,15 @@ import click import xarray as xr +import pandas as pd from util.fof_utils import ( compare_var_and_attr_ds, primary_check, split_feedback_dataset, ) +from util.dataframe_ops import check_file_with_tolerances, parse_check +from util.utils import FileInfo @click.command() @@ -58,36 +61,28 @@ def fof_compare( print("Different types of files") return - ds1 = xr.open_dataset(file1) - ds2 = xr.open_dataset(file2) + n_righe = xr.open_dataset(file1).sizes["d_body"] + tolerance_file = "tolerance_file.csv" - ds_reports1_sorted, ds_obs1_sorted = split_feedback_dataset(ds1) - ds_reports2_sorted, ds_obs2_sorted = split_feedback_dataset(ds2) + def create_tolerance_csv(n_righe, tol, tolerance_file_name): + df = pd.DataFrame( + {"tolerance": [tol] * n_righe} + ) + df.to_csv(tolerance_file_name) - total_elements_all, equal_elements_all = 0, 0 + create_tolerance_csv(n_righe, tol, tolerance_file) - if print_lines: - nl = lines - else: - nl = 0 + out, err, tol = check_file_with_tolerances( + tolerance_file, + FileInfo(file1), + FileInfo(file2), + factor=4, + rules="", + ) + # print(out) + # print(err) - for ds1, ds2 in [ - (ds_reports1_sorted, ds_reports2_sorted), - (ds_obs1_sorted, ds_obs2_sorted), - ]: - t, e = compare_var_and_attr_ds(ds1, ds2, nl, output, location, tol) - total_elements_all += t - equal_elements_all += e - if total_elements_all > 0: - percent_equal_all = (equal_elements_all / total_elements_all) * 100 - percent_diff_all = 100 - percent_equal_all - print(f"Total percentage of equality: {percent_equal_all:.2f}%") - print(f"Total percentage of difference: {percent_diff_all:.2f}%") - if equal_elements_all == total_elements_all: - print("Files are consistent!") - else: - print("Files are NOT consistent!") if __name__ == "__main__": From 704c0bd5d36c2e81f6720d77dc8f0f03ad9b8039 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Thu, 22 Jan 2026 15:52:40 +0100 Subject: [PATCH 11/41] make fof-comare more similar to check --- engine/fof_compare.py | 24 ++++++++++++++---------- util/dataframe_ops.py | 17 +++++++++++++---- util/fof_utils.py | 28 ++++++++++++++-------------- 3 files changed, 41 insertions(+), 28 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index 73b051aa..d5d9320d 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -57,20 +57,22 @@ def fof_compare( file1, file2, print_lines, lines, output, location, tol ): # pylint: disable=too-many-positional-arguments - if not primary_check(file1, file2): - print("Different types of files") - return + if print_lines: + nl = lines + else: + nl = 0 - n_righe = xr.open_dataset(file1).sizes["d_body"] + n_rows = xr.open_dataset(file1).sizes["d_body"] tolerance_file = "tolerance_file.csv" - def create_tolerance_csv(n_righe, tol, tolerance_file_name): + def create_tolerance_csv(n_rows, tol, tolerance_file_name): df = pd.DataFrame( - {"tolerance": [tol] * n_righe} + {"tolerance": [tol] * n_rows} ) df.to_csv(tolerance_file_name) - create_tolerance_csv(n_righe, tol, tolerance_file) + create_tolerance_csv(n_rows, tol, tolerance_file) + out, err, tol = check_file_with_tolerances( tolerance_file, @@ -78,10 +80,12 @@ def create_tolerance_csv(n_righe, tol, tolerance_file_name): FileInfo(file2), factor=4, rules="", + fof_compare_settings = [nl, output, location] ) - # print(out) - # print(err) - + if out: + print("Files are consistent!") + else: + print("Files are NOT consistent!") diff --git a/util/dataframe_ops.py b/util/dataframe_ops.py index b5a0fb12..b29e0866 100644 --- a/util/dataframe_ops.py +++ b/util/dataframe_ops.py @@ -318,7 +318,7 @@ def parse_check(tolerance_file_name, input_file_ref, input_file_cur, factor): def check_file_with_tolerances( - tolerance_file_name, input_file_ref, input_file_cur, factor, rules="" + tolerance_file_name, input_file_ref, input_file_cur, factor, rules="", fof_compare_settings=[] ): """ This function calculates the relative difference between the current file and @@ -340,9 +340,10 @@ def check_file_with_tolerances( ) if input_file_ref.file_type == FileType.FOF: - errors = check_multiple_solutions_from_dict(df_ref, df_cur, rules) + errors = check_multiple_solutions_from_dict(df_ref, df_cur, rules, fof_compare_settings) if errors: + logger.error("RESULT: check FAILED") sys.exit(1) @@ -438,7 +439,7 @@ def compare_cells(ref_df, cur_df, cols_present, rules_dict): return errors -def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules): +def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules, fof_compare_settings): """ This function compares two Python dictionaries—each containing DataFrames under the keys "reports" and "observation"—row by row and column by column, according @@ -469,9 +470,17 @@ def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules): if cols_other: ref_df_xr = ref_df[cols_other].to_xarray() cur_df_xr = cur_df[cols_other].to_xarray() + if fof_compare_settings: + nl = fof_compare_settings[0] + output = fof_compare_settings[1] + location = fof_compare_settings[2] + else: + nl = 0 + output = False + location = None t, e = compare_var_and_attr_ds( - ref_df_xr, cur_df_xr, nl=5, output=False, location=None, tol=0 + ref_df_xr, cur_df_xr, nl=nl, output=output, location=location ) if t != e: return errors == 1 diff --git a/util/fof_utils.py b/util/fof_utils.py index 1d4ab28c..b0ba0ed5 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -72,28 +72,28 @@ def split_feedback_dataset(ds): return ds_report_sorted, ds_obs_sorted -def compare_arrays(arr1, arr2, var_name, tol): +def compare_arrays(arr1, arr2, var_name): """ Comparison of two arrays containing the values of the same variable. If not the same, it tells you in percentage terms how different they are. """ total = arr1.size - if var_name == "veri_data": - diff_rel = np.abs((arr1 - arr2) / (1.0 + np.abs(arr1))) - diff_rel_df = pd.DataFrame(diff_rel) + # if var_name == "veri_data": + # diff_rel = np.abs((arr1 - arr2) / (1.0 + np.abs(arr1))) + # diff_rel_df = pd.DataFrame(diff_rel) - diff = diff_rel_df - tol + # diff = diff_rel_df - tol - selector = (diff > CHECK_THRESHOLD).any(axis=1) + # selector = (diff > CHECK_THRESHOLD).any(axis=1) - out = (~selector).all() - diff_err = diff.index[selector].to_numpy() + # out = (~selector).all() + # diff_err = diff.index[selector].to_numpy() - if out: - return total, total, np.array([]) - equal = total - len(diff_err) - return total, equal, diff_err + # if out: + # return total, total, np.array([]) + # equal = total - len(diff_err) + # return total, equal, diff_err if np.array_equal(arr1, arr2): equal = total @@ -232,7 +232,7 @@ def write_different_size(output, var, sizes, path_name=None): def compare_var_and_attr_ds( - ds1, ds2, nl, output, location, tol + ds1, ds2, nl, output, location ): # pylint: disable=too-many-positional-arguments """ Variable by variable and attribute by attribute, @@ -260,7 +260,7 @@ def compare_var_and_attr_ds( arr2 = fill_nans_for_float32(ds2[var].values) if arr1.size == arr2.size: - t, e, diff = compare_arrays(arr1, arr2, var, tol) + t, e, diff = compare_arrays(arr1, arr2, var) if output: write_lines(ds1, ds2, diff, path_name) From 5c6775168e5e7e38fee23fec9dbe1f1958d4d0ea Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Mon, 26 Jan 2026 10:40:09 +0100 Subject: [PATCH 12/41] log file for error --- engine/fof_compare.py | 67 ++---------- tests/util/test_fof_utils.py | 148 ++++++++------------------ util/dataframe_ops.py | 23 ++-- util/fof_utils.py | 200 +++++++++++------------------------ 4 files changed, 120 insertions(+), 318 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index d5d9320d..e5576d17 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -6,87 +6,40 @@ Veri data are not considered, only reports and observations are compared. """ +import os + import click import xarray as xr -import pandas as pd -from util.fof_utils import ( - compare_var_and_attr_ds, - primary_check, - split_feedback_dataset, -) -from util.dataframe_ops import check_file_with_tolerances, parse_check +from util.dataframe_ops import check_file_with_tolerances +from util.fof_utils import create_tolerance_csv from util.utils import FileInfo @click.command() @click.argument("file1", type=click.Path(exists=True)) @click.argument("file2", type=click.Path(exists=True)) -@click.option( - "--print-lines", - is_flag=True, - help="Prints the lines where there are differences. " - "If --lines is not specified, then the first 10 " - "differences per variables are shown.", -) -@click.option( - "--lines", - "-n", - default=10, - help="Option to specify how many lines to print " "with the --print-lines option", -) -@click.option( - "--output", - "-o", - is_flag=True, - help="Option to save differences in a CSV file. " - "If the location is not specified, the file " - "is saved in the same location as this code. ", -) -@click.option( - "--location", - "-l", - default=None, - help="If specified, location where to save the CSV file with the differences.", -) @click.option( "--tol", default=1e-12, ) -def fof_compare( - file1, file2, print_lines, lines, output, location, tol -): # pylint: disable=too-many-positional-arguments - - if print_lines: - nl = lines - else: - nl = 0 +def fof_compare(file1, file2, tol): n_rows = xr.open_dataset(file1).sizes["d_body"] tolerance_file = "tolerance_file.csv" - def create_tolerance_csv(n_rows, tol, tolerance_file_name): - df = pd.DataFrame( - {"tolerance": [tol] * n_rows} - ) - df.to_csv(tolerance_file_name) - create_tolerance_csv(n_rows, tol, tolerance_file) - - out, err, tol = check_file_with_tolerances( - tolerance_file, - FileInfo(file1), - FileInfo(file2), - factor=4, - rules="", - fof_compare_settings = [nl, output, location] - ) + out, _, _ = check_file_with_tolerances( + tolerance_file, FileInfo(file1), FileInfo(file2), factor=1, rules="" + ) if out: print("Files are consistent!") else: print("Files are NOT consistent!") + if os.path.exists(tolerance_file): + os.remove(tolerance_file) if __name__ == "__main__": diff --git a/tests/util/test_fof_utils.py b/tests/util/test_fof_utils.py index 9f7c91a8..407cdf79 100644 --- a/tests/util/test_fof_utils.py +++ b/tests/util/test_fof_utils.py @@ -5,17 +5,14 @@ import numpy as np import pytest -from util.fof_utils import ( +from util.fof_utils import ( # write_lines, clean_value, compare_arrays, compare_var_and_attr_ds, fill_nans_for_float32, get_observation_variables, get_report_variables, - primary_check, - print_entire_line, split_feedback_dataset, - write_lines, ) @@ -164,10 +161,8 @@ def test_compare_array_equal(arr1, arr2, arr1_nan, arr2_nan): - they have the same content - they have nan values in the same positions """ - total, equal, diff = compare_arrays(arr1, arr2, "var_name", tol=1e-12) - total_nan, equal_nan, diff_nan = compare_arrays( - arr1_nan, arr2_nan, "var_name", tol=1e-12 - ) + total, equal, diff = compare_arrays(arr1, arr2, "var_name") + total_nan, equal_nan, diff_nan = compare_arrays(arr1_nan, arr2_nan, "var_name") assert (total, equal, total_nan, equal_nan, diff.size, diff_nan.size) == ( 5, @@ -183,7 +178,7 @@ def test_compare_array_diff(arr1, arr3): """ Test that if I compare two different arrays I get the number of total and equal vales and the number of the position where values are different.""" - total, equal, diff = compare_arrays(arr1, arr3, "var_name", tol=1e-12) + total, equal, diff = compare_arrays(arr1, arr3, "var_name") assert (total, equal, diff.tolist()) == (5, 3, [0, 3]) @@ -239,94 +234,49 @@ def fixture_sample_dataset_2(sample_dataset_fof): return data -def test_print_entire_line(ds1, ds2, capsys): - """ - Test that in case of differences, these are printed correctly. - """ - diff = np.array([5]) - print_entire_line(ds1, ds2, diff) - captured = capsys.readouterr() - output = captured.out.splitlines() - - assert output[0] == ( - "\x1b[1mid\x1b[0m : d_hdr |d_body |lat |lon " - "|varno |statid |time_nomi |codetype |level " - "|l_body |i_body |veri_data |obs |bcor " - "|level_typ |level_sig |state |flags |check " - "|e_o |qual |plevel " - ) - assert output[1] == ( - "\x1b[1mref\x1b[0m : 0 |5 |1 |5 " - "|4 |a |0 |5 |750 " - "|1 |1 |78 |0.155 |0.969 " - "|0.524 |0.366 |1 |9 |13 " - "|0.52 |0.138 |0.755 " - ) - assert output[2] == ( - "\x1b[1mcur\x1b[0m : 0 |5 |1 |5 " - "|4 |a |0 |5 |750 " - "|1 |1 |78 |0.155 |0.969 " - "|0.524 |0.366 |1 |9 |13 " - "|0.52 |0.138 |0.755 " - ) - assert output[3] == ( - "\x1b[1mdiff\x1b[0m: 0 |0 |0 |0 " - "|0 |nan |0 |0 |0 " - "|0 |0 |0 |0.0 |0.0 " - "|0.0 |0.0 |0 |0 |0 " - "|0.0 |0.0 |0.0 " - ) - - -def test_write_lines(ds1, ds2, tmp_path): - """ - Test that if there are any differences, they are saved in a separate csv file. - """ - file_path = tmp_path / "differences.csv" - diff = np.array([5]) - write_lines(ds1, ds2, diff, file_path) - - content = file_path.read_text(encoding="utf-8") - - expected = ( - "id : d_hdr |d_body |lat |lon |varno " - "|statid |time_nomi |codetype |level |l_body " - "|i_body |veri_data |obs |bcor |level_typ " - "|level_sig |state |flags |check |e_o " - "|qual |plevel \n" - "ref : 0 |5 |1 |5 |4 " - "|a |0 |5 |750 |1 " - "|1 |78 |0.155 |0.969 |0.524 " - "|0.366 |1 |9 |13 |0.52 " - "|0.138 |0.755 \n" - "cur : 0 |5 |1 |5 |4 " - "|a |0 |5 |750 |1 " - "|1 |78 |0.155 |0.969 |0.524 " - "|0.366 |1 |9 |13 |0.52 " - "|0.138 |0.755 \n" - "diff : 0 |0 |0 |0 |0 " - "|nan |0 |0 |0 |0 " - "|0 |0 |0.0 |0.0 |0.0 " - "|0.0 |0 |0 |0 |0.0 " - "|0.0 |0.0 \n" - ) - assert content == expected - - -def test_compare_var_and_attr_ds(ds1, ds2, tmp_path): +# def test_write_lines(ds1, ds2, tmp_path): +# """ +# Test that if there are any differences, they are saved in a separate csv file. +# """ +# file_path = tmp_path / "differences.csv" +# diff = np.array([5]) +# write_lines(ds1, ds2, diff, file_path) + +# content = file_path.read_text(encoding="utf-8") + +# expected = ( +# "id : d_hdr |d_body |lat |lon |varno " +# "|statid |time_nomi |codetype |level |l_body " +# "|i_body |veri_data |obs |bcor |level_typ " +# "|level_sig |state |flags |check |e_o " +# "|qual |plevel \n" +# "ref : 0 |5 |1 |5 |4 " +# "|a |0 |5 |750 |1 " +# "|1 |78 |0.155 |0.969 |0.524 " +# "|0.366 |1 |9 |13 |0.52 " +# "|0.138 |0.755 \n" +# "cur : 0 |5 |1 |5 |4 " +# "|a |0 |5 |750 |1 " +# "|1 |78 |0.155 |0.969 |0.524 " +# "|0.366 |1 |9 |13 |0.52 " +# "|0.138 |0.755 \n" +# "diff : 0 |0 |0 |0 |0 " +# "|nan |0 |0 |0 |0 " +# "|0 |0 |0.0 |0.0 |0.0 " +# "|0.0 |0 |0 |0 |0.0 " +# "|0.0 |0.0 \n" +# ) +# assert content == expected + + +def test_compare_var_and_attr_ds(ds1, ds2): """ Test that, given two datasets, returns the number of elements in which the variables are the same and in which they differ. """ - file_path = tmp_path / "differences.csv" - - total1, equal1 = compare_var_and_attr_ds( - ds1, ds2, nl=0, output=True, location=file_path, tol=1e-12 - ) - total2, equal2 = compare_var_and_attr_ds( - ds1, ds2, nl=4, output=True, location=None, tol=1e-12 - ) + total1, equal1 = compare_var_and_attr_ds(ds1, ds2) + total2, equal2 = compare_var_and_attr_ds(ds1, ds2) assert (total1, equal1) == (104, 103) assert (total2, equal2) == (104, 103) @@ -341,17 +291,3 @@ def fixture_sample_dataset_3(sample_dataset_fof): ds.attrs["plevel"] = np.array([0.374, 0.950, 0.731, 0.598, 0.156]) return ds - - -def test_primary_check(tmp_path): - """ - Note that if two fof files are not of the same type, then the primary_check fails. - """ - test_fof1 = tmp_path / "fofAIREP.nc" - test_fof2 = tmp_path / "fofAIREP.nc" - test_fof3 = tmp_path / "fofPILOT.nc" - - assert primary_check(test_fof1, test_fof2) - - false_result = primary_check(test_fof1, test_fof3) - assert false_result is False diff --git a/util/dataframe_ops.py b/util/dataframe_ops.py index b29e0866..f5b8ea96 100644 --- a/util/dataframe_ops.py +++ b/util/dataframe_ops.py @@ -318,7 +318,7 @@ def parse_check(tolerance_file_name, input_file_ref, input_file_cur, factor): def check_file_with_tolerances( - tolerance_file_name, input_file_ref, input_file_cur, factor, rules="", fof_compare_settings=[] + tolerance_file_name, input_file_ref, input_file_cur, factor, rules="" ): """ This function calculates the relative difference between the current file and @@ -331,7 +331,7 @@ def check_file_with_tolerances( if input_file_ref.file_type != input_file_cur.file_type: logger.critical( "The current and the reference files are not of the same type; " - "it is impossible to calculate the tolerances. Abort." + "it is impossible to compare them. Abort." ) sys.exit(1) @@ -340,10 +340,9 @@ def check_file_with_tolerances( ) if input_file_ref.file_type == FileType.FOF: - errors = check_multiple_solutions_from_dict(df_ref, df_cur, rules, fof_compare_settings) + errors = check_multiple_solutions_from_dict(df_ref, df_cur, rules) if errors: - logger.error("RESULT: check FAILED") sys.exit(1) @@ -439,7 +438,7 @@ def compare_cells(ref_df, cur_df, cols_present, rules_dict): return errors -def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules, fof_compare_settings): +def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules): """ This function compares two Python dictionaries—each containing DataFrames under the keys "reports" and "observation"—row by row and column by column, according @@ -470,18 +469,8 @@ def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules, fof_compare_se if cols_other: ref_df_xr = ref_df[cols_other].to_xarray() cur_df_xr = cur_df[cols_other].to_xarray() - if fof_compare_settings: - nl = fof_compare_settings[0] - output = fof_compare_settings[1] - location = fof_compare_settings[2] - else: - nl = 0 - output = False - location = None - - t, e = compare_var_and_attr_ds( - ref_df_xr, cur_df_xr, nl=nl, output=output, location=location - ) + + t, e = compare_var_and_attr_ds(ref_df_xr, cur_df_xr) if t != e: return errors == 1 diff --git a/util/fof_utils.py b/util/fof_utils.py index b0ba0ed5..b8022c47 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -2,16 +2,12 @@ This module contains functions for handling fof files """ -import os -import re -import shutil +import logging import numpy as np import pandas as pd import xarray as xr -from util.constants import CHECK_THRESHOLD - def get_report_variables(ds): """ @@ -79,22 +75,6 @@ def compare_arrays(arr1, arr2, var_name): """ total = arr1.size - # if var_name == "veri_data": - # diff_rel = np.abs((arr1 - arr2) / (1.0 + np.abs(arr1))) - # diff_rel_df = pd.DataFrame(diff_rel) - - # diff = diff_rel_df - tol - - # selector = (diff > CHECK_THRESHOLD).any(axis=1) - - # out = (~selector).all() - # diff_err = diff.index[selector].to_numpy() - - # if out: - # return total, total, np.array([]) - # equal = total - len(diff_err) - # return total, equal, diff_err - if np.array_equal(arr1, arr2): equal = total diff = np.array([]) @@ -139,101 +119,68 @@ def clean_value(x): return str(x).rstrip(" '") -def print_entire_line(ds1, ds2, diff): - """ - If the specific option is called, this function print - the entire line in which differences are found. - """ - if diff.size > 0: - da1 = ds1.to_dataframe().reset_index() - da2 = ds2.to_dataframe().reset_index() - - for i in diff: - col_width = 13 - row1 = "|".join(f"{clean_value(x):<{col_width}}" for x in da1.loc[i]) +def write_lines(ds1, ds2, diff, logger): + if diff.size == 0: + return - row2 = "|".join(f"{clean_value(x):<{col_width}}" for x in da2.loc[i]) + da1 = ds1.to_dataframe().reset_index() + da2 = ds2.to_dataframe().reset_index() + col_width = 13 + index = "|".join(f"{str(x):<{col_width}}" for x in da1.columns) - diff_row = [] - for x, y in zip(da1.loc[i], da2.loc[i]): - if pd.api.types.is_number(x) and pd.api.types.is_number(y): - row_diff = x - y - else: - row_diff = "nan" + for i in diff: + row1 = "|".join(f"{clean_value(x):<{col_width}}" for x in da1.loc[i]) + row2 = "|".join(f"{clean_value(x):<{col_width}}" for x in da2.loc[i]) - diff_row.append(row_diff) - - row_diff = "|".join(f"{str(x):<{col_width}}" for x in diff_row) + diff_vals = [] + for x, y in zip(da1.loc[i], da2.loc[i]): + if pd.api.types.is_number(x) and pd.api.types.is_number(y): + diff_vals.append(x - y) + else: + diff_vals.append("nan") - index = "|".join(f"{str(x):<{col_width}}" for x in da1.columns) + row_diff = "|".join(f"{str(x):<{col_width}}" for x in diff_vals) - print(f"\033[1mid\033[0m : {index}") - print(f"\033[1mref\033[0m : {row1}") - print(f"\033[1mcur\033[0m : {row2}") - print(f"\033[1mdiff\033[0m: {row_diff}") - term_width = shutil.get_terminal_size().columns - print("-" * term_width) + logger.info(f"id : {index}") + logger.info(f"ref : {row1}") + logger.info(f"cur : {row2}") + logger.info(f"diff : {row_diff}") + logger.info("") -def write_lines(ds1, ds2, diff, path_name): +def write_different_size(var, size1, size2, logger): """ - If the specific option is called, this function save - the lines in which differences are found. + This function is triggered when the array sizes do not match and records + in the log file that a comparison is not possible. """ - if diff.size > 0: - da1 = ds1.to_dataframe().reset_index() - da2 = ds2.to_dataframe().reset_index() - col_width = 13 - index = "|".join(f"{str(x):<{col_width}}" for x in da1.columns) - for i in diff: - row1 = "|".join(f"{clean_value(x):<{col_width}}" for x in da1.loc[i]) + logger.info( + f"variable : {var} -> datasets have different lengths " + f"({size1} vs. {size2} ), comparison not possible" + "\n" + ) - row2 = "|".join(f"{clean_value(x):<{col_width}}" for x in da2.loc[i]) - diff_row = [] - for x, y in zip(da1.loc[i], da2.loc[i]): - if pd.api.types.is_number(x) and pd.api.types.is_number(y): - row_diff = x - y - else: - row_diff = "nan" - - diff_row.append(row_diff) - - row_diff = "|".join(f"{str(x):<{col_width}}" for x in diff_row) +def setup_logger(log_path): + """ + Sets up a logger that appends plain-text messages to the given log + file and returns the configured logger. + """ + logger = logging.getLogger("diff_logger") + logger.setLevel(logging.INFO) + logger.propagate = False - with open(path_name, "a", encoding="utf-8") as f: - f.write(f"id : {index}" + "\n") - f.write(f"ref : {row1}" + "\n") - f.write(f"cur : {row2}" + "\n") - f.write(f"diff : {row_diff}" + "\n") + if logger.handlers: + logger.handlers.clear() + handler = logging.FileHandler(log_path, mode="a", encoding="utf-8") + formatter = logging.Formatter("%(message)s") + handler.setFormatter(formatter) + logger.addHandler(handler) -def write_different_size(output, var, sizes, path_name=None): - """ - This function appends a message to a file (and optionally prints it) warning - that a given variable cannot be compared because two datasets have different - lengths. The message is written only if output is enabled, and printed to the - console if nl is not zero. - """ - # print(sizes) - print(var) - if output: - with open(path_name, "a", encoding="utf-8") as f: - f.write( - f"variable : {var} -> datasets have different lengths " - f"({sizes[0]} vs. {sizes[1]} ), comparison not possible" + "\n" - ) - else: - print( - f"\033[1mvar\033[0m : {var} -> datasets have different lengths " - f"({sizes[0]} vs. {sizes[1]} ), comparison not possible" - ) + return logger -def compare_var_and_attr_ds( - ds1, ds2, nl, output, location -): # pylint: disable=too-many-positional-arguments +def compare_var_and_attr_ds(ds1, ds2): """ Variable by variable and attribute by attribute, comparison of the two files. @@ -241,17 +188,13 @@ def compare_var_and_attr_ds( total_all, equal_all = 0, 0 list_to_skip = ["source", "i_body", "l_body"] - path_name = "" - if output: - if location: - path_name = location - else: - script_dir = os.path.dirname(os.path.abspath(__file__)) - path_name = os.path.join(script_dir, "differences.csv") + log_path = "/home/ghc/probtest/differences.log" + + with open(log_path, "w", encoding="utf-8") as f: + f.write("Differences\n\n") - with open(path_name, "w", encoding="utf-8") as f: - f.write("Differences\n") + logger = setup_logger(log_path) for var in set(ds1.data_vars).union(ds2.data_vars): if var in ds1.data_vars and var in ds2.data_vars and var not in list_to_skip: @@ -262,16 +205,11 @@ def compare_var_and_attr_ds( if arr1.size == arr2.size: t, e, diff = compare_arrays(arr1, arr2, var) - if output: - write_lines(ds1, ds2, diff, path_name) - - if nl != 0: - diff = diff[:nl] - print_entire_line(ds1, ds2, diff) + write_lines(ds1, ds2, diff, logger) else: t, e = max(arr1.size, arr2.size), 0 - write_different_size(output, var, [arr1.size, arr2.size], path_name) + write_different_size(var, arr1.size, arr2.size, logger) total_all += t equal_all += e @@ -280,18 +218,12 @@ def compare_var_and_attr_ds( arr1 = np.array(ds1.attrs[var], dtype=object) arr2 = np.array(ds2.attrs[var], dtype=object) if arr1.size == arr2.size: - t, e, diff = compare_arrays(arr1, arr2, var, tol) - - if output: - write_lines(ds1, ds2, diff, path_name) - - if nl != 0: - diff = diff[:nl] - print_entire_line(ds1, ds2, diff) + t, e, diff = compare_arrays(arr1, arr2, var) + write_lines(ds1, ds2, diff, logger) else: t, e = max(arr1.size, arr2.size), 0 - write_different_size(output, var, [arr1.size, arr2.size], path_name) + write_different_size(var, arr1.size, arr2.size, logger) total_all += t equal_all += e @@ -299,19 +231,11 @@ def compare_var_and_attr_ds( return total_all, equal_all -def primary_check(file1, file2): +def create_tolerance_csv(n_rows, tol, tolerance_file_name): """ - Check if two files are of the observation type, ignoring timestamp differences. - The check includes the prefix, the observation type and the ensemble suffix if - present. + This function generates a file with the same number of lines as the file being + analyzed, where each line contains the tolerances specified when fof-compare + is called. """ - - def core_name(path): - # Filename without directory - name = os.path.basename(path) - # Remove extension - name = os.path.splitext(name)[0] - # Remove timestamp - return re.sub(r"_(\d{14})(?=(_ens\d+)?$)", "", name) - - return core_name(file1) == core_name(file2) + df = pd.DataFrame({"tolerance": [tol] * n_rows}) + df.to_csv(tolerance_file_name) From 17e62565f7bf815ca8fda3586a34a7923a040982 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Mon, 26 Jan 2026 10:47:26 +0100 Subject: [PATCH 13/41] correct path --- util/fof_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/fof_utils.py b/util/fof_utils.py index b8022c47..a496f95f 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -189,7 +189,7 @@ def compare_var_and_attr_ds(ds1, ds2): total_all, equal_all = 0, 0 list_to_skip = ["source", "i_body", "l_body"] - log_path = "/home/ghc/probtest/differences.log" + log_path = "differences.log" with open(log_path, "w", encoding="utf-8") as f: f.write("Differences\n\n") From 8c276bbd9f1528aa84bb2cc230d319dfa41baf42 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Mon, 26 Jan 2026 18:37:39 +0100 Subject: [PATCH 14/41] in progress --- engine/fof_compare.py | 6 +++-- util/dataframe_ops.py | 15 +++++++----- util/fof_utils.py | 53 ++++++++++++++++++++++++++++--------------- 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index e5576d17..1e88aa97 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -12,7 +12,7 @@ import xarray as xr from util.dataframe_ops import check_file_with_tolerances -from util.fof_utils import create_tolerance_csv +from util.fof_utils import create_tolerance_csv, primary_check from util.utils import FileInfo @@ -30,13 +30,15 @@ def fof_compare(file1, file2, tol): create_tolerance_csv(n_rows, tol, tolerance_file) - out, _, _ = check_file_with_tolerances( + out, err, _ = check_file_with_tolerances( tolerance_file, FileInfo(file1), FileInfo(file2), factor=1, rules="" ) + if out: print("Files are consistent!") else: print("Files are NOT consistent!") + print(err) if os.path.exists(tolerance_file): os.remove(tolerance_file) diff --git a/util/dataframe_ops.py b/util/dataframe_ops.py index f5b8ea96..770bb839 100644 --- a/util/dataframe_ops.py +++ b/util/dataframe_ops.py @@ -8,6 +8,7 @@ import ast import sys import warnings +import os import numpy as np import pandas as pd @@ -335,17 +336,18 @@ def check_file_with_tolerances( ) sys.exit(1) + df_tol, df_ref, df_cur = parse_check( tolerance_file_name, input_file_ref, input_file_cur, factor ) if input_file_ref.file_type == FileType.FOF: - errors = check_multiple_solutions_from_dict(df_ref, df_cur, rules) + name_core = os.path.basename(input_file_ref.path).replace(".nc", "") + errors = check_multiple_solutions_from_dict(df_ref, df_cur, rules, name_core) if errors: logger.error("RESULT: check FAILED") - sys.exit(1) - + return False, 0, 0 else: # check if variables are available in reference file skip_test, df_ref, df_cur = check_intersection(df_ref, df_cur) @@ -438,7 +440,7 @@ def compare_cells(ref_df, cur_df, cols_present, rules_dict): return errors -def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules): +def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules, name_core): """ This function compares two Python dictionaries—each containing DataFrames under the keys "reports" and "observation"—row by row and column by column, according @@ -470,9 +472,10 @@ def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules): ref_df_xr = ref_df[cols_other].to_xarray() cur_df_xr = cur_df[cols_other].to_xarray() - t, e = compare_var_and_attr_ds(ref_df_xr, cur_df_xr) + t, e = compare_var_and_attr_ds(ref_df_xr, cur_df_xr, name_core) if t != e: - return errors == 1 + errors = True + return errors if cols_present: errors.extend(compare_cells(ref_df, cur_df, cols_present, rules_dict)) diff --git a/util/fof_utils.py b/util/fof_utils.py index a496f95f..ca693fa8 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -3,6 +3,7 @@ """ import logging +import os import numpy as np import pandas as pd @@ -115,14 +116,21 @@ def clean_value(x): alignment when printing the value. """ if isinstance(x, bytes): - return x.decode().rstrip(" '") + return x.decode("utf-8", errors="replace").rstrip(" '") return str(x).rstrip(" '") -def write_lines(ds1, ds2, diff, logger): +def write_lines(ds1, ds2, diff, log_path): if diff.size == 0: return + logger = setup_logger(log_path) + + if not hasattr(write_lines, "_header_written"): + with open(log_path, "w", encoding="utf-8") as f: + f.write("Differences\n\n") + write_lines._header_written = True + da1 = ds1.to_dataframe().reset_index() da2 = ds2.to_dataframe().reset_index() col_width = 13 @@ -153,7 +161,8 @@ def write_different_size(var, size1, size2, logger): This function is triggered when the array sizes do not match and records in the log file that a comparison is not possible. """ - + log_path = "error_fof.log" + logger = setup_logger(log_path) logger.info( f"variable : {var} -> datasets have different lengths " f"({size1} vs. {size2} ), comparison not possible" + "\n" @@ -180,21 +189,15 @@ def setup_logger(log_path): return logger -def compare_var_and_attr_ds(ds1, ds2): +def compare_var_and_attr_ds(ds1, ds2, name_core): """ Variable by variable and attribute by attribute, comparison of the two files. """ total_all, equal_all = 0, 0 - list_to_skip = ["source", "i_body", "l_body"] - - log_path = "differences.log" - - with open(log_path, "w", encoding="utf-8") as f: - f.write("Differences\n\n") - - logger = setup_logger(log_path) + list_to_skip = ["source", "i_body", "l_body", "veri_data"] + log_path = f"error_{name_core}.log" for var in set(ds1.data_vars).union(ds2.data_vars): if var in ds1.data_vars and var in ds2.data_vars and var not in list_to_skip: @@ -205,25 +208,26 @@ def compare_var_and_attr_ds(ds1, ds2): if arr1.size == arr2.size: t, e, diff = compare_arrays(arr1, arr2, var) - write_lines(ds1, ds2, diff, logger) + write_lines(ds1, ds2, diff, log_path) else: t, e = max(arr1.size, arr2.size), 0 - write_different_size(var, arr1.size, arr2.size, logger) + write_different_size(var, arr1.size, arr2.size, log_path) total_all += t equal_all += e if var in ds1.attrs and var in ds2.attrs and var not in list_to_skip: - arr1 = np.array(ds1.attrs[var], dtype=object) - arr2 = np.array(ds2.attrs[var], dtype=object) + + arr1 = fill_nans_for_float32(ds1[var].values) + arr2 = fill_nans_for_float32(ds2[var].values) if arr1.size == arr2.size: t, e, diff = compare_arrays(arr1, arr2, var) - write_lines(ds1, ds2, diff, logger) + write_lines(ds1, ds2, diff, log_path) else: t, e = max(arr1.size, arr2.size), 0 - write_different_size(var, arr1.size, arr2.size, logger) + write_different_size(var, arr1.size, arr2.size, log_path) total_all += t equal_all += e @@ -239,3 +243,16 @@ def create_tolerance_csv(n_rows, tol, tolerance_file_name): """ df = pd.DataFrame({"tolerance": [tol] * n_rows}) df.to_csv(tolerance_file_name) + + +def primary_check(file1, file2): + """ + Test that the two files are of the same type. + """ + name1 = os.path.basename(file1) + name2 = os.path.basename(file2) + + name1_core = name1.replace("fof", "").replace(".nc", "") + name2_core = name2.replace("fof", "").replace(".nc", "") + + return name1_core == name2_core, name1_core From 84fac35c81b092a2a943d2dfbbe46ef27351cd8d Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Tue, 27 Jan 2026 08:03:56 +0100 Subject: [PATCH 15/41] adapt to ekf --- util/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/utils.py b/util/utils.py index 713c01f0..0023d500 100644 --- a/util/utils.py +++ b/util/utils.py @@ -324,7 +324,7 @@ def __post_init__(self): name = self.path.lower() - if "fof" in name: + if "fof" in name or "ekf" in name: self.file_type = FileType.FOF return if "csv" in name or "stats" in name: From c320c838020dc101abce7f6db5dbf045292be0ba Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Tue, 27 Jan 2026 09:31:19 +0100 Subject: [PATCH 16/41] make code more efficient and clean --- engine/fof_compare.py | 2 ++ util/dataframe_ops.py | 69 +++++++++++++++++-------------------------- 2 files changed, 29 insertions(+), 42 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index 1e88aa97..8b4c725e 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -39,6 +39,8 @@ def fof_compare(file1, file2, tol): else: print("Files are NOT consistent!") print(err) + if err: + print("DDD") if os.path.exists(tolerance_file): os.remove(tolerance_file) diff --git a/util/dataframe_ops.py b/util/dataframe_ops.py index 770bb839..8c1e8298 100644 --- a/util/dataframe_ops.py +++ b/util/dataframe_ops.py @@ -399,15 +399,16 @@ def has_enough_data(dfs): def parse_rules(rules): - if isinstance(rules, str): - rules = rules.strip() - return ast.literal_eval(rules) if rules else {} if isinstance(rules, dict): return rules + + if isinstance(rules, str) and rules.strip(): + return ast.literal_eval(rules) + return {} -def compare_cells(ref_df, cur_df, cols_present, rules_dict): +def compare_cells_rules(ref_df, cur_df, cols, rules_dict): """ This function compares two DataFrames cell by cell for a selected set of columns. For each row and column, it ignores values that are equal or whose differences @@ -415,22 +416,21 @@ def compare_cells(ref_df, cur_df, cols_present, rules_dict): All other differences are collected and returned as a list of error descriptions. """ errors = [] - for i in range(len(ref_df)): - row1 = ref_df.iloc[i] - row2 = cur_df.iloc[i] - - for col in cols_present: - val1 = row1[col] - val2 = row2[col] + for row_idx, (row1, row2) in enumerate(zip(ref_df.itertuples(), cur_df.itertuples())): + for col in cols: + val1 = getattr(row1, col) + val2 = getattr(row2, col) if val1 == val2: continue - if val1 in rules_dict[col] and val2 in rules_dict[col]: + + allowed = rules_dict.get(col, []) + if val1 in allowed and val2 in allowed: continue errors.append( { - "row": i, + "row": row_idx, "column": col, "file1": val1, "file2": val2, @@ -452,43 +452,28 @@ def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules, name_core): rules_dict = parse_rules(rules) errors = [] - for key in dict_ref.keys(): - ref_df = dict_ref[key] + for key, ref_df in dict_ref.items(): cur_df = dict_cur[key] - cols_present = [ - col - for col in rules_dict.keys() - if col in ref_df.columns and col in cur_df.columns - ] + common_cols = set(ref_df.columns) & set(cur_df.columns) + rule_cols = set(rules_dict) - cols_other = [ - col - for col in ref_df.columns - if col not in cols_present and col in cur_df.columns - ] + cols_with_rules = common_cols & rule_cols + cols_without_rules = common_cols - cols_with_rules - if cols_other: - ref_df_xr = ref_df[cols_other].to_xarray() - cur_df_xr = cur_df[cols_other].to_xarray() - t, e = compare_var_and_attr_ds(ref_df_xr, cur_df_xr, name_core) + if cols_without_rules: + t, e = compare_var_and_attr_ds( + ref_df[list(cols_without_rules)].to_xarray(), + cur_df[list(cols_without_rules)].to_xarray(), + name_core, + ) if t != e: errors = True return errors - if cols_present: - errors.extend(compare_cells(ref_df, cur_df, cols_present, rules_dict)) - if errors: - logger.error("Errors found while comparing the files:") - for e in errors: - logger.error( - "Row %s - Column '%s': file1=%s, file2=%s → %s", - e["row"], - e["column"], - e["file1"], - e["file2"], - e["error"], - ) + if cols_with_rules: + errors.extend(compare_cells_rules(ref_df, cur_df, cols_without_rules, rules_dict)) + return errors From ec729f68948e34f3df18a0a360ea6ace8f5769e8 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Tue, 27 Jan 2026 10:25:09 +0100 Subject: [PATCH 17/41] add log file for tolerance --- engine/fof_compare.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index 8b4c725e..7bf8ee1d 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -38,9 +38,12 @@ def fof_compare(file1, file2, tol): print("Files are consistent!") else: print("Files are NOT consistent!") - print(err) if err: - print("DDD") + with open("error_tolerance.log", 'a') as f: + f.write(f"Differences") + f.write(err) + f.write("\nTolerance") + f.write(tol) if os.path.exists(tolerance_file): os.remove(tolerance_file) From ea37cfdf6858449e32226ae0b4019ec6c2ee3653 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Tue, 27 Jan 2026 11:22:38 +0100 Subject: [PATCH 18/41] write log tolerance --- engine/fof_compare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index 7bf8ee1d..68470989 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -30,7 +30,7 @@ def fof_compare(file1, file2, tol): create_tolerance_csv(n_rows, tol, tolerance_file) - out, err, _ = check_file_with_tolerances( + out, err, tol = check_file_with_tolerances( tolerance_file, FileInfo(file1), FileInfo(file2), factor=1, rules="" ) From 1c6d5da0c5433d7f73b04ac217fe02c78e4c101e Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Tue, 27 Jan 2026 13:42:47 +0100 Subject: [PATCH 19/41] change way to write log tolerance --- engine/fof_compare.py | 10 ++++------ util/fof_utils.py | 13 +++++++++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index 68470989..ff3c5b98 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -10,9 +10,10 @@ import click import xarray as xr +import numpy as np from util.dataframe_ops import check_file_with_tolerances -from util.fof_utils import create_tolerance_csv, primary_check +from util.fof_utils import create_tolerance_csv, primary_check, write_tolerance_log from util.utils import FileInfo @@ -39,11 +40,8 @@ def fof_compare(file1, file2, tol): else: print("Files are NOT consistent!") if err: - with open("error_tolerance.log", 'a') as f: - f.write(f"Differences") - f.write(err) - f.write("\nTolerance") - f.write(tol) + write_tolerance_log(err, tol, "tolerance_error.log") + if os.path.exists(tolerance_file): os.remove(tolerance_file) diff --git a/util/fof_utils.py b/util/fof_utils.py index ca693fa8..0a9f8a97 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -10,6 +10,7 @@ import xarray as xr + def get_report_variables(ds): """ Get variable names of reports. @@ -156,18 +157,26 @@ def write_lines(ds1, ds2, diff, log_path): logger.info("") -def write_different_size(var, size1, size2, logger): +def write_different_size(var, size1, size2, log_path): """ This function is triggered when the array sizes do not match and records in the log file that a comparison is not possible. """ - log_path = "error_fof.log" logger = setup_logger(log_path) logger.info( f"variable : {var} -> datasets have different lengths " f"({size1} vs. {size2} ), comparison not possible" + "\n" ) +def write_tolerance_log(err, tol, log_path): + """ + This function is triggered when the array sizes do not match and records + in the log file that a comparison is not possible. + """ + logger = setup_logger(log_path) + logger.info("Differences, veri_data outside of tolerance range") + logger.info(err) + def setup_logger(log_path): """ From 85b9c346be1b488104048da31fd3ada5f7719801 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Tue, 27 Jan 2026 14:18:53 +0100 Subject: [PATCH 20/41] change text write_tolerance_log --- util/fof_utils.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/util/fof_utils.py b/util/fof_utils.py index 0a9f8a97..de62c847 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -121,16 +121,16 @@ def clean_value(x): return str(x).rstrip(" '") -def write_lines(ds1, ds2, diff, log_path): +def write_lines_log(ds1, ds2, diff, log_path): if diff.size == 0: return logger = setup_logger(log_path) - if not hasattr(write_lines, "_header_written"): + if not hasattr(write_lines_log, "_header_written"): with open(log_path, "w", encoding="utf-8") as f: f.write("Differences\n\n") - write_lines._header_written = True + write_lines_log._header_written = True da1 = ds1.to_dataframe().reset_index() da2 = ds2.to_dataframe().reset_index() @@ -157,7 +157,7 @@ def write_lines(ds1, ds2, diff, log_path): logger.info("") -def write_different_size(var, size1, size2, log_path): +def write_different_size_log(var, size1, size2, log_path): """ This function is triggered when the array sizes do not match and records in the log file that a comparison is not possible. @@ -170,12 +170,14 @@ def write_different_size(var, size1, size2, log_path): def write_tolerance_log(err, tol, log_path): """ - This function is triggered when the array sizes do not match and records - in the log file that a comparison is not possible. + This function is triggered when the fof-compare step fails because the + veri_data fall outside the specified tolerance range. + Any resulting errors are recorded in a log file. """ logger = setup_logger(log_path) logger.info("Differences, veri_data outside of tolerance range") logger.info(err) + logger.info(tol) def setup_logger(log_path): @@ -217,11 +219,11 @@ def compare_var_and_attr_ds(ds1, ds2, name_core): if arr1.size == arr2.size: t, e, diff = compare_arrays(arr1, arr2, var) - write_lines(ds1, ds2, diff, log_path) + write_lines_log(ds1, ds2, diff, log_path) else: t, e = max(arr1.size, arr2.size), 0 - write_different_size(var, arr1.size, arr2.size, log_path) + write_different_size_log(var, arr1.size, arr2.size, log_path) total_all += t equal_all += e @@ -233,10 +235,10 @@ def compare_var_and_attr_ds(ds1, ds2, name_core): if arr1.size == arr2.size: t, e, diff = compare_arrays(arr1, arr2, var) - write_lines(ds1, ds2, diff, log_path) + write_lines_log(ds1, ds2, diff, log_path) else: t, e = max(arr1.size, arr2.size), 0 - write_different_size(var, arr1.size, arr2.size, log_path) + write_different_size_log(var, arr1.size, arr2.size, log_path) total_all += t equal_all += e From 60b187a4342372c1831be99a6ad04611f7ab63c4 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Wed, 28 Jan 2026 09:36:14 +0100 Subject: [PATCH 21/41] clean create_tolerance_csv --- engine/fof_compare.py | 4 +--- util/fof_utils.py | 5 ++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index ff3c5b98..aee4eec7 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -27,9 +27,7 @@ def fof_compare(file1, file2, tol): n_rows = xr.open_dataset(file1).sizes["d_body"] - tolerance_file = "tolerance_file.csv" - - create_tolerance_csv(n_rows, tol, tolerance_file) + tolerance_file = create_tolerance_csv(n_rows, tol) out, err, tol = check_file_with_tolerances( tolerance_file, FileInfo(file1), FileInfo(file2), factor=1, rules="" diff --git a/util/fof_utils.py b/util/fof_utils.py index de62c847..57e0a8aa 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -246,15 +246,18 @@ def compare_var_and_attr_ds(ds1, ds2, name_core): return total_all, equal_all -def create_tolerance_csv(n_rows, tol, tolerance_file_name): +def create_tolerance_csv(n_rows, tol): """ This function generates a file with the same number of lines as the file being analyzed, where each line contains the tolerances specified when fof-compare is called. """ + tolerance_file_name = "tolerance_file.csv" df = pd.DataFrame({"tolerance": [tol] * n_rows}) df.to_csv(tolerance_file_name) + return tolerance_file_name + def primary_check(file1, file2): """ From b554bc8b782cfc7cb3f906b6f919be9d8cf56401 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Thu, 29 Jan 2026 10:25:15 +0100 Subject: [PATCH 22/41] clean log file creation --- engine/fof_compare.py | 16 +++-- util/dataframe_ops.py | 21 ++++--- util/fof_utils.py | 137 +++++++++++++++++++++++++----------------- 3 files changed, 105 insertions(+), 69 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index aee4eec7..5a21cf7c 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -10,10 +10,14 @@ import click import xarray as xr -import numpy as np from util.dataframe_ops import check_file_with_tolerances -from util.fof_utils import create_tolerance_csv, primary_check, write_tolerance_log +from util.fof_utils import ( + create_tolerance_csv, + primary_check, + remove_log_if_only_header, + write_tolerance_log, +) from util.utils import FileInfo @@ -26,6 +30,10 @@ ) def fof_compare(file1, file2, tol): + if not primary_check(file1, file2): + print("Different types of files") + return + n_rows = xr.open_dataset(file1).sizes["d_body"] tolerance_file = create_tolerance_csv(n_rows, tol) @@ -38,9 +46,9 @@ def fof_compare(file1, file2, tol): else: print("Files are NOT consistent!") if err: - write_tolerance_log(err, tol, "tolerance_error.log") - + write_tolerance_log(err, tol) + remove_log_if_only_header() if os.path.exists(tolerance_file): os.remove(tolerance_file) diff --git a/util/dataframe_ops.py b/util/dataframe_ops.py index 8c1e8298..b97abcd9 100644 --- a/util/dataframe_ops.py +++ b/util/dataframe_ops.py @@ -6,9 +6,9 @@ """ import ast +import os import sys import warnings -import os import numpy as np import pandas as pd @@ -336,7 +336,6 @@ def check_file_with_tolerances( ) sys.exit(1) - df_tol, df_ref, df_cur = parse_check( tolerance_file_name, input_file_ref, input_file_cur, factor ) @@ -416,7 +415,9 @@ def compare_cells_rules(ref_df, cur_df, cols, rules_dict): All other differences are collected and returned as a list of error descriptions. """ errors = [] - for row_idx, (row1, row2) in enumerate(zip(ref_df.itertuples(), cur_df.itertuples())): + for row_idx, (row1, row2) in enumerate( + zip(ref_df.itertuples(), cur_df.itertuples()) + ): for col in cols: val1 = getattr(row1, col) val2 = getattr(row2, col) @@ -442,10 +443,10 @@ def compare_cells_rules(ref_df, cur_df, cols, rules_dict): def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules, name_core): """ - This function compares two Python dictionaries—each containing DataFrames under - the keys "reports" and "observation"—row by row and column by column, according - to rules defined in a separate dictionary. If the corresponding cells are - different and the values are not allowed by the rules, it records an error. + This function compares two Python dictionaries,each containing DataFrames under + the keys "reports" and "observation",row by row and column by column, according + to rules defined in a separate dictionary. If the variable does not need to follow + specific rules, the values must be identical. It returns a list indicating the row, the column and which values are wrong. """ @@ -461,7 +462,6 @@ def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules, name_core): cols_with_rules = common_cols & rule_cols cols_without_rules = common_cols - cols_with_rules - if cols_without_rules: t, e = compare_var_and_attr_ds( ref_df[list(cols_without_rules)].to_xarray(), @@ -472,8 +472,9 @@ def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules, name_core): errors = True return errors - if cols_with_rules: - errors.extend(compare_cells_rules(ref_df, cur_df, cols_without_rules, rules_dict)) + errors.extend( + compare_cells_rules(ref_df, cur_df, cols_without_rules, rules_dict) + ) return errors diff --git a/util/fof_utils.py b/util/fof_utils.py index 57e0a8aa..9c59aa0d 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -4,11 +4,14 @@ import logging import os +from pathlib import Path import numpy as np import pandas as pd import xarray as xr +_LOGGER = None +_LOG_PATH = None def get_report_variables(ds): @@ -121,17 +124,8 @@ def clean_value(x): return str(x).rstrip(" '") -def write_lines_log(ds1, ds2, diff, log_path): - if diff.size == 0: - return - - logger = setup_logger(log_path) - - if not hasattr(write_lines_log, "_header_written"): - with open(log_path, "w", encoding="utf-8") as f: - f.write("Differences\n\n") - write_lines_log._header_written = True - +def write_lines_log(ds1, ds2, diff): + logger = get_logger() da1 = ds1.to_dataframe().reset_index() da2 = ds2.to_dataframe().reset_index() col_width = 13 @@ -150,98 +144,116 @@ def write_lines_log(ds1, ds2, diff, log_path): row_diff = "|".join(f"{str(x):<{col_width}}" for x in diff_vals) - logger.info(f"id : {index}") - logger.info(f"ref : {row1}") - logger.info(f"cur : {row2}") - logger.info(f"diff : {row_diff}") + logger.info("id : %s", index) + logger.info("ref : %s", row1) + logger.info("cur : %s", row2) + logger.info("diff : %s", row_diff) logger.info("") -def write_different_size_log(var, size1, size2, log_path): +def write_different_size_log(var, size1, size2): """ This function is triggered when the array sizes do not match and records in the log file that a comparison is not possible. """ - logger = setup_logger(log_path) + logger = get_logger() logger.info( - f"variable : {var} -> datasets have different lengths " - f"({size1} vs. {size2} ), comparison not possible" + "\n" + "variable : %s -> datasets have different lengths " + "(%s vs. %s), comparison not possible\n", + var, + size1, + size2, ) -def write_tolerance_log(err, tol, log_path): + +def write_tolerance_log(err, tol): """ This function is triggered when the fof-compare step fails because the veri_data fall outside the specified tolerance range. Any resulting errors are recorded in a log file. """ - logger = setup_logger(log_path) + logger = get_logger() logger.info("Differences, veri_data outside of tolerance range") logger.info(err) logger.info(tol) -def setup_logger(log_path): +def init_logger(log_path): """ Sets up a logger that appends plain-text messages to the given log file and returns the configured logger. """ + global _LOGGER, _LOG_PATH + + if _LOGGER is not None: + return _LOGGER + + _LOG_PATH = Path(log_path) + logger = logging.getLogger("diff_logger") logger.setLevel(logging.INFO) logger.propagate = False - if logger.handlers: - logger.handlers.clear() + if not logger.handlers: + handler = logging.FileHandler(_LOG_PATH, mode="w", encoding="utf-8") + formatter = logging.Formatter("%(message)s") + handler.setFormatter(formatter) + logger.addHandler(handler) - handler = logging.FileHandler(log_path, mode="a", encoding="utf-8") - formatter = logging.Formatter("%(message)s") - handler.setFormatter(formatter) - logger.addHandler(handler) + logger.info("Differences\n") + _LOGGER = logger return logger +def get_logger(): + if _LOGGER is None: + raise RuntimeError( + "Logger not initialized. Call init_logger(log_path) once at startup." + ) + return _LOGGER + + +def remove_log_if_only_header(): + """ + Remove the log file if it contains only the initial header 'Differences'. + """ + if _LOG_PATH is None: + return + + path = Path(_LOG_PATH) + + if not path.exists(): + return + + content = path.read_text(encoding="utf-8").strip() + + if content == "Differences": + path.unlink() + + def compare_var_and_attr_ds(ds1, ds2, name_core): """ Variable by variable and attribute by attribute, - comparison of the two files. + comparison of the two datasets. """ total_all, equal_all = 0, 0 + total, equal = 0, 0 list_to_skip = ["source", "i_body", "l_body", "veri_data"] - log_path = f"error_{name_core}.log" + init_logger(f"error_{name_core}.log") for var in set(ds1.data_vars).union(ds2.data_vars): if var in ds1.data_vars and var in ds2.data_vars and var not in list_to_skip: - arr1 = fill_nans_for_float32(ds1[var].values) - arr2 = fill_nans_for_float32(ds2[var].values) - - if arr1.size == arr2.size: - t, e, diff = compare_arrays(arr1, arr2, var) - - write_lines_log(ds1, ds2, diff, log_path) - - else: - t, e = max(arr1.size, arr2.size), 0 - write_different_size_log(var, arr1.size, arr2.size, log_path) - - total_all += t - equal_all += e + total, equal = process_var(ds1, ds2, var) if var in ds1.attrs and var in ds2.attrs and var not in list_to_skip: - arr1 = fill_nans_for_float32(ds1[var].values) - arr2 = fill_nans_for_float32(ds2[var].values) - if arr1.size == arr2.size: - t, e, diff = compare_arrays(arr1, arr2, var) - - write_lines_log(ds1, ds2, diff, log_path) - else: - t, e = max(arr1.size, arr2.size), 0 - write_different_size_log(var, arr1.size, arr2.size, log_path) + total, equal = process_var(ds1, ds2, var) - total_all += t - equal_all += e + total_all += total + equal_all += equal return total_all, equal_all @@ -270,3 +282,18 @@ def primary_check(file1, file2): name2_core = name2.replace("fof", "").replace(".nc", "") return name1_core == name2_core, name1_core + + +def process_var(ds1, ds2, var): + arr1 = fill_nans_for_float32(ds1[var].values) + arr2 = fill_nans_for_float32(ds2[var].values) + if arr1.size == arr2.size: + t, e, diff = compare_arrays(arr1, arr2, var) + if diff.size != 0: + write_lines_log(ds1, ds2, diff) + + else: + t, e = max(arr1.size, arr2.size), 0 + write_different_size_log(var, arr1.size, arr2.size) + + return t, e From d8e1805b88a7f98c5cafdc5022fa8f53a58025c8 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Thu, 29 Jan 2026 18:25:44 +0100 Subject: [PATCH 23/41] fof types names and two loggers --- engine/fof_compare.py | 58 +++++++++++++++----------- util/fof_utils.py | 96 +++++++++++-------------------------------- util/log_handler.py | 23 +++++++++++ 3 files changed, 81 insertions(+), 96 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index 5a21cf7c..767a38fb 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -10,47 +10,59 @@ import click import xarray as xr +from util.click_util import CommaSeparatedStrings, cli_help from util.dataframe_ops import check_file_with_tolerances from util.fof_utils import ( create_tolerance_csv, - primary_check, - remove_log_if_only_header, write_tolerance_log, ) from util.utils import FileInfo - +from util.log_handler import logger, initialize_detailed_logger @click.command() -@click.argument("file1", type=click.Path(exists=True)) -@click.argument("file2", type=click.Path(exists=True)) +@click.argument("file1") +@click.argument("file2") +@click.option( + "--fof-types", + type=CommaSeparatedStrings(), + default="", + help=cli_help["fof_types"], +) @click.option( "--tol", default=1e-12, ) -def fof_compare(file1, file2, tol): +def fof_compare(file1, file2, fof_types,tol): - if not primary_check(file1, file2): - print("Different types of files") - return + for ft in fof_types: + file1_path = file1.format(fof_type=ft) + file2_path = file2.format(fof_type=ft) - n_rows = xr.open_dataset(file1).sizes["d_body"] - tolerance_file = create_tolerance_csv(n_rows, tol) + n_rows = xr.open_dataset(file1_path).sizes["d_body"] + tolerance_file = create_tolerance_csv(n_rows, tol) - out, err, tol = check_file_with_tolerances( - tolerance_file, FileInfo(file1), FileInfo(file2), factor=1, rules="" - ) + out, err, tol = check_file_with_tolerances( + tolerance_file, FileInfo(file1_path), FileInfo(file2_path), factor=1, rules="" + ) - if out: - print("Files are consistent!") - else: - print("Files are NOT consistent!") - if err: - write_tolerance_log(err, tol) + if out: + print("Files are consistent!") + else: + print("Files are NOT consistent!") + if err: + file_logger = initialize_detailed_logger( + "DETAILS", + log_level="DEBUG", + log_file=f"error_{ft}.log", + ) + file_logger.info("Differences, veri_data outside of tolerance range") + file_logger.info(err) + file_logger.info(tol) - remove_log_if_only_header() - if os.path.exists(tolerance_file): - os.remove(tolerance_file) + #remove_log_if_only_header() + if os.path.exists(tolerance_file): + os.remove(tolerance_file) if __name__ == "__main__": diff --git a/util/fof_utils.py b/util/fof_utils.py index 9c59aa0d..ccb6bd5e 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -10,8 +10,8 @@ import pandas as pd import xarray as xr -_LOGGER = None -_LOG_PATH = None +from util.log_handler import logger, initialize_detailed_logger +from util.log_handler import initialize_logger def get_report_variables(ds): @@ -124,8 +124,8 @@ def clean_value(x): return str(x).rstrip(" '") -def write_lines_log(ds1, ds2, diff): - logger = get_logger() +def write_lines_log(ds1, ds2, diff, file_logger): + da1 = ds1.to_dataframe().reset_index() da2 = ds2.to_dataframe().reset_index() col_width = 13 @@ -144,20 +144,20 @@ def write_lines_log(ds1, ds2, diff): row_diff = "|".join(f"{str(x):<{col_width}}" for x in diff_vals) - logger.info("id : %s", index) - logger.info("ref : %s", row1) - logger.info("cur : %s", row2) - logger.info("diff : %s", row_diff) - logger.info("") + file_logger.info("id : %s", index) + file_logger.info("ref : %s", row1) + file_logger.info("cur : %s", row2) + file_logger.info("diff : %s", row_diff) + file_logger.info("") -def write_different_size_log(var, size1, size2): +def write_different_size_log(var, size1, size2,file_logger): """ This function is triggered when the array sizes do not match and records in the log file that a comparison is not possible. """ - logger = get_logger() - logger.info( + + file_logger.info( "variable : %s -> datasets have different lengths " "(%s vs. %s), comparison not possible\n", var, @@ -172,66 +172,12 @@ def write_tolerance_log(err, tol): veri_data fall outside the specified tolerance range. Any resulting errors are recorded in a log file. """ - logger = get_logger() + logger.info("Differences, veri_data outside of tolerance range") logger.info(err) logger.info(tol) -def init_logger(log_path): - """ - Sets up a logger that appends plain-text messages to the given log - file and returns the configured logger. - """ - global _LOGGER, _LOG_PATH - - if _LOGGER is not None: - return _LOGGER - - _LOG_PATH = Path(log_path) - - logger = logging.getLogger("diff_logger") - logger.setLevel(logging.INFO) - logger.propagate = False - - if not logger.handlers: - handler = logging.FileHandler(_LOG_PATH, mode="w", encoding="utf-8") - formatter = logging.Formatter("%(message)s") - handler.setFormatter(formatter) - logger.addHandler(handler) - - logger.info("Differences\n") - - _LOGGER = logger - return logger - - -def get_logger(): - if _LOGGER is None: - raise RuntimeError( - "Logger not initialized. Call init_logger(log_path) once at startup." - ) - return _LOGGER - - -def remove_log_if_only_header(): - """ - Remove the log file if it contains only the initial header 'Differences'. - """ - if _LOG_PATH is None: - return - - path = Path(_LOG_PATH) - - if not path.exists(): - return - - content = path.read_text(encoding="utf-8").strip() - - if content == "Differences": - path.unlink() - - def compare_var_and_attr_ds(ds1, ds2, name_core): """ Variable by variable and attribute by attribute, @@ -241,16 +187,20 @@ def compare_var_and_attr_ds(ds1, ds2, name_core): total_all, equal_all = 0, 0 total, equal = 0, 0 list_to_skip = ["source", "i_body", "l_body", "veri_data"] - init_logger(f"error_{name_core}.log") + file_logger = initialize_detailed_logger( + "DETAILS", + log_level="DEBUG", + log_file=f"error_{name_core}.log", + ) for var in set(ds1.data_vars).union(ds2.data_vars): if var in ds1.data_vars and var in ds2.data_vars and var not in list_to_skip: - total, equal = process_var(ds1, ds2, var) + total, equal = process_var(ds1, ds2, var, file_logger) if var in ds1.attrs and var in ds2.attrs and var not in list_to_skip: - total, equal = process_var(ds1, ds2, var) + total, equal = process_var(ds1, ds2, var, file_logger) total_all += total equal_all += equal @@ -284,16 +234,16 @@ def primary_check(file1, file2): return name1_core == name2_core, name1_core -def process_var(ds1, ds2, var): +def process_var(ds1, ds2, var, file_logger): arr1 = fill_nans_for_float32(ds1[var].values) arr2 = fill_nans_for_float32(ds2[var].values) if arr1.size == arr2.size: t, e, diff = compare_arrays(arr1, arr2, var) if diff.size != 0: - write_lines_log(ds1, ds2, diff) + write_lines_log(ds1, ds2, diff, file_logger) else: t, e = max(arr1.size, arr2.size), 0 - write_different_size_log(var, arr1.size, arr2.size) + write_different_size_log(var, arr1.size, arr2.size, file_logger) return t, e diff --git a/util/log_handler.py b/util/log_handler.py index f3f25585..d75c22a5 100644 --- a/util/log_handler.py +++ b/util/log_handler.py @@ -28,3 +28,26 @@ def initialize_logger(log_level="DEBUG", log_file="probtest.log"): logger.setLevel(log_level) logger.info("initialized logger with level %s", log_level) + + +def initialize_detailed_logger( + name, + log_level="DEBUG", + log_file=None, +): + detailed_logger = logging.getLogger(name) + detailed_logger.setLevel(log_level) + detailed_logger.propagate = False + + if detailed_logger.handlers: + return detailed_logger + + formatter = logging.Formatter("%(message)s") + + if log_file: + file_handler = logging.FileHandler(log_file, mode="w") + file_handler.setFormatter(formatter) + detailed_logger.addHandler(file_handler) + + detailed_logger.info("initialized named logger '%s'", name) + return detailed_logger From 9166d428225d2ac198e09386e1dc5ff785ed8e46 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Fri, 30 Jan 2026 08:59:51 +0100 Subject: [PATCH 24/41] differenciate better between detailed and normal logger --- engine/fof_compare.py | 47 +++++++++++---------- util/dataframe_ops.py | 35 +++++++--------- util/fof_utils.py | 98 ++++++++++++++++--------------------------- 3 files changed, 77 insertions(+), 103 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index 767a38fb..23560bac 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -10,15 +10,13 @@ import click import xarray as xr -from util.click_util import CommaSeparatedStrings, cli_help +from util.click_util import CommaSeparatedStrings, cli_help from util.dataframe_ops import check_file_with_tolerances -from util.fof_utils import ( - create_tolerance_csv, - write_tolerance_log, -) +from util.fof_utils import create_tolerance_csv +from util.log_handler import initialize_detailed_logger, logger from util.utils import FileInfo -from util.log_handler import logger, initialize_detailed_logger + @click.command() @click.argument("file1") @@ -33,34 +31,41 @@ "--tol", default=1e-12, ) -def fof_compare(file1, file2, fof_types,tol): +def fof_compare(file1, file2, fof_types, tol): - for ft in fof_types: - file1_path = file1.format(fof_type=ft) - file2_path = file2.format(fof_type=ft) + for fof_type in fof_types: + file1_path = file1.format(fof_type=fof_type) + file2_path = file2.format(fof_type=fof_type) n_rows = xr.open_dataset(file1_path).sizes["d_body"] tolerance_file = create_tolerance_csv(n_rows, tol) out, err, tol = check_file_with_tolerances( - tolerance_file, FileInfo(file1_path), FileInfo(file2_path), factor=1, rules="" + tolerance_file, + FileInfo(file1_path), + FileInfo(file2_path), + factor=1, + rules="", ) if out: - print("Files are consistent!") + logger.info("Files are consistent!") else: - print("Files are NOT consistent!") + logger.info("Files are NOT consistent!") + log_file_name = f"error_{fof_type}.log" + logger.info("Complete output available in %s", log_file_name) if err: - file_logger = initialize_detailed_logger( - "DETAILS", - log_level="DEBUG", - log_file=f"error_{ft}.log", + detailed_logger = initialize_detailed_logger( + "DETAILS", + log_level="DEBUG", + log_file=log_file_name, + ) + detailed_logger.info( + "Differences, veri_data outside of tolerance range" ) - file_logger.info("Differences, veri_data outside of tolerance range") - file_logger.info(err) - file_logger.info(tol) + detailed_logger.info(err) + detailed_logger.info(tol) - #remove_log_if_only_header() if os.path.exists(tolerance_file): os.remove(tolerance_file) diff --git a/util/dataframe_ops.py b/util/dataframe_ops.py index b97abcd9..8b480f30 100644 --- a/util/dataframe_ops.py +++ b/util/dataframe_ops.py @@ -412,9 +412,9 @@ def compare_cells_rules(ref_df, cur_df, cols, rules_dict): This function compares two DataFrames cell by cell for a selected set of columns. For each row and column, it ignores values that are equal or whose differences are allowed by predefined rules. - All other differences are collected and returned as a list of error descriptions. + All other differences not admitted are stored in a log file. """ - errors = [] + errors = False for row_idx, (row1, row2) in enumerate( zip(ref_df.itertuples(), cur_df.itertuples()) ): @@ -429,29 +429,29 @@ def compare_cells_rules(ref_df, cur_df, cols, rules_dict): if val1 in allowed and val2 in allowed: continue - errors.append( - { - "row": row_idx, - "column": col, - "file1": val1, - "file2": val2, - "error": "values different and not admitted", - } + logger.info( + "Values different and not admitted | " + "row=%s, column=%s, file1=%s, file2=%s", + row_idx, + col, + val1, + val2, ) + errors = True return errors def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules, name_core): """ - This function compares two Python dictionaries,each containing DataFrames under - the keys "reports" and "observation",row by row and column by column, according + This function compares two Python dictionaries, each containing DataFrames under + the keys "reports" and "observation", row by row and column by column, according to rules defined in a separate dictionary. If the variable does not need to follow specific rules, the values must be identical. - It returns a list indicating the row, the column and which values are wrong. + It records the row, column and invalid values in a log file. """ rules_dict = parse_rules(rules) - errors = [] + errors = False for key, ref_df in dict_ref.items(): cur_df = dict_cur[key] @@ -469,12 +469,9 @@ def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules, name_core): name_core, ) if t != e: - errors = True - return errors + return True if cols_with_rules: - errors.extend( - compare_cells_rules(ref_df, cur_df, cols_without_rules, rules_dict) - ) + errors = compare_cells_rules(ref_df, cur_df, cols_without_rules, rules_dict) return errors diff --git a/util/fof_utils.py b/util/fof_utils.py index ccb6bd5e..355badbd 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -2,16 +2,11 @@ This module contains functions for handling fof files """ -import logging -import os -from pathlib import Path - import numpy as np import pandas as pd import xarray as xr -from util.log_handler import logger, initialize_detailed_logger -from util.log_handler import initialize_logger +from util.log_handler import initialize_detailed_logger, logger def get_report_variables(ds): @@ -96,9 +91,11 @@ def compare_arrays(arr1, arr2, var_name): mask_equal = arr1 == arr2 equal = mask_equal.sum() percent = (equal / total) * 100 - print( - f"Differences in '{var_name}': {percent:.2f}% equal. " - f"{total} total entries for this variable" + logger.info( + "Differences in '%s': %.2f%% equal. %s total entries for this variable", + var_name, + percent, + total, ) diff = np.where(~mask_equal.ravel())[0] @@ -124,7 +121,7 @@ def clean_value(x): return str(x).rstrip(" '") -def write_lines_log(ds1, ds2, diff, file_logger): +def write_lines_log(ds1, ds2, diff, detailed_logger): da1 = ds1.to_dataframe().reset_index() da2 = ds2.to_dataframe().reset_index() @@ -144,20 +141,20 @@ def write_lines_log(ds1, ds2, diff, file_logger): row_diff = "|".join(f"{str(x):<{col_width}}" for x in diff_vals) - file_logger.info("id : %s", index) - file_logger.info("ref : %s", row1) - file_logger.info("cur : %s", row2) - file_logger.info("diff : %s", row_diff) - file_logger.info("") + detailed_logger.info("id : %s", index) + detailed_logger.info("ref : %s", row1) + detailed_logger.info("cur : %s", row2) + detailed_logger.info("diff : %s", row_diff) + detailed_logger.info("") -def write_different_size_log(var, size1, size2,file_logger): +def write_different_size_log(var, size1, size2, detailed_logger): """ This function is triggered when the array sizes do not match and records in the log file that a comparison is not possible. """ - file_logger.info( + detailed_logger.info( "variable : %s -> datasets have different lengths " "(%s vs. %s), comparison not possible\n", var, @@ -166,18 +163,6 @@ def write_different_size_log(var, size1, size2,file_logger): ) -def write_tolerance_log(err, tol): - """ - This function is triggered when the fof-compare step fails because the - veri_data fall outside the specified tolerance range. - Any resulting errors are recorded in a log file. - """ - - logger.info("Differences, veri_data outside of tolerance range") - logger.info(err) - logger.info(tol) - - def compare_var_and_attr_ds(ds1, ds2, name_core): """ Variable by variable and attribute by attribute, @@ -187,20 +172,20 @@ def compare_var_and_attr_ds(ds1, ds2, name_core): total_all, equal_all = 0, 0 total, equal = 0, 0 list_to_skip = ["source", "i_body", "l_body", "veri_data"] - file_logger = initialize_detailed_logger( - "DETAILS", - log_level="DEBUG", - log_file=f"error_{name_core}.log", + detailed_logger = initialize_detailed_logger( + "DETAILS", + log_level="DEBUG", + log_file=f"error_{name_core}.log", ) for var in set(ds1.data_vars).union(ds2.data_vars): if var in ds1.data_vars and var in ds2.data_vars and var not in list_to_skip: - total, equal = process_var(ds1, ds2, var, file_logger) + total, equal = process_var(ds1, ds2, var, detailed_logger) if var in ds1.attrs and var in ds2.attrs and var not in list_to_skip: - total, equal = process_var(ds1, ds2, var, file_logger) + total, equal = process_var(ds1, ds2, var, detailed_logger) total_all += total equal_all += equal @@ -208,6 +193,21 @@ def compare_var_and_attr_ds(ds1, ds2, name_core): return total_all, equal_all +def process_var(ds1, ds2, var, detailed_logger): + arr1 = fill_nans_for_float32(ds1[var].values) + arr2 = fill_nans_for_float32(ds2[var].values) + if arr1.size == arr2.size: + t, e, diff = compare_arrays(arr1, arr2, var) + if diff.size != 0: + write_lines_log(ds1, ds2, diff, detailed_logger) + + else: + t, e = max(arr1.size, arr2.size), 0 + write_different_size_log(var, arr1.size, arr2.size, detailed_logger) + + return t, e + + def create_tolerance_csv(n_rows, tol): """ This function generates a file with the same number of lines as the file being @@ -219,31 +219,3 @@ def create_tolerance_csv(n_rows, tol): df.to_csv(tolerance_file_name) return tolerance_file_name - - -def primary_check(file1, file2): - """ - Test that the two files are of the same type. - """ - name1 = os.path.basename(file1) - name2 = os.path.basename(file2) - - name1_core = name1.replace("fof", "").replace(".nc", "") - name2_core = name2.replace("fof", "").replace(".nc", "") - - return name1_core == name2_core, name1_core - - -def process_var(ds1, ds2, var, file_logger): - arr1 = fill_nans_for_float32(ds1[var].values) - arr2 = fill_nans_for_float32(ds2[var].values) - if arr1.size == arr2.size: - t, e, diff = compare_arrays(arr1, arr2, var) - if diff.size != 0: - write_lines_log(ds1, ds2, diff, file_logger) - - else: - t, e = max(arr1.size, arr2.size), 0 - write_different_size_log(var, arr1.size, arr2.size, file_logger) - - return t, e From f22df5046030b45e06eec03b6dd28f253ab10413 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Fri, 30 Jan 2026 09:31:26 +0100 Subject: [PATCH 25/41] solved tolerance problem --- engine/fof_compare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index 23560bac..b235d29d 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -28,17 +28,17 @@ help=cli_help["fof_types"], ) @click.option( - "--tol", + "--tolerance", default=1e-12, ) -def fof_compare(file1, file2, fof_types, tol): +def fof_compare(file1, file2, fof_types, tolerance): for fof_type in fof_types: file1_path = file1.format(fof_type=fof_type) file2_path = file2.format(fof_type=fof_type) n_rows = xr.open_dataset(file1_path).sizes["d_body"] - tolerance_file = create_tolerance_csv(n_rows, tol) + tolerance_file = create_tolerance_csv(n_rows, tolerance) out, err, tol = check_file_with_tolerances( tolerance_file, From f488e446e246ed7a5bc687615c63f157533a2b94 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Fri, 30 Jan 2026 10:13:33 +0100 Subject: [PATCH 26/41] allow multiple log files --- engine/fof_compare.py | 5 +++-- util/dataframe_ops.py | 3 ++- util/log_handler.py | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index b235d29d..4a7a2f35 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -52,9 +52,10 @@ def fof_compare(file1, file2, fof_types, tolerance): logger.info("Files are consistent!") else: logger.info("Files are NOT consistent!") - log_file_name = f"error_{fof_type}.log" + core_name = os.path.basename(file1_path).replace(".nc", "") + log_file_name = f"error_{core_name}.log" logger.info("Complete output available in %s", log_file_name) - if err: + if not err.empty: detailed_logger = initialize_detailed_logger( "DETAILS", log_level="DEBUG", diff --git a/util/dataframe_ops.py b/util/dataframe_ops.py index 8b480f30..703beb9f 100644 --- a/util/dataframe_ops.py +++ b/util/dataframe_ops.py @@ -346,7 +346,8 @@ def check_file_with_tolerances( if errors: logger.error("RESULT: check FAILED") - return False, 0, 0 + err = pd.DataFrame() + return False, err , 0 else: # check if variables are available in reference file skip_test, df_ref, df_cur = check_intersection(df_ref, df_cur) diff --git a/util/log_handler.py b/util/log_handler.py index d75c22a5..c30f7015 100644 --- a/util/log_handler.py +++ b/util/log_handler.py @@ -39,7 +39,9 @@ def initialize_detailed_logger( detailed_logger.setLevel(log_level) detailed_logger.propagate = False - if detailed_logger.handlers: + existing_handlers = [h for h in detailed_logger.handlers if getattr(h, 'baseFilename', None) == log_file] + + if existing_handlers: return detailed_logger formatter = logging.Formatter("%(message)s") From 082c9eb1023f4c1360e141cc3ee448338590d384 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Fri, 30 Jan 2026 10:40:56 +0100 Subject: [PATCH 27/41] function for names --- engine/fof_compare.py | 5 ++--- util/dataframe_ops.py | 22 +++++++++++++++------- util/fof_utils.py | 16 +++++++++------- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index 4a7a2f35..d2858bfc 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -13,7 +13,7 @@ from util.click_util import CommaSeparatedStrings, cli_help from util.dataframe_ops import check_file_with_tolerances -from util.fof_utils import create_tolerance_csv +from util.fof_utils import create_tolerance_csv, get_log_file_name from util.log_handler import initialize_detailed_logger, logger from util.utils import FileInfo @@ -52,8 +52,7 @@ def fof_compare(file1, file2, fof_types, tolerance): logger.info("Files are consistent!") else: logger.info("Files are NOT consistent!") - core_name = os.path.basename(file1_path).replace(".nc", "") - log_file_name = f"error_{core_name}.log" + log_file_name = get_log_file_name(file1_path) logger.info("Complete output available in %s", log_file_name) if not err.empty: detailed_logger = initialize_detailed_logger( diff --git a/util/dataframe_ops.py b/util/dataframe_ops.py index 703beb9f..b45b4341 100644 --- a/util/dataframe_ops.py +++ b/util/dataframe_ops.py @@ -16,8 +16,9 @@ from util.constants import CHECK_THRESHOLD, compute_statistics from util.file_system import file_names_from_pattern -from util.fof_utils import compare_var_and_attr_ds, split_feedback_dataset +from util.fof_utils import compare_var_and_attr_ds, split_feedback_dataset, get_log_file_name from util.log_handler import logger +from util.log_handler import initialize_detailed_logger from util.model_output_parser import model_output_parser from util.utils import FileType @@ -341,8 +342,9 @@ def check_file_with_tolerances( ) if input_file_ref.file_type == FileType.FOF: + log_file_name = get_log_file_name(input_file_ref.path) name_core = os.path.basename(input_file_ref.path).replace(".nc", "") - errors = check_multiple_solutions_from_dict(df_ref, df_cur, rules, name_core) + errors = check_multiple_solutions_from_dict(df_ref, df_cur, rules, log_file_name) if errors: logger.error("RESULT: check FAILED") @@ -408,7 +410,7 @@ def parse_rules(rules): return {} -def compare_cells_rules(ref_df, cur_df, cols, rules_dict): +def compare_cells_rules(ref_df, cur_df, cols, rules_dict, detailed_logger): """ This function compares two DataFrames cell by cell for a selected set of columns. For each row and column, it ignores values that are equal or whose differences @@ -430,7 +432,7 @@ def compare_cells_rules(ref_df, cur_df, cols, rules_dict): if val1 in allowed and val2 in allowed: continue - logger.info( + detailed_logger.info( "Values different and not admitted | " "row=%s, column=%s, file1=%s, file2=%s", row_idx, @@ -442,7 +444,7 @@ def compare_cells_rules(ref_df, cur_df, cols, rules_dict): return errors -def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules, name_core): +def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules, log_file_name): """ This function compares two Python dictionaries, each containing DataFrames under the keys "reports" and "observation", row by row and column by column, according @@ -453,6 +455,11 @@ def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules, name_core): rules_dict = parse_rules(rules) errors = False + detailed_logger = initialize_detailed_logger( + "DETAILS", + log_level="DEBUG", + log_file=log_file_name, + ) for key, ref_df in dict_ref.items(): cur_df = dict_cur[key] @@ -467,12 +474,13 @@ def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules, name_core): t, e = compare_var_and_attr_ds( ref_df[list(cols_without_rules)].to_xarray(), cur_df[list(cols_without_rules)].to_xarray(), - name_core, + detailed_logger ) if t != e: return True if cols_with_rules: - errors = compare_cells_rules(ref_df, cur_df, cols_without_rules, rules_dict) + errors = compare_cells_rules(ref_df, cur_df, cols_without_rules, rules_dict, + detailed_logger) return errors diff --git a/util/fof_utils.py b/util/fof_utils.py index 355badbd..4bacb2bd 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -5,8 +5,9 @@ import numpy as np import pandas as pd import xarray as xr +import os -from util.log_handler import initialize_detailed_logger, logger +from util.log_handler import logger def get_report_variables(ds): @@ -163,7 +164,7 @@ def write_different_size_log(var, size1, size2, detailed_logger): ) -def compare_var_and_attr_ds(ds1, ds2, name_core): +def compare_var_and_attr_ds(ds1, ds2, detailed_logger): """ Variable by variable and attribute by attribute, comparison of the two datasets. @@ -172,11 +173,6 @@ def compare_var_and_attr_ds(ds1, ds2, name_core): total_all, equal_all = 0, 0 total, equal = 0, 0 list_to_skip = ["source", "i_body", "l_body", "veri_data"] - detailed_logger = initialize_detailed_logger( - "DETAILS", - log_level="DEBUG", - log_file=f"error_{name_core}.log", - ) for var in set(ds1.data_vars).union(ds2.data_vars): if var in ds1.data_vars and var in ds2.data_vars and var not in list_to_skip: @@ -219,3 +215,9 @@ def create_tolerance_csv(n_rows, tol): df.to_csv(tolerance_file_name) return tolerance_file_name + +def get_log_file_name(file_path): + + core_name = os.path.basename(file_path).replace(".nc", "") + log_file_name = f"error_{core_name}.log" + return log_file_name From 1641123b8f7003154097fc0b6380efed0d55fb17 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Fri, 30 Jan 2026 12:03:41 +0100 Subject: [PATCH 28/41] add description funcitons and polish --- engine/fof_compare.py | 19 +++++++++++-------- util/dataframe_ops.py | 30 +++++++++++++++--------------- util/fof_utils.py | 38 +++++++++++++++++++++++++++++++++++++- util/log_handler.py | 13 ++++++++++++- 4 files changed, 75 insertions(+), 25 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index d2858bfc..7a35668a 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -13,8 +13,12 @@ from util.click_util import CommaSeparatedStrings, cli_help from util.dataframe_ops import check_file_with_tolerances -from util.fof_utils import create_tolerance_csv, get_log_file_name -from util.log_handler import initialize_detailed_logger, logger +from util.fof_utils import ( + clean_logger_file_if_only_details, + create_tolerance_csv, + get_log_file_name, +) +from util.log_handler import get_detailed_logger, logger from util.utils import FileInfo @@ -48,24 +52,23 @@ def fof_compare(file1, file2, fof_types, tolerance): rules="", ) + log_file_name = get_log_file_name(file1_path) if out: logger.info("Files are consistent!") + else: logger.info("Files are NOT consistent!") - log_file_name = get_log_file_name(file1_path) + logger.info("Complete output available in %s", log_file_name) if not err.empty: - detailed_logger = initialize_detailed_logger( - "DETAILS", - log_level="DEBUG", - log_file=log_file_name, - ) + detailed_logger = get_detailed_logger(log_file_name) detailed_logger.info( "Differences, veri_data outside of tolerance range" ) detailed_logger.info(err) detailed_logger.info(tol) + clean_logger_file_if_only_details(log_file_name) if os.path.exists(tolerance_file): os.remove(tolerance_file) diff --git a/util/dataframe_ops.py b/util/dataframe_ops.py index b45b4341..1e821a72 100644 --- a/util/dataframe_ops.py +++ b/util/dataframe_ops.py @@ -6,7 +6,6 @@ """ import ast -import os import sys import warnings @@ -16,9 +15,12 @@ from util.constants import CHECK_THRESHOLD, compute_statistics from util.file_system import file_names_from_pattern -from util.fof_utils import compare_var_and_attr_ds, split_feedback_dataset, get_log_file_name -from util.log_handler import logger -from util.log_handler import initialize_detailed_logger +from util.fof_utils import ( + compare_var_and_attr_ds, + get_log_file_name, + split_feedback_dataset, +) +from util.log_handler import get_detailed_logger, logger from util.model_output_parser import model_output_parser from util.utils import FileType @@ -343,13 +345,14 @@ def check_file_with_tolerances( if input_file_ref.file_type == FileType.FOF: log_file_name = get_log_file_name(input_file_ref.path) - name_core = os.path.basename(input_file_ref.path).replace(".nc", "") - errors = check_multiple_solutions_from_dict(df_ref, df_cur, rules, log_file_name) + errors = check_multiple_solutions_from_dict( + df_ref, df_cur, rules, log_file_name + ) if errors: logger.error("RESULT: check FAILED") err = pd.DataFrame() - return False, err , 0 + return False, err, 0 else: # check if variables are available in reference file skip_test, df_ref, df_cur = check_intersection(df_ref, df_cur) @@ -455,11 +458,7 @@ def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules, log_file_name) rules_dict = parse_rules(rules) errors = False - detailed_logger = initialize_detailed_logger( - "DETAILS", - log_level="DEBUG", - log_file=log_file_name, - ) + detailed_logger = get_detailed_logger(log_file_name) for key, ref_df in dict_ref.items(): cur_df = dict_cur[key] @@ -474,13 +473,14 @@ def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules, log_file_name) t, e = compare_var_and_attr_ds( ref_df[list(cols_without_rules)].to_xarray(), cur_df[list(cols_without_rules)].to_xarray(), - detailed_logger + detailed_logger, ) if t != e: return True if cols_with_rules: - errors = compare_cells_rules(ref_df, cur_df, cols_without_rules, rules_dict, - detailed_logger) + errors = compare_cells_rules( + ref_df, cur_df, cols_without_rules, rules_dict, detailed_logger + ) return errors diff --git a/util/fof_utils.py b/util/fof_utils.py index 4bacb2bd..33ce00c1 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -2,10 +2,11 @@ This module contains functions for handling fof files """ +import os + import numpy as np import pandas as pd import xarray as xr -import os from util.log_handler import logger @@ -123,6 +124,10 @@ def clean_value(x): def write_lines_log(ds1, ds2, diff, detailed_logger): + """ + This function writes the differences detected between + two files to a detailed log file. + """ da1 = ds1.to_dataframe().reset_index() da2 = ds2.to_dataframe().reset_index() @@ -190,6 +195,14 @@ def compare_var_and_attr_ds(ds1, ds2, detailed_logger): def process_var(ds1, ds2, var, detailed_logger): + """ + This function first checks whether two arrays have the same size. + If they do, their values are compared. + If they don't, the differences are written to a log file. + The function outputs the total number of elements and the + number of matching elements. + """ + arr1 = fill_nans_for_float32(ds1[var].values) arr2 = fill_nans_for_float32(ds2[var].values) if arr1.size == arr2.size: @@ -216,8 +229,31 @@ def create_tolerance_csv(n_rows, tol): return tolerance_file_name + def get_log_file_name(file_path): + """ + This function gives the name of the detailed log file, + according to the file path. + """ core_name = os.path.basename(file_path).replace(".nc", "") log_file_name = f"error_{core_name}.log" return log_file_name + + +def clean_logger_file_if_only_details(file_path): + """ + This function deletes the detailed log file if it doesn't + contain anything. + """ + target_line = "initialized named logger 'DETAILS'" + + with open(file_path, "r", encoding="utf-8") as f: + lines = f.readlines() + + stripped_lines = [line.strip() for line in lines if line.strip()] + + if 0 < len(stripped_lines) <= 2 and all( + line == target_line for line in stripped_lines + ): + os.remove(file_path) diff --git a/util/log_handler.py b/util/log_handler.py index c30f7015..8e4ff678 100644 --- a/util/log_handler.py +++ b/util/log_handler.py @@ -39,7 +39,11 @@ def initialize_detailed_logger( detailed_logger.setLevel(log_level) detailed_logger.propagate = False - existing_handlers = [h for h in detailed_logger.handlers if getattr(h, 'baseFilename', None) == log_file] + existing_handlers = [ + h + for h in detailed_logger.handlers + if getattr(h, "baseFilename", None) == log_file + ] if existing_handlers: return detailed_logger @@ -53,3 +57,10 @@ def initialize_detailed_logger( detailed_logger.info("initialized named logger '%s'", name) return detailed_logger + + +def get_detailed_logger(log_file_name, logger_name="DETAILS", log_level="DEBUG"): + + return initialize_detailed_logger( + logger_name, log_level=log_level, log_file=log_file_name + ) From 70f4b3b86b6e09e0b1ce6fa3477f37e1f8146abb Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Fri, 30 Jan 2026 14:05:47 +0100 Subject: [PATCH 29/41] fix tests --- tests/util/test_dataframe_ops.py | 33 ++++++++++++++++---------------- tests/util/test_fof_utils.py | 13 +++++++++---- util/dataframe_ops.py | 10 ++++------ 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/tests/util/test_dataframe_ops.py b/tests/util/test_dataframe_ops.py index e57c8a69..0d411e31 100644 --- a/tests/util/test_dataframe_ops.py +++ b/tests/util/test_dataframe_ops.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from pathlib import Path from typing import Optional -from unittest.mock import patch +from unittest.mock import mock_open, patch import numpy as np import pandas as pd @@ -853,8 +853,12 @@ def test_multiple_solutions_from_dict_no_rules(dataframes_dict): dict_cur = {key: df.copy() for key, df in dict_ref.items()} rules = "" - errors = check_multiple_solutions_from_dict(dict_ref, dict_cur, rules) - assert errors == [] + with patch("builtins.open", mock_open()): + errors = check_multiple_solutions_from_dict( + dict_ref, dict_cur, rules, log_file_name="file_name.log" + ) + + assert errors is False def test_multiple_solutions_from_dict_with_rules(dataframes_dict): @@ -865,8 +869,11 @@ def test_multiple_solutions_from_dict_with_rules(dataframes_dict): rules = {"check": [9, 1], "state": [13, 14]} - errors = check_multiple_solutions_from_dict(dict_ref, dict_cur, rules) - assert errors == [] + with patch("builtins.open", mock_open()): + errors = check_multiple_solutions_from_dict( + dict_ref, dict_cur, rules, log_file_name="file_name.log" + ) + assert errors is False def test_multiple_solutions_from_dict_with_rules_wrong(dataframes_dict): @@ -877,15 +884,9 @@ def test_multiple_solutions_from_dict_with_rules_wrong(dataframes_dict): rules = {"check": [9, 1], "state": [13, 14]} - errors = check_multiple_solutions_from_dict(dict_ref, dict_cur, rules) + with patch("builtins.open", mock_open()): + errors = check_multiple_solutions_from_dict( + dict_ref, dict_cur, rules, log_file_name="file_name.log" + ) - expected = [ - { - "row": 1, - "column": "check", - "file1": np.int64(9), - "file2": np.int64(6), - "error": "values different and not admitted", - } - ] - assert errors == expected + assert errors is True diff --git a/tests/util/test_fof_utils.py b/tests/util/test_fof_utils.py index 407cdf79..af30f20f 100644 --- a/tests/util/test_fof_utils.py +++ b/tests/util/test_fof_utils.py @@ -2,6 +2,8 @@ This module contains unit tests for the `util/fof_utils.py` module. """ +from unittest.mock import mock_open, patch + import numpy as np import pytest @@ -14,6 +16,7 @@ get_report_variables, split_feedback_dataset, ) +from util.log_handler import get_detailed_logger @pytest.fixture(name="ds1", scope="function") @@ -274,12 +277,14 @@ def test_compare_var_and_attr_ds(ds1, ds2): Test that, given two datasets, returns the number of elements in which the variables are the same and in which they differ. """ + with patch("builtins.open", mock_open()): + detailed_logger = get_detailed_logger("test_log.log") - total1, equal1 = compare_var_and_attr_ds(ds1, ds2) - total2, equal2 = compare_var_and_attr_ds(ds1, ds2) + total1, equal1 = compare_var_and_attr_ds(ds1, ds2, detailed_logger) + total2, equal2 = compare_var_and_attr_ds(ds1, ds2, detailed_logger) - assert (total1, equal1) == (104, 103) - assert (total2, equal2) == (104, 103) + assert (total1, equal1) == (114, 113) + assert (total2, equal2) == (114, 113) @pytest.fixture(name="ds3") diff --git a/util/dataframe_ops.py b/util/dataframe_ops.py index 1e821a72..15ae83b7 100644 --- a/util/dataframe_ops.py +++ b/util/dataframe_ops.py @@ -462,12 +462,10 @@ def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules, log_file_name) for key, ref_df in dict_ref.items(): cur_df = dict_cur[key] + common_cols = [col for col in ref_df.columns if col in cur_df.columns] - common_cols = set(ref_df.columns) & set(cur_df.columns) - rule_cols = set(rules_dict) - - cols_with_rules = common_cols & rule_cols - cols_without_rules = common_cols - cols_with_rules + cols_with_rules = [col for col in common_cols if col in rules_dict] + cols_without_rules = [col for col in common_cols if col not in rules_dict] if cols_without_rules: t, e = compare_var_and_attr_ds( @@ -480,7 +478,7 @@ def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules, log_file_name) if cols_with_rules: errors = compare_cells_rules( - ref_df, cur_df, cols_without_rules, rules_dict, detailed_logger + ref_df, cur_df, cols_with_rules, rules_dict, detailed_logger ) return errors From 34619534cf773c1886e7b516368a75c4b231d2d6 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Fri, 30 Jan 2026 14:45:22 +0100 Subject: [PATCH 30/41] ready for review --- engine/fof_compare.py | 2 -- util/dataframe_ops.py | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index 7a35668a..3aa8ea7f 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -14,7 +14,6 @@ from util.click_util import CommaSeparatedStrings, cli_help from util.dataframe_ops import check_file_with_tolerances from util.fof_utils import ( - clean_logger_file_if_only_details, create_tolerance_csv, get_log_file_name, ) @@ -68,7 +67,6 @@ def fof_compare(file1, file2, fof_types, tolerance): detailed_logger.info(err) detailed_logger.info(tol) - clean_logger_file_if_only_details(log_file_name) if os.path.exists(tolerance_file): os.remove(tolerance_file) diff --git a/util/dataframe_ops.py b/util/dataframe_ops.py index 15ae83b7..b2cc47bb 100644 --- a/util/dataframe_ops.py +++ b/util/dataframe_ops.py @@ -16,6 +16,7 @@ from util.constants import CHECK_THRESHOLD, compute_statistics from util.file_system import file_names_from_pattern from util.fof_utils import ( + clean_logger_file_if_only_details, compare_var_and_attr_ds, get_log_file_name, split_feedback_dataset, @@ -480,5 +481,5 @@ def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules, log_file_name) errors = compare_cells_rules( ref_df, cur_df, cols_with_rules, rules_dict, detailed_logger ) - + clean_logger_file_if_only_details(log_file_name) return errors From 360e12707a56118a3772b4dc57580c3aa3866182 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Fri, 30 Jan 2026 16:30:43 +0100 Subject: [PATCH 31/41] ready for review for real --- tests/util/test_fof_utils.py | 4 ++-- util/fof_utils.py | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/util/test_fof_utils.py b/tests/util/test_fof_utils.py index af30f20f..f623ecec 100644 --- a/tests/util/test_fof_utils.py +++ b/tests/util/test_fof_utils.py @@ -283,8 +283,8 @@ def test_compare_var_and_attr_ds(ds1, ds2): total1, equal1 = compare_var_and_attr_ds(ds1, ds2, detailed_logger) total2, equal2 = compare_var_and_attr_ds(ds1, ds2, detailed_logger) - assert (total1, equal1) == (114, 113) - assert (total2, equal2) == (114, 113) + assert (total1, equal1) == (103, 102) + assert (total2, equal2) == (103, 102) @pytest.fixture(name="ds3") diff --git a/util/fof_utils.py b/util/fof_utils.py index 33ce00c1..36571098 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -176,20 +176,22 @@ def compare_var_and_attr_ds(ds1, ds2, detailed_logger): """ total_all, equal_all = 0, 0 - total, equal = 0, 0 + #total, equal = 0, 0 list_to_skip = ["source", "i_body", "l_body", "veri_data"] - for var in set(ds1.data_vars).union(ds2.data_vars): + for var in sorted(set(ds1.data_vars).union(ds2.data_vars)): if var in ds1.data_vars and var in ds2.data_vars and var not in list_to_skip: total, equal = process_var(ds1, ds2, var, detailed_logger) + total_all += total + equal_all += equal if var in ds1.attrs and var in ds2.attrs and var not in list_to_skip: total, equal = process_var(ds1, ds2, var, detailed_logger) + total_all += total + equal_all += equal - total_all += total - equal_all += equal return total_all, equal_all From a6fb9a7b917a94a371c45d39a3158507323e7a19 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Tue, 3 Feb 2026 08:33:20 +0100 Subject: [PATCH 32/41] correct pylint --- util/fof_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/util/fof_utils.py b/util/fof_utils.py index 36571098..6ce4bb05 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -176,7 +176,7 @@ def compare_var_and_attr_ds(ds1, ds2, detailed_logger): """ total_all, equal_all = 0, 0 - #total, equal = 0, 0 + # total, equal = 0, 0 list_to_skip = ["source", "i_body", "l_body", "veri_data"] for var in sorted(set(ds1.data_vars).union(ds2.data_vars)): @@ -192,7 +192,6 @@ def compare_var_and_attr_ds(ds1, ds2, detailed_logger): total_all += total equal_all += equal - return total_all, equal_all From 535cfb609d22a0da5c98fbaf74395cd969ce2669 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Mon, 2 Mar 2026 12:54:30 +0100 Subject: [PATCH 33/41] first part of suggestions --- engine/fof_compare.py | 72 +++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index 3aa8ea7f..8b43948f 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -35,38 +35,50 @@ default=1e-12, ) def fof_compare(file1, file2, fof_types, tolerance): + try: + for fof_type in fof_types: + file1_path = file1.format(fof_type=fof_type) + file2_path = file2.format(fof_type=fof_type) - for fof_type in fof_types: - file1_path = file1.format(fof_type=fof_type) - file2_path = file2.format(fof_type=fof_type) - - n_rows = xr.open_dataset(file1_path).sizes["d_body"] - tolerance_file = create_tolerance_csv(n_rows, tolerance) - - out, err, tol = check_file_with_tolerances( - tolerance_file, - FileInfo(file1_path), - FileInfo(file2_path), - factor=1, - rules="", - ) - - log_file_name = get_log_file_name(file1_path) - if out: - logger.info("Files are consistent!") - - else: - logger.info("Files are NOT consistent!") - - logger.info("Complete output available in %s", log_file_name) - if not err.empty: - detailed_logger = get_detailed_logger(log_file_name) - detailed_logger.info( - "Differences, veri_data outside of tolerance range" - ) - detailed_logger.info(err) - detailed_logger.info(tol) + n_rows_file1 = xr.open_dataset(file1_path).sizes["d_body"] + n_rows_file2 = xr.open_dataset(file2_path).sizes["d_body"] + + if n_rows_file1 != n_rows_file2: + raise ValueError("Files have different numbers of lines!") + + + tolerance_file = create_tolerance_csv(n_rows_file1, tolerance) + + out, err, tol = check_file_with_tolerances( + tolerance_file, + FileInfo(file1_path), + FileInfo(file2_path), + factor=1, + rules="", + ) + + log_file_name = get_log_file_name(file1_path) + if out: + logger.info("Files are consistent!") + + else: + logger.info("Files are NOT consistent!") + + logger.info("Complete output available in %s", log_file_name) + if not err.empty: + detailed_logger = get_detailed_logger(log_file_name) + detailed_logger.info( + "Differences, veri_data outside of tolerance range" + ) + detailed_logger.info(err) + detailed_logger.info(tol) + + except Exception as e: + print(f"Errore: {e}") + raise + + finally: if os.path.exists(tolerance_file): os.remove(tolerance_file) From 5d4ec20eea3caed943d86f0902f0b0abb2ef6ae0 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Mon, 2 Mar 2026 15:31:33 +0100 Subject: [PATCH 34/41] integration all suggestions --- engine/fof_compare.py | 43 ++++++++++++++++-------------------- tests/util/test_fof_utils.py | 7 ++++-- util/dataframe_ops.py | 7 ++++-- util/fof_utils.py | 13 ----------- util/log_handler.py | 7 ------ 5 files changed, 29 insertions(+), 48 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index 8b43948f..6e0e3528 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -6,18 +6,18 @@ Veri data are not considered, only reports and observations are compared. """ -import os +import tempfile import click +import pandas as pd import xarray as xr from util.click_util import CommaSeparatedStrings, cli_help from util.dataframe_ops import check_file_with_tolerances from util.fof_utils import ( - create_tolerance_csv, get_log_file_name, ) -from util.log_handler import get_detailed_logger, logger +from util.log_handler import initialize_detailed_logger, logger from util.utils import FileInfo @@ -35,53 +35,48 @@ default=1e-12, ) def fof_compare(file1, file2, fof_types, tolerance): - try: - for fof_type in fof_types: - file1_path = file1.format(fof_type=fof_type) - file2_path = file2.format(fof_type=fof_type) - n_rows_file1 = xr.open_dataset(file1_path).sizes["d_body"] - n_rows_file2 = xr.open_dataset(file2_path).sizes["d_body"] + for fof_type in fof_types: + file1_path = file1.format(fof_type=fof_type) + file2_path = file2.format(fof_type=fof_type) + n_rows_file1 = xr.open_dataset(file1_path).sizes["d_body"] + n_rows_file2 = xr.open_dataset(file2_path).sizes["d_body"] - if n_rows_file1 != n_rows_file2: - raise ValueError("Files have different numbers of lines!") + if n_rows_file1 != n_rows_file2: + raise ValueError("Files have different numbers of lines!") - - tolerance_file = create_tolerance_csv(n_rows_file1, tolerance) + with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=True) as tmp: + df = pd.DataFrame({"tolerance": [tolerance] * n_rows_file1}) + df.to_csv(tmp.name) out, err, tol = check_file_with_tolerances( - tolerance_file, + tmp.name, FileInfo(file1_path), FileInfo(file2_path), factor=1, rules="", ) - log_file_name = get_log_file_name(file1_path) if out: logger.info("Files are consistent!") else: logger.info("Files are NOT consistent!") + log_file_name = get_log_file_name(file1_path) logger.info("Complete output available in %s", log_file_name) if not err.empty: - detailed_logger = get_detailed_logger(log_file_name) + detailed_logger = initialize_detailed_logger( + "DETAILS", log_level="DEBUG", log_file=log_file_name + ) + detailed_logger.info( "Differences, veri_data outside of tolerance range" ) detailed_logger.info(err) detailed_logger.info(tol) - except Exception as e: - print(f"Errore: {e}") - raise - - finally: - if os.path.exists(tolerance_file): - os.remove(tolerance_file) - if __name__ == "__main__": fof_compare() # pylint: disable=no-value-for-parameter diff --git a/tests/util/test_fof_utils.py b/tests/util/test_fof_utils.py index f623ecec..598f4f65 100644 --- a/tests/util/test_fof_utils.py +++ b/tests/util/test_fof_utils.py @@ -16,7 +16,7 @@ get_report_variables, split_feedback_dataset, ) -from util.log_handler import get_detailed_logger +from util.log_handler import initialize_detailed_logger @pytest.fixture(name="ds1", scope="function") @@ -278,7 +278,10 @@ def test_compare_var_and_attr_ds(ds1, ds2): the variables are the same and in which they differ. """ with patch("builtins.open", mock_open()): - detailed_logger = get_detailed_logger("test_log.log") + + detailed_logger = initialize_detailed_logger( + "DETAILS", log_level="DEBUG", log_file="test_log.log" + ) total1, equal1 = compare_var_and_attr_ds(ds1, ds2, detailed_logger) total2, equal2 = compare_var_and_attr_ds(ds1, ds2, detailed_logger) diff --git a/util/dataframe_ops.py b/util/dataframe_ops.py index b2cc47bb..f58c6767 100644 --- a/util/dataframe_ops.py +++ b/util/dataframe_ops.py @@ -21,7 +21,7 @@ get_log_file_name, split_feedback_dataset, ) -from util.log_handler import get_detailed_logger, logger +from util.log_handler import initialize_detailed_logger, logger from util.model_output_parser import model_output_parser from util.utils import FileType @@ -373,6 +373,7 @@ def check_file_with_tolerances( if input_file_ref.file_type == FileType.FOF: df_ref = df_ref["observation"]["veri_data"] df_cur = df_cur["observation"]["veri_data"] + print(df_tol) df_tol.columns = ["veri_data"] # compute relative difference @@ -459,7 +460,9 @@ def check_multiple_solutions_from_dict(dict_ref, dict_cur, rules, log_file_name) rules_dict = parse_rules(rules) errors = False - detailed_logger = get_detailed_logger(log_file_name) + detailed_logger = initialize_detailed_logger( + "DETAILS", log_level="DEBUG", log_file=log_file_name + ) for key, ref_df in dict_ref.items(): cur_df = dict_cur[key] diff --git a/util/fof_utils.py b/util/fof_utils.py index 6ce4bb05..5adbef23 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -218,19 +218,6 @@ def process_var(ds1, ds2, var, detailed_logger): return t, e -def create_tolerance_csv(n_rows, tol): - """ - This function generates a file with the same number of lines as the file being - analyzed, where each line contains the tolerances specified when fof-compare - is called. - """ - tolerance_file_name = "tolerance_file.csv" - df = pd.DataFrame({"tolerance": [tol] * n_rows}) - df.to_csv(tolerance_file_name) - - return tolerance_file_name - - def get_log_file_name(file_path): """ This function gives the name of the detailed log file, diff --git a/util/log_handler.py b/util/log_handler.py index 8e4ff678..f66bb241 100644 --- a/util/log_handler.py +++ b/util/log_handler.py @@ -57,10 +57,3 @@ def initialize_detailed_logger( detailed_logger.info("initialized named logger '%s'", name) return detailed_logger - - -def get_detailed_logger(log_file_name, logger_name="DETAILS", log_level="DEBUG"): - - return initialize_detailed_logger( - logger_name, log_level=log_level, log_file=log_file_name - ) From 91a161e627d8325eba12fcbb4c7869298848bd6d Mon Sep 17 00:00:00 2001 From: cghielmini Date: Tue, 3 Mar 2026 08:34:03 +0100 Subject: [PATCH 35/41] Update engine/fof_compare.py Co-authored-by: Daniel Hupp --- engine/fof_compare.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index 6e0e3528..1841a0c5 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -46,7 +46,8 @@ def fof_compare(file1, file2, fof_types, tolerance): if n_rows_file1 != n_rows_file2: raise ValueError("Files have different numbers of lines!") - with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=True) as tmp: + with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=True, dir="/dev/shm" +) as tmp: df = pd.DataFrame({"tolerance": [tolerance] * n_rows_file1}) df.to_csv(tmp.name) From 6aaf68132cff7a55d3d41745216f0dbd97c5db87 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Tue, 3 Mar 2026 08:43:23 +0100 Subject: [PATCH 36/41] add rules and temp directory --- engine/fof_compare.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index 1841a0c5..3653e682 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -34,7 +34,8 @@ "--tolerance", default=1e-12, ) -def fof_compare(file1, file2, fof_types, tolerance): +@click.option("--rules", default="") +def fof_compare(file1, file2, fof_types, tolerance, rules): for fof_type in fof_types: file1_path = file1.format(fof_type=fof_type) @@ -46,8 +47,9 @@ def fof_compare(file1, file2, fof_types, tolerance): if n_rows_file1 != n_rows_file2: raise ValueError("Files have different numbers of lines!") - with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=True, dir="/dev/shm" -) as tmp: + with tempfile.NamedTemporaryFile( + mode="w", suffix=".csv", delete=True, dir="/dev/shm" + ) as tmp: df = pd.DataFrame({"tolerance": [tolerance] * n_rows_file1}) df.to_csv(tmp.name) @@ -56,7 +58,7 @@ def fof_compare(file1, file2, fof_types, tolerance): FileInfo(file1_path), FileInfo(file2_path), factor=1, - rules="", + rules=rules, ) if out: From 31238397851a02b230c8095f1e242394e0dee702 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Wed, 4 Mar 2026 16:32:18 +0100 Subject: [PATCH 37/41] add test for fof-compare --- tests/engine/test_fof_compare.py | 112 +++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 tests/engine/test_fof_compare.py diff --git a/tests/engine/test_fof_compare.py b/tests/engine/test_fof_compare.py new file mode 100644 index 00000000..f38fdd01 --- /dev/null +++ b/tests/engine/test_fof_compare.py @@ -0,0 +1,112 @@ +""" +This module contains test cases to validate the functionality +of fof-compare CLI commands. +""" + +import logging +import os +from pathlib import Path + +import pytest +from click.testing import CliRunner + +from engine.fof_compare import fof_compare + + +@pytest.fixture(name="fof_datasets", scope="function") +def fixture_fof_datasets(fof_datasets_base): + """ + FOF datasets written to disk, returns file paths. + """ + ds1, ds2, _, _ = fof_datasets_base + ds3 = ds2.copy(deep=True) + ds3["flags"] = (("d_body",), ds3["flags"].values * 1.55) + tmp_dir = Path(".").resolve() + + ds1_file = os.path.join(tmp_dir, "fof1_SYNOP.nc") + ds2_file = os.path.join(tmp_dir, "fof2_SYNOP.nc") + ds3_file = os.path.join(tmp_dir, "fof3_SYNOP.nc") + + ds1.to_netcdf(ds1_file) + ds2.to_netcdf(ds2_file) + ds3.to_netcdf(ds3_file) + + yield ds1_file, ds2_file, ds3_file + + +def test_fof_compare_works(fof_datasets, tmp_dir, monkeypatch): + """ + Test that fof-compare works and produces a log file. + """ + + df1, df2, _ = fof_datasets + + df1 = df1.replace("SYNOP", "{fof_type}") + df2 = df2.replace("SYNOP", "{fof_type}") + monkeypatch.chdir(tmp_dir) + rules = "" + runner = CliRunner() + + result = runner.invoke( + fof_compare, + [ + df1, + df2, + "--fof-types", + "SYNOP", + "--tolerance", + "1e-12", + "--rules", + rules, + ], + ) + + assert result.exit_code == 0 + + log_file = Path(tmp_dir + "/error_fof1_SYNOP.log") + + assert (log_file).exists() + + +def test_fof_compare_not_consistent(fof_datasets, tmp_dir, monkeypatch, caplog): + """ + Test that if there are differences in the files, then fof-compare writes + in the log file that the files are not consistent. + """ + + df1, _, df3 = fof_datasets + df1 = df1.replace("SYNOP", "{fof_type}") + df3 = df3.replace("SYNOP", "{fof_type}") + monkeypatch.chdir(tmp_dir) + + rules = "" + runner = CliRunner() + with caplog.at_level(logging.INFO): + runner.invoke( + fof_compare, + [df1, df3, "--fof-types", "SYNOP", "--tolerance", "5", "--rules", rules], + ) + + assert "Files are NOT consistent!" in caplog.text + + +def test_fof_compare_consistent(fof_datasets, tmp_dir, monkeypatch, caplog): + """ + Test that if there are no differences in the files and the tolerance is big + enough, then fof-compare writes in the log file that the files are consistent. + """ + + df1, df2, _ = fof_datasets + df1 = df1.replace("SYNOP", "{fof_type}") + df2 = df2.replace("SYNOP", "{fof_type}") + monkeypatch.chdir(tmp_dir) + + rules = "" + runner = CliRunner() + with caplog.at_level(logging.INFO): + runner.invoke( + fof_compare, + [df1, df2, "--fof-types", "SYNOP", "--tolerance", "5", "--rules", rules], + ) + + assert "Files are consistent!" in caplog.text From 96f0fe544bd78533ae4772380b8ab61f8db7c0f1 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Wed, 4 Mar 2026 16:36:02 +0100 Subject: [PATCH 38/41] cleaning test --- tests/engine/test_fof_compare.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/engine/test_fof_compare.py b/tests/engine/test_fof_compare.py index f38fdd01..4d0ca7be 100644 --- a/tests/engine/test_fof_compare.py +++ b/tests/engine/test_fof_compare.py @@ -14,14 +14,13 @@ @pytest.fixture(name="fof_datasets", scope="function") -def fixture_fof_datasets(fof_datasets_base): +def fixture_fof_datasets(fof_datasets_base, tmp_dir): """ FOF datasets written to disk, returns file paths. """ ds1, ds2, _, _ = fof_datasets_base ds3 = ds2.copy(deep=True) ds3["flags"] = (("d_body",), ds3["flags"].values * 1.55) - tmp_dir = Path(".").resolve() ds1_file = os.path.join(tmp_dir, "fof1_SYNOP.nc") ds2_file = os.path.join(tmp_dir, "fof2_SYNOP.nc") From 88d8f317a66945c7e5a3f0001a28388725902504 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Thu, 5 Mar 2026 16:47:40 +0100 Subject: [PATCH 39/41] make fof_type mandatory and add help for file path 1 and 2 --- engine/fof_compare.py | 14 +++++++++++--- tests/engine/test_fof_compare.py | 28 ++++++++++++++++++++++++++-- util/dataframe_ops.py | 1 - 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index 3653e682..aa6bd720 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -22,12 +22,20 @@ @click.command() -@click.argument("file1") -@click.argument("file2") +@click.option( + "--file1", + required=True, + help="Path to the file 1; it must contain the {fof_type} " "placeholder.", +) +@click.option( + "--file2", + required=True, + help="Path to the file 2; it must contain the {fof_type} " "placeholder.", +) @click.option( "--fof-types", type=CommaSeparatedStrings(), - default="", + required=True, help=cli_help["fof_types"], ) @click.option( diff --git a/tests/engine/test_fof_compare.py b/tests/engine/test_fof_compare.py index 4d0ca7be..e51fc25d 100644 --- a/tests/engine/test_fof_compare.py +++ b/tests/engine/test_fof_compare.py @@ -49,7 +49,9 @@ def test_fof_compare_works(fof_datasets, tmp_dir, monkeypatch): result = runner.invoke( fof_compare, [ + "--file1", df1, + "--file2", df2, "--fof-types", "SYNOP", @@ -83,7 +85,18 @@ def test_fof_compare_not_consistent(fof_datasets, tmp_dir, monkeypatch, caplog): with caplog.at_level(logging.INFO): runner.invoke( fof_compare, - [df1, df3, "--fof-types", "SYNOP", "--tolerance", "5", "--rules", rules], + [ + "--file1", + df1, + "--file2", + df3, + "--fof-types", + "SYNOP", + "--tolerance", + "5", + "--rules", + rules, + ], ) assert "Files are NOT consistent!" in caplog.text @@ -105,7 +118,18 @@ def test_fof_compare_consistent(fof_datasets, tmp_dir, monkeypatch, caplog): with caplog.at_level(logging.INFO): runner.invoke( fof_compare, - [df1, df2, "--fof-types", "SYNOP", "--tolerance", "5", "--rules", rules], + [ + "--file1", + df1, + "--file2", + df2, + "--fof-types", + "SYNOP", + "--tolerance", + "5", + "--rules", + rules, + ], ) assert "Files are consistent!" in caplog.text diff --git a/util/dataframe_ops.py b/util/dataframe_ops.py index 56442ebd..7720ab85 100644 --- a/util/dataframe_ops.py +++ b/util/dataframe_ops.py @@ -378,7 +378,6 @@ def check_file_with_tolerances( if input_file_ref.file_type == FileType.FOF: df_ref = df_ref["observation"]["veri_data"] df_cur = df_cur["observation"]["veri_data"] - print(df_tol) df_tol.columns = ["veri_data"] # compute relative difference From 9428b08507b1fbadc117dfd828ecb2588e374cb5 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Thu, 5 Mar 2026 17:03:04 +0100 Subject: [PATCH 40/41] clean --- engine/fof_compare.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/engine/fof_compare.py b/engine/fof_compare.py index aa6bd720..00064d29 100644 --- a/engine/fof_compare.py +++ b/engine/fof_compare.py @@ -25,12 +25,12 @@ @click.option( "--file1", required=True, - help="Path to the file 1; it must contain the {fof_type} " "placeholder.", + help="Path to the file 1; it must contain the {fof_type} placeholder.", ) @click.option( "--file2", required=True, - help="Path to the file 2; it must contain the {fof_type} " "placeholder.", + help="Path to the file 2; it must contain the {fof_type} placeholder.", ) @click.option( "--fof-types", From 096eef707d9efd4d7750957cc2ead62799e536e4 Mon Sep 17 00:00:00 2001 From: Chiara Ghielmini Date: Fri, 6 Mar 2026 09:17:20 +0100 Subject: [PATCH 41/41] clean commented lines --- tests/util/test_fof_utils.py | 35 ----------------------------------- util/fof_utils.py | 1 - 2 files changed, 36 deletions(-) diff --git a/tests/util/test_fof_utils.py b/tests/util/test_fof_utils.py index 598f4f65..3ecd8a7b 100644 --- a/tests/util/test_fof_utils.py +++ b/tests/util/test_fof_utils.py @@ -237,41 +237,6 @@ def fixture_sample_dataset_2(sample_dataset_fof): return data -# def test_write_lines(ds1, ds2, tmp_path): -# """ -# Test that if there are any differences, they are saved in a separate csv file. -# """ -# file_path = tmp_path / "differences.csv" -# diff = np.array([5]) -# write_lines(ds1, ds2, diff, file_path) - -# content = file_path.read_text(encoding="utf-8") - -# expected = ( -# "id : d_hdr |d_body |lat |lon |varno " -# "|statid |time_nomi |codetype |level |l_body " -# "|i_body |veri_data |obs |bcor |level_typ " -# "|level_sig |state |flags |check |e_o " -# "|qual |plevel \n" -# "ref : 0 |5 |1 |5 |4 " -# "|a |0 |5 |750 |1 " -# "|1 |78 |0.155 |0.969 |0.524 " -# "|0.366 |1 |9 |13 |0.52 " -# "|0.138 |0.755 \n" -# "cur : 0 |5 |1 |5 |4 " -# "|a |0 |5 |750 |1 " -# "|1 |78 |0.155 |0.969 |0.524 " -# "|0.366 |1 |9 |13 |0.52 " -# "|0.138 |0.755 \n" -# "diff : 0 |0 |0 |0 |0 " -# "|nan |0 |0 |0 |0 " -# "|0 |0 |0.0 |0.0 |0.0 " -# "|0.0 |0 |0 |0 |0.0 " -# "|0.0 |0.0 \n" -# ) -# assert content == expected - - def test_compare_var_and_attr_ds(ds1, ds2): """ Test that, given two datasets, returns the number of elements in which diff --git a/util/fof_utils.py b/util/fof_utils.py index 5adbef23..f9e544d0 100644 --- a/util/fof_utils.py +++ b/util/fof_utils.py @@ -176,7 +176,6 @@ def compare_var_and_attr_ds(ds1, ds2, detailed_logger): """ total_all, equal_all = 0, 0 - # total, equal = 0, 0 list_to_skip = ["source", "i_body", "l_body", "veri_data"] for var in sorted(set(ds1.data_vars).union(ds2.data_vars)):