Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

typing, linting and example fix #32

Merged
merged 11 commits into from
Nov 21, 2024
7 changes: 7 additions & 0 deletions src/exengine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,10 @@

from .kernel.executor import ExecutionEngine
from .kernel.threading_decorator import on_thread

__all__ = [
"ExecutionEngine",
"on_thread",
"__version__",
"version_info",
]
19 changes: 11 additions & 8 deletions src/exengine/backends/micromanager/mm_device_implementations.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
from exengine.device_types import (Detector, TriggerableSingleAxisPositioner, TriggerableDoubleAxisPositioner)
from exengine.kernel.device import Device
from mmpycorex import Core
import numpy as np
import numpy.typing as npt
import pymmcore
import time
from concurrent.futures import ThreadPoolExecutor
from typing import List, Union, Iterable, Tuple
from typing import Union, Iterable, Tuple, TYPE_CHECKING

if TYPE_CHECKING:
from typing import Any



Expand All @@ -29,7 +32,7 @@ def __init__(self, name=None, _validatename=True):
self._core_noexec = Core()
if _validatename:
loaded_devices = self._core_noexec.get_loaded_devices()
if name is not None and not name in loaded_devices:
if name is not None and name not in loaded_devices:
raise Exception(f'Device with name {name} not found')
if name is None and len(loaded_devices) > 1:
raise ValueError("Multiple Stage device_implementations found, must specify device name")
Expand Down Expand Up @@ -73,7 +76,7 @@ def __dir__(self):
print(f"Warning: Failed to retrieve device properties: {e}")
return sorted(attributes)

def get_allowed_property_values(self, property_name: str) -> List[str]:
def get_allowed_property_values(self, property_name: str) -> list[str]:
return self._core_noexec.get_allowed_property_values(self._device_name_noexec, property_name)

def is_property_read_only(self, property_name: str) -> bool:
Expand Down Expand Up @@ -128,7 +131,7 @@ def set_position(self, position: float) -> None:
def get_position(self) -> float:
return self._core_noexec.get_position(self._device_name_noexec)

def set_position_sequence(self, positions: np.ndarray) -> None:
def set_position_sequence(self, positions: npt.NDArray["Any"]) -> None:
if not self._core_noexec.is_stage_sequenceable(self._device_name_noexec):
raise ValueError("Stage does not support sequencing")
max_length = self._core_noexec.get_stage_sequence_max_length(self._device_name_noexec)
Expand Down Expand Up @@ -175,7 +178,7 @@ def set_position(self, x: float, y: float) -> None:
def get_position(self) -> Tuple[float, float]:
return self._core_noexec.get_xy_position(self._device_name_noexec)

def set_position_sequence(self, positions: np.ndarray) -> None:
def set_position_sequence(self, positions: npt.NDArray["Any"]) -> None:
if not self._core_noexec.is_xy_stage_sequenceable(self._device_name_noexec):
raise ValueError("Stage does not support sequencing")
max_length = self._core_noexec.get_xy_stage_sequence_max_length(self._device_name_noexec)
Expand Down Expand Up @@ -262,14 +265,14 @@ def stop(self) -> None:
def is_stopped(self) -> bool:
return not self._core_noexec.is_sequence_running(self._device_name_noexec) and not self._snap_active

def pop_data(self, timeout=None) -> Tuple[np.ndarray, dict]:
def pop_data(self, timeout=None) -> Tuple[npt.NDArray["Any"], dict]:
if self._frame_count != 1:
md = pymmcore.Metadata()
start_time = time.time()
while True:
try:
pix = self._core_noexec.pop_next_image_md(0, 0, md)
except IndexError as e:
except IndexError:
pix = None
if pix is not None:
break
Expand Down
8 changes: 3 additions & 5 deletions src/exengine/backends/micromanager/test/test_mm_camera.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import pytest
import time
import os
import itertools
from mmpycorex import create_core_instance, terminate_core_instances, get_default_install_location
from exengine.kernel.executor import ExecutionEngine
from exengine.kernel.data_handler import DataHandler
from exengine.kernel.data_coords import DataCoordinates
Expand Down Expand Up @@ -33,7 +31,7 @@ def capture_images(num_images, executor, camera):

executor.submit([start_capture_event, readout_images_event])

while not {'time': num_images - 1} in storage:
while {'time': num_images - 1} not in storage:
time.sleep(1)

data_handler.finish()
Expand All @@ -60,11 +58,11 @@ def test_continuous_capture(executor, camera):
storage = NDRAMStorage()
data_handler = DataHandler(storage=storage)

start_capture_event = StartContinuousCapture(camera=camera)
start_capture_event = StartContinuousCapture(detector=camera)
readout_images_event = ReadoutData(detector=camera,
data_coordinates_iterator=(DataCoordinates(time=t) for t in itertools.count()),
data_handler=data_handler)
stop_capture_event = StopCapture(camera=camera)
stop_capture_event = StopCapture(detector=camera)

_, readout_future, _ = executor.submit([start_capture_event, readout_images_event, stop_capture_event])
time.sleep(2)
Expand Down
13 changes: 12 additions & 1 deletion src/exengine/base_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,15 @@
from .kernel.ex_event_capabilities import DataProducing, Stoppable, Abortable
from .kernel.ex_event_base import ExecutorEvent
from .kernel.device import Device
from .kernel.data_storage_base import DataStorage
from .kernel.data_storage_base import DataStorage

__all__ = [
"Notification",
"NotificationCategory",
"DataProducing",
"Stoppable",
"Abortable",
"ExecutorEvent",
"Device",
"DataStorage"
]
8 changes: 0 additions & 8 deletions src/exengine/conftest.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
import os
import sys
import shutil
import warnings

import pytest
import re
import glob

import socket
from mmpycorex import (download_and_install_mm, find_existing_mm_install, create_core_instance,
terminate_core_instances, get_default_install_location)

Expand Down
8 changes: 7 additions & 1 deletion src/exengine/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,10 @@
Convenience file for imports
"""
from .kernel.data_coords import DataCoordinates, DataCoordinatesIterator
from .kernel.data_handler import DataHandler
from .kernel.data_handler import DataHandler

__all__ = [
"DataCoordinates",
"DataCoordinatesIterator",
"DataHandler"
]
12 changes: 6 additions & 6 deletions src/exengine/device_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
"""

from abc import abstractmethod
from typing import Tuple
import numpy as np
from typing import Tuple, Any
from .kernel.device import Device
import numpy.typing as npt


# TODO: could replace hard coded classes with
Expand All @@ -31,7 +31,7 @@ class TriggerableSingleAxisPositioner(SingleAxisPositioner):
A special type of positioner that can accept a sequence of positions to move to when provided external TTL triggers
"""
@abstractmethod
def set_position_sequence(self, positions: np.ndarray) -> None:
def set_position_sequence(self, positions: npt.NDArray[Any]) -> None:
...

@abstractmethod
Expand All @@ -54,13 +54,13 @@ def set_position(self, x: float, y: float) -> None:
...

@abstractmethod
def get_position(self) -> Tuple[float, float]:
def get_position(self) -> "Tuple[float, float]":
...

class TriggerableDoubleAxisPositioner(DoubleAxisPositioner):

@abstractmethod
def set_position_sequence(self, positions: np.ndarray) -> None:
def set_position_sequence(self, positions: npt.NDArray[Any]) -> None:
...

@abstractmethod
Expand Down Expand Up @@ -101,7 +101,7 @@ def is_stopped(self) -> bool:
...

@abstractmethod
def pop_data(self, timeout=None) -> Tuple[np.ndarray, dict]:
def pop_data(self, timeout=None) -> Tuple[npt.NDArray[Any], dict[str, Any]]:
"""
Get the next image and metadata from the camera buffer. If timeout is None, this function will block until
an image is available. If timeout is a number, this function will block for that many seconds before returning
Expand Down
24 changes: 12 additions & 12 deletions src/exengine/events/detector_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

class DataAcquiredNotification(Notification[DataCoordinates]):
category = NotificationCategory.Data
description = "Data has been acquired by a camera or other data-producing device and is now available"
description = "Data has been acquired by a detector or other data-producing device and is now available"
# payload is the data coordinates of the acquired data

class ReadoutData(Stoppable, DataProducing, ExecutorEvent):
Expand Down Expand Up @@ -57,8 +57,8 @@ def execute(self) -> None:
# if detector is a string, look it up in the device registry
self.detector: Detector = (self.detector if isinstance(self.detector, Detector)
else ExecutionEngine.get_device(self.detector))
# TODO a more efficient way to do this is with callbacks from the camera
# but this is not currently implemented, at least for Micro-Manager cameras
# TODO a more efficient way to do this is with callbacks from the detector
# but this is not currently implemented, at least for Micro-Manager cameras
image_counter = itertools.count() if self.num_blocks is None else range(self.num_blocks)
for image_number, image_coordinates in zip(image_counter, self.data_coordinate_iterator):
while True:
Expand Down Expand Up @@ -108,29 +108,29 @@ class StartContinuousCapture(ExecutorEvent):
Tell Detector device to start capturing images continuously, until a stop signal is received
"""

def __init__(self, camera: Optional[Detector] = None):
def __init__(self, detector: Optional[Detector] = None):
super().__init__()
self.camera = camera
self.detector = detector

def execute(self):
"""
Capture images from the camera
Capture images from the detector
"""
try:
self.camera.arm()
self.camera.start()
self.detector.arm()
self.detector.start()
except Exception as e:
self.camera.stop()
self.detector.stop()
raise e

class StopCapture(ExecutorEvent):
"""
Tell Detector device to start capturing data continuously, until a stop signal is received
"""

def __init__(self, camera: Optional[Detector] = None):
def __init__(self, detector: Optional[Detector] = None):
super().__init__()
self.camera = camera
self.detector = detector

def execute(self):
self.camera.stop()
self.detector.stop()
8 changes: 4 additions & 4 deletions src/exengine/events/multi_d_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from exengine.kernel.data_coords import DataCoordinates
from exengine.events.property_events import (SetPropertiesEvent)
from exengine.events.positioner_events import SetTriggerable1DPositionsEvent, SetPosition1DEvent
from typing import Union, List, Iterable, Optional
from typing import Union, Iterable, Optional
import numpy as np
import copy
from itertools import chain
Expand All @@ -16,7 +16,7 @@ def flatten(lst):

def multi_d_acquisition_events(
num_time_points: int = None,
time_interval_s: Union[float, List[float]] = 0,
time_interval_s: Union[float, list[float]] = 0,
z_start: float = None,
z_end: float = None,
z_step: float = None,
Expand All @@ -27,7 +27,7 @@ def multi_d_acquisition_events(

xy_positions: Iterable = None,
xyz_positions: Iterable = None,
position_labels: List[str] = None,
position_labels: list[str] = None,
order: str = "tpcz",
sequence: str = None, # should be "zc", "cz", "tzc", etc
camera: Optional[Union[Detector, str]] = None,
Expand Down Expand Up @@ -64,7 +64,7 @@ def multi_d_acquisition_events(

has_zsteps = False
if any([z_start, z_step, z_end]):
if not None in [z_start, z_step, z_end]:
if None not in [z_start, z_step, z_end]:
has_zsteps = True
else:
raise ValueError('All of z_start, z_step, and z_end must be provided')
Expand Down
11 changes: 7 additions & 4 deletions src/exengine/events/positioner_events.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from typing import List, Union, Tuple, Optional, SupportsFloat
import numpy as np
from typing import Union, Tuple, Optional, SupportsFloat, TYPE_CHECKING
import numpy.typing as npt

from exengine.kernel.ex_event_base import ExecutorEvent
from exengine.device_types import (DoubleAxisPositioner, SingleAxisPositioner,
TriggerableSingleAxisPositioner, TriggerableDoubleAxisPositioner)

if TYPE_CHECKING:
from typing import Any


class SetPosition2DEvent(ExecutorEvent):
"""
Expand All @@ -23,7 +26,7 @@ class SetTriggerable2DPositionsEvent(ExecutorEvent):
Set the position of a movable device
"""

def __init__(self, device: Optional[TriggerableDoubleAxisPositioner], positions: Union[List[Tuple[float, float]], np.ndarray]):
def __init__(self, device: Optional[TriggerableDoubleAxisPositioner], positions: Union[list[Tuple[float, float]], npt.NDArray["Any"]]):
super().__init__()
self.device = device
self.positions = positions
Expand All @@ -49,7 +52,7 @@ class SetTriggerable1DPositionsEvent(ExecutorEvent):
Send a sequence of positions to a 1D positioner that will be triggered by TTL pulses
"""

def __init__(self, device: Optional[TriggerableSingleAxisPositioner], positions: Union[List[float], np.ndarray]):
def __init__(self, device: Optional[TriggerableSingleAxisPositioner], positions: Union[list[float], npt.NDArray["Any"]]):
super().__init__()
self.device = device
self.positions = positions
Expand Down
3 changes: 1 addition & 2 deletions src/exengine/events/property_events.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from typing import Any, Iterable, Tuple, Union, List
from dataclasses import dataclass
from typing import Any, Iterable, Tuple, Union
from exengine.kernel.device import Device
from exengine.kernel.executor import ExecutionEngine
from exengine.kernel.ex_event_base import ExecutorEvent
Expand Down
39 changes: 23 additions & 16 deletions src/exengine/examples/implicit_vs_explicit_excutor.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
from mmpycorex import create_core_instance, download_and_install_mm, terminate_core_instances
from mmpycorex import create_core_instance, terminate_core_instances
from exengine.kernel.executor import ExecutionEngine
from exengine.backends.micromanager.mm_device_implementations import MicroManagerCamera, MicroManagerSingleAxisStage
from exengine.kernel.notification_base import Notification
from exengine.backends.micromanager.mm_device_implementations import MicroManagerSingleAxisStage
from exengine.events.positioner_events import SetPosition1DEvent

def event_complete(notification: Notification) -> None:
print(f"Event complete, notification: {notification.category} - {notification.description} - {notification.payload}")

# download_and_install_mm() # If needed
# Start Micro-Manager core instance with Demo config
create_core_instance()
try:
create_core_instance()

executor = ExecutionEngine()
z_stage = MicroManagerSingleAxisStage()
executor = ExecutionEngine()
z_stage = MicroManagerSingleAxisStage()

# This occurs on the executor thread. The event is submitted to the executor and its result is awaited,
# meaning the call will block until the method is executed.
z_stage.set_position(100, thread='device_setting_thread')
# it is equivalent to:
executor.submit(SetPosition1DEvent(position=100, device=z_stage)).await_execution()
executor.subscribe_to_notifications(event_complete)

# explicit
z_stage.set_position(100)
# it is equivalent to:
# executor.submit(SetPosition1DEvent(position=100, device=z_stage), thread_name='device_setting_thread').await_execution()
# but the execution thread is the main thread

# implicit
# start capture first; we use await execution in order to make sure that the camera has finished acquisition
executor.submit(SetPosition1DEvent(position=100, device=z_stage), thread_name='device_setting_thread')

executor.submit(SetPosition1DEvent(position=100, device=z_stage), thread='device_setting_thread')
executor.submit(ReadoutImages(), thread='readout_thread')



executor.shutdown()
executor.shutdown()
terminate_core_instances()
except Exception as e:
print(f"An error occurred: {e}")
terminate_core_instances()
Loading
Loading