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

240 lines
9.0 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 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()