data_logger.py
DataLogger genérico para hivebr.engine.
Registra o estado de todos os robôs presentes na arena a cada frame,
via draw_callback. Suporta janelas de plot interativas (Tk + Matplotlib)
e exportação em pickle e csv.
Comportamento de gravação
- Gravação ativa enquanto
sim.running_simulation for verdadeiro.
- Cada robô possui o seu próprio log e a sua própria sequência de segmentos.
- Um novo segmento é iniciado sempre que novos waypoints são adicionados
àquele robô.
- Cada segmento é identificado pelo campo
segment nos frames.
- Cada frame guarda uma flag
collision (por robô). Além disso, toda
colisão (robô-robô, obstáculo ou mapa) é registrada em
MultiRobotLog.collision_events com o instante t e os participantes;
no CSV, essas colisões vão para um arquivo *_collisions.csv.
Uso
from hivebr.engine.data_logger import DataLogger
logger = DataLogger()
sim = Simulator(controller=ctrl)
sim.run()
logger.save("run_01.pkl") # pickle (recommended)
logger.save_csv("run_01.csv") # CSV (+ *_collisions.csv)
logger.export("run_01.csv") # pick format by extension
logger.print_collisions() # human-readable collision summary
logger.plot() # interactive plot window
# Carregar e plotar depois
log = DataLogger.load("run_01.pkl")
log.plot_log()
Colisões
Cada frame guarda uma flag collision por robô. Além disso, cada colisão
(robô-robô, obstáculo ou mapa) é registrada uma única vez, na borda de subida,
em :attr:MultiRobotLog.collision_events no formato
{"t": 0.03, "kind": "robot", "robots": [0, 1], "distance": 0.0}
com kind em {"robot", "obstacle", "grid"}, o instante t e os
participantes (índices de robô, obstacle_index/obstacle_xy). No CSV,
os eventos vão para um arquivo *_collisions.csv. Ver
DataLogger.print_collisions e format_collision_event.
Frame
dataclass
Frame(
t,
x,
y,
theta,
vx=None,
vy=None,
v=None,
omega=None,
speed=None,
steer=None,
v_ref=None,
omega_ref=None,
vx_real=None,
vy_real=None,
v_real=None,
omega_real=None,
speed_real=None,
steer_real=None,
collision=False,
goal=None,
waypoint=None,
wp_index=None,
segment=0,
)
Uma amostra do simulador em um instante t.
vx
class-attribute
instance-attribute
vy
class-attribute
instance-attribute
v
class-attribute
instance-attribute
omega
class-attribute
instance-attribute
speed
class-attribute
instance-attribute
steer
class-attribute
instance-attribute
v_ref
class-attribute
instance-attribute
omega_ref
class-attribute
instance-attribute
vx_real
class-attribute
instance-attribute
vy_real
class-attribute
instance-attribute
v_real
class-attribute
instance-attribute
omega_real
class-attribute
instance-attribute
speed_real
class-attribute
instance-attribute
steer_real
class-attribute
instance-attribute
collision
class-attribute
instance-attribute
goal
class-attribute
instance-attribute
waypoint
class-attribute
instance-attribute
wp_index
class-attribute
instance-attribute
segment
class-attribute
instance-attribute
SimLog
dataclass
SimLog(
robot_type,
frames=list(),
planned_path=None,
planned_curve=None,
robot_index=0,
label="",
)
Container com todos os frames de um robô.
robot_type
instance-attribute
frames
class-attribute
instance-attribute
frames = field(default_factory=list)
planned_path
class-attribute
instance-attribute
planned_curve
class-attribute
instance-attribute
robot_index
class-attribute
instance-attribute
label
class-attribute
instance-attribute
to_numpy
Converte frames para dicionário de arrays numpy.
Source code in src/hivebr/engine/data_logger.py
| def to_numpy(self) -> dict[str, np.ndarray]:
"""Converte frames para dicionário de arrays numpy."""
if not self.frames:
return {}
def _get(attr):
return np.array([getattr(f, attr) for f in self.frames], dtype=float)
def _get_nullable(attr):
return np.array(
[float(v) if (v := getattr(f, attr)) is not None else np.nan
for f in self.frames], dtype=float,
)
result = {
"t": _get("t"),
"x": _get("x"),
"y": _get("y"),
"theta": _get("theta"),
"segment": np.array([f.segment for f in self.frames], dtype=int),
"collision": np.array([1 if f.collision else 0 for f in self.frames], dtype=int),
}
# ação de controle
for cmd in _ROBOT_CMD_FIELDS.get(self.robot_type, []):
result[cmd] = _get_nullable(cmd)
# velocidade real
for real_key in _ROBOT_REAL_MAP.get(self.robot_type, {}):
result[real_key] = _get_nullable(real_key)
# goal
goals = [f.goal for f in self.frames]
if any(g is not None for g in goals):
result["goal_x"] = np.array([g[0] if g is not None else np.nan for g in goals])
result["goal_y"] = np.array([g[1] if g is not None else np.nan for g in goals])
if any(len(g) > 2 for g in goals if g is not None):
result["goal_theta"] = np.array(
[g[2] if (g is not None and len(g) > 2) else np.nan for g in goals]
)
# waypoint atual
wps = [f.waypoint for f in self.frames]
if any(w is not None for w in wps):
result["wp_x"] = np.array([w[0] if w is not None else np.nan for w in wps])
result["wp_y"] = np.array([w[1] if w is not None else np.nan for w in wps])
# índice do waypoint
wp_idx = [f.wp_index for f in self.frames]
if any(i is not None for i in wp_idx):
result["wp_index"] = np.array(
[i if i is not None else -1 for i in wp_idx], dtype=int
)
return result
|
segments
Source code in src/hivebr/engine/data_logger.py
| def segments(self) -> dict[int, list[Frame]]:
segs: dict[int, list[Frame]] = {}
for f in self.frames:
segs.setdefault(f.segment, []).append(f)
return segs
|
plot_log
Source code in src/hivebr/engine/data_logger.py
| def plot_log(self):
from .plot_window import show_interactive_plot
index = getattr(self, "robot_index", 0)
show_interactive_plot(MultiRobotLog(logs=OrderedDict([(index, self)])))
|
MultiRobotLog
dataclass
MultiRobotLog(logs=dict(), collision_events=list())
Container com os logs de todos os robôs de uma simulação.
logs
class-attribute
instance-attribute
logs = field(default_factory=dict)
collision_events
class-attribute
instance-attribute
collision_events = field(default_factory=list)
to_numpy
Source code in src/hivebr/engine/data_logger.py
| def to_numpy(self) -> dict[int, dict[str, np.ndarray]]:
return {i: log.to_numpy() for i, log in self.logs.items()}
|
segments
Source code in src/hivebr/engine/data_logger.py
| def segments(self) -> dict[int, dict[int, list[Frame]]]:
return {i: log.segments() for i, log in self.logs.items()}
|
plot_log
Source code in src/hivebr/engine/data_logger.py
| def plot_log(self):
from .plot_window import show_interactive_plot
show_interactive_plot(self)
|
DataLogger
DataLogger(max_samples=None)
Registra estado + comando + velocidade real de cada robô a cada frame
via draw_callback.
A gravação ocorre enquanto sim.running_simulation for verdadeiro.
Cada robô tem o seu próprio :class:SimLog e a sua própria sequência
de segmentos (novo segmento a cada waypoint adicionado).
Source code in src/hivebr/engine/data_logger.py
| def __init__(self, max_samples: int | None = None):
self.max_samples = max_samples
self._t = 0.0
self._logs: "OrderedDict[int, SimLog]" = OrderedDict()
self._segment_idx: dict[int, int] = {}
self._last_wp_count: dict[int, int] = {}
self._initialized = False
self._sim = None
|
max_samples
instance-attribute
max_samples = max_samples
reset
Descarta frames e reseta estado de gravação.
Source code in src/hivebr/engine/data_logger.py
| def reset(self):
"""Descarta frames e reseta estado de gravação."""
self._t = 0.0
self._logs = OrderedDict()
self._segment_idx = {}
self._last_wp_count = {}
self._initialized = False
self._sim = None
print("[DataLogger] Logger reiniciado.")
|
draw_callback
draw_callback(sim, screen)
Source code in src/hivebr/engine/data_logger.py
| def draw_callback(self, sim, screen):
if not sim.running_simulation:
return
if not self._initialized:
self._init(sim)
self._t += sim.dt
for index in range(len(sim.robots)):
_, total_wp = self._waypoint_state(sim, index)
# Novos waypoints -> novo segmento
if total_wp > self._last_wp_count.get(index, 0):
self._segment_idx[index] = self._segment_idx.get(index, -1) + 1
print(f"[DataLogger] B-Bot{index} — segmento "
f"{self._segment_idx[index]} iniciado "
f"({len(self._logs[index].frames)} frames, "
f"{total_wp} waypoints).")
self._last_wp_count[index] = total_wp
self._record(sim, index, self._waypoint_manager(sim, index))
|
set_planned_path
set_planned_path(path, robot_index=0)
Registra caminho planejado de um robô como lista de (x,y[,theta]).
Source code in src/hivebr/engine/data_logger.py
| def set_planned_path(self, path: list[tuple], robot_index: int = 0):
"""Registra caminho planejado de um robô como lista de (x,y[,theta])."""
if robot_index not in self._logs:
print(f"[DataLogger] Robô {robot_index} não inicializado.")
return
self._logs[robot_index].planned_path = [
tuple(float(v) for v in p) for p in path
]
print(f"[DataLogger] Caminho planejado (B-Bot{robot_index}): "
f"{len(path)} pontos.")
|
set_planned_curve
set_planned_curve(t, x, y, theta=None, robot_index=0)
Registra caminho planejado de um robô como curva paramétrica.
Source code in src/hivebr/engine/data_logger.py
| def set_planned_curve(self, t, x, y, theta=None, robot_index: int = 0):
"""Registra caminho planejado de um robô como curva paramétrica."""
if robot_index not in self._logs:
print(f"[DataLogger] Robô {robot_index} não inicializado.")
return
curve = {
"t": np.array(t, dtype=float),
"x": np.array(x, dtype=float),
"y": np.array(y, dtype=float),
}
if theta is not None:
curve["theta"] = np.array(theta, dtype=float)
self._logs[robot_index].planned_curve = curve
print(f"[DataLogger] Curva planejada (B-Bot{robot_index}): "
f"{len(curve['x'])} pontos.")
|
result
Source code in src/hivebr/engine/data_logger.py
| def result(self) -> MultiRobotLog:
events = list(getattr(self._sim, "collision_events", []) or [])
return MultiRobotLog(logs=self._logs, collision_events=events)
|
collision_summary
Linhas legíveis descrevendo cada colisão registrada.
Source code in src/hivebr/engine/data_logger.py
| def collision_summary(self) -> list[str]:
"""Linhas legíveis descrevendo cada colisão registrada."""
return [format_collision_event(e) for e in self.result().collision_events]
|
print_collisions
Source code in src/hivebr/engine/data_logger.py
| def print_collisions(self):
lines = self.collision_summary()
if not lines:
print("[DataLogger] Nenhuma colisão registrada.")
return
print(f"[DataLogger] {len(lines)} colisão(ões) registrada(s):")
for line in lines:
print(" " + line)
|
save
Salva o log completo (incluindo colisões) em pickle.
Source code in src/hivebr/engine/data_logger.py
| def save(self, path: str = "log.pkl"):
"""Salva o log completo (incluindo colisões) em pickle."""
if not self._has_data():
print("[DataLogger] Nenhum dado para salvar.")
return
result = self.result()
with open(path, "wb") as f:
pickle.dump(result, f)
total = sum(len(l.frames) for l in self._logs.values())
print(f"[DataLogger] {total} frames / {len(self._logs)} robô(s) -> '{path}'")
self._write_collisions(result.collision_events, path)
|
save_csv
Salva o log completo em CSV (uma linha por robô por frame).
Source code in src/hivebr/engine/data_logger.py
| def save_csv(self, path: str = "log.csv"):
"""Salva o log completo em CSV (uma linha por robô por frame)."""
if not self._has_data():
print("[DataLogger] Nenhum dado para salvar.")
return
columns = ["robot", "robot_type", "segment", "t", "x", "y", "theta"]
arrays: "OrderedDict[int, dict]" = OrderedDict()
for index in sorted(self._logs):
arr = self._logs[index].to_numpy()
arrays[index] = arr
for key in arr:
if key not in columns:
columns.append(key)
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(columns)
for index, arr in arrays.items():
log = self._logs[index]
for row in range(len(log.frames)):
writer.writerow([
index if col == "robot"
else log.robot_type if col == "robot_type"
else arr[col][row] if col in arr
else ""
for col in columns
])
total = sum(len(l.frames) for l in self._logs.values())
print(f"[DataLogger] {total} frames / {len(self._logs)} robô(s) -> '{path}'")
self._write_collisions(self.result().collision_events, path)
|
export
Exporta escolhendo o formato pela extensão (.csv ou .pkl).
Source code in src/hivebr/engine/data_logger.py
| def export(self, path: str):
"""Exporta escolhendo o formato pela extensão (.csv ou .pkl)."""
if str(path).lower().endswith(".csv"):
self.save_csv(path)
else:
self.save(path)
|
load
staticmethod
Source code in src/hivebr/engine/data_logger.py
| @staticmethod
def load(path: str):
with open(path, "rb") as f:
log = pickle.load(f)
if isinstance(log, MultiRobotLog):
total = sum(len(l.frames) for l in log.logs.values())
print(f"[DataLogger] '{path}': {total} frames, "
f"{len(log.logs)} robô(s)")
else:
print(f"[DataLogger] '{path}': {len(log.frames)} frames, "
f"tipo={log.robot_type}")
return log
|
plot
Source code in src/hivebr/engine/data_logger.py
| def plot(self):
if not self._has_data():
print("[DataLogger] Nenhum dado para plotar.")
return
from .plot_window import show_interactive_plot
show_interactive_plot(self.result())
|
format_collision_event(event)
Formata um evento de colisão em uma linha legível.
Source code in src/hivebr/engine/data_logger.py
| def format_collision_event(event: dict) -> str:
"""Formata um evento de colisão em uma linha legível."""
t = float(event.get("t", 0.0))
kind = event.get("kind", "?")
robots = event.get("robots", []) or []
if kind == "robot":
pair = " <-> ".join(f"B-Bot{r}" for r in robots)
dist = event.get("distance")
dist_txt = f" (d={float(dist):.3f} m)" if dist is not None else ""
return f"[{t:8.2f}s] COLISÃO robô-robô: {pair}{dist_txt}"
if kind == "obstacle":
xy = event.get("obstacle_xy")
xy_txt = f" em ({float(xy[0]):.2f}, {float(xy[1]):.2f})" if xy else ""
return (
f"[{t:8.2f}s] COLISÃO B-Bot{robots[0]} com obstáculo "
f"#{event.get('obstacle_index')}{xy_txt}"
)
if kind == "grid":
return f"[{t:8.2f}s] COLISÃO B-Bot{robots[0]} com o mapa (inflado)"
return f"[{t:8.2f}s] COLISÃO {event}"
|