# 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](<https://pymmcore-plus.github.io/pymmcore-plus/guides/mda_engine/index.md>)) 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](<https://micro-manager.org>)). 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](<https://github.com/pymmcore-plus/useq-schema/issues/new>).

## A quick example

```python
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:

```python
>>> 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](<https://pymmcore-plus.github.io/useq-schema/guides/sequences/index.md>) 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).

- For [Micro-Manager](<https://github.com/micro-manager/micro-manager>), this object is most similar (though not identical) to the events generated by [`generate-acq-sequence`](<https://github.com/micro-manager/micro-manager/blob/2b0f51a2f916112d39c6135ad35a112065f8d58d/acqEngine/src/main/clj/org/micromanager/sequence_generator.clj#L410>) in the clojure acquisition engine that drives Micro-Manager's multi-dimensional acquisitions.
- For [Pycro-Manager](<https://github.com/micro-manager/pycro-manager>), this object is similar to an individual [acquisition event `dict`](<https://pycro-manager.readthedocs.io/en/latest/apis.html#acquisition-event-specification>) generated by [`multi_d_acquisition_events`](<https://github.com/micro-manager/pycro-manager/blob/63cf209a8907fd23932ee9f8016cb6a2b61b45aa/pycromanager/acquire.py#L605>), (and, `useq.pycromanager` provides a `to_pycromanager` function that returns a dict following the pycro-manager event spec)

### `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](<https://pymmcore-plus.github.io/useq-schema/schema/sequence/#how-sequences-are-iterated>), and yields `useq.MDAEvent` objects.

- For [Micro-Manager](<https://github.com/micro-manager/micro-manager>), this object is most similar to [`org.micromanager.acquisition.SequenceSettings`](<https://github.com/micro-manager/micro-manager/blob/2b0f51a2f916112d39c6135ad35a112065f8d58d/mmstudio/src/main/java/org/micromanager/acquisition/SequenceSettings.java#L39>), (generated by clicking the `Acquire!` button in the [Multi-D Acquisition GUI](<https://micro-manager.org/Version_2.0_Users_Guide#multi-dimensional-acquisition>))
- For [Pycro-Manager](<https://github.com/micro-manager/pycro-manager>), this object is similar to the [`multi_d_acquisition_events`](<https://github.com/micro-manager/pycro-manager/blob/63cf209a8907fd23932ee9f8016cb6a2b61b45aa/pycromanager/acquire.py#L605>) convenience function, (and `useq.pycromanager` provides a `to_pycromanager` function that returns a list of pycro-manager event dicts)

## Executing an MDASequence

This library is just a schema, and does not provide any built-in functionality for executing an `MDASequence`. However, [pymmcore-plus](<https://pymmcore-plus.github.io/pymmcore-plus/guides/mda_engine/index.md>) implements an acquisition engine that can execute an `MDASequence` object (or any iterable of `MDAEvent` objects) with [Micro-Manager](<https://micro-manager.org>) (via the [`pymmcore`](<https://github.com/micro-manager/pymmcore>) python wrapper around the C++ MMCore). The `MDAWidget` in pymmcore-widgets, [pymmcore-gui](<https://pymmcore-plus.github.io/pymmcore-gui/index.md>) and [napari-micromanager](<https://pymmcore-plus.github.io/napari-micromanager/index.md>) 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](<https://github.com/pymmcore-plus/useq-schema/issues/new>) 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

> [!NOTE]
>
> **Example**
>
> my\_experiment.yaml
>
> ```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
>
> ```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:

```python
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](<https://pymmcore-plus.github.io/useq-schema/guides/sequences/#saving-and-loading-sequences>)).

> [!WARNING]
>
> **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)](<https://pymmcore-plus.github.io/useq-schema/v2-migration/index.md>) if you're curious.

## Next steps

- [Building Sequences](<https://pymmcore-plus.github.io/useq-schema/guides/sequences/index.md>): common experiments, step by step.
- The [`MDASequence`](<https://pymmcore-plus.github.io/useq-schema/schema/sequence/index.md>), [`MDAEvent`](<https://pymmcore-plus.github.io/useq-schema/schema/event/index.md>), [axes](<https://pymmcore-plus.github.io/useq-schema/schema/axes/index.md>) and [autofocus](<https://pymmcore-plus.github.io/useq-schema/schema/hardware_autofocus/index.md>) reference pages.
- [The Acquisition Engine](<https://pymmcore-plus.github.io/pymmcore-plus/guides/mda_engine/index.md>), to run a sequence on a microscope.
