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

648 lines
22 KiB
Python

# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted for academic and research use only (subject to the
# limitations in the disclaimer below) provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the copyright holders nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from pathlib import Path
import multiprocessing as mp
from multiprocessing import Manager
import threading
import time
# Third Party Imports
import numpy as np
import random
# Local Imports
from navigate.config.config import (
load_configs,
verify_experiment_config,
verify_waveform_constants,
verify_configuration,
verify_positions_config,
)
from navigate.model.devices.camera.synthetic import (
SyntheticCamera,
SyntheticCameraController,
)
from navigate.model.features.feature_container import (
load_features,
)
from navigate.tools.file_functions import load_yaml_file
class DummyController:
"""Dummy Controller"""
def __init__(self, view):
"""Initialize the Dummy controller.
Parameters
----------
view : DummyView
The view to be controlled by this controller.
Example
-------
>>> controller = DummyController(view)
"""
from navigate.controller.configuration_controller import ConfigurationController
from navigate.controller.sub_controllers import MenuController
from navigate.controller.sub_controllers.multiposition import (
MultiPositionController,
)
from navigate.controller.sub_controllers.channels_tab import (
ChannelsTabController,
)
#: dict: The configuration dictionary.
self.configuration = DummyModel().configuration
#: list: The list of commands.
self.commands = []
#: dict: The custom events
self.event_listeners = {}
self.manager = Manager()
#: DummyView: The view to be controlled by this controller.
self.view = view
#: ConfigurationController: The configuration controller.
self.configuration_controller = ConfigurationController(self.configuration)
#: MenuController: The menu controller.
self.menu_controller = MenuController(view=self.view, parent_controller=self)
#: ChannelsTabController: The channels tab controller.
self.channels_tab_controller = ChannelsTabController(
self.view.settings.channels_tab, self
)
#: MultiPositionController: The multiposition tab controller.
self.multiposition_tab_controller = MultiPositionController(
self.view.settings.multiposition_tab.multipoint_list, self
)
#: dict: The stage positions.
self.stage_pos = {}
#: dict: The stage offset positions.
self.off_stage_pos = {}
base_directory = Path.joinpath(Path(__file__).resolve().parent.parent)
configuration_directory = Path.joinpath(base_directory, "config")
self.waveform_constants_path = Path.joinpath(
configuration_directory, "waveform_constants.yml"
)
#: bool: Flag to indicate if the resize is ready.
self.resize_ready_flag = True
def execute(self, str, *args, sec=None):
"""Execute a command.
Appends commands sent via execute,
first element is oldest command/first to pop off
Parameters
----------
str : str
The command to be executed.
sec : float
The time to wait before executing the command.
Example
-------
>>> controller.execute('move_stage', 1)
"""
self.commands.append(str)
if str in ["move_stage_and_acquire_image", "move_stage_and_update_info"]:
self.commands.append(*args)
if sec is not None:
self.commands.append(sec)
if str == "get_stage_position":
self.stage_pos["x"] = int(random.random())
self.stage_pos["y"] = int(random.random())
return self.stage_pos
def pop(self):
"""Pop the oldest command.
Use this method in testing code to grab the next command.
Returns
-------
str
The oldest command.
Example
-------
>>> controller.pop()
"""
if len(self.commands) > 0:
return self.commands.pop(0)
else:
return "Empty command list"
def clear(self):
"""Clear command list"""
self.commands = []
class DummyModel:
"""Dummy Model - This class is used to test the controller and view."""
def __init__(self):
"""Initialize the Dummy model."""
# Set up the model, experiment, waveform dictionaries
base_directory = Path(__file__).resolve().parent.parent.parent
configuration_directory = Path.joinpath(
base_directory, "src", "navigate", "config"
)
config = Path.joinpath(configuration_directory, "configuration.yaml")
experiment = Path.joinpath(configuration_directory, "experiment.yml")
waveform_constants = Path.joinpath(
configuration_directory, "waveform_constants.yml"
)
gui_configuration_path = Path.joinpath(
configuration_directory, "gui_configuration.yml"
)
multi_positions_path = Path.joinpath(
configuration_directory, "multi_positions.yml"
)
#: Manager: The manager.
self.manager = Manager()
#: dict: The configuration dictionary.
self.configuration = load_configs(
self.manager,
configuration=config,
experiment=experiment,
waveform_constants=waveform_constants,
gui=gui_configuration_path,
)
verify_configuration(self.manager, self.configuration)
verify_experiment_config(self.manager, self.configuration)
verify_waveform_constants(self.manager, self.configuration)
positions = load_yaml_file(multi_positions_path)
positions = verify_positions_config(positions)
self.configuration["multi_positions"] = positions
#: DummyDevice: The device.
self.device = DummyDevice()
#: Pipe: The pipe for sending signals.
self.signal_pipe, self.data_pipe = None, None
#: DummyMicroscope: The microscope.
self.active_microscope = DummyMicroscope(
"Mesoscale", self.configuration, devices_dict={}, is_synthetic=True
)
#: Object: The signal container.
self.signal_container = None
#: Object: The data container.
self.data_container = None
#: Thread: The signal thread.
self.signal_thread = None
#: Thread: The data thread.
self.data_thread = None
#: bool: The flag for stopping the model.
self.stop_flag = False
#: int: The frame id.
self.frame_id = 0 # signal_num
#: list: The list of data.
self.data = []
#: list: The list of signal records.
self.signal_records = []
#: list: The list of data records.
self.data_records = []
#: int: The image width.
self.img_width = int(
self.configuration["experiment"]["CameraParameters"]["x_pixels"]
)
#: int: The image height.
self.img_height = int(
self.configuration["experiment"]["CameraParameters"]["y_pixels"]
)
#: int: The number of frames in the data buffer.
self.number_of_frames = 10
#: ndarray: The data buffer.
self.data_buffer = np.zeros(
(self.number_of_frames, self.img_width, self.img_height)
)
#: ndarray: The data buffer positions.
self.data_buffer_positions = np.zeros(
shape=(self.number_of_frames, 5), dtype=float
) # z-index, x, y, z, theta, f
#: dict: The camera dictionary.
self.camera = {}
#: str: The active microscope name.
self.active_microscope_name = self.configuration["experiment"][
"MicroscopeState"
]["microscope_name"]
for k in self.configuration["configuration"]["microscopes"].keys():
self.camera[k] = SyntheticCamera(
self.active_microscope_name,
SyntheticCameraController(),
self.configuration,
)
self.camera[k].initialize_image_series(
self.data_buffer, self.number_of_frames
)
def signal_func(self):
"""Perform signal-related functionality.
This method is responsible for signal processing operations. It resets the
signal container and continues processing signals until the end flag is set.
During each iteration, it runs the signal container and communicates with
a separate process using a signal pipe. The `frame_id` is incremented after
each signal processing step.
Note
----
- The function utilizes a signal container and a signal pipe for communication.
- It terminates when the `end_flag` is set and sends a "shutdown" signal.
"""
self.signal_container.reset()
while not self.signal_container.end_flag:
if self.signal_container:
self.signal_container.run()
self.signal_pipe.send("signal")
self.signal_pipe.recv()
if self.signal_container:
self.signal_container.run(wait_response=True)
self.frame_id += 1 # signal_num
self.signal_pipe.send("shutdown")
self.stop_flag = True
def data_func(self):
"""The function responsible for sending and processing data.
This method continuously sends data requests using a data pipe and receives
corresponding frame IDs. It appends the received frame IDs to the data storage
and runs data processing operations if a data container is available.
Notes
-----
- The function operates in a loop until the `stop_flag` is set.
- It communicates with a separate process using a data pipe for data retrieval.
- Received frame IDs are appended to the data storage and processed if
applicable.
- The method terminates by sending a "shutdown" signal.
"""
while not self.stop_flag:
self.data_pipe.send("getData")
frame_ids = self.data_pipe.recv()
print("receive: ", frame_ids)
if not frame_ids:
continue
self.data.append(frame_ids)
if self.data_container:
self.data_container.run(frame_ids)
self.data_pipe.send("shutdown")
def start(self, feature_list):
"""Start the model.
Parameters
----------
feature_list : list
The list of features to be used.
Returns
-------
bool
True if the model is started successfully, False otherwise.
Example
-------
>>> model.start(['signal', 'data'])
"""
if feature_list is None:
return False
self.data = []
self.signal_records = []
self.data_records = []
self.stop_flag = False
self.frame_id = 0 # signal_num
self.signal_pipe, self.data_pipe = self.device.setup()
self.signal_container, self.data_container = load_features(self, feature_list)
self.signal_thread = threading.Thread(target=self.signal_func, name="signal")
self.data_thread = threading.Thread(target=self.data_func, name="data")
self.signal_thread.start()
self.data_thread.start()
self.signal_thread.join()
self.stop_flag = True
self.data_thread.join()
return True
class DummyDevice:
"""Dummy Device - class is used to test the controller and view."""
def __init__(self, timecost=0.2):
"""Initialize the Dummy device.
Parameters
----------
timecost : float
The time cost for generating a message.
"""
#: int: The message count.
self.msg_count = mp.Value("i", 0)
#: int: The sendout message count.
self.sendout_msg_count = 0
#: Pipe: The pipe for sending signals.
self.out_port = None
#: Pipe: The pipe for receiving signals.
self.in_port = None
#: float: The time cost for generating a message.
self.timecost = timecost
#: bool: The flag for stopping the device.
self.stop_flag = False
def setup(self):
"""Set up the pipes.
Returns
-------
Pipe
The pipe for sending signals.
Pipe
The pipe for receiving signals.
Example
-------
>>> device.setup()
"""
signalPort, self.in_port = mp.Pipe()
dataPort, self.out_port = mp.Pipe()
in_process = mp.Process(target=self.listen)
out_process = mp.Process(target=self.sendout)
in_process.start()
out_process.start()
self.sendout_msg_count = 0
self.msg_count.value = 0
self.stop_flag = False
return signalPort, dataPort
def generate_message(self):
"""Generate a message.
Example
-------
>>> device.generate_message()
"""
time.sleep(self.timecost)
self.msg_count.value += 1
def clear(self):
"""Clear the pipes.
Example
-------
>>> device.clear()
"""
self.msg_count.value = 0
def listen(self):
"""Listen to the pipe.
Example
-------
>>> device.listen()
"""
while not self.stop_flag:
signal = self.in_port.recv()
if signal == "shutdown":
self.stop_flag = True
self.in_port.close()
break
self.generate_message()
self.in_port.send("done")
def sendout(self, timeout=100):
"""Send out the message.
Parameters
----------
timeout : int
The timeout for sending out the message.
Example
-------
>>> device.sendout()
"""
while not self.stop_flag:
msg = self.out_port.recv()
if msg == "shutdown":
self.out_port.close()
break
c = 0
while self.msg_count.value == self.sendout_msg_count and c < timeout:
time.sleep(0.01)
c += 1
self.out_port.send(
list(range(self.sendout_msg_count, self.msg_count.value))
)
self.sendout_msg_count = self.msg_count.value
class DummyMicroscope:
"""Dummy Microscope - Class is used to test the controller and view."""
def __init__(self, name, configuration, devices_dict, is_synthetic=False):
"""Initialize the Dummy microscope.
Parameters
----------
name : str
The microscope name.
configuration : dict
The configuration dictionary.
devices_dict : dict
The dictionary of devices.
is_synthetic : bool
The flag for using a synthetic microscope.
"""
#: str: The microscope name.
self.microscope_name = name
#: dict: The configuration dictionary.
self.configuration = configuration
#: np.ndarray: The data buffer.
self.data_buffer = None
#: dict: The stage dictionary.
self.stages = {}
#: dict: The lasers dictionary.
self.lasers = {}
#: dict: The galvo dictionary.
self.galvo = {}
#: dict: The DAQ dictionary.
self.daq = devices_dict.get("daq", None)
#: int: The current channel.
self.current_channel = 0
self.camera = SyntheticCamera(
self.configuration["experiment"]["MicroscopeState"]["microscope_name"],
SyntheticCameraController(),
self.configuration,
)
def calculate_exposure_sweep_times(self):
"""Get the exposure and sweep times for all channels.
Returns
-------
dict
The dictionary of exposure times.
dict
The dictionary of sweep times.
"""
exposure_times = {}
sweep_times = {}
microscope_state = self.configuration["experiment"]["MicroscopeState"]
waveform_constants = self.configuration["waveform_constants"]
camera_delay = (
self.configuration["configuration"]["microscopes"][self.microscope_name][
"camera"
]["delay"]
/ 1000
)
camera_settle_duration = (
self.configuration["configuration"]["microscopes"][self.microscope_name][
"camera"
].get("settle_duration", 0)
/ 1000
)
remote_focus_ramp_falling = (
float(waveform_constants["other_constants"]["remote_focus_ramp_falling"])
/ 1000
)
duty_cycle_wait_duration = (
float(waveform_constants["other_constants"]["remote_focus_settle_duration"])
/ 1000
)
ps = float(waveform_constants["other_constants"].get("percent_smoothing", 0.0))
readout_time = 0
readout_mode = self.configuration["experiment"]["CameraParameters"][
"sensor_mode"
]
if readout_mode == "Normal":
readout_time = self.camera.calculate_readout_time()
elif self.configuration["experiment"]["CameraParameters"][
"readout_direction"
] in ["Bidirectional", "Rev. Bidirectional"]:
remote_focus_ramp_falling = 0
# set readout out time
self.configuration["experiment"]["CameraParameters"]["readout_time"] = (
readout_time * 1000
)
for channel_key in microscope_state["channels"].keys():
channel = microscope_state["channels"][channel_key]
if channel["is_selected"] is True:
exposure_time = channel["camera_exposure_time"] / 1000
if readout_mode == "Light-Sheet":
(
_,
_,
updated_exposure_time,
) = self.camera.calculate_light_sheet_exposure_time(
exposure_time,
int(
self.configuration["experiment"]["CameraParameters"][
"number_of_pixels"
]
),
)
if updated_exposure_time != exposure_time:
print(
f"*** Notice: The actual exposure time of the camera for "
f"{channel_key} is {round(updated_exposure_time*1000, 1)}"
f"ms, not {exposure_time*1000}ms!"
)
exposure_time = round(updated_exposure_time, 4)
# update the experiment file
channel["camera_exposure_time"] = round(
updated_exposure_time * 1000, 1
)
self.output_event_queue.put(
(
"exposure_time",
(channel_key, channel["camera_exposure_time"]),
)
)
sweep_time = (
exposure_time
+ readout_time
+ camera_delay
+ max(
remote_focus_ramp_falling + duty_cycle_wait_duration,
camera_settle_duration,
camera_delay,
)
- camera_delay
)
# TODO: should we keep the percent_smoothing?
if ps > 0:
sweep_time = (1 + ps / 100) * sweep_time
exposure_times[channel_key] = exposure_time + readout_time
sweep_times[channel_key] = sweep_time
return exposure_times, sweep_times