Module diffpy.mpdf.magutils

functions to facilitate mPDF analysis.

Expand source code
#!/usr/bin/env python
##############################################################################
#
# diffpy.mpdf         by Frandsen Group
#                     Benjamin A. Frandsen benfrandsen@byu.edu
#                     (c) 2022 Benjamin Allen Frandsen
#                      All rights reserved
#
# File coded by:    Benjamin Frandsen and Parker Hamilton
#
# See AUTHORS.txt for a list of people who contributed.
# See LICENSE.txt for license information.
#
##############################################################################


"""functions to facilitate mPDF analysis."""

import copy
import re
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import least_squares
from diffpy.srreal.bondcalculator import BondCalculator
from diffpy.srreal.pdfcalculator import PDFCalculator
from scipy.signal import convolve, fftconvolve, correlate
from scipy.optimize import least_squares
import periodictable as pt

def generateAtomsXYZ(struc, rmax=30.0, strucIdxs=[0], square=False):
    """Generate array of atomic Cartesian coordinates from a given structure.

    Args:
        struc (diffpy.Structure object): provides lattice parameters and unit
            cell of the desired structure
        rmax (float): largest distance from central atom that should be
            included
        strucIdxs (python list): list of integers giving indices of magnetic
            atoms in the unit cell
        square (boolean): if not True, atoms within a given radius from the
            origin will be returned; if True, then the full grid will be
            returned rather than just a spherical selection.

    Returns:
        numpy array of triples giving the Cartesian coordinates of all the
            magnetic atoms. Atom closest to the origin placed first in array.

    Note: If square = True, this may have problems for structures that have
        a several distorted unit cell (i.e. highly non-orthorhombic).
    """
    if not square:
        magAtoms = struc[strucIdxs]
        bc = BondCalculator(rmax=rmax + np.linalg.norm(struc.lattice.stdbase.sum(axis=1)))
        bc.setPairMask(0, 'all', True, others=False)
        bc(magAtoms)
        r0 = struc.xyz_cartn[strucIdxs[0]]
        atoms = np.vstack([r0, r0 + bc.directions[bc.sites0 == 0]])

    else:
        ### generate the coordinates of each unit cell
        lat = struc.lattice
        unitcell = lat.stdbase
        cellwithatoms = struc.xyz_cartn[np.array(strucIdxs)]
        dim1 = int(np.round(rmax / np.linalg.norm(unitcell[0])))
        dim2 = int(np.round(rmax / np.linalg.norm(unitcell[1])))
        dim3 = int(np.round(rmax / np.linalg.norm(unitcell[2])))
        ocoords = np.mgrid[-dim1:dim1 + 1, -dim2:dim2 + 1, -dim3:dim3 + 1].transpose().ravel().reshape(
            (2 * dim1 + 1) * (2 * dim2 + 1) * (2 * dim3 + 1), 3)
        latos = np.dot(ocoords, unitcell)

        ### rearrange latos array so that [0, 0, 0] is the first one (for convenience)
        latos[np.where(np.all(latos == [0, 0, 0], axis=1))] = latos[0]
        latos[0] = np.array([0, 0, 0])

        ### create list of all atomic positions
        atoms = np.empty([len(latos) * len(cellwithatoms), 3])
        index = 0
        for lato in latos:
            for atompos in cellwithatoms:
                atoms[index] = lato + atompos
                index += 1

    return atoms


def generateSpinsXYZ(struc, atoms=np.array([[]]), kvecs=np.array([[0, 0, 0]]),
                     basisvecs=np.array([[0, 0, 1]]), origin=np.array([0, 0, 0]),
                     avgmom=np.array([0, 0, 0])):
    """Generate array of 3-vectors representing the spins in a structure.

    Args:
        struc (diffpy.Structure object): provides lattice parameters and unit
            cell of the desired structure
        atoms (numpy array): list of atomic coordinates of all the magnetic
            atoms of a given magnetic species in the structure
        avgmom (numpy array): three-vector giving the average moment for the
            magnetic species.
        kvecs (numpy array): list of three-vectors giving the propagation
            vector(s) of the magnetic structure
        basisvecs (numpy array): list of three-vectors describing the spin
            located at the spin origin.
        origin (numpy array): Cartesian coordinates specifying the origin
            to be used when calculating the phases of the spins.

    Returns:
        numpy array of triples giving the spin vectors of all the magnetic
            atoms, in the same order as the atoms array provided as input.

    """

    lat = struc.lattice
    rlat = lat.reciprocal()
    (astar, bstar, cstar) = (rlat.cartesian((1, 0, 0)), rlat.cartesian((0, 1, 0)),
                             rlat.cartesian((0, 0, 1)))
    i = 1j

    spins = 0 * atoms
    cspins = 0 * atoms + 0j * atoms
    if len(np.array(kvecs).shape) == 1:
        kvecs = [kvecs]
    if len(np.array(basisvecs).shape) == 1:
        basisvecs = [basisvecs]
    for idx, kvec in enumerate(kvecs):
        kcart = kvec[0] * astar + kvec[1] * bstar + kvec[2] * cstar
        phasefac = np.exp(-2.0 * np.pi * i * np.dot(atoms - origin, kcart))
        cspins += basisvecs[idx] * phasefac[:, np.newaxis]
    cspins += avgmom
    spins = np.real(cspins)

    if np.abs(np.imag(cspins)).max() > 0.0001:
        print((np.abs(np.imag(cspins)).max()))
        print('Warning: basis vectors resulted in complex spins.')
        print('Imaginary parts have been discarded.')

    return spins


def generateFromUnitCell(unitcell, atombasis, spinbasis, rmax=30.0):
    """Generate array of atomic Cartesian coordinates from a given structure.

    Args:
        unitcell (numpy array): np.array([avec, bvec, cvec])
        atombasis (numpy array): gives positions of magnetic atoms in
            fractional coordinates; np.array([pos1, pos2, pos3, ...])
        spinbasis (numpy array): gives orientations of the magnetic moments
            in the unit cell, in the same order as atombasis
        rmax (float): largest distance from central atom that should be
            included

    Returns:
        atoms = numpy array of triples giving the Cartesian coordinates of all
              the magnetic atoms. Atom closest to the origin placed first in
              array.
        spins = numpy array of triples giving the Cartesian coordinates of all
              the spins in the structure, in the same order as atoms.

    Note: This will only work well for structures that can be expressed with a
        unit cell that is close to orthorhombic or higher symmetry.
    """
    if len(np.array(atombasis).shape) == 1:
        atombasis = [atombasis]
    if len(np.array(spinbasis).shape) == 1:
        spinbasis = [spinbasis]
    cellwithatoms = np.dot(atombasis, unitcell)  ### check this
    radius = rmax + 15.0
    dim1 = int(np.round(radius / np.linalg.norm(unitcell[0])))
    dim2 = int(np.round(radius / np.linalg.norm(unitcell[1])))
    dim3 = int(np.round(radius / np.linalg.norm(unitcell[2])))

    ### generate the coordinates of each unit cell
    ocoords = np.mgrid[-dim1:dim1 + 1, -dim2:dim2 + 1, -dim3:dim3 + 1].transpose().ravel().reshape(
        (2 * dim1 + 1) * (2 * dim2 + 1) * (2 * dim3 + 1), 3)
    latos = np.dot(ocoords, unitcell)

    ### select points within a desired radius from origin
    latos = latos[np.where(np.apply_along_axis(np.linalg.norm, 1, latos) <=
                           (rmax + np.linalg.norm(unitcell.sum(axis=1))))]

    ## rearrange latos array so that [0, 0, 0] is the first one (for convenience)
    latos[np.where(np.all(latos == [0, 0, 0], axis=1))] = latos[0]
    latos[0] = np.array([0, 0, 0])

    ### create list of all atomic positions
    atoms = np.empty([len(latos) * len(cellwithatoms), 3])
    spins = np.empty_like(atoms)
    index = 0
    for lato in latos:
        for j, atompos in enumerate(cellwithatoms):
            atoms[index] = lato + atompos
            spins[index] = spinbasis[j]
            index += 1
    return atoms, spins


def getFFparams(name, j2=False):
    """Get list of parameters for approximation of magnetic form factor

    Args:
        name (str): Name of magnetic ion in form 'Mn2' for Mn2+, etc.
        j2 (boolean): True of the j2 approximation should be calculated;
            otherwise, the j0 approximation is calculated.

    Returns:
        Python list of the 7 coefficients in the analytical approximation
            given at e.g. https://www.ill.eu/sites/ccsl/ffacts/ffachtml.html
    """
    if not j2:
        j0dict = {'Am2': [0.4743, 21.7761, 1.58, 5.6902, -1.0779, 4.1451, 0.0218],
                  'Am3': [0.4239, 19.5739, 1.4573, 5.8722, -0.9052, 3.9682, 0.0238],
                  'Am4': [0.3737, 17.8625, 1.3521, 6.0426, -0.7514, 3.7199, 0.0258],
                  'Am5': [0.2956, 17.3725, 1.4525, 6.0734, -0.7755, 3.6619, 0.0277],
                  'Am6': [0.2302, 16.9533, 1.4864, 6.1159, -0.7457, 3.5426, 0.0294],
                  'Am7': [0.3601, 12.7299, 1.964, 5.1203, -1.356, 3.7142, 0.0316],
                  'Ce2': [0.2953, 17.6846, 0.2923, 6.7329, 0.4313, 5.3827, -0.0194],
                  'Co0': [0.4139, 16.1616, 0.6013, 4.7805, -0.1518, 0.021, 0.1345],
                  'Co1': [0.099, 33.1252, 0.3645, 15.1768, 0.547, 5.0081, -0.0109],
                  'Co2': [0.4332, 14.3553, 0.5857, 4.6077, -0.0382, 0.1338, 0.0179],
                  'Co3': [0.3902, 12.5078, 0.6324, 4.4574, -0.15, 0.0343, 0.1272],
                  'Co4': [0.3515, 10.7785, 0.6778, 4.2343, -0.0389, 0.2409, 0.0098],
                  'Cr0': [0.1135, 45.199, 0.3481, 19.4931, 0.5477, 7.3542, -0.0092],
                  'Cr1': [-0.0977, 0.047, 0.4544, 26.0054, 0.5579, 7.4892, 0.0831],
                  'Cr2': [1.2024, -0.0055, 0.4158, 20.5475, 0.6032, 6.956, -1.2218],
                  'Cr3': [-0.3094, 0.0274, 0.368, 17.0355, 0.6559, 6.5236, 0.2856],
                  'Cr4': [-0.232, 0.0433, 0.3101, 14.9518, 0.7182, 6.1726, 0.2042],
                  'Cu0': [0.0909, 34.9838, 0.4088, 11.4432, 0.5128, 3.8248, -0.0124],
                  'Cu1': [0.0749, 34.9656, 0.4147, 11.7642, 0.5238, 3.8497, -0.0127],
                  'Cu2': [0.0232, 34.9686, 0.4023, 11.564, 0.5882, 3.8428, -0.0137],
                  'Cu3': [0.0031, 34.9074, 0.3582, 10.9138, 0.6531, 3.8279, -0.0147],
                  'Cu4': [-0.0132, 30.6817, 0.2801, 11.1626, 0.749, 3.8172, -0.0165],
                  'Dy2': [0.1308, 18.3155, 0.3118, 7.6645, 0.5795, 3.1469, -0.0226],
                  'Dy3': [0.1157, 15.0732, 0.327, 6.7991, 0.5821, 3.0202, -0.0249],
                  'Er2': [0.1122, 18.1223, 0.3462, 6.9106, 0.5649, 2.7614, -0.0235],
                  'Er3': [0.0586, 17.9802, 0.354, 7.0964, 0.6126, 2.7482, -0.0251],
                  'Eu2': [0.0755, 25.296, 0.3001, 11.5993, 0.6438, 4.0252, -0.0196],
                  'Eu3': [0.0204, 25.3078, 0.301, 11.4744, 0.7005, 3.942, -0.022],
                  'Fe0': [0.0706, 35.0085, 0.3589, 15.3583, 0.5819, 5.5606, -0.0114],
                  'Fe1': [0.1251, 34.9633, 0.3629, 15.5144, 0.5223, 5.5914, -0.0105],
                  'Fe2': [0.0263, 34.9597, 0.3668, 15.9435, 0.6188, 5.5935, -0.0119],
                  'Fe3': [0.3972, 13.2442, 0.6295, 4.9034, -0.0314, 0.3496, 0.0044],
                  'Fe4': [0.3782, 11.38, 0.6556, 4.592, -0.0346, 0.4833, 0.0005],
                  'Gd2': [0.0636, 25.3823, 0.3033, 11.2125, 0.6528, 3.7877, -0.0199],
                  'Gd3': [0.0186, 25.3867, 0.2895, 11.1421, 0.7135, 3.752, -0.0217],
                  'Ho2': [0.0995, 18.1761, 0.3305, 7.8556, 0.5921, 2.9799, -0.023],
                  'Ho3': [0.0566, 18.3176, 0.3365, 7.688, 0.6317, 2.9427, -0.0248],
                  'Mn0': [0.2438, 24.9629, 0.1472, 15.6728, 0.6189, 6.5403, -0.0105],
                  'Mn1': [-0.0138, 0.4213, 0.4231, 24.668, 0.5905, 6.6545, -0.001],
                  'Mn2': [0.422, 17.684, 0.5948, 6.005, 0.0043, -0.609, -0.0219],
                  'Mn3': [0.4198, 14.2829, 0.6054, 5.4689, 0.9241, -0.0088, -0.9498],
                  'Mn4': [0.376, 12.5661, 0.6602, 5.1329, -0.0372, 0.563, 0.0011],
                  'Mo0': [0.1806, 49.0568, 1.2306, 14.7859, -0.4268, 6.9866, 0.0171],
                  'Mo1': [0.35, 48.0354, 1.0305, 15.0604, -0.3929, 7.479, 0.0139],
                  'Nb0': [0.3946, 49.2297, 1.3197, 14.8216, -0.7269, 9.6156, 0.0129],
                  'Nb1': [0.4572, 49.9182, 1.0274, 15.7256, -0.4962, 9.1573, 0.0118],
                  'Nd2': [0.1645, 25.0453, 0.2522, 11.9782, 0.6012, 4.9461, -0.018],
                  'Nd3': [0.054, 25.0293, 0.3101, 12.102, 0.6575, 4.7223, -0.0216],
                  'Ni0': [-0.0172, 35.7392, 0.3174, 14.2689, 0.7136, 4.5661, -0.0143],
                  'Ni1': [0.0705, 35.8561, 0.3984, 13.8042, 0.5427, 4.3965, -0.0118],
                  'Ni2': [0.0163, 35.8826, 0.3916, 13.2233, 0.6052, 4.3388, -0.0133],
                  'Ni3': [-0.0134, 35.8677, 0.2678, 12.3326, 0.7614, 4.2369, -0.0162],
                  'Ni4': [-0.009, 35.8614, 0.2776, 11.7904, 0.7474, 4.2011, -0.0163],
                  'Np3': [0.5157, 20.8654, 2.2784, 5.893, -1.8163, 4.8457, 0.0211],
                  'Np4': [0.4206, 19.8046, 2.8004, 5.9783, -2.2436, 4.9848, 0.0228],
                  'Np5': [0.3692, 18.19, 3.151, 5.85, -2.5446, 4.9164, 0.0248],
                  'Np6': [0.2929, 17.5611, 3.4866, 5.7847, -2.8066, 4.8707, 0.0267],
                  'Pd0': [0.2003, 29.3633, 1.1446, 9.5993, -0.3689, 4.0423, 0.0251],
                  'Pd1': [0.5033, 24.5037, 1.9982, 6.9082, -1.524, 5.5133, 0.0213],
                  'Pr3': [0.0504, 24.9989, 0.2572, 12.0377, 0.7142, 5.0039, -0.0219],
                  'Pu3': [0.384, 16.6793, 3.1049, 5.421, -2.5148, 4.5512, 0.0263],
                  'Pu4': [0.4934, 16.8355, 1.6394, 5.6384, -1.1581, 4.1399, 0.0248],
                  'Pu5': [0.3888, 16.5592, 2.0362, 5.6567, -1.4515, 4.2552, 0.0267],
                  'Pu6': [0.3172, 16.0507, 3.4654, 5.3507, -2.8102, 4.5133, 0.0281],
                  'Rh0': [0.0976, 49.8825, 1.1601, 11.8307, -0.2789, 4.1266, 0.0234],
                  'Rh1': [0.3342, 29.7564, 1.2209, 9.4384, -0.5755, 5.332, 0.021],
                  'Ru0': [0.1069, 49.4238, 1.1912, 12.7417, -0.3176, 4.9125, 0.0213],
                  'Ru1': [0.441, 33.3086, 1.4775, 9.5531, -0.9361, 6.722, 0.0176],
                  'Sc0': [0.2512, 90.0296, 0.329, 39.4021, 0.4235, 14.3222, -0.0043],
                  'Sc1': [0.4889, 51.1603, 0.5203, 14.0764, -0.0286, 0.1792, 0.0185],
                  'Sc2': [0.5048, 31.4035, 0.5186, 10.9897, -0.0241, 1.1831, 0.0],
                  'Sm2': [0.0909, 25.2032, 0.3037, 11.8562, 0.625, 4.2366, -0.02],
                  'Sm3': [0.0288, 25.2068, 0.2973, 11.8311, 0.6954, 4.2117, -0.0213],
                  'Tb2': [0.0547, 25.5086, 0.3171, 10.5911, 0.649, 3.5171, -0.0212],
                  'Tb3': [0.0177, 25.5095, 0.2921, 10.5769, 0.7133, 3.5122, -0.0231],
                  'Tc0': [0.1298, 49.6611, 1.1656, 14.1307, -0.3134, 5.5129, 0.0195],
                  'Tc1': [0.2674, 48.9566, 0.9569, 15.1413, -0.2387, 5.4578, 0.016],
                  'Ti0': [0.4657, 33.5898, 0.549, 9.8791, -0.0291, 0.3232, 0.0123],
                  'Ti1': [0.5093, 36.7033, 0.5032, 10.3713, -0.0263, 0.3106, 0.0116],
                  'Ti2': [0.5091, 24.9763, 0.5162, 8.7569, -0.0281, 0.916, 0.0015],
                  'Ti3': [0.3571, 22.8413, 0.6688, 8.9306, -0.0354, 0.4833, 0.0099],
                  'Tm2': [0.0983, 18.3236, 0.338, 6.9178, 0.5875, 2.6622, -0.0241],
                  'Tm3': [0.0581, 15.0922, 0.2787, 7.8015, 0.6854, 2.7931, -0.0224],
                  'U3': [0.5058, 23.2882, 1.3464, 7.0028, -0.8724, 4.8683, 0.0192],
                  'U4': [0.3291, 23.5475, 1.0836, 8.454, -0.434, 4.1196, 0.0214],
                  'U5': [0.365, 19.8038, 3.2199, 6.2818, -2.6077, 5.301, 0.0233],
                  'V0': [0.4086, 28.8109, 0.6077, 8.5437, -0.0295, 0.2768, 0.0123],
                  'V1': [0.4444, 32.6479, 0.5683, 9.0971, -0.2285, 0.0218, 0.215],
                  'V2': [0.4085, 23.8526, 0.6091, 8.2456, -0.1676, 0.0415, 0.1496],
                  'V3': [0.3598, 19.3364, 0.6632, 7.6172, -0.3064, 0.0296, 0.2835],
                  'V4': [0.3106, 16.816, 0.7198, 7.0487, -0.0521, 0.302, 0.0221],
                  'Y0': [0.5915, 67.6081, 1.5123, 17.9004, -1.113, 14.1359, 0.008],
                  'Yb2': [0.0855, 18.5123, 0.2943, 7.3734, 0.6412, 2.6777, -0.0213],
                  'Yb3': [0.0416, 16.0949, 0.2849, 7.8341, 0.6961, 2.6725, -0.0229],
                  'Zr0': [0.4106, 59.9961, 1.0543, 18.6476, -0.4751, 10.54, 0.0106],
                  'Zr1': [0.4532, 59.5948, 0.7834, 21.4357, -0.2451, 9.036, 0.0098]}
        try:
            return j0dict[name]
        except KeyError:
            print('No magnetic form factor found for that element/ion.')
            return ['none']
    else:
        j2dict = {'Am2': [3.5237, 15.9545, 2.2855, 5.1946, -0.0142, 0.5853, 0.0033],
                  'Am3': [2.8622, 14.7328, 2.4099, 5.1439, -0.1326, 0.0309, 0.1233],
                  'Am4': [2.4141, 12.9478, 2.3687, 4.9447, -0.2490, 0.0215, 0.2371],
                  'Am5': [2.0109, 12.0534, 2.4155, 4.8358, -0.2264, 0.0275, 0.2128],
                  'Am6': [1.6778, 11.3372, 2.4531, 4.7247, -0.2043, 0.0337, 0.1892],
                  'Am7': [1.8845, 9.1606, 2.0746, 4.0422, -0.1318, 1.7227, 0.0020],
                  'Ce2': [0.9809, 18.0630, 1.8413, 7.7688, 0.9905, 2.8452, 0.0120],
                  'Co0': [1.9678, 14.1699, 1.4911, 4.9475, 0.3844, 1.7973, 0.0027],
                  'Co1': [2.4097, 16.1608, 1.5780, 5.4604, 0.4095, 1.9141, 0.0031],
                  'Co2': [1.9049, 11.6444, 1.3159, 4.3574, 0.3146, 1.6453, 0.0017],
                  'Co3': [1.7058, 8.8595, 1.1409, 3.3086, 0.1474, 1.0899, -0.0025],
                  'Co4': [1.3110, 8.0252, 1.1551, 3.1792, 0.1608, 1.1301, -0.0011],
                  'Cr0': [3.4085, 20.1267, 2.1006, 6.8020, 0.4266, 2.3941, 0.0019],
                  'Cr1': [3.7768, 20.3456, 2.1028, 6.8926, 0.4010, 2.4114, 0.0017],
                  'Cr2': [2.6422, 16.0598, 1.9198, 6.2531, 0.4446, 2.3715, 0.0020],
                  'Cr3': [1.6262, 15.0656, 2.0618, 6.2842, 0.5281, 2.3680, 0.0023],
                  'Cr4': [1.0293, 13.9498, 1.9933, 6.0593, 0.5974, 2.3457, 0.0027],
                  'Cu0': [1.9182, 14.4904, 1.3329, 4.7301, 0.3842, 1.6394, 0.0035],
                  'Cu1': [1.8814, 13.4333, 1.2809, 4.5446, 0.3646, 1.6022, 0.0033],
                  'Cu2': [1.5189, 10.4779, 1.1512, 3.8132, 0.2918, 1.3979, 0.0017],
                  'Cu3': [1.2797, 8.4502, 1.0315, 3.2796, 0.2401, 1.2498, 0.0015],
                  'Cu4': [0.9568, 7.4481, 0.9099, 3.3964, 0.3729, 1.4936, 0.0049],
                  'Dy2': [0.5917, 18.5114, 1.1828, 6.7465, 0.8801, 2.2141, 0.0229],
                  'Dy3': [0.2523, 18.5172, 1.0914, 6.7362, 0.9345, 2.2082, 0.0250],
                  'Er2': [0.4693, 18.5278, 1.0545, 6.6493, 0.8679, 2.1201, 0.0261],
                  'Er3': [0.1710, 18.5337, 0.9879, 6.6246, 0.9044, 2.1004, 0.0278],
                  'Eu2': [0.8970, 18.4429, 1.3769, 7.0054, 0.9060, 2.4213, 0.0190],
                  'Eu3': [0.3985, 18.4514, 1.3307, 6.9556, 0.9603, 2.3780, 0.0197],
                  'Fe0': [1.9405, 18.4733, 1.9566, 6.3234, 0.5166, 2.1607, 0.0036],
                  'Fe1': [2.6290, 18.6598, 1.8704, 6.3313, 0.4690, 2.1628, 0.0031],
                  'Fe2': [1.6490, 16.5593, 1.9064, 6.1325, 0.5206, 2.1370, 0.0035],
                  'Fe3': [1.3602, 11.9976, 1.5188, 5.0025, 0.4705, 1.9914, 0.0038],
                  'Fe4': [1.5582, 8.2750, 1.1863, 3.2794, 0.1366, 1.1068, -0.0022],
                  'Gd2': [0.7756, 18.4695, 1.3124, 6.8990, 0.8956, 2.3383, 0.0199],
                  'Gd3': [0.3347, 18.4758, 1.2465, 6.8767, 0.9537, 2.3184, 0.0217],
                  'Ho2': [0.5094, 18.5155, 1.1234, 6.7060, 0.8727, 2.1589, 0.0242],
                  'Ho3': [0.2188, 18.5157, 1.0240, 6.7070, 0.9251, 2.1614, 0.0268],
                  'Mn0': [2.6681, 16.0601, 1.7561, 5.6396, 0.3675, 2.0488, 0.0017],
                  'Mn1': [3.2953, 18.6950, 1.8792, 6.2403, 0.3927, 2.2006, 0.0022],
                  'Mn2': [2.0515, 15.5561, 1.8841, 6.0625, 0.4787, 2.2323, 0.0027],
                  'Mn3': [1.2427, 14.9966, 1.9567, 6.1181, 0.5732, 2.2577, 0.0031],
                  'Mn4': [0.7879, 13.8857, 1.8717, 5.7433, 0.5981, 2.1818, 0.0034],
                  'Mo0': [5.1180, 23.4217, 4.1809, 9.2080, -0.0505, 1.7434, 0.0053],
                  'Mo1': [7.2367, 28.1282, 4.0705, 9.9228, -0.0317, 1.4552, 0.0049],
                  'Nb0': [7.4796, 33.1789, 5.0884, 11.5708, -0.0281, 1.5635, 0.0047],
                  'Nb1': [8.7735, 33.2848, 4.6556, 11.6046, -0.0268, 1.5389, 0.0044],
                  'Nd2': [1.4530, 18.3398, 1.6196, 7.2854, 0.8752, 2.6224, 0.0126],
                  'Nd3': [0.6751, 18.3421, 1.6272, 7.2600, 0.9644, 2.6016, 0.0150],
                  'Ni0': [1.0302, 12.2521, 1.4669, 4.7453, 0.4521, 1.7437, 0.0036],
                  'Ni1': [2.1040, 14.8655, 1.4302, 5.0714, 0.4031, 1.7784, 0.0034],
                  'Ni2': [1.7080, 11.0160, 1.2147, 4.1031, 0.3150, 1.5334, 0.0018],
                  'Ni3': [1.1612, 7.7000, 1.0027, 3.2628, 0.2719, 1.3780, 0.0025],
                  'Ni4': [1.1612, 7.7000, 1.0027, 3.2628, 0.2719, 1.3780, 0.0025],
                  'Np3': [3.7170, 15.1333, 2.3216, 5.5025, -0.0275, 0.7996, 0.0052],
                  'Np4': [2.9203, 14.6463, 2.5979, 5.5592, -0.0301, 0.3669, 0.0141],
                  'Np5': [2.3308, 13.6540, 2.7219, 5.4935, -0.1357, 0.0493, 0.1224],
                  'Np6': [1.8245, 13.1803, 2.8508, 5.4068, -0.1579, 0.0444, 0.1438],
                  'Pd0': [3.3105, 14.7265, 2.6332, 5.8618, -0.0437, 1.1303, 0.0053],
                  'Pd1': [4.2749, 17.9002, 2.7021, 6.3541, -0.0258, 0.6999, 0.0071],
                  'Pr3': [0.8734, 18.9876, 1.5594, 6.0872, 0.8142, 2.4150, 0.0111],
                  'Pu3': [2.0885, 12.8712, 2.5961, 5.1896, -0.1465, 0.0393, 0.1343],
                  'Pu4': [2.7244, 12.9262, 2.3387, 5.1633, -0.1300, 0.0457, 0.1177],
                  'Pu5': [2.1409, 12.8319, 2.5664, 5.1522, -0.1338, 0.0457, 0.1210],
                  'Pu6': [1.7262, 12.3240, 2.6652, 5.0662, -0.1695, 0.0406, 0.1550],
                  'Rh0': [3.3651, 17.3444, 3.2121, 6.8041, -0.0350, 0.5031, 0.0146],
                  'Rh1': [4.0260, 18.9497, 3.1663, 6.9998, -0.0296, 0.4862, 0.0127],
                  'Ru0': [3.7445, 18.6128, 3.4749, 7.4201, -0.0363, 1.0068, 0.0073],
                  'Ru1': [5.2826, 23.6832, 3.5813, 8.1521, -0.0257, 0.4255, 0.0131],
                  'Sc0': [10.8172, 54.3270, 4.7353, 14.8471, 0.6071, 4.2180, 0.0011],
                  'Sc1': [8.5021, 34.2851, 3.2116, 10.9940, 0.4244, 3.6055, 0.0009],
                  'Sc2': [4.3683, 28.6544, 3.7231, 10.8233, 0.6074, 3.6678, 0.0014],
                  'Sm2': [1.0360, 18.4249, 1.4769, 7.0321, 0.8810, 2.4367, 0.0152],
                  'Sm3': [0.4707, 18.4301, 1.4261, 7.0336, 0.9574, 2.4387, 0.0182],
                  'Tb2': [0.6688, 18.4909, 1.2487, 6.8219, 0.8888, 2.2751, 0.0215],
                  'Tb3': [0.2892, 18.4973, 1.1678, 6.7972, 0.9437, 2.2573, 0.0232],
                  'Tc0': [4.2441, 21.3974, 3.9439, 8.3753, -0.0371, 1.1870, 0.0066],
                  'Tc1': [6.4056, 24.8243, 3.5400, 8.6112, -0.0366, 1.4846, 0.0044],
                  'Ti0': [4.3583, 36.0556, 3.8230, 11.1328, 0.6855, 3.4692, 0.0020],
                  'Ti1': [6.1567, 27.2754, 2.6833, 8.9827, 0.4070, 3.0524, 0.0011],
                  'Ti2': [4.3107, 18.3484, 2.0960, 6.7970, 0.2984, 2.5476, 0.0007],
                  'Ti3': [3.3717, 14.4441, 1.8258, 5.7126, 0.2470, 2.2654, 0.0005],
                  'Tm2': [0.4198, 18.5417, 0.9959, 6.6002, 0.8593, 2.0818, 0.0284],
                  'Tm3': [0.1760, 18.5417, 0.9105, 6.5787, 0.8970, 2.0622, 0.0294],
                  'U3': [4.1582, 16.5336, 2.4675, 5.9516, -0.0252, 0.7646, 0.0057],
                  'U4': [3.7449, 13.8944, 2.6453, 4.8634, -0.5218, 3.1919, 0.0009],
                  'U5': [3.0724, 12.5460, 2.3076, 5.2314, -0.0644, 1.4738, 0.0035],
                  'V0': [3.8099, 21.3471, 2.3295, 7.4089, 0.4333, 2.6324, 0.0015],
                  'V1': [4.7474, 23.3226, 2.3609, 7.8082, 0.4105, 2.7063, 0.0014],
                  'V2': [3.4386, 16.5303, 1.9638, 6.1415, 0.2997, 2.2669, 0.0009],
                  'V3': [2.3005, 14.6821, 2.0364, 6.1304, 0.4099, 2.3815, 0.0014],
                  'V4': [1.8377, 12.2668, 1.8247, 5.4578, 0.3979, 2.2483, 0.0012],
                  'Y0': [14.4084, 44.6577, 5.1045, 14.9043, -0.0535, 3.3189, 0.0028],
                  'Yb2': [0.3852, 18.5497, 0.9415, 6.5507, 0.8492, 2.0425, 0.0301],
                  'Yb3': [0.1570, 18.5553, 0.8484, 6.5403, 0.8880, 2.0367, 0.0318],
                  'Zr0': [10.1378, 35.3372, 4.7734, 12.5453, -0.0489, 2.6721, 0.0036],
                  'Zr1': [11.8722, 34.9200, 4.0502, 12.1266, -0.0632, 2.8278, 0.0034]}
        try:
            return j2dict[name]
        except KeyError:
            print('No magnetic form factor found for that element/ion.')
            return ['none']


def spinsFromAtoms(magstruc, positions, fractional=True, returnIdxs=False):
    """Return the spin vectors corresponding to specified atomic
       positions.

    Args:
        magstruc: MagSpecies or MagStructure object containing atoms and spins
        positions (list or array): atomic positions for which the
            corresponding spins should be returned.
        fractional (boolean): set as True if the atomic positions are in
            fractional coordinates of the crystallographic lattice
            vectors.
        returnIdxs (boolean): if True, the indices of the spins will also be
            returned.
    Returns:
        Array consisting of the spins corresponding to the atomic positions.
    """
    if len(np.array(positions).shape) == 1:
        positions = [positions]
    spins = []
    idxs = []
    badlist = []
    for pos in positions:
        if fractional:
            pos = magstruc.struc.lattice.cartesian(pos)
        mask = np.all(np.round(magstruc.atoms, decimals=4) ==
                      np.round(pos, decimals=4), axis=1)
        goodspins = magstruc.spins[mask]
        goodidxs = np.where(mask)[0]
        if len(goodspins) == 1:
            spins.append(goodspins[0])
            idxs.append(goodidxs[0])
        elif len(goodspins) > 1:
            print('Warning: duplicate atoms in structure')
            spins.append(goodspins)
            idxs.append(goodidxs)
        elif len(goodspins) == 0:
            spins.append([])
            idxs.append([])
            badlist.append(pos)
    if len(badlist) > 0:
        print('The following atomic positions do not correspond to a spin:')
        for bad in badlist:
            if fractional:
                bad = magstruc.struc.lattice.fractional(pos)
            print(bad)
    if not returnIdxs:
        return spins
    else:
        return spins, idxs


def atomsFromSpins(magstruc, spinvecs, fractional=True, returnIdxs=False):
    """Return the atomic positions corresponding to specified spins.

    Args:
        magstruc: MagSpecies or MagStructure object containing atoms and spins
        spinvecs (list or array): spin vectors for which the
            corresponding atoms should be returned.
        fractional (boolean): set as True if the atomic positions are to be
            returned as fractional coordinates of the crystallographic lattice
            vectors.
        returnIdxs (boolean): if True, the indices of the atoms will also be
            returned.

    Returns:
        List of arrays of atoms corresponding to the spins.
    """
    if len(np.array(spinvecs).shape) == 1:
        spinvecs = [spinvecs]
    atoms = []
    idxs = []
    badlist = []
    for spin in spinvecs:
        mask = np.all(np.round(magstruc.spins, decimals=5) ==
                      np.round(spin, decimals=5), axis=1)
        goodatoms = magstruc.atoms[mask]
        goodidxs = np.where(mask)[0]
        if fractional:
            goodatoms = magstruc.struc.lattice.fractional(goodatoms)
        atoms.append(goodatoms)
        idxs.append(goodidxs)
        if len(goodidxs) == 0:
            badlist.append(spin)
    if len(badlist) > 0:
        print('The following spins were not found in the structure:')
        for bad in badlist:
            print(bad)
    if not returnIdxs:
        return atoms
    else:
        return atoms, idxs


def visualizeSpins(atoms, spins):
    """Set up a 3d plot showing an arrangement of spins.

    Args:
        atoms (numpy array): array of atomic positions of spins to be
            visualized.
        spins (numpy array): array of spin vectors in same order as atoms.

    Returns:
        matplotlib figure object with a quiver plot on 3d axes.
    """
    import matplotlib.pyplot as plt
    from mpl_toolkits.mplot3d import Axes3D

    xx, yy, zz = np.transpose(atoms)
    uu, vv, ww = np.transpose(spins)

    fig = plt.figure()
    ax = fig.add_subplot(111, projection='3d')
    for i in range(len(xx)):
        x, y, z = 1.0 * xx[i], 1.0 * yy[i], 1.0 * zz[i]
        u, v, w = 1.0 * uu[i], 1.0 * vv[i], 1.0 * ww[i]
        ax.quiver(x, y, z, u, v, w, pivot='middle') # I think this gives the correct arrow length
        #lngth = np.sqrt(u ** 2 + v ** 2 + w ** 2)
        #ax.quiver(x, y, z, u, v, w, length=lngth, pivot='middle')

    xmin, xmax = ax.get_xlim3d()
    ax.set_xlim3d(np.min((-1, xmin)), np.max((1, xmax)))
    ymin, ymax = ax.get_ylim3d()
    ax.set_ylim3d(np.min((-1, ymin)), np.max((1, ymax)))
    zmin, zmax = ax.get_zlim3d()
    ax.set_zlim3d(np.min((-1, zmin)), np.max((1, zmax)))

    return fig


def findAtomIndices(magstruc, atomList):
    """Return list of indices corresponding to input list of atomic coordinates.

    Args:
        atomList (numpy array of atomic coordinates)

    Returns:
        List of indices corresponding to the atomList.
    """
    if len(np.array(atomList).shape) == 1:
        atomList = [atomList]
    indices = []
    x, y, z = magstruc.atoms.transpose()
    allIdxs = np.arange(len(x))
    for idx, atom in enumerate(atomList):
        xa, ya, za = atom[0], atom[1], atom[2]
        maskx = (np.abs(x - xa) < 0.01)
        masky = (np.abs(y - ya) < 0.01)
        maskz = (np.abs(z - za) < 0.01)
        match = allIdxs[np.logical_and(maskx, np.logical_and(masky, maskz))]
        if len(match) == 0:
            print(('Warning: atom with index ' + str(idx) + ' in atomList could not'))
            print('be found in the MagStructure, so the index -1 has been')
            print('returned instead.')
            indices.append(-1)
        if len(match) == 1:
            indices.append(match[0])
        if len(match) > 1:
            print(('Warning: ' + str(len(match)) + ' atoms matching index ' + str(idx)))
            print('have been found in the MagStructure, so just the first index has')
            print('been returned.')
            indices.append(match[0])

    return indices


def getStrucFromPDFgui(fileName, fitIdx=0, phaseIdx=0):
    """Extract the refined atomic structure from a PDFgui project file.

    Args:
        fileName (str): path to the .ddp file containing the fit
        fitIdx (int): index of fit in .ddp file from which the refined
             structure is to be extracted. Default is 0.
        phaseIdx (int): index of phase within the specified fit for
             which the refined structure is to be extracted. Default
             is 0.

    Returns:
        struc (diffpy.Structure object): Refined atomic structure.
    """
    if fileName[-4:] == '.ddp':
        from diffpy.pdfgui import tui
        prj = tui.LoadProject(fileName)
        try:
            fit = prj.getFits()[fitIdx]
        except IndexError:
            print('Fit index does not correspond to any fit in your')
            print('PDFgui project.')
            struc = []
            return struc
        try:
            struc = prj.getPhases([fit])[phaseIdx]
            struc = struc.refined
        except IndexError:
            print('Phase index does not correspond to any phase in your')
            print('specified fit in the PDFgui project.')
            struc = []
        return struc
    else:
        print('Please provide a PDFgui project file (.ddp extension)')
        return []


def jCalc(q, params=[0.2394, 26.038, 0.4727, 12.1375, 0.3065, 3.0939, -0.01906],
          j2=False):
    """Calculate the magnetic form factor j0.

    This method for calculating the magnetic form factor is based on the
    approximate empirical forms based on the tables by Brown, consisting of
    the sum of 3 Gaussians and a constant.

    Args:
        q (numpy array): 1-d grid of momentum transfer on which the form
            factor is to be computed
        params (python list): provides the 7 numerical coefficients. The
            default is an average form factor of 3d j0 approximations.
        j2 (boolean): if True, calculate the j2 approximation for orbital
            angular momentum contributions

    Returns:
        numpy array with same shape as q giving the magnetic form factor j0 or j2.
    """
    [A, a, B, b, C, c, D] = params
    if j2:
        return (A * np.exp(-a * (q / 4 / np.pi) ** 2) + B * np.exp(-b * (q / 4 / np.pi) ** 2) + C * np.exp(
            -c * (q / 4 / np.pi) ** 2) + D) * (q / 4.0 / np.pi) ** 2
    else:
        return A * np.exp(-a * (q / 4 / np.pi) ** 2) + B * np.exp(-b * (q / 4 / np.pi) ** 2) + C * np.exp(
            -c * (q / 4 / np.pi) ** 2) + D


def cv(x1, y1, x2, y2, align=False, normalize=False):
    """Perform the convolution of two functions and give the correct output.


    Args:
        x1 (numpy array): independent variable of first function; must be in
            ascending order
        y1 (numpy array): dependent variable of first function
        x2 (numpy array): independent variable of second function; must have
            same grid spacing as x1
        y2 (numpy array): dependent variable of second function
        align (boolean): if True, output grid will be identical to input x1, y1
        normalize (boolean): if True, the output will be scaled to match y1

    Returns:
        xcv (numpy array): independent variable of convoluted function, has
            dimension len(x1) + len(x2) - 1 unless align option us used.

        ycv (numpy array): convolution of y1 and y2, same shape as xcv

    """
    dx = x1[1] - x1[0]
    ycv = dx * convolve(y1, y2, 'full')
    xcv = np.linspace(x1[0] + x2[0], x1[-1] + x2[-1], len(ycv))
    if align: # align output with the x1 and y1 input grids
        lb = x1[0] - 0.5 * dx
        ub = x1[-1] + 0.5 * dx
        mask = np.logical_and(xcv > lb, xcv < ub)
        ycv = ycv[mask]
        xcv = xcv[mask]
    if normalize: # normalize y1 to its original scale
        ycv /= np.trapz(y2, x2) 
    return xcv, ycv


def fourierTransform(q, fq, rmin=0.0, rmax=50.0, rstep=0.1):  # requires even q-grid
    """Compute the Fourier transform of a function.

    This method uses the FFT algorithm and returns correctly spaced x and
    y arrays on an even and specifiable grid. The input grid must be evenly
    spaced.

    Args:
        q (numpy array): independent variable for function to be transformed
        fq (numpy array): dependent variable for function to be transformed
        rmin (float, default = 0.0): min value of conjugate independent variable
            grid
        rmax (float, default = 50.0): maximum value of conjugate independent
            variable grid
        rstep (float, default = 0.1): grid spacing for conjugate independent
            variable

    Returns:
        r (numpy array): independent variable grid for transformed quantity

        fr (numpy array): Fourier transform of fq (complex)
    """
    lostep = int(np.ceil((rmin - 1e-8) / rstep))
    histep = int(np.floor((rmax + 1e-8) / rstep)) + 1
    r = np.arange(lostep, histep) * rstep
    qstep = q[1] - q[0]
    if (q[0] - 0.01 * qstep) > 0:
        nn = int(np.round(q[0] / qstep))
        addme = np.linspace(0.0, q[0] - qstep, nn)
        q = np.concatenate((addme, q))
        fq = np.concatenate((0.0 * addme, fq))
    qmaxrstep = np.pi / rstep
    nin = len(q)
    nbase = max([nin, histep, qmaxrstep / qstep])
    nlog2 = int(np.ceil(np.log2(nbase)))
    nout = 2 ** nlog2
    qmaxdb = 2 * nout * qstep
    yindb = np.concatenate((fq, np.zeros(2 * nout - nin)))
    cyoutdb = np.fft.ifft(yindb) * np.sqrt(2 / np.pi) * qmaxdb
    frdb = cyoutdb
    rstepfine = 2 * np.pi / qmaxdb
    rfine = np.arange(nout) * rstepfine
    frfine = frdb[:nout]
    frr = np.interp(r, rfine, np.real(frfine))
    fri = np.interp(r, rfine, np.imag(frfine))
    if r[0] + 0.0001 * rstep < 0:
        nn = int(np.round(-r[0] / rstep))
        frr[:nn] = 1.0 * frr[2 * nn:nn:-1]
        fri[:nn] = -1.0 * fri[2 * nn:nn:-1]
    fr = frr + 1j * fri
    return r, fr


def cosTransform(q, fq, rmin=0.0, rmax=50.0, rstep=0.1):  # requires even q-grid
    """Compute the cosine Fourier transform of a function.

    This method uses the FFT algorithm and returns correctly spaced x and
    y arrays on an even and specifiable grid. The input grid must be evenly
    spaced.

    Args:
        q (numpy array): independent variable for function to be transformed
        fq (numpy array): dependent variable for function to be transformed
        rmin (float, default = 0.0): min value of conjugate independent variable
            grid
        rmax (float, default = 50.0): maximum value of conjugate independent
            variable grid
        rstep (float, default = 0.1): grid spacing for conjugate independent
            variable

    Returns:
        r (numpy array): independent variable grid for transformed quantity

        fr (numpy array): cosine Fourier transform of fq
    """
    r, fr = fourierTransform(q, fq, rmin, rmax, rstep)
    fr = np.real(fr)
    return r, fr


def sinTransform(q, fq, rmin=0.0, rmax=50.0, rstep=0.1):  # requires even q-grid
    """Compute the sine Fourier transform of a function.

    This method uses direct integration rather than an FFT and doesn't require
    an even grid. The grid for the Fourier transform is even and specifiable.

    Args:
        q (numpy array): independent variable for function to be transformed
        fq (numpy array): dependent variable for function to be transformed
        rmin (float, default = 0.0): min value of conjugate independent variable
            grid
        rmax (float, default = 50.0): maximum value of conjugate independent
            variable grid
        rstep (float, default = 0.1): grid spacing for conjugate independent
            variable

    Returns:
        r (numpy array): independent variable grid for transformed quantity

        fr (numpy array): sine Fourier transform of fq
    """
    r, fr = fourierTransform(q, fq, rmin, rmax, rstep)
    fr = np.imag(fr)
    return r, fr


def cosTransformDirectIntegration(q, fq, rmin=0.0, rmax=50.0, rstep=0.1):  # does not require even q-grid
    """Compute the cosine Fourier transform of a function.

    This method uses direct integration rather than an FFT and doesn't require
    an even grid. The grid for the Fourier transform is even and specifiable.

    Args:
        q (numpy array): independent variable for function to be transformed
        fq (numpy array): dependent variable for function to be transformed
        rmin (float, default = 0.0): min value of conjugate independent variable
            grid
        rmax (float, default = 50.0): maximum value of conjugate independent
            variable grid
        rstep (float, default = 0.1): grid spacing for conjugate independent
            variable

    Returns:
        r (numpy array): independent variable grid for transformed quantity

        fr (numpy array): cosine Fourier transform of fq
    """
    lostep = int(np.ceil((rmin - 1e-8) / rstep))
    histep = int(np.floor((rmax + 1e-8) / rstep)) + 1
    r = np.arange(lostep, histep) * rstep
    qrmat = np.outer(r, q)
    integrand = fq * np.cos(qrmat)
    fr = np.sqrt(2.0 / np.pi) * np.trapz(integrand, q)
    return r, fr


def sinTransformDirectIntegration(q, fq, rmin=0.0, rmax=50.0, rstep=0.1):  # does not require even q-grid
    """Compute the sine Fourier transform of a function.

    This method uses direct integration rather than an FFT and doesn't require
    an even grid. The grid for the Fourier transform is even and specifiable.

    Args:
        q (numpy array): independent variable for function to be transformed
        fq (numpy array): dependent variable for function to be transformed
        rmin (float, default = 0.0): min value of conjugate independent variable
            grid
        rmax (float, default = 50.0): maximum value of conjugate independent
            variable grid
        rstep (float, default = 0.1): grid spacing for conjugate independent
            variable

    Returns:
        r (numpy array): independent variable grid for transformed quantity

        fr (numpy array): cosine Fourier transform of fq
    """
    lostep = int(np.ceil((rmin - 1e-8) / rstep))
    histep = int(np.floor((rmax + 1e-8) / rstep)) + 1
    r = np.arange(lostep, histep) * rstep
    qrmat = np.outer(r, q)
    integrand = fq * np.sin(qrmat)
    fr = np.sqrt(2.0 / np.pi) * np.trapz(integrand, q)
    return r, fr


def getStdUnc(fitResult, data, dataErr=None, numConstraints=0):
    """Return the standard uncertainty of refined parameters.
    This method is based on the scipy.optimize.least_squares routine.

    Args:
        fitResult: Output from scipy.optimize.least_squares routine
        data (numpy array): The data against which the fit is performed
        dataErr (numpy array): Experimental uncertainties on the data points
            (set to unity if not provided)
        numConstraints (int): Number of constraints used in the model
    Returns:
        pUnc (numpy array): standard uncertainties of the refined parameters.
        chisq (float): Value of chi^2 for the fit.
    """
    if dataErr is None:
        dataErr = np.ones_like(data)
    weights = 1.0 / dataErr ** 2
    Rw = np.sqrt((fitResult.fun ** 2).sum() / (data ** 2 * weights).sum())
    numParams = len(fitResult.x)
    Rexp = np.sqrt((data.shape[0] - numParams + numConstraints) / (data ** 2 * weights).sum())
    j = fitResult.jac
    jac = np.dot(j.transpose(), j)
    cov = np.linalg.inv(jac) * Rw ** 2 / Rexp ** 2
    pUnc = np.sqrt(cov.diagonal())
    chisq = Rw ** 2 / Rexp ** 2
    return pUnc, chisq


def optimizedSubtraction(rhigh, dhigh, rlow, dlow):
    '''
    This routine stretches and broadens a low-temperature atomic PDF fit residual
    to match a high-temperature fit residual as closely as possible. The idea is
    to minimize thermal effects when doing the temperature subtraction so that
    the mDPF can be obtained as cleanly as possible.

    rhigh = r array for high-temperature atomic PDF fit residual
    dhigh = high-temperature atomic PDF fit residual
    rlow = r array for low-temperature atomic PDF fit residual
    dlow = low-temperature atomic PDF fit residual
    Note: the high- and low-temperature fits should be over the same r range
    '''

    def gaussianSmooth(x, y, sig=0.1):
        dr = np.mean((x - np.roll(x, 1))[1:])
        rs = np.arange(-10, 10, dr)
        s = np.exp(-rs ** 2 / 2.0 / sig ** 2)
        xsmooth, ysmooth = cv(x, y, rs, s)
        ysmooth /= np.trapz(s, rs)
        msk = np.logical_and(xsmooth > (x.min() - 0.5 * dr), xsmooth < (x.max() + 0.5 * dr))
        return ysmooth[msk]

    def residual(p, x, y, ycomp):
        stretch, width = p
        newx = stretch * x
        newy = np.interp(newx, x, y)
        newy = gaussianSmooth(newx, newy, width)
        msk = (x <= x.max() / stretch)
        return ycomp[msk] - newy[msk]

    p0 = [0.999, 0.1]  # typical starting guesses
    opt = least_squares(residual, p0, args=(rlow, dlow, dhigh))
    print(opt.x)
    stretch = opt.x[0]
    msk = (rlow <= rlow.max() / stretch)
    newdllow = dhigh[msk] - residual(opt.x, rlow, dlow, dhigh)
    diff = newdllow - dhigh[msk]
    return rhigh[msk], diff


def smoothData(xdata, ydata, qCutoff, func='sinc', gaussHeight=0.01):
    """Smooth out high-frequency contributions from the data.

    This method performs a convolution in real space to simulate a truncation
    in reciprocal space. This is motivated by the fact that high-frequency
    noise can sometimes obscure the lower-frequency mPDF signal when the mPDF
    is collected together with the nuclear PDF. This high-frequency noise comes
    from scattering intensity at high q that cannot possibly come from magnetic
    scattering, due to the strong suppression from the magnetic form factor.
    To better isolate the mPDF from this high-frequency noise, one could
    truncate the Fourier transfrom at some cutoff q value beyond which the
    magnetic scattering is negligible (e.g. where the square of the magnetic
    form factor is reduced to 1% of its original value). This can be done by
    multiplying the scattering in q-space by a "window function" that is equal
    to unity for q between 0 and the cutoff value and 0 outside this window.
    By the convolution theorem, this is equivalent to convoluting the full
    Fourier transform in real space with a sinc function. Alternatively, one
    could multiply the scattering in q-space by a Guassian function which is
    reduced to some small amplitude at the desired cutoff q value.  This is
    equivalent to a convolution in real space with another Gaussian function.
    The former method is recommended because it will generally be more
    physically justifiable.

    Args:
        xdata: numpy array containing the independent variable of the data
            to be smoothed.
        ydata: numpy array containing the dependent variable; this array will
            be smoothed.
        qCutoff: q value beyond which all contributions will be ignored.
        func: string specifying the type of real-space convolution function,
            either 'sinc' or 'gaussian' (see previous discussion).
        gaussHeight: float specifying what the height of the q-space Gaussian
            function should be at the specified value of qCutoff.

    Returns:
        Numpy array containing the smoothed version of ydata.
    """
    dr = np.mean((xdata - np.roll(xdata, 1))[1:])
    rs = np.arange(-10, 10, dr)
    if func == 'sinc':
        s = np.sinc(rs * qCutoff / np.pi)
    elif func == 'gaussian':
        rg = 1.0 / (qCutoff * np.sqrt(np.log(1.0 / gaussHeight) / 2.0))
        s = np.exp(-rs ** 2 / 2.0 / rg ** 2)
    else:
        print('The only function options are sinc and gaussian. Please check')
        print('your input. Using sinc by default.')
        s = np.sinc(rs * qCutoff / np.pi)
    xsmooth, ysmooth = cv(xdata, ydata, rs, s, align=True, normalize=True)
    return ysmooth


def getDiffData(fileName, fitIdx=0, writedata=False):
    """Extract the fit residual from a structural PDF fit. Works for .fgr and
       .ddp files.

    Args:
        fileName (str): path to the .fgr or .ddp file containing the fit
        fitIdx (int): index of fit in .ddp file from which the residual
             is to be extracted.
        writedata (boolean): whether or not the output should be saved to file

    Returns:
        r (numpy array): same r-grid as contained in the fit file

        diff (numpy array): the structural PDF fit residual (i.e. the mPDF)
    """
    if fileName[-4:] == '.fgr':
        lines = open(fileName).readlines()[:50]
        for idx, line in enumerate(lines):
            if 'start data' in line:
                startLine = 1 * idx
                break
        allcols = np.loadtxt(fileName, unpack=True, comments='#', skiprows=startLine)
        r, diff = allcols[0], allcols[4]
        if writedata:
            np.savetxt(fileName[:-4] + '.diff', np.transpose((r, diff)))
        return r, diff
    elif fileName[-4:] == '.ddp':
        from diffpy.pdfgui import tui
        prj = tui.LoadProject(fileName)
        fit = prj.getFits()[fitIdx]
        dataSet = fit.getDataSet(0)
        r = np.array(dataSet.rcalc)
        diff = np.array(dataSet.Gdiff)
        if writedata:
            np.savetxt(fileName[:-4] + '_' + str(fitIdx) + '.diff',
                       np.transpose((r, diff)))
        return r, diff
    else:
        print('This file format is not currently supported.')
        return np.array([0]), np.array([0])

def read_fgr(fileName):
    """Extract the PDF data and calculation from a .fgr file.

    Args:
        fileName (str): path to the .fgr file containing the fit

    Returns:
        r (numpy array): same r-grid as contained in the fit file
        gobs (numpy array): experimental PDF data
        gcalc (numpy array): the calculated PDF
        gdiff (numpy array): the structural PDF fit residual
    """
    if fileName[-4:] == '.fgr':
        lines = open(fileName).readlines()[:50]
        for idx, line in enumerate(lines):
            if 'start data' in line:
                startLine = 1 * idx
                break
        allcols = np.loadtxt(fileName, unpack=True, comments='#', skiprows=startLine)
        r, gcalc, gdiff = allcols[0], allcols[1], allcols[4]
        gobs = gcalc + gdiff
        return r, gobs, gcalc, gdiff
    else:
        print('Incompatible file type. Please provide a .fgr file from PDFgui.')
        return None

def calculateAvgB(struc):
    """Calculate average coherent neutron scattering length.

    Args:
        struc: Diffpy Structure object

    Returns:
        bAvg: average coherent neutron scattering length.
    """
    totalOcc = struc.occupancy.sum()
    bAvg = 0
    for idx, atom in enumerate(struc):
        el = re.findall("[a-zA-Z]+", atom.element)
        b = getattr(pt, el[0]).neutron.b_c
        bAvg += struc.occupancy[idx] * b
    bAvg /= totalOcc
    return bAvg

def calculate_avg_spin_magnitude(magstruc, method='species'):
    """Calculate average spin magnitude from a MagStructure instance.
    
    Args:
        method (string): must be 'full' or 'species'. If 'full', the
            mean magnitude of all spins in the MagStructure will be
            returned. If 'species', then just a single spin from
            each species will be included in the calculation and weighted
            according to the species fraction. 'species' is faster and
            should be used if all spins belonging to a given species
            have the same magnitude.
    
    Returns: Mean magnitude of spins.
    """
    if method == 'species':
        try:
            magnitude = 0
            idx_dict = magstruc.getSpeciesIdxs()
            for key in magstruc.species:
                frac = magstruc.fractions[key]
                species_magnitude = np.linalg.norm(magstruc.spins[idx_dict[key]])
                magnitude += frac * species_magnitude
            return magnitude
        except:
            return np.mean(np.apply_along_axis(np.linalg.norm, 1, magstruc.spins))
    elif method == 'full':
        return np.mean(np.apply_along_axis(np.linalg.norm, 1, magstruc.spins))
    else:
        print("Please choose either 'species' or 'full' for the method.")
        print("Using 'full' for now.")
        return np.mean(np.apply_along_axis(np.linalg.norm, 1, magstruc.spins))

def calculatemPDF(xyz, sxyz, gfactors=np.array([2.0]), calcIdxs=np.array([0]),
                  rstep=0.01, rmin=0.0, rmax=20.0, psigma=0.1, qmin=0,
                  qmax=-1, qdamp=0.0, extendedrmin=4.0, extendedrmax=4.0,
                  orderedScale=1.0,
                  K1=0.66667 * (1.913 * 2.81794 / 2.0) ** 2 * 2.0 ** 2 * 0.5 * (0.5 + 1),
                  rho0=0, netMag=0, corrLength=0, linearTermMethod='exact',
                  applyEnvelope=False, qwindow=np.array([0]),
                  qgrid=np.array([0])):
    """Calculate the normalized mPDF.

    At minimum, this module requires input lists of atomic positions and spins.

    Args:
        xyz (numpy array): list of atomic coordinates of all the magnetic
            atoms in the structure.
        sxyz (numpy array): triplets giving the spin vectors of all the
            atoms, in the same order as the xyz array provided as input.
        gfactors (numpy array): Lande g-factors of spins in same order as
            spin array.
        calcIdxs (python list): list giving the indices of the atoms array
            specifying the atoms to be used as the origin when calculating
            the mPDF.
        rstep (float): step size for r-grid of calculated mPDF.
        rmin (float): minimum value of r for which mPDF should be calculated.
        rmax (float): maximum value of r for which mPDF should be calculated.
        psigma(float): std deviation (in Angstroms) of Gaussian peak
            to be convoluted with the calculated mPDF to simulate thermal
            motion.
        qmin (float): minimum experimentally accessible q-value (to be used
            for simulating termination ripples). If <0, no termination effects
            are included.
        qmax (float): maximum experimentally accessible q-value (to be used
            for simulating termination ripples). If <0, no termination effects
            are included.
        qdamp (float): usual PDF qdamp parameter. Turned off if set to zero.
        extendedrmin (float): extension of the r-grid on which the mPDF is
            calculated to properly account for contribution of pairs just
            before the boundary.
        extendedrmax (float): extension of the r-grid on which the mPDF is
            calculated to properly account for contribution of pairs just
            outside the boundary.
        ordScale (float): overall scale factor for the mPDF function f(r).
        K1 (float): A constant related to the total angular momentum quantum
            number and the average Lande splitting factor.
        rho0 (float): number of magnetic moments per cubic Angstrom in the
            magnetic structure; default value is 0.
        netMag (float): net magnetization in Bohr magnetons per magnetic moment
            in the sample; default is 0. Only nonzero for ferro/ferrimagnets or
            canted antiferromagnets.
        corrLength (float): exponential magnetic correlation length of the
            structure; default is 0, which is considered to be infinite.
            This is important to get the linear term right for samples
            with nonzero net magnetization.
        linearTermMethod (string): determines how the calculation will
            handle the linear term present for magnetic structures with a net
            magnetization. Options are:
            'exact'; slope will be calculated from the values of
                MagStructure.rho0 and MagStructure.netMag, damping set by
                MagStructure.corrLength.
            'autoslope': slope will be determined by least-squares
                minimization of the calculated mPDF, thereby ensuring that
                the mPDF oscillates around zero, as it is supposed to.
                Damping set by MagStructure.corrLength.
            'fullauto': slope and damping set by least-squares minimization.
                This should only be used in the case of an anisotropic
                correlation length.
            Note that any other option will be converted to 'exact'.
        applyEnvelope (boolean): if True, an exponential damping envelope will
            be applied to the calculated f(r) (not including the linear term).
            The parameter corrLength is used to determine the envelope.
        qwindow (numpy array): Q-space window function applied to the data
            prior to Fourier transformation. Not used by default.
        qgrid (numpy array): Q-space grid on which the window function is
            defined.
    Returns: numpy arrays for r and the mPDF fr, on the extended grid.
        """
    # check if g-factors have been provided
    if sxyz.shape[0] != gfactors.shape[0]:
        gfactors = 2.0 * np.ones(sxyz.shape[0])
    # calculate s1, s2
    r = np.arange(rmin - extendedrmin, rmax + extendedrmax + rstep, rstep)
    r = np.round(r, decimals=6)
    # don't calculate for negative r
    if r[0] < 0:
        startIdx = np.argmin(np.abs(r))
        if r[startIdx] < 0:
            startIdx += 1
        r = r[startIdx:]
    rbin = np.concatenate([r - rstep / 2, [r[-1] + rstep / 2]])

    s1 = np.zeros(len(r))
    s2 = np.zeros(len(r))

    if type(calcIdxs) is str:
        if calcIdxs == 'all':
            calcIdxs = np.arange(len(xyz))

    for uu in calcIdxs:
        ri = xyz[uu]
        rj = xyz
        si = sxyz[uu]
        sj = sxyz
        gi = gfactors[uu]
        gj = gfactors

        dxyz = rj - ri
        d2xyz = np.sum((dxyz) ** 2, axis=1).reshape(dxyz.shape[0], 1)
        d1xyz = np.sqrt(d2xyz)
        d1xyz[uu] = 1e-6  ### avoid divide by zero problem
        d1xyzr = d1xyz.ravel()

        xh = dxyz / d1xyz
        yh = si - xh * np.sum(si * xh, axis=1).reshape(dxyz.shape[0], 1)
        yh_dis = np.sum(yh ** 2, axis=1)
        yh_ind = np.nonzero(np.abs(yh_dis) < 1e-10)
        yh[yh_ind] = [0, 0, 0]
        yh_dis[yh_ind] = 1e-6  ### avoid divide by zero problem

        aij = np.sum(si * yh, axis=1) * np.sum(sj * yh, axis=1) / yh_dis
        aij[uu] = 0
        bij = 2 * np.sum(si * xh, axis=1) * np.sum(sj * xh, axis=1) - aij

        w2 = bij / d1xyzr ** 3

        d1xyzr[uu] = 0.0
        s1 += np.histogram(d1xyzr, bins=rbin, weights=gi * gj * aij)[0]
        s2 += np.histogram(d1xyzr, bins=rbin, weights=gi * gj * w2)[0]

    # apply Gaussian shape function
    if psigma != None:
        x = np.arange(-3, 3, rstep)
        y = np.exp(-x ** 2 / psigma ** 2 / 2) * (1 / np.sqrt(2 * np.pi) / psigma)

        s1[0] = 0
        s1 = fftconvolve(s1, y)
        s1 = s1[int(len(x) / 2): int(-len(x) / 2 + 1)]

        s2 = fftconvolve(s2, y) * rstep
        s2 = s2[int(len(x) / 2): int(-len(x) / 2 + 1)]

    ss2 = np.cumsum(s2)

    if r[0] == 0:
        r[0] = 1e-4 * rstep  # avoid infinities at r = 0
        fr = s1 / r + r * (ss2[-1] - ss2)
        r[0] = 0
    else:
        fr = s1 / r + r * (ss2[-1] - ss2)

    ### prefactor
    fr /= len(calcIdxs) * K1 / (1.913 * 2.81794 / 2.0) ** 2
    if applyEnvelope and (corrLength != 0):
        fr *= np.exp(-r/corrLength)

    ### Now include the linear term
    if linearTermMethod == 'autoslope':
        if corrLength == 0:
            def residual(p):
                return fr - p[0] * r
            opt = least_squares(residual, [0])
            linearTerm = opt.x[0] * r
        else:
            def residual(p):
                return fr - p[0] * r * np.exp(-r/corrLength)
            opt = least_squares(residual, [0])
            linearTerm = opt.x[0] * r * np.exp(-r/corrLength)
    elif linearTermMethod == 'fullauto':    
            def residual(p):
                return fr - p[0] * r * np.exp(-r/p[1])
            opt = least_squares(residual, [0, 100], bounds=[[-1e6,0], [1e6,1e6]])
            linearTerm = opt.x[0] * r * np.exp(-r/opt.x[1])
    else:
        if corrLength == 0:
            linearTerm = 4 * np.pi * r * rho0 * (2.0/3.0) * netMag**2  \
                         / ( K1 / (1.913 * 2.81794 / 2.0) ** 2)
        else:
            linearTerm = 4 * np.pi * r * rho0 * (2.0 / 3.0) * netMag ** 2 * np.exp(-r/corrLength) \
                          / ( K1 / (1.913 * 2.81794 / 2.0) ** 2)
    fr -= linearTerm


    ### apply the scale factor and qdamp
    fr *= orderedScale * np.exp((-1.0 * (qdamp * r) ** 2) / 2)
    # Do the convolution with the termination function if qmin/qmax or a window
    # function have been given
    if qmin >= 0 and qmax > qmin and len(qgrid) == 1: # no window function
        rth = np.arange(-rmax - extendedrmax, rmax + extendedrmax + rstep, rstep)
        th = (qmax/np.pi) * np.sinc(qmax * rth / np.pi) - (qmin / np.pi) * np.sinc(qmin * rth / np.pi)
        rcv, frcv = cv(r, fr, rth, th, align=True, normalize=True)
    elif len(qgrid) > 1: # use the window function
        rFT, windowFT = fourierTransform(qgrid, qwindow, rmin=-rmax, rmax=rmax, rstep=rstep)
        windowFT = np.real(windowFT)
        rcv, frcv = cv(r, fr, rFT, windowFT, align=True, normalize=True)
    else:
        rcv, frcv = r, fr

    return rcv, frcv


def calculateDr(r, fr, q, ff, paraScale=1.0, rmintr=-5.0, rmaxtr=5.0,
                drtr=0.01, qmin=0, qmax=-1, K1=None, K2=None):
    """Calculate the unnormalized mPDF quantity D(r).

    This module requires a normalized mPDF as an input, as well as a magnetic
    form factor and associated q grid.

    Args:
        r (numpy array): r grid for the properly normalized mPDF.
        fr (numpy array): the properly normalized mPDF.
        q (numpy array): grid of momentum transfer values used for calculating
            the magnetic form factor.
        ff (numpy array): magnetic form factor. Same shape as ffqgrid.
        paraScale (float): scale factor for the paramagnetic part of the
            unnormalized mPDF function D(r).
        rmintr (float): minimum value of r for the Fourier transform of the
            magnetic form factor required for unnormalized mPDF.
        rmaxtr (float): maximum value of r for the Fourier transform of the
            magnetic form factor required for unnormalized mPDF.
        drtr (float): step size for r-grid used for calculating Fourier
            transform of magnetic form mactor.
        qmin (float): minimum experimentally accessible q-value (to be used
            for simulating termination ripples). If <0, no termination effects
            are included.
        qmax (float): maximum experimentally accessible q-value (to be used
            for simulating termination ripples). If <0, no termination effects
            are included.
        K1 (float): a constant used for calculating Dr; should be averaged
            over all magnetic species. Important if physical information is
            to be extracted from mPDF scale factors, e.g. moment size.
        K2 (float): another constant used for calculating Dr.

    Returns: numpy array for the unnormalized mPDF Dr.
    """
    if K1 is None:
        K1 = 0.66667 * (1.913 * 2.81794 / 2.0) ** 2 * 2.0 ** 2 * 0.5 * (0.5 + 1)
    if K2 is None:
        K2 = K1
    rsr, sr = cosTransform(q, ff, rmintr, rmaxtr, drtr)
    #### Double-check whether or not I should normalize sr!
    # norm = np.trapz(sr,rsr)
    # sr /= norm
    # sr = np.sqrt(np.pi/2.0)*sr ### I don't think we should multiply by sqrt(pi/2)
    rSr, Sr = cv(rsr, sr, rsr, sr)
    rDr, Dr = cv(r, K1 / (2.0 * np.pi) * fr, rSr, Sr)
    para = -K2 / np.pi * np.gradient(Sr, rSr[1] - rSr[0])  ### paramagnetic term in d(r)
    rpara, para = rSr, para
    # The code below is used to apply the termination convolution to the paramagnetic peak.
    # I don't think it's actually necessary.
#    if qmin >= 0 and qmax > qmin:
#        rstep = r[1] - r[0]
#        rth = np.arange(0.0, r.max() + rstep, rstep)
#        rth[0] = 1e-4 * rstep  # avoid infinities at r = 0
#        th = (np.sin(qmax * rth) - np.sin(qmin * rth)) / np.pi / rth
#        rth[0] = 0.0
#        rpara, para = cv(rSr, para, rth, th)
#    else:
#        rpara, para = rSr, para
    # make sure para and Dr match up in shape
    dr = r[2] - r[1]
    goodslice = np.logical_and(rDr >= r.min() - 0.5 * dr, rDr <= r.max() + 0.5 * dr)
    Dr = Dr[goodslice]
    para = para[rpara >= r.min() - 0.5 * dr]
    if para.shape[0] < Dr.shape[0]:
        para = np.concatenate((para, np.zeros(Dr.shape[0] - para.shape[0])))
    else:
        para = para[:Dr.shape[0]]
    Dr += paraScale * para
    return Dr


def calculateMagScatt(r, fr, qmin=0.0, qmax=20.0, qstep=0.01, quantity='sq'):
    """Calculate the magnetic scattering via Fourier transform of the mPDF.

    Args:
        r (numpy array): r grid for the properly normalized mPDF.
        fr (numpy array): the mPDF, either normalized or unnormalized.
        qmin (float): minimum value of the output q-grid.
        qmax (float): maximum value of the output q-grid.
        qstep (float): spacing for the q-grid.
            unnormalized mPDF function D(r).
        quantity (str): type of magnetic scattering quantity to return; either
            'sq' or 'iq'. If 'sq', provide properly normalized mPDF; if 'iq',
            provide the unnormalized mPDF.
    Returns: q-grid and associated magnetic scattering.
    """
    if quantity == 'sq':
        q, fq = sinTransform(r, fr, qmin, qmax, qstep)
        sq = 1.0 * fq
        sq[1:] = fq[1:] / q[1:] + 1
        sq[0] = 0.0
        return q, sq
    elif quantity == 'iq':
        q, iqq = sinTransform(r, fr, qmin, qmax, qstep)
        iq = 1.0 * iqq
        iq[1:] = iq[1:] / q[1:]
        iq[0] = 0.0
        return q, iq
    else:
        print('Please specify a valid magnetic scattering type (sq or iq).')
        return 0 * r, 0 * fr


def calculate_ordered_moment(mc,nucScale,returnUncertainty=False,inputUnc=[]):
    """
    Calculate the ordered moment in Bohr magnetons determined from a fit.
    This only works for a magnetic structure with a single magnetic species.
    
    Args:
        mc: MPDFcalculator object used for the fit
        nucScale (float): nuclear scale factor determined from atomic PDF fit
        returnUncertainty (Boolean): If true, the uncertainty of the ordered
            moment will be estimated and returned.
        inputUnc (list): input uncertainties for mPDF ordered scale, nuclear
            scale factor, and correlation length, in that order.

    Returns: ordered moment in Bohr magnetons. If the correlation length is
             finite, this yields the locally ordered moment at the nearest
             neighbor level. Also returns the estimated uncertainty if selected.
    """
    mstr = mc.magstruc
    struc = mstr.struc
    mstr.calcMagneticAtomRatio()
    ns = mstr.magneticAtomRatio # fraction of total atoms that are magnetic
    bAvg = calculateAvgB(struc) # average nuclear scattering length
    g = mstr.gfactors[0] # g factor for the magnetic structrue
    vectorMag = np.linalg.norm(mstr.spins[0]) # magnitude of spin vectors used in calculation
    m = g * np.sqrt(mc.ordScale * bAvg**2 / (nucScale * ns)) * vectorMag
    if mstr.corrLength != 0:
        # find correlation length and nearest neighbor distance
        xi = mstr.corrLength
        rNN = np.min(np.apply_along_axis(np.linalg.norm, 1, mstr.atoms[1:] - mstr.atoms[0]))
        m *= np.exp(-rNN / 2.0 / xi)
    if returnUncertainty:
        d_mscl, d_nscl, d_xi = inputUnc
        # partial derivatives for error estimation
        dmdmscl = bAvg/np.sqrt(nucScale*ns*mc.ordScale)
        dmdnscl = -bAvg*np.sqrt(mc.ordScale/(ns*nucScale**3))
        if mstr.corrLength != 0:
            dmdxi = rNN*m/(2*xi**2)
        else:
            dmdxi = 0
        dm = np.sqrt(dmdmscl**2*d_mscl**2+dmdnscl**2*d_nscl**2+dmdxi**2*d_xi**2)
        return m, dm
    else:
        return m

def calculate_ordered_scale(magstruc,orderedMoment,nucScale=1.0):
    """
    Calculate the ordered scale factor corresponding to a given ordered moment
    in Bohr magnetons. This only works for a magnetic structure with a single
    magnetic species. 
    
    Args:
        magstruc: MagStructure object
        orderedMoment (float): ordered moment in Bohr magnetons.
        nucScale (float): nuclear scale factor determined from atomic PDF fit.
                          Default value of 1.0
        
    Returns: Ordered scale factor corresponding to the given magnetic moment. If
             the correlation length is finite, this corresponds to the locally
             ordered moment at the nearest neighbor level.
    """
    struc = magstruc.struc
    magstruc.calcMagneticAtomRatio()
    ns = magstruc.magneticAtomRatio # fraction of total atoms that are magnetic
    bAvg = calculateAvgB(struc) # average nuclear scattering length
    g = np.mean(magstruc.gfactors) # g factor for the magnetic structrue
    vectorMag = calculate_avg_spin_magnitude(magstruc) # magnitude of spin vectors used in calculation
    oscl = (orderedMoment / g / vectorMag)**2 * nucScale * ns / bAvg**2 # ordered scale factor
    if magstruc.corrLength != 0:
        # find correlation length and nearest neighbor distance
        xi = magstruc.corrLength
        rNN = np.min(np.apply_along_axis(np.linalg.norm, 1, magstruc.atoms[1:] - magstruc.atoms[0]))
        oscl *= np.exp(rNN / xi)
    return oscl

def compare_mPDF_nucPDF(mcif, rmin=0, rmax=20, ffparamkey='', qdamp=0.02,
              uiso=0.005, ordered_moment=-1):
    """Quick calculation of nuclear PDF and non-deconvoluted mPDF to scale.

    Reads in an mcif to create the corresponding magnetic and atomic structures
    and calculates the mPDF and nuclear PDF to scale with each other for
    the purpose of comparing them.
    
    Args:
        mcif (string): file name of mcif containing the magnetic structure.
        rmin (float): minimum r value to be used in calculation. Default value
                      is 0.
        rmax (float): maximum r value to be used in calculation. Default value
                      is 20.
        ffparamkey (str): string specifying the magnetic form factor, e.g.
                          'Mn2' for Mn2+ or 'Fe3' for Fe3+. If left unspecified,
                          an average magnetic form factor will be used.
        qdamp (float): instrument resolution parameter. Default value 0.02.
        uiso (float): isotropic atomic displacement parameter. Default value 0.005.
        ordered_moment (float): magnitude of ordered moment in Bohr magnetons.
                                If left as the default -1, the length of the
                                spin vectors will be assumed to represent the
                                ordered moment.
    
    Returns:
        Python list containing the MPDFcalculator instance, r grid, deconvoluted
        mPDF, non-deconvoluted mPDF, and nuclear PDF.

    """
    from diffpy.mpdf.mciftools import create_from_mcif
    from diffpy.mpdf.magstructure import MagStructure, MagSpecies
    from diffpy.mpdf.mpdfcalculator import MPDFcalculator
    mstruc = create_from_mcif(mcif, ffparamkey=ffparamkey,
                              rmaxAtoms=rmax)
    mstruc.Uiso = uiso
    mstruc.rmaxAtoms = rmax
    mstruc.makeAll()
    mstruc.calcNetMag()
    mstruc.calcAtomicDensity()
    if ordered_moment == -1:
        ordered_moment = calculate_avg_spin_magnitude(mstruc)
    scale = calculate_ordered_scale(mstruc, ordered_moment)
    mc = MPDFcalculator(mstruc, rmin=rmin, rmax=rmax, qdamp=qdamp,
                        ordScale=scale, paraScale=scale)
    r, g_mag, d_mag = mc.calc(both=True)

    struc = mstruc.struc
    struc.Uisoequiv = uiso
    pc = PDFCalculator(scatteringfactortable='neutron', rmin=rmin,
                       rmax=rmax+0.00001, qdamp=qdamp) # fix indexing error
    r, g_nuc = pc(struc)
    return_items = [mc, r, g_mag, d_mag, g_nuc]

    return return_items

def generate_damping_matrix(e1, e2, e3, xi1, xi2, xi3):
    """Generate the damping matrix from the eigenvectors and eigenvalues.
    
    Args:
        e1, e2, e3 (numpy arrays): right orthonormal eigenvectors
        xi1, xi2, xi3 (floats): correlation lengths along the directions
            of the eigenvectors
    
    Returns:
        dampingMat: 3x3 symmetric damping matrix.
    """
    m1 = np.array([e1,e2,e3]).T
    m2 = np.array([[1.0/xi1**2, 0, 0], [0, 1.0/xi2**2, 0], [0, 0, 1.0/xi3**2]])
    m3 = m1.T
    dampingMat = np.matmul(np.matmul(m1, m2), m3)
    return dampingMat

def estimate_effective_xi(dampingMat, N=1000):
    """
    Estimate the effective correlation length for a given damping matrix. 
    
    Determines the correlation length for a given number of randomly generated
    direction vectors, then calculates the weighted average where the weights
    are given by the volume of the damping matrix ellipsoid subtended by each
    direction vector.
    Note: This is somewhat of an experimental function, not yet rigorously
    shown to be correct.

    Args:
        dampingMat (numpy array): 3x3 damping matrix corresponding to a
            (potentially) anisotropic correlation length.
        N (int): number of random directions to average over. Default is 1000.
        
    Returns: estimated effective correlation length.
    """
    # put matrix in diagonal form
    ev = np.linalg.eig(dampingMat)[0]
    dampingMat = np.array([[ev[0],0,0],[0,ev[1],0],[0,0,ev[2]]])
    # generate uniform sampling of direction
    ths = np.arccos(np.random.uniform(-1, 1, size=N))
    phis = np.random.uniform(-np.pi, np.pi, size=N)
    x = np.sin(ths)*np.cos(phis)
    y = np.sin(ths)*np.sin(phis)
    z = np.cos(ths)
    randomVecs = np.transpose((x, y, z))
    # calculate correlation length along each direction
    mult1 = np.tensordot(dampingMat, randomVecs, axes=(0,1)).T
    xi = 1.0/np.sqrt(np.apply_along_axis(np.sum, 1, randomVecs*mult1))
    # generate weights according to volume subtended by each direction vector
    weights = xi**5 * np.sin(ths)
    return np.sum(weights * xi)/np.sum(weights)

def gauss(grid,s=0.5):
    """Generate a gaussian kernel of arbitrary size and density
    This function generates a 3D guassian kernel based on the input grid and the standard
    deviation chosen. This is a simple replacement for the form factors that will be
    implemented in the future.
    Args:
        grid (array): the 3D spatial coordinates for the gaussian kernel
        s (float): The standard deviation for the gaussian kernel
    """
    g = lambda point: 1/(s*np.sqrt((2*np.pi*s)**3))*np.exp(-1/2*(np.linalg.norm(point)/s)**2)
    return np.apply_along_axis(g,3,grid)

def vec_ac(a1,a2,delta,corr_mode="same"):
    """Correlate two 3D vector fields
    This function computes the autocorrelation of two 3D vector fields on regular 
    grids. The autocorrelation is computed for each vector component and then summed
    Args:
        a1 (array): The virst array to correlate
        a2 (array): The second array to correlate
        delta (float): the spacing between grid points
        corr_mode (string): The mode to use for the scipy correlation function
    """
    ac = correlate(a1[:,:,:,0],a2[:,:,:,0],mode=corr_mode)*delta**3
    ac += correlate(a1[:,:,:,1],a2[:,:,:,1],mode=corr_mode)*delta**3
    ac += correlate(a1[:,:,:,2],a2[:,:,:,2],mode=corr_mode)*delta**3
    return ac

def vec_con(a1,a2,delta,conv_mode="same"):
    """Implement convolution for 3D vector fields
    This function implements a convolution function for 3D vector fields on regular 
    grids. The convolution is computed for each component of the vector fields and summed
    Args:
        a1 (array): The first array to convolve
        a2 (array): The second array to convolve
        delta (float): The grid spacing
        conv_mode (string): The mode to use for scipy convolution
    """
    con = convolve(a1[:,:,:,0],a2[:,:,:,0],mode=conv_mode)*delta**3
    con += convolve(a1[:,:,:,1],a2[:,:,:,1],mode=conv_mode)*delta**3
    con += convolve(a1[:,:,:,2],a2[:,:,:,2],mode=conv_mode)*delta**3
    return con

def ups(grid):
    """A function to generat the Upsilon filter from Roth et.al. (2018)
    This function computes an kernel using the upsilon function defined in Roth et.al.
    (2018), https://doi.org/10.1107/S2052252518006590.
    Args:
        grid (array): the spatial grid over which the kernel is to be defined
    """
    g = lambda point: [0,0,0] if np.abs(np.linalg.norm(point)) <1e-6 else  point/np.linalg.norm(point)**4
    return np.apply_along_axis(g,3,grid)
    
def calculate_g_factors(S, L, J):
    """A function to calculate the Lande g factor and the spin and orbital
    components gS and gL.
    
    Args:
        S (float): Spin angular momentum quantum number in units of hbar.
        L (float): Orbital angular momentum quantum number in units of hbar.
        J (float): Total angular momentum quantum number in units of hbar.

    Returns:
        gS: spin component of Lande g factor
        gL: orbital component of Lande g factor
        g: total Lande g factor
    """
    gS = 1.0 + 1.0*(S*(S+1)-L*(L+1))/(J*(J+1))
    gL = 0.5 + 1.0*(L*(L+1)-S*(S+1))/(2*J*(J+1))
    g = gS + gL
    return gS, gL, g

Functions

def atomsFromSpins(magstruc, spinvecs, fractional=True, returnIdxs=False)

Return the atomic positions corresponding to specified spins.

Args

magstruc
MagSpecies or MagStructure object containing atoms and spins
spinvecs : list or array
spin vectors for which the corresponding atoms should be returned.
fractional : boolean
set as True if the atomic positions are to be returned as fractional coordinates of the crystallographic lattice vectors.
returnIdxs : boolean
if True, the indices of the atoms will also be returned.

Returns

List of arrays of atoms corresponding to the spins.

Expand source code
def atomsFromSpins(magstruc, spinvecs, fractional=True, returnIdxs=False):
    """Return the atomic positions corresponding to specified spins.

    Args:
        magstruc: MagSpecies or MagStructure object containing atoms and spins
        spinvecs (list or array): spin vectors for which the
            corresponding atoms should be returned.
        fractional (boolean): set as True if the atomic positions are to be
            returned as fractional coordinates of the crystallographic lattice
            vectors.
        returnIdxs (boolean): if True, the indices of the atoms will also be
            returned.

    Returns:
        List of arrays of atoms corresponding to the spins.
    """
    if len(np.array(spinvecs).shape) == 1:
        spinvecs = [spinvecs]
    atoms = []
    idxs = []
    badlist = []
    for spin in spinvecs:
        mask = np.all(np.round(magstruc.spins, decimals=5) ==
                      np.round(spin, decimals=5), axis=1)
        goodatoms = magstruc.atoms[mask]
        goodidxs = np.where(mask)[0]
        if fractional:
            goodatoms = magstruc.struc.lattice.fractional(goodatoms)
        atoms.append(goodatoms)
        idxs.append(goodidxs)
        if len(goodidxs) == 0:
            badlist.append(spin)
    if len(badlist) > 0:
        print('The following spins were not found in the structure:')
        for bad in badlist:
            print(bad)
    if not returnIdxs:
        return atoms
    else:
        return atoms, idxs
def calculateAvgB(struc)

Calculate average coherent neutron scattering length.

Args

struc
Diffpy Structure object

Returns

bAvg
average coherent neutron scattering length.
Expand source code
def calculateAvgB(struc):
    """Calculate average coherent neutron scattering length.

    Args:
        struc: Diffpy Structure object

    Returns:
        bAvg: average coherent neutron scattering length.
    """
    totalOcc = struc.occupancy.sum()
    bAvg = 0
    for idx, atom in enumerate(struc):
        el = re.findall("[a-zA-Z]+", atom.element)
        b = getattr(pt, el[0]).neutron.b_c
        bAvg += struc.occupancy[idx] * b
    bAvg /= totalOcc
    return bAvg
def calculateDr(r, fr, q, ff, paraScale=1.0, rmintr=-5.0, rmaxtr=5.0, drtr=0.01, qmin=0, qmax=-1, K1=None, K2=None)

Calculate the unnormalized mPDF quantity D(r).

This module requires a normalized mPDF as an input, as well as a magnetic form factor and associated q grid.

Args

r : numpy array
r grid for the properly normalized mPDF.
fr : numpy array
the properly normalized mPDF.
q : numpy array
grid of momentum transfer values used for calculating the magnetic form factor.
ff : numpy array
magnetic form factor. Same shape as ffqgrid.
paraScale : float
scale factor for the paramagnetic part of the unnormalized mPDF function D(r).
rmintr : float
minimum value of r for the Fourier transform of the magnetic form factor required for unnormalized mPDF.
rmaxtr : float
maximum value of r for the Fourier transform of the magnetic form factor required for unnormalized mPDF.
drtr : float
step size for r-grid used for calculating Fourier transform of magnetic form mactor.
qmin : float
minimum experimentally accessible q-value (to be used for simulating termination ripples). If <0, no termination effects are included.
qmax : float
maximum experimentally accessible q-value (to be used for simulating termination ripples). If <0, no termination effects are included.
K1 : float
a constant used for calculating Dr; should be averaged over all magnetic species. Important if physical information is to be extracted from mPDF scale factors, e.g. moment size.
K2 : float
another constant used for calculating Dr.

Returns: numpy array for the unnormalized mPDF Dr.

Expand source code
def calculateDr(r, fr, q, ff, paraScale=1.0, rmintr=-5.0, rmaxtr=5.0,
                drtr=0.01, qmin=0, qmax=-1, K1=None, K2=None):
    """Calculate the unnormalized mPDF quantity D(r).

    This module requires a normalized mPDF as an input, as well as a magnetic
    form factor and associated q grid.

    Args:
        r (numpy array): r grid for the properly normalized mPDF.
        fr (numpy array): the properly normalized mPDF.
        q (numpy array): grid of momentum transfer values used for calculating
            the magnetic form factor.
        ff (numpy array): magnetic form factor. Same shape as ffqgrid.
        paraScale (float): scale factor for the paramagnetic part of the
            unnormalized mPDF function D(r).
        rmintr (float): minimum value of r for the Fourier transform of the
            magnetic form factor required for unnormalized mPDF.
        rmaxtr (float): maximum value of r for the Fourier transform of the
            magnetic form factor required for unnormalized mPDF.
        drtr (float): step size for r-grid used for calculating Fourier
            transform of magnetic form mactor.
        qmin (float): minimum experimentally accessible q-value (to be used
            for simulating termination ripples). If <0, no termination effects
            are included.
        qmax (float): maximum experimentally accessible q-value (to be used
            for simulating termination ripples). If <0, no termination effects
            are included.
        K1 (float): a constant used for calculating Dr; should be averaged
            over all magnetic species. Important if physical information is
            to be extracted from mPDF scale factors, e.g. moment size.
        K2 (float): another constant used for calculating Dr.

    Returns: numpy array for the unnormalized mPDF Dr.
    """
    if K1 is None:
        K1 = 0.66667 * (1.913 * 2.81794 / 2.0) ** 2 * 2.0 ** 2 * 0.5 * (0.5 + 1)
    if K2 is None:
        K2 = K1
    rsr, sr = cosTransform(q, ff, rmintr, rmaxtr, drtr)
    #### Double-check whether or not I should normalize sr!
    # norm = np.trapz(sr,rsr)
    # sr /= norm
    # sr = np.sqrt(np.pi/2.0)*sr ### I don't think we should multiply by sqrt(pi/2)
    rSr, Sr = cv(rsr, sr, rsr, sr)
    rDr, Dr = cv(r, K1 / (2.0 * np.pi) * fr, rSr, Sr)
    para = -K2 / np.pi * np.gradient(Sr, rSr[1] - rSr[0])  ### paramagnetic term in d(r)
    rpara, para = rSr, para
    # The code below is used to apply the termination convolution to the paramagnetic peak.
    # I don't think it's actually necessary.
#    if qmin >= 0 and qmax > qmin:
#        rstep = r[1] - r[0]
#        rth = np.arange(0.0, r.max() + rstep, rstep)
#        rth[0] = 1e-4 * rstep  # avoid infinities at r = 0
#        th = (np.sin(qmax * rth) - np.sin(qmin * rth)) / np.pi / rth
#        rth[0] = 0.0
#        rpara, para = cv(rSr, para, rth, th)
#    else:
#        rpara, para = rSr, para
    # make sure para and Dr match up in shape
    dr = r[2] - r[1]
    goodslice = np.logical_and(rDr >= r.min() - 0.5 * dr, rDr <= r.max() + 0.5 * dr)
    Dr = Dr[goodslice]
    para = para[rpara >= r.min() - 0.5 * dr]
    if para.shape[0] < Dr.shape[0]:
        para = np.concatenate((para, np.zeros(Dr.shape[0] - para.shape[0])))
    else:
        para = para[:Dr.shape[0]]
    Dr += paraScale * para
    return Dr
def calculateMagScatt(r, fr, qmin=0.0, qmax=20.0, qstep=0.01, quantity='sq')

Calculate the magnetic scattering via Fourier transform of the mPDF.

Args

r : numpy array
r grid for the properly normalized mPDF.
fr : numpy array
the mPDF, either normalized or unnormalized.
qmin : float
minimum value of the output q-grid.
qmax : float
maximum value of the output q-grid.
qstep : float
spacing for the q-grid. unnormalized mPDF function D(r).
quantity : str
type of magnetic scattering quantity to return; either 'sq' or 'iq'. If 'sq', provide properly normalized mPDF; if 'iq', provide the unnormalized mPDF.

Returns: q-grid and associated magnetic scattering.

Expand source code
def calculateMagScatt(r, fr, qmin=0.0, qmax=20.0, qstep=0.01, quantity='sq'):
    """Calculate the magnetic scattering via Fourier transform of the mPDF.

    Args:
        r (numpy array): r grid for the properly normalized mPDF.
        fr (numpy array): the mPDF, either normalized or unnormalized.
        qmin (float): minimum value of the output q-grid.
        qmax (float): maximum value of the output q-grid.
        qstep (float): spacing for the q-grid.
            unnormalized mPDF function D(r).
        quantity (str): type of magnetic scattering quantity to return; either
            'sq' or 'iq'. If 'sq', provide properly normalized mPDF; if 'iq',
            provide the unnormalized mPDF.
    Returns: q-grid and associated magnetic scattering.
    """
    if quantity == 'sq':
        q, fq = sinTransform(r, fr, qmin, qmax, qstep)
        sq = 1.0 * fq
        sq[1:] = fq[1:] / q[1:] + 1
        sq[0] = 0.0
        return q, sq
    elif quantity == 'iq':
        q, iqq = sinTransform(r, fr, qmin, qmax, qstep)
        iq = 1.0 * iqq
        iq[1:] = iq[1:] / q[1:]
        iq[0] = 0.0
        return q, iq
    else:
        print('Please specify a valid magnetic scattering type (sq or iq).')
        return 0 * r, 0 * fr
def calculate_avg_spin_magnitude(magstruc, method='species')

Calculate average spin magnitude from a MagStructure instance.

Args

method : string
must be 'full' or 'species'. If 'full', the mean magnitude of all spins in the MagStructure will be returned. If 'species', then just a single spin from each species will be included in the calculation and weighted according to the species fraction. 'species' is faster and should be used if all spins belonging to a given species have the same magnitude.

Returns: Mean magnitude of spins.

Expand source code
def calculate_avg_spin_magnitude(magstruc, method='species'):
    """Calculate average spin magnitude from a MagStructure instance.
    
    Args:
        method (string): must be 'full' or 'species'. If 'full', the
            mean magnitude of all spins in the MagStructure will be
            returned. If 'species', then just a single spin from
            each species will be included in the calculation and weighted
            according to the species fraction. 'species' is faster and
            should be used if all spins belonging to a given species
            have the same magnitude.
    
    Returns: Mean magnitude of spins.
    """
    if method == 'species':
        try:
            magnitude = 0
            idx_dict = magstruc.getSpeciesIdxs()
            for key in magstruc.species:
                frac = magstruc.fractions[key]
                species_magnitude = np.linalg.norm(magstruc.spins[idx_dict[key]])
                magnitude += frac * species_magnitude
            return magnitude
        except:
            return np.mean(np.apply_along_axis(np.linalg.norm, 1, magstruc.spins))
    elif method == 'full':
        return np.mean(np.apply_along_axis(np.linalg.norm, 1, magstruc.spins))
    else:
        print("Please choose either 'species' or 'full' for the method.")
        print("Using 'full' for now.")
        return np.mean(np.apply_along_axis(np.linalg.norm, 1, magstruc.spins))
def calculate_g_factors(S, L, J)

A function to calculate the Lande g factor and the spin and orbital components gS and gL.

Args

S : float
Spin angular momentum quantum number in units of hbar.
L : float
Orbital angular momentum quantum number in units of hbar.
J : float
Total angular momentum quantum number in units of hbar.

Returns

gS
spin component of Lande g factor
gL
orbital component of Lande g factor
g
total Lande g factor
Expand source code
def calculate_g_factors(S, L, J):
    """A function to calculate the Lande g factor and the spin and orbital
    components gS and gL.
    
    Args:
        S (float): Spin angular momentum quantum number in units of hbar.
        L (float): Orbital angular momentum quantum number in units of hbar.
        J (float): Total angular momentum quantum number in units of hbar.

    Returns:
        gS: spin component of Lande g factor
        gL: orbital component of Lande g factor
        g: total Lande g factor
    """
    gS = 1.0 + 1.0*(S*(S+1)-L*(L+1))/(J*(J+1))
    gL = 0.5 + 1.0*(L*(L+1)-S*(S+1))/(2*J*(J+1))
    g = gS + gL
    return gS, gL, g
def calculate_ordered_moment(mc, nucScale, returnUncertainty=False, inputUnc=[])

Calculate the ordered moment in Bohr magnetons determined from a fit. This only works for a magnetic structure with a single magnetic species.

Args

mc
MPDFcalculator object used for the fit
nucScale : float
nuclear scale factor determined from atomic PDF fit
returnUncertainty : Boolean
If true, the uncertainty of the ordered moment will be estimated and returned.
inputUnc : list
input uncertainties for mPDF ordered scale, nuclear scale factor, and correlation length, in that order.

Returns: ordered moment in Bohr magnetons. If the correlation length is finite, this yields the locally ordered moment at the nearest neighbor level. Also returns the estimated uncertainty if selected.

Expand source code
def calculate_ordered_moment(mc,nucScale,returnUncertainty=False,inputUnc=[]):
    """
    Calculate the ordered moment in Bohr magnetons determined from a fit.
    This only works for a magnetic structure with a single magnetic species.
    
    Args:
        mc: MPDFcalculator object used for the fit
        nucScale (float): nuclear scale factor determined from atomic PDF fit
        returnUncertainty (Boolean): If true, the uncertainty of the ordered
            moment will be estimated and returned.
        inputUnc (list): input uncertainties for mPDF ordered scale, nuclear
            scale factor, and correlation length, in that order.

    Returns: ordered moment in Bohr magnetons. If the correlation length is
             finite, this yields the locally ordered moment at the nearest
             neighbor level. Also returns the estimated uncertainty if selected.
    """
    mstr = mc.magstruc
    struc = mstr.struc
    mstr.calcMagneticAtomRatio()
    ns = mstr.magneticAtomRatio # fraction of total atoms that are magnetic
    bAvg = calculateAvgB(struc) # average nuclear scattering length
    g = mstr.gfactors[0] # g factor for the magnetic structrue
    vectorMag = np.linalg.norm(mstr.spins[0]) # magnitude of spin vectors used in calculation
    m = g * np.sqrt(mc.ordScale * bAvg**2 / (nucScale * ns)) * vectorMag
    if mstr.corrLength != 0:
        # find correlation length and nearest neighbor distance
        xi = mstr.corrLength
        rNN = np.min(np.apply_along_axis(np.linalg.norm, 1, mstr.atoms[1:] - mstr.atoms[0]))
        m *= np.exp(-rNN / 2.0 / xi)
    if returnUncertainty:
        d_mscl, d_nscl, d_xi = inputUnc
        # partial derivatives for error estimation
        dmdmscl = bAvg/np.sqrt(nucScale*ns*mc.ordScale)
        dmdnscl = -bAvg*np.sqrt(mc.ordScale/(ns*nucScale**3))
        if mstr.corrLength != 0:
            dmdxi = rNN*m/(2*xi**2)
        else:
            dmdxi = 0
        dm = np.sqrt(dmdmscl**2*d_mscl**2+dmdnscl**2*d_nscl**2+dmdxi**2*d_xi**2)
        return m, dm
    else:
        return m
def calculate_ordered_scale(magstruc, orderedMoment, nucScale=1.0)

Calculate the ordered scale factor corresponding to a given ordered moment in Bohr magnetons. This only works for a magnetic structure with a single magnetic species.

Args

magstruc
MagStructure object
orderedMoment : float
ordered moment in Bohr magnetons.
nucScale : float
nuclear scale factor determined from atomic PDF fit. Default value of 1.0

Returns: Ordered scale factor corresponding to the given magnetic moment. If the correlation length is finite, this corresponds to the locally ordered moment at the nearest neighbor level.

Expand source code
def calculate_ordered_scale(magstruc,orderedMoment,nucScale=1.0):
    """
    Calculate the ordered scale factor corresponding to a given ordered moment
    in Bohr magnetons. This only works for a magnetic structure with a single
    magnetic species. 
    
    Args:
        magstruc: MagStructure object
        orderedMoment (float): ordered moment in Bohr magnetons.
        nucScale (float): nuclear scale factor determined from atomic PDF fit.
                          Default value of 1.0
        
    Returns: Ordered scale factor corresponding to the given magnetic moment. If
             the correlation length is finite, this corresponds to the locally
             ordered moment at the nearest neighbor level.
    """
    struc = magstruc.struc
    magstruc.calcMagneticAtomRatio()
    ns = magstruc.magneticAtomRatio # fraction of total atoms that are magnetic
    bAvg = calculateAvgB(struc) # average nuclear scattering length
    g = np.mean(magstruc.gfactors) # g factor for the magnetic structrue
    vectorMag = calculate_avg_spin_magnitude(magstruc) # magnitude of spin vectors used in calculation
    oscl = (orderedMoment / g / vectorMag)**2 * nucScale * ns / bAvg**2 # ordered scale factor
    if magstruc.corrLength != 0:
        # find correlation length and nearest neighbor distance
        xi = magstruc.corrLength
        rNN = np.min(np.apply_along_axis(np.linalg.norm, 1, magstruc.atoms[1:] - magstruc.atoms[0]))
        oscl *= np.exp(rNN / xi)
    return oscl
def calculatemPDF(xyz, sxyz, gfactors=array([2.]), calcIdxs=array([0]), rstep=0.01, rmin=0.0, rmax=20.0, psigma=0.1, qmin=0, qmax=-1, qdamp=0.0, extendedrmin=4.0, extendedrmax=4.0, orderedScale=1.0, K1=14.529999504072979, rho0=0, netMag=0, corrLength=0, linearTermMethod='exact', applyEnvelope=False, qwindow=array([0]), qgrid=array([0]))

Calculate the normalized mPDF.

At minimum, this module requires input lists of atomic positions and spins.

Args

xyz : numpy array
list of atomic coordinates of all the magnetic atoms in the structure.
sxyz : numpy array
triplets giving the spin vectors of all the atoms, in the same order as the xyz array provided as input.
gfactors : numpy array
Lande g-factors of spins in same order as spin array.
calcIdxs : python list
list giving the indices of the atoms array specifying the atoms to be used as the origin when calculating the mPDF.
rstep : float
step size for r-grid of calculated mPDF.
rmin : float
minimum value of r for which mPDF should be calculated.
rmax : float
maximum value of r for which mPDF should be calculated.
psigma(float): std deviation (in Angstroms) of Gaussian peak
to be convoluted with the calculated mPDF to simulate thermal
motion.
qmin : float
minimum experimentally accessible q-value (to be used for simulating termination ripples). If <0, no termination effects are included.
qmax : float
maximum experimentally accessible q-value (to be used for simulating termination ripples). If <0, no termination effects are included.
qdamp : float
usual PDF qdamp parameter. Turned off if set to zero.
extendedrmin : float
extension of the r-grid on which the mPDF is calculated to properly account for contribution of pairs just before the boundary.
extendedrmax : float
extension of the r-grid on which the mPDF is calculated to properly account for contribution of pairs just outside the boundary.
ordScale : float
overall scale factor for the mPDF function f(r).
K1 : float
A constant related to the total angular momentum quantum number and the average Lande splitting factor.
rho0 : float
number of magnetic moments per cubic Angstrom in the magnetic structure; default value is 0.
netMag : float
net magnetization in Bohr magnetons per magnetic moment in the sample; default is 0. Only nonzero for ferro/ferrimagnets or canted antiferromagnets.
corrLength : float
exponential magnetic correlation length of the structure; default is 0, which is considered to be infinite. This is important to get the linear term right for samples with nonzero net magnetization.
linearTermMethod : string
determines how the calculation will handle the linear term present for magnetic structures with a net magnetization. Options are: 'exact'; slope will be calculated from the values of MagStructure.rho0 and MagStructure.netMag, damping set by MagStructure.corrLength. 'autoslope': slope will be determined by least-squares minimization of the calculated mPDF, thereby ensuring that the mPDF oscillates around zero, as it is supposed to. Damping set by MagStructure.corrLength. 'fullauto': slope and damping set by least-squares minimization. This should only be used in the case of an anisotropic correlation length. Note that any other option will be converted to 'exact'.
applyEnvelope : boolean
if True, an exponential damping envelope will be applied to the calculated f(r) (not including the linear term). The parameter corrLength is used to determine the envelope.
qwindow : numpy array
Q-space window function applied to the data prior to Fourier transformation. Not used by default.
qgrid : numpy array
Q-space grid on which the window function is defined.

Returns: numpy arrays for r and the mPDF fr, on the extended grid.

Expand source code
def calculatemPDF(xyz, sxyz, gfactors=np.array([2.0]), calcIdxs=np.array([0]),
                  rstep=0.01, rmin=0.0, rmax=20.0, psigma=0.1, qmin=0,
                  qmax=-1, qdamp=0.0, extendedrmin=4.0, extendedrmax=4.0,
                  orderedScale=1.0,
                  K1=0.66667 * (1.913 * 2.81794 / 2.0) ** 2 * 2.0 ** 2 * 0.5 * (0.5 + 1),
                  rho0=0, netMag=0, corrLength=0, linearTermMethod='exact',
                  applyEnvelope=False, qwindow=np.array([0]),
                  qgrid=np.array([0])):
    """Calculate the normalized mPDF.

    At minimum, this module requires input lists of atomic positions and spins.

    Args:
        xyz (numpy array): list of atomic coordinates of all the magnetic
            atoms in the structure.
        sxyz (numpy array): triplets giving the spin vectors of all the
            atoms, in the same order as the xyz array provided as input.
        gfactors (numpy array): Lande g-factors of spins in same order as
            spin array.
        calcIdxs (python list): list giving the indices of the atoms array
            specifying the atoms to be used as the origin when calculating
            the mPDF.
        rstep (float): step size for r-grid of calculated mPDF.
        rmin (float): minimum value of r for which mPDF should be calculated.
        rmax (float): maximum value of r for which mPDF should be calculated.
        psigma(float): std deviation (in Angstroms) of Gaussian peak
            to be convoluted with the calculated mPDF to simulate thermal
            motion.
        qmin (float): minimum experimentally accessible q-value (to be used
            for simulating termination ripples). If <0, no termination effects
            are included.
        qmax (float): maximum experimentally accessible q-value (to be used
            for simulating termination ripples). If <0, no termination effects
            are included.
        qdamp (float): usual PDF qdamp parameter. Turned off if set to zero.
        extendedrmin (float): extension of the r-grid on which the mPDF is
            calculated to properly account for contribution of pairs just
            before the boundary.
        extendedrmax (float): extension of the r-grid on which the mPDF is
            calculated to properly account for contribution of pairs just
            outside the boundary.
        ordScale (float): overall scale factor for the mPDF function f(r).
        K1 (float): A constant related to the total angular momentum quantum
            number and the average Lande splitting factor.
        rho0 (float): number of magnetic moments per cubic Angstrom in the
            magnetic structure; default value is 0.
        netMag (float): net magnetization in Bohr magnetons per magnetic moment
            in the sample; default is 0. Only nonzero for ferro/ferrimagnets or
            canted antiferromagnets.
        corrLength (float): exponential magnetic correlation length of the
            structure; default is 0, which is considered to be infinite.
            This is important to get the linear term right for samples
            with nonzero net magnetization.
        linearTermMethod (string): determines how the calculation will
            handle the linear term present for magnetic structures with a net
            magnetization. Options are:
            'exact'; slope will be calculated from the values of
                MagStructure.rho0 and MagStructure.netMag, damping set by
                MagStructure.corrLength.
            'autoslope': slope will be determined by least-squares
                minimization of the calculated mPDF, thereby ensuring that
                the mPDF oscillates around zero, as it is supposed to.
                Damping set by MagStructure.corrLength.
            'fullauto': slope and damping set by least-squares minimization.
                This should only be used in the case of an anisotropic
                correlation length.
            Note that any other option will be converted to 'exact'.
        applyEnvelope (boolean): if True, an exponential damping envelope will
            be applied to the calculated f(r) (not including the linear term).
            The parameter corrLength is used to determine the envelope.
        qwindow (numpy array): Q-space window function applied to the data
            prior to Fourier transformation. Not used by default.
        qgrid (numpy array): Q-space grid on which the window function is
            defined.
    Returns: numpy arrays for r and the mPDF fr, on the extended grid.
        """
    # check if g-factors have been provided
    if sxyz.shape[0] != gfactors.shape[0]:
        gfactors = 2.0 * np.ones(sxyz.shape[0])
    # calculate s1, s2
    r = np.arange(rmin - extendedrmin, rmax + extendedrmax + rstep, rstep)
    r = np.round(r, decimals=6)
    # don't calculate for negative r
    if r[0] < 0:
        startIdx = np.argmin(np.abs(r))
        if r[startIdx] < 0:
            startIdx += 1
        r = r[startIdx:]
    rbin = np.concatenate([r - rstep / 2, [r[-1] + rstep / 2]])

    s1 = np.zeros(len(r))
    s2 = np.zeros(len(r))

    if type(calcIdxs) is str:
        if calcIdxs == 'all':
            calcIdxs = np.arange(len(xyz))

    for uu in calcIdxs:
        ri = xyz[uu]
        rj = xyz
        si = sxyz[uu]
        sj = sxyz
        gi = gfactors[uu]
        gj = gfactors

        dxyz = rj - ri
        d2xyz = np.sum((dxyz) ** 2, axis=1).reshape(dxyz.shape[0], 1)
        d1xyz = np.sqrt(d2xyz)
        d1xyz[uu] = 1e-6  ### avoid divide by zero problem
        d1xyzr = d1xyz.ravel()

        xh = dxyz / d1xyz
        yh = si - xh * np.sum(si * xh, axis=1).reshape(dxyz.shape[0], 1)
        yh_dis = np.sum(yh ** 2, axis=1)
        yh_ind = np.nonzero(np.abs(yh_dis) < 1e-10)
        yh[yh_ind] = [0, 0, 0]
        yh_dis[yh_ind] = 1e-6  ### avoid divide by zero problem

        aij = np.sum(si * yh, axis=1) * np.sum(sj * yh, axis=1) / yh_dis
        aij[uu] = 0
        bij = 2 * np.sum(si * xh, axis=1) * np.sum(sj * xh, axis=1) - aij

        w2 = bij / d1xyzr ** 3

        d1xyzr[uu] = 0.0
        s1 += np.histogram(d1xyzr, bins=rbin, weights=gi * gj * aij)[0]
        s2 += np.histogram(d1xyzr, bins=rbin, weights=gi * gj * w2)[0]

    # apply Gaussian shape function
    if psigma != None:
        x = np.arange(-3, 3, rstep)
        y = np.exp(-x ** 2 / psigma ** 2 / 2) * (1 / np.sqrt(2 * np.pi) / psigma)

        s1[0] = 0
        s1 = fftconvolve(s1, y)
        s1 = s1[int(len(x) / 2): int(-len(x) / 2 + 1)]

        s2 = fftconvolve(s2, y) * rstep
        s2 = s2[int(len(x) / 2): int(-len(x) / 2 + 1)]

    ss2 = np.cumsum(s2)

    if r[0] == 0:
        r[0] = 1e-4 * rstep  # avoid infinities at r = 0
        fr = s1 / r + r * (ss2[-1] - ss2)
        r[0] = 0
    else:
        fr = s1 / r + r * (ss2[-1] - ss2)

    ### prefactor
    fr /= len(calcIdxs) * K1 / (1.913 * 2.81794 / 2.0) ** 2
    if applyEnvelope and (corrLength != 0):
        fr *= np.exp(-r/corrLength)

    ### Now include the linear term
    if linearTermMethod == 'autoslope':
        if corrLength == 0:
            def residual(p):
                return fr - p[0] * r
            opt = least_squares(residual, [0])
            linearTerm = opt.x[0] * r
        else:
            def residual(p):
                return fr - p[0] * r * np.exp(-r/corrLength)
            opt = least_squares(residual, [0])
            linearTerm = opt.x[0] * r * np.exp(-r/corrLength)
    elif linearTermMethod == 'fullauto':    
            def residual(p):
                return fr - p[0] * r * np.exp(-r/p[1])
            opt = least_squares(residual, [0, 100], bounds=[[-1e6,0], [1e6,1e6]])
            linearTerm = opt.x[0] * r * np.exp(-r/opt.x[1])
    else:
        if corrLength == 0:
            linearTerm = 4 * np.pi * r * rho0 * (2.0/3.0) * netMag**2  \
                         / ( K1 / (1.913 * 2.81794 / 2.0) ** 2)
        else:
            linearTerm = 4 * np.pi * r * rho0 * (2.0 / 3.0) * netMag ** 2 * np.exp(-r/corrLength) \
                          / ( K1 / (1.913 * 2.81794 / 2.0) ** 2)
    fr -= linearTerm


    ### apply the scale factor and qdamp
    fr *= orderedScale * np.exp((-1.0 * (qdamp * r) ** 2) / 2)
    # Do the convolution with the termination function if qmin/qmax or a window
    # function have been given
    if qmin >= 0 and qmax > qmin and len(qgrid) == 1: # no window function
        rth = np.arange(-rmax - extendedrmax, rmax + extendedrmax + rstep, rstep)
        th = (qmax/np.pi) * np.sinc(qmax * rth / np.pi) - (qmin / np.pi) * np.sinc(qmin * rth / np.pi)
        rcv, frcv = cv(r, fr, rth, th, align=True, normalize=True)
    elif len(qgrid) > 1: # use the window function
        rFT, windowFT = fourierTransform(qgrid, qwindow, rmin=-rmax, rmax=rmax, rstep=rstep)
        windowFT = np.real(windowFT)
        rcv, frcv = cv(r, fr, rFT, windowFT, align=True, normalize=True)
    else:
        rcv, frcv = r, fr

    return rcv, frcv
def compare_mPDF_nucPDF(mcif, rmin=0, rmax=20, ffparamkey='', qdamp=0.02, uiso=0.005, ordered_moment=-1)

Quick calculation of nuclear PDF and non-deconvoluted mPDF to scale.

Reads in an mcif to create the corresponding magnetic and atomic structures and calculates the mPDF and nuclear PDF to scale with each other for the purpose of comparing them.

Args

mcif : string
file name of mcif containing the magnetic structure.
rmin : float
minimum r value to be used in calculation. Default value is 0.
rmax : float
maximum r value to be used in calculation. Default value is 20.
ffparamkey : str
string specifying the magnetic form factor, e.g. 'Mn2' for Mn2+ or 'Fe3' for Fe3+. If left unspecified, an average magnetic form factor will be used.
qdamp : float
instrument resolution parameter. Default value 0.02.
uiso : float
isotropic atomic displacement parameter. Default value 0.005.
ordered_moment : float
magnitude of ordered moment in Bohr magnetons. If left as the default -1, the length of the spin vectors will be assumed to represent the ordered moment.

Returns

Python list containing the MPDFcalculator instance, r grid, deconvoluted mPDF, non-deconvoluted mPDF, and nuclear PDF.

Expand source code
def compare_mPDF_nucPDF(mcif, rmin=0, rmax=20, ffparamkey='', qdamp=0.02,
              uiso=0.005, ordered_moment=-1):
    """Quick calculation of nuclear PDF and non-deconvoluted mPDF to scale.

    Reads in an mcif to create the corresponding magnetic and atomic structures
    and calculates the mPDF and nuclear PDF to scale with each other for
    the purpose of comparing them.
    
    Args:
        mcif (string): file name of mcif containing the magnetic structure.
        rmin (float): minimum r value to be used in calculation. Default value
                      is 0.
        rmax (float): maximum r value to be used in calculation. Default value
                      is 20.
        ffparamkey (str): string specifying the magnetic form factor, e.g.
                          'Mn2' for Mn2+ or 'Fe3' for Fe3+. If left unspecified,
                          an average magnetic form factor will be used.
        qdamp (float): instrument resolution parameter. Default value 0.02.
        uiso (float): isotropic atomic displacement parameter. Default value 0.005.
        ordered_moment (float): magnitude of ordered moment in Bohr magnetons.
                                If left as the default -1, the length of the
                                spin vectors will be assumed to represent the
                                ordered moment.
    
    Returns:
        Python list containing the MPDFcalculator instance, r grid, deconvoluted
        mPDF, non-deconvoluted mPDF, and nuclear PDF.

    """
    from diffpy.mpdf.mciftools import create_from_mcif
    from diffpy.mpdf.magstructure import MagStructure, MagSpecies
    from diffpy.mpdf.mpdfcalculator import MPDFcalculator
    mstruc = create_from_mcif(mcif, ffparamkey=ffparamkey,
                              rmaxAtoms=rmax)
    mstruc.Uiso = uiso
    mstruc.rmaxAtoms = rmax
    mstruc.makeAll()
    mstruc.calcNetMag()
    mstruc.calcAtomicDensity()
    if ordered_moment == -1:
        ordered_moment = calculate_avg_spin_magnitude(mstruc)
    scale = calculate_ordered_scale(mstruc, ordered_moment)
    mc = MPDFcalculator(mstruc, rmin=rmin, rmax=rmax, qdamp=qdamp,
                        ordScale=scale, paraScale=scale)
    r, g_mag, d_mag = mc.calc(both=True)

    struc = mstruc.struc
    struc.Uisoequiv = uiso
    pc = PDFCalculator(scatteringfactortable='neutron', rmin=rmin,
                       rmax=rmax+0.00001, qdamp=qdamp) # fix indexing error
    r, g_nuc = pc(struc)
    return_items = [mc, r, g_mag, d_mag, g_nuc]

    return return_items
def cosTransform(q, fq, rmin=0.0, rmax=50.0, rstep=0.1)

Compute the cosine Fourier transform of a function.

This method uses the FFT algorithm and returns correctly spaced x and y arrays on an even and specifiable grid. The input grid must be evenly spaced.

Args

q : numpy array
independent variable for function to be transformed
fq : numpy array
dependent variable for function to be transformed
rmin : float, default = 0.0
min value of conjugate independent variable grid
rmax : float, default = 50.0
maximum value of conjugate independent variable grid
rstep : float, default = 0.1
grid spacing for conjugate independent variable

Returns

r (numpy array): independent variable grid for transformed quantity

fr (numpy array): cosine Fourier transform of fq

Expand source code
def cosTransform(q, fq, rmin=0.0, rmax=50.0, rstep=0.1):  # requires even q-grid
    """Compute the cosine Fourier transform of a function.

    This method uses the FFT algorithm and returns correctly spaced x and
    y arrays on an even and specifiable grid. The input grid must be evenly
    spaced.

    Args:
        q (numpy array): independent variable for function to be transformed
        fq (numpy array): dependent variable for function to be transformed
        rmin (float, default = 0.0): min value of conjugate independent variable
            grid
        rmax (float, default = 50.0): maximum value of conjugate independent
            variable grid
        rstep (float, default = 0.1): grid spacing for conjugate independent
            variable

    Returns:
        r (numpy array): independent variable grid for transformed quantity

        fr (numpy array): cosine Fourier transform of fq
    """
    r, fr = fourierTransform(q, fq, rmin, rmax, rstep)
    fr = np.real(fr)
    return r, fr
def cosTransformDirectIntegration(q, fq, rmin=0.0, rmax=50.0, rstep=0.1)

Compute the cosine Fourier transform of a function.

This method uses direct integration rather than an FFT and doesn't require an even grid. The grid for the Fourier transform is even and specifiable.

Args

q : numpy array
independent variable for function to be transformed
fq : numpy array
dependent variable for function to be transformed
rmin : float, default = 0.0
min value of conjugate independent variable grid
rmax : float, default = 50.0
maximum value of conjugate independent variable grid
rstep : float, default = 0.1
grid spacing for conjugate independent variable

Returns

r (numpy array): independent variable grid for transformed quantity

fr (numpy array): cosine Fourier transform of fq

Expand source code
def cosTransformDirectIntegration(q, fq, rmin=0.0, rmax=50.0, rstep=0.1):  # does not require even q-grid
    """Compute the cosine Fourier transform of a function.

    This method uses direct integration rather than an FFT and doesn't require
    an even grid. The grid for the Fourier transform is even and specifiable.

    Args:
        q (numpy array): independent variable for function to be transformed
        fq (numpy array): dependent variable for function to be transformed
        rmin (float, default = 0.0): min value of conjugate independent variable
            grid
        rmax (float, default = 50.0): maximum value of conjugate independent
            variable grid
        rstep (float, default = 0.1): grid spacing for conjugate independent
            variable

    Returns:
        r (numpy array): independent variable grid for transformed quantity

        fr (numpy array): cosine Fourier transform of fq
    """
    lostep = int(np.ceil((rmin - 1e-8) / rstep))
    histep = int(np.floor((rmax + 1e-8) / rstep)) + 1
    r = np.arange(lostep, histep) * rstep
    qrmat = np.outer(r, q)
    integrand = fq * np.cos(qrmat)
    fr = np.sqrt(2.0 / np.pi) * np.trapz(integrand, q)
    return r, fr
def cv(x1, y1, x2, y2, align=False, normalize=False)

Perform the convolution of two functions and give the correct output.

Args

x1 : numpy array
independent variable of first function; must be in ascending order
y1 : numpy array
dependent variable of first function
x2 : numpy array
independent variable of second function; must have same grid spacing as x1
y2 : numpy array
dependent variable of second function
align : boolean
if True, output grid will be identical to input x1, y1
normalize : boolean
if True, the output will be scaled to match y1

Returns

xcv (numpy array): independent variable of convoluted function, has dimension len(x1) + len(x2) - 1 unless align option us used.

ycv (numpy array): convolution of y1 and y2, same shape as xcv

Expand source code
def cv(x1, y1, x2, y2, align=False, normalize=False):
    """Perform the convolution of two functions and give the correct output.


    Args:
        x1 (numpy array): independent variable of first function; must be in
            ascending order
        y1 (numpy array): dependent variable of first function
        x2 (numpy array): independent variable of second function; must have
            same grid spacing as x1
        y2 (numpy array): dependent variable of second function
        align (boolean): if True, output grid will be identical to input x1, y1
        normalize (boolean): if True, the output will be scaled to match y1

    Returns:
        xcv (numpy array): independent variable of convoluted function, has
            dimension len(x1) + len(x2) - 1 unless align option us used.

        ycv (numpy array): convolution of y1 and y2, same shape as xcv

    """
    dx = x1[1] - x1[0]
    ycv = dx * convolve(y1, y2, 'full')
    xcv = np.linspace(x1[0] + x2[0], x1[-1] + x2[-1], len(ycv))
    if align: # align output with the x1 and y1 input grids
        lb = x1[0] - 0.5 * dx
        ub = x1[-1] + 0.5 * dx
        mask = np.logical_and(xcv > lb, xcv < ub)
        ycv = ycv[mask]
        xcv = xcv[mask]
    if normalize: # normalize y1 to its original scale
        ycv /= np.trapz(y2, x2) 
    return xcv, ycv
def estimate_effective_xi(dampingMat, N=1000)

Estimate the effective correlation length for a given damping matrix.

Determines the correlation length for a given number of randomly generated direction vectors, then calculates the weighted average where the weights are given by the volume of the damping matrix ellipsoid subtended by each direction vector. Note: This is somewhat of an experimental function, not yet rigorously shown to be correct.

Args

dampingMat : numpy array
3x3 damping matrix corresponding to a (potentially) anisotropic correlation length.
N : int
number of random directions to average over. Default is 1000.

Returns: estimated effective correlation length.

Expand source code
def estimate_effective_xi(dampingMat, N=1000):
    """
    Estimate the effective correlation length for a given damping matrix. 
    
    Determines the correlation length for a given number of randomly generated
    direction vectors, then calculates the weighted average where the weights
    are given by the volume of the damping matrix ellipsoid subtended by each
    direction vector.
    Note: This is somewhat of an experimental function, not yet rigorously
    shown to be correct.

    Args:
        dampingMat (numpy array): 3x3 damping matrix corresponding to a
            (potentially) anisotropic correlation length.
        N (int): number of random directions to average over. Default is 1000.
        
    Returns: estimated effective correlation length.
    """
    # put matrix in diagonal form
    ev = np.linalg.eig(dampingMat)[0]
    dampingMat = np.array([[ev[0],0,0],[0,ev[1],0],[0,0,ev[2]]])
    # generate uniform sampling of direction
    ths = np.arccos(np.random.uniform(-1, 1, size=N))
    phis = np.random.uniform(-np.pi, np.pi, size=N)
    x = np.sin(ths)*np.cos(phis)
    y = np.sin(ths)*np.sin(phis)
    z = np.cos(ths)
    randomVecs = np.transpose((x, y, z))
    # calculate correlation length along each direction
    mult1 = np.tensordot(dampingMat, randomVecs, axes=(0,1)).T
    xi = 1.0/np.sqrt(np.apply_along_axis(np.sum, 1, randomVecs*mult1))
    # generate weights according to volume subtended by each direction vector
    weights = xi**5 * np.sin(ths)
    return np.sum(weights * xi)/np.sum(weights)
def findAtomIndices(magstruc, atomList)

Return list of indices corresponding to input list of atomic coordinates.

Args

atomList (numpy array of atomic coordinates)

Returns

List of indices corresponding to the atomList.

Expand source code
def findAtomIndices(magstruc, atomList):
    """Return list of indices corresponding to input list of atomic coordinates.

    Args:
        atomList (numpy array of atomic coordinates)

    Returns:
        List of indices corresponding to the atomList.
    """
    if len(np.array(atomList).shape) == 1:
        atomList = [atomList]
    indices = []
    x, y, z = magstruc.atoms.transpose()
    allIdxs = np.arange(len(x))
    for idx, atom in enumerate(atomList):
        xa, ya, za = atom[0], atom[1], atom[2]
        maskx = (np.abs(x - xa) < 0.01)
        masky = (np.abs(y - ya) < 0.01)
        maskz = (np.abs(z - za) < 0.01)
        match = allIdxs[np.logical_and(maskx, np.logical_and(masky, maskz))]
        if len(match) == 0:
            print(('Warning: atom with index ' + str(idx) + ' in atomList could not'))
            print('be found in the MagStructure, so the index -1 has been')
            print('returned instead.')
            indices.append(-1)
        if len(match) == 1:
            indices.append(match[0])
        if len(match) > 1:
            print(('Warning: ' + str(len(match)) + ' atoms matching index ' + str(idx)))
            print('have been found in the MagStructure, so just the first index has')
            print('been returned.')
            indices.append(match[0])

    return indices
def fourierTransform(q, fq, rmin=0.0, rmax=50.0, rstep=0.1)

Compute the Fourier transform of a function.

This method uses the FFT algorithm and returns correctly spaced x and y arrays on an even and specifiable grid. The input grid must be evenly spaced.

Args

q : numpy array
independent variable for function to be transformed
fq : numpy array
dependent variable for function to be transformed
rmin : float, default = 0.0
min value of conjugate independent variable grid
rmax : float, default = 50.0
maximum value of conjugate independent variable grid
rstep : float, default = 0.1
grid spacing for conjugate independent variable

Returns

r (numpy array): independent variable grid for transformed quantity

fr (numpy array): Fourier transform of fq (complex)

Expand source code
def fourierTransform(q, fq, rmin=0.0, rmax=50.0, rstep=0.1):  # requires even q-grid
    """Compute the Fourier transform of a function.

    This method uses the FFT algorithm and returns correctly spaced x and
    y arrays on an even and specifiable grid. The input grid must be evenly
    spaced.

    Args:
        q (numpy array): independent variable for function to be transformed
        fq (numpy array): dependent variable for function to be transformed
        rmin (float, default = 0.0): min value of conjugate independent variable
            grid
        rmax (float, default = 50.0): maximum value of conjugate independent
            variable grid
        rstep (float, default = 0.1): grid spacing for conjugate independent
            variable

    Returns:
        r (numpy array): independent variable grid for transformed quantity

        fr (numpy array): Fourier transform of fq (complex)
    """
    lostep = int(np.ceil((rmin - 1e-8) / rstep))
    histep = int(np.floor((rmax + 1e-8) / rstep)) + 1
    r = np.arange(lostep, histep) * rstep
    qstep = q[1] - q[0]
    if (q[0] - 0.01 * qstep) > 0:
        nn = int(np.round(q[0] / qstep))
        addme = np.linspace(0.0, q[0] - qstep, nn)
        q = np.concatenate((addme, q))
        fq = np.concatenate((0.0 * addme, fq))
    qmaxrstep = np.pi / rstep
    nin = len(q)
    nbase = max([nin, histep, qmaxrstep / qstep])
    nlog2 = int(np.ceil(np.log2(nbase)))
    nout = 2 ** nlog2
    qmaxdb = 2 * nout * qstep
    yindb = np.concatenate((fq, np.zeros(2 * nout - nin)))
    cyoutdb = np.fft.ifft(yindb) * np.sqrt(2 / np.pi) * qmaxdb
    frdb = cyoutdb
    rstepfine = 2 * np.pi / qmaxdb
    rfine = np.arange(nout) * rstepfine
    frfine = frdb[:nout]
    frr = np.interp(r, rfine, np.real(frfine))
    fri = np.interp(r, rfine, np.imag(frfine))
    if r[0] + 0.0001 * rstep < 0:
        nn = int(np.round(-r[0] / rstep))
        frr[:nn] = 1.0 * frr[2 * nn:nn:-1]
        fri[:nn] = -1.0 * fri[2 * nn:nn:-1]
    fr = frr + 1j * fri
    return r, fr
def gauss(grid, s=0.5)

Generate a gaussian kernel of arbitrary size and density This function generates a 3D guassian kernel based on the input grid and the standard deviation chosen. This is a simple replacement for the form factors that will be implemented in the future.

Args

grid : array
the 3D spatial coordinates for the gaussian kernel
s : float
The standard deviation for the gaussian kernel
Expand source code
def gauss(grid,s=0.5):
    """Generate a gaussian kernel of arbitrary size and density
    This function generates a 3D guassian kernel based on the input grid and the standard
    deviation chosen. This is a simple replacement for the form factors that will be
    implemented in the future.
    Args:
        grid (array): the 3D spatial coordinates for the gaussian kernel
        s (float): The standard deviation for the gaussian kernel
    """
    g = lambda point: 1/(s*np.sqrt((2*np.pi*s)**3))*np.exp(-1/2*(np.linalg.norm(point)/s)**2)
    return np.apply_along_axis(g,3,grid)
def generateAtomsXYZ(struc, rmax=30.0, strucIdxs=[0], square=False)

Generate array of atomic Cartesian coordinates from a given structure.

Args

struc : diffpy.Structure object
provides lattice parameters and unit cell of the desired structure
rmax : float
largest distance from central atom that should be included
strucIdxs : python list
list of integers giving indices of magnetic atoms in the unit cell
square : boolean
if not True, atoms within a given radius from the origin will be returned; if True, then the full grid will be returned rather than just a spherical selection.

Returns

numpy array of triples giving the Cartesian coordinates of all the magnetic atoms. Atom closest to the origin placed first in array. Note: If square = True, this may have problems for structures that have a several distorted unit cell (i.e. highly non-orthorhombic).

Expand source code
def generateAtomsXYZ(struc, rmax=30.0, strucIdxs=[0], square=False):
    """Generate array of atomic Cartesian coordinates from a given structure.

    Args:
        struc (diffpy.Structure object): provides lattice parameters and unit
            cell of the desired structure
        rmax (float): largest distance from central atom that should be
            included
        strucIdxs (python list): list of integers giving indices of magnetic
            atoms in the unit cell
        square (boolean): if not True, atoms within a given radius from the
            origin will be returned; if True, then the full grid will be
            returned rather than just a spherical selection.

    Returns:
        numpy array of triples giving the Cartesian coordinates of all the
            magnetic atoms. Atom closest to the origin placed first in array.

    Note: If square = True, this may have problems for structures that have
        a several distorted unit cell (i.e. highly non-orthorhombic).
    """
    if not square:
        magAtoms = struc[strucIdxs]
        bc = BondCalculator(rmax=rmax + np.linalg.norm(struc.lattice.stdbase.sum(axis=1)))
        bc.setPairMask(0, 'all', True, others=False)
        bc(magAtoms)
        r0 = struc.xyz_cartn[strucIdxs[0]]
        atoms = np.vstack([r0, r0 + bc.directions[bc.sites0 == 0]])

    else:
        ### generate the coordinates of each unit cell
        lat = struc.lattice
        unitcell = lat.stdbase
        cellwithatoms = struc.xyz_cartn[np.array(strucIdxs)]
        dim1 = int(np.round(rmax / np.linalg.norm(unitcell[0])))
        dim2 = int(np.round(rmax / np.linalg.norm(unitcell[1])))
        dim3 = int(np.round(rmax / np.linalg.norm(unitcell[2])))
        ocoords = np.mgrid[-dim1:dim1 + 1, -dim2:dim2 + 1, -dim3:dim3 + 1].transpose().ravel().reshape(
            (2 * dim1 + 1) * (2 * dim2 + 1) * (2 * dim3 + 1), 3)
        latos = np.dot(ocoords, unitcell)

        ### rearrange latos array so that [0, 0, 0] is the first one (for convenience)
        latos[np.where(np.all(latos == [0, 0, 0], axis=1))] = latos[0]
        latos[0] = np.array([0, 0, 0])

        ### create list of all atomic positions
        atoms = np.empty([len(latos) * len(cellwithatoms), 3])
        index = 0
        for lato in latos:
            for atompos in cellwithatoms:
                atoms[index] = lato + atompos
                index += 1

    return atoms
def generateFromUnitCell(unitcell, atombasis, spinbasis, rmax=30.0)

Generate array of atomic Cartesian coordinates from a given structure.

Args

unitcell : numpy array
np.array([avec, bvec, cvec])
atombasis : numpy array
gives positions of magnetic atoms in fractional coordinates; np.array([pos1, pos2, pos3, …])
spinbasis : numpy array
gives orientations of the magnetic moments in the unit cell, in the same order as atombasis
rmax : float
largest distance from central atom that should be included

Returns

atoms = numpy array of triples giving the Cartesian coordinates of all the magnetic atoms. Atom closest to the origin placed first in array. spins = numpy array of triples giving the Cartesian coordinates of all the spins in the structure, in the same order as atoms. Note: This will only work well for structures that can be expressed with a unit cell that is close to orthorhombic or higher symmetry.

Expand source code
def generateFromUnitCell(unitcell, atombasis, spinbasis, rmax=30.0):
    """Generate array of atomic Cartesian coordinates from a given structure.

    Args:
        unitcell (numpy array): np.array([avec, bvec, cvec])
        atombasis (numpy array): gives positions of magnetic atoms in
            fractional coordinates; np.array([pos1, pos2, pos3, ...])
        spinbasis (numpy array): gives orientations of the magnetic moments
            in the unit cell, in the same order as atombasis
        rmax (float): largest distance from central atom that should be
            included

    Returns:
        atoms = numpy array of triples giving the Cartesian coordinates of all
              the magnetic atoms. Atom closest to the origin placed first in
              array.
        spins = numpy array of triples giving the Cartesian coordinates of all
              the spins in the structure, in the same order as atoms.

    Note: This will only work well for structures that can be expressed with a
        unit cell that is close to orthorhombic or higher symmetry.
    """
    if len(np.array(atombasis).shape) == 1:
        atombasis = [atombasis]
    if len(np.array(spinbasis).shape) == 1:
        spinbasis = [spinbasis]
    cellwithatoms = np.dot(atombasis, unitcell)  ### check this
    radius = rmax + 15.0
    dim1 = int(np.round(radius / np.linalg.norm(unitcell[0])))
    dim2 = int(np.round(radius / np.linalg.norm(unitcell[1])))
    dim3 = int(np.round(radius / np.linalg.norm(unitcell[2])))

    ### generate the coordinates of each unit cell
    ocoords = np.mgrid[-dim1:dim1 + 1, -dim2:dim2 + 1, -dim3:dim3 + 1].transpose().ravel().reshape(
        (2 * dim1 + 1) * (2 * dim2 + 1) * (2 * dim3 + 1), 3)
    latos = np.dot(ocoords, unitcell)

    ### select points within a desired radius from origin
    latos = latos[np.where(np.apply_along_axis(np.linalg.norm, 1, latos) <=
                           (rmax + np.linalg.norm(unitcell.sum(axis=1))))]

    ## rearrange latos array so that [0, 0, 0] is the first one (for convenience)
    latos[np.where(np.all(latos == [0, 0, 0], axis=1))] = latos[0]
    latos[0] = np.array([0, 0, 0])

    ### create list of all atomic positions
    atoms = np.empty([len(latos) * len(cellwithatoms), 3])
    spins = np.empty_like(atoms)
    index = 0
    for lato in latos:
        for j, atompos in enumerate(cellwithatoms):
            atoms[index] = lato + atompos
            spins[index] = spinbasis[j]
            index += 1
    return atoms, spins
def generateSpinsXYZ(struc, atoms=array([], shape=(1, 0), dtype=float64), kvecs=array([[0, 0, 0]]), basisvecs=array([[0, 0, 1]]), origin=array([0, 0, 0]), avgmom=array([0, 0, 0]))

Generate array of 3-vectors representing the spins in a structure.

Args

struc : diffpy.Structure object
provides lattice parameters and unit cell of the desired structure
atoms : numpy array
list of atomic coordinates of all the magnetic atoms of a given magnetic species in the structure
avgmom : numpy array
three-vector giving the average moment for the magnetic species.
kvecs : numpy array
list of three-vectors giving the propagation vector(s) of the magnetic structure
basisvecs : numpy array
list of three-vectors describing the spin located at the spin origin.
origin : numpy array
Cartesian coordinates specifying the origin to be used when calculating the phases of the spins.

Returns

numpy array of triples giving the spin vectors of all the magnetic atoms, in the same order as the atoms array provided as input.

Expand source code
def generateSpinsXYZ(struc, atoms=np.array([[]]), kvecs=np.array([[0, 0, 0]]),
                     basisvecs=np.array([[0, 0, 1]]), origin=np.array([0, 0, 0]),
                     avgmom=np.array([0, 0, 0])):
    """Generate array of 3-vectors representing the spins in a structure.

    Args:
        struc (diffpy.Structure object): provides lattice parameters and unit
            cell of the desired structure
        atoms (numpy array): list of atomic coordinates of all the magnetic
            atoms of a given magnetic species in the structure
        avgmom (numpy array): three-vector giving the average moment for the
            magnetic species.
        kvecs (numpy array): list of three-vectors giving the propagation
            vector(s) of the magnetic structure
        basisvecs (numpy array): list of three-vectors describing the spin
            located at the spin origin.
        origin (numpy array): Cartesian coordinates specifying the origin
            to be used when calculating the phases of the spins.

    Returns:
        numpy array of triples giving the spin vectors of all the magnetic
            atoms, in the same order as the atoms array provided as input.

    """

    lat = struc.lattice
    rlat = lat.reciprocal()
    (astar, bstar, cstar) = (rlat.cartesian((1, 0, 0)), rlat.cartesian((0, 1, 0)),
                             rlat.cartesian((0, 0, 1)))
    i = 1j

    spins = 0 * atoms
    cspins = 0 * atoms + 0j * atoms
    if len(np.array(kvecs).shape) == 1:
        kvecs = [kvecs]
    if len(np.array(basisvecs).shape) == 1:
        basisvecs = [basisvecs]
    for idx, kvec in enumerate(kvecs):
        kcart = kvec[0] * astar + kvec[1] * bstar + kvec[2] * cstar
        phasefac = np.exp(-2.0 * np.pi * i * np.dot(atoms - origin, kcart))
        cspins += basisvecs[idx] * phasefac[:, np.newaxis]
    cspins += avgmom
    spins = np.real(cspins)

    if np.abs(np.imag(cspins)).max() > 0.0001:
        print((np.abs(np.imag(cspins)).max()))
        print('Warning: basis vectors resulted in complex spins.')
        print('Imaginary parts have been discarded.')

    return spins
def generate_damping_matrix(e1, e2, e3, xi1, xi2, xi3)

Generate the damping matrix from the eigenvectors and eigenvalues.

Args

e1, e2, e3 (numpy arrays): right orthonormal eigenvectors xi1, xi2, xi3 (floats): correlation lengths along the directions of the eigenvectors

Returns

dampingMat
3x3 symmetric damping matrix.
Expand source code
def generate_damping_matrix(e1, e2, e3, xi1, xi2, xi3):
    """Generate the damping matrix from the eigenvectors and eigenvalues.
    
    Args:
        e1, e2, e3 (numpy arrays): right orthonormal eigenvectors
        xi1, xi2, xi3 (floats): correlation lengths along the directions
            of the eigenvectors
    
    Returns:
        dampingMat: 3x3 symmetric damping matrix.
    """
    m1 = np.array([e1,e2,e3]).T
    m2 = np.array([[1.0/xi1**2, 0, 0], [0, 1.0/xi2**2, 0], [0, 0, 1.0/xi3**2]])
    m3 = m1.T
    dampingMat = np.matmul(np.matmul(m1, m2), m3)
    return dampingMat
def getDiffData(fileName, fitIdx=0, writedata=False)

Extract the fit residual from a structural PDF fit. Works for .fgr and .ddp files.

Args

fileName : str
path to the .fgr or .ddp file containing the fit
fitIdx : int
index of fit in .ddp file from which the residual is to be extracted.
writedata : boolean
whether or not the output should be saved to file

Returns

r (numpy array): same r-grid as contained in the fit file

diff (numpy array): the structural PDF fit residual (i.e. the mPDF)

Expand source code
def getDiffData(fileName, fitIdx=0, writedata=False):
    """Extract the fit residual from a structural PDF fit. Works for .fgr and
       .ddp files.

    Args:
        fileName (str): path to the .fgr or .ddp file containing the fit
        fitIdx (int): index of fit in .ddp file from which the residual
             is to be extracted.
        writedata (boolean): whether or not the output should be saved to file

    Returns:
        r (numpy array): same r-grid as contained in the fit file

        diff (numpy array): the structural PDF fit residual (i.e. the mPDF)
    """
    if fileName[-4:] == '.fgr':
        lines = open(fileName).readlines()[:50]
        for idx, line in enumerate(lines):
            if 'start data' in line:
                startLine = 1 * idx
                break
        allcols = np.loadtxt(fileName, unpack=True, comments='#', skiprows=startLine)
        r, diff = allcols[0], allcols[4]
        if writedata:
            np.savetxt(fileName[:-4] + '.diff', np.transpose((r, diff)))
        return r, diff
    elif fileName[-4:] == '.ddp':
        from diffpy.pdfgui import tui
        prj = tui.LoadProject(fileName)
        fit = prj.getFits()[fitIdx]
        dataSet = fit.getDataSet(0)
        r = np.array(dataSet.rcalc)
        diff = np.array(dataSet.Gdiff)
        if writedata:
            np.savetxt(fileName[:-4] + '_' + str(fitIdx) + '.diff',
                       np.transpose((r, diff)))
        return r, diff
    else:
        print('This file format is not currently supported.')
        return np.array([0]), np.array([0])
def getFFparams(name, j2=False)

Get list of parameters for approximation of magnetic form factor

Args

name : str
Name of magnetic ion in form 'Mn2' for Mn2+, etc.
j2 : boolean
True of the j2 approximation should be calculated; otherwise, the j0 approximation is calculated.

Returns

Python list of the 7 coefficients in the analytical approximation given at e.g. https://www.ill.eu/sites/ccsl/ffacts/ffachtml.html

Expand source code
def getFFparams(name, j2=False):
    """Get list of parameters for approximation of magnetic form factor

    Args:
        name (str): Name of magnetic ion in form 'Mn2' for Mn2+, etc.
        j2 (boolean): True of the j2 approximation should be calculated;
            otherwise, the j0 approximation is calculated.

    Returns:
        Python list of the 7 coefficients in the analytical approximation
            given at e.g. https://www.ill.eu/sites/ccsl/ffacts/ffachtml.html
    """
    if not j2:
        j0dict = {'Am2': [0.4743, 21.7761, 1.58, 5.6902, -1.0779, 4.1451, 0.0218],
                  'Am3': [0.4239, 19.5739, 1.4573, 5.8722, -0.9052, 3.9682, 0.0238],
                  'Am4': [0.3737, 17.8625, 1.3521, 6.0426, -0.7514, 3.7199, 0.0258],
                  'Am5': [0.2956, 17.3725, 1.4525, 6.0734, -0.7755, 3.6619, 0.0277],
                  'Am6': [0.2302, 16.9533, 1.4864, 6.1159, -0.7457, 3.5426, 0.0294],
                  'Am7': [0.3601, 12.7299, 1.964, 5.1203, -1.356, 3.7142, 0.0316],
                  'Ce2': [0.2953, 17.6846, 0.2923, 6.7329, 0.4313, 5.3827, -0.0194],
                  'Co0': [0.4139, 16.1616, 0.6013, 4.7805, -0.1518, 0.021, 0.1345],
                  'Co1': [0.099, 33.1252, 0.3645, 15.1768, 0.547, 5.0081, -0.0109],
                  'Co2': [0.4332, 14.3553, 0.5857, 4.6077, -0.0382, 0.1338, 0.0179],
                  'Co3': [0.3902, 12.5078, 0.6324, 4.4574, -0.15, 0.0343, 0.1272],
                  'Co4': [0.3515, 10.7785, 0.6778, 4.2343, -0.0389, 0.2409, 0.0098],
                  'Cr0': [0.1135, 45.199, 0.3481, 19.4931, 0.5477, 7.3542, -0.0092],
                  'Cr1': [-0.0977, 0.047, 0.4544, 26.0054, 0.5579, 7.4892, 0.0831],
                  'Cr2': [1.2024, -0.0055, 0.4158, 20.5475, 0.6032, 6.956, -1.2218],
                  'Cr3': [-0.3094, 0.0274, 0.368, 17.0355, 0.6559, 6.5236, 0.2856],
                  'Cr4': [-0.232, 0.0433, 0.3101, 14.9518, 0.7182, 6.1726, 0.2042],
                  'Cu0': [0.0909, 34.9838, 0.4088, 11.4432, 0.5128, 3.8248, -0.0124],
                  'Cu1': [0.0749, 34.9656, 0.4147, 11.7642, 0.5238, 3.8497, -0.0127],
                  'Cu2': [0.0232, 34.9686, 0.4023, 11.564, 0.5882, 3.8428, -0.0137],
                  'Cu3': [0.0031, 34.9074, 0.3582, 10.9138, 0.6531, 3.8279, -0.0147],
                  'Cu4': [-0.0132, 30.6817, 0.2801, 11.1626, 0.749, 3.8172, -0.0165],
                  'Dy2': [0.1308, 18.3155, 0.3118, 7.6645, 0.5795, 3.1469, -0.0226],
                  'Dy3': [0.1157, 15.0732, 0.327, 6.7991, 0.5821, 3.0202, -0.0249],
                  'Er2': [0.1122, 18.1223, 0.3462, 6.9106, 0.5649, 2.7614, -0.0235],
                  'Er3': [0.0586, 17.9802, 0.354, 7.0964, 0.6126, 2.7482, -0.0251],
                  'Eu2': [0.0755, 25.296, 0.3001, 11.5993, 0.6438, 4.0252, -0.0196],
                  'Eu3': [0.0204, 25.3078, 0.301, 11.4744, 0.7005, 3.942, -0.022],
                  'Fe0': [0.0706, 35.0085, 0.3589, 15.3583, 0.5819, 5.5606, -0.0114],
                  'Fe1': [0.1251, 34.9633, 0.3629, 15.5144, 0.5223, 5.5914, -0.0105],
                  'Fe2': [0.0263, 34.9597, 0.3668, 15.9435, 0.6188, 5.5935, -0.0119],
                  'Fe3': [0.3972, 13.2442, 0.6295, 4.9034, -0.0314, 0.3496, 0.0044],
                  'Fe4': [0.3782, 11.38, 0.6556, 4.592, -0.0346, 0.4833, 0.0005],
                  'Gd2': [0.0636, 25.3823, 0.3033, 11.2125, 0.6528, 3.7877, -0.0199],
                  'Gd3': [0.0186, 25.3867, 0.2895, 11.1421, 0.7135, 3.752, -0.0217],
                  'Ho2': [0.0995, 18.1761, 0.3305, 7.8556, 0.5921, 2.9799, -0.023],
                  'Ho3': [0.0566, 18.3176, 0.3365, 7.688, 0.6317, 2.9427, -0.0248],
                  'Mn0': [0.2438, 24.9629, 0.1472, 15.6728, 0.6189, 6.5403, -0.0105],
                  'Mn1': [-0.0138, 0.4213, 0.4231, 24.668, 0.5905, 6.6545, -0.001],
                  'Mn2': [0.422, 17.684, 0.5948, 6.005, 0.0043, -0.609, -0.0219],
                  'Mn3': [0.4198, 14.2829, 0.6054, 5.4689, 0.9241, -0.0088, -0.9498],
                  'Mn4': [0.376, 12.5661, 0.6602, 5.1329, -0.0372, 0.563, 0.0011],
                  'Mo0': [0.1806, 49.0568, 1.2306, 14.7859, -0.4268, 6.9866, 0.0171],
                  'Mo1': [0.35, 48.0354, 1.0305, 15.0604, -0.3929, 7.479, 0.0139],
                  'Nb0': [0.3946, 49.2297, 1.3197, 14.8216, -0.7269, 9.6156, 0.0129],
                  'Nb1': [0.4572, 49.9182, 1.0274, 15.7256, -0.4962, 9.1573, 0.0118],
                  'Nd2': [0.1645, 25.0453, 0.2522, 11.9782, 0.6012, 4.9461, -0.018],
                  'Nd3': [0.054, 25.0293, 0.3101, 12.102, 0.6575, 4.7223, -0.0216],
                  'Ni0': [-0.0172, 35.7392, 0.3174, 14.2689, 0.7136, 4.5661, -0.0143],
                  'Ni1': [0.0705, 35.8561, 0.3984, 13.8042, 0.5427, 4.3965, -0.0118],
                  'Ni2': [0.0163, 35.8826, 0.3916, 13.2233, 0.6052, 4.3388, -0.0133],
                  'Ni3': [-0.0134, 35.8677, 0.2678, 12.3326, 0.7614, 4.2369, -0.0162],
                  'Ni4': [-0.009, 35.8614, 0.2776, 11.7904, 0.7474, 4.2011, -0.0163],
                  'Np3': [0.5157, 20.8654, 2.2784, 5.893, -1.8163, 4.8457, 0.0211],
                  'Np4': [0.4206, 19.8046, 2.8004, 5.9783, -2.2436, 4.9848, 0.0228],
                  'Np5': [0.3692, 18.19, 3.151, 5.85, -2.5446, 4.9164, 0.0248],
                  'Np6': [0.2929, 17.5611, 3.4866, 5.7847, -2.8066, 4.8707, 0.0267],
                  'Pd0': [0.2003, 29.3633, 1.1446, 9.5993, -0.3689, 4.0423, 0.0251],
                  'Pd1': [0.5033, 24.5037, 1.9982, 6.9082, -1.524, 5.5133, 0.0213],
                  'Pr3': [0.0504, 24.9989, 0.2572, 12.0377, 0.7142, 5.0039, -0.0219],
                  'Pu3': [0.384, 16.6793, 3.1049, 5.421, -2.5148, 4.5512, 0.0263],
                  'Pu4': [0.4934, 16.8355, 1.6394, 5.6384, -1.1581, 4.1399, 0.0248],
                  'Pu5': [0.3888, 16.5592, 2.0362, 5.6567, -1.4515, 4.2552, 0.0267],
                  'Pu6': [0.3172, 16.0507, 3.4654, 5.3507, -2.8102, 4.5133, 0.0281],
                  'Rh0': [0.0976, 49.8825, 1.1601, 11.8307, -0.2789, 4.1266, 0.0234],
                  'Rh1': [0.3342, 29.7564, 1.2209, 9.4384, -0.5755, 5.332, 0.021],
                  'Ru0': [0.1069, 49.4238, 1.1912, 12.7417, -0.3176, 4.9125, 0.0213],
                  'Ru1': [0.441, 33.3086, 1.4775, 9.5531, -0.9361, 6.722, 0.0176],
                  'Sc0': [0.2512, 90.0296, 0.329, 39.4021, 0.4235, 14.3222, -0.0043],
                  'Sc1': [0.4889, 51.1603, 0.5203, 14.0764, -0.0286, 0.1792, 0.0185],
                  'Sc2': [0.5048, 31.4035, 0.5186, 10.9897, -0.0241, 1.1831, 0.0],
                  'Sm2': [0.0909, 25.2032, 0.3037, 11.8562, 0.625, 4.2366, -0.02],
                  'Sm3': [0.0288, 25.2068, 0.2973, 11.8311, 0.6954, 4.2117, -0.0213],
                  'Tb2': [0.0547, 25.5086, 0.3171, 10.5911, 0.649, 3.5171, -0.0212],
                  'Tb3': [0.0177, 25.5095, 0.2921, 10.5769, 0.7133, 3.5122, -0.0231],
                  'Tc0': [0.1298, 49.6611, 1.1656, 14.1307, -0.3134, 5.5129, 0.0195],
                  'Tc1': [0.2674, 48.9566, 0.9569, 15.1413, -0.2387, 5.4578, 0.016],
                  'Ti0': [0.4657, 33.5898, 0.549, 9.8791, -0.0291, 0.3232, 0.0123],
                  'Ti1': [0.5093, 36.7033, 0.5032, 10.3713, -0.0263, 0.3106, 0.0116],
                  'Ti2': [0.5091, 24.9763, 0.5162, 8.7569, -0.0281, 0.916, 0.0015],
                  'Ti3': [0.3571, 22.8413, 0.6688, 8.9306, -0.0354, 0.4833, 0.0099],
                  'Tm2': [0.0983, 18.3236, 0.338, 6.9178, 0.5875, 2.6622, -0.0241],
                  'Tm3': [0.0581, 15.0922, 0.2787, 7.8015, 0.6854, 2.7931, -0.0224],
                  'U3': [0.5058, 23.2882, 1.3464, 7.0028, -0.8724, 4.8683, 0.0192],
                  'U4': [0.3291, 23.5475, 1.0836, 8.454, -0.434, 4.1196, 0.0214],
                  'U5': [0.365, 19.8038, 3.2199, 6.2818, -2.6077, 5.301, 0.0233],
                  'V0': [0.4086, 28.8109, 0.6077, 8.5437, -0.0295, 0.2768, 0.0123],
                  'V1': [0.4444, 32.6479, 0.5683, 9.0971, -0.2285, 0.0218, 0.215],
                  'V2': [0.4085, 23.8526, 0.6091, 8.2456, -0.1676, 0.0415, 0.1496],
                  'V3': [0.3598, 19.3364, 0.6632, 7.6172, -0.3064, 0.0296, 0.2835],
                  'V4': [0.3106, 16.816, 0.7198, 7.0487, -0.0521, 0.302, 0.0221],
                  'Y0': [0.5915, 67.6081, 1.5123, 17.9004, -1.113, 14.1359, 0.008],
                  'Yb2': [0.0855, 18.5123, 0.2943, 7.3734, 0.6412, 2.6777, -0.0213],
                  'Yb3': [0.0416, 16.0949, 0.2849, 7.8341, 0.6961, 2.6725, -0.0229],
                  'Zr0': [0.4106, 59.9961, 1.0543, 18.6476, -0.4751, 10.54, 0.0106],
                  'Zr1': [0.4532, 59.5948, 0.7834, 21.4357, -0.2451, 9.036, 0.0098]}
        try:
            return j0dict[name]
        except KeyError:
            print('No magnetic form factor found for that element/ion.')
            return ['none']
    else:
        j2dict = {'Am2': [3.5237, 15.9545, 2.2855, 5.1946, -0.0142, 0.5853, 0.0033],
                  'Am3': [2.8622, 14.7328, 2.4099, 5.1439, -0.1326, 0.0309, 0.1233],
                  'Am4': [2.4141, 12.9478, 2.3687, 4.9447, -0.2490, 0.0215, 0.2371],
                  'Am5': [2.0109, 12.0534, 2.4155, 4.8358, -0.2264, 0.0275, 0.2128],
                  'Am6': [1.6778, 11.3372, 2.4531, 4.7247, -0.2043, 0.0337, 0.1892],
                  'Am7': [1.8845, 9.1606, 2.0746, 4.0422, -0.1318, 1.7227, 0.0020],
                  'Ce2': [0.9809, 18.0630, 1.8413, 7.7688, 0.9905, 2.8452, 0.0120],
                  'Co0': [1.9678, 14.1699, 1.4911, 4.9475, 0.3844, 1.7973, 0.0027],
                  'Co1': [2.4097, 16.1608, 1.5780, 5.4604, 0.4095, 1.9141, 0.0031],
                  'Co2': [1.9049, 11.6444, 1.3159, 4.3574, 0.3146, 1.6453, 0.0017],
                  'Co3': [1.7058, 8.8595, 1.1409, 3.3086, 0.1474, 1.0899, -0.0025],
                  'Co4': [1.3110, 8.0252, 1.1551, 3.1792, 0.1608, 1.1301, -0.0011],
                  'Cr0': [3.4085, 20.1267, 2.1006, 6.8020, 0.4266, 2.3941, 0.0019],
                  'Cr1': [3.7768, 20.3456, 2.1028, 6.8926, 0.4010, 2.4114, 0.0017],
                  'Cr2': [2.6422, 16.0598, 1.9198, 6.2531, 0.4446, 2.3715, 0.0020],
                  'Cr3': [1.6262, 15.0656, 2.0618, 6.2842, 0.5281, 2.3680, 0.0023],
                  'Cr4': [1.0293, 13.9498, 1.9933, 6.0593, 0.5974, 2.3457, 0.0027],
                  'Cu0': [1.9182, 14.4904, 1.3329, 4.7301, 0.3842, 1.6394, 0.0035],
                  'Cu1': [1.8814, 13.4333, 1.2809, 4.5446, 0.3646, 1.6022, 0.0033],
                  'Cu2': [1.5189, 10.4779, 1.1512, 3.8132, 0.2918, 1.3979, 0.0017],
                  'Cu3': [1.2797, 8.4502, 1.0315, 3.2796, 0.2401, 1.2498, 0.0015],
                  'Cu4': [0.9568, 7.4481, 0.9099, 3.3964, 0.3729, 1.4936, 0.0049],
                  'Dy2': [0.5917, 18.5114, 1.1828, 6.7465, 0.8801, 2.2141, 0.0229],
                  'Dy3': [0.2523, 18.5172, 1.0914, 6.7362, 0.9345, 2.2082, 0.0250],
                  'Er2': [0.4693, 18.5278, 1.0545, 6.6493, 0.8679, 2.1201, 0.0261],
                  'Er3': [0.1710, 18.5337, 0.9879, 6.6246, 0.9044, 2.1004, 0.0278],
                  'Eu2': [0.8970, 18.4429, 1.3769, 7.0054, 0.9060, 2.4213, 0.0190],
                  'Eu3': [0.3985, 18.4514, 1.3307, 6.9556, 0.9603, 2.3780, 0.0197],
                  'Fe0': [1.9405, 18.4733, 1.9566, 6.3234, 0.5166, 2.1607, 0.0036],
                  'Fe1': [2.6290, 18.6598, 1.8704, 6.3313, 0.4690, 2.1628, 0.0031],
                  'Fe2': [1.6490, 16.5593, 1.9064, 6.1325, 0.5206, 2.1370, 0.0035],
                  'Fe3': [1.3602, 11.9976, 1.5188, 5.0025, 0.4705, 1.9914, 0.0038],
                  'Fe4': [1.5582, 8.2750, 1.1863, 3.2794, 0.1366, 1.1068, -0.0022],
                  'Gd2': [0.7756, 18.4695, 1.3124, 6.8990, 0.8956, 2.3383, 0.0199],
                  'Gd3': [0.3347, 18.4758, 1.2465, 6.8767, 0.9537, 2.3184, 0.0217],
                  'Ho2': [0.5094, 18.5155, 1.1234, 6.7060, 0.8727, 2.1589, 0.0242],
                  'Ho3': [0.2188, 18.5157, 1.0240, 6.7070, 0.9251, 2.1614, 0.0268],
                  'Mn0': [2.6681, 16.0601, 1.7561, 5.6396, 0.3675, 2.0488, 0.0017],
                  'Mn1': [3.2953, 18.6950, 1.8792, 6.2403, 0.3927, 2.2006, 0.0022],
                  'Mn2': [2.0515, 15.5561, 1.8841, 6.0625, 0.4787, 2.2323, 0.0027],
                  'Mn3': [1.2427, 14.9966, 1.9567, 6.1181, 0.5732, 2.2577, 0.0031],
                  'Mn4': [0.7879, 13.8857, 1.8717, 5.7433, 0.5981, 2.1818, 0.0034],
                  'Mo0': [5.1180, 23.4217, 4.1809, 9.2080, -0.0505, 1.7434, 0.0053],
                  'Mo1': [7.2367, 28.1282, 4.0705, 9.9228, -0.0317, 1.4552, 0.0049],
                  'Nb0': [7.4796, 33.1789, 5.0884, 11.5708, -0.0281, 1.5635, 0.0047],
                  'Nb1': [8.7735, 33.2848, 4.6556, 11.6046, -0.0268, 1.5389, 0.0044],
                  'Nd2': [1.4530, 18.3398, 1.6196, 7.2854, 0.8752, 2.6224, 0.0126],
                  'Nd3': [0.6751, 18.3421, 1.6272, 7.2600, 0.9644, 2.6016, 0.0150],
                  'Ni0': [1.0302, 12.2521, 1.4669, 4.7453, 0.4521, 1.7437, 0.0036],
                  'Ni1': [2.1040, 14.8655, 1.4302, 5.0714, 0.4031, 1.7784, 0.0034],
                  'Ni2': [1.7080, 11.0160, 1.2147, 4.1031, 0.3150, 1.5334, 0.0018],
                  'Ni3': [1.1612, 7.7000, 1.0027, 3.2628, 0.2719, 1.3780, 0.0025],
                  'Ni4': [1.1612, 7.7000, 1.0027, 3.2628, 0.2719, 1.3780, 0.0025],
                  'Np3': [3.7170, 15.1333, 2.3216, 5.5025, -0.0275, 0.7996, 0.0052],
                  'Np4': [2.9203, 14.6463, 2.5979, 5.5592, -0.0301, 0.3669, 0.0141],
                  'Np5': [2.3308, 13.6540, 2.7219, 5.4935, -0.1357, 0.0493, 0.1224],
                  'Np6': [1.8245, 13.1803, 2.8508, 5.4068, -0.1579, 0.0444, 0.1438],
                  'Pd0': [3.3105, 14.7265, 2.6332, 5.8618, -0.0437, 1.1303, 0.0053],
                  'Pd1': [4.2749, 17.9002, 2.7021, 6.3541, -0.0258, 0.6999, 0.0071],
                  'Pr3': [0.8734, 18.9876, 1.5594, 6.0872, 0.8142, 2.4150, 0.0111],
                  'Pu3': [2.0885, 12.8712, 2.5961, 5.1896, -0.1465, 0.0393, 0.1343],
                  'Pu4': [2.7244, 12.9262, 2.3387, 5.1633, -0.1300, 0.0457, 0.1177],
                  'Pu5': [2.1409, 12.8319, 2.5664, 5.1522, -0.1338, 0.0457, 0.1210],
                  'Pu6': [1.7262, 12.3240, 2.6652, 5.0662, -0.1695, 0.0406, 0.1550],
                  'Rh0': [3.3651, 17.3444, 3.2121, 6.8041, -0.0350, 0.5031, 0.0146],
                  'Rh1': [4.0260, 18.9497, 3.1663, 6.9998, -0.0296, 0.4862, 0.0127],
                  'Ru0': [3.7445, 18.6128, 3.4749, 7.4201, -0.0363, 1.0068, 0.0073],
                  'Ru1': [5.2826, 23.6832, 3.5813, 8.1521, -0.0257, 0.4255, 0.0131],
                  'Sc0': [10.8172, 54.3270, 4.7353, 14.8471, 0.6071, 4.2180, 0.0011],
                  'Sc1': [8.5021, 34.2851, 3.2116, 10.9940, 0.4244, 3.6055, 0.0009],
                  'Sc2': [4.3683, 28.6544, 3.7231, 10.8233, 0.6074, 3.6678, 0.0014],
                  'Sm2': [1.0360, 18.4249, 1.4769, 7.0321, 0.8810, 2.4367, 0.0152],
                  'Sm3': [0.4707, 18.4301, 1.4261, 7.0336, 0.9574, 2.4387, 0.0182],
                  'Tb2': [0.6688, 18.4909, 1.2487, 6.8219, 0.8888, 2.2751, 0.0215],
                  'Tb3': [0.2892, 18.4973, 1.1678, 6.7972, 0.9437, 2.2573, 0.0232],
                  'Tc0': [4.2441, 21.3974, 3.9439, 8.3753, -0.0371, 1.1870, 0.0066],
                  'Tc1': [6.4056, 24.8243, 3.5400, 8.6112, -0.0366, 1.4846, 0.0044],
                  'Ti0': [4.3583, 36.0556, 3.8230, 11.1328, 0.6855, 3.4692, 0.0020],
                  'Ti1': [6.1567, 27.2754, 2.6833, 8.9827, 0.4070, 3.0524, 0.0011],
                  'Ti2': [4.3107, 18.3484, 2.0960, 6.7970, 0.2984, 2.5476, 0.0007],
                  'Ti3': [3.3717, 14.4441, 1.8258, 5.7126, 0.2470, 2.2654, 0.0005],
                  'Tm2': [0.4198, 18.5417, 0.9959, 6.6002, 0.8593, 2.0818, 0.0284],
                  'Tm3': [0.1760, 18.5417, 0.9105, 6.5787, 0.8970, 2.0622, 0.0294],
                  'U3': [4.1582, 16.5336, 2.4675, 5.9516, -0.0252, 0.7646, 0.0057],
                  'U4': [3.7449, 13.8944, 2.6453, 4.8634, -0.5218, 3.1919, 0.0009],
                  'U5': [3.0724, 12.5460, 2.3076, 5.2314, -0.0644, 1.4738, 0.0035],
                  'V0': [3.8099, 21.3471, 2.3295, 7.4089, 0.4333, 2.6324, 0.0015],
                  'V1': [4.7474, 23.3226, 2.3609, 7.8082, 0.4105, 2.7063, 0.0014],
                  'V2': [3.4386, 16.5303, 1.9638, 6.1415, 0.2997, 2.2669, 0.0009],
                  'V3': [2.3005, 14.6821, 2.0364, 6.1304, 0.4099, 2.3815, 0.0014],
                  'V4': [1.8377, 12.2668, 1.8247, 5.4578, 0.3979, 2.2483, 0.0012],
                  'Y0': [14.4084, 44.6577, 5.1045, 14.9043, -0.0535, 3.3189, 0.0028],
                  'Yb2': [0.3852, 18.5497, 0.9415, 6.5507, 0.8492, 2.0425, 0.0301],
                  'Yb3': [0.1570, 18.5553, 0.8484, 6.5403, 0.8880, 2.0367, 0.0318],
                  'Zr0': [10.1378, 35.3372, 4.7734, 12.5453, -0.0489, 2.6721, 0.0036],
                  'Zr1': [11.8722, 34.9200, 4.0502, 12.1266, -0.0632, 2.8278, 0.0034]}
        try:
            return j2dict[name]
        except KeyError:
            print('No magnetic form factor found for that element/ion.')
            return ['none']
def getStdUnc(fitResult, data, dataErr=None, numConstraints=0)

Return the standard uncertainty of refined parameters. This method is based on the scipy.optimize.least_squares routine.

Args

fitResult
Output from scipy.optimize.least_squares routine
data : numpy array
The data against which the fit is performed
dataErr : numpy array
Experimental uncertainties on the data points (set to unity if not provided)
numConstraints : int
Number of constraints used in the model

Returns

pUnc (numpy array): standard uncertainties of the refined parameters. chisq (float): Value of chi^2 for the fit.

Expand source code
def getStdUnc(fitResult, data, dataErr=None, numConstraints=0):
    """Return the standard uncertainty of refined parameters.
    This method is based on the scipy.optimize.least_squares routine.

    Args:
        fitResult: Output from scipy.optimize.least_squares routine
        data (numpy array): The data against which the fit is performed
        dataErr (numpy array): Experimental uncertainties on the data points
            (set to unity if not provided)
        numConstraints (int): Number of constraints used in the model
    Returns:
        pUnc (numpy array): standard uncertainties of the refined parameters.
        chisq (float): Value of chi^2 for the fit.
    """
    if dataErr is None:
        dataErr = np.ones_like(data)
    weights = 1.0 / dataErr ** 2
    Rw = np.sqrt((fitResult.fun ** 2).sum() / (data ** 2 * weights).sum())
    numParams = len(fitResult.x)
    Rexp = np.sqrt((data.shape[0] - numParams + numConstraints) / (data ** 2 * weights).sum())
    j = fitResult.jac
    jac = np.dot(j.transpose(), j)
    cov = np.linalg.inv(jac) * Rw ** 2 / Rexp ** 2
    pUnc = np.sqrt(cov.diagonal())
    chisq = Rw ** 2 / Rexp ** 2
    return pUnc, chisq
def getStrucFromPDFgui(fileName, fitIdx=0, phaseIdx=0)

Extract the refined atomic structure from a PDFgui project file.

Args

fileName : str
path to the .ddp file containing the fit
fitIdx : int
index of fit in .ddp file from which the refined structure is to be extracted. Default is 0.
phaseIdx : int
index of phase within the specified fit for which the refined structure is to be extracted. Default is 0.

Returns

struc (diffpy.Structure object): Refined atomic structure.

Expand source code
def getStrucFromPDFgui(fileName, fitIdx=0, phaseIdx=0):
    """Extract the refined atomic structure from a PDFgui project file.

    Args:
        fileName (str): path to the .ddp file containing the fit
        fitIdx (int): index of fit in .ddp file from which the refined
             structure is to be extracted. Default is 0.
        phaseIdx (int): index of phase within the specified fit for
             which the refined structure is to be extracted. Default
             is 0.

    Returns:
        struc (diffpy.Structure object): Refined atomic structure.
    """
    if fileName[-4:] == '.ddp':
        from diffpy.pdfgui import tui
        prj = tui.LoadProject(fileName)
        try:
            fit = prj.getFits()[fitIdx]
        except IndexError:
            print('Fit index does not correspond to any fit in your')
            print('PDFgui project.')
            struc = []
            return struc
        try:
            struc = prj.getPhases([fit])[phaseIdx]
            struc = struc.refined
        except IndexError:
            print('Phase index does not correspond to any phase in your')
            print('specified fit in the PDFgui project.')
            struc = []
        return struc
    else:
        print('Please provide a PDFgui project file (.ddp extension)')
        return []
def jCalc(q, params=[0.2394, 26.038, 0.4727, 12.1375, 0.3065, 3.0939, -0.01906], j2=False)

Calculate the magnetic form factor j0.

This method for calculating the magnetic form factor is based on the approximate empirical forms based on the tables by Brown, consisting of the sum of 3 Gaussians and a constant.

Args

q : numpy array
1-d grid of momentum transfer on which the form factor is to be computed
params : python list
provides the 7 numerical coefficients. The default is an average form factor of 3d j0 approximations.
j2 : boolean
if True, calculate the j2 approximation for orbital angular momentum contributions

Returns

numpy array with same shape as q giving the magnetic form factor j0 or j2.

Expand source code
def jCalc(q, params=[0.2394, 26.038, 0.4727, 12.1375, 0.3065, 3.0939, -0.01906],
          j2=False):
    """Calculate the magnetic form factor j0.

    This method for calculating the magnetic form factor is based on the
    approximate empirical forms based on the tables by Brown, consisting of
    the sum of 3 Gaussians and a constant.

    Args:
        q (numpy array): 1-d grid of momentum transfer on which the form
            factor is to be computed
        params (python list): provides the 7 numerical coefficients. The
            default is an average form factor of 3d j0 approximations.
        j2 (boolean): if True, calculate the j2 approximation for orbital
            angular momentum contributions

    Returns:
        numpy array with same shape as q giving the magnetic form factor j0 or j2.
    """
    [A, a, B, b, C, c, D] = params
    if j2:
        return (A * np.exp(-a * (q / 4 / np.pi) ** 2) + B * np.exp(-b * (q / 4 / np.pi) ** 2) + C * np.exp(
            -c * (q / 4 / np.pi) ** 2) + D) * (q / 4.0 / np.pi) ** 2
    else:
        return A * np.exp(-a * (q / 4 / np.pi) ** 2) + B * np.exp(-b * (q / 4 / np.pi) ** 2) + C * np.exp(
            -c * (q / 4 / np.pi) ** 2) + D
def optimizedSubtraction(rhigh, dhigh, rlow, dlow)

This routine stretches and broadens a low-temperature atomic PDF fit residual to match a high-temperature fit residual as closely as possible. The idea is to minimize thermal effects when doing the temperature subtraction so that the mDPF can be obtained as cleanly as possible.

rhigh = r array for high-temperature atomic PDF fit residual dhigh = high-temperature atomic PDF fit residual rlow = r array for low-temperature atomic PDF fit residual dlow = low-temperature atomic PDF fit residual Note: the high- and low-temperature fits should be over the same r range

Expand source code
def optimizedSubtraction(rhigh, dhigh, rlow, dlow):
    '''
    This routine stretches and broadens a low-temperature atomic PDF fit residual
    to match a high-temperature fit residual as closely as possible. The idea is
    to minimize thermal effects when doing the temperature subtraction so that
    the mDPF can be obtained as cleanly as possible.

    rhigh = r array for high-temperature atomic PDF fit residual
    dhigh = high-temperature atomic PDF fit residual
    rlow = r array for low-temperature atomic PDF fit residual
    dlow = low-temperature atomic PDF fit residual
    Note: the high- and low-temperature fits should be over the same r range
    '''

    def gaussianSmooth(x, y, sig=0.1):
        dr = np.mean((x - np.roll(x, 1))[1:])
        rs = np.arange(-10, 10, dr)
        s = np.exp(-rs ** 2 / 2.0 / sig ** 2)
        xsmooth, ysmooth = cv(x, y, rs, s)
        ysmooth /= np.trapz(s, rs)
        msk = np.logical_and(xsmooth > (x.min() - 0.5 * dr), xsmooth < (x.max() + 0.5 * dr))
        return ysmooth[msk]

    def residual(p, x, y, ycomp):
        stretch, width = p
        newx = stretch * x
        newy = np.interp(newx, x, y)
        newy = gaussianSmooth(newx, newy, width)
        msk = (x <= x.max() / stretch)
        return ycomp[msk] - newy[msk]

    p0 = [0.999, 0.1]  # typical starting guesses
    opt = least_squares(residual, p0, args=(rlow, dlow, dhigh))
    print(opt.x)
    stretch = opt.x[0]
    msk = (rlow <= rlow.max() / stretch)
    newdllow = dhigh[msk] - residual(opt.x, rlow, dlow, dhigh)
    diff = newdllow - dhigh[msk]
    return rhigh[msk], diff
def read_fgr(fileName)

Extract the PDF data and calculation from a .fgr file.

Args

fileName : str
path to the .fgr file containing the fit

Returns

r (numpy array): same r-grid as contained in the fit file gobs (numpy array): experimental PDF data gcalc (numpy array): the calculated PDF gdiff (numpy array): the structural PDF fit residual

Expand source code
def read_fgr(fileName):
    """Extract the PDF data and calculation from a .fgr file.

    Args:
        fileName (str): path to the .fgr file containing the fit

    Returns:
        r (numpy array): same r-grid as contained in the fit file
        gobs (numpy array): experimental PDF data
        gcalc (numpy array): the calculated PDF
        gdiff (numpy array): the structural PDF fit residual
    """
    if fileName[-4:] == '.fgr':
        lines = open(fileName).readlines()[:50]
        for idx, line in enumerate(lines):
            if 'start data' in line:
                startLine = 1 * idx
                break
        allcols = np.loadtxt(fileName, unpack=True, comments='#', skiprows=startLine)
        r, gcalc, gdiff = allcols[0], allcols[1], allcols[4]
        gobs = gcalc + gdiff
        return r, gobs, gcalc, gdiff
    else:
        print('Incompatible file type. Please provide a .fgr file from PDFgui.')
        return None
def sinTransform(q, fq, rmin=0.0, rmax=50.0, rstep=0.1)

Compute the sine Fourier transform of a function.

This method uses direct integration rather than an FFT and doesn't require an even grid. The grid for the Fourier transform is even and specifiable.

Args

q : numpy array
independent variable for function to be transformed
fq : numpy array
dependent variable for function to be transformed
rmin : float, default = 0.0
min value of conjugate independent variable grid
rmax : float, default = 50.0
maximum value of conjugate independent variable grid
rstep : float, default = 0.1
grid spacing for conjugate independent variable

Returns

r (numpy array): independent variable grid for transformed quantity

fr (numpy array): sine Fourier transform of fq

Expand source code
def sinTransform(q, fq, rmin=0.0, rmax=50.0, rstep=0.1):  # requires even q-grid
    """Compute the sine Fourier transform of a function.

    This method uses direct integration rather than an FFT and doesn't require
    an even grid. The grid for the Fourier transform is even and specifiable.

    Args:
        q (numpy array): independent variable for function to be transformed
        fq (numpy array): dependent variable for function to be transformed
        rmin (float, default = 0.0): min value of conjugate independent variable
            grid
        rmax (float, default = 50.0): maximum value of conjugate independent
            variable grid
        rstep (float, default = 0.1): grid spacing for conjugate independent
            variable

    Returns:
        r (numpy array): independent variable grid for transformed quantity

        fr (numpy array): sine Fourier transform of fq
    """
    r, fr = fourierTransform(q, fq, rmin, rmax, rstep)
    fr = np.imag(fr)
    return r, fr
def sinTransformDirectIntegration(q, fq, rmin=0.0, rmax=50.0, rstep=0.1)

Compute the sine Fourier transform of a function.

This method uses direct integration rather than an FFT and doesn't require an even grid. The grid for the Fourier transform is even and specifiable.

Args

q : numpy array
independent variable for function to be transformed
fq : numpy array
dependent variable for function to be transformed
rmin : float, default = 0.0
min value of conjugate independent variable grid
rmax : float, default = 50.0
maximum value of conjugate independent variable grid
rstep : float, default = 0.1
grid spacing for conjugate independent variable

Returns

r (numpy array): independent variable grid for transformed quantity

fr (numpy array): cosine Fourier transform of fq

Expand source code
def sinTransformDirectIntegration(q, fq, rmin=0.0, rmax=50.0, rstep=0.1):  # does not require even q-grid
    """Compute the sine Fourier transform of a function.

    This method uses direct integration rather than an FFT and doesn't require
    an even grid. The grid for the Fourier transform is even and specifiable.

    Args:
        q (numpy array): independent variable for function to be transformed
        fq (numpy array): dependent variable for function to be transformed
        rmin (float, default = 0.0): min value of conjugate independent variable
            grid
        rmax (float, default = 50.0): maximum value of conjugate independent
            variable grid
        rstep (float, default = 0.1): grid spacing for conjugate independent
            variable

    Returns:
        r (numpy array): independent variable grid for transformed quantity

        fr (numpy array): cosine Fourier transform of fq
    """
    lostep = int(np.ceil((rmin - 1e-8) / rstep))
    histep = int(np.floor((rmax + 1e-8) / rstep)) + 1
    r = np.arange(lostep, histep) * rstep
    qrmat = np.outer(r, q)
    integrand = fq * np.sin(qrmat)
    fr = np.sqrt(2.0 / np.pi) * np.trapz(integrand, q)
    return r, fr
def smoothData(xdata, ydata, qCutoff, func='sinc', gaussHeight=0.01)

Smooth out high-frequency contributions from the data.

This method performs a convolution in real space to simulate a truncation in reciprocal space. This is motivated by the fact that high-frequency noise can sometimes obscure the lower-frequency mPDF signal when the mPDF is collected together with the nuclear PDF. This high-frequency noise comes from scattering intensity at high q that cannot possibly come from magnetic scattering, due to the strong suppression from the magnetic form factor. To better isolate the mPDF from this high-frequency noise, one could truncate the Fourier transfrom at some cutoff q value beyond which the magnetic scattering is negligible (e.g. where the square of the magnetic form factor is reduced to 1% of its original value). This can be done by multiplying the scattering in q-space by a "window function" that is equal to unity for q between 0 and the cutoff value and 0 outside this window. By the convolution theorem, this is equivalent to convoluting the full Fourier transform in real space with a sinc function. Alternatively, one could multiply the scattering in q-space by a Guassian function which is reduced to some small amplitude at the desired cutoff q value. This is equivalent to a convolution in real space with another Gaussian function. The former method is recommended because it will generally be more physically justifiable.

Args

xdata
numpy array containing the independent variable of the data to be smoothed.
ydata
numpy array containing the dependent variable; this array will be smoothed.
qCutoff
q value beyond which all contributions will be ignored.
func
string specifying the type of real-space convolution function, either 'sinc' or 'gaussian' (see previous discussion).
gaussHeight
float specifying what the height of the q-space Gaussian function should be at the specified value of qCutoff.

Returns

Numpy array containing the smoothed version of ydata.

Expand source code
def smoothData(xdata, ydata, qCutoff, func='sinc', gaussHeight=0.01):
    """Smooth out high-frequency contributions from the data.

    This method performs a convolution in real space to simulate a truncation
    in reciprocal space. This is motivated by the fact that high-frequency
    noise can sometimes obscure the lower-frequency mPDF signal when the mPDF
    is collected together with the nuclear PDF. This high-frequency noise comes
    from scattering intensity at high q that cannot possibly come from magnetic
    scattering, due to the strong suppression from the magnetic form factor.
    To better isolate the mPDF from this high-frequency noise, one could
    truncate the Fourier transfrom at some cutoff q value beyond which the
    magnetic scattering is negligible (e.g. where the square of the magnetic
    form factor is reduced to 1% of its original value). This can be done by
    multiplying the scattering in q-space by a "window function" that is equal
    to unity for q between 0 and the cutoff value and 0 outside this window.
    By the convolution theorem, this is equivalent to convoluting the full
    Fourier transform in real space with a sinc function. Alternatively, one
    could multiply the scattering in q-space by a Guassian function which is
    reduced to some small amplitude at the desired cutoff q value.  This is
    equivalent to a convolution in real space with another Gaussian function.
    The former method is recommended because it will generally be more
    physically justifiable.

    Args:
        xdata: numpy array containing the independent variable of the data
            to be smoothed.
        ydata: numpy array containing the dependent variable; this array will
            be smoothed.
        qCutoff: q value beyond which all contributions will be ignored.
        func: string specifying the type of real-space convolution function,
            either 'sinc' or 'gaussian' (see previous discussion).
        gaussHeight: float specifying what the height of the q-space Gaussian
            function should be at the specified value of qCutoff.

    Returns:
        Numpy array containing the smoothed version of ydata.
    """
    dr = np.mean((xdata - np.roll(xdata, 1))[1:])
    rs = np.arange(-10, 10, dr)
    if func == 'sinc':
        s = np.sinc(rs * qCutoff / np.pi)
    elif func == 'gaussian':
        rg = 1.0 / (qCutoff * np.sqrt(np.log(1.0 / gaussHeight) / 2.0))
        s = np.exp(-rs ** 2 / 2.0 / rg ** 2)
    else:
        print('The only function options are sinc and gaussian. Please check')
        print('your input. Using sinc by default.')
        s = np.sinc(rs * qCutoff / np.pi)
    xsmooth, ysmooth = cv(xdata, ydata, rs, s, align=True, normalize=True)
    return ysmooth
def spinsFromAtoms(magstruc, positions, fractional=True, returnIdxs=False)

Return the spin vectors corresponding to specified atomic positions.

Args

magstruc
MagSpecies or MagStructure object containing atoms and spins
positions : list or array
atomic positions for which the corresponding spins should be returned.
fractional : boolean
set as True if the atomic positions are in fractional coordinates of the crystallographic lattice vectors.
returnIdxs : boolean
if True, the indices of the spins will also be returned.

Returns

Array consisting of the spins corresponding to the atomic positions.

Expand source code
def spinsFromAtoms(magstruc, positions, fractional=True, returnIdxs=False):
    """Return the spin vectors corresponding to specified atomic
       positions.

    Args:
        magstruc: MagSpecies or MagStructure object containing atoms and spins
        positions (list or array): atomic positions for which the
            corresponding spins should be returned.
        fractional (boolean): set as True if the atomic positions are in
            fractional coordinates of the crystallographic lattice
            vectors.
        returnIdxs (boolean): if True, the indices of the spins will also be
            returned.
    Returns:
        Array consisting of the spins corresponding to the atomic positions.
    """
    if len(np.array(positions).shape) == 1:
        positions = [positions]
    spins = []
    idxs = []
    badlist = []
    for pos in positions:
        if fractional:
            pos = magstruc.struc.lattice.cartesian(pos)
        mask = np.all(np.round(magstruc.atoms, decimals=4) ==
                      np.round(pos, decimals=4), axis=1)
        goodspins = magstruc.spins[mask]
        goodidxs = np.where(mask)[0]
        if len(goodspins) == 1:
            spins.append(goodspins[0])
            idxs.append(goodidxs[0])
        elif len(goodspins) > 1:
            print('Warning: duplicate atoms in structure')
            spins.append(goodspins)
            idxs.append(goodidxs)
        elif len(goodspins) == 0:
            spins.append([])
            idxs.append([])
            badlist.append(pos)
    if len(badlist) > 0:
        print('The following atomic positions do not correspond to a spin:')
        for bad in badlist:
            if fractional:
                bad = magstruc.struc.lattice.fractional(pos)
            print(bad)
    if not returnIdxs:
        return spins
    else:
        return spins, idxs
def ups(grid)

A function to generat the Upsilon filter from Roth et.al. (2018) This function computes an kernel using the upsilon function defined in Roth et.al. (2018), https://doi.org/10.1107/S2052252518006590.

Args

grid : array
the spatial grid over which the kernel is to be defined
Expand source code
def ups(grid):
    """A function to generat the Upsilon filter from Roth et.al. (2018)
    This function computes an kernel using the upsilon function defined in Roth et.al.
    (2018), https://doi.org/10.1107/S2052252518006590.
    Args:
        grid (array): the spatial grid over which the kernel is to be defined
    """
    g = lambda point: [0,0,0] if np.abs(np.linalg.norm(point)) <1e-6 else  point/np.linalg.norm(point)**4
    return np.apply_along_axis(g,3,grid)
def vec_ac(a1, a2, delta, corr_mode='same')

Correlate two 3D vector fields This function computes the autocorrelation of two 3D vector fields on regular grids. The autocorrelation is computed for each vector component and then summed

Args

a1 : array
The virst array to correlate
a2 : array
The second array to correlate
delta : float
the spacing between grid points
corr_mode : string
The mode to use for the scipy correlation function
Expand source code
def vec_ac(a1,a2,delta,corr_mode="same"):
    """Correlate two 3D vector fields
    This function computes the autocorrelation of two 3D vector fields on regular 
    grids. The autocorrelation is computed for each vector component and then summed
    Args:
        a1 (array): The virst array to correlate
        a2 (array): The second array to correlate
        delta (float): the spacing between grid points
        corr_mode (string): The mode to use for the scipy correlation function
    """
    ac = correlate(a1[:,:,:,0],a2[:,:,:,0],mode=corr_mode)*delta**3
    ac += correlate(a1[:,:,:,1],a2[:,:,:,1],mode=corr_mode)*delta**3
    ac += correlate(a1[:,:,:,2],a2[:,:,:,2],mode=corr_mode)*delta**3
    return ac
def vec_con(a1, a2, delta, conv_mode='same')

Implement convolution for 3D vector fields This function implements a convolution function for 3D vector fields on regular grids. The convolution is computed for each component of the vector fields and summed

Args

a1 : array
The first array to convolve
a2 : array
The second array to convolve
delta : float
The grid spacing
conv_mode : string
The mode to use for scipy convolution
Expand source code
def vec_con(a1,a2,delta,conv_mode="same"):
    """Implement convolution for 3D vector fields
    This function implements a convolution function for 3D vector fields on regular 
    grids. The convolution is computed for each component of the vector fields and summed
    Args:
        a1 (array): The first array to convolve
        a2 (array): The second array to convolve
        delta (float): The grid spacing
        conv_mode (string): The mode to use for scipy convolution
    """
    con = convolve(a1[:,:,:,0],a2[:,:,:,0],mode=conv_mode)*delta**3
    con += convolve(a1[:,:,:,1],a2[:,:,:,1],mode=conv_mode)*delta**3
    con += convolve(a1[:,:,:,2],a2[:,:,:,2],mode=conv_mode)*delta**3
    return con
def visualizeSpins(atoms, spins)

Set up a 3d plot showing an arrangement of spins.

Args

atoms : numpy array
array of atomic positions of spins to be visualized.
spins : numpy array
array of spin vectors in same order as atoms.

Returns

matplotlib figure object with a quiver plot on 3d axes.

Expand source code
def visualizeSpins(atoms, spins):
    """Set up a 3d plot showing an arrangement of spins.

    Args:
        atoms (numpy array): array of atomic positions of spins to be
            visualized.
        spins (numpy array): array of spin vectors in same order as atoms.

    Returns:
        matplotlib figure object with a quiver plot on 3d axes.
    """
    import matplotlib.pyplot as plt
    from mpl_toolkits.mplot3d import Axes3D

    xx, yy, zz = np.transpose(atoms)
    uu, vv, ww = np.transpose(spins)

    fig = plt.figure()
    ax = fig.add_subplot(111, projection='3d')
    for i in range(len(xx)):
        x, y, z = 1.0 * xx[i], 1.0 * yy[i], 1.0 * zz[i]
        u, v, w = 1.0 * uu[i], 1.0 * vv[i], 1.0 * ww[i]
        ax.quiver(x, y, z, u, v, w, pivot='middle') # I think this gives the correct arrow length
        #lngth = np.sqrt(u ** 2 + v ** 2 + w ** 2)
        #ax.quiver(x, y, z, u, v, w, length=lngth, pivot='middle')

    xmin, xmax = ax.get_xlim3d()
    ax.set_xlim3d(np.min((-1, xmin)), np.max((1, xmax)))
    ymin, ymax = ax.get_ylim3d()
    ax.set_ylim3d(np.min((-1, ymin)), np.max((1, ymax)))
    zmin, zmax = ax.get_zlim3d()
    ax.set_zlim3d(np.min((-1, zmin)), np.max((1, zmax)))

    return fig