Skip to content

doc/tutorial: adding a tutorial for MorphFuncy and proper docstring #215

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 5 commits into from
Jun 30, 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
79 changes: 79 additions & 0 deletions doc/source/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,85 @@ There is also support for morphing from a nanoparticle to a bulk. When
applying the inverse morphs, it is recommended to set ``--rmax=psize``
where ``psize`` is the longest diameter of the nanoparticle.

MorphFuncy: Applying custom functions
-------------------------------------

The ``MorphFuncy`` morph allows users to apply a custom Python function
to the y-axis values of a dataset, enabling flexible and user-defined
transformations.

In this tutorial, we walk through how to use ``MorphFuncy`` with an example
transformation. Unlike other morphs that can be run from the command line,
``MorphFuncy`` requires a Python function and is therefore intended to be used
through Python scripting.

1. Import the necessary modules into your Python script:

.. code-block:: python

from diffpy.morph.morph_api import morph, morph_default_config
import numpy as np

2. Define a custom Python function to apply a transformation to the data.
The function must take ``x`` and ``y`` (1D arrays of the same length)
along with named parameters, and return a transformed ``y`` array of the
same length.
For this example, we will use a simple linear transformation that
scales the input and applies an offset:

.. code-block:: python

def linear_function(x, y, scale, offset):
return (scale * x) * y + offset

3. In this example, we use a sine function for the morph data and generate
the target data by applying the linear transformation with known scale
and offset to it:

.. code-block:: python

x_morph = np.linspace(0, 10, 101)
y_morph = np.sin(x_morph)
x_target = x_morph.copy()
y_target = np.sin(x_target) * 20 * x_target + 0.8

4. Set up the morph configuration dictionary. This includes both the
transformation parameters (our initial guess) and the transformation
function itself:

.. code-block:: python

morph_config = morph_default_config(funcy={"scale": 1.2, "offset": 0.1})
morph_config["function"] = linear_function

# morph_config now contains:
# {'funcy': {'scale': 1.2, 'offset': 0.1}, 'function': linear_function}

5. Run the morph using the ``morph(...)``. This will apply the user-defined
function and refine the parameters to best align the morph data
with the target data:

.. code-block:: python

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add a line that shows what the dictionary looks like after it is created in case users want to create it in an editor and not use python code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Luiskitsu gentle ping on this

morph_result = morph(x_morph, y_morph, x_target, y_target, **morph_config)

6. Extract the morphed output and the fitted parameters from the result:

.. code-block:: python

fitted_config = morph_result["morphed_config"]
x_morph_out, y_morph_out, x_target_out, y_target_out = morph_result["morph_chain"].xyallout

fitted_params = fitted_config["funcy"]
print(f"Fitted scale: {fitted_params['scale']}")
print(f"Fitted offset: {fitted_params['offset']}")

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.

Bug Reports
===========

Expand Down
23 changes: 23 additions & 0 deletions news/tutorial_morphfuncy.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
**Added:**

* Added a tutorial for MorphFuncy

**Changed:**

* Changed docstrings location for MorphFuncy and MorphSqueeze

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
108 changes: 58 additions & 50 deletions src/diffpy/morph/morphs/morphfuncy.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,63 @@
"""Class MorphFuncy -- apply a user-supplied python function to the
y-axis."""

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


class MorphFuncy(Morph):
"""Apply the user-supplied Python function to the y-coordinates of
the morph data."""
"""Apply a custom function to the y-axis of the morph function.

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

Configuration Variables
-----------------------
function: callable
The user-supplied function that applies a transformation to the
y-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 funcy morph function:

>>> from diffpy.morph.morphs.morphfuncy import MorphFuncy

Define or import the user-supplied transformation function:

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

Provide initial guess for parameters:

>>> parameters = {'amplitude': 2, 'frequency': 2}

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

>>> morph = MorphFuncy()
>>> morph.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)

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.funcy
"""

# Define input output types
summary = "Apply a Python function to the y-axis data"
Expand All @@ -14,54 +68,8 @@ class MorphFuncy(Morph):
parnames = ["function", "funcy"]

def morph(self, x_morph, y_morph, x_target, y_target):
"""General morph function that applies a user-supplied function
to the y-coordinates of morph data to make it align with a
target.

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

parameters: dict
A dictionary of parameters to pass to the function.
These parameters are unpacked using **kwargs.

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 funcy morph function:
>>> from diffpy.morph.morphs.morphfuncy import MorphFuncy

Define or import the user-supplied transformation function:
>>> def sine_function(x, y, amplitude, frequency):
>>> return amplitude * np.sin(frequency * x) * y

Provide initial guess for parameters:
>>> parameters = {'amplitude': 2, 'frequency': 2}

Run the funcy morph given input morph array (x_morph, y_morph)
and target array (x_target, y_target):
>>> morph = MorphFuncy()
>>> morph.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)

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.funcy
"""
"""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.x_morph_in, self.y_morph_in, **self.funcy
Expand Down
89 changes: 50 additions & 39 deletions src/diffpy/morph/morphs/morphsqueeze.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""Class MorphSqueeze -- Apply a polynomial to squeeze the morph
function."""

import numpy as np
from numpy.polynomial import Polynomial
from scipy.interpolate import CubicSpline
Expand All @@ -6,9 +9,50 @@


class MorphSqueeze(Morph):
"""Apply a polynomial to squeeze the morph function.
"""Squeeze the morph function.

This applies a polynomial to squeeze the morph non-linearly.

Configuration Variables
-----------------------
squeeze : Dictionary
The polynomial coefficients {a0, a1, ..., an} for the squeeze
function where the polynomial would be of the form
a0 + a1*x + a2*x^2 and so on. The order of the polynomial is
determined by the length of the dictionary.

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
shifted according to the squeeze. The morphed data is returned on
the same grid as the unmorphed data.

Example
-------
Import the squeeze morph function:

>>> from diffpy.morph.morphs.morphsqueeze import MorphSqueeze

The morphed data is returned on the same grid as the unmorphed data.
Provide initial guess for squeezing coefficients:

>>> squeeze_coeff = {"a0":0.1, "a1":-0.01, "a2":0.005}

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

>>> morph = MorphSqueeze()
>>> morph.squeeze = squeeze_coeff
>>> x_morph_out, y_morph_out, x_target_out, y_target_out =
... 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
>>> squeeze_coeff_out = morph.squeeze
"""

# Define input output types
Expand All @@ -24,43 +68,10 @@ class MorphSqueeze(Morph):
extrap_index_high = None

def morph(self, x_morph, y_morph, x_target, y_target):
"""Squeeze the morph function.

This applies a polynomial to squeeze the morph non-linearly.

Configuration Variables
-----------------------
squeeze : Dictionary
The polynomial coefficients {a0, a1, ..., an} for the squeeze
function where the polynomial would be of the form
a0 + a1*x + a2*x^2 and so on. The order of the polynomial is
determined by the length of the dictionary.

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
shifted according to the squeeze. The morphed data is returned on
the same grid as the unmorphed data.

Example
-------
Import the squeeze morph function:
>>> from diffpy.morph.morphs.morphsqueeze import MorphSqueeze
Provide initial guess for squeezing coefficients:
>>> squeeze_coeff = {"a0":0.1, "a1":-0.01, "a2":0.005}
Run the squeeze morph given input morph array (x_morph, y_morph)
and target array (x_target, y_target):
>>> morph = MorphSqueeze()
>>> morph.squeeze = squeeze_coeff
>>> x_morph_out, y_morph_out, x_target_out, y_target_out = 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
>>> squeeze_coeff_out = morph.squeeze
"""Apply a polynomial to squeeze the morph function.

The morphed data is returned on the same grid as the unmorphed
data.
"""
Morph.morph(self, x_morph, y_morph, x_target, y_target)

Expand Down
Loading