Files
navigate/test/model/test_microscope.py
2025-12-04 16:07:30 +08:00

243 lines
8.7 KiB
Python

# 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,
)