Skip to content

Commit 6474d1a

Browse files
committed
Add funcy support to morphpy
1 parent 05dc6de commit 6474d1a

File tree

7 files changed

+304
-36
lines changed

7 files changed

+304
-36
lines changed

src/diffpy/morph/morph_io.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,20 +66,34 @@ def single_morph_output(
6666
+ "\n"
6767
)
6868

69+
mr_copy = morph_results.copy()
6970
morphs_out = "# Optimized morphing parameters:\n"
70-
# Handle special inputs
71-
if "squeeze" in morph_results:
72-
sq_dict = morph_results.pop("squeeze")
73-
rw_pos = list(morph_results.keys()).index("Rw")
74-
morph_results_list = list(morph_results.items())
71+
# Handle special inputs (numerical)
72+
if "squeeze" in mr_copy:
73+
sq_dict = mr_copy.pop("squeeze")
74+
rw_pos = list(mr_copy.keys()).index("Rw")
75+
morph_results_list = list(mr_copy.items())
7576
for idx, _ in enumerate(sq_dict):
7677
morph_results_list.insert(
7778
rw_pos + idx, (f"squeeze a{idx}", sq_dict[f"a{idx}"])
7879
)
79-
morph_results = dict(morph_results_list)
80+
mr_copy = dict(morph_results_list)
81+
funcy_function = None
82+
if "function" in mr_copy:
83+
funcy_function = mr_copy.pop("function")
84+
print(funcy_function)
85+
if "funcy" in mr_copy:
86+
fy_dict = mr_copy.pop("funcy")
87+
rw_pos = list(mr_copy.keys()).index("Rw")
88+
morph_results_list = list(mr_copy.items())
89+
for idx, key in enumerate(fy_dict):
90+
morph_results_list.insert(
91+
rw_pos + idx, (f"funcy {key}", fy_dict[key])
92+
)
93+
mr_copy = dict(morph_results_list)
8094
# Normal inputs
8195
morphs_out += "\n".join(
82-
f"# {key} = {morph_results[key]:.6f}" for key in morph_results.keys()
96+
f"# {key} = {mr_copy[key]:.6f}" for key in mr_copy.keys()
8397
)
8498

8599
# Printing to terminal
@@ -88,7 +102,10 @@ def single_morph_output(
88102

89103
# Saving to file
90104
if save_file is not None:
91-
path_name = str(Path(morph_file).resolve())
105+
if not Path(morph_file).exists():
106+
path_name = "NO FILE PATH PROVIDED"
107+
else:
108+
path_name = str(Path(morph_file).resolve())
92109
header = "# PDF created by diffpy.morph\n"
93110
header += f"# from {path_name}"
94111

src/diffpy/morph/morphapp.py

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,9 @@ def custom_error(self, msg):
452452
return parser
453453

454454

455-
def single_morph(parser, opts, pargs, stdout_flag=True, python_wrap=False):
455+
def single_morph(
456+
parser, opts, pargs, stdout_flag=True, python_wrap=False, pymorphs=None
457+
):
456458
if len(pargs) < 2:
457459
parser.error("You must supply FILE1 and FILE2.")
458460
elif len(pargs) > 2 and not python_wrap:
@@ -507,9 +509,27 @@ def single_morph(parser, opts, pargs, stdout_flag=True, python_wrap=False):
507509
chain.append(morphs.MorphRGrid())
508510
refpars = []
509511

512+
# Python-Specific Morphs
513+
if pymorphs is not None:
514+
# funcy value is a tuple (function,{param_dict})
515+
if "funcy" in pymorphs:
516+
mfy_function = pymorphs["funcy"][0]
517+
mfy_params = pymorphs["funcy"][1]
518+
chain.append(morphs.MorphFuncy())
519+
config["function"] = mfy_function
520+
config["funcy"] = mfy_params
521+
refpars.append("funcy")
522+
510523
# Squeeze
511524
squeeze_poly_deg = -1
512525
if opts.squeeze is not None:
526+
# Handles both list and csv input
527+
if (
528+
len(opts.squeeze) > 1
529+
and opts.squeeze[0] == "["
530+
and opts.squeeze[-1] == "]"
531+
):
532+
opts.squeeze = opts.squeeze[1:-1]
513533
squeeze_coeffs = opts.squeeze.strip().split(",")
514534
squeeze_dict_in = {}
515535
for idx, coeff in enumerate(squeeze_coeffs):
@@ -531,20 +551,6 @@ def single_morph(parser, opts, pargs, stdout_flag=True, python_wrap=False):
531551
chain.append(morphs.MorphStretch())
532552
config["stretch"] = stretch_in
533553
refpars.append("stretch")
534-
# Shift
535-
# Only enable hshift is squeeze is not enabled
536-
if (
537-
opts.hshift is not None and squeeze_poly_deg < 0
538-
) or opts.vshift is not None:
539-
chain.append(morphs.MorphShift())
540-
if opts.hshift is not None and squeeze_poly_deg < 0:
541-
hshift_in = opts.hshift
542-
config["hshift"] = hshift_in
543-
refpars.append("hshift")
544-
if opts.vshift is not None:
545-
vshift_in = opts.vshift
546-
config["vshift"] = vshift_in
547-
refpars.append("vshift")
548554
# Smear
549555
if opts.smear_pdf is not None:
550556
smear_in = opts.smear_pdf
@@ -563,6 +569,20 @@ def single_morph(parser, opts, pargs, stdout_flag=True, python_wrap=False):
563569
chain.append(morphs.MorphSmear())
564570
refpars.append("smear")
565571
config["smear"] = smear_in
572+
# Shift
573+
# Only enable hshift is squeeze is not enabled
574+
if (
575+
opts.hshift is not None and squeeze_poly_deg < 0
576+
) or opts.vshift is not None:
577+
chain.append(morphs.MorphShift())
578+
if opts.hshift is not None and squeeze_poly_deg < 0:
579+
hshift_in = opts.hshift
580+
config["hshift"] = hshift_in
581+
refpars.append("hshift")
582+
if opts.vshift is not None:
583+
vshift_in = opts.vshift
584+
config["vshift"] = vshift_in
585+
refpars.append("vshift")
566586
# Size
567587
radii = [opts.radius, opts.pradius]
568588
nrad = 2 - radii.count(None)
@@ -659,6 +679,12 @@ def single_morph(parser, opts, pargs, stdout_flag=True, python_wrap=False):
659679
squeeze_dict.update({f"a{idx}": float(coeff)})
660680
for idx, _ in enumerate(squeeze_dict):
661681
morph_inputs.update({f"squeeze a{idx}": squeeze_dict[f"a{idx}"]})
682+
if pymorphs is not None:
683+
if "funcy" in pymorphs:
684+
for funcy_param in pymorphs["funcy"][1].keys():
685+
morph_inputs.update(
686+
{f"funcy {funcy_param}": pymorphs["funcy"][1][funcy_param]}
687+
)
662688

663689
# Output morph parameters
664690
morph_results = dict(config.items())

src/diffpy/morph/morphpy.py

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def get_args(parser, params, kwargs):
1919
return opts, pargs
2020

2121

22+
# Take in file names as input.
2223
def morph(
2324
morph_file,
2425
target_file,
@@ -29,11 +30,12 @@ def morph(
2930
**kwargs,
3031
):
3132
"""Run diffpy.morph at Python level.
33+
3234
Parameters
3335
----------
34-
morph_file: str
36+
morph_file: str or numpy.array
3537
Path-like object to the file to be morphed.
36-
target_file: str
38+
target_file: str or numpy.array
3739
Path-like object to the target file.
3840
scale: float, optional
3941
Initial guess for the scaling parameter.
@@ -57,22 +59,39 @@ def morph(
5759
morph_table[:,1] is the ordinate.
5860
"""
5961

62+
# Check for Python-specific morphs
63+
python_morphs = ["funcy"]
64+
pymorphs = {}
65+
for pmorph in python_morphs:
66+
if pmorph in kwargs:
67+
pmorph_value = kwargs.pop(pmorph)
68+
pymorphs.update({pmorph: pmorph_value})
69+
70+
# Wrap the CLI
6071
parser = create_option_parser()
6172
params = {
6273
"scale": scale,
6374
"stretch": stretch,
6475
"smear": smear,
6576
"noplot": True if not plot else None,
6677
}
67-
opts, pargs = get_args(parser, params, kwargs)
78+
opts, _ = get_args(parser, params, kwargs)
6879

6980
pargs = [morph_file, target_file]
7081

82+
if not len(pymorphs) > 0:
83+
pymorphs = None
7184
return single_morph(
72-
parser, opts, pargs, stdout_flag=False, python_wrap=True
85+
parser,
86+
opts,
87+
pargs,
88+
stdout_flag=False,
89+
python_wrap=True,
90+
pymorphs=pymorphs,
7391
)
7492

7593

94+
# Take in array-like objects as input.
7695
def morphpy(
7796
morph_table,
7897
target_table,
@@ -83,6 +102,7 @@ def morphpy(
83102
**kwargs,
84103
):
85104
"""Run diffpy.morph at Python level.
105+
86106
Parameters
87107
----------
88108
morph_table: numpy.array
@@ -110,15 +130,23 @@ def morphpy(
110130
Function after morph where morph_table[:,0] is the abscissa and
111131
morph_table[:,1] is the ordinate.
112132
"""
113-
133+
# Check for Python-specific morphs
134+
python_morphs = ["funcy"]
135+
pymorphs = {}
136+
for pmorph in python_morphs:
137+
if pmorph in kwargs:
138+
pmorph_value = kwargs.pop(pmorph)
139+
pymorphs.update({pmorph: pmorph_value})
140+
141+
# Wrap the CLI
114142
parser = create_option_parser()
115143
params = {
116144
"scale": scale,
117145
"stretch": stretch,
118146
"smear": smear,
119147
"noplot": True if not plot else None,
120148
}
121-
opts, pargs = get_args(parser, params, kwargs)
149+
opts, _ = get_args(parser, params, kwargs)
122150

123151
morph_table = np.array(morph_table)
124152
target_table = np.array(target_table)
@@ -130,6 +158,13 @@ def morphpy(
130158

131159
pargs = ["Morph", "Target", x_morph, y_morph, x_target, y_target]
132160

161+
if not len(pymorphs) > 0:
162+
pymorphs = None
133163
return single_morph(
134-
parser, opts, pargs, stdout_flag=False, python_wrap=True
164+
parser,
165+
opts,
166+
pargs,
167+
stdout_flag=False,
168+
python_wrap=True,
169+
pymorphs=pymorphs,
135170
)

src/diffpy/morph/morphs/morphfuncy.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class MorphFuncy(Morph):
1111
yinlabel = LABEL_GR
1212
xoutlabel = LABEL_RA
1313
youtlabel = LABEL_GR
14-
parnames = ["funcy"]
14+
parnames = ["function", "funcy"]
1515

1616
def morph(self, x_morph, y_morph, x_target, y_target):
1717
"""General morph function that applies a user-supplied function
@@ -63,7 +63,6 @@ def morph(self, x_morph, y_morph, x_target, y_target):
6363
>>> parameters_out = morph.funcy
6464
"""
6565
Morph.morph(self, x_morph, y_morph, x_target, y_target)
66-
6766
self.y_morph_out = self.function(
6867
self.x_morph_in, self.y_morph_in, **self.funcy
6968
)

tests/test_morphio.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
single_morph,
1212
)
1313

14+
# from diffpy.morph.morphpy import morphpy
15+
1416
# Support Python 2
1517
try:
1618
from future_builtins import filter, zip
@@ -150,7 +152,7 @@ def test_morph_outputs(self, setup, tmp_path):
150152
target = filter(ignore_path, tf)
151153
assert all(x == y for x, y in zip(generated, target))
152154

153-
def test_morph_squeeze_outputs(self, setup, tmp_path):
155+
def test_morphsqueeze_outputs(self, setup, tmp_path):
154156
# The file squeeze_morph has a squeeze and stretch applied
155157
morph_file = testdata_dir / "squeeze_morph.cgr"
156158
target_file = testdata_dir / "squeeze_target.cgr"
@@ -192,3 +194,7 @@ def test_morph_squeeze_outputs(self, setup, tmp_path):
192194
)
193195
else:
194196
assert m_row[idx] == t_row[idx]
197+
198+
def test_morphfuncy_outputs(self, tmp_path):
199+
def quadratic(x, y, a0, a1, a2):
200+
return a0 + a1 * x + a2 * y**2

tests/test_morphpy.py

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from pathlib import Path
44

5-
import numpy
5+
import numpy as np
66
import pytest
77

88
from diffpy.morph.morphapp import create_option_parser, single_morph
@@ -88,7 +88,7 @@ class Chain:
8888
chain = Chain()
8989
rw = getRw(chain)
9090
del chain
91-
assert numpy.allclose(
91+
assert np.allclose(
9292
[rw], [self.morphapp_results[target_file.name]["Rw"]]
9393
)
9494
assert morph_results == self.morphapp_results
@@ -110,11 +110,61 @@ class Chain:
110110
chain = Chain()
111111
rw = getRw(chain)
112112
del chain
113-
assert numpy.allclose(
113+
assert np.allclose(
114114
[rw], [self.morphapp_results[target_file.name]["Rw"]]
115115
)
116116
assert morph_results == self.morphapp_results
117117

118+
def test_morphfuncy(self, setup_morph):
119+
def gaussian(x, mu, sigma):
120+
return np.exp(-((x - mu) ** 2) / (2 * sigma**2)) / (
121+
sigma * np.sqrt(2 * np.pi)
122+
)
123+
124+
def gaussian_like_function(x, y, mu):
125+
return gaussian((x + y) / 2, mu, 3)
126+
127+
morph_r = np.linspace(0, 100, 1001)
128+
morph_gr = np.linspace(0, 100, 1001)
129+
130+
target_r = np.linspace(0, 100, 1001)
131+
target_gr = 0.5 * gaussian(target_r, 50, 5) + 0.05
132+
133+
morph_info, _ = morphpy(
134+
np.array([morph_r, morph_gr]).T,
135+
np.array([target_r, target_gr]).T,
136+
scale=1,
137+
vshift=0.01,
138+
smear=3.75,
139+
funcy=(gaussian_like_function, {"mu": 47.5}),
140+
tolerance=1e-12,
141+
)
142+
143+
assert pytest.approx(morph_info["scale"]) == 0.5
144+
assert pytest.approx(morph_info["vshift"]) == 0.05
145+
assert pytest.approx(abs(morph_info["smear"])) == 4.0
146+
assert pytest.approx(morph_info["funcy"]["mu"]) == 50.0
147+
148+
def test_morphpy_outputs(self, tmp_path):
149+
r = np.linspace(0, 1, 11)
150+
gr = np.linspace(0, 1, 11)
151+
152+
def linear(x, y, s):
153+
return s * (x + y)
154+
155+
morph_info, _ = morphpy(
156+
np.array([r, gr]).T,
157+
np.array([r, gr]).T,
158+
squeeze=[1, 2, 3, 4, 5],
159+
funcy=(linear, {"s": 2.5}),
160+
apply=True,
161+
)
162+
163+
print(morph_info)
164+
for i in range(5):
165+
assert pytest.approx(morph_info["squeeze"][f"a{i}"]) == i + 1
166+
assert pytest.approx(morph_info["funcy"]["s"]) == 2.5
167+
118168

119169
if __name__ == "__main__":
120170
TestMorphpy()

0 commit comments

Comments
 (0)