4. The contact graph
catalog/graph.py builds edges between templates: transfer-adjacent (same body pair, and the features are adjacent on both bodies, so one contact can slide into the other) and shared-feature (the two templates use a common feature). It should also carry the exclusion table. This step counts the edges and looks at what is actually stored.
python walkthrough/04_graph.py
The script, 04_graph.py (download)
"""Step 4: the contact graph (catalog-level edges).
ContactGraph builds two edge types over templates: transfer-adjacent (same
body pair, features adjacent on both bodies) and shared-feature (the two
templates share a feature). Count them, and look at what the edge list
actually stores.
"""
from collections import Counter
from cmg.catalog.graph import ContactGraph, EdgeTypes
from _common import STEP_YAML, load_spec, template_names
graph = ContactGraph.from_yaml(str(STEP_YAML))
catalog = graph.catalog
names = template_names(load_spec(), catalog)
print("Edges by type:", dict(Counter(EdgeTypes(t).name for _, _, t in graph.edges)))
print("First three stored edges:", graph.edges[:3])
print("Has an excluded-edge list:", hasattr(graph, "edges_excluded"))
# Recompute with catalog positions so the edges can be read.
index = {id(t): i for i, t in enumerate(catalog)}
first = names.index("B:zn-L:boundary")
print(f"\nNeighbours of {names[first]} (template {first}):")
for t1, t2 in ((a, b) for a in catalog for b in catalog if a is not b):
if index[id(t1)] != first:
continue
kinds = []
if catalog.transfer_adjacent(t1, t2):
kinds.append("transfer-adjacent")
if set(t1.key()) & set(t2.key()):
kinds.append("shared-feature")
if kinds:
print(f" {names[index[id(t2)]]:18s} {', '.join(kinds)}")Output
Edges not provided for ContactGraph. Building edges using the default rules
Edges by type: {'SHARED_FEATURE': 192, 'TRANSFER_ADJACENT': 96}
First three stored edges: [(None, None, <EdgeTypes.SHARED_FEATURE: 1>), (None, None, <EdgeTypes.TRANSFER_ADJACENT: 0>), (None, None, <EdgeTypes.SHARED_FEATURE: 1>)]
Has an excluded-edge list: False
Neighbours of B:zn-L:boundary (template 11):
M:boundary-B:zn shared-feature
B:xp-L:boundary transfer-adjacent, shared-feature
B:xn-L:boundary transfer-adjacent, shared-feature
B:yp-L:boundary transfer-adjacent, shared-feature
B:yn-L:boundary transfer-adjacent, shared-feature
B:zp-L:boundary shared-feature
B:zn-S:boundary shared-feature
B:zn-P:boundary shared-feature
What to notice
- Every stored edge is
(None, None, type). Edges are recorded bytemplate_id, whichCatalog.from_specnever sets (step 2). The edge list cannot be mapped back to templates. The script works around this by recomputing the edges with catalog positions. - The exclusion table is dropped.
ContactGraph.from_specpassesedges_excluded, but__init__never stores it. - Nothing downstream uses the graph.
Cand(step 6) andSearch(step 8) work from the catalog alone, so no exclusion is enforced anywhere today. - Adjacency follows the box’s faces.
B:zn-Lis transfer-adjacent to the four side faces on the floor, not toB:zp-L: opposite faces share no edge. - Shared-feature edges here include shared environment features.
B:zn-LandB:xp-Lshare only the floor’sboundary, yet they are linked. - Each edge appears twice, once in each direction.
Both defects (unset IDs, dropped exclusions) are on the list of code issues for Angelo in the discussion compendium.
Code to read
src/cmg/catalog/graph.py:ContactGraph.__init__and__create_default_edges.src/cmg/catalog/templates.py,Catalog.transfer_adjacent, andsrc/cmg/geometry/features.py, theadjacentmethod ofBoxFaceFeatureandBoxVertexFeature.
Exercises
4.1 Account for the 96 transfer-adjacent edges.
Within one body pair, the other body’s feature is a single boundary, which is adjacent to itself, so two templates are transfer-adjacent exactly when their box faces are adjacent. Each face is adjacent to the four faces on other axes, giving 6 × 4 = 24 ordered pairs per body pair. With four body pairs that makes 4 × 24 = 96.
4.2 Account for the 192 shared-feature edges. How many would there be if only shared object features counted, as the docstring’s name (shared-object-feature) suggests?
Take a template (box face f, other feature b). It shares f with the three other templates that use f (one per other body pair), and b with the five other templates of its own pair. That gives 8 neighbors per template and 24 × 8 = 192 ordered edges. Counting only the shared box face leaves 3 per template, 24 × 3 = 72.
4.3 Fix the missing template IDs so that the stored edges name templates, and check that the test suite still passes.
In src/cmg/catalog/templates.py, Catalog.from_spec, re-create the templates with their positions just before the ExecutionWorld line:
templates = [
Template.create(t.feature1, t.feature2, template_id=i)
for i, t in enumerate(templates)
]
exec_world = ExecutionWorld(spec.execution)Then python walkthrough/04_graph.py prints
First three stored edges: [(0, 1, <EdgeTypes.SHARED_FEATURE: 1>), (0, 2, <EdgeTypes.TRANSFER_ADJACENT: 0>), (0, 2, <EdgeTypes.SHARED_FEATURE: 1>)]
and PYTHONPATH=tests python -m pytest -q tests still reports 6 passed. Templates 0 and 1 (the disk on xp and on xn) share the disk’s boundary but are not transfer-adjacent, because opposite faces share no edge. Undo the change with git checkout src/cmg/catalog/templates.py when you are done.