From ed382bbf23601f24dc1e6d5897c85e4125a5c0da Mon Sep 17 00:00:00 2001 From: Luis Kitsu Iglesias Date: Fri, 20 Jun 2025 16:54:41 +0200 Subject: [PATCH 1/4] doc/tutorial: adding a tutorial for MorphFuncy and proper docstring --- doc/source/quickstart.rst | 61 ++++++++++++++ src/diffpy/morph/morphs/morphfuncy.py | 105 +++++++++++++----------- src/diffpy/morph/morphs/morphsqueeze.py | 89 +++++++++++--------- 3 files changed, 166 insertions(+), 89 deletions(-) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 0164ce20..e3d69302 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -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 :: + + 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 :: + + 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}) + cfg["function"] = linear_function + + 5. Run the morph using the API function ``morph(...)``. This will apply the + 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) + + 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"]) + 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 =========== diff --git a/src/diffpy/morph/morphs/morphfuncy.py b/src/diffpy/morph/morphs/morphfuncy.py index e0b5fe7f..a6e756a5 100644 --- a/src/diffpy/morph/morphs/morphfuncy.py +++ b/src/diffpy/morph/morphs/morphfuncy.py @@ -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 + 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 + """ # Define input output types summary = "Apply a Python function to the y-axis data" @@ -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( diff --git a/src/diffpy/morph/morphs/morphsqueeze.py b/src/diffpy/morph/morphs/morphsqueeze.py index fc6493d6..ba2b20d0 100644 --- a/src/diffpy/morph/morphs/morphsqueeze.py +++ b/src/diffpy/morph/morphs/morphsqueeze.py @@ -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 @@ -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" @@ -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))] From a7431d98183b21dcbbf8a5a651bef3c68e6483cb Mon Sep 17 00:00:00 2001 From: Luis Kitsu Iglesias Date: Fri, 20 Jun 2025 17:03:42 +0200 Subject: [PATCH 2/4] doc/tutorial: forgot the news --- news/tutorial_morphfuncy.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 news/tutorial_morphfuncy.rst diff --git a/news/tutorial_morphfuncy.rst b/news/tutorial_morphfuncy.rst new file mode 100644 index 00000000..e50900f3 --- /dev/null +++ b/news/tutorial_morphfuncy.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added a tutorial for MorphFuncy + +**Changed:** + +* Changed docstrings location for MorphFuncy and MorphSqueeze + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* From 77d987b5839b3dc9de08aaff1f2e00332509f6db Mon Sep 17 00:00:00 2001 From: Luis Kitsu Iglesias Date: Thu, 26 Jun 2025 12:22:10 +0200 Subject: [PATCH 3/4] doc/tutorial: adding morphfuncy tutorial and doc --- doc/source/quickstart.rst | 64 +++++++++++++++++---------- src/diffpy/morph/morphs/morphfuncy.py | 5 ++- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index e3d69302..0f6de504 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -407,56 +407,74 @@ 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. +through Python scripting. - 1. Import the necessary modules into your Python script :: + 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 :: + 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 :: + 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 configuration dictionary. This includes both the + 4. Set up the morph configuration dictionary. This includes both the transformation parameters (our initial guess) and the transformation - function itself :: + 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: - cfg = morph_default_config(funcy={"scale": 1.2, "offset": 0.1}) - cfg["function"] = linear_function + .. code-block:: python - 5. Run the morph using the API function ``morph(...)``. This will apply the - user-defined function and refine the parameters to best align the morph data - with the target data :: + morph_result = morph(x_morph, y_morph, x_target, y_target, **morph_config) - morph_rv = morph(x_morph, y_morph, x_target, y_target, **cfg) + 6. Extract the morphed output and the fitted parameters from the result: - 6. Extract the morphed output and the fitted parameters from the result :: + .. code-block:: python - morphed_cfg = morph_rv["morphed_config"] - x_morph_out, y_morph_out, x_target_out, y_target_out = morph_rv["morph_chain"].xyallout + fitted_config = morph_result["morphed_config"] + x_morph_out, y_morph_out, x_target_out, y_target_out = morph_result["morph_chain"].xyallout - fitted_parameters = morphed_cfg["funcy"] - print("Fitted scale:", fitted_parameters["scale"]) - print("Fitted offset:", fitted_parameters["offset"]) + 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. +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 =========== diff --git a/src/diffpy/morph/morphs/morphfuncy.py b/src/diffpy/morph/morphs/morphfuncy.py index a6e756a5..7aea2a77 100644 --- a/src/diffpy/morph/morphs/morphfuncy.py +++ b/src/diffpy/morph/morphs/morphfuncy.py @@ -4,7 +4,9 @@ class MorphFuncy(Morph): - """General morph function that applies a user-supplied function to the + """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 @@ -15,7 +17,6 @@ class MorphFuncy(Morph): parameters: dict A dictionary of parameters to pass to the function. - These parameters are unpacked using **kwargs. Returns ------- From d535e4a5916ab56d7761c17cc85c35996a63c1b8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 07:52:36 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit hooks --- src/diffpy/morph/morphs/morphfuncy.py | 7 ++++--- src/diffpy/morph/morphs/morphsqueeze.py | 10 +++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/diffpy/morph/morphs/morphfuncy.py b/src/diffpy/morph/morphs/morphfuncy.py index a8bf0c92..5b633416 100644 --- a/src/diffpy/morph/morphs/morphfuncy.py +++ b/src/diffpy/morph/morphs/morphfuncy.py @@ -1,4 +1,5 @@ -"""class MorphFuncy -- apply a user-supplied python function to the y-axis.""" +"""Class MorphFuncy -- apply a user-supplied python function to the +y-axis.""" from diffpy.morph.morphs.morph import LABEL_GR, LABEL_RA, Morph @@ -67,8 +68,8 @@ class MorphFuncy(Morph): parnames = ["function", "funcy"] def morph(self, x_morph, y_morph, x_target, y_target): - """Apply the user-supplied Python function to the y-coordinates of the - morph data""" + """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 diff --git a/src/diffpy/morph/morphs/morphsqueeze.py b/src/diffpy/morph/morphs/morphsqueeze.py index ba2b20d0..146ce280 100644 --- a/src/diffpy/morph/morphs/morphsqueeze.py +++ b/src/diffpy/morph/morphs/morphsqueeze.py @@ -1,4 +1,5 @@ -"""class MorphSqueeze -- Apply a polynomial to squeeze the morph function.""" +"""Class MorphSqueeze -- Apply a polynomial to squeeze the morph +function.""" import numpy as np from numpy.polynomial import Polynomial @@ -67,8 +68,11 @@ class MorphSqueeze(Morph): extrap_index_high = None def morph(self, x_morph, y_morph, x_target, y_target): - """Apply a polynomial to squeeze the morph function. The morphed - data is returned on the same grid as the unmorphed data.""" + """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))]