diff --git a/doc/source/morphpy.rst b/doc/source/morphpy.rst new file mode 100644 index 00000000..3f7dea32 --- /dev/null +++ b/doc/source/morphpy.rst @@ -0,0 +1,174 @@ +.. _morphpy: + +Using diffpy.morph in Python +############################ + +On top of the command-line usage described in the `quickstart tutorial `__, +``diffpy.morph`` also supports Python integration. +This functionality is intended for those acquainted with the basic morphs +described in the aforementioned quickstart tutorial who want to use ``diffpy.morph`` in their +Python scripts. + +Python Morphing Functions +========================= + + 1. In the quickstart tutorial, you were asked to try a combined scale, stretch, and smear + morph on the files `darkSub_rh20_C_01.gr` and `darkSub_rh20_C_44.gr` using the command-line + command :: + + diffpy.morph --scale=0.8 --smear=-0.08 --stretch=0.5 --rmin=1.5 --rmax=30 darkSub_rh20_C_01.gr darkSub_rh20_C_44.gr + + 2. To do the same on Python, we must first create a new Python script in the same directory as the + data files `darkSub_rh20_C_01.gr` and `darkSub_rh20_C_44.gr`. + 3. Then, in that script, import :: + + from diffpy.morph.morphpy import morph + + 3. Finally, we run the ``morph`` function :: + + morph_info, morph_table = morph("darkSub_rh20_C_01.gr", "darkSub_rh20_C_44.gr", scale=0.8, smear=-0.08, stretch=0.5, rmin=1.5, rmax=30) + + * The ``morph`` function takes in two file names (or paths). You can also provide various parameters + for morphing (see the Full Parameter List below). + * If, let's say, the file `darkSub_rh20_C_01.gr` is in a subdirectory `subdir/darkSub_rh20_C_01.gr`, + you should replace ``"darkSub_rh20_C_01.gr"`` in the above example with ``"subdir/darkSub_rh20_C_01.gr"``. + + 4. The ``morph`` function returns a dictionary ``morph_info`` and a numpy array ``morph_table``. + + * ``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. + * ``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 + the morphs are applied interpolated onto the grid of `darkSub_rh20_C_44.gr`). + 5. Notice that most parameters you are able to use are the same as the options provided in the command-line + interface version of ``diffpy.morph``. For example, the ``--apply`` option becomes the ``apply=True`` parameter. + 6. With that, you have already mastered the basics of using ``diffpy.morph`` on Python! + 7. Note that instead of passing two files to ``diffpy.morph``, you might instead want to directly + pass arrays. For example, rather than passing `darkSub_rh20_C_01.gr`, I may want to pass + a two-column array named ``ds_rh20_c_01_array`` containing the data table contents of the file + `darkSub_rh20_C_01.gr`. In this case, we have a separate function :: + + from diffpy.morph.morphpy import morph_arrays + + 8. Assuming we have loaded the data in `darkSub_rh20_C_01.gr` into ``ds_rh20_c_01_array`` and + `darkSub_rh20_C_44.gr` into ``ds_rh20_c_44_array``, we can apply the same morph as step 3 + by running :: + + morph_info, morph_table = morph_arrays(ds_rh20_c_01_array, ds_rh20_c_44_array, scale=0.8, smear=-0.08, stretch=0.5, rmin=1.5, rmax=30) + + 9. Notice that the two-column format of the input to ``morph_arrays`` is the same as the + output of ``morph`` and ``morph_arrays``. It is VERY IMPORTANT that the data is in two-column format + rather than the traditional two-row format. This is to reflect the file formats conventionally + used to store PDFs. + 10. For a full list of parameters used by (both) ``morph`` and ``morph_arrays``, see the Full Parameter List + section below. + +Full Parameter List +=================== + +General Parameters +------------------ + +save: str or path + Save the morphed function to a the file passed to save. Use '-' for stdout. +verbose: bool + Print additional header details to saved files. These include details about the morph + inputs and outputs. +rmin: float + Minimum r-value (abscissa) to use for function comparisons. +rmax: float + Maximum r-value (abscissa) to use for function comparisons. +tolerance: float + Specify least squares refiner tolerance when optimizing for morph parameters. Default: 10e-8. +pearson: bool + The refiner instead maximizes agreement in the Pearson function + (default behavior is to minimize the residual). + Note that this is insensitive to scale. +addpearson: bool + Maximize agreement in the Pearson function as well as minimizing the residual. + +Manipulations +------------- +These parameters select the manipulations that are to be applied to the +function. The passed values will be refined unless specifically +excluded with the apply or exclude parameters. + +apply: bool + Apply morphs but do not refine. +exclude: str + Exclude a manipulation from refinement by name. +scale: float + Apply scale factor. This multiplies the function ordinate by scale. +stretch: float + Stretch function grid by a fraction stretch. Specifically, this multiplies the function grid by 1+stretch. +squeeze: list of float + Squeeze function grid given a polynomial + p(x) = squeeze[0]+squeeze[1]*x+...+squeeze[n]*x^n. n is dependent on the number + of values in the user-inputted comma-separated list. + The morph transforms the function grid from x to x+p(x). + When this parameter is given, hshift is disabled. + When n>1, stretch is disabled. +smear: float + Smear the peaks with a Gaussian of width smear. This + is done by convolving the function with a Gaussian + with standard deviation smear. If both smear and + smear_pdf are used, only smear_pdf will be + applied. +smear_pdf: float + Convert PDF to RDF. Then, smear peaks with a Gaussian + of width smear_pdf. Convert back to PDF. If both smear and + smear_pdf are used, only smear_pdf will be + applied. +slope: float + Slope of the baseline used in converting from PDF to RDF. + This is used with the option smear_pdf. The slope will + be estimated if not provided. +hshift: float + Shift the function horizontally by hshift to the right. +vshift: float + Shift the function vertically by vshift upward. +qdamp: float + Dampen PDF by a factor qdamp. +radius: float + Apply characteristic function of sphere with radius + given by parameter radius. If pradius is also specified, instead apply + characteristic function of spheroid with equatorial + radius radius and polar radius pradius. +pradius: float + Apply characteristic function of spheroid with + equatorial radius given by above parameter radius and polar radius pradius. + If only pradius is specified, instead apply + characteristic function of sphere with radius pradius. +iradius: float + Apply inverse characteristic function of sphere with + radius iradius. If ipradius is also specified, instead + apply inverse characteristic function of spheroid with + equatorial radius iradius and polar radius ipradius. +ipradius: float + Apply inverse characteristic function of spheroid with + equatorial radius iradius and polar radius ipradius. + If only ipradius is specified, instead apply inverse + characteristic function of sphere with radius ipradius. +funcy: tuple (function, dict) + See Python-Specific Morphs below. + +Python-Specific Morphs +====================== + +Some morphs in ``diffpy.morph`` are supported only in Python. Here, we detail +how they are used and how to call them. + +funcy: tuple (function, dict) + This morph applies the function funcy[0] with parameters given in funcy[1]. + The function funcy[0] must be a function of both the abscissa and ordinate + (e.g. take in at least two inputs with as many additional parameters as needed). + For example, let's start with a two-column table with abscissa x and ordinate y. + let us say we want to apply the function :: + + def linear(x, y, a, b, c): + return a * x + b * y + c + + This function takes in both the abscissa and ordinate on top of three additional + parameters a, b, and c. To use the funcy parameter with initial guesses + a=1.0, b=2.0, c=3.0, we would pass ``funcy=(linear, {a: 1.0, b: 2.0, c: 3.0})``. diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 0164ce20..54c68c5a 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -4,7 +4,11 @@ diffpy.morph Tutorial ##################### Welcome! This will be a quick tutorial to accquaint users with ``diffpy.morph`` -and some of what it can do. To see more details and definitions about +and some of what it can do on the command-line. +For those wishing to integrate ``diffpy.morph`` into their Python scripts, +see the `morphpy tutorial `__. + +To see more details and definitions about the morphs please see the publication describing ``diffpy.morph``. To be published: diff --git a/news/morphpy.rst b/news/morphpy.rst new file mode 100644 index 00000000..663d12e5 --- /dev/null +++ b/news/morphpy.rst @@ -0,0 +1,24 @@ +**Added:** + +* Python interfacing to call PDFmorph +* Returns dictionary of morph metrics (dict) and the r, gr pair for plotting or further manipulation + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/morph/morph_io.py b/src/diffpy/morph/morph_io.py index 79aeb8c8..4433fc79 100644 --- a/src/diffpy/morph/morph_io.py +++ b/src/diffpy/morph/morph_io.py @@ -16,6 +16,7 @@ from __future__ import print_function +import inspect import sys from pathlib import Path @@ -66,21 +67,44 @@ def single_morph_output( + "\n" ) + mr_copy = morph_results.copy() 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()) + # Handle special inputs (numerical) + if "squeeze" in mr_copy: + sq_dict = mr_copy.pop("squeeze") + rw_pos = list(mr_copy.keys()).index("Rw") + morph_results_list = list(mr_copy.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) + mr_copy = dict(morph_results_list) + funcy_function = None + if "function" in mr_copy: + funcy_function = mr_copy.pop("function") + print(funcy_function) + if "funcy" in mr_copy: + fy_dict = mr_copy.pop("funcy") + rw_pos = list(mr_copy.keys()).index("Rw") + morph_results_list = list(mr_copy.items()) + for idx, key in enumerate(fy_dict): + morph_results_list.insert( + rw_pos + idx, (f"funcy {key}", fy_dict[key]) + ) + mr_copy = dict(morph_results_list) # Normal inputs morphs_out += "\n".join( - f"# {key} = {morph_results[key]:.6f}" for key in morph_results.keys() + f"# {key} = {mr_copy[key]:.6f}" for key in mr_copy.keys() ) + # Special inputs (functional) + if funcy_function is not None: + morphs_in += '# funcy function =\n"""\n' + f_code, _ = inspect.getsourcelines(funcy_function) + n_leading = len(f_code[0]) - len(f_code[0].lstrip()) + for idx, f_line in enumerate(f_code): + f_code[idx] = f_line[n_leading:] + morphs_in += "".join(f_code) + morphs_in += '"""\n' # Printing to terminal if stdout_flag: @@ -88,7 +112,10 @@ def single_morph_output( # Saving to file if save_file is not None: - path_name = str(Path(morph_file).resolve()) + if not Path(morph_file).exists(): + path_name = "NO FILE PATH PROVIDED" + else: + path_name = str(Path(morph_file).resolve()) header = "# PDF created by diffpy.morph\n" header += f"# from {path_name}" diff --git a/src/diffpy/morph/morphapp.py b/src/diffpy/morph/morphapp.py index 891ce652..2dd6c623 100755 --- a/src/diffpy/morph/morphapp.py +++ b/src/diffpy/morph/morphapp.py @@ -18,6 +18,8 @@ import sys from pathlib import Path +import numpy + import diffpy.morph.morph_helpers as helpers import diffpy.morph.morph_io as io import diffpy.morph.morphs as morphs @@ -450,22 +452,33 @@ def custom_error(self, msg): return parser -def single_morph(parser, opts, pargs, stdout_flag=True): +def single_morph( + parser, opts, pargs, stdout_flag=True, python_wrap=False, pymorphs=None +): if len(pargs) < 2: parser.error("You must supply FILE1 and FILE2.") - elif len(pargs) > 2: + elif len(pargs) > 2 and not python_wrap: parser.error( "Too many arguments. Make sure you only supply FILE1 and FILE2." ) + elif not (len(pargs) == 2 or len(pargs) == 6) and python_wrap: + parser.error("Python wrapper error.") # Get the PDFs - x_morph, y_morph = getPDFFromFile(pargs[0]) - x_target, y_target = getPDFFromFile(pargs[1]) + # If we get from python, we may wrap, which has input size 4 + if len(pargs) == 6 and python_wrap: + x_morph = pargs[2] + y_morph = pargs[3] + x_target = pargs[4] + y_target = pargs[5] + else: + x_morph, y_morph = getPDFFromFile(pargs[0]) + x_target, y_target = getPDFFromFile(pargs[1]) if y_morph is None: - parser.error(f"No data table found in file: {pargs[0]}.") + parser.error(f"No data table found in: {pargs[0]}.") if y_target is None: - parser.error(f"No data table found in file: {pargs[1]}.") + parser.error(f"No data table found in: {pargs[1]}.") # Get tolerance tolerance = 1e-08 @@ -496,9 +509,33 @@ def single_morph(parser, opts, pargs, stdout_flag=True): chain.append(morphs.MorphRGrid()) refpars = [] + # Python-Specific Morphs + if pymorphs is not None: + # funcy value is a tuple (function,{param_dict}) + if "funcy" in pymorphs: + mfy_function = pymorphs["funcy"][0] + mfy_params = pymorphs["funcy"][1] + chain.append(morphs.MorphFuncy()) + config["function"] = mfy_function + config["funcy"] = mfy_params + refpars.append("funcy") + # Squeeze squeeze_poly_deg = -1 if opts.squeeze is not None: + # Handles both list and csv input + if ( + len(opts.squeeze) > 1 + and opts.squeeze[0] == "[" + and opts.squeeze[-1] == "]" + ): + opts.squeeze = opts.squeeze[1:-1] + elif ( + len(opts.squeeze) > 1 + and opts.squeeze[0] == "(" + and opts.squeeze[-1] == ")" + ): + opts.squeeze = opts.squeeze[1:-1] squeeze_coeffs = opts.squeeze.strip().split(",") squeeze_dict_in = {} for idx, coeff in enumerate(squeeze_coeffs): @@ -520,20 +557,6 @@ def single_morph(parser, opts, pargs, stdout_flag=True): chain.append(morphs.MorphStretch()) config["stretch"] = stretch_in refpars.append("stretch") - # Shift - # 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 and squeeze_poly_deg < 0: - hshift_in = opts.hshift - config["hshift"] = hshift_in - refpars.append("hshift") - if opts.vshift is not None: - vshift_in = opts.vshift - config["vshift"] = vshift_in - refpars.append("vshift") # Smear if opts.smear_pdf is not None: smear_in = opts.smear_pdf @@ -552,6 +575,20 @@ def single_morph(parser, opts, pargs, stdout_flag=True): chain.append(morphs.MorphSmear()) refpars.append("smear") config["smear"] = smear_in + # Shift + # 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 and squeeze_poly_deg < 0: + hshift_in = opts.hshift + config["hshift"] = hshift_in + refpars.append("hshift") + if opts.vshift is not None: + vshift_in = opts.vshift + config["vshift"] = vshift_in + refpars.append("vshift") # Size radii = [opts.radius, opts.pradius] nrad = 2 - radii.count(None) @@ -648,6 +685,12 @@ def single_morph(parser, opts, pargs, stdout_flag=True): 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}"]}) + if pymorphs is not None: + if "funcy" in pymorphs: + for funcy_param in pymorphs["funcy"][1].keys(): + morph_inputs.update( + {f"funcy {funcy_param}": pymorphs["funcy"][1][funcy_param]} + ) # Output morph parameters morph_results = dict(config.items()) @@ -698,10 +741,16 @@ def single_morph(parser, opts, pargs, stdout_flag=True): l_width=l_width, ) - return morph_results + # 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 + return morph_info, morph_table + else: + return morph_results -def multiple_targets(parser, opts, pargs, stdout_flag=True): +def multiple_targets(parser, opts, pargs, stdout_flag=True, python_wrap=False): # Custom error messages since usage is distinct when --multiple tag is # applied if len(pargs) < 2: @@ -884,7 +933,7 @@ def multiple_targets(parser, opts, pargs, stdout_flag=True): return morph_results -def multiple_morphs(parser, opts, pargs, stdout_flag=True): +def multiple_morphs(parser, opts, pargs, stdout_flag=True, python_wrap=False): # Custom error messages since usage is distinct when --multiple tag is # applied if len(pargs) < 2: diff --git a/src/diffpy/morph/morphpy.py b/src/diffpy/morph/morphpy.py new file mode 100644 index 00000000..945ad749 --- /dev/null +++ b/src/diffpy/morph/morphpy.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python + +import numpy as np + +from diffpy.morph.morphapp import create_option_parser, single_morph + + +def get_args(parser, params, kwargs): + inputs = [] + for key, value in params.items(): + if value is not None: + inputs.append(f"--{key}") + inputs.append(f"{value}") + for key, value in kwargs.items(): + key = key.replace("_", "-") + inputs.append(f"--{key}") + inputs.append(f"{value}") + (opts, pargs) = parser.parse_args(inputs) + return opts, pargs + + +# Take in file names as input. +def morph( + morph_file, + target_file, + scale=None, + stretch=None, + smear=None, + plot=False, + **kwargs, +): + """Run diffpy.morph at Python level. + + Parameters + ---------- + morph_file: str or numpy.array + Path-like object to the file to be morphed. + target_file: str or numpy.array + Path-like object to the target file. + scale: float, optional + Initial guess for the scaling parameter. + Refinement is done only for parameter that are not None. + stretch: float, optional + Initial guess for the stretching parameter. + smear: float, optional + Initial guess for the smearing parameter. + plot: bool + Show a plot of the morphed and target functions as well as the + difference curve (default: False). + kwargs: dict + See the diffpy.morph website for full list of options. + Returns + ------- + morph_info: dict + Summary of morph parameters (e.g. scale, stretch, smear, rmin, rmax) + and results (e.g. Pearson, Rw). + morph_table: list + Function after morph where morph_table[:,0] is the abscissa and + morph_table[:,1] is the ordinate. + """ + + # Check for Python-specific morphs + python_morphs = ["funcy"] + pymorphs = {} + for pmorph in python_morphs: + if pmorph in kwargs: + pmorph_value = kwargs.pop(pmorph) + pymorphs.update({pmorph: pmorph_value}) + + # Special handling of parameters with dashes + kwargs_copy = kwargs.copy() + kwargs = {} + for key in kwargs_copy.keys(): + new_key = key + if "_" in key: + new_key = key.replace("_", "-") + kwargs.update({new_key: kwargs_copy[key]}) + + # Wrap the CLI + parser = create_option_parser() + params = { + "scale": scale, + "stretch": stretch, + "smear": smear, + "noplot": True if not plot else None, + } + opts, _ = get_args(parser, params, kwargs) + + pargs = [morph_file, target_file] + + if not len(pymorphs) > 0: + pymorphs = None + return single_morph( + parser, + opts, + pargs, + stdout_flag=False, + python_wrap=True, + pymorphs=pymorphs, + ) + + +# Take in array-like objects as input. +def morph_arrays( + morph_table, + target_table, + scale=None, + stretch=None, + smear=None, + plot=False, + **kwargs, +): + """Run diffpy.morph at Python level. + + Parameters + ---------- + morph_table: numpy.array + Two-column array of (r, gr) for morphed function. + target_table: numpy.array + Two-column array of (r, gr) for target function. + scale: float, optional + Initial guess for the scaling parameter. + Refinement is done only for parameter that are not None. + stretch: float, optional + Initial guess for the stretching parameter. + smear: float, optional + Initial guess for the smearing parameter. + plot: bool + Show a plot of the morphed and target functions as well as the + difference curve (default: False). + kwargs: dict + See the diffpy.morph website for full list of options. + Returns + ------- + morph_info: dict + Summary of morph parameters (e.g. scale, stretch, smear, rmin, rmax) + and results (e.g. Pearson, Rw). + morph_table: list + Function after morph where morph_table[:,0] is the abscissa and + morph_table[:,1] is the ordinate. + """ + # Check for Python-specific morphs + python_morphs = ["funcy"] + pymorphs = {} + for pmorph in python_morphs: + if pmorph in kwargs: + pmorph_value = kwargs.pop(pmorph) + pymorphs.update({pmorph: pmorph_value}) + + # Wrap the CLI + parser = create_option_parser() + params = { + "scale": scale, + "stretch": stretch, + "smear": smear, + "noplot": True if not plot else None, + } + opts, _ = get_args(parser, params, kwargs) + + morph_table = np.array(morph_table) + target_table = np.array(target_table) + + x_morph = morph_table[:, 0] + y_morph = morph_table[:, 1] + x_target = target_table[:, 0] + y_target = target_table[:, 1] + + pargs = ["Morph", "Target", x_morph, y_morph, x_target, y_target] + + if not len(pymorphs) > 0: + pymorphs = None + return single_morph( + parser, + opts, + pargs, + stdout_flag=False, + python_wrap=True, + pymorphs=pymorphs, + ) diff --git a/src/diffpy/morph/morphs/morphfuncy.py b/src/diffpy/morph/morphs/morphfuncy.py index 427db48d..98360251 100644 --- a/src/diffpy/morph/morphs/morphfuncy.py +++ b/src/diffpy/morph/morphs/morphfuncy.py @@ -11,7 +11,7 @@ class MorphFuncy(Morph): yinlabel = LABEL_GR xoutlabel = LABEL_RA youtlabel = LABEL_GR - parnames = ["funcy"] + parnames = ["function", "funcy"] def morph(self, x_morph, y_morph, x_target, y_target): """General morph function that applies a user-supplied function @@ -63,7 +63,6 @@ def morph(self, x_morph, y_morph, x_target, y_target): >>> parameters_out = morph.funcy """ Morph.morph(self, x_morph, y_morph, x_target, y_target) - self.y_morph_out = self.function( self.x_morph_in, self.y_morph_in, **self.funcy ) diff --git a/tests/test_morphio.py b/tests/test_morphio.py index 0bfc9fd0..37f7dcd1 100644 --- a/tests/test_morphio.py +++ b/tests/test_morphio.py @@ -10,6 +10,7 @@ multiple_targets, single_morph, ) +from diffpy.morph.morphpy import morph_arrays # Support Python 2 try: @@ -150,7 +151,7 @@ def test_morph_outputs(self, setup, tmp_path): 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): + 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" target_file = testdata_dir / "squeeze_target.cgr" @@ -192,3 +193,28 @@ def test_morph_squeeze_outputs(self, setup, tmp_path): ) else: assert m_row[idx] == t_row[idx] + + def test_morphfuncy_outputs(self, tmp_path): + def quadratic(x, y, a0, a1, a2): + return a0 + a1 * x + a2 * y**2 + + r = np.linspace(0, 10, 101) + gr = np.linspace(0, 10, 101) + + morph_arrays( + np.array([r, gr]).T, + np.array([r, quadratic(r, gr, 1, 2, 3)]).T, + squeeze=[0, 0, 0], + funcy=(quadratic, {"a0": 1.0, "a1": 2.0, "a2": 3.0}), + apply=True, + save=tmp_path / "funcy_target.cgr", + verbose=True, + ) + + with open(testdata_dir.joinpath("funcy_target.cgr")) as tf: + with open(tmp_path.joinpath("funcy_target.cgr")) as gf: + generated = filter(ignore_path, gf) + target = filter(ignore_path, tf) + for x, y in zip(generated, target): + assert x == y + assert all(x == y for x, y in zip(generated, target)) diff --git a/tests/test_morphpy.py b/tests/test_morphpy.py new file mode 100644 index 00000000..cc4d43a0 --- /dev/null +++ b/tests/test_morphpy.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python + +from pathlib import Path + +import numpy as np +import pytest + +from diffpy.morph.morphapp import create_option_parser, single_morph +from diffpy.morph.morphpy import morph, morph_arrays +from diffpy.morph.tools import getRw + +thisfile = locals().get("__file__", "file.py") +tests_dir = Path(thisfile).parent.resolve() +testdata_dir = tests_dir.joinpath("testdata") +testsequence_dir = testdata_dir.joinpath("testsequence") + +nickel_PDF = testdata_dir.joinpath("nickel_ss0.01.cgr") +serial_JSON = testdata_dir.joinpath("testsequence_serialfile.json") + +testsaving_dir = testsequence_dir.joinpath("testsaving") +test_saving_succinct = testsaving_dir.joinpath("succinct") +test_saving_verbose = testsaving_dir.joinpath("verbose") +tssf = testdata_dir.joinpath("testsequence_serialfile.json") + + +class TestMorphpy: + @pytest.fixture + def setup_morph(self): + self.parser = create_option_parser() + filenames = [ + "g_174K.gr", + "f_180K.gr", + "e_186K.gr", + "d_192K.gr", + "c_198K.gr", + "b_204K.gr", + "a_210K.gr", + ] + self.testfiles = [] + self.morphapp_results = {} + + # Parse arguments sorting by field + (opts, pargs) = self.parser.parse_args( + [ + "--scale", + "1", + "--stretch", + "0", + "-n", + "--sort-by", + "temperature", + ] + ) + for filename in filenames: + self.testfiles.append(testsequence_dir.joinpath(filename)) + + # Run multiple single morphs + morph_file = self.testfiles[0] + for target_file in self.testfiles[1:]: + pargs = [morph_file, target_file] + # store in same format of dictionary as multiple_targets + self.morphapp_results.update( + { + target_file.name: single_morph( + self.parser, opts, pargs, stdout_flag=False + ) + } + ) + return + + def test_morph(self, setup_morph): + morph_results = {} + morph_file = self.testfiles[0] + for target_file in self.testfiles[1:]: + mr, grm = morph( + morph_file, + target_file, + scale=1, + stretch=0, + sort_by="temperature", + ) + _, grt = morph(target_file, target_file) + morph_results.update({target_file.name: mr}) + + class Chain: + xyallout = grm[:, 0], grm[:, 1], grt[:, 0], grt[:, 1] + + chain = Chain() + rw = getRw(chain) + del chain + assert np.allclose( + [rw], [self.morphapp_results[target_file.name]["Rw"]] + ) + assert morph_results == self.morphapp_results + + def test_morphpy(self, setup_morph): + morph_results = {} + morph_file = self.testfiles[0] + for target_file in self.testfiles[1:]: + _, grm0 = morph(morph_file, morph_file) + _, grt = morph(target_file, target_file) + mr, grm = morph_arrays( + grm0, grt, scale=1, stretch=0, sort_by="temperature" + ) + morph_results.update({target_file.name: mr}) + + class Chain: + xyallout = grm[:, 0], grm[:, 1], grt[:, 0], grt[:, 1] + + chain = Chain() + rw = getRw(chain) + del chain + assert np.allclose( + [rw], [self.morphapp_results[target_file.name]["Rw"]] + ) + assert morph_results == self.morphapp_results + + def test_morphfuncy(self, setup_morph): + def gaussian(x, mu, sigma): + return np.exp(-((x - mu) ** 2) / (2 * sigma**2)) / ( + sigma * np.sqrt(2 * np.pi) + ) + + def gaussian_like_function(x, y, mu): + return gaussian((x + y) / 2, mu, 3) + + morph_r = np.linspace(0, 100, 1001) + morph_gr = np.linspace(0, 100, 1001) + + target_r = np.linspace(0, 100, 1001) + target_gr = 0.5 * gaussian(target_r, 50, 5) + 0.05 + + morph_info, _ = morph_arrays( + np.array([morph_r, morph_gr]).T, + np.array([target_r, target_gr]).T, + scale=1, + smear=3.75, + vshift=0.01, + funcy=(gaussian_like_function, {"mu": 47.5}), + tolerance=1e-12, + ) + + assert pytest.approx(morph_info["scale"]) == 0.5 + assert pytest.approx(morph_info["vshift"]) == 0.05 + assert pytest.approx(abs(morph_info["smear"])) == 4.0 + assert pytest.approx(morph_info["funcy"]["mu"]) == 50.0 + + def test_morphpy_outputs(self, tmp_path): + r = np.linspace(0, 1, 11) + gr = np.linspace(0, 1, 11) + + def linear(x, y, s): + return s * (x + y) + + morph_info, _ = morph_arrays( + np.array([r, gr]).T, + np.array([r, gr]).T, + squeeze=[1, 2, 3, 4, 5], + funcy=(linear, {"s": 2.5}), + apply=True, + ) + + print(morph_info) + for i in range(5): + assert pytest.approx(morph_info["squeeze"][f"a{i}"]) == i + 1 + assert pytest.approx(morph_info["funcy"]["s"]) == 2.5 + + +if __name__ == "__main__": + TestMorphpy() diff --git a/tests/testdata/funcy_target.cgr b/tests/testdata/funcy_target.cgr new file mode 100644 index 00000000..5744e86a --- /dev/null +++ b/tests/testdata/funcy_target.cgr @@ -0,0 +1,136 @@ +# PDF created by diffpy.morph +# from NO FILE PATH PROVIDED + +# Input morphing parameters: +# scale = None +# stretch = None +# smear = None +# hshift = None +# vshift = None +# squeeze a0 = 0.0 +# squeeze a1 = 0.0 +# squeeze a2 = 0.0 +# funcy a0 = 1.0 +# funcy a1 = 2.0 +# funcy a2 = 3.0 +# funcy function = +""" +def quadratic(x, y, a0, a1, a2): + return a0 + a1 * x + a2 * y**2 +""" + +# Optimized morphing parameters: +# rmin = 0.000000 +# rmax = 10.100000 +# rstep = 0.100000 +# squeeze a0 = 0.000000 +# squeeze a1 = 0.000000 +# squeeze a2 = 0.000000 +# funcy a0 = 1.000000 +# funcy a1 = 2.000000 +# funcy a2 = 3.000000 +# Rw = 0.000000 +# Pearson = 1.000000 + +# Labels: [r] [gr] +0.000000000000000000e+00 1.000000000000000000e+00 +1.000000000000000056e-01 1.229999999999999982e+00 +2.000000000000000111e-01 1.520000000000000018e+00 +3.000000000000000444e-01 1.870000000000000107e+00 +4.000000000000000222e-01 2.280000000000000249e+00 +5.000000000000000000e-01 2.750000000000000000e+00 +6.000000000000000888e-01 3.280000000000000249e+00 +7.000000000000000666e-01 3.870000000000000551e+00 +8.000000000000000444e-01 4.520000000000000462e+00 +9.000000000000000222e-01 5.230000000000000426e+00 +1.000000000000000000e+00 6.000000000000000000e+00 +1.100000000000000089e+00 6.830000000000000959e+00 +1.200000000000000178e+00 7.720000000000001528e+00 +1.300000000000000044e+00 8.669999999999999929e+00 +1.400000000000000133e+00 9.680000000000001492e+00 +1.500000000000000000e+00 1.075000000000000000e+01 +1.600000000000000089e+00 1.188000000000000256e+01 +1.700000000000000178e+00 1.307000000000000206e+01 +1.800000000000000044e+00 1.432000000000000028e+01 +1.900000000000000133e+00 1.563000000000000256e+01 +2.000000000000000000e+00 1.700000000000000000e+01 +2.100000000000000089e+00 1.842999999999999972e+01 +2.200000000000000178e+00 1.992000000000000171e+01 +2.300000000000000266e+00 2.147000000000000242e+01 +2.400000000000000355e+00 2.308000000000000540e+01 +2.500000000000000000e+00 2.475000000000000000e+01 +2.600000000000000089e+00 2.648000000000000043e+01 +2.700000000000000178e+00 2.827000000000000313e+01 +2.800000000000000266e+00 3.012000000000000455e+01 +2.900000000000000355e+00 3.203000000000000114e+01 +3.000000000000000000e+00 3.400000000000000000e+01 +3.100000000000000089e+00 3.603000000000000824e+01 +3.200000000000000178e+00 3.812000000000000455e+01 +3.300000000000000266e+00 4.027000000000001023e+01 +3.400000000000000355e+00 4.248000000000000398e+01 +3.500000000000000000e+00 4.475000000000000000e+01 +3.600000000000000089e+00 4.707999999999999829e+01 +3.700000000000000178e+00 4.947000000000000597e+01 +3.800000000000000266e+00 5.192000000000000881e+01 +3.900000000000000355e+00 5.443000000000000682e+01 +4.000000000000000000e+00 5.700000000000000000e+01 +4.100000000000000533e+00 5.963000000000002387e+01 +4.200000000000000178e+00 6.232000000000000028e+01 +4.299999999999999822e+00 6.506999999999999318e+01 +4.400000000000000355e+00 6.788000000000000966e+01 +4.500000000000000000e+00 7.075000000000000000e+01 +4.600000000000000533e+00 7.368000000000000682e+01 +4.700000000000000178e+00 7.667000000000001592e+01 +4.800000000000000711e+00 7.972000000000002728e+01 +4.900000000000000355e+00 8.283000000000001251e+01 +5.000000000000000000e+00 8.600000000000000000e+01 +5.100000000000000533e+00 8.923000000000001819e+01 +5.200000000000000178e+00 9.252000000000001023e+01 +5.300000000000000711e+00 9.587000000000003297e+01 +5.400000000000000355e+00 9.928000000000001535e+01 +5.500000000000000000e+00 1.027500000000000000e+02 +5.600000000000000533e+00 1.062800000000000153e+02 +5.700000000000000178e+00 1.098700000000000045e+02 +5.800000000000000711e+00 1.135200000000000102e+02 +5.900000000000000355e+00 1.172300000000000040e+02 +6.000000000000000000e+00 1.210000000000000000e+02 +6.100000000000000533e+00 1.248300000000000267e+02 +6.200000000000000178e+00 1.287200000000000273e+02 +6.300000000000000711e+00 1.326700000000000443e+02 +6.400000000000000355e+00 1.366800000000000352e+02 +6.500000000000000000e+00 1.407500000000000000e+02 +6.600000000000000533e+00 1.448800000000000239e+02 +6.700000000000000178e+00 1.490700000000000216e+02 +6.800000000000000711e+00 1.533200000000000216e+02 +6.900000000000000355e+00 1.576300000000000239e+02 +7.000000000000000000e+00 1.620000000000000000e+02 +7.100000000000000533e+00 1.664300000000000068e+02 +7.200000000000000178e+00 1.709200000000000159e+02 +7.300000000000000711e+00 1.754700000000000273e+02 +7.400000000000000355e+00 1.800800000000000409e+02 +7.500000000000000000e+00 1.847500000000000000e+02 +7.600000000000000533e+00 1.894800000000000182e+02 +7.700000000000000178e+00 1.942700000000000102e+02 +7.800000000000000711e+00 1.991200000000000330e+02 +7.900000000000000355e+00 2.040300000000000296e+02 +8.000000000000000000e+00 2.090000000000000000e+02 +8.099999999999999645e+00 2.140299999999999727e+02 +8.200000000000001066e+00 2.191200000000000898e+02 +8.300000000000000711e+00 2.242700000000000387e+02 +8.400000000000000355e+00 2.294800000000000182e+02 +8.500000000000000000e+00 2.347500000000000000e+02 +8.599999999999999645e+00 2.400799999999999841e+02 +8.700000000000001066e+00 2.454700000000000557e+02 +8.800000000000000711e+00 2.509200000000000443e+02 +8.900000000000000355e+00 2.564300000000000068e+02 +9.000000000000000000e+00 2.620000000000000000e+02 +9.099999999999999645e+00 2.676299999999999386e+02 +9.200000000000001066e+00 2.733200000000000500e+02 +9.300000000000000711e+00 2.790700000000000500e+02 +9.400000000000000355e+00 2.848800000000000523e+02 +9.500000000000000000e+00 2.907500000000000000e+02 +9.600000000000001421e+00 2.966800000000000637e+02 +9.700000000000001066e+00 3.026700000000000159e+02 +9.800000000000000711e+00 3.087200000000000841e+02 +9.900000000000000355e+00 3.148300000000000409e+02 +1.000000000000000000e+01 3.210000000000000000e+02