feat: init
This commit is contained in:
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()
|
||||
Reference in New Issue
Block a user