def powdersim(crystal, q, fwhm_g=0.03, fwhm_l=0.06, **kwargs): """ Simulates polycrystalline diffraction pattern. Parameters ---------- crystal : `skued.structure.Crystal` Crystal from which to diffract. q : `~numpy.ndarray`, shape (N,) Range of scattering vector norm over which to compute the diffraction pattern [1/Angs]. fwhm_g, fwhm_l : float, optional Full-width at half-max of the Gaussian and Lorentzian parts of the Voigt profile. See `skued.pseudo_voigt` for more details. Returns ------- pattern : `~numpy.ndarray`, shape (N,) Diffraction pattern """ refls = np.vstack(tuple(crystal.bounded_reflections(q.max()))) h, k, l = np.hsplit(refls, 3) Gx, Gy, Gz = change_basis_mesh( h, k, l, basis1=crystal.reciprocal_vectors, basis2=np.eye(3) ) qs = np.sqrt(Gx ** 2 + Gy ** 2 + Gz ** 2) intensities = np.absolute(structure_factor(crystal, h, k, l)) ** 2 pattern = np.zeros_like(q) for qi, i in zip(qs, intensities): pattern += i * pseudo_voigt(q, qi, fwhm_g, fwhm_l) return pattern
def potential_synthesis(reflections, intensities, crystal, mesh): """ Synthesize the electrostatic potential from a list of experimental reflections and associated diffracted intensities. Diffraction phases are taken from a known structure Parameters ---------- reflections : iterable of tuples Iterable of Miller indices as tuples (e.g. `[(0,1,0), (0, -1, 2)]`) intensities : Iterable of floats Experimental diffracted intensity for corresponding reflections. crystal : crystals.Crystal Crystal that gave rise to the diffracted intensities. mesh : 3-tuple ndarrays, ndim 2 or ndim 3 Real-space mesh over which to calculate the scattering map. Format should be similar to the output of numpy.meshgrid. Returns ------- out : ndarray, ndim 2 or ndim 3 Electrostatic potential computed over the mesh. """ assert len(intensities) == len(reflections) intensities = np.array(intensities) if np.any(intensities < 0): raise ValueError("Diffracted intensity cannot physically be negative.") # We want to support 2D and 3D meshes, therefore # expand mesh until 4D (last dim is for loop over reflections) # Extra dimensions will be squeezed out later xx, yy, zz = mesh while xx.ndim < 4: xx, yy, zz = ( np.expand_dims(xx, xx.ndim), np.expand_dims(yy, yy.ndim), np.expand_dims(zz, zz.ndim), ) # Reconstruct the structure factors from experimental data # We need to compute the theoretical phases from the crystal structure # To do this, we need to change 'reflections' into three iterables: # h, k, and l arrays hs, ks, ls = np.hsplit(np.array(reflections), 3) theoretical_SF = structure_factor(crystal, hs, ks, ls) phases = np.angle(theoretical_SF) experimental_SF = np.sqrt(intensities) * np.exp(1j * phases) experimental_SF = experimental_SF.reshape((1, 1, 1, -1)) qx, qy, qz = change_basis_mesh(hs, ks, ls, basis1=crystal.reciprocal_vectors, basis2=np.eye(3)) qx, qy, qz = ( qx.reshape((1, 1, 1, -1)), qy.reshape((1, 1, 1, -1)), qz.reshape((1, 1, 1, -1)), ) p = np.sum(experimental_SF * np.cos(xx * qx + yy * qy + zz * qz), axis=3) return np.squeeze(np.real(p))
def structure_factor(crystal, h, k, l, normalized=False): """ Computation of the static structure factor for electron diffraction. Parameters ---------- crystal : Crystal Crystal instance h, k, l : array_likes or floats Miller indices. Can be given in a few different formats: * floats : returns structure factor computed for a single scattering vector * 3 coordinate ndarrays, shapes (L,M,N) : returns structure factor computed over all coordinate space normalized : bool, optional If True, the normalized structure factor :math`E` is returned. This is the statis structure factor normalized by the sum of form factors squared. Returns ------- sf : ndarray, dtype complex Output is the same shape as input G[0]. Takes into account the Debye-Waller effect. """ # Distribute input # This works whether G is a list of 3 numbers, a ndarray shape(3,) or # a list of meshgrid arrays. h, k, l = np.atleast_1d(h, k, l) Gx, Gy, Gz = change_basis_mesh(h, k, l, basis1=crystal.reciprocal_vectors, basis2=np.eye(3)) nG = np.sqrt(Gx**2 + Gy**2 + Gz**2) # Separating the structure factor into sine and cosine parts avoids adding # complex arrays together. About 3x speedup vs. using complex exponentials SFsin, SFcos = ( np.zeros(shape=nG.shape, dtype=np.float), np.zeros(shape=nG.shape, dtype=np.float), ) # Pre-allocation of form factors gives huge speedups atomff_dict = dict() for atom in crystal: # TODO: implement in parallel? if atom.element not in atomff_dict: atomff_dict[atom.element] = affe(atom, nG) x, y, z = atom.coords_cartesian arg = x * Gx + y * Gy + z * Gz # TODO: debye waller factor based on displacement atomff = atomff_dict[atom.element] SFsin += atomff * np.sin(arg) SFcos += atomff * np.cos(arg) SF = SFcos + 1j * SFsin if normalized: SF /= np.sqrt(sum(atomff_dict[atom.element]**2 for atom in crystal)) return SF
def potential_map(q, I, crystal, mesh): """ Compute the electrostatic potential from powder diffraction data. Parameters ---------- q : ndarray, shape (N,) Scattering vector norm (:math:`Å^{-1}`). I : ndarray, shape (N,) Experimental diffracted intensity. crystal : crystals.Crystal Crystal that gave rise to diffraction pattern `I`. mesh : 3-tuple ndarrays, ndim 2 or ndim 3 Real-space mesh over which to calculate the scattering map. Format should be similar to the output of numpy.meshgrid. Returns ------- out : ndarray, ndim 2 or ndim 3 Electrostatic potential computed over the mesh. Raises ------ ValueError: if intensity data is not strictly positive. Notes ----- To compute the scattering map from a difference of intensities, note that scattering maps are linear in *structure factor* norm. Thus, to get the map of difference data :code:`I1 - I2`: .. math:: I = (\sqrt{I_1} - \sqrt{I_2})^2 References ---------- .. [#] Otto et al., How optical excitation controls the structure and properties of vanadium dioxide. PNAS, vol. 116 issue 2, pp. 450-455 (2018). :DOI:`10.1073/pnas.1808414115` """ if np.any(I < 0): raise ValueError("Diffracted intensity cannot physically be negative.") # We want to support 2D and 3D meshes, therefore # expand mesh until 4D (last dim is for loop over reflections) # Extra dimensions will be squeezed out later xx, yy, zz = mesh while xx.ndim < 4: xx, yy, zz = ( np.expand_dims(xx, xx.ndim), np.expand_dims(yy, yy.ndim), np.expand_dims(zz, zz.ndim), ) # Prepare reflections # G is reshaped so that it is perpendicular to xx, yy, zz to enables broadcasting reflections = np.vstack(tuple(crystal.bounded_reflections(q.max()))) hs, ks, ls = np.hsplit(reflections, 3) SF = structure_factor(crystal, hs, ks, ls) # Extract structure factor with correction factors # Diffracted intensities add up linearly (NOT structure factors) qx, qy, qz = change_basis_mesh(hs, ks, ls, basis1=crystal.reciprocal_vectors, basis2=np.eye(3)) qx, qy, qz = ( qx.reshape((1, 1, 1, -1)), qy.reshape((1, 1, 1, -1)), qz.reshape((1, 1, 1, -1)), ) SF = SF.reshape((1, 1, 1, -1)) q_theo = np.squeeze(np.sqrt(qx ** 2 + qy ** 2 + qz ** 2)) theo_I = powdersim(crystal, q_theo) peak_mult_corr = np.abs(SF) ** 2 / theo_I exp_SF = np.sqrt(np.interp(q_theo, q, I)) * peak_mult_corr # Squeeze out extra dimensions (e.g. if mesh was 2D) potential_map = np.sum( exp_SF * np.real(np.exp(1j * np.angle(SF))) * np.cos(xx * qx + yy * qy + zz * qz), axis=3, ) return np.squeeze(potential_map)