qmcpack/nexus/lib/structure.py

7361 lines
239 KiB
Python

##################################################################
## (c) Copyright 2015- by Jaron T. Krogel ##
##################################################################
#====================================================================#
# structure.py #
# Support for atomic structure I/O, generation, and manipulation. #
# #
# Content summary: #
# Structure #
# Represents a simulation cell containing a set of atoms. #
# Many functions for manipulating structures or obtaining #
# data regarding local atomic structure. #
# #
# generate_cell #
# User-facing function to generate an empty simulation cell. #
# #
# generate_structure #
# User-facing function to specify arbitrary atomic structures #
# or generate structures corresponding to atoms, dimers, or #
# crystals. #
# #
#====================================================================#
"""
The :py:mod:`structure` module provides support for atomic structure I/O,
generation, and manipulation.
List of module contents
-----------------------
Read cif file functions:
* :py:func:`read_cif_celldata`
* :py:func:`read_cif_cell`
* :py:func:`read_cif`
Operations on logical conditions:
* :py:func:`equate`
* :py:func:`negate`
Create a Monkhorst-Pack k-point mesh function
* :py:func:`kmesh`
Tile matrix malipulation functions
* :py:func:`reduce_tilematrix`
* :py:func:`tile_magnetization`
Rotate plane function
* :py:func:`rotate_plane`
Trivial filter function
* :py:func:`trivial_filter`
* :py:class:`MaskFilter`
* :py:func:`optimal_tilematrix`
Base class for :py:class:`Structure` class:
* :py:class:`Sobj`
Base class for :py:class:`DefectStructure`, :py:class:`Crystal`, and :py:class:`Jellium` classes:
* :py:class:`Structure`
SeeK-path functions
* :py:func:`\_getseekpath`
* :py:func:`get_conventional_cell`
* :py:func:`get_primitive_cell`
* :py:func:`get_kpath`
* :py:func:`get_symmetry`
* :py:func:`get_structure_with_bands`
* :py:func:`get_band_tiling`
* :py:func:`get_seekpath_full`
Interpolate structures functions
* :py:func:`interpolate_structures`
Animate structures functions
* :py:func:`structure_animation`
Concrete :py:class:`Structure` classes:
* :py:class:`DefectStructure`
* :py:class:`Crystal`
* :py:class:`Jellium`
Structure generation functions:
* :py:func:`generate_cell`
* :py:func:`generate_structure`
* :py:func:`generate_atom_structure`
* :py:func:`generate_dimer_structure`
* :py:func:`generate_trimer_structure`
* :py:func:`generate_jellium_structure`
* :py:func:`generate_crystal_structure`
* :py:func:`generate_defect_structure`
Read structure functions
* :py:func:`read_structure`
Module contents
---------------
"""
import os
import numpy as np
from copy import deepcopy
from random import randint
from numpy import abs,all,append,arange,around,array,atleast_2d,ceil,cos,cross,cross,diag,dot,empty,exp,flipud,floor,identity,isclose,logical_not,mgrid,mod,ndarray,ones,pi,round,sign,sin,sqrt,uint64,zeros
from numpy.linalg import inv,det,norm
from unit_converter import convert
from numerics import nearest_neighbors,convex_hull,voronoi_neighbors
from periodic_table import pt,is_element
from fileio import XsfFile,PoscarFile
from generic import obj
from developer import DevBase,unavailable,error,warn
from debug import ci,ls,gs
try:
from scipy.special import erfc
except:
erfc = unavailable('scipy.special','erfc')
#end try
try:
import matplotlib.pyplot as plt
from matplotlib.pyplot import plot,subplot,title,xlabel,ylabel
except:
plot,subplot,title,xlabel,ylabel,plt = unavailable('matplotlib.pyplot','plot','subplot','title','xlabel','ylabel','plt')
#end try
# installation instructions to enable cif file read
#
# cif file support in Nexus currently requires two external libraries
# PyCifRW - base interface to read cif files into object format: CifFile
# cif2cell - translation layer from CifFile object to cell reconstruction: CellData
# (note: cif2cell installation includes PyCifRW)
#
# installation of cif2cell
# go to http://sourceforge.net/projects/cif2cell/
# click on Download (example: cif2cell-1.2.10.tar.gz)
# unpack directory (tar -xzf cif2cell-1.2.10.tar.gz)
# enter directory (cd cif2cell-1.2.10)
# install cif2cell (python setup.py install)
# check python installation
# >python
# >>>from CifFile import CifFile
# >>>from uctools import CellData
#
# Nexus is currently compatible with
# cif2cell-1.2.10 and PyCifRW-3.3
# cif2cell-1.2.7 and PyCifRW-4.1.1
# compatibility last tested: 20 Mar 2017
#
try:
from CifFile import CifFile
except:
CifFile = unavailable('CifFile','CifFile')
#end try
try:
from cif2cell.uctools import CellData
except:
CellData = unavailable('cif2cell.uctools','CellData')
#end try
cif2cell_unit_dict = dict(angstrom='A',bohr='B',nm='nm')
def read_cif_celldata(filepath,block=None,grammar='1.1'):
# read cif file with PyCifRW
path,cif_file = os.path.split(filepath)
if path!='':
cwd = os.getcwd()
os.chdir(path)
#end if
cf = CifFile(cif_file,grammar=grammar)
#cf = ReadCif(cif_file,grammar=grammar)
if path!='':
os.chdir(cwd)
#end if
if block is None:
block = list(cf.keys())[0]
#end if
cb = cf.get(block)
if cb is None:
error('block {0} was not found in cif file {1}'.format(block,filepath),'read_cif_celldata')
#end if
# repack H-M symbols as normal strings so CellData.getFromCIF won't choke on unicode
#for k in ['_symmetry_space_group_name_H-M','_space_group_name_H-M_alt','_symmetry_space_group_name_h-m','_space_group_name_h-m_alt']:
# if k in cb.block:
# v = cb.block[k]
# if isinstance(v,(list,tuple)):
# for i in range(len(v)):
# if isinstance(v[i],unicode):
# v[i] = str(v[i])
# #end if
# #end for
# #end if
# #end if
##end for
# extract structure from CifFile with uctools CellData class
cd = CellData()
cd.getFromCIF(cb)
return cd
#end def read_cif_celldata
def read_cif_cell(filepath,block=None,grammar='1.1',cell='prim'):
cd = read_cif_celldata(filepath,block,grammar)
if cell.startswith('prim'):
cell = cd.primitive()
elif cell.startswith('conv'):
cell = cd.conventional()
else:
error('cell argument must be primitive or conventional\nyou provided: {0}'.format(cell),'read_cif_cell')
#end if
return cell
#end def read_cif_cell
def read_cif(filepath,block=None,grammar='1.1',cell='prim',args_only=False):
if isinstance(filepath,str):
cell = read_cif_cell(filepath,block,grammar,cell)
else:
cell = filepath
#end if
# create Structure object from cell
if cell.alloy:
error('cannot handle alloys','read_cif')
#end if
units = cif2cell_unit_dict[cell.unit]
scale = float(cell.lengthscale)
scale = convert(scale,units,'A')
units = 'A'
axes = scale*array(cell.latticevectors,dtype=float)
elem = []
pos = []
for wyckoff_atoms in cell.atomdata:
for atom in wyckoff_atoms:
elem.append(str(list(atom.species.keys())[0]))
pos.append(atom.position)
#end for
#end for
pos = dot(array(pos,dtype=float),axes)
if not args_only:
s = Structure(
axes = axes,
elem = elem,
pos = pos,
units = units
)
return s
else:
return axes,elem,pos,units
#end if
#end def read_cif
# installation instructions for spglib interface
#
# this is bootstrapped off of spglib's ASE Python interface
#
# installation of spglib
# go to http://sourceforge.net/projects/spglib/files/
# click on Download spglib-1.8.2.tar.gz (952.6 kB)
# unpack directory (tar -xzf spglib-1.8.2.tar.gz)
# enter ase directory (cd spglib-1.8.2/python/ase/)
# build and install (sudo python setup.py install)
from periodic_table import pt as ptable
#try:
# from pyspglib import spglib
#except:
# spglib = unavailable('pyspglib','spglib')
##end try
try:
import spglib
except:
spglib = unavailable('spglib')
#end try
def equate(expr):
return expr
#end def equate
def negate(expr):
return not expr
#end def negate
def kmesh(kaxes,dim,shift=None):
'''
Create a Monkhorst-Pack k-point mesh
'''
if shift is None:
shift = (0.,0,0)
#end if
ndim = len(dim)
d = array(dim)
s = array(shift)
s.shape = 1,ndim
d.shape = 1,ndim
kp = empty((1,ndim),dtype=float)
kgrid = empty((d.prod(),ndim))
n=0
for k in range(dim[2]):
for j in range(dim[1]):
for i in range(dim[0]):
kp[:] = i,j,k
kp = dot((kp+s)/d,kaxes)
#kp = (kp+s)/d
kgrid[n] = kp
n+=1
#end for
#end for
#end for
return kgrid
#end def kmesh
def reduce_tilematrix(tiling):
tiling = array(tiling)
t = array(tiling,dtype=int)
if abs(tiling-t).sum()>1e-6:
Structure.class_error('requested tiling is non-integer\n tiling requested: '+str(tiling))
#end if
dim = len(t)
matrix_tiling = t.shape == (dim,dim)
if matrix_tiling:
if abs(det(t))==0:
Structure.class_error('requested tiling matrix is singular\ntiling requested: {0}'.format(t))
#end if
#find a tiling tuple from the tiling matrix
# do this by shearing the tiling matrix (or equivalently the tiled cell)
# until it is orthogonal (in the untiled cell axes)
# this is just rearranging triangular tiles of space to reshape the cell
# so that t1*t2*t3 = det(T) = det(A_tiled)/det(A_untiled)
#this way the atoms in the (perhaps oddly shaped) supercell can be
# obtained from simple translations of the untiled cell positions
T = t #tiling matrix
tilematrix = T.copy()
del t
tbar = identity(dim) #basis for shearing
dr = list(range(dim))
#dr = [1,0,2]
other = dim*[0] # other[d] = dimensions other than d
for d in dr:
other[d] = set(dr)-set([d])
#end for
#move each axis to be parallel to barred directions
# these are volume preserving shears of the supercell
# each shear keeps two cell face planes fixed while moving the others
tvecs = []
for dp in [(0,1,2),(2,0,1),(1,2,0),(2,1,0),(0,2,1),(1,0,2)]:
success = True
Tnew = array(T,dtype=float) #sheared/orthogonal tiling matrix
for d in dr:
tb = tbar[dp[d]]
t = T[d]
d2,d3 = other[d]
n = cross(Tnew[d2],Tnew[d3]) #vector normal to 2 cell faces
vol = dot(n,t)
bcomp = dot(n,tb)
if abs(bcomp)<1e-6:
success = False
break
#end if
tn = vol*1./bcomp*tb #new axis vector
Tnew[d] = tn
#end for
if success:
# apply inverse permutation, if needed
Tn = Tnew.copy()
for d in dr:
d2 = dp[d]
Tnew[d2] = Tn[d]
#end for
#the resulting tiling matrix should be diagonal and integer
tr = diag(Tnew)
nondiagonal = abs(Tnew-diag(tr)).sum()>1e-6
if nondiagonal:
Structure.class_error('could not find a diagonal tiling matrix for generating tiled coordinates')
#end if
tvecs.append(abs(tr))
#end if
#end for
tvecs_old = tvecs
tvecs = []
tvset = set()
for tv in tvecs_old:
tvk = tuple(array(around(1e7*tv),dtype=uint64))
if tvk not in tvset:
tvset.add(tvk)
tvecs.append(tv)
#end if
#end for
tilevector = array(tvecs)
else:
tilevector = t
tilematrix = diag(t)
#end if
return tilematrix,tilevector
#end def reduce_tilematrix
def rotate_plane(plane,angle,points,units='degrees'):
if units=='degrees':
angle *= pi/180
elif not units.startswith('rad'):
error('angular units must be degrees or radians\nyou provided: {0}'.format(angle),'rotate_plane')
#end if
c = cos(angle)
s = sin(angle)
if plane=='xy':
R = [[ c,-s, 0],
[ s, c, 0],
[ 0, 0, 1]]
elif plane=='yx':
R = [[ c, s, 0],
[-s, c, 0],
[ 0, 0, 1]]
elif plane=='yz':
R = [[ 1, 0, 0],
[ 0, c,-s],
[ 0, s, c]]
elif plane=='zy':
R = [[ 1, 0, 0],
[ 0, c, s],
[ 0,-s, c]]
elif plane=='zx':
R = [[ c, 0, s],
[ 0, 1, 0],
[-s, 0, c]]
elif plane=='xz':
R = [[ c, 0,-s],
[ 0, 1, 0],
[ s, 0, c]]
else:
error('plane must be xy/yx/yz/zy/zx/xz\nyou provided: {0}'.format(plane),'rotate_plane')
#end if
R = array(R,dtype=float)
return dot(R,points.T).T
#end def rotate_plane
opt_tm_matrices = obj()
opt_tm_wig_indices = obj()
def trivial_filter(T):
return True
#end def trival_filter
class MaskFilter(DevBase):
def set(self,mask,dim=3):
omask = array(mask)
mask = array(mask,dtype=bool)
if mask.size==dim:
mvec = mask.ravel()
mask = empty((dim,dim),dtype=bool)
i=0
for mi in mvec:
j=0
for mj in mvec:
mask[i,j] = mi==mj
j+=1
#end for
i+=1
#end for
elif mask.shape!=(dim,dim):
error('shape of mask array must be {0},{0}\nshape received: {1},{2}\nmask array received: {3}'.format(dim,mask.shape[0],mask.shape[1],omask),'optimal_tilematrix')
#end if
self.mask = mask==False
#end def set
def __call__(self,T):
return (T[self.mask]==0).all()
#end def __call__
#end class MaskFilter
mask_filter = MaskFilter()
def optimal_tilematrix(axes,volfac,dn=1,tol=1e-3,filter=trivial_filter,mask=None,nc=5,Tref=None):
if mask is not None:
mask_filter.set(mask)
filter = mask_filter
#end if
dim = 3
if isinstance(axes,Structure):
axes = axes.axes
else:
axes = array(axes,dtype=float)
#end if
if not isinstance(volfac,int):
volfac = int(around(volfac))
#end if
volume = abs(det(axes))*volfac
axinv = inv(axes)
cube = volume**(1./3)*identity(dim)
if Tref is None:
Tref = array(around(dot(cube,axinv)),dtype=int)
else:
Tref = np.asarray(Tref)
#end if
# calculate and store all tiling matrix variations
if dn not in opt_tm_matrices:
mats = []
rng = tuple(range(-dn,dn+1))
for n1 in rng:
for n2 in rng:
for n3 in rng:
for n4 in rng:
for n5 in rng:
for n6 in rng:
for n7 in rng:
for n8 in rng:
for n9 in rng:
mats.append((n1,n2,n3,n4,n5,n6,n7,n8,n9))
#end for
#end for
#end for
#end for
#end for
#end for
#end for
#end for
#end for
mats = array(mats,dtype=int)
mats.shape = (2*dn+1)**(dim*dim),dim,dim
opt_tm_matrices[dn] = mats
else:
mats = opt_tm_matrices[dn]
#end if
# calculate and store all wigner image indices
if nc not in opt_tm_wig_indices:
inds = []
rng = tuple(range(-nc,nc+1))
for k in rng:
for j in rng:
for i in rng:
if i!=0 or j!=0 or k!=0:
inds.append((i,j,k))
#end if
#end for
#end for
#end for
inds = array(inds,dtype=int)
opt_tm_wig_indices[nc] = inds
else:
inds = opt_tm_wig_indices[nc]
#end if
# track counts of tiling matrices
ntilings = len(mats)
nequiv_volume = 0
nfilter = 0
nequiv_inscribe = 0
nequiv_wigner = 0
nequiv_cubicity = 0
nequiv_shape = 0
# try a faster search for cells w/ target volume
det_inds_p = [
[(0,0),(1,1),(2,2)],
[(0,1),(1,2),(2,0)],
[(0,2),(1,0),(2,1)]
]
det_inds_m = [
[(0,0),(1,2),(2,1)],
[(0,1),(1,0),(2,2)],
[(0,2),(1,1),(2,0)]
]
volfacs = zeros((len(mats),),dtype=int)
for (i1,j1),(i2,j2),(i3,j3) in det_inds_p:
volfacs += (Tref[i1,j1]+mats[:,i1,j1])*(Tref[i2,j2]+mats[:,i2,j2])*(Tref[i3,j3]+mats[:,i3,j3])
#end for
for (i1,j1),(i2,j2),(i3,j3) in det_inds_m:
volfacs -= (Tref[i1,j1]+mats[:,i1,j1])*(Tref[i2,j2]+mats[:,i2,j2])*(Tref[i3,j3]+mats[:,i3,j3])
#end for
Tmats = mats[abs(volfacs)==volfac]
nequiv_volume = len(Tmats)
# find the set of cells with maximal inscribing radius
inscribe_tilings = []
rmax = -1e99
for mat in Tmats:
T = Tref + mat
if filter(T):
nfilter+=1
Taxes = dot(T,axes)
rc1 = norm(cross(Taxes[0],Taxes[1]))
rc2 = norm(cross(Taxes[1],Taxes[2]))
rc3 = norm(cross(Taxes[2],Taxes[0]))
r = 0.5*volume/max(rc1,rc2,rc3) # inscribing radius
if r>rmax or abs(r-rmax)<tol:
inscribe_tilings.append((r,T,Taxes))
rmax = r
#end if
#end if
#end for
# find the set of cells w/ maximal wigner radius out of the inscribing set
wigner_tilings = []
rwmax = -1e99
for r,T,Taxes in inscribe_tilings:
if abs(r-rmax)<tol:
nequiv_inscribe+=1
rw = 1e99
for ind in inds:
rw = min(rw,0.5*norm(dot(ind,Taxes)))
#end for
if rw>rwmax or abs(rw-rwmax)<tol:
wigner_tilings.append((rw,T,Taxes))
rwmax = rw
#end if
#end if
#end for
# find the set of cells w/ maximal cubicity
# (minimum cube_deviation)
cube_tilings = []
cmin = 1e99
for rw,T,Ta in wigner_tilings:
if abs(rw-rwmax)<tol:
nequiv_wigner+=1
dc = volume**(1./3)*sqrt(2.)
d1 = abs(norm(Ta[0]+Ta[1])-dc)
d2 = abs(norm(Ta[1]+Ta[2])-dc)
d3 = abs(norm(Ta[2]+Ta[0])-dc)
d4 = abs(norm(Ta[0]-Ta[1])-dc)
d5 = abs(norm(Ta[1]-Ta[2])-dc)
d6 = abs(norm(Ta[2]-Ta[0])-dc)
cube_dev = (d1+d2+d3+d4+d5+d6)/(6*dc)
if cube_dev<cmin or abs(cube_dev-cmin)<tol:
cube_tilings.append((cube_dev,rw,T,Ta))
cmin = cube_dev
#end if
#end if
#end for
# prioritize selection by "shapeliness" of tiling matrix
# prioritize positive diagonal elements
# penalize off-diagonal elements
# penalize negative off-diagonal elements
shapely_tilings = []
smax = -1e99
for cd,rw,T,Taxes in cube_tilings:
if abs(cd-cmin)<tol:
nequiv_cubicity+=1
d = diag(T)
o = (T-diag(d)).ravel()
s = sign(d).sum()-(abs(o)>0).sum()-(o<0).sum()
if s>smax or abs(s-smax)<tol:
shapely_tilings.append((s,rw,T,Taxes))
smax = s
#end if
#end if
#end for
# prioritize selection by symmetry of tiling matrix
ropt = -1e99
Topt = None
Taxopt = None
diagonal = []
symmetric = []
antisymmetric = []
other = []
for s,rw,T,Taxes in shapely_tilings:
if abs(s-smax)<tol:
nequiv_shape+=1
Td = diag(diag(T))
if abs(Td-T).sum()==0:
diagonal.append((rw,T,Taxes))
elif abs(T.T-T).sum()==0:
symmetric.append((rw,T,Taxes))
elif abs(T.T+T-2*Td).sum()==0:
antisymmetric.append((rw,T,Taxes))
else:
other.append((rw,T,Taxes))
#end if
#end if
#end for
s = 1
if len(diagonal)>0:
cells = diagonal
elif len(symmetric)>0:
cells = symmetric
elif len(antisymmetric)>0:
cells = antisymmetric
s = -1
elif len(other)>0:
cells = other
else:
cells = []
#end if
skew_min = 1e99
if len(cells)>0:
for rw,T,Taxes in cells:
Td = diag(diag(T))
skew = abs(T.T-s*T-(1-s)*Td).sum()
if skew<skew_min:
ropt = rw
Topt = T
Taxopt = Taxes
skew_min = skew
#end if
#end for
#end if
if Taxopt is None:
error('optimal tilematrix for volfac={0} not found with tolerance {1}\ndifference range (dn): {2}\ntiling matrices searched: {3}\ncells with target volume: {4}\ncells that passed the filter: {5}\ncells with equivalent inscribing radius: {6}\ncells with equivalent wigner radius: {7}\ncells with equivalent cubicity: {8}\nmatrices with equivalent shapeliness: {9}\nplease try again with dn={10}'.format(volfac,tol,dn,ntilings,nequiv_volume,nfilter,nequiv_inscribe,nequiv_wigner,nequiv_cubicity,nequiv_shape,dn+1))
#end if
if det(Taxopt)<0:
Topt = -Topt
#end if
return Topt,ropt
#end def optimal_tilematrix
class Sobj(DevBase):
None
#end class Sobj
class Structure(Sobj):
operations = obj()
@classmethod
def set_operations(cls):
cls.operations.set(
remove_folded_structure = cls.remove_folded_structure,
recenter = cls.recenter,
)
#end def set_operations
def __init__(self,
axes = None,
scale = 1.,
elem = None,
pos = None,
elem_pos = None,
mag = None,
center = None,
kpoints = None,
kweights = None,
kgrid = None,
kshift = None,
permute = None,
units = None,
tiling = None,
rescale = True,
dim = 3,
magnetization = None,
operations = None,
background_charge = 0,
frozen = None,
bconds = None,
posu = None,
use_prim = None,
add_kpath = False,
symm_kgrid = False,
):
if isinstance(axes,str):
axes = array(axes.split(),dtype=float)
axes.shape = dim,dim
#end if
if center is None:
if axes is not None:
center = array(axes,dtype=float).sum(0)/2
else:
center = dim*[0]
#end if
#end if
if bconds is None or bconds=='periodic':
bconds = dim*['p']
#end if
if axes is None:
axes = []
bconds = []
#end if
if elem_pos is not None:
ep = array(elem_pos.split(),dtype=str)
ep.shape = ep.size//(dim+1),(dim+1)
elem = ep[:,0].ravel()
pos = ep[:,1:dim+1]
#end if
if elem is None:
elem = []
#end if
if posu is not None:
pos = posu
#end if
if pos is None:
pos = empty((0,dim))
#end if
if kshift is None:
kshift = 0,0,0
#end if
self.scale = 1.
self.units = units
self.dim = dim
self.center = array(center,dtype=float)
self.axes = array(axes,dtype=float)
self.set_bconds(bconds)
self.set_elem(elem)
self.set_pos(pos)
self.set_mag(mag)
self.set_frozen(frozen)
self.kpoints = empty((0,dim))
self.kweights = empty((0,))
self.background_charge = background_charge
self.remove_folded_structure()
if len(axes)==0:
self.kaxes=array([])
else:
self.kaxes=2*pi*inv(self.axes).T
#end if
if posu is not None:
self.pos_to_cartesian()
#end if
if magnetization is not None:
self.magnetize(magnetization)
#end if
if use_prim is not None and use_prim is not False:
self.become_primitive(source=use_prim,add_kpath=add_kpath)
#end if
if tiling is not None:
self.tile(tiling,in_place=True)
#end if
if kpoints is not None:
self.add_kpoints(kpoints,kweights)
#end if
if kgrid is not None:
if not symm_kgrid:
self.add_kmesh(kgrid,kshift)
else:
self.add_symmetrized_kmesh(kgrid,kshift)
#end if
#end if
if rescale:
self.rescale(scale)
else:
self.scale = scale
#end if
if permute is not None:
self.permute(permute)
#end if
if operations is not None:
self.operate(operations)
#end if
#end def __init__
def check_consistent(self,tol=1e-8,exit=True,message=False):
msg = ''
if self.has_axes():
kaxes = 2*pi*inv(self.axes).T
abs_diff = abs(self.kaxes-kaxes).sum()
if abs_diff>tol:
msg += 'Direct and reciprocal space axes are not consistent.\naxes present:\n{0}\nkaxes present:\n{1}\nConsistent kaxes:\n{2}\nAbsolute difference: {3}\n'.format(self.axes,self.kaxes,kaxes,abs_diff)
#end if
#end if
N = len(self.elem)
D = self.dim
pshape = (N,D)
if self.pos.shape!=pshape:
msg += 'pos is not the right shape\npos shape: {}\nCorrect shape: {}\n'.format(self.pos.shape,pshape)
#end if
if self.mag is not None and len(self.mag)!=N:
msg += 'mag does not have the right length\nmag length: {}\nCorrect length: {}\n'.format(self.mag,N)
#end if
if self.frozen is not None and self.frozen.shape!=pshape:
msg += 'frozen is not the right shape\nfrozen shape: {}\nCorrect shape: {}\n'.format(self.frozen.shape,pshape)
#end if
consistent = len(msg)==0
if not consistent and exit:
self.error(msg)
#end if
if not message:
return consistent
else:
return consistent,msg
#end if
#end def check_consistent
def set_axes(self,axes):
self.reset_axes(axes)
#end def set_axes
def set_bconds(self,bconds):
self.bconds = array(tuple(bconds),dtype=str)
#end def bconds
def set_elem(self,elem):
self.elem = array(elem,dtype=object)
#end def set_elem
def set_pos(self,pos):
self.pos = array(pos,dtype=float)
if len(self.pos)!=len(self.elem):
self.error('Atomic positions must have same length as elem.\nelem length: {}\nAtomic positions length: {}\n'.format(len(self.elem),len(self.pos)))
#end if
#end def set_pos
def set_mag(self,mag=None):
if mag is None:
self.mag = None
else:
self.mag = np.array(mag,dtype=object)
if len(self.mag)!=len(self.elem):
self.error('Magnetic moments must have same length as elem.\nelem length: {}\nMagnetic moments length: {}\n'.format(len(self.elem),len(self.mag)))
#end if
#end if
#end def set_mag
def set_frozen(self,frozen=None):
if frozen is None:
self.frozen = None
else:
self.frozen = np.array(frozen,dtype=bool)
if self.frozen.shape!=self.pos.shape:
self.error('Frozen directions must have the same shape as positions.\nPositions shape: {0}\nFrozen directions shape: {1}'.format(self.pos.shape,self.frozen.shape))
#end if
#end if
#end def set_frozen
def size(self):
return len(self.elem)
#end def size
def has_axes(self):
return len(self.axes)==self.dim
#end def has_axes
def operate(self,operations):
for op in operations:
if not op in self.operations:
self.error('{0} is not a known operation\nvalid options are:\n {1}'.format(op,list(self.operations.keys())))
else:
self.operations[op](self)
#end if
#end for
#end def operate
def has_tmatrix(self):
return 'tmatrix' in self and self.tmatrix is not None
#end def has_tmatrix
def is_tiled(self):
return self.has_folded() and self.has_tmatrix()
#end def is_tiled
def set_folded(self,folded):
self.set_folded_structure(folded)
#end def set_folded
def remove_folded(self):
self.remove_folded_structure()
#end def remove_folded
def has_folded(self):
return self.has_folded_structure()
#end def has_folded
def set_folded_structure(self,folded):
if not isinstance(folded,Structure):
self.error('cannot set folded structure\nfolded structure must be an object with type Structure\nreceived type: {0}'.format(folded.__class__.__name__))
#end if
self.folded_structure = folded
if self.has_axes():
self.tmatrix = self.tilematrix(folded)
#end if
#end def set_folded_structure
def remove_folded_structure(self):
self.folded_structure = None
if 'tmatrix' in self:
del self.tmatrix
#end if
#end def remove_folded_structure
def has_folded_structure(self):
return self.folded_structure is not None
#end def has_folded_structure
# test needed
def group_atoms(self,folded=True):
if len(self.elem)>0:
order = self.elem.argsort()
if (self.elem!=self.elem[order]).any():
self.elem = self.elem[order]
self.pos = self.pos[order]
#end if
#end if
if self.folded_structure!=None and folded:
self.folded_structure.group_atoms(folded)
#end if
#end def group_atoms
# test needed
def rename(self,folded=True,**name_pairs):
elem = self.elem
for old,new in name_pairs.items():
for i in range(len(self.elem)):
if old==elem[i]:
elem[i] = new
#end if
#end for
#end for
if self.folded_structure!=None and folded:
self.folded_structure.rename(folded=folded,**name_pairs)
#end if
#end def rename
# test needed
def reset_axes(self,axes=None):
if axes is None:
axes = self.axes
else:
axes = array(axes)
self.remove_folded_structure()
#end if
self.axes = axes
self.kaxes = 2*pi*inv(axes).T
self.center = axes.sum(0)/2
#end def reset_axes
# test needed
def adjust_axes(self,axes):
self.skew(dot(inv(self.axes),axes))
#end def adjust_axes
# test needed
def reshape_axes(self,reshaping):
R = array(reshaping)
if abs(abs(det(R))-1)<1e-6:
self.axes = dot(self.axes,R)
else:
R = dot(inv(self.axes),R)
if abs(abs(det(R))-1)<1e-6:
self.axes = dot(self.axes,R)
else:
self.error('reshaping matrix must not change the volume\n reshaping matrix:\n {0}\n volume change ratio: {1}'.format(R,abs(det(R))))
#end if
#end if
#end def reshape_axes
def write_axes(self):
c = ''
for a in self.axes:
c+='{0:12.8f} {1:12.8f} {2:12.8f}\n'.format(a[0],a[1],a[2])
#end for
return c
#end def write_axes
def corners(self):
a = self.axes
c = array([(0,0,0),
a[0],
a[1],
a[2],
a[0]+a[1],
a[1]+a[2],
a[2]+a[0],
a[0]+a[1]+a[2],
])
return c
#end def corners
# test needed
def miller_direction(self,h,k,l,normalize=False):
d = dot((h,k,l),self.axes)
if normalize:
d/=norm(d)
#end if
return d
#end def miller_direction
# test needed
def miller_normal(self,h,k,l,normalize=False):
d = dot((h,k,l),self.kaxes)
if normalize:
d/=norm(d)
#end if
return d
#end def miller_normal
# test needed
def project_plane(self,a1,a2,points=None):
# a1/a2: in plane vectors
if points is None:
points = self.pos
#end if
a1n = norm(a1)
a2n = norm(a2)
a1/=a1n
a2/=a2n
n = cross(a1,a2)
plane_coords = []
for p in points:
p -= dot(n,p)*n # project point into plane
c1 = dot(a1,p)/a1n
c2 = dot(a2,p)/a2n
plane_coords.append((c1,c2))
#end for
return array(plane_coords,dtype=float)
#end def project_plane
def bounding_box(self,scale=1.0,minsize=None,mindist=0,box='tight',recenter=False):
pmin = self.pos.min(0)-mindist
pmax = self.pos.max(0)+mindist
pcenter = (pmax+pmin)/2
prange = pmax-pmin
if minsize is not None:
for i,pr in enumerate(prange):
prange[i] = max(minsize,prange[i])
#end for
#end if
if box=='tight':
axes = diag(prange)
elif box=='cubic' or box=='cube':
prmax = prange.max()
axes = diag((prmax,prmax,prmax))
elif isinstance(box,ndarray) or isinstance(box,list):
box = array(box)
if box.shape!=(3,3):
self.error('requested box must be 3-dimensional (3x3 axes)\n you provided: '+str(box)+'\n shape: '+str(box.shape))
#end if
binv = inv(box)
pu = dot(self.pos,binv)
pmin = pu.min(0)
pmax = pu.max(0)
pcenter = (pmax+pmin)/2
prange = pmax-pmin
axes = dot(diag(prange),box)
else:
self.error("invalid request for box\n valid options are 'tight', 'cubic', or axes array (3x3)\n you provided: "+str(box))
#end if
self.reset_axes(scale*axes)
self.slide(self.center-pcenter,recenter)
#end def bounding_box
def center_molecule(self):
self.slide(self.center-self.pos.mean(0),recenter=False)
#end def center_molecule
# test needed
def center_solid(self):
u = self.pos_unit()
du = (1-u.min(0)-u.max(0))/2
self.slide(dot(du,self.axes),recenter=False)
#end def center_solid
# test needed
def permute(self,permutation):
dim = self.dim
P = empty((dim,dim),dtype=int)
if len(permutation)!=dim:
self.error(' permutation vector must have {0} elements\n you provided {1}'.format(dim,permutation))
#end if
for i in range(dim):
p = permutation[i]
pv = zeros((dim,),dtype=int)
if p=='x' or p=='0':
pv[0] = 1
elif p=='y' or p=='1':
pv[1] = 1
elif p=='z' or p=='2':
pv[2] = 1
#end if
P[:,i] = pv[:]
#end for
self.center = dot(self.center,P)
if self.has_axes():
self.axes = dot(self.axes,P)
#end if
if len(self.pos)>0:
self.pos = dot(self.pos,P)
#end if
if len(self.kaxes)>0:
self.kaxes = dot(self.kaxes,P)
#end if
if len(self.kpoints)>0:
self.kpoints = dot(self.kpoints,P)
#end if
if self.folded_structure!=None:
self.folded_structure.permute(permutation)
#end if
#end def permute
# test needed
def rotate_plane(self,plane,angle,units='degrees'):
self.pos = rotate_plane(plane,angle,self.pos,units)
if self.has_axes():
axes = rotate_plane(plane,angle,self.axes,units)
self.reset_axes(axes)
#end if
#end def rotate_plane
# test needed
def upcast(self,DerivedStructure):
if not issubclass(DerivedStructure,Structure):
self.error(DerivedStructure.__name__,'is not derived from Structure')
#end if
ds = DerivedStructure()
for name,value in self.items():
ds[name] = deepcopy(value)
#end for
return ds
#end def upcast
# test needed
def incorporate(self,other):
self.set_elem(list(self.elem)+list(other.elem))
self.pos=array(list(self.pos)+list(other.pos))
#end def incorporate
# test needed
def clone_from(self,other):
if not isinstance(other,Structure):
self.error('cloning failed\ncan only clone from other Structure objects\nreceived object of type: {0}'.format(other.__class__.__name__))
#end if
o = other.copy()
self.__dict__ = o.__dict__
#end def clone_from
# test needed
def add_atoms(self,elem,pos):
self.set_elem(list(self.elem)+list(elem))
self.pos=array(list(self.pos)+list(pos))
#end def add_atoms
def is_open(self):
return not self.any_periodic()
#end def is_open
def is_periodic(self):
return self.any_periodic()
#end def is_periodic
def any_periodic(self):
has_cell = self.has_axes()
pbc = False
for bc in self.bconds:
pbc |= bc=='p'
#end if
periodic = has_cell and pbc
return periodic
#end def any_periodic
def all_periodic(self):
has_cell = self.has_axes()
pbc = True
for bc in self.bconds:
pbc &= bc=='p'
#end if
periodic = has_cell and pbc
return periodic
#end def all_periodic
# test needed
def distances(self,pos1=None,pos2=None):
if isinstance(pos1,Structure):
pos1 = pos1.pos
#end if
if pos2 is None:
if pos1 is None:
return sqrt((self.pos**2).sum(1))
else:
pos2 = self.pos
#end if
#end if
if len(pos1)!=len(pos2):
self.error('positions arrays are not the same length')
#end if
return sqrt(((pos1-pos2)**2).sum(1))
#end def distances
def count_kshells(self, kcut, tilevec=[12, 12, 12], nkdig=10):
# check tilevec input
for nt in tilevec:
if nt % 2 != 0:
msg = 'tilevec must contain even integers'
msg += ' so that kgrid can be zero centered.'
Structure.class_error(msg, 'count_kshells')
#end if
#end for
origin = np.array([[0, 0, 0]])
axes = self.axes
raxes = 2*np.pi*np.linalg.inv(axes).T
kvecs = self.tile_points(origin, raxes, tilevec)
kvecs -= np.dot(tilevec, raxes)/2 # center around 0
kmags = np.linalg.norm(kvecs, axis=-1)
# make sure tilevec is sufficient for kcut
klimit = 0.5*kmags.max()
if kcut > klimit:
msg = 'kcut %3.2f > klimit=%3.2f\n' % (kcut, klimit)
msg += ' please increase tilevec to be safe.\n'
Structure.class_error(msg, 'count_kshells')
#end if
sel = (0<kmags) & (kmags<kcut)
ukmags = np.unique(kmags[sel].round(nkdig))
return len(ukmags)
#end def count_kshells
def volume(self):
if not self.has_axes():
return None
else:
return abs(det(self.axes))
#end if
#end def volume
def rwigner(self,nc=5):
if self.dim!=3:
self.error('rwigner is currently only implemented for 3 dimensions')
#end if
rmin = 1e90
n=empty((1,3))
rng = tuple(range(-nc,nc+1))
for k in rng:
for j in rng:
for i in rng:
if i!=0 or j!=0 or k!=0:
n[:] = i,j,k
rmin = min(rmin,.5*norm(dot(n,self.axes)))
#end if
#end for
#end for
#end for
return rmin
#end def rwigner
def rinscribe(self):
if self.dim!=3:
self.error('rinscribe is currently only implemented for 3 dimensions')
#end if
radius = 1e99
dim=3
axes=self.axes
for d in range(dim):
i = d
j = (d+1)%dim
rc = cross(axes[i,:],axes[j,:])
radius = min(radius,.5*abs(det(axes))/norm(rc))
#end for
return radius
#end def rinscribe
# test needed
def rwigner_cube(self,*args,**kwargs):
cube = Structure()
a = self.volume()**(1./3)
cube.set_axes([[a,0,0],[0,a,0],[0,0,a]])
return cube.rwigner(*args,**kwargs)
#end def rwigner_cube
# test needed
def rinscribe_cube(self,*args,**kwargs):
cube = Structure()
a = self.volume()**(1./3)
cube.set_axes([[a,0,0],[0,a,0],[0,0,a]])
return cube.rinscribe(*args,**kwargs)
#end def rinscribe_cube
def rmin(self):
return self.rwigner()
#end def rmin
def rcell(self):
return self.rinscribe()
#end def rcell
# test needed
# scale invariant measure of deviation from cube shape
# based on deviation of face diagonals from cube
def cube_deviation(self):
a = self.axes
dc = self.volume()**(1./3)*sqrt(2.)
d1 = abs(norm(a[0]+a[1])-dc)
d2 = abs(norm(a[1]+a[2])-dc)
d3 = abs(norm(a[2]+a[0])-dc)
d4 = abs(norm(a[0]-a[1])-dc)
d5 = abs(norm(a[1]-a[2])-dc)
d6 = abs(norm(a[2]-a[0])-dc)
return (d1+d2+d3+d4+d5+d6)/(6*dc)
#end def cube_deviation
# test needed
# apply volume preserving shear-removing transformations to cell axes
# resulting unsheared cell has orthogonal axes
# while remaining periodically correct
# note that the unshearing procedure is not unique
# it depends on the order of unshearing operations
def unsheared_axes(self,axes=None,distances=False):
if self.dim!=3:
self.error('unsheared_axes is currently only implemented for 3 dimensions')
#end if
if axes is None:
axes = self.axes
#end if
dim=3
axbar = identity(dim)
axnew = array(axes,dtype=float)
dists = empty((dim,))
for d in range(dim):
d2 = (d+1)%dim
d3 = (d+2)%dim
n = cross(axnew[d2],axnew[d3]) #vector normal to 2 cell faces
axdist = dot(n,axes[d])/dot(n,axbar[d])
axnew[d] = axdist*axbar[d]
dists[d] = axdist
#end for
if not distances:
return axnew
else:
return axnew,dists
#end if
#end def unsheared_axes
# test needed
# vectors parallel to cell faces
# length of vectors is distance between parallel face planes
# note that the product of distances is not the cell volume in general
# see "unsheared_axes" function
# (e.g. a volume preserving shear may bring two face planes arbitrarily close)
def face_vectors(self,axes=None,distances=False):
if axes is None:
axes = self.axes
#end if
fv = inv(axes).T
for d in range(len(fv)):
fv[d] /= norm(fv[d]) # face normals
#end for
dv = dot(axes,fv.T) # axis projections onto face normals
fv = dot(dv,fv) # face normals lengthened by plane separation
if not distances:
return fv
else:
return fv,diag(dv)
#end if
#end def face_vectors
# test needed
def face_distances(self):
return self.face_vectors(distances=True)[1]
#end def face_distances
# test needed
def rescale(self,scale):
self.scale *= scale
self.axes *= scale
self.pos *= scale
self.center *= scale
self.kaxes /= scale
self.kpoints/= scale
if self.folded_structure!=None:
self.folded_structure.rescale(scale)
#end if
#end def rescale
# test needed
def stretch(self,s1,s2,s3):
if self.dim!=3:
self.error('stretch is currently only implemented for 3 dimensions')
#end if
d = diag((s1,s2,s3))
self.skew(d)
#end def stretch
# test needed
def rotate(self,r,rp=None,passive=False,units="radians",check=True):
"""
Arbitrary rotation of the structure.
Parameters
----------
r : `array_like, float, shape (3,3)` or `array_like, float, shape (3,)` or `str`
If a 3x3 matrix, then code executes rotation consistent with this matrix --
it is assumed that the matrix acts on a column-major vector (eg, v'=Rv)
If a three-dimensional array, then the operation of the function depends
on the input type of rp in the following ways:
1. If rp is a scalar, then rp is assumed to be an angle and a rotation
of rp is made about the axis defined by r
2. If rp is a vector, then rp is assumed to be an axis and a rotation is made
such that r aligns with rp
3. If rp is a str, then the rotation is such that r aligns with the
axis given by the str ('x', 'y', 'z', 'a0', 'a1', or 'a2')
If a str then the axis, r, is defined by the input label (e.g. 'x', 'y', 'z', 'a1', 'a2', or 'a3')
and the operation of the function depends on the input type of rp in the following
ways (same as above):
1. If rp is a scalar, then rp is assumed to be an angle and a rotation
of rp is made about the axis defined by r
2. If rp is a vector, then rp is assumed to be an axis and a rotation is made
such that r aligns with rp
3. If rp is a str, then the rotation is such that r aligns with the
axis given by the str ('x', 'y', 'z', 'a0', 'a1', or 'a2')
rp : `array_like, float, shape (3), optional` or `str, optional`
If a 3-dimensional vector is given, then rp is assumed to be an axis and a rotation is made
such that the axis r is aligned with rp.
If a str, then rp is assumed to be an angle and a rotation about the axis defined by r
is made by an angle rp
If a str is given, then rp is assumed to be an axis defined by the given label
(e.g. 'x', 'y', 'z', 'a1', 'a2', or 'a3') and a rotation is made such that the axis r
is aligned with rp.
passive : `bool, optional, default False`
If `True`, perform a passive rotation
If `False`, perform an active rotation
units : `str, optional, default "radians"`
Units of rp, if rp is given as an angle (scalar)
check : `bool, optional, default True`
Perform a check to verify rotation matrix is orthogonal
"""
if rp is not None:
dirmap = dict(x=[1,0,0],y=[0,1,0],z=[0,0,1])
if isinstance(r,str):
if r[0]=='a': # r= 'a0', 'a1', or 'a2'
r = self.axes[int(r[1])]
else: # r= 'x', 'y', or 'z'
r = dirmap[r]
#end if
else:
r = array(r,dtype=float)
if len(r.shape)>1:
self.error('r must be given as a 1-d vector or string, if rp is not None')
#end if
#end if
if isinstance(rp,(int,float)):
if units=="radians" or units=="rad":
theta = float(rp)
else:
theta = float(rp)*np.pi/180.0
#end if
c = np.cos(theta)
s = np.sin(theta)
else:
if isinstance(rp,str):
if rp[0]=='a': # rp= 'a0', 'a1', or 'a2'
rp = self.axes[int(rp[1])]
else: # rp= 'x', 'y', or 'z'
rp = dirmap[rp]
#end if
else:
rp = array(rp,dtype=float)
#end if
# go from r,rp to r,theta
c = np.dot(r,rp)/np.linalg.norm(r)/np.linalg.norm(rp)
if abs(c-1)<1e-6:
s = 0.0
r = np.array([1,0,0])
else:
s = np.dot(np.cross(r,rp),np.cross(r,rp))/np.linalg.norm(r)/np.linalg.norm(rp)/np.linalg.norm(np.cross(r,rp))
r = np.cross(r,rp)/np.linalg.norm(np.cross(r,rp))
#end if
#end if
# make R from r,theta
R = [[ c+r[0]**2.0*(1.0-c), r[0]*r[1]*(1.0-c)-r[2]*s, r[0]*r[2]*(1.0-c)+r[1]*s],
[r[1]*r[0]*(1.0-c)+r[2]*s, c+r[1]**2.0*(1.0-c), r[1]*r[2]*(1.0-c)-r[0]*s],
[r[2]*r[0]*(1.0-c)-r[1]*s, r[2]*r[1]*(1.0-c)+r[0]*s, c+r[2]**2.0*(1.0-c)]]
else:
R = r
#end if
R = array(R,dtype=float)
if passive:
R = R.T
#end if
if check:
if not np.allclose(dot(R,R.T),identity(len(R))):
self.error('the function, rotate, must be given an orthogonal matrix')
#end if
#end if
self.matrix_transform(R)
#end def rotate
# test needed
def matrix_transform(self,A):
"""
Arbitrary transformation matrix (column-major).
Parameters
----------
A : `array_like, float, shape (3,3)`
Transform the structure using the matrix A. It is assumed that
A is in column-major form, i.e., it transforms a vector v as
v' = Av
"""
A = A.T
axinv = inv(self.axes)
axnew = dot(self.axes,A)
kaxinv = inv(self.kaxes)
kaxnew = dot(self.kaxes,inv(A).T)
self.pos = dot(dot(self.pos,axinv),axnew)
self.center = dot(dot(self.center,axinv),axnew)
self.kpoints = dot(dot(self.kpoints,kaxinv),kaxnew)
self.axes = axnew
self.kaxes = kaxnew
if self.folded_structure!=None:
self.folded_structure.matrix_transform(A.T)
#end if
#end def matrix_transform
# test needed
def skew(self,skew):
"""
Arbitrary transformation matrix (row-major).
Parameters
----------
skew : `array_like, float, shape (3,3)`
Transform the structure using the matrix skew. It is assumed that
skew is in row-major form, i.e., it transforms a vector v as
v' = vT
"""
self.matrix_transform(skew.T)
#end def skew
# test needed
def change_units(self,units,folded=True):
if units!=self.units:
scale = convert(1,self.units,units)
self.scale *= scale
self.axes *= scale
self.pos *= scale
self.center *= scale
self.kaxes /= scale
self.kpoints/= scale
self.units = units
#end if
if self.folded_structure!=None and folded:
self.folded_structure.change_units(units,folded=folded)
#end if
#end def change_units
# test needed
# insert sep space at loc along axis
# if sep<0, space is removed instead
def cleave(self,axis,loc,sep=None,remove=False,tol=1e-6):
self.remove_folded_structure()
if isinstance(axis,int):
if sep is None:
self.error('separation induced by cleave must be provided')
#end if
v = self.face_vectors()[axis]
if isinstance(loc,float):
c = loc*v/norm(v)
#end if
else:
v = axis
#end if
c = array(c) # point on cleave plane
v = array(v) # normal vector to cleave plane, norm is cleave separation
if sep!=None:
v = abs(sep)*v/norm(v)
#end if
if norm(v)<tol:
return
#end if
vn = array(v/norm(v))
if sep!=None and sep<0:
v = -v # preserve the normal direction for atom identification, but reverse the shift direction
#end if
self.recorner() # want box contents to be static
if self.has_axes():
components = 0
dim = self.dim
axes = self.axes
for i in range(dim):
i2 = (i+1)%dim
i3 = (i+2)%dim
a2 = axes[i2]/norm(axes[i2])
a3 = axes[i3]/norm(axes[i3])
comp = abs(dot(a2,vn))+abs(dot(a3,vn))
if comp < 1e-6:
components+=1
iaxis = i
#end if
#end for
commensurate = components==1
if not commensurate:
self.error('cannot insert vacuum because cleave is incommensurate with the cell\n cleave plane must be parallel to a cell face')
#end if
a = self.axes[iaxis]
#self.axes[iaxis] = (1.+dot(v,a)/dot(a,a))*a
self.axes[iaxis] = (1.+dot(v,v)/dot(v,a))*a
#end if
indices = []
pos = self.pos
for i in range(len(pos)):
p = pos[i]
comp = dot(p-c,vn)
if comp>0 or abs(comp)<tol:
pos[i] += v
indices.append(i)
#end if
#end for
if remove:
self.remove(indices)
#end if
#end def cleave
# test needed
def translate(self,v):
v = array(v)
pos = self.pos
for i in range(len(pos)):
pos[i]+=v
#end for
self.center+=v
if self.folded_structure!=None:
self.folded_structure.translate(v)
#end if
#end def translate
# test needed
def slide(self,v,recenter=True):
v = array(v)
pos = self.pos
for i in range(len(pos)):
pos[i]+=v
#end for
if recenter:
self.recenter()
#end if
if self.folded_structure!=None:
self.folded_structure.slide(v,recenter)
#end if
#end def slide
# test needed
def zero_corner(self):
corner = self.center-self.axes.sum(0)/2
self.translate(-corner)
#end def zero_corner
# test needed
def locate_simple(self,pos):
pos = array(pos)
if pos.shape==(self.dim,):
pos = [pos]
#end if
nn = nearest_neighbors(1,self.pos,pos)
return nn.ravel()
#end def locate_simple
# test needed
def locate(self,identifiers,radii=None,exterior=False):
indices = None
if isinstance(identifiers,Structure):
cell = identifiers
indices = cell.inside(self.pos)
elif isinstance(identifiers,ndarray) and identifiers.dtype==bool:
indices = arange(len(self.pos))[identifiers]
elif isinstance(identifiers,int):
indices = [identifiers]
elif len(identifiers)>0 and isinstance(identifiers[0],int):
indices = identifiers
elif isinstance(identifiers,str):
atom = identifiers
indices = []
for i in range(len(self.elem)):
if self.elem[i]==atom:
indices.append(i)
#end if
#end for
elif len(identifiers)>0 and isinstance(identifiers[0],str):
indices = []
for atom in identifiers:
for i in range(len(self.elem)):
if self.elem[i]==atom:
indices.append(i)
#end if
#end for
#end for
#end if
if radii is not None or indices is None:
if indices is None:
pos = identifiers
else:
pos = self.pos[indices]
#end if
if isinstance(radii,float) or isinstance(radii,int):
radii = len(pos)*[radii]
elif radii is not None and len(radii)!=len(pos):
self.error('lengths of input radii and positions do not match\n len(radii)={0}\n len(pos)={1}'.format(len(radii),len(pos)))
#end if
dtable = self.min_image_distances(pos)
indices = []
if radii is None:
for i in range(len(pos)):
indices.append(dtable[i].argmin())
#end for
else:
ipos = arange(len(self.pos))
for i in range(len(pos)):
indices.extend(ipos[dtable[i]<radii[i]])
#end for
#end if
#end if
if exterior:
indices = list(set(range(len(self.pos)))-set(indices))
#end if
return indices
#end def locate
def freeze(self,identifiers=None,radii=None,exterior=False,negate=False,directions='xyz'):
if isinstance(identifiers,ndarray) and identifiers.shape==self.pos.shape and identifiers.dtype==bool:
if negate:
self.frozen = ~identifiers
else:
self.frozen = identifiers.copy()
#end if
return
#end if
if identifiers is None:
indices = arange(len(self.pos),dtype=int)
else:
indices = self.locate(identifiers,radii,exterior)
#end if
if len(indices)==0:
self.error('failed to select any atoms to freeze')
#end if
if isinstance(directions,str):
d = empty((3,),dtype=bool)
d[0] = 'x' in directions
d[1] = 'y' in directions
d[2] = 'z' in directions
directions = len(indices)*[d]
else:
directions = array(directions,dtype=bool)
#end if
if self.frozen is None:
self.frozen = zeros(self.pos.shape,dtype=bool)
#end if
frozen = self.frozen
i=0
if not negate:
for index in indices:
frozen[index] = directions[i]
i+=1
#end for
else:
for index in indices:
frozen[index] = directions[i]==False
i+=1
#end for
#end if
#end def freeze
def is_frozen(self):
if self.frozen is None:
return np.zeros((len(self.pos),),dtype=bool)
else:
return self.frozen.sum(1)>0
#end if
#end def is_frozen
# test needed
def magnetize(self,identifiers=None,magnetization='',**mags):
magsin = None
if isinstance(identifiers,obj):
magsin = identifiers.copy()
elif isinstance(magnetization,obj):
magsin = magnetization.copy()
#endif
if magsin!=None:
magsin.transfer_from(mags)
mags = magsin
identifiers = None
magnetization = ''
#end if
for e,m in mags.items():
if not e in self.elem:
self.error('cannot magnetize non-existent element {0}'.format(e))
elif m is not None or not isinstance(m,int):
self.error('magnetizations provided must be either None or integer\n you provided: {0}\n full magnetization request provided:\n {1}'.format(m,mags))
#end if
self.mag[self.elem==e] = m
#end for
if identifiers is None and magnetization=='':
return
elif magnetization=='':
magnetization = identifiers
indices = list(range(len(self.elem)))
else:
indices = self.locate(identifiers)
#end if
if not isinstance(magnetization,(list,tuple,ndarray)):
magnetization = [magnetization]
#end if
for m in magnetization:
if m is not None or not isinstance(m,int):
self.error('magnetizations provided must be either None or integer\n you provided: {0}\n full magnetization list provided: {1}'.format(m,magnetization))
#end if
#end for
if len(magnetization)==1:
m = magnetization[0]
for i in indices:
self.mag[i] = m
#end for
elif len(magnetization)==len(indices):
for i in range(len(indices)):
self.mag[indices[i]] = magnetization[i]
#end for
else:
self.error('magnetization list and list selected atoms differ in length\n length of magnetization list: {0}\n number of atoms selected: {1}\n magnetization list: {2}\n atom indices selected: {3}\n atoms selected: {4}'.format(len(magnetization),len(indices),magnetization,indices,self.elem[indices]))
#end if
#end def magnetize
def is_magnetic(self,tol=1e-8):
magnetic = False
if self.mag is not None:
for m in self.mag:
if m is not None and abs(m)>tol:
magnetic = True
break
#end if
#end for
#end if
return magnetic
#end def is_magnetic
# test needed
def carve(self,identifiers):
indices = self.locate(identifiers)
if isinstance(identifiers,Structure):
sub = identifiers
sub.elem = self.elem[indices].copy()
sub.pos = self.pos[indices].copy()
else:
sub = self.copy()
sub.elem = self.elem[indices]
sub.pos = self.pos[indices]
#end if
sub.host_indices = array(indices)
return sub
#end def carve
# test needed
def remove(self,identifiers):
indices = self.locate(identifiers)
keep = list(set(range(len(self.pos)))-set(indices))
erem = self.elem[indices]
prem = self.pos[indices]
self.elem = self.elem[keep]
self.pos = self.pos[keep]
self.remove_folded_structure()
return erem,prem
#end def remove
# test needed
def replace(self,identifiers,elem=None,pos=None,radii=None,exterior=False):
indices = self.locate(identifiers,radii,exterior)
if isinstance(elem,Structure):
cell = elem
elem = cell.elem
pos = cell.pos
elif elem==None:
elem = self.elem
#end if
indices=array(indices)
elem=array(elem,dtype=object)
pos =array(pos)
nrem = len(indices)
nadd = len(pos)
if nadd<nrem:
ar = array(list(range(0,nadd)))
rr = array(list(range(nadd,nrem)))
self.elem[indices[ar]] = elem[:]
self.pos[indices[ar]] = pos[:]
self.remove(indices[rr])
elif nadd>nrem:
ar = array(list(range(0,nrem)))
er = array(list(range(nrem,nadd)))
self.elem[indices[ar]] = elem[ar]
self.pos[indices[ar]] = pos[ar]
ii = indices[ar[-1]]
self.set_elem( list(self.elem[0:ii])+list(elem[er])+list(self.elem[ii:]) )
self.pos = array( list(self.pos[0:ii])+list(pos[er])+list(self.pos[ii:]) )
else:
self.elem[indices] = elem[:]
self.pos[indices] = pos[:]
#end if
self.remove_folded_structure()
#end def replace
# test needed
def replace_nearest(self,elem,pos=None):
if isinstance(elem,Structure):
cell = elem
elem = cell.elem
pos = cell.pos
#end if
nn = nearest_neighbors(1,self.pos,pos)
np = len(pos)
nps= len(self.pos)
d = empty((np,))
ip = array(list(range(np)))
ips= nn.ravel()
for i in ip:
j = ips[i]
d[i]=sqrt(((pos[i]-self.pos[j])**2).sum())
#end for
order = d.argsort()
ip = ip[order]
ips=ips[order]
replacable = empty((nps,))
replacable[:] = False
replacable[ips]=True
insert = []
last_replaced=nps-1
for n in range(np):
i = ip[n]
j = ips[n]
if replacable[j]:
self.pos[j] = pos[i]
self.elem[j]=elem[i]
replacable[j]=False
last_replaced = j
else:
insert.append(i)
#end if
#end for
insert=array(insert)
ii = last_replaced
if len(insert)>0:
self.set_elem( list(self.elem[0:ii])+list(elem[insert])+list(self.elem[ii:]) )
self.pos = array( list(self.pos[0:ii])+list(pos[insert])+list(self.pos[ii:]) )
#end if
self.remove_folded_structure()
#end def replace_nearest
# test needed
def point_defect(self,identifiers=None,elem=None,dr=None):
if isinstance(elem,str):
elem = [elem]
if dr!=None:
dr = [dr]
#end if
#end if
if not 'point_defects' in self:
self.point_defects = obj()
#end if
point_defects = self.point_defects
ncenters = len(point_defects)
if identifiers is None:
index = ncenters
if index>=len(self.pos):
self.error('attempted to add a point defect at index {0}, which does not exist\n for reference there are {1} atoms in the structure'.format(index,len(self.pos)))
#end if
else:
indices = self.locate(identifiers)
if len(indices)>1:
self.error('{0} atoms were located by identifiers provided\n a point defect replaces only a single atom\n atom indices located: {1}'.format(len(indices),indices))
#end if
index = indices[0]
#end if
if elem is None:
self.error('must supply substitutional elements comprising the point defect\n expected a list or similar for input argument elem')
elif len(elem)>1 and dr is None:
self.error('must supply displacements (dr) since many atoms comprise the point defect')
elif dr!=None and len(elem)!=len(dr):
self.error('elem and dr must have the same length')
#end if
r = self.pos[index]
e = self.elem[index]
elem = array(elem)
pos = zeros((len(elem),len(r)))
if dr is None:
rc = r
for i in range(len(elem)):
pos[i] = r
#end for
else:
nrc = 0
rc = 0*r
dr = array(dr)
for i in range(len(elem)):
pos[i] = r + dr[i]
if norm(dr[i])>1e-5:
rc+=dr[i]
nrc+=1
#end if
#end for
if nrc==0:
rc = r
else:
rc = r + rc/nrc
#end if
#end if
point_defect = obj(
center = rc,
elem_replaced = e,
elem = elem,
pos = pos
)
point_defects.append(point_defect)
elist = list(self.elem)
plist = list(self.pos)
if len(elem)==0 or len(elem)==1 and elem[0]=='':
elist.pop(index)
plist.pop(index)
else:
elist[index] = elem[0]
plist[index] = pos[0]
for i in range(1,len(elem)):
elist.append(elem[i])
plist.append(pos[i])
#end for
#end if
self.set_elem(elist)
self.pos = array(plist)
self.remove_folded_structure()
#end def point_defect
# test needed
def species(self,symbol=False):
if not symbol:
return set(self.elem)
else:
species_labels = set(self.elem)
species = set()
for e in species_labels:
is_elem,symbol = is_element(e,symbol=True)
species.add(symbol)
#end for
return species_labels,species
#end if
#end def species
# test needed
def ordered_species(self,symbol=False):
speclab_set = set()
species_labels = []
if not symbol:
for e in self.elem:
if e not in speclab_set:
speclab_set.add(e)
species_labels.append(e)
#end if
#end for
return species_labels
else:
species = []
spec_set = set()
for e in self.elem:
is_elem,symbol = is_element(e,symbol=True)
if e not in speclab_set:
speclab_set.add(e)
species_labels.append(e)
#end if
if symbol not in spec_set:
spec_set.add(symbol)
species.append(symbol)
#end if
#end for
return species_labels,species
#end if
#end def ordered_species
# test needed
def order_by_species(self,folded=False):
species = []
species_counts = []
elem_indices = []
spec_set = set()
for i in range(len(self.elem)):
e = self.elem[i]
if not e in spec_set:
spec_set.add(e)
species.append(e)
species_counts.append(0)
elem_indices.append([])
#end if
sindex = species.index(e)
species_counts[sindex] += 1
elem_indices[sindex].append(i)
#end for
elem_order = []
for elem_inds in elem_indices:
elem_order.extend(elem_inds)
#end for
self.reorder(elem_order)
if folded and self.folded_structure!=None:
self.folded_structure.order_by_species(folded)
#end if
return species,species_counts
#end def order_by_species
# test needed
def reorder(self,order):
order = array(order)
self.elem = self.elem[order]
self.pos = self.pos[order]
#end def reorder
# test needed
# find layers parallel to a particular cell face
# layers are found by scanning a window of width dtol along the axis and counting
# the number of atoms within the window. window position w/ max number of atoms
# defines the layer. layer distance is the window position.
# the resolution of the scan is determined by dbin
# (axis length)/dbin is the number of fine bins
# dtol/dbin is the number of fine bins in the moving (boxcar) window
# plot=True: plot the layer histogram (fine hist and moving average)
# composition=True: return the composition of each layer (count of each species)
# returns an object containing indices of atoms in each layer by distance along axis
# example: structure w/ 3 layers of 4 atoms each at distances 3.0, 6.0, and 9.0 Angs.
# layers
# 3.0 = [ 0, 1, 2, 3 ]
# 6.0 = [ 4, 5, 6, 7 ]
# 9.0 = [ 8, 9,10,11 ]
def layers(self,axis=0,dtol=0.03,dbin=0.01,plot=False,composition=False):
nbox = int(dtol/dbin)
if nbox%2==0:
nbox+=1
#end if
nwind = (nbox-1)//2
s = self.copy()
s.recenter()
vaxis = s.axes[axis]
daxis = norm(vaxis)
naxis = vaxis/daxis
dbin = dtol/nbox
nbins = int(ceil(daxis/dbin))
dbin = daxis/nbins
dbins = daxis*(arange(nbins)+.5)/nbins
dists = daxis*s.pos_unit()[:,axis]
hist = zeros((nbins,),dtype=int)
boxhist = zeros((nbins,),dtype=int)
ihist = obj()
iboxhist = obj()
index = 0
for d in dists:
ibin = int(floor(d/dbin))
hist[ibin]+=1
if not ibin in ihist:
ihist[ibin] = []
#end if
ihist[ibin].append(index)
index+=1
#end for
for ib in range(nbins):
for i in range(ib-nwind,ib+nwind+1):
n = hist[i%nbins]
if n>0:
boxhist[ib]+=n
if not ib in iboxhist:
iboxhist[ib] = []
#end if
iboxhist[ib].extend(ihist[i%nbins])
#end if
#end for
#end for
peaks = []
nlast=0
for ib in range(nbins):
n = boxhist[ib]
if nlast==0 and n>0:
pcur = []
peaks.append(pcur)
#end if
if n>0:
pcur.append(ib)
#end if
nlast = n
#end for
if boxhist[0]>0 and boxhist[-1]>0:
peaks[0].extend(peaks[-1])
peaks.pop()
#end if
layers = obj()
ip = []
for peak in peaks:
ib = peak[boxhist[peak].argmax()]
ip.append(ib)
pindices = iboxhist[ib]
ldist = dbins[ib] # distance is along an axis vector
faxis = self.face_vectors()[axis]
ldist = dot(ldist*naxis,faxis/norm(faxis))
layers[ldist] = array(pindices,dtype=int)
#end for
if plot:
plt.plot(dbins,boxhist,'b.-',label='boxcar histogram')
plt.plot(dbins,hist,'r.-',label='fine histogram')
plt.plot(dbins[ip],boxhist[ip],'rv',markersize=20)
plt.show()
plt.legend()
#end if
if not composition:
return layers
else:
return layers,self.layer_composition(layers)
#end if
#end def layers
# test needed
def layer_composition(self,layers):
lcomp = obj()
for d,ind in layers.items():
comp = obj()
elem = self.elem[ind]
for e in elem:
if e not in comp:
comp[e] = 1
else:
comp[e] += 1
#end if
#end for
lcomp[d]=comp
#end for
return lcomp
#end def layer_composition
# test needed
def shells(self,identifiers,radii=None,exterior=False,cumshells=False,distances=False,dtol=1e-6):
# get indices for 'core' and 'bulk'
# core is selected by identifiers, forms core for shells to be built around
# bulk is all atoms except for core
if identifiers=='point_defects':
if not 'point_defects' in self:
self.error('requested shells around point defects, but structure has no point defects')
#end if
core = []
for pd in self.point_defects:
core.append(pd.center)
#end for
core = array(core)
bulk_ind = self.locate(core,radii=dtol,exterior=True)
core_ind = self.locate(bulk_ind,exterior=True)
bulk = self.pos[bulk_ind]
else:
core_ind = self.locate(identifiers,radii,exterior)
bulk_ind = self.locate(core_ind,exterior=True)
core = self.pos[core_ind]
bulk = self.pos[bulk_ind]
#end if
bulk_ind = array(bulk_ind,dtype=int)
# build distance table between bulk and core
dtable = self.distance_table(bulk,core)
# find shortest distance for each bulk atom to any core atom and order by distance
dist = dtable.min(1)
ind = arange(len(bulk))
order = dist.argsort()
dist = dist[order]
ind = bulk_ind[ind[order]]
# find shells around the core
# the closest atom to the core starts the first shell and defines a shell distance
# other atoms are in the shell if within dtol distance of the first atom
# otherwise a new shell is started
ns = 0
ds = -1
shells = obj()
shells[ns] = list(core_ind) # first shell is all core atoms
dshells = [0.]
for n in range(len(dist)):
if abs(dist[n]-ds)>dtol:
shell = [ind[n]] # new shell starts with single atom
ns+=1
shells[ns] = shell
ds = dist[n] # shell distance is distance of this atom from core
dshells.append(ds)
else:
shell.append(ind[n])
#end if
#end for
dshells = array(dshells,dtype=float)
results = [shells]
if cumshells:
# assemble cumulative shells, ie cumshell[ns] = sum(shells[n],n=0 to ns)
cumshells = obj()
cumshells[0] = list(shells[0])
for ns in range(1,len(shells)):
cumshells[ns] = cumshells[ns-1]+shells[ns]
#end for
for ns,cshell in cumshells.items():
cumshells[ns] = array(cshell,dtype=int)
#end for
results.append(cumshells)
#end if
for ns,shell in shells.items():
shells[ns] = array(shell,dtype=int)
if distances:
results.append(dshells)
#end if
if len(results)==1:
results = results[0]
#end if
return results
#end def shells
# test needed
# find connected sets of atoms.
# indices is a list of atomic indices to consider (self.pos[indices] are their positions)
# atoms are considered connected if they are within rmax of each other
# order sets the maximum number of atoms in any connected graph
# order = 1 returns single atoms
# order = 2 returns dimers + order=1 results
# order = 3 returns trimers + order=2 results
# ...
# degree is explained w/ an example: a triangle of atoms 0,1,2 and a line of atoms 3,4,5 (3 & 5 are not neighbors)
# degree = False : returned object (cgraphs) has following structure:
# cgraphs[1] = [ (0,), (1,), (2,), (3,), (4,), (5,) ] # first order connected graphs (atoms)
# cgraphs[2] = [ (0,1), (0,2), (1,2), (3,4), (4,5) ] # second order connected graphs (dimers)
# cgraphs[3] = [ (0,1,2), (3,4,5) ] # third order connected graphs (trimers)
# degree = True : returned object (cgraphs) has following structure:
# cgraphs
# 1 # first order connected graphs (atoms)
# 0 # sum of vertex degrees is 0 (a single atom has no neighbors)
# (0,) = [ (0,), (1,), (2,), (3,), (4,), (5,) ] # graphs with vertex degree (0,)
# 2 # second order connected graphs (dimers)
# 2 # sum of vertex degrees is 2 (each atom is connected to 1 neighbor)
# (1,1) = [ (0,1), (0,2), (1,2), (3,4), (4,5) ] # graphs with vertex degree (1,1)
# 3 # third order connected graphs (trimers)
# 4 # sum of vertex degrees is 4 (2 atoms have 1 neighbor and 1 atom has 2)
# (1,1,2) = [ (3,5,4) ]
# 6 # sum of vertex degrees is 6 (each atom is connected to 2 others)
# (2,2,2) = [ (0,1,2) ] # graphs with vertex degree (2,2,2)
def connected_graphs(self,order,indices=None,rmax=None,nmax=None,voronoi=False,degree=False,site_maps=False,**spec_max):
if indices is None:
indices = arange(len(self.pos),dtype=int)
pos = self.pos
else:
pos = self.pos[indices]
#end if
np = len(indices)
neigh_table = []
actual_indices = None
if voronoi:
actual_indices = True
neighbors = self.voronoi_neighbors(indices,restrict=True,distance_ordered=False)
for nilist in neighbors:
neigh_table.append(nilist)
#end for
else:
actual_indices = False
elem = set(self.elem[indices])
spec = set(spec_max.keys())
if spec==elem or rmax!=None:
None
elif spec<elem and nmax!=None:
for e in elem:
if e not in spec:
spec_max[e] = nmax
#end if
#end for
#end if
# get neighbor table for subset of atoms specified by indices
nt,dt = self.neighbor_table(pos,pos,distances=True)
# determine how many neighbors to consider based on rmax (all are neighbors if rmax is None)
nneigh = zeros((np,),dtype=int)
if len(spec_max)>0:
for n in range(np):
nneigh[n] = min(spec_max[self.elem[n]],len(nt[n]))
#end for
elif rmax is None:
nneigh[:] = np
else:
nneigh = (dt<rmax).sum(1)
#end if
for i in range(np):
neigh_table.append(nt[i,1:nneigh[i]])
#end for
del nt,dt,nneigh,elem,spec,rmax
#end if
neigh_table = array(neigh_table,dtype=int)
# record which atoms are neighbors to each other
neigh_pairs = set()
if actual_indices:
for i in range(np):
for ni in neigh_table[i]:
neigh_pairs.add((i,ni))
neigh_pairs.add((ni,i))
#end for
#end for
else:
for i in range(np):
for ni in neigh_table[i]:
ii = indices[i]
jj = indices[ni]
neigh_pairs.add((ii,jj))
neigh_pairs.add((jj,ii))
#end for
#end for
#end if
# find the connected graphs
graphs_found = set() # map to contain tuples of connected atom's indices
cgraphs = obj()
for o in range(1,order+1): # organize by order
cgraphs[o] = []
#end for
if order>0:
cg = cgraphs[1]
for i in range(np): # list of single atoms
gi = (i,) # graph indices
cg.append(gi) # add graph to graph list of order 1
graphs_found.add(gi) # add graph to set of all graphs
#end for
for o in range(2,order+1): # graphs of order o are found by adding all
cglast = cgraphs[o-1] # possible single neighbors to each graph of order o-1
cg = cgraphs[o]
for gilast in cglast: # all graphs of order o-1
for i in gilast: # all indices in each graph of order o-1
for ni in neigh_table[i]: # neighbors of selected atom in o-1 graph
gi = tuple(sorted(gilast+(ni,))) # new graph with neighbor added
if gi not in graphs_found and len(set(gi))==o: # add it if it is new and really is order o
graphs_found.add(gi) # add graph to set of all graphs
cg.append(gi) # add graph to graph list of order o
#end if
#end for
#end for
#end for
#end for
#end if
if actual_indices:
for o,cg in cgraphs.items():
cgraphs[o] = array(cg,dtype=int)
#end for
else:
# map indices back to actual atomic indices
for o,cg in cgraphs.items():
cgmap = []
for gi in cg:
#gi = array(gi)
gimap = tuple(sorted(indices[array(gi)]))
cgmap.append(gimap)
#end for
cgraphs[o] = array(sorted(cgmap),dtype=int)
#end for
#end if
# reorganize the graph listing by cluster and vertex degree, if desired
if degree:
#degree_map = obj()
cgraphs_deg = obj()
for o,cg in cgraphs.items():
dgo = obj()
cgraphs_deg[o] = dgo
for gi in cg:
di = zeros((o,),dtype=int)
for m in range(o):
i = gi[m]
for n in range(m+1,o):
j = gi[n]
if (i,j) in neigh_pairs:
di[m]+=1
di[n]+=1
#end if
#end for
#end for
d = int(di.sum())
dorder = di.argsort()
di = tuple(di[dorder])
gi = tuple(array(gi)[dorder])
if not d in dgo:
dgo[d]=obj()
#end if
dgd = dgo[d]
if not di in dgd:
dgd[di] = []
#end if
dgd[di].append(gi)
#degree_map[gi] = d,di
#end for
for dgd in dgo:
for di,dgi in dgd.items():
dgd[di]=array(sorted(dgi),dtype=int)
#end for
#end for
#end for
cgraphs = cgraphs_deg
#end if
if not site_maps:
return cgraphs
else:
cmaps = obj()
if not degree:
for order,og in cgraphs.items():
cmap = obj()
for slist in og:
for s in slist:
if not s in cmap:
cmap[s] = obj()
#end if
cmap[s].append(slist)
#end for
#end for
cmaps[order] = cmap
#end for
else:
for order,og in cgraphs.items():
for total_degree,tg in og.items():
for local_degree,lg in tg.items():
cmap = obj()
for slist in lg:
n=0
for s in slist:
d = local_degree[n]
if not s in cmap:
cmap[s] = obj()
#end if
if not d in cmap[s]:
cmap[s][d] = obj()
#end if
cmap[s][d].append(slist)
n+=1
#end for
#end for
cmaps.add_attribute_path((order,total_degree,local_degree),cmap)
#end for
#end for
#end for
#end if
return cgraphs,cmaps
#end if
#end def connected_graphs
# test needed
# returns connected graphs that are rings up to the requested order
# rings are constructed by pairing lines that share endpoints
# all vertices of a ring have degree two
def ring_graphs(self,order,**kwargs):
# get all half order connected graphs
line_order = order//2+order%2+1
cgraphs = self.connected_graphs(line_order,degree=True,site_maps=False,**kwargs)
# collect half order graphs that are lines
lgraphs = obj()
for o in range(2,line_order+1):
total_degree = 2*o-2
vertex_degree = tuple([1,1]+(o-2)*[2])
lg = None
if o in cgraphs:
cg = cgraphs[o]
if total_degree in cg:
dg = cg[total_degree]
if vertex_degree in dg:
lg = dg[vertex_degree]
#end if
#end if
#end if
if lg!=None:
lg_end = obj()
for gi in lg:
end_key = tuple(sorted(gi[0:2])) # end points
if end_key not in lg_end:
lg_end[end_key] = []
#end if
lg_end[end_key].append(tuple(gi))
#end for
lgraphs[o] = lg_end
#end if
#end for
# contruct rings from lines that share endpoints
rgraphs = obj()
for o in range(3,order+1):
o1 = o/2+1 # split half order for odd, same for even,
o2 = o1+o%2
lg1 = lgraphs.get_optional(o1,None) # sets of half order lines
lg2 = lgraphs.get_optional(o2,None)
if lg1!=None and lg2!=None:
rg = []
rset = set()
for end_key,llist1 in lg1.items(): # list of lines sharing endpoints
if end_key in lg2:
llist2 = lg2[end_key] # second list of lines sharing endpoints
for gi1 in llist1: # combine line pairs into rings
for gi2 in llist2:
ri = tuple(sorted(set(gi1+gi2[2:]))) # ring indices
if ri not in rset and len(ri)==o: # exclude repeated lines or rings
rg.append(ri)
rset.add(ri)
#end if
#end for
#end for
#end if
#end for
rgraphs[o] = array(sorted(rg),dtype=int)
#end if
#end for
return rgraphs
#end def ring_graphs
# test needed
# find the centroid of a set of points/atoms in min image convention
def min_image_centroid(self,points=None,indices=None):
if indices!=None:
points = self.pos[indices]
elif points is None:
self.error('points or images must be provided to min_image_centroid')
#end if
p = array(points,dtype=float)
cprev = p[0]+1e99
c = p[0]
while(norm(c-cprev)>1e-8):
p = self.cell_image(p,center=c)
cprev = c
c = p.mean(axis=0)
#end def min_image_centroid
return c
#end def min_image_centroid
# test needed
# find min image centroids of multiple sets of points/atoms
def min_image_centroids(self,points=None,indices=None):
cents = []
if points!=None:
for p in points:
cents.append(self.min_image_centroid(p))
#end for
elif indices!=None:
for ind in indices:
cents.append(self.min_image_centroid(indices=ind))
#end for
else:
self.error('points or images must be provided to min_image_centroid')
#end if
return array(cents,dtype=float)
#end def min_image_centroids
def min_image_vectors(self,points=None,points2=None,axes=None,pairs=True):
if points is None:
points = self.pos
elif isinstance(points,Structure):
points = points.pos
#end if
if axes is None:
axes = self.axes
#end if
axinv = inv(axes)
points = array(points)
single = points.shape==(self.dim,)
if single:
points = [points]
#end if
if points2 is None:
points2 = self.pos
elif isinstance(points2,Structure):
points2 = points2.pos
elif points2.shape==(self.dim,):
points2 = [points2]
#end if
npoints = len(points)
npoints2 = len(points2)
if pairs:
vtable = empty((npoints,npoints2,self.dim),dtype=float)
i=-1
for p in points:
i+=1
j=-1
for pp in points2:
j+=1
u = dot(pp-p,axinv)
vtable[i,j] = dot(u-floor(u+.5),axes)
#end for
#end for
result = vtable
else:
if npoints!=npoints2:
self.error('cannot create one to one minimum image vectors, point sets differ in length\n npoints1 = {0}\n npoints2 = {1}'.format(npoints,npoints2))
#end if
vectors = empty((npoints,self.dim),dtype=float)
n = 0
for p in points:
pp = points2[n]
u = dot(pp-p,axinv)
vectors[n] = dot(u-floor(u+.5),axes)
n+=1
#end for
result = vectors
#end if
return result
#end def min_image_vectors
def min_image_distances(self,points=None,points2=None,axes=None,vectors=False,pairs=True):
vtable = self.min_image_vectors(points,points2,axes,pairs=pairs)
rdim = len(vtable.shape)-1
dtable = sqrt((vtable**2).sum(rdim))
if not vectors:
return dtable
else:
return dtable,vtable
#end if
#end def min_image_distances
def distance_table(self,points=None,points2=None,axes=None,vectors=False):
return self.min_image_distances(points,points2,axes,vectors)
#end def distance_table
def vector_table(self,points=None,points2=None,axes=None):
return self.min_image_vectors(points,points2,axes)
#end def vector_table
def neighbor_table(self,points=None,points2=None,axes=None,distances=False,vectors=False):
dtable,vtable = self.min_image_distances(points,points2,axes,vectors=True)
ntable = empty(dtable.shape,dtype=int)
for i in range(len(dtable)):
ntable[i] = dtable[i].argsort()
#end for
results = [ntable]
if distances:
for i in range(len(dtable)):
dtable[i] = dtable[i][ntable[i]]
#end for
results.append(dtable)
#end if
if vectors:
for i in range(len(vtable)):
vtable[i] = vtable[i][ntable[i]]
#end for
results.append(vtable)
#end if
if len(results)==1:
results = results[0]
#end if
return results
#end def neighbor_table
# test needed
def min_image_norms(self,points,norms):
if isinstance(norms,int) or isinstance(norms,float):
norms = [norms]
#end if
vtable = self.min_image_vectors(points)
rdim = len(vtable.shape)-1
nout = []
for p in norms:
nout.append( ((abs(vtable)**p).sum(rdim))**(1./p) )
#end for
if len(norms)==1:
nout = nout[0]
#end if
return nout
#end def min_image_norms
# test needed
# get all neighbors according to contacting voronoi polyhedra in PBC
def voronoi_neighbors(self,indices=None,restrict=False,distance_ordered=True):
if indices is None:
indices = arange(len(self.pos))
#end if
indices = set(indices)
# make a new version of this (small cell)
sn = self.copy()
sn.recenter()
# tile a large cell periodically
d = 3
t = tuple(zeros((d,),dtype=int)+3)
ss = sn.tile(t)
ss.recenter(sn.center)
# get nearest neighbor index pairs in the large cell
neigh_pairs = voronoi_neighbors(ss.pos)
# create a mapping from large to small indices
large_to_small = 3**d*list(range(len(self.pos)))
# find the neighbor pairs in the small cell
neighbors = obj()
small_inds = set(ss.locate(sn.pos))
for n in range(len(neigh_pairs)):
i,j = neigh_pairs[n,:]
if i in small_inds or j in small_inds: # pairs w/ at least one in cell image
i = large_to_small[i] # mapping to small cell indices
j = large_to_small[j]
if not restrict or (i in indices and j in indices): # restrict to orig index set
if not i in neighbors:
neighbors[i] = [j]
else:
neighbors[i].append(j)
#ned if
if not j in neighbors:
neighbors[j] = [i]
else:
neighbors[j].append(i)
#end if
#end if
#end if
#end for
# remove any duplicates and order by distance
if distance_ordered:
dt = self.distance_table()
for i,ni in neighbors.items():
ni = array(list(set(ni)),dtype=int)
di = dt[i,ni]
order = di.argsort()
neighbors[i] = ni[order]
#end for
else: # just remove duplicates
for i,ni in neighbors.items():
neighbors[i] = array(list(set(ni)),dtype=int)
#end for
#end if
return neighbors
#end def voronoi_neighbors
def voronoi_vectors(self,indices=None,restrict=None):
ni = self.voronoi_neighbors(indices,restrict)
vt = self.vector_table()
vv = obj()
for i,vi in ni.items():
vv[i] = vt[i,vi]
#end for
return vv
#end def voronoi_vectors
def voronoi_distances(self,indices=None,restrict=False):
vv = self.voronoi_vectors(indices,restrict)
vd = obj()
for i,vvi in vv.items():
vd[i] = np.linalg.norm(vvi,axis=1)
#end for
return vd
#end def voronoi_distances
def voronoi_radii(self,indices=None,restrict=None):
vd = self.voronoi_distances(indices,restrict)
vr = obj()
for i,vdi in vd.items():
vr[i] = vdi.min()/2
#end for
return vr
#end def voronoi_radii
def voronoi_species_radii(self):
vr = self.voronoi_radii()
vsr = obj()
for i,r in vr.items():
e = self.elem[i]
if e not in vsr:
vsr[e] = r
else:
vsr[e] = min(vsr[e],r)
#end if
#end for
return vsr
#end def voronoi_species_radii
# test needed
# get nearest neighbors according to constraints (voronoi, max distance, coord. number)
def nearest_neighbors(self,indices=None,rmax=None,nmax=None,restrict=False,voronoi=False,distances=False,**spec_max):
if indices is None:
indices = arange(len(self.pos))
#end if
elem = set(self.elem[indices])
spec = set(spec_max.keys())
if spec==elem or rmax!=None or voronoi:
None
elif spec<elem and nmax!=None:
for e in elem:
if e not in spec:
spec_max[e] = nmax
#end if
#end for
else:
self.error('must specify nmax for all species\n species present: {0}\n you only provided nmax for these species: {1}'.format(sorted(elem),sorted(spec)))
#end if
pos = self.pos[indices]
if not restrict:
pos2 = self.pos
else:
pos2 = pos
#end if
if voronoi:
neighbors = self.voronoi_neighbors(indices=indices,restrict=restrict)
dt = self.distance_table(pos,pos2)[:,1:]
else:
nt,dt = self.neighbor_table(pos,pos2,distances=True)
dt=dt[:,1:]
nt=nt[:,1:]
neighbors = list(nt)
#end if
for i in range(len(indices)):
neighbors[i] = indices[neighbors[i]]
#end for
dist = list(dt)
if rmax is None:
for i in range(len(indices)):
nn = neighbors[i]
dn = dist[i]
e = self.elem[indices[i]]
if e in spec_max:
smax = spec_max[e]
if len(nn)>smax:
neighbors[i] = nn[:smax]
dist[i] = dn[:smax]
#end if
#end if
#end for
else:
for i in range(len(indices)):
neighbors[i] = neighbors[i][dt[i]<rmax]
#end for
#end if
if not distances:
return neighbors
else:
return neighbors,dist
#end if
#end def nearest_neighbors
# test needed
# determine local chemical coordination limited by constraints
def chemical_coordination(self,indices=None,nmax=None,rmax=None,restrict=False,voronoi=False,neighbors=False,distances=False,**spec_max):
if indices is None:
indices = arange(len(self.pos))
#end if
if not distances:
neigh = self.nearest_neighbors(indices=indices,nmax=nmax,rmax=rmax,restrict=restrict,voronoi=voronoi,**spec_max)
else:
neigh,dist = self.nearest_neighbors(indices=indices,nmax=nmax,rmax=rmax,restrict=restrict,voronoi=voronoi,distances=True,**spec_max)
#end if
neigh_elem = []
for i in range(len(indices)):
neigh_elem.extend(self.elem[neigh[i]])
#end for
chem_key = tuple(sorted(set(neigh_elem)))
chem_coord = zeros((len(indices),len(chem_key)),dtype=int)
for i in range(len(indices)):
counts = zeros((len(chem_key),),dtype=int)
nn = list(self.elem[neigh[i]])
for n in range(len(counts)):
chem_coord[i,n] = nn.count(chem_key[n])
#end for
#end for
chem_map = obj()
i=0
for coord in chem_coord:
coord = tuple(coord)
if not coord in chem_map:
chem_map[coord] = [indices[i]]
else:
chem_map[coord].append(indices[i])
#end if
i+=1
#end for
for coord,ind in chem_map.items():
chem_map[coord] = array(ind,dtype=int)
#end for
results = [chem_key,chem_coord,chem_map]
if neighbors:
results.append(neigh)
#end if
if distances:
results.append(dist)
#end if
return results
#end def chemical_coordination
# test needed
def rcore_max(self,units=None):
nt,dt = self.neighbor_table(self.pos,distances=True)
d = dt[:,1]
rcm = d.min()/2
if units!=None:
rcm = convert(rcm,self.units,units)
#end if
return rcm
#end def rcore_max
# test needed
def cell_image(self,p,center=None):
pos = array(p,dtype=float)
if center is None:
c = self.center.copy()
else:
c = array(center,dtype=float)
#end if
axes = self.axes
axinv = inv(axes)
for i in range(len(pos)):
u = dot(pos[i]-c,axinv)
pos[i] = dot(u-floor(u+.5),axes)+c
#end for
return pos
#end def cell_image
# test needed
def center_distances(self,points,center=None):
if center is None:
c = self.center.copy()
else:
c = array(center,dtype=float)
#end if
points = self.cell_image(points,center=c)
for i in range(len(points)):
points[i] -= c
#end for
return sqrt((points**2).sum(1))
#end def center_distances
# test needed
def recenter(self,center=None):
if center is not None:
self.center=array(center,dtype=float)
#end if
pos = self.pos
c = empty((1,self.dim),dtype=float)
c[:] = self.center[:]
axes = self.axes
axinv = inv(axes)
for i in range(len(pos)):
u = dot(pos[i]-c,axinv)
pos[i] = dot(u-floor(u+.5),axes)+c
#end for
self.recenter_k()
#end def recenter
# test needed
def recorner(self):
pos = self.pos
axes = self.axes
axinv = inv(axes)
for i in range(len(pos)):
u = dot(pos[i],axinv)
pos[i] = dot(u-floor(u),axes)
#end for
#end def recorner
# test needed
def recenter_k(self,kpoints=None,kaxes=None,kcenter=None,remove_duplicates=False):
use_self = kpoints is None
if use_self:
kpoints=self.kpoints
#end if
if kaxes is None:
kaxes=self.kaxes
#end if
if len(kpoints)>0:
axes = kaxes
axinv = inv(axes)
if kcenter is None:
c = axes.sum(0)/2
else:
c = array(kcenter)
#end if
for i in range(len(kpoints)):
u = dot(kpoints[i]-c,axinv)
u -= floor(u+.5)
u[abs(u-.5)<1e-12] -= 1.0
u[abs(u )<1e-12] = 0.0
kpoints[i] = dot(u,axes)+c
#end for
if remove_duplicates:
inside = self.inside(kpoints,axes,c)
kpoints = kpoints[inside]
nkpoints = len(kpoints)
unique = empty((nkpoints,),dtype=bool)
unique[:] = True
nn = nearest_neighbors(1,kpoints)
if nkpoints>1:
nn.shape = nkpoints,
dist = self.distances(kpoints,kpoints[nn])
tol = 1e-8
duplicates = arange(nkpoints)[dist<tol]
for i in duplicates:
if unique[i]:
for j in duplicates:
if sqrt(((kpoints[i]-kpoints[j])**2).sum(1))<tol:
unique[j] = False
#end if
#end for
#end if
#end for
#end if
kpoints = kpoints[unique]
#end if
#end if
if use_self:
self.kpoints = kpoints
else:
return kpoints
#end if
#end def recenter_k
# test needed
def inside(self,pos,axes=None,center=None,tol=1e-8,separate=False):
if axes==None:
axes=self.axes
#end if
if center==None:
center=self.center
#end if
axes = array(axes)
center = array(center)
inside = []
surface = []
su = []
axinv = inv(axes)
for i in range(len(pos)):
u = dot(pos[i]-center,axinv)
umax = abs(u).max()
if abs(umax-.5)<tol:
surface.append(i)
su.append(u)
elif umax<.5:
inside.append(i)
#end if
#end for
npos,dim = pos.shape
drange = list(range(dim))
n = len(surface)
i=0
while i<n:
j=i+1
while j<n:
du = abs(su[i]-su[j])
match = False
for d in drange:
match = match or abs(du[d]-1.)<tol
#end for
if match:
surface[j]=surface[-1]
surface.pop()
su[j]=su[-1]
su.pop()
n-=1
else:
j+=1
#end if
#end while
i+=1
#end while
if not separate:
inside+=surface
return inside
else:
return inside,surface
#end if
#end def inside
def tile(self,*td,**kwargs):
in_place = kwargs.pop('in_place',False)
check = kwargs.pop('check',False)
dim = self.dim
if len(td)==1:
if isinstance(td[0],int):
tiling = dim*[td[0]]
else:
tiling = td[0]
#end if
else:
tiling = td
#end if
tiling = array(tiling)
matrix_tiling = tiling.shape == (dim,dim)
tilematrix,tilevector = reduce_tilematrix(tiling)
ncells = int(round( abs(det(tilematrix)) ))
if ncells==1 and abs(tilematrix-identity(self.dim)).sum()<1e-1:
if in_place:
return self
else:
return self.copy()
#end if
#end if
self.recenter()
elem = array(ncells*list(self.elem))
pos = self.tile_points(self.pos,self.axes,tilematrix,tilevector)
axes = dot(tilematrix,self.axes)
center = axes.sum(0)/2
kaxes = dot(inv(tilematrix.T),self.kaxes)
kpoints = array(self.kpoints)
kweights = array(self.kweights)
mag = None
frozen = None
if self.mag is not None:
mag = ncells*list(self.mag)
#end if
if self.frozen is not None:
frozen = ncells*list(self.frozen)
#end if
ts = self.copy()
ts.center = center
ts.set_elem(elem)
ts.axes = axes
ts.pos = pos
ts.mag = mag
ts.kaxes = kaxes
ts.kpoints = kpoints
ts.kweights = kweights
ts.set_mag(mag)
ts.set_frozen(frozen)
ts.background_charge = ncells*self.background_charge
ts.recenter()
ts.unique_kpoints()
if self.is_tiled():
ts.tmatrix = dot(tilematrix,self.tmatrix)
ts.folded_structure = self.folded_structure.copy()
else:
ts.tmatrix = tilematrix
ts.folded_structure = self.copy()
#end if
if in_place:
self.clear()
self.transfer_from(ts)
ts = self
#end if
if check:
ts.check_tiling()
#end if
return ts
#end def tile
def tile_points(self,points,axes,tilemat,tilevec=None):
if tilevec is None:
tilemat,tilevec = reduce_tilematrix(tilemat)
#end if
if not isinstance(tilemat,ndarray):
tilemat = array(tilemat)
#end if
matrix_tiling = abs(tilemat-diag(diag(tilemat))).sum()>0.1
if not matrix_tiling:
return self.tile_points_simple(points,axes,diag(abs(tilemat)))
else:
if not isinstance(axes,ndarray):
axes = array(axes)
#end if
if not isinstance(tilevec,ndarray):
tilevec = array(tilevec)
#end if
dim = len(axes)
npoints = len(points)
ntpoints = npoints*int(round(abs(det(tilemat))))
if tilevec.size==dim:
tilevec.shape = 1,dim
#end if
taxes = dot(tilemat,axes)
success = False
for tvec in tilevec:
tpoints = self.tile_points_simple(points,axes,tvec)
tpoints,weights,pmap = self.unique_points_fast(tpoints,taxes)
if len(tpoints)==ntpoints:
success = True
break
#end if
#end for
if not success:
tpoints = self.tile_points_brute(points,axes,tilemat)
tpoints,weights,pmap = self.unique_points_fast(tpoints,taxes)
if len(tpoints)!=ntpoints:
self.error('brute force tiling failed')
#end if
#end if
#end if
return tpoints
#end def tile_points
def tile_points_simple(self,points,axes,tilevec):
if not isinstance(points,ndarray):
points = array(points)
#end if
if not isinstance(tilevec,ndarray):
tilevec = array(tilevec)
#end if
if not isinstance(axes,ndarray):
axes = array(axes)
#end if
if len(points.shape)==1:
npoints,dim = len(points),1
else:
npoints,dim = points.shape
#end if
t = tilevec
ti = array(around(t),dtype=int)
noninteger = abs(t-ti).sum()>1e-6
if noninteger:
tp = t.prod()
if abs(tp-int(tp))>1e-6:
self.error('tiling vector does not correspond to an integer volume change\ntiling vector: {0}\nvolume change: {1} {2} {3}'.format(tilevec,tilevec.prod(),ntpoints,int(ntpoints)))
#end if
t = array(ceil(t),dtype=int)+1
else:
t = ti
#end if
if t.min()<0:
self.error('tiling vector cannot be negative\ntiling vector provided: {}'.format(t))
#end if
ntpoints = npoints*int(round( t.prod() ))
if ntpoints==0:
tpoints = array([])
else:
tpoints = empty((ntpoints,dim))
ns=0
ne=npoints
for k in range(t[2]):
for j in range(t[1]):
for i in range(t[0]):
v = dot(array([[i,j,k]]),axes)
for d in range(dim):
tpoints[ns:ne,d] = points[:,d]+v[0,d]
#end for
ns+=npoints
ne+=npoints
#end for
#end for
#end for
#end if
return tpoints
#end def tile_points_simple
def tile_points_brute(self,points,axes,tilemat):
tcorners = [[0,0,0],
[1,0,0],
[0,1,0],
[0,0,1],
[0,1,1],
[1,0,1],
[1,1,0],
[1,1,1]]
tcorners = dot(tcorners,tilemat)
tmin = tcorners.min(axis=0)
tmax = tcorners.max(axis=0)
tilevec = tmax-tmin
tpoints = self.tile_points_simple(points,axes,tilevec)
return tpoints
#end def tile_points_brute
def opt_tilematrix(self,*args,**kwargs):
return optimal_tilematrix(self.axes,*args,**kwargs)
#end def opt_tilematrix
def tile_opt(self,*args,**kwargs):
Topt,ropt = self.opt_tilematrix(*args,**kwargs)
return self.tile(Topt)
#end def tile_opt
def check_tiling(self,tol=1e-6,exit=True):
msg = ''
if not self.is_tiled():
return msg
#end if
msgs = []
st = self
s = self.folded_structure
nt = len(st.pos)
n = len(s.pos)
if nt%n!=0:
msgs.append('tiled atom count does is not divisible by untiled atom count')
#end if
vratio = st.volume()/s.volume()
if abs(vratio-float(nt)/n)>tol:
msgs.append('tiled/untiled volume ratio does not match tiled/untiled atom count ratio')
#end if
if abs(vratio-abs(det(st.tmatrix)))>tol:
msgs.append('tiled/untiled volume ratio does not match tiling matrix determinant')
#end if
p,w,pmap = self.unique_points_fast(st.pos,st.axes)
if len(p)!=nt:
msgs.append('tiled positions are not unique')
#end if
if len(msgs)>0:
msg = 'tiling check failed'
for m in msgs:
msg += '\n'+m
#end for
if exit:
self.error(msg)
#end if
#end if
return msg
#end def check_tiling
# test needed
def kfold(self,tiling,kpoints,kweights):
if isinstance(tiling,int):
tiling = self.dim*[tiling]
#end if
tiling = array(tiling)
if tiling.shape==(self.dim,self.dim):
tiling = tiling.T
#end if
tilematrix,tilevector = reduce_tilematrix(tiling)
ncells = int(round( abs(det(tilematrix)) ))
kp = self.tile_points(kpoints,self.kaxes,tilematrix,tilevector)
kw = array(ncells*list(kweights),dtype=float)/ncells
return kp,kw
#end def kfold
def get_smallest(self):
if self.has_folded():
return self.folded_structure
else:
return self
#end if
#end def get_smallest
# test needed
def fold(self,small,*requests):
self.error('fold needs a developers attention to make it equivalent with tile')
if self.dim!=3:
self.error('fold is currently only implemented for 3 dimensions')
#end if
self.recenter_k()
corners = []
ndim = len(small.axes)
imin = empty((ndim,),dtype=int)
imax = empty((ndim,),dtype=int)
imin[:] = 1000000
imax[:] = -1000000
axinv = inv(self.kaxes)
center = self.kaxes.sum(0)/2
c = empty((1,3))
for k in -1,2:
for j in -1,2:
for i in -1,2:
c[:] = i,j,k
c = dot(c,small.kaxes)
u = dot(c-center,axinv)
for d in range(ndim):
imin[d] = min(int(floor(u[0,d])),imin[d])
imax[d] = max(int(ceil(u[0,d])),imax[d])
#end for
#end for
#end for
#end for
axes = small.kaxes
axinv = inv(small.kaxes)
center = small.kaxes.sum(0)/2
nkpoints = len(self.kpoints)
kindices = []
kpoints = []
shift = empty((ndim,))
kr = list(range(nkpoints))
for k in range(imin[2],imax[2]+1):
for j in range(imin[1],imax[1]+1):
for i in range(imin[0],imax[0]+1):
for n in kr:
shift[:] = i,j,k
shift = dot(shift,self.kaxes)
kp = self.kpoints[n]+shift
u = dot(kp-center,axinv)
if abs(u).max()<.5+1e-10:
kindices.append(n)
kpoints.append(kp)
#end if
#end for
#end for
#end for
#end for
kindices = array(kindices)
kpoints = array(kpoints)
inside = self.inside(kpoints,axes,center)
kindices = kindices[inside]
kpoints = kpoints[inside]
small.kpoints = kpoints
small.recenter_k()
kpoints = array(small.kpoints)
if len(requests)>0:
results = []
for request in requests:
if request=='kmap':
kmap = obj()
for k in self.kpoints:
kmap[tuple(k)] = []
#end for
for i in range(len(kpoints)):
kp = tuple(self.kpoints[kindices[i]])
kmap[kp].append(array(kpoints[i]))
#end for
for kl,ks in kmap.items():
kmap[kl] = array(ks)
#end for
res = kmap
elif request=='tilematrix':
res = self.tilematrix(small)
else:
self.error(request+' is not a recognized input to fold')
#end if
results.append(res)
#end if
return results
#end if
#end def fold
def tilematrix(self,small=None,tol=1e-6,status=False):
if small is None:
if self.folded_structure is not None:
small = self.folded_structure
else:
return identity(self.dim,dtype=int)
#end if
#end if
tm = dot(self.axes,inv(small.axes))
tilemat = array(around(tm),dtype=int)
error = abs(tilemat-tm).sum()
non_integer_elements = error > tol
if status:
return tilemat,not non_integer_elements
else:
if non_integer_elements:
self.error('large cell cannot be constructed as an integer tiling of the small cell\nlarge cell axes:\n'+str(self.axes)+'\nsmall cell axes: \n'+str(small.axes)+'\nlarge/small:\n'+str(self.axes/small.axes)+'\ntiling matrix:\n'+str(tm)+'\nintegerized tiling matrix:\n'+str(tilemat)+'\nerror: '+str(error)+'\ntolerance: '+str(tol))
#end if
return tilemat
#end if
#end def tilematrix
def primitive(self,source=None,tmatrix=False,add_kpath=False,**kwargs):
res = None
allowed_sources = set(['seekpath'])
if source is None or isinstance(source,bool):
source = 'seekpath'
#end if
if source not in allowed_sources:
self.error('source used to obtain primitive cell is unrecognized\nsource requested: {0}\nallowed sources: {1}'.format(source,sorted(allowed_sources)))
#end if
if source=='seekpath':
res_skp = get_seekpath_full(structure=self,primitive=True,**kwargs)
prim = res_skp.primitive
T = res_skp.prim_tmatrix
if add_kpath:
prim.add_kpoints(res_skp.explicit_kpoints_abs)
#end if
if tmatrix:
res = prim,T
else:
res = prim
#end if
else:
self.error('primitive source "{0}" is not implemented\nplease contact a developer'.format(source))
#end if
if prim.units!=self.units:
prim.change_units(self.units)
#end if
return res
#end def primitive
def become_primitive(self,source=None,add_kpath=False,**kwargs):
prim = self.primitive(source=source,add_kpath=add_kpath,**kwargs)
self.clone_from(prim)
#end def become_primitive
def add_kpoints(self,kpoints,kweights=None,unique=False,recenter=True,cell_unit=False):
if kweights is None:
kweights = ones((len(kpoints),))
#end if
if cell_unit:
kpoints = np.dot(array(kpoints),self.kaxes)
#end if
self.kpoints = append(self.kpoints,kpoints,axis=0)
self.kweights = append(self.kweights,kweights)
if unique:
self.unique_kpoints()
#end if
if recenter:
self.recenter_k() #added because qmcpack cannot handle kpoints outside the box
#end if
if self.is_tiled():
kp,kw = self.kfold(self.tmatrix,kpoints,kweights)
self.folded_structure.add_kpoints(kp,kw,unique=unique)
#end if
#end def add_kpoints
# test needed
def clear_kpoints(self):
self.kpoints = empty((0,self.dim))
self.kweights = empty((0,))
if self.folded_structure!=None:
self.folded_structure.clear_kpoints()
#end if
#end def clear_kpoints
def kgrid_from_kspacing(self,kspacing):
kgrid = []
for ka in self.kaxes:
km = np.linalg.norm(ka)
kg = int(np.ceil(km/kspacing))
kgrid.append(kg)
#end for
return tuple(kgrid)
#end def kgrid_from_kspacing
def add_kmesh(self,kgrid=None,kshift=None,unique=False,kspacing=None):
if kspacing is not None:
kgrid = self.kgrid_from_kspacing(kspacing)
elif kgrid is None:
self.error('kgrid input is required by add_kmesh')
#end if
self.add_kpoints(kmesh(self.kaxes,kgrid,kshift),unique=unique)
#end def add_kmesh
def add_symmetrized_kmesh(self,kgrid=None,kshift=(0,0,0),kspacing=None):
# find kgrid from kspacing, if requested
if kspacing is not None:
kgrid = self.kgrid_from_kspacing(kspacing)
elif kgrid is None:
self.error('kgrid input is required by add_kmesh')
#end if
# get spglib cell data structure
cell = self.spglib_cell()
# get the symmetry mapping
kmap,kpoints_int = spglib.get_ir_reciprocal_mesh(
kgrid,
cell,
is_shift=kshift
)
# create the Monkhorst-Pack mesh
kshift = array(kshift,dtype=float)
okgrid = 1.0/array(kgrid,dtype=float)
kpoints = empty(kpoints_int.shape,dtype=float)
for i,ki in enumerate(kpoints_int):
kpoints[i] = (ki+kshift)*okgrid
#end for
kpoints = dot(kpoints,self.kaxes)
# reduce to only the symmetric kpoints with weights
kwmap = obj()
for ik in kmap:
if ik not in kwmap:
kwmap[ik] = 1
else:
kwmap[ik] += 1
#end if
#end for
nkpoints = len(kwmap)
kpoints_symm = empty((nkpoints,self.dim),dtype=float)
kweights_symm = empty((nkpoints,),dtype=float)
n = 0
for ik,kw in kwmap.items():
kpoints_symm[n] = kpoints[ik]
kweights_symm[n] = kw
n+=1
#end for
self.add_kpoints(kpoints_symm,kweights_symm)
#end def add_symmetrized_kmesh
def kpoints_unit(self,kpoints=None):
if kpoints is None:
kpoints = self.kpoints
#end if
return dot(kpoints,inv(self.kaxes))
#end def kpoints_unit
def kpoints_reduced(self,kpoints=None):
if kpoints is None:
kpoints = self.kpoints
#end if
return kpoints*self.scale/(2*pi)
#end def kpoints_reduced
def kpoints_qmcpack(self,kpoints=None):
if kpoints is None:
kpoints = self.kpoints.copy()
#end if
kpoints = self.recenter_k(kpoints,kcenter=(0,0,0))
kpoints = self.kpoints_unit(kpoints)
kpoints = -kpoints
return kpoints
#end def kpoints_qmcpack
# test needed
def inversion_symmetrize_kpoints(self,tol=1e-10,folded=False):
kp = self.kpoints
kaxes = self.kaxes
ntable,dtable = self.neighbor_table(kp,-kp,kaxes,distances=True)
pairs = set()
keep = empty((len(kp),),dtype=bool)
keep[:] = True
for i in range(len(dtable)):
if keep[i] and dtable[i,0]<tol:
j = ntable[i,0]
if j!=i and keep[j]:
keep[j] = False
self.kweights[i] += self.kweights[j]
#end if
#end if
#end for
self.kpoints = self.kpoints[keep]
self.kweights = self.kweights[keep]
if folded and self.folded_structure!=None:
self.folded_structure.inversion_symmetrize_kpoints(tol)
#end if
#end def inversion_symmetrize_kpoints
# test needed
def unique_points(self,points,axes,weights=None,tol=1e-10):
pmap = obj()
npoints = len(points)
if npoints>0:
if weights is None:
weights = ones((npoints,),dtype=int)
#end if
ntable,dtable = self.neighbor_table(points,points,axes,distances=True)
keep = empty((npoints,),dtype=bool)
keep[:] = True
pmo = obj()
for i in range(npoints):
if keep[i]:
pm = []
jn=0
while jn<npoints and dtable[i,jn]<tol:
j = ntable[i,jn]
pm.append(j)
if j!=i and keep[j]:
keep[j] = False
weights[i] += weights[j]
#end if
jn+=1
#end while
pmo[i] = set(pm)
#end if
#end for
points = points[keep]
weights = weights[keep]
j=0
for i in range(len(keep)):
if keep[i]:
pmap[j] = pmo[i]
j+=1
#end if
#end for
#end if
return points,weights,pmap
#end def unique_points
# test needed
def unique_points_fast(self,points,axes,weights=None,tol=1e-10):
# use an O(N) cell table instead of an O(N^2) neighbor table
pmap = obj()
points = array(points)
axes = array(axes)
npoints = len(points)
if npoints>0:
if weights is None:
weights = ones((npoints,),dtype=int)
else:
weights = array(weights)
#end if
keep = ones((npoints,),dtype=bool)
# place all the points in the box, converted to unit coords
upoints = array(points)
axinv = inv(axes)
for i in range(len(points)):
u = dot(points[i],axinv)
upoints[i] = u-floor(u)
#end for
# create an integer array of cell indices
axmax = -1.0
for a in axes:
axmax = max(axmax,norm(a))
#end for
# make an integer space corresponding to 1e-7 self.units spatial resolution
cmax = uint64(1e7)*uint64(ceil(axmax))
ipoints = array(around(cmax*upoints),dtype=uint64)
ipoints[ipoints==cmax] = 0 # make the outer boundary the same as the inner boundary
# load the cell table with point indices
# points in the same cell are identical
ctable = obj()
i=0
for ip in ipoints:
ip = tuple(ip)
if ip not in ctable:
ctable[ip] = i
pmap[i] = [i]
else:
j = ctable[ip]
keep[i] = False
weights[j] += weights[i]
pmap[j].append(i)
#end if
i+=1
#end for
points = points[keep]
weights = weights[keep]
#end if
return points,weights,pmap
#end def unique_points_fast
# test needed
def unique_positions(self,tol=1e-10,folded=False):
pos,weights,pmap = self.unique_points(self.pos,self.axes)
if len(pos)!=len(self.pos):
self.pos = pos
#end if
if folded and self.folded_structure!=None:
self.folded_structure.unique_positions(tol)
#end if
return pmap
#end def unique_positions
# test needed
def unique_kpoints(self,tol=1e-10,folded=False):
kmap = obj()
kp = self.kpoints
if len(kp)>0:
kaxes = self.kaxes
ntable,dtable = self.neighbor_table(kp,kp,kaxes,distances=True)
npoints = len(kp)
keep = empty((len(kp),),dtype=bool)
keep[:] = True
kmo = obj()
for i in range(npoints):
if keep[i]:
km = []
jn=0
while jn<npoints and dtable[i,jn]<tol:
j = ntable[i,jn]
km.append(j)
if j!=i and keep[j]:
keep[j] = False
self.kweights[i] += self.kweights[j]
#end if
jn+=1
#end while
kmo[i] = set(km)
#end if
#end for
self.kpoints = self.kpoints[keep]
self.kweights = self.kweights[keep]
j=0
for i in range(len(keep)):
if keep[i]:
kmap[j] = kmo[i]
j+=1
#end if
#end for
#end if
if folded and self.folded_structure!=None:
self.folded_structure.unique_kpoints(tol)
#end if
return kmap
#end def unique_kpoints
def kmap(self):
kmap = None
if self.folded_structure!=None:
fs = self.folded_structure
self.kpoints = array(fs.kpoints)
self.kweights = array(fs.kweights)
kmap = self.unique_kpoints()
#end if
return kmap
#end def kmap
# test needed
def select_twist(self,selector='smallest',tol=1e-6):
index = None
invalid_selector = False
if isinstance(selector,str):
if selector=='smallest':
index = (self.kpoints**2).sum(1).argmin()
elif selector=='random':
index = randint(0,len(self.kpoints)-1)
else:
invalid_selector = True
#end if
elif isinstance(selector,(tuple,list,ndarray)):
ku_sel = array(selector,dtype=float)
n = 0
for ku in self.kpoints_unit():
if norm(ku-ku_sel)<tol:
index = n
break
#end if
n+=1
#end for
if index is None:
self.error('cannot identify twist number\ntwist requested: {0}\ntwists present: {1}'.format(ku_sel,sorted([tuple(k) for k in self.kpoints_unit()])))
#end if
else:
invalid_selector = True
#end if
if invalid_selector:
self.error('cannot identify twist number\ninvalid selector provided: {0}\nvalid string inputs for selector: smallest, random\nselector can also be a length 3 tuple, list or array (a twist vector)'.format(selector))
#end if
return index
#end def select_twist
# test needed
def fold_pos(self,large,tol=0.001):
vratio = large.volume()/self.volume()
if abs(vratio-int(around(vratio)))>1e-6:
self.error('cannot fold positions from large cell into current one\nlarge cell volume is not an integer multiple of the current one\nlarge cell volume: {0}\ncurrent cell volume: {1}\nvolume ratio: {2}'.format(large.volume(),self.volume(),vratio))
T,success = large.tilematrix(self,status=True)
if not success:
self.error('cannot fold positions from large cell into current one\ncells are related by non-integer tilematrix')
#end if
nnearest = int(around(vratio))
self.elem = large.elem.copy()
self.pos = large.pos.copy()
self.recenter()
nt,dt = self.neighbor_table(distances=True)
nt = nt[:,:nnearest]
dt = dt[:,:nnearest]
if dt.ravel().max()>tol:
self.error('cannot fold positions from large cell into current one\npositions of equivalent atoms are further apart than the tolerance\nmax distance encountered: {0}\ntolerance: {1}'.format(dt.ravel().max(),tol))
#end if
counts = zeros((len(self.pos),),dtype=int)
for n in nt.ravel():
counts[n] += 1
#end for
if (counts!=nnearest).any():
self.error('cannot fold positions from large cell into current one\neach atom must have {0} equivalent positions\nsome atoms found with the following equivalent position counts: {1}'.format(nnearest,counts[counts!=nnearest]))
#end if
ind_visited = set()
neigh_map = obj()
keep = []
n=0
for nset in nt:
if n not in ind_visited:
neigh_map[n] = nset
keep.append(n)
for ind in nset:
ind_visited.add(ind)
#end for
#end if
n+=1
#end for
if len(ind_visited)!=len(self.pos):
self.error('cannot fold positions from large cell into current one\nsome equivalent atoms could not be identified')
#end if
new_elem = []
new_pos = []
for n in keep:
nset = neigh_map[n]
elist = list(set(self.elem[nset]))
if len(elist)!=1:
self.error('cannot fold positions from large cell into current one\nspecies of some equivalent atoms do not match')
#end if
new_elem.append(elist[0])
new_pos.append(self.pos[nset].mean(0))
#end for
self.set_elem(new_elem)
self.set_pos(new_pos)
#end def fold_pos
def pos_unit(self,pos=None):
if pos is None:
pos = self.pos
#end if
return dot(pos,inv(self.axes))
#end def pos_unit
def pos_to_cartesian(self):
self.pos = dot(self.pos,self.axes)
#end def pos_to_cartesian
# test needed
def at_Gpoint(self):
kpu = self.kpoints_unit()
kg = array([0,0,0])
return len(kpu)==1 and norm(kg-kpu[0])<1e-6
#end def at_Gpoint
# test needed
def at_Lpoint(self):
kpu = self.kpoints_unit()
kg = array([.5,.5,.5])
return len(kpu)==1 and norm(kg-kpu[0])<1e-6
#end def at_Lpoint
# test needed
def at_real_kpoint(self):
kpu = 2*self.kpoints_unit()
return len(kpu)==1 and abs(kpu-around(kpu)).sum()<1e-6
#end def at_real_kpoint
# test needed
def bonds(self,neighbors,vectors=False):
if self.dim!=3:
self.error('bonds is currently only implemented for 3 dimensions')
#end if
natoms,dim = self.pos.shape
centers = empty((natoms,neighbors,dim))
distances = empty((natoms,neighbors))
vect = empty((natoms,neighbors,dim))
t = self.tile((3,3,3))
t.recenter(self.center)
nn = nearest_neighbors(neighbors+1,t.pos,self.pos)
for i in range(natoms):
ii = nn[i,0]
n=0
for jj in nn[i,1:]:
p1 = t.pos[ii]
p2 = t.pos[jj]
centers[i,n,:] = (p1+p2)/2
distances[i,n]= sqrt(((p1-p2)**2).sum())
vect[i,n,:] = p2-p1
n+=1
#end for
#end for
sn = self.copy()
nnr = nn[:,1:].ravel()
sn.set_elem(t.elem[nnr])
sn.pos = t.pos[nnr]
sn.recenter()
indices = self.locate(sn.pos)
indices = indices.reshape(natoms,neighbors)
if not vectors:
return indices,centers,distances
else:
return indices,centers,distances,vect
#end if
#end def bonds
# test needed
def displacement(self,reference,map=False):
if self.dim!=3:
self.error('displacement is currently only implemented for 3 dimensions')
#end if
ref = reference.tile((3,3,3))
ref.recenter(reference.center)
rmap = array(3**3*list(range(len(reference.pos)),dtype=int))
nn = nearest_neighbors(1,ref.pos,self.pos).ravel()
displacement = self.pos - ref.pos[nn]
if not map:
return displacement
else:
return displacement,rmap[nn]
#end if
#end def displacement
# test needed
def scalar_displacement(self,reference):
return sqrt((self.displacement(reference)**2).sum(1))
#end def scalar_displacement
# test needed
def distortion(self,reference,neighbors):
if self.dim!=3:
self.error('distortion is currently only implemented for 3 dimensions')
#end if
if reference.volume()/self.volume() < 1.1:
ref = reference.tile((3,3,3))
ref.recenter(reference.center)
else:
ref = reference
#end if
rbi,rbc,rbl,rbv = ref.bonds(neighbors,vectors=True)
sbi,sbc,sbl,sbv = self.bonds(neighbors,vectors=True)
nn = nearest_neighbors(1,reference.pos,self.pos).ravel()
distortion = empty(sbv.shape)
magnitude = empty((len(self.pos),))
for i in range(len(self.pos)):
ir = nn[i]
bonds = sbv[i]
rbonds = rbv[ir]
ib = empty((neighbors,),dtype=int)
ibr = empty((neighbors,),dtype=int)
r = list(range(neighbors))
rr = list(range(neighbors))
for n in range(neighbors):
mindist = 1e99
ibmin = -1
ibrmin = -1
for nb in r:
for nbr in rr:
d = norm(bonds[nb]-rbonds[nbr])
if d<mindist:
mindist=d
ibmin=nb
ibrmin=nbr
#end if
#end for
#end for
ib[n]=ibmin
ibr[n]=ibrmin
r.remove(ibmin)
rr.remove(ibrmin)
#end for
#end for
d = bonds[ib]-rbonds[ibr]
distortion[i] = d
magnitude[i] = (sqrt((d**2).sum(axis=1))).sum()
#end for
return distortion,magnitude
#end def distortion
# test needed
def bond_compression(self,reference,neighbors):
ref = reference
rbi,rbc,rbl = ref.bonds(neighbors)
sbi,sbc,sbl = self.bonds(neighbors)
bondlen = rbl.mean()
return abs(1.-sbl/bondlen).max(axis=1)
#end def bond_compression
# test needed
def boundary(self,dims=(0,1,2),dtol=1e-6):
dim_eff = len(dims)
natoms,dim = self.pos.shape
bdims = array(dim*[False])
for d in dims:
bdims[d] = True
#end for
p = self.pos[:,bdims]
indices = convex_hull(p,dim_eff,dtol)
return indices
#end def boundary
def embed(self,small,dims=(0,1,2),dtol=1e-6,utol=1e-6):
small = small.copy()
small.recenter()
center = array(self.center)
self.recenter(small.center)
bind = small.boundary(dims,dtol)
bpos = small.pos[bind]
belem= small.elem[bind]
nn = nearest_neighbors(1,self.pos,bpos).ravel()
mpos = self.pos[nn]
dr = (mpos-bpos).mean(0)
for i in range(len(bpos)):
bpos[i]+=dr
#end for
dmax = sqrt(((mpos-bpos)**2).sum(1)).max()
for i in range(len(small.pos)):
small.pos[i]+=dr
#end for
ins,surface = small.inside(self.pos,tol=utol,separate=True)
replaced = empty((len(self.pos),),dtype=bool)
replaced[:] = False
inside = replaced.copy()
inside[ins] = True
nn = nearest_neighbors(1,self.pos,small.pos).ravel()
elist = list(self.elem)
plist = list(self.pos)
pos = small.pos
elem = small.elem
for i in range(len(pos)):
n = nn[i]
if not replaced[n]:
elist[n] = elem[i]
plist[n] = pos[i]
replaced[n] = True
else:
elist.append(elem[i])
plist.append(pos[i])
#end if
#end for
remove = arange(len(self.pos))[inside & logical_not(replaced)]
remove.sort()
remove = flipud(remove)
for i in remove:
elist.pop(i)
plist.pop(i)
#end for
self.set_elem(elist)
self.pos = array(plist)
self.recenter(center)
return dmax
#end def embed
# test needed
def shell(self,cell,neighbors,direction='in'):
if self.dim!=3:
self.error('shell is currently only implemented for 3 dimensions')
#end if
dd = {'in':equate,'out':negate}
dir = dd[direction]
natoms,dim=self.pos.shape
ncells=3**3
ntile = ncells*natoms
pos = empty((ntile,dim))
ind = empty((ntile,),dtype=int)
oind = list(range(natoms))
for nt in range(ncells):
n=nt*natoms
ind[n:n+natoms]=oind[:]
pos[n:n+natoms]=self.pos[:]
#end for
nt=0
for k in -1,0,1:
for j in -1,0,1:
for i in -1,0,1:
iv = array([[i,j,k]])
v = dot(iv,self.axes)
for d in range(dim):
ns = nt*natoms
ne = ns+natoms
pos[ns:ne,d] += v[0,d]
#end for
nt+=1
#end for
#end for
#end for
inside = empty(ntile,)
inside[:]=False
ins = cell.inside(pos)
inside[ins]=True
iishell = set()
nn = nearest_neighbors(neighbors,pos)
for ii in range(len(nn)):
for jj in nn[ii]:
in1 = inside[ii]
in2 = inside[jj]
if dir(in1 and not in2):
iishell.add(ii)
#end if
if dir(in2 and not in1):
iishell.add(jj)
#end if
#end if
#end if
ishell = ind[list(iishell)]
return ishell
#end def shell
def interpolate(self,other,images,min_image=True,recenter=True,match_com=False,chained=False):
s1 = self.copy()
s2 = other.copy()
s1.remove_folded()
s2.remove_folded()
if s2.units!=s1.units:
s2.change_units(s1.units)
#end if
if (s1.elem!=s2.elem).any():
self.error('cannot interpolate structures, atoms do not match\n atoms1: {0}\n atoms2: {1}'.format(s1.elem,s2.elem))
#end if
structures = []
npath = images+2
c1 = s1.center
c2 = s2.center
ax1 = s1.axes
ax2 = s2.axes
pos1 = s1.pos
pos2 = s2.pos
min_image &= abs(ax1-ax2).max()<1e-6
if min_image:
dp = self.min_image_vectors(pos1,pos2,ax1,pairs=False)
pos2 = pos1 + dp
#end if
if match_com:
com1 = pos1.mean(axis=0)
com2 = pos2.mean(axis=1)
dcom = com1-com2
for n in range(len(pos2)):
pos2[n] += dcom
#end for
if chained:
other.pos = pos2
#end if
#end if
for n in range(npath):
f1 = 1.-float(n)/(npath-1)
f2 = 1.-f1
center = f1*c1 + f2*c2
axes = f1*ax1 + f2*ax2
pos = f1*pos1 + f2*pos2
s = s1.copy()
s.reset_axes(axes)
s.center = center
s.pos = pos
if recenter:
s.recenter()
#end if
structures.append(s)
#end for
return structures
#end def interpolate
# returns madelung potential constant v_M
# see equation 7 in PRB 78 125106 (2008)
def madelung(self,axes=None,tol=1e-10):
if self.dim!=3:
self.error('madelung is currently only implemented for 3 dimensions')
#end if
if axes is None:
a = self.axes.T.copy()
else:
a = axes.T.copy()
#end if
if self.units!='B':
a = convert(a,self.units,'B')
#end if
volume = abs(det(a))
b = 2*pi*inv(a).T
rconv = 8*(3.*volume/(4*pi))**(1./3)
kconv = 2*pi/rconv
gconst = -1./(4*kconv**2)
vmc = -pi/(kconv**2*volume)-2*kconv/sqrt(pi)
nshells = 20
vshell = [0.]
p = Sobj()
m = Sobj()
for n in range(1,nshells+1):
i = mgrid[-n:n+1,-n:n+1,-n:n+1]
i = i.reshape(3,(2*n+1)**3)
R = sqrt((dot(a,i)**2).sum(0))
G2 = (dot(b,i)**2).sum(0)
izero = n + n*(2*n+1) + n*(2*n+1)**2
p.R = R[0:izero]
p.G2 = G2[0:izero]
m.R = R[izero+1:]
m.G2 = G2[izero+1:]
domains = [p,m]
vshell.append(0.)
for d in domains:
vshell[n] += (erfc(kconv*d.R)/d.R).sum() + 4*pi/volume*(exp(gconst*d.G2)/d.G2).sum()
#end for
if abs(vshell[n]-vshell[n-1])<tol:
break
#end if
#end for
vm = vmc + vshell[-1]
if axes is None:
self.Vmadelung = vm
#end if
return vm
#end def madelung
def makov_payne(self,q=1,eps=1.0,units='Ha',order=1):
if order!=1:
self.error('Only first order Makov-Payne correction is currently supported.')
#end if
if 'Vmadelung' not in self:
vm = self.madelung()
else:
vm = self.Vmadelung
#end if
mp = -0.5*q**2*vm/eps
if units!='Ha':
mp = convert(mp,'Ha',units)
#end if
return mp
#end def makov_payne
def read(self,filepath,format=None,elem=None,block=None,grammar='1.1',cell='prim',contents=False):
if os.path.exists(filepath):
path,file = os.path.split(filepath)
if format is None:
if '.' in file:
name,format = file.rsplit('.',1)
elif file.lower().endswith('poscar'):
format = 'poscar'
else:
self.error('file format could not be determined\nunrecognized file: {0}'.format(filepath))
#end if
#end if
elif not contents:
self.error('file does not exist: {0}'.format(filepath))
#end if
if format is None:
self.error('file format must be provided')
#end if
self.mag = None
self.frozen = None
format = format.lower()
if format=='xyz':
self.read_xyz(filepath)
elif format=='xsf':
self.read_xsf(filepath)
elif format=='poscar':
self.read_poscar(filepath,elem=elem)
elif format=='cif':
self.read_cif(filepath,block=block,grammar=grammar,cell=cell)
elif format=='fhi-aims':
self.read_fhi_aims(filepath)
else:
self.error('cannot read structure from file\nunsupported file format: {0}'.format(format))
#end if
if self.has_axes():
self.set_bconds('ppp')
#end if
#end def read
def read_xyz(self,filepath):
elem = []
pos = []
if os.path.exists(filepath):
lines = open(filepath,'r').read().strip().splitlines()
else:
lines = filepath.strip().splitlines() # "filepath" is file contents
#end if
if len(lines)>1:
ntot = int(lines[0].strip())
natoms = 0
e = None
p = None
try:
tokens = lines[1].split()
if len(tokens)==4:
e = tokens[0]
p = array(tokens[1:],float)
#end if
except:
None
#end try
if p is not None:
elem.append(e)
pos.append(p)
natoms+=1
#end if
if len(lines)>2:
for l in lines[2:]:
tokens = l.split()
if len(tokens)==4:
elem.append(tokens[0])
pos.append(array(tokens[1:],float))
natoms+=1
if natoms==ntot:
break
#end if
#end if
#end for
#end if
if natoms!=ntot:
self.error('xyz file read failed\nattempted to read file: {0}\nnumber of atoms expected: {1}\nnumber of atoms found: {2}'.format(filepath,ntot,natoms))
#end if
#end if
self.dim = 3
self.set_elem(elem)
self.pos = array(pos)
self.units = 'A'
#end def read_xyz
def read_xsf(self,filepath):
if isinstance(filepath,XsfFile):
f = filepath
elif os.path.exists(filepath):
f = XsfFile(filepath)
else:
f = XsfFile()
f.read_text(filepath) # "filepath" is file contents
#end if
elem = []
for n in f.elem:
if isinstance(n,str):
elem.append(n)
else:
elem.append(pt.simple_elements[n].symbol)
#end if
#end for
self.dim = 3
self.units = 'A'
self.reset_axes(f.primvec)
self.set_elem(elem)
self.pos = f.pos
#end def read_xsf
def read_poscar(self,filepath,elem=None):
if os.path.exists(filepath):
lines = open(filepath,'r').read().splitlines()
else:
lines = filepath.splitlines() # "filepath" is file contents
#end if
nlines = len(lines)
min_lines = 8
if nlines<min_lines:
self.error('POSCAR file must have at least {0} lines\n only {1} lines found'.format(min_lines,nlines))
#end if
dim = 3
scale = float(lines[1].strip())
axes = empty((dim,dim))
axes[0] = array(lines[2].split(),dtype=float)
axes[1] = array(lines[3].split(),dtype=float)
axes[2] = array(lines[4].split(),dtype=float)
if scale<0.0:
scale = abs(scale)/det(axes)
#end if
axes = scale*axes
tokens = lines[5].split()
if tokens[0].isdigit():
counts = array(tokens,dtype=int)
if elem is None:
self.error('variable elem must be provided to read_poscar() to assign atomic species to positions for POSCAR format')
elif len(elem)!=len(counts):
self.error('one elem must be given for each element count in the POSCAR file\n number of elem counts: {0}\n number of elem given: {1}'.format(len(counts),len(elem)))
#end if
lcur = 6
else:
elem = tokens
counts = array(lines[6].split(),dtype=int)
lcur = 7
#end if
species = elem
# relabel species that have multiple occurances
sset = set(species)
for spec in sset:
if species.count(spec)>1:
cnt=0
for n in range(len(species)):
specn = species[n]
if specn==spec:
cnt+=1
species[n] = specn+str(cnt)
#end if
#end for
#end if
#end for
elem = []
for i in range(len(counts)):
elem.extend(counts[i]*[species[i]])
#end for
self.dim = dim
self.units = 'A'
self.reset_axes(axes)
if lcur<len(lines) and len(lines[lcur])>0:
c = lines[lcur].lower().strip()[0]
lcur+=1
else:
return
#end if
selective_dynamics = c=='s'
if selective_dynamics: # Selective dynamics
if lcur<len(lines) and len(lines[lcur])>0:
c = lines[lcur].lower().strip()[0]
lcur+=1
else:
return
#end if
#end if
cartesian = c=='c' or c=='k'
npos = counts.sum()
if lcur+npos>len(lines):
return
#end if
spos = []
for i in range(npos):
spos.append(lines[lcur+i].split())
#end for
spos = array(spos)
pos = array(spos[:,0:3],dtype=float)
if cartesian:
pos = scale*pos
else:
pos = dot(pos,axes)
#end if
self.set_elem(elem)
self.pos = pos
if selective_dynamics or spos.shape[1]>3:
move = array(spos[:,3:6],dtype=str)
self.freeze(list(range(self.size())),directions=move=='F')
#end if
#end def read_poscar
def read_cif(self,filepath,block=None,grammar='1.1',cell='prim'):
axes,elem,pos,units = read_cif(filepath,block,grammar,cell,args_only=True)
self.dim = 3
self.set_axes(axes)
self.set_elem(elem)
self.pos = pos
self.units = units
#end def read_cif
# test needed
def read_fhi_aims(self,filepath):
if os.path.exists(filepath):
lines = open(filepath,'r').read().splitlines()
else:
lines = filepath.splitlines() # "filepath" is contents
#end if
axes = []
pos = []
elem = []
constrain_relax = []
unit_pos = False
for line in lines:
ls = line.strip()
if len(ls)>0 and ls[0]!='#':
tokens = ls.split()
t0 = tokens[0]
if t0=='lattice_vector':
axes.append(tokens[1:])
elif t0=='atom_frac':
pos.append(tokens[1:4])
elem.append(tokens[4])
unit_pos = True
elif t0=='atom':
pos.append(tokens[1:4])
elem.append(tokens[4])
elif t0=='constrain_relaxation':
constrain_relax.append(tokens[1])
elif t0.startswith('initial'):
None
else:
#None
self.error('unrecogonized or not yet supported token in fhi-aims geometry file: {0}'.format(t0))
#end if
#end if
#end for
axes = array(axes,dtype=float)
pos = array(pos,dtype=float)
if unit_pos:
pos = dot(pos,axes)
#end if
self.dim = 3
if len(axes)>0:
self.set_axes(axes)
#end if
self.set_elem(elem)
self.pos = pos
self.units = 'A'
if len(constrain_relax)>0:
constrain_relax = array(constrain_relax)
self.freeze(list(range(self.size())),directions=constrain_relax=='.true.')
#end if
#end def read_fhi_aims
def write(self,filepath=None,format=None):
if filepath is None and format is None:
self.error('please specify either the filepath or format arguments to write()')
elif format is None:
if '.' in filepath:
format = filepath.split('.')[-1]
else:
self.error('file format could not be determined\neither request the format directly with the format keyword or add a file format extension to the file name')
#end if
#end if
format = format.lower()
if format=='xyz':
c = self.write_xyz(filepath)
elif format=='xsf':
c = self.write_xsf(filepath)
elif format=='poscar':
c = self.write_poscar(filepath)
elif format=='fhi-aims':
c = self.write_fhi_aims(filepath)
else:
self.error('file format {0} is unrecognized'.format(format))
#end if
return c
#end def write
def write_xyz(self,filepath=None,header=True,units='A'):
if self.dim!=3:
self.error('write_xyz is currently only implemented for 3 dimensions')
#end if
s = self.copy()
s.change_units(units)
c=''
if header:
c += str(len(s.elem))+'\n\n'
#end if
for i in range(len(s.elem)):
e = s.elem[i]
p = s.pos[i]
c+=' {0:2} {1:12.8f} {2:12.8f} {3:12.8f}\n'.format(e,p[0],p[1],p[2])
#end for
if filepath!=None:
open(filepath,'w').write(c)
#end if
return c
#end def write_xyz
def write_xsf(self,filepath=None):
if self.dim!=3:
self.error('write_xsf is currently only implemented for 3 dimensions')
#end if
s = self.copy()
s.change_units('A')
c = ' CRYSTAL\n'
c += ' PRIMVEC\n'
for a in s.axes:
c += ' {0:12.8f} {1:12.8f} {2:12.8f}\n'.format(*a)
#end for
c += ' PRIMCOORD\n'
c += ' {0} 1\n'.format(len(s.elem))
for i in range(len(s.elem)):
e = s.elem[i]
identified = e in pt.elements
if not identified:
if len(e)>2:
e = e[0:2]
elif len(e)==2:
e = e[0:1]
#end if
identified = e in pt.elements
#end if
if not identified:
self.error('{0} is not an element\nxsf file cannot be written'.format(e))
#end if
enum = pt.elements[e].atomic_number
r = s.pos[i]
c += ' {0:>3} {1:12.8f} {2:12.8f} {3:12.8f}\n'.format(enum,r[0],r[1],r[2])
#end for
if filepath!=None:
open(filepath,'w').write(c)
#end if
return c
#end def write_xsf
def write_poscar(self,filepath=None):
s = self.copy()
s.change_units('A')
species,species_count = s.order_by_species()
poscar = PoscarFile()
poscar.scale = 1.0
poscar.axes = s.axes
poscar.elem = species
poscar.elem_count = species_count
poscar.coord = 'cartesian'
poscar.pos = s.pos
c = poscar.write_text()
if filepath is not None:
open(filepath,'w').write(c)
#end if
return c
#end def write_poscar
# test needed
def write_fhi_aims(self,filepath=None):
s = self.copy()
s.change_units('A')
c = ''
c+='\n'
for a in s.axes:
c += 'lattice_vector {0: 12.8f} {1: 12.8f} {2: 12.8f}\n'.format(*a)
#end for
c+='\n'
for p,e in zip(self.pos,self.elem):
c += 'atom_frac {0: 12.8f} {1: 12.8f} {2: 12.8f} {3}\n'.format(p[0],p[1],p[2],e)
#end for
if filepath!=None:
open(filepath,'w').write(c)
#end if
return c
#end def write_fhi_aims
def plot2d_ax(self,ix=0,iy=1,*args,**kwargs):
if self.dim!=3:
self.error('plot2d_ax is currently only implemented for 3 dimensions')
#end if
iz = list(set([0,1,2])-set([ix,iy]))[0]
ax = self.axes.copy()
a = self.axes[iz]
dc = self.center-ax.sum(0)/2
pp = array([0*a,ax[ix],ax[ix]+ax[iy],ax[iy],0*a])
for i in range(len(pp)):
pp[i]+=dc
pp[i]-=dot(a,pp[i])/dot(a,a)*a
#end for
plot(pp[:,ix],pp[:,iy],*args,**kwargs)
#end def plot2d_ax
def plot2d_pos(self,ix=0,iy=1,*args,**kwargs):
if self.dim!=3:
self.error('plot2d_pos is currently only implemented for 3 dimensions')
#end if
iz = list(set([0,1,2])-set([ix,iy]))[0]
pp = self.pos.copy()
a = self.axes[iz]
for i in range(len(pp)):
pp[i] -= dot(a,pp[i])/dot(a,a)*a
#end for
plot(pp[:,ix],pp[:,iy],*args,**kwargs)
#end def plot2d_pos
def plot2d_points(self,points,ix=0,iy=1,*args,**kwargs):
if self.dim!=3:
self.error('plot2d_points is currently only implemented for 3 dimensions')
#end if
iz = list(set([0,1,2])-set([ix,iy]))[0]
pp = np.array(points,dtype=float)
a = self.axes[iz]
for i in range(len(pp)):
pp[i] -= dot(a,pp[i])/dot(a,a)*a
#end for
plot(pp[:,ix],pp[:,iy],*args,**kwargs)
#end def plot2d_points
def plot2d(self,pos_style='b.',ax_style='k-'):
if self.dim!=3:
self.error('plot2d is currently only implemented for 3 dimensions')
#end if
subplot(1,3,1)
self.plot2d_ax(0,1,ax_style,lw=2)
self.plot2d_pos(0,1,pos_style)
title('a1,a2')
subplot(1,3,2)
self.plot2d_ax(1,2,ax_style,lw=2)
self.plot2d_pos(1,2,pos_style)
title('a2,a3')
subplot(1,3,3)
self.plot2d_ax(2,0,ax_style,lw=2)
self.plot2d_pos(2,0,pos_style)
title('a3,a1')
#end def plot2d
def plot2d_kax(self,ix,iy,*args,**kwargs):
if self.dim!=3:
self.error('plot2d_ax is currently only implemented for 3 dimensions')
#end if
iz = list(set([0,1,2])-set([ix,iy]))[0]
ax = self.kaxes.copy()
a = ax[iz]
dc = 0*a
pp = array([0*a,ax[ix],ax[ix]+ax[iy],ax[iy],0*a])
for i in range(len(pp)):
pp[i]+=dc
pp[i]-=dot(a,pp[i])/dot(a,a)*a
#end for
plot(pp[:,ix],pp[:,iy],*args,**kwargs)
#end def plot2d_kax
def plot2d_kp(self,ix,iy,*args,**kwargs):
if self.dim!=3:
self.error('plot2d_kp is currently only implemented for 3 dimensions')
#end if
iz = list(set([0,1,2])-set([ix,iy]))[0]
pp = self.kpoints.copy()
a = self.kaxes[iz]
for i in range(len(pp)):
pp[i] -= dot(a,pp[i])/dot(a,a)*a
#end for
plot(pp[:,ix],pp[:,iy],*args,**kwargs)
#end def plot2d_kp
def show(self,viewer='vmd',filepath='/tmp/tmp.xyz'):
if self.dim!=3:
self.error('show is currently only implemented for 3 dimensions')
#end if
self.write_xyz(filepath)
os.system(viewer+' '+filepath)
#end def show
# minimal ASE Atoms-like interface to Structure objects for spglib
def get_cell(self):
return self.axes.copy()
#end def get_cell
def get_scaled_positions(self):
return self.pos_unit()
#end def get_scaled_positions
def get_number_of_atoms(self):
return len(self.elem)
#end def get_number_of_atoms
def get_atomic_numbers(self):
an = []
for e in self.elem:
iselem,esymb = is_element(e,symbol=True)
if not iselem:
self.error('Atomic symbol, {}, not recognized'.format(esymb))
else:
an.append(ptable[esymb].atomic_number)
#end if
#end for
return array(an,dtype='intc')
#end def get_atomic_numbers
def get_magnetic_moments(self):
self.error('structure objects do not currently support magnetic moments')
#end def get_magnetic_moments
# direct spglib interface
def spglib_cell(self):
lattice = self.axes.copy()
positions = self.pos_unit()
numbers = self.get_atomic_numbers()
cell = (lattice,positions,numbers)
return cell
#end def spglib_cell
def get_symmetry(self,symprec=1e-5):
cell = self.spglib_cell()
return spglib.get_symmetry(cell,symprec=symprec)
#end def get_symmetry
def get_symmetry_dataset(self,symprec=1e-5, angle_tolerance=-1.0, hall_number=0):
cell = self.spglib_cell()
ds = spglib.get_symmetry_dataset(
cell,
symprec = symprec,
angle_tolerance = angle_tolerance,
hall_number = hall_number,
)
return ds
#end def get_symmetry
# functions based on direct spglib interface
def symmetry_data(self,*args,**kwargs):
ds = self.get_symmetry_dataset(*args,**kwargs)
if isinstance(ds,dict):
# Spglib version < v.2.5.0, see https://spglib.readthedocs.io/en/stable/releases.html
ds = obj(ds)
elif isinstance(ds,spglib.SpglibDataset):
# Spglib version >= v.2.5.0
ds = obj(ds.__dict__)
else:
raise TypeError('Invalid symmetry dataset type: {}'.format(type(ds)))
for k,v in ds.items():
if isinstance(v,dict):
ds[k] = obj(v)
#end if
#end for
return ds
#end def symmetry_data
def bravais_lattice_name(self,symm_data=None):
if symm_data is None:
symm_data = self.symmetry_data()
#end if
sg = symm_data.number
name = symm_data.international
if not isinstance(sg,int) or sg<1 or sg>230:
self.error('Invalid space group from spglib: {}'.format(sg))
#end if
if not isinstance(name,str):
self.error('Invalid space group name from spglib: {}'.format(name))
#end if
bv = None
if sg>=1 and sg<=2:
bv = 'triclinic_'+name[0]
elif sg>=3 and sg<=15:
bv = 'monoclinic_'+name[0]
elif sg>=16 and sg<=74:
bv = 'orthorhombic_'+name[0]
elif sg>=75 and sg<=142:
bv = 'tetragonal_'+name[0]
elif sg>=143 and sg<=167:
bv = 'trigonal_'+name[0]
elif sg>=168 and sg<=194:
bv = 'hexagonal_'+name[0]
elif sg>=195 and sg<=230:
bv = 'cubic_'+name[0]
#end if
if bv is None:
self.error('Bravais lattice could not be determined.\nSpace group number and name: {} {}'.format(sg,name))
#end if
return bv
#end def bravais_lattice_name
# test needed
def space_group_operations(self,tol=1e-5,unit=False):
ds = self.get_symmetry(symprec=tol)
if ds is None:
self.error('Symmetry search failed.\nspglib error message:\n{}'.format(spglib.get_error_message()))
#end if
ds = obj(ds)
rotations = ds.rotations
translations = ds.translations
if not unit:
# Transform to Cartesian
axes = self.axes
axinv = np.linalg.inv(axes)
for n,(R,t) in enumerate(zip(rotations,translations)):
rotations[n] = np.dot(axinv,np.dot(R,axes))
translations[n] = np.dot(t,axes)
#end for
#end if
return rotations,translations
#end def space_group_operations
def point_group_operations(self,tol=1e-5,unit=False):
rotations,translations = self.space_group_operations(tol=tol,unit=unit)
no_trans = translations.max(axis=1) < tol
return rotations[no_trans]
#end def point_group_operations
def check_point_group_operations(self,rotations=None,tol=1e-5,unit=False,dtol=1e-5,ncheck=1,exit=False):
if rotations is None:
rotations = self.point_group_operations(tol=tol,unit=unit)
#ned if
r = self.pos
if ncheck=='all':
ncheck = len(r)
#end if
all_same = True
for n in range(ncheck):
rc = r[n]
for R in rotations:
rp = np.dot(r-rc,R)+rc
dt = self.min_image_distances(r,rp)
same = True
for d in dt:
same &= dt.min()<dtol
#end for
all_same &= same
#end for
#end for
if not all_same and exit:
self.error('Point group operators are not all symmetries of the structure.')
#end if
return all_same
#end def check_point_group_operations
def equivalent_atoms(self):
ds = self.symmetry_data()
# collect sets of species labels
species_by_specnum = obj()
for e,sn in zip(self.elem,ds.equivalent_atoms):
is_elem,es = is_element(e,symbol=True)
if sn not in species_by_specnum:
species_by_specnum[sn] = set()
#end if
species_by_specnum[sn].add(es)
#end for
for sn,sset in species_by_specnum.items():
if len(sset)>1:
self.error('Cannot find equivalent atoms.\nMultiple atomic species were marked as being equivalent.\nSpecies marked in this way: {}'.format(list(sset)))
#end if
species_by_specnum[sn] = list(sset)[0]
#end for
# give each unique species a unique label
labels_by_specnum = obj()
species_list = list(species_by_specnum.values())
species_set = set(species_list)
species_counts = obj()
for s in species_set:
species_counts[s] = species_list.count(s)
#end for
spec_counts = obj()
for sn,s in species_by_specnum.items():
if species_counts[s]==1:
labels_by_specnum[sn] = s
else:
if s not in spec_counts:
spec_counts[s] = 1
else:
spec_counts[s] += 1
#end if
labels_by_specnum[sn] = s + str(spec_counts[s])
#end if
#end for
# find indices for each unique species
equiv_indices = obj()
for s in labels_by_specnum.values():
equiv_indices[s] = list()
#end for
for i,sn in enumerate(ds.equivalent_atoms):
equiv_indices[labels_by_specnum[sn]].append(i)
#end for
for s,indices in equiv_indices.items():
equiv_indices[s] = np.array(indices,dtype=int)
#end for
return equiv_indices
#end def equivalent_atoms
# operations to support restricted cases for RMG code
# supported rmg lattices
rmg_lattices = obj(
orthorhombic_P = 'Orthorhombic Primitive',
tetragonal_P = 'Tetragonal Primitive',
hexagonal_P = 'Hexagonal Primitive',
cubic_P = 'Cubic Primitive',
cubic_I = 'Cubic Body Centered',
cubic_F = 'Cubic Face Centered',
)
def rmg_lattice(self,allow_tile=False,all_results=False,ret_bravais=False,exit=False,warn=False):
# output variables
rmg_lattice = None
tmatrix = None
rmg_lattices = Structure.rmg_lattices
# represent current bravais lattice
s = Structure(
units = str(self.units),
axes = self.axes.copy(),
elem = ['H'],
pos = [[0,0,0]],
)
# get standard primitive cell based on bravais lattice
sp = s.primitive()
# get current bravais lattice name
d = sp.symmetry_data()
bv = sp.bravais_lattice_name(d)
bvp = bv.rsplit('_',1)[0]+'_P'
if bv in rmg_lattices:
rmg_lattice = bv
#end if
# attempt to get a valid cell by tiling if current one is not valid
if rmg_lattice is None and bvp in rmg_lattices and allow_tile:
spt = Structure(
units = self.units,
axes = d.std_lattice,
)
tmatrix,valid_by_tiling = spt.tilematrix(sp,status=True)
if not valid_by_tiling:
tmatrix = None
else:
# apply tiling matrix
st = sp.copy().tile(tmatrix)
# update lattice type
rmg_lattice,bv = st.rmg_lattice(ret_bravais=True)
#end if
#end if
if rmg_lattice is None and (exit or warn):
msg = 'Bravais lattice is not supported by the RMG code.\nCell bravais lattice: {}\nLattices supported by RMG: {}'.format(bv,list(sorted(rmg_lattices.keys())))
if exit:
self.error(msg)
elif warn:
self.warn(msg)
#end if
#end if
if all_results:
return rmg_lattice,tmatrix,s,sp,bv
elif allow_tile:
return rmg_lattice,tmatrix
elif ret_bravais:
return rmg_lattice,bv
else:
return rmg_lattice
#end if
#end def rmg_lattice
def rmg_transform(self,allow_tile=False,allow_general=False,all_results=False):
rmg_lattice,tmatrix,s,sp,bv = self.rmg_lattice(
allow_tile = allow_tile,
exit = not allow_general,
all_results = True,
)
if rmg_lattice is None and allow_general:
s_trans = self.copy()
rmg_inputs = obj()
R = None
tmatrix = None
else:
s_trans = self.copy()
R = np.dot(np.linalg.inv(s.axes),sp.axes)
s_trans.matrix_transform(R.T)
if tmatrix is not None:
s_trans = s_trans.tile(tmatrix)
#end if
if s_trans.units=='A':
rmg_units = 'Angstrom'
elif s_trans.units=='B':
rmg_units = 'Bohr'
else:
self.error('Unrecognized length units in structure "{}"'.format(s_trans.units))
#end if
bl_type = self.rmg_lattices[rmg_lattice]
axes = s_trans.axes
if bl_type=='Cubic Primitive':
a = axes[0,0]
b = a
c = a
elif bl_type=='Tetragonal Primitive':
a = axes[0,0]
b = a
c = axes[2,2]
elif bl_type=='Orthorhombic Primitive':
a = axes[0,0]
b = axes[1,1]
c = axes[2,2]
elif bl_type=='Cubic Body Centered':
a = np.linalg.norm(axes[0])*2/np.sqrt(3.)
b = a
c = a
elif bl_type=='Cubic Face Centered':
a = np.linalg.norm(axes[0])*2/np.sqrt(2.)
b = a
c = a
elif bl_type=='Hexagonal Primitive':
a = np.linalg.norm(axes[0])
b = a
c = np.linalg.norm(axes[2])
else:
self.error('Unrecognized RMG bravais_lattice_type "{}"'.format(bl_type))
#end if
rmg_inputs = obj(
bravais_lattice_type = bl_type,
a_length = a,
b_length = b,
c_length = c,
length_units = rmg_units,
)
#end if
if not all_results:
return s_trans,rmg_inputs
else:
return s_trans,rmg_inputs,R,tmatrix,bv
#end if
#end def rmg_transform
#end class Structure
Structure.set_operations()
#======================#
# SeeK-path functions #
#======================#
# installation instructions for seekpath interface
#
# installation of seekpath
# pip install seekpath
import itertools
from periodic_table import pt as ptable
try:
from numpy import array_equal
except:
array_equal = unavailable('numpy','array_equal')
#end try
try:
import seekpath
from seekpath import get_explicit_k_path
version = seekpath.__version__
try:
version = [int(i) for i in version.split('.')]
if len(version) < 3:
raise ValueError
#end if
except ValueError:
raise ValueError("Unable to parse version number")
#end try
if tuple(version) < (1, 8, 3):
raise ValueError("Invalid seekpath version, need >= 1.8.4")
#end if
del version
del seekpath
except:
get_explicit_k_path = unavailable('seekpath','get_explicit_k_path')
#end try
def _getseekpath(
structure = None,
with_time_reversal = False,
recipe = 'hpkot',
reference_distance = 0.025,
threshold = 1E-7,
symprec = 1E-5,
angle_tolerance = 1.0,
):
if not isinstance(structure, Structure):
raise TypeError('structure is not of type Structure\ntype received: {0}'.format(structure.__class__.__name__))
#end if
if structure.has_folded():
structure = structure.folded_structure
#end if
structure = structure.copy()
if structure.units != 'A':
structure.change_units('A')
#end if
axes = structure.axes
unit_pos = structure.get_scaled_positions()
atomic_num = structure.get_atomic_numbers()
cell = (axes,unit_pos,atomic_num)
return get_explicit_k_path(cell)
#end def _getseekpath
def get_conventional_cell(
structure = None,
symprec = 1E-5,
angle_tolerance = 1.0,
seekpathout = None,
):
if seekpathout is None:
seekpathout = _getseekpath(structure=structure, symprec = symprec, angle_tolerance=angle_tolerance)
#end if
axes = seekpathout['conv_lattice']
enumbers = seekpathout['conv_types']
posd = seekpathout['conv_positions']
volfac = seekpathout['volume_original_wrt_conv']
bcharge = structure.background_charge*volfac
pos = dot(posd,axes)
sout = structure.copy()
elem = empty(len(enumbers), dtype='str')
for el in ptable.elements.items():
elem[enumbers==el[1].atomic_number]=el[0]
#end for
if abs(bcharge-int(bcharge)) > 1E-6:
raise ValueError("Invalid background charge for conventional structure")
#end if
return {'structure': Structure(axes=axes, elem=elem, pos=pos, background_charge = bcharge, units='A')}
#end def get_conventional_cell
def get_primitive_cell(
structure = None,
symprec = 1E-5,
angle_tolerance = 1.0,
seekpathout = None,
):
if seekpathout is None:
seekpathout = _getseekpath(structure = structure, symprec = symprec, angle_tolerance=angle_tolerance)
#end if
axes = seekpathout['primitive_lattice']
enumbers = seekpathout['primitive_types']
posd = seekpathout['primitive_positions']
volfac = seekpathout['volume_original_wrt_prim']
bcharge = structure.background_charge*volfac
pos = dot(posd,axes)
sout = structure.copy()
elem = array(enumbers, dtype='str')
for el in ptable.elements.items():
elem[enumbers==el[1].atomic_number]=el[0]
#end for
return {'structure' : Structure(axes=axes, elem=elem, pos=pos, background_charge=bcharge, units='A'),
'T' : seekpathout['primitive_transformation_matrix']}
#end def get_primitive_cell
def get_kpath(
structure = None,
check_standard = True,
with_time_reversal = False,
recipe = 'hpkot',
reference_distance = 0.025,
threshold = 1E-7,
symprec = 1E-5,
angle_tolerance = 1.0,
seekpathout = None,
):
if seekpathout is None:
seekpathout = _getseekpath(structure=structure, symprec = symprec, angle_tolerance=angle_tolerance,
recipe=recipe, reference_distance=reference_distance, with_time_reversal=with_time_reversal)
#end if
if check_standard:
structure = structure.copy()
structure.change_units('A')
axes = structure.axes
primlat = seekpathout['primitive_lattice']
if not isclose(primlat, axes).all():
#print primlat, axes
Structure.class_error('Input lattice is not the conventional lattice. If you like otherwise, set check_standard=False.')
#end if
#end if
inverse_A_to_inverse_B = convert(1.0,'A','B')
return {'explicit_kpoints_abs_inv_A' : seekpathout['explicit_kpoints_abs'],
'explicit_kpoints_abs_inv_B' : seekpathout['explicit_kpoints_abs']*inverse_A_to_inverse_B,
'explicit_kpoints_rel' : seekpathout['explicit_kpoints_rel'],
'explicit_kpoints_labels' : seekpathout['explicit_kpoints_labels'],
'path' : seekpathout['path'],
'explicit_path_linearcoords': seekpathout['explicit_kpoints_linearcoord'],
'point_coords' : seekpathout['point_coords']}
#end def get_kpath
def get_symmetry(
structure = None,
symprec = 1E-5,
angle_tolerance = 1.0,
seekpathout = None,
):
if seekpathout is None:
seekpathout = _getseekpath(structure = structure, symprec = symprec, angle_tolerance=angle_tolerance)
#end if
sgint = seekpathout['spacegroup_international']
bravais = seekpathout['bravais_lattice']
invsym = seekpathout['has_inversion_symmetry']
sgnum = seekpathout['spacegroup_number']
return {'sgint': sgint, 'bravais': bravais, 'inv_sym_exists': invsym, 'sgnum': sgnum}
#end def get_symmetry
def get_structure_with_bands(
cell = 0,
structure = None,
with_time_reversal = False,
reference_distance = 0.025,
threshold = 1E-7,
symprec = 1E-5,
angle_tolerance = 1.0,
):
if cell == 0:
''' Use input structure '''
struct_band = structure.copy()
elif cell == 1:
''' Use conventional structure '''
struct_band = get_conventional_cell(structure=structure, symprec=symprec, angle_tolerance=angle_tolerance)['structure']
elif cell == 2:
''' Use primitive structure '''
struct_band = get_primitive_cell(structure=structure, symprec=symprec, angle_tolerance=angle_tolerance)['structure']
else:
Structure.class_error('Invalid cell type')
#end if
kpath = get_kpath(structure=struct_band, check_standard=False, with_time_reversal=with_time_reversal)
return Structure(axes = struct_band.axes,
elem = struct_band.elem,
pos = struct_band.pos,
background_charge = struct_band.background_charge,
kpoints = kpath['explicit_kpoints_rel'],
units = 'A')
#end def get_structure_with_bands
# test needed
def get_band_tiling(
structure = None,
check_standard = True,
use_ktol = True,
kpoints_label = None,
kpoints_rel = None,
max_volfac = 20,
min_volfac = 0,
target_volfac = None,
):
def cube_deviation(axes):
a = axes
volume = abs(dot(cross(axes[0,:], axes[1,:]), axes[2,:]))
dc = volume**(1./3)*sqrt(2.)
d1 = abs(norm(a[0]+a[1])-dc)
d2 = abs(norm(a[1]+a[2])-dc)
d3 = abs(norm(a[2]+a[0])-dc)
d4 = abs(norm(a[0]-a[1])-dc)
d5 = abs(norm(a[1]-a[2])-dc)
d6 = abs(norm(a[2]-a[0])-dc)
return (d1+d2+d3+d4+d5+d6)/(6*dc)
#end def cube_deviation
def cuboid_with_int_edges(vol):
# Given a volume, return the cuboids which have integer edges
divisors = []
edges = []
if isinstance(vol, int):
i = 1
while i<=vol:
if vol%i==0:
divisors.append(i)
#end if
i+=1
#end while
for i in divisors:
for j in divisors:
for k in divisors:
if i*j*k == vol:
edges.append([i,j,k])
#end if
#end for
#end for
#end for
else:
self.error('Volume multiplier must be integer')
#end if
return edges
#end def cuboid_with_int_edges
def alphas_on_grid(alphas, divs):
new_alphas = []
for alpha in alphas:
abs_alpha = abs(alpha)
sign_alpha = sign(alpha)
new_alpha = round(abs_alpha*divs)*1./divs*sign_alpha
new_alphas.append(new_alpha)
#end for
return new_alphas
#end def alphas_on_grid
def find_alphas(structure, kpoints_label, kpoints_rel, check_standard):
# Read wavevectors from the input and return the differences between all wavevectors (alphas)
kpath = get_kpath(structure = structure, check_standard = check_standard)
kpath_label = array(kpath['explicit_kpoints_labels'])
kpath_rel = kpath['explicit_kpoints_rel']
kpts = dict()
if kpoints_label is None:
kpoints_label = []
if kpoints_rel is None:
Structure.class_error('Please define symbolic or crystal coordinates for kpoints. e.g. [\'GAMMA\', \'K\'] or [[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]]')
else:
for k in kpoints_rel:
kindex = isclose(kpath_rel,k, atol=1e-5).all(1)
if any(kindex):
kpts[kpath_label[kindex][0]] = array(k)
else:
Structure.class_error('{0} is not found in the kpath'.format(k))
#end if
#end for
#end if
else:
if kpoints_rel is not None:
Structure.class_error('Both symbolic and crystal k-points are defined.')
else:
kpoints_rel = []
num_kpoints = 0
for k in kpoints_label:
kindex = k == kpath_label
if any(kindex):
if k == '' or k == None:
k = '{0}'.format(num_kpoints)
#end if
kpts[k] = array(kpath_rel[kindex][0])
else:
Structure.class_error('{0} is not found in the kpath'.format(k))
#end if
#end for
#end if
#end if
alphas = array([x[0] - x[1] for x in itertools.combinations(kpts.values(),2)]) #Combinations of k_1 - k_2 in kpts list
kpt0 = list(kpts.values())[0]
return alphas, kpt0
#end def find_alphas
def find_vars(alphas,min_volfac, max_volfac, target_volfac, use_ktol):
'''
Find variables to generate possible smallest matrices in the Upper Triangular Hermite Normal Form, from PHYSICAL REVIEW B 92, 184301 (2015)
For target or min/max volumes, it returns vol_mul, which is the smallest volume multiplier to be used on the volfac here
'''
if use_ktol:
ktol = 0.25/max_volfac
else:
ktol = 0.0
#end if
if target_volfac is not None:
if min_volfac is None and max_volfac is None:
min_volfac = target_volfac
max_volfac = target_volfac
else:
print("target_volfac and {min_volfac, max_volfac} cannot be defined together!")
exit()
#end if
#end if
cur_volfac = 1e6
vars = []
for mvol in range(max_volfac, 0, -1):
cuboids = cuboid_with_int_edges(mvol)
for c in cuboids:
a_1, a_2, a_3 = c
new_alphas = alphas_on_grid(alphas, c)
rec_grid_voxel = array([1./a_1,1./a_2,1./a_3]) # reciprocal grid voxel size
rem = []
for alpha in alphas:
rem.append(mod(abs(alpha), rec_grid_voxel))
#end for
if all(isclose(rem,0.0,atol=ktol)):
n1 = a_1
n2 = a_2
n3 = a_3
g12 = np.gcd.reduce([n1,n2])
g13 = np.gcd.reduce([n1,n3])
g23 = np.gcd.reduce([n2,n3])
g123 = np.gcd.reduce([n1, n2, n3])
volfac = n1*n2*n3*g123//(g12*g13*g23)
if volfac < cur_volfac: #min_volfac <= volfac and and volfac <= max_volfac:
vars = [[n1, n2, n3, g12, g13, g23, g123]]
cur_volfac = volfac
elif volfac == cur_volfac:
vars.append([n1, n2, n3, g12, g13, g23, g123])
#end if
#end if
#end for
#end for
if vars == []:
print('Change ktol')
exit()
else:
can_be_found = False
vol_mul = 1
while not can_be_found:
if volfac*vol_mul <= max_volfac and volfac*vol_mul >= min_volfac:
can_be_found = True
elif volfac*vol_mul > max_volfac:
print('Increase max_volfac or target_volfac!')
exit()
else:
vol_mul+=1
#end if
#end while
return vars, vol_mul
#end if
#end def find_vars
def find_mats(mat_vars, alphas):
'''
Given the variables (v), return the list of all upper triangular matrices as in PHYSICAL REVIEW B 92, 184301 (2015)
'''
mats = []
for v in mat_vars:
n1, n2, n3, g12, g13, g23, g123 = v
#New alphas exactly on the voxels thanks to ktol
for p in range(0, g23):
for q in range(0, g12//g123):
for r in range(g13*g23//g123):
temp_mat = [
[g123*n1//(g12*g13), q*g123*n2//(g12*g23), r*g123*n3//(g13*g23)],
[0, n2//g23, p*n3//g23],
[0,0,n3]]
comm = []
div = array([n1, n2, n3])
new_alphas = alphas_on_grid(alphas, div)
for new_alpha in new_alphas:
if (isclose(abs(dot(temp_mat,new_alpha))%1.0, 0, atol = 1e-6)).all():
comm.append(True) # new_alpha is commensurate with tmat
else:
comm.append(False)
#end if
#end for
if all(comm) and temp_mat not in mats: # if all new_alphas are commensurate with tmat
mats.append(temp_mat)
#end if
#end for
#end for
#end for
#end for
return mats
#end def find_mats
def find_cubic_mat(mats, structure, mat_vol_mul):
final_axes = []
final_t = []
final_cubicity = 1e6
mats = array(mats)
for m in mats:
axes = structure.axes.copy()
s = structure.copy()
[m_t, r] = optimal_tilematrix(s.tile(m), volfac=mat_vol_mul)
m_axes = dot(dot(m_t, m), axes)
m_cubicity = cube_deviation(m_axes)
if m_cubicity < final_cubicity:
final_axes = m_axes
final_cubicity = m_cubicity
final_t = dot(m_t, m)
#end if
#end for
return final_t.tolist()
#end def find_cubic_mat
def find_shift(final_mat, structure, kpt0):
return None
#end def find_cubic_mat
alphas, kpt0 = find_alphas(structure,kpoints_label,kpoints_rel, check_standard) # Wavevector differences
mat_vars, mat_vol_mul = find_vars(alphas,min_volfac,max_volfac,target_volfac,use_ktol) # Variables to construct upper triangular matrices
mats = find_mats(mat_vars,alphas) # List of upper triangular matrices that are commensurate with alphas
final_mat = find_cubic_mat(mats, structure, mat_vol_mul) # Matrix leading to a lattice with highest cubicity, optimized using elementary operations
shift = find_shift(final_mat, structure, kpts0) # Find the grid shift
o = obj()
o.mat = final_mat
o.shift = shift
o.det = det(final_mat)
return o
#end def get_band_tiling
def get_seekpath_full(
structure = None,
seekpathout = None,
conventional = False,
primitive = False,
**kwargs
):
if seekpathout is None:
seekpathout = _getseekpath(structure,**kwargs)
#end if
res = obj(seekpathout)
for k,v in res.items():
if isinstance(v,dict):
res[k] = obj(v)
#end if
#end for
if conventional:
conv = get_conventional_cell(structure,seekpathout=seekpathout)
res.conventional = conv['structure']
#end if
if primitive:
prim = get_primitive_cell(structure,seekpathout=seekpathout)
res.primitive = prim['structure']
res.prim_tmatrix = prim['T']
#end if
return res
#end def get_seekpath_full
skp = obj(
_getseekpath = _getseekpath,
get_conventional_cell = get_conventional_cell,
get_primitive_cell = get_primitive_cell,
get_kpath = get_kpath,
get_symmetry = get_symmetry,
get_structure_with_bands = get_structure_with_bands,
get_band_tiling = get_band_tiling,
)
#==========================#
# end SeeK-path functions #
#==========================#
def interpolate_structures(struct1,struct2=None,images=None,min_image=True,recenter=True,match_com=False,repackage=False,chained=False):
if images is None:
Structure.class_error('images must be provided','interpolate_structures')
#end if
# if a list of structures is provided,
# interpolate between pairs in the chain of structures
if isinstance(struct1,(list,tuple)):
structures_in = struct1
structures = []
for n in range(len(structures_in)-1):
struct1 = structures_in[n]
struct2 = structures_in[n+1]
structs = interpolate_structures(struct1,struct2,images,min_image,recenter,match_com,repackage,chained=True)
if n==0:
structures.append(structs[0])
#end if
structures.extend(structs[1:-1])
if n==len(structures_in)-2:
structures.append(structs[-1])
#end if
#end for
return structures
#end if
# handle PhysicalSystem objects indirectly
system1 = None
system2 = None
if not isinstance(struct1,Structure):
system1 = struct1.copy()
system1.remove_folded()
struct1 = system1.structure
#end if
if not isinstance(struct2,Structure):
system2 = struct2.copy()
system2.remove_folded()
struct2 = system2.structure
#end if
# perform the interpolation
structures = struct1.interpolate(struct2,images,min_image,recenter,match_com)
# repackage into physical system objects if requested
if repackage:
if system1!=None:
system = system1
elif system2!=None:
system = system2
else:
Structure.class_error('cannot repackage into physical systems since no system object was provided in place of a structure','interpolate_structures')
#end if
systems = []
for s in structures:
ps = system.copy()
ps.structure = s
systems.append(ps)
#end for
result = systems
else:
result = structures
#end if
return result
#end def interpolate_structures
# test needed
def structure_animation(filepath,structures,tiling=None):
path,file = os.path.split(filepath)
if not file.endswith('xyz'):
Structure.class_error('only xyz files are supported for now','structure_animation')
#end if
anim = ''
for s in structures:
if tiling is None:
anim += s.write_xyz()
else:
anim += s.tile(tiling).write_xyz()
#end if
#end for
open(filepath,'w').write(anim)
#end def structure_animation
class DefectStructure(Structure):
def __init__(self,*args,**kwargs):
if len(args)>0 and isinstance(args[0],Structure):
self.transfer_from(args[0],copy=True)
else:
Structure.__init__(self,*args,**kwargs)
#end if
#end def __init__
def defect_from_bond_compression(self,compression_cutoff,bond_eq,neighbors):
bind,bcent,blens = self.bonds(neighbors)
ind = bind[ abs(blens/bond_eq - 1.) > compression_cutoff ]
idefect = array(list(set(ind.ravel())))
defect = self.carve(idefect)
return defect
#end def defect_from_bond_compression
def defect_from_displacement(self,displacement_cutoff,reference):
displacement = self.scalar_displacement(reference)
idefect = displacement > displacement_cutoff
defect = self.carve(idefect)
return defect
#end def defect_from_displacement
def compare(self,dist_cutoff,d1,d2=None):
if d2==None:
d2 = d1
d1 = self
#end if
res = Sobj()
natoms1 = len(d1.pos)
natoms2 = len(d2.pos)
if natoms1<natoms2:
dsmall,dlarge = d1,d2
else:
dsmall,dlarge = d2,d1
#end if
nn = nearest_neighbors(1,dlarge,dsmall)
dist = dsmall.distances(dlarge[nn.ravel()])
dmatch = dist<dist_cutoff
ismall = array(list(range(len(dsmall.pos))))
ismall = ismall[dmatch]
ilarge = nn[ismall]
if natoms1<natoms2:
i1,i2 = ismall,ilarge
else:
i2,i1 = ismall,ilarge
#end if
natoms_match = dmatch.sum()
res.all_match = natoms1==natoms2 and natoms1==natoms_match
res.natoms_match = natoms_match
res.imatch1 = i1
res.imatch2 = i2
return res
#end def compare
#end class DefectStructure
class Crystal(Structure):
lattice_constants = obj(
triclinic = ['a','b','c','alpha','beta','gamma'],
monoclinic = ['a','b','c','beta'],
orthorhombic = ['a','b','c'],
tetragonal = ['a','c'],
hexagonal = ['a','c'],
cubic = ['a'],
rhombohedral = ['a','alpha']
)
lattices = list(lattice_constants.keys())
centering_types = obj(
primitive = 'P',
base_centered = ('A','B','C'),
face_centered = 'F',
body_centered = 'I',
rhombohedral_centered = 'R'
)
lattice_centerings = obj(
triclinic = ['P'],
monoclinic = ['P','A','B','C'],
orthorhombic = ['P','C','I','F'],
tetragonal = ['P','I'],
hexagonal = ['P','R'],
cubic = ['P','I','F'],
rhombohedral = ['P']
)
centerings = obj(
P = [],
A = [[0,.5,.5]],
B = [[.5,0,.5]],
C = [[.5,.5,0]],
F = [[0,.5,.5],[.5,0,.5],[.5,.5,0]],
I = [[.5,.5,.5]],
R = [[2./3, 1./3, 1./3],[1./3, 2./3, 2./3]]
)
cell_types = set(['primitive','conventional'])
cell_aliases = obj(
prim = 'primitive',
conv = 'conventional'
)
cell_classes = obj(
sc = 'cubic',
bcc = 'cubic',
fcc = 'cubic',
hex = 'hexagonal'
)
for lattice in lattices:
cell_classes[lattice]=lattice
#end for
#helpful websites for structures
# wikipedia.org
# webelements.com
# webmineral.com
# springermaterials.com
known_crystals = {
('diamond','fcc'):obj(
lattice = 'cubic',
cell = 'primitive',
centering = 'F',
constants = 3.57,
units = 'A',
atoms = 'C',
basis = [[0,0,0],[.25,.25,.25]]
),
('diamond','sc'):obj(
lattice = 'cubic',
cell = 'conventional',
centering = 'F',
constants = 3.57,
units = 'A',
atoms = 'C',
basis = [[0,0,0],[.25,.25,.25]]
),
('diamond','prim'):obj(
lattice = 'cubic',
cell = 'primitive',
centering = 'F',
constants = 3.57,
units = 'A',
atoms = 'C',
basis = [[0,0,0],[.25,.25,.25]]
),
('diamond','conv'):obj(
lattice = 'cubic',
cell = 'conventional',
centering = 'F',
constants = 3.57,
units = 'A',
atoms = 'C',
basis = [[0,0,0],[.25,.25,.25]]
),
('wurtzite','prim'):obj(
lattice = 'hexagonal',
cell = 'primitive',
centering = 'P',
constants = (3.35,5.22),
units = 'A',
#atoms = ('Zn','O'),
#basis = [[1./3, 2./3, 3./8],[1./3, 2./3, 0]]
atoms = ('Zn','O','Zn','O'),
basis = [[0,0,5./8],[0,0,0],[2./3,1./3,1./8],[2./3,1./3,1./2]]
),
('ZnO','prim'):obj(
lattice = 'wurtzite',
cell = 'prim',
constants = (3.35,5.22),
units = 'A',
atoms = ('Zn','O','Zn','O')
),
('NaCl','prim'):obj(
lattice = 'cubic',
cell = 'primitive',
centering = 'F',
constants = 5.64,
units = 'A',
atoms = ('Na','Cl'),
basis = [[0,0,0],[.5,0,0]],
basis_vectors = 'conventional'
),
('rocksalt','prim'):obj(
lattice = 'cubic',
cell = 'primitive',
centering = 'F',
constants = 5.64,
units = 'A',
atoms = ('Na','Cl'),
basis = [[0,0,0],[.5,0,0]],
basis_vectors = 'conventional'
),
('copper','prim'):obj(
lattice = 'cubic',
cell = 'primitive',
centering = 'F',
constants = 3.615,
units = 'A',
atoms = 'Cu'
),
('calcium','prim'):obj(
lattice = 'cubic',
cell = 'primitive',
centering = 'F',
constants = 5.588,
units = 'A',
atoms = 'Ca'
),
# http://www.webelements.com/oxygen/crystal_structure.html
# Phys Rev 160 694
('oxygen','prim'):obj(
lattice = 'monoclinic',
cell = 'primitive',
centering = 'C',
constants = (5.403,3.429,5.086,132.53),
units = 'A',
angular_units = 'degrees',
atoms = ('O','O'),
basis = [[0,0,1.15/2],[0,0,-1.15/2]],
basis_vectors = identity(3)
),
# http://en.wikipedia.org/wiki/Calcium_oxide
# http://www.springermaterials.com/docs/info/10681719_224.html
('CaO','prim'):obj(
lattice = 'NaCl',
cell = 'prim',
constants = 4.81,
atoms = ('Ca','O')
),
('CaO','conv'):obj(
lattice = 'NaCl',
cell = 'conv',
constants = 4.81,
atoms = ('Ca','O')
),
# http://en.wikipedia.org/wiki/Copper%28II%29_oxide
# http://iopscience.iop.org/0953-8984/3/28/001/
# http://www.webelements.com/compounds/copper/copper_oxide.html
# http://www.webmineral.com/data/Tenorite.shtml
('CuO','prim'):obj(
lattice = 'monoclinic',
cell = 'primitive',
centering = 'C',
constants = (4.683,3.422,5.128,99.54),
units = 'A',
angular_units = 'degrees',
atoms = ('Cu','O','Cu','O'),
basis = [[.25,.25,0],[0,.418,.25],
[.25,.75,.5],[.5,.5-.418,.75]],
basis_vectors = 'conventional'
),
('Ca2CuO3','prim'):obj(# kateryna foyevtsova
lattice = 'orthorhombic',
cell = 'primitive',
centering = 'I',
constants = (3.77,3.25,12.23),
units = 'A',
atoms = ('Cu','O','O','O','Ca','Ca'),
basis = [[ 0, 0, 0 ],
[ .50, 0, 0 ],
[ 0, 0, .16026165],
[ 0, 0, .83973835],
[ 0, 0, .35077678],
[ 0, 0, .64922322]],
basis_vectors = 'conventional'
),
('La2CuO4','prim'):obj( #tetragonal structure
lattice = 'tetragonal',
cell = 'primitive',
centering = 'I',
constants = (3.809,13.169),
units = 'A',
atoms = ('Cu','O','O','O','O','La','La'),
basis = [[ 0, 0, 0],
[ .5, 0, 0],
[ 0, .5, 0],
[ 0, 0, .182],
[ 0, 0, -.182],
[ 0, 0, .362],
[ 0, 0, -.362]]
),
('Cl2Ca2CuO2','prim'):obj(
lattice = 'tetragonal',
cell = 'primitive',
centering = 'I',
constants = (3.869,15.05),
units = 'A',
atoms = ('Cu','O','O','Ca','Ca','Cl','Cl'),
basis = [[ 0, 0, 0 ],
[ .5, 0, 0 ],
[ 0, .5, 0 ],
[ .5, .5, .104],
[ 0, 0, .396],
[ 0, 0, .183],
[ .5, .5, .317]],
basis_vectors = 'conventional'
),
('Cl2Ca2CuO2','afm'):obj(
lattice = 'tetragonal',
cell = 'conventional',
centering = 'P',
axes = [[.5,-.5,0],[.5,.5,0],[0,0,1]],
constants = (2*3.869,15.05),
units = 'A',
atoms = 4*['Cu','O','O','Ca','Ca','Cl','Cl'],
basis = [[ 0, 0, 0 ], #Cu
[ .25, 0, 0 ],
[ 0, .25, 0 ],
[ .25, .25, .104],
[ 0, 0, .396],
[ 0, 0, .183],
[ .25, .25, .317],
[ .25, .25, .5 ], #Cu
[ .5, .25, .5 ],
[ .25, .5, .5 ],
[ .5, .5, .604],
[ .25, .25, .896],
[ .25, .25, .683],
[ .5, .5, .817],
[ .5, 0, 0 ], #Cu2
[ .75, 0, 0 ],
[ .5, .25, 0 ],
[ .75, .25, .104],
[ .5, 0, .396],
[ .5, 0, .183],
[ .75, .25, .317],
[ .75, .25, .5 ], #Cu2
[ 0, .25, .5 ],
[ .75, .5, .5 ],
[ 0, .5, .604],
[ .75, .25, .896],
[ .75, .25, .683],
[ 0, .5, .817]],
basis_vectors = 'conventional'
),
('CuO2_plane','prim'):obj(
lattice = 'tetragonal',
cell = 'primitive',
centering = 'P',
constants = (3.809,13.169),
units = 'A',
atoms = ('Cu','O','O'),
basis = [[ 0, 0, 0],
[ .5, 0, 0],
[ 0, .5, 0]]
),
('graphite_aa','hex'):obj(
axes = [[1./2,-sqrt(3.)/2,0],[1./2,sqrt(3.)/2,0],[0,0,1]],
constants = (2.462,3.525),
units = 'A',
atoms = ('C','C'),
basis = [[0,0,0],[2./3,1./3,0]]
),
('graphite_ab','hex'):obj(
axes = [[1./2,-sqrt(3.)/2,0],[1./2,sqrt(3.)/2,0],[0,0,1]],
constants = (2.462,3.525),
units = 'A',
cscale = (1,2),
atoms = ('C','C','C','C'),
basis = [[0,0,0],[2./3,1./3,0],[0,0,1./2],[1./3,2./3,1./2]]
),
('graphene','prim'):obj(
lattice = 'hexagonal',
cell = 'primitive',
centering = 'P',
constants = (2.462,15.0),
units = 'A',
atoms = ('C','C'),
basis = [[0,0,0],[2./3,1./3,0]]
),
('graphene','rect'):obj(
lattice = 'orthorhombic',
cell = 'conventional',
centering = 'C',
constants = (2.462,sqrt(3.)*2.462,15.0),
units = 'A',
atoms = ('C','C'),
basis = [[0,0,0],[1./2,1./6,0]]
)
}
kc_keys = list(known_crystals.keys())
for (name,cell) in kc_keys:
desc = known_crystals[name,cell]
if cell=='prim' and not (name,'conv') in known_crystals:
cdesc = desc.copy()
if cdesc.cell=='primitive':
cdesc.cell = 'conventional'
known_crystals[name,'conv'] = cdesc
elif cdesc.cell=='prim':
cdesc.cell = 'conv'
known_crystals[name,'conv'] = cdesc
#end if
#end if
#end if
del kc_keys
def __init__(self,
lattice = None,
cell = None,
centering = None,
constants = None,
atoms = None,
basis = None,
basis_vectors = None,
tiling = None,
cscale = None,
axes = None,
units = None,
angular_units = 'degrees',
kpoints = None,
kgrid = None,
mag = None,
frozen = None,
magnetization = None,
kshift = (0,0,0),
permute = None,
operations = None,
elem = None,
pos = None,
use_prim = None,
add_kpath = False,
symm_kgrid = False,
):
if lattice is None and cell is None and atoms is None and units is None:
return
#end if
gi = obj(
lattice = lattice ,
cell = cell ,
centering = centering ,
constants = constants ,
atoms = atoms ,
basis = basis ,
basis_vectors = basis_vectors ,
tiling = tiling ,
cscale = cscale ,
axes = axes ,
units = units ,
angular_units = angular_units ,
frozen = frozen ,
mag = mag ,
magnetization = magnetization ,
kpoints = kpoints ,
kgrid = kgrid ,
kshift = kshift ,
permute = permute ,
operations = operations ,
elem = elem ,
pos = pos ,
use_prim = use_prim ,
add_kpath = add_kpath ,
symm_kgrid = symm_kgrid ,
)
generation_info = gi.copy()
lattice_in = lattice
if isinstance(lattice,str):
lattice=lattice.lower()
#end if
if isinstance(cell,str):
cell=cell.lower()
#end if
known_crystal = False
if (lattice_in,cell) in self.known_crystals:
known_crystal = True
lattice_info = self.known_crystals[lattice_in,cell].copy()
elif (lattice,cell) in self.known_crystals:
known_crystal = True
lattice_info = self.known_crystals[lattice,cell].copy()
#end if
if known_crystal:
while 'lattice' in lattice_info and 'cell' in lattice_info and (lattice_info.lattice,lattice_info.cell) in self.known_crystals:
li_old = lattice_info
lattice_info = self.known_crystals[li_old.lattice,li_old.cell].copy()
del li_old.lattice
del li_old.cell
lattice_info.transfer_from(li_old,copy=False)
#end while
if 'cell' in lattice_info:
cell = lattice_info.cell
elif cell in self.cell_aliases:
cell = self.cell_aliases[cell]
elif cell in self.cell_classes:
lattice = self.cell_classes[cell]
else:
self.error('cell shape '+cell+' is not recognized\n the variable cell_classes or cell_aliases must be updated to include '+cell)
#end if
if 'lattice' in lattice_info:
lattice = lattice_info.lattice
#end if
if 'angular_units' in lattice_info:
angular_units = lattice_info.angular_units
#end if
inputs = obj(
centering = centering,
constants = constants,
atoms = atoms,
basis = basis,
basis_vectors = basis_vectors,
tiling = tiling,
cscale = cscale,
axes = axes,
units = units
)
for var,val in inputs.items():
if val is None and var in lattice_info:
inputs[var] = lattice_info[var]
#end if
#end for
centering,constants,atoms,basis,basis_vectors,tiling,cscale,axes,units=inputs.list('centering','constants','atoms','basis','basis_vectors','tiling','cscale','axes','units')
#end if
if constants is None:
self.error('the variable constants must be provided')
#end if
if atoms is None:
self.error('the variable atoms must be provided')
#end if
if lattice not in self.lattices:
self.error('lattice type '+str(lattice)+' is not recognized\n valid lattice types are: '+str(list(self.lattices)))
#end if
if cell=='conventional':
if centering is None:
self.error('centering must be provided for a conventional cell\n options for a '+lattice+' lattice are: '+str(self.lattice_centerings[lattice]))
elif centering not in self.centerings:
self.error('centering type '+str(centering)+' is not recognized\n options for a '+lattice+' lattice are: '+str(self.lattice_centerings[lattice]))
#end if
#end if
if isinstance(constants,int) or isinstance(constants,float):
constants=[constants]
#end if
if len(constants)!=len(self.lattice_constants[lattice]):
self.error('the '+lattice+' lattice depends on the constants '+str(self.lattice_constants[lattice])+'\n you provided '+str(len(constants))+': '+str(constants))
#end if
if isinstance(atoms,str):
if basis!=None:
atoms = len(basis)*[atoms]
else:
atoms=[atoms]
#end if
#end if
if basis is None:
if len(atoms)==1:
basis = [(0,0,0)]
else:
self.error('must provide as many basis coordinates as basis atoms\n atoms provided: '+str(atoms)+'\n basis provided: '+str(basis))
#end if
#end if
if basis_vectors is not None and not isinstance(basis_vectors,str) and len(basis_vectors)!=3:
self.error('3 basis vectors must be given, you provided '+str(len(basis))+':\n '+str(basis_vectors))
#end if
if tiling is None:
tiling = (1,1,1)
#end if
if cscale is None:
cscale = len(constants)*[1]
#end if
if len(cscale)!=len(constants):
self.error('cscale and constants must be the same length')
#end if
basis = array(basis)
tiling = array(tiling,dtype=int)
cscale = array(cscale)
constants = cscale*array(constants)
a,b,c,alpha,beta,gamma = None,None,None,None,None,None
if angular_units=='radians':
pi_1o2 = pi/2
pi_2o3 = 2*pi/3
elif angular_units=='degrees':
pi_1o2 = 90.
pi_2o3 = 120.
else:
self.error('angular units must be radians or degrees\n you provided '+str(angular_units))
#end if
if lattice=='triclinic':
a,b,c,alpha,beta,gamma = constants
elif lattice=='monoclinic':
a,b,c,beta = constants
alpha = gamma = pi_1o2
elif lattice=='orthorhombic':
a,b,c = constants
alpha=beta=gamma=pi_1o2
elif lattice=='tetragonal':
a,c = constants
b=a
alpha=beta=gamma=pi_1o2
elif lattice=='hexagonal':
a,c = constants
b=a
alpha=beta=pi_1o2
gamma=pi_2o3
elif lattice=='cubic':
a=constants[0]
b=c=a
alpha=beta=gamma=pi_1o2
elif lattice=='rhombohedral':
a,alpha = constants
b=c=a
beta=gamma=alpha
#end if
if angular_units=='degrees':
alpha *= pi/180
beta *= pi/180
gamma *= pi/180
#end if
points = [[0,0,0]]
#get the conventional axes
sa,ca = sin(alpha),cos(alpha)
sb,cb = sin(beta) ,cos(beta)
sg,cg = sin(gamma),cos(gamma)
y = (ca-cg*cb)/sg
a1c = a*array([1,0,0])
a2c = b*array([cg,sg,0])
a3c = c*array([cb,y,sqrt(sb**2-y**2)])
#a1c = array([a,0,0])
#a2c = array([b*cos(gamma),b*sin(gamma),0])
#a3c = array([c*cos(beta),c*cos(alpha)*sin(beta),c*sin(alpha)*sin(beta)])
axes_conv = array([a1c,a2c,a3c]).copy()
if axes is None:
if cell not in self.cell_types:
self.error('cell must be primitive or conventional\n You provided: '+str(cell))
#end if
if cell=='primitive' and centering=='P':
cell='conventional'
#end if
#get the primitive axes
if centering=='P':
a1 = a1c
a2 = a2c
a3 = a3c
elif centering=='A':
a1 = a1c
a2 = (a2c+a3c)/2
a3 = (-a2c+a3c)/2
elif centering=='B':
a1 = (a1c+a3c)/2
a2 = a2c
a3 = (-a1c+a3c)/2
elif centering=='C':
a1 = (a1c-a2c)/2
a2 = (a1c+a2c)/2
a3 = a3c
elif centering=='I':
a1=[ a/2, b/2,-c/2]
a2=[-a/2, b/2, c/2]
a3=[ a/2,-b/2, c/2]
elif centering=='F':
a1=[a/2, b/2, 0]
a2=[ 0, b/2, c/2]
a3=[a/2, 0, c/2]
elif centering=='R':
a1=[ a, 0, 0]
a2=[ a/2, a*sqrt(3.)/2, 0]
a3=[-a/6, a/(2*sqrt(3.)), c/3]
else:
self.error('the variable centering must be specified\n valid options are: P,A,B,C,I,F,R')
#end if
axes_prim = array([a1,a2,a3])
if cell=='primitive':
axes = axes_prim
elif cell=='conventional':
axes = axes_conv
points.extend(self.centerings[centering])
#end if
elif known_crystal:
axes = dot(diag([a,b,c]),array(axes))
#end if
points = array(points,dtype=float)
elem = []
pos = []
if basis_vectors is None:
basis_vectors = axes
elif isinstance(basis_vectors,str):
if basis_vectors=='primitive':
basis_vectors = axes_prim
elif basis_vectors=='conventional':
basis_vectors = axes_conv
#end if
#end if
nbasis = len(atoms)
for point in points:
for i in range(nbasis):
atom = atoms[i]
bpoint = basis[i]
p = dot(point,axes) + dot(bpoint,basis_vectors)
elem.append(atom)
pos.append(p)
#end for
#end for
pos = array(pos)
self.set(
constants = array([a,b,c]),
angles = array([alpha,beta,gamma]),
generation_info = generation_info
)
Structure.__init__(
self,
axes = axes,
scale = a,
elem = elem,
pos = pos,
center = axes.sum(0)/2,
units = units,
frozen = frozen,
mag = mag,
magnetization = magnetization,
tiling = tiling,
kpoints = kpoints,
kgrid = kgrid,
kshift = kshift,
permute = permute,
rescale = False,
operations = operations,
use_prim = use_prim,
add_kpath = add_kpath,
symm_kgrid = symm_kgrid,
)
#end def __init__
#end class Crystal
# test needed
class Jellium(Structure):
prefactors = obj()
prefactors.transfer_from({1:2*pi,2:4*pi,3:4./3*pi})
def __init__(self,charge=None,background_charge=None,cell=None,volume=None,density=None,rs=None,dim=3,
axes=None,kpoints=None,kweights=None,kgrid=None,kshift=None,units=None,tiling=None):
del tiling
if rs!=None:
if not dim in self.prefactors:
self.error('only 1,2, or 3 dimensional jellium is currently supported\n you requested one with dimension {0}'.format(dim))
#end if
density = 1.0/(self.prefactors[dim]*rs**dim)
#end if
if axes!=None:
cell = axes
#end if
if background_charge!=None:
charge = background_charge
#end if
if cell!=None:
cell = array(cell)
dim = len(cell)
volume = det(cell)
elif volume!=None:
volume = float(volume)
cell = volume**(1./dim)*identity(dim)
#end if
if density!=None:
density = float(density)
if charge is None and volume!=None:
charge = density*volume
elif volume is None and charge!=None:
volume = charge/density
cell = volume**(1./dim)*identity(dim)
#end if
#end if
if charge is None or cell is None:
self.error('not enough information to form jellium structure\n information provided:\n charge: {0}\n cell: {1}\n volume: {2}\n density: {3}\n rs: {4}\n dim: {5}'.format(charge,cell,volume,density,rs,dim))
#end if
Structure.__init__(self,background_charge=charge,axes=cell,dim=dim,kpoints=kpoints,kweights=kweights,kgrid=kgrid,kshift=kshift,units=units)
#end def __init__
def density(self):
return self.background_charge/self.volume()
#end def density
def rs(self):
return 1.0/(self.density()*self.prefactors[self.dim])**(1./self.dim)
#end def rs
def tile(self):
self.not_implemented()
#end def tile
#end class Jellium
# test needed
def generate_cell(shape,tiling=None,scale=1.,units=None,struct_type=Structure):
if tiling is None:
tiling = (1,1,1)
#end if
axes = Sobj()
axes.sc = 1.*array([[ 1,0,0],[0, 1,0],[0,0, 1]])
axes.bcc = .5*array([[-1,1,1],[1,-1,1],[1,1,-1]])
axes.fcc = .5*array([[ 1,1,0],[1, 0,1],[0,1, 1]])
ax = dot(diag(tiling),axes[shape])
center = ax.sum(0)/2
c = Structure(axes=ax,scale=scale,center=center,units=units)
if struct_type!=Structure:
c=c.upcast(struct_type)
#end if
return c
#end def generate_cell
def generate_structure(type='crystal',*args,**kwargs):
if type=='crystal':
s = generate_crystal_structure(*args,**kwargs)
elif type=='defect':
s = generate_defect_structure(*args,**kwargs)
elif type=='atom':
s = generate_atom_structure(*args,**kwargs)
elif type=='dimer':
s = generate_dimer_structure(*args,**kwargs)
elif type=='trimer':
s = generate_trimer_structure(*args,**kwargs)
elif type=='jellium':
s = generate_jellium_structure(*args,**kwargs)
elif type=='empty':
s = Structure()
elif type=='basic':
s = Structure(*args,**kwargs)
else:
Structure.class_error(str(type)+' is not a valid structure type\noptions are crystal, defect, atom, dimer, trimer, jellium, empty, or basic')
#end if
return s
#end def generate_structure
# test needed
def generate_atom_structure(
atom = None,
units = 'A',
Lbox = None,
skew = 0,
axes = None,
kgrid = (1,1,1),
kshift = (0,0,0),
bconds = tuple('nnn'),
struct_type = Structure
):
if atom is None:
Structure.class_error('atom must be provided','generate_atom_structure')
#end if
if Lbox!=None:
axes = [[Lbox*(1-skew),0,0],[0,Lbox,0],[0,0,Lbox*(1+skew)]]
#end if
if axes is None:
s = Structure(elem=[atom],pos=[[0,0,0]],units=units,bconds=bconds)
else:
s = Structure(elem=[atom],pos=[[0,0,0]],axes=axes,kgrid=kgrid,kshift=kshift,bconds=bconds,units=units)
s.center_molecule()
#end if
return s
#end def generate_atom_structure
# test needed
def generate_dimer_structure(
dimer = None,
units = 'A',
separation = None,
Lbox = None,
skew = 0,
axes = None,
kgrid = (1,1,1),
kshift = (0,0,0),
bconds = tuple('nnn'),
struct_type = Structure,
axis = 'x'
):
if dimer is None:
Structure.class_error('dimer atoms must be provided to construct dimer','generate_dimer_structure')
#end if
if separation is None:
Structure.class_error('separation must be provided to construct dimer','generate_dimer_structure')
#end if
if Lbox!=None:
axes = [[Lbox*(1-skew),0,0],[0,Lbox,0],[0,0,Lbox*(1+skew)]]
#end if
if axis=='x':
p2 = [separation,0,0]
elif axis=='y':
p2 = [0,separation,0]
elif axis=='z':
p2 = [0,0,separation]
else:
Structure.class_error('dimer orientation axis must be x,y,z\n you provided: {0}'.format(axis),'generate_dimer_structure')
#end if
if axes is None:
s = Structure(elem=dimer,pos=[[0,0,0],p2],units=units,bconds=bconds)
else:
s = Structure(elem=dimer,pos=[[0,0,0],p2],axes=axes,kgrid=kgrid,kshift=kshift,units=units,bconds=bconds)
s.center_molecule()
#end if
return s
#end def generate_dimer_structure
# test needed
def generate_trimer_structure(
trimer = None,
units = 'A',
separation = None,
angle = None,
Lbox = None,
skew = 0,
axes = None,
kgrid = (1,1,1),
kshift = (0,0,0),
struct_type = Structure,
axis = 'x',
axis2 = 'y',
angular_units = 'degrees',
plane_rot = None
):
if trimer is None:
Structure.class_error('trimer atoms must be provided to construct trimer','generate_trimer_structure')
#end if
if separation is None:
Structure.class_error('separation must be provided to construct trimer','generate_trimer_structure')
#end if
if len(separation)!=2:
Structure.class_error('two separation distances (atom1-atom2,atom1-atom3) must be provided to construct trimer\nyou provided {0} separation distances'.format(len(separation)),'generate_trimer_structure')
#end if
if angle is None:
Structure.class_error('angle must be provided to construct trimer','generate_trimer_structure')
#end if
if angular_units=='degrees':
angle *= pi/180
elif not angular_units.startswith('rad'):
Structure.class_error('angular units must be degrees or radians\nyou provided: {0}'.format(angular_units),'generate_trimer_structure')
#end if
if axis==axis2:
Structure.class_error('axis and axis2 must be different to define the trimer plane\nyou provided {0} for both'.format(axis),'generate_trimer_structure')
#end if
if Lbox!=None:
axes = [[Lbox*(1-skew),0,0],[0,Lbox,0],[0,0,Lbox*(1+skew)]]
#end if
p1 = [0,0,0]
if axis=='x':
p2 = [separation[0],0,0]
elif axis=='y':
p2 = [0,separation[0],0]
elif axis=='z':
p2 = [0,0,separation[0]]
else:
Structure.class_error('trimer bond1 (atom2-atom1) orientation axis must be x,y,z\n you provided: {0}'.format(axis),'generate_trimer_structure')
#end if
r = separation[1]
c = cos(angle)
s = sin(angle)
axpair = axis+axis2
if axpair=='xy':
p3 = [r*c,r*s,0]
elif axpair=='yx':
p3 = [r*s,r*c,0]
elif axpair=='yz':
p3 = [0,r*c,r*s]
elif axpair=='zy':
p3 = [0,r*s,r*c]
elif axpair=='zx':
p3 = [r*s,0,r*c]
elif axpair=='xz':
p3 = [r*c,0,r*s]
else:
Structure.class_error('trimer bond2 (atom3-atom1) orientation axis must be x,y,z\n you provided: {0}'.format(axis2),'generate_trimer_structure')
#end if
if axes is None:
s = Structure(elem=trimer,pos=[p1,p2,p3],units=units)
else:
s = Structure(elem=trimer,pos=[p1,p2,p3],axes=axes,kgrid=kgrid,kshift=kshift,units=units)
s.center_molecule()
#end if
if plane_rot!=None:
s.rotate_plane(axpair,plane_rot,angular_units)
#end if
return s
#end def generate_trimer_structure
# test needed
def generate_jellium_structure(*args,**kwargs):
return Jellium(*args,**kwargs)
#end def generate_jellium_structure
def generate_crystal_structure(
lattice = None,
cell = None,
centering = None,
constants = None,
atoms = None,
basis = None,
basis_vectors = None,
tiling = None,
cscale = None,
axes = None,
units = None,
angular_units = 'degrees',
magnetization = None,
mag = None,
kpoints = None,
kweights = None,
kgrid = None,
kshift = (0,0,0),
permute = None,
operations = None,
struct_type = Crystal,
elem = None,
pos = None,
frozen = None,
posu = None,
elem_pos = None,
folded_elem = None,
folded_pos = None,
folded_units = None,
use_prim = None,
add_kpath = False,
symm_kgrid = False,
#legacy inputs
structure = None,
shape = None,
element = None,
scale = None,
):
if structure is not None:
lattice = structure
#end if
if shape is not None:
cell = shape
#end if
if element is not None:
atoms = element
#end if
if scale is not None:
constants = scale
#end if
#interface for total manual specification
# this is only here because 'crystal' is default and must handle other cases
s = None
has_elem_and_pos = elem is not None and (pos is not None or posu is not None)
has_elem_and_pos |= elem_pos is not None
if has_elem_and_pos:
s = Structure(
axes = axes,
elem = elem,
pos = pos,
units = units,
mag = mag,
frozen = frozen,
magnetization = magnetization,
tiling = tiling,
kpoints = kpoints,
kgrid = kgrid,
kshift = kshift,
permute = permute,
rescale = False,
operations = operations,
posu = posu,
elem_pos = elem_pos,
use_prim = use_prim,
add_kpath = add_kpath,
symm_kgrid = symm_kgrid,
)
elif isinstance(structure,Structure):
s = structure
if use_prim is not None and use_prim is not False:
s.become_primitive(source=use_prim,add_kpath=add_kpath)
#end if
if tiling is not None:
s = s.tile(tiling)
#end if
if kpoints is not None:
s.add_kpoints(kpoints,kweights)
#end if
if kgrid is not None:
if not symm_kgrid:
s.add_kmesh(kgrid,kshift)
else:
s.add_symmetrized_kmesh(kgrid,kshift)
#end if
#end if
#end if
if s is not None:
# add point group folded molecular system if present
if folded_elem is not None and folded_pos is not None:
if folded_units is None:
folded_units = units
#end if
fs = Structure(
elem = folded_elem,
pos = folded_pos,
units = folded_units,
rescale = False,
)
s.set_folded(fs)
#end if
return s
#end if
s=Crystal(
lattice = lattice ,
cell = cell ,
centering = centering ,
constants = constants ,
atoms = atoms ,
basis = basis ,
basis_vectors = basis_vectors ,
tiling = tiling ,
cscale = cscale ,
axes = axes ,
units = units ,
angular_units = angular_units ,
frozen = frozen ,
mag = mag ,
magnetization = magnetization ,
kpoints = kpoints ,
kgrid = kgrid ,
kshift = kshift ,
permute = permute ,
operations = operations ,
elem = elem ,
pos = pos ,
use_prim = use_prim ,
add_kpath = add_kpath ,
symm_kgrid = symm_kgrid ,
)
if struct_type!=Crystal:
s=s.upcast(struct_type)
#end if
return s
#end def generate_crystal_structure
defects = obj(
diamond = obj(
H = obj(
pristine = [[0,0,0]],
defect = [[0,0,0],[.625,.375,.375]]
),
T = obj(
pristine = [[0,0,0]],
defect = [[0,0,0],[.5,.5,.5]]
),
X = obj(
pristine = [[.25,.25,.25]],
defect = [[.39,.11,.15],[.11,.39,.15]]
),
FFC = obj(
#pristine = [[ 0, 0, 0],[.25,.25,.25]],
#defect = [[.151,.151,-.08],[.10,.10,.33]]
pristine = [[ 0, 0, 0],[.25 ,.25 ,.25 ],[.5 ,.5 , 0],[.75 ,.75 ,.25 ],[1.5 ,1.5 ,0 ],[1.75 ,1.75 ,.25 ]],
defect = [[.151,.151,-.081],[.099,.099,.331],[.473,.473,-.059],[.722,.722,.230],[1.528,1.528,.020],[1.777,1.777,.309]]
)
)
)
def generate_defect_structure(defect,structure,shape=None,element=None,
tiling=None,scale=1.,kgrid=None,kshift=(0,0,0),
units=None,struct_type=DefectStructure):
if structure in defects:
dstruct = defects[structure]
else:
DefectStructure.class_error('defects for '+structure+' structure have not yet been implemented')
#end if
if defect in dstruct:
drep = dstruct[defect]
else:
DefectStructure.class_error(defect+' defect not found for '+structure+' structure')
#end if
ds = generate_crystal_structure(
structure = structure,
shape = shape,
element = element,
tiling = tiling,
scale = 1.0,
kgrid = kgrid,
kshift = kshift,
units = units,
struct_type = struct_type
)
ds.replace(drep.pristine,pos=drep.defect)
ds.rescale(scale)
return ds
#end def generate_defect_structure
def read_structure(filepath,elem=None,format=None):
s = generate_structure('empty')
s.read(filepath,elem=elem,format=format)
return s
#end def read_structure
if __name__=='__main__':
large = generate_structure(
type = 'crystal',
structure = 'diamond',
cell = 'fcc',
atoms = 'Ge',
constants = 5.639,
units = 'A',
tiling = (2,2,2),
kgrid = (1,1,1),
kshift = (0,0,0),
)
small = large.folded_structure
print(small.kpoints_unit())
prim = read_structure('scf.struct.xsf')
prim = get_primitive_cell(structure=prim)['structure']
tiling = get_band_tiling(structure=prim, kpoints_label = ['L', 'F'], min_volfac=6, max_volfac = 6)
print(tiling)
#end if