The Caset C++ API

Caset is a package for simulating lattice spacetime interactions building on the notion of causal sets as well as causal simplicial complexes.

Below the C++ API is documented for users. To document for developers you can edit the Doxyfile and comment out exclusions to see the entire interface.

Core Spacetime

namespace caset

Enums

enum class SpacetimeType : uint8_t

Values:

enumerator CDT
enumerator REGGE
enumerator COSET
enumerator REGGE_PACHNER
enumerator GFT_SPIN_FOAM
enumerator RICCI_FLOW_DISCRETIZATION
class Spacetime
#include <Spacetime.h>

Spacetime

The Spacetime class provides methods to create and manipulate the basic building blocks of a simplicial complex \( \mathcal{K} \).

The Spacetime manages the simplicial complex structure, including vertices \( V \), edges \( E \), and simplices \( \{\sigma^k_i\} \) of varying dimensions. It is responsible for constructing and maintaining the topological relationships between these elements.

The Spacetime Topology is responsible for constructing Simplex(es) and the Topology (subclass) is responsible for building the complex to match that topology.

Any assertions or state needed by the Topology to build the complex should be implemented in the Simplex.

Public Functions

Spacetime()

Default constructor. Creates a 4D Lorentzian spacetime with CDT type and Toroid topology.

Spacetime(std::shared_ptr<Metric> metric_, SpacetimeType spacetimeType_, std::optional<double> alpha_, std::optional<double> a_, Foliation foliation_, std::optional<std::shared_ptr<Topology>> topology_)

Parameterized constructor.

Parameters:
  • metric_ – The metric tensor defining the signature and dimension

  • spacetimeType_ – The type of quantum gravity formulation (CDT, Regge, etc.)

  • alpha_ – The fundamental length scale coefficient, the number of times the length of a timelike edge is the length of a spacelike edge

  • a_ – The fundamental length unit for a spacelike edge.

  • foliation_ – The type of foliation for the spacetime. Preferred means spacelike slices are separated by timelike slices. None means they can be interspersed.

  • topology_ – The topology of the spatial slices (default: Toroid)

std::pair<SimplexPtr, bool> createSimplex(const VertexPtrs &vertices)
std::pair<SimplexPtr, bool> createSimplex(const VertexPtrs &vertices, const Edges &edges)

Creates a simplex \( \sigma^k \) from explicit vertices and edges.

Parameters:
  • vertices – The vertices \( \{v_0, \ldots, v_k\} \) of the simplex

  • edges – The edges \( \{e_{ij}\} \) connecting the vertices

Returns:

{simplex, wasCreated} pair where wasCreated=true if newly created, false if already existed

std::pair<SimplexPtr, bool> createSimplex(const std::tuple<uint8_t, uint8_t> &numericOrientation)

Creates a simplex \( \sigma^{(t_i, t_f)} \) with the given causal orientation.

Parameters:

numericOrientation – Tuple (timelike_initial, timelike_final) defining the orientation

Returns:

{simplex, wasCreated} pair where wasCreated=true if newly created

std::pair<SimplexPtr, bool> createSimplex(std::size_t k)

Creates a k-simplex with randomly positioned vertices and edges of length \( \alpha \).

Parameters:

k – The dimension of the simplex (k+1 vertices)

Returns:

{simplex, wasCreated} pair where wasCreated=true if newly created

VertexPtr createVertex() noexcept

Creates a vertex \( v \) with the next available ID at the current time.

Returns:

Shared pointer to the created vertex

VertexPtr createVertex(const std::vector<double> &coords) noexcept
VertexPtr createVertex(std::uint64_t id) const noexcept

Creates a vertex \( v \) with the given ID at the current time.

Parameters:

id – The unique identifier for the vertex

Returns:

Shared pointer to the created vertex

VertexPtr createVertex(std::uint64_t id, const std::vector<double> &coords) const noexcept

Creates a vertex \( v \) with the given ID and coordinates \( (t, x^1, \ldots, x^{n-1}) \).

Parameters:
  • id – The unique identifier for the vertex

  • coords – The spacetime coordinates of the vertex

Returns:

Shared pointer to the created vertex

EdgePtr createEdge(const VertexPtr &src, const VertexPtr &tgt) const noexcept

Creates an edge \( e = (v_s, v_t) \) with squared length computed from the metric.

The squared length is \( \ell^2 = g_{\mu\nu}(x^t - x^s)^\mu(x^t - x^s)^\nu \).

Parameters:
  • src – The source vertex \( v_s \)

  • tgt – The target vertex \( v_t \)

Returns:

Shared pointer to the created edge

EdgePtr createEdge(const VertexPtr &src, const VertexPtr &tgt, double squaredLength) const noexcept

Creates an edge \( e = (v_s, v_t) \) with explicit squared length.

Parameters:
  • src – The source vertex \( v_s \)

  • tgt – The target vertex \( v_t \)

  • squaredLength – The squared length \( \ell^2 \) of the edge

Returns:

Shared pointer to the created edge

void build(int numSimplices = 3)

Builds an n-dimensional (depending on your metric) triangulation/slice for t=0 with edge lengths equal to alpha matching the chosen topology.

The default Topology is Toroid.

Constructs an initial simplicial complex \( \mathcal{K}_0 \) by iteratively gluing \( n \) simplices.

Parameters:

numSimplices – The number of simplices to add to the initial complex

std::size_t getSimplexCount() const noexcept
Returns:

Total number of top-dimensional ( \( d \)-) simplices, i.e., the four-volume \( N_4 = N_{41} + N_{32} \) in 4D CDT.

std::size_t getVertexCount() const noexcept
Returns:

Number of vertices \( N_0 \) in the triangulation. In the Regge action this appears as \( -(k_0 + 6\Delta)\, N_0 \).

std::size_t getN41() const noexcept
Returns:

Count of \((d,1) + (1,d)\)-type simplices \( N_{41} \). These simplices have \( d \) vertices on one time slice and 1 on the adjacent slice.

std::size_t getN32() const noexcept
Returns:

Count of \((d\!-\!1, 2) + (2, d\!-\!1)\)-type simplices \( N_{32} \). These simplices have their vertices split \((d\!-\!1, 2)\) across adjacent slices.

const SimplexSet &getSimplices() const noexcept
Returns:

Const reference to the full simplex set \( \mathcal{K} \) (all simplices of all dimensions registered in the complex).

SimplexPtr getRandomSimplex()

Select a uniformly random simplex from the parallel access vector.

Used by the Metropolis algorithm to pick random move targets.

Returns:

A random simplex, or nullptr if the complex is empty

SimplexPtr getRandomSimplexWithOrientation(uint8_t ti, uint8_t tf)

Select a uniformly random simplex with a specific causal orientation.

Tries random sampling first (fast when many match), then falls back to a linear scan.

Parameters:
  • ti – Number of vertices on the initial time slice

  • tf – Number of vertices on the final time slice

Returns:

A matching simplex, or nullptr if none exist

void removeSimplex(const SimplexPtr &simplex)

Fully remove a \( d \)-simplex from the complex: unregister it from the simplex set, remove it from each constituent vertex’s simplex list, and clean up coface references in its facets.

Used by CDT Pachner moves.

Parameters:

simplex – The simplex \( \sigma \) to remove

SpacetimeType getSpacetimeType() const noexcept
Returns:

The type of quantum gravity formulation (CDT, Regge, etc.)

double getCurrentTime() const noexcept
Returns:

The current time coordinate \( t \) for time-slicing

std::shared_ptr<EdgeList> getEdgeList() const noexcept
Returns:

The edge list \( E \) containing all edges in the complex

std::shared_ptr<Metric> getMetric() const noexcept
Returns:

The metric tensor \( g_{\mu\nu} \) defining the geometry

std::shared_ptr<VertexList> getVertexList() const noexcept
Returns:

The vertex list \( V \) containing all vertices in the complex

SimplexSet getExternalSimplices() noexcept
Returns:

Simplices around the boundary of the simplicial complex. These simplices have at least one external face. They will tend to be in order of orientation (e.g. (4, 1) and (3, 2) for 4D CDT). Note that this method does not return 2-simplices as you might expect, but 5-simplices since those are the standard building blocks. You can get the 2-simplices by calling getFacets() on the 5-simplices and their facets until \( k=2 \).

SimplexSet getSimplicesWithOrientation(std::tuple<uint8_t, uint8_t> orientation)

Retrieves all simplices with a specific causal orientation.

Note

This method is for testing only and has poor runtime performance.

Parameters:

orientation – The orientation tuple (timelike_initial, timelike_final)

Returns:

Set of simplices \( \{\sigma^{(t_i, t_f)}\} \) with the given orientation

Foliation getFoliation() const noexcept

Returns the foliation of the spacetime.

Either NONE or PREFERRED. With PREFERRED foliation; each spatial slice lies between a time slice and vis versa. Otherwise they can be any-which-a-way.

Returns:

Foliation::PREFERRED or Foliation::NONE.

std::vector<VertexPtrs> getConnectedComponents() const

Computes the connected components of the vertex graph.

Uses depth-first search to identify connected components in the graph \( G = (V, E) \) where vertices are connected by edges.

Returns:

Vector of connected components, each containing a list of vertices

SimplexPtr getSimplex(SimplexPtr simplex) const

Retrieves a simplex from the complex by pointer lookup.

Parameters:

simplex – The simplex to find

Returns:

The canonical simplex from the complex, or nullptr if not found

SimplexPtr getSimplex(std::uint64_t fingerprint) const

Retrieves a simplex from the complex by fingerprint.

Parameters:

fingerprint – The fingerprint hash \( h(\sigma) \) of the simplex

Returns:

The simplex with the given fingerprint, or nullptr if not found

double incrementTime() noexcept

Increments the current time coordinate.

Returns:

The new current time \( t \leftarrow t + 1 \)

bool removeIfIsolated(const VertexPtr &vertex) const noexcept

Removes a vertex if it has no incident edges.

Checks if \( \deg(v) = 0 \) and removes \( v \) from the vertex list if so.

Parameters:

vertex – The vertex \( v \) to potentially remove

Returns:

true if the vertex was removed, false if it has incident edges

void addObservable(const std::shared_ptr<Observable> &observable)

Adds an observable to track during evolution.

Observables are measured after each update step to monitor the system’s properties.

Parameters:

observable – The observable \( \mathcal{O} \) to track

SimplexPtr registerSimplex(const SimplexPtr &simplex, bool internal)

Registers a simplex in the spacetime’s internal data structures.

Adds the simplex to:

  • The main simplex set \( K \)

  • The appropriate orientation-indexed sets (external or internal)

Parameters:
  • simplex – The simplex \( \sigma \) to register

  • internal – true if the simplex is fully internal (all faces glued), false otherwise

Returns:

The registered simplex

void reserve(int nSimplices)
double getAlpha() const noexcept

Alpha is the coefficient that determines the ratio of timelike edge lengths to space like edge lengths.

That relationship is

\[ l_s = +a \]
and
\[ l_t = - \alpha a \]

Returns:

The coefficient representing the number of times the timelike length is compared to the spatial length.

double getA() const noexcept

a is the coefficient that sets the fixed edge length for spacelike edges according to

\[ l_s = +a \]
for spacelike edges and
\[ l_t = - \alpha a \]

for timelike edges.

Returns:

The constant spacelike edge length.

void unregisterSimplex(const SimplexPtr &simplex)

Unregisters a simplex from the spacetime’s internal data structures.

Removes the simplex from all indexed sets. This should be called before modifying a simplex’s fingerprint to avoid hash table corruption.

Parameters:

simplex – The simplex \( \sigma \) to unregister

Private Functions

void updateOrientationCounters(const SimplexPtr &simplex, int delta)

Private Members

std::shared_ptr<EdgeList> edgeList = std::make_shared<EdgeList>()
std::shared_ptr<VertexList> vertexList = std::make_shared<VertexList>()
IdType vertexIdCounter = 0
SpacetimeType spacetimeType
double alpha = 1.
double a = 1.
Foliation foliation = Foliation::PREFERRED
std::shared_ptr<Metric> metric
std::shared_ptr<Topology> topology
std::uint64_t currentTime = 0
SimplexSet simplices = {}
std::vector<SimplexPtr> simplicesVec = {}
std::unordered_map<std::uint64_t, std::size_t> simplexVecIndex = {}
std::size_t n41Count = 0
std::size_t n32Count = 0
std::mt19937 rng = {std::random_device{}()}
std::unordered_map<SimplexOrientation, SimplexSet, SimplexOrientationHash, SimplexOrientationEq> externalSimplicesByFacialOrientation = {}

These are simplices on the boundary of a simplicial complex.

They have at least one external face, and hence can be glued to other simplices. The externalSimplices are organized by the orientation of their available faces. If a face is available; the orientation of that face can be found as a key corresponding to a SimplexSet containing the Simplex to which that Face belongs.

This makes for fast lookups when gluing simplices together to form a complex.

std::vector<std::shared_ptr<Observable>> observables = {}
namespace caset
class Metric
#include <Metric.h>

The Metric

Public Functions

Metric(bool coordinateFree_, Signature &signature_)
double getSquaredLength(const std::vector<double> &sourceCoords, const std::vector<double> &targetCoords) const

This method computes the length of the edge between the source and target vertices when we’re using a coordinate system/euclidean metric.

This uses the metric, \( g_{\mu \nu} \), to compute the distance between vertex \( i \) and vertex \( j \) as

\[ l_{ij}^2 = g_{\mu \nu} \Delta x^{\mu} \Delta x^{\nu} \]

where

\[ \Delta x^{\mu} := x_i^{\mu} - x_j^{\mu} \]

with signature (-,+,+,+).

Timelike edges will have negative squared lengths, spacelike edges positive squared lengths, and null/lightlike edges zero squared lengths.

Note that the CDT (Causal Dynamical Triangulations) approach typically uses fixed length spacelike edges to build (and update) the triangulation while Regge Calculus allows for dynamically updated edge lengths. See Quantum Gravity from Causal Dynamical Triangulations: A Review by R. Loll Section 4, p 11-12 for more details.

std::shared_ptr<Signature> getSignature() const noexcept

Private Members

std::shared_ptr<Signature> signature
bool coordinateFree
namespace caset

Enums

enum class SignatureType : uint8_t

The signature type of the spacetime metric tensor \( g_{\mu\nu} \).

  • Lorentzian \((-,+,+,\ldots,+)\): physical Minkowski-type signature with one timelike and \( d-1 \) spacelike dimensions. Used in CDT.

  • Euclidean \((+,+,+,\ldots,+)\): all positive signature, obtained after Wick rotation \( t \to -i\tau \). Used in Euclidean quantum gravity.

Values:

enumerator Lorentzian
enumerator Euclidean
class Signature
#include <Signature.h>

Metric Signature

Encodes the diagonal of the metric tensor \( g_{\mu\nu} \) for a \( d \)-dimensional spacetime. The signature determines which edges are timelike (negative squared length) and which are spacelike (positive).

For a 4D Lorentzian spacetime, the diagonal is \( (-1, +1, +1, +1) \), giving the Minkowski metric

\[ ds^2 = g_{\mu\nu}\, dx^\mu\, dx^\nu = -c^2\, dt^2 + dx^2 + dy^2 + dz^2 \]

with \( c = 1 \) in natural units.

Public Functions

Signature(int dimensions_, SignatureType signatureType_)
Parameters:
  • dimensions_ – The spacetime dimension \( d \) (typically 2, 3, or 4)

  • signatureType_ – Lorentzian \((-,+,\ldots)\) or Euclidean \((+,+,\ldots)\)

std::vector<int> getDiagonal() const noexcept
Returns:

The diagonal entries of \( g_{\mu\nu} \), e.g., \(\{-1, 1, 1, 1\}\) for 4D Lorentzian.

int getDimensions() const noexcept
Returns:

The spacetime dimension \( d \).

SignatureType getSignatureType() const noexcept
Returns:

The signature type (Lorentzian or Euclidean).

Private Members

std::vector<int> diag
int dimensions
SignatureType signatureType

Private Static Attributes

static const double c = 1.

Simplicial Complex

namespace caset
class Simplex : public std::enable_shared_from_this<Simplex>
#include <Simplex.h>

Simplex Class

A simplex is a generalization of the concept of a triangle or tetrahedron to arbitrary dimensions. Each simplex is defined by its vertices.

Each simplex has a volume \( V_s \), which can represent various physical properties depending on the context.

A k-simplex, \( \sigma^k \), within a simplicial complex, \( K \) is defined as a set of k+1 vertices. Simplicial complex construction is a bit of a bottleneck in simulation of spacetime. At the moment; we declare some vertices, then use coning to create a Simplex from those vertices. Those vertices are passed to the Simplex along with the edges used to connect them as a performance optimization.

Most of the time building the simplicial complex is spent calculating facets from all subsets of Simplex Vertices. A faster method for building the complex would be to avoid computing those vertices and edges; and just compute the simplex as an abstraction with faces, cofaces, and an orientation. We’ll leave this for a “Version 2 feature”.

Public Functions

explicit Simplex(Spacetime *spacetime_, const VertexPtrs &vertices_, Edges edges_)
Parameters:

vertices_

Simplex(Spacetime *spacetime_, const VertexPtrs &vertices_, Edges edges_, const SimplexOrientation &orientation_)
inline std::uint64_t size() const noexcept
void initialize(const std::shared_ptr<Simplex> &simplex)

The initialize step is necessary because the canonical owner of the Simplex object is the Spacetime, and ideally that canonical owner is the only one to permanently hang onto a std::shared_ptr.

So when we initialize with this method we add the std::shared_ptr<Simplex> (aka SimplexPtr) to all the Vertex (es) that are members of the Simplex. Again; we define a Simplex abstractly as a set of vertices with a time orientation. When you construct a Spacetime which can abstractly be considered a Simplicial complex; having access to the Simplex by Vertex is pretty handy for bookkeeping.

inline std::string toString() const noexcept
inline const SimplexOrientation &getOrientation() const noexcept

Each simplex has an associated orientation in the case you’re preserving causality with your work.

You can find specifics of the SimplexOrientation abstractly and concretely/computationally in the documentation for the SimplexOrientation

inline double getTi() const noexcept

The earliest time assigned to a vertex in this Simplex.

Returns:

ti for the Simplex.

inline double getTf() const noexcept

The latest time assigned to a vertex in this Simplex.

Returns:

tf for the Simplex.

const VertexPtrs &getVertices() const noexcept
Returns:

A list of Vertex (es) in traversal order. You can iterate these to walk the Face.

bool hasVertex(const VertexPtr &vertex) const

This method is self-explanatory. O(1) lookups for who has what.

VertexIdMap getVertexIdLookup() const noexcept

This method produces a lookup table \( Id \rightarrow Vertex \).

The only place it’s used at the moment is for verifying state in our Python unit tests.

const Edges &getEdges() const
Returns:

Edges in traversal order (the order of input vertices).

std::size_t getNumberOfEdges() const
bool hasEdge(const EdgePtr &edge) const

This method computes Edge (s) of the Simplex in traversal order.

Note that the edges are effectively undirected since it can point either way as the direction relates to vertex order. So it’s possible for e.g. vertices \( \{v_0, v_1, v_2\} \) to correspond to edges \( \{ e_{0 \rightarrow 1}, e_{2 \rightarrow 1}, e_{2 \rightarrow 0} \} \)

bool hasEdge(const VertexPtr &vertexA, const VertexPtr &vertexB) const
bool hasEdgeContaining(IdType vertexId) const
std::size_t getNumberOfFaces(std::size_t j) const

A k-simplex is the convex hull of k + 1 affinely independent points.

Each has faces of all dimensions from 0 up to k–1. A k-1 simplex is called a Facet.

A j-face is a j-simplex incorporating a subset (of size j) of the k-simplex vertices.

The number of j-faces ( \( \sigma^j \) ) of a k-simplex \( \sigma^k \) is given by

\[ \binom{k+1}{j+1} \]

And the total number of faces of all dimensions is \( \sum_{j=0}^{k-1} \binom{k+1}{j+1} = 2^{k+1} - 2 \)

const Simplices &getFacets()

A Face, \( \sigma^{k-1} \subset \sigma^{k} \) of a k-simplex \( \sigma^k \) is any k-1 simplex contained by the k-simplex.

To attach one Simplex \( \sigma_i^k \) to another \( \sigma_j^k \), we define the respective faces \( \sigma_i^{k-1} \) and \( \sigma_j^{k-1} \) at which they should be attached. The orientation is determined by the orientation of those respective Simplexes.

The Facets are the \( \sigma^{k-1} \subset \sigma^{k} \) faces on which we’ll most commonly join two simplices to form a simplicial complex \( K \).

Returns:

all k-1 simplices contained within this k-simplex.

bool hasFacets() const
bool hasStoredFacet(const SimplexPtr &facet)
void addCoface(const std::shared_ptr<Simplex> &simplex)

A simplex, \( \sigma \in K \) with vertices \( V_{\sigma} \) is a coface of \( \tau \in K \) with vertices \( V_{\tau} \) iff \( V_{\tau} \subset V_{\sigma} \).

For our purposes, however, we confine cofaces to those of dimensionality \( k+1 \) compared to the facet of dimension \( k \)

We define a facet as a set of shared vertices. The facet of any given k-simplex \( \sigma^k \) is a k-1 simplex, such that \( \sigma_{k} \) is a coface of \( \sigma_{k-1} \).

Register a \((k\!+\!1)\)-simplex as a coface of this \( k \)-simplex. The coface relation encodes the incidence structure of the simplicial complex: \( \sigma^{k+1} \) is a coface of \( \sigma^k \) iff \( \sigma^k \subset \sigma^{k+1} \) (the lower-dimensional simplex is a face of the higher-dimensional one).

void removeCoface(const std::shared_ptr<Simplex> &simplex)

Unregister a coface from this simplex.

Called during simplex removal in Pachner moves to maintain consistent coface bookkeeping.

bool isCofaceTo(const SimplexPtr &simplex, bool shallow = true) const

Check whether this simplex is a coface of the given facet, i.e., whether all vertices of the facet are contained in this simplex.

Parameters:
  • facet – The candidate lower-dimensional simplex

  • shallow – If true, also require the dimension difference to be exactly 1

bool hasCoface(const std::shared_ptr<Simplex> &simplex) const
const Simplices &getCofaces() const noexcept

Co-faces are maintained as state rather than computed on the fly.

This means any time a Simplex is attached to another Simplex; it must be added to the face at which it’s attached as a co-face. If a Simplex, Edge, or Vertex within that Face is removed at any point; that effect should cascade up the ownership tree, which goes

\[ Vertex \subset Edge \subset Simplex \subset Spacetime \]

Returns:

The set of k-simplices that share this face.

std::size_t maxKPlusOneCofaces() const

This method computes the maximum number of k+1 co-faces that can be joined to this k-Simplex in general.

Do not use this method the purpose of causal gluing in CDT. It would create internal/non-manifold simplices and hence violate causality. If that’s your goal then you want to use isCausallyAvailable

For a given k-simplex \( \sigma^k \), a co-face is defined as an m-simplex, \( \sigma^m \) such that \( m \gt k \) and \( \sigma^k \subset \sigma^m \). The maximum number of co-faces that can be joined to a k-simplex is in general unbounded, but for our purposes we set it to the number of faces of the simplex, so we impose the constraint that the coface no be generally \( m \gt k \), but exactly \( k + 1 \), so \( m = k + 1 \).

This can be confusing because for the purpose of causally gluing simplices we look at a face, \( \sigma^k \) of the (k+1)-simplex, \( \sigma^{k+1} \) where to that (k-) face we want to glue another (k+1)-simplex on one of it’s k-faces. So the maximum number of co-faces that can be joined to a k-simplex is the number of faces of that simplex.

Returns:

inline bool isTimelike() const noexcept
bool isCausallyAvailable() const noexcept

This method just returns whether or not the simplex has fewer than 2 co-faces. If it does; then it is available.

bool hasCausallyAvailableFacet()

This method iterates over all faces of this Simplex; and counts the number of co-faces for each face.

If a face has fewer than 2 co-faces; it’s available to glue. We limit to 2 co-faces because we want to preserve manifoldness. There’s nothing wrong with internal simplicies from the perspective of simplicial algebra, but there is from the perspective of relativity.

Returns:

Whether or not this Simplex is available to glue. A face is only available when it has less than 2 co-faces.

bool isInternal() const noexcept
std::uint64_t hash() const noexcept
template<typename T>
T binomial(unsigned n, unsigned k) const
bool addEdge(const EdgePtr &edge)
bool removeEdge(const EdgePtr &edge)
std::pair<SimplexPtr, Simplices> cone(VertexPtr &vertex)

If you’re working in a 3-complex (tetrahedrons), \( K \) this method should be appropriately called on a 2-simplex (a triangle), \( \sigma^2 \) or in general for a given k-complex, \( K \) you should just be calling this method on simplices of dimension k-1.

It creates a new k-simplex by writing drawing edges from each vertex of this Facet to the new vertex. This creates a new k-simplex with a shared face (this Simplex!) in effectively O(1) time.

Parameters:

vertex – A new, standalone, orphaned vertex with no existing edges or associated simplices.

Returns:

A pair of {simplex, facets}; The new k-simplex created by coning vertex to this facet and a vector of new exterior facets resulting from the new simplex.

void validate() const
bool operator==(const Simplex &other) const noexcept
bool operator==(const std::shared_ptr<Simplex> &other) const noexcept
bool replaceVertex(const VertexPtr &oldVertex, const VertexPtr &newVertex)

Returns the hinges of the simplex.

A hinge is a simplex contained within a higher dimensional simplex. The hinge is one dimension lower than the “parent” simplex. For a 4-simplex, \( \sigma = {v_0, ..., v_4} \) there are 10 edges and 10 triangular hinges. In this case a hinge is any triangle \( {v_i, v_j, v_k} \). There are \( \binom{5}{3} = 10 \) such triangles.

The curvature at the hinge is the deficit angle. Assuming the simplex is a hinge; returns the deficit angle associated with the hinge.

The deficit angle is given by:

\[ \epsilon = 2 \pi - \sum_{\sigma \supset h} \theta_h^{(\sigma)} \]

\( \theta_h^{(\sigma)} \) is the 4D dihedral angle between the two tetrahedral faces of simplex \( \sigma \) meeting along triangle (hinge) \( h \).

Or in english; the deficit angle is equal to \( 2 \pi \) minus the sum of the 4D dihedral angle of each simplex between the two tetrahedral faces meeting along triangle \( h \).

When the hinge is exterior/on a boundary; the \( 2 \pi \) is replaced with \( \pi \). Compute dihedral angles from edge lengths.

Let \( C \) be the cofactors of \( G \), \( C = cof(G) \) (a matrix of cofactors). Then the dihedral angle between the two tetrahedral faces opposite vertices \( i \) and \( j \) is given by:

\[ cos(\theta_{ij}) = - \frac{C_{ij}}{\sqrt{C_{ii} C_{jj}}}, i \neq j, i, j \in {0, ..., n} \]

Map \( (i, j) \) to the hinge (triangle for a 4-simplex) opposite that pair. This method replaces the vertex only, Edge (s) should be replaced by the Spacetime, because it maintains the global lookup for Edge (s). If the Edge source/target is replaced; it’s not enough to update the Edge, since squaredLength data could be lost.

WARNING: This Simplex must be removed from it’s containers prior to calling this method. NOT removing it from it’s containers first (and adding back in after) results in UNDEFINED BEHAVIOR!

Parameters:
  • oldVertex – The Vertex to replace

  • newVertex – The vertex with which to replace it.

Returns:

bool isInitialized() const noexcept

Public Members

Fingerprint fingerprint = {}
bool initialized = {false}

Public Static Functions

static std::shared_ptr<Simplex> create(Spacetime *spacetime_, const VertexPtrs &vertices_, const Edges &edges_)
static std::shared_ptr<Simplex> create(Spacetime *spacetime_, const VertexPtrs &vertices_, const Edges &edges_, const SimplexOrientation &orientation_)
static std::size_t computeNumberOfEdges(std::size_t k)
static void registerToVertices(const SimplexPtr &simplex)

Private Members

Spacetime *spacetime = {nullptr}
SimplexOrientation orientation = {}
VertexIdToIndex vertexIdToIndex = {}
VertexIndexToId vertexIndexToId = {}
VertexPtrs vertices = {}
Edges edges = {}
Simplices facets = {}
Simplices cofaces = {}
bool _isTimelike
double ti = {std::numeric_limits<double>::max()}
double tf = {-std::numeric_limits<double>::max()}
namespace caset

Enums

enum class TimeOrientation : uint8_t
Param timeOrientation:

Values:

enumerator FUTURE
enumerator PRESENT
enumerator UNKNOWN
class SimplexOrientation

Public Functions

SimplexOrientation(uint8_t ti_, uint8_t tf_)

The orientation of a simplex is determined by how many vertices lie on the initial and final time slice for the simplex.

The orientation is largely only relevant for Lorentzian/CDT complexes where causality is preserved. Those complexes restrict to allowed orientations that ensure progression forward in time and “fit together” (so they share faces without gaps in the complex).

The convention was established in Ambjorn-Loll’s “Causal Dynamical Triangulations” paper from 1998-2001. Every d-simplex must have its vertices split across two adjacent time slices, t and t+1. That means every simplex has a split

\( (n, d + 1 - n) \)

Parameters:
  • ti_ – The number of vertices on the initial time slice.

  • tf_ – The number of vertices on the final time slice.

SimplexOrientation()
SimplexOrientation decTf() const
SimplexOrientation decTi() const
SimplexOrientation flip() const
TimeOrientation getOrientation() const
std::pair<uint8_t, uint8_t> numeric() const
std::string toString() const noexcept
std::vector<SimplexOrientation> getFacialOrientations() const
uint8_t getK() const

A k-simplex has \( k+1 \) vertices.

size_t hash() const

A k-simplex has \( k+1 \) vertices.

bool operator==(const SimplexOrientation &other) const noexcept

Public Members

Fingerprint fingerprint

Public Static Functions

static SimplexOrientation orientationOf(const VertexPtrs &vertices)

Private Members

uint8_t ti = {0}
uint8_t tf = {0}
uint8_t k = {0}
namespace caset
class Vertex : public std::enable_shared_from_this<Vertex>
#include <Vertex.h>

Represents a vertex in a causal set (causet) spacetime discretization.

Physical Context

In lattice gauge theory, vertices represent discrete points in spacetime where gauge fields and matter fields are defined. The coupling parameters at each vertex determine the strength of interactions:

  • Strong Force: Described by quantum chromodynamics (QCD) with running coupling \( \alpha_s(Q^2) \) that varies with energy scale \( Q^2 \)

  • Weak Force: Governed by the electroweak coupling \( g_W \)

  • Electromagnetic Force: Characterized by the fine structure constant \( \alpha_{EM} \approx 1/137 \)

A key challenge is that QCD exhibits asymptotic freedom: the coupling becomes weaker at high energies (short distances) and stronger at low energies (long distances), preventing perturbative calculations in the infrared regime. This is modeled through “running coupling” theories.

Gauge Invariance

Observables in gauge theory must be gauge-invariant. The electromagnetic 4-potential \( A_\mu \) is gauge-variant and thus not directly observable. However, field strengths \( F_{\mu\nu} = \partial_\mu A_\nu - \partial_\nu A_\mu \) are gauge-invariant observables.

Implementation Details

This class represents vertices in a directed graph structure where:

  • Edges have direction (source → target) to represent causal relationships

  • Vertices can have coordinates in arbitrary dimensions (though time calculation has constraints)

  • Each vertex maintains bidirectional edge lists (incoming and outgoing)

  • Simplices are registered to their constituent vertices for efficient topology queries

The vertex class uses shared_from_this to enable safe shared_ptr creation from member functions.

Public Functions

Vertex() noexcept

Default constructor creating a vertex with ID 0.

Vertex(const std::uint64_t id_, const std::vector<double> &coords) noexcept

Construct vertex with ID and spatial coordinates.

Creates a vertex with specified coordinates. The dimensionality is determined by coords.size():

  • 1D: Time is \( |x_0| \)

  • 4D+: Time is \( \sqrt{\sum_{i=0}^{N-1} x_i^2} \)

  • 2D, 3D: Invalid - getTime() will throw std::out_of_range

Parameters:
  • id_ – Unique identifier for this vertex

  • coords – Position in spacetime (arbitrary dimension)

explicit Vertex(const std::uint64_t id_) noexcept

Construct coordinate-independent vertex with ID only.

Creates a vertex without coordinate information. Calling getCoordinates() on such a vertex will throw std::runtime_error.

Parameters:

id_ – Unique identifier for this vertex

std::uint64_t getId() const noexcept

Get the unique identifier of this vertex.

Returns:

The vertex ID

double getTime() const

Compute the temporal coordinate in arbitrary dimensions.

Mathematical Definition

The time coordinate is computed based on coordinate dimensionality:

  • 0D (empty): Returns 0

  • 1D: \( t = |x_0| \)

  • 4D+: \( t = \sqrt{\sum_{i=0}^{N-1} x_i^2} \) (Euclidean norm)

  • 2D, 3D: Throws std::out_of_range (unsupported)

Rationale

In standard 4D Minkowski spacetime, we typically separate time and space. For higher-dimensional theories (e.g., Kaluza-Klein, string theory), this convention uses the Euclidean magnitude across all temporal dimensions, with spatial dimensions handled separately by the embedding geometry.

Throws:

std::out_of_range – if coordinate vector has length 2 or 3

Returns:

The time coordinate

void setTime(double time) noexcept
std::vector<double> getCoordinates() const

Get the coordinate vector for this vertex.

Usage Notes

Not all vertices need coordinates - some algorithms work purely with combinatorial structure. Only call this if you’re certain the vertex has coordinate data.

Throws:

std::runtime_error – if vertex is coordinate-independent (empty coordinates)

Returns:

Vector of coordinate values

void setCoordinates(const std::vector<double> &coords) noexcept

Set new coordinates for this vertex.

This operation does not update any cached values in edges or simplices. Use with caution if edge lengths depend on coordinates.

Parameters:

coords – New coordinate vector

std::size_t degree() const noexcept

Get the total degree (number of incident edges)

For a directed graph, this returns \( \deg(v) = \deg^-(v) + \deg^+(v) \)

Returns:

Sum of in-degree and out-degree

EdgePtr getEdge(const EdgePtr &edge)

Find a specific edge incident to this vertex.

Searches both inEdges and outEdges. Useful for verifying edge membership without needing to know direction.

Parameters:

edgeEdge to search for (compared by ID)

Returns:

Shared pointer to the edge if found, nullptr otherwise

EdgePtrSet getEdges() const noexcept

Get all incident edges (both incoming and outgoing)

The returned set is a copy. Complexity: O(|inEdges| + |outEdges|)

Returns:

Set containing all edges where this vertex is source or target

EdgePtrSet getInEdges() const noexcept

Get all edges targeting this vertex.

Returns:

Set of incoming edges \( \{e \mid e.target = v\} \)

EdgePtrSet getOutEdges() const noexcept

Get all edges originating from this vertex.

Returns:

Set of outgoing edges \( \{e \mid e.source = v\} \)

void addInEdge(const EdgePtr &edge) noexcept

Add an incoming edge to this vertex.

Caveat: Does not verify that edge->getTarget() == this. Caller must ensure consistency.

Parameters:

edgeEdge where this vertex is the target

void addOutEdge(const EdgePtr &edge) noexcept

Add an outgoing edge from this vertex.

Caveat: Does not verify that edge->getSource() == this. Caller must ensure consistency.

Parameters:

edgeEdge where this vertex is the source

SimplexPtrSet removeInEdge(const EdgePtr &edge) noexcept

Remove an incoming edge and update all affected simplices.

Implementation Details

  1. Removes the edge from all simplices that contain it via Simplex::removeEdge()

  2. Removes the edge from this vertex’s inEdges set

  3. Returns affected simplices for caller to handle (e.g., re-validation)

Assertions: When CASET_ASSERTIONS is defined:

  • Aborts if edge is nullptr

  • Aborts if edge is not in inEdges

Parameters:

edge – The edge to remove from inEdges

Returns:

Set of simplices that contained this edge (now modified)

SimplexPtrSet removeOutEdge(const EdgePtr &edge) noexcept

Remove an outgoing edge and update all affected simplices.

Symmetric to removeInEdge() but operates on outEdges.

Assertions: When CASET_ASSERTIONS is defined:

  • Aborts if edge is nullptr

  • Aborts if edge is not in outEdges

Parameters:

edge – The edge to remove from outEdges

Returns:

Set of simplices that contained this edge (now modified)

const SimplexPtrSet &getSimplices() const noexcept

Get all simplices that contain this vertex.

A vertex belongs to a simplex if it’s one of the simplex’s vertices. This is the inverse relationship: vertex → simplices containing it.

Returns:

Set of simplices where this vertex is a constituent

bool addSimplex(const SimplexPtr &simplex)

Register a simplex as containing this vertex.

Duplicate Detection

Assertions: When CASET_ASSERTIONS is defined:

  • Checks for null simplex pointer

  • Checks for null ‘this’ pointer

  • Calls checkDuplicates() before and after insertion

  • Aborts on any violation

This bidirectional relationship (SimplexVertex) must be maintained consistently: when a simplex is created with vertices, it must call addSimplex() on each vertex.

Parameters:

simplex – The simplex to add

Returns:

true if simplex was newly added, false if already present

bool removeSimplex(const SimplexPtr &simplex)

Unregister a simplex from this vertex.

Called during simplex destruction or vertex replacement operations. Removes the simplex from the internal simplices set.

Parameters:

simplex – The simplex to remove

Returns:

true if simplex was removed, false if not found

void checkDuplicates(std::string msg) const

Debug utility to detect duplicate simplices.

Assertions Only: Only performs checks when CASET_ASSERTIONS is defined. Scans all registered simplices and checks for duplicate fingerprints. Logs at CRITICAL_LEVEL and throws std::runtime_error if duplicates exist.

Parameters:

msg – Error message to log/throw if duplicates found

std::pair<EdgePtrSet, EdgePtrSet> moveEdgesTo(const VertexPtr &vertex, Spacetime *spacetime)

Move all edges (both in and out) to another vertex.

Algorithm

For each edge connected to this vertex:

  1. Remove edge from source/target vertex’s edge lists

  2. Remove edge from spacetime’s edge registry

  3. Create new edge with ‘vertex’ substituted for ‘this’

  4. Insert new edge into spacetime

This is equivalent to “redirecting” all edges to point to/from the new vertex while preserving edge properties (e.g., squared length).

Assertions: When CASET_ASSERTIONS is defined:

  • Throws std::runtime_error if spacetime is nullptr

Parameters:
  • vertex – Target vertex to receive edges

  • spacetime – The spacetime context (must be non-null)

Returns:

Pair of (old edges removed, new edges created)

std::pair<EdgePtrSet, EdgePtrSet> moveInEdgesTo(const VertexPtr &vertex, Spacetime *spacetime)

Move only incoming edges to another vertex.

Example

Before: \( u \rightarrow \text{this} \) After: \( u \rightarrow \text{vertex} \)

The source vertices remain unchanged; only the target is redirected.

Assertions: When CASET_ASSERTIONS is defined:

  • Throws std::runtime_error if spacetime is nullptr

  • Verifies sourceVertex != this (logic error if violated)

Parameters:
  • vertex – Target vertex to receive incoming edges

  • spacetime – The spacetime context (must be non-null)

Returns:

Pair of (old edges removed, new edges created)

std::pair<EdgePtrSet, EdgePtrSet> moveOutEdgesTo(const VertexPtr &vertex, Spacetime *spacetime)

Move only outgoing edges to another vertex.

Example

Before: \( \text{this} \rightarrow u \) After: \( \text{vertex} \rightarrow u \)

The target vertices remain unchanged; only the source is redirected.

Assertions: When CASET_ASSERTIONS is defined:

  • Throws std::runtime_error if spacetime is nullptr

  • Verifies targetVertex != this (logic error if violated)

Parameters:
  • vertex – Target vertex to become new source

  • spacetime – The spacetime context (must be non-null)

Returns:

Pair of (old edges removed, new edges created)

bool operator==(const Vertex &vertex) const noexcept

Equality comparison based on vertex ID.

Two vertices are considered equal iff they have the same ID, regardless of coordinates or topology. This is consistent with the hash function.

Parameters:

vertexVertex to compare against

Returns:

true if IDs match, false otherwise

inline std::string toString() const noexcept

Generate human-readable string representation.

Format

When CASET_VERBOSE is defined:

  • Shows vertex ID, in-degree, out-degree, and time

  • Example: \( V_{42}^{in=3} _{out=5}~(t=1.0) \)

When not defined: returns empty string for performance

Returns:

LaTeX-formatted UTF-8 string describing the vertex

Public Members

Fingerprint fingerprint

Fingerprint for hashing and equality testing.

The fingerprint is computed from the vertex ID and used in hash tables for SimplexPtrSet, VertexPtrSet, etc. This enables O(1) lookup.

Note: This is public to allow direct access for performance-critical code.

Private Types

enum class EdgeDirection

Edge direction for internal moveEdgesToImpl implementation.

Values:

enumerator In
enumerator Out

Private Functions

std::pair<EdgePtrSet, EdgePtrSet> moveEdgesToImpl(const VertexPtr &recipient, Spacetime *spacetime, EdgeDirection direction)

Internal implementation for moving edges.

Parameters:
  • recipient – Target vertex

  • spacetimeSpacetime context

  • direction – Whether to move In or Out edges

Returns:

Pair of (old edges, new edges)

Private Members

EdgePtrSet outEdges = {}

Edges where this vertex is the source.

EdgePtrSet inEdges = {}

Edges where this vertex is the target.

SimplexPtrSet simplices = {}

Simplices containing this vertex.

std::uint64_t id

Unique identifier.

std::vector<double> coordinates = {}

Spacetime position (may be empty)

namespace std
template<>
struct equal_to<caset::Vertex>

Equality comparison specialization for caset::Vertex.

Used by standard library containers to compare Vertex objects. Two vertices are equal iff they have the same ID.

Note

This is consistent with the hash specialization above.

Public Functions

inline size_t operator()(const caset::Vertex &a, const caset::Vertex &b) const noexcept
template<>
struct equal_to<std::shared_ptr<caset::Vertex>>

Equality comparison specialization for std::shared_ptr<caset::Vertex>

Compares vertices by ID, not by pointer address. Consistent with the hash specialization for shared_ptr<Vertex>.

Public Functions

inline size_t operator()(const caset::VertexPtr &a, const caset::VertexPtr &b) const noexcept
template<>
struct hash<caset::Vertex>

Hash function specialization for caset::Vertex.

Enables Vertex objects to be used as keys in std::unordered_set and std::unordered_map. The hash is computed from the vertex ID, ensuring consistent hashing across equal vertices.

Complexity

O(1) - delegates to std::hash<std::uint64_t>

Public Functions

inline size_t operator()(const caset::Vertex &vertex) const noexcept
template<>
struct hash<std::shared_ptr<caset::Vertex>>

Hash function specialization for std::shared_ptr<caset::Vertex>

Enables VertexPtr (shared_ptr<Vertex>) to be used as keys in hash tables. Hashes the underlying vertex ID, not the pointer address.

Important

Two shared_ptrs pointing to vertices with the same ID will hash to the same value, even if they are different pointer instances.

Public Functions

inline size_t operator()(const std::shared_ptr<caset::Vertex> &vertex) const noexcept
namespace caset

Enums

enum class EdgeDisposition : uint8_t

Edge Disposition

There are two things that determine the disposition (spacelike, timelike, light/null-like). The first is the squared edge length. If the squared length is negative in a (-, +, +, +) signature it’s timelike. A negative edge length in a (+, -, -, -) signature is spacelike. A 0-length in either is lightlike/null.

The second thing that determines the edge disposition is whether the vertices exist both in space (lightlike), both at the same time (timelike), or one in space and one in time (spacelike). See “Quantum Gravity from Causal Dynamical

Triangulations: A Review” by R. Loll, 2019. Figure 1. There’s no discussion of lightlike edges since

CDT does not treat that case. I’m making that up to fill in the gaps. If there’s some existing discussion around this in the literature I’m not aware at the time of this writing.

Values:

enumerator Spacelike
enumerator Timelike
enumerator Lightlike
class Edge : public std::enable_shared_from_this<Edge>
#include <Edge.h>

Edge Class

An edge that links two points (vertices) in spacetime. When we merge two vertices in the process of connecting two adjacent simplices; we cannot modify the edges in place without first removing them from their containers. Otherwise avoiding the necessary re-hashing results in undefined behavior. We should keep as little state as possible on the edge in favor of maintaining that state on the Vertex.

Param source_If:

this Edge represents a directed Edge; then this is the Vertex from which the Edge originates. For undirected edges; it’s just one of two Vertices that define the Edge.

Param target_If:

this Edge represents a directed Edge; then this is the Vertex at which the Edge terminates. For undirected edges; it’s just one of two Vertices that define the Edge.

Param squaredLength_:

The squared length of the edge according to whatever spacetime metric is being used. We work in squared lengths to allow the use of imaginary Edge lengths (they have negative values).

Public Functions

Edge(const VertexPtr &source, const VertexPtr &target, double squaredLength_)
Edge(const VertexPtr &source, const VertexPtr &target)
const VertexPtr &getSource() const noexcept

Every edge has a beginning and an end.

Many have two! And by that I mean they’re undirected, so the beginning is the end and the end, the beginning. Edges are bidirectional, so it doesn’t really matter if you consider them directed or undirected. If you want to use a directed edge; in your code you should just specify that you only traverse Vertex::outEdges and avoid Vertex::inEdges when you traverse around.

const VertexPtr &getTarget() const noexcept

getTarget is getSource’s better half.

All good things come to an end, with a wonderful journey left to memory. But seriously, though, getTarget gives the vertex on one end, and getSource gives the other.

double getSquaredLength() const noexcept

We work in squared edge lengths because imaginary numbers don’t play so nicely with floating point arithmetic.

To be less cryptic: timelike edges have imaginary length. Their squared edge length is negative. Something I’ve always thought was kind of neat is a right triangle with the opposite and adjacent edges of length \( i \) and \( 1 \) respectively. So the hypotenuse is zero. So timelike edges have imaginary length, spacelike edges have a positive length, and lightlike edges have zero length.

Returns:

The square of the length of the edge.

inline std::string toString() const noexcept
void replaceSourceVertex(const VertexPtr &newSource)

This method changes the target source in-place.

Note that if this edge is registered elsewhere (e.g. in a std::unordered_map in the Spacetime) then it needs to be unregistered first, modified, then re-registered to ensure consistent hashing/lookup. This method also updates the fingerprint hastily. If you want to update in batches remove the fingerprint.refresh() call.

void replaceTargetVertex(const VertexPtr &newTarget)

This method changes the target Vertex in-place.

Note that if this edge is registered elsewhere (e.g. in a std::unordered_map in the Spacetime) then it needs to be unregistered first, modified, then re-registered to ensure consistent hashing/lookup. CRITICAL: TODO: we need to remove edges from their containers before changing their fingerprints! Same as replaceSourceVertex above, but for targets.

bool hasVertex(std::uint64_t vertexId)

Check whether or not this Edge has a particular Vertex.

The comparison is against source/target node IDs, so don’t worry too much about accidentally comparing pointers. This is mostly a convenience method to make your code more clear and avoid typing.

Parameters:

vertexId – The ID of a Vertex for which ownership should be checked.

Returns:

true if the Vertex exists as an endpoint of this edge

bool hasVertex(const VertexPtr &vertex)
bool operator==(const Edge &other) const
std::uint64_t toHash() const
EdgeKey getKey() const noexcept

If you want to compare two edges by value; you can compare their keys.

Assume two Edges with the same EdgeKey are, for all intents and purposes, equal. This will change if we begin storing state on the Edge, but at the moment let’s focus on storing as much state on the Vertex as possible. Edges have potentially MUCH higher cardinality than Vertices, so as much state as we can fit on the Vertex, we should fit on the Vertex. This should be at the expense of slight inconvenience.

Returns:

A tuple of {sourceId, targetId}.

Public Members

Fingerprint fingerprint = {}

Private Members

VertexPtr source = nullptr
VertexPtr target = nullptr
double squaredLength

Simulations

namespace caset
class Simulation
#include <Simulation.h>

Simulation Base Class

Abstract interface for Monte Carlo simulations of discrete spacetime geometries. Each subclass implements a specific approach to quantum gravity:

  • CDT (Causal Dynamical Triangulations): Metropolis sampling over causal triangulations weighted by the Regge action. Preserves a global time foliation.

  • Regge calculus: Varies edge lengths on a fixed triangulation, computing the Einstein-Hilbert action via deficit angles at hinges.

  • Causal Set Theory (CST): Poisson sprinklings of points in a Lorentzian manifold, with order relations encoding causal structure.

The simulation lifecycle has two phases:

  1. Tuning: Adjust bare coupling constants (e.g., the cosmological constant) so that macroscopic observables (e.g., total spacetime volume) reach their target values. In CDT, this drives \( N_4 \to \bar{N}_4 \).

  2. Thermalization: Evolve the system under the Monte Carlo dynamics until it reaches thermal equilibrium, after which configurations are drawn from the stationary distribution \( P(\mathcal{T}) \propto e^{-S[\mathcal{T}]} \).

Subclassed by caset::CDT

Public Functions

virtual ~Simulation() = default
virtual void tune()

Tune bare coupling constants toward physically meaningful values.

In CDT this adjusts \( k_4 \) so the four-volume fluctuates around the target. In Regge calculus this constructs an initial triangulation. In CST this sets the sprinkling density.

virtual void thermalize()

Evolve the system to thermal equilibrium.

In CDT this runs Monte Carlo sweeps until the action stabilizes. In CST this applies a Poisson sprinkling to break Lorentz-invariance-violating lattice artifacts. In Regge calculus this applies random edge-length perturbations.

namespace caset
class CDT : public caset::Simulation
#include <CDT.h>

Causal Dynamical Triangulations (CDT) Simulation

This class implements the Metropolis Monte Carlo algorithm for Causal Dynamical Triangulations in \( d \) dimensions (typically \( d = 4 \)). CDT is a non-perturbative approach to quantum gravity introduced by Ambjorn, Jurkiewicz, and Loll in which the gravitational path integral is approximated by a sum over causal triangulations of spacetime.

The Regge Action

The Euclidean Regge action for CDT (after Wick rotation) is

\[ S_{\text{Regge}} = -(k_0 + 6\Delta)\, N_0 + (k_4 + \Delta)\, N_{41} + k_4\, N_{32} \]

where:

  • \( N_0 \) is the number of vertices,

  • \( N_{41} \) is the number of \((d,1) + (1,d)\)-type simplices,

  • \( N_{32} \) is the number of \((d\!-\!1, 2) + (2, d\!-\!1)\)-type simplices,

  • \( k_0 \) is a coupling related to the inverse bare Newton constant \( G_N^{-1} \),

  • \( k_4 \) is a coupling related to the bare cosmological constant \( \Lambda \),

  • \( \Delta \) is the asymmetry parameter encoding the ratio of timelike to spacelike squared edge lengths via \( \ell_t^2 = -\alpha\, a^2 \).

An additional volume-fixing term

\[ S_{\text{fix}} = \varepsilon \left( N_4 - \bar{N}_4 \right)^2 \]

constrains the total four-volume \( N_4 = N_{41} + N_{32} \) to fluctuate around the target value \( \bar{N}_4 \).

Metropolis Algorithm

Each Monte Carlo sweep proposes \( N_4 \) random local moves and accepts or rejects each according to the Metropolis criterion:

\[ P_{\text{accept}} = \min\!\left(1,\; e^{-\Delta S}\right) \]

where \( \Delta S = S_{\text{new}} - S_{\text{old}} \). This satisfies detailed balance with respect to the CDT partition function \( Z = \sum_{\mathcal{T}} e^{-S[\mathcal{T}]} \).

Pachner Moves

The local moves are Pachner (bistellar) moves that preserve the piecewise-linear manifold structure and the causal (foliated) time slicing:

  • Add (grow): Cone an external facet to a new vertex, adding one \( d \)-simplex.

  • Remove (shrink): Remove a \( d \)-simplex whose apex vertex has no other incident top-simplices, reversing an add move.

  • Flip \((2, d)\): Replace two \( d \)-simplices sharing a \((d\!-\!1)\)-face with \( d \) new simplices sharing an edge. This is the standard bistellar flip.

  • Shift \((3, 3)\): Replace three \( d \)-simplices sharing a \((d\!-\!2)\)-face with three new simplices sharing a different \((d\!-\!2)\)-face.

These moves are ergodic: any causal triangulation of a given topology can be reached from any other by a finite sequence of moves (Alexander’s theorem).

Phase Structure

In 4D, the coupling-constant space \((k_0, \Delta)\) exhibits four phases:

  • Phase A (branched polymer): elongated, fractal geometry,

  • Phase B (crumpled): collapsed, high-connectivity geometry,

  • **Phase \( C_{dS} \)** (de Sitter): extended, four-dimensional geometry whose volume profile \( N_3(t) \) matches the Euclidean four-sphere,

  • **Phase \( C_b \)** (bifurcation): exhibits a singular vertex.

The physically relevant phase is \( C_{dS} \), where the average spatial volume profile follows

\[ \langle N_3(t) \rangle \propto \cos^4\!\left(\frac{\pi\, t}{T}\right) \]

matching the round \( S^4 \) metric of Euclidean de Sitter space.

References

  • Ambjorn, Jurkiewicz, Loll, Reconstructing the Universe, Phys. Rev. D 72 (2005)

  • Gorlich, Introduction to Causal Dynamical Triangulations (2013)

  • Loll, Quantum Gravity from Causal Dynamical Triangulations: A Review, Class. Quant. Grav. 37 (2020)

Public Functions

CDT(std::shared_ptr<Spacetime> spacetime, double k0, double k4, double delta, double epsilon, std::size_t targetN4)

Construct a CDT simulation for a given spacetime.

The spacetime should already be initialized via build() before constructing the simulation. The coupling constants \((k_0, k_4, \Delta)\) determine which phase the simulation explores. Typical de Sitter phase values are \( k_0 \approx 2 \), \( \Delta \approx 0.6 \).

Parameters:
  • spacetime – The built spacetime triangulation to simulate

  • k0 – Coupling constant \( k_0 \) (related to \( G_N^{-1} \))

  • k4 – Coupling constant \( k_4 \) (related to \( \Lambda \))

  • delta – Asymmetry parameter \( \Delta \)

  • epsilon – Volume-fixing strength \( \varepsilon \)

  • targetN4 – Target four-volume \( \bar{N}_4 \)

bool add()

Grow the triangulation by coning an external \((d\!-\!1)\)-face to a new vertex.

Picks a random causally-available facet of a random \( d \)-simplex and cones it to a new vertex, creating one new \( d \)-simplex. The new vertex is placed at the appropriate time slice to preserve the causal structure.

Returns:

true if the move was accepted by the Metropolis criterion

bool remove()

Shrink the triangulation by removing a \( d \)-simplex with an isolated apex.

Finds a \( d \)-simplex that has a facet with exactly one coface (itself) and whose unique vertex belongs to no other top-dimensional simplex. Removing it reverses a previous add move.

Returns:

true if the move was accepted

bool flip()

Bistellar \((2, d)\) flip: replace two \( d \)-simplices with \( d \) new ones.

Finds a \((d\!-\!1)\)-face shared by exactly two \( d \)-simplices. The two simplices together span \( d + 2 \) vertices ( \( d \) shared, 2 unique). They are replaced by \( d \) new simplices, each containing both unique vertices and \( d - 1 \) of the \( d \) shared vertices. The move preserves vertex count and topology.

Returns:

true if the move was accepted

bool shift()

\((3, 3)\) Pachner move: replace three simplices sharing a \((d\!-\!2)\)-face with three new simplices sharing the complementary \((d\!-\!2)\)-face.

The three old simplices share a triangle ( \( d - 2 \) face) of 3 vertices and have 3 unique vertices. The replacement simplices share the dual triangle formed by the 3 unique vertices.

Returns:

true if the move was accepted

bool ishift()

Inverse of the \((3, 3)\) shift.

Structurally identical; the Metropolis criterion handles the directional asymmetry.

Returns:

true if the move was accepted

virtual void tune() override

Adjust the cosmological coupling \( k_4 \) to drive the total four-volume \( N_4 \) toward the target \( \bar{N}_4 \).

Uses a proportional controller that increases \( k_4 \) when volume exceeds the target (making growth more expensive) and decreases it when volume is below target.

virtual void thermalize() override

Run Monte Carlo sweeps until the action \( S \) stabilizes, indicating the system has reached thermal equilibrium.

Equilibrium is detected when the relative change in \( S \) between sweeps drops below 1%.

int sweep()

Execute one Monte Carlo sweep: propose \( N_4 \) random moves (uniformly chosen among add, remove, flip, shift, ishift) and accept or reject each via the Metropolis criterion.

Returns:

Number of accepted moves in this sweep

double computeAction() const

Evaluate the full Regge action \( S = S_{\text{Regge}} + S_{\text{fix}} \) for the current triangulation state.

Returns:

The action value \( S \)

std::vector<int> getVolumeProfile() const

Compute the spatial volume profile \( N_3(t) \): the number of top-dimensional simplices whose earliest vertex lies at time slice \( t \).

In the de Sitter phase, the average of this quantity over many configurations approximates \( \langle N_3(t) \rangle \propto \cos^4(\pi t / T) \).

Returns:

Vector of simplex counts indexed by time slice offset

std::map<std::string, double> getAcceptanceRates() const
Returns:

Acceptance rate (accepted/attempted) for each move type.

std::shared_ptr<Spacetime> getSpacetime() const noexcept
Returns:

The spacetime being simulated.

double getK0() const noexcept
Returns:

Coupling constant \( k_0 \).

double getK4() const noexcept
Returns:

Coupling constant \( k_4 \) (may be modified by tuning).

double getDelta() const noexcept
Returns:

Asymmetry parameter \( \Delta \).

Private Functions

bool accept(double deltaS)

Metropolis acceptance test: accept if \( \Delta S \le 0 \), otherwise accept with probability \( e^{-\Delta S} \).

double computeDeltaAction(int dN0, int dN41, int dN32) const

Compute the incremental action change for a proposed move without recomputing the full sum over simplices.

Private Members

std::shared_ptr<Spacetime> spacetime
double k0
double k4
double delta
double epsilon
std::size_t targetN4
std::mt19937 rng = {std::random_device{}()}
int addAttempts = 0
int addAccepted = 0
int removeAttempts = 0
int removeAccepted = 0
int flipAttempts = 0
int flipAccepted = 0
int shiftAttempts = 0
int shiftAccepted = 0
int ishiftAttempts = 0
int ishiftAccepted = 0

Observables

namespace caset
class Observable
#include <Observable.h>

Observable Base Class

Interface for physical observables measured on a triangulated spacetime configuration. In lattice quantum gravity, observables are functions \( \mathcal{O}[\mathcal{T}] \) of the triangulation \( \mathcal{T} \) whose expectation values are estimated by averaging over Monte Carlo samples:

\[ \langle \mathcal{O} \rangle = \frac{1}{Z} \sum_{\mathcal{T}} \mathcal{O}[\mathcal{T}]\, e^{-S[\mathcal{T}]} \approx \frac{1}{N_{\text{meas}}} \sum_{i=1}^{N_{\text{meas}}} \mathcal{O}[\mathcal{T}_i] \]

Subclasses include:

  • SpacetimeVolume: total number of \( d \)-simplices \( N_d \),

  • VolumeProfile: spatial volume per time slice \( N_{d-1}(t) \).

Subclassed by caset::SpacetimeVolume, caset::VolumeProfile

Public Functions

virtual double compute(std::shared_ptr<Spacetime> &spacetime)

Compute the observable from scratch for the given spacetime.

Parameters:

spacetime – The spacetime configuration to measure

Returns:

The scalar value of the observable

virtual double update(std::shared_ptr<Spacetime> &spacetime)

Incrementally update the observable after a local move.

Default implementation delegates to compute().

Parameters:

spacetime – The spacetime after the most recent move

Returns:

The updated scalar value

virtual ~Observable() = default
namespace caset
class VolumeProfile : public caset::Observable
#include <VolumeProfile.h>

Volume Profile Observable

Measures the spatial volume profile \( N_3(t) \), defined as the number of top-dimensional simplices whose initial vertex lies at time slice \( t \). This is the primary observable for comparing CDT simulations to continuum cosmology.

Physical Significance

In the de Sitter phase \((C_{dS})\) of 4D CDT, the ensemble-averaged volume profile matches the metric of the Euclidean four-sphere \( S^4 \):

\[ \langle N_3(t) \rangle \;\propto\; \cos^4\!\left(\frac{\pi\, t}{T}\right) \]

where \( T \) is the total time extent. This is the discrete analogue of the scale factor in Friedmann-Lemaitre-Robertson-Walker (FLRW) cosmology:

\[ ds^2 = -dt^2 + a(t)^2\, d\Omega_3^2 \qquad\text{with}\quad a(t) = \cos\!\left(\frac{\pi\, t}{T}\right) \]

In the crumpled phase (B), the volume concentrates on a single time slice. In the branched-polymer phase (A), the profile becomes thin and elongated.

References

  • Ambjorn, Jurkiewicz, Loll, Reconstructing the Universe, Phys. Rev. D 72 (2005)

  • Gorlich, Introduction to Causal Dynamical Triangulations (2013), Section 3.3

Public Functions

virtual double compute(std::shared_ptr<Spacetime> &spacetime) override

Compute the current volume profile and return the peak volume.

Parameters:

spacetime – The spacetime to measure

Returns:

The maximum value of \( N_3(t) \) across all time slices

virtual double update(std::shared_ptr<Spacetime> &spacetime) override

Recompute the profile (equivalent to compute for this observable).

std::vector<int> getProfile() const
Returns:

The most recently computed volume profile as a vector indexed by time slice.

std::vector<double> getAverageProfile() const
Returns:

The average volume profile over all recorded measurements.

void measure(std::shared_ptr<Spacetime> &spacetime)

Record the current volume profile for later averaging.

Call this after each decorrelated Monte Carlo measurement to build statistics.

Parameters:

spacetime – The spacetime to measure

void reset()

Reset all accumulated measurements.

Private Members

std::vector<int> currentProfile

Most recent \( N_3(t) \).

std::vector<std::vector<int>> measurements

History of profiles for averaging.

Topologies

namespace caset
class Topology
#include <Topology.h>

Spatial Topology

Abstract base class for the topology of spatial slices in a foliated spacetime. In CDT, the spacetime manifold \( \mathcal{M} \) has the product structure

\[ \mathcal{M} \cong \Sigma \times I \]

where \( \Sigma \) is a compact spatial manifold and \( I \) is either an interval \([0, T]\) (cylinder) or a circle \( S^1 \) (periodic time). The topology of \( \Sigma \) is fixed throughout the simulation and determines the boundary conditions and initial triangulation.

Subclasses implement build() to construct an initial triangulation matching their spatial topology via the coning mechanism: a seed \( d \)-simplex is created at each time layer, and exterior facets are iteratively coned to new vertices to grow the complex.

Subclassed by caset::Cylinder, caset::Sphere, caset::Toroid

Public Functions

virtual ~Topology()
virtual void build(Spacetime *spacetime, int numSimplices) = 0

Build an initial triangulation with the given spatial topology.

Creates \( d \)-simplices across multiple time layers using iterated coning from seed simplices. The resulting simplicial complex \( \mathcal{K} \) has the combinatorial structure of the chosen topology.

Parameters:
  • spacetime – The spacetime in which to build the triangulation

  • numSimplices – Target number of top-dimensional simplices to create

namespace caset
class Toroid : public caset::Topology
#include <Toroid.h>

Toroidal Topology \f$ T^{d-1} \f$

Spatial slices have the topology of a \((d\!-\!1)\)-torus, giving a spacetime manifold \( \mathcal{M} \cong T^{d-1} \times S^1 \) with periodic boundary conditions in both space and time. This is the default topology for CDT simulations and the most commonly used in the literature.

Periodic time means the last time slice \( t = T \) is identified with the first \( t = 0 \), so the triangulation has no temporal boundaries.

The build creates multiple time layers, each grown from a seed \( d \)-simplex via iterated coning in random (forward/backward) time directions.

Public Functions

virtual void build(Spacetime *spacetime, int numSimplices) override

Build a toroidal triangulation with multiple time layers.

Cones exterior facets in random \( \pm t \) directions within each layer, then advances to the next time slice. The result is a multi-layer simplicial complex where each layer spans one unit of coordinate time.

Parameters:
  • spacetime – The spacetime to populate

  • numSimplices – Target number of top-dimensional simplices

namespace caset
class Cylinder : public caset::Topology
#include <Cylinder.h>

Cylindrical Topology \f$ \Sigma \times [0, T] \f$

Spatial slices have a closed topology \( \Sigma \) but time is non-periodic: the manifold has the structure \( \Sigma \times [0, T] \) with open temporal boundaries at \( t = 0 \) and \( t = T \).

This topology is useful for studying spacetimes with initial and final spatial slices, analogous to the “no-boundary” proposals in quantum cosmology, or for computing transition amplitudes between two spatial geometries.

The build creates layers by coning only in the forward time direction, producing a monotonically increasing time structure.

Public Functions

virtual void build(Spacetime *spacetime, int numSimplices) override

Build a cylindrical triangulation with open time boundaries.

Each layer grows by coning exterior facets forward in time only. The first and last time slices are boundary slices.

Parameters:
  • spacetime – The spacetime to populate

  • numSimplices – Target number of top-dimensional simplices

namespace caset
class Sphere : public caset::Topology
#include <Sphere.h>

Spherical Topology \f$ S^{d-1} \f$

Spatial slices have the topology of a \((d\!-\!1)\)-sphere, giving a spacetime manifold \( \mathcal{M} \cong S^{d-1} \times S^1 \). This is the topology used in most 4D CDT simulations, where spatial slices are three-spheres \( S^3 \).

The Euclidean de Sitter solution (the round four-sphere \( S^4 \)) naturally decomposes into \( S^3 \) spatial slices, making this topology the natural choice for studying de Sitter quantum gravity. The volume profile of each slice follows

\[ V_3(t) \propto \cos^3\!\left(\frac{\pi\, t}{T}\right) \]

for the continuum \( S^4 \) geometry.

The build alternates coning direction between layers to close the manifold.

Public Functions

virtual void build(Spacetime *spacetime, int numSimplices) override

Build a spherical triangulation by coning in alternating \( \pm t \) directions.

Parameters:
  • spacetime – The spacetime to populate

  • numSimplices – Target number of top-dimensional simplices

Constraints

namespace caset

Enums

enum class ConstraintType : uint8_t

Types of constraints that can be applied to a spacetime.

  • PachnerMove: Constraints specific to a proposed Pachner move (e.g., checking that a bistellar flip preserves the manifold condition).

  • All: Global constraints that must hold at all times (e.g., the simplicial complex must remain a manifold, the causal structure must be preserved).

Values:

enumerator PachnerMove
enumerator All
class Constraint
#include <Constraint.h>

Constraint Base Class

Encodes conditions that a spacetime triangulation must satisfy. In CDT, the central constraint is causality: every \( d \)-simplex must have its vertices distributed across exactly two adjacent time slices, preserving the global time foliation. This ensures that each spatial slice is a closed \((d\!-\!1)\)-manifold and that the causal structure is well-defined.

Additional constraints may include:

  • Manifoldness: every \((d\!-\!1)\)-face is shared by at most 2 \( d \)-simplices (the link of every simplex is a sphere).

  • Topology preservation: Pachner moves must not change the topology of the spatial slices or the overall manifold.

  • Volume bounds: the total four-volume \( N_4 \) must remain within a specified range during the simulation.

Public Functions

virtual ~Constraint() = default
virtual bool applies(std::shared_ptr<Spacetime> &spacetime, ConstraintType &type_)

Check whether a constraint is satisfied for the current spacetime state.

Parameters:
  • spacetime – The spacetime to check

  • type_ – The type of constraint check to perform

Returns:

true if the constraint is satisfied