FlyBrain-Lab / src /world3d /perception.py
timfromhcs's picture
FlyBrain V5.0.0 Space sync (v1 API, backup, watchdog, fixed tabs)
3865888 verified
Raw
History Blame Contribute Delete
3.34 kB
"""First-person geometric perception (V6 phase_07).
The organism NEVER receives hidden world state. Each observation is built
from MuJoCo raycasts through the live physics (occlusion included):
eye position + yaw -> ray fan -> hits -> entities + depth panorama
Perception kind is honestly labelled GEOMETRIC (VLM slot: UNAVAILABLE until
a verified local vision model exists). Events fire on: new entity, close
obstacle ahead, novel location cell.
"""
import math
from typing import Any, Dict, List, Optional, Set
import numpy as np
from src.world3d.physics import EYE_HEIGHT
FAN_YAW = [-60, -40, -20, 0, 20, 40, 60] # degrees, 7 horizontal rays
FAN_PITCH = [-10, 0, 8] # degrees, 3 rows
MAX_RANGE = 15.0
def _dir_vec(yaw_rad: float, pitch_rad: float) -> List[float]:
return [math.sin(yaw_rad) * math.cos(pitch_rad),
math.cos(yaw_rad) * math.cos(pitch_rad),
math.sin(pitch_rad)]
class EyeSensor:
def __init__(self):
self.seen_entities: Set[str] = set()
self.seen_cells: Set[str] = set()
def observe(self, world, char_name: str, yaw: float) -> Dict[str, Any]:
st = world.physics.char_state(char_name)
eye = [st["pos"][0], st["pos"][1], st["pos"][2] + 0.55]
depths: List[float] = []
entities: Dict[str, Dict[str, Any]] = {}
for pitch in FAN_PITCH:
for dyaw in FAN_YAW:
d = _dir_vec(yaw + math.radians(dyaw), math.radians(pitch))
hit = world.physics.raycast(eye, d, MAX_RANGE, exclude_body=char_name)
depths.append(hit["dist"] if hit["hit"] else MAX_RANGE)
if hit["hit"]:
ent = hit["entity"]
prev = entities.get(ent, {"dist": 1e9, "count": 0})
prev["count"] += 1
prev["dist"] = min(prev["dist"], hit["dist"])
prev["geom"] = hit["geom"]
entities[ent] = prev
events: List[Dict[str, Any]] = []
for ent, info in entities.items():
if ent not in self.seen_entities:
self.seen_entities.add(ent)
events.append({"type": "new_entity", "entity": ent,
"dist": info["dist"]})
# close obstacle straight ahead (center column rays)
center = [depths[r * len(FAN_YAW) + 3] for r in range(len(FAN_PITCH))]
if min(center) < 1.5:
events.append({"type": "obstacle_ahead", "dist": round(min(center), 2)})
cell = f"{int(eye[0])}:{int(eye[1])}"
if cell not in self.seen_cells:
self.seen_cells.add(cell)
events.append({"type": "novel_location", "cell": cell})
pano = np.array(depths, dtype=np.float32)
import hashlib
frame_hash = hashlib.sha256(pano.tobytes()).hexdigest()
return {"kind": "GEOMETRIC", "tick": world.tick,
"eye": [round(v, 3) for v in eye], "yaw": round(float(yaw), 3),
"depth_panorama": [round(float(v), 2) for v in depths],
"entities": {k: {"dist": v["dist"], "count": v["count"],
"geom": v.get("geom", "")}
for k, v in sorted(entities.items())},
"frame_hash": frame_hash, "events": events}