Skip to content

useq-schema

useq-schema (imported as useq) is an implementation-agnostic schema for describing a sequence of events during a multi-dimensional imaging acquisition: the time points, stage positions, channels, z-planes, etc... that you want to acquire. It describes what to do, and leaves it to an acquisition engine (such as the one in pymmcore-plus) to actually do it.

The goal of useq-schema is to provide a specification (and some python utilities) for generating event objects that can be consumed by microscope acquisition engines. A hope is that this will encourage inter-operability between various efforts to drive automated image acquisition.

The schema tries to remain agnostic to the specific acquisition engine (though it was designed based on the capabilities of Micro-Manager). We welcome feedback from interested parties regarding limitations and/or potential extensions to the schema! Similarly, while the "ideal" schema will support arbitrary dimensions (i.e. more than the conventional position, time, channel, z, ...), it is also hard to avoid hard-coding some assumptions about dimensionality in certain places. Any and all feedback (even minor stuff, such as parameter naming, etc...) is welcome! Please open an issue.

A quick example

from useq import MDASequence

mda_seq = MDASequence(
    stage_positions=[(100, 100, 30), (200, 150, 35)],
    channels=["DAPI", "FITC"],
    time_plan={"interval": 1, "loops": 20},
    z_plan={"range": 4, "step": 0.5},
    axis_order="tpcz",
)

The MDASequence object is iterable, yielding MDAEvent objects in the order specified by the axis_order attribute:

>>> mda_seq.sizes
{'t': 20, 'p': 2, 'c': 2, 'z': 9}
>>> events = list(mda_seq)
>>> len(events)
720
>>> events[1]
MDAEvent(index={'t': 0, 'p': 0, 'c': 0, 'z': 1}, channel=Channel(config='DAPI'), min_start_time=0.0, x_pos=100.0, y_pos=100.0, z_pos=28.5)

Building Sequences walks through the common cases (time series, z-stacks, grids, skipping channels, etc...).

Core schema

MDAEvent

The primary "event" object is useq.MDAEvent. This describes a single event that a microscope should perform, including preparation of the hardware, and execution of the event (such as an image acquisition).

MDASequence

useq.MDASequence represents a sequence of events, as might be generated by the multidimensional acquisition GUI in most microscope software. The Python MDASequence object is itself iterable, and yields useq.MDAEvent objects.

Executing an MDASequence

This library is just a schema, and does not provide any built-in functionality for executing an MDASequence. However, pymmcore-plus implements an acquisition engine that can execute an MDASequence object (or any iterable of MDAEvent objects) with Micro-Manager (via the pymmcore python wrapper around the C++ MMCore). The MDAWidget in pymmcore-widgets, pymmcore-gui and napari-micromanager all build a useq.MDASequence from user input and hand it to pymmcore-plus to run.

Built an engine that runs an MDASequence (or know of a similar schema in other software)? Open an issue and we'll list it here.

Serialization and deserialization

MDASequence and MDAEvent objects are designed to be serialized and deserialized, allowing you to define an entire multi-dimensional acquisition in a human-readable YAML (or JSON) file, and then load that file into your acquisition engine.

For example, the following file defines an experiment with:

  • 3 channels (Cy5, FITC, and DAPI), specifying exposure times in ms for Cy5 and FITC, and acquiring DAPI (without a z-stack) only every third timepoint
  • a two-phase time-lapse: 3 frames, 3 seconds apart, followed by a frame every 10 seconds for 40 minutes
  • a Z-stack at each timepoint, with a range of 4 microns and a step size of 0.5 micron
  • two stage positions, specifying a unique Z-stack for the second position

Example

my_experiment.yaml
axis_order: tpcz
channels:
  - config: Cy5
    exposure: 50.0
  - config: FITC
    exposure: 100.0
  - config: DAPI
    acquire_every: 3
    do_stack: false
time_plan:
  phases:
    - interval: 0:00:03
      loops: 3
    - duration: 0:40:00
      interval: 0:00:10
z_plan:
  range: 4.0
  step: 0.5
stage_positions:
  - x: 10.0
    y: 20.0
    z: 50.0
  - name: Cell 1
    x: 110.0
    y: 220.0
    z: 50.0
    sequence:
      z_plan:
        above: 10.0
        below: 0.0
        step: 1.0
metadata:
  some info: about my experiment
my_experiment.json
{
  "axis_order": "tpcz",
  "channels": [
    {
      "config": "Cy5",
      "exposure": 50.0
    },
    {
      "config": "FITC",
      "exposure": 100.0
    },
    {
      "config": "DAPI",
      "acquire_every": 3,
      "do_stack": false
    }
  ],
  "time_plan": {
    "phases": [
      {
        "interval": "0:00:03",
        "loops": 3
      },
      {
        "duration": "0:40:00",
        "interval": "0:00:10"
      }
    ]
  },
  "z_plan": {
    "range": 4.0,
    "step": 0.5
  },
  "stage_positions": [
    {
      "x": 10.0,
      "y": 20.0,
      "z": 50.0
    },
    {
      "name": "Cell 1",
      "x": 110.0,
      "y": 220.0,
      "z": 50.0,
      "sequence": {
        "z_plan": {
          "above": 10.0,
          "below": 0.0,
          "step": 1.0
        }
      }
    }
  ],
  "metadata": {
    "some info": "about my experiment"
  }
}

Either file can be loaded with:

import useq

seq = useq.MDASequence.from_file("my_experiment.yaml")

... and seq.yaml() or seq.model_dump_json(exclude_unset=True) go the other way (see saving and loading sequences).

Unknown keys are ignored

Keys that aren't part of the schema are silently dropped when a file is loaded, so a misspelled or misplaced key (for example a z_plan placed directly on a stage position, rather than inside its sequence) won't raise an error. It's worth checking the loaded sequence (list(seq), or seq.sizes) before running it.

What about useq.v2?

useq.v2 is a preview of the next version of the API (with user-defined axes, among other things). useq.MDASequence, as described on these pages, is unchanged, and nothing needs to migrate yet. See useq-schema v2 (preview) if you're curious.

Next steps