feat: init
This commit is contained in:
0
test/__init__.py
Normal file
0
test/__init__.py
Normal file
855
test/config/test_config.py
Normal file
855
test/config/test_config.py
Normal file
@@ -0,0 +1,855 @@
|
||||
# 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 Imports
|
||||
import pathlib
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from multiprocessing import Manager
|
||||
from multiprocessing.managers import ListProxy, DictProxy
|
||||
import os
|
||||
import time
|
||||
import random
|
||||
import yaml
|
||||
import sys
|
||||
|
||||
# Third Party Imports
|
||||
|
||||
# Local Imports
|
||||
import navigate.config.config as config
|
||||
from navigate.tools.file_functions import save_yaml_file, delete_folder, load_yaml_file
|
||||
|
||||
|
||||
def test_config_methods():
|
||||
methods = dir(config)
|
||||
desired_methods = [
|
||||
"DictProxy",
|
||||
"ListProxy",
|
||||
"Path",
|
||||
"__builtins__",
|
||||
"__cached__",
|
||||
"__doc__",
|
||||
"__file__",
|
||||
"__loader__",
|
||||
"__name__",
|
||||
"__package__",
|
||||
"__spec__",
|
||||
"build_nested_dict",
|
||||
"build_ref_name",
|
||||
"load_param_from_module",
|
||||
"save_yaml_file",
|
||||
"get_configuration_paths",
|
||||
"get_navigate_path",
|
||||
"isfile",
|
||||
"load_configs",
|
||||
"os",
|
||||
"platform",
|
||||
"shutil",
|
||||
"sys",
|
||||
"time",
|
||||
"update_config_dict",
|
||||
"verify_experiment_config",
|
||||
"verify_waveform_constants",
|
||||
"verify_positions_config",
|
||||
"verify_configuration",
|
||||
"support_deceased_configuration",
|
||||
"yaml",
|
||||
"logging",
|
||||
"logger",
|
||||
"p",
|
||||
"Union",
|
||||
"multiprocessing",
|
||||
]
|
||||
for method in methods:
|
||||
assert method in desired_methods
|
||||
|
||||
|
||||
def test_get_navigate_path():
|
||||
"""Test that the Navigate path is a string."""
|
||||
assert isinstance(config.get_navigate_path(), str)
|
||||
path_string = config.get_navigate_path()
|
||||
assert ".navigate" in path_string
|
||||
|
||||
|
||||
def test_get_navigate_path_windows(monkeypatch):
|
||||
"""Test that the Navigate path is a string."""
|
||||
monkeypatch.setattr(config.platform, "system", lambda: "Windows")
|
||||
monkeypatch.setattr(config.os, "getenv", lambda x: "LOCALAPPDATA")
|
||||
monkeypatch.setattr(config.os.path, "exists", lambda x: True)
|
||||
assert isinstance(config.get_navigate_path(), str)
|
||||
path_string = config.get_navigate_path()
|
||||
assert path_string.startswith("LOCALAPPDATA")
|
||||
assert path_string.endswith(".navigate")
|
||||
|
||||
|
||||
def test_get_navigate_path_mac(monkeypatch):
|
||||
"""Test that the Navigate path is a string."""
|
||||
monkeypatch.setattr(config.platform, "system", lambda: "Darwin")
|
||||
monkeypatch.setattr(config.os, "getenv", lambda x: "HOME")
|
||||
monkeypatch.setattr(config.os.path, "exists", lambda x: True)
|
||||
assert isinstance(config.get_navigate_path(), str)
|
||||
path_string = config.get_navigate_path()
|
||||
assert path_string.startswith("HOME")
|
||||
assert path_string.endswith(".navigate")
|
||||
|
||||
|
||||
# Write a test for config.get_configuration_paths()
|
||||
def test_get_configuration_paths():
|
||||
"""Test that the configuration paths are a list."""
|
||||
paths = config.get_configuration_paths()
|
||||
for path in paths:
|
||||
assert isinstance(path, pathlib.Path)
|
||||
assert len(paths) == 7
|
||||
|
||||
|
||||
def test_get_configuration_paths_create_dir(monkeypatch):
|
||||
"""Test that the configuration path is created,
|
||||
and that they are a list."""
|
||||
monkeypatch.setattr(config, "get_navigate_path", lambda: "TESTPATH")
|
||||
paths = config.get_configuration_paths()
|
||||
for path in paths:
|
||||
assert isinstance(path, pathlib.Path)
|
||||
assert os.path.exists(path), "Each configuration yaml file is copied"
|
||||
assert path.suffix.lower() in [".yml", ".yaml"]
|
||||
# delete generated folder
|
||||
delete_folder("TESTPATH")
|
||||
|
||||
|
||||
# test that the system is exited if no file is provided to load_yaml_config
|
||||
def test_load_yaml_config_no_file():
|
||||
"""Test that the system exits if no file is provided."""
|
||||
from unittest import mock
|
||||
|
||||
with mock.patch("sys.exit") as mock_sys_exit:
|
||||
config.load_configs(manager=Manager(), **{})
|
||||
mock_sys_exit.assert_called_once()
|
||||
|
||||
|
||||
class TestLoadConfigsWithYAMLError(unittest.TestCase):
|
||||
"""Test the load_configs function.
|
||||
|
||||
Target is the yaml.YAMLError exception clause.
|
||||
"""
|
||||
|
||||
@patch("yaml.load")
|
||||
@patch("builtins.open")
|
||||
@patch("pathlib.Path.exists")
|
||||
def test_yaml_error(self, mock_exists, mock_open, mock_yaml_load):
|
||||
# Set up the mocks
|
||||
mock_exists.return_value = True
|
||||
mock_open.return_value = MagicMock()
|
||||
mock_yaml_load.side_effect = yaml.YAMLError("Test YAMLError")
|
||||
|
||||
# Mocking sys.exit to prevent the test runner from exiting
|
||||
with patch.object(sys, "exit") as mock_exit:
|
||||
manager = MagicMock()
|
||||
config.load_configs(manager, config1="path/to/config1.yaml")
|
||||
|
||||
# Check if sys.exit was called with the expected argument
|
||||
mock_exit.assert_called_with(1)
|
||||
|
||||
# Check if the YAMLError was triggered
|
||||
mock_yaml_load.assert_called_once()
|
||||
|
||||
|
||||
class TestBuildNestedDict(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.manager = Manager()
|
||||
self.parent_dict = {}
|
||||
self.key_name = "nested_dict"
|
||||
|
||||
def tearDown(self):
|
||||
self.manager.shutdown()
|
||||
|
||||
def test_build_nested_dict_with_string_data(self):
|
||||
string_data = "string"
|
||||
expected_result = {"nested_dict": "string"}
|
||||
|
||||
config.build_nested_dict(
|
||||
self.manager, self.parent_dict, self.key_name, string_data
|
||||
)
|
||||
|
||||
self.assertEqual(self.parent_dict, expected_result)
|
||||
self.assertEqual(self.parent_dict[self.key_name], "string")
|
||||
assert isinstance(self.parent_dict, dict)
|
||||
|
||||
def test_build_nested_dict_with_list_data(self):
|
||||
list_data = ["string1", "string2"]
|
||||
|
||||
config.build_nested_dict(
|
||||
self.manager, self.parent_dict, self.key_name, list_data
|
||||
)
|
||||
|
||||
assert isinstance(self.parent_dict, dict)
|
||||
assert isinstance(self.parent_dict[self.key_name], ListProxy)
|
||||
for i in range(2):
|
||||
assert self.parent_dict[self.key_name][i] == list_data[i]
|
||||
|
||||
def test_build_nested_dict_with_dict_data(self):
|
||||
dict_data = {"key1": "string1", "key2": "string2"}
|
||||
|
||||
config.build_nested_dict(
|
||||
self.manager, self.parent_dict, self.key_name, dict_data
|
||||
)
|
||||
|
||||
assert isinstance(self.parent_dict, dict)
|
||||
assert isinstance(self.parent_dict[self.key_name], DictProxy)
|
||||
for key in dict_data.keys():
|
||||
assert self.parent_dict[self.key_name][key] == dict_data[key]
|
||||
|
||||
def test_update_config_dict_with_bad_file_name(self):
|
||||
test_entry = "string"
|
||||
dict_data = {"key1": "string1", "key2": "string2"}
|
||||
# Build the nested config
|
||||
config.build_nested_dict(
|
||||
self.manager, self.parent_dict, self.key_name, dict_data
|
||||
)
|
||||
|
||||
# Update the nested config
|
||||
result = config.update_config_dict(
|
||||
self.manager, self.parent_dict, self.key_name, test_entry
|
||||
)
|
||||
assert result is False
|
||||
|
||||
def test_update_config_dict_with_file_name(self):
|
||||
test_entry = "test.yml"
|
||||
# create an yaml file
|
||||
test_yaml_data = {"test_key1": "test_string1", "test_key2": "test_string2"}
|
||||
save_yaml_file("", test_yaml_data, test_entry)
|
||||
|
||||
dict_data = {"key1": "string1", "key2": "string2"}
|
||||
# Build the nested config
|
||||
config.build_nested_dict(
|
||||
self.manager, self.parent_dict, self.key_name, dict_data
|
||||
)
|
||||
|
||||
# Update the nested config
|
||||
result = config.update_config_dict(
|
||||
self.manager, self.parent_dict, self.key_name, test_entry
|
||||
)
|
||||
|
||||
assert result is True
|
||||
assert isinstance(self.parent_dict[self.key_name], DictProxy)
|
||||
for k in self.parent_dict[self.key_name].keys():
|
||||
assert self.parent_dict[self.key_name][k] == test_yaml_data[k]
|
||||
|
||||
# delete test yaml file
|
||||
os.remove(test_entry)
|
||||
|
||||
|
||||
class TestVerifyExperimentConfig(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.manager = Manager()
|
||||
current_path = os.path.abspath(os.path.dirname(__file__))
|
||||
root_path = os.path.dirname(os.path.dirname(current_path))
|
||||
self.config_path = os.path.join(root_path, "src", "navigate", "config")
|
||||
self.test_root = "test_dir"
|
||||
os.mkdir(self.test_root)
|
||||
|
||||
configuration = config.load_configs(
|
||||
self.manager,
|
||||
configuration=os.path.join(self.config_path, "configuration.yaml"),
|
||||
)
|
||||
config.verify_configuration(self.manager, configuration)
|
||||
saving_dict_sample = {
|
||||
"root_directory": config.get_navigate_path(),
|
||||
"save_directory": config.get_navigate_path(),
|
||||
"user": "Kevin",
|
||||
"tissue": "Lung",
|
||||
"celltype": "MV3",
|
||||
"label": "GFP",
|
||||
"file_type": "TIFF",
|
||||
"date": time.strftime("%Y-%m-%d"),
|
||||
"solvent": "BABB",
|
||||
}
|
||||
|
||||
camera_parameters_dict_sample = {
|
||||
"x_pixels": 2048,
|
||||
"y_pixels": 2048,
|
||||
"img_x_pixels": 2048,
|
||||
"img_y_pixels": 2048,
|
||||
"sensor_mode": "Normal",
|
||||
"readout_direction": "Top-to-Bottom",
|
||||
"number_of_pixels": 10,
|
||||
"binning": "1x1",
|
||||
"frames_to_average": 1,
|
||||
"databuffer_size": 100,
|
||||
}
|
||||
|
||||
# Autofocus
|
||||
# autofocus_sample = {
|
||||
# "coarse_range": 500,
|
||||
# "coarse_step_size": 50,
|
||||
# "coarse_selected": True,
|
||||
# "fine_range": 50,
|
||||
# "fine_step_size": 5,
|
||||
# "fine_selected": True,
|
||||
# "robust_fit": False,
|
||||
# }
|
||||
|
||||
stage_parameters_dict_sample = {
|
||||
"limits": True,
|
||||
}
|
||||
for microscope_name in configuration["configuration"]["microscopes"].keys():
|
||||
stage_parameters_dict_sample[microscope_name] = {}
|
||||
for k in ["theta_step", "f_step", "z_step"]:
|
||||
stage_parameters_dict_sample[microscope_name][k] = configuration[
|
||||
"configuration"
|
||||
]["microscopes"][microscope_name]["stage"].get(k, 30)
|
||||
stage_parameters_dict_sample[microscope_name]["xy_step"] = min(
|
||||
configuration["configuration"]["microscopes"][microscope_name][
|
||||
"stage"
|
||||
].get("x_step", 500),
|
||||
configuration["configuration"]["microscopes"][microscope_name][
|
||||
"stage"
|
||||
].get("y_step", 500),
|
||||
)
|
||||
|
||||
microscope_name = configuration["configuration"]["microscopes"].keys()[0]
|
||||
zoom = configuration["configuration"]["microscopes"][microscope_name]["zoom"][
|
||||
"position"
|
||||
].keys()[0]
|
||||
microscope_parameters_dict_sample = {
|
||||
"microscope_name": microscope_name,
|
||||
"image_mode": "live",
|
||||
"zoom": zoom,
|
||||
"stack_cycling_mode": "per_stack",
|
||||
"start_position": 0.0,
|
||||
"end_position": 100.0,
|
||||
"step_size": 20.0,
|
||||
"number_z_steps": 5,
|
||||
"timepoints": 1,
|
||||
"stack_pause": 0.0,
|
||||
"is_save": False,
|
||||
"stack_acq_time": 1.0,
|
||||
"timepoint_interval": 0,
|
||||
"experiment_duration": 1.03,
|
||||
"is_multiposition": False,
|
||||
"stack_z_origin": 0,
|
||||
"stack_focus_origin": 0,
|
||||
"start_focus": 0.0,
|
||||
"end_focus": 0.0,
|
||||
"abs_z_start": 0.0,
|
||||
"abs_z_end": 100.0,
|
||||
"waveform_template": "Default",
|
||||
}
|
||||
|
||||
# multipositions_sample = [[10.0, 10.0, 10.0, 10.0, 10.0]]
|
||||
|
||||
self.experiment_sample = {
|
||||
"Saving": saving_dict_sample,
|
||||
"CameraParameters": camera_parameters_dict_sample,
|
||||
"StageParameters": stage_parameters_dict_sample,
|
||||
"MicroscopeState": microscope_parameters_dict_sample,
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
delete_folder(self.test_root)
|
||||
self.manager.shutdown()
|
||||
|
||||
def assert_equal_dict(self, dict1, dict2):
|
||||
# dict1 and dict2 are not nested dict
|
||||
for k in dict1.keys():
|
||||
assert dict1[k] == dict2[k], f"{k}: {dict1[k]} -- {dict2[k]}"
|
||||
|
||||
def test_load_empty_experiment_file(self):
|
||||
experiment_file_path = os.path.join(self.test_root, "experiment.yml")
|
||||
with open(experiment_file_path, "w") as f:
|
||||
f.write("")
|
||||
configuration = config.load_configs(
|
||||
self.manager,
|
||||
configuration=os.path.join(self.config_path, "configuration.yaml"),
|
||||
experiment=experiment_file_path,
|
||||
)
|
||||
config.verify_configuration(self.manager, configuration)
|
||||
config.verify_experiment_config(self.manager, configuration)
|
||||
|
||||
experiement_config = configuration["experiment"]
|
||||
assert type(experiement_config) == DictProxy
|
||||
|
||||
# Saving parameters
|
||||
self.assert_equal_dict(
|
||||
self.experiment_sample["Saving"], experiement_config["Saving"]
|
||||
)
|
||||
|
||||
# Camera parameters
|
||||
self.assert_equal_dict(
|
||||
self.experiment_sample["CameraParameters"],
|
||||
experiement_config["CameraParameters"],
|
||||
)
|
||||
|
||||
# AutoFocusParameters
|
||||
|
||||
# Stage parameters
|
||||
for k, value in self.experiment_sample["StageParameters"].items():
|
||||
if type(value) == dict:
|
||||
assert k in experiement_config["StageParameters"].keys()
|
||||
self.assert_equal_dict(value, experiement_config["StageParameters"][k])
|
||||
else:
|
||||
assert value == experiement_config["StageParameters"][k]
|
||||
|
||||
# MicroscopeState parameters
|
||||
self.assert_equal_dict(
|
||||
self.experiment_sample["MicroscopeState"],
|
||||
experiement_config["MicroscopeState"],
|
||||
)
|
||||
|
||||
# # MultiPositions
|
||||
# for i, position in enumerate(self.experiment_sample["MultiPositions"]):
|
||||
# assert position == experiement_config["MultiPositions"][i]
|
||||
|
||||
def test_load_experiment_file_with_missing_parameters(self):
|
||||
experiment = load_yaml_file(os.path.join(self.config_path, "experiment.yml"))
|
||||
# Saving prameters
|
||||
saving_parameters = list(self.experiment_sample["Saving"].keys())
|
||||
saving_parameters_deleted = self.delete_random_entries_from_dict(
|
||||
saving_parameters, experiment["Saving"]
|
||||
)
|
||||
|
||||
# Camera parameters
|
||||
camera_parameters = list(self.experiment_sample["CameraParameters"].keys())
|
||||
camera_parameters.append("img_x_pixels")
|
||||
camera_parameters.append("img_y_pixels")
|
||||
camera_parameters_deleted = self.delete_random_entries_from_dict(
|
||||
camera_parameters, experiment["CameraParameters"]
|
||||
)
|
||||
|
||||
# StageParameters
|
||||
configuration = load_yaml_file(
|
||||
os.path.join(self.config_path, "configuration.yaml")
|
||||
)
|
||||
# delete limits
|
||||
if "limits" in experiment["StageParameters"].keys():
|
||||
del experiment["StageParameters"]["limits"]
|
||||
# delete part of stage parameters of one microscope
|
||||
microscope_names = list(configuration["microscopes"].keys())
|
||||
if microscope_names[0] not in experiment["StageParameters"]:
|
||||
experiment["StageParameters"][microscope_names[0]] = {
|
||||
"z_step": 100.0,
|
||||
"theta_step": 10.0,
|
||||
}
|
||||
# delete all stage parameter of another microscope
|
||||
if microscope_names[1] in experiment["StageParameters"].keys():
|
||||
del experiment["StageParameters"][microscope_names[1]]
|
||||
|
||||
# MicroscopeState
|
||||
micrscope_parameters = list(self.experiment_sample["MicroscopeState"].keys())
|
||||
micrscope_parameters.append("channels")
|
||||
micrscope_parameters_deleted = self.delete_random_entries_from_dict(
|
||||
micrscope_parameters, experiment["MicroscopeState"]
|
||||
)
|
||||
|
||||
save_yaml_file(self.test_root, experiment, "experiment_missing_parameters.yml")
|
||||
configuration = config.load_configs(
|
||||
self.manager,
|
||||
configuration=os.path.join(self.config_path, "configuration.yaml"),
|
||||
experiment=os.path.join(
|
||||
self.test_root, "experiment_missing_parameters.yml"
|
||||
),
|
||||
)
|
||||
config.verify_configuration(self.manager, configuration)
|
||||
config.verify_experiment_config(self.manager, configuration)
|
||||
# verify Saving parameters are added
|
||||
for k in saving_parameters_deleted:
|
||||
assert (
|
||||
k in configuration["experiment"]["Saving"].keys()
|
||||
), f"parameter {k} should be added to Saving parameters"
|
||||
|
||||
# verify CameraParameters are added
|
||||
for k in camera_parameters_deleted:
|
||||
assert (
|
||||
k in configuration["experiment"]["CameraParameters"].keys()
|
||||
), f"parameter {k} should be added into CameraParameters"
|
||||
|
||||
# verify MicroscopeState parameters are added
|
||||
for k in micrscope_parameters_deleted:
|
||||
assert (
|
||||
k in configuration["experiment"]["MicroscopeState"].keys()
|
||||
), f"parameter {k} should be added to MicroscopeState"
|
||||
|
||||
# verify Stage parameters are added
|
||||
assert (
|
||||
"limits" in configuration["experiment"]["StageParameters"].keys()
|
||||
), "limits should be added to Stage parameters"
|
||||
for microscope_name in microscope_names:
|
||||
for k in ["xy_step", "z_step", "f_step", "theta_step"]:
|
||||
assert (
|
||||
k in configuration["experiment"]["StageParameters"][microscope_name]
|
||||
)
|
||||
|
||||
def test_load_experiment_file_with_wrong_parameter_values(self):
|
||||
configuration = config.load_configs(
|
||||
self.manager,
|
||||
configuration=os.path.join(self.config_path, "configuration.yaml"),
|
||||
experiment=os.path.join(self.config_path, "experiment.yml"),
|
||||
)
|
||||
config.verify_configuration(self.manager, configuration)
|
||||
experiment = configuration["experiment"]
|
||||
# Saving parameters
|
||||
# check if root_directory and save_directory exist
|
||||
experiment["Saving"]["root_directory"] = self.config_path
|
||||
experiment["Saving"]["save_directory"] = os.path.join(
|
||||
self.test_root, "not_exist", "not_exist"
|
||||
)
|
||||
config.verify_experiment_config(self.manager, configuration)
|
||||
assert experiment["Saving"]["root_directory"] == self.config_path
|
||||
assert os.path.exists(experiment["Saving"]["save_directory"])
|
||||
assert experiment["Saving"]["save_directory"] != os.path.join(
|
||||
self.test_root, "not_exist", "not_exist"
|
||||
)
|
||||
|
||||
# CameraParameters
|
||||
# x_pixels, y_pixels
|
||||
experiment["CameraParameters"]["x_pixels"] = -10
|
||||
experiment["CameraParameters"]["y_pixels"] = "abcd"
|
||||
config.verify_experiment_config(self.manager, configuration)
|
||||
assert (
|
||||
experiment["CameraParameters"]["x_pixels"]
|
||||
== self.experiment_sample["CameraParameters"]["x_pixels"]
|
||||
)
|
||||
assert (
|
||||
experiment["CameraParameters"]["y_pixels"]
|
||||
== self.experiment_sample["CameraParameters"]["y_pixels"]
|
||||
)
|
||||
binning = int(experiment["CameraParameters"]["binning"][0])
|
||||
assert (
|
||||
experiment["CameraParameters"]["img_x_pixels"]
|
||||
== experiment["CameraParameters"]["x_pixels"] // binning
|
||||
)
|
||||
assert (
|
||||
experiment["CameraParameters"]["img_y_pixels"]
|
||||
== experiment["CameraParameters"]["y_pixels"] // binning
|
||||
)
|
||||
|
||||
# binning
|
||||
for v in ["abcd", "3x3", "12.4"]:
|
||||
experiment["CameraParameters"]["binning"] = v
|
||||
config.verify_experiment_config(self.manager, configuration)
|
||||
assert experiment["CameraParameters"]["binning"] == "1x1"
|
||||
assert (
|
||||
experiment["CameraParameters"]["img_x_pixels"]
|
||||
== experiment["CameraParameters"]["x_pixels"]
|
||||
)
|
||||
assert (
|
||||
experiment["CameraParameters"]["img_y_pixels"]
|
||||
== experiment["CameraParameters"]["y_pixels"]
|
||||
)
|
||||
|
||||
# sensor_mode
|
||||
experiment["CameraParameters"]["sensor_mode"] = "None"
|
||||
config.verify_experiment_config(self.manager, configuration)
|
||||
assert experiment["CameraParameters"]["sensor_mode"] == "Normal"
|
||||
experiment["CameraParameters"]["sensor_mode"] = "Lightsheet"
|
||||
config.verify_experiment_config(self.manager, configuration)
|
||||
assert experiment["CameraParameters"]["sensor_mode"] == "Normal"
|
||||
experiment["CameraParameters"]["sensor_mode"] = "Light-Sheet"
|
||||
config.verify_experiment_config(self.manager, configuration)
|
||||
assert experiment["CameraParameters"]["sensor_mode"] == "Light-Sheet"
|
||||
|
||||
# readout_direction
|
||||
for v in ["abcd", 123, None]:
|
||||
experiment["CameraParameters"]["readout_direction"] = v
|
||||
config.verify_experiment_config(self.manager, configuration)
|
||||
assert (
|
||||
experiment["CameraParameters"]["readout_direction"] == "Top-to-Bottom"
|
||||
)
|
||||
|
||||
experiment["CameraParameters"]["readout_direction"] = "Bottom-to-Top"
|
||||
config.verify_experiment_config(self.manager, configuration)
|
||||
assert experiment["CameraParameters"]["readout_direction"] == "Bottom-to-Top"
|
||||
|
||||
# other parameters should be int
|
||||
for k in ["number_of_pixels", "databuffer_size", "frames_to_average"]:
|
||||
for v in ["abc", -10, 0]:
|
||||
experiment["CameraParameters"][k] = v
|
||||
config.verify_experiment_config(self.manager, configuration)
|
||||
assert (
|
||||
experiment["CameraParameters"][k]
|
||||
== self.experiment_sample["CameraParameters"][k]
|
||||
)
|
||||
|
||||
# StageParameters
|
||||
experiment["StageParameters"]["limits"] = "abc"
|
||||
config.verify_experiment_config(self.manager, configuration)
|
||||
assert experiment["StageParameters"]["limits"] is True
|
||||
|
||||
microscope_names = list(configuration["configuration"]["microscopes"].keys())
|
||||
for microscope_name in microscope_names:
|
||||
for k in ["xy_step", "z_step", "f_step", "theta_step"]:
|
||||
experiment["StageParameters"][microscope_name][k] = "abc"
|
||||
config.verify_experiment_config(self.manager, configuration)
|
||||
assert isinstance(
|
||||
experiment["StageParameters"][microscope_name][k], int
|
||||
)
|
||||
|
||||
# MicroscopeState
|
||||
experiment["MicroscopeState"]["microscope_name"] = "nonexist_microscope"
|
||||
config.verify_experiment_config(self.manager, configuration)
|
||||
assert experiment["MicroscopeState"]["microscope_name"] == microscope_names[0]
|
||||
|
||||
experiment["MicroscopeState"]["zoom"] = "abc"
|
||||
config.verify_experiment_config(self.manager, configuration)
|
||||
assert (
|
||||
experiment["MicroscopeState"]["zoom"]
|
||||
== list(
|
||||
configuration["configuration"]["microscopes"][microscope_names[0]][
|
||||
"zoom"
|
||||
]["position"].keys()
|
||||
)[0]
|
||||
)
|
||||
|
||||
for k in [
|
||||
"start_position",
|
||||
"end_position",
|
||||
"step_size",
|
||||
"number_z_steps",
|
||||
"timepoints",
|
||||
"stack_acq_time",
|
||||
"timepoint_interval",
|
||||
"experiment_duration",
|
||||
"stack_z_origin",
|
||||
"stack_focus_origin",
|
||||
"start_focus",
|
||||
"end_focus",
|
||||
"abs_z_start",
|
||||
"abs_z_end",
|
||||
]:
|
||||
experiment["MicroscopeState"][k] = "nonsense_value"
|
||||
config.verify_experiment_config(self.manager, configuration)
|
||||
assert isinstance(experiment["MicroscopeState"][k], int) or isinstance(
|
||||
experiment["MicroscopeState"][k], float
|
||||
)
|
||||
|
||||
# channels
|
||||
experiment["MicroscopeState"]["channels"] = [
|
||||
{
|
||||
"is_selected": True,
|
||||
"laser": "488nm",
|
||||
"laser_index": 0,
|
||||
"filter": "Empty-Alignment",
|
||||
"filter_position": 0,
|
||||
"camera_exposure_time": 200.0,
|
||||
"laser_power": 20.0,
|
||||
"interval_time": 5.0,
|
||||
"defocus": 0.0,
|
||||
}
|
||||
]
|
||||
# number_of_filter_wheels =
|
||||
|
||||
config.verify_experiment_config(self.manager, configuration)
|
||||
assert type(experiment["MicroscopeState"]["channels"]) is DictProxy
|
||||
assert len(list(experiment["MicroscopeState"]["channels"].keys())) == 0
|
||||
|
||||
experiment["MicroscopeState"]["channels"] = {
|
||||
"channel_0": {
|
||||
"is_selected": True,
|
||||
"laser": "488nm",
|
||||
"laser_index": 0,
|
||||
"filter": "Empty-Alignment",
|
||||
"filter_position": 0,
|
||||
"camera_exposure_time": 200.0,
|
||||
"laser_power": 20.0,
|
||||
"interval_time": 5.0,
|
||||
"defocus": 0.0,
|
||||
}
|
||||
}
|
||||
config.verify_experiment_config(self.manager, configuration)
|
||||
assert type(experiment["MicroscopeState"]["channels"]) is DictProxy
|
||||
assert len(list(experiment["MicroscopeState"]["channels"].keys())) == 0
|
||||
|
||||
experiment["MicroscopeState"]["channels"] = {
|
||||
"channel_100": {
|
||||
"is_selected": True,
|
||||
"laser": "488nm",
|
||||
"laser_index": 0,
|
||||
"filter": "Empty-Alignment",
|
||||
"filter_position": 0,
|
||||
"camera_exposure_time": 200.0,
|
||||
"laser_power": 20.0,
|
||||
"interval_time": 5.0,
|
||||
"defocus": 0.0,
|
||||
}
|
||||
}
|
||||
config.verify_experiment_config(self.manager, configuration)
|
||||
assert type(experiment["MicroscopeState"]["channels"]) is DictProxy
|
||||
assert len(list(experiment["MicroscopeState"]["channels"].keys())) == 0
|
||||
|
||||
microscope_name = experiment["MicroscopeState"]["microscope_name"]
|
||||
lasers = [
|
||||
f"{laser['wavelength']}nm"
|
||||
for laser in configuration["configuration"]["microscopes"][microscope_name][
|
||||
"laser"
|
||||
]
|
||||
]
|
||||
filterwheels = list(
|
||||
configuration["configuration"]["microscopes"][microscope_name][
|
||||
"filter_wheel"
|
||||
][0]["available_filters"].keys()
|
||||
)
|
||||
config.update_config_dict(
|
||||
self.manager,
|
||||
experiment["MicroscopeState"]["channels"],
|
||||
"channel_2",
|
||||
{
|
||||
"is_selected": 1,
|
||||
"laser": "48nm",
|
||||
"laser_index": -1,
|
||||
"filter_wheel_0": "nonexsit_filter_***",
|
||||
"filter_position_0": 1,
|
||||
"camera_exposure_time": -200.0,
|
||||
"laser_power": "a",
|
||||
"interval_time": -3,
|
||||
"defocus": "a",
|
||||
},
|
||||
)
|
||||
expected_value = {
|
||||
"is_selected": False,
|
||||
"laser": lasers[0],
|
||||
"laser_index": 0,
|
||||
"filter_wheel_0": filterwheels[0],
|
||||
"filter_position_0": 0,
|
||||
"camera_exposure_time": 200.0,
|
||||
"laser_power": 20.0,
|
||||
"interval_time": 0.0,
|
||||
"defocus": 0.0,
|
||||
}
|
||||
config.verify_experiment_config(self.manager, configuration)
|
||||
assert type(experiment["MicroscopeState"]["channels"]) is DictProxy
|
||||
assert "channel_2" in experiment["MicroscopeState"]["channels"].keys()
|
||||
for k in expected_value:
|
||||
assert (
|
||||
experiment["MicroscopeState"]["channels"]["channel_2"][k]
|
||||
== expected_value[k]
|
||||
)
|
||||
|
||||
config.update_config_dict(
|
||||
self.manager,
|
||||
experiment["MicroscopeState"]["channels"],
|
||||
"channel_2",
|
||||
{
|
||||
"is_selected": 1,
|
||||
"laser": lasers[1],
|
||||
"laser_index": 3,
|
||||
"filter_wheel_0": filterwheels[2],
|
||||
"filter_position_0": 1,
|
||||
"camera_exposure_time": -200.0,
|
||||
"laser_power": "a",
|
||||
"interval_time": -3,
|
||||
"defocus": "a",
|
||||
},
|
||||
)
|
||||
expected_value = {
|
||||
"is_selected": False,
|
||||
"laser": lasers[1],
|
||||
"laser_index": 1,
|
||||
"filter_wheel_0": filterwheels[2],
|
||||
"filter_position_0": 2,
|
||||
"camera_exposure_time": 200.0,
|
||||
"laser_power": 20.0,
|
||||
"interval_time": 0.0,
|
||||
"defocus": 0.0,
|
||||
}
|
||||
config.verify_experiment_config(self.manager, configuration)
|
||||
assert type(experiment["MicroscopeState"]["channels"]) is DictProxy
|
||||
assert "channel_2" in experiment["MicroscopeState"]["channels"].keys()
|
||||
for k in expected_value:
|
||||
assert (
|
||||
experiment["MicroscopeState"]["channels"]["channel_2"][k]
|
||||
== expected_value[k]
|
||||
)
|
||||
|
||||
def select_random_entries_from_list(self, parameter_list):
|
||||
n = random.randint(1, len(parameter_list))
|
||||
return random.choices(parameter_list, k=n)
|
||||
|
||||
def delete_random_entries_from_dict(self, parameter_list, parameter_dict):
|
||||
n = random.randint(1, len(parameter_list))
|
||||
deleted_parameters = random.choices(parameter_list, k=n)
|
||||
for k in deleted_parameters:
|
||||
if k in parameter_dict.keys():
|
||||
del parameter_dict[k]
|
||||
return deleted_parameters
|
||||
|
||||
def test_load_empty_multi_positions(self):
|
||||
positions_file_path = os.path.join(self.test_root, "multi_positions.yml")
|
||||
with open(positions_file_path, "w") as f:
|
||||
f.write("")
|
||||
positions = load_yaml_file(positions_file_path)
|
||||
new_positions = config.verify_positions_config(positions)
|
||||
assert isinstance(new_positions, list)
|
||||
assert len(new_positions) == 0
|
||||
|
||||
def test_load_multi_positions_with_corrupted_values(self):
|
||||
positions = [
|
||||
[1, 2, 3],
|
||||
["a", "b", "c", 1, 2],
|
||||
[
|
||||
10,
|
||||
"a",
|
||||
30,
|
||||
40,
|
||||
],
|
||||
]
|
||||
new_positions = config.verify_positions_config(positions)
|
||||
assert isinstance(new_positions, list)
|
||||
assert len(new_positions) == 1
|
||||
|
||||
positions = [
|
||||
[1, 2, 3],
|
||||
["a", "b", "c", 1, 2],
|
||||
[1, 2, 3, 4, 5],
|
||||
[
|
||||
10,
|
||||
"a",
|
||||
30,
|
||||
40,
|
||||
],
|
||||
]
|
||||
new_positions = config.verify_positions_config(positions)
|
||||
assert isinstance(new_positions, list)
|
||||
assert len(new_positions) == 2
|
||||
|
||||
positions = [
|
||||
[1, 2, 3],
|
||||
["a", "b", "c", 1, 2],
|
||||
[1, 2, 3, 4, 5],
|
||||
[
|
||||
10,
|
||||
"a",
|
||||
30,
|
||||
40,
|
||||
],
|
||||
[1, 2, 3, 4, 5, 6],
|
||||
]
|
||||
new_positions = config.verify_positions_config(positions)
|
||||
assert isinstance(new_positions, list)
|
||||
assert len(new_positions) == 3
|
||||
426
test/config/test_configuration.py
Normal file
426
test/config/test_configuration.py
Normal file
@@ -0,0 +1,426 @@
|
||||
# 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 Imports
|
||||
import unittest
|
||||
import yaml
|
||||
import os
|
||||
|
||||
# Third Party Imports
|
||||
|
||||
# Local Imports
|
||||
|
||||
|
||||
class TestConfiguration(unittest.TestCase):
|
||||
def setUp(self):
|
||||
current_path = os.path.abspath(os.path.dirname(__file__))
|
||||
root_path = os.path.dirname(os.path.dirname(current_path))
|
||||
yaml_path = os.path.join(
|
||||
root_path, "src", "navigate", "config", "configuration.yaml"
|
||||
)
|
||||
|
||||
with open(yaml_path) as file:
|
||||
self.data = yaml.safe_load(file)
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
# # hardware head section has been removed
|
||||
# def test_hardware_section(self):
|
||||
# expected_hardware = ["daq", "camera", "filter_wheel", "stage", "zoom"]
|
||||
|
||||
# hardware_types = self.data["hardware"].keys()
|
||||
# for hardware_type in hardware_types:
|
||||
# self.assertIn(hardware_type, expected_hardware)
|
||||
# if isinstance(self.data["hardware"][hardware_type], dict):
|
||||
# hardware_keys = self.data["hardware"][hardware_type].keys()
|
||||
# for key in hardware_keys:
|
||||
# self.assertIn("type", self.data["hardware"][hardware_type])
|
||||
# elif isinstance(self.data["hardware"][hardware_type], list):
|
||||
# for i in range(len(self.data["hardware"][hardware_type])):
|
||||
# self.assertIn("type", self.data["hardware"][hardware_type][i])
|
||||
|
||||
def test_gui_section(self):
|
||||
expected_keys = ["channels"]
|
||||
expected_channel_keys = [
|
||||
"count",
|
||||
# "laser_power",
|
||||
# "exposure_time",
|
||||
# "interval_time",
|
||||
]
|
||||
expected_stack_keys = ["step_size", "start_pos", "end_pos"]
|
||||
min_max_step_keys = ["min", "max", "step"]
|
||||
|
||||
gui_keys = self.data["gui"].keys()
|
||||
for key in gui_keys:
|
||||
self.assertIn(key, expected_keys)
|
||||
|
||||
# Channels Entry
|
||||
if key == "channels":
|
||||
channel_keys = self.data["gui"][key].keys()
|
||||
for channel_key in channel_keys:
|
||||
self.assertIn(channel_key, expected_channel_keys)
|
||||
if channel_key != "count":
|
||||
channel_key_keys = self.data["gui"][key][channel_key].keys()
|
||||
for channel_key_key in channel_key_keys:
|
||||
self.assertIn(channel_key_key, min_max_step_keys)
|
||||
|
||||
# Stack Acquisition Entry
|
||||
elif key == "stack_acquisition":
|
||||
stack_keys = self.data["gui"][key].keys()
|
||||
for stack_key in stack_keys:
|
||||
self.assertIn(stack_key, expected_stack_keys)
|
||||
stack_key_keys = self.data["gui"][key][stack_key].keys()
|
||||
for stack_key_key in stack_key_keys:
|
||||
self.assertIn(stack_key_key, min_max_step_keys)
|
||||
|
||||
# Timepoint Entry
|
||||
elif key == "timepoint":
|
||||
timepoint_keys = self.data["gui"][key].keys()
|
||||
for timepoint_key in timepoint_keys:
|
||||
timepoint_key_keys = self.data["gui"][key][timepoint_key].keys()
|
||||
for timepoint_key_key in timepoint_key_keys:
|
||||
self.assertIn(timepoint_key_key, min_max_step_keys)
|
||||
|
||||
else:
|
||||
raise ValueError("Unexpected key in gui section")
|
||||
|
||||
def test_microscope_section(self):
|
||||
expected_hardware = [
|
||||
"daq",
|
||||
"camera",
|
||||
"remote_focus",
|
||||
"galvo",
|
||||
"shutter",
|
||||
"laser",
|
||||
"filter_wheel",
|
||||
"stage",
|
||||
"zoom",
|
||||
]
|
||||
|
||||
microscopes = self.data["microscopes"].keys()
|
||||
for microscope in microscopes:
|
||||
hardware = self.data["microscopes"][microscope].keys()
|
||||
for hardware_type in hardware:
|
||||
self.assertIn(hardware_type, expected_hardware)
|
||||
|
||||
if hardware_type == "daq":
|
||||
self.daq_section(microscope=microscope, hardware_type=hardware_type)
|
||||
|
||||
elif hardware_type == "camera":
|
||||
self.camera_section(
|
||||
microscope=microscope, hardware_type=hardware_type
|
||||
)
|
||||
|
||||
elif hardware_type == "remote_focus":
|
||||
self.remote_focus_section(
|
||||
microscope=microscope, hardware_type=hardware_type
|
||||
)
|
||||
|
||||
elif hardware_type == "galvo":
|
||||
self.galvo_section(
|
||||
microscope=microscope, hardware_type=hardware_type
|
||||
)
|
||||
|
||||
elif hardware_type == "filter_wheel":
|
||||
self.filter_wheel_section(
|
||||
microscope=microscope, hardware_type=hardware_type
|
||||
)
|
||||
elif hardware_type == "stage":
|
||||
self.stage_section(
|
||||
microscope=microscope, hardware_type=hardware_type
|
||||
)
|
||||
|
||||
elif hardware_type == "zoom":
|
||||
self.zoom_section(
|
||||
microscope=microscope, hardware_type=hardware_type
|
||||
)
|
||||
|
||||
elif hardware_type == "shutter":
|
||||
self.shutter_section(
|
||||
microscope=microscope, hardware_type=hardware_type
|
||||
)
|
||||
|
||||
elif hardware_type == "laser":
|
||||
self.laser_section(
|
||||
microscope=microscope, hardware_type=hardware_type
|
||||
)
|
||||
|
||||
else:
|
||||
raise ValueError("Unexpected hardware type")
|
||||
|
||||
def daq_section(self, microscope, hardware_type):
|
||||
expected_daq_keys = [
|
||||
"hardware",
|
||||
"sample_rate",
|
||||
"sweep_time",
|
||||
"master_trigger_out_line",
|
||||
"camera_trigger_out_line",
|
||||
"trigger_source",
|
||||
"laser_port_switcher",
|
||||
"laser_switch_state",
|
||||
]
|
||||
type_keys = ["type"]
|
||||
|
||||
daq_keys = self.data["microscopes"][microscope][hardware_type].keys()
|
||||
for key in daq_keys:
|
||||
if key == "hardware":
|
||||
for type_key in type_keys:
|
||||
self.assertIn(
|
||||
type_key,
|
||||
self.data["microscopes"][microscope][hardware_type][key],
|
||||
)
|
||||
else:
|
||||
self.assertIn(key, expected_daq_keys)
|
||||
|
||||
def camera_section(self, microscope, hardware_type):
|
||||
expected_keys = [
|
||||
"hardware",
|
||||
"defect_correct_mode",
|
||||
"delay",
|
||||
"settle_down",
|
||||
"flip_x",
|
||||
"flip_y",
|
||||
]
|
||||
type_keys = ["type", "serial_number", "camera_connection"]
|
||||
|
||||
camera_keys = self.data["microscopes"][microscope][hardware_type].keys()
|
||||
for key in camera_keys:
|
||||
if key == "hardware":
|
||||
for type_key in type_keys:
|
||||
self.assertIn(
|
||||
type_key,
|
||||
self.data["microscopes"][microscope][hardware_type][key],
|
||||
)
|
||||
else:
|
||||
self.assertIn(key, expected_keys)
|
||||
|
||||
def remote_focus_section(self, microscope, hardware_type):
|
||||
expected_keys = [
|
||||
"hardware",
|
||||
]
|
||||
type_keys = ["type", "channel", "min", "max", "port", "baudrate"]
|
||||
remote_focus_keys = self.data["microscopes"][microscope][hardware_type].keys()
|
||||
for key in remote_focus_keys:
|
||||
if key == "hardware":
|
||||
for type_key in type_keys:
|
||||
self.assertIn(
|
||||
type_key,
|
||||
self.data["microscopes"][microscope][hardware_type][key],
|
||||
)
|
||||
else:
|
||||
self.assertIn(key, expected_keys)
|
||||
|
||||
def galvo_section(self, microscope, hardware_type):
|
||||
expected_keys = [
|
||||
"hardware",
|
||||
"waveform",
|
||||
"phase",
|
||||
]
|
||||
type_keys = ["type", "channel", "min", "max"]
|
||||
|
||||
if isinstance(self.data["microscopes"][microscope][hardware_type], list):
|
||||
for i in range(len(self.data["microscopes"][microscope][hardware_type])):
|
||||
galvo_keys = self.data["microscopes"][microscope][hardware_type][
|
||||
i
|
||||
].keys()
|
||||
for key in galvo_keys:
|
||||
if key == "hardware":
|
||||
for type_key in type_keys:
|
||||
self.assertIn(
|
||||
type_key,
|
||||
self.data["microscopes"][microscope][hardware_type][i][
|
||||
key
|
||||
],
|
||||
)
|
||||
else:
|
||||
self.assertIn(key, expected_keys)
|
||||
else:
|
||||
raise ValueError("Galvo section is not a list")
|
||||
|
||||
def filter_wheel_section(self, microscope, hardware_type):
|
||||
expected_keys = [
|
||||
"hardware",
|
||||
"filter_wheel_delay",
|
||||
"available_filters",
|
||||
]
|
||||
type_keys = ["type", "wheel_number", "port", "baudrate"]
|
||||
|
||||
keys = self.data["microscopes"][microscope][hardware_type].keys()
|
||||
for key in keys:
|
||||
if key == "hardware":
|
||||
for type_key in type_keys:
|
||||
self.assertIn(
|
||||
type_key,
|
||||
self.data["microscopes"][microscope][hardware_type][key],
|
||||
)
|
||||
elif key == "available_filters":
|
||||
assert isinstance(
|
||||
self.data["microscopes"][microscope][hardware_type][key], dict
|
||||
)
|
||||
else:
|
||||
self.assertIn(key, expected_keys)
|
||||
|
||||
def stage_section(self, microscope, hardware_type):
|
||||
expected_keys = [
|
||||
"hardware",
|
||||
"x_max",
|
||||
"x_min",
|
||||
"y_max",
|
||||
"y_min",
|
||||
"z_max",
|
||||
"z_min",
|
||||
"f_max",
|
||||
"f_min",
|
||||
"theta_max",
|
||||
"theta_min",
|
||||
"x_offset",
|
||||
"y_offset",
|
||||
"z_offset",
|
||||
"theta_offset",
|
||||
"f_offset",
|
||||
"joystick_axes",
|
||||
"flip_x",
|
||||
"flip_y",
|
||||
"flip_z",
|
||||
"flip_f",
|
||||
]
|
||||
type_keys = [
|
||||
"type",
|
||||
"serial_number",
|
||||
"axes",
|
||||
"volts_per_micron",
|
||||
"axes_mapping",
|
||||
"max",
|
||||
"min",
|
||||
"controllername",
|
||||
"stages",
|
||||
"refmode",
|
||||
"port",
|
||||
"baudrate",
|
||||
"timeout",
|
||||
]
|
||||
|
||||
for key in self.data["microscopes"][microscope][hardware_type].keys():
|
||||
if key == "hardware":
|
||||
if isinstance(
|
||||
self.data["microscopes"][microscope][hardware_type][key], list
|
||||
):
|
||||
for i in range(
|
||||
len(self.data["microscopes"][microscope][hardware_type][key])
|
||||
):
|
||||
for type_key in type_keys:
|
||||
self.assertIn(
|
||||
type_key,
|
||||
self.data["microscopes"][microscope][hardware_type][
|
||||
key
|
||||
][i],
|
||||
)
|
||||
elif isinstance(
|
||||
self.data["microscopes"][microscope][hardware_type][key], dict
|
||||
):
|
||||
for type_key in type_keys:
|
||||
self.assertIn(
|
||||
type_key,
|
||||
self.data["microscopes"][microscope][hardware_type][key],
|
||||
)
|
||||
else:
|
||||
raise ValueError("Stage hardware is not a list or dict")
|
||||
else:
|
||||
self.assertIn(key, expected_keys)
|
||||
|
||||
def zoom_section(self, microscope, hardware_type):
|
||||
expected_keys = ["hardware", "position", "pixel_size", "stage_positions"]
|
||||
type_keys = ["type", "servo_id", "port", "baudrate"]
|
||||
|
||||
for key in self.data["microscopes"][microscope][hardware_type].keys():
|
||||
if key == "hardware":
|
||||
for type_key in type_keys:
|
||||
self.assertIn(
|
||||
type_key,
|
||||
self.data["microscopes"][microscope][hardware_type][key],
|
||||
)
|
||||
elif key == "position":
|
||||
assert isinstance(
|
||||
self.data["microscopes"][microscope][hardware_type][key], dict
|
||||
)
|
||||
elif key == "pixel_size":
|
||||
assert isinstance(
|
||||
self.data["microscopes"][microscope][hardware_type][key], dict
|
||||
)
|
||||
elif key == "stage_positions":
|
||||
assert isinstance(
|
||||
self.data["microscopes"][microscope][hardware_type][key], dict
|
||||
)
|
||||
else:
|
||||
self.assertIn(key, expected_keys)
|
||||
|
||||
def shutter_section(self, microscope, hardware_type):
|
||||
expected_keys = ["hardware"]
|
||||
type_keys = ["type", "channel", "min", "max"]
|
||||
|
||||
for key in self.data["microscopes"][microscope][hardware_type].keys():
|
||||
if key == "hardware":
|
||||
for type_key in type_keys:
|
||||
self.assertIn(
|
||||
type_key,
|
||||
self.data["microscopes"][microscope][hardware_type][key],
|
||||
)
|
||||
else:
|
||||
self.assertIn(key, expected_keys)
|
||||
|
||||
def laser_section(self, microscope, hardware_type):
|
||||
expected_keys = [
|
||||
"wavelength",
|
||||
"onoff",
|
||||
"power",
|
||||
"type",
|
||||
]
|
||||
|
||||
hardware_keys = ["type", "channel", "min", "max"]
|
||||
|
||||
if isinstance(self.data["microscopes"][microscope][hardware_type], list):
|
||||
for i in range(len(self.data["microscopes"][microscope][hardware_type])):
|
||||
laser_keys = self.data["microscopes"][microscope][hardware_type][
|
||||
i
|
||||
].keys()
|
||||
for key in laser_keys:
|
||||
if key == "onoff" or key == "power":
|
||||
onoff_keys = self.data["microscopes"][microscope][
|
||||
hardware_type
|
||||
][i][key]["hardware"].keys()
|
||||
for onoff_key in onoff_keys:
|
||||
self.assertIn(onoff_key, hardware_keys)
|
||||
else:
|
||||
self.assertIn(key, expected_keys)
|
||||
else:
|
||||
raise ValueError("Laser section is not a list")
|
||||
203
test/config/test_experiment.py
Normal file
203
test/config/test_experiment.py
Normal file
@@ -0,0 +1,203 @@
|
||||
# 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 Imports
|
||||
import unittest
|
||||
import yaml
|
||||
import os
|
||||
import datetime
|
||||
|
||||
# Third Party Imports
|
||||
|
||||
# Local Imports
|
||||
|
||||
|
||||
class TextExperimentFile(unittest.TestCase):
|
||||
"""Test the experiment configuration file."""
|
||||
|
||||
def setUp(self):
|
||||
current_path = os.path.abspath(os.path.dirname(__file__))
|
||||
root_path = os.path.dirname(os.path.dirname(current_path))
|
||||
yaml_path = os.path.join(
|
||||
root_path, "src", "navigate", "config", "experiment.yml"
|
||||
)
|
||||
|
||||
with open(yaml_path) as file:
|
||||
self.data = yaml.safe_load(file)
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def parse_entries(self, section, expected_values):
|
||||
"""Parse the entries in the configuration file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
section : str
|
||||
The section of the configuration file to parse
|
||||
expected_values : dict
|
||||
A dictionary of expected values for the section
|
||||
|
||||
|
||||
Raises
|
||||
------
|
||||
AssertionError
|
||||
If the key is not in the section or if the value is not the expected type
|
||||
"""
|
||||
keys = self.data[section].keys()
|
||||
for key in keys:
|
||||
self.assertIn(key, expected_values)
|
||||
assert isinstance(
|
||||
self.data[section][key], expected_values[key]
|
||||
), f"{key} is not of type {expected_values[key]}"
|
||||
|
||||
def test_user(self):
|
||||
expected_values = {"name": str}
|
||||
|
||||
self.parse_entries(section="User", expected_values=expected_values)
|
||||
|
||||
def test_saving(self):
|
||||
expected_values = {
|
||||
"root_directory": str,
|
||||
"save_directory": str,
|
||||
"user": str,
|
||||
"tissue": str,
|
||||
"celltype": str,
|
||||
"label": str,
|
||||
"file_type": str,
|
||||
"date": datetime.date,
|
||||
"solvent": str,
|
||||
}
|
||||
|
||||
self.parse_entries(section="Saving", expected_values=expected_values)
|
||||
|
||||
def test_camera_parameters(self):
|
||||
expected_values = {
|
||||
"x_pixels": int,
|
||||
"y_pixels": int,
|
||||
"sensor_mode": str,
|
||||
"readout_direction": str,
|
||||
"number_of_pixels": int,
|
||||
"binning": str,
|
||||
"pixel_size": float,
|
||||
"frames_to_average": float,
|
||||
"databuffer_size": int,
|
||||
}
|
||||
|
||||
self.parse_entries(section="CameraParameters", expected_values=expected_values)
|
||||
|
||||
def test_autofocus_parameters(self):
|
||||
expected_values = {
|
||||
"coarse_range": int,
|
||||
"coarse_step_size": int,
|
||||
"coarse_selected": int,
|
||||
"fine_range": int,
|
||||
"fine_step_size": int,
|
||||
"fine_selected": bool,
|
||||
"robust_fit": bool,
|
||||
"spline_fit": bool,
|
||||
"test_significance": bool,
|
||||
}
|
||||
|
||||
self.parse_entries(
|
||||
section="AutoFocusParameters", expected_values=expected_values
|
||||
)
|
||||
|
||||
def test_stage_parameters(self):
|
||||
expected_values = {
|
||||
"xy_step": float,
|
||||
"z_step": float,
|
||||
"theta_step": float,
|
||||
"f_step": float,
|
||||
"x": float,
|
||||
"y": float,
|
||||
"z": float,
|
||||
"theta": float,
|
||||
"f": float,
|
||||
"limits": bool,
|
||||
}
|
||||
|
||||
self.parse_entries(section="StageParameters", expected_values=expected_values)
|
||||
|
||||
def test_microscope_state(self):
|
||||
expected_values = {
|
||||
"microscope_name": str,
|
||||
"image_mode": str,
|
||||
"zoom": str,
|
||||
"stack_cycling_mode": str,
|
||||
"start_position": float,
|
||||
"end_position": float,
|
||||
"step_size": float,
|
||||
"number_z_steps": float,
|
||||
"timepoints": int,
|
||||
"stack_pause": float,
|
||||
"is_save": bool,
|
||||
"stack_acq_time": float,
|
||||
"timepoint_interval": int,
|
||||
"experiment_duration": float,
|
||||
"is_multiposition": bool,
|
||||
"channels": dict,
|
||||
"stack_z_origin": float,
|
||||
"stack_focus_origin": float,
|
||||
"start_focus": float,
|
||||
"end_focus": float,
|
||||
"abs_z_start": float,
|
||||
"abs_z_end": float,
|
||||
"waveform_template": str,
|
||||
}
|
||||
|
||||
self.parse_entries(section="MicroscopeState", expected_values=expected_values)
|
||||
|
||||
# Check that the channels dictionary has the correct keys
|
||||
channel_keys = self.data["MicroscopeState"]["channels"].keys()
|
||||
for key in channel_keys:
|
||||
# Number of channels can vary depending upon the experiment.
|
||||
assert "channel_" in key
|
||||
expected_values = {
|
||||
"is_selected": bool,
|
||||
"laser": str,
|
||||
"laser_index": int,
|
||||
"filter": str,
|
||||
"filter_position": int,
|
||||
"camera_exposure_time": float,
|
||||
"laser_power": str,
|
||||
"interval_time": str,
|
||||
"defocus": float,
|
||||
}
|
||||
|
||||
key_keys = self.data["MicroscopeState"]["channels"][key].keys()
|
||||
for key_key in key_keys:
|
||||
self.assertIn(key_key, expected_values)
|
||||
assert isinstance(
|
||||
self.data["MicroscopeState"]["channels"][key][key_key],
|
||||
expected_values[key_key],
|
||||
), f"{key_key} is not of type {expected_values[key_key]}"
|
||||
62
test/config/test_rest_api_config.py
Normal file
62
test/config/test_rest_api_config.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# 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 Imports
|
||||
import unittest
|
||||
import yaml
|
||||
import os
|
||||
|
||||
# Third Party Imports
|
||||
|
||||
# Local Imports
|
||||
|
||||
|
||||
class TestAPIConfiguration(unittest.TestCase):
|
||||
def setUp(self):
|
||||
current_path = os.path.abspath(os.path.dirname(__file__))
|
||||
root_path = os.path.dirname(os.path.dirname(current_path))
|
||||
yaml_path = os.path.join(
|
||||
root_path, "src", "navigate", "config", "rest_api_config.yml"
|
||||
)
|
||||
|
||||
with open(yaml_path) as file:
|
||||
self.data = yaml.safe_load(file)
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_api_config(self):
|
||||
keys = self.data["Ilastik"].keys()
|
||||
self.assertIn("url", keys)
|
||||
ilastik_path = self.data["Ilastik"]["url"]
|
||||
assert "http" in ilastik_path, "Ilastik path not found in config file."
|
||||
assert "ilastik" in ilastik_path, "Ilastik path not found in config file."
|
||||
110
test/config/test_waveform_constants.py
Normal file
110
test/config/test_waveform_constants.py
Normal file
@@ -0,0 +1,110 @@
|
||||
# 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 Imports
|
||||
import unittest
|
||||
import yaml
|
||||
import os
|
||||
|
||||
|
||||
# Third Party Imports
|
||||
|
||||
# Local Imports
|
||||
|
||||
|
||||
class TestWaveformConstants(unittest.TestCase):
|
||||
def test_yaml_structure(self):
|
||||
current_path = os.path.abspath(os.path.dirname(__file__))
|
||||
root_path = os.path.dirname(os.path.dirname(current_path))
|
||||
yaml_path = os.path.join(
|
||||
root_path, "src", "navigate", "config", "waveform_constants.yml"
|
||||
)
|
||||
|
||||
# Load the YAML file
|
||||
with open(yaml_path) as file:
|
||||
data = yaml.safe_load(file)
|
||||
|
||||
expected_keys = ["amplitude", "offset"]
|
||||
|
||||
microscope_names = data["remote_focus_constants"].keys()
|
||||
for microscope_name in microscope_names:
|
||||
assert isinstance(microscope_name, str)
|
||||
|
||||
magnifications = data["remote_focus_constants"][microscope_name].keys()
|
||||
for magnification in magnifications:
|
||||
assert isinstance(magnification, str)
|
||||
|
||||
wavelengths = data["remote_focus_constants"][microscope_name][
|
||||
magnification
|
||||
].keys()
|
||||
for wavelength in wavelengths:
|
||||
assert isinstance(wavelength, str)
|
||||
|
||||
for key in expected_keys:
|
||||
assert (
|
||||
key
|
||||
in data["remote_focus_constants"][microscope_name][
|
||||
magnification
|
||||
][wavelength].keys()
|
||||
)
|
||||
|
||||
expected_keys = ["amplitude", "offset", "frequency"]
|
||||
|
||||
galvos = data["galvo_constants"].keys()
|
||||
for galvo in galvos:
|
||||
assert isinstance(galvo, str)
|
||||
|
||||
for microscope_name in microscope_names:
|
||||
assert microscope_name in data["galvo_constants"][galvo].keys()
|
||||
|
||||
magnifications = data["galvo_constants"][galvo][microscope_name].keys()
|
||||
for magnification in magnifications:
|
||||
assert (
|
||||
magnification
|
||||
in data["galvo_constants"][galvo][microscope_name].keys()
|
||||
)
|
||||
for key in expected_keys:
|
||||
assert (
|
||||
key
|
||||
in data["galvo_constants"][galvo][microscope_name][
|
||||
magnification
|
||||
].keys()
|
||||
)
|
||||
|
||||
other_constants = data["other_constants"].keys()
|
||||
assert "remote_focus_settle_duration" in other_constants
|
||||
# assert "percent_smoothing" in other_constants
|
||||
# assert "remote_focus_delay" in other_constants
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
64
test/config/test_waveform_templates.py
Normal file
64
test/config/test_waveform_templates.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# 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 Imports
|
||||
import unittest
|
||||
import yaml
|
||||
import os
|
||||
|
||||
# Third Party Imports
|
||||
|
||||
# Local Imports
|
||||
|
||||
|
||||
class TestWaveformTemplates(unittest.TestCase):
|
||||
def test_yaml_structure(self):
|
||||
current_path = os.path.abspath(os.path.dirname(__file__))
|
||||
root_path = os.path.dirname(os.path.dirname(current_path))
|
||||
yaml_path = os.path.join(
|
||||
root_path, "src", "navigate", "config", "waveform_templates.yml"
|
||||
)
|
||||
|
||||
with open(yaml_path) as file:
|
||||
data = yaml.safe_load(file)
|
||||
|
||||
expected_keys = ["Default", "Confocal-Projection", "Bidirectional"]
|
||||
|
||||
waveform_keys = data.keys()
|
||||
for key in waveform_keys:
|
||||
assert key in expected_keys
|
||||
|
||||
for subkey in data[key].keys():
|
||||
assert subkey in ["repeat", "expand"]
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
174
test/conftest.py
Normal file
174
test/conftest.py
Normal file
@@ -0,0 +1,174 @@
|
||||
"""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 tkinter as tk
|
||||
|
||||
# Third Party Imports
|
||||
import pytest
|
||||
|
||||
# Local Imports
|
||||
|
||||
|
||||
@pytest.fixture(scope="package")
|
||||
def dummy_model():
|
||||
"""Dummy model for testing.
|
||||
|
||||
Returns
|
||||
-------
|
||||
DummyModel
|
||||
Dummy model for testing.
|
||||
"""
|
||||
from test.model.dummy import DummyModel
|
||||
|
||||
model = DummyModel()
|
||||
return model
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tk_root():
|
||||
root = tk.Tk()
|
||||
yield root
|
||||
root.destroy()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def dummy_view(tk_root):
|
||||
"""Dummy view for testing.
|
||||
|
||||
Creates a dummy view for the controller tests.
|
||||
Will be deleted post test session
|
||||
|
||||
Returns:
|
||||
tkinter.Tk: Dummy view
|
||||
"""
|
||||
from navigate.view.main_application_window import MainApp
|
||||
|
||||
view = MainApp(tk_root)
|
||||
tk_root.update()
|
||||
yield view
|
||||
|
||||
|
||||
@pytest.fixture(scope="package")
|
||||
def dummy_controller(dummy_view):
|
||||
"""Dummy controller for testing.
|
||||
|
||||
Fixture that will mock controller functions called by sub controllers
|
||||
|
||||
Returns
|
||||
-------
|
||||
DummyController
|
||||
Dummy controller for testing.
|
||||
"""
|
||||
from test.model.dummy import DummyController
|
||||
|
||||
controller = DummyController(dummy_view)
|
||||
return controller
|
||||
|
||||
|
||||
# @pytest.fixture(scope="package")
|
||||
# def root():
|
||||
# import tkinter as tk
|
||||
|
||||
# root = tk.Tk()
|
||||
# yield root
|
||||
# root.destroy()
|
||||
|
||||
|
||||
# @pytest.fixture(scope="package")
|
||||
# def splash_screen(root):
|
||||
# from navigate.view.splash_screen import SplashScreen
|
||||
|
||||
# splash_screen = SplashScreen(root, "./icon/splash_screen_image.png")
|
||||
|
||||
# return splash_screen
|
||||
|
||||
|
||||
# @pytest.fixture(scope="package")
|
||||
# def controller(root, splash_screen):
|
||||
# from types import SimpleNamespace
|
||||
# from pathlib import Path
|
||||
|
||||
# from navigate.controller.controller import Controller
|
||||
|
||||
# # Use configuration files that ship with the code base
|
||||
# configuration_directory = Path.joinpath(
|
||||
# Path(__file__).resolve().parent.parent, "src", "navigate", "config"
|
||||
# )
|
||||
# configuration_path = Path.joinpath(configuration_directory, "configuration.yaml")
|
||||
# experiment_path = Path.joinpath(configuration_directory, "experiment.yml")
|
||||
# waveform_constants_path = Path.joinpath(configuration_directory,
|
||||
# "waveform_constants.yml")
|
||||
# rest_api_path = Path.joinpath(configuration_directory, "rest_api_config.yml")
|
||||
|
||||
# controller = Controller(
|
||||
# root,
|
||||
# splash_screen,
|
||||
# configuration_path,
|
||||
# experiment_path,
|
||||
# waveform_constants_path,
|
||||
# rest_api_path,
|
||||
# False,
|
||||
# SimpleNamespace(synthetic_hardware=True),
|
||||
# )
|
||||
|
||||
# yield controller
|
||||
# controller.execute("exit")
|
||||
|
||||
# @pytest.fixture(scope="package")
|
||||
# def model(controller):
|
||||
# return controller.model
|
||||
|
||||
|
||||
class IgnoreObj:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __getattr__(self, __name: str):
|
||||
return self
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def __setattr__(self, __name: str, __value):
|
||||
pass
|
||||
|
||||
def __getitem__(self, __key: str):
|
||||
return self
|
||||
|
||||
def __setitem__(self, __key: str, __value):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(scope="package")
|
||||
def ignore_obj():
|
||||
return IgnoreObj()
|
||||
0
test/controller/__init__.py
Normal file
0
test/controller/__init__.py
Normal file
0
test/controller/sub_controllers/__init__.py
Normal file
0
test/controller/sub_controllers/__init__.py
Normal file
617
test/controller/sub_controllers/test_acquire_bar.py
Normal file
617
test/controller/sub_controllers/test_acquire_bar.py
Normal file
@@ -0,0 +1,617 @@
|
||||
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
|
||||
# All rights reserved.
|
||||
import tkinter
|
||||
|
||||
# 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
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
# Third party imports
|
||||
import pytest
|
||||
|
||||
# Local imports
|
||||
from navigate.controller.sub_controllers import AcquireBarController
|
||||
from navigate.view.popups.acquire_popup import (
|
||||
AcquirePopUp,
|
||||
)
|
||||
from navigate.model.data_sources import FILE_TYPES
|
||||
|
||||
|
||||
class TestAcquireBarController:
|
||||
"""Tests for the AcquireBarController class"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_class(self, dummy_controller):
|
||||
"""Setup for the TestAcquireBarController class
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dummy_controller : DummyController
|
||||
Instance of the DummyController class
|
||||
"""
|
||||
c = dummy_controller
|
||||
v = dummy_controller.view
|
||||
|
||||
self.acquire_bar_controller = AcquireBarController(
|
||||
view=v.acquire_bar, parent_controller=c
|
||||
)
|
||||
self.acquire_bar_controller.populate_experiment_values()
|
||||
c.channels_tab_controller.populate_experiment_values()
|
||||
c.camera_setting_controller = MagicMock()
|
||||
|
||||
def test_init(self):
|
||||
"""Tests the initialization of the AcquireBarController class
|
||||
|
||||
Raises
|
||||
------
|
||||
AssertionError
|
||||
If the initialization of the AcquireBarController class is not correct
|
||||
"""
|
||||
assert isinstance(self.acquire_bar_controller, AcquireBarController)
|
||||
|
||||
def test_attr(self):
|
||||
"""Tests the attributes of the AcquireBarController class
|
||||
|
||||
Raises
|
||||
------
|
||||
AssertionError
|
||||
If the attributes of the AcquireBarController class are not correct
|
||||
"""
|
||||
# Listing off attributes to check existence
|
||||
attrs = ["mode", "is_save", "mode_dict"]
|
||||
|
||||
for attr in attrs:
|
||||
assert hasattr(self.acquire_bar_controller, attr)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode,mode_expected,value_expected",
|
||||
[
|
||||
("live", "indeterminate", None),
|
||||
("single", "determinate", 0),
|
||||
("customized", "indeterminate", None),
|
||||
("z-stack", "determinate", 0),
|
||||
],
|
||||
)
|
||||
def test_progress_bar(self, mode, mode_expected, value_expected):
|
||||
"""Tests the progress bar of the AcquireBarController class
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mode : str
|
||||
Mode of the progress bar
|
||||
mode_expected : str
|
||||
Expected mode of the progress bar
|
||||
value_expected : int
|
||||
Expected value of the progress bar
|
||||
|
||||
|
||||
Raises
|
||||
------
|
||||
AssertionError
|
||||
If the progress bar of the AcquireBarController class is not correct
|
||||
"""
|
||||
|
||||
# Startup progress bars
|
||||
images_received = 0
|
||||
mode = mode
|
||||
stop = False
|
||||
self.acquire_bar_controller.progress_bar(
|
||||
images_received=images_received,
|
||||
microscope_state=self.acquire_bar_controller.parent_controller.configuration[
|
||||
"experiment"
|
||||
][
|
||||
"MicroscopeState"
|
||||
],
|
||||
mode=mode,
|
||||
stop=stop,
|
||||
)
|
||||
|
||||
progress_mode = str(self.acquire_bar_controller.view.CurAcq["mode"])
|
||||
ovr_mode = str(self.acquire_bar_controller.view.OvrAcq["mode"])
|
||||
|
||||
assert progress_mode == mode_expected, (
|
||||
f"Wrong progress bar mode ({progress_mode}) "
|
||||
f"relative to microscope mode ({mode})"
|
||||
)
|
||||
assert ovr_mode == mode_expected, (
|
||||
f"Wrong progress bar mode ({progress_mode}) "
|
||||
f"relative to microscope mode ({mode})"
|
||||
)
|
||||
|
||||
if value_expected is not None:
|
||||
progress_start = int(self.acquire_bar_controller.view.CurAcq["value"])
|
||||
ovr_start = int(self.acquire_bar_controller.view.OvrAcq["value"])
|
||||
assert (
|
||||
progress_start == value_expected
|
||||
), "Wrong starting value for progress bar"
|
||||
assert ovr_start == value_expected, "Wrong starting value for progress bar"
|
||||
|
||||
# Updating progress bar
|
||||
images_received = 1
|
||||
while images_received < 6:
|
||||
self.acquire_bar_controller.progress_bar(
|
||||
images_received=images_received,
|
||||
microscope_state=self.acquire_bar_controller.parent_controller.configuration[
|
||||
"experiment"
|
||||
][
|
||||
"MicroscopeState"
|
||||
],
|
||||
mode=mode,
|
||||
stop=stop,
|
||||
)
|
||||
making_progress = float(self.acquire_bar_controller.view.CurAcq["value"])
|
||||
ovr_progress = float(self.acquire_bar_controller.view.OvrAcq["value"])
|
||||
assert (
|
||||
making_progress > 0
|
||||
), f"Progress bar should be moving in {mode} mode (making_progress)"
|
||||
assert (
|
||||
ovr_progress > 0
|
||||
), f"Progress bar should be moving in {mode} mode (ovr_progress)"
|
||||
images_received += 1
|
||||
|
||||
# Stopping progress bar
|
||||
self.acquire_bar_controller.progress_bar(
|
||||
images_received=images_received,
|
||||
microscope_state=self.acquire_bar_controller.parent_controller.configuration[
|
||||
"experiment"
|
||||
][
|
||||
"MicroscopeState"
|
||||
],
|
||||
mode=mode,
|
||||
stop=True,
|
||||
)
|
||||
|
||||
after_stop = float(self.acquire_bar_controller.view.CurAcq["value"])
|
||||
after_ovr = float(self.acquire_bar_controller.view.OvrAcq["value"])
|
||||
|
||||
assert after_stop == 0, "Progress Bar did not stop"
|
||||
assert after_ovr == 0, "Progress Bar did not stop"
|
||||
|
||||
@pytest.mark.parametrize("mode", ["live", "single", "z-stack", "customized"])
|
||||
def test_get_set_mode(self, mode):
|
||||
"""Tests the get_mode and set_mode methods of the AcquireBarController class
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mode : str
|
||||
Mode of the progress bar
|
||||
|
||||
|
||||
Raises
|
||||
------
|
||||
AssertionError
|
||||
If the get_mode and set_mode methods of the
|
||||
AcquireBarController class are not correct
|
||||
"""
|
||||
self.acquire_bar_controller.set_mode(mode)
|
||||
test = self.acquire_bar_controller.get_mode()
|
||||
assert test == mode, "Mode not set correctly"
|
||||
# assert imaging mode is updated in the experiment
|
||||
assert (
|
||||
self.acquire_bar_controller.parent_controller.configuration["experiment"][
|
||||
"MicroscopeState"
|
||||
]["image_mode"]
|
||||
== mode
|
||||
)
|
||||
|
||||
def test_set_save(self):
|
||||
"""Tests the set_save method of the AcquireBarController class
|
||||
|
||||
|
||||
Raises
|
||||
------
|
||||
AssertionError
|
||||
If the set_save method of the AcquireBarController class is not correct
|
||||
"""
|
||||
|
||||
# Assuming save state starts as False
|
||||
self.acquire_bar_controller.set_save_option(True)
|
||||
assert self.acquire_bar_controller.is_save is True, "Save option not correct"
|
||||
|
||||
# Return value to False
|
||||
self.acquire_bar_controller.set_save_option(False)
|
||||
assert (
|
||||
self.acquire_bar_controller.is_save is False
|
||||
), "Save option did not return to original value"
|
||||
|
||||
def test_stop_acquire(self):
|
||||
"""Tests the stop_acquire method of the AcquireBarController class
|
||||
|
||||
|
||||
Raises
|
||||
------
|
||||
AssertionError
|
||||
If the stop_acquire method of the AcquireBarController class is not correct
|
||||
"""
|
||||
|
||||
# Stopping acquisition
|
||||
self.acquire_bar_controller.stop_acquire()
|
||||
assert self.acquire_bar_controller.view.acquire_btn["text"] == "Acquire"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"user_mode,expected_mode",
|
||||
[
|
||||
("Continuous Scan", "live"),
|
||||
("Z-Stack", "z-stack"),
|
||||
("Single Acquisition", "single"),
|
||||
("Customized", "customized"),
|
||||
],
|
||||
)
|
||||
def test_update_microscope_mode(self, user_mode, expected_mode):
|
||||
"""Tests the update_microscope_mode method of the AcquireBarController class
|
||||
|
||||
Parameters
|
||||
----------
|
||||
user_mode : str
|
||||
Mode of the progress bar
|
||||
expected_mode : str
|
||||
Expected state of the progress bar
|
||||
|
||||
|
||||
Raises
|
||||
------
|
||||
AssertionError
|
||||
If the update_microscope_mode method of
|
||||
the AcquireBarController class is not correct
|
||||
"""
|
||||
# Assuming mode starts on live
|
||||
self.acquire_bar_controller.mode = "live"
|
||||
|
||||
# Setting to mode specified by user
|
||||
self.acquire_bar_controller.view.pull_down.set(user_mode)
|
||||
|
||||
# Generate event that calls update microscope mode
|
||||
self.acquire_bar_controller.view.pull_down.event_generate(
|
||||
"<<ComboboxSelected>>"
|
||||
)
|
||||
|
||||
# Checking that new mode gets set by function
|
||||
assert self.acquire_bar_controller.mode == expected_mode
|
||||
assert (
|
||||
self.acquire_bar_controller.parent_controller.configuration["experiment"][
|
||||
"MicroscopeState"
|
||||
]["image_mode"]
|
||||
== expected_mode
|
||||
)
|
||||
|
||||
# Resetting to live
|
||||
self.acquire_bar_controller.view.pull_down.set("Continuous Scan")
|
||||
self.acquire_bar_controller.view.pull_down.event_generate(
|
||||
"<<ComboboxSelected>>"
|
||||
)
|
||||
assert self.acquire_bar_controller.mode == "live"
|
||||
|
||||
def test_populate_experiment_values(self):
|
||||
"""Tests the populate_experiment_values method of the AcquireBarController class
|
||||
|
||||
|
||||
Raises
|
||||
------
|
||||
AssertionError
|
||||
If the populate_experiment_values method
|
||||
of the AcquireBarController class is not correct
|
||||
"""
|
||||
|
||||
# Calling function to populate values
|
||||
self.acquire_bar_controller.populate_experiment_values()
|
||||
|
||||
# Checking values are what we expect
|
||||
for key, value in self.acquire_bar_controller.saving_settings.items():
|
||||
assert (
|
||||
self.acquire_bar_controller.saving_settings[key]
|
||||
== self.acquire_bar_controller.parent_controller.configuration[
|
||||
"experiment"
|
||||
]["Saving"][key]
|
||||
)
|
||||
|
||||
# Assuming default value in exp file,
|
||||
# can be altered TODO maybe set default to current date
|
||||
assert self.acquire_bar_controller.saving_settings["date"] == "2022-06-07"
|
||||
assert (
|
||||
self.acquire_bar_controller.mode
|
||||
== self.acquire_bar_controller.parent_controller.configuration[
|
||||
"experiment"
|
||||
]["MicroscopeState"]["image_mode"]
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text,is_acquiring, save,mode,file_types,choice",
|
||||
[
|
||||
("Stop", False, None, "live", [], None),
|
||||
("Stop", True, None, "live", [], None),
|
||||
("Acquire", True, True, "live", [], None),
|
||||
("Acquire", False, True, "live", [], None),
|
||||
("Acquire", False, False, "z-stack", [], None),
|
||||
("Acquire", False, True, "z-stack", FILE_TYPES, "Done"),
|
||||
("Acquire", False, True, "z-stack", FILE_TYPES, "Cancel"),
|
||||
],
|
||||
)
|
||||
def test_launch_popup_window(
|
||||
self, text, is_acquiring, save, mode, file_types, choice
|
||||
):
|
||||
"""Tests the launch_popup_window method of the AcquireBarController class
|
||||
|
||||
This is the largest test for this controller.
|
||||
It will test multiple functions that are all used together
|
||||
and difficult to isolate.
|
||||
|
||||
Funcs Tested:
|
||||
launch_popup_window
|
||||
update_file_type
|
||||
launch_acquisition
|
||||
update_experiment_values
|
||||
acquire_pop.popup.dismiss # This will be double tested in view
|
||||
|
||||
Parameters
|
||||
----------
|
||||
text : str
|
||||
Text of the button that is clicked
|
||||
save : bool
|
||||
Whether or not to save the image
|
||||
mode : str
|
||||
Mode of the progress bar
|
||||
file_types : list
|
||||
List of file types to save as
|
||||
choice : str
|
||||
Choice of the user in the popup window
|
||||
|
||||
|
||||
Raises
|
||||
------
|
||||
AssertionError
|
||||
If the launch_popup_window method of the
|
||||
AcquireBarController class is not correct
|
||||
"""
|
||||
|
||||
# Setup Gui for test
|
||||
self.acquire_bar_controller.view.acquire_btn.configure(state="normal")
|
||||
self.acquire_bar_controller.view.acquire_btn.configure(text=text)
|
||||
self.acquire_bar_controller.is_save = save
|
||||
self.acquire_bar_controller.set_mode(mode)
|
||||
self.acquire_bar_controller.is_acquiring = is_acquiring
|
||||
|
||||
# Test based on setup, launches popup
|
||||
self.acquire_bar_controller.view.acquire_btn.invoke()
|
||||
|
||||
# Checking things are what we expect
|
||||
if text == "Stop":
|
||||
assert self.acquire_bar_controller.view.acquire_btn["text"] == "Stop"
|
||||
if is_acquiring:
|
||||
assert (
|
||||
str(self.acquire_bar_controller.view.acquire_btn["state"])
|
||||
== "disabled"
|
||||
)
|
||||
res = self.acquire_bar_controller.parent_controller.pop()
|
||||
assert res == "stop_acquire"
|
||||
else:
|
||||
assert (
|
||||
str(self.acquire_bar_controller.view.acquire_btn["state"])
|
||||
== "normal"
|
||||
)
|
||||
res = self.acquire_bar_controller.parent_controller.pop()
|
||||
assert res == "Empty command list"
|
||||
|
||||
if text == "Acquire":
|
||||
if is_acquiring:
|
||||
assert (
|
||||
str(self.acquire_bar_controller.view.acquire_btn["state"])
|
||||
== "normal"
|
||||
)
|
||||
res = self.acquire_bar_controller.parent_controller.pop()
|
||||
assert res == "Empty command list"
|
||||
return
|
||||
# First scenario Save is on and in live mode
|
||||
if save is True and mode == "live":
|
||||
assert self.acquire_bar_controller.view.acquire_btn["text"] == "Acquire"
|
||||
assert (
|
||||
str(self.acquire_bar_controller.view.acquire_btn["state"])
|
||||
== "disabled"
|
||||
)
|
||||
res = self.acquire_bar_controller.parent_controller.pop()
|
||||
print(res)
|
||||
print(self.acquire_bar_controller.parent_controller.pop())
|
||||
assert res == "acquire"
|
||||
|
||||
# Second scenario Save is off and mode is not live
|
||||
if save is False and mode != "live":
|
||||
assert self.acquire_bar_controller.view.acquire_btn["text"] == "Acquire"
|
||||
assert (
|
||||
str(self.acquire_bar_controller.view.acquire_btn["state"])
|
||||
== "disabled"
|
||||
)
|
||||
res = self.acquire_bar_controller.parent_controller.pop()
|
||||
assert res == "acquire"
|
||||
|
||||
# Third and final scenario Save is on and mode is not live
|
||||
if save is True and mode != "live":
|
||||
|
||||
# Checking if popup created
|
||||
assert isinstance(self.acquire_bar_controller.acquire_pop, AcquirePopUp)
|
||||
assert self.acquire_bar_controller.acquire_pop.popup.winfo_exists() == 1
|
||||
|
||||
# Testing update_file_type if list exists
|
||||
widgets = self.acquire_bar_controller.acquire_pop.get_widgets()
|
||||
if len(file_types) > 0:
|
||||
for file in file_types:
|
||||
widgets["file_type"].set(file)
|
||||
assert (
|
||||
self.acquire_bar_controller.saving_settings["file_type"]
|
||||
== file
|
||||
)
|
||||
# Resetting file type back to orginal
|
||||
widgets["file_type"].set("TIFF")
|
||||
assert (
|
||||
self.acquire_bar_controller.saving_settings["file_type"]
|
||||
== "TIFF"
|
||||
)
|
||||
|
||||
# Check that loop thru saving settings is correct
|
||||
for k, v in self.acquire_bar_controller.saving_settings.items():
|
||||
if widgets.get(k, None):
|
||||
value = widgets[k].get().strip()
|
||||
assert value == v
|
||||
|
||||
# Grabbing buttons to test
|
||||
buttons = self.acquire_bar_controller.acquire_pop.get_buttons()
|
||||
|
||||
if choice == "Cancel":
|
||||
# Testing cancel button
|
||||
|
||||
buttons["Cancel"].invoke() # Call to dismiss popup
|
||||
# Check toplevel gone
|
||||
assert (
|
||||
self.acquire_bar_controller.acquire_pop.popup.winfo_exists()
|
||||
== 0
|
||||
)
|
||||
assert (
|
||||
str(self.acquire_bar_controller.view.acquire_btn["state"])
|
||||
== "normal"
|
||||
)
|
||||
elif choice == "Done":
|
||||
# Testing done button
|
||||
|
||||
# Update experiment values test
|
||||
# Changing popup vals to test update
|
||||
# experiment values inside launch acquisition
|
||||
widgets["user"].set("John")
|
||||
widgets["tissue"].set("Heart")
|
||||
widgets["celltype"].set("34T")
|
||||
widgets["label"].set("BCB")
|
||||
widgets["solvent"].set("uDISCO")
|
||||
widgets["file_type"].set("OME-TIFF")
|
||||
|
||||
# Tab frame
|
||||
for i in range(100):
|
||||
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
|
||||
"misc"
|
||||
].insert(tkinter.END, f"L{i}")
|
||||
|
||||
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
|
||||
"shear_data"
|
||||
].set(True)
|
||||
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
|
||||
"shear_dimension"
|
||||
].set("XZ")
|
||||
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
|
||||
"shear_angle"
|
||||
].set(45)
|
||||
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
|
||||
"rotate_data"
|
||||
].set(True)
|
||||
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
|
||||
"rotate_angle_x"
|
||||
].set(90)
|
||||
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
|
||||
"rotate_angle_y"
|
||||
].set(90)
|
||||
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
|
||||
"rotate_angle_z"
|
||||
].set(90)
|
||||
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
|
||||
"down_sample_data"
|
||||
].set(True)
|
||||
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
|
||||
"lateral_down_sample"
|
||||
].set("2x")
|
||||
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
|
||||
"axial_down_sample"
|
||||
].set("2x")
|
||||
|
||||
# Launch acquisition start/test
|
||||
buttons["Done"].invoke() # Call to launch acquisition
|
||||
|
||||
# Check if update experiment values works correctly
|
||||
pop_vals = self.acquire_bar_controller.acquire_pop.get_variables()
|
||||
for k, v in self.acquire_bar_controller.saving_settings.items():
|
||||
if pop_vals.get(k, None):
|
||||
value = pop_vals[k].strip()
|
||||
assert value == v
|
||||
|
||||
# Check command sent to controller
|
||||
# and if acquire button changed to Stop
|
||||
res = self.acquire_bar_controller.parent_controller.pop()
|
||||
assert res == "acquire_and_save"
|
||||
assert (
|
||||
str(self.acquire_bar_controller.view.acquire_btn["state"])
|
||||
== "disabled"
|
||||
)
|
||||
assert (
|
||||
self.acquire_bar_controller.acquire_pop.popup.winfo_exists()
|
||||
== 0
|
||||
)
|
||||
|
||||
def test_frequent_start_and_stop_acquisition(self):
|
||||
# set up
|
||||
self.acquire_bar_controller.view.acquire_btn.configure(state="normal")
|
||||
self.acquire_bar_controller.view.acquire_btn.configure(text="Acquire")
|
||||
self.acquire_bar_controller.is_save = False
|
||||
self.acquire_bar_controller.set_mode("live")
|
||||
self.acquire_bar_controller.is_acquiring = False
|
||||
|
||||
# start acquisition
|
||||
self.acquire_bar_controller.view.acquire_btn.invoke()
|
||||
assert self.acquire_bar_controller.view.acquire_btn["text"] == "Acquire"
|
||||
assert str(self.acquire_bar_controller.view.acquire_btn["state"]) == "disabled"
|
||||
assert self.acquire_bar_controller.is_acquiring is True
|
||||
# assert dummy_controller_to_test_acquire_bar.acquisition_count == 1
|
||||
res = self.acquire_bar_controller.parent_controller.pop()
|
||||
assert res == "acquire"
|
||||
|
||||
# not in acquisition, click the "Acquire" button several times
|
||||
self.acquire_bar_controller.view.acquire_btn.invoke()
|
||||
assert self.acquire_bar_controller.view.acquire_btn["text"] == "Acquire"
|
||||
assert str(self.acquire_bar_controller.view.acquire_btn["state"]) == "disabled"
|
||||
res = self.acquire_bar_controller.parent_controller.pop()
|
||||
assert res == "Empty command list"
|
||||
self.acquire_bar_controller.view.acquire_btn.invoke()
|
||||
res = self.acquire_bar_controller.parent_controller.pop()
|
||||
assert res == "Empty command list"
|
||||
|
||||
# in acquisition, click "Stop" button several times
|
||||
self.acquire_bar_controller.view.acquire_btn.configure(state="normal")
|
||||
self.acquire_bar_controller.view.acquire_btn.configure(text="Stop")
|
||||
self.acquire_bar_controller.is_acquiring = True
|
||||
|
||||
self.acquire_bar_controller.view.acquire_btn.invoke()
|
||||
assert self.acquire_bar_controller.view.acquire_btn["text"] == "Stop"
|
||||
assert str(self.acquire_bar_controller.view.acquire_btn["state"]) == "disabled"
|
||||
res = self.acquire_bar_controller.parent_controller.pop()
|
||||
assert res == "stop_acquire"
|
||||
self.acquire_bar_controller.view.acquire_btn.invoke()
|
||||
res = self.acquire_bar_controller.parent_controller.pop()
|
||||
assert res == "Empty command list"
|
||||
self.acquire_bar_controller.view.acquire_btn.invoke()
|
||||
res = self.acquire_bar_controller.parent_controller.pop()
|
||||
assert res == "Empty command list"
|
||||
self.acquire_bar_controller.view.acquire_btn.invoke()
|
||||
res = self.acquire_bar_controller.parent_controller.pop()
|
||||
assert res == "Empty command list"
|
||||
202
test/controller/sub_controllers/test_autofocus.py
Normal file
202
test/controller/sub_controllers/test_autofocus.py
Normal file
@@ -0,0 +1,202 @@
|
||||
# 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 application imports
|
||||
from navigate.controller.sub_controllers import AutofocusPopupController
|
||||
from navigate.view.popups.autofocus_setting_popup import AutofocusPopup
|
||||
|
||||
|
||||
class TestAutofocusPopupController:
|
||||
"""Class for testing autofocus popup controller
|
||||
|
||||
Methods
|
||||
-------
|
||||
test_init()
|
||||
Tests that the controller is initialized correctly
|
||||
test_attr()
|
||||
Tests that the attributes are initialized correctly
|
||||
test_populate_experiment_values()
|
||||
Tests that the values are populated correctly
|
||||
test_update_experiment_values()
|
||||
Tests that the values are updated correctly
|
||||
test_start_autofocus()
|
||||
Tests that the start autofocus function works correctly
|
||||
test_display_plot()
|
||||
Tests that the display plot function works correctly
|
||||
"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_class(self, dummy_controller):
|
||||
"""Setup for testing autofocus popup controller
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dummy_controller : DummyController
|
||||
Dummy controller for testing
|
||||
|
||||
"""
|
||||
autofocus_popup = AutofocusPopup(dummy_controller.view)
|
||||
self.autofocus_controller = AutofocusPopupController(
|
||||
autofocus_popup, dummy_controller
|
||||
)
|
||||
|
||||
def test_init(self):
|
||||
"""Tests that the controller is initialized correctly
|
||||
|
||||
Raises
|
||||
------
|
||||
AssertionError
|
||||
If the controller is not initialized correctly
|
||||
"""
|
||||
assert isinstance(self.autofocus_controller, AutofocusPopupController)
|
||||
assert self.autofocus_controller.view.popup.winfo_exists() == 1
|
||||
|
||||
def test_attr(self):
|
||||
"""Tests that the attributes are initialized correctly
|
||||
|
||||
Raises
|
||||
------
|
||||
AssertionError
|
||||
If the attributes are not initialized correctly
|
||||
"""
|
||||
|
||||
# Listing off attributes to check existence
|
||||
attrs = [
|
||||
"autofocus_fig",
|
||||
"autofocus_coarse",
|
||||
"widgets",
|
||||
"setting_dict",
|
||||
]
|
||||
|
||||
for attr in attrs:
|
||||
assert hasattr(self.autofocus_controller, attr)
|
||||
|
||||
def test_populate_experiment_values(self):
|
||||
"""Tests that the values are populated correctly
|
||||
|
||||
Raises
|
||||
------
|
||||
AssertionError
|
||||
If the values are not populated correctly
|
||||
"""
|
||||
microscope_name = self.autofocus_controller.microscope_name
|
||||
device = self.autofocus_controller.widgets["device"].get()
|
||||
device_ref = self.autofocus_controller.widgets["device_ref"].get()
|
||||
for k in self.autofocus_controller.widgets:
|
||||
if k != "device" and k != "device_ref":
|
||||
assert self.autofocus_controller.widgets[k].get() == str(
|
||||
self.autofocus_controller.setting_dict[microscope_name][device][
|
||||
device_ref
|
||||
][k]
|
||||
)
|
||||
# Some values are ints but Tkinter only uses strings
|
||||
|
||||
def test_update_experiment_values(self):
|
||||
"""Tests that the values are updated correctly
|
||||
|
||||
Raises
|
||||
------
|
||||
AssertionError
|
||||
If the values are not updated correctly
|
||||
"""
|
||||
# Changing values
|
||||
self.autofocus_controller.widgets["coarse_range"].set(200)
|
||||
self.autofocus_controller.widgets["coarse_step_size"].set(30)
|
||||
self.autofocus_controller.view.setting_vars["coarse_selected"].set(False)
|
||||
self.autofocus_controller.widgets["fine_range"].set(25)
|
||||
self.autofocus_controller.widgets["fine_step_size"].set(2)
|
||||
self.autofocus_controller.view.setting_vars["fine_selected"].set(False)
|
||||
|
||||
microscope_name = self.autofocus_controller.microscope_name
|
||||
device = self.autofocus_controller.widgets["device"].get()
|
||||
device_ref = self.autofocus_controller.widgets["device_ref"].get()
|
||||
|
||||
# Checking values match
|
||||
for k in self.autofocus_controller.widgets:
|
||||
if k != "device" and k != "device_ref":
|
||||
assert self.autofocus_controller.widgets[k].get() == str(
|
||||
self.autofocus_controller.setting_dict[microscope_name][device][
|
||||
device_ref
|
||||
][k]
|
||||
)
|
||||
for k in self.autofocus_controller.view.setting_vars:
|
||||
assert (
|
||||
self.autofocus_controller.view.setting_vars[k].get()
|
||||
== self.autofocus_controller.setting_dict[microscope_name][device][
|
||||
device_ref
|
||||
][k]
|
||||
)
|
||||
|
||||
def test_start_autofocus(self):
|
||||
"""Tests that the start autofocus function works correctly
|
||||
|
||||
Raises
|
||||
------
|
||||
AssertionError
|
||||
If the start autofocus function does not work correctly
|
||||
"""
|
||||
|
||||
# Calling function
|
||||
self.autofocus_controller.start_autofocus()
|
||||
|
||||
# Checking message sent
|
||||
res = self.autofocus_controller.parent_controller.pop()
|
||||
assert res == "autofocus"
|
||||
|
||||
self.autofocus_controller.parent_controller.clear()
|
||||
|
||||
def test_display_plot(self):
|
||||
"""Tests that the display plot function works correctly
|
||||
|
||||
Todo: Retrieve data from axessubplot instance and
|
||||
check that it is correct
|
||||
|
||||
Raises
|
||||
------
|
||||
AssertionError
|
||||
If the display plot function does not work correctly
|
||||
"""
|
||||
# Make this robust by sending data and then
|
||||
# checking each plot is plotting correct data low priority
|
||||
|
||||
x_data = np.linspace(start=69750.0, stop=70250.0, num=101)
|
||||
y_data = np.random.rand(101)
|
||||
data = [x_data, y_data]
|
||||
self.autofocus_controller.display_plot([data, False, True])
|
||||
pass
|
||||
607
test/controller/sub_controllers/test_camera_settings.py
Normal file
607
test/controller/sub_controllers/test_camera_settings.py
Normal file
@@ -0,0 +1,607 @@
|
||||
# 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 Imports
|
||||
|
||||
# Third Party Imports
|
||||
import pytest
|
||||
import random
|
||||
|
||||
# Local Imports
|
||||
from navigate.controller.sub_controllers.camera_settings import (
|
||||
CameraSettingController,
|
||||
)
|
||||
|
||||
|
||||
class TestCameraSettingController:
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_class(self, dummy_controller):
|
||||
c = dummy_controller
|
||||
v = dummy_controller.view
|
||||
self.camera_settings = CameraSettingController(
|
||||
v.settings.camera_settings_tab, c
|
||||
)
|
||||
|
||||
def test_init(self):
|
||||
|
||||
assert isinstance(self.camera_settings, CameraSettingController)
|
||||
|
||||
# Setup, going to check what the default values widgets are set too
|
||||
microscope_name = self.camera_settings.parent_controller.configuration[
|
||||
"experiment"
|
||||
]["MicroscopeState"]["microscope_name"]
|
||||
camera_config_dict = self.camera_settings.parent_controller.configuration[
|
||||
"configuration"
|
||||
]["microscopes"][microscope_name]["camera"]
|
||||
|
||||
# Default Values
|
||||
assert (
|
||||
self.camera_settings.default_pixel_size
|
||||
== camera_config_dict["pixel_size_in_microns"]
|
||||
)
|
||||
assert self.camera_settings.default_height == camera_config_dict["y_pixels"]
|
||||
assert self.camera_settings.default_width == camera_config_dict["x_pixels"]
|
||||
|
||||
# Camera Mode
|
||||
assert list(self.camera_settings.mode_widgets["Sensor"].widget["values"]) == [
|
||||
"Normal",
|
||||
"Light-Sheet",
|
||||
]
|
||||
assert (
|
||||
str(self.camera_settings.mode_widgets["Sensor"].widget["state"])
|
||||
== "readonly"
|
||||
)
|
||||
|
||||
# Readout Mode
|
||||
assert list(self.camera_settings.mode_widgets["Readout"].widget["values"]) == [
|
||||
"Top-to-Bottom",
|
||||
"Bottom-to-Top",
|
||||
"Bidirectional",
|
||||
"Rev. Bidirectional",
|
||||
]
|
||||
assert (
|
||||
str(self.camera_settings.mode_widgets["Readout"].widget["state"])
|
||||
== "disabled"
|
||||
)
|
||||
|
||||
# Pixels
|
||||
assert (
|
||||
str(self.camera_settings.mode_widgets["Pixels"].widget["state"])
|
||||
== "disabled"
|
||||
)
|
||||
assert self.camera_settings.mode_widgets["Pixels"].widget.get() == ""
|
||||
assert self.camera_settings.mode_widgets["Pixels"].widget.cget("from") == 1
|
||||
assert (
|
||||
self.camera_settings.mode_widgets["Pixels"].widget.cget("to")
|
||||
== self.camera_settings.default_height / 2
|
||||
)
|
||||
assert self.camera_settings.mode_widgets["Pixels"].widget.cget("increment") == 1
|
||||
|
||||
# Framerate
|
||||
assert (
|
||||
str(self.camera_settings.framerate_widgets["exposure_time"].widget["state"])
|
||||
== "disabled"
|
||||
)
|
||||
assert (
|
||||
str(self.camera_settings.framerate_widgets["readout_time"].widget["state"])
|
||||
== "disabled"
|
||||
)
|
||||
assert (
|
||||
str(self.camera_settings.framerate_widgets["max_framerate"].widget["state"])
|
||||
== "disabled"
|
||||
)
|
||||
|
||||
# Set range value
|
||||
assert (
|
||||
self.camera_settings.roi_widgets["Width"].widget.cget("to")
|
||||
== self.camera_settings.default_width
|
||||
)
|
||||
assert self.camera_settings.roi_widgets["Width"].widget.cget("from") == 2
|
||||
assert self.camera_settings.roi_widgets["Width"].widget.cget("increment") == 2
|
||||
assert (
|
||||
self.camera_settings.roi_widgets["Height"].widget.cget("to")
|
||||
== self.camera_settings.default_height
|
||||
)
|
||||
assert self.camera_settings.roi_widgets["Height"].widget.cget("from") == 2
|
||||
assert self.camera_settings.roi_widgets["Height"].widget.cget("increment") == 2
|
||||
|
||||
# Set binning options
|
||||
assert list(self.camera_settings.roi_widgets["Binning"].widget["values"]) == [
|
||||
"{}x{}".format(i, i) for i in [1, 2, 4]
|
||||
]
|
||||
assert (
|
||||
str(self.camera_settings.roi_widgets["Binning"].widget["state"])
|
||||
== "readonly"
|
||||
)
|
||||
|
||||
# FOV
|
||||
assert (
|
||||
str(self.camera_settings.roi_widgets["FOV_X"].widget["state"]) == "disabled"
|
||||
)
|
||||
assert (
|
||||
str(self.camera_settings.roi_widgets["FOV_Y"].widget["state"]) == "disabled"
|
||||
)
|
||||
|
||||
def test_attr(self):
|
||||
|
||||
attrs = [
|
||||
"in_initialization",
|
||||
"resolution_value",
|
||||
"mode",
|
||||
"solvent",
|
||||
"mode_widgets",
|
||||
"framerate_widgets",
|
||||
"roi_widgets",
|
||||
"roi_btns",
|
||||
"default_pixel_size",
|
||||
"default_width",
|
||||
"default_height",
|
||||
"pixel_event_id",
|
||||
]
|
||||
|
||||
for attr in attrs:
|
||||
assert hasattr(self.camera_settings, attr)
|
||||
|
||||
def test_populate_experiment_values(self):
|
||||
microscope_name = self.camera_settings.parent_controller.configuration[
|
||||
"experiment"
|
||||
]["MicroscopeState"]["microscope_name"]
|
||||
self.camera_settings.parent_controller.configuration["experiment"][
|
||||
"CameraParameters"
|
||||
][microscope_name]["readout_time"] = 0.1
|
||||
# Populate widgets with values from experiment file and check
|
||||
self.camera_settings.populate_experiment_values()
|
||||
camera_setting_dict = self.camera_settings.parent_controller.configuration[
|
||||
"experiment"
|
||||
]["CameraParameters"][microscope_name]
|
||||
|
||||
# Checking values altered are correct
|
||||
assert dict(self.camera_settings.camera_setting_dict) == dict(
|
||||
self.camera_settings.parent_controller.configuration["experiment"][
|
||||
"CameraParameters"
|
||||
][microscope_name]
|
||||
)
|
||||
assert (
|
||||
str(self.camera_settings.mode_widgets["Sensor"].get())
|
||||
== camera_setting_dict["sensor_mode"]
|
||||
)
|
||||
if camera_setting_dict["sensor_mode"] == "Normal":
|
||||
pass
|
||||
# assert str(self.camera_settings.mode_widgets[
|
||||
# "Readout"].get()) == ""
|
||||
# assert str(self.camera_settings.mode_widgets[
|
||||
# "Pixels"].get()) == ""
|
||||
elif camera_setting_dict["sensor_mode"] == "Light-Sheet":
|
||||
assert (
|
||||
str(self.camera_settings.mode_widgets["Readout"].get())
|
||||
== self.camera_settings.camera_setting_dict["readout_direction"]
|
||||
)
|
||||
assert (
|
||||
str(self.camera_settings.mode_widgets["Pixels"].get())
|
||||
== self.camera_settings.camera_setting_dict["number_of_pixels"]
|
||||
)
|
||||
|
||||
# ROI
|
||||
assert (
|
||||
self.camera_settings.roi_widgets["Width"].get()
|
||||
== camera_setting_dict["x_pixels"]
|
||||
)
|
||||
assert (
|
||||
self.camera_settings.roi_widgets["Height"].get()
|
||||
== camera_setting_dict["y_pixels"]
|
||||
)
|
||||
|
||||
assert self.camera_settings.roi_widgets[
|
||||
"Top_X"
|
||||
].get() == camera_setting_dict.get("top_x", 0)
|
||||
assert self.camera_settings.roi_widgets[
|
||||
"Top_Y"
|
||||
].get() == camera_setting_dict.get("top_y", 0)
|
||||
if camera_setting_dict.get("is_centered", True):
|
||||
assert (
|
||||
str(self.camera_settings.roi_widgets["Top_X"].widget["state"])
|
||||
== "disabled"
|
||||
)
|
||||
assert (
|
||||
str(self.camera_settings.roi_widgets["Top_Y"].widget["state"])
|
||||
== "disabled"
|
||||
)
|
||||
|
||||
# Binning
|
||||
assert (
|
||||
str(self.camera_settings.roi_widgets["Binning"].get())
|
||||
== camera_setting_dict["binning"]
|
||||
)
|
||||
|
||||
# Exposure Time
|
||||
channels = self.camera_settings.parent_controller.configuration["experiment"][
|
||||
"MicroscopeState"
|
||||
]["channels"]
|
||||
exposure_time = channels[list(channels.keys())[0]]["camera_exposure_time"]
|
||||
assert (
|
||||
self.camera_settings.framerate_widgets["exposure_time"].get()
|
||||
== exposure_time
|
||||
)
|
||||
assert (
|
||||
self.camera_settings.framerate_widgets["frames_to_average"].get()
|
||||
== camera_setting_dict["frames_to_average"]
|
||||
)
|
||||
assert self.camera_settings.in_initialization is False
|
||||
|
||||
@pytest.mark.parametrize("mode", ["Normal", "Light-Sheet"])
|
||||
def test_update_experiment_values(self, mode):
|
||||
|
||||
microscope_name = self.camera_settings.parent_controller.configuration[
|
||||
"experiment"
|
||||
]["MicroscopeState"]["microscope_name"]
|
||||
# Setup basic default experiment
|
||||
self.camera_settings.camera_setting_dict = (
|
||||
self.camera_settings.parent_controller.configuration["experiment"][
|
||||
"CameraParameters"
|
||||
][microscope_name]
|
||||
)
|
||||
|
||||
# Setting up new values in widgets
|
||||
self.camera_settings.mode_widgets["Sensor"].set(mode)
|
||||
self.camera_settings.roi_widgets["Binning"].set("4x4")
|
||||
if mode == "Light-Sheet":
|
||||
self.camera_settings.mode_widgets["Readout"].set("Bottom-to-Top")
|
||||
self.camera_settings.mode_widgets["Pixels"].set(15)
|
||||
self.camera_settings.roi_widgets["Binning"].set("1x1")
|
||||
width, height = random.randint(1, 2000), random.randint(1, 2000)
|
||||
self.camera_settings.roi_widgets["Width"].set(width)
|
||||
self.camera_settings.roi_widgets["Height"].set(height)
|
||||
self.camera_settings.framerate_widgets["frames_to_average"].set(5)
|
||||
|
||||
# Update experiment dict and assert
|
||||
self.camera_settings.update_experiment_values()
|
||||
assert self.camera_settings.camera_setting_dict["sensor_mode"] == mode
|
||||
if mode == "Light-Sheet":
|
||||
assert (
|
||||
self.camera_settings.camera_setting_dict["readout_direction"]
|
||||
== "Bottom-to-Top"
|
||||
)
|
||||
assert (
|
||||
int(self.camera_settings.camera_setting_dict["number_of_pixels"]) == 15
|
||||
)
|
||||
step_width = self.camera_settings.step_width
|
||||
step_height = self.camera_settings.step_height
|
||||
set_width = int(width // step_width) * step_width
|
||||
set_height = int(height // step_height) * step_height
|
||||
if mode == "Light-Sheet":
|
||||
assert self.camera_settings.camera_setting_dict["binning"] == "1x1"
|
||||
assert self.camera_settings.camera_setting_dict["img_x_pixels"] == set_width
|
||||
assert (
|
||||
self.camera_settings.camera_setting_dict["img_y_pixels"] == set_height
|
||||
)
|
||||
binning = 1
|
||||
else:
|
||||
assert self.camera_settings.camera_setting_dict["binning"] == "4x4"
|
||||
# make sure image size is divisible by step_width and step_height
|
||||
assert self.camera_settings.camera_setting_dict["img_x_pixels"] == (
|
||||
set_width // 4
|
||||
) - (set_width // 4 % step_width)
|
||||
assert self.camera_settings.camera_setting_dict["img_y_pixels"] == (
|
||||
set_height // 4
|
||||
) - (set_height // 4 % step_height)
|
||||
binning = 4
|
||||
|
||||
# make sure x, y pixels are img_x, img_y pixels * binning
|
||||
assert (
|
||||
self.camera_settings.camera_setting_dict["x_pixels"]
|
||||
== self.camera_settings.camera_setting_dict["img_x_pixels"] * binning
|
||||
)
|
||||
assert (
|
||||
self.camera_settings.camera_setting_dict["y_pixels"]
|
||||
== self.camera_settings.camera_setting_dict["img_y_pixels"] * binning
|
||||
)
|
||||
assert (
|
||||
self.camera_settings.camera_setting_dict["pixel_size"]
|
||||
== self.camera_settings.default_pixel_size
|
||||
)
|
||||
assert self.camera_settings.camera_setting_dict["frames_to_average"] == 5
|
||||
|
||||
@pytest.mark.parametrize("mode", ["Normal", "Light-Sheet"])
|
||||
def test_update_sensor_mode(self, mode):
|
||||
self.camera_settings.populate_experiment_values()
|
||||
microscope_name = self.camera_settings.parent_controller.configuration[
|
||||
"experiment"
|
||||
]["MicroscopeState"]["microscope_name"]
|
||||
camera_setting_dict = self.camera_settings.parent_controller.configuration[
|
||||
"experiment"
|
||||
]["CameraParameters"][microscope_name]
|
||||
|
||||
# Set mode
|
||||
self.camera_settings.mode_widgets["Sensor"].widget.set(mode)
|
||||
self.camera_settings.mode_widgets["Sensor"].widget.event_generate(
|
||||
"<<ComboboxSelected>>"
|
||||
)
|
||||
|
||||
# Call update
|
||||
# self.camera_settings.update_sensor_mode()
|
||||
|
||||
# Check values
|
||||
if mode == "Normal":
|
||||
assert str(self.camera_settings.mode_widgets["Readout"].get()) == " "
|
||||
assert (
|
||||
str(self.camera_settings.mode_widgets["Readout"].widget["state"])
|
||||
== "disabled"
|
||||
)
|
||||
assert (
|
||||
str(self.camera_settings.mode_widgets["Pixels"].widget["state"])
|
||||
== "disabled"
|
||||
)
|
||||
assert str(self.camera_settings.mode_widgets["Pixels"].widget.get()) == ""
|
||||
|
||||
if mode == "Light-Sheet":
|
||||
assert (
|
||||
str(self.camera_settings.mode_widgets["Readout"].get())
|
||||
== camera_setting_dict["readout_direction"]
|
||||
)
|
||||
assert (
|
||||
str(self.camera_settings.mode_widgets["Readout"].widget["state"])
|
||||
== "readonly"
|
||||
)
|
||||
assert (
|
||||
str(self.camera_settings.mode_widgets["Pixels"].widget["state"])
|
||||
== "normal"
|
||||
)
|
||||
assert int(self.camera_settings.mode_widgets["Pixels"].widget.get()) == int(
|
||||
self.camera_settings.camera_setting_dict["number_of_pixels"]
|
||||
)
|
||||
|
||||
def test_update_exposure_time(self):
|
||||
|
||||
# Call funciton
|
||||
self.camera_settings.update_exposure_time(35)
|
||||
|
||||
# Check
|
||||
assert self.camera_settings.framerate_widgets["exposure_time"].get() == 35
|
||||
|
||||
@pytest.mark.parametrize("name", ["All", "1600", "1024", "512"])
|
||||
def test_update_roi(self, name):
|
||||
|
||||
# Call button to check if handler setup correctly
|
||||
self.camera_settings.roi_btns[name].invoke()
|
||||
|
||||
# Check
|
||||
if name == "All":
|
||||
name = "2048"
|
||||
assert str(self.camera_settings.roi_widgets["Width"].get()) == name
|
||||
assert str(self.camera_settings.roi_widgets["Height"].get()) == name
|
||||
|
||||
def test_update_fov(self):
|
||||
self.camera_settings.populate_experiment_values()
|
||||
|
||||
# Change invoke
|
||||
self.camera_settings.in_initialization = False
|
||||
self.camera_settings.roi_widgets["Width"].widget.set(2048)
|
||||
self.camera_settings.roi_widgets["Height"].widget.set(2048)
|
||||
xFov = int(self.camera_settings.roi_widgets["FOV_X"].get())
|
||||
yFov = int(self.camera_settings.roi_widgets["FOV_Y"].get())
|
||||
self.camera_settings.roi_widgets["Width"].widget.set(1600)
|
||||
self.camera_settings.roi_widgets["Height"].widget.set(1600)
|
||||
|
||||
# need these since we switched to read events
|
||||
self.camera_settings.roi_widgets["Width"].get_variable().get()
|
||||
self.camera_settings.roi_widgets["Height"].get_variable().get()
|
||||
|
||||
# Check
|
||||
assert xFov != int(self.camera_settings.roi_widgets["FOV_X"].get())
|
||||
assert yFov != int(self.camera_settings.roi_widgets["FOV_Y"].get())
|
||||
|
||||
# Reset
|
||||
self.camera_settings.roi_widgets["Width"].widget.set(2048)
|
||||
self.camera_settings.roi_widgets["Height"].widget.set(2048)
|
||||
|
||||
# need these since we switched to read events
|
||||
self.camera_settings.roi_widgets["Width"].get_variable().get()
|
||||
self.camera_settings.roi_widgets["Height"].get_variable().get()
|
||||
|
||||
assert int(self.camera_settings.roi_widgets["FOV_X"].get()) == 13066
|
||||
assert int(self.camera_settings.roi_widgets["FOV_Y"].get()) == 13066
|
||||
|
||||
@pytest.mark.parametrize("mode", ["live", "z-stack", "stop", "single"])
|
||||
@pytest.mark.parametrize("readout", ["Normal", "Light-Sheet"])
|
||||
def test_set_mode(self, mode, readout):
|
||||
|
||||
# Populate widgets with values from experiment file
|
||||
self.camera_settings.populate_experiment_values()
|
||||
|
||||
# Set mode
|
||||
self.camera_settings.mode_widgets["Sensor"].widget.set(readout)
|
||||
self.camera_settings.update_sensor_mode()
|
||||
self.camera_settings.set_mode(mode)
|
||||
|
||||
# Check
|
||||
assert self.camera_settings.mode == mode
|
||||
if mode != "stop":
|
||||
state = "disabled"
|
||||
else:
|
||||
state = "normal"
|
||||
if mode != "stop":
|
||||
state_readonly = "disabled"
|
||||
else:
|
||||
state_readonly = "readonly"
|
||||
|
||||
assert (
|
||||
str(self.camera_settings.mode_widgets["Sensor"].widget["state"])
|
||||
== state_readonly
|
||||
)
|
||||
if str(self.camera_settings.mode_widgets["Sensor"].get()) == "Light-Sheet":
|
||||
assert (
|
||||
str(self.camera_settings.mode_widgets["Readout"].widget["state"])
|
||||
== state_readonly
|
||||
)
|
||||
if mode == "live":
|
||||
assert (
|
||||
str(self.camera_settings.mode_widgets["Pixels"].widget["state"])
|
||||
== "normal"
|
||||
)
|
||||
else:
|
||||
assert (
|
||||
str(self.camera_settings.mode_widgets["Pixels"].widget["state"])
|
||||
== state
|
||||
)
|
||||
else:
|
||||
assert (
|
||||
str(self.camera_settings.mode_widgets["Readout"].widget["state"])
|
||||
== "disabled"
|
||||
)
|
||||
assert (
|
||||
str(self.camera_settings.mode_widgets["Pixels"].widget["state"])
|
||||
== "disabled"
|
||||
)
|
||||
assert (
|
||||
str(
|
||||
self.camera_settings.framerate_widgets["frames_to_average"].widget[
|
||||
"state"
|
||||
]
|
||||
)
|
||||
== state
|
||||
)
|
||||
assert str(self.camera_settings.roi_widgets["Width"].widget["state"]) == state
|
||||
assert str(self.camera_settings.roi_widgets["Height"].widget["state"]) == state
|
||||
assert (
|
||||
str(self.camera_settings.roi_widgets["Binning"].widget["state"])
|
||||
== state_readonly
|
||||
)
|
||||
for btn_name in self.camera_settings.roi_btns:
|
||||
assert str(self.camera_settings.roi_btns[btn_name]["state"]) == state
|
||||
|
||||
@pytest.mark.parametrize("zoom", ["0.63x", "1x", "2x", "3x", "4x", "5x", "6x"])
|
||||
def test_calculate_physical_dimensions(self, zoom):
|
||||
self.camera_settings.parent_controller.configuration["experiment"][
|
||||
"MicroscopeState"
|
||||
]["zoom"] = zoom
|
||||
|
||||
self.camera_settings.populate_experiment_values()
|
||||
|
||||
# Calling
|
||||
self.camera_settings.calculate_physical_dimensions()
|
||||
|
||||
pixel_size = self.camera_settings.default_pixel_size
|
||||
x_pixel = float(self.camera_settings.roi_widgets["Width"].get())
|
||||
y_pixel = float(self.camera_settings.roi_widgets["Height"].get())
|
||||
|
||||
microscope_state_dict = self.camera_settings.parent_controller.configuration[
|
||||
"experiment"
|
||||
]["MicroscopeState"]
|
||||
zoom = microscope_state_dict["zoom"]
|
||||
microscope_name = microscope_state_dict["microscope_name"]
|
||||
pixel_size = self.camera_settings.parent_controller.configuration[
|
||||
"configuration"
|
||||
]["microscopes"][microscope_name]["zoom"]["pixel_size"][zoom]
|
||||
|
||||
dim_x = x_pixel * pixel_size
|
||||
dim_y = y_pixel * pixel_size
|
||||
|
||||
assert float(self.camera_settings.roi_widgets["FOV_X"].get()) == float(
|
||||
int(dim_x)
|
||||
)
|
||||
assert float(self.camera_settings.roi_widgets["FOV_Y"].get()) == float(
|
||||
int(dim_y)
|
||||
)
|
||||
|
||||
# Reset to zoom of 1
|
||||
self.camera_settings.parent_controller.configuration["experiment"][
|
||||
"MicroscopeState"
|
||||
]["zoom"] = "1x"
|
||||
assert (
|
||||
self.camera_settings.parent_controller.configuration["experiment"][
|
||||
"MicroscopeState"
|
||||
]["zoom"]
|
||||
== "1x"
|
||||
)
|
||||
|
||||
def test_calculate_readout_time(self):
|
||||
"""
|
||||
TODO need more info about camera before testing
|
||||
"""
|
||||
pass
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode", ["single", "live", "customized", "z-stack", "stop"]
|
||||
)
|
||||
def test_update_number_of_pixels(self, mode):
|
||||
import random
|
||||
|
||||
self.camera_settings.populate_experiment_values()
|
||||
|
||||
self.camera_settings.mode = mode
|
||||
|
||||
self.camera_settings.mode_widgets["Pixels"].set("")
|
||||
assert self.camera_settings.camera_setting_dict["number_of_pixels"] != ""
|
||||
|
||||
n_pixels = random.randint(1, 100)
|
||||
self.camera_settings.mode_widgets["Pixels"].set(n_pixels)
|
||||
|
||||
# Check
|
||||
assert self.camera_settings.camera_setting_dict["number_of_pixels"] == int(
|
||||
n_pixels
|
||||
)
|
||||
if mode != "live" and mode != "stop":
|
||||
assert (
|
||||
self.camera_settings.camera_setting_dict["number_of_pixels"] == n_pixels
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"x_pixels, y_pixels", [(512, 512), (4096, 4096), (2304, 1024), (1024, 2048)]
|
||||
)
|
||||
def test_update_camera_device_related_setting(self, x_pixels, y_pixels):
|
||||
self.camera_settings.populate_experiment_values()
|
||||
|
||||
microscope_name = self.camera_settings.parent_controller.configuration[
|
||||
"experiment"
|
||||
]["MicroscopeState"]["microscope_name"]
|
||||
camera_config = self.camera_settings.parent_controller.configuration[
|
||||
"configuration"
|
||||
]["microscopes"][microscope_name]["camera"]
|
||||
default_x_pixels = camera_config["x_pixels"]
|
||||
default_y_pixels = camera_config["y_pixels"]
|
||||
|
||||
camera_config["x_pixels"] = x_pixels
|
||||
camera_config["y_pixels"] = y_pixels
|
||||
self.camera_settings.update_camera_device_related_setting()
|
||||
|
||||
assert self.camera_settings.roi_widgets["Width"].get() == min(
|
||||
self.camera_settings.camera_setting_dict["x_pixels"], x_pixels
|
||||
)
|
||||
assert self.camera_settings.roi_widgets["Height"].get() == min(
|
||||
self.camera_settings.camera_setting_dict["y_pixels"], y_pixels
|
||||
)
|
||||
assert self.camera_settings.roi_widgets["Width"].widget["to"] == x_pixels
|
||||
assert self.camera_settings.roi_widgets["Height"].widget["to"] == y_pixels
|
||||
|
||||
camera_config["x_pixels"] = default_x_pixels
|
||||
camera_config["y_pixels"] = default_y_pixels
|
||||
813
test/controller/sub_controllers/test_camera_view.py
Normal file
813
test/controller/sub_controllers/test_camera_view.py
Normal file
@@ -0,0 +1,813 @@
|
||||
# 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.
|
||||
#
|
||||
|
||||
from navigate.controller.sub_controllers.camera_view import CameraViewController
|
||||
import pytest
|
||||
import random
|
||||
from unittest.mock import MagicMock
|
||||
import numpy as np
|
||||
|
||||
|
||||
class TestCameraViewController:
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_class(self, dummy_controller):
|
||||
c = dummy_controller
|
||||
self.v = dummy_controller.view
|
||||
c.model = MagicMock()
|
||||
c.model.get_offset_variance_maps = MagicMock(return_value=[None, None])
|
||||
|
||||
self.camera_view = CameraViewController(self.v.camera_waveform.camera_tab, c)
|
||||
|
||||
self.microscope_state = {
|
||||
"channels": {
|
||||
"channel_1": {
|
||||
"is_selected": True,
|
||||
"laser": "488nm",
|
||||
"laser_index": 0,
|
||||
"camera_exposure_time": 200.0,
|
||||
"laser_power": 20.0,
|
||||
"interval_time": 1.0,
|
||||
"defocus": 100.0,
|
||||
"filter_wheel_0": "Empty-Alignment",
|
||||
"filter_position_0": 0,
|
||||
"filter_wheel_1": "Empty-Alignment",
|
||||
"filter_position_1": 0,
|
||||
},
|
||||
"channel_2": {
|
||||
"is_selected": True,
|
||||
"laser": "488nm",
|
||||
"laser_index": 0,
|
||||
"camera_exposure_time": 200.0,
|
||||
"laser_power": 20.0,
|
||||
"interval_time": 1.0,
|
||||
"defocus": 100.0,
|
||||
"filter_wheel_0": "Empty-Alignment",
|
||||
"filter_position_0": 0,
|
||||
"filter_wheel_1": "Empty-Alignment",
|
||||
"filter_position_1": 0,
|
||||
},
|
||||
"channel_3": {
|
||||
"is_selected": True,
|
||||
"laser": "488nm",
|
||||
"laser_index": 0,
|
||||
"camera_exposure_time": 200.0,
|
||||
"laser_power": 20.0,
|
||||
"interval_time": 1.0,
|
||||
"defocus": 100.0,
|
||||
"filter_wheel_0": "Empty-Alignment",
|
||||
"filter_position_0": 0,
|
||||
"filter_wheel_1": "Empty-Alignment",
|
||||
"filter_position_1": 0,
|
||||
},
|
||||
},
|
||||
"number_z_steps": np.random.randint(10, 100),
|
||||
"stack_cycling_mode": "per_stack",
|
||||
"image_mode": "z-stack",
|
||||
}
|
||||
|
||||
def test_init(self):
|
||||
|
||||
assert isinstance(self.camera_view, CameraViewController)
|
||||
|
||||
def test_update_display_state(self):
|
||||
pass
|
||||
|
||||
def test_get_absolute_position(self, monkeypatch):
|
||||
def mock_winfo_pointerx():
|
||||
self.x = int(random.random())
|
||||
return self.x
|
||||
|
||||
def mock_winfo_pointery():
|
||||
self.y = int(random.random())
|
||||
return self.y
|
||||
|
||||
monkeypatch.setattr(self.v, "winfo_pointerx", mock_winfo_pointerx)
|
||||
monkeypatch.setattr(self.v, "winfo_pointery", mock_winfo_pointery)
|
||||
|
||||
# call the function under test
|
||||
x, y = self.camera_view.get_absolute_position()
|
||||
|
||||
# make assertions about the return value
|
||||
assert x == self.x
|
||||
assert y == self.y
|
||||
|
||||
def test_popup_menu(self, monkeypatch):
|
||||
|
||||
# create a fake event object
|
||||
self.startx = int(random.random())
|
||||
self.starty = int(random.random())
|
||||
event = type("Event", (object,), {"x": self.startx, "y": self.starty})()
|
||||
self.grab_released = False
|
||||
self.x = int(random.random())
|
||||
self.y = int(random.random())
|
||||
self.absx = int(random.random())
|
||||
self.absy = int(random.random())
|
||||
|
||||
# monkey patch the get_absolute_position method to return specific values
|
||||
def mock_get_absolute_position():
|
||||
self.absx = int(random.random())
|
||||
self.absy = int(random.random())
|
||||
return self.absx, self.absy
|
||||
|
||||
monkeypatch.setattr(
|
||||
self.camera_view, "get_absolute_position", mock_get_absolute_position
|
||||
)
|
||||
|
||||
def mock_tk_popup(x, y):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def mock_grab_release():
|
||||
self.grab_released = True
|
||||
|
||||
monkeypatch.setattr(self.camera_view.menu, "tk_popup", mock_tk_popup)
|
||||
monkeypatch.setattr(self.camera_view.menu, "grab_release", mock_grab_release)
|
||||
|
||||
# call the function under test
|
||||
self.camera_view.popup_menu(event)
|
||||
|
||||
# make assertions about the state of the view object
|
||||
assert self.camera_view.move_to_x == self.startx
|
||||
assert self.camera_view.move_to_y == self.starty
|
||||
assert self.x == self.absx
|
||||
assert self.y == self.absy
|
||||
assert self.grab_released is True
|
||||
|
||||
@pytest.mark.parametrize("name", ["minmax", "image"])
|
||||
@pytest.mark.parametrize("data", [[random.randint(0, 49), random.randint(50, 100)]])
|
||||
def test_initialize(self, name, data):
|
||||
|
||||
self.camera_view.initialize(name, data)
|
||||
|
||||
# Checking values
|
||||
if name == "minmax":
|
||||
assert self.camera_view.image_palette["Min"].get() == data[0]
|
||||
assert self.camera_view.image_palette["Max"].get() == data[1]
|
||||
if name == "image":
|
||||
assert self.camera_view.image_metrics["Frames"].get() == data[0]
|
||||
|
||||
def test_set_mode(self):
|
||||
|
||||
# Test default mode
|
||||
self.camera_view.set_mode()
|
||||
assert self.camera_view.mode == ""
|
||||
assert self.camera_view.menu.entrycget("Move Here", "state") == "disabled"
|
||||
|
||||
# Test 'live' mode
|
||||
self.camera_view.set_mode("live")
|
||||
assert self.camera_view.mode == "live"
|
||||
assert self.camera_view.menu.entrycget("Move Here", "state") == "normal"
|
||||
|
||||
# Test 'stop' mode
|
||||
self.camera_view.set_mode("stop")
|
||||
assert self.camera_view.mode == "stop"
|
||||
assert self.camera_view.menu.entrycget("Move Here", "state") == "normal"
|
||||
|
||||
# Test invalid mode
|
||||
self.camera_view.set_mode("invalid")
|
||||
assert self.camera_view.mode == "invalid"
|
||||
assert self.camera_view.menu.entrycget("Move Here", "state") == "disabled"
|
||||
|
||||
@pytest.mark.parametrize("mode", ["stop", "live"])
|
||||
def test_move_stage(self, mode):
|
||||
|
||||
# Setup to check formula inside func is correct
|
||||
microscope_name = self.camera_view.parent_controller.configuration[
|
||||
"experiment"
|
||||
]["MicroscopeState"]["microscope_name"]
|
||||
zoom_value = self.camera_view.parent_controller.configuration["experiment"][
|
||||
"MicroscopeState"
|
||||
]["zoom"]
|
||||
pixel_size = self.camera_view.parent_controller.configuration["configuration"][
|
||||
"microscopes"
|
||||
][microscope_name]["zoom"]["pixel_size"][zoom_value]
|
||||
|
||||
current_center_x = (
|
||||
self.camera_view.zoom_rect[0][0] + self.camera_view.zoom_rect[0][1]
|
||||
) / 2
|
||||
current_center_y = (
|
||||
self.camera_view.zoom_rect[1][0] + self.camera_view.zoom_rect[1][1]
|
||||
) / 2
|
||||
|
||||
self.camera_view.move_to_x = int(random.random())
|
||||
self.camera_view.move_to_y = int(random.random())
|
||||
|
||||
# This is the formula to check
|
||||
offset_x = (
|
||||
(self.camera_view.move_to_x - current_center_x)
|
||||
/ self.camera_view.zoom_scale
|
||||
* self.camera_view.canvas_width_scale
|
||||
* pixel_size
|
||||
)
|
||||
offset_y = (
|
||||
(self.camera_view.move_to_y - current_center_y)
|
||||
/ self.camera_view.zoom_scale
|
||||
* self.camera_view.canvas_width_scale
|
||||
* pixel_size
|
||||
)
|
||||
|
||||
# Set the mode to check if statements
|
||||
self.camera_view.mode = mode
|
||||
|
||||
# Act
|
||||
self.camera_view.move_stage()
|
||||
|
||||
# Check
|
||||
assert self.camera_view.parent_controller.pop() == "get_stage_position"
|
||||
res = self.camera_view.parent_controller.pop()
|
||||
if mode == "stop":
|
||||
assert res == "move_stage_and_acquire_image"
|
||||
else:
|
||||
assert res == "move_stage_and_update_info"
|
||||
# Checking that move stage properly changed pos
|
||||
new_pos = self.camera_view.parent_controller.pop()
|
||||
self.camera_view.parent_controller.stage_pos["x"] -= offset_x
|
||||
self.camera_view.parent_controller.stage_pos["y"] += offset_y
|
||||
assert new_pos == self.camera_view.parent_controller.stage_pos
|
||||
|
||||
def test_reset_display(self, monkeypatch):
|
||||
|
||||
# Mock process image function
|
||||
process_image_called = False
|
||||
|
||||
def mock_process_image():
|
||||
nonlocal process_image_called
|
||||
process_image_called = True
|
||||
|
||||
monkeypatch.setattr(self.camera_view, "process_image", mock_process_image)
|
||||
|
||||
# Reset and check
|
||||
self.camera_view.reset_display()
|
||||
assert np.array_equal(
|
||||
self.camera_view.zoom_rect,
|
||||
np.array(
|
||||
[
|
||||
[0, self.camera_view.view.canvas_width],
|
||||
[0, self.camera_view.view.canvas_height],
|
||||
]
|
||||
),
|
||||
)
|
||||
assert np.array_equal(self.camera_view.zoom_offset, np.array([[0], [0]]))
|
||||
assert self.camera_view.zoom_value == 1
|
||||
assert self.camera_view.zoom_scale == 1
|
||||
assert self.camera_view.zoom_width == self.camera_view.view.canvas_width
|
||||
assert self.camera_view.zoom_height == self.camera_view.view.canvas_height
|
||||
assert process_image_called is True
|
||||
|
||||
def test_process_image(self):
|
||||
self.camera_view.image = np.random.randint(0, 256, (600, 800))
|
||||
self.camera_view.digital_zoom = MagicMock()
|
||||
# self.camera_view.detect_saturation = MagicMock()
|
||||
self.camera_view.down_sample_image = MagicMock()
|
||||
self.camera_view.scale_image_intensity = MagicMock()
|
||||
self.camera_view.add_crosshair = MagicMock()
|
||||
self.camera_view.apply_lut = MagicMock()
|
||||
self.camera_view.populate_image = MagicMock()
|
||||
|
||||
self.camera_view.process_image()
|
||||
|
||||
self.camera_view.digital_zoom.assert_called()
|
||||
# self.camera_view.detect_saturation.assert_called()
|
||||
self.camera_view.down_sample_image.assert_called()
|
||||
self.camera_view.scale_image_intensity.assert_called()
|
||||
self.camera_view.add_crosshair.assert_called()
|
||||
self.camera_view.apply_lut.assert_called()
|
||||
self.camera_view.populate_image.assert_called()
|
||||
|
||||
@pytest.mark.parametrize("num,value", [(4, 0.95), (5, 1.05)])
|
||||
def test_mouse_wheel(self, num, value):
|
||||
|
||||
# Test zoom in and out
|
||||
event = MagicMock()
|
||||
event.num = num
|
||||
event.x = int(random.random())
|
||||
event.y = int(random.random())
|
||||
event.delta = 120
|
||||
self.camera_view.zoom_value = 1
|
||||
self.camera_view.zoom_scale = 1
|
||||
self.camera_view.zoom_width = self.camera_view.view.canvas_width
|
||||
self.camera_view.zoom_height = self.camera_view.view.canvas_height
|
||||
self.camera_view.reset_display = MagicMock()
|
||||
self.camera_view.process_image = MagicMock()
|
||||
self.camera_view.mouse_wheel(event)
|
||||
|
||||
assert self.camera_view.zoom_value == value
|
||||
assert self.camera_view.zoom_scale == value
|
||||
assert self.camera_view.zoom_width == self.camera_view.view.canvas_width / value
|
||||
assert (
|
||||
self.camera_view.zoom_height == self.camera_view.view.canvas_height / value
|
||||
)
|
||||
|
||||
if (
|
||||
self.camera_view.zoom_width > self.camera_view.view.canvas_width
|
||||
or self.camera_view.zoom_height > self.camera_view.view.canvas_height
|
||||
):
|
||||
assert self.camera_view.reset_display.called is True
|
||||
self.camera_view.process_image.assert_called()
|
||||
else:
|
||||
assert self.camera_view.reset_display.called is False
|
||||
not self.camera_view.process_image.assert_called()
|
||||
|
||||
@pytest.mark.skip("AssertionError: Expected 'mock' to have been called.")
|
||||
def test_digital_zoom(self):
|
||||
|
||||
# Setup
|
||||
a, b, c, d, e, f = [random.randint(1, 100) for _ in range(6)]
|
||||
g, h = [random.randint(100, 400) for _ in range(2)]
|
||||
i, j = [random.randint(500, 1000) for _ in range(2)]
|
||||
val, scale, widthsc, heightsc = [random.randint(2, 4) for _ in range(4)]
|
||||
self.camera_view.zoom_rect = np.array([[a, b], [c, d]])
|
||||
self.camera_view.zoom_offset = np.array([[e], [f]])
|
||||
self.camera_view.zoom_value = val
|
||||
self.camera_view.zoom_scale = scale
|
||||
self.camera_view.zoom_width = g # 300
|
||||
self.camera_view.zoom_height = h # 400
|
||||
self.camera_view.view.canvas_width = i # 800
|
||||
self.camera_view.view.canvas_height = j # 600
|
||||
self.camera_view.canvas_width_scale = widthsc
|
||||
self.camera_view.canvas_height_scale = heightsc
|
||||
self.camera_view.image = np.random.randint(0, 256, (600, 800))
|
||||
self.camera_view.reset_display = MagicMock()
|
||||
|
||||
# Preprocess
|
||||
new_zoom_rec = self.camera_view.zoom_rect - self.camera_view.zoom_offset
|
||||
new_zoom_rec *= self.camera_view.zoom_value
|
||||
new_zoom_rec += self.camera_view.zoom_offset
|
||||
|
||||
x_start_index = int(-new_zoom_rec[0][0] / self.camera_view.zoom_scale)
|
||||
x_end_index = int(x_start_index + self.camera_view.zoom_width)
|
||||
y_start_index = int(-new_zoom_rec[1][0] / self.camera_view.zoom_scale)
|
||||
y_end_index = int(y_start_index + self.camera_view.zoom_height)
|
||||
|
||||
crosshair_x = (new_zoom_rec[0][0] + new_zoom_rec[0][1]) / 2
|
||||
crosshair_y = (new_zoom_rec[1][0] + new_zoom_rec[1][1]) / 2
|
||||
if crosshair_x < 0 or crosshair_x >= self.camera_view.view.canvas_width:
|
||||
crosshair_x = -1
|
||||
if crosshair_y < 0 or crosshair_y >= self.camera_view.view.canvas_height:
|
||||
crosshair_y = -1
|
||||
|
||||
new_image = self.camera_view.image[
|
||||
y_start_index
|
||||
* self.camera_view.canvas_height_scale : y_end_index
|
||||
* self.camera_view.canvas_height_scale,
|
||||
x_start_index
|
||||
* self.camera_view.canvas_width_scale : x_end_index
|
||||
* self.camera_view.canvas_width_scale,
|
||||
]
|
||||
|
||||
# Call func
|
||||
self.camera_view.digital_zoom()
|
||||
|
||||
# Check zoom rec
|
||||
assert np.array_equal(self.camera_view.zoom_rect, new_zoom_rec)
|
||||
# Check zoom offset
|
||||
assert np.array_equal(self.camera_view.zoom_offset, np.array([[0], [0]]))
|
||||
# Check zoom_value
|
||||
assert self.camera_view.zoom_value == 1
|
||||
# Check zoom_image
|
||||
assert np.array_equal(self.camera_view.zoom_image, new_image)
|
||||
# Check crosshairs
|
||||
assert self.camera_view.crosshair_x == int(crosshair_x)
|
||||
assert self.camera_view.crosshair_y == int(crosshair_y)
|
||||
# Check reset display
|
||||
self.camera_view.reset_display.assert_called()
|
||||
|
||||
@pytest.mark.parametrize("onoff", [True, False])
|
||||
def test_left_click(self, onoff):
|
||||
self.camera_view.add_crosshair = MagicMock()
|
||||
self.camera_view.digital_zoom = MagicMock()
|
||||
# self.camera_view.detect_saturation = MagicMock()
|
||||
self.camera_view.down_sample_image = MagicMock()
|
||||
self.camera_view.transpose_image = MagicMock()
|
||||
self.camera_view.scale_image_intensity = MagicMock()
|
||||
self.camera_view.apply_lut = MagicMock()
|
||||
self.camera_view.populate_image = MagicMock()
|
||||
event = MagicMock()
|
||||
|
||||
self.camera_view.image = np.random.randint(0, 256, (600, 800))
|
||||
self.camera_view.apply_cross_hair = onoff
|
||||
|
||||
self.camera_view.left_click(event)
|
||||
|
||||
self.camera_view.add_crosshair.assert_called()
|
||||
self.camera_view.populate_image.assert_called()
|
||||
assert self.camera_view.apply_cross_hair == (not onoff)
|
||||
|
||||
@pytest.mark.parametrize("frames", [0, 1, 2, 10])
|
||||
def test_update_max_count(self, frames):
|
||||
self.camera_view.image_metrics["Frames"].set(frames)
|
||||
|
||||
idx = 0
|
||||
temp = [0] * 40
|
||||
|
||||
while idx < 40:
|
||||
self.camera_view._last_frame_display_max = np.random.randint(0, 500)
|
||||
temp[idx] = self.camera_view._last_frame_display_max
|
||||
idx += 1
|
||||
# Act
|
||||
self.camera_view.update_max_counts()
|
||||
|
||||
assert self.camera_view._max_intensity_history_idx == idx % 32
|
||||
assert (
|
||||
self.camera_view.max_intensity_history[
|
||||
self.camera_view._max_intensity_history_idx - 1
|
||||
]
|
||||
== self.camera_view._last_frame_display_max
|
||||
)
|
||||
|
||||
# Assert
|
||||
if frames == 0:
|
||||
assert self.camera_view.image_metrics["Frames"].get() == 1
|
||||
frame_num = 1
|
||||
else:
|
||||
frame_num = frames
|
||||
|
||||
if frame_num <= idx:
|
||||
rolling_average = sum(temp[idx - frame_num : idx]) / frame_num
|
||||
else:
|
||||
rolling_average = sum(temp[:idx]) / frame_num
|
||||
|
||||
assert self.camera_view.image_metrics["Image"].get() == round(
|
||||
rolling_average, 0
|
||||
)
|
||||
|
||||
def test_down_sample_image(self, monkeypatch):
|
||||
import cv2
|
||||
|
||||
# create a test image
|
||||
test_image = np.random.rand(100, 100)
|
||||
self.zoom_image = test_image
|
||||
|
||||
# set the widget size
|
||||
widget = type("MyWidget", (object,), {"widget": self.camera_view.view})
|
||||
event = type(
|
||||
"MyEvent",
|
||||
(object,),
|
||||
{
|
||||
"widget": widget,
|
||||
"width": np.random.randint(5, 1000),
|
||||
"height": np.random.randint(5, 1000),
|
||||
},
|
||||
)
|
||||
self.camera_view.resize(event)
|
||||
|
||||
# monkeypatch cv2.resize
|
||||
def mocked_resize(src, dsize, interpolation=1):
|
||||
return np.ones((dsize[0], dsize[1]))
|
||||
|
||||
monkeypatch.setattr(cv2, "resize", mocked_resize)
|
||||
|
||||
# call the function
|
||||
down_sampled_image = self.camera_view.down_sample_image(test_image)
|
||||
|
||||
# assert that the image has been resized correctly
|
||||
assert np.shape(down_sampled_image) == (
|
||||
self.camera_view.view.canvas_width,
|
||||
self.camera_view.view.canvas_height,
|
||||
)
|
||||
|
||||
# assert that the image has not been modified
|
||||
assert not np.array_equal(down_sampled_image, test_image)
|
||||
|
||||
@pytest.mark.parametrize("auto", [True, False])
|
||||
def test_scale_image_intensity(self, auto):
|
||||
# Create a test image
|
||||
test_image = np.random.rand(100, 100)
|
||||
|
||||
# Set autoscale to True
|
||||
self.camera_view.autoscale = auto
|
||||
|
||||
if auto is False:
|
||||
self.camera_view.max_counts = 1.5
|
||||
self.camera_view.min_counts = 0.5
|
||||
|
||||
# Call the function
|
||||
scaled_image = self.camera_view.scale_image_intensity(test_image)
|
||||
|
||||
# Assert that max_counts and min_counts have been set correctly
|
||||
if auto is True:
|
||||
assert self.camera_view._last_frame_display_max == np.max(test_image)
|
||||
|
||||
# Assert that the image has been scaled correctly
|
||||
assert np.min(scaled_image) >= 0
|
||||
assert np.max(scaled_image) <= 255
|
||||
|
||||
def test_populate_image(self, monkeypatch):
|
||||
from PIL import Image, ImageTk
|
||||
import cv2
|
||||
|
||||
# Create test image
|
||||
self.camera_view.cross_hair_image = np.random.rand(100, 100)
|
||||
self.camera_view.ilastik_seg_mask = np.random.rand(100, 100)
|
||||
|
||||
# Set display_mask_flag to True
|
||||
self.camera_view.display_mask_flag = True
|
||||
|
||||
# Monkeypatch the Image.fromarray() method of PIL
|
||||
def mocked_fromarray(arr):
|
||||
return arr
|
||||
|
||||
monkeypatch.setattr(Image, "fromarray", mocked_fromarray)
|
||||
|
||||
# Monkeypatch the cv2.resize() function
|
||||
def mocked_resize(arr, size):
|
||||
return arr
|
||||
|
||||
monkeypatch.setattr(cv2, "resize", mocked_resize)
|
||||
|
||||
# Monkeypatch the Image.blend() method of PIL
|
||||
def mocked_blend(img1, img2, alpha):
|
||||
return img1 * alpha + img2 * (1 - alpha)
|
||||
|
||||
monkeypatch.setattr(Image, "blend", mocked_blend)
|
||||
|
||||
def mocked_PhotoImage(img):
|
||||
mock_photo = MagicMock()
|
||||
# Set up width and height methods to return appropriate dimensions
|
||||
mock_photo.width.return_value = 100
|
||||
mock_photo.height.return_value = 100
|
||||
return mock_photo
|
||||
|
||||
monkeypatch.setattr(ImageTk, "PhotoImage", mocked_PhotoImage)
|
||||
|
||||
self.camera_view.canvas.create_image = MagicMock()
|
||||
self.camera_view.image_cache_flag = True
|
||||
|
||||
# Call the function
|
||||
self.camera_view.populate_image(self.camera_view.cross_hair_image)
|
||||
|
||||
# Assert that the tk_image has been created correctly
|
||||
assert self.camera_view._img_buf is not None
|
||||
|
||||
# Set display_mask_flag to True
|
||||
self.camera_view.display_mask_flag = False
|
||||
|
||||
# Call the function
|
||||
self.camera_view.populate_image(self.camera_view.cross_hair_image)
|
||||
|
||||
def test_initialize_non_live_display(self):
|
||||
# Create test buffer and microscope_state
|
||||
camera_parameters = {
|
||||
"img_x_pixels": np.random.randint(1, 200),
|
||||
"img_y_pixels": np.random.randint(1, 200),
|
||||
}
|
||||
|
||||
# Call the function
|
||||
self.camera_view.initialize_non_live_display(
|
||||
self.microscope_state, camera_parameters
|
||||
)
|
||||
|
||||
# Assert that the variables have been set correctly
|
||||
assert self.camera_view.image_count == 0
|
||||
assert self.camera_view.slice_index == 0
|
||||
assert self.camera_view.number_of_channels == len(
|
||||
self.microscope_state["channels"]
|
||||
)
|
||||
assert (
|
||||
self.camera_view.number_of_slices == self.microscope_state["number_z_steps"]
|
||||
)
|
||||
assert (
|
||||
self.camera_view.total_images_per_volume
|
||||
== self.camera_view.number_of_channels * self.camera_view.number_of_slices
|
||||
)
|
||||
assert self.camera_view.original_image_width == int(
|
||||
camera_parameters["img_x_pixels"]
|
||||
)
|
||||
assert self.camera_view.original_image_height == int(
|
||||
camera_parameters["img_y_pixels"]
|
||||
)
|
||||
assert self.camera_view.canvas_width_scale == float(
|
||||
self.camera_view.original_image_width / self.camera_view.canvas_width
|
||||
)
|
||||
assert self.camera_view.canvas_height_scale == float(
|
||||
self.camera_view.original_image_height / self.camera_view.canvas_height
|
||||
)
|
||||
|
||||
def test_identify_channel_index_and_slice(self):
|
||||
# Not currently in use
|
||||
pass
|
||||
|
||||
def test_retrieve_image_slice_from_volume(self):
|
||||
# Not currently in use
|
||||
pass
|
||||
|
||||
@pytest.mark.parametrize("transpose", [True, False])
|
||||
def test_display_image(self, transpose):
|
||||
"""Test the display of an image on the camera view
|
||||
|
||||
TODO: The recent refactor makes this test non-functional. It needs to be updated
|
||||
The newer code does not use the camera_view.image attribute after the
|
||||
transpose operation, so any transpose/image flipping is not reflected in the
|
||||
final image. The test should be updated to reflect this.,
|
||||
"""
|
||||
|
||||
self.camera_view.initialize_non_live_display(
|
||||
self.microscope_state, {"img_x_pixels": 50, "img_y_pixels": 100}
|
||||
)
|
||||
self.camera_view.digital_zoom = MagicMock()
|
||||
# self.camera_view.detect_saturation = MagicMock()
|
||||
self.camera_view.down_sample_image = MagicMock()
|
||||
self.camera_view.scale_image_intensity = MagicMock()
|
||||
self.camera_view.apply_lut = MagicMock()
|
||||
self.camera_view.populate_image = MagicMock()
|
||||
images = np.random.rand(10, 100, 50)
|
||||
|
||||
self.camera_view.transpose = transpose
|
||||
count = 0
|
||||
self.camera_view.image_count = count
|
||||
self.camera_view.image_metrics = {"Channel": MagicMock()}
|
||||
self.camera_view.update_max_counts = MagicMock()
|
||||
self.camera_view.flip_flags = {"x": False, "y": False}
|
||||
|
||||
image_id = np.random.randint(0, 10)
|
||||
self.camera_view.try_to_display_image(images[image_id])
|
||||
|
||||
assert (
|
||||
self.camera_view.spooled_images.size_y,
|
||||
self.camera_view.spooled_images.size_x,
|
||||
) == np.shape(images[image_id])
|
||||
assert self.camera_view.image_count == count + 1
|
||||
|
||||
self.camera_view.flip_flags = {"x": True, "y": False}
|
||||
image_id = np.random.randint(0, 10)
|
||||
self.camera_view.try_to_display_image(images[image_id])
|
||||
# assert np.shape(self.camera_view.image) == np.shape(images[image_id])
|
||||
# if not transpose:
|
||||
# assert (self.camera_view.image == images[image_id][:, ::-1]).all()
|
||||
# else:
|
||||
# assert (self.camera_view.image == images[image_id][:, ::-1].T).all()
|
||||
assert self.camera_view.image_count == count + 2
|
||||
|
||||
self.camera_view.flip_flags = {"x": False, "y": True}
|
||||
image_id = np.random.randint(0, 10)
|
||||
self.camera_view.try_to_display_image(images[image_id])
|
||||
# assert np.shape(self.camera_view.image) == np.shape(images[image_id])
|
||||
# if not transpose:
|
||||
# assert (self.camera_view.image == images[image_id][::-1, :]).all()
|
||||
# else:
|
||||
# assert (self.camera_view.image == images[image_id][::-1, :].T).all()
|
||||
assert self.camera_view.image_count == count + 3
|
||||
|
||||
self.camera_view.flip_flags = {"x": True, "y": True}
|
||||
image_id = np.random.randint(0, 10)
|
||||
self.camera_view.try_to_display_image(images[image_id])
|
||||
# assert np.shape(self.camera_view.image) == np.shape(images[image_id])
|
||||
# if not transpose:
|
||||
# assert (self.camera_view.image == images[image_id][::-1, ::-1]).all()
|
||||
# else:
|
||||
# assert (self.camera_view.image == images[image_id][::-1, ::-1].T).all()
|
||||
assert self.camera_view.image_count == count + 4
|
||||
|
||||
def test_add_crosshair(self):
|
||||
|
||||
# Arrange
|
||||
x = self.camera_view.canvas_width
|
||||
y = self.camera_view.canvas_height
|
||||
image = np.random.rand(x, y)
|
||||
self.camera_view.apply_cross_hair = True
|
||||
|
||||
# Act
|
||||
image2 = self.camera_view.add_crosshair(image)
|
||||
|
||||
# Assert
|
||||
assert np.all(image2[:, self.camera_view.zoom_rect[0][1] // 2] == 255)
|
||||
assert np.all(image2[self.camera_view.zoom_rect[1][1] // 2, :] == 255)
|
||||
|
||||
def test_apply_LUT(self):
|
||||
# Someone else with better numpy understanding will need to do this TODO
|
||||
|
||||
pass
|
||||
|
||||
def test_update_LUT(self):
|
||||
# Same as apply LUT TODO
|
||||
pass
|
||||
|
||||
def test_toggle_min_max_button(self):
|
||||
|
||||
# Setup for true path
|
||||
self.camera_view.image_palette["Autoscale"].set(True)
|
||||
|
||||
# Act by calling function
|
||||
self.camera_view.toggle_min_max_buttons()
|
||||
|
||||
# Assert things are correct
|
||||
assert str(self.camera_view.image_palette["Min"].widget["state"]) == "disabled"
|
||||
assert str(self.camera_view.image_palette["Max"].widget["state"]) == "disabled"
|
||||
|
||||
# Setup for false path
|
||||
self.camera_view.image_palette["Autoscale"].set(False)
|
||||
|
||||
# Mock function call to isolate
|
||||
self.camera_view.update_min_max_counts = MagicMock()
|
||||
|
||||
# Act by calling function
|
||||
self.camera_view.toggle_min_max_buttons()
|
||||
|
||||
# Assert things are correct and called
|
||||
assert str(self.camera_view.image_palette["Min"].widget["state"]) == "normal"
|
||||
assert str(self.camera_view.image_palette["Max"].widget["state"]) == "normal"
|
||||
self.camera_view.update_min_max_counts.assert_called()
|
||||
|
||||
def test_transpose_image(self):
|
||||
# Create test data
|
||||
self.camera_view.image_palette["Flip XY"].set(True)
|
||||
self.camera_view.transpose = None
|
||||
|
||||
# Call the function
|
||||
self.camera_view.update_transpose_state()
|
||||
|
||||
# Assert the output
|
||||
assert self.camera_view.transpose is True
|
||||
|
||||
# Create test data
|
||||
self.camera_view.image_palette["Flip XY"].set(False)
|
||||
self.camera_view.transpose = None
|
||||
|
||||
# Call the function
|
||||
self.camera_view.update_transpose_state()
|
||||
|
||||
# Assert the output
|
||||
assert self.camera_view.transpose is False
|
||||
|
||||
def test_update_min_max_counts(self):
|
||||
# Create test data
|
||||
min = np.random.randint(0, 10)
|
||||
max = np.random.randint(0, 10)
|
||||
self.camera_view.image_palette["Min"].set(min)
|
||||
self.camera_view.image_palette["Max"].set(max)
|
||||
|
||||
self.camera_view.min_counts = None
|
||||
self.camera_view.max_counts = None
|
||||
|
||||
# Call the function
|
||||
self.camera_view.update_min_max_counts()
|
||||
|
||||
# Assert the output
|
||||
assert self.camera_view.min_counts == min
|
||||
assert self.camera_view.max_counts == max
|
||||
|
||||
def test_set_mask_color_table(self):
|
||||
# This is beyond me currently TODO
|
||||
pass
|
||||
|
||||
def test_display_mask(self, monkeypatch):
|
||||
import cv2
|
||||
|
||||
# Create test data
|
||||
self.camera_view.ilastik_seg_mask = None
|
||||
self.camera_view.ilastik_mask_ready_lock.acquire()
|
||||
mask = np.zeros((5, 5), dtype=np.uint8)
|
||||
self.camera_view.mask_color_table = np.zeros((256, 1, 3), dtype=np.uint8)
|
||||
|
||||
# Define the monkeypatch
|
||||
def mock_applyColorMap(mask, mask_color_table):
|
||||
return mask
|
||||
|
||||
# Apply the monkeypatch
|
||||
monkeypatch.setattr(cv2, "applyColorMap", mock_applyColorMap)
|
||||
|
||||
# Call the function
|
||||
self.camera_view.display_mask(mask)
|
||||
|
||||
# Assert the output
|
||||
assert (self.camera_view.ilastik_seg_mask == mask).all()
|
||||
assert not self.camera_view.ilastik_mask_ready_lock.locked()
|
||||
|
||||
def test_update_canvas_size(self):
|
||||
self.camera_view.view.canvas["width"] = random.randint(1, 2000)
|
||||
self.camera_view.view.canvas["height"] = random.randint(1, 2000)
|
||||
|
||||
self.camera_view.update_canvas_size()
|
||||
|
||||
assert self.camera_view.canvas_width > 0
|
||||
assert self.camera_view.canvas_height > 0
|
||||
168
test/controller/sub_controllers/test_channels_settings.py
Normal file
168
test/controller/sub_controllers/test_channels_settings.py
Normal file
@@ -0,0 +1,168 @@
|
||||
# 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
|
||||
|
||||
|
||||
class TestChannelSettingController:
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_class(self, dummy_controller):
|
||||
from navigate.controller.sub_controllers.channels_tab import (
|
||||
ChannelsTabController,
|
||||
)
|
||||
from navigate.controller.sub_controllers.channels_settings import (
|
||||
ChannelSettingController,
|
||||
)
|
||||
|
||||
self.ctc = ChannelsTabController(
|
||||
dummy_controller.view.settings.channels_tab, dummy_controller
|
||||
)
|
||||
self.ctc.commands = []
|
||||
self.ctc.execute = lambda command: self.ctc.commands.append(command)
|
||||
|
||||
self.channel_setting = ChannelSettingController(
|
||||
self.ctc.view.channel_widgets_frame,
|
||||
self.ctc,
|
||||
dummy_controller.configuration_controller,
|
||||
)
|
||||
self.channel_setting.populate_experiment_values(
|
||||
dummy_controller.configuration["experiment"]["MicroscopeState"]["channels"]
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode,state,state_readonly",
|
||||
[("stop", "normal", "readonly"), ("live", "disabled", "readonly")],
|
||||
)
|
||||
def test_set_mode(self, mode, state, state_readonly):
|
||||
|
||||
self.channel_setting.set_mode(mode)
|
||||
|
||||
for i in range(5):
|
||||
assert str(self.channel_setting.view.channel_checks[i]["state"]) == state
|
||||
# interval widget is disabled now
|
||||
assert (
|
||||
str(self.channel_setting.view.interval_spins[i]["state"]) == "disabled"
|
||||
)
|
||||
if mode == "stop":
|
||||
assert (
|
||||
str(self.channel_setting.view.laser_pulldowns[i]["state"])
|
||||
== state_readonly
|
||||
)
|
||||
else:
|
||||
assert (
|
||||
str(self.channel_setting.view.laser_pulldowns[i]["state"])
|
||||
== "disabled"
|
||||
)
|
||||
if self.channel_setting.mode != "live" or (
|
||||
self.channel_setting.mode == "live"
|
||||
and not self.channel_setting.view.channel_variables[i].get()
|
||||
):
|
||||
assert (
|
||||
str(self.channel_setting.view.exptime_pulldowns[i]["state"])
|
||||
== state
|
||||
)
|
||||
if not self.channel_setting.view.channel_variables[i].get():
|
||||
assert (
|
||||
str(self.channel_setting.view.laserpower_pulldowns[i]["state"])
|
||||
== state
|
||||
)
|
||||
assert (
|
||||
str(self.channel_setting.view.filterwheel_pulldowns[i]["state"])
|
||||
== state_readonly
|
||||
)
|
||||
assert str(self.channel_setting.view.defocus_spins[i]["state"]) == state
|
||||
|
||||
def test_channel_callback(self):
|
||||
import random
|
||||
|
||||
self.channel_setting.in_initialization = False
|
||||
channel_dict = (
|
||||
self.channel_setting.parent_controller.parent_controller.configuration[
|
||||
"experiment"
|
||||
]["MicroscopeState"]["channels"]
|
||||
)
|
||||
# shuffle the channels
|
||||
new_channel_dict = {
|
||||
k: v
|
||||
for k, v in zip(
|
||||
channel_dict.keys(),
|
||||
random.choices(channel_dict.values(), k=len(channel_dict.keys())),
|
||||
)
|
||||
}
|
||||
|
||||
self.channel_setting.populate_experiment_values(channel_dict)
|
||||
for channel_id in range(self.channel_setting.num):
|
||||
vals = self.channel_setting.get_vals_by_channel(channel_id)
|
||||
channel_key = f"channel_{str(channel_id + 1)}"
|
||||
try:
|
||||
setting_dict = channel_dict[channel_key]
|
||||
new_setting_dict = new_channel_dict[channel_key]
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
# Test channel callback through trace writes
|
||||
for k in setting_dict.keys():
|
||||
if k == "laser_index" or k.startswith("filter_position"):
|
||||
continue
|
||||
if k == "defocus":
|
||||
new_val = float(random.randint(1, 10))
|
||||
else:
|
||||
new_val = new_setting_dict[k]
|
||||
|
||||
vals[k].set(new_val)
|
||||
|
||||
assert str(vals[k].get()) == str(new_val)
|
||||
if k != "defocus":
|
||||
assert setting_dict[k] == new_setting_dict[k]
|
||||
|
||||
if k == "laser":
|
||||
assert (
|
||||
setting_dict["laser_index"] == new_setting_dict["laser_index"]
|
||||
)
|
||||
elif k == "filter":
|
||||
assert (
|
||||
setting_dict["filter_position"]
|
||||
== new_setting_dict["filter_position"]
|
||||
)
|
||||
elif k == "camera_exposure_time" or k == "is_selected":
|
||||
assert (
|
||||
self.channel_setting.parent_controller.commands.pop()
|
||||
== "recalculate_timepoint"
|
||||
)
|
||||
self.channel_setting.parent_controller.commands = [] # reset
|
||||
|
||||
def test_get_vals_by_channel(self):
|
||||
# Not needed to test IMO
|
||||
pass
|
||||
|
||||
def test_get_index(self):
|
||||
# Not needed to test IMO
|
||||
pass
|
||||
385
test/controller/sub_controllers/test_channels_tab.py
Normal file
385
test/controller/sub_controllers/test_channels_tab.py
Normal file
@@ -0,0 +1,385 @@
|
||||
# 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 random
|
||||
import copy
|
||||
|
||||
import pytest
|
||||
import numpy as np
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def channels_tab_controller(dummy_controller):
|
||||
from navigate.controller.sub_controllers.channels_tab import (
|
||||
ChannelsTabController,
|
||||
)
|
||||
|
||||
return ChannelsTabController(
|
||||
dummy_controller.view.settings.channels_tab, dummy_controller
|
||||
)
|
||||
|
||||
|
||||
def test_update_z_steps(channels_tab_controller):
|
||||
# Calculate params
|
||||
z_start, f_start = random.randint(1, 1000), random.randint(1, 1000)
|
||||
z_end, f_end = random.randint(1, 1000), random.randint(1, 1000)
|
||||
if z_end < z_start:
|
||||
# Sort so we are always going low to high
|
||||
tmp = z_start
|
||||
tmp_f = f_start
|
||||
z_start = z_end
|
||||
f_start = f_end
|
||||
z_end = tmp
|
||||
f_end = tmp_f
|
||||
step_size = max(1, min(random.randint(1, 10), (z_end - z_start) // 2))
|
||||
|
||||
# Set params
|
||||
channels_tab_controller.microscope_state_dict = (
|
||||
channels_tab_controller.parent_controller.configuration["experiment"][
|
||||
"MicroscopeState"
|
||||
]
|
||||
)
|
||||
channels_tab_controller.in_initialization = False
|
||||
channels_tab_controller.stack_acq_vals["start_position"].set(z_start)
|
||||
channels_tab_controller.stack_acq_vals["start_focus"].set(f_start)
|
||||
channels_tab_controller.stack_acq_vals["end_position"].set(z_end)
|
||||
channels_tab_controller.stack_acq_vals["end_focus"].set(f_end)
|
||||
channels_tab_controller.stack_acq_vals["step_size"].set(step_size)
|
||||
|
||||
# Run
|
||||
channels_tab_controller.update_z_steps()
|
||||
|
||||
# Verify
|
||||
number_z_steps = int(np.ceil(np.abs((z_start - z_end) / step_size)))
|
||||
assert (
|
||||
int(channels_tab_controller.stack_acq_vals["number_z_steps"].get())
|
||||
== number_z_steps
|
||||
)
|
||||
|
||||
# test flip_z is True
|
||||
microscope_name = (
|
||||
channels_tab_controller.parent_controller.configuration_controller.microscope_name
|
||||
)
|
||||
stage_config = channels_tab_controller.parent_controller.configuration[
|
||||
"configuration"
|
||||
]["microscopes"][microscope_name]["stage"]
|
||||
stage_config["flip_z"] = True
|
||||
channels_tab_controller.z_origin = (z_start + z_end) / 2
|
||||
channels_tab_controller.stack_acq_vals["start_position"].set(z_end)
|
||||
channels_tab_controller.stack_acq_vals["start_focus"].set(f_end)
|
||||
channels_tab_controller.stack_acq_vals["end_position"].set(z_start)
|
||||
channels_tab_controller.stack_acq_vals["end_focus"].set(f_start)
|
||||
channels_tab_controller.update_z_steps()
|
||||
assert channels_tab_controller.stack_acq_vals["step_size"].get() == step_size
|
||||
assert channels_tab_controller.microscope_state_dict["step_size"] == -1 * step_size
|
||||
assert (
|
||||
channels_tab_controller.stack_acq_vals["number_z_steps"].get() == number_z_steps
|
||||
)
|
||||
stage_config["flip_z"] = False
|
||||
|
||||
|
||||
def test_update_start_position(channels_tab_controller):
|
||||
z, f = random.randint(0, 1000), random.randint(0, 1000)
|
||||
channels_tab_controller.parent_controller.configuration["experiment"][
|
||||
"StageParameters"
|
||||
]["z"] = z
|
||||
channels_tab_controller.parent_controller.configuration["experiment"][
|
||||
"StageParameters"
|
||||
]["f"] = f
|
||||
|
||||
channels_tab_controller.update_start_position()
|
||||
|
||||
assert channels_tab_controller.z_origin == z
|
||||
assert channels_tab_controller.focus_origin == f
|
||||
assert int(channels_tab_controller.stack_acq_vals["start_position"].get()) == 0
|
||||
assert int(channels_tab_controller.stack_acq_vals["start_focus"].get()) == 0
|
||||
|
||||
# test flip_z is True
|
||||
microscope_name = (
|
||||
channels_tab_controller.parent_controller.configuration_controller.microscope_name
|
||||
)
|
||||
stage_config = channels_tab_controller.parent_controller.configuration[
|
||||
"configuration"
|
||||
]["microscopes"][microscope_name]["stage"]
|
||||
stage_config["flip_z"] = True
|
||||
channels_tab_controller.update_start_position()
|
||||
|
||||
assert channels_tab_controller.z_origin == z
|
||||
assert channels_tab_controller.focus_origin == f
|
||||
assert int(channels_tab_controller.stack_acq_vals["end_position"].get()) == 0
|
||||
assert int(channels_tab_controller.stack_acq_vals["end_focus"].get()) == 0
|
||||
stage_config["flip_z"] = False
|
||||
|
||||
|
||||
def test_update_end_position(channels_tab_controller):
|
||||
configuration = channels_tab_controller.parent_controller.configuration
|
||||
|
||||
# Initialize
|
||||
z, f = random.randint(0, 1000), random.randint(0, 1000)
|
||||
z_shift, f_shift = random.randint(1, 500), random.randint(1, 500)
|
||||
configuration["experiment"]["StageParameters"]["z"] = z + z_shift
|
||||
configuration["experiment"]["StageParameters"]["f"] = f + f_shift
|
||||
|
||||
print(f"z: {z} z-shift: {z_shift} f: {f} f-shift: {f_shift}")
|
||||
print(f'z-dict: {configuration["experiment"]["StageParameters"]["z"]}')
|
||||
print(f'f-dict: {configuration["experiment"]["StageParameters"]["f"]}')
|
||||
|
||||
# Step backwards and record results
|
||||
channels_tab_controller.z_origin = z - z_shift
|
||||
channels_tab_controller.focus_origin = f - f_shift
|
||||
channels_tab_controller.update_end_position()
|
||||
z_origin_minus = copy.deepcopy(channels_tab_controller.z_origin)
|
||||
f_origin_minus = copy.deepcopy(channels_tab_controller.focus_origin)
|
||||
start_position_minus = copy.deepcopy(
|
||||
channels_tab_controller.stack_acq_vals["start_position"].get()
|
||||
)
|
||||
end_position_minus = copy.deepcopy(
|
||||
channels_tab_controller.stack_acq_vals["end_position"].get()
|
||||
)
|
||||
start_focus_minus = copy.deepcopy(
|
||||
channels_tab_controller.stack_acq_vals["start_focus"].get()
|
||||
)
|
||||
end_focus_minus = copy.deepcopy(
|
||||
channels_tab_controller.stack_acq_vals["end_focus"].get()
|
||||
)
|
||||
|
||||
print("back")
|
||||
print(f"z: {z} z-shift: {z_shift} f: {f} f-shift: {f_shift}")
|
||||
print(f'z-dict: {configuration["experiment"]["StageParameters"]["z"]}')
|
||||
print(f'f-dict: {configuration["experiment"]["StageParameters"]["f"]}')
|
||||
|
||||
# Step forward
|
||||
configuration["experiment"]["StageParameters"]["z"] = z - z_shift
|
||||
configuration["experiment"]["StageParameters"]["f"] = f - f_shift
|
||||
channels_tab_controller.z_origin = z + z_shift
|
||||
channels_tab_controller.focus_origin = f + f_shift
|
||||
channels_tab_controller.update_end_position()
|
||||
|
||||
print("forward")
|
||||
print(f"z: {z} z-shift: {z_shift} f: {f} f-shift: {f_shift}")
|
||||
print(f'z-dict: {configuration["experiment"]["StageParameters"]["z"]}')
|
||||
print(f'f-dict: {configuration["experiment"]["StageParameters"]["f"]}')
|
||||
|
||||
# Ensure we achieve the same origin
|
||||
assert channels_tab_controller.z_origin == z_origin_minus
|
||||
assert channels_tab_controller.focus_origin == f_origin_minus
|
||||
assert (
|
||||
channels_tab_controller.stack_acq_vals["start_position"].get()
|
||||
== start_position_minus
|
||||
)
|
||||
assert (
|
||||
channels_tab_controller.stack_acq_vals["end_position"].get()
|
||||
== end_position_minus
|
||||
)
|
||||
assert (
|
||||
channels_tab_controller.stack_acq_vals["start_focus"].get() == start_focus_minus
|
||||
)
|
||||
assert channels_tab_controller.stack_acq_vals["end_focus"].get() == end_focus_minus
|
||||
|
||||
# test flip_z is True
|
||||
microscope_name = (
|
||||
channels_tab_controller.parent_controller.configuration_controller.microscope_name
|
||||
)
|
||||
stage_config = channels_tab_controller.parent_controller.configuration[
|
||||
"configuration"
|
||||
]["microscopes"][microscope_name]["stage"]
|
||||
stage_config["flip_z"] = True
|
||||
# forward
|
||||
channels_tab_controller.z_origin = z
|
||||
channels_tab_controller.focus_origin = f
|
||||
configuration["experiment"]["StageParameters"]["z"] = z - 2 * z_shift
|
||||
configuration["experiment"]["StageParameters"]["f"] = f - 2 * f_shift
|
||||
channels_tab_controller.update_end_position()
|
||||
assert channels_tab_controller.z_origin == z - z_shift
|
||||
assert channels_tab_controller.focus_origin == f - f_shift
|
||||
assert channels_tab_controller.stack_acq_vals["start_position"].get() == z_shift
|
||||
assert channels_tab_controller.stack_acq_vals["end_position"].get() == -1 * z_shift
|
||||
assert channels_tab_controller.stack_acq_vals["start_focus"].get() == f_shift
|
||||
assert channels_tab_controller.stack_acq_vals["end_focus"].get() == -1 * f_shift
|
||||
|
||||
# backward
|
||||
channels_tab_controller.z_origin = z
|
||||
channels_tab_controller.focus_origin = f
|
||||
configuration["experiment"]["StageParameters"]["z"] = z + 2 * z_shift
|
||||
configuration["experiment"]["StageParameters"]["f"] = f + 2 * f_shift
|
||||
channels_tab_controller.update_end_position()
|
||||
assert channels_tab_controller.z_origin == z + z_shift
|
||||
assert channels_tab_controller.focus_origin == f + f_shift
|
||||
assert channels_tab_controller.stack_acq_vals["start_position"].get() == z_shift
|
||||
assert channels_tab_controller.stack_acq_vals["end_position"].get() == -1 * z_shift
|
||||
assert channels_tab_controller.stack_acq_vals["start_focus"].get() == f_shift
|
||||
assert channels_tab_controller.stack_acq_vals["end_focus"].get() == -1 * f_shift
|
||||
stage_config["flip_z"] = False
|
||||
|
||||
|
||||
def test_update_start_update_end_position(channels_tab_controller):
|
||||
configuration = channels_tab_controller.parent_controller.configuration
|
||||
channels_tab_controller.microscope_state_dict = configuration["experiment"][
|
||||
"MicroscopeState"
|
||||
]
|
||||
channels_tab_controller.in_initialization = False
|
||||
|
||||
# Initialize
|
||||
z, f = random.randint(0, 1000), random.randint(0, 1000)
|
||||
z_shift, f_shift = random.randint(1, 500), random.randint(1, 500)
|
||||
configuration["experiment"]["StageParameters"]["z"] = z - z_shift
|
||||
configuration["experiment"]["StageParameters"]["f"] = f - f_shift
|
||||
channels_tab_controller.update_start_position()
|
||||
|
||||
print(f"z: {z} z-shift: {z_shift} f: {f} f-shift: {f_shift}")
|
||||
print(f'z-dict: {configuration["experiment"]["StageParameters"]["z"]}')
|
||||
print(f'f-dict: {configuration["experiment"]["StageParameters"]["f"]}')
|
||||
|
||||
# Step forward and record results
|
||||
configuration["experiment"]["StageParameters"]["z"] = z + z_shift
|
||||
configuration["experiment"]["StageParameters"]["f"] = f + f_shift
|
||||
channels_tab_controller.update_end_position()
|
||||
z_origin_minus = copy.deepcopy(channels_tab_controller.z_origin)
|
||||
f_origin_minus = copy.deepcopy(channels_tab_controller.focus_origin)
|
||||
start_position_minus = copy.deepcopy(
|
||||
channels_tab_controller.stack_acq_vals["start_position"].get()
|
||||
)
|
||||
end_position_minus = copy.deepcopy(
|
||||
channels_tab_controller.stack_acq_vals["end_position"].get()
|
||||
)
|
||||
start_focus_minus = copy.deepcopy(
|
||||
channels_tab_controller.stack_acq_vals["start_focus"].get()
|
||||
)
|
||||
end_focus_minus = copy.deepcopy(
|
||||
channels_tab_controller.stack_acq_vals["end_focus"].get()
|
||||
)
|
||||
|
||||
print("back")
|
||||
print(f"z: {z} z-shift: {z_shift} f: {f} f-shift: {f_shift}")
|
||||
print(f'z-dict: {configuration["experiment"]["StageParameters"]["z"]}')
|
||||
print(f'f-dict: {configuration["experiment"]["StageParameters"]["f"]}')
|
||||
|
||||
channels_tab_controller.update_start_position()
|
||||
|
||||
# Step back
|
||||
configuration["experiment"]["StageParameters"]["z"] = z - z_shift
|
||||
configuration["experiment"]["StageParameters"]["f"] = f - f_shift
|
||||
channels_tab_controller.update_end_position()
|
||||
|
||||
print("forward")
|
||||
print(f"z: {z} z-shift: {z_shift} f: {f} f-shift: {f_shift}")
|
||||
print(f'z-dict: {configuration["experiment"]["StageParameters"]["z"]}')
|
||||
print(f'f-dict: {configuration["experiment"]["StageParameters"]["f"]}')
|
||||
|
||||
# Ensure we achieve the same origin
|
||||
assert channels_tab_controller.z_origin == z_origin_minus
|
||||
assert channels_tab_controller.focus_origin == f_origin_minus
|
||||
assert (
|
||||
channels_tab_controller.stack_acq_vals["start_position"].get()
|
||||
== start_position_minus
|
||||
)
|
||||
assert (
|
||||
channels_tab_controller.stack_acq_vals["end_position"].get()
|
||||
== end_position_minus
|
||||
)
|
||||
assert (
|
||||
channels_tab_controller.stack_acq_vals["start_focus"].get() == start_focus_minus
|
||||
)
|
||||
assert channels_tab_controller.stack_acq_vals["end_focus"].get() == end_focus_minus
|
||||
|
||||
# test flip_z is true
|
||||
microscope_name = (
|
||||
channels_tab_controller.parent_controller.configuration_controller.microscope_name
|
||||
)
|
||||
stage_config = channels_tab_controller.parent_controller.configuration[
|
||||
"configuration"
|
||||
]["microscopes"][microscope_name]["stage"]
|
||||
stage_config["flip_z"] = True
|
||||
configuration = channels_tab_controller.parent_controller.configuration
|
||||
z, f = random.randint(0, 1000), random.randint(0, 1000)
|
||||
z_shift, f_shift = random.randint(1, 500), random.randint(1, 500)
|
||||
configuration["experiment"]["StageParameters"]["z"] = z - z_shift
|
||||
configuration["experiment"]["StageParameters"]["f"] = f - f_shift
|
||||
channels_tab_controller.update_start_position()
|
||||
configuration["experiment"]["StageParameters"]["z"] = z + z_shift
|
||||
configuration["experiment"]["StageParameters"]["f"] = f + f_shift
|
||||
channels_tab_controller.update_end_position()
|
||||
|
||||
assert channels_tab_controller.z_origin == z
|
||||
assert channels_tab_controller.focus_origin == f
|
||||
assert channels_tab_controller.stack_acq_vals["start_position"].get() == z_shift
|
||||
assert channels_tab_controller.stack_acq_vals["end_position"].get() == -1 * z_shift
|
||||
assert channels_tab_controller.stack_acq_vals["start_focus"].get() == f_shift
|
||||
assert channels_tab_controller.stack_acq_vals["end_focus"].get() == -1 * f_shift
|
||||
|
||||
assert configuration["experiment"]["MicroscopeState"]["start_position"] == z_shift
|
||||
assert (
|
||||
configuration["experiment"]["MicroscopeState"]["end_position"] == -1 * z_shift
|
||||
)
|
||||
assert configuration["experiment"]["MicroscopeState"]["abs_z_start"] == z - z_shift
|
||||
assert configuration["experiment"]["MicroscopeState"]["abs_z_end"] == z + z_shift
|
||||
assert configuration["experiment"]["MicroscopeState"]["start_focus"] == f_shift
|
||||
assert configuration["experiment"]["MicroscopeState"]["end_focus"] == -1 * f_shift
|
||||
|
||||
configuration["experiment"]["StageParameters"]["z"] = z + z_shift
|
||||
configuration["experiment"]["StageParameters"]["f"] = f + f_shift
|
||||
channels_tab_controller.update_start_position()
|
||||
configuration["experiment"]["StageParameters"]["z"] = z - z_shift
|
||||
configuration["experiment"]["StageParameters"]["f"] = f - f_shift
|
||||
channels_tab_controller.update_end_position()
|
||||
|
||||
assert channels_tab_controller.z_origin == z
|
||||
assert channels_tab_controller.focus_origin == f
|
||||
assert channels_tab_controller.stack_acq_vals["start_position"].get() == z_shift
|
||||
assert channels_tab_controller.stack_acq_vals["end_position"].get() == -1 * z_shift
|
||||
assert channels_tab_controller.stack_acq_vals["start_focus"].get() == f_shift
|
||||
assert channels_tab_controller.stack_acq_vals["end_focus"].get() == -1 * f_shift
|
||||
|
||||
assert configuration["experiment"]["MicroscopeState"]["start_position"] == z_shift
|
||||
assert (
|
||||
configuration["experiment"]["MicroscopeState"]["end_position"] == -1 * z_shift
|
||||
)
|
||||
assert configuration["experiment"]["MicroscopeState"]["abs_z_start"] == z - z_shift
|
||||
assert configuration["experiment"]["MicroscopeState"]["abs_z_end"] == z + z_shift
|
||||
assert configuration["experiment"]["MicroscopeState"]["start_focus"] == f_shift
|
||||
assert configuration["experiment"]["MicroscopeState"]["end_focus"] == -1 * f_shift
|
||||
stage_config["flip_z"] = False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_multiposition", [True, False])
|
||||
def test_toggle_multiposition(channels_tab_controller, is_multiposition):
|
||||
channels_tab_controller.populate_experiment_values()
|
||||
channels_tab_controller.is_multiposition_val.set(is_multiposition)
|
||||
with patch.object(channels_tab_controller, "update_timepoint_setting") as uts:
|
||||
channels_tab_controller.toggle_multiposition()
|
||||
assert channels_tab_controller.is_multiposition == is_multiposition
|
||||
assert (
|
||||
channels_tab_controller.microscope_state_dict["is_multiposition"]
|
||||
== is_multiposition
|
||||
)
|
||||
uts.assert_called()
|
||||
259
test/controller/sub_controllers/test_menus.py
Normal file
259
test/controller/sub_controllers/test_menus.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 unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
import tkinter as tk
|
||||
|
||||
# Third Party Imports
|
||||
import pytest
|
||||
|
||||
# Local Imports
|
||||
from navigate.controller.sub_controllers.menus import (
|
||||
MenuController,
|
||||
FakeEvent,
|
||||
)
|
||||
|
||||
|
||||
class TestFakeEvent(unittest.TestCase):
|
||||
def test_fake_event_creation(self):
|
||||
fake_event = FakeEvent(char="a", keysym="A")
|
||||
self.assertEqual(fake_event.char, "a")
|
||||
self.assertEqual(fake_event.keysym, "A")
|
||||
self.assertEqual(fake_event.state, 0)
|
||||
|
||||
|
||||
class TestStageMovement(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Create a mock parent controller and view
|
||||
self.root = tk.Tk()
|
||||
self.parent_controller = MagicMock()
|
||||
self.parent_controller.stage_controller = MagicMock()
|
||||
self.view = MagicMock()
|
||||
self.view.root = self.root
|
||||
|
||||
# Initialize the menu controller
|
||||
self.mc = MenuController(self.view, self.parent_controller)
|
||||
|
||||
# Mock the histogram configuration entry.
|
||||
self.parent_controller.configuration["gui"]["histogram"] = MagicMock()
|
||||
self.parent_controller.configuration["gui"]["histogram"].get.return_value = True
|
||||
|
||||
def tearDown(self):
|
||||
self.root.destroy()
|
||||
|
||||
def test_initialize_menus(self):
|
||||
self.mc.initialize_menus()
|
||||
|
||||
def test_stage_movement_with_ttk_entry(self):
|
||||
self.mc.parent_controller.view.focus_get.return_value = MagicMock(
|
||||
widgetName="ttk::entry"
|
||||
)
|
||||
self.mc.stage_movement("a")
|
||||
self.mc.parent_controller.stage_controller.stage_key_press.assert_not_called()
|
||||
|
||||
def test_stage_movement_with_ttk_combobox(self):
|
||||
self.mc.parent_controller.view.focus_get.return_value = MagicMock(
|
||||
widgetName="ttk::combobox"
|
||||
)
|
||||
self.mc.stage_movement("a")
|
||||
self.mc.parent_controller.stage_controller.stage_key_press.assert_not_called()
|
||||
|
||||
def test_stage_movement_with_other_widget(self):
|
||||
self.mc.parent_controller.view.focus_get.return_value = MagicMock(
|
||||
widgetName="other_widget"
|
||||
)
|
||||
self.mc.stage_movement("a")
|
||||
self.mc.parent_controller.stage_controller.stage_key_press.assert_called_with(
|
||||
self.mc.fake_event
|
||||
)
|
||||
|
||||
def test_stage_movement_with_key_error(self):
|
||||
self.mc.parent_controller.view.focus_get.side_effect = KeyError
|
||||
# Test that no exception is raised
|
||||
try:
|
||||
self.mc.stage_movement("a")
|
||||
except KeyError:
|
||||
self.fail("stage_movement() raised KeyError unexpectedly!")
|
||||
|
||||
def test_stage_movement_with_no_focus(self):
|
||||
self.mc.parent_controller.view.focus_get.return_value = None
|
||||
self.mc.stage_movement("a")
|
||||
self.mc.parent_controller.stage_controller.stage_key_press.assert_called_with(
|
||||
self.mc.fake_event
|
||||
)
|
||||
|
||||
|
||||
class TestMenuController(unittest.TestCase):
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_class(self, dummy_controller):
|
||||
c = dummy_controller
|
||||
v = dummy_controller.view
|
||||
self.menu_controller = MenuController(v, c)
|
||||
|
||||
def test_attributes(self):
|
||||
methods = dir(MenuController)
|
||||
desired_methods = [
|
||||
"initialize_menus",
|
||||
"populate_menu",
|
||||
"new_experiment",
|
||||
"load_experiment",
|
||||
"save_experiment",
|
||||
"load_images",
|
||||
"popup_camera_map_setting",
|
||||
"popup_ilastik_setting",
|
||||
"popup_help",
|
||||
"toggle_stage_limits",
|
||||
"popup_autofocus_setting",
|
||||
"popup_waveform_setting",
|
||||
"popup_microscope_setting",
|
||||
"toggle_save",
|
||||
"acquire_data",
|
||||
"not_implemented",
|
||||
"stage_movement",
|
||||
"switch_tabs",
|
||||
]
|
||||
|
||||
for method in desired_methods:
|
||||
assert method in methods
|
||||
|
||||
def test_popup_camera_map_setting(self):
|
||||
assert (
|
||||
hasattr(
|
||||
self.menu_controller.parent_controller, "camera_map_popup_controller"
|
||||
)
|
||||
is False
|
||||
)
|
||||
self.menu_controller.popup_camera_map_setting()
|
||||
assert (
|
||||
hasattr(
|
||||
self.menu_controller.parent_controller, "camera_map_popup_controller"
|
||||
)
|
||||
is True
|
||||
)
|
||||
|
||||
def test_autofocus_settings(self):
|
||||
assert (
|
||||
hasattr(self.menu_controller.parent_controller, "af_popup_controller")
|
||||
is False
|
||||
)
|
||||
self.menu_controller.popup_autofocus_setting()
|
||||
assert (
|
||||
hasattr(self.menu_controller.parent_controller, "af_popup_controller")
|
||||
is True
|
||||
)
|
||||
|
||||
def test_popup_waveform_setting(self):
|
||||
# TODO: Incomplete.
|
||||
assert (
|
||||
hasattr(self.menu_controller.parent_controller, "waveform_popup_controller")
|
||||
is False
|
||||
)
|
||||
|
||||
def test_popup_microscope_setting(self):
|
||||
# TODO: Incomplete. DummyController has no attribute 'model'
|
||||
assert (
|
||||
hasattr(
|
||||
self.menu_controller.parent_controller, "microscope_popup_controller"
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
def test_toggle_save(self):
|
||||
class MockWidget:
|
||||
def __int__(self):
|
||||
self.value = False
|
||||
|
||||
def set(self, value):
|
||||
self.value = value
|
||||
|
||||
def get(self):
|
||||
return self.value
|
||||
|
||||
channel_tab_controller = MagicMock()
|
||||
self.menu_controller.parent_controller.channels_tab_controller = (
|
||||
channel_tab_controller
|
||||
)
|
||||
channel_tab_controller.timepoint_vals = {"is_save": MockWidget()}
|
||||
temp = self.menu_controller.view.settings.channels_tab.stack_timepoint_frame
|
||||
temp.save_data.get = MagicMock(return_value=False)
|
||||
self.menu_controller.toggle_save()
|
||||
assert channel_tab_controller.timepoint_vals["is_save"].get() is True
|
||||
|
||||
temp = self.menu_controller.view.settings.channels_tab.stack_timepoint_frame
|
||||
temp.save_data.get = MagicMock(return_value=True)
|
||||
self.menu_controller.toggle_save()
|
||||
assert channel_tab_controller.timepoint_vals["is_save"].get() is False
|
||||
|
||||
def test_stage_movement(self):
|
||||
# TODO: DummyController does not have a stage controller.
|
||||
pass
|
||||
|
||||
def test_switch_tabs(self):
|
||||
for i in range(1, 4):
|
||||
self.menu_controller.switch_tabs(window="left", tab=i)
|
||||
assert (
|
||||
self.menu_controller.parent_controller.view.settings.index("current")
|
||||
== i - 1
|
||||
)
|
||||
|
||||
@patch("src.navigate.controller.sub_controllers.menus.platform.system")
|
||||
@patch("src.navigate.controller.sub_controllers.menus.subprocess.check_call")
|
||||
def test_open_folder(self, mock_check_call, mock_system):
|
||||
mock_system.return_value = "Darwin"
|
||||
self.menu_controller.open_folder("test_path")
|
||||
mock_check_call.assert_called_once_with(["open", "--", "test_path"])
|
||||
|
||||
mock_check_call.reset_mock()
|
||||
mock_system.return_value = "Windows"
|
||||
self.menu_controller.open_folder("test_path")
|
||||
mock_check_call.assert_called_once_with(["explorer", "test_path"])
|
||||
|
||||
mock_check_call.reset_mock()
|
||||
mock_system.return_value = "Linux"
|
||||
self.menu_controller.open_folder("test_path")
|
||||
self.assertEqual(mock_check_call.call_count, 0)
|
||||
|
||||
@patch("src.navigate.controller.sub_controllers.menus.os.path.join")
|
||||
def test_open_log_files(self, mock_join):
|
||||
with patch.object(self.menu_controller, "open_folder") as mock_open_folder:
|
||||
mock_join.return_value = "joined_path"
|
||||
self.menu_controller.open_log_files()
|
||||
mock_open_folder.assert_called_once_with("joined_path")
|
||||
|
||||
@patch("src.navigate.controller.sub_controllers.menus.os.path.join")
|
||||
def test_open_configuration_files(self, mock_join):
|
||||
with patch.object(self.menu_controller, "open_folder") as mock_open_folder:
|
||||
mock_join.return_value = "joined_path"
|
||||
self.menu_controller.open_configuration_files()
|
||||
mock_open_folder.assert_called_once_with("joined_path")
|
||||
171
test/controller/sub_controllers/test_multiposition.py
Normal file
171
test/controller/sub_controllers/test_multiposition.py
Normal file
@@ -0,0 +1,171 @@
|
||||
# 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 unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
import pandas as pd
|
||||
|
||||
# Local application imports
|
||||
from navigate.controller.sub_controllers.multiposition import MultiPositionController
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def multiposition_controller(dummy_controller):
|
||||
# Create a copy/clone of the dummy_controller to avoid side effects
|
||||
isolated_controller = MagicMock()
|
||||
isolated_controller.configuration = dummy_controller.configuration
|
||||
|
||||
# Create a mock pt attribute for the multiposition_tab
|
||||
isolated_controller.view.settings.multiposition_tab.pt = MagicMock()
|
||||
isolated_controller.view.settings.multiposition_tab.pt.model = MagicMock()
|
||||
isolated_controller.view.settings.multiposition_tab.pt.model.df = pd.DataFrame()
|
||||
|
||||
# Add other required mock attributes and methods
|
||||
isolated_controller.view.settings.multiposition_tab.pt.redraw = MagicMock()
|
||||
isolated_controller.view.settings.multiposition_tab.pt.tableChanged = MagicMock()
|
||||
isolated_controller.view.settings.multiposition_tab.pt.resetColors = MagicMock()
|
||||
isolated_controller.view.settings.multiposition_tab.pt.update_rowcolors = (
|
||||
MagicMock()
|
||||
)
|
||||
|
||||
# Mock the master and tiling buttons
|
||||
isolated_controller.view.settings.multiposition_tab.master = MagicMock()
|
||||
isolated_controller.view.settings.multiposition_tab.master.tiling_buttons = (
|
||||
MagicMock()
|
||||
)
|
||||
isolated_controller.view.settings.multiposition_tab.master.tiling_buttons.buttons = {
|
||||
"tiling": MagicMock(),
|
||||
"save_data": MagicMock(),
|
||||
"load_data": MagicMock(),
|
||||
"eliminate_tiles": MagicMock(),
|
||||
}
|
||||
|
||||
# This is the important part - configure the stage axes
|
||||
isolated_controller.configuration_controller = MagicMock()
|
||||
isolated_controller.configuration_controller.stage_axes = [
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"theta",
|
||||
"f",
|
||||
]
|
||||
|
||||
return MultiPositionController(
|
||||
isolated_controller.view.settings.multiposition_tab, isolated_controller
|
||||
)
|
||||
|
||||
|
||||
@patch("navigate.controller.sub_controllers.multiposition.filedialog.askopenfilenames")
|
||||
@patch("navigate.controller.sub_controllers.multiposition.yaml.safe_load")
|
||||
@patch("builtins.open", new_callable=unittest.mock.mock_open, read_data="dummy content")
|
||||
def test_load_positions_yaml(
|
||||
mock_file, mock_safe_load, mock_askopen, multiposition_controller
|
||||
):
|
||||
"""Test loading positions from YAML"""
|
||||
controller = multiposition_controller
|
||||
table = controller.table
|
||||
|
||||
mock_askopen.return_value = ("dummy_file.yml",)
|
||||
mock_safe_load.return_value = [
|
||||
["X", "Y", "Z", "THETA", "F"],
|
||||
[0, 0, 0, 0, 0],
|
||||
[100, 200, 300, 400, 500],
|
||||
]
|
||||
|
||||
controller.load_positions()
|
||||
|
||||
mock_file.assert_called_once_with("dummy_file.yml", "r")
|
||||
|
||||
expected = pd.DataFrame(
|
||||
[[0, 0, 0, 0, 0], [100, 200, 300, 400, 500]],
|
||||
columns=["X", "Y", "Z", "THETA", "F"],
|
||||
)
|
||||
pd.testing.assert_frame_equal(table.model.df, expected)
|
||||
|
||||
|
||||
@patch("navigate.controller.sub_controllers.multiposition.filedialog.askopenfilenames")
|
||||
@patch("navigate.controller.sub_controllers.multiposition.pd.read_csv")
|
||||
def test_load_positions_csv(mock_read_csv, mock_askopen, multiposition_controller):
|
||||
"""Test loading positions from CSV"""
|
||||
controller = multiposition_controller
|
||||
table = controller.table
|
||||
|
||||
mock_askopen.return_value = ("dummy_file.csv",)
|
||||
mock_read_csv.return_value = pd.DataFrame(
|
||||
{"X": [1, 2], "Y": [3, 4], "Z": [5, 6], "THETA": [0, 0], "F": [0, 0]}
|
||||
)
|
||||
|
||||
controller.load_positions()
|
||||
|
||||
expected = pd.DataFrame(
|
||||
{"X": [1, 2], "Y": [3, 4], "Z": [5, 6], "THETA": [0, 0], "F": [0, 0]}
|
||||
)
|
||||
pd.testing.assert_frame_equal(table.model.df, expected)
|
||||
|
||||
|
||||
@patch("navigate.controller.sub_controllers.multiposition.filedialog.asksaveasfilename")
|
||||
@patch("navigate.controller.sub_controllers.multiposition.save_yaml_file")
|
||||
def test_export_positions_yaml(mock_save_yaml, mock_asksave, multiposition_controller):
|
||||
"""Test exporting positions to YAML"""
|
||||
controller = multiposition_controller
|
||||
table = controller.table
|
||||
|
||||
table.model.df = pd.DataFrame(
|
||||
{"X": [1, 2], "Y": [3, 4], "Z": [5, 6], "THETA": [0, 0], "F": [0, 0]}
|
||||
)
|
||||
mock_asksave.return_value = "/tmp/output.yml"
|
||||
|
||||
controller.export_positions()
|
||||
mock_save_yaml.assert_called_once()
|
||||
|
||||
|
||||
@patch("navigate.controller.sub_controllers.multiposition.filedialog.asksaveasfilename")
|
||||
def test_export_positions_csv(mock_asksave, multiposition_controller):
|
||||
"""Test exporting positions to CSV"""
|
||||
controller = multiposition_controller
|
||||
table = controller.table
|
||||
|
||||
df = pd.DataFrame(
|
||||
{"X": [1, 2], "Y": [3, 4], "Z": [5, 6], "THETA": [0, 0], "F": [0, 0]}
|
||||
)
|
||||
table.model.df = df
|
||||
table.model.df.to_csv = MagicMock()
|
||||
|
||||
mock_asksave.return_value = "/tmp/output.csv"
|
||||
|
||||
controller.export_positions()
|
||||
table.model.df.to_csv.assert_called_once_with("/tmp/output.csv", index=False)
|
||||
376
test/controller/sub_controllers/test_stages.py
Normal file
376
test/controller/sub_controllers/test_stages.py
Normal file
@@ -0,0 +1,376 @@
|
||||
# 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
|
||||
from unittest.mock import MagicMock, call
|
||||
import numpy as np
|
||||
|
||||
AXES = ["x", "y", "z", "theta", "f"]
|
||||
CAXES = ["xy", "z", "theta", "f"]
|
||||
|
||||
|
||||
def pos_dict(v, axes=AXES):
|
||||
return {k: v for k in axes}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stage_controller(dummy_controller):
|
||||
from navigate.controller.sub_controllers.stages import StageController
|
||||
|
||||
dummy_controller.camera_view_controller = MagicMock()
|
||||
|
||||
stage_controller = StageController(
|
||||
dummy_controller.view.settings.stage_control_tab,
|
||||
dummy_controller,
|
||||
)
|
||||
|
||||
dummy_controller.view.settings.stage_control_tab.focus_get = MagicMock(
|
||||
return_value=True
|
||||
)
|
||||
|
||||
return stage_controller
|
||||
|
||||
|
||||
# test before set position variables to MagicMock()
|
||||
def test_set_position(stage_controller):
|
||||
|
||||
widgets = stage_controller.view.get_widgets()
|
||||
vals = {}
|
||||
for axis in AXES:
|
||||
widgets[axis].widget.trigger_focusout_validation = MagicMock()
|
||||
vals[axis] = np.random.randint(0, 9)
|
||||
|
||||
stage_controller.view.get_widgets = MagicMock(return_value=widgets)
|
||||
stage_controller.show_verbose_info = MagicMock()
|
||||
position = {
|
||||
"x": np.random.random(),
|
||||
"y": np.random.random(),
|
||||
"z": np.random.random(),
|
||||
}
|
||||
stage_controller.set_position(position)
|
||||
for axis in position.keys():
|
||||
assert float(stage_controller.widget_vals[axis].get()) == position[axis]
|
||||
assert widgets[axis].widget.trigger_focusout_validation.called
|
||||
assert stage_controller.stage_setting_dict[axis] == position.get(axis, 0)
|
||||
stage_controller.show_verbose_info.assert_has_calls(
|
||||
[call("Stage position changed"), call("Set stage position")]
|
||||
)
|
||||
|
||||
|
||||
def test_set_position_silent(stage_controller):
|
||||
|
||||
widgets = stage_controller.view.get_widgets()
|
||||
vals = {}
|
||||
for axis in AXES:
|
||||
widgets[axis].widget.trigger_focusout_validation = MagicMock()
|
||||
vals[axis] = np.random.randint(0, 9)
|
||||
|
||||
stage_controller.view.get_widgets = MagicMock(return_value=widgets)
|
||||
stage_controller.show_verbose_info = MagicMock()
|
||||
position = {
|
||||
"x": np.random.random(),
|
||||
"y": np.random.random(),
|
||||
"z": np.random.random(),
|
||||
}
|
||||
stage_controller.set_position_silent(position)
|
||||
for axis in position.keys():
|
||||
assert float(stage_controller.widget_vals[axis].get()) == position[axis]
|
||||
widgets[axis].widget.trigger_focusout_validation.assert_called_once()
|
||||
assert stage_controller.stage_setting_dict[axis] == position.get(axis, 0)
|
||||
stage_controller.show_verbose_info.assert_has_calls([call("Set stage position")])
|
||||
assert (
|
||||
call("Stage position changed")
|
||||
not in stage_controller.show_verbose_info.mock_calls
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"flip_x, flip_y",
|
||||
[(False, False), (True, False), (True, True), (False, True), (True, True)],
|
||||
)
|
||||
def test_stage_key_press(stage_controller, flip_x, flip_y):
|
||||
microscope_name = (
|
||||
stage_controller.parent_controller.configuration_controller.microscope_name
|
||||
)
|
||||
stage_config = stage_controller.parent_controller.configuration["configuration"][
|
||||
"microscopes"
|
||||
][microscope_name]["stage"]
|
||||
stage_config["flip_x"] = flip_x
|
||||
stage_config["flip_y"] = flip_y
|
||||
stage_controller.initialize()
|
||||
x = round(np.random.random(), 1)
|
||||
y = round(np.random.random(), 1)
|
||||
increment = round(np.random.random() + 1, 1)
|
||||
stage_controller.widget_vals["xy_step"].get = MagicMock(return_value=increment)
|
||||
stage_controller.widget_vals["x"].get = MagicMock(return_value=x)
|
||||
stage_controller.widget_vals["x"].set = MagicMock()
|
||||
stage_controller.widget_vals["y"].get = MagicMock(return_value=y)
|
||||
stage_controller.widget_vals["y"].set = MagicMock()
|
||||
event = MagicMock()
|
||||
|
||||
axes_map = {"w": "y", "a": "x", "s": "y", "d": "x"}
|
||||
|
||||
for char, xs, ys in zip(
|
||||
["w", "a", "s", "d"],
|
||||
[0, -increment, 0, increment],
|
||||
[increment, 0, -increment, 0],
|
||||
):
|
||||
event.char = char
|
||||
# <a> instead of <Control+a>
|
||||
event.state = 0
|
||||
axis = axes_map[char]
|
||||
if axis == "x":
|
||||
temp = x + xs * (-1 if flip_x else 1)
|
||||
else:
|
||||
temp = y + ys * (-1 if flip_y else 1)
|
||||
stage_controller.stage_key_press(event)
|
||||
stage_controller.widget_vals[axis].set.assert_called_once_with(temp)
|
||||
stage_controller.widget_vals[axis].set.reset_mock()
|
||||
stage_controller.widget_vals[axis].get.reset_mock()
|
||||
stage_controller.widget_vals["xy_step"].get.reset_mock()
|
||||
|
||||
stage_config["flip_x"] = False
|
||||
stage_config["flip_y"] = False
|
||||
|
||||
|
||||
def test_get_position(stage_controller):
|
||||
import tkinter as tk
|
||||
|
||||
vals = {}
|
||||
for axis in AXES:
|
||||
vals[axis] = np.random.randint(0, 9)
|
||||
stage_controller.widget_vals[axis].get = MagicMock(return_value=vals[axis])
|
||||
|
||||
step_vals = {}
|
||||
for axis in CAXES:
|
||||
step_vals[axis] = np.random.randint(1, 9)
|
||||
stage_controller.widget_vals[axis + "_step"].get = MagicMock(
|
||||
return_value=step_vals[axis]
|
||||
)
|
||||
|
||||
stage_controller.position_min = pos_dict(0)
|
||||
stage_controller.position_max = pos_dict(10)
|
||||
position = stage_controller.get_position()
|
||||
assert position == {k: vals[k] for k in AXES}
|
||||
|
||||
stage_controller.position_min = pos_dict(2)
|
||||
|
||||
vals = {}
|
||||
for axis in AXES:
|
||||
vals[axis] = np.random.choice(
|
||||
np.concatenate((np.arange(-9, 0), np.arange(10, 20)))
|
||||
)
|
||||
stage_controller.widget_vals[axis].get = MagicMock(return_value=vals[axis])
|
||||
|
||||
position = stage_controller.get_position()
|
||||
assert position is None
|
||||
|
||||
stage_controller.widget_vals["x"].get.side_effect = tk.TclError
|
||||
position = stage_controller.get_position()
|
||||
assert position is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"flip_x, flip_y, flip_z",
|
||||
[
|
||||
(False, False, False),
|
||||
(True, False, False),
|
||||
(True, True, False),
|
||||
(False, True, True),
|
||||
(True, True, True),
|
||||
],
|
||||
)
|
||||
def test_up_btn_handler(stage_controller, flip_x, flip_y, flip_z):
|
||||
microscope_name = (
|
||||
stage_controller.parent_controller.configuration_controller.microscope_name
|
||||
)
|
||||
stage_config = stage_controller.parent_controller.configuration["configuration"][
|
||||
"microscopes"
|
||||
][microscope_name]["stage"]
|
||||
stage_config["flip_x"] = flip_x
|
||||
stage_config["flip_y"] = flip_y
|
||||
stage_config["flip_z"] = flip_z
|
||||
stage_controller.initialize()
|
||||
flip_flags = (
|
||||
stage_controller.parent_controller.configuration_controller.stage_flip_flags
|
||||
)
|
||||
|
||||
vals = {}
|
||||
for axis in AXES:
|
||||
vals[axis] = np.random.randint(1, 9)
|
||||
stage_controller.widget_vals[axis].get = MagicMock(return_value=vals[axis])
|
||||
stage_controller.widget_vals[axis].set = MagicMock()
|
||||
|
||||
step_vals = {}
|
||||
for axis in CAXES:
|
||||
step_vals[axis] = np.random.randint(1, 9)
|
||||
stage_controller.widget_vals[axis + "_step"].get = MagicMock(
|
||||
return_value=step_vals[axis]
|
||||
)
|
||||
|
||||
stage_controller.position_max = pos_dict(10)
|
||||
|
||||
# Test for each axis
|
||||
for axis in AXES:
|
||||
pos = stage_controller.widget_vals[axis].get()
|
||||
if axis == "x" or axis == "y":
|
||||
step = stage_controller.widget_vals["xy_step"].get()
|
||||
else:
|
||||
step = stage_controller.widget_vals[axis + "_step"].get()
|
||||
temp = pos + step * (-1 if flip_flags[axis] else 1)
|
||||
if temp > stage_controller.position_max[axis]:
|
||||
temp = stage_controller.position_max[axis]
|
||||
stage_controller.up_btn_handler(axis)()
|
||||
stage_controller.widget_vals[axis].set.assert_called_once_with(temp)
|
||||
|
||||
# Test for out of limit condition
|
||||
for axis in AXES:
|
||||
stage_controller.widget_vals[axis].set.reset_mock()
|
||||
stage_controller.widget_vals[axis].get.return_value = 10
|
||||
stage_controller.up_btn_handler(axis)()
|
||||
if flip_flags[axis] is False:
|
||||
stage_controller.widget_vals[axis].set.assert_not_called()
|
||||
|
||||
stage_config["flip_x"] = False
|
||||
stage_config["flip_y"] = False
|
||||
stage_config["flip_z"] = False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"flip_x, flip_y, flip_z",
|
||||
[
|
||||
(False, False, False),
|
||||
(True, False, False),
|
||||
(True, True, False),
|
||||
(False, True, True),
|
||||
(True, True, True),
|
||||
],
|
||||
)
|
||||
def test_down_btn_handler(stage_controller, flip_x, flip_y, flip_z):
|
||||
microscope_name = (
|
||||
stage_controller.parent_controller.configuration_controller.microscope_name
|
||||
)
|
||||
stage_config = stage_controller.parent_controller.configuration["configuration"][
|
||||
"microscopes"
|
||||
][microscope_name]["stage"]
|
||||
stage_config["flip_x"] = flip_x
|
||||
stage_config["flip_y"] = flip_y
|
||||
stage_config["flip_z"] = flip_z
|
||||
stage_controller.initialize()
|
||||
flip_flags = (
|
||||
stage_controller.parent_controller.configuration_controller.stage_flip_flags
|
||||
)
|
||||
vals = {}
|
||||
for axis in AXES:
|
||||
vals[axis] = np.random.randint(1, 9)
|
||||
stage_controller.widget_vals[axis].get = MagicMock(return_value=vals[axis])
|
||||
stage_controller.widget_vals[axis].set = MagicMock()
|
||||
|
||||
step_vals = {}
|
||||
for axis in CAXES:
|
||||
step_vals[axis] = np.random.randint(1, 9)
|
||||
stage_controller.widget_vals[axis + "_step"].get = MagicMock(
|
||||
return_value=step_vals[axis]
|
||||
)
|
||||
|
||||
stage_controller.position_min = pos_dict(0)
|
||||
|
||||
# Test for each axis
|
||||
for axis in AXES:
|
||||
pos = stage_controller.widget_vals[axis].get()
|
||||
if axis == "x" or axis == "y":
|
||||
step = stage_controller.widget_vals["xy_step"].get()
|
||||
else:
|
||||
step = stage_controller.widget_vals[axis + "_step"].get()
|
||||
temp = pos - step * (-1 if flip_flags[axis] else 1)
|
||||
if temp < stage_controller.position_min[axis]:
|
||||
temp = stage_controller.position_min[axis]
|
||||
stage_controller.down_btn_handler(axis)()
|
||||
stage_controller.widget_vals[axis].set.assert_called_once_with(temp)
|
||||
|
||||
# Test for out of limit condition
|
||||
for axis in ["x", "y", "z", "theta", "f"]:
|
||||
stage_controller.widget_vals[axis].set.reset_mock()
|
||||
stage_controller.widget_vals[axis].get.return_value = 0
|
||||
stage_controller.down_btn_handler(axis)()
|
||||
if flip_flags[axis] is False:
|
||||
stage_controller.widget_vals[axis].set.assert_not_called()
|
||||
|
||||
stage_config["flip_x"] = False
|
||||
stage_config["flip_y"] = False
|
||||
stage_config["flip_z"] = False
|
||||
|
||||
|
||||
def test_stop_button_handler(stage_controller):
|
||||
|
||||
stage_controller.view.after = MagicMock()
|
||||
|
||||
stage_controller.stop_button_handler()
|
||||
|
||||
stage_controller.view.after.assert_called_once()
|
||||
|
||||
|
||||
def test_position_callback(stage_controller):
|
||||
|
||||
stage_controller.show_verbose_info = MagicMock()
|
||||
|
||||
stage_controller.view.after = MagicMock()
|
||||
|
||||
vals = {}
|
||||
widgets = stage_controller.view.get_widgets()
|
||||
for axis in AXES:
|
||||
vals[axis] = np.random.randint(1, 9)
|
||||
stage_controller.widget_vals[axis].get = MagicMock(return_value=vals[axis])
|
||||
stage_controller.widget_vals[axis].set = MagicMock()
|
||||
widgets[axis].widget.set(vals[axis])
|
||||
widgets[axis].widget.trigger_focusout_validation = MagicMock()
|
||||
|
||||
stage_controller.position_min = pos_dict(0)
|
||||
stage_controller.position_max = pos_dict(10)
|
||||
stage_controller.stage_setting_dict = {}
|
||||
|
||||
for axis in AXES:
|
||||
|
||||
callback = stage_controller.position_callback(axis)
|
||||
|
||||
# Test case 1: Position variable is within limits
|
||||
widgets[axis].widget.get = MagicMock(return_value=vals[axis])
|
||||
callback()
|
||||
stage_controller.view.after.assert_called()
|
||||
stage_controller.view.after.reset_mock()
|
||||
assert stage_controller.stage_setting_dict[axis] == vals[axis]
|
||||
|
||||
# Test case 2: Position variable is outside limits
|
||||
widgets[axis].widget.get = MagicMock(return_value=11)
|
||||
callback()
|
||||
stage_controller.view.after.assert_called_once()
|
||||
stage_controller.view.after.reset_mock()
|
||||
215
test/controller/sub_controllers/test_tiling.py
Normal file
215
test/controller/sub_controllers/test_tiling.py
Normal file
@@ -0,0 +1,215 @@
|
||||
import random
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def tiling_wizard_controller(dummy_view, dummy_controller):
|
||||
from navigate.view.popups.tiling_wizard_popup2 import TilingWizardPopup
|
||||
from navigate.controller.sub_controllers.tiling import (
|
||||
TilingWizardController,
|
||||
)
|
||||
|
||||
tiling_wizard = TilingWizardPopup(dummy_view)
|
||||
|
||||
class SubController:
|
||||
def __init__(self):
|
||||
self.parent_controller = dummy_controller
|
||||
|
||||
return TilingWizardController(tiling_wizard, SubController())
|
||||
|
||||
|
||||
def test_traces(tiling_wizard_controller):
|
||||
"""TODO: Find a way to access the actual lambda functions.
|
||||
|
||||
If we can, inspect.getsource(myfunc) should provide us the lambda definition.
|
||||
"""
|
||||
|
||||
def assert_one_trace(var):
|
||||
tinfo = var.trace_info()
|
||||
assert len(tinfo) >= 1
|
||||
assert tinfo[0][0][0] == "write"
|
||||
assert "lambda" in tinfo[0][1]
|
||||
|
||||
for ax in ["x", "y", "z", "f"]:
|
||||
# self.variables["x_start"], etc. should all be bound to two lambda functions
|
||||
# calling calculate_distance() and update_fov()
|
||||
for bound in ["start", "end"]:
|
||||
tinfo = tiling_wizard_controller.variables[f"{ax}_{bound}"].trace_info()
|
||||
assert len(tinfo) >= 1
|
||||
for ti in tinfo:
|
||||
assert ti[0][0] == "write"
|
||||
assert "lambda" in ti[1]
|
||||
|
||||
# fov should be bound to one lambda, calling calculate_tiles()
|
||||
assert_one_trace(tiling_wizard_controller.variables[f"{ax}_fov"])
|
||||
|
||||
# dist should be bound to one lambda, calling calculate_tiles()
|
||||
assert_one_trace(tiling_wizard_controller.variables[f"{ax}_dist"])
|
||||
|
||||
# Special cases
|
||||
assert_one_trace(tiling_wizard_controller.variables["percent_overlap"])
|
||||
assert_one_trace(
|
||||
tiling_wizard_controller.cam_settings_widgets["FOV_X"].get_variable()
|
||||
)
|
||||
assert_one_trace(
|
||||
tiling_wizard_controller.cam_settings_widgets["FOV_Y"].get_variable()
|
||||
)
|
||||
assert_one_trace(
|
||||
tiling_wizard_controller.stack_acq_widgets["start_position"].get_variable()
|
||||
)
|
||||
assert_one_trace(
|
||||
tiling_wizard_controller.stack_acq_widgets["end_position"].get_variable()
|
||||
)
|
||||
# Channels tab controller binds these a bunch
|
||||
# assert_one_trace(
|
||||
# tiling_wizard_controller.stack_acq_widgets["start_focus"].get_variable()
|
||||
# )
|
||||
# assert_one_trace(
|
||||
# tiling_wizard_controller.stack_acq_widgets["end_focus"].get_variable()
|
||||
# )
|
||||
|
||||
|
||||
def test_update_total_tiles(tiling_wizard_controller):
|
||||
tiling_wizard_controller.update_total_tiles()
|
||||
|
||||
assert True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("axis", ["x", "y", "z", "f"])
|
||||
def test_calculate_tiles(tiling_wizard_controller, axis):
|
||||
from navigate.tools.multipos_table_tools import calc_num_tiles
|
||||
|
||||
ov, dist, fov = random.random(), random.random() * 100, random.random() * 10
|
||||
tiling_wizard_controller._percent_overlap = ov * 100
|
||||
tiling_wizard_controller.variables[f"{axis}_dist"].set(dist)
|
||||
tiling_wizard_controller.variables[f"{axis}_fov"].set(fov)
|
||||
tiling_wizard_controller.calculate_tiles(axis)
|
||||
|
||||
if axis == "x" or axis == "y":
|
||||
dist += fov
|
||||
|
||||
assert int(
|
||||
tiling_wizard_controller.variables[f"{axis}_tiles"].get()
|
||||
) == calc_num_tiles(dist, ov, fov)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("axis", ["x", "y", "z", "f"])
|
||||
def test_calculate_distance(tiling_wizard_controller, axis):
|
||||
start, end = random.random() * 10, random.random() * 100
|
||||
tiling_wizard_controller.variables[axis + "_start"].set(start)
|
||||
tiling_wizard_controller.variables[axis + "_end"].set(end)
|
||||
tiling_wizard_controller.calculate_distance(axis)
|
||||
assert float(tiling_wizard_controller.variables[axis + "_dist"].get()) == abs(
|
||||
start - end
|
||||
)
|
||||
|
||||
|
||||
def test_update_overlap(tiling_wizard_controller):
|
||||
tiling_wizard_controller.variables["percent_overlap"].set("")
|
||||
tiling_wizard_controller.update_overlap()
|
||||
tiling_wizard_controller.variables["percent_overlap"].set("10")
|
||||
tiling_wizard_controller.update_overlap()
|
||||
|
||||
assert True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("axis", ["x", "y", "z", "f"])
|
||||
def test_update_fov(tiling_wizard_controller, axis):
|
||||
import random
|
||||
from navigate.tools.multipos_table_tools import sign
|
||||
|
||||
if axis == "y":
|
||||
tiling_wizard_controller.cam_settings_widgets["FOV_X"].set(
|
||||
int(random.random() * 1000)
|
||||
)
|
||||
tiling_wizard_controller.variables["x_start"].set(random.random() * 10)
|
||||
tiling_wizard_controller.variables["x_end"].set(random.random() * 1000)
|
||||
var = float(
|
||||
tiling_wizard_controller.cam_settings_widgets["FOV_X"].get()
|
||||
) * sign(
|
||||
float(tiling_wizard_controller.variables["x_end"].get())
|
||||
- float(tiling_wizard_controller.variables["x_start"].get())
|
||||
)
|
||||
elif axis == "x":
|
||||
tiling_wizard_controller.cam_settings_widgets["FOV_Y"].set(
|
||||
int(random.random() * 1000)
|
||||
)
|
||||
tiling_wizard_controller.variables["y_start"].set(random.random() * 10)
|
||||
tiling_wizard_controller.variables["y_end"].set(random.random() * 1000)
|
||||
var = float(
|
||||
tiling_wizard_controller.cam_settings_widgets["FOV_Y"].get()
|
||||
) * sign(
|
||||
float(tiling_wizard_controller.variables["y_end"].get())
|
||||
- float(tiling_wizard_controller.variables["y_start"].get())
|
||||
)
|
||||
elif axis == "z":
|
||||
tiling_wizard_controller.stack_acq_widgets["start_position"].set(
|
||||
random.random() * 10
|
||||
)
|
||||
tiling_wizard_controller.stack_acq_widgets["end_position"].set(
|
||||
random.random() * 1000
|
||||
)
|
||||
var = float(
|
||||
tiling_wizard_controller.stack_acq_widgets["end_position"].get()
|
||||
) - float(tiling_wizard_controller.stack_acq_widgets["start_position"].get())
|
||||
elif axis == "f":
|
||||
tiling_wizard_controller.stack_acq_widgets["start_focus"].set(
|
||||
random.random() * 10
|
||||
)
|
||||
tiling_wizard_controller.stack_acq_widgets["end_focus"].set(
|
||||
random.random() * 1000
|
||||
)
|
||||
var = float(
|
||||
tiling_wizard_controller.stack_acq_widgets["end_focus"].get()
|
||||
) - float(tiling_wizard_controller.stack_acq_widgets["start_focus"].get())
|
||||
tiling_wizard_controller.update_fov(axis)
|
||||
|
||||
assert float(tiling_wizard_controller.variables[f"{axis}_fov"].get()) == abs(var)
|
||||
|
||||
|
||||
def test_set_table(tiling_wizard_controller):
|
||||
# from navigate.tools.multipos_table_tools import compute_tiles_from_bounding_box
|
||||
tiling_wizard_controller.set_table()
|
||||
|
||||
x_start = float(tiling_wizard_controller.variables["x_start"].get())
|
||||
x_stop = float(tiling_wizard_controller.variables["x_end"].get())
|
||||
|
||||
y_start = float(tiling_wizard_controller.variables["y_start"].get())
|
||||
y_stop = float(tiling_wizard_controller.variables["y_end"].get())
|
||||
|
||||
# shift z by coordinate origin of local z-stack
|
||||
z_start = float(tiling_wizard_controller.variables["z_start"].get()) - float(
|
||||
tiling_wizard_controller.stack_acq_widgets["start_position"].get()
|
||||
)
|
||||
z_stop = float(tiling_wizard_controller.variables["z_end"].get()) - float(
|
||||
tiling_wizard_controller.stack_acq_widgets["end_position"].get()
|
||||
)
|
||||
|
||||
# Default to fixed theta
|
||||
r_start = tiling_wizard_controller.stage_position_vars["theta"].get()
|
||||
r_stop = tiling_wizard_controller.stage_position_vars["theta"].get()
|
||||
|
||||
f_start = float(tiling_wizard_controller.variables["f_start"].get()) - float(
|
||||
tiling_wizard_controller.stack_acq_widgets["start_focus"].get()
|
||||
)
|
||||
f_stop = float(tiling_wizard_controller.variables["f_end"].get()) - float(
|
||||
tiling_wizard_controller.stack_acq_widgets["end_focus"].get()
|
||||
)
|
||||
|
||||
# for consistency, always go from low to high
|
||||
def sort_vars(a, b):
|
||||
if a > b:
|
||||
return b, a
|
||||
return a, b
|
||||
|
||||
x_start, x_stop = sort_vars(x_start, x_stop)
|
||||
y_start, y_stop = sort_vars(y_start, y_stop)
|
||||
z_start, z_stop = sort_vars(z_start, z_stop)
|
||||
r_start, r_stop = sort_vars(r_start, r_stop)
|
||||
f_start, f_stop = sort_vars(f_start, f_stop)
|
||||
|
||||
assert tiling_wizard_controller.multipoint_table.model.df["X"].min() == x_start
|
||||
assert tiling_wizard_controller.multipoint_table.model.df["Y"].min() == y_start
|
||||
assert tiling_wizard_controller.multipoint_table.model.df["Z"].min() == z_start
|
||||
assert tiling_wizard_controller.multipoint_table.model.df["THETA"].min() == r_start
|
||||
assert tiling_wizard_controller.multipoint_table.model.df["F"].min() == f_start
|
||||
178
test/controller/sub_controllers/test_waveform_popup.py
Normal file
178
test/controller/sub_controllers/test_waveform_popup.py
Normal file
@@ -0,0 +1,178 @@
|
||||
import pytest
|
||||
import random
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def waveform_popup_controller(dummy_view, dummy_controller):
|
||||
from navigate.controller.sub_controllers.waveform_popup import (
|
||||
WaveformPopupController,
|
||||
)
|
||||
from navigate.view.popups.waveform_parameter_popup_window import (
|
||||
WaveformParameterPopupWindow,
|
||||
)
|
||||
|
||||
waveform_constants_popup = WaveformParameterPopupWindow(
|
||||
dummy_view, dummy_controller.configuration_controller
|
||||
)
|
||||
|
||||
return WaveformPopupController(
|
||||
waveform_constants_popup,
|
||||
dummy_controller,
|
||||
dummy_controller.waveform_constants_path,
|
||||
)
|
||||
|
||||
|
||||
def test_populate_experiment_values(waveform_popup_controller):
|
||||
exp_dict = waveform_popup_controller.parent_controller.configuration["experiment"][
|
||||
"MicroscopeState"
|
||||
]
|
||||
resolution = exp_dict["microscope_name"]
|
||||
zoom = exp_dict["zoom"]
|
||||
waveform_constants = waveform_popup_controller.parent_controller.configuration[
|
||||
"waveform_constants"
|
||||
]
|
||||
widgets = waveform_popup_controller.view.get_widgets()
|
||||
|
||||
def assert_widget_values():
|
||||
resolution = exp_dict["microscope_name"]
|
||||
zoom = exp_dict["zoom"]
|
||||
assert widgets["Mode"].get() == resolution
|
||||
assert widgets["Mag"].get() == zoom
|
||||
|
||||
# remote focus
|
||||
remote_focus_dict = waveform_constants["remote_focus_constants"][resolution][
|
||||
zoom
|
||||
]
|
||||
for k in remote_focus_dict.keys():
|
||||
assert widgets[k + " Amp"].get() == remote_focus_dict[k]["amplitude"]
|
||||
assert widgets[k + " Off"].get() == remote_focus_dict[k]["offset"]
|
||||
|
||||
# galvo
|
||||
galvo_dict = waveform_constants["galvo_constants"]
|
||||
for g in galvo_dict.keys():
|
||||
if resolution in [galvo_dict[g].keys()]:
|
||||
galvo_info = galvo_dict[g][resolution][zoom]
|
||||
assert widgets[g + " Amp"].get() == galvo_info["amplitude"]
|
||||
assert widgets[g + " Off"].get() == galvo_info["offset"]
|
||||
|
||||
# delay, fly back time, settle duraation, smoothing
|
||||
assert widgets["Delay"].get() == str(
|
||||
waveform_constants["other_constants"]["remote_focus_delay"]
|
||||
)
|
||||
assert widgets["Ramp_falling"].get() == str(
|
||||
waveform_constants["other_constants"]["remote_focus_ramp_falling"]
|
||||
)
|
||||
assert widgets["Duty"].get() == str(
|
||||
waveform_constants["other_constants"]["remote_focus_settle_duration"]
|
||||
)
|
||||
assert widgets["Smoothing"].get() == str(
|
||||
waveform_constants["other_constants"]["percent_smoothing"]
|
||||
)
|
||||
|
||||
# default values
|
||||
waveform_popup_controller.populate_experiment_values()
|
||||
assert_widget_values()
|
||||
|
||||
# change resolution and/or zoom
|
||||
for microscope_name in waveform_constants["remote_focus_constants"].keys():
|
||||
for z in waveform_constants["remote_focus_constants"][microscope_name].keys():
|
||||
exp_dict["microscope_name"] = microscope_name
|
||||
exp_dict["zoom"] = z
|
||||
waveform_popup_controller.populate_experiment_values()
|
||||
assert_widget_values()
|
||||
|
||||
exp_dict["microscope_name"] = resolution
|
||||
exp_dict["zoom"] = zoom
|
||||
waveform_popup_controller.populate_experiment_values()
|
||||
assert_widget_values()
|
||||
|
||||
# update waveform_constants
|
||||
for k in waveform_constants["remote_focus_constants"][resolution][zoom].keys():
|
||||
amplitude = round(random.random() * 5, 2)
|
||||
offset = round(random.random() * 5, 2)
|
||||
temp = waveform_constants["remote_focus_constants"][resolution][zoom][k]
|
||||
temp["amplitude"] = amplitude
|
||||
temp["offset"] = offset
|
||||
|
||||
# update galvo
|
||||
for g in waveform_constants["galvo_constants"].keys():
|
||||
amplitude = round(random.random() * 5, 2)
|
||||
offset = round(random.random() * 5, 2)
|
||||
temp = waveform_constants["galvo_constants"][g][resolution][zoom]
|
||||
temp["amplitude"] = amplitude
|
||||
temp["offset"] = offset
|
||||
|
||||
for k in [
|
||||
"remote_focus_ramp_falling",
|
||||
"remote_focus_settle_duration",
|
||||
"percent_smoothing",
|
||||
"remote_focus_delay",
|
||||
]:
|
||||
waveform_constants["other_constants"][k] = round(random.random() * 100, 2)
|
||||
|
||||
waveform_popup_controller.populate_experiment_values(force_update=True)
|
||||
assert_widget_values()
|
||||
|
||||
|
||||
def test_show_laser_info(waveform_popup_controller):
|
||||
waveform_popup_controller.show_laser_info()
|
||||
assert True
|
||||
|
||||
|
||||
def test_configure_widget_range(waveform_popup_controller):
|
||||
waveform_popup_controller.configure_widget_range()
|
||||
assert True
|
||||
|
||||
|
||||
def test_estimate_galvo_setting_empty_string(waveform_popup_controller):
|
||||
"""Test if the function returns without calling the camera setting controller."""
|
||||
# Galvo name
|
||||
galvo_name = "galvo_0"
|
||||
|
||||
# Mocked camera setting controller
|
||||
waveform_popup_controller.parent_controller.camera_setting_controller = MagicMock()
|
||||
waveform_popup_controller.parent_controller.camera_setting_controller.mode_widgets[
|
||||
"Pixels"
|
||||
].get = MagicMock(return_value="")
|
||||
waveform_popup_controller.parent_controller.camera_setting_controller.framerate_widgets[
|
||||
"exposure_time"
|
||||
].get = MagicMock()
|
||||
|
||||
waveform_popup_controller.estimate_galvo_setting(galvo_name)
|
||||
waveform_popup_controller.parent_controller.camera_setting_controller.framerate_widgets[
|
||||
"exposure_time"
|
||||
].get.assert_not_called()
|
||||
|
||||
|
||||
def test_estimate_galvo_setting_with_string(waveform_popup_controller):
|
||||
"""Test if the function calls the camera setting controller."""
|
||||
# Galvo name
|
||||
galvo_name = "galvo_0"
|
||||
number_of_pixels = 50
|
||||
|
||||
# Mocked camera setting controller
|
||||
waveform_popup_controller.parent_controller.camera_setting_controller = MagicMock()
|
||||
waveform_popup_controller.parent_controller.camera_setting_controller.mode_widgets[
|
||||
"Pixels"
|
||||
].get = MagicMock(return_value=str(number_of_pixels))
|
||||
waveform_popup_controller.parent_controller.camera_setting_controller.framerate_widgets[
|
||||
"exposure_time"
|
||||
].get = MagicMock()
|
||||
|
||||
# Mocked model
|
||||
waveform_popup_controller.parent_controller.model = MagicMock()
|
||||
mock_model = waveform_popup_controller.parent_controller.model
|
||||
mock_model.get_camera_line_interval_and_exposure_time = MagicMock(
|
||||
return_value=(0.05, 50, 500)
|
||||
)
|
||||
|
||||
# Mocked view
|
||||
waveform_popup_controller.view = MagicMock()
|
||||
waveform_popup_controller.view.inputs[galvo_name].widget.set = MagicMock()
|
||||
|
||||
# Call the function
|
||||
waveform_popup_controller.estimate_galvo_setting(galvo_name)
|
||||
|
||||
# Check to see what the view was called with.
|
||||
waveform_popup_controller.view.inputs[galvo_name].widget.set.assert_called_once()
|
||||
787
test/controller/test_controller.py
Normal file
787
test/controller/test_controller.py
Normal file
@@ -0,0 +1,787 @@
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, ANY
|
||||
import pytest
|
||||
import numpy
|
||||
import multiprocessing as mp
|
||||
import logging
|
||||
import platform
|
||||
from logging.handlers import QueueHandler
|
||||
|
||||
from navigate.log_files.log_functions import log_setup
|
||||
|
||||
|
||||
class _NullQueue:
|
||||
"""Minimal queue-like sink for logging; avoids mp feeder threads on Windows."""
|
||||
|
||||
def put(self, _): # QueueHandler calls .put()
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
def cancel_join_thread(self):
|
||||
pass
|
||||
|
||||
|
||||
class DummySplashScreen:
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
|
||||
def _normalize_log_setup(start_listener):
|
||||
"""Call log_setup and normalize its return to (log_queue, log_listener)."""
|
||||
from navigate.log_files.log_functions import log_setup
|
||||
|
||||
try:
|
||||
res = log_setup("logging.yml", logging_path=None, start_listener=start_listener)
|
||||
except Exception:
|
||||
res = None
|
||||
|
||||
# Accept (queue, listener), or queue-only, or None.
|
||||
if isinstance(res, tuple) and len(res) == 2:
|
||||
return res[0], res[1]
|
||||
if res is not None and hasattr(res, "put"):
|
||||
return res, None
|
||||
|
||||
# Fallbacks:
|
||||
if platform.system() == "Windows":
|
||||
# Avoid mp.Queue on Windows to prevent hangs in CI.
|
||||
return _NullQueue(), None
|
||||
# Non-Windows: a real mp.Queue is fine without a listener.
|
||||
try:
|
||||
return mp.Queue(), None
|
||||
except Exception:
|
||||
return _NullQueue(), None
|
||||
|
||||
|
||||
def _remove_queue_handlers(target_queue=None):
|
||||
# Detach and close any QueueHandler that targets target_queue.
|
||||
def strip_handlers(logger):
|
||||
for h in list(getattr(logger, "handlers", [])):
|
||||
if isinstance(h, QueueHandler) and (
|
||||
target_queue is None or getattr(h, "queue", None) is target_queue
|
||||
):
|
||||
try:
|
||||
logger.removeHandler(h)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
h.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
strip_handlers(logging.getLogger())
|
||||
for name, obj in logging.root.manager.loggerDict.items():
|
||||
if isinstance(obj, logging.Logger):
|
||||
strip_handlers(logging.getLogger(name))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def controller(tk_root):
|
||||
from navigate.controller.controller import Controller
|
||||
|
||||
base_directory = Path.joinpath(
|
||||
Path(__file__).resolve().parent.parent.parent, "src", "navigate"
|
||||
)
|
||||
configuration_directory = Path.joinpath(base_directory, "config")
|
||||
|
||||
configuration_path = Path.joinpath(configuration_directory, "configuration.yaml")
|
||||
experiment_path = Path.joinpath(configuration_directory, "experiment.yml")
|
||||
waveform_constants_path = Path.joinpath(
|
||||
configuration_directory, "waveform_constants.yml"
|
||||
)
|
||||
rest_api_path = Path.joinpath(configuration_directory, "rest_api_config.yml")
|
||||
waveform_templates_path = Path.joinpath(
|
||||
configuration_directory, "waveform_templates.yml"
|
||||
)
|
||||
gui_configuration_path = Path.joinpath(
|
||||
configuration_directory, "gui_configuration.yml"
|
||||
)
|
||||
multi_positions_path = Path.joinpath(configuration_directory, "multi_positions.yml")
|
||||
args = SimpleNamespace(synthetic_hardware=True)
|
||||
|
||||
start_listener = platform.system() != "Windows"
|
||||
log_queue, log_listener = _normalize_log_setup(start_listener)
|
||||
|
||||
controller = Controller(
|
||||
root=tk_root,
|
||||
splash_screen=DummySplashScreen(),
|
||||
configuration_path=configuration_path,
|
||||
experiment_path=experiment_path,
|
||||
waveform_constants_path=waveform_constants_path,
|
||||
rest_api_path=rest_api_path,
|
||||
waveform_templates_path=waveform_templates_path,
|
||||
gui_configuration_path=gui_configuration_path,
|
||||
multi_positions_path=multi_positions_path,
|
||||
log_queue=log_queue,
|
||||
args=args,
|
||||
)
|
||||
|
||||
# To make sure the testcases won't hang on because of the model.event_queue
|
||||
# The changes here won't affect other testcases,
|
||||
# because the testcases from other files use DummyController
|
||||
# and DummyModel instead of this controller fixture
|
||||
controller.model = MagicMock()
|
||||
controller.threads_pool = MagicMock()
|
||||
controller.model.get_offset_variance_maps.return_value = (None, None)
|
||||
|
||||
yield controller
|
||||
|
||||
try:
|
||||
controller.execute("exit")
|
||||
except SystemExit:
|
||||
pass
|
||||
|
||||
# Tear down the controller properly
|
||||
q = getattr(controller, "event_queue", None)
|
||||
if q is not None:
|
||||
try:
|
||||
q.close()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
q.cancel_join_thread()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Close any Pipes
|
||||
if getattr(controller, "show_img_pipe", None):
|
||||
try:
|
||||
controller.show_img_pipe.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
controller.manager.shutdown()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Detach QueueHandlers first so no more puts go to log_queue
|
||||
_remove_queue_handlers(log_queue)
|
||||
|
||||
# Stop the queue listener (only if started)
|
||||
try:
|
||||
if start_listener and log_listener:
|
||||
try:
|
||||
log_listener.enqueue_sentinel()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
log_listener.stop()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Close the logging queue and skip join on Windows
|
||||
if platform.system() == "Windows":
|
||||
|
||||
try:
|
||||
log_queue.close()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
log_queue.cancel_join_thread()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logging.shutdown()
|
||||
|
||||
# As a last resort on Windows, hard-terminate any alive mp children
|
||||
if platform.system() == "Windows":
|
||||
try:
|
||||
children = list(mp.active_children())
|
||||
for p in children:
|
||||
try:
|
||||
p.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
for p in children:
|
||||
try:
|
||||
p.join(timeout=5)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def test_update_buffer(controller):
|
||||
camera_parameters = controller.configuration["experiment"]["CameraParameters"]
|
||||
controller.update_buffer()
|
||||
assert controller.img_width == camera_parameters["img_x_pixels"]
|
||||
assert controller.img_height == camera_parameters["img_y_pixels"]
|
||||
|
||||
# Make sure that the get_data_buffer method is not called.
|
||||
assert controller.model.get_data_buffer.called is False
|
||||
|
||||
# Change the buffer size
|
||||
microscope_name = controller.configuration["experiment"]["MicroscopeState"][
|
||||
"microscope_name"
|
||||
]
|
||||
controller.configuration["experiment"]["CameraParameters"][microscope_name][
|
||||
"img_x_pixels"
|
||||
] = 100
|
||||
controller.configuration["experiment"]["CameraParameters"][microscope_name][
|
||||
"img_y_pixels"
|
||||
] = 100
|
||||
controller.update_buffer()
|
||||
|
||||
# Make sure that the get_data_buffer method is called.
|
||||
assert controller.model.get_data_buffer.called is True
|
||||
|
||||
# Confirm that the buffer size has been updated.
|
||||
assert controller.img_width == 100
|
||||
assert controller.img_height == 100
|
||||
|
||||
assert True
|
||||
|
||||
|
||||
def test_change_microscope(controller):
|
||||
# Get the microscopes from the configuration file
|
||||
microscopes = controller.configuration["configuration"]["microscopes"]
|
||||
|
||||
# Iterate through the microscopes and change the microscope
|
||||
for microscope_name in microscopes.keys():
|
||||
|
||||
# Patch the configuration_controller
|
||||
controller.configuration_controller.change_microscope = MagicMock()
|
||||
|
||||
# Default zoom is '0.63x'
|
||||
zoom = microscopes[microscope_name]["zoom"]["position"].keys()[0]
|
||||
controller.configuration["experiment"]["MicroscopeState"]["zoom"] = zoom
|
||||
|
||||
# Change the microscope without passing the zoom
|
||||
controller.change_microscope(microscope_name)
|
||||
assert (
|
||||
controller.configuration["experiment"]["MicroscopeState"]["microscope_name"]
|
||||
== microscope_name
|
||||
)
|
||||
|
||||
# Confirm that the zoom has not changed.
|
||||
assert controller.configuration["experiment"]["MicroscopeState"]["zoom"] == zoom
|
||||
|
||||
# Call it and pass the zoom
|
||||
zoom = microscopes[microscope_name]["zoom"]["position"].keys()[-1]
|
||||
controller.change_microscope(microscope_name, zoom)
|
||||
assert controller.configuration["experiment"]["MicroscopeState"]["zoom"] == zoom
|
||||
|
||||
# Make sure that the configuration_controller has been called.
|
||||
assert controller.configuration_controller.change_microscope.called is True
|
||||
|
||||
# Have configuration controller return False
|
||||
controller.configuration_controller.change_microscope.return_value = False
|
||||
|
||||
# Patch the stage controller, channels_tab_controller...
|
||||
controller.stage_controller.initialize = MagicMock()
|
||||
controller.channels_tab_controller.initialize = MagicMock()
|
||||
camera_setting_controller = controller.camera_setting_controller
|
||||
camera_setting_controller.update_camera_device_related_setting = MagicMock()
|
||||
camera_setting_controller.calculate_physical_dimensions = MagicMock()
|
||||
controller.camera_view_controller.update_snr = MagicMock()
|
||||
|
||||
# Call change microscope, assert patched methods are not called
|
||||
controller.change_microscope(microscope_name)
|
||||
assert controller.stage_controller.initialize.called is False
|
||||
assert controller.channels_tab_controller.initialize.called is False
|
||||
assert (
|
||||
camera_setting_controller.update_camera_device_related_setting.called
|
||||
is False
|
||||
)
|
||||
assert camera_setting_controller.calculate_physical_dimensions.called is False
|
||||
assert controller.camera_view_controller.update_snr.called is False
|
||||
|
||||
# Have configuration controller return True
|
||||
controller.configuration_controller.change_microscope.return_value = True
|
||||
|
||||
# Call change microscope, assert patched methods are called
|
||||
controller.change_microscope(microscope_name)
|
||||
assert controller.stage_controller.initialize.called is True
|
||||
assert controller.channels_tab_controller.initialize.called is True
|
||||
assert (
|
||||
camera_setting_controller.update_camera_device_related_setting.called
|
||||
is True
|
||||
)
|
||||
assert camera_setting_controller.calculate_physical_dimensions.called is True
|
||||
assert controller.camera_view_controller.update_snr.called is True
|
||||
|
||||
# Test waveform popup controller.
|
||||
controller.waveform_popup_controller = MagicMock()
|
||||
controller.change_microscope(microscope_name)
|
||||
assert (
|
||||
controller.waveform_popup_controller.populate_experiment_values.called
|
||||
is True
|
||||
)
|
||||
|
||||
assert True
|
||||
|
||||
|
||||
def test_initialize_cam_view(controller):
|
||||
minmax_values = [0, 2**16 - 1]
|
||||
image_metrics = [1, 0, 0]
|
||||
controller.initialize_cam_view()
|
||||
|
||||
assert (
|
||||
controller.camera_view_controller.image_palette["Min"].get() == minmax_values[0]
|
||||
)
|
||||
assert (
|
||||
controller.camera_view_controller.image_palette["Max"].get() == minmax_values[1]
|
||||
)
|
||||
assert (
|
||||
controller.camera_view_controller.image_metrics["Frames"].get()
|
||||
== image_metrics[0]
|
||||
)
|
||||
|
||||
assert True
|
||||
|
||||
|
||||
def test_populate_experiment_setting(controller):
|
||||
controller.populate_experiment_setting(in_initialize=False)
|
||||
assert True
|
||||
|
||||
|
||||
def test_prepare_acquire_data(controller):
|
||||
|
||||
# Test without warning message
|
||||
controller.set_mode_of_sub = MagicMock()
|
||||
assert controller.prepare_acquire_data() is True
|
||||
assert controller.set_mode_of_sub.called is True
|
||||
|
||||
# Test with warning message. Challenging since controller is local.
|
||||
# with patch('controller.tkinter.messagebox.showerror') as mock_showerror:
|
||||
# controller.update_experiment_setting.return_value = "Warning!"
|
||||
# assert controller.prepare_acquire_data() is False
|
||||
# mock_showerror.assert_called_once()
|
||||
|
||||
assert True
|
||||
|
||||
|
||||
def test_set_mode_of_sub(controller):
|
||||
modes = ["customized", "stop", "live"]
|
||||
|
||||
for mode in modes:
|
||||
controller.set_mode_of_sub(mode=mode)
|
||||
|
||||
assert True
|
||||
|
||||
|
||||
def test_execute_stop_stage(controller):
|
||||
controller.threads_pool.createThread = MagicMock()
|
||||
controller.execute(command="stop_stage")
|
||||
assert controller.threads_pool.createThread.called is True
|
||||
|
||||
assert True
|
||||
|
||||
|
||||
def test_execute_move_stage_and_update_info(controller):
|
||||
positions = {"x": 51, "y": 52.0, "z": -530.3, "theta": 1, "f": 0}
|
||||
|
||||
controller.execute("move_stage_and_update_info", positions)
|
||||
for axis, value in positions.items():
|
||||
assert (
|
||||
float(controller.stage_controller.widget_vals[axis].get())
|
||||
== positions[axis]
|
||||
)
|
||||
|
||||
assert True
|
||||
|
||||
|
||||
def test_execute_move_stage_and_acquire_image(controller):
|
||||
positions = {"x": 51, "y": 52.0, "z": -530.3, "theta": 1, "f": 0}
|
||||
controller.model.move_stage = MagicMock()
|
||||
controller.threads_pool.createThread = MagicMock()
|
||||
controller.execute("move_stage_and_acquire_image", positions)
|
||||
|
||||
assert controller.model.move_stage.called is True
|
||||
|
||||
for axis, value in positions.items():
|
||||
assert (
|
||||
float(controller.stage_controller.widget_vals[axis].get())
|
||||
== positions[axis]
|
||||
)
|
||||
|
||||
assert controller.threads_pool.createThread.called is True
|
||||
assert True
|
||||
|
||||
|
||||
def test_execute_get_stage_position(controller):
|
||||
# Set the positions in the GUI
|
||||
set_positions = {"x": 51, "y": 52.0, "z": -530.3, "theta": 1, "f": 0}
|
||||
controller.execute("move_stage_and_update_info", set_positions)
|
||||
|
||||
# Get the positions from the GUI
|
||||
get_positions = controller.execute("get_stage_position")
|
||||
assert type(get_positions) is dict
|
||||
|
||||
axes = ["x", "y", "z", "theta", "f"]
|
||||
for axis in axes:
|
||||
assert axis in get_positions.keys()
|
||||
|
||||
# assert that get_positions is equal to set_positions
|
||||
assert get_positions == set_positions
|
||||
|
||||
assert True
|
||||
|
||||
|
||||
def test_execute_mark_position(controller):
|
||||
|
||||
set_positions = {"x": 51, "y": 52.0, "z": -530.3, "theta": 1, "f": 0}
|
||||
controller.execute("mark_position", set_positions)
|
||||
|
||||
# Get the positions from the multiposition table. Returns a list of lists.
|
||||
get_positions = controller.multiposition_tab_controller.get_positions()
|
||||
|
||||
# Assert that the last position in get_positions is equal to set_positions
|
||||
assert get_positions[-1] == [
|
||||
set_positions["x"],
|
||||
set_positions["y"],
|
||||
set_positions["z"],
|
||||
set_positions["theta"],
|
||||
set_positions["f"],
|
||||
]
|
||||
|
||||
assert True
|
||||
|
||||
|
||||
def test_execute_resolution(controller):
|
||||
pass
|
||||
|
||||
|
||||
def test_execute_set_save(controller):
|
||||
|
||||
for save_data in [True, False]:
|
||||
controller.execute("set_save", save_data)
|
||||
assert controller.acquire_bar_controller.is_save == save_data
|
||||
assert (
|
||||
controller.configuration["experiment"]["MicroscopeState"]["is_save"]
|
||||
== save_data
|
||||
)
|
||||
|
||||
assert True
|
||||
|
||||
|
||||
def test_execute_update_setting(controller):
|
||||
controller.threads_pool.createThread = MagicMock()
|
||||
args = ["resolution", {"resolution_mode", "1x"}]
|
||||
controller.execute("update_setting", args)
|
||||
assert controller.threads_pool.createThread.called is True
|
||||
|
||||
assert True
|
||||
|
||||
|
||||
def test_execute_stage_limits(controller):
|
||||
controller.threads_pool.createThread = MagicMock()
|
||||
for stage_limits in [True, False]:
|
||||
controller.threads_pool.createThread.reset_mock()
|
||||
controller.execute("stage_limits", stage_limits)
|
||||
assert controller.stage_controller.stage_limits == stage_limits
|
||||
assert controller.threads_pool.createThread.called is True
|
||||
|
||||
assert True
|
||||
|
||||
|
||||
def test_execute_autofocus(controller):
|
||||
# Create mock objects
|
||||
controller.threads_pool.createThread = MagicMock()
|
||||
|
||||
# Test non-acquiring case
|
||||
controller.acquire_bar_controller.is_acquiring = False
|
||||
controller.execute("autofocus")
|
||||
controller.threads_pool.createThread.assert_called_with(
|
||||
resourceName="camera",
|
||||
target=controller.capture_image,
|
||||
args=("autofocus", "live"),
|
||||
)
|
||||
|
||||
# Test the acquiring case
|
||||
controller.acquire_bar_controller.mode = "live"
|
||||
controller.acquire_bar_controller.is_acquiring = True
|
||||
controller.threads_pool.createThread.reset_mock()
|
||||
controller.execute("autofocus")
|
||||
controller.threads_pool.createThread.assert_called_once()
|
||||
controller.threads_pool.createThread.assert_any_call(
|
||||
resourceName="model", target=ANY
|
||||
)
|
||||
args, kwargs = controller.threads_pool.createThread.call_args
|
||||
assert kwargs["resourceName"] == "model"
|
||||
|
||||
# Confirm that the lambda is callable.
|
||||
assert callable(kwargs["target"])
|
||||
|
||||
assert True
|
||||
|
||||
|
||||
def test_execute_eliminate_tiles(controller):
|
||||
controller.threads_pool.createThread = MagicMock()
|
||||
|
||||
# Populate Feature List
|
||||
controller.menu_controller.feature_list_names = ["Remove Empty Tiles"]
|
||||
|
||||
# Set the mode to live
|
||||
controller.acquire_bar_controller.set_mode("live")
|
||||
|
||||
# Execute the command
|
||||
controller.execute("eliminate_tiles")
|
||||
assert controller.acquire_bar_controller.get_mode() == "customized"
|
||||
|
||||
# Assert that the thread pool is called
|
||||
assert controller.threads_pool.createThread.called is True
|
||||
|
||||
assert True
|
||||
|
||||
|
||||
def test_execute_load_features(controller):
|
||||
controller.threads_pool.createThread = MagicMock()
|
||||
|
||||
controller.execute("load_features")
|
||||
controller.threads_pool.createThread.assert_any_call("model", ANY)
|
||||
args, kwargs = controller.threads_pool.createThread.call_args
|
||||
assert args[0] == "model"
|
||||
assert callable(args[1])
|
||||
|
||||
assert True
|
||||
|
||||
|
||||
def test_execute_acquire_and_save_return(controller):
|
||||
|
||||
# Prepare mock objects.
|
||||
controller.prepare_acquire_data = MagicMock()
|
||||
controller.acquire_bar_controller.stop_acquire = MagicMock()
|
||||
controller.camera_setting_controller.calculate_physical_dimensions = MagicMock()
|
||||
|
||||
# Prepare mock returns
|
||||
controller.prepare_acquire_data.return_value = False
|
||||
|
||||
# Test and make sure return is called
|
||||
controller.execute("acquire_and_save")
|
||||
assert controller.acquire_bar_controller.stop_acquire.called is True
|
||||
assert (
|
||||
controller.camera_setting_controller.calculate_physical_dimensions.called
|
||||
is False
|
||||
)
|
||||
|
||||
assert True
|
||||
|
||||
|
||||
def test_execute_acquire_and_acquire_and_save(controller):
|
||||
# The modes "customized" & "live" results in the thread not being called.
|
||||
# TODO: Figure out why the thread is not being called.
|
||||
|
||||
# controller.plugin_acquisition_modes = {}
|
||||
# controller.threads_pool.createThread = MagicMock()
|
||||
#
|
||||
# for statement in ["acquire", "acquire_and_save"]:
|
||||
# for mode in ["z-stack", "single"]:
|
||||
# controller.acquire_bar_controller.mode = mode
|
||||
# controller.execute(statement)
|
||||
# controller.threads_pool.createThread.assert_called_with(
|
||||
# "camera",
|
||||
# controller.capture_image,
|
||||
# args=(
|
||||
# "acquire",
|
||||
# controller.acquire_bar_controller.mode,
|
||||
# ),
|
||||
# )
|
||||
# controller.stop_acquisition_flag = True
|
||||
# controller.threads_pool.createThread.reset_mock()
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def test_execute_stop_acquire(controller):
|
||||
# TODO: Currently hangs indefinitely.
|
||||
# Prepare mock objects.
|
||||
# controller.show_img_pipe.poll = MagicMock()
|
||||
# controller.show_img_pipe.recv = MagicMock()
|
||||
# controller.sloppy_stop = MagicMock()
|
||||
# controller.threads_pool.createThread = MagicMock()
|
||||
#
|
||||
# # Prepare mock returns
|
||||
# controller.show_img_pipe.poll.return_value = True
|
||||
#
|
||||
# # Test and make sure return is called
|
||||
# controller.stop_acquisition_flag = False
|
||||
# controller.execute("acquire", "continuous")
|
||||
# controller.execute("stop_acquire")
|
||||
pass
|
||||
|
||||
|
||||
def test_execute_exit(controller):
|
||||
# Essentially already tested by teardown of controller fixture.
|
||||
pass
|
||||
|
||||
|
||||
def test_execute_adaptive_optics(controller):
|
||||
controller.threads_pool.createThread = MagicMock()
|
||||
for command in [
|
||||
"flatten_mirror",
|
||||
"zero_mirror",
|
||||
"set_mirror",
|
||||
"set_mirror_from_wcs",
|
||||
]:
|
||||
controller.execute(command)
|
||||
controller.threads_pool.createThread.assert_called_with("model", ANY)
|
||||
args, kwargs = controller.threads_pool.createThread.call_args
|
||||
assert args[0] == "model"
|
||||
assert callable(args[1])
|
||||
|
||||
controller.execute("tony_wilson")
|
||||
controller.threads_pool.createThread.assert_called_with(
|
||||
"camera", controller.capture_image, args=("tony_wilson", "live")
|
||||
)
|
||||
|
||||
|
||||
def test_execute_random(controller):
|
||||
controller.threads_pool.createThread = MagicMock()
|
||||
for command in ["random1", "random2"]:
|
||||
controller.execute(command)
|
||||
controller.threads_pool.createThread.assert_called_with("model", ANY)
|
||||
args, kwargs = controller.threads_pool.createThread.call_args
|
||||
assert args[0] == "model"
|
||||
assert callable(args[1])
|
||||
|
||||
assert True
|
||||
|
||||
|
||||
def test_capture_image(controller):
|
||||
|
||||
count = 0
|
||||
|
||||
def get_image_id():
|
||||
nonlocal count
|
||||
count += 1
|
||||
if count >= 10:
|
||||
return "stop"
|
||||
return numpy.random.randint(0, 10)
|
||||
|
||||
microscope_name = controller.configuration["experiment"]["MicroscopeState"][
|
||||
"microscope_name"
|
||||
]
|
||||
width = controller.configuration["experiment"]["CameraParameters"][microscope_name][
|
||||
"img_x_pixels"
|
||||
]
|
||||
height = controller.configuration["experiment"]["CameraParameters"][
|
||||
microscope_name
|
||||
]["img_y_pixels"]
|
||||
images = numpy.random.rand(10, width, height)
|
||||
controller.data_buffer = images
|
||||
work_thread = MagicMock()
|
||||
work_thread.join = MagicMock()
|
||||
controller.threads_pool.createThread = MagicMock()
|
||||
controller.threads_pool.createThread.return_value = work_thread
|
||||
controller.show_img_pipe.recv = get_image_id
|
||||
controller.show_img_pipe.poll = MagicMock()
|
||||
controller.show_img_pipe.poll.return_value = False
|
||||
|
||||
# Deal with stop_acquire
|
||||
controller.sloppy_stop = MagicMock()
|
||||
controller.menu_controller.feature_id_val.set = MagicMock()
|
||||
|
||||
# Deal with camera view controller trying to launch a thread
|
||||
controller.camera_view_controller.is_displaying_image = MagicMock()
|
||||
controller.camera_view_controller.is_displaying_image.return_value = True
|
||||
|
||||
for command in ["acquire"]: # "autofocus"
|
||||
for mode in ["continuous", "live", "z-stack", "single"]:
|
||||
controller.capture_image(command, mode)
|
||||
|
||||
# Evaluate calls
|
||||
controller.threads_pool.createThread.assert_called_with("model", ANY)
|
||||
args, kwargs = controller.threads_pool.createThread.call_args
|
||||
assert args[0] == "model"
|
||||
assert callable(args[1])
|
||||
|
||||
controller.stop_acquisition_flag = True
|
||||
controller.threads_pool.createThread.reset_mock()
|
||||
|
||||
assert controller.acquire_bar_controller.framerate != 0
|
||||
assert controller.camera_setting_controller.framerate_widgets[
|
||||
"max_framerate"
|
||||
].get() != str(0)
|
||||
|
||||
assert True
|
||||
|
||||
|
||||
def test_launch_additional_microscope():
|
||||
# This looks awful to test...
|
||||
pass
|
||||
|
||||
|
||||
def test_move_stage(controller):
|
||||
pos_dict = {"x": 1, "y": 2.0, "z": 3.14, "theta": 400, "f": 5.01}
|
||||
controller.model.move_stage = MagicMock()
|
||||
controller.move_stage(pos_dict)
|
||||
controller.model.move_stage.assert_called_with(pos_dict)
|
||||
|
||||
|
||||
def test_stop_stage(controller):
|
||||
controller.model.stop_stage = MagicMock()
|
||||
controller.stop_stage()
|
||||
controller.model.stop_stage.assert_called_with()
|
||||
|
||||
|
||||
def test_update_stage_controller_silent(controller):
|
||||
pos_dict = {"x": 1, "y": 2.0, "z": 3.14, "theta": 400, "f": 5.01}
|
||||
controller.update_stage_controller_silent(pos_dict)
|
||||
for axis, value in pos_dict.items():
|
||||
assert (
|
||||
float(controller.stage_controller.widget_vals[axis].get()) == pos_dict[axis]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"acquisition_mode, sensor_mode, readout_direction, template_name, "
|
||||
"expected_template_name",
|
||||
[
|
||||
("live", "Normal", "", "Bidirectional", "Default"),
|
||||
("z-stack", "Normal", "", "Bidirectional", "Default"),
|
||||
("customized", "Normal", "", "Bidirectional", "Bidirectional"),
|
||||
("live", "Light-Sheet", "Top-To-Bottom", "Bidirectional", "Default"),
|
||||
("live", "Light-Sheet", "Bidirectional", "Bidirectional", "Bidirectional"),
|
||||
(
|
||||
"customized",
|
||||
"Light-Sheet",
|
||||
"Bidirectional",
|
||||
"Bidirectional",
|
||||
"Bidirectional",
|
||||
),
|
||||
("z-stack", "Light-Sheet", "Bidirectional", "Default", "Bidirectional"),
|
||||
("z-stack", "Light-Sheet", "Top-To-Bottom", "Default", "Default"),
|
||||
],
|
||||
)
|
||||
def test_waveform_template(
|
||||
controller,
|
||||
acquisition_mode,
|
||||
sensor_mode,
|
||||
readout_direction,
|
||||
template_name,
|
||||
expected_template_name,
|
||||
):
|
||||
controller.configuration["experiment"]["MicroscopeState"][
|
||||
"waveform_template"
|
||||
] = template_name
|
||||
controller.configuration["experiment"]["MicroscopeState"][
|
||||
"image_mode"
|
||||
] = acquisition_mode
|
||||
microscope_name = controller.configuration["experiment"]["MicroscopeState"][
|
||||
"microscope_name"
|
||||
]
|
||||
controller.configuration["experiment"]["CameraParameters"][microscope_name][
|
||||
"number_of_pixels"
|
||||
] = 10
|
||||
controller.populate_experiment_setting(in_initialize=True)
|
||||
|
||||
controller.camera_setting_controller.mode_widgets["Readout"].set(readout_direction)
|
||||
controller.camera_setting_controller.mode_widgets["Sensor"].set(sensor_mode)
|
||||
controller.update_experiment_setting()
|
||||
|
||||
assert (
|
||||
controller.configuration["experiment"]["MicroscopeState"]["waveform_template"]
|
||||
== expected_template_name
|
||||
)
|
||||
|
||||
assert True
|
||||
63
test/log_files/test_log_functions.py
Normal file
63
test/log_files/test_log_functions.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# 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.
|
||||
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("logging_configuration", ["logging.yml"])
|
||||
@pytest.mark.parametrize("logging_path", [None, Path("./")])
|
||||
def test_log_setup(logging_configuration, logging_path):
|
||||
from datetime import datetime
|
||||
|
||||
from navigate.log_files.log_functions import log_setup
|
||||
from navigate.config.config import get_navigate_path
|
||||
|
||||
time = datetime.now()
|
||||
time_stamp = Path(
|
||||
"%s-%s-%s-%s%s"
|
||||
% (
|
||||
f"{time.year:04d}",
|
||||
f"{time.month:02d}",
|
||||
f"{time.day:02d}",
|
||||
f"{time.hour:02d}",
|
||||
f"{time.minute:02d}",
|
||||
)
|
||||
)
|
||||
|
||||
if logging_path is None:
|
||||
logging_path = Path.joinpath(Path(get_navigate_path()), "logs")
|
||||
todays_path = Path.joinpath(logging_path, time_stamp)
|
||||
|
||||
log_setup(logging_configuration, logging_path, queue=False)
|
||||
|
||||
assert Path.joinpath(todays_path, "debug.log").is_file()
|
||||
0
test/model/__init__.py
Normal file
0
test/model/__init__.py
Normal file
0
test/model/analysis/__init__.py
Normal file
0
test/model/analysis/__init__.py
Normal file
112
test/model/analysis/test_boundary_detect.py
Normal file
112
test/model/analysis/test_boundary_detect.py
Normal file
@@ -0,0 +1,112 @@
|
||||
# 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 math
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def im_circ(r=1, N=128):
|
||||
X, Y = np.meshgrid(range(-N // 2, N // 2), range(-N // 2, N // 2))
|
||||
return (X * X + Y * Y) < r * r
|
||||
|
||||
|
||||
def test_has_tissue():
|
||||
from navigate.model.analysis.boundary_detect import has_tissue
|
||||
|
||||
for _ in range(100):
|
||||
N = 2 ** np.random.randint(5, 9)
|
||||
r = np.random.randint(math.ceil(0.2 * N), int(0.4 * N))
|
||||
ds = np.random.randint(1, 6)
|
||||
|
||||
print(N, r, ds)
|
||||
|
||||
im = im_circ(r, N) * 1001
|
||||
|
||||
mu, sig = 100 * np.random.rand() + 1, 10 * np.random.rand() + 1
|
||||
print(mu, sig)
|
||||
offsets = [None, np.ones((N, N)) * mu]
|
||||
variances = [None, np.ones((N, N)) * sig]
|
||||
|
||||
for off, var in zip(offsets, variances):
|
||||
assert has_tissue(im, 0, 0, N, off, var) and not has_tissue(
|
||||
im, 0, 0, N // 2 - r, off, var
|
||||
)
|
||||
|
||||
|
||||
def test_find_tissue_boundary_2d():
|
||||
from skimage.transform import downscale_local_mean
|
||||
|
||||
from navigate.model.analysis.boundary_detect import find_tissue_boundary_2d
|
||||
|
||||
for _ in range(100):
|
||||
N = 2 ** np.random.randint(5, 9)
|
||||
r = np.random.randint(1, int(0.4 * N))
|
||||
ds = np.random.randint(1, 6)
|
||||
|
||||
print(N, r, ds)
|
||||
|
||||
im = im_circ(r, N)
|
||||
b = find_tissue_boundary_2d(im, ds)
|
||||
b = np.vstack([x for x in b if x is not None])
|
||||
|
||||
idx_x, idx_y = np.where(downscale_local_mean(im, (ds, ds)))
|
||||
iixy = (np.unique(idx_x)[:, None] == idx_x[None, :]) * idx_y
|
||||
low, high = idx_y[np.argmax(iixy != 0, 1)], np.max(iixy, 1)
|
||||
|
||||
np.testing.assert_equal(b, np.vstack([low, high]).T)
|
||||
|
||||
|
||||
def test_binary_detect():
|
||||
from navigate.model.analysis.boundary_detect import (
|
||||
find_tissue_boundary_2d,
|
||||
binary_detect,
|
||||
)
|
||||
|
||||
for _ in range(100):
|
||||
N = 2 ** np.random.randint(5, 9)
|
||||
r = np.random.randint(1, int(0.4 * N))
|
||||
ds = np.random.randint(1, 6)
|
||||
|
||||
print(N, r, ds)
|
||||
|
||||
im = im_circ(r, N)
|
||||
b = find_tissue_boundary_2d(im, ds)
|
||||
|
||||
assert binary_detect(im * 1001, b, ds) == b
|
||||
|
||||
|
||||
def test_map_boundary():
|
||||
from navigate.model.analysis.boundary_detect import map_boundary
|
||||
|
||||
assert map_boundary([[1, 2]]) == [(0, 1), (0, 2)]
|
||||
assert map_boundary([None, [1, 2]]) == [(1, 1), (1, 2)]
|
||||
assert map_boundary([None, [1, 2], None]) == [(1, 1), (1, 2)]
|
||||
56
test/model/analysis/test_camera.py
Normal file
56
test/model/analysis/test_camera.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip("volatile")
|
||||
def test_compute_scmos_offset_and_variance_map():
|
||||
from navigate.model.analysis.camera import compute_scmos_offset_and_variance_map
|
||||
|
||||
mu, sig = 100 * np.random.rand() + 1, 100 * np.random.rand() + 1
|
||||
im = sig * np.random.randn(256, 256, 256) + mu
|
||||
|
||||
offset, variance = compute_scmos_offset_and_variance_map(im)
|
||||
|
||||
print(mu, sig)
|
||||
# TODO: 1 is a bit high?
|
||||
np.testing.assert_allclose(offset, mu, rtol=1)
|
||||
np.testing.assert_allclose(variance, sig * sig, rtol=1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("local", [True, False])
|
||||
def test_compute_flatfield_map(local):
|
||||
from navigate.model.analysis.camera import compute_flatfield_map
|
||||
|
||||
image = np.ones((256, 256))
|
||||
offset = np.zeros((256, 256))
|
||||
ffmap = compute_flatfield_map(image, offset, local)
|
||||
|
||||
np.testing.assert_allclose(ffmap, 0.5)
|
||||
|
||||
|
||||
def test_compute_noise_sigma():
|
||||
from navigate.model.analysis.camera import compute_noise_sigma
|
||||
|
||||
Fn = np.random.rand()
|
||||
qe = np.random.rand()
|
||||
S = np.random.rand(256, 256)
|
||||
Ib = np.random.rand()
|
||||
Nr = np.random.rand()
|
||||
M = np.random.rand()
|
||||
sigma = compute_noise_sigma(Fn=Fn, qe=qe, S=S, Ib=Ib, Nr=Nr, M=M)
|
||||
sigma_true = np.sqrt(Fn * Fn * qe * (S + Ib) + (Nr / M) ** 2)
|
||||
|
||||
np.testing.assert_allclose(sigma, sigma_true)
|
||||
|
||||
|
||||
def test_compute_signal_to_noise():
|
||||
from navigate.model.analysis.camera import compute_signal_to_noise
|
||||
|
||||
A = np.random.rand() * 100 + 10
|
||||
image = A * np.ones((256, 256))
|
||||
offset = np.zeros((256, 256))
|
||||
variance = 3 * A * A * np.ones((256, 256))
|
||||
|
||||
snr = compute_signal_to_noise(image, offset, variance)
|
||||
|
||||
np.testing.assert_allclose(snr, 0.5, rtol=0.2)
|
||||
0
test/model/concurrency/__init__.py
Normal file
0
test/model/concurrency/__init__.py
Normal file
758
test/model/concurrency/test_concurrency_tools.py
Normal file
758
test/model/concurrency/test_concurrency_tools.py
Normal file
@@ -0,0 +1,758 @@
|
||||
# Standard Library Imports
|
||||
import threading
|
||||
from multiprocessing import shared_memory
|
||||
|
||||
# Third Party Imports
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
# Local Imports
|
||||
from navigate.model.concurrency.concurrency_tools import (
|
||||
ObjectInSubprocess,
|
||||
ResultThread,
|
||||
CustodyThread,
|
||||
_WaitingList,
|
||||
SharedNDArray,
|
||||
)
|
||||
|
||||
|
||||
def time_it(
|
||||
n_loops, func, args=None, kwargs=None, fail=True, timeout_us=None, name=None
|
||||
):
|
||||
"""Useful for testing the performance of a specific function.
|
||||
|
||||
Args:
|
||||
- n_loops <int> | number of loops to test
|
||||
- func <callable> | function/method to test
|
||||
- args/kwargs | arguments to the function
|
||||
- fail <bool> | Allow the method to raise an exception?
|
||||
- timeout_us <int/float> | If the average duration exceeds this
|
||||
limit, raise a TimeoutError.
|
||||
- name <str> | formatted name for the progress bar.
|
||||
"""
|
||||
import time
|
||||
|
||||
try:
|
||||
from tqdm import tqdm
|
||||
except ImportError:
|
||||
tqdm = None # No progress bars :(
|
||||
if args is None:
|
||||
args = ()
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
if tqdm is not None:
|
||||
f = "{desc: <38}{n: 7d}-{bar:17}|[{rate_fmt}]"
|
||||
pb = tqdm(total=n_loops, desc=name, bar_format=f)
|
||||
start = time.perf_counter()
|
||||
for i in range(n_loops):
|
||||
if tqdm is not None:
|
||||
pb.update(1)
|
||||
try:
|
||||
func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
if fail:
|
||||
raise e
|
||||
else:
|
||||
pass
|
||||
end = time.perf_counter()
|
||||
if tqdm is not None:
|
||||
pb.close()
|
||||
time_per_loop_us = ((end - start) / n_loops) * 1e6
|
||||
if timeout_us is not None:
|
||||
if time_per_loop_us > timeout_us:
|
||||
name = func.__name__ if name is None else name
|
||||
raise TimeoutError(
|
||||
f"Timed out on {name}\n"
|
||||
f" args:{args}\n"
|
||||
f" kwargs: {kwargs}\n"
|
||||
f" Each loop took {time_per_loop_us:.2f} \u03BCs"
|
||||
f" (Allowed: {timeout_us:.2f} \u03BCs)"
|
||||
)
|
||||
return time_per_loop_us
|
||||
|
||||
|
||||
def test_subclassed_threading_types():
|
||||
r_th = ResultThread(target=lambda: 1)
|
||||
c_th = CustodyThread(target=lambda custody: 1)
|
||||
|
||||
assert isinstance(r_th, threading.Thread)
|
||||
assert isinstance(c_th, threading.Thread)
|
||||
assert isinstance(r_th, ResultThread)
|
||||
assert isinstance(c_th, ResultThread)
|
||||
assert isinstance(c_th, CustodyThread)
|
||||
|
||||
|
||||
def test_threadlike_behavior():
|
||||
th = ResultThread(target=lambda: 1)
|
||||
th.start()
|
||||
th.join()
|
||||
assert not th.is_alive()
|
||||
|
||||
|
||||
def test_new_start_behavior():
|
||||
th = ResultThread(target=lambda: 1)
|
||||
_th = th.start()
|
||||
assert isinstance(_th, ResultThread)
|
||||
assert th is _th
|
||||
|
||||
|
||||
def test_getting_result():
|
||||
th = ResultThread(target=lambda: 1).start()
|
||||
assert hasattr(th, "_return")
|
||||
th.join()
|
||||
assert th.get_result() == 1
|
||||
assert th.get_result() == 1, "Couldn't get result twice!"
|
||||
|
||||
|
||||
def test_passing_args_and_kwargs():
|
||||
def mirror(*args, **kwargs):
|
||||
return args, kwargs
|
||||
|
||||
a = (1,)
|
||||
k = dict(a=1)
|
||||
th = ResultThread(target=mirror, args=a, kwargs=k).start()
|
||||
_a, _k = th.get_result()
|
||||
assert a == _a, f"{a} != {_a}"
|
||||
assert k == _k, f"{k} != {_k}"
|
||||
|
||||
|
||||
# def test_catching_exception():
|
||||
# def e():
|
||||
# raise ValueError("Don't worry, this exception occurred on purpose!")
|
||||
# th = ResultThread(target=e).start()
|
||||
# th.join() # join won't reraise exception in main thread
|
||||
# assert hasattr(th, 'exc_value')
|
||||
# try:
|
||||
# th.get_result()
|
||||
# except ValueError:
|
||||
# pass
|
||||
# else:
|
||||
# raise AssertionError("We didn't get the exception we expected...")
|
||||
# # We should be able to reraise this exception as long as we have
|
||||
# # a reference to it:
|
||||
# try:
|
||||
# th.get_result()
|
||||
# except ValueError:
|
||||
# pass
|
||||
# else:
|
||||
# raise AssertionError("We didn't get the exception we expected...")
|
||||
|
||||
|
||||
def test_custody_thread_target_args():
|
||||
# CustodyThread accepts a target with a kwarg 'custody'
|
||||
def custody_f(custody=None):
|
||||
return 1
|
||||
|
||||
CustodyThread(target=custody_f, first_resource=None).start()
|
||||
# CustodyThread accepts a target with a positional arg 'custody'
|
||||
def custody_f(custody):
|
||||
return 1
|
||||
|
||||
CustodyThread(target=custody_f, first_resource=None).start()
|
||||
|
||||
# CustodyThread will otherwise raise a ValueError
|
||||
def f():
|
||||
return 1
|
||||
|
||||
try:
|
||||
CustodyThread(target=f, first_resource=None).start()
|
||||
except ValueError:
|
||||
pass # We expect this
|
||||
else:
|
||||
raise AssertionError("We didn't get the exception we expected...")
|
||||
|
||||
def f(a):
|
||||
return 1
|
||||
|
||||
try:
|
||||
CustodyThread(target=f, first_resource=None).start()
|
||||
except ValueError:
|
||||
pass # We expect this
|
||||
else:
|
||||
raise AssertionError("We didn't get the exception we expected...")
|
||||
|
||||
def f(a=1):
|
||||
return 1
|
||||
|
||||
try:
|
||||
CustodyThread(target=f, first_resource=None).start()
|
||||
except ValueError:
|
||||
pass # We expect this
|
||||
else:
|
||||
raise AssertionError("We didn't get the exception we expected...")
|
||||
|
||||
|
||||
def test_providing_first_resource():
|
||||
resource = _WaitingList()
|
||||
mutable_variables = {"step": 0, "progress": 0}
|
||||
|
||||
def f(custody):
|
||||
while mutable_variables["step"] == 0:
|
||||
pass
|
||||
custody.switch_from(None, resource)
|
||||
mutable_variables["progress"] += 1
|
||||
while mutable_variables["step"] == 1:
|
||||
pass
|
||||
custody.switch_from(resource, None)
|
||||
mutable_variables["progress"] += 1
|
||||
return
|
||||
|
||||
try:
|
||||
th = CustodyThread(target=f, first_resource=resource).start()
|
||||
assert hasattr(th, "custody"), "Should have a custody attribute."
|
||||
assert not th.custody.has_custody, "Should not have custody yet."
|
||||
assert th.custody.target_resource is resource, "Should be in line."
|
||||
# Make target thread progress one step and acquire custody
|
||||
mutable_variables["step"] = 1
|
||||
while mutable_variables["progress"] == 0:
|
||||
pass # Wait for thread
|
||||
assert th.custody.has_custody, "Should have gotten custody."
|
||||
assert th.custody.target_resource is resource
|
||||
# Make target thread progress one step, release custody, and exit
|
||||
mutable_variables["step"] = 2
|
||||
while mutable_variables["progress"] == 1:
|
||||
pass # Wait for thread
|
||||
assert not th.custody.has_custody
|
||||
assert th.custody.target_resource is None
|
||||
th.join()
|
||||
finally: # if anything goes wrong, make sure the thread exits
|
||||
mutable_variables["step"] = -1
|
||||
|
||||
|
||||
def test_subclassed_numpy_array_types():
|
||||
a = SharedNDArray(shape=(1,), dtype="uint8")
|
||||
assert isinstance(a, SharedNDArray)
|
||||
assert isinstance(a, np.ndarray)
|
||||
assert type(a) is SharedNDArray, type(a)
|
||||
assert type(a) is not np.ndarray
|
||||
assert hasattr(a, "shared_memory")
|
||||
assert isinstance(a.shared_memory, shared_memory.SharedMemory)
|
||||
|
||||
del a
|
||||
|
||||
|
||||
def test_ndarraylike_behavior():
|
||||
"""Testing if we broke how an ndarray is supposed to behave."""
|
||||
ri = np.random.randint # Just to get short lines
|
||||
original_dimensions = (3, 3, 3, 256, 256)
|
||||
a = SharedNDArray(shape=original_dimensions, dtype="uint8")
|
||||
c = ri(0, 255, original_dimensions, dtype="uint8")
|
||||
a[:] = c # Fill 'a' with 'c's random values
|
||||
# A slice should still share memory
|
||||
view_by_slice = a[:1, 2:3, ..., :10, 100:-100]
|
||||
assert isinstance(a, SharedNDArray)
|
||||
assert type(a) is type(view_by_slice)
|
||||
assert np.may_share_memory(a, view_by_slice)
|
||||
assert a.shared_memory is view_by_slice.shared_memory
|
||||
|
||||
# Some functions should not return a SharedNDArray
|
||||
b = a.sum(axis=-1)
|
||||
assert isinstance(b, np.ndarray), type(b)
|
||||
assert not isinstance(b, SharedNDArray)
|
||||
|
||||
b = a + 1
|
||||
assert isinstance(b, np.ndarray), type(b)
|
||||
assert not isinstance(b, SharedNDArray), type(b)
|
||||
|
||||
b = a.sum()
|
||||
assert np.isscalar(b)
|
||||
assert not isinstance(b, SharedNDArray)
|
||||
|
||||
del a
|
||||
|
||||
|
||||
def test_serialization():
|
||||
"""Testing serializing/deserializing a SharedNDArray"""
|
||||
import pickle
|
||||
|
||||
ri = np.random.randint # Just to get short lines
|
||||
original_dimensions = (3, 3, 3, 256, 256)
|
||||
a = SharedNDArray(shape=original_dimensions, dtype="uint8")
|
||||
c = ri(0, 255, original_dimensions, dtype="uint8")
|
||||
a[:] = c # Fill 'a' with 'c's random values
|
||||
view_by_slice = a[:1, 2:3, ..., :10, 100:-100]
|
||||
view_of_a_view = view_by_slice[..., 1:, 10:-10:3]
|
||||
|
||||
_a = pickle.loads(pickle.dumps(a))
|
||||
assert _a.sum() == a.sum()
|
||||
assert np.array_equal(a, _a)
|
||||
|
||||
_view_by_slice = pickle.loads(pickle.dumps(view_by_slice))
|
||||
assert _view_by_slice.sum() == view_by_slice.sum()
|
||||
assert np.array_equal(_view_by_slice, view_by_slice)
|
||||
|
||||
_view_of_a_view = pickle.loads(pickle.dumps(view_of_a_view))
|
||||
assert _view_of_a_view.sum() == view_of_a_view.sum()
|
||||
assert np.array_equal(_view_of_a_view, view_of_a_view)
|
||||
|
||||
del a
|
||||
|
||||
|
||||
def test_viewcasting():
|
||||
a = SharedNDArray(shape=(1,))
|
||||
v = a.view(np.ndarray)
|
||||
assert isinstance(v, np.ndarray), type(v)
|
||||
assert not isinstance(v, SharedNDArray), type(v)
|
||||
a = np.zeros(shape=(1,))
|
||||
try:
|
||||
v = a.view(SharedNDArray)
|
||||
del a
|
||||
except ValueError:
|
||||
pass # we expected this
|
||||
else:
|
||||
raise AssertionError("We didn't raise the correct exception!")
|
||||
|
||||
|
||||
def test_auto_unlinking_memory():
|
||||
import gc
|
||||
|
||||
a = SharedNDArray(shape=(1,))
|
||||
name = str(a.shared_memory.name) # Really make sure we don't get a ref
|
||||
del a
|
||||
gc.collect() # Now memory should be unlinked
|
||||
try:
|
||||
shared_memory.SharedMemory(name=name)
|
||||
except FileNotFoundError:
|
||||
pass # This is the error we expected if the memory was unlinked.
|
||||
else:
|
||||
raise AssertionError("We didn't raise the correct exception!")
|
||||
|
||||
# Views should prevent deallocation
|
||||
a = SharedNDArray(shape=(10,))
|
||||
v = a[:5]
|
||||
name = str(a.shared_memory.name) # Really make sure we don't get a ref
|
||||
del a
|
||||
gc.collect()
|
||||
v.sum() # Should still be able to interact with 'v'
|
||||
shared_memory.SharedMemory(name=name) # Memory not unlinked yet
|
||||
del v
|
||||
gc.collect() # Now memory should be unlinked
|
||||
try:
|
||||
shared_memory.SharedMemory(name=name)
|
||||
except FileNotFoundError:
|
||||
pass # This is the error we expected if the memory was unlinked.
|
||||
else:
|
||||
raise AssertionError("We didn't raise the correct exception!")
|
||||
|
||||
|
||||
def test_accessing_unlinked_memory_during_deserialization():
|
||||
import pickle
|
||||
|
||||
original_dimensions = (3, 3, 3, 256, 256)
|
||||
a = SharedNDArray(shape=original_dimensions, dtype="uint8")
|
||||
string_of_a = pickle.dumps(a)
|
||||
del a
|
||||
try:
|
||||
_a = pickle.loads(string_of_a)
|
||||
except FileNotFoundError:
|
||||
pass # We expected this error
|
||||
else:
|
||||
raise AssertionError("Did not get the error we expected")
|
||||
|
||||
|
||||
def test_accessing_unlinked_memory_in_subprocess():
|
||||
p = ObjectInSubprocess(TestClass)
|
||||
original_dimensions = (3, 3, 3, 256, 256)
|
||||
a = SharedNDArray(shape=original_dimensions, dtype="uint8")
|
||||
p.store_array(a)
|
||||
p.a.sum()
|
||||
try:
|
||||
# close and unlink the memory
|
||||
del a
|
||||
# try to access the memory
|
||||
p.a.sum()
|
||||
except FileNotFoundError:
|
||||
pass # we expected this error
|
||||
else:
|
||||
import os
|
||||
|
||||
if os.name == "nt":
|
||||
# This is allowed on Windows. Windows will keep memory
|
||||
# allocated until all references have been lost from every
|
||||
# process.
|
||||
pass
|
||||
else:
|
||||
# However, on posix systems, we expect the system to unlink
|
||||
# the memory once the process that originally allocated it
|
||||
# loses all references to the array.
|
||||
raise AssertionError("Did not get the error we expected")
|
||||
|
||||
@pytest.mark.flaky(reruns=3, reruns_delay=2)
|
||||
def test_serializing_and_deserializing():
|
||||
"""Test serializing/deserializing arrays with random shapes, dtypes, and
|
||||
slicing operators.
|
||||
"""
|
||||
for i in range(500):
|
||||
_trial_slicing_of_shared_array()
|
||||
|
||||
|
||||
def _trial_slicing_of_shared_array():
|
||||
import pickle
|
||||
|
||||
ri = np.random.randint # Just to get short lines
|
||||
dtype = np.dtype(
|
||||
np.random.choice([int, np.uint8, np.uint16, float, np.float32, np.float64])
|
||||
)
|
||||
original_dimensions = tuple(ri(2, 100) for d in range(ri(2, 5)))
|
||||
slicer = tuple(
|
||||
slice(ri(0, a // 2), ri(0, a // 2) * -1, ri(1, min(6, a)))
|
||||
for a in original_dimensions
|
||||
)
|
||||
a = SharedNDArray(shape=original_dimensions, dtype=dtype)
|
||||
a.fill(0)
|
||||
b = a[slicer] # Should be a view
|
||||
b.fill(1)
|
||||
expected_total = int(b.sum())
|
||||
reloaded_total = int(pickle.loads(pickle.dumps(b)).sum())
|
||||
assert (
|
||||
expected_total == reloaded_total
|
||||
), f"Failed {dtype.name}/{original_dimensions}/{slicer}"
|
||||
del a
|
||||
|
||||
|
||||
# class TestObjectInSubprocess(unittest.TestCase):
|
||||
class TestClass:
|
||||
"""Toy class that can be put in a subprocess for testing."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
for i, a in enumerate(args):
|
||||
setattr(self, f"arg_{i}", a)
|
||||
|
||||
def printing_method(self, *args, **kwargs):
|
||||
print(*args, **kwargs)
|
||||
|
||||
def get_attribute(self, attr):
|
||||
return getattr(self, attr, None)
|
||||
|
||||
def mirror(self, *args, **kwargs):
|
||||
return args, kwargs
|
||||
|
||||
def black_hole(self, *args, **kwargs):
|
||||
return None
|
||||
|
||||
def get_shape_of_numpy_array(self, ndarray):
|
||||
return ndarray.shape
|
||||
|
||||
def fill_and_return_array(self, a, i=1):
|
||||
a.fill(i)
|
||||
return a
|
||||
|
||||
def sleep(self, seconds):
|
||||
import time
|
||||
|
||||
time.sleep(seconds)
|
||||
|
||||
def return_slice(self, a, *args):
|
||||
return a[args]
|
||||
|
||||
def sum(self, a):
|
||||
return a.sum()
|
||||
|
||||
def store_array(self, a):
|
||||
self.a = a
|
||||
|
||||
def nested_method(self, crash=False):
|
||||
self._nested_method(crash)
|
||||
|
||||
def _nested_method(self, crash):
|
||||
if crash:
|
||||
raise ValueError("This error was supposed to be raised")
|
||||
|
||||
|
||||
# def test_create_and_close_object_in_subprocess():
|
||||
# import gc
|
||||
# p = ObjectInSubprocess(TestClass)
|
||||
# child_process = p._.child_process
|
||||
# del p
|
||||
# gc.collect()
|
||||
# child_process.join(timeout=1)
|
||||
# assert not child_process.is_alive()
|
||||
|
||||
# # Other objects to finalize can cause some strange behavior
|
||||
# weakref.finalize({1,}, _dummy_function)
|
||||
# p = ObjectInSubprocess(TestClass)
|
||||
# child_process = p._.child_process
|
||||
# # Trigger ref count increase (ref in handled exception tb)
|
||||
# hasattr(p, 'attribute_that_does_not_exist')
|
||||
# del p
|
||||
# gc.collect()
|
||||
# child_process.join(timeout=1)
|
||||
# assert not child_process.is_alive()
|
||||
|
||||
|
||||
def test_create_and_close_object_in_subprocess():
|
||||
import gc
|
||||
|
||||
p = ObjectInSubprocess(TestClass)
|
||||
dummy_namespace = p._
|
||||
del p
|
||||
gc.collect()
|
||||
dummy_namespace.child_process.join(timeout=1)
|
||||
assert not dummy_namespace.child_process.is_alive()
|
||||
|
||||
|
||||
def test_passing_normal_numpy_array():
|
||||
a = np.zeros((3, 3), dtype=int)
|
||||
p = ObjectInSubprocess(TestClass)
|
||||
(_a,), _ = p.mirror(a)
|
||||
assert np.array_equal(a, _a), f"{a} != {_a} ({a.dtype}|{_a.dtype}"
|
||||
|
||||
del p
|
||||
|
||||
|
||||
def test_passing_modifying_and_retrieving_shared_array():
|
||||
a = SharedNDArray(shape=(10, 10), dtype=int)
|
||||
p = ObjectInSubprocess(TestClass)
|
||||
b = p.fill_and_return_array(a, 1)
|
||||
assert np.array_equal(a, b)
|
||||
|
||||
del a
|
||||
del p
|
||||
|
||||
|
||||
def test_attribute_access():
|
||||
p = ObjectInSubprocess(TestClass, "attribute", x=4)
|
||||
assert p.x == 4
|
||||
assert getattr(p, "arg_0") == "attribute"
|
||||
try:
|
||||
p.z
|
||||
del p
|
||||
except Exception as e: # Get __this__ specific error
|
||||
print("Expected attribute error handled by parent process:\n ", e)
|
||||
else:
|
||||
raise AssertionError("Did not get the error we expected")
|
||||
|
||||
|
||||
def test_printing_in_child_processes():
|
||||
a = ObjectInSubprocess(TestClass)
|
||||
b = ObjectInSubprocess(TestClass)
|
||||
expected_output = ""
|
||||
b.printing_method("Hello")
|
||||
expected_output += "Hello\n"
|
||||
a.printing_method("Hello from subprocess a.")
|
||||
expected_output += "Hello from subprocess a.\n"
|
||||
b.printing_method("Hello from subprocess b.")
|
||||
expected_output += "Hello from subprocess b.\n"
|
||||
a.printing_method("Hello world", end=", ", flush=True)
|
||||
expected_output += "Hello world, "
|
||||
b.printing_method("Hello world!", end="", flush=True)
|
||||
expected_output += "Hello world!"
|
||||
del a
|
||||
del b
|
||||
return expected_output
|
||||
|
||||
|
||||
def test_setting_attribute_of_object_in_subprocess():
|
||||
p = ObjectInSubprocess(TestClass)
|
||||
try:
|
||||
hasattr(p, "z")
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
# We already have z
|
||||
assert False
|
||||
p.z = 10
|
||||
assert hasattr(p, "z")
|
||||
assert p.z == 10
|
||||
setattr(p, "z", 100)
|
||||
assert p.z == 100
|
||||
assert p.get_attribute("z") == 100
|
||||
|
||||
del p
|
||||
|
||||
|
||||
def test_array_values_after_passing_to_subprocess():
|
||||
p = ObjectInSubprocess(TestClass)
|
||||
a = SharedNDArray(shape=(10, 1))
|
||||
a[:] = 1
|
||||
assert a.sum() == p.sum(a)
|
||||
|
||||
del p
|
||||
del a
|
||||
|
||||
|
||||
def test_object_in_subprocess_overhead():
|
||||
"""Test the overhead of accessing ObjectInSubprocess methods/attributes.
|
||||
|
||||
TODO: 200 is supposed to be 100 and 400 is supposed to be 200 us. Why is
|
||||
this so slow?
|
||||
"""
|
||||
print("Performance summary:")
|
||||
n_loops = 10000
|
||||
p = ObjectInSubprocess(TestClass, x=4)
|
||||
t = time_it(n_loops, lambda: p.x, timeout_us=200, name="Attribute access") # noqa
|
||||
print(f" {t:.2f} \u03BCs per get-attribute.")
|
||||
t = time_it(
|
||||
n_loops,
|
||||
lambda: setattr(p, "x", 5), # noqa
|
||||
timeout_us=200,
|
||||
name="Attribute setting",
|
||||
)
|
||||
print(f" {t:.2f} \u03BCs per set-attribute.")
|
||||
t = time_it(
|
||||
n_loops, lambda: p.z, fail=False, timeout_us=400, name="Attribute error" # noqa
|
||||
)
|
||||
print(f" {t:.2f} \u03BCs per parent-handled exception.")
|
||||
t = time_it(n_loops, p.mirror, timeout_us=200, name="Trivial method call")
|
||||
print(f" {t:.2f} \u03BCs per trivial method call.")
|
||||
_test_passing_array_performance()
|
||||
|
||||
del p
|
||||
|
||||
|
||||
def _test_passing_array_performance():
|
||||
"""Test the performance of passing random arrays to/from
|
||||
ObjectInSubprocess.
|
||||
"""
|
||||
from itertools import product
|
||||
|
||||
pass_by = ["reference", "serialization"]
|
||||
methods = ["black_hole", "mirror"]
|
||||
shapes = [(10, 10), (1000, 1000)]
|
||||
for s, pb, m in product(shapes, pass_by, methods):
|
||||
_test_array_passing(s, pb, m, "uint8", 1000)
|
||||
|
||||
|
||||
def _test_array_passing(shape, pass_by, method_name, dtype, n_loops):
|
||||
dtype = np.dtype(dtype)
|
||||
direction = "<->" if method_name == "mirror" else "->"
|
||||
name = f"{shape} array {direction} {pass_by}"
|
||||
shm_obj = ObjectInSubprocess(TestClass)
|
||||
if pass_by == "reference":
|
||||
a = SharedNDArray(shape, dtype=dtype)
|
||||
timeout_us = 5e3
|
||||
elif pass_by == "serialization":
|
||||
a = np.zeros(shape=shape, dtype=dtype)
|
||||
timeout_us = 1e6
|
||||
func = getattr(shm_obj, method_name)
|
||||
t_per_loop = time_it(n_loops, func, (a,), timeout_us=timeout_us, name=name)
|
||||
print(f" {t_per_loop:.2f} \u03BCs per {name}")
|
||||
|
||||
|
||||
def test_lock_with_waitlist():
|
||||
"""Test that CustodyThreads stay in order while using resources.
|
||||
ObjectsInSubprocess are just mocked as _WaitingList objects.
|
||||
"""
|
||||
import time
|
||||
|
||||
try:
|
||||
from tqdm import tqdm
|
||||
except ImportError:
|
||||
tqdm = None # No progress bars :(
|
||||
|
||||
camera_lock = _WaitingList()
|
||||
display_lock = _WaitingList()
|
||||
|
||||
num_snaps = 100
|
||||
usage_record = {"camera": [], "display": []}
|
||||
if tqdm is not None:
|
||||
pbars = {
|
||||
resource: tqdm(
|
||||
total=num_snaps,
|
||||
bar_format="{desc: <30}{n: 3d}-{bar:45}|",
|
||||
desc=f"Threads waiting on {resource}",
|
||||
)
|
||||
for resource in usage_record.keys()
|
||||
}
|
||||
|
||||
def snap(i, custody):
|
||||
if tqdm is not None:
|
||||
pbars["camera"].update(1)
|
||||
if tqdm is not None:
|
||||
pbars["camera"].refresh()
|
||||
# We're already in line for the camera; wait until we're first
|
||||
custody.switch_from(None, camera_lock)
|
||||
# Pretend to use the resource
|
||||
time.sleep(0.02)
|
||||
usage_record["camera"].append(i)
|
||||
|
||||
custody.switch_from(camera_lock, display_lock, wait=False)
|
||||
if tqdm is not None:
|
||||
pbars["camera"].update(-1)
|
||||
if tqdm is not None:
|
||||
pbars["camera"].refresh()
|
||||
if tqdm is not None:
|
||||
pbars["display"].update(1)
|
||||
if tqdm is not None:
|
||||
pbars["display"].refresh()
|
||||
custody._wait_in_line()
|
||||
# Pretend to use the resource
|
||||
time.sleep(0.05)
|
||||
usage_record["display"].append(i)
|
||||
# Move to the next resource
|
||||
custody.switch_from(display_lock, None)
|
||||
if tqdm is not None:
|
||||
pbars["display"].update(-1)
|
||||
if tqdm is not None:
|
||||
pbars["display"].refresh()
|
||||
return None
|
||||
|
||||
threads = []
|
||||
for i in range(num_snaps):
|
||||
threads.append(
|
||||
CustodyThread(target=snap, first_resource=camera_lock, args=(i,)).start()
|
||||
)
|
||||
for th in threads:
|
||||
th.get_result()
|
||||
|
||||
if tqdm is not None:
|
||||
for pb in pbars.values():
|
||||
pb.close()
|
||||
|
||||
assert usage_record["camera"] == list(range(num_snaps))
|
||||
assert usage_record["display"] == list(range(num_snaps))
|
||||
|
||||
|
||||
def test_incorrect_thread_management():
|
||||
"""Test accessing an object in a subprocess from multiple threads
|
||||
without using a custody object. This is expected to raise a
|
||||
RunTimeError.
|
||||
"""
|
||||
p = ObjectInSubprocess(TestClass)
|
||||
exceptions = []
|
||||
|
||||
def unsafe_fn():
|
||||
try:
|
||||
p.sleep(0.1) # noqa
|
||||
except RuntimeError: # Should raise this sometimes
|
||||
exceptions.append(1)
|
||||
|
||||
threads = [threading.Thread(target=unsafe_fn) for i in range(20)]
|
||||
for th in threads:
|
||||
th.start()
|
||||
for th in threads:
|
||||
th.join()
|
||||
assert len(exceptions) == 19, "This should have raised some exceptions."
|
||||
|
||||
del p
|
||||
|
||||
|
||||
def test_sending_shared_arrays():
|
||||
"""Testing sending a SharedNDArray to a ObjectInSubprocess."""
|
||||
|
||||
p = ObjectInSubprocess(TestClass)
|
||||
original_dimensions = (3, 3, 3, 256, 256)
|
||||
a = SharedNDArray(shape=original_dimensions, dtype="uint8")
|
||||
|
||||
(_a,), _ = p.mirror(a)
|
||||
assert isinstance(_a, SharedNDArray)
|
||||
assert _a.shared_memory.name == a.shared_memory.name
|
||||
assert _a.offset == a.offset
|
||||
assert _a.strides == a.strides
|
||||
|
||||
_a = p.fill_and_return_array(a, 1)
|
||||
assert isinstance(_a, SharedNDArray)
|
||||
assert _a.shared_memory.name == a.shared_memory.name
|
||||
assert _a.offset == a.offset
|
||||
assert _a.strides == a.strides
|
||||
|
||||
_a = p.return_slice(a, slice(1, -1), ..., slice(3, 100, 10))
|
||||
assert isinstance(_a, SharedNDArray)
|
||||
assert _a.shared_memory.name == a.shared_memory.name
|
||||
assert _a.offset != a.offset
|
||||
assert _a.strides != a.strides
|
||||
|
||||
del p
|
||||
del a
|
||||
269
test/model/data_sources/test_bdv_data_source.py
Normal file
269
test/model/data_sources/test_bdv_data_source.py
Normal file
@@ -0,0 +1,269 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
import numpy as np
|
||||
import h5py
|
||||
|
||||
from navigate.tools.file_functions import delete_folder
|
||||
|
||||
|
||||
def recurse_dtype(group):
|
||||
for key, subgroup in group.items():
|
||||
subgroup_type = type(subgroup)
|
||||
if subgroup_type == h5py._hl.group.Group:
|
||||
recurse_dtype(subgroup)
|
||||
elif subgroup_type == h5py._hl.dataset.Dataset:
|
||||
if key == "resolutions":
|
||||
assert subgroup.dtype == "float64"
|
||||
elif key == "subdivisions":
|
||||
assert subgroup.dtype == "int32"
|
||||
elif key == "cells":
|
||||
assert subgroup.dtype == "uint16"
|
||||
else:
|
||||
print("Unknown how to handle:", key, subgroup_type)
|
||||
|
||||
|
||||
def bdv_ds(fn, multiposition, per_stack, z_stack, stop_early, size):
|
||||
from test.model.dummy import DummyModel
|
||||
from navigate.model.data_sources.bdv_data_source import BigDataViewerDataSource
|
||||
|
||||
print(
|
||||
f"Conditions are multiposition: {multiposition} per_stack: {per_stack} "
|
||||
f"z_stack: {z_stack} stop_early: {stop_early}"
|
||||
)
|
||||
|
||||
# Set up model with a random number of z-steps to modulate the shape
|
||||
model = DummyModel()
|
||||
z_steps = np.random.randint(1, 3)
|
||||
timepoints = np.random.randint(1, 3)
|
||||
|
||||
x_size, y_size = size
|
||||
model.configuration["experiment"]["CameraParameters"]["x_pixels"] = x_size
|
||||
model.configuration["experiment"]["CameraParameters"]["y_pixels"] = y_size
|
||||
model.img_width = x_size
|
||||
model.img_height = y_size
|
||||
|
||||
model.configuration["experiment"]["MicroscopeState"]["image_mode"] = (
|
||||
"z-stack" if z_stack else "single"
|
||||
)
|
||||
model.configuration["experiment"]["MicroscopeState"]["number_z_steps"] = z_steps
|
||||
model.configuration["experiment"]["MicroscopeState"][
|
||||
"is_multiposition"
|
||||
] = multiposition
|
||||
model.configuration["experiment"]["MicroscopeState"]["timepoints"] = timepoints
|
||||
model.configuration["experiment"]["BDVParameters"] = {
|
||||
"shear": {
|
||||
"shear_data": True,
|
||||
"shear_dimension": "YZ",
|
||||
"shear_angle": 45,
|
||||
},
|
||||
"rotate": {
|
||||
"rotate_data": False,
|
||||
"X": 0,
|
||||
"Y": 0,
|
||||
"Z": 0,
|
||||
},
|
||||
"down_sample": {
|
||||
"down_sample": False,
|
||||
"axial_down_sample": 1,
|
||||
"lateral_down_sample": 1,
|
||||
},
|
||||
}
|
||||
if per_stack:
|
||||
model.configuration["experiment"]["MicroscopeState"][
|
||||
"stack_cycling_mode"
|
||||
] = "per_stack"
|
||||
else:
|
||||
model.configuration["experiment"]["MicroscopeState"][
|
||||
"stack_cycling_mode"
|
||||
] = "per_slice"
|
||||
|
||||
# Establish a BDV data source
|
||||
ds = BigDataViewerDataSource(fn)
|
||||
ds.set_metadata_from_configuration_experiment(model.configuration)
|
||||
|
||||
# Populate one image per channel per timepoint
|
||||
n_images = ds.shape_c * ds.shape_z * ds.shape_t * ds.positions
|
||||
print(
|
||||
f"x: {ds.shape_x} y: {ds.shape_y} z: {ds.shape_z} c: {ds.shape_c} "
|
||||
f"t: {ds.shape_t} positions: {ds.positions} per_stack: {ds.metadata.per_stack}"
|
||||
)
|
||||
data = (np.random.rand(n_images, ds.shape_y, ds.shape_x) * 2**16).astype("uint16")
|
||||
dbytes = np.sum(
|
||||
ds.shapes.prod(1) * ds.shape_t * ds.shape_c * ds.positions * 2
|
||||
) # 2 bytes per pixel (16-bit)
|
||||
assert dbytes == ds.nbytes
|
||||
data_positions = (np.random.rand(n_images, 5) * 50e3).astype(float)
|
||||
for i in range(n_images):
|
||||
ds.write(
|
||||
data[i, ...].squeeze(),
|
||||
x=data_positions[i, 0],
|
||||
y=data_positions[i, 1],
|
||||
z=data_positions[i, 2],
|
||||
theta=data_positions[i, 3],
|
||||
f=data_positions[i, 4],
|
||||
)
|
||||
if stop_early and np.random.rand() > 0.5:
|
||||
break
|
||||
|
||||
return ds
|
||||
|
||||
|
||||
def close_bdv_ds(ds, file_name=None):
|
||||
ds.close()
|
||||
|
||||
if file_name is None:
|
||||
file_name = ds.file_name
|
||||
|
||||
# Delete
|
||||
try:
|
||||
xml_fn = os.path.splitext(file_name)[0] + ".xml"
|
||||
if os.path.isdir(file_name):
|
||||
# n5 is a directory
|
||||
delete_folder(file_name)
|
||||
else:
|
||||
os.remove(file_name)
|
||||
os.remove(xml_fn)
|
||||
except PermissionError:
|
||||
# Windows seems to think these files are still open
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize("multiposition", [True, False])
|
||||
@pytest.mark.parametrize("per_stack", [True, False])
|
||||
@pytest.mark.parametrize("z_stack", [True, False])
|
||||
@pytest.mark.parametrize("stop_early", [True, False])
|
||||
@pytest.mark.parametrize("size", [(1024, 2048), (2048, 1024), (2048, 2048)])
|
||||
@pytest.mark.parametrize("ext", ["h5", "n5"])
|
||||
def test_bdv_write(multiposition, per_stack, z_stack, stop_early, size, ext):
|
||||
|
||||
fn = f"test.{ext}"
|
||||
|
||||
ds = bdv_ds(fn, multiposition, per_stack, z_stack, stop_early, size)
|
||||
|
||||
file_name = ds.file_name
|
||||
ds.close()
|
||||
|
||||
# check datatypes
|
||||
# todo: extend to n5
|
||||
if ext == "h5":
|
||||
ds = h5py.File(f"test.{ext}", "r")
|
||||
for key in ds.keys():
|
||||
recurse_dtype(ds[key])
|
||||
|
||||
close_bdv_ds(ds, file_name=file_name)
|
||||
|
||||
assert True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("multiposition", [True, False])
|
||||
@pytest.mark.parametrize("per_stack", [True, False])
|
||||
@pytest.mark.parametrize("z_stack", [True, False])
|
||||
@pytest.mark.parametrize("size", [(1024, 2048), (2048, 1024), (2048, 2048)])
|
||||
def test_bdv_getitem(multiposition, per_stack, z_stack, size):
|
||||
ds = bdv_ds("test.h5", multiposition, per_stack, z_stack, False, size)
|
||||
|
||||
# Check indexing
|
||||
assert ds[0, ...].shape == (
|
||||
ds.positions,
|
||||
ds.shape_t,
|
||||
ds.shape_z,
|
||||
ds.shape_c,
|
||||
ds.shape_y,
|
||||
1,
|
||||
)
|
||||
assert ds[:, 0, ...].shape == (
|
||||
ds.positions,
|
||||
ds.shape_t,
|
||||
ds.shape_z,
|
||||
ds.shape_c,
|
||||
1,
|
||||
ds.shape_x,
|
||||
)
|
||||
assert ds[:, :, 0, ...].shape == (
|
||||
ds.positions,
|
||||
ds.shape_t,
|
||||
ds.shape_z,
|
||||
1,
|
||||
ds.shape_y,
|
||||
ds.shape_x,
|
||||
)
|
||||
assert ds[:, :, :, 0, ...].shape == (
|
||||
ds.positions,
|
||||
ds.shape_t,
|
||||
1,
|
||||
ds.shape_c,
|
||||
ds.shape_y,
|
||||
ds.shape_x,
|
||||
)
|
||||
assert ds[:, :, :, :, 0, ...].shape == (
|
||||
ds.positions,
|
||||
1,
|
||||
ds.shape_z,
|
||||
ds.shape_c,
|
||||
ds.shape_y,
|
||||
ds.shape_x,
|
||||
)
|
||||
assert ds[:, :, :, :, :, 0].shape == (
|
||||
1,
|
||||
ds.shape_t,
|
||||
ds.shape_z,
|
||||
ds.shape_c,
|
||||
ds.shape_y,
|
||||
ds.shape_x,
|
||||
)
|
||||
|
||||
# Check slicing
|
||||
sx = 5
|
||||
assert ds[:sx, ...].shape == (
|
||||
ds.positions,
|
||||
ds.shape_t,
|
||||
ds.shape_z,
|
||||
ds.shape_c,
|
||||
ds.shape_y,
|
||||
min(ds.shape_x, sx),
|
||||
)
|
||||
assert ds[:, :sx, ...].shape == (
|
||||
ds.positions,
|
||||
ds.shape_t,
|
||||
ds.shape_z,
|
||||
ds.shape_c,
|
||||
min(ds.shape_y, sx),
|
||||
ds.shape_x,
|
||||
)
|
||||
assert ds[:, :, :sx, ...].shape == (
|
||||
ds.positions,
|
||||
ds.shape_t,
|
||||
ds.shape_z,
|
||||
min(ds.shape_c, sx),
|
||||
ds.shape_y,
|
||||
ds.shape_x,
|
||||
)
|
||||
assert ds[:, :, :, :sx, ...].shape == (
|
||||
ds.positions,
|
||||
ds.shape_t,
|
||||
min(ds.shape_z, sx),
|
||||
ds.shape_c,
|
||||
ds.shape_y,
|
||||
ds.shape_x,
|
||||
)
|
||||
assert ds[:, :, :, :, :sx, ...].shape == (
|
||||
ds.positions,
|
||||
min(ds.shape_t, sx),
|
||||
ds.shape_z,
|
||||
ds.shape_c,
|
||||
ds.shape_y,
|
||||
ds.shape_x,
|
||||
)
|
||||
assert ds[:, :, :, :, :, :sx].shape == (
|
||||
min(ds.positions, sx),
|
||||
ds.shape_t,
|
||||
ds.shape_z,
|
||||
ds.shape_c,
|
||||
ds.shape_y,
|
||||
ds.shape_x,
|
||||
)
|
||||
|
||||
close_bdv_ds(ds)
|
||||
|
||||
assert True
|
||||
95
test/model/data_sources/test_data_source.py
Normal file
95
test/model/data_sources/test_data_source.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
||||
def test_data_source_mode():
|
||||
from navigate.model.data_sources.data_source import DataSource
|
||||
|
||||
ds = DataSource()
|
||||
|
||||
# set read and write
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
ds.mode = "r"
|
||||
assert ds.mode == "r"
|
||||
|
||||
ds.mode = "w"
|
||||
assert ds.mode == "w"
|
||||
|
||||
# set unknown mode, default to read
|
||||
with pytest.raises(NotImplementedError):
|
||||
ds.mode = "goblin"
|
||||
assert ds.mode == "r"
|
||||
|
||||
|
||||
def test_data_source_cztp_indices():
|
||||
import itertools
|
||||
from navigate.model.data_sources.data_source import DataSource
|
||||
|
||||
MAX = 25
|
||||
|
||||
ds = DataSource()
|
||||
|
||||
ds.shape_c = np.random.randint(1, MAX)
|
||||
ds.shape_z = 1
|
||||
ds.shape_t = np.random.randint(1, MAX)
|
||||
ds.positions = np.random.randint(1, MAX)
|
||||
n_inds = ds.shape_c * ds.shape_z * ds.shape_t * ds.positions
|
||||
|
||||
print(f"n_inds : {n_inds}")
|
||||
|
||||
cztp_inds = itertools.product(
|
||||
range(ds.positions), range(ds.shape_z), range(ds.shape_t), range(ds.shape_c)
|
||||
)
|
||||
|
||||
for i, inds in zip(range(n_inds), cztp_inds):
|
||||
c, z, t, p = ds._cztp_indices(i, False)
|
||||
pt, zt, tt, ct = inds
|
||||
assert c == ct
|
||||
assert z == zt
|
||||
assert t == tt
|
||||
assert p == pt
|
||||
|
||||
print(
|
||||
f"Shape (XYCZTP): {ds.shape} {ds.positions} "
|
||||
f"Final (CZTP): {ds._cztp_indices(n_inds-1, False)}"
|
||||
)
|
||||
|
||||
ds.shape_z = np.random.randint(2, MAX)
|
||||
n_inds = ds.shape_c * ds.shape_z * ds.shape_t * ds.positions
|
||||
|
||||
cztp_inds = itertools.product(
|
||||
range(ds.positions), range(ds.shape_t), range(ds.shape_z), range(ds.shape_c)
|
||||
)
|
||||
|
||||
for i, inds in zip(range(n_inds), cztp_inds):
|
||||
c, z, t, p = ds._cztp_indices(i, False)
|
||||
pt, tt, zt, ct = inds
|
||||
assert c == ct
|
||||
assert z == zt
|
||||
assert t == tt
|
||||
assert p == pt
|
||||
|
||||
print(
|
||||
f"Shape (XYCZTP): {ds.shape} {ds.positions} "
|
||||
f"Final (CZTP): {ds._cztp_indices(n_inds-1, False)}"
|
||||
)
|
||||
|
||||
cztp_inds = itertools.product(
|
||||
range(ds.positions), range(ds.shape_t), range(ds.shape_c), range(ds.shape_z)
|
||||
)
|
||||
|
||||
for i, inds in zip(range(n_inds), cztp_inds):
|
||||
c, z, t, p = ds._cztp_indices(i, True)
|
||||
pt, tt, ct, zt = inds
|
||||
assert c == ct
|
||||
assert z == zt
|
||||
assert t == tt
|
||||
assert p == pt
|
||||
|
||||
print(
|
||||
f"Shape (XYCZTP): {ds.shape} {ds.positions} "
|
||||
f"Final (CZTP): {ds._cztp_indices(n_inds-1, False)}"
|
||||
)
|
||||
|
||||
# assert False
|
||||
108
test/model/data_sources/test_tiff_data_source.py
Normal file
108
test/model/data_sources/test_tiff_data_source.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from navigate.tools.file_functions import delete_folder
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_ome", [True, False])
|
||||
@pytest.mark.parametrize("multiposition", [True, False])
|
||||
@pytest.mark.parametrize("per_stack", [True, False])
|
||||
@pytest.mark.parametrize("z_stack", [True, False])
|
||||
@pytest.mark.parametrize("stop_early", [True, False])
|
||||
def test_tiff_write_read(is_ome, multiposition, per_stack, z_stack, stop_early):
|
||||
import numpy as np
|
||||
|
||||
from test.model.dummy import DummyModel
|
||||
from navigate.model.data_sources.tiff_data_source import TiffDataSource
|
||||
|
||||
print(
|
||||
f"Conditions are is_ome: {is_ome} multiposition: {multiposition} "
|
||||
f"per_stack: {per_stack} z_stack: {z_stack} stop_early: {stop_early}"
|
||||
)
|
||||
|
||||
# Set up model with a random number of z-steps to modulate the shape
|
||||
model = DummyModel()
|
||||
z_steps = np.random.randint(1, 3)
|
||||
timepoints = np.random.randint(1, 3)
|
||||
model.configuration["experiment"]["MicroscopeState"]["image_mode"] = (
|
||||
"z-stack" if z_stack else "single"
|
||||
)
|
||||
model.configuration["experiment"]["MicroscopeState"]["number_z_steps"] = z_steps
|
||||
model.configuration["experiment"]["MicroscopeState"][
|
||||
"is_multiposition"
|
||||
] = multiposition
|
||||
model.configuration["experiment"]["MicroscopeState"]["timepoints"] = timepoints
|
||||
if per_stack:
|
||||
model.configuration["experiment"]["MicroscopeState"][
|
||||
"stack_cycling_mode"
|
||||
] == "per_stack"
|
||||
else:
|
||||
model.configuration["experiment"]["MicroscopeState"][
|
||||
"stack_cycling_mode"
|
||||
] == "per_slice"
|
||||
|
||||
if not os.path.exists("test_save_dir"):
|
||||
os.mkdir("test_save_dir")
|
||||
|
||||
# Establish a TIFF data source
|
||||
if is_ome:
|
||||
fn = "./test_save_dir/test.ome.tif"
|
||||
else:
|
||||
fn = "./test_save_dir/test.tif"
|
||||
ds = TiffDataSource(fn)
|
||||
ds.set_metadata_from_configuration_experiment(model.configuration)
|
||||
|
||||
# Populate one image per channel per timepoint per position
|
||||
n_images = ds.shape_c * ds.shape_z * ds.shape_t * ds.positions
|
||||
data = (np.random.rand(n_images, ds.shape_y, ds.shape_x) * 2**16).astype(
|
||||
np.uint16
|
||||
)
|
||||
file_names_raw = []
|
||||
for i in range(n_images):
|
||||
ds.write(data[i, ...].squeeze())
|
||||
file_names_raw.extend(ds.file_name)
|
||||
if stop_early and np.random.rand() > 0.5:
|
||||
break
|
||||
ds.close()
|
||||
|
||||
# Cannot use list(set()) trick here because ordering is important
|
||||
file_names = []
|
||||
for fn in file_names_raw:
|
||||
if fn not in file_names:
|
||||
file_names.append(fn)
|
||||
# print(file_names)
|
||||
|
||||
try:
|
||||
# For each file...
|
||||
for i, fn in enumerate(file_names):
|
||||
ds2 = TiffDataSource(fn, "r")
|
||||
# Make sure XYZ size is correct (and C and T are each of size 1)
|
||||
assert (
|
||||
(ds2.shape_x == ds.shape_x)
|
||||
and (ds2.shape_y == ds.shape_y)
|
||||
and (ds2.shape_c == 1)
|
||||
and (ds2.shape_t == 1)
|
||||
and (ds2.shape_z == ds.shape_z)
|
||||
)
|
||||
# Make sure the data copied properly
|
||||
np.testing.assert_equal(
|
||||
ds2.data, data[i * ds.shape_z : (i + 1) * ds.shape_z, ...].squeeze()
|
||||
)
|
||||
ds2.close()
|
||||
except IndexError as e:
|
||||
if stop_early:
|
||||
# This file was not written
|
||||
pass
|
||||
else:
|
||||
raise e
|
||||
except AssertionError as e:
|
||||
if stop_early:
|
||||
# This file has an underfilled axes
|
||||
pass
|
||||
else:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise e
|
||||
finally:
|
||||
delete_folder("test_save_dir")
|
||||
152
test/model/data_sources/test_zarr_data_source.py
Normal file
152
test/model/data_sources/test_zarr_data_source.py
Normal file
@@ -0,0 +1,152 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
from pydantic import ValidationError
|
||||
from pydantic_ome_ngff.v04.multiscale import Group
|
||||
|
||||
pydantic = True
|
||||
except (ImportError, TypeError):
|
||||
pydantic = False
|
||||
|
||||
from navigate.tools.file_functions import delete_folder
|
||||
|
||||
|
||||
def zarr_ds(fn, multiposition, per_stack, z_stack, stop_early, size):
|
||||
from test.model.dummy import DummyModel
|
||||
from navigate.model.data_sources.zarr_data_source import OMEZarrDataSource
|
||||
|
||||
print(
|
||||
f"Conditions are multiposition: {multiposition} per_stack: {per_stack} "
|
||||
f"z_stack: {z_stack} stop_early: {stop_early}"
|
||||
)
|
||||
|
||||
# Set up model with a random number of z-steps to modulate the shape
|
||||
model = DummyModel()
|
||||
z_steps = np.random.randint(1, 3)
|
||||
timepoints = np.random.randint(1, 3)
|
||||
|
||||
x_size, y_size = size
|
||||
microscope_name = model.configuration["experiment"]["MicroscopeState"][
|
||||
"microscope_name"
|
||||
]
|
||||
model.configuration["experiment"]["CameraParameters"][microscope_name][
|
||||
"x_pixels"
|
||||
] = x_size
|
||||
model.configuration["experiment"]["CameraParameters"][microscope_name][
|
||||
"y_pixels"
|
||||
] = y_size
|
||||
model.img_width = x_size
|
||||
model.img_height = y_size
|
||||
|
||||
model.configuration["experiment"]["MicroscopeState"]["image_mode"] = (
|
||||
"z-stack" if z_stack else "single"
|
||||
)
|
||||
model.configuration["experiment"]["MicroscopeState"]["number_z_steps"] = z_steps
|
||||
model.configuration["experiment"]["MicroscopeState"][
|
||||
"is_multiposition"
|
||||
] = multiposition
|
||||
model.configuration["experiment"]["MicroscopeState"]["timepoints"] = timepoints
|
||||
|
||||
model.configuration["experiment"]["BDVParameters"] = {
|
||||
"shear": {
|
||||
"shear_data": True,
|
||||
"shear_dimension": "YZ",
|
||||
"shear_angle": 45,
|
||||
},
|
||||
"rotate": {
|
||||
"rotate_data": False,
|
||||
"X": 0,
|
||||
"Y": 0,
|
||||
"Z": 0,
|
||||
},
|
||||
"down_sample": {
|
||||
"down_sample": False,
|
||||
"axial_down_sample": 1,
|
||||
"lateral_down_sample": 1,
|
||||
},
|
||||
}
|
||||
|
||||
if per_stack:
|
||||
model.configuration["experiment"]["MicroscopeState"][
|
||||
"stack_cycling_mode"
|
||||
] = "per_stack"
|
||||
else:
|
||||
model.configuration["experiment"]["MicroscopeState"][
|
||||
"stack_cycling_mode"
|
||||
] = "per_slice"
|
||||
|
||||
# Establish a BDV data source
|
||||
ds = OMEZarrDataSource(fn)
|
||||
ds.set_metadata_from_configuration_experiment(model.configuration)
|
||||
|
||||
# Populate one image per channel per timepoint
|
||||
n_images = ds.shape_c * ds.shape_z * ds.shape_t * ds.positions
|
||||
print(
|
||||
f"x: {ds.shape_x} y: {ds.shape_y} z: {ds.shape_z} c: {ds.shape_c} "
|
||||
f"t: {ds.shape_t} positions: {ds.positions} per_stack: {ds.metadata.per_stack}"
|
||||
)
|
||||
data = (np.random.rand(n_images, ds.shape_y, ds.shape_x) * 2**16).astype("uint16")
|
||||
dbytes = np.sum(
|
||||
ds.shapes.prod(1) * ds.shape_t * ds.shape_c * ds.positions * 2
|
||||
) # 2 bytes per pixel (16-bit)
|
||||
assert dbytes == ds.nbytes
|
||||
data_positions = (np.random.rand(n_images, 5) * 50e3).astype(float)
|
||||
for i in range(n_images):
|
||||
ds.write(
|
||||
data[i, ...].squeeze(),
|
||||
x=data_positions[i, 0],
|
||||
y=data_positions[i, 1],
|
||||
z=data_positions[i, 2],
|
||||
theta=data_positions[i, 3],
|
||||
f=data_positions[i, 4],
|
||||
)
|
||||
if stop_early and np.random.rand() > 0.5:
|
||||
break
|
||||
|
||||
return ds
|
||||
|
||||
|
||||
def close_zarr_ds(ds, file_name=None):
|
||||
ds.close()
|
||||
|
||||
if file_name is None:
|
||||
file_name = ds.file_name
|
||||
|
||||
# Delete
|
||||
try:
|
||||
if os.path.isdir(file_name):
|
||||
# zarr is a directory
|
||||
delete_folder(file_name)
|
||||
else:
|
||||
os.remove(file_name)
|
||||
except PermissionError:
|
||||
# Windows seems to think these files are still open
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize("multiposition", [True, False])
|
||||
@pytest.mark.parametrize("per_stack", [True, False])
|
||||
@pytest.mark.parametrize("z_stack", [True, False])
|
||||
@pytest.mark.parametrize("stop_early", [True, False])
|
||||
@pytest.mark.parametrize("size", [(1024, 2048), (2048, 1024), (2048, 2048)])
|
||||
def test_zarr_write(multiposition, per_stack, z_stack, stop_early, size):
|
||||
|
||||
fn = "test.zarr"
|
||||
|
||||
ds = zarr_ds(fn, multiposition, per_stack, z_stack, stop_early, size)
|
||||
|
||||
if pydantic:
|
||||
try:
|
||||
Group.from_zarr(ds.image)
|
||||
except ValidationError as e:
|
||||
print(e)
|
||||
assert False
|
||||
|
||||
file_name = ds.file_name
|
||||
|
||||
close_zarr_ds(ds, file_name=file_name)
|
||||
|
||||
assert True
|
||||
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()
|
||||
647
test/model/dummy.py
Normal file
647
test/model/dummy.py
Normal file
@@ -0,0 +1,647 @@
|
||||
# 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.
|
||||
|
||||
from pathlib import Path
|
||||
import multiprocessing as mp
|
||||
from multiprocessing import Manager
|
||||
import threading
|
||||
import time
|
||||
|
||||
# Third Party Imports
|
||||
import numpy as np
|
||||
import random
|
||||
|
||||
# Local Imports
|
||||
from navigate.config.config import (
|
||||
load_configs,
|
||||
verify_experiment_config,
|
||||
verify_waveform_constants,
|
||||
verify_configuration,
|
||||
verify_positions_config,
|
||||
)
|
||||
from navigate.model.devices.camera.synthetic import (
|
||||
SyntheticCamera,
|
||||
SyntheticCameraController,
|
||||
)
|
||||
from navigate.model.features.feature_container import (
|
||||
load_features,
|
||||
)
|
||||
from navigate.tools.file_functions import load_yaml_file
|
||||
|
||||
|
||||
class DummyController:
|
||||
"""Dummy Controller"""
|
||||
|
||||
def __init__(self, view):
|
||||
"""Initialize the Dummy controller.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
view : DummyView
|
||||
The view to be controlled by this controller.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> controller = DummyController(view)
|
||||
"""
|
||||
from navigate.controller.configuration_controller import ConfigurationController
|
||||
from navigate.controller.sub_controllers import MenuController
|
||||
from navigate.controller.sub_controllers.multiposition import (
|
||||
MultiPositionController,
|
||||
)
|
||||
from navigate.controller.sub_controllers.channels_tab import (
|
||||
ChannelsTabController,
|
||||
)
|
||||
|
||||
#: dict: The configuration dictionary.
|
||||
self.configuration = DummyModel().configuration
|
||||
|
||||
#: list: The list of commands.
|
||||
self.commands = []
|
||||
|
||||
#: dict: The custom events
|
||||
self.event_listeners = {}
|
||||
|
||||
self.manager = Manager()
|
||||
|
||||
#: DummyView: The view to be controlled by this controller.
|
||||
self.view = view
|
||||
|
||||
#: ConfigurationController: The configuration controller.
|
||||
self.configuration_controller = ConfigurationController(self.configuration)
|
||||
|
||||
#: MenuController: The menu controller.
|
||||
self.menu_controller = MenuController(view=self.view, parent_controller=self)
|
||||
|
||||
#: ChannelsTabController: The channels tab controller.
|
||||
self.channels_tab_controller = ChannelsTabController(
|
||||
self.view.settings.channels_tab, self
|
||||
)
|
||||
|
||||
#: MultiPositionController: The multiposition tab controller.
|
||||
self.multiposition_tab_controller = MultiPositionController(
|
||||
self.view.settings.multiposition_tab.multipoint_list, self
|
||||
)
|
||||
|
||||
#: dict: The stage positions.
|
||||
self.stage_pos = {}
|
||||
|
||||
#: dict: The stage offset positions.
|
||||
self.off_stage_pos = {}
|
||||
base_directory = Path.joinpath(Path(__file__).resolve().parent.parent)
|
||||
configuration_directory = Path.joinpath(base_directory, "config")
|
||||
self.waveform_constants_path = Path.joinpath(
|
||||
configuration_directory, "waveform_constants.yml"
|
||||
)
|
||||
|
||||
#: bool: Flag to indicate if the resize is ready.
|
||||
self.resize_ready_flag = True
|
||||
|
||||
def execute(self, str, *args, sec=None):
|
||||
"""Execute a command.
|
||||
|
||||
Appends commands sent via execute,
|
||||
first element is oldest command/first to pop off
|
||||
|
||||
Parameters
|
||||
----------
|
||||
str : str
|
||||
The command to be executed.
|
||||
sec : float
|
||||
The time to wait before executing the command.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> controller.execute('move_stage', 1)
|
||||
"""
|
||||
|
||||
self.commands.append(str)
|
||||
if str in ["move_stage_and_acquire_image", "move_stage_and_update_info"]:
|
||||
self.commands.append(*args)
|
||||
if sec is not None:
|
||||
self.commands.append(sec)
|
||||
|
||||
if str == "get_stage_position":
|
||||
self.stage_pos["x"] = int(random.random())
|
||||
self.stage_pos["y"] = int(random.random())
|
||||
return self.stage_pos
|
||||
|
||||
def pop(self):
|
||||
"""Pop the oldest command.
|
||||
|
||||
Use this method in testing code to grab the next command.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The oldest command.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> controller.pop()
|
||||
"""
|
||||
|
||||
if len(self.commands) > 0:
|
||||
return self.commands.pop(0)
|
||||
else:
|
||||
return "Empty command list"
|
||||
|
||||
def clear(self):
|
||||
"""Clear command list"""
|
||||
self.commands = []
|
||||
|
||||
|
||||
class DummyModel:
|
||||
"""Dummy Model - This class is used to test the controller and view."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the Dummy model."""
|
||||
# Set up the model, experiment, waveform dictionaries
|
||||
base_directory = Path(__file__).resolve().parent.parent.parent
|
||||
configuration_directory = Path.joinpath(
|
||||
base_directory, "src", "navigate", "config"
|
||||
)
|
||||
|
||||
config = Path.joinpath(configuration_directory, "configuration.yaml")
|
||||
experiment = Path.joinpath(configuration_directory, "experiment.yml")
|
||||
waveform_constants = Path.joinpath(
|
||||
configuration_directory, "waveform_constants.yml"
|
||||
)
|
||||
gui_configuration_path = Path.joinpath(
|
||||
configuration_directory, "gui_configuration.yml"
|
||||
)
|
||||
multi_positions_path = Path.joinpath(
|
||||
configuration_directory, "multi_positions.yml"
|
||||
)
|
||||
|
||||
#: Manager: The manager.
|
||||
self.manager = Manager()
|
||||
#: dict: The configuration dictionary.
|
||||
self.configuration = load_configs(
|
||||
self.manager,
|
||||
configuration=config,
|
||||
experiment=experiment,
|
||||
waveform_constants=waveform_constants,
|
||||
gui=gui_configuration_path,
|
||||
)
|
||||
|
||||
verify_configuration(self.manager, self.configuration)
|
||||
verify_experiment_config(self.manager, self.configuration)
|
||||
verify_waveform_constants(self.manager, self.configuration)
|
||||
|
||||
positions = load_yaml_file(multi_positions_path)
|
||||
positions = verify_positions_config(positions)
|
||||
self.configuration["multi_positions"] = positions
|
||||
|
||||
#: DummyDevice: The device.
|
||||
self.device = DummyDevice()
|
||||
#: Pipe: The pipe for sending signals.
|
||||
self.signal_pipe, self.data_pipe = None, None
|
||||
#: DummyMicroscope: The microscope.
|
||||
self.active_microscope = DummyMicroscope(
|
||||
"Mesoscale", self.configuration, devices_dict={}, is_synthetic=True
|
||||
)
|
||||
#: Object: The signal container.
|
||||
self.signal_container = None
|
||||
#: Object: The data container.
|
||||
self.data_container = None
|
||||
#: Thread: The signal thread.
|
||||
self.signal_thread = None
|
||||
#: Thread: The data thread.
|
||||
self.data_thread = None
|
||||
|
||||
#: bool: The flag for stopping the model.
|
||||
self.stop_flag = False
|
||||
#: int: The frame id.
|
||||
self.frame_id = 0 # signal_num
|
||||
#: list: The list of data.
|
||||
self.data = []
|
||||
#: list: The list of signal records.
|
||||
self.signal_records = []
|
||||
#: list: The list of data records.
|
||||
self.data_records = []
|
||||
#: int: The image width.
|
||||
self.img_width = int(
|
||||
self.configuration["experiment"]["CameraParameters"]["x_pixels"]
|
||||
)
|
||||
#: int: The image height.
|
||||
self.img_height = int(
|
||||
self.configuration["experiment"]["CameraParameters"]["y_pixels"]
|
||||
)
|
||||
#: int: The number of frames in the data buffer.
|
||||
self.number_of_frames = 10
|
||||
#: ndarray: The data buffer.
|
||||
self.data_buffer = np.zeros(
|
||||
(self.number_of_frames, self.img_width, self.img_height)
|
||||
)
|
||||
#: ndarray: The data buffer positions.
|
||||
self.data_buffer_positions = np.zeros(
|
||||
shape=(self.number_of_frames, 5), dtype=float
|
||||
) # z-index, x, y, z, theta, f
|
||||
#: dict: The camera dictionary.
|
||||
self.camera = {}
|
||||
#: str: The active microscope name.
|
||||
self.active_microscope_name = self.configuration["experiment"][
|
||||
"MicroscopeState"
|
||||
]["microscope_name"]
|
||||
for k in self.configuration["configuration"]["microscopes"].keys():
|
||||
self.camera[k] = SyntheticCamera(
|
||||
self.active_microscope_name,
|
||||
SyntheticCameraController(),
|
||||
self.configuration,
|
||||
)
|
||||
self.camera[k].initialize_image_series(
|
||||
self.data_buffer, self.number_of_frames
|
||||
)
|
||||
|
||||
def signal_func(self):
|
||||
"""Perform signal-related functionality.
|
||||
|
||||
This method is responsible for signal processing operations. It resets the
|
||||
signal container and continues processing signals until the end flag is set.
|
||||
During each iteration, it runs the signal container and communicates with
|
||||
a separate process using a signal pipe. The `frame_id` is incremented after
|
||||
each signal processing step.
|
||||
|
||||
Note
|
||||
----
|
||||
- The function utilizes a signal container and a signal pipe for communication.
|
||||
- It terminates when the `end_flag` is set and sends a "shutdown" signal.
|
||||
"""
|
||||
|
||||
self.signal_container.reset()
|
||||
while not self.signal_container.end_flag:
|
||||
if self.signal_container:
|
||||
self.signal_container.run()
|
||||
|
||||
self.signal_pipe.send("signal")
|
||||
self.signal_pipe.recv()
|
||||
|
||||
if self.signal_container:
|
||||
self.signal_container.run(wait_response=True)
|
||||
|
||||
self.frame_id += 1 # signal_num
|
||||
|
||||
self.signal_pipe.send("shutdown")
|
||||
|
||||
self.stop_flag = True
|
||||
|
||||
def data_func(self):
|
||||
"""The function responsible for sending and processing data.
|
||||
|
||||
This method continuously sends data requests using a data pipe and receives
|
||||
corresponding frame IDs. It appends the received frame IDs to the data storage
|
||||
and runs data processing operations if a data container is available.
|
||||
|
||||
Notes
|
||||
-----
|
||||
- The function operates in a loop until the `stop_flag` is set.
|
||||
- It communicates with a separate process using a data pipe for data retrieval.
|
||||
- Received frame IDs are appended to the data storage and processed if
|
||||
applicable.
|
||||
- The method terminates by sending a "shutdown" signal.
|
||||
|
||||
"""
|
||||
while not self.stop_flag:
|
||||
self.data_pipe.send("getData")
|
||||
frame_ids = self.data_pipe.recv()
|
||||
print("receive: ", frame_ids)
|
||||
if not frame_ids:
|
||||
continue
|
||||
|
||||
self.data.append(frame_ids)
|
||||
|
||||
if self.data_container:
|
||||
self.data_container.run(frame_ids)
|
||||
self.data_pipe.send("shutdown")
|
||||
|
||||
def start(self, feature_list):
|
||||
"""Start the model.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
feature_list : list
|
||||
The list of features to be used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if the model is started successfully, False otherwise.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> model.start(['signal', 'data'])
|
||||
"""
|
||||
|
||||
if feature_list is None:
|
||||
return False
|
||||
self.data = []
|
||||
self.signal_records = []
|
||||
self.data_records = []
|
||||
self.stop_flag = False
|
||||
self.frame_id = 0 # signal_num
|
||||
|
||||
self.signal_pipe, self.data_pipe = self.device.setup()
|
||||
|
||||
self.signal_container, self.data_container = load_features(self, feature_list)
|
||||
self.signal_thread = threading.Thread(target=self.signal_func, name="signal")
|
||||
self.data_thread = threading.Thread(target=self.data_func, name="data")
|
||||
self.signal_thread.start()
|
||||
self.data_thread.start()
|
||||
|
||||
self.signal_thread.join()
|
||||
self.stop_flag = True
|
||||
self.data_thread.join()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class DummyDevice:
|
||||
"""Dummy Device - class is used to test the controller and view."""
|
||||
|
||||
def __init__(self, timecost=0.2):
|
||||
"""Initialize the Dummy device.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
timecost : float
|
||||
The time cost for generating a message.
|
||||
"""
|
||||
|
||||
#: int: The message count.
|
||||
self.msg_count = mp.Value("i", 0)
|
||||
#: int: The sendout message count.
|
||||
self.sendout_msg_count = 0
|
||||
#: Pipe: The pipe for sending signals.
|
||||
self.out_port = None
|
||||
#: Pipe: The pipe for receiving signals.
|
||||
self.in_port = None
|
||||
#: float: The time cost for generating a message.
|
||||
self.timecost = timecost
|
||||
#: bool: The flag for stopping the device.
|
||||
self.stop_flag = False
|
||||
|
||||
def setup(self):
|
||||
"""Set up the pipes.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Pipe
|
||||
The pipe for sending signals.
|
||||
Pipe
|
||||
The pipe for receiving signals.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> device.setup()
|
||||
"""
|
||||
|
||||
signalPort, self.in_port = mp.Pipe()
|
||||
dataPort, self.out_port = mp.Pipe()
|
||||
in_process = mp.Process(target=self.listen)
|
||||
out_process = mp.Process(target=self.sendout)
|
||||
in_process.start()
|
||||
out_process.start()
|
||||
|
||||
self.sendout_msg_count = 0
|
||||
self.msg_count.value = 0
|
||||
self.stop_flag = False
|
||||
|
||||
return signalPort, dataPort
|
||||
|
||||
def generate_message(self):
|
||||
"""Generate a message.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> device.generate_message()
|
||||
"""
|
||||
|
||||
time.sleep(self.timecost)
|
||||
self.msg_count.value += 1
|
||||
|
||||
def clear(self):
|
||||
"""Clear the pipes.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> device.clear()
|
||||
"""
|
||||
self.msg_count.value = 0
|
||||
|
||||
def listen(self):
|
||||
"""Listen to the pipe.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> device.listen()
|
||||
"""
|
||||
while not self.stop_flag:
|
||||
signal = self.in_port.recv()
|
||||
if signal == "shutdown":
|
||||
self.stop_flag = True
|
||||
self.in_port.close()
|
||||
break
|
||||
self.generate_message()
|
||||
self.in_port.send("done")
|
||||
|
||||
def sendout(self, timeout=100):
|
||||
"""Send out the message.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
timeout : int
|
||||
The timeout for sending out the message.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> device.sendout()
|
||||
"""
|
||||
while not self.stop_flag:
|
||||
msg = self.out_port.recv()
|
||||
if msg == "shutdown":
|
||||
self.out_port.close()
|
||||
break
|
||||
c = 0
|
||||
while self.msg_count.value == self.sendout_msg_count and c < timeout:
|
||||
time.sleep(0.01)
|
||||
c += 1
|
||||
self.out_port.send(
|
||||
list(range(self.sendout_msg_count, self.msg_count.value))
|
||||
)
|
||||
self.sendout_msg_count = self.msg_count.value
|
||||
|
||||
|
||||
class DummyMicroscope:
|
||||
"""Dummy Microscope - Class is used to test the controller and view."""
|
||||
|
||||
def __init__(self, name, configuration, devices_dict, is_synthetic=False):
|
||||
"""Initialize the Dummy microscope.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The microscope name.
|
||||
configuration : dict
|
||||
The configuration dictionary.
|
||||
devices_dict : dict
|
||||
The dictionary of devices.
|
||||
is_synthetic : bool
|
||||
The flag for using a synthetic microscope.
|
||||
"""
|
||||
#: str: The microscope name.
|
||||
self.microscope_name = name
|
||||
#: dict: The configuration dictionary.
|
||||
self.configuration = configuration
|
||||
#: np.ndarray: The data buffer.
|
||||
self.data_buffer = None
|
||||
#: dict: The stage dictionary.
|
||||
self.stages = {}
|
||||
#: dict: The lasers dictionary.
|
||||
self.lasers = {}
|
||||
#: dict: The galvo dictionary.
|
||||
self.galvo = {}
|
||||
#: dict: The DAQ dictionary.
|
||||
self.daq = devices_dict.get("daq", None)
|
||||
#: int: The current channel.
|
||||
self.current_channel = 0
|
||||
self.camera = SyntheticCamera(
|
||||
self.configuration["experiment"]["MicroscopeState"]["microscope_name"],
|
||||
SyntheticCameraController(),
|
||||
self.configuration,
|
||||
)
|
||||
|
||||
def calculate_exposure_sweep_times(self):
|
||||
"""Get the exposure and sweep times for all channels.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The dictionary of exposure times.
|
||||
dict
|
||||
The dictionary of sweep times.
|
||||
"""
|
||||
exposure_times = {}
|
||||
sweep_times = {}
|
||||
microscope_state = self.configuration["experiment"]["MicroscopeState"]
|
||||
waveform_constants = self.configuration["waveform_constants"]
|
||||
camera_delay = (
|
||||
self.configuration["configuration"]["microscopes"][self.microscope_name][
|
||||
"camera"
|
||||
]["delay"]
|
||||
/ 1000
|
||||
)
|
||||
camera_settle_duration = (
|
||||
self.configuration["configuration"]["microscopes"][self.microscope_name][
|
||||
"camera"
|
||||
].get("settle_duration", 0)
|
||||
/ 1000
|
||||
)
|
||||
remote_focus_ramp_falling = (
|
||||
float(waveform_constants["other_constants"]["remote_focus_ramp_falling"])
|
||||
/ 1000
|
||||
)
|
||||
|
||||
duty_cycle_wait_duration = (
|
||||
float(waveform_constants["other_constants"]["remote_focus_settle_duration"])
|
||||
/ 1000
|
||||
)
|
||||
ps = float(waveform_constants["other_constants"].get("percent_smoothing", 0.0))
|
||||
|
||||
readout_time = 0
|
||||
readout_mode = self.configuration["experiment"]["CameraParameters"][
|
||||
"sensor_mode"
|
||||
]
|
||||
if readout_mode == "Normal":
|
||||
readout_time = self.camera.calculate_readout_time()
|
||||
elif self.configuration["experiment"]["CameraParameters"][
|
||||
"readout_direction"
|
||||
] in ["Bidirectional", "Rev. Bidirectional"]:
|
||||
remote_focus_ramp_falling = 0
|
||||
# set readout out time
|
||||
self.configuration["experiment"]["CameraParameters"]["readout_time"] = (
|
||||
readout_time * 1000
|
||||
)
|
||||
for channel_key in microscope_state["channels"].keys():
|
||||
channel = microscope_state["channels"][channel_key]
|
||||
if channel["is_selected"] is True:
|
||||
exposure_time = channel["camera_exposure_time"] / 1000
|
||||
|
||||
if readout_mode == "Light-Sheet":
|
||||
(
|
||||
_,
|
||||
_,
|
||||
updated_exposure_time,
|
||||
) = self.camera.calculate_light_sheet_exposure_time(
|
||||
exposure_time,
|
||||
int(
|
||||
self.configuration["experiment"]["CameraParameters"][
|
||||
"number_of_pixels"
|
||||
]
|
||||
),
|
||||
)
|
||||
if updated_exposure_time != exposure_time:
|
||||
print(
|
||||
f"*** Notice: The actual exposure time of the camera for "
|
||||
f"{channel_key} is {round(updated_exposure_time*1000, 1)}"
|
||||
f"ms, not {exposure_time*1000}ms!"
|
||||
)
|
||||
exposure_time = round(updated_exposure_time, 4)
|
||||
# update the experiment file
|
||||
channel["camera_exposure_time"] = round(
|
||||
updated_exposure_time * 1000, 1
|
||||
)
|
||||
self.output_event_queue.put(
|
||||
(
|
||||
"exposure_time",
|
||||
(channel_key, channel["camera_exposure_time"]),
|
||||
)
|
||||
)
|
||||
|
||||
sweep_time = (
|
||||
exposure_time
|
||||
+ readout_time
|
||||
+ camera_delay
|
||||
+ max(
|
||||
remote_focus_ramp_falling + duty_cycle_wait_duration,
|
||||
camera_settle_duration,
|
||||
camera_delay,
|
||||
)
|
||||
- camera_delay
|
||||
)
|
||||
# TODO: should we keep the percent_smoothing?
|
||||
if ps > 0:
|
||||
sweep_time = (1 + ps / 100) * sweep_time
|
||||
|
||||
exposure_times[channel_key] = exposure_time + readout_time
|
||||
sweep_times[channel_key] = sweep_time
|
||||
|
||||
return exposure_times, sweep_times
|
||||
176
test/model/features/conftest.py
Normal file
176
test/model/features/conftest.py
Normal file
@@ -0,0 +1,176 @@
|
||||
import pytest
|
||||
import time
|
||||
import threading
|
||||
import multiprocessing as mp
|
||||
from navigate.model.features.feature_container import load_features
|
||||
|
||||
|
||||
class DummyDevice:
|
||||
def __init__(self, timecost=0.2):
|
||||
self.msg_count = mp.Value("i", 0)
|
||||
self.sendout_msg_count = 0
|
||||
self.out_port = None
|
||||
self.in_port = None
|
||||
self.timecost = timecost
|
||||
self.stop_acquisition = False
|
||||
|
||||
def setup(self):
|
||||
signalPort, self.in_port = mp.Pipe()
|
||||
dataPort, self.out_port = mp.Pipe()
|
||||
in_process = mp.Process(target=self.listen)
|
||||
out_process = mp.Process(target=self.sendout)
|
||||
in_process.start()
|
||||
out_process.start()
|
||||
|
||||
self.sendout_msg_count = 0
|
||||
self.msg_count.value = 0
|
||||
self.stop_acquisition = False
|
||||
|
||||
return signalPort, dataPort
|
||||
|
||||
def generate_message(self):
|
||||
time.sleep(self.timecost)
|
||||
self.msg_count.value += 1
|
||||
|
||||
def clear(self):
|
||||
self.msg_count.value = 0
|
||||
|
||||
def listen(self):
|
||||
while not self.stop_acquisition:
|
||||
signal = self.in_port.recv()
|
||||
if signal == "shutdown":
|
||||
self.stop_acquisition = True
|
||||
self.in_port.close()
|
||||
break
|
||||
self.generate_message()
|
||||
self.in_port.send("done")
|
||||
|
||||
def sendout(self, timeout=100):
|
||||
while not self.stop_acquisition:
|
||||
msg = self.out_port.recv()
|
||||
if msg == "shutdown":
|
||||
self.out_port.close()
|
||||
break
|
||||
c = 0
|
||||
while self.msg_count.value == self.sendout_msg_count and c < timeout:
|
||||
time.sleep(0.01)
|
||||
c += 1
|
||||
self.out_port.send(
|
||||
list(range(self.sendout_msg_count, self.msg_count.value))
|
||||
)
|
||||
self.sendout_msg_count = self.msg_count.value
|
||||
|
||||
|
||||
class RecordObj:
|
||||
def __init__(self, name_list, record_list, frame_id, frame_id_completed=-1):
|
||||
self.name_list = name_list
|
||||
self.record_list = record_list
|
||||
self.frame_id = frame_id
|
||||
self.frame_id_completed = frame_id_completed
|
||||
|
||||
def __getattr__(self, __name: str):
|
||||
self.name_list += "." + __name
|
||||
return self
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
kwargs["__test_frame_id"] = self.frame_id
|
||||
kwargs["__test_frame_id_completed"] = self.frame_id_completed
|
||||
if self.name_list == "active_microscope.stages.keys":
|
||||
return ["x", "y", "z", "theta", "f"]
|
||||
print("* calling", self.name_list, args, kwargs)
|
||||
self.record_list.append((self.name_list, args, kwargs))
|
||||
|
||||
|
||||
class DummyModelToTestFeatures:
|
||||
def __init__(self, configuration):
|
||||
self.configuration = configuration
|
||||
|
||||
self.device = DummyDevice()
|
||||
self.signal_pipe, self.data_pipe = None, None
|
||||
|
||||
self.signal_container = None
|
||||
self.data_container = None
|
||||
self.signal_thread = None
|
||||
self.data_thread = None
|
||||
|
||||
self.stop_acquisition = False
|
||||
self.frame_id = 0 # signal_num
|
||||
self.frame_id_completed = -1
|
||||
|
||||
self.data = []
|
||||
self.signal_records = []
|
||||
self.data_records = []
|
||||
|
||||
def signal_func(self):
|
||||
self.signal_container.reset()
|
||||
while not self.signal_container.end_flag:
|
||||
if self.signal_container:
|
||||
self.signal_container.run()
|
||||
|
||||
self.signal_pipe.send("signal")
|
||||
self.signal_pipe.recv()
|
||||
|
||||
self.frame_id_completed += 1
|
||||
|
||||
if self.signal_container:
|
||||
self.signal_container.run(wait_response=True)
|
||||
|
||||
self.frame_id += 1 # signal_num
|
||||
|
||||
self.signal_pipe.send("shutdown")
|
||||
|
||||
self.stop_acquisition = True
|
||||
|
||||
def data_func(self):
|
||||
while not self.stop_acquisition:
|
||||
self.data_pipe.send("getData")
|
||||
frame_ids = self.data_pipe.recv()
|
||||
print("receive: ", frame_ids)
|
||||
if not frame_ids:
|
||||
continue
|
||||
|
||||
self.data.append(frame_ids)
|
||||
|
||||
if self.data_container:
|
||||
self.data_container.run(frame_ids)
|
||||
self.data_pipe.send("shutdown")
|
||||
|
||||
def start(self, feature_list):
|
||||
if feature_list is None:
|
||||
return False
|
||||
self.data = []
|
||||
self.signal_records = []
|
||||
self.data_records = []
|
||||
self.stop_acquisition = False
|
||||
self.frame_id = 0 # signal_num
|
||||
self.frame_id_completed = -1
|
||||
|
||||
self.signal_pipe, self.data_pipe = self.device.setup()
|
||||
|
||||
self.signal_container, self.data_container = load_features(self, feature_list)
|
||||
self.signal_thread = threading.Thread(target=self.signal_func, name="signal")
|
||||
self.data_thread = threading.Thread(target=self.data_func, name="data")
|
||||
self.signal_thread.start()
|
||||
self.data_thread.start()
|
||||
|
||||
self.signal_thread.join()
|
||||
self.stop_acquisition = True
|
||||
self.data_thread.join()
|
||||
|
||||
return True
|
||||
|
||||
def get_stage_position(self):
|
||||
axes = ["x", "y", "z", "theta", "f"]
|
||||
stage_pos = self.configuration["experiment"]["StageParameters"]
|
||||
return dict(map(lambda axis: (axis + "_pos", stage_pos[axis]), axes))
|
||||
|
||||
def __getattr__(self, __name: str):
|
||||
return RecordObj(
|
||||
__name, self.signal_records, self.frame_id, self.frame_id_completed
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def dummy_model_to_test_features(dummy_model):
|
||||
model = DummyModelToTestFeatures(dummy_model.configuration)
|
||||
return model
|
||||
970
test/model/features/test_aslm_feature_container.py
Normal file
970
test/model/features/test_aslm_feature_container.py
Normal file
@@ -0,0 +1,970 @@
|
||||
# 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
|
||||
import random
|
||||
import threading
|
||||
|
||||
from navigate.model.features.feature_container import (
|
||||
SignalNode,
|
||||
DataNode,
|
||||
DataContainer,
|
||||
load_features,
|
||||
)
|
||||
from navigate.model.features.common_features import WaitToContinue, LoopByCount
|
||||
from navigate.model.features.feature_container import dummy_True
|
||||
from test.model.dummy import DummyModel
|
||||
|
||||
|
||||
class DummyFeature:
|
||||
def __init__(self, *args):
|
||||
"""
|
||||
args:
|
||||
0: model
|
||||
1: name
|
||||
2: with response (True/False) (1/0)
|
||||
3: device related (True/False) (1/0)
|
||||
4: multi step (integer >= 1)
|
||||
5: has data function? There could be no data functions when node_type is 'multi-step'
|
||||
"""
|
||||
self.init_times = 0
|
||||
self.running_times_main_func = 0
|
||||
self.running_times_response_func = 0
|
||||
self.running_times_cleanup_func = 0
|
||||
self.is_end = False
|
||||
self.is_closed = False
|
||||
|
||||
self.model = None if len(args) == 0 else args[0]
|
||||
self.feature_name = args[1] if len(args) > 1 else "none"
|
||||
self.config_table = {
|
||||
"signal": {
|
||||
"name-for-test": self.feature_name,
|
||||
"init": self.signal_init_func,
|
||||
"main": self.signal_main_func,
|
||||
},
|
||||
"data": {
|
||||
"name-for-test": self.feature_name,
|
||||
"init": self.data_init_func,
|
||||
"main": self.data_main_func,
|
||||
},
|
||||
"node": {},
|
||||
}
|
||||
|
||||
if len(args) > 2 and args[2]:
|
||||
self.config_table["signal"]["main-response"] = self.signal_wait_func
|
||||
self.has_response_func = True
|
||||
else:
|
||||
self.has_response_func = False
|
||||
|
||||
if len(args) > 3:
|
||||
self.config_table["node"]["device_related"] = args[3] == 1
|
||||
|
||||
if len(args) > 4 and args[4] > 1:
|
||||
self.config_table["node"]["node_type"] = "multi-step"
|
||||
self.multi_steps = args[4]
|
||||
self.config_table["signal"]["end"] = self.signal_end_func
|
||||
self.config_table["data"]["end"] = self.data_end_func
|
||||
else:
|
||||
self.multi_steps = 1
|
||||
|
||||
if len(args) > 5 and args[4] > 1 and args[2] == False and args[5] == False:
|
||||
self.config_table["data"] = {}
|
||||
|
||||
self.target_frame_id = 0
|
||||
self.response_value = 0
|
||||
self.current_signal_step = 0
|
||||
self.current_data_step = 0
|
||||
self.wait_lock = threading.Lock()
|
||||
|
||||
def init_func(self):
|
||||
self.init_times += 1
|
||||
|
||||
def main_func(self, value=None):
|
||||
self.running_times_main_func += 1
|
||||
return value
|
||||
|
||||
def response_func(self, value=None):
|
||||
self.running_times_response_func += 1
|
||||
return value
|
||||
|
||||
def end_func(self):
|
||||
return self.is_end
|
||||
|
||||
def close(self):
|
||||
self.is_closed = True
|
||||
self.running_times_cleanup_func += 1
|
||||
|
||||
def clear(self):
|
||||
self.init_times = 0
|
||||
self.running_times_main_func = 0
|
||||
self.running_times_response_func = 0
|
||||
self.running_times_cleanup_func = 0
|
||||
self.is_end = False
|
||||
self.is_closed = False
|
||||
|
||||
def signal_init_func(self, *args):
|
||||
self.target_frame_id = -1
|
||||
self.current_signal_step = 0
|
||||
if self.wait_lock.locked():
|
||||
self.wait_lock.release()
|
||||
|
||||
def signal_main_func(self, *args):
|
||||
self.target_frame_id = self.model.frame_id # signal_num
|
||||
if self.feature_name.startswith("node"):
|
||||
self.model.signal_records.append((self.target_frame_id, self.feature_name))
|
||||
if self.has_response_func:
|
||||
self.wait_lock.acquire()
|
||||
print(
|
||||
self.feature_name, ": wait lock is acquired!!!!", self.target_frame_id
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def signal_wait_func(self, *args):
|
||||
self.wait_lock.acquire()
|
||||
self.wait_lock.release()
|
||||
print(self.feature_name, ": wait response!(signal)", self.response_value)
|
||||
return self.response_value
|
||||
|
||||
def signal_end_func(self):
|
||||
self.current_signal_step += 1
|
||||
return self.current_signal_step >= self.multi_steps
|
||||
|
||||
def data_init_func(self):
|
||||
self.current_data_step = 0
|
||||
pass
|
||||
|
||||
def data_pre_main_func(self, frame_ids):
|
||||
return self.target_frame_id in frame_ids
|
||||
|
||||
def data_main_func(self, frame_ids):
|
||||
# assert self.target_frame_id in frame_ids, 'frame is not ready'
|
||||
if self.feature_name.startswith("node"):
|
||||
self.model.data_records.append((frame_ids[0], self.feature_name))
|
||||
|
||||
if self.has_response_func and self.wait_lock.locked():
|
||||
# random Yes/No
|
||||
self.response_value = random.randint(0, 1)
|
||||
print(
|
||||
self.feature_name,
|
||||
": wait lock is released!(data)",
|
||||
frame_ids,
|
||||
self.response_value,
|
||||
)
|
||||
self.wait_lock.release()
|
||||
return self.response_value
|
||||
return True
|
||||
|
||||
def data_end_func(self):
|
||||
self.current_data_step += 1
|
||||
return self.current_data_step >= self.multi_steps
|
||||
|
||||
|
||||
def generate_random_feature_list(
|
||||
has_response_func=False, multi_step=False, with_data_func=True, loop_node=False
|
||||
):
|
||||
feature_list = []
|
||||
m = random.randint(1, 10)
|
||||
node_count = 0
|
||||
for i in range(m):
|
||||
n = random.randint(1, 10)
|
||||
temp = []
|
||||
for j in range(n):
|
||||
has_response = random.randint(0, 1) if has_response_func else 0
|
||||
device_related = random.randint(0, 1)
|
||||
steps = random.randint(1, 10) if multi_step else 1
|
||||
steps = 1 if steps < 5 else steps
|
||||
if with_data_func == False:
|
||||
no_data_func = random.randint(0, 1)
|
||||
else:
|
||||
no_data_func = 0
|
||||
if steps >= 5 and no_data_func:
|
||||
has_response = False
|
||||
feature = {
|
||||
"name": DummyFeature,
|
||||
"args": (
|
||||
f"multi-step{node_count}",
|
||||
has_response,
|
||||
1,
|
||||
steps,
|
||||
False,
|
||||
),
|
||||
}
|
||||
temp.append(feature)
|
||||
temp.append({"name": WaitToContinue})
|
||||
else:
|
||||
feature = {
|
||||
"name": DummyFeature,
|
||||
"args": (
|
||||
f"node{node_count}",
|
||||
has_response,
|
||||
device_related,
|
||||
steps,
|
||||
),
|
||||
}
|
||||
if has_response:
|
||||
feature["node"] = {"need_response": True}
|
||||
temp.append(feature)
|
||||
node_count += 1
|
||||
# has response function means that node can only have child node
|
||||
if has_response:
|
||||
break
|
||||
turn_to_loop_flag = random.randint(0, 1) if loop_node else 0
|
||||
if turn_to_loop_flag:
|
||||
temp.append({"name": LoopByCount, "args": (3,)})
|
||||
node_count += 1
|
||||
temp = tuple(temp)
|
||||
feature_list.append(temp)
|
||||
return feature_list
|
||||
|
||||
|
||||
def print_feature_list(feature_list):
|
||||
result = []
|
||||
for features in feature_list:
|
||||
temp = []
|
||||
for node in features:
|
||||
if "args" in node:
|
||||
temp.append(node["args"])
|
||||
else:
|
||||
temp.append(node["name"].__name__)
|
||||
if type(features) is tuple:
|
||||
temp = tuple(temp)
|
||||
result.append(temp)
|
||||
print("--------feature list-------------")
|
||||
print(result)
|
||||
print("---------------------------------")
|
||||
return str(result)
|
||||
|
||||
|
||||
def convert_to_feature_list(feature_str):
|
||||
result = []
|
||||
for features in feature_str:
|
||||
temp = []
|
||||
for feature in features:
|
||||
if type(feature) == str:
|
||||
node = {"name": WaitToContinue}
|
||||
else:
|
||||
node = {"name": DummyFeature, "args": (*feature,)}
|
||||
temp.append(node)
|
||||
result.append(temp)
|
||||
return result
|
||||
|
||||
|
||||
class TestFeatureContainer(unittest.TestCase):
|
||||
def setUp(self):
|
||||
print("-------------new test case-----------------")
|
||||
|
||||
@unittest.skip("takes long time to finish the test")
|
||||
def test_feature_container(self):
|
||||
model = DummyModel()
|
||||
print("# test signal and data are synchronous")
|
||||
print("--all function nodes are single step")
|
||||
|
||||
print("----all signal nodes are without waiting function")
|
||||
for i in range(10):
|
||||
feature_list = generate_random_feature_list()
|
||||
model.start(feature_list)
|
||||
print(model.signal_records)
|
||||
print(model.data_records)
|
||||
assert model.signal_records == model.data_records, print_feature_list(
|
||||
feature_list
|
||||
)
|
||||
|
||||
print("----some signal nodes have waiting function")
|
||||
for i in range(10):
|
||||
# feature_list = convert_to_feature_list([[('node0', 1, 1, 1)], [('node1', 1, 0, 1)], [('node2', 1, 1, 1)], [('node3', 1, 0, 1)], [('node4', 1, 0, 1)], [('node5', 1, 0, 1)], [('node6', 1, 0, 1)], [('node7', 1, 1, 1)], [('node8', 0, 1, 1), ('node9', 0, 1, 1), ('node10', 1, 0, 1)], [('node11', 1, 1, 1)]])
|
||||
feature_list = generate_random_feature_list(has_response_func=True)
|
||||
print_feature_list(feature_list)
|
||||
model.start(feature_list)
|
||||
print(model.signal_records)
|
||||
print(model.data_records)
|
||||
assert model.signal_records == model.data_records, print_feature_list(
|
||||
feature_list
|
||||
)
|
||||
|
||||
print("--Some function nodes are multi-step")
|
||||
print(
|
||||
"----multi-step nodes have both signal and data functions, and without waiting function"
|
||||
)
|
||||
for i in range(10):
|
||||
feature_list = generate_random_feature_list(multi_step=True)
|
||||
# feature_list = convert_to_feature_list([[('node0', 0, 0, 10), ('node1', 0, 1, 5), ('node2', 0, 0, 1), ('node3', 0, 0, 9)], [('node4', 0, 1, 8), ('node5', 0, 1, 10), ('node6', 0, 1, 6), ('node7', 0, 0, 1), ('node8', 0, 0, 6), ('node9', 0, 1, 1), ('node10', 0, 0, 6)], [('node11', 0, 0, 1), ('node12', 0, 0, 1), ('node13', 0, 0, 1), ('node14', 0, 1, 7), ('node15', 0, 0, 1)], [('node16', 0, 0, 1), ('node17', 0, 1, 1), ('node18', 0, 1, 7), ('node19', 0, 1, 7), ('node20', 0, 1, 10), ('node21', 0, 0, 1), ('node22', 0, 1, 9), ('node23', 0, 1, 1), ('node24', 0, 0, 10), ('node25', 0, 0, 6)], [('node26', 0, 1, 1), ('node27', 0, 1, 7), ('node28', 0, 1, 8), ('node29', 0, 1, 7), ('node30', 0, 0, 5), ('node31', 0, 1, 1), ('node32', 0, 0, 10)]])
|
||||
# feature_list = convert_to_feature_list([[('node0', 0, 1, 2), ('node1', 0, 0, 3)]])
|
||||
# feature_list = convert_to_feature_list([[('node0', 0, 0, 5)], [('node1', 0, 0, 5), ('node2', 0, 0, 10), ('node3', 0, 1, 7), ('node4', 0, 0, 1), ('node5', 0, 0, 9), ('node6', 0, 1, 9)], [('node7', 0, 0, 9), ('node8', 0, 0, 6), ('node9', 0, 0, 7), ('node10', 0, 1, 3), ('node11', 0, 1, 6), ('node12', 0, 1, 5), ('node13', 0, 0, 4), ('node14', 0, 0, 1), ('node15', 0, 0, 2)], [('node16', 0, 0, 5), ('node17', 0, 1, 2), ('node18', 0, 0, 6), ('node19', 0, 0, 3)], [('node20', 0, 0, 9), ('node21', 0, 0, 7), ('node22', 0, 0, 1), ('node23', 0, 0, 8), ('node24', 0, 0, 2), ('node25', 0, 1, 7), ('node26', 0, 0, 9)], [('node27', 0, 0, 2), ('node28', 0, 1, 3), ('node29', 0, 0, 3), ('node30', 0, 0, 8)], [('node31', 0, 0, 8), ('node32', 0, 0, 10), ('node33', 0, 1, 4), ('node34', 0, 1, 2), ('node35', 0, 1, 8), ('node36', 0, 1, 4), ('node37', 0, 0, 5), ('node38', 0, 0, 9)], [('node39', 0, 0, 9), ('node40', 0, 1, 8), ('node41', 0, 1, 4)], [('node42', 0, 0, 1), ('node43', 0, 0, 1), ('node44', 0, 1, 1), ('node45', 0, 0, 2), ('node46', 0, 1, 3)]])
|
||||
print_feature_list(feature_list)
|
||||
model.start(feature_list)
|
||||
print(model.signal_records)
|
||||
print(model.data_records)
|
||||
assert model.signal_records == model.data_records, print_feature_list(
|
||||
feature_list
|
||||
)
|
||||
|
||||
print(
|
||||
"----multi-step nodes have both signal and data functions, and with waiting function"
|
||||
)
|
||||
for i in range(10):
|
||||
# feature_list = convert_to_feature_list([[('node0', 0, 0, 1), ('node1', 1, 1, 1)], [('node2', 1, 1, 7)], [('node3', 0, 1, 1), ('node4', 0, 1, 9), ('node5', 1, 1, 1)], [('node6', 1, 0, 1)], [('node7', 0, 1, 6), ('node8', 1, 1, 1)], [('node9', 0, 1, 7), ('node10', 1, 0, 1)], [('node11', 0, 0, 7), ('node12', 0, 0, 6), ('node13', 0, 0, 5)], [('node14', 0, 1, 6), ('node15', 0, 0, 1), ('node16', 0, 1, 9), ('node17', 0, 1, 10), ('node18', 0, 0, 1), ('node19', 0, 0, 1), ('node20', 0, 0, 1), ('node21', 1, 1, 1)], [('node22', 1, 0, 1)]])
|
||||
# feature_list = convert_to_feature_list([[('node0', 1, 1, 5)], [('node1', 0, 1, 5), ('node2', 1, 1, 1)], [('node3', 1, 1, 6)], [('node4', 1, 1, 9)], [('node5', 0, 1, 6), ('node6', 1, 0, 1)], [('node7', 1, 1, 6)], [('node8', 1, 1, 5)]])
|
||||
# feature_list = convert_to_feature_list([[('node0', 0, 0, 6), ('node1', 0, 1, 1), ('node2', 0, 1, 8)], [('node3', 0, 1, 1), ('node4', 0, 1, 1), ('node5', 1, 0, 6)], [('node6', 0, 1, 1), ('node7', 1, 0, 1)]])
|
||||
feature_list = generate_random_feature_list(
|
||||
has_response_func=True, multi_step=True
|
||||
)
|
||||
print_feature_list(feature_list)
|
||||
model.start(feature_list)
|
||||
print(model.signal_records)
|
||||
print(model.data_records)
|
||||
assert model.signal_records == model.data_records, print_feature_list(
|
||||
feature_list
|
||||
)
|
||||
|
||||
print("----some multi-step nodes don't have data functions")
|
||||
for i in range(10):
|
||||
# feature_list = convert_to_feature_list([[('node0', 0, 1, 1), ('multi-step1', False, 0, 6, False), 'WaitToContinue', ('multi-step2', False, 0, 8, False), 'WaitToContinue', ('node3', 0, 1, 1), ('multi-step4', False, 0, 9, False), 'WaitToContinue'], [('node5', 0, 0, 6)]])
|
||||
# feature_list = convert_to_feature_list([[('node0', 1, 1, 1)], [('node1', 0, 1, 9), ('node2', 0, 1, 1), ('node3', 0, 0, 1), ('multi-step4', False, 0, 9, False), 'WaitToContinue', ('multi-step5', False, 0, 5, False), 'WaitToContinue', ('node6', 1, 1, 1)]])
|
||||
# feature_list = convert_to_feature_list([[('multi-step0', False, 1, 9, False), 'WaitToContinue', ('multi-step1', False, 1, 8, False), 'WaitToContinue', ('multi-step2', False, 1, 7, False), 'WaitToContinue', ('multi-step3', False, 1, 7, False), 'WaitToContinue', ('multi-step4', False, 1, 7, False), 'WaitToContinue', ('node5', 1, 0, 5)], [('node6', 1, 1, 1)], [('node7', 0, 0, 1), ('node8', 0, 1, 1), ('node9', 1, 0, 1)], [('node10', 1, 1, 10)], [('multi-step11', False, 1, 6, False), 'WaitToContinue', ('node12', 1, 0, 1)], [('multi-step13', False, 1, 8, False), 'WaitToContinue', ('multi-step14', False, 1, 9, False), 'WaitToContinue', ('node15', 0, 1, 1), ('multi-step16', False, 1, 9, False), 'WaitToContinue', ('node17', 1, 0, 10)], [('multi-step18', False, 1, 9, False), 'WaitToContinue'], [('node19', 1, 1, 7)], [('node20', 1, 1, 1)]])
|
||||
feature_list = generate_random_feature_list(
|
||||
has_response_func=True, multi_step=True, with_data_func=False
|
||||
)
|
||||
print_feature_list(feature_list)
|
||||
model.start(feature_list)
|
||||
print(model.signal_records)
|
||||
print(model.data_records)
|
||||
assert model.signal_records == model.data_records, print_feature_list(
|
||||
feature_list
|
||||
)
|
||||
|
||||
print("----with loop node")
|
||||
# test case: (,)
|
||||
feature_list = [
|
||||
(
|
||||
{
|
||||
"name": DummyFeature,
|
||||
"args": (
|
||||
"node0",
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
),
|
||||
},
|
||||
{"name": LoopByCount, "args": (3,)},
|
||||
)
|
||||
]
|
||||
model.start(feature_list)
|
||||
print(model.signal_records)
|
||||
print(model.data_records)
|
||||
assert model.signal_records == model.data_records, print_feature_list(
|
||||
feature_list
|
||||
)
|
||||
assert model.signal_records == [(0, "node0"), (1, "node0"), (2, "node0")]
|
||||
|
||||
# test case: random loop
|
||||
for i in range(10):
|
||||
# feature_list = [({'name': DummyFeature, 'args': ('node0', 0, 0, 1,),}, {'name': LoopByCount, 'args': (3,)}), [{'name': DummyFeature, 'args': ('node1', 0, 0, 1,),}, {'name': DummyFeature, 'args': ('node2', 0, 0, 1,),}], ({'name': DummyFeature, 'args': ('node3', 0, 0, 1,),}, {'name': LoopByCount, 'args': (3,)}), ({'name': DummyFeature, 'args': ('node4', 0, 0, 1,),}, {'name': DummyFeature, 'args': ('node5', 0, 0, 1,),}, {'name': LoopByCount, 'args': (3,)})]
|
||||
feature_list = generate_random_feature_list(
|
||||
has_response_func=True,
|
||||
multi_step=True,
|
||||
with_data_func=False,
|
||||
loop_node=True,
|
||||
)
|
||||
print_feature_list(feature_list)
|
||||
model.start(feature_list)
|
||||
print(model.signal_records)
|
||||
print(model.data_records)
|
||||
assert model.signal_records == model.data_records, print_feature_list(
|
||||
feature_list
|
||||
)
|
||||
|
||||
print("----nested loop nodes")
|
||||
# test case: ((), , ), , ,
|
||||
feature_list = [
|
||||
(
|
||||
(
|
||||
generate_random_feature_list(
|
||||
has_response_func=True,
|
||||
multi_step=True,
|
||||
with_data_func=False,
|
||||
loop_node=True,
|
||||
),
|
||||
{"name": LoopByCount, "args": (3,)},
|
||||
),
|
||||
{
|
||||
"name": DummyFeature,
|
||||
"args": (
|
||||
"node100",
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
),
|
||||
},
|
||||
{"name": LoopByCount, "args": (3,)},
|
||||
),
|
||||
{
|
||||
"name": DummyFeature,
|
||||
"args": (
|
||||
"node101",
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": DummyFeature,
|
||||
"args": (
|
||||
"node102",
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
),
|
||||
},
|
||||
]
|
||||
model.start(feature_list)
|
||||
print(model.signal_records)
|
||||
print(model.data_records)
|
||||
assert model.signal_records == model.data_records, print_feature_list(
|
||||
feature_list
|
||||
)
|
||||
|
||||
# test case: (,,(),), ,
|
||||
feature_list = [
|
||||
(
|
||||
{
|
||||
"name": DummyFeature,
|
||||
"args": (
|
||||
"node200",
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": DummyFeature,
|
||||
"args": (
|
||||
"node201",
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
),
|
||||
},
|
||||
(
|
||||
generate_random_feature_list(
|
||||
has_response_func=True,
|
||||
multi_step=True,
|
||||
with_data_func=False,
|
||||
loop_node=True,
|
||||
),
|
||||
{"name": LoopByCount, "args": (3,)},
|
||||
),
|
||||
{
|
||||
"name": DummyFeature,
|
||||
"args": (
|
||||
"node100",
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
),
|
||||
},
|
||||
{"name": LoopByCount, "args": (3,)},
|
||||
),
|
||||
{
|
||||
"name": DummyFeature,
|
||||
"args": (
|
||||
"node101",
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": DummyFeature,
|
||||
"args": (
|
||||
"node102",
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
),
|
||||
},
|
||||
]
|
||||
model.start(feature_list)
|
||||
print(model.signal_records)
|
||||
print(model.data_records)
|
||||
assert model.signal_records == model.data_records, print_feature_list(
|
||||
feature_list
|
||||
)
|
||||
|
||||
# test case: (((((),),),),), ,
|
||||
feature_list = [
|
||||
(
|
||||
(
|
||||
(
|
||||
(
|
||||
generate_random_feature_list(loop_node=True),
|
||||
{"name": LoopByCount, "args": (3,)},
|
||||
),
|
||||
{"name": LoopByCount, "args": (3,)},
|
||||
),
|
||||
{"name": LoopByCount, "args": (3,)},
|
||||
),
|
||||
{"name": LoopByCount, "args": (3,)},
|
||||
),
|
||||
{
|
||||
"name": DummyFeature,
|
||||
"args": (
|
||||
"node101",
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": DummyFeature,
|
||||
"args": (
|
||||
"node102",
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
),
|
||||
},
|
||||
]
|
||||
model.start(feature_list)
|
||||
print(model.signal_records)
|
||||
print(model.data_records)
|
||||
assert model.signal_records == model.data_records, print_feature_list(
|
||||
feature_list
|
||||
)
|
||||
|
||||
# test case: (,(,(,(,(),),),),), ,
|
||||
feature_list = [
|
||||
{
|
||||
"name": DummyFeature,
|
||||
"args": (
|
||||
"node200",
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
),
|
||||
},
|
||||
(
|
||||
{
|
||||
"name": DummyFeature,
|
||||
"args": (
|
||||
"node300",
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
),
|
||||
},
|
||||
(
|
||||
{
|
||||
"name": DummyFeature,
|
||||
"args": (
|
||||
"node400",
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
),
|
||||
},
|
||||
(
|
||||
{
|
||||
"name": DummyFeature,
|
||||
"args": (
|
||||
"node500",
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
),
|
||||
},
|
||||
(
|
||||
generate_random_feature_list(loop_node=True),
|
||||
{"name": LoopByCount, "args": (3,)},
|
||||
),
|
||||
{"name": LoopByCount, "args": (3,)},
|
||||
),
|
||||
{"name": LoopByCount, "args": (3,)},
|
||||
),
|
||||
{"name": LoopByCount, "args": (3,)},
|
||||
),
|
||||
{
|
||||
"name": DummyFeature,
|
||||
"args": (
|
||||
"node101",
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": DummyFeature,
|
||||
"args": (
|
||||
"node102",
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
),
|
||||
},
|
||||
]
|
||||
model.start(feature_list)
|
||||
print(model.signal_records)
|
||||
print(model.data_records)
|
||||
assert model.signal_records == model.data_records, print_feature_list(
|
||||
feature_list
|
||||
)
|
||||
|
||||
def test_load_feature(self):
|
||||
def check(tnode1, tnode2):
|
||||
if tnode1 is None and tnode2 is None:
|
||||
return True
|
||||
if tnode1 is None or tnode2 is None:
|
||||
return False
|
||||
return tnode1.node_name == tnode2.node_name
|
||||
|
||||
def is_isomorphic(tree1, tree2):
|
||||
p, q = tree1, tree2
|
||||
stack = []
|
||||
visited = {}
|
||||
while p or q or stack:
|
||||
if not check(p, q):
|
||||
return False
|
||||
if p is None:
|
||||
p, q = stack.pop()
|
||||
elif p.node_name not in visited:
|
||||
visited[p.node_name] = True
|
||||
stack.append((p.sibling, q.sibling))
|
||||
p, q = p.child, q.child
|
||||
else:
|
||||
p, q = None, None
|
||||
return True
|
||||
|
||||
# generates 10 random feature lists and verify whether they are loaded correctly
|
||||
for i in range(10):
|
||||
feature_list = generate_random_feature_list(loop_node=True)
|
||||
signal_container, data_container = load_features(self, feature_list)
|
||||
assert is_isomorphic(signal_container.root, data_container.root)
|
||||
print("-", i, "random feature list is correct!")
|
||||
|
||||
def test_signal_node(self):
|
||||
feature = DummyFeature()
|
||||
func_dict = {
|
||||
"init": feature.init_func,
|
||||
"main": feature.main_func,
|
||||
"end": feature.end_func,
|
||||
}
|
||||
|
||||
print("without waiting for a response:")
|
||||
node = SignalNode("test_1", func_dict)
|
||||
assert node.need_response == False
|
||||
assert node.node_funcs["end"]() == False
|
||||
|
||||
feature.is_end = True
|
||||
assert node.node_funcs["end"]() == True
|
||||
|
||||
result, is_end = node.run()
|
||||
assert feature.init_times == 1
|
||||
assert feature.running_times_main_func == 1
|
||||
assert result == None
|
||||
assert is_end == True
|
||||
assert node.is_initialized == False
|
||||
|
||||
result, is_end = node.run(True)
|
||||
assert feature.init_times == 2
|
||||
assert feature.running_times_main_func == 2
|
||||
assert result == True
|
||||
assert is_end == True
|
||||
assert node.is_initialized == False
|
||||
assert node.wait_response == False
|
||||
|
||||
print("--running with waiting option")
|
||||
feature.clear()
|
||||
result, is_end = node.run(wait_response=True)
|
||||
assert is_end == True
|
||||
assert node.is_initialized == False
|
||||
assert node.wait_response == False
|
||||
assert feature.running_times_main_func == 1
|
||||
assert feature.init_times == 1
|
||||
|
||||
print("--device related")
|
||||
feature.clear()
|
||||
node = SignalNode("test_1", func_dict, device_related=True)
|
||||
print(node.node_type)
|
||||
assert node.need_response == False
|
||||
result, is_end = node.run()
|
||||
assert is_end == True
|
||||
assert node.wait_response == False
|
||||
assert feature.running_times_main_func == 1
|
||||
|
||||
print("----running with waitint option")
|
||||
feature.clear()
|
||||
result, is_end = node.run(wait_response=True)
|
||||
assert is_end == False
|
||||
assert node.wait_response == False
|
||||
assert feature.running_times_main_func == 0
|
||||
assert node.is_initialized == True
|
||||
|
||||
print("----multi-step function")
|
||||
feature.clear()
|
||||
node = SignalNode(
|
||||
"test_1", func_dict, device_related=True, node_type="multi-step"
|
||||
)
|
||||
# node.node_type = "multi-step"
|
||||
func_dict["main-response"] = dummy_True
|
||||
# assert func_dict.get("main-response", None) == None
|
||||
assert node.need_response == True
|
||||
steps = 5
|
||||
for i in range(steps + 1):
|
||||
feature.is_end = i == steps
|
||||
if i == 0:
|
||||
assert node.is_initialized == False
|
||||
else:
|
||||
assert node.is_initialized == True
|
||||
result, is_end = node.run()
|
||||
if i <= steps:
|
||||
assert node.is_initialized == True
|
||||
assert is_end == False
|
||||
assert feature.running_times_main_func == i + 1
|
||||
assert node.wait_response == True
|
||||
result, is_end = node.run(wait_response=True)
|
||||
if i < steps:
|
||||
assert is_end == False
|
||||
else:
|
||||
assert is_end == True
|
||||
|
||||
print("--multi-step function")
|
||||
feature.clear()
|
||||
node = SignalNode(
|
||||
"test_1", func_dict, node_type="multi-step", device_related=True
|
||||
)
|
||||
# assert func_dict.get("main-response") == None
|
||||
assert node.need_response == True
|
||||
assert node.device_related == True
|
||||
steps = 5
|
||||
for i in range(steps + 1):
|
||||
feature.is_end = i == steps
|
||||
result, is_end = node.run()
|
||||
if i < steps:
|
||||
assert is_end == False
|
||||
assert is_end == False
|
||||
assert feature.running_times_main_func == i + 1
|
||||
assert node.is_initialized == True
|
||||
assert node.wait_response == True
|
||||
result, is_end = node.run(wait_response=True)
|
||||
assert node.wait_response == False
|
||||
assert node.is_initialized == False
|
||||
|
||||
print("wait for a response:")
|
||||
feature.clear()
|
||||
func_dict["main-response"] = feature.response_func
|
||||
node = SignalNode("test_2", func_dict, need_response=True)
|
||||
assert node.need_response == True
|
||||
assert node.wait_response == False
|
||||
|
||||
print("--running without waiting option")
|
||||
result, is_end = node.run()
|
||||
assert result == None
|
||||
assert is_end == False
|
||||
assert node.is_initialized == True
|
||||
assert node.wait_response == True
|
||||
|
||||
result, is_end = node.run(True)
|
||||
assert result == True
|
||||
assert is_end == False
|
||||
assert feature.init_times == 1
|
||||
assert feature.running_times_main_func == 2
|
||||
assert node.wait_response == True
|
||||
assert node.is_initialized == True
|
||||
|
||||
print("--running with waiting option")
|
||||
result, is_end = node.run(wait_response=True)
|
||||
assert feature.running_times_main_func == 2
|
||||
assert feature.running_times_response_func == 1
|
||||
assert node.wait_response == False
|
||||
assert node.is_initialized == False
|
||||
assert is_end == True
|
||||
|
||||
feature.clear()
|
||||
result, is_end = node.run(wait_response=True)
|
||||
assert is_end == False
|
||||
assert feature.init_times == 1
|
||||
assert feature.running_times_main_func == 0
|
||||
assert feature.running_times_response_func == 0
|
||||
assert node.is_initialized == True
|
||||
|
||||
print("----device related")
|
||||
node.device_related = True
|
||||
feature.clear()
|
||||
result, is_end = node.run()
|
||||
assert is_end == False
|
||||
assert feature.running_times_main_func == 1
|
||||
assert node.wait_response == True
|
||||
assert node.is_initialized == True
|
||||
|
||||
result, is_end = node.run(wait_response=True)
|
||||
assert is_end == True
|
||||
assert feature.running_times_response_func == 1
|
||||
assert feature.running_times_main_func == 1
|
||||
assert node.wait_response == False
|
||||
assert node.is_initialized == False
|
||||
|
||||
feature.clear()
|
||||
result, is_end = node.run(wait_response=True)
|
||||
assert is_end == False
|
||||
assert feature.running_times_main_func == 0
|
||||
assert node.wait_response == False
|
||||
assert feature.init_times == 1
|
||||
assert node.is_initialized == True
|
||||
|
||||
print("--multi-step function")
|
||||
feature.clear()
|
||||
node = SignalNode("test", func_dict, node_type="multi-step", need_response=True)
|
||||
steps = 5
|
||||
for i in range(steps + 1):
|
||||
feature.is_end = i == steps
|
||||
result, is_end = node.run()
|
||||
assert is_end == False
|
||||
assert feature.running_times_main_func == i + 1
|
||||
assert node.is_initialized == True
|
||||
result, is_end = node.run(wait_response=True)
|
||||
if i < steps:
|
||||
assert is_end == False
|
||||
else:
|
||||
assert is_end == True
|
||||
assert feature.running_times_response_func == i + 1
|
||||
assert node.wait_response == False
|
||||
|
||||
def test_node_cleanup(self):
|
||||
def wrap_error_func(func):
|
||||
def temp_func(raise_error=False):
|
||||
if raise_error:
|
||||
raise Exception
|
||||
func()
|
||||
|
||||
return temp_func
|
||||
|
||||
feature = DummyFeature()
|
||||
func_dict = {
|
||||
"init": feature.init_func,
|
||||
"pre-main": dummy_True,
|
||||
"main": wrap_error_func(feature.main_func),
|
||||
"end": feature.end_func,
|
||||
}
|
||||
# one-step node without response
|
||||
print("- one-step node without response")
|
||||
node = DataNode("cleanup_node", func_dict)
|
||||
data_container = DataContainer(node)
|
||||
assert data_container.root == node
|
||||
data_container.run()
|
||||
assert feature.running_times_main_func == 1, feature.running_times_main_func
|
||||
assert data_container.end_flag == True
|
||||
data_container.reset()
|
||||
data_container.run(True)
|
||||
assert node.is_marked == True
|
||||
assert feature.running_times_main_func == 1, feature.running_times_main_func
|
||||
|
||||
feature.clear()
|
||||
data_container.reset()
|
||||
func_dict["cleanup"] = feature.close
|
||||
node = DataNode("cleanup_node", func_dict)
|
||||
data_container = DataContainer(node)
|
||||
data_container.run()
|
||||
data_container.reset()
|
||||
data_container.run(True)
|
||||
assert feature.is_closed == True
|
||||
assert node.is_marked == True
|
||||
assert feature.running_times_main_func == 1
|
||||
data_container.run()
|
||||
assert feature.running_times_main_func == 1
|
||||
|
||||
# node with response
|
||||
print("- node with response")
|
||||
feature.clear()
|
||||
node = DataNode("cleanup_node", func_dict, need_response=True)
|
||||
data_container = DataContainer(node, [node])
|
||||
assert data_container.root == node
|
||||
data_container.run()
|
||||
assert feature.running_times_main_func == 1, feature.running_times_main_func
|
||||
data_container.reset()
|
||||
data_container.run(True)
|
||||
assert feature.running_times_cleanup_func == 1
|
||||
assert feature.is_closed == True
|
||||
assert node.is_marked == False
|
||||
assert feature.running_times_main_func == 1
|
||||
assert data_container.end_flag == True
|
||||
data_container.run()
|
||||
assert feature.running_times_main_func == 1
|
||||
|
||||
# multiple nodes
|
||||
print("- multiple nodes")
|
||||
feature.clear()
|
||||
node1 = DataNode("cleanup_node1", func_dict)
|
||||
node2 = DataNode("cleanup_node2", func_dict, device_related=True)
|
||||
node3 = DataNode(
|
||||
"cleanup_node3", func_dict, need_response=True, device_related=True
|
||||
)
|
||||
node1.sibling = node2
|
||||
node2.sibling = node3
|
||||
cleanup_list = [node1, node2, node3]
|
||||
data_container = DataContainer(node1, cleanup_list)
|
||||
assert data_container.root == node1
|
||||
assert feature.running_times_main_func == 0
|
||||
|
||||
for i in range(1, 4):
|
||||
data_container.run()
|
||||
assert feature.running_times_main_func == i, feature.running_times_main_func
|
||||
# mark a single node
|
||||
data_container.reset()
|
||||
data_container.run(True)
|
||||
assert feature.is_closed == True
|
||||
assert feature.running_times_cleanup_func == 1
|
||||
feature.is_closed = False
|
||||
assert node1.is_marked == True
|
||||
assert feature.running_times_main_func == 3
|
||||
assert data_container.end_flag == False
|
||||
data_container.run()
|
||||
assert feature.running_times_main_func == 4
|
||||
assert node2.is_marked == False
|
||||
data_container.run()
|
||||
assert feature.running_times_main_func == 5
|
||||
assert node3.is_marked == False
|
||||
# run node1 which is marked
|
||||
data_container.reset()
|
||||
data_container.run()
|
||||
assert feature.running_times_main_func == 5
|
||||
# run node2
|
||||
data_container.run()
|
||||
assert feature.running_times_main_func == 6
|
||||
assert node2.is_marked == False
|
||||
# run node3 and clean up all nodes
|
||||
data_container.run(True)
|
||||
assert feature.running_times_cleanup_func == 4
|
||||
assert feature.running_times_main_func == 6
|
||||
assert data_container.end_flag == True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
134
test/model/features/test_autofocus.py
Normal file
134
test/model/features/test_autofocus.py
Normal file
@@ -0,0 +1,134 @@
|
||||
# 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 numpy as np
|
||||
|
||||
# Local imports
|
||||
from navigate.model.features.autofocus import power_tent
|
||||
from navigate.model.features.autofocus import Autofocus
|
||||
from test.model.dummy import DummyModel
|
||||
|
||||
|
||||
class TestPowerTentFunction(unittest.TestCase):
|
||||
def test_power_tent(self):
|
||||
# Test with known parameters and expected result
|
||||
x = 2.0
|
||||
x_offset = 1.0
|
||||
y_offset = 0.0
|
||||
amplitude = 2.0
|
||||
sigma = 0.5
|
||||
alpha = 2.0
|
||||
|
||||
# Calculate the expected result manually
|
||||
expected_result = y_offset + amplitude * (
|
||||
1 - np.abs(sigma * (x - x_offset)) ** alpha
|
||||
)
|
||||
|
||||
# Call the function and check if the result is close to the expected result
|
||||
result = power_tent(x, x_offset, y_offset, amplitude, sigma, alpha)
|
||||
self.assertAlmostEqual(result, expected_result, places=6)
|
||||
|
||||
def test_power_tent_boundary_cases(self):
|
||||
# Test some boundary cases
|
||||
x_offset = 0.0
|
||||
y_offset = 0.0
|
||||
amplitude = 1.0
|
||||
sigma = 1.0
|
||||
alpha = 1.0
|
||||
|
||||
# Test at x = x_offset, should be y_offset + amplitude
|
||||
result = power_tent(x_offset, x_offset, y_offset, amplitude, sigma, alpha)
|
||||
self.assertAlmostEqual(result, y_offset + amplitude, places=6)
|
||||
|
||||
# Test at x = x_offset + 1, should be y_offset
|
||||
result = power_tent(x_offset + 1, x_offset, y_offset, amplitude, sigma, alpha)
|
||||
self.assertAlmostEqual(result, y_offset, places=6)
|
||||
|
||||
|
||||
class TestAutofocusClass(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Initialize an instance of the Autofocus class for testing
|
||||
model = DummyModel()
|
||||
model.active_microscope_name = "Mesoscale"
|
||||
self.autofocus = Autofocus(model=model, device="stage", device_ref="f")
|
||||
|
||||
def test_get_autofocus_frame_num(self):
|
||||
# Test the get_autofocus_frame_num method
|
||||
settings = {
|
||||
"coarse_selected": True,
|
||||
"coarse_range": 8.0,
|
||||
"coarse_step_size": 2.0,
|
||||
"fine_selected": True,
|
||||
"fine_range": 5.0,
|
||||
"fine_step_size": 1.0,
|
||||
}
|
||||
self.autofocus.model.configuration = {
|
||||
"experiment": {
|
||||
"AutoFocusParameters": {"Mesoscale": {"stage": {"f": settings}}}
|
||||
}
|
||||
}
|
||||
# Both Fine and Coarse Selected
|
||||
frames = self.autofocus.get_autofocus_frame_num()
|
||||
self.assertEqual(frames, 11) # Expected number of frames
|
||||
|
||||
# Only Coarse Selected
|
||||
self.autofocus.model.configuration["experiment"]["AutoFocusParameters"][
|
||||
"Mesoscale"
|
||||
]["stage"]["f"]["fine_selected"] = False
|
||||
self.autofocus.model.configuration["experiment"]["AutoFocusParameters"][
|
||||
"Mesoscale"
|
||||
]["stage"]["f"]["coarse_selected"] = True
|
||||
frames = self.autofocus.get_autofocus_frame_num()
|
||||
self.assertEqual(frames, 5) # Expected number of frames
|
||||
|
||||
# Only Fine Selected
|
||||
self.autofocus.model.configuration["experiment"]["AutoFocusParameters"][
|
||||
"Mesoscale"
|
||||
]["stage"]["f"]["fine_selected"] = True
|
||||
self.autofocus.model.configuration["experiment"]["AutoFocusParameters"][
|
||||
"Mesoscale"
|
||||
]["stage"]["f"]["coarse_selected"] = False
|
||||
frames = self.autofocus.get_autofocus_frame_num()
|
||||
self.assertEqual(frames, 6) # Expected number of frames
|
||||
|
||||
def test_get_steps(self):
|
||||
# Test the get_steps method
|
||||
steps, pos_offset = self.autofocus.get_steps(10.0, 2.0)
|
||||
self.assertEqual(steps, 6) # Expected number of steps
|
||||
self.assertEqual(pos_offset, 8.0) # Expected position offset
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
476
test/model/features/test_common_features.py
Normal file
476
test/model/features/test_common_features.py
Normal file
@@ -0,0 +1,476 @@
|
||||
# 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 random
|
||||
import pytest
|
||||
from navigate.model.features.common_features import ZStackAcquisition
|
||||
|
||||
|
||||
class TestZStack:
|
||||
@pytest.fixture(autouse=True)
|
||||
def _prepare_test(self, dummy_model_to_test_features):
|
||||
self.model = dummy_model_to_test_features
|
||||
self.model.virtual_microscopes = {}
|
||||
self.config = self.model.configuration["experiment"]["MicroscopeState"]
|
||||
self.record_num = 0
|
||||
self.feature_list = [[{"name": ZStackAcquisition}]]
|
||||
|
||||
self.config["start_position"] = 0
|
||||
self.config["end_position"] = 200
|
||||
self.config["number_z_steps"] = 5
|
||||
self.config["step_size"] = (
|
||||
self.config["end_position"] - self.config["start_position"]
|
||||
) / self.config["number_z_steps"]
|
||||
|
||||
position_list = self.model.configuration["multi_positions"]
|
||||
if len(position_list) < 5:
|
||||
for i in range(5):
|
||||
pos = [0] * len(position_list[0])
|
||||
for i in range(len(position_list[0])):
|
||||
pos[i] = random.randint(1, 10000)
|
||||
position_list.append(pos)
|
||||
|
||||
def get_next_record(self, record_prefix, idx):
|
||||
idx += 1
|
||||
while self.model.signal_records[idx][0] != record_prefix:
|
||||
idx += 1
|
||||
if idx >= self.record_num:
|
||||
assert False, "Some device movements are missed!"
|
||||
return idx
|
||||
|
||||
def exist_record(self, record_prefix, idx_start, idx_end):
|
||||
for i in range(idx_start, idx_end + 1):
|
||||
if self.model.signal_records[i][0] == record_prefix:
|
||||
return True
|
||||
return False
|
||||
|
||||
def z_stack_verification(self):
|
||||
self.record_num = len(self.model.signal_records)
|
||||
change_channel_func_str = "active_microscope.prepare_next_channel"
|
||||
close_daq_tasks_str = "active_microscope.daq.stop_acquisition"
|
||||
create_daq_tasks_str = "active_microscope.daq.prepare_acquisition"
|
||||
# save all the selected channels
|
||||
selected_channels = []
|
||||
for channel_key in self.config["channels"].keys():
|
||||
if self.config["channels"][channel_key]["is_selected"]:
|
||||
selected_channels.append(dict(self.config["channels"][channel_key]))
|
||||
selected_channels[-1]["id"] = int(channel_key[len("channel_") :])
|
||||
|
||||
# restore z and f
|
||||
pos_dict = self.model.get_stage_position()
|
||||
restore_z = pos_dict["z_pos"]
|
||||
restore_f = pos_dict["f_pos"]
|
||||
|
||||
mode = self.config["stack_cycling_mode"] # per_z/pre_stack
|
||||
is_multiposition = self.config["is_multiposition"]
|
||||
if is_multiposition:
|
||||
positions = self.model.configuration["multi_positions"][1:]
|
||||
else:
|
||||
pos_dict = self.model.configuration["experiment"]["StageParameters"]
|
||||
positions = [
|
||||
[
|
||||
pos_dict["x"],
|
||||
pos_dict["y"],
|
||||
self.config.get("stack_z_origin", pos_dict["z"]),
|
||||
pos_dict["theta"],
|
||||
self.config.get("stack_focus_origin", pos_dict["f"]),
|
||||
]
|
||||
]
|
||||
|
||||
z_step = self.config["step_size"]
|
||||
f_step = (self.config["end_focus"] - self.config["start_focus"]) / self.config[
|
||||
"number_z_steps"
|
||||
]
|
||||
|
||||
frame_id = 0
|
||||
idx = -1
|
||||
z_moved_times = 0
|
||||
if mode == "per_z":
|
||||
z_should_move_times = len(positions) * int(self.config["number_z_steps"])
|
||||
else:
|
||||
z_should_move_times = (
|
||||
len(selected_channels)
|
||||
* len(positions)
|
||||
* int(self.config["number_z_steps"])
|
||||
)
|
||||
|
||||
has_ni_galvo_stage = self.model.configuration["configuration"]["microscopes"][
|
||||
self.config["microscope_name"]
|
||||
]["stage"]["has_ni_galvo_stage"]
|
||||
prepared_next_channel = False
|
||||
|
||||
# prepare first channel in pre_signal_func
|
||||
idx = self.get_next_record(change_channel_func_str, idx)
|
||||
prepared_next_channel = True
|
||||
pre_change_channel_idx = idx
|
||||
assert (
|
||||
self.model.signal_records[idx][2]["__test_frame_id_completed"] == -1
|
||||
), "prepare first channel should happen before 0"
|
||||
assert (
|
||||
self.model.signal_records[idx][2]["__test_frame_id"] == 0
|
||||
), "prepare first channel should happen for frame: 0"
|
||||
|
||||
for i, pos in enumerate(positions):
|
||||
|
||||
idx = self.get_next_record("move_stage", idx)
|
||||
|
||||
# x, y, theta
|
||||
pos_moved = self.model.signal_records[idx][1][0]
|
||||
for i, axis in [(0, "x"), (1, "y"), (3, "theta")]:
|
||||
assert pos[i] == pos_moved[axis + "_abs"], (
|
||||
f"should move to {axis}: {pos[i]}, "
|
||||
f"but moved to {pos_moved[axis + '_abs']}"
|
||||
)
|
||||
|
||||
# (x, y, z, theta, f)
|
||||
z_pos = pos[2] + self.config["start_position"]
|
||||
f_pos = pos[4] + self.config["start_focus"]
|
||||
|
||||
if mode == "per_z":
|
||||
f_pos += selected_channels[0]["defocus"]
|
||||
for j in range(self.config["number_z_steps"]):
|
||||
idx = self.get_next_record("move_stage", idx)
|
||||
|
||||
pos_moved = self.model.signal_records[idx][1][0]
|
||||
# z, f
|
||||
assert pos_moved["z_abs"] == z_pos + j * z_step, (
|
||||
f"should move to z: {z_pos + j * z_step}, "
|
||||
f"but moved to {pos_moved['z_abs']}"
|
||||
)
|
||||
assert pos_moved["f_abs"] == f_pos + j * f_step, (
|
||||
f"should move to f: {f_pos + j * f_step}, "
|
||||
f"but moved to {pos_moved['f_abs']}"
|
||||
)
|
||||
z_moved_times += 1
|
||||
|
||||
# if the system has NIGalvo stage, should close the DAQ tasks and
|
||||
# then create new tasks to override the new waveforms
|
||||
if has_ni_galvo_stage and prepared_next_channel:
|
||||
idx = self.get_next_record(close_daq_tasks_str, idx)
|
||||
pre_change_channel_idx = idx
|
||||
assert (
|
||||
self.model.signal_records[idx][2]["__test_frame_id"]
|
||||
== frame_id
|
||||
), f"close DAQ tasks should happen before {frame_id}"
|
||||
|
||||
idx = self.get_next_record(create_daq_tasks_str, idx)
|
||||
pre_change_channel_idx = idx
|
||||
assert (
|
||||
self.model.signal_records[idx][2]["__test_frame_id"]
|
||||
== frame_id
|
||||
), f"create DAQ tasks should happen before {frame_id}"
|
||||
|
||||
# channel
|
||||
for k in range(len(selected_channels)):
|
||||
idx = self.get_next_record(change_channel_func_str, idx)
|
||||
prepared_next_channel = True
|
||||
assert (
|
||||
self.model.signal_records[idx][2]["__test_frame_id"]
|
||||
== frame_id
|
||||
), (
|
||||
"prepare next channel (change channel) "
|
||||
f"should happen after {frame_id}"
|
||||
)
|
||||
|
||||
assert (
|
||||
self.model.signal_records[idx][2][
|
||||
"__test_frame_id_completed"
|
||||
]
|
||||
== self.model.signal_records[idx][2]["__test_frame_id"]
|
||||
), (
|
||||
"prepare next channel (change channel) "
|
||||
"should happen inside signal_end_func()"
|
||||
)
|
||||
|
||||
assert (
|
||||
self.exist_record(
|
||||
change_channel_func_str,
|
||||
pre_change_channel_idx + 1,
|
||||
idx - 1,
|
||||
)
|
||||
is False
|
||||
), (
|
||||
"prepare next channel (change channel) "
|
||||
"should not happen more than once"
|
||||
)
|
||||
pre_change_channel_idx = idx
|
||||
frame_id += 1
|
||||
|
||||
else: # per_stack
|
||||
for k in range(len(selected_channels)):
|
||||
# z
|
||||
f_pos += selected_channels[k]["defocus"]
|
||||
for j in range(self.config["number_z_steps"]):
|
||||
idx = self.get_next_record("move_stage", idx)
|
||||
|
||||
pos_moved = self.model.signal_records[idx][1][0]
|
||||
# z, f
|
||||
assert pos_moved["z_abs"] == z_pos + j * z_step, (
|
||||
f"should move to z: {z_pos + j * z_step}, "
|
||||
f"but moved to {pos_moved['z_abs']}"
|
||||
)
|
||||
assert pos_moved["f_abs"] == f_pos + j * f_step, (
|
||||
f"should move to f: {f_pos + j * f_step}, "
|
||||
f"but moved to {pos_moved['f_abs']}"
|
||||
)
|
||||
z_moved_times += 1
|
||||
frame_id += 1
|
||||
f_pos -= selected_channels[k]["defocus"]
|
||||
idx = self.get_next_record(change_channel_func_str, idx)
|
||||
prepared_next_channel = True
|
||||
assert (
|
||||
self.model.signal_records[idx][2]["__test_frame_id"]
|
||||
== frame_id - 1
|
||||
), (
|
||||
"prepare next channel (change channel) "
|
||||
f"should happen at {frame_id - 1}"
|
||||
)
|
||||
|
||||
# restore z, f
|
||||
idx = self.get_next_record("move_stage", idx)
|
||||
pos_moved = self.model.signal_records[idx][1][0]
|
||||
assert (
|
||||
pos_moved["z_abs"] == restore_z
|
||||
), f"should restore z to {restore_z}, but moved to {pos_moved['z_abs']}"
|
||||
assert (
|
||||
pos_moved["f_abs"] == restore_f
|
||||
), f"should restore f to {restore_f}, but moved to {pos_moved['f_abs']}"
|
||||
|
||||
assert z_moved_times == z_should_move_times, (
|
||||
f"should verify all the stage movements! {z_moved_times} -- "
|
||||
f"{z_should_move_times}"
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("has_ni_galvo_stage", [False])
|
||||
def test_single_position_one_channel_per_z(self, has_ni_galvo_stage):
|
||||
# single position
|
||||
self.config["is_multiposition"] = False
|
||||
self.model.configuration["configuration"]["microscopes"][
|
||||
self.config["microscope_name"]
|
||||
]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage
|
||||
|
||||
# 1 channel per_z
|
||||
self.config["stack_cycling_mode"] = "per_z"
|
||||
self.config["channels"]["channel_1"]["is_selected"] = True
|
||||
self.config["channels"]["channel_2"]["is_selected"] = False
|
||||
self.config["channels"]["channel_3"]["is_selected"] = False
|
||||
self.model.start(self.feature_list)
|
||||
print(self.model.signal_records)
|
||||
self.z_stack_verification()
|
||||
|
||||
@pytest.mark.parametrize("has_ni_galvo_stage", [False])
|
||||
def test_single_position_one_channel_per_stack(self, has_ni_galvo_stage):
|
||||
# single position
|
||||
self.config["is_multiposition"] = False
|
||||
self.model.configuration["configuration"]["microscopes"][
|
||||
self.config["microscope_name"]
|
||||
]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage
|
||||
|
||||
# 1 channel per_stack
|
||||
self.config["stack_cycling_mode"] = "per_stack"
|
||||
self.config["channels"]["channel_1"]["is_selected"] = True
|
||||
self.config["channels"]["channel_2"]["is_selected"] = False
|
||||
self.config["channels"]["channel_3"]["is_selected"] = False
|
||||
self.model.start(self.feature_list)
|
||||
self.z_stack_verification()
|
||||
|
||||
@pytest.mark.parametrize("has_ni_galvo_stage", [False])
|
||||
def test_single_position_two_channels_per_z(self, has_ni_galvo_stage):
|
||||
# single position
|
||||
self.config["is_multiposition"] = False
|
||||
self.model.configuration["configuration"]["microscopes"][
|
||||
self.config["microscope_name"]
|
||||
]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage
|
||||
|
||||
# 2 channels per_z
|
||||
self.config["stack_cycling_mode"] = "per_z"
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
self.config["channels"]["channel_" + str(j + 1)]["is_selected"] = True
|
||||
self.config["channels"]["channel_" + str(i + 1)]["is_selected"] = False
|
||||
self.model.start(self.feature_list)
|
||||
self.z_stack_verification()
|
||||
|
||||
@pytest.mark.parametrize("has_ni_galvo_stage", [False])
|
||||
def test_single_position_two_channels_per_stack(self, has_ni_galvo_stage):
|
||||
# single position
|
||||
self.config["is_multiposition"] = False
|
||||
self.model.configuration["configuration"]["microscopes"][
|
||||
self.config["microscope_name"]
|
||||
]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage
|
||||
|
||||
# 2 channels per_stack
|
||||
self.config["stack_cycling_mode"] = "per_stack"
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
self.config["channels"]["channel_" + str(j + 1)]["is_selected"] = True
|
||||
self.config["channels"]["channel_" + str(i + 1)]["is_selected"] = False
|
||||
self.model.start(self.feature_list)
|
||||
self.z_stack_verification()
|
||||
|
||||
@pytest.mark.parametrize("has_ni_galvo_stage", [False])
|
||||
def test_single_position_three_channels_per_stack(self, has_ni_galvo_stage):
|
||||
# single position
|
||||
self.config["is_multiposition"] = False
|
||||
self.model.configuration["configuration"]["microscopes"][
|
||||
self.config["microscope_name"]
|
||||
]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage
|
||||
|
||||
# 3 channels per_stack
|
||||
self.config["channels"]["channel_1"]["is_selected"] = True
|
||||
self.config["channels"]["channel_2"]["is_selected"] = True
|
||||
self.config["channels"]["channel_3"]["is_selected"] = True
|
||||
self.config["stack_cycling_mode"] = "per_stack"
|
||||
self.model.start(self.feature_list)
|
||||
self.z_stack_verification()
|
||||
|
||||
@pytest.mark.parametrize("has_ni_galvo_stage", [False])
|
||||
def test_single_position_three_channels_per_z(self, has_ni_galvo_stage):
|
||||
# single position
|
||||
self.config["is_multiposition"] = False
|
||||
self.model.configuration["configuration"]["microscopes"][
|
||||
self.config["microscope_name"]
|
||||
]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage
|
||||
|
||||
# 3 channels per_z
|
||||
self.config["channels"]["channel_1"]["is_selected"] = True
|
||||
self.config["channels"]["channel_2"]["is_selected"] = True
|
||||
self.config["channels"]["channel_3"]["is_selected"] = True
|
||||
self.config["stack_cycling_mode"] = "per_z"
|
||||
self.model.start(self.feature_list)
|
||||
self.z_stack_verification()
|
||||
|
||||
@pytest.mark.parametrize("has_ni_galvo_stage", [False])
|
||||
def test_multi_position_one_channel_per_z(self, has_ni_galvo_stage):
|
||||
# multi position
|
||||
self.config["is_multiposition"] = True
|
||||
self.model.configuration["configuration"]["microscopes"][
|
||||
self.config["microscope_name"]
|
||||
]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage
|
||||
|
||||
# 1 channel per_z
|
||||
self.config["stack_cycling_mode"] = "per_z"
|
||||
self.config["channels"]["channel_1"]["is_selected"] = True
|
||||
self.config["channels"]["channel_2"]["is_selected"] = False
|
||||
self.config["channels"]["channel_3"]["is_selected"] = False
|
||||
self.model.start(self.feature_list)
|
||||
self.z_stack_verification()
|
||||
|
||||
self.config["is_multiposition"] = False
|
||||
|
||||
@pytest.mark.parametrize("has_ni_galvo_stage", [False])
|
||||
def test_multi_position_one_channel_per_stack(self, has_ni_galvo_stage):
|
||||
self.config["is_multiposition"] = True
|
||||
self.model.configuration["configuration"]["microscopes"][
|
||||
self.config["microscope_name"]
|
||||
]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage
|
||||
|
||||
# 1 channel per_stack
|
||||
self.config["stack_cycling_mode"] = "per_stack"
|
||||
self.config["channels"]["channel_1"]["is_selected"] = True
|
||||
self.config["channels"]["channel_2"]["is_selected"] = False
|
||||
self.config["channels"]["channel_3"]["is_selected"] = False
|
||||
self.model.start(self.feature_list)
|
||||
self.z_stack_verification()
|
||||
|
||||
self.config["is_multiposition"] = False
|
||||
|
||||
@pytest.mark.parametrize("has_ni_galvo_stage", [False])
|
||||
def test_multi_position_two_channels_per_z(self, has_ni_galvo_stage):
|
||||
self.config["is_multiposition"] = True
|
||||
self.model.configuration["configuration"]["microscopes"][
|
||||
self.config["microscope_name"]
|
||||
]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage
|
||||
|
||||
# 2 channels per_z
|
||||
self.config["stack_cycling_mode"] = "per_z"
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
self.config["channels"]["channel_" + str(j + 1)]["is_selected"] = True
|
||||
self.config["channels"]["channel_" + str(i + 1)]["is_selected"] = False
|
||||
self.model.start(self.feature_list)
|
||||
self.z_stack_verification()
|
||||
|
||||
self.config["is_multiposition"] = False
|
||||
|
||||
@pytest.mark.parametrize("has_ni_galvo_stage", [False])
|
||||
def test_multi_position_two_channels_per_stack(self, has_ni_galvo_stage):
|
||||
self.config["is_multiposition"] = True
|
||||
self.model.configuration["configuration"]["microscopes"][
|
||||
self.config["microscope_name"]
|
||||
]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage
|
||||
|
||||
# 2 channels per_stack
|
||||
self.config["stack_cycling_mode"] = "per_stack"
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
self.config["channels"]["channel_" + str(j + 1)]["is_selected"] = True
|
||||
self.config["channels"]["channel_" + str(i + 1)]["is_selected"] = False
|
||||
self.model.start(self.feature_list)
|
||||
self.z_stack_verification()
|
||||
|
||||
self.config["is_multiposition"] = False
|
||||
|
||||
@pytest.mark.parametrize("has_ni_galvo_stage", [False])
|
||||
def test_multi_position_three_channels_per_stack(self, has_ni_galvo_stage):
|
||||
self.config["is_multiposition"] = True
|
||||
self.model.configuration["configuration"]["microscopes"][
|
||||
self.config["microscope_name"]
|
||||
]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage
|
||||
|
||||
# 3 channels per_stack
|
||||
self.config["channels"]["channel_1"]["is_selected"] = True
|
||||
self.config["channels"]["channel_2"]["is_selected"] = True
|
||||
self.config["channels"]["channel_3"]["is_selected"] = True
|
||||
self.config["stack_cycling_mode"] = "per_stack"
|
||||
self.model.start(self.feature_list)
|
||||
self.z_stack_verification()
|
||||
|
||||
self.config["is_multiposition"] = False
|
||||
|
||||
@pytest.mark.parametrize("has_ni_galvo_stage", [False])
|
||||
def test_multi_position_three_channels_per_z(self, has_ni_galvo_stage):
|
||||
self.config["is_multiposition"] = True
|
||||
self.model.configuration["configuration"]["microscopes"][
|
||||
self.config["microscope_name"]
|
||||
]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage
|
||||
|
||||
# 3 channels per_z
|
||||
self.config["channels"]["channel_1"]["is_selected"] = True
|
||||
self.config["channels"]["channel_2"]["is_selected"] = True
|
||||
self.config["channels"]["channel_3"]["is_selected"] = True
|
||||
self.config["stack_cycling_mode"] = "per_z"
|
||||
self.model.start(self.feature_list)
|
||||
self.z_stack_verification()
|
||||
|
||||
self.config["is_multiposition"] = False
|
||||
133
test/model/features/test_feature_related_functions.py
Normal file
133
test/model/features/test_feature_related_functions.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.
|
||||
|
||||
# Standard library imports
|
||||
|
||||
# Third party imports
|
||||
import pytest
|
||||
|
||||
# local imports
|
||||
from navigate.model.features.feature_related_functions import (
|
||||
convert_str_to_feature_list,
|
||||
convert_feature_list_to_str,
|
||||
)
|
||||
from navigate.model.features.common_features import (
|
||||
PrepareNextChannel,
|
||||
LoopByCount,
|
||||
ZStackAcquisition,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"feature_list_str, expected_list",
|
||||
[
|
||||
("", None),
|
||||
("[]", []),
|
||||
("[{'name': PrepareNextChannel}]", [{"name": PrepareNextChannel}]),
|
||||
("[{'name': NonExistFeature}]", None),
|
||||
(
|
||||
"[({'name': PrepareNextChannel}, {'name': LoopByCount})]",
|
||||
[({"name": PrepareNextChannel}, {"name": LoopByCount})],
|
||||
),
|
||||
(
|
||||
"[({'name': PrepareNextChannel}, {'name': LoopByCount, 'args': (3,)})]",
|
||||
[({"name": PrepareNextChannel}, {"name": LoopByCount, "args": (3,)})],
|
||||
),
|
||||
(
|
||||
"[({'name': PrepareNextChannel}, {'name': LoopByCount, 'args': 3})]",
|
||||
[({"name": PrepareNextChannel}, {"name": LoopByCount, "args": (3,)})],
|
||||
),
|
||||
(
|
||||
"[({'name': PrepareNextChannel}, {'name': LoopByCount, 'args': (3)})]",
|
||||
[({"name": PrepareNextChannel}, {"name": LoopByCount, "args": (3,)})],
|
||||
),
|
||||
(
|
||||
"[(({'name': PrepareNextChannel}, {'name': LoopByCount, 'args': (3)}))]",
|
||||
[({"name": PrepareNextChannel}, {"name": LoopByCount, "args": (3,)})],
|
||||
),
|
||||
(
|
||||
"[{'name': ZStackAcquisition, 'args': (True, False, 'zstack',)}]",
|
||||
[
|
||||
{
|
||||
"name": ZStackAcquisition,
|
||||
"args": (
|
||||
True,
|
||||
False,
|
||||
"zstack",
|
||||
),
|
||||
}
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_convert_str_to_feature_list(feature_list_str, expected_list):
|
||||
feature_list = convert_str_to_feature_list(feature_list_str)
|
||||
|
||||
assert feature_list == expected_list
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"feature_list, expected_str",
|
||||
[
|
||||
(None, "[]"),
|
||||
([], "[]"),
|
||||
([{"name": PrepareNextChannel}], '[{"name": PrepareNextChannel,},]'),
|
||||
(
|
||||
[({"name": PrepareNextChannel}, {"name": LoopByCount})],
|
||||
'[({"name": PrepareNextChannel,},{"name": LoopByCount,},),]',
|
||||
),
|
||||
(
|
||||
[({"name": PrepareNextChannel}, {"name": LoopByCount, "args": (3,)})],
|
||||
'[({"name": PrepareNextChannel,},{"name": LoopByCount,"args": (3,),},),]',
|
||||
),
|
||||
(
|
||||
[
|
||||
{
|
||||
"name": ZStackAcquisition,
|
||||
"args": (
|
||||
True,
|
||||
False,
|
||||
"zstack",
|
||||
),
|
||||
}
|
||||
],
|
||||
'[{"name": ZStackAcquisition,"args": (True,False,"zstack",),},]',
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_convert_feature_list_to_str(feature_list, expected_str):
|
||||
feature_str = convert_feature_list_to_str(feature_list)
|
||||
|
||||
assert feature_str == expected_str
|
||||
|
||||
if feature_list:
|
||||
assert convert_str_to_feature_list(feature_str) == feature_list
|
||||
48
test/model/features/test_image_writer.py
Normal file
48
test/model/features/test_image_writer.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from navigate.tools.file_functions import delete_folder
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def image_writer(dummy_model):
|
||||
from navigate.model.features.image_writer import ImageWriter
|
||||
|
||||
model = dummy_model
|
||||
model.configuration["experiment"]["Saving"]["save_directory"] = "test_save_dir"
|
||||
|
||||
writer = ImageWriter(dummy_model)
|
||||
|
||||
yield writer
|
||||
|
||||
writer.close()
|
||||
|
||||
|
||||
def test_image_write_fail(image_writer):
|
||||
image_writer.save_image([-1, image_writer.model.data_buffer.shape[0]])
|
||||
|
||||
# make sure the directory is empty
|
||||
ls = os.listdir("test_save_dir")
|
||||
ls.remove("MIP")
|
||||
assert not ls
|
||||
|
||||
delete_folder("test_save_dir")
|
||||
|
||||
|
||||
def test_image_write(image_writer):
|
||||
from numpy.random import rand
|
||||
|
||||
# Randomize the data buffer
|
||||
for i in range(image_writer.model.data_buffer.shape[0]):
|
||||
image_writer.model.data_buffer[i, ...] = rand(
|
||||
image_writer.model.img_width, image_writer.model.img_height
|
||||
)
|
||||
|
||||
image_writer.save_image(list(range(image_writer.model.number_of_frames)))
|
||||
|
||||
# make sure the directory isn't empty
|
||||
ls = os.listdir("test_save_dir")
|
||||
ls.remove("MIP")
|
||||
assert ls
|
||||
|
||||
delete_folder("test_save_dir")
|
||||
222
test/model/features/test_restful_features.py
Normal file
222
test/model/features/test_restful_features.py
Normal file
@@ -0,0 +1,222 @@
|
||||
# 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 Import
|
||||
import base64
|
||||
import unittest
|
||||
import json
|
||||
import logging
|
||||
from unittest.mock import patch, Mock, MagicMock
|
||||
from io import BytesIO
|
||||
|
||||
# Third Party Imports
|
||||
import numpy as np
|
||||
|
||||
# Local Imports
|
||||
from navigate.model.features.restful_features import (
|
||||
prepare_service,
|
||||
IlastikSegmentation,
|
||||
)
|
||||
|
||||
|
||||
class TestPrepareService(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.service_url = "http://example.com/ilastik"
|
||||
self.project_file = "path/to/project.ilp"
|
||||
self.expected_url = f"{self.service_url}/load?project={self.project_file}"
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
self.logger = logging.getLogger(
|
||||
"mymodule"
|
||||
) # Replace with the actual logger name used
|
||||
|
||||
@patch("navigate.model.features.restful_features.requests.get")
|
||||
def test_prepare_service_success(self, mock_get):
|
||||
expected_response = {"status": "success", "data": "segmentation data"}
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = json.dumps(expected_response)
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
response = prepare_service(self.service_url, project_file=self.project_file)
|
||||
|
||||
self.assertEqual(response, expected_response)
|
||||
mock_get.assert_called_once_with(self.expected_url)
|
||||
|
||||
@patch("navigate.model.features.restful_features.requests.get")
|
||||
def test_prepare_service_failure(self, mock_get):
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 404
|
||||
mock_response.content = "Error"
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
response = prepare_service(self.service_url, project_file=self.project_file)
|
||||
|
||||
self.assertIsNone(response)
|
||||
mock_get.assert_called_once_with(self.expected_url)
|
||||
|
||||
def test_prepare_service_non_ilastik_url(self):
|
||||
non_ilastik_url = "http://example.com/not_ilastik"
|
||||
response = prepare_service(non_ilastik_url, project_file=self.project_file)
|
||||
|
||||
self.assertIsNone(response)
|
||||
|
||||
|
||||
class TestIlastikSegmentation(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Set up a mock model object
|
||||
shape = (2048, 2048)
|
||||
self.mock_model = Mock()
|
||||
self.mock_model.configuration = {
|
||||
"rest_api_config": {"Ilastik": {"url": "http://example.com/ilastik"}},
|
||||
"experiment": {
|
||||
"MicroscopeState": {"microscope_name": "Nanoscale", "zoom": "1.0"},
|
||||
"CameraParameters": {
|
||||
"Nanoscale": {"x_pixels": "2048", "y_pixels": "2048"},
|
||||
},
|
||||
"StageParameters": {
|
||||
"x": "100",
|
||||
"y": "100",
|
||||
"z": "50",
|
||||
"theta": "0",
|
||||
"f": "1.0",
|
||||
},
|
||||
},
|
||||
"configuration": {
|
||||
"microscopes": {
|
||||
"Nanoscale": {"zoom": {"pixel_size": {"N/A": "1.0", "1.0": "1.0"}}}
|
||||
}
|
||||
},
|
||||
}
|
||||
self.mock_model.data_buffer = {
|
||||
0: np.random.randint(0, 65536, size=shape, dtype=np.uint16),
|
||||
1: np.random.randint(0, 65536, size=shape, dtype=np.uint16),
|
||||
}
|
||||
|
||||
self.mock_model.img_height = shape[0]
|
||||
self.mock_model.img_width = shape[1]
|
||||
self.mock_model.display_ilastik_segmentation = True
|
||||
self.mock_model.mark_ilastik_position = False
|
||||
self.mock_model.event_queue = MagicMock()
|
||||
self.mock_model.active_microscope_name = "Nanoscale"
|
||||
|
||||
self.ilastik_segmentation = IlastikSegmentation(self.mock_model)
|
||||
|
||||
@patch("requests.post")
|
||||
def test_data_func_success(self, mock_post):
|
||||
frame_ids = [0, 1]
|
||||
expected_json_data = {
|
||||
"dtype": "uint16",
|
||||
"shape": (self.mock_model.img_height, self.mock_model.img_width),
|
||||
"image": [
|
||||
base64.b64encode(self.mock_model.data_buffer[0]).decode("utf-8"),
|
||||
base64.b64encode(self.mock_model.data_buffer[1]).decode("utf-8"),
|
||||
],
|
||||
}
|
||||
|
||||
# Create a valid numpy array to simulate the response
|
||||
array_data = np.array([np.zeros((2048, 2048, 1), dtype=np.uint16)])
|
||||
buffer = BytesIO()
|
||||
np.savez(buffer, *array_data)
|
||||
buffer.seek(0)
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.raw.read.return_value = buffer.read()
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
self.ilastik_segmentation.data_func(frame_ids)
|
||||
|
||||
mock_post.assert_called_once_with(
|
||||
"http://example.com/ilastik/segmentation",
|
||||
json=expected_json_data,
|
||||
stream=True,
|
||||
)
|
||||
self.mock_model.event_queue.put.assert_called()
|
||||
|
||||
# Test with self.model.mark_ilastik_position set to True
|
||||
self.mock_model.mark_ilastik_position = True
|
||||
self.mock_model.event_queue.reset_mock()
|
||||
|
||||
self.mock_model.ilastik_target_labels = range(1)
|
||||
self.ilastik_segmentation.update_setting()
|
||||
self.ilastik_segmentation.data_func(frame_ids)
|
||||
assert self.mock_model.event_queue.put.call_count == 2
|
||||
# self.mock_model.event_queue.put.assert_called_with(("multiposition"))
|
||||
called_args, _ = self.mock_model.event_queue.put.call_args
|
||||
assert "multiposition" in called_args[0]
|
||||
|
||||
@patch("requests.post")
|
||||
def test_data_func_failure(self, mock_post):
|
||||
frame_ids = [0, 1]
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 404
|
||||
mock_response.content = "Error"
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
with patch("builtins.print") as mocked_print:
|
||||
self.ilastik_segmentation.data_func(frame_ids)
|
||||
mocked_print.assert_called_once_with("There is something wrong!")
|
||||
|
||||
def test_update_setting(self):
|
||||
self.ilastik_segmentation.update_setting()
|
||||
|
||||
self.assertEqual(self.ilastik_segmentation.resolution, "Nanoscale")
|
||||
self.assertEqual(self.ilastik_segmentation.zoom, "1.0")
|
||||
self.assertEqual(self.ilastik_segmentation.pieces_num, 1)
|
||||
self.assertEqual(self.ilastik_segmentation.pieces_size, 2048)
|
||||
self.assertEqual(self.ilastik_segmentation.posistion_step_size, 2048)
|
||||
self.assertEqual(self.ilastik_segmentation.x_start, -924)
|
||||
self.assertEqual(self.ilastik_segmentation.y_start, -924)
|
||||
|
||||
def test_init_func_update_settings(self):
|
||||
with patch.object(
|
||||
self.ilastik_segmentation, "update_setting"
|
||||
) as mock_update_setting:
|
||||
self.ilastik_segmentation.resolution = "DifferentResolution"
|
||||
self.ilastik_segmentation.zoom = "DifferentZoom"
|
||||
self.ilastik_segmentation.init_func()
|
||||
mock_update_setting.assert_called_once()
|
||||
|
||||
def test_mark_position(self):
|
||||
mask = np.zeros((2048, 2048, 1), dtype=np.uint16)
|
||||
mask[0:1024, 0:1024, 0] = 1 # Mock some segmentation data
|
||||
self.mock_model.ilastik_target_labels = [1]
|
||||
|
||||
self.ilastik_segmentation.update_setting()
|
||||
self.ilastik_segmentation.mark_position(mask)
|
||||
|
||||
self.mock_model.event_queue.put.assert_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
239
test/model/features/test_volume_search.py
Normal file
239
test/model/features/test_volume_search.py
Normal file
@@ -0,0 +1,239 @@
|
||||
# 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 queue
|
||||
|
||||
import pytest
|
||||
import numpy as np
|
||||
|
||||
from navigate.model.features.volume_search import VolumeSearch
|
||||
|
||||
|
||||
class TestVolumeSearch:
|
||||
@pytest.fixture(
|
||||
autouse=True,
|
||||
params=[[False, False], [True, False], [False, True], [True, True]],
|
||||
)
|
||||
def _prepare_test(self, request, dummy_model_to_test_features):
|
||||
flipx, flipy = request.param
|
||||
self.model = dummy_model_to_test_features
|
||||
self.config = self.model.configuration["experiment"]["MicroscopeState"]
|
||||
self.record_num = 0
|
||||
self.overlap = 0.0 # TODO: Make this consistently work for
|
||||
# overlap > 0.0 (add fudge factor)
|
||||
self.feature_list = [
|
||||
[
|
||||
{
|
||||
"name": VolumeSearch,
|
||||
"args": ("Nanoscale", "N/A", flipx, flipy, self.overlap),
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
self.sinx = 1 if flipx else -1
|
||||
self.siny = 1 if flipy else -1
|
||||
|
||||
self.model.active_microscope_name = self.config["microscope_name"]
|
||||
curr_zoom = self.model.configuration["experiment"]["MicroscopeState"]["zoom"]
|
||||
self.curr_pixel_size = float(
|
||||
self.model.configuration["configuration"]["microscopes"][
|
||||
self.model.active_microscope_name
|
||||
]["zoom"]["pixel_size"][curr_zoom]
|
||||
)
|
||||
self.target_pixel_size = float(
|
||||
self.model.configuration["configuration"]["microscopes"]["Nanoscale"][
|
||||
"zoom"
|
||||
]["pixel_size"]["N/A"]
|
||||
)
|
||||
|
||||
self.N = 128
|
||||
# The target image size in pixels
|
||||
self.mag_ratio = int(self.curr_pixel_size / self.target_pixel_size)
|
||||
self.target_grid_pixels = int(self.N // self.mag_ratio)
|
||||
# The target image size in microns
|
||||
self.target_grid_width = self.N * self.target_pixel_size * (1 - self.overlap)
|
||||
|
||||
self.model.event_queue = queue.Queue(10)
|
||||
|
||||
self.model.configuration["experiment"]["StageParameters"]["z"] = 100
|
||||
self.model.configuration["experiment"]["StageParameters"]["f"] = 100
|
||||
|
||||
self.config["start_position"] = np.random.randint(-200, 0)
|
||||
self.config["end_position"] = self.config["start_position"] + 200
|
||||
self.config["number_z_steps"] = np.random.randint(5, 10)
|
||||
self.config["step_size"] = (
|
||||
self.config["end_position"] - self.config["start_position"]
|
||||
) / self.config["number_z_steps"]
|
||||
self.config["start_focus"] = -10
|
||||
self.config["end_focus"] = 10
|
||||
|
||||
self.focus_step = (
|
||||
self.config["end_focus"] - self.config["start_focus"]
|
||||
) / self.config["number_z_steps"]
|
||||
|
||||
def get_next_record(self, record_prefix, idx):
|
||||
idx += 1
|
||||
while self.model.signal_records[idx][0] != record_prefix:
|
||||
idx += 1
|
||||
if idx >= self.record_num:
|
||||
break
|
||||
return idx
|
||||
|
||||
def verify_volume_search(self):
|
||||
self.record_num = len(self.model.signal_records)
|
||||
|
||||
idx = -1
|
||||
|
||||
z_pos = (
|
||||
self.model.configuration["experiment"]["StageParameters"]["z"]
|
||||
+ self.config["number_z_steps"] // 2 * self.config["step_size"]
|
||||
)
|
||||
f_pos = (
|
||||
self.model.configuration["experiment"]["StageParameters"]["f"]
|
||||
+ self.config["number_z_steps"] // 2 * self.focus_step
|
||||
)
|
||||
|
||||
for j in range(self.config["number_z_steps"]):
|
||||
idx = self.get_next_record("move_stage", idx)
|
||||
if idx >= self.record_num:
|
||||
# volume search ended early
|
||||
break
|
||||
pos_moved = self.model.signal_records[idx][1][0]
|
||||
|
||||
fact = (
|
||||
j
|
||||
if j < (self.config["number_z_steps"] + 1) // 2
|
||||
else (self.config["number_z_steps"] + 1) // 2 - j - 1
|
||||
)
|
||||
|
||||
# z, f
|
||||
assert (
|
||||
pos_moved["z_abs"] - (z_pos + fact * self.config["step_size"])
|
||||
) < 1e-6, (
|
||||
f"should move to z: {z_pos + fact * self.config['step_size']}, "
|
||||
f"but moved to {pos_moved['z_abs']}"
|
||||
)
|
||||
assert (pos_moved["f_abs"] - (f_pos + fact * self.focus_step)) < 1e-6, (
|
||||
f"should move to f: {f_pos + fact * self.focus_step}, "
|
||||
f"but moved to {pos_moved['f_abs']}"
|
||||
)
|
||||
|
||||
for _ in range(100):
|
||||
event, value = self.model.event_queue.get()
|
||||
if event == "multiposition":
|
||||
break
|
||||
|
||||
positions = np.vstack(value) # Columns: X, Y, Z, Theta, F
|
||||
|
||||
# Check the bounding box. TODO: Make exact.
|
||||
min_x = (
|
||||
self.model.configuration["experiment"]["StageParameters"]["x"]
|
||||
- self.sinx * self.lxy
|
||||
)
|
||||
max_x = self.model.configuration["experiment"]["StageParameters"][
|
||||
"x"
|
||||
] + self.sinx * (self.lxy + self.N // 2 * self.curr_pixel_size) * (
|
||||
1 - self.overlap
|
||||
)
|
||||
min_y = (
|
||||
self.model.configuration["experiment"]["StageParameters"]["y"]
|
||||
- self.siny * self.lxy
|
||||
)
|
||||
max_y = self.model.configuration["experiment"]["StageParameters"][
|
||||
"y"
|
||||
] + self.siny * (self.lxy + self.N // 2 * self.curr_pixel_size) * (
|
||||
1 - self.overlap
|
||||
)
|
||||
|
||||
if self.sinx == -1:
|
||||
tmp = max_x
|
||||
max_x = min_x
|
||||
min_x = tmp
|
||||
|
||||
if self.siny == -1:
|
||||
tmp = max_y
|
||||
max_y = min_y
|
||||
min_y = tmp
|
||||
|
||||
min_z = self.model.configuration["experiment"]["StageParameters"]["z"] - self.lz
|
||||
max_z = self.model.configuration["experiment"]["StageParameters"]["z"] + (
|
||||
self.lz + self.N // 2 * self.curr_pixel_size
|
||||
) * (1 - self.overlap)
|
||||
|
||||
assert np.min(positions[:, 0]) >= min_x
|
||||
assert np.max(positions[:, 0]) <= max_x
|
||||
assert np.min(positions[:, 1]) >= min_y
|
||||
assert np.max(positions[:, 1]) <= max_y
|
||||
assert np.min(positions[:, 2]) >= min_z
|
||||
assert np.max(positions[:, 2]) <= max_z
|
||||
|
||||
def test_box_volume_search(self):
|
||||
from navigate.tools.sdf import volume_from_sdf, box
|
||||
|
||||
M = int(self.config["number_z_steps"])
|
||||
microscope_name = self.model.configuration["experiment"]["MicroscopeState"][
|
||||
"microscope_name"
|
||||
]
|
||||
self.model.configuration["experiment"]["CameraParameters"][microscope_name][
|
||||
"x_pixels"
|
||||
] = self.N
|
||||
self.lxy = (
|
||||
np.random.randint(int(0.1 * self.N), int(0.4 * self.N))
|
||||
* self.curr_pixel_size
|
||||
)
|
||||
self.lz = (
|
||||
np.random.randint(int(0.1 * self.N), int(0.4 * self.N))
|
||||
* self.curr_pixel_size
|
||||
)
|
||||
|
||||
vol = volume_from_sdf(
|
||||
lambda p: box(
|
||||
p,
|
||||
(
|
||||
self.lxy,
|
||||
self.lxy,
|
||||
self.lz,
|
||||
),
|
||||
),
|
||||
self.N,
|
||||
pixel_size=self.curr_pixel_size,
|
||||
subsample_z=self.N // M,
|
||||
)
|
||||
vol = (vol <= 0) * 100
|
||||
vol = vol[np.r_[(M // 2) : M, 0 : (M // 2)]]
|
||||
self.model.data_buffer = vol
|
||||
|
||||
def get_offset_variance_maps():
|
||||
return np.zeros((self.N, self.N)), np.ones((self.N, self.N))
|
||||
|
||||
self.model.get_offset_variance_maps = get_offset_variance_maps
|
||||
|
||||
self.model.start(self.feature_list)
|
||||
self.verify_volume_search()
|
||||
185
test/model/metadata_sources/test_bdv_metadata.py
Normal file
185
test/model/metadata_sources/test_bdv_metadata.py
Normal file
@@ -0,0 +1,185 @@
|
||||
import os
|
||||
import numpy as np
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ext", ["h5", "n5", "tiff"])
|
||||
def test_bdv_metadata(ext):
|
||||
from navigate.model.metadata_sources.bdv_metadata import BigDataViewerMetadata
|
||||
|
||||
md = BigDataViewerMetadata()
|
||||
|
||||
views = []
|
||||
for _ in range(10):
|
||||
views.append(
|
||||
{
|
||||
"x": np.random.randint(-1000, 1000),
|
||||
"y": np.random.randint(-1000, 1000),
|
||||
"z": np.random.randint(-1000, 1000),
|
||||
"theta": np.random.randint(-1000, 1000),
|
||||
"f": np.random.randint(-1000, 1000),
|
||||
}
|
||||
)
|
||||
|
||||
for view in views:
|
||||
arr = md.stage_positions_to_affine_matrix(**view)
|
||||
assert arr[0, 3] == view["y"] / md.dy
|
||||
assert arr[1, 3] == view["x"] / md.dx
|
||||
assert arr[2, 3] == view["z"] / md.dz
|
||||
|
||||
md.write_xml(f"test_bdv.{ext}", views)
|
||||
|
||||
os.remove("test_bdv.xml")
|
||||
|
||||
# Test defaults for shear transform.
|
||||
assert md.rotate_data is False
|
||||
assert md.shear_data is False
|
||||
assert np.shape(md.rotate_transform) == (3, 4)
|
||||
assert np.shape(md.shear_transform) == (3, 4)
|
||||
|
||||
# Confirm that the shear/rotation transforms are identity matrices by default
|
||||
assert np.all(md.shear_transform == np.eye(3, 4))
|
||||
assert np.all(md.rotate_transform == np.eye(3, 4))
|
||||
|
||||
# Confirm that the shear/rotation transforms are identity matrices by default
|
||||
# even after calling calculate_shear_transform and calculate_rotate_transform
|
||||
md.bdv_shear_transform()
|
||||
md.bdv_rotate_transform()
|
||||
assert np.all(md.shear_transform == np.eye(3, 4))
|
||||
assert np.all(md.rotate_transform == np.eye(3, 4))
|
||||
|
||||
# Test that the shear/rotation transforms are correctly calculated.
|
||||
md.shear_data = True
|
||||
md.shear_dimension = "XZ"
|
||||
md.shear_angle = 15
|
||||
md.dx, md.dy, md.dz = 1, 1, 1
|
||||
md.bdv_shear_transform()
|
||||
assert md.shear_transform[0, 2] == np.tan(np.deg2rad(15))
|
||||
|
||||
md.rotate_data = True
|
||||
md.rotate_angle_x = 15
|
||||
md.rotate_angle_y = 0
|
||||
md.rotate_angle_z = 0
|
||||
md.bdv_rotate_transform()
|
||||
assert md.rotate_transform[1, 1] == np.cos(np.deg2rad(15))
|
||||
assert md.rotate_transform[1, 2] == -np.sin(np.deg2rad(15))
|
||||
assert md.rotate_transform[2, 1] == np.sin(np.deg2rad(15))
|
||||
assert md.rotate_transform[2, 2] == np.cos(np.deg2rad(15))
|
||||
|
||||
# Make sure we can still write the data.
|
||||
md.write_xml(f"test_bdv.{ext}", views)
|
||||
os.remove("test_bdv.xml")
|
||||
|
||||
@pytest.mark.parametrize("stack_cycling_mode", ["per_stack", "per_z"])
|
||||
def test_bdv_xml_dict(dummy_model, stack_cycling_mode):
|
||||
from navigate.model.metadata_sources.bdv_metadata import BigDataViewerMetadata
|
||||
|
||||
md = BigDataViewerMetadata()
|
||||
|
||||
md.configuration = dummy_model.configuration.copy()
|
||||
|
||||
# set shape from configuration and experiment
|
||||
md.configuration["experiment"]["MicroscopeState"]["image_mode"] = "z-stack"
|
||||
|
||||
# timepoints, channels, z-slices, positions
|
||||
for tp in [1]: #, 2, 3, 5]:
|
||||
for pos in [1, 3, 5]:
|
||||
for ch in [1, 2, 3]:
|
||||
for z in [1, 5, 10, 20]:
|
||||
md.configuration["experiment"]["MicroscopeState"]["timepoints"] = tp
|
||||
# channel settings
|
||||
channel_dict = md.configuration["experiment"]["MicroscopeState"]["channels"]
|
||||
for i in range(1, ch+1):
|
||||
channel_name = f"channel_{i}"
|
||||
if channel_name not in channel_dict:
|
||||
channel_dict[channel_name] = {
|
||||
"is_selected": True,
|
||||
"laser": "488nm",
|
||||
"laser_index": 0,
|
||||
"camera_exposure_time": 200.0,
|
||||
"laser_power": 20.0,
|
||||
"interval_time": 1.0,
|
||||
"defocus": 104.0,
|
||||
"filter_wheel_0": "Empty-Alignment",
|
||||
"filter_position_0": 6,
|
||||
"filter_wheel_1": "Empty-Alignment",
|
||||
"filter_position_1": 6
|
||||
}
|
||||
else:
|
||||
channel_dict[channel_name]["is_selected"] = True
|
||||
for i in range(ch+1, 5):
|
||||
channel_name = f"channel_{i}"
|
||||
if channel_name in channel_dict:
|
||||
channel_dict[channel_name]["is_selected"] = False
|
||||
# z-stack settings
|
||||
start_z_position = md.configuration["experiment"]["MicroscopeState"]["start_position"]
|
||||
md.configuration["experiment"]["MicroscopeState"]["number_z_steps"] = z
|
||||
md.configuration["experiment"]["MicroscopeState"]["step_size"] = 0.2
|
||||
md.configuration["experiment"]["MicroscopeState"]["end_position"] = z * 0.2 + start_z_position
|
||||
# multiposition settings
|
||||
md.configuration["experiment"]["MicroscopeState"]["is_multiposition"] = (
|
||||
True if pos > 1 else False
|
||||
)
|
||||
multipositions = [
|
||||
["X", "Y", "Z", "THETA", "F"]
|
||||
]
|
||||
for p in range(pos):
|
||||
position = [
|
||||
np.random.uniform(0, 100), # X
|
||||
np.random.uniform(0, 100), # Y
|
||||
np.random.uniform(0, 100), # Z
|
||||
np.random.uniform(0, 360), # THETA
|
||||
np.random.uniform(0, 10), # F
|
||||
]
|
||||
multipositions.append(position)
|
||||
|
||||
md.configuration["multi_positions"] = multipositions
|
||||
|
||||
md.set_from_configuration_experiment()
|
||||
|
||||
assert md.shape_t == tp
|
||||
assert md.shape_c == ch
|
||||
assert md.shape_z == z
|
||||
assert md.positions == pos
|
||||
|
||||
if pos > 1:
|
||||
assert md._multiposition is True
|
||||
else:
|
||||
assert md._multiposition is False
|
||||
|
||||
# view
|
||||
views = []
|
||||
for p in range(1, pos):
|
||||
for c in range(ch):
|
||||
position = md.configuration["multi_positions"][p + 1]
|
||||
for _z in range(z):
|
||||
views.append(
|
||||
{
|
||||
"x": position[0],
|
||||
"y": position[1],
|
||||
"z": position[2] + start_z_position + _z * 0.2,
|
||||
"theta": position[3],
|
||||
"f": position[4],
|
||||
}
|
||||
)
|
||||
|
||||
xml_dict = md.bdv_xml_dict("test_file.h5", views)
|
||||
|
||||
assert "ImageLoader" in xml_dict["SequenceDescription"]
|
||||
assert "ViewSetups" in xml_dict["SequenceDescription"]
|
||||
assert "Timepoints" in xml_dict["SequenceDescription"]
|
||||
assert "ViewRegistrations" in xml_dict
|
||||
# verify affine values are the same for a position
|
||||
view_registrations = xml_dict["ViewRegistrations"]["ViewRegistration"]
|
||||
for i in range(len(view_registrations)):
|
||||
# assert view id
|
||||
view_id = view_registrations[i]["setup"]
|
||||
if ch > 1:
|
||||
assert view_id == ((i % ch) * pos + (i // ch))
|
||||
# assert affine position consistency between channels
|
||||
if i % ch == 0:
|
||||
affine = view_registrations[i]["ViewTransform"][0]["affine"]["text"]
|
||||
else:
|
||||
affine_c = view_registrations[i]["ViewTransform"][0]["affine"]["text"]
|
||||
assert affine == affine_c
|
||||
154
test/model/metadata_sources/test_metadata.py
Normal file
154
test/model/metadata_sources/test_metadata.py
Normal file
@@ -0,0 +1,154 @@
|
||||
import pytest
|
||||
import random
|
||||
|
||||
|
||||
def test_metadata_voxel_size(dummy_model):
|
||||
from navigate.model.metadata_sources.metadata import Metadata
|
||||
|
||||
md = Metadata()
|
||||
|
||||
md.configuration = dummy_model.configuration
|
||||
|
||||
zoom = dummy_model.configuration["experiment"]["MicroscopeState"]["zoom"]
|
||||
active_microscope = dummy_model.configuration["experiment"]["MicroscopeState"][
|
||||
"microscope_name"
|
||||
]
|
||||
pixel_size = float(
|
||||
dummy_model.configuration["configuration"]["microscopes"][active_microscope][
|
||||
"zoom"
|
||||
]["pixel_size"][zoom]
|
||||
)
|
||||
|
||||
dx, dy, dz = md.voxel_size
|
||||
|
||||
assert (
|
||||
(dx == pixel_size)
|
||||
and (dy == pixel_size)
|
||||
and (
|
||||
dz
|
||||
== float(
|
||||
dummy_model.configuration["experiment"]["MicroscopeState"]["step_size"]
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_metadata_shape(dummy_model):
|
||||
from navigate.model.metadata_sources.metadata import Metadata
|
||||
|
||||
dummy_model.configuration["experiment"]["MicroscopeState"]["image_mode"] = "z-stack"
|
||||
|
||||
md = Metadata()
|
||||
|
||||
md.configuration = dummy_model.configuration
|
||||
|
||||
microscope_name = dummy_model.configuration["experiment"]["MicroscopeState"][
|
||||
"microscope_name"
|
||||
]
|
||||
txs = dummy_model.configuration["experiment"]["CameraParameters"][microscope_name][
|
||||
"img_x_pixels"
|
||||
]
|
||||
tys = dummy_model.configuration["experiment"]["CameraParameters"][microscope_name][
|
||||
"img_y_pixels"
|
||||
]
|
||||
tzs = dummy_model.configuration["experiment"]["MicroscopeState"]["number_z_steps"]
|
||||
tts = dummy_model.configuration["experiment"]["MicroscopeState"]["timepoints"]
|
||||
tcs = sum(
|
||||
[
|
||||
v["is_selected"] is True
|
||||
for k, v in dummy_model.configuration["experiment"]["MicroscopeState"][
|
||||
"channels"
|
||||
].items()
|
||||
]
|
||||
)
|
||||
|
||||
xs, ys, cs, zs, ts = md.shape
|
||||
|
||||
assert (xs == txs) and (ys == tys) and (zs == tzs) and (ts == tts) and (cs == tcs)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"image_mode",
|
||||
[
|
||||
"single",
|
||||
"Confocal Projection",
|
||||
"z-stack",
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("stack_cycling_mode", ["per_stack", "per_z"])
|
||||
@pytest.mark.parametrize("conpro_cycling_mode", ["per_stack", "per_plane"])
|
||||
def test_metadata_set_stack_order_from_configuration_experiment(
|
||||
dummy_model, image_mode, stack_cycling_mode, conpro_cycling_mode
|
||||
):
|
||||
from navigate.model.metadata_sources.metadata import Metadata
|
||||
|
||||
dummy_model.configuration["experiment"]["MicroscopeState"][
|
||||
"image_mode"
|
||||
] = image_mode
|
||||
dummy_model.configuration["experiment"]["MicroscopeState"][
|
||||
"stack_cycling_mode"
|
||||
] = stack_cycling_mode
|
||||
dummy_model.configuration["experiment"]["MicroscopeState"][
|
||||
"conpro_cycling_mode"
|
||||
] = conpro_cycling_mode
|
||||
|
||||
md = Metadata()
|
||||
|
||||
md.configuration = dummy_model.configuration
|
||||
|
||||
if image_mode == "z-stack" and stack_cycling_mode == "per_stack":
|
||||
assert md._per_stack is True
|
||||
elif image_mode == "Confocal Projection" and stack_cycling_mode == "per_stack":
|
||||
assert md._per_stack is True
|
||||
else:
|
||||
assert md._per_stack is False
|
||||
|
||||
def set_shape_from_configuration_experiment(dummy_model):
|
||||
from navigate.model.metadata_sources.metadata import Metadata
|
||||
|
||||
md = Metadata()
|
||||
|
||||
md.configuration = dummy_model.configuration.copy()
|
||||
|
||||
|
||||
# set up experiment with multiposition
|
||||
# no position
|
||||
md.configuration["experiment"]["MicroscopeState"]["image_mode"] = "z-stack"
|
||||
md.configuration["multi_positions"] = [
|
||||
["X", "Y", "Z", "THETA", "F"]
|
||||
]
|
||||
md.configuration["expriment"]["MicroscopeState"]["is_multiposition"] = False
|
||||
|
||||
md._set_shape_from_configuration_experiment()
|
||||
|
||||
assert md._multiposition is False
|
||||
assert md.positions == 1
|
||||
|
||||
# customized mode
|
||||
md.configuration["experiment"]["MicroscopeState"]["image_mode"] = "customized"
|
||||
assert md._multiposition is True
|
||||
assert md.positions == 1
|
||||
|
||||
# random multiposition
|
||||
md.configuration["experiment"]["MicroscopeState"]["image_mode"] = "z-stack"
|
||||
for i in range(5):
|
||||
num_positions = random.randint(2, 10)
|
||||
md.configuration["multi_positions"] = [["X", "Y", "Z", "THETA", "F"]]
|
||||
for p in range(num_positions):
|
||||
pos = [
|
||||
random.uniform(0, 100), # X
|
||||
random.uniform(0, 100), # Y
|
||||
random.uniform(0, 100), # Z
|
||||
random.uniform(0, 360), # THETA
|
||||
random.uniform(0, 10), # F
|
||||
]
|
||||
md.configuration["multi_positions"].append(pos)
|
||||
|
||||
md.configuration["experiment"]["MicroscopeState"]["is_multiposition"] = True
|
||||
|
||||
md._set_shape_from_configuration_experiment()
|
||||
|
||||
assert md._multiposition is True
|
||||
assert md.positions == num_positions
|
||||
|
||||
|
||||
46
test/model/metadata_sources/test_ome_tiff_metadata.py
Normal file
46
test/model/metadata_sources/test_ome_tiff_metadata.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import urllib.request
|
||||
import os
|
||||
import platform
|
||||
|
||||
from navigate.tools.file_functions import delete_folder
|
||||
|
||||
|
||||
def test_ome_metadata_valid(dummy_model):
|
||||
from navigate.model.metadata_sources.ome_tiff_metadata import OMETIFFMetadata
|
||||
|
||||
# First, download OME-XML validation tools
|
||||
# new_path = https://downloads.openmicroscopy.org/bio-formats/8.1.0/artifacts/bftools.zip
|
||||
# old_path = https://downloads.openmicroscopy.org/bio-formats/6.0.1/artifacts/bftools.zip
|
||||
|
||||
urllib.request.urlretrieve(
|
||||
"https://downloads.openmicroscopy.org/bio-formats/8.1.0/artifacts/bftools.zip",
|
||||
"bftools.zip",
|
||||
)
|
||||
|
||||
# Unzip
|
||||
_ = os.popen("tar -xzvf bftools.zip").read()
|
||||
|
||||
# Create metadata
|
||||
md = OMETIFFMetadata()
|
||||
|
||||
md.configuration = dummy_model.configuration
|
||||
|
||||
# Write metadata to file
|
||||
md.write_xml("test.xml")
|
||||
|
||||
# Validate the XML
|
||||
if platform.system() == "Windows":
|
||||
output = os.popen("bftools\\xmlvalid.bat test.xml").read()
|
||||
else:
|
||||
output = os.popen("./bftools/xmlvalid test.xml").read()
|
||||
|
||||
print(output)
|
||||
|
||||
# Delete bftools
|
||||
delete_folder("./bftools")
|
||||
os.remove("bftools.zip")
|
||||
|
||||
# Delete XML
|
||||
os.remove("test.xml")
|
||||
|
||||
assert "No validation errors found." in output
|
||||
189
test/model/metadata_sources/test_zarr_metadata.py
Normal file
189
test/model/metadata_sources/test_zarr_metadata.py
Normal file
@@ -0,0 +1,189 @@
|
||||
import pytest
|
||||
|
||||
SPACE_UNITS = [
|
||||
"angstrom",
|
||||
"attometer",
|
||||
"centimeter",
|
||||
"decimeter",
|
||||
"exameter",
|
||||
"femtometer",
|
||||
"foot",
|
||||
"gigameter",
|
||||
"hectometer",
|
||||
"inch",
|
||||
"kilometer",
|
||||
"megameter",
|
||||
"meter",
|
||||
"micrometer",
|
||||
"mile",
|
||||
"millimeter",
|
||||
"nanometer",
|
||||
"parsec",
|
||||
"petameter",
|
||||
"picometer",
|
||||
"terameter",
|
||||
"yard",
|
||||
"yoctometer",
|
||||
"yottameter",
|
||||
"zeptometer",
|
||||
"zettameter",
|
||||
]
|
||||
|
||||
TIME_UNITS = [
|
||||
"attosecond",
|
||||
"centisecond",
|
||||
"day",
|
||||
"decisecond",
|
||||
"exasecond",
|
||||
"femtosecond",
|
||||
"gigasecond",
|
||||
"hectosecond",
|
||||
"hour",
|
||||
"kilosecond",
|
||||
"megasecond",
|
||||
"microsecond",
|
||||
"millisecond",
|
||||
"minute",
|
||||
"nanosecond",
|
||||
"petasecond",
|
||||
"picosecond",
|
||||
"second",
|
||||
"terasecond",
|
||||
"yoctosecond",
|
||||
"yottasecond",
|
||||
"zeptosecond",
|
||||
"zettasecond",
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dummy_metadata(dummy_model):
|
||||
from navigate.model.metadata_sources.zarr_metadata import OMEZarrMetadata
|
||||
|
||||
# Create metadata
|
||||
md = OMEZarrMetadata()
|
||||
|
||||
md.configuration = dummy_model.configuration
|
||||
|
||||
return md
|
||||
|
||||
|
||||
def test_axes(dummy_metadata):
|
||||
|
||||
axes = dummy_metadata._axes
|
||||
|
||||
# Check length
|
||||
assert (len(axes) > 1) and (len(axes) < 6)
|
||||
|
||||
# Check list types and count
|
||||
time_count = 0
|
||||
space_count = 0
|
||||
channel_count = 0
|
||||
custom_count = 0
|
||||
for d in axes:
|
||||
if d["type"] == "time":
|
||||
assert d["unit"] in TIME_UNITS
|
||||
time_count += 1
|
||||
elif d["type"] == "space":
|
||||
assert d["unit"] in SPACE_UNITS
|
||||
space_count += 1
|
||||
elif d["type"] == "channel":
|
||||
channel_count += 1
|
||||
else:
|
||||
custom_count += 1
|
||||
|
||||
assert (space_count > 1) and (space_count < 4)
|
||||
assert time_count < 2
|
||||
assert ((channel_count < 2) and (custom_count == 0)) or (
|
||||
(channel_count == 0) and (custom_count < 2)
|
||||
)
|
||||
|
||||
# Check order
|
||||
order_type = [x["type"] for x in axes]
|
||||
if "time" in order_type:
|
||||
# Time must be first, if present
|
||||
assert order_type.index("time") == 0
|
||||
if "channel" in order_type:
|
||||
# Channel must be before all the space axes, if present
|
||||
ci = order_type.index("channel")
|
||||
for i, el in enumerate(order_type):
|
||||
if el == "space":
|
||||
assert i > ci
|
||||
|
||||
# Skip zyx order spec as the naming of axes is not enforcable.
|
||||
|
||||
|
||||
def test_stage_positions_to_translation_transform(dummy_metadata):
|
||||
import random
|
||||
|
||||
pos = [random.random() for _ in range(5)]
|
||||
|
||||
translation = dummy_metadata._stage_positions_to_translation_transform(*pos)
|
||||
|
||||
axes = dummy_metadata._axes
|
||||
|
||||
assert len(translation) == len(axes)
|
||||
|
||||
|
||||
def test_scale_transform(dummy_metadata):
|
||||
scale = dummy_metadata._scale_transform()
|
||||
|
||||
axes = dummy_metadata._axes
|
||||
|
||||
assert len(scale) == len(axes)
|
||||
|
||||
|
||||
def test_coordinate_transformations(dummy_metadata):
|
||||
import random
|
||||
|
||||
pos = [random.random() for _ in range(5)]
|
||||
|
||||
translation = dummy_metadata._stage_positions_to_translation_transform(*pos)
|
||||
scale = dummy_metadata._scale_transform()
|
||||
|
||||
assert len(dummy_metadata._coordinate_transformations(scale)) == 1
|
||||
|
||||
combo = dummy_metadata._coordinate_transformations(scale, translation)
|
||||
assert len(combo) == 2
|
||||
assert combo[0]["type"] == "scale" and combo[1]["type"] == "translation"
|
||||
|
||||
with pytest.raises(UserWarning):
|
||||
dummy_metadata._coordinate_transformations(translation=translation)
|
||||
|
||||
|
||||
def test_multiscale_metadata(dummy_metadata):
|
||||
"""https://ngff.openmicroscopy.org/0.4/#multiscale-md"""
|
||||
import numpy as np
|
||||
import random
|
||||
|
||||
resolutions = np.array([[1, 1, 1], [2, 2, 1], [4, 4, 1], [8, 8, 1]], dtype=int)
|
||||
paths = [f"path{i}" for i in range(resolutions.shape[0])]
|
||||
view = {k: random.random() for k in ["x", "y", "z", "theta", "f"]}
|
||||
|
||||
msd = dummy_metadata.multiscales_dict("test", paths, resolutions, view)
|
||||
|
||||
# Each "multiscales" dictionary MUST contain the field "axes"
|
||||
assert "axes" in msd.keys()
|
||||
|
||||
# Each "multiscales" dictionary MUST contain the field "datasets"
|
||||
assert "datasets" in msd.keys()
|
||||
# Each dictionary in "datasets" MUST contain the field "path",
|
||||
# whose value contains the path to the array for this resolution
|
||||
# relative to the current zarr group. The "path"s MUST be ordered
|
||||
# from largest (i.e. highest resolution) to smallest.
|
||||
# Each "datasets" dictionary MUST have the same number of dimensions
|
||||
# and MUST NOT have more than 5 dimensions.
|
||||
|
||||
# Each "multiscales" dictionary SHOULD contain the field "name"
|
||||
assert "name" in msd.keys()
|
||||
|
||||
# Each "multiscales" dictionary MAY contain the field "coordinateTransformations"
|
||||
assert "coordinateTransformations" in msd.keys()
|
||||
|
||||
# It SHOULD contain the field "version"
|
||||
assert "version" in msd.keys()
|
||||
|
||||
# Each "multiscales" dictionary SHOULD contain the field "type", which gives
|
||||
# the type of downscaling method used to generate the multiscale image pyramid.
|
||||
# It SHOULD contain the field "metadata", which contains a dictionary with
|
||||
# additional information about the downscaling method.
|
||||
145
test/model/test_aslm_analysis.py
Normal file
145
test/model/test_aslm_analysis.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.
|
||||
|
||||
# Standard Library Imports
|
||||
import unittest
|
||||
|
||||
# Third Party Imports
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
# Local Imports
|
||||
# sys.path.append('../../../')
|
||||
|
||||
|
||||
def box(size):
|
||||
x = np.linspace(0, 1, 100)
|
||||
X, Y = np.meshgrid(x, x)
|
||||
l = (1 - size) / 2 # noqa
|
||||
u = l + size
|
||||
image = (X > l) & (X < u) & (Y > l) & (Y < u)
|
||||
return image.astype(float)
|
||||
|
||||
|
||||
def power_tent(r, off, scale, sigma, alpha):
|
||||
return off + scale * (1 - np.abs(sigma * r) ** alpha)
|
||||
|
||||
|
||||
def power_tent_res(x, r, val):
|
||||
return power_tent(r, *x) - val
|
||||
|
||||
|
||||
def rsq(res_func, x, r, val):
|
||||
ss_err = (res_func(x, r, val) ** 2).sum()
|
||||
ss_tot = ((val - val.mean()) ** 2).sum()
|
||||
rsq = 1 - (ss_err / ss_tot)
|
||||
return rsq
|
||||
|
||||
|
||||
def test_fast_normalized_dct_shannon_entropy_tent():
|
||||
from scipy.ndimage import gaussian_filter
|
||||
from scipy.optimize import least_squares
|
||||
|
||||
from navigate.model.analysis.image_contrast import (
|
||||
fast_normalized_dct_shannon_entropy,
|
||||
)
|
||||
|
||||
im = box(0.5)
|
||||
|
||||
r = range(0, 60)
|
||||
points = np.zeros((len(r),))
|
||||
for i in r:
|
||||
points[i] = fast_normalized_dct_shannon_entropy(gaussian_filter(im, i), 1)[0]
|
||||
|
||||
res = least_squares(
|
||||
power_tent_res, [np.min(points), np.max(points), 1, 0.5], args=(r, points)
|
||||
)
|
||||
|
||||
assert rsq(power_tent_res, res.x, r, points) > 0.9
|
||||
|
||||
|
||||
def test_fast_normalized_dct_shannon_entropy():
|
||||
from navigate.model.analysis.image_contrast import (
|
||||
fast_normalized_dct_shannon_entropy,
|
||||
)
|
||||
|
||||
# image_array = np.ones((np.random.randint(1,4),128,128)).squeeze()
|
||||
image_array = np.ones((128, 128)).squeeze()
|
||||
psf_support_diameter_xy = np.random.randint(3, 10)
|
||||
|
||||
entropy = fast_normalized_dct_shannon_entropy(image_array, psf_support_diameter_xy)
|
||||
|
||||
assert np.all(entropy == 0)
|
||||
|
||||
|
||||
"""
|
||||
Delete the below assert once the calculate entropy function is found
|
||||
"""
|
||||
|
||||
|
||||
def test_entropy():
|
||||
assert True
|
||||
|
||||
|
||||
try:
|
||||
# from navigate.model.navigate_analysis import Analysis as navigate_analysis
|
||||
from navigate.model.navigate_debug_model import calculate_entropy
|
||||
|
||||
class TestNavigateAnalysis(unittest.TestCase):
|
||||
"""
|
||||
Unit Tests for the Navigate Analysis Module
|
||||
"""
|
||||
|
||||
@pytest.mark.skip(reason="file path not found")
|
||||
def test_calculate_entropy_on(self):
|
||||
"""
|
||||
Test the calculation of the Shannon Entropy
|
||||
"""
|
||||
dct_array = np.ones((128, 128))
|
||||
otf_support_x = 3
|
||||
otf_support_y = 3
|
||||
# This trys to call from the navigate_analysis module however its only
|
||||
# located in the navigate_debug_model
|
||||
# entropy = navigate_analysis.calculate_entropy()
|
||||
entropy = calculate_entropy(
|
||||
self,
|
||||
dct_array=dct_array,
|
||||
otf_support_x=otf_support_x,
|
||||
otf_support_y=otf_support_y,
|
||||
)
|
||||
self.assertEqual(entropy, 0)
|
||||
|
||||
except ImportError as e:
|
||||
print(e)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
84
test/model/test_device_startup_functions.py
Normal file
84
test/model/test_device_startup_functions.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# 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 MagicMock
|
||||
|
||||
# Third party imports
|
||||
|
||||
# Local application imports
|
||||
from navigate.model.device_startup_functions import auto_redial
|
||||
|
||||
|
||||
class TestAutoRedial(unittest.TestCase):
|
||||
"""Test the auto_redial function."""
|
||||
|
||||
def test_successful_connection_first_try(self):
|
||||
"""Test successful connection on the first try."""
|
||||
mock_func = MagicMock(return_value="success")
|
||||
result = auto_redial(mock_func, ())
|
||||
self.assertEqual(result, "success")
|
||||
|
||||
def test_successful_connection_after_failures(self):
|
||||
"""Test successful connection after a few failures."""
|
||||
mock_func = MagicMock(
|
||||
side_effect=[
|
||||
Exception("fail"),
|
||||
Exception("fail"),
|
||||
"success",
|
||||
Exception("fail"),
|
||||
"success",
|
||||
]
|
||||
)
|
||||
result = auto_redial(mock_func, (), n_tries=5)
|
||||
self.assertEqual(result, "success")
|
||||
assert mock_func.call_count == 3
|
||||
|
||||
def test_failure_after_all_retries(self):
|
||||
"""Test failure after all retries."""
|
||||
mock_func = MagicMock(side_effect=Exception("fail"))
|
||||
with self.assertRaises(Exception):
|
||||
auto_redial(mock_func, (), n_tries=3)
|
||||
assert mock_func.call_count == 3
|
||||
|
||||
def test_exception_type_handling(self):
|
||||
"""Test that only the specified exception type is caught."""
|
||||
mock_func = MagicMock(side_effect=[ValueError("wrong exception"), "success"])
|
||||
with self.assertRaises(ValueError):
|
||||
auto_redial(mock_func, (), n_tries=3, exception=TypeError)
|
||||
assert mock_func.call_count == 1
|
||||
|
||||
def test_arguments_passing(self):
|
||||
"""Test that arguments and keyword arguments are correctly passed."""
|
||||
mock_func = MagicMock()
|
||||
auto_redial(mock_func, (1, 2), n_tries=1, kwarg1="test")
|
||||
mock_func.assert_called_with(1, 2, kwarg1="test")
|
||||
242
test/model/test_microscope.py
Normal file
242
test/model/test_microscope.py
Normal file
@@ -0,0 +1,242 @@
|
||||
# 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
|
||||
import random
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def dummy_microscope(dummy_model):
|
||||
from navigate.model.microscope import Microscope
|
||||
from navigate.model.device_startup_functions import load_devices
|
||||
|
||||
devices_dict = load_devices(
|
||||
dummy_model.active_microscope_name, dummy_model.configuration, is_synthetic=True
|
||||
)
|
||||
|
||||
return Microscope(
|
||||
dummy_model.active_microscope_name,
|
||||
dummy_model.configuration,
|
||||
devices_dict,
|
||||
is_synthetic=True,
|
||||
is_virtual=False,
|
||||
)
|
||||
|
||||
|
||||
def test_prepare_acquisition(dummy_microscope):
|
||||
waveform_dict = dummy_microscope.prepare_acquisition()
|
||||
|
||||
channels = dummy_microscope.configuration["experiment"]["MicroscopeState"][
|
||||
"channels"
|
||||
]
|
||||
|
||||
assert dummy_microscope.current_channel == 0
|
||||
assert dummy_microscope.central_focus is None
|
||||
assert dummy_microscope.available_channels == list(
|
||||
map(
|
||||
lambda c: int(c[len("channel_") :]),
|
||||
filter(lambda k: channels[k]["is_selected"], channels.keys()),
|
||||
)
|
||||
)
|
||||
assert dummy_microscope.camera.is_acquiring is True
|
||||
assert dummy_microscope.shutter.shutter_state is True
|
||||
assert isinstance(waveform_dict, dict)
|
||||
assert [
|
||||
k in waveform_dict.keys()
|
||||
for k in ["camera_waveform", "remote_focus_waveform", "galvo_waveform"]
|
||||
]
|
||||
|
||||
def test_move_stage(dummy_microscope):
|
||||
import numpy as np
|
||||
|
||||
acquisition_mode = dummy_microscope.configuration["experiment"]["MicroscopeState"][
|
||||
"image_mode"
|
||||
]
|
||||
|
||||
expected_device_flag = {
|
||||
"continous": True,
|
||||
"single": True,
|
||||
"z-stack": False,
|
||||
"customized": False,
|
||||
}
|
||||
for mode in expected_device_flag:
|
||||
dummy_microscope.configuration["experiment"]["MicroscopeState"][
|
||||
"image_mode"
|
||||
] = mode
|
||||
|
||||
# move stage to random position
|
||||
axes = ["x", "y", "z", "theta", "f"]
|
||||
for i in range(5):
|
||||
test_axes = random.sample(axes, i+1)
|
||||
pos_dict = {
|
||||
f"{k}_abs": v
|
||||
for k, v in zip(test_axes, np.random.rand(len(test_axes)) * 100)
|
||||
}
|
||||
dummy_microscope.move_stage(pos_dict, wait_until_done=True)
|
||||
|
||||
assert dummy_microscope.ask_stage_for_position == expected_device_flag[mode]
|
||||
|
||||
if expected_device_flag[mode] == False:
|
||||
# assert position is cached
|
||||
for axis in test_axes:
|
||||
assert round(dummy_microscope.ret_pos_dict[axis + "_pos"], 2) == round(
|
||||
pos_dict[f"{axis}_abs"], 2
|
||||
)
|
||||
|
||||
# set back acquisition mode
|
||||
dummy_microscope.configuration["experiment"]["MicroscopeState"][
|
||||
"image_mode"
|
||||
] = acquisition_mode
|
||||
|
||||
def test_get_stage_position(dummy_microscope):
|
||||
import numpy as np
|
||||
|
||||
acquisition_mode = dummy_microscope.configuration["experiment"]["MicroscopeState"][
|
||||
"image_mode"
|
||||
]
|
||||
|
||||
report_position_funcs = {}
|
||||
axes_dict = {}
|
||||
for stage, axes in dummy_microscope.stages_list:
|
||||
for axis in axes:
|
||||
axes_dict[axis] = axes
|
||||
report_position_funcs[axis] = stage.report_position
|
||||
|
||||
is_called = dict([(axis, False) for axis in dummy_microscope.stages])
|
||||
def report_position_mock(axis):
|
||||
def func():
|
||||
for a in axes_dict[axis]:
|
||||
is_called[a] = True
|
||||
return report_position_funcs[axis]()
|
||||
return func
|
||||
|
||||
for axis in dummy_microscope.stages:
|
||||
dummy_microscope.stages[axis].report_position = report_position_mock(axis)
|
||||
|
||||
expected_device_flag = {
|
||||
"continous": True,
|
||||
"single": True,
|
||||
"z-stack": False,
|
||||
"customized": False,
|
||||
}
|
||||
for mode in expected_device_flag:
|
||||
dummy_microscope.configuration["experiment"]["MicroscopeState"][
|
||||
"image_mode"
|
||||
] = mode
|
||||
|
||||
# move stage to random position
|
||||
pos_dict = {
|
||||
f"{k}_abs": v
|
||||
for k, v in zip(["x", "y", "z", "theta", "f"], np.random.rand(5) * 100)
|
||||
}
|
||||
dummy_microscope.move_stage(pos_dict, wait_until_done=True)
|
||||
|
||||
assert dummy_microscope.ask_stage_for_position == expected_device_flag[mode]
|
||||
|
||||
for axis in is_called:
|
||||
is_called[axis] = False
|
||||
|
||||
stage_dict = dummy_microscope.get_stage_position()
|
||||
|
||||
# verify if report_position is called according to mode
|
||||
for axis in is_called:
|
||||
assert is_called[axis] == expected_device_flag[mode]
|
||||
|
||||
ret_pos_dict = {}
|
||||
for axis in dummy_microscope.stages:
|
||||
pos_axis = axis + "_pos"
|
||||
temp_pos = dummy_microscope.stages[axis].report_position()
|
||||
ret_pos_dict[pos_axis] = round(temp_pos[pos_axis], 2)
|
||||
|
||||
assert isinstance(stage_dict, dict)
|
||||
assert ret_pos_dict == stage_dict
|
||||
|
||||
# Check caching
|
||||
assert dummy_microscope.ask_stage_for_position is False
|
||||
for axis in is_called:
|
||||
is_called[axis] = False
|
||||
stage_dict = dummy_microscope.get_stage_position()
|
||||
assert ret_pos_dict == stage_dict
|
||||
assert dummy_microscope.ask_stage_for_position is False
|
||||
for axis in is_called:
|
||||
assert is_called[axis] is False
|
||||
|
||||
|
||||
# set back acquisition mode
|
||||
dummy_microscope.configuration["experiment"]["MicroscopeState"][
|
||||
"image_mode"
|
||||
] = acquisition_mode
|
||||
|
||||
# restore report position functions
|
||||
for axis in dummy_microscope.stages:
|
||||
dummy_microscope.stages[axis].report_position = report_position_funcs[axis]
|
||||
|
||||
|
||||
def test_prepare_next_channel(dummy_microscope):
|
||||
dummy_microscope.prepare_acquisition()
|
||||
|
||||
current_channel = dummy_microscope.available_channels[0]
|
||||
channel_key = f"channel_{current_channel}"
|
||||
channel_dict = dummy_microscope.configuration["experiment"]["MicroscopeState"][
|
||||
"channels"
|
||||
][channel_key]
|
||||
channel_dict["defocus"] = random.randint(1, 10)
|
||||
|
||||
dummy_microscope.prepare_next_channel()
|
||||
|
||||
assert dummy_microscope.current_channel == current_channel
|
||||
assert dummy_microscope.get_stage_position()["f_pos"] == (
|
||||
dummy_microscope.central_focus + channel_dict["defocus"]
|
||||
)
|
||||
|
||||
|
||||
def test_calculate_all_waveform(dummy_microscope):
|
||||
# set waveform template to default
|
||||
dummy_microscope.configuration["experiment"]["MicroscopeState"][
|
||||
"waveform_template"
|
||||
] = "Default"
|
||||
waveform_dict = dummy_microscope.calculate_all_waveform()
|
||||
# verify the waveform lengths
|
||||
sweep_times = dummy_microscope.sweep_times
|
||||
sample_rate = dummy_microscope.configuration["configuration"]["microscopes"][
|
||||
dummy_microscope.microscope_name
|
||||
]["daq"]["sample_rate"]
|
||||
for channel_key in sweep_times:
|
||||
waveform_length = int(sweep_times[channel_key] * sample_rate)
|
||||
assert waveform_dict["camera_waveform"][channel_key].shape == (waveform_length,)
|
||||
assert waveform_dict["remote_focus_waveform"][channel_key].shape == (
|
||||
waveform_length,
|
||||
)
|
||||
for i in range(len(waveform_dict["galvo_waveform"])):
|
||||
assert waveform_dict["galvo_waveform"][i][channel_key].shape == (
|
||||
waveform_length,
|
||||
)
|
||||
443
test/model/test_model.py
Normal file
443
test/model/test_model.py
Normal file
@@ -0,0 +1,443 @@
|
||||
# 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
|
||||
import pytest
|
||||
import os
|
||||
from multiprocessing import Manager
|
||||
from unittest.mock import MagicMock
|
||||
import multiprocessing
|
||||
|
||||
# Third Party Imports
|
||||
|
||||
# Local Imports
|
||||
|
||||
IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def model():
|
||||
from types import SimpleNamespace
|
||||
from pathlib import Path
|
||||
|
||||
from navigate.model.model import Model
|
||||
from navigate.config.config import (
|
||||
load_configs,
|
||||
verify_experiment_config,
|
||||
verify_waveform_constants,
|
||||
verify_configuration,
|
||||
verify_positions_config,
|
||||
)
|
||||
from navigate.tools.file_functions import load_yaml_file
|
||||
|
||||
with Manager() as manager:
|
||||
|
||||
# Use configuration files that ship with the code base
|
||||
configuration_directory = Path.joinpath(
|
||||
Path(__file__).resolve().parent.parent.parent, "src", "navigate", "config"
|
||||
)
|
||||
configuration_path = Path.joinpath(
|
||||
configuration_directory, "configuration.yaml"
|
||||
)
|
||||
experiment_path = Path.joinpath(configuration_directory, "experiment.yml")
|
||||
waveform_constants_path = Path.joinpath(
|
||||
configuration_directory, "waveform_constants.yml"
|
||||
)
|
||||
rest_api_path = Path.joinpath(configuration_directory, "rest_api_config.yml")
|
||||
multi_positions_path = Path.joinpath(
|
||||
configuration_directory, "multi_positions.yml"
|
||||
)
|
||||
|
||||
event_queue = MagicMock()
|
||||
|
||||
configuration = load_configs(
|
||||
manager,
|
||||
configuration=configuration_path,
|
||||
experiment=experiment_path,
|
||||
waveform_constants=waveform_constants_path,
|
||||
rest_api_config=rest_api_path,
|
||||
)
|
||||
verify_configuration(manager, configuration)
|
||||
verify_experiment_config(manager, configuration)
|
||||
verify_waveform_constants(manager, configuration)
|
||||
|
||||
positions = load_yaml_file(multi_positions_path)
|
||||
positions = verify_positions_config(positions)
|
||||
configuration["multi_positions"] = positions
|
||||
|
||||
queue = multiprocessing.Queue()
|
||||
|
||||
model = Model(
|
||||
args=SimpleNamespace(synthetic_hardware=True),
|
||||
configuration=configuration,
|
||||
event_queue=event_queue,
|
||||
log_queue=queue,
|
||||
)
|
||||
|
||||
model.__test_manager = manager
|
||||
|
||||
yield model
|
||||
while not queue.empty():
|
||||
queue.get()
|
||||
queue.close()
|
||||
queue.join_thread()
|
||||
|
||||
|
||||
def test_single_acquisition(model):
|
||||
state = model.configuration["experiment"]["MicroscopeState"]
|
||||
state["image_mode"] = "single"
|
||||
state["is_save"] = False
|
||||
|
||||
n_frames = len(
|
||||
list(filter(lambda channel: channel["is_selected"], state["channels"].values()))
|
||||
)
|
||||
|
||||
show_img_pipe = model.create_pipe("show_img_pipe")
|
||||
|
||||
model.run_command("acquire")
|
||||
|
||||
image_id = show_img_pipe.recv()
|
||||
n_images = 0
|
||||
max_iters = 10
|
||||
while image_id != "stop" and max_iters > 0:
|
||||
image_id = show_img_pipe.recv()
|
||||
n_images += 1
|
||||
max_iters -= 1
|
||||
|
||||
assert n_images == n_frames
|
||||
model.data_thread.join()
|
||||
model.release_pipe("show_img_pipe")
|
||||
|
||||
|
||||
def test_live_acquisition(model):
|
||||
state = model.configuration["experiment"]["MicroscopeState"]
|
||||
state["image_mode"] = "live"
|
||||
|
||||
n_images = 0
|
||||
pre_channel = 0
|
||||
|
||||
show_img_pipe = model.create_pipe("show_img_pipe")
|
||||
|
||||
model.run_command("acquire")
|
||||
|
||||
while True:
|
||||
image_id = show_img_pipe.recv()
|
||||
if image_id == "stop":
|
||||
break
|
||||
channel_id = model.active_microscope.current_channel
|
||||
assert channel_id != pre_channel
|
||||
pre_channel = channel_id
|
||||
n_images += 1
|
||||
if n_images >= 30:
|
||||
model.run_command("stop")
|
||||
model.data_thread.join()
|
||||
model.release_pipe("show_img_pipe")
|
||||
|
||||
|
||||
def test_autofocus_live_acquisition(model):
|
||||
state = model.configuration["experiment"]["MicroscopeState"]
|
||||
state["image_mode"] = "live"
|
||||
|
||||
n_images = 0
|
||||
pre_channel = 0
|
||||
autofocus = False
|
||||
|
||||
show_img_pipe = model.create_pipe("show_img_pipe")
|
||||
|
||||
model.run_command("acquire")
|
||||
|
||||
while True:
|
||||
image_id = show_img_pipe.recv()
|
||||
if image_id == "stop":
|
||||
break
|
||||
channel_id = model.active_microscope.current_channel
|
||||
if not autofocus:
|
||||
assert channel_id != pre_channel
|
||||
pre_channel = channel_id
|
||||
n_images += 1
|
||||
if n_images >= 100:
|
||||
model.run_command("stop")
|
||||
elif n_images >= 70:
|
||||
autofocus = False
|
||||
elif n_images == 30:
|
||||
autofocus = True
|
||||
model.run_command("autofocus")
|
||||
|
||||
model.data_thread.join()
|
||||
model.release_pipe("show_img_pipe")
|
||||
|
||||
|
||||
@pytest.mark.skipif(IN_GITHUB_ACTIONS, reason="Test hangs entire workflow on GitHub.")
|
||||
def test_multiposition_acquisition(model):
|
||||
"""Test that the multiposition acquisition works as expected.
|
||||
|
||||
This test is meant to confirm that if the multi position check box is set,
|
||||
but there aren't actually any positions in the multi-position table, that the
|
||||
acquisition proceeds as if it is not a multi position acquisition.
|
||||
|
||||
Sleep statements are used to ensure that the event queue has ample opportunity to
|
||||
be populated with the disable_multiposition event. This is because the event queue
|
||||
is a multiprocessing.Queue, which is not thread safe.
|
||||
"""
|
||||
# from time import sleep
|
||||
from navigate.config.config import update_config_dict
|
||||
|
||||
# def check_queue(event, event_queue):
|
||||
# """Check if the event queue contains the event. If it does, return True.
|
||||
# Otherwise, return False.
|
||||
|
||||
# Parameters
|
||||
# ----------
|
||||
# event : str
|
||||
# The event to check for in the event queue.
|
||||
# event_queue : multiprocessing.Queue
|
||||
# The event queue to check.
|
||||
# """
|
||||
# while not event_queue.empty():
|
||||
# ev, _ = event_queue.get()
|
||||
# if ev == event:
|
||||
# return True
|
||||
# return False
|
||||
|
||||
_ = model.create_pipe("show_img_pipe")
|
||||
|
||||
# Multiposition is selected and actually is True
|
||||
model.configuration["experiment"]["MicroscopeState"]["is_multiposition"] = True
|
||||
update_config_dict(
|
||||
model.__test_manager, # noqa
|
||||
model.configuration,
|
||||
"multi_positions",
|
||||
[["X", "Y", "Z", "THETA", "F"],[10.0, 10.0, 10.0, 10.0, 10.0]],
|
||||
)
|
||||
model.configuration["experiment"]["MicroscopeState"]["image_mode"] = "z-stack"
|
||||
model.configuration["experiment"]["MicroscopeState"]["number_z_steps"] = 10
|
||||
|
||||
model.configuration["experiment"]["MicroscopeState"]["step_size"] = 5.0
|
||||
model.configuration["experiment"]["MicroscopeState"]["end_position"] = (
|
||||
model.configuration["experiment"]["MicroscopeState"]["start_position"] + 15.0
|
||||
)
|
||||
model.run_command("acquire")
|
||||
|
||||
# sleep(1)
|
||||
# assert (
|
||||
# check_queue(event="disable_multiposition", event_queue=model.event_queue)
|
||||
# is False
|
||||
# )
|
||||
assert (
|
||||
model.configuration["experiment"]["MicroscopeState"]["is_multiposition"] is True
|
||||
)
|
||||
model.data_thread.join()
|
||||
|
||||
# Multiposition is selected but not actually True
|
||||
update_config_dict(
|
||||
model.__test_manager,
|
||||
model.configuration,
|
||||
"multi_positions",
|
||||
[], # noqa
|
||||
)
|
||||
|
||||
model.run_command("acquire")
|
||||
# sleep(1)
|
||||
# # Check that the event queue is called with the disable_multiposition statement
|
||||
# assert (
|
||||
# check_queue(event="disable_multiposition", event_queue=model.event_queue)
|
||||
# is True
|
||||
# )
|
||||
assert (
|
||||
model.configuration["experiment"]["MicroscopeState"]["is_multiposition"]
|
||||
is False
|
||||
)
|
||||
model.data_thread.join()
|
||||
model.release_pipe("show_img_pipe")
|
||||
|
||||
|
||||
def test_change_resolution(model):
|
||||
"""
|
||||
Note: The stage position check is an absolute mess due to us instantiating two
|
||||
SyntheticStages--one for each microsocpe. We have to continuously reset the
|
||||
stage positions to all zeros and make the configuration.yaml that comes with the
|
||||
software have negative stage bounds.
|
||||
"""
|
||||
scopes = random.choices(
|
||||
model.configuration["configuration"]["microscopes"].keys(), k=10
|
||||
)
|
||||
zooms = [
|
||||
random.choice(
|
||||
model.configuration["configuration"]["microscopes"][scope]["zoom"][
|
||||
"position"
|
||||
].keys()
|
||||
)
|
||||
for scope in scopes
|
||||
]
|
||||
axes = ["x", "y", "z", "theta", "f"]
|
||||
|
||||
for scope, zoom in zip(scopes, zooms):
|
||||
# reset stage axes to all zeros, to match default SyntheticStage behaviour
|
||||
for microscope in model.microscopes:
|
||||
for ax in axes:
|
||||
model.microscopes[microscope].stages[ax].move_absolute(
|
||||
{ax + "_abs": 0}, wait_until_done=True
|
||||
)
|
||||
|
||||
former_offset_dict = model.configuration["configuration"]["microscopes"][
|
||||
model.configuration["experiment"]["MicroscopeState"]["microscope_name"]
|
||||
]["stage"]
|
||||
former_pos_dict = model.get_stage_position()
|
||||
former_zoom = model.configuration["experiment"]["MicroscopeState"]["zoom"]
|
||||
model.active_microscope.zoom.set_zoom(former_zoom)
|
||||
print(f"{model.active_microscope_name}: {former_pos_dict}")
|
||||
|
||||
print(
|
||||
f"CHANGING {model.active_microscope_name} at "
|
||||
f'{model.configuration["experiment"]["MicroscopeState"]["zoom"]} to {scope}'
|
||||
f" at {zoom}"
|
||||
)
|
||||
model.configuration["experiment"]["MicroscopeState"]["microscope_name"] = scope
|
||||
model.configuration["experiment"]["MicroscopeState"]["zoom"] = zoom
|
||||
solvent = model.configuration["experiment"]["Saving"]["solvent"]
|
||||
|
||||
model.change_resolution(scope)
|
||||
|
||||
self_offset_dict = model.configuration["configuration"]["microscopes"][scope][
|
||||
"stage"
|
||||
]
|
||||
pos_dict = model.get_stage_position()
|
||||
|
||||
print(f"{model.active_microscope_name}: {pos_dict}")
|
||||
|
||||
# reset stage axes to all zeros, to match default SyntheticStage behaviour
|
||||
for ax in model.active_microscope.stages:
|
||||
print(f"axis {ax}")
|
||||
try:
|
||||
shift_ax = float(
|
||||
model.active_microscope.zoom.stage_offsets[solvent][ax][
|
||||
former_zoom
|
||||
][zoom]
|
||||
)
|
||||
print(f"shift_ax {shift_ax}")
|
||||
except (TypeError, KeyError):
|
||||
shift_ax = 0
|
||||
assert (
|
||||
pos_dict[ax + "_pos"]
|
||||
- self_offset_dict[ax + "_offset"]
|
||||
+ former_offset_dict[ax + "_offset"]
|
||||
- shift_ax
|
||||
) == 0
|
||||
|
||||
assert model.active_microscope_name == scope
|
||||
assert model.active_microscope.zoom.zoomvalue == zoom
|
||||
|
||||
|
||||
def test_get_feature_list(model):
|
||||
feature_lists = model.feature_list
|
||||
|
||||
assert model.get_feature_list(0) == ""
|
||||
assert model.get_feature_list(len(feature_lists) + 1) == ""
|
||||
|
||||
from navigate.model.features.feature_related_functions import (
|
||||
convert_feature_list_to_str,
|
||||
)
|
||||
|
||||
for i in range(len(feature_lists)):
|
||||
feature_str = model.get_feature_list(i + 1)
|
||||
if "shared_list" not in feature_str:
|
||||
assert feature_str == convert_feature_list_to_str(feature_lists[i])
|
||||
# assert convert_str_to_feature_list(feature_str) == feature_lists[i]
|
||||
|
||||
|
||||
def test_load_feature_list_from_str(model):
|
||||
feature_lists = model.feature_list
|
||||
|
||||
l = len(feature_lists) # noqa
|
||||
model.load_feature_list_from_str('[{"name": PrepareNextChannel}]')
|
||||
assert len(feature_lists) == l + 1
|
||||
from navigate.model.features.feature_related_functions import (
|
||||
convert_feature_list_to_str,
|
||||
)
|
||||
|
||||
assert (
|
||||
convert_feature_list_to_str(feature_lists[-1])
|
||||
== '[{"name": PrepareNextChannel,},]'
|
||||
)
|
||||
del feature_lists[-1]
|
||||
feature_str = '[{"name": LoopByCount,"args": ([1, 2.0, True, False, \'abc\'],),},]'
|
||||
model.load_feature_list_from_str(feature_str)
|
||||
assert len(feature_lists) == l + 1
|
||||
assert convert_feature_list_to_str(feature_lists[-1]) == feature_str
|
||||
del feature_lists[-1]
|
||||
|
||||
|
||||
def test_load_feature_records(model):
|
||||
feature_lists = model.feature_list
|
||||
l = len(feature_lists) # noqa
|
||||
|
||||
from navigate.config.config import get_navigate_path
|
||||
from navigate.tools.file_functions import save_yaml_file, load_yaml_file
|
||||
from navigate.model.features.feature_related_functions import (
|
||||
convert_feature_list_to_str,
|
||||
)
|
||||
|
||||
feature_lists_path = get_navigate_path() + "/feature_lists"
|
||||
|
||||
if not os.path.exists(feature_lists_path):
|
||||
os.makedirs(feature_lists_path)
|
||||
|
||||
feature_records = load_yaml_file(f"{feature_lists_path}/__sequence.yml")
|
||||
if not feature_records:
|
||||
feature_records = []
|
||||
|
||||
save_yaml_file(
|
||||
feature_lists_path,
|
||||
{
|
||||
"module_name": None,
|
||||
"feature_list_name": "Test Feature List 5",
|
||||
"feature_list": "[({'name': PrepareNextChannel}, "
|
||||
"{'name': LoopByCount, 'args': (3,),})]",
|
||||
},
|
||||
"__test_1.yml",
|
||||
)
|
||||
|
||||
model.load_feature_records()
|
||||
assert len(feature_lists) == l + len(feature_records) + 1
|
||||
assert (
|
||||
convert_feature_list_to_str(feature_lists[-1])
|
||||
== '[({"name": PrepareNextChannel,},{"name": LoopByCount,"args": (3,),},),]'
|
||||
)
|
||||
|
||||
del feature_lists[-1]
|
||||
os.remove(f"{feature_lists_path}/__test_1.yml")
|
||||
|
||||
model.load_feature_records()
|
||||
assert len(feature_lists) == l + len(feature_records) * 2
|
||||
feature_records_2 = load_yaml_file(f"{feature_lists_path}/__sequence.yml")
|
||||
assert feature_records == feature_records_2
|
||||
os.remove(f"{feature_lists_path}/__sequence.yml")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user