Skip to content

test: adding a test to unsqueeze squeezed data #180

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 10 commits into from
23 changes: 23 additions & 0 deletions news/test_squeeze.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
**Added:**

* Polynomial squeeze of x-axis of morphed data

**Changed:**

* <news item>

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
52 changes: 52 additions & 0 deletions src/diffpy/morph/morphs/morphsqueeze.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import numpy as np
from numpy.polynomial import Polynomial
from scipy.interpolate import CubicSpline

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. 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
-----------------------
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:
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)
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_target_out
)
self.x_morph_out = self.x_target_out
return self.xyallout
101 changes: 101 additions & 0 deletions tests/test_morphsqueeze.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import numpy as np
import pytest
from numpy.polynomial import Polynomial

from diffpy.morph.morphs.morphsqueeze import MorphSqueeze


@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_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, 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)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

close up. no blank line here (and everywhere below)


@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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure we have thus quite right. Let's take a step back and start over. Don't modify any code, let's just discuss behavior again. Things got a bit more complicated when we went to different grids. This wasn't a Use Case (UC) before so we need to move carefully, as it will have knock-on effects for the test of the package.

Since you learned everything else, we may as well learn about Use Cases which is what we use when we develop new functionality, as here. I actually suggest we close this PR and open a new one. We will reuse code from here because the code is good....

The approach is first UCs (what are some little scenarios that exemplify user behavior). Then tests that capture the UCs we want to implement (no code so all tests will fail, but we write an empty function so we get the signature and docstring right)

Only when this is fully covered we write the code to pass the tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Simon, hope you have a great flight!
I like your suggestion, lets close this PR and open a new one. I am going to create a new issue with the UCs you provided. Then I will create a test for each UC with the function being empty, just the behavior we want.

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)