Prepare Layers

After successfully installing MoirePy, this tutorial will walk you through the process of constructing moiré lattices from individual lattice layers.

As you may know, moiré patterns emerge when two lattice layers are stacked with a slight rotational misalignment between them. In this guide, we'll start by defining individual layers using MoirePy, and then demonstrate how to combine them to generate a moiré lattice.

Defining a layer

We will start by importing the parent class Layer from MoirePy, which helps us define single layers.

>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> from moirepy import Layer

The Layer class handles tasks such as generating lattice points, identifying their neighbours, managing boundary conditions, efficiently searching for specific points in the lattice, and more. For Layer to work properly, we need to define the following properties of a lattice:

  • Lattice vectors: These define the periodicity of the lattice. In MoirePy, they are named lv1 and lv2. For simplicity, lv1 must lie along the \(x\)-direction, and lv2 must lie above the \(x\)-axis. This is a reasonable constraint, as any lattice can be expressed in this form.
  • Lattice points: These are the points within the unit cell of the lattice. Each point is defined by its coordinates relative to the unit cell, along with a unique identifier (called a point type). The point type is useful for lattices with multiple points per unit cell (such as hexagonal or kagome lattices).
  • Neighbours: This is a dictionary defining the neighbours of each lattice point. Each key is a point type, and its value is a list of vectors that represent the relative positions of the neighbouring points.

Omitting even one of these properties will result in an error.

Defining Some Example Lattices

First, we will start with a simple square lattice, which has only one lattice point in the unit cell and four orthogonal neighbours. Then we will look at a more complex example — the kagome lattice — which has three lattice points in the unit cell and four neighbours each.

Square Lattice

Now let us define a square lattice using this Layer class.

class SquareLayer(Layer):  # Inherit the Layer class
    def __init__(self, pbc=False, study_proximity: int=1) -> None:
        self.lv1 = np.array([1, 0])
        self.lv2 = np.array([0, 1])
        self.lattice_points = (
            # location of the point inside the unit cell
            [0, 0, "A"],  # coo_x, coo_y, point_type (unique string)
        )
        self.neighbours = {
            "A": [
                [-1, 0],  # Left
                [1, 0],   # Right
                [0, 1],   # Up
                [0, -1],  # Down
            ],
        }
        # do not forget to initialise the parent class at the end
        super().__init__(pbc=pbc, study_proximity=study_proximity)

Things to Note

  • Make sure to define all required properties for the lattice.
  • Remember to include super().__init__(pbc=pbc, study_proximity=study_proximity) at the end of your __init__ method to properly initialize the parent class.
  • The values in self.neighbours are defined relative to the lattice points. So if the lattice point moves, the neighbour positions will automatically update — no manual changes needed.

Now we can create an instance of this SquareLayer class:

>>> square_layer = SquareLayer(pbc=True, study_proximity=1)
>>> square_layer
Layer(
    lv1 = [1 0],
    lv2 = [0 1],
    lattice_points = ([0, 0, 'A'],),
    pbc = True,
)

Next, let us try plotting this lattice. First, we need to generate the points and then plot them.

>>> square_layer.generate_points(
>>>     np.array([7, 0]),
>>>     np.array([0, 5]),
>>>     1, 1, test=True
>>> )

Making moiré lattices requires precise cutting of the layers. This can only be done using the Moiré lattice classes. Since we’re cutting them arbitrarily here, we set test=True and mln1 = mln2 = 1 to prevent the system from freaking out.

Now, we can either use the attributes square_layer.points and square_layer.point_types to manually plot the lattice, or — for quick testing — use the built-in Layer.plot_lattice() method. However, we encourage you to write your own customised plotting function to better suit your preferences.

>>> square_layer.plot_lattice()

Square Lattice

Kagome Lattice

Kagome structure

Now let us look at a more complex lattice — the kagome lattice. This structure has three lattice points (A, B, and C, marked with red, blue, and green in the above image) in each unit cell. Each of them has four neighbours — two within the same cell and two in diagonally opposite positions to the first two.

class KagomeLayer(Layer):
    def __init__(self, pbc=False, study_proximity: int=1) -> None:
        self.lv1 = np.array([1, 0])  # Lattice vector in the x-direction
        self.lv2 = np.array([0.5, np.sqrt(3)/2])  # Lattice vector at 60 degrees

        self.lattice_points = (
            [0, 0, "A"],
            [0.5, 0, "B"],
            [0.25, np.sqrt(3)/4, "C"],
        )

        self.neighbours = {
            "A": [
                [ 0.5,              0],  # Right
                [ 0.25,  np.sqrt(3)/4],  # Right-up
                [-0.5,              0],  # Left
                [-0.25, -np.sqrt(3)/4],  # Left-down
            ],
            "B": [
                [ 0.5,              0],  # Right
                [-0.25,  np.sqrt(3)/4],  # Left-up
                [-0.5,              0],  # Left
                [ 0.25, -np.sqrt(3)/4],  # Right-down
            ],
            "C": [
                [ 0.25,  np.sqrt(3)/4],  # Right-up
                [-0.25,  np.sqrt(3)/4],  # Left-up
                [-0.25, -np.sqrt(3)/4],  # Left-down
                [ 0.25, -np.sqrt(3)/4],  # Right-down
            ],
        }
        super().__init__(pbc=pbc, study_proximity=study_proximity)

Now let us plot this lattice as well.

>>> kagome_layer = KagomeLayer(pbc=True, study_proximity=1)
>>> kagome_layer.generate_points(
>>>     np.array([7, 0]),
>>>     np.array([0, 5]),
>>>     1, 1, test=True,
>>> )
>>> kagome_layer.plot_lattice()

Kagome Lattice

Inbuilt Layers

Although we highly recommend defining your own layers for research purposes, MoirePy provides a few basic lattices for quick testing and prototyping. These are available in the moirepy.layers module. Here’s a list of the available layers:

  • moirepy.SquareLayer: A simple square lattice as shown above.
  • moirepy.TriangularLayer: A triangular lattice with \(60^\circ\) angles.
  • moirepy.HexagonalLayer: A hexagonal lattice with \(60^\circ\) angles and two points per unit cell.
  • moirepy.KagomeLayer: A kagome lattice as shown above.

Here are the blueprints of these lattices:

Concentric Lattice Points
Square Lattice
Number of Points per Radius Level
Triangle Lattice
Number of Points per Radius Level
Hexagon Lattice
Number of Points per Radius Level
Kagome Lattice

Now that you've seen how to define custom layers — and also explored the inbuilt ones — you should have a good understanding of how individual lattices are represented in MoirePy.

We encourage you to experiment with creating your own layer classes suited to your specific research needs. The inbuilt layers are helpful for quick prototyping, but the real power of MoirePy lies in its flexibility.


In this section we learned how to define custom layers using the Layer class. We also noted the inbuilt layers available in MoirePy for quick testing.

In the next section, we’ll explore how to combine two layers into a moiré superlattice, and how to use these structures to build the tight-binding Hamiltonians and compute physical observables.