zymera.dynamics
dynamics
Movement, collision resolution, action masks, and masked sampling.
This module kills v0's three-place collision split: the env's _move /
_resolve_collisions (examples/comm_coverage.py) and the trainer's
_targets / _masked_sample / _masked_logp_ent
(examples/_frontier_core.py) all collapse onto ONE source of truth —
:meth:GridDynamics.targets. The trainer's collision-free sampler consumes
the same per-action target table the env steps with, so the two can never
drift apart again.
Semantics (ported bit-for-bit from v0):
- A move into the boundary is clipped in-bounds; a move into a wall cell is reverted (the agent stays put). With an open grid the wall check is a no-op.
- :class:
SequentialClaimcommits moves one agent at a time inside alax.scan(doctrine: scan-over-agents for sequential conflict resolution); an agent whose target cell is already taken reverts to its old cell and is reportedblocked— v0'shard_collisionsilently erased the attempt, so the collision reward term lost its learning signal.
Pinned invariant (tested, forever): agents claim in order 0..N-1.
Lower index wins a contested cell; an agent may enter a cell a LOWER-indexed
agent vacated this step, but not one a higher-indexed agent is about to
vacate. The masked samplers below walk the same order, so a masked-sampled
joint action is never reverted by :class:SequentialClaim.
CollisionRule
Bases: Protocol
Resolves simultaneous move proposals into committed positions.
resolve
resolve(old_pos, proposed)
(old_pos (N,2), proposed (N,2)) -> (new_pos (N,2), blocked (N,) bool).
Source code in zymera/dynamics.py
51 52 53 54 55 | |
NoCollision
dataclass
NoCollision()
Pass-through — agents may share a cell; nothing is ever blocked.
SequentialClaim
dataclass
SequentialClaim()
HARD collision avoidance — v0 _resolve_collisions plus blocked.
Commits moves one agent at a time (lax.scan over agents 0..N-1,
each commit visible to the next); an agent whose target cell is already
taken by another agent reverts to its old cell and is flagged blocked.
Guarantees no two agents ever share a cell (given distinct old_pos).
GridDynamics
dataclass
GridDynamics(collision=NoCollision())
Square-grid movement: boundary clip + wall revert + a collision rule.
Frozen / hashable — close over it in jitted code (the static-object rule).
targets
targets(world)
(N, A, 2) int32 — the committed cell for every agent × action.
Boundary-clipped then wall-reverted, exactly as v0
_frontier_core._targets / comm_coverage._move. THE single
source of truth: step gathers from it, the trainer's
collision-free sampler masks against it.
Source code in zymera/dynamics.py
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | |
action_mask
action_mask(world)
(N, A) bool — physical validity per action.
True iff the action's target differs from the agent's current cell (i.e. the move is in-bounds and not into a wall) OR the action is STAY. STAY is always valid.
Source code in zymera/dynamics.py
129 130 131 132 133 134 135 136 137 138 139 | |
step
step(world, action)
Apply action (N,) int32 → (Body', blocked (N,) bool).
Gathers each agent's target from :meth:targets, then resolves
conflicts through the configured :class:CollisionRule.
Source code in zymera/dynamics.py
141 142 143 144 145 146 147 148 149 150 151 | |
sequential_masked_sample
sequential_masked_sample(logits, targets, init_pos, key)
Sample collision-free actions agent-by-agent → (actions, masked logp).
Port of v0 _frontier_core._masked_sample consuming a precomputed
targets table (from :meth:GridDynamics.targets). Walks agents in
the pinned order 0..N-1: an action is masked iff its target cell is
already claimed (lower index → its committed target; higher index → its
current cell). STAY is always valid — nobody can claim your own cell
before your turn. By construction the sampled joint action is never
reverted by :class:SequentialClaim.resolve.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
logits
|
Array
|
(N, A) float — unmasked policy logits. |
required |
targets
|
Array
|
(N, A, 2) int32 — per agent × action committed cells. |
required |
init_pos
|
Array
|
(N, 2) int32 — current positions (the claim-scan seed). |
required |
key
|
Array
|
PRNG key. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
|
Array
|
sampled actions. |
Source code in zymera/dynamics.py
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | |
sequential_masked_logp_ent
sequential_masked_logp_ent(logits, targets, init_pos, actions)
Recompute masked log-probs + entropy for stored actions.
Port of v0 _frontier_core._masked_logp_ent consuming a precomputed
targets table. Replays the same 0..N-1 claim order as
:func:sequential_masked_sample, so the returned logp equals the
sample-time value for the same (logits, targets, init_pos, actions).
Returns:
| Type | Description |
|---|---|
Array
|
|
Array
|
over agents of each masked distribution's entropy. |
Source code in zymera/dynamics.py
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | |