Source code for jscatter.structurefactor.ordered

# -*- coding: utf-8 -*-
# written by Ralf Biehl at the Forschungszentrum Jülich ,
# Jülich Center for Neutron Science 1 and Institute of Complex Systems 1
#    Jscatter is a program to read, analyse and plot data
#    Copyright (C) 2015-2024  Ralf Biehl
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

import os
import numbers
import inspect

import numpy as np
from numpy import linalg as la
from scipy import stats

from ..dataarray import dataArray as dA
from .. import formel

try:
    from ..libs import fscatter

    useFortran = True
except ImportError:
    fscatter = None
    useFortran = False

_path_ = os.path.realpath(os.path.dirname(__file__))

# variable to allow printout for debugging as if debug:print 'message'
# set it to integer value above debuglevel
debug = False


def _LhklVoigt(q, center, lg, domainsize, asym):
    # Voigt bragg peak shape
    return formel.voigt(q, center=center, lg=lg, fwhm=2 * np.pi / domainsize, asym=asym).Y


[docs] def latticeStructureFactor(q, lattice=None, domainsize=1000, asym=0, lg=1, rmsd=0.02, beta=None, hklmax=7, c=1., wavelength=None, corrections=[]): r""" Radial structure factor S(q) in powder average of a crystal lattice with particle asymmetry, Debye-Waller factor, diffusive scattering and broadening due to domain size. - To get the full scattering the formfactor needs to be included (See Notes and Examples). - 1-3 dimensional lattice structures with basis containing multiple atoms (see lattice). - For 1D and 2D a corresponding 1D or 2D formfactor has to be used. See Notes. - Self absorption and self extinction are not included. Polarisation and Lorentz correction are optional. - **How to fit see last example** `latticeStructureFactor as a fit model`_ . Parameters ---------- q : array float Norm of wavevectors in inverse units of lattice constant, units 1/nm domainsize : float Domainsize of the crystal, units as lattice constant of lattice. According to Debye-Scherrer equation :math:`fwhm=2\pi/domainsize` the peak width is determined [2]_. lattice : lattice object The crystal structure as defined in a lattice object. The size of the lattice is ignored. One of rhombicLattice, bravaisLattice, scLattice, bccLattice, fccLattice, diamondLattice, hexLattice, hcpLattice. See respective definitions. rhombicLattice can be used to define **arbitrary shaped particles** in the unit cell. See examples. lg : float, default = 1 Lorenzian/Gaussian fraction describes the contributions of Gaussian and Lorenzian shape in peak shape (Voigt function). - lorenzian/gaussian >> 1 lorenzian, - lorenzian/gaussian ~ 1 central part gaussian, outside lorenzian wings - lorenzian/gaussian << 1 gaussian asym : float, default=0 Asymmetry factor in sigmoidal as :math:`2fwhm/(1+e^{asym*(x-center)})` For asym=0 the Voigt is symmetric with fwhm. See formel.voigt . rmsd : float, [3xfloat], default=0.02 Root mean square displacement :math:`rmsd=<u^2>^{0.5}` determining the Debye Waller factor. Units as domainsize and lattice units. - float : The Debye Waller factor is used as :math:`DW(Q)=e^{-Q^2 rmsd^2 }=e^{-Q^2 u^2 }` - 3xfloat : Assuming different displacements along the lattice vectors. In general a matrix describes anisotropic displacements according to Kronenburg [6]_. We allow here anisotropic displacements :math:`rmsd = (u_1,u_2,u_3)` along the .latticeVectors :math:`v_i` with :math:`DW_{hkl}(Q)=exp(-q_{hkl} U q_{hkl})` and :math:`U=diag(u1,u2,u3)`. See Notes. beta : float, None, dataArray Asymmetry factor of the formfactor or reduction due to polydispersity. - None beta=1, No beta assumed (spherical symmetric formfactor, no polydispersity) - dataArray explicitly given as dataArray with beta in .Y column. Missing values are interpolated. - An approximation for polydisperse beta can be found in [1]_ equ.17. This can be realized by beta=js.dA(np.vstack(q,np.exp(-(q*sr*R)**2))) with sr as relative standard deviation of gaussian distribution of the size R. - See .formfactor for different formfactors which explicit calculation of beta. hklmax : int Maximum order of the Bragg peaks to include. c : float, default=1 Porod constant. See 3.8 in [1]_. wavelength : float, default = None Wavelength of the measurement in units nm. If None .Braggtheta is not calculated. For Xray Cu K_a it is 0.15406 nm. corrections : list, default=[] List of corrections to apply, which depend on the measurement type/geometry [5]_. :math:`\theta` is here the scattering angle (not :math:`2\theta` as in diffraction is used) - *'TP'* Thompson polarisation correction :math:`(1+cos^2(\theta)/2)` for electromagnetic scattering as Xrays [4]_. For small angle scattering this is negligible but valid. For polarised beams the polarisation has to be included. - *'lh'* likelihood of a crystallite being in diffraction position :math:`cos(\theta/2)`. - *'LC'* Lorentz correction :math:`\sin(\theta)^{-1}` due to integration over the width of reciprocal Bragg peaks due to lattice imperfections and the width of the incoming beam. Use for Debye-Scherrer (powder of crystallites) diffraction. - *'area'* the intensity for a given diffraction peak is recorded on a narrow strip of photographic film instead of over the entire diffraction cone :math:`\sin(\theta)^{-1}`. - *'all'* common Lorentz and polarisation correction powder measurements of crystalline material. Use all from above. NOT for flat transmission geometry (typical SAS) or non crystallite . Corresponds to :math:`(1+cos^2(\theta)/2)/sin^2(\theta/2)/sin(\theta/2)`. The correction for the pixel area presented to scattering solid angle is included in sasImage in 2D also correcting for offset detector positions of a flat detector, which cannot use the scattering angle :math:`\theta` as the geometry changes. Returns ------- dataArray Columns [q, Sq, DW, beta, Z0q, correction, theta] - q wavevector - Sq = S(q) = (1+beta(q)*(Z0(q)-1)*DW(q))*correction structure factor - DW(q) Debye-Waller factor with (1-DW)=diffusive scattering. - beta(q) asymmetry factor of the formfactor. - Z0q lattice factor Z0(q) optional - correction [optional] factor polarisation from Thompson scattering - theta scattering angle Attributes - .q_hkl peak positions - .fhkl symmetry factor - .mhkl multiplicity - .Braggtheta Bragg angles Notes ----- Analytical expressions for the scattering functions of **atomic crystals and ordered mesoscopic materials** . Ordered structures in 3D (fcc, bcc, hcp, sc), 2D (hex, sq) and lamellar structures are considered. The expressions take into account particle size distributions and lattice point deviations, domain size, core/shell structures, as well as peak shapes varying analytically between Lorentzian and Gaussian functions. The expressions allow one to quantitatively describe high-resolution synchrotron small-angle X-ray (SAXS) and neutron scattering (SANS) curves from lipid and block copolymer lyotropic phases, core/shell nanoparticle superstructures, ordered nanocomposites, ordered mesoporous materials and atomic crystal structures (see AgBe example). - The scattering intensity of a crystal domain in powder average is for isotropic scattering particles .. math:: I(q) = {\Delta\rho}^2 n P(q) S(q) with - :math:`\Delta\rho` scattering length difference between matrix and particles - :math:`n` number density (of elementary cells) - :math:`P(q)` radial averaged form factor of the particles - :math:`S(q)` structure factor :math:`S(q)` Separation of structure factor and formfactor :math:`P(q)` in 3D is possible under the assumption of isotropic oriented particles. Including polydispersity of particles as size polydispersity or anisotropic shape like ellipsoids (but still isotropic orientation) leads to the correction :math:`\beta(q)`. :math:`\beta(q)` does not account for non isotropic alignment as e.g.in lyotropic phases of cylinders. For 1D and 2D structure factor a corresponding formfactor with reduced dimesionlity that factorizes has to be used. Explicitly e.g. using a 3D formfactor as a sphere in 2D hexagonal lattice is wrong. Approximately a very long oriented cylinder might be used, but a flat disc (2D) is correct. - The above structure factor is [1]_ : .. math:: S(q)=1+ \beta(q)(Z_0(q)-1)*DW(q) with - :math:`\beta(q)=<F(q)>^2/<F(q)^2>` as asymmetry factor [3]_ dependent on the scattering amplitude :math:`F(q)` and particle polydispersity - :math:`DW(q)` Debye Waller factor - The lattice factor is [1]_ : .. math :: Z_0(q) = \frac{(2\pi)^{d-1}c}{nv_dq^{d-1}} \sum\limits_{hkl}m_{hkl}f_{hkl}^2L_{hkl}(q) with - :math:`n` number of particles per unit cell - :math:`f_{hkl}` unit cell structure factor that takes into account symmetry-related extinction rules - :math:`v_d` volume of the d-dimensional unit cell - :math:`hkl` reflections - :math:`m_{hkl}` peak multiplicity - :math:`c` Porod constant :math:`\simeq 1` - The structure factors of lattices of anisotropic particles with strong orientation like ordered cylinders can be calculated using only the lattice factor and using a unit cell with additional dummy atoms representing the asymmetric particle. The unit cell structure factor represents in these cases the formfactor amplitude in direction of Bragg peaks. See later example **Lyotropic hexagonal phase**. - Unit cell structure factors :math:`f_{hkl}` are normalised that the lattice factor is normalised for infinite q to 1. With i as unit cell atoms at fractional position in the unit cell :math:`[x_i,y_i,z_i]` and scattering amplitude :math:`b_i` we get : .. math:: f_{hkl}^2 = \big(\sum_i b_i e^{-2\pi (hx_i+ky_i+lz_i)}\big)^2 / \sum_i b_i^2 - We use a Voigt function for the peak shape :math:`L_{hkl}(q)` (see formel.voigt). - DW (isotropic) is a Debye Waller like factor as :math:`DW(q)=e^{-q^2<u^2>}` leading to a reduction of scattered intensity and diffusive scattering. It has contributions from thermal lattice disorder ( DW factor with 1/3 factor in 3D), surface roughness and size polydispersity. - DW anisotropic: in case of anisotropic Debye Waller factor according to Kronenburg [6]_ (beta==1): .. math:: S(q)= \frac{(2\pi)^{d-1}c}{nv_dq^{d-1}} \sum\limits_{hkl} DW_{hkl}(q) m_{hkl}f_{hkl}^2L_{hkl}(q) + (1-\sum\limits_{hkl}DW_{hkl}(q)/N) with :math:`DW_{hkl}(q) = exp(-q_{hkl}Uq_{hkl})`, N as number of hkl. :math:`q_{hkl}` is the scattering vector in hkl direction and :math:`U` the diagonal matrix of squared displacements in direction of the basis lattice vectors. :math:`\beta(q)` is not included in this case as no particle orientational average is required. - For the limiting behaviour q->0 see the discussion in [1]_ in 3.9. : "... The zero-order peak is not explicitly considered because of the q^(1-dim) singularity and because its intensity depends also on the scattering length difference between the lattice inside and outside... Due to the singularity and since structural features on length scales d > a, such as packing defects, grain boundaries or fluctuations decaying on larger length scales are only indirectly considered via the domain size D, eq 30 is not expected to give good agreement with experimentally determined scattering curves in the range of scattering vectors q < 2π/a. However, for q > 2π/a, this approach describes remarkably well experimentally measured high-resolution scattering curves...." A good description of the real scattering for low Q is shown in example :ref:`A nano cube build of different lattices` for the example of a cube crystal domain.. Examples -------- Structure factor for *hexagonal lattice* dependent on rmsd. :: import jscatter as js import numpy as np q = np.r_[0.02:1:800j] a = 50. R=15 sr=0.1 p = js.grace() beta=js.dA(np.vstack([q,np.exp(-(q*sr*R)**2)])) p.title('structure factor for hexagonal 2D lattice with a={0} nm'.format(a)) p.subtitle('with diffusive scattering and asymmetry factor beta') for i,rmsd in enumerate([1., 3., 10., 30.],1): grid=js.sf.hexLattice(50,5,5) hex = js.sf.latticeStructureFactor(q, rmsd=rmsd, domainsize=500., beta=beta,lattice=grid) p.plot(hex, li=[1, 2, i], sy=0, le='rmsd=$rmsd') p.plot(hex.X,1-hex._DW, li=[3, 2, i], sy=0) p.plot(hex.X, hex._beta, li=[2, 2, i], sy=0, le='beta') p.text(r'broken lines \nshow diffusive scattering',x=0.4,y=6) p.yaxis(label='S(q)') p.xaxis(label='q / nm') p.legend(x=0.6,y=4) **Comparison of sc, bcc, fcc** for same cubic unit cell size to demonstrate selection rules. :: import jscatter as js import numpy as np q=np.r_[js.loglist(0.1,3,200),3:40:800j] unitcelllength=1.5 N=2 R=0.5 sr=0.1 beta=js.dA(np.vstack([q,np.exp(-(q*sr*R)**2)])) rmsd=0.02 # scgrid= js.lattice.scLattice(unitcelllength,N) sc=js.sf.latticeStructureFactor(q, rmsd=rmsd, domainsize=50., beta=beta,lattice=scgrid) bccgrid= js.lattice.bccLattice(unitcelllength,N) bcc=js.sf.latticeStructureFactor(q, rmsd=rmsd, domainsize=50., beta=beta,lattice=bccgrid) fccgrid= js.lattice.fccLattice(unitcelllength,N) fcc=js.sf.latticeStructureFactor(q, rmsd=rmsd, domainsize=50., beta=beta,lattice=fccgrid) # p=js.grace() p.plot(sc,legend='sc') p.plot(bcc,legend='bcc') p.plot(fcc,legend='fcc') p.yaxis(label='S(q)',scale='l',max=50,min=0.05) p.xaxis(label='q / nm',scale='l',max=50,min=0.5) p.legend(x=1,y=30,charsize=1.5) # p.save(js.examples.imagepath+'/latticeStructureFactor2.jpg') .. image:: ../../examples/images/latticeStructureFactor2.jpg :align: center :height: 300px :alt: multiParDistributedAverage A realistic example of a **calibration measurement with AgBe**. We load the cif file of the crystal structure to build the lattice and find excellent agreement. According to materialsproject.org calculated XRD tends to underestimate lattice parameters. For AgBe the first peak is found at 1.07 and lamellar peaks go up to 12.9 because of the Ag atoms in lamellar stack. The broad strong peak 13.7-14 (and upwards) is caused by multiple not lamellar peaks from structure of Be atoms. :: import jscatter as js # # Look at raw calibration measurement calibration = js.sas.sasImage(js.examples.datapath+'/calibration.tiff') bc=calibration.center calibration.mask4Polygon([bc[0]+8,bc[1]],[bc[0]-8,bc[1]],[bc[0]-8+60,0],[bc[0]+8+60,0]) # mask center calibration.maskCircle(calibration.center, 18) # mask outside shadow calibration.maskCircle([500,320], 280,invert=True) # calibration.show(axis='pixel',scale='log') cal=calibration.radialAverage() # lattice from crystallographic data in cif file. agbe=js.sf.latticeFromCIF(js.examples.datapath + '/1507774.cif',size=[0,0,0]) sfagbe=js.sf.latticeStructureFactor(np.r_[cal.X, cal.X[-1]:20:30j], lattice=agbe, domainsize=50, rmsd=0.001, lg=1, hklmax=17,wavelength=0.15406) p=js.grace() p.plot(cal) # add scaling and background (because of unscaled raw data) p.plot(sfagbe.X,190*sfagbe.Y+1.9,sy=0,li=[1,3,4]) p.yaxis(scale='log',label='I(q) / counts/pixel') p.xaxis(scale='log',label='q / nm|S-1',min=0.7,max=20) p.title('AgBe reference measurements') # p.save(js.examples.imagepath+'/latticeStructureFactor.jpg') .. image:: ../../examples/images/latticeStructureFactor.jpg :align: center :height: 300px :alt: multiParDistributedAverage **Anisotropic Debye Waller factor** We look at a hexagonal lyotropic phase of long cylinders (e.g. micelles) and calculate the structure factor. The formfactor amplitude influence is missing here. This cannot be included by multiplying with the orientational averaged fomfactor as the cylinders keep relative orientation. To include the formfactor amplitude look at the next example The dislocation along the cylinder axis direction might be larger than within the hexagonal plane. We might have dislocations and length polydispersity leading to different rmsd in crystal orientations. :: import jscatter as js import numpy as np q = js.loglist(0.03,20,800) # grid = js.lattice.hexLattice(3,25,5) # stronger rmsd along cylinder axis hex = js.sf.latticeStructureFactor(q, rmsd=[0.01,0.01,1.5], domainsize=50.,lattice=grid) p=js.grace() #p.plot(hex0) p.plot(hex,sy=0,li=[1,2,1]) p.yaxis(scale='log',label='I(q) / counts/pixel',min=0.1,max=200) p.xaxis(scale='log',label='q / nm|S-1',min=0.05,max=20) # p.subtitle(r'Structure factor of a hexagonal lattice \nfilled with long cylinders') p.text('lamellar plane peaks ',x=0.15,y=80) p.text('hexagonal peaks ',x=2,y=10) # p.save(js.examples.imagepath+'/hexLyotropicPhaseWithDisorder.jpg') .. image:: ../../examples/images/hexLyotropicPhaseWithDisorder.jpg :align: center :height: 300px :alt: latticeStructureFactor **Lyotropic hexagonal phase in 3D** Opposite to the above we include here the cylinder shape in the hexagonal lyotropic unit cell. We add in the unit cell a dummy atom representation of the cylinder with good enough resolution. This is used to calculate the :math:`f_{hkl}^2` which represents the formfactor amplitude in hkl direction. This type of scattering is often described as 2D hexagonal lattice with 2D disc formfactor assuming an infinite long cylinder. Here we could add ellipsoids or other anisotropic shapes and inhomogeneous particles. We explicitly observe here suppression of higher order hexagonal peaks. The first hexagonal peak at :math:`2.46 nm^{-1}` show a shift due to the strong decrease on the ff amplitude. The general increase is due to diffuse scattering. :: import jscatter as js import numpy as np q = js.loglist(0.03,20,800) # create lattice vectors in a simple way hexgrid = js.sf.hexLattice(3,20,1) # create cylinder with atoms in much shorter distance than hex lattice cylinder = js.sf.scLattice(0.3,[6, 6, 30]) cylinder.move([0,0,9]) cylinder.inCylinder(v=[0,0,1], R=0.5, a=[0,0,0.5], length=15, b=0, invert=True) # Convert the cylinder points coordinates to # fractional unit cell coordinates of the hexagonal lattice mat = np.linalg.inv(hexgrid.latticeVectors).T cellAtoms = mat @ cylinder.XYZ.T # create lattice with cylinder unit cell atoms grid=js.sf.rhombicLattice(latticeVectors=hexgrid.latticeVectors, size=[1,1,1], unitCellAtoms=cellAtoms.T, b=cylinder.b) q = np.r_[0.1:25:1000j] hex = js.sf.latticeStructureFactor(q,rmsd= [0.001,0.001,1.], domainsize=50, lattice=hexgrid) hexff = js.sf.latticeStructureFactor(q,rmsd=[0.001,0.001,1.], domainsize=50, lattice=grid) ffcylinder = js.ff.cloudScattering(q,cloud=cylinder) p=js.grace() p.plot(hex,sy=0,li=[1,1,1],le='structurefactor 1 atom unit cell') p.plot(hexff, sy=0,li=[1,3,2], le='+ cylinder in unit cell ') p.plot(ffcylinder.X, ffcylinder.Y *100 , sy=0, li=[3,1.5,4], le='cyl. formfactor') p.yaxis(scale='log',label='I(q)',min=0.03,max=200) p.xaxis(scale='log',label='q / nm\S-1',min=0.05,max=20) # p.title(r'Scattering of a hexagonal lattice') p.subtitle(r'filled with long cylinders, comparison with formfactor') p.text('lamellar plane peaks ',x=0.1,y=0.5) p.text(r'hexagonal peaks \nsuppressed by \nformfactor',x=3,y=10) p.legend(x=0.7,y=200) # p.save(js.examples.imagepath+'/hexLyotropicPhaseWithDisorder2.jpg') .. image:: ../../examples/images/hexLyotropicPhaseWithDisorder2.jpg :align: center :height: 300px :alt: hexLyotropicPhaseWithDisorder2 .. _latticeStructureFactor as a fit model: **latticeStructureFactor as a fit model** We include the possibility of polydispersity. We use a hexagonal lattice with small hex_a lattice constant and large hex_c to mimic a lamellar structure with lattice constant 5.833 nm as found for AgBe with main scattering coming from Ag atoms in a plane (z=0). The fit results are not as good as the above AgBe example. The fit can be improved limiting it to Q<7. This highlights the importance of the atom distribution in the unit cell in the example above. :: import jscatter as js # smearing even for SAXS here with a single width (for one of our SAXS machines). fbeam_12=js.sas.prepareBeamProfile(0.035) def hexSF(q, hex_c,hex_a, domainsize, rmsd,): # hexagonal structure factor # first make a lattice (size is later ignored) hex = js.sf.hexLattice(ab=hex_a,c=hex_c,size=5) # then calculate the structure factor and return it sf = js.sf.latticeStructureFactor(q=q, lattice=hex, domainsize=domainsize, hklmax=17, rmsd=rmsd, wavelength=0.15406) return sf # This includes a beamprofile for smearing (may be ommited) @js.sas.smear(beamProfile=fbeam_12) def hexmodel(q, hex_c,hex_a,dc, domainsize, rmsd, bgr, I0): if dc >0: # include a polydispersity in lattice constant, or wavelength or whatever is reasonable # also multiple parameters are possible using mPDA result = js.formel.pDA(hexSF, dc, 'hex_c',q=q,hex_a=hex_a,hex_c=hex_c,domainsize=domainsize,rmsd=rmsd) else: # no polydispersity, do it direct result = hexSF(q=q,hex_a=hex_a,hex_c=hex_c,domainsize=domainsize,rmsd=rmsd) result.Y=I0*result.Y+bgr return result # Use data from agbe from above example calibration = js.sas.sasImage(js.examples.datapath+'/calibration.tiff') bc=calibration.center calibration.mask4Polygon([bc[0]+8,bc[1]],[bc[0]-8,bc[1]],[bc[0]-8+60,0],[bc[0]+8+60,0]) calibration.maskCircle(calibration.center, 18) calibration.maskCircle([500,320], 280,invert=True) cal=calibration.radialAverage() cal.makeErrPlot(xscale='log',yscale='log') cal.setlimit(bgr=[0]) cal.fit(model=hexmodel, freepar={'hex_c':5.8, 'domainsize':50,'rmsd':0.1,'bgr': 2,'I0':3}, fixpar={'hex_a':0.5,'dc':0,}, mapNames={'q': 'X'}, method='Nelder-Mead', # Nelder-Mead is better for these cases condition=lambda a:(a.X>0)&(a.X<10)) References ---------- .. [1] Scattering curves of ordered mesoscopic materials. Förster, S. et al. J. Phys. Chem. B 109, 1347–1360 (2005). .. [2] Patterson, A. The Scherrer Formula for X-Ray Particle Size Determination Phys. Rev. 56 (10): 978–982 (1939) doi:10.1103/PhysRev.56.978. .. [3] M. Kotlarchyk and S.-H. Chen, J. Chem. Phys. 79, 2461 (1983).1 .. [4] https://en.wikipedia.org/wiki/Thomson_scattering .. [5] Modern Physical Metallurgy chapter 5 Characterization and Analysis R.E.SmallmanA.H.W.Ngan https://doi.org/10.1016/B978-0-08-098204-5.00005-5 .. [6] Atomic displacement parameters and anisotropic thermal ellipsoid lengths and angles Kronenburg M. J. Acta Cryst. (2004). A60, 250-256, DOI: 10.1107/S0108767304007688 """ if corrections == 'all' or 'all' in corrections: corrections = ['TP', 'lh', 'LC', 'area'] qq = q.copy() qq[q == 0] = min(q[q > 0]) * 1e-4 # avoid zero n = len(lattice.unitCellAtoms) vd = lattice.unitCellVolume dim = len(lattice.latticeVectors) qhkl, f2hkl, mhkl, hkl = lattice.getRadialReciprocalLattice(hklmax) # assert dim == 3, 'latticeStructureFactor currently only for 3D lattices.' if isinstance(rmsd, numbers.Number): # lattice factor if useFortran: # factor 3 faster for single cpu, additional factor 3 for multiprocessing (on 6 core) Z0q = fscatter.utils.sumlhklvoigt(qq, qhkl, f2hkl, mhkl, lg, domainsize, asym, dim, c, n, vd, 0) else: Z0q = np.c_[[m * f2 * _LhklVoigt(qq, qr, lg, domainsize, asym) for qr, f2, m in zip(qhkl, f2hkl, mhkl)]].sum(axis=0) Z0q *= (2 * np.pi) ** (dim - 1) * c / n / vd / qq ** (dim - 1) # normalisation Z0q = Z0q / np.sum(np.r_[lattice.unitCellAtoms_b]**2) if beta is None: beta = np.ones_like(q) elif hasattr(beta, '_isdataArray'): beta = beta.interp(q) # Debye Waller factor DW = np.exp(-q ** 2 * rmsd ** 2) # structure factor Sq = 1 + beta * (Z0q - 1) * DW elif len(rmsd) == 3: U = np.diag(rmsd)**2 # diagonal matrix of squared displacements prefactor = (2 * np.pi) ** (dim - 1) * c / n / vd / qq ** (dim - 1) Z0qhkl = np.zeros([qhkl.shape[0], qq.shape[0]]) DW = np.zeros_like(qq) for i, (qr, f2, m, _hkl) in enumerate(zip(qhkl, f2hkl, mhkl, hkl)): # Debye Waller factor in hkl direction v = _hkl @ lattice.reciprocalVectors nhkl = v/ la.norm(v) # norm in hkl direction dw = np.exp(- qq ** 2 * (nhkl @ U @ nhkl)) DW += dw Z0qhkl[i] = prefactor * m * f2 * dw * _LhklVoigt(qq, qr, lg, domainsize, asym) Z0q = Z0qhkl.sum(axis=0) / np.sum(np.r_[lattice.unitCellAtoms_b] ** 2) Z0q = Z0q + (1 - DW/Z0qhkl.shape[0]) DW = DW/qhkl.shape[0] beta = np.ones_like(q) # structure factor Sq = Z0q else: raise TypeError('rmsd should be float or 3xfloat. Not ', rmsd) if wavelength is None: # prepare result result = dA(np.vstack([q, Sq, DW, beta, Z0q])) result.columnname = 'q; Sq; DW; beta; Z0q' else: theta = 2 * np.arcsin(qq * wavelength / 4. / np.pi) correction = np.ones_like(Sq) if 'TP' in corrections: correction = correction * (1 + np.cos(theta) ** 2) / 2 if 'LC' in corrections: correction = correction / np.sin(theta) if 'area' in corrections: correction = correction / np.sin(theta) if 'lh' in corrections: correction = correction * np.cos(theta / 2) # prepare result result = dA(np.vstack([q, Sq * correction, DW, beta, Z0q, correction, theta])) result.columnname = 'q; Sq; DW; beta; Z0q; TPf; theta' result.setColumnIndex(iey=None) result.q_hkl = qhkl result.fhkl = f2hkl result.sumfi2 = np.sum(np.r_[lattice.unitCellAtoms_b] ** 2) result.mhkl = mhkl result.hkl = hkl if wavelength is not None: result.Braggtheta = lattice.getScatteringAngle(size=hklmax, wavelength=wavelength) result.latticeconstants = la.norm(lattice.latticeVectors, axis=1) result.peakFWHM = 2 * np.pi / domainsize result.peaksigma = (result.peakFWHM / (2 * np.sqrt(2 * np.log(2)))) result.peakAsymmetry = asym result.domainsize = domainsize result.rmsd = rmsd result.lorenzianOverGaussian = lg result.modelname = inspect.currentframe().f_code.co_name return result
[docs] def radial3DLSF(qxyz, lattice=None, domainsize=1000, asym=0, lg=1, rmsd=0.02, beta=None, hklmax=7, c=1., wavelength=None, corrections=[]): r""" 3D structure factor S(q) in powder average of a crystal lattice with particle asymmetry, DebyeWaller factor, diffusive scattering and broadening due to domain size. The qxyz can be an arbitrary composition of points in reciprocal space. Uses latticeStructureFactor. The peak shape is a Voigt function. Parameters ---------- qxyz : 3xN array Wavevector plane in inverse units of lattice constant, units 1/A or 1/nm. domainsize : float Domainsize of the crystal, units as lattice constant of lattice. According to Debye-Scherrer equation :math:`fwhm=2\pi/domainsize` the peak width is determined [2]_. lattice : lattice object The crystal structure as defined in a lattice object. The size of the lattice is ignored. One of rhombicLattice, bravaisLattice, scLattice, bccLattice, fccLattice, diamondLattice, hexLattice, hcpLattice. See respective definitions. lg : float, default = 1 Lorenzian/gaussian fraction describes the contributions of gaussian and lorenzian shape in peak shape. - lorenzian/gaussian >> 1 lorenzian, - lorenzian/gaussian ~ 1 central part gaussian, outside lorenzian wings - lorenzian/gaussian << 1 gaussian asym : float, default=0 Asymmetry factor in sigmoidal as :math:`2fwhm/(1+e^{asym*(x-center)})` For asym=0 the Voigt is symmetric with fwhm. See formel.voigt . rmsd : float, default=0.02 Root mean square displacement :math:`rmsd=<u^2>^{0.5}` determining the Debye Waller factor. Units as domainsize and lattice units. Here Debye Waller factor is used as :math:`DW(q)=e^{-q^2 rmsd^2 }` beta : float, None, dataArray Asymmetry factor of the formfactor or reduction due to polydispersity. - None beta=1, No beta assumed (spherical symmetric formfactor, no polydispersity) - dataArray explicitly given as dataArray with beta in .Y column. Missing values are interpolated. - An approximation for polydisperse beta can be found in [1]_ equ.17. This can be realized by beta=js.dA(np.vstack(q,np.exp(-(q*sr*R)**2))) with sr as relative standard deviation of gaussian distribution of the size R. - See .formfactor for different formfactors which explicit calculation of beta. hklmax : int Maximum order of the Bragg peaks to include. c : float, default=1 Porod constant. See 3.8 in [1]_. wavelength : float, default = None Wavelength of the measurement in units nm. If None .Braggtheta is not calculated. For Xray Cu K_a it is 0.15406 nm. corrections : list, default=[] List of corrections to apply, which depend on the measurement type/geometry. See :py:func:`~.sf.latticeStructureFactor` Returns ------- dataArray Columns [qx,qz,qw,Sq] - Sq = S(q) = 1+beta(q)*(Z0(q)-1)*DW(q) structure factor Attributes - .q_hkl peak positions - .fhkl symmetry factor - .mhkl multiplicity Notes ----- See latticeStructureFactor. Examples -------- :: import jscatter as js import numpy as np import matplotlib.pyplot as pyplot from matplotlib import cm from matplotlib import colors norm=colors.LogNorm(clip=True) # create lattice sclattice = js.lattice.scLattice(2.1, 1) ds = 50 # add flat detector xy plane xzw = np.mgrid[-8:8:500j, -8:8:500j] qxzw = np.stack([np.zeros_like(xzw[0]), xzw[0], xzw[1]], axis=0) ff1 = js.sf.radial3DLSF(qxzw.reshape(3, -1).T, sclattice, domainsize=ds, rmsd=0.03, hklmax=7) norm.autoscale(ff1.Y) fig = pyplot.figure() ax = fig.add_subplot(1, 1, 1) im = ax.imshow(ff1.Y.reshape(500,-1),norm=norm) fig.colorbar(im, shrink=0.8) js.mpl.show() Note that for to low number of points in the xzw plane Moiré patterns appear. :: import jscatter as js import numpy as np import matplotlib.pyplot as pyplot from matplotlib import cm # Set the aspect ratio to 1 so our sphere looks spherical fig = pyplot.figure(figsize=pyplot.figaspect(1.)) ax = fig.add_subplot(111, projection='3d') # create lattice sclattice = js.lattice.scLattice(2.1, 1) ds = 50 # add flat detector xy plane xzw = np.mgrid[-8:8:250j, -8:8:250j] qxzw = np.stack([np.zeros_like(xzw[0]), xzw[0], xzw[1]], axis=0) ff1 = js.sf.radial3DLSF(qxzw.reshape(3, -1).T, sclattice, domainsize=ds, rmsd=0.03, hklmax=7) ffs1 = ff1.Y # np.log(ff1.Y) fmax, fmin = ffs1.max(), ffs1.min() ff1Y = (np.reshape(ffs1, xzw[0].shape) - fmin) / (fmax - fmin) ax.plot_surface(qxzw[0], qxzw[1], qxzw[2], rstride=1, cstride=1, facecolors=cm.gist_ncar(ff1Y), alpha=0.3) qxzw = np.stack([xzw[0]+8, np.zeros_like(xzw[0])+8, xzw[1]], axis=0) ff2 = js.sf.radial3DLSF(qxzw.reshape(3, -1).T, sclattice, domainsize=ds, rmsd=0.03, hklmax=7) ffs2 = ff2.Y #np.log(ff2.Y) fmax, fmin = ffs2.max(), ffs2.min() ff2Y = (np.reshape(ffs2, xzw[0].shape) - fmin) / (fmax - fmin) ax.plot_surface(qxzw[0], qxzw[1], qxzw[2], rstride=1, cstride=1, facecolors=cm.gray(ff2Y), alpha=0.3) ax.set_xlabel('x axis') ax.set_ylabel('y axis') ax.set_zlabel('z axis') fig.suptitle('Scattering planes of simple cubic lattice \nin powder average') pyplot.show(block=False) References ---------- .. [1] Scattering curves of ordered mesoscopic materials. Förster, S. et al. J. Phys. Chem. B 109, 1347–1360 (2005). .. [2] Patterson, A. The Scherrer Formula for X-Ray Particle Size Determination Phys. Rev. 56 (10): 978–982 (1939) doi:10.1103/PhysRev.56.978. .. [3] M. Kotlarchyk and S.-H. Chen, J. Chem. Phys. 79, 2461 (1983).1 """ qr = np.linalg.norm(qxyz, axis=1) qx = np.r_[0:np.max(qr):1j * 2 * np.mean(qxyz.shape) ** 0.5] # radial lSF lsf = latticeStructureFactor(q=qx, lattice=lattice, domainsize=domainsize, asym=asym, lg=lg, rmsd=rmsd, beta=beta, hklmax=hklmax, c=c, wavelength=wavelength, corrections=corrections) # prepare result for 3D result = dA(np.c_[qxyz, lsf.interp(qr)].T) # copy attributes from lsf result.setattr(lsf) result.setColumnIndex(ix=0, iz=1, iw=2, iy=3) result.modelname = inspect.currentframe().f_code.co_name return result
# Bragg peak shape as Gaussian def _Lhkl(q, center, pWsigma): # Gaussian peak at center with width pWsigma Lhkl = np.multiply.reduce(np.exp(-0.5 * ((q - center) / pWsigma) ** 2) / pWsigma / np.sqrt(2 * np.pi), axis=1) return Lhkl def _Z0q(qxyz, qpeaks, f2peaks, peakWidthSigma, rotvector, angle=0, ncpu2=0): # calculates scattering intensity in direction qhkl as 3d q vectors # qpeaks are 3d peak positions # f2peaks are peak intensities # peakWidthSigma gaussian width # rotvector , angle: rotate q by angle around rotvector is the same as rotate crystal # ncpu2 parallel cores only used with Fortran (the 2 prevent usage of multiprocessing in pDA) # rotate qxyz if rotvector is not None and angle != 0: # As we rotate here the qhkl instead of the lattice angle gets a minus sign R = formel.rotationMatrix(rotvector, -angle) rqxyz = np.einsum('ij,kj->ki', R, qxyz) else: rqxyz = qxyz.copy() # calc Z0q if useFortran: # Z0q = np.c_[[f2 * fscatter.cloud.lhkl(rqxyz, q, peakWidthSigma) # for q, f2 in zip(qpeaks, f2peaks) if la.norm(q)>0]].sum(axis=0) # 10% faster than above qpnorm = la.norm(qpeaks, axis=1) Z0q = fscatter.utils.sumlhklgauss(rqxyz, qpeaks[qpnorm > 0, :], peakWidthSigma, f2peaks[qpnorm > 0], ncpu=ncpu2) else: Z0q = np.c_[[f2 * _Lhkl(rqxyz, q, peakWidthSigma) for q, f2 in zip(qpeaks, f2peaks) if la.norm(q) > 0]].sum(axis=0) return Z0q
[docs] def orientedLatticeStructureFactor(qxyz, lattice, rotation=None, domainsize=1000, rmsd=0.02, beta=None, hklmax=3, nGauss=13, ncpu=0, wavelength=None, corrections=[]): r""" 3D Structure factor S(q) of an oriented crystal lattice including particle asymmetry, DebyeWaller factor, diffusive scattering, domain rotation and domain size. To get the full scattering the formfactor needs to be included (See Notes and Examples). 1-3 dimensional lattice structures with basis containing multiple atoms (see lattice). To orient the crystal lattice use lattice methods .rotatehkl2Vector and .rotateAroundhkl Parameters ---------- qxyz : array 3xN Wavevector array representing a slice/surface in 3D q-space, 1/nm. This can describe a detector plane, section of the Ewald sphere or a line in reciprocal space. lattice : lattice object Lattice object with arbitrary atoms/particles in the unit cell, or predefined lattice from rhombicLattice, bravaisLattice, scLattice,bccLattice, fccLattice, diamondLattice, hexLattice, hcpLattice with scattering length of unit cell atoms. See lattices for examples. rotation : 4x float as [h,k,l,sigma], None Rotation of the crystal around axis hkl to get the average of a distribution of orientations. Uses a Gaussian distribution of width sigma (units rad) around actual orientation to integrate. For 2D lattices the (l) index corresponds to norm vector perpendicular to the plane. Rotation is not defined for 1D. domainsize : float,list, list of directions Domainsize of the crystal, units as lattice constant of lattice. According to Debye-Scherrer equation :math:`fwhm=2\pi/domainsize` the peak width is determined [2]_. - float : assume same domainsize in all directions. - list 3 float : domainsize in directions of latticeVectors. - list 4 x 3 : 3 times domainsize in hkl direction as [[size,h,k,l] ,[..],[..] ] [[3,1,1,1],[100,1,-1,0],[100,1,1,-2]] is thin in 111 direction and others are thick The user should take care that the directions are nearly orthogonal. rmsd : float, default=0.02 Root mean square displacement :math:`<u^2>^{0.5}` determining the Debye Waller factor. Units as lattice constant. beta : float, None, dataArray Asymmetry factor of the formfactor or reduction due to polydispersity. - None beta=1, No beta assumed (spherical symmetric formfactor, no polydispersity) - dataArray beta explicitly given as dataArray with beta in .Y column. Missing values are interpolated. - An approximation for polydisperse beta can be found in [1]_ equ.17. This can be realized by beta=js.dA(np.vstack(q,np.exp(-(q*sr*R)**2))) with sr as relative standard deviation of gaussian distribution of the size R. - See .formfactor for different formfactors which explicit calculation of beta. hklmax : int Maximum order of the Bragg peaks. wavelength : float, default = None Wavelength of the measurement in units nm. For Xray Cu K_a it is 0.15406 nm. corrections : list, default=[] List of corrections to apply, which depend on the measurement type/geometry. See :py:func:`~.structurefactor.ordered.latticeStructureFactor` nGauss : int, default 13 Number of points in integration over Gaussian for rotation width sigma. ncpu : int, optional Number of cpus in the pool. Set this to 1 if the integrated function uses multiprocessing to avoid errors. - not given or 0 -> all cpus are used - int>0 min (ncpu, mp.cpu_count) - int<0 ncpu not to use Returns ------- dataArray Columns [qx,qy,qz,Sq,DW,beta,Z0q] - q wavevector - Sq = S(q) = (1+beta(q)*(Z0(q)-1)*DW(q))*correction structure factor - DW(q) Debye-Waller factor with (1-DW)=diffusive scattering. - beta(q) asymmetry factor of the formfactor. - Z0q lattice factor Z0(q) optional - correction [optional] factor polarisation from Thompson scattering - theta scattering angle Attributes (+ input parameters) - .q_hkl peak positions - .hkl Miller indices - .peakFWHM full width half maximum Notes ----- - The scattering intensity of a crystal domain is .. math:: I(q)={\Delta\rho}^2 n P(q) S(q) with - :math:`\Delta\rho` scattering length difference between matrix and particles - :math:`n` number density (of elementary cells) - :math:`P(q)` form factor - :math:`S(q)` structure factor :math:`S(q)` For inhomogeneous particles we can incorporate :math:`\Delta\rho(r)` in the formfactor :math:`P(q)` if this includes the integrated scattering length differences. - The structure factor is [1]_ : .. math:: S(q)=1+ \beta(q)(Z_0(q)-1)*DW(Q) with - :math:`\beta(q)=<F(q)>^2/<F(q)^2>` as asymmetry factor [3]_ dependent on the scattering amplitude :math:`F(q)` and particle polydispersity - :math:`DW(q)` Debye Waller factor - The lattice factor is [1]_ : .. math :: Z_0(q) = \frac{(2\pi)^3}{mv} \sum\limits_{hkl}f_{hkl}^2L_{hkl}(q,g_{hkl}) with - :math:`g_{hkl}` peak positions - :math:`m` number of particles per unit cell - :math:`f_{hkl}` unit cell structure factor that takes into account symmetry-related extinction rules - :math:`v` volume of the unit cell - :math:`hkl` reflections - Unit cell structure factors :math:`f_{hkl}` are normalised that the lattice factor is normalised for infinite q to 1. With i as unit cell atoms at fractional position in the unit cell :math:`[x_i,y_i,z_i]` and scattering amplitude :math:`b_i` we get : .. math:: f_{hkl}^2 = \big(\sum_i b_i e^{-2\pi (hx_i+ky_i+lz_i)}\big)^2 / \sum_i b_i^2 - The peak shape function is .. math :: L_{hkl}(q,g_{hkl}) = \frac{1}{ \sqrt{2\pi} \sigma} e^{-\frac{(q-g_{hkl})^2}{2\sigma^2}} with :math:`\sigma=fwhm/2\sqrt{2log(2)}` related to the domainsize. Correspondingly :math:`\sigma` is a vector describing the peak shapes in all directions. - Distributions of domain orientation are included by the parameter rotation that describes gaussian distributions with mean and sigma around an axis defined by the corresponding hkl indices. - DW is a Debye Waller like factor as :math:`DW(q)=e^{-q^2<u^2>}` leading to a reduction of scattered intensity and diffusive scattering. It has contributions from thermal lattice disorder ( DW factor with 1/3 factor in 3D). - To get the scattering of a specific particle shape the formfactor has to be included. The above is valid for isotropic scatterers (symmetric or uncorrelated to the crystal orientation) as only in this case we can separate structure factor and form factor. Examples -------- **Comparison fcc and sc** to demonstrate selection rules :: import jscatter as js import numpy as np R=8 N=50 ds=10 fcclattice= js.lattice.fccLattice(3.1, 5) qxy=np.mgrid[-R:R:N*1j, -R:R:N*1j].reshape(2,-1).T qxyz=np.c_[qxy,np.zeros(qxy.shape[0])].T fcclattice.rotatehkl2Vector([1,1,1],[0,0,1]) ffe=js.sf.orientedLatticeStructureFactor(qxyz,fcclattice,domainsize=ds,rmsd=0.1,hklmax=4) fig=js.mpl.surface(ffe.X,ffe.Z,ffe.Y) sclattice= js.lattice.scLattice(3.1, 5) sclattice.rotatehkl2Vector([1,1,1],[0,0,1]) ffs=js.sf.orientedLatticeStructureFactor(qxyz,sclattice,domainsize=ds,rmsd=0.1,hklmax=4) fig=js.mpl.surface(ffs.X,ffs.Z,ffs.Y) Comparison of different **domainsizes** dependent on direction of scattering The domainsize determines the lattice extension into a specific direction. :: import jscatter as js import numpy as np R=8 N=50 qxy=np.mgrid[-R:R:N*1j, -R:R:N*1j].reshape(2,-1).T qxyz=np.c_[qxy,np.zeros(qxy.shape[0])].T sclattice= js.lattice.scLattice(2.1, 5) # thin z ds1=[[20,1,0,0],[20,0,1,0],[5,0,0,1]] thin=js.sf.orientedLatticeStructureFactor(qxyz,sclattice,domainsize=ds1,rmsd=0.1,hklmax=2) # thin y ds2=[[20,1,0,0],[5,0,1,0],[20,0,0,1]] thick=js.sf.orientedLatticeStructureFactor(qxyz,sclattice,domainsize=ds2,rmsd=0.1,hklmax=2) fig = js.mpl.figure(figsize=[10,5]) ax0 = fig.add_subplot(1, 2, 1, projection='3d') js.mpl.surface(thin.X,thin.Z,thin.Y,ax=ax0) ax1 = fig.add_subplot(1, 2, 2, projection='3d') ax0.set_title('symmetric peaks: \nthin direction perpendicular to scattering plane') js.mpl.surface(thick.X,thick.Z,thick.Y,ax=ax1) ax1.set_title('asymmetric: \nthin direction parallel to scattering plane') ax0.view_init(70,40) ax1.view_init(70,40) js.mpl.pyplot.draw() #fig.savefig(js.examples.imagepath+'/orientedlatticeStructureFactor1.jpg') .. image:: ../../examples/images/orientedlatticeStructureFactor1.jpg :align: center :height: 300px :alt: orientedlatticeStructureFactor asymetric peaks **Rotation along axis** [1,1,1] leading to broadened peaks. It looks spiky because of low number of points in xy plane. To improve this the user can use more points, which needs longer computing time :: import jscatter as js import numpy as np # make xy grid in q space R=8 # maximum N=800 # number of points ds=15; qxy=np.mgrid[-R:R:N*1j, -R:R:N*1j].reshape(2,-1).T qxyz=np.c_[qxy,np.zeros(qxy.shape[0])].T # add z=0 component # create sc lattice which includes reciprocal lattice vectors and methods to get peak positions sclattice= js.lattice.scLattice(3.1, 5) # Orient 111 direction perpendicular to qxy plane sclattice.rotatehkl2Vector([1,1,1],[0,0,1]) ffs=js.sf.orientedLatticeStructureFactor(qxyz,sclattice, rotation=[1,1,1,np.deg2rad(10)], domainsize=ds,rmsd=0.1,hklmax=2,nGauss=23) fig=js.mpl.surface(ffs.X,ffs.Z,ffs.Y) fig.axes[0].view_init(70,40) js.mpl.pyplot.draw() #fig.savefig(js.examples.imagepath+'/orientedlatticeStructureFactor.jpg') .. image:: ../../examples/images/orientedlatticeStructureFactor.jpg :align: center :height: 300px :alt: orientedlatticeStructureFactor Scattering of a slightly tilted **2D hexagonal plane** showing partly the scattering lines in reciprocal space. For 2D planes the Bragg peaks become Bragg lines in reciprocal space that result in elongated scattering patterns when intersecting with the scattering plane. Remember to use the real Ewald sphere. The missing peaks in the x-plane corner are because of hklmax=11. Homework : try with ``rotation = [1,1,1,np.deg2rad(10)]`` :: import jscatter as js import numpy as np R=8 # maximum N=200 # number of points ds=40; hex2D_lattice= js.lattice.hex2DLattice(9, 5) hex2D_lattice.rotatehkl2Vector([1,1], [0,60,4]) hex2D_lattice.show() q = np.mgrid[-R:R:200*1j, -R:R:200*1j].reshape(2,-1).T qz=np.c_[q,np.zeros_like(q[:,0])] # for z=0 qy=np.c_[q[:,:1],np.zeros_like(q[:,0]),q[:,1:]] # for z=0 qx=np.c_[np.zeros_like(q[:,0]),q] # for z=0 # rotation = [1,1,1,np.deg2rad(10)] ffz=js.sf.orientedLatticeStructureFactor(qz, hex2D_lattice, rotation=None, domainsize=ds, rmsd=0.1, hklmax=11) ffy=js.sf.orientedLatticeStructureFactor(qy, hex2D_lattice, rotation=None, domainsize=ds, rmsd=0.1, hklmax=11) ffx=js.sf.orientedLatticeStructureFactor(qx, hex2D_lattice, rotation=None, domainsize=ds, rmsd=0.1, hklmax=11) # show as cube surfaces ax=js.mpl.contourOnCube(ffz[[0,1,3]].array,ffx[[1,2,3]].array,ffy[[0,2,3]].array,offset=[-9,-9,9]) ax.set_title('2D hexagonal plane scattering in different directions') #ax.figure.savefig(js.examples.imagepath+'/contour2Dhex.jpg') .. image:: ../../examples/images/contour2Dhex.jpg :align: center :height: 300px :alt: contour2Dhex Scattering of a **line of scatterers**. Now we find scattering planes. :: import jscatter as js import numpy as np R=8 # maximum N=200 # number of points ds=20; hex2D_lattice= js.lattice.lamLattice(9, 5) hex2D_lattice.rotatehkl2Vector([1], [0,60,4]) # hex2D_lattice.show() q = np.mgrid[-R:R:200*1j, -R:R:200*1j].reshape(2,-1).T qz=np.c_[q,np.zeros_like(q[:,0])] # for z=0 qy=np.c_[q[:,:1],np.zeros_like(q[:,0]),q[:,1:]] # for z=0 qx=np.c_[np.zeros_like(q[:,0]),q] # for z=0 ffz=js.sf.orientedLatticeStructureFactor(qz, hex2D_lattice, domainsize=ds, rmsd=0.1, hklmax=11) ffy=js.sf.orientedLatticeStructureFactor(qy, hex2D_lattice, domainsize=ds, rmsd=0.1, hklmax=11) ffx=js.sf.orientedLatticeStructureFactor(qx, hex2D_lattice, domainsize=ds, rmsd=0.1, hklmax=11) # show as cube surfaces ax=js.mpl.contourOnCube(ffz[[0,1,3]].array,ffx[[1,2,3]].array,ffy[[0,2,3]].array,offset=[-9,-9,9]) ax.set_title('1D line scattering in different directions') #ax.figure.savefig(js.examples.imagepath+'/contour1Dlines.jpg') .. image:: ../../examples/images/contour1Dlines.jpg :align: center :height: 300px :alt: contour2Dhex References ---------- .. [1] Order causes secondary Bragg peaks in soft materials Förster et al.Nature Materials doi: 10.1038/nmat1995 .. [2] Patterson, A. The Scherrer Formula for X-Ray Particle Size Determination Phys. Rev. 56 (10): 978–982 (1939) doi:10.1103/PhysRev.56.978. .. [3] M. Kotlarchyk and S.-H. Chen, J. Chem. Phys. 79, 2461 (1983).1 """ if corrections == 'all' or 'all' in corrections: corrections = ['TP', 'lh', 'LC', 'area'] # check that qxyz is in 3xN shape if qxyz.shape[1] == 3 and qxyz.shape[0] != 3: # transpose qxyz = qxyz.T vd = lattice.unitCellVolume n = len(lattice.unitCellAtoms) dim = len(lattice.latticeVectors) # dimensionality # peakWidthSigma describes Bragg peak width as 3D vector relative to lattice if isinstance(domainsize, numbers.Number): domainsize = np.array([domainsize] * 3) fwhm = 2 * np.pi / np.abs(domainsize) peakWidthSigma = fwhm / (2 * np.sqrt(2 * np.log(2))) elif isinstance(domainsize, list): if np.ndim(domainsize) == 1: # use latticevector direction domainsize = np.atleast_1d(domainsize) # broadening due to domainsize in direction of latticeVectors fwhm = 2 * np.pi / np.abs(domainsize) sigma = fwhm / (2 * np.sqrt(2 * np.log(2))) peakWidthSigma = np.abs( np.sum([s * lV / la.norm(lV) for lV, s in zip(lattice.latticeVectors, sigma)], axis=1)) else: # we assume that width with Miller indices is given ds = np.array(domainsize) sigma = 2 * np.pi / np.abs(ds[:, 0]) / (2 * np.sqrt(2 * np.log(2))) # width as sigma # transform hkl to real directions by using the latticeVectors hkldirection = np.einsum('ij,lj', lattice.latticeVectors, ds[:, 1:4]) peakWidthSigma = np.abs(np.sum([s * lV / la.norm(lV) for lV, s in zip(hkldirection, sigma)], axis=1)) else: raise TypeError('domainsize cannot be interpreted.') # Debye Waller factor qr = la.norm(qxyz, axis=0) DW = np.exp(-qr ** 2 * rmsd ** 2) # reciprocal lattice peaks = lattice.getReciprocalLattice(hklmax) qpeaks = peaks[:, :3] # positions f2peaks = peaks[:, 3] # scattering intensity hkl = peaks[:, 4:] # hkl indices # mainly important for dim<3 # for 2D lattice the peaks are lines perpendicular to reciprocal vectors, for 1D these are planes # so we use only the projection of qxyz on the plane or line if dim == 3: pqxyz = qxyz elif dim == 2: # We need the projection of la rV1, rV2 = lattice.reciprocalVectors[:dim] rV3 = np.cross(rV1, rV2) rV3 /= la.norm(rV3) # normal to plane rV1,rV2 # projection in plane pqxyz = qxyz - rV3[:, None] * np.dot(qxyz.T, rV3) elif dim == 1: rV1 = lattice.reciprocalVectors[0] / la.norm(lattice.reciprocalVectors[0]) # projection along normal vector pqxyz = rV1[:, None] * np.dot(qxyz.T, rV1) rotation = None # determine rotation vector from hkl if rotation is not None and la.norm(rotation[:3]) > 0: # rotation direction rotvector = lattice.vectorhkl(rotation[:dim]) if dim == 2: # for 2D we use normal perpendicular vector for (l) rV1, rV2 = lattice.reciprocalVectors[:dim] rV3 = np.cross(rV1, rV2) rV3 /= la.norm(rV3) rotvector = rotvector + rV3 * rotation[3] else: rotvector = None if rotation is not None and abs(rotation[3]) > 0: # gauss distribution of rotation angle Z0q = formel.parDistributedAverage(_Z0q, abs(rotation[3]), parname='angle', nGauss=nGauss, qxyz=pqxyz.T, qpeaks=qpeaks, f2peaks=f2peaks, peakWidthSigma=peakWidthSigma, rotvector=rotvector, angle=0, ncpu2=ncpu) else: # single orientation Z0q = _Z0q(qxyz=pqxyz.T, qpeaks=qpeaks, f2peaks=f2peaks, peakWidthSigma=peakWidthSigma, rotvector=rotvector, angle=0, ncpu2=ncpu) Z0q *= (2 * np.pi) ** dim / n / vd # normalisation Z0q = Z0q / np.sum(np.r_[lattice.unitCellAtoms_b]**2) if beta is None: beta = np.ones_like(qr) elif hasattr(beta, '_isdataArray'): beta = beta.interp(qr) # structure factor Sq = 1 + beta * (Z0q - 1) * DW if wavelength is None: # prepare result result = dA(np.vstack([qxyz, Sq, DW, beta, Z0q])) result.columnname = 'qx; qy; qz; Sq; DW; beta; Z0q' else: theta = 2 * np.arcsin(qr * wavelength / 4. / np.pi) # Thompson polarisation for electromagnetic scattering # https://en.wikipedia.org/wiki/Thomson_scattering correction = np.ones_like(Sq) if 'TP' in corrections: correction = correction * (1 + np.cos(theta) ** 2) / 2 if 'LC' in corrections: correction = correction / np.sin(theta) if 'area' in corrections: correction = correction / np.sin(theta) if 'lh' in corrections: correction = correction / np.cos(theta / 2) # prepare result result = dA(np.vstack([qxyz, Sq * correction, DW, beta, Z0q, correction, theta])) result.columnname = 'qx; qy; qz; Sq; DW; beta; Z0q; correction; theta' # prepare result result.setColumnIndex(iey=None, ix=0, iz=1, iw=2, iy=3) result.q_hkl = qpeaks result.hkl = hkl result.sumfi2 = np.sum(np.r_[lattice.unitCellAtoms_b] ** 2) result.peaksigma = peakWidthSigma result.domainsize = domainsize result.rmsd = rmsd result.rotation = rotation result.modelname = inspect.currentframe().f_code.co_name return result
# noinspection PyIncorrectDocstring
[docs] def radialorientedLSF(*args, **kwargs): r""" Radial averaged structure factor S(q) of an oriented crystal lattice calculated as orientedLatticeStructureFactor. For a detailed description and parameters see orientedLatticeStructureFactor. Additionally the qxyz plane according to orientedLatticeStructureFactor is radial averaged over qxyz. Parameters ---------- q : int, array Explicit list of q values or number of points between min and max wavevector values To large number results in noisy data as the average gets artificial. Each q points will be averaged in intervals around q neighbors from values in qxyz plane. Returns ------- dataArray Columns [q,Sq,DW,beta,Z0q] - q wavevector as norm(qx,qy,qz) - Sq = S(q) = 1+beta(q)*(Z0(q)-1)*DW(q) structure factor - DW(q) Debye-Waller factor with (1-DW)=diffusive scattering. - beta(q) asymmetry factor of the formfactor. - Z0q lattice factor Z0(q) Attributes (+ input parameters) - .q_hkl peak positions - .hkl Miller indices - .peakFWHM full width half maximum Notes ----- qxyz might be any number and geometrical distribution as plane or 3D cube. 3D qxyz points will be converted to qr=norm(qxyz) and averaged. Examples -------- :: import jscatter as js import numpy as np R=12 N=200 ds=10 fcclattice= js.lattice.fccLattice(3.1, 5) qxy=np.mgrid[-R:R:N*1j, -R:R:N*1j].reshape(2,-1).T qxyz=np.c_[qxy,np.zeros(N**2)].T q=np.r_[0.1:16:100j] p=js.grace() for rmsd in [0.07,0.03,0.01]: ffe=js.sf.radialorientedLSF(q=q,qxyz=qxyz,lattice=fcclattice,rotation=[1,1,1,np.deg2rad(10)],domainsize=ds,rmsd=rmsd,hklmax=6) p.plot(ffe,li=1,le=f'rmsd {rmsd}') p.legend(x=8,y=1.8) p.yaxis(label='S(Q)',min=0,max=2.2) p.xaxis(label='Q / nm\S-1') #p.save(js.examples.imagepath+'/radialorientedLSF.jpg') .. image:: ../../examples/images/radialorientedLSF.jpg :width: 50 % :align: center :alt: radialorientedLSF """ # get q values or number of values q = kwargs.pop('q', kwargs['qxyz'].shape[0] ** 0.5 / 2) olsf = orientedLatticeStructureFactor(*args, **kwargs) # set X to the value of radial wavevectors olsf[0] = np.linalg.norm(olsf[[olsf._ix, olsf._iz, olsf._iw]], axis=0) # cut z and w columns radial = olsf[[0, 3, 4, 5, 6]] radial.setColumnIndex(ix=0, iy=1, iey=None, iz=None, iw=None) radial.isort() # sorts along X by default if isinstance(q, numbers.Number): # return lower number of points from prune result = radial.prune(number=int(q), type='mean') else: # explicit given list of q values result = radial.prune(kind=q, type='mean', fillvalue = 0.) # force exact same Q values ignoring statistical mean result.X = q result.modelname = inspect.currentframe().f_code.co_name return result
def _Caille_single(q, N, d, CaiP): euler = np.euler_gamma k = np.r_[1:N] single = N + 2 * np.sum((N - k[:, None]) * np.cos(k[:, None] * q * d) * np.exp(-(q * d / (2 * np.pi)) ** 2 * CaiP * euler) * (np.pi * k[:, None]) ** (-(q * d / (2 * np.pi)) ** 2 * CaiP), axis=0) return single def _para_single(q, N, d, delta): k = np.r_[1:N] single = N + 2 * np.sum((N - k[:, None]) * np.cos(k[:, None] * q * d) * np.exp(-(k[:, None] * q * delta) ** 2 / 2), axis=0) return single def _diffuse_single(q, N, d, delta): k = np.r_[1:N] single = N + 2 * np.sum((N - k[:, None]) * np.cos(k[:, None] * q * d), axis=0) * np.exp(-(q * delta) ** 2 / 2) return single
[docs] def diffuseLamellarStack(q, d, N, dN, kind='caille', dc=0.1): r""" Bragg peaks and diffuse scattering from lamellar structures. Lamellar phases are characterized by scattering patterns containing pseudo-Bragg peaks from the layer ordering. Fluctuations of the lamellae cause different kinds of diffuse scattering. See [1]_ for details and references. Parameters ---------- q : array Wavevectors in units nm. d : float Layer spacing (distance between layers) in units nm. N : float, int Number of layers. Actually we use a Poisson distribution with mean N and standard deviation dN. For Large N this is similar to a Gaussian distribution and a valid ditribution for small N. dN : float Standard deviation from mean describing the width of the Poisson distribution. dN=0 no distribution. kind : 'difffuse', 'para', 'caille' Kind of distortions in the layers - 'diffuse' : Thermal disorder. [2]_ Each layer k fluctuates uncorrelated with an amplitude :math:`\Delta = (d_k - d)^2` using a common Debye Waller factor. Uncorrelated fluctuations of layers. - 'para' : paracrystalline model. [3]_ Small fluctuations of layer spacing :math:`\Delta` are considered, stacking disorder corresponding to a paracrystal of the second kind. The position of individual fluctuating layers are determined solely by its nearest neighbours. - 'caille' modified Caille. [4]_ Lamellar stacks, the fluctuations are quantified in terms of the flexibility of the membranes dependent on bulk compression modulus B and bending rigidity K of the lamellae. For low dN it approximates normal Caille dc : float Strength of fluctuations. - 'para' and 'diffuse' Fluctuations of amplitude relative to d. :math:`dc = \Delta/d` with :math:`\Delta = (d_k - d)^2`. - 'caille' Caille Parameter in modified Caille model. .. math:: dc = \eta = \frac{\pi kT}{2d(BK)^{1/2}} with bulk compression modulus B and bending rigidity K of the lamellae. Returns ------- dataArray : [q, Sq] Notes ----- Multi lamellar structures with disorder. See [1]_ - thermal disorder .. math:: S(Q) = N + 2 exp(-\frac{Q^2\Delta^2}{2}) \sum_{k=1}^{N-1} (N-k) cos(kQd) - paracrystalline theory .. math:: S(Q) = N + 2 \sum_{k=1}^{N-1} (N-k) cos(kQd) exp(-\frac{k^2Q^2\Delta^2}{2}) - Modified Caillé Theory .. math:: S(Q) = N + 2 \sum_{k=1}^{N-1} (N-k) cos(kQd) \ exp(-(\frac{Qd}{2\pi})^2 \eta \gamma) (\pi k)^{-(Qd/2\pi)^2 \eta} Distribution of stack sizes (for dN>0): .. math:: S(Q) = N + \sum_{N_k=pmf(0.001)}^{N_k=pmf(0.999)} w_k S_k(Q) using a Poisson distribution with probabilities :math:`w_k` for :math:`N_k` between percent point function (ppf) 0.001..0.999 . For reasonable large N the Poisson distribution approximates a Gaussian. Examples -------- Comparison of the kind's. See Fig 2 in [1]_ . :: import jscatter as js import numpy as np q = np.r_[js.loglist(0.01,1,100),js.loglist(1,8,300)] N = 10 d= 5 dN = 0.1 dc = 0.1 Sqcaille = js.sf.diffuseLamellarStack(q, d, N, dN, kind='caille', dc=dc) Sqpara = js.sf.diffuseLamellarStack(q, d, N, dN, kind='para', dc=dc) Sqdiffuse = js.sf.diffuseLamellarStack(q, d, N, dN, kind='diffuse', dc=dc) p = js.grace() p.plot(q, Sqcaille.Y*10,li=1,le='modified caille') p.plot(q, Sqpara.Y,li=2,le='para crystaline') p.plot(q, Sqdiffuse.Y*0.1,li=3,le='thermal disorder') p.xaxis(scale='l',label='q / nm\S-1') p.yaxis(scale='l',label='S(q) / a.u.',min=0.05,max=2000) p.legend(x=0.012,y=0.4) p.title('lamellar stacks with disorder') p.subtitle('N=10; dN=0.5; d=5; dc=0.1') # p.save(js.examples.imagepath+'/diffuseLamellarStack.jpg') .. image:: ../../examples/images/diffuseLamellarStack.jpg :width: 50 % :align: center :alt: idealhelix0 References ---------- .. [1] Diffuse scattering from lamellar structures Ian W. Hamley Soft Matter, 18, 711 (2022) DOI: 10.1039/d1sm01758f .. [2] Giacovazzo et al Fundamentals of Crystallography, International Union of Crystallography/Oxford University Press, Oxford, 1992. B. D. Cullity and S. R. Stock, Elements of X-ray Diffraction, Prentice Hall, Upper Saddle River, New Jersey, 2001. .. [3] I. W. Hamley, Small-Angle Scattering: Theory, Instrumentation, Data and Applications Wiley, Chichester, 2021. R. Hosemann and S. N. Bagchi, Direct Analysis of Diffraction by Matter, North-Holland, Amsterdam, 1962. .. [4] R. T. Zhang, R. M. Suter and J. F. Nagle, Phys. Rev. E, 1994, 50, 5047–5060. G. Pabst, M. Rappolt, H. Amenitsch and P. Laggner, Phys. Rev. E, 2000, 62, 4000–4009. """ if dN>0: distrib = stats.poisson(mu=N, loc=dN) x = np.arange(distrib.ppf(0.001), distrib.ppf(0.999)) wx = distrib.pmf(x) x = np.array(x, dtype=int) wx = wx / wx.sum() else: x = np.r_[N] wx = np.r_[1] if kind.startswith('c'): Sq = np.sum([w * _Caille_single(q, n, d, dc) for n, w in zip(x, wx)], axis=0) elif kind.startswith('p'): Sq = np.sum([w * _para_single(q, n, d, dc * d) for n, w in zip(x, wx)], axis=0) elif kind.startswith('d'): Sq = np.sum([w * _diffuse_single(q, n, d, dc * d) for n, w in zip(x, wx)], axis=0) result = dA(np.c_[q, Sq].T) result.columnname = 'q; Sq' result.modelname = inspect.currentframe().f_code.co_name result.fluctuations = dc result.numberofLayers = N result.sigmaNumberLayer = dN result.contributingN = x result.contributingNw = wx return result