# This Source Code Form is subject to the terms of the Mozilla Public# License, v. 2.0. If a copy of the MPL was not distributed with this# file, You can obtain one at https://mozilla.org/MPL/2.0/."""Define a lattice with cell-parameters and supercellsThis class is the basis of many different objects."""from__future__importannotationsimportloggingimportmathfromcollections.abcimportSequencefromenumimportIntEnum,autofromnumbersimportIntegralfrompathlibimportPathfromtypingimportOptional,Unionimportnumpyasnpimportnumpy.typingasnptfromnumpyimportdot,ndarrayimportsisl._arrayas_afromsisl._dispatch_classimport_Dispatchsfromsisl._dispatcherimportAbstractDispatch,ClassDispatcher,TypeDispatcherfromsisl._internalimportset_modulefromsisl._math_smallimportcross3,dot3fromsisl.messagesimportSislError,deprecate,deprecate_argument,deprecation,warnfromsisl.shape.prism4importCuboidfromsisl.typingimportCellAxes,CellAxis,LatticeLikefromsisl.utils.mathematicsimportfnormfromsisl.utils.miscimportdirection,listifyfrom._latticeimportcell_invert,cell_reciprocal__all__=["Lattice","SuperCell","LatticeChild","BoundaryCondition"]_log=logging.getLogger(__name__)
[docs]classBoundaryCondition(IntEnum):"""Enum for boundary conditions"""UNKNOWN=auto()PERIODIC=auto()DIRICHLET=auto()NEUMANN=auto()OPEN=auto()
[docs]@classmethoddefgetitem(cls,key):"""Search for a specific integer entry by value, and not by name"""ifisinstance(key,cls):returnkeyifisinstance(key,bool):ifkey:returncls.PERIODICraiseValueError(f"{cls.__name__}.getitem does not allow False, which BC should this refer to?")ifisinstance(key,str):key=key.upper()iflen(key)==1:key={"U":"UNKNOWN","P":"PERIODIC","D":"DIRICHLET","N":"NEUMANN","O":"OPEN",}[key]forbcincls:ifbc.name.startswith(key):returnbcelse:forbcincls:ifbc==key:returnbcraiseKeyError(f"{cls.__name__}.getitem could not find key={key}")
BoundaryConditionType=Union[BoundaryCondition,int,str,bool]SeqBoundaryConditionType=Union[BoundaryConditionType,Sequence[BoundaryConditionType]]@set_module("sisl")classLattice(_Dispatchs,dispatchs=[ClassDispatcher("new",instance_dispatcher=TypeDispatcher),ClassDispatcher("to",type_dispatcher=None),],when_subclassing="copy",):r"""A cell class to retain lattice vectors and a supercell structure The supercell structure is comprising the *primary* unit-cell and neighboring unit-cells. The number of supercells is given by the attribute `nsc` which is a vector with 3 elements, one per lattice vector. It describes *how many* times the primary unit-cell is extended along the i'th lattice vector. For ``nsc[i] == 3`` the supercell is made up of 3 unit-cells. One *behind*, the primary unit-cell and one *after*. Parameters ---------- cell : the lattice parameters of the unit cell (the actual cell is returned from `tocell`. nsc : number of supercells along each lattice vector origin : (3,) of float, optional the origin of the supercell. boundary_condition : the boundary conditions for each of the cell's planes. Defaults to periodic boundary condition. See `BoundaryCondition` for valid enumerations. """# We limit the scope of this Lattice object.__slots__=("cell","_origin","nsc","n_s","_sc_off","_isc_off","_bc")#: Internal reference to `BoundaryCondition` for simpler short-handsBC=BoundaryConditiondef__init__(self,cell:CellLike,nsc:npt.ArrayLike=None,origin=None,boundary_condition:SeqBoundaryConditionType=BoundaryCondition.PERIODIC,):ifnscisNone:nsc=[1,1,1]# If the length of cell is 6 it must be cell-parameters, not# actual cell coordinatesself.cell=self.tocell(cell)ifnp.any(self.length<1e-7):warn(f"{self.__class__.__name__} got initialized with one or more ""lattice vector(s) with 0 length. Use with care.")iforiginisNone:self._origin=_a.zerosd(3)else:self._origin=_a.arrayd(origin)ifself._origin.size!=3:raiseValueError("Origin *must* be 3 numbers.")self.nsc=_a.onesi(3)# Set the super-cellself.set_nsc(nsc=nsc)self.set_boundary_condition(boundary_condition)@propertydeflength(self)->ndarray:"""Length of each lattice vector"""returnfnorm(self.cell)
[docs]deflengthf(self,axes:CellAxes=(0,1,2))->ndarray:"""Length of specific lattice vectors (as a function) Parameters ---------- axes: only calculate the volume based on a subset of axes Examples -------- Only get lengths of two lattice vectors: >>> lat = Lattice(1) >>> lat.lengthf([0, 1]) """axes=map(direction,listify(axes))|listifyreturnfnorm(self.cell[axes])
@propertydefvolume(self)->float:"""Volume of cell"""returnself.volumef((0,1,2))
[docs]defvolumef(self,axes:CellAxes=(0,1,2))->float:"""Volume of cell (as a function) Default to the 3D volume. For `axes` with only 2 elements, it corresponds to an area. For `axes` with only 1 element, it corresponds to a length. Parameters ---------- axes: only calculate the volume based on a subset of axes Examples -------- Only get the volume of the periodic directions: >>> lat = Lattice(1) >>> lat.pbc = (True, False, True) >>> lat.volumef(lat.pbc.nonzero()[0]) """axes=map(direction,listify(axes))|listifycell=self.celliflen(axes)==3:returnabs(dot3(cell[axes[0]],cross3(cell[axes[1]],cell[axes[2]])))iflen(axes)==2:returnfnorm(cross3(cell[axes[0]],cell[axes[1]]))iflen(axes)==1:returnfnorm(cell[axes])return0.0
[docs]defarea(self,axis1:CellAxis,axis2:CellAxis)->float:"""Calculate the area spanned by the two axis `ax0` and `ax1`"""axis1=direction(axis1)axis2=direction(axis2)returnfnorm(cross3(self.cell[axis1],self.cell[axis2]))
@propertydefboundary_condition(self)->np.ndarray:"""Boundary conditions for each lattice vector (lower/upper) sides ``(3, 2)``"""returnself._bc@boundary_condition.setterdefboundary_condition(self,boundary_condition):"""Boundary conditions for each lattice vector (lower/upper) sides ``(3, 2)``"""self.set_boundary_condition(boundary_condition)@propertydefpbc(self)->np.ndarray:"""Boolean array to specify whether the boundary conditions are periodic`"""# set_boundary_condition does not allow to have PERIODIC and non-PERIODIC# along the same lattice vector. So checking one should sufficereturnself._bc[:,0]==BoundaryCondition.PERIODIC@pbc.setterdefpbc(self,pbc)->None:"""Boolean array to specify whether the boundary conditions are periodic`"""# set_boundary_condition does not allow to have PERIODIC and non-PERIODIC# along the same lattice vector. So checking one should sufficeassertlen(pbc)==3PERIODIC=BoundaryCondition.PERIODICforaxis,bcinenumerate(pbc):# Simply skip those that are not T|Fifnotisinstance(bc,bool):continueifbc:self._bc[axis]=PERIODICelifself._bc[axis,0]==PERIODIC:self._bc[axis]=BoundaryCondition.UNKNOWN@propertydeforigin(self)->ndarray:"""Origin for the cell"""returnself._origin@origin.setterdeforigin(self,origin):"""Set origin for the cell"""self._origin[:]=origin
[docs]@deprecation("toCuboid is deprecated, please use lattice.to['cuboid'](...) instead.","0.15","0.16",)deftoCuboid(self,*args,**kwargs):"""A cuboid with vectors as this unit-cell and center with respect to its origin Parameters ---------- orthogonal : bool, optional if true the cuboid has orthogonal sides such that the entire cell is contained """returnself.to[Cuboid](*args,**kwargs)
[docs]defset_boundary_condition(self,boundary:Optional[SeqBoundaryConditionType]=None,a:Optional[SeqBoundaryConditionType]=None,b:Optional[SeqBoundaryConditionType]=None,c:Optional[SeqBoundaryConditionType]=None,):"""Set the boundary conditions on the grid Parameters ---------- boundary : (3, 2) or (3, ) or int, optional boundary condition for all boundaries (or the same for all) a : int or list of int, optional boundary condition for the first unit-cell vector direction b : int or list of int, optional boundary condition for the second unit-cell vector direction c : int or list of int, optional boundary condition for the third unit-cell vector direction Raises ------ ValueError if specifying periodic one one boundary, so must the opposite side. """getitem=BoundaryCondition.getitemdefconv(v):ifvisNone:returnvifisinstance(v,(np.ndarray,list,tuple)):returnlist(map(getitem,v))returngetitem(v)ifnothasattr(self,"_bc"):self._bc=_a.fulli([3,2],getitem("Unknown"))old=self._bc.copy()ifnotboundaryisNone:ifisinstance(boundary,(Integral,str,bool)):try:getitem(boundary)self._bc[:,:]=conv(boundary)exceptKeyError:ford,bcinenumerate(boundary):bc=conv(bc)ifbcisnotNone:self._bc[d]=conv(bc)else:ford,bcinenumerate(boundary):bc=conv(bc)ifbcisnotNone:self._bc[d]=bcford,vinenumerate([a,b,c]):v=conv(v)ifvisnotNone:self._bc[d]=v# shorthand for bcfornsc,bc,changedinzip(self.nsc,self._bc==BoundaryCondition.PERIODIC,self._bc!=old):ifbc.any()andnotbc.all():raiseValueError(f"{self.__class__.__name__}.set_boundary_condition has a one non-periodic and ""one periodic direction. If one direction is periodic, both instances ""must have that BC.")ifchanged.any()and(~bc).all()andnsc>1:warn(f"{self.__class__.__name__}.set_boundary_condition is having image connections (nsc={nsc}>1) ""while having a non-periodic boundary condition.")
[docs]defparameters(self,rad:bool=False)->tuple[float,float,float,float,float,float]:r"""Cell parameters of this cell in 3 lengths and 3 angles Notes ----- Since we return the length and angles between vectors it may not be possible to recreate the same cell. Only in the case where the first lattice vector *only* has a Cartesian :math:`x` component will this be the case. Parameters ---------- rad : bool, optional whether the angles are returned in radians (otherwise in degree) Returns ------- length : numpy.ndarray length of each lattice vector angles : numpy.ndarray angles between the lattice vectors (in Voigt notation) ``[0]`` is between 2nd and 3rd lattice vector, etc. """ifrad:f=1.0else:f=180/np.pi# Calculate length of each lattice vectorcell=self.cell.copy()abc=fnorm(cell)cell=cell/abc.reshape(-1,1)angles=np.empty(3)angles[0]=math.acos(dot3(cell[1],cell[2]))*fangles[1]=math.acos(dot3(cell[0],cell[2]))*fangles[2]=math.acos(dot3(cell[0],cell[1]))*freturnabc,angles
def_fill(self,non_filled,dtype=None):"""Return a zero filled array of length 3"""iflen(non_filled)==3:returnnon_filled# Fill in zeros# This will purposefully raise an exception# if the dimensions of the periodic one# are not consistent.ifdtypeisNone:try:dtype=non_filled.dtypeexceptException:dtype=np.dtype(non_filled[0].__class__)ifdtype==np.dtype(int):# Never go higher than int32 for default# guesses on integer lists.dtype=np.int32f=np.zeros(3,dtype)i=0ifself.nsc[0]>1:f[0]=non_filled[i]i+=1ifself.nsc[1]>1:f[1]=non_filled[i]i+=1ifself.nsc[2]>1:f[2]=non_filled[i]returnfdef_fill_sc(self,supercell_index):"""Return a filled supercell index by filling in zeros where needed"""returnself._fill(supercell_index,dtype=np.int32)
[docs]defset_nsc(self,nsc=None,a:Optional[int]=None,b:Optional[int]=None,c:Optional[int]=None,)->None:"""Sets the number of supercells in the 3 different cell directions Parameters ---------- nsc : list of int, optional number of supercells in each direction a : int, optional number of supercells in the first unit-cell vector direction b : int, optional number of supercells in the second unit-cell vector direction c : int, optional number of supercells in the third unit-cell vector direction """ifnotnscisNone:foriinrange(3):ifnotnsc[i]isNone:self.nsc[i]=nsc[i]ifa:self.nsc[0]=aifb:self.nsc[1]=bifc:self.nsc[2]=c# Correct for misplaced number of unit-cellsforiinrange(3):ifself.nsc[i]==0:self.nsc[i]=1ifnp.sum(self.nsc%2)!=3:raiseValueError("Supercells has to be of un-even size. The primary cell counts "+"one, all others count 2")# We might use this very often, hence we store itself.n_s=_a.prodi(self.nsc)self._sc_off=_a.zerosi([self.n_s,3])self._isc_off=_a.zerosi(self.nsc)n=self.nsc# We define the following ones like this:defret_range(val):i=val//2returnrange(-i,i+1)x=ret_range(n[0])y=ret_range(n[1])z=ret_range(n[2])i=0forizinz:foriyiny:forixinx:ifix==0andiy==0andiz==0:continue# Increment indexi+=1# The offsets for the supercells in the# sparsity patternself._sc_off[i,0]=ixself._sc_off[i,1]=iyself._sc_off[i,2]=izself._update_isc_off()
def_update_isc_off(self):"""Internal routine for updating the supercell indices"""foriinrange(self.n_s):d=self.sc_off[i]self._isc_off[d[0],d[1],d[2]]=i@propertydefsc_off(self)->ndarray:"""Integer supercell offsets"""returnself._sc_off@sc_off.setterdefsc_off(self,sc_off):"""Set the supercell offset"""self._sc_off[:,:]=_a.arrayi(sc_off,order="C")self._update_isc_off()@propertydefisc_off(self)->ndarray:"""Internal indexed supercell ``[ia, ib, ic] == i``"""returnself._isc_offdef__iter__(self):"""Iterate the supercells and the indices of the supercells"""yield fromenumerate(self.sc_off)
[docs]@deprecate_argument("axis","axes","argument axis has been deprecated in favor of axes, please update your code.","0.15","0.16",)@deprecate_argument("tol","atol","argument tol has been deprecated in favor of atol, please update your code.","0.15","0.16",)deffit(self,xyz,axes:CellAxes=(0,1,2),atol:float=0.05)->Lattice:"""Fit the supercell to `xyz` such that the unit-cell becomes periodic in the specified directions The fitted supercell tries to determine the unit-cell parameters by solving a set of linear equations corresponding to the current supercell vectors. >>> numpy.linalg.solve(self.cell.T, xyz.T) It is important to know that this routine will *only* work if at least some of the atoms are integer offsets of the lattice vectors. I.e. the resulting fit will depend on the translation of the coordinates. Parameters ---------- xyz : array_like ``shape(*, 3)`` the coordinates that we will wish to encompass and analyze. axes : only the cell-vectors along the provided axes will be used atol : tolerance (in Angstrom) of the positions. I.e. we neglect coordinates which are not within the radius of this magnitude Raises ------ RuntimeError : when the cell-parameters does not fit within the given tolerance (`atol`). """# In case the passed coordinates are from a Geometryfrom.geometryimportGeometryifisinstance(xyz,Geometry):xyz=xyz.xyzcell=np.copy(self.cell)# Get fractional coordinates to get the divisions in the current cellx=dot(xyz,self.icell.T)# Now we should figure out the correct repetitions# by rounding to integer positions of the cell vectorsix=np.rint(x)# Figure out the displacements from integers# Then reduce search space by removing those coordinates# that are more than the tolerance.dist=np.sqrt((dot(cell.T,(x-ix).T)**2).sum(0))idx=(dist<=atol).nonzero()[0]iflen(idx)==0:raiseRuntimeError("Could not fit the cell parameters to the coordinates ""due to insufficient accuracy (try to increase the tolerance)")# Reduce problem to allowed values below the toleranceix=ix[idx]# Reduce to total repetitionsireps=np.amax(ix,axis=0)-np.amin(ix,axis=0)+1# Reduce the non-set axisifnotaxesisNone:axes=map(direction,listify(axes))foraxin(0,1,2):ifaxnotinaxes:ireps[ax]=1# Enlarge the cell vectorscell[0]*=ireps[0]cell[1]*=ireps[1]cell[2]*=ireps[2]returnself.copy(cell)
[docs]defplane(self,axis1:CellAxis,axis2:CellAxis,origin:bool=True)->tuple[ndarray,ndarray]:"""Query point and plane-normal for the plane spanning `ax1` and `ax2` Parameters ---------- axis1 : the first axis vector axis2 : the second axis vector origin : bool, optional whether the plane intersects the origin or the opposite corner of the unit-cell. Returns ------- normal_V : numpy.ndarray planes normal vector (pointing outwards with regards to the cell) p : numpy.ndarray a point on the plane Examples -------- All 6 faces of the supercell can be retrieved like this: >>> lattice = Lattice(4) >>> n1, p1 = lattice.plane(0, 1, True) >>> n2, p2 = lattice.plane(0, 1, False) >>> n3, p3 = lattice.plane(0, 2, True) >>> n4, p4 = lattice.plane(0, 2, False) >>> n5, p5 = lattice.plane(1, 2, True) >>> n6, p6 = lattice.plane(1, 2, False) However, for performance critical calculations it may be advantageous to do this: >>> lattice = Lattice(4) >>> uc = lattice.cell.sum(0) >>> n1, p1 = lattice.plane(0, 1) >>> n2 = -n1 >>> p2 = p1 + uc >>> n3, p3 = lattice.plane(0, 2) >>> n4 = -n3 >>> p4 = p3 + uc >>> n5, p5 = lattice.plane(1, 2) >>> n6 = -n5 >>> p6 = p5 + uc Secondly, the variables ``p1``, ``p3`` and ``p5`` are always ``[0, 0, 0]`` and ``p2``, ``p4`` and ``p6`` are always ``uc``. Hence this may be used to further reduce certain computations. """axis1=direction(axis1)axis2=direction(axis2)cell=self.celln=cross3(cell[axis1],cell[axis2])# Normalizen/=dot3(n,n)**0.5# Now we need to figure out if the normal vector# is pointing outwards# Take the cell centerup=cell.sum(0)# Calculate the distance from the plane to the center of the cell# If d is positive then the normal vector is pointing towards# the center, so rotate 180ifdot3(n,up/2)>0.0:n*=-1iforigin:returnn,_a.zerosd([3])# We have to reverse the normal vectorreturn-n,up
def__mul__(self,m)->Lattice:"""Implement easy repeat function Parameters ---------- m : int or array_like of length 3 a single integer may be regarded as [m, m, m]. A list will expand the unit-cell along the equivalent lattice vector. Returns ------- Lattice enlarged supercell """# Simple formifisinstance(m,Integral):returnself.tile(m,0).tile(m,1).tile(m,2)lattice=self.copy()fori,rinenumerate(m):lattice=lattice.tile(r,i)returnlattice@propertydeficell(self)->ndarray:"""Returns the reciprocal (inverse) cell for the `Lattice`. Note: The returned vectors are still in ``[0, :]`` format and not as returned by an inverse LAPACK algorithm. """returncell_invert(self.cell)@propertydefrcell(self)->ndarray:"""Returns the reciprocal cell for the `Lattice` with ``2*np.pi`` Note: The returned vectors are still in [0, :] format and not as returned by an inverse LAPACK algorithm. """returncell_reciprocal(self.cell)
[docs]defcell2length(self,length,axes:CellAxes=(0,1,2))->ndarray:"""Calculate cell vectors such that they each have length `length` Parameters ---------- length : float or array_like length for cell vectors, if an array it corresponds to the individual vectors and it must have length equal to `axes` axes : which axes the `length` variable refers too. Returns ------- numpy.ndarray cell-vectors with prescribed length, same order as `axes` """axes=map(direction,listify(axes))|listifylength=_a.asarray(length).ravel()iflen(length)!=len(axes):iflen(length)==1:length=np.tile(length,len(axes))else:raiseValueError(f"{self.__class__.__name__}.cell2length length parameter should be a single ""float, or an array of values according to axes argument.")returnself.cell[axes]*(length/self.length[axes]).reshape(-1,1)
[docs]defoffset(self,isc=None)->tuple[float,float,float]:"""Returns the supercell offset of the supercell index"""ifiscisNone:return_a.arrayd([0,0,0])returndot(isc,self.cell)
[docs]defadd_vacuum(self,vacuum:float,axis:CellAxis,orthogonal_to_plane:bool=False)->Lattice:"""Returns a new object with vacuum along the `axis` lattice vector Parameters ---------- vacuum : float amount of vacuum added, in Ang axis : the lattice vector to add vacuum along orthogonal_to_plane : bool, optional whether the lattice vector should be elongated so that it is `vacuum` longer when projected onto the normal vector of the other two axis. """axis=direction(axis)cell=np.copy(self.cell)d=cell[axis].copy()d/=fnorm(d)iforthogonal_to_plane:# first calculate the normal vector of the other planen=cross3(cell[axis-1],cell[axis-2])n/=fnorm(n)# now project onto cellprojection=n@d# calculate how long it should be so that the normal vector# is `vacuum` longerscale=vacuum/abs(projection)else:scale=vacuum# normalize to get direction vectorcell[axis]+=d*scalereturnself.copy(cell)
[docs]defsc_index(self,sc_off)->Union[int,Sequence[int]]:"""Returns the integer index in the sc_off list that corresponds to `sc_off` Returns the index for the supercell in the global offset. Parameters ---------- sc_off : (3,) or list of (3,) super cell specification. For each axis having value ``None`` all supercells along that axis is returned. """def_assert(m,v):ifnp.any(np.abs(v)>m):raiseValueError("Requesting a non-existing supercell index")hsc=self.nsc//2iflen(sc_off)==0:return_a.arrayi([[]])elifisinstance(sc_off[0],ndarray):_assert(hsc[0],sc_off[:,0])_assert(hsc[1],sc_off[:,1])_assert(hsc[2],sc_off[:,2])returnself._isc_off[sc_off[:,0],sc_off[:,1],sc_off[:,2]]elifisinstance(sc_off[0],(tuple,list)):# We are dealing with a list of listssc_off=np.asarray(sc_off)_assert(hsc[0],sc_off[:,0])_assert(hsc[1],sc_off[:,1])_assert(hsc[2],sc_off[:,2])returnself._isc_off[sc_off[:,0],sc_off[:,1],sc_off[:,2]]# Fall back to the other routinessc_off=self._fill_sc(sc_off)ifsc_off[0]isnotNoneandsc_off[1]isnotNoneandsc_off[2]isnotNone:_assert(hsc[0],sc_off[0])_assert(hsc[1],sc_off[1])_assert(hsc[2],sc_off[2])returnself._isc_off[sc_off[0],sc_off[1],sc_off[2]]# We build it because there are 'none'ifsc_off[0]isNone:idx=_a.arangei(self.n_s)else:idx=(self.sc_off[:,0]==sc_off[0]).nonzero()[0]ifnotsc_off[1]isNone:idx=idx[(self.sc_off[idx,1]==sc_off[1]).nonzero()[0]]ifnotsc_off[2]isNone:idx=idx[(self.sc_off[idx,2]==sc_off[2]).nonzero()[0]]returnidx
[docs]defvertices(self)->ndarray:"""Vertices of the cell Returns -------- array of shape (2, 2, 2, 3): The coordinates of the vertices of the cell. The first three dimensions correspond to each cell axis (off, on), and the last one contains the xyz coordinates. """verts=np.zeros([2,2,2,3])verts[1,:,:,0]=1verts[:,1,:,1]=1verts[:,:,1,2]=1returnverts@self.cell
[docs]@classmethoddeftocell(cls,*args)->Lattice:r"""Returns a 3x3 unit-cell dependent on the input 1 argument a unit-cell along Cartesian coordinates with side-length equal to the argument. 3 arguments the diagonal components of a Cartesian unit-cell 6 arguments the cell parameters given by :math:`a`, :math:`b`, :math:`c`, :math:`\alpha`, :math:`\beta` and :math:`\gamma` (angles in degrees). 9 arguments a 3x3 unit-cell. Parameters ---------- *args : float May be either, 1, 3, 6 or 9 elements. Note that the arguments will be put into an array and flattened before checking the number of arguments. Examples -------- >>> cell_1_1_1 = Lattice.tocell(1.) >>> cell_1_2_3 = Lattice.tocell(1., 2., 3.) >>> cell_1_2_3 = Lattice.tocell([1., 2., 3.]) # same as above """# Convert into true array (flattened)args=_a.arrayd(args,order="C").ravel()nargs=len(args)# A square-boxifnargs==1:returnnp.diag([args[0]]*3)# Diagonal componentsifnargs==3:returnnp.diag(args)# Cell parametersifnargs==6:cell=_a.zerosd([3,3])a=args[0]b=args[1]c=args[2]alpha=args[3]beta=args[4]gamma=args[5]frommathimportcos,pi,sin,sqrtpi180=pi/180.0cell[0,0]=ag=gamma*pi180cg=cos(g)sg=sin(g)cell[1,0]=b*cgcell[1,1]=b*sgb=beta*pi180cb=cos(b)sb=sin(b)cell[2,0]=c*cba=alpha*pi180d=(cos(a)-cb*cg)/sgcell[2,1]=c*dcell[2,2]=c*sqrt(sb**2-d**2)returncell# A complete cellifnargs==9:returnargs.reshape(3,3)raiseValueError(f"Creating a unit cell has to have 1, 3, 6 or 9 arguments, got {nargs}.")
[docs]@deprecate_argument("tol","rtol","argument tol has been deprecated in favor of rtol, please update your code.","0.15","0.16",)defis_orthogonal(self,rtol:float=0.001)->bool:""" Returns true if the cell vectors are orthogonal. Internally this will be done on the normalized lattice vectors to ensure no numerical instability. Parameters ----------- rtol: float, optional the threshold above which the scalar product of two normalized cell vectors will be considered non-zero. """# Convert to unit-vector cellcell=self.cellcl=fnorm(cell)cell[0]=cell[0]/cl[0]cell[1]=cell[1]/cl[1]cell[2]=cell[2]/cl[2]i_s=dot3(cell[0],cell[1])<rtoli_s=dot3(cell[0],cell[2])<rtolandi_si_s=dot3(cell[1],cell[2])<rtolandi_sreturni_s
[docs]@deprecate_argument("tol","atol","argument tol has been deprecated in favor of atol, please update your code.","0.15","0.16",)defis_cartesian(self,atol:float=0.001)->bool:""" Checks if cell vectors a,b,c are multiples of the cartesian axis vectors (x, y, z) Parameters ----------- atol: float, optional the threshold above which an off diagonal term will be considered non-zero. """# Get the off diagonal terms of the celloff_diagonal=self.cell.ravel()[:-1].reshape(2,4)[:,1:]# Check if all are bolew the threshold tolerancereturnnp.all(np.abs(off_diagonal)<=atol)
[docs]defparallel(self,other,axes:CellAxes=(0,1,2))->bool:"""Returns true if the cell vectors are parallel to `other` Parameters ---------- other : Lattice the other object to check whether the axis are parallel axes : only check the specified axes (default to all) """axis=map(direction,listify(axes))# Convert to unit-vector cellforiinaxis:a=self.cell[i]/fnorm(self.cell[i])b=other.cell[i]/fnorm(other.cell[i])ifabs(dot3(a,b)-1)>0.001:returnFalsereturnTrue
[docs]defangle(self,axis1:CellAxis,axis2:CellAxis,rad:bool=False)->float:"""The angle between two of the cell vectors Parameters ---------- axis1 : the first cell vector axis2 : the second cell vector rad : bool, optional whether the returned value is in radians """axis1=direction(axis1)axis2=direction(axis2)n=fnorm(self.cell[[axis1,axis2]])ang=math.acos(dot3(self.cell[axis1],self.cell[axis2])/(n[0]*n[1]))ifrad:returnangreturnmath.degrees(ang)
[docs]@staticmethoddefread(sile,*args,**kwargs)->Lattice:"""Reads the supercell from the `Sile` using ``Sile.read_lattice`` Parameters ---------- sile : Sile, str or pathlib.Path a `Sile` object which will be used to read the supercell if it is a string it will create a new sile using `sisl.io.get_sile`. """# This only works because, they *must*# have been imported previouslyfromsisl.ioimportBaseSile,get_sileifisinstance(sile,BaseSile):returnsile.read_lattice(*args,**kwargs)else:withget_sile(sile,mode="r")asfh:returnfh.read_lattice(*args,**kwargs)
[docs]@deprecate_argument("tol","atol","argument tol has been deprecated in favor of atol, please update your code.","0.15","0.16",)defequal(self,other,atol:float=1e-4)->bool:"""Check whether two lattices are equivalent Parameters ---------- other : Lattice the other object to check whether the lattice is equivalent atol : float, optional tolerance value for the cell vectors and origin """ifnotisinstance(other,(Lattice,LatticeChild)):returnFalsesame=np.allclose(self.cell,other.cell,atol=atol)same=sameandnp.allclose(self.nsc,other.nsc)same=sameandnp.allclose(self.origin,other.origin,atol=atol)returnsame
def__str__(self)->str:"""Returns a string representation of the object"""# Create format for lattice vectorsdefbcstr(bc):left=BoundaryCondition.getitem(bc[0]).name.capitalize()ifbc[0]==bc[1]:# single stringreturnleftright=BoundaryCondition.getitem(bc[1]).name.capitalize()returnf"[{left}, {right}]"s=",\n ".join(["ABC"[i]+"=[{:.4f}, {:.4f}, {:.4f}]".format(*self.cell[i])foriin(0,1,2)])origin="[{:.4f}, {:.4f}, {:.4f}]".format(*self.origin)bc=",\n ".join(map(bcstr,self.boundary_condition))returnf"{self.__class__.__name__}{{nsc: {self.nsc},\n origin={origin},\n{s},\n bc=[{bc}]\n}}"def__repr__(self)->str:abc,abg=self.parameters()a,b,c=map(lambdar:round(r,4),abc.tolist())alpha,beta,gamma=map(lambdar:round(r,4),abg.tolist())BC=BoundaryConditionbc=self.boundary_conditiondefbcstr(bc):left=BC.getitem(bc[0]).name[0]ifbc[0]==bc[1]:# single stringreturnleftright=BC.getitem(bc[1]).name[0]returnf"[{left}, {right}]"bc=", ".join(map(bcstr,self.boundary_condition))returnf"<{self.__module__}.{self.__class__.__name__} a={a}, b={b}, c={c}, α={alpha}, β={beta}, γ={gamma}, bc=[{bc}], nsc={self.nsc}>"def__eq__(self,other)->bool:"""Equality check"""returnself.equal(other)def__ne__(self,b)->bool:"""In-equality check"""returnnot(self==b)# Create pickling routinesdef__getstate__(self):"""Returns the state of this object"""return{"cell":self.cell,"nsc":self.nsc,"sc_off":self.sc_off,"origin":self.origin,}def__setstate__(self,d):"""Re-create the state of this object"""self.__init__(d["cell"],d["nsc"],d["origin"])self.sc_off=d["sc_off"]new_dispatch=Lattice.newto_dispatch=Lattice.to# Define base-class for thisclassLatticeNewDispatch(AbstractDispatch):"""Base dispatcher from class passing arguments to Lattice class"""classLatticeNewLatticeDispatch(LatticeNewDispatch):defdispatch(self,lattice,copy:bool=False)->Lattice:"""Return Lattice as-is, for sanitization purposes"""cls=self._get_class()ifcls!=lattice.__class__:lattice=cls(lattice.cell.copy(),nsc=lattice.nsc.copy(),origin=lattice.origin.copy(),boundary_condition=lattice.boundary_condition.copy(),)copy=Falseifcopy:returnlattice.copy()returnlatticenew_dispatch.register(Lattice,LatticeNewLatticeDispatch)classLatticeNewListLikeDispatch(LatticeNewDispatch):defdispatch(self,cell,*args,**kwargs)->Lattice:"""Converts simple `array-like` variables to a `Lattice` Examples -------- >>> Lattice.new([1, 2, 3]) == Lattice([1, 2, 3]) """returnLattice(cell,*args,**kwargs)# A cell can be created form a ndarray/list/tuplenew_dispatch.register("ndarray",LatticeNewListLikeDispatch)new_dispatch.register(np.ndarray,LatticeNewListLikeDispatch)new_dispatch.register(int,LatticeNewListLikeDispatch)new_dispatch.register(float,LatticeNewListLikeDispatch)new_dispatch.register(list,LatticeNewListLikeDispatch)new_dispatch.register(tuple,LatticeNewListLikeDispatch)classLatticeNewAseDispatch(LatticeNewDispatch):defdispatch(self,aseg)->Lattice:"""`ase.Cell` conversion to `Lattice`"""cls=self._get_class(allow_instance=True)cell=aseg.get_cell()nsc=[3ifpbcelse1forpbcinaseg.pbc]returncls(cell,nsc=nsc)new_dispatch.register("ase",LatticeNewAseDispatch)# currently we can't ensure the ase Atoms type# to get it by type(). That requires ase to be importable.try:fromaseimportCellasase_Cellnew_dispatch.register(ase_Cell,LatticeNewAseDispatch)# ensure we don't pollute name-spacedelase_CellexceptException:passclassLatticeNewFileDispatch(LatticeNewDispatch):defdispatch(self,*args,**kwargs)->Lattice:"""Defer the `Lattice.read` method by passing down arguments"""cls=self._get_class()returncls.read(*args,**kwargs)new_dispatch.register(str,LatticeNewFileDispatch)new_dispatch.register(Path,LatticeNewFileDispatch)# see sisl/__init__.py for new_dispatch.register(BaseSile, ...)classLatticeToDispatch(AbstractDispatch):"""Base dispatcher from class passing from Lattice class"""classLatticeToAseDispatch(LatticeToDispatch):defdispatch(self,**kwargs)->ase.Cell:"""`Lattice` conversion to an `ase.Cell` object."""fromaseimportCellasase_Celllattice=self._get_object()returnase_Cell(lattice.cell.copy())to_dispatch.register("ase",LatticeToAseDispatch)classLatticeToSileDispatch(LatticeToDispatch):defdispatch(self,*args,**kwargs)->Any:"""`Lattice` writing to a sile. Examples -------- >>> geom = si.geom.graphene() >>> geom.lattice.to("hello.xyz") >>> geom.lattice.to(pathlib.Path("hello.xyz")) """lattice=self._get_object()returnlattice.write(*args,**kwargs)to_dispatch.register("str",LatticeToSileDispatch)to_dispatch.register("Path",LatticeToSileDispatch)# to do geom.to[Path](path)to_dispatch.register(str,LatticeToSileDispatch)to_dispatch.register(Path,LatticeToSileDispatch)classLatticeToCuboidDispatch(LatticeToDispatch):defdispatch(self,center=None,origin=None,orthogonal=False)->Cuboid:"""Convert lattice parameters to a `Cuboid`"""lattice=self._get_object()cell=lattice.cell.copy()ifcenterisNone:center=lattice.center()center=_a.asarray(center)iforiginisNone:origin=lattice.originorigin=_a.asarray(origin)center_off=center+originifnotorthogonal:returnCuboid(cell,center_off)deffind_min_max(cmin,cmax,new):foriinrange(3):cmin[i]=min(cmin[i],new[i])cmax[i]=max(cmax[i],new[i])cmin=cell.min(0)cmax=cell.max(0)find_min_max(cmin,cmax,cell[[0,1]].sum(0))find_min_max(cmin,cmax,cell[[0,2]].sum(0))find_min_max(cmin,cmax,cell[[1,2]].sum(0))find_min_max(cmin,cmax,cell.sum(0))returnCuboid(cmax-cmin,center_off)to_dispatch.register("Cuboid",LatticeToCuboidDispatch)to_dispatch.register(Cuboid,LatticeToCuboidDispatch)@set_module("sisl")classSuperCell(Lattice):"""Deprecated class, please use `Lattice` instead"""def__init__(self,*args,**kwargs):deprecate(f"{self.__class__.__name__} is deprecated; please use 'Lattice' class instead","0.15","0.16",)super().__init__(*args,**kwargs)@set_module("sisl")classLatticeChild:"""Class to be inherited by using the ``self.lattice`` as a `Lattice` object Initialize by a `Lattice` object and get access to several different routines directly related to the `Lattice` class. """@propertydefsc(self):"""[deprecated] Return the lattice object associated with the `Lattice`."""deprecate(f"{self.__class__.__name__}.sc is deprecated; please use 'lattice' instead","0.15","0.16",)returnself.latticedefset_nsc(self,*args,**kwargs):"""Set the number of super-cells in the `Lattice` object See `set_nsc` for allowed parameters. See Also -------- Lattice.set_nsc : the underlying called method """self.lattice.set_nsc(*args,**kwargs)defset_lattice(self,lattice:LatticeLike):"""Overwrites the local lattice."""iflatticeisNone:# Default supercell is a simple# 1x1x1 unit-celllattice=[1.0,1.0,1.0]self.lattice=Lattice.new(lattice)set_supercell=deprecation("set_sc is deprecated; please use set_lattice instead","0.15","0.16")(set_lattice)@propertydeflength(self)->float:"""Returns the inherent `Lattice.length`"""returnself.lattice.length@propertydefvolume(self)->float:"""Returns the inherent `Lattice.volume`"""returnself.lattice.volumedefarea(self,ax0,ax1)->float:"""Calculate the area spanned by the two axis `ax0` and `ax1`"""returnself.lattice.area(ax0,ax1)@propertydefcell(self)->ndarray:"""Returns the inherent `Lattice.cell`"""returnself.lattice.cell@propertydeficell(self)->ndarray:"""Returns the inherent `Lattice.icell`"""returnself.lattice.icell@propertydefrcell(self)->ndarray:"""Returns the inherent `Lattice.rcell`"""returnself.lattice.rcell@propertydeforigin(self)->ndarray:"""Returns the inherent `Lattice.origin`"""returnself.lattice.origin@propertydefn_s(self)->int:"""Returns the inherent `Lattice.n_s`"""returnself.lattice.n_s@propertydefnsc(self)->ndarray:"""Returns the inherent `Lattice.nsc`"""returnself.lattice.nsc@propertydefsc_off(self)->ndarray:"""Returns the inherent `Lattice.sc_off`"""returnself.lattice.sc_off@propertydefisc_off(self)->ndarray:"""Returns the inherent `Lattice.isc_off`"""returnself.lattice.isc_offdefsc_index(self,*args,**kwargs)->Union[int,Sequence[int]]:"""Call local `Lattice.sc_index` function"""returnself.lattice.sc_index(*args,**kwargs)@propertydefboundary_condition(self)->np.ndarray:f"""{Lattice.boundary_condition.__doc__}"""returnself.lattice.boundary_condition@boundary_condition.setterdefboundary_condition(self,boundary_condition:Sequence[BoundaryConditionType]):f"""{Lattice.boundary_condition.__doc__}"""raiseSislError(f"Cannot use property to set boundary conditions of LatticeChild")@propertydefpbc(self)->np.ndarray:__doc__=Lattice.pbc.__doc__returnself.lattice.pbcclassLatticeNewLatticeChildDispatch(LatticeNewDispatch):defdispatch(self,obj,copy:bool=False)->Lattice:"""Extraction of `Lattice` object from a `LatticeChild` object."""# for sanitation purposesifcopy:returnobj.lattice.copy()returnobj.latticenew_dispatch.register(LatticeChild,LatticeNewLatticeChildDispatch)# Remove referencesdelnew_dispatch,to_dispatch