Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
36ab727
feat: async run_experiment via RunHandle + cancellation + status widget
hinderling May 15, 2026
fa22eb4
refactor: route image-size lookup through AbstractMicroscope, not mmc
hinderling May 15, 2026
8bf38dd
fix: make ExperimentStatusWidget work under napari/Qt
hinderling May 15, 2026
c8b7d55
example: async optogenetic demo notebook with napari + status widget
hinderling May 15, 2026
8b0f331
feat: observable + controllable async runs (events, pause, restart fix)
hinderling May 16, 2026
ccfdb69
feat: rich ExperimentStatusWidget with event strip + FOV map
hinderling May 16, 2026
3c0e798
example: multi-FOV plan in the async optogenetic demo
hinderling May 16, 2026
4cd5399
feat: make FrameDispenser waits cancellable
hinderling May 18, 2026
45ba3c6
feat: harden async experiment runs
hinderling May 18, 2026
b23c3b6
feat: refine the experiment status widget
hinderling May 18, 2026
b45d683
Drain engine queue on interactive pause
hinderling May 21, 2026
3ea0172
Add fixed-duration WaitEvents between experiment phases
hinderling May 21, 2026
b7e97ec
Render wait events + polish the experiment status strip
hinderling May 21, 2026
eea4086
Test interactive pause + WaitEvent behaviour
hinderling May 21, 2026
bc6c0b2
Add motile to the test extra
hinderling May 21, 2026
c367a2c
Move motile into its own extra, referenced by test
hinderling May 21, 2026
94fe031
Handle WaitEvents in event introspection and validation
hinderling May 21, 2026
5175909
Add Niesen.shutdown() for thread + COM cleanup
hinderling May 21, 2026
30b6b88
Refresh async optogenetic demo notebook outputs
hinderling May 21, 2026
062225d
Document async runs, pause, and timed waits in the README
hinderling May 21, 2026
bccdb03
Block on run_experiment().wait() in non-interactive notebooks
hinderling May 21, 2026
bce2d9f
Migrate cell-migration + line-stim notebooks to async run
hinderling May 21, 2026
ec233d7
Drain the pipeline before reading results in 21/22 notebooks
hinderling May 21, 2026
e95f94b
Migrate 11 single/two-phase stim notebooks to async run
hinderling May 21, 2026
5edf76d
Migrate stim_ramp multi-phase notebook to async run
hinderling May 21, 2026
c2551a5
Fix mis-tagged markdown cells in stim_dfacquire
hinderling May 28, 2026
6dfbc6b
Drop the napari core_link teardown dance from run cells
hinderling May 28, 2026
6dae4e6
Bump virtual-microscope lock pin; relock motile extra
hinderling May 28, 2026
13a5402
feat: run DMD calibration in the background
dario-bassi May 28, 2026
5fb5239
test: remove the stale top-level hardware test duplicates
dario-bassi May 28, 2026
ea4e880
test: speed up the pipeline-lag hardware test (~2 min -> ~30 s)
dario-bassi May 28, 2026
e7fbec4
test: halve the hardware-test interval (5 s -> 2 s)
dario-bassi May 28, 2026
6e2afd6
feat: stagger per-FOV min_start_time in apply_fov_batching
hinderling May 19, 2026
ee97718
fix: poll status in the widget tick so it updates without queued signals
dario-bassi May 28, 2026
7a3549c
fix: validate phase_id up front for phased experiments
dario-bassi May 28, 2026
7facbf2
fix: guard phase_id access in the deferred pipeline too
dario-bassi May 28, 2026
9fdd5b7
fix: don't add an inferred interval across a WaitEvent boundary
dario-bassi May 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 34 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ events = RTMSequence(

# 4. Run!
ctrl = Controller(mic, pipeline)
ctrl.run_experiment(list(events), stim_mode="current")
handle = ctrl.run_experiment(list(events), stim_mode="current")
handle.wait() # run_experiment is non-blocking; wait() blocks until done
```

## Pipeline
Expand Down Expand Up @@ -132,6 +133,14 @@ events = combine(setup_a, setup_b, axis="p")

`combine()` is variadic (`combine(a, b, c, d, ..., axis=...)`), handles the N=0 and N=1 degenerate cases, and is the only composition primitive — there is deliberately no shorthand operator, so every multi-step experiment reads the composition axis explicitly.

**Timed waits between phases.** `wait(seconds)` inserts a fixed-duration pause — e.g. to let cells recover before stimulating. It acquires no frames and just delays everything after it:

```python
from faro.core.data_structures import wait

events = combine(baseline, wait(60), stim_phase, axis="t")
```

### Stimulation

Stimulation channels are acquired on specific frames, controlled via DMD/SLM. Define them with `stim_channels` and `stim_frames`:
Expand Down Expand Up @@ -235,11 +244,25 @@ events = apply_fov_batching(events, time_per_fov=2.0)
from faro.core.controller import Controller

ctrl = Controller(mic, pipeline)
ctrl.run_experiment(events, stim_mode="current")
handle = ctrl.run_experiment(events, stim_mode="current")
handle.wait() # block until the run finishes
```

`validate_events()` runs automatically before the experiment starts (disable with `validate=False`). It checks both pipeline compatibility and hardware limits.

`run_experiment()` and `continue_experiment()` are **non-blocking** — they return a `RunHandle` so the kernel stays free (e.g. to use the napari viewer). Call `handle.wait()` to block until the run finishes.

```python
handle = ctrl.run_experiment(events, stim_mode="current")
handle.pause(); handle.resume() # stop/resume acquiring; schedule is preserved
handle.cancel() # graceful stop
handle.wait() # block until done

# Live status badge, progress strip, and Pause/Stop buttons in napari:
from faro.widgets import ExperimentStatusWidget
viewer.window.add_dock_widget(ExperimentStatusWidget(ctrl), area="right")
```

### Experiment Continuation

Call `run_experiment()` once, then `continue_experiment()` to append more phases. The Analyzer (and all per-FOV tracking state) is reused, so timesteps, filenames, and particle IDs continue seamlessly.
Expand All @@ -249,15 +272,15 @@ ctrl = Controller(mic, pipeline)

# Phase 1: baseline — find cells, measure growth rate
phase1 = RTMSequence(time_plan={"interval": 10, "loops": 60}, ...)
ctrl.run_experiment(phase1, validate=False)
ctrl.run_experiment(phase1, validate=False).wait() # wait() before reading results

# Analyse phase-1 results to decide what to do next
df = pd.read_parquet("tracks/000_latest.parquet")
fast_growers = df.groupby("particle")["area"].apply(lambda x: x.diff().mean())

# Phase 2: stimulate based on analysis
phase2 = RTMSequence(time_plan={"interval": 10, "loops": 120}, ...)
ctrl.continue_experiment(phase2)
ctrl.continue_experiment(phase2).wait()

# Always call finish_experiment() when done
ctrl.finish_experiment()
Expand All @@ -270,12 +293,12 @@ ctrl.run_experiment(baseline_events, validate=False) # runs in background threa
ctrl.extend_experiment(extra_events) # non-blocking, appends to running acquisition
```

| Method | When to use |
|--------|-------------|
| `run_experiment()` | First acquisition — creates a fresh Analyzer |
| `continue_experiment()` | Subsequent phases — reuses Analyzer, offsets timesteps |
| `extend_experiment()` | Mid-run additions — pushes events into the running loop |
| `finish_experiment()` | Cleanup — shuts down Analyzer, resets state |
| Method | When to use | Returns |
|--------|-------------|---------|
| `run_experiment()` | First acquisition — creates a fresh Analyzer | `RunHandle` (non-blocking) |
| `continue_experiment()` | Subsequent phases — reuses Analyzer, offsets timesteps | `RunHandle` (non-blocking) |
| `extend_experiment()` | Mid-run additions — pushes events into the running loop | — (non-blocking) |
| `finish_experiment()` | Cleanup — shuts down Analyzer, resets state | — (blocks until drained) |

## Simulated Controller

Expand All @@ -287,7 +310,7 @@ It supports both **TIFF** (`raw/`, `ref/` folders) and **OME-Zarr** (`acquisitio
from faro.core.controller import ControllerSimulated

ctrl = ControllerSimulated(mic, pipeline, old_data_project_path="/path/to/old_experiment")
ctrl.run_experiment(events, stim_mode="current")
ctrl.run_experiment(events, stim_mode="current").wait()
```

Use cases:
Expand Down
Loading