Use Case: Sports Analytics
Sports analytics is fundamentally an operational world model problem — the persistence and query layer, not the prediction layer. 22 players tracked at 50Hz. Formations that emerge from spatial relationships. Events detected from position history. Historical queries at any moment in the match. Neural world model outputs (pose estimates, trajectory predictions) arrive as _observation_class: 'predicted' facts alongside camera-confirmed positions. ArcFlow stores all of it, versions all of it, and makes all of it queryable with one syntax.
The problem#
A sports world model requires all of these simultaneously:
- Spatial tracking — player positions at 50Hz (22 players, 90 minutes = 5.94M data points)
- Temporal queries — "Where was player X at minute 35?" —
AS OF seq Non the world model - Graph relationships — team membership, passing networks, marking assignments
- Real-time algorithms — nearest player to ball, formation detection, offside detection
- Confidence scoring — distinguishing high-accuracy optical tracking from radar estimates
Why ArcFlow#
One world model engine handles spatial, temporal, graph, and real-time computation. No separate databases for positions, events, and relationships. The spatial-temporal graph is the single source of truth.
What becomes possible: formation patterns across 90 minutes become a graph query, not a video review task. Pressure networks emerge from player spatial relationships without manual annotation. Offside detection runs on the live graph during play, not post-hoc. And temporal replay means you can reconstruct any moment of the match — exact positions, exact events, exact confidence scores — for coaching analysis or broadcast enhancement.
Implementation#
import { open } from 'arcflow'
const db = open('./match-data')
// Create players with positions
db.batchMutate([
"CREATE (p:Player {id: 'p1', name: 'Player-10', team: 'home', number: 10, position: point({x: 52.3, y: 34.1}), speed: 7.2})",
"CREATE (p:Player {id: 'p2', name: 'Player-07', team: 'home', number: 7, position: point({x: 65.0, y: 20.5}), speed: 4.1})",
"CREATE (ball:Ball {id: 'ball', position: point({x: 50.0, y: 33.5}), speed: 12.4})",
])
// Spatial: nearest players to ball (ArcFlow Spatial Index, ≥ 2000/s at 22 entities)
db.query("CALL algo.nearestNodes(ball.position, 'Player', 5) YIELD node, distance RETURN node.name, distance")
// Spatial: offside check — attacking players ahead of last defender
db.query(`
MATCH (defender:Player)-[:PLAYS_FOR]->(defending:Team {side: 'defending'})
WITH max(defender.position.x) AS last_defender_x
MATCH (attacker:Player)-[:PLAYS_FOR]->(attacking:Team {side: 'attacking'})
WHERE attacker.position.x > last_defender_x
RETURN attacker.name, attacker.position.x AS attacker_x, last_defender_x
`)
// Live geofence: fires the instant a player enters or leaves the penalty area
db.subscribe(
`MATCH (p:Player)
WHERE p.position.x >= 83 AND p.position.x <= 105
AND p.position.y >= 13 AND p.position.y <= 55
RETURN p.name, p.team`,
(event) => {
for (const row of event.added) console.log(`${row.get('name')} entered penalty area`)
for (const row of event.removed) console.log(`${row.get('name')} left penalty area`)
}
)
// Algorithms: passing network centrality
db.query("CALL algo.pageRank()")
// Algorithms: player clusters (formation detection)
db.query("CALL algo.louvain()")50fps Tracking Loop#
// Update position each frame — spatial index auto-updates in O(log N)
function onPositionUpdate(playerId: string, x: number, y: number) {
db.mutate(
"MATCH (p:Player {id: $id}) SET p.position = point({x: $x, y: $y})",
{ id: playerId, x, y }
)
// Live geofences fire automatically — no polling needed
}See also#
- Spatial Queries — full spatial primitives reference
- Programs — bundle YOLO detectors, player trackers, and PTZ controllers into installable manifests with hardware validation
- Triggers — fire a skill automatically when a new
:Frameor:Eventnode enters the graph - Live Queries — standing queries for real-time formation and zone tracking
- examples/sports-tracking/ — runnable example