6. Candidate edits
From a node with formation σ at pose χ, Cand.legal_ops lists the edits the search may hand to the solver. There are 24 makes (set one bit), 24 breaks (clear one bit), and one transport (keep σ and move). An edit to σ′ is legal when
- it changes the formation (σ′ ≠ σ),
- Hold: the contacts that persist, σ ∩ σ′, pass Supported at χ (step 5), and
- Near: every added template has gap <
phi_gate= 0.125 at χ.
Transport is always legal. All 48 make/break checks run in one vmap.
python walkthrough/06_candidates.py
The script, 06_candidates.py (download)
"""Step 6: candidate edits (Cand.legal_ops).
From a node's formation, every single-bit make and break is a candidate,
plus one transport (no change). A make or break is legal when it changes
the formation, the persisting contacts pass Supported (Hold), and every
added template is within phi_gate (Near). Print the legal edits at three
fixtures.
"""
import time
import jax
import jax.numpy as jnp
import numpy as np
from cmg.marking.candidate import Cand
from _common import fixtures, load_spec, on, template_names
spec = load_spec()
cand = Cand.from_spec(spec)
names = template_names(spec, cand.catalog)
n = len(names)
legal_ops = jax.jit(lambda q: cand.legal_ops(config=q))
for fixture in ("flat", "flat_stable_close", "toppled30_close"):
qpos = jnp.array(fixtures()[fixture])
start = time.perf_counter()
ops, types = legal_ops(qpos)
ops = np.asarray(ops)
elapsed = time.perf_counter() - start
formation = cand.catalog.band(config=qpos)
print(f"== {fixture} ({elapsed:.1f} s)")
print(" formation:", on(formation, names))
print(" make: ", on(ops[:n], names))
print(" break:", on(ops[n:2 * n], names))
print(" transport:", bool(ops[2 * n]))Output
== flat (22.6 s)
formation: {B:xp-L:boundary, B:xn-L:boundary, B:yp-L:boundary, B:yn-L:boundary, B:zn-L:boundary}
make: {M:boundary-B:xn, M:boundary-B:yp, M:boundary-B:yn, M:boundary-B:zp, M:boundary-B:zn, B:zp-L:boundary, B:xp-S:boundary, B:yp-S:boundary, B:yn-S:boundary, B:zp-S:boundary, B:zn-S:boundary, B:xp-P:boundary, B:yp-P:boundary, B:yn-P:boundary, B:zp-P:boundary, B:zn-P:boundary}
break: {B:xn-L:boundary, B:yn-L:boundary, B:zn-L:boundary}
transport: True
== flat_stable_close (0.0 s)
formation: {M:boundary-B:xn, B:xp-L:boundary, B:xn-L:boundary, B:yp-L:boundary, B:yn-L:boundary, B:zn-L:boundary, B:xp-S:boundary, B:yp-S:boundary, B:yn-S:boundary, B:zn-S:boundary, B:xp-P:boundary, B:yp-P:boundary, B:yn-P:boundary}
make: {M:boundary-B:yp, M:boundary-B:yn, M:boundary-B:zp, M:boundary-B:zn, B:zp-L:boundary, B:xn-S:boundary, B:zp-S:boundary, B:xn-P:boundary, B:zp-P:boundary, B:zn-P:boundary}
break: {M:boundary-B:xn, B:xn-L:boundary, B:yn-L:boundary, B:zn-L:boundary, B:xp-S:boundary, B:yp-S:boundary, B:yn-S:boundary, B:zn-S:boundary, B:xp-P:boundary, B:yp-P:boundary, B:yn-P:boundary}
transport: True
== toppled30_close (0.0 s)
formation: {M:boundary-B:xn, B:xp-L:boundary, B:yp-L:boundary, B:yn-L:boundary, B:zn-L:boundary, B:xp-S:boundary, B:yp-S:boundary, B:yn-S:boundary, B:xp-P:boundary, B:yp-P:boundary, B:yn-P:boundary}
make: {M:boundary-B:yp, M:boundary-B:yn, M:boundary-B:zp, M:boundary-B:zn, B:xn-L:boundary, B:zp-L:boundary, B:xn-S:boundary, B:zp-S:boundary, B:zn-S:boundary, B:xn-P:boundary, B:zp-P:boundary, B:zn-P:boundary}
break: {B:xp-L:boundary, B:yn-L:boundary, B:zn-L:boundary, B:xp-S:boundary, B:yp-S:boundary, B:yn-S:boundary, B:xp-P:boundary, B:yp-P:boundary, B:yn-P:boundary}
transport: True
The first call compiles (about 22 s); later calls are fast.
What to notice
- Makes are cheap to admit. A make keeps every current contact, so its hold test is just “is the current formation supported”, and Near is a distance threshold. At
flat16 of the 19 unset bits pass. - Near admits geometric nonsense.
B:zp-L(the box’s top face onto the floor) is legal atflat: its gap, 0.125, is just underphi_gate(the kernels subtract about 1e−6). So are the disk on the box’s ±y faces, which the disk cannot reach because it never moves in y. The solver must reject each of them, one expensive call at a time (steps 7 and 8). - Breaks are where Hold bites. At
flatonly 3 of the 5 floor bits may be cleared; see exercise 6.2. - No exclusion is checked, because
Candnever consults the contact graph (step 4). - The formation is recomputed from the band when none is supplied, as here. Inside the search, a node’s stored formation is used instead.
Code to read
src/cmg/marking/candidate.py: Cand.legal_ops, Cand.near, Cand.hold (the keep-set is new_formation & old_formation, which is the persistent-contact rule of Decision 7, bit for bit), Cand._hold_and_new (the σ′ ≠ σ check), and Cand.apply_op (how an operation index becomes a formation).
Exercises
6.1 Why is making M:boundary-B:xp illegal at flat?
Near fails: its gap is 0.14687 (step 2), above phi_gate = 0.125. The disk would have to travel around the box to reach its front face.
6.2 At flat, why are exactly B:xn-L, B:yn-L and B:zn-L breakable, and not B:xp-L or B:yp-L?
Use the midpoints from exercise 5.1. B:xn-L, B:yn-L and B:zn-L all sit at the same corner (−0.1625, −0.0625), so clearing any one of them leaves the same three distinct support points, and the box stays (marginally) supported. B:xp-L is the only contact at (−0.0375, −0.0625), and B:yp-L the only one at (−0.1625, 0.0625). Clearing either leaves two support points, and the box cannot be balanced on them. Which bits may be dropped is decided by which corner the closest-point routine happens to pick, not by the physics.
6.3 At toppled30_close, why is breaking M:boundary-B:xn illegal, and why is making B:xn-L legal?
The break would leave only environment contacts, and those fail EnvStable there (step 5): the box would fall back. The make keeps every current contact, which is supported, and the box’s −x face is within 0.125 of the floor because the box is tilted only 30°. Hold says nothing about whether the make can be realized; that is the solver’s job.