Skip to content

Added MorphSqueeze and MorphFuncy tutorials #226

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 4 commits into from
Jul 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
35 changes: 12 additions & 23 deletions doc/source/morphpy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ through Python scripting.

.. code-block:: python

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

2. Define a custom Python function to apply a transformation to the data.
Expand All @@ -215,39 +215,28 @@ through Python scripting.
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:
4. Setup and run the morph using the ``morph_arrays(...)``.
``morph_arrays`` expects the morph and target data as **2D arrays** in
*two-column* format ``[[x0, y0], [x1, y1], ...]``. This will apply
the user-defined function and refine the parameters to best align the
morph data with the target data. 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_params, morph_table = morph_arrays(np.array([x_morph, y_morph]).T,np.array([x_target, y_target]).T,
funcy=(linear_function,{'scale': 1.2, 'offset': 0.1}))

# 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

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:
5. Extract 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"]
fitted_params = morph_params["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.
for analyzing your data.
78 changes: 78 additions & 0 deletions doc/source/tutorials.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,84 @@ selected directory and plot resulting :math:`R_w` values from each morph.
PDFs. See the ``--save-names-file`` option to see how you can set
the names for these saved morphs!

Polynomial Squeeze Morph
=========================

Another advanced feature in ``diffpy.morph`` is the ``MorphSqueeze`` morph,
which applies a user-defined polynomial to squeeze the morph function along the
x-axis. This provides a flexible way to correct for higher-order distortions
that simple shift or stretch morphs cannot fully address.
Such distortions can arise from geometric artifacts in X-ray detector modules,
including tilts, curved detection planes, or angle-dependent offsets, as well
as from intrinsic structural effects in the sample.

A first-order squeeze polynomial recovers the behavior of simple shift or stretch,
while higher-order terms enable non-linear corrections. The squeeze transformation
is defined as:

.. math::

\Delta r(r) = a_0 + a_1 r + a_2 r^2 + \dots + a_n r^n

where :math:`a_0, a_1, ..., a_n` are the polynomial coefficients defined by the user.

In this example, we show how to apply a squeeze morph in combination
with a scale morph to match a morph function to its target. The required
files can be found in ``additionalData/morphsqueeze/``.

1. ``cd`` into the ``morphsqueeze`` directory::

cd additionalData/morphsqueeze

Here you will find:

- ``squeeze_morph.cgr`` — the morph function with a small built-in polynomial distortion.
- ``squeeze_target.cgr`` — the target function.

2. Suppose we know that the morph needs a quadratic and cubic squeeze,
plus a scale factor to best match the target. As an initial guess,
we can use:

- ``squeeze = 0,-0.001,-0.0001,0.0001``
(for a polynomial: :math:`a_0 + a_1 x + a_2 x^2 + a_3 x^3`)
- ``scale = 1.1``

The squeeze polynomial is provided as a comma-separated list (no spaces)::

diffpy.morph --scale=1.1 --squeeze=0,-0.001,-0.0001,0.0001 -a squeeze_morph.cgr squeeze_target.cgr

3. ``diffpy.morph`` will apply the polynomial squeeze and scale,
display the initial and refined coefficients, and show the final
difference ``Rw``.

To refine the squeeze polynomial and scale automatically, remove
the ``-a`` tag if you used it. For example::

diffpy.morph --scale=1.1 --squeeze=0,-0.001,-0.0001,0.0001 squeeze_morph.cgr squeeze_target.cgr

4. Check the output for the final squeeze polynomial coefficients and scale.
They should match the true values used to generate the test data:

- ``squeeze = 0, 0.01, 0.0001, 0.001``
Copy link
Collaborator

Choose a reason for hiding this comment

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

What will display is formatted like:

squeeze a0 = 0
squeeze a1 = 0.01
squeeze a2 = 0.0001
squeeze a3 = 0.001

- ``scale = 0.5``

``diffpy.morph`` refines the coefficients to minimize the residual
between the squeezed, scaled morph function and the target.

.. warning::

**Extrapolation risk:**
A polynomial squeeze can shift morph data outside the target’s ``r``-range,
so parts of the output may be extrapolated.
This is generally fine if the polynomial coefficients are small and
the distortion is therefore small. If your coefficients are large, check the
plots carefully — strong extrapolation can produce unrealistic features at
the edges. If needed, adjust the coefficients to keep the morph physically
meaningful.

Experiment with your own squeeze polynomials to fine-tune your morphs — even
small higher-order corrections can make a big difference!

Nanoparticle Shape Effects
==========================

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

* Added tutorial for MorphSqueeze and MorphFuncy

**Changed:**

* <news item>

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
28 changes: 14 additions & 14 deletions src/diffpy/morph/morphs/morphfuncy.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,33 +30,33 @@ class MorphFuncy(Morph):
-------
Import the funcy morph function:

>>> from diffpy.morph.morphs.morphfuncy import MorphFuncy
>>> 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
>>> def sine_function(x, y, amplitude, frequency):
>>> return amplitude * np.sin(frequency * x) * y

Provide initial guess for parameters:

>>> parameters = {'amplitude': 2, 'frequency': 2}
>>> 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)
>>> 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
>>> 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
Expand Down
22 changes: 11 additions & 11 deletions src/diffpy/morph/morphs/morphsqueeze.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,27 +32,27 @@ class MorphSqueeze(Morph):
-------
Import the squeeze morph function:

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

Provide initial guess for squeezing coefficients:

>>> squeeze_coeff = {"a0":0.1, "a1":-0.01, "a2":0.005}
>>> 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)
>>> 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
>>> 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 Down
Binary file modified tutorial/additionalData.zip
Binary file not shown.
Loading