optofly-analysis
Analyzes fly flight-tracking recordings (.braidz files) to answer two
questions, without writing any code:
- Do the flies behave normally? — velocity and saccade (fast-turn) statistics.
- Did they respond to the stimulus (a looming visual threat, or an optogenetic light activation), and how? — % responsive, turn direction, turn size, broken down by the condition that varied (stimulus angle, light intensity, …).
It also compares groups of recordings side by side (e.g. control vs mutant).
If you just want to run an analysis and get plots, skip to Quick start. If something goes wrong, jump to Troubleshooting.
Requirements
- uv — the only thing you need to install yourself. It manages Python and all dependencies for you.
- One or more
.braidzrecording files.
You do not need to install Python separately, and you do not need to
pip install anything.
Quick start
# 1. From the optofly-analysis folder, install everything (once, or after an update):
uv sync
# 2. Run the analysis on your recording(s):
uv run optofly-analyze /path/to/your_recording.braidz -o results/
That's it. When it finishes, open the newest folder inside results/ — it's
named after the date and time of the run (e.g. results/20260805_143022/) —
and look at summary.txt plus the .png plots inside.
Every run gets its own timestamped subfolder, so re-running never overwrites
a previous result. It's safe to point -o at the same folder every time.
Comparing groups (e.g. control vs mutant)
To analyze several recordings together and get a side-by-side comparison, list them in a "runs" TOML file instead of passing paths directly:
# runs/my_experiment.toml
[groups.control]
files = ["/data/control_1.braidz", "/data/control_2.braidz"]
[groups.mutant]
files = ["/data/mutant_1.braidz"]
uv run optofly-analyze -r runs/my_experiment.toml -o results/
Two or more groups automatically get extra overlay plots under
results/<timestamp>/comparison/.
How it works, in plain terms
For each recording, the pipeline:
- Loads the tracked flight paths and keeps only the "real" ones (drops short, stationary, or out-of-arena trajectories — these are almost always tracking noise, not real flies).
- Computes heading, linear velocity, and angular velocity for each remaining trajectory.
- Detects saccades (fast turns) from angular velocity spikes.
- If the file contains a looming-stimulus or optogenetics log, matches each stimulus/activation event to what the fly did just after it, and scores whether that counts as a "response."
- Saves one PNG per plot, plus a
summary.txtwith the headline numbers (object counts, saccade counts, % responsive).
Nothing is guessed or invented for missing data: if a recording has no stimulus log, the stimulus plots are simply skipped for that file (see Troubleshooting for the most common surprise here).
Two ways to use it
1. Batch CLI — recommended for most runs
Run the whole pipeline hands-off and get a folder of plots. See Quick start above and the full reference below.
2. Notebooks — for exploring one recording interactively
The notebooks/ directory holds thin driver notebooks. Open one, edit the
Parameters cell near the top (just the file path, and optionally which
config preset to use), then run the notebook top to bottom:
notebooks/analysis.ipynb— trajectories, kinematics, and saccade statsnotebooks/stim_analysis.ipynb— looming-stimulus responsesnotebooks/opto_analysis.ipynb— optogenetic-activation responses
uv run jupyter lab
Use notebooks when you want to inspect intermediate results (e.g. "show me this fly's trajectory") rather than just the final plots.
CLI reference
optofly-analyze [files ...] [-p PARAMS] [-r RUNS] [--only {stim,opto,both,none}] [-o OUTPUT]
| Flag | Meaning |
|---|---|
files (positional) |
One or more .braidz paths for a quick single-group run. Treated as one group named data. Cannot be combined with -r. |
-p, --params PATH |
TOML file with a [parameters] table (tuning). Optional — sensible defaults are used if omitted. |
-r, --runs PATH |
TOML file with [groups.<name>] tables (which files to analyze, and how to group them for comparison). Cannot be combined with positional files. |
--only {stim,opto,both,none} |
Force which event analyses run, overriding auto-detection. Behavioral plots always run regardless. |
-o, --output DIR |
Output directory (default ./optofly_results). Each run writes its own timestamped subfolder inside it — safe to reuse across runs. |
Examples:
# Quick single-group run with default parameters
uv run optofly-analyze exp1.braidz exp2.braidz -o results/
# Preset parameters + a file list with named groups
uv run optofly-analyze -p configs/stimulus.toml -r runs/my_experiment.toml -o results/
# Only look at the optogenetics response, even if the file also has a stim log
uv run optofly-analyze exp1.braidz --only opto -o results/
Two kinds of TOML config are kept separate, because they change at different rates:
- Parameter presets (
configs/) — tuning tied to your rig and analysis type (behavior.toml,stimulus.toml,opto.toml). A[parameters]table only; edited rarely. Every field is documented inline in those files. - File lists (
runs/) — which recordings to analyze, grouped for comparison.[groups.<name>]tables only; edited every run.
They can also live in one combined file if you prefer — pass it to both -p
and -r.
Output layout
results/
20260805_143022/ <- one timestamped folder per run
<group>/
behavior/ velocity_distributions.png, saccade_amplitude.png,
saccade_rate.png, saccade_locations.png, saccade_traces.png
stim/ responsiveness.png, evoked_rate.png, response_angular.png,
response_angular_signed.png, response_linear.png,
heading_difference.png, turn_direction.png
(only if the file has a real looming-stimulus log)
+ responsiveness_subtracted.png (only if the data has sham trials)
opto/ same plot set as stim/ (only if opto.csv present)
opto_effect/ responsiveness.png, evoked_rate.png, response_angular.png
(only if both stim and opto data are present and enabled --
compares the loom response with real opto vs. opto-sham vs.
no-opto-nearby, using each fly's nearest opto event within
opto_stim_match_window_s seconds as the trial's pairing)
comparison/ velocity.png, saccade_amplitude.png,
stim_responsiveness.png, opto_responsiveness.png
(only if you defined 2+ groups)
summary.txt object/saccade/event counts and % responsive, per group
stim/ and opto/ folders only appear when the corresponding data was found
and usable — see the first item in Troubleshooting below.
Troubleshooting
command not found: uv
uv isn't installed. Follow the install instructions,
then re-open your terminal.
ModuleNotFoundError: No module named 'optofly_analysis'
You ran python3 ... instead of uv run ... (or forgot uv run in front of
optofly-analyze). Always prefix commands with uv run — that's what puts
you in the right Python environment. Run uv sync first if you haven't yet.
[<group>] no usable files, skipping group
The .braidz path is wrong, or the file doesn't actually contain tracking
data (kalman_estimates.csv.gz inside the zip). Double-check the path and
that the file finished writing/copying correctly.
[<group>] no objects passed filtering, skipping group
Every trajectory in the file was filtered out — usually because the arena
box (x_range / y_range / z_range in your parameter preset) doesn't
match your rig's coordinates, or min_frames / min_distance are too strict
for a short recording. Open configs/behavior.toml (or whichever preset you
used) and loosen these for a test run to confirm.
Stim (or opto) plots didn't show up, and I know the file has that data
Some acquisition setups write a generic stim.csv trigger log even for
opto-only recordings — it exists but has no real looming parameters, so it's
correctly skipped by default. You'll see a printed line like:
stim.csv found but has no stim-specific data; skipping stim analysis.
If you're sure it should run anyway, force it: add run_stim = true (or
run_opto = true) to your [parameters] table, or pass --only stim /
--only opto / --only both on the command line.
I have both stim and opto data but no opto_effect/ folder
Either only one of stim/opto actually ran for that file (see the entry
above), or every looming-stimulus event ended up classified the same way
(e.g. opto always fired near every loom, so there's no loom_only baseline
to compare against) -- you'll see a printed line like stim and opto both
present but every stim event matched the same opto_condition; skipping
opto_effect comparison. If events that should have paired are ending up
loom_only, your rig's FOV offset may exceed the matching tolerance --
raise opto_stim_match_window_s (default 0.5 seconds) in your
[parameters] table. (This check, like the plots themselves, only counts
non-looming-sham trials -- looming-sham trials are pooled into their own
grey "sham" bar in these plots, a different concept from opto-sham /
loom_sham_opto.)
I passed 2+ groups but don't see comparison plots Comparison overlays are only written for an analysis kind (stim or opto) if more than one group actually produced results for it — e.g. if only one group has a real stim log, there's nothing to compare it against.
Could not read --params/--runs '...': ...
Something's wrong with the config file itself — a typo'd path, broken TOML
syntax, or (for --params) an unknown parameter name. The message after the
colon says which: No such file or directory means the path is wrong;
Unknown parameter(s) in config: ... means a key in [parameters] is
misspelled — check the exact field names in configs/behavior.toml /
stimulus.toml / opto.toml, or see AnalysisConfig in
optofly_analysis/config.py.
Provide either files or --runs, not both.
Pick one input mode: either list .braidz paths directly on the command
line, or put them in a -r/--runs TOML file — not both at once.
Where are my results?
Inside a timestamped subfolder of whatever you passed to -o (default
./optofly_results/) — not directly in it. Look for the most recent
YYYYMMDD_HHMMSS folder.
Library API
For custom analysis in your own script or notebook:
from optofly_analysis import (
read_braidz, filter_objects, calculate_kinematics,
detect_saccades, filter_saccades,
extract_stim_responses, extract_opto_responses,
)
data = read_braidz("exp.braidz") # -> BraidzData(df, stim, opto)
df = filter_objects(data.df) # length / distance / arena filters
df = calculate_kinematics(df, fps=100) # heading + angular/linear velocity
saccades = detect_saccades(df, fps=100) # -> SaccadeResults
saccades = filter_saccades(saccades) # drop edge-of-arena events
responses = extract_stim_responses(df, data.stim, saccades, fps=100)
Package layout
| Module | Responsibility |
|---|---|
io.py |
Load braidz files (read_braidz → BraidzData) |
filtering.py |
Select valid object trajectories |
kinematics.py |
Heading, angular and linear velocity |
saccades.py |
Saccade detection and edge filtering (SaccadeResults) |
responses.py |
Generic event-response engine (EventResponses) |
stimulus.py / opto.py |
Looming-stimulus / optogenetic response extraction |
plotting.py |
All figures (each returns a matplotlib Figure) |
config.py |
AnalysisConfig + TOML loading |
pipeline.py / cli.py |
Batch pipeline and optofly-analyze entry point |
Development
uv run pytest # tests in tests/
uv run ruff check # lint