From b6136cff0234538f058da57d58cd12aecd8a6862 Mon Sep 17 00:00:00 2001 From: Sparks29032 Date: Mon, 15 Dec 2025 22:30:17 -0800 Subject: [PATCH 1/5] Squeeze morph warning handling --- news/squeeze_warnings.rst | 23 ++++ src/diffpy/morph/morph_io.py | 25 ++++- src/diffpy/morph/morphapp.py | 7 +- src/diffpy/morph/morphpy.py | 1 + src/diffpy/morph/morphs/morphsqueeze.py | 38 ++++++- tests/test_morphsqueeze.py | 138 ++++++++++++++++++++++++ 6 files changed, 224 insertions(+), 8 deletions(-) create mode 100644 news/squeeze_warnings.rst diff --git a/news/squeeze_warnings.rst b/news/squeeze_warnings.rst new file mode 100644 index 00000000..3c89e8a6 --- /dev/null +++ b/news/squeeze_warnings.rst @@ -0,0 +1,23 @@ +**Added:** + +* Warnings added to `squeeze` morph if the squeeze causes the grid to become non-monotonic. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/morph/morph_io.py b/src/diffpy/morph/morph_io.py index 84d5c15a..6ee44800 100644 --- a/src/diffpy/morph/morph_io.py +++ b/src/diffpy/morph/morph_io.py @@ -408,9 +408,9 @@ def tabulate_results(multiple_morph_results): return tabulated_results -def handle_warnings(squeeze_morph): - if squeeze_morph is not None: - extrapolation_info = squeeze_morph.extrapolation_info +def handle_extrapolation_warnings(morph): + if morph is not None: + extrapolation_info = morph.extrapolation_info is_extrap_low = extrapolation_info["is_extrap_low"] is_extrap_high = extrapolation_info["is_extrap_high"] cutoff_low = extrapolation_info["cutoff_low"] @@ -443,3 +443,22 @@ def handle_warnings(squeeze_morph): wmsg, UserWarning, ) + + +def handle_check_increase_warning(squeeze_morph): + if squeeze_morph is not None: + if squeeze_morph.strictly_increasing: + wmsg = None + else: + wmsg = ( + "Warning: The squeeze morph has interpolated your morphed " + "function from a non-monotonically increasing grid. " + "This can result in strange behavior in the non-unique " + "grid regions. To disable this setting, " + "please enable --check-increasing." + ) + if wmsg: + warnings.warn( + wmsg, + UserWarning, + ) diff --git a/src/diffpy/morph/morphapp.py b/src/diffpy/morph/morphapp.py index 327ed2da..e96aa456 100755 --- a/src/diffpy/morph/morphapp.py +++ b/src/diffpy/morph/morphapp.py @@ -707,9 +707,10 @@ def single_morph( chain(x_morph, y_morph, x_target, y_target) # THROW ANY WARNINGS HERE - io.handle_warnings(squeeze_morph) - io.handle_warnings(shift_morph) - io.handle_warnings(stretch_morph) + io.handle_extrapolation_warnings(squeeze_morph) + io.handle_check_increase_warning(squeeze_morph) + io.handle_extrapolation_warnings(shift_morph) + io.handle_extrapolation_warnings(stretch_morph) # Get Rw for the morph range rw = tools.getRw(chain) diff --git a/src/diffpy/morph/morphpy.py b/src/diffpy/morph/morphpy.py index bac6eee8..3889f824 100644 --- a/src/diffpy/morph/morphpy.py +++ b/src/diffpy/morph/morphpy.py @@ -51,6 +51,7 @@ def __get_morph_opts__(parser, scale, stretch, smear, plot, **kwargs): "reverse", "diff", "get-diff", + "check-increase", ] opts_to_ignore = ["multiple-morphs", "multiple-targets"] for opt in opts_storing_values: diff --git a/src/diffpy/morph/morphs/morphsqueeze.py b/src/diffpy/morph/morphs/morphsqueeze.py index 3d0250da..28cd1cf1 100644 --- a/src/diffpy/morph/morphs/morphsqueeze.py +++ b/src/diffpy/morph/morphs/morphsqueeze.py @@ -1,6 +1,7 @@ """Class MorphSqueeze -- Apply a polynomial to squeeze the morph function.""" +import numpy from numpy.polynomial import Polynomial from scipy.interpolate import CubicSpline @@ -67,10 +68,36 @@ class MorphSqueeze(Morph): extrap_index_high = None squeeze_cutoff_low = None squeeze_cutoff_high = None + strictly_increasing = None def __init__(self, config=None): super().__init__(config) + def _ensure_strictly_increase(self, x, x_sorted): + if list(x) != list(x_sorted): + self.strictly_increasing = False + else: + self.strictly_increasing = True + + def _sort_squeeze(self, x, y): + """Sort x,y according to the value of x.""" + xy = list(zip(x, y)) + xy_sorted = sorted(xy, key=lambda pair: pair[0]) + x_sorted, y_sorted = list(zip(*xy_sorted)) + return x_sorted, y_sorted + + def _handle_duplicates(self, x, y): + """Remove duplicated x and use the mean value of y corresponded + to the duplicated x.""" + unq_x, unq_inv = numpy.unique(x, return_inverse=True) + if len(unq_x) == len(x): + return x, y + else: + y_avg = numpy.zeros_like(unq_x) + for i in range(len(unq_x)): + y_avg[i] = numpy.array(y)[unq_inv == i].mean() + return unq_x, y_avg + def morph(self, x_morph, y_morph, x_target, y_target): """Apply a polynomial to squeeze the morph function. @@ -82,9 +109,16 @@ def morph(self, x_morph, y_morph, x_target, y_target): coeffs = [self.squeeze[f"a{i}"] for i in range(len(self.squeeze))] squeeze_polynomial = Polynomial(coeffs) x_squeezed = self.x_morph_in + squeeze_polynomial(self.x_morph_in) - self.y_morph_out = CubicSpline(x_squeezed, self.y_morph_in)( + x_squeezed_sorted, y_morph_sorted = self._sort_squeeze( + x_squeezed, self.y_morph_in + ) + self._ensure_strictly_increase(x_squeezed, x_squeezed_sorted) + x_squeezed_sorted, y_morph_sorted = self._handle_duplicates( + x_squeezed_sorted, y_morph_sorted + ) + self.y_morph_out = CubicSpline(x_squeezed_sorted, y_morph_sorted)( self.x_morph_in ) - self.set_extrapolation_info(x_squeezed, self.x_morph_in) + self.set_extrapolation_info(x_squeezed_sorted, self.x_morph_in) return self.xyallout diff --git a/tests/test_morphsqueeze.py b/tests/test_morphsqueeze.py index 07b99372..6b29f893 100644 --- a/tests/test_morphsqueeze.py +++ b/tests/test_morphsqueeze.py @@ -170,3 +170,141 @@ def test_morphsqueeze_extrapolate(user_filesystem, squeeze_coeffs, wmsg_gen): ) with pytest.warns(UserWarning, match=expected_wmsg): single_morph(parser, opts, pargs, stdout_flag=False) + + +@pytest.mark.parametrize( + "squeeze_coeffs, x_morph", + [ + ({"a0": 0.01, "a1": 0.01, "a2": -0.1}, np.linspace(0, 10, 101)), + ], +) +def test_non_strictly_increasing_squeeze(squeeze_coeffs, x_morph): + x_target = x_morph + y_target = np.sin(x_target) + coeffs = [squeeze_coeffs[f"a{i}"] for i in range(len(squeeze_coeffs))] + squeeze_polynomial = Polynomial(coeffs) + x_squeezed = x_morph + squeeze_polynomial(x_morph) + # non-strictly-increasing + assert not np.all(np.sign(np.diff(x_squeezed)) > 0) + y_morph = np.sin(x_squeezed) + # all zero initial guess + morph_results = morphpy.morph_arrays( + np.array([x_morph, y_morph]).T, + np.array([x_target, y_target]).T, + squeeze=[0, 0, 0], + apply=True, + ) + _, y_morph_actual = morph_results[1].T # noqa: F841 + y_morph_expected = np.sin(x_morph) # noqa: F841 + # squeeze morph extrapolates. + # Need to extract extrap_index from morph_results to examine + # the convergence. + # assert np.allclose(y_morph_actual, y_morph_expected, atol=1e-3) + # Raise warning when called without --check-increase + with pytest.warns() as w: + morph_results = morphpy.morph_arrays( + np.array([x_morph, y_morph]).T, + np.array([x_target, y_target]).T, + squeeze=[0.01, 0.01, -0.1], + apply=True, + ) + assert w[0].category is UserWarning + actual_wmsg = " ".join([str(w[i].message) for i in range(len(w))]) + expected_wmsg = ( + "Warning: The squeeze morph has interpolated your morphed " + "function from a non-monotonically increasing grid. " + ) + assert expected_wmsg in actual_wmsg + _, y_morph_actual = morph_results[1].T # noqa: F841 + y_morph_expected = np.sin(x_morph) # noqa: F841 + # squeeze morph extrapolates. + # Need to extract extrap_index from morph_results to examine + # the convergence. + # assert np.allclose(y_morph_actual, y_morph_expected, atol=1e-3) + # System exits when called with --check-increase + with pytest.raises(SystemExit) as excinfo: + morphpy.morph_arrays( + np.array([x_morph, y_morph]).T, + np.array([x_target, y_target]).T, + squeeze=[0.01, 0.009, -0.1], + check_increase=True, + ) + actual_emsg = str(excinfo.value) + expected_emsg = "2" + assert expected_emsg == actual_emsg + + +@pytest.mark.parametrize( + "squeeze_coeffs, x_morph", + [ + ({"a0": -1, "a1": -1, "a2": 2}, np.linspace(-1, 1, 101)), + ( + {"a0": -1, "a1": -1, "a2": 0, "a3": 0, "a4": 2}, + np.linspace(-1, 1, 101), + ), + ], +) +def test_sort_squeeze_bad(user_filesystem, squeeze_coeffs, x_morph): + # call in .py without --check-increase + x_target = x_morph + y_target = np.sin(x_target) + coeffs = [squeeze_coeffs[f"a{i}"] for i in range(len(squeeze_coeffs))] + squeeze_polynomial = Polynomial(coeffs) + x_squeezed = x_morph + squeeze_polynomial(x_morph) + y_morph = np.sin(x_squeezed) + morph = MorphSqueeze() + morph.squeeze = squeeze_coeffs + with pytest.warns() as w: + morphpy.morph_arrays( + np.array([x_morph, y_morph]).T, + np.array([x_target, y_target]).T, + squeeze=coeffs, + apply=True, + ) + assert len(w) == 1 + assert w[0].category is UserWarning + actual_wmsg = str(w[0].message) + expected_wmsg = ( + "Warning: The squeeze morph has interpolated your morphed " + "function from a non-monotonically increasing grid. " + ) + assert expected_wmsg in actual_wmsg + + # call in CLI without --check-increase + morph_file, target_file = create_morph_data_file( + user_filesystem / "cwd_dir", x_morph, y_morph, x_target, y_target + ) + parser = create_option_parser() + (opts, pargs) = parser.parse_args( + [ + "--squeeze", + ",".join(map(str, coeffs)), + f"{morph_file.as_posix()}", + f"{target_file.as_posix()}", + "--apply", + "-n", + ] + ) + with pytest.warns(UserWarning) as w: + single_morph(parser, opts, pargs, stdout_flag=False) + assert len(w) == 1 + actual_wmsg = str(w[0].message) + assert expected_wmsg in actual_wmsg + + +def test_handle_duplicates(): + unq_x = np.linspace(0, 11, 10) + iter = 10 + morph = MorphSqueeze() + for i in range(iter): + actual_x = np.random.choice(unq_x, size=20) + actual_y = np.sin(actual_x) + actual_handled_x, actual_handled_y = morph._handle_duplicates( + actual_x, actual_y + ) + expected_handled_x = np.unique(actual_x) + expected_handled_y = np.array( + [actual_y[actual_x == x].mean() for x in expected_handled_x] + ) + assert np.allclose(actual_handled_x, expected_handled_x) + assert np.allclose(actual_handled_y, expected_handled_y) From 97383a0b7cce3c8172fd6b393b482a0e69ed727b Mon Sep 17 00:00:00 2001 From: Sparks29032 Date: Tue, 16 Dec 2025 13:20:37 -0800 Subject: [PATCH 2/5] Clarity update --- src/diffpy/morph/morph_io.py | 10 +-- src/diffpy/morph/morphpy.py | 1 - src/diffpy/morph/morphs/morphsqueeze.py | 18 ++-- tests/test_morphsqueeze.py | 112 ++++++++++-------------- 4 files changed, 56 insertions(+), 85 deletions(-) diff --git a/src/diffpy/morph/morph_io.py b/src/diffpy/morph/morph_io.py index 6ee44800..04aed204 100644 --- a/src/diffpy/morph/morph_io.py +++ b/src/diffpy/morph/morph_io.py @@ -447,17 +447,13 @@ def handle_extrapolation_warnings(morph): def handle_check_increase_warning(squeeze_morph): if squeeze_morph is not None: - if squeeze_morph.strictly_increasing: - wmsg = None - else: + if not squeeze_morph.strictly_increasing: wmsg = ( "Warning: The squeeze morph has interpolated your morphed " "function from a non-monotonically increasing grid. " - "This can result in strange behavior in the non-unique " - "grid regions. To disable this setting, " - "please enable --check-increasing." + "This can result in strange behavior in certain " + "grid regions." ) - if wmsg: warnings.warn( wmsg, UserWarning, diff --git a/src/diffpy/morph/morphpy.py b/src/diffpy/morph/morphpy.py index 3889f824..bac6eee8 100644 --- a/src/diffpy/morph/morphpy.py +++ b/src/diffpy/morph/morphpy.py @@ -51,7 +51,6 @@ def __get_morph_opts__(parser, scale, stretch, smear, plot, **kwargs): "reverse", "diff", "get-diff", - "check-increase", ] opts_to_ignore = ["multiple-morphs", "multiple-targets"] for opt in opts_storing_values: diff --git a/src/diffpy/morph/morphs/morphsqueeze.py b/src/diffpy/morph/morphs/morphsqueeze.py index 28cd1cf1..e1431fc8 100644 --- a/src/diffpy/morph/morphs/morphsqueeze.py +++ b/src/diffpy/morph/morphs/morphsqueeze.py @@ -73,7 +73,7 @@ class MorphSqueeze(Morph): def __init__(self, config=None): super().__init__(config) - def _ensure_strictly_increase(self, x, x_sorted): + def _check_strictly_increasing(self, x, x_sorted): if list(x) != list(x_sorted): self.strictly_increasing = False else: @@ -83,20 +83,20 @@ def _sort_squeeze(self, x, y): """Sort x,y according to the value of x.""" xy = list(zip(x, y)) xy_sorted = sorted(xy, key=lambda pair: pair[0]) - x_sorted, y_sorted = list(zip(*xy_sorted)) + x_sorted, y_sorted = numpy.array(list(zip(*xy_sorted))) return x_sorted, y_sorted def _handle_duplicates(self, x, y): """Remove duplicated x and use the mean value of y corresponded to the duplicated x.""" - unq_x, unq_inv = numpy.unique(x, return_inverse=True) - if len(unq_x) == len(x): + x_unique, inv = numpy.unique(x, return_inverse=True) + if len(x_unique) == len(x): return x, y else: - y_avg = numpy.zeros_like(unq_x) - for i in range(len(unq_x)): - y_avg[i] = numpy.array(y)[unq_inv == i].mean() - return unq_x, y_avg + y_avg = numpy.zeros_like(x_unique) + for idx, _ in enumerate(x_unique): + y_avg[idx] = y[inv == idx].mean() + return x_unique, y_avg def morph(self, x_morph, y_morph, x_target, y_target): """Apply a polynomial to squeeze the morph function. @@ -112,7 +112,7 @@ def morph(self, x_morph, y_morph, x_target, y_target): x_squeezed_sorted, y_morph_sorted = self._sort_squeeze( x_squeezed, self.y_morph_in ) - self._ensure_strictly_increase(x_squeezed, x_squeezed_sorted) + self._check_strictly_increasing(x_squeezed, x_squeezed_sorted) x_squeezed_sorted, y_morph_sorted = self._handle_duplicates( x_squeezed_sorted, y_morph_sorted ) diff --git a/tests/test_morphsqueeze.py b/tests/test_morphsqueeze.py index 6b29f893..9ec83be9 100644 --- a/tests/test_morphsqueeze.py +++ b/tests/test_morphsqueeze.py @@ -172,71 +172,47 @@ def test_morphsqueeze_extrapolate(user_filesystem, squeeze_coeffs, wmsg_gen): single_morph(parser, opts, pargs, stdout_flag=False) -@pytest.mark.parametrize( - "squeeze_coeffs, x_morph", - [ - ({"a0": 0.01, "a1": 0.01, "a2": -0.1}, np.linspace(0, 10, 101)), - ], -) -def test_non_strictly_increasing_squeeze(squeeze_coeffs, x_morph): - x_target = x_morph - y_target = np.sin(x_target) +def test_non_unique_grid(): + squeeze_coeffs = {"a0": 0.01, "a1": 0.01, "a2": -0.1} + x_grid = np.linspace(0, 10, 101) + coeffs = [squeeze_coeffs[f"a{i}"] for i in range(len(squeeze_coeffs))] squeeze_polynomial = Polynomial(coeffs) - x_squeezed = x_morph + squeeze_polynomial(x_morph) - # non-strictly-increasing - assert not np.all(np.sign(np.diff(x_squeezed)) > 0) - y_morph = np.sin(x_squeezed) - # all zero initial guess - morph_results = morphpy.morph_arrays( + x_morph = x_grid + squeeze_polynomial(x_grid) + x_gradient = np.diff(x_morph) + x_gradient_sign = np.sign(x_gradient) + # non-strictly increasing means the gradient becomes 0 or negative + assert not np.all(x_gradient_sign > 0) + + x_target = np.linspace(min(x_morph), max(x_morph), len(x_morph)) + y_target = np.sin(x_target) + + y_morph = np.sin(x_morph) + # apply no squeeze, but the morph should sort the function + _, table = morphpy.morph_arrays( np.array([x_morph, y_morph]).T, np.array([x_target, y_target]).T, - squeeze=[0, 0, 0], + squeeze=[0, 0, 0.0001], apply=True, ) - _, y_morph_actual = morph_results[1].T # noqa: F841 - y_morph_expected = np.sin(x_morph) # noqa: F841 - # squeeze morph extrapolates. - # Need to extract extrap_index from morph_results to examine - # the convergence. - # assert np.allclose(y_morph_actual, y_morph_expected, atol=1e-3) - # Raise warning when called without --check-increase - with pytest.warns() as w: - morph_results = morphpy.morph_arrays( - np.array([x_morph, y_morph]).T, - np.array([x_target, y_target]).T, - squeeze=[0.01, 0.01, -0.1], - apply=True, - ) - assert w[0].category is UserWarning - actual_wmsg = " ".join([str(w[i].message) for i in range(len(w))]) - expected_wmsg = ( - "Warning: The squeeze morph has interpolated your morphed " - "function from a non-monotonically increasing grid. " - ) - assert expected_wmsg in actual_wmsg - _, y_morph_actual = morph_results[1].T # noqa: F841 - y_morph_expected = np.sin(x_morph) # noqa: F841 - # squeeze morph extrapolates. - # Need to extract extrap_index from morph_results to examine - # the convergence. - # assert np.allclose(y_morph_actual, y_morph_expected, atol=1e-3) - # System exits when called with --check-increase - with pytest.raises(SystemExit) as excinfo: - morphpy.morph_arrays( - np.array([x_morph, y_morph]).T, - np.array([x_target, y_target]).T, - squeeze=[0.01, 0.009, -0.1], - check_increase=True, - ) - actual_emsg = str(excinfo.value) - expected_emsg = "2" - assert expected_emsg == actual_emsg + x_refined, y_refined = table[:, 0], table[:, 1] + + # grid should be properly sorted + assert np.allclose(x_refined, x_target) + + print(y_refined, y_target) + x_sorted = x_morph.copy() + x_sorted.sort() + print(x_morph) + print(x_sorted) + # function values + assert np.allclose(y_refined, y_target) @pytest.mark.parametrize( "squeeze_coeffs, x_morph", [ + # The following squeezes make the function non-monotonic ({"a0": -1, "a1": -1, "a2": 2}, np.linspace(-1, 1, 101)), ( {"a0": -1, "a1": -1, "a2": 0, "a3": 0, "a4": 2}, @@ -244,8 +220,8 @@ def test_non_strictly_increasing_squeeze(squeeze_coeffs, x_morph): ), ], ) -def test_sort_squeeze_bad(user_filesystem, squeeze_coeffs, x_morph): - # call in .py without --check-increase +def test_squeeze_warnings(user_filesystem, squeeze_coeffs, x_morph): + # call in .py x_target = x_morph y_target = np.sin(x_target) coeffs = [squeeze_coeffs[f"a{i}"] for i in range(len(squeeze_coeffs))] @@ -267,10 +243,12 @@ def test_sort_squeeze_bad(user_filesystem, squeeze_coeffs, x_morph): expected_wmsg = ( "Warning: The squeeze morph has interpolated your morphed " "function from a non-monotonically increasing grid. " + "This can result in strange behavior in certain " + "grid regions." ) assert expected_wmsg in actual_wmsg - # call in CLI without --check-increase + # call in CLI morph_file, target_file = create_morph_data_file( user_filesystem / "cwd_dir", x_morph, y_morph, x_target, y_target ) @@ -293,18 +271,16 @@ def test_sort_squeeze_bad(user_filesystem, squeeze_coeffs, x_morph): def test_handle_duplicates(): - unq_x = np.linspace(0, 11, 10) + x_choices = np.linspace(0, 10, 11) iter = 10 morph = MorphSqueeze() for i in range(iter): - actual_x = np.random.choice(unq_x, size=20) - actual_y = np.sin(actual_x) - actual_handled_x, actual_handled_y = morph._handle_duplicates( - actual_x, actual_y - ) - expected_handled_x = np.unique(actual_x) - expected_handled_y = np.array( - [actual_y[actual_x == x].mean() for x in expected_handled_x] + x_sampled = np.random.choice(x_choices, size=20) + y_sampled = np.sin(x_sampled) + x_handled, y_handled = morph._handle_duplicates(x_sampled, y_sampled) + x_target = np.unique(x_sampled) + y_target = np.array( + [y_sampled[x_sampled == x].mean() for x in x_target] ) - assert np.allclose(actual_handled_x, expected_handled_x) - assert np.allclose(actual_handled_y, expected_handled_y) + assert np.allclose(x_handled, x_target) + assert np.allclose(y_handled, y_target) From 42307affe9987ef09586806a0817a89429cdf2e6 Mon Sep 17 00:00:00 2001 From: Sparks29032 Date: Tue, 16 Dec 2025 13:33:32 -0800 Subject: [PATCH 3/5] rework test --- tests/test_morphsqueeze.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/tests/test_morphsqueeze.py b/tests/test_morphsqueeze.py index 9ec83be9..3d0f5700 100644 --- a/tests/test_morphsqueeze.py +++ b/tests/test_morphsqueeze.py @@ -192,21 +192,14 @@ def test_non_unique_grid(): _, table = morphpy.morph_arrays( np.array([x_morph, y_morph]).T, np.array([x_target, y_target]).T, - squeeze=[0, 0, 0.0001], + squeeze=[0, 0, 0], apply=True, ) - x_refined, y_refined = table[:, 0], table[:, 1] + x_refined, _ = table[:, 0], table[:, 1] # grid should be properly sorted assert np.allclose(x_refined, x_target) - - print(y_refined, y_target) - x_sorted = x_morph.copy() - x_sorted.sort() - print(x_morph) - print(x_sorted) - # function values - assert np.allclose(y_refined, y_target) + # note that the function itself may be distorted @pytest.mark.parametrize( From b94423221bfe9923b5e39f1b7933c6bd29246e94 Mon Sep 17 00:00:00 2001 From: Sparks29032 Date: Thu, 18 Dec 2025 13:14:11 -0800 Subject: [PATCH 4/5] Update test docstrings, warning message --- src/diffpy/morph/morph_io.py | 21 +++++- src/diffpy/morph/morphs/morphsqueeze.py | 2 +- tests/test_morphsqueeze.py | 91 ++++++++++++++++--------- 3 files changed, 78 insertions(+), 36 deletions(-) diff --git a/src/diffpy/morph/morph_io.py b/src/diffpy/morph/morph_io.py index 04aed204..613bd3a1 100644 --- a/src/diffpy/morph/morph_io.py +++ b/src/diffpy/morph/morph_io.py @@ -451,8 +451,25 @@ def handle_check_increase_warning(squeeze_morph): wmsg = ( "Warning: The squeeze morph has interpolated your morphed " "function from a non-monotonically increasing grid. " - "This can result in strange behavior in certain " - "grid regions." + "\nThis may not be an issue, but please check for your " + "particular case. " + "\nTo avoid squeeze making your grid non-monotonic, " + "here are some suggested fixes: " + "\n(1) Please decrease the order of your polynomial and " + "try again. " + "\n(2) If you are using initial guesses of all 0, please " + "ensure your objective function only requires a small " + "polynomial squeeze to match your reference. " + "(In other words, there is good agreement between the two " + "functions.) " + "\n(3) If you expect a large polynomial squeeze to be " + "needed, please ensure your initial parameters for the " + "polynomial morph result in good agreement between your " + "reference and objective functions. " + "One way to obtain such parameters is to " + "first apply a --hshift and --stretch morph. " + "Then, use the hshift parameter for a0 and stretch " + "parameter for a1." ) warnings.warn( wmsg, diff --git a/src/diffpy/morph/morphs/morphsqueeze.py b/src/diffpy/morph/morphs/morphsqueeze.py index e1431fc8..5909d654 100644 --- a/src/diffpy/morph/morphs/morphsqueeze.py +++ b/src/diffpy/morph/morphs/morphsqueeze.py @@ -93,7 +93,7 @@ def _handle_duplicates(self, x, y): if len(x_unique) == len(x): return x, y else: - y_avg = numpy.zeros_like(x_unique) + y_avg = numpy.zeros_like(x_unique, dtype=float) for idx, _ in enumerate(x_unique): y_avg[idx] = y[inv == idx].mean() return x_unique, y_avg diff --git a/tests/test_morphsqueeze.py b/tests/test_morphsqueeze.py index 3d0f5700..e847e7de 100644 --- a/tests/test_morphsqueeze.py +++ b/tests/test_morphsqueeze.py @@ -54,6 +54,8 @@ def test_morphsqueeze(x_morph, x_target, squeeze_coeffs): x_target_expected = x_target y_target_expected = y_target # actual output + # turn the coefficients into a list for passing to Polynomial + # the morphsqueeze function itself requires a dictionary coeffs = [squeeze_coeffs[f"a{i}"] for i in range(len(squeeze_coeffs))] squeeze_polynomial = Polynomial(coeffs) x_squeezed = x_morph + squeeze_polynomial(x_morph) @@ -139,16 +141,16 @@ def test_morphsqueeze_extrapolate(user_filesystem, squeeze_coeffs, wmsg_gen): coeffs = [squeeze_coeffs[f"a{i}"] for i in range(len(squeeze_coeffs))] squeeze_polynomial = Polynomial(coeffs) x_squeezed = x_morph + squeeze_polynomial(x_morph) - with pytest.warns() as w: + with pytest.warns() as warning: morphpy.morph_arrays( np.array([x_morph, y_morph]).T, np.array([x_target, y_target]).T, squeeze=coeffs, apply=True, ) - assert len(w) == 1 - assert w[0].category is UserWarning - actual_wmsg = str(w[0].message) + assert len(warning) == 1 + assert warning[0].category is UserWarning + actual_wmsg = str(warning[0].message) expected_wmsg = wmsg_gen([min(x_squeezed), max(x_squeezed)]) assert actual_wmsg == expected_wmsg @@ -173,6 +175,8 @@ def test_morphsqueeze_extrapolate(user_filesystem, squeeze_coeffs, wmsg_gen): def test_non_unique_grid(): + # Test giving morphsqueeze a non-unique grid + # Expect it to return a unique grid squeeze_coeffs = {"a0": 0.01, "a1": 0.01, "a2": -0.1} x_grid = np.linspace(0, 10, 101) @@ -205,10 +209,11 @@ def test_non_unique_grid(): @pytest.mark.parametrize( "squeeze_coeffs, x_morph", [ - # The following squeezes make the function non-monotonic - ({"a0": -1, "a1": -1, "a2": 2}, np.linspace(-1, 1, 101)), + # The following squeezes make the function non-monotonic. + # Expect code to work but issue the correct warning. + ([-1, -1, 2], np.linspace(-1, 1, 101)), ( - {"a0": -1, "a1": -1, "a2": 0, "a3": 0, "a4": 2}, + [-1, -1, 0, 0, 2], np.linspace(-1, 1, 101), ), ], @@ -217,27 +222,40 @@ def test_squeeze_warnings(user_filesystem, squeeze_coeffs, x_morph): # call in .py x_target = x_morph y_target = np.sin(x_target) - coeffs = [squeeze_coeffs[f"a{i}"] for i in range(len(squeeze_coeffs))] - squeeze_polynomial = Polynomial(coeffs) + squeeze_polynomial = Polynomial(squeeze_coeffs) x_squeezed = x_morph + squeeze_polynomial(x_morph) y_morph = np.sin(x_squeezed) morph = MorphSqueeze() morph.squeeze = squeeze_coeffs - with pytest.warns() as w: + with pytest.warns() as warning: morphpy.morph_arrays( np.array([x_morph, y_morph]).T, np.array([x_target, y_target]).T, - squeeze=coeffs, + squeeze=squeeze_coeffs, apply=True, ) - assert len(w) == 1 - assert w[0].category is UserWarning - actual_wmsg = str(w[0].message) + assert len(warning) == 1 + assert warning[0].category is UserWarning + actual_wmsg = str(warning[0].message) expected_wmsg = ( "Warning: The squeeze morph has interpolated your morphed " "function from a non-monotonically increasing grid. " - "This can result in strange behavior in certain " - "grid regions." + "\nThis may not be an issue, but please check for your " + "particular case. " + "\nTo avoid squeeze making your grid non-monotonic, " + "here are some suggested fixes: " + "\n(1) Please decrease the order of your polynomial and try again. " + "\n(2) If you are using initial guesses of all 0, please ensure " + "your objective function only requires a small polynomial " + "squeeze to match your reference. " + "(In other words, there is good agreement between the two " + "functions.) " + "\n(3) If you expect a large polynomial squeeze to be needed, " + "please ensure your initial parameters for the polynomial " + "morph result in good agreement between your reference and " + "objective functions. One way to obtain such parameters is to " + "first apply a --hshift and --stretch morph. " + "Then, use the hshift parameter for a0 and stretch parameter for a1." ) assert expected_wmsg in actual_wmsg @@ -249,31 +267,38 @@ def test_squeeze_warnings(user_filesystem, squeeze_coeffs, x_morph): (opts, pargs) = parser.parse_args( [ "--squeeze", - ",".join(map(str, coeffs)), + ",".join(map(str, squeeze_coeffs)), f"{morph_file.as_posix()}", f"{target_file.as_posix()}", "--apply", "-n", ] ) - with pytest.warns(UserWarning) as w: + with pytest.warns(UserWarning) as warning: single_morph(parser, opts, pargs, stdout_flag=False) - assert len(w) == 1 - actual_wmsg = str(w[0].message) + assert len(warning) == 1 + actual_wmsg = str(warning[0].message) assert expected_wmsg in actual_wmsg -def test_handle_duplicates(): - x_choices = np.linspace(0, 10, 11) - iter = 10 +@pytest.mark.parametrize( + "x_sampled", + [ + # Test one duplicate per number + np.array([0, 0, 1, 1, 2, 2, 3, 3]), + # Test more than one duplicates per number + np.array([0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2]), + # Test with only one grid number + np.array([0, 0, 0, 0]), + # Test no duplicates + np.array([0, 1, 2, 3, 4]), + ], +) +def test_handle_duplicates(x_sampled): morph = MorphSqueeze() - for i in range(iter): - x_sampled = np.random.choice(x_choices, size=20) - y_sampled = np.sin(x_sampled) - x_handled, y_handled = morph._handle_duplicates(x_sampled, y_sampled) - x_target = np.unique(x_sampled) - y_target = np.array( - [y_sampled[x_sampled == x].mean() for x in x_target] - ) - assert np.allclose(x_handled, x_target) - assert np.allclose(y_handled, y_target) + y_sampled = np.sin(x_sampled) + x_handled, y_handled = morph._handle_duplicates(x_sampled, y_sampled) + x_target = np.unique(x_sampled) + y_target = np.array([y_sampled[x_sampled == x].mean() for x in x_target]) + assert np.allclose(x_handled, x_target) + assert np.allclose(y_handled, y_target) From c791189744f7efcbcbc5c4a0cf8ad2025f782c45 Mon Sep 17 00:00:00 2001 From: Sparks29032 Date: Thu, 18 Dec 2025 13:18:02 -0800 Subject: [PATCH 5/5] Update duplicate test docstring --- tests/test_morphsqueeze.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_morphsqueeze.py b/tests/test_morphsqueeze.py index e847e7de..2bb65b0f 100644 --- a/tests/test_morphsqueeze.py +++ b/tests/test_morphsqueeze.py @@ -284,6 +284,7 @@ def test_squeeze_warnings(user_filesystem, squeeze_coeffs, x_morph): @pytest.mark.parametrize( "x_sampled", [ + # Expected output: all repeated datapoints are removed # Test one duplicate per number np.array([0, 0, 1, 1, 2, 2, 3, 3]), # Test more than one duplicates per number