Skip to content

Add squeeze morph to CLI #216

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

Merged
merged 3 commits into from
Jun 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
404 changes: 404 additions & 0 deletions doc/source/squeeze.rst

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions news/squeeze_cli.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
**Added:**

* Squeeze morph now added to CLI.

**Changed:**

* Stretch disabled when squeeze is above polynomial order 0.
* Horizontal shift morph disabled when squeeze is enabled.

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
11 changes: 11 additions & 0 deletions src/diffpy/morph/morph_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ def single_morph_output(
)

morphs_out = "# Optimized morphing parameters:\n"
# Handle special inputs
if "squeeze" in morph_results:
sq_dict = morph_results.pop("squeeze")
rw_pos = list(morph_results.keys()).index("Rw")
morph_results_list = list(morph_results.items())
for idx, _ in enumerate(sq_dict):
morph_results_list.insert(
rw_pos + idx, (f"squeeze a{idx}", sq_dict[f"a{idx}"])
)
morph_results = dict(morph_results_list)
# Normal inputs
morphs_out += "\n".join(
f"# {key} = {morph_results[key]:.6f}" for key in morph_results.keys()
)
Expand Down
65 changes: 57 additions & 8 deletions src/diffpy/morph/morphapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,26 +148,50 @@ def custom_error(self, msg):
action="append",
dest="exclude",
metavar="MANIP",
help="""Exclude a manipulation from refinement by name. This can
appear multiple times.""",
help=(
"Exclude a manipulation from refinement by name. "
"This can appear multiple times."
),
)
group.add_option(
"--scale",
type="float",
metavar="SCALE",
help="Apply scale factor SCALE.",
help=(
"Apply scale factor SCALE. "
"This multiplies the function ordinate by SCALE."
),
)
group.add_option(
"--stretch",
type="float",
metavar="STRETCH",
help="Stretch PDF by a fraction STRETCH.",
help=(
"Stretch function grid by a fraction STRETCH. "
"This multiplies the function grid by 1+STRETCH."
),
)
group.add_option(
"--squeeze",
metavar="a0,a1,...,an",
help=(
"Squeeze function grid given a polynomial "
"a0+a1*x+a2*x^2+...a_n*x^n."
"n is dependent on the number of values in the "
"user-inputted comma-separated list. "
"When this option is enabled, --hshift is disabled. "
"When n>1, --stretch is disabled. "
"See online documentation for more information."
),
)
group.add_option(
"--smear",
type="float",
metavar="SMEAR",
help="Smear peaks with a Gaussian of width SMEAR.",
help=(
"Smear peaks with a Gaussian of width SMEAR. "
"This convolves the function with a Gaussian of width SMEAR."
),
)
group.add_option(
"--slope",
Expand Down Expand Up @@ -444,22 +468,37 @@ def single_morph(parser, opts, pargs, stdout_flag=True):
chain.append(morphs.MorphRGrid())
refpars = []

# Squeeze
squeeze_poly_deg = -1
if opts.squeeze is not None:
squeeze_coeffs = opts.squeeze.strip().split(",")
squeeze_dict_in = {}
for idx, coeff in enumerate(squeeze_coeffs):
squeeze_dict_in.update({f"a{idx}": float(coeff)})
squeeze_poly_deg = len(squeeze_coeffs) - 1
chain.append(morphs.MorphSqueeze())
config["squeeze"] = squeeze_dict_in
refpars.append("squeeze")
# Scale
if opts.scale is not None:
scale_in = opts.scale
chain.append(morphs.MorphScale())
config["scale"] = scale_in
refpars.append("scale")
# Stretch
if opts.stretch is not None:
# Only enable stretch if squeeze is lower than degree 1
if opts.stretch is not None and squeeze_poly_deg < 1:
stretch_in = opts.stretch
chain.append(morphs.MorphStretch())
config["stretch"] = stretch_in
refpars.append("stretch")
# Shift
if opts.hshift is not None or opts.vshift is not None:
# Only enable hshift is squeeze is not enabled
if (
opts.hshift is not None and squeeze_poly_deg < 0
) or opts.vshift is not None:
chain.append(morphs.MorphShift())
if opts.hshift is not None:
if opts.hshift is not None and squeeze_poly_deg < 0:
hshift_in = opts.hshift
config["hshift"] = hshift_in
refpars.append("hshift")
Expand Down Expand Up @@ -557,13 +596,23 @@ def single_morph(parser, opts, pargs, stdout_flag=True):
chain[0] = morphs.Morph()
chain(x_morph, y_morph, x_target, y_target)

# FOR FUTURE MAINTAINERS
# Any new morph should have their input morph parameters updated here
# Input morph parameters
morph_inputs = {
"scale": scale_in,
"stretch": stretch_in,
"smear": smear_in,
}
morph_inputs.update({"hshift": hshift_in, "vshift": vshift_in})
# More complex input morph parameters are only displayed conditionally
if opts.squeeze is not None:
squeeze_coeffs = opts.squeeze.strip().split(",")
squeeze_dict = {}
for idx, coeff in enumerate(squeeze_coeffs):
squeeze_dict.update({f"a{idx}": float(coeff)})
for idx, _ in enumerate(squeeze_dict):
morph_inputs.update({f"squeeze a{idx}": squeeze_dict[f"a{idx}"]})

# Output morph parameters
morph_results = dict(config.items())
Expand Down
75 changes: 65 additions & 10 deletions tests/test_morphio.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from pathlib import Path

import numpy as np
import pytest

from diffpy.morph.morphapp import (
Expand All @@ -27,6 +28,27 @@
tssf = testdata_dir.joinpath("testsequence_serialfile.json")


# Ignore PATH data when comparing files
def ignore_path(line):
# Lines containing FILE PATH data begin with '# from '
if "# from " in line:
return False
# Lines containing DIRECTORY PATH data begin with '# with '
if "# with " in line:
return False
return True


def isfloat(s):
"""True if s is convertible to float."""
try:
float(s)
return True
except ValueError:
pass
return False


class TestApp:
@pytest.fixture
def setup(self):
Expand All @@ -46,16 +68,6 @@ def setup(self):
return

def test_morph_outputs(self, setup, tmp_path):
# Ignore PATH data when comparing files
def ignore_path(line):
# Lines containing FILE PATH data begin with '# from '
if "# from " in line:
return False
# Lines containing DIRECTORY PATH data begin with '# with '
if "# with " in line:
return False
return True

morph_file = self.testfiles[0]
target_file = self.testfiles[-1]

Expand Down Expand Up @@ -137,3 +149,46 @@ def ignore_path(line):
generated = filter(ignore_path, gf)
target = filter(ignore_path, tf)
assert all(x == y for x, y in zip(generated, target))

def test_morph_squeeze_outputs(self, setup, tmp_path):
# The file squeeze_morph has a squeeze and stretch applied
morph_file = testdata_dir / "squeeze_morph.cgr"
target_file = testdata_dir / "squeeze_target.cgr"
sqr = tmp_path / "squeeze_morph_result.cgr"
sqr_name = sqr.resolve().as_posix()
# Note that stretch and hshift should not be considered
(opts, _) = self.parser.parse_args(
[
"--scale",
"2",
"--squeeze",
"0,-0.001,-0.0001,0.0001",
"--stretch",
"1",
"--hshift",
"1",
"-s",
sqr_name,
"-n",
"--verbose",
]
)
pargs = [morph_file, target_file]
single_morph(self.parser, opts, pargs, stdout_flag=False)

# Check squeeze morph generates the correct output
with open(sqr) as mf:
with open(target_file) as tf:
morphed = filter(ignore_path, mf)
target = filter(ignore_path, tf)
for m, t in zip(morphed, target):
m_row = m.split()
t_row = t.split()
assert len(m_row) == len(t_row)
for idx, _ in enumerate(m_row):
if isfloat(m_row[idx]) and isfloat(t_row[idx]):
assert np.isclose(
float(m_row[idx]), float(t_row[idx])
)
else:
assert m_row[idx] == t_row[idx]
105 changes: 105 additions & 0 deletions tests/testdata/squeeze_morph.cgr
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# PDF created by diffpy.morph
# from PATH

# Labels: [r] [gr]
0.000000000000000000e+00 0.000000000000000000e+00
1.000000000000000056e-01 2.016607210740701817e-01
2.000000000000000111e-01 4.012816425967311584e-01
3.000000000000000444e-01 5.968384836782407721e-01
4.000000000000000222e-01 7.863460390718853832e-01
5.000000000000000000e-01 9.678774221035963965e-01
6.000000000000000888e-01 1.139582805695501078e+00
7.000000000000000666e-01 1.299707497430252046e+00
8.000000000000000444e-01 1.446609189174991705e+00
9.000000000000000222e-01 1.578774227495413296e+00
1.000000000000000000e+00 1.694832757920656352e+00
1.100000000000000089e+00 1.793572604043012886e+00
1.200000000000000178e+00 1.873951751421987222e+00
1.300000000000000044e+00 1.935109316274401969e+00
1.400000000000000133e+00 1.976374889929940482e+00
1.500000000000000000e+00 1.997276161968231323e+00
1.600000000000000089e+00 1.997544737777092294e+00
1.700000000000000178e+00 1.977120079923743390e+00
1.800000000000000044e+00 1.936151517147448820e+00
1.900000000000000133e+00 1.874998279892714503e+00
2.000000000000000000e+00 1.794227537029879693e+00
2.100000000000000089e+00 1.694610424671280313e+00
2.200000000000000178e+00 1.577116074695904757e+00
2.300000000000000266e+00 1.442903667646756372e+00
2.400000000000000355e+00 1.293312551959283851e+00
2.500000000000000000e+00 1.129850488905959738e+00
2.600000000000000089e+00 9.541801000842509151e-01
2.700000000000000178e+00 7.681036116097271771e-01
2.800000000000000266e+00 5.735460062730138864e-01
2.900000000000000355e+00 3.725367116437122150e-01
3.000000000000000000e+00 1.671899683154590699e-01
3.100000000000000089e+00 -4.031596196150075834e-02
3.200000000000000178e+00 -2.477605737698468646e-01
3.300000000000000266e+00 -4.529036928636547832e-01
3.400000000000000355e+00 -6.535086474160737291e-01
3.500000000000000000e+00 -8.473658233427887598e-01
3.600000000000000089e+00 -1.032316381403495242e+00
3.700000000000000178e+00 -1.206275905404040172e+00
3.800000000000000266e+00 -1.367257744179736667e+00
3.900000000000000355e+00 -1.513395805283929141e+00
4.000000000000000000e+00 -1.642966555577477061e+00
4.100000000000000533e+00 -1.754409983348725222e+00
4.200000000000000178e+00 -1.846349278313989206e+00
4.299999999999999822e+00 -1.917608989968017763e+00
4.400000000000000355e+00 -1.967231431369953931e+00
4.500000000000000000e+00 -1.994491104644685819e+00
4.600000000000000533e+00 -1.998906936314116090e+00
4.700000000000000178e+00 -1.980252125089067672e+00
4.800000000000000711e+00 -1.938561421967258891e+00
4.900000000000000355e+00 -1.874135682387076951e+00
5.000000000000000000e+00 -1.787543552743151798e+00
5.100000000000000533e+00 -1.679620178709657630e+00
5.200000000000000178e+00 -1.551462850439542906e+00
5.300000000000000711e+00 -1.404423529676374072e+00
5.400000000000000355e+00 -1.240098235957646233e+00
5.500000000000000000e+00 -1.060313303193321621e+00
5.600000000000000533e+00 -8.671085537216786099e-01
5.700000000000000178e+00 -6.627174741864847451e-01
5.800000000000000711e+00 -4.495445159157776538e-01
5.900000000000000355e+00 -2.301396815438895571e-01
6.000000000000000000e+00 -7.170598996826864358e-03
6.100000000000000533e+00 2.166076767888532317e-01
6.200000000000000178e+00 4.383848554024750710e-01
6.300000000000000711e+00 6.553292792208245121e-01
6.400000000000000355e+00 8.646231160092439083e-01
6.500000000000000000e+00 1.063498489850685624e+00
6.600000000000000533e+00 1.249274133578965218e+00
6.700000000000000178e+00 1.419392118491213939e+00
6.800000000000000711e+00 1.571454193726408821e+00
6.900000000000000355e+00 1.703257248921869360e+00
7.000000000000000000e+00 1.812827400185563764e+00
7.100000000000000533e+00 1.898452191616431639e+00
7.200000000000000178e+00 1.958710403102522868e+00
7.300000000000000711e+00 1.992498960420514109e+00
7.400000000000000355e+00 1.999056456187634501e+00
7.500000000000000000e+00 1.977982810349711684e+00
7.600000000000000533e+00 1.929254626918742277e+00
7.700000000000000178e+00 1.853235839800372542e+00
7.800000000000000711e+00 1.750683284871877854e+00
7.900000000000000355e+00 1.622746887964007678e+00
8.000000000000000000e+00 1.470964218916802180e+00
8.099999999999999645e+00 1.297249230133007636e+00
8.200000000000001066e+00 1.103875073606781942e+00
8.300000000000000711e+00 8.934509726687409614e-01
8.400000000000000355e+00 6.688932129054430131e-01
8.500000000000000000e+00 4.333904099623434036e-01
8.599999999999999645e+00 1.903633091336342231e-01
8.700000000000001066e+00 -5.658052847403799435e-02
8.800000000000000711e+00 -3.036966972619735139e-01
8.900000000000000355e+00 -5.471580191460774234e-01
9.000000000000000000e+00 -7.831120606064566614e-01
9.099999999999999645e+00 -1.007741344138793016e+00
9.200000000000001066e+00 -1.217325414256249960e+00
9.300000000000000711e+00 -1.408303836788730168e+00
9.400000000000000355e+00 -1.577339138701817634e+00
9.500000000000000000e+00 -1.721378636094831327e+00
9.600000000000001421e+00 -1.837714052498993222e+00
9.700000000000001066e+00 -1.924037800077388338e+00
9.800000000000000711e+00 -1.978494784713575205e+00
9.900000000000000355e+00 -1.999728603987454667e+00
1.000000000000000000e+01 -1.986921036185803846e+00
Loading
Loading