From 9ec877423d182a66df53c0317d7551f4c1f4149d Mon Sep 17 00:00:00 2001 From: Sparky <59151395+Sparks29032@users.noreply.github.com> Date: Tue, 30 Sep 2025 17:57:05 -0700 Subject: [PATCH 1/2] Remove PDF language from plot (#263) * Remove PDF language from plot * Add news --- news/refactor_plot.rst | 23 +++++++ src/diffpy/morph/plot.py | 126 +++++++++++++++++++-------------------- 2 files changed, 86 insertions(+), 63 deletions(-) create mode 100644 news/refactor_plot.rst diff --git a/news/refactor_plot.rst b/news/refactor_plot.rst new file mode 100644 index 0000000..d4e3ffa --- /dev/null +++ b/news/refactor_plot.rst @@ -0,0 +1,23 @@ +**Added:** + +* + +**Changed:** + +* Removed PDF-specific language from all plotting functions. + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/morph/plot.py b/src/diffpy/morph/plot.py index 22eb46b..e10c963 100644 --- a/src/diffpy/morph/plot.py +++ b/src/diffpy/morph/plot.py @@ -24,13 +24,13 @@ # FIXME - make this return the figure object in the future, so several views # can be composed. -def plot_funcs(pairlist, labels=None, offset="auto", rmin=None, rmax=None): - """Plots several functions g(r) on top of one another. +def plot_funcs(pairlist, labels=None, offset="auto", xmin=None, xmax=None): + """Plots several functions f(x) on top of one another. Parameters ---------- pairlist - Iterable of (r, gr) pairs to plot. + Iterable of (x, fx) pairs to plot. labels Iterable of names for the pairs. If this is not the same length as the pairlist, a legend will not be shown (default []). @@ -38,10 +38,10 @@ def plot_funcs(pairlist, labels=None, offset="auto", rmin=None, rmax=None): Offset to place between plots. Functions will be sequentially shifted in the y-direction by the offset. If offset is 'auto' (default), the optimal offset will be determined automatically. - rmin + xmin The minimum r-value to plot. If this is None (default), the lower bound of the function is not altered. - rmax + xmax The maximum r-value to plot. If this is None (default), the upper bound of the function is not altered. """ @@ -55,16 +55,16 @@ def plot_funcs(pairlist, labels=None, offset="auto", rmin=None, rmax=None): labels.extend([""] * gap) for idx, pair in enumerate(pairlist): - r, gr = pair - plt.plot(r, gr + idx * offset, label=labels[idx]) - plt.xlim(rmin, rmax) + x, fx = pair + plt.plot(x, fx + idx * offset, label=labels[idx]) + plt.xlim(xmin, xmax) if gap == 0: plt.legend(loc=0) plt.legend() - plt.xlabel(r"$r (\mathrm{\AA})$") - plt.ylabel(r"$G (\mathrm{\AA}^{-1})$") + # plt.xlabel(r"$r (\mathrm{\AA})$") + # plt.ylabel(r"$G (\mathrm{\AA}^{-1})$") plt.show() return @@ -72,8 +72,8 @@ def plot_funcs(pairlist, labels=None, offset="auto", rmin=None, rmax=None): def compare_funcs( pairlist, labels=None, - rmin=None, - rmax=None, + xmin=None, + xmax=None, show=True, maglim=None, mag=5, @@ -94,11 +94,11 @@ def compare_funcs( labels Iterable of names for the pairs. If this is not the same length as the pairlist, a legend will not be shown (default []). - rmin - The minimum r-value to plot. If this is None (default), the lower + xmin + The minimum x-value to plot. If this is None (default), the lower bound of the function is not altered. - rmax - The maximum r-value to plot. If this is None (default), the upper + xmax + The maximum x-value to plot. If this is None (default), the upper bound of the function is not altered. show Show the plot (default True) @@ -119,24 +119,24 @@ def compare_funcs( else: labeldata = labels[1] labelfit = labels[0] - rfit, grfit = pairlist[0] - rdat, grdat = pairlist[1] + xfit, fxfit = pairlist[0] + xdat, fxdat = pairlist[1] # View min and max - rvmin = max(rfit[0], rdat[0]) - rvmin = rmin or rvmin - rvmax = min(rfit[-1], rdat[-1]) - rvmax = rmax or rvmax + xvmin = max(xfit[0], xdat[0]) + xvmin = xmin or xvmin + xvmax = min(xfit[-1], xdat[-1]) + xvmax = xmax or xvmax gap = 2 - len(labels) labels = list(labels) labels.extend([""] * gap) - # Put gr1 on the same grid as rdat - gtemp = numpy.interp(rdat, rfit, grfit) + # Put fx1 on the same grid as xdat + ftemp = numpy.interp(xdat, xfit, fxfit) # Calculate the difference - diff = grdat - gtemp + diff = fxdat - ftemp # Put rw in the label labeldiff = "difference" if len(labels) < 3 else labels[2] @@ -145,24 +145,24 @@ def compare_funcs( # Magnify if necessary if maglim is not None: - grfit = grfit.copy() - grfit[rfit > maglim] *= mag - sel = rdat > maglim - grdat = grdat.copy() - grdat[sel] *= mag + fxfit = fxfit.copy() + fxfit[xfit > maglim] *= mag + sel = xdat > maglim + fxdat = fxdat.copy() + fxdat[sel] *= mag diff[sel] *= mag - gtemp[sel] *= mag + ftemp[sel] *= mag # Determine the offset for the difference curve. - sel = numpy.logical_and(rdat <= rvmax, rdat >= rvmin) - ymin = min(min(grdat[sel]), min(gtemp[sel])) + sel = numpy.logical_and(xdat <= xvmax, xdat >= xvmin) + ymin = min(min(fxdat[sel]), min(ftemp[sel])) ymax = max(diff[sel]) offset = -1.1 * (ymax - ymin) # Scale the x-limit based on the r-extent of the signal. This gives a nice # density of function peaks. - rlim = rvmax - rvmin - scale = rlim / 25.0 + xlim = xvmax - xvmin + scale = xlim / 25.0 # Set a reasonable minimum of .8 and maximum of 1 scale = min(1, max(scale, 0.8)) figsize = [13.5, 4.5] @@ -177,12 +177,12 @@ def compare_funcs( fig.add_axes(axes) plt.minorticks_on() - plt.plot(rdat, grdat, linewidth=l_width, label=labeldata) - plt.plot(rfit, grfit, linewidth=l_width, label=labelfit) - plt.plot(rdat, offset * numpy.ones_like(diff), linewidth=3, color="black") + plt.plot(xdat, fxdat, linewidth=l_width, label=labeldata) + plt.plot(xfit, fxfit, linewidth=l_width, label=labelfit) + plt.plot(xdat, offset * numpy.ones_like(diff), linewidth=3, color="black") diff += offset - plt.plot(rdat, diff, linewidth=l_width, label=labeldiff) + plt.plot(xdat, diff, linewidth=l_width, label=labeldiff) if maglim is not None: # Add a line for the magnification cutoff @@ -196,14 +196,14 @@ def compare_funcs( dashes=(14, 7), ) # FIXME - look for a place to put the maglim - xpos = (rvmax * 0.85 + maglim) / 2 / (rvmax - rvmin) + xpos = (xvmax * 0.85 + maglim) / 2 / (xvmax - xvmin) if xpos <= 0.9: plt.figtext(xpos, 0.7, "x%.1f" % mag, backgroundcolor="w") # Get a tight view - plt.xlim(rvmin, rvmax) + plt.xlim(xvmin, xvmax) ymin = min(diff[sel]) - ymax = max(max(grdat[sel]), max(gtemp[sel])) + ymax = max(max(fxdat[sel]), max(ftemp[sel])) yspan = ymax - ymin # Give a small border to the plot gap = 0.05 * yspan @@ -306,38 +306,38 @@ def plot_param(target_labels, param_list, param_name=None, field=None): return -def truncate_func(r, gr, rmin=None, rmax=None): - """Truncate a function g(r) to specified bounds. +def truncate_func(x, fx, xmin=None, xmax=None): + """Truncate a function f(x) to specified bounds. Parameters ---------- - r - The r-values of the function g(r). - gr - Function g(r) values. - rmin - The minimum r-value. If this is None (default), the lower bound of + x + The x-values of the function f(x). + fx + Function f(x) values at each x-value. + xmin + The minimum x-value. If this is None (default), the lower bound of the function is not altered. - rmax - The maximum r-value. If this is None (default), the upper bound of + xmax + The maximum x-value. If this is None (default), the upper bound of the function is not altered. Returns ------- - r, gr - Returns the truncated r, gr. + x, fx + Returns the truncated x, fx. """ - if rmin is not None: - sel = r >= rmin - gr = gr[sel] - r = r[sel] - if rmax is not None: - sel = r <= rmax - gr = gr[sel] - r = r[sel] + if xmin is not None: + sel = x >= xmin + fx = fx[sel] + x = x[sel] + if xmax is not None: + sel = x <= xmax + fx = fx[sel] + x = x[sel] - return r, gr + return x, fx def _find_offset(pairlist): From c0588b4d736f0248b22462762d252a6078401654 Mon Sep 17 00:00:00 2001 From: Sparky <59151395+Sparks29032@users.noreply.github.com> Date: Tue, 30 Sep 2025 22:10:00 -0700 Subject: [PATCH 2/2] Add tutorial for wrapping PDFgetx3 and PyFai with funcxy (#264) * funcxy initial * Add preliminary pyfai morph * Add links to tutorials * News --- docs/source/funcxy.rst | 181 ++++++++++++++++++++++++++++++++++++++ docs/source/morphpy.rst | 11 ++- docs/source/tutorials.rst | 8 ++ news/funcxy_tutorial.rst | 23 +++++ 4 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 docs/source/funcxy.rst create mode 100644 news/funcxy_tutorial.rst diff --git a/docs/source/funcxy.rst b/docs/source/funcxy.rst new file mode 100644 index 0000000..689ad3d --- /dev/null +++ b/docs/source/funcxy.rst @@ -0,0 +1,181 @@ +.. _funcxy: + +Using funcxy with Commonly-Used Diffraction Software +#################################################### + +The general xy morph ``funcxy`` can be used to tune parameters +of many popular diffraction software functions. + +Below, we give templates for how one can use ``funcxy`` +with `PDFgetx3 `_ +and `PyFai `_. + +Getting a Better PDF with PDFgetx3 +================================== + +In PDFgetx3, the ``PDFGetter`` takes in a 1D diffraction +pattern I(Q) and returns a PDF G(r). + +There are many parameters you can specify, such as + - ``qmin``: Lower Q-cutoff for the Fourier transform giving the PDF + - ``qmax``: Upper Q-cutoff for the Fourier transform giving the PDF + - ``qmaxinst``: Upper Q-boundary for meaningful signal + - ``rpoly``: Approximately the low-r bound of meaningful G(r) values + +Furthermore, you can supply a background file ``backgroundfile`` +and subtract a scaled version of the background file by the +scaling factor ``bgscale``. + +We will showcase an example of how one would refine over the +``PDFGetter`` parameters using ``funcxy`` to obtain a PDF. + +Let's say you have a measured I(Q) with Q in angstroms of a lead +nanoparticle (composition PbS) named ``sample.chi`` taken on a +glass background. We want to match a target calculated PDF G(r) +stored in a file named ``target.cgr``. +Let's also say we have a measured I(Q) of the +glass background ``background.chi``. + +.. code-block:: python + + from diffpy.pdfgetx.pdfgetter import PDFGetter + from diffpy.morph.morphpy import morph_arrays + from diffpy.utils.parsers.loaddata import loadData + + pg = PDFGetter() + + backgroundfile = loadData("background.chi") + composition = "PbS" + + + def wrap(x, y, **kwargs): + xy_out = pg.__call__( + x=x, y=y, dataformat="QA", + composition=composition, + backgroundfile=backgroundfile, + **kwargs + ) + r = xy_out[0] + gr = xy_out[1] + return (r, gr) + + + sample_iq = loadData("sample.chi") + target_gr = loadData("target.cgr") + params_to_morph = { + "bgscale": 1.0, + "qmin": 0.0, "qmax": 25.0, + "qmaxinst": 25.0, "rpoly": 0.9 + } + + morph_info, morphed_gr = morph_arrays( + sample_iq, target_gr, + funcxy=(wrap, params_to_morph) + ) + +You can now plot ``morphed_gr`` against your ``target_gr`` to see +how well your morphing refinement of the PDF-getting parameters +as done! +To see what the refined values of the parameters are, +print out ``morph_info``. +You can freely add and remove entries in +``params_to_morph`` to include or not include them as +parameters to refine over. + +If you expect to see thermal effect differences between your +measured PDF and ``target_gr``, you can also include +the ``stretch``, ``scale``, and ``smear`` morphs in your +call to ``morph_arrays``. + + +Performing Detector Calibration with PyFai +========================================== + +When performing azimuthal integration, it is important to +ensure your beam center and detector distances are calibrated. +However, it is possible that they have shifted +across measurements. Here, we will use morphing to the rescue! + +Let's say we just measured a diffraction pattern stored +as a NumPy object in ``diffraction_image.npy``, but some +of the detector geometries are off. +Our azimuthally integrated ``sample.chi`` looks a bit off. +Before this measurement, you measured an amazing +I(Q) pattern ``target.chi`` with a perfectly calibrated +sample-to-detector distance and beam center. +We will use morphing to try to match the integration of +the 2D pattern to the target 1D function. + +For the integration, we will need some information, such as +the wavelength of the beam, +the size of each pixel in the 2D image +(``pixel1`` is the horizontal length in meters and +``pixel2`` is the vertical length in meters), +and a guess of the beam center. +This information can be found on the +`PyFai documentation `_. +For our example, let's say we have a ``1024``x``1024`` pixel image +where each pixel is a ``100`` micron by ``100`` micron region, and +our wavelength was ``1.11`` angstroms. + +.. code-block:: python + + import numpy as np + import pyFAI.integrator.azimuthal as pyfai + import pyFAI.detectors as pfd + from diffpy.morph.morphpy import morph_arrays + from diffpy.utils.parsers.loaddata import loadData + + pattern_2d = np.load("diffraction_image.npy") + wavelength = 0.1110e-9 # in m + pixel1 = 1e-4 # in m + pixel2 = 1e-4 # in m + cent_x = 511 # in number of pixels + cent_y = 511 # in number of pixels + + ai = pyfai.AzimuthalIntegrator() + ai.wavelength = wavelength + detector = pfd.Detector() + detector.max_shape = pattern_2d.shape + + + def wrap(x, y, sample_to_detector_dist, cent_offset_x, cent_offset_y): + detector.pixel1 = pixel1 + detector.pixel2 = pixel2 + ai.detector = detector + + ai.setFit2D( + directDist=sample_to_detector_dist, + centerX=cent_x+cent_offset_x, + centerY=cent_y+cent_offset_y + ) + + return ai.integrate1D_ng( + pattern_2d, + npt=1000, unit="q_A^-1", + method="mean" + ) + + + params_to_morph = { + "sample_to_detector_dist": 60, # in mm + "cent_offset_x": 0, # in number of pixels + "cent_offset_y": 0 # in number of pixels + } + + sample_chi = loadData("sample.chi") + target_chi = loadData("target.chi") + + morph_info, morphed_chi = morph_arrays( + sample_chi, target_chi, + funcxy=(wrap, params_to_morph) + ) + +You can now plot ``morphed_chi`` against your ``target_chi`` +to see if the refinement has helped in the calibration! +To see the calibrated values, you can print out ``morph_info``. + +If you would like to morph over other PyFai parameters +(e.g. ``rot1``, ``tilt``, ``wavelength``), +you can adjust the wrapper function ``wrap`` to take in +these parameters. diff --git a/docs/source/morphpy.rst b/docs/source/morphpy.rst index 1fd4f1c..54a17ce 100644 --- a/docs/source/morphpy.rst +++ b/docs/source/morphpy.rst @@ -10,6 +10,11 @@ This page is intended for those acquainted with the basic morphs described in the aforementioned quickstart tutorial who want to use ``diffpy.morph`` in their Python scripts. +For those looking to use the Python-specific morph ``MorphFuncxy`` (described below) +with commonly used diffraction software like `PDFgetx3 `_ +and `PyFai `_ are directed to the +`funcxy tutorials `__. + Python Morphing Functions ========================= @@ -408,8 +413,10 @@ This is equivalent to applying a ``MorphFuncx`` and ``MorphFuncy`` simultaneously. This morph is useful when you want to apply operations that modify both -the grid and function value. A PDF-specific example includes computing -PDFs from 1D diffraction data (see paragraph at the end of this section). +the grid and function value. +Examples of using ``MorphFuncxy`` with ``PyFai`` azimuthal integration +and ``PDFgetx3`` PDF calculation are included `here `__. + For this tutorial, we will go through two examples. One simple one involving shifting a function in the ``x`` and ``y`` directions, and diff --git a/docs/source/tutorials.rst b/docs/source/tutorials.rst index 2fccec1..01fac3d 100644 --- a/docs/source/tutorials.rst +++ b/docs/source/tutorials.rst @@ -8,6 +8,14 @@ Tutorials for these are included below. The files required for these tutorials c For a full list of options offered by ``diffpy.morph``, please run ``diffpy.morph --help`` on the command line. +Using MorphFuncxy +================= + +Examples of how to use the general morph ``MorphFuncxy`` with commonly used +diffraction software like `PDFgetx3 `_ +and `PyFai `_ are directed to the +`funcxy tutorials `__. + Performing Multiple Morphs ========================== diff --git a/news/funcxy_tutorial.rst b/news/funcxy_tutorial.rst new file mode 100644 index 0000000..f035aaf --- /dev/null +++ b/news/funcxy_tutorial.rst @@ -0,0 +1,23 @@ +**Added:** + +* Tutorials for wrapping PDFgetx3 and PyFai with funcxy. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +*