ctde_v0.controller¶
controller
¶
L1 controller + goal-candidate stencil + the mission-safety mechanism.
The multi-level (L3->L1) action stack lives here. L3 (the goal head in
nets.Actor) picks one of K candidate relative waypoints; this module turns
that choice into the actual env move, emitting ONLY valid 1-step moves (STAY
fallback). The simulator therefore still sees movement-only actions and the
100-step budget is unchanged (agent_architecture.md L2/L1; EXPERIMENT_PLAN 1a').
Pieces¶
- :func:
goal_stencil— the fixed K relative offsets (center + 8 compass dirs atstride, in ABSOLUTE cells so the goal geometry is scale-invariant). - :func:
goal_targets— absolute goal cell for every agent × candidate. - :func:
greedy_move— the L1 controller: of the env-valid moves (dynamics.targets/action_mask), take the one that most reduces Chebyshev distance to the chosen goal; STAY if none helps. - :func:
occupied_cell_mask— (N,A) bool flagging actions whose committed cell is the CURRENT cell of another agent (the hard collision-mask signal); both controllers canforbid_collisionto remove those actions before argmin/max. - :func:
team_lambda2_after_action— true λ₂ the team would have after a proposed joint move (used by the action-mask mechanism to score candidates). - :func:
candidate_first_moves— for each agent × candidate, the env move the greedy controller would take FIRST (so the mechanism can mask a candidate by the connectivity it would cause).
Everything is pure JAX (vmap/scan-safe). The env (its dynamics /
action_mask tables) is the single source of truth for wall-awareness.
goal_stencil
¶
goal_stencil(K, stride)
(K, 2) int32 relative offsets — the first K of the compass stencil,
each scaled by stride (cell 0 = here, unscaled).
Source code in experiments/ctde_v0/controller.py
54 55 56 57 58 59 60 61 | |
goal_targets
¶
goal_targets(pos, stencil, h, w)
(N, K, 2) int32 absolute goal cells (clipped in-bounds) for each agent ×
candidate. pos (N,2), stencil (K,2).
Source code in experiments/ctde_v0/controller.py
64 65 66 67 68 69 70 | |
occupied_cell_mask
¶
occupied_cell_mask(pos, valid_targets)
(N, A) bool — True where action a's committed target valid_targets[i,a]
lands on the CURRENT cell of ANY OTHER agent j != i (the hard collision
signal). Exact integer-cell match, broadcast over agents.
STAY (valid_targets[i, STAY] == pos[i], the agent's own cell) is NEVER
flagged: an agent's own current cell does not count as "occupied by another",
and we force STAY's column off regardless so it always remains selectable.
NOTE: this forbids moving onto a cell another agent occupies RIGHT NOW. The remaining
case — two agents simultaneously claiming the same EMPTY cell under the NoCollision env
semantics — is closed by :func:resolve_target_conflicts (deterministic index priority),
applied to the chosen moves after selection. The two together GUARANTEE no two agents share
a cell after the step (verified by tests/test_collision_free.py).
Source code in experiments/ctde_v0/controller.py
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | |
resolve_target_conflicts
¶
resolve_target_conflicts(move, valid_targets)
(N,) resolved move — the hard-collision COMPLETION of :func:occupied_cell_mask.
occupied_cell_mask forbids stepping onto a cell occupied RIGHT NOW, but two agents can
still commit to the same EMPTY cell in a single tick (the documented NoCollision hole). This
closes it by DETERMINISTIC INDEX PRIORITY: among all agents whose committed target cell
valid_targets[i, move[i]] is identical, only the LOWEST-index agent proceeds; every
higher-index agent reverts to STAY (its own current cell).
Together with occupied_cell_mask (no move targets a currently-occupied cell) and distinct
start cells this GUARANTEES no two agents share a cell after the step: convergence is broken
here; moving-onto-occupied is masked upstream; and a reverted agent lands on its own cell,
which no other agent can target (that cell was occupied, hence already masked). Deterministic
and jit-stable (no data-dependent shapes).
Source code in experiments/ctde_v0/controller.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | |
greedy_move
¶
greedy_move(pos, goal, valid_targets, action_valid, forbid_collision=False)
(N,) int32 — L1 greedy controller move toward goal.
For each agent: among env-VALID actions (action_valid (N,A) bool, from
env.action_mask), pick the one whose committed cell (valid_targets
(N,A,2), from dynamics.targets) minimizes Chebyshev distance to the goal.
Ties and "no move helps" fall back to STAY (STAY is always valid). The result
is always a valid move, so :class:SequentialClaim/the env never reverts it.
goal (N,2) int32 — the chosen absolute goal cell per agent. When
forbid_collision is True the hard collision-mask (:func:occupied_cell_mask)
removes actions whose target sits on another agent's CURRENT cell (their
distance is set to +inf before the argmin); STAY always stays selectable.
Source code in experiments/ctde_v0/controller.py
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | |
nav_distance_field
¶
nav_distance_field(goal, blocked, planner='wavefront')
(H,W) float32 distance-to-goal field over the FREE cells of blocked (True =
obstacle), the L2 nav-field. planner (Python-static) selects the solver:
"wavefront"/"bfs"— BFS distance transform (Jacobi min-plus relaxation)."astar"— reuses the BFS field. Descending a shortest-path field reconstructs the FIRST move of an A shortest path; a real priority-queue A is not vmap-safe, and the emitted 1-step move is identical, so the field is shared."fmm"— fast-sweeping Eikonal approximation (:func:_fmm_...).
Iterations are fixed from the grid size (2*(H+W) BFS sweeps / 4 fast-sweep rounds) —
enough to fully propagate an open grid and route moderate wall detours; the grid is
small (agent_architecture.md L2). Pure JAX (vmap/scan/jit-safe).
Source code in experiments/ctde_v0/controller.py
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 | |
navfield_move
¶
navfield_move(pos, goal, blocked, valid_targets, action_valid, planner='wavefront', forbid_collision=True)
(N,) int32 — the L2 nav-field + L1 reactive controller move toward goal.
For each agent: plan a distance field to its goal over its OWN KB occupancy
blocked[i] (:func:nav_distance_field), then among the env-VALID actions take the
one whose committed cell (valid_targets) most reduces the field — descending the
nav-field gradient one step. The reactive collision veto (the hard
:func:occupied_cell_mask, on by default) removes moves onto a cell another agent
occupies NOW; STAY is always selectable and is the fallback when no valid move strictly
improves on staying (so the emitted move is always env-valid, exactly like
:func:greedy_move). On open terrain the field is Chebyshev distance -> this reproduces
the greedy move; around known walls it routes around them.
blocked (N,H,W) bool — per-agent obstacle map (known walls; unknown/known-free are
traversable, optimistic). planner (Python-static) picks the field solver.
Source code in experiments/ctde_v0/controller.py
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 | |
candidate_first_moves
¶
candidate_first_moves(pos, goal_cells, valid_targets, action_valid)
(N, K) int32 — the FIRST greedy move each agent would take for every candidate goal. Used by the action-mask mechanism to evaluate the connectivity each candidate would cause.
goal_cells (N,K,2). Vmaps :func:greedy_move over the K candidates.
Source code in experiments/ctde_v0/controller.py
321 322 323 324 325 326 327 328 329 330 331 332 333 | |
relay_move
¶
relay_move(pos, valid_targets, action_valid, comm_r, sharp, forbid_collision=False)
(N,) int32 — relay L1 controller: each relay agent takes the env-VALID move that MAXIMIZES its own local connectivity proxy (soft incident-edge mass), others held at their current cell while it is scored. STAY is the safe default (it is always valid and is scored at the agent's current anchoring).
For agent i and candidate action a, we move ONLY i to valid_targets[i,a]
(everyone else stays) and read i's soft degree at the resulting layout; the
valid action with the largest value wins. This makes the relay hold/strengthen
the bridge from purely local information (agent_architecture.md: relay = the
λ̂₂-anchor tool). Result is always a valid move, so the env never reverts it.
When forbid_collision is True the hard collision-mask
(:func:occupied_cell_mask) removes actions whose target sits on another
agent's CURRENT cell (their score is set to -inf before the argmax); STAY
always stays selectable.
Source code in experiments/ctde_v0/controller.py
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 | |
relay_hold_move
¶
relay_hold_move(pos, valid_targets, action_valid, comm_r, sharp, hold_target=0.5, forbid_collision=False)
(N,) int32 — the "hold" relay L1 controller: a STATIC BEACON. Each relay agent simply STAYS where it is — a low-energy "don't wander, keep the bridge from where you stand" tool — UNLESS staying would leave it isolated, in which case it takes the SINGLE env-valid move that best restores a neighbour (minimal movement to re-anchor). STAY is the safe default and is always valid.
This is the complement of :func:relay_move (the lambda2_anchor tool, which
ACTIVELY climbs local connectivity every step): relay_hold_move moves ONLY
when its anchoring drops below the floor, so a well-connected relay never wanders
off its post (agent_architecture.md: relay = "stop & hold the connection";
Relay-tool axis = hold-connection heuristic).
Decision (per relay i, from purely LOCAL information):
* soft_deg_i(STAY) = i's soft incident-edge mass at its CURRENT cell
(:func:_local_conn_score, the same proxy relay_move / the edge-margin
signal use, so "anchored enough" agrees across the agent).
* if soft_deg_i(STAY) >= hold_target -> STAY (it is comfortably anchored).
* else (about to isolate) -> among the env-VALID moves, take the one that
MAXIMIZES soft_deg_i at the resulting layout (others held), i.e. the minimal
step that best re-establishes a neighbour. STAY is included in the argmax, so
if no move improves anchoring the agent still STAYs (never an invalid move).
hold_target is the soft-degree floor that defines "isolated" (default 0.5 — a
single in-range neighbour at the comm edge already carries ~0.5 soft mass at the
default sharpness, so the relay holds as long as it has roughly one live link).
When forbid_collision is True the hard collision-mask
(:func:occupied_cell_mask) removes actions whose target sits on another agent's
CURRENT cell (their score is set to -inf before the argmax); STAY always stays
selectable. Result is always a valid move, so the env never reverts it. Pure JAX
(vmap/scan/jit-safe) — same signature/contract as :func:relay_move.
Source code in experiments/ctde_v0/controller.py
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 | |
positions_after
¶
positions_after(pos, actions, valid_targets)
(N,2) int32 — committed positions if every agent took actions (N,).
Reads the env's valid_targets (N,A,2) table (already wall/boundary
resolved). NoCollision semantics (agents may share a cell) — matches the
comm-coverage recipe default; the mechanism only needs the comm graph, which
is collision-agnostic.
Source code in experiments/ctde_v0/controller.py
446 447 448 449 450 451 452 453 454 455 456 | |
team_lambda2_after
¶
team_lambda2_after(pos_next, comm_r, sharp)
Scalar true λ₂ of the soft comm-graph at pos_next (N,2).
Source code in experiments/ctde_v0/controller.py
459 460 461 | |
safe_goal_mask
¶
safe_goal_mask(pos, goal_cells, valid_targets, action_valid, comm_r, sharp, min_lambda2)
(N, K) bool — action-mask mechanism: a candidate is SAFE iff, when its
agent alone takes the greedy first move toward it (others STAY), the team's
true λ₂ stays >= min_lambda2.
This is a LOCAL, per-agent guardrail (each agent screens its own candidates independently against the connectivity floor) — the "forbid goals that would disconnect" mechanism. If a row would mask ALL candidates, the "here" candidate (index 0, a STAY goal) is force-unmasked so a valid goal always exists.
Source code in experiments/ctde_v0/controller.py
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 | |