From 400851b188dc37fecc8ccf5dea3ab62e7ba2916f Mon Sep 17 00:00:00 2001 From: Yuchen Ethan Xiao Date: Mon, 25 Aug 2025 14:13:25 -0400 Subject: [PATCH 1/6] feat: add warning for extrapolation in `morphsqueeze` --- src/diffpy/morph/morphs/morphsqueeze.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/diffpy/morph/morphs/morphsqueeze.py b/src/diffpy/morph/morphs/morphsqueeze.py index bc0e4d49..1ebdbce2 100644 --- a/src/diffpy/morph/morphs/morphsqueeze.py +++ b/src/diffpy/morph/morphs/morphsqueeze.py @@ -4,6 +4,7 @@ import numpy as np from numpy.polynomial import Polynomial from scipy.interpolate import CubicSpline +import warnings from diffpy.morph.morphs.morph import LABEL_GR, LABEL_RA, Morph @@ -85,4 +86,17 @@ def morph(self, x_morph, y_morph, x_target, y_target): high_extrap = np.where(self.x_morph_in > x_squeezed[-1])[0] self.extrap_index_low = low_extrap[-1] if low_extrap.size else None self.extrap_index_high = high_extrap[0] if high_extrap.size else None + + low_extrap_x = x_squeezed[0] - self.x_morph_in[0] + high_extrap_x = self.x_morph_in[-1] - x_squeezed[-1] + if low_extrap_x > 0 or high_extrap_x > 0: + wmsg = "Extrapolating the morphed function: " + if low_extrap_x > 0: + wmsg += f"extrapolating length in the lowe r {low_extrap_x} " + if high_extrap_x > 0: + wmsg += f"extrapolating length in the high r {high_extrap_x} " + warnings.warn( + wmsg, + UserWarning, + ) return self.xyallout From 4f59c54977d88bd1de4520f805ff15e6c0505ea7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 18:28:55 +0000 Subject: [PATCH 2/6] [pre-commit.ci] auto fixes from pre-commit hooks --- src/diffpy/morph/morphs/morphsqueeze.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/diffpy/morph/morphs/morphsqueeze.py b/src/diffpy/morph/morphs/morphsqueeze.py index 1ebdbce2..e672a8a2 100644 --- a/src/diffpy/morph/morphs/morphsqueeze.py +++ b/src/diffpy/morph/morphs/morphsqueeze.py @@ -1,10 +1,11 @@ """Class MorphSqueeze -- Apply a polynomial to squeeze the morph function.""" +import warnings + import numpy as np from numpy.polynomial import Polynomial from scipy.interpolate import CubicSpline -import warnings from diffpy.morph.morphs.morph import LABEL_GR, LABEL_RA, Morph From bdd29ae3ab52bd69d829ba12ebe8f49f210b92c1 Mon Sep 17 00:00:00 2001 From: Yuchen Ethan Xiao Date: Mon, 25 Aug 2025 14:33:45 -0400 Subject: [PATCH 3/6] chore: add news --- news/extrapolate-warning.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 news/extrapolate-warning.rst diff --git a/news/extrapolate-warning.rst b/news/extrapolate-warning.rst new file mode 100644 index 00000000..c669afc9 --- /dev/null +++ b/news/extrapolate-warning.rst @@ -0,0 +1,23 @@ +**Added:** + +* No news added: Add warning for extrapolation in morphsqueeze.py. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* From 0cbcee41b2b3cc3760830388174f713ac8a08493 Mon Sep 17 00:00:00 2001 From: Yuchen Ethan Xiao Date: Mon, 25 Aug 2025 21:57:18 -0400 Subject: [PATCH 4/6] test: add test for warning extrapolation in `morphsqueeze` --- src/diffpy/morph/morphs/morphsqueeze.py | 28 ++++++++++++++++++------- tests/test_morphsqueeze.py | 27 ++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src/diffpy/morph/morphs/morphsqueeze.py b/src/diffpy/morph/morphs/morphsqueeze.py index e672a8a2..c795255e 100644 --- a/src/diffpy/morph/morphs/morphsqueeze.py +++ b/src/diffpy/morph/morphs/morphsqueeze.py @@ -10,6 +10,13 @@ from diffpy.morph.morphs.morph import LABEL_GR, LABEL_RA, Morph +def custom_formatwarning(msg, *args, **kwargs): + return f"{msg}\n" + + +warnings.formatwarning = custom_formatwarning + + class MorphSqueeze(Morph): """Squeeze the morph function. @@ -88,14 +95,19 @@ def morph(self, x_morph, y_morph, x_target, y_target): self.extrap_index_low = low_extrap[-1] if low_extrap.size else None self.extrap_index_high = high_extrap[0] if high_extrap.size else None - low_extrap_x = x_squeezed[0] - self.x_morph_in[0] - high_extrap_x = self.x_morph_in[-1] - x_squeezed[-1] - if low_extrap_x > 0 or high_extrap_x > 0: - wmsg = "Extrapolating the morphed function: " - if low_extrap_x > 0: - wmsg += f"extrapolating length in the lowe r {low_extrap_x} " - if high_extrap_x > 0: - wmsg += f"extrapolating length in the high r {high_extrap_x} " + begin_end_sqeeze = min(x_squeezed), max(x_squeezed) + begin_end_in = min(self.x_morph_in), max(self.x_morph_in) + if not ( + begin_end_sqeeze[0] <= begin_end_in[0] + and begin_end_in[-1] >= begin_end_in[-1] + ): + wmsg = ( + "\nExtrapolating the morphed function via CubicSpline:\n" + f"Obtaining grid points between {begin_end_in[0]} and " + f"{begin_end_in[1]}.\n" + f"Points below {begin_end_sqeeze[0]} and " + f"above {begin_end_sqeeze[1]} will be extrapolated." + ) warnings.warn( wmsg, UserWarning, diff --git a/tests/test_morphsqueeze.py b/tests/test_morphsqueeze.py index e5ce2a56..5fdfa48e 100644 --- a/tests/test_morphsqueeze.py +++ b/tests/test_morphsqueeze.py @@ -85,3 +85,30 @@ def test_morphsqueeze(x_morph, x_target, squeeze_coeffs): assert np.allclose(x_morph_actual, x_morph_expected) assert np.allclose(x_target_actual, x_target) assert np.allclose(y_target_actual, y_target) + + +def test_morphsqueeze_extrapolate(): + x_morph = np.linspace(0, 10, 101) + y_morph = np.sin(x_morph) + x_target = x_morph + y_target = y_morph + squeeze_coeff = {"a0": 0.01, "a1": -0.0005, "a2": -0.0005, "a3": -1e-6} + morph = MorphSqueeze() + morph.squeeze = squeeze_coeff + coeffs = [squeeze_coeff[f"a{i}"] for i in range(len(squeeze_coeff))] + squeeze_polynomial = Polynomial(coeffs) + x_squeezed = x_morph + squeeze_polynomial(x_morph) + with pytest.warns() as w: + x_morph_actual, y_morph_actual, x_target_actual, y_target_actual = ( + morph(x_morph, y_morph, x_target, y_target) + ) + assert len(w) == 1 + assert w[0].category is UserWarning + actual_wmsg = str(w[0].message) + expected_wmsg = ( + "\nExtrapolating the morphed function via CubicSpline:\n" + f"Obtaining grid points between {x_morph[0]} and {x_morph[-1]}.\n" + f"Points below {x_squeezed[0]} and " + f"above {x_squeezed[-1]} will be extrapolated." + ) + assert actual_wmsg == expected_wmsg From 026984aa158248aa3622c2aae629e1723f5869b4 Mon Sep 17 00:00:00 2001 From: Yuchen Ethan Xiao Date: Mon, 25 Aug 2025 23:07:18 -0400 Subject: [PATCH 5/6] chore: update warning message for extrapolation in `morphsqueeze.py` --- src/diffpy/morph/morphs/morphsqueeze.py | 33 +++++++++++-------- tests/test_morphsqueeze.py | 44 +++++++++++++++++++------ 2 files changed, 53 insertions(+), 24 deletions(-) diff --git a/src/diffpy/morph/morphs/morphsqueeze.py b/src/diffpy/morph/morphs/morphsqueeze.py index c795255e..d57957ac 100644 --- a/src/diffpy/morph/morphs/morphsqueeze.py +++ b/src/diffpy/morph/morphs/morphsqueeze.py @@ -94,20 +94,25 @@ def morph(self, x_morph, y_morph, x_target, y_target): high_extrap = np.where(self.x_morph_in > x_squeezed[-1])[0] self.extrap_index_low = low_extrap[-1] if low_extrap.size else None self.extrap_index_high = high_extrap[0] if high_extrap.size else None - - begin_end_sqeeze = min(x_squeezed), max(x_squeezed) - begin_end_in = min(self.x_morph_in), max(self.x_morph_in) - if not ( - begin_end_sqeeze[0] <= begin_end_in[0] - and begin_end_in[-1] >= begin_end_in[-1] - ): - wmsg = ( - "\nExtrapolating the morphed function via CubicSpline:\n" - f"Obtaining grid points between {begin_end_in[0]} and " - f"{begin_end_in[1]}.\n" - f"Points below {begin_end_sqeeze[0]} and " - f"above {begin_end_sqeeze[1]} will be extrapolated." - ) + below_extrap = min(x_morph) < min(x_squeezed) + above_extrap = max(x_morph) > max(x_squeezed) + if below_extrap or above_extrap: + if not above_extrap: + wmsg = ( + "Warning: points with grid value below " + f"{min(x_squeezed)} will be extrapolated." + ) + elif not below_extrap: + wmsg = ( + "Warning: points with grid value above " + f"{max(x_squeezed)} will be extrapolated." + ) + else: + wmsg = ( + "Warning: points with grid value below " + f"{min(x_squeezed)} and above {max(x_squeezed)} will be " + "extrapolated." + ) warnings.warn( wmsg, UserWarning, diff --git a/tests/test_morphsqueeze.py b/tests/test_morphsqueeze.py index 5fdfa48e..ce030f28 100644 --- a/tests/test_morphsqueeze.py +++ b/tests/test_morphsqueeze.py @@ -87,15 +87,44 @@ def test_morphsqueeze(x_morph, x_target, squeeze_coeffs): assert np.allclose(y_target_actual, y_target) -def test_morphsqueeze_extrapolate(): +@pytest.mark.parametrize( + "squeeze_coeffs, wmsg_gen", + [ + # extrapolate below + ( + {"a0": 0.01}, + lambda x: ( + "Warning: points with grid value below " + f"{x[0]} will be extrapolated." + ), + ), + # extrapolate above + ( + {"a0": -0.01}, + lambda x: ( + "Warning: points with grid value above " + f"{x[1]} will be extrapolated." + ), + ), + # extrapolate below and above + ( + {"a0": 0.01, "a1": -0.002}, + lambda x: ( + "Warning: points with grid value below " + f"{x[0]} and above {x[1]} will be " + "extrapolated." + ), + ), + ], +) +def test_morphsqueeze_extrapolate(squeeze_coeffs, wmsg_gen): x_morph = np.linspace(0, 10, 101) y_morph = np.sin(x_morph) x_target = x_morph y_target = y_morph - squeeze_coeff = {"a0": 0.01, "a1": -0.0005, "a2": -0.0005, "a3": -1e-6} morph = MorphSqueeze() - morph.squeeze = squeeze_coeff - coeffs = [squeeze_coeff[f"a{i}"] for i in range(len(squeeze_coeff))] + morph.squeeze = squeeze_coeffs + 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: @@ -105,10 +134,5 @@ def test_morphsqueeze_extrapolate(): assert len(w) == 1 assert w[0].category is UserWarning actual_wmsg = str(w[0].message) - expected_wmsg = ( - "\nExtrapolating the morphed function via CubicSpline:\n" - f"Obtaining grid points between {x_morph[0]} and {x_morph[-1]}.\n" - f"Points below {x_squeezed[0]} and " - f"above {x_squeezed[-1]} will be extrapolated." - ) + expected_wmsg = wmsg_gen([min(x_squeezed), max(x_squeezed)]) assert actual_wmsg == expected_wmsg From e165b41a2aee79dc8165676d7fe6909085ec2384 Mon Sep 17 00:00:00 2001 From: Yuchen Ethan Xiao Date: Mon, 25 Aug 2025 23:43:40 -0400 Subject: [PATCH 6/6] test: add CLI test for extrapolation warning --- tests/test_morphsqueeze.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/test_morphsqueeze.py b/tests/test_morphsqueeze.py index ce030f28..cc741bc1 100644 --- a/tests/test_morphsqueeze.py +++ b/tests/test_morphsqueeze.py @@ -1,3 +1,5 @@ +import subprocess + import numpy as np import pytest from numpy.polynomial import Polynomial @@ -117,7 +119,7 @@ def test_morphsqueeze(x_morph, x_target, squeeze_coeffs): ), ], ) -def test_morphsqueeze_extrapolate(squeeze_coeffs, wmsg_gen): +def test_morphsqueeze_extrapolate(user_filesystem, squeeze_coeffs, wmsg_gen): x_morph = np.linspace(0, 10, 101) y_morph = np.sin(x_morph) x_target = x_morph @@ -136,3 +138,32 @@ def test_morphsqueeze_extrapolate(squeeze_coeffs, wmsg_gen): actual_wmsg = str(w[0].message) expected_wmsg = wmsg_gen([min(x_squeezed), max(x_squeezed)]) assert actual_wmsg == expected_wmsg + + # CLI test + morph_file, target_file = create_morph_data_file( + user_filesystem / "cwd_dir", x_morph, y_morph, x_target, y_target + ) + run_cmd = ["diffpy.morph"] + run_cmd.extend(["--squeeze=" + ",".join(map(str, coeffs))]) + run_cmd.extend([str(morph_file), str(target_file)]) + run_cmd.append("-n") + result = subprocess.run(run_cmd, capture_output=True, text=True) + assert expected_wmsg in result.stderr + + +def create_morph_data_file( + data_dir_path, x_morph, y_morph, x_target, y_target +): + morph_file = data_dir_path / "morph_data" + morph_data_text = [ + str(x_morph[i]) + " " + str(y_morph[i]) for i in range(len(x_morph)) + ] + morph_data_text = "\n".join(morph_data_text) + morph_file.write_text(morph_data_text) + target_file = data_dir_path / "target_data" + target_data_text = [ + str(x_target[i]) + " " + str(y_target[i]) for i in range(len(x_target)) + ] + target_data_text = "\n".join(target_data_text) + target_file.write_text(target_data_text) + return morph_file, target_file