Write a ARCHITECTURE.md at the repo root (/Users/loaloa/homedataAir/phd/ratvr/VirtualReality/CoreRatVR/ARCHITECTURE.md) covering the following sections in order.
FastAPI server on :8000, Svelte UI at /ui, orchestrates hardware I/O via shared memory on Linux/macOS; designed for rodent VR neuroscience experiments. One sentence per major concern (hardware bridging, memory bus, logging, post-processing).
Five components, prose + short bullet lists:
- API server (
main_vr.py) —app.state.stateas single source of truth; keysprocs(PID dict),shm(created flags),initiated,paradigmRunning, SHM interface handles. - Config singleton (
Parameters.py) — all directories, SHM names/sizes, camera specs, hardware details; distinguish locked keys (allSHM_NAME_*, System/Hardware group) vs. patchable keys editable at runtime viaPATCH /parameters/{key}. - SHM bus (
SHM/) — three region types, JSON descriptor files written toSHM_STRUCTURE_DIRECTORY/, macOS file-backed shim (OSXFileBasedSHM). - Subprocess pool (
process_launcher.py) — allread2SHM/,dataloggers/, Unity binary, Maxwell binaries; each launched viasubprocess.Popenwith its log file; CLI args pass JSON descriptor paths so subprocesses find SHM by path. - Session data store — HDF5 per modality in a timestamped session directory under
DATA_DIRECTORY/.
Include a diagram showing:
Browser UI (Svelte /ui)
│ HTTP / WebSocket :8000
▼
main_vr.py (FastAPI / uvicorn)
app.state.state
├─ procs {name → PID}
├─ shm {name → bool}
├─ initiated / paradigmRunning
└─ SHM interface handles
│
┌─────┴───────────────────────────────────┐
│ POST /shm/create_* POST /procs/launch_* │
│ │
▼ ▼
shm_creation.py process_launcher.py
creates SHM regions subprocess.Popen(...)
+ JSON descriptors → read2SHM/*
→ dataloggers/*
→ Unity / Maxwell binaries
│ │
└──────── SHARED MEMORY BUS ──────────┘
termflag paradigmflag ballvelocity
portentaoutput portentainput
unityoutput unityinput
facecam ttlcam2/3/4 bodycam unitycam
│
▼
HDF5 session files (written by loggers during paradigm)
portenta_output.hdf5
unity_output.hdf5
{cam_name}.hdf5 (per camera)
│
▼
session_processing/process_session.py → NAS + DB
Callout boxes: paradigmflag = start/stop gate for all loggers; termflag = shutdown broadcast to all subprocesses.
Explain the three region types in prose:
singlebyteflag — 1-byte region;FlagSHMInterfacewraps it withset(),reset(),is_set(). Used as broadcast signals readable by every process without coordination.cyclic_packagesring-buffer —[pkg_0][pkg_1]…[pkg_N-1][write_ptr (8 B)];CyclicPackagesSHMInterfaceprovidespush()/popitem(). Lock-free single-writer, single-reader design; write pointer stored at tail so any reader can find it.cyclic_framesring-buffer — same structure ascyclic_packagesbut each slot is[80 B metadata header][raw frame bytes]; used for camera streams.
Then a representative table (~5 key regions):
| SHM name | Type | Key size | Producer | Consumers | Purpose |
|---|---|---|---|---|---|
termflag |
singlebyte | 1 B | API server | all subprocesses | Shutdown broadcast |
paradigmflag |
singlebyte | 1 B | API server | all loggers, portenta bridge | Start/stop logging gate |
ballvelocity |
cyclic_packages | 4096 × 80 B | portenta2shm2portenta |
log_portenta, streamer |
Treadmill velocity stream |
unityoutput |
cyclic_packages | 128 × 256 B | Unity binary | log_unity, WS streamer |
VR frame + trial events |
facecam |
cyclic_frames | 32 × (80 B + frame) | vimbacam2shm |
log_camera_cyclic, WS streamer |
Face camera frames |
Note: descriptor JSON for each region is written to SHM_STRUCTURE_DIRECTORY/ and passed to subprocesses via CLI argument --xxx_shm_struc_fname.
Numbered sequence keyed to real API endpoints. Call out that validate_state() in backend_helpers.py enforces order and raises HTTP 400 for out-of-sequence calls.
POST /initiate— creates timestamped session directory (DATA_DIRECTORY/{ts}_{animal}_{paradigm}/), writesparameters.json, startsCustomLogger. Must happen before any SHM or process endpoints.- Set session metadata (can be done in any order after initiate):
POST /session/paradigm/{name}POST /session/animal/{name}POST /session/animalweight/{value}
POST /shm/create_*— create each required SHM region. The server holds writer handles totermflag,paradigmflag, andunityinputdirectly.POST /procs/launch_*— start each subprocess. All subprocesses are now running but paused — they loop onparadigmflag.is_set()returningFalse.POST /start_paradigm— callsSessionParamters.handle_start_session(), then raisesparadigmflag→ all loggers unblock simultaneously and begin writing HDF5.POST /stop_paradigm— callsSessionParamters.handle_stop_session(), then lowersparadigmflag→ loggers pause but stay alive (ready for another start/stop cycle).POST /raise_term_flag— raisestermflag→ all subprocesses exit their loops → SHM regions are deleted → optionally launchessession_processing/process_session.py→ server state fully reset.
Detail what happens at each gate transition:
POST /start_paradigm triggers:
SessionParamters.handle_start_session():- Records
start_time = datetime.now() - Parses
paradigm_idfrom paradigm filename (chars 1–4, e.g.P0800_…→800) - Copies paradigm
.xlsxto session directory - Reads Excel sheets:
Environment,EnvParameters,SessionParameters - Loads FSM JSON assets:
fsm_states.json,fsm_transitions.json,fsm_decisions.json,fsm_actions.jsonfromUnityRatVR/paradigmFSMs/
- Records
paradigm_running_shm_interface.set()— raisesparadigmflag- All loggers (
log_portenta,log_unity,log_camera_cyclic× N,log_ephys) unblock from their wait loop and callshm.reset_reader()to skip stale data, then start writing HDF5
POST /stop_paradigm triggers:
SessionParamters.handle_stop_session():- Records
stop_time, computesduration - Writes
session_parameters.jsonto session directory (merged metadata + FSM assets)
- Records
paradigm_running_shm_interface.reset()— lowersparadigmflag- All loggers return to their wait loop (no process restart required)
Arduino Portenta side-effect (portenta2shm2portenta.py):
- The serial bridge polls
paradigmflagon every loop iteration - On rising edge (low → high): sends
W1000\r\nto the serial port (1000 ms pause command) - On falling edge (high → low): sends
W2000\r\nto the serial port (2000 ms pause command) - This synchronises the Arduino's internal state machine to the paradigm gate
Table of files written per session:
| File | Written by | Contents |
|---|---|---|
parameters.json |
POST /initiate |
Full Parameters singleton snapshot |
session_parameters.json |
POST /stop_paradigm |
Animal, paradigm metadata, FSM JSON assets, timing |
unity_output.hdf5 |
log_unity |
Keys: unityframes (VR frame packages), trialPackages (trial events) |
portenta_output.hdf5 |
log_portenta |
Keys: ballvelocity (treadmill), portentaoutput (events) |
facecam.hdf5 |
log_camera_cyclic |
JPEG-encoded frames + frame IDs; separate *_packages.hdf5 for metadata |
ttlcam2/3/4.hdf5 |
log_camera_cyclic |
Same as face cam |
bodycam.hdf5 |
log_camera_cyclic |
Same as face cam (color: 3-channel) |
unitycam.hdf5 |
log_camera (single-frame variant) |
JPEG frames from Unity render camera |
| Ephys files | mxwserver binary (Maxwell) | Raw MEA1K recordings; path set by log_ephys via mx.Saving API |
*.log × N |
CustomLogger per subprocess |
Per-process log files in LOGGING_DIRECTORY/ |
This section is inferred from endpoint structure and the Svelte UI mount at /ui. Label it clearly as inferred.
The operator interacts entirely through the browser UI (http://localhost:8000/ui). The UI is a Svelte single-page app served from UIRatVR/dist/. UI state is driven by an SSE stream (GET /statestream, polling every 100 ms) which reflects app.state.state live.
Operator flow:
- Start the server: run
python main_vr.pyin the terminal; navigate tohttp://localhost:8000/ui. - Configure parameters: edit mutable fields (animal names, paradigm directories, camera IDs, etc.) via the Parameters panel →
PATCH /parameters/{key}. Locked fields (hardware/SHM names) are read-only. - Initiate a session: click "Initiate" button →
POST /initiate. Session directory is created; logger starts. The UI transitions to the session setup view. - Select animal and paradigm: dropdown populated from
GET /animalsandGET /paradigms→POST /session/animal/{name},POST /session/paradigm/{name},POST /session/animalweight/{val}. - Create SHM regions: click per-region "Create" buttons (or a global "Create All") →
POST /shm/create_*. Indicators in the UI turn green as each region is confirmed created. - Launch processes: click per-process "Launch" buttons (or "Launch All") →
POST /procs/launch_*. Process status indicators show PID or ✗. Live camera previews become available via WebSocket streams (/stream/facecam,/stream/bodycam, etc.). - Start paradigm: click "Start" →
POST /start_paradigm. A timer starts in the UI. Live data streams (/stream/ballvelocity,/stream/unityoutput) become active. All loggers begin recording. - Monitor: live velocity plot, camera feeds, and Unity output are rendered via WebSocket. Log file panel (
/stream/logfiles) shows rolling subprocess logs. - Stop paradigm: click "Stop" →
POST /stop_paradigm. Timer freezes. Loggers pause. Data is flushed to HDF5. - Terminate session: click "Terminate" →
POST /raise_term_flagwith options (trash session / copy to NAS / launch processing). All processes exit; SHM cleaned up; UI resets to initial state.
Inspect mode (no hardware required):
- Navigate to the Inspect panel →
POST /inspect/initiate_session_selection/{session_name}(sessions listed from NAS viaGET /inspect/sessions). - Browse trial data (
GET /inspect/trials), events (GET /inspect/events), frames (GET /inspect/unityframes). - WebSocket streams support
?inspect=trueto replay camera frames and package data from NAS HDF5 at arbitrary timestamps. - Exit with
POST /inspect/terminate_inspection.
- Audience: new lab member who knows Python but is new to this codebase.
- Prose tone: technical, precise, no filler sentences.
- All file references should link to actual files in the repo.
- All API endpoint paths should be formatted as inline code.
- No frontmatter in the output file.
- No code blocks in the final
ARCHITECTURE.mdprose sections — use tables, markdown lists, and the ASCII diagram only. - The ASCII diagram should be inside a fenced code block.