Module isopytools.iso2diffpy

Tools for adapting ISODISTORT outputs to DiffPy.

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

"""Tools for adapting ISODISTORT outputs to DiffPy.
"""

import re
import itertools
from collections import OrderedDict
import diffpy.structure as dps
import diffpy.structure.symmetryutilities as su
import diffpy.structure.spacegroups as sg
import numpy as np

_pf = r'[+-]?\d[^; \n\t]*'

def get_xyz(atomName,iso):
    """Extract the xyz position of a given atom from mode amplitudes.

    Args:
        atomName (string): label of the atom for which the coordinates are
            desired.
        iso: instance of IsoInfo class.

    Returns: tuple with the x, y, and z fractional coordinates.
    """
    return tuple(iso.positions[atomName+'_'+c][1] for c in 'xyz')

def get_shifts(iso):
    """Get fractional shifts of symmetry-unique atoms from mode amplitudes.

    Args:
        iso: instance of IsoInfo class.

    Returns:
        shiftDict: OrderedDict with fractional shifts of each atom.
    """
    shiftDict = OrderedDict()
    for key in iso.deltas.keys():
        equation = iso.deltas[key][0]
        pieces = equation.split()
        signs = pieces[::2]
        terms = pieces[1::2]
        coefficients = []
        modenums = []
        for term in terms:
            coefficient, modenum = term.split('*')
            coefficients.append(coefficient)
            modenums.append(modenum)
        modenums2 = []
        for modenum in modenums:
            modenum2 = "iso.modepars['"+modenum+"']"
            modenums2.append(modenum2)
        newequation = ''
        for idx, sign in enumerate(signs):
            newequation += sign+coefficients[idx]+'*'+modenums2[idx]
        shiftDict[key] = eval(newequation)
    return shiftDict

def get_positions(iso, returnDict=True):
    """Get fractional positions of symmetry-unique atoms from mode amplitudes.
    Also updates the iso.positions dictionary.

    Args:
        iso: instance of IsoInfo class.
        returnDict: If True, this function returns a dictionary with a separate
            key for each component of each atom. If False, two items are
            returned: a list containing the labels of each unique site, and an
            array with the positions of each atom.
            

    Returns:
        if returnDict:
        posDict: OrderedDict with positions of each atom
        if not returnDict:
        names: List of the labels of each atom
        coords: numpy array of the coordinates of each atom (same order as names)
    """
    if returnDict:
        posDict = OrderedDict()
        shiftDict = get_shifts(iso)
        for key in iso.positions.keys():
            equation = iso.positions[key][0]
            k = key[:-1]+'d'+key[-1]
            pParams = re.findall(k,equation)
            for pParam in pParams:
                equation = equation.replace(pParam,"shiftDict['"+pParam+"']")
            posDict[key] = eval(equation)
            temp = iso.positions[key]
            iso.positions[key] = (temp[0], posDict[key])
        return posDict
    else:
        coords = []
        names = []
        for idx, key in enumerate(iso.positions):
            coords.append(iso.positions[key][1])
            if idx % 3 == 0:
                names.append(key[:-2])
        coords = np.array(coords)
        coords = np.reshape(coords, (len(names), 3))
        return names, coords

def update_positions(strucObj, iso):
    """Update fractional positions of diffpy structure object from mode amps.

    Args:
        strucObj: the diffpy structure object to be updated.
        iso: instance of IsoInfo class.

    Returns: Nothing. Just updates the strucObj.
    """
    posDict = get_positions(iso)
    for idx, key in enumerate(posDict.keys()):
        k = key[:-2]
        XYZ = key[-1]
        string = 'strucObj['+str(idx//3)+'].'+ XYZ +' = posDict[key]'
        exec(string)

def set_amps(iso, ampArray):
    """Set the symmetry mode amplitudes to desired values.

    Args:
        iso: instance of IsoInfo class that will be updated with desired amps
        ampArray: array or list of amplitudes, in the same order as
            iso.modepars.

    Returns: Nothing. Just updates iso.
    """
    for idx, key in enumerate(iso.modepars.keys()):
        iso.modepars[key] = ampArray[idx]

def get_amps(iso):
    """Get the symmetry mode amplitudes from current iso instance.

    Args:
        iso: instance of IsoInfo class from which amplitudes will be extracted.
        ampArray: array or list of amplitudes, in the same order as
            iso.modepars.

    Returns: list containing the mode amplitudes.
    """
    amps = []
    for idx, key in enumerate(iso.modepars.keys()):
        amps.append(iso.modepars[key])
    return amps

def build_struc(iso):
    """Create a PDFfitStructure instance from the IsoInfo instance.
    
    Args:
        iso: instance of IsoInfo class to be converted to a diffpy structure.

    Returns:
        struc: diffpy structure object corresponding to info in IsoInfo.
    """
    # create lattice
    a, b, c, alpha, beta, gamma = iso.abcdegabg 
    lat = dps.Lattice(a, b, c, alpha, beta, gamma)
    
    sgobj = sg.GetSpaceGroup(iso.spacegroupnum) # space group object
    
    # create the atoms
    atoms = []
    for key in list(iso.positions.keys())[::3]: # grab every third entry because x, y, z are listed for each atom
        letters = re.findall(r"[a-zA-Z]",key)
        elem = ''.join(letters[:-1])
        expanded_pos = su.expandPosition(sgobj, get_xyz(key[:-2],iso))[0]
        for pos in expanded_pos:
            atoms.append(dps.Atom(atype=elem, xyz=pos, anisotropy=True))

    struc = dps.Structure(atoms=atoms)
    struc.lattice = lat
    return struc

class IsoInfo:
    """Container for structural information contained in ISODISTORT output file.

    This class takes a TOPAS-format output file from ISODISTORT,
    extracts the structural/symmetry information, and prepares it for use by
    other DiffPy-based functions.
    
    Args:
        filename (string): name of the .str ISODISTORT file.
    
    Attributes:
        abcdegabg: 6-tuple containing a, b, c, alpha, beta, gamma (angles in deg)
        abcradabg: 6-tuple containing a, b, c, alpha, beta, gamma (angles in rad)
        modedefs: dictionary pairing mode labels with group-theoretical
            definitions.
        modepars: dictionary pairing mode labels with mode amplitudes.
        deltas: dictionary pairing mode labels with fractional changes in position
            as a function of mode amplitudes.
        positions: dictionary pairing atomic coordinate labels with their values.
        spacegroup: string giving the spacegroup symbol of the child structure.
        spacegroupnum: integer space group number
        
    """
    def __init__(self, filename):
        from math import radians
        fp = open(filename, 'r')
        lines = fp.readlines()
        fp.close()
        #with open(filename) as fp:
        #    lines = fp.readlines()
        self.abcdegabg = self._parse_abcabg(lines)
        radabg = tuple(radians(x) for x in self.abcdegabg[3:])
        self.abcradabg = self.abcdegabg[:3] + radabg
        self.modepars = self._parse_modepars(lines)
        self.deltas = self._parse_deltas(lines)
        self.positions = self._parse_positions(lines)
        self.modedefs = self._parse_modedefs(lines)
        # find spacegroup
        sgline = next((i for i, line in enumerate(lines)
                       if 'space_group' in line))
        self.spacegroup = lines[sgline - 1].strip().strip("'\"")
        self.spacegroupnum = int(lines[sgline].strip().split()[1])
        return

    @staticmethod
    def _parse_abcabg(lines):
        """Extract lattice constants.
        """
        mx = re.compile(fr'^\s*([abc]|al|be|ga)\s+({_pf})')
        words = (mx.search(s).group(2) for s in lines if mx.search(s))
        rv = tuple(float(x) for x in itertools.islice(words, 6))
        return rv


    @staticmethod
    def _parse_modepars(lines):
        """Parse the 'mode definitions' block to get parameter values.
        """
        mx = re.compile(fr'^\s*prm *!?(a\d+) +({_pf})')
        words = (mx.search(s).groups() for s in lines if mx.search(s))
        rv = OrderedDict((n, float(v)) for n, v in words)
        return rv

    @staticmethod
    def _parse_modedefs(lines):
        """Parse the 'mode definitions' block to get mode names.
        """
        modes_start = next((i for i, line in enumerate(lines) if 'mode definitions' in line))
        for idx, line in enumerate(lines[modes_start:]):
            if "'}}}" in line:
                modes_end = modes_start + idx
                break
        chunk = lines[modes_start+1 : modes_end-1]
        rv = OrderedDict()
        for line in chunk:
            segments = line.split()
            label = segments[1][1:]
            name = segments[-2][1:] + ' ' + segments[-1]
            rv[label] = name
        return rv    

    @staticmethod
    def _parse_deltas(lines):
        """
        Parse the "mode-amplitude to delta transformation" block.
        """
        mx = re.compile(fr'^\s*prm *(\w+_d[xyz]) += +(\S[^;]*);: *({_pf})')
        words = (mx.search(s).groups() for s in lines if mx.search(s))
        rv = OrderedDict((w[0], (w[1], float(w[2]))) for w in words)
        return rv


    @staticmethod
    def _parse_positions(lines):
        """Parse the 'distorted parameters' block.
        """
        mx = re.compile(fr'^\s*prm *!?(\w+_[xyz]) += +(\S[^;]*);: *({_pf})')
        words = (mx.search(s).groups() for s in lines if mx.search(s))
        rv = OrderedDict((w[0], (w[1], float(w[2]))) for w in words)
        return rv

# end of class IsoInfo

Functions

def build_struc(iso)

Create a PDFfitStructure instance from the IsoInfo instance.

Args

iso
instance of IsoInfo class to be converted to a diffpy structure.

Returns

struc
diffpy structure object corresponding to info in IsoInfo.
Expand source code
def build_struc(iso):
    """Create a PDFfitStructure instance from the IsoInfo instance.
    
    Args:
        iso: instance of IsoInfo class to be converted to a diffpy structure.

    Returns:
        struc: diffpy structure object corresponding to info in IsoInfo.
    """
    # create lattice
    a, b, c, alpha, beta, gamma = iso.abcdegabg 
    lat = dps.Lattice(a, b, c, alpha, beta, gamma)
    
    sgobj = sg.GetSpaceGroup(iso.spacegroupnum) # space group object
    
    # create the atoms
    atoms = []
    for key in list(iso.positions.keys())[::3]: # grab every third entry because x, y, z are listed for each atom
        letters = re.findall(r"[a-zA-Z]",key)
        elem = ''.join(letters[:-1])
        expanded_pos = su.expandPosition(sgobj, get_xyz(key[:-2],iso))[0]
        for pos in expanded_pos:
            atoms.append(dps.Atom(atype=elem, xyz=pos, anisotropy=True))

    struc = dps.Structure(atoms=atoms)
    struc.lattice = lat
    return struc
def get_amps(iso)

Get the symmetry mode amplitudes from current iso instance.

Args

iso
instance of IsoInfo class from which amplitudes will be extracted.
ampArray
array or list of amplitudes, in the same order as iso.modepars.

Returns: list containing the mode amplitudes.

Expand source code
def get_amps(iso):
    """Get the symmetry mode amplitudes from current iso instance.

    Args:
        iso: instance of IsoInfo class from which amplitudes will be extracted.
        ampArray: array or list of amplitudes, in the same order as
            iso.modepars.

    Returns: list containing the mode amplitudes.
    """
    amps = []
    for idx, key in enumerate(iso.modepars.keys()):
        amps.append(iso.modepars[key])
    return amps
def get_positions(iso, returnDict=True)

Get fractional positions of symmetry-unique atoms from mode amplitudes. Also updates the iso.positions dictionary.

Args

iso
instance of IsoInfo class.
returnDict
If True, this function returns a dictionary with a separate key for each component of each atom. If False, two items are returned: a list containing the labels of each unique site, and an array with the positions of each atom.

Returns

if returnDict:
posDict
OrderedDict with positions of each atom
if not returnDict:
names
List of the labels of each atom
coords
numpy array of the coordinates of each atom (same order as names)
Expand source code
def get_positions(iso, returnDict=True):
    """Get fractional positions of symmetry-unique atoms from mode amplitudes.
    Also updates the iso.positions dictionary.

    Args:
        iso: instance of IsoInfo class.
        returnDict: If True, this function returns a dictionary with a separate
            key for each component of each atom. If False, two items are
            returned: a list containing the labels of each unique site, and an
            array with the positions of each atom.
            

    Returns:
        if returnDict:
        posDict: OrderedDict with positions of each atom
        if not returnDict:
        names: List of the labels of each atom
        coords: numpy array of the coordinates of each atom (same order as names)
    """
    if returnDict:
        posDict = OrderedDict()
        shiftDict = get_shifts(iso)
        for key in iso.positions.keys():
            equation = iso.positions[key][0]
            k = key[:-1]+'d'+key[-1]
            pParams = re.findall(k,equation)
            for pParam in pParams:
                equation = equation.replace(pParam,"shiftDict['"+pParam+"']")
            posDict[key] = eval(equation)
            temp = iso.positions[key]
            iso.positions[key] = (temp[0], posDict[key])
        return posDict
    else:
        coords = []
        names = []
        for idx, key in enumerate(iso.positions):
            coords.append(iso.positions[key][1])
            if idx % 3 == 0:
                names.append(key[:-2])
        coords = np.array(coords)
        coords = np.reshape(coords, (len(names), 3))
        return names, coords
def get_shifts(iso)

Get fractional shifts of symmetry-unique atoms from mode amplitudes.

Args

iso
instance of IsoInfo class.

Returns

shiftDict
OrderedDict with fractional shifts of each atom.
Expand source code
def get_shifts(iso):
    """Get fractional shifts of symmetry-unique atoms from mode amplitudes.

    Args:
        iso: instance of IsoInfo class.

    Returns:
        shiftDict: OrderedDict with fractional shifts of each atom.
    """
    shiftDict = OrderedDict()
    for key in iso.deltas.keys():
        equation = iso.deltas[key][0]
        pieces = equation.split()
        signs = pieces[::2]
        terms = pieces[1::2]
        coefficients = []
        modenums = []
        for term in terms:
            coefficient, modenum = term.split('*')
            coefficients.append(coefficient)
            modenums.append(modenum)
        modenums2 = []
        for modenum in modenums:
            modenum2 = "iso.modepars['"+modenum+"']"
            modenums2.append(modenum2)
        newequation = ''
        for idx, sign in enumerate(signs):
            newequation += sign+coefficients[idx]+'*'+modenums2[idx]
        shiftDict[key] = eval(newequation)
    return shiftDict
def get_xyz(atomName, iso)

Extract the xyz position of a given atom from mode amplitudes.

Args

atomName : string
label of the atom for which the coordinates are desired.
iso
instance of IsoInfo class.

Returns: tuple with the x, y, and z fractional coordinates.

Expand source code
def get_xyz(atomName,iso):
    """Extract the xyz position of a given atom from mode amplitudes.

    Args:
        atomName (string): label of the atom for which the coordinates are
            desired.
        iso: instance of IsoInfo class.

    Returns: tuple with the x, y, and z fractional coordinates.
    """
    return tuple(iso.positions[atomName+'_'+c][1] for c in 'xyz')
def set_amps(iso, ampArray)

Set the symmetry mode amplitudes to desired values.

Args

iso
instance of IsoInfo class that will be updated with desired amps
ampArray
array or list of amplitudes, in the same order as iso.modepars.

Returns: Nothing. Just updates iso.

Expand source code
def set_amps(iso, ampArray):
    """Set the symmetry mode amplitudes to desired values.

    Args:
        iso: instance of IsoInfo class that will be updated with desired amps
        ampArray: array or list of amplitudes, in the same order as
            iso.modepars.

    Returns: Nothing. Just updates iso.
    """
    for idx, key in enumerate(iso.modepars.keys()):
        iso.modepars[key] = ampArray[idx]
def update_positions(strucObj, iso)

Update fractional positions of diffpy structure object from mode amps.

Args

strucObj
the diffpy structure object to be updated.
iso
instance of IsoInfo class.

Returns: Nothing. Just updates the strucObj.

Expand source code
def update_positions(strucObj, iso):
    """Update fractional positions of diffpy structure object from mode amps.

    Args:
        strucObj: the diffpy structure object to be updated.
        iso: instance of IsoInfo class.

    Returns: Nothing. Just updates the strucObj.
    """
    posDict = get_positions(iso)
    for idx, key in enumerate(posDict.keys()):
        k = key[:-2]
        XYZ = key[-1]
        string = 'strucObj['+str(idx//3)+'].'+ XYZ +' = posDict[key]'
        exec(string)

Classes

class IsoInfo (filename)

Container for structural information contained in ISODISTORT output file.

This class takes a TOPAS-format output file from ISODISTORT, extracts the structural/symmetry information, and prepares it for use by other DiffPy-based functions.

Args

filename : string
name of the .str ISODISTORT file.

Attributes

abcdegabg
6-tuple containing a, b, c, alpha, beta, gamma (angles in deg)
abcradabg
6-tuple containing a, b, c, alpha, beta, gamma (angles in rad)
modedefs
dictionary pairing mode labels with group-theoretical definitions.
modepars
dictionary pairing mode labels with mode amplitudes.
deltas
dictionary pairing mode labels with fractional changes in position as a function of mode amplitudes.
positions
dictionary pairing atomic coordinate labels with their values.
spacegroup
string giving the spacegroup symbol of the child structure.
spacegroupnum
integer space group number
Expand source code
class IsoInfo:
    """Container for structural information contained in ISODISTORT output file.

    This class takes a TOPAS-format output file from ISODISTORT,
    extracts the structural/symmetry information, and prepares it for use by
    other DiffPy-based functions.
    
    Args:
        filename (string): name of the .str ISODISTORT file.
    
    Attributes:
        abcdegabg: 6-tuple containing a, b, c, alpha, beta, gamma (angles in deg)
        abcradabg: 6-tuple containing a, b, c, alpha, beta, gamma (angles in rad)
        modedefs: dictionary pairing mode labels with group-theoretical
            definitions.
        modepars: dictionary pairing mode labels with mode amplitudes.
        deltas: dictionary pairing mode labels with fractional changes in position
            as a function of mode amplitudes.
        positions: dictionary pairing atomic coordinate labels with their values.
        spacegroup: string giving the spacegroup symbol of the child structure.
        spacegroupnum: integer space group number
        
    """
    def __init__(self, filename):
        from math import radians
        fp = open(filename, 'r')
        lines = fp.readlines()
        fp.close()
        #with open(filename) as fp:
        #    lines = fp.readlines()
        self.abcdegabg = self._parse_abcabg(lines)
        radabg = tuple(radians(x) for x in self.abcdegabg[3:])
        self.abcradabg = self.abcdegabg[:3] + radabg
        self.modepars = self._parse_modepars(lines)
        self.deltas = self._parse_deltas(lines)
        self.positions = self._parse_positions(lines)
        self.modedefs = self._parse_modedefs(lines)
        # find spacegroup
        sgline = next((i for i, line in enumerate(lines)
                       if 'space_group' in line))
        self.spacegroup = lines[sgline - 1].strip().strip("'\"")
        self.spacegroupnum = int(lines[sgline].strip().split()[1])
        return

    @staticmethod
    def _parse_abcabg(lines):
        """Extract lattice constants.
        """
        mx = re.compile(fr'^\s*([abc]|al|be|ga)\s+({_pf})')
        words = (mx.search(s).group(2) for s in lines if mx.search(s))
        rv = tuple(float(x) for x in itertools.islice(words, 6))
        return rv


    @staticmethod
    def _parse_modepars(lines):
        """Parse the 'mode definitions' block to get parameter values.
        """
        mx = re.compile(fr'^\s*prm *!?(a\d+) +({_pf})')
        words = (mx.search(s).groups() for s in lines if mx.search(s))
        rv = OrderedDict((n, float(v)) for n, v in words)
        return rv

    @staticmethod
    def _parse_modedefs(lines):
        """Parse the 'mode definitions' block to get mode names.
        """
        modes_start = next((i for i, line in enumerate(lines) if 'mode definitions' in line))
        for idx, line in enumerate(lines[modes_start:]):
            if "'}}}" in line:
                modes_end = modes_start + idx
                break
        chunk = lines[modes_start+1 : modes_end-1]
        rv = OrderedDict()
        for line in chunk:
            segments = line.split()
            label = segments[1][1:]
            name = segments[-2][1:] + ' ' + segments[-1]
            rv[label] = name
        return rv    

    @staticmethod
    def _parse_deltas(lines):
        """
        Parse the "mode-amplitude to delta transformation" block.
        """
        mx = re.compile(fr'^\s*prm *(\w+_d[xyz]) += +(\S[^;]*);: *({_pf})')
        words = (mx.search(s).groups() for s in lines if mx.search(s))
        rv = OrderedDict((w[0], (w[1], float(w[2]))) for w in words)
        return rv


    @staticmethod
    def _parse_positions(lines):
        """Parse the 'distorted parameters' block.
        """
        mx = re.compile(fr'^\s*prm *!?(\w+_[xyz]) += +(\S[^;]*);: *({_pf})')
        words = (mx.search(s).groups() for s in lines if mx.search(s))
        rv = OrderedDict((w[0], (w[1], float(w[2]))) for w in words)
        return rv