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

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from
Draft
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
196 changes: 177 additions & 19 deletions docs/source/morphpy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -105,19 +105,26 @@ apply: bool
exclude: str
Exclude a manipulation from refinement by name.
scale: float
Apply scale factor. This multiplies the function ordinate by scale.
Apply scale factor.

This multiplies the function ordinate by scale.
stretch: float
Stretch function grid by a fraction stretch. Specifically, this multiplies the function grid by 1+stretch.
Stretch function grid by a fraction stretch.

This multiplies the function grid by 1+stretch.
squeeze: list of float
Squeeze function grid given a polynomial
p(x) = squeeze[0]+squeeze[1]*x+...+squeeze[n]*x^n. n is dependent on the number
p(x) = squeeze[0]+squeeze[1]*x+...+squeeze[n]*x^n.

n is dependent on the number
of values in the user-inputted comma-separated list.
The morph transforms the function grid from x to x+p(x).
When this parameter is given, hshift is disabled.
When n>1, stretch is disabled.
smear: float
Smear the peaks with a Gaussian of width smear. This
is done by convolving the function with a Gaussian
Smear the peaks with a Gaussian of width smear.

This is done by convolving the function with a Gaussian
with standard deviation smear. If both smear and
smear_pdf are used, only smear_pdf will be
applied.
Expand All @@ -128,6 +135,7 @@ smear_pdf: float
applied.
slope: float
Slope of the baseline used in converting from PDF to RDF.

This is used with the option smear_pdf. The slope will
be estimated if not provided.
hshift: float
Expand All @@ -138,57 +146,125 @@ qdamp: float
Dampen PDF by a factor qdamp.
radius: float
Apply characteristic function of sphere with radius
given by parameter radius. If pradius is also specified, instead apply
given by parameter radius.

If pradius is also specified, instead apply
characteristic function of spheroid with equatorial
radius radius and polar radius pradius.
pradius: float
Apply characteristic function of spheroid with
equatorial radius given by above parameter radius and polar radius pradius.

If only pradius is specified, instead apply
characteristic function of sphere with radius pradius.
iradius: float
Apply inverse characteristic function of sphere with
radius iradius. If ipradius is also specified, instead
radius iradius.

If ipradius is also specified, instead
apply inverse characteristic function of spheroid with
equatorial radius iradius and polar radius ipradius.
ipradius: float
Apply inverse characteristic function of spheroid with
equatorial radius iradius and polar radius ipradius.

If only ipradius is specified, instead apply inverse
characteristic function of sphere with radius ipradius.
funcy: tuple (function, dict)
Apply a function to the y-axis of the (two-column) data.

This morph applies the function funcy[0] with parameters given in funcy[1].
The function funcy[0] must be a function of both the abscissa and ordinate
(e.g. take in at least two inputs with as many additional parameters as needed).
The function funcy[0] take in as parameters both the abscissa and ordinate
(i.e. take in at least two inputs with as many additional parameters as needed).
The y-axis values of the data are then replaced by the return value of funcy[0].

For example, let's start with a two-column table with abscissa x and ordinate y.
let us say we want to apply the function ::

def linear(x, y, a, b, c):
return a * x + b * y + c

This function takes in both the abscissa and ordinate on top of three additional
parameters a, b, and c. To use the funcy parameter with initial guesses
a=1.0, b=2.0, c=3.0, we would pass ``funcy=(linear, {a: 1.0, b: 2.0, c: 3.0})``.
For an example use-case, see the Python-Specific Morphs section below.
This example function above takes in both the abscissa and ordinate on top of
three additional parameters a, b, and c.
To use the funcy parameter with parameter values a=1.0, b=2.0, and c=3.0,
we would pass ``funcy=(linear, {"a": 1.0, "b": 2.0, "c": 3.0})``.
For an explicit example, see the Python-Specific Morphs section below.
funcx: tuple (function, dict)
Apply a function to the x-axis of the (two-column) data.

This morph works fundamentally differently from the other grid morphs
(e.g. stretch and squeeze) as it directly modifies the grid of the
morph function.
The other morphs maintain the original grid and apply the morphs by interpolating
the function ***.

This morph applies the function funcx[0] with parameters given in funcx[1].
The function funcx[0] take in as parameters both the abscissa and ordinate
(i.e. take in at least two inputs with as many additional parameters as needed).
The x-axis values of the data are then replaced by the return value of funcx[0].
Note that diffpy.morph requires the x-axis be monotonic increasing
(i.e. for i < j, x[i] < x[j]): as such,
if funcx[0] is not a monotonic increasing function of the provided x-axis data,
the error ``x must be a strictly increasing sequence`` will be thrown.

For example, let's start with a two-column table with abscissa x and ordinate y.
let us say we want to apply the function ::

def exponential(x, y, amp, decay):
return abs(amp) * (1 - 2**(-decay * x))

This example function above takes in both the abscissa and ordinate on top of
three additional parameters amp and decay.
(Even though the ordinate is not used in the function,
it is still required that the function take in both acscissa and ordinate.)
To use the funcx parameter with parameter values amp=1.0 and decay=2.0,
we would pass ``funcx=(exponential, {"amp": 1.0, "decay:: 2.0})``.
For an explicit example, see the Python-Specific Morphs section below.
funcxy: tuple (function, dict)
Apply a function the (two-column) data.

This morph applies the function funcxy[0] with parameters given in funcxy[1].
The function funcxy[0] take in as parameters both the abscissa and ordinate
(i.e. take in at least two inputs with as many additional parameters as needed).
The two columns of the data are then replaced by the two return values of funcxy[0].

For example, let's start with a two-column table with abscissa x and ordinate y.
let us say we want to apply the function ::

def shift(x, y, hshift, vshift):
return x + hshift, y + vshift

This example function above takes in both the abscissa and ordinate on top of
two additional parameters hshift and vshift.
To use the funcy parameter with parameter values hshift=1.0 and vshift=2.0,
we would pass ``funcy=(shift, {"hshift": 1.0, "vshift": 1.0})``.
For an example use-case, see the Python-Specific Morphs section below.

Python-Specific Morphs
======================

Some morphs in ``diffpy.morph`` are supported only in Python. Here, we detail
how they are used and how to call them.

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

In these tutorial, we walk through how to use the ``MorphFunc`` morphs
(``MorphFuncy``, ``MorphFuncx``, ``MorphFuncxy``)
with some example transformations.

Unlike other morphs that can be run from the command line,
``MorphFunc`` moprhs require a Python function and is therefore
intended to be used through Python scripting.

MorphFuncy:
^^^^^^^^^^^

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.
Let's try out this morph!

1. Import the necessary modules into your Python script:

Expand Down Expand Up @@ -229,7 +305,7 @@ through Python scripting.

.. code-block:: python

morph_params, morph_table = morph_arrays(np.array([x_morph, y_morph]).T,np.array([x_target, y_target]).T,
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}))

5. Extract the fitted parameters from the result:
Expand All @@ -245,3 +321,85 @@ 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.

MorphFuncx:
^^^^^^^^^^^

The ``MorphFuncx`` morph allows users to apply a custom Python function
to the x-axis values of a dataset, similar to the ``MorphFuncy`` morph.

One caveat to this morph is that the x-axis values must remain monotonic
increasing, so it is possible to run into errors when applying this morph.
For example, if your initial grid is ``[-1, 0, 1]``, and your function is
``lambda x, y: x**2``, the grid after the function is applied will be
``[1, 0, 1]``, which is no longer monotonic increasing.
In this case, the error ``x must be a strictly increasing sequence``
will be thrown.

Let's try out this morph!

1. Import the necessary modules into your Python script:

.. code-block:: python

from diffpy.morph.morphpy import morph_arrays
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 ``x`` array of the
same length. Recall that this function must maintain the monotonic
increasing nature of the ``x`` array.

For this example, we will use a simple exponential function transformation that
greatly modifies the input:

.. code-block:: python

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

Notice that, though the function only uses the ``x`` input,
the function signature takes in both ``x`` and ``y``.

3. Like in the previous example, we will use a sine function for the morph
data and generate the target data by applying the decay transfomration
with a known scale and rate:

.. code-block:: python

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

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_params, morph_table = morph_arrays(np.array([x_morph, y_morph]).T, np.array([x_target, y_target]).T,
funcx=(decay_function, {'scale': 1.2, 'rate': 1.0}))

5. Extract the fitted parameters from the result:

.. code-block:: python

fitted_params = morph_params["funcx"]
print(f"Fitted scale: {fitted_params['scale']}")
print(f"Fitted rate: {fitted_params['rate']}")

Again, we should see that the fitted scale and offset values match the ones used
to generate the target (scale=20 & rate=0.8).

For fun, you can plot the original function to the morphed function to see
how much the

MorphFuncxy:
^^^^^^^^^^^^
The ``MorphFuncxy`` morph allows users to apply a custom Python function
to a dataset, ***.
23 changes: 23 additions & 0 deletions news/exclude.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
**Added:**

* <news item>

**Changed:**

* Exclude option in morphpy now takes in a list of morphs to exclude rather than excluding a single morph.

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
23 changes: 23 additions & 0 deletions news/kwargs.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
**Added:**

* <news item>

**Changed:**

* <news item>

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* Docstring for kwargs updated from dict to Any since various input types from kwargs are possible (e.g. a list/tuple for squeeze, a tuple for funcy, a float for smear-pdf). This should remove the warning that inputs are not dicts.

**Security:**

* <news item>
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
Loading
Loading