Simulation Monitoring
Agent-based models produce emergent behaviors -- resource hoarding, cascade failures, oscillating populations. These behaviors are hard to predict but easy to describe as temporal patterns. Sifting detects them as they happen.
| Time | ~15 minutes |
| Prerequisites | What is Sifting? |
1. Resource Hoarding
An agent acquires resources repeatedly without ever sharing. The pattern triggers when the same agent acquires two different resources with no sharing event between them.
Result: agent_alpha matches -- acquired food then water (and water then shelter) with no sharing between acquisitions. agent_beta acquired food, shared at t=4, then acquired water -- the sharing event between acquisitions kills the match via unless between.
The variable ?agent joins both acquisitions and the negation clause to the same agent. A sharing event by a different agent does not suppress the match. The pattern fires for each consecutive pair of hoarding acquisitions, so a persistent hoarder generates multiple matches.
2. Cascade Failure
One agent's failure triggers a dependent agent's failure. The pattern chains two failures through a shared dependency variable and uses negation to exclude cases where the first agent recovered in time.
Result: 1 match -- power_plant failed at t=1, factory depends on power_plant, factory failed at t=3, and power_plant never recovered. water_plant also failed, and farm depends on it, but water_plant recovered at t=3 before farm could fail at t=5 -- the unless between negation kills that cascade.
The dependency relationship is an edge in the graph, not hardcoded in the pattern. The variable ?agent_a threads from the first failure through the dependency edge to the negation clause. Any dependency topology the simulation produces -- chains, trees, cycles -- is surfaced by the same pattern.
3. Population Oscillation
A population rises then falls in the same region. This two-stage pattern detects boom-bust cycles by joining on a shared region variable.
Result: region_north matches -- population increased at t=1 (and again at t=3), then decreased at t=5. region_south has two increases but no decrease, so no match. The pattern fires once per increase-decrease pair in the same region, so region_north produces two matches (t=1 to t=5, and t=3 to t=5).
No negation needed. The pattern is purely structural: rise then fall in the same region. To detect sustained oscillation (rise-fall-rise-fall), compose two instances with sequence or add more stages. To require a minimum gap between rise and fall, add temporal e1 before e2 gap 3..100.
Real-Time Monitoring with Incremental Mode
The playgrounds above use batch evaluation. In a running simulation, use incremental mode instead: feed edges as they happen and react immediately.
let mut engine: SiftEngineFor<MemGraph> = SiftEngine::new();
let mut graph = MemGraph::new();
engine.register(
PatternBuilder::new("resource_hoarding")
.stage("e1", |s| {
s.edge("e1", "type".into(), MemValue::Str("acquire".into()))
.edge_bind("e1", "agent".into(), "agent")
.edge_bind("e1", "resource".into(), "r1")
})
.stage("e2", |s| {
s.edge("e2", "type".into(), MemValue::Str("acquire".into()))
.edge_bind("e2", "agent".into(), "agent")
.edge_bind("e2", "resource".into(), "r2")
})
.unless_between("e1", "e2", |neg| {
neg.edge("mid", "type".into(), MemValue::Str("share".into()))
.edge_bind("mid", "agent".into(), "agent")
})
.build(),
);
// Simulate a few ticks of an agent-based model.
struct SimEvent {
id: String,
kind: String,
agent: String,
resource: String,
}
let ticks: Vec<(i64, Vec<SimEvent>)> = vec![
(
1,
vec![SimEvent {
id: "ev1".into(),
kind: "acquire".into(),
agent: "alpha".into(),
resource: "food".into(),
}],
),
(
2,
vec![SimEvent {
id: "ev2".into(),
kind: "acquire".into(),
agent: "alpha".into(),
resource: "water".into(),
}],
),
];
let mut completed = Vec::new();
for (tick, sim_events) in &ticks {
for event in sim_events {
graph.add_str(&event.id, "type", &event.kind, *tick);
graph.add_ref(&event.id, "agent", &event.agent, *tick);
graph.add_ref(&event.id, "resource", &event.resource, *tick);
graph.set_time(*tick);
let interval = Interval::open(*tick);
// Notify the engine about the type edge (trigger clause).
let mut sift_events = engine.on_edge_added(
&graph,
&event.id,
&"type".to_string(),
&MemValue::Str(event.kind.clone()),
&interval,
);
// Also notify about the agent edge.
sift_events.extend(engine.on_edge_added(
&graph,
&event.id,
&"agent".to_string(),
&MemValue::Node(event.agent.clone()),
&interval,
));
for se in &sift_events {
if let SiftEvent::Completed {
pattern, bindings, ..
} = se
{
println!("[tick {}] detected: {} {:?}", tick, pattern, bindings);
completed.push(pattern.clone());
}
}
}
let (_delta, _expired) = engine.end_tick(50);
}
Call on_edge_added() for each edge produced by the simulation. Call end_tick() once per round to finalize the tick, expire stale partial matches, and produce a TickDelta for narrative scoring. React to SiftEvent::Completed to trigger simulation-level responses -- spawn rescue agents, adjust resource allocation, log anomalies.
Mapping your data
Agent-based model events map to fabula edges as follows:
| Real-world field | Fabula edge |
|---|---|
| eventID or step+agentID | source node |
| action type | label value |
| agent, target entity, resource | target nodes |
| simulation step | interval start |
Each simulation step produces edges for agent actions. The agent and resource fields become target nodes, so patterns can join across events by the same agent or involving the same resource.
How fabula compares
- vs custom observer code: Hard-coded callbacks that check specific conditions each tick. No gap analysis (you cannot ask "how close did we get to a cascade?"), no composition for building complex detection from reusable fragments, no variable-scoped negation.
- vs Flink CEP: Complex event processing over flat event streams. No graph topology -- Flink patterns match sequences of events, not events connected by shared entities in a graph. No Allen algebra for temporal relations, no incremental partial match tracking with negation windows.
Where to go next
- Incremental Integration -- Full walkthrough of the incremental API with memory management and scoring.
- How the Engine Works -- The four-phase evaluation algorithm under the hood.
- Scoring Reference -- Narrative quality scoring for MCTS evaluation.