Site types and layouts
A site type is a value-level description of the physical degree of freedom living on a lattice site. A site layout is how those site types are stored per-lattice. LatticeCore keeps the two concerns distinct from the geometric sublattice (LatticeCoord.sublattice) so mixed-spin and diluted models compose naturally.
For the physics behind the taxonomy, see the concept column Site types and spin models.
The built-in site types
All of the classical spin flavours are available out of the box:
| Site type | State | Default storage |
|---|---|---|
IsingSite | $\pm 1$ | Int8 |
PottsSite{Q} | 1..Q | Int8 |
XYSite | angle in [0, 2π) | Float64 |
HeisenbergSite | unit vector on $S^2$ | SVector{3, Float64} |
EmptySite | Nothing | Nothing |
Each one is constructed by calling the struct with no arguments for the default storage type:
IsingSite() # IsingSite{Int8}()
IsingSite{Int}() # Int storage (more memory, wider range)
PottsSite(3) # 3-state Potts, Int8 storage
XYSite() # Float64 angle
HeisenbergSite() # SVector{3, Float64} unit vector
EmptySite() # vacancyThe required interface on every site type is
state_type(st) # the Julia type storing one state
random_state(rng, st) # uniform sampler for initialisationplus the optional
zero_state(st) # canonical zero state, if meaningful
domain(st) # finite iterator over the state space, for discrete typesand the element-centering trait
element_type(st) # default VertexCenter()which is covered in more detail below.
Picking a layout
Sites with site types are stored in the lattice's AbstractSiteLayout. Three concrete layouts ship:
UniformLayout — every site the same
sq = SimpleSquareLattice(4, 4) # defaults to IsingSite
site_layout(sq) # UniformLayout{IsingSite{Int8}}
site_type(sq, 1) === site_type(sq, 16) # true
# Swap in a different site type via the keyword argument
heis = SimpleSquareLattice(4, 4; layout = UniformLayout(HeisenbergSite()))
site_type(heis, 1) # HeisenbergSite{Float64}()UniformLayout has zero per-site memory overhead — the single site type is stored once on the lattice, so the MC hot loop can treat site_type(lat, i) as a loop-invariant and constant-fold it. This is the layout you want for homogeneous Ising / XY / Heisenberg models, i.e. the overwhelming majority.
SublatticeLayout — one site type per geometric sublattice
# 6 sites with the A/B/A/B/A/B pattern of a chain
mixed = SublatticeLayout(
(IsingSite(), XYSite()), # per-sublattice site types
[1, 2, 1, 2, 1, 2], # sublattice id of each site
)
site_type(mixed, 1) isa IsingSite # true
site_type(mixed, 2) isa XYSite # true
site_type(mixed, 5) isa IsingSite # trueThe first field is an NTuple of site type instances, one per geometric sublattice. The second is a Vector{Int} mapping every site index to its sublattice id. Downstream code can then write polymorphic interactions:
interaction(m::MixedSpinModel, ::IsingSite, ::XYSite, bond, s, θ) =
-m.J_AB * s * cos(θ)and the multiple-dispatch mechanism picks out every A–B bond automatically.
ExplicitLayout — quenched per-site heterogeneity
types = AbstractSiteType[IsingSite(), IsingSite(), XYSite(), HeisenbergSite()]
disorder = ExplicitLayout(types)
site_type(disorder, 1) isa IsingSite # true
site_type(disorder, 3) isa XYSite # true
site_type(disorder, 4) isa HeisenbergSite # trueExplicitLayout stores one site type per site, which is the right representation for quenched disorder, defect models, or any configuration where the pattern cannot factor through a small sublattice tuple. The per-site storage cost is the trade-off.
A custom site type from scratch
Custom site types are what you add when the existing flavours do not fit — for instance, a Blume–Capel spin ($S = 1$), a $U(1)$ clock rotor, a Kitaev bond variable, or a dimer variable. The required steps are
- Define a subtype of
AbstractSiteType. - Implement
state_typeandrandom_state. - (Optional) implement
zero_state,domain,element_type.
A three-state Blume–Capel example:
using Random
struct BlumeCapelSite <: LatticeCore.AbstractSiteType end
LatticeCore.state_type(::BlumeCapelSite) = Int8
LatticeCore.random_state(rng, ::BlumeCapelSite) =
Int8(rand(rng, (-1, 0, 1)))
LatticeCore.zero_state(::BlumeCapelSite) = Int8(0)
LatticeCore.domain(::BlumeCapelSite) = (Int8(-1), Int8(0), Int8(1))It plugs straight into the existing layouts:
lat = SimpleSquareLattice(4, 4; layout = UniformLayout(BlumeCapelSite()))
site_type(lat, 1) isa BlumeCapelSite # trueElement centering
Every AbstractSiteType carries an element_type trait declaring where its DOF lives. The default is VertexCenter, but bond / plaquette / cell-centered variables are first-class through this trait:
struct DimerVariable <: LatticeCore.AbstractSiteType end
LatticeCore.element_type(::DimerVariable) = BondCenter()
LatticeCore.state_type(::DimerVariable) = Bool
LatticeCore.random_state(rng, ::DimerVariable) = rand(rng, Bool)A downstream Monte Carlo layer that wants to know whether a site type lives on a vertex or a bond asks element_type(st). For the homogeneous classical spin models that currently dominate the LatticeCore test suite, this trait returns VertexCenter() and every code path ignores it, so adding a bond-centered type does not bloat vertex-only code.
See Concepts: Quasiperiodic order for why this matters when you start thinking about dimer models, gauge theories, and flux variables.
Querying a lattice
Three accessors on AbstractLattice let downstream code read the site structure of a lattice:
site_layout(lat) # AbstractSiteLayout
site_type(lat, i) # AbstractSiteType at site i
num_sublattices(lat) # Int, defaults to 1
sublattice(lat, i) # geometric sublattice id, defaults to 1The default implementations of site_type(lat, i) delegate to site_layout(lat), so a concrete lattice only needs to implement site_layout to get everything for free. num_sublattices and sublattice default to 1 — lattices with non-trivial geometric sublattices (honeycomb, kagome, Lieb) override them.