Working with Temporal Graphs and Bi-Temporal Facts in Semantica: A Complete Guide
Semantica implements bi-temporal knowledge graphs through the BiTemporalFact class, separating valid time (when facts were true in reality) from recorded time (when the system learned them) to enable deterministic time-travel queries and complete audit trails.
The semantica-agi/semantica repository provides native support for working with temporal graphs and bi-temporal facts, allowing developers to track relationship provenance across both real-world validity periods and system transaction timestamps. This architecture ensures that knowledge graphs remain queryable at any historical point without requiring data re-ingestion or destructive updates.
Understanding the Bi-Temporal Data Model
Semantica distinguishes between two orthogonal time dimensions. Valid time represents when a fact was true in the real world, while recorded time (also called transaction time) marks when the system ingested that knowledge. This separation ensures that correcting historical records never destroys audit trails, and that retroactive data entry preserves the actual timeline of events.
Core Implementation in temporal_model.py
The foundation resides in semantica/kg/temporal_model.py, where the BiTemporalFact class wraps relationship dictionaries with four critical temporal fields:
valid_fromandvalid_until– Define the real-world validity intervalrecorded_at– Captures when the system learned the fact (defaults to current UTC)superseded_at– Marks logical deletion or versioning timestamps
The sentinel value TemporalBound.OPEN represents open-ended intervals that extend infinitely into the future. Helper functions including parse_temporal_value, serialize_temporal_value, and deserialize_relationship_temporal_fields handle conversion between ISO-formatted strings and timezone-aware Python datetime objects.
Storing Temporal Facts via ContextGraph
The ContextGraph class in semantica/context.py (delegating to semantica/kg/graph_builder.py) exposes bi-temporal capabilities through the add_edge method. When creating relationships, you explicitly declare the valid time window, while the system automatically populates the recorded timestamp.
from semantica.context import ContextGraph
from datetime import datetime
# Initialize graph with temporal support
graph = ContextGraph(advanced_analytics=True)
# Add entities
graph.add_node("alice_chen", "Person", role="VP Engineering")
graph.add_node("acme_corp", "Organization", valuation=1_200_000_000)
# Create bi-temporal edge: Alice worked at Acme from March 2024 to January 2025
graph.add_edge(
"alice_chen",
"acme_corp",
edge_type="works_for",
valid_from="2024-03-01T00:00:00Z",
valid_until="2025-01-01T00:00:00Z",
)
Omitting valid_until or passing TemporalBound.OPEN creates a fact that remains valid indefinitely. The underlying storage preserves these intervals as BiTemporalFact instances, ensuring that subsequent updates create new temporal versions rather than overwriting historical truth.
Querying Historical Graph States
Semantica provides two complementary approaches for working with temporal graphs and bi-temporal facts: absolute point-in-time snapshots and range-based interval queries.
Point-in-Time Snapshots with state_at
The state_at method returns a complete graph snapshot representing all facts that were valid at a specific historical instant. This enables "time-travel" debugging and retrospective analysis without database restoration.
# Retrieve graph state as of June 15, 2024
snapshot_2024 = graph.state_at("2024-06-15")
This method filters the underlying BiTemporalFact instances, returning only those where valid_from <= query_time < valid_until and the fact has not been superseded.
Range Queries with TemporalGraphQuery
For analytical workloads requiring interval overlap detection, semantica/kg/temporal_query.py provides TemporalGraphQuery with Allen-interval algebra support.
from semantica.kg.temporal_query import TemporalGraphQuery
tq = TemporalGraphQuery()
# Find all facts valid during Q1-Q4 2024
facts_2024 = tq.query_time_range(
kg=graph.to_kg_dict(),
query="valid_facts",
start_time="2024-01-01",
end_time="2024-12-31",
)
Methods like query_time_range leverage the bi-temporal indices to efficiently retrieve facts overlapping arbitrary windows, supporting both retrospective analysis and temporal join operations across different versions of the knowledge graph.
Visualizing Temporal Evolution
The TemporalVisualizer in semantica/visualization/temporal_visualizer.py transforms bi-temporal datasets into interactive HTML timelines using Plotly. It consumes the same payload format produced by state_at, rendering edge lifetimes and metric evolution with scrubbable time controls.
from semantica.visualization import TemporalVisualizer
viz = TemporalVisualizer()
# Generate interactive browser visualization
viz.visualize_timeline(
temporal_kg=graph.to_kg_dict(),
output="interactive",
file_path="temporal_demo.html"
)
This component enables analysts to visually correlate graph topology changes with external events, identifying exactly when specific relationships formed or dissolved across the valid-time axis.
Summary
- BiTemporalFact in
semantica/kg/temporal_model.pyseparatesvalid_from/valid_until(real-world time) fromrecorded_at/superseded_at(system time), usingTemporalBound.OPENfor indefinite intervals. - ContextGraph automatically stores temporal edges as bi-temporal facts and populates
recorded_attimestamps, while allowing explicit control over valid-time windows. - TemporalGraphQuery provides efficient range queries and Allen-interval algebra for analyzing facts across arbitrary time windows without data reprocessing.
- TemporalVisualizer converts temporal graph states into interactive HTML timelines for exploratory analysis and debugging.
Frequently Asked Questions
What is the difference between valid time and recorded time in Semantica?
Valid time refers to when a fact was actually true in the real world, controlled via the valid_from and valid_until parameters when calling add_edge. Recorded time (stored in recorded_at) indicates when the system ingested that fact, automatically set to the current UTC timestamp unless overridden. This bi-temporal distinction ensures that late-arriving data or corrections preserve the original real-world timeline while maintaining audit trails.
How do I represent open-ended temporal intervals?
Use the sentinel TemporalBound.OPEN from semantica/kg/temporal_model.py as the valid_until value when calling graph.add_edge(). This represents a fact that remains valid indefinitely rather than expiring at a specific timestamp. The temporal query engines recognize this sentinel value when evaluating interval overlaps and point-in-time snapshots.
Can I query the graph state at a specific historical date?
Yes. The ContextGraph.state_at() method accepts ISO-formatted date strings or datetime objects and returns a snapshot containing only the BiTemporalFact instances that were valid at that specific instant. This operates directly on the stored bi-temporal facts without requiring data re-ingestion or separate historical archives.
What file contains the core bi-temporal fact implementation?
The class definition and serialization logic reside in semantica/kg/temporal_model.py. This module defines BiTemporalFact, TemporalBound.OPEN, and helper functions including parse_temporal_value and deserialize_relationship_temporal_fields that normalize timezone-aware datetimes and handle ISO string conversion throughout the bi-temporal stack.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →