# useq-schema

*An implementation agnostic schema for describing a sequence of events during a
multi-dimensional imaging acquisition.*

The goal of this repo is to provide a specification (and some python utilities)
for generating event objects that can be consumed by microscope acquisition
engines.  The *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 Micro-Manager acquisition engine. One hope is to
solicit 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 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!

## `MDAEvent`

The primary "event" object is `useq.MDAEvent`.  This captures 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 *that* similar) 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.
- 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.MDAEvent` provides a `to_pycromanager()` method that returns a
  single pycro-manager event dict)
- *your object here?...*

```python
# simplified, and possibly outdated.  See useq.MDAEvent in codebase

# where `None` generally means "make no change"
class MDAEvent:
    metadata: Dict[str, Any] = {}  # user-specific data
    index: Dict[str, int] = {}  # {'axis'->index} for this event
    channel: Optional[Channel]  # optical config
    exposure: Optional[PositiveFloat]  # will likely expand for camera
    min_start_time: Optional[int]  # min time delta for beginning event
    x_pos: Optional[float]  # stage x
    y_pos: Optional[float]  # stage y
    z_pos: Optional[float]  # stage z
    properties: Optional[Sequence[PropertyTuple]]  # set arbitrary device props
    # TBD
    # action: Action ... such as "acquire", etc...

class Channel:
    config: str
    group: str = "Channel"

class PropertyTuple(NamedTuple):
    device_name: str
    property_name: str
    property_value: Any
```

> `useq-schema` uses [`pydantic`](https://pydantic-docs.helpmanual.io/) to
> define models, so you can retrieve the [json schema](https://json-schema.org/)
> for the `MDAEvent` object with `MDAEvent.schema_json()`

## `MDASequence`

`useq.MDASequence` represents a sequence of events (as might be generated by the
multidimensional acquisition GUI in most microscope software).  A
`useq.MDASequence` object is itself iterable, and yields `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)
- 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.MDASequence` provides a `to_pycromanager()`
  method that returns a list of pycro-manager events)
- *your object here?...*

```python
# simplified, and possibly outdated.  See useq.MDASequence in codebase

class MDASequence(BaseModel):
    metadata: Dict[str, Any] = {}  # user-specific data
    axis_order: str  # e.g. 'tpcz'
    stage_positions: Tuple[Position]
    channels: Tuple[Channel, ...]
    time_plan: AnyTimePlan  # see details below
    z_plan: AnyZPlan  # see details below

class Position(BaseModel):
    # if None, implies 'do not move this axis'
    x: Optional[float]
    y: Optional[float]
    z: Optional[float]
    name: Optional[str]
    z_plan: Optional[AnyZPlan]


class Channel(BaseModel):
    config: str
    group: str
    exposure: Optional[PositiveFloat]
    do_stack: bool = True
    z_offset: float = 0.0
    acquire_every: PositiveInt = 1  # acquire every n frames
    camera: Optional[str]
```

> `useq-schema` uses [`pydantic`](https://pydantic-docs.helpmanual.io/) to
> define models, so you can retrieve the [json schema](https://json-schema.org/)
> for the `MDASequence` object with `MDASequence.schema_json()`


`TimePlan` and `ZPlan` are each iterable objects that allow for various ways to
describe time and Z series.  These are each rather configurable and will be documented more later.

`TimePlans`:

- `TIntervalDuration` - specify *interval* and *duration*
- `TIntervalLoops` - specify *interval* and *number of timepoints*
- `TDurationLoops` - specify *duration* and *number of timepoints*

`ZPlans`:

- `ZTopBottom` - specify absolute *top*, *bottom*, and *step*
- `ZRangeAround` - specify symmetric *range* and *step*
- `ZAboveBelow` - specify asymmetric range *above* and *below* reference, with *step*
- `ZRelativePositions` - directly specify a sequence of relative z positions
- `ZAbsolutePositions` - directly specify a sequence of absolute z positions

#### example `MDASequence` usage:

```python
from useq import MDASequence

mda = 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',
)

len(mda)
# 720

list(mda)
# [
#     MDAEvent(index={'t': 0, 'p': 0, 'c': 0, 'z': 0}, ... z_pos=28.0),
#     MDAEvent(index={'t': 0, 'p': 0, 'c': 0, 'z': 1}, ... z_pos=28.5),
#     ...
# ]

mda.to_pycromanager()

# [
#  {'axes': {'position': 0, 'time': 0, 'z': 0},
#   'z': 28.0,
#   'x': 100.0,
#   'y': 100.0,
#   'min_start_time': 0,
#   'channel': {'config': 'DAPI', 'group': 'Channel'}},
#  {'axes': {'position': 0, 'time': 0, 'z': 1},
#   'z': 28.5,
#   'x': 100.0,
#   'y': 100.0,
#   'min_start_time': 0,
#   'channel': {'config': 'DAPI', 'group': 'Channel'}},
#   ...
# ]
```
