diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 54c68c5a..388e32ad 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -401,6 +401,85 @@ 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 +through Python scripting. + + 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: + + .. 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: + + .. 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 morph configuration dictionary. 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_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: + + .. 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"] + 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. + Bug Reports =========== 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:** + +* diff --git a/src/diffpy/morph/morphs/morphfuncy.py b/src/diffpy/morph/morphs/morphfuncy.py index 98360251..5b633416 100644 --- a/src/diffpy/morph/morphs/morphfuncy.py +++ b/src/diffpy/morph/morphs/morphfuncy.py @@ -1,9 +1,63 @@ +"""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.""" + """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 + ----------------------- + 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. + + 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,54 +68,8 @@ class MorphFuncy(Morph): parnames = ["function", "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( 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 6e41fc3d..146ce280 100644 --- a/src/diffpy/morph/morphs/morphsqueeze.py +++ b/src/diffpy/morph/morphs/morphsqueeze.py @@ -1,3 +1,6 @@ +"""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,9 +9,50 @@ class MorphSqueeze(Morph): - """Apply a polynomial to squeeze the morph function. + """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 - The morphed data is returned on the same grid as the unmorphed data. + 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 @@ -24,43 +68,10 @@ 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)