From 9a53b247d1cb9b946798c4a9ed440c00765d0ab9 Mon Sep 17 00:00:00 2001 From: Luis Kitsu Iglesias Date: Sun, 30 Mar 2025 10:30:06 -0600 Subject: [PATCH 01/10] test: adding a test to unsqueeze squeezed data --- news/test_squeeze.rst | 23 +++++++++++++++++++ tests/test_morphsqueeze.py | 46 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 news/test_squeeze.rst create mode 100644 tests/test_morphsqueeze.py diff --git a/news/test_squeeze.rst b/news/test_squeeze.rst new file mode 100644 index 0000000..47cc0d4 --- /dev/null +++ b/news/test_squeeze.rst @@ -0,0 +1,23 @@ +**Added:** + +* Squeeze test: given a squeezed signal, unsqueezing recovers the original + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/tests/test_morphsqueeze.py b/tests/test_morphsqueeze.py new file mode 100644 index 0000000..6e6d66e --- /dev/null +++ b/tests/test_morphsqueeze.py @@ -0,0 +1,46 @@ +import matplotlib.pyplot as plt +import numpy as np + + +def test_morphsqueeze(): + """ + Test that we can unsqueeze squeezed data. + The test inputs are an expected uniform target (e.g. synchrotron data) + and a squeezed version of the target (e.g. XFEL data). The squeezed data + is created by applying a nonlinear distortion to the uniform target. + Both input data are in the same uniform x-axis grid. + Then we unsqueeze the squeezed data by doing the inverse transformation + using interpolatiion. + Finally we check that the unsqueezed data matches the expected uniform + target. + """ + + # Uniform x-axis grid. This is the same x-axis for all data. + x = np.linspace(0, 10, 1000) + # Expected uniform target + y_expected = np.sin(x) + + # Apply squeeze parameters to uniform data to get the squeezed data + squeeze_1 = 0.003 + squeeze_2 = 0.004 + x_squeezed = x + squeeze_1 * x**2 + squeeze_2 * x**3 + y_squeezed = np.sin(x_squeezed) + + # Unsqueeze the data by interpolating back to uniform grid + y_unsqueezed = np.interp(x, x_squeezed, y_squeezed) + y_actual = y_unsqueezed + + # Check that the unsqueezed (actual) data matches the expected data + # Including tolerance error because I was having issues + # with y_actual == y_expected. I think is because interpolation? + assert np.allclose(y_actual, y_expected, atol=1e-3) + + # Plot to verify what we are doing + plt.figure(figsize=(7, 4)) + plt.plot(x, y_expected, color="black", label="Expected uniform data") + plt.plot(x, y_squeezed, "--", color="purple", label="Squeezed data") + plt.plot(x, y_unsqueezed, "--", color="gold", label="Unsqueezed data") + plt.xlabel("x") + plt.ylabel("y") + plt.legend() + plt.show() From ac388e9d78e8873d60e884479491ac19ff45a863 Mon Sep 17 00:00:00 2001 From: Luis Kitsu Iglesias Date: Tue, 1 Apr 2025 13:27:44 -0600 Subject: [PATCH 02/10] test: adding squeeze_0 as well as bounds --- tests/test_morphsqueeze.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/test_morphsqueeze.py b/tests/test_morphsqueeze.py index 6e6d66e..3dfdd10 100644 --- a/tests/test_morphsqueeze.py +++ b/tests/test_morphsqueeze.py @@ -1,3 +1,21 @@ +""" +The squeeze morph is used to correct for small non-linear geometric distortions +from detectors that are not effectively corrected during calibration, such as +individual module misalignment or tilt. This squeezing is applied as: +x_squeezed = x + squeeze_0 + squeeze_1 * x**2 + squeeze_2 * x**3 + +The squeeze distortions that we might encounter practically are going to be +very small. Furthermore, large values for the squeezing parameters lead to +missing values during interpolation. Therefore is important to use small +squeezing values to avoid error or unphysical results. +Safe bounds for the squeezing parameters are: +squeeze_0 [-0.2, 0.2] +squeeze_1 [-0.001, 0.001] +squeeze_2 [-0.0001, 0.0001] +Values outside these bounds should be used carefully. +Note that these bounds are established for an x-axis that goes from 0 to 10. +""" + import matplotlib.pyplot as plt import numpy as np @@ -21,9 +39,11 @@ def test_morphsqueeze(): y_expected = np.sin(x) # Apply squeeze parameters to uniform data to get the squeezed data - squeeze_1 = 0.003 - squeeze_2 = 0.004 - x_squeezed = x + squeeze_1 * x**2 + squeeze_2 * x**3 + # Include squeeze_0 for squeezes with offset + squeeze_0 = 0.2 + squeeze_1 = 0.001 + squeeze_2 = 0.001 + x_squeezed = x + squeeze_0 + squeeze_1 * x**2 + squeeze_2 * x**3 y_squeezed = np.sin(x_squeezed) # Unsqueeze the data by interpolating back to uniform grid @@ -33,7 +53,7 @@ def test_morphsqueeze(): # Check that the unsqueezed (actual) data matches the expected data # Including tolerance error because I was having issues # with y_actual == y_expected. I think is because interpolation? - assert np.allclose(y_actual, y_expected, atol=1e-3) + assert np.allclose(y_actual, y_expected, atol=1) # Plot to verify what we are doing plt.figure(figsize=(7, 4)) From ce863d319fbfa762951bf859a22349866f544271 Mon Sep 17 00:00:00 2001 From: Luis Kitsu Iglesias Date: Thu, 3 Apr 2025 11:14:11 -0600 Subject: [PATCH 03/10] test: adding extrapolation with scipy.interp1d --- tests/test_morphsqueeze.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/test_morphsqueeze.py b/tests/test_morphsqueeze.py index 3dfdd10..0fb30cf 100644 --- a/tests/test_morphsqueeze.py +++ b/tests/test_morphsqueeze.py @@ -18,6 +18,7 @@ import matplotlib.pyplot as plt import numpy as np +from scipy.interpolate import interp1d def test_morphsqueeze(): @@ -41,19 +42,26 @@ def test_morphsqueeze(): # Apply squeeze parameters to uniform data to get the squeezed data # Include squeeze_0 for squeezes with offset squeeze_0 = 0.2 - squeeze_1 = 0.001 - squeeze_2 = 0.001 + squeeze_1 = -0.001 + squeeze_2 = -0.001 x_squeezed = x + squeeze_0 + squeeze_1 * x**2 + squeeze_2 * x**3 y_squeezed = np.sin(x_squeezed) # Unsqueeze the data by interpolating back to uniform grid - y_unsqueezed = np.interp(x, x_squeezed, y_squeezed) + # y_unsqueezed = np.interp(x, x_squeezed, y_squeezed) + y_unsqueezed = interp1d( + x_squeezed, + y_squeezed, + kind="cubic", + bounds_error=False, + fill_value="extrapolate", + )(x) y_actual = y_unsqueezed # Check that the unsqueezed (actual) data matches the expected data # Including tolerance error because I was having issues # with y_actual == y_expected. I think is because interpolation? - assert np.allclose(y_actual, y_expected, atol=1) + assert np.allclose(y_actual, y_expected, atol=100) # Plot to verify what we are doing plt.figure(figsize=(7, 4)) From ca69295d0aca3126fa48028cfe981cb408f00979 Mon Sep 17 00:00:00 2001 From: Luis Kitsu Iglesias Date: Mon, 7 Apr 2025 11:50:26 -0600 Subject: [PATCH 04/10] test: adding pytest.parametrize and numpy.polynomial --- news/test_squeeze.rst | 2 +- tests/test_morphsqueeze.py | 88 ++++++++++++++++---------------------- 2 files changed, 38 insertions(+), 52 deletions(-) diff --git a/news/test_squeeze.rst b/news/test_squeeze.rst index 47cc0d4..750fbcf 100644 --- a/news/test_squeeze.rst +++ b/news/test_squeeze.rst @@ -1,6 +1,6 @@ **Added:** -* Squeeze test: given a squeezed signal, unsqueezing recovers the original +* Polynomial squeeze of x-axis of morphed data **Changed:** diff --git a/tests/test_morphsqueeze.py b/tests/test_morphsqueeze.py index 0fb30cf..cf999b2 100644 --- a/tests/test_morphsqueeze.py +++ b/tests/test_morphsqueeze.py @@ -1,54 +1,40 @@ -""" -The squeeze morph is used to correct for small non-linear geometric distortions -from detectors that are not effectively corrected during calibration, such as -individual module misalignment or tilt. This squeezing is applied as: -x_squeezed = x + squeeze_0 + squeeze_1 * x**2 + squeeze_2 * x**3 - -The squeeze distortions that we might encounter practically are going to be -very small. Furthermore, large values for the squeezing parameters lead to -missing values during interpolation. Therefore is important to use small -squeezing values to avoid error or unphysical results. -Safe bounds for the squeezing parameters are: -squeeze_0 [-0.2, 0.2] -squeeze_1 [-0.001, 0.001] -squeeze_2 [-0.0001, 0.0001] -Values outside these bounds should be used carefully. -Note that these bounds are established for an x-axis that goes from 0 to 10. -""" - -import matplotlib.pyplot as plt import numpy as np +import pytest +from numpy.polynomial import Polynomial from scipy.interpolate import interp1d -def test_morphsqueeze(): - """ - Test that we can unsqueeze squeezed data. - The test inputs are an expected uniform target (e.g. synchrotron data) - and a squeezed version of the target (e.g. XFEL data). The squeezed data - is created by applying a nonlinear distortion to the uniform target. - Both input data are in the same uniform x-axis grid. - Then we unsqueeze the squeezed data by doing the inverse transformation - using interpolatiion. - Finally we check that the unsqueezed data matches the expected uniform - target. - """ - +@pytest.mark.parametrize( + "squeeze_coeffs", + [ + # The order of coefficients is [a0, a1, a2, ..., an] + # Negative cubic squeeze coefficients + [-0.2, -0.01, -0.001, -0.001], + # Positive cubic squeeze coefficients + [0.2, 0.01, 0.001, 0.001], + # Positive and negative cubic squeeze coefficients + [0.2, -0.01, 0.001, -0.001], + # Quadratic squeeze coefficients + [-0.2, 0.005, -0.003], + # Linear squeeze coefficients + [0.1, 0.3], + # 4th order squeeze coefficients + [0.2, -0.01, 0.001, -0.001, 0.0001], + ], +) +def test_morphsqueeze(squeeze_coeffs): # Uniform x-axis grid. This is the same x-axis for all data. x = np.linspace(0, 10, 1000) # Expected uniform target y_expected = np.sin(x) + # Create polynomial based on a list of values for polynomial coefficients + squeeze_polynomial = Polynomial(squeeze_coeffs) # Apply squeeze parameters to uniform data to get the squeezed data - # Include squeeze_0 for squeezes with offset - squeeze_0 = 0.2 - squeeze_1 = -0.001 - squeeze_2 = -0.001 - x_squeezed = x + squeeze_0 + squeeze_1 * x**2 + squeeze_2 * x**3 + x_squeezed = x + squeeze_polynomial(x) y_squeezed = np.sin(x_squeezed) # Unsqueeze the data by interpolating back to uniform grid - # y_unsqueezed = np.interp(x, x_squeezed, y_squeezed) y_unsqueezed = interp1d( x_squeezed, y_squeezed, @@ -59,16 +45,16 @@ def test_morphsqueeze(): y_actual = y_unsqueezed # Check that the unsqueezed (actual) data matches the expected data - # Including tolerance error because I was having issues - # with y_actual == y_expected. I think is because interpolation? - assert np.allclose(y_actual, y_expected, atol=100) - - # Plot to verify what we are doing - plt.figure(figsize=(7, 4)) - plt.plot(x, y_expected, color="black", label="Expected uniform data") - plt.plot(x, y_squeezed, "--", color="purple", label="Squeezed data") - plt.plot(x, y_unsqueezed, "--", color="gold", label="Unsqueezed data") - plt.xlabel("x") - plt.ylabel("y") - plt.legend() - plt.show() + # Including tolerance error because of extrapolation error + assert np.allclose(y_actual, y_expected, atol=1) + + # This plotting code was used for the comments in the github + # PR https://github.com/diffpy/diffpy.morph/pull/180 + # plt.figure(figsize=(7, 4)) + # plt.plot(x, y_expected, color="black", label="Expected uniform data") + # plt.plot(x, y_squeezed, "--", color="purple", label="Squeezed data") + # plt.plot(x, y_unsqueezed, "--", color="gold", label="Unsqueezed data") + # plt.xlabel("x") + # plt.ylabel("y") + # plt.legend() + # plt.show() From a1b64251d1cb0108e97e7d1597cccdaabd2acc74 Mon Sep 17 00:00:00 2001 From: Luis Kitsu Iglesias Date: Mon, 7 Apr 2025 18:21:30 -0600 Subject: [PATCH 05/10] test/func: added squeeze morph class and call it in the test --- src/diffpy/morph/morphs/morphsqueeze.py | 48 ++++++++++++++++++++++ tests/test_morphsqueeze.py | 54 ++++++++++--------------- 2 files changed, 69 insertions(+), 33 deletions(-) create mode 100644 src/diffpy/morph/morphs/morphsqueeze.py diff --git a/src/diffpy/morph/morphs/morphsqueeze.py b/src/diffpy/morph/morphs/morphsqueeze.py new file mode 100644 index 0000000..3451433 --- /dev/null +++ b/src/diffpy/morph/morphs/morphsqueeze.py @@ -0,0 +1,48 @@ +import numpy as np +from numpy.polynomial import Polynomial +from scipy.interpolate import interp1d + +from diffpy.morph.morphs.morph import LABEL_GR, LABEL_RA, Morph + + +class MorphSqueeze(Morph): + """Squeeze the morph function. + + This applies a polynomial to squeeze the morph non-linearly. + + Configuration Variables + ----------------------- + squeeze + list or array-like + Polynomial coefficients [a0, a1, ..., an] for the squeeze function. + """ + + # Define input output types + summary = "Squeeze morph by polynomial shift" + xinlabel = LABEL_RA + yinlabel = LABEL_GR + xoutlabel = LABEL_RA + youtlabel = LABEL_GR + parnames = ["squeeze"] + + def morph(self, x_morph, y_morph, x_target, y_target): + + Morph.morph(self, x_morph, y_morph, x_target, y_target) + if self.squeeze is None or np.allclose(self.squeeze, 0): + self.x_morph_out = self.x_morph_in + self.y_morph_out = self.y_morph_in + return self.xyallout + + squeeze_polynomial = Polynomial(self.squeeze) + x_squeezed = self.x_morph_in + squeeze_polynomial(self.x_morph_in) + + self.y_morph_out = interp1d( + x_squeezed, + self.y_morph_in, + kind="cubic", + bounds_error=False, + fill_value="extrapolate", + )(self.x_morph_in) + self.x_morph_out = self.x_morph_in + + return self.xyallout diff --git a/tests/test_morphsqueeze.py b/tests/test_morphsqueeze.py index cf999b2..ab47ffa 100644 --- a/tests/test_morphsqueeze.py +++ b/tests/test_morphsqueeze.py @@ -1,7 +1,8 @@ import numpy as np import pytest from numpy.polynomial import Polynomial -from scipy.interpolate import interp1d + +from diffpy.morph.morphs.morphsqueeze import MorphSqueeze @pytest.mark.parametrize( @@ -20,41 +21,28 @@ [0.1, 0.3], # 4th order squeeze coefficients [0.2, -0.01, 0.001, -0.001, 0.0001], + # Testing zeros + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ], ) def test_morphsqueeze(squeeze_coeffs): - # Uniform x-axis grid. This is the same x-axis for all data. - x = np.linspace(0, 10, 1000) - # Expected uniform target - y_expected = np.sin(x) - # Create polynomial based on a list of values for polynomial coefficients + x_target = np.linspace(0, 10, 1000) + y_target = np.sin(x_target) + squeeze_polynomial = Polynomial(squeeze_coeffs) - # Apply squeeze parameters to uniform data to get the squeezed data - x_squeezed = x + squeeze_polynomial(x) - y_squeezed = np.sin(x_squeezed) - - # Unsqueeze the data by interpolating back to uniform grid - y_unsqueezed = interp1d( - x_squeezed, - y_squeezed, - kind="cubic", - bounds_error=False, - fill_value="extrapolate", - )(x) - y_actual = y_unsqueezed - - # Check that the unsqueezed (actual) data matches the expected data + x_squeezed = x_target + squeeze_polynomial(x_target) + + x_morph = x_target.copy() + y_morph = np.sin(x_squeezed) + + morph = MorphSqueeze() + morph.squeeze = squeeze_coeffs + + x_actual, y_actual, x_expected, y_expected = morph( + x_morph, y_morph, x_target, y_target + ) + + # Check that the morphed (actual) data matches the expected data # Including tolerance error because of extrapolation error - assert np.allclose(y_actual, y_expected, atol=1) - - # This plotting code was used for the comments in the github - # PR https://github.com/diffpy/diffpy.morph/pull/180 - # plt.figure(figsize=(7, 4)) - # plt.plot(x, y_expected, color="black", label="Expected uniform data") - # plt.plot(x, y_squeezed, "--", color="purple", label="Squeezed data") - # plt.plot(x, y_unsqueezed, "--", color="gold", label="Unsqueezed data") - # plt.xlabel("x") - # plt.ylabel("y") - # plt.legend() - # plt.show() + assert np.allclose(y_actual, y_expected, atol=0.1) From 14d92cbd08bd1c5b62fd1fd23e8d89b41fff71a4 Mon Sep 17 00:00:00 2001 From: Luis Kitsu Iglesias Date: Tue, 8 Apr 2025 18:27:40 -0600 Subject: [PATCH 06/10] Test: added expanded x-axis and no tolerance --- src/diffpy/morph/morphs/morphsqueeze.py | 9 ++----- tests/test_morphsqueeze.py | 36 ++++++++++++++++--------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/diffpy/morph/morphs/morphsqueeze.py b/src/diffpy/morph/morphs/morphsqueeze.py index 3451433..8270094 100644 --- a/src/diffpy/morph/morphs/morphsqueeze.py +++ b/src/diffpy/morph/morphs/morphsqueeze.py @@ -26,7 +26,6 @@ class MorphSqueeze(Morph): parnames = ["squeeze"] def morph(self, x_morph, y_morph, x_target, y_target): - Morph.morph(self, x_morph, y_morph, x_target, y_target) if self.squeeze is None or np.allclose(self.squeeze, 0): self.x_morph_out = self.x_morph_in @@ -37,12 +36,8 @@ def morph(self, x_morph, y_morph, x_target, y_target): x_squeezed = self.x_morph_in + squeeze_polynomial(self.x_morph_in) self.y_morph_out = interp1d( - x_squeezed, - self.y_morph_in, - kind="cubic", - bounds_error=False, - fill_value="extrapolate", + x_squeezed, self.y_morph_in, kind="cubic", bounds_error=False )(self.x_morph_in) - self.x_morph_out = self.x_morph_in + self.x_morph_out = self.x_morph_in return self.xyallout diff --git a/tests/test_morphsqueeze.py b/tests/test_morphsqueeze.py index ab47ffa..86fd2fa 100644 --- a/tests/test_morphsqueeze.py +++ b/tests/test_morphsqueeze.py @@ -14,26 +14,31 @@ # Positive cubic squeeze coefficients [0.2, 0.01, 0.001, 0.001], # Positive and negative cubic squeeze coefficients - [0.2, -0.01, 0.001, -0.001], + [0.2, -0.01, 0.002, -0.001], # Quadratic squeeze coefficients - [-0.2, 0.005, -0.003], + [-0.2, 0.005, -0.007], # Linear squeeze coefficients [0.1, 0.3], # 4th order squeeze coefficients - [0.2, -0.01, 0.001, -0.001, 0.0001], - # Testing zeros + [0.2, -0.01, 0.001, -0.001, 0.0004], + # Zeros and non-zeros, expect 0 + a1x + 0 + a3x**3 + [0, 0.03, 0, -0.001], + # Testing zeros, expect no squeezing [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ], ) def test_morphsqueeze(squeeze_coeffs): - - x_target = np.linspace(0, 10, 1000) + x_target = np.linspace(0, 10, 1001) y_target = np.sin(x_target) + x_make = np.linspace(-3, 13, 1601) + lower_idx = np.where(x_make == 0.0)[0][0] + upper_idx = np.where(x_make == 10.0)[0][0] + squeeze_polynomial = Polynomial(squeeze_coeffs) - x_squeezed = x_target + squeeze_polynomial(x_target) + x_squeezed = x_make + squeeze_polynomial(x_make) - x_morph = x_target.copy() + x_morph = x_make.copy() y_morph = np.sin(x_squeezed) morph = MorphSqueeze() @@ -42,7 +47,14 @@ def test_morphsqueeze(squeeze_coeffs): x_actual, y_actual, x_expected, y_expected = morph( x_morph, y_morph, x_target, y_target ) - - # Check that the morphed (actual) data matches the expected data - # Including tolerance error because of extrapolation error - assert np.allclose(y_actual, y_expected, atol=0.1) + y_actual = y_actual[lower_idx : upper_idx + 1] + assert np.allclose(y_actual, y_expected) + + # Plotting code used for figures in PR comments + # https://github.com/diffpy/diffpy.morph/pull/180 + # plt.figure() + # plt.scatter(x_expected, y_expected, color='black', label='Expected') + # plt.plot(x_morph, y_morph, color='purple', label='morph') + # plt.plot(x_actual, y_actual, '--', color='gold', label='Actual') + # plt.legend() + # plt.show() From a48ce43c6919a4edfbd2124dbef46796ae21d63e Mon Sep 17 00:00:00 2001 From: Luis Kitsu Iglesias Date: Wed, 9 Apr 2025 14:26:17 -0600 Subject: [PATCH 07/10] Test: added different grids --- src/diffpy/morph/morphs/morphsqueeze.py | 5 +--- tests/test_morphsqueeze.py | 31 +++++++++++-------------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/diffpy/morph/morphs/morphsqueeze.py b/src/diffpy/morph/morphs/morphsqueeze.py index 8270094..f25eaa1 100644 --- a/src/diffpy/morph/morphs/morphsqueeze.py +++ b/src/diffpy/morph/morphs/morphsqueeze.py @@ -1,4 +1,3 @@ -import numpy as np from numpy.polynomial import Polynomial from scipy.interpolate import interp1d @@ -27,17 +26,15 @@ class MorphSqueeze(Morph): def morph(self, x_morph, y_morph, x_target, y_target): Morph.morph(self, x_morph, y_morph, x_target, y_target) - if self.squeeze is None or np.allclose(self.squeeze, 0): + if self.squeeze is None: self.x_morph_out = self.x_morph_in self.y_morph_out = self.y_morph_in return self.xyallout squeeze_polynomial = Polynomial(self.squeeze) x_squeezed = self.x_morph_in + squeeze_polynomial(self.x_morph_in) - self.y_morph_out = interp1d( x_squeezed, self.y_morph_in, kind="cubic", bounds_error=False )(self.x_morph_in) - self.x_morph_out = self.x_morph_in return self.xyallout diff --git a/tests/test_morphsqueeze.py b/tests/test_morphsqueeze.py index 86fd2fa..b04b6dc 100644 --- a/tests/test_morphsqueeze.py +++ b/tests/test_morphsqueeze.py @@ -1,6 +1,7 @@ import numpy as np import pytest from numpy.polynomial import Polynomial +from scipy.interpolate import interp1d from diffpy.morph.morphs.morphsqueeze import MorphSqueeze @@ -21,40 +22,36 @@ [0.1, 0.3], # 4th order squeeze coefficients [0.2, -0.01, 0.001, -0.001, 0.0004], - # Zeros and non-zeros, expect 0 + a1x + 0 + a3x**3 + # Zeros and non-zeros, the full polynomial is applied [0, 0.03, 0, -0.001], # Testing zeros, expect no squeezing - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], ], ) def test_morphsqueeze(squeeze_coeffs): - x_target = np.linspace(0, 10, 1001) - y_target = np.sin(x_target) - - x_make = np.linspace(-3, 13, 1601) - lower_idx = np.where(x_make == 0.0)[0][0] - upper_idx = np.where(x_make == 10.0)[0][0] - + x_expected = np.linspace(0, 10, 1001) + y_expected = np.sin(x_expected) + x_make = np.linspace(-3, 13, 3250) squeeze_polynomial = Polynomial(squeeze_coeffs) x_squeezed = x_make + squeeze_polynomial(x_make) - - x_morph = x_make.copy() y_morph = np.sin(x_squeezed) - morph = MorphSqueeze() morph.squeeze = squeeze_coeffs - - x_actual, y_actual, x_expected, y_expected = morph( - x_morph, y_morph, x_target, y_target + x_actual, y_actual, x_target, y_target = morph( + x_make, y_morph, x_expected, y_expected ) - y_actual = y_actual[lower_idx : upper_idx + 1] + y_actual = interp1d(x_actual, y_actual)(x_target) + x_actual = x_target assert np.allclose(y_actual, y_expected) + assert np.allclose(x_actual, x_expected) + assert np.allclose(x_target, x_expected) + assert np.allclose(y_target, y_expected) # Plotting code used for figures in PR comments # https://github.com/diffpy/diffpy.morph/pull/180 # plt.figure() # plt.scatter(x_expected, y_expected, color='black', label='Expected') - # plt.plot(x_morph, y_morph, color='purple', label='morph') + # plt.plot(x_make, y_morph, color='purple', label='morph') # plt.plot(x_actual, y_actual, '--', color='gold', label='Actual') # plt.legend() # plt.show() From d42d23430bf2ae9fa41268946d6f8f74ee36cc16 Mon Sep 17 00:00:00 2001 From: Luis Kitsu Iglesias Date: Wed, 9 Apr 2025 15:59:29 -0600 Subject: [PATCH 08/10] Test: changed names, coarsed grid and added comment --- src/diffpy/morph/morphs/morphsqueeze.py | 13 ++++++---- tests/test_morphsqueeze.py | 34 +++++++++---------------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/src/diffpy/morph/morphs/morphsqueeze.py b/src/diffpy/morph/morphs/morphsqueeze.py index f25eaa1..5581b31 100644 --- a/src/diffpy/morph/morphs/morphsqueeze.py +++ b/src/diffpy/morph/morphs/morphsqueeze.py @@ -1,5 +1,5 @@ from numpy.polynomial import Polynomial -from scipy.interpolate import interp1d +from scipy.interpolate import CubicSpline from diffpy.morph.morphs.morph import LABEL_GR, LABEL_RA, Morph @@ -33,8 +33,11 @@ def morph(self, x_morph, y_morph, x_target, y_target): squeeze_polynomial = Polynomial(self.squeeze) x_squeezed = self.x_morph_in + squeeze_polynomial(self.x_morph_in) - self.y_morph_out = interp1d( - x_squeezed, self.y_morph_in, kind="cubic", bounds_error=False - )(self.x_morph_in) - self.x_morph_out = self.x_morph_in + self.y_morph_out = CubicSpline(x_squeezed, self.y_morph_in)( + self.x_morph_in + ) + self.y_morph_out = CubicSpline(self.x_morph_in, self.y_morph_out)( + self.x_target_in + ) + self.x_morph_out = self.x_target_in return self.xyallout diff --git a/tests/test_morphsqueeze.py b/tests/test_morphsqueeze.py index b04b6dc..89eec27 100644 --- a/tests/test_morphsqueeze.py +++ b/tests/test_morphsqueeze.py @@ -1,7 +1,6 @@ import numpy as np import pytest from numpy.polynomial import Polynomial -from scipy.interpolate import interp1d from diffpy.morph.morphs.morphsqueeze import MorphSqueeze @@ -29,29 +28,20 @@ ], ) def test_morphsqueeze(squeeze_coeffs): - x_expected = np.linspace(0, 10, 1001) - y_expected = np.sin(x_expected) - x_make = np.linspace(-3, 13, 3250) + x_target_expected = np.linspace(0, 10, 101) + y_target_expected = np.sin(x_target_expected) + # Different grid for morph data to test inputs with different grids + # Morph grid must be finer than the target to avoid interpolation issues + x_morph = np.linspace(-3, 13, 301) squeeze_polynomial = Polynomial(squeeze_coeffs) - x_squeezed = x_make + squeeze_polynomial(x_make) + x_squeezed = x_morph + squeeze_polynomial(x_morph) y_morph = np.sin(x_squeezed) morph = MorphSqueeze() morph.squeeze = squeeze_coeffs - x_actual, y_actual, x_target, y_target = morph( - x_make, y_morph, x_expected, y_expected + x_morph_actual, y_morph_actual, x_target_actual, y_target_actual = morph( + x_morph, y_morph, x_target_expected, y_target_expected ) - y_actual = interp1d(x_actual, y_actual)(x_target) - x_actual = x_target - assert np.allclose(y_actual, y_expected) - assert np.allclose(x_actual, x_expected) - assert np.allclose(x_target, x_expected) - assert np.allclose(y_target, y_expected) - - # Plotting code used for figures in PR comments - # https://github.com/diffpy/diffpy.morph/pull/180 - # plt.figure() - # plt.scatter(x_expected, y_expected, color='black', label='Expected') - # plt.plot(x_make, y_morph, color='purple', label='morph') - # plt.plot(x_actual, y_actual, '--', color='gold', label='Actual') - # plt.legend() - # plt.show() + assert np.allclose(y_morph_actual, y_target_expected) + assert np.allclose(x_morph_actual, x_target_expected) + assert np.allclose(x_target_actual, x_target_expected) + assert np.allclose(y_target_actual, y_target_expected) From f47e167ae5d1da3926d98068e7fc8839816a43a6 Mon Sep 17 00:00:00 2001 From: Luis Kitsu Iglesias Date: Thu, 10 Apr 2025 09:30:19 -0600 Subject: [PATCH 09/10] Test/funct: trim target data to overlapping region and added more test --- src/diffpy/morph/morphs/morphsqueeze.py | 21 ++++-- tests/test_morphsqueeze.py | 88 +++++++++++++++++++++---- 2 files changed, 92 insertions(+), 17 deletions(-) diff --git a/src/diffpy/morph/morphs/morphsqueeze.py b/src/diffpy/morph/morphs/morphsqueeze.py index 5581b31..9a615b0 100644 --- a/src/diffpy/morph/morphs/morphsqueeze.py +++ b/src/diffpy/morph/morphs/morphsqueeze.py @@ -1,3 +1,4 @@ +import numpy as np from numpy.polynomial import Polynomial from scipy.interpolate import CubicSpline @@ -7,7 +8,12 @@ class MorphSqueeze(Morph): """Squeeze the morph function. - This applies a polynomial to squeeze the morph non-linearly. + This applies a polynomial to squeeze the morph non-linearly. The resulting + squeezed morph is interpolated to the (trimmed) target grid. + Only the overlapping region between the squeezed morph and the target + grid is used. The target is trimmed (or not) accordingly, and the final + outputs (morph and target) are returned on the same grid, defined by this + trimmed target range. Configuration Variables ----------------------- @@ -33,11 +39,14 @@ def morph(self, x_morph, y_morph, x_target, y_target): squeeze_polynomial = Polynomial(self.squeeze) x_squeezed = self.x_morph_in + squeeze_polynomial(self.x_morph_in) + x_min = max(float(self.x_target_in[0]), float(x_squeezed[0])) + x_max = min(float(self.x_target_in[-1]), float(x_squeezed[-1])) + min_index = np.where(self.x_target_in >= x_min)[0][0] + max_index = np.where(self.x_target_in <= x_max)[0][-1] + self.x_target_out = self.x_target_in[min_index : max_index + 1] + self.y_target_out = self.y_target_in[min_index : max_index + 1] self.y_morph_out = CubicSpline(x_squeezed, self.y_morph_in)( - self.x_morph_in + self.x_target_out ) - self.y_morph_out = CubicSpline(self.x_morph_in, self.y_morph_out)( - self.x_target_in - ) - self.x_morph_out = self.x_target_in + self.x_morph_out = self.x_target_out return self.xyallout diff --git a/tests/test_morphsqueeze.py b/tests/test_morphsqueeze.py index 89eec27..3091128 100644 --- a/tests/test_morphsqueeze.py +++ b/tests/test_morphsqueeze.py @@ -27,21 +27,87 @@ [0, 0, 0, 0, 0, 0], ], ) -def test_morphsqueeze(squeeze_coeffs): - x_target_expected = np.linspace(0, 10, 101) - y_target_expected = np.sin(x_target_expected) - # Different grid for morph data to test inputs with different grids - # Morph grid must be finer than the target to avoid interpolation issues - x_morph = np.linspace(-3, 13, 301) +def test_morphsqueeze_target_extends_beyond_morph(squeeze_coeffs): + # Target data extends beyond morph and different grids + x_target = np.linspace(-3, 25, 401) + y_target = np.sin(x_target) + x_morph = np.linspace(0, 10, 301) squeeze_polynomial = Polynomial(squeeze_coeffs) x_squeezed = x_morph + squeeze_polynomial(x_morph) y_morph = np.sin(x_squeezed) + # Trim target data to the region overlapping with the squeezed morph + x_min = max(float(x_target[0]), float(x_squeezed[0])) + x_max = min(float(x_target[-1]), float(x_squeezed[-1])) + min_index = np.where(x_target >= x_min)[0][0] + max_index = np.where(x_target <= x_max)[0][-1] + x_morph_expected = x_target[min_index : max_index + 1] + y_morph_expected = y_target[min_index : max_index + 1] morph = MorphSqueeze() morph.squeeze = squeeze_coeffs x_morph_actual, y_morph_actual, x_target_actual, y_target_actual = morph( - x_morph, y_morph, x_target_expected, y_target_expected + x_morph, y_morph, x_target, y_target ) - assert np.allclose(y_morph_actual, y_target_expected) - assert np.allclose(x_morph_actual, x_target_expected) - assert np.allclose(x_target_actual, x_target_expected) - assert np.allclose(y_target_actual, y_target_expected) + assert np.allclose(y_morph_actual, y_morph_expected) + assert np.allclose(x_morph_actual, x_morph_expected) + assert np.allclose(x_target_actual, x_morph_expected) + assert np.allclose(y_target_actual, y_morph_expected) + + import matplotlib.pyplot as plt + + plt.figure() + plt.plot(x_target, y_target, color="gray", label="target") + plt.plot(x_morph, y_morph, color="black", label="morph") + plt.scatter(x_morph_actual, y_morph_actual, color="gold", label="actual") + plt.plot( + x_morph_expected, y_morph_expected, color="purple", label="expected" + ) + plt.legend() + plt.show() + + +@pytest.mark.parametrize( + "squeeze_coeffs", + [ + # The order of coefficients is [a0, a1, a2, ..., an] + # Negative cubic squeeze coefficients + [-0.2, -0.01, -0.001, -0.001], + # Positive cubic squeeze coefficients + [0.2, 0.01, 0.001, 0.001], + # Positive and negative cubic squeeze coefficients + [0.2, -0.01, 0.002, -0.001], + # Quadratic squeeze coefficients + [-0.2, 0.005, -0.007], + # Linear squeeze coefficients + [0.1, 0.3], + # 4th order squeeze coefficients + [0.2, -0.01, 0.001, -0.001, 0.0004], + # Zeros and non-zeros, the full polynomial is applied + [0, 0.03, 0, -0.001], + # Testing zeros, expect no squeezing + [0, 0, 0, 0, 0, 0], + ], +) +def test_morphsqueeze_morph_extends_beyond_target(squeeze_coeffs): + # Different grid for morph and target data to test different grids + x_target = np.linspace(0, 10, 101) + y_target = np.sin(x_target) + x_morph = np.linspace(-3, 15, 301) + squeeze_polynomial = Polynomial(squeeze_coeffs) + x_squeezed = x_morph + squeeze_polynomial(x_morph) + y_morph = np.sin(x_squeezed) + # Trim target data to the region overlapping with the squeezed morph + x_min = max(float(x_target[0]), float(x_squeezed[0])) + x_max = min(float(x_target[-1]), float(x_squeezed[-1])) + min_index = np.where(x_target >= x_min)[0][0] + max_index = np.where(x_target <= x_max)[0][-1] + x_morph_expected = x_target[min_index : max_index + 1] + y_morph_expected = y_target[min_index : max_index + 1] + morph = MorphSqueeze() + morph.squeeze = squeeze_coeffs + x_morph_actual, y_morph_actual, x_target_actual, y_target_actual = morph( + x_morph, y_morph, x_target, y_target + ) + assert np.allclose(y_morph_actual, y_morph_expected) + assert np.allclose(x_morph_actual, x_morph_expected) + assert np.allclose(x_target_actual, x_morph_expected) + assert np.allclose(y_target_actual, y_morph_expected) From 4933b4fa61b9939ae443a32a9b63142148d0db91 Mon Sep 17 00:00:00 2001 From: Luis Kitsu Iglesias Date: Thu, 10 Apr 2025 09:33:39 -0600 Subject: [PATCH 10/10] Test: deleted plotting --- tests/test_morphsqueeze.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/test_morphsqueeze.py b/tests/test_morphsqueeze.py index 3091128..b3d1929 100644 --- a/tests/test_morphsqueeze.py +++ b/tests/test_morphsqueeze.py @@ -52,18 +52,6 @@ def test_morphsqueeze_target_extends_beyond_morph(squeeze_coeffs): assert np.allclose(x_target_actual, x_morph_expected) assert np.allclose(y_target_actual, y_morph_expected) - import matplotlib.pyplot as plt - - plt.figure() - plt.plot(x_target, y_target, color="gray", label="target") - plt.plot(x_morph, y_morph, color="black", label="morph") - plt.scatter(x_morph_actual, y_morph_actual, color="gold", label="actual") - plt.plot( - x_morph_expected, y_morph_expected, color="purple", label="expected" - ) - plt.legend() - plt.show() - @pytest.mark.parametrize( "squeeze_coeffs",