Module diffpy.mpdf.mciftools

functions for creating magnetic structures from MCIF files.

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:    Eric Stubben and Benjamin Frandsen
#
# See AUTHORS.txt for a list of people who contributed.
# See LICENSE.txt for license information.
#
##############################################################################

"""functions for creating magnetic structures from MCIF files."""

import re

from cmath import exp

from diffpy.mpdf import *
from diffpy.structure.atom import Atom
from diffpy.structure.lattice import Lattice
from diffpy.structure.structure import Structure

from math import floor, ceil

import numpy as np
from numpy import pi, sin, cos, sqrt, dot
from numpy.linalg import det, inv, norm

from diffpy.mpdf.simpleparser import SimpleParser


def create_from_mcif(mcif, ffparamkey=None, rmaxAtoms=20, S=0.5, L=0.0,
                     J=None, gS=None, gL=None, g=None, j2type=None, occ=None):
    """
    Creates a MagStructure object from an MCIF file.
    
    Note: The only acceptable incommensurate structures at the moment are 1-k
    structures with no     Fourier harmonics (i.e. only coefficients for
    n = 1) and no atoms with nonzero average magnetic moment.

    Args:
        mcif (string): path to MCIF file for desired magnetic structure.
        ffparamkey (string): optional; gives the appropriate key for getFFparams()
            to generate the correct magnetic form factor.
        rmaxAtoms (float): radius to which magnetic atoms should be generated.
            Default is 20 Angstroms.
        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.
        gS (float): spin component of the Lande g-factor (g = gS+gL).
            Calculated automatically from S, L, and J if not specified.
        gL (float): orbital component of the Lande g-factor. Calculated
            automatically from S, L, and J if not specified.
        g (float): Lande g-factor. Calculated automatically as gS+gL if not
            specified.
        j2type (string): Specifies the way the j2 integral is included in the
            magnetic form factor. Must be either 'RE' for rare earth or 'TM' for
            transition metal. If 'RE', the coefficient on the j2 integral is
            gL/g; if 'TM', the coefficient is (g-2)/g. Default is 'RE'. Note
            that for the default values of S, L, and J, we will have gS=2,
            gL=0, and g=2, and there will be no difference in the form factor
            calculation for 'RE' or 'TM'.

    Returns:
        MagStructure object corresponding to the magnetic structure
            encoded in the MCIF file. Note that the position and spin arrays
            are not automatically populated when using this function, so
            MagStructure.makeAll() will likely need to be called afterward.
            Also note that variables such as ffparamkey, rmaxAtoms, S, L, J,
            etc are applied uniformly to all magnetic atoms in the unit cell,
            so this method cannot create MagSpecies objects for which these
            attributes are different.

    """
    # creates an empty MagStructure
    mstruc = MagStructure()    

    #Flags whether the structure is incommensurate
    incomm = False

    #Extracts the needed information from the mcif file
    tags = {}
    parser = SimpleParser(tags)
    tags = parser.ReadFile(mcif)

    #These are the parameters of the basic or unit magnetic cell
    #'a', 'b', and 'c' are in angstroms, while 'alpha', 'beta', and 'gamma' are in degrees
    a = tags['_cell_length_a'][0]
    b = tags['_cell_length_b'][0]
    c = tags['_cell_length_c'][0]
    alpha = tags['_cell_angle_alpha'][0]
    beta = tags['_cell_angle_beta'][0]
    gamma = tags['_cell_angle_gamma'][0]
    transform = tags['_parent_space_group.child_transform_Pp_abc'][0]

    #symbols = tags['_atom_site_type_symbol']
    symbols = tags['_atom_site_label']
    symbols0 = 1*symbols # starting list of atom symbols
    try:
        occs = tags['_atom_site_occupancy']
    except KeyError:
        occs = [1.0] * len(symbols)
    occs0 = 1*occs # starting list of occupancies
    scaled_pos = np.array([tags['_atom_site_fract_x'],
                           tags['_atom_site_fract_y'],
                           tags['_atom_site_fract_z']]).T
    scaled_pos = np.mod(scaled_pos,1.)

    mag_mom = np.zeros_like(scaled_pos)
    mag_idx = []

    if '_cell_wave_vector_x' in tags:
        #This is the branch for incommensurate structures
        incomm = True
        k = np.matrix.flatten(np.array([tags['_cell_wave_vector_x'],
                                        tags['_cell_wave_vector_y'],
                                        tags['_cell_wave_vector_z']]).T)

        symm_cent = tags['_space_group_symop_magn_ssg_centering.algebraic']
        symm_ops = tags['_space_group_symop_magn_ssg_operation.algebraic']

        #For incommensurate structures, mag_mom is the average magnetic moment of an atom. Currently,
        #this variable is unused for incommensurate structures
        four_cos = np.zeros_like(scaled_pos)
        four_sin = np.zeros_like(scaled_pos)
        for i, label in enumerate(tags['_atom_site_label']):
            if label in tags['_atom_site_moment.label']:
                mag_idx.append(i)
                mi = tags['_atom_site_moment.label'].index(label)
                mag_mom[i] = [tags['_atom_site_moment.crystalaxis_x'][mi],
                              tags['_atom_site_moment.crystalaxis_y'][mi],
                              tags['_atom_site_moment.crystalaxis_z'][mi]]

                four_cos[i] = [tags['_atom_site_moment_Fourier_param.cos'][3*mi],
                               tags['_atom_site_moment_Fourier_param.cos'][3*mi + 1],
                               tags['_atom_site_moment_Fourier_param.cos'][3*mi + 2]]

                four_sin[i] = [tags['_atom_site_moment_Fourier_param.sin'][3*mi],
                               tags['_atom_site_moment_Fourier_param.sin'][3*mi + 1],
                               tags['_atom_site_moment_Fourier_param.sin'][3*mi + 2]]
    else:
        #This is the branch for commensurate structures
        k = np.array([0,0,0])

        symm_cent = tags['_space_group_symop_magn_centering.xyz']
        symm_ops = tags['_space_group_symop_magn_operation.xyz']

        for i, label in enumerate(tags['_atom_site_label']):
            if label in tags['_atom_site_moment.label']:
                mag_idx.append(i)
                mi = tags['_atom_site_moment.label'].index(label)
                mag_mom[i] = [tags['_atom_site_moment.crystalaxis_x'][mi],
                              tags['_atom_site_moment.crystalaxis_y'][mi],
                              tags['_atom_site_moment.crystalaxis_z'][mi]]

    #Takes the magnetic information to reduced lattice coordinates
    LL = np.diag([1./a,1./b,1./c])
    mag_mom = dot(mag_mom,LL)
    if incomm:
        four_cos = dot(four_cos,LL)
        four_sin = dot(four_sin,LL)

    #Performs the symmetry operations to populate the rest of the unit cell
    all_pos = np.copy(scaled_pos)
    all_mom = np.copy(mag_mom)
    if incomm:
        all_cos = np.copy(four_cos)
        all_sin = np.copy(four_sin)

    tol = 1e-3
    for j, pos in enumerate(scaled_pos):
        pos_subset = np.array([pos])
        eq_site_counter = 1
        for cent in symm_cent:
            #Applies a centering operation
            if incomm:
                Rc, hc, r1c, tc, t4c, trc = parse_magn_ssg_operation(cent)
            else:
                Rc, tc, trc = parse_magn_operation(cent)
            cent_pos = np.mod(dot(Rc,pos) + tc,1.)

            for op in symm_ops:
                #Applies a symmetry operation
                if incomm:
                    R, h, r1, t, t4, tr = parse_magn_ssg_operation(op)
                else:
                    R, t, tr = parse_magn_operation(op)
                eq_site = dot(R,cent_pos) + t

                #Handles the cases where eq_site has coordinate values extremely close to 0 or 1
                for l, coord in enumerate(eq_site):
                    if abs(1 - coord) < tol:
                        eq_site[l] = 1.
                    if abs(coord) < tol:
                        eq_site[l] = 0.
                eq_site = np.mod(eq_site,1.)
                eq_site = np.round(eq_site, decimals=6)

                #Checks to make sure site hasn't already been occupied
                for l, pp in enumerate(pos_subset):
                    if np.allclose(eq_site,pp,atol=tol): # and the atom labels are the same (fix this)
                        break
                else:
                    #Adds the new site found with symmetry
                    if j in mag_idx:
                        mag_idx.append(len(symbols))
                    symbols.append(symbols0[j]+'.'+str(eq_site_counter))
                    occs.append(occs0[j])
                    eq_site_counter += 1
                    pos_subset = np.append(pos_subset,[eq_site],axis=0)
                    all_pos = np.append(all_pos,[eq_site],axis=0)

                    #Transforms the moment information
                    if incomm:
                        deltc = 2*pi*(t4c + dot(hc,pos))
                        delt = 2*pi*(t4 + dot(h,cent_pos))

                        cent_cos = trc*det(Rc)*(cos(deltc)*dot(Rc,four_cos[j]) - r1c*sin(deltc)*dot(Rc,four_sin[j]))
                        cent_sin = trc*det(Rc)*(sin(deltc)*dot(Rc,four_cos[j]) + r1c*cos(deltc)*dot(Rc,four_sin[j]))

                        new_cos = tr*det(R)*(cos(delt)*dot(R,cent_cos) - r1*sin(delt)*dot(R,cent_sin))
                        new_sin = tr*det(R)*(sin(delt)*dot(R,cent_cos) + r1*cos(delt)*dot(R,cent_sin))

                        all_cos = np.append(all_cos,[new_cos],axis=0)
                        all_sin = np.append(all_sin,[new_sin],axis=0)

                    new_mom = tr*trc*det(R)*det(Rc)*dot(R,dot(Rc,mag_mom[j]))   
                    all_mom = np.append(all_mom,[new_mom],axis=0)

    #Determines the basis vectors of the structure (same as the magnetic moments if commensurate)
    basis_vecs = np.empty(np.shape(all_pos),dtype=complex)
    if incomm:
        basis_vecs = (0.5*np.exp(-2*pi*1j*dot(all_pos,k)))[:,np.newaxis]*(all_cos + 1j*all_sin)
        #for i, pos in enumerate(all_pos):
            #basis_vecs[i] = 0.5*exp(-2*pi*1j*dot(k,pos))*(all_cos[i] + 1j*all_sin[i])
    else:
        basis_vecs = all_mom.astype(complex)
        #for i, pos in enumerate(all_pos):
            #basis_vecs[i] = all_mom[i].astype(complex)

    #Converts the basis vectors to Cartesian coordinates
    lat = Lattice(a,b,c,alpha,beta,gamma)
    basis_vecs = dot(basis_vecs,lat.base)
    all_mom = dot(all_mom,lat.base)

    #Creates a diffpy Structure object
    atoms = []
    for i, pos in enumerate(all_pos):
        atoms.append(Atom(atype=symbols[i],xyz=pos, occupancy=occs[i]))
    astruc = Structure(atoms=atoms)
    astruc.lattice = lat
    for atom in astruc: # remove extra numbers from atom names
        name = atom.element
        newname = re.findall(r'[a-zA-Z]+', name)[0]
        atom.element = newname


    #Creates a separate MagSpecies object for each magnetic atom in the unit cell
    for idx in mag_idx:
        new_mspec = MagSpecies(ffparamkey=ffparamkey, rmaxAtoms=rmaxAtoms,
                               S=S, L=L, J=J, gS=gS, gL=gL, g=g,
                               j2type=j2type)

        new_mspec.struc = astruc
        new_mspec.label = symbols[idx]

        new_mspec.strucIdxs = [idx]
        new_mspec.origin = all_pos[idx]
        new_mspec.occ = occs[idx]

        new_mspec.kvecs = np.array([k])
        new_mspec.basisvecs = np.array([basis_vecs[idx]])
        if incomm:
            new_mspec.kvecs = np.append(new_mspec.kvecs,[-k],axis=0)
            new_mspec.basisvecs = np.append(new_mspec.basisvecs,[np.conjugate(basis_vecs[idx])],axis=0)
        new_mspec.avgmom = all_mom[idx] if incomm else np.array([0, 0, 0])

        mstruc.loadSpecies(new_mspec)

    mstruc.transform = transform
    print('MagStructure creation from mcif file successful.')
    return mstruc


   
def create_atomic_cell(mstruc,transform):
    """
    This method accepts a MagStructure object that has already been loaded with an mcif file
    and a string containing the parent-child basis transformation information. The output will be a new
    diffpy.Structure object loaded with the atomic structure information related to the magnetic structure
    of the original sample.
    """
    #Processes the string with the parent-child transformation info
    R, t = parse_parent_child_transform(transform)
    R_p = inv(R)
    t_p = dot(t,R_p)

    #Transforms the magnetic cell basis to an atomic cell basis per the inverse of the given transform
    mag_cell = mstruc.struc.lattice.base
    atom_cell = dot(R_p,mag_cell)

    #Determines the dimension of the supercell we need to completely contain the atomic cell
    dim  = [[floor(min(R_p[:,0]) - t_p[0]) - 1,ceil(max(R_p[:,0]) - t_p[0]) + 1],
            [floor(min(R_p[:,1]) - t_p[1]) - 1,ceil(max(R_p[:,1]) - t_p[1]) + 1],
            [floor(min(R_p[:,2]) - t_p[2]) - 1,ceil(max(R_p[:,2]) - t_p[2]) + 1]]

    sup_pos, sup_occup, sup_symb = supersize_me(mstruc.struc,dim)

    #Determines atomic unit cell from the newly created magnetic supercell
    scaled_pos = []
    occup = []
    symb = []

    tol = 1e-3
    for l, pos in enumerate(sup_pos):
        #Converts the position to fractional atomic lattice coordinates
        new_pos = dot(pos,R) + t

        #Tidies up coordinates close enough to 0 or 1        
        for i, coord in enumerate(new_pos):
            if abs(1 - coord) < tol:
                new_pos[i] = 1.
            if abs(coord) < tol:
                new_pos[i] = 0.

        #Checks if the atoms sits inside the atomic unit cell
        if (0<=new_pos[0]<1) and (0<=new_pos[1]<1) and (0<=new_pos[2]<1):
            add_pos = True            

            #Also checks for duplicates
            dupes, dupe_idxs = find_dupes(scaled_pos,new_pos,tol)
            if len(dupes) >= 1:
                tot_occup = sup_occup[l]
                for idx in dupe_idxs:
                    tot_occup += occup[idx]
                    if element(symb[idx]) == element(sup_symb[l]):
                        add_pos = False
                if (tot_occup - 1) > tol:
                    add_pos = False
            #for m, pp in enumerate(scaled_pos):
            #    if np.allclose(new_pos,pp,atol=tol):
            #        break
            #else:
            if add_pos:
                scaled_pos.append(new_pos)
                occup.append(sup_occup[l])
                symb.append(sup_symb[l])

    #Creates a new diffpy.Structure object representing the atomic unit cell
    atoms = []
    for l, pos in enumerate(scaled_pos):
        atoms.append(Atom(atype=symb[l],xyz=pos,occupancy=occup[l]))
    new_struc = Structure(atoms=atoms)
    new_struc.lattice = Lattice(base=atom_cell)

    return new_struc


def create_magnetic_cell(astruc,transform):
    """
    Takes a diffpy.Structure object representing the atomic unit cell and a string containing information 
    about the parent to child basis vector transformation. Returns a new diffpy.Structure object
    representing the magnetic unit cell. 

    Note: At the moment, the resulting magnetic structure won't actually have any magnetic moment 
    information; it will just be a list of atoms and their positions in the unit cell. Work still needs to
    be done to figure out how to connect atoms in the magnetic cell with the correct moment information.
    """
    #Processes the string with the parent-child transformation info
    R, t = parse_parent_child_transform(transform)
    R_p = inv(R)
    t_p = dot(t,R_p)
    
    #Transforms the atomic cell basis to a magnetic cell basis using the given transform
    atom_cell = astruc.lattice.base
    mag_cell = dot(R,atom_cell)
    
    #Determines the dimensions of the supercell we want to work with to get the magnetic cell
    dim = [[floor(min(R[:,0]) + t[0]) - 1,ceil(max(R[:,0]) + t[0]) + 1],
           [floor(min(R[:,1]) + t[1]) - 1,ceil(max(R[:,1]) + t[1]) + 1],
           [floor(min(R[:,2]) + t[2]) - 1,ceil(max(R[:,2]) + t[2]) + 1]]
    
    sup_pos, sup_occup, sup_symb = supersize_me(astruc,dim)
    
    #Picks out the magnetic unit cell from the newly created atomic supercell
    scaled_pos = []
    occup = []
    symb = []
    
    tol = 1e-3
    for l, pos in enumerate(sup_pos):
        #Converts the position to fractional magnetic lattice coordinates
        new_pos = dot(pos,R_p) - t_p

        #Tidies up coordinates close enough to 0 or 1        
        for i, coord in enumerate(new_pos):
            if abs(1 - coord) < tol:
                new_pos[i] = 1.
            if abs(coord) < tol:
                new_pos[i] = 0.

        #Checks if the atom sits inside the magnetic unit cell
        if (0<=new_pos[0]<1) and (0<=new_pos[1]<1) and (0<=new_pos[2]<1):
            add_pos = True

            #Also checks for duplicates (which may be allowed based on occupancy)
            dupes, dupe_idxs = find_dupes(scaled_pos,new_pos,tol)
            if len(dupes) >= 1:
                tot_occup = sup_occup[l]
                for idx in dupe_idxs:
                    tot_occup += occup[idx]
                    if element(symb[idx]) == element(sup_symb[l]):
                        add_pos = False
                if (tot_occup - 1) > tol:
                    add_pos = False
            #for m, pp in enumerate(scaled_pos):
            #    if np.allclose(new_pos,pp,atol=tol):
            #        break
            #else:
            if add_pos:
                scaled_pos.append(new_pos)
                occup.append(sup_occup[l])
                symb.append(sup_symb[l])
            
    #Creates a new diffpy.Structure object representing the magnetic unit cell (without the moment information)
    atoms = []
    for l, pos in enumerate(scaled_pos):
        atoms.append(Atom(atype=symb[l],xyz=pos,occupancy=occup[l]))
    new_struc = Structure(atoms=atoms)
    new_struc.lattice = Lattice(base=mag_cell)
    
    return new_struc


def parse_magn_ssg_operation(op):
    """
    Parses a string containing a superspace group symmetry operation in standard
    notation. Returns the rotation and translation part of the associated space
    group operation (R and t), the average structure reciprocal lattice vector (h), 
    superspace phase information (r1 and t4), and the time reversal sign (p).
    """
    r1,r2,r3,r4,p=op.split(',')
    M = np.zeros([4,4])
    t = np.zeros([3])
    h = np.zeros([3])
    
    #Compiles translation information
    t[0] = convert_to_float(r1[-4:]) if '/' in r1 else 0.
    t[1] = convert_to_float(r2[-4:]) if '/' in r2 else 0.
    t[2] = convert_to_float(r3[-4:]) if '/' in r3 else 0.
    t4 = convert_to_float(r4[-4:]) if '/' in r4 else 0.
    
    #Compiles rotation matrix
    for i, line in enumerate([r1,r2,r3,r4]):
        x1=x2=x3=x4=0.
        m = re.search(r'-?\d?x1',line)
        if m:
            xs=m.group()
            x1 = 1. if ('x1' in xs and not '-' in xs) else -1.
            xs = xs.replace('x1','')
            m = re.search(r'\d',xs)
            if m:
                x1 *= float(m.group())

        m = re.search(r'-?\d?x2',line)
        if m:
            ys=m.group()
            x2 = 1. if ('x2' in ys and not '-' in ys) else -1.
            ys = ys.replace('x2','')
            m = re.search(r'\d',ys)
            if m:
                x2 *= float(m.group())
                
        m = re.search(r'-?\d?x3',line)
        if m:
            zs=m.group()
            x3 = 1. if ('x3' in zs and not '-' in zs) else -1.
            zs = zs.replace('x3','')
            m = re.search(r'\d',zs)
            if m:
                x3 *= float(m.group())
                
        m = re.search(r'-?\d?x4',line)
        if m:
            ts = m.group()
            x4 = 1. if ('x4' in ts and not '-' in ts) else -1.
            ts = ts.replace('x4','')
            m = re.search(r'\d',ts)
            if m:
                x4 *= float(m.group())
            
        M[i,0], M[i,1], M[i,2], M[i,3] = [x1,x2,x3,x4]
    
    R = M[:3,:3]
    h = M[3,:3]
    r1 = M[3,3]
        
    return (R,h,r1,t,t4,float(p))
    

#Essentially a copy of muesr's parse_magn_operation_xyz_string function
def parse_magn_operation(op):
    """
    Parses a string containg a space group symmetry operation in standard notation.
    Returns the rotation and translation parts of the operation (R and t) and the 
    time reversal sign (p).
    """
    r1,r2,r3,p=op.split(',')
    R = np.zeros([3,3]) # rotations
    t = np.zeros([3])   # translations
    
    # compile traslation matrix
    t[0] = convert_to_float(r1[-4:]) if '/' in r1 else 0.
    t[1] = convert_to_float(r2[-4:]) if '/' in r2 else 0.
    t[2] = convert_to_float(r3[-4:]) if '/' in r3 else 0.
    
    # compile rotation matrix
    for i, line in enumerate([r1,r2,r3]):
        x=y=z=0.
        m = re.search(r'-?\d?x',line)
        if m:
            xs=m.group()
            x = 1. if ('x' in xs and not '-' in xs) else -1.
            m = re.search(r'\d',xs)
            if m:
                x *= float(m.group())

        m = re.search(r'-?\d?y',line)
        if m:
            ys=m.group()
            y = 1. if ('y' in ys and not '-' in ys) else -1.
            m = re.search(r'\d',ys)
            if m:
                y *= float(m.group())
                
        m = re.search(r'-?\d?z',line)
        if m:
            zs=m.group()
            z = 1. if ('z' in zs and not '-' in zs) else -1.
            m = re.search(r'\d',zs)
            if m:
                z *= float(m.group())                            
            
        R[i,0], R[i,1], R[i,2] = [x,y,z]
        
    return (R,t,float(p))


#This is based on muesr's parse_magn_operation_xyz_string method    
def parse_parent_child_transform(transform):
    """
    Takes in a string containing information about the transformation from the parent
    (paramagnetic) basis to the child (magnetic) basis and returns the "rotation" (not
    necessarily a pure rotation) matrix and translation vector associated with the 
    transformation.
    """
    #Splits up the string into related pieces of information
    rotation, translation = transform.split(';')
    r1, r2, r3 = rotation.split(',')
    t1, t2, t3 = translation.split(',')

    #Initializes the rotation matrix and translation vector
    R = np.zeros((3,3))
    t = np.zeros(3)
    
    #Compiles the translation vector
    t[0] = convert_to_float(t1) if '/' in t1 else t1
    t[1] = convert_to_float(t2) if '/' in t2 else t2
    t[2] = convert_to_float(t3) if '/' in t3 else t3
    
    #Compiles the rotation matrix
    for i, line in enumerate([r1,r2,r3]):
        x = y = z = 0
        
        m = re.search(r'-?\d?/?\d?a',line)
        if m:
            xs = m.group()
            x = 1. if ('a' in xs and not '-' in xs) else -1.
            m = re.search(r'\d/?\d?',xs)
            if m:
                if '/' in m.group():
                    x *= convert_to_float(m.group())
                else:
                    x *= float(m.group())
        
        m = re.search(r'-?\d?/?\d?b',line)
        if m:
            ys = m.group()
            y = 1. if ('b' in ys and not '-' in ys) else -1.
            m = re.search(r'\d/?\d?',ys)
            if m:
                if '/' in m.group():
                    y *= convert_to_float(m.group())
                else:
                    y *= float(m.group())
                    
        m = re.search(r'-?\d?/?\d?c',line)
        if m:
            zs = m.group()
            z = 1. if ('c' in zs and not '-' in zs) else -1.
            m = re.search(r'\d/?\d?',zs)
            if m:
                if '/' in m.group():
                    z *= convert_to_float(m.group())
                else:
                    z *= float(m.group())
        
        R[i,0] = x
        R[i,1] = y
        R[i,2] = z
        
    return (R,t)


#ATTENTION: This method should now work with diffpy, but testing still needs to be done
def supersize_me(struc,dim):
    """
    Accepts a diffpy.Structure object and a list of pairs specifying the beginning and ending
    cell along each crystal axis and returns two arrays containing the fractional coordinates of the 
    atoms in the supercell relative to the original unit cell and their chemical symbols, respectively.
    """
    #Extracts the necessary information from the MagStructure object
    old_pos = struc.xyz
    old_occup = struc.occupancy
    old_symb = []
    for i in range(len(struc.element)):
        old_symb.append(struc.element[i])
    
    #Appends the desired additional cells to the original unit cell
    pos_list = []
    occup_list = []
    symb = []
    
    for k in range(dim[2][0],dim[2][1]):
        for j in range(dim[1][0],dim[1][1]):
            for i in range(dim[0][0],dim[0][1]):
                for l, pos in enumerate(old_pos):
                    pos_list.append(pos + np.array([i,j,k]))
                    occup_list.append(old_occup[l])
                    symb.append(old_symb[l])
    
    pos = np.array(pos_list)
    occup = np.array(occup_list)
    
    return (pos,occup,symb)


def order_by_pos(original,unordered):
    """
    This method accepts two arrays of the same size and outputs a third array also of that size.
    The third array is created by permuting the rows of the 'unordered' array in such a way
    that the point given by the ith row of the output array is closer (in terms of Euclidean distance)
    to the point given by the ith row vector of the 'original' array than to the point given by any
    other row of the 'original' array. The intended application of this method is to recover the
    ordering of an array of atomic positions in a magnetic unit cell after downsizing to an atomic unit
    cell and then upsizing once more.
    
    In this method, we're assuming that each point in the 'unordered' array is closest to exactly one 
    point in the original array. The motivation for this is that the rescaling of the atomic unit cell
    during PDF and mPDF fits should only cause small changes in atomic positions within the unit cell, 
    so no atom should stray far enough from their original position to end up closer to a different
    atom.
    """
    #Checks if the input arrays have the same shape
    if np.shape(original)!=np.shape(unordered):
        print('Error: Input arrays must be of the same size.')
        return

    ordered = np.zeros_like(unordered)
    unordered_copy = np.copy(unordered)

    for i, pos_orig in enumerate(original):
        #For a given point in the 'original' array, finds the closest point in the 'unordered' array
        closest_pos = 0
        closest_dist = 1e6

        for j, pos_unord in enumerate(unordered_copy):
            curr_dist = norm(pos_orig - pos_unord)
            if curr_dist < closest_dist:
                closest_dist = curr_dist
                closest_pos = j

        #Adds the closest point in the 'unordered' array to the 'ordered' array in the correct position
        ordered[i] = unordered_copy[closest_pos]

        #Deletes the position from the 'unordered' array to speed up computation
        unordered_copy = np.delete(unordered_copy,closest_pos,0)

    return ordered


def find_dupes(array,value,tol=1e-3):
    """
    Given an m x n array, an n-vector, and a tolerance, determines which rows of the array match 
    the n-vector within the desired (absolute) tolerance. Returns an array consisting of the 
    matching rows and a list containing the respective indices of the rows as they were in the 
    original array.
    """
    dupes = np.array([])
    dupe_idxs = []
    
    for i, row in enumerate(array):
        if np.allclose(row,value,atol=tol):
            dupes = np.append(dupes,row,axis=0)
            dupe_idxs.append(i)
    
    return dupes, dupe_idxs


def element(site_label):
    """
    Finds and returns the chemical symbol of an element based on its site label. 
    """
    chem_symbol = site_label[0]
    
    if len(site_label) > 1:
        if site_label[1].isalpha():
            chem_symbol += site_label[1]
    
    return chem_symbol


def convert_to_float(frac_str):
    """
    This function takes in a string containing a fraction and returns a float.
    """
    num, denom = frac_str.split('/')
    return (float(num) / float(denom))
    

#This method can probably be removed in favor of diffpy's own cell vector generating method
def cellpar_to_cell(cell_par):
    """
    Takes in a list of cell parameters (three side lengths and three angles are expected) and
    returns an array with the cell lattice vectors as rows. The lattice vectors are output in 
    standard Cartesian coordinates with the a- and b-vectors in the xy-plane and the a-vector
    parallel to the x-axis.
    """
    #Extract the cell parameters from the list
    a, b, c, alpha, beta, gamma = cell_par
    
    #Initializes a-vector parallel to x-axis
    va = a * np.array([1,0.,0.])

    #Constructs b-vector in the xy-plane
    vb = b * np.array([cos(gamma),sin(gamma),0.])

    #Determines resulting c-vector subject to parameter constraints
    cx = cos(beta)
    cy = (cos(alpha) - cos(beta)*cos(gamma))/sin(gamma)
    cz = sqrt(1 - cx**2 - cy**2)
    vc = c * np.array([cx,cy,cz])

    #Makes and returns the cell array
    cell = np.vstack((va,vb,vc))
    return cell

Functions

def cellpar_to_cell(cell_par)

Takes in a list of cell parameters (three side lengths and three angles are expected) and returns an array with the cell lattice vectors as rows. The lattice vectors are output in standard Cartesian coordinates with the a- and b-vectors in the xy-plane and the a-vector parallel to the x-axis.

Expand source code
def cellpar_to_cell(cell_par):
    """
    Takes in a list of cell parameters (three side lengths and three angles are expected) and
    returns an array with the cell lattice vectors as rows. The lattice vectors are output in 
    standard Cartesian coordinates with the a- and b-vectors in the xy-plane and the a-vector
    parallel to the x-axis.
    """
    #Extract the cell parameters from the list
    a, b, c, alpha, beta, gamma = cell_par
    
    #Initializes a-vector parallel to x-axis
    va = a * np.array([1,0.,0.])

    #Constructs b-vector in the xy-plane
    vb = b * np.array([cos(gamma),sin(gamma),0.])

    #Determines resulting c-vector subject to parameter constraints
    cx = cos(beta)
    cy = (cos(alpha) - cos(beta)*cos(gamma))/sin(gamma)
    cz = sqrt(1 - cx**2 - cy**2)
    vc = c * np.array([cx,cy,cz])

    #Makes and returns the cell array
    cell = np.vstack((va,vb,vc))
    return cell
def convert_to_float(frac_str)

This function takes in a string containing a fraction and returns a float.

Expand source code
def convert_to_float(frac_str):
    """
    This function takes in a string containing a fraction and returns a float.
    """
    num, denom = frac_str.split('/')
    return (float(num) / float(denom))
def create_atomic_cell(mstruc, transform)

This method accepts a MagStructure object that has already been loaded with an mcif file and a string containing the parent-child basis transformation information. The output will be a new diffpy.Structure object loaded with the atomic structure information related to the magnetic structure of the original sample.

Expand source code
def create_atomic_cell(mstruc,transform):
    """
    This method accepts a MagStructure object that has already been loaded with an mcif file
    and a string containing the parent-child basis transformation information. The output will be a new
    diffpy.Structure object loaded with the atomic structure information related to the magnetic structure
    of the original sample.
    """
    #Processes the string with the parent-child transformation info
    R, t = parse_parent_child_transform(transform)
    R_p = inv(R)
    t_p = dot(t,R_p)

    #Transforms the magnetic cell basis to an atomic cell basis per the inverse of the given transform
    mag_cell = mstruc.struc.lattice.base
    atom_cell = dot(R_p,mag_cell)

    #Determines the dimension of the supercell we need to completely contain the atomic cell
    dim  = [[floor(min(R_p[:,0]) - t_p[0]) - 1,ceil(max(R_p[:,0]) - t_p[0]) + 1],
            [floor(min(R_p[:,1]) - t_p[1]) - 1,ceil(max(R_p[:,1]) - t_p[1]) + 1],
            [floor(min(R_p[:,2]) - t_p[2]) - 1,ceil(max(R_p[:,2]) - t_p[2]) + 1]]

    sup_pos, sup_occup, sup_symb = supersize_me(mstruc.struc,dim)

    #Determines atomic unit cell from the newly created magnetic supercell
    scaled_pos = []
    occup = []
    symb = []

    tol = 1e-3
    for l, pos in enumerate(sup_pos):
        #Converts the position to fractional atomic lattice coordinates
        new_pos = dot(pos,R) + t

        #Tidies up coordinates close enough to 0 or 1        
        for i, coord in enumerate(new_pos):
            if abs(1 - coord) < tol:
                new_pos[i] = 1.
            if abs(coord) < tol:
                new_pos[i] = 0.

        #Checks if the atoms sits inside the atomic unit cell
        if (0<=new_pos[0]<1) and (0<=new_pos[1]<1) and (0<=new_pos[2]<1):
            add_pos = True            

            #Also checks for duplicates
            dupes, dupe_idxs = find_dupes(scaled_pos,new_pos,tol)
            if len(dupes) >= 1:
                tot_occup = sup_occup[l]
                for idx in dupe_idxs:
                    tot_occup += occup[idx]
                    if element(symb[idx]) == element(sup_symb[l]):
                        add_pos = False
                if (tot_occup - 1) > tol:
                    add_pos = False
            #for m, pp in enumerate(scaled_pos):
            #    if np.allclose(new_pos,pp,atol=tol):
            #        break
            #else:
            if add_pos:
                scaled_pos.append(new_pos)
                occup.append(sup_occup[l])
                symb.append(sup_symb[l])

    #Creates a new diffpy.Structure object representing the atomic unit cell
    atoms = []
    for l, pos in enumerate(scaled_pos):
        atoms.append(Atom(atype=symb[l],xyz=pos,occupancy=occup[l]))
    new_struc = Structure(atoms=atoms)
    new_struc.lattice = Lattice(base=atom_cell)

    return new_struc
def create_from_mcif(mcif, ffparamkey=None, rmaxAtoms=20, S=0.5, L=0.0, J=None, gS=None, gL=None, g=None, j2type=None, occ=None)

Creates a MagStructure object from an MCIF file.

Note: The only acceptable incommensurate structures at the moment are 1-k structures with no Fourier harmonics (i.e. only coefficients for n = 1) and no atoms with nonzero average magnetic moment.

Args

mcif : string
path to MCIF file for desired magnetic structure.
ffparamkey : string
optional; gives the appropriate key for getFFparams() to generate the correct magnetic form factor.
rmaxAtoms : float
radius to which magnetic atoms should be generated. Default is 20 Angstroms.
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.
gS : float
spin component of the Lande g-factor (g = gS+gL). Calculated automatically from S, L, and J if not specified.
gL : float
orbital component of the Lande g-factor. Calculated automatically from S, L, and J if not specified.
g : float
Lande g-factor. Calculated automatically as gS+gL if not specified.
j2type : string
Specifies the way the j2 integral is included in the magnetic form factor. Must be either 'RE' for rare earth or 'TM' for transition metal. If 'RE', the coefficient on the j2 integral is gL/g; if 'TM', the coefficient is (g-2)/g. Default is 'RE'. Note that for the default values of S, L, and J, we will have gS=2, gL=0, and g=2, and there will be no difference in the form factor calculation for 'RE' or 'TM'.

Returns

MagStructure object corresponding to the magnetic structure encoded in the MCIF file. Note that the position and spin arrays are not automatically populated when using this function, so MagStructure.makeAll() will likely need to be called afterward. Also note that variables such as ffparamkey, rmaxAtoms, S, L, J, etc are applied uniformly to all magnetic atoms in the unit cell, so this method cannot create MagSpecies objects for which these attributes are different.

Expand source code
def create_from_mcif(mcif, ffparamkey=None, rmaxAtoms=20, S=0.5, L=0.0,
                     J=None, gS=None, gL=None, g=None, j2type=None, occ=None):
    """
    Creates a MagStructure object from an MCIF file.
    
    Note: The only acceptable incommensurate structures at the moment are 1-k
    structures with no     Fourier harmonics (i.e. only coefficients for
    n = 1) and no atoms with nonzero average magnetic moment.

    Args:
        mcif (string): path to MCIF file for desired magnetic structure.
        ffparamkey (string): optional; gives the appropriate key for getFFparams()
            to generate the correct magnetic form factor.
        rmaxAtoms (float): radius to which magnetic atoms should be generated.
            Default is 20 Angstroms.
        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.
        gS (float): spin component of the Lande g-factor (g = gS+gL).
            Calculated automatically from S, L, and J if not specified.
        gL (float): orbital component of the Lande g-factor. Calculated
            automatically from S, L, and J if not specified.
        g (float): Lande g-factor. Calculated automatically as gS+gL if not
            specified.
        j2type (string): Specifies the way the j2 integral is included in the
            magnetic form factor. Must be either 'RE' for rare earth or 'TM' for
            transition metal. If 'RE', the coefficient on the j2 integral is
            gL/g; if 'TM', the coefficient is (g-2)/g. Default is 'RE'. Note
            that for the default values of S, L, and J, we will have gS=2,
            gL=0, and g=2, and there will be no difference in the form factor
            calculation for 'RE' or 'TM'.

    Returns:
        MagStructure object corresponding to the magnetic structure
            encoded in the MCIF file. Note that the position and spin arrays
            are not automatically populated when using this function, so
            MagStructure.makeAll() will likely need to be called afterward.
            Also note that variables such as ffparamkey, rmaxAtoms, S, L, J,
            etc are applied uniformly to all magnetic atoms in the unit cell,
            so this method cannot create MagSpecies objects for which these
            attributes are different.

    """
    # creates an empty MagStructure
    mstruc = MagStructure()    

    #Flags whether the structure is incommensurate
    incomm = False

    #Extracts the needed information from the mcif file
    tags = {}
    parser = SimpleParser(tags)
    tags = parser.ReadFile(mcif)

    #These are the parameters of the basic or unit magnetic cell
    #'a', 'b', and 'c' are in angstroms, while 'alpha', 'beta', and 'gamma' are in degrees
    a = tags['_cell_length_a'][0]
    b = tags['_cell_length_b'][0]
    c = tags['_cell_length_c'][0]
    alpha = tags['_cell_angle_alpha'][0]
    beta = tags['_cell_angle_beta'][0]
    gamma = tags['_cell_angle_gamma'][0]
    transform = tags['_parent_space_group.child_transform_Pp_abc'][0]

    #symbols = tags['_atom_site_type_symbol']
    symbols = tags['_atom_site_label']
    symbols0 = 1*symbols # starting list of atom symbols
    try:
        occs = tags['_atom_site_occupancy']
    except KeyError:
        occs = [1.0] * len(symbols)
    occs0 = 1*occs # starting list of occupancies
    scaled_pos = np.array([tags['_atom_site_fract_x'],
                           tags['_atom_site_fract_y'],
                           tags['_atom_site_fract_z']]).T
    scaled_pos = np.mod(scaled_pos,1.)

    mag_mom = np.zeros_like(scaled_pos)
    mag_idx = []

    if '_cell_wave_vector_x' in tags:
        #This is the branch for incommensurate structures
        incomm = True
        k = np.matrix.flatten(np.array([tags['_cell_wave_vector_x'],
                                        tags['_cell_wave_vector_y'],
                                        tags['_cell_wave_vector_z']]).T)

        symm_cent = tags['_space_group_symop_magn_ssg_centering.algebraic']
        symm_ops = tags['_space_group_symop_magn_ssg_operation.algebraic']

        #For incommensurate structures, mag_mom is the average magnetic moment of an atom. Currently,
        #this variable is unused for incommensurate structures
        four_cos = np.zeros_like(scaled_pos)
        four_sin = np.zeros_like(scaled_pos)
        for i, label in enumerate(tags['_atom_site_label']):
            if label in tags['_atom_site_moment.label']:
                mag_idx.append(i)
                mi = tags['_atom_site_moment.label'].index(label)
                mag_mom[i] = [tags['_atom_site_moment.crystalaxis_x'][mi],
                              tags['_atom_site_moment.crystalaxis_y'][mi],
                              tags['_atom_site_moment.crystalaxis_z'][mi]]

                four_cos[i] = [tags['_atom_site_moment_Fourier_param.cos'][3*mi],
                               tags['_atom_site_moment_Fourier_param.cos'][3*mi + 1],
                               tags['_atom_site_moment_Fourier_param.cos'][3*mi + 2]]

                four_sin[i] = [tags['_atom_site_moment_Fourier_param.sin'][3*mi],
                               tags['_atom_site_moment_Fourier_param.sin'][3*mi + 1],
                               tags['_atom_site_moment_Fourier_param.sin'][3*mi + 2]]
    else:
        #This is the branch for commensurate structures
        k = np.array([0,0,0])

        symm_cent = tags['_space_group_symop_magn_centering.xyz']
        symm_ops = tags['_space_group_symop_magn_operation.xyz']

        for i, label in enumerate(tags['_atom_site_label']):
            if label in tags['_atom_site_moment.label']:
                mag_idx.append(i)
                mi = tags['_atom_site_moment.label'].index(label)
                mag_mom[i] = [tags['_atom_site_moment.crystalaxis_x'][mi],
                              tags['_atom_site_moment.crystalaxis_y'][mi],
                              tags['_atom_site_moment.crystalaxis_z'][mi]]

    #Takes the magnetic information to reduced lattice coordinates
    LL = np.diag([1./a,1./b,1./c])
    mag_mom = dot(mag_mom,LL)
    if incomm:
        four_cos = dot(four_cos,LL)
        four_sin = dot(four_sin,LL)

    #Performs the symmetry operations to populate the rest of the unit cell
    all_pos = np.copy(scaled_pos)
    all_mom = np.copy(mag_mom)
    if incomm:
        all_cos = np.copy(four_cos)
        all_sin = np.copy(four_sin)

    tol = 1e-3
    for j, pos in enumerate(scaled_pos):
        pos_subset = np.array([pos])
        eq_site_counter = 1
        for cent in symm_cent:
            #Applies a centering operation
            if incomm:
                Rc, hc, r1c, tc, t4c, trc = parse_magn_ssg_operation(cent)
            else:
                Rc, tc, trc = parse_magn_operation(cent)
            cent_pos = np.mod(dot(Rc,pos) + tc,1.)

            for op in symm_ops:
                #Applies a symmetry operation
                if incomm:
                    R, h, r1, t, t4, tr = parse_magn_ssg_operation(op)
                else:
                    R, t, tr = parse_magn_operation(op)
                eq_site = dot(R,cent_pos) + t

                #Handles the cases where eq_site has coordinate values extremely close to 0 or 1
                for l, coord in enumerate(eq_site):
                    if abs(1 - coord) < tol:
                        eq_site[l] = 1.
                    if abs(coord) < tol:
                        eq_site[l] = 0.
                eq_site = np.mod(eq_site,1.)
                eq_site = np.round(eq_site, decimals=6)

                #Checks to make sure site hasn't already been occupied
                for l, pp in enumerate(pos_subset):
                    if np.allclose(eq_site,pp,atol=tol): # and the atom labels are the same (fix this)
                        break
                else:
                    #Adds the new site found with symmetry
                    if j in mag_idx:
                        mag_idx.append(len(symbols))
                    symbols.append(symbols0[j]+'.'+str(eq_site_counter))
                    occs.append(occs0[j])
                    eq_site_counter += 1
                    pos_subset = np.append(pos_subset,[eq_site],axis=0)
                    all_pos = np.append(all_pos,[eq_site],axis=0)

                    #Transforms the moment information
                    if incomm:
                        deltc = 2*pi*(t4c + dot(hc,pos))
                        delt = 2*pi*(t4 + dot(h,cent_pos))

                        cent_cos = trc*det(Rc)*(cos(deltc)*dot(Rc,four_cos[j]) - r1c*sin(deltc)*dot(Rc,four_sin[j]))
                        cent_sin = trc*det(Rc)*(sin(deltc)*dot(Rc,four_cos[j]) + r1c*cos(deltc)*dot(Rc,four_sin[j]))

                        new_cos = tr*det(R)*(cos(delt)*dot(R,cent_cos) - r1*sin(delt)*dot(R,cent_sin))
                        new_sin = tr*det(R)*(sin(delt)*dot(R,cent_cos) + r1*cos(delt)*dot(R,cent_sin))

                        all_cos = np.append(all_cos,[new_cos],axis=0)
                        all_sin = np.append(all_sin,[new_sin],axis=0)

                    new_mom = tr*trc*det(R)*det(Rc)*dot(R,dot(Rc,mag_mom[j]))   
                    all_mom = np.append(all_mom,[new_mom],axis=0)

    #Determines the basis vectors of the structure (same as the magnetic moments if commensurate)
    basis_vecs = np.empty(np.shape(all_pos),dtype=complex)
    if incomm:
        basis_vecs = (0.5*np.exp(-2*pi*1j*dot(all_pos,k)))[:,np.newaxis]*(all_cos + 1j*all_sin)
        #for i, pos in enumerate(all_pos):
            #basis_vecs[i] = 0.5*exp(-2*pi*1j*dot(k,pos))*(all_cos[i] + 1j*all_sin[i])
    else:
        basis_vecs = all_mom.astype(complex)
        #for i, pos in enumerate(all_pos):
            #basis_vecs[i] = all_mom[i].astype(complex)

    #Converts the basis vectors to Cartesian coordinates
    lat = Lattice(a,b,c,alpha,beta,gamma)
    basis_vecs = dot(basis_vecs,lat.base)
    all_mom = dot(all_mom,lat.base)

    #Creates a diffpy Structure object
    atoms = []
    for i, pos in enumerate(all_pos):
        atoms.append(Atom(atype=symbols[i],xyz=pos, occupancy=occs[i]))
    astruc = Structure(atoms=atoms)
    astruc.lattice = lat
    for atom in astruc: # remove extra numbers from atom names
        name = atom.element
        newname = re.findall(r'[a-zA-Z]+', name)[0]
        atom.element = newname


    #Creates a separate MagSpecies object for each magnetic atom in the unit cell
    for idx in mag_idx:
        new_mspec = MagSpecies(ffparamkey=ffparamkey, rmaxAtoms=rmaxAtoms,
                               S=S, L=L, J=J, gS=gS, gL=gL, g=g,
                               j2type=j2type)

        new_mspec.struc = astruc
        new_mspec.label = symbols[idx]

        new_mspec.strucIdxs = [idx]
        new_mspec.origin = all_pos[idx]
        new_mspec.occ = occs[idx]

        new_mspec.kvecs = np.array([k])
        new_mspec.basisvecs = np.array([basis_vecs[idx]])
        if incomm:
            new_mspec.kvecs = np.append(new_mspec.kvecs,[-k],axis=0)
            new_mspec.basisvecs = np.append(new_mspec.basisvecs,[np.conjugate(basis_vecs[idx])],axis=0)
        new_mspec.avgmom = all_mom[idx] if incomm else np.array([0, 0, 0])

        mstruc.loadSpecies(new_mspec)

    mstruc.transform = transform
    print('MagStructure creation from mcif file successful.')
    return mstruc
def create_magnetic_cell(astruc, transform)

Takes a diffpy.Structure object representing the atomic unit cell and a string containing information about the parent to child basis vector transformation. Returns a new diffpy.Structure object representing the magnetic unit cell.

Note: At the moment, the resulting magnetic structure won't actually have any magnetic moment information; it will just be a list of atoms and their positions in the unit cell. Work still needs to be done to figure out how to connect atoms in the magnetic cell with the correct moment information.

Expand source code
def create_magnetic_cell(astruc,transform):
    """
    Takes a diffpy.Structure object representing the atomic unit cell and a string containing information 
    about the parent to child basis vector transformation. Returns a new diffpy.Structure object
    representing the magnetic unit cell. 

    Note: At the moment, the resulting magnetic structure won't actually have any magnetic moment 
    information; it will just be a list of atoms and their positions in the unit cell. Work still needs to
    be done to figure out how to connect atoms in the magnetic cell with the correct moment information.
    """
    #Processes the string with the parent-child transformation info
    R, t = parse_parent_child_transform(transform)
    R_p = inv(R)
    t_p = dot(t,R_p)
    
    #Transforms the atomic cell basis to a magnetic cell basis using the given transform
    atom_cell = astruc.lattice.base
    mag_cell = dot(R,atom_cell)
    
    #Determines the dimensions of the supercell we want to work with to get the magnetic cell
    dim = [[floor(min(R[:,0]) + t[0]) - 1,ceil(max(R[:,0]) + t[0]) + 1],
           [floor(min(R[:,1]) + t[1]) - 1,ceil(max(R[:,1]) + t[1]) + 1],
           [floor(min(R[:,2]) + t[2]) - 1,ceil(max(R[:,2]) + t[2]) + 1]]
    
    sup_pos, sup_occup, sup_symb = supersize_me(astruc,dim)
    
    #Picks out the magnetic unit cell from the newly created atomic supercell
    scaled_pos = []
    occup = []
    symb = []
    
    tol = 1e-3
    for l, pos in enumerate(sup_pos):
        #Converts the position to fractional magnetic lattice coordinates
        new_pos = dot(pos,R_p) - t_p

        #Tidies up coordinates close enough to 0 or 1        
        for i, coord in enumerate(new_pos):
            if abs(1 - coord) < tol:
                new_pos[i] = 1.
            if abs(coord) < tol:
                new_pos[i] = 0.

        #Checks if the atom sits inside the magnetic unit cell
        if (0<=new_pos[0]<1) and (0<=new_pos[1]<1) and (0<=new_pos[2]<1):
            add_pos = True

            #Also checks for duplicates (which may be allowed based on occupancy)
            dupes, dupe_idxs = find_dupes(scaled_pos,new_pos,tol)
            if len(dupes) >= 1:
                tot_occup = sup_occup[l]
                for idx in dupe_idxs:
                    tot_occup += occup[idx]
                    if element(symb[idx]) == element(sup_symb[l]):
                        add_pos = False
                if (tot_occup - 1) > tol:
                    add_pos = False
            #for m, pp in enumerate(scaled_pos):
            #    if np.allclose(new_pos,pp,atol=tol):
            #        break
            #else:
            if add_pos:
                scaled_pos.append(new_pos)
                occup.append(sup_occup[l])
                symb.append(sup_symb[l])
            
    #Creates a new diffpy.Structure object representing the magnetic unit cell (without the moment information)
    atoms = []
    for l, pos in enumerate(scaled_pos):
        atoms.append(Atom(atype=symb[l],xyz=pos,occupancy=occup[l]))
    new_struc = Structure(atoms=atoms)
    new_struc.lattice = Lattice(base=mag_cell)
    
    return new_struc
def element(site_label)

Finds and returns the chemical symbol of an element based on its site label.

Expand source code
def element(site_label):
    """
    Finds and returns the chemical symbol of an element based on its site label. 
    """
    chem_symbol = site_label[0]
    
    if len(site_label) > 1:
        if site_label[1].isalpha():
            chem_symbol += site_label[1]
    
    return chem_symbol
def find_dupes(array, value, tol=0.001)

Given an m x n array, an n-vector, and a tolerance, determines which rows of the array match the n-vector within the desired (absolute) tolerance. Returns an array consisting of the matching rows and a list containing the respective indices of the rows as they were in the original array.

Expand source code
def find_dupes(array,value,tol=1e-3):
    """
    Given an m x n array, an n-vector, and a tolerance, determines which rows of the array match 
    the n-vector within the desired (absolute) tolerance. Returns an array consisting of the 
    matching rows and a list containing the respective indices of the rows as they were in the 
    original array.
    """
    dupes = np.array([])
    dupe_idxs = []
    
    for i, row in enumerate(array):
        if np.allclose(row,value,atol=tol):
            dupes = np.append(dupes,row,axis=0)
            dupe_idxs.append(i)
    
    return dupes, dupe_idxs
def order_by_pos(original, unordered)

This method accepts two arrays of the same size and outputs a third array also of that size. The third array is created by permuting the rows of the 'unordered' array in such a way that the point given by the ith row of the output array is closer (in terms of Euclidean distance) to the point given by the ith row vector of the 'original' array than to the point given by any other row of the 'original' array. The intended application of this method is to recover the ordering of an array of atomic positions in a magnetic unit cell after downsizing to an atomic unit cell and then upsizing once more.

In this method, we're assuming that each point in the 'unordered' array is closest to exactly one point in the original array. The motivation for this is that the rescaling of the atomic unit cell during PDF and mPDF fits should only cause small changes in atomic positions within the unit cell, so no atom should stray far enough from their original position to end up closer to a different atom.

Expand source code
def order_by_pos(original,unordered):
    """
    This method accepts two arrays of the same size and outputs a third array also of that size.
    The third array is created by permuting the rows of the 'unordered' array in such a way
    that the point given by the ith row of the output array is closer (in terms of Euclidean distance)
    to the point given by the ith row vector of the 'original' array than to the point given by any
    other row of the 'original' array. The intended application of this method is to recover the
    ordering of an array of atomic positions in a magnetic unit cell after downsizing to an atomic unit
    cell and then upsizing once more.
    
    In this method, we're assuming that each point in the 'unordered' array is closest to exactly one 
    point in the original array. The motivation for this is that the rescaling of the atomic unit cell
    during PDF and mPDF fits should only cause small changes in atomic positions within the unit cell, 
    so no atom should stray far enough from their original position to end up closer to a different
    atom.
    """
    #Checks if the input arrays have the same shape
    if np.shape(original)!=np.shape(unordered):
        print('Error: Input arrays must be of the same size.')
        return

    ordered = np.zeros_like(unordered)
    unordered_copy = np.copy(unordered)

    for i, pos_orig in enumerate(original):
        #For a given point in the 'original' array, finds the closest point in the 'unordered' array
        closest_pos = 0
        closest_dist = 1e6

        for j, pos_unord in enumerate(unordered_copy):
            curr_dist = norm(pos_orig - pos_unord)
            if curr_dist < closest_dist:
                closest_dist = curr_dist
                closest_pos = j

        #Adds the closest point in the 'unordered' array to the 'ordered' array in the correct position
        ordered[i] = unordered_copy[closest_pos]

        #Deletes the position from the 'unordered' array to speed up computation
        unordered_copy = np.delete(unordered_copy,closest_pos,0)

    return ordered
def parse_magn_operation(op)

Parses a string containg a space group symmetry operation in standard notation. Returns the rotation and translation parts of the operation (R and t) and the time reversal sign (p).

Expand source code
def parse_magn_operation(op):
    """
    Parses a string containg a space group symmetry operation in standard notation.
    Returns the rotation and translation parts of the operation (R and t) and the 
    time reversal sign (p).
    """
    r1,r2,r3,p=op.split(',')
    R = np.zeros([3,3]) # rotations
    t = np.zeros([3])   # translations
    
    # compile traslation matrix
    t[0] = convert_to_float(r1[-4:]) if '/' in r1 else 0.
    t[1] = convert_to_float(r2[-4:]) if '/' in r2 else 0.
    t[2] = convert_to_float(r3[-4:]) if '/' in r3 else 0.
    
    # compile rotation matrix
    for i, line in enumerate([r1,r2,r3]):
        x=y=z=0.
        m = re.search(r'-?\d?x',line)
        if m:
            xs=m.group()
            x = 1. if ('x' in xs and not '-' in xs) else -1.
            m = re.search(r'\d',xs)
            if m:
                x *= float(m.group())

        m = re.search(r'-?\d?y',line)
        if m:
            ys=m.group()
            y = 1. if ('y' in ys and not '-' in ys) else -1.
            m = re.search(r'\d',ys)
            if m:
                y *= float(m.group())
                
        m = re.search(r'-?\d?z',line)
        if m:
            zs=m.group()
            z = 1. if ('z' in zs and not '-' in zs) else -1.
            m = re.search(r'\d',zs)
            if m:
                z *= float(m.group())                            
            
        R[i,0], R[i,1], R[i,2] = [x,y,z]
        
    return (R,t,float(p))
def parse_magn_ssg_operation(op)

Parses a string containing a superspace group symmetry operation in standard notation. Returns the rotation and translation part of the associated space group operation (R and t), the average structure reciprocal lattice vector (h), superspace phase information (r1 and t4), and the time reversal sign (p).

Expand source code
def parse_magn_ssg_operation(op):
    """
    Parses a string containing a superspace group symmetry operation in standard
    notation. Returns the rotation and translation part of the associated space
    group operation (R and t), the average structure reciprocal lattice vector (h), 
    superspace phase information (r1 and t4), and the time reversal sign (p).
    """
    r1,r2,r3,r4,p=op.split(',')
    M = np.zeros([4,4])
    t = np.zeros([3])
    h = np.zeros([3])
    
    #Compiles translation information
    t[0] = convert_to_float(r1[-4:]) if '/' in r1 else 0.
    t[1] = convert_to_float(r2[-4:]) if '/' in r2 else 0.
    t[2] = convert_to_float(r3[-4:]) if '/' in r3 else 0.
    t4 = convert_to_float(r4[-4:]) if '/' in r4 else 0.
    
    #Compiles rotation matrix
    for i, line in enumerate([r1,r2,r3,r4]):
        x1=x2=x3=x4=0.
        m = re.search(r'-?\d?x1',line)
        if m:
            xs=m.group()
            x1 = 1. if ('x1' in xs and not '-' in xs) else -1.
            xs = xs.replace('x1','')
            m = re.search(r'\d',xs)
            if m:
                x1 *= float(m.group())

        m = re.search(r'-?\d?x2',line)
        if m:
            ys=m.group()
            x2 = 1. if ('x2' in ys and not '-' in ys) else -1.
            ys = ys.replace('x2','')
            m = re.search(r'\d',ys)
            if m:
                x2 *= float(m.group())
                
        m = re.search(r'-?\d?x3',line)
        if m:
            zs=m.group()
            x3 = 1. if ('x3' in zs and not '-' in zs) else -1.
            zs = zs.replace('x3','')
            m = re.search(r'\d',zs)
            if m:
                x3 *= float(m.group())
                
        m = re.search(r'-?\d?x4',line)
        if m:
            ts = m.group()
            x4 = 1. if ('x4' in ts and not '-' in ts) else -1.
            ts = ts.replace('x4','')
            m = re.search(r'\d',ts)
            if m:
                x4 *= float(m.group())
            
        M[i,0], M[i,1], M[i,2], M[i,3] = [x1,x2,x3,x4]
    
    R = M[:3,:3]
    h = M[3,:3]
    r1 = M[3,3]
        
    return (R,h,r1,t,t4,float(p))
def parse_parent_child_transform(transform)

Takes in a string containing information about the transformation from the parent (paramagnetic) basis to the child (magnetic) basis and returns the "rotation" (not necessarily a pure rotation) matrix and translation vector associated with the transformation.

Expand source code
def parse_parent_child_transform(transform):
    """
    Takes in a string containing information about the transformation from the parent
    (paramagnetic) basis to the child (magnetic) basis and returns the "rotation" (not
    necessarily a pure rotation) matrix and translation vector associated with the 
    transformation.
    """
    #Splits up the string into related pieces of information
    rotation, translation = transform.split(';')
    r1, r2, r3 = rotation.split(',')
    t1, t2, t3 = translation.split(',')

    #Initializes the rotation matrix and translation vector
    R = np.zeros((3,3))
    t = np.zeros(3)
    
    #Compiles the translation vector
    t[0] = convert_to_float(t1) if '/' in t1 else t1
    t[1] = convert_to_float(t2) if '/' in t2 else t2
    t[2] = convert_to_float(t3) if '/' in t3 else t3
    
    #Compiles the rotation matrix
    for i, line in enumerate([r1,r2,r3]):
        x = y = z = 0
        
        m = re.search(r'-?\d?/?\d?a',line)
        if m:
            xs = m.group()
            x = 1. if ('a' in xs and not '-' in xs) else -1.
            m = re.search(r'\d/?\d?',xs)
            if m:
                if '/' in m.group():
                    x *= convert_to_float(m.group())
                else:
                    x *= float(m.group())
        
        m = re.search(r'-?\d?/?\d?b',line)
        if m:
            ys = m.group()
            y = 1. if ('b' in ys and not '-' in ys) else -1.
            m = re.search(r'\d/?\d?',ys)
            if m:
                if '/' in m.group():
                    y *= convert_to_float(m.group())
                else:
                    y *= float(m.group())
                    
        m = re.search(r'-?\d?/?\d?c',line)
        if m:
            zs = m.group()
            z = 1. if ('c' in zs and not '-' in zs) else -1.
            m = re.search(r'\d/?\d?',zs)
            if m:
                if '/' in m.group():
                    z *= convert_to_float(m.group())
                else:
                    z *= float(m.group())
        
        R[i,0] = x
        R[i,1] = y
        R[i,2] = z
        
    return (R,t)
def supersize_me(struc, dim)

Accepts a diffpy.Structure object and a list of pairs specifying the beginning and ending cell along each crystal axis and returns two arrays containing the fractional coordinates of the atoms in the supercell relative to the original unit cell and their chemical symbols, respectively.

Expand source code
def supersize_me(struc,dim):
    """
    Accepts a diffpy.Structure object and a list of pairs specifying the beginning and ending
    cell along each crystal axis and returns two arrays containing the fractional coordinates of the 
    atoms in the supercell relative to the original unit cell and their chemical symbols, respectively.
    """
    #Extracts the necessary information from the MagStructure object
    old_pos = struc.xyz
    old_occup = struc.occupancy
    old_symb = []
    for i in range(len(struc.element)):
        old_symb.append(struc.element[i])
    
    #Appends the desired additional cells to the original unit cell
    pos_list = []
    occup_list = []
    symb = []
    
    for k in range(dim[2][0],dim[2][1]):
        for j in range(dim[1][0],dim[1][1]):
            for i in range(dim[0][0],dim[0][1]):
                for l, pos in enumerate(old_pos):
                    pos_list.append(pos + np.array([i,j,k]))
                    occup_list.append(old_occup[l])
                    symb.append(old_symb[l])
    
    pos = np.array(pos_list)
    occup = np.array(occup_list)
    
    return (pos,occup,symb)