def test_index_shape2(self, setup): g = setup.g.copy() n = 0 for r in [0.5, 1., 1.5]: s = Cuboid(r) idx = g.index(s) assert len(idx) > n n = len(idx) # Check that if we place the sphere an integer # amount of cells away we retain an equal amount of indices # Also we can check whether they are the same if we add the # offset v = g.dcell.sum(0) vd = v * 0.001 s = Cuboid(1.) idx0 = g.index(s) idx0.sort(0) for d in [10, 15, 20, 60, 100, 340]: idx = g.index(v * d + vd) s = Cuboid(1., center=v * d + vd) idx1 = g.index(s) idx1.sort(0) assert len(idx1) == len(idx0) assert np.all(idx0 == idx1 - idx.reshape(1, 3))
def test_sparse_orbital_replace_hole(): """ Create a big graphene flake remove a hole (1 orbital system) """ g = graphene(orthogonal=True) spo = SparseOrbital(g) # create the sparse-orbital spo.construct([(0.1, 1.45), (0, 2.7)]) # create 10x10 geoemetry nx, ny = 10, 10 big = spo.tile(10, 0).tile(10, 1) hole = spo.tile(6, 0).tile(6, 1) hole = hole.remove(hole.close(hole.center(), R=3)) def create_sp(geom): spo = SparseOrbital(geom) # create the sparse-orbital spo.construct([(0.1, 1.45), (0, 2.7)]) return spo # now replace every position that can be replaced for y in [0, 2, 3]: for x in [1, 2, 3]: cube = Cuboid(hole.sc.cell, origin=g.sc.offset([x, y, 0]) - 0.1) atoms = big.within(cube) assert len(atoms) == 4 * 6 * 6 new = big.replace(atoms, hole) new_copy = create_sp(new.geometry) assert np.fabs((new - new_copy)._csr._D).sum() == 0.
def test_sparse_orbital_replace_hole_norbs(): """ Create a big graphene flake remove a hole (multiple orbitals) """ a1 = Atom(5, R=(1.44, 1.44)) a2 = Atom(7, R=(1.44, 1.44, 1.44)) g = graphene(atoms=[a1, a2], orthogonal=True) spo = SparseOrbital(g) def func(self, ia, atoms, atoms_xyz=None): geom = self.geometry def a2o(idx): return geom.a2o(idx, True) io = a2o(ia) idx = self.geometry.close(ia, R=[0.1, 1.44], atoms=atoms, atoms_xyz=atoms_xyz) idx = list(map(a2o, idx)) self[io, idx[0]] = 0 for i in io: self[i, idx[1]] = 2.7 # create the sparse-orbital spo.construct(func) # create 10x10 geoemetry nx, ny = 10, 10 big = spo.tile(10, 0).tile(10, 1) hole = spo.tile(6, 0).tile(6, 1) hole = hole.remove(hole.close(hole.center(), R=3)) def create_sp(geom): spo = SparseOrbital(geom) # create the sparse-orbital spo.construct(func) return spo # now replace every position that can be replaced for y in [0, 3]: for x in [1, 3]: cube = Cuboid(hole.sc.cell, origin=g.sc.offset([x, y, 0]) - 0.1) atoms = big.within(cube) assert len(atoms) == 4 * 6 * 6 new = big.replace(atoms, hole) new_copy = create_sp(new.geometry) assert np.fabs((new - new_copy)._csr._D).sum() == 0.
def test_geom_category_shape(): hBN_gr = bilayer(1.42, Atom[5, 7], Atom[6]) * (4, 5, 1) mid_layer = np.average(hBN_gr.xyz[:, 2]) A_site = AtomFracSite(graphene()) bottom = AtomXYZ(Cuboid([10000, 10000, mid_layer], [-100, -100, 0])) top = ~bottom bottom_A = A_site & bottom top_A = A_site & top cat = (bottom_A | top_A).categorize(hBN_gr) cnull = 0 for i, c in enumerate(cat): if c == NullCategory(): cnull += 1 elif hBN_gr.xyz[i, 2] < mid_layer: assert c == bottom_A else: assert False assert cnull == len(hBN_gr) // 4 * 3
def bilayer(bond=1.42, bottom_atom=None, top_atom=None, stacking='AB', twist=(0, 0), separation=3.35, ret_angle=False, layer='both'): r""" Commensurate unit cell of a hexagonal bilayer structure, possibly with a twist angle. This routine follows the prescription of twisted bilayer graphene found in [1]_. Notes ----- This routine may change in the future to force some of the arguments. Parameters ---------- bond : float, optional bond length between atoms in the honeycomb lattice bottom_atom : Atom, optional atom (or atoms) in the bottom layer. Defaults to ``Atom(6)`` top_atom : Atom, optional atom (or atoms) in the top layer, defaults to `bottom_atom` stacking : {'AB', 'AA', 'BA'} stacking sequence of the bilayer, where XY means that site X in bottom layer coincides with site Y in top layer twist : tuple of int, optional integer coordinates (m, n) defining a commensurate twist angle separation : float, optional distance between the two layers ret_angle : bool, optional return the twist angle (in degrees) in addition to the geometry instance layer : {'both', 'bottom', 'top'} control which layer(s) to return See Also -------- honeycomb: honeycomb lattices graphene: graphene geometry References ---------- .. [1] G. Trambly de Laissardiere, D. Mayou, L. Magaud, "Localization of Dirac Electrons in Rotated Graphene Bilayers", Nano Letts. 10, 804-808 (2010) """ if bottom_atom is None: bottom_atom = top_atom if bottom_atom is None: bottom_atom = Atom(Z=6, R=bond * 1.01) if top_atom is None: top_atom = bottom_atom # Construct two layers bottom = geom.honeycomb(bond, bottom_atom) top = geom.honeycomb(bond, top_atom) ref_cell = bottom.cell.copy() stacking = stacking.lower() if stacking == 'aa': top = top.move([0, 0, separation]) elif stacking == 'ab': top = top.move([-bond, 0, separation]) elif stacking == 'ba': top = top.move([bond, 0, separation]) else: raise ValueError("bilayer: stacking must be one of {AA, AB, BA}") # Compute twist angle m, n = twist m, n = abs(m), abs(n) if m > n: # Set m as the smaller integer m, n = n, m if not (isinstance(n, int) and isinstance(m, int)): raise ValueError("bilayer: twist coordinates need to be integers!") if m == n: # No twisting theta = 0 rep = 1 natoms = 2 else: # Twisting cos_theta = (n**2 + 4 * n * m + m**2) / (2 * (n**2 + n * m + m**2)) theta = acos(cos_theta) * 180 / pi rep = 4 * (n + m) natoms = 2 * (n**2 + n * m + m**2) if rep > 1: # Set origo through an A atom near the middle of the geometry align_vec = -rep * (ref_cell[0] + ref_cell[1]) / 2 bottom = (bottom.tile(rep, axis=0).tile(rep, axis=1).move(align_vec)) # Set new lattice vectors bottom.cell[0] = n * ref_cell[0] + m * ref_cell[1] bottom.cell[1] = -m * ref_cell[0] + (n + m) * ref_cell[1] # Remove atoms outside cell cell_box = Cuboid(bottom.cell, center=[-bond * 1e-4] * 3) # Reduce atoms in bottom inside_idx = cell_box.within_index(bottom.xyz) bottom = bottom.sub(inside_idx) # Rotate top layer around A atom in bottom layer top = (top.tile(rep, axis=0).tile(rep, axis=1).move(align_vec).rotate( theta, [0, 0, 1])) inside_idx = cell_box.within_index(top.xyz) top = top.sub(inside_idx) # Ensure the cells are commensurate top.cell[:] = bottom.cell[:] # Which layers to be returned layer = layer.lower() if layer == 'bottom': bilayer = bottom elif layer == 'top': bilayer = top elif layer == 'both': bilayer = bottom.add(top) natoms *= 2 else: raise ValueError("bilayer: layer must be one of {both, bottom, top}") if rep > 1: # Rotate and shift unit cell back fxyz_min = bilayer.fxyz.min(axis=0) fxyz_min[2] = 0. # This is a small hack since rotate is not numerically # precise. # TODO We could consider using mpfmath in Quaternion for increased # precision... fxyz_min[np.fabs(fxyz_min) > 1.e-7] *= 0.49 offset = fxyz_min.dot(bilayer.cell) vec = bilayer.cell[0] + bilayer.cell[1] vec_costh = vec[0] / vec.dot(vec)**0.5 vec_th = acos(vec_costh) * 180 / pi bilayer = bilayer.move(-offset).rotate(vec_th, [0, 0, 1]) # Sanity check assert len(bilayer) == natoms if ret_angle: return bilayer, theta else: return bilayer