Building Sequences¶
For most standard multi-dimensional experiments, you will want to use
useq.MDASequence to construct your sequence of events. It allows you to
declare a "plan" for each axis in your experiment (channels, time, z, etc...)
along with the order in which the axes should be iterated. Iterating over the
sequence yields useq.MDAEvent objects (usually one per image), so
list(sequence) shows you which events an acquisition engine will be asked to
run.
A sequence is just data, so you can build and inspect the examples on this page without a microscope. To run a sequence, see The Acquisition Engine.
Axes and plans¶
Each axis has a field on MDASequence, and a one-letter key that is used in
axis_order and in the index of each event:
| key | field | describes | reference |
|---|---|---|---|
t |
time_plan |
time points (interval, duration, number of loops, or several phases) | time plans |
p |
stage_positions |
a list of stage positions (or a well plate) | Position, well plates |
g |
grid_plan |
a grid (or random points) at each position | grid plans |
c |
channels |
a list of channel presets, each with an optional exposure | Channel |
z |
z_plan |
a z-stack | z plans |
axis_order (default "tpgcz") says how the axes are nested (the "fastest"
axes come last). There are also a few fields that aren't axes, such as
autofocus_plan (hardware autofocus),
keep_shutter_open_across, setup, and metadata. See the
MDASequence reference for all of them.
Let's look at how MDASequence can be used to create a few common experiments.
A two-channel time series¶
from useq import MDASequence
mda_sequence = MDASequence(
time_plan={"interval": 2, "loops": 6}, # (1)!
channels=[
{"config": "DAPI", "exposure": 50},
{"config": "FITC", "exposure": 80},
],
)
- 6 time points, 2 seconds apart. See also, additional time plans.
output of list(mda_sequence)
min_start_time is the time (in seconds) at which the event should start.
reset_event_timer=True on the first event tells the engine to (re)start the
clock that min_start_time is measured from.
[
MDAEvent(
index={"t": 0, "c": 0},
channel=Channel(config="DAPI"),
exposure=50.0,
min_start_time=0.0,
reset_event_timer=True,
),
MDAEvent(
index={"t": 0, "c": 1},
channel=Channel(config="FITC"),
exposure=80.0,
min_start_time=0.0,
),
MDAEvent(
index={"t": 1, "c": 0},
channel=Channel(config="DAPI"),
exposure=50.0,
min_start_time=2.0,
),
MDAEvent(
index={"t": 1, "c": 1},
channel=Channel(config="FITC"),
exposure=80.0,
min_start_time=2.0,
),
MDAEvent(
index={"t": 2, "c": 0},
channel=Channel(config="DAPI"),
exposure=50.0,
min_start_time=4.0,
),
MDAEvent(
index={"t": 2, "c": 1},
channel=Channel(config="FITC"),
exposure=80.0,
min_start_time=4.0,
),
MDAEvent(
index={"t": 3, "c": 0},
channel=Channel(config="DAPI"),
exposure=50.0,
min_start_time=6.0,
),
MDAEvent(
index={"t": 3, "c": 1},
channel=Channel(config="FITC"),
exposure=80.0,
min_start_time=6.0,
),
MDAEvent(
index={"t": 4, "c": 0},
channel=Channel(config="DAPI"),
exposure=50.0,
min_start_time=8.0,
),
MDAEvent(
index={"t": 4, "c": 1},
channel=Channel(config="FITC"),
exposure=80.0,
min_start_time=8.0,
),
MDAEvent(
index={"t": 5, "c": 0},
channel=Channel(config="DAPI"),
exposure=50.0,
min_start_time=10.0,
),
MDAEvent(
index={"t": 5, "c": 1},
channel=Channel(config="FITC"),
exposure=80.0,
min_start_time=10.0,
),
]
A z-stack at three positions¶
from useq import MDASequence, Position
mda_sequence = MDASequence(
z_plan={"range": 4, "step": 0.5}, # (1)!
stage_positions=[ # (2)!
(10, 10, 20),
{"x": 30, "y": 40, "z": 50},
Position(x=60, y=70, z=80),
],
)
- A 4-micron z-stack with 0.5 micron steps, centered on each position's
z. See also, additional z plans. - These are all valid ways to specify a stage position.
None of
x,y, orzare required.
output of list(mda_sequence)
[
MDAEvent(index={"p": 0, "z": 0}, x_pos=10.0, y_pos=10.0, z_pos=18.0),
MDAEvent(index={"p": 0, "z": 1}, x_pos=10.0, y_pos=10.0, z_pos=18.5),
MDAEvent(index={"p": 0, "z": 2}, x_pos=10.0, y_pos=10.0, z_pos=19.0),
MDAEvent(index={"p": 0, "z": 3}, x_pos=10.0, y_pos=10.0, z_pos=19.5),
MDAEvent(index={"p": 0, "z": 4}, x_pos=10.0, y_pos=10.0, z_pos=20.0),
MDAEvent(index={"p": 0, "z": 5}, x_pos=10.0, y_pos=10.0, z_pos=20.5),
MDAEvent(index={"p": 0, "z": 6}, x_pos=10.0, y_pos=10.0, z_pos=21.0),
MDAEvent(index={"p": 0, "z": 7}, x_pos=10.0, y_pos=10.0, z_pos=21.5),
MDAEvent(index={"p": 0, "z": 8}, x_pos=10.0, y_pos=10.0, z_pos=22.0),
MDAEvent(index={"p": 1, "z": 0}, x_pos=30.0, y_pos=40.0, z_pos=48.0),
MDAEvent(index={"p": 1, "z": 1}, x_pos=30.0, y_pos=40.0, z_pos=48.5),
MDAEvent(index={"p": 1, "z": 2}, x_pos=30.0, y_pos=40.0, z_pos=49.0),
MDAEvent(index={"p": 1, "z": 3}, x_pos=30.0, y_pos=40.0, z_pos=49.5),
MDAEvent(index={"p": 1, "z": 4}, x_pos=30.0, y_pos=40.0, z_pos=50.0),
MDAEvent(index={"p": 1, "z": 5}, x_pos=30.0, y_pos=40.0, z_pos=50.5),
MDAEvent(index={"p": 1, "z": 6}, x_pos=30.0, y_pos=40.0, z_pos=51.0),
MDAEvent(index={"p": 1, "z": 7}, x_pos=30.0, y_pos=40.0, z_pos=51.5),
MDAEvent(index={"p": 1, "z": 8}, x_pos=30.0, y_pos=40.0, z_pos=52.0),
MDAEvent(index={"p": 2, "z": 0}, x_pos=60.0, y_pos=70.0, z_pos=78.0),
MDAEvent(index={"p": 2, "z": 1}, x_pos=60.0, y_pos=70.0, z_pos=78.5),
MDAEvent(index={"p": 2, "z": 2}, x_pos=60.0, y_pos=70.0, z_pos=79.0),
MDAEvent(index={"p": 2, "z": 3}, x_pos=60.0, y_pos=70.0, z_pos=79.5),
MDAEvent(index={"p": 2, "z": 4}, x_pos=60.0, y_pos=70.0, z_pos=80.0),
MDAEvent(index={"p": 2, "z": 5}, x_pos=60.0, y_pos=70.0, z_pos=80.5),
MDAEvent(index={"p": 2, "z": 6}, x_pos=60.0, y_pos=70.0, z_pos=81.0),
MDAEvent(index={"p": 2, "z": 7}, x_pos=60.0, y_pos=70.0, z_pos=81.5),
MDAEvent(index={"p": 2, "z": 8}, x_pos=60.0, y_pos=70.0, z_pos=82.0),
]
Relative to what?
Most z plans (range, above/below, relative positions) and grid plans
(rows/columns, width/height, random points) are relative: they are
added to the
z (or x/y) of each stage position. If a position doesn't specify z, or
there are no stage_positions at all, a relative z plan is centered on
z = 0, not on the current focus position:
(A relative grid with no x/y is likewise centered on x = y = 0.) On a real
microscope, that may send the objective somewhere you didn't expect. So when
you want a z-stack "around where I am now", put the current position in the
sequence, e.g. with pymmcore-plus:
(Absolute plans, like ZTopBottom or ZAbsolutePositions, aren't affected.)
A grid of z-stacks¶
Here we use axis_order to declare that we want the full z-stack
to happen at each (row, col) before moving to the next
position in the grid.
from useq import MDASequence
mda_sequence = MDASequence(
stage_positions=[{"x": 100, "y": 200, "z": 300}],
grid_plan={"fov_width": 20, "fov_height": 10, "rows": 2, "columns": 2},
z_plan={"range": 10, "step": 2.5},
axis_order="pgz", # (1)!
)
- The "fastest" axes come last. By putting
zaftergin theaxis_order, we're saying "at eachg, do a fullziteration".
output of list(mda_sequence)
[
MDAEvent(index={"p": 0, "g": 0, "z": 0}, x_pos=90.0, y_pos=205.0, z_pos=295.0),
MDAEvent(index={"p": 0, "g": 0, "z": 1}, x_pos=90.0, y_pos=205.0, z_pos=297.5),
MDAEvent(index={"p": 0, "g": 0, "z": 2}, x_pos=90.0, y_pos=205.0, z_pos=300.0),
MDAEvent(index={"p": 0, "g": 0, "z": 3}, x_pos=90.0, y_pos=205.0, z_pos=302.5),
MDAEvent(index={"p": 0, "g": 0, "z": 4}, x_pos=90.0, y_pos=205.0, z_pos=305.0),
MDAEvent(index={"p": 0, "g": 1, "z": 0}, x_pos=110.0, y_pos=205.0, z_pos=295.0),
MDAEvent(index={"p": 0, "g": 1, "z": 1}, x_pos=110.0, y_pos=205.0, z_pos=297.5),
MDAEvent(index={"p": 0, "g": 1, "z": 2}, x_pos=110.0, y_pos=205.0, z_pos=300.0),
MDAEvent(index={"p": 0, "g": 1, "z": 3}, x_pos=110.0, y_pos=205.0, z_pos=302.5),
MDAEvent(index={"p": 0, "g": 1, "z": 4}, x_pos=110.0, y_pos=205.0, z_pos=305.0),
MDAEvent(index={"p": 0, "g": 2, "z": 0}, x_pos=110.0, y_pos=195.0, z_pos=295.0),
MDAEvent(index={"p": 0, "g": 2, "z": 1}, x_pos=110.0, y_pos=195.0, z_pos=297.5),
MDAEvent(index={"p": 0, "g": 2, "z": 2}, x_pos=110.0, y_pos=195.0, z_pos=300.0),
MDAEvent(index={"p": 0, "g": 2, "z": 3}, x_pos=110.0, y_pos=195.0, z_pos=302.5),
MDAEvent(index={"p": 0, "g": 2, "z": 4}, x_pos=110.0, y_pos=195.0, z_pos=305.0),
MDAEvent(index={"p": 0, "g": 3, "z": 0}, x_pos=90.0, y_pos=195.0, z_pos=295.0),
MDAEvent(index={"p": 0, "g": 3, "z": 1}, x_pos=90.0, y_pos=195.0, z_pos=297.5),
MDAEvent(index={"p": 0, "g": 3, "z": 2}, x_pos=90.0, y_pos=195.0, z_pos=300.0),
MDAEvent(index={"p": 0, "g": 3, "z": 3}, x_pos=90.0, y_pos=195.0, z_pos=302.5),
MDAEvent(index={"p": 0, "g": 3, "z": 4}, x_pos=90.0, y_pos=195.0, z_pos=305.0),
]
Tip
You can usually leave out fov_width and fov_height. pymmcore-plus fills
them in from the camera ROI and pixel size when the acquisition starts.
Skipping timepoints or z-stacks¶
The Channel field has a few tricks, such as skipping timepoints
or z-stacks for specific channels. Here we take a fast z-stack (leaving the
shutter open) in the FITC channel only, and a single image in the DIC
channel every 3 timepoints:
from useq import MDASequence
mda_sequence = MDASequence(
z_plan={"range": 10, "step": 2.5}, # (1)!
time_plan={"interval": 2, "loops": 6},
channels=[
"FITC",
{"config": "DIC", "acquire_every": 3, "do_stack": False},
],
keep_shutter_open_across=["z"],
)
- There are no stage positions here, so the stack is centered on z = 0 (see above).
output of list(mda_sequence) (first two timepoints)
[
MDAEvent(
index={"t": 0, "c": 0, "z": 0},
channel=Channel(config="FITC"),
min_start_time=0.0,
z_pos=-5.0,
keep_shutter_open=True,
reset_event_timer=True,
),
MDAEvent(
index={"t": 0, "c": 0, "z": 1},
channel=Channel(config="FITC"),
min_start_time=0.0,
z_pos=-2.5,
keep_shutter_open=True,
),
MDAEvent(
index={"t": 0, "c": 0, "z": 2},
channel=Channel(config="FITC"),
min_start_time=0.0,
z_pos=0.0,
keep_shutter_open=True,
),
MDAEvent(
index={"t": 0, "c": 0, "z": 3},
channel=Channel(config="FITC"),
min_start_time=0.0,
z_pos=2.5,
keep_shutter_open=True,
),
MDAEvent(
index={"t": 0, "c": 0, "z": 4},
channel=Channel(config="FITC"),
min_start_time=0.0,
z_pos=5.0,
),
MDAEvent(
index={"t": 0, "c": 1, "z": 2},
channel=Channel(config="DIC"),
min_start_time=0.0,
z_pos=0.0,
),
MDAEvent(
index={"t": 1, "c": 0, "z": 0},
channel=Channel(config="FITC"),
min_start_time=2.0,
z_pos=-5.0,
keep_shutter_open=True,
),
MDAEvent(
index={"t": 1, "c": 0, "z": 1},
channel=Channel(config="FITC"),
min_start_time=2.0,
z_pos=-2.5,
keep_shutter_open=True,
),
MDAEvent(
index={"t": 1, "c": 0, "z": 2},
channel=Channel(config="FITC"),
min_start_time=2.0,
z_pos=0.0,
keep_shutter_open=True,
),
MDAEvent(
index={"t": 1, "c": 0, "z": 3},
channel=Channel(config="FITC"),
min_start_time=2.0,
z_pos=2.5,
keep_shutter_open=True,
),
MDAEvent(
index={"t": 1, "c": 0, "z": 4},
channel=Channel(config="FITC"),
min_start_time=2.0,
z_pos=5.0,
),
# ... t=2 is like t=1, then t=3 has DIC again (like t=0), etc...
]
Different plans at different positions¶
A stage position may carry its own sequence, whose plans replace the
global ones at that position (any plan it doesn't set comes from the main
sequence). Here, the second position gets a different z-stack:
from useq import MDASequence
mda_sequence = MDASequence(
z_plan={"range": 4, "step": 1},
stage_positions=[
{"x": 0, "y": 0, "z": 10},
{
"name": "Cell 1",
"x": 100,
"y": 50,
"z": 12,
"sequence": {"z_plan": {"above": 2, "below": 0, "step": 0.5}},
},
],
)
for event in mda_sequence:
print(event.index, event.pos_name, event.z_pos)
output
Note
pymmcore-plus can't yet save a sequence like this one as a multi-dimensional
array (the positions don't share one shape), so it writes the frames as a flat
stack.
A sub-sequence that only adds a grid_plan is fine.
A note on syntax¶
If you prefer, you can use useq objects rather than dicts
for all of these fields. This has the advantage of providing
type-checking and auto-completion in your IDE.
Example
The following two sequences are equivalent:
import useq
mda_sequence1 = useq.MDASequence(
time_plan={"interval": 2, "loops": 10},
z_plan={"range": 4, "step": 0.5},
channels=[
{"config": "DAPI", "exposure": 50},
{"config": "FITC", "exposure": 80},
],
)
mda_sequence2 = useq.MDASequence(
time_plan=useq.TIntervalLoops(interval=2, loops=10),
z_plan=useq.ZRangeAround(range=4, step=0.5),
channels=[
useq.Channel(config="DAPI", exposure=50),
useq.Channel(config="FITC", exposure=80),
],
)
assert mda_sequence1 == mda_sequence2
Saving and loading sequences¶
MDASequence objects can be serialized and deserialized to and
from JSON or YAML, making it easy to save and load them from
file:
from pathlib import Path
import useq
mda_sequence = useq.MDASequence(
stage_positions=[(100, 100, 30)],
time_plan={"interval": 2, "loops": 10},
z_plan={"range": 4, "step": 0.5},
channels=[
{"config": "DAPI", "exposure": 50, "do_stack": False},
{"config": "FITC", "exposure": 20},
],
axis_order="tpcz",
)
Path("mda_sequence.yaml").write_text(mda_sequence.yaml())
... results in:
axis_order:
- t
- p
- c
- z
channels:
- config: DAPI
do_stack: false
exposure: 50.0
- config: FITC
exposure: 20.0
stage_positions:
- x: 100.0
y: 100.0
z: 30.0
time_plan:
interval: 2.0
loops: 10
z_plan:
range: 4.0
step: 0.5
... which can be loaded back into an MDASequence object:
For JSON, use mda_sequence.model_dump_json(exclude_unset=True) to write and the
same from_file (with a .json extension) to read. Only the fields you set are
written, so the files stay short and readable. The overview
has a larger example file, with multi-phase time plans and per-position z-stacks.
Next steps¶
Once you have a sequence, you'll probably want to run it:
- The Acquisition Engine runs an
MDASequence(or any iterable ofMDAEvent) on a Micro-Manager-controlled microscope, and Saving Data shows how to write the images to OME-Zarr or OME-TIFF. - The
MDASequence,MDAEventand axes reference pages describe every field.