Skip to content

Commit e44cfd5

Browse files
committed
Add morphpy and tests
1 parent a8f53ae commit e44cfd5

File tree

4 files changed

+270
-9
lines changed

4 files changed

+270
-9
lines changed

news/morphpy.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
**Added:**
2+
3+
* Python interfacing to call PDFmorph
4+
* Returns dictionary of morph metrics (dict) and the r, gr pair for plotting or further manipulation
5+
6+
**Changed:**
7+
8+
* <news item>
9+
10+
**Deprecated:**
11+
12+
* <news item>
13+
14+
**Removed:**
15+
16+
* <news item>
17+
18+
**Fixed:**
19+
20+
* <news item>
21+
22+
**Security:**
23+
24+
* <news item>

src/diffpy/morph/morphapp.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import sys
1919
from pathlib import Path
2020

21+
import numpy
22+
2123
import diffpy.morph.morph_helpers as helpers
2224
import diffpy.morph.morph_io as io
2325
import diffpy.morph.morphs as morphs
@@ -450,22 +452,31 @@ def custom_error(self, msg):
450452
return parser
451453

452454

453-
def single_morph(parser, opts, pargs, stdout_flag=True):
455+
def single_morph(parser, opts, pargs, stdout_flag=True, python_wrap=False):
454456
if len(pargs) < 2:
455457
parser.error("You must supply FILE1 and FILE2.")
456-
elif len(pargs) > 2:
458+
elif len(pargs) > 2 and not python_wrap:
457459
parser.error(
458460
"Too many arguments. Make sure you only supply FILE1 and FILE2."
459461
)
462+
elif not (len(pargs) == 2 or len(pargs) == 6) and python_wrap:
463+
parser.error("Python wrapper error.")
460464

461465
# Get the PDFs
462-
x_morph, y_morph = getPDFFromFile(pargs[0])
463-
x_target, y_target = getPDFFromFile(pargs[1])
466+
# If we get from python, we may wrap, which has input size 4
467+
if len(pargs) == 6 and python_wrap:
468+
x_morph = pargs[2]
469+
y_morph = pargs[3]
470+
x_target = pargs[4]
471+
y_target = pargs[5]
472+
else:
473+
x_morph, y_morph = getPDFFromFile(pargs[0])
474+
x_target, y_target = getPDFFromFile(pargs[1])
464475

465476
if y_morph is None:
466-
parser.error(f"No data table found in file: {pargs[0]}.")
477+
parser.error(f"No data table found in: {pargs[0]}.")
467478
if y_target is None:
468-
parser.error(f"No data table found in file: {pargs[1]}.")
479+
parser.error(f"No data table found in: {pargs[1]}.")
469480

470481
# Get tolerance
471482
tolerance = 1e-08
@@ -698,10 +709,16 @@ def single_morph(parser, opts, pargs, stdout_flag=True):
698709
l_width=l_width,
699710
)
700711

701-
return morph_results
712+
# Return different things depending on whether it is python interfaced
713+
if python_wrap:
714+
morph_info = morph_results
715+
morph_table = numpy.array([chain.x_morph_out, chain.y_morph_out]).T
716+
return morph_info, morph_table
717+
else:
718+
return morph_results
702719

703720

704-
def multiple_targets(parser, opts, pargs, stdout_flag=True):
721+
def multiple_targets(parser, opts, pargs, stdout_flag=True, python_wrap=False):
705722
# Custom error messages since usage is distinct when --multiple tag is
706723
# applied
707724
if len(pargs) < 2:
@@ -884,7 +901,7 @@ def multiple_targets(parser, opts, pargs, stdout_flag=True):
884901
return morph_results
885902

886903

887-
def multiple_morphs(parser, opts, pargs, stdout_flag=True):
904+
def multiple_morphs(parser, opts, pargs, stdout_flag=True, python_wrap=False):
888905
# Custom error messages since usage is distinct when --multiple tag is
889906
# applied
890907
if len(pargs) < 2:

src/diffpy/morph/morphpy.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env python
2+
3+
import numpy as np
4+
5+
from diffpy.morph.morphapp import (
6+
create_option_parser,
7+
multiple_morphs,
8+
multiple_targets,
9+
single_morph,
10+
)
11+
12+
13+
def get_args(parser, params, kwargs):
14+
inputs = []
15+
for key, value in params.items():
16+
if value is not None:
17+
inputs.append(f"--{key}")
18+
inputs.append(f"{value}")
19+
for key, value in kwargs.items():
20+
key = key.replace("_", "-")
21+
inputs.append(f"--{key}")
22+
inputs.append(f"{value}")
23+
(opts, pargs) = parser.parse_args(inputs)
24+
return opts, pargs
25+
26+
27+
def morph(morph_file, target_file, scale=None, stretch=None, smear=None, plot=False, **kwargs):
28+
"""Run diffpy.morph at Python level.
29+
Parameters
30+
----------
31+
morph_file: str
32+
Path-like object to the file to be morphed.
33+
target_file: str
34+
Path-like object to the target file.
35+
scale: float, optional
36+
Initial guess for the scaling parameter.
37+
Refinement is done only for parameter that are not None.
38+
stretch: float, optional
39+
Initial guess for the stretching parameter.
40+
smear: float, optional
41+
Initial guess for the smearing parameter.
42+
plot: bool
43+
Show a plot of the morphed and target functions as well as the difference curve (default: False).
44+
kwargs: dict
45+
See the diffpy.morph website for full list of options.
46+
Returns
47+
-------
48+
morph_info: dict
49+
Summary of morph parameters (e.g. scale, stretch, smear, rmin, rmax) and results (e.g. Pearson, Rw).
50+
morph_table: list
51+
Function after morph where morph_table[:,0] is the abscissa and morph_table[:,1] is the ordinate.
52+
"""
53+
54+
parser = create_option_parser()
55+
params = {"scale": scale, "stretch": stretch, "smear": smear, "noplot": True if not plot else None}
56+
opts, pargs = get_args(parser, params, kwargs)
57+
58+
pargs = [morph_file, target_file]
59+
60+
return single_morph(
61+
parser, opts, pargs, stdout_flag=False, python_wrap=True
62+
)
63+
64+
65+
def morphpy(morph_table, target_table, scale=None, stretch=None, smear=None, plot=False, **kwargs):
66+
"""Run diffpy.morph at Python level.
67+
Parameters
68+
----------
69+
morph_table: numpy.array
70+
Two-column array of (r, gr) for morphed function.
71+
target_table: numpy.array
72+
Two-column array of (r, gr) for target function.
73+
scale: float, optional
74+
Initial guess for the scaling parameter.
75+
Refinement is done only for parameter that are not None.
76+
stretch: float, optional
77+
Initial guess for the stretching parameter.
78+
smear: float, optional
79+
Initial guess for the smearing parameter.
80+
plot: bool
81+
Show a plot of the morphed and target functions as well as the difference curve (default: False).
82+
kwargs: dict
83+
See the diffpy.morph website for full list of options.
84+
Returns
85+
-------
86+
morph_info: dict
87+
Summary of morph parameters (e.g. scale, stretch, smear, rmin, rmax) and results (e.g. Pearson, Rw).
88+
morph_table: list
89+
Function after morph where morph_table[:,0] is the abscissa and morph_table[:,1] is the ordinate.
90+
"""
91+
92+
parser = create_option_parser()
93+
params = {"scale": scale, "stretch": stretch, "smear": smear, "noplot": True if not plot else None}
94+
opts, pargs = get_args(parser, params, kwargs)
95+
96+
morph_table = np.array(morph_table)
97+
target_table = np.array(target_table)
98+
99+
x_morph = morph_table[:, 0]
100+
y_morph = morph_table[:, 1]
101+
x_target = target_table[:, 0]
102+
y_target = target_table[:, 1]
103+
104+
pargs = ["Morph", "Target", x_morph, y_morph, x_target, y_target]
105+
106+
return single_morph(
107+
parser, opts, pargs, stdout_flag=False, python_wrap=True
108+
)

tests/test_morphpy.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#!/usr/bin/env python
2+
3+
from pathlib import Path
4+
5+
import pytest
6+
import numpy
7+
8+
from diffpy.morph.morphapp import (
9+
create_option_parser,
10+
single_morph,
11+
)
12+
from diffpy.morph.tools import getRw
13+
from diffpy.morph.morphpy import (
14+
morph,
15+
morphpy,
16+
)
17+
18+
thisfile = locals().get("__file__", "file.py")
19+
tests_dir = Path(thisfile).parent.resolve()
20+
testdata_dir = tests_dir.joinpath("testdata")
21+
testsequence_dir = testdata_dir.joinpath("testsequence")
22+
23+
nickel_PDF = testdata_dir.joinpath("nickel_ss0.01.cgr")
24+
serial_JSON = testdata_dir.joinpath("testsequence_serialfile.json")
25+
26+
testsaving_dir = testsequence_dir.joinpath("testsaving")
27+
test_saving_succinct = testsaving_dir.joinpath("succinct")
28+
test_saving_verbose = testsaving_dir.joinpath("verbose")
29+
tssf = testdata_dir.joinpath("testsequence_serialfile.json")
30+
31+
32+
class TestApp:
33+
@pytest.fixture
34+
def setup_morph(self):
35+
self.parser = create_option_parser()
36+
filenames = [
37+
"g_174K.gr",
38+
"f_180K.gr",
39+
"e_186K.gr",
40+
"d_192K.gr",
41+
"c_198K.gr",
42+
"b_204K.gr",
43+
"a_210K.gr",
44+
]
45+
self.testfiles = []
46+
self.morphapp_results = {}
47+
48+
# Parse arguments sorting by field
49+
(opts, pargs) = self.parser.parse_args(
50+
[
51+
"--scale",
52+
"1",
53+
"--stretch",
54+
"0",
55+
"-n",
56+
"--sort-by",
57+
"temperature",
58+
]
59+
)
60+
for filename in filenames:
61+
self.testfiles.append(testsequence_dir.joinpath(filename))
62+
63+
# Run multiple single morphs
64+
morph_file = self.testfiles[0]
65+
for target_file in self.testfiles[1:]:
66+
pargs = [morph_file, target_file]
67+
# store in same format of dictionary as multiple_targets
68+
self.morphapp_results.update(
69+
{
70+
target_file.name: single_morph(
71+
self.parser, opts, pargs, stdout_flag=False
72+
)
73+
}
74+
)
75+
return
76+
77+
def test_morph(self, setup_morph):
78+
morph_results = {}
79+
morph_file = self.testfiles[0]
80+
for target_file in self.testfiles[1:]:
81+
mr, grm = morph(morph_file, target_file, scale=1, stretch=0, sort_by="temperature")
82+
_, grt = morph(target_file, target_file)
83+
morph_results.update({target_file.name: mr})
84+
class Chain:
85+
xyallout = grm[:, 0], grm[:, 1], grt[:, 0], grt[:, 1]
86+
chain = Chain()
87+
rw = getRw(chain)
88+
del chain
89+
assert numpy.allclose([rw], [self.morphapp_results[target_file.name]["Rw"]])
90+
assert morph_results == self.morphapp_results
91+
92+
def test_morphpy(self, setup_morph):
93+
morph_results = {}
94+
morph_file = self.testfiles[0]
95+
for target_file in self.testfiles[1:]:
96+
_, grm0 = morph(morph_file, morph_file)
97+
_, grt = morph(target_file, target_file)
98+
mr, grm = morphpy(grm0, grt, scale=1, stretch=0, sort_by="temperature")
99+
morph_results.update({target_file.name: mr})
100+
101+
class Chain:
102+
xyallout = grm[:, 0], grm[:, 1], grt[:, 0], grt[:, 1]
103+
104+
chain = Chain()
105+
rw = getRw(chain)
106+
del chain
107+
assert numpy.allclose([rw], [self.morphapp_results[target_file.name]["Rw"]])
108+
assert morph_results == self.morphapp_results
109+
110+
111+
if __name__ == "__main__":
112+
TestApp()

0 commit comments

Comments
 (0)