# 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 supercell
This class is the basis of many different objects.
"""
import math
import warnings
from numbers import Integral
import numpy as np
from numpy import dot
from ._internal import set_module
from . import _plot as plt
from . import _array as _a
from .messages import deprecate_method
from .utils.mathematics import fnorm
from .shape.prism4 import Cuboid
from .quaternion import Quaternion
from ._math_small import cross3, dot3
from ._supercell import cell_invert, cell_reciprocal
__all__ = ['SuperCell', 'SuperCellChild']
@set_module("sisl")
class SuperCell:
r""" A cell class to retain lattice vectors and a supercell structure
The supercell structure is comprising the *primary* unit-cell and neighbouring
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 : array_like
the lattice parameters of the unit cell (the actual cell
is returned from `tocell`.
nsc : array_like of int
number of supercells along each latticevector
origin : (3,) of float, optional
the origin of the supercell.
"""
# We limit the scope of this SuperCell object.
__slots__ = ('cell', '_origin', 'nsc', 'n_s', '_sc_off', '_isc_off')
[docs] def __init__(self, cell, nsc=None, origin=None):
if nsc is None:
nsc = [1, 1, 1]
# If the length of cell is 6 it must be cell-parameters, not
# actual cell coordinates
self.cell = self.tocell(cell)
if origin is None:
self._origin = _a.zerosd(3)
else:
self._origin = _a.arrayd(origin)
if self._origin.size != 3:
raise ValueError("Origin *must* be 3 numbers.")
self.nsc = _a.onesi(3)
# Set the super-cell
self.set_nsc(nsc=nsc)
@property
def length(self):
""" Length of each lattice vector """
return fnorm(self.cell)
@property
def volume(self):
""" Volume of cell """
return abs(dot3(self.cell[0, :], cross3(self.cell[1, :], self.cell[2, :])))
[docs] def area(self, ax0, ax1):
""" Calculate the area spanned by the two axis `ax0` and `ax1` """
return (cross3(self.cell[ax0, :], self.cell[ax1, :]) ** 2).sum() ** 0.5
@property
def origin(self):
""" Origin for the cell """
return self._origin
@origin.setter
def origin(self, origin):
""" Set origin for the cell """
self._origin[:] = origin
@property
@deprecate_method("use .origin instead")
def origo(self):
""" Origin for the cell """
return self._origin
@origo.setter
@deprecate_method("use .origin instead")
def origo(self, origin):
""" Set origin """
self._origin[:] = origin
[docs] def toCuboid(self, orthogonal=False):
""" 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
"""
if not orthogonal:
return Cuboid(self.cell.copy(), self.center() + self.origin)
def find_min_max(cmin, cmax, new):
for i in range(3):
cmin[i] = min(cmin[i], new[i])
cmax[i] = max(cmax[i], new[i])
cmin = self.cell.min(0)
cmax = self.cell.max(0)
find_min_max(cmin, cmax, self.cell[[0, 1], :].sum(0))
find_min_max(cmin, cmax, self.cell[[0, 2], :].sum(0))
find_min_max(cmin, cmax, self.cell[[1, 2], :].sum(0))
find_min_max(cmin, cmax, self.cell.sum(0))
return Cuboid(cmax - cmin, self.center() + self.origin)
[docs] def parameters(self, rad=False):
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
-------
float
length of first lattice vector
float
length of second lattice vector
float
length of third lattice vector
float
angle between b and c vectors
float
angle between a and c vectors
float
angle between a and b vectors
"""
if rad:
f = 1.
else:
f = 180 / np.pi
# Calculate length of each lattice vector
cell = self.cell.copy()
abc = fnorm(cell)
from math import acos
cell = cell / abc.reshape(-1, 1)
alpha = acos(dot3(cell[1, :], cell[2, :])) * f
beta = acos(dot3(cell[0, :], cell[2, :])) * f
gamma = acos(dot3(cell[0, :], cell[1, :])) * f
return abc[0], abc[1], abc[2], alpha, beta, gamma
def _fill(self, non_filled, dtype=None):
""" Return a zero filled array of length 3 """
if len(non_filled) == 3:
return non_filled
# Fill in zeros
# This will purposefully raise an exception
# if the dimensions of the periodic ones
# are not consistent.
if dtype is None:
try:
dtype = non_filled.dtype
except Exception:
dtype = np.dtype(non_filled[0].__class__)
if dtype == np.dtype(int):
# Never go higher than int32 for default
# guesses on integer lists.
dtype = np.int32
f = np.zeros(3, dtype)
i = 0
if self.nsc[0] > 1:
f[0] = non_filled[i]
i += 1
if self.nsc[1] > 1:
f[1] = non_filled[i]
i += 1
if self.nsc[2] > 1:
f[2] = non_filled[i]
return f
def _fill_sc(self, supercell_index):
""" Return a filled supercell index by filling in zeros where needed """
return self._fill(supercell_index, dtype=np.int32)
[docs] def set_nsc(self, nsc=None, a=None, b=None, c=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 : integer, optional
number of supercells in the first unit-cell vector direction
b : integer, optional
number of supercells in the second unit-cell vector direction
c : integer, optional
number of supercells in the third unit-cell vector direction
"""
if not nsc is None:
for i in range(3):
if not nsc[i] is None:
self.nsc[i] = nsc[i]
if a:
self.nsc[0] = a
if b:
self.nsc[1] = b
if c:
self.nsc[2] = c
# Correct for misplaced number of unit-cells
for i in range(3):
if self.nsc[i] == 0:
self.nsc[i] = 1
if np.sum(self.nsc % 2) != 3:
raise ValueError(
"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 it
self.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:
def ret_range(val):
i = val // 2
return range(-i, i+1)
x = ret_range(n[0])
y = ret_range(n[1])
z = ret_range(n[2])
i = 0
for iz in z:
for iy in y:
for ix in x:
if ix == 0 and iy == 0 and iz == 0:
continue
# Increment index
i += 1
# The offsets for the supercells in the
# sparsity pattern
self._sc_off[i, 0] = ix
self._sc_off[i, 1] = iy
self._sc_off[i, 2] = iz
self._update_isc_off()
def _update_isc_off(self):
""" Internal routine for updating the supercell indices """
for i in range(self.n_s):
d = self.sc_off[i, :]
self._isc_off[d[0], d[1], d[2]] = i
@property
def sc_off(self):
""" Integer supercell offsets """
return self._sc_off
@sc_off.setter
def sc_off(self, sc_off):
""" Set the supercell offset """
self._sc_off[:, :] = _a.arrayi(sc_off, order='C')
self._update_isc_off()
@property
def isc_off(self):
""" Internal indexed supercell ``[ia, ib, ic] == i`` """
return self._isc_off
def __iter__(self):
""" Iterate the supercells and the indices of the supercells """
yield from enumerate(self.sc_off)
[docs] def copy(self, cell=None, origin=None):
""" A deepcopy of the object
Parameters
----------
cell : array_like
the new cell parameters
origin : array_like
the new origin
"""
if origin is None:
origin = self.origin.copy()
if cell is None:
copy = self.__class__(np.copy(self.cell), nsc=np.copy(self.nsc), origin=origin)
else:
copy = self.__class__(np.copy(cell), nsc=np.copy(self.nsc), origin=origin)
# Ensure that the correct super-cell information gets carried through
if not np.allclose(copy.sc_off, self.sc_off):
copy.sc_off = self.sc_off
return copy
[docs] def fit(self, xyz, axis=None, tol=0.05):
""" 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.
axis : None or array_like
if ``None`` equivalent to ``[0, 1, 2]``, else only the cell-vectors
along the provided axis will be used
tol : float
tolerance (in Angstrom) of the positions. I.e. we neglect coordinates
which are not within the radius of this magnitude
"""
# In case the passed coordinates are from a Geometry
from .geometry import Geometry
if isinstance(xyz, Geometry):
xyz = xyz.xyz[:, :]
cell = np.copy(self.cell[:, :])
# Get fractional coordinates to get the divisions in the current cell
x = dot(xyz, self.icell.T)
# Now we should figure out the correct repetitions
# by rounding to integer positions of the cell vectors
ix = 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 <= tol).nonzero()[0]
if len(idx) == 0:
raise ValueError('Could not fit the cell parameters to the coordinates '
'due to insufficient accuracy (try increase the tolerance)')
# Reduce problem to allowed values below the tolerance
ix = ix[idx, :]
# Reduce to total repetitions
ireps = np.amax(ix, axis=0) - np.amin(ix, axis=0) + 1
# Only repeat the axis requested
if isinstance(axis, Integral):
axis = [axis]
# Reduce the non-set axis
if not axis is None:
for ax in (0, 1, 2):
if ax not in axis:
ireps[ax] = 1
# Enlarge the cell vectors
cell[0, :] *= ireps[0]
cell[1, :] *= ireps[1]
cell[2, :] *= ireps[2]
return self.copy(cell)
[docs] def swapaxes(self, a, b):
""" Swap axis `a` and `b` in a new `SuperCell`
If ``swapaxes(0,1)`` it returns the 0 in the 1 values.
"""
# Create index vector
idx = _a.arrayi([0, 1, 2])
idx[b] = a
idx[a] = b
# There _can_ be errors when sc_off isn't created by sisl
return self.__class__(np.copy(self.cell[idx, :], order='C'),
nsc=self.nsc[idx],
origin=np.copy(self.origin[idx], order='C'))
[docs] def plane(self, ax1, ax2, origin=True):
""" Query point and plane-normal for the plane spanning `ax1` and `ax2`
Parameters
----------
ax1 : int
the first axis vector
ax2 : int
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:
>>> sc = SuperCell(4)
>>> n1, p1 = sc.plane(0, 1, True)
>>> n2, p2 = sc.plane(0, 1, False)
>>> n3, p3 = sc.plane(0, 2, True)
>>> n4, p4 = sc.plane(0, 2, False)
>>> n5, p5 = sc.plane(1, 2, True)
>>> n6, p6 = sc.plane(1, 2, False)
However, for performance critical calculations it may be advantageous to
do this:
>>> sc = SuperCell(4)
>>> uc = sc.cell.sum(0)
>>> n1, p1 = sc.plane(0, 1)
>>> n2 = -n1
>>> p2 = p1 + uc
>>> n3, p3 = sc.plane(0, 2)
>>> n4 = -n3
>>> p4 = p3 + uc
>>> n5, p5 = sc.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.
"""
cell = self.cell
n = cross3(cell[ax1, :], cell[ax2, :])
# Normalize
n /= dot3(n, n) ** 0.5
# Now we need to figure out if the normal vector
# is pointing outwards
# Take the cell center
up = 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 180
if dot3(n, up / 2) > 0.:
n *= -1
if origin:
return n, _a.zerosd([3])
# We have to reverse the normal vector
return -n, up
def __mul__(self, m):
""" 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
-------
SuperCell
enlarged supercell
"""
# Simple form
if isinstance(m, Integral):
return self.tile(m, 0).tile(m, 1).tile(m, 2)
sc = self.copy()
for i, r in enumerate(m):
sc = sc.tile(r, i)
return sc
@property
def icell(self):
""" Returns the reciprocal (inverse) cell for the `SuperCell`.
Note: The returned vectors are still in ``[0, :]`` format
and not as returned by an inverse LAPACK algorithm.
"""
return cell_invert(self.cell)
@property
def rcell(self):
""" Returns the reciprocal cell for the `SuperCell` with ``2*np.pi``
Note: The returned vectors are still in [0, :] format
and not as returned by an inverse LAPACK algorithm.
"""
return cell_reciprocal(self.cell)
[docs] def cell_length(self, length):
""" 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 3
Returns
-------
numpy.ndarray
cell-vectors with prescribed length
"""
length = _a.asarrayd(length)
if length.size == 1:
length = np.tile(length, 3)
if length.size != 3:
raise ValueError(self.__class__.__name__ + '.cell_length length parameter should be a single '
'float, or an array of 3 values.')
return self.cell * (length.ravel() / self.length).reshape(3, 1)
[docs] def rotate(self, angle, v, only='abc', rad=False):
""" Rotates the supercell, in-place by the angle around the vector
One can control which cell vectors are rotated by designating them
individually with ``only='[abc]'``.
Parameters
----------
angle : float
the angle of which the geometry should be rotated
v : array_like
the vector around the rotation is going to happen
``v = [1,0,0]`` will rotate in the ``yz`` plane
rad : bool, optional
Whether the angle is in radians (True) or in degrees (False)
only : ('abc'), str, optional
only rotate the designated cell vectors.
"""
# flatten => copy
vn = _a.asarrayd(v).flatten()
vn /= fnorm(vn)
q = Quaternion(angle, vn, rad=rad)
q /= q.norm() # normalize the quaternion
cell = np.copy(self.cell)
if 'a' in only:
cell[0, :] = q.rotate(self.cell[0, :])
if 'b' in only:
cell[1, :] = q.rotate(self.cell[1, :])
if 'c' in only:
cell[2, :] = q.rotate(self.cell[2, :])
return self.copy(cell)
[docs] def offset(self, isc=None):
""" Returns the supercell offset of the supercell index """
if isc is None:
return _a.arrayd([0, 0, 0])
return dot(isc, self.cell)
[docs] def add(self, other):
""" Add two supercell lattice vectors to each other
Parameters
----------
other : SuperCell, array_like
the lattice vectors of the other supercell to add
"""
if not isinstance(other, SuperCell):
other = SuperCell(other)
cell = self.cell + other.cell
origin = self.origin + other.origin
nsc = np.where(self.nsc > other.nsc, self.nsc, other.nsc)
return self.__class__(cell, nsc=nsc, origin=origin)
def __add__(self, other):
return self.add(other)
__radd__ = __add__
[docs] def add_vacuum(self, vacuum, axis):
""" Add vacuum along the `axis` lattice vector
Parameters
----------
vacuum : float
amount of vacuum added, in Ang
axis : int
the lattice vector to add vacuum along
"""
cell = np.copy(self.cell)
d = cell[axis, :].copy()
# normalize to get direction vector
cell[axis, :] += d * (vacuum / fnorm(d))
return self.copy(cell)
[docs] def sc_index(self, sc_off):
""" 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):
if np.any(np.abs(v) > m):
raise ValueError("Requesting a non-existing supercell index")
hsc = self.nsc // 2
if len(sc_off) == 0:
return _a.arrayi([[]])
elif isinstance(sc_off[0], np.ndarray):
_assert(hsc[0], sc_off[:, 0])
_assert(hsc[1], sc_off[:, 1])
_assert(hsc[2], sc_off[:, 2])
return self._isc_off[sc_off[:, 0], sc_off[:, 1], sc_off[:, 2]]
elif isinstance(sc_off[0], (tuple, list)):
# We are dealing with a list of lists
sc_off = np.asarray(sc_off)
_assert(hsc[0], sc_off[:, 0])
_assert(hsc[1], sc_off[:, 1])
_assert(hsc[2], sc_off[:, 2])
return self._isc_off[sc_off[:, 0], sc_off[:, 1], sc_off[:, 2]]
# Fall back to the other routines
sc_off = self._fill_sc(sc_off)
if sc_off[0] is not None and sc_off[1] is not None and sc_off[2] is not None:
_assert(hsc[0], sc_off[0])
_assert(hsc[1], sc_off[1])
_assert(hsc[2], sc_off[2])
return self._isc_off[sc_off[0], sc_off[1], sc_off[2]]
# We build it because there are 'none'
if sc_off[0] is None:
idx = _a.arangei(self.n_s)
else:
idx = (self.sc_off[:, 0] == sc_off[0]).nonzero()[0]
if not sc_off[1] is None:
idx = idx[(self.sc_off[idx, 1] == sc_off[1]).nonzero()[0]]
if not sc_off[2] is None:
idx = idx[(self.sc_off[idx, 2] == sc_off[2]).nonzero()[0]]
return idx
[docs] def vertices(self):
"""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] = 1
verts[:, 1, :, 1] = 1
verts[:, :, 1, 2] = 1
return verts @ self.cell
[docs] def scale(self, scale, what="abc"):
""" Scale lattice vectors
Does not scale `origin`.
Parameters
----------
scale : float or (3,)
the scale factor for the new lattice vectors.
what: {"abc", "xyz"}
If three different scale factors are provided, whether each scaling factor
is to be applied on the corresponding lattice vector ("abc") or on the
corresponding cartesian coordinate ("xyz").
"""
if what == "abc":
return self.copy((self.cell.T * scale).T)
if what == "xyz":
return self.copy(self.cell * scale)
raise ValueError(f"{self.__class__.__name__}.scale argument what='{what}' is not in ['abc', 'xyz'].")
[docs] def tile(self, reps, axis):
""" Extend the unit-cell `reps` times along the `axis` lattice vector
Notes
-----
This is *exactly* equivalent to the `repeat` routine.
Parameters
----------
reps : int
number of times the unit-cell is repeated along the specified lattice vector
axis : int
the lattice vector along which the repetition is performed
"""
cell = np.copy(self.cell)
nsc = np.copy(self.nsc)
origin = np.copy(self.origin)
cell[axis, :] *= reps
# Only reduce the size if it is larger than 5
if nsc[axis] > 3 and reps > 1:
# This is number of connections for the primary cell
h_nsc = nsc[axis] // 2
# The new number of supercells will then be
nsc[axis] = max(1, int(math.ceil(h_nsc / reps))) * 2 + 1
return self.__class__(cell, nsc=nsc, origin=origin)
[docs] def repeat(self, reps, axis):
""" Extend the unit-cell `reps` times along the `axis` lattice vector
Notes
-----
This is *exactly* equivalent to the `tile` routine.
Parameters
----------
reps : int
number of times the unit-cell is repeated along the specified lattice vector
axis : int
the lattice vector along which the repetition is performed
"""
return self.tile(reps, axis)
[docs] def untile(self, reps, axis):
"""Reverses a `SuperCell.tile` and returns the segmented version
See Also
--------
tile : opposite of this method
"""
cell = np.copy(self.cell)
cell[axis, :] /= reps
return self.copy(cell)
unrepeat = untile
cut = deprecate_method("*.cut is deprecated, use .untile instead", "0.13")(untile)
[docs] def append(self, other, axis):
""" Appends other `SuperCell` to this grid along axis """
cell = np.copy(self.cell)
cell[axis, :] += other.cell[axis, :]
return self.copy(cell)
[docs] def prepend(self, other, axis):
""" Prepends other `SuperCell` to this grid along axis
For a `SuperCell` object this is equivalent to `append`.
"""
return self.append(other, axis)
[docs] def move(self, v):
""" Appends additional space to the object """
# check which cell vector resembles v the most,
# use that
cell = np.copy(self.cell)
p = np.empty([3], np.float64)
cl = fnorm(cell)
for i in range(3):
p[i] = abs(np.sum(cell[i, :] * v)) / cl[i]
cell[np.argmax(p), :] += v
return self.copy(cell)
translate = move
[docs] def center(self, axis=None):
""" Returns center of the `SuperCell`, possibly with respect to an axis """
if axis is None:
return self.cell.sum(0) * 0.5
return self.cell[axis, :] * 0.5
[docs] @classmethod
def tocell(cls, *args):
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 give 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 = SuperCell.tocell(1.)
>>> cell_1_2_3 = SuperCell.tocell(1., 2., 3.)
>>> cell_1_2_3 = SuperCell.tocell([1., 2., 3.]) # same as above
"""
# Convert into true array (flattened)
args = _a.asarrayd(args).ravel()
nargs = len(args)
# A square-box
if nargs == 1:
return np.diag([args[0]] * 3)
# Diagonal components
if nargs == 3:
return np.diag(args)
# Cell parameters
if nargs == 6:
cell = _a.zerosd([3, 3])
a = args[0]
b = args[1]
c = args[2]
alpha = args[3]
beta = args[4]
gamma = args[5]
from math import sqrt, cos, sin, pi
pi180 = pi / 180.
cell[0, 0] = a
g = gamma * pi180
cg = cos(g)
sg = sin(g)
cell[1, 0] = b * cg
cell[1, 1] = b * sg
b = beta * pi180
cb = cos(b)
sb = sin(b)
cell[2, 0] = c * cb
a = alpha * pi180
d = (cos(a) - cb * cg) / sg
cell[2, 1] = c * d
cell[2, 2] = c * sqrt(sb ** 2 - d ** 2)
return cell
# A complete cell
if nargs == 9:
return args.copy().reshape(3, 3)
raise ValueError(
"Creating a unit-cell has to have 1, 3 or 6 arguments, please correct.")
[docs] def is_orthogonal(self, tol=0.001):
"""
Returns true if the cell vectors are orthogonal.
Parameters
-----------
tol: float, optional
the threshold above which the scalar product of two cell vectors will be considered non-zero.
"""
# Convert to unit-vector cell
cell = np.copy(self.cell)
cl = 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, :]) < tol
i_s = dot3(cell[0, :], cell[2, :]) < tol and i_s
i_s = dot3(cell[1, :], cell[2, :]) < tol and i_s
return i_s
[docs] def is_cartesian(self, tol=0.001):
"""
Checks if cell vectors a,b,c are multiples of the cartesian axis vectors (x, y, z)
Parameters
-----------
tol: float, optional
the threshold above which an off diagonal term will be considered non-zero.
"""
# Get the off diagonal terms of the cell
off_diagonal = self.cell.ravel()[:-1].reshape(2, 4)[:, 1:]
# Check if any of them are above the threshold tolerance
return ~np.any(np.abs(off_diagonal) > tol)
[docs] def parallel(self, other, axis=(0, 1, 2)):
""" Returns true if the cell vectors are parallel to `other`
Parameters
----------
other : SuperCell
the other object to check whether the axis are parallel
axis : int or array_like
only check the specified axis (default to all)
"""
axis = _a.asarrayi(axis).ravel()
# Convert to unit-vector cell
for i in axis:
a = self.cell[i, :] / fnorm(self.cell[i, :])
b = other.cell[i, :] / fnorm(other.cell[i, :])
if abs(dot3(a, b) - 1) > 0.001:
return False
return True
[docs] def angle(self, i, j, rad=False):
""" The angle between two of the cell vectors
Parameters
----------
i : int
the first cell vector
j : int
the second cell vector
rad : bool, optional
whether the returned value is in radians
"""
n = fnorm(self.cell[[i, j], :])
ang = math.acos(dot3(self.cell[i, :], self.cell[j, :]) / (n[0] * n[1]))
if rad:
return ang
return math.degrees(ang)
[docs] @staticmethod
def read(sile, *args, **kwargs):
""" Reads the supercell from the `Sile` using ``Sile.read_supercell``
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 previously
from sisl.io import get_sile, BaseSile
if isinstance(sile, BaseSile):
return sile.read_supercell(*args, **kwargs)
else:
with get_sile(sile) as fh:
return fh.read_supercell(*args, **kwargs)
[docs] def equal(self, other, tol=1e-4):
""" Check whether two supercell are equivalent
Parameters
----------
tol : float, optional
tolerance value for the cell vectors and origin
"""
if not isinstance(other, (SuperCell, SuperCellChild)):
return False
for tol in [1e-2, 1e-3, 1e-4]:
same = np.allclose(self.cell, other.cell, atol=tol)
same = same and np.allclose(self.nsc, other.nsc)
same = same and np.allclose(self.origin, other.origin, atol=tol)
return same
def __str__(self):
""" Returns a string representation of the object """
# Create format for lattice vectors
s = ',\n '.join(['ABC'[i] + '=[{:.3f}, {:.3f}, {:.3f}]'.format(*self.cell[i]) for i in (0, 1, 2)])
return self.__class__.__name__ + ('{{nsc: [{:} {:} {:}],\n ' + s + ',\n}}').format(*self.nsc)
def __repr__(self):
a, b, c, alpha, beta, gamma = map(lambda r: round(r, 3), self.parameters())
return f"<{self.__module__}.{self.__class__.__name__} a={a}, b={b}, c={c}, α={alpha}, β={beta}, γ={gamma}, nsc={self.nsc}>"
def __eq__(self, other):
""" Equality check """
return self.equal(other)
def __ne__(self, b):
""" In-equality check """
return not (self == b)
# Create pickling routines
def __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']
def __plot__(self, axis=None, axes=False, *args, **kwargs):
""" Plot the supercell in a specified ``matplotlib.Axes`` object.
Parameters
----------
axis : array_like, optional
only plot a subset of the axis, defaults to all axis
axes : bool or matplotlib.Axes, optional
the figure axes to plot in (if ``matplotlib.Axes`` object).
If ``True`` it will create a new figure to plot in.
If ``False`` it will try and grap the current figure and the current axes.
"""
# Default dictionary for passing to newly created figures
d = dict()
# Try and default the color and alpha
if 'color' not in kwargs and len(args) == 0:
kwargs['color'] = 'k'
if 'alpha' not in kwargs:
kwargs['alpha'] = 0.5
if axis is None:
axis = [0, 1, 2]
# Ensure we have a new 3D Axes3D
if len(axis) == 3:
d['projection'] = '3d'
axes = plt.get_axes(axes, **d)
# Create vector objects
o = self.origin
v = []
for a in axis:
v.append(np.vstack((o[axis], o[axis] + self.cell[a, axis])))
v = np.array(v)
if axes.__class__.__name__.startswith('Axes3D'):
# We should plot in 3D plots
for vv in v:
axes.plot(vv[:, 0], vv[:, 1], vv[:, 2], *args, **kwargs)
v0, v1 = v[0], v[1] - o
axes.plot(v0[1, 0] + v1[:, 0], v0[1, 1] + v1[:, 1], v0[1, 2] + v1[:, 2], *args, **kwargs)
axes.set_zlabel('Ang')
else:
for vv in v:
axes.plot(vv[:, 0], vv[:, 1], *args, **kwargs)
v0, v1 = v[0], v[1] - o[axis]
axes.plot(v0[1, 0] + v1[:, 0], v0[1, 1] + v1[:, 1], *args, **kwargs)
axes.plot(v1[1, 0] + v0[:, 0], v1[1, 1] + v0[:, 1], *args, **kwargs)
axes.set_xlabel('Ang')
axes.set_ylabel('Ang')
return axes
class SuperCellChild:
""" Class to be inherited by using the ``self.sc`` as a `SuperCell` object
Initialize by a `SuperCell` object and get access to several different
routines directly related to the `SuperCell` class.
"""
def set_nsc(self, *args, **kwargs):
""" Set the number of super-cells in the `SuperCell` object
See `set_nsc` for allowed parameters.
See Also
--------
SuperCell.set_nsc : the underlying called method
"""
self.sc.set_nsc(*args, **kwargs)
def set_supercell(self, sc):
""" Overwrites the local supercell """
if sc is None:
# Default supercell is a simple
# 1x1x1 unit-cell
self.sc = SuperCell([1., 1., 1.])
elif isinstance(sc, SuperCell):
self.sc = sc
elif isinstance(sc, SuperCellChild):
self.sc = sc.sc
else:
# The supercell is given as a cell
self.sc = SuperCell(sc)
# Loop over attributes in this class
# if it inherits SuperCellChild, we call
# set_sc on that too.
# Sadly, getattr fails for @property methods
# which forces us to use try ... except
with warnings.catch_warnings():
warnings.simplefilter("ignore")
for a in dir(self):
try:
if isinstance(getattr(self, a), SuperCellChild):
getattr(self, a).set_supercell(self.sc)
except Exception:
pass
set_sc = set_supercell
@property
def length(self):
""" Returns the inherent `SuperCell` objects `length` """
return self.sc.length
@property
def volume(self):
""" Returns the inherent `SuperCell` objects `volume` """
return self.sc.volume
def area(self, ax0, ax1):
""" Calculate the area spanned by the two axis `ax0` and `ax1` """
return self.sc.area(ax0, ax1)
@property
def cell(self):
""" Returns the inherent `SuperCell` objects `cell` """
return self.sc.cell
@property
def icell(self):
""" Returns the inherent `SuperCell` objects `icell` """
return self.sc.icell
@property
def rcell(self):
""" Returns the inherent `SuperCell` objects `rcell` """
return self.sc.rcell
@property
def origin(self):
""" Returns the inherent `SuperCell` objects `origin` """
return self.sc.origin
@property
def origo(self):
""" Returns the inherent `SuperCell` objects `origin` """
return self.sc.origo
@property
def n_s(self):
""" Returns the inherent `SuperCell` objects `n_s` """
return self.sc.n_s
@property
def nsc(self):
""" Returns the inherent `SuperCell` objects `nsc` """
return self.sc.nsc
@property
def sc_off(self):
""" Returns the inherent `SuperCell` objects `sc_off` """
return self.sc.sc_off
@property
def isc_off(self):
""" Returns the inherent `SuperCell` objects `isc_off` """
return self.sc.isc_off
def add_vacuum(self, vacuum, axis):
""" Add vacuum along the `axis` lattice vector
Parameters
----------
vacuum : float
amount of vacuum added, in Ang
axis : int
the lattice vector to add vacuum along
"""
copy = self.copy()
copy.set_supercell(self.sc.add_vacuum(vacuum, axis))
return copy
def _fill(self, non_filled, dtype=None):
return self.sc._fill(non_filled, dtype)
def _fill_sc(self, supercell_index):
return self.sc._fill_sc(supercell_index)
def sc_index(self, *args, **kwargs):
""" Call local `SuperCell` object `sc_index` function """
return self.sc.sc_index(*args, **kwargs)
def is_orthogonal(self):
""" Return true if all cell vectors are linearly independent"""
return self.sc.is_orthogonal()