Skip to content

Defining Custom Layers in MoirePy

A Layer describes a single 2D crystal: its primitive lattice vectors, the positions of atoms within the unit cell, and the displacement vectors to each atom's nearest neighbours. Every layer class in MoirePy is a subclass of Layer and provides these four pieces of information:

  • lv1, lv2: primitive lattice vectors
  • basis_points: atom positions inside one unit cell, labelled by sublattice type
  • neighbours: displacement vectors to nearest neighbours, keyed by sublattice type
  • pbc, study_proximity: runtime controls, passed through directly to super().__init__(...)

SquareLayer

A minimal example: one atom per unit cell, four nearest neighbours along the cardinal directions.

import numpy as np
from moirepy import BilayerMoireLattice, Layer

class SquareLayer(Layer):
    def __init__(self, pbc=False, study_proximity: int=1) -> None:
        # local definitions, passed to parent
        lv1 = np.array([1, 0])  # Lattice vector in the x-direction
        lv2 = np.array([0, 1])  # Lattice vector in the y-direction

        basis_points = [
            # location and name of that one point inside the unit cell
            (0, 0, "A"),  # pos_x, pos_y, sublattice_name
        ]

        neighbours = {
            "A": [
                [1, 0],   # Right
                [0, 1],   # Up
                [-1, 0],  # Left
                [0, -1],  # Down
            ],
        }

        super().__init__(lv1, lv2, basis_points, neighbours, pbc, study_proximity)

Note

The sublattice labels assigned in basis_points (e.g. "A") must match the keys used in the neighbours dictionary exactly. Each key in neighbours is expected to correspond to a distinct sublattice in the unit cell; any mismatch will result in errors.

Using a layer with BilayerMoireLattice

Once you have a layer class, pass it to BilayerMoireLattice. The integer pairs (ll1, ll2) and (ul1, ul2) define the commensurate supercell and implicitly determine the twist angle.

lattice = BilayerMoireLattice(
    latticetype=SquareLayer,
    ll1=3, ll2=4, ul1=4, ul2=3
)

# Output:
# twist angle = 0.2838 rad (16.2602 deg)
# 25 cells in upper lattice
# 25 cells in lower lattice

BilayerMoireLattice takes the layer geometry, stacks two copies at the twist angle implied by those integers, and constructs the full moiré supercell. From this point you can extract the Hamiltonian, compute band structures, and so on. The layer class is the only thing that changes between different material systems.

A more realistic example: graphene

The honeycomb lattice is the most studied moiré system. Compared to SquareLayer, a Graphene layer introduces two additional considerations: physical units and a two-site unit cell.

class Graphene(Layer):
    def __init__(self, pbc=False, study_proximity: int=1) -> None:
        A_GRAPHENE = 2.46  # lattice constant in angstrom

        lv1 = np.array([1, 0]) * A_GRAPHENE
        lv2 = np.array([0.5, np.sqrt(3) / 2]) * A_GRAPHENE

        basis_points = [
            (0, 0, "A"),
            (A_GRAPHENE, A_GRAPHENE / np.sqrt(3), "B"),
        ]

        # Standard neighbours for hexagonal lattice
        neigh_deltas = np.array([
            [0, 1/np.sqrt(3)],
            [-0.5, -1/(2 * np.sqrt(3))],
            [0.5, -1/(2 * np.sqrt(3))]
        ]) * A_GRAPHENE

        neighbours = {"A": neigh_deltas, "B": -neigh_deltas}

        super().__init__(lv1, lv2, basis_points, neighbours, pbc, study_proximity)

A few things to note:

  • Physical units. All vectors are scaled by the real graphene lattice constant A_GRAPHENE = 2.46 angstrom. For toy models the absolute scale is irrelevant, but for real materials it determines all inter-atomic distances and coupling strengths.
  • Two sublattices. The honeycomb lattice has two inequivalent carbon sites, A and B, so basis_points carries two entries and neighbours carries two keys. The antisymmetry "B": -neigh_deltas reflects the fact that the neighbour displacements of B are exactly those of A reversed.
  • The call to super().__init__ is unchanged. The same interface holds regardless of the complexity of the layer.

Substituting Graphene for SquareLayer in the BilayerMoireLattice call above produces a twisted bilayer graphene supercell with no other changes to the workflow.