Skip to content

Morphfuncxy #239

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Aug 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 Down Expand 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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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