This repository trains and runs discrete-action SAC policies for closed-loop optogenetics on worm calcium-imaging data. The current codebase has two primary workflows and they must stay separate.
-
sac/mainline_replay.pyis the offline model-development loop. It reads a complete H5 file directly, builds feature matrices directly from H5, does a time split, trains on the early frames, validates on the later frames, and writes model-test artifacts. It does not subscribe to ZMQ, publish ZMQ, or replay checkpoints. -
sac/zmq_closed_loop.pyis the actual worm closed-loop program. It trains/prepares from H5, then runs online inference from ZMQ frames.realmode subscribes to external microscope ZMQ.dummymode self-publishes H5-derived fake ZMQ frames to test the subscriber, online projection, inference guard, and optional LabJack output. -
Training does not need ZMQ. Training needs
features: [T, neurons]andlight: [T]. These come from H5 throughsac/h5_to_feature.py. -
ZMQ is a communication layer for online inference and dummy dry runs.
sac/h5_to_zmq.pyshould not be part of the normal training path. -
Use the pixi environment. Run commands as
pixi run python ...unless there is a specific reason not to.
Use this as the first default for offline model-parameter testing:
data/Raw_Data/20260316_immobile_OptoAct_AWCON/w4/data_loader_saved.h5
Why:
- 3605 frames, about 2.0 minutes at 30 fps.
- 113 neurons.
green,red, andratiohave no missing feature pairs.- 16 light events, about 5 frames each.
- The current 70/30 time split has similar light statistics:
- train: light ratio about 2.18%, about 7.85 events/min, 11 events
- test: light ratio about 2.31%, about 8.32 events/min, 5 events
Second choice:
data/Raw_Data/20260316_immobile_OptoAct_AWCON/w5/data_loader_saved.h5
It also has clean feature coverage, but the 30% validation segment has only 2 light events, so metrics are noisier.
Avoid using outputs/closed_loop_data/* as the main 70/30 validation set. Those files often have very different train/test light frequencies because they are closed-loop outputs. They are useful for stress tests or behavior analysis, not clean parameter validation.
The mainline split is currently a direct chronological split:
split_idx = floor(total_frames * split_ratio)
train = frames[:split_idx]
test = frames[split_idx:]
Default split_ratio is 0.70.
If train/test light frequency differs strongly, do not trust a single 70/30 score as a clean validation result. Compare train/test light ratio, event count, and events per minute. For unstable data, prefer a block-based or event-balanced validation design before drawing conclusions.
pixi run python -c "
from pathlib import Path
from sac.mainline_replay import run_mainline_split
run_mainline_split(
h5_path=Path('data/Raw_Data/20260316_immobile_OptoAct_AWCON/w4/data_loader_saved.h5'),
out_dir=Path('outputs/mainline_model_test'),
split_ratio=0.70,
signal_source='green',
channel_index=1,
device='cuda:0',
)
"run_train_zmq_split() still exists only as a compatibility wrapper. New code should call run_mainline_split().
pixi run python -c "
from pathlib import Path
from sac.zmq_closed_loop import run_online_workflow
run_online_workflow(
h5_path=Path('data/Raw_Data/20260316_immobile_OptoAct_AWCON/w4/data_loader_saved.h5'),
out_dir=Path('outputs/closed_loop_dummy'),
mode='dummy',
limit_frames=200,
signal_source='green',
channel_index=1,
labjack_enabled=False,
)
"pixi run python -c "
from pathlib import Path
from sac.zmq_closed_loop import run_online_workflow
run_online_workflow(
h5_path=Path('data/Raw_Data/20260316_immobile_OptoAct_AWCON/w4/data_loader_saved.h5'),
out_dir=Path('outputs/closed_loop_real'),
mode='real',
zmq_host='127.0.0.1',
zmq_port=33091,
signal_source='green',
channel_index=1,
max_frames=1000,
labjack_enabled=True,
)
"Use labjack_enabled=True only when the hardware output should actually be active.
pixi run python -m py_compile sac/h5_to_feature.py sac/mainline_replay.py sac/zmq_closed_loop.py
pixi run python -c "from sac.mainline_replay import run_mainline_split; from sac.zmq_closed_loop import run_online_workflow; from sac.h5_to_feature import load_h5_frame_data; print('imports ok')"H5
-> h5_to_feature.load_h5_frame_data()
-> features/light/frame_times/neuron_ids
-> chronological split
-> train_sac_from_arrays(train_features, train_light)
-> PCA/state/replay/training inside train_sac.py
-> project all features with train PCA
-> predict validation frames
H5 -> features/light -> train SAC -> save/load policy artifacts
external microscope ZMQ -> OnlineFeatureProjector -> ensemble action -> guard -> optional LabJack
H5 -> features/light -> train SAC
H5-derived fake ZMQ messages -> local PUB/SUB -> OnlineFeatureProjector -> ensemble action -> optional LabJack
-
sac/h5_to_feature.pyShared H5 feature loading and small helpers. Its primary output isH5FrameData(features, light, frame_times, neuron_ids, coverage, light_intervals, meta). It intentionally stops before fitting PCA because PCA/state generation depends on the train split. -
sac/mainline_replay.pyOffline split-mode model testing. Keep it H5-only. -
sac/zmq_closed_loop.pyHardware-facing closed-loop workflow. Keep real/dummy behavior stable and be cautious with LabJack changes. -
sac/train_sac.pyCore SAC training. It fits PCA, builds stacked states, builds replay, trains the vectorized SAC ensemble, and writes artifacts. -
sac/data_utils.pyPCA/state/replay utilities plusOnlineFeatureProjectorfor online ZMQ frames. -
sac/h5_to_zmq.pyUtility for H5-derived fake ZMQ messages and optional publishing. Use it for dummy mode or ZMQ testing, not for normal training. -
sac/naive_baseline.pyRule-based comparison baseline. Some paths still use H5-derived fake ZMQ helpers.
-
signal_sourceH5 table family used as the canonical source for IDs/times:green,red, orratio. -
channel_indexColumn inside ZMQ-style messagefluorescence[:, channel_index]. In H5-direct loading, it selects the corresponding source fromoutput_channel_sources. -
output_channel_sourcesUsually"red,green,ratio", sochannel_index=1selectsgreen. -
pca_dimandstack_framesState dimension ispca_dim * stack_frames. Current defaults inTrainConfigare 10 and 3, so state dim is 30. -
fill_missingMissing-value policy is shared with the old H5-to-ZMQ-like path throughh5_to_zmq.apply_fill_policy(). Keep this consistent when comparing runs.
Typical run directory contains:
config.jsondataset_stats.jsonpseudo_reward_reference.jsontarget_state.npypca_components.npypca_mean.npycheckpoints/agent_XX.ptbest_checkpoints.jsontrain_log.jsonlagent_stop_summary.jsonensemble_summary.json- mainline-only validation CSV/HTML summaries when using
run_mainline_split()
- Keep
mainline_replay.pyandzmq_closed_loop.pyseparate. - Do not make
zmq_closed_loop.pydepend on high-level mainline workflows. - Do not put argparse/click parsing inside core business logic.
- Core functions should accept plain parameters and stay importable.
- Use
python -m module.pathfor module entry points. - Read relevant files before making changes; keep edits minimal around hardware-facing code.
- Do not enable LabJack output by default in test commands.