Skip to content

Commit

Permalink
Merge pull request micro-manager#692 from henrypinkard/main
Browse files Browse the repository at this point in the history
documentation improvements, examples, bug fixes, and some tests for notification API
  • Loading branch information
henrypinkard authored Sep 7, 2023
2 parents 6a52841 + abe0afc commit 274de12
Show file tree
Hide file tree
Showing 16 changed files with 196 additions and 48 deletions.
2 changes: 1 addition & 1 deletion docs/source/advanced_usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Advanced use cases may require a deeper understanding of how pycromanager works.
:width: 800
:alt: Architecture of pycro-manager

**Headless mode** In headless mode, pycro-manager can be run without the micro-manager user interface, enabling the implementation of custom user interfaces or no user interfaces at all.
**Headless mode (using Java backend)** In headless mode, pycro-manager can be run without the micro-manager user interface, enabling the implementation of custom user interfaces or no user interfaces at all.


.. toctree::
Expand Down
8 changes: 8 additions & 0 deletions docs/source/apis.rst
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,18 @@ MagellanAcquisition
.. autoclass:: MagellanAcquisition
:members:


Headless mode
#######################################################

start_headless
===========================
.. autofunction:: start_headless

stop_headless
===========================
.. autofunction:: stop_headless


Dataset
##############################################
Expand Down
10 changes: 7 additions & 3 deletions docs/source/headless_mode.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
Headless Mode
**************************

Headless mode allows you to use Pycro-manager without having to launch Micro-manager. The :meth:`start_headless<pycromanager.start_headless>` method should be run prior to any other calls. This will launch in the background with only the essential Java components for running the Pycro-manager acquisition system, without showing a user interface. It provides a lightweight environment that can also be run without a GUI in order to use Pycro-manager as a hidden backend for custom applications. This could be useful, for example, if you want to implement your own user interface, or run Pycro-manager from a server environment.
Headless mode allows you to use Pycro-manager without having to launch Micro-manager. The :meth:`start_headless<pycromanager.start_headless>` method should be run prior to any other calls. This function launches Micro-Manager core and the acqusition engine, which is used by the ``Acquisition`` class.

Headless mode can be run with either a Java backend (i.e. the same components that run when you use pycromanager along with the Micro-Manager application) or a Python backend, which provides pure python versions of the same components.

Headless mode provides a lightweight environment that can also be run without a GUI in order to use Pycro-manager as a hidden backend for custom applications. This could be useful, for example, if you want to implement your own user interface, or run Pycro-manager from a server environment.


.. code-block:: python
Expand All @@ -20,7 +24,7 @@ Headless mode allows you to use Pycro-manager without having to launch Micro-man
core = Core()
The example below shows headless mode in combination with an saved image callback, which calls a user-defined function whenever new data is written to disk. This setup could be used to replace the pycro-manager viewer with a custom user interface (note the ``show_display=False`` in the acquisition).
The example below shows headless mode in combination with an saved image callback, which calls a user-defined function whenever new data is stored (on disk or in RAM depending on the arguments to ``Acquisition``). This setup could be used to replace the pycro-manager viewer with a custom user interface (note the ``show_display=False`` in the acquisition).


.. code-block:: python
Expand Down Expand Up @@ -55,7 +59,7 @@ The example below shows headless mode in combination with an saved image callbac
How to install Java for Mac OS
=============================================
Running headless mode is easy on Windows, because the correct version of Java comes bundled with the Micro-Manager installer. However, on Mac OS, this is not the case. As a result, it can be helpful to manually install a compatible version of Java.
Running headless mode with the Java backend is easy on Windows, because the correct version of Java comes bundled with the Micro-Manager installer. However, on Mac OS, this is not the case. As a result, it can be helpful to manually install a compatible version of Java.

This can be done through Python as follows: First install the Python package ``install-jdk``.

Expand Down
4 changes: 2 additions & 2 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
:width: 600
:alt: Alternative text

``pycromanager`` is a python package for microscope hardware automation and image acquisition. It provides a variety of components that can be used independently or in combination, with the aim of being flexible enough to enable a variety of applications.
``pycromanager`` is a python package for microscope hardware automation and image acquisition. It provides many components that can be used independently or in combination, with the aim of being flexible enough to enable a variety of applications.

The best place to get started is the :doc:`user_guide`. The :doc:`tutorials` section shows jupyter notebook tutorials of using pycro-manager vs various applications, as well as some more in depth descriptions of certain features.
The best place to get started is the :doc:`user_guide`. The :doc:`tutorials` section shows jupyter notebook tutorials of using pycro-manager for various applications, as well as some more in depth descriptions of certain features.


Information on the original motivations for Pycro-Manager can be found in the `publication <https://rdcu.be/cghwk>`_, though its capabilities have since expanded.
Expand Down
1 change: 1 addition & 0 deletions pycromanager/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
from pycromanager.mm_java_classes import Studio, Magellan
from pycromanager.core import Core
from pycromanager.zmq_bridge.wrappers import JavaObject, JavaClass, PullSocket, PushSocket
from pycromanager.acquisition.acq_eng_py.main.acq_notification import AcqNotification
from ._version import __version__, version_info
36 changes: 24 additions & 12 deletions pycromanager/acq_future.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import threading
from pycromanager.acquisition.acq_eng_py.main.acq_notification import AcqNotification

def _axes_to_key(axes_or_axes_list):
def _axes_to_key(axes):
""" Turn axes into a hashable key """
return frozenset(axes_or_axes_list.items())

return frozenset(axes.items())

class AcquisitionFuture:

Expand Down Expand Up @@ -38,12 +37,18 @@ def _notify(self, notification):
received. Want to store this, rather than just waiting around for it, in case the await methods are called
after the notification has already been sent.
"""
if notification.type == AcqNotification.Acquisition.ACQ_EVENTS_FINISHED:
if notification.phase == AcqNotification.Acquisition.ACQ_EVENTS_FINISHED or \
notification.phase == AcqNotification.Image.DATA_SINK_FINISHED:
return # ignore for now...
key = _axes_to_key(notification.axes)
if key not in self._notification_recieved.keys():
if isinstance(notification.id, list):
keys = [_axes_to_key(ax) for ax in notification.id]
else:
keys = [_axes_to_key(notification.id)]
# check if any keys are present in the notification_recieved dict
if not any([key in self._notification_recieved.keys() for key in keys]):
return # ignore notifications that aren't relevant to this future
self._notification_recieved[key][notification.phase] = True
for key in keys:
self._notification_recieved[key][notification.phase] = True
with self._condition:
self._condition.notify_all()

Expand All @@ -57,11 +62,18 @@ def await_execution(self, axes, phase):
self._condition.wait()

def await_image_saved(self, axes, return_image=False):
key = _axes_to_key(axes)
with self._condition:
while not self._notification_recieved[key][AcqNotification.Image.IMAGE_SAVED]:
self._condition.wait()
if isinstance(axes, list):
keys = [_axes_to_key(ax) for ax in axes]
else:
keys = [_axes_to_key(axes)]
for key in keys:
with self._condition:
while not self._notification_recieved[key][AcqNotification.Image.IMAGE_SAVED]:
self._condition.wait()
if return_image:
return self._acq.get_dataset().read_image(**axes)
if isinstance(axes, list):
return [self._acq.get_dataset().read_image(**ax) for ax in axes]
else:
return self._acq.get_dataset().read_image(**axes)


2 changes: 1 addition & 1 deletion pycromanager/acquisition/acq_constructor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
class Acquisition(PycromanagerAcquisitionBase):
def __new__(cls,
directory: str = None,
name: str = None,
name: str = 'default_acq_name',
image_process_fn: callable = None,
event_generation_hook_fn: callable = None,
pre_hardware_hook_fn: callable = None,
Expand Down
43 changes: 26 additions & 17 deletions pycromanager/acquisition/acq_eng_py/main/acq_notification.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import json

class AcqNotification:

class Acquisition:
Expand All @@ -6,15 +8,15 @@ class Acquisition:

@staticmethod
def to_string():
return "Global"
return "global"

class Hardware:
PRE_HARDWARE = "pre_hardware"
POST_HARDWARE = "post_hardware"

@staticmethod
def to_string():
return "Hardware"
return "hardware"

class Camera:
PRE_SEQUENCE_STARTED = "pre_sequence_started"
Expand All @@ -23,31 +25,38 @@ class Camera:

@staticmethod
def to_string():
return "Camera"
return "camera"

class Image:
IMAGE_SAVED = "image_saved"
DATA_SINK_FINISHED = "data_sink_finished"

@staticmethod
def to_string():
return "Image"
return "image"

def __init__(self, type, id, phase=None):
if type is None:
# then figure it out based on the phase
if phase in [AcqNotification.Camera.PRE_SNAP, AcqNotification.Camera.POST_EXPOSURE,
AcqNotification.Camera.PRE_SEQUENCE_STARTED]:
type = AcqNotification.Camera
elif phase in [AcqNotification.Hardware.PRE_HARDWARE, AcqNotification.Hardware.POST_HARDWARE]:
type = AcqNotification.Hardware
elif phase == AcqNotification.Image.IMAGE_SAVED:
type = AcqNotification.Image
else:
raise ValueError("Unknown phase")
self.type = type
if type == AcqNotification.Acquisition.to_string():
self.type = AcqNotification.Acquisition
self.id = id
self.phase = phase
elif type == AcqNotification.Image.to_string() and phase == AcqNotification.Image.DATA_SINK_FINISHED:
self.type = AcqNotification.Image
self.id = id
self.phase = phase
elif phase in [AcqNotification.Camera.PRE_SNAP, AcqNotification.Camera.POST_EXPOSURE,
AcqNotification.Camera.PRE_SEQUENCE_STARTED]:
self.type = AcqNotification.Camera
self.id = json.loads(id)
elif phase in [AcqNotification.Hardware.PRE_HARDWARE, AcqNotification.Hardware.POST_HARDWARE]:
self.type = AcqNotification.Hardware
self.id = json.loads(id)
elif phase == AcqNotification.Image.IMAGE_SAVED:
self.type = AcqNotification.Image
self.id = id
else:
raise ValueError("Unknown phase")
self.phase = phase
self.id = id


@staticmethod
Expand Down
2 changes: 1 addition & 1 deletion pycromanager/acquisition/acquisition_superclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def __init__(
include various stages of the control of hardware and the camera and saving of images. Notification
callbacks will execute asynchronously with respect to the acquisition process. The supplied function
should take a single argument, which will be an AcqNotification object. It should execute quickly,
so as to not back up the processing of other notifications.
so as to not back up the processing of other notifications.
image_saved_fn : Callable
function that takes two arguments (the Axes of the image that just finished saving, and the Dataset)
or three arguments (Axes, Dataset and the event_queue) and gets called whenever a new image is written to
Expand Down
15 changes: 9 additions & 6 deletions pycromanager/acquisition/java_backend_acquisitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,16 @@ def _notification_handler_fn(acquisition, notification_push_port, connected_even
while True:
message = monitor_socket.receive()
notification = AcqNotification.from_json(message)
acquisition._notification_queue.put(notification)
# these are processed seperately to handle image saved callback
if AcqNotification.is_image_saved_notification(notification):
if not notification.is_data_sink_finished_notification():
# decode the NDTiff index entry
index_entry = notification.id.encode('ISO-8859-1')
axes = acquisition._dataset._add_index_entry(index_entry)
# swap the notification id from the byte array of index information to axes
notification.id = axes
acquisition._image_notification_queue.put(notification)

acquisition._notification_queue.put(notification)
if AcqNotification.is_acquisition_finished_notification(notification):
events_finished = True
elif AcqNotification.is_data_sink_finished_notification(notification):
Expand All @@ -218,7 +223,7 @@ class JavaBackendAcquisition(Acquisition, metaclass=NumpyDocstringInheritanceMet
def __init__(
self,
directory: str=None,
name: str=None,
name: str='default_acq_name',
image_process_fn : callable=None,
event_generation_hook_fn: callable=None,
pre_hardware_hook_fn: callable=None,
Expand Down Expand Up @@ -439,11 +444,9 @@ def _storage_monitor_fn():
image_notification = self._image_notification_queue.get()
if AcqNotification.is_data_sink_finished_notification(image_notification):
break
index_entry = image_notification.id.encode('ISO-8859-1')
axes = dataset._add_index_entry(index_entry)
dataset._new_image_arrived = True
if callback is not None:
callback(axes, dataset)
callback(image_notification.id, dataset)
t = threading.Thread(target=_storage_monitor_fn, name='StorageMonitorThread')
t.start()
return t
Expand Down
2 changes: 1 addition & 1 deletion pycromanager/acquisition/python_backend_acquisitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class PythonBackendAcquisition(Acquisition, metaclass=NumpyDocstringInheritanceM
def __init__(
self,
directory: str=None,
name: str=None,
name: str='default_acq_name',
image_process_fn: callable=None,
event_generation_hook_fn: callable = None,
pre_hardware_hook_fn: callable=None,
Expand Down
9 changes: 5 additions & 4 deletions pycromanager/headless.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def stop_headless(debug=False):

def start_headless(
mm_app_path: str, config_file: str='', java_loc: str=None,
core_log_path: str='', python_backend=False,
python_backend=False, core_log_path: str='',
buffer_size_mb: int=1024, max_memory_mb: int=2000,
port: int=_Bridge.DEFAULT_PORT, debug=False):
"""
Expand All @@ -141,14 +141,14 @@ def start_headless(
is left to the user.
java_loc: str
Path to the java version that it should be run with (Java backend only)
core_log_path : str
Path to where core log files should be created
python_backend : bool
Whether to use the python backend or the Java backend
core_log_path : str
Path to where core log files should be created
buffer_size_mb : int
Size of circular buffer in MB in MMCore
max_memory_mb : int
Maximum amount of memory to be allocated to JVM
Maximum amount of memory to be allocated to JVM (Java backend only
port : int
Default port to use for ZMQServer (Java backend only)
debug : bool
Expand All @@ -159,6 +159,7 @@ def start_headless(
mmc = _create_pymmcore_instance()
mmc.set_device_adapter_search_paths([mm_app_path])
mmc.load_system_configuration(config_file)
mmc.set_circular_buffer_memory_footprint(buffer_size_mb)
_PYMMCORES.append(mmc) # Store so it doesn't get garbage collected
Engine(mmc)
else:
Expand Down
54 changes: 54 additions & 0 deletions pycromanager/test/test_notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""
Tests for notification API and asynchronous acquisition API
"""
from pycromanager import multi_d_acquisition_events, Acquisition, AcqNotification
import time
import numpy as np

# TODO: add tests for timing of blocking until different parts of the hardware sequence
# def test_async_images_read(launch_mm_headless, setup_data_folder):
# start = time.time()
# events = multi_d_acquisition_events(num_time_points=10, time_interval_s=0.5)
# with Acquisition(directory=setup_data_folder, show_display=False) as acq:
# future = acq.acquire(events)
#
# future.await_execution({'time': 5}, AcqNotification.Hardware.POST_HARDWARE)



def test_async_image_read(launch_mm_headless, setup_data_folder):
events = multi_d_acquisition_events(num_time_points=10, time_interval_s=0.5)
with Acquisition(directory=setup_data_folder, show_display=False) as acq:
future = acq.acquire(events)
image = future.await_image_saved({'time': 5}, return_image=True)
assert np.all(image == acq.get_dataset().read_image(time=5))

def test_async_image_read_sequence(launch_mm_headless, setup_data_folder):
events = multi_d_acquisition_events(num_time_points=10, time_interval_s=0)
with Acquisition(directory=setup_data_folder, show_display=False) as acq:
future = acq.acquire(events)
image = future.await_image_saved({'time': 5}, return_image=True)
assert np.all(image == acq.get_dataset().read_image(time=5))

def test_async_images_read(launch_mm_headless, setup_data_folder):
events = multi_d_acquisition_events(num_time_points=10, time_interval_s=0.5)
with Acquisition(directory=setup_data_folder, show_display=False) as acq:
future = acq.acquire(events)
images = future.await_image_saved([{'time': 7}, {'time': 8}, {'time': 9}], return_image=True)
assert (len(images) == 3)

# Make sure the returned images were the correct ones
on_disk = [acq.get_dataset().read_image(time=t) for t in [7, 8, 9]]
assert all([np.all(on_disk[i] == images[i]) for i in range(3)])

def test_async_images_read_sequence(launch_mm_headless, setup_data_folder):
events = multi_d_acquisition_events(num_time_points=10, time_interval_s=0)
with Acquisition(directory=setup_data_folder, show_display=False) as acq:
future = acq.acquire(events)
images = future.await_image_saved([{'time': 7}, {'time': 8}, {'time': 9}], return_image=True)
assert (len(images) == 3)

# Make sure the returned images were the correct ones
on_disk = [acq.get_dataset().read_image(time=t) for t in [7, 8, 9]]
assert all([np.all(on_disk[i] == images[i]) for i in range(3)])

Loading

0 comments on commit 274de12

Please sign in to comment.