Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
61 changes: 61 additions & 0 deletions doc/source/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,67 @@ 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
within the Python API.

1. Import the necessary modules into your Python script ::
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is copied from the other docs but it is not the preferred pattern. At least for new docs (not sure if we want to fix all the old code blocks, but maybe?) It looks better if:

   1. Import the necessary modules into your Python script ::

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

goes to

   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

I am not 100% sure about the indentation of the .. code-block and there are requirements for blank lines here and thee, but you can figure it out so it builds correctly it will be color coded in the built documentation.


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.
For this example, we will use a simple linear transformation that
scales the input and applies an offset ::
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe explicitly specify requirements: x and y must be vectors of the same length, parameters are passed in, transformed x and y vectors are returned...etc. I am making this up, but having this in the docs is important I think.


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 ::

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 configuration dictionary. This includes both the
transformation parameters (our initial guess) and the transformation
function itself ::

cfg = morph_default_config(funcy={"scale": 1.2, "offset": 0.1})
Copy link
Contributor

Choose a reason for hiding this comment

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

can we model the use of better variable names?

cfg["function"] = linear_function

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

5. Run the morph using the API function ``morph(...)``. This will apply the
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 avoid using Computer science jargon such as "API function" I think it is more accessible to people if you just tell them what to do and show an example. It really doesn't matter if morph() as an API function or some other function. For example, math.sin() is an API function but I never see documentation that says "calculate the sine using the API function sin()"

user-defined function and refine the parameters to best align the morph data
with the target data ::

morph_rv = morph(x_morph, y_morph, x_target, y_target, **cfg)
Copy link
Contributor

Choose a reason for hiding this comment

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

what is rv? again, longer variable names that are more descriptive here are probably better.


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

morphed_cfg = morph_rv["morphed_config"]
x_morph_out, y_morph_out, x_target_out, y_target_out = morph_rv["morph_chain"].xyallout

fitted_parameters = morphed_cfg["funcy"]
print("Fitted scale:", fitted_parameters["scale"])
Copy link
Contributor

Choose a reason for hiding this comment

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

let's model using f-strings here for the print

print("Fitted offset:", fitted_parameters["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>
105 changes: 56 additions & 49 deletions src/diffpy/morph/morphs/morphfuncy.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,61 @@
"""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"""
"""General morph function that applies a user-supplied function to the
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs one high level statement of <=72 characters, then blank line, then a longer description

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.
Copy link
Contributor

Choose a reason for hiding this comment

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

This line not needed. User doesn't care how the program works, just how to use it.


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,53 +66,8 @@ class MorphFuncy(Morph):
parnames = ["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(
Expand Down
89 changes: 49 additions & 40 deletions src/diffpy/morph/morphs/morphsqueeze.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""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,8 +8,51 @@


class MorphSqueeze(Morph):
"""Apply a polynomial to squeeze the morph function. The morphed
data is returned on the same grid as the unmorphed data."""
"""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
"""

# Define input output types
summary = "Squeeze morph by polynomial shift"
Expand All @@ -22,44 +67,8 @@ 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)

coeffs = [self.squeeze[f"a{i}"] for i in range(len(self.squeeze))]
Expand Down