Skip to content

Commit c4eba1e

Browse files
authored
Add option to save difference curve (#237)
* Add save diff * Add documentation * Name change diff->get-diff
1 parent 322b75e commit c4eba1e

File tree

7 files changed

+159
-25
lines changed

7 files changed

+159
-25
lines changed

docs/source/morphpy.rst

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Python Morphing Functions
3838

3939
* ``morph_info`` contains all morphs as keys (e.g. ``"scale"``, ``"stretch"``, ``"smear"``) with
4040
the optimized morphing parameters found by ``diffpy.morph`` as values. ``morph_info`` also contains
41-
the Rw and Pearson correlation coefficients found post-morphing. Try printing ``print(morph_info)``
41+
the Rw and Pearson correlation coefficients found post-morphing. Try printing ``print(morph_info)``
4242
and compare the values stored in this dictionary to those given by the CLI output!
4343
* ``morph_table`` is a two-column array of the morphed function interpolated onto the grid of the
4444
target function (e.g. in our example, it returns the contents of `darkSub_rh20_C_01.gr` after
@@ -74,6 +74,10 @@ General Parameters
7474

7575
save: str or path
7676
Save the morphed function to a the file passed to save. Use '-' for stdout.
77+
get_diff: bool
78+
Return the difference function (morphed function minus target function) instead of
79+
the morphed function (default). When save is enabled, the difference function
80+
is saved instead of the morphed function.
7781
verbose: bool
7882
Print additional header details to saved files. These include details about the morph
7983
inputs and outputs.
@@ -240,4 +244,4 @@ As you can see, the fitted scale and offset values match the ones used
240244
to generate the target (scale=20 & offset=0.8). This example shows how
241245
``MorphFuncy`` can be used to fit and apply custom transformations. Now
242246
it's your turn to experiment with other custom functions that may be useful
243-
for analyzing your data.
247+
for analyzing your data.

news/save_diff.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
**Added:**
2+
3+
* There is now an option to save the difference curve. This is computed on the common interval between the two curves.
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: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def single_morph_output(
4747
save_file
4848
Name of file to print to. If None (default) print to terminal.
4949
morph_file
50-
Name of the morphed PDF file. Required when printing to a
50+
Name of the morphed function file. Required when printing to a
5151
non-terminal file.
5252
param xy_out: list
5353
List of the form [x_morph_out, y_morph_out]. x_morph_out is a List of
@@ -144,7 +144,7 @@ def single_morph_output(
144144

145145

146146
def create_morphs_directory(save_directory):
147-
"""Create a directory for saving multiple morphed PDFs.
147+
"""Create a directory for saving multiple morphed functions.
148148
149149
Takes in a user-given path to a directory save_directory and create a
150150
subdirectory named Morphs. diffpy.morph will save all morphs into the
@@ -183,7 +183,7 @@ def get_multisave_names(target_list: list, save_names_file=None, mm=False):
183183
Parameters
184184
----------
185185
target_list: list
186-
Target (or Morph if mm enabled) PDFs used for each morph.
186+
Target (or Morph if mm enabled) functions used for each morph.
187187
save_names_file
188188
Name of file to import save names dictionary from (default None).
189189
mm: bool
@@ -192,8 +192,8 @@ def get_multisave_names(target_list: list, save_names_file=None, mm=False):
192192
Returns
193193
-------
194194
dict
195-
The names to save each morph as. Keys are the target PDF file names
196-
used to produce that morph.
195+
The names to save each morph as. Keys are the target function file
196+
names used to produce that morph.
197197
"""
198198

199199
# Dictionary storing save file names
@@ -252,20 +252,20 @@ def multiple_morph_output(
252252
morph_results: dict
253253
Resulting data after morphing.
254254
target_files: list
255-
PDF files that acted as targets to morphs.
255+
Files that acted as targets to morphs.
256256
save_directory
257257
Name of directory to save morphs in.
258258
field
259259
Name of field if data was sorted by a particular field.
260260
Otherwise, leave blank.
261261
field_list: list
262-
List of field values for each target PDF.
262+
List of field values for each target function.
263263
Generated by diffpy.morph.tools.field_sort().
264264
morph_file
265-
Name of the morphed PDF file.
265+
Name of the morphed function file.
266266
Required to give summary data after saving to a directory.
267267
target_directory
268-
Name of the directory containing the target PDF files.
268+
Name of the directory containing the target function files.
269269
Required to give summary data after saving to a directory.
270270
verbose: bool
271271
Print additional summary details when True (default False).

src/diffpy/morph/morphapp.py

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,27 @@ def custom_error(self, msg):
8181
metavar="NAME",
8282
dest="slocation",
8383
help=(
84-
"Save the manipulated PDF to a file named NAME. "
84+
"Save the manipulated function to a file named NAME. "
8585
"Use '-' for stdout.\n"
8686
"When --multiple-<targets/morphs> is enabled, "
87-
"save each manipulated PDF as a file in a directory named NAME;\n"
88-
"you can specify names for each saved PDF file using "
87+
"save each manipulated function as a file in a directory "
88+
"named NAME;\n"
89+
"you can specify names for each saved function file using "
8990
"--save-names-file."
9091
),
9192
)
93+
parser.add_option(
94+
"--diff",
95+
"--get-diff",
96+
dest="get_diff",
97+
action="store_true",
98+
help=(
99+
"Save the difference curve rather than the manipulated function.\n"
100+
"This is computed as manipulated function minus target function.\n"
101+
"The difference curve is computed on the interval shared by the "
102+
"grid of the objective and target function."
103+
),
104+
)
92105
parser.add_option(
93106
"-v",
94107
"--verbose",
@@ -99,12 +112,12 @@ def custom_error(self, msg):
99112
parser.add_option(
100113
"--rmin",
101114
type="float",
102-
help="Minimum r-value to use for PDF comparisons.",
115+
help="Minimum r-value (abscissa) to use for function comparisons.",
103116
)
104117
parser.add_option(
105118
"--rmax",
106119
type="float",
107-
help="Maximum r-value to use for PDF comparisons.",
120+
help="Maximum r-value (abscissa) to use for function comparisons.",
108121
)
109122
parser.add_option(
110123
"--tolerance",
@@ -419,9 +432,9 @@ def custom_error(self, msg):
419432
"using a serial file NAMESFILE. The format of NAMESFILE should be "
420433
"as follows: each target PDF is an entry in NAMESFILE. For each "
421434
"entry, there should be a key {__save_morph_as__} whose value "
422-
"specifies the name to save the manipulated PDF as. An example "
423-
".json serial file is included in the tutorial directory "
424-
"on the package GitHub repository."
435+
"specifies the name to save the manipulated function as."
436+
"An example .json serial file is included in the tutorial "
437+
"directory on the package GitHub repository."
425438
),
426439
)
427440
group.add_option(
@@ -492,10 +505,7 @@ def single_morph(
492505
smear_in = "None"
493506
hshift_in = "None"
494507
vshift_in = "None"
495-
config = {}
496-
config["rmin"] = opts.rmin
497-
config["rmax"] = opts.rmax
498-
config["rstep"] = None
508+
config = {"rmin": opts.rmin, "rmax": opts.rmax, "rstep": None}
499509
if (
500510
opts.rmin is not None
501511
and opts.rmax is not None
@@ -708,13 +718,29 @@ def single_morph(
708718
morph_results.update({"Pearson": pcc})
709719

710720
# Print summary to terminal and save morph to file if requested
721+
xy_save = [chain.x_morph_out, chain.y_morph_out]
722+
if opts.get_diff is not None:
723+
diff_chain = morphs.MorphChain(
724+
{"rmin": None, "rmax": None, "rstep": None}
725+
)
726+
diff_chain.append(morphs.MorphRGrid())
727+
diff_chain(
728+
chain.x_morph_out,
729+
chain.y_morph_out,
730+
chain.x_target_in,
731+
chain.y_target_in,
732+
)
733+
xy_save = [
734+
diff_chain.x_morph_out,
735+
diff_chain.y_morph_out - diff_chain.y_target_out,
736+
]
711737
try:
712738
io.single_morph_output(
713739
morph_inputs,
714740
morph_results,
715741
save_file=opts.slocation,
716742
morph_file=pargs[0],
717-
xy_out=[chain.x_morph_out, chain.y_morph_out],
743+
xy_out=xy_save,
718744
verbose=opts.verbose,
719745
stdout_flag=stdout_flag,
720746
)
@@ -753,7 +779,7 @@ def single_morph(
753779
# Return different things depending on whether it is python interfaced
754780
if python_wrap:
755781
morph_info = morph_results
756-
morph_table = numpy.array([chain.x_morph_out, chain.y_morph_out]).T
782+
morph_table = numpy.array(xy_save).T
757783
return morph_info, morph_table
758784
else:
759785
return morph_results

src/diffpy/morph/morphpy.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ def __get_morph_opts__(parser, scale, stretch, smear, plot, **kwargs):
4444
"addpearson",
4545
"apply",
4646
"reverse",
47+
"diff",
48+
"get-diff",
4749
]
4850
opts_to_ignore = ["multiple-morphs", "multiple-targets"]
4951
for opt in opts_storing_values:

tests/test_morphio.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
single_morph,
1212
)
1313
from diffpy.morph.morphpy import morph_arrays
14+
from diffpy.utils.parsers.loaddata import loadData
1415

1516
# Support Python 2
1617
try:
@@ -64,6 +65,29 @@ def are_files_same(file1, file2):
6465
assert f1_arr[idx] == f2_arr[idx]
6566

6667

68+
def are_diffs_right(file1, file2, diff_file):
69+
"""Assert that diff_file ordinate data is approximately file1
70+
ordinate data minus file2 ordinate data."""
71+
f1_data = loadData(file1)
72+
f2_data = loadData(file2)
73+
diff_data = loadData(diff_file)
74+
75+
rmin = max(min(f1_data[:, 0]), min(f1_data[:, 1]))
76+
rmax = min(max(f2_data[:, 0]), max(f2_data[:, 1]))
77+
rnumsteps = max(
78+
len(f1_data[:, 0][(rmin <= f1_data[:, 0]) & (f1_data[:, 0] <= rmax)]),
79+
len(f2_data[:, 0][(rmin <= f2_data[:, 0]) & (f2_data[:, 0] <= rmax)]),
80+
)
81+
82+
share_grid = np.linspace(rmin, rmax, rnumsteps)
83+
f1_interp = np.interp(share_grid, f1_data[:, 0], f1_data[:, 1])
84+
f2_interp = np.interp(share_grid, f2_data[:, 0], f2_data[:, 1])
85+
diff_interp = np.interp(share_grid, diff_data[:, 0], diff_data[:, 1])
86+
87+
for idx, diff in enumerate(diff_interp):
88+
assert np.isclose(f1_interp[idx] - f2_interp[idx], diff)
89+
90+
6791
class TestApp:
6892
@pytest.fixture
6993
def setup(self):
@@ -165,6 +189,59 @@ def test_morph_outputs(self, setup, tmp_path):
165189
expected = filter(ignore_path, tf)
166190
are_files_same(actual, expected)
167191

192+
# Similar format as test_morph_outputs
193+
def test_morph_diff_outputs(self, setup, tmp_path):
194+
morph_file = self.testfiles[0]
195+
target_file = self.testfiles[-1]
196+
197+
# Save multiple diff morphs
198+
tmp_diff = tmp_path.joinpath("diff")
199+
tmp_diff_name = tmp_diff.resolve().as_posix()
200+
201+
(opts, pargs) = self.parser.parse_args(
202+
[
203+
"--multiple-targets",
204+
"--sort-by",
205+
"temperature",
206+
"-s",
207+
tmp_diff_name,
208+
"-n",
209+
"--save-names-file",
210+
tssf,
211+
"--diff",
212+
]
213+
)
214+
pargs = [morph_file, testsequence_dir]
215+
multiple_targets(self.parser, opts, pargs, stdout_flag=False)
216+
217+
# Save a single diff morph
218+
diff_name = "single_diff_morph.cgr"
219+
diff_file = tmp_diff.joinpath(diff_name)
220+
df_name = diff_file.resolve().as_posix()
221+
(opts, pargs) = self.parser.parse_args(["-s", df_name, "-n", "--diff"])
222+
pargs = [morph_file, target_file]
223+
single_morph(self.parser, opts, pargs, stdout_flag=False)
224+
225+
# Check that the saved diff matches the morph minus target
226+
# Morphs are saved in testdata/testsequence/testsaving/succinct
227+
# Targets are stored in testdata/testsequence
228+
229+
# Single morph diff
230+
morphed_file = test_saving_succinct / diff_name.replace(
231+
"diff", "succinct"
232+
)
233+
are_diffs_right(morphed_file, target_file, diff_file)
234+
235+
# Multiple morphs diff
236+
diff_files = list((tmp_diff / "Morphs").iterdir())
237+
morphed_files = list((test_saving_succinct / "Morphs").iterdir())
238+
target_files = self.testfiles[1:]
239+
diff_files.sort()
240+
morphed_files.sort()
241+
target_files.sort()
242+
for idx, diff_file in enumerate(diff_files):
243+
are_diffs_right(morphed_files[idx], target_files[idx], diff_file)
244+
168245
def test_morphsqueeze_outputs(self, setup, tmp_path):
169246
# The file squeeze_morph has a squeeze and stretch applied
170247
morph_file = testdata_dir / "squeeze_morph.cgr"

tests/test_morphpy.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ def test_morph_opts(self, setup_morph):
7575
"addpearson": False,
7676
"apply": False,
7777
"reverse": False,
78+
"get_diff": False,
7879
"multiple_morphs": False,
7980
"multiple_targets": False,
8081
}
@@ -96,6 +97,7 @@ def test_morph_opts(self, setup_morph):
9697
"addpearson": True,
9798
"apply": True,
9899
"reverse": True,
100+
"get_diff": True,
99101
"multiple_morphs": True,
100102
"multiple_targets": True,
101103
}

0 commit comments

Comments
 (0)