BilayerMoireLattice

Source code in moirepy/moire.py
class BilayerMoireLattice:  # both layers same, only one point in one unit cell
    def __init__(
        self,
        latticetype: Layer,
        ll1:int, ll2:int,  # lower lattice
        ul1:int, ul2:int,  # upper lattice
        n1:int=1, n2:int=1,
        translate_upper=(0, 0),
        pbc:bool=True,
        k:int=1,  # number of orbitals
    ):
        """
        Initializes a Moiré lattice composed of two twisted layers of the same type.

        Args:
            latticetype (Layer): A subclass of the `Layer` class representing the lattice type used for both layers.
            ll1, ll2, ul1, ul2 (int): Values select from the [AVC tool](https://jabed-umar.github.io/MoirePy/theory/avc/).
            n1 (int, optional): Number of moiré cells along the first lattice vector.
            n2 (int, optional): Number of moiré cells along the second lattice vector.
            translate_upper (tuple, optional): Translation vector (dx, dy) applied to the upper layer before rotation.
            pbc (bool, optional): Whether to apply periodic boundary conditions. If False, open boundary conditions are used.
            k (int, optional): Number of orbitals on each lattice point.
        """

        # study_proximity = 1 means only studying nearest neighbours will be enabled,
        # 2 means study of next nearest neighbours will be enabled too and so on,
        # always better to keep this value 1 or two more than what you will actually need.
        lower_lattice = latticetype(pbc=pbc)
        upper_lattice = latticetype(pbc=pbc)

        lv1, lv2 = lower_lattice.lv1, lower_lattice.lv2

        # c = cos(theta) between lv1 and lv2 (60 degree for triangular, 90 for square and so on)
        c = np.dot(lv1, lv2) / (np.linalg.norm(lv1) * np.linalg.norm(lv2))
        beta = np.arccos(c)
        mlv1 = ll1*lv1 + ll2*lv2  # because lower latice is fixed
        mlv2 = get_rotation_matrix(beta).dot(mlv1)

        # calculating the moire twist angle
        one = ll1*lv1 + ll2*lv2  # the coords of overlapping point in the lower lattice
        two = ul1*lv1 + ul2*lv2  # the coords of overlapping point in the upper lattice
        assert np.isclose(np.linalg.norm(one), np.linalg.norm(two)), "INPUT ERROR: the two points are not overlapping, check ll1, ll2, ul1, ul2 values"
        c = np.dot(one, two) / (np.linalg.norm(one) * np.linalg.norm(two))
        theta = np.arccos(c)  # in radians
        print(f"twist angle = {theta:.4f} rad ({np.rad2deg(theta):.4f} deg)")

        upper_lattice.perform_rotation_translation(theta, translate_upper)
        assert (
            are_coeffs_integers(lower_lattice.lv1, lower_lattice.lv2, mlv1) and
            are_coeffs_integers(upper_lattice.lv1, upper_lattice.lv2, mlv1)
        ), "FATAL ERROR: calculated mlv2 is incorrect"
        lower_lattice.generate_points(mlv1, mlv2, n1, n2)
        upper_lattice.generate_points(mlv1, mlv2, n1, n2)
        # print(f"{mlv1 = }")
        # print(f"{mlv2 = }")

        self.ll1 = ll1
        self.ll2 = ll2
        self.ul1 = ul1
        self.ul2 = ul2
        self.n1 = n1
        self.n2 = n2
        self.translate_upper = translate_upper
        self.lower_lattice = lower_lattice
        self.upper_lattice = upper_lattice
        self.theta = theta
        self.mlv1 = mlv1
        self.mlv2 = mlv2
        self.pbc = pbc
        self.orbitals = k
        self.ham = None

        print(f"{len(self.lower_lattice.points)} points in lower lattice")
        print(f"{len(self.upper_lattice.points)} points in upper lattice")
        assert len(self.lower_lattice.points) == len(self.upper_lattice.points), "FATAL ERROR: number of points in lower and upper lattice are not equal, take different ll1, ll2, ul1, ul2 values"

        # self.plot_lattice()

    def plot_lattice(self):
        mlv1 = self.mlv1
        mlv2 = self.mlv2
        n1 = self.n1
        n2 = self.n2

        # plt.plot(*zip(*self.lower_lattice.points), 'r.', markersize=2)
        # plt.plot(*zip(*self.upper_lattice.points), 'b.', markersize=2)
        self.lower_lattice.plot_lattice(colours=["b"], plot_connections=True)
        self.upper_lattice.plot_lattice(colours=["r"], plot_connections=True)

        # parallellogram around the whole lattice
        plt.plot([0, n1*mlv1[0]], [0, n1*mlv1[1]], 'k', linewidth=1)
        plt.plot([0, n2*mlv2[0]], [0, n2*mlv2[1]], 'k', linewidth=1)
        plt.plot([n1*mlv1[0], n1*mlv1[0] + n2*mlv2[0]], [n1*mlv1[1], n1*mlv1[1] + n2*mlv2[1]], 'k', linewidth=1)
        plt.plot([n2*mlv2[0], n1*mlv1[0] + n2*mlv2[0]], [n2*mlv2[1], n1*mlv1[1] + n2*mlv2[1]], 'k', linewidth=1)

        # just plot mlv1 and mlv2 parallellogram
        plt.plot([0, mlv1[0]], [0, mlv1[1]], 'k', linewidth=1)
        plt.plot([0, mlv2[0]], [0, mlv2[1]], 'k', linewidth=1)
        plt.plot([mlv1[0], mlv1[0] + mlv2[0]], [mlv1[1], mlv1[1] + mlv2[1]], 'k', linewidth=1)
        plt.plot([mlv2[0], mlv1[0] + mlv2[0]], [mlv2[1], mlv1[1] + mlv2[1]], 'k', linewidth=1)

        # set equal aspect ratio
        plt.gca().set_aspect('equal', adjustable='box')
        # plt.grid()
        # plt.show()
        # plt.savefig("moire.pdf", bbox_inches='tight')

    def _validate_input1(self, a, name):
        if a is None:
            a = 0
            print(f"WARNING: {name} is not set, setting it to 0")
        if callable(a): return a
        return lambda this_coo, neigh_coo, this_type, neigh_type: a

    def _validate_input2(self, a, name):
        if a is None:
            a = 0
            print(f"WARNING: {name} is not set, setting it to 0")
        if callable(a): return a
        return lambda this_coo, this_type: a

    def generate_hamiltonian(
        self,
        tll: Union[float, int, Callable[[Sequence[float], Sequence[float], str, str], float]] = None,
        tuu: Union[float, int, Callable[[Sequence[float], Sequence[float], str, str], float]] = None,
        tlu: Union[float, int, Callable[[Sequence[float], Sequence[float], str, str], float]] = None,
        tul: Union[float, int, Callable[[Sequence[float], Sequence[float], str, str], float]] = None,
        tuself: Union[float, int, Callable[[Sequence[float], str], float]] = None,
        tlself: Union[float, int, Callable[[Sequence[float], str], float]] = None,
        data_type: np.dtype = np.float64,  # set to np.complex128 if you want complex numbers
    ):
        k = self.orbitals
        if tll is None or isinstance(tll, int) or isinstance(tll, float): tll = self._validate_input1(tll, "tll")
        if tuu is None or isinstance(tuu, int) or isinstance(tuu, float): tuu = self._validate_input1(tuu, "tuu")
        if tlu is None or isinstance(tlu, int) or isinstance(tlu, float): tlu = self._validate_input1(tlu, "tlu")
        if tul is None or isinstance(tul, int) or isinstance(tul, float): tul = self._validate_input1(tul, "tul")
        if tuself is None or isinstance(tuself, int) or isinstance(tuself, float): tuself = self._validate_input2(tuself, "tuself")
        if tlself is None or isinstance(tlself, int) or isinstance(tlself, float): tlself = self._validate_input2(tlself, "tlself")
        assert (
                callable(tll)
            and callable(tuu)
            and callable(tlu)
            and callable(tul)
            and callable(tuself)
            and callable(tlself)
        ), "tuu, tll, tlu, tul, tuself and tlself must be floats, ints or callable objects like functions"
        # self.plot_lattice()

        # 1. interaction inside the lower lattice
        ham_ll = np.zeros((len(self.lower_lattice.points)*k, len(self.lower_lattice.points)*k), dtype=data_type)
        for i in range(len(self.lower_lattice.points)):  # self interactions
            ham_ll[i*k:(i+1)*k, i*k:(i+1)*k] += tlself(
                self.lower_lattice.points[i],
                self.lower_lattice.point_types[i]
            )
        bigger_indices, indices, _ = self.lower_lattice.first_nearest_neighbours(self.lower_lattice.points, self.lower_lattice.point_types)
        for this_i in range(len(self.lower_lattice.points)):  # neighbour interactions
            this_coo = self.lower_lattice.points[this_i]
            this_type = self.lower_lattice.point_types[this_i]
            for phantom_neigh_i, neigh_i in zip(bigger_indices[this_i], indices[this_i]):
                if self.pbc: neigh_coo = self.lower_lattice.bigger_points[phantom_neigh_i]
                else:        neigh_coo = self.lower_lattice.points[neigh_i]
                neigh_type = self.lower_lattice.point_types[neigh_i]
                ham_ll[this_i*k:(this_i+1)*k, neigh_i*k:(neigh_i+1)*k] += tll(this_coo, neigh_coo, this_type, neigh_type)

        # 2. interaction inside the upper lattice
        ham_uu = np.zeros((len(self.upper_lattice.points)*k, len(self.upper_lattice.points)*k), dtype=data_type)
        for i in range(len(self.upper_lattice.points)):  # self interactions
            ham_uu[i*k:(i+1)*k, i*k:(i+1)*k] += tuself(
                self.upper_lattice.points[i],
                self.upper_lattice.point_types[i]
            )
        bigger_indices, indices, _ = self.upper_lattice.first_nearest_neighbours(self.upper_lattice.points, self.upper_lattice.point_types)
        for this_i in range(len(self.upper_lattice.points)):  # neighbour interactions
            this_coo = self.upper_lattice.points[this_i]
            this_type = self.upper_lattice.point_types[this_i]
            # for neigh_i in indices[this_i]:
            for phantom_neigh_i, neigh_i in zip(bigger_indices[this_i], indices[this_i]):
                if self.pbc: neigh_coo = self.lower_lattice.bigger_points[phantom_neigh_i]
                else:        neigh_coo = self.lower_lattice.points[neigh_i]
                neigh_type = self.upper_lattice.point_types[neigh_i]
                ham_uu[this_i*k:(this_i+1)*k, neigh_i*k:(neigh_i+1)*k] += tuu(this_coo, neigh_coo, this_type, neigh_type)

        # 3. interaction from the lower to the upper lattice
        ham_lu = np.zeros((len(self.lower_lattice.points)*k, len(self.upper_lattice.points)*k), dtype=data_type)
        bigger_indices, indices, _ = self.upper_lattice.query_one(self.lower_lattice.points)
        for this_i in range(len(self.lower_lattice.points)):
            neigh_i = indices[this_i, 0]
            phantom_neigh_i = bigger_indices[this_i, 0]
            ham_lu[this_i*k:(this_i+1)*k, neigh_i*k:(neigh_i+1)*k] += tlu(
                self.lower_lattice.points[this_i],
                self.upper_lattice.bigger_points[phantom_neigh_i] if self.pbc else self.upper_lattice.points[neigh_i],
                self.lower_lattice.point_types[this_i],
                self.upper_lattice.point_types[neigh_i],
            )

        # 4. interaction from the upper to the lower lattice
        ham_ul = np.zeros((len(self.upper_lattice.points)*k, len(self.lower_lattice.points)*k), dtype=data_type)
        bigger_indices, indices, _ = self.lower_lattice.query_one(self.upper_lattice.points)
        for this_i in range(len(self.upper_lattice.points)):
            neigh_i = indices[this_i, 0]
            phantom_neigh_i = bigger_indices[this_i, 0]
            ham_ul[this_i*k:(this_i+1)*k, neigh_i*k:(neigh_i+1)*k] += tul(
                self.upper_lattice.points[this_i],
                self.lower_lattice.bigger_points[phantom_neigh_i] if self.pbc else self.lower_lattice.points[neigh_i],
                self.upper_lattice.point_types[this_i],
                self.lower_lattice.point_types[neigh_i],
            )

        # # in ham_ll and ham_uu, check sum of all the rows...
        # # for constant t it should represent the number of neighbours for each point
        # print(f"unique sums in ham_ll: {np.unique(np.sum(ham_ll, axis=1))}")
        # print(f"unique sums in ham_uu: {np.unique(np.sum(ham_uu, axis=1))}")
        # print(f"unique sums in ham_lu: {np.unique(np.sum(ham_lu, axis=1))}")
        # print(f"unique sums in ham_ul: {np.unique(np.sum(ham_ul, axis=1))}")

        # combine the hamiltonians
        self.ham = np.block([
            [ham_ll, ham_lu],
            [ham_ul, ham_uu]
        ])

        return self.ham

    def generate_k_space_hamiltonian(
        self,
        k: np.ndarray,
        tll: Union[float, int, Callable[[Sequence[float], Sequence[float], str, str], float]] = None,
        tuu: Union[float, int, Callable[[Sequence[float], Sequence[float], str, str], float]] = None,
        tlu: Union[float, int, Callable[[Sequence[float], Sequence[float], str, str], float]] = None,
        tul: Union[float, int, Callable[[Sequence[float], Sequence[float], str, str], float]] = None,
        tuself: Union[float, int, Callable[[Sequence[float], str], float]] = None,
        tlself: Union[float, int, Callable[[Sequence[float], str], float]] = None,
        suppress_nxny_warning: bool = False,
    ):
        if suppress_nxny_warning is False and (self.n1 != 1 or self.n2 != 1):
            print("WARNING: atleast one of n1 and n2 are not 1, are you sure you want to use generate_k_space_hamiltonian with this lattice?")

        if tll is None or isinstance(tll, int) or isinstance(tll, float): tll = self._validate_input1(tll, "tll")
        if tuu is None or isinstance(tuu, int) or isinstance(tuu, float): tuu = self._validate_input1(tuu, "tuu")
        if tlu is None or isinstance(tlu, int) or isinstance(tlu, float): tlu = self._validate_input1(tlu, "tlu")
        if tul is None or isinstance(tul, int) or isinstance(tul, float): tul = self._validate_input1(tul, "tul")
        if tuself is None or isinstance(tuself, int) or isinstance(tuself, float): tuself = self._validate_input2(tuself, "tuself")
        if tlself is None or isinstance(tlself, int) or isinstance(tlself, float): tlself = self._validate_input2(tlself, "tlself")
        assert (
                callable(tll)
            and callable(tuu)
            and callable(tlu)
            and callable(tul)
            and callable(tuself)
            and callable(tlself)
        ), "tuu, tll, tlu, tul, tuself and tlself must be floats, ints or callable objects like functions"

        part = lambda k, this_coo, neigh_coo: np.exp(1j * (k @ (this_coo.squeeze() - neigh_coo.squeeze())))
        return self.generate_hamiltonian(
            lambda this_coo, neigh_coo, this_type, neigh_type: tll(this_coo, neigh_coo, this_type, neigh_type) * part(k, this_coo, neigh_coo),
            lambda this_coo, neigh_coo, this_type, neigh_type: tuu(this_coo, neigh_coo, this_type, neigh_type) * part(k, this_coo, neigh_coo),
            lambda this_coo, neigh_coo, this_type, neigh_type: tlu(this_coo, neigh_coo, this_type, neigh_type) * part(k, this_coo, neigh_coo),
            lambda this_coo, neigh_coo, this_type, neigh_type: tul(this_coo, neigh_coo, this_type, neigh_type) * part(k, this_coo, neigh_coo),
            tuself, tlself,
            data_type=np.complex128
        )

__init__(latticetype, ll1, ll2, ul1, ul2, n1=1, n2=1, translate_upper=(0, 0), pbc=True, k=1)

Initializes a Moiré lattice composed of two twisted layers of the same type.

Parameters:
  • latticetype (Layer) –

    A subclass of the Layer class representing the lattice type used for both layers.

  • ll1, (ll2, ul1, ul2 (int) –

    Values select from the AVC tool.

  • n1 (int, default: 1 ) –

    Number of moiré cells along the first lattice vector.

  • n2 (int, default: 1 ) –

    Number of moiré cells along the second lattice vector.

  • translate_upper (tuple, default: (0, 0) ) –

    Translation vector (dx, dy) applied to the upper layer before rotation.

  • pbc (bool, default: True ) –

    Whether to apply periodic boundary conditions. If False, open boundary conditions are used.

  • k (int, default: 1 ) –

    Number of orbitals on each lattice point.

Source code in moirepy/moire.py
def __init__(
    self,
    latticetype: Layer,
    ll1:int, ll2:int,  # lower lattice
    ul1:int, ul2:int,  # upper lattice
    n1:int=1, n2:int=1,
    translate_upper=(0, 0),
    pbc:bool=True,
    k:int=1,  # number of orbitals
):
    """
    Initializes a Moiré lattice composed of two twisted layers of the same type.

    Args:
        latticetype (Layer): A subclass of the `Layer` class representing the lattice type used for both layers.
        ll1, ll2, ul1, ul2 (int): Values select from the [AVC tool](https://jabed-umar.github.io/MoirePy/theory/avc/).
        n1 (int, optional): Number of moiré cells along the first lattice vector.
        n2 (int, optional): Number of moiré cells along the second lattice vector.
        translate_upper (tuple, optional): Translation vector (dx, dy) applied to the upper layer before rotation.
        pbc (bool, optional): Whether to apply periodic boundary conditions. If False, open boundary conditions are used.
        k (int, optional): Number of orbitals on each lattice point.
    """

    # study_proximity = 1 means only studying nearest neighbours will be enabled,
    # 2 means study of next nearest neighbours will be enabled too and so on,
    # always better to keep this value 1 or two more than what you will actually need.
    lower_lattice = latticetype(pbc=pbc)
    upper_lattice = latticetype(pbc=pbc)

    lv1, lv2 = lower_lattice.lv1, lower_lattice.lv2

    # c = cos(theta) between lv1 and lv2 (60 degree for triangular, 90 for square and so on)
    c = np.dot(lv1, lv2) / (np.linalg.norm(lv1) * np.linalg.norm(lv2))
    beta = np.arccos(c)
    mlv1 = ll1*lv1 + ll2*lv2  # because lower latice is fixed
    mlv2 = get_rotation_matrix(beta).dot(mlv1)

    # calculating the moire twist angle
    one = ll1*lv1 + ll2*lv2  # the coords of overlapping point in the lower lattice
    two = ul1*lv1 + ul2*lv2  # the coords of overlapping point in the upper lattice
    assert np.isclose(np.linalg.norm(one), np.linalg.norm(two)), "INPUT ERROR: the two points are not overlapping, check ll1, ll2, ul1, ul2 values"
    c = np.dot(one, two) / (np.linalg.norm(one) * np.linalg.norm(two))
    theta = np.arccos(c)  # in radians
    print(f"twist angle = {theta:.4f} rad ({np.rad2deg(theta):.4f} deg)")

    upper_lattice.perform_rotation_translation(theta, translate_upper)
    assert (
        are_coeffs_integers(lower_lattice.lv1, lower_lattice.lv2, mlv1) and
        are_coeffs_integers(upper_lattice.lv1, upper_lattice.lv2, mlv1)
    ), "FATAL ERROR: calculated mlv2 is incorrect"
    lower_lattice.generate_points(mlv1, mlv2, n1, n2)
    upper_lattice.generate_points(mlv1, mlv2, n1, n2)
    # print(f"{mlv1 = }")
    # print(f"{mlv2 = }")

    self.ll1 = ll1
    self.ll2 = ll2
    self.ul1 = ul1
    self.ul2 = ul2
    self.n1 = n1
    self.n2 = n2
    self.translate_upper = translate_upper
    self.lower_lattice = lower_lattice
    self.upper_lattice = upper_lattice
    self.theta = theta
    self.mlv1 = mlv1
    self.mlv2 = mlv2
    self.pbc = pbc
    self.orbitals = k
    self.ham = None

    print(f"{len(self.lower_lattice.points)} points in lower lattice")
    print(f"{len(self.upper_lattice.points)} points in upper lattice")
    assert len(self.lower_lattice.points) == len(self.upper_lattice.points), "FATAL ERROR: number of points in lower and upper lattice are not equal, take different ll1, ll2, ul1, ul2 values"