multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
f7dc16a verified
Raw History Blame Contribute Delete
9.36 kB
"""Lossless tick states; model camera embeddings cannot replace this input."""
from __future__ import annotations
from dataclasses import dataclass, fields
from pathlib import Path
import re
import numpy as np
@dataclass
class GroupingState:
ticks: np.ndarray
pos: np.ndarray
yaw: np.ndarray
pitch: np.ndarray
alive: np.ndarray
present: np.ndarray
player_slots: np.ndarray
round_tick_start: int
round_tick_end: int # inclusive, before cropping
match_id: int
map_name: str
round_no: int
clip_start_frame: int
clip_num_frames: int
@property
def num_players(self):
return len(self.player_slots)
def frame_to_tick(self, frame):
return int(self.round_tick_start + round(frame * 64.0 / 32.0))
def frame_window(self, start_frame, num_frames):
return slice(
int(np.searchsorted(self.ticks, self.frame_to_tick(start_frame))),
int(
np.searchsorted(
self.ticks, self.frame_to_tick(start_frame + num_frames)
)
),
)
def eye_pos(self):
# Addition in float32, THEN float64 conversion in relevance is deliberate.
eye = self.pos.copy()
eye[..., 2] += 64.0
return eye
def validate(self):
t, p = len(self.ticks), self.num_players
if not t or p < 2 or p > 10:
raise ValueError("Grouping state requires ticks and 2–10 players")
shapes = {
"ticks": ((t,), np.int64),
"pos": ((t, p, 3), np.float32),
"yaw": ((t, p), np.float32),
"pitch": ((t, p), np.float32),
"alive": ((t, p), np.bool_),
"present": ((t, p), np.bool_),
"player_slots": ((p,), np.int64),
}
for name, (shape, dtype) in shapes.items():
value = getattr(self, name)
if value.shape != shape or value.dtype != dtype:
raise ValueError(f"Invalid {name}: expected {shape}/{np.dtype(dtype)}")
if not np.all(np.diff(self.ticks) == 1):
raise ValueError(
"Grouping tick grid must be continuous (retain missing ticks)"
)
if len(set(self.player_slots.tolist())) != p or np.any(
(self.player_slots < 0) | (self.player_slots >= 10)
):
raise ValueError(
"Grouping player_slots must be distinct physical slots in [0,9]"
)
if self.clip_start_frame < 0 or self.clip_num_frames <= 0:
raise ValueError("Invalid grouping clip range")
lo = self.frame_to_tick(max(0, self.clip_start_frame - 48))
hi = self.frame_to_tick(self.clip_start_frame + self.clip_num_frames) - 1
if (
self.ticks[0] > lo
or self.ticks[-1] < hi
or self.round_tick_start > self.ticks[0]
or self.round_tick_end < self.ticks[-1]
):
raise ValueError(
"Grouping state does not cover the clip and its 48-frame history"
)
active = self.alive & self.present
for name in ("pos", "yaw", "pitch"):
if not np.isfinite(getattr(self, name)[active]).all():
raise ValueError(f"Non-finite {name} for an active player")
def save(self, path):
self.validate()
payload = {f.name: getattr(self, f.name) for f in fields(self)}
np.savez_compressed(path, format_version=np.int32(1), **payload)
@classmethod
def load(cls, path):
with np.load(path, allow_pickle=False) as data:
expected = {f.name for f in fields(cls)}
if set(data.files) != expected | {"format_version"}:
raise ValueError(f"Invalid grouping state fields: {path}")
if data["format_version"].shape != () or data["format_version"].item() != 1:
raise ValueError(f"Unsupported grouping state version: {path}")
values = {name: data[name].copy() for name in expected}
for name in (
"round_tick_start",
"round_tick_end",
"match_id",
"round_no",
"clip_start_frame",
"clip_num_frames",
):
value = values[name]
if value.shape != () or value.dtype.kind not in "iu":
raise ValueError(f"{name} must be an integer scalar: {path}")
values[name] = int(value)
if values["map_name"].shape != () or values["map_name"].dtype.kind != "U":
raise ValueError(f"map_name must be a string scalar: {path}")
values["map_name"] = str(values["map_name"].item())
result = cls(**values)
result.validate()
return result
def row_identity(row, num_players):
"""CSV order is distinct from physical player slots, especially for P6."""
pattern = r"match_id=(\d+)/map_name=([^/]+)/round=(\d+)/player=(\d+)(?:/|$)"
matches = [
re.search(pattern, str(row[f"video_path{i}"]).replace("\\", "/"))
for i in range(1, num_players + 1)
]
if any(m is None for m in matches):
raise ValueError(
"Grouping requires video_path1..N containing round/player identity"
)
keys = {(int(m[1]), m[2], int(m[3])) for m in matches}
if len(keys) != 1:
raise ValueError("All grouping players must belong to the same round")
slots = [int(m[4]) for m in matches]
if len(set(slots)) != num_players:
raise ValueError("Duplicate physical players in grouping row")
def integer(value, name):
if isinstance(value, (bool, np.bool_)) or not np.isfinite(float(value)):
raise ValueError(f"{name} must be an integer")
result = int(value)
if float(value) != result:
raise ValueError(f"{name} must be an integer")
return result
start = integer(row["start_frame"], "start_frame")
lengths = [
integer(row[f"video{i}_frame"], f"video{i}_frame")
for i in range(1, num_players + 1)
]
if start < 0 or min(lengths) <= 0:
raise ValueError("Invalid grouping frame range")
return (*next(iter(keys)), slots, start, min(lengths))
def state_from_parquet(rounds_root, row, num_players):
"""Read only required columns, align full-round states, then crop losslessly."""
match, map_name, round_no, slots, start, count = row_identity(row, num_players)
folder = (
Path(rounds_root)
/ f"match_id={match}"
/ f"map_name={map_name}"
/ f"round={round_no:02d}"
)
return state_from_tick_files(
[folder / f"player={s:02d}" / "ticks.parquet" for s in slots],
(match, map_name, round_no, slots, start, count),
)
def state_from_tick_files(paths, identity):
"""Build a state from one tick file per player, in CSV player order.
``identity`` is ``(match_id, map_name, round_no, player_slots, start_frame,
num_frames)``, as returned by :func:`row_identity`.
"""
import pandas as pd
match, map_name, round_no, slots, start, count = identity
num_players = len(paths)
columns = ["tick", "x", "y", "z", "yaw", "pitch", "is_alive"]
frames = [
pd.read_parquet(path, columns=columns).sort_values("tick", kind="stable")
for path in paths
]
if any(df.empty for df in frames):
raise ValueError("Empty player tick file")
tick0 = min(int(df.tick.iloc[0]) for df in frames)
tick1 = max(int(df.tick.iloc[-1]) for df in frames)
ticks = np.arange(tick0, tick1 + 1, dtype=np.int64)
shape = (len(ticks), num_players)
pos = np.full((*shape, 3), np.nan, np.float32)
yaw, pitch = (np.full(shape, np.nan, np.float32) for _ in range(2))
alive, present = (np.zeros(shape, bool) for _ in range(2))
for k, df in enumerate(frames):
idx = df.tick.to_numpy(np.int64) - tick0
for j, name in enumerate(("x", "y", "z")):
pos[idx, k, j] = df[name].to_numpy(np.float32)
yaw[idx, k] = df.yaw.to_numpy(np.float32)
pitch[idx, k] = df.pitch.to_numpy(np.float32)
alive[idx, k] = df.is_alive.to_numpy(bool)
present[idx, k] = True
last = np.flatnonzero(present[:, k])[-1]
pos[last + 1 :, k] = pos[last, k]
yaw[last + 1 :, k] = yaw[last, k]
pitch[last + 1 :, k] = pitch[last, k]
crop = slice(max(0, start - 48) * 2, (start + count) * 2)
result = GroupingState(
ticks[crop],
pos[crop],
yaw[crop],
pitch[crop],
alive[crop],
present[crop],
np.asarray(slots, np.int64),
tick0,
tick1,
match,
map_name,
round_no,
start,
count,
)
result.validate()
return result
def eligible_players(world, sl):
present = world.present[sl]
grid = present.any(1) if len(present) else np.zeros(0, bool)
if not grid.any():
return np.zeros(world.num_players, bool)
return present[grid].mean(0) >= 1.0
def update_slices(world, sl, f0, every):
n = int(round((sl.stop - sl.start) / 2.0))
if every <= 0 or every >= n:
return [(f0, sl)]
out = []
for k in range(0, n, every):
ssl = world.frame_window(f0 + k, min(every, n - k))
if ssl.stop > ssl.start:
out.append((f0 + k, ssl))
return out