braid-opto-power-measure
Measures the spatial distribution of optical power in a fly-tracking arena, for two purposes:
- A general power/irradiance heatmap of the arena.
- The intensity of the optogenetic (red LED) stimulus, both ON and OFF (baseline), at each position — using the same on/off pulse pattern as real experiments.
It works by tracking 4 IR LEDs with braid (as 4 separate objects) while a Thorlabs PM100D power meter is swept by hand through the arena, held at the centroid of the 4 LEDs. Positions and power readings are recorded to two independent CSVs and aligned afterward.
Hardware setup
- 4 IR LEDs, arranged in a known square (
square_side_mminconfig.toml), rigidly attached around the PM100D sensor head so their centroid tracks the sensor's position. - Thorlabs PM100D power meter, connected via USB.
- Optogenetic light Arduino (
/home/nfc/src/OptoFly/arduino/firmware/opto_trigger), connected via USB. This tool expects it to show up at/dev/opto_trigger(a stable device name, typically set up via a udev rule on the tracking machine) — if it shows up elsewhere on your machine, updateportinconfig.toml's[light]section, or find it withls /dev/serial/by-id/. - A running braid installation, configured with
~/braid-configs/power_meter.toml(created once — see "One-time setup" below).
Installation
This project uses uv for all dependency management:
uv sync
This also installs the project's own code (src/braid_opto_power_measure/) into
the project's virtual environment, which is what makes the record and
analyze commands below available via uv run.
One-time setup
Create the braid tracking config for this rig (tracks up to 6 bright points per camera — 4 real LEDs plus headroom — instead of the single point most other configs track):
cp ~/braid-configs/laser.toml ~/braid-configs/power_meter.toml
sed -i 's/max_num_points = 1/max_num_points = 6/' ~/braid-configs/power_meter.toml
Configuration
Rig constants live in config.toml (checked into this repo):
[braid]
url = "http://127.0.0.1:8397/"
[power_meter]
wavelength_nm = 625
device_address = "" # blank = auto-select the first connected PM100D that supports wavelength_nm
sensor_diameter_mm = 9.5
[leds]
square_side_mm = 100.0
[light]
enabled = true
port = "/dev/opto_trigger"
baud_rate = 115200
color = "red"
intensity = 128 # 0-255
on_duration_ms = 300 # firmware clamps to 0-3000
period_s = 10.0 # time between the start of successive pulses
Per-run knobs are CLI flags (see below), not config — session name, duration, output location, and hardware dry-run switches.
Recording a session
Two equivalent ways to run it:
Option A: one combined script
./launch_power_meter.sh --session-name arena_sweep_1
Starts braid-run with power_meter.toml, waits for it to come up, runs the
recorder in the foreground, and stops braid-run when the recorder exits.
Option B: launch braid yourself
# terminal 1
braid-run ~/braid-configs/power_meter.toml
# terminal 2
uv run record --session-name arena_sweep_1
Useful if you want to inspect braid's own UI/logs independently, or braid is already running for another reason.
CLI flags (both options, record/launch_power_meter.sh forward the same flags)
| Flag | Default | Meaning |
|---|---|---|
--session-name |
(required) | Used in the output directory name |
--duration |
run until Ctrl+C | Stop automatically after this many seconds |
--output-dir |
sessions/ |
Where session directories are created |
--config |
config.toml |
Path to the rig config file |
--virtual |
off | Simulate the power meter (no real PM100D needed) |
--no-light |
off | Disable light cycling (ambient-only sweep, no Arduino needed) |
--virtual-light |
off | Simulate the light controller (no real Arduino needed) |
--power-interval |
0.01 |
Minimum seconds between power meter reads |
Stop recording any time with Ctrl+C — everything already written to disk is kept.
Output
Each run creates sessions/<timestamp>_<session-name>/ containing:
braid_positions.csv—wall_clock_ts, trigger_timestamp, obj_id, frame, x, y, z, xvel, yvel, zvel(one row per tracked LED per frame; positions in meters).power.csv—wall_clock_ts, power, power_units, light_on(one row per power meter reading;light_onis1while an optogenetic pulse is active, else0).session_metadata.toml— a snapshot of the settings actually used for this run (wavelength, sensor diameter, light intensity/duration/period, whether light-cycling was enabled), independent of later changes toconfig.toml.
Analysis
uv run analyze sessions/<timestamp>_<session-name>
Loads both CSVs, computes the 4-LED centroid per frame (skipping any frame that
doesn't have exactly 4 tracked objects), aligns each centroid to the nearest
power reading (within 20ms by default — override with --tolerance-s), and
writes up to two heatmaps into the session directory (or --output-dir if
given):
heatmap_light_on.png— stimulus intensity map.heatmap_light_off.png— ambient/baseline map.
Either file is skipped if the session has no samples in that state (e.g. a
--no-light session only produces heatmap_light_off.png). Warnings are
printed if the 4 LEDs' geometry deviated significantly from the configured
square, or if some position samples had no power reading within tolerance.
If no frame in the whole session has exactly 4 tracked objects (e.g. an
LED was occluded throughout, or braid was tracking the wrong config),
analyze prints a message and exits without writing any heatmaps.
Project layout
src/braid_opto_power_measure/ # the installable package (record.py, analyze.py, and their helpers)
tests/ # pytest suite, one file per module
config.toml # rig constants (checked in)
launch_power_meter.sh # combined braid-run + recorder launcher
Running the tests
uv run pytest
Hardware-adjacent modules (power_reader.py, light_controller.py) are tested
against real simulated-device modes (virtual=True / loop://), not mocks, so
the test suite exercises the actual driver code paths without needing the PM100D
or Arduino attached.
License
GPLv3 — see LICENSE.