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
-
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.
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, 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(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
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
and\[ l_s = +a \]\[ 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¶
ais the coefficient that sets the fixed edge length for spacelike edges according tofor spacelike edges and\[ l_s = +a \]\[ 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
-
IdType vertexIdCounter = 0¶
-
SpacetimeType spacetimeType¶
-
double alpha = 1.¶
-
double a = 1.¶
-
Foliation foliation = Foliation::PREFERRED¶
-
SimplexSet simplices = {}¶
-
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 = {}¶
-
Spacetime()¶
-
class Spacetime¶
-
namespace caset
-
class Metric¶
- #include <Metric.h>
The Metric¶
Public Functions
-
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.
-
double getSquaredLength(const std::vector<double> &sourceCoords, const std::vector<double> &targetCoords) const¶
-
class Metric¶
-
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 Static Attributes
-
static const double c = 1.¶
-
Signature(int dimensions_, SignatureType signatureType_)¶
-
enum class SignatureType : uint8_t¶
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_)¶
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 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).
-
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)¶
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).
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
-
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
isCausallyAvailableFor 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¶
-
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
vertexto this facet and a vector of new exterior facets resulting from the new simplex.
-
void validate() const¶
-
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 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 void registerToVertices(const SimplexPtr &simplex)¶
-
explicit Simplex(Spacetime *spacetime_, const VertexPtrs &vertices_, Edges edges_)¶
-
class Simplex : public std::enable_shared_from_this<Simplex>¶
-
namespace caset
Enums
-
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::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)¶
-
SimplexOrientation(uint8_t ti_, uint8_t tf_)¶
-
class SimplexOrientation¶
-
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:
edge – Edge 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:
edge – Edge 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:
edge – Edge where this vertex is the source
-
SimplexPtrSet removeInEdge(const EdgePtr &edge) noexcept¶
Remove an incoming edge and update all affected simplices.
Implementation Details¶
Removes the edge from all simplices that contain it via Simplex::removeEdge()
Removes the edge from this vertex’s inEdges set
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 (Simplex ↔ Vertex) 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:
Remove edge from source/target vertex’s edge lists
Remove edge from spacetime’s edge registry
Create new edge with ‘vertex’ substituted for ‘this’
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
See also
- 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:
vertex – Vertex 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
Private Functions
-
std::pair<EdgePtrSet, EdgePtrSet> moveEdgesToImpl(const VertexPtr &recipient, Spacetime *spacetime, EdgeDirection direction)¶
Internal implementation for moving edges.
- Parameters:
recipient – Target vertex
spacetime – Spacetime context
direction – Whether to move In or Out edges
- Returns:
Pair of (old edges, new edges)
-
class Vertex : public std::enable_shared_from_this<Vertex>¶
-
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.
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
-
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>
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.
Public Functions
-
template<>
-
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¶
-
enumerator Spacelike¶
-
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::outEdgesand avoidVertex::inEdgeswhen you traverse around.
-
const VertexPtr &getTarget() const noexcept¶
getTargetisgetSource’s better half.All good things come to an end, with a wonderful journey left to memory. But seriously, though,
getTargetgives the vertex on one end, andgetSourcegives 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.
-
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.
-
bool hasVertex(const VertexPtr &vertex)¶
-
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 = {}¶
-
enum class EdgeDisposition : uint8_t¶
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:
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 \).
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¶
-
class Simulation¶
-
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
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.
-
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.
-
class CDT : public caset::Simulation¶
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
Compute the observable from scratch for the given spacetime.
- Parameters:
spacetime – The spacetime configuration to measure
- Returns:
The scalar value of the observable
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¶
-
class Observable¶
-
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
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
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.
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.
-
class VolumeProfile : public caset::Observable¶
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
-
virtual ~Topology()¶
-
class Topology¶
-
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
-
virtual void build(Spacetime *spacetime, int numSimplices) override¶
-
class Toroid : public caset::Topology¶
-
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
-
virtual void build(Spacetime *spacetime, int numSimplices) override¶
-
class Cylinder : public caset::Topology¶
-
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.
-
class Sphere : public caset::Topology¶
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¶
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
-
enum class ConstraintType : uint8_t¶