pyextal.superspace.saed ======================= .. py:module:: pyextal.superspace.saed .. autoapi-nested-parse:: Kinematic zone-axis diffraction patterns from a modulated structure. Item 4: the SAED-like pattern. Intensities are ``|F(H)|^2`` weighted by a relrod shape factor, which is what decides whether a satellite is actually visible: a reflection sitting out of the zero-order Laue zone contributes only in so far as its relrod, of length ~1/t for a specimen of thickness t, reaches the Ewald sphere. That weighting is not decoration. 1T-TaS2's commensurate CDW has a ``1/3 c*`` component, which puts every first-order satellite out of the [001] ZOLZ; whether they show up in a real pattern is entirely a question of thickness and excitation error. Plotting ``|F|^2`` alone would draw them at full strength and say something false about the experiment. The projection geometry is borrowed read-only from :mod:`pyextal.acom.geometry`, which already gets the handedness right and needs no changes for this: its helpers take float reflection indices, and a satellite's effective index is exactly that. Classes ------- .. autoapisummary:: pyextal.superspace.saed.DiffractionPattern Functions --------- .. autoapisummary:: pyextal.superspace.saed.saed_pattern pyextal.superspace.saed.project_pattern pyextal.superspace.saed.marker_sizes pyextal.superspace.saed.plot_pattern pyextal.superspace.saed.pseudo_voigt_kernel pyextal.superspace.saed.render_image pyextal.superspace.saed.saturation_level pyextal.superspace.saed.plot_image Module Contents --------------- .. py:class:: DiffractionPattern(H, xy, intensity, order, sg, F, zone, xaxis, satlen=None) A computed zone-axis pattern. :ivar H: (n, 3+d) superspace indices. :vartype H: np.ndarray :ivar xy: (n, 2) in-plane coordinates, inverse Angstrom. :vartype xy: np.ndarray :ivar intensity: (n,) relative intensities, normalised so the strongest *diffracted* beam is 1. The direct beam is far above that and is meant to be clipped when plotted. :vartype intensity: np.ndarray :ivar order: (n,) satellite order, ranked by modulation length so symmetry-equivalent satellites share it. See :mod:`~pyextal.superspace.indexing`. :vartype order: np.ndarray :ivar satlen: (n,) ``|sum_j m_j q_j|`` in inverse Angstrom. :vartype satlen: np.ndarray :ivar sg: (n,) excitation errors, inverse Angstrom. :vartype sg: np.ndarray :ivar F: (n,) complex structure factors. :vartype F: np.ndarray :ivar zone: The zone axis. :vartype zone: tuple :ivar xaxis: The in-plane reference reflection. :vartype xaxis: tuple .. py:attribute:: satlen .. py:property:: index_sum (n,) ``sum |m_j|``; not symmetry-invariant, see `order`. :type: np.ndarray .. py:method:: __len__() .. py:method:: __repr__() .. py:method:: visible(rel=0.0001) Reflections above a relative intensity threshold. The threshold is measured against the strongest *diffracted* beam, not the direct one -- intensities are already normalised that way, and the direct beam is orders of magnitude above it, so scaling to that would cut everything. :param rel: Fraction of the strongest diffracted intensity. :type rel: float :returns: Boolean mask. :rtype: np.ndarray .. py:method:: brightest(n=20, exclude_origin=True) Indices of the strongest reflections, for a quick look at a list. :param n: How many to return. :type n: int :param exclude_origin: Skip the direct beam, which dominates. :type exclude_origin: bool :returns: Indices into the pattern arrays. :rtype: np.ndarray .. py:function:: saed_pattern(lat, atoms, zone=(0, 0, 1), gmax=1.6, max_index=8, max_order=4, thickness=200.0, hv=200.0, xaxis=None, kind='electron', sg_max=None, n_internal=None, condition=None, merge=None) Compute a kinematic zone-axis pattern. :param lat: The superspace lattice. :type lat: SuperspaceLattice :param atoms: Structure, listed in P1. :type atoms: list[ModulatedAtom] :param zone: Zone axis, direct-space indices. :type zone: tuple :param gmax: Resolution limit, inverse Angstrom. :type gmax: float :param max_index: Half-width of the ``(h, k, l)`` search box. :type max_index: int :param max_order: Largest satellite order. :type max_order: int :param thickness: Specimen thickness in Angstrom, setting the relrod length ``1/t``. Larger means a thinner slice of reciprocal space contributes, so out-of-zone satellites fade. :type thickness: float :param hv: Accelerating voltage in kV, for the Ewald sphere radius. :type hv: float :param xaxis: In-plane reference reflection; the shortest in-plane main reflection when None. :type xaxis: tuple, optional :param kind: ``'electron'`` or ``'xray'`` scattering factors. :type kind: str :param sg_max: Discard reflections with ``|s_g|`` above this. ``5 / thickness`` when None, past which the relrod contributes essentially nothing. :type sg_max: float, optional :param n_internal: Internal quadrature grid; auto when None. :type n_internal: int, optional :param condition: Reflection condition, passed to :func:`~pyextal.superspace.indexing.enumerate_reflections`. :type condition: callable, optional :param merge: Add the structure factors of superspace indices that are the same physical reflection before squaring. Only commensurate lattices have any, and for them it is not optional -- the intensity there is ``|F_1 + F_2|^2``, and treating the indices separately splits it arbitrarily and destroys the pattern's symmetry. Decided from :meth:`SuperspaceLattice.is_commensurate` when None, which is the right answer either way. :type merge: bool, optional :returns: The pattern. :rtype: DiffractionPattern :raises ValueError: If the zone admits no in-plane reflection within `gmax`. .. py:function:: project_pattern(lat, H, zone=(0, 0, 1), xaxis=None) In-plane coordinates of superspace reflections at a zone axis. Shared by every route that produces a pattern -- kinematic, the superspace engine, the exact supercell -- so their images can be laid side by side and compared pixel for pixel. Two routes computing the same reflection at slightly different coordinates would be a subtle and very annoying bug. :param lat: The lattice. :type lat: SuperspaceLattice :param H: (n, 3+d) superspace indices. :type H: array-like :param zone: Zone axis, direct-space indices. :type zone: tuple :param xaxis: In-plane reference reflection. The shortest in-plane *main* reflection when None, which keeps the axis in the basic structure's terms so it survives a change of q. :type xaxis: tuple, optional :returns: ``(xy, xaxis)`` with `xy` (n, 2) in inverse Angstrom. :rtype: tuple[np.ndarray, tuple] :raises ValueError: If the zone admits no in-plane main reflection. .. py:function:: marker_sizes(intensity, size=260, gamma=0.35, floor=1.5) Marker areas for a diffraction pattern, saturating the direct beam. Two things make a linear area-to-intensity map useless here. The direct beam is normalised against the strongest *diffracted* beam and the Mott-Bethe factor diverges as ``s -> 0``, so its "intensity" is of order 1e7 -- scaled linearly it becomes a single marker larger than the axes, and the whole plot comes out a solid block of colour. And satellites are ~1e-3 of a main reflection, so anything that renders the mains sensibly renders the satellites as nothing. Intensity is therefore clipped at 1 (the direct beam saturates, exactly as it does on a recorded plate) and raised to `gamma`, the power stretch normally used to display a diffraction pattern. :param intensity: Relative intensities, mains near 1. :type intensity: array-like :param size: Marker area for a saturated reflection. :type size: float :param gamma: Power stretch. 1.0 is linear; smaller lifts weak reflections into view. :type gamma: float :param floor: Smallest marker area, so a very weak reflection is still a visible dot rather than nothing. :type floor: float :returns: Marker areas in points squared. :rtype: np.ndarray .. py:function:: plot_pattern(pattern, ax=None, size=260, rel_min=0.0001, label_orders=True, colors=None, annotate=0, title=None, gamma=0.35) Draw a pattern, distinguishing main reflections from satellite orders. Marker area is a power-stretched, saturating function of intensity; see :func:`marker_sizes` for why a linear map does not work. :param pattern: What to draw. :type pattern: DiffractionPattern :param ax: Target axes; a new figure when None. :type ax: matplotlib.axes.Axes, optional :param size: Marker area for a saturated reflection. :type size: float :param rel_min: Relative intensity below which a reflection is dropped. :type rel_min: float :param label_orders: Show a legend keyed by satellite order. :type label_orders: bool :param colors: Satellite order -> colour. :type colors: dict, optional :param annotate: Label this many of the strongest reflections with their superspace index. Zero draws none. :type annotate: int :param title: Axes title. :type title: str, optional :param gamma: Power stretch on intensity; smaller lifts the satellites. :type gamma: float :returns: The axes drawn on. :rtype: matplotlib.axes.Axes .. py:function:: pseudo_voigt_kernel(radius, sigma, gamma) The pseudo-Voigt point spread the JAX refinement fits, on a pixel grid. Identical in form to ``jaxEngine.postProcess.create_pseudo_voigt_kernel``, including the Thompson-Cox-Hastings width combination and the eta approximation, so a `sigma` and `gamma` refined against experimental data can be handed straight to :func:`render_image` and mean the same thing. Widths are in pixels. The kernel is tapered to zero on a disc rather than left square. Its Lorentzian part decays only as 1/r^2, so at any affordable radius it is still a per cent or so of the peak at the edge -- and truncating that squarely puts a visible square halo around every strong reflection in the convolved image. A raised-cosine taper over the outer quarter removes it and costs nothing, since the discarded intensity is renormalised back. :param radius: Kernel half-width; the kernel is (2r+1) square, with the corners tapered away. :type radius: int :param sigma: Gaussian width, pixels. :type sigma: float :param gamma: Lorentzian width, pixels. :type gamma: float :returns: (2r+1, 2r+1) kernel summing to 1. :rtype: np.ndarray .. py:function:: render_image(pattern, shape=(512, 512), extent=None, sigma=2.0, gamma=1.0, radius=None, rel_min=0.0, include_direct=True) Render a pattern as a 2D intensity image, the way a plate records it. A scatter plot draws markers whose area someone chose; this puts intensity on a pixel grid, so the result is an image in the same sense as the engine's own diffraction output -- something you can threshold, integrate, line-scan or compare against a recorded pattern. Each reflection's intensity is spread bilinearly over the four pixels around its position, so a peak that lands between pixels keeps its centroid and its total counts rather than snapping to a grid point. The whole image is then convolved with the pseudo-Voigt of :func:`pseudo_voigt_kernel`. The direct beam is orders of magnitude above everything else, so plot the result on a log or power scale -- :func:`plot_image` does. :param pattern: What to render. :type pattern: DiffractionPattern :param shape: ``(rows, cols)`` in pixels. :type shape: tuple :param extent: Half-width of the field in inverse Angstrom. Taken from the pattern, with a margin, when None. :type extent: float, optional :param sigma: Gaussian width of the point spread, in pixels. :type sigma: float :param gamma: Lorentzian width, in pixels. :type gamma: float :param radius: Kernel half-width. ``ceil(6*max(sigma, gamma))`` when None -- generous because the Lorentzian component has long tails; :func:`pseudo_voigt_kernel` tapers whatever is left. :type radius: int, optional :param rel_min: Drop reflections below this fraction of the strongest diffracted beam before rendering. :type rel_min: float :param include_direct: Render the direct beam. It saturates everything; turning it off is often the more readable picture. :type include_direct: bool :returns: The ``(rows, cols)`` image, and the ``(left, right, bottom, top)`` extent in inverse Angstrom, ready for ``imshow(..., extent=...)``. :rtype: tuple[np.ndarray, tuple] .. py:function:: saturation_level(pattern, shape=(512, 512), extent=None, sigma=2.0, gamma=1.0, radius=None, rel_min=0.0) The intensity :func:`plot_image` clips at when it chooses for itself. The peak of the strongest *diffracted* beam once rendered and convolved. Useful for putting two patterns on one scale: `plot_image` picks this separately for each, so two panels drawn side by side are each normalised to their own maximum and their brightnesses cannot be compared. Compute it once and pass it to both as `vmax`:: level = saturation_level(kinematic, shape=(650, 650), sigma=3.0) plot_image(kinematic, vmax=level, shape=(650, 650), sigma=3.0) plot_image(dynamical, vmax=level, shape=(650, 650), sigma=3.0) The render arguments must match the ones `plot_image` is given, since the convolution spreads a fixed number of counts and the peak height therefore depends on the grid and the kernel. :param pattern: The pattern. :type pattern: DiffractionPattern :param shape: Image size in pixels. :type shape: tuple :param extent: Half-width of the field, inverse Angstrom. :type extent: float, optional :param sigma: Gaussian width of the point spread, pixels. :type sigma: float :param gamma: Lorentzian width, pixels. :type gamma: float :param radius: Kernel half-width. :type radius: int, optional :param rel_min: Intensity cutoff before rendering. :type rel_min: float :returns: Peak image intensity excluding the direct beam. :rtype: float .. py:function:: plot_image(pattern, ax=None, shape=(512, 512), extent=None, sigma=2.0, gamma=1.0, radius=None, stretch=0.4, cmap='inferno', include_direct=True, saturate=True, title=None, rel_min=0.0, colorbar=False, vmax=None) Draw a rendered pattern as an image. Two things have to be handled or the picture is a black square with one white dot. The direct beam is some seven orders of magnitude above the strongest diffracted beam, so the display is scaled to the *diffracted* maximum and the centre is allowed to saturate -- which is what a recorded plate does anyway. And satellites are ~1e-3 of a main reflection, so a power stretch lifts them into view. :param pattern: What to draw. :type pattern: DiffractionPattern :param ax: Target axes. :type ax: matplotlib.axes.Axes, optional :param shape: Image size in pixels. :type shape: tuple :param extent: Half-width of the field, inverse Angstrom. :type extent: float, optional :param sigma: Gaussian width of the point spread, pixels. :type sigma: float :param gamma: Lorentzian width, pixels. :type gamma: float :param radius: Kernel half-width. :type radius: int, optional :param stretch: Display power. 1.0 is linear; smaller lifts the weak reflections. 0.4 shows satellites without washing out the mains. :type stretch: float :param cmap: Colormap. :type cmap: str :param include_direct: Render the direct beam at all. :type include_direct: bool :param saturate: Scale the display to the strongest diffracted beam and let the direct beam clip. Turning this off scales to the true maximum, which is the direct beam, and shows little else. :type saturate: bool :param title: Axes title. :type title: str, optional :param rel_min: Intensity cutoff before rendering. :type rel_min: float :param colorbar: Add a colorbar keyed to the saturation level. :type colorbar: bool :param vmax: Intensity that maps to full scale, in the units of the returned image; everything above it clips. Chosen automatically when None -- the strongest diffracted beam, which is what `saturate` describes. Pass it to brighten (smaller) or dim (larger) a plot, and above all to put several patterns on **one** scale: the automatic choice is made per pattern, so side-by-side panels are otherwise each normalised to their own maximum and cannot be compared. :func:`saturation_level` computes the automatic value without drawing anything. :type vmax: float, optional :returns: ``(ax, image)`` with `image` the unscaled intensity array. :rtype: tuple :raises ValueError: If `vmax` is not positive.