diff --git a/docs/source/morphpy.rst b/docs/source/morphpy.rst index d3d3af57..34050fdf 100644 --- a/docs/source/morphpy.rst +++ b/docs/source/morphpy.rst @@ -38,7 +38,7 @@ Python Morphing Functions * ``morph_info`` contains all morphs as keys (e.g. ``"scale"``, ``"stretch"``, ``"smear"``) with the optimized morphing parameters found by ``diffpy.morph`` as values. ``morph_info`` also contains - the Rw and Pearson correlation coefficients found post-morphing. Try printing ``print(morph_info)`` + the Rw and Pearson correlation coefficients found post-morphing. Try printing ``print(morph_info)`` and compare the values stored in this dictionary to those given by the CLI output! * ``morph_table`` is a two-column array of the morphed function interpolated onto the grid of the target function (e.g. in our example, it returns the contents of `darkSub_rh20_C_01.gr` after @@ -74,6 +74,10 @@ General Parameters save: str or path Save the morphed function to a the file passed to save. Use '-' for stdout. +get_diff: bool + Return the difference function (morphed function minus target function) instead of + the morphed function (default). When save is enabled, the difference function + is saved instead of the morphed function. verbose: bool Print additional header details to saved files. These include details about the morph inputs and outputs. @@ -240,4 +244,4 @@ As you can see, the fitted scale and offset values match the ones used to generate the target (scale=20 & offset=0.8). This example shows how ``MorphFuncy`` can be used to fit and apply custom transformations. Now it's your turn to experiment with other custom functions that may be useful -for analyzing your data. +for analyzing your data. diff --git a/news/save_diff.rst b/news/save_diff.rst new file mode 100644 index 00000000..5ddb57fd --- /dev/null +++ b/news/save_diff.rst @@ -0,0 +1,23 @@ +**Added:** + +* There is now an option to save the difference curve. This is computed on the common interval between the two curves. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/morph/morph_io.py b/src/diffpy/morph/morph_io.py index 4433fc79..0c35a3f3 100644 --- a/src/diffpy/morph/morph_io.py +++ b/src/diffpy/morph/morph_io.py @@ -47,7 +47,7 @@ def single_morph_output( save_file Name of file to print to. If None (default) print to terminal. morph_file - Name of the morphed PDF file. Required when printing to a + Name of the morphed function file. Required when printing to a non-terminal file. param xy_out: list 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( def create_morphs_directory(save_directory): - """Create a directory for saving multiple morphed PDFs. + """Create a directory for saving multiple morphed functions. Takes in a user-given path to a directory save_directory and create a 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): Parameters ---------- target_list: list - Target (or Morph if mm enabled) PDFs used for each morph. + Target (or Morph if mm enabled) functions used for each morph. save_names_file Name of file to import save names dictionary from (default None). mm: bool @@ -192,8 +192,8 @@ def get_multisave_names(target_list: list, save_names_file=None, mm=False): Returns ------- dict - The names to save each morph as. Keys are the target PDF file names - used to produce that morph. + The names to save each morph as. Keys are the target function file + names used to produce that morph. """ # Dictionary storing save file names @@ -252,20 +252,20 @@ def multiple_morph_output( morph_results: dict Resulting data after morphing. target_files: list - PDF files that acted as targets to morphs. + Files that acted as targets to morphs. save_directory Name of directory to save morphs in. field Name of field if data was sorted by a particular field. Otherwise, leave blank. field_list: list - List of field values for each target PDF. + List of field values for each target function. Generated by diffpy.morph.tools.field_sort(). morph_file - Name of the morphed PDF file. + Name of the morphed function file. Required to give summary data after saving to a directory. target_directory - Name of the directory containing the target PDF files. + Name of the directory containing the target function files. Required to give summary data after saving to a directory. verbose: bool Print additional summary details when True (default False). diff --git a/src/diffpy/morph/morphapp.py b/src/diffpy/morph/morphapp.py index 43f71ce1..3647614a 100755 --- a/src/diffpy/morph/morphapp.py +++ b/src/diffpy/morph/morphapp.py @@ -81,14 +81,27 @@ def custom_error(self, msg): metavar="NAME", dest="slocation", help=( - "Save the manipulated PDF to a file named NAME. " + "Save the manipulated function to a file named NAME. " "Use '-' for stdout.\n" "When --multiple- is enabled, " - "save each manipulated PDF as a file in a directory named NAME;\n" - "you can specify names for each saved PDF file using " + "save each manipulated function as a file in a directory " + "named NAME;\n" + "you can specify names for each saved function file using " "--save-names-file." ), ) + parser.add_option( + "--diff", + "--get-diff", + dest="get_diff", + action="store_true", + help=( + "Save the difference curve rather than the manipulated function.\n" + "This is computed as manipulated function minus target function.\n" + "The difference curve is computed on the interval shared by the " + "grid of the objective and target function." + ), + ) parser.add_option( "-v", "--verbose", @@ -99,12 +112,12 @@ def custom_error(self, msg): parser.add_option( "--rmin", type="float", - help="Minimum r-value to use for PDF comparisons.", + help="Minimum r-value (abscissa) to use for function comparisons.", ) parser.add_option( "--rmax", type="float", - help="Maximum r-value to use for PDF comparisons.", + help="Maximum r-value (abscissa) to use for function comparisons.", ) parser.add_option( "--tolerance", @@ -419,9 +432,9 @@ def custom_error(self, msg): "using a serial file NAMESFILE. The format of NAMESFILE should be " "as follows: each target PDF is an entry in NAMESFILE. For each " "entry, there should be a key {__save_morph_as__} whose value " - "specifies the name to save the manipulated PDF as. An example " - ".json serial file is included in the tutorial directory " - "on the package GitHub repository." + "specifies the name to save the manipulated function as." + "An example .json serial file is included in the tutorial " + "directory on the package GitHub repository." ), ) group.add_option( @@ -492,10 +505,7 @@ def single_morph( smear_in = "None" hshift_in = "None" vshift_in = "None" - config = {} - config["rmin"] = opts.rmin - config["rmax"] = opts.rmax - config["rstep"] = None + config = {"rmin": opts.rmin, "rmax": opts.rmax, "rstep": None} if ( opts.rmin is not None and opts.rmax is not None @@ -708,13 +718,29 @@ def single_morph( morph_results.update({"Pearson": pcc}) # Print summary to terminal and save morph to file if requested + xy_save = [chain.x_morph_out, chain.y_morph_out] + if opts.get_diff is not None: + diff_chain = morphs.MorphChain( + {"rmin": None, "rmax": None, "rstep": None} + ) + diff_chain.append(morphs.MorphRGrid()) + diff_chain( + chain.x_morph_out, + chain.y_morph_out, + chain.x_target_in, + chain.y_target_in, + ) + xy_save = [ + diff_chain.x_morph_out, + diff_chain.y_morph_out - diff_chain.y_target_out, + ] try: io.single_morph_output( morph_inputs, morph_results, save_file=opts.slocation, morph_file=pargs[0], - xy_out=[chain.x_morph_out, chain.y_morph_out], + xy_out=xy_save, verbose=opts.verbose, stdout_flag=stdout_flag, ) @@ -753,7 +779,7 @@ def single_morph( # Return different things depending on whether it is python interfaced if python_wrap: morph_info = morph_results - morph_table = numpy.array([chain.x_morph_out, chain.y_morph_out]).T + morph_table = numpy.array(xy_save).T return morph_info, morph_table else: return morph_results diff --git a/src/diffpy/morph/morphpy.py b/src/diffpy/morph/morphpy.py index 7240f9f8..974573fa 100644 --- a/src/diffpy/morph/morphpy.py +++ b/src/diffpy/morph/morphpy.py @@ -44,6 +44,8 @@ def __get_morph_opts__(parser, scale, stretch, smear, plot, **kwargs): "addpearson", "apply", "reverse", + "diff", + "get-diff", ] opts_to_ignore = ["multiple-morphs", "multiple-targets"] for opt in opts_storing_values: diff --git a/tests/test_morphio.py b/tests/test_morphio.py index 08699dcb..c86b66c6 100644 --- a/tests/test_morphio.py +++ b/tests/test_morphio.py @@ -11,6 +11,7 @@ single_morph, ) from diffpy.morph.morphpy import morph_arrays +from diffpy.utils.parsers.loaddata import loadData # Support Python 2 try: @@ -64,6 +65,29 @@ def are_files_same(file1, file2): assert f1_arr[idx] == f2_arr[idx] +def are_diffs_right(file1, file2, diff_file): + """Assert that diff_file ordinate data is approximately file1 + ordinate data minus file2 ordinate data.""" + f1_data = loadData(file1) + f2_data = loadData(file2) + diff_data = loadData(diff_file) + + rmin = max(min(f1_data[:, 0]), min(f1_data[:, 1])) + rmax = min(max(f2_data[:, 0]), max(f2_data[:, 1])) + rnumsteps = max( + len(f1_data[:, 0][(rmin <= f1_data[:, 0]) & (f1_data[:, 0] <= rmax)]), + len(f2_data[:, 0][(rmin <= f2_data[:, 0]) & (f2_data[:, 0] <= rmax)]), + ) + + share_grid = np.linspace(rmin, rmax, rnumsteps) + f1_interp = np.interp(share_grid, f1_data[:, 0], f1_data[:, 1]) + f2_interp = np.interp(share_grid, f2_data[:, 0], f2_data[:, 1]) + diff_interp = np.interp(share_grid, diff_data[:, 0], diff_data[:, 1]) + + for idx, diff in enumerate(diff_interp): + assert np.isclose(f1_interp[idx] - f2_interp[idx], diff) + + class TestApp: @pytest.fixture def setup(self): @@ -165,6 +189,59 @@ def test_morph_outputs(self, setup, tmp_path): expected = filter(ignore_path, tf) are_files_same(actual, expected) + # Similar format as test_morph_outputs + def test_morph_diff_outputs(self, setup, tmp_path): + morph_file = self.testfiles[0] + target_file = self.testfiles[-1] + + # Save multiple diff morphs + tmp_diff = tmp_path.joinpath("diff") + tmp_diff_name = tmp_diff.resolve().as_posix() + + (opts, pargs) = self.parser.parse_args( + [ + "--multiple-targets", + "--sort-by", + "temperature", + "-s", + tmp_diff_name, + "-n", + "--save-names-file", + tssf, + "--diff", + ] + ) + pargs = [morph_file, testsequence_dir] + multiple_targets(self.parser, opts, pargs, stdout_flag=False) + + # Save a single diff morph + diff_name = "single_diff_morph.cgr" + diff_file = tmp_diff.joinpath(diff_name) + df_name = diff_file.resolve().as_posix() + (opts, pargs) = self.parser.parse_args(["-s", df_name, "-n", "--diff"]) + pargs = [morph_file, target_file] + single_morph(self.parser, opts, pargs, stdout_flag=False) + + # Check that the saved diff matches the morph minus target + # Morphs are saved in testdata/testsequence/testsaving/succinct + # Targets are stored in testdata/testsequence + + # Single morph diff + morphed_file = test_saving_succinct / diff_name.replace( + "diff", "succinct" + ) + are_diffs_right(morphed_file, target_file, diff_file) + + # Multiple morphs diff + diff_files = list((tmp_diff / "Morphs").iterdir()) + morphed_files = list((test_saving_succinct / "Morphs").iterdir()) + target_files = self.testfiles[1:] + diff_files.sort() + morphed_files.sort() + target_files.sort() + for idx, diff_file in enumerate(diff_files): + are_diffs_right(morphed_files[idx], target_files[idx], diff_file) + def test_morphsqueeze_outputs(self, setup, tmp_path): # The file squeeze_morph has a squeeze and stretch applied morph_file = testdata_dir / "squeeze_morph.cgr" diff --git a/tests/test_morphpy.py b/tests/test_morphpy.py index b599d9e4..643216e2 100644 --- a/tests/test_morphpy.py +++ b/tests/test_morphpy.py @@ -75,6 +75,7 @@ def test_morph_opts(self, setup_morph): "addpearson": False, "apply": False, "reverse": False, + "get_diff": False, "multiple_morphs": False, "multiple_targets": False, } @@ -96,6 +97,7 @@ def test_morph_opts(self, setup_morph): "addpearson": True, "apply": True, "reverse": True, + "get_diff": True, "multiple_morphs": True, "multiple_targets": True, }