Skip to content

braid-opto-power-measure

Measures the spatial distribution of optical power in a fly-tracking arena, for two purposes:

  1. A general power/irradiance heatmap of the arena.
  2. 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_mm in config.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, update port in config.toml's [light] section, or find it with ls /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.csvwall_clock_ts, trigger_timestamp, obj_id, frame, x, y, z, xvel, yvel, zvel (one row per tracked LED per frame; positions in meters).
  • power.csvwall_clock_ts, power, power_units, light_on (one row per power meter reading; light_on is 1 while an optogenetic pulse is active, else 0).
  • 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 to config.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.