This document maps the PycroFlow package as it stands after the
restructuring (Stages 0–5, see /Users/hgrabmayr/.claude/plans/please- deeply-investigate-the-soft-waffle.md for the full plan).
┌──────────────────────┐ ┌──────────────────────┐
│ frontend_cli.py │ │ gui/ (PyQt6) │ Monet tab
│ (pycroflow) │ │ (pycroflow-gui) │ embeds monet
└──────────┬───────────┘ └──────────┬───────────┘
└────────────┬──────────────┘
▼
┌─────────────────────────────┐
│ services/ │ ExperimentService,
│ (frontend-agnostic API) │ SystemService, mm_core
└──────────────┬──────────────┘
▼
┌─────────────────────────────┐
│ orchestration/ │ ProtocolOrchestrator,
│ - SignalRegistry │ AbstractSystem,
│ - per-subsystem handlers │ AbstractSystemHandler
│ - ThreadExchange state │
└─┬───────────┬──────────────┬┘
│ │ │
┌───────────▼──┐ ┌──────▼─────┐ ┌──────▼──────┐
│ fluid/legacy │ │ imaging.py │ │ illumination│
│ │ │ (MM) │ │ .py (monet) │
└──┬───────────┘ └──┬─────────┘ └─────────────┘
│ │
pyHamilton/ pycromanager
(in-house (vendor)
serial driver)
Both frontends sit on the services/ layer. The GUI's Monet tab embeds
monet's MonetMainWindow in-process so the two share one Micro-Manager
Core (see the MM-Core section below and ADR 006).
Every subsystem inherits from orchestration.AbstractSystem and runs in
its own thread via a matching AbstractSystemHandler:
| Handler | System | Backed by |
|---|---|---|
FluidHandler |
LegacyArchitecture |
pyHamilton/ + hamilton_components.py |
ImagingHandler |
ImagingSystem |
pycromanager (Micro-Manager Core / Studio) |
IlluminationHandler |
IlluminationSystem |
monet (sibling repo) |
Handlers are daemon threads sharing a threadexchange dict for locks,
events, and message lists.
Cross-subsystem synchronization uses protocol entries:
- {$type: signal, value: "fluid round 1 done"}
- {$type: wait for signal, target: img, value: "round 1 done"}
- {$type: wait for signal, target: fluid, value: "round 2 done", timeout: 600}signal appends a message to threadexchange[target_name] and fires the
matching SignalRegistry event; wait for signal waits on that event
(Event.wait(timeout)), falling back to a list scan for legacy
substring-prefixed entries. It raises WaitForSignalTimeout if the
deadline expires, or returns silently on abort / protocol-iter change.
The SignalRegistry of threading.Events (Stage 4) replaced the original
busy-poll, and protocol entries are parsed into a pydantic
discriminated-union and dispatched via functools.singledispatch.
| What | Lives at |
|---|---|
| Instrument topology (legacy system) | PycroFlow/configs/legacy_system.yaml |
| Tubing volumes | PycroFlow/configs/legacy_tubing.yaml |
| Protocol wire-format schema | PycroFlow/schemas/protocol_schema.py |
| Demo protocols / REPL examples | PycroFlow/examples/demo_protocols.py |
| Test fixtures (input configs) | PycroFlow/tests/fixtures/configs/*.py |
| Regression snapshots | PycroFlow/tests/fixtures/snapshots/*.json |
| Output protocol files (per run) | <save_dir>/<base_name>_YYMMDD-HHMM.yaml |
hamilton_architecture.py re-exports legacy_system_config and
legacy_tubing_config as module attributes by loading the YAML at import
time, preserving back-compat with existing call sites.
Per-subsystem protocol entries are dicts keyed by $type. The current
catalog (see schemas/protocol_schema.py):
$type |
Subsystem | Required fields |
|---|---|---|
inject |
fluid | reservoir_id, volume |
incubate |
fluid | duration (float-or-str) |
flush |
fluid | flushfactor |
pump_out |
fluid | volume |
await_acquisition |
fluid | — |
signal |
any | value (optional target) |
wait for signal |
any | target, value (optional timeout) |
acquire |
img | frames, t_exp |
power |
illu | value |
set power |
illu | laser, power |
set shutter |
illu | state |
laser enable |
illu | laser, state |
Entries may carry extra fields; the schema uses extra='allow'. The
schema fires at ProtocolBuilder.create_protocol so a typo in $type is
caught at construction, not mid-run.
- Orchestrator owns the threads.
ProtocolOrchestrator.start_orchestrationstarts one handler thread per subsystem; each runs untilabort_flagorgraceful_stop_flagfires. - All handlers are daemons.
Ctrl-Cor interpreter exit reliably terminates them; the previous non-daemon flag could leave zombies. - State lives in
ThreadExchange. A per-instance object (Stage 4, replacing a shared class-level dict) holdingthreading.Lock,threading.Event,queue.Queue, per-subsystem messagelists, and theSignalRegistry. It subclassesdict, sotxchange['fluid_lock']and the typedtxchange.fluid_lockboth work.
PycroFlow's ImagingSystem and a separately-run monet GUI cannot share
Micro-Manager across two processes — the second connection silently breaks
the first. Two mechanisms address this:
- In-process GUI (Stage 5). The
pycroflow-guiQt frontend embeds monet'sMonetMainWindowas a tab and callsservices.mm_core.share_with_monet(), so PycroFlow imaging and monet use one Core inside one process — the conflict is gone structurally. - Lockfile guard (Stage 1). For users still running the CLI alongside a
standalone monet GUI, an
MmCoreLockfilesystem mutex (%LOCALAPPDATA%\PycroFlow\mm.lockon Windows,~/.cache/PycroFlow/mm.lockon POSIX) catches the conflict atImagingSystem.__init__and raisesMmLockHeldwith a clear error.
loguru-based, configured by PycroFlow.setup_logging(). Importing
PycroFlow no longer touches the filesystem (the old import-time call to
rem_old_logfiles() was removed). Frontends decide whether to clean
rotated logs (clean_old=True).
| Stage | Title | Status |
|---|---|---|
| 0 | Safety net (CI, snapshot, packaging) | ✅ shipped |
| 1 | Reliability fixes | ✅ shipped |
| 2 | Decouple config & data from code | ✅ shipped |
| 3 | Break up monoliths + introduce HAL | ✅ shipped |
| 4 | Protocol / signal redesign | ✅ shipped |
| 5 | Qt GUI + embedded monet | ✅ shipped |
| 6 | Packaging & documentation polish | ✅ shipped |
Deferred follow-ups (tracked in module docstrings): full extraction of
fluid/calibration.py / fluid/protocol_exec.py and the per-experiment
protocols/*.py step builders from their host classes; migrating the
remaining frontend_cli commands onto services/; harmonizing
create_steps_flushtest to the nested config schema.