Cell complexes and link fields
Put states on the links and plaquettes of a lattice, or on the edges and faces of a network, as lattice gauge theories
and discrete exterior calculus do. A CellComplexSpace holds the sites, links, and plaquettes of a lattice, or the
nodes, edges, and faces of a network, with the oriented incidence between them. It is a NetworkSpace, so groups on
its 'edges' and 'hyperedges' hold states on links and plaquettes.
Cells of lattices and networks
Build the complex of a torus and of a tetrahedron, and read how their cells connect:
import networkx as nx
from skcomplex.core.components.spaces import CellComplexSpace, LatticeSpace, NetworkSpace
torus = CellComplexSpace(LatticeSpace((8, 8))).prepare()
[matrix.shape for matrix in torus.incidences_], (torus.incidences_[1] @ torus.incidences_[0]).count_nonzero()
tetrahedron = NetworkSpace(nx.complete_graph(4))
cliques = CellComplexSpace(tetrahedron, faces='cliques', dimension=3).prepare()
cliques.incidences_[2].shape
([(128, 64), (64, 128)], 0)
(1, 4)
- The links of a lattice point along the positive direction of their axis. Fixed and reflecting edges drop the cells that leave the box.
- A network takes
faces=Nonefor a minimum cycle basis,'hyperedges'for its hyperedges,'cliques'for its cliques up todimension, or a sequence of node cycles. incidences_[k]has one row per cell of degreek + 1and one column per cell of degreek, so the product of two consecutive ones is 0.
Exterior calculus
Write field equations on a complex with the operators of discrete exterior calculus. Here a potential on links follows lattice Maxwell equations, and their Gauss law holds to round-off:
from typing import Annotated
import numpy as np
from skcomplex.core.components.elements import Element, GroupElement
from skcomplex.core.components.interactions import RateInteraction
from skcomplex.core.components.laws.utils import codifferential, exterior_derivative
from skcomplex.core.components.spaces import CellComplexSpace, LatticeSpace
from skcomplex.core.components.system import System
from skcomplex.core.engine.runtime import Simulation
from skcomplex.core.specs import StateSpec
class FieldLinkElement(Element):
def __init__(
self,
label,
visual=None,
*,
A: Annotated[float, StateSpec()] = 0.0,
E: Annotated[float, StateSpec()] = 0.0,
):
super().__init__(label, visual=visual)
self.A = A
self.E = E
space = CellComplexSpace(LatticeSpace((8, 8))).prepare()
potential = np.random.default_rng(0).normal(size=128)
links = GroupElement('links', element=FieldLinkElement, space=space, on='edges', values={'A': potential})
interactions = [
RateInteraction('potential', target='links', state='A', rate=lambda n: -n.target.E),
RateInteraction('field', target='links', state='E', rate=lambda n: codifferential(exterior_derivative(n.target.A))),
]
system = System(elements=[links], interactions=interactions)
trajectory = Simulation(system, dt=0.1).prepare().run(until=2.0).trajectory_
bool(np.abs(space.incidences_[0].T @ trajectory.values('links.E').T).max() < 1e-10)
True
The law utilities exterior_derivative, codifferential, and hodge_star act on the states of groups on the cells of
a complex. The degree of their argument is the part its group lives on.
exterior_derivativemaps to the next degree, and applied twice it is exactly 0.codifferentialis(-1)^k star^-1 d^T staron a state of degreek. On an edge state it is the outflow minus the inflow at every site, and on a face state it is the curl on every edge. Applied twice it is 0 up to round-off.hodge_staris a diagonal factor. On a lattice it is the ratio of dual to primal cell volumes, and on a network it is the parameterweightof the group on that part of the complex.
With this sign, codifferential(exterior_derivative(x)) is the graph Laplacian of a site state, and the curl of the
curl of an edge state. A rate of RateInteraction reads another group on the complex through sources, and the value
must have the degree of the state it changes.
Coface reductions
Sum over the faces around an edge, or the edges around a site. neighbors(expression, over=CofaceNeighborhood())
reduces over the faces that contain an edge, or the edges that contain a site. Inside the reduction,
exterior_derivative is the oriented sum around the one coface, which is the holonomy of an abelian connection, and
every coface counts with weight one. The next example uses it for the Wilson action.
Sampling links
Sample a lattice gauge field at equilibrium, such as compact U(1) with the Wilson action, link by link:
from typing import Annotated
import numpy as np
from sage.all__sagemath_symbolics import cos, pi
from skcomplex.core.components.elements import Element, GroupElement
from skcomplex.core.components.interactions import SamplingInteraction
from skcomplex.core.components.laws.utils import exterior_derivative, neighbors
from skcomplex.core.components.spaces import CellComplexSpace, CofaceNeighborhood, DiscreteTime, LatticeSpace
from skcomplex.core.components.system import System
from skcomplex.core.engine.runtime import Simulation
from skcomplex.core.specs import StateSpec
class AngleLinkElement(Element):
def __init__(self, label, visual=None, *, theta: Annotated[float, StateSpec(period=2 * pi)] = 0.0):
super().__init__(label, visual=visual)
self.theta = theta
space = CellComplexSpace(LatticeSpace((8, 8))).prepare()
links = GroupElement('links', element=AngleLinkElement, space=space, on='edges')
sampling = SamplingInteraction(
'wilson',
target='links',
state='theta',
objective=lambda n: -2.0 * neighbors(cos(exterior_derivative(n.target.theta)), over=CofaceNeighborhood()),
move='rotate',
width=1.5,
schedule='colored',
)
system = System(elements=[links], interactions=[sampling], time=DiscreteTime())
states = Simulation(system, random_state=0).prepare().run(until=200).trajectory_.values('links.theta')
round(float(np.cos(space.incidences_[1] @ states[-50:].T).mean()), 1)
0.7
The last line is the mean plaquette, the average cosine of the oriented sum of the angles around every face, which lies between 0.6 and 0.8 here.
SamplingInteractionsamples a group on the edges of a cell complex with a local objective over cofaces.- Its
'colored'schedule updates together only links that share no face, and every draw comes from the generator of the simulation. - The move is
'rotate'for U(1), and'random-value'for Z_N with the'heat-bath'rule.
Matrix links
Hold a matrix of a compact group on every link, as the SU(2) and SU(3) gauge fields of the weak and strong forces do. A
MatrixGroupSpec names SU(n) or U(n), and a state declared with StateSpec(group=...) stores the 2 n^2 real
components of every matrix. The spec turns components into matrices and back, draws matrices from the Haar measure or
near the identity, projects rounded products back onto the group, and multiplies matrices along paths:
import numpy as np
from skcomplex.core.components.spaces import CellComplexSpace, LatticeSpace
from skcomplex.core.specs import MatrixGroupSpec
group = MatrixGroupSpec('SU', 3)
space = CellComplexSpace(LatticeSpace((4, 4))).prepare()
links = group.uniform_of(np.random.default_rng(0), 32)
plaquettes = group.holonomy_of(links, space.boundaries_, space.boundary_signs_)
plaquettes.shape, bool(np.allclose(np.linalg.det(plaquettes), 1.0))
((16, 3, 3), True)
boundaries_lists the links around every face in the cyclic order of its nodes, andboundary_signs_the sign of their traversal, soholonomy_ofgives the holonomy of every face, the ordered product of the link matrices with the inverse of a link traversed against its orientation. Any other path, such as a Wilson or a Polyakov loop, is a row of links and signs, padded with the sign 0.- The matrices do not commute, so the holonomy depends on the order of the links, and its trace on the start of the loop only up to a cyclic shift, which leaves it unchanged.
Sample matrix links with a HolonomyScoreSpec, a class function of the holonomy of every face, such as the real part of
the trace for the Wilson action:
from typing import Annotated
import numpy as np
from skcomplex.core.components.elements import Element, GroupElement
from skcomplex.core.components.interactions import SamplingInteraction
from skcomplex.core.components.spaces import CellComplexSpace, DiscreteTime, LatticeSpace
from skcomplex.core.components.system import System
from skcomplex.core.engine.runtime import Simulation
from skcomplex.core.specs import HolonomyScoreSpec, MatrixGroupSpec, StateSpec
group = MatrixGroupSpec('SU', 2)
class MatrixLinkElement(Element):
def __init__(self, label, visual=None, *, U: Annotated[object, StateSpec(group=group)] = tuple(group.identity)):
super().__init__(label, visual=visual)
self.U = U
def wilson(holonomies):
return -np.trace(holonomies, axis1=-2, axis2=-1).real
space = CellComplexSpace(LatticeSpace((8, 8))).prepare()
links = GroupElement('links', element=MatrixLinkElement, space=space, on='edges')
sampling = SamplingInteraction(
'wilson',
target='links',
state='U',
objective=HolonomyScoreSpec(wilson, group, hits=4),
move='rotate',
width=0.6,
schedule='colored',
)
system = System(elements=[links], interactions=[sampling], time=DiscreteTime())
states = Simulation(system, random_state=0).prepare().run(until=100).trajectory_.values('links.U')
loops = group.holonomy_of(group.matrices_of(states[-1]), space.boundaries_, space.boundary_signs_)
round(float(np.trace(loops, axis1=1, axis2=2).real.mean() / 2), 1)
0.5
The last line is the mean plaquette at beta = 2, close to I_2(2) / I_1(2) = 0.43.
- The move
'rotate'multiplies a link by a random matrix of the group near the identity of spreadwidth, and'random-value'draws a new one from the Haar measure. Every sweep proposeshitsMetropolis or Glauber moves per link and projects the links back onto the group. - The function receives the holonomies of the faces around a link, each started at the link, and the parameters it names, so it must be a class function such as a trace.
- A drive changes a matrix state as a whole, with the components of every link on a last axis, and receives the
complex by the keyword
spacewhen it names it.
The physics systems lattice_yang_mills and GaugeAnalysis with a group realize SU(2) and SU(3) lattice gauge theory
with its average plaquette, Wilson loops, and Polyakov loop.
Analysing complexes
Split a link field into its gradient, curl, and harmonic parts, count the holes of a complex, or measure how curved a gauge field is:
import numpy as np
from skcomplex.core.analysis import CellComplexAnalysis
from skcomplex.core.components.spaces import CellComplexSpace, LatticeSpace
torus = CellComplexSpace(LatticeSpace((8, 8))).prepare()
uniform = np.where(torus.link_axes_ == 0, 1.0, 0.0)
hodge = CellComplexAnalysis('hodge', space=torus).prepare(uniform).run()
CellComplexAnalysis('betti', space=torus).prepare().run().betti_, bool(np.allclose(hodge.harmonic_[0], uniform))
((1, 2, 1), True)
A uniform flow around the torus is purely harmonic, and the torus has one component, two independent loops, and one
cavity. CellComplexAnalysis reads a state of a group on a complex from a trajectory, or an array with the complex as
space.
- The kind
'hodge'setsgradient_,curl_, andharmonic_, which are orthogonal and sum to the state, and the site potentialpotential_. - The kind
'betti'setsbetti_. - The kind
'curvature'setsshare_, the share of faces whose oriented sum of an angle state is not a multiple of its period, andenergy_, the sum of one minus the cosine of the sums.
CellVisual, named VisualSpec('cell'), draws edge states as colored segments. It draws their face sums and
hyperedge states as an image on a lattice complex, or as polygons on a network complex, with a cyclic palette for angle
states.