Source code for crystal

#!/usr/bin/env python
# -*- coding=utf-8 -*-

""" The aim of the crystal module is to be able to easily manipulate all lattice 
parameters of a crystal and do some basic operation on atomic coordinate such as :

    * Compute lattice vectors, volume
    * Coordinates transformation
    * input or output in VASP or CRYSTAL format

Today, symmetry is not considered. See python ASE for symetry support.

Requirements :
    numpy module is used internally in several functions or methods.

WARNING : 
    please, paid attention to angle unit. All angles are in degrees, they are
    internally converted to radian when needed.


In the following we use a Cu2O crystal as an example. 

    >>> import crystal
    >>> Cu2O = crystal.fromPOSCAR("POSCAR")
    --------------------------------------------------
     lattice parameters : Cu2O Bulk
    --------------------------------------------------
     a     =    4.26000
     b     =    4.28000
     c     =    4.27000
     alpha =     90.000
     beta  =     90.000
     gamma =     90.000
    --------------------------------------------------

""" 

import doctest
import copy
import array

import numpy as np
from numpy import sqrt, arccos, fabs, pi, cos, sin

__author__ = "Germain Vallverdu <germain.vallverdu@univ-pau.fr>"
__date__ = "Mardi 16 Mars 2012"
__licence__ = "GPL"

# -----------------------------------------------------------------------------
#  TODO
# -----------------------------------------------------------------------------
#
# toXYZ
#
# -----------------------------------------------------------------------------

bravais = [ "triclinic", "monoclinic", "orthorhombic", "tetragonal",
            "rhombohedral", "hexagonal", "cubic"] 

# central cube and 26 neighbors
trans = np.array([[0. , 0., 0.],
                  [ 1., 0., 0.],
                  [ 1., 1., 0.],
                  [ 1.,-1., 0.],
                  [ 1., 0., 1.],
                  [ 1., 0.,-1.],
                  [ 1., 1., 1.],
                  [ 1.,-1., 1.],
                  [ 1., 1.,-1.],
                  [ 1.,-1.,-1.],
                  [-1., 0., 0.],
                  [-1., 1., 0.],
                  [-1.,-1., 0.],
                  [-1., 0., 1.],
                  [-1., 0.,-1.],
                  [-1., 1., 1.],
                  [-1.,-1., 1.],
                  [-1., 1.,-1.], 
                  [-1.,-1.,-1.],
                  [ 0., 1., 0.],
                  [ 0.,-1., 0.],
                  [ 0., 0., 1.],
                  [ 0., 0.,-1.],
                  [ 0., 1., 1.],
                  [ 0.,-1., 1.],
                  [ 0., 1.,-1.],
                  [ 0.,-1.,-1.]])

[docs]class Crystal(object): """ class *Crystal* allows to describe and do operations on a crystal lattice :param float a: lattice parameter a. :param float b: lattice parameter b. :param float c: lattice parameter c. :param float alpha: lattice parameter alpha. :param float beta: lattice parameter beta. :param float gamma: lattice parameter gamma. :param string name: crystal name. :param string lattice: bravais lattice name. :param list veca: lattice vector a (length = 3). :param list vecb: lattice vector b (length = 3). :param list vecc: lattice vector c (length = 3). :param integer Z: number of unti formula If you give the bravais lattice name you may not set all lattice parameters : >>> cry = crystal.Crystal(a = 3., lattice = "cubic") >>> print(cry) -------------------------------------------------- lattice parameters : -------------------------------------------------- a = 3.00000 b = 3.00000 c = 3.00000 alpha = 90.000 beta = 90.000 gamma = 90.000 -------------------------------------------------- If you give only the bravais lattice, the prompt will ask you parameters needed according to the bravais lattice. >>> cry = crystal.Crystal(lattice = "hexagonal") a = 3. c = 7. >>> print(cry) -------------------------------------------------- lattice parameters : -------------------------------------------------- a = 3.00000 b = 3.00000 c = 7.00000 alpha = 90.000 beta = 90.000 gamma = 120.000 -------------------------------------------------- You can give directly lattice vectors. >>> cry = crystal.Crystal(veca = [2., 0., 0.], vecb = [-1., sqrt(3), 0.], vecc = [0., 0., 4.]) >>> cry.lattice 'hexagonal' >>> print(cry) -------------------------------------------------- lattice parameters : -------------------------------------------------- a = 2.00000 b = 2.00000 c = 4.00000 alpha = 90.000 beta = 90.000 gamma = 120.000 -------------------------------------------------- You cannot set lattice parameters **and** lattice vectors at the same time. Only one of them can be set when you create a crystal object in order to ensure consistency between lattice vectors, lattice parameters and bravais lattice name. A method is available in order to print lattice vectors in a cartesian frame. >>> cry = crystal.Crystal(lattice = "hexagonal") a = 3. c = 7. >>> cry.printLatticeVectors() -------------------------------------------------- Lattice vectors in cartesian frame : lattice is hexagonal -------------------------------------------------- a = 3.0000000000 0.0000000000 0.0000000000 b = -1.5000000000 2.5980762114 0.0000000000 c = 0.0000000000 0.0000000000 7.0000000000 -------------------------------------------------- """ # ------------------------------------------------------------------------- # Constructor # ------------------------------------------------------------------------- def __init__(self, *largs, **args): """ constructor """ # # set all attribute to None or False # self._veca = None self._vecb = None self._vecc = None """ Lattice vectors :math:`\vec{a}`, :math:`\vec{b}` and :math:`\vec{c}` in a cartesian frame where :math:`\vec{a}` is along x axes and :math:`\vec{b}` is in the (x,y) plane. >>> Cu2O.veca, Cu2O.vecb, Cu2O.vecc ([4.26, 0.0, 0.0], [0.0, 4.28, 0.0], [0.0, 0.0, 4.27]) """ self._a = None self._b = None self._c = None """ Lattice parameters **a**, **b** and **c**, lengths of the vectors :math:`\vec{a}`, :math:`\vec{b}` and :math:`\vec{c}`. >>> Cu2O.a, Cu2O.b, Cu2O.c (4.2599999999999998, 4.2800000000000002, 4.2699999999999996) **c**, third vector of the lattice """ self._alpha = None self._beta = None self._gamma = None """ Lattice angles :math:`\alpha` , :math:`\beta` and :math:`\gamma`. All angles must be given in degrees and are return in degrees. They are internally convert in radian when needed. >>> Cu2O.alpha, Cu2O.beta, Cu2O.gamma (90.0, 90.0, 90.0) """ self._volume = None """ Volume of the unit cell """ self._lattice = None """ Name of the lattice according to the 7 Bravais lattices. Remind that symmetry is not considered for the moment. See :py:func:`bravaisLattice` """ self.name = None """ Name of the crystal object """ self.Z = None """ number of unit formula """ self.Natoms = None """ number of atoms in the unit cell """ self.group = None """ Symmetry group. Nevertheless, remind that symmetry is not considered. """ self.XYZCoord = list() """ List of cartesian coordinates of the atoms in the unit cell. This is a list object. See :py:func:`computeRedCoord` and :py:func:`red2cart` """ self.redCoord = list() """ List of reduce coordinates of the atoms in the unit cell. This is a list object. See :py:func:`computeXYZCoord` and :py:func:`cart2red` """ self.atomNames = list() """ List of atom names of all atoms in the unit cell. """ self.__matrixSet = False """ *bool* control if transformation matrix are set or not. """ self.verbose = True """ *bool* control verbosity of methods. """ # # arguments list # argList = ["a", "b", "c", "alpha", "beta", "gamma", "veca", "vecb", "vecc", "name", "lattice", "verbose", "Z"] # # check args and set attributes # if len(largs) != 0: raise TypeError("Use a 'key = val' syntax such as Crystal( key1 = val, key2 = val) ") for arg in args.keys(): if arg not in argList: raise NameError("name '" + arg + "' unknown") else: if arg == "a": try: self._a = float(args[arg]) except ValueError: raise ValueError("a must be a number") elif arg == "b": try: self._b = float(args[arg]) except ValueError: raise ValueError("b must be a number") elif arg == "c": try: self._c = float(args[arg]) except ValueError: raise ValueError("c must be a number") elif arg == "alpha": try: self._alpha = float(args[arg]) except ValueError: raise ValueError("alpha must be a number") elif arg == "beta": try: self._beta = float(args[arg]) except ValueError: raise ValueError("beta must be a number") elif arg == "gamma": try: self._gamma = float(args[arg]) except ValueError: raise ValueError("gamma must be a number") elif arg == "Z": try: self.Z = int(args[arg]) except ValueError: raise ValueError("Z must be an integer") elif arg == "veca": self._veca = _checkXinR3(args[arg], "veca") elif arg == "vecb": self._vecb = _checkXinR3(args[arg], "vecb") elif arg == "vecc": self._vecc = _checkXinR3(args[arg], "vecc") elif arg == "name": self.name = str(args[arg]) elif arg == "lattice": self._lattice = str(args[arg]).strip() if self._lattice not in bravais: raise LatticeError(self._lattice + " is not a Bravais lattice :\n" + str(bravais)) elif arg == "verbose": self.verbose = str(args[arg]) # # check and compute crystal data # if self.__vectorsExist() and self.__parametersExist(): raise Warning("When you create a crystal you have to set either \ lattice parameters or lattice vetors but not both of them") # set lattice parameters according to bravais lattice if self._lattice is not None: self._a, self._b, self._c, self._alpha, self._beta, self._gamma = self._setLatticeParameters() if not self.__vectorsExist() and not self.__parametersExist(): _warningMessage("You did not set neither lattice parameters neither lattice vectors") # compute crystal data # case 1 : user gives lattice parameters if self.__parametersExist(): # check bravais lattice calculatedLattice = self._getBravaisLattice() if self._lattice is None: self._lattice = calculatedLattice elif self._lattice != calculatedLattice: print("lattice you set :" + self._lattice) print("lattice I found :" + calculatedLattice) raise LatticeError("Bravais lattice inconsistent") # compute lattice vectors and transformation matrix self.__computeLatticeVectors() self.__fillMatrix() self.__computeVolume() # case 2 : user gives lattice vectors elif self.__vectorsExist(): # compute lattice parameters and transformation matrix self.__computeLatticeParameters() self.__fillMatrix() self.__computeVolume() # check bravais lattice calculatedLattice = self._getBravaisLattice() if self._lattice is None: self._lattice = calculatedLattice elif self._lattice != calculatedLattice: print("lattice you set :" + self._lattice) print("lattice I found :" + calculatedLattice) raise LatticeError("Bravais lattice inconsistent") # ------------------------------------------------------------------------- # Construction from a function # ------------------------------------------------------------------------- @classmethod
[docs] def fromPOSCAR(cls, poscar = "POSCAR", verbose = True): """ Create a crystal object from a POSCAR/CONTCAR VASP structure file. The POSCAR file can be given as a list of string (lines of the POSCAR file) or you can give a file object, or a file name. :param poscar: POSCAR file :type poscar: list, string, or file object :param verbose: verbosity :type verbose: bool """ # check argument if isinstance(poscar, file): poscar = poscar.readlines() elif isinstance(poscar, list): pass elif isinstance(poscar, str): if "\n" in poscar: poscar = poscar.split("\n") else: poscar = open(poscar, "r").readlines() else: raise Warning("argument of fromPOSCAR must be a list, a file object, or a file name") # delete blank lines for line in poscar: if line.strip() == "": del line # lecture de l'entete du fichier title = poscar[0].strip() scale = float(poscar[1].split()[0]) veca = [float(val) * scale for val in poscar[2].split()[0:3] ] vecb = [float(val) * scale for val in poscar[3].split()[0:3] ] vecc = [float(val) * scale for val in poscar[4].split()[0:3] ] line = poscar[5].strip() vasp4 = True for el in line.split(): vasp4 *= el.isdigit() if vasp4: # type vasp4.x NatomPerType = [int(val) for val in line.split()] if verbose: print("vasp 4.x POSCAR file") else: # type vasp5.x atomTypeNames = line.split() NatomPerType = [int(val) for val in poscar[6].strip().split()] if verbose: print("vasp 5.x POSCAR file") # read coordinate direct = False cartesian = False start = False for line in poscar[6:]: if line.strip() == "": break elif line.strip()[0].lower() == "s": # Selective Dynamics continue elif line.strip()[0].lower() == "d": direct = True start = True x = list() elif line.strip()[0].lower() == "c" or line.strip()[0].lower() == "k": cartesian = True start = True x = list() elif line.strip()[0].lower() == "s": continue elif start: coord = [float(val) for val in line.split()[0:3]] x.append(coord) else: continue if cartesian: # apply scale factor for xi in x: xi[0] *= scale xi[1] *= scale xi[2] *= scale # definition du cristal Natoms = len(x) n = 0 for ntyp in NatomPerType: n += ntyp if Natoms != n: print("Natoms = {0} \t n = {1}".format(Natoms, n)) raise Warning("Unconsistant atom number") crystal = cls(veca = veca, vecb = vecb, vecc = vecc ) crystal.Natoms = Natoms crystal.name = title.strip() if verbose: print(crystal) # nom des atomes ntype = 0 itype = 0 for iat in range(Natoms): if vasp4: name = "X{0}".format(itype + 1) else: name = atomTypeNames[itype] crystal.atomNames.append(name) ntype += 1 if ntype == NatomPerType[itype]: itype += 1 ntype = 0 # passage en coordonnee cartesienne si necessaire for coord in x: if direct: crystal.redCoord.append(coord ) crystal.XYZCoord.append(crystal.red2cart(coord)) elif cartesian: crystal.redCoord.append(crystal.cart2red(coord)) crystal.XYZCoord.append(coord) return crystal
@classmethod
[docs] def fromCRYSTAL09(cls, outputfile, verbose = True): """ Create a crystal object from an output of CRYSTAL09 program. The function read the last crystallographic cell. Outputfile can be a file or a file name. :param outputfile: CRYSTAL09 output file (.out) :type outputfile: string or file object :param verbose: verbosity :type verbose: bool """ # check argument if isinstance(outputfile, file): outfile = outputfile if isinstance(outputfile, str): outfile = open(outputfile, "r") else: raise Warning("argument of fromCRYSTAL09 must be a file object, or a file name") slab = False locGroupe = "SPACE GROUP" locMaille = " PRIMITIVE CELL" # read general data line = outfile.readline() end = True while line != "": line = outfile.readline() if "SLAB CALCULATION" in line: slab = True locGroupe = "PLANE GROUP" elif "EEEEEEEEEE STARTING" in line: phasename = outfile.readline().strip() if verbose: print("name : {0}".format(phasename)) elif locGroupe in line: group = line.split(":")[1].strip() if verbose: print("groupe : {0}".format(group)) elif "TRANSFORMATION MATRIX PRIMITIVE-CRYSTALLOGRAPHIC CELL" in line: locMaille = " CRYSTALLOGRAPHIC CELL " elif "FINAL OPTIMIZED GEOMETRY" in line: end = False break if end: print("Optimisation did not converge, final optimized geometry not found.") print("Input geometry will be read instead.") outfile.seek(0) line = outfile.readline() while " GEOMETRY FOR WAVE FUNCTION " not in line: line = outfile.readline() # read geometry located at locMaille line = outfile.readline() while locMaille not in line: line = outfile.readline() outfile.readline() a, b, c, alpha, beta, gamma = outfile.readline().split() for i in range(4): outfile.readline() names = list() red = list() Z = list() line = outfile.readline() while line != "\n": i, p, Zi, namei, xi, yi, zi = line.split() names.append(namei) Z.append(int(Zi)) if slab: red.append([float(xi), float(yi), float(zi) / float(c) ]) else: red.append([float(xi), float(yi), float(zi)]) line = outfile.readline() # list atom names atomnames = list() for name in names: if name not in atomnames: atomnames.append(name) # sort atoms by name redSorted = list() namesSorted = list() for name in atomnames: for iat, r in enumerate(red): if names[iat].strip() == name.strip(): redSorted.append(r) namesSorted.append(names[iat]) if len(redSorted) != len(red) or len(names) != len(namesSorted): raise ValueError("Error during atom sorting") # create crystal object crystal = cls(a = a, b = b, c = c, alpha = alpha, beta = beta, gamma = gamma) crystal.name = phasename crystal.group = group crystal.redCoord = redSorted crystal.computeXYZCoord() crystal.Natoms = len(redSorted) crystal.atomNames = namesSorted if verbose: print(crystal) return crystal
@classmethod
[docs] def fromCONFIG(cls, config = "CONFIG", verbose = True): """ Create a crystal object from a CONFIG/REVCON file of DLPOLY classic. :param config: name of the CONFIG/REVCON file :type config: string :param verbose: verbosity :type verbose: bool """ # check argument if isinstance(config, file): config = config.readlines() elif isinstance(config, list): pass elif isinstance(config, str): if "\n" in config: config = config.split("\n") else: config = open(config, "r").readlines() else: raise Warning("argument of fromCONFIG must be a list, a file object, or a file name") # delete blank lines for line in config: if line.strip() == "": del line # lecture de l'entete du fichier title = config[0].strip() ndata = int(config[1].split()[0]) if ndata not in [0, 1, 2]: print("Error : levcfg is not 0, 1 or 2") exit(1) veca = [float(val) for val in config[2].split()[0:3]] vecb = [float(val) for val in config[3].split()[0:3]] vecc = [float(val) for val in config[4].split()[0:3]] crystal = cls(veca = veca, vecb = vecb, vecc = vecc) crystal.name = title.strip() # read coordinate il = 4 nat = 0 while il < len(config) - 1: il += 1 atname = config[il].split()[0] atnum = int(config[il].split()[1]) nat += 1 if atnum != nat: print("Error : atom number inconsitant") print(config[il]) exit(1) il += 1 coord = [float(val) for val in config[il].split()[0:3]] if ndata == 1: il += 1 elif ndata == 2: il += 2 crystal.XYZCoord.append(coord) crystal.atomNames.append(atname) crystal.Natoms = len(crystal.XYZCoord) # calc reduce coordinate crystal.computeRedCoord() return crystal # ------------------------------------------------------------------------- # Bravais lattice name and lattice parameters # -------------------------------------------------------------------------
def _setLatticeParameters(self): """ set lattice parameters according to bravais lattice name """ th = 1.e-8 if self._lattice == "triclinic": # nothing to do print("Triclinic lattice :") if self._a is None: self._a = float(raw_input("a = ")) if self._b is None: self._b = float(raw_input("b = ")) if self._c is None: self._c = float(raw_input("c = ")) if self._alpha is None: self._alpha = float(raw_input("alpha = ")) if self._beta is None: self._beta = float(raw_input("beta = ")) if self._gamma is None: self._gamma = float(raw_input("gamma = ")) elif self._lattice == "monoclinic": # beta = gamma = 90 degree print("Monoclinic lattice :") if self._a is None: self._a = float(raw_input("a = ")) if self._b is None: self._b = float(raw_input("b = ")) if self._c is None: self._c = float(raw_input("c = ")) if self._alpha is not None and fabs(self._alpha - 90.0) > th: print("alpha = " + str(self._alpha)) raise LatticeError("Monoclinic lattice alpha must be 90 degrees") else: self._beta = 90.0 if self._beta is None: self._beta = float(raw_input("beta = ")) if self._gamma is not None and fabs(self._gamma - 90.0) > th: print("beta = " + str(self._beta)) raise LatticeError("Monoclinic lattice gamma must be 90 degrees") else: self._gamma = 90.0 elif self._lattice == "orthorhombic": # alpha = beta = gamma = 90 degree print("Monoclinic lattice :") if self._a is None: self._a = float(raw_input("a = ")) if self._b is None: self._b = float(raw_input("b = ")) if self._c is None: self._c = float(raw_input("c = ")) if self._alpha is not None and fabs(self._alpha - 90.0) > th: print("alpha = " + str(self._alpha)) raise LatticeError("Orthorhombic lattice alpha must be 90 degrees") else: self._beta = 90.0 if self._beta is not None and fabs(self._beta - 90.0) > th: print("beta = " + str(self._beta)) raise LatticeError("Orthorhombic lattice beta must be 90 degrees") else: self._beta = 90.0 if self._gamma is not None and fabs(self._gamma - 90.0) > th: print("gamma = " + str(self._gamma)) raise LatticeError("Orthorhombic lattice gamma must be 90 degrees") else: self._gamma = 90.0 elif self._lattice == "tetragonal": # a = b != c, alpha = beta = gamma = 90 if self._a is None: self._a = float(raw_input("a = ")) if self._b is not None and fabs(self._a - self._b) > th: print("a = " + str(self._a)) print("b = " + str(self._b)) raise LatticeError("Tetragonal lattice : a = b") else: self._b = self._a if self._c is None: self._c = float(raw_input("c = ")) if self._alpha is not None and fabs(self._alpha - 90.0) > th: print("alpha = " + str(self._alpha)) raise LatticeError("Tetragonal lattice alpha must be 90 degrees") else: self._alpha = 90.0 if self._beta is not None and fabs(self._beta - 90.0) > th: print("beta = " + str(self._beta)) raise LatticeError("Tetragonal lattice beta must be 90 degrees") else: self._beta = 90.0 if self._gamma is not None and fabs(self._gamma - 90.0) > th: print("gamma = " + str(self._gamma)) raise LatticeError("Tetragonal lattice gamma must be 90 degrees") else: self._gamma = 90.0 elif self._lattice == "rhombohedral": # a = b = c, alpha = beta = gamma != 90 if self._a is None: self._a = float(raw_input("a = ")) if self._b is not None and fabs(self._a - self._b) > th: print("a = " + str(self._a)) print("b = " + str(self._b)) raise LatticeError("Rhombohedral lattice : a = b = c") else: self._b = self._a if self._c is not None and fabs(self._a - self._c) > th: print("a = " + str(self._a)) print("c = " + str(self._c)) raise LatticeError("Rhombohedral lattice : a = b = c") else: self._c = self._a if self._alpha is None: self._alpha = float(raw_input("alpha = ")) if self._beta is not None and fabs(self._beta - self._alpha) > th: print("alpha = " + str(self._alpha)) print("beta = " + str(self._beta)) raise LatticeError("Rhombohedral lattice alpha = beta = gamma") else: self._beta = 90.0 if self._gamma is not None and fabs(self._gamma - self._alpha) > th: print("alpha = " + str(self._alpha)) print("gamma = " + str(self._gamma)) raise LatticeError("Rhombohedral lattice alpha = beta = gamma") else: self._gamma = 90.0 elif self._lattice == "hexagonal": # a = b != c, alpha = beta = 90 degree, gamma = 120 degree if self._a is None: self._a = float(raw_input("a = ")) if self._b is not None and fabs(self._a - self._b) > th: print("a = " + str(self._a)) print("b = " + str(self._b)) raise LatticeError("Hexagonal lattice : a = b") else: self._b = self._a if self._c is None: self._c = float(raw_input("c = ")) if self._alpha is not None and fabs(self._alpha - 90.0) > th: print("alpha = " + str(self._alpha)) raise LatticeError("Hexagonal lattice alpha must be 90 degrees") else: self._alpha = 90.0 if self._beta is not None and fabs(self._beta - 90.0) > th: print("beta = " + str(self._beta)) raise LatticeError("Hexagonal lattice beta must be 90 degrees") else: self._beta = 90.0 if self._gamma is not None and fabs(self._gamma - 120.0) > th: print("gamma = " + str(self._gamma)) raise LatticeError("Hexagonal lattice gamma must be 120 degrees") else: self._gamma = 120.0 elif self._lattice == "cubic": # a = b = c, alpha = beta == gamma = 90 degree if self._a is None: self._a = float(raw_input("a = ")) if self._b is not None and fabs(self._a - self._b) > th: print("a = " + str(self._a)) print("b = " + str(self._b)) raise LatticeError("Cubic lattice : a = b = c") else: self._b = self._a if self._c is not None and fabs(self._a - self._c) > th: print("a = " + str(self._a)) print("c = " + str(self._c)) raise LatticeError("Cubic lattice : a = b = c") else: self._c = self._a if self._alpha is not None and fabs(self._alpha - 90.0) > th: print("alpha = " + str(self._alpha)) raise LatticeError("Cubic lattice alpha must be 90 degrees") else: self._alpha = 90.0 if self._beta is not None and fabs(self._beta - 90.0) > th: print("beta = " + str(self._beta)) raise LatticeError("Cubic lattice beta must be 90 degrees") else: self._beta = 90.0 if self._gamma is not None and fabs(self._gamma - 90.0) > th: print("gamma = " + str(self._gamma)) raise LatticeError("Cubic lattice gamma must be 90 degrees") else: self._gamma = 90.0 return self._a, self._b, self._c, self._alpha, self._beta, self._gamma def _getBravaisLattice(self): """ return the bravais lattice name according to lattice parameters """ th = 1.e-4 if not self.__parametersExist(): raise LatticeError("Parameters unknown") if fabs(self._alpha - 90.0) < th and fabs(self._gamma - 90.0) < th: if fabs(self._beta - 90.0) < th: if fabs(self._a - self._b) < th and fabs(self._a - self._c) < th: lattice = "cubic" elif fabs(self._a - self._b) < th: lattice = "tetragonal" else: lattice = "orthorhombic" else: lattice = "monoclinic" elif fabs(self._alpha - 90.0) < th and fabs(self._beta - 90.0) < th and \ fabs(self._gamma - 120.0) < th: lattice = "hexagonal" elif fabs(self._alpha - self._beta) < th and fabs(self._alpha - self._gamma) < th \ and fabs(self._a - self._b) < th and fabs(self._a - self._c) < th: lattice = "rhombohedral" else: lattice = "triclinic" return lattice # ------------------------------------------------------------------------- # Attribute properties : a, b, c, alpha, beta, gamma, lattice and lattice vectors # ------------------------------------------------------------------------- def _set_a(self, val): """ check instance of val and set self._a """ _warningMessage("You are changing the lattice") try: self._a = float(val) except ValueError: raise ValueError("a must be a number") if not self.__parametersExist(): _warningMessage("All lattice parameters are still not known") else: self.__computeLatticeVectors() self.__fillMatrix() self.__computeVolume() self._lattice = self._getBravaisLattice() self.computeXYZCoord() def _get_a(self): """ return self._a """ return self._a a = property(_get_a, _set_a) """ Lattice parameter a, lengths of the vectors :math:`\vec{a}` """ def _set_b(self, val): """ check instance of val and set self._b """ _warningMessage("You are changing the lattice") try: self._b = float(val) except ValueError: raise ValueError("b must be a number") if not self.__parametersExist(): _warningMessage("All lattice parameters are still not known") else: self.__computeLatticeVectors() self.__fillMatrix() self.__computeVolume() self._lattice = self._getBravaisLattice() self.computeXYZCoord() def _get_b(self): """ return self._b """ return self._b b = property(_get_b, _set_b) """ Lattice parameter b, lengths of the vectors :math:`\vec{b}` """ def _set_c(self, val): """ check instance of val and set self._c """ _warningMessage("You are changing the lattice") try: self._c = float(val) except ValueError: raise ValueError("c must be a number") if not self.__parametersExist(): _warningMessage("All lattice parameters are still not known") else: self.__computeLatticeVectors() self.__fillMatrix() self.__computeVolume() self._lattice = self._getBravaisLattice() self.computeXYZCoord() def _get_c(self): """ return self._c """ return self._c c = property(_get_c, _set_c) """ Lattice parameter c, lengths of the vectors :math:`\vec{c}` """ def _set_alpha(self, val): """ check instance of val and set self._alpha """ _warningMessage("You are changing the lattice") try: self._alpha = float(val) except ValueError: raise ValueError("alpha must be a number") if not self.__parametersExist(): _warningMessage("All lattice parameters are still not known") else: self.__computeLatticeVectors() self.__fillMatrix() self.__computeVolume() self._lattice = self._getBravaisLattice() self.computeXYZCoord() def _get_alpha(self): """ return self._alpha """ return self._alpha alpha = property(_get_alpha, _set_alpha) """ Lattice angle :math:`\alpha` . All angles must be given in degrees and are return in degrees. They are internally convert in radian when needed. """ def _set_beta(self, val): """ check instance of val and set self._beta """ _warningMessage("You are changing the lattice") try: self._beta = float(val) except ValueError: raise ValueError("beta must be a number") if not self.__parametersExist(): _warningMessage("All lattice parameters are still not known") else: self.__computeLatticeVectors() self.__fillMatrix() self.__computeVolume() self._lattice = self._getBravaisLattice() self.computeXYZCoord() def _get_beta(self): """ return self._beta """ return self._beta beta = property(_get_beta, _set_beta) """ Lattice angle :math:`\beta` . All angles must be given in degrees and are return in degrees. They are internally convert in radian when needed. """ def _set_gamma(self, val): """ check instance of val and set self._gamma """ _warningMessage("You are changing the lattice") try: self._gamma = float(val) except ValueError: raise ValueError("gamma must be a number") if not self.__parametersExist(): _warningMessage("All lattice parameters are still not known") else: self.__computeLatticeVectors() self.__fillMatrix() self.__computeVolume() self._lattice = self._getBravaisLattice() self.computeXYZCoord() def _get_gamma(self): """ return self._gamma """ return self._gamma gamma = property(_get_gamma, _set_gamma) """ Lattice angle :math:`\gamma` . All angles must be given in degrees and are return in degrees. They are internally convert in radian when needed. """ def _set_lattice(self, val): """ check instance of val and set self._gamma """ _warningMessage("You are changing the lattice all lattice parameters may change") if val not in bravais: raise LatticeError( self._lattice + " is not a Bravais lattice :\n" + str(bravais)) else: self._lattice = val # reset all parameters self._a = None self._b = None self._c = None self._alpha = None self._beta = None self._gamma = None self._a, self._b, self._c, self._alpha, self._beta, self._gamma = self._setLatticeParameters() # compute new crystal data self.__computeLatticeVectors() self.__fillMatrix() self.__computeVolume() self.computeXYZCoord() def _get_lattice(self): """ return self._lattice """ return self._lattice lattice = property( _get_lattice, _set_lattice) """ Name of the lattice according to the 7 Bravais lattices. Remind that symmetry is not considered for the moment. See :py:func:`bravaisLattice` """ def _set_veca(self, val): """ check instance of val and set self._veca """ _warningMessage("You are changing the lattice") self._veca = _checkXinR3(val) if not self.__vectorsExist(): _warningMessage("All lattice vectors are still not known") else: self.__computeLatticeParameters() self.__fillMatrix() self.__computeVolume() self._lattice = self._getBravaisLattice() self.computeXYZCoord() def _get_veca(self): """ return self._veca """ return self._veca veca = property( _get_veca, _set_veca) """ Lattice vector :math:`\vec{a}` in a cartesian frame where :math:`\vec{a}` is along x axes and :math:`\vec{b}` is in the (x,y) plane. """ def _set_vecb(self, val): """ check instance of val and set self._vecb """ _warningMessage("You are changing the lattice") self._vecb = _checkXinR3(val) if not self.__vectorsExist(): _warningMessage("All lattice vectors are still not known") else: self.__computeLatticeParameters() self.__fillMatrix() self.__computeVolume() self._lattice = self._getBravaisLattice() self.computeXYZCoord() def _get_vecb(self): """ return self._vecb """ return self._vecb vecb = property( _get_vecb, _set_vecb) """ Lattice vector :math:`\vec{b}` in a cartesian frame where :math:`\vec{a}` is along x axes and :math:`\vec{b}` is in the (x,y) plane. """ def _set_vecc(self, val): """ check instance of val and set self._vecc """ _warningMessage("You are changing the lattice") self._vecc = _checkXinR3(val) if not self.__vectorsExist(): _warningMessage("All lattice vectors are still not known") else: self.__computeLatticeParameters() self.__fillMatrix() self.__computeVolume() self._lattice = self._getBravaisLattice() self.computeXYZCoord() def _get_vecc(self): """ return self._vecc """ return self._vecc vecc = property(_get_vecc, _set_vecc) """ Lattice vector :math:`\vec{c}` in a cartesian frame where :math:`\vec{a}` is along x axes and :math:`\vec{b}` is in the (x,y) plane. """ def _set_volume(self, val): """ changing volume by hand is forbiden """ _warningMessage("You can change volume by hand") def _get_volume(self): """ return self._vecc """ return self._volume volume = property(_get_volume, _set_volume) """ volume of the unit cell. """ # ------------------------------------------------------------------------- # Parameters or lattice vectors calculations # ------------------------------------------------------------------------- def __computeLatticeVectors(self): """ Compute lattice vectors coordinates in a cartesian frame. Vector a is along the x axes, vector b is in the (x, y) plane. """ if not self.__parametersExist(): raise LatticeError("Parameters unknown") # conversion en radian alphar = self._alpha * pi / 180.0 betar = self._beta * pi / 180.0 gammar = self._gamma * pi / 180.0 # calcul des vecteurs self._veca = [0.,0.,0.] self._veca[0] = self._a self._vecb = [0.,0.,0.] self._vecb[0] = self._b * cos( gammar ) self._vecb[1] = self._b * sin( gammar ) self._vecc = [0.,0.,0.] self._vecc[0] = self._c * cos( betar ) cy = ( cos(alphar) - cos(gammar) * cos(betar) ) / sin(gammar) self._vecc[1] = self._c * cy cz = sqrt( ( sin( betar ) )**2 - cy**2 ) self._vecc[2] = self._c * cz def __computeLatticeParameters(self, verbose = False): """ compute lattice parameters from lattice vectors """ if not self.__vectorsExist() : raise LatticeError("Vectors unknown") # make vectors veca = np.array( self._veca ) vecb = np.array( self._vecb ) vecc = np.array( self._vecc ) # norm self._a = sqrt( (veca**2).sum() ) self._b = sqrt( (vecb**2).sum() ) self._c = sqrt( (vecc**2).sum() ) # alpha in degree u = vecb / self._b v = vecc / self._c scalaire = np.dot( u, v) if fabs(scalaire < 1.): self._alpha = arccos( scalaire ) * 180.0 / pi else: print("Erreur : produit scalaire alpha" + str(scalaire)) # beta in degree u = veca / self._a v = vecc / self._c scalaire = np.dot( u, v) if fabs(scalaire < 1.): self._beta = arccos( scalaire ) * 180.0 / pi else: print("Erreur : produit scalaire beta" + str(scalaire)) # gamma in degree u = veca / self._a v = vecb / self._b scalaire = np.dot( u, v) if fabs(scalaire < 1.): self._gamma = arccos( scalaire ) * 180.0 / pi else: print("Erreur : produit scalaire gamma" + str(scalaire)) # print if verbose : print("a = %10.5f" % self._a) print("b = %10.5f" % self._b) print("c = %10.5f" % self._c) print("alpha = %10.3f" % (self._alpha)) print("beta = %10.3f" % (self._beta )) print("gamma = %10.3f" % (self._gamma) + "\n") def __fillMatrix(self) : """ fill transformation matrix : * Mred2cart : reduce -> cartesian * Mcart2red : cartesian -> reduce Theses matrix are numpy.ndarray object. """ # check if lattice vectors are known if not self.__vectorsExist(): raise LatticeError("Lattice vectors unknown") # check if lattice parameters are known if not self.__parametersExist(): raise LatticeError("Lattice parameters unknown") # transformation matrix from reduce to cartesian coordinate self.Mred2cart = np.array([self._veca, self._vecb, self._vecc]).transpose() # transformation matrix from cartesian to reduce coordinate self.Mcart2red = np.linalg.inv(self.Mred2cart) # matrice de passage coordonnées cartésiennes -> coordonnées réduites #alphar = self._alpha * pi / 180.0 #betar = self._beta * pi / 180.0 #gammar = self._gamma * pi / 180.0 #cotgamma = cos(gammar) / sin(gammar) #cy = ( cos(alphar) - cos(gammar) * cos(betar) ) / sin(gammar) #cz = sqrt( ( sin( betar ) )**2 - cy**2 ) #self.Mcart2red = np.zeros( (3,3) ) #self.Mcart2red[0,0] = 1. / self._a #self.Mcart2red[0,1] = - cotgamma / self._a #self.Mcart2red[0,2] = 1. / self._a * ( cy/cz * cotgamma - cos( betar ) / cz ) #self.Mcart2red[1,1] = 1. / ( self._b * sin( gammar ) ) #self.Mcart2red[1,2] = - cy / ( self._b * cz * sin( gammar ) ) #self.Mcart2red[2,2] = 1. / ( self._c * cz ) # varialble de contrôle self.__matrixSet = True def __computeVolume(self): """ compute the volume of the cell """ if not self.__vectorsExist(): self.__computeLatticeVectors() self._volume = np.dot(self._veca, np.cross(self._vecb, self._vecc)) return self._volume # ------------------------------------------------------------------------- # Check if parameters or vectors are setted # ------------------------------------------------------------------------- def __parametersExist(self): """ Did lattice parameters (a, b, c, alpha, beta, gamma) set ? """ if self._a == None or self._b == None or self._c == None or \ self._alpha == None or self._beta == None or self._gamma == None: return False else: return True def __vectorsExist(self): """ Did lattice vectors (veca, vecb et vecc) set ? """ if self._veca == None or self._vecb == None or self._vecc == None: return False else: return True # ------------------------------------------------------------------------- # conversion between cartesian and reduce unit coordinates # -------------------------------------------------------------------------
[docs] def red2cart(self, x): """ Convert reduce coordinate into cartesian coordinate. :param x: cartesian coordinate vector :type x: list or array or ndarray, len(x) = 3 :rtype: list of lenght 3 """ vecx = _checkXinR3(x) if not self.__matrixSet: self.__fillMatrix() return np.dot(self.Mred2cart, vecx).tolist()
[docs] def cart2red(self, x): """ Convert cartesian coordinate into reduce coordinate. :param x: reduce coordinate vector :type x: list or array or ndarray, len(x) = 3 :rtype: list of lenght 3 """ vecx = _checkXinR3(x) if not self.__matrixSet: self.__fillMatrix() return np.dot(self.Mcart2red, vecx).tolist()
[docs] def computeRedCoord(self): """ Compute cartesian coordinates for all coordinates stored in self.redCoord. """ if self.XYZCoord == []: _warningMessage("no cartesian coordinates availlable") self.redCoord = [self.cart2red(xyz) for xyz in self.XYZCoord] return self.redCoord
[docs] def computeXYZCoord(self): """ Compute reduce coordinates for all coordinates stored in self.XYZCoord. """ if self.redCoord == []: _warningMessage("no reduce coordinates availlable") self.XYZCoord = [self.red2cart(red) for red in self.redCoord] return self.XYZCoord # ------------------------------------------------------------------------- # Distance calculations # -------------------------------------------------------------------------
[docs] def dist_r(self, r1, r2): """ Compute the minimum distance between reduce coordinates r1 and r2 considering the periodic conditions. :param r1: reduce coordinate of atom 1 :type r1: list or array or ndarray, len(A1) = 3 :param r2: reduce coordinate of atom 2 :type r2: list or array or ndarray, len(A2) = 3 :rtype: float """ dr = self.img(r1, r2) dx = self.red2cart(dr) return np.sqrt(dx[0]**2 + dx[1]**2 + dx[2]**2)
[docs] def dist_x(self, x1, x2): """ Compute the minimum distance between cartesian coordinates x1 and x2 considering the periodic conditions. :param x1: cartesian coordinates of atom 1 :type x1: list or array or ndarray, len(A1) = 3 :param x2: cartesian coordinates of atom 2 :type x2: list or array or ndarray, len(A2) = 3 :rtype: float """ dr = self.img(self.cart2red(x1), self.cart2red(x2)) dx = self.red2cart(dr) return np.sqrt(dx[0]**2 + dx[1]**2 + dx[2]**2)
[docs] def dist_i(self, iat, jat): """ Compute the minimum distance between atom with index iat and jat considering the periodic conditions. :param iat: atom index of atom i :type A1: integer :param A2: atom index of atom j :type A2: atom index :rtype: float """ dr = self.img(self.redCoord[iat], self.redCoord[jat]) dx = self.red2cart(dr) return np.sqrt(dx[0]**2 + dx[1]**2 + dx[2]**2)
[docs] def img(self, r1, r2): """ Return the shortest vector in reduce coordinates considering the periodic conditions. :param r1: reduce coordinate of atom r1 :type r1: list or array or ndarray, len(A1) = 3 :param r2: reduce coordinate of atom r2 :type r2: list or array or ndarray, len(A2) = 3 :rtype: list r1 and r2 must be reduce coordinate and an object of length 3 ! In order to improve efficiency no check are done. """ dr = [0., 0., 0.] for i in range(3): dr[i] = r2[i] - r1[i] if dr[i] > .5: dr[i] -= 1. elif dr[i] < -.5: dr[i] += 1. return dr # ------------------------------------------------------------------------- # Neighbors list # -------------------------------------------------------------------------
def neighbor(self, rcut, iatom, ligand = None): """" look for neighbors of atoms in the list iatom. If ligand present, only atoms with name ligand are considered. If rcut < cell parameters / 2, the subroutine looks for multiple neighbors by considering all atoms in the 26 adjacent cells around the central the considered cell. :param rcut: cut off radius :type rcut: float :param iatom: list of atom for which neighbors have to be identified :type iatom: list :param ligand: limit the neighbors list to atoms name ligand (default: all) :type ligand: string :return: neighbor list for each atom in iatom list :rtype: list[list] """ neigh = list() if rcut > self.a / 2. or rcut > self.b / 2. or rcut > self.c / 2: # small cell look for multiple neighbor due to translation print("cell parameters / 2 < rcut => look for multiple neighbors") for iv, iat in enumerate(iatom): riat = np.array(self.redCoord[iat]) neigh.append(list()) for jat in range(self.Natoms): if iat == jat: continue if ligand and self.atomNames[jat] != ligand: continue rjat = np.array([val for val in self.redCoord[jat]]) for t in trans: dr = rjat + t - riat dx = self.red2cart(dr) d = np.sqrt((dx**2).sum()) if d <= rcut: # new neighbor neigh[iv].append(jat) else: # simpler case where atoms can be neighbor only one time for iv, iat in enumerate(iatom): riat = np.array(self.redCoord[iat]) neigh.append(list()) for jat in range(self.Natoms): if iat == jat: continue if ligand and self.atomNames[jat] != ligand: continue if self.dist_i(iat, jat) <= rcut: # new neighbor neigh[iv].append(jat) return neigh def allneighbor(self, rcut): """" look for neighbors of all atoms in the cell. If rcut < cell parameters / 2, the subroutine looks for multiple neighbors by considering all atoms in the 26 adjacent cells around the central the considered cell. :param rcut: cut off radius :type rcut: float :param iatom: list of atom for which neighbors have to be identified :type iatom: list :return: neighbor list for each atom in iatom list :rtype: list[list] """ # initialisation neigh = [list() for iat in range(self.Natoms)] if rcut > self.a / 2. or rcut > self.b / 2. or rcut > self.c / 2: # small cell look for multiple neighbor due to translation print("cell parameters / 2 < rcut => look for multiple neighbors") for iat in range(self.Natoms - 1): for jat in range(iat + 1, self.Natoms): for t in trans: dx = self.red2cart(self.redCoord[jat] + t - self.redCoord[iat]) d = np.sqrt((dx**2).sum()) if d <= rcut: neigh[iat].append(jat) neigh[jat].append(iat) else: # simpler case where atoms can be neighbor only one time for iat in range(self.Natoms - 1): for jat in range(iat + 1, self.Natoms): if self.dist_i(iat, jat) <= rcut: neigh[iat].append(jat) neigh[jat].append(iat) return neigh # ------------------------------------------------------------------------- # operation on atoms # -------------------------------------------------------------------------
[docs] def calcDeplacementAtomes(self, other): """ Compute the distance between the cartesian postion fo the same atom in self and other structures. :param other: crystal object :rtype: list of distance """ # check atom number if self.Natoms != other.Natoms: raise ValueError("Error : atom numbers does not match") # make copy of each crystal c1 = copy.deepcopy(self) c2 = copy.deepcopy(other) # put each atom of struct1 inside unitcell c1.wrapAtomes() # place atoms at minimum distance c1.alignCrystal(c2) deplacement = list() for xyz1, xyz2 in zip( c1.XYZCoord, c2.XYZCoord): d = 0. for x1, x2 in zip(xyz1, xyz2): d += (x2 - x1)**2 deplacement.append(sqrt(d)) return deplacement
[docs] def wrapAtoms(self): """ Replace all atoms into the unit cell using reduce coordinate """ if self.redCoord == []: self.calcRedCoord() for iat in range(self.Natoms): for i in range(3): if self.redCoord[iat][i] > 1.0: self.redCoord[iat][i] -= 1.0 elif self.redCoord[iat][i] < 0.0: self.redCoord[iat][i] += 1.0 self.calcXYZCoord() return self.XYZCoord
[docs] def alignCrystal(self, other): """ For each atom of the crystal, if an image of the atom belonging to other is closer to the same atom belonging to self, the other's atom is translated to that position. This method could be usefull if one wants to compute displacements of atoms after a relaxation of the structre. :param other: crystal object :rtype: list of distance Warning :if lattices of self and other are not the same, this have not any sense. """ if self.redCoord == []: self.computeRedCoord() if self.redCoord == []: raise NameError("crystal 1 : reduce unit have to be known") if other.redCoord == []: other.computeRedCoord() if self.redCoord == []: raise NameError("crystal 2 : reduce unit have to be known") for iat in range(self.Natoms): for i in range(3): d = fabs(other.redCoord[iat][i] - self.redCoord[iat][i]) if d > 0.5: if other.redCoord[iat][i] > self.redCoord[iat][i]: other.redCoord[iat][i] -= 1.0 else: other.redCoord[iat][i] += 1.0 other.calcXYZCoord() return other.XYZCoord
[docs] def makeSupercell(self, nx, ny, nz, sort = True): """ Build a supercell from the cell defined by the object. Return a new Crystal object as output. :param nx: number of cell in x direction :type nx: int :param ny: number of cell in y direction :type ny: int :param nz: number of cell in z direction :type nz: int :param sort: if True atoms are sorted by name and by z (needed for POSCAR output !). :type sort: bool :rtype: Crystal """ if nx == 1 and ny == 1 and nz == 1: print("{0}x{1}x{2} : nothing to do".format(nx, ny, nz)) new = copy.deepcopy(self) return new # check if self.redCoord == []: self.computeRedCoord() if self.redCoord == []: raise ValueError("coordinate unknown") # atom number if self.Natoms == None: self.Natoms = len(self.redCoord) # atom names atomList = list() if self.atomNames == []: self.atomNames = ["X" for i in range(self.Natoms)] atomList = ["X"] else: for name in self.atomNames: if name not in atomList: atomList.append(name) # make supercell red = list() atom = copy.deepcopy(self.atomNames) for ix in range(0, nx): tx = float(ix) for iy in range(0, ny): ty = float(iy) for iz in range(0, nz): tz = float(iz) for iat in range(self.Natoms): r = self.redCoord[iat] rx = (r[0] + tx) / float(nx) ry = (r[1] + ty) / float(ny) rz = (r[2] + tz) / float(nz) red.append([rx, ry, rz]) atom.append(atom[iat]) # new atom number Natoms = len(red) if Natoms != nx * ny * nz * self.Natoms: print("Natoms = %d" % Natoms) print("nx * ny * nz * self.Natoms = %d " % (nx * ny * nz * self.Natoms)) raise ValueError("Atom number error") # create supercell object veca = [nx * v for v in self.veca] vecb = [ny * v for v in self.vecb] vecc = [nz * v for v in self.vecc] new = Crystal(veca = veca, vecb = vecb, vecc = vecc) new.name = self.name new.Natoms = Natoms if sort: # sort atoms per name redNameSorted = list() atomNames = list() for name in atomList: for iat in range(Natoms): if atom[iat].strip() == name.strip(): redNameSorted.append(red[iat]) atomNames.append(name.strip()) # number of each atom nAtomPerType = dict() for name in atomList: nAtomPerType[name] = atomNames.count(name) # sort atoms by z redSorted = list() for ityp in range(len(atomList)): if ityp == 0: n1 = 0 n2 = nAtomPerType[atomList[ityp]] else: n1 += nAtomPerType[atomList[ityp - 1]] n2 = n1 + nAtomPerType[atomList[ityp]] redSorted += sorted(redNameSorted[n1:n2], key = lambda f:f[2], reverse = True) if len(redSorted) != len(red): print(len(redSorted)) print(len(red)) print("Error in makeSupercell, check atom number") exit(1) new.redCoord = redSorted else: new.redCoord = red atomNames = nx * ny * nz * self.atomNames new.atomNames = atomNames new.computeXYZCoord() return new # ------------------------------------------------------------------------- # output methods # -------------------------------------------------------------------------
[docs] def toXYZ(self, filename): """ (Under development) Write cartesian coordinate in a XYZ format :param filename: name of the output file. :type filename: string """ print("TODO") pass
[docs] def toPOSCAR(self, filename = "POSCAR", sort = True): """ Write crystal object in a vasp POSCAR file :param filename: name of the output file. :type filename: string """ # check if self.redCoord == []: self.computeRedCoord() if self.redCoord == []: raise ValueError("coordinate unknown") if self.Natoms == None: _warningMessage("Natoms was not set. I use the number of reduce coordinate") self.Natoms = len(self.redCoord) # title line if self.name is not None: poscar = self.name + "\n" else: poscar = "POSCAR file\n" # scaling factor poscar += " 1.0\n" # lattice vectors if not self.__vectorsExist(): self.computeLatticeVectors() for x in self.veca: poscar += "%20.12f" % x poscar += "\n" for x in self.vecb: poscar += "%20.12f" % x poscar += "\n" for x in self.vecc: poscar += "%20.12f" % x poscar += "\n" # atom name atomList = list() if self.atomNames != []: # look for atom type for name in self.atomNames: if name not in atomList: atomList.append(name) # print atom type for name in atomList: poscar += "%4s" % name poscar += "\n" # sort atom according to atom type if sort: redSorted = list() for name in atomList: for iat in range(self.Natoms): if self.atomNames[iat].strip() == name.strip(): redSorted.append(self.redCoord[iat]) if len(redSorted) != len(self.redCoord): print("Error check atom number") exit(1) else: redSorted = self.redCoord # print the atom number per type for name in atomList: poscar += "%4d" % self.atomNames.count(name) poscar += "\n" else: _warningMessage("Atom names unknown, I print only atom number") _warningMessage("toPOSCAR assume that atom coordinate are correctly sorted") poscar += "%5d\n" % len(self.redCoord) # reduce coordinate poscar += "Direct\n" for red in redSorted: for r in red: poscar += "%20.12f" % r poscar += "\n" open(filename, "w").write(poscar)
[docs] def toCONFIG(self, filename = "CONFIG"): """ Write crystal object in a CONFIG DLPOLY file :param filename: name of the output file. :type filename: string """ #_warningMessage("toCRYSTAL assume that atom coordinate are correctly sorted") # title line if self.name is not None: config = self.name + "\n" else: config = "CONFIG file\n" # periodic boundary if self.lattice == "cubic": config += "%10d%10d\n" % (0,1) elif self.lattice == "tetragonal" or self.lattice == "orthorhombic": config += "%10d%10d\n" % (0,2) else: config += "%10d%10d\n" % (0,3) # lattice vectors if not self.__vectorsExist(): self.computeLatticeVectors() for x in self.veca: config += "%20.12f" % x config += "\n" for x in self.vecb: config += "%20.12f" % x config += "\n" for x in self.vecc: config += "%20.12f" % x config += "\n" if self.XYZCoord == []: self.computeXYZCoord() atomNameKnown = True if self.atomNames == [] or len(self.atomNames) != len(self.XYZCoord): print(self.atomNames) print(len(self.atomNames), len(self.XYZCoord)) _warningMessage("Atom names unknown, I will use 'X' instead") atomNameKnown = False for iat, xyz in enumerate(self.XYZCoord): if atomNameKnown: config += "%s%10d\n" % (self.atomNames[iat].ljust(8), iat + 1) else: config += "%8s%10d\n" % ("X ", iat + 1) config += "%20.10f%20.10f%20.10f\n" % tuple(xyz) open(filename, "w").write(config)
[docs] def printLatticeVectors(self): """ Print the lattice vectors in a cartesian frame. """ dashline = "".join(50 * ["-"]) print(dashline) print(" Lattice vectors in cartesian frame : lattice is {0}".format(self.lattice)) print(dashline) print(" a = %15.10f %15.10f %15.10f" % (self.veca[0], self.veca[1], self.veca[2])) print(" b = %15.10f %15.10f %15.10f" % (self.vecb[0], self.vecb[1], self.vecb[2])) print(" c = %15.10f %15.10f %15.10f" % (self.vecc[0], self.vecc[1], self.vecc[2])) print(dashline + "\n")
[docs] def printLatticeParameters(self): """ Print lattice paramters. """ print(str(self)) # ------------------------------------------------------------------------- # Special methods # -------------------------------------------------------------------------
def __str__(self): """ string conversion """ if not self.__parametersExist(): raise LatticeError("Lattice parameters are not known did you set all \ lattice vectors or all lattice parameters") dashline = "".join(50 * ["-"]) + "\n" line = dashline if self.name != None: line += " lattice parameters : " + self.name + "\n" else : line += " lattice parameters : \n" line += dashline line += " a = %10.5f" % self._a + "\n" line += " b = %10.5f" % self._b + "\n" line += " c = %10.5f" % self._c + "\n" line += " alpha = %10.3f" % (self._alpha ) + "\n" line += " beta = %10.3f" % (self._beta ) + "\n" line += " gamma = %10.3f" % (self._gamma ) + "\n" line += dashline return line def __eq__(self, other): """ equal """ selfatt = [self.a, self.b, self.c, self.alpha, self.beta, self.gamma] for xyz in self.XYZCoord: selfatt += xyz for red in self.redCoord: selfatt += red selfatt += self.veca + self.vecb + self.vecc otheratt = [other.a, other.b, other.c, other.alpha, other.beta, other.gamma] for xyz in other.XYZCoord: otheratt += xyz for red in other.redCoord: otheratt += red otheratt += other.veca + other.vecb + other.vecc th = 1.e-6 res = True if self.lattice != other.lattice: res = False else: for s, o in zip(selfatt, otheratt): if fabs(s - o) > th: res = False return res # ----------------------------------------------------------------------------- # inner function or exception # -----------------------------------------------------------------------------
class LatticeError(Exception): """ Exception concerning lattice parameters, bravais name or lattice vectors. """ def __init__(self, why): self.why = why def __str__(self): return self.why def _checkXinR3( X, name = ""): """ check if X in R^3 and return a list object >>> _checkXinR3([0,1,2], "X") [0.0, 1.0, 2.0] >>> _checkXinR3(3,"a") Traceback (most recent call last): ... TypeError: Variable a must be a vector in R^3 >>> _checkXinR3(["0","1","2"], "X") [0.0, 1.0, 2.0] >>> _checkXinR3(["a","b","c"], "X") Traceback (most recent call last): ... ValueError: Variable X must be a vector in R^3 """ if (isinstance( X, list) and len(X) == 3) or \ (isinstance( X, np.ndarray) and X.size == 3) or \ (isinstance( X, array.ArrayType) and len(X) == 3): try: listX = [float(xi) for xi in X] except ValueError: raise ValueError("Variable " + name + " must be a vector in R^3") else: raise TypeError("Variable " + name + " must be a vector in R^3") return listX def _warningMessage(why): """ pring a warning >>> _warningMessage("small error") * * * WARNING : small error """ print(" * * * WARNING : " + why) # ----------------------------------------------------------------------------- # output functions or instanciation from a function # ----------------------------------------------------------------------------- def printBravaisLattice(): """ Print bravais lattice names and caracteristics and return the output in a string. """ data = """ triclinic : * a != b != c * alpha != beta != gamma != 90 monoclinic : * a != b != c * alpha = gamma = 90 ; beta != 90 orthorhombic : * a != b != c * alpha = beta = gamma = 90 tetragonal (quadratique) : * a = b != c * alpha = beta = gamma = 90 hexagonal : * a = b != c * alpha = beta = 90 ; gamma = 120 rhombohedral : * a = b = c * alpha = beta = gamma != 90 cubic : * a = b = c * alpha = beta = gamma = 90""" print(data) # ----------------------------------------------------------------------------- # main # ----------------------------------------------------------------------------- if __name__ == "__main__": doctest.testmod()