Skip to content

Commit 3a19913

Browse files
Morganclaude
andcommitted
Add doom-neuron integration: UDP protocols, live monitor, dashboard
Adds the compatibility layer for doom-neuron closed-loop DOOM experiments: - stats_protocol.py: extended neural stats packet (356 bytes, port 12351) - action_protocol.py: action protocol for decoder integration - udp_bridge.py: stats streaming, MJPEG visualization, stim tracking - bl1.monitor module: composable ActivityMonitor (raster, mountain plot, MEA heatmap, firing rate), NeuralMJPEGServer, animation utilities - README section documenting the BL-1 + doom-neuron architecture - Dashboard screenshot (docs/images/bl1_doom_dashboard.png) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 436a4f5 commit 3a19913

11 files changed

Lines changed: 2664 additions & 1 deletion

File tree

README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,90 @@ BL-1 is a JAX-based framework for simulating dissociated cortical cultures growi
6464

6565
---
6666

67+
## Simulated Neurons Playing DOOM
68+
69+
BL-1 integrates with [doom-neuron](https://github.com/SeanCole02/doom-neuron) to create an in-silico replication of the biological DishBrain DOOM experiment. BL-1's virtual CL1 server replaces the real Cortical Labs hardware with 10,000 biophysically grounded spiking neurons -- speaking the exact same UDP protocol -- so the PPO training system connects without modification.
70+
71+
![BL-1 + doom-neuron dashboard](docs/images/bl1_doom_dashboard.png)
72+
73+
**Live 4-panel dashboard** showing the closed-loop system in real-time:
74+
- **DOOM Gameplay** -- first-person view from VizDoom
75+
- **Neural Activity** -- spike raster, mountain plot, MEA heatmap, and firing rate timeseries from the simulated culture (RatInABox-inspired composable monitoring)
76+
- **Decoder Inference** -- action probability compass and per-head bar chart showing what the ML decoder infers from the neural spikes
77+
- **Automap (2D)** -- retro Atari-style top-down map view with pixelated rendering
78+
79+
### How It Works
80+
81+
```
82+
DOOM Game State ─► Encoder (PyTorch) ─► Stimulation frequencies/amplitudes
83+
84+
UDP :12345 (72 bytes)
85+
86+
87+
BL-1 Virtual CL1 (JAX)
88+
10,000 Izhikevich neurons
89+
AMPA/NMDA/GABA synapses + STP
90+
64-channel virtual MEA
91+
92+
UDP :12346 (40 bytes)
93+
94+
Reward ◄── DOOM executes ◄── Decoder (PyTorch) ◄── Spike counts per channel
95+
```
96+
97+
The encoder learns stimulation patterns via REINFORCE (non-differentiable spikes). The decoder maps 8 channel-group spike counts to movement, camera, and attack actions. Feedback stimulation rewards kills and punishes damage, scaled by TD-error surprise.
98+
99+
### Why BL-1 Instead of Random Spikes
100+
101+
doom-neuron's SDK mode generates random spikes. BL-1 provides neurons that actually respond to stimulation with structured dynamics -- validated against Wagenaar et al. (2006) cortical culture recordings. Ablation tests (`--decoder-ablation zero`) confirm the simulated neurons carry meaningful signal.
102+
103+
### Quick Start
104+
105+
```bash
106+
# Clone both repos
107+
git clone https://github.com/m9h/bl1.git
108+
git clone https://github.com/SeanCole02/doom-neuron.git
109+
110+
# Shared venv
111+
uv venv .venv --python 3.12 && source .venv/bin/activate
112+
uv pip install -e "./bl1[vizdoom,dev]"
113+
uv pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
114+
uv pip install tensorboard==2.20.0 tables opencv-python
115+
116+
# Launch (BL-1 virtual CL1 + doom-neuron training)
117+
./run_bl1_doom.sh
118+
119+
# Open dashboard in browser
120+
# doom-neuron/visualisation.html
121+
```
122+
123+
See [INTEGRATION.md](../INTEGRATION.md) for full documentation.
124+
125+
### Live Monitoring Module (`bl1.monitor`)
126+
127+
A RatInABox-inspired monitoring system with composable plot methods:
128+
129+
```python
130+
from bl1.monitor import ActivityMonitor
131+
132+
mon = ActivityMonitor(n_neurons=10_000)
133+
134+
# Composable single-panel (RatInABox pattern)
135+
fig, ax = mon.plot_raster(window_s=10.0)
136+
fig, ax = mon.plot_mountain(window_s=10.0) # stacked filled-area
137+
fig, ax = mon.plot_mea_heatmap(window_s=1.0) # 8x8 electrode grid
138+
139+
# Full dashboard
140+
fig, axes = mon.plot_dashboard(window_s=10.0)
141+
142+
# MJPEG streaming for browser
143+
from bl1.monitor import NeuralMJPEGServer
144+
server = NeuralMJPEGServer(port=12350)
145+
server.start()
146+
server.update_frame(mon.render_frame(640, 480))
147+
```
148+
149+
---
150+
67151
## Installation
68152

69153
**From source (recommended):**

docs/images/bl1_doom_dashboard.png

2.65 MB
Loading

src/bl1/compat/action_protocol.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""Action inference protocol: doom-neuron -> BL-1.
2+
3+
Sent on port 12352 so BL-1 can visualize what the decoder is inferring
4+
from the neural spike responses.
5+
6+
Packet Format (80 bytes):
7+
[8B timestamp] uint64, microseconds
8+
[3 x 4B forward_probs] float32[3], softmax [none, forward, backward]
9+
[3 x 4B strafe_probs] float32[3], softmax [none, left, right]
10+
[3 x 4B camera_probs] float32[3], softmax [none, turn_left, turn_right]
11+
[4B attack_prob] float32, sigmoid probability of attack
12+
[4B forward_action] uint32, selected action (0/1/2)
13+
[4B strafe_action] uint32, selected action (0/1/2)
14+
[4B camera_action] uint32, selected action (0/1/2)
15+
[4B attack_action] uint32, selected action (0/1)
16+
[4B reward] float32, current step reward
17+
[4B episode_reward] float32, cumulative episode reward
18+
[4B kill_count] float32, kills this episode
19+
20+
Usage (doom-neuron sender)::
21+
22+
from action_protocol import pack_action_inference, ACTION_PORT
23+
sock.sendto(
24+
pack_action_inference(fwd_probs, strafe_probs, cam_probs, atk_prob,
25+
fwd_act, str_act, cam_act, atk_act,
26+
reward, ep_reward, kills),
27+
(bl1_host, ACTION_PORT),
28+
)
29+
30+
Usage (BL-1 receiver)::
31+
32+
from bl1.compat.action_protocol import unpack_action_inference, ACTION_PORT
33+
data = unpack_action_inference(packet)
34+
# data['forward_probs'] = [0.1, 0.8, 0.1] etc.
35+
"""
36+
37+
from __future__ import annotations
38+
39+
import struct
40+
import time
41+
42+
import numpy as np
43+
44+
ACTION_PORT = 12352
45+
46+
# 8 + 12 + 12 + 12 + 4 + 4 + 4 + 4 + 4 + 4 + 4 + 4 = 76... let me recount
47+
# 8 (ts) + 3*4 (fwd) + 3*4 (strafe) + 3*4 (cam) + 4 (atk_prob)
48+
# + 4*4 (actions) + 4 (reward) + 4 (ep_reward) + 4 (kills) = 80
49+
ACTION_PACKET_SIZE = 80
50+
ACTION_FORMAT = "<Q3f3f3fffffffffff"
51+
# Unrolled: Q + 3f(fwd) + 3f(strafe) + 3f(cam) + f(atk) + 4f(actions) + 3f(metrics)
52+
# = 8 + 12 + 12 + 12 + 4 + 16 + 12 = 76... hmm
53+
54+
# Let me be precise:
55+
# Q=8, 3f=12, 3f=12, 3f=12, f=4, I=4, I=4, I=4, I=4, f=4, f=4, f=4 = 80
56+
ACTION_FORMAT = "<Q9fffIIIIfff"
57+
58+
assert struct.calcsize(ACTION_FORMAT) == ACTION_PACKET_SIZE, (
59+
f"Format size {struct.calcsize(ACTION_FORMAT)} != expected {ACTION_PACKET_SIZE}"
60+
)
61+
62+
# Action labels for visualization
63+
FORWARD_LABELS = ["none", "forward", "backward"]
64+
STRAFE_LABELS = ["none", "left", "right"]
65+
CAMERA_LABELS = ["none", "turn_left", "turn_right"]
66+
67+
68+
def pack_action_inference(
69+
forward_probs: np.ndarray,
70+
strafe_probs: np.ndarray,
71+
camera_probs: np.ndarray,
72+
attack_prob: float,
73+
forward_action: int,
74+
strafe_action: int,
75+
camera_action: int,
76+
attack_action: int,
77+
reward: float = 0.0,
78+
episode_reward: float = 0.0,
79+
kill_count: float = 0.0,
80+
) -> bytes:
81+
"""Pack decoder inference into an 80-byte UDP packet."""
82+
timestamp = int(time.time() * 1_000_000)
83+
fp = np.asarray(forward_probs, dtype=np.float32)
84+
sp = np.asarray(strafe_probs, dtype=np.float32)
85+
cp = np.asarray(camera_probs, dtype=np.float32)
86+
87+
return struct.pack(
88+
ACTION_FORMAT,
89+
timestamp,
90+
fp[0], fp[1], fp[2],
91+
sp[0], sp[1], sp[2],
92+
cp[0], cp[1], cp[2],
93+
float(attack_prob),
94+
int(forward_action),
95+
int(strafe_action),
96+
int(camera_action),
97+
int(attack_action),
98+
float(reward),
99+
float(episode_reward),
100+
float(kill_count),
101+
)
102+
103+
104+
def unpack_action_inference(packet: bytes) -> dict:
105+
"""Unpack an 80-byte action inference packet."""
106+
if len(packet) != ACTION_PACKET_SIZE:
107+
raise ValueError(f"Expected {ACTION_PACKET_SIZE} bytes, got {len(packet)}")
108+
109+
v = struct.unpack(ACTION_FORMAT, packet)
110+
return {
111+
"timestamp_us": v[0],
112+
"forward_probs": np.array(v[1:4], dtype=np.float32),
113+
"strafe_probs": np.array(v[4:7], dtype=np.float32),
114+
"camera_probs": np.array(v[7:10], dtype=np.float32),
115+
"attack_prob": v[10],
116+
"forward_action": v[11],
117+
"strafe_action": v[12],
118+
"camera_action": v[13],
119+
"attack_action": v[14],
120+
"reward": v[15],
121+
"episode_reward": v[16],
122+
"kill_count": v[17],
123+
}

0 commit comments

Comments
 (0)