feat: init

This commit is contained in:
2025-12-04 16:07:30 +08:00
commit 262583a57f
681 changed files with 117578 additions and 0 deletions

View 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

View 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()

View 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()

View 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

View 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

View 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")

View 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()

View 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()