def unfold_points(self, k): r""" Return a list of k-points to be evaluated for this objects unfolding The k-point `k` is with respect to the unfolded geometry. The return list of `k` points are the k-points required to be sampled in the folded geometry. Parameters ---------- k : (3,) of float k-point evaluation corresponding to the unfolded unit-cell Returns ------- k_unfold a list of ``np.prod(self.bloch)`` k-points used for the unfolding """ k = _a.arrayd(k) # Create expansion points B = self._bloch unfold = _a.emptyd([B[2], B[1], B[0], 3]) # Use B-casting rules (much simpler) unfold[:, :, :, 0] = (aranged(B[0]).reshape(1, 1, -1) + k[0]) / B[0] unfold[:, :, :, 1] = (aranged(B[1]).reshape(1, -1, 1) + k[1]) / B[1] unfold[:, :, :, 2] = (aranged(B[2]).reshape(-1, 1, 1) + k[2]) / B[2] # Back-transform shape return unfold.reshape(-1, 3)
def test_oplist_math(op, key1, key2): d = { 1: oplist([ar.aranged(1, 10), ar.aranged(1, 10)]), 2: 2, } l1 = d[key1] l2 = d[key2] op(l1, l2)
def test_oplist_imath(op, key): d = { 1: oplist([ar.aranged(1, 10), ar.aranged(1, 10)]), 2: 2, } l2 = d[key] try: l1 = oplist([data * 2 for data in l2]) except: l1 = oplist([ar.aranged(1, 10), ar.aranged(2, 3)]) op(l1, l2)
def grid(n, displ=0., size=1., trs=False): r""" Create a grid of `n` points with an offset of `displ` and sampling `size` around `displ` The :math:`k`-points are :math:`\Gamma` centered. Parameters ---------- n : int number of points in the grid. If `trs` is ``True`` this may be smaller than `n` displ : float, optional the displacement of the grid size : float, optional the total size of the Brillouin zone to sample trs : bool, optional whether time-reversal-symmetry is applied Returns ------- k : np.ndarray the list of k-points in the Brillouin zone to be sampled w : np.ndarray weights for the k-points """ # First ensure that displ is in the Brillouin displ = displ % 1. if displ > 0.5: displ -= 1. if trs and displ == 0.: n_half = n // 2 if n % 2 == 1: # Odd case, we have Gamma and remove all negative values k = _a.aranged(n_half + 1) * size / n + displ # Weights are all twice (except Gamma) w = _a.onesd(len(k)) / n * size w[1:] *= 2 else: # Even case, we do not have Gamma, but we shift to Gamma # All points except Gamma and edge have weights doubled k = _a.aranged(n_half + 1) * size / n + displ # Weights are all twice (except Gamma and band-edge) w = _a.onesd(len(k)) / n * size w[1:-1] *= 2 else: # Not TRS if n % 2 == 0: k = (_a.aranged(n) * 2 - n) * size / (2 * n) + displ else: k = (_a.aranged(n) * 2 - n + 1) * size / (2 * n) + displ w = _a.onesd(n) * size / n # Return values return k, w
def read_basis(self): """ Returns data associated with the ion.xml file """ # Get the element-tree ET = ElementTree(None, self.file) root = ET.getroot() # Get number of orbitals label = root.find('label').text.strip() Z = int(root.find('z').text) # atomic number mass = float(root.find('mass').text) # Read in the PAO's paos = root.find('paos') # Now loop over all orbitals orbital = [] # All orbital data Bohr2Ang = unit_convert('Bohr', 'Ang') for orb in paos: n = int(orb.get('n')) l = int(orb.get('l')) z = int(orb.get('z')) # zeta q0 = float(orb.get('population')) P = not int(orb.get('ispol')) == 0 # Radial components rad = orb.find('radfunc') npts = int(rad.find('npts').text) # Grid spacing in Bohr (conversion is done later # because the normalization is easier) delta = float(rad.find('delta').text) # Read in data to a list dat = list(map(float, rad.find('data').text.split())) # Since the readed data has fewer significant digits we # might as well re-create the table of the radial component. r = aranged(npts) * delta # To get it per Ang**3 # TODO, check that this is correct. # The fact that we have to have it normalized means that we need # to convert psi /sqrt(Bohr**3) -> /sqrt(Ang**3) # \int psi^\dagger psi == 1 psi = arrayd(dat[1::2]) * r**l / Bohr2Ang**(3. / 2.) # Create the sphericalorbital and then the atomicorbital sorb = SphericalOrbital(l, (r * Bohr2Ang, psi), q0) # This will be -l:l (this is the way siesta does it) orbital.extend(sorb.toAtomicOrbital(n=n, Z=z, P=P)) # Now create the atom and return return Atom(Z, orbital, mass=mass, tag=label)
def _index_shape_cuboid(self, cuboid): """ Internal routine for cuboid shape-indices """ # Construct all points on the outer rim of the cuboids min_d = fnorm(self.dcell).min() # Retrieve cuboids edge-lengths v = cuboid.edge_length # Create normalized cuboid vectors (because we expan via the lengths below vn = cuboid._v / fnorm(cuboid._v).reshape(-1, 1) LL = (cuboid.center - cuboid._v.sum(0) / 2).reshape(1, 3) UR = (cuboid.center + cuboid._v.sum(0) / 2).reshape(1, 3) # Create coordinates a = vn[0, :].reshape(1, -1) * _a.aranged(0, v[0] + min_d, min_d).reshape(-1, 1) b = vn[1, :].reshape(1, -1) * _a.aranged(0, v[1] + min_d, min_d).reshape(-1, 1) c = vn[2, :].reshape(1, -1) * _a.aranged(0, v[2] + min_d, min_d).reshape(-1, 1) # Now create all sides sa = a.shape[0] sb = b.shape[0] sc = c.shape[0] def plane(v1, v2): return (v1.reshape(-1, 1, 3) + v2.reshape(1, -1, 3)).reshape(1, -1, 3) # Allocate for the 6 faces of the cuboid rxyz = _a.emptyd([2, sa * sb + sa * sc + sb * sc, 3]) # Define the LL and UR rxyz[0, :, :] = LL rxyz[1, :, :] = UR i = 0 rxyz[:, i:i + sa * sb, :] += plane(a, b) i += sa * sb rxyz[:, i:i + sa * sc, :] += plane(a, c) i += sa * sc rxyz[:, i:i + sb * sc, :] += plane(b, c) del a, b, c, sa, sb, sc rxyz.shape = (-1, 3) # Get all indices of the cuboid planes return self.index(rxyz)
def _index_shape(self, shape): """ Internal routine for shape-indices """ # First grab the sphere, subsequent indices will be reduced # by the actual shape cuboid = shape.toCuboid() ellipsoid = shape.toEllipsoid() if ellipsoid.volume() > cuboid.volume(): idx = self._index_shape_cuboid(cuboid) else: idx = self._index_shape_ellipsoid(ellipsoid) # Get min/max imin = idx.min(0) imax = idx.max(0) del idx dc = self.dcell # Now to find the actual points inside the shape # First create all points in the square and then retrieve all indices # within. ix = _a.aranged(imin[0], imax[0] + 0.5) iy = _a.aranged(imin[1], imax[1] + 0.5) iz = _a.aranged(imin[2], imax[2] + 0.5) output_shape = (ix.size, iy.size, iz.size, 3) rxyz = _a.emptyd(output_shape) ao = add.outer ao(ao(ix * dc[0, 0], iy * dc[1, 0]), iz * dc[2, 0], out=rxyz[:, :, :, 0]) ao(ao(ix * dc[0, 1], iy * dc[1, 1]), iz * dc[2, 1], out=rxyz[:, :, :, 1]) ao(ao(ix * dc[0, 2], iy * dc[1, 2]), iz * dc[2, 2], out=rxyz[:, :, :, 2]) idx = shape.within_index(rxyz.reshape(-1, 3)) del rxyz i = _a.emptyi(output_shape) i[:, :, :, 0] = ix.reshape(-1, 1, 1) i[:, :, :, 1] = iy.reshape(1, -1, 1) i[:, :, :, 2] = iz.reshape(1, 1, -1) del ix, iy, iz i.shape = (-1, 3) i = take(i, idx, axis=0) del idx return i
def read_basis(self): """ Returns data associated with the ion.xml file """ no = len(self._dimension('norbs')) # Get number of orbitals label = self.Label.strip() Z = int(self.Atomic_number) mass = float(self.Mass) # Retrieve values orb_l = self._variable('orbnl_l')[:] # angular quantum number orb_n = self._variable('orbnl_n')[:] # principal quantum number orb_z = self._variable('orbnl_z')[:] # zeta orb_P = self._variable( 'orbnl_ispol')[:] > 0 # polarization shell, or not orb_q0 = self._variable('orbnl_pop')[:] # q0 for the orbitals orb_delta = self._variable('delta')[:] # delta for the functions orb_psi = self._variable('orb')[:, :] # Now loop over all orbitals orbital = [] # All orbital data Bohr2Ang = unit_convert('Bohr', 'Ang') for io in range(no): n = orb_n[io] l = orb_l[io] z = orb_z[io] P = orb_P[io] # Grid spacing in Bohr (conversion is done later # because the normalization is easier) delta = orb_delta[io] # Since the readed data has fewer significant digits we # might as well re-create the table of the radial component. r = aranged(orb_psi.shape[1]) * delta # To get it per Ang**3 # TODO, check that this is correct. # The fact that we have to have it normalized means that we need # to convert psi /sqrt(Bohr**3) -> /sqrt(Ang**3) # \int psi^\dagger psi == 1 psi = orb_psi[io, :] * r**l / Bohr2Ang**(3. / 2.) # Create the sphericalorbital and then the atomicorbital sorb = SphericalOrbital(l, (r * Bohr2Ang, psi), orb_q0[io]) # This will be -l:l (this is the way siesta does it) orbital.extend(sorb.toAtomicOrbital(n=n, Z=z, P=P)) # Now create the atom and return return Atom(Z, orbital, mass=mass, tag=label)
def read_basis(self): """ Returns a set of atoms corresponding to the basis-sets in the nc file """ if 'BASIS' not in self.groups: return None basis = self.groups['BASIS'] atom = [None] * len(basis.groups) for a_str in basis.groups: a = basis.groups[a_str] if 'orbnl_l' not in a.variables: # Do the easy thing. # Get number of orbitals label = a.Label.strip() Z = int(a.Atomic_number) mass = float(a.Mass) i = int(a.ID) - 1 atom[i] = Atom(Z, [-1] * a.Number_of_orbitals, mass=mass, tag=label) continue # Retrieve values orb_l = a.variables['orbnl_l'][:] # angular quantum number orb_n = a.variables['orbnl_n'][:] # principal quantum number orb_z = a.variables['orbnl_z'][:] # zeta orb_P = a.variables[ 'orbnl_ispol'][:] > 0 # polarization shell, or not orb_q0 = a.variables['orbnl_pop'][:] # q0 for the orbitals orb_delta = a.variables['delta'][:] # delta for the functions orb_psi = a.variables['orb'][:, :] # Now loop over all orbitals orbital = [] # Number of basis-orbitals (before m-expansion) no = len(a.dimensions['norbs']) # All orbital data for io in range(no): n = orb_n[io] l = orb_l[io] z = orb_z[io] P = orb_P[io] # Grid spacing in Bohr (conversion is done later # because the normalization is easier) delta = orb_delta[io] # Since the readed data has fewer significant digits we # might as well re-create the table of the radial component. r = aranged(orb_psi.shape[1]) * delta # To get it per Ang**3 # TODO, check that this is correct. # The fact that we have to have it normalized means that we need # to convert psi /sqrt(Bohr**3) -> /sqrt(Ang**3) # \int psi^\dagger psi == 1 psi = orb_psi[io, :] * r**l / Bohr2Ang**(3. / 2.) # Create the sphericalorbital and then the atomicorbital sorb = SphericalOrbital(l, (r * Bohr2Ang, psi), orb_q0[io]) # This will be -l:l (this is the way siesta does it) orbital.extend(sorb.toAtomicOrbital(n=n, Z=z, P=P)) # Get number of orbitals label = a.Label.strip() Z = int(a.Atomic_number) mass = float(a.Mass) i = int(a.ID) - 1 atom[i] = Atom(Z, orbital, mass=mass, tag=label) return atom
def tile(self, reps, axis, normalize=False, offset=0): r"""Tile the state vectors for a new supercell Tiling a state vector makes use of the Bloch factors for a state by utilizing .. math:: \psi_{\mathbf k}(\mathbf r + \mathbf T) \propto e^{i\mathbf k\cdot \mathbf T} where :math:`\mathbf T = i\mathbf a_0 + j\mathbf a_1 + l\mathbf a_2`. Note that `axis` selects which of the :math:`\mathbf a_i` vectors that are translated and `reps` corresponds to the :math:`i`, :math:`j` and :math:`l` variables. The `offset` moves the individual states by said amount, i.e. :math:`i\to i+\mathrm{offset}`. Parameters ---------- reps : int number of repetitions along a specific lattice vector axis : int lattice vector to tile along normalize: bool, optional whether the states are normalized upon return, may be useful for eigenstates offset: float, optional the offset for the phase factors See Also -------- Geometry.tile """ # the parent gets tiled parent = self.parent.tile(reps, axis) # the k-point gets reduced k = _a.asarrayd(self.info.get("k", [0] * 3)) # now tile the state vectors state = np.tile(self.state, (1, reps)).astype(np.complex128, copy=False) # re-shape to apply phase-factors state.shape = (len(self), reps, -1) # Tiling stuff is trivial since we simply # translate the bloch coefficients with: # exp(i k.T) # with T being # i * a_0 + j * a_1 + k * a_2 # We can leave out the lattice vectors entirely phase = exp(2j * _pi * k[axis] * (_a.aranged(reps) + offset)) state *= phase.reshape(1, -1, 1) state.shape = (len(self), -1) # update new k; when we double the system, we halve the periodicity # and hence we need to account for this k[axis] = (k[axis] * reps % 1) while k[axis] > 0.5: k[axis] -= 1 while k[axis] <= -0.5: k[axis] += 1 # this allows us to make the same usable for StateC classes s = self.copy() s.parent = parent s.state = state # update the k-point s.info = dict(**self.info) s.info.update({'k': k}) if normalize: return s.normalize() return s
def unfold(self, M, k_unfold): r""" Unfold the matrix list of matrices `M` into a corresponding k-point (unfolding k-points are `k_unfold`) Parameters ---------- M : list of numpy arrays matrices used for unfolding k_unfold : (*, 3) of float unfolding k-points as returned by `Bloch.unfold_points` Returns ------- M_unfold : unfolded matrix of size ``M[0].shape * k_unfold.shape[0] ** 2`` """ Bi, Bj, Bk = self.bloch # Retrieve shapes M0, M1 = M[0].shape shape = (Bk, Bj, Bi, M0, Bk, Bj, Bi, M1) Mshape = (M0, 1, 1, 1, M1) # Allocate the unfolded matrix Mu = zeros(shape, dtype=dtype_real_to_complex(M[0].dtype)) # Use B-casting rules (much simpler) # Perform unfolding N = len(self) w = 1 / N if Bi == 1: if Bj == 1: K = aranged(Bk).reshape(1, Bk, 1, 1, 1) for T in range(N): m = M[T].reshape(Mshape) * w kjpi = 2j * pi * k_unfold[T, 2] for k in range(Bk): add(Mu[k, 0, 0], m * exp(kjpi * (K - k)), out=Mu[k, 0, 0]) elif Bk == 1: J = aranged(Bj).reshape(1, 1, Bj, 1, 1) for T in range(N): m = M[T].reshape(Mshape) * w kjpi = 2j * pi * k_unfold[T, 1] for j in range(Bj): add(Mu[0, j, 0], m * exp(kjpi * (J - j)), out=Mu[0, j, 0]) else: J = aranged(Bj).reshape(1, 1, Bj, 1, 1) K = aranged(Bk).reshape(1, Bk, 1, 1, 1) for T in range(N): m = M[T].reshape(Mshape) * w kpi = pi * k_unfold[T, :] for k in range(Bk): Kk = (K - k) * kpi[2] for j in range(Bj): add(Mu[k, j, 0], m * exp(2j * (Kk + kpi[1] * (J - j))), out=Mu[k, j, 0]) elif Bj == 1: if Bk == 1: I = aranged(Bi).reshape(1, 1, 1, Bi, 1) for T in range(N): m = M[T].reshape(Mshape) * w kjpi = 2j * pi * k_unfold[T, 0] for i in range(Bi): add(Mu[0, 0, i], m * exp(kjpi * (I - i)), out=Mu[0, 0, i]) else: I = aranged(Bi).reshape(1, 1, 1, Bi, 1) K = aranged(Bk).reshape(1, Bk, 1, 1, 1) for T in range(N): m = M[T].reshape(Mshape) * w kpi = pi * k_unfold[T, :] for k in range(Bk): Kk = (K - k) * kpi[2] for i in range(Bi): add(Mu[k, 0, i], m * exp(2j * (Kk + kpi[0] * (I - i))), out=Mu[k, 0, i]) elif Bk == 1: I = aranged(Bi).reshape(1, 1, 1, Bi, 1) J = aranged(Bj).reshape(1, 1, Bj, 1, 1) for T in range(N): m = M[T].reshape(Mshape) * w kpi = pi * k_unfold[T, :] for j in range(Bj): Jj = (J - j) * kpi[1] for i in range(Bi): add(Mu[0, j, i], m * exp(2j * (Jj + kpi[0] * (I - i))), out=Mu[0, j, i]) else: I = aranged(Bi).reshape(1, 1, 1, Bi, 1) J = aranged(Bj).reshape(1, 1, Bj, 1, 1) K = aranged(Bk).reshape(1, Bk, 1, 1, 1) for T in range(N): m = M[T].reshape(Mshape) * w kpi = pi * k_unfold[T, :] for k in range(Bk): Kk = (K - k) * kpi[2] for j in range(Bj): KkJj = Kk + (J - j) * kpi[1] for i in range(Bi): # Calculate phases and add for all expansions add(Mu[k, j, i], m * exp(2j * (KkJj + kpi[0] * (I - i))), out=Mu[k, j, i]) return Mu.reshape(N * M0, N * M1)
def ArgumentParser(self, p=None, *args, **kwargs): """ Returns the arguments that is available for this Sile """ #limit_args = kwargs.get('limit_arguments', True) short = kwargs.get('short', False) def opts(*args): if short: return args return [args[0]] # We limit the import to occur here import argparse Bohr2Ang = unit_convert('Bohr', 'Ang') Ry2eV = unit_convert('Bohr', 'Ang') # The first thing we do is adding the geometry to the NameSpace of the # parser. # This will enable custom actions to interact with the geometry in a # straight forward manner. # convert netcdf file to a dictionary ion_nc = PropertyDict() ion_nc.n = self._variable('orbnl_n')[:] ion_nc.l = self._variable('orbnl_l')[:] ion_nc.zeta = self._variable('orbnl_z')[:] ion_nc.pol = self._variable('orbnl_ispol')[:] ion_nc.orbital = self._variable('orb')[:] # this gets converted later delta = self._variable('delta')[:] r = aranged(ion_nc.orbital.shape[1]).reshape(1, -1) * delta.reshape(-1, 1) ion_nc.orbital *= r ** ion_nc.l.reshape(-1, 1) / Bohr2Ang * (3./2.) ion_nc.r = r * Bohr2Ang ion_nc.kb = PropertyDict() ion_nc.kb.n = self._variable('pjnl_n')[:] ion_nc.kb.l = self._variable('pjnl_l')[:] ion_nc.kb.e = self._variable('pjnl_ekb')[:] * Ry2eV ion_nc.kb.proj = self._variable('proj')[:] delta = self._variable('kbdelta')[:] r = aranged(ion_nc.kb.proj.shape[1]).reshape(1, -1) * delta.reshape(-1, 1) ion_nc.kb.proj *= r ** ion_nc.kb.l.reshape(-1, 1) / Bohr2Ang * (3./2.) ion_nc.kb.r = r * Bohr2Ang vna = self._variable('vna') r = aranged(vna[:].size) * vna.Vna_delta ion_nc.vna = PropertyDict() ion_nc.vna.v = vna[:] * Ry2eV * r / Bohr2Ang ** 3 ion_nc.vna.r = r * Bohr2Ang # this is charge (not 1/sqrt(charge)) chlocal = self._variable('chlocal') r = aranged(chlocal[:].size) * chlocal.Chlocal_delta ion_nc.chlocal = PropertyDict() ion_nc.chlocal.v = chlocal[:] * r / Bohr2Ang ** 3 ion_nc.chlocal.r = r * Bohr2Ang vlocal = self._variable('reduced_vlocal') r = aranged(vlocal[:].size) * vlocal.Reduced_vlocal_delta ion_nc.vlocal = PropertyDict() ion_nc.vlocal.v = vlocal[:] * r / Bohr2Ang ** 3 ion_nc.vlocal.r = r * Bohr2Ang if "core" in self.variables: # this is charge (not 1/sqrt(charge)) core = self._variable('core') r = aranged(core[:].size) * core.Core_delta ion_nc.core = PropertyDict() ion_nc.core.v = core[:] * r / Bohr2Ang ** 3 ion_nc.core.r = r * Bohr2Ang d = { "_data": ion_nc, "_kb_proj": False, "_l": True, "_n": True, } namespace = default_namespace(**d) # l-quantum number class lRange(argparse.Action): def __call__(self, parser, ns, value, option_string=None): value = (value .replace("s", 0) .replace("p", 1) .replace("d", 2) .replace("f", 3) .replace("g", 4) ) ns._l = strmap(int, value)[0] p.add_argument('-l', action=lRange, help='Denote the sub-section of l-shells that are plotted: "s,f"') # n quantum number class nRange(argparse.Action): def __call__(self, parser, ns, value, option_string=None): ns._n = strmap(int, value)[0] p.add_argument('-n', action=nRange, help='Denote the sub-section of n quantum numbers that are plotted: "2-4,6"') class Plot(argparse.Action): def __call__(self, parser, ns, value, option_string=None): import matplotlib.pyplot as plt # Retrieve values data = ns._data # We have these plots: # - orbitals # - projectors # - chlocal # - vna # - vlocal # - core (optional) # We'll plot them like this: # orbitals | projectors # vna + vlocal | chlocal + core # # Determine different n, l fig, axs = plt.subplots(2, 2) # Now plot different orbitals for n, l, zeta, pol, r, orb in zip(data.n, data.l, data.zeta, data.pol, data.r, data.orbital): if pol == 1: pol = 'P' else: pol = '' axs[0][0].plot(r, orb, label=f"n{n}l{l}Z{zeta}{pol}") axs[0][0].set_title("Orbitals") axs[0][0].set_xlabel("Distance [Ang]") axs[0][0].set_ylabel("Value [a.u.]") axs[0][0].legend() # plot projectors for n, l, e, r, proj in zip( data.kb.n, data.kb.l, data.kb.e, data.kb.r, data.kb.proj): axs[0][1].plot(r, proj, label=f"n{n}l{l} e={e:.5f}") axs[0][1].set_title("KB projectors") axs[0][1].set_xlabel("Distance [Ang]") axs[0][1].set_ylabel("Value [a.u.]") axs[0][1].legend() axs[1][0].plot(data.vna.r, data.vna.v, label='Vna') axs[1][0].plot(data.vlocal.r, data.vlocal.v, label='Vlocal') axs[1][0].set_title("Potentials") axs[1][0].set_xlabel("Distance [Ang]") axs[1][0].set_ylabel("Potential [eV]") axs[1][0].legend() axs[1][1].plot(data.chlocal.r, data.chlocal.v, label='Chlocal') if "core" in data: axs[1][1].plot(data.core.r, data.core.v, label='core') axs[1][1].set_title("Charge") axs[1][1].set_xlabel("Distance [Ang]") axs[1][1].set_ylabel("Charge [Ang^3]") axs[1][1].legend() if value is None: plt.show() else: plt.savefig(value) p.add_argument(*opts('--plot', '-p'), action=Plot, nargs='?', metavar='FILE', help='Plot the content basis set file, possibly saving plot to a file.') return p, namespace
def test_oplist_single(op): d = oplist([ar.aranged(1, 10), ar.aranged(1, 10)]) op(d)
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 grid(cls, n, displ=0., size=1., centered=True, trs=False): r""" Create a grid of `n` points with an offset of `displ` and sampling `size` around `displ` The :math:`k`-points are :math:`\Gamma` centered. Parameters ---------- n : int number of points in the grid. If `trs` is ``True`` this may be smaller than `n` displ : float, optional the displacement of the grid size : float, optional the total size of the Brillouin zone to sample centered : bool, optional if the points are centered trs : bool, optional whether time-reversal-symmetry is applied Returns ------- k : np.ndarray the list of k-points in the Brillouin zone to be sampled w : np.ndarray weights for the k-points """ # First ensure that displ is in the Brillouin displ = displ % 1. if displ > 0.5: displ -= 1. if displ < -0.5: displ += 1. # Centered _only_ has effect IFF # displ == 0. and size == 1 # Otherwise we resort to other schemes if displ != 0. or size != 1.: centered = False # We create the full grid, then afterwards we figure out TRS n_half = n // 2 if n % 2 == 1: k = _a.aranged(-n_half, n_half + 1) * size / n + displ else: k = _a.aranged(-n_half, n_half) * size / n + displ if not centered: # Shift everything by halve the size each occupies k += size / (2 * n) # Move k to the primitive cell and generate weights k = cls.in_primitive(k) w = _a.onesd(n) * size / n # Check for TRS points if trs and np.any(k < 0.): # Make all positive to remove the double conting terms k_pos = np.abs(k) # Sort k-points and weights idx = np.argsort(k_pos) # Re-arange according to k value k_pos = k_pos[idx] w = w[idx] # Find indices of all equivalent k-points (tolerance of 1e-10 in reciprocal units) # 1e-10 ~ 1e10 k-points (no body will do this!) idx_same = (np.diff(k_pos) < 1e-10).nonzero()[0] # The above algorithm should never create more than two duplicates. # Hence we can simply remove all idx_same and double the weight for all # idx_same + 1. w[idx_same + 1] *= 2 # Delete the duplicated k-points (they are already sorted) k = np.delete(k_pos, idx_same, axis=0) w = np.delete(w, idx_same) else: # Sort them, because it makes more visual sense idx = np.argsort(k) k = k[idx] w = w[idx] # Return values return k, w