feat: init
This commit is contained in:
0
test/model/devices/APIs/__init__.py
Normal file
0
test/model/devices/APIs/__init__.py
Normal file
0
test/model/devices/APIs/coherent/__init__.py
Normal file
0
test/model/devices/APIs/coherent/__init__.py
Normal file
0
test/model/devices/APIs/dynamixel/__init__.py
Normal file
0
test/model/devices/APIs/dynamixel/__init__.py
Normal file
0
test/model/devices/APIs/hamamatsu/__init__.py
Normal file
0
test/model/devices/APIs/hamamatsu/__init__.py
Normal file
175
test/model/devices/APIs/hamamatsu/test_hamamatsu_api.py
Normal file
175
test/model/devices/APIs/hamamatsu/test_hamamatsu_api.py
Normal file
@@ -0,0 +1,175 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only
|
||||
# (subject to the limitations in the disclaimer below)
|
||||
# provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
# Standard library imports
|
||||
|
||||
# Third party imports
|
||||
import pytest
|
||||
|
||||
# Local application imports
|
||||
|
||||
|
||||
@pytest.mark.hardware
|
||||
@pytest.fixture(autouse=True, scope="class")
|
||||
def open_camera():
|
||||
from navigate.model.devices.APIs.hamamatsu.HamamatsuAPI import DCAM, camReg
|
||||
|
||||
# open camera
|
||||
for i in range(10):
|
||||
assert camReg.numCameras == 0
|
||||
try:
|
||||
camera = DCAM()
|
||||
if camera.get_camera_handler() != 0:
|
||||
break
|
||||
camera.dev_close()
|
||||
camera = None
|
||||
except Exception:
|
||||
continue
|
||||
yield camera
|
||||
if camera is not None:
|
||||
assert camReg.numCameras == 1
|
||||
camera.dev_close()
|
||||
assert camReg.numCameras == 0
|
||||
|
||||
|
||||
@pytest.mark.hardware
|
||||
class TestHamamatsuAPI:
|
||||
@pytest.fixture(autouse=True)
|
||||
def _prepare_camera(self, open_camera):
|
||||
self.camera = open_camera
|
||||
assert self.camera is not None
|
||||
|
||||
def test_get_and_set_property_value(self):
|
||||
# set property
|
||||
configuration = {
|
||||
"subarray_mode": 1,
|
||||
"sensor_mode": 12, # 12 for progressive
|
||||
"defect_correct_mode": 2.0,
|
||||
"binning": 1.0,
|
||||
"readout_speed": 1.0,
|
||||
"trigger_active": 1.0,
|
||||
"trigger_mode": 1.0, # external light-sheet mode
|
||||
"trigger_polarity": 2.0, # positive pulse
|
||||
"trigger_source": 3.0, # software
|
||||
"exposure_time": 0.02,
|
||||
"internal_line_interval": 0.000075,
|
||||
}
|
||||
for k in configuration:
|
||||
assert self.camera.set_property_value(
|
||||
k, configuration[k]
|
||||
), f"can't set property{k} with value{configuration[k]}"
|
||||
|
||||
def is_in_range(value, target, precision=100):
|
||||
target_min = target - target / precision
|
||||
target_max = target + target / precision
|
||||
return value > target_min and value < target_max
|
||||
|
||||
# get property
|
||||
for k in configuration:
|
||||
v = self.camera.get_property_value(k)
|
||||
assert is_in_range(v, configuration[k]), f"The value of {k} isn't right!"
|
||||
|
||||
# set a non-exist property
|
||||
assert (
|
||||
self.camera.set_property_value("non-exist-property", 100) is False
|
||||
), "can't handle non-exist property name"
|
||||
|
||||
def test_ROI(self):
|
||||
import random
|
||||
|
||||
rects = [(0, 0, 2047, 2047), (512, 512, 1535, 1535), (768, 768, 1279, 1279)]
|
||||
|
||||
for i in range(10):
|
||||
r = random.randint(0, len(rects) - 1)
|
||||
rect = rects[r]
|
||||
self.camera.set_ROI(*rect)
|
||||
assert self.camera.get_property_value("image_width") == (
|
||||
rect[2] - rect[0] + 1
|
||||
), f"ROI Width: {(rect[2]-rect[0]+1)}"
|
||||
assert self.camera.get_property_value("image_height") == (
|
||||
rect[3] - rect[1] + 1
|
||||
), f"ROI Height: {(rect[3]-rect[1]+1)}"
|
||||
|
||||
def test_acquisition(self):
|
||||
import random
|
||||
import time
|
||||
from navigate.model.concurrency.concurrency_tools import SharedNDArray
|
||||
|
||||
configuration = {
|
||||
"sensor_mode": 12, # 12 for progressive
|
||||
"defect_correct_mode": 2.0,
|
||||
"binning": 1.0,
|
||||
"readout_speed": 1.0,
|
||||
"trigger_active": 1.0,
|
||||
"trigger_mode": 1.0, # external light-sheet mode
|
||||
"trigger_polarity": 2.0, # positive pulse
|
||||
"trigger_source": 3.0, # software
|
||||
"exposure_time": 0.02,
|
||||
"internal_line_interval": 0.000075,
|
||||
}
|
||||
|
||||
for k in configuration:
|
||||
self.camera.set_property_value(k, configuration[k])
|
||||
|
||||
number_of_frames = 100
|
||||
|
||||
data_buffer = [
|
||||
SharedNDArray(shape=(2048, 2048), dtype="uint16")
|
||||
for i in range(number_of_frames)
|
||||
]
|
||||
|
||||
# attach a buffer without detach a buffer
|
||||
r = self.camera.start_acquisition(data_buffer, number_of_frames)
|
||||
assert r is True, "attach the buffer correctly!"
|
||||
r = self.camera.start_acquisition(data_buffer, number_of_frames)
|
||||
# Confirmed that we can't attach a new buffer before detaching one
|
||||
assert r is False, "attach the buffer correctly!"
|
||||
|
||||
self.camera.start_acquisition(data_buffer, number_of_frames)
|
||||
|
||||
readout_time = self.camera.get_property_value("readout_time")
|
||||
|
||||
for i in range(10):
|
||||
trigger_num = random.randint(0, 30)
|
||||
for j in range(trigger_num):
|
||||
self.camera.fire_software_trigger()
|
||||
time.sleep(configuration["exposure_time"] + readout_time)
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
frames = self.camera.get_frames()
|
||||
assert len(frames) == trigger_num, "can not get all frames back!"
|
||||
|
||||
self.camera.stop_acquisition()
|
||||
|
||||
# detach a detached buffer
|
||||
self.camera.stop_acquisition()
|
||||
0
test/model/devices/APIs/logitech/__init__.py
Normal file
0
test/model/devices/APIs/logitech/__init__.py
Normal file
0
test/model/devices/APIs/omicron/__init__.py
Normal file
0
test/model/devices/APIs/omicron/__init__.py
Normal file
0
test/model/devices/APIs/optotune/__init__.py
Normal file
0
test/model/devices/APIs/optotune/__init__.py
Normal file
0
test/model/devices/APIs/pi/__init__.py
Normal file
0
test/model/devices/APIs/pi/__init__.py
Normal file
0
test/model/devices/APIs/sutter/__init__.py
Normal file
0
test/model/devices/APIs/sutter/__init__.py
Normal file
0
test/model/devices/APIs/thorlabs/__init__.py
Normal file
0
test/model/devices/APIs/thorlabs/__init__.py
Normal file
0
test/model/devices/__init__.py
Normal file
0
test/model/devices/__init__.py
Normal file
0
test/model/devices/camera/__init__.py
Normal file
0
test/model/devices/camera/__init__.py
Normal file
80
test/model/devices/camera/test_camera_base.py
Normal file
80
test/model/devices/camera/test_camera_base.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only
|
||||
(subject to the limitations in the disclaimer below)
|
||||
# provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#"""
|
||||
|
||||
# Third Party Imports
|
||||
|
||||
from navigate.model.devices.camera.synthetic import SyntheticCamera
|
||||
|
||||
|
||||
def test_start_camera(dummy_model):
|
||||
model = dummy_model
|
||||
for microscope_name in model.configuration["configuration"]["microscopes"].keys():
|
||||
camera = SyntheticCamera(microscope_name, None, model.configuration)
|
||||
assert (
|
||||
camera.camera_parameters["hardware"]["serial_number"]
|
||||
== model.configuration["configuration"]["microscopes"][microscope_name][
|
||||
"camera"
|
||||
]["hardware"]["serial_number"]
|
||||
), f"didn't load correct camera parameter for microscope {microscope_name}"
|
||||
|
||||
# non-exist microscope name
|
||||
microscope_name = (
|
||||
model.configuration["configuration"]["microscopes"].keys()[0] + "_random_error"
|
||||
)
|
||||
raised_error = False
|
||||
try:
|
||||
_ = SyntheticCamera(microscope_name, None, model.configuration)
|
||||
except NameError:
|
||||
raised_error = True
|
||||
assert (
|
||||
raised_error
|
||||
), "should raise NameError when the microscope name doesn't exist!"
|
||||
|
||||
|
||||
def test_camera_base_functions(dummy_model):
|
||||
import random
|
||||
|
||||
model = dummy_model
|
||||
microscope_name = model.configuration["experiment"]["MicroscopeState"][
|
||||
"microscope_name"
|
||||
]
|
||||
|
||||
camera = SyntheticCamera(microscope_name, None, model.configuration)
|
||||
funcs = ["set_readout_direction", "calculate_light_sheet_exposure_time"]
|
||||
args = [[random.random()], [random.random(), random.random()]]
|
||||
|
||||
for f, a in zip(funcs, args):
|
||||
if a is not None:
|
||||
getattr(camera, f)(*a)
|
||||
else:
|
||||
getattr(camera, f)()
|
||||
193
test/model/devices/camera/test_camera_synthetic.py
Normal file
193
test/model/devices/camera/test_camera_synthetic.py
Normal file
@@ -0,0 +1,193 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
# Third Party Imports
|
||||
import pytest
|
||||
import numpy as np
|
||||
|
||||
from navigate.model.devices.camera.synthetic import (
|
||||
SyntheticCamera,
|
||||
SyntheticCameraController,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def synthetic_camera(dummy_model):
|
||||
dummy_model = dummy_model
|
||||
scc = SyntheticCameraController()
|
||||
microscope_name = dummy_model.configuration["experiment"]["MicroscopeState"][
|
||||
"microscope_name"
|
||||
]
|
||||
synthetic_camera = SyntheticCamera(microscope_name, scc, dummy_model.configuration)
|
||||
return synthetic_camera
|
||||
|
||||
|
||||
class TestSyntheticCamera:
|
||||
"""Unit Test for Camera Synthetic Class"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _prepare_camera(self, synthetic_camera):
|
||||
self.synthetic_camera = synthetic_camera
|
||||
|
||||
def test_synthetic_camera_attributes(self):
|
||||
desired_attributes = [
|
||||
"x_pixels",
|
||||
"y_pixels",
|
||||
"is_acquiring",
|
||||
"_mean_background_count",
|
||||
"_noise_sigma",
|
||||
"camera_controller",
|
||||
"current_frame_idx",
|
||||
"data_buffer",
|
||||
"num_of_frame",
|
||||
"pre_frame_idx",
|
||||
]
|
||||
for da in desired_attributes:
|
||||
assert hasattr(self.synthetic_camera, da)
|
||||
|
||||
def test_synthetic_camera_wheel_attributes_type(self):
|
||||
desired_attributes = {
|
||||
"x_pixels": int,
|
||||
"y_pixels": int,
|
||||
"is_acquiring": bool,
|
||||
"_mean_background_count": int,
|
||||
"_noise_sigma": np.float64,
|
||||
# 'current_frame_idx': None,
|
||||
# 'data_buffer': None,
|
||||
# 'num_of_frame': None,
|
||||
# 'pre_frame_idx': None,
|
||||
}
|
||||
|
||||
for key in desired_attributes:
|
||||
attribute = getattr(self.synthetic_camera, key)
|
||||
print(key, type(attribute), desired_attributes[key])
|
||||
assert type(attribute) == desired_attributes[key]
|
||||
|
||||
def test_synthetic_camera_methods(self):
|
||||
methods = [
|
||||
"report_settings",
|
||||
"close_camera",
|
||||
"set_sensor_mode",
|
||||
"set_exposure_time",
|
||||
"set_line_interval",
|
||||
"set_binning",
|
||||
"initialize_image_series",
|
||||
"close_image_series",
|
||||
"generate_new_frame",
|
||||
"get_new_frame",
|
||||
"set_ROI",
|
||||
]
|
||||
|
||||
for m in methods:
|
||||
assert hasattr(self.synthetic_camera, m) and callable(
|
||||
getattr(self.synthetic_camera, m)
|
||||
)
|
||||
|
||||
def test_synthetic_camera_wheel_method_calls(self):
|
||||
self.synthetic_camera.report_settings()
|
||||
self.synthetic_camera.close_camera()
|
||||
self.synthetic_camera.set_sensor_mode(mode="test")
|
||||
self.synthetic_camera.set_exposure_time(exposure_time=0.2)
|
||||
self.synthetic_camera.set_line_interval(line_interval_time=1)
|
||||
self.synthetic_camera.set_binning(binning_string="2x2")
|
||||
self.synthetic_camera.initialize_image_series()
|
||||
self.synthetic_camera.close_image_series()
|
||||
self.synthetic_camera.get_new_frame()
|
||||
self.synthetic_camera.set_ROI()
|
||||
|
||||
def test_synthetic_camera_exposure(self):
|
||||
exposure_time = 200
|
||||
self.synthetic_camera.set_exposure_time(exposure_time=exposure_time / 1000)
|
||||
assert (exposure_time / 1000) == self.synthetic_camera.camera_exposure_time
|
||||
|
||||
def test_synthetic_camera_binning(self):
|
||||
x_pixels = self.synthetic_camera.x_pixels
|
||||
self.synthetic_camera.set_binning(binning_string="2x2")
|
||||
assert self.synthetic_camera.x_binning == 2
|
||||
assert self.synthetic_camera.y_binning == 2
|
||||
assert type(self.synthetic_camera.x_binning) == int
|
||||
assert type(self.synthetic_camera.y_binning) == int
|
||||
assert self.synthetic_camera.x_pixels == x_pixels / 2
|
||||
|
||||
def test_synthetic_camera_initialize_image_series(self):
|
||||
self.synthetic_camera.initialize_image_series()
|
||||
assert self.synthetic_camera.num_of_frame == 100
|
||||
assert self.synthetic_camera.data_buffer is None
|
||||
assert self.synthetic_camera.current_frame_idx == 0
|
||||
assert self.synthetic_camera.pre_frame_idx == 0
|
||||
assert self.synthetic_camera.is_acquiring is True
|
||||
|
||||
def test_synthetic_camera_close_image_series(self):
|
||||
self.synthetic_camera.close_image_series()
|
||||
assert self.synthetic_camera.pre_frame_idx == 0
|
||||
assert self.synthetic_camera.current_frame_idx == 0
|
||||
assert self.synthetic_camera.is_acquiring is False
|
||||
|
||||
def test_synthetic_camera_acquire_images(self):
|
||||
import random
|
||||
from navigate.model.concurrency.concurrency_tools import SharedNDArray
|
||||
|
||||
number_of_frames = 100
|
||||
data_buffer = [
|
||||
SharedNDArray(shape=(2048, 2048), dtype="uint16")
|
||||
for i in range(number_of_frames)
|
||||
]
|
||||
|
||||
self.synthetic_camera.initialize_image_series(data_buffer, number_of_frames)
|
||||
|
||||
assert self.synthetic_camera.is_acquiring is True, "should be acquring"
|
||||
|
||||
frame_idx = 0
|
||||
|
||||
for i in range(10):
|
||||
frame_num = random.randint(1, 30)
|
||||
for j in range(frame_num):
|
||||
self.synthetic_camera.generate_new_frame()
|
||||
frames = self.synthetic_camera.get_new_frame()
|
||||
|
||||
assert len(frames) == frame_num, "frame number isn't right!"
|
||||
assert frames[0] == frame_idx, "frame idx isn't right!"
|
||||
|
||||
frame_idx = (frame_idx + frame_num) % number_of_frames
|
||||
|
||||
self.synthetic_camera.close_image_series()
|
||||
assert (
|
||||
self.synthetic_camera.is_acquiring is False
|
||||
), "is_acquiring should be False"
|
||||
|
||||
def test_synthetic_camera_set_roi(self):
|
||||
self.synthetic_camera.set_ROI()
|
||||
assert self.synthetic_camera.x_pixels == 2048
|
||||
assert self.synthetic_camera.y_pixels == 2048
|
||||
self.synthetic_camera.set_ROI(roi_height=500, roi_width=700)
|
||||
assert self.synthetic_camera.x_pixels == 700
|
||||
assert self.synthetic_camera.y_pixels == 500
|
||||
618
test/model/devices/camera/test_daheng.py
Normal file
618
test/model/devices/camera/test_daheng.py
Normal file
@@ -0,0 +1,618 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
# Standard Library Imports
|
||||
import pytest
|
||||
from typing import Tuple
|
||||
from unittest.mock import patch, MagicMock
|
||||
import logging
|
||||
import io
|
||||
|
||||
# Third Party Imports
|
||||
import numpy as np
|
||||
from numpy.testing import assert_array_equal
|
||||
|
||||
# Local Imports
|
||||
from navigate.model.utils.exceptions import UserVisibleException
|
||||
|
||||
try:
|
||||
import gxipy # noqa: F401
|
||||
except:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_daheng_module():
|
||||
fake_gx = MagicMock()
|
||||
with patch.dict("sys.modules", {"gxipy": fake_gx}):
|
||||
yield fake_gx
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_daheng_sdk():
|
||||
"""Patch Daheng SDK (gxipy) and return mocked device + subsystems."""
|
||||
with patch(
|
||||
"navigate.model.devices.camera.daheng.gx.DeviceManager"
|
||||
) as mock_device_manager:
|
||||
device = _create_mock_device(mock_device_manager)
|
||||
feature_control = _create_mock_feature_control()
|
||||
data_stream, raw_image = _create_mock_image_pipeline()
|
||||
|
||||
_attach_mock_interfaces_to_device(device, feature_control, data_stream)
|
||||
|
||||
yield {
|
||||
"device": device,
|
||||
"feature_control": feature_control,
|
||||
"data_stream": data_stream,
|
||||
"raw_image": raw_image,
|
||||
}
|
||||
|
||||
|
||||
def _attach_mock_interfaces_to_device(device, feature_control, data_stream):
|
||||
"""
|
||||
Attach core SDK interfaces to a mock Daheng device.
|
||||
|
||||
In the gxipy SDK, once a device is opened, it provides two key subsystems:
|
||||
|
||||
- feature_control (via get_remote_device_feature_control()):
|
||||
This is the interface for configuring camera hardware settings such as
|
||||
exposure time, gain, trigger mode, binning, resolution, and ROI.
|
||||
The SDK exposes these through feature "objects" with .get()/.set() methods.
|
||||
|
||||
- data_stream (accessed as a property):
|
||||
This handles actual image acquisition. It provides methods to start/stop
|
||||
streaming and to retrieve frames via .snap_image().
|
||||
|
||||
This function sets up mock versions of those subsystems to a MagicMock-based
|
||||
device object, enabling testable interaction without requiring physical hardware.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
device : MagicMock
|
||||
The mocked gxipy.Device object to configure.
|
||||
|
||||
feature_control : MagicMock
|
||||
Mocked feature control interface to simulate hardware parameters.
|
||||
|
||||
data_stream : MagicMock
|
||||
Mocked data stream interface to simulate image capture.
|
||||
"""
|
||||
device.get_remote_device_feature_control.return_value = feature_control
|
||||
device.data_stream = data_stream
|
||||
|
||||
|
||||
def _create_mock_device(mock_device_manager) -> MagicMock:
|
||||
"""
|
||||
Create a fake Daheng device and configure DeviceManager return values.
|
||||
|
||||
This sets up:
|
||||
- update_device_list() -> None
|
||||
- get_device_list() -> list with one fake serial number
|
||||
- open_device_by_index(i) -> the mock device
|
||||
|
||||
Returns
|
||||
-------
|
||||
MagicMock
|
||||
A fake Daheng device object.
|
||||
"""
|
||||
mock_device = MagicMock(name="FakeDevice")
|
||||
|
||||
mock_device_manager.return_value.update_device_list.return_value = None
|
||||
mock_device_manager.return_value.get_device_list.return_value = [{"sn": "1234"}]
|
||||
mock_device_manager.return_value.open_device_by_index.return_value = mock_device
|
||||
|
||||
return mock_device
|
||||
|
||||
|
||||
def _create_mock_feature_control() -> MagicMock:
|
||||
"""
|
||||
Create a fake FeatureControl interface that simulates Daheng camera settings.
|
||||
|
||||
Simulates:
|
||||
- get_string_feature("DeviceSerialNumber").get() -> "1234"
|
||||
- get_int_feature(...).get() -> 2048
|
||||
- get_enum_feature(...).set(...) -> None
|
||||
|
||||
Returns
|
||||
-------
|
||||
MagicMock
|
||||
A mock feature_control object.
|
||||
"""
|
||||
mock_feature_control = MagicMock(name="FakeFeatureControl")
|
||||
mock_feature_control.get_string_feature.return_value.get.return_value = "1234"
|
||||
mock_feature_control.get_int_feature.side_effect = lambda name: MagicMock(
|
||||
get=MagicMock(return_value=2048)
|
||||
)
|
||||
mock_feature_control.get_enum_feature.return_value.set.return_value = None
|
||||
return mock_feature_control
|
||||
|
||||
|
||||
def _create_mock_image_pipeline() -> Tuple[MagicMock, MagicMock]:
|
||||
"""
|
||||
Create a mocked data stream and raw image pipeline.
|
||||
|
||||
Simulates:
|
||||
- data_stream.snap_image() -> mock_raw_image
|
||||
- raw_image.get_numpy_array() -> np.zeros((2048, 2048), dtype=np.uint16)
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[MagicMock, MagicMock]
|
||||
(mock_data_stream, mock_raw_image)
|
||||
"""
|
||||
stream = MagicMock(name="FakeDataStream")
|
||||
image = MagicMock(name="FakeRawImage")
|
||||
stream.snap_image.return_value = image
|
||||
image.get_numpy_array.return_value = np.zeros((2048, 2048), dtype=np.uint16)
|
||||
return stream, image
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def camera(mock_daheng_sdk):
|
||||
"""
|
||||
Return a DahengCamera instance connected via mocked SDK.
|
||||
|
||||
The mock_daheng_sdk fixture is required to patch the SDK and simulate hardware.
|
||||
It's not used directly in this function, but must be active when connect() is called.
|
||||
"""
|
||||
# Use the patched classmethod to simulate SDK connection.
|
||||
# This is where mock_daheng_sdk enters.
|
||||
from navigate.model.devices.camera.daheng import DahengCamera
|
||||
|
||||
# Minimal config object matching Navigate's expected schema
|
||||
config = {
|
||||
"configuration": {
|
||||
"microscopes": {
|
||||
"test_scope": {
|
||||
"camera": {
|
||||
"hardware": {
|
||||
"serial_number": "1234",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
camera = DahengCamera(
|
||||
microscope_name="test_scope",
|
||||
device_connection=mock_daheng_sdk["device"],
|
||||
configuration=config,
|
||||
)
|
||||
|
||||
# Initialize and return the test camera instance
|
||||
return camera
|
||||
|
||||
|
||||
@patch("navigate.model.devices.camera.daheng.gx.DeviceManager")
|
||||
def test_connect_without_serial(mock_dm):
|
||||
"""
|
||||
Test that DahengCamera.connect() connects to the first camera if no serial number is provided.
|
||||
|
||||
This uses patching to replace the actual DeviceManager with a mock,
|
||||
simulating a single connected camera with serial '1234'.
|
||||
"""
|
||||
|
||||
mock_device = MagicMock()
|
||||
|
||||
# Simulate SDK returning one device with serial '1234'
|
||||
mock_dm.return_value.get_device_list.return_value = [{"sn": "1234"}]
|
||||
|
||||
# Simulate opening that device returns our mock_device
|
||||
mock_dm.return_value.open_device_by_index.return_value = mock_device
|
||||
|
||||
# Call connect without specifying serial number
|
||||
from navigate.model.devices.camera.daheng import DahengCamera
|
||||
|
||||
device = DahengCamera.connect()
|
||||
|
||||
# Verify that we get the mocked device object
|
||||
assert device == mock_device
|
||||
|
||||
|
||||
@patch("navigate.model.devices.camera.daheng.gx.DeviceManager")
|
||||
def test_connect_invalid_serial_raises(mock_dm):
|
||||
"""
|
||||
Test that DahengCamera.connect() raises a UserVisibleException if the
|
||||
specified serial number does not match any connected camera.
|
||||
|
||||
This verifies the fallback else-block logic in the for-loop of connect().
|
||||
"""
|
||||
# Simulate one connected device with serial '1234'
|
||||
mock_dm.return_value.get_device_list.return_value = [{"sn": "1234"}]
|
||||
|
||||
from navigate.model.devices.camera.daheng import DahengCamera
|
||||
|
||||
# Attempt to connect with a non-existent serial number
|
||||
with pytest.raises(
|
||||
UserVisibleException, match="Daheng camera with serial INVALID_SN not found."
|
||||
):
|
||||
DahengCamera.connect(serial_number="INVALID_SN")
|
||||
|
||||
|
||||
def test_str(camera):
|
||||
"""
|
||||
Test the string representation of the DahengCamera object.
|
||||
|
||||
Ensures that the __str__ method includes the camera model name,
|
||||
serial number, and connection status in the returned string.
|
||||
"""
|
||||
result = str(camera)
|
||||
assert "MER2_1220_32U3C Camera" in result
|
||||
assert "Serial: 1234" in result
|
||||
assert "Connected" in result
|
||||
|
||||
|
||||
def test_camera_connected(camera):
|
||||
"""
|
||||
Test that the camera object reports a connected state after setup.
|
||||
|
||||
This relies on the 'camera' fixture, which internally calls DahengCamera.connect()
|
||||
and initializes the SDK state. Verifies that is_connected is True and the
|
||||
device serial number is correctly cached.
|
||||
"""
|
||||
assert camera.is_connected
|
||||
assert camera.device_serial_number == "1234"
|
||||
|
||||
|
||||
def test_disconnect_clears_state(camera):
|
||||
"""
|
||||
Test that disconnect() resets internal state and marks camera as disconnected.
|
||||
"""
|
||||
camera.disconnect()
|
||||
assert camera.device is None
|
||||
assert camera.feature_control is None
|
||||
assert camera.is_connected is False
|
||||
assert camera.serial_number == "UNKNOWN"
|
||||
|
||||
|
||||
def test_set_exposure_time(camera):
|
||||
"""
|
||||
Test that set_exposure_time() updates internal state and calls correct SDK feature.
|
||||
"""
|
||||
camera.set_exposure_time(0.1)
|
||||
|
||||
# Internal caching of exposure time (in seconds)
|
||||
assert camera._exposure_time == 0.1
|
||||
|
||||
# Verifies that the SDK was told to get the 'ExposureTime' feature
|
||||
camera.feature_control.get_float_feature.assert_called_with("ExposureTime")
|
||||
|
||||
|
||||
def test_set_gain(camera):
|
||||
"""
|
||||
Ensure set_gain() calls the Gain feature with the expected float value.
|
||||
"""
|
||||
camera.set_gain(5.0)
|
||||
camera.feature_control.get_float_feature.assert_called_with("Gain")
|
||||
camera.feature_control.get_float_feature.return_value.set.assert_called_with(5.0)
|
||||
|
||||
|
||||
def test_set_binning(camera):
|
||||
"""
|
||||
Test that set_binning() parses input string, updates binning values,
|
||||
and accesses correct SDK features.
|
||||
"""
|
||||
result = camera.set_binning("2x2")
|
||||
assert result is True
|
||||
assert camera.x_binning == 2
|
||||
assert camera.y_binning == 2
|
||||
|
||||
# Check that the SDK was asked for the correct feature names at least once
|
||||
camera.feature_control.get_int_feature.assert_any_call("BinningHorizontal")
|
||||
camera.feature_control.get_int_feature.assert_any_call("BinningVertical")
|
||||
|
||||
|
||||
def test_set_invalid_ROI(camera):
|
||||
"""
|
||||
Test that set_ROI() returns False and logs a warning when given invalid dimensions.
|
||||
"""
|
||||
# Set invalid ROI parameters
|
||||
roi_width = 9000
|
||||
roi_height = 2048
|
||||
center_x = 1000
|
||||
center_y = 1000
|
||||
|
||||
logger = logging.getLogger("model")
|
||||
logger.propagate = False # prevent sending logs to root CLI handler
|
||||
|
||||
stream = io.StringIO()
|
||||
handler = logging.StreamHandler(stream)
|
||||
logger.addHandler(handler)
|
||||
|
||||
camera.stop_acquisition()
|
||||
handler.flush()
|
||||
result = camera.set_ROI(
|
||||
roi_width=roi_width, roi_height=roi_height, center_x=center_x, center_y=center_y
|
||||
)
|
||||
assert result is False
|
||||
assert f"Invalid ROI dimensions: {roi_width}x{roi_height}" in stream.getvalue()
|
||||
logger.removeHandler(handler)
|
||||
|
||||
|
||||
def test_snap_image_returns_numpy_array(camera):
|
||||
"""
|
||||
Test that snap_image() calls the SDK and returns a NumPy array.
|
||||
|
||||
The camera fixture uses the mock_daheng_sdk fixture to simulate:
|
||||
- A data stream whose snap_image() returns a fake image object
|
||||
- An image object whose get_numpy_array() returns np.ndarray representing a fake image (zeros)
|
||||
|
||||
"""
|
||||
result = camera.snap_image()
|
||||
expected = np.zeros((2048, 2048), dtype=np.uint16)
|
||||
assert_array_equal(result, expected)
|
||||
camera.data_stream.snap_image.assert_called_once()
|
||||
|
||||
|
||||
def test_snap_software_triggered_invalid_config(camera):
|
||||
"""
|
||||
Test that snap_software_triggered() raises if trigger config is invalid.
|
||||
|
||||
This mocks the 'TriggerMode' and 'TriggerSource' enum features to return
|
||||
incorrect values ('OFF' and 'HARDWARE'), and verifies that the method
|
||||
raises a UserVisibleException with a helpful message.
|
||||
"""
|
||||
# Override trigger mode/source with bad values
|
||||
mock_enum_feature = MagicMock()
|
||||
mock_enum_feature.get_current_entry.return_value.get_symbolic.side_effect = [
|
||||
"OFF",
|
||||
"HARDWARE",
|
||||
]
|
||||
camera.feature_control.get_enum_feature.return_value = mock_enum_feature
|
||||
|
||||
with pytest.raises(
|
||||
UserVisibleException, match="TriggerMode='ON' and TriggerSource='SOFTWARE'"
|
||||
):
|
||||
camera.snap_software_triggered()
|
||||
|
||||
|
||||
def test_send_software_trigger(camera):
|
||||
"""
|
||||
Test that send_software_trigger() calls the correct Daheng SDK command.
|
||||
|
||||
Verifies that the camera issues a 'TriggerSoftware' command via the
|
||||
command feature interface and that send_command() is called exactly once.
|
||||
"""
|
||||
camera.send_software_trigger()
|
||||
camera.feature_control.get_command_feature.assert_called_with("TriggerSoftware")
|
||||
camera.feature_control.get_command_feature.return_value.send_command.assert_called_once()
|
||||
|
||||
|
||||
def test_set_trigger_mode(camera):
|
||||
"""
|
||||
Test that set_trigger_mode() calls the correct enum feature and sets it to 'ON'.
|
||||
"""
|
||||
camera.set_trigger_mode("ON")
|
||||
camera.feature_control.get_enum_feature.assert_called_with("TriggerMode")
|
||||
camera.feature_control.get_enum_feature.return_value.set.assert_called_with("ON")
|
||||
|
||||
|
||||
def test_set_trigger_source(camera):
|
||||
"""
|
||||
Test that set_trigger_source() selects the correct SDK enum feature and sets it to 'LINE1'.
|
||||
|
||||
'LINE1' refers to a physical input pin used for hardware triggering,
|
||||
typically driven by a DAQ, microcontroller, or timing controller.
|
||||
"""
|
||||
camera.set_trigger_source("LINE1")
|
||||
camera.feature_control.get_enum_feature.assert_called_with("TriggerSource")
|
||||
camera.feature_control.get_enum_feature.return_value.set.assert_called_with("LINE1")
|
||||
|
||||
|
||||
def test_initialize_and_start_acquisition(camera):
|
||||
"""
|
||||
Test that initialize_image_series and start_acquisition correctly
|
||||
update internal state and interact with the SDK.
|
||||
"""
|
||||
# Create a fake image buffer with shape matching camera resolution
|
||||
fake_buffer = [MagicMock(name=f"Frame{i}") for i in range(5)]
|
||||
number_of_frames = 5
|
||||
|
||||
# Initialize acquisition
|
||||
camera.initialize_image_series(
|
||||
data_buffer=fake_buffer, number_of_frames=number_of_frames
|
||||
)
|
||||
|
||||
# Assert acquisition is marked as started
|
||||
assert camera.is_acquiring is True
|
||||
assert camera._number_of_frames == number_of_frames
|
||||
assert camera._frames_received == 0
|
||||
assert camera._data_buffer == fake_buffer
|
||||
|
||||
# Start the acquisition and verify SDK interaction
|
||||
camera.data_stream.start_stream.assert_called_once()
|
||||
camera.feature_control.get_command_feature.assert_called_with("AcquisitionStart")
|
||||
camera.feature_control.get_command_feature.return_value.send_command.assert_called_once()
|
||||
|
||||
|
||||
def test_initialize_start_and_receive_image(camera):
|
||||
"""
|
||||
Test full acquisition flow:
|
||||
- initialize_image_series()
|
||||
- start_acquisition()
|
||||
- get_new_frame() to simulate image reception
|
||||
|
||||
Verifies that the SDK methods are called, internal state is updated,
|
||||
and image data is written to the circular buffer.
|
||||
"""
|
||||
fake_buffer = [MagicMock(name=f"Frame{i}") for i in range(3)]
|
||||
number_of_frames = 3
|
||||
|
||||
camera.initialize_image_series(
|
||||
data_buffer=fake_buffer, number_of_frames=number_of_frames
|
||||
)
|
||||
|
||||
# Simulate receiving frames
|
||||
for i in range(3):
|
||||
frame_indices = camera.get_new_frame()
|
||||
assert frame_indices == [i]
|
||||
fake_buffer[i].__setitem__.assert_called() # Simulates [:, :] = image_data
|
||||
|
||||
# Circular buffer check
|
||||
wraparound = camera.get_new_frame()
|
||||
assert wraparound == [0]
|
||||
|
||||
|
||||
def test_stop_acquisition(camera):
|
||||
"""
|
||||
Test that stop_acquisition() stops both the command and data stream,
|
||||
clears acquisition state, and accesses the correct command feature.
|
||||
"""
|
||||
# Pretend acquisition is running
|
||||
camera.is_acquiring = True
|
||||
|
||||
# Run method
|
||||
camera.stop_acquisition()
|
||||
|
||||
# Ensure the correct SDK command was accessed and triggered
|
||||
camera.feature_control.get_command_feature.assert_called_with("AcquisitionStop")
|
||||
camera.feature_control.get_command_feature.return_value.send_command.assert_called_once()
|
||||
|
||||
# Ensure the data stream was stopped
|
||||
camera.data_stream.stop_stream.assert_called_once()
|
||||
|
||||
# Verify internal state was updated
|
||||
assert camera.is_acquiring is False
|
||||
|
||||
|
||||
def test_stop_acquisition_when_disconnected(camera):
|
||||
"""
|
||||
Test that stop_acquisition() logs a warning and does not raise
|
||||
when called on a disconnected camera.
|
||||
"""
|
||||
camera.is_connected = False
|
||||
|
||||
logger = logging.getLogger("model")
|
||||
logger.propagate = False # prevent sending logs to root CLI handler
|
||||
|
||||
stream = io.StringIO()
|
||||
handler = logging.StreamHandler(stream)
|
||||
logger.addHandler(handler)
|
||||
|
||||
camera.stop_acquisition()
|
||||
handler.flush()
|
||||
|
||||
assert "not connected" in stream.getvalue()
|
||||
|
||||
logger.removeHandler(handler)
|
||||
|
||||
|
||||
def test_set_sensor_mode_logs(camera):
|
||||
"""
|
||||
Test that set_sensor_mode() logs a warning for unsupported modes.
|
||||
|
||||
If an invalid mode is specified, the camera will be set to Normal
|
||||
mode (using global shutter).
|
||||
"""
|
||||
camera.set_sensor_mode("InvalidModeName")
|
||||
camera.device.SensorShutterMode.set.assert_called_with(0)
|
||||
assert camera._scan_mode == 0
|
||||
|
||||
|
||||
def test_snap_software_triggered_success(camera):
|
||||
"""
|
||||
Test that snap_software_triggered() works when trigger config is correct.
|
||||
|
||||
Mocks TriggerMode='ON' and TriggerSource='SOFTWARE', verifies that the
|
||||
method sends a software trigger, captures an image, and returns the result.
|
||||
|
||||
Uses side_effect to return two enum values from a shared enum feature mock.
|
||||
"""
|
||||
# Patch enum feature to simulate correct trigger mode and source
|
||||
mock_enum_feature = MagicMock()
|
||||
|
||||
# First call to get_symbolic() returns 'ON', second returns 'SOFTWARE'
|
||||
mock_enum_feature.get_current_entry.return_value.get_symbolic.side_effect = [
|
||||
"ON",
|
||||
"SOFTWARE",
|
||||
]
|
||||
camera.feature_control.get_enum_feature.return_value = mock_enum_feature
|
||||
|
||||
# Snap image - behind the scenes, this calls data_stream.snap_image() which
|
||||
# is mocked during setup to return a fake image whose get_numpy_array() method
|
||||
# returns np.ndarray representing a fake image.
|
||||
result = camera.snap_software_triggered()
|
||||
|
||||
expected = np.zeros((2048, 2048), dtype=np.uint16)
|
||||
assert_array_equal(result, expected)
|
||||
|
||||
# Ensure the correct trigger command was issued via SDK
|
||||
camera.feature_control.get_command_feature.return_value.send_command.assert_called_with()
|
||||
|
||||
|
||||
def test_get_new_frame(camera):
|
||||
"""
|
||||
Test that get_new_frame() returns correct buffer index in sequence,
|
||||
and wraps around when the number of received frames exceeds the buffer length.
|
||||
|
||||
This simulates a circular buffer behavior across multiple frames.
|
||||
"""
|
||||
number_of_images = 3
|
||||
buffer = [MagicMock() for _ in range(number_of_images)]
|
||||
|
||||
# Initialize image acquisition
|
||||
camera.initialize_image_series(
|
||||
data_buffer=buffer, number_of_frames=number_of_images
|
||||
)
|
||||
camera._frames_received = 0
|
||||
|
||||
# First full loop through buffer
|
||||
for i in range(number_of_images):
|
||||
result = camera.get_new_frame()
|
||||
assert result == [i]
|
||||
|
||||
# Wraparound: next result should start from 0 again
|
||||
result = camera.get_new_frame()
|
||||
assert result == [0]
|
||||
|
||||
result = camera.get_new_frame()
|
||||
assert result == [1]
|
||||
|
||||
|
||||
def test_close_image_series(camera):
|
||||
"""
|
||||
Test that close_image_series() stops acquisition and clears buffer state.
|
||||
|
||||
This ensures the SDK stream is stopped and internal flags like
|
||||
is_acquiring and _data_buffer are reset properly.
|
||||
|
||||
The data_stream is mocked in the camera fixture (via mock_daheng_sdk).
|
||||
"""
|
||||
camera.is_acquiring = True
|
||||
camera._data_buffer = [MagicMock(), MagicMock()] # Simulate buffered frames
|
||||
|
||||
camera.close_image_series()
|
||||
|
||||
# Acquisition state should be cleared
|
||||
assert camera.is_acquiring is False
|
||||
assert camera._data_buffer == None
|
||||
|
||||
# SDK stream should be stopped
|
||||
camera.data_stream.stop_stream.assert_called_once()
|
||||
379
test/model/devices/camera/test_hamamatsu.py
Normal file
379
test/model/devices/camera/test_hamamatsu.py
Normal file
@@ -0,0 +1,379 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
# Third Party Imports
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.hardware
|
||||
@pytest.fixture(scope="module")
|
||||
def prepare_cameras(dummy_model):
|
||||
from navigate.model.devices.APIs.hamamatsu.HamamatsuAPI import DCAM, camReg
|
||||
from navigate.model.devices.camera.hamamatsu import HamamatsuOrca
|
||||
|
||||
def start_camera(idx=0):
|
||||
# open camera
|
||||
for i in range(10):
|
||||
assert camReg.numCameras == idx
|
||||
try:
|
||||
camera = DCAM(idx)
|
||||
if camera.get_camera_handler() != 0:
|
||||
break
|
||||
camera.dev_close()
|
||||
except Exception:
|
||||
continue
|
||||
camera = None
|
||||
return camera
|
||||
|
||||
model = dummy_model
|
||||
|
||||
temp = {}
|
||||
for microscope_name in model.configuration["configuration"]["microscopes"].keys():
|
||||
serial_number = model.configuration["configuration"]["microscopes"][
|
||||
microscope_name
|
||||
]["camera"]["hardware"]["serial_number"]
|
||||
temp[str(serial_number)] = microscope_name
|
||||
|
||||
camera_connections = {}
|
||||
|
||||
camera = start_camera()
|
||||
for i in range(camReg.maxCameras):
|
||||
if i > 0:
|
||||
camera = start_camera(i)
|
||||
if str(camera._serial_number) in temp:
|
||||
microscope_name = temp[str(camera._serial_number)]
|
||||
camera = HamamatsuOrca(microscope_name, camera, model.configuration)
|
||||
camera_connections[microscope_name] = camera
|
||||
|
||||
yield camera_connections
|
||||
|
||||
# close all the cameras
|
||||
for k in camera_connections:
|
||||
camera_connections[k].camera_controller.dev_close()
|
||||
|
||||
|
||||
@pytest.mark.hardware
|
||||
class TestHamamatsuOrca:
|
||||
"""Unit Test for HamamamatsuOrca Class"""
|
||||
|
||||
model = None
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _prepare_test(self, dummy_model, prepare_cameras):
|
||||
self.num_of_tests = 10
|
||||
self.model = dummy_model
|
||||
self.cameras = prepare_cameras
|
||||
|
||||
self.microscope_name = self.model.configuration["experiment"][
|
||||
"MicroscopeState"
|
||||
]["microscope_name"]
|
||||
self.camera = self.cameras[self.microscope_name]
|
||||
|
||||
def is_in_range(self, value, target, precision=100):
|
||||
target_min = target - target / precision
|
||||
target_max = target + target / precision
|
||||
return value > target_min and value < target_max
|
||||
|
||||
def test_hamamatsu_camera_attributes(self):
|
||||
from navigate.model.devices.camera.hamamatsu import HamamatsuOrca
|
||||
|
||||
attributes = dir(HamamatsuOrca)
|
||||
desired_attributes = [
|
||||
"serial_number",
|
||||
"report_settings",
|
||||
"close_camera",
|
||||
"set_sensor_mode",
|
||||
"set_readout_direction",
|
||||
"calculate_light_sheet_exposure_time",
|
||||
"calculate_readout_time",
|
||||
"set_exposure_time",
|
||||
"set_line_interval",
|
||||
"set_binning",
|
||||
"set_ROI",
|
||||
"initialize_image_series",
|
||||
"close_image_series",
|
||||
"get_new_frame",
|
||||
]
|
||||
|
||||
for da in desired_attributes:
|
||||
assert da in attributes
|
||||
|
||||
def test_init_camera(self):
|
||||
for microscope_name in self.model.configuration["configuration"][
|
||||
"microscopes"
|
||||
].keys():
|
||||
|
||||
camera = self.cameras[microscope_name]
|
||||
|
||||
assert camera is not None, f"Should start the camera {microscope_name}"
|
||||
|
||||
camera_controller = camera.camera_controller
|
||||
camera_configs = self.model.configuration["configuration"]["microscopes"][
|
||||
microscope_name
|
||||
]["camera"]
|
||||
|
||||
# serial number
|
||||
assert str(camera_controller._serial_number) == str(
|
||||
camera_configs["hardware"]["serial_number"]
|
||||
), f"the camera serial number isn't right for {microscope_name}!"
|
||||
assert str(camera.serial_number) == str(
|
||||
camera_configs["hardware"]["serial_number"]
|
||||
), f"the camera serial number isn't right for {microscope_name}!"
|
||||
|
||||
# verify camera is initialized with the attributes from configuration.yaml
|
||||
parameters = [
|
||||
"defect_correct_mode",
|
||||
"readout_speed",
|
||||
"trigger_active",
|
||||
"trigger_mode",
|
||||
"trigger_polarity",
|
||||
"trigger_source",
|
||||
]
|
||||
for parameter in parameters:
|
||||
value = camera_controller.get_property_value(parameter)
|
||||
assert value == camera_configs[parameter]
|
||||
|
||||
# sensor mode
|
||||
sensor_mode = camera_controller.get_property_value("sensor_mode")
|
||||
expected_value = 1 if camera_configs["sensor_mode"] == "Normal" else 12
|
||||
assert sensor_mode == expected_value, "Sensor mode isn't right!"
|
||||
|
||||
# exposure time
|
||||
exposure_time = camera_controller.get_property_value("exposure_time")
|
||||
assert self.is_in_range(
|
||||
exposure_time, camera_configs["exposure_time"] / 1000, 10
|
||||
), "Exposure time isn't right!"
|
||||
|
||||
# binning
|
||||
binning = camera_controller.get_property_value("binning")
|
||||
assert int(binning) == int(
|
||||
camera_configs["binning"][0]
|
||||
), "Binning isn't right!"
|
||||
|
||||
# image width and height
|
||||
width = camera_controller.get_property_value("image_width")
|
||||
assert width == camera_configs["x_pixels"], "image width isn't right"
|
||||
height = camera_controller.get_property_value("image_height")
|
||||
assert height == camera_configs["y_pixels"], "image height isn't right"
|
||||
|
||||
def test_set_sensor_mode(self):
|
||||
modes = {"Normal": 1, "Light-Sheet": 12, "RandomMode": None}
|
||||
for mode in modes:
|
||||
pre_value = self.camera.camera_controller.get_property_value("sensor_mode")
|
||||
self.camera.set_sensor_mode(mode)
|
||||
value = self.camera.camera_controller.get_property_value("sensor_mode")
|
||||
if modes[mode] is not None:
|
||||
assert value == modes[mode], f"sensor mode {mode} isn't right!"
|
||||
else:
|
||||
assert value == pre_value, "sensor mode shouldn't be set!"
|
||||
|
||||
def test_set_readout_direction(self):
|
||||
readout_directions = {"Top-to-Bottom": 1, "Bottom-to-Top": 2}
|
||||
for direction in readout_directions:
|
||||
self.camera.set_readout_direction(direction)
|
||||
value = self.camera.camera_controller.get_property_value(
|
||||
"readout_direction"
|
||||
)
|
||||
assert (
|
||||
value == readout_directions[direction]
|
||||
), f"readout direction setting isn't right for {direction}"
|
||||
|
||||
# def test_calculate_readout_time(self):
|
||||
# pass
|
||||
|
||||
def test_set_exposure_time(self):
|
||||
import random
|
||||
|
||||
modes_dict = {
|
||||
"Normal": 10000,
|
||||
"Light-Sheet": 20,
|
||||
}
|
||||
for mode in modes_dict:
|
||||
self.camera.set_sensor_mode(mode)
|
||||
for i in range(self.num_of_tests):
|
||||
exposure_time = random.randint(1, modes_dict[mode])
|
||||
self.camera.set_exposure_time(exposure_time / 1000)
|
||||
value = self.camera.camera_controller.get_property_value(
|
||||
"exposure_time"
|
||||
)
|
||||
assert self.is_in_range(
|
||||
value, exposure_time / 1000, 10
|
||||
), f"exposure time({exposure_time}) isn't right!"
|
||||
self.camera.set_sensor_mode("Normal")
|
||||
|
||||
def test_set_line_interval(self):
|
||||
import random
|
||||
|
||||
self.camera.set_sensor_mode("Light-Sheet")
|
||||
for i in range(self.num_of_tests):
|
||||
line_interval = random.random() / 10.0
|
||||
r = self.camera.set_line_interval(line_interval)
|
||||
if r is True:
|
||||
value = self.camera.camera_controller.get_property_value(
|
||||
"internal_line_interval"
|
||||
)
|
||||
assert self.is_in_range(
|
||||
value, line_interval
|
||||
), f"line interval {line_interval} isn't right! {value}"
|
||||
self.camera.set_sensor_mode("Normal")
|
||||
|
||||
def test_set_binning(self):
|
||||
import random
|
||||
|
||||
binning_dict = {
|
||||
"1x1": 1,
|
||||
"2x2": 2,
|
||||
"4x4": 4,
|
||||
# '8x8': 8,
|
||||
# '16x16': 16,
|
||||
# '1x2': 102,
|
||||
# '2x4': 204
|
||||
}
|
||||
for binning_string in binning_dict:
|
||||
self.camera.set_binning(binning_string)
|
||||
value = self.camera.camera_controller.get_property_value("binning")
|
||||
assert (
|
||||
int(value) == binning_dict[binning_string]
|
||||
), f"binning {binning_string} isn't right!"
|
||||
|
||||
for i in range(self.num_of_tests):
|
||||
x = random.randint(1, 20)
|
||||
y = random.randint(1, 20)
|
||||
binning_string = f"{x}x{y}"
|
||||
assert self.camera.set_binning(binning_string) == (
|
||||
binning_string in binning_dict
|
||||
)
|
||||
|
||||
def test_set_ROI(self):
|
||||
import random
|
||||
|
||||
self.camera.set_binning("1x1")
|
||||
width = self.camera.camera_parameters["x_pixels"]
|
||||
height = self.camera.camera_parameters["x_pixels"]
|
||||
w = self.camera.camera_controller.get_property_value("image_width")
|
||||
h = self.camera.camera_controller.get_property_value("image_height")
|
||||
assert width == w, f"maximum width should be the same {width} - {w}"
|
||||
assert height == h, f"maximum height should be the same {height} -{h}"
|
||||
|
||||
for i in range(self.num_of_tests):
|
||||
pre_x, pre_y = self.camera.x_pixels, self.camera.y_pixels
|
||||
x = random.randint(1, self.camera.camera_parameters["x_pixels"])
|
||||
y = random.randint(1, self.camera.camera_parameters["y_pixels"])
|
||||
r = self.camera.set_ROI(y, x)
|
||||
if x % 2 == 1 or y % 2 == 1:
|
||||
assert r is False
|
||||
assert self.camera.x_pixels == pre_x, "width shouldn't be chaged!"
|
||||
assert self.camera.y_pixels == pre_y, "height shouldn't be changed!"
|
||||
else:
|
||||
top = (height - y) / 2
|
||||
bottom = top + y - 1
|
||||
if top % 2 == 1 or bottom % 2 == 0:
|
||||
assert r is False
|
||||
else:
|
||||
assert r is True, (
|
||||
f"try to set{x}x{y}, but get "
|
||||
f"{self.camera.x_pixels}x{self.camera.y_pixels}"
|
||||
)
|
||||
assert (
|
||||
self.camera.x_pixels == x
|
||||
), f"trying to set {x}x{y}. width should be changed to {x}"
|
||||
assert self.camera.y_pixels == y, f"height should be chagned to {y}"
|
||||
|
||||
self.camera.set_ROI(512, 512)
|
||||
assert self.camera.x_pixels == 512
|
||||
assert self.camera.y_pixels == 512
|
||||
|
||||
self.camera.set_ROI(
|
||||
self.camera.camera_parameters["x_pixels"],
|
||||
self.camera.camera_parameters["y_pixels"],
|
||||
)
|
||||
assert self.camera.x_pixels == self.camera.camera_parameters["x_pixels"]
|
||||
assert self.camera.y_pixels == self.camera.camera_parameters["y_pixels"]
|
||||
|
||||
self.camera.set_ROI(
|
||||
self.camera.camera_parameters["x_pixels"] + 100,
|
||||
self.camera.camera_parameters["y_pixels"] + 100,
|
||||
)
|
||||
assert self.camera.x_pixels == self.camera.camera_parameters["x_pixels"]
|
||||
assert self.camera.y_pixels == self.camera.camera_parameters["y_pixels"]
|
||||
|
||||
def test_acquire_image(self):
|
||||
import random
|
||||
import time
|
||||
from navigate.model.concurrency.concurrency_tools import SharedNDArray
|
||||
|
||||
# set software trigger
|
||||
self.camera.camera_controller.set_property_value("trigger_source", 3)
|
||||
|
||||
assert self.camera.is_acquiring is False
|
||||
|
||||
number_of_frames = 100
|
||||
data_buffer = [
|
||||
SharedNDArray(shape=(2048, 2048), dtype="uint16")
|
||||
for i in range(number_of_frames)
|
||||
]
|
||||
|
||||
# initialize without release/close the camera
|
||||
self.camera.initialize_image_series(data_buffer, number_of_frames)
|
||||
assert self.camera.is_acquiring is True
|
||||
|
||||
self.camera.initialize_image_series(data_buffer, number_of_frames)
|
||||
assert self.camera.is_acquiring is True
|
||||
|
||||
exposure_time = self.camera.camera_controller.get_property_value(
|
||||
"exposure_time"
|
||||
)
|
||||
readout_time = self.camera.camera_controller.get_property_value("readout_time")
|
||||
|
||||
for i in range(self.num_of_tests):
|
||||
triggers = random.randint(1, 100)
|
||||
for j in range(triggers):
|
||||
self.camera.camera_controller.fire_software_trigger()
|
||||
time.sleep(exposure_time + readout_time)
|
||||
|
||||
time.sleep(0.01)
|
||||
frames = self.camera.get_new_frame()
|
||||
assert len(frames) == triggers
|
||||
|
||||
self.camera.close_image_series()
|
||||
assert self.camera.is_acquiring is False
|
||||
|
||||
for i in range(self.num_of_tests):
|
||||
self.camera.initialize_image_series(data_buffer, number_of_frames)
|
||||
assert self.camera.is_acquiring is True
|
||||
self.camera.close_image_series()
|
||||
assert self.camera.is_acquiring is False
|
||||
|
||||
# close a closed camera
|
||||
self.camera.close_image_series()
|
||||
self.camera.close_image_series()
|
||||
assert self.camera.is_acquiring is False
|
||||
34
test/model/devices/daq/test_daq_base.py
Normal file
34
test/model/devices/daq/test_daq_base.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from navigate.model.devices.daq.synthetic import SyntheticDAQ
|
||||
from test.model.dummy import DummyModel
|
||||
import numpy as np
|
||||
|
||||
|
||||
def test_initialize_daq():
|
||||
model = DummyModel()
|
||||
SyntheticDAQ(model.configuration)
|
||||
|
||||
|
||||
def test_calculate_all_waveforms():
|
||||
model = DummyModel()
|
||||
daq = SyntheticDAQ(model.configuration)
|
||||
microscope_state = model.configuration["experiment"]["MicroscopeState"]
|
||||
microscope_name = microscope_state["microscope_name"]
|
||||
exposure_times = {
|
||||
k: v["camera_exposure_time"] / 1000
|
||||
for k, v in microscope_state["channels"].items()
|
||||
}
|
||||
sweep_times = {
|
||||
k: 2 * v["camera_exposure_time"] / 1000
|
||||
for k, v in microscope_state["channels"].items()
|
||||
}
|
||||
waveform_dict = daq.calculate_all_waveforms(
|
||||
microscope_name, exposure_times, sweep_times
|
||||
)
|
||||
|
||||
for k, v in waveform_dict.items():
|
||||
channel = microscope_state["channels"][k]
|
||||
if not channel["is_selected"]:
|
||||
continue
|
||||
exposure_time = channel["camera_exposure_time"] / 1000
|
||||
print(k, channel["is_selected"], np.sum(v > 0), exposure_time)
|
||||
assert np.sum(v > 0) == daq.sample_rate * exposure_time
|
||||
78
test/model/devices/daq/test_daq_ni.py
Normal file
78
test/model/devices/daq/test_daq_ni.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
# Standard Library Imports
|
||||
|
||||
# Third Party Imports
|
||||
import pytest
|
||||
|
||||
# Local Imports
|
||||
|
||||
|
||||
@pytest.mark.hardware
|
||||
def test_initialize_daq_ni():
|
||||
from navigate.model.devices.daq.ni import NIDAQ
|
||||
from test.model.dummy import DummyModel
|
||||
|
||||
model = DummyModel()
|
||||
daq = NIDAQ(model.configuration)
|
||||
daq.camera_trigger_task = None
|
||||
|
||||
|
||||
@pytest.mark.hardware
|
||||
def test_daq_ni_functions():
|
||||
from navigate.model.devices.daq.ni import NIDAQ
|
||||
from test.model.dummy import DummyModel
|
||||
|
||||
model = DummyModel()
|
||||
daq = NIDAQ(model.configuration)
|
||||
microscope_name = model.configuration["experiment"]["MicroscopeState"][
|
||||
"microscope_name"
|
||||
]
|
||||
|
||||
funcs = [
|
||||
"enable_microscope",
|
||||
"prepare_acquisition",
|
||||
"run_acquisition",
|
||||
"stop_acquisition",
|
||||
]
|
||||
args = [
|
||||
[microscope_name],
|
||||
[list(daq.waveform_dict.keys())[0]],
|
||||
None,
|
||||
None,
|
||||
]
|
||||
|
||||
for f, a in zip(funcs, args):
|
||||
if a is not None:
|
||||
getattr(daq, f)(*a)
|
||||
else:
|
||||
getattr(daq, f)()
|
||||
40
test/model/devices/daq/test_daq_synthetic.py
Normal file
40
test/model/devices/daq/test_daq_synthetic.py
Normal file
@@ -0,0 +1,40 @@
|
||||
def test_initialize_daq_synthetic():
|
||||
from navigate.model.devices.daq.synthetic import SyntheticDAQ
|
||||
from test.model.dummy import DummyModel
|
||||
|
||||
model = DummyModel()
|
||||
_ = SyntheticDAQ(model.configuration)
|
||||
|
||||
|
||||
def test_synthetic_daq_functions():
|
||||
import random
|
||||
|
||||
from navigate.model.devices.daq.synthetic import SyntheticDAQ
|
||||
from test.model.dummy import DummyModel
|
||||
|
||||
model = DummyModel()
|
||||
daq = SyntheticDAQ(model.configuration)
|
||||
microscope_name = model.configuration["experiment"]["MicroscopeState"][
|
||||
"microscope_name"
|
||||
]
|
||||
|
||||
funcs = [
|
||||
"add_camera",
|
||||
"prepare_acquisition",
|
||||
"run_acquisition",
|
||||
"stop_acquisition",
|
||||
"wait_acquisition_done",
|
||||
]
|
||||
args = [
|
||||
[microscope_name, model.camera[microscope_name]],
|
||||
[f"channel_{random.randint(1, 5)}"],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
]
|
||||
|
||||
for f, a in zip(funcs, args):
|
||||
if a is not None:
|
||||
getattr(daq, f)(*a)
|
||||
else:
|
||||
getattr(daq, f)()
|
||||
141
test/model/devices/filter_wheel/test_asi.py
Normal file
141
test/model/devices/filter_wheel/test_asi.py
Normal file
@@ -0,0 +1,141 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
# Standard Library Imports
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
import time
|
||||
|
||||
# Third Party Imports
|
||||
|
||||
# Local Imports
|
||||
from navigate.model.devices.filter_wheel.asi import ASIFilterWheel
|
||||
|
||||
|
||||
class TestASIFilterWheel(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.speed = 2
|
||||
self.number_of_filter_wheels = 2
|
||||
self.filter_wheel_delay = 0.5
|
||||
self.microscope_name = "mock_filter_wheel"
|
||||
self.mock_configuration = {
|
||||
"configuration": {
|
||||
"microscopes": {
|
||||
"mock_filter_wheel": {
|
||||
"filter_wheel": [
|
||||
{
|
||||
"filter_wheel_delay": self.filter_wheel_delay,
|
||||
"hardware": {
|
||||
"wheel_number": self.number_of_filter_wheels
|
||||
},
|
||||
"available_filters": {
|
||||
"filter1": 0,
|
||||
"filter2": 1,
|
||||
"filter3": 2,
|
||||
"filter4": 3,
|
||||
"filter5": 4,
|
||||
"filter6": 5,
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Mock Device Connection
|
||||
self.mock_device_connection = Mock()
|
||||
self.mock_device_connection.select_filter_wheel()
|
||||
self.mock_device_connection.move_filter_wheel()
|
||||
self.mock_device_connection.move_filter_wheel_to_home()
|
||||
self.mock_device_connection.disconnect_from_serial()
|
||||
self.mock_device_connection.is_open()
|
||||
self.mock_device_connection.is_open.return_value = True
|
||||
|
||||
self.filter_wheel = ASIFilterWheel(
|
||||
microscope_name=self.microscope_name,
|
||||
device_connection=self.mock_device_connection,
|
||||
configuration=self.mock_configuration,
|
||||
device_id=0,
|
||||
)
|
||||
|
||||
def test_init(self):
|
||||
self.assertEqual(self.filter_wheel.filter_wheel, self.mock_device_connection)
|
||||
self.assertEqual(
|
||||
self.filter_wheel.filter_wheel_number, self.number_of_filter_wheels
|
||||
)
|
||||
self.assertEqual(
|
||||
self.filter_wheel.wait_until_done_delay, self.filter_wheel_delay
|
||||
)
|
||||
self.assertEqual(self.filter_wheel.filter_wheel_position, 0)
|
||||
|
||||
def test_init_sends_filter_wheels_to_zeroth_position(self):
|
||||
self.mock_device_connection.select_filter_wheel.assert_called()
|
||||
self.assertEqual(self.filter_wheel.wheel_position, 0)
|
||||
|
||||
def test_filter_change_delay(self):
|
||||
# Current position
|
||||
self.filter_wheel.filter_wheel_position = 0
|
||||
|
||||
# Position to move to
|
||||
filter_to_move_to = "filter4"
|
||||
self.filter_wheel.filter_change_delay(filter_to_move_to)
|
||||
self.assertEqual(self.filter_wheel.wait_until_done_delay, (3 * 0.04))
|
||||
|
||||
def test_set_filter_does_not_exist(self):
|
||||
self.mock_device_connection.reset_mock()
|
||||
with self.assertRaises(ValueError):
|
||||
self.filter_wheel.set_filter("magic")
|
||||
|
||||
def test_set_filter_without_waiting(self):
|
||||
self.mock_device_connection.reset_mock()
|
||||
delta = 4
|
||||
self.filter_wheel.set_filter(
|
||||
list(self.filter_wheel.filter_dictionary.keys())[0]
|
||||
)
|
||||
start_time = time.time()
|
||||
self.filter_wheel.set_filter(
|
||||
list(self.filter_wheel.filter_dictionary.keys())[delta],
|
||||
wait_until_done=False,
|
||||
)
|
||||
actual_duration = time.time() - start_time
|
||||
if_wait_duration = (delta - 1) * 0.04
|
||||
self.assertGreater(if_wait_duration, actual_duration)
|
||||
|
||||
def test_close(self):
|
||||
self.mock_device_connection.reset_mock()
|
||||
self.filter_wheel.close()
|
||||
self.filter_wheel.filter_wheel.move_filter_wheel_to_home.assert_called()
|
||||
self.filter_wheel.filter_wheel.is_open.assert_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
28
test/model/devices/filter_wheel/test_fw_base.py
Normal file
28
test/model/devices/filter_wheel/test_fw_base.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from navigate.model.devices.filter_wheel.synthetic import SyntheticFilterWheel
|
||||
from test.model.dummy import DummyModel
|
||||
|
||||
|
||||
def test_filter_wheel_base_functions():
|
||||
|
||||
model = DummyModel()
|
||||
microscope_name = model.configuration["experiment"]["MicroscopeState"][
|
||||
"microscope_name"
|
||||
]
|
||||
fw = SyntheticFilterWheel(
|
||||
microscope_name=microscope_name,
|
||||
device_connection=None,
|
||||
configuration=model.configuration,
|
||||
device_id=0,
|
||||
)
|
||||
|
||||
filter_dict = model.configuration["configuration"]["microscopes"][microscope_name][
|
||||
"filter_wheel"
|
||||
][0]["available_filters"]
|
||||
|
||||
assert fw.check_if_filter_in_filter_dictionary(list(filter_dict.keys())[0])
|
||||
try:
|
||||
fw.check_if_filter_in_filter_dictionary("not a filter")
|
||||
except ValueError:
|
||||
assert True
|
||||
return
|
||||
assert False
|
||||
55
test/model/devices/filter_wheel/test_fw_synthetic.py
Normal file
55
test/model/devices/filter_wheel/test_fw_synthetic.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only
|
||||
# (subject to the limitations in the disclaimer below)
|
||||
# provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
|
||||
def test_synthetic_filter_wheel_functions():
|
||||
|
||||
from navigate.model.devices.filter_wheel.synthetic import (
|
||||
SyntheticFilterWheel,
|
||||
)
|
||||
from test.model.dummy import DummyModel
|
||||
|
||||
model = DummyModel()
|
||||
microscope_name = model.configuration["experiment"]["MicroscopeState"][
|
||||
"microscope_name"
|
||||
]
|
||||
fw = SyntheticFilterWheel(microscope_name, None, model.configuration, 0)
|
||||
|
||||
funcs = ["set_filter", "close"]
|
||||
args = [["channel_dummy"], None]
|
||||
|
||||
for f, a in zip(funcs, args):
|
||||
if a is not None:
|
||||
getattr(fw, f)(*a)
|
||||
else:
|
||||
getattr(fw, f)()
|
||||
189
test/model/devices/filter_wheel/test_sutter.py
Normal file
189
test/model/devices/filter_wheel/test_sutter.py
Normal file
@@ -0,0 +1,189 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
# Standard Library Imports
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
import time
|
||||
|
||||
# Third Party Imports
|
||||
|
||||
# Local Imports
|
||||
from navigate.model.devices.filter_wheel.sutter import SutterFilterWheel
|
||||
|
||||
|
||||
class TestSutterFilterWheel(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.mock_device_connection = Mock()
|
||||
self.mock_device_connection.read.return_value = b"00"
|
||||
self.mock_device_connection.inWaiting.return_value = 2
|
||||
self.mock_device_connection.write.return_value = None
|
||||
self.mock_device_connection.set_filter()
|
||||
self.mock_device_connection.close()
|
||||
|
||||
self.speed = 2
|
||||
self.number_of_filter_wheels = 2
|
||||
self.microscope_name = "mock_filter_wheel"
|
||||
self.mock_configuration = {
|
||||
"configuration": {
|
||||
"microscopes": {
|
||||
"mock_filter_wheel": {
|
||||
"filter_wheel": [
|
||||
{
|
||||
"hardware": {
|
||||
"wheel_number": self.number_of_filter_wheels
|
||||
},
|
||||
"available_filters": {
|
||||
"filter1": 0,
|
||||
"filter2": 1,
|
||||
"filter3": 2,
|
||||
"filter4": 3,
|
||||
"filter5": 4,
|
||||
"filter6": 5,
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.filter_wheel = SutterFilterWheel(
|
||||
microscope_name=self.microscope_name,
|
||||
device_connection=self.mock_device_connection,
|
||||
configuration=self.mock_configuration,
|
||||
device_id=0,
|
||||
)
|
||||
|
||||
def test_init(self):
|
||||
self.assertEqual(self.filter_wheel.serial, self.mock_device_connection)
|
||||
self.assertEqual(
|
||||
self.filter_wheel.filter_wheel_number, self.number_of_filter_wheels
|
||||
)
|
||||
self.assertEqual(self.filter_wheel.wait_until_done, True)
|
||||
self.assertEqual(self.filter_wheel.read_on_init, True)
|
||||
self.assertEqual(self.filter_wheel.speed, self.speed)
|
||||
|
||||
def test_init_sends_filter_wheels_to_zeroth_position(self):
|
||||
self.mock_device_connection.write.assert_called()
|
||||
self.mock_device_connection.set_filter.assert_called()
|
||||
self.assertEqual(self.filter_wheel.wheel_position, 0)
|
||||
|
||||
def test_filter_wheel_delay(self):
|
||||
for delta in range(6):
|
||||
self.filter_wheel.set_filter(
|
||||
list(self.filter_wheel.filter_dictionary.keys())[0]
|
||||
)
|
||||
self.filter_wheel.set_filter(
|
||||
list(self.filter_wheel.filter_dictionary.keys())[delta]
|
||||
)
|
||||
self.assertEqual(
|
||||
self.filter_wheel.wait_until_done_delay,
|
||||
self.filter_wheel.delay_matrix[self.speed, delta],
|
||||
)
|
||||
|
||||
def test_set_filter_does_not_exist(self):
|
||||
self.mock_device_connection.reset_mock()
|
||||
with self.assertRaises(ValueError):
|
||||
self.filter_wheel.set_filter("magic")
|
||||
|
||||
def test_set_filter_init_not_finished(self):
|
||||
self.mock_device_connection.reset_mock()
|
||||
self.filter_wheel.init_finished = False
|
||||
self.filter_wheel.set_filter(
|
||||
list(self.filter_wheel.filter_dictionary.keys())[2]
|
||||
)
|
||||
self.mock_device_connection.read.assert_called()
|
||||
self.filter_wheel.init_finished = True
|
||||
|
||||
def test_set_filter_init_finished(self):
|
||||
for wait_flag, read_num in [(True, 2), (False, 1)]:
|
||||
self.mock_device_connection.reset_mock()
|
||||
self.filter_wheel.init_finished = True
|
||||
read_count = 0
|
||||
for i in range(6):
|
||||
self.filter_wheel.set_filter(
|
||||
list(self.filter_wheel.filter_dictionary.keys())[i],
|
||||
wait_until_done=wait_flag,
|
||||
)
|
||||
self.mock_device_connection.write.assert_called()
|
||||
self.mock_device_connection.read.assert_called()
|
||||
read_count += read_num
|
||||
assert self.mock_device_connection.read.call_count == read_count
|
||||
|
||||
def test_set_filter_without_waiting(self):
|
||||
self.mock_device_connection.reset_mock()
|
||||
delta = 4
|
||||
self.filter_wheel.set_filter(
|
||||
list(self.filter_wheel.filter_dictionary.keys())[0]
|
||||
)
|
||||
start_time = time.time()
|
||||
self.filter_wheel.set_filter(
|
||||
list(self.filter_wheel.filter_dictionary.keys())[delta],
|
||||
wait_until_done=False,
|
||||
)
|
||||
actual_duration = time.time() - start_time
|
||||
if_wait_duration = self.filter_wheel.delay_matrix[self.speed, delta]
|
||||
self.assertGreater(if_wait_duration, actual_duration)
|
||||
|
||||
def test_read_wrong_number_bytes_returned(self):
|
||||
self.mock_device_connection.reset_mock()
|
||||
# fewer response bytes than expected
|
||||
with self.assertRaises(UserWarning):
|
||||
# in_waiting() returns an integer.
|
||||
self.mock_device_connection.inWaiting.return_value = 1
|
||||
self.filter_wheel.read(num_bytes=10)
|
||||
# more response bytes than expected
|
||||
self.mock_device_connection.inWaiting.return_value = 12
|
||||
self.filter_wheel.read(num_bytes=10)
|
||||
|
||||
def test_read_correct_number_bytes_returned(self):
|
||||
# Mocked device connection expected to return 2 bytes
|
||||
self.mock_device_connection.reset_mock()
|
||||
number_bytes = 2
|
||||
self.mock_device_connection.reset_mock()
|
||||
self.mock_device_connection.inWaiting.return_value = number_bytes
|
||||
returned_bytes = self.filter_wheel.read(num_bytes=number_bytes)
|
||||
self.assertEqual(len(returned_bytes), number_bytes)
|
||||
|
||||
def test_close(self):
|
||||
self.mock_device_connection.reset_mock()
|
||||
self.filter_wheel.close()
|
||||
self.mock_device_connection.close.assert_called()
|
||||
|
||||
def test_exit(self):
|
||||
self.mock_device_connection.reset_mock()
|
||||
del self.filter_wheel
|
||||
self.mock_device_connection.close.assert_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
145
test/model/devices/galvo/test_galvo_base.py
Normal file
145
test/model/devices/galvo/test_galvo_base.py
Normal file
@@ -0,0 +1,145 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import unittest
|
||||
from unittest.mock import MagicMock
|
||||
from navigate.model.devices.galvo.synthetic import SyntheticGalvo
|
||||
from navigate.config import (
|
||||
load_configs,
|
||||
get_configuration_paths,
|
||||
verify_configuration,
|
||||
verify_waveform_constants,
|
||||
)
|
||||
from multiprocessing import Manager
|
||||
import numpy as np
|
||||
|
||||
|
||||
class TestGalvoBase(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
"""Set up the configuration, experiment, etc."""
|
||||
self.manager = Manager()
|
||||
self.parent_dict = {}
|
||||
|
||||
(
|
||||
configuration_path,
|
||||
experiment_path,
|
||||
waveform_constants_path,
|
||||
rest_api_path,
|
||||
waveform_templates_path,
|
||||
gui_configuration_path,
|
||||
multi_positions_path,
|
||||
) = get_configuration_paths()
|
||||
|
||||
self.configuration = load_configs(
|
||||
self.manager,
|
||||
configuration=configuration_path,
|
||||
experiment=experiment_path,
|
||||
waveform_constants=waveform_constants_path,
|
||||
rest_api_config=rest_api_path,
|
||||
waveform_templates=waveform_templates_path,
|
||||
gui_configuration_path=gui_configuration_path,
|
||||
)
|
||||
|
||||
verify_configuration(self.manager, self.configuration)
|
||||
verify_waveform_constants(self.manager, self.configuration)
|
||||
self.microscope_name = "Mesoscale"
|
||||
self.device_connection = MagicMock()
|
||||
galvo_id = 0
|
||||
|
||||
self.galvo = SyntheticGalvo(
|
||||
microscope_name=self.microscope_name,
|
||||
device_connection=self.device_connection,
|
||||
configuration=self.configuration,
|
||||
device_id=galvo_id,
|
||||
)
|
||||
|
||||
self.exposure_times = {"channel_1": 0.11, "channel_2": 0.2, "channel_3": 0.3}
|
||||
self.sweep_times = {"channel_1": 0.115, "channel_2": 0.2, "channel_3": 0.3}
|
||||
|
||||
def tearDown(self):
|
||||
"""Tear down the multiprocessing manager."""
|
||||
self.manager.shutdown()
|
||||
|
||||
def test_galvo_base_initialization(self):
|
||||
# Parent Class Super Init
|
||||
assert self.galvo.microscope_name == "Mesoscale"
|
||||
assert self.galvo.galvo_name == "Galvo 0"
|
||||
assert self.galvo.sample_rate == 100000
|
||||
|
||||
assert (
|
||||
self.galvo.camera_delay
|
||||
== self.configuration["configuration"]["microscopes"][self.microscope_name][
|
||||
"camera"
|
||||
]["delay"]
|
||||
/ 1000
|
||||
)
|
||||
assert self.galvo.galvo_max_voltage == 5
|
||||
assert self.galvo.galvo_min_voltage == -5
|
||||
assert self.galvo.galvo_waveform == "sawtooth" or "sine"
|
||||
assert self.galvo.waveform_dict == {}
|
||||
|
||||
def test_adjust_with_valid_input(self):
|
||||
# Test the method with valid input data
|
||||
for waveform in ["sawtooth", "sine"]:
|
||||
self.galvo.galvo_waveform = waveform
|
||||
result = self.galvo.adjust(self.exposure_times, self.sweep_times)
|
||||
|
||||
# Assert that the result is a dictionary
|
||||
self.assertIsInstance(result, dict)
|
||||
|
||||
# Assert that the keys in the result dictionary are the same as in the input
|
||||
# dictionaries
|
||||
self.assertSetEqual(set(result.keys()), set(self.exposure_times.keys()))
|
||||
|
||||
# Assert that the values in the result dictionary are not None
|
||||
for value in result.values():
|
||||
self.assertIsNotNone(value)
|
||||
|
||||
def test_adjust_with_invalid_input(self):
|
||||
# Test the method with invalid input data
|
||||
invalid_exposure_times = {"channel_1": 0.1} # Missing channel 2 and 3 keys
|
||||
invalid_sweep_times = {"channel_1": 0.1} # Missing channel 2 and 3 keys
|
||||
|
||||
# Test if the method raises an exception or returns None with invalid input
|
||||
with self.assertRaises(KeyError):
|
||||
_ = self.galvo.adjust(invalid_exposure_times, invalid_sweep_times)
|
||||
|
||||
def test_with_improper_waveform(self):
|
||||
self.galvo.galvo_waveform = "banana"
|
||||
result = self.galvo.adjust(self.exposure_times, self.sweep_times)
|
||||
assert result == self.galvo.waveform_dict
|
||||
|
||||
def test_waveform_clipping(self):
|
||||
self.galvo.galvo_waveform = "sawtooth"
|
||||
result = self.galvo.adjust(self.exposure_times, self.sweep_times)
|
||||
for channel in "channel_1", "channel_2", "channel_3":
|
||||
assert np.all(result[channel] <= self.galvo.galvo_max_voltage)
|
||||
assert np.all(result[channel] >= self.galvo.galvo_min_voltage)
|
||||
133
test/model/devices/galvo/test_galvo_ni.py
Normal file
133
test/model/devices/galvo/test_galvo_ni.py
Normal file
@@ -0,0 +1,133 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import unittest
|
||||
from unittest.mock import MagicMock
|
||||
from navigate.model.devices.galvo.ni import NIGalvo
|
||||
from navigate.config import (
|
||||
load_configs,
|
||||
get_configuration_paths,
|
||||
verify_configuration,
|
||||
verify_waveform_constants,
|
||||
)
|
||||
from multiprocessing import Manager
|
||||
|
||||
|
||||
class TestNIGalvo(unittest.TestCase):
|
||||
"""Unit tests for the Galvo NI Device."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Set up the configuration, experiment, etc."""
|
||||
self.manager = Manager()
|
||||
self.parent_dict = {}
|
||||
|
||||
(
|
||||
configuration_path,
|
||||
experiment_path,
|
||||
waveform_constants_path,
|
||||
rest_api_path,
|
||||
waveform_templates_path,
|
||||
gui_configuration_path,
|
||||
multi_positions_path,
|
||||
) = get_configuration_paths()
|
||||
|
||||
self.configuration = load_configs(
|
||||
self.manager,
|
||||
configuration=configuration_path,
|
||||
experiment=experiment_path,
|
||||
waveform_constants=waveform_constants_path,
|
||||
rest_api_config=rest_api_path,
|
||||
waveform_templates=waveform_templates_path,
|
||||
gui_configuration_path=gui_configuration_path,
|
||||
)
|
||||
verify_configuration(self.manager, self.configuration)
|
||||
verify_waveform_constants(self.manager, self.configuration)
|
||||
self.microscope_name = "Mesoscale"
|
||||
self.device_connection = MagicMock()
|
||||
galvo_id = 0
|
||||
|
||||
self.galvo = NIGalvo(
|
||||
microscope_name=self.microscope_name,
|
||||
device_connection=self.device_connection,
|
||||
configuration=self.configuration,
|
||||
device_id=galvo_id,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
"""Tear down the multiprocessing manager."""
|
||||
self.manager.shutdown()
|
||||
|
||||
def test_galvo_ni_initialization(self):
|
||||
# Parent Class Super Init
|
||||
assert self.galvo.microscope_name == "Mesoscale"
|
||||
assert self.galvo.galvo_name == "Galvo 0"
|
||||
assert self.galvo.sample_rate == 100000
|
||||
assert (
|
||||
self.galvo.camera_delay
|
||||
== self.configuration["configuration"]["microscopes"][self.microscope_name][
|
||||
"camera"
|
||||
]["delay"]
|
||||
/ 1000
|
||||
)
|
||||
assert self.galvo.galvo_max_voltage == 5
|
||||
assert self.galvo.galvo_min_voltage == -5
|
||||
assert self.galvo.galvo_waveform == "sawtooth" or "sine"
|
||||
assert self.galvo.waveform_dict == {}
|
||||
|
||||
# NIGalvo Init
|
||||
assert self.galvo.trigger_source == "/PXI6259/PFI0"
|
||||
assert hasattr(self.galvo, "daq")
|
||||
|
||||
def test_adjust(self):
|
||||
sweep_times = {"channel_1": 0.3, "channel_2": 0.4, "channel_3": 0.5}
|
||||
|
||||
exposure_times = {"channel_1": 0.25, "channel_2": 0.35, "channel_3": 0.45}
|
||||
|
||||
waveforms = self.galvo.adjust(
|
||||
exposure_times=exposure_times, sweep_times=sweep_times
|
||||
)
|
||||
|
||||
assert type(waveforms) == dict
|
||||
self.device_connection.assert_not_called()
|
||||
|
||||
for channel_key, channel_setting in self.configuration["experiment"][
|
||||
"MicroscopeState"
|
||||
]["channels"].items():
|
||||
if channel_setting["is_selected"]:
|
||||
assert channel_key in waveforms.keys()
|
||||
|
||||
self.device_connection.analog_outputs.__setitem__.assert_called_with(
|
||||
self.galvo.device_config["hardware"]["channel"],
|
||||
{
|
||||
"trigger_source": self.galvo.trigger_source,
|
||||
"waveform": waveforms,
|
||||
},
|
||||
)
|
||||
86
test/model/devices/galvo/test_galvo_synthetic.py
Normal file
86
test/model/devices/galvo/test_galvo_synthetic.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import unittest
|
||||
from unittest.mock import MagicMock
|
||||
from navigate.model.devices.galvo.synthetic import SyntheticGalvo
|
||||
from navigate.config import (
|
||||
load_configs,
|
||||
get_configuration_paths,
|
||||
verify_configuration,
|
||||
verify_waveform_constants,
|
||||
)
|
||||
from multiprocessing import Manager
|
||||
|
||||
|
||||
class TestGalvoSynthetic(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.manager = Manager()
|
||||
self.parent_dict = {}
|
||||
|
||||
(
|
||||
configuration_path,
|
||||
experiment_path,
|
||||
waveform_constants_path,
|
||||
rest_api_path,
|
||||
waveform_templates_path,
|
||||
gui_configuration_path,
|
||||
multi_positions_path,
|
||||
) = get_configuration_paths()
|
||||
|
||||
self.configuration = load_configs(
|
||||
self.manager,
|
||||
configuration=configuration_path,
|
||||
experiment=experiment_path,
|
||||
waveform_constants=waveform_constants_path,
|
||||
rest_api_config=rest_api_path,
|
||||
waveform_templates=waveform_templates_path,
|
||||
gui_configuration=gui_configuration_path,
|
||||
)
|
||||
verify_configuration(self.manager, self.configuration)
|
||||
verify_waveform_constants(self.manager, self.configuration)
|
||||
self.microscope_name = "Mesoscale"
|
||||
self.device_connection = MagicMock()
|
||||
galvo_id = 0
|
||||
|
||||
self.galvo = SyntheticGalvo(
|
||||
microscope_name=self.microscope_name,
|
||||
device_connection=self.device_connection,
|
||||
configuration=self.configuration,
|
||||
device_id=galvo_id,
|
||||
)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.manager.shutdown()
|
||||
|
||||
def test_dunder_del(self):
|
||||
"""Test the __del__ method"""
|
||||
self.galvo.__del__()
|
||||
21
test/model/devices/lasers/test_laser_base.py
Normal file
21
test/model/devices/lasers/test_laser_base.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from navigate.model.devices.laser.synthetic import SyntheticLaser
|
||||
from test.model.dummy import DummyModel
|
||||
import random
|
||||
|
||||
|
||||
def test_laser_base_functions():
|
||||
|
||||
model = DummyModel()
|
||||
microscope_name = model.configuration["experiment"]["MicroscopeState"][
|
||||
"microscope_name"
|
||||
]
|
||||
laser = SyntheticLaser(microscope_name, None, model.configuration, 0)
|
||||
|
||||
funcs = ["set_power", "turn_on", "turn_off", "close"]
|
||||
args = [[random.random()], None, None, None]
|
||||
|
||||
for f, a in zip(funcs, args):
|
||||
if a is not None:
|
||||
getattr(laser, f)(*a)
|
||||
else:
|
||||
getattr(laser, f)()
|
||||
97
test/model/devices/lasers/test_laser_ni.py
Normal file
97
test/model/devices/lasers/test_laser_ni.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from multiprocessing import Manager
|
||||
|
||||
import random
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from navigate.config import load_configs, get_configuration_paths
|
||||
from navigate.model.devices.laser.ni import NILaser
|
||||
|
||||
|
||||
class TestLaserNI(unittest.TestCase):
|
||||
"""Unit tests for the Laser NI Device."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Set up the configuration, experiment, etc."""
|
||||
|
||||
self.manager = Manager()
|
||||
self.parent_dict = {}
|
||||
|
||||
(
|
||||
configuration_path,
|
||||
experiment_path,
|
||||
waveform_constants_path,
|
||||
rest_api_path,
|
||||
waveform_templates_path,
|
||||
gui_configuration_path,
|
||||
multi_positions_path,
|
||||
) = get_configuration_paths()
|
||||
|
||||
self.configuration = load_configs(
|
||||
self.manager,
|
||||
configuration=configuration_path,
|
||||
experiment=experiment_path,
|
||||
waveform_constants=waveform_constants_path,
|
||||
rest_api_config=rest_api_path,
|
||||
waveform_templates=waveform_templates_path,
|
||||
gui_configuration=gui_configuration_path,
|
||||
)
|
||||
|
||||
self.microscope_name = self.configuration["configuration"][
|
||||
"microscopes"
|
||||
].keys()[0]
|
||||
self.device_connection = None
|
||||
laser_id = 0
|
||||
|
||||
with patch("nidaqmx.Task") as self.mock_task:
|
||||
# self.mock_task_instance = MagicMock()
|
||||
# self.mock_task.return_value = self.mock_task_instance
|
||||
self.laser = NILaser(
|
||||
microscope_name=self.microscope_name,
|
||||
device_connection=self.device_connection,
|
||||
configuration=self.configuration,
|
||||
device_id=laser_id,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
"""Tear down the multiprocessing manager."""
|
||||
self.manager.shutdown()
|
||||
|
||||
def test_set_power(self):
|
||||
self.current_intensity = random.randint(1, 100)
|
||||
scaled_intensity = (int(self.current_intensity) / 100) * self.laser.laser_max_ao
|
||||
self.laser.set_power(self.current_intensity)
|
||||
|
||||
self.laser.laser_ao_task.write.assert_called_once_with(
|
||||
scaled_intensity, auto_start=True
|
||||
)
|
||||
assert self.laser._current_intensity == self.current_intensity
|
||||
|
||||
def test_turn_on(self):
|
||||
self.laser.digital_port_type = "digital"
|
||||
self.laser.turn_on()
|
||||
self.laser.laser_do_task.write.assert_called_with(True, auto_start=True)
|
||||
|
||||
self.laser.digital_port_type = "analog"
|
||||
self.laser.turn_on()
|
||||
self.laser.laser_do_task.write.assert_called_with(
|
||||
self.laser.laser_max_do, auto_start=True
|
||||
)
|
||||
|
||||
def test_turn_off(self):
|
||||
self.current_intensity = random.randint(1, 100)
|
||||
self.laser._current_intensity = self.current_intensity
|
||||
|
||||
self.laser.digital_port_type = "digital"
|
||||
self.laser.turn_off()
|
||||
self.laser.laser_do_task.write.assert_called_with(False, auto_start=True)
|
||||
|
||||
assert self.laser._current_intensity == self.current_intensity
|
||||
|
||||
self.laser.digital_port_type = "analog"
|
||||
self.laser.turn_off()
|
||||
self.laser.laser_do_task.write.assert_called_with(
|
||||
self.laser.laser_min_do, auto_start=True
|
||||
)
|
||||
|
||||
assert self.laser._current_intensity == self.current_intensity
|
||||
321
test/model/devices/pump/test_tecan.py
Normal file
321
test/model/devices/pump/test_tecan.py
Normal file
@@ -0,0 +1,321 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
# Standard Library Imports
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
|
||||
# Third Party Imports
|
||||
|
||||
# Local Imports
|
||||
from navigate.model.devices.pump.tecan import XCaliburPump
|
||||
from navigate.model.utils.exceptions import UserVisibleException
|
||||
|
||||
|
||||
class FakeSerial:
|
||||
def __init__(self, port, baudrate, timeout):
|
||||
self.commands = [] # Record of all sent commands (as bytes).
|
||||
self.is_open = True # Pretend the serial port is open.
|
||||
self.last_command = None # Stores the last command sent (as string, no \r).
|
||||
self.command_responses = (
|
||||
{}
|
||||
) # Maps command strings (e.g., "S5") to fake byte responses.
|
||||
|
||||
self.port = port
|
||||
self.baudrate = baudrate
|
||||
self.timeout = timeout
|
||||
|
||||
def open(self):
|
||||
self.is_open = True
|
||||
|
||||
def close(self):
|
||||
self.is_open = False
|
||||
|
||||
def write(self, data: bytes):
|
||||
"""
|
||||
Simulate sending a command to the pump.
|
||||
|
||||
- Updates last_command with the stripped string version (used for read lookup).
|
||||
- Appends the raw byte-formatted command to the commands list to keep track of which order the commands are sent.
|
||||
"""
|
||||
|
||||
self.last_command = data.decode("ascii").strip()
|
||||
self.commands.append(data)
|
||||
|
||||
def read(self, n: int) -> bytes:
|
||||
"""
|
||||
Simulate receiving a response from the pump.
|
||||
|
||||
If a response has been predefined for the last command (e.g., to simulate an error or custom reply),
|
||||
that specific response is returned.
|
||||
|
||||
Otherwise, a default success response (b"/00") is returned to simulate normal operation.
|
||||
"""
|
||||
if self.last_command in self.command_responses:
|
||||
return self.command_responses[self.last_command]
|
||||
return b"/00" # If no command has been sent yet, return the "success" response as fallback.
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_pump():
|
||||
"""
|
||||
Fixture that returns an XCaliburPump with a mocked serial connection.
|
||||
"""
|
||||
|
||||
# Pick some speeds within the known bounds 0-40.
|
||||
min_speed_code = 2
|
||||
max_speed_code = 19
|
||||
port = "FAKE"
|
||||
baudrate = 9600
|
||||
timeout = 0.5
|
||||
|
||||
fake_serial = FakeSerial(port=port, baudrate=baudrate, timeout=timeout)
|
||||
|
||||
config = {
|
||||
"min_speed_code": min_speed_code,
|
||||
"max_speed_code": max_speed_code,
|
||||
"fine_positioning": False,
|
||||
}
|
||||
|
||||
pump = XCaliburPump(
|
||||
microscope_name="TestPump",
|
||||
device_connection=fake_serial,
|
||||
configuration=config,
|
||||
)
|
||||
|
||||
return pump
|
||||
|
||||
|
||||
def test_set_speed_command_rejected(fake_pump):
|
||||
"""
|
||||
Simulate a firmware-level rejection of a valid speed code.
|
||||
|
||||
This test configures the FakeSerial to return error code '/03' (Invalid Operand)
|
||||
in response to a speed code that is within the allowed local range. This models a case
|
||||
where the driver sends a syntactically valid command (e.g., 'S4'), but the pump
|
||||
firmware rejects the operand value due to internal state or configuration.
|
||||
|
||||
The test verifies that the driver:
|
||||
- Sends the command correctly.
|
||||
- Parses the response.
|
||||
- Raises a RuntimeError with an appropriate error message.
|
||||
"""
|
||||
valid_speed = fake_pump.max_speed_code - 1 # Within bounds.
|
||||
|
||||
fake_pump.serial.command_responses["S" + str(valid_speed)] = (
|
||||
b"/03" # Simulate command-response.
|
||||
)
|
||||
|
||||
# Make sure the pre-defined response raises the correct error.
|
||||
with pytest.raises(
|
||||
UserVisibleException,
|
||||
match="Pump error /3: Invalid operand - bad parameter value",
|
||||
):
|
||||
fake_pump.set_speed(valid_speed)
|
||||
|
||||
|
||||
@patch("navigate.model.devices.pump.tecan.Serial")
|
||||
def test_connect_and_initialize_success(
|
||||
mock_serial_class,
|
||||
): # Argument passed automatically from patch (mocked version of Serial).
|
||||
"""
|
||||
Simulate a successful connection using FakeSerial via patching.
|
||||
"""
|
||||
# Create a custom FakeSerial instance to return instead of MagicMock.
|
||||
fake_serial = FakeSerial(port="FAKE", baudrate=9600, timeout=0.5)
|
||||
fake_serial.command_responses["ZR"] = b"/00" # Simulate valid response.
|
||||
|
||||
# Tell the mock object what to return instead of Serial.
|
||||
mock_serial_class.return_value = fake_serial
|
||||
|
||||
# Simulate the connect call that is done when all device connections are set up.
|
||||
# Will be the same as fake_serial if successful.
|
||||
serial_connection = XCaliburPump.connect(port="FAKE", baudrate=9600, timeout=0.5)
|
||||
|
||||
mock_serial_class.assert_called_once_with(port="FAKE", baudrate=9600, timeout=0.5)
|
||||
|
||||
# Create the pump and call connect - now it will receive the FakeSerial.
|
||||
pump = XCaliburPump(
|
||||
microscope_name="TestPump",
|
||||
device_connection=serial_connection,
|
||||
configuration={},
|
||||
)
|
||||
|
||||
pump.initialize_pump()
|
||||
|
||||
# Assertions
|
||||
assert pump.serial == fake_serial
|
||||
assert fake_serial.commands[-1] == b"ZR\r"
|
||||
assert fake_serial.is_open
|
||||
|
||||
|
||||
@patch("serial.Serial")
|
||||
def test_initialization_error(
|
||||
mock_serial_class,
|
||||
): # Argument passed automatically from patch (mocked version of Serial).
|
||||
"""
|
||||
Simulate a pump that fails to initialize (command 'ZR', response '/01').
|
||||
|
||||
Verifies that:
|
||||
- The 'ZR' command is sent.
|
||||
- The driver raises RuntimeError when pump reports an init failure.
|
||||
"""
|
||||
# Create a custom FakeSerial instance to return instead of MagicMock.
|
||||
fake_serial = FakeSerial(port="FAKE", baudrate=9600, timeout=0.5)
|
||||
fake_serial.command_responses["ZR"] = b"/01" # Simulate "fail" response.
|
||||
|
||||
# Make sure Serial() returns this custom fake.
|
||||
mock_serial_class.return_value = fake_serial
|
||||
|
||||
# Create the pump.
|
||||
pump = XCaliburPump(
|
||||
microscope_name="TestPump",
|
||||
device_connection=fake_serial,
|
||||
configuration={},
|
||||
)
|
||||
|
||||
# Expect a RuntimeError due to /01 response.
|
||||
with pytest.raises(
|
||||
UserVisibleException, match="Pump error /1: Initialization error"
|
||||
):
|
||||
pump.initialize_pump()
|
||||
|
||||
# Check that the correct command was sent.
|
||||
assert pump.serial.commands[-1] == b"ZR\r"
|
||||
|
||||
|
||||
# NOTE: We do not wrap or handle exceptions in XCaliburPump.connect().
|
||||
# Errors like Serial(port=...) failures are allowed to propagate.
|
||||
# Therefore, no test is needed for connect() error handling.
|
||||
|
||||
|
||||
def test_send_command_raises_if_serial_is_none():
|
||||
"""
|
||||
Verifies that send_command() raises if self.serial is None.
|
||||
"""
|
||||
fake_serial = FakeSerial(port="FAKE", baudrate=9600, timeout=0.5)
|
||||
|
||||
pump = XCaliburPump(
|
||||
microscope_name="TestPump",
|
||||
device_connection=fake_serial,
|
||||
configuration={},
|
||||
)
|
||||
pump.serial = None # Simulate uninitialized or failed connection
|
||||
|
||||
with pytest.raises(UserVisibleException, match="Serial object is None"):
|
||||
pump.send_command("ZR")
|
||||
|
||||
|
||||
def test_move_absolute_success_standard_and_fine_modes(fake_pump):
|
||||
"""
|
||||
Test that move_absolute() sends the correct command and succeeds in both
|
||||
standard and fine positioning modes, assuming valid position input.
|
||||
|
||||
Verifies that:
|
||||
- The correct 'A{pos}' command is sent.
|
||||
- The pump responds with success.
|
||||
- No exception is raised.
|
||||
"""
|
||||
# --- Standard mode ---
|
||||
fake_pump.fine_positioning = False
|
||||
position_std = 3000 # Max allowed position in standard (non-fine) mode.
|
||||
|
||||
# Predefine the pump's response to this specific absolute move command.
|
||||
fake_pump.serial.command_responses[f"A{position_std}"] = b"/00"
|
||||
|
||||
# Send the move_absolute command (which internally sends 'A{position}' + parses response).
|
||||
fake_pump.move_absolute(position_std)
|
||||
|
||||
# Verify that the correct byte-encoded command was sent to the serial interface.
|
||||
assert fake_pump.serial.commands[-1] == f"A{position_std}\r".encode()
|
||||
|
||||
# --- Fine positioning mode ---
|
||||
fake_pump.fine_positioning = True
|
||||
position_fine = 24000 # Max allowed position in fine mode.
|
||||
fake_pump.serial.command_responses[f"A{position_fine}"] = b"/00"
|
||||
|
||||
fake_pump.move_absolute(position_fine)
|
||||
assert fake_pump.serial.commands[-1] == f"A{position_fine}\r".encode()
|
||||
|
||||
|
||||
def test_move_absolute_out_of_bounds_raises(fake_pump):
|
||||
"""
|
||||
Verify that move_absolute() raises UserVisibleException when given a position
|
||||
outside the valid range for the current positioning mode.
|
||||
"""
|
||||
# Standard mode: max is 3000.
|
||||
fake_pump.fine_positioning = False
|
||||
with pytest.raises(UserVisibleException, match="out of bounds"):
|
||||
fake_pump.move_absolute(3000 + 1)
|
||||
|
||||
# Fine mode: max is 24000.
|
||||
fake_pump.fine_positioning = True
|
||||
with pytest.raises(UserVisibleException, match="out of bounds"):
|
||||
fake_pump.move_absolute(24000 + 1)
|
||||
|
||||
|
||||
def test_set_fine_positioning_mode_toggle(fake_pump):
|
||||
"""
|
||||
Verify that set_fine_positioning_mode() sends the correct 'N' and 'R' commands,
|
||||
handles responses properly, and updates the fine_positioning attribute.
|
||||
"""
|
||||
|
||||
# Mock responses for enabling fine positioning.
|
||||
# "N1" loads the fine mode into the buffer; "R" applies the change.
|
||||
# Both return "/00" to simulate success.
|
||||
fake_pump.serial.command_responses["N1"] = b"/00"
|
||||
fake_pump.serial.command_responses["R"] = b"/00"
|
||||
|
||||
# Enable fine positioning mode.
|
||||
fake_pump.set_fine_positioning_mode(True)
|
||||
|
||||
# Check that the internal state was updated.
|
||||
assert fake_pump.fine_positioning is True
|
||||
|
||||
# Confirm that the correct commands were sent in the correct order
|
||||
# inside set_fine_positioning_mode().
|
||||
assert fake_pump.serial.commands[-2] == b"N1\r"
|
||||
assert fake_pump.serial.commands[-1] == b"R\r"
|
||||
|
||||
# Now test disabling fine positioning mode.
|
||||
# "N0" loads standard mode; "R" applies it. Again, simulate success.
|
||||
fake_pump.serial.command_responses["N0"] = b"/00"
|
||||
fake_pump.serial.command_responses["R"] = b"/00"
|
||||
|
||||
fake_pump.set_fine_positioning_mode(False)
|
||||
|
||||
assert fake_pump.fine_positioning is False
|
||||
assert fake_pump.serial.commands[-2] == b"N0\r"
|
||||
assert fake_pump.serial.commands[-1] == b"R\r"
|
||||
|
||||
|
||||
# TODO: Once pump is integrated into Model/Controller, test that
|
||||
# UserVisibleException raised by pump results in a warning event.
|
||||
106
test/model/devices/remote_focus/test_rf_base.py
Normal file
106
test/model/devices/remote_focus/test_rf_base.py
Normal file
@@ -0,0 +1,106 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
# Standard Library Imports
|
||||
|
||||
# Third Party Imports
|
||||
import pytest
|
||||
import numpy as np
|
||||
|
||||
# Local Imports
|
||||
from navigate.model.devices.remote_focus.synthetic import SyntheticRemoteFocus
|
||||
from test.model.dummy import DummyModel
|
||||
|
||||
|
||||
def test_remote_focus_base_init():
|
||||
model = DummyModel()
|
||||
microscope_name = model.configuration["experiment"]["MicroscopeState"][
|
||||
"microscope_name"
|
||||
]
|
||||
SyntheticRemoteFocus(microscope_name, None, model.configuration)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("smoothing", [0] + list(np.random.rand(5) * 100))
|
||||
def test_remote_focus_base_adjust(smoothing):
|
||||
from test.model.dummy import DummyModel
|
||||
|
||||
model = DummyModel()
|
||||
microscope_name = model.configuration["experiment"]["MicroscopeState"][
|
||||
"microscope_name"
|
||||
]
|
||||
microscope_state = model.configuration["experiment"]["MicroscopeState"]
|
||||
|
||||
waveform_constants = model.configuration["waveform_constants"]
|
||||
imaging_mode = microscope_state["microscope_name"]
|
||||
zoom = microscope_state["zoom"]
|
||||
for channel_key in microscope_state["channels"].keys():
|
||||
# channel includes 'is_selected', 'laser', 'filter', 'camera_exposure'...
|
||||
channel = microscope_state["channels"][channel_key]
|
||||
|
||||
# Only proceed if it is enabled in the GUI
|
||||
if channel["is_selected"] is True:
|
||||
laser = channel["laser"]
|
||||
waveform_constants["remote_focus_constants"][imaging_mode][zoom][laser][
|
||||
"percent_smoothing"
|
||||
] = smoothing
|
||||
channel["camera_exposure_time"] = np.random.rand() * 150 + 50
|
||||
|
||||
rf = SyntheticRemoteFocus(microscope_name, None, model.configuration)
|
||||
|
||||
# exposure_times = {
|
||||
# k: v["camera_exposure_time"] / 1000
|
||||
# for k, v in microscope_state["channels"].items()
|
||||
# }
|
||||
# sweep_times = {
|
||||
# k: 2 * v["camera_exposure_time"] / 1000
|
||||
# for k, v in microscope_state["channels"].items()
|
||||
# }
|
||||
|
||||
(
|
||||
exposure_times,
|
||||
sweep_times,
|
||||
) = model.active_microscope.calculate_exposure_sweep_times()
|
||||
|
||||
waveform_dict = rf.adjust(exposure_times, sweep_times)
|
||||
|
||||
for k, v in waveform_dict.items():
|
||||
try:
|
||||
channel = microscope_state["channels"][k]
|
||||
if not channel["is_selected"]:
|
||||
continue
|
||||
assert np.all(v <= rf.remote_focus_max_voltage)
|
||||
assert np.all(v >= rf.remote_focus_min_voltage)
|
||||
assert len(v) == int(sweep_times[k] * rf.sample_rate)
|
||||
except KeyError:
|
||||
# The channel doesn't exist. Points to an issue in how waveform dict
|
||||
# is created.
|
||||
continue
|
||||
29
test/model/devices/remote_focus/test_rf_ni.py
Normal file
29
test/model/devices/remote_focus/test_rf_ni.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.hardware
|
||||
def test_remote_focus_ni_functions():
|
||||
from navigate.model.devices.daq.ni import NIDAQ
|
||||
from navigate.model.devices.remote_focus.ni import NIRemoteFocus
|
||||
from test.model.dummy import DummyModel
|
||||
|
||||
model = DummyModel()
|
||||
daq = NIDAQ(model.configuration)
|
||||
microscope_name = model.configuration["experiment"]["MicroscopeState"][
|
||||
"microscope_name"
|
||||
]
|
||||
rf = NIRemoteFocus(microscope_name, daq, model.configuration)
|
||||
|
||||
funcs = ["adjust"]
|
||||
args = [
|
||||
[
|
||||
{"channel_1": 0.2, "channel_2": 0.1, "channel_3": 0.15},
|
||||
{"channel_1": 0.3, "channel_2": 0.2, "channel_3": 0.25},
|
||||
]
|
||||
]
|
||||
|
||||
for f, a in zip(funcs, args):
|
||||
if a is not None:
|
||||
getattr(rf, f)(*a)
|
||||
else:
|
||||
getattr(rf, f)()
|
||||
20
test/model/devices/remote_focus/test_rf_synthetic.py
Normal file
20
test/model/devices/remote_focus/test_rf_synthetic.py
Normal file
@@ -0,0 +1,20 @@
|
||||
def test_remote_focus_synthetic_functions():
|
||||
from navigate.model.devices.remote_focus.synthetic import (
|
||||
SyntheticRemoteFocus,
|
||||
)
|
||||
from test.model.dummy import DummyModel
|
||||
|
||||
model = DummyModel()
|
||||
microscope_name = model.configuration["experiment"]["MicroscopeState"][
|
||||
"microscope_name"
|
||||
]
|
||||
rf = SyntheticRemoteFocus(microscope_name, None, model.configuration)
|
||||
|
||||
funcs = ["move"]
|
||||
args = [[0.1, None]]
|
||||
|
||||
for f, a in zip(funcs, args):
|
||||
if a is not None:
|
||||
getattr(rf, f)(*a)
|
||||
else:
|
||||
getattr(rf, f)()
|
||||
59
test/model/devices/shutter/test_shutter_base.py
Normal file
59
test/model/devices/shutter/test_shutter_base.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
import unittest
|
||||
from navigate.model.devices.shutter.synthetic import SyntheticShutter
|
||||
from test.model.dummy import DummyModel
|
||||
|
||||
|
||||
class TestLaserBase(unittest.TestCase):
|
||||
"""Unit Test for ShutterBase Class"""
|
||||
|
||||
dummy_model = DummyModel()
|
||||
microscope_name = "Mesoscale"
|
||||
|
||||
def test_shutter_base_attributes(self):
|
||||
shutter = SyntheticShutter(
|
||||
self.microscope_name, None, self.dummy_model.configuration
|
||||
)
|
||||
|
||||
# Methods
|
||||
assert hasattr(shutter, "open_shutter") and callable(
|
||||
getattr(shutter, "open_shutter")
|
||||
)
|
||||
assert hasattr(shutter, "close_shutter") and callable(
|
||||
getattr(shutter, "close_shutter")
|
||||
)
|
||||
assert hasattr(shutter, "state")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
56
test/model/devices/shutter/test_shutter_ni.py
Normal file
56
test/model/devices/shutter/test_shutter_ni.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
# Standard Library Imports
|
||||
import unittest
|
||||
|
||||
# Third Party Imports
|
||||
|
||||
# Local Imports
|
||||
from navigate.model.devices.shutter.ni import NIShutter
|
||||
|
||||
|
||||
class TestNIShutter(unittest.TestCase):
|
||||
"""Unit Test for NIShutter Class"""
|
||||
|
||||
def test_shutter_ttl_attributes(self):
|
||||
assert hasattr(NIShutter, "open_shutter") and callable(
|
||||
getattr(NIShutter, "open_shutter")
|
||||
)
|
||||
assert hasattr(NIShutter, "close_shutter") and callable(
|
||||
getattr(NIShutter, "close_shutter")
|
||||
)
|
||||
assert hasattr(NIShutter, "state")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
73
test/model/devices/shutter/test_shutter_synthetic.py
Normal file
73
test/model/devices/shutter/test_shutter_synthetic.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
# Standard Library Imports
|
||||
import unittest
|
||||
|
||||
# Third Party Imports
|
||||
|
||||
# Local Imports
|
||||
from navigate.model.devices.shutter.synthetic import SyntheticShutter
|
||||
from test.model.dummy import DummyModel
|
||||
|
||||
|
||||
class TestSyntheticShutter(unittest.TestCase):
|
||||
"""Unit Test for SyntheticShutter Class"""
|
||||
|
||||
dummy_model = DummyModel()
|
||||
microscope_name = "Mesoscale"
|
||||
|
||||
def test_synthetic_shutter_attributes(self):
|
||||
shutter = SyntheticShutter(
|
||||
self.microscope_name, None, self.dummy_model.configuration
|
||||
)
|
||||
|
||||
# Attributes
|
||||
# assert hasattr(shutter, 'configuration')
|
||||
# assert hasattr(shutter, 'experiment')
|
||||
# assert hasattr(shutter, 'shutter_right')
|
||||
# assert hasattr(shutter, 'shutter_right_state')
|
||||
# assert hasattr(shutter, 'shutter_left')
|
||||
# assert hasattr(shutter, 'shutter_left_state')
|
||||
|
||||
# Methods
|
||||
assert hasattr(shutter, "open_shutter") and callable(
|
||||
getattr(shutter, "open_shutter")
|
||||
)
|
||||
assert hasattr(shutter, "close_shutter") and callable(
|
||||
getattr(shutter, "close_shutter")
|
||||
)
|
||||
assert hasattr(shutter, "state")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
0
test/model/devices/stages/__init__.py
Normal file
0
test/model/devices/stages/__init__.py
Normal file
160
test/model/devices/stages/conftest.py
Normal file
160
test/model/devices/stages/conftest.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#"""
|
||||
|
||||
# Standard Library Imports
|
||||
import random
|
||||
|
||||
# Third Party Imports
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def stage_configuration():
|
||||
return {
|
||||
"stage": {
|
||||
"hardware": {
|
||||
"name": "stage",
|
||||
"type": "",
|
||||
"port": "COM10",
|
||||
"baudrate": 115200,
|
||||
"serial_number": 123456,
|
||||
"axes": ["x", "y", "z", "f", "theta"],
|
||||
},
|
||||
"x_max": 100,
|
||||
"x_min": -10,
|
||||
"y_max": 200,
|
||||
"y_min": -20,
|
||||
"z_max": 300,
|
||||
"z_min": -30,
|
||||
"f_max": 400,
|
||||
"f_min": -40,
|
||||
"theta_max": 360,
|
||||
"theta_min": 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def random_single_axis_test(stage_configuration):
|
||||
pos_sequence = []
|
||||
for _ in range(10):
|
||||
axis = random.choice(["x", "y", "z", "theta", "f"])
|
||||
# random valid pos
|
||||
axis_min = stage_configuration["stage"][f"{axis}_min"]
|
||||
axis_max = stage_configuration["stage"][f"{axis}_max"]
|
||||
pos = random.randrange(axis_min, axis_max)
|
||||
pos_sequence.append((axis, pos))
|
||||
|
||||
for _ in range(10):
|
||||
# valid and non-valid pos
|
||||
axis = random.choice(["x", "y", "z", "theta", "f"])
|
||||
pos = random.randrange(-100, 500)
|
||||
pos_sequence.append((axis, pos))
|
||||
|
||||
def _verify_move_axis_absolute(stage):
|
||||
axes_mapping = stage.axes_mapping
|
||||
|
||||
stage_pos = stage.report_position()
|
||||
for axis, pos in pos_sequence:
|
||||
stage.move_axis_absolute(axis, pos, True)
|
||||
temp_pos = stage.report_position()
|
||||
axis_min = stage_configuration["stage"][f"{axis}_min"]
|
||||
axis_max = stage_configuration["stage"][f"{axis}_max"]
|
||||
if axis in axes_mapping:
|
||||
if not stage.stage_limits or (pos >= axis_min and pos <= axis_max):
|
||||
stage_pos[f"{axis}_pos"] = pos
|
||||
assert stage_pos == temp_pos
|
||||
|
||||
return _verify_move_axis_absolute
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def random_multiple_axes_test(stage_configuration):
|
||||
pos_sequence = []
|
||||
axes = ["x", "y", "z", "f", "theta"]
|
||||
for _ in range(20):
|
||||
pos = {}
|
||||
for axis in axes:
|
||||
pos[axis] = random.randrange(-100, 500)
|
||||
pos_sequence.append(pos)
|
||||
|
||||
def _verify_move_absolute(stage):
|
||||
axes_mapping = stage.axes_mapping
|
||||
|
||||
# move one axis inside supported axes
|
||||
stage_pos = stage.report_position()
|
||||
for pos_dict in pos_sequence:
|
||||
axis = random.choice(list(axes_mapping.keys()))
|
||||
pos = pos_dict[axis]
|
||||
axis_min = stage_configuration["stage"][f"{axis}_min"]
|
||||
axis_max = stage_configuration["stage"][f"{axis}_max"]
|
||||
move_dict = {f"{axis}_abs": pos}
|
||||
stage.move_absolute(move_dict)
|
||||
temp_pos = stage.report_position()
|
||||
if not stage.stage_limits or (pos >= axis_min and pos <= axis_max):
|
||||
stage_pos[f"{axis}_pos"] = pos
|
||||
assert stage_pos == temp_pos
|
||||
|
||||
# move all axes inside supported axes
|
||||
stage_pos = stage.report_position()
|
||||
for pos_dict in pos_sequence:
|
||||
move_dict = {}
|
||||
for axis in axes_mapping.keys():
|
||||
move_dict[f"{axis}_abs"] = pos_dict[axis]
|
||||
|
||||
stage.move_absolute(move_dict)
|
||||
temp_pos = stage.report_position()
|
||||
for axis in axes_mapping:
|
||||
pos = pos_dict[axis]
|
||||
axis_min = stage_configuration["stage"][f"{axis}_min"]
|
||||
axis_max = stage_configuration["stage"][f"{axis}_max"]
|
||||
if not stage.stage_limits or (pos >= axis_min and pos <= axis_max):
|
||||
stage_pos[f"{axis}_pos"] = pos
|
||||
assert stage_pos == temp_pos
|
||||
|
||||
# move all axes (including supported axes and non-supported axes)
|
||||
stage_pos = stage.report_position()
|
||||
for pos_dict in pos_sequence:
|
||||
move_dict = dict(
|
||||
map(lambda axis: (f"{axis}_abs", pos_dict[axis]), pos_dict)
|
||||
)
|
||||
stage.move_absolute(move_dict)
|
||||
temp_pos = stage.report_position()
|
||||
for axis in axes_mapping:
|
||||
pos = pos_dict[axis]
|
||||
axis_min = stage_configuration["stage"][f"{axis}_min"]
|
||||
axis_max = stage_configuration["stage"][f"{axis}_max"]
|
||||
if not stage.stage_limits or (pos >= axis_min and pos <= axis_max):
|
||||
stage_pos[f"{axis}_pos"] = pos
|
||||
assert stage_pos == temp_pos
|
||||
|
||||
return _verify_move_absolute
|
||||
337
test/model/devices/stages/test_asi.py
Normal file
337
test/model/devices/stages/test_asi.py
Normal file
@@ -0,0 +1,337 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
# Standard Library Imports
|
||||
import pytest
|
||||
import random
|
||||
|
||||
# Third Party Imports
|
||||
|
||||
# Local Imports
|
||||
from navigate.model.devices.stage.asi import ASIStage
|
||||
from navigate.model.devices.APIs.asi.asi_tiger_controller import TigerController
|
||||
|
||||
|
||||
class MockASIStage:
|
||||
def __init__(self, ignore_obj):
|
||||
self.axes = ["X", "Y", "Z", "M", "N"]
|
||||
self.is_open = False
|
||||
self.input_buffer = []
|
||||
self.output_buffer = []
|
||||
self.ignore_obj = ignore_obj
|
||||
|
||||
for axis in self.axes:
|
||||
setattr(self, f"{axis}_abs", 0)
|
||||
|
||||
def open(self):
|
||||
self.is_open = True
|
||||
|
||||
def reset_input_buffer(self):
|
||||
self.input_buffer = []
|
||||
|
||||
def reset_output_buffer(self):
|
||||
self.output_buffer = []
|
||||
|
||||
def write(self, command):
|
||||
command = command.decode(encoding="ascii")[:-1]
|
||||
temps = command.split()
|
||||
command = temps[0]
|
||||
if command == "WHERE":
|
||||
axes = temps[1:]
|
||||
pos = [":A"]
|
||||
for axis in self.axes:
|
||||
if axis not in axes:
|
||||
continue
|
||||
pos.append(str(getattr(self, f"{axis}_abs")))
|
||||
self.output_buffer.append(" ".join(pos))
|
||||
elif command == "MOVE":
|
||||
success = True
|
||||
for i in range(1, len(temps)):
|
||||
axis, pos = temps[i].split("=")
|
||||
if axis in self.axes:
|
||||
setattr(self, f"{axis}_abs", float(pos))
|
||||
else:
|
||||
success = False
|
||||
if success:
|
||||
self.output_buffer.append(":A")
|
||||
else:
|
||||
self.output_buffer.append(":N")
|
||||
|
||||
elif command == "/":
|
||||
self.output_buffer.append(":A")
|
||||
elif command == "HALT":
|
||||
self.output_buffer.append(":A")
|
||||
elif command == "SPEED":
|
||||
self.output_buffer.append(":A")
|
||||
elif command == "BU":
|
||||
axes = " ".join(self.axes)
|
||||
self.output_buffer.append(
|
||||
f"TIGER_COMM\rMotor Axes: {axes} 0 1\rAxis Addr: 1 1 2 2 8 8\rHex "
|
||||
"Addr: 31 31 32 32 39 39\rAxis Props: 10 10 0 0 0 0"
|
||||
)
|
||||
elif command == "AA":
|
||||
self.output_buffer.append(":A")
|
||||
elif command == "AZ":
|
||||
self.output_buffer.append(":A")
|
||||
elif command == "B":
|
||||
self.output_buffer.append(":A")
|
||||
elif command == "PC":
|
||||
self.output_buffer.append(":A")
|
||||
elif command == "E":
|
||||
self.output_buffer.append(":A")
|
||||
|
||||
def readline(self):
|
||||
return bytes(self.output_buffer.pop(0), encoding="ascii")
|
||||
|
||||
def __getattr__(self, __name: str):
|
||||
return self.ignore_obj
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def asi_serial_device(ignore_obj):
|
||||
return MockASIStage(ignore_obj)
|
||||
|
||||
|
||||
class TestStageASI:
|
||||
"""Unit Test for ASI Stage Class"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_class(
|
||||
self,
|
||||
stage_configuration,
|
||||
asi_serial_device,
|
||||
random_single_axis_test,
|
||||
random_multiple_axes_test,
|
||||
):
|
||||
self.microscope_name = "Mesoscale"
|
||||
self.configuration = {
|
||||
"configuration": {
|
||||
"microscopes": {self.microscope_name: stage_configuration}
|
||||
}
|
||||
}
|
||||
self.stage_configuration = stage_configuration
|
||||
self.stage_configuration["stage"]["hardware"]["type"] = "ASI"
|
||||
self.asi_serial_device = asi_serial_device
|
||||
self.random_single_axis_test = random_single_axis_test
|
||||
self.random_multiple_axes_test = random_multiple_axes_test
|
||||
|
||||
def build_device_connection(self):
|
||||
port = self.stage_configuration["stage"]["hardware"]["port"]
|
||||
baudrate = self.stage_configuration["stage"]["hardware"]["baudrate"]
|
||||
|
||||
# Patch TigerController.get_default_motor_axis_sequence
|
||||
TigerController.get_default_motor_axis_sequence = lambda self: [
|
||||
"X",
|
||||
"Y",
|
||||
"Z",
|
||||
"M",
|
||||
"N",
|
||||
]
|
||||
asi_stage = TigerController(port, baudrate)
|
||||
asi_stage.serial = self.asi_serial_device
|
||||
asi_stage.connect_to_serial()
|
||||
return asi_stage
|
||||
|
||||
def test_stage_attributes(self):
|
||||
stage = ASIStage(self.microscope_name, None, self.configuration)
|
||||
|
||||
# Methods
|
||||
assert hasattr(stage, "get_position_dict") and callable(
|
||||
getattr(stage, "get_position_dict")
|
||||
)
|
||||
assert hasattr(stage, "report_position") and callable(
|
||||
getattr(stage, "report_position")
|
||||
)
|
||||
assert hasattr(stage, "move_axis_absolute") and callable(
|
||||
getattr(stage, "move_axis_absolute")
|
||||
)
|
||||
assert hasattr(stage, "move_absolute") and callable(
|
||||
getattr(stage, "move_absolute")
|
||||
)
|
||||
assert hasattr(stage, "stop") and callable(getattr(stage, "stop"))
|
||||
assert hasattr(stage, "get_abs_position") and callable(
|
||||
getattr(stage, "get_abs_position")
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], None),
|
||||
(["y"], None),
|
||||
(["x", "z"], None),
|
||||
(["f", "z"], None),
|
||||
(["x", "y", "z"], None),
|
||||
(["x", "y", "z", "f"], None),
|
||||
(["x"], ["Y"]),
|
||||
(["y"], ["Z"]),
|
||||
(["x", "z"], ["X", "Y"]),
|
||||
(["f", "z"], ["M", "X"]),
|
||||
(["x", "y", "z"], ["Y", "X", "M"]),
|
||||
(["x", "y", "z", "f"], ["X", "M", "Y", "Z"]),
|
||||
(["x", "y", "z", "f"], ["x", "M", "y", "Z"]),
|
||||
],
|
||||
)
|
||||
def test_initialize_stage(self, axes, axes_mapping):
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
stage = ASIStage(self.microscope_name, None, self.configuration)
|
||||
|
||||
# Attributes
|
||||
for axis in axes:
|
||||
assert hasattr(stage, f"{axis}_pos")
|
||||
assert hasattr(stage, f"{axis}_min")
|
||||
assert hasattr(stage, f"{axis}_max")
|
||||
assert getattr(stage, f"{axis}_pos") == 0
|
||||
assert (
|
||||
getattr(stage, f"{axis}_min")
|
||||
== self.stage_configuration["stage"][f"{axis}_min"]
|
||||
)
|
||||
assert (
|
||||
getattr(stage, f"{axis}_max")
|
||||
== self.stage_configuration["stage"][f"{axis}_max"]
|
||||
)
|
||||
|
||||
if axes_mapping is None:
|
||||
# using default mapping which is hard coded in pi.py
|
||||
default_mapping = {"x": "Z", "y": "Y", "z": "X", "f": "M"}
|
||||
for axis, device_axis in stage.axes_mapping.items():
|
||||
assert default_mapping[axis] == device_axis
|
||||
assert len(stage.axes_mapping) <= len(stage.axes)
|
||||
else:
|
||||
for i, axis in enumerate(axes):
|
||||
assert stage.axes_mapping[axis] == axes_mapping[i].upper()
|
||||
|
||||
assert stage.stage_limits is True
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], None),
|
||||
(["y"], None),
|
||||
(["x", "z"], None),
|
||||
(["f", "z"], None),
|
||||
(["x", "y", "z"], None),
|
||||
(["x", "y", "z", "f"], None),
|
||||
(["x"], ["Y"]),
|
||||
(["y"], ["Z"]),
|
||||
(["x", "z"], ["X", "Y"]),
|
||||
(["f", "z"], ["M", "X"]),
|
||||
(["x", "y", "z"], ["Y", "X", "M"]),
|
||||
(["x", "y", "z", "f"], ["X", "M", "Y", "Z"]),
|
||||
(["x", "y", "z", "f"], ["x", "M", "y", "Z"]),
|
||||
],
|
||||
)
|
||||
def test_report_position(self, axes, axes_mapping):
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
self.configuration["configuration"]["microscopes"][self.microscope_name][
|
||||
"zoom"
|
||||
] = {}
|
||||
self.configuration["configuration"]["microscopes"][self.microscope_name][
|
||||
"zoom"
|
||||
]["pixel_size"] = {"5X": 1.3}
|
||||
asi_stage = self.build_device_connection()
|
||||
stage = ASIStage(self.microscope_name, asi_stage, self.configuration)
|
||||
|
||||
for _ in range(10):
|
||||
pos_dict = {}
|
||||
for axis in axes:
|
||||
pos = random.randrange(-100, 500)
|
||||
pos_dict[f"{axis}_pos"] = float(pos)
|
||||
if axis == "theta":
|
||||
setattr(
|
||||
asi_stage.serial,
|
||||
f"{stage.axes_mapping[axis]}_abs",
|
||||
pos * 1000.0,
|
||||
)
|
||||
else:
|
||||
setattr(
|
||||
asi_stage.serial,
|
||||
f"{stage.axes_mapping[axis]}_abs",
|
||||
pos * 10.0,
|
||||
)
|
||||
temp_pos = stage.report_position()
|
||||
assert pos_dict == temp_pos
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], None),
|
||||
(["y"], None),
|
||||
(["x", "z"], None),
|
||||
(["f", "z"], None),
|
||||
(["x", "y", "z"], None),
|
||||
(["x", "y", "z", "f"], None),
|
||||
(["x"], ["Y"]),
|
||||
(["y"], ["Z"]),
|
||||
(["x", "z"], ["X", "Y"]),
|
||||
(["f", "z"], ["M", "X"]),
|
||||
(["x", "y", "z"], ["Y", "X", "M"]),
|
||||
(["x", "y", "z", "f"], ["X", "M", "Y", "Z"]),
|
||||
(["x", "y", "z", "f"], ["x", "M", "y", "Z"]),
|
||||
],
|
||||
)
|
||||
def test_move_axis_absolute(self, axes, axes_mapping):
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
asi_stage = self.build_device_connection()
|
||||
stage = ASIStage(self.microscope_name, asi_stage, self.configuration)
|
||||
self.random_single_axis_test(stage)
|
||||
stage.stage_limits = False
|
||||
self.random_single_axis_test(stage)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], None),
|
||||
(["y"], None),
|
||||
(["x", "z"], None),
|
||||
(["f", "z"], None),
|
||||
(["x", "y", "z"], None),
|
||||
(["x", "y", "z", "f"], None),
|
||||
(["x"], ["Y"]),
|
||||
(["y"], ["Z"]),
|
||||
(["x", "z"], ["X", "Y"]),
|
||||
(["f", "z"], ["M", "X"]),
|
||||
(["x", "y", "z"], ["Y", "X", "M"]),
|
||||
(["x", "y", "z", "f"], ["X", "M", "Y", "Z"]),
|
||||
(["x", "y", "z", "f"], ["x", "M", "y", "Z"]),
|
||||
],
|
||||
)
|
||||
def test_move_absolute(self, axes, axes_mapping):
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
asi_stage = self.build_device_connection()
|
||||
stage = ASIStage(self.microscope_name, asi_stage, self.configuration)
|
||||
self.random_multiple_axes_test(stage)
|
||||
stage.stage_limits = False
|
||||
self.random_multiple_axes_test(stage)
|
||||
244
test/model/devices/stages/test_mcl.py
Normal file
244
test/model/devices/stages/test_mcl.py
Normal file
@@ -0,0 +1,244 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
# Standard Library Imports
|
||||
import pytest
|
||||
import random
|
||||
|
||||
# Third Party Imports
|
||||
|
||||
# Local Imports
|
||||
from navigate.model.devices.stage.mcl import MCLStage
|
||||
|
||||
|
||||
class MockMCLController:
|
||||
def __init__(self):
|
||||
self.axes = ["x", "y", "z", "f", "aux"]
|
||||
for axis in self.axes:
|
||||
setattr(self, f"{axis}_abs", 0)
|
||||
self.MadlibError = Exception
|
||||
|
||||
def MCL_SingleReadN(self, axis, handle=None):
|
||||
try:
|
||||
return getattr(self, f"{axis}_abs")
|
||||
except Exception:
|
||||
raise self.MadlibError
|
||||
|
||||
def MCL_SingleWriteN(self, pos, axis, handle=None):
|
||||
setattr(self, f"{axis}_abs", pos)
|
||||
|
||||
def MCL_ReleaseHandle(self, handle):
|
||||
pass
|
||||
|
||||
|
||||
class TestStageMCL:
|
||||
"""Unit Test for StageBase Class"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_class(
|
||||
self, stage_configuration, random_single_axis_test, random_multiple_axes_test
|
||||
):
|
||||
self.microscope_name = "Mesoscale"
|
||||
self.configuration = {
|
||||
"configuration": {
|
||||
"microscopes": {self.microscope_name: stage_configuration}
|
||||
}
|
||||
}
|
||||
self.stage_configuration = stage_configuration
|
||||
self.stage_configuration["stage"]["hardware"]["type"] = "MCL"
|
||||
self.random_single_axis_test = random_single_axis_test
|
||||
self.random_multiple_axes_test = random_multiple_axes_test
|
||||
|
||||
def test_stage_attributes(self):
|
||||
stage = MCLStage(self.microscope_name, None, self.configuration)
|
||||
|
||||
# Methods
|
||||
assert hasattr(stage, "get_position_dict") and callable(
|
||||
getattr(stage, "get_position_dict")
|
||||
)
|
||||
assert hasattr(stage, "report_position") and callable(
|
||||
getattr(stage, "report_position")
|
||||
)
|
||||
assert hasattr(stage, "move_axis_absolute") and callable(
|
||||
getattr(stage, "move_axis_absolute")
|
||||
)
|
||||
assert hasattr(stage, "move_absolute") and callable(
|
||||
getattr(stage, "move_absolute")
|
||||
)
|
||||
assert hasattr(stage, "stop") and callable(getattr(stage, "stop"))
|
||||
assert hasattr(stage, "get_abs_position") and callable(
|
||||
getattr(stage, "get_abs_position")
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], None),
|
||||
(["y"], None),
|
||||
(["x", "z"], None),
|
||||
(["f", "z"], None),
|
||||
(["x", "y", "z"], None),
|
||||
(["x", "y", "z", "f"], None),
|
||||
(["x", "y", "z", "f", "theta"], None),
|
||||
(["x"], ["x"]),
|
||||
(["y"], ["z"]),
|
||||
(["x", "z"], ["y", "z"]),
|
||||
(["f", "z"], ["z", "x"]),
|
||||
(["x", "y", "z"], ["y", "x", "f"]),
|
||||
(["x", "y", "z", "f"], ["x", "z", "f", "y"]),
|
||||
(["x", "y", "z", "f", "theta"], ["z", "f", "x", "y", "aux"]),
|
||||
],
|
||||
)
|
||||
def test_initialize_stage(self, axes, axes_mapping):
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
stage = MCLStage(self.microscope_name, None, self.configuration)
|
||||
|
||||
# Attributes
|
||||
for axis in axes:
|
||||
assert hasattr(stage, f"{axis}_pos")
|
||||
assert hasattr(stage, f"{axis}_min")
|
||||
assert hasattr(stage, f"{axis}_max")
|
||||
assert getattr(stage, f"{axis}_pos") == 0
|
||||
assert (
|
||||
getattr(stage, f"{axis}_min")
|
||||
== self.stage_configuration["stage"][f"{axis}_min"]
|
||||
)
|
||||
assert (
|
||||
getattr(stage, f"{axis}_max")
|
||||
== self.stage_configuration["stage"][f"{axis}_max"]
|
||||
)
|
||||
|
||||
if axes_mapping is None:
|
||||
# using default mapping which is hard coded in mcl.py
|
||||
default_mapping = {"x": "x", "y": "y", "z": "z", "f": "f", "theta": "aux"}
|
||||
for axis, device_axis in stage.axes_mapping.items():
|
||||
assert default_mapping[axis] == device_axis
|
||||
assert len(stage.axes_mapping) <= len(stage.axes)
|
||||
else:
|
||||
for i, axis in enumerate(axes):
|
||||
assert stage.axes_mapping[axis] == axes_mapping[i]
|
||||
|
||||
assert stage.stage_limits is True
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], None),
|
||||
(["y"], None),
|
||||
(["x", "z"], None),
|
||||
(["f", "z"], None),
|
||||
(["x", "y", "z"], None),
|
||||
(["x", "y", "z", "f"], None),
|
||||
(["x", "y", "z", "f", "theta"], None),
|
||||
(["x"], ["x"]),
|
||||
(["y"], ["z"]),
|
||||
(["x", "z"], ["y", "z"]),
|
||||
(["f", "z"], ["z", "x"]),
|
||||
(["x", "y", "z"], ["y", "x", "f"]),
|
||||
(["x", "y", "z", "f"], ["x", "z", "f", "y"]),
|
||||
(["x", "y", "z", "f", "theta"], ["z", "f", "x", "y", "aux"]),
|
||||
],
|
||||
)
|
||||
def test_report_position(self, axes, axes_mapping):
|
||||
MCL_device = MockMCLController()
|
||||
device_connection = {"controller": MCL_device, "handle": None}
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
stage = MCLStage(self.microscope_name, device_connection, self.configuration)
|
||||
|
||||
for _ in range(10):
|
||||
pos_dict = {}
|
||||
for axis in axes:
|
||||
pos = random.randrange(-100, 500)
|
||||
pos_dict[f"{axis}_pos"] = float(pos)
|
||||
setattr(MCL_device, f"{stage.axes_mapping[axis]}_abs", float(pos))
|
||||
temp_pos = stage.report_position()
|
||||
assert pos_dict == temp_pos
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], None),
|
||||
(["y"], None),
|
||||
(["x", "z"], None),
|
||||
(["f", "z"], None),
|
||||
(["x", "y", "z"], None),
|
||||
(["x", "y", "z", "f"], None),
|
||||
(["x", "y", "z", "f", "theta"], None),
|
||||
(["x"], ["x"]),
|
||||
(["y"], ["z"]),
|
||||
(["x", "z"], ["y", "z"]),
|
||||
(["f", "z"], ["z", "x"]),
|
||||
(["x", "y", "z"], ["y", "x", "f"]),
|
||||
(["x", "y", "z", "f"], ["x", "z", "f", "y"]),
|
||||
(["x", "y", "z", "f", "theta"], ["z", "f", "x", "y", "aux"]),
|
||||
],
|
||||
)
|
||||
def test_move_axis_absolute(self, axes, axes_mapping):
|
||||
MCL_device = MockMCLController()
|
||||
device_connection = {"controller": MCL_device, "handle": None}
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
stage = MCLStage(self.microscope_name, device_connection, self.configuration)
|
||||
self.random_single_axis_test(stage)
|
||||
stage.stage_limits = False
|
||||
self.random_single_axis_test(stage)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], None),
|
||||
(["y"], None),
|
||||
(["x", "z"], None),
|
||||
(["f", "z"], None),
|
||||
(["x", "y", "z"], None),
|
||||
(["x", "y", "z", "f"], None),
|
||||
(["x", "y", "z", "f", "theta"], None),
|
||||
(["x"], ["x"]),
|
||||
(["y"], ["z"]),
|
||||
(["x", "z"], ["y", "z"]),
|
||||
(["f", "z"], ["z", "x"]),
|
||||
(["x", "y", "z"], ["y", "x", "f"]),
|
||||
(["x", "y", "z", "f"], ["x", "z", "f", "y"]),
|
||||
(["x", "y", "z", "f", "theta"], ["z", "f", "x", "y", "aux"]),
|
||||
],
|
||||
)
|
||||
def test_move_absolute(self, axes, axes_mapping):
|
||||
MCL_device = MockMCLController()
|
||||
device_connection = {"controller": MCL_device, "handle": None}
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
stage = MCLStage(self.microscope_name, device_connection, self.configuration)
|
||||
self.random_multiple_axes_test(stage)
|
||||
stage.stage_limits = False
|
||||
self.random_multiple_axes_test(stage)
|
||||
259
test/model/devices/stages/test_pi.py
Normal file
259
test/model/devices/stages/test_pi.py
Normal file
@@ -0,0 +1,259 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
# Standard Library Imports
|
||||
import pytest
|
||||
import random
|
||||
|
||||
# Third Party Imports
|
||||
from pipython import GCSError
|
||||
|
||||
# Local Imports
|
||||
from navigate.model.devices.stage.pi import PIStage
|
||||
|
||||
|
||||
class MockPIStage:
|
||||
def __init__(self):
|
||||
self.axes = [1, 2, 3, 4, 5]
|
||||
|
||||
for axis in self.axes:
|
||||
setattr(self, f"{axis}_abs", 0)
|
||||
|
||||
def MOV(self, pos_dict):
|
||||
for axis in pos_dict:
|
||||
if axis not in self.axes:
|
||||
continue
|
||||
setattr(self, f"{axis}_abs", pos_dict[axis])
|
||||
|
||||
def qPOS(self, axes):
|
||||
pos = {}
|
||||
for axis in axes:
|
||||
if axis not in self.axes:
|
||||
raise GCSError
|
||||
pos[str(axis)] = getattr(self, f"{axis}_abs")
|
||||
return pos
|
||||
|
||||
def STP(self, noraise=True):
|
||||
pass
|
||||
|
||||
def waitontarget(self, pi_device, timeout=5.0, **kwargs):
|
||||
pass
|
||||
|
||||
def CloseConnection(self):
|
||||
pass
|
||||
|
||||
|
||||
class TestStagePI:
|
||||
"""Unit Test for PI Stage Class"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_class(
|
||||
self, stage_configuration, random_single_axis_test, random_multiple_axes_test
|
||||
):
|
||||
self.microscope_name = "Mesoscale"
|
||||
self.configuration = {
|
||||
"configuration": {
|
||||
"microscopes": {self.microscope_name: stage_configuration}
|
||||
}
|
||||
}
|
||||
self.stage_configuration = stage_configuration
|
||||
self.stage_configuration["stage"]["hardware"]["type"] = "PI"
|
||||
self.random_single_axis_test = random_single_axis_test
|
||||
self.random_multiple_axes_test = random_multiple_axes_test
|
||||
|
||||
def test_stage_attributes(self):
|
||||
stage = PIStage(self.microscope_name, None, self.configuration)
|
||||
|
||||
# Methods
|
||||
assert hasattr(stage, "get_position_dict") and callable(
|
||||
getattr(stage, "get_position_dict")
|
||||
)
|
||||
assert hasattr(stage, "report_position") and callable(
|
||||
getattr(stage, "report_position")
|
||||
)
|
||||
assert hasattr(stage, "move_axis_absolute") and callable(
|
||||
getattr(stage, "move_axis_absolute")
|
||||
)
|
||||
assert hasattr(stage, "move_absolute") and callable(
|
||||
getattr(stage, "move_absolute")
|
||||
)
|
||||
assert hasattr(stage, "stop") and callable(getattr(stage, "stop"))
|
||||
assert hasattr(stage, "get_abs_position") and callable(
|
||||
getattr(stage, "get_abs_position")
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], None),
|
||||
(["y"], None),
|
||||
(["x", "z"], None),
|
||||
(["f", "z"], None),
|
||||
(["x", "y", "z"], None),
|
||||
(["x", "y", "z", "f"], None),
|
||||
(["x", "y", "z", "f", "theta"], None),
|
||||
(["x"], [1]),
|
||||
(["y"], [2]),
|
||||
(["x", "z"], [1, 3]),
|
||||
(["f", "z"], [2, 3]),
|
||||
(["x", "y", "z"], [1, 2, 3]),
|
||||
(["x", "y", "z", "f"], [1, 3, 2, 4]),
|
||||
(["x", "y", "z", "f", "theta"], [3, 5, 2, 1, 4]),
|
||||
],
|
||||
)
|
||||
def test_initialize_stage(self, axes, axes_mapping):
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
stage = PIStage(self.microscope_name, None, self.configuration)
|
||||
|
||||
# Attributes
|
||||
for axis in axes:
|
||||
assert hasattr(stage, f"{axis}_pos")
|
||||
assert hasattr(stage, f"{axis}_min")
|
||||
assert hasattr(stage, f"{axis}_max")
|
||||
assert getattr(stage, f"{axis}_pos") == 0
|
||||
assert (
|
||||
getattr(stage, f"{axis}_min")
|
||||
== self.stage_configuration["stage"][f"{axis}_min"]
|
||||
)
|
||||
assert (
|
||||
getattr(stage, f"{axis}_max")
|
||||
== self.stage_configuration["stage"][f"{axis}_max"]
|
||||
)
|
||||
|
||||
if axes_mapping is None:
|
||||
# using default mapping which is hard coded in pi.py
|
||||
default_mapping = {"x": 1, "y": 2, "z": 3, "f": 5, "theta": 4}
|
||||
for axis, device_axis in stage.axes_mapping.items():
|
||||
assert default_mapping[axis] == device_axis
|
||||
assert len(stage.axes_mapping) <= len(stage.axes)
|
||||
else:
|
||||
for i, axis in enumerate(axes):
|
||||
assert stage.axes_mapping[axis] == axes_mapping[i]
|
||||
|
||||
assert stage.stage_limits is True
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], None),
|
||||
(["y"], None),
|
||||
(["x", "z"], None),
|
||||
(["f", "z"], None),
|
||||
(["x", "y", "z"], None),
|
||||
(["x", "y", "z", "f"], None),
|
||||
(["x", "y", "z", "f", "theta"], None),
|
||||
(["x"], [1]),
|
||||
(["y"], [2]),
|
||||
(["x", "z"], [1, 3]),
|
||||
(["f", "z"], [2, 3]),
|
||||
(["x", "y", "z"], [1, 2, 3]),
|
||||
(["x", "y", "z", "f"], [1, 3, 2, 4]),
|
||||
(["x", "y", "z", "f", "theta"], [3, 5, 2, 1, 4]),
|
||||
],
|
||||
)
|
||||
def test_report_position(self, axes, axes_mapping):
|
||||
PI_device = MockPIStage()
|
||||
device_connection = {"pi_tools": PI_device, "pi_device": PI_device}
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
stage = PIStage(self.microscope_name, device_connection, self.configuration)
|
||||
|
||||
for _ in range(10):
|
||||
pos_dict = {}
|
||||
for axis in axes:
|
||||
pos = random.randrange(-100, 500)
|
||||
pos_dict[f"{axis}_pos"] = float(pos)
|
||||
if axis != "theta":
|
||||
setattr(PI_device, f"{stage.axes_mapping[axis]}_abs", pos / 1000)
|
||||
else:
|
||||
setattr(PI_device, f"{stage.axes_mapping[axis]}_abs", float(pos))
|
||||
temp_pos = stage.report_position()
|
||||
assert pos_dict == temp_pos
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], None),
|
||||
(["y"], None),
|
||||
(["x", "z"], None),
|
||||
(["f", "z"], None),
|
||||
(["x", "y", "z"], None),
|
||||
(["x", "y", "z", "f"], None),
|
||||
(["x", "y", "z", "f", "theta"], None),
|
||||
(["x"], [1]),
|
||||
(["y"], [2]),
|
||||
(["x", "z"], [1, 3]),
|
||||
(["f", "z"], [2, 3]),
|
||||
(["x", "y", "z"], [1, 2, 3]),
|
||||
(["x", "y", "z", "f"], [1, 3, 2, 4]),
|
||||
(["x", "y", "z", "f", "theta"], [3, 5, 2, 1, 4]),
|
||||
],
|
||||
)
|
||||
def test_move_axis_absolute(self, axes, axes_mapping):
|
||||
PI_device = MockPIStage()
|
||||
device_connection = {"pi_tools": PI_device, "pi_device": PI_device}
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
stage = PIStage(self.microscope_name, device_connection, self.configuration)
|
||||
self.random_single_axis_test(stage)
|
||||
stage.stage_limits = False
|
||||
self.random_single_axis_test(stage)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], None),
|
||||
(["y"], None),
|
||||
(["x", "z"], None),
|
||||
(["f", "z"], None),
|
||||
(["x", "y", "z"], None),
|
||||
(["x", "y", "z", "f"], None),
|
||||
(["x", "y", "z", "f", "theta"], None),
|
||||
(["x"], [1]),
|
||||
(["y"], [2]),
|
||||
(["x", "z"], [1, 3]),
|
||||
(["f", "z"], [2, 3]),
|
||||
(["x", "y", "z"], [1, 2, 3]),
|
||||
(["x", "y", "z", "f"], [1, 3, 2, 4]),
|
||||
(["x", "y", "z", "f", "theta"], [3, 5, 2, 1, 4]),
|
||||
],
|
||||
)
|
||||
def test_move_absolute(self, axes, axes_mapping):
|
||||
PI_device = MockPIStage()
|
||||
device_connection = {"pi_tools": PI_device, "pi_device": PI_device}
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
stage = PIStage(self.microscope_name, device_connection, self.configuration)
|
||||
self.random_multiple_axes_test(stage)
|
||||
stage.stage_limits = False
|
||||
self.random_multiple_axes_test(stage)
|
||||
231
test/model/devices/stages/test_stage_base.py
Normal file
231
test/model/devices/stages/test_stage_base.py
Normal file
@@ -0,0 +1,231 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
# Standard Library Imports
|
||||
import pytest
|
||||
import random
|
||||
|
||||
# Third Party Imports
|
||||
|
||||
# Local Imports
|
||||
from navigate.model.devices.stage.synthetic import SyntheticStage
|
||||
|
||||
|
||||
class TestStageBase:
|
||||
"""Unit Test for StageBase Class"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_class(self, stage_configuration):
|
||||
self.microscope_name = "Mesoscale"
|
||||
self.configuration = {
|
||||
"configuration": {
|
||||
"microscopes": {self.microscope_name: stage_configuration}
|
||||
}
|
||||
}
|
||||
self.stage_configuration = stage_configuration
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], None),
|
||||
(["y"], None),
|
||||
(["x", "z"], None),
|
||||
(["f", "z"], None),
|
||||
(["x", "y", "z"], None),
|
||||
(["x", "y", "z", "f"], None),
|
||||
(["x", "y", "z", "f", "theta"], None),
|
||||
(["x"], [1]),
|
||||
(["y"], [2]),
|
||||
(["x", "z"], [1, 3]),
|
||||
(["f", "z"], [2, 3]),
|
||||
(["x", "y", "z"], [1, 2, 3]),
|
||||
(["x", "y", "z", "f"], [1, 3, 2, 4]),
|
||||
(["x", "y", "z", "f", "theta"], [3, 5, 2, 1, 4]),
|
||||
],
|
||||
)
|
||||
def test_stage_attributes(self, axes, axes_mapping):
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
stage = SyntheticStage(self.microscope_name, None, self.configuration)
|
||||
|
||||
# Attributes
|
||||
for axis in axes:
|
||||
assert hasattr(stage, f"{axis}_pos")
|
||||
assert hasattr(stage, f"{axis}_min")
|
||||
assert hasattr(stage, f"{axis}_max")
|
||||
assert getattr(stage, f"{axis}_pos") == 0
|
||||
assert (
|
||||
getattr(stage, f"{axis}_min")
|
||||
== self.stage_configuration["stage"][f"{axis}_min"]
|
||||
)
|
||||
assert (
|
||||
getattr(stage, f"{axis}_max")
|
||||
== self.stage_configuration["stage"][f"{axis}_max"]
|
||||
)
|
||||
|
||||
# Check default axes mapping
|
||||
if axes_mapping is None:
|
||||
assert stage.axes_mapping == {axis: axis.capitalize() for axis in axes}
|
||||
else:
|
||||
for i, axis in enumerate(axes):
|
||||
assert stage.axes_mapping[axis] == axes_mapping[i]
|
||||
|
||||
assert stage.stage_limits is True
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_pos",
|
||||
[
|
||||
(["x"], [1]),
|
||||
(["y"], [2]),
|
||||
(["x", "z"], [1, 3]),
|
||||
(["f", "z"], [2, 3]),
|
||||
(["x", "y", "z"], [1, 2, 3]),
|
||||
(["x", "y", "z", "f"], [1, 3, 2, 4]),
|
||||
(["x", "y", "z", "f", "theta"], [3, 5, 2, 1, 4]),
|
||||
],
|
||||
)
|
||||
def test_get_position_dict(self, axes, axes_pos):
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
stage = SyntheticStage(self.microscope_name, None, self.configuration)
|
||||
for i, axis in enumerate(axes):
|
||||
setattr(stage, f"{axis}_pos", axes_pos[i])
|
||||
|
||||
pos_dict = stage.get_position_dict()
|
||||
for k, v in pos_dict.items():
|
||||
assert getattr(stage, k) == v
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], [1]),
|
||||
(["y"], [2]),
|
||||
(["x", "z"], [1, 3]),
|
||||
(["f", "z"], [2, 3]),
|
||||
(["x", "y", "z"], [1, 2, 3]),
|
||||
(["x", "y", "z", "f"], [1, 3, 2, 4]),
|
||||
(["x", "y", "z", "f", "theta"], [3, 5, 2, 1, 4]),
|
||||
],
|
||||
)
|
||||
def test_get_abs_position(self, axes, axes_mapping):
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
stage = SyntheticStage(self.microscope_name, None, self.configuration)
|
||||
|
||||
for axis in axes:
|
||||
axis_min = self.stage_configuration["stage"][f"{axis}_min"]
|
||||
axis_max = self.stage_configuration["stage"][f"{axis}_max"]
|
||||
# axis_abs_position inside the boundaries
|
||||
axis_abs = random.randrange(axis_min, axis_max)
|
||||
assert stage.get_abs_position(axis, axis_abs) == axis_abs
|
||||
|
||||
# axis_abs_position < axis_min
|
||||
axis_abs = axis_min - 10.5
|
||||
assert stage.get_abs_position(axis, axis_abs) == -1e50
|
||||
# turn off stage_limits
|
||||
stage.stage_limits = False
|
||||
assert stage.get_abs_position(axis, axis_abs) == axis_abs
|
||||
stage.stage_limits = True
|
||||
|
||||
# axis_abs_position > axis_max
|
||||
axis_abs = axis_max + 10.5
|
||||
assert stage.get_abs_position(axis, axis_abs) == -1e50
|
||||
# turn off stage_limits
|
||||
stage.stage_limits = False
|
||||
assert stage.get_abs_position(axis, axis_abs) == axis_abs
|
||||
stage.stage_limits = True
|
||||
|
||||
# axis is not supported
|
||||
all_axes = set(["x", "y", "z", "f", "theta"])
|
||||
sub_axes = all_axes - set(axes)
|
||||
for axis in sub_axes:
|
||||
assert stage.get_abs_position(axis, 1.0) == -1e50
|
||||
# turn off stage_limits
|
||||
stage.stage_limits = False
|
||||
assert stage.get_abs_position(axis, axis_abs) == -1e50
|
||||
stage.stage_limits = True
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], [1]),
|
||||
(["y"], [2]),
|
||||
(["x", "z"], [1, 3]),
|
||||
(["f", "z"], [2, 3]),
|
||||
(["x", "y", "z"], [1, 2, 3]),
|
||||
(["x", "y", "z", "f"], [1, 3, 2, 4]),
|
||||
(["x", "y", "z", "f", "theta"], [3, 5, 2, 1, 4]),
|
||||
],
|
||||
)
|
||||
def test_verify_abs_position(self, axes, axes_mapping):
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
stage = SyntheticStage(self.microscope_name, None, self.configuration)
|
||||
|
||||
move_dict = {}
|
||||
abs_dict = {}
|
||||
for axis in axes:
|
||||
axis_min = self.stage_configuration["stage"][f"{axis}_min"]
|
||||
axis_max = self.stage_configuration["stage"][f"{axis}_max"]
|
||||
# axis_abs_position inside the boundaries
|
||||
axis_abs = random.randrange(axis_min, axis_max)
|
||||
move_dict[f"{axis}_abs"] = axis_abs
|
||||
abs_dict[axis] = axis_abs
|
||||
assert stage.verify_abs_position(move_dict) == abs_dict
|
||||
|
||||
# turn off stage_limits
|
||||
stage.stage_limits = False
|
||||
axis = random.choice(axes)
|
||||
axis_min = self.stage_configuration["stage"][f"{axis}_min"]
|
||||
axis_max = self.stage_configuration["stage"][f"{axis}_max"]
|
||||
|
||||
# Test minimum boundary
|
||||
move_dict[f"{axis}_abs"] = axis_min - 1.5
|
||||
abs_dict[axis] = axis_min - 1.5
|
||||
assert stage.verify_abs_position(move_dict) == abs_dict
|
||||
|
||||
# Test maximum boundary
|
||||
move_dict[f"{axis}_abs"] = axis_max + 1.5
|
||||
abs_dict[axis] = axis_max + 1.5
|
||||
assert stage.verify_abs_position(move_dict) == abs_dict
|
||||
stage.stage_limits = True
|
||||
|
||||
# axis is not included in axes list
|
||||
axis_abs = random.randrange(axis_min, axis_max)
|
||||
move_dict[f"{axis}_abs"] = axis_abs
|
||||
abs_dict[axis] = axis_abs
|
||||
|
||||
move_dict["theta_abs"] = 180
|
||||
if "theta" in axes:
|
||||
abs_dict["theta"] = 180
|
||||
assert stage.verify_abs_position(move_dict) == abs_dict
|
||||
stage.stage_limits = False
|
||||
assert stage.verify_abs_position(move_dict) == abs_dict
|
||||
158
test/model/devices/stages/test_stage_ni.py
Normal file
158
test/model/devices/stages/test_stage_ni.py
Normal file
@@ -0,0 +1,158 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
# Standard Library Imports
|
||||
import pytest
|
||||
import random
|
||||
from unittest.mock import patch
|
||||
|
||||
# Third Party Imports
|
||||
|
||||
# Local Imports
|
||||
from navigate.model.devices.stage.ni import NIStage
|
||||
from test.model.dummy import DummyModel
|
||||
from navigate.tools.common_functions import copy_proxy_object
|
||||
|
||||
|
||||
class TestNIStage:
|
||||
"""Unit Test for NI stage Class"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_class(
|
||||
self,
|
||||
stage_configuration,
|
||||
ignore_obj,
|
||||
random_single_axis_test,
|
||||
random_multiple_axes_test,
|
||||
):
|
||||
dummy_model = DummyModel()
|
||||
self.configuration = copy_proxy_object(dummy_model.configuration)
|
||||
self.microscope_name = list(
|
||||
self.configuration["configuration"]["microscopes"].keys()
|
||||
)[0]
|
||||
self.configuration["configuration"]["microscopes"][self.microscope_name][
|
||||
"stage"
|
||||
] = stage_configuration["stage"]
|
||||
self.stage_configuration = stage_configuration
|
||||
self.stage_configuration["stage"]["hardware"]["type"] = "NI"
|
||||
self.stage_configuration["stage"]["hardware"]["volts_per_micron"] = "0.1"
|
||||
self.stage_configuration["stage"]["hardware"]["max"] = 5.0
|
||||
self.stage_configuration["stage"]["hardware"]["min"] = 0.1
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = ["PXI6259/ao2"]
|
||||
|
||||
self.daq = ignore_obj
|
||||
self.random_single_axis_test = random_single_axis_test
|
||||
self.random_multiple_axes_test = random_multiple_axes_test
|
||||
|
||||
@patch("nidaqmx.Task")
|
||||
def test_stage_attributes(self, *args):
|
||||
stage = NIStage(self.microscope_name, self.daq, self.configuration)
|
||||
|
||||
# Methods
|
||||
assert hasattr(stage, "get_position_dict") and callable(
|
||||
getattr(stage, "get_position_dict")
|
||||
)
|
||||
assert hasattr(stage, "report_position") and callable(
|
||||
getattr(stage, "report_position")
|
||||
)
|
||||
assert hasattr(stage, "move_axis_absolute") and callable(
|
||||
getattr(stage, "move_axis_absolute")
|
||||
)
|
||||
assert hasattr(stage, "move_absolute") and callable(
|
||||
getattr(stage, "move_absolute")
|
||||
)
|
||||
assert hasattr(stage, "stop") and callable(getattr(stage, "stop"))
|
||||
assert hasattr(stage, "get_abs_position") and callable(
|
||||
getattr(stage, "get_abs_position")
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("axes", [(["x"]), (["y"]), (["f"])])
|
||||
def test_initialize_stage(self, axes):
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
with patch("nidaqmx.Task"):
|
||||
stage = NIStage(self.microscope_name, self.daq, self.configuration)
|
||||
|
||||
# Attributes
|
||||
for axis in axes:
|
||||
assert hasattr(stage, f"{axis}_pos")
|
||||
assert hasattr(stage, f"{axis}_min")
|
||||
assert hasattr(stage, f"{axis}_max")
|
||||
assert getattr(stage, f"{axis}_pos") == 0
|
||||
assert (
|
||||
getattr(stage, f"{axis}_min")
|
||||
== self.stage_configuration["stage"][f"{axis}_min"]
|
||||
)
|
||||
assert (
|
||||
getattr(stage, f"{axis}_max")
|
||||
== self.stage_configuration["stage"][f"{axis}_max"]
|
||||
)
|
||||
|
||||
for i, axis in enumerate(axes):
|
||||
assert (
|
||||
stage.axes_mapping[axis]
|
||||
== self.stage_configuration["stage"]["hardware"]["axes_mapping"][i]
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("axes", [(["x"]), (["y"]), (["f"])])
|
||||
def test_report_position(self, axes):
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
with patch("nidaqmx.Task"):
|
||||
stage = NIStage(self.microscope_name, self.daq, self.configuration)
|
||||
|
||||
for _ in range(10):
|
||||
pos_dict = {}
|
||||
for axis in axes:
|
||||
pos = random.randrange(-100, 500)
|
||||
pos_dict[f"{axis}_pos"] = float(pos)
|
||||
setattr(stage, f"{axis}_pos", float(pos))
|
||||
temp_pos = stage.report_position()
|
||||
assert pos_dict == temp_pos
|
||||
|
||||
@pytest.mark.parametrize("axes", [(["x"]), (["y"]), (["f"])])
|
||||
def test_move_axis_absolute(self, axes):
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
with patch("nidaqmx.Task"):
|
||||
stage = NIStage(self.microscope_name, self.daq, self.configuration)
|
||||
|
||||
self.random_single_axis_test(stage)
|
||||
stage.stage_limits = False
|
||||
self.random_single_axis_test(stage)
|
||||
|
||||
@pytest.mark.parametrize("axes", [(["x"]), (["y"]), (["f"])])
|
||||
def test_move_absolute(self, axes):
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
with patch("nidaqmx.Task"):
|
||||
stage = NIStage(self.microscope_name, self.daq, self.configuration)
|
||||
|
||||
self.random_multiple_axes_test(stage)
|
||||
stage.stage_limits = False
|
||||
self.random_multiple_axes_test(stage)
|
||||
298
test/model/devices/stages/test_sutter.py
Normal file
298
test/model/devices/stages/test_sutter.py
Normal file
@@ -0,0 +1,298 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
# Standard Library Imports
|
||||
import pytest
|
||||
import random
|
||||
|
||||
# Third Party Imports
|
||||
|
||||
# Local Imports
|
||||
from navigate.model.devices.stage.sutter import MP285Stage
|
||||
from navigate.model.devices.APIs.sutter.MP285 import MP285
|
||||
|
||||
|
||||
class MockMP285Stage:
|
||||
def __init__(self, ignore_obj):
|
||||
self.axes = ["x", "y", "z"]
|
||||
for axis in self.axes:
|
||||
setattr(self, f"{axis}_abs", 0)
|
||||
self.input_buffer = []
|
||||
self.output_buffer = []
|
||||
self.in_waiting = 0
|
||||
self.ignore_obj = ignore_obj
|
||||
|
||||
def open(self):
|
||||
pass
|
||||
|
||||
def reset_input_buffer(self):
|
||||
self.input_buffer = []
|
||||
|
||||
def reset_output_buffer(self):
|
||||
self.output_buffer = []
|
||||
|
||||
def write(self, command):
|
||||
if command == bytes.fromhex("63") + bytes.fromhex("0d"):
|
||||
# get current x, y, and z position
|
||||
self.output_buffer.append(
|
||||
self.x_abs.to_bytes(4, byteorder="little", signed=True)
|
||||
+ self.y_abs.to_bytes(4, byteorder="little", signed=True)
|
||||
+ self.z_abs.to_bytes(4, byteorder="little", signed=True)
|
||||
+ bytes.fromhex("0d")
|
||||
)
|
||||
self.in_waiting += 13
|
||||
elif (
|
||||
command[0] == int("6d", 16)
|
||||
and len(command) == 14
|
||||
and command[-1] == int("0d", 16)
|
||||
):
|
||||
# move x, y, and z to specific position
|
||||
self.x_abs = int.from_bytes(command[1:5], byteorder="little", signed=True)
|
||||
self.y_abs = int.from_bytes(command[5:9], byteorder="little", signed=True)
|
||||
self.z_abs = int.from_bytes(command[9:13], byteorder="little", signed=True)
|
||||
self.output_buffer.append(bytes.fromhex("0d"))
|
||||
self.in_waiting += 1
|
||||
elif (
|
||||
command[0] == int("56", 16)
|
||||
and len(command) == 4
|
||||
and command[-1] == int("0d", 16)
|
||||
):
|
||||
# set resolution and velocity
|
||||
self.output_buffer.append(bytes.fromhex("0d"))
|
||||
self.in_waiting += 1
|
||||
elif command[0] == int("03", 16) and len(command) == 1:
|
||||
# interrupt move
|
||||
self.output_buffer.append(bytes.fromhex("0d"))
|
||||
self.in_waiting += 1
|
||||
elif command == bytes.fromhex("61") + bytes.fromhex("0d"):
|
||||
# set absolute mode
|
||||
self.output_buffer.append(bytes.fromhex("0d"))
|
||||
self.in_waiting += 1
|
||||
elif command == bytes.fromhex("62") + bytes.fromhex("0d"):
|
||||
# set relative mode
|
||||
self.in_waiting += 1
|
||||
self.output_buffer.append(bytes.fromhex("0d"))
|
||||
|
||||
def read_until(self, expected, size=100):
|
||||
return self.output_buffer.pop(0)
|
||||
|
||||
def read(self, byte_num=1):
|
||||
self.in_waiting -= len(self.output_buffer[0])
|
||||
return self.output_buffer.pop(0)
|
||||
|
||||
def __getattr__(self, __name: str):
|
||||
return self.ignore_obj
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mp285_serial_device(ignore_obj):
|
||||
return MockMP285Stage(ignore_obj)
|
||||
|
||||
|
||||
class TestStageSutter:
|
||||
"""Unit Test for StageBase Class"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_class(
|
||||
self,
|
||||
stage_configuration,
|
||||
mp285_serial_device,
|
||||
random_single_axis_test,
|
||||
random_multiple_axes_test,
|
||||
):
|
||||
self.microscope_name = "Mesoscale"
|
||||
self.configuration = {
|
||||
"configuration": {
|
||||
"microscopes": {self.microscope_name: stage_configuration}
|
||||
}
|
||||
}
|
||||
self.stage_configuration = stage_configuration
|
||||
self.stage_configuration["stage"]["hardware"]["type"] = "MP285"
|
||||
self.mp285_serial_device = mp285_serial_device
|
||||
self.random_single_axis_test = random_single_axis_test
|
||||
self.random_multiple_axes_test = random_multiple_axes_test
|
||||
|
||||
def build_device_connection(self):
|
||||
port = self.stage_configuration["stage"]["hardware"]["port"]
|
||||
baudrate = self.stage_configuration["stage"]["hardware"]["baudrate"]
|
||||
timeout = 5.0
|
||||
|
||||
mp285 = MP285(port, baudrate, timeout)
|
||||
mp285.serial = self.mp285_serial_device
|
||||
mp285.connect_to_serial()
|
||||
return mp285
|
||||
|
||||
def test_stage_attributes(self):
|
||||
stage = MP285Stage(
|
||||
self.microscope_name, self.build_device_connection(), self.configuration
|
||||
)
|
||||
|
||||
# Methods
|
||||
assert hasattr(stage, "get_position_dict") and callable(
|
||||
getattr(stage, "get_position_dict")
|
||||
)
|
||||
assert hasattr(stage, "report_position") and callable(
|
||||
getattr(stage, "report_position")
|
||||
)
|
||||
assert hasattr(stage, "move_absolute") and callable(
|
||||
getattr(stage, "move_absolute")
|
||||
)
|
||||
assert hasattr(stage, "stop") and callable(getattr(stage, "stop"))
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], None),
|
||||
(["y"], None),
|
||||
(["x", "z"], None),
|
||||
(["f", "z"], None),
|
||||
(["x", "y", "z"], None),
|
||||
(["x"], ["y"]),
|
||||
(["y"], ["z"]),
|
||||
(["x", "z"], ["y", "z"]),
|
||||
(["f", "z"], ["x", "z"]),
|
||||
(["x", "y", "z"], ["y", "z", "x"]),
|
||||
],
|
||||
)
|
||||
def test_initialize_stage(self, axes, axes_mapping):
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
stage = MP285Stage(
|
||||
self.microscope_name, self.build_device_connection(), self.configuration
|
||||
)
|
||||
|
||||
# Attributes
|
||||
for axis in axes:
|
||||
assert hasattr(stage, f"{axis}_pos")
|
||||
assert hasattr(stage, f"{axis}_min")
|
||||
assert hasattr(stage, f"{axis}_max")
|
||||
assert getattr(stage, f"{axis}_pos") == 0
|
||||
assert (
|
||||
getattr(stage, f"{axis}_min")
|
||||
== self.stage_configuration["stage"][f"{axis}_min"]
|
||||
)
|
||||
assert (
|
||||
getattr(stage, f"{axis}_max")
|
||||
== self.stage_configuration["stage"][f"{axis}_max"]
|
||||
)
|
||||
|
||||
if axes_mapping is None:
|
||||
# using default mapping which is hard coded in sutter.py
|
||||
default_mapping = {"x": "x", "y": "y", "z": "z"}
|
||||
for axis, device_axis in stage.axes_mapping.items():
|
||||
assert default_mapping[axis] == device_axis
|
||||
assert len(stage.axes_mapping) <= len(stage.axes)
|
||||
else:
|
||||
for i, axis in enumerate(axes):
|
||||
assert stage.axes_mapping[axis] == axes_mapping[i]
|
||||
|
||||
assert stage.stage_limits is True
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], None),
|
||||
(["y"], None),
|
||||
(["x", "z"], None),
|
||||
(["f", "z"], None),
|
||||
(["x", "y", "z"], None),
|
||||
(["x"], ["y"]),
|
||||
(["y"], ["z"]),
|
||||
(["x", "z"], ["y", "z"]),
|
||||
(["f", "z"], ["x", "z"]),
|
||||
(["x", "y", "z"], ["y", "z", "x"]),
|
||||
],
|
||||
)
|
||||
def test_report_position(self, axes, axes_mapping):
|
||||
mp285_stage = self.build_device_connection()
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
stage = MP285Stage(self.microscope_name, mp285_stage, self.configuration)
|
||||
for _ in range(10):
|
||||
pos_dict = {}
|
||||
for axis in axes:
|
||||
pos = random.randrange(-100, 500)
|
||||
if axis in stage.axes_mapping:
|
||||
pos_dict[f"{axis}_pos"] = pos * 0.04
|
||||
setattr(mp285_stage.serial, f"{stage.axes_mapping[axis]}_abs", pos)
|
||||
else:
|
||||
pos_dict[f"{axis}_pos"] = 0
|
||||
temp_pos = stage.report_position()
|
||||
assert pos_dict == temp_pos
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], None),
|
||||
(["y"], None),
|
||||
(["x", "z"], None),
|
||||
(["f", "z"], None),
|
||||
(["x", "y", "z"], None),
|
||||
(["x"], ["y"]),
|
||||
(["y"], ["z"]),
|
||||
(["x", "z"], ["y", "z"]),
|
||||
(["f", "z"], ["x", "z"]),
|
||||
(["x", "y", "z"], ["y", "z", "x"]),
|
||||
],
|
||||
)
|
||||
def test_move_axis_absolute(self, axes, axes_mapping):
|
||||
mp285_stage = self.build_device_connection()
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
stage = MP285Stage(self.microscope_name, mp285_stage, self.configuration)
|
||||
self.random_single_axis_test(stage)
|
||||
stage.stage_limits = False
|
||||
self.random_single_axis_test(stage)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], None),
|
||||
(["y"], None),
|
||||
(["x", "z"], None),
|
||||
(["f", "z"], None),
|
||||
(["x", "y", "z"], None),
|
||||
(["x"], ["y"]),
|
||||
(["y"], ["z"]),
|
||||
(["x", "z"], ["y", "z"]),
|
||||
(["f", "z"], ["x", "z"]),
|
||||
(["x", "y", "z"], ["y", "z", "x"]),
|
||||
],
|
||||
)
|
||||
def test_move_absolute(self, axes, axes_mapping):
|
||||
mp285_stage = self.build_device_connection()
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
stage = MP285Stage(self.microscope_name, mp285_stage, self.configuration)
|
||||
self.random_multiple_axes_test(stage)
|
||||
stage.stage_limits = False
|
||||
self.random_multiple_axes_test(stage)
|
||||
253
test/model/devices/stages/test_tl_kcube_inertial.py
Normal file
253
test/model/devices/stages/test_tl_kcube_inertial.py
Normal file
@@ -0,0 +1,253 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
# Standard Library Imports
|
||||
import pytest
|
||||
import random
|
||||
|
||||
# Third Party Imports
|
||||
|
||||
# Local Imports
|
||||
from navigate.model.devices.stage.thorlabs import KIM001Stage
|
||||
|
||||
|
||||
class MockKimController:
|
||||
# mocks single serial number device
|
||||
def __init__(self, ignore_obj):
|
||||
self.axes = [1, 2, 3, 4]
|
||||
self.ignore_obj = ignore_obj
|
||||
|
||||
for axis in self.axes:
|
||||
setattr(self, f"{axis}_abs", 0)
|
||||
|
||||
def KIM_RequestCurrentPosition(self, serial_number, axis):
|
||||
pass
|
||||
|
||||
def KIM_GetCurrentPosition(self, serial_number, axis):
|
||||
return getattr(self, f"{axis}_abs", 0)
|
||||
|
||||
def KIM_MoveAbsolute(self, serial_number, axis, pos: int):
|
||||
if axis in self.axes:
|
||||
setattr(self, f"{axis}_abs", int(pos))
|
||||
|
||||
def __getattr__(self, __name: str):
|
||||
return self.ignore_obj
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def kim_controller(ignore_obj):
|
||||
return MockKimController(ignore_obj)
|
||||
|
||||
|
||||
class TestStageTlKCubeInertial:
|
||||
"""Unit Test for StageBase Class"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_class(
|
||||
self,
|
||||
stage_configuration,
|
||||
kim_controller,
|
||||
random_single_axis_test,
|
||||
random_multiple_axes_test,
|
||||
):
|
||||
self.microscope_name = "Mesoscale"
|
||||
self.configuration = {
|
||||
"configuration": {
|
||||
"microscopes": {self.microscope_name: stage_configuration}
|
||||
}
|
||||
}
|
||||
self.stage_configuration = stage_configuration
|
||||
self.stage_configuration["stage"]["hardware"]["type"] = "Thorlabs"
|
||||
self.kim_controller = kim_controller
|
||||
self.random_single_axis_test = random_single_axis_test
|
||||
self.random_multiple_axes_test = random_multiple_axes_test
|
||||
|
||||
def test_stage_attributes(self):
|
||||
stage = KIM001Stage(
|
||||
self.microscope_name, self.kim_controller, self.configuration
|
||||
)
|
||||
|
||||
# Methods
|
||||
assert hasattr(stage, "get_position_dict") and callable(
|
||||
getattr(stage, "get_position_dict")
|
||||
)
|
||||
assert hasattr(stage, "report_position") and callable(
|
||||
getattr(stage, "report_position")
|
||||
)
|
||||
assert hasattr(stage, "move_axis_absolute") and callable(
|
||||
getattr(stage, "move_axis_absolute")
|
||||
)
|
||||
assert hasattr(stage, "move_absolute") and callable(
|
||||
getattr(stage, "move_absolute")
|
||||
)
|
||||
assert hasattr(stage, "stop") and callable(getattr(stage, "stop"))
|
||||
assert hasattr(stage, "get_abs_position") and callable(
|
||||
getattr(stage, "get_abs_position")
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], None),
|
||||
(["y"], None),
|
||||
(["x", "z"], None),
|
||||
(["f", "z"], None),
|
||||
(["x", "y", "z"], None),
|
||||
(["x", "y", "z", "f"], None),
|
||||
(["x"], [1]),
|
||||
(["y"], [3]),
|
||||
(["x", "z"], [3, 1]),
|
||||
(["f", "z"], [1, 4]),
|
||||
(["x", "y", "z"], [1, 2, 4]),
|
||||
(["x", "y", "z", "f"], [1, 3, 2, 4]),
|
||||
],
|
||||
)
|
||||
def test_initialize_stage(self, axes, axes_mapping):
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
stage = KIM001Stage(
|
||||
self.microscope_name, self.kim_controller, self.configuration
|
||||
)
|
||||
|
||||
# Attributes
|
||||
for axis in axes:
|
||||
assert hasattr(stage, f"{axis}_pos")
|
||||
assert hasattr(stage, f"{axis}_min")
|
||||
assert hasattr(stage, f"{axis}_max")
|
||||
assert getattr(stage, f"{axis}_pos") == 0
|
||||
assert (
|
||||
getattr(stage, f"{axis}_min")
|
||||
== self.stage_configuration["stage"][f"{axis}_min"]
|
||||
)
|
||||
assert (
|
||||
getattr(stage, f"{axis}_max")
|
||||
== self.stage_configuration["stage"][f"{axis}_max"]
|
||||
)
|
||||
|
||||
if axes_mapping is None:
|
||||
# using default mapping which is hard coded in pi.py
|
||||
default_mapping = {"x": 4, "y": 2, "z": 3, "f": 1}
|
||||
for axis, device_axis in stage.axes_mapping.items():
|
||||
assert default_mapping[axis] == device_axis
|
||||
assert len(stage.axes_mapping) <= len(stage.axes)
|
||||
else:
|
||||
for i, axis in enumerate(axes):
|
||||
assert stage.axes_mapping[axis] == axes_mapping[i]
|
||||
|
||||
assert stage.stage_limits is True
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], None),
|
||||
(["y"], None),
|
||||
(["x", "z"], None),
|
||||
(["f", "z"], None),
|
||||
(["x", "y", "z"], None),
|
||||
(["x", "y", "z", "f"], None),
|
||||
(["x"], [1]),
|
||||
(["y"], [3]),
|
||||
(["x", "z"], [3, 1]),
|
||||
(["f", "z"], [1, 4]),
|
||||
(["x", "y", "z"], [1, 2, 4]),
|
||||
(["x", "y", "z", "f"], [1, 3, 2, 4]),
|
||||
],
|
||||
)
|
||||
def test_report_position(self, axes, axes_mapping):
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
stage = KIM001Stage(
|
||||
self.microscope_name, self.kim_controller, self.configuration
|
||||
)
|
||||
|
||||
for _ in range(10):
|
||||
pos_dict = {}
|
||||
for axis in axes:
|
||||
pos = random.randrange(-100, 500)
|
||||
pos_dict[f"{axis}_pos"] = float(pos)
|
||||
setattr(self.kim_controller, f"{stage.axes_mapping[axis]}_abs", pos)
|
||||
temp_pos = stage.report_position()
|
||||
assert pos_dict == temp_pos
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], None),
|
||||
(["y"], None),
|
||||
(["x", "z"], None),
|
||||
(["f", "z"], None),
|
||||
(["x", "y", "z"], None),
|
||||
(["x", "y", "z", "f"], None),
|
||||
(["x"], [1]),
|
||||
(["y"], [3]),
|
||||
(["x", "z"], [3, 1]),
|
||||
(["f", "z"], [1, 4]),
|
||||
(["x", "y", "z"], [1, 2, 4]),
|
||||
(["x", "y", "z", "f"], [1, 3, 2, 4]),
|
||||
],
|
||||
)
|
||||
def test_move_axis_absolute(self, axes, axes_mapping):
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
stage = KIM001Stage(
|
||||
self.microscope_name, self.kim_controller, self.configuration
|
||||
)
|
||||
self.random_single_axis_test(stage)
|
||||
stage.stage_limits = False
|
||||
self.random_single_axis_test(stage)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axes, axes_mapping",
|
||||
[
|
||||
(["x"], None),
|
||||
(["y"], None),
|
||||
(["x", "z"], None),
|
||||
(["f", "z"], None),
|
||||
(["x", "y", "z"], None),
|
||||
(["x", "y", "z", "f"], None),
|
||||
(["x"], [1]),
|
||||
(["y"], [3]),
|
||||
(["x", "z"], [3, 1]),
|
||||
(["f", "z"], [1, 4]),
|
||||
(["x", "y", "z"], [1, 2, 4]),
|
||||
(["x", "y", "z", "f"], [1, 3, 2, 4]),
|
||||
],
|
||||
)
|
||||
def test_move_absolute(self, axes, axes_mapping):
|
||||
self.stage_configuration["stage"]["hardware"]["axes"] = axes
|
||||
self.stage_configuration["stage"]["hardware"]["axes_mapping"] = axes_mapping
|
||||
stage = KIM001Stage(
|
||||
self.microscope_name, self.kim_controller, self.configuration
|
||||
)
|
||||
self.random_multiple_axes_test(stage)
|
||||
stage.stage_limits = False
|
||||
self.random_multiple_axes_test(stage)
|
||||
205
test/model/devices/stages/test_tl_kcube_steppermotor.py
Normal file
205
test/model/devices/stages/test_tl_kcube_steppermotor.py
Normal file
@@ -0,0 +1,205 @@
|
||||
# Standard Library Imports
|
||||
import time
|
||||
import unittest
|
||||
|
||||
# Third Party Imports
|
||||
import pytest
|
||||
|
||||
# Local Imports
|
||||
from navigate.model.devices.stage.thorlabs import KST101Stage
|
||||
|
||||
|
||||
@pytest.mark.hardware
|
||||
class TestStageClass(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Create configuration for microscope stage
|
||||
self.serial_number = 26001318
|
||||
self.dv_units = 20000000
|
||||
self.real_units = 9.957067 # mm
|
||||
self.dv_per_mm = self.dv_units / self.real_units
|
||||
self.mm_per_dv = self.real_units / self.dv_units
|
||||
self.microscope_name = "test"
|
||||
self.config = {
|
||||
"configuration": {
|
||||
"microscopes": {
|
||||
f"{self.microscope_name}": {
|
||||
"stage": {
|
||||
"hardware": {
|
||||
"serial_number": str(self.serial_number),
|
||||
"axes": "f",
|
||||
"axes_mapping": [1],
|
||||
"device_units_per_mm": self.dv_per_mm,
|
||||
"f_min": 0,
|
||||
"f_max": 25,
|
||||
},
|
||||
"f_min": 0,
|
||||
"f_max": 25,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Create the stage controller class
|
||||
self.stage = KST101Stage(
|
||||
microscope_name=self.microscope_name,
|
||||
device_connection=None,
|
||||
configuration=self.config,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
self.kcube_connection.KST_Close(str(self.serial_number))
|
||||
|
||||
def test_homing(self):
|
||||
"""Test the homing function"""
|
||||
self.stage.run_homing()
|
||||
|
||||
def test_move_axis_absolute(self):
|
||||
distance = 0.100
|
||||
|
||||
# Get the current position
|
||||
self.stage.report_position()
|
||||
start = self.stage.f_pos
|
||||
print(f"starting stage position = {start}")
|
||||
|
||||
# Move the target distance
|
||||
target = start + distance
|
||||
self.stage.move_axis_absolute("f", target, True)
|
||||
|
||||
# Read the position and report
|
||||
self.stage.report_position()
|
||||
end = self.stage.f_pos
|
||||
|
||||
print(
|
||||
f"The final position in device units:{end/self.dv_per_mm}, "
|
||||
f"in real units:{end}mm,\n",
|
||||
f"Distance moved = {(end-start)}mm",
|
||||
)
|
||||
|
||||
def test_move_absolute(self):
|
||||
distance = 0.200
|
||||
|
||||
# Get the current position
|
||||
self.stage.report_position()
|
||||
start = self.stage.f_pos
|
||||
print(f"starting stage position = {start}")
|
||||
|
||||
# Move the target distance
|
||||
target = start + distance
|
||||
self.stage.move_to_position(target, True)
|
||||
|
||||
# Read the position and report
|
||||
self.stage.report_position()
|
||||
end = self.stage.f_pos
|
||||
|
||||
print(
|
||||
f"The final position in device units:{end}, in real units:{end}mm,\n",
|
||||
f"Distance moved = {(end-start)}mm",
|
||||
)
|
||||
|
||||
def test_move_to_position(self):
|
||||
distance = 0.100
|
||||
|
||||
# Get the current position
|
||||
self.stage.report_position()
|
||||
start = self.stage.f_pos
|
||||
print(f"starting stage position = {start:.4f}")
|
||||
|
||||
# move target distance, wait till done
|
||||
self.stage.move_to_position(start + distance, True)
|
||||
|
||||
# get the final position
|
||||
self.stage.report_position()
|
||||
end = self.stage.f_pos
|
||||
print(f"End stage position = {end:.4f}", f"distance moved = {end-start:.6f}")
|
||||
|
||||
|
||||
@pytest.mark.hardware
|
||||
class TestKSTDeviceController(unittest.TestCase):
|
||||
def setUp(self):
|
||||
|
||||
# test build connection function
|
||||
self.serial_number = 26001318
|
||||
|
||||
# perform calibration
|
||||
dv_units = 20000000
|
||||
real_units = 9.957067 # mm
|
||||
self.dv_per_mm = dv_units / real_units
|
||||
|
||||
# Open connection to stage
|
||||
self.kcube_connection = KST101Stage.connect(self.serial_number)
|
||||
time.sleep(2)
|
||||
|
||||
# Move the stage to middle of travel
|
||||
self.kcube_connection.KST_MoveToPosition(
|
||||
str(self.serial_number), int(12.5 * self.dv_per_mm)
|
||||
)
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
current_pos = self.kcube_connection.KST_GetCurrentPosition(
|
||||
str(self.serial_number)
|
||||
)
|
||||
print(f"Stage currently at:{current_pos} dvUnits")
|
||||
|
||||
def tearDown(self):
|
||||
self.kcube_connection.KST_Close(str(self.serial_number))
|
||||
|
||||
def test_move(self):
|
||||
"""Test how long commands take to execute move some distance"""
|
||||
distance = 12.5
|
||||
start = self.kcube_connection.KST_GetCurrentPosition(str(self.serial_number))
|
||||
final_position = start + distance
|
||||
|
||||
self.kcube_connection.KST_MoveToPosition(
|
||||
str(self.serial_number), int(final_position * self.dv_per_mm)
|
||||
)
|
||||
time.sleep(5)
|
||||
|
||||
tstart = time.time()
|
||||
self.kcube_connection.KST_MoveToPosition(str(self.serial_number), start)
|
||||
|
||||
pos = None
|
||||
while pos != start:
|
||||
pos = self.kcube_connection.KST_GetCurrentPosition(str(self.serial_number))
|
||||
tend = time.time()
|
||||
|
||||
print(f"it takes {tend - tstart:.3f}s to move {distance:.3}mm")
|
||||
|
||||
def test_jog(self):
|
||||
"""Test MoveJog"""
|
||||
# get the initial position
|
||||
start = self.kcube_connection.KST_GetCurrentPosition(str(self.serial_number))
|
||||
|
||||
# Test a short jog
|
||||
self.kcube_connection.KST_MoveJog(str(self.serial_number), 1)
|
||||
time.sleep(2)
|
||||
self.kcube_connection.KST_MoveStop(str(self.serial_number))
|
||||
|
||||
time.sleep(2)
|
||||
# read stage and make sure it moved
|
||||
jog_pos = self.kcube_connection.KST_GetCurrentPosition(str(self.serial_number))
|
||||
print(f"JogMove moved from {start} to {jog_pos}, starting jog back...")
|
||||
|
||||
self.kcube_connection.KST_MoveJog(str(self.serial_number), 2)
|
||||
time.sleep(2)
|
||||
self.kcube_connection.KST_MoveStop(str(self.serial_number))
|
||||
|
||||
time.sleep(2)
|
||||
end = self.kcube_connection.KST_GetCurrentPosition(str(self.serial_number))
|
||||
print(f"JogMove back moved from {jog_pos} to {end}")
|
||||
|
||||
def test_polling(self):
|
||||
"""Start polling, then run the jog test"""
|
||||
print("testing polling")
|
||||
# start polling
|
||||
self.kcube_connection.KST_StartPolling(str(self.serial_number), 100)
|
||||
|
||||
# Run Jog during active polling
|
||||
self.test_jog()
|
||||
|
||||
# End polling
|
||||
self.kcube_connection.KST_StopPolling(str(self.serial_number))
|
||||
# pos = self.kcube_connection.KST_GetCurrentPosition(str(self.serial_number))
|
||||
|
||||
# print(f"final position: {pos}")
|
||||
80
test/model/devices/test_synthetic_hardware.py
Normal file
80
test/model/devices/test_synthetic_hardware.py
Normal file
@@ -0,0 +1,80 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below)
|
||||
# provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
# Standard Library Imports
|
||||
import pytest
|
||||
|
||||
# Third Party Imports
|
||||
|
||||
|
||||
# Local Imports
|
||||
class TestSyntheticHardware:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_class(self, dummy_model):
|
||||
self.dummy_model = dummy_model
|
||||
self.microscope_name = "Mesoscale"
|
||||
|
||||
def test_synthetic_daq(self):
|
||||
from navigate.model.devices.daq.synthetic import SyntheticDAQ
|
||||
|
||||
SyntheticDAQ(self.dummy_model.configuration)
|
||||
|
||||
def test_synthetic_camera(self):
|
||||
from navigate.model.devices.camera.synthetic import (
|
||||
SyntheticCamera,
|
||||
SyntheticCameraController,
|
||||
)
|
||||
|
||||
scc = SyntheticCameraController()
|
||||
SyntheticCamera(self.microscope_name, scc, self.dummy_model.configuration)
|
||||
|
||||
def test_synthetic_stage(self):
|
||||
from navigate.model.devices.stage.synthetic import SyntheticStage
|
||||
|
||||
SyntheticStage(self.microscope_name, None, self.dummy_model.configuration)
|
||||
|
||||
def test_synthetic_zoom(self):
|
||||
from navigate.model.devices.zoom.synthetic import SyntheticZoom
|
||||
|
||||
SyntheticZoom(self.microscope_name, None, self.dummy_model.configuration)
|
||||
|
||||
def test_synthetic_shutter(self):
|
||||
from navigate.model.devices.shutter.synthetic import SyntheticShutter
|
||||
|
||||
SyntheticShutter(self.microscope_name, None, self.dummy_model.configuration)
|
||||
|
||||
def test_synthetic_laser(self):
|
||||
from navigate.model.devices.laser.synthetic import SyntheticLaser
|
||||
|
||||
SyntheticLaser(self.microscope_name, None, self.dummy_model.configuration, 0)
|
||||
85
test/model/devices/zoom/test_base.py
Normal file
85
test/model/devices/zoom/test_base.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dummy_zoom(dummy_model):
|
||||
from navigate.model.devices.zoom.synthetic import SyntheticZoom
|
||||
|
||||
return SyntheticZoom(
|
||||
dummy_model.active_microscope_name, None, dummy_model.configuration
|
||||
)
|
||||
|
||||
|
||||
def test_zoom_base_attributes(dummy_zoom):
|
||||
|
||||
assert hasattr(dummy_zoom, "zoomdict")
|
||||
assert hasattr(dummy_zoom, "zoomvalue")
|
||||
|
||||
assert hasattr(dummy_zoom, "set_zoom") and callable(getattr(dummy_zoom, "set_zoom"))
|
||||
assert hasattr(dummy_zoom, "move") and callable(getattr(dummy_zoom, "move"))
|
||||
assert hasattr(dummy_zoom, "read_position") and callable(
|
||||
getattr(dummy_zoom, "read_position")
|
||||
)
|
||||
|
||||
|
||||
def test_build_stage_dict(dummy_zoom):
|
||||
import random
|
||||
|
||||
a, b, c = random.randint(1, 1000), random.randint(1, 1000), random.randint(1, 1000)
|
||||
dummy_zoom.configuration["stage_positions"] = {
|
||||
"BABB": {"f": {"0.63x": a, "1x": b, "2x": c}}
|
||||
}
|
||||
dummy_zoom.build_stage_dict()
|
||||
|
||||
assert dummy_zoom.stage_offsets["BABB"]["f"]["0.63x"]["0.63x"] == 0
|
||||
assert dummy_zoom.stage_offsets["BABB"]["f"]["0.63x"]["1x"] == b - a
|
||||
assert dummy_zoom.stage_offsets["BABB"]["f"]["0.63x"]["2x"] == c - a
|
||||
assert dummy_zoom.stage_offsets["BABB"]["f"]["1x"]["0.63x"] == a - b
|
||||
assert dummy_zoom.stage_offsets["BABB"]["f"]["1x"]["1x"] == 0
|
||||
assert dummy_zoom.stage_offsets["BABB"]["f"]["1x"]["2x"] == c - b
|
||||
assert dummy_zoom.stage_offsets["BABB"]["f"]["2x"]["0.63x"] == a - c
|
||||
assert dummy_zoom.stage_offsets["BABB"]["f"]["2x"]["1x"] == b - c
|
||||
assert dummy_zoom.stage_offsets["BABB"]["f"]["2x"]["2x"] == 0
|
||||
|
||||
|
||||
def test_set_zoom(dummy_zoom):
|
||||
for zoom in dummy_zoom.zoomdict.keys():
|
||||
dummy_zoom.set_zoom(zoom)
|
||||
assert dummy_zoom.zoomvalue == zoom
|
||||
|
||||
try:
|
||||
dummy_zoom.set_zoom("not_a_zoom")
|
||||
assert False
|
||||
except ValueError:
|
||||
assert True
|
||||
60
test/model/devices/zoom/test_dynamixel.py
Normal file
60
test/model/devices/zoom/test_dynamixel.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
# Standard Library Imports
|
||||
import unittest
|
||||
|
||||
# Third Party Imports
|
||||
import pytest
|
||||
import platform
|
||||
|
||||
# Local Imports
|
||||
|
||||
|
||||
class TestZoomDynamixel(unittest.TestCase):
|
||||
"""Unit Test for DynamixelZoom Class
|
||||
|
||||
Does not instantiate object owing to DLL"""
|
||||
|
||||
@pytest.mark.skipif(platform.system() != "Windows", reason="No DLL for mac")
|
||||
def test_zoom_dynamixel_attributes(self):
|
||||
from navigate.model.devices.zoom.dynamixel import DynamixelZoom
|
||||
|
||||
attributes = dir(DynamixelZoom)
|
||||
desired_attributes = ["move", "read_position", "set_zoom"]
|
||||
|
||||
for da in desired_attributes:
|
||||
assert da in attributes
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
67
test/model/devices/zoom/test_synthetic.py
Normal file
67
test/model/devices/zoom/test_synthetic.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted for academic and research use only (subject to the
|
||||
# limitations in the disclaimer below) provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
|
||||
# * Neither the name of the copyright holders nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
# Standard Library Imports
|
||||
import unittest
|
||||
|
||||
# Third Party Imports
|
||||
|
||||
# Local Imports
|
||||
from navigate.model.devices.zoom.synthetic import SyntheticZoom
|
||||
from test.model.dummy import DummyModel
|
||||
|
||||
|
||||
class TestZoomSynthetic(unittest.TestCase):
|
||||
"""Unit Test for SyntheticZoom Class"""
|
||||
|
||||
dummy_model = DummyModel()
|
||||
microscope_name = "Mesoscale"
|
||||
zoom_class = SyntheticZoom(microscope_name, None, dummy_model.configuration)
|
||||
|
||||
def test_zoom_synthetic_attributes(self):
|
||||
|
||||
assert hasattr(self.zoom_class, "zoomdict")
|
||||
assert hasattr(self.zoom_class, "zoomvalue")
|
||||
|
||||
assert hasattr(self.zoom_class, "set_zoom") and callable(
|
||||
getattr(self.zoom_class, "set_zoom")
|
||||
)
|
||||
assert hasattr(self.zoom_class, "move") and callable(
|
||||
getattr(self.zoom_class, "move")
|
||||
)
|
||||
assert hasattr(self.zoom_class, "read_position") and callable(
|
||||
getattr(self.zoom_class, "read_position")
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user