Skip to content

Commit b6136cf

Browse files
committed
Squeeze morph warning handling
1 parent f5fc9ac commit b6136cf

File tree

6 files changed

+224
-8
lines changed

6 files changed

+224
-8
lines changed

news/squeeze_warnings.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
**Added:**
2+
3+
* Warnings added to `squeeze` morph if the squeeze causes the grid to become non-monotonic.
4+
5+
**Changed:**
6+
7+
* <news item>
8+
9+
**Deprecated:**
10+
11+
* <news item>
12+
13+
**Removed:**
14+
15+
* <news item>
16+
17+
**Fixed:**
18+
19+
* <news item>
20+
21+
**Security:**
22+
23+
* <news item>

src/diffpy/morph/morph_io.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -408,9 +408,9 @@ def tabulate_results(multiple_morph_results):
408408
return tabulated_results
409409

410410

411-
def handle_warnings(squeeze_morph):
412-
if squeeze_morph is not None:
413-
extrapolation_info = squeeze_morph.extrapolation_info
411+
def handle_extrapolation_warnings(morph):
412+
if morph is not None:
413+
extrapolation_info = morph.extrapolation_info
414414
is_extrap_low = extrapolation_info["is_extrap_low"]
415415
is_extrap_high = extrapolation_info["is_extrap_high"]
416416
cutoff_low = extrapolation_info["cutoff_low"]
@@ -443,3 +443,22 @@ def handle_warnings(squeeze_morph):
443443
wmsg,
444444
UserWarning,
445445
)
446+
447+
448+
def handle_check_increase_warning(squeeze_morph):
449+
if squeeze_morph is not None:
450+
if squeeze_morph.strictly_increasing:
451+
wmsg = None
452+
else:
453+
wmsg = (
454+
"Warning: The squeeze morph has interpolated your morphed "
455+
"function from a non-monotonically increasing grid. "
456+
"This can result in strange behavior in the non-unique "
457+
"grid regions. To disable this setting, "
458+
"please enable --check-increasing."
459+
)
460+
if wmsg:
461+
warnings.warn(
462+
wmsg,
463+
UserWarning,
464+
)

src/diffpy/morph/morphapp.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -707,9 +707,10 @@ def single_morph(
707707
chain(x_morph, y_morph, x_target, y_target)
708708

709709
# THROW ANY WARNINGS HERE
710-
io.handle_warnings(squeeze_morph)
711-
io.handle_warnings(shift_morph)
712-
io.handle_warnings(stretch_morph)
710+
io.handle_extrapolation_warnings(squeeze_morph)
711+
io.handle_check_increase_warning(squeeze_morph)
712+
io.handle_extrapolation_warnings(shift_morph)
713+
io.handle_extrapolation_warnings(stretch_morph)
713714

714715
# Get Rw for the morph range
715716
rw = tools.getRw(chain)

src/diffpy/morph/morphpy.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def __get_morph_opts__(parser, scale, stretch, smear, plot, **kwargs):
5151
"reverse",
5252
"diff",
5353
"get-diff",
54+
"check-increase",
5455
]
5556
opts_to_ignore = ["multiple-morphs", "multiple-targets"]
5657
for opt in opts_storing_values:

src/diffpy/morph/morphs/morphsqueeze.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Class MorphSqueeze -- Apply a polynomial to squeeze the morph
22
function."""
33

4+
import numpy
45
from numpy.polynomial import Polynomial
56
from scipy.interpolate import CubicSpline
67

@@ -67,10 +68,36 @@ class MorphSqueeze(Morph):
6768
extrap_index_high = None
6869
squeeze_cutoff_low = None
6970
squeeze_cutoff_high = None
71+
strictly_increasing = None
7072

7173
def __init__(self, config=None):
7274
super().__init__(config)
7375

76+
def _ensure_strictly_increase(self, x, x_sorted):
77+
if list(x) != list(x_sorted):
78+
self.strictly_increasing = False
79+
else:
80+
self.strictly_increasing = True
81+
82+
def _sort_squeeze(self, x, y):
83+
"""Sort x,y according to the value of x."""
84+
xy = list(zip(x, y))
85+
xy_sorted = sorted(xy, key=lambda pair: pair[0])
86+
x_sorted, y_sorted = list(zip(*xy_sorted))
87+
return x_sorted, y_sorted
88+
89+
def _handle_duplicates(self, x, y):
90+
"""Remove duplicated x and use the mean value of y corresponded
91+
to the duplicated x."""
92+
unq_x, unq_inv = numpy.unique(x, return_inverse=True)
93+
if len(unq_x) == len(x):
94+
return x, y
95+
else:
96+
y_avg = numpy.zeros_like(unq_x)
97+
for i in range(len(unq_x)):
98+
y_avg[i] = numpy.array(y)[unq_inv == i].mean()
99+
return unq_x, y_avg
100+
74101
def morph(self, x_morph, y_morph, x_target, y_target):
75102
"""Apply a polynomial to squeeze the morph function.
76103
@@ -82,9 +109,16 @@ def morph(self, x_morph, y_morph, x_target, y_target):
82109
coeffs = [self.squeeze[f"a{i}"] for i in range(len(self.squeeze))]
83110
squeeze_polynomial = Polynomial(coeffs)
84111
x_squeezed = self.x_morph_in + squeeze_polynomial(self.x_morph_in)
85-
self.y_morph_out = CubicSpline(x_squeezed, self.y_morph_in)(
112+
x_squeezed_sorted, y_morph_sorted = self._sort_squeeze(
113+
x_squeezed, self.y_morph_in
114+
)
115+
self._ensure_strictly_increase(x_squeezed, x_squeezed_sorted)
116+
x_squeezed_sorted, y_morph_sorted = self._handle_duplicates(
117+
x_squeezed_sorted, y_morph_sorted
118+
)
119+
self.y_morph_out = CubicSpline(x_squeezed_sorted, y_morph_sorted)(
86120
self.x_morph_in
87121
)
88-
self.set_extrapolation_info(x_squeezed, self.x_morph_in)
122+
self.set_extrapolation_info(x_squeezed_sorted, self.x_morph_in)
89123

90124
return self.xyallout

tests/test_morphsqueeze.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,141 @@ def test_morphsqueeze_extrapolate(user_filesystem, squeeze_coeffs, wmsg_gen):
170170
)
171171
with pytest.warns(UserWarning, match=expected_wmsg):
172172
single_morph(parser, opts, pargs, stdout_flag=False)
173+
174+
175+
@pytest.mark.parametrize(
176+
"squeeze_coeffs, x_morph",
177+
[
178+
({"a0": 0.01, "a1": 0.01, "a2": -0.1}, np.linspace(0, 10, 101)),
179+
],
180+
)
181+
def test_non_strictly_increasing_squeeze(squeeze_coeffs, x_morph):
182+
x_target = x_morph
183+
y_target = np.sin(x_target)
184+
coeffs = [squeeze_coeffs[f"a{i}"] for i in range(len(squeeze_coeffs))]
185+
squeeze_polynomial = Polynomial(coeffs)
186+
x_squeezed = x_morph + squeeze_polynomial(x_morph)
187+
# non-strictly-increasing
188+
assert not np.all(np.sign(np.diff(x_squeezed)) > 0)
189+
y_morph = np.sin(x_squeezed)
190+
# all zero initial guess
191+
morph_results = morphpy.morph_arrays(
192+
np.array([x_morph, y_morph]).T,
193+
np.array([x_target, y_target]).T,
194+
squeeze=[0, 0, 0],
195+
apply=True,
196+
)
197+
_, y_morph_actual = morph_results[1].T # noqa: F841
198+
y_morph_expected = np.sin(x_morph) # noqa: F841
199+
# squeeze morph extrapolates.
200+
# Need to extract extrap_index from morph_results to examine
201+
# the convergence.
202+
# assert np.allclose(y_morph_actual, y_morph_expected, atol=1e-3)
203+
# Raise warning when called without --check-increase
204+
with pytest.warns() as w:
205+
morph_results = morphpy.morph_arrays(
206+
np.array([x_morph, y_morph]).T,
207+
np.array([x_target, y_target]).T,
208+
squeeze=[0.01, 0.01, -0.1],
209+
apply=True,
210+
)
211+
assert w[0].category is UserWarning
212+
actual_wmsg = " ".join([str(w[i].message) for i in range(len(w))])
213+
expected_wmsg = (
214+
"Warning: The squeeze morph has interpolated your morphed "
215+
"function from a non-monotonically increasing grid. "
216+
)
217+
assert expected_wmsg in actual_wmsg
218+
_, y_morph_actual = morph_results[1].T # noqa: F841
219+
y_morph_expected = np.sin(x_morph) # noqa: F841
220+
# squeeze morph extrapolates.
221+
# Need to extract extrap_index from morph_results to examine
222+
# the convergence.
223+
# assert np.allclose(y_morph_actual, y_morph_expected, atol=1e-3)
224+
# System exits when called with --check-increase
225+
with pytest.raises(SystemExit) as excinfo:
226+
morphpy.morph_arrays(
227+
np.array([x_morph, y_morph]).T,
228+
np.array([x_target, y_target]).T,
229+
squeeze=[0.01, 0.009, -0.1],
230+
check_increase=True,
231+
)
232+
actual_emsg = str(excinfo.value)
233+
expected_emsg = "2"
234+
assert expected_emsg == actual_emsg
235+
236+
237+
@pytest.mark.parametrize(
238+
"squeeze_coeffs, x_morph",
239+
[
240+
({"a0": -1, "a1": -1, "a2": 2}, np.linspace(-1, 1, 101)),
241+
(
242+
{"a0": -1, "a1": -1, "a2": 0, "a3": 0, "a4": 2},
243+
np.linspace(-1, 1, 101),
244+
),
245+
],
246+
)
247+
def test_sort_squeeze_bad(user_filesystem, squeeze_coeffs, x_morph):
248+
# call in .py without --check-increase
249+
x_target = x_morph
250+
y_target = np.sin(x_target)
251+
coeffs = [squeeze_coeffs[f"a{i}"] for i in range(len(squeeze_coeffs))]
252+
squeeze_polynomial = Polynomial(coeffs)
253+
x_squeezed = x_morph + squeeze_polynomial(x_morph)
254+
y_morph = np.sin(x_squeezed)
255+
morph = MorphSqueeze()
256+
morph.squeeze = squeeze_coeffs
257+
with pytest.warns() as w:
258+
morphpy.morph_arrays(
259+
np.array([x_morph, y_morph]).T,
260+
np.array([x_target, y_target]).T,
261+
squeeze=coeffs,
262+
apply=True,
263+
)
264+
assert len(w) == 1
265+
assert w[0].category is UserWarning
266+
actual_wmsg = str(w[0].message)
267+
expected_wmsg = (
268+
"Warning: The squeeze morph has interpolated your morphed "
269+
"function from a non-monotonically increasing grid. "
270+
)
271+
assert expected_wmsg in actual_wmsg
272+
273+
# call in CLI without --check-increase
274+
morph_file, target_file = create_morph_data_file(
275+
user_filesystem / "cwd_dir", x_morph, y_morph, x_target, y_target
276+
)
277+
parser = create_option_parser()
278+
(opts, pargs) = parser.parse_args(
279+
[
280+
"--squeeze",
281+
",".join(map(str, coeffs)),
282+
f"{morph_file.as_posix()}",
283+
f"{target_file.as_posix()}",
284+
"--apply",
285+
"-n",
286+
]
287+
)
288+
with pytest.warns(UserWarning) as w:
289+
single_morph(parser, opts, pargs, stdout_flag=False)
290+
assert len(w) == 1
291+
actual_wmsg = str(w[0].message)
292+
assert expected_wmsg in actual_wmsg
293+
294+
295+
def test_handle_duplicates():
296+
unq_x = np.linspace(0, 11, 10)
297+
iter = 10
298+
morph = MorphSqueeze()
299+
for i in range(iter):
300+
actual_x = np.random.choice(unq_x, size=20)
301+
actual_y = np.sin(actual_x)
302+
actual_handled_x, actual_handled_y = morph._handle_duplicates(
303+
actual_x, actual_y
304+
)
305+
expected_handled_x = np.unique(actual_x)
306+
expected_handled_y = np.array(
307+
[actual_y[actual_x == x].mean() for x in expected_handled_x]
308+
)
309+
assert np.allclose(actual_handled_x, expected_handled_x)
310+
assert np.allclose(actual_handled_y, expected_handled_y)

0 commit comments

Comments
 (0)