Composable Primitives for Spatiotemporal Reasoning
Joe Scanlin
November 2025
This section explores Spaxiom's type system and composition model. Unlike ad-hoc sensor integration scripts, Spaxiom enforces type safety at DSL construction time and provides algebraic composition operators that enable complex spatiotemporal queries to be built from simple building blocks.
You'll learn about the core type hierarchy (Sensor, Condition, Zone, Entity, Quantity), composition operators (logical, temporal, spatial), and how these primitives combine to express complex behaviors concisely. The section concludes with formal semantics and performance characteristics of the composition model.
Spaxiom's expressiveness comes from a small set of composable primitives with a well-defined type system. Unlike ad-hoc sensor integration scripts, Spaxiom enforces type safety at DSL construction time and provides algebraic composition operators that enable complex spatiotemporal queries to be built from simple building blocks.
The DSL defines the following fundamental types:
Sensor: base abstraction for any physical or virtual sensor. Key methods:
read() → Value: synchronous read of current sensor stateposition → (x, y, z): 3D location in spacezone → Zone: containing spatial regionunits → Unit: physical units (e.g., meters, celsius, pascal)Condition: boolean-valued predicate over sensor state and time. Conditions are lazy: they are not evaluated until subscribed or explicitly triggered. Key properties:
holds() → bool: evaluate condition at current timesince_true → float: seconds since condition became true (or 0.0 if false)since_false → float: seconds since condition became false (or 0.0 if true)Zone: 3D spatial region with geometric operations. Supports axis-aligned boxes, convex polygons, and union/intersection:
contains(x, y, z) → booloverlaps(other_zone) → booldistance_to(x, y, z) → floatvolume → floatEntity: tracked object with identity and attributes. Examples: a person, robot, forklift, or pallet. Entities have:
id → str: unique identifierposition → (x, y, z): current location (if trackable)velocity → (vx, vy, vz): optional velocity vectorattributes → dict: arbitrary key-value metadataQuantity: numeric value with physical units. Enforces dimensional analysis:
value → floatunit → UnitConditions form a boolean algebra with standard operators:
from spaxiom import Condition
# Logical operators
c1 = Condition(lambda: temp.read() > 25.0)
c2 = Condition(lambda: humidity.read() > 60.0)
hot_and_humid = c1 & c2 # conjunction (AND)
hot_or_humid = c1 | c2 # disjunction (OR)
not_hot = ~c1 # negation (NOT)
# Temporal chaining
humid_then_hot = c2.before(c1, within_seconds=300) # c2 before c1, within 5 min
Importantly, these operators are not evaluated eagerly. They construct a lazy evaluation graph that the runtime optimizes.
Spaxiom provides temporal logic operators inspired by Linear Temporal Logic (LTL) but adapted for continuous real-time systems:
within(duration, condition): condition holds continuously for at least duration seconds.
sustained_motion = within(5.0, motion_detected) # motion for ≥5s
always(duration, condition): condition holds at all sampled times over the past duration seconds (similar to within but checks discrete samples).
stable_temp = always(60.0, temp_in_range) # stable for 1 minute
eventually(duration, condition): condition becomes true at least once within the next duration seconds (forward-looking, requires buffering or prediction).
# Predictive: will door open in next 10s?
door_will_open = eventually(10.0, door_sensor_active)
before(c1, c2, within_seconds=T): condition c1 occurs before c2 within time window T.
alarm_then_evacuation = alarm_triggered.before(exit_door_opened, within_seconds=120)
after(c1, c2, within_seconds=T): condition c1 occurs after c2 within time window T.
These operators enable expressive temporal patterns without manually managing timers or state machines.
Spatial queries are first-class in Spaxiom. Common patterns:
inside(entity, zone): returns a Condition that is true when entity is within zone.
robot_in_hazard_zone = inside(robot_entity, hazard_zone)
near(entity1, entity2, threshold): true when Euclidean distance < threshold.
collision_risk = near(robot, human, threshold=2.0) # within 2 meters
overlaps(zone1, zone2): true if zones intersect.
distance(entity, zone): returns Quantity in meters.
dist_to_exit = distance(person, exit_zone)
far_from_exit = Condition(lambda: dist_to_exit.value > 10.0)
Spaxiom performs compile-time validation where possible:
Quantity objects check dimensional compatibility. For example:
from spaxiom.units import meters, seconds, kg
distance = 10.0 * meters
time = 5.0 * seconds
velocity = distance / time # OK: m/s
mass = 70 * kg
invalid = distance + mass # TypeError: incompatible units (length + mass)
&, |, ~) only accept Condition objects, preventing accidental confusion with Python's native boolean operators.
Zone methods enforce 3D coordinate tuples or Entity objects with positions.
Runtime validation handles cases that cannot be checked statically:
SensorException with configurable retry/fallback policies.eventually failing to observe condition) can fire explicit callbacks.Combining these primitives enables concise expression of complex behaviors. Example: detect a "loitering near exit" pattern.
from spaxiom import Sensor, Zone, Entity, Condition, within, inside, near
from spaxiom.units import meters
# Spatial setup
exit_zone = Zone(x=10, y=20, width=3, height=2)
exit_vicinity = exit_zone.buffer(5 * meters) # 5m buffer around exit
# Entity tracking
person = Entity(id="person_42")
# Conditions
near_exit = inside(person, exit_vicinity)
not_exiting = ~inside(person, exit_zone)
stationary = Condition(lambda: person.velocity.magnitude() < 0.1) # < 0.1 m/s
# Composite pattern: near exit, not moving, for 30+ seconds
loitering = within(30.0, near_exit & not_exiting & stationary)
@on(loitering)
def alert_security():
print(f"Person {person.id} loitering near exit at {person.position}")
This single composite condition would require dozens of lines of manual state tracking, timers, and geometric computations in a traditional imperative approach.
For verification purposes (see Section 7.3), we can give Spaxiom conditions a denotational semantics as functions from time to truth values:
Where ⟦c⟧(t) is the truth value of condition c at time t. Composition operators have natural interpretations:
This formal interpretation enables model checking and equivalence proofs between Spaxiom programs and temporal logic formulas.
Lazy evaluation and operator fusion are critical for performance:
Condition objects are not evaluated until subscribed via @on() or explicitly polled. This avoids wasted computation on conditions that are never used.within(5.0, c1) & within(5.0, c2) shares a single 5-second time buffer rather than maintaining two separate buffers.within use circular buffers and incremental updates, avoiding full re-evaluation on every sensor tick.&, |) short-circuit when possible, skipping expensive sensor reads if the result is already determined.These optimizations make Spaxiom practical even on resource-constrained edge devices with hundreds of sensors and conditions.