From 41ead680a883196eed858255e47ae62f028b6e16 Mon Sep 17 00:00:00 2001 From: Anton Soloviev Date: Fri, 23 Oct 2026 20:00:46 -0700 Subject: [PATCH 1/2] Preserve input summaries when merging continuous scores --- .../test_verification_detcontscores_merge.py | 44 +++++++++++++++++++ pysteps/verification/detcontscores.py | 3 +- 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 pysteps/tests/test_verification_detcontscores_merge.py diff --git a/pysteps/tests/test_verification_detcontscores_merge.py b/pysteps/tests/test_verification_detcontscores_merge.py new file mode 100644 index 000000000..5784d7536 --- /dev/null +++ b/pysteps/tests/test_verification_detcontscores_merge.py @@ -0,0 +1,44 @@ +"""Merging reusable verification summaries must preserve their inputs.""" + +from copy import deepcopy + +import numpy as np +import pytest + +from pysteps.verification import detcontscores + + +@pytest.mark.parametrize("axis", [None, 0, (1, 2), -1]) +def test_merge_preserves_inputs(axis): + rng = np.random.default_rng(42) + pred = rng.uniform(0, 10, size=(2, 2, 3, 4)) + obs = rng.uniform(0, 10, size=pred.shape) + summaries = [] + expected = detcontscores.det_cont_fct_init(axis=axis) + for p, o in zip(pred, obs): + summary = detcontscores.det_cont_fct_init(axis=axis) + detcontscores.det_cont_fct_accum(summary, p, o) + detcontscores.det_cont_fct_accum(expected, p, o) + summaries.append(summary) + + before = deepcopy(summaries) + merged = detcontscores.det_cont_fct_merge(*summaries) + for summary, original in zip(summaries, before): + for key in original: + np.testing.assert_equal(summary[key], original[key]) + + # The same partial summaries may be reused for another aggregate. + repeated = detcontscores.det_cont_fct_merge(*summaries) + reverse = detcontscores.det_cont_fct_merge(*summaries[::-1]) + for key, value in expected.items(): + if isinstance(value, np.ndarray): + for result in (merged, repeated, reverse): + np.testing.assert_allclose(result[key], value) + assert not np.shares_memory(result[key], summaries[0][key]) + assert not np.shares_memory(result[key], summaries[1][key]) + + # Updating the merged object must also leave the partial summaries alone. + detcontscores.det_cont_fct_accum(merged, pred[0], obs[0]) + for summary, original in zip(summaries, before): + for key in original: + np.testing.assert_equal(summary[key], original[key]) diff --git a/pysteps/verification/detcontscores.py b/pysteps/verification/detcontscores.py index 25d989016..f483f1313 100644 --- a/pysteps/verification/detcontscores.py +++ b/pysteps/verification/detcontscores.py @@ -16,6 +16,7 @@ """ import collections +import copy import numpy as np from scipy.stats import spearmanr @@ -433,7 +434,7 @@ def det_cont_fct_merge(err_1, err_2): raise ValueError("cannot merge: no data found") # merge the two verification error objects - err = err_1.copy() + err = copy.deepcopy(err_1) # update variances _parallel_var( From 64a973a64d383308f7f43935c0ddd08b8837582a Mon Sep 17 00:00:00 2001 From: Anton Soloviev Date: Fri, 23 Oct 2026 20:10:31 -0700 Subject: [PATCH 2/2] Pass merge test summaries explicitly for static analysis --- pysteps/tests/test_verification_detcontscores_merge.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pysteps/tests/test_verification_detcontscores_merge.py b/pysteps/tests/test_verification_detcontscores_merge.py index 5784d7536..58ca9db87 100644 --- a/pysteps/tests/test_verification_detcontscores_merge.py +++ b/pysteps/tests/test_verification_detcontscores_merge.py @@ -22,14 +22,15 @@ def test_merge_preserves_inputs(axis): summaries.append(summary) before = deepcopy(summaries) - merged = detcontscores.det_cont_fct_merge(*summaries) + first, second = summaries + merged = detcontscores.det_cont_fct_merge(first, second) for summary, original in zip(summaries, before): for key in original: np.testing.assert_equal(summary[key], original[key]) # The same partial summaries may be reused for another aggregate. - repeated = detcontscores.det_cont_fct_merge(*summaries) - reverse = detcontscores.det_cont_fct_merge(*summaries[::-1]) + repeated = detcontscores.det_cont_fct_merge(first, second) + reverse = detcontscores.det_cont_fct_merge(second, first) for key, value in expected.items(): if isinstance(value, np.ndarray): for result in (merged, repeated, reverse):