Skip to content

Commit 784b3df

Browse files
doc/tutorial: adding a tutorial for MorphFuncy and proper docstring (#215)
* doc/tutorial: adding a tutorial for MorphFuncy and proper docstring * doc/tutorial: forgot the news * doc/tutorial: adding morphfuncy tutorial and doc * [pre-commit.ci] auto fixes from pre-commit hooks --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 4b832d4 commit 784b3df

File tree

4 files changed

+210
-89
lines changed

4 files changed

+210
-89
lines changed

doc/source/quickstart.rst

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,85 @@ There is also support for morphing from a nanoparticle to a bulk. When
401401
applying the inverse morphs, it is recommended to set ``--rmax=psize``
402402
where ``psize`` is the longest diameter of the nanoparticle.
403403

404+
MorphFuncy: Applying custom functions
405+
-------------------------------------
406+
407+
The ``MorphFuncy`` morph allows users to apply a custom Python function
408+
to the y-axis values of a dataset, enabling flexible and user-defined
409+
transformations.
410+
411+
In this tutorial, we walk through how to use ``MorphFuncy`` with an example
412+
transformation. Unlike other morphs that can be run from the command line,
413+
``MorphFuncy`` requires a Python function and is therefore intended to be used
414+
through Python scripting.
415+
416+
1. Import the necessary modules into your Python script:
417+
418+
.. code-block:: python
419+
420+
from diffpy.morph.morph_api import morph, morph_default_config
421+
import numpy as np
422+
423+
2. Define a custom Python function to apply a transformation to the data.
424+
The function must take ``x`` and ``y`` (1D arrays of the same length)
425+
along with named parameters, and return a transformed ``y`` array of the
426+
same length.
427+
For this example, we will use a simple linear transformation that
428+
scales the input and applies an offset:
429+
430+
.. code-block:: python
431+
432+
def linear_function(x, y, scale, offset):
433+
return (scale * x) * y + offset
434+
435+
3. In this example, we use a sine function for the morph data and generate
436+
the target data by applying the linear transformation with known scale
437+
and offset to it:
438+
439+
.. code-block:: python
440+
441+
x_morph = np.linspace(0, 10, 101)
442+
y_morph = np.sin(x_morph)
443+
x_target = x_morph.copy()
444+
y_target = np.sin(x_target) * 20 * x_target + 0.8
445+
446+
4. Set up the morph configuration dictionary. This includes both the
447+
transformation parameters (our initial guess) and the transformation
448+
function itself:
449+
450+
.. code-block:: python
451+
452+
morph_config = morph_default_config(funcy={"scale": 1.2, "offset": 0.1})
453+
morph_config["function"] = linear_function
454+
455+
# morph_config now contains:
456+
# {'funcy': {'scale': 1.2, 'offset': 0.1}, 'function': linear_function}
457+
458+
5. Run the morph using the ``morph(...)``. This will apply the user-defined
459+
function and refine the parameters to best align the morph data
460+
with the target data:
461+
462+
.. code-block:: python
463+
464+
morph_result = morph(x_morph, y_morph, x_target, y_target, **morph_config)
465+
466+
6. Extract the morphed output and the fitted parameters from the result:
467+
468+
.. code-block:: python
469+
470+
fitted_config = morph_result["morphed_config"]
471+
x_morph_out, y_morph_out, x_target_out, y_target_out = morph_result["morph_chain"].xyallout
472+
473+
fitted_params = fitted_config["funcy"]
474+
print(f"Fitted scale: {fitted_params['scale']}")
475+
print(f"Fitted offset: {fitted_params['offset']}")
476+
477+
As you can see, the fitted scale and offset values match the ones used
478+
to generate the target (scale=20 & offset=0.8). This example shows how
479+
``MorphFuncy`` can be used to fit and apply custom transformations. Now
480+
it's your turn to experiment with other custom functions that may be useful
481+
for analyzing your data.
482+
404483
Bug Reports
405484
===========
406485

news/tutorial_morphfuncy.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
**Added:**
2+
3+
* Added a tutorial for MorphFuncy
4+
5+
**Changed:**
6+
7+
* Changed docstrings location for MorphFuncy and MorphSqueeze
8+
9+
**Deprecated:**
10+
11+
* <news item>
12+
13+
**Removed:**
14+
15+
* <news item>
16+
17+
**Fixed:**
18+
19+
* <news item>
20+
21+
**Security:**
22+
23+
* <news item>

src/diffpy/morph/morphs/morphfuncy.py

Lines changed: 58 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,63 @@
1+
"""Class MorphFuncy -- apply a user-supplied python function to the
2+
y-axis."""
3+
14
from diffpy.morph.morphs.morph import LABEL_GR, LABEL_RA, Morph
25

36

47
class MorphFuncy(Morph):
5-
"""Apply the user-supplied Python function to the y-coordinates of
6-
the morph data."""
8+
"""Apply a custom function to the y-axis of the morph function.
9+
10+
General morph function that applies a user-supplied function to the
11+
y-coordinates of morph data to make it align with a target.
12+
13+
Configuration Variables
14+
-----------------------
15+
function: callable
16+
The user-supplied function that applies a transformation to the
17+
y-coordinates of the data.
18+
19+
parameters: dict
20+
A dictionary of parameters to pass to the function.
21+
22+
Returns
23+
-------
24+
A tuple (x_morph_out, y_morph_out, x_target_out, y_target_out)
25+
where the target values remain the same and the morph data is
26+
transformed according to the user-specified function and parameters
27+
The morphed data is returned on the same grid as the unmorphed data
28+
29+
Example
30+
-------
31+
Import the funcy morph function:
32+
33+
>>> from diffpy.morph.morphs.morphfuncy import MorphFuncy
34+
35+
Define or import the user-supplied transformation function:
36+
37+
>>> def sine_function(x, y, amplitude, frequency):
38+
>>> return amplitude * np.sin(frequency * x) * y
39+
40+
Provide initial guess for parameters:
41+
42+
>>> parameters = {'amplitude': 2, 'frequency': 2}
43+
44+
Run the funcy morph given input morph array (x_morph, y_morph)and target
45+
array (x_target, y_target):
46+
47+
>>> morph = MorphFuncy()
48+
>>> morph.function = sine_function
49+
>>> morph.funcy = parameters
50+
>>> x_morph_out, y_morph_out, x_target_out, y_target_out =
51+
... morph.morph(x_morph, y_morph, x_target, y_target)
52+
53+
To access parameters from the morph instance:
54+
55+
>>> x_morph_in = morph.x_morph_in
56+
>>> y_morph_in = morph.y_morph_in
57+
>>> x_target_in = morph.x_target_in
58+
>>> y_target_in = morph.y_target_in
59+
>>> parameters_out = morph.funcy
60+
"""
761

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

1670
def morph(self, x_morph, y_morph, x_target, y_target):
17-
"""General morph function that applies a user-supplied function
18-
to the y-coordinates of morph data to make it align with a
19-
target.
20-
21-
Configuration Variables
22-
-----------------------
23-
function: callable
24-
The user-supplied function that applies a transformation to the
25-
y-coordinates of the data.
26-
27-
parameters: dict
28-
A dictionary of parameters to pass to the function.
29-
These parameters are unpacked using **kwargs.
30-
31-
Returns
32-
-------
33-
A tuple (x_morph_out, y_morph_out, x_target_out, y_target_out)
34-
where the target values remain the same and the morph data is
35-
transformed according to the user-specified function and parameters
36-
The morphed data is returned on the same grid as the unmorphed data
37-
38-
Example
39-
-------
40-
Import the funcy morph function:
41-
>>> from diffpy.morph.morphs.morphfuncy import MorphFuncy
42-
43-
Define or import the user-supplied transformation function:
44-
>>> def sine_function(x, y, amplitude, frequency):
45-
>>> return amplitude * np.sin(frequency * x) * y
46-
47-
Provide initial guess for parameters:
48-
>>> parameters = {'amplitude': 2, 'frequency': 2}
49-
50-
Run the funcy morph given input morph array (x_morph, y_morph)
51-
and target array (x_target, y_target):
52-
>>> morph = MorphFuncy()
53-
>>> morph.function = sine_function
54-
>>> morph.funcy = parameters
55-
>>> x_morph_out, y_morph_out, x_target_out, y_target_out = morph.morph(
56-
... x_morph, y_morph, x_target, y_target)
57-
58-
To access parameters from the morph instance:
59-
>>> x_morph_in = morph.x_morph_in
60-
>>> y_morph_in = morph.y_morph_in
61-
>>> x_target_in = morph.x_target_in
62-
>>> y_target_in = morph.y_target_in
63-
>>> parameters_out = morph.funcy
64-
"""
71+
"""Apply the user-supplied Python function to the y-coordinates
72+
of the morph data."""
6573
Morph.morph(self, x_morph, y_morph, x_target, y_target)
6674
self.y_morph_out = self.function(
6775
self.x_morph_in, self.y_morph_in, **self.funcy

src/diffpy/morph/morphs/morphsqueeze.py

Lines changed: 50 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
"""Class MorphSqueeze -- Apply a polynomial to squeeze the morph
2+
function."""
3+
14
import numpy as np
25
from numpy.polynomial import Polynomial
36
from scipy.interpolate import CubicSpline
@@ -6,9 +9,50 @@
69

710

811
class MorphSqueeze(Morph):
9-
"""Apply a polynomial to squeeze the morph function.
12+
"""Squeeze the morph function.
13+
14+
This applies a polynomial to squeeze the morph non-linearly.
15+
16+
Configuration Variables
17+
-----------------------
18+
squeeze : Dictionary
19+
The polynomial coefficients {a0, a1, ..., an} for the squeeze
20+
function where the polynomial would be of the form
21+
a0 + a1*x + a2*x^2 and so on. The order of the polynomial is
22+
determined by the length of the dictionary.
23+
24+
Returns
25+
-------
26+
A tuple (x_morph_out, y_morph_out, x_target_out, y_target_out)
27+
where the target values remain the same and the morph data is
28+
shifted according to the squeeze. The morphed data is returned on
29+
the same grid as the unmorphed data.
30+
31+
Example
32+
-------
33+
Import the squeeze morph function:
34+
35+
>>> from diffpy.morph.morphs.morphsqueeze import MorphSqueeze
1036
11-
The morphed data is returned on the same grid as the unmorphed data.
37+
Provide initial guess for squeezing coefficients:
38+
39+
>>> squeeze_coeff = {"a0":0.1, "a1":-0.01, "a2":0.005}
40+
41+
Run the squeeze morph given input morph array (x_morph, y_morph) and target
42+
array (x_target, y_target):
43+
44+
>>> morph = MorphSqueeze()
45+
>>> morph.squeeze = squeeze_coeff
46+
>>> x_morph_out, y_morph_out, x_target_out, y_target_out =
47+
... morph(x_morph, y_morph, x_target, y_target)
48+
49+
To access parameters from the morph instance:
50+
51+
>>> x_morph_in = morph.x_morph_in
52+
>>> y_morph_in = morph.y_morph_in
53+
>>> x_target_in = morph.x_target_in
54+
>>> y_target_in = morph.y_target_in
55+
>>> squeeze_coeff_out = morph.squeeze
1256
"""
1357

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

2670
def morph(self, x_morph, y_morph, x_target, y_target):
27-
"""Squeeze the morph function.
28-
29-
This applies a polynomial to squeeze the morph non-linearly.
30-
31-
Configuration Variables
32-
-----------------------
33-
squeeze : Dictionary
34-
The polynomial coefficients {a0, a1, ..., an} for the squeeze
35-
function where the polynomial would be of the form
36-
a0 + a1*x + a2*x^2 and so on. The order of the polynomial is
37-
determined by the length of the dictionary.
38-
39-
Returns
40-
-------
41-
A tuple (x_morph_out, y_morph_out, x_target_out, y_target_out)
42-
where the target values remain the same and the morph data is
43-
shifted according to the squeeze. The morphed data is returned on
44-
the same grid as the unmorphed data.
45-
46-
Example
47-
-------
48-
Import the squeeze morph function:
49-
>>> from diffpy.morph.morphs.morphsqueeze import MorphSqueeze
50-
Provide initial guess for squeezing coefficients:
51-
>>> squeeze_coeff = {"a0":0.1, "a1":-0.01, "a2":0.005}
52-
Run the squeeze morph given input morph array (x_morph, y_morph)
53-
and target array (x_target, y_target):
54-
>>> morph = MorphSqueeze()
55-
>>> morph.squeeze = squeeze_coeff
56-
>>> x_morph_out, y_morph_out, x_target_out, y_target_out = morph(
57-
... x_morph, y_morph, x_target, y_target)
58-
To access parameters from the morph instance:
59-
>>> x_morph_in = morph.x_morph_in
60-
>>> y_morph_in = morph.y_morph_in
61-
>>> x_target_in = morph.x_target_in
62-
>>> y_target_in = morph.y_target_in
63-
>>> squeeze_coeff_out = morph.squeeze
71+
"""Apply a polynomial to squeeze the morph function.
72+
73+
The morphed data is returned on the same grid as the unmorphed
74+
data.
6475
"""
6576
Morph.morph(self, x_morph, y_morph, x_target, y_target)
6677

0 commit comments

Comments
 (0)