-
Notifications
You must be signed in to change notification settings - Fork 181
Add BD FACSMelody cell sorter support #1156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
di-omics
wants to merge
2
commits into
PyLabRobot:main
Choose a base branch
from
di-omics:facsmelody-sorter
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| --- | ||
| orphan: true | ||
| --- | ||
|
|
||
| # Reverse-engineering the BD FACSMelody | ||
|
|
||
| ## Why this is worth doing | ||
|
|
||
| Sorting is the one step in plate-based single-cell sequencing that usually has to | ||
| happen off-automation. A liquid handler can build libraries end to end, but the | ||
| sort itself, one gated cell per well or a targeted, enriched population, is | ||
| typically a manual hand-off. Bring the sorter under PyLabRobot and that gap closes: | ||
| the deck stages the plate, calls `sort_to_plate`, and finishes the prep. Walkaway | ||
| library prep becomes possible. | ||
|
|
||
| The same control also unlocks sorting for a target population. Enrich a cell type, | ||
| a marker-positive subset, or live singlets before the assay, and every downstream | ||
| measurement inherits that resolution. For cell-type-specific epigenomics this is | ||
| the whole point: you want to read regulatory state in the exact cell type a | ||
| variant acts in, not in a bulk average that blurs it away. | ||
|
|
||
| The BD FACSMelody is a closed instrument driven by BD FACSChorus, so getting there | ||
| means reverse-engineering its control link. This document is the playbook. | ||
|
|
||
| ## The method (credit where it is due) | ||
|
|
||
| This follows the methodology **Rick Wierenga** used to build PyLabRobot's device | ||
| backends: do not invent a protocol, work down to the OEM's own command layer, | ||
| sniff the OEM-to-device traffic, correlate each UI action to its bytes, decode the | ||
| framing, and replay over PyUSB or pyserial before wrapping it in a backend. The | ||
| same approach produced PLR's Hamilton STAR and Tecan EVO backends from captured | ||
| traffic and firmware command strings. | ||
|
|
||
| The steps below are numbered so the path is reproducible and so a reviewer can see | ||
| exactly where the safety checks sit: the backend refuses to drive hardware until | ||
| every required command is decoded, and it refuses to actuate without a human in | ||
| the loop. | ||
|
|
||
| The reverse-engineering toolkit that produces the deliverable lives in a separate | ||
| repository ([plr-clarity](https://github.com/di-omics/plr-clarity)) and is not | ||
| vendored into PyLabRobot. Only the consumer side ships here: the backend loads the | ||
| toolkit's output, a `ProtocolMap`, and turns it into `CellSorter` operations. | ||
|
|
||
| ## The deliverable: a ProtocolMap | ||
|
|
||
| A `ProtocolMap` is a JSON file mapping each logical command (`start_sort`, | ||
| `clean`, and so on) to the exact bytes that drive it, plus how each frame is framed | ||
| and checksummed. The minimum set a sort-to-plate run needs is fixed: | ||
|
|
||
| `connect`, `get_status`, `load_template`, `set_deposition`, `prime`, `start_sort`, | ||
| `wait_complete`, `abort`, `clean`. | ||
|
|
||
| Combinatorial indexing tolerates tens to a hundred cells per well and does not need | ||
| index-sorting (recording which cell landed where). So the target is small: trigger | ||
| a saved gate template, configure count-controlled deposition, and poll status. We | ||
| mostly reverse-engineer how to trigger a saved template and move plates, not BD's | ||
| gating math. | ||
|
|
||
| ## Step 1: map the OEM stack and transport | ||
|
|
||
| FACSChorus talks to the Melody over USB and/or an Ethernet cart link. Enumerate | ||
| everything, unplug and replug the instrument, and diff the enumeration to isolate | ||
| its link. Record the endpoint (for example `usb:0x1fbd:0x0002`, `COM4`, or | ||
| `10.0.0.5:9100`) and which transport it uses. | ||
|
|
||
| The highest-leverage move is often not the wire but FACSChorus itself: its logs, | ||
| a local control daemon, and the experiment database. If Chorus logs the command | ||
| bytes or exposes a localhost service, you may not need to sniff USB at all. | ||
|
|
||
| ## Step 2: capture traffic while performing labeled UI actions | ||
|
|
||
| Start your platform sniffer on the Melody's interface, then drive Chorus by hand. | ||
|
|
||
| - Windows: Wireshark with USBPcap on the USB interface, exported to `.pcapng`. | ||
| - Linux: `usbmon` or Wireshark. | ||
| - Serial: a COM sniffer saved as `<timestamp> <direction> <hexbytes>` lines. | ||
|
|
||
| The core trick: perform one discrete Chorus action, mark the instant, and look | ||
| only at the bytes inside that window. Cover every required command (click "Start | ||
| Sort" and mark `start_sort`, click "Abort" and mark `abort`, and so on). | ||
|
|
||
| ## Step 3: correlate action to bytes, decode the framing | ||
|
|
||
| For each labeled window, isolate the frames unique to that action by diffing across | ||
| windows to cancel the periodic keep-alive and status chatter. Then work out the | ||
| structure: | ||
|
|
||
| - framing: fixed length, terminator bytes, or length-prefixed. | ||
| - opcode: the longest common prefix across a command's frames. | ||
| - parameters: the bytes that change when you vary one setting (cells per well, | ||
| well count). Find their encoders by changing one thing in Chorus and diffing. | ||
| - checksum: brute-force the common schemes (sum8, xor8, CRC16 variants) over | ||
| candidate byte ranges. | ||
|
|
||
| Each hypothesis carries its evidence (the capture frames it came from) so a human | ||
| confirms it before anything is replayed. | ||
|
|
||
| ## Step 4: build the ProtocolMap with coverage tracking | ||
|
|
||
| Fold the decoded commands into a `ProtocolMap` seeded with the required set, so the | ||
| result reports exactly which required commands are decoded and which are still | ||
| missing. Coverage is the gate: the backend will not open a live link until it is | ||
| complete. | ||
|
|
||
| ## Step 5: guarded replay | ||
|
|
||
| The Melody is a laser plus pressurized fluidics, so replay is conservative by | ||
| default, with two independent safety switches, both off by default: | ||
|
|
||
| - `armed` opens the link and permits transmission. Without it, everything is a | ||
| dry-run that logs the exact bytes and sends nothing. | ||
| - Commands that move fluid, open a nozzle, or fire a sort additionally require an | ||
| actuation opt-in (`allow_actuation`) and a human present. | ||
|
|
||
| Confirm the read-only command first. Only once `get_status` round-trips cleanly do | ||
| you touch actuating commands, with BD service or your safety officer in the loop. | ||
| The backend refuses actuating commands without the opt-in and refuses any live run | ||
| against an incomplete map, so a half-mapped protocol can never drive the hardware. | ||
|
|
||
| ## Step 6: validate against the instrument | ||
|
|
||
| Run a real sort-to-plate and confirm the deposition. This is the step that moves | ||
| the backend's label from "not yet hardware-validated" to "hardware-validated". The | ||
| backend reports which state it is in and does not claim a sort it has not run. | ||
|
|
||
| ## Physical bridge (still required) | ||
|
|
||
| Software control is not full autonomy. You still need to move a sample tube onto | ||
| the SIP and the plate on and off the deposition stage (a bench cobot or a shared | ||
| plate hotel), and to expose the sorter's clog and error status as an interlock. A | ||
| clogged nozzle silently ruins a plate, so surface it. | ||
|
|
||
| ## Decision gate | ||
|
|
||
| Reverse-engineering a closed sorter is real work. If the Melody is not a fixed | ||
| constraint, an API-controllable single-cell dispenser (cellenONE, WOLF G2, | ||
| Namocell Hana) does the same job, doublet exclusion plus count-controlled plate | ||
| deposition, with a documented interface and none of this RE. The orchestrator does | ||
| not change: the sorter becomes a different backend behind the same `sort_to_plate` | ||
| call. Decide the go or no-go after Step 1, once you know how open Chorus really is. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| # Cell sorting (FACS) | ||
|
|
||
| `CellSorter` controls fluorescence-activated cell sorters that deposit gated | ||
| events into the wells of a plate. | ||
|
|
||
| ## Why this capability exists | ||
|
|
||
| Sorting is the one step in plate-based single-cell sequencing that usually has to | ||
| happen off-automation. A liquid handler can build libraries end to end, but the | ||
| sort itself, one gated cell per well or a targeted, enriched population, is | ||
| typically a manual hand-off. Modeling the sorter as a capability closes that gap: a | ||
| deck stages the plate, calls `sort_to_plate`, and finishes the prep, so walkaway | ||
| library prep becomes possible. | ||
|
|
||
| It also lets a workflow sort for a target population, enriching a cell type, a | ||
| marker-positive subset, or live singlets before the assay. That is what gives | ||
| cell-type resolution to downstream measurements such as cell-type-specific | ||
| epigenomics, where the goal is to read regulatory state in the exact cell type a | ||
| variant acts in rather than in a bulk average. | ||
|
|
||
| ## Interface | ||
|
|
||
| The frontend is hardware-agnostic. The primitive backend operations are: | ||
|
|
||
| - `get_status()` -- a coarse instrument state (`idle`, `running`, `error`). | ||
| - `load_template(name)` -- select a pre-built sort template (gate hierarchy). | ||
| - `set_deposition(cells_per_well, plate_format)` -- configure the deposition target. | ||
| - `prime()` -- prime fluidics and stabilize the stream. | ||
| - `start_sort(wells)` -- begin depositing into the staged plate. | ||
| - `wait_for_completion(poll_interval, timeout)` -- block until the sort finishes. | ||
| - `abort()` -- stop the current sort immediately. | ||
| - `clean()` -- run the clean or flush cycle between samples. | ||
|
|
||
| The frontend adds validation and the `sort_to_plate` convenience method, which | ||
| sequences load, deposition, prime, sort, wait, and clean in one call. | ||
|
|
||
| The interface is intentionally small and event-oriented so it can sit next to a | ||
| future acquisition/cytometry capability that reuses the same fluidics and gate | ||
| template concepts, letting a single instrument expose both without duplicating an | ||
| interface. | ||
|
|
||
| ## Device-free testing | ||
|
|
||
| `CellSorterChatterboxBackend` implements the interface with logging and no I/O, so | ||
| you can build and test a workflow without an instrument: | ||
|
|
||
| ```python | ||
| from pylabrobot.capabilities.cell_sorting import CellSorter, CellSorterChatterboxBackend | ||
|
|
||
| sorter = CellSorter(backend=CellSorterChatterboxBackend()) | ||
| await sorter._on_setup() | ||
| await sorter.sort_to_plate(cells_per_well=1, wells=96, template="singlet_deposit") | ||
| ``` | ||
|
|
||
| ## Supported hardware | ||
|
|
||
| - BD FACSMelody (`pylabrobot.becton_dickinson.facsmelody`). This backend replays a | ||
| decoded `ProtocolMap` and is not yet hardware-validated; it runs end to end dry | ||
| and refuses to open a live link until a complete map is supplied. See | ||
| [Reverse-engineering the BD FACSMelody](../../facsmelody-re.md) for how the map | ||
| is produced and the safety model that governs a live sort. |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| from .backend import FACSMelodyCellSorterBackend, FACSMelodyDriver | ||
| from .chatterbox import FACSMelodyChatterbox | ||
| from .constants import DEFAULT_SORT_TEMPLATE, REQUIRED_COMMANDS, Transport | ||
| from .errors import ( | ||
| FACSMelodyError, | ||
| ProtocolMapIncompleteError, | ||
| SortActuationError, | ||
| SortNotReadyError, | ||
| SortTimeoutError, | ||
| ) | ||
| from .facsmelody import FACSMelody | ||
| from .protocol_map import Command, ProtocolMap, seed_required |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you share the protocol map if you still have it? it is needed in PLR to make it usable