The context layer of DeepCausality: the Context hypergraph, the
contextoids that populate it, the context node types for data, space, time and spacetime, and the
traits that describe them.
The context holds what a causal model reasons about: a typed environment that a causaloid or a causal-monad chain reads as it runs.
A consumer of deep_causality_core alone pays nothing for context; a model that needs context
declares this dependency and imports from it. deep_causality depends on this crate
unconditionally, so a causaloid model gets context either way. The crates that depend on this one
directly are the crates that model a context.
[dependencies]
deep_causality_context = "0.1"With deep_causality_core and this crate, a monad chain carries a typed context without pulling
in the causaloid stack:
use deep_causality_context::BaseContext;
use deep_causality_core::{PropagatingEffect, PropagatingProcess};
fn start(ctx: BaseContext) -> PropagatingProcess<f64, (), BaseContext> {
PropagatingProcess::with_state(PropagatingEffect::pure(0.0), (), Some(ctx))
}Context— the context hypergraph, with extra contexts, data/time indices, freezing and neighbour listing.get_node_index_by_idfinds a node by its contextoid identifier, andget_data_by_idreturns a data node's payload by that identifier.Contextoid/ContextoidType— the nodes it holds.- Context node types —
Data,Root, and the space, time and spacetime families:EuclideanSpace,EcefSpace,NedSpaceandGeoSpace;NewtonianTime,MinkowskiTime,DiscreteTime,EntropicTimeandSymbolicTime;GalileanSpacetime,NewtonianSpacetime,MinkowskiSpacetimeandTangentSpacetime;SpaceKind,TimeKindandSpaceTimeKind, enums over the family's types (TimeKindleaves outSymbolicTime); andNoSpace,NoTimeandNoSpaceTimefor a slot the context does not use. Each type in the geometry table below documents the definition it follows and its source, with copies of the open-access sources inpapers/. CausalSetSpacetime, an element of a causal set with the elements that precede it. It holds the order alone and implements none of the coordinate or time traits, so it cannot fill a context's spacetime slot; a context carries it as a data payload,Data<CausalSetSpacetime>.Contextuable,Datable,Spatial,Temporal,SpaceTemporal,Coordinate,Distance,MetricSignature,MetricTensor4Dand the indexable traits.Adjustable<T>for nodes that update from anArrayGrid<T, ..>, andUncertainAdjustablefor nodes that update from their own associatedSelf::Data.Storable,ContextStore,StoreError,Context::snapshot,Context::restoreandContext::applyfor persistence, with theSubstrateContextandSubstrateContextoidaliases.RelationKind,TimeScale,VerticalDatumandSubstrateRef, re-exported fromdeep_causality_context_store, so a model names them from this crate alone.
The base graph has two states. A mutable graph takes changes. freeze builds a compressed sparse
row form of it in O(V + E) for the graph algorithms. outbound_edges and inbound_edges list a
node's successors and predecessors in both states, as do reads such as get_node,
contains_edge and get_node_index_by_id. While the graph is frozen, every
change to it returns an error, and Context::apply refuses an event that would change it with
ProjectionError::Frozen. unfreeze makes the graph mutable again.
A node's index is permanent: freeze and unfreeze keep every index, and an index the caller
holds stays valid until the node is removed. Removing a node drops the data and time index entries
that named its index; the index is never reissued. Extra contexts are not frozen with the base
graph.
use deep_causality_context::{
BaseContext, BaseContextoid, Context, ContextIndexError, Contextoid, ContextoidType,
ContextuableGraph, Data, RelationKind,
};
fn datum(id: u64, value: f64) -> BaseContextoid {
Contextoid::new(id, ContextoidType::Datoid(Data::new(id, value)))
}
fn main() -> Result<(), ContextIndexError> {
let mut context: BaseContext = Context::with_capacity(1, "plant", 3);
let pump = context.add_node(datum(10, 1.0))?;
let valve = context.add_node(datum(20, 0.5))?;
context.add_edge(pump, valve, RelationKind::Datial)?;
context.freeze();
let downstream: Vec<usize> = context.outbound_edges(pump)?.collect();
assert_eq!(downstream, [valve]);
assert!(context.add_node(datum(30, 0.0)).is_err());
context.unfreeze();
context.add_node(datum(30, 0.0))?;
Ok(())
}Each graph holds a contextoid identifier at most once. add_node, update_node and
extra_ctx_add_node refuse an identifier the graph already holds. The base graph and each extra
may hold the same identifier, as a context restored from a store does.
Each type below models one geometry, named as the cited source names it.
| Type | Geometry | Distance or interval | Source |
|---|---|---|---|
EuclideanSpace |
Three-dimensional Euclidean space | √(Δx² + Δy² + Δz²) |
Weatherall 2021, §2 |
EcefSpace |
WGS 84 Earth-centred, Earth-fixed Cartesian coordinates, metres | Euclidean norm | NGA 2014, §2.1; IOGP 2012, §2.2.1 |
NedSpace |
Local topocentric frame, north-east-down, metres | Euclidean norm | IOGP 2012, §2.2.2 |
GeoSpace |
WGS 84 geodetic latitude, longitude, altitude against a VerticalDatum |
Straight line through the geocentric conversion; NaN unless both altitudes are WGS 84 ellipsoidal heights | NGA 2014, Tables 3.1 and 3.5; IOGP 2012, §2.2.1 |
NewtonianTime |
Absolute time of a classical spacetime | — | Weatherall 2021, §3; Malament 2012, §4.1 |
MinkowskiTime |
Coordinate time of an inertial frame of Minkowski spacetime | — | Carroll 1997, ch. 1 |
GalileanSpacetime |
Galilean spacetime: affine, absolute time, Euclidean space at each instant, no rest frame | Spatial distance between simultaneous events; NaN between events at different times | Weatherall 2021, §3; Malament 2012, §4.1 |
NewtonianSpacetime |
Newtonian spacetime: Galilean spacetime with absolute space, in coordinates at rest in it | Spatial distance between any two events, a pseudometric | Weatherall 2021, §4; Stein 1967; Malament 2012, Prop. 4.1.2 |
MinkowskiSpacetime |
Flat Minkowski spacetime, east-coast signature (−, +, +, +) | s² = −(cΔt)² + Δx² + Δy² + Δz²; NaN for a time scale that names no duration |
Malament 2012, §2.1; Carroll 1997, eqs. (1.3), (1.8) |
TangentSpacetime |
An event of a relativistic spacetime with a tangent vector and the metric tensor there | g_ab Δxᵃ Δxᵇ under the event's own tensor |
Malament 2012, §2.1; Poisson, Pound & Vega 2011, §3.1 |
CausalSetSpacetime |
An element of a causal set and its past | — | Sorkin 2003, p. 5 |
GalileanSpacetime, NewtonianSpacetime, MinkowskiSpacetime and TangentSpacetime index their
coordinates 0 => t, 1 => x, 2 => y, 3 => z, the order of Carroll 1997, eq. (1.5), with t at
index 0 where Carroll places x⁰ = ct. Each reports through MetricSignature the signature of the
metric it measures with, generator i its coordinate i: Metric::Lorentzian(4) for the
relativistic types, and Metric::PGA(4), the spatial metric's (0, +, +, +), for the classical ones.
CausalSetSpacetime has no coordinates, no time and no signature: it holds the order alone.
TangentSpacetime::update_metric_tensor accepts only a finite, symmetric tensor of signature
(−, +, +, +). It counts the signs of the eigenvalues by symmetric block elimination, which keeps
them by Sylvester's law of inertia (Horn & Johnson 2013, Theorem 4.5.8).
NoSpace, NoTime and NoSpaceTime fill the spatial, temporal and spacetime slots of a context
whose graph holds no node of that kind.
Open-access sources are copied into papers/.
- Carroll, S. M. (1997). Lecture Notes on General Relativity. arXiv:gr-qc/9712019.
papers/carroll_1997_lecture_notes_on_general_relativity_arXiv_gr-qc_9712019.pdf - Horn, R. A., & Johnson, C. R. (2013). Matrix Analysis, 2nd ed. Cambridge University Press.
- IOGP (2012). Geomatics Guidance Note 7, part 2: Coordinate Conversions and Transformations
including Formulas, OGP Publication 373-7-2.
papers/iogp_2012_373-7-2_coordinate_conversions_and_transformations.pdf - Malament, D. B. (2012). Topics in the Foundations of General Relativity and Newtonian Gravitation Theory. University of Chicago Press.
- National Geospatial-Intelligence Agency (2014). World Geodetic System 1984: Its Definition and
Relationships with Local Geodetic Systems, NGA.STND.0036_1.0.0_WGS84.
papers/NGA.STND.0036_1.0.0_WGS84.pdf - Poisson, E., Pound, A., & Vega, I. (2011). The Motion of Point Particles in Curved Spacetime.
Living Reviews in Relativity, article 7. doi:10.12942/lrr-2011-7. arXiv:1102.0529.
papers/poisson_pound_vega_2011_motion_of_point_particles_in_curved_spacetime_arXiv_1102.0529.pdf - Sorkin, R. D. (2003). Causal Sets: Discrete Gravity. arXiv:gr-qc/0309009.
papers/sorkin_2003_causal_sets_discrete_gravity_arXiv_gr-qc_0309009.pdf - Stein, H. (1967). Newtonian space-time. The Texas Quarterly 10, 174–200.
- Weatherall, J. O. (2021). Classical Spacetime Structure. In E. Knox & A. Wilson (Eds.), The
Routledge Companion to Philosophy of Physics, pp. 33–45. Routledge. arXiv:1707.05887.
papers/weatherall_2017_classical_spacetime_structure_arXiv_1707.05887.pdf
| Configuration | Cargo features | Supported |
|---|---|---|
std, hosted |
default | ✓ |
no-std with a heap |
default-features = false, features = ["no-std"] |
✓ |
no-std on core alone, no heap |
– | – |
Without std the crate needs a heap: the program must define a #[global_allocator]. os-random is host-only. Without std the id index is a BTreeMap.
CI builds every supported bare-metal configuration for thumbv7em-none-eabihf.
README_NO_STD.md describes the build configuration of every crate in the workspace.
The context builds without the standard library.
default: Enablesstd.std: Uses the standard library (includesalloc).no-std: Builds without the standard library (includesalloc); floating-point math comes fromlibm. Select withdefault-features = false, features = ["no-std"].alloc: Allocation (Vec, String). Enabled by bothstdandno-std; on its own it selects no floating-point backend and does not build.os-random: Seedsdeep_causality_randfrom the operating system. Host-only.
The context finds a node by its id through an index map. Under std that map is a HashMap.
Without std there is no entropy to seed the default hasher, so it is a BTreeMap, and a lookup
costs O(log n): at 10 000 nodes about five times a hashed lookup (measured on an M3 Max).
Spatial, spacetime and real-valued temporal types carry the scalar they measure in, bounded once on
deep_causality_algebra::RealField. The crate names no concrete float outside its ready-made
aliases, so a type satisfying that bound works without an entry being added for it:
let narrow: EuclideanSpace<BFloat16> = EuclideanSpace::new(1, x, y, z);
let wide: EuclideanSpace<Float106> = EuclideanSpace::new(1, x, y, z);BaseContext, BaseContextoid, UniformContext and UniformContextoid stay concrete, so reaching
the context through one of those needs no annotation.
Tick-based times keep their integers. A tick counts, and its failure mode is overflow rather than rounding, so widening its significand buys nothing.
A context lives in memory. deep_causality_context_store describes what a store keeps of one;
this crate projects in both directions.
Context::snapshot writes the base graph and every extra context into a ContextSnapshot, in
canonical order, and Context::restore builds a context from one. A ContextStore puts the
projection and a backend together:
use deep_causality_context::{ContextStore, UniformContext};
let store = ContextStore::new(backend);
let ctx: UniformContext = store.hydrate(&container).await?;
// explore a branch in memory, then keep it:
let branch_id = store.store_branch("branch", &branch).await?;store_branch links every node the store already holds under the same record, creates every node
it does not hold, and creates under a fresh identifier every node the store holds under a different
record, all in one commit, so a refused store leaves nothing behind. A stored branch is a record
of a world, not a continuation of one; to keep exploring it, hydrate it. With a backend that
streams, subscribe returns a context and the stream of changes to it, and Context::apply applies
each event idempotently.
A data payload persists once its type implements Storable, which maps it onto a DataRecord
value tree. A struct maps to Fields, one entry per field:
use deep_causality_context::Storable;
use deep_causality_context_store::{ContextoidId, DataRecord, ProjectionError};
#[derive(Debug, Clone, Default, PartialEq)]
struct Reading {
temperature: f64,
samples: u64,
}
impl Storable for Reading {
fn to_record(&self) -> DataRecord {
DataRecord::Fields(vec![
("temperature".to_string(), self.temperature.to_record()),
("samples".to_string(), self.samples.to_record()),
])
}
fn from_record(id: ContextoidId, record: DataRecord) -> Result<Self, ProjectionError> {
let DataRecord::Fields(entries) = record else {
return Err(ProjectionError::WrongPayload(id, "Fields", record.kind_name()));
};
let field = |name: &'static str| {
entries
.iter()
.find(|(key, _)| key == name)
.map(|(_, value)| value.clone())
.ok_or(ProjectionError::MissingField(id, name))
};
Ok(Self {
temperature: f64::from_record(id, field("temperature")?)?,
samples: u64::from_record(id, field("samples")?)?,
})
}
}Data<Reading> then snapshots, restores, stores and streams like Data<f64>. The crate implements
Storable for f32, f64, u64, i64, bool, String, SubstrateRef, Vec<T>,
Option<T>, Float106 and BFloat16.
A store that holds structure and not values takes a data node as a SubstrateRef. SubstrateContext
names that shape, and ContextStore::create_node_via and hydrate_via move the values through a
Substrate on the way in and out, so a value-typed context reaches such a store unchanged.
An extra context has a name and an identifier. extra_ctx_add_new(name, capacity, default)
allocates the identifier as one past the highest ever held; extra_ctx_add_new_with_id takes one, and
refuses 0. In a store an extra is a separate container the base references, so a hydrated context
holds each referenced container as an extra under that container's identifier and name, and a
stored branch's extras become containers of their own. The store assigns container identifiers:
an extra added locally is not a container of the store, so a store event naming its identifier
as a container never reaches it, and Context::apply refuses an attachment under that identifier with
ProjectionError::Identity rather than hold two containers under one identifier.
Every scalar field of a record is f64. A node type over a wider or narrower scalar narrows to
f64 on the way out and lifts on the way in, so EuclideanSpace<Float106> coordinates restore at
double precision and EuclideanSpace<BFloat16> coordinates restore rounded as that type rounds.
Ticks stay u64. The exception is a Float106 data payload: Storable stores its two halves as
Fields [("hi", Number), ("lo", Number)] and restores every bit.
An f32 payload widens exactly and reads back exactly; a Number it did not write rounds to
the nearest f32, a magnitude too small for the smallest subnormal becomes a zero of the same sign, and a
finite value beyond the f32 range is ProjectionError::Scalar.
MIT. See LICENSE.