Skip to content

ctde_v0.run_ctde_sweep

run_ctde_sweep

Increment-1 permutation sweep — does labor-division (roles) + anti-overlap break the v0 huddle (coverage collapses to ~7% while agents clump for trivial 100% connectivity)?

The I1 permutation (8 configs), with index / edge-content / compass left at their v0 defaults for now:

role_picker  {off, expl_relay}      # homogeneous goal head vs a learned role head

x mechanism {action_mask, soft_lambda} # the mission-safety axis x anti_overlap {off, on} # subtract same_step_overlap from the reward = 8 configs.

Sharded for parallel CPU workers (this experiment is CPU-only — do NOT run on the GPU server): run_ctde_sweep.py <shard> <nshards> runs the configs where global_index % nshards == shard and appends each result to its OWN results/ctde_<name>.jsonl (combine the files for reporting). Resumable: a config already present in ANY results/ctde_*.jsonl is skipped, so a re-launch never redoes work. Every run saves the full §5 config (config.py) alongside its metrics, so any result is reproducible.

Per config we log the FINAL-iteration: coverage%, connectivity%, aux-λ₂ accuracy, controller-valid%, episode reward, and the role split (explorer / relay fraction).

JAX_PLATFORMS=cpu     /Users/bijanmehr/Project.Zymera/zymera_lab/.venv/bin/python -u         ctde_v0/run_ctde_sweep.py 0 1            # single worker, all 8 configs

Tunables via env vars (so the runner stays argv-compatible with run_grid.py): CTDE_ITERS (default 50) · CTDE_ROLLOUTS (8) · CTDE_GRID (16) · CTDE_NAGENTS (4) · CTDE_HORIZON (100) · CTDE_SEED (0) · CTDE_AO_WEIGHT (1.0)

build_config

build_config(role_picker, mechanism, anti_overlap)

One full §5 config for a permutation cell (defaults = the 16×16/4 v0 slice; index / edge-content / compass stay at v0 defaults for now).

Source code in experiments/ctde_v0/run_ctde_sweep.py
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def build_config(role_picker: str, mechanism: str, anti_overlap: str) -> CTDEConfig:
    """One full §5 config for a permutation cell (defaults = the 16×16/4 v0 slice;
    index / edge-content / compass stay at v0 defaults for now)."""
    grid = _env_int("CTDE_GRID", 16)
    n_agents = _env_int("CTDE_NAGENTS", 4)
    horizon = _env_int("CTDE_HORIZON", 100)
    return CTDEConfig(
        world=World(grid=grid, n_agents=n_agents, comm_r=5, horizon=horizon),
        backbone=Backbone(),                       # lpac / max agg / mp_rounds 2
        action_head=CTDEConfig().action_head,      # goal_pointer K=9 stride 3
        mission_safety=MissionSafety(mechanism=mechanism),
        reward=Reward(),
        loss=Loss(),
        trainer=Trainer(),
        regularization=Regularization(),
        role_picker=role_picker,
        reward_anti_overlap=anti_overlap,
        anti_overlap_weight=_env_float("CTDE_AO_WEIGHT", 1.0),
        scale=f"{grid}x{grid}/{n_agents}",
        iters=_env_int("CTDE_ITERS", 50),
        rollouts_per_iter=_env_int("CTDE_ROLLOUTS", 8),
        seed=_env_int("CTDE_SEED", 0),
    )

build_grid

build_grid()

The 8 permutation cells, each a dict the runner can name + skip by key.

Source code in experiments/ctde_v0/run_ctde_sweep.py
103
104
105
106
107
108
109
def build_grid() -> list[dict]:
    """The 8 permutation cells, each a dict the runner can name + skip by key."""
    cells = []
    for rp, mech, ao in itertools.product(ROLE_PICKERS, MECHANISMS, ANTI_OVERLAP):
        cells.append({"role_picker": rp, "mechanism": mech, "anti_overlap": ao,
                      "name": config_name(rp, mech, ao)})
    return cells

run_config

run_config(cell)

Train one permutation cell to iters and return its result record (final-iter metrics + the full saved config). Errors are captured, not raised, so one bad cell never kills the shard.

Source code in experiments/ctde_v0/run_ctde_sweep.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
def run_config(cell: dict) -> dict:
    """Train one permutation cell to ``iters`` and return its result record
    (final-iter metrics + the full saved config). Errors are captured, not raised,
    so one bad cell never kills the shard."""
    import jax  # local import: keep module import cheap / argv-parse fast.

    cfg = build_config(cell["role_picker"], cell["mechanism"], cell["anti_overlap"])
    rec: dict = {"name": cell["name"], "config": cfg.to_dict()}
    try:
        env = env_utils.build_env(cfg)
        _state, history = ppo.train(env, cfg, key=jax.random.PRNGKey(cfg.seed))
        last = history[-1]
        rec.update({
            "iters": len(history),
            "coverage_pct": last["coverage_pct"],
            "connectivity_pct": last["connectivity_pct"],
            "mean_lambda2": last["mean_lambda2"],
            "aux_acc": last["aux_acc"],
            "median_rel_l2": last["median_rel_l2"],
            "ctrl_valid_frac": last["ctrl_valid_frac"],
            "ep_reward": last["ep_reward"],
            "explorer_frac": last.get("explorer_frac", 1.0),
            "relay_frac": last.get("relay_frac", 0.0),
            "role_entropy": last.get("role_entropy", 0.0),
            # a couple of first->last deltas so the jsonl is self-describing.
            "coverage_pct_first": history[0]["coverage_pct"],
            "connectivity_pct_first": history[0]["connectivity_pct"],
        })
    except Exception as e:  # noqa: BLE001 — record-and-continue by design.
        import traceback
        rec["error"] = f"{type(e).__name__}: {e}"
        rec["traceback"] = traceback.format_exc()
    return rec