markuz89's picture
MiniJEV v2 model export
026f63e verified
Raw History Blame Contribute Delete
4.8 kB
"""Snake as a MiniJEV task: SnakeState -> multi-channel board image.
Channels (board padded by a 1-cell wall border; smaller boards in a batch are
padded with more wall, which is semantically exact):
0 wall 1 head 2 body (excl. head) 3 tail 4 food
5 time-until-free: (L - i) / area for body segment i (head=0)
6 row offset from the head (r - r_head) / max(H, W)
7 col offset from the head (c - c_head) / max(H, W)
Channel 5 lets the model reason about which cells will open up as the snake
moves - the same information the oracle's time-aware search uses. Channels 6-7
make every cell egocentric ("the food is 3 up, 2 left"), which removes the
long learning plateau of discovering relative geometry from absolute positions.
"""
from __future__ import annotations
from typing import Sequence
import numpy as np
from minijev.tasks import Task, ragged_pack, ragged_take
from .env import ACTIONS, SnakeState
N_CHANNELS = 8
DIRS = {a: i for i, a in enumerate(ACTIONS)}
SEMANTIC_CHOICES = {"UP": "move up", "DOWN": "move down", "LEFT": "move left", "RIGHT": "move right"}
class SnakeTask(Task):
"""task.choice_style: "semantic" -> choices "move up/down/left/right" (v2 models);
"legacy" (default, what pre-v2 checkpoints were trained with) -> "UP/DOWN/LEFT/RIGHT"."""
name = "snake"
state_encoder = "grid"
default_question = "Choose the next move"
@property
def choice_alias(self) -> dict[str, str]:
return SEMANTIC_CHOICES if self.cfg.get("choice_style", "legacy") == "semantic" else {}
@property
def default_choices(self) -> tuple[str, ...]:
return tuple(self.model_label(a) for a in ACTIONS)
def encoder_class(self):
from .encoder import SnakeStateEncoder
return SnakeStateEncoder
def encoder_config(self) -> dict:
return {"in_channels": N_CHANNELS}
def states_to_arrays(self, states: Sequence[SnakeState]) -> dict[str, np.ndarray]:
bodies = [[r * s.width + c for r, c in s.body] for s in states]
flat, offsets = ragged_pack(bodies, np.int16)
return {
"body": flat,
"body_offsets": offsets,
"food": np.array([s.food[0] * s.width + s.food[1] if s.food else -1 for s in states], np.int16),
"height": np.array([s.height for s in states], np.uint8),
"width": np.array([s.width for s in states], np.uint8),
"direction": np.array([DIRS[s.direction] for s in states], np.uint8),
}
def example_states(self, n=4, seed=0):
from .agents import OracleAgent
from .env import SnakeConfig, SnakeEnv
states, oracle = [], OracleAgent()
env = SnakeEnv(SnakeConfig.from_dict({"board_size": self.cfg.get("board_size", 10)}), seed=seed)
while len(states) < n:
states.append(env.state())
for _ in range(7): # spread samples along the game
env.step(oracle.act(env.state()))
if env.done:
env.reset()
return states
def encode_arrays(self, arrays, idx):
return {"state_grid": encode_grid(arrays, np.asarray(idx))}
def encode_grid(arrays: dict[str, np.ndarray], idx: np.ndarray) -> np.ndarray:
B = len(idx)
hs = arrays["height"][idx].astype(np.int64)
ws = arrays["width"][idx].astype(np.int64)
H, W = int(hs.max()) + 2, int(ws.max()) + 2
x = np.zeros((B, N_CHANNELS, H, W), dtype=np.float32)
x[:, 0] = 1.0
for h, w in set(zip(hs.tolist(), ws.tolist())):
sel = np.nonzero((hs == h) & (ws == w))[0]
x[sel, 0, 1:h + 1, 1:w + 1] = 0.0
cells, lengths = ragged_take(arrays["body"], arrays["body_offsets"], idx)
cells = cells.astype(np.int64)
sample = np.repeat(np.arange(B), lengths)
pos = np.arange(len(cells)) - np.repeat(np.cumsum(lengths) - lengths, lengths) # index from head
Ls = np.repeat(lengths, lengths)
w_s = ws[sample]
r, c = cells // w_s + 1, cells % w_s + 1
head = pos == 0
x[sample[head], 1, r[head], c[head]] = 1.0
x[sample[~head], 2, r[~head], c[~head]] = 1.0
tail = pos == Ls - 1
x[sample[tail], 3, r[tail], c[tail]] = 1.0
x[sample, 5, r, c] = (Ls - pos) / (hs[sample] * w_s)
hr, hc = np.zeros(B, np.int64), np.zeros(B, np.int64)
hr[sample[head]], hc[sample[head]] = r[head], c[head]
scale = np.maximum(hs, ws).astype(np.float32)[:, None, None]
x[:, 6] = (np.arange(H)[None, :, None] - hr[:, None, None]) / scale
x[:, 7] = (np.arange(W)[None, None, :] - hc[:, None, None]) / scale
food = arrays["food"][idx].astype(np.int64)
has = np.nonzero(food >= 0)[0]
x[has, 4, food[has] // ws[has] + 1, food[has] % ws[has] + 1] = 1.0
return x
def make_task(cfg: dict) -> SnakeTask:
return SnakeTask(cfg)