diff --git a/doc/source/morphpy.rst b/doc/source/morphpy.rst index 4aeb5386..30bd5c99 100644 --- a/doc/source/morphpy.rst +++ b/doc/source/morphpy.rst @@ -189,7 +189,7 @@ through Python scripting. .. code-block:: python - from diffpy.morph.morph_api import morph, morph_default_config + from diffpy.morph.morphpy import morph_arrays import numpy as np 2. Define a custom Python function to apply a transformation to the data. @@ -215,34 +215,23 @@ through Python scripting. 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: + 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_config = morph_default_config(funcy={"scale": 1.2, "offset": 0.1}) - morph_config["function"] = linear_function + 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})) - # 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: + 5. Extract 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"] + fitted_params = morph_params["funcy"] print(f"Fitted scale: {fitted_params['scale']}") print(f"Fitted offset: {fitted_params['offset']}") @@ -250,4 +239,4 @@ 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. +for analyzing your data. diff --git a/doc/source/tutorials.rst b/doc/source/tutorials.rst index 218b38dd..dd7ad63a 100644 --- a/doc/source/tutorials.rst +++ b/doc/source/tutorials.rst @@ -106,6 +106,84 @@ selected directory and plot resulting :math:`R_w` values from each morph. PDFs. See the ``--save-names-file`` option to see how you can set the names for these saved morphs! +Polynomial Squeeze Morph +========================= + +Another advanced feature in ``diffpy.morph`` is the ``MorphSqueeze`` morph, +which applies a user-defined polynomial to squeeze the morph function along the +x-axis. This provides a flexible way to correct for higher-order distortions +that simple shift or stretch morphs cannot fully address. +Such distortions can arise from geometric artifacts in X-ray detector modules, +including tilts, curved detection planes, or angle-dependent offsets, as well +as from intrinsic structural effects in the sample. + +A first-order squeeze polynomial recovers the behavior of simple shift or stretch, +while higher-order terms enable non-linear corrections. The squeeze transformation +is defined as: + +.. math:: + + \Delta r(r) = a_0 + a_1 r + a_2 r^2 + \dots + a_n r^n + +where :math:`a_0, a_1, ..., a_n` are the polynomial coefficients defined by the user. + +In this example, we show how to apply a squeeze morph in combination +with a scale morph to match a morph function to its target. The required +files can be found in ``additionalData/morphsqueeze/``. + +1. ``cd`` into the ``morphsqueeze`` directory:: + + cd additionalData/morphsqueeze + + Here you will find: + + - ``squeeze_morph.cgr`` — the morph function with a small built-in polynomial distortion. + - ``squeeze_target.cgr`` — the target function. + +2. Suppose we know that the morph needs a quadratic and cubic squeeze, + plus a scale factor to best match the target. As an initial guess, + we can use: + + - ``squeeze = 0,-0.001,-0.0001,0.0001`` + (for a polynomial: :math:`a_0 + a_1 x + a_2 x^2 + a_3 x^3`) + - ``scale = 1.1`` + + The squeeze polynomial is provided as a comma-separated list (no spaces):: + + diffpy.morph --scale=1.1 --squeeze=0,-0.001,-0.0001,0.0001 -a squeeze_morph.cgr squeeze_target.cgr + +3. ``diffpy.morph`` will apply the polynomial squeeze and scale, + display the initial and refined coefficients, and show the final + difference ``Rw``. + + To refine the squeeze polynomial and scale automatically, remove + the ``-a`` tag if you used it. For example:: + + diffpy.morph --scale=1.1 --squeeze=0,-0.001,-0.0001,0.0001 squeeze_morph.cgr squeeze_target.cgr + +4. Check the output for the final squeeze polynomial coefficients and scale. + They should match the true values used to generate the test data: + + - ``squeeze = 0, 0.01, 0.0001, 0.001`` + - ``scale = 0.5`` + + ``diffpy.morph`` refines the coefficients to minimize the residual + between the squeezed, scaled morph function and the target. + +.. warning:: + + **Extrapolation risk:** + A polynomial squeeze can shift morph data outside the target’s ``r``-range, + so parts of the output may be extrapolated. + This is generally fine if the polynomial coefficients are small and + the distortion is therefore small. If your coefficients are large, check the + plots carefully — strong extrapolation can produce unrealistic features at + the edges. If needed, adjust the coefficients to keep the morph physically + meaningful. + +Experiment with your own squeeze polynomials to fine-tune your morphs — even +small higher-order corrections can make a big difference! + Nanoparticle Shape Effects ========================== diff --git a/news/tutorial_squeeze_funcy.rst b/news/tutorial_squeeze_funcy.rst new file mode 100644 index 00000000..f4c4d7ae --- /dev/null +++ b/news/tutorial_squeeze_funcy.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added tutorial for MorphSqueeze and MorphFuncy + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/morph/morphs/morphfuncy.py b/src/diffpy/morph/morphs/morphfuncy.py index 5b633416..41716e7e 100644 --- a/src/diffpy/morph/morphs/morphfuncy.py +++ b/src/diffpy/morph/morphs/morphfuncy.py @@ -30,33 +30,33 @@ class MorphFuncy(Morph): ------- Import the funcy morph function: - >>> from diffpy.morph.morphs.morphfuncy import MorphFuncy + >>> 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 + >>> def sine_function(x, y, amplitude, frequency): + >>> return amplitude * np.sin(frequency * x) * y Provide initial guess for parameters: - >>> parameters = {'amplitude': 2, 'frequency': 2} + >>> 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) + >>> 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 + >>> 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 diff --git a/src/diffpy/morph/morphs/morphsqueeze.py b/src/diffpy/morph/morphs/morphsqueeze.py index 146ce280..bc0e4d49 100644 --- a/src/diffpy/morph/morphs/morphsqueeze.py +++ b/src/diffpy/morph/morphs/morphsqueeze.py @@ -32,27 +32,27 @@ class MorphSqueeze(Morph): ------- Import the squeeze morph function: - >>> from diffpy.morph.morphs.morphsqueeze import MorphSqueeze + >>> from diffpy.morph.morphs.morphsqueeze import MorphSqueeze Provide initial guess for squeezing coefficients: - >>> squeeze_coeff = {"a0":0.1, "a1":-0.01, "a2":0.005} + >>> 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) + >>> 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 + >>> 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 diff --git a/tutorial/additionalData.zip b/tutorial/additionalData.zip index 66520ccf..6ea6621b 100644 Binary files a/tutorial/additionalData.zip and b/tutorial/additionalData.zip differ