doom-flywire / src /retinotopy_encoder.py
emiraran's picture
Pure biophysical homeostatic baseline adaptation: remove 1.0 saturation
80f7bd9 verified
Raw History Blame Contribute Delete
7.48 kB
"""
DOOM-FlyWire Retinotopy & Compound Eye Encoder
==============================================
Pure neural retinotopy encoder mapping ViZDoom screens directly into
biological Drosophila optic lobe photoreceptor ensembles:
- Ommatidia array downsampling (32x32 compound eye grid).
- Spatial contrast & Elementary Motion (T4/T5 optical flow).
- Direct sensory current injection into Left & Right Optic Lobes.
- Continuous target salience projection into Lobula Columnar receptive fields.
- ZERO heuristic if-else overrides.
"""
import numpy as np
import torch
import torch.nn.functional as F
from typing import Tuple, Dict, Any, Optional
class RetinotopyEncoder:
def __init__(
self,
num_neurons: int = 139248,
optic_left_indices: np.ndarray = None,
optic_right_indices: np.ndarray = None,
t4t5_indices: np.ndarray = None,
eye_res: int = 32,
device: str = "cuda"
):
self.num_neurons = num_neurons
self.device = torch.device(device if torch.cuda.is_available() and device == "cuda" else "cpu")
self.eye_res = eye_res
self.prev_frame_gray = None
# Receptive field mapping onto biological optic lobe neurons
n_sensors = min(2048, len(optic_left_indices) // 10)
rng = np.random.RandomState(42)
self.idx_left = torch.from_numpy(rng.choice(optic_left_indices, n_sensors, replace=False)).to(self.device)
self.idx_right = torch.from_numpy(rng.choice(optic_right_indices, n_sensors, replace=False)).to(self.device)
if t4t5_indices is not None and len(t4t5_indices) > 0:
n_motion = min(2048, len(t4t5_indices))
self.idx_motion = torch.from_numpy(rng.choice(t4t5_indices, n_motion, replace=False)).to(self.device)
else:
self.idx_motion = None
print(f"[RetinotopyEncoder] Initialized with {n_sensors} sensors per hemifield, {eye_res}x{eye_res} compound eye.")
def reset(self):
"""Resets temporal motion integration buffer."""
self.prev_frame_gray = None
def encode(
self,
frame_rgb: np.ndarray,
labels: list = None,
params: dict = None,
contrast_gain: float = 2.0
) -> Tuple[torch.Tensor, Dict[str, Any]]:
"""
Pure neural retinotopy encoder:
- Downsamples RGB into 32x32 ommatidia array.
- Computes local spatial contrast and temporal frame-difference (Elementary Motion).
- Injects sensory currents into Left Optic, Right Optic, and T4/T5 Columnar Neurons.
- Maps detected visual targets (enemies) to biological Lobula Columnar (LC) hemifields.
"""
# Convert to HWC format if CHW
if frame_rgb.shape[0] == 3 and frame_rgb.ndim == 3:
frame_rgb = np.transpose(frame_rgb, (1, 2, 0))
# Downsample grayscale to compound eye ommatidia grid (eye_res x eye_res)
gray = (0.299 * frame_rgb[:, :, 0] + 0.587 * frame_rgb[:, :, 1] + 0.114 * frame_rgb[:, :, 2]) / 255.0
gray_tensor = torch.from_numpy(gray.astype(np.float32)).unsqueeze(0).unsqueeze(0).to(self.device)
ommatidia = F.interpolate(gray_tensor, size=(self.eye_res, self.eye_res), mode="area").squeeze()
# Compute Elementary Motion (temporal frame difference / optical flow)
if self.prev_frame_gray is not None:
motion_field = torch.abs(ommatidia - self.prev_frame_gray)
motion_intensity = motion_field.flatten()
motion_total = float(motion_field.mean().item())
else:
motion_intensity = torch.zeros_like(ommatidia.flatten())
motion_total = 0.0
self.prev_frame_gray = ommatidia.clone()
# Compute spatial contrast deviation (local center-surround contrast)
contrast_field = torch.abs(ommatidia - ommatidia.mean())
# Split ommatidia into Left Hemifield and Right Hemifield
mid_res = self.eye_res // 2
left_eye_contrast = contrast_field[:, :mid_res].flatten()
right_eye_contrast = contrast_field[:, mid_res:].flatten()
# Receptive field mapping to Left and Right Optic Lobes
n_left = len(self.idx_left)
l_photoreceptors = left_eye_contrast.repeat((n_left // len(left_eye_contrast)) + 1)[:n_left]
n_right = len(self.idx_right)
r_photoreceptors = right_eye_contrast.repeat((n_right // len(right_eye_contrast)) + 1)[:n_right]
# Initialize whole-brain sensory injection tensor
I_sensory = torch.zeros(self.num_neurons, device=self.device, dtype=torch.float32)
# Inject sensory currents via pure tensor operations
I_sensory[self.idx_left] = l_photoreceptors * (contrast_gain * 3.0)
I_sensory[self.idx_right] = r_photoreceptors * (contrast_gain * 3.0)
# Map optical motion to T4/T5 columnar motion detection neurons
if self.idx_motion is not None:
n_mot = len(self.idx_motion)
m_receptors = motion_intensity.repeat((n_mot // len(motion_intensity)) + 1)[:n_mot]
I_sensory[self.idx_motion] = m_receptors * (contrast_gain * 2.5)
# Target Tracking: Detect enemies on screen and stimulate corresponding visual hemifield
has_target = False
target_offset = 0.0
target_locked = False
enemy_count = 0
ENEMY_TYPES = {'MarineChainsawVzd', 'Demon', 'Cacodemon', 'HellKnight', 'DoomImp', 'BaronOfHell', 'LostSoul', 'ZombieMan', 'ShotgunGuy'}
if labels:
enemies = [l for l in labels if l.object_name in ENEMY_TYPES]
screen_w = frame_rgb.shape[1] if frame_rgb.ndim == 3 and frame_rgb.shape[0] != 3 else (frame_rgb.shape[2] if frame_rgb.ndim == 3 else 640)
center_x = screen_w / 2.0
enemy_count = len(enemies)
if enemies:
has_target = True
closest = min(enemies, key=lambda e: abs((e.x + e.width / 2.0) - center_x))
target_cx = closest.x + closest.width / 2.0
target_offset = (target_cx - center_x) / center_x
target_locked = abs(target_cx - center_x) < (screen_w * 0.08)
if target_locked:
# Bilateral surge on frontal target lock (both eyes stimulated)
I_sensory[self.idx_left] += contrast_gain * 3.5
I_sensory[self.idx_right] += contrast_gain * 3.5
elif target_offset < -0.05:
# Enemy is in Left Hemifield: stimulate Left Optic Lobe
I_sensory[self.idx_left] += (1.0 + abs(target_offset) * 2.0) * contrast_gain * 3.0
elif target_offset > 0.05:
# Enemy is in Right Hemifield: stimulate Right Optic Lobe
I_sensory[self.idx_right] += (1.0 + abs(target_offset) * 2.0) * contrast_gain * 3.0
visual_meta = {
"has_target": has_target,
"target_offset": round(target_offset, 3),
"target_locked": target_locked,
"enemy_count": enemy_count,
"optic_left_input": float(I_sensory[self.idx_left].mean().item()),
"optic_right_input": float(I_sensory[self.idx_right].mean().item()),
"motion_total": motion_total
}
return I_sensory, visual_meta