Skip to content
Merged
329 changes: 310 additions & 19 deletions docs/source/morphpy.rst

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions news/morphfuncxy.rst
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
**Added:**

* morphfuncx added: apply a function to the grid of your morphed function; this function should maintain the monotonic increasing nature of the grid
* morphfuncxy added: apply a general function which can modify both the ordinate and abscissa; useful when applying fourier transform or integration functions

**Changed:**

* <news item>

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
2 changes: 1 addition & 1 deletion src/diffpy/morph/morph_api.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -209,7 +209,7 @@ def morph(
refpars.append("baselineslope")
elif k == "funcy":
morph_inst = morph_cls()
morph_inst.function = rv_cfg.get("function", None)
morph_inst.function = rv_cfg.get("funcy_function", None)
if morph_inst.function is None:
raise ValueError(
"Must provide a 'function' when using 'parameters'"
Expand Down
53 changes: 31 additions & 22 deletions src/diffpy/morph/morph_io.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -79,32 +79,41 @@ def single_morph_output(
rw_pos + idx, (f"squeeze a{idx}", sq_dict[f"a{idx}"])
)
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)

# Handle special inputs (functional remove)
func_dicts = {
"funcxy": [None, None],
"funcx": [None, None],
"funcy": [None, None],
}
for func in func_dicts.keys():
if f"{func}_function" in mr_copy:
func_dicts[func][0] = mr_copy.pop(f"{func}_function")
if func in mr_copy:
func_dicts[func][1] = mr_copy.pop(func)
rw_pos = list(mr_copy.keys()).index("Rw")
morph_results_list = list(mr_copy.items())
for idx, key in enumerate(func_dicts[func][1]):
morph_results_list.insert(
rw_pos + idx, (f"{func} {key}", func_dicts[func][1][key])
)
mr_copy = dict(morph_results_list)

# Normal inputs
morphs_out += "\n".join(
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'

# Handle special inputs (functional add)
for func in func_dicts.keys():
if func_dicts[func][0] is not None:
morphs_in += f'# {func} function =\n"""\n'
f_code, _ = inspect.getsourcelines(func_dicts[func][0])
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:
Expand Down
35 changes: 33 additions & 2 deletions src/diffpy/morph/morphapp.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -520,12 +520,26 @@ def single_morph(

# Python-Specific Morphs
if pymorphs is not None:
# funcy value is a tuple (function,{param_dict})
# funcxy/funcx/funcy value is a tuple (function,{param_dict})
if "funcxy" in pymorphs:
mfxy_function = pymorphs["funcxy"][0]
mfxy_params = pymorphs["funcxy"][1]
chain.append(morphs.MorphFuncxy())
config["funcxy_function"] = mfxy_function
config["funcxy"] = mfxy_params
refpars.append("funcxy")
if "funcx" in pymorphs:
mfx_function = pymorphs["funcx"][0]
mfx_params = pymorphs["funcx"][1]
chain.append(morphs.MorphFuncx())
config["funcx_function"] = mfx_function
config["funcx"] = mfx_params
refpars.append("funcx")
if "funcy" in pymorphs:
mfy_function = pymorphs["funcy"][0]
mfy_params = pymorphs["funcy"][1]
chain.append(morphs.MorphFuncy())
config["function"] = mfy_function
config["funcy_function"] = mfy_function
config["funcy"] = mfy_params
refpars.append("funcy")

Expand DownExpand Up@@ -692,6 +706,9 @@ def single_morph(

# FOR FUTURE MAINTAINERS
# Any new morph should have their input morph parameters updated here
# You should also update the IO in morph_io
# if you think there requires special handling

# Input morph parameters
morph_inputs = {
"scale": scale_in,
Expand All@@ -705,11 +722,25 @@ def single_morph(
for idx, _ in enumerate(squeeze_dict):
morph_inputs.update({f"squeeze a{idx}": squeeze_dict[f"a{idx}"]})
if pymorphs is not None:
if "funcxy" in pymorphs:
for funcxy_param in pymorphs["funcxy"][1].keys():
morph_inputs.update(
{
f"funcxy {funcxy_param}": pymorphs["funcxy"][1][
funcxy_param
]
}
)
if "funcy" in pymorphs:
for funcy_param in pymorphs["funcy"][1].keys():
morph_inputs.update(
{f"funcy {funcy_param}": pymorphs["funcy"][1][funcy_param]}
)
if "funcx" in pymorphs:
for funcy_param in pymorphs["funcx"][1].keys():
morph_inputs.update(
{f"funcx {funcy_param}": pymorphs["funcx"][1][funcy_param]}
)

# Output morph parameters
morph_results = dict(config.items())
Expand Down
2 changes: 1 addition & 1 deletion src/diffpy/morph/morphpy.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,7 @@ def get_args(parser, params, kwargs):

def __get_morph_opts__(parser, scale, stretch, smear, plot, **kwargs):
# Check for Python-specific options
python_morphs = ["funcy"]
python_morphs = ["funcy", "funcx", "funcxy"]
pymorphs = {}
for pmorph in python_morphs:
if pmorph in kwargs:
Expand Down
4 changes: 4 additions & 0 deletions src/diffpy/morph/morphs/__init__.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,8 @@

from diffpy.morph.morphs.morph import Morph # noqa: F401
from diffpy.morph.morphs.morphchain import MorphChain # noqa: F401
from diffpy.morph.morphs.morphfuncx import MorphFuncx
from diffpy.morph.morphs.morphfuncxy import MorphFuncxy
from diffpy.morph.morphs.morphfuncy import MorphFuncy
from diffpy.morph.morphs.morphishape import MorphISphere, MorphISpheroid
from diffpy.morph.morphs.morphresolution import MorphResolutionDamping
Expand All@@ -42,6 +44,8 @@
MorphShift,
MorphSqueeze,
MorphFuncy,
MorphFuncx,
MorphFuncxy,
]

# End of file
87 changes: 87 additions & 0 deletions src/diffpy/morph/morphs/morphfuncx.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
"""Class MorphFuncx -- apply a user-supplied python function to the
x-axis."""

from diffpy.morph.morphs.morph import LABEL_GR, LABEL_RA, Morph


class MorphFuncx(Morph):
"""Apply a custom function to the x-axis (grid) of the morph
function.

General morph function that applies a user-supplied function to the
x-coordinates of morph data to make it align with a target.

Notice: the function provided must preserve the monotonic
increase of the grid.
I.e. the function f applied on the grid x must ensure for all
indices i<j, f(x[i]) < f(x[j]).

Configuration Variables
-----------------------
function: callable
The user-supplied function that applies a transformation to the
x-coordinates of the data.

parameters: dict
A dictionary of parameters to pass to the function.

Returns
-------
A tuple (x_morph_out, y_morph_out, x_target_out, y_target_out)
where the target values remain the same and the morph data is
transformed according to the user-specified function and parameters
The morphed data is returned on the same grid as the unmorphed data

Example
-------
Import the funcx morph function:

>>> from diffpy.morph.morphs.morphfuncx import MorphFuncx

Define or import the user-supplied transformation function:

>>> import numpy as np
>>> def exp_function(x, y, scale, rate):
>>> return abs(scale) * np.exp(rate * x)

Note that this transformation is monotonic increasing, so will preserve
the monotonic increasing nature of the provided grid.

Provide initial guess for parameters:

>>> parameters = {'scale': 1, 'rate': 1}

Run the funcy morph given input morph array (x_morph, y_morph)and target
array (x_target, y_target):

>>> morph = MorphFuncx()
>>> morph.funcx_function = exp_function
>>> morph.funcx = parameters
>>> x_morph_out, y_morph_out, x_target_out, y_target_out =
... morph.morph(x_morph, y_morph, x_target, y_target)

To access parameters from the morph instance:

>>> x_morph_in = morph.x_morph_in
>>> y_morph_in = morph.y_morph_in
>>> x_target_in = morph.x_target_in
>>> y_target_in = morph.y_target_in
>>> parameters_out = morph.funcx
"""

# Define input output types
summary = "Apply a Python function to the x-axis data"
xinlabel = LABEL_RA
yinlabel = LABEL_GR
xoutlabel = LABEL_RA
youtlabel = LABEL_GR
parnames = ["funcx_function", "funcx"]

def morph(self, x_morph, y_morph, x_target, y_target):
"""Apply the user-supplied Python function to the x-coordinates
of the morph data."""
Morph.morph(self, x_morph, y_morph, x_target, y_target)
self.x_morph_out = self.funcx_function(
self.x_morph_in, self.y_morph_in, **self.funcx
)
return self.xyallout
85 changes: 85 additions & 0 deletions src/diffpy/morph/morphs/morphfuncxy.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
"""Class MorphFuncxy -- apply a user-supplied python function to both
the x and y axes."""

from diffpy.morph.morphs.morph import LABEL_GR, LABEL_RA, Morph


class MorphFuncxy(Morph):
"""Apply a custom function to the morph function.

General morph function that applies a user-supplied function to the
morph data to make it align with a target.

This function may modify both the grid (x-axis) and function (y-axis)
of the morph data.

The user-provided function must return a two-column 1D function.

Configuration Variables
-----------------------
function: callable
The user-supplied function that applies a transformation to the
grid (x-axis) and morph function (y-axis).

parameters: dict
A dictionary of parameters to pass to the function.

Returns
-------
A tuple (x_morph_out, y_morph_out, x_target_out, y_target_out)
where the target values remain the same and the morph data is
transformed according to the user-specified function and parameters
The morphed data is returned on the same grid as the unmorphed data

Example (EDIT)
-------
Import the funcxy morph function:

>>> from diffpy.morph.morphs.morphfuncxy import MorphFuncxy

Define or import the user-supplied transformation function:

>>> import numpy as np
>>> def shift_function(x, y, hshift, vshift):
>>> return x + hshift, y + vshift

Provide initial guess for parameters:

>>> parameters = {'hshift': 1, 'vshift': 1}

Run the funcy morph given input morph array (x_morph, y_morph)and target
array (x_target, y_target):

>>> morph = MorphFuncxy()
>>> morph.function = shift_function
>>> morph.funcy = parameters
>>> x_morph_out, y_morph_out, x_target_out, y_target_out =
... morph.morph(x_morph, y_morph, x_target, y_target)

To access parameters from the morph instance:

>>> x_morph_in = morph.x_morph_in
>>> y_morph_in = morph.y_morph_in
>>> x_target_in = morph.x_target_in
>>> y_target_in = morph.y_target_in
>>> parameters_out = morph.funcxy
"""

# Define input output types
summary = (
"Apply a Python function to the data (y-axis) and data grid (x-axis)"
)
xinlabel = LABEL_RA
yinlabel = LABEL_GR
xoutlabel = LABEL_RA
youtlabel = LABEL_GR
parnames = ["funcxy_function", "funcxy"]

def morph(self, x_morph, y_morph, x_target, y_target):
"""Apply the user-supplied Python function to the y-coordinates
of the morph data."""
Morph.morph(self, x_morph, y_morph, x_target, y_target)
self.x_morph_out, self.y_morph_out = self.funcxy_function(
self.x_morph_in, self.y_morph_in, **self.funcxy
)
return self.xyallout
7 changes: 4 additions & 3 deletions src/diffpy/morph/morphs/morphfuncy.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,7 @@ class MorphFuncy(Morph):

Define or import the user-supplied transformation function:

>>> import numpy as np
>>> def sine_function(x, y, amplitude, frequency):
>>> return amplitude * np.sin(frequency * x) * y

Expand All@@ -45,7 +46,7 @@ class MorphFuncy(Morph):
array (x_target, y_target):

>>> morph = MorphFuncy()
>>> morph.function = sine_function
>>> morph.funcy_function = sine_function
>>> morph.funcy = parameters
>>> x_morph_out, y_morph_out, x_target_out, y_target_out =
... morph.morph(x_morph, y_morph, x_target, y_target)
Expand All@@ -65,13 +66,13 @@ class MorphFuncy(Morph):
yinlabel = LABEL_GR
xoutlabel = LABEL_RA
youtlabel = LABEL_GR
parnames = ["function", "funcy"]
parnames = ["funcy_function", "funcy"]

def morph(self, x_morph, y_morph, x_target, y_target):
"""Apply the user-supplied Python function to the y-coordinates
of the morph data."""
Morph.morph(self, x_morph, y_morph, x_target, y_target)
self.y_morph_out = self.function(
self.y_morph_out = self.funcy_function(
self.x_morph_in, self.y_morph_in, **self.funcy
)
return self.xyallout
Loading
Loading