scene_service.scene_graph.geometry

Deterministic geometric relation predicates and cache-invalidation signatures, shared by the scene-graph builder/store.

This is the single source of truth for “what the bounding boxes alone say” — the contact/containment judgements that were historically split between state/relations.py (the 1 Hz RelationEngine) and the GeometryHint cues in scene_graph/relations.py. Operating on SceneGraphNode.bbox_center / bbox_extent keeps it usable from the scene-graph layer without importing the state-layer structs.

Two roles:
  • geometric_relation — when geometry alone is decisive, name the relation (so the builder can later skip the LLM); otherwise return None and leave it to the LLM.

  • geometry_signature — a coarse, jitter-stable fingerprint of a pair’s relational geometry, used as part of the relation cache key so a cached edge invalidates when the spatial configuration actually changes (not on every EMA position wobble).

Functions

compute_geometric_edges(nodes)

Deterministic contact/containment edges over a node list.

geometric_relation(a, b)

Return (relation, confidence) from a to b when the bounding boxes alone decide it, else None (defer to the LLM).

geometry_signature(hint)

Coarse, jitter-stable fingerprint of a pair's relational geometry.

scene_service.scene_graph.geometry.compute_geometric_edges(nodes: list[SceneGraphNode]) list[SceneGraphEdge][source]

Deterministic contact/containment edges over a node list.

For each unordered pair (i<j) ask geometric_relation and emit a method="geometric" edge only when geometry decides a contact or containment relation (on_top_of/under/inside/contains). near and ambiguous (None) pairs are skipped: proximity is served separately (get_object_context.nearby_objects) and ambiguous nesting is left to the LLM. One edge per pair (the inverse direction is implied — consumers read edges touching either endpoint).

reachable_by is NOT produced here: it needs the gripper object, which lives in the registry, not in this node set — the fast loop adds it. O(N²) over a small node set (N<80); each call is cheap.

scene_service.scene_graph.geometry.geometric_relation(a: SceneGraphNode, b: SceneGraphNode) tuple[str, float] | None[source]

Return (relation, confidence) from a to b when the bounding boxes alone decide it, else None (defer to the LLM).

Decision order (strongest first):
  • a rests on bon_top_of (high)

  • b rests on aunder (high)

  • a’s center inside b (only) → inside (high)

  • b’s center inside a (only) → contains (high)

  • mutual containment / ambiguous nesting → None (LLM disambiguates)

  • centers within proximity radius → near (medium)

  • otherwise → None

Returns geometric-spatial relations only; semantic relations (attached_to / part_of / same_object) are never produced here and remain the LLM’s job.

scene_service.scene_graph.geometry.geometry_signature(hint: GeometryHint) str[source]

Coarse, jitter-stable fingerprint of a pair’s relational geometry.

Quantizes the continuous cues (distance, xy_overlap) into buckets and keeps the already-discrete cues (vertical_order, containment) as-is. Same spatial configuration → same string (cache hit, no recompute); a real change → different string (cache miss → re-evaluate). Unlike a raw-coordinate key it does not drift on sub-bucket EMA jitter.