Source code for qlinks.lattice.kagome

from __future__ import annotations

from typing import ClassVar

import numpy as np

from qlinks.lattice.graph import LatticeGraph
from qlinks.lattice.types import BoundaryCondition, Link, Plaquette, Site


[docs] class KagomeLattice(LatticeGraph): """Two-dimensional kagome lattice with a three-site triangular unit cell. Unit cell: A(x, y), sublattice 0 B(x, y), sublattice 1 C(x, y), sublattice 2 Primitive vectors: a1 = (1, 0) a2 = (1/2, sqrt(3)/2) Link convention: Each unit cell contributes six nearest-neighbor bonds: ab : A(x, y) -> B(x, y) ab_prev : A(x, y) -> B(x - 1, y) ac : A(x, y) -> C(x, y) ac_prev : A(x, y) -> C(x, y - 1) bc : B(x, y) -> C(x, y) bc_next : B(x, y) -> C(x + 1, y - 1) Plaquettes: triangle_up / triangle_down are included for visualization and local geometry diagnostics. Kagome QDM/QLM resonance terms use only the hexagon plaquettes returned by :meth:`qdm_plaquette_ids` and :meth:`qlm_plaquette_ids`. """ __slots__ = ("lx", "ly") _sqrt3: ClassVar[float] = float(np.sqrt(3.0)) _primitive_vectors: ClassVar[tuple[np.ndarray, ...]] = ( np.array([1.0, 0.0], dtype=float), np.array([0.5, _sqrt3 / 2.0], dtype=float), ) _basis_offsets: ClassVar[tuple[np.ndarray, ...]] = ( np.array([0.0, 0.0], dtype=float), np.array([0.5, 0.0], dtype=float), np.array([0.25, _sqrt3 / 4.0], dtype=float), ) _link_displacements: ClassVar[dict[str, tuple[int, int, int, int]]] = { # kind: (source_sublattice, target_sublattice, dx, dy) "ab": (0, 1, 0, 0), "ab_prev": (0, 1, -1, 0), "ac": (0, 2, 0, 0), "ac_prev": (0, 2, 0, -1), "bc": (1, 2, 0, 0), "bc_next": (1, 2, 1, -1), }
[docs] def __init__( self, lx: int, ly: int, boundary_condition: BoundaryCondition | str = BoundaryCondition.OPEN, *, include_triangles: bool = True, include_hexagons: bool = True, ) -> None: if lx <= 0: raise ValueError("lx must be positive.") if ly <= 0: raise ValueError("ly must be positive.") bc = BoundaryCondition(boundary_condition) def site_id(x: int, y: int, sublattice: int) -> int: return 3 * (x * ly + y) + sublattice def in_bounds(x: int, y: int) -> bool: return 0 <= x < lx and 0 <= y < ly def wrap_coord(x: int, y: int) -> tuple[int, int]: return x % lx, y % ly def maybe_site_id(x: int, y: int, sublattice: int) -> int | None: if bc == BoundaryCondition.PERIODIC: wx, wy = wrap_coord(x, y) return site_id(wx, wy, sublattice) if in_bounds(x, y): return site_id(x, y, sublattice) return None def position(x: int, y: int, sublattice: int) -> tuple[float, float]: primitive = ( float(x) * self._primitive_vectors[0] + float(y) * self._primitive_vectors[1] + self._basis_offsets[int(sublattice)] ) return (float(primitive[0]), float(primitive[1])) sites = tuple( Site( id=site_id(x, y, sub), cell=(x, y), sublattice=sub, position=position(x, y, sub), ) for x in range(lx) for y in range(ly) for sub in (0, 1, 2) ) links: list[Link] = [] directed_link_lookup: dict[tuple[int, int], int] = {} cell_link_lookup: dict[tuple[int, int, str], int] = {} undirected_seen: set[tuple[int, int]] = set() def add_link( source: int, target: int, *, cell: tuple[int, int], kind: str, wrap: bool, ) -> None: if source == target: return undirected_key = tuple(sorted((int(source), int(target)))) if undirected_key in undirected_seen: return link_id = len(links) links.append( Link( id=link_id, source=source, target=target, kind=kind, wrap=wrap, ) ) directed_link_lookup[(source, target)] = link_id undirected_seen.add(undirected_key) cell_link_lookup[(int(cell[0]), int(cell[1]), str(kind))] = link_id for x in range(lx): for y in range(ly): for kind, (source_sub, target_sub, dx, dy) in self._link_displacements.items(): source = site_id(x, y, source_sub) tx, ty = x + dx, y + dy target = maybe_site_id(tx, ty, target_sub) if target is None: continue crosses = not in_bounds(tx, ty) if bc == BoundaryCondition.PERIODIC or not crosses: add_link( source, target, cell=(x, y), kind=kind, wrap=crosses, ) def oriented_link_between(source: int, target: int) -> tuple[int, int]: direct = directed_link_lookup.get((source, target)) if direct is not None: return direct, +1 reverse = directed_link_lookup.get((target, source)) if reverse is not None: return reverse, -1 raise KeyError(f"No link between {source} and {target}.") plaquettes: list[Plaquette] = [] seen_plaquette_links: set[tuple[int, ...]] = set() def add_loop( kind: str, *, anchor: tuple[int, int], site_specs: tuple[tuple[int, int, int], ...], ) -> None: site_ids: list[int] = [] for sx, sy, sub in site_specs: sid = maybe_site_id(sx, sy, sub) if sid is None: return site_ids.append(int(sid)) if len(set(site_ids)) != len(site_ids): return loop_links: list[int] = [] orientations: list[int] = [] for current, nxt in zip(site_ids, site_ids[1:] + site_ids[:1], strict=True): try: link_id, orientation = oriented_link_between(current, nxt) except KeyError: return loop_links.append(int(link_id)) orientations.append(int(orientation)) if len(set(loop_links)) != len(loop_links): return plaquette_key = tuple(sorted(loop_links)) if plaquette_key in seen_plaquette_links: return seen_plaquette_links.add(plaquette_key) plaquettes.append( Plaquette( id=len(plaquettes), links=tuple(loop_links), orientations=tuple(orientations), sites=tuple(site_ids), kind=kind, anchor_cell=anchor, ) ) max_x = lx if bc == BoundaryCondition.PERIODIC else lx max_y = ly if bc == BoundaryCondition.PERIODIC else ly for x in range(max_x): for y in range(max_y): if include_triangles: add_loop( "triangle_up", anchor=(x, y), site_specs=((x, y, 0), (x, y, 1), (x, y, 2)), ) add_loop( "triangle_down", anchor=(x, y), site_specs=((x, y, 1), (x + 1, y - 1, 2), (x + 1, y, 0)), ) if include_hexagons: add_loop( "hexagon", anchor=(x, y), site_specs=( (x, y, 1), (x, y, 2), (x, y + 1, 0), (x, y + 1, 1), (x + 1, y, 2), (x + 1, y, 0), ), ) translations: dict[tuple[int, tuple[int, int]], int] = {} for x in range(lx): for y in range(ly): for sub in (0, 1, 2): source = site_id(x, y, sub) for disp in ((1, 0), (-1, 0), (0, 1), (0, -1)): target = maybe_site_id(x + disp[0], y + disp[1], sub) if target is not None: translations[(source, disp)] = target super().__init__( sites=sites, links=tuple(links), plaquettes=tuple(plaquettes), boundary_condition=bc, translations=translations, ) object.__setattr__(self, "lx", lx) object.__setattr__(self, "ly", ly)
[docs] def site_id(self, x: int, y: int, sublattice: int) -> int: if sublattice not in (0, 1, 2): raise ValueError("sublattice must be 0, 1, or 2.") if self.boundary_condition == BoundaryCondition.PERIODIC: x %= self.lx y %= self.ly elif not (0 <= x < self.lx and 0 <= y < self.ly): raise IndexError(f"site coordinate ({x}, {y}) outside lattice.") return 3 * (x * self.ly + y) + sublattice
[docs] def qdm_plaquette_ids(self): return [plaquette.id for plaquette in self.plaquettes if plaquette.kind == "hexagon"]
[docs] def qlm_plaquette_ids(self): return self.qdm_plaquette_ids()
[docs] def hexagon_plaquette_id(self, x: int, y: int) -> int: return self.plaquette_id_from_anchor((x, y), kind="hexagon")