ctde_v0.env_utils¶
env_utils
¶
Env construction + reward composition + λ₂ oracle + KB-adjacency + metrics.
The lab/experiment boundary (agent_architecture.md): zymera runs the WORLD
only and is reward-agnostic; the experiment composes the scalar reward HERE from
the env's UNWEIGHTED per-term magnitudes in info["reward_terms"], re-weighting
coverage / connectivity / collision per the config (Reward block).
The auxiliary supervision target is the simulator's true Fiedler value
_lambda2(world.body.position, comm_r, sharp) (missions_terms) — a scalar per
step, broadcast to all agents (one true λ₂ for the team). Grading uses the same
oracle: connectivity-% = fraction of steps with true λ₂ > τ.
This module also exposes the comm-graph adjacency the GNN-KB fuses over
(kb_adjacency: in-range neighbours at comm_r, diagonal cleared) and the
degree statistics the SizeShiftReg-style regularizer penalizes.
build_env
¶
build_env(cfg)
Construct the comm-coverage env from the config's World block (recipe defaults supply the reward TERMS; we re-weight their magnitudes ourselves).
When reward_anti_overlap == 'on' we append a zero-weight overlap
term (same_step_overlap) so the env populates info['reward_terms']
['overlap'] — :func:compose_reward then re-weights it. Zero weight keeps the
env's own (unused) scalar reward identical; we always compose the reward here.
Source code in experiments/ctde_v0/env_utils.py
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | |
compose_reward
¶
compose_reward(reward_terms, world, cfg, lambda2_penalty=None, congestion_penalty=None)
(N,) scalar reward from the env's unweighted per-term (N,) magnitudes.
base_i = w_covcoverage_i + w_connconnectivity_i + w_coll*collision_i
(collision weight negative -> a penalty). When Reward.normalized is set,
the coverage term is divided by the free-cell count (fractional coverage).
When the soft-λ mechanism is active, lambda2_penalty (a shared scalar
shortfall) is subtracted with weight Reward.soft_lambda_penalty.
When Reward.barrier_weight > 0 the per-agent connectivity-FLOOR barrier
(:func:connectivity_barrier, read off world.body.position — the SAME source
true λ₂ / :func:local_edge_margin use) is SUBTRACTED. It composes with every
other connectivity mechanism (it is NOT a replacement). At the default
barrier_weight == 0 the branch is skipped entirely, so the composed reward is
byte-identical to the pre-barrier behaviour.
Source code in experiments/ctde_v0/env_utils.py
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 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 153 154 155 156 157 158 | |
occlusion_penalty
¶
occlusion_penalty(position, wall, cfg)
(N,N) float32 — additive comm-distance penalty c*k from wall occlusion ("wall RF").
k = number of wall-runs crossed on the straight i-j segment; c =
cfg.world.occlusion_c (or comm_r/3). A link that passes through walls has an
inflated EFFECTIVE distance d_eff = d + c*k and so attenuates / drops. Zero on the
diagonal and for any clear-line-of-sight pair. Pure JAX (vmap/jit-safe); wall (H,W) bool.
Source code in experiments/ctde_v0/env_utils.py
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | |
true_lambda2
¶
true_lambda2(position, cfg, wall=None)
Scalar true Fiedler value of the soft comm-graph at position (N,2). With
wall + cfg.world.occlusion the graph is wall-occluded (d_eff); else the
plain distance graph (delegates to the shared _lambda2 — byte-identical).
Source code in experiments/ctde_v0/env_utils.py
197 198 199 200 201 202 203 204 205 206 207 | |
local_edge_margin
¶
local_edge_margin(position, cfg, wall=None)
(N,) float32 — the PER-AGENT "you're at the edge of comms range" signal.
Where true_lambda2 is a GLOBAL scalar (the team's λ₂ floor) broadcast
identically to every agent — so no single agent knows IT is the one stretching
the bridge — this is its LOCAL, per-agent counterpart: each agent reads only its
OWN incident-edge mass and is penalized for the shortfall against a target
degree. An agent comfortably surrounded by in-range teammates scores ≈0; one
drifting toward the edge of its comm range (links approaching / crossing
comm_r) sees its penalty ramp up. It is ANTICIPATORY (the soft edge weight
decays smoothly as a link nears comm_r, so it fires BEFORE the link breaks)
and partial-observability-native (computable from an agent's own neighbourhood).
Construction — the SAME soft incident-edge mass the relay tool maximizes
(:func:controller._local_conn_score, reused so the signal and the relay
anchor agree):
w_ij = sigmoid(sharp · (comm_r − cheby_dist_ij)) for j ≠ i soft_deg_i = Σ_{j≠i} w_ij (soft neighbour count) p_i = relu(degree_target − soft_deg_i) (the shortfall)
where sharp = cfg.connectivity.lambda2_sharp, comm_r = cfg.world.comm_r
and degree_target = cfg.mission_safety.degree_target. The result is the
per-agent shortfall in exactly the soft-degree the relay maximizes — NOT
averaged/broadcast: p_i is agent i's own margin, so the rollout can charge
the stretching agent specifically. Pure JAX (vmap/scan/jit-safe).
Source code in experiments/ctde_v0/env_utils.py
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | |
connectivity_barrier
¶
connectivity_barrier(position, cfg, wall=None)
(N,) float32 — the per-agent connectivity FLOOR barrier ("Hyper-Singularity").
A one-sided interior-point wall on each agent's NEAREST-NEIGHBOUR distance: it is
EXACTLY 0 while the agent is comfortably linked (silent in the safe zone), rises
smoothly as that agent's closest teammate drifts toward the edge of comm range,
and saturates at a finite cap ("almost infinity") at / past the break point —
so the rollout feels an explosive-but-finite push BEFORE the link snaps. It is a
standalone, config-knobbed reward TERM that COMPOSES with every other connectivity
mechanism (conn_signal / mechanism); it does NOT replace any of them. When
Reward.barrier_weight == 0 (the default) it is identically 0 and a no-op.
The signal is the LOCAL Chebyshev nearest-neighbour distance — the SAME metric as
the comm graph (:func:controller._cheby / :func:kb_adjacency), so the wall and
the link agree on "range":
x_i = min_{j != i} cheby_dist(i, j) (self masked with +inf before the min)
The barrier is the user's formula, made RL-safe (the literal pole at M is
GUARDED so there is no inf/nan for ANY x, including x_i == M exactly and
x_i > M / a lone agent's x_i = +inf):
raw(x) = barrier_weight * relu(x - a)^2 / (M - x)^p (0 for x<=a; pole at x=M) xc_i = minimum(x_i, M - eps) eps=1e-3, so (M - xc) in [eps, .] > 0 f_i = minimum( barrier_weight * relu(xc_i - a)^2 / (M - xc_i)^p , cap ) f_i = cap where x_i >= M (link already broken / agent isolated)
Here a = barrier_a (launch point), M = barrier_M (the wall / break range),
p = barrier_p (explosion power), cap = barrier_cap (the finite ceiling) and
barrier_weight IS the k of the formula (already folded in). The relu(·)^2
is the user's (x - a + |x - a|)^2 / 4 written via the ReLU identity. Result is
finite and in [0, cap] for every x. Pure JAX (vmap/scan/jit-safe).
Source code in experiments/ctde_v0/env_utils.py
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 | |
kb_adjacency
¶
kb_adjacency(position, cfg, wall=None)
(N,N) bool — in-range neighbours at comm_r with the diagonal CLEARED.
This is the comm graph the GNN-KB message-passing fuses over (the formalism's
bridge). Chebyshev disk, derived from positions — matches the env's
DiskTopology / the true-λ₂ soft graph support. With wall + cfg.world.occlusion
the effective distance is wall-inflated (d_eff) so links through walls drop.
Source code in experiments/ctde_v0/env_utils.py
310 311 312 313 314 315 316 317 318 319 320 321 | |
kb_distance
¶
kb_distance(position, cfg, wall=None)
(N,N) float32 — the comm-graph sender→receiver Chebyshev distance, NORMALIZED
by comm_r (so in-range edges land in [0,1]) with the diagonal CLEARED to 0.
This is the SAME Chebyshev distance kb_adjacency thresholds (the comm graph /
DiskTopology metric), exposed for the GNN-KB message_content modes that append a
per-edge geometry channel to each message (nets.MPLayer / edge_distance). It
is normalized by comm_r (NOT raw cells) so a model trained @16²/4 reads the same
edge geometry @32²/10 — SCALE-INVARIANT. The receiver multiplies it by the (boolean)
adjacency, so out-of-range entries (> 1 here) never enter the aggregation.
Pure JAX (vmap/scan/jit-safe).
Source code in experiments/ctde_v0/env_utils.py
324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 | |
degree_stats
¶
degree_stats(position, cfg)
(N,) float32 per-node in-range degree (neighbour count) over the comm graph — the per-node statistic the SizeShiftReg-style regularizer watches.
Source code in experiments/ctde_v0/env_utils.py
342 343 344 345 346 | |
skill_congestion
¶
skill_congestion(skill_idx, position, cfg)
(N,) float32 — the FREE-MARKET congestion price: for each agent, the number of its IN-RANGE neighbours that chose the SAME skill this step.
Choosing a crowded skill (one many neighbours also picked) costs more, so the team
spreads across the skill library instead of collapsing into one mode — the decentralized,
learned-against anti-collapse force (the price is subtracted from the reward in
:func:compose_reward, weighted by congestion_weight). It is LOCAL (reads only the
comm neighbourhood) and emergent — NOT a global auction. Pure JAX (vmap/scan/jit-safe):
adj_ij = in-range neighbour (Chebyshev ≤ comm_r, i ≠ j; kb_adjacency)
same_ij = skill_i == skill_j
price_i = Σ_j adj_ij · same_ij (same-skill neighbour count)
Source code in experiments/ctde_v0/env_utils.py
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 | |
sense_frontier_bonus
¶
sense_frontier_bonus(world, cfg)
(N,) float32 — the exploration "info-gain" bonus: per agent, the count of UNCOVERED,
non-wall cells within Chebyshev sense_r of the agent. Rewards being where there is
still ground to learn (drives agents toward the frontier) WITHOUT changing the coverage
metric (it reads only the current world). Pure JAX (vmap/scan/jit-safe).
Source code in experiments/ctde_v0/env_utils.py
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 | |
coverage_fraction_free
¶
coverage_fraction_free(world, cfg)
Covered FREE cells / free cells (the campaign coverage-% definition). Falls back to all-cells when there are no walls (free == all).
Source code in experiments/ctde_v0/env_utils.py
391 392 393 394 395 396 397 398 399 | |
coverage_difference_credit
¶
coverage_difference_credit(prev_covered, position, wall, cfg)
(N,) float32 — the EXACT submodular difference reward for coverage THIS step.
D_i = cov(S) − cov(S\{i}) on the submodular team-coverage objective, where
cov = number of free cells the team covers this step that were NOT covered
before. Because coverage is a set-cover (submodular) objective this marginal is a
CLOSED FORM — no learned COMA estimate, no counterfactual rollout: it is exactly the
cells that are (a) new to the team this step, (b) inside agent i's cover footprint,
and (c) inside NO OTHER agent's footprint this step (a cell covered by ≥2 agents is
redundant, so removing any one leaves it covered → it contributes 0 to every D_i).
Read off the SAME cheby_footprint / prev.covered the env's new_coverage
reward term uses (:func:zymera.metrics.derive), so the credit and the team coverage
metric agree exactly:
fp_i = cheby_footprint(pos, cover_r) & ~wall (N,H,W) i's footprint new_i = fp_i & ~prev_covered new-to-team cells in fp_i count = Σ_i fp_i (H,W) coverers this step D_i = |{ c : new_i[c] ∧ count[c] == 1 }| uniquely-provided new cells
pos is the POST-step position (matching the env's coverage bookkeeping); walls are
static so wall may be taken from either the prev or the next world. Pure JAX
(vmap/scan/jit-safe). Σ_i D_i ≤ team new-coverage this step (equality iff no two agents
cover the same new cell).
Source code in experiments/ctde_v0/env_utils.py
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 | |