Skip to content

Commit

Permalink
Merge pull request #794 from henrypinkard/main
Browse files Browse the repository at this point in the history
Adaptive acquisition, python backend tests/documentation
  • Loading branch information
henrypinkard authored Aug 28, 2024
2 parents aa53237 + fbc6d0b commit a205b8d
Show file tree
Hide file tree
Showing 38 changed files with 1,078 additions and 1,595 deletions.
94 changes: 57 additions & 37 deletions docs/source/acq_events.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,69 +4,89 @@
Custom Acquisition Events
****************************************************************

An acquisition event is a Python ``dict`` object with a specific structure. The :meth:`multi_d_acquisition_events<pycromanager.multi_d_acquisition_events>` function can be used to create these events, but since it only covers a limited number of use cases, it often may be more useful to create them manually.
Acquisition events in Pycro-Manager are Python dictionaries that define how hardware is controlled and images are acquired. While the ``multi_d_acquisition_events()`` function can generate events for common scenarios, custom events offer greater flexibility and control for more complex imaging protocols.

A full description of all possible fields in an acquisition event can be found in the :ref:`acq_event_spec`.

Every event must have an ``axes`` field that uniquely identifies the image that the event will produce. This field is a dictionary which contains the name and position of each axis used to identify the image. The position can either be an int or a string.

For example, in a timelapse of ten images would vary only over the ``time`` axis, and the first two events would be:
Basic Structure
---------------

Every event must have an ``axes`` field to uniquely identify the resulting image:

.. code-block:: python
event = {
'axes': {'time': 0, 'z': 3, 'channel': 'DAPI'}
}
The position in each axis can be either an int or a string.

In most cases, each event will produce a single image. Thus, a series of images in a time-lapse acquisition can be defined as:

.. code-block:: python
event_0 = { 'axes': {'time': 0} }
event_1 = { 'axes': {'time': 1} }
event_0 = { 'axes': {'time': 0} }
event_1 = { 'axes': {'time': 1} }
event_2 = { 'axes': {'time': 2} }
A full description of all possible fields in an acquisition event can be found in the :ref:`acq_event_spec`.
The following example shows the a the events used to acquire a z-stack. Note that this is primarily for demonstration purposes, as the same events can be generated more conveniently using the :meth:`multi_d_acquisition_events<pycromanager.multi_d_acquisition_events>` function.
In addition to the ``axes`` field, events other fields that determine how the hardware is controlled (see :ref:`acq_event_spec` for a complete listing of these).

For example, the ``'z'`` field specifies the z position of the focus stage in microns. A Z-stack acquisition can be performed by creating events with different z positions:

.. code-block:: python
with Acquisition('/path/to/data', 'acq_name') as acq:
# create one event for the image at each z-slice
events = []
for index, z_um in enumerate(np.arange(start=0, stop=10, step=0.5)):
evt = {
# 'axes' is required. It is used by the image viewer and data storage to
# identify the acquired image
'axes': {'z': index},
# the 'z' field provides the z position in µm
'z': z_um
}
events.append(evt)
with Acquisition('/path/to/data', 'z_stack_acq') as acq:
events = []
for index, z_um in enumerate(np.arange(start=0, stop=10, step=0.5)):
evt = {
'axes': {'z': index}, # this indexes the resulting image
'z': z_um # this specifies the z position in microns
}
events.append(evt)
acq.acquire(events)
acq.acquire(events)
Creating custom acquisition events provides more flexibility in controlling hardware. For example, custom device properties can be specified in events:
Creating custom acquisition events provides more flexibility in controlling hardware. For example, custom `device properties <https://micro-manager.org/Version_2.0_Users_Guide#device-property-browser>`_ can be specified in events:

.. code-block:: python
with Acquisition('/path/to/data', 'acq_name') as acq:
events = []
for index in range(10):
evt = {
'axes': {'arbitrary_axis_name': index},
#'properties' for the manipulation of hardware by specifying an arbitrary
#list of properties
'properties':
[['device_name', 'property_name', 'property_value'],
['device_name_2', 'property_name_2', 'property_value_2']]
}
events.append(evt)
with Acquisition('/path/to/data', 'acq_name') as acq:
events = []
for index in range(10):
evt = {
'axes': {'arbitrary_axis_name': index},
# 'properties' allows manipulation of hardware by specifying an arbitrary
# list of properties
'properties': [
['device_name', 'property_name', 'property_value'],
['device_name_2', 'property_name_2', 'property_value_2']
]
}
events.append(evt)
acq.acquire(events)
acq.acquire(events)
For a practical example of custom events in action, see the `Intermittent Z-T Acquisition tutorial <intermittent_Z_T.ipynb>`_, which demonstrates how to acquire alternating time series and z-stacks.


.. toctree::
:maxdepth: 1
:hidden:

intermittent_Z_T.ipynb

The channel axis
==========================
The axis ``"channel"`` has a special significance because it not only determines how the corresponding image is stored, it also determines how it is displayed in the default viewer. Images with different ``"channel"`` values that match on all the other axes will by default be overlayed in a multi-channel image.
The ``channel`` axis has unique behavior: it not only determines the storage of images like other axes but also their display in the default viewer (:ref:`viewers`). Images with different ``channel`` values but matching other axes are overlaid in the default viewer.

In Micro-Manager, hardware settings for different channels are typically controlled by providing the group and preset name of a `Config group <https://micro-manager.org/wiki/Micro-Manager_Configuration_Guide#Configuration_Presets>`_. This is specified using the ``config_group`` field of acquisition events. These hardware control instructions can be specified independently of how the image is stored/displayed in the acquisition event.

In Micro-Manager, hardware settings for different channels are typically controlled by providing the group and preset name of a `Config group <https://micro-manager.org/wiki/Micro-Manager_Configuration_Guide#Configuration_Presets>`_. This is specified using the ``config_group`` field of acquisition events. These hardware control instructions can be specified independently of how the image is stored/displayed in the acquisition event

.. code-block:: python
Expand Down
69 changes: 29 additions & 40 deletions docs/source/acq_hooks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,63 +4,52 @@
Acquisition hooks
****************************************************************

Acquisition hooks can be used for several purposes: 1) Executing arbitrary code at different points within the acquisition cycle. For example, this could be used to incorporate devices outside of micro-manager into the acquisition cycle. 2) Modifying/deleting acquisition events in progress, for example to skip certain channels, or applying focus corrections on-the-fly. 3) Communcation with external devices at specific points in the acquisition cycle to enable the use of hardware TTL triggering for fast acquisitions

Hooks can either be executed before the hardware updates for a particular acquisition event (a ``pre_hardware_hook``), just after the hardware updates, just before the image is captured (a ``post_hardware_hook``), or after the camera has been instructed to take images or wait for an external trigger (a ``post_camera_hook``).
Acquisition hooks allow custom code to be injected at specific points in the acquisition process. This could be used, for example, to:

The simplest type of acquisition hook is function that takes a single argument (the current acquisition event). Pass this function to the acquisition by adding it as an argument in the constructor. This form might be used, for example, to control other hardware and have it synchronized with acquisition.
1. Execute arbitrary code during the acquisition cycle
2. Modify or delete acquisition events on-the-fly
3. Communicate with external devices for :ref:`hardware_triggering`

.. code-block:: python
def hook_fn(event):
### Do some other stuff here ###
return event
# pass in the function as a post_hardware_hook
with Acquisition(directory='/path/to/saving/dir', name='acquisition_name',
post_hardware_hook_fn=hook_fn) as acq:
### acquire some stuff ###
Types of Hooks
--------------

There are three types of hooks, each executed at a different point in the acquisition cycle:

Acquisition hooks can also be used to modify or delete acquisition events:

.. code-block:: python
1. ``pre_hardware_hook``: Executed before hardware updates
2. ``post_hardware_hook``: Executed after hardware updates, just before image capture
3. ``post_camera_hook``: Executed after the camera has been instructed to take images or wait for an external trigger

def hook_fn(event):
if some_condition:
return event
# condition isn't met, so delete this event by not returning it

Depending on where in the acquisition cycle the hook is, modifying or deleting the event may not have any effect. For example, modifying an event in a ``post_camera_hook_fn`` won't have any effect since the hardware has already been moved and the camera started. In contrast, in a ``pre_hardware_hook_fn``, the event can be modified and the acquistion engine will use the modified event. For example, the z position could be changed in the hook function, which would cause the acquisition engine to move the microscope's focus drive to a different position than it otherwise woudl have prior to taking an image.
Basic Usage
-----------

A hook function that takes two arguments can also be used in cases where one wants to submit additional acquisition events. The second argument, ``event_queue``, can be used for submitting additional acquisition events:
The simplest hook is a function that takes a single argument (the current acquisition event):

.. code-block:: python
def hook_fn(event, event_queue):
### create a new acquisition event in response to something ###
#event =
event_queue.put(event)
return event
def hook_fn(event):
# Custom code here
return event
If additional events will be submitted here, the typical syntax of ``with Acquisition...`` cannot be used because it will automatically close the acquisition too soon. Instead the acquisition should be created as:
with Acquisition(directory='/path/to/saving/dir', name='acquisition_name',
post_hardware_hook_fn=hook_fn) as acq:
# Acquisition code here
.. code-block:: python
acq = Acquisition(directory='/path/to/saving/dir', name='acquisition_name',
post_hardware_hook_fn=hook_fn)
Modifying or Deleting Events
----------------------------

When it is finished, it can be closed and cleaned up by passing an ``None`` to the ``event_queue``.
Hooks can modify or delete events by returning a modified event or not returning an event:

.. code-block:: python
def hook_fn(event, event_queue):
def hook_fn(event):
if some_condition:
return modified_event
# Delete event by not returning anything
if acq_end_condition:
event_queue.put(None)
else:
return event
The effect of modifying or deleting events depends on the hook's position in the acquisition cycle. For example:

- ``post_camera_hook_fn``: Modifications have no effect as hardware movement and camera activation have already occurred.
- ``pre_hardware_hook_fn``: Changes are fully applied. For example, modifying the z-position will cause the microscope to adjust its focus accordingly before image capture.
Loading

0 comments on commit a205b8d

Please sign in to comment.