def toSphere(self): """ Create a sphere which is surely encompassing the *full* shape """ from .ellipsoid import Sphere # Retrieve spheres A = self.A.toSphere() Ar = A.radius Ac = A.center B = self.B.toSphere() Br = B.radius Bc = B.center # Calculate the distance between the spheres dist = fnorm(Ac - Bc) # If one is fully enclosed in the other, we can simply neglect the other if dist + Ar <= Br: # A is fully enclosed in B (or they are the same) return A elif dist + Br <= Ar: # B is fully enclosed in A (or they are the same) return B elif dist <= (Ar + Br): # We can reduce the sphere drastically because only the overlapping region is important # i_r defines the intersection radius, search for Sphere-Sphere Intersection dx = (dist**2 - Br**2 + Ar**2) / (2 * dist) if dx > dist: # the intersection is placed after the radius of B # And in this case B is smaller (otherwise dx < 0) return B elif dx < 0: return A i_r = msqrt(4 * (dist * Ar)**2 - (dist**2 - Br**2 + Ar**2)**2) / (2 * dist) # Now we simply need to find the dx point along the vector Bc - Ac # Then we can easily calculate the point from A center = Bc - Ac center = Ac + center / fnorm(center) * dx A = i_r B = i_r else: # In this case there is actually no overlap. So perhaps we should # create an infinitisemal sphere such that no point will ever be found # Or we should return a new Shape which *always* return False for indices etc. center = (Ac + Bc) * 0.5 # Currently we simply use a very small sphere and put it in the middle between # the spheres # This should at least speed up comparisons A = 0.001 B = 0.001 return Sphere(max(A, B), center)
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 [3] 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. """ # flatte => copy vn = np.asarray(v, dtype=np.float64).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)
def test_sphere_and(): # For all intersections after v = np.array([1.] * 3) v /= fnorm(v) D = np.linspace(0, 5, 50) inside = np.ones(len(D), dtype=bool) A = Sphere(2.) is_first = True inside[:] = True for i, d in enumerate(D): B = Sphere(1., center=v * d) C = (A & B).toSphere() if is_first and C.radius < B.radius: is_first = False inside[i:] = False if inside[i]: assert C.radius == pytest.approx(B.radius) else: assert C.radius < B.radius A = Sphere(0.5) is_first = True inside[:] = True for i, d in enumerate(D): B = Sphere(1., center=v * d) C = (A & B).toSphere() str(A) + str(B) + str(C) if is_first and C.radius < A.radius: inside[i:] = False is_first = False if inside[i]: assert C.radius == pytest.approx(A.radius) else: assert C.radius < A.radius
def find_all_bonds(geometry, tol=0.2): """ Finds all bonds present in a geometry. Parameters ----------- geometry: sisl.Geometry the structure where the bonds should be found. tol: float the fraction that the distance between atoms is allowed to differ from the "standard" in order to be considered a bond. Return --------- np.ndarray of shape (nbonds, 2) each item of the array contains the 2 indices of the atoms that participate in the bond. """ pt = PeriodicTable() bonds = [] for at in geometry: neighs = geometry.close(at, R=[0.1, 3])[-1] for neigh in neighs[neighs > at]: summed_radius = pt.radius([ abs(geometry.atoms[at].Z), abs(geometry.atoms[neigh % geometry.na].Z) ]).sum() bond_thresh = (1 + tol) * summed_radius if bond_thresh > fnorm(geometry[neigh] - geometry[at]): bonds.append([at, neigh]) return np.array(bonds, dtype=int)
def toSphere(self): """ Create a sphere which is surely encompassing the *full* shape """ from .ellipsoid import Sphere # Retrieve spheres A = self.A.toSphere() Ar = A.radius Ac = A.center B = self.B.toSphere() Br = B.radius Bc = B.center center = (Ac + Bc) * 0.5 A = Ar + fnorm(center - Ac) B = Br + fnorm(center - Bc) return Sphere(max(A, B), center)
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
def _projected_1Dcoords(cls, geometry, xyz=None, axis="x", nsc=(1, 1, 1)): """ Moves the 3D positions of the atoms to a 2D supspace. In this way, we can plot the structure from the "point of view" that we want. NOTE: If axis is one of {"a", "b", "c", "1", "2", "3"} the function doesn't project the coordinates in the direction of the lattice vector. The fractional coordinates, taking in consideration the three lattice vectors, are returned instead. Parameters ------------ geometry: sisl.Geometry the geometry for which you want the projected coords xyz: array-like of shape (natoms, 3), optional the 3D coordinates that we want to project. otherwise they are taken from the geometry. axis: {"x", "y", "z", "a", "b", "c", "1", "2", "3"} or array-like of shape 3, optional the direction to be displayed along the X axis. nsc: array-like of shape (3, ), optional only used if `axis` is a lattice vector. It is used to rescale everything to the unit cell lattice vectors, otherwise `GeometryPlot` doesn't play well with `GridPlot`. Returns ---------- np.ndarray of shape (natoms, ) the 1D coordinates of the geometry, with all positions projected into the line defined by axis. """ if xyz is None: xyz = geometry.xyz if isinstance(axis, str) and axis in ("a", "b", "c", "0", "1", "2"): return cls._projected_2Dcoords(geometry, xyz, xaxis=axis, yaxis="a" if axis == "c" else "c", nsc=nsc)[..., 0] # Get the direction that the axis represents axis = cls._direction(axis, geometry.cell) return xyz.dot(axis / fnorm(axis)) / fnorm(axis)
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)
def is_orthogonal(self): """ Returns true if the cell vectors are orthogonal """ # 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, :]) < 0.001 i_s = dot3(cell[0, :], cell[2, :]) < 0.001 and i_s i_s = dot3(cell[1, :], cell[2, :]) < 0.001 and i_s return i_s
def _bond_length(geom, bond): """ Returns the length of a bond between two atoms. Parameters ------------ geom: Geometry the structure where the atoms are bond: array-like of two int the indices of the atoms that form the bond """ return fnorm(geom[bond[1]] - geom[bond[0]])
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)
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)
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 radius(self): """ Return the radius of the Ellipsoid """ return fnorm(self._v)
def param_circle(self, sc, N_or_dk, kR, normal, origo, loop=False): r""" Create a parameterized k-point list where the k-points are generated on a circle around an origo The generated circle is a perfect circle in the reciprocal space (Cartesian coordinates). To generate a perfect circle in units of the reciprocal lattice vectors one can generate the circle for a diagonal supercell with side-length :math:`2\pi`, see example below. Parameters ---------- sc : SuperCell, or SuperCellChild the supercell used to construct the k-points N_or_dk : int number of k-points generated using the parameterization (if an integer), otherwise it specifies the discretization length on the circle (in 1/Ang), If the latter case will use less than 4 points a warning will be raised and the number of points increased to 4. kR : float radius of the k-point. In 1/Ang normal : array_like of float normal vector to determine the circle plane origo : array_like of float origo of the circle used to generate the circular parameterization loop : bool, optional whether the first and last point are equal Examples -------- >>> sc = SuperCell([1, 1, 10, 90, 90, 60]) >>> bz = BrillouinZone.param_circle(sc, 10, 0.05, [0, 0, 1], [1./3, 2./3, 0]) To generate a circular set of k-points in reduced coordinates (reciprocal >>> sc = SuperCell([1, 1, 10, 90, 90, 60]) >>> bz = BrillouinZone.param_circle(sc, 10, 0.05, [0, 0, 1], [1./3, 2./3, 0]) >>> bz_rec = BrillouinZone.param_circle(2*np.pi, 10, 0.05, [0, 0, 1], [1./3, 2./3, 0]) >>> bz.k[:, :] = bz_rec.k[:, :] Returns ------- BrillouinZone : with the parameterized k-points. """ if isinstance(N_or_dk, Integral): N = N_or_dk else: # Calculate the required number of points N = int(kR ** 2 * np.pi / N_or_dk + 0.5) if N < 4: N = 4 info('BrillouinZone.param_circle increased the number of circle points to 4.') # Conversion object bz = BrillouinZone(sc) normal = _a.arrayd(normal) origo = _a.arrayd(origo) k_n = bz.tocartesian(normal) k_o = bz.tocartesian(origo) # Generate a preset list of k-points on the unit-circle if loop: radians = _a.aranged(N) / (N-1) * 2 * np.pi else: radians = _a.aranged(N) / N * 2 * np.pi k = _a.emptyd([N, 3]) k[:, 0] = np.cos(radians) k[:, 1] = np.sin(radians) k[:, 2] = 0. # Now generate the rotation _, theta, phi = cart2spher(k_n) if theta != 0: pv = _a.arrayd([k_n[0], k_n[1], 0]) pv /= fnorm(pv) q = Quaternion(phi, pv, rad=True) * Quaternion(theta, [0, 0, 1], rad=True) else: q = Quaternion(0., [0, 0, k_n[2] / abs(k_n[2])], rad=True) # Calculate k-points k = q.rotate(k) k *= kR / fnorm(k).reshape(-1, 1) k = bz.toreduced(k + k_o) # The sum of weights is equal to the BZ area W = np.pi * kR ** 2 w = np.repeat([W / N], N) return BrillouinZone(sc, k, w)
def initialize(self): """ Initialize the internal data-arrays used for efficient calculation of the real-space quantities This method should first be called *after* all options has been specified. If the user hasn't specified the ``bz`` value as an option this method will update the internal integration Brillouin zone based on the ``dk`` option. """ # Try and guess the directions unfold = self._unfold nsc = self.parent.nsc.copy() axes = self._options['axes'] if axes is None: if nsc[2] == 1: axes = _a.arrayi([0, 1]) elif nsc[1] == 1: axes = _a.arrayi([0, 2]) elif nsc[0] == 1: axes = _a.arrayi([1, 2]) else: raise ValueError(self.__class__.__name__ + '.initialize currently only supports a 2D real-space self-energy, hence the MonkhorstPack grid should reflect only 2 periodic directions.') self._options['axes'] = axes # Check that we have periodicity along the chosen axes nsc_sum = nsc[axes].sum() if nsc_sum == 1: raise ValueError(self.__class__.__name__ + '.initialize found no periodic directions for the chosen integration axes: {} and {}.'.format(*nsc[axes])) elif nsc_sum < 6: raise ValueError((self.__class__.__name__ + '.initialize found one periodic direction ' 'out of two for the chosen integration axes: {} and {}. ' 'For 1D systems the regular surface self-energy method is appropriate.').format(*nsc[axes])) if self._options['semi_axis'] is None and self._options['k_axis'] is None: # None of the axis has been described if nsc[axes[0]] > 3: k_ax = axes[0] s_ax = axes[1] elif nsc[axes[1]] > 3: k_ax = axes[1] s_ax = axes[0] else: # Choose the direction of k to be the smallest shortest sc = self.parent.sc.tile(unfold[axes[0]], axes[0]).tile(unfold[axes[1]], axes[1]) rcell = fnorm(sc.rcell)[axes] k_ax = axes[np.argmax(rcell)] if k_ax == axes[0]: s_ax = axes[1] else: s_ax = axes[0] self._options['semi_axis'] = s_ax self._options['k_axis'] = k_ax s_ax = 'ABC'[s_ax] k_ax = 'ABC'[k_ax] info(self.__class__.__name__ + '.initialize determined the semi-inf- and k-directions to be: {} and {}'.format(s_ax, k_ax)) elif self._options['k_axis'] is None: s_ax = self._options['semi_axis'] if s_ax == axes[0]: k_ax = axes[1] else: k_ax = axes[0] self._options['k_axis'] = k_ax k_ax = 'ABC'[k_ax] info(self.__class__.__name__ + '.initialize determined the k direction to be: {}'.format(k_ax)) elif self._options['semi_axis'] is None: k_ax = self._options['k_axis'] if k_ax == axes[0]: s_ax = axes[1] else: s_ax = axes[0] self._options['semi_axis'] = s_ax s_ax = 'ABC'[s_ax] info(self.__class__.__name__ + '.initialize determined the semi-infinite direction to be: {}'.format(s_ax)) k_ax = self._options['k_axis'] s_ax = self._options['semi_axis'] if nsc[s_ax] != 3: raise ValueError(self.__class__.__name__ + '.initialize found the self-energy direction to be ' 'incompatible with the parent object. It *must* have 3 supercells along the ' 'semi-infinite direction.') P0 = self.real_space_parent() V_atoms = self.real_space_coupling(True)[1] self._calc = { # The below algorithm requires the direction to be negative # if changed, B, C should be reversed below 'SE': RecursiveSI(self.parent, '-' + 'ABC'[s_ax], eta=self._options['eta']), # Used to calculate the real-space self-energy 'P0': P0.Pk(), # in sparse format 'S0': P0.Sk(), # in sparse format # Orbitals in the coupling atoms 'orbs': P0.a2o(V_atoms, True).reshape(-1, 1), } # Update the BrillouinZone integration grid in case it isn't specified if self._options['bz'] is None: # Update the integration grid # Note this integration grid is based on the big system. sc = self.parent.sc.tile(unfold[axes[0]], axes[0]).tile(unfold[axes[1]], axes[1]) rcell = fnorm(sc.rcell)[k_ax] nk = [1] * 3 nk[k_ax] = int(self._options['dk'] * rcell) self._options['bz'] = MonkhorstPack(sc, nk, trs=self._options['trs']) info(self.__class__.__name__ + '.initialize determined the number of k-points: {}'.format(nk[k_ax]))
def edge_length(self): """ The lengths of each of the vector that defines the cuboid """ return fnorm(self._v)
def length(self): """ Length of each lattice vector """ return fnorm(self.cell)