From ab20e1c00c46342c5f50792919c3f6e99f1c6b57 Mon Sep 17 00:00:00 2001 From: adamkglaser Date: Sun, 5 Jan 2025 19:09:01 -0800 Subject: [PATCH 1/2] adding docstrings --- examples/base-device/camera_example.py | 59 +- examples/base-device/instrument_example.py | 87 +-- examples/base-device/laser_example.py | 57 +- examples/base-device/schema_example.py | 26 +- examples/base-device/writer_example.py | 39 +- examples/camera_example.py | 59 +- examples/channel_plan_example.py | 88 ++- examples/filter_wheel_example.py | 72 ++- examples/filter_wheel_simulated_example.py | 93 +-- examples/laser_example.py | 43 +- examples/resources/imaris.py | 342 ----------- examples/resources/simulated_camera.py | 197 ------ examples/resources/simulated_laser.py | 144 ----- .../simulated-view.py | 15 +- .../simulated-instrument/simulated-view.py | 96 +-- instruments/speakeasy-view/speakeasy_view.py | 20 +- src/view/__init__.py | 2 +- src/view/acquisition_view.py | 303 ++++----- src/view/instrument_view.py | 321 +++++----- .../channel_plan_widget.py | 360 ++++++----- .../acquisition_widgets/metadata_widget.py | 63 +- .../acquisition_widgets/volume_model.py | 218 ++++--- .../acquisition_widgets/volume_plan_widget.py | 298 +++++---- src/view/widgets/base_device_widget.py | 234 ++++--- .../widgets/device_widgets/camera_widget.py | 128 ++-- .../device_widgets/filter_wheel_widget.py | 238 +++++--- .../widgets/device_widgets/joystick_widget.py | 61 +- .../widgets/device_widgets/laser_widget.py | 77 +-- src/view/widgets/device_widgets/ni_widget.py | 574 +++++++++--------- .../widgets/device_widgets/stage_widget.py | 34 +- .../widgets/device_widgets/waveform_widget.py | 319 ++++++---- .../gl_ortho_view_widget.py | 39 +- .../miscellaneous_widgets/gl_path_item.py | 123 ++-- .../gl_shaded_box_item.py | 139 +++-- .../q_clickable_label.py | 14 +- .../q_dock_widget_title_bar.py | 135 ++-- .../miscellaneous_widgets/q_item_delegates.py | 116 +++- .../q_non_scrollable_tree_widget.py | 12 +- .../q_scrollable_float_slider.py | 74 ++- .../q_scrollable_line_edit.py | 30 +- .../q_start_stop_table_header.py | 56 +- tests/test_acquisition_view.py | 548 +++++++---------- tests/test_base_device_widget.py | 179 +++--- tests/test_camera_widget.py | 120 ++-- tests/test_volume_plan_widget.py | 41 +- 45 files changed, 3136 insertions(+), 3157 deletions(-) delete mode 100644 examples/resources/imaris.py delete mode 100644 examples/resources/simulated_camera.py delete mode 100644 examples/resources/simulated_laser.py diff --git a/examples/base-device/camera_example.py b/examples/base-device/camera_example.py index 2171457..b3f23bc 100644 --- a/examples/base-device/camera_example.py +++ b/examples/base-device/camera_example.py @@ -1,55 +1,64 @@ -from examples.resources.simulated_camera import Camera -from view.widgets.base_device_widget import BaseDeviceWidget -from qtpy.QtWidgets import QApplication import sys -from qtpy.QtCore import Slot import threading from time import sleep +from voxel.devices.camera.simulated import Camera +from qtpy.QtCore import Slot +from qtpy.QtWidgets import QApplication + +from view.widgets.base_device_widget import BaseDeviceWidget + + def scan_for_properties(device): - """Scan for properties with setters and getters in class and return dictionary - :param device: object to scan through for properties - """ + """_summary_ + :param device: _description_ + :type device: _type_ + :return: _description_ + :rtype: _type_ + """ prop_dict = {} for attr_name in dir(device): attr = getattr(type(device), attr_name, None) - if isinstance(attr, property): #and attr.fset is not None: + if isinstance(attr, property): # and attr.fset is not None: prop_dict[attr_name] = getattr(device, attr_name) return prop_dict -def device_change(): - """Simulate changing device properties""" +def device_change(): + """_summary_""" for i in range(0, 100): if i == 25: - print('changing exposure_time_ms') + print("changing exposure_time_ms") base.exposure_time_ms = 2500.0 if i == 50: - print('changing pixel_type') - base.pixel_type = 'mono16' - if i ==75: - print('changing roi') + print("changing pixel_type") + base.pixel_type = "mono16" + if i == 75: + print("changing roi") # Need to change whole dictionary to trigger update. DOES NOT WORK changing one item - base.roi = {'width_px': 1016, 'height_px': 2032, 'width_offset_px': 0, 'height_offest_px': 0} - if i ==99: - print('changing sensor_height_px') - base.sensor_height_px = 10640/2 - sleep(.1) + base.roi = {"width_px": 1016, "height_px": 2032, "width_offset_px": 0, "height_offest_px": 0} + if i == 99: + print("changing sensor_height_px") + base.sensor_height_px = 10640 / 2 + sleep(0.1) @Slot(str) def widget_property_changed(name): - """Slot to signal when widget has been changed - :param name: name of attribute and widget""" + """_summary_ + + :param name: _description_ + :type name: _type_ + """ + name_lst = name.split(".") + print(name, " changed to ", getattr(base, name_lst[0])) - name_lst = name.split('.') - print(name, ' changed to ', getattr(base, name_lst[0])) if __name__ == "__main__": app = QApplication(sys.argv) - simulated_camera = Camera('camera') + simulated_camera = Camera("camera") camera_properties = scan_for_properties(simulated_camera) print(camera_properties) base = BaseDeviceWidget(Camera, camera_properties) diff --git a/examples/base-device/instrument_example.py b/examples/base-device/instrument_example.py index 72a40d5..2e50f33 100644 --- a/examples/base-device/instrument_example.py +++ b/examples/base-device/instrument_example.py @@ -1,23 +1,27 @@ -from view.widgets.base_device_widget import BaseDeviceWidget -from qtpy.QtWidgets import QApplication +import os import sys +from pathlib import Path + from qtpy.QtCore import Slot -from voxel.instruments import Instrument +from qtpy.QtWidgets import QApplication + +from view.widgets.base_device_widget import BaseDeviceWidget from voxel.acquisition import Acquisition -from pathlib import Path -import os +from voxel.instruments import Instrument + +RESOURCES_DIR = Path(os.path.dirname(os.path.realpath(__file__))) / "resources" +ACQUISITION_YAML = RESOURCES_DIR / "test_acquisition.yaml" +INSTRUMENT_YAML = RESOURCES_DIR / "simulated_instrument.yaml" -RESOURCES_DIR = ( - Path(os.path.dirname(os.path.realpath(__file__))) / "resources" -) -ACQUISITION_YAML = RESOURCES_DIR / 'test_acquisition.yaml' -INSTRUMENT_YAML = RESOURCES_DIR / 'simulated_instrument.yaml' def scan_for_properties(device): - """Scan for properties with setters and getters in class and return dictionary - :param device: object to scan through for properties - """ + """_summary_ + :param device: _description_ + :type device: _type_ + :return: _description_ + :rtype: _type_ + """ prop_dict = {} for attr_name in dir(device): attr = getattr(type(device), attr_name, None) @@ -28,38 +32,44 @@ def scan_for_properties(device): def set_up_guis(devices, device_type): + """_summary_ + + :param devices: _description_ + :type devices: _type_ + :param device_type: _description_ + :type device_type: _type_ + :return: _description_ + :rtype: _type_ + """ guis = {} for name, device in devices.items(): properties = scan_for_properties(device) # TODO: better way to find out what module guis[name] = BaseDeviceWidget(type(device), properties) - guis[name].setWindowTitle(f'{device_type} {name}') + guis[name].setWindowTitle(f"{device_type} {name}") guis[name].ValueChangedInside[str].connect( - lambda value, dev=device, widget=guis[name],: widget_property_changed(value, dev, widget)) + lambda value, dev=device, widget=guis[name],: widget_property_changed(value, dev, widget) + ) guis[name].show() return guis @Slot(str) def widget_property_changed(name, device, widget): - """Slot to signal when widget has been changed - :param name: name of attribute and widget""" - - name_lst = name.split('.') - print('widget', name, ' changed to ', getattr(widget, name_lst[0])) + """_summary_ + + :param name: _description_ + :type name: _type_ + :param device: _description_ + :type device: _type_ + :param widget: _description_ + :type widget: _type_ + """ + name_lst = name.split(".") + print("widget", name, " changed to ", getattr(widget, name_lst[0])) value = getattr(widget, name_lst[0]) setattr(device, name_lst[0], value) - # if len(name_lst) == 1: # name refers to attribute - # setattr(device, name, value) - # else: # name is a dictionary and key pair split by . - # dictionary = getattr(device, name_lst[0]) - # # new = {k:getattr(widget, f'{name_lst[0]}.{k}') for k in dictionary.keys()} - # # print(new) - # for k in dictionary.keys(): - # print(k, dir(widget)) - # print(k, getattr(widget, f'{name_lst[0]}.{k}_')) - - print('Device', name, ' changed to ', getattr(device, name_lst[0])) + print("Device", name, " changed to ", getattr(device, name_lst[0])) if __name__ == "__main__": @@ -68,20 +78,11 @@ def widget_property_changed(name, device, widget): # instrument instrument = Instrument(INSTRUMENT_YAML) - laser_ui = set_up_guis(instrument.lasers, 'laser') - combiner_ui = set_up_guis(instrument.combiners, 'combiner') - camera_ui = set_up_guis(instrument.cameras, 'camera') + laser_ui = set_up_guis(instrument.lasers, "laser") + combiner_ui = set_up_guis(instrument.combiners, "combiner") + camera_ui = set_up_guis(instrument.cameras, "camera") # acquisition acquisition = Acquisition(instrument, ACQUISITION_YAML) - # simulated_camera = Camera('camera') - # camera_properties = scan_for_properties(simulated_camera) - # print(camera_properties) - # base = BaseDeviceWidget(Camera, "examples.resources.simulated_camera", camera_properties) - # base.ValueChangedInside[str].connect(widget_property_changed) - - # t1 = threading.Thread(target=device_change, args=()) - # t1.start() - sys.exit(app.exec_()) diff --git a/examples/base-device/laser_example.py b/examples/base-device/laser_example.py index 3e84218..7dfd176 100644 --- a/examples/base-device/laser_example.py +++ b/examples/base-device/laser_example.py @@ -1,55 +1,64 @@ -from examples.resources.simulated_laser import SimulatedLaser -from view.widgets.base_device_widget import BaseDeviceWidget -from qtpy.QtWidgets import QApplication import sys -from qtpy.QtCore import Slot import threading from time import sleep +from voxel.devices.laser.simulated import SimulatedLaser +from qtpy.QtCore import Slot +from qtpy.QtWidgets import QApplication + +from view.widgets.base_device_widget import BaseDeviceWidget + + def scan_for_properties(device): - """Scan for properties with setters and getters in class and return dictionary - :param device: object to scan through for properties - """ + """_summary_ + :param device: _description_ + :type device: _type_ + :return: _description_ + :rtype: _type_ + """ prop_dict = {} for attr_name in dir(device): attr = getattr(type(device), attr_name, None) - if isinstance(attr, property): #and attr.fset is not None: + if isinstance(attr, property): # and attr.fset is not None: prop_dict[attr_name] = getattr(device, attr_name) return prop_dict -def device_change(): - """Simulate changing device properties""" +def device_change(): + """_summary_""" for i in range(0, 100): if i == 25: - print('changing temperature') + print("changing temperature") base.temperature = 25.0 if i == 50: - print('changing cdrh') - base.cdrh = 'OFF' - if i ==75: - print('changing test_property') + print("changing cdrh") + base.cdrh = "OFF" + if i == 75: + print("changing test_property") # Need to change whole dictionary to trigger update. DOES NOT WORK changing one item - base.test_property = {"value0":"internal", "value1":"on"} - if i ==99: - print('changing power') + base.test_property = {"value0": "internal", "value1": "on"} + if i == 99: + print("changing power") base.power_setpoint_mw = 67.0 - sleep(.1) + sleep(0.1) @Slot(str) def widget_property_changed(name): - """Slot to signal when widget has been changed - :param name: name of attribute and widget""" + """_summary_ + + :param name: _description_ + :type name: _type_ + """ + name_lst = name.split(".") + print(name, " changed to ", getattr(base, name_lst[0])) - name_lst = name.split('.') - print(name, ' changed to ', getattr(base, name_lst[0])) if __name__ == "__main__": app = QApplication(sys.argv) - simulated_laser = SimulatedLaser(port='COM3') + simulated_laser = SimulatedLaser(port="COM3") laser_properties = scan_for_properties(simulated_laser) base = BaseDeviceWidget(laser_properties, laser_properties) base.ValueChangedInside[str].connect(widget_property_changed) diff --git a/examples/base-device/schema_example.py b/examples/base-device/schema_example.py index 95e4ccb..28a073a 100644 --- a/examples/base-device/schema_example.py +++ b/examples/base-device/schema_example.py @@ -1,26 +1,26 @@ -from view.widgets.base_device_widget import BaseDeviceWidget -from qtpy.QtWidgets import QApplication import sys -from qtpy.QtCore import Slot + from aind_data_schema.core import acquisition +from qtpy.QtCore import Slot +from qtpy.QtWidgets import QApplication + +from view.widgets.base_device_widget import BaseDeviceWidget @Slot(str) def widget_property_changed(name): - """Slot to signal when widget has been changed - :param name: name of attribute and widget""" + """_summary_ + + :param name: _description_ + :type name: _type_ + """ + name_lst = name.split(".") + print(name, " changed to ", getattr(base, name_lst[0])) - name_lst = name.split('.') - print(name, ' changed to ', getattr(base, name_lst[0])) - # if len(name_lst) == 1: # name refers to attribute - # setattr(writer, name, value) - # else: # name is a dictionary and key pair split by . - # getattr(writer, name_lst[0]).__setitem__(name_lst[1], value) - # print(name, ' changed to ', getattr(writer, name_lst[0])) if __name__ == "__main__": app = QApplication(sys.argv) - acquisition_properties = {k:'' for k in acquisition.Acquisition.model_fields.keys()} + acquisition_properties = {k: "" for k in acquisition.Acquisition.model_fields.keys()} base = BaseDeviceWidget(acquisition.Acquisition.model_fields, acquisition_properties) base.ValueChangedInside[str].connect(widget_property_changed) diff --git a/examples/base-device/writer_example.py b/examples/base-device/writer_example.py index 19030f0..aee6b32 100644 --- a/examples/base-device/writer_example.py +++ b/examples/base-device/writer_example.py @@ -1,15 +1,20 @@ -from examples.resources.imaris import Writer -from view.widgets.base_device_widget import BaseDeviceWidget -from qtpy.QtWidgets import QApplication import sys + +from voxel.writers.imaris import ImarisWriter from qtpy.QtCore import Slot +from qtpy.QtWidgets import QApplication + +from view.widgets.base_device_widget import BaseDeviceWidget def scan_for_properties(device): - """Scan for properties with setters and getters in class and return dictionary - :param device: object to scan through for properties - """ + """_summary_ + :param device: _description_ + :type device: _type_ + :return: _description_ + :rtype: _type_ + """ prop_dict = {} for attr_name in dir(device): attr = getattr(type(device), attr_name, None) @@ -18,30 +23,34 @@ def scan_for_properties(device): return prop_dict + @Slot(str) def widget_property_changed(name): - """Slot to signal when widget has been changed - :param name: name of attribute and widget""" + """_summary_ - name_lst = name.split('.') + :param name: _description_ + :type name: _type_ + """ + name_lst = name.split(".") value = getattr(base, name_lst[0]) if len(name_lst) == 1: # name refers to attribute setattr(writer, name, value) else: # name is a dictionary and key pair split by . getattr(writer, name_lst[0]).__setitem__(name_lst[1], value) - print(name, ' changed to ', getattr(writer, name_lst[0])) + print(name, " changed to ", getattr(writer, name_lst[0])) + if __name__ == "__main__": app = QApplication(sys.argv) - writer = Writer() - writer.compression = 'lz4shuffle' - writer.data_type = 'uint16' - writer.path = r"C:\Users\micah.woodard\Downloads" + writer = ImarisWriter() + writer.compression = "lz4shuffle" + writer.data_type = "uint16" + writer.path = "." writer.color = "#00ff92" writer_properties = scan_for_properties(writer) base = BaseDeviceWidget(Writer, writer_properties) base.ValueChangedInside[str].connect(widget_property_changed) - base.data_type = 'uint8' + base.data_type = "uint8" sys.exit(app.exec_()) diff --git a/examples/camera_example.py b/examples/camera_example.py index 02a8125..b0dfc39 100644 --- a/examples/camera_example.py +++ b/examples/camera_example.py @@ -3,13 +3,16 @@ from qtpy.QtWidgets import QApplication import sys from qtpy.QtCore import Slot -import inspect + def scan_for_properties(device): - """Scan for properties with setters and getters in class and return dictionary - :param device: object to scan through for properties - """ + """_summary_ + :param device: _description_ + :type device: _type_ + :return: _description_ + :rtype: _type_ + """ prop_dict = {} for attr_name in dir(device): attr = getattr(type(device), attr_name, None) @@ -21,46 +24,40 @@ def scan_for_properties(device): @Slot(str) def widget_property_changed(name, device, widget): - """Slot to signal when widget has been changed - :param name: name of attribute and widget""" - - name_lst = name.split('.') - #print('widget', name, ' changed to ', getattr(widget, name_lst[0])) + """_summary_ + + :param name: _description_ + :type name: _type_ + :param device: _description_ + :type device: _type_ + :param widget: _description_ + :type widget: _type_ + """ + name_lst = name.split(".") value = getattr(widget, name_lst[0]) setattr(device, name_lst[0], value) - #print('Device', name, ' changed to ', getattr(device, name_lst[0])) for k, v in widget.property_widgets.items(): instrument_value = getattr(device, k) - # print(k, instrument_value) setattr(widget, k, instrument_value) + if __name__ == "__main__": app = QApplication(sys.argv) - camera_object = Camera('') - props = {'exposure_time_ms': 20.0, - 'pixel_type': 'mono16', - 'width_px': 1152, - 'height_px': 1152, - } + camera_object = Camera("") + props = { + "exposure_time_ms": 20.0, + "pixel_type": "mono16", + "width_px": 1152, + "height_px": 1152, + } for k, v in props.items(): setattr(camera_object, k, v) camera = CameraWidget(camera_object) - camera.setWindowTitle('Camera') + camera.setWindowTitle("Camera") camera.show() camera.ValueChangedInside[str].connect( - lambda value, dev=camera_object, widget=camera,: widget_property_changed(value, dev, widget)) + lambda value, dev=camera_object, widget=camera, : widget_property_changed(value, dev, widget) + ) sys.exit(app.exec_()) - # app = QApplication(sys.argv) - # simulated_camera = Camera('camera') - # camera_properties = scan_for_properties(simulated_camera) - # print(camera_properties) - # base = BaseDeviceWidget(Camera, "examples.resources.simulated_camera", camera_properties) - # base.ValueChangedInside[str].connect(widget_property_changed) - # base.show() - # - # t1 = threading.Thread(target=device_change, args=()) - # t1.start() - # - # sys.exit(app.exec_()) diff --git a/examples/channel_plan_example.py b/examples/channel_plan_example.py index 80841d9..a3eb4ae 100644 --- a/examples/channel_plan_example.py +++ b/examples/channel_plan_example.py @@ -1,82 +1,62 @@ -from view.widgets.acquisition_widgets.channel_plan_widget import ChannelPlanWidget -from qtpy.QtWidgets import QApplication import sys from unittest.mock import MagicMock -from pathlib import Path -import os -from voxel.devices.lasers.simulated import SimulatedLaser -from voxel.devices.stage.simulated import Stage + +from qtpy.QtWidgets import QApplication + +from view.widgets.acquisition_widgets.channel_plan_widget import ChannelPlanWidget from view.widgets.device_widgets.laser_widget import LaserWidget from view.widgets.device_widgets.stage_widget import StageWidget -from threading import Lock +from voxel.devices.laser.simulated import SimulatedLaser +from voxel.devices.stage.simulated import Stage if __name__ == "__main__": app = QApplication(sys.argv) channels = { - '488': { - 'filters': ['BP488'], - 'lasers': ['488nm'], - 'cameras': ['vnp - 604mx', 'vp-151mx']}, - '639': { - 'filters': ['LP638'], - 'lasers': ['639nm'], - 'cameras': ['vnp - 604mx', 'vp-151mx']} + "488": {"filters": ["BP488"], "lasers": ["488nm"], "cameras": ["vnp - 604mx", "vp-151mx"]}, + "639": {"filters": ["LP638"], "lasers": ["639nm"], "cameras": ["vnp - 604mx", "vp-151mx"]}, } properties = { - 'lasers': ['power_setpoint_mw'], - 'focusing_stages': ['position_mm'], - 'start_delay_time': { - 'delegate': 'spin', - 'type': 'float', - 'minimum': 0, - 'initial_value': 15, + "lasers": ["power_setpoint_mw"], + "focusing_stages": ["position_mm"], + "start_delay_time": { + "delegate": "spin", + "type": "float", + "minimum": 0, + "initial_value": 15, + }, + "repeats": { + "delegate": "spin", + "type": "int", + "minimum": 0, }, - 'repeats': { - 'delegate': 'spin', - 'type': 'int', - 'minimum': 0, + "example": { + "delegate": "combo", + "type": "str", + "items": ["this", "is", "an", "example"], + "initial_value": "example", }, - 'example': { - 'delegate': 'combo', - 'type': 'str', - 'items': ['this', 'is', 'an', 'example'], - 'initial_value': 'example' - } } - lasers = { - '488nm': SimulatedLaser(id='hello', wavelength=488), - '639nm': SimulatedLaser(id='there', wavelength=639) - } + lasers = {"488nm": SimulatedLaser(id="hello", wavelength=488), "639nm": SimulatedLaser(id="there", wavelength=639)} - focusing_stages = { - 'n': Stage(hardware_axis='n', instrument_axis='n') - } + focusing_stages = {"n": Stage(hardware_axis="n", instrument_axis="n")} - laser_widgets = { - '488nm': LaserWidget(lasers['488nm']), - '639nm': LaserWidget(lasers['639nm']) - } + laser_widgets = {"488nm": LaserWidget(lasers["488nm"]), "639nm": LaserWidget(lasers["639nm"])} focusing_stage_widgets = { - 'n': StageWidget(focusing_stages['n']), + "n": StageWidget(focusing_stages["n"]), } mocked_instrument = MagicMock() - mocked_instrument.configure_mock( - lasers=lasers, - focusing_stages=focusing_stages) + mocked_instrument.configure_mock(lasers=lasers, focusing_stages=focusing_stages) mocked_instrument_view = MagicMock() - mocked_instrument_view.configure_mock(instrument=mocked_instrument, - laser_widgets=laser_widgets, - focusing_stage_widgets=focusing_stage_widgets - ) + mocked_instrument_view.configure_mock( + instrument=mocked_instrument, laser_widgets=laser_widgets, focusing_stage_widgets=focusing_stage_widgets + ) - plan = ChannelPlanWidget(mocked_instrument_view, - channels, - properties) + plan = ChannelPlanWidget(mocked_instrument_view, channels, properties) plan.show() plan.channelAdded.connect(lambda ch: plan.add_channel_rows(ch, [[0, 0]])) diff --git a/examples/filter_wheel_example.py b/examples/filter_wheel_example.py index 9fa881c..73b9eab 100644 --- a/examples/filter_wheel_example.py +++ b/examples/filter_wheel_example.py @@ -1,51 +1,69 @@ -from view.widgets.device_widgets.filter_wheel_widget import FilterWheelWidget -from qtpy.QtWidgets import QApplication import sys -from voxel.devices.filterwheel.asi import FilterWheel -from voxel.devices.filter.asi import Filter -from time import sleep import threading +from time import sleep + +from qtpy.QtWidgets import QApplication from tigerasi.tiger_controller import TigerController +from view.widgets.device_widgets.filter_wheel_widget import FilterWheelWidget +from voxel.devices.filter.asi import Filter +from voxel.devices.filterwheel.asi import FilterWheel + + def move_filter(): - filter_wheel.filter = 'BP405' + """_summary_""" + filter_wheel.filter = "BP405" sleep(3) - filter_wheel.filter = 'LP638' + filter_wheel.filter = "LP638" sleep(3) BP405_filter.enable() + def widget_property_changed(name, device, widget): - """Slot to signal when widget has been changed - :param name: name of attribute and widget""" + """_summary_ - print('widget property changed') - name_lst = name.split('.') - print('widget', name, ' changed to ', getattr(widget, name_lst[0])) + :param name: _description_ + :type name: _type_ + :param device: _description_ + :type device: _type_ + :param widget: _description_ + :type widget: _type_ + """ + print("widget property changed") + name_lst = name.split(".") + print("widget", name, " changed to ", getattr(widget, name_lst[0])) value = getattr(widget, name_lst[0]) setattr(device, name_lst[0], value) - print('Device', name, ' changed to ', getattr(device, name_lst[0])) + print("Device", name, " changed to ", getattr(device, name_lst[0])) if __name__ == "__main__": app = QApplication(sys.argv) - stage = TigerController('COM4') - filter_wheel = FilterWheel(stage,0, {'BP405': 0, - 'BP488': 1, - 'BP561': 2, - 'LP638': 3, - 'MB405/488/561/638': 4, - 'Empty1': 5, - 'Empty2': 6,}) - - BP405_filter = Filter(filter_wheel, 'BP405') - BP561_filter = Filter(filter_wheel, 'BP561') - LP638_filter = Filter(filter_wheel, 'LP638') - BP488_filter = Filter(filter_wheel, 'BP488') + stage = TigerController("COM4") + filter_wheel = FilterWheel( + stage, + 0, + { + "BP405": 0, + "BP488": 1, + "BP561": 2, + "LP638": 3, + "MB405/488/561/638": 4, + "Empty1": 5, + "Empty2": 6, + }, + ) + BP405_filter = Filter(filter_wheel, "BP405") + BP561_filter = Filter(filter_wheel, "BP561") + LP638_filter = Filter(filter_wheel, "LP638") + BP488_filter = Filter(filter_wheel, "BP488") widget = FilterWheelWidget(filter_wheel) - widget.ValueChangedInside[str].connect(lambda value, dev=filter_wheel, gui=widget: widget_property_changed(value, dev, widget)) + widget.ValueChangedInside[str].connect( + lambda value, dev=filter_wheel, gui=widget: widget_property_changed(value, dev, widget) + ) widget.show() t1 = threading.Thread(target=move_filter) diff --git a/examples/filter_wheel_simulated_example.py b/examples/filter_wheel_simulated_example.py index 32fe941..22bdcbb 100644 --- a/examples/filter_wheel_simulated_example.py +++ b/examples/filter_wheel_simulated_example.py @@ -1,68 +1,79 @@ -from view.widgets.device_widgets.filter_wheel_widget import FilterWheelWidget -from qtpy.QtWidgets import QApplication import sys -from voxel.devices.filterwheel.simulated import FilterWheel -from voxel.devices.filter.simulated import Filter from time import sleep -import threading + +from qtpy.QtWidgets import QApplication + +from view.widgets.device_widgets.filter_wheel_widget import FilterWheelWidget +from voxel.devices.filter.simulated import Filter +from voxel.devices.filterwheel.simulated import FilterWheel def move_filter(): - widget.filter = 'BP561' + """_summary_""" + widget.filter = "BP561" sleep(3) - widget.filter = 'MB405/488/561/638' + widget.filter = "MB405/488/561/638" sleep(3) - filter_wheel.filter = 'BP405' + filter_wheel.filter = "BP405" sleep(3) - filter_wheel.filter = 'LP638' + filter_wheel.filter = "LP638" sleep(3) BP405_filter.enable() def widget_property_changed(name, device, widget): - """Slot to signal when widget has been changed - :param name: name of attribute and widget""" + """_summary_ - name_lst = name.split('.') + :param name: _description_ + :type name: _type_ + :param device: _description_ + :type device: _type_ + :param widget: _description_ + :type widget: _type_ + """ + name_lst = name.split(".") value = getattr(widget, name_lst[0]) setattr(device, name_lst[0], value) - print('Device', name, ' changed to ', getattr(device, name_lst[0])) + print("Device", name, " changed to ", getattr(device, name_lst[0])) if __name__ == "__main__": app = QApplication(sys.argv) - filter_wheel = FilterWheel(0, {'BP405': 0, - 'BP488': 1, - 'BP561': 2, - 'LP638': 3, - 'MB405/488/561/638': 4, - 'Empty1': 5, - 'Empty2': 6, - 'Empty3': 7, - 'Empty4': 8, - }) + filter_wheel = FilterWheel( + 0, + { + "BP405": 0, + "BP488": 1, + "BP561": 2, + "LP638": 3, + "MB405/488/561/638": 4, + "Empty1": 5, + "Empty2": 6, + "Empty3": 7, + "Empty4": 8, + }, + ) - BP405_filter = Filter(filter_wheel, 'BP405') - BP561_filter = Filter(filter_wheel, 'BP561') - LP638_filter = Filter(filter_wheel, 'LP638') - BP488_filter = Filter(filter_wheel, 'BP488') + BP405_filter = Filter(filter_wheel, "BP405") + BP561_filter = Filter(filter_wheel, "BP561") + LP638_filter = Filter(filter_wheel, "LP638") + BP488_filter = Filter(filter_wheel, "BP488") - colors = {'BP405': 'purple', - 'BP488': 'blue', - 'BP561': 'yellowgreen', - 'LP638': 'red', - 'MB405/488/561/638': 'pink', - 'Empty1': 'black', - 'Empty2': 'black', - 'Empty3': 'black', - 'Empty4': 'black', - } + colors = { + "BP405": "purple", + "BP488": "blue", + "BP561": "yellowgreen", + "LP638": "red", + "MB405/488/561/638": "pink", + "Empty1": "black", + "Empty2": "black", + "Empty3": "black", + "Empty4": "black", + } widget = FilterWheelWidget(filter_wheel, colors) widget.ValueChangedInside[str].connect( - lambda value, dev=filter_wheel, widget=widget,: widget_property_changed(value, dev, widget)) + lambda value, dev=filter_wheel, widget=widget, : widget_property_changed(value, dev, widget) + ) widget.show() - # t1 = threading.Thread(target=move_filter) - # t1.start() - sys.exit(app.exec_()) diff --git a/examples/laser_example.py b/examples/laser_example.py index 82b4787..8ba39a5 100644 --- a/examples/laser_example.py +++ b/examples/laser_example.py @@ -1,15 +1,20 @@ -from voxel.devices.laser.simulated import SimulatedLaser -from view.widgets.device_widgets.laser_widget import LaserWidget -from qtpy.QtWidgets import QApplication import sys + from qtpy.QtCore import Slot +from qtpy.QtWidgets import QApplication + +from view.widgets.device_widgets.laser_widget import LaserWidget +from voxel.devices.laser.simulated import SimulatedLaser def scan_for_properties(device): - """Scan for properties with setters and getters in class and return dictionary - :param device: object to scan through for properties - """ + """_summary_ + :param device: _description_ + :type device: _type_ + :return: _description_ + :rtype: _type_ + """ prop_dict = {} for attr_name in dir(device): attr = getattr(type(device), attr_name, None) @@ -21,9 +26,15 @@ def scan_for_properties(device): @Slot(str) def widget_property_changed(name, device, widget): - """Slot to signal when widget has been changed - :param name: name of attribute and widget""" - + """_summary_ + + :param name: _description_ + :type name: _type_ + :param device: _description_ + :type device: _type_ + :param widget: _description_ + :type widget: _type_ + """ name_lst = name.split('.') print('widget', name, ' changed to ', getattr(widget, name_lst[0])) value = getattr(widget, name_lst[0]) @@ -42,18 +53,6 @@ def widget_property_changed(name, device, widget): laser.show() laser.ValueChangedInside[str].connect( - lambda value, dev=laser_object, widget=laser,: widget_property_changed(value, dev, widget)) + lambda value, dev=laser_object, widget=laser, : widget_property_changed(value, dev, widget)) laser.setWindowTitle('Laser') sys.exit(app.exec_()) - # app = QApplication(sys.argv) - # simulated_camera = Camera('camera') - # camera_properties = scan_for_properties(simulated_camera) - # print(camera_properties) - # base = BaseDeviceWidget(Camera, "examples.resources.simulated_camera", camera_properties) - # base.ValueChangedInside[str].connect(widget_property_changed) - # base.show() - # - # t1 = threading.Thread(target=device_change, args=()) - # t1.start() - # - # sys.exit(app.exec_()) diff --git a/examples/resources/imaris.py b/examples/resources/imaris.py deleted file mode 100644 index 10d780d..0000000 --- a/examples/resources/imaris.py +++ /dev/null @@ -1,342 +0,0 @@ -import numpy as np -import logging -import multiprocessing -import re -import os -import sys -from multiprocessing import Process, Array, Event -from multiprocessing.shared_memory import SharedMemory -from ctypes import c_wchar -from PyImarisWriter import PyImarisWriter as pw -from pathlib import Path -from datetime import datetime -from matplotlib.colors import hex2color -from time import sleep, perf_counter -from math import ceil - -CHUNK_SIZE = 64 - -COMPRESSION_TYPES = { - "none": pw.eCompressionAlgorithmShuffleLZ4, - "lz4shuffle": pw.eCompressionAlgorithmNone, -} - -DATA_TYPES = { - "uint8": "uint8", - "uint16": "uint16", -} - -class ImarisProgressChecker(pw.CallbackClass): - """Class for tracking progress of an active ImarisWriter disk-writing - operation.""" - - def __init__(self): - self.progress = 0 # a float representing the progress (0 to 1.0). - - def RecordProgress(self, progress, total_bytes_written): - self.progress = progress - -class Writer: - - def __init__(self): - - self.log = logging.getLogger(f"{__name__}.{self.__class__.__name__}") - - # Opinioated decision on chunking dimension order - self.chunk_dim_order = ('z', 'y', 'x') - # Flow control attributes to synchronize inter-process communication. - self.done_reading = Event() - self.done_reading.set() # Set after processing all data in shared mem. - # Internal flow control attributes to monitor compression progress. - self.callback_class = ImarisProgressChecker() - - @property - def x_voxel_size(self): - return self.pixel_x_size_um - - @x_voxel_size.setter - def x_voxel_size(self, x_voxel_size: float): - self.log.info(f'setting x voxel size to: {x_voxel_size} [um]') - self.pixel_x_size_um = x_voxel_size - - @property - def y_voxel_size(self): - return self.pixel_y_size_um - - @y_voxel_size.setter - def y_voxel_size(self, y_voxel_size: float): - self.log.info(f'setting y voxel size to: {y_voxel_size} [um]') - self.pixel_y_size_um = y_voxel_size - - @property - def z_voxel_size(self): - return self.pixel_z_size_um - - @z_voxel_size.setter - def z_voxel_size(self, z_voxel_size: float): - self.log.info(f'setting z voxel size to: {z_voxel_size} [um]') - self.pixel_z_size_um = z_voxel_size - - @property - def x_pos_mm(self): - return self.first_img_centroid_x_um - - @x_pos_mm.setter - def x_pos_mm(self, x_pos_mm: float): - self.log.info(f'setting x position to: {x_pos_mm} [mm]') - self.first_img_centroid_x_um = x_pos_mm*1000 - - @property - def y_pos_mm(self): - return self.first_img_centroid_y_um - - @y_pos_mm.setter - def y_pos_mm(self, y_pos_mm: float): - self.log.info(f'setting y position to: {y_pos_mm} [mm]') - self.first_img_centroid_y_um = y_pos_mm*1000 - - @property - def z_pos_mm(self): - return self.first_img_centroid_z_um - - @z_pos_mm.setter - def z_pos_mm(self, z_pos_mm: float): - self.log.info(f'setting z position to: {z_pos_mm} [mm]') - self.first_img_centroid_z_um = z_pos_mm*1000 - - @property - def frame_count(self): - return self.img_count - - @frame_count.setter - def frame_count(self, frame_count: int): - self.log.info(f'setting frame count to: {frame_count} [px]') - self.img_count = frame_count - - @property - def column_count(self): - return self.cols - - @column_count.setter - def column_count(self, column_count: int): - self.log.info(f'setting column count to: {column_count} [px]') - self.cols = column_count - - @property - def row_count(self): - return self.rows - - @row_count.setter - def row_count(self, row_count: int): - self.log.info(f'setting row count to: {row_count} [px]') - self.rows = row_count - - @property - def chunk_count(self): - return CHUNK_SIZE - - @property - def compression(self): - return next(key for key, value in COMPRESSION_TYPES.items() if value == self.compression_style) - - @compression.setter - def compression(self, compression: str): - valid = list(COMPRESSION_TYPES.keys()) - if compression not in valid: - raise ValueError("compression type must be one of %r." % valid) - self.log.info(f'setting compression mode to: {compression}') - self.compression_style = COMPRESSION_TYPES[compression] - - @property - def data_type(self): - return self.dtype - - @data_type.setter - def data_type(self, data_type: np.unsignedinteger): - self.log.info(f'setting data type to: {data_type}') - self.dtype = data_type - - @property - def path(self): - return self.dest_path - - @path.setter - def path(self, path: Path or str): - if os.path.isdir(path): - self.dest_path = Path(path) - else: - raise ValueError("%r is not a valid path." % path) - self.log.info(f'setting path to: {path}') - - @property - def filename(self): - return self.stack_name - - @filename.setter - def filename(self, filename: str): - self.stack_name = filename \ - if filename.endswith(".ims") else f"{filename}.ims" - self.log.info(f'setting filename to: {filename}') - - @property - def channel(self): - return self.channel_name - - @channel.setter - def channel(self, channel: str): - self.log.info(f'setting channel name to: {channel}') - self.channel_name = channel - - @property - def color(self): - return self.viz_color_hex - - @color.setter - def color(self, color: str): - if re.search(r'^#(?:[0-9a-fA-F]{3}){1,2}$', color): - self.viz_color_hex = color - else: - raise ValueError("%r is not a valid hex color code." % color) - self.log.info(f'setting color to: {color}') - - @property - def shm_name(self): - """Convenience getter to extract the shared memory address (string) - from the c array.""" - return str(self._shm_name[:]).split('\x00')[0] - - @shm_name.setter - def shm_name(self, name: str): - """Convenience setter to set the string value within the c array.""" - for i, c in enumerate(name): - self._shm_name[i] = c - self._shm_name[len(name)] = '\x00' # Null terminate the string. - self.log.info(f'setting shared memory to: {name}') - - def prepare(self): - self.p = Process(target=self._run) - # Specs for reconstructing the shared memory object. - self._shm_name = Array(c_wchar, 32) # hidden and exposed via property. - # This is almost always going to be: (chunk_size, rows, columns). - chunk_shape_map = {'x': self.cols, - 'y': self.rows, - 'z': CHUNK_SIZE} - self.shm_shape = [chunk_shape_map[x] for x in self.chunk_dim_order] - self.shm_nbytes = \ - int(np.prod(self.shm_shape, dtype=np.int64)*np.dtype(DATA_TYPES[self.dtype]).itemsize) - self.log.info(f"{self.stack_name}: intializing writer.") - self.application_name = 'PyImarisWriter' - self.application_version = '1.0.0' - # voxel size metadata to create the converter - self.image_size = pw.ImageSize(x=self.cols, y=self.rows, z=self.img_count, - c=1, t=1) - self.block_size = pw.ImageSize(x=self.cols, y=self.rows, z=CHUNK_SIZE, - c=1, t=1) - self.sample_size = pw.ImageSize(x=1, y=1, z=1, c=1, t=1) - # compute the start/end extremes of the enclosed rectangular solid. - # (x0, y0, z0) position (in [um]) of the beginning of the first voxel, - # (xf, yf, zf) position (in [um]) of the end of the last voxel. - x0 = self.first_img_centroid_x_um - (self.pixel_x_size_um * 0.5 * self.cols) - y0 = self.first_img_centroid_y_um - (self.pixel_y_size_um * 0.5 * self.rows) - z0 = self.first_img_centroid_z_um - xf = self.first_img_centroid_x_um + (self.pixel_x_size_um * 0.5 * self.cols) - yf = self.first_img_centroid_y_um + (self.pixel_y_size_um * 0.5 * self.rows) - zf = self.first_img_centroid_z_um + self.img_count * self.pixel_z_size_um - self.image_extents = pw.ImageExtents(-x0, -y0, -z0, -xf, -yf, -zf) - # c = channel, t = time. These fields are unused for now. - # Note: ImarisWriter performs MUCH faster when the dimension sequence - # is arranged: x, y, z, c, t. - # It is more efficient to transpose/reshape the data into this - # shape beforehand instead of defining an arbitrary - # DimensionSequence and passing the chunk data in as-is. - self.chunk_dim_order = ('z', 'y', 'x') - self.dimension_sequence = pw.DimensionSequence('x', 'y', 'z', 'c', 't') - # lookups for deducing order - self.dim_map = {'x': 0, 'y': 1, 'z': 2, 'c': 3, 't': 4} - # name parameters - self.parameters = pw.Parameters() - self.parameters.set_channel_name(0, self.channel_name) - # create options object - self.opts = pw.Options() - self.opts.mEnableLogProgress = True - # set threads to double number of cores - self.thread_count = 2*multiprocessing.cpu_count() - self.opts.mNumberOfThreads = self.thread_count - # set compression type - if self.compression_style == 'lz4shuffle': - self.opts.mCompressionAlgorithmType = pw.eCompressionAlgorithmShuffleLZ4 - elif self.compression_style == 'none': - self.opts.mCompressionAlgorithmType = pw.eCompressionAlgorithmNone - # color parameters - self.adjust_color_range = False - self.color_infos = [pw.ColorInfo()] - self.color_infos[0].set_base_color(pw.Color(*(*hex2color(self.viz_color_hex), 1.0))) - # date time parameters - self.time_infos = [datetime.today()] - - def start(self): - self.log.info(f"{self.stack_name}: starting writer.") - self.p.start() - - def _run(self): - """Loop to wait for data from a specified location and write it to disk - as an Imaris file. Close up the file afterwards. - - This function executes when called with the start() method. - """ - # internal logger for process - logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") - fmt = '%(asctime)s.%(msecs)03d %(levelname)s %(name)s: %(message)s' - datefmt = '%Y-%m-%d,%H:%M:%S' - log_formatter = logging.Formatter(fmt=fmt, datefmt=datefmt) - log_handler = logging.StreamHandler(sys.stdout) - log_handler.setFormatter(log_formatter) - logger.addHandler(log_handler) - - filepath = str((self.dest_path/Path(f"{self.stack_name}")).absolute()) - converter = \ - pw.ImageConverter(DATA_TYPES[self.dtype], self.image_size, self.sample_size, - self.dimension_sequence, self.block_size, filepath, - self.opts, self.application_name, - self.application_version, self.callback_class) - chunk_count = ceil(self.img_count/CHUNK_SIZE) - for chunk_num in range(chunk_count): - block_index = pw.ImageSize(x=0, y=0, z=chunk_num, c=0, t=0) - # Wait for new data. - while self.done_reading.is_set(): - print('in imaris writer while self.done_reading.is_set(): ' , self.done_reading.is_set()) - sleep(0.001) - # Attach a reference to the data from shared memory. - shm = SharedMemory(self.shm_name, create=False, size=self.shm_nbytes) - frames = np.ndarray(self.shm_shape, DATA_TYPES[self.dtype], buffer=shm.buf) - logger.warning(f"{self.stack_name}: writing chunk " - f"{chunk_num+1}/{chunk_count} of size {frames.shape}.") - start_time = perf_counter() - dim_order = [self.dim_map[x] for x in self.chunk_dim_order] - # Put the frames back into x, y, z, c, t order. - converter.CopyBlock(frames.transpose(dim_order), block_index) - frames = None - logger.warning(f"{self.stack_name}: writing chunk took " - f"{perf_counter() - start_time:.3f} [s]") - shm.close() - self.done_reading.set() - - # Wait for file writing to finish. - if self.callback_class.progress < 1.0: - logger.warning(f"{self.stack_name}: waiting for data writing to complete for " - f"{self.stack_name}. " - f"current progress is {100*self.callback_class.progress:.1f}%.") - while self.callback_class.progress < 1.0: - print('in imaris writer while self.callback_class.progress < 1.0: ', self.callback_class.progress) - sleep(0.5) - logger.warning(f"{self.stack_name}: waiting for data writing to complete for " - f"{self.stack_name}. " - f"current progress is {100*self.callback_class.progress:.1f}%.") - - converter.Finish(self.image_extents, self.parameters, self.time_infos, - self.color_infos, self.adjust_color_range) - converter.Destroy() - - def wait_to_finish(self): - self.log.info(f"{self.stack_name}: waiting to finish.") - self.p.join() \ No newline at end of file diff --git a/examples/resources/simulated_camera.py b/examples/resources/simulated_camera.py deleted file mode 100644 index 9a8909b..0000000 --- a/examples/resources/simulated_camera.py +++ /dev/null @@ -1,197 +0,0 @@ -import logging -import numpy -import time -from multiprocessing import Process -from threading import Thread - -# constants for VP-151MX camera -BUFFER_SIZE_FRAMES = 8 -MIN_WIDTH_PX = 64 -MAX_WIDTH_PX = 14192 -DIVISIBLE_WIDTH_PX = 16 -MIN_HEIGHT_PX = 2 -MAX_HEIGHT_PX = 10640 -DIVISIBLE_HEIGHT_PX = 1 -MIN_EXPOSURE_TIME_MS = 0.001 -MAX_EXPOSURE_TIME_MS = 6e4 - -PIXEL_TYPES = { - "mono8": "uint8", - "mono16": "uint16" -} - -LINE_INTERVALS_US = { - "mono8": 15.00, - "mono16": 45.44 -} - -class Camera: - - def __init__(self, id): - - self.log = logging.getLogger(f"{__name__}.{self.__class__.__name__}") - self.id = id - self.simulated_pixel_type = "mono8" - self.simulated_line_interval_us = 10 - self.simulated_width_px = 2032 - self.simulated_height_px = 2032 - self.simulated_width_offset_px = 0 - self.simulated_height_offset_px = 0 - self.simulated_exposure_time_ms = 1000 - - @property - def exposure_time_ms(self): - return self.simulated_exposure_time_ms - - @exposure_time_ms.setter - def exposure_time_ms(self, exposure_time_ms: float): - - if exposure_time_ms < MIN_EXPOSURE_TIME_MS or \ - exposure_time_ms > MAX_EXPOSURE_TIME_MS: - self.log.error(f"exposure time must be >{MIN_EXPOSURE_TIME_MS} ms \ - and <{MAX_EXPOSURE_TIME_MS} ms") - raise ValueError(f"exposure time must be >{MIN_EXPOSURE_TIME_MS} ms \ - and <{MAX_EXPOSURE_TIME_MS} ms") - - # Note: round ms to nearest us - self.simulated_exposure_time_ms = exposure_time_ms - self.log.info(f"exposure time set to: {exposure_time_ms} ms") - - @property - def roi(self): - return {'width_px': self.simulated_width_px, - 'height_px': self.simulated_height_px, - 'width_offset_px': self.simulated_width_offset_px, - 'height_offest_px': self.simulated_height_offset_px} - - @roi.setter - def roi(self, roi: dict): - - width_px = roi['width_px'] - height_px = roi['height_px'] - - sensor_height_px = MAX_HEIGHT_PX - sensor_width_px = MAX_WIDTH_PX - - if height_px < MIN_WIDTH_PX or \ - (height_px % DIVISIBLE_HEIGHT_PX) != 0 or \ - height_px > MAX_HEIGHT_PX: - self.log.error(f"Height must be >{MIN_HEIGHT_PX} px, \ - <{MAX_HEIGHT_PX} px, \ - and a multiple of {DIVISIBLE_HEIGHT_PX} px!") - raise ValueError(f"Height must be >{MIN_HEIGHT_PX} px, \ - <{MAX_HEIGHT_PX} px, \ - and a multiple of {DIVISIBLE_HEIGHT_PX} px!") - - if width_px < MIN_WIDTH_PX or \ - (width_px % DIVISIBLE_WIDTH_PX) != 0 or \ - width_px > MAX_WIDTH_PX: - self.log.error(f"Width must be >{MIN_WIDTH_PX} px, \ - <{MAX_WIDTH_PX}, \ - and a multiple of {DIVISIBLE_WIDTH_PX} px!") - raise ValueError(f"Width must be >{MIN_WIDTH_PX} px, \ - <{MAX_WIDTH_PX}, \ - and a multiple of {DIVISIBLE_WIDTH_PX} px!") - - # width offset must be a multiple of the divisible width in px - centered_width_offset_px = round((sensor_width_px/2 - width_px/2)/DIVISIBLE_WIDTH_PX)*DIVISIBLE_WIDTH_PX - # Height offset must be a multiple of the divisible height in px - centered_height_offset_px = round((sensor_height_px/2 - height_px/2)/DIVISIBLE_HEIGHT_PX)*DIVISIBLE_HEIGHT_PX - - self.simulated_width_px = width_px - self.simulated_height_px = height_px - self.simulated_width_offset_px = centered_width_offset_px - self.simulated_height_offset_px = centered_height_offset_px - self.log.info(f"roi set to: {width_px} x {height_px} [width x height]") - self.log.info(f"roi offset set to: {centered_width_offset_px} x {centered_height_offset_px} [width x height]") - - @property - def pixel_type(self): - pixel_type = self.simulated_pixel_type - # invert the dictionary and find the abstracted key to output - #return next(key for key, value in PIXEL_TYPES.items() if value == pixel_type) - return pixel_type - - @pixel_type.setter - def pixel_type(self, pixel_type_bits: str): - valid = list(PIXEL_TYPES.keys()) - if pixel_type_bits not in valid: - raise ValueError("pixel_type_bits must be one of %r." % valid) - self.simulated_pixel_type = pixel_type_bits - # self.simulated_pixel_type = PIXEL_TYPES[pixel_type_bits] - #self.simulated_line_interval_us = pixel_type_bits - # self.log.info(f"pixel type set_to: {pixel_type_bits}") - - @property - def line_interval_us(self): - return self.simulated_line_interval_us - - @property - def sensor_width_px(self): - return MAX_WIDTH_PX - - @property - def sensor_height_px(self): - return MAX_HEIGHT_PX - - def prepare(self): - self.log.info('simulated camera preparing...') - self.buffer = list() - - def start(self, frame_count: int, live: bool = False): - self.log.info('simulated camera starting...') - self.thread = Thread(target=self.generate_frames, args=(frame_count,)) - self.thread.daemon = True - self.thread.start() - - def stop(self): - self.log.info('simulated camera stopping...') - self.thread.join() - - def grab_frame(self): - while not self.buffer: - time.sleep(0.01) - image = self.buffer.pop(0) - return image - - def get_camera_acquisition_state(self): - """return a dict with the state of the acquisition buffers""" - # Detailed description of constants here: - # https://documentation.euresys.com/Products/Coaxlink/Coaxlink/en-us/Content/IOdoc/egrabber-reference/ - # namespace_gen_t_l.html#a6b498d9a4c08dea2c44566722699706e - state = {} - state['frame_index'] = self.frame - state['in_buffer_size'] = len(self.buffer) - state['out_buffer_size'] = BUFFER_SIZE_FRAMES - len(self.buffer) - # number of underrun, i.e. dropped frames - state['dropped_frames'] = self.dropped_frames - state['data_rate'] = self.frame_rate*self.simulated_width_px*self.simulated_height_px*numpy.dtype(self.simulated_pixel_type).itemsize/1e6 - state['frame_rate'] = self.frame_rate - self.log.info(f"id: {self.id}, " - f"frame: {state['frame_index']}, " - f"input: {state['in_buffer_size']}, " - f"output: {state['out_buffer_size']}, " - f"dropped: {state['dropped_frames']}, " - f"data rate: {state['data_rate']:.2f} [MB/s], " - f"frame rate: {state['frame_rate']:.2f} [fps].") - - def generate_frames(self, frame_count: int): - self.frame = 0 - self.dropped_frames = 0 - while self.frame < frame_count: - start_time = time.time() - column_count = self.simulated_width_px - row_count = self.simulated_height_px - frame_time_s = (row_count*self.simulated_line_interval_us/1000+self.simulated_exposure_time_ms)/1000 - # image = numpy.random.randint(low=128, high=256, size=(row_count, column_count), dtype=self.simulated_pixel_type) - image = numpy.zeros(shape=(row_count, column_count), dtype=self.simulated_pixel_type) - while (time.time() - start_time) < frame_time_s: - time.sleep(0.01) - if len(self.buffer) < BUFFER_SIZE_FRAMES: - self.buffer.append(image) - else: - self.dropped_frames += 1 - self.log.warning('buffer full, frame dropped.') - self.frame += 1 - end_time = time.time() - self.frame_rate = 1/(end_time - start_time) diff --git a/examples/resources/simulated_laser.py b/examples/resources/simulated_laser.py deleted file mode 100644 index 5d0cb25..0000000 --- a/examples/resources/simulated_laser.py +++ /dev/null @@ -1,144 +0,0 @@ -import logging -from sympy import symbols, solve -from serial import Serial -from enum import Enum -import sys - -# Define StrEnums if they don't yet exist. -if sys.version_info < (3, 11): - class StrEnum(str, Enum): - pass -else: - from enum import StrEnum - - -class BoolVal(StrEnum): - OFF = "0" - ON = "1" - - -MODULATION_MODES = { - 'off': {'external_control_mode': 'OFF', 'digital_modulation': 'OFF'}, - 'analog': {'external_control_mode': 'ON', 'digital_modulation': 'OFF'}, - 'digital': {'external_control_mode': 'OFF', 'digital_modulation': 'ON'} -} -TEST_PROPERTY = { - "value0": { - "internal": None, - "external": 0, - }, - "value1": { - "on": True, - "off": False, - } -} - - -class SimulatedCombiner: - - def __init__(self, port): - """Class for the L6CC oxxius combiner. This combiner can have LBX lasers or LCX""" - - self.ser = Serial - self.log = logging.getLogger(f"{__name__}.{self.__class__.__name__}") - self._PercentageSplitStatus = 0 - - @property - def percentage_split(self): - """Set percentage split of lasers""" - - return self._PercentageSplitStatus - - @percentage_split.setter - def percentage_split(self, value): - """Get percentage split of lasers""" - if value > 100 or value < 0: - self.log.error(f'Impossible to set percentage spilt to {value}') - return - self._PercentageSplitStatus = value - - -class SimulatedLaser: - - def __init__(self, port: Serial or str, prefix: str = '', coefficients: dict = {}): - """Communicate with specific LBX laser in L6CC Combiner box. - - :param port: comm port for lasers. - :param prefix: prefix specic to laser. - """ - - self.log = logging.getLogger(f"{__name__}.{self.__class__.__name__}") - self.prefix = prefix - self.ser = Serial - self._simulated_power_setpoint_m = 10.0 - self._max_power_mw = 100.0 - self._modulation_mode = 'digital' - self._temperature = 20.0 - self._cdrh = BoolVal.ON - self._test_property = {"value0":"external", "value1":"on"} - - @property - def power_setpoint_mw(self): - """Power of laser in mw""" - return self._simulated_power_setpoint_m - - @power_setpoint_mw.setter - def power_setpoint_mw(self, value: float): - self._simulated_power_setpoint_m = value - - @property - def max_power_mw(self): - """Maximum power of laser in mw""" - return self._max_power_mw - - @property - def modulation_mode(self): - """Modulation mode of laser""" - return self._modulation_mode - - @modulation_mode.setter - def modulation_mode(self, value: str): - if value not in MODULATION_MODES.keys(): - raise ValueError("mode must be one of %r." % MODULATION_MODES.keys()) - for attribute, state in MODULATION_MODES[value].items(): - setattr(self, attribute, state) - - @property - def temperature(self): - """Temperature of laser in Celsius""" - return self._temperature - - def status(self): - return [] - - @property - def cdrh(self): - """Status of five-second safety delay""" - return self._cdrh - - @cdrh.setter - def cdrh(self, value: BoolVal or str): - self._cdrh = value - - @property - def test_property(self): - """Test property used for UI construction""" - return self._test_property - - @test_property.setter - def test_property(self, value: dict): - - value0 = value['value0'] - value1 = value['value1'] - - if value0 not in TEST_PROPERTY['value0'].keys(): - raise ValueError("mode must be one of %r." % TEST_PROPERTY['value0'].keys()) - if value1 not in TEST_PROPERTY['value1'].keys(): - raise ValueError("mode must be one of %r." % TEST_PROPERTY['value1'].keys()) - self._test_property = value - - def enable(self): - pass - - def disable(self): - pass diff --git a/instruments/simulated-instrument-z-y-x-coordinate/simulated-view.py b/instruments/simulated-instrument-z-y-x-coordinate/simulated-view.py index 89ceb78..a0772fc 100644 --- a/instruments/simulated-instrument-z-y-x-coordinate/simulated-view.py +++ b/instruments/simulated-instrument-z-y-x-coordinate/simulated-view.py @@ -1,14 +1,13 @@ -from qtpy.QtWidgets import QApplication, QMessageBox, QPushButton, QFileDialog +import os import sys -from view.instrument_view import InstrumentView +from pathlib import Path + +from qtpy.QtWidgets import QApplication + from view.acquisition_view import AcquisitionView -from voxel.instruments.instrument import Instrument +from view.instrument_view import InstrumentView from voxel.acquisition.acquisition import Acquisition -from pathlib import Path -import os -import yaml -import inflection -from qtpy.QtCore import Qt +from voxel.instruments.instrument import Instrument RESOURCES_DIR = (Path(os.path.dirname(os.path.realpath(__file__)))) ACQUISITION_YAML = RESOURCES_DIR / 'test_acquisition.yaml' diff --git a/instruments/simulated-instrument/simulated-view.py b/instruments/simulated-instrument/simulated-view.py index e99dbd3..d67a077 100644 --- a/instruments/simulated-instrument/simulated-view.py +++ b/instruments/simulated-instrument/simulated-view.py @@ -1,61 +1,78 @@ -from qtpy.QtWidgets import QApplication, QMessageBox, QPushButton, QFileDialog -import sys -from view.instrument_view import InstrumentView -from view.acquisition_view import AcquisitionView -from exaspim_control.exa_spim_instrument import ExASPIM -from exaspim_control.exa_spim_acquisition import ExASPIMAcquisition -from pathlib import Path import os -import inflection -from ruamel.yaml import YAML -import numpy as np +import sys from pathlib import Path, WindowsPath -RESOURCES_DIR = (Path(os.path.dirname(os.path.realpath(__file__)))) -ACQUISITION_YAML = RESOURCES_DIR / 'acquisition.yaml' -INSTRUMENT_YAML = RESOURCES_DIR / 'instrument.yaml' -GUI_YAML = RESOURCES_DIR / 'gui_config.yaml' +import inflection +import numpy as np +from exaspim_control.exa_spim_acquisition import ExASPIMAcquisition +from exaspim_control.exa_spim_instrument import ExASPIM +from qtpy.QtWidgets import QApplication, QFileDialog, QMessageBox, QPushButton +from ruamel.yaml import YAML +from view.acquisition_view import AcquisitionView +from view.instrument_view import InstrumentView -class SimulatedInstrumentView(InstrumentView): - """View for ExASPIM Instrument""" +RESOURCES_DIR = Path(os.path.dirname(os.path.realpath(__file__))) +ACQUISITION_YAML = RESOURCES_DIR / "acquisition.yaml" +INSTRUMENT_YAML = RESOURCES_DIR / "instrument.yaml" +GUI_YAML = RESOURCES_DIR / "gui_config.yaml" - def __init__(self, instrument, config_path: Path, log_level='INFO'): +class SimulatedInstrumentView(InstrumentView): + """_summary_""" + + def __init__(self, instrument, config_path: Path, log_level="INFO"): + """_summary_ + + :param instrument: _description_ + :type instrument: _type_ + :param config_path: _description_ + :type config_path: Path + :param log_level: _description_, defaults to 'INFO' + :type log_level: str, optional + """ super().__init__(instrument, config_path, log_level) app.aboutToQuit.connect(self.update_config_on_quit) self.config_save_to = self.instrument.config_path def update_config_on_quit(self): - """Add functionality to close function to save device properties to instrument config""" - + """_summary_""" return_value = self.update_config_query() if return_value == QMessageBox.Ok: self.instrument.update_current_state_config() self.instrument.save_config(self.config_save_to) def update_config(self, device_name, device_specs): - """Update setting in instrument config if already there - :param device_name: name of device - :param device_specs: dictionary dictating how device should be set up""" - - device_type = inflection.pluralize(device_specs['type']) - for key in device_specs.get('settings', {}).keys(): + """_summary_ + + :param device_name: _description_ + :type device_name: _type_ + :param device_specs: _description_ + :type device_specs: _type_ + """ + device_type = inflection.pluralize(device_specs["type"]) + for key in device_specs.get("settings", {}).keys(): device_object = getattr(self.instrument, device_type)[device_name] - device_specs.get('settings')[key] = getattr(device_object, key) - for subdevice_name, subdevice_specs in device_specs.get('subdevices', {}).items(): + device_specs.get("settings")[key] = getattr(device_object, key) + for subdevice_name, subdevice_specs in device_specs.get("subdevices", {}).items(): self.update_config(subdevice_name, subdevice_specs) def update_config_query(self): - """Pop up message asking if configuration would like to be saved""" + """_summary_ + + :return: _description_ + :rtype: _type_ + """ msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Question) - msgBox.setText(f"Do you want to update the instrument configuration file at {self.config_save_to} " - f"to current instrument state?") + msgBox.setText( + f"Do you want to update the instrument configuration file at {self.config_save_to} " + f"to current instrument state?" + ) msgBox.setWindowTitle("Updating Configuration") msgBox.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) - save_elsewhere = QPushButton('Change Directory') + save_elsewhere = QPushButton("Change Directory") msgBox.addButton(save_elsewhere, QMessageBox.DestructiveRole) save_elsewhere.pressed.connect(lambda: self.select_directory(True, msgBox)) @@ -63,13 +80,20 @@ def update_config_query(self): return msgBox.exec() def select_directory(self, pressed, msgBox): - """Select directory""" + """_summary_ + :param pressed: _description_ + :type pressed: _type_ + :param msgBox: _description_ + :type msgBox: _type_ + """ fname = QFileDialog() folder = fname.getSaveFileName(directory=str(self.instrument.config_path)) - if folder[0] != '': # user pressed cancel - msgBox.setText(f"Do you want to update the instrument configuration file at {folder[0]} " - f"to current instrument state?") + if folder[0] != "": # user pressed cancel + msgBox.setText( + f"Do you want to update the instrument configuration file at {folder[0]} " + f"to current instrument state?" + ) self.config_save_to = folder[0] @@ -91,4 +115,4 @@ def select_directory(self, pressed, msgBox): instrument_view = SimulatedInstrumentView(instrument, GUI_YAML) acquisition_view = AcquisitionView(acquisition, instrument_view) - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) diff --git a/instruments/speakeasy-view/speakeasy_view.py b/instruments/speakeasy-view/speakeasy_view.py index 81aac55..a89e4fe 100644 --- a/instruments/speakeasy-view/speakeasy_view.py +++ b/instruments/speakeasy-view/speakeasy_view.py @@ -1,15 +1,13 @@ -from qtpy.QtWidgets import QApplication +import os import sys -from qtpy.QtCore import Slot -import threading -from time import sleep -from view.instrument_view import InstrumentView -from voxel.instruments.instrument import Instrument +from pathlib import Path + +from qtpy.QtWidgets import QApplication + from view.acquisition_view import AcquisitionView +from view.instrument_view import InstrumentView from voxel.acquisition.acquisition import Acquisition -from pathlib import Path -import os -from time import sleep +from voxel.instruments.instrument import Instrument RESOURCES_DIR = (Path(os.path.dirname(os.path.realpath(__file__)))) @@ -27,9 +25,5 @@ instrument_view = InstrumentView(instrument, GUI_YAML) - # instrument_view.grab_stage_positions_worker.pause() - # while not instrument_view.grab_stage_positions_worker.is_paused: - # sleep(.1) - acquisition_view = AcquisitionView(acquisition, instrument_view) sys.exit(app.exec_()) diff --git a/src/view/__init__.py b/src/view/__init__.py index d458f74..24157a5 100644 --- a/src/view/__init__.py +++ b/src/view/__init__.py @@ -1,3 +1,3 @@ -"""View for Voxel Instruments and Acquisition""" +"""view for voxel instruments and acquisition""" __version__ = "0.1.0" diff --git a/src/view/acquisition_view.py b/src/view/acquisition_view.py index ad95a6d..e61f63c 100644 --- a/src/view/acquisition_view.py +++ b/src/view/acquisition_view.py @@ -1,54 +1,58 @@ -import logging import importlib -from view.widgets.base_device_widget import BaseDeviceWidget, scan_for_properties, create_widget, label_maker -from view.widgets.acquisition_widgets.metadata_widget import MetadataWidget -from view.widgets.acquisition_widgets.volume_plan_widget import ( - VolumePlanWidget, - GridFromEdges, - GridWidthHeight, - GridRowsColumns, -) -from view.widgets.acquisition_widgets.volume_model import VolumeModel -from view.widgets.acquisition_widgets.channel_plan_widget import ChannelPlanWidget -from qtpy.QtCore import Slot, Qt -import inflection +import logging +from pathlib import Path from time import sleep +from typing import Iterator, Literal, Union + +import inflection +import napari +import numpy as np +from napari.qt import get_stylesheet +from napari.qt.threading import create_worker, thread_worker +from napari.settings import get_settings +from qtpy.QtCore import Qt, Slot +from qtpy.QtGui import QFont from qtpy.QtWidgets import ( - QGridLayout, - QWidget, + QApplication, QComboBox, - QSizePolicy, - QScrollArea, QDockWidget, + QDoubleSpinBox, + QFileDialog, + QFrame, + QGridLayout, + QHBoxLayout, QLabel, - QPushButton, - QSplitter, QLineEdit, - QSpinBox, - QDoubleSpinBox, + QMessageBox, QProgressBar, + QPushButton, + QScrollArea, + QSizePolicy, QSlider, - QApplication, - QHBoxLayout, - QFrame, - QFileDialog, - QMessageBox, + QSpinBox, + QSplitter, + QWidget, ) -from qtpy.QtGui import QFont -from napari.qt.threading import thread_worker, create_worker + +from view.widgets.acquisition_widgets.channel_plan_widget import ChannelPlanWidget +from view.widgets.acquisition_widgets.metadata_widget import MetadataWidget +from view.widgets.acquisition_widgets.volume_model import VolumeModel +from view.widgets.acquisition_widgets.volume_plan_widget import ( + GridFromEdges, + GridRowsColumns, + GridWidthHeight, + VolumePlanWidget, +) +from view.widgets.base_device_widget import BaseDeviceWidget, create_widget, label_maker, scan_for_properties from view.widgets.miscellaneous_widgets.q_dock_widget_title_bar import QDockWidgetTitleBar from view.widgets.miscellaneous_widgets.q_scrollable_float_slider import QScrollableFloatSlider from view.widgets.miscellaneous_widgets.q_scrollable_line_edit import QScrollableLineEdit -from pathlib import Path -from typing import Literal, Union, Iterator -import numpy as np -import napari -from napari.qt import get_stylesheet -from napari.settings import get_settings class AcquisitionView(QWidget): - """ "Class to act as a general acquisition view model to voxel instrument""" + """ + Class to act as a general acquisition view model to voxel instrument. + """ def __init__( self, @@ -154,22 +158,22 @@ def __init__( app.lastWindowClosed.connect(self.close) # shut everything down when closing def create_start_button(self) -> QPushButton: - """ - Create button to start acquisition - :return: start button - """ + """_summary_ + :return: _description_ + :rtype: QPushButton + """ start = QPushButton("Start") start.clicked.connect(self.start_acquisition) start.setStyleSheet("background-color: green") return start def create_stop_button(self) -> QPushButton: - """ - Create button to stop acquisition - :return: stop button - """ + """_summary_ + :return: _description_ + :rtype: QPushButton + """ stop = QPushButton("Stop") stop.clicked.connect(self.acquisition.stop_acquisition) stop.setStyleSheet("background-color: red") @@ -178,10 +182,7 @@ def create_stop_button(self) -> QPushButton: return stop def start_acquisition(self) -> None: - """ - Start acquisition and disable widgets - """ - + """_summary_""" # add tiles to acquisition config self.update_tiles() @@ -222,10 +223,7 @@ def start_acquisition(self) -> None: sleep(1) def acquisition_ended(self) -> None: - """ - Re-enable UI's and threads after acquisition has ended - """ - + """_summary_""" # enable acquisition view self.start_button.setEnabled(True) self.metadata_widget.setEnabled(True) @@ -257,12 +255,13 @@ def acquisition_ended(self) -> None: worker.pause() def stack_device_widgets(self, device_type: str) -> QWidget: - """ - Stack like device widgets in layout and hide/unhide with combo box - :param device_type: type of device being stacked - :return: widget containing all widgets pertaining to device type stacked ontop of each other - """ + """_summary_ + :param device_type: _description_ + :type device_type: str + :return: _description_ + :rtype: QWidget + """ device_widgets = { f"{inflection.pluralize(device_type)} {device_name}": create_widget("V", **widgets) for device_name, widgets in getattr(self, f"{device_type}_widgets").items() @@ -288,12 +287,13 @@ def stack_device_widgets(self, device_type: str) -> QWidget: @staticmethod def hide_devices(text: str, device_widgets: dict) -> None: - """ - Hide device widget if not selected in combo box - :param text: selected text of combo box - :param device_widgets: dictionary of widget groups - """ + """_summary_ + :param text: _description_ + :type text: str + :param device_widgets: _description_ + :type device_widgets: dict + """ for name, widget in device_widgets.items(): if name != text: widget.setVisible(False) @@ -301,26 +301,27 @@ def hide_devices(text: str, device_widgets: dict) -> None: widget.setVisible(True) def create_metadata_widget(self) -> MetadataWidget: - """ - Create custom widget for metadata in config - :return: widget for metadata - """ + """_summary_ + :return: _description_ + :rtype: MetadataWidget + """ metadata_widget = MetadataWidget(self.acquisition.metadata) metadata_widget.ValueChangedInside[str].connect( lambda name: setattr(self.acquisition.metadata, name, getattr(metadata_widget, name)) ) for name, widget in metadata_widget.property_widgets.items(): widget.setToolTip("") # reset tooltips - metadata_widget.setWindowTitle(f"Metadata") + metadata_widget.setWindowTitle("Metadata") return metadata_widget def create_acquisition_widget(self) -> QSplitter: - """ - Create widget to visualize acquisition grid - :return: splitter widget containing the volume model, volume plan, and channel plan widget - """ + """_summary_ + :raises KeyError: _description_ + :return: _description_ + :rtype: QSplitter + """ # find limits of all axes lim_dict = {} # add tiling stages @@ -408,22 +409,22 @@ def create_acquisition_widget(self) -> QSplitter: return acquisition_widget def channel_plan_changed(self, channel: str) -> None: - """ - Handle channel being added to scan - :param channel: channel added - """ + """_summary_ + :param channel: _description_ + :type channel: str + """ tile_order = [[t.row, t.col] for t in self.volume_plan.value()] if len(tile_order) != 0: self.channel_plan.add_channel_rows(channel, tile_order) self.update_tiles() def volume_plan_changed(self, value: Union[GridRowsColumns, GridFromEdges, GridWidthHeight]) -> None: - """ - Update channel plan and volume model when volume plan is changed - :param value: new value from volume_plan - """ + """_summary_ + :param value: _description_ + :type value: Union[GridRowsColumns, GridFromEdges, GridWidthHeight] + """ tile_volumes = self.volume_plan.scan_ends - self.volume_plan.scan_starts # update volume model @@ -443,16 +444,14 @@ def volume_plan_changed(self, value: Union[GridRowsColumns, GridFromEdges, GridW self.update_tiles() def update_tiles(self) -> None: - """ - Update config with the latest tiles - """ - + """_summary_""" self.acquisition.config["acquisition"]["tiles"] = self.create_tile_list() def move_stage(self, fov_position: list[float, float, float]) -> None: - """ - Slot for moving stage when fov_position is changed internally by grid_widget - :param fov_position: new fov position to move to + """_summary_ + + :param fov_position: _description_ + :type fov_position: list[float, float, float] """ scalar_coord_plane = [x.strip("-") for x in self.coordinate_plane] stage_names = {stage.instrument_axis: name for name, stage in self.instrument.tiling_stages.items()} @@ -463,10 +462,7 @@ def move_stage(self, fov_position: list[float, float, float]) -> None: scan_stage.move_absolute_mm(fov_position[2], wait=False) def stop_stage(self) -> None: - """ - Slot for stop stage - """ - + """_summary_""" for name, stage in { **getattr(self.instrument, "scanning_stages", {}), **getattr(self.instrument, "tiling_stages", {}), @@ -474,10 +470,7 @@ def stop_stage(self) -> None: stage.halt() def setup_fov_position(self) -> None: - """ - Set up live position thread - """ - + """_summary_""" self.grab_fov_positions_worker = self.grab_fov_positions() self.grab_fov_positions_worker.yielded.connect(lambda pos: setattr(self.volume_plan, "fov_position", pos)) self.grab_fov_positions_worker.yielded.connect(lambda pos: setattr(self.volume_model, "fov_position", pos)) @@ -485,8 +478,10 @@ def setup_fov_position(self) -> None: @thread_worker def grab_fov_positions(self) -> Iterator[list[float, float, float]]: - """ - Grab stage position from all stage objects and yield positions + """_summary_ + + :yield: _description_ + :rtype: Iterator[list[float, float, float]] """ scalar_coord_plane = [x.strip("-") for x in self.coordinate_plane] while True: # best way to do this or have some sort of break? @@ -501,19 +496,21 @@ def grab_fov_positions(self) -> Iterator[list[float, float, float]]: try: pos = stage.position_mm fov_pos[index] = pos if pos is not None else self.volume_plan.fov_position[index] - except ValueError as e: # Tigerbox sometime coughs up garbage. Locking issue? + except ValueError: # Tigerbox sometime coughs up garbage. Locking issue? pass sleep(0.1) yield fov_pos def create_operation_widgets(self, device_name: str, operation_name: str, operation_specs: dict) -> None: - """ - Create widgets based on operation dictionary attributes from instrument or acquisition - :param operation_name: name of operation - :param device_name: name of device correlating to operation - :param operation_specs: dictionary describing set up of operation - """ + """_summary_ + :param device_name: _description_ + :type device_name: str + :param operation_name: _description_ + :type operation_name: str + :param operation_specs: _description_ + :type operation_specs: dict + """ operation_type = operation_specs["type"] operation = getattr(self.acquisition, inflection.pluralize(operation_type))[device_name][operation_name] @@ -574,12 +571,13 @@ def create_operation_widgets(self, device_name: str, operation_name: str, operat labeled.show() def update_acquisition_layer(self, image: np.ndarray, camera_name: str) -> None: - """ - Update viewer with latest frame taken during acquisition - :param image: numpy array to add to viewer - :param camera_name: name of camera that image came off - """ + """_summary_ + :param image: _description_ + :type image: np.ndarray + :param camera_name: _description_ + :type camera_name: str + """ if image is not None: # TODO: How to get current channel layer_name = f"{camera_name}" @@ -591,26 +589,30 @@ def update_acquisition_layer(self, image: np.ndarray, camera_name: str) -> None: @thread_worker def grab_property_value(self, device: object, property_name: str, widget) -> Iterator: - """ - Grab value of property and yield - :param device: device to grab property from - :param property_name: name of property to get - :param widget: corresponding device widget - :return: value of property and widget to update - """ + """_summary_ + :param device: _description_ + :type device: object + :param property_name: _description_ + :type property_name: str + :param widget: _description_ + :type widget: _type_ + :yield: _description_ + :rtype: Iterator + """ while True: # best way to do this or have some sort of break? sleep(1) value = getattr(device, property_name) yield value, widget def update_property_value(self, value, widget) -> None: - """ - Update stage position in stage widget - :param widget: widget to update - :param value: value to update with - """ + """_summary_ + :param value: _description_ + :type value: _type_ + :param widget: _description_ + :type widget: _type_ + """ try: if type(widget) in [QLineEdit, QScrollableLineEdit]: widget.setText(str(value)) @@ -626,13 +628,15 @@ def update_property_value(self, value, widget) -> None: @Slot(str) def operation_property_changed(self, attr_name: str, operation: object, widget) -> None: - """ - Slot to signal when operation widget has been changed - :param widget: widget object relating to operation - :param operation: operation object - :param attr_name: name of attribute - """ + """_summary_ + :param attr_name: _description_ + :type attr_name: str + :param operation: _description_ + :type operation: object + :param widget: _description_ + :type widget: _type_ + """ name_lst = attr_name.split(".") self.log.debug(f"widget {attr_name} changed to {getattr(widget, name_lst[0])}") value = getattr(widget, name_lst[0]) @@ -654,11 +658,11 @@ def operation_property_changed(self, attr_name: str, operation: object, widget) pass def create_tile_list(self) -> list: - """ - Return a list of tiles for a scan - :return: list of tiles - """ + """_summary_ + :return: _description_ + :rtype: list + """ tiles = [] tile_slice = slice(self.volume_plan.start, self.volume_plan.stop) value = self.volume_plan.value() @@ -674,13 +678,15 @@ def create_tile_list(self) -> list: return tiles def write_tile(self, channel: str, tile) -> dict: - """ - Write dictionary describing tile parameters - :param channel: channel the tile is in - :param tile: tile object - :return - """ + """_summary_ + :param channel: _description_ + :type channel: str + :param tile: _description_ + :type tile: _type_ + :return: _description_ + :rtype: dict + """ row, column = tile.row, tile.col table_row = self.volume_plan.tile_table.findItems(str([row, column]), Qt.MatchExactly)[0].row() @@ -722,18 +728,17 @@ def write_tile(self, channel: str, tile) -> dict: return tile_dict def update_config_on_quit(self) -> None: - """ - Add functionality to close function to save device properties to instrument config - """ - + """_summary_""" return_value = self.update_config_query() if return_value == QMessageBox.Ok: self.acquisition.update_current_state_config() self.acquisition.save_config(self.config_save_to) def update_config_query(self) -> None: - """ - Pop up message asking if configuration would like to be saved + """_summary_ + + :return: _description_ + :rtype: _type_ """ msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Question) @@ -751,12 +756,13 @@ def update_config_query(self) -> None: return msgBox.exec() def select_directory(self, pressed: bool, msgBox: QMessageBox) -> None: - """ - Select directory - :param pressed: boolean for button press - :param msgBox: - """ + """_summary_ + :param pressed: _description_ + :type pressed: bool + :param msgBox: _description_ + :type msgBox: QMessageBox + """ fname = QFileDialog() folder = fname.getSaveFileName(directory=str(self.acquisition.config_path)) if folder[0] != "": # user pressed cancel @@ -767,10 +773,7 @@ def select_directory(self, pressed: bool, msgBox: QMessageBox) -> None: self.config_save_to = Path(folder[0]) def close(self) -> None: - """ - Close operations and end threads - """ - + """_summary_""" for worker in self.property_workers: worker.quit() self.grab_fov_positions_worker.quit() diff --git a/src/view/instrument_view.py b/src/view/instrument_view.py index 92e6ed9..a39b192 100644 --- a/src/view/instrument_view.py +++ b/src/view/instrument_view.py @@ -1,54 +1,51 @@ -from ruamel.yaml import YAML -from qtpy.QtCore import Slot, Signal, Qt -from qtpy.QtGui import QMouseEvent -from pathlib import Path +import datetime import importlib -from view.widgets.base_device_widget import ( - BaseDeviceWidget, - create_widget, - pathGet, - scan_for_properties, - disable_button, -) +import inspect +import logging +from pathlib import Path +from time import sleep +from typing import Iterator, Literal, Union + +import inflection +import napari +import numpy as np +import tifffile +from napari.qt.threading import create_worker, thread_worker +from napari.utils.theme import get_theme +from qtpy.QtCore import Qt, Signal, Slot +from qtpy.QtGui import QMouseEvent from qtpy.QtWidgets import ( - QStyle, - QRadioButton, - QWidget, + QApplication, QButtonGroup, - QSlider, - QGridLayout, QComboBox, - QApplication, - QVBoxLayout, - QLabel, + QFileDialog, QFrame, - QSizePolicy, - QLineEdit, - QSpinBox, - QDoubleSpinBox, + QGridLayout, + QLabel, QMessageBox, QPushButton, - QFileDialog, + QRadioButton, QScrollArea, + QSizePolicy, + QStyle, + QVBoxLayout, + QWidget, +) +from ruamel.yaml import YAML + +from view.widgets.base_device_widget import ( + BaseDeviceWidget, + create_widget, + disable_button, + pathGet, + scan_for_properties, ) -import tifffile -from napari.qt.threading import thread_worker, create_worker -from napari.utils.theme import get_theme -import napari -import datetime -from time import sleep -import logging -import inflection -import inspect -from view.widgets.miscellaneous_widgets.q_scrollable_line_edit import QScrollableLineEdit -from view.widgets.miscellaneous_widgets.q_scrollable_float_slider import QScrollableFloatSlider -from view.widgets.miscellaneous_widgets.q_dock_widget_title_bar import QDockWidgetTitleBar -import numpy as np -from typing import Literal, Union, Iterator class InstrumentView(QWidget): - """ "Class to act as a general instrument view model to voxel instrument""" + """ + Class to act as a general instrument view model to voxel instrument. + """ snapshotTaken = Signal((np.ndarray, list)) contrastChanged = Signal((np.ndarray, list)) @@ -59,10 +56,14 @@ def __init__( config_path: Path, log_level: Literal["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO", ): - """ - :param instrument: voxel like instrument object - :param config_path: path to gui config yaml - :param log_level: level to set logger + """_summary_ + + :param instrument: _description_ + :type instrument: _type_ + :param config_path: _description_ + :type config_path: Path + :param log_level: _description_, defaults to "INFO" + :type log_level: Literal["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], optional """ super().__init__() @@ -127,9 +128,8 @@ def __init__( def setup_daqs(self) -> None: """ - Initialize daqs with livestreaming tasks if different from data acquisition tasks + Initialize daqs with livestreaming tasks if different from data acquisition tasks. """ - for daq_name, daq in self.instrument.daqs.items(): if daq_name in self.config["instrument_view"].get("livestream_tasks", {}).keys(): daq.tasks = self.config["instrument_view"]["livestream_tasks"][daq_name]["tasks"] @@ -143,9 +143,8 @@ def setup_daqs(self) -> None: def setup_stage_widgets(self) -> None: """ - Arrange stage position and joystick widget + Arrange stage position and joystick widget. """ - stage_widgets = [] for name, widget in { **self.tiling_stage_widgets, @@ -175,10 +174,7 @@ def setup_stage_widgets(self) -> None: self.viewer.window.add_dock_widget(joystick_scroll, area="left", name="Joystick") def setup_laser_widgets(self) -> None: - """ - Arrange laser widgets - """ - + """_summary_""" laser_widgets = [] for name, widget in self.laser_widgets.items(): label = QLabel(name) @@ -194,9 +190,8 @@ def setup_laser_widgets(self) -> None: def setup_daq_widgets(self) -> None: """ - Setup saving to config if widget is from device-widget repo + Setup saving to config if widget is from device-widget repo. """ - for daq_name, daq_widget in self.daq_widgets.items(): # if daq_widget is BaseDeviceWidget or inherits from it, update waveforms when gui is changed if type(daq_widget) == BaseDeviceWidget or BaseDeviceWidget in type(daq_widget).__bases__: @@ -215,12 +210,13 @@ def setup_daq_widgets(self) -> None: self.viewer.window.add_dock_widget(stacked, area="right", name="DAQs", add_vertical_stretch=False) def stack_device_widgets(self, device_type: str) -> QWidget: - """ - Stack like device widgets in layout and hide/unhide with combo box - :param device_type: type of device being stacked - :return: widget containing all widgets pertaining to device type stacked ontop of each other - """ + """_summary_ + :param device_type: _description_ + :type device_type: str + :return: _description_ + :rtype: QWidget + """ device_widgets = getattr(self, f"{device_type}_widgets") overlap_layout = QGridLayout() overlap_layout.addWidget(QWidget(), 1, 0) # spacer widget @@ -240,12 +236,13 @@ def stack_device_widgets(self, device_type: str) -> QWidget: return overlap_widget def hide_devices(self, text: str, device_type: str) -> None: - """ - Hide device widget if not selected in combo box - :param text: selected text of combo box - :param device_type: type of device related to combo box - """ + """_summary_ + :param text: _description_ + :type text: str + :param device_type: _description_ + :type device_type: str + """ device_widgets = getattr(self, f"{device_type}_widgets") for name, widget in device_widgets.items(): if name != text: @@ -254,11 +251,11 @@ def hide_devices(self, text: str, device_type: str) -> None: widget.setVisible(True) def write_waveforms(self, daq) -> None: - """ - Write waveforms if livestreaming is on - :param daq: daq object - """ + """_summary_ + :param daq: _description_ + :type daq: _type_ + """ if self.grab_frames_worker.is_running: # if currently livestreaming if daq.ao_task is not None: daq.generate_waveforms("ao", self.livestream_channel) @@ -268,14 +265,16 @@ def write_waveforms(self, daq) -> None: daq.write_do_waveforms(rereserve_buffer=False) def update_config_waveforms(self, daq_widget, daq_name: str, attr_name: str) -> None: - """ - If waveforms are changed in gui, apply changes to livestream_tasks and data_acquisition_tasks if - applicable - :param daq_widget: widget pertaining to daq object - :param daq_name: name of daq - :param attr_name: waveform attribute to update - """ + """_summary_ + :param daq_widget: _description_ + :type daq_widget: _type_ + :param daq_name: _description_ + :type daq_name: str + :param attr_name: _description_ + :type attr_name: str + :raises KeyError: _description_ + """ path = attr_name.split(".") value = getattr(daq_widget, attr_name) self.log.debug(f"{daq_name} {attr_name} changed to {getattr(daq_widget, path[0])}") @@ -303,17 +302,15 @@ def update_config_waveforms(self, daq_widget, daq_name: str, attr_name: str) -> def setup_filter_wheel_widgets(self): """ - Stack filter wheels + Stack filter wheels. """ - stacked = self.stack_device_widgets("filter_wheel") self.viewer.window.add_dock_widget(stacked, area="bottom", name="Filter Wheels") def setup_camera_widgets(self): """ - Setup live view and snapshot button + Setup live view and snapshot button. """ - for camera_name, camera_widget in self.camera_widgets.items(): # Add functionality to snapshot button snapshot_button = getattr(camera_widget, "snapshot_button", QPushButton()) @@ -332,11 +329,11 @@ def setup_camera_widgets(self): self.viewer.window.add_dock_widget(stacked, area="right", name="Cameras", add_vertical_stretch=False) def toggle_live_button(self, camera_name: str) -> None: - """ - Toggle text and functionality of live button when pressed - :param camera_name: name of camera to set up - """ + """_summary_ + :param camera_name: _description_ + :type camera_name: str + """ live_button = getattr(self.camera_widgets[camera_name], "live_button", QPushButton()) live_button.disconnect() if live_button.text() == "Live": @@ -354,12 +351,13 @@ def toggle_live_button(self, camera_name: str) -> None: live_button.pressed.connect(lambda camera=camera_name: self.toggle_live_button(camera_name)) def setup_live(self, camera_name: str, frames=float("inf")) -> None: - """ - Set up for either livestream or snapshot - :param camera_name: name of camera to set up - :param frames: how many frames to take - """ + """_summary_ + :param camera_name: _description_ + :type camera_name: str + :param frames: _description_, defaults to float("inf") + :type frames: _type_, optional + """ if self.grab_frames_worker.is_running: if frames == 1: # create snapshot layer with the latest image layer = self.viewer.layers[f"{camera_name} {self.livestream_channel}"] @@ -404,11 +402,11 @@ def setup_live(self, camera_name: str, frames=float("inf")) -> None: daq.start() def dismantle_live(self, camera_name: str) -> None: - """ - Safely shut down live - :param camera_name: name of camera to shut down live - """ + """_summary_ + :param camera_name: _description_ + :type camera_name: str + """ self.instrument.cameras[camera_name].abort() for daq_name, daq in self.instrument.daqs.items(): daq.stop() @@ -417,12 +415,15 @@ def dismantle_live(self, camera_name: str) -> None: @thread_worker def grab_frames(self, camera_name: str, frames=float("inf")) -> Iterator[tuple[np.ndarray, str]]: - """ - Grab frames from camera - :param frames: how many frames to take - :param camera_name: name of camera - """ + """_summary_ + :param camera_name: _description_ + :type camera_name: str + :param frames: _description_, defaults to float("inf") + :type frames: _type_, optional + :yield: _description_ + :rtype: Iterator[tuple[np.ndarray, str]] + """ i = 0 while i < frames: # while loop since frames can == inf sleep(0.1) @@ -430,12 +431,13 @@ def grab_frames(self, camera_name: str, frames=float("inf")) -> Iterator[tuple[n i += 1 def update_layer(self, args, snapshot: bool = False) -> None: - """ - Update viewer with new camera frame - :param args: tuple of image and camera name - :param snapshot: if image taken is a snapshot or not - """ + """_summary_ + :param args: _description_ + :type args: _type_ + :param snapshot: _description_, defaults to False + :type snapshot: bool, optional + """ (image, camera_name) = args if image is not None: @@ -454,7 +456,7 @@ def update_layer(self, args, snapshot: bool = False) -> None: if snapshot: # emit signal if snapshot image = image if not layer.multiscale else image[-3] self.snapshotTaken.emit(image, layer.contrast_limits) - if layer.multiscale == True: # emit most down sampled image if multiscale + if layer.multiscale: # emit most down sampled image if multiscale layer.events.contrast_limits.connect( lambda event: self.contrastChanged.emit(layer.data[-3], layer.contrast_limits) ) @@ -467,12 +469,13 @@ def update_layer(self, args, snapshot: bool = False) -> None: def save_image( layer: Union[napari.layers.image.image.Image, list[napari.layers.image.image.Image]], event: QMouseEvent ) -> None: - """ - Save image in viewer by right-clicking viewer - :param layer: layer that was pressed - :param event: mouse event - """ + """_summary_ + :param layer: _description_ + :type layer: Union[napari.layers.image.image.Image, list[napari.layers.image.image.Image]] + :param event: _description_ + :type event: QMouseEvent + """ if event.button == 2: # Left click if layer.multiscale: image = layer.data[0] @@ -490,7 +493,7 @@ def save_image( def setup_channel_widget(self) -> None: """ - Create widget to select which laser to livestream with + Create widget to select which laser to livestream with. """ widget = QWidget() @@ -508,12 +511,13 @@ def setup_channel_widget(self) -> None: self.viewer.window.add_dock_widget(widget, area="bottom", name="Channels") def change_channel(self, checked: bool, channel: str) -> None: - """ - Update livestream_channel to newly selected channel - :param channel: name of channel - :param checked: if button is checked (True) or unchecked(False) - """ + """_summary_ + :param checked: _description_ + :type checked: bool + :param channel: _description_ + :type channel: str + """ if checked: if self.grab_frames_worker.is_running: # livestreaming is going for old_laser_name in self.channels[self.livestream_channel].get("lasers", []): @@ -532,12 +536,13 @@ def change_channel(self, checked: bool, channel: str) -> None: self.instrument.filters[filter].enable() def create_device_widgets(self, device_name: str, device_specs: dict) -> None: - """ - Create widgets based on device dictionary attributes from instrument or acquisition - :param device_name: name of device - :param device_specs: dictionary dictating how device should be set up - """ + """_summary_ + :param device_name: _description_ + :type device_name: str + :param device_specs: _description_ + :type device_specs: dict + """ device_type = device_specs["type"] device = getattr(self.instrument, inflection.pluralize(device_type))[device_name] @@ -576,14 +581,17 @@ def create_device_widgets(self, device_name: str, device_specs: dict) -> None: @thread_worker def grab_property_value(self, device: object, property_name: str, device_widget) -> Iterator: - """ - Grab value of property and yield - :param device: device to grab property from - :param property_name: name of property to get - :param device_widget: widget of entire device that is the parent of property widget - :return: value of property and widget to update - """ + """_summary_ + :param device: _description_ + :type device: object + :param property_name: _description_ + :type property_name: str + :param device_widget: _description_ + :type device_widget: _type_ + :yield: _description_ + :rtype: Iterator + """ while True: # best way to do this or have some sort of break? sleep(0.5) try: @@ -593,13 +601,15 @@ def grab_property_value(self, device: object, property_name: str, device_widget) yield value, device_widget, property_name def update_property_value(self, value, device_widget, property_name: str) -> None: - """ - Update stage position in stage widget - :param device_widget: widget of entire device that is the parent of property widget - :param value: value to update with - :param property_name: name of property to set - """ + """_summary_ + :param value: _description_ + :type value: _type_ + :param device_widget: _description_ + :type device_widget: _type_ + :param property_name: _description_ + :type property_name: str + """ try: setattr(device_widget, property_name, value) # setting attribute value will update widget except (RuntimeError, AttributeError): # Pass when window's closed or widget doesn't have position_mm_widget @@ -607,13 +617,15 @@ def update_property_value(self, value, device_widget, property_name: str) -> Non @Slot(str) def device_property_changed(self, attr_name: str, device: object, widget) -> None: - """ - Slot to signal when device widget has been changed - :param widget: widget object relating to device - :param device: device object - :param attr_name: name of attribute - """ + """_summary_ + :param attr_name: _description_ + :type attr_name: str + :param device: _description_ + :type device: object + :param widget: _description_ + :type widget: _type_ + """ name_lst = attr_name.split(".") self.log.debug(f"widget {attr_name} changed to {getattr(widget, name_lst[0])}") value = getattr(widget, name_lst[0]) @@ -645,9 +657,8 @@ def device_property_changed(self, attr_name: str, device: object, widget) -> Non def add_undocked_widgets(self) -> None: """ - Add undocked widget so all windows close when closing napari viewer + Add undocked widget so all windows close when closing napari viewer. """ - widgets = [] for key, dictionary in self.__dict__.items(): if "_widgets" in key: @@ -661,11 +672,11 @@ def add_undocked_widgets(self) -> None: undocked_widget.setVisible(False) def setDisabled(self, disable: bool) -> None: - """ - Enable/disable viewer - :param disable: boolean specifying whether to disable - """ + """_summary_ + :param disable: _description_ + :type disable: bool + """ widgets = [] for key, dictionary in self.__dict__.items(): if "_widgets" in key: @@ -678,18 +689,18 @@ def setDisabled(self, disable: bool) -> None: def update_config_on_quit(self) -> None: """ - Add functionality to close function to save device properties to instrument config + Add functionality to close function to save device properties to instrument config. """ - return_value = self.update_config_query() if return_value == QMessageBox.Ok: self.instrument.update_current_state_config() self.instrument.save_config(self.config_save_to) def update_config_query(self) -> Literal[0, 1]: - """ - Pop up message asking if configuration would like to be saved - :return: user reply to message box + """_summary_ + + :return: _description_ + :rtype: Literal[0, 1] """ msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Question) @@ -707,12 +718,13 @@ def update_config_query(self) -> Literal[0, 1]: return msgBox.exec() def select_directory(self, pressed: bool, msgBox: QMessageBox) -> None: - """ - Select directory - :param pressed: boolean for button press - :param msgBox: - """ + """_summary_ + :param pressed: _description_ + :type pressed: bool + :param msgBox: _description_ + :type msgBox: QMessageBox + """ fname = QFileDialog() folder = fname.getSaveFileName(directory=str(self.instrument.config_path)) if folder[0] != "": # user pressed cancel @@ -724,9 +736,8 @@ def select_directory(self, pressed: bool, msgBox: QMessageBox) -> None: def close(self) -> None: """ - Close instruments and end threads + Close instruments and end threads. """ - for worker in self.property_workers: worker.quit() self.grab_frames_worker.quit() diff --git a/src/view/widgets/acquisition_widgets/channel_plan_widget.py b/src/view/widgets/acquisition_widgets/channel_plan_widget.py index a35bd55..4e95913 100644 --- a/src/view/widgets/acquisition_widgets/channel_plan_widget.py +++ b/src/view/widgets/acquisition_widgets/channel_plan_widget.py @@ -1,38 +1,59 @@ -from qtpy.QtWidgets import QTabWidget, QTabBar, QWidget, QPushButton, \ - QMenu, QToolButton, QAction, QTableWidget, QTableWidgetItem, QComboBox, QSpinBox -from view.widgets.miscellaneous_widgets.q_item_delegates import QSpinItemDelegate, QTextItemDelegate, QComboItemDelegate -from view.widgets.miscellaneous_widgets.q_scrollable_line_edit import QScrollableLineEdit -from view.widgets.base_device_widget import label_maker -import numpy as np -from qtpy.QtCore import Signal, Qt -from inflection import singularize +import inspect from math import isnan + +import numpy as np import pint -import inspect +from inflection import singularize +from qtpy.QtCore import Qt, Signal +from qtpy.QtWidgets import ( + QAction, + QComboBox, + QMenu, + QPushButton, + QSpinBox, + QTabBar, + QTableWidget, + QTableWidgetItem, + QTabWidget, + QToolButton, + QWidget, +) + +from view.widgets.base_device_widget import label_maker +from view.widgets.miscellaneous_widgets.q_item_delegates import QComboItemDelegate, QSpinItemDelegate, QTextItemDelegate +from view.widgets.miscellaneous_widgets.q_scrollable_line_edit import QScrollableLineEdit + + class ChannelPlanWidget(QTabWidget): - """Widget defining parameters per tile per channel """ + """ + Widget defining parameters per tile per channel. + """ channelAdded = Signal([str]) channelChanged = Signal() - def __init__(self, instrument_view, channels: dict, properties: dict, unit: str = 'um'): - """ - :param instrument_view: view associated with instrument - :param channels: dictionary defining channels for instrument - :param properties: allowed prop for devices - :param unit: unit of all values + def __init__(self, instrument_view, channels: dict, properties: dict, unit: str = "um"): + """_summary_ + + :param instrument_view: _description_ + :type instrument_view: _type_ + :param channels: _description_ + :type channels: dict + :param properties: _description_ + :type properties: dict + :param unit: _description_, defaults to 'um' + :type unit: str, optional """ - super().__init__() self.possible_channels = channels self.channels = [] self.properties = properties - self.column_data_types = {'step size [um]': float, 'steps': int, 'prefix': str} + self.column_data_types = {"step size [um]": float, "steps": int, "prefix": str} # setup units for step size and step calculation unit_registry = pint.UnitRegistry() - self.unit = getattr(unit_registry, unit) # TODO: How to check if unit is in pint? + self.unit = getattr(unit_registry, unit) # TODO: How to check if unit is in pint? self.micron = unit_registry.um self.steps = {} # dictionary of number of steps for each tile in each channel @@ -46,17 +67,22 @@ def __init__(self, instrument_view, channels: dict, properties: dict, unit: str self.setTabBar(self.tab_bar) self.channel_order = QComboBox() - self.channel_order.addItems(['per Tile', 'per Volume', ]) + self.channel_order.addItems( + [ + "per Tile", + "per Volume", + ] + ) self.setCornerWidget(self.channel_order) self.mode = self.channel_order.currentText() - self.channel_order.currentTextChanged.connect(lambda value: setattr(self, 'mode', value)) + self.channel_order.currentTextChanged.connect(lambda value: setattr(self, "mode", value)) # initialize column dictionaries and column delgates self.initialize_tables(instrument_view) # add tab with button to add channels self.add_tool = QToolButton() - self.add_tool.setText('+') + self.add_tool.setText("+") menu = QMenu() for channel in self.possible_channels: action = QAction(str(channel), self) @@ -64,89 +90,89 @@ def __init__(self, instrument_view, channels: dict, properties: dict, unit: str menu.addAction(action) self.add_tool.setMenu(menu) self.add_tool.setPopupMode(QToolButton.InstantPopup) - self.insertTab(0, QWidget(), '') # insert dummy qwidget + self.insertTab(0, QWidget(), "") # insert dummy qwidget self.tab_bar.setTabButton(0, QTabBar.RightSide, self.add_tool) # reorder channels if tabbar moved - self.tab_bar.tabMoved.connect(lambda: - setattr(self, 'channels', [self.tabText(ch) for ch in range(self.count() - 1)])) + self.tab_bar.tabMoved.connect( + lambda: setattr(self, "channels", [self.tabText(ch) for ch in range(self.count() - 1)]) + ) self._apply_all = True # external flag to dictate behaviour of added tab def initialize_tables(self, instrument_view) -> None: - """ - Initialize table for all channels with proper columns and delegates - :param instrument_view: view object that contains all widget for devices in an instrument - """ + """_summary_ + :param instrument_view: _description_ + :type instrument_view: _type_ + """ # TODO: Checks here if prop or device isn't part of the instrument? Or go in instrument validation? - for channel in self.possible_channels: - setattr(self, f'{channel}_table', QTableWidget()) - table = getattr(self, f'{channel}_table') + setattr(self, f"{channel}_table", QTableWidget()) + table = getattr(self, f"{channel}_table") table.cellChanged.connect(self.cell_edited) - columns = ['step size [um]', 'steps', 'prefix'] + columns = ["step size [um]", "steps", "prefix"] delegates = [QSpinItemDelegate(), QSpinItemDelegate(minimum=0, step=1), QTextItemDelegate()] for device_type, properties in self.properties.items(): if device_type in self.possible_channels[channel].keys(): for device_name in self.possible_channels[channel][device_type]: - device_widget = getattr(instrument_view, f'{singularize(device_type)}_widgets')[device_name] + device_widget = getattr(instrument_view, f"{singularize(device_type)}_widgets")[device_name] device_object = getattr(instrument_view.instrument, device_type)[device_name] for prop in properties: # select delegate to use based on type - column_name = label_maker(f'{device_name}_{prop}') + column_name = label_maker(f"{device_name}_{prop}") descriptor = getattr(type(device_object), prop) - if not isinstance(descriptor, property) or getattr(descriptor, 'fset', None) is None: + if not isinstance(descriptor, property) or getattr(descriptor, "fset", None) is None: self.column_data_types[column_name] = None continue # try and correctly type properties based on setter - fset = getattr(descriptor, 'fset') + fset = getattr(descriptor, "fset") input_type = list(inspect.signature(fset).parameters.values())[-1].annotation self.column_data_types[column_name] = input_type if input_type != inspect._empty else None setattr(self, column_name, {}) columns.append(column_name) - prop_widget = getattr(device_widget, f'{prop}_widget') + prop_widget = getattr(device_widget, f"{prop}_widget") if type(prop_widget) in [QScrollableLineEdit, QSpinBox]: - minimum = getattr(descriptor, 'minimum', float('-inf')) - maximum = getattr(descriptor, 'maximum', float('inf')) - step = getattr(descriptor, 'step', .1) + minimum = getattr(descriptor, "minimum", float("-inf")) + maximum = getattr(descriptor, "maximum", float("inf")) + step = getattr(descriptor, "step", 0.1) delegates.append(QSpinItemDelegate(minimum=minimum, maximum=maximum, step=step)) - setattr(self, column_name + '_value_function', prop_widget.value) - elif type(getattr(device_widget, f'{prop}_widget')) == QComboBox: - widget = getattr(device_widget, f'{prop}_widget') + setattr(self, column_name + "_value_function", prop_widget.value) + elif type(getattr(device_widget, f"{prop}_widget")) == QComboBox: + widget = getattr(device_widget, f"{prop}_widget") items = [widget.itemText(i) for i in range(widget.count())] delegates.append(QComboItemDelegate(items=items)) - setattr(self, column_name + '_value_function', prop_widget.currentText) + setattr(self, column_name + "_value_function", prop_widget.currentText) else: # TODO: How to handle dictionary values delegates.append(QTextItemDelegate()) - setattr(self, column_name + '_value_function', prop_widget.text) - elif dict in type(properties).__mro__: # TODO: how to validate the GUI yaml? + setattr(self, column_name + "_value_function", prop_widget.text) + elif dict in type(properties).__mro__: # TODO: how to validate the GUI yaml? column_name = label_maker(device_type) setattr(self, column_name, {}) - setattr(self, column_name + '_initial_value', properties.get('initial_value', None)) + setattr(self, column_name + "_initial_value", properties.get("initial_value", None)) columns.append(column_name) - if properties['delegate'] == 'spin': - minimum = properties.get('minimum', None) - maximum = properties.get('maximum', None) - step = properties.get('step', .1 if properties['type'] == 'float' else 1 ) + if properties["delegate"] == "spin": + minimum = properties.get("minimum", None) + maximum = properties.get("maximum", None) + step = properties.get("step", 0.1 if properties["type"] == "float" else 1) delegates.append(QSpinItemDelegate(minimum=minimum, maximum=maximum, step=step)) - self.column_data_types[column_name] = float if properties['type'] == 'float' else int - elif properties['delegate'] == 'combo': - items = properties['items'] + self.column_data_types[column_name] = float if properties["type"] == "float" else int + elif properties["delegate"] == "combo": + items = properties["items"] delegates.append(QComboItemDelegate(items=items)) - type_mapping = {'int':int, 'float':float, 'str': str} - self.column_data_types[column_name] = type_mapping[properties['type']] + type_mapping = {"int": int, "float": float, "str": str} + self.column_data_types[column_name] = type_mapping[properties["type"]] else: delegates.append(QTextItemDelegate()) self.column_data_types[column_name] = str - columns.append('row, column') + columns.append("row, column") for i, delegate in enumerate(delegates): # table does not take ownership of the delegates, so they are removed from memory as they # are local variables causing a Segmentation fault. Need to be attributes - setattr(self, f'{columns[i]}_{channel}_delegate', delegate) + setattr(self, f"{columns[i]}_{channel}_delegate", delegate) table.setItemDelegateForColumn(i, delegate) table.setColumnCount(len(columns)) table.setHorizontalHeaderLabels(columns) @@ -157,17 +183,23 @@ def initialize_tables(self, instrument_view) -> None: @property def apply_all(self) -> bool: - """Property for the state of apply all - :return: boolean indicating if settings for the 0,0 tile are applied to all tiles""" + """_summary_ + + :return: _description_ + :rtype: bool + """ return self._apply_all @apply_all.setter def apply_all(self, value: bool) -> None: - """When apply all is toggled, update existing channels""" + """_summary_ + :param value: _description_ + :type value: bool + """ if self._apply_all != value: for channel in self.channels: - table = getattr(self, f'{channel}_table') + table = getattr(self, f"{channel}_table") for i in range(1, table.rowCount()): # skip first row for j in range(table.columnCount() - 1): # skip last column @@ -179,23 +211,27 @@ def apply_all(self, value: bool) -> None: @property def tile_volumes(self) -> np.ndarray: - """ - Property of tile volumes in 2d numpy array - :return: 2d numpy array containing the volume of the tile at the i, j location + """_summary_ + + :return: _description_ + :rtype: np.ndarray """ return self._tile_volumes @tile_volumes.setter def tile_volumes(self, value: np.ndarray) -> None: - """When tile dims is updated, update size of channel arrays""" + """_summary_ + :param value: _description_ + :type value: np.ndarray + """ self._tile_volumes = value for channel in self.channels: - table = getattr(self, f'{channel}_table') + table = getattr(self, f"{channel}_table") for i in range(table.columnCount() - 1): # skip row, column header = table.horizontalHeaderItem(i).text() - if header == 'step size [um]': - getattr(self, 'step_size')[channel] = np.resize(getattr(self, 'step_size')[channel], value.shape) + if header == "step size [um]": + getattr(self, "step_size")[channel] = np.resize(getattr(self, "step_size")[channel], value.shape) else: getattr(self, header)[channel] = np.resize(getattr(self, header)[channel], value.shape) self.step_size[channel] = np.resize(self.step_size[channel], value.shape) @@ -207,15 +243,14 @@ def tile_volumes(self, value: np.ndarray) -> None: if tile_index[0] < value.shape[0] and tile_index[1] < value.shape[1]: self.update_steps(tile_index, row, channel) - - def enable_item(self, item: QTableWidgetItem, enable: bool) -> None: - """ - Change flags for enabling/disabling items in channel_plan table - :param item: item to change flag for - :param enable: boolean value indicating if flags should be configured to enable or disable item - """ + """_summary_ + :param item: _description_ + :type item: QTableWidgetItem + :param enable: _description_ + :type enable: bool + """ flags = QTableWidgetItem().flags() if not enable: flags &= ~Qt.ItemIsEditable @@ -226,39 +261,40 @@ def enable_item(self, item: QTableWidgetItem, enable: bool) -> None: item.setFlags(flags) def add_channel(self, channel: str) -> None: - """Add channel to acquisition - :param channel: name of channel to add - """ + """_summary_ - table = getattr(self, f'{channel}_table') + :param channel: _description_ + :type channel: str + """ + table = getattr(self, f"{channel}_table") - for i in range(3, table.columnCount()-1): # skip steps, step_size, prefix, row/col + for i in range(3, table.columnCount() - 1): # skip steps, step_size, prefix, row/col column_name = table.horizontalHeaderItem(i).text() - delegate = getattr(self, f'{column_name}_{channel}_delegate', None) + delegate = getattr(self, f"{column_name}_{channel}_delegate", None) if delegate is not None: # Skip if prop did not have setter - array = getattr(self, f'{column_name}') + array = getattr(self, f"{column_name}") if type(delegate) == QSpinItemDelegate: array[channel] = np.zeros(self._tile_volumes.shape) elif type(delegate) == QComboItemDelegate: - array[channel] = np.empty(self._tile_volumes.shape, dtype='U100') + array[channel] = np.empty(self._tile_volumes.shape, dtype="U100") else: - array[channel] = np.empty(self._tile_volumes.shape, dtype='U100') + array[channel] = np.empty(self._tile_volumes.shape, dtype="U100") - if getattr(self, column_name + '_initial_value', None) is not None: # get initial value - array[channel][:, :] = getattr(self, column_name + '_initial_value') - elif getattr(self, column_name + '_value_function', None) is not None: + if getattr(self, column_name + "_initial_value", None) is not None: # get initial value + array[channel][:, :] = getattr(self, column_name + "_initial_value") + elif getattr(self, column_name + "_value_function", None) is not None: # call value function to get current set point - array[channel][:, :] = getattr(self, column_name + '_value_function')() + array[channel][:, :] = getattr(self, column_name + "_value_function")() self.steps[channel] = np.zeros(self._tile_volumes.shape, dtype=int) self.step_size[channel] = np.zeros(self._tile_volumes.shape, dtype=float) - self.prefix[channel] = np.zeros(self._tile_volumes.shape, dtype='U100') + self.prefix[channel] = np.zeros(self._tile_volumes.shape, dtype="U100") self.insertTab(0, table, channel) self.setCurrentIndex(0) # add button to remove channel - button = QPushButton('x') + button = QPushButton("x") button.setMaximumWidth(20) button.setMaximumHeight(20) button.pressed.connect(lambda: self.remove_channel(channel)) @@ -276,22 +312,24 @@ def add_channel(self, channel: str) -> None: self.channelAdded.emit(channel) def add_channel_rows(self, channel: str, order: list) -> None: - """Add rows to channel table in specific order of tiles - :param channel: name of channel - :param order: list of tile order e.g. [[0,0], [0,1]] - """ + """_summary_ - table = getattr(self, f'{channel}_table') + :param channel: _description_ + :type channel: str + :param order: _description_ + :type order: list + """ + table = getattr(self, f"{channel}_table") table.blockSignals(True) table.clearContents() table.setRowCount(0) arrays = [self.step_size[channel]] - delegates = [getattr(self, f'step size [um]_{channel}_delegate')] + delegates = [getattr(self, f"step size [um]_{channel}_delegate")] # iterate through columns to find relevant arrays to update for i in range(1, table.columnCount() - 1): # skip row, column arrays.append(getattr(self, table.horizontalHeaderItem(i).text())[channel]) - delegates.append(getattr(self, f'{table.horizontalHeaderItem(i).text()}_{channel}_delegate')) + delegates.append(getattr(self, f"{table.horizontalHeaderItem(i).text()}_{channel}_delegate")) for tile in order: table_row = table.rowCount() table.insertRow(table_row) @@ -310,13 +348,14 @@ def add_channel_rows(self, channel: str, order: list) -> None: table.blockSignals(False) def remove_channel(self, channel: str) -> None: - """Remove channel from acquisition - :param channel: name of channel - """ + """_summary_ + :param channel: _description_ + :type channel: str + """ self.channels.remove(channel) - table = getattr(self, f'{channel}_table') + table = getattr(self, f"{channel}_table") index = self.indexOf(table) self.removeTab(index) @@ -324,8 +363,8 @@ def remove_channel(self, channel: str) -> None: # remove key from attributes for i in range(table.columnCount() - 1): # skip row, column header = table.horizontalHeaderItem(i).text() - if header == 'step size [um]': - del getattr(self, 'step_size')[channel] + if header == "step size [um]": + del getattr(self, "step_size")[channel] else: del getattr(self, header)[channel] @@ -339,24 +378,29 @@ def remove_channel(self, channel: str) -> None: self.channelChanged.emit() def cell_edited(self, row: int, column: int, channel: str = None) -> None: + """_summary_ + + :param row: _description_ + :type row: int + :param column: _description_ + :type column: int + :param channel: _description_, defaults to None + :type channel: str, optional """ - Update table based on cell edit - :param row: row of item edited - :param column: column of item edited - :param channel: channel name of item edited - """ - channel = self.tabText(self.currentIndex()) if channel is None else channel - table = getattr(self, f'{channel}_table') + table = getattr(self, f"{channel}_table") table.blockSignals(True) # block signals so updating cells doesn't trigger cell edit again tile_index = [int(x) for x in table.item(row, table.columnCount() - 1).text() if x.isdigit()] if column in [0, 1]: - step_size, steps = self.update_steps(tile_index, row, channel) if column == 0 else \ - self.update_step_size(tile_index, row, channel) - table.item(row, 0).setData(Qt.EditRole,step_size) - table.item(row, 1).setData(Qt.EditRole,steps) + step_size, steps = ( + self.update_steps(tile_index, row, channel) + if column == 0 + else self.update_step_size(tile_index, row, channel) + ) + table.item(row, 0).setData(Qt.EditRole, step_size) + table.item(row, 1).setData(Qt.EditRole, steps) # FIXME: I think this is would be considered unexpected behavior array = getattr(self, table.horizontalHeaderItem(column).text(), self.step_size)[channel] @@ -365,7 +409,7 @@ def cell_edited(self, row: int, column: int, channel: str = None) -> None: array[:, :] = value for i in range(1, table.rowCount()): item_0 = table.item(0, column) - table.item(i, column).setData(Qt.EditRole,item_0.data(Qt.EditRole)) + table.item(i, column).setData(Qt.EditRole, item_0.data(Qt.EditRole)) if column == 0: # update steps as well table.item(i, column + 1).setData(Qt.EditRole, int(steps)) elif column == 1: # update step_size as well @@ -375,20 +419,25 @@ def cell_edited(self, row: int, column: int, channel: str = None) -> None: table.blockSignals(False) self.channelChanged.emit() - def update_steps(self, tile_index: list[int], row: int, channel: str) -> list[float, int]: - """ - Update number of steps based on volume - :param tile_index: integer list specifying row, column value of tile - :param row: row of item that correspond to tile at position tile_index - :param channel: name of channel - :return: step_size in um and number of steps + def update_steps(self, tile_index: list[int], row: int, channel: str) -> list[float, int]: + """_summary_ + + :param tile_index: _description_ + :type tile_index: list[int] + :param row: _description_ + :type row: int + :param channel: _description_ + :type channel: str + :return: _description_ + :rtype: list[float, int] """ - - volume_um = (self.tile_volumes[*tile_index]*self.unit).to(self.micron) + volume_um = (self.tile_volumes[*tile_index] * self.unit).to(self.micron) index = tile_index if not self.apply_all else [slice(None), slice(None)] - steps = volume_um / (float(getattr(self, f'{channel}_table').item(row, 0).data(Qt.EditRole))*self.micron) - if steps != 0 and not isnan(steps) and steps not in [float('inf'), float('-inf')]: - step_size = float(round(volume_um / steps, 4)/self.micron) # make dimensionless again for simplicity in code + steps = volume_um / (float(getattr(self, f"{channel}_table").item(row, 0).data(Qt.EditRole)) * self.micron) + if steps != 0 and not isnan(steps) and steps not in [float("inf"), float("-inf")]: + step_size = float( + round(volume_um / steps, 4) / self.micron + ) # make dimensionless again for simplicity in code steps = int(round(steps)) else: steps = 0 @@ -397,21 +446,24 @@ def update_steps(self, tile_index: list[int], row: int, channel: str) -> list[f return step_size, steps - def update_step_size(self, tile_index: list[int], row: int, channel: str) -> list[float, int]: + def update_step_size(self, tile_index: list[int], row: int, channel: str) -> list[float, int]: + """_summary_ + + :param tile_index: _description_ + :type tile_index: list[int] + :param row: _description_ + :type row: int + :param channel: _description_ + :type channel: str + :return: _description_ + :rtype: list[float, int] """ - Update number of steps based on volume - :param tile_index: integer list specifying row, column value of tile - :param row: row of item that correspond to tile at position tile_index - :param channel: name of channel - :return: step_size in um and number of steps - """ - - volume_um = (self.tile_volumes[*tile_index]*self.unit).to(self.micron) + volume_um = (self.tile_volumes[*tile_index] * self.unit).to(self.micron) index = tile_index if not self.apply_all else [slice(None), slice(None)] # make dimensionless again for simplicity in code - step_size = (volume_um / float(getattr(self, f'{channel}_table').item(row, 1).data(Qt.EditRole)))/self.micron - if step_size != 0 and not isnan(step_size) and step_size not in [float('inf'), float('-inf')]: - steps = int(round(volume_um / (step_size*self.micron))) + step_size = (volume_um / float(getattr(self, f"{channel}_table").item(row, 1).data(Qt.EditRole))) / self.micron + if step_size != 0 and not isnan(step_size) and step_size not in [float("inf"), float("-inf")]: + steps = int(round(volume_um / (step_size * self.micron))) step_size = float(round(step_size, 4)) else: steps = 0 @@ -419,29 +471,33 @@ def update_step_size(self, tile_index: list[int], row: int, channel: str) -> li self.step_size[channel][*index] = step_size return step_size, steps + class ChannelPlanTabBar(QTabBar): - """TabBar that will keep add channel tab at end""" + """ + Tab bar that will keep add channel tab at end. + """ def __init__(self): - + """_summary_""" super(ChannelPlanTabBar, self).__init__() self.tabMoved.connect(self.tab_index_check) def tab_index_check(self, prev_index: int, curr_index: int) -> None: - """ - Keep last tab as last tab - :param prev_index: previous index of tab - :param curr_index: index tab was moved to - """ + """_summary_ + :param prev_index: _description_ + :type prev_index: int + :param curr_index: _description_ + :type curr_index: int + """ if prev_index == self.count() - 1: self.moveTab(curr_index, prev_index) def mouseMoveEvent(self, ev) -> None: - """ - Make last tab immovable - :param ev: qmouseevent that triggered call - :return: + """_summary_ + + :param ev: _description_ + :type ev: _type_ """ index = self.currentIndex() if index == self.count() - 1: # last tab is immovable diff --git a/src/view/widgets/acquisition_widgets/metadata_widget.py b/src/view/widgets/acquisition_widgets/metadata_widget.py index 8c504df..58be99d 100644 --- a/src/view/widgets/acquisition_widgets/metadata_widget.py +++ b/src/view/widgets/acquisition_widgets/metadata_widget.py @@ -1,42 +1,65 @@ -from view.widgets.base_device_widget import BaseDeviceWidget, scan_for_properties -from qtpy.QtWidgets import QWidget from typing import Callable +from qtpy.QtWidgets import QWidget + +from view.widgets.base_device_widget import BaseDeviceWidget, scan_for_properties + + class MetadataWidget(BaseDeviceWidget): - """Widget for handling metadata class""" + """ + Widget for handling metadata class. + """ + def __init__(self, metadata_class, advanced_user: bool = True) -> None: - """ - :param metadata_class: class to create widget out of - :param advanced_user: future use argument to determine what should be shown - """ + """_summary_ + :param metadata_class: _description_ + :type metadata_class: _type_ + :param advanced_user: _description_, defaults to True + :type advanced_user: bool, optional + """ properties = scan_for_properties(metadata_class) self.metadata_class = metadata_class super().__init__(type(metadata_class), properties) self.metadata_class = metadata_class - self.property_widgets.get('acquisition_name_format', - QWidget()).hide() # hide until BaseClassWidget can handle lists + self.property_widgets.get( + "acquisition_name_format", QWidget() + ).hide() # hide until BaseClassWidget can handle lists # wrap property setters that are in acquisition_name_format so acquisition name update when changed - for name in getattr(self, 'acquisition_name_format', []) + \ - ['date_format' if hasattr(self, 'date_format') else None] + \ - ['delimiter' if hasattr(self, 'delimiter') else None]: + for name in ( + getattr(self, "acquisition_name_format", []) + + ["date_format" if hasattr(self, "date_format") else None] + + ["delimiter" if hasattr(self, "delimiter") else None] + ): if name is not None: prop = getattr(type(metadata_class), name) - prop_setter = getattr(prop, 'fset') - filter_getter = getattr(prop, 'fget') - setattr(type(metadata_class), name, - property(filter_getter, self.name_property_change_wrapper(prop_setter))) + prop_setter = getattr(prop, "fset") + filter_getter = getattr(prop, "fget") + setattr( + type(metadata_class), name, property(filter_getter, self.name_property_change_wrapper(prop_setter)) + ) def name_property_change_wrapper(self, func: Callable) -> Callable: - """Wrapper function that emits a signal when property setters that are in acquisition_name_format is called - :param func: function to wrap - :return: wrapped input function + """_summary_ + + :param func: _description_ + :type func: Callable + :return: _description_ + :rtype: Callable """ + def wrapper(object, value): + """_summary_ + + :param object: _description_ + :type object: _type_ + :param value: _description_ + :type value: _type_ + """ func(object, value) self.acquisition_name = self.metadata_class.acquisition_name - self.update_property_widget('acquisition_name') + self.update_property_widget("acquisition_name") return wrapper diff --git a/src/view/widgets/acquisition_widgets/volume_model.py b/src/view/widgets/acquisition_widgets/volume_model.py index 812df00..1b7d71d 100644 --- a/src/view/widgets/acquisition_widgets/volume_model.py +++ b/src/view/widgets/acquisition_widgets/volume_model.py @@ -1,32 +1,60 @@ -from pyqtgraph.opengl import GLImageItem -from qtpy.QtWidgets import QMessageBox, QCheckBox, QGridLayout, QButtonGroup, QLabel, QRadioButton, QPushButton, QWidget -from qtpy.QtCore import Signal, Qt -from qtpy.QtGui import QMatrix4x4, QVector3D, QQuaternion -from math import tan, radians, sqrt +from math import radians, sqrt, tan + import numpy as np -from scipy import spatial from pyqtgraph import makeRGBA +from pyqtgraph.opengl import GLImageItem +from qtpy.QtCore import Qt, Signal +from qtpy.QtGui import QMatrix4x4, QQuaternion, QVector3D +from qtpy.QtWidgets import QButtonGroup, QCheckBox, QGridLayout, QLabel, QMessageBox, QPushButton, QRadioButton, QWidget +from scipy import spatial + from view.widgets.miscellaneous_widgets.gl_ortho_view_widget import GLOrthoViewWidget -from view.widgets.miscellaneous_widgets.gl_shaded_box_item import GLShadedBoxItem from view.widgets.miscellaneous_widgets.gl_path_item import GLPathItem +from view.widgets.miscellaneous_widgets.gl_shaded_box_item import GLShadedBoxItem class SignalChangeVar: + """_summary_""" def __set_name__(self, owner, name): + """_summary_ + + :param owner: _description_ + :type owner: _type_ + :param name: _description_ + :type name: _type_ + """ self.name = f"_{name}" def __set__(self, instance, value): + """_summary_ + + :param instance: _description_ + :type instance: _type_ + :param value: _description_ + :type value: _type_ + """ setattr(instance, self.name, value) # initially setting attr instance.valueChanged.emit(self.name[1:]) def __get__(self, instance, value): + """_summary_ + + :param instance: _description_ + :type instance: _type_ + :param value: _description_ + :type value: _type_ + :return: _description_ + :rtype: _type_ + """ return getattr(instance, self.name) class VolumeModel(GLOrthoViewWidget): - """Widget to display configured acquisition grid. Note that the x and y refer to the tiling - dimensions and z is the scanning dimension""" + """ + Widget to display configured acquisition grid. Note that the x and y refer to the tiling + dimensions and z is the scanning dimension + """ fov_dimensions = SignalChangeVar() fov_position = SignalChangeVar() @@ -62,32 +90,51 @@ def __init__( limits_color: str = "white", limits_opacity: float = 0.1, ): + """_summary_ + + :param unit: _description_, defaults to "mm" + :type unit: str, optional + :param limits: _description_, defaults to None + :type limits: list[[float, float], [float, float], [float, float]], optional + :param fov_dimensions: _description_, defaults to None + :type fov_dimensions: list[float, float, float], optional + :param fov_position: _description_, defaults to None + :type fov_position: list[float, float, float], optional + :param coordinate_plane: _description_, defaults to None + :type coordinate_plane: list[str, str, str], optional + :param fov_color: _description_, defaults to "yellow" + :type fov_color: str, optional + :param fov_line_width: _description_, defaults to 2 + :type fov_line_width: int, optional + :param fov_opacity: _description_, defaults to 0.15 + :type fov_opacity: float, optional + :param path_line_width: _description_, defaults to 2 + :type path_line_width: int, optional + :param path_arrow_size: _description_, defaults to 6.0 + :type path_arrow_size: float, optional + :param path_arrow_aspect_ratio: _description_, defaults to 4 + :type path_arrow_aspect_ratio: int, optional + :param path_start_color: _description_, defaults to "magenta" + :type path_start_color: str, optional + :param path_end_color: _description_, defaults to "green" + :type path_end_color: str, optional + :param active_tile_color: _description_, defaults to "cyan" + :type active_tile_color: str, optional + :param active_tile_opacity: _description_, defaults to 0.075 + :type active_tile_opacity: float, optional + :param inactive_tile_color: _description_, defaults to "red" + :type inactive_tile_color: str, optional + :param inactive_tile_opacity: _description_, defaults to 0.025 + :type inactive_tile_opacity: float, optional + :param tile_line_width: _description_, defaults to 2 + :type tile_line_width: int, optional + :param limits_line_width: _description_, defaults to 2 + :type limits_line_width: int, optional + :param limits_color: _description_, defaults to "white" + :type limits_color: str, optional + :param limits_opacity: _description_, defaults to 0.1 + :type limits_opacity: float, optional """ - GLViewWidget to display proposed grid of acquisition - - :param unit: unit of the volume model. - :param coordinate_plane: coordinate plane displayed on widget. - :param fov_dimensions: dimensions of field of view in coordinate plane - :param fov_position: position of fov - :param limits: list of limits ordered in [tile_dim[0], tile_dim[1], scan_dim[0]] - :param fov_line_width: width of fov outline - :param fov_line_width: width of fov outline - :param fov_opacity: opacity of fov face where 1 is fully opaque - :param path_line_width: width of path line - :param path_arrow_size: size of arrow at the end of path as a percentage of the field of view - :param path_arrow_aspect_ratio: aspect ratio of arrow - :param path_start_color: start color of path - :param path_end_color: end color of path - :param active_tile_color: color of tiles when fov is within tile grid - :param active_tile_opacity: opacity of active tile grid faces where 1 is fully opaque - :param inactive_tile_color: color of tiles when fov is outside of tile grid - :param inactive_tile_opacity: opacity of inactive tile grid faces where 1 is fully opaque - :param tile_line_width: width of tiles - :param limits_line_width: width of limits box - :param limits_color: color of limits box - :param limits_opacity: opacity of limits box - """ - super().__init__(rotationMethod="quaternion") # initialize attributes @@ -165,17 +212,6 @@ def __init__( if limits != [[float("-inf"), float("inf")], [float("-inf"), float("inf")], [float("-inf"), float("inf")]]: size = [((max(limits[i]) - min(limits[i])) + self.fov_dimensions[i]) for i in range(3)] - pos = np.array( - [ - [ - [ - min([x * self.polarity[0] for x in limits[0]]), - min([y * self.polarity[1] for y in limits[1]]), - min([z * self.polarity[2] for z in limits[2]]), - ] - ] - ] - ) stage_limits = GLShadedBoxItem( width=self.limits_line_width, pos=np.array( @@ -234,9 +270,11 @@ def __init__( self.widgets.show() def update_model(self, attribute_name) -> None: - """Update attributes of grid - :param attribute_name: name of attribute to update""" + """_summary_ + :param attribute_name: _description_ + :type attribute_name: _type_ + """ # update color of tiles based on z position flat_coords = self.grid_coords.reshape([-1, 3]) # flatten array flat_dims = self.scan_volumes.flatten() # flatten array @@ -319,18 +357,20 @@ def update_model(self, attribute_name) -> None: self._update_opts() def toggle_view_plane(self, button) -> None: - """ - Update view plane optics - :param button: button pressed to change view - """ + """_summary_ + :param button: _description_ + :type button: _type_ + """ view_plane = tuple(x for x in button.text() if x.isalpha()) self.view_plane = view_plane def set_path_pos(self, coord_order: list) -> None: - """Set the pos of path in correct order - :param coord_order: ordered list of coords for path""" + """_summary_ + :param coord_order: _description_ + :type coord_order: list + """ path = np.array( [ [ @@ -343,10 +383,13 @@ def set_path_pos(self, coord_order: list) -> None: self.path.setData(pos=path) def add_fov_image(self, image: np.ndarray, levels: list[float]) -> None: - """add image to model assuming image has same fov dimensions and orientation - :param image: numpy array of image to display in model - :param levels: levels for passed in image""" + """_summary_ + :param image: _description_ + :type image: np.ndarray + :param levels: _description_ + :type levels: list[float] + """ image_rgba = makeRGBA(image, levels=levels) image_rgba[0][:, :, 3] = 200 @@ -379,28 +422,29 @@ def add_fov_image(self, image: np.ndarray, levels: list[float]) -> None: gl_image.setVisible(False) def adjust_glimage_contrast(self, image: np.ndarray, contrast_levels: list[float]) -> None: - """ - Adjust image in model contrast levels - :param image: numpy array of image key in fov_images - :param contrast_levels: levels for passed in image - """ + """_summary_ + :param image: _description_ + :type image: np.ndarray + :param contrast_levels: _description_ + :type contrast_levels: list[float] + """ if image.tobytes() in self.fov_images.keys(): # check if image has been deleted glimage = self.fov_images[image.tobytes()] - coords = [glimage.transform()[i, 3] / pol for i, pol in zip(range(3), self.polarity)] self.removeItem(glimage) self.add_fov_image(image, contrast_levels) def toggle_fov_image_visibility(self, visible: bool) -> None: - """Function to hide all fov_images - :param visible: boolean for if fov_images should be visible""" + """_summary_ + :param visible: _description_ + :type visible: bool + """ for image in self.fov_images.values(): image.setVisible(visible) def _update_opts(self) -> None: - """Update view of widget. Note that x/y notation refers to horizontal/vertical dimensions of grid view""" - + """_summary_""" view_plane = self.view_plane view_pol = [ self.polarity[self.coordinate_plane.index(view_plane[0])], @@ -497,11 +541,13 @@ def _update_opts(self) -> None: self.update() def move_fov_query(self, new_fov_pos: list[float]) -> [int, bool]: - """Message box asking if user wants to move fov position - :param new_fov_pos: position to move the fov to in um - :return: user reply to pop up and whether to move to the tile nearest the new_fov_pos - """ + """_summary_ + :param new_fov_pos: _description_ + :type new_fov_pos: list[float] + :return: _description_ + :rtype: [int, bool] + """ msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Question) msgBox.setText( @@ -519,10 +565,13 @@ def move_fov_query(self, new_fov_pos: list[float]) -> [int, bool]: return msgBox.exec(), checkbox.isChecked() def delete_fov_image_query(self, fov_image_pos: list[float]) -> int: - """Message box asking if user wants to move fov position - :param fov_image_pos: coordinates of fov image - :return: user reply to deleting image""" + """_summary_ + :param fov_image_pos: _description_ + :type fov_image_pos: list[float] + :return: _description_ + :rtype: int + """ msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Question) msgBox.setText(f"Do you want to delete image at {fov_image_pos} [{self.unit}]?") @@ -532,10 +581,11 @@ def delete_fov_image_query(self, fov_image_pos: list[float]) -> int: return msgBox.exec() def mousePressEvent(self, event) -> None: - """Override mouseMoveEvent so user can't change view - and allow user to move fov easier - :param event: QMouseEvent of users mouse""" + """_summary_ + :param event: _description_ + :type event: _type_ + """ plane = list(self.view_plane) + [ax for ax in self.coordinate_plane if ax not in self.view_plane] view_pol = [ self.polarity[self.coordinate_plane.index(plane[0])], @@ -602,17 +652,25 @@ def mousePressEvent(self, event) -> None: del self.fov_images[delete_key] def mouseMoveEvent(self, event): - """Override mouseMoveEvent so user can't change view""" + """ + Override mouseMoveEvent so user can't change view. + """ pass def wheelEvent(self, event): - """Override wheelEvent so user can't change view""" + """ + Override wheelEvent so user can't change view. + """ pass def keyPressEvent(self, event): - """Override keyPressEvent so user can't change view""" + """ + Override keyPressEvent so user can't change view. + """ pass def keyReleaseEvent(self, event): - """Override keyPressEvent so user can't change view""" + """ + Override keyPressEvent so user can't change view. + """ pass diff --git a/src/view/widgets/acquisition_widgets/volume_plan_widget.py b/src/view/widgets/acquisition_widgets/volume_plan_widget.py index f2d861b..6c05423 100644 --- a/src/view/widgets/acquisition_widgets/volume_plan_widget.py +++ b/src/view/widgets/acquisition_widgets/volume_plan_widget.py @@ -1,53 +1,73 @@ -import useq -from view.widgets.base_device_widget import create_widget -from view.widgets.miscellaneous_widgets.q_item_delegates import QSpinItemDelegate -from view.widgets.miscellaneous_widgets.q_start_stop_table_header import QStartStopTableHeader +from typing import Generator, Literal, Union + import numpy as np +import useq from qtpy.QtCore import Qt, Signal from qtpy.QtWidgets import ( QButtonGroup, + QCheckBox, + QComboBox, QDoubleSpinBox, + QFrame, QLabel, + QMainWindow, QRadioButton, + QSizePolicy, QSpinBox, - QVBoxLayout, - QWidget, - QComboBox, - QMainWindow, - QFrame, - QCheckBox, QTableWidget, QTableWidgetItem, - QSizePolicy, + QVBoxLayout, + QWidget, ) -from typing import Literal, Union, Generator + +from view.widgets.base_device_widget import create_widget +from view.widgets.miscellaneous_widgets.q_item_delegates import QSpinItemDelegate +from view.widgets.miscellaneous_widgets.q_start_stop_table_header import QStartStopTableHeader class GridFromEdges(useq.GridFromEdges): - """Subclassing useq.GridFromEdges to add row and column attributes and allow reversible order""" + """ + Subclassing useq.GridFromEdges to add row and column attributes and allow reversible order. + """ reverse = property() # initialize property def __init__(self, reverse=False, *args, **kwargs): + """_summary_ + + :param reverse: _description_, defaults to False + :type reverse: bool, optional + """ # rewrite property since pydantic doesn't allow to add attr setattr(type(self), "reverse", property(fget=lambda x: reverse)) super().__init__(*args, **kwargs) @property def rows(self) -> int: - """Property that returns number of rows in configured scan""" + """_summary_ + + :return: _description_ + :rtype: int + """ dx, _ = self._step_size(self.fov_width, self.fov_height) return self._nrows(dx) @property def columns(self) -> int: - """Property that returns number of columns in configured scan""" + """_summary_ + + :return: _description_ + :rtype: int + """ _, dy = self._step_size(self.fov_width, self.fov_height) return self._ncolumns(dy) def iter_grid_positions(self, *args, **kwargs) -> Generator: - """Return generator that contains positions of tiles. If reversed property is True, yield in revere order""" + """_summary_ + :yield: _description_ + :rtype: Generator + """ if not self.reverse: for tile in super().iter_grid_positions(*args, **kwargs): yield tile @@ -57,30 +77,48 @@ def iter_grid_positions(self, *args, **kwargs) -> Generator: class GridWidthHeight(useq.GridWidthHeight): - """Subclassing useq.GridWidthHeight to add row and column attributes and allow reversible order""" + """ + Subclassing useq.GridWidthHeight to add row and column attributes and allow reversible order. + """ reverse = property() def __init__(self, reverse=False, *args, **kwargs): + """_summary_ + + :param reverse: _description_, defaults to False + :type reverse: bool, optional + """ # rewrite property since pydantic doesn't allow to add attr setattr(type(self), "reverse", property(fget=lambda x: reverse)) super().__init__(*args, **kwargs) @property def rows(self) -> int: - """Property that returns number of rows in configured scan""" + """_summary_ + + :return: _description_ + :rtype: int + """ dx, _ = self._step_size(self.fov_width, self.fov_height) return self._nrows(dx) @property def columns(self) -> int: - """Property that returns number of rows in configured scan""" + """_summary_ + + :return: _description_ + :rtype: int + """ _, dy = self._step_size(self.fov_width, self.fov_height) return self._ncolumns(dy) def iter_grid_positions(self, *args, **kwargs) -> Generator: - """Return generator that contains positions of tiles. If reversed property is True, yield in revere order""" + """_summary_ + :yield: _description_ + :rtype: Generator + """ if not self.reverse: for tile in super().iter_grid_positions(*args, **kwargs): yield tile @@ -90,17 +128,27 @@ def iter_grid_positions(self, *args, **kwargs) -> Generator: class GridRowsColumns(useq.GridRowsColumns): - """Subclass useq.GridRowsColumns to allow reversible order""" + """ + Subclass useq.GridRowsColumns to allow reversible order. + """ reverse = property() def __init__(self, reverse=False, *args, **kwargs): + """_summary_ + + :param reverse: _description_, defaults to False + :type reverse: bool, optional + """ setattr(type(self), "reverse", property(fget=lambda x: reverse)) super().__init__(*args, **kwargs) def iter_grid_positions(self, *args, **kwargs) -> Generator: - """Return generator that contains positions of tiles. If reversed property is True, yield in revere order""" + """_summary_ + :yield: _description_ + :rtype: Generator + """ if not self.reverse: for tile in super().iter_grid_positions(*args, **kwargs): yield tile @@ -110,7 +158,9 @@ def iter_grid_positions(self, *args, **kwargs) -> Generator: class VolumePlanWidget(QMainWindow): - """Widget to plan out volume. Grid aspect based on pymmcore GridPlanWidget""" + """ + Widget to plan out volume. Grid aspect based on pymmcore GridPlanWidget. + """ valueChanged = Signal(object) @@ -122,16 +172,18 @@ def __init__( coordinate_plane: list[str, str, str] = None, unit: str = "um", ): - """ - :param limits: 2D list containing min and max stage limits for each coordinate plane in the order of [ - tiling_dim[0], tiling_dim[1], scanning_dim[0]] - :param fov_dimensions: dimensions of field of view in - specified unit in order of [tiling_dim[0], tiling_dim[1], scanning_dim[0]] - :param fov_position: position of - field of view in specified unit in order of [tiling_dim[0], tiling_dim[1], scanning_dim[0]] - :param coordinate_plane: coordinate plane describing the [tiling_dim[0], tiling_dim[1], scanning_dim[0]]. Can - contain negatives. - :param unit: common unit of all arguments. Defaults to um + """_summary_ + + :param limits: _description_, defaults to None + :type limits: list[[float, float], [float, float], [float, float]], optional + :param fov_dimensions: _description_, defaults to None + :type fov_dimensions: list[float, float, float], optional + :param fov_position: _description_, defaults to None + :type fov_position: list[float, float, float], optional + :param coordinate_plane: _description_, defaults to None + :type coordinate_plane: list[str, str, str], optional + :param unit: _description_, defaults to "um" + :type unit: str, optional """ super().__init__() @@ -360,11 +412,11 @@ def __init__( self.update_tile_table(self.value()) # initialize table def update_tile_table(self, value: Union[GridRowsColumns, GridFromEdges, GridWidthHeight]) -> None: - """ - Update tile table when value changes - :param value: newest value containing details of scan - """ + """_summary_ + :param value: _description_ + :type value: Union[GridRowsColumns, GridFromEdges, GridWidthHeight] + """ # check if order changed table_order = [ [int(x) for x in self.tile_table.item(i, 0).text() if x.isdigit()] @@ -398,7 +450,7 @@ def update_tile_table(self, value: Union[GridRowsColumns, GridFromEdges, GridWid # return def refill_table(self) -> None: - """Function to clear and populate tile table with current tile configuration""" + """_summary_""" value = self.value() self.tile_table.clearContents() self.tile_table.setRowCount(0) @@ -412,12 +464,13 @@ def refill_table(self) -> None: self.header.blockSignals(False) def add_tile_to_table(self, row: int, column: int) -> None: - """ - Add a configured tile into tile_table - :param row: row of tile - :param column: column of value - """ + """_summary_ + :param row: _description_ + :type row: int + :param column: _description_ + :type column: int + """ self.tile_table.blockSignals(True) # add new row to table table_row = self.tile_table.rowCount() @@ -462,13 +515,15 @@ def add_tile_to_table(self, row: int, column: int) -> None: self.tile_table.blockSignals(False) def toggle_visibility(self, checked: bool, row: int, column: int) -> None: - """ - Handle visibility checkbox being toggled - :param checked: check state of checkbox - :param row: row of tile - :param column: column of tile - """ + """_summary_ + :param checked: _description_ + :type checked: bool + :param row: _description_ + :type row: int + :param column: _description_ + :type column: int + """ self._tile_visibility[row, column] = checked if self.apply_all and [row, column] == [0, 0]: # trigger update of all subsequent checkboxes for r in range(self.tile_table.rowCount()): @@ -479,11 +534,11 @@ def toggle_visibility(self, checked: bool, row: int, column: int) -> None: self.valueChanged.emit(self.value()) def tile_table_changed(self, item: QTableWidgetItem) -> None: - """ - Update values if item is changed - :param item: item that has been changed - """ + """_summary_ + :param item: _description_ + :type item: QTableWidgetItem + """ row, column = [int(x) for x in self.tile_table.item(item.row(), 0).text() if x.isdigit()] col_title = self.table_columns[item.column()] titles = [f"{self.coordinate_plane[2]} [{self.unit}]", f"{self.coordinate_plane[2]} max [{self.unit}]"] @@ -504,12 +559,13 @@ def tile_table_changed(self, item: QTableWidgetItem) -> None: self.grid_offset_widgets[2].setValue(value) def toggle_grid_position(self, enable: bool, index: Literal[0, 1, 2]) -> None: - """ - Function connected to the anchor checkboxes. If grid is anchored, allow user to input grid position - :param enable: State checkbox was toggled to - :param index: Index of what anchor was checked (0-2) - """ + """_summary_ + :param enable: _description_ + :type enable: bool + :param index: _description_ + :type index: Literal[0, 1, 2] + """ self.grid_offset_widgets[index].setEnabled(enable) if not enable: # Graph is not anchored self.grid_offset_widgets[index].setValue(self.fov_position[index]) @@ -519,19 +575,20 @@ def toggle_grid_position(self, enable: bool, index: Literal[0, 1, 2]) -> None: @property def apply_all(self) -> bool: - """ - Return boolean specifying if settings for the 0, 0 tile apply to all tiles - :return: boolean specifying if settings for the 0, 0 tile apply to all tiles + """_summary_ + + :return: _description_ + :rtype: bool """ return self._apply_all @apply_all.setter def apply_all(self, value: bool) -> None: - """ - Setting for the 0, 0 tile apply all. If True, will update all tiles - :param value: boolean to set apply all - """ + """_summary_ + :param value: _description_ + :type value: bool + """ self._apply_all = value # correctly configure anchor and grid_offset_widget @@ -554,17 +611,20 @@ def apply_all(self, value: bool) -> None: @property def fov_position(self) -> list[float, float, float]: - """ - Current position of the field of view in the specified unit - :return: list of length 3 specifying current position of fov + """_summary_ + + :return: _description_ + :rtype: list[float, float, float] """ return self._fov_position @fov_position.setter def fov_position(self, value: list[float, float, float]) -> None: - """ - Set the current position of the field of view in the specified unit - :param value: list of length 3 specifying new position of fov + """_summary_ + + :param value: _description_ + :type value: list[float, float, float] + :raises ValueError: _description_ """ if type(value) is not list and len(value) != 3: raise ValueError @@ -580,17 +640,20 @@ def fov_position(self, value: list[float, float, float]) -> None: @property def fov_dimensions(self) -> list[float, float, float]: - """ - Returns current field of view dimensions - :return: list of 3 floats defining the field of view dimensions + """_summary_ + + :return: _description_ + :rtype: list[float, float, float] """ return self._fov_dimensions @fov_dimensions.setter def fov_dimensions(self, value: list[float, float, float]) -> None: - """ - Setting the fov dimension in the specified unit - :param value: list of length 3 specifying dimension for field of view + """_summary_ + + :param value: _description_ + :type value: list[float, float, float] + :raises ValueError: _description_ """ if type(value) is not list and len(value) != 2: raise ValueError @@ -599,14 +662,20 @@ def fov_dimensions(self, value: list[float, float, float]) -> None: @property def grid_offset(self) -> list[float, float, float]: - """Returns off set from 0 of tile positions""" + """_summary_ + + :return: _description_ + :rtype: list[float, float, float] + """ return self._grid_offset @grid_offset.setter def grid_offset(self, value: list[float, float, float]) -> None: - """ - Setting offset from 0 of tile positions in the 3 dimensions of coordinate plane - :param value: a list of len 3 specifying offset for tile starts + """_summary_ + + :param value: _description_ + :type value: list[float, float, float] + :raises ValueError: _description_ """ if type(value) is not list and len(value) != 3: raise ValueError @@ -616,11 +685,11 @@ def grid_offset(self, value: list[float, float, float]) -> None: @property def tile_positions(self) -> [[float, float, float]]: - """ - Creates 3d list of tile positions based on widget values - :return: 3D list of tile coordinates - """ + """_summary_ + :return: _description_ + :rtype: [[float, float, float]] + """ value = self.value() coords = np.zeros((value.rows, value.columns, 3)) if self._mode != "bounds": @@ -637,35 +706,36 @@ def tile_positions(self) -> [[float, float, float]]: @property def tile_visibility(self) -> np.ndarray: - """ - 2D matrix of boolean values specifying if tile should be visible - :return: 2D numpy array containing the start coordinates where the i, j position of start coordinates correlates - to the i, j position of tile in scan + """_summary_ + + :return: _description_ + :rtype: np.ndarray """ return self._tile_visibility @property def scan_starts(self) -> np.ndarray: - """ - 2D matrix of tile start position in scan dimension - :return: 2D numpy array containing the start coordinates where the i, j position of start coordinates correlates - to the i, j position of tile in scan + """_summary_ + + :return: _description_ + :rtype: np.ndarray """ return self._scan_starts @property def scan_ends(self) -> np.ndarray: - """ - 2D matrix of tile start position in scan dimension - :return: 2D numpy array containing the end coordinates where the i, j position of end coordinates correlates to - the i, j position of tile in scan + """_summary_ + + :return: _description_ + :rtype: np.ndarray """ return self._scan_ends def _on_change(self) -> None: - """ - Function called when things are changed within the widget. Handles formatting start, end, and visibility - of tiles and emits signal when done. + """_summary_ + + :return: _description_ + :rtype: _type_ """ if (val := self.value()) is None: return # pragma: no cover @@ -679,18 +749,21 @@ def _on_change(self) -> None: @property def mode(self) -> Literal["number", "area", "bounds"]: - """Mode used to calculate tile position - :return: current mode of widget + """_summary_ + + :return: _description_ + :rtype: _type_ """ return self._mode @mode.setter def mode(self, value: Literal["number", "area", "bounds"]) -> None: - """ - Set mode of widget - :param value: value to change mode to. Must be 'number', 'area', or 'bounds' - """ + """_summary_ + :param value: _description_ + :type value: Literal["number", "area", "bounds"] + :raises ValueError: _description_ + """ if value not in ["number", "area", "bounds"]: raise ValueError self._mode = value @@ -710,9 +783,11 @@ def mode(self, value: Literal["number", "area", "bounds"]) -> None: self._on_change() def value(self) -> Union[GridRowsColumns, GridFromEdges, GridWidthHeight]: - """ - Value based on widget values - :return: value containing information about tiles + """_summary_ + + :raises NotImplementedError: _description_ + :return: _description_ + :rtype: Union[GridRowsColumns, GridFromEdges, GridWidthHeight] """ over = self.overlap.value() common = { @@ -748,6 +823,11 @@ def value(self) -> Union[GridRowsColumns, GridFromEdges, GridWidthHeight]: def line(): + """_summary_ + + :return: _description_ + :rtype: _type_ + """ frame = QFrame() frame.setFrameShape(QFrame.HLine) return frame diff --git a/src/view/widgets/base_device_widget.py b/src/view/widgets/base_device_widget.py index 7f24998..611b357 100644 --- a/src/view/widgets/base_device_widget.py +++ b/src/view/widgets/base_device_widget.py @@ -1,41 +1,46 @@ -from qtpy.QtCore import Signal, Slot, QTimer -from qtpy.QtGui import QIntValidator, QDoubleValidator +import enum +import inspect +import logging +import re +import types +from importlib import import_module +from inspect import currentframe + +import inflection +from qtpy.QtCore import QTimer, Signal, Slot +from qtpy.QtGui import QDoubleValidator, QIntValidator from qtpy.QtWidgets import ( - QWidget, - QLabel, QComboBox, + QDoubleSpinBox, QHBoxLayout, - QVBoxLayout, - QMainWindow, + QLabel, QLineEdit, - QSpinBox, - QDoubleSpinBox, + QMainWindow, QSlider, + QSpinBox, + QVBoxLayout, + QWidget, ) -from inspect import currentframe -from importlib import import_module -import enum -import types -import re -import logging -import inflection -from view.widgets.miscellaneous_widgets.q_scrollable_line_edit import QScrollableLineEdit -from view.widgets.miscellaneous_widgets.q_scrollable_float_slider import QScrollableFloatSlider -import inspect from schema import Schema, SchemaError +from view.widgets.miscellaneous_widgets.q_scrollable_float_slider import QScrollableFloatSlider +from view.widgets.miscellaneous_widgets.q_scrollable_line_edit import QScrollableLineEdit + class BaseDeviceWidget(QMainWindow): + """_summary_""" + ValueChangedOutside = Signal((str,)) ValueChangedInside = Signal((str,)) def __init__(self, device_type: object = None, properties: dict = {}): - """Base widget for devices like camera, laser, stage, ect. Widget will scan properties of - device object and create editable inputs for each if not in device_widgets class of device. If no device_widgets - class is provided, then all properties are exposed - :param device_type: type of class or dictionary of device object - :param properties: dictionary contain properties displayed in widget as keys and initial values as values""" + """_summary_ + :param device_type: _description_, defaults to None + :type device_type: object, optional + :param properties: _description_, defaults to {} + :type properties: dict, optional + """ self.log = logging.getLogger(f"{__name__}.{self.__class__.__name__}") super().__init__() @@ -52,10 +57,15 @@ class is provided, then all properties are exposed self.ValueChangedOutside[str].connect(self.update_property_widget) # Trigger update when property value changes def create_property_widgets(self, properties: dict, widget_group): - """Create input widgets based on properties - :param properties: dictionary containing properties within a class and mapping to values - :param widget_group: attribute name for dictionary of widgets""" - + """_summary_ + + :param properties: _description_ + :type properties: dict + :param widget_group: _description_ + :type widget_group: _type_ + :return: _description_ + :rtype: _type_ + """ widgets = {} for name, value in properties.items(): setattr(self, name, value) # Add device properties as widget properties @@ -109,11 +119,17 @@ def create_property_widgets(self, properties: dict, widget_group): return widgets def create_attribute_widget(self, name, widget_type, values): - """Create a widget and create corresponding attribute - :param name: name of property - :param widget_type: widget type (QLineEdit or QCombobox) - :param values: input into widget""" - + """_summary_ + + :param name: _description_ + :type name: _type_ + :param widget_type: _description_ + :type widget_type: _type_ + :param values: _description_ + :type values: _type_ + :return: _description_ + :rtype: _type_ + """ # options = values.keys() if widget_type == 'combo' else values box = getattr(self, f"create_{widget_type}_box")(name, values) setattr(self, f"{name}_widget", box) # add attribute for widget input for easy access @@ -121,10 +137,13 @@ def create_attribute_widget(self, name, widget_type, values): return box def check_driver_variables(self, name: str): - """Check if there is variable in device driver that has name of - property to inform input widget type and values - :param name: name of property to search for""" + """_summary_ + :param name: _description_ + :type name: str + :return: _description_ + :rtype: _type_ + """ driver_vars = self.device_driver.__dict__ for variable in driver_vars: search_name = inflection.pluralize(name.replace(".", "_")) @@ -137,10 +156,15 @@ def check_driver_variables(self, name: str): return {i.name: i.value for i in enum_class} def create_text_box(self, name, value): - """Convenience function to build editable text boxes and add initial value and validator - :param name: name to emit when text is edited is changed - :param value: initial value to add to box""" - + """_summary_ + + :param name: _description_ + :type name: _type_ + :param value: _description_ + :type value: _type_ + :return: _description_ + :rtype: _type_ + """ # TODO: better way to handle weird types that will crash QT? value_type = type(value) textbox = QScrollableLineEdit(str(value)) @@ -157,12 +181,11 @@ def create_text_box(self, name, value): return textbox def textbox_edited(self, name): - """ - Correctly set attribute after textbox has been edited - :param name: name of property that was edited - :return: - """ + """_summary_ + :param name: _description_ + :type name: _type_ + """ name_lst = name.split(".") parent_attr = pathGet(self.__dict__, name_lst[0:-1]) value = getattr(self, name + "_widget").text() @@ -175,10 +198,15 @@ def textbox_edited(self, name): self.ValueChangedInside.emit(name) def create_combo_box(self, name, items): - """Convenience function to build combo boxes and add items - :param name: name to emit when combobox index is changed - :param items: items to add to combobox""" - + """_summary_ + + :param name: _description_ + :type name: _type_ + :param items: _description_ + :type items: _type_ + :return: _description_ + :rtype: _type_ + """ options = items.keys() if hasattr(items, "keys") else items box = QComboBox() box.addItems([str(x) for x in options]) @@ -188,13 +216,13 @@ def create_combo_box(self, name, items): return box def combo_box_changed(self, value, name): - """ - Correctly set attribute after combobox index has been changed - :param value: new value combobox has been changed to - :param name: name of property that was edited - :return: - """ + """_summary_ + :param value: _description_ + :type value: _type_ + :param name: _description_ + :type name: _type_ + """ name_lst = name.split(".") parent_attr = pathGet(self.__dict__, name_lst[0:-1]) @@ -210,9 +238,11 @@ def combo_box_changed(self, value, name): @Slot(str) def update_property_widget(self, name): - """Update property widget. Triggers when attribute has been changed outside of widget - :param name: name of attribute and widget""" + """_summary_ + :param name: _description_ + :type name: _type_ + """ value = getattr(self, name, None) if dict not in type(value).__mro__ and list not in type(value).__mro__: # not a dictionary or list like value self._set_widget_text(name, value) @@ -227,10 +257,13 @@ def update_property_widget(self, name): self.update_property_widget(f"{name}.{i}") def _set_widget_text(self, name, value): - """Set widget text based on widget type - :param name: widget name to set text to - :param value: value of text""" + """_summary_ + :param name: _description_ + :type name: _type_ + :param value: _description_ + :type value: _type_ + """ if hasattr(self, f"{name}_widget"): widget = getattr(self, f"{name}_widget") widget.blockSignals(True) # block signal indicating change since changing internally @@ -250,7 +283,13 @@ def _set_widget_text(self, name, value): self.log.warning(f"{name} doesn't correspond to a widget") def __setattr__(self, name, value): - """Overwrite __setattr__ to trigger update if property is changed""" + """_summary_ + + :param name: _description_ + :type name: _type_ + :param value: _description_ + :type value: _type_ + """ # check that values adhere to schema of correlating variable if f"{name}_schema" in self.__dict__.keys(): schema = getattr(self, f"{name}_schema") @@ -267,10 +306,12 @@ def __setattr__(self, name, value): # Convenience Functions def create_dict_schema(dictionary: dict): - """ - Helper function to create a schema for a dictionary object - :param dictionary: dictionary to create schema from - :return: schema of dictionary + """_summary_ + + :param dictionary: _description_ + :type dictionary: dict + :return: _description_ + :rtype: _type_ """ schema = {} for key, value in dictionary.items(): @@ -285,10 +326,12 @@ def create_dict_schema(dictionary: dict): def create_list_schema(list_ob: dict): - """ - Helper function to create a schema for a list object - :param list_ob: list to create schema from - :return: schema of list_ob + """_summary_ + + :param list_ob: _description_ + :type list_ob: dict + :return: _description_ + :rtype: _type_ """ schema = [] for value in list_ob: @@ -302,6 +345,15 @@ def create_list_schema(list_ob: dict): def check_if_valid(schema, item): + """_summary_ + + :param schema: _description_ + :type schema: _type_ + :param item: _description_ + :type item: _type_ + :return: _description_ + :rtype: _type_ + """ try: schema.validate(item) return True @@ -310,11 +362,13 @@ def check_if_valid(schema, item): def create_widget(struct: str, *args, **kwargs): - """Creates either a horizontal or vertical layout populated with widgets - :param struct: specifies whether the layout will be horizontal, vertical, or combo - :param kwargs: all widgets contained in layout - :return QWidget()""" + """_summary_ + :param struct: _description_ + :type struct: str + :return: _description_ + :rtype: _type_ + """ layouts = {"H": QHBoxLayout(), "V": QVBoxLayout()} widget = QWidget() if struct == "V" or struct == "H": @@ -344,10 +398,13 @@ def create_widget(struct: str, *args, **kwargs): def label_maker(string): - """Removes underscores from variable names and capitalizes words - :param string: string to make label out of - """ + """_summary_ + :param string: _description_ + :type string: _type_ + :return: _description_ + :rtype: _type_ + """ possible_units = ["mm", "um", "px", "mW", "W", "ms", "C", "V", "us"] label = string.split("_") label = [words.capitalize() for words in label] @@ -362,8 +419,15 @@ def label_maker(string): def pathGet(iterable: dict or list, path: list): - """Based on list of nested dictionary keys or list indices, return inner dictionary""" - + """_summary_ + + :param iterable: _description_ + :type iterable: dictorlist + :param path: _description_ + :type path: list + :return: _description_ + :rtype: _type_ + """ for k in path: k = int(k) if type(iterable) == list else k iterable = iterable.__getitem__(k) @@ -371,10 +435,13 @@ def pathGet(iterable: dict or list, path: list): def scan_for_properties(device): - """Scan for properties with setters and getters in class and return dictionary - :param device: object to scan through for properties - """ + """_summary_ + :param device: _description_ + :type device: _type_ + :return: _description_ + :rtype: _type_ + """ prop_dict = {} for attr_name in dir(device): try: @@ -388,7 +455,12 @@ def scan_for_properties(device): def disable_button(button, pause=1000): - """Function to disable button clicks for a period of time to avoid crashing gui""" + """_summary_ + :param button: _description_ + :type button: _type_ + :param pause: _description_, defaults to 1000 + :type pause: int, optional + """ button.setEnabled(False) QTimer.singleShot(pause, lambda: button.setDisabled(False)) diff --git a/src/view/widgets/device_widgets/camera_widget.py b/src/view/widgets/device_widgets/camera_widget.py index e461a6e..76bdf7a 100644 --- a/src/view/widgets/device_widgets/camera_widget.py +++ b/src/view/widgets/device_widgets/camera_widget.py @@ -1,32 +1,33 @@ -from view.widgets.base_device_widget import BaseDeviceWidget, create_widget, scan_for_properties -from qtpy.QtWidgets import QPushButton, QStyle, QWidget, QHBoxLayout from qtpy.QtCore import Qt +from qtpy.QtWidgets import QPushButton, QStyle, QWidget + +from view.widgets.base_device_widget import BaseDeviceWidget, create_widget, scan_for_properties class CameraWidget(BaseDeviceWidget): + """_summary_""" - def __init__(self, camera, - advanced_user: bool = True): - """Modify BaseDeviceWidget to be specifically for camera. Main need are adding roi validator, - live view button, and snapshot button. - :param camera: camera object - :param advanced_user: boolean specifying complexity of widget. If True, all property widget of camera will be - hidden and only the snapshot and live button will be shown. - """ + def __init__(self, camera, advanced_user: bool = True): + """_summary_ + :param camera: _description_ + :type camera: _type_ + :param advanced_user: _description_, defaults to True + :type advanced_user: bool, optional + """ self.camera_properties = scan_for_properties(camera) - del self.camera_properties['latest_frame'] # remove image property + del self.camera_properties["latest_frame"] # remove image property super().__init__(type(camera), self.camera_properties) - if not advanced_user: # hide widgets + if not advanced_user: # hide widgets for widget in self.property_widgets.values(): widget.setVisible(False) # create and format livestream button and snapshot button self.live_button = self.create_live_button() self.snapshot_button = self.create_snapshot_button() - picture_buttons = create_widget('H', self.live_button, self.snapshot_button) + picture_buttons = create_widget("H", self.live_button, self.snapshot_button) if advanced_user: # Format widgets better in advaced user mode @@ -34,87 +35,94 @@ def __init__(self, camera, direct = Qt.FindDirectChildrenOnly # reformat binning and pixel type - pixel_widgets = create_widget('VH', - *self.property_widgets.get('binning', _).findChildren(QWidget, - options=direct), - *self.property_widgets.get('pixel_type', _).findChildren(QWidget, - options=direct)) + pixel_widgets = create_widget( + "VH", + *self.property_widgets.get("binning", _).findChildren(QWidget, options=direct), + *self.property_widgets.get("pixel_type", _).findChildren(QWidget, options=direct), + ) # check if properties have setters and if not, disable widgets. Have to do it inside pixel widget - for i, prop in enumerate(['binning', 'pixel_type']): + for i, prop in enumerate(["binning", "pixel_type"]): attr = getattr(type(camera), prop) - if getattr(attr, 'fset', None) is None: + if getattr(attr, "fset", None) is None: pixel_widgets.children()[i + 1].setEnabled(False) # reformat timing widgets - timing_widgets = create_widget('VH', - *self.property_widgets.get('exposure_time_ms', _).findChildren(QWidget, - options=direct), - *self.property_widgets.get('frame_time_ms', _).findChildren(QWidget, - options=direct), - *self.property_widgets.get('line_interval_us', _).findChildren(QWidget, - options=direct)) + timing_widgets = create_widget( + "VH", + *self.property_widgets.get("exposure_time_ms", _).findChildren(QWidget, options=direct), + *self.property_widgets.get("frame_time_ms", _).findChildren(QWidget, options=direct), + *self.property_widgets.get("line_interval_us", _).findChildren(QWidget, options=direct), + ) # check if properties have setters and if not, disable widgets - for i, prop in enumerate(['exposure_time_ms', 'frame_time_ms', 'line_interval_us']): + for i, prop in enumerate(["exposure_time_ms", "frame_time_ms", "line_interval_us"]): attr = getattr(type(camera), prop, False) - if getattr(attr, 'fset', None) is None: + if getattr(attr, "fset", None) is None: timing_widgets.children()[i + 1].setEnabled(False) # reformat sensor height and width widget - sensor_size_widget = create_widget('VH', - *self.property_widgets.get('sensor_width_px', _).findChildren(QWidget, - options=direct), - *self.property_widgets.get('sensor_height_px', _).findChildren(QWidget, - options=direct)) + sensor_size_widget = create_widget( + "VH", + *self.property_widgets.get("sensor_width_px", _).findChildren(QWidget, options=direct), + *self.property_widgets.get("sensor_height_px", _).findChildren(QWidget, options=direct), + ) # check if properties have setters and if not, disable widgets - for i, prop in enumerate(['sensor_width_px', 'sensor_height_px']): + for i, prop in enumerate(["sensor_width_px", "sensor_height_px"]): attr = getattr(type(camera), prop, False) - if getattr(attr, 'fset', None) is None: + if getattr(attr, "fset", None) is None: sensor_size_widget.children()[i + 1].setEnabled(False) # reformat roi widget - self.roi_widget = create_widget('VH', - *self.property_widgets.get('width_px', _).findChildren(QWidget, - options=direct), - *self.property_widgets.get('width_offset_px', _).findChildren(QWidget, - options=direct), - *self.property_widgets.get('height_px', _).findChildren(QWidget, - options=direct), - *self.property_widgets.get('height_offset_px', _).findChildren(QWidget, - options=direct)) + self.roi_widget = create_widget( + "VH", + *self.property_widgets.get("width_px", _).findChildren(QWidget, options=direct), + *self.property_widgets.get("width_offset_px", _).findChildren(QWidget, options=direct), + *self.property_widgets.get("height_px", _).findChildren(QWidget, options=direct), + *self.property_widgets.get("height_offset_px", _).findChildren(QWidget, options=direct), + ) self.roi_widget.setContentsMargins(0, 0, 0, 0) # check if properties have setters and if not, disable widgets - for i, prop in enumerate(['width_px', 'width_offset_px', 'height_px', 'height_offset_px']): + for i, prop in enumerate(["width_px", "width_offset_px", "height_px", "height_offset_px"]): attr = getattr(type(camera), prop, False) - if getattr(attr, 'fset', None) is None: + if getattr(attr, "fset", None) is None: self.roi_widget.children()[i + 1].setEnabled(False) central_widget = self.centralWidget() central_widget.layout().setSpacing(0) # remove space between central widget and newly formatted widgets - self.setCentralWidget(create_widget('V', - picture_buttons, - pixel_widgets, - timing_widgets, - self.roi_widget, - sensor_size_widget, - central_widget)) - else: # add snapshot button and liveview + self.setCentralWidget( + create_widget( + "V", + picture_buttons, + pixel_widgets, + timing_widgets, + self.roi_widget, + sensor_size_widget, + central_widget, + ) + ) + else: # add snapshot button and liveview central_widget = self.centralWidget() central_widget.layout().setSpacing(0) # remove space between central widget and newly formatted widgets - self.setCentralWidget(create_widget('H',self.live_button, self.snapshot_button)) + self.setCentralWidget(create_widget("H", self.live_button, self.snapshot_button)) def create_live_button(self) -> QPushButton: - """Add live button""" + """_summary_ - button = QPushButton('Live') + :return: _description_ + :rtype: QPushButton + """ + button = QPushButton("Live") icon = self.style().standardIcon(QStyle.StandardPixmap.SP_MediaPlay) button.setIcon(icon) return button def create_snapshot_button(self) -> QPushButton: - """Add snapshot button""" + """_summary_ - button = QPushButton('Snapshot') + :return: _description_ + :rtype: QPushButton + """ + button = QPushButton("Snapshot") # icon = self.style().standardIcon(QStyle.StandardPixmap.SP_MediaPlay) # button.setIcon(icon) return button diff --git a/src/view/widgets/device_widgets/filter_wheel_widget.py b/src/view/widgets/device_widgets/filter_wheel_widget.py index e7d913a..5205895 100644 --- a/src/view/widgets/device_widgets/filter_wheel_widget.py +++ b/src/view/widgets/device_widgets/filter_wheel_widget.py @@ -1,33 +1,37 @@ -from pyqtgraph import PlotWidget, TextItem, mkPen, mkBrush, ScatterPlotItem, setConfigOptions -from qtpy.QtWidgets import QGraphicsEllipseItem, QComboBox -from qtpy.QtCore import Signal, QTimer, Property, QObject, Slot -from math import sin, cos, pi, atan, degrees, radians -from qtpy.QtGui import QFont, QColor -from view.widgets.base_device_widget import BaseDeviceWidget, scan_for_properties +from math import atan, cos, degrees, pi, radians, sin from typing import Callable, Union +from pyqtgraph import (PlotWidget, ScatterPlotItem, TextItem, mkBrush, mkPen, + setConfigOptions) +from qtpy.QtCore import Property, QObject, QTimer, Signal, Slot +from qtpy.QtGui import QColor, QFont +from qtpy.QtWidgets import QComboBox, QGraphicsEllipseItem + +from view.widgets.base_device_widget import (BaseDeviceWidget, + scan_for_properties) + setConfigOptions(antialias=True) class FilterWheelWidget(BaseDeviceWidget): + """_summary_""" - def __init__(self, filter_wheel, - colors: dict = None, - advanced_user: bool = True): - """ - Simple scroll widget for filter wheel - :param filter_wheel: filter wheel object - :param colors: colors for filters. Defaults to empty dictionary if not specified - :param advanced_user: boolean specifying complexity of widget. If False, user won't be able to manually change - filter wheel but graphics will still work. - """ + def __init__(self, filter_wheel, colors: dict = None, advanced_user: bool = True): + """_summary_ + :param filter_wheel: _description_ + :type filter_wheel: _type_ + :param colors: _description_, defaults to None + :type colors: dict, optional + :param advanced_user: _description_, defaults to True + :type advanced_user: bool, optional + """ properties = scan_for_properties(filter_wheel) # wrap filterwheel filter property to emit signal when set - filter_setter = getattr(type(filter_wheel).filter, 'fset') - filter_getter = getattr(type(filter_wheel).filter, 'fget') - setattr(type(filter_wheel), 'filter', property(filter_getter, self.filter_change_wrapper(filter_setter))) + filter_setter = getattr(type(filter_wheel).filter, "fset") + filter_getter = getattr(type(filter_wheel).filter, "fget") + setattr(type(filter_wheel), "filter", property(filter_getter, self.filter_change_wrapper(filter_setter))) super().__init__(type(filter_wheel), properties) @@ -38,18 +42,22 @@ def __init__(self, filter_wheel, # recreate as combo box with filters as options self.filter_widget = QComboBox() - self.filter_widget.addItems([f'{v}: {k}' for k, v in self.filters.items()]) - self.filter_widget.currentTextChanged.connect(lambda val: setattr(self, 'filter', val[val.index(' ')+1:])) - self.filter_widget.currentTextChanged.connect(lambda: self.ValueChangedInside.emit('filter')) - self.filter_widget.setCurrentText(f'{self.filters[filter_wheel.filter]}: {filter_wheel.filter}') + self.filter_widget.addItems([f"{v}: {k}" for k, v in self.filters.items()]) + self.filter_widget.currentTextChanged.connect(lambda val: setattr(self, "filter", val[val.index(" ") + 1 :])) + self.filter_widget.currentTextChanged.connect(lambda: self.ValueChangedInside.emit("filter")) + self.filter_widget.setCurrentText(f"{self.filters[filter_wheel.filter]}: {filter_wheel.filter}") # Add back to property widget - self.property_widgets['filter'].layout().addWidget(self.filter_widget) + self.property_widgets["filter"].layout().addWidget(self.filter_widget) # Create wheel widget and connect to signals self.wheel_widget = FilterWheelGraph(self.filters, colors if colors else {}) - self.wheel_widget.ValueChangedInside[str].connect(lambda v: self.filter_widget.setCurrentText(f'{self.filters[v]}: {v}')) - self.filter_widget.currentTextChanged.connect(lambda val: self.wheel_widget.move_wheel(val[val.index(' ')+1:])) + self.wheel_widget.ValueChangedInside[str].connect( + lambda v: self.filter_widget.setCurrentText(f"{self.filters[v]}: {v}") + ) + self.filter_widget.currentTextChanged.connect( + lambda val: self.wheel_widget.move_wheel(val[val.index(" ") + 1 :]) + ) self.ValueChangedOutside[str].connect(lambda name: self.wheel_widget.move_wheel(self.filter)) self.centralWidget().layout().addWidget(self.wheel_widget) @@ -58,25 +66,41 @@ def __init__(self, filter_wheel, self.filter_widget.setDisabled(True) def filter_change_wrapper(self, func: Callable) -> Callable: - """ - Wrapper function that emits a signal when filterwheel filter setter has been called - :param func: setter of filter wheel object + """_summary_ + + :param func: _description_ + :type func: Callable + :return: _description_ + :rtype: Callable """ def wrapper(object, value): + """_summary_ + + :param object: _description_ + :type object: _type_ + :param value: _description_ + :type value: _type_ + """ func(object, value) self.filter = value - self.ValueChangedOutside[str].emit('filter') + self.ValueChangedOutside[str].emit("filter") return wrapper + class FilterItem(ScatterPlotItem): - """ScatterPlotItem that will emit signal when pressed""" + """ + ScatterPlotItem that will emit signal when pressed. + """ + pressed = Signal(str) def __init__(self, filter_name: str, *args, **kwargs): - """ - :param filter_name: name of filter that will be emitted when pressed + """_summary_ + + :param filter_name: _description_ + :type filter_name: str """ self.filter_name = filter_name super().__init__(*args, **kwargs) @@ -89,24 +113,28 @@ def mousePressEvent(self, ev) -> None: super().mousePressEvent(ev) self.pressed.emit(self.filter_name) + class FilterWheelGraph(PlotWidget): + """_summary_""" + ValueChangedInside = Signal((str,)) - def __init__(self, filters: dict, colors: dict, diameter: float = 10.0 , **kwargs): - """ - Plot widget that creates a visual of filter wheel using points on graph - :param filters: list possible filters - :param colors: desired colors of filters where key is filter name and value is string of color. If empty dict, - default is grey. - :param diameter: desired diameter of wheel graphic - """ + def __init__(self, filters: dict, colors: dict, diameter: float = 10.0, **kwargs): + """_summary_ + :param filters: _description_ + :type filters: dict + :param colors: _description_ + :type colors: dict + :param diameter: _description_, defaults to 10.0 + :type diameter: float, optional + """ super().__init__(**kwargs) self._timelines = [] self.setMouseEnabled(x=False, y=False) self.showAxes(False, False) - self.setBackground('#262930') + self.setBackground("#262930") self.filters = filters self.diameter = diameter @@ -120,14 +148,14 @@ def __init__(self, filters: dict, colors: dict, diameter: float = 10.0 , **kwarg self.filter_path = self.diameter - 3 # calculate diameter of filters based on quantity l = len(self.filters) - max_diameter = (self.diameter - self.filter_path - .5) * 2 + max_diameter = (self.diameter - self.filter_path - 0.5) * 2 del_filter = self.filter_path * cos((pi / 2) - (2 * pi / l)) - max_diameter # dist between two filter points filter_diameter = max_diameter if del_filter > 0 or l == 2 else self.filter_path * cos((pi / 2) - (2 * pi / l)) - angles = [pi / 2+(2 * pi / l * i) for i in range(l)] + angles = [pi / 2 + (2 * pi / l * i) for i in range(l)] self.points = {} for angle, (filter, i) in zip(angles, self.filters.items()): - color = QColor(colors.get(filter, 'black')).getRgb() + color = QColor(colors.get(filter, "black")).getRgb() pos = [self.filter_path * cos(angle), self.filter_path * sin(angle)] # create scatter point filter point = FilterItem(filter_name=filter, size=filter_diameter, pxMode=False, pos=[pos]) @@ -138,7 +166,7 @@ def __init__(self, filters: dict, colors: dict, diameter: float = 10.0 , **kwarg self.points[filter] = point # create label - index = TextItem(text=str(i), anchor=(.5, .5), color='white') + index = TextItem(text=str(i), anchor=(0.5, 0.5), color="white") font = QFont() font.setPointSize(round(filter_diameter**2)) index.setFont(font) @@ -147,9 +175,10 @@ def __init__(self, filters: dict, colors: dict, diameter: float = 10.0 , **kwarg self.points[i] = index # create active wheel graphic. Add after to display over filters - active = ScatterPlotItem(size=2, pxMode=False, symbol='t1', pos=[[self.diameter * cos(pi / 2), - self.diameter * sin(pi / 2)]]) - black = QColor('black').getRgb() + active = ScatterPlotItem( + size=2, pxMode=False, symbol="t1", pos=[[self.diameter * cos(pi / 2), self.diameter * sin(pi / 2)]] + ) + black = QColor("black").getRgb() active.setPen(mkPen(black)) # outline active.setBrush(mkBrush(black)) # color self.addItem(active) @@ -157,25 +186,26 @@ def __init__(self, filters: dict, colors: dict, diameter: float = 10.0 , **kwarg self.setAspectLocked(1) def move_wheel(self, name: str) -> None: - """ - Create visual of wheel moving to new filer by changing position of filter points - :param name: name of active filter + """_summary_ + + :param name: _description_ + :type name: str """ self.ValueChangedInside.emit(name) point = self.points[name] - filter_pos = [point.getData()[0][0],point.getData()[1][0]] + filter_pos = [point.getData()[0][0], point.getData()[1][0]] notch_pos = [self.diameter * cos(pi / 2), self.diameter * sin(pi / 2)] thetas = [] - for x,y in [filter_pos, notch_pos]: + for x, y in [filter_pos, notch_pos]: if y > 0 > x or (y < 0 and x < 0): - thetas.append(180+degrees(atan(y/x))) + thetas.append(180 + degrees(atan(y / x))) elif y < 0 < x: - thetas.append(360+degrees(atan(y/x))) + thetas.append(360 + degrees(atan(y / x))) else: - thetas.append(degrees(atan(y/x))) + thetas.append(degrees(atan(y / x))) filter_theta, notch_theta = thetas - delta_theta = notch_theta-filter_theta + delta_theta = notch_theta - filter_theta if notch_theta > filter_theta and delta_theta <= 180: step_size = 1 elif notch_theta > filter_theta and delta_theta > 180: @@ -192,12 +222,14 @@ def move_wheel(self, name: str) -> None: # create timelines for all filters and labels filter_names = list(self.filters.keys()) filter_index = filter_names.index(name) - filters = [filter_names[(filter_index+i) % len(filter_names)] for i in range(len(filter_names))] # reorder filters starting with filter selected + filters = [ + filter_names[(filter_index + i) % len(filter_names)] for i in range(len(filter_names)) + ] # reorder filters starting with filter selected del_theta = 2 * pi / len(filters) for i, filt in enumerate(filters): - shift = degrees((del_theta*i)) + shift = degrees((del_theta * i)) timeline = TimeLine(loopCount=1, interval=10, step_size=step_size) - timeline.setFrameRange(filter_theta+shift, notch_theta+shift) + timeline.setFrameRange(filter_theta + shift, notch_theta + shift) timeline.frameChanged.connect(lambda i, slot=self.points[filt]: self.move_point(i, slot)) timeline.frameChanged.connect(lambda i, slot=self.points[self.filters[filt]]: self.move_point(i, slot)) self._timelines.append(timeline) @@ -208,29 +240,38 @@ def move_wheel(self, name: str) -> None: @Slot(float) def move_point(self, angle: float, point: Union[FilterItem, TextItem]) -> None: + """_summary_ + + :param angle: _description_ + :type angle: float + :param point: _description_ + :type point: Union[FilterItem, TextItem] """ - Calculate new position of point based on the angle given - :param angle: angle in degrees - :param point: point needing to be moved - """ - pos = [self.filter_path * cos(radians(angle)), - self.filter_path * sin(radians(angle))] + pos = [self.filter_path * cos(radians(angle)), self.filter_path * sin(radians(angle))] if type(point) == FilterItem: point.setData(pos=[pos]) elif type(point) == TextItem: point.setPos(*pos) + class TimeLine(QObject): - """QObject that steps through values over a period of time and emits values at set interval""" + """ + QObject that steps through values over a period of time and emits values at set interval. + """ frameChanged = Signal(float) - def __init__(self, interval: int = 60, loopCount: int = 1, step_size: float =1, parent=None): - """ - :param interval: interval at which to step up and emit value in milliseconds - :param loopCount: how many times to repeat timeline - :param step_size: step size to take between emitted values - :param parent: parent of widget + def __init__(self, interval: int = 60, loopCount: int = 1, step_size: float = 1, parent=None): + """_summary_ + + :param interval: _description_, defaults to 60 + :type interval: int, optional + :param loopCount: _description_, defaults to 1 + :type loopCount: int, optional + :param step_size: _description_, defaults to 1 + :type step_size: float, optional + :param parent: _description_, defaults to None + :type parent: _type_, optional """ super(TimeLine, self).__init__(parent) self._stepSize = step_size @@ -244,10 +285,11 @@ def __init__(self, interval: int = 60, loopCount: int = 1, step_size: float =1, def on_timeout(self) -> None: """ - Function called by Qtimer that will trigger a step of current step_size and emit new counter value + Function called by Qtimer that will trigger a step of current step_size and emit new counter value. """ - if (self._startFrame <= self._counter <= self._endFrame and self._stepSize > 0) or \ - (self._startFrame >= self._counter >= self._endFrame and self._stepSize < 0): + if (self._startFrame <= self._counter <= self._endFrame and self._stepSize > 0) or ( + self._startFrame >= self._counter >= self._endFrame and self._stepSize < 0 + ): self.frameChanged.emit(self._counter) self._counter += self._stepSize else: @@ -258,42 +300,48 @@ def on_timeout(self) -> None: self._timer.stop() def setLoopCount(self, loopCount: int) -> None: - """ - Function set loop count variable - :param loopCount: integer specifying how many times to repeat timeline + """_summary_ + + :param loopCount: _description_ + :type loopCount: int """ self._loopCount = loopCount def loopCount(self) -> int: - """ - Current loop count - :return: Current loop count + """_summary_ + + :return: _description_ + :rtype: int """ return self._loopCount interval = Property(int, fget=loopCount, fset=setLoopCount) def setInterval(self, interval: int) -> None: - """ - Function to set interval variable in seconds - :param interval: integer specifying the length of timeline in milliseconds + """_summary_ + + :param interval: _description_ + :type interval: int """ self._timer.setInterval(interval) def interval(self) -> int: - """ - Current interval time in milliseconds - :return: integer value of current interval time in milliseconds + """_summary_ + + :return: _description_ + :rtype: int """ return self._timer.interval() interval = Property(int, fget=interval, fset=setInterval) def setFrameRange(self, startFrame: float, endFrame: float) -> None: - """ - Setting function for starting and end value that timeline will step through - :param startFrame: starting value - :param endFrame: ending value + """_summary_ + + :param startFrame: _description_ + :type startFrame: float + :param endFrame: _description_ + :type endFrame: float """ self._startFrame = startFrame self._endFrame = endFrame @@ -301,12 +349,12 @@ def setFrameRange(self, startFrame: float, endFrame: float) -> None: @Slot() def start(self) -> None: """ - Function to start QTimer and begin emitting and stepping through value + Function to start QTimer and begin emitting and stepping through value. """ self._counter = self._startFrame self._loop_counter = 0 self._timer.start() def stop(self) -> None: - """Function to stop QTimer and stop stepping through values""" + """Function to stop QTimer and stop stepping through values.""" self._timer.stop() diff --git a/src/view/widgets/device_widgets/joystick_widget.py b/src/view/widgets/device_widgets/joystick_widget.py index 978ef2e..d9e0b21 100644 --- a/src/view/widgets/device_widgets/joystick_widget.py +++ b/src/view/widgets/device_widgets/joystick_widget.py @@ -1,16 +1,19 @@ +from qtpy.QtWidgets import QFrame, QLabel, QVBoxLayout + from view.widgets.base_device_widget import BaseDeviceWidget, create_widget, label_maker, scan_for_properties -from qtpy.QtWidgets import QLabel, QFrame, QVBoxLayout + class JoystickWidget(BaseDeviceWidget): + """_summary_""" - def __init__(self, joystick, - advanced_user: bool = True): - """ - Modify BaseDeviceWidget to be specifically for Joystick. - :param joystick: joystick object - :param advanced_user: boolean specifying complexity of widget. If False, returns blank widget - """ + def __init__(self, joystick, advanced_user: bool = True): + """_summary_ + :param joystick: _description_ + :type joystick: _type_ + :param advanced_user: _description_, defaults to True + :type advanced_user: bool, optional + """ properties = scan_for_properties(joystick) if advanced_user else {} super().__init__(type(joystick), properties) self.advanced_user = advanced_user @@ -20,47 +23,49 @@ def __init__(self, joystick, def create_axis_combo_box(self) -> None: """ - Transform Instrument Axis text box into combo box and allow selection of only available axes + Transform Instrument Axis text box into combo box and allow selection of only available axes. """ - - joystick_widgets = [QLabel('Joystick Mapping'), QLabel()] + joystick_widgets = [QLabel("Joystick Mapping"), QLabel()] for joystick_axis, specs in self.joystick_mapping.items(): unused = list( - set(self.stage_axes) - set(axis['instrument_axis'] for axis in self.joystick_mapping.values())) - unused.append(specs['instrument_axis']) - old_widget = getattr(self, f'joystick_mapping.{joystick_axis}.instrument_axis_widget') - new_widget = self.create_combo_box(f'joystick_mapping.{joystick_axis}.instrument_axis', unused) + set(self.stage_axes) - set(axis["instrument_axis"] for axis in self.joystick_mapping.values()) + ) + unused.append(specs["instrument_axis"]) + old_widget = getattr(self, f"joystick_mapping.{joystick_axis}.instrument_axis_widget") + new_widget = self.create_combo_box(f"joystick_mapping.{joystick_axis}.instrument_axis", unused) old_widget.parentWidget().layout().removeItem(old_widget.parentWidget().layout().itemAt(0)) old_widget.parentWidget().layout().replaceWidget(old_widget, new_widget) - setattr(self, f'joystick_mapping.{joystick_axis}.instrument_axis_widget', new_widget) + setattr(self, f"joystick_mapping.{joystick_axis}.instrument_axis_widget", new_widget) new_widget.currentTextChanged.connect(self.update_axes_selection) - widget_dict = {'label': QLabel(label_maker(joystick_axis)), - **getattr(self, f'joystick_mapping.{joystick_axis}_widgets')} + widget_dict = { + "label": QLabel(label_maker(joystick_axis)), + **getattr(self, f"joystick_mapping.{joystick_axis}_widgets"), + } # add frame frame = QFrame() layout = QVBoxLayout() - layout.addWidget(create_widget('V', **widget_dict)) + layout.addWidget(create_widget("V", **widget_dict)) frame.setLayout(layout) - frame.setStyleSheet(f".QFrame {{ border:1px solid grey ; }} ") + frame.setStyleSheet(".QFrame {{ border:1px solid grey ; }} ") if not self.advanced_user: frame.setEnabled(False) joystick_widgets.append(frame) - self.centralWidget().layout().replaceWidget(self.property_widgets['joystick_mapping'], - create_widget('HV', *joystick_widgets)) + self.centralWidget().layout().replaceWidget( + self.property_widgets["joystick_mapping"], create_widget("HV", *joystick_widgets) + ) def update_axes_selection(self) -> None: """ - When joystick axis mapped to new stage axis, update available stage axis + When joystick axis mapped to new stage axis, update available stage axis. """ - for joystick_axis, specs in self.joystick_mapping.items(): - unused = list(set(self.stage_axes) - set(ax['instrument_axis'] for ax in self.joystick_mapping.values())) - unused.append(specs['instrument_axis']) - widget = getattr(self, f'joystick_mapping.{joystick_axis}.instrument_axis_widget') + unused = list(set(self.stage_axes) - set(ax["instrument_axis"] for ax in self.joystick_mapping.values())) + unused.append(specs["instrument_axis"]) + widget = getattr(self, f"joystick_mapping.{joystick_axis}.instrument_axis_widget") # block signals to not trigger currentTextChanged widget.blockSignals(True) widget.clear() widget.addItems(unused) - widget.setCurrentText(specs['instrument_axis']) + widget.setCurrentText(specs["instrument_axis"]) widget.blockSignals(False) diff --git a/src/view/widgets/device_widgets/laser_widget.py b/src/view/widgets/device_widgets/laser_widget.py index 4ff7167..d2e0fe3 100644 --- a/src/view/widgets/device_widgets/laser_widget.py +++ b/src/view/widgets/device_widgets/laser_widget.py @@ -1,35 +1,41 @@ -from view.widgets.base_device_widget import BaseDeviceWidget, create_widget, scan_for_properties -from qtpy.QtCore import Qt import importlib -from view.widgets.miscellaneous_widgets.q_scrollable_float_slider import QScrollableFloatSlider -from qtpy.QtGui import QIntValidator, QDoubleValidator + +from qtpy.QtCore import Qt +from qtpy.QtGui import QDoubleValidator, QIntValidator from qtpy.QtWidgets import QSizePolicy + +from view.widgets.base_device_widget import BaseDeviceWidget, scan_for_properties +from view.widgets.miscellaneous_widgets.q_scrollable_float_slider import QScrollableFloatSlider + + class LaserWidget(BaseDeviceWidget): + """_summary_""" - def __init__(self, laser, - color: str = 'blue', - advanced_user: bool = True): - """ - Modify BaseDeviceWidget to be specifically for laser. Main need is adding slider . - :param laser: laser object - :param color: color of laser slider - :param advanced_user: boolean specifying complexity of widget. If False, only power widget will be visible - """ + def __init__(self, laser, color: str = "blue", advanced_user: bool = True): + """_summary_ - self.laser_properties = scan_for_properties(laser) if advanced_user else \ - {'power_setpoint_mw': laser.power_setpoint_mw, - 'power_mw': laser.power_mw} + :param laser: _description_ + :type laser: _type_ + :param color: _description_, defaults to 'blue' + :type color: str, optional + :param advanced_user: _description_, defaults to True + :type advanced_user: bool, optional + """ + self.laser_properties = ( + scan_for_properties(laser) + if advanced_user + else {"power_setpoint_mw": laser.power_setpoint_mw, "power_mw": laser.power_mw} + ) self.laser_module = importlib.import_module(laser.__module__) self.slider_color = color super().__init__(type(laser), self.laser_properties) - self.max_power_mw = getattr(type(laser).power_setpoint_mw, 'maximum', 110) + self.max_power_mw = getattr(type(laser).power_setpoint_mw, "maximum", 110) self.add_power_slider() def add_power_slider(self) -> None: """ - Modify power widget to be slider + Modify power widget to be slider. """ - setpoint = self.power_setpoint_mw_widget power = self.power_mw_widget @@ -46,34 +52,35 @@ def add_power_slider(self) -> None: setpoint.editingFinished.connect(lambda: slider.setValue(round(float(setpoint.text())))) setpoint.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) - textbox_power_mw_label = self.property_widgets['power_mw'].layout().itemAt(0).widget() + textbox_power_mw_label = self.property_widgets["power_mw"].layout().itemAt(0).widget() textbox_power_mw_label.setVisible(False) # hide power_mw label slider = QScrollableFloatSlider(orientation=Qt.Horizontal) slider.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - slider.setStyleSheet("QSlider::groove:horizontal {border: 1px solid #777;height: 10px;border-radius: 4px;}" - "QSlider::handle:horizontal {background-color: grey; width: 16px; height: 20px; " - "line-height: 20px; margin-top: -5px; margin-bottom: -5px; border-radius: 10px; }" - f"QSlider::sub-page:horizontal {{background: {self.slider_color};border: 1px solid #777;" - f"height: 10px;border-radius: 4px;}}") + slider.setStyleSheet( + "QSlider::groove:horizontal {border: 1px solid #777;height: 10px;border-radius: 4px;}" + "QSlider::handle:horizontal {background-color: grey; width: 16px; height: 20px; " + "line-height: 20px; margin-top: -5px; margin-bottom: -5px; border-radius: 10px; }" + f"QSlider::sub-page:horizontal {{background: {self.slider_color};border: 1px solid #777;" + f"height: 10px;border-radius: 4px;}}" + ) slider.setMinimum(0) # Todo: is it always zero? slider.setMaximum(int(self.max_power_mw)) slider.setValue(int(self.power_setpoint_mw)) slider.sliderMoved.connect(lambda: setpoint.setText(str(slider.value()))) - slider.sliderReleased.connect(lambda: setattr(self, 'power_setpoint_mw', float(slider.value()))) - slider.sliderReleased.connect(lambda: self.ValueChangedInside.emit('power_setpoint_mw')) + slider.sliderReleased.connect(lambda: setattr(self, "power_setpoint_mw", float(slider.value()))) + slider.sliderReleased.connect(lambda: self.ValueChangedInside.emit("power_setpoint_mw")) self.power_setpoint_mw_widget_slider = slider - self.property_widgets['power_setpoint_mw'].layout().addWidget(self.power_mw_widget) - self.property_widgets['power_setpoint_mw'].layout().addWidget(slider) - + self.property_widgets["power_setpoint_mw"].layout().addWidget(self.power_mw_widget) + self.property_widgets["power_setpoint_mw"].layout().addWidget(slider) def power_slider_fixup(self, value) -> None: - """ - Fix entered values that are larger than max power - :param value: value entered that is above maximum of slider - """ + """_summary_ + :param value: _description_ + :type value: _type_ + """ self.power_setpoint_mw_widget.setText(str(self.max_power_mw)) - self.power_setpoint_mw_widget.editingFinished.emit() \ No newline at end of file + self.power_setpoint_mw_widget.editingFinished.emit() diff --git a/src/view/widgets/device_widgets/ni_widget.py b/src/view/widgets/device_widgets/ni_widget.py index 0d0f265..4be8584 100644 --- a/src/view/widgets/device_widgets/ni_widget.py +++ b/src/view/widgets/device_widgets/ni_widget.py @@ -1,42 +1,43 @@ -from view.widgets.base_device_widget import BaseDeviceWidget, create_widget, label_maker, pathGet -from qtpy.QtWidgets import QTreeWidgetItem, QSizePolicy, QComboBox -from qtpy.QtCore import Qt +from random import randint +from typing import Union + +import numpy as np import qtpy.QtGui as QtGui -from view.widgets.miscellaneous_widgets.q_scrollable_float_slider import QScrollableFloatSlider +from qtpy.QtCore import Qt, Slot +from qtpy.QtWidgets import QComboBox, QSizePolicy, QTreeWidgetItem +from scipy import signal + +from view.widgets.base_device_widget import BaseDeviceWidget, create_widget, label_maker, pathGet +from view.widgets.device_widgets.waveform_widget import WaveformWidget from view.widgets.miscellaneous_widgets.q_non_scrollable_tree_widget import QNonScrollableTreeWidget +from view.widgets.miscellaneous_widgets.q_scrollable_float_slider import QScrollableFloatSlider from view.widgets.miscellaneous_widgets.q_scrollable_line_edit import QScrollableLineEdit -from view.widgets.device_widgets.waveform_widget import WaveformWidget -import numpy as np -from scipy import signal -from qtpy.QtCore import Slot -from random import randint -from typing import Union + class NIWidget(BaseDeviceWidget): + """_summary_""" - def __init__(self, daq, - exposed_branches: dict = None, - advanced_user: bool = True - ): - """ - Modify BaseDeviceWidget to be specifically for ni daq. - :param advanced_user: flag to disable waveform widget - :param exposed_branches: branches of tasks to be exposed in tree. - Needs to have keys that map directly into dask - :param advanced_user: boolean specifying complexity of widget. If False, Waveform widget is not interactive - """ + def __init__(self, daq, exposed_branches: dict = None, advanced_user: bool = True): + """_summary_ + :param daq: _description_ + :type daq: _type_ + :param exposed_branches: _description_, defaults to None + :type exposed_branches: dict, optional + :param advanced_user: _description_, defaults to True + :type advanced_user: bool, optional + """ self.advanced_user = advanced_user - self.exposed_branches = {'tasks': daq.tasks} if exposed_branches is None else exposed_branches + self.exposed_branches = {"tasks": daq.tasks} if exposed_branches is None else exposed_branches # initialize base widget to create convenient widgets and signals - super().__init__(daq, {'tasks': daq.tasks}) - del self.property_widgets['tasks'] # delete so view won't confuse and try and update. Hacky? + super().__init__(daq, {"tasks": daq.tasks}) + del self.property_widgets["tasks"] # delete so view won't confuse and try and update. Hacky? # available channels #TODO: this seems pretty hard coded? A way to avoid this? - self.ao_physical_chans = [x.replace(f'{daq.id}/', '') for x in daq.ao_physical_chans] - self.co_physical_chans = [x.replace(f'{daq.id}/', '') for x in daq.co_physical_chans] - self.do_physical_chans = [x.replace(f'{daq.id}/', '') for x in daq.do_physical_chans] - self.dio_ports = [x.replace(f'{daq.id}/', '') for x in daq.dio_ports] + self.ao_physical_chans = [x.replace(f"{daq.id}/", "") for x in daq.ao_physical_chans] + self.co_physical_chans = [x.replace(f"{daq.id}/", "") for x in daq.co_physical_chans] + self.do_physical_chans = [x.replace(f"{daq.id}/", "") for x in daq.do_physical_chans] + self.dio_ports = [x.replace(f"{daq.id}/", "") for x in daq.dio_ports] # create waveform widget if advanced_user: @@ -46,19 +47,21 @@ def __init__(self, daq, # create tree widget and format configured widgets into tree self.tree = QNonScrollableTreeWidget() for tasks, widgets in self.exposed_branches.items(): - header = QTreeWidgetItem(self.tree, - [label_maker(tasks.split('.')[-1])]) # take last of list incase key is a map + header = QTreeWidgetItem( + self.tree, [label_maker(tasks.split(".")[-1])] + ) # take last of list incase key is a map self.create_tree_widget(tasks, header) - self.tree.setHeaderLabels(['Tasks', 'Values']) + self.tree.setHeaderLabels(["Tasks", "Values"]) self.tree.setColumnCount(2) # Set up waveform widget if advanced_user: - graph_parent = QTreeWidgetItem(self.tree, ['Graph']) + graph_parent = QTreeWidgetItem(self.tree, ["Graph"]) graph_child = QTreeWidgetItem(graph_parent) - self.tree.setItemWidget(graph_child, 1, create_widget('H', self.waveform_widget.legend, - self.waveform_widget)) + self.tree.setItemWidget( + graph_child, 1, create_widget("H", self.waveform_widget.legend, self.waveform_widget) + ) graph_parent.addChild(graph_child) self.setCentralWidget(self.tree) @@ -67,90 +70,107 @@ def __init__(self, daq, self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) def update_waveform(self, channel_name: str) -> None: - """Add waveforms to waveform widget - :param channel_name: name of channel to update""" + """_summary_ + :param channel_name: _description_ + :type channel_name: str + """ if not self.advanced_user: return - name_lst = channel_name.split('.') - task = '.'.join(name_lst[:name_lst.index("ports")]) - port_name = '.'.join(name_lst[:name_lst.index("ports") + 2]) + name_lst = channel_name.split(".") + task = ".".join(name_lst[: name_lst.index("ports")]) + port_name = ".".join(name_lst[: name_lst.index("ports") + 2]) wl = name_lst[-1] - waveform = getattr(self, f'{port_name}.waveform') + waveform = getattr(self, f"{port_name}.waveform") kwargs = { - 'sampling_frequency_hz': getattr(self, f'{task}.timing.sampling_frequency_hz'), - 'period_time_ms': getattr(self, f'{task}.timing.period_time_ms'), - 'start_time_ms': getattr(self, f'{port_name}.parameters.start_time_ms.channels.{wl}'), - 'end_time_ms': getattr(self, f'{port_name}.parameters.end_time_ms.channels.{wl}'), - 'rest_time_ms': getattr(self, f'{task}.timing.rest_time_ms'), + "sampling_frequency_hz": getattr(self, f"{task}.timing.sampling_frequency_hz"), + "period_time_ms": getattr(self, f"{task}.timing.period_time_ms"), + "start_time_ms": getattr(self, f"{port_name}.parameters.start_time_ms.channels.{wl}"), + "end_time_ms": getattr(self, f"{port_name}.parameters.end_time_ms.channels.{wl}"), + "rest_time_ms": getattr(self, f"{task}.timing.rest_time_ms"), } - scale = kwargs['sampling_frequency_hz']/1000 # account for scaling that occurs in waveform functions + scale = kwargs["sampling_frequency_hz"] / 1000 # account for scaling that occurs in waveform functions - if waveform == 'square wave': - kwargs['max_volts'] = getattr(self, f'{port_name}.parameters.max_volts.channels.{wl}', 5) - kwargs['min_volts'] = getattr(self, f'{port_name}.parameters.min_volts.channels.{wl}', 0) + if waveform == "square wave": + kwargs["max_volts"] = getattr(self, f"{port_name}.parameters.max_volts.channels.{wl}", 5) + kwargs["min_volts"] = getattr(self, f"{port_name}.parameters.min_volts.channels.{wl}", 0) voltages = square_wave(**kwargs) - start = int(kwargs['start_time_ms'] * scale) - end = int(kwargs['period_time_ms'] * scale) - y = [kwargs['min_volts'], kwargs['min_volts'], kwargs['max_volts'], - kwargs['max_volts'], kwargs['min_volts'], kwargs['min_volts']] + start = int(kwargs["start_time_ms"] * scale) + end = int(kwargs["period_time_ms"] * scale) + y = [ + kwargs["min_volts"], + kwargs["min_volts"], + kwargs["max_volts"], + kwargs["max_volts"], + kwargs["min_volts"], + kwargs["min_volts"], + ] x = [0, start - 1, start, end, end + 1, len(voltages)] else: - kwargs['amplitude_volts'] = getattr(self, f'{port_name}.parameters.amplitude_volts.channels.{wl}') - kwargs['offset_volts'] = getattr(self, f'{port_name}.parameters.offset_volts.channels.{wl}') - kwargs['cutoff_frequency_hz'] = getattr(self, - f'{port_name}.parameters.cutoff_frequency_hz.channels.{wl}') - if waveform == 'sawtooth': + kwargs["amplitude_volts"] = getattr(self, f"{port_name}.parameters.amplitude_volts.channels.{wl}") + kwargs["offset_volts"] = getattr(self, f"{port_name}.parameters.offset_volts.channels.{wl}") + kwargs["cutoff_frequency_hz"] = getattr(self, f"{port_name}.parameters.cutoff_frequency_hz.channels.{wl}") + if waveform == "sawtooth": voltages = sawtooth(**kwargs) - max_point = int(kwargs['end_time_ms'] * scale) if kwargs['end_time_ms'] != kwargs['period_time_ms'] else -1 + max_point = ( + int(kwargs["end_time_ms"] * scale) if kwargs["end_time_ms"] != kwargs["period_time_ms"] else -1 + ) else: voltages = triangle_wave(**kwargs) max_point = round( - (kwargs['start_time_ms'] + ((kwargs['period_time_ms'] - kwargs['start_time_ms']) / 2)) * scale) + (kwargs["start_time_ms"] + ((kwargs["period_time_ms"] - kwargs["start_time_ms"]) / 2)) * scale + ) - pre_rise_point = int(kwargs['start_time_ms'] * scale) - post_rise_point = int(kwargs['period_time_ms'] * scale) if kwargs['rest_time_ms'] != 0 else -1 + pre_rise_point = int(kwargs["start_time_ms"] * scale) + post_rise_point = int(kwargs["period_time_ms"] * scale) if kwargs["rest_time_ms"] != 0 else -1 - y = [voltages[0], voltages[pre_rise_point], voltages[max_point], voltages[post_rise_point], - voltages[-1]] + y = [voltages[0], voltages[pre_rise_point], voltages[max_point], voltages[post_rise_point], voltages[-1]] x = [0, pre_rise_point, max_point, post_rise_point, len(voltages)] - if 'do_task' not in channel_name: + if "do_task" not in channel_name: # Add min and max for graph - kwargs['device_max_volts'] = getattr(self, f'{port_name}.device_max_volts') - kwargs['device_min_volts'] = getattr(self, f'{port_name}.device_min_volts') + kwargs["device_max_volts"] = getattr(self, f"{port_name}.device_max_volts") + kwargs["device_min_volts"] = getattr(self, f"{port_name}.device_min_volts") - if item := getattr(self, f'{port_name}.{wl}_plot_item', False): + if item := getattr(self, f"{port_name}.{wl}_plot_item", False): color = item.color self.waveform_widget.removeDraggableGraphItem(item) else: colors = QtGui.QColor.colorNames() - colors.remove('black') + colors.remove("black") color = colors[randint(0, len(colors) - 1)] - item = self.waveform_widget.plot(pos=np.column_stack((x, y)), - waveform=waveform, - name=name_lst[name_lst.index("ports") + 1] + ' ' + wl, - color=color, - parameters={**{k: v['channels'][wl] for k, v in - getattr(self, f'{port_name}.parameters').items()}, **kwargs}) - item.valueChanged[str, float].connect(lambda var, val: self.waveform_value_changed( - val, f'{port_name}.parameters.{var}.channels.{wl}')) - setattr(self, f'{port_name}.{wl}_plot_item', item) + item = self.waveform_widget.plot( + pos=np.column_stack((x, y)), + waveform=waveform, + name=name_lst[name_lst.index("ports") + 1] + " " + wl, + color=color, + parameters={ + **{k: v["channels"][wl] for k, v in getattr(self, f"{port_name}.parameters").items()}, + **kwargs, + }, + ) + item.valueChanged[str, float].connect( + lambda var, val: self.waveform_value_changed(val, f"{port_name}.parameters.{var}.channels.{wl}") + ) + setattr(self, f"{port_name}.{wl}_plot_item", item) @Slot(str, float) def waveform_value_changed(self, value: float, name: str) -> None: - """Update textbox if waveform is changed - :param value: new value to update - :param name: name of parameter """ - - name_lst = name.split('.') - if hasattr(self, f'{name}_slider'): # value is included in exposed branches - textbox = getattr(self, f'{name}_widget') - slider = getattr(self, f'{name}_slider') - value = round(value, 0) if 'time' in name else round(value, 3) + """_summary_ + + :param value: _description_ + :type value: float + :param name: _description_ + :type name: str + """ + name_lst = name.split(".") + if hasattr(self, f"{name}_slider"): # value is included in exposed branches + textbox = getattr(self, f"{name}_widget") + slider = getattr(self, f"{name}_slider") + value = round(value, 0) if "time" in name else round(value, 3) textbox.setText(str(value)) slider.setValue(value) dictionary = pathGet(self.__dict__, name_lst[0:-1]) @@ -158,73 +178,74 @@ def waveform_value_changed(self, value: float, name: str) -> None: setattr(self, name, value) self.ValueChangedInside.emit(name) - def remodel_timing_widgets(self, name: str, widget: Union[QComboBox, QScrollableLineEdit]) \ - -> Union[QComboBox, QScrollableLineEdit]: - """ - Remodel timing widget to be combo boxes with driver specific variables like possible ports - :param name: name of attribute - :param widget: widget object - :return widget: updated widget corresponding to name attribute + def remodel_timing_widgets( + self, name: str, widget: Union[QComboBox, QScrollableLineEdit] + ) -> Union[QComboBox, QScrollableLineEdit]: + """_summary_ + + :return: _description_ + :rtype: _type_ """ - path = name.split('.') + path = name.split(".") if options := self.check_driver_variables(path[-1]): - widget = self.create_attribute_widget(name, 'combo', options) + widget = self.create_attribute_widget(name, "combo", options) - elif path[-1] in ['trigger_port', 'output_port']: - widget = self.create_attribute_widget(name, 'combo', self.dio_ports) + elif path[-1] in ["trigger_port", "output_port"]: + widget = self.create_attribute_widget(name, "combo", self.dio_ports) return widget - def remodel_port_widgets(self, name: str, widget: Union[QComboBox, QScrollableLineEdit]) \ - -> Union[QComboBox, QScrollableLineEdit]: - """Remodel port widgets with possible ports and waveforms - :param name: name of attribute - :param widget: widget object - :return widget: updated widget corresponding to name attribute - """ + def remodel_port_widgets( + self, name: str, widget: Union[QComboBox, QScrollableLineEdit] + ) -> Union[QComboBox, QScrollableLineEdit]: + """_summary_ - path = name.split('.') - task = 'ao' if 'ao_task' in path else 'do' + :return: _description_ + :rtype: _type_ + """ + path = name.split(".") + task = "ao" if "ao_task" in path else "do" - if path[-1] == 'port': - options = getattr(self, f'{task}_physical_chans') - widget = self.create_attribute_widget(name, 'combo', options) + if path[-1] == "port": + options = getattr(self, f"{task}_physical_chans") + widget = self.create_attribute_widget(name, "combo", options) - elif path[-1] == 'waveform': - options = self.check_driver_variables(f'{task}_waveforms') - widget = self.create_attribute_widget(name, 'combo', options) + elif path[-1] == "waveform": + options = self.check_driver_variables(f"{task}_waveforms") + widget = self.create_attribute_widget(name, "combo", options) widget.setDisabled(True) # can't change waveform for now. Maybe implemented later on if useful return widget def create_sliders(self, name: str) -> None: - """ - Create slide bars for channel widgets - :param name: name of attribute - """ + """_summary_ - textbox = getattr(self, f'{name}_widget') + :param name: _description_ + :type name: str + """ + textbox = getattr(self, f"{name}_widget") slider = QScrollableFloatSlider(orientation=Qt.Horizontal) - path = name.split('.') - if 'time' in name: - task = '.'.join(path[:path.index("ports")]) - maximum = getattr(self, f'{task}.timing.period_time_ms') + path = name.split(".") + if "time" in name: + task = ".".join(path[: path.index("ports")]) + maximum = getattr(self, f"{task}.timing.period_time_ms") slider.setMaximum(maximum) textbox.validator().setRange(0.0, maximum, decimals=0) - elif 'volt' in name: + elif "volt" in name: slider.divisor = 1000 - port = '.'.join(path[:path.index("ports") + 2]) if 'ports' in path else 0 + port = ".".join(path[: path.index("ports") + 2]) if "ports" in path else 0 # Triangle and sawtooths max amplitude can be less than max volts due to offset so force fixup check - maximum = getattr(self, f'{port}.device_max_volts', 5) - minimum = getattr(self, f'{port}.device_min_volts', - 0) if 'amplitude' not in name else -maximum # allow for negative amplitude + maximum = getattr(self, f"{port}.device_max_volts", 5) + minimum = ( + getattr(self, f"{port}.device_min_volts", 0) if "amplitude" not in name else -maximum + ) # allow for negative amplitude slider.setMaximum(maximum) slider.setMinimum(minimum) textbox.validator().setRange(minimum, maximum, decimals=4) - slider.setValue(getattr(self, f'{name}')) + slider.setValue(getattr(self, f"{name}")) - if 'amplitude_volts' in name or 'offset_volts' in name: + if "amplitude_volts" in name or "offset_volts" in name: textbox.editingFinished.connect(lambda: self.check_amplitude(float(textbox.text()), name)) slider.sliderMoved.connect(lambda value: self.check_amplitude(value, name)) else: @@ -234,107 +255,110 @@ def create_sliders(self, name: str) -> None: slider.sliderMoved.connect(lambda value: textbox.setText(str(value))) slider.sliderMoved.connect(lambda value: setattr(self, name, float(value))) - slider.sliderMoved.connect(lambda value: - pathGet(self.__dict__, path[0:-1]).__setitem__(path[-1], value)) + slider.sliderMoved.connect(lambda value: pathGet(self.__dict__, path[0:-1]).__setitem__(path[-1], value)) slider.sliderMoved.connect(lambda: self.ValueChangedInside.emit(name)) slider.sliderMoved.connect(lambda: self.update_waveform(name)) - setattr(self, f'{name}_slider', slider) + setattr(self, f"{name}_slider", slider) def create_tree_widget(self, name: str, parent=None) -> QTreeWidgetItem: + """_summary_ + + :param name: _description_ + :type name: str + :param parent: _description_, defaults to None + :type parent: _type_, optional + :return: _description_ + :rtype: QTreeWidgetItem """ - Recursive function to format nested dictionary of ni task items - :param name: name of attribute - :param parent: parent to pass into QTreeWidgetItems. Each node will pass itself down as a - parent so future nodes will appear underneath - :return items: items to add to QTreeWidget - """ - parent = self.tree if parent is None else parent # TODO: This is haaaaaacky. but might be good for now - iterable = self.mappedpathGet(self.exposed_branches.copy(), name.split('.')) + iterable = self.mappedpathGet(self.exposed_branches.copy(), name.split(".")) items = [] for i, item in enumerate(iterable): - key = item if hasattr(iterable, 'keys') else str(i) # account for yaml typed - id = f'{name}.{key}' - if widget := getattr(self, f'{id}_widget', False): + key = item if hasattr(iterable, "keys") else str(i) # account for yaml typed + id = f"{name}.{key}" + if widget := getattr(self, f"{id}_widget", False): item = QTreeWidgetItem(parent, [key]) - if 'channel' in name: + if "channel" in name: self.update_waveform(id) self.create_sliders(id) - widget = create_widget('H', getattr(self, f'{id}_widget'), getattr(self, f'{id}_slider')) - elif 'timing' in name: + widget = create_widget("H", getattr(self, f"{id}_widget"), getattr(self, f"{id}_slider")) + elif "timing" in name: widget = self.remodel_timing_widgets(id, widget) - elif key in ['port', 'waveform']: + elif key in ["port", "waveform"]: widget = self.remodel_port_widgets(id, widget) self.tree.setItemWidget(item, 1, widget) else: item = QTreeWidgetItem(parent, [key]) - children = self.create_tree_widget(f'{name}.{key}', item) + children = self.create_tree_widget(f"{name}.{key}", item) item.addChildren(children) items.append(item) self.check_to_hide(id, item) return items def mappedpathGet(self, dictionary: dict, path: list[str]) -> dict: - - """ - Recursive function to map a given path of strings into a dictionary which may contain keys that are parts of - the path strung together by periods. For example, the path may be ['keys', 'to', 'value', 'I', 'would', 'like'] - and the dictionary could resemble {'keys.to': {'value.I.would':{ 'like': value}}} - - :param dictionary: dictionary which may contain keys that are parts of the path strung together by periods - :param path: list of strings that map a path into dictionary + """_summary_ + + :param dictionary: _description_ + :type dictionary: dict + :param path: _description_ + :type path: list[str] + :return: _description_ + :rtype: dict """ # TODO: This is haaaaaacky. but might be good for now try: dictionary = pathGet(dictionary, path) except KeyError: - if '.'.join(path[0:2]) in dictionary.keys(): - dictionary = self.mappedpathGet(dictionary['.'.join(path[0:2])], path[2:]) + if ".".join(path[0:2]) in dictionary.keys(): + dictionary = self.mappedpathGet(dictionary[".".join(path[0:2])], path[2:]) else: - dictionary = self.mappedpathGet(dictionary, ['.'.join(path[0:2]), *path[2:]]) + dictionary = self.mappedpathGet(dictionary, [".".join(path[0:2]), *path[2:]]) finally: return dictionary - def check_to_hide(self, name: str, item: QTreeWidgetItem, dictionary: dict=None) -> None: - """ - Check if name split by '.' maps into exposed branched. If not, hide associated items - :param name: name of attribute - :param item: item associated with attribute - :param dictionary: dictionary to search in - :return: None + def check_to_hide(self, name: str, item: QTreeWidgetItem, dictionary: dict = None) -> None: + """_summary_ + + :param name: _description_ + :type name: str + :param item: _description_ + :type item: QTreeWidgetItem + :param dictionary: _description_, defaults to None + :type dictionary: dict, optional """ # TODO: This is haaaaaacky. but might be good for now dictionary = self.exposed_branches.copy() if dictionary is None else dictionary try: - self.mappedpathGet(dictionary, name.split('.')) + self.mappedpathGet(dictionary, name.split(".")) except KeyError: item.setHidden(True) def check_amplitude(self, value: float, name: str) -> None: - """Check if amplitude of triangle or sawtooth is below maximum - :param value: newly input amplitude value - :param name: attribute name - """ + """_summary_ - textbox = getattr(self, f'{name}_widget') - slider = getattr(self, f'{name}_slider') + :param value: _description_ + :type value: float + :param name: _description_ + :type name: str + """ + textbox = getattr(self, f"{name}_widget") + slider = getattr(self, f"{name}_slider") maximum = slider.maximum() - name_lst = name.split('.') - parameters = '.'.join(name_lst[:name_lst.index("parameters") + 1]) + name_lst = name.split(".") + parameters = ".".join(name_lst[: name_lst.index("parameters") + 1]) wl = name_lst[-1] # sawtooth or triangle - offset = value if 'offset_volts' in name else getattr(self, f'{parameters}.offset_volts.channels.{wl}') - amplitude = value if 'amplitude_volts' in name else getattr(self, f'{parameters}.amplitude_volts.channels.{wl}') - other_voltage = amplitude if 'offset_volts' in name else offset + offset = value if "offset_volts" in name else getattr(self, f"{parameters}.offset_volts.channels.{wl}") + amplitude = value if "amplitude_volts" in name else getattr(self, f"{parameters}.amplitude_volts.channels.{wl}") + other_voltage = amplitude if "offset_volts" in name else offset total_amplitude = offset + amplitude if total_amplitude > maximum: value = value - (total_amplitude - maximum) - elif ('amplitude_volts' in name and amplitude > offset) or \ - ('offset_volts' in name and offset < amplitude): + elif ("amplitude_volts" in name and amplitude > offset) or ("offset_volts" in name and offset < amplitude): value = other_voltage textbox.setText(str(value)) slider.setValue(float(value)) @@ -343,83 +367,96 @@ def check_amplitude(self, value: float, name: str) -> None: pathGet(self.__dict__, name_lst[0:-1]).__setitem__(name_lst[-1], value) self.update_waveform(name) - def textbox_fixup(self, value: float or str, name: str) -> None: - """ - Fix entered values that are larger than maximum - :param value: new value entered into textbox - :param name: name of attribute + def textbox_fixup(self, value: float | str, name: str) -> None: + """_summary_ + + :param value: _description_ + :type value: floatorstr + :param name: _description_ + :type name: str """ - textbox = getattr(self, f'{name}_widget') - slider = getattr(self, f'{name}_slider') + textbox = getattr(self, f"{name}_widget") + slider = getattr(self, f"{name}_slider") maximum = slider.maximum() textbox.setText(str(maximum)) textbox.editingFinished.emit() -def sawtooth(sampling_frequency_hz: float, - period_time_ms: float, - start_time_ms: float, - end_time_ms: float, - rest_time_ms: float, - amplitude_volts: float, - offset_volts: float, - cutoff_frequency_hz: float - ) -> np.ndarray: - """ - Function to create a sawtooth wave - :param sampling_frequency_hz: frequency of waveform. Determines how many samples in a waveform - :param period_time_ms: duration of wave in ms - :param start_time_ms: starting time of waveform in ms - :param end_time_ms: termination time of sawtooth - :param rest_time_ms: supplemental time after period time has ended in ms - :param amplitude_volts: amplitude of sawtooth - :param offset_volts: offset of sawtooth - :param cutoff_frequency_hz: unused - :return: numpy array of waveform +def sawtooth( + sampling_frequency_hz: float, + period_time_ms: float, + start_time_ms: float, + end_time_ms: float, + rest_time_ms: float, + amplitude_volts: float, + offset_volts: float, + cutoff_frequency_hz: float, +) -> np.ndarray: + """_summary_ + + :param sampling_frequency_hz: _description_ + :type sampling_frequency_hz: float + :param period_time_ms: _description_ + :type period_time_ms: float + :param start_time_ms: _description_ + :type start_time_ms: float + :param end_time_ms: _description_ + :type end_time_ms: float + :param rest_time_ms: _description_ + :type rest_time_ms: float + :param amplitude_volts: _description_ + :type amplitude_volts: float + :param offset_volts: _description_ + :type offset_volts: float + :param cutoff_frequency_hz: _description_ + :type cutoff_frequency_hz: float + :return: _description_ + :rtype: np.ndarray """ - - time_samples_ms = np.linspace(0, 2 * np.pi, - int(((period_time_ms - start_time_ms) / 1000) * sampling_frequency_hz)) - waveform = offset_volts + amplitude_volts * signal.sawtooth(t=time_samples_ms, - width=end_time_ms / period_time_ms) + time_samples_ms = np.linspace(0, 2 * np.pi, int(((period_time_ms - start_time_ms) / 1000) * sampling_frequency_hz)) + waveform = offset_volts + amplitude_volts * signal.sawtooth(t=time_samples_ms, width=end_time_ms / period_time_ms) # add in delay delay_samples = int((start_time_ms / 1000) * sampling_frequency_hz) - waveform = np.pad(array=waveform, - pad_width=(delay_samples, 0), - mode='constant', - constant_values=(offset_volts - amplitude_volts) - ) + waveform = np.pad( + array=waveform, pad_width=(delay_samples, 0), mode="constant", constant_values=(offset_volts - amplitude_volts) + ) # add in rest rest_samples = int((rest_time_ms / 1000) * sampling_frequency_hz) - waveform = np.pad(array=waveform, - pad_width=(0, rest_samples), - mode='constant', - constant_values=(offset_volts - amplitude_volts) - ) + waveform = np.pad( + array=waveform, pad_width=(0, rest_samples), mode="constant", constant_values=(offset_volts - amplitude_volts) + ) return waveform -def square_wave(sampling_frequency_hz: float, - period_time_ms: float, - start_time_ms: float, - end_time_ms: float, - rest_time_ms: float, - max_volts: float, - min_volts: float - ) -> np.ndarray: - """ - Function to create a sawtooth wave - :param sampling_frequency_hz: frequency of waveform. Determines how many samples in a waveform - :param period_time_ms: duration of wave in ms - :param start_time_ms: starting time of waveform in ms - :param end_time_ms: termination time of square wave - :param rest_time_ms: supplemental time after period time has ended in ms - :param max_volts: maximum volts of square wave - :param min_volts: minimum volts of square wave - :return: numpy array of waveform +def square_wave( + sampling_frequency_hz: float, + period_time_ms: float, + start_time_ms: float, + end_time_ms: float, + rest_time_ms: float, + max_volts: float, + min_volts: float, +) -> np.ndarray: + """_summary_ + + :param sampling_frequency_hz: _description_ + :type sampling_frequency_hz: float + :param period_time_ms: _description_ + :type period_time_ms: float + :param start_time_ms: _description_ + :type start_time_ms: float + :param end_time_ms: _description_ + :type end_time_ms: float + :param rest_time_ms: _description_ + :type rest_time_ms: float + :param max_volts: _description_ + :type max_volts: float + :param min_volts: _description_ + :type min_volts: float + :return: _description_ + :rtype: np.ndarray """ - time_samples = int(((period_time_ms + rest_time_ms) / 1000) * sampling_frequency_hz) start_sample = int((start_time_ms / 1000) * sampling_frequency_hz) end_sample = int((end_time_ms / 1000) * sampling_frequency_hz) @@ -430,36 +467,31 @@ def square_wave(sampling_frequency_hz: float, return waveform -def triangle_wave(sampling_frequency_hz: float, - period_time_ms: float, - start_time_ms: float, - end_time_ms: float, - rest_time_ms: float, - amplitude_volts: float, - offset_volts: float, - cutoff_frequency_hz: float - ) -> np.ndarray: - - """Function to create a sawtooth wave - :param sampling_frequency_hz: frequency of waveform. Determines how many samples in a waveform - :param period_time_ms: duration of wave in ms - :param start_time_ms: starting time of waveform in ms - :param end_time_ms: termination time of triangle_wave - :param rest_time_ms: supplemental time after period time has ended in ms - :param amplitude_volts: amplitude of triangle_wave - :param offset_volts: offset of triangle_wave - :param cutoff_frequency_hz: unused - :return: numpy array of waveform""" - +def triangle_wave( + sampling_frequency_hz: float, + period_time_ms: float, + start_time_ms: float, + end_time_ms: float, + rest_time_ms: float, + amplitude_volts: float, + offset_volts: float, + cutoff_frequency_hz: float, +) -> np.ndarray: + """_summary_ + + :return: _description_ + :rtype: _type_ + """ # sawtooth with end time in center of waveform - waveform = sawtooth(sampling_frequency_hz, - period_time_ms, - start_time_ms, - (period_time_ms - start_time_ms) / 2, - rest_time_ms, - amplitude_volts, - offset_volts, - cutoff_frequency_hz - ) + waveform = sawtooth( + sampling_frequency_hz, + period_time_ms, + start_time_ms, + (period_time_ms - start_time_ms) / 2, + rest_time_ms, + amplitude_volts, + offset_volts, + cutoff_frequency_hz, + ) return waveform diff --git a/src/view/widgets/device_widgets/stage_widget.py b/src/view/widgets/device_widgets/stage_widget.py index fb7bfda..5e58a38 100644 --- a/src/view/widgets/device_widgets/stage_widget.py +++ b/src/view/widgets/device_widgets/stage_widget.py @@ -1,27 +1,31 @@ -from view.widgets.base_device_widget import BaseDeviceWidget, scan_for_properties -from qtpy.QtWidgets import QLabel import importlib +from qtpy.QtWidgets import QLabel + +from view.widgets.base_device_widget import BaseDeviceWidget, scan_for_properties + + class StageWidget(BaseDeviceWidget): + """_summary_""" - def __init__(self, stage, - advanced_user: bool = True): - """ - Modify BaseDeviceWidget to be specifically for Stage. Main need is advanced user. - :param stage: stage object - :param advanced_user: boolean specifying complexity of widget. If False, only position is shown - """ + def __init__(self, stage, advanced_user: bool = True): + """_summary_ - self.stage_properties = scan_for_properties(stage) if advanced_user else {'position_mm': stage.position_mm} + :param stage: _description_ + :type stage: _type_ + :param advanced_user: _description_, defaults to True + :type advanced_user: bool, optional + """ + self.stage_properties = scan_for_properties(stage) if advanced_user else {"position_mm": stage.position_mm} self.stage_module = importlib.import_module(stage.__module__) super().__init__(type(stage), self.stage_properties) # alter position_mm widget to use instrument_axis as label - self.property_widgets['position_mm'].setEnabled(False) - position_label = self.property_widgets['position_mm'].findChild(QLabel) - unit = getattr(type(stage).position_mm, 'unit', 'mm') # TODO: Change when deliminated property is updated - position_label.setText(f'{stage.instrument_axis} [{unit}]') + self.property_widgets["position_mm"].setEnabled(False) + position_label = self.property_widgets["position_mm"].findChild(QLabel) + unit = getattr(type(stage).position_mm, "unit", "mm") # TODO: Change when deliminated property is updated + position_label.setText(f"{stage.instrument_axis} [{unit}]") # update property_widgets['position_mm'] text to be white style = """ @@ -33,4 +37,4 @@ def __init__(self, stage, color : white; } """ - self.property_widgets['position_mm'].setStyleSheet(style) + self.property_widgets["position_mm"].setStyleSheet(style) diff --git a/src/view/widgets/device_widgets/waveform_widget.py b/src/view/widgets/device_widgets/waveform_widget.py index 702cdac..91e970c 100644 --- a/src/view/widgets/device_widgets/waveform_widget.py +++ b/src/view/widgets/device_widgets/waveform_widget.py @@ -4,41 +4,54 @@ from qtpy.QtWidgets import QWidget, QVBoxLayout from view.widgets.miscellaneous_widgets.q_clickable_label import QClickableLabel from typing import Literal, TypedDict -import inspect -# TODO: Use this else where to. Consider moving it so we don't have to copy paste? + class SignalChangeVar: - """Class that emits signal containing name when set function is used""" + """ + Class that emits signal containing name when set function is used. + """ def __set_name__(self, owner, name) -> None: - """ - Set name of class. Called in the init of class - :param owner: instance of class - :param name: set name of class. Will be variable name + """_summary_ + + :param owner: _description_ + :type owner: _type_ + :param name: _description_ + :type name: _type_ """ self.name = f"_{name}" def __set__(self, instance, value) -> None: - """ - Setting function of class - :param instance: instance of class - :param value: value to set to - """ + """_summary_ + :param instance: _description_ + :type instance: _type_ + :param value: _description_ + :type value: _type_ + """ setattr(instance, self.name, value) # initially setting attr instance.valueChanged.emit(self.name[1:], value) def __get__(self, instance, value): - """ - Getting function of class - :param instance: instance of class - :param value: object calling function - :return: value of class + """_summary_ + + :param instance: _description_ + :type instance: _type_ + :param value: _description_ + :type value: _type_ + :return: _description_ + :rtype: _type_ """ return getattr(instance, self.name) class PointyWaveParameters(TypedDict): + """_summary_ + + :param TypedDict: _description_ + :type TypedDict: _type_ + """ + start_time_ms: float end_time_ms: float amplitude_volts: float @@ -49,6 +62,12 @@ class PointyWaveParameters(TypedDict): class SquareWaveParameters(TypedDict): + """_summary_ + + :param TypedDict: _description_ + :type TypedDict: _type_ + """ + start_time_ms: float end_time_ms: float max_volts: float @@ -58,7 +77,14 @@ class SquareWaveParameters(TypedDict): class DraggableGraphItem(GraphItem): - """Graph item representing triangle, sawtooth, and square wave that can be dragged by user to modify wave""" + """_summary_ + + :param GraphItem: _description_ + :type GraphItem: _type_ + :raises Exception: _description_ + :raises Exception: _description_ + """ + # initialize waveform parameters start_time_ms = SignalChangeVar() end_time_ms = SignalChangeVar() @@ -69,68 +95,75 @@ class DraggableGraphItem(GraphItem): min_volts = SignalChangeVar() valueChanged = Signal((str, float)) - def __init__(self, pos: np.ndarray, - waveform: Literal['square wave', 'sawtooth', 'triangle wave'], - parameters: PointyWaveParameters or SquareWaveParameters, **kwargs): - """ - :param pos: 2d numpy array of concatenated lists of [[x],[y]] values - :param waveform: type of waveform positions represent - :param parameters: dictionary of parameters like amplitude_volts, end_time_ms, ect. and the corresponding values - :param kwargs: kwargs relating to PlotWidget plot function + def __init__( + self, + pos: np.ndarray, + waveform: Literal["square wave", "sawtooth", "triangle wave"], + parameters: PointyWaveParameters or SquareWaveParameters, + **kwargs, + ): + """_summary_ + + :param pos: _description_ + :type pos: np.ndarray + :param waveform: _description_ + :type waveform: Literal['square wave', 'sawtooth', 'triangle wave'] + :param parameters: _description_ + :type parameters: PointyWaveParametersorSquareWaveParameters """ - self.pos = pos self.waveform = waveform self.dragPoint = None self.dragOffset = None self.parameters = parameters - self.name = kwargs.get('name', None) - self.color = kwargs.get('color', 'black') + self.name = kwargs.get("name", None) + self.color = kwargs.get("color", "black") super().__init__(pos=pos, **kwargs) def setData(self, **kwargs) -> None: - """ - Set data for waveform graph item - :param kwargs: data to set - """ - - self.pos = kwargs.get('pos', self.pos) - self.waveform = kwargs.get('waveform', self.waveform) - self.parameters = kwargs.get('parameters', self.parameters) + """_summary_""" + self.pos = kwargs.get("pos", self.pos) + self.waveform = kwargs.get("waveform", self.waveform) + self.parameters = kwargs.get("parameters", self.parameters) self.define_waves(self.waveform) npts = self.pos.shape[0] - kwargs['adj'] = np.column_stack((np.arange(0, npts - 1), np.arange(1, npts))) - kwargs['data'] = np.empty(npts, dtype=[('index', int)]) - kwargs['data']['index'] = np.arange(npts) + kwargs["adj"] = np.column_stack((np.arange(0, npts - 1), np.arange(1, npts))) + kwargs["data"] = np.empty(npts, dtype=[("index", int)]) + kwargs["data"]["index"] = np.arange(npts) super().setData(**kwargs) - def define_waves(self, waveform: Literal['square wave', 'sawtooth', 'triangle wave']) -> None: - """ - Validate and define key indices in waveform - :param waveform: specification of what waveform graph item represents - """ + def define_waves(self, waveform: Literal["square wave", "sawtooth", "triangle wave"]) -> None: + """_summary_ - if 'sawtooth' in waveform or 'triangle' in waveform: + :param waveform: _description_ + :type waveform: Literal['square wave', 'sawtooth', 'triangle wave'] + :raises Exception: _description_ + :raises Exception: _description_ + """ + if "sawtooth" in waveform or "triangle" in waveform: if self.pos.shape[0] != 5: - raise Exception(f"Waveform {waveform} must have 5 points in data set. " - f"Waveform has {self.pos.shape[0]}") + raise Exception( + f"Waveform {waveform} must have 5 points in data set. " f"Waveform has {self.pos.shape[0]}" + ) - elif 'square' in waveform: + elif "square" in waveform: if self.pos.shape[0] != 6: - raise Exception(f"Waveform {waveform} must have 6 points in data set. " - f"Waveform has {self.pos.shape[0]}") + raise Exception( + f"Waveform {waveform} must have 6 points in data set. " f"Waveform has {self.pos.shape[0]}" + ) # block signals self.blockSignals(True) for k, v in self.parameters.items(): setattr(self, k, v) self.blockSignals(False) + def mouseDragEvent(self, ev) -> None: - """ - Register if user clicks and drags point of waveform - :param ev: mouse drag event - """ + """_summary_ + :param ev: _description_ + :type ev: _type_ + """ if ev.isStart(): pos = ev.buttonDownPos() pts = self.scatter.pointsAt(pos) @@ -150,37 +183,38 @@ def mouseDragEvent(self, ev) -> None: ev.ignore() return ind = self.dragPoint.data()[0] - if self.waveform == 'square wave': + if self.waveform == "square wave": self.move_square_wave(ind, ev) - elif self.waveform == 'sawtooth': + elif self.waveform == "sawtooth": self.move_sawtooth(ind, ev) - elif self.waveform == 'triangle wave': + elif self.waveform == "triangle wave": self.move_triangle_wave(ind, ev) self.setData(pos=self.pos) ev.accept() def move_square_wave(self, ind: int, ev) -> None: - """ - Move square wave type waveform. Square wave will have 6 indices - :param ind: index being dragged - :param ev: mouse event - """ + """_summary_ + :param ind: _description_ + :type ind: int + :param ev: _description_ + :type ev: _type_ + """ min_v = self.device_min_volts max_v = self.device_max_volts y_pos = ev.pos()[1] + self.dragOffsetY # new y pos is old plus drag offset - if ind in [1, 4] and min_v <= y_pos <= max_v: # either side of square is moved + if ind in [1, 4] and min_v <= y_pos <= max_v: # either side of square is moved for i in [0, 1, 4, 5]: self.pos[i][1] = y_pos - elif ind in [2, 3] and min_v <= y_pos <= max_v: # square is moved + elif ind in [2, 3] and min_v <= y_pos <= max_v: # square is moved for i in [2, 3]: self.pos[i][1] = y_pos self.min_volts = self.pos[1][1] self.max_volts = self.pos[2][1] - x_pos = ev.pos()[0] + self.dragOffsetX # new x pos is old plus drag offset + x_pos = ev.pos()[0] + self.dragOffsetX # new x pos is old plus drag offset lower_limit_x = self.pos[ind - 1][0] if ind in [1, 3] else self.pos[ind - 2][0] upper_limit_x = self.pos[ind + 2][0] if ind in [1, 3] else self.pos[ind + 1][0] if lower_limit_x <= x_pos <= upper_limit_x and ind in [1, 2, 3, 4]: @@ -192,74 +226,75 @@ def move_square_wave(self, ind: int, ev) -> None: self.end_time_ms = self.pos[4][0] / 10 def move_sawtooth(self, ind: int, ev) -> None: - """ - Move sawtooth type waveform. Sawtooth will have 5 indices - :param ind: index being dragged - :param ev: mouse event - """ + """_summary_ + :param ind: _description_ + :type ind: int + :param ev: _description_ + :type ev: _type_ + """ min_v = self.device_min_volts max_v = self.device_max_volts y_pos = ev.pos()[1] + self.dragOffsetY # new y pos is old plus drag offset - if ind in [1, 3] and min_v <= y_pos <= max_v: # either side of peak is moved + if ind in [1, 3] and min_v <= y_pos <= max_v: # either side of peak is moved self.pos[2][1] = y_pos + (self.pos[2][1] - self.pos[3][1]) # update peak to account for new offset volts for i in [0, 1, 3, 4]: # update points to include drag value self.pos[i][1] = ev.pos()[1] + self.dragOffsetY - self.offset_volts = (self.pos[2][1] + y_pos) / 2 # update offset volts + self.offset_volts = (self.pos[2][1] + y_pos) / 2 # update offset volts - elif ind == 2 and min_v <= y_pos <= max_v and min_v <= 2 * self.offset_volts - y_pos <= max_v: # peak is moved - self.pos[2][1] = ev.pos()[1] + self.dragOffsetY # update peak with drag value + elif ind == 2 and min_v <= y_pos <= max_v and min_v <= 2 * self.offset_volts - y_pos <= max_v: # peak is moved + self.pos[2][1] = ev.pos()[1] + self.dragOffsetY # update peak with drag value self.amplitude_volts = y_pos - self.offset_volts # update amplitude - for i in [0, 1, 3, 4]: # update points to account for new amplitude + for i in [0, 1, 3, 4]: # update points to account for new amplitude self.pos[i][1] = self.offset_volts - self.amplitude_volts - x_pos = ev.pos()[0] + self.dragOffsetX # new x pos is old plus drag offset - if ind in [1] and self.pos[ind - 1][0] <= x_pos <= self.pos[ind + 1][0]: # start time dragged + x_pos = ev.pos()[0] + self.dragOffsetX # new x pos is old plus drag offset + if ind in [1] and self.pos[ind - 1][0] <= x_pos <= self.pos[ind + 1][0]: # start time dragged self.pos[ind][0] = x_pos self.start_time_ms = x_pos / 10 self.pos[2][0] = x_pos + (self.end_time_ms / self.period_time_ms) * (self.pos[3][0] - x_pos) - elif ind == 2 and self.pos[1][0] <= x_pos <= self.pos[3][0]: # peak is dragged + elif ind == 2 and self.pos[1][0] <= x_pos <= self.pos[3][0]: # peak is dragged self.pos[ind][0] = x_pos - self.end_time_ms = ((x_pos - self.pos[1][0]) / (self.pos[3][0] - self.pos[1][0])) * \ - self.period_time_ms + self.end_time_ms = ((x_pos - self.pos[1][0]) / (self.pos[3][0] - self.pos[1][0])) * self.period_time_ms def move_triangle_wave(self, ind: int, ev) -> None: - """ - Move triangle type waveform. Triangle will have 5 indices - :param ind: index being dragged - :param ev: mouse event - """ + """_summary_ + :param ind: _description_ + :type ind: int + :param ev: _description_ + :type ev: _type_ + """ min_v = self.device_min_volts max_v = self.device_max_volts y_pos = ev.pos()[1] + self.dragOffsetY # new y pos is old plus drag offset - if ind in [1, 3] and min_v <= y_pos <= max_v: # either side of peak is moved + if ind in [1, 3] and min_v <= y_pos <= max_v: # either side of peak is moved for i in [0, 1, 3, 4]: # update points to include drag value self.pos[i][1] = ev.pos()[1] + self.dragOffsetY - self.offset_volts = (self.pos[2][1] + y_pos) / 2 # update offset volts + self.offset_volts = (self.pos[2][1] + y_pos) / 2 # update offset volts self.pos[2][1] = y_pos + (self.pos[2][1] - self.pos[3][1]) # update peak to account for new offset volts - elif ind == 2 and min_v <= y_pos <= max_v and min_v <= 2 * self.offset_volts - y_pos <= max_v: # peak is moved - self.pos[2][1] = ev.pos()[1] + self.dragOffsetY # update peak with drag value - self.amplitude_volts = y_pos - self.offset_volts # update amplitude - for i in [0, 1, 3, 4]: # update points to account for new amplitude + elif ind == 2 and min_v <= y_pos <= max_v and min_v <= 2 * self.offset_volts - y_pos <= max_v: # peak is moved + self.pos[2][1] = ev.pos()[1] + self.dragOffsetY # update peak with drag value + self.amplitude_volts = y_pos - self.offset_volts # update amplitude + for i in [0, 1, 3, 4]: # update points to account for new amplitude self.pos[i][1] = self.offset_volts - self.amplitude_volts - x_pos = ev.pos()[0] + self.dragOffsetX # new x pos is old plus drag offset - if ind == 1 and self.pos[0][0] <= x_pos <= self.pos[2][0]: # point before peak + x_pos = ev.pos()[0] + self.dragOffsetX # new x pos is old plus drag offset + if ind == 1 and self.pos[0][0] <= x_pos <= self.pos[2][0]: # point before peak self.pos[1][0] = x_pos - self.start_time_ms = x_pos / 10 # update start time - self.pos[2][0] = x_pos + (.5 * (self.pos[3][0] - x_pos)) # shift peak + self.start_time_ms = x_pos / 10 # update start time + self.pos[2][0] = x_pos + (0.5 * (self.pos[3][0] - x_pos)) # shift peak + class WaveformWidget(PlotWidget): + """_summary_""" def __init__(self, **kwargs): - """ - Plot widget to show daq waveforms - """ + """_summary_""" # initialize legend widget self.legend = QWidget() self.legend.setLayout(QVBoxLayout()) @@ -267,65 +302,85 @@ def __init__(self, **kwargs): super().__init__(**kwargs) - self.setBackground('#262930') - - def plot(self, pos: np.ndarray, - waveform: Literal['square wave', 'sawtooth', 'triangle wave'], - parameters: PointyWaveParameters or SquareWaveParameters, **kwargs) -> DraggableGraphItem: + self.setBackground("#262930") + + def plot( + self, + pos: np.ndarray, + waveform: Literal["square wave", "sawtooth", "triangle wave"], + parameters: PointyWaveParameters or SquareWaveParameters, + **kwargs, + ) -> DraggableGraphItem: + """_summary_ + + :param pos: _description_ + :type pos: np.ndarray + :param waveform: _description_ + :type waveform: Literal['square wave', 'sawtooth', 'triangle wave'] + :param parameters: _description_ + :type parameters: PointyWaveParametersorSquareWaveParameters + :return: _description_ + :rtype: DraggableGraphItem """ - Plot waveforms on graph - :param pos: 2d numpy array of concatenated lists of [[x],[y]] values - :param waveform: type of waveform positions represent - :param parameters: dictionary of parameters like amplitude_volts, end_time_ms, ect. and the corresponding values - :param kwargs: kwargs relating to PlotWidget plot function - :return: item plotted in graph - """ - kwargs['pen'] = mkPen(color=kwargs.get('color', 'grey'), width=3) + kwargs["pen"] = mkPen(color=kwargs.get("color", "grey"), width=3) item = DraggableGraphItem(pos=pos, waveform=waveform, parameters=parameters, **kwargs) item.setData(pos=pos, waveform=waveform, parameters=parameters, **kwargs) self.addItem(item) - if 'name' in kwargs.keys(): + if "name" in kwargs.keys(): self.add_legend_item(item) return item def add_legend_item(self, item: DraggableGraphItem) -> None: - """ - Add item to legend widget - :param item: item to add to legend + """_summary_ + + :param item: _description_ + :type item: DraggableGraphItem """ - self.legend_labels[item.name] = QClickableLabel(f'{item.name}' - f'   ' - f'') + self.legend_labels[item.name] = QClickableLabel( + f'{item.name}' + f'   ' + f"" + ) self.legend_labels[item.name].clicked.connect(lambda: self.hide_show_line(item)) self.legend.layout().addWidget(self.legend_labels[item.name]) def removeDraggableGraphItem(self, item: DraggableGraphItem) -> None: - """ - Remove DraggableGraphItem and remove from legend - :param item: item to remove""" + """_summary_ + :param item: _description_ + :type item: DraggableGraphItem + """ self.removeItem(item) if item.name is not None: label = self.legend_labels[item.name] self.legend.layout().removeWidget(label) def hide_show_line(self, item) -> None: - """ - Hide or reveal line if legend is clicked - :param item: item to hide + """_summary_ + + :param item: _description_ + :type item: _type_ """ if item.isVisible(): item.setVisible(False) - self.legend_labels[item.name].setText(f'{item.name}' - f'   ' - f'') + self.legend_labels[item.name].setText( + f'{item.name}' + f'   ' + f"" + ) else: item.setVisible(True) - self.legend_labels[item.name].setText(f'{item.name}' - f'   ' - f'') + self.legend_labels[item.name].setText( + f'{item.name}' + f'   ' + f"" + ) def wheelEvent(self, ev): - """Overwriting to disable zoom""" + """_summary_ + + :param ev: _description_ + :type ev: _type_ + """ pass diff --git a/src/view/widgets/miscellaneous_widgets/gl_ortho_view_widget.py b/src/view/widgets/miscellaneous_widgets/gl_ortho_view_widget.py index 755f9ce..e86d805 100644 --- a/src/view/widgets/miscellaneous_widgets/gl_ortho_view_widget.py +++ b/src/view/widgets/miscellaneous_widgets/gl_ortho_view_widget.py @@ -1,33 +1,38 @@ +from typing import Literal + import numpy as np from pyqtgraph.opengl import GLViewWidget from qtpy.QtGui import QMatrix4x4 -from typing import Literal + class GLOrthoViewWidget(GLViewWidget): """ - Class inheriting from GLViewWidget that only allows specification of orthogonal or frustum view + Class inheriting from GLViewWidget that only allows specification of orthogonal or frustum view. """ + # override projectionMatrix is overrided to enable true ortho projection - def projectionMatrix(self, region=None, projection: Literal['ortho', 'frustum'] ='ortho') -> QMatrix4x4: - """ - Function that return projection matrix of space - :param region: region to create projection matrix for - :param projection: type of projection. Limited to orthogonal or frustum - :return: - """ + def projectionMatrix(self, region=None, projection: Literal["ortho", "frustum"] = "ortho") -> QMatrix4x4: + """_summary_ - assert projection in ['ortho', 'frustum'] + :param region: _description_, defaults to None + :type region: _type_, optional + :param projection: _description_, defaults to 'ortho' + :type projection: Literal['ortho', 'frustum'], optional + :return: _description_ + :rtype: QMatrix4x4 + """ + assert projection in ["ortho", "frustum"] if region is None: dpr = self.devicePixelRatio() region = (0, 0, self.width() * dpr, self.height() * dpr) x0, y0, w, h = self.getViewport() - dist = self.opts['distance'] - fov = self.opts['fov'] + dist = self.opts["distance"] + fov = self.opts["fov"] nearClip = dist * 0.001 - farClip = dist * 1000. + farClip = dist * 1000.0 - r = nearClip * np.tan(fov * 0.5 * np.pi / 180.) + r = nearClip * np.tan(fov * 0.5 * np.pi / 180.0) t = r * h / w # note that x0 and width in these equations must @@ -38,8 +43,8 @@ def projectionMatrix(self, region=None, projection: Literal['ortho', 'frustum'] top = t * ((region[1] + region[3] - y0) * (2.0 / h) - 1) tr = QMatrix4x4() - if projection == 'ortho': + if projection == "ortho": tr.ortho(left, right, bottom, top, nearClip, farClip) - elif projection == 'frustum': + elif projection == "frustum": tr.frustum(left, right, bottom, top, nearClip, farClip) - return tr \ No newline at end of file + return tr diff --git a/src/view/widgets/miscellaneous_widgets/gl_path_item.py b/src/view/widgets/miscellaneous_widgets/gl_path_item.py index 98bd748..13a8265 100644 --- a/src/view/widgets/miscellaneous_widgets/gl_path_item.py +++ b/src/view/widgets/miscellaneous_widgets/gl_path_item.py @@ -1,95 +1,78 @@ -from pyqtgraph.opengl import GLLinePlotItem -from OpenGL.GL import * # noqa import numpy as np +from OpenGL.GL import * # noqa +from pyqtgraph.opengl import GLLinePlotItem from qtpy.QtGui import QColor class GLPathItem(GLLinePlotItem): - """ Subclass of GLLinePlotItem that creates arrow at end of path""" + """Subclass of GLLinePlotItem that creates arrow at end of path.""" def __init__(self, parentItem=None, **kwds): + """_summary_ + :param parentItem: _description_, defaults to None + :type parentItem: _type_, optional + """ super().__init__(parentItem) - self.arrow_size_percent = kwds.get('arrow_size', 6.0) - self.arrow_aspect_ratio = kwds.get('arrow_aspect_ratio', 4) - self.path_start_color = kwds.get('path_start_color', 'magenta') - self.path_end_color = kwds.get('path_end_color', 'green') - self.width = kwds.get('width', 1) + self.arrow_size_percent = kwds.get("arrow_size", 6.0) + self.arrow_aspect_ratio = kwds.get("arrow_aspect_ratio", 4) + self.path_start_color = kwds.get("path_start_color", "magenta") + self.path_end_color = kwds.get("path_end_color", "green") + self.width = kwds.get("width", 1) def setData(self, **kwds): - """Rewrite to draw arrow at end of path""" - - kwds['width'] = self.width + """_summary_""" + kwds["width"] = self.width - if 'pos' in kwds.keys(): - path = kwds['pos'] + if "pos" in kwds.keys(): + path = kwds["pos"] # draw the end arrow # determine last line segment direction and draw arrowhead correctly if len(path) > 1: vector = path[-1] - path[-2] if vector[1] > 0: # calculate arrow size based on vector - arrow_size = abs(vector[1])*self.arrow_size_percent / 100 - x = np.array([path[-1, 0] - arrow_size, - path[-1, 0] + arrow_size, - path[-1, 0], - path[-1, 0] - arrow_size]) - y = np.array([path[-1, 1], - path[-1, 1], - path[-1, 1] + arrow_size * self.arrow_aspect_ratio, - path[-1, 1]]) - z = np.array([path[-1, 2], - path[-1, 2], - path[-1, 2], - path[-1, 2]]) + arrow_size = abs(vector[1]) * self.arrow_size_percent / 100 + x = np.array( + [path[-1, 0] - arrow_size, path[-1, 0] + arrow_size, path[-1, 0], path[-1, 0] - arrow_size] + ) + y = np.array( + [path[-1, 1], path[-1, 1], path[-1, 1] + arrow_size * self.arrow_aspect_ratio, path[-1, 1]] + ) + z = np.array([path[-1, 2], path[-1, 2], path[-1, 2], path[-1, 2]]) elif vector[1] < 0: # calculate arrow size based on vector - arrow_size = abs(vector[1])*self.arrow_size_percent / 100 - x = np.array([path[-1, 0] + arrow_size, - path[-1, 0] - arrow_size, - path[-1, 0], - path[-1, 0] + arrow_size]) - y = np.array([path[-1, 1], - path[-1, 1], - path[-1, 1] - arrow_size * self.arrow_aspect_ratio, - path[-1, 1]]) - z = np.array([path[-1, 2], - path[-1, 2], - path[-1, 2], - path[-1, 2]]) + arrow_size = abs(vector[1]) * self.arrow_size_percent / 100 + x = np.array( + [path[-1, 0] + arrow_size, path[-1, 0] - arrow_size, path[-1, 0], path[-1, 0] + arrow_size] + ) + y = np.array( + [path[-1, 1], path[-1, 1], path[-1, 1] - arrow_size * self.arrow_aspect_ratio, path[-1, 1]] + ) + z = np.array([path[-1, 2], path[-1, 2], path[-1, 2], path[-1, 2]]) elif vector[0] < 0: # calculate arrow size based on vector - arrow_size = abs(vector[0])*self.arrow_size_percent / 100 - x = np.array([path[-1, 0], - path[-1, 0], - path[-1, 0] - arrow_size * self.arrow_aspect_ratio, - path[-1, 0]]) - y = np.array([path[-1, 1] + arrow_size, - path[-1, 1] - arrow_size, - path[-1, 1], - path[-1, 1] + arrow_size]) - z = np.array([path[-1, 2], - path[-1, 2], - path[-1, 2], - path[-1, 2]]) + arrow_size = abs(vector[0]) * self.arrow_size_percent / 100 + x = np.array( + [path[-1, 0], path[-1, 0], path[-1, 0] - arrow_size * self.arrow_aspect_ratio, path[-1, 0]] + ) + y = np.array( + [path[-1, 1] + arrow_size, path[-1, 1] - arrow_size, path[-1, 1], path[-1, 1] + arrow_size] + ) + z = np.array([path[-1, 2], path[-1, 2], path[-1, 2], path[-1, 2]]) else: # calculate arrow size based on vector - arrow_size = abs(vector[0])*self.arrow_size_percent / 100 - x = np.array([path[-1, 0], - path[-1, 0], - path[-1, 0] + arrow_size * self.arrow_aspect_ratio, - path[-1, 0]]) - y = np.array([path[-1, 1] - arrow_size, - path[-1, 1] + arrow_size, - path[-1, 1], - path[-1, 1] - arrow_size]) - z = np.array([path[-1, 2], - path[-1, 2], - path[-1, 2], - path[-1, 2]]) + arrow_size = abs(vector[0]) * self.arrow_size_percent / 100 + x = np.array( + [path[-1, 0], path[-1, 0], path[-1, 0] + arrow_size * self.arrow_aspect_ratio, path[-1, 0]] + ) + y = np.array( + [path[-1, 1] - arrow_size, path[-1, 1] + arrow_size, path[-1, 1], path[-1, 1] - arrow_size] + ) + z = np.array([path[-1, 2], path[-1, 2], path[-1, 2], path[-1, 2]]) xyz = np.transpose(np.array([x, y, z])) - kwds['pos'] = np.concatenate((path, xyz), axis=0) + kwds["pos"] = np.concatenate((path, xyz), axis=0) num_tiles = len(path) path_gradient = np.zeros((num_tiles, 4)) @@ -98,10 +81,10 @@ def setData(self, **kwds): # fill in (rgb)a first with linear weighted average start = QColor(self.path_start_color).getRgbF() end = QColor(self.path_end_color).getRgbF() - path_gradient[tile, :] = \ - (num_tiles - tile) / num_tiles * np.array(start) + \ - (tile / num_tiles) * np.array(end) + path_gradient[tile, :] = (num_tiles - tile) / num_tiles * np.array(start) + ( + tile / num_tiles + ) * np.array(end) colors = np.repeat([path_gradient[-1, :]], repeats=4, axis=0) - kwds['color'] = np.concatenate((path_gradient, colors), axis=0) + kwds["color"] = np.concatenate((path_gradient, colors), axis=0) super().setData(**kwds) diff --git a/src/view/widgets/miscellaneous_widgets/gl_shaded_box_item.py b/src/view/widgets/miscellaneous_widgets/gl_shaded_box_item.py index bfef88b..3813323 100644 --- a/src/view/widgets/miscellaneous_widgets/gl_shaded_box_item.py +++ b/src/view/widgets/miscellaneous_widgets/gl_shaded_box_item.py @@ -1,25 +1,37 @@ -from pyqtgraph.opengl import GLMeshItem import numpy as np -from qtpy.QtGui import QColor from OpenGL.GL import * # noqa +from pyqtgraph.opengl import GLMeshItem +from qtpy.QtGui import QColor class GLShadedBoxItem(GLMeshItem): - """Subclass of GLMeshItem creates a rectangular mesh item""" - - def __init__(self, pos: np.ndarray, - size: np.ndarray, - color: str = 'cyan', - width: float = 1, - opacity: float = 1, - *args, - **kwargs): - """ - :param pos: position of item - :param size: size of item - :param color: color of item + """ + Subclass of GLMeshItem creates a rectangular mesh item. + """ + + def __init__( + self, + pos: np.ndarray, + size: np.ndarray, + color: str = "cyan", + width: float = 1, + opacity: float = 1, + *args, + **kwargs, + ): + """_summary_ + + :param pos: _description_ + :type pos: np.ndarray + :param size: _description_ + :type size: np.ndarray + :param color: _description_, defaults to 'cyan' + :type color: str, optional + :param width: _description_, defaults to 1 + :type width: float, optional + :param opacity: _description_, defaults to 1 + :type opacity: float, optional """ - self._size = size self._width = width self._opacity = opacity @@ -29,26 +41,44 @@ def __init__(self, pos: np.ndarray, self._pos = pos self._vertexes, self._faces = self._create_box(pos, size) - super().__init__(vertexes=self._vertexes, faces=self._faces, faceColors=colors, - drawEdges=True, edgeColor=(0, 0, 0, 1), *args, **kwargs) + super().__init__( + vertexes=self._vertexes, + faces=self._faces, + faceColors=colors, + drawEdges=True, + edgeColor=(0, 0, 0, 1), + *args, + **kwargs, + ) def _create_box(self, pos: np.ndarray, size: np.ndarray) -> (np.ndarray, np.ndarray): + """_summary_ + + :param self: _description_ + :type self: _type_ + :param np: _description_ + :type np: _type_ + :return: _description_ + :rtype: _type_ """ - Convenience method create the vertexes and faces of box to draw - :param pos: position of upper right corner of box - :param size: x,y,z size of box - :return: - """ - nCubes = np.prod(pos.shape[:-1]) cubeVerts = np.mgrid[0:2, 0:2, 0:2].reshape(3, 8).transpose().reshape(1, 8, 3) - cubeFaces = np.array([ - [0, 1, 2], [3, 2, 1], - [4, 5, 6], [7, 6, 5], - [0, 1, 4], [5, 4, 1], - [2, 3, 6], [7, 6, 3], - [0, 2, 4], [6, 4, 2], - [1, 3, 5], [7, 5, 3]]).reshape(1, 12, 3) + cubeFaces = np.array( + [ + [0, 1, 2], + [3, 2, 1], + [4, 5, 6], + [7, 6, 5], + [0, 1, 4], + [5, 4, 1], + [2, 3, 6], + [7, 6, 3], + [0, 2, 4], + [6, 4, 2], + [1, 3, 5], + [7, 5, 3], + ] + ).reshape(1, 12, 3) size = size.reshape((nCubes, 1, 3)) pos = pos.reshape((nCubes, 1, 3)) vertexes = (cubeVerts * size + pos)[0] @@ -57,18 +87,30 @@ def _create_box(self, pos: np.ndarray, size: np.ndarray) -> (np.ndarray, np.ndar return vertexes, faces def color(self) -> str or list[float, float, float, float]: - """Color of box and outline""" + """_summary_ + + :return: _description_ + :rtype: str or list[float, float, float, float] + """ return self._color def setColor(self, color: str or list[float, float, float, float]) -> None: + """_summary_ + + :param color: _description_ + :type color: strorlist[float, float, float, float] + """ self._color = color colors = np.array([self._convert_color(self._color) for i in range(12)]) self.setMeshData(vertexes=self._vertexes, faces=self._faces, faceColors=colors) def _convert_color(self, color: str) -> list[float, float, float, float]: - """ - Convenience method used to convert string color - :param color: name of color to convert to rgbF values + """_summary_ + + :param color: _description_ + :type color: str + :return: _description_ + :rtype: list[float, float, float, float] """ if isinstance(color, str): rgbf = list(QColor(color).getRgbF()) @@ -76,27 +118,30 @@ def _convert_color(self, color: str) -> list[float, float, float, float]: return color def size(self) -> np.ndarray: - """Size of box and outline""" + """_summary_ + + :return: _description_ + :rtype: np.ndarray + """ return self._size def setSize(self, x: float, y: float, z: float) -> None: + """_summary_ + + :param x: _description_ + :type x: float + :param y: _description_ + :type y: float + :param z: _description_ + :type z: float """ - Set size of box - :param x: size in the x dimension - :param y: size in the y dimension - :param z: size in the z dimension - """ - self._size = np.array([x, y, z]) self._vertexes, self._faces = self._create_box(self._pos, self._size) colors = np.array([self._convert_color(self._color) for i in range(12)]) - self.setMeshData(vertexes=self._vertexes, - faces=self._faces, - faceColors=colors) + self.setMeshData(vertexes=self._vertexes, faces=self._faces, faceColors=colors) def paint(self) -> None: - """Overwriting to include box outline""" - + """_summary_""" super().paint() self.setupGLState() diff --git a/src/view/widgets/miscellaneous_widgets/q_clickable_label.py b/src/view/widgets/miscellaneous_widgets/q_clickable_label.py index f98b867..08720ab 100644 --- a/src/view/widgets/miscellaneous_widgets/q_clickable_label.py +++ b/src/view/widgets/miscellaneous_widgets/q_clickable_label.py @@ -1,16 +1,20 @@ -from qtpy.QtWidgets import QLabel from qtpy.QtCore import Signal from qtpy.QtGui import QMouseEvent +from qtpy.QtWidgets import QLabel + class QClickableLabel(QLabel): - """QLabel that emits signal when clicked""" + """ + QLabel that emits signal when clicked. + """ clicked = Signal() def mousePressEvent(self, ev: QMouseEvent, **kwargs) -> None: - """ - Overwriting to emit signal - :param ev: mouse click event + """_summary_ + + :param ev: _description_ + :type ev: QMouseEvent """ self.clicked.emit() super().mousePressEvent(ev, **kwargs) \ No newline at end of file diff --git a/src/view/widgets/miscellaneous_widgets/q_dock_widget_title_bar.py b/src/view/widgets/miscellaneous_widgets/q_dock_widget_title_bar.py index 1aaf41b..741f6f0 100644 --- a/src/view/widgets/miscellaneous_widgets/q_dock_widget_title_bar.py +++ b/src/view/widgets/miscellaneous_widgets/q_dock_widget_title_bar.py @@ -1,19 +1,21 @@ -from qtpy.QtWidgets import QFrame, QPushButton, QStyle, QDockWidget, QLabel, QHBoxLayout +from qtpy.QtCore import Property, QObject, Qt, QTimer, Signal, Slot from qtpy.QtGui import QMouseEvent -from qtpy.QtCore import Signal, QTimer, Property, QObject, Slot, Qt +from qtpy.QtWidgets import QDockWidget, QFrame, QHBoxLayout, QLabel, QPushButton, QStyle class QDockWidgetTitleBar(QFrame): - """Widget to act as a QDockWidget title bar. Will allow user to collapse, expand, pop out, and close widget""" + """ + Widget to act as a QDockWidget title bar. Will allow user to collapse, expand, pop out, and close widget. + """ resized = Signal() def __init__(self, dock: QDockWidget, *args, **kwargs): + """_summary_ + :param dock: _description_ + :type dock: QDockWidget """ - :param dock: QDockWidget that widget will be placed in - """ - super().__init__(*args, **kwargs) self._timeline = None @@ -66,18 +68,21 @@ def __init__(self, dock: QDockWidget, *args, **kwargs): self.setLayout(layout) def close(self) -> None: - """Close widget""" - + """ + Close widget. + """ self.dock.close() def pop_out(self) -> None: - """Pop out widget""" - + """ + Pop out widget. + """ self.dock.setFloating(not self.dock.isFloating()) def minimize(self) -> None: - """Minimize widget""" - + """ + Minimize widget. + """ self.dock.setMinimumHeight(25) self.current_height = self.dock.widget().height() self._timeline = TimeLine(loopCount=1, interval=1, step_size=-5) @@ -86,8 +91,9 @@ def minimize(self) -> None: self._timeline.start() def maximize(self) -> None: - """Minimize widget""" - + """ + Minimize widget. + """ if self.current_height is not None: self._timeline = TimeLine(loopCount=1, interval=1, step_size=5) self._timeline.timerEnded.connect(lambda: self.dock.setMinimumHeight(25)) @@ -97,11 +103,11 @@ def maximize(self) -> None: self._timeline.start() def set_widget_size(self, i) -> None: - """ - Change size of widget based on qtimer - :param i: height to set widgets that will iterate based on qtimer - """ + """_summary_ + :param i: _description_ + :type i: _type_ + """ self.dock.widget().resize(self.dock.widget().width(), int(i)) self.dock.resize(self.dock.width(), int(i)) if i > self.dock.minimumHeight(): @@ -110,9 +116,10 @@ def set_widget_size(self, i) -> None: self.resized.emit() def mousePressEvent(self, event: QMouseEvent) -> None: - """ - Overwrite to update initial pos of mouse - :param event: mouse press event + """_summary_ + + :param event: _description_ + :type event: QMouseEvent """ if event.button() == Qt.MouseButton.LeftButton: self.initial_pos = event.position().toPoint() @@ -120,10 +127,10 @@ def mousePressEvent(self, event: QMouseEvent) -> None: event.accept() def mouseMoveEvent(self, event: QMouseEvent) -> None: - """ - Overwrite to move window when mouse is dragged - :param event: mouse event - :return: + """_summary_ + + :param event: _description_ + :type event: QMouseEvent """ if self.initial_pos is not None: delta = event.position().toPoint() - self.initial_pos @@ -134,16 +141,24 @@ def mouseMoveEvent(self, event: QMouseEvent) -> None: super().mouseMoveEvent(event) event.accept() + class TimeLine(QObject): + """_summary_""" + frameChanged = Signal(float) timerEnded = Signal() def __init__(self, interval=60, loopCount=1, step_size=1, parent=None): - """ - :param interval: interval at which to step up and emit value in milliseconds - :param loopCount: how many times to repeat timeline - :param step_size: step size to take between emitted values - :param parent: parent of widget + """_summary_ + + :param interval: _description_, defaults to 60 + :type interval: int, optional + :param loopCount: _description_, defaults to 1 + :type loopCount: int, optional + :param step_size: _description_, defaults to 1 + :type step_size: int, optional + :param parent: _description_, defaults to None + :type parent: _type_, optional """ super(TimeLine, self).__init__(parent) self._stepSize = step_size @@ -157,11 +172,11 @@ def __init__(self, interval=60, loopCount=1, step_size=1, parent=None): def on_timeout(self) -> None: """ - Function called by Qtimer that will trigger a step of current step_size and emit new counter value + Function called by Qtimer that will trigger a step of current step_size and emit new counter value. """ - - if (self._startFrame <= self._counter <= self._endFrame and self._stepSize > 0) or \ - (self._startFrame >= self._counter >= self._endFrame and self._stepSize < 0): + if (self._startFrame <= self._counter <= self._endFrame and self._stepSize > 0) or ( + self._startFrame >= self._counter >= self._endFrame and self._stepSize < 0 + ): self.frameChanged.emit(self._counter) self._counter += self._stepSize else: @@ -173,58 +188,60 @@ def on_timeout(self) -> None: self.timerEnded.emit() def setLoopCount(self, loopCount: int) -> None: - """ - Function set loop count variable - :param loopCount: integer specifying how many times to repeat timeline + """_summary_ + + :param loopCount: _description_ + :type loopCount: int """ self._loopCount = loopCount def loopCount(self) -> int: - """ - Current loop count - :return: Current loop count + """_summary_ + + :return: _description_ + :rtype: int """ return self._loopCount interval = Property(int, fget=loopCount, fset=setLoopCount) def setInterval(self, interval: int) -> None: - """ - Function to set interval variable in seconds - :param interval: integer specifying the length of timeline in milliseconds + """_summary_ + + :param interval: _description_ + :type interval: int """ self._timer.setInterval(interval) def interval(self) -> int: - """ - Current interval time in milliseconds - :return: integer value of current interval time in milliseconds + """_summary_ + + :return: _description_ + :rtype: int """ return self._timer.interval() interval = Property(int, fget=interval, fset=setInterval) def setFrameRange(self, startFrame: float, endFrame: float) -> None: - """ - Setting function for starting and end value that timeline will step through - :param startFrame: starting value - :param endFrame: ending value + """_summary_ + + :param startFrame: _description_ + :type startFrame: float + :param endFrame: _description_ + :type endFrame: float """ self._startFrame = startFrame self._endFrame = endFrame @Slot() - def start(self)-> None: - """ - Function to start QTimer and begin emitting and stepping through value - """ + def start(self) -> None: + """_summary_""" self._counter = self._startFrame self._loop_counter = 0 self._timer.start() - def stop(self)-> None: - """ - Function to stop QTimer and stop stepping through values - """ + def stop(self) -> None: + """_summary_""" self._timer.stop() - self.timerEnded.emit() \ No newline at end of file + self.timerEnded.emit() diff --git a/src/view/widgets/miscellaneous_widgets/q_item_delegates.py b/src/view/widgets/miscellaneous_widgets/q_item_delegates.py index 22ba9f5..d29c0f4 100644 --- a/src/view/widgets/miscellaneous_widgets/q_item_delegates.py +++ b/src/view/widgets/miscellaneous_widgets/q_item_delegates.py @@ -1,45 +1,135 @@ -from qtpy.QtWidgets import QStyledItemDelegate, QTextEdit, QSpinBox, QComboBox, QDoubleSpinBox +from qtpy.QtWidgets import QComboBox, QDoubleSpinBox, QSpinBox, QStyledItemDelegate, QTextEdit + class QTextItemDelegate(QStyledItemDelegate): - """QStyledItemDelegate acting like QTextEdit""" + """ + QStyledItemDelegate acting like QTextEdit. + """ def createEditor(self, parent, options, index): + """_summary_ + + :param parent: _description_ + :type parent: _type_ + :param options: _description_ + :type options: _type_ + :param index: _description_ + :type index: _type_ + :return: _description_ + :rtype: _type_ + """ return QTextEdit(parent) def setEditorData(self, editor, index): + """_summary_ + + :param editor: _description_ + :type editor: _type_ + :param index: _description_ + :type index: _type_ + """ editor.setText(str(index.data())) def setModelData(self, editor, model, index): + """_summary_ + + :param editor: _description_ + :type editor: _type_ + :param model: _description_ + :type model: _type_ + :param index: _description_ + :type index: _type_ + """ model.setData(index, editor.toPlainText()) class QComboItemDelegate(QStyledItemDelegate): - """QStyledItemDelegate acting like QComboBox""" + """ + QStyledItemDelegate acting like QComboBox. + """ def __init__(self, items: list, parent=None): + """_summary_ + + :param items: _description_ + :type items: list + :param parent: _description_, defaults to None + :type parent: _type_, optional + """ super().__init__(parent) self.items = items def createEditor(self, parent, options, index): + """_summary_ + + :param parent: _description_ + :type parent: _type_ + :param options: _description_ + :type options: _type_ + :param index: _description_ + :type index: _type_ + :return: _description_ + :rtype: _type_ + """ return QComboBox(parent) def setEditorData(self, editor, index): + """_summary_ + + :param editor: _description_ + :type editor: _type_ + :param index: _description_ + :type index: _type_ + """ editor.addItems(self.items) def setModelData(self, editor, model, index): + """_summary_ + + :param editor: _description_ + :type editor: _type_ + :param model: _description_ + :type model: _type_ + :param index: _description_ + :type index: _type_ + """ model.setData(index, editor.currentText()) class QSpinItemDelegate(QStyledItemDelegate): - """QStyledItemDelegate acting like QSpinBox""" + """ + QStyledItemDelegate acting like QSpinBox. + """ def __init__(self, minimum=None, maximum=None, step=None, parent=None): + """_summary_ + + :param minimum: _description_, defaults to None + :type minimum: _type_, optional + :param maximum: _description_, defaults to None + :type maximum: _type_, optional + :param step: _description_, defaults to None + :type step: _type_, optional + :param parent: _description_, defaults to None + :type parent: _type_, optional + """ super().__init__(parent) self.minimum = minimum if minimum is not None else -2147483647 self.maximum = maximum if maximum is not None else 2147483647 - self.step = step if step is not None else .01 + self.step = step if step is not None else 0.01 def createEditor(self, parent, options, index): + """_summary_ + + :param parent: _description_ + :type parent: _type_ + :param options: _description_ + :type options: _type_ + :param index: _description_ + :type index: _type_ + :return: _description_ + :rtype: _type_ + """ box = QSpinBox(parent) if type(self.step) == int else QDoubleSpinBox(parent) box.setMinimum(self.minimum) @@ -50,9 +140,25 @@ def createEditor(self, parent, options, index): return box def setEditorData(self, editor, index): + """_summary_ + + :param editor: _description_ + :type editor: _type_ + :param index: _description_ + :type index: _type_ + """ value = int(index.data()) if type(self.step) == int else float(index.data()) editor.setValue(value) def setModelData(self, editor, model, index): + """_summary_ + + :param editor: _description_ + :type editor: _type_ + :param model: _description_ + :type model: _type_ + :param index: _description_ + :type index: _type_ + """ value = int(editor.value()) if type(self.step) == int else float(editor.value()) model.setData(index, value) diff --git a/src/view/widgets/miscellaneous_widgets/q_non_scrollable_tree_widget.py b/src/view/widgets/miscellaneous_widgets/q_non_scrollable_tree_widget.py index 009ddd9..c5f2463 100644 --- a/src/view/widgets/miscellaneous_widgets/q_non_scrollable_tree_widget.py +++ b/src/view/widgets/miscellaneous_widgets/q_non_scrollable_tree_widget.py @@ -1,7 +1,13 @@ from qtpy.QtWidgets import QTreeWidget -class QNonScrollableTreeWidget(QTreeWidget): - """Disable mouse wheel scroll""" +class QNonScrollableTreeWidget(QTreeWidget): + """_summary_ + """ def wheelEvent(self, event): - pass \ No newline at end of file + """_summary_ + + :param event: _description_ + :type event: _type_ + """ + pass diff --git a/src/view/widgets/miscellaneous_widgets/q_scrollable_float_slider.py b/src/view/widgets/miscellaneous_widgets/q_scrollable_float_slider.py index e254392..025432a 100644 --- a/src/view/widgets/miscellaneous_widgets/q_scrollable_float_slider.py +++ b/src/view/widgets/miscellaneous_widgets/q_scrollable_float_slider.py @@ -3,49 +3,119 @@ class QScrollableFloatSlider(QSlider): - """QSlider that will emit signal if scrolled with mouse wheel and allow float values""" + """ + QSlider that will emit signal if scrolled with mouse wheel and allow float values. + """ + sliderMoved = Signal(float) # redefine slider move to emit float def __init__(self, decimals=0, *args, **kwargs): + """_summary_ + + :param decimals: _description_, defaults to 0 + :type decimals: int, optional + """ super().__init__(*args, **kwargs) - self.divisor = 10 ** decimals + self.divisor = 10**decimals def value(self): + """_summary_ + + :return: _description_ + :rtype: _type_ + """ return float(super().value()) / self.divisor def setMinimum(self, value): + """_summary_ + + :param value: _description_ + :type value: _type_ + :return: _description_ + :rtype: _type_ + """ return super().setMinimum(int(value * self.divisor)) def setMaximum(self, value): + """_summary_ + + :param value: _description_ + :type value: _type_ + :return: _description_ + :rtype: _type_ + """ return super().setMaximum(int(value * self.divisor)) def maximum(self): + """_summary_ + + :return: _description_ + :rtype: _type_ + """ return super().maximum() / self.divisor def minimum(self): + """_summary_ + + :return: _description_ + :rtype: _type_ + """ return super().minimum() / self.divisor def setSingleStep(self, value): + """_summary_ + + :param value: _description_ + :type value: _type_ + :return: _description_ + :rtype: _type_ + """ return super().setSingleStep(value * self.divisor) def singleStep(self): + """_summary_ + + :return: _description_ + :rtype: _type_ + """ return float(super().singleStep()) / self.divisor def setValue(self, value): + """_summary_ + + :param value: _description_ + :type value: _type_ + """ super().setValue(int(value * self.divisor)) def wheelEvent(self, event): + """_summary_ + + :param event: _description_ + :type event: _type_ + """ super().wheelEvent(event) value = self.value() self.sliderMoved.emit(value) self.sliderReleased.emit() + def mouseMoveEvent(self, event): + """_summary_ + + :param event: _description_ + :type event: _type_ + """ super().mouseMoveEvent(event) if event.buttons() == Qt.MouseButton.LeftButton: value = self.value() self.sliderMoved.emit(value) def mousePressEvent(self, event): + """_summary_ + + :param event: _description_ + :type event: _type_ + """ super().mousePressEvent(event) value = self.value() self.sliderMoved.emit(value) diff --git a/src/view/widgets/miscellaneous_widgets/q_scrollable_line_edit.py b/src/view/widgets/miscellaneous_widgets/q_scrollable_line_edit.py index 5c0e473..1159a32 100644 --- a/src/view/widgets/miscellaneous_widgets/q_scrollable_line_edit.py +++ b/src/view/widgets/miscellaneous_widgets/q_scrollable_line_edit.py @@ -1,16 +1,23 @@ +from qtpy.QtGui import QDoubleValidator, QIntValidator from qtpy.QtWidgets import QLineEdit -from qtpy.QtGui import QIntValidator, QDoubleValidator -import typing + class QScrollableLineEdit(QLineEdit): - """Widget inheriting from QLineEdit that allows value to be scrollable""" + """ + Widget inheriting from QLineEdit that allows value to be scrollable. + """ def wheelEvent(self, event): + """_summary_ + + :param event: _description_ + :type event: _type_ + """ super().wheelEvent(event) if self.validator() is not None and type(self.validator()) in [QIntValidator, QDoubleValidator]: if type(self.validator()) == QDoubleValidator: - dec = len(self.text()[self.text().index('.') + 1:]) if '.' in self.text() else 0 - change = 10 ** (-dec) if event.angleDelta().y() > 0 else -10 ** (-dec) + dec = len(self.text()[self.text().index(".") + 1 :]) if "." in self.text() else 0 + change = 10 ** (-dec) if event.angleDelta().y() > 0 else -(10 ** (-dec)) new_value = float(f"%.{dec}f" % float(float(self.text()) + change)) else: # QIntValidator new_value = int(self.text()) + 1 if event.angleDelta().y() > 0 else int(self.text()) - 1 @@ -18,11 +25,18 @@ def wheelEvent(self, event): self.setText(str(new_value)) self.editingFinished.emit() - def value(self): - """Get float or integer of text""" + """_summary_ + + :return: _description_ + :rtype: _type_ + """ return float(self.text()) def setValue(self, value): - """Set number as text""" + """_summary_ + + :param value: _description_ + :type value: _type_ + """ self.setText(str(value)) diff --git a/src/view/widgets/miscellaneous_widgets/q_start_stop_table_header.py b/src/view/widgets/miscellaneous_widgets/q_start_stop_table_header.py index 4341461..a97bb94 100644 --- a/src/view/widgets/miscellaneous_widgets/q_start_stop_table_header.py +++ b/src/view/widgets/miscellaneous_widgets/q_start_stop_table_header.py @@ -1,16 +1,23 @@ -from qtpy.QtWidgets import QTableWidgetItem, QHeaderView, QMenu, QAction, QStyle from qtpy.QtCore import Qt, Signal from qtpy.QtGui import QMouseEvent +from qtpy.QtWidgets import QAction, QHeaderView, QMenu, QStyle, QTableWidgetItem class QStartStopTableHeader(QHeaderView): - """QTableWidgetItem to be used to select certain tiles to start at""" + """ + QTableWidgetItem to be used to select certain tiles to start at. + """ sectionRightClicked = Signal(QMouseEvent) startChanged = Signal(int) stopChanged = Signal(int) def __init__(self, parent): + """_summary_ + + :param parent: _description_ + :type parent: _type_ + """ super().__init__(Qt.Vertical, parent) self.start = None @@ -19,8 +26,10 @@ def __init__(self, parent): self.sectionRightClicked.connect(self.menu_popup) def mousePressEvent(self, event, **kwargs): - """Detect click event and set correct setting - :param **kwargs: + """_summary_ + + :param event: _description_ + :type event: _type_ """ super().mousePressEvent(event, **kwargs) @@ -28,12 +37,11 @@ def mousePressEvent(self, event, **kwargs): self.sectionRightClicked.emit(event) def menu_popup(self, event: QMouseEvent): - """ - Function to set tile start or stop - :param event: mousePressEvent of click - :return: - """ + """_summary_ + :param event: _description_ + :type event: QMouseEvent + """ index = self.logicalIndexAt(event.pos()) start_act = QAction("Set Start", self) @@ -55,12 +63,11 @@ def menu_popup(self, event: QMouseEvent): menu.popup(self.mapToGlobal(event.pos())) def set_start(self, index: int): - """ - Set start tile - :param index: index to set to start at - :return: - """ + """_summary_ + :param index: _description_ + :type index: int + """ if self.start is not None: self.clear(self.start) @@ -73,12 +80,11 @@ def set_start(self, index: int): self.startChanged.emit(index) def set_stop(self, index: int): - """ - Set stop tile - :param index: index to set to stop at - :return: - """ + """_summary_ + :param index: _description_ + :type index: int + """ if self.stop is not None: self.clear(self.stop) @@ -91,18 +97,16 @@ def set_stop(self, index: int): self.stopChanged.emit(index) def clear(self, index: int): - """ - Clear index of start or stop - :param index: - :return: - """ - + """_summary_ + :param index: _description_ + :type index: int + """ if index == self.stop: self.stop = None elif index == self.start: self.start = None item = QTableWidgetItem() - item.setText(str(index+1)) + item.setText(str(index + 1)) self.parent().setVerticalHeaderItem(index, item) diff --git a/tests/test_acquisition_view.py b/tests/test_acquisition_view.py index e0a6b05..146a88b 100644 --- a/tests/test_acquisition_view.py +++ b/tests/test_acquisition_view.py @@ -1,135 +1,104 @@ -""" testing AcquisitionView """ - -import unittest -from view.acquisition_view import AcquisitionView -from qtpy.QtWidgets import QApplication, QWidget -from qtpy.QtCore import Qt import sys -import numpy as np +import unittest from unittest.mock import MagicMock -from pathlib import Path -import os + +from qtpy.QtCore import Qt +from qtpy.QtTest import QSignalSpy +from qtpy.QtWidgets import QApplication + +from view.acquisition_view import AcquisitionView +from view.widgets.device_widgets.laser_widget import LaserWidget from view.widgets.device_widgets.stage_widget import StageWidget -from voxel.devices.lasers.simulated import SimulatedLaser +from voxel.devices.laser.simulated import SimulatedLaser from voxel.devices.stage.simulated import Stage -from view.widgets.device_widgets.laser_widget import LaserWidget -from threading import Lock -from qtpy.QtTest import QTest, QSignalSpy app = QApplication(sys.argv) class AcquisitionViewTests(unittest.TestCase): - """Tests for AcquisitionView""" - - # TODO: A lot more to test for + """_summary_ + """ def test_update_tiles(self): - """test update_tiles functions""" - + """_summary_""" channels = { - '488': { - 'filters': ['BP488'], - 'lasers': ['488nm'], - 'cameras': ['vnp - 604mx', 'vp-151mx']}, - '639': { - 'filters': ['LP638'], - 'lasers': ['639nm'], - 'cameras': ['vnp - 604mx', 'vp-151mx']} + "488": {"filters": ["BP488"], "lasers": ["488nm"], "cameras": ["vnp - 604mx", "vp-151mx"]}, + "639": {"filters": ["LP638"], "lasers": ["639nm"], "cameras": ["vnp - 604mx", "vp-151mx"]}, } properties = { - 'lasers': ['power_setpoint_mw'], - 'focusing_stages': ['position_mm'], - 'start_delay_time': { - 'delegate': 'spin', - 'type': 'float', - 'minimum': 0, - 'initial_value': 15, + "lasers": ["power_setpoint_mw"], + "focusing_stages": ["position_mm"], + "start_delay_time": { + "delegate": "spin", + "type": "float", + "minimum": 0, + "initial_value": 15, }, - 'repeats': { - 'delegate': 'spin', - 'type': 'int', - 'minimum': 0, + "repeats": { + "delegate": "spin", + "type": "int", + "minimum": 0, + }, + "example": { + "delegate": "combo", + "type": "str", + "items": ["this", "is", "an", "example"], + "initial_value": "example", }, - 'example': { - 'delegate': 'combo', - 'type': 'str', - 'items': ['this', 'is', 'an', 'example'], - 'initial_value': 'example' - } } lasers = { - '488nm': SimulatedLaser(id='hello', wavelength=488), - '639nm': SimulatedLaser(id='there', wavelength=639) + "488nm": SimulatedLaser(id="hello", wavelength=488), + "639nm": SimulatedLaser(id="there", wavelength=639), } tiling_stages = { - 'x': Stage(hardware_axis='x', instrument_axis='x'), - 'y': Stage(hardware_axis='y', instrument_axis='y') + "x": Stage(hardware_axis="x", instrument_axis="x"), + "y": Stage(hardware_axis="y", instrument_axis="y"), } - scanning_stages = { - 'z': Stage(hardware_axis='z', instrument_axis='z') - } + scanning_stages = {"z": Stage(hardware_axis="z", instrument_axis="z")} - focusing_stages = { - 'n': Stage(hardware_axis='n', instrument_axis='n') - } + focusing_stages = {"n": Stage(hardware_axis="n", instrument_axis="n")} focusing_stage_widgets = { - 'n': StageWidget(focusing_stages['n']), + "n": StageWidget(focusing_stages["n"]), } - laser_widgets = { - '488nm': LaserWidget(lasers['488nm']), - '639nm': LaserWidget(lasers['639nm']) - } + laser_widgets = {"488nm": LaserWidget(lasers["488nm"]), "639nm": LaserWidget(lasers["639nm"])} gui_config = { - 'acquisition_view': { - 'coordinate_plane': ['x', 'y', 'z'], - 'unit': 'mm', - 'fov_dimensions': [1, 1, 0], - 'acquisition_widgets': { - 'channel_plan': { - 'init': { - 'properties': properties - } - } - } + "acquisition_view": { + "coordinate_plane": ["x", "y", "z"], + "unit": "mm", + "fov_dimensions": [1, 1, 0], + "acquisition_widgets": {"channel_plan": {"init": {"properties": properties}}}, } } - instrument_config = { - 'instrument': { - 'channels': channels - } - } + instrument_config = {"instrument": {"channels": channels}} - acquisition_config = { - 'acquisition': { - 'operations': {}, - 'tiles': [] - } - } + acquisition_config = {"acquisition": {"operations": {}, "tiles": []}} mocked_instrument = MagicMock() - mocked_instrument.configure_mock(config=instrument_config, - lasers=lasers, - tiling_stages=tiling_stages, - scanning_stages=scanning_stages, - focusing_stages=focusing_stages) + mocked_instrument.configure_mock( + config=instrument_config, + lasers=lasers, + tiling_stages=tiling_stages, + scanning_stages=scanning_stages, + focusing_stages=focusing_stages, + ) mocked_instrument_view = MagicMock() - mocked_instrument_view.configure_mock(instrument=mocked_instrument, - laser_widgets=laser_widgets, - focusing_stage_widgets=focusing_stage_widgets, - config=gui_config) + mocked_instrument_view.configure_mock( + instrument=mocked_instrument, + laser_widgets=laser_widgets, + focusing_stage_widgets=focusing_stage_widgets, + config=gui_config, + ) mocked_acquisition = MagicMock() - mocked_acquisition.configure_mock(instrument=mocked_instrument, - config=acquisition_config) + mocked_acquisition.configure_mock(instrument=mocked_instrument, config=acquisition_config) view = AcquisitionView(mocked_acquisition, mocked_instrument_view) @@ -142,256 +111,202 @@ def test_update_tiles(self): self.assertEqual(len(valueChanged_spy), 1) # triggered once self.assertTrue(valueChanged_spy.isValid()) - view.channel_plan.add_channel('488') + view.channel_plan.add_channel("488") # check channel added is emitted once self.assertEqual(len(channelsAdded_spy), 1) # triggered once self.assertTrue(channelsAdded_spy.isValid()) - expected_tiles = [{ - 'channel': '488', - 'position_mm': { - 'x': 0.0, - 'y': 0.5, - 'z': 0.0 - }, - 'tile_number': 0, - '488nm': { - 'power_setpoint_mw': 10.0 + expected_tiles = [ + { + "channel": "488", + "position_mm": {"x": 0.0, "y": 0.5, "z": 0.0}, + "tile_number": 0, + "488nm": {"power_setpoint_mw": 10.0}, + "start_delay_time": 15.0, + "repeats": 0, + "example": "example", + "steps": 0, + "step_size": 0.0, + "prefix": "", }, - 'start_delay_time': 15.0, - 'repeats': 0, - 'example': 'example', - 'steps': 0, - 'step_size': 0.0, - 'prefix': ''}, { - 'channel': '488', - 'position_mm': { - 'x': 0.0, - 'y': -0.5, - 'z': 0.0 - }, - 'tile_number': 1, - '488nm': { - 'power_setpoint_mw': 10.0 - }, - 'start_delay_time': 15.0, - 'repeats': 0, - 'example': 'example', - 'steps': 0, - 'step_size': 0.0, - 'prefix': ''}] - - actual_tiles = mocked_acquisition.config['acquisition']['tiles'] + "channel": "488", + "position_mm": {"x": 0.0, "y": -0.5, "z": 0.0}, + "tile_number": 1, + "488nm": {"power_setpoint_mw": 10.0}, + "start_delay_time": 15.0, + "repeats": 0, + "example": "example", + "steps": 0, + "step_size": 0.0, + "prefix": "", + }, + ] + + actual_tiles = mocked_acquisition.config["acquisition"]["tiles"] self.assertEqual(expected_tiles, actual_tiles) - table = getattr(view.channel_plan, '488_table') - table.item(0, 2).setData(Qt.EditRole, 'tile_prefix') + table = getattr(view.channel_plan, "488_table") + table.item(0, 2).setData(Qt.EditRole, "tile_prefix") # check channel changed is emitted once self.assertEqual(len(channelChanged_spy), 1) # triggered once self.assertTrue(channelChanged_spy.isValid()) - expected_tiles = [{ - 'channel': '488', - 'position_mm': { - 'x': 0.0, - 'y': 0.5, - 'z': 0.0 - }, - 'tile_number': 0, - '488nm': { - 'power_setpoint_mw': 10.0 + expected_tiles = [ + { + "channel": "488", + "position_mm": {"x": 0.0, "y": 0.5, "z": 0.0}, + "tile_number": 0, + "488nm": {"power_setpoint_mw": 10.0}, + "start_delay_time": 15.0, + "repeats": 0, + "example": "example", + "steps": 0, + "step_size": 0.0, + "prefix": "tile_prefix", }, - 'start_delay_time': 15.0, - 'repeats': 0, - 'example': 'example', - 'steps': 0, - 'step_size': 0.0, - 'prefix': 'tile_prefix'}, { - 'channel': '488', - 'position_mm': { - 'x': 0.0, - 'y': -0.5, - 'z': 0.0 - }, - 'tile_number': 1, - '488nm': { - 'power_setpoint_mw': 10.0 - }, - 'start_delay_time': 15.0, - 'repeats': 0, - 'example': 'example', - 'steps': 0, - 'step_size': 0.0, - 'prefix': 'tile_prefix'}] - actual_tiles = mocked_acquisition.config['acquisition']['tiles'] + "channel": "488", + "position_mm": {"x": 0.0, "y": -0.5, "z": 0.0}, + "tile_number": 1, + "488nm": {"power_setpoint_mw": 10.0}, + "start_delay_time": 15.0, + "repeats": 0, + "example": "example", + "steps": 0, + "step_size": 0.0, + "prefix": "tile_prefix", + }, + ] + actual_tiles = mocked_acquisition.config["acquisition"]["tiles"] self.assertEqual(expected_tiles, actual_tiles) def test_subset_write_tiles(self): - """test writing a subset of tiles""" - + """_summary_""" channels = { - '488': { - 'filters': ['BP488'], - 'lasers': ['488nm'], - 'cameras': ['vnp - 604mx', 'vp-151mx']}, - '639': { - 'filters': ['LP638'], - 'lasers': ['639nm'], - 'cameras': ['vnp - 604mx', 'vp-151mx']} + "488": {"filters": ["BP488"], "lasers": ["488nm"], "cameras": ["vnp - 604mx", "vp-151mx"]}, + "639": {"filters": ["LP638"], "lasers": ["639nm"], "cameras": ["vnp - 604mx", "vp-151mx"]}, } properties = { - 'lasers': ['power_setpoint_mw'], - 'focusing_stages': ['position_mm'], - 'start_delay_time': { - 'delegate': 'spin', - 'type': 'float', - 'minimum': 0, - 'initial_value': 15, + "lasers": ["power_setpoint_mw"], + "focusing_stages": ["position_mm"], + "start_delay_time": { + "delegate": "spin", + "type": "float", + "minimum": 0, + "initial_value": 15, }, - 'repeats': { - 'delegate': 'spin', - 'type': 'int', - 'minimum': 0, + "repeats": { + "delegate": "spin", + "type": "int", + "minimum": 0, + }, + "example": { + "delegate": "combo", + "type": "str", + "items": ["this", "is", "an", "example"], + "initial_value": "example", }, - 'example': { - 'delegate': 'combo', - 'type': 'str', - 'items': ['this', 'is', 'an', 'example'], - 'initial_value': 'example' - } } lasers = { - '488nm': SimulatedLaser(id='hello', wavelength=488), - '639nm': SimulatedLaser(id='there', wavelength=639) + "488nm": SimulatedLaser(id="hello", wavelength=488), + "639nm": SimulatedLaser(id="there", wavelength=639), } tiling_stages = { - 'x': Stage(hardware_axis='x', instrument_axis='x'), - 'y': Stage(hardware_axis='y', instrument_axis='y') + "x": Stage(hardware_axis="x", instrument_axis="x"), + "y": Stage(hardware_axis="y", instrument_axis="y"), } - scanning_stages = { - 'z': Stage(hardware_axis='z', instrument_axis='z') - } + scanning_stages = {"z": Stage(hardware_axis="z", instrument_axis="z")} - focusing_stages = { - 'n': Stage(hardware_axis='n', instrument_axis='n') - } + focusing_stages = {"n": Stage(hardware_axis="n", instrument_axis="n")} focusing_stage_widgets = { - 'n': StageWidget(focusing_stages['n']), + "n": StageWidget(focusing_stages["n"]), } - laser_widgets = { - '488nm': LaserWidget(lasers['488nm']), - '639nm': LaserWidget(lasers['639nm']) - } + laser_widgets = {"488nm": LaserWidget(lasers["488nm"]), "639nm": LaserWidget(lasers["639nm"])} gui_config = { - 'acquisition_view': { - 'coordinate_plane': ['x', 'y', 'z'], - 'unit': 'mm', - 'fov_dimensions': [1, 1, 0], - 'acquisition_widgets': { - 'channel_plan': { - 'init': { - 'properties': properties - } - } - } + "acquisition_view": { + "coordinate_plane": ["x", "y", "z"], + "unit": "mm", + "fov_dimensions": [1, 1, 0], + "acquisition_widgets": {"channel_plan": {"init": {"properties": properties}}}, } } - instrument_config = { - 'instrument': { - 'channels': channels - } - } + instrument_config = {"instrument": {"channels": channels}} - acquisition_config = { - 'acquisition': { - 'operations': {}, - 'tiles': [] - } - } + acquisition_config = {"acquisition": {"operations": {}, "tiles": []}} mocked_instrument = MagicMock() - mocked_instrument.configure_mock(config=instrument_config, - lasers=lasers, - tiling_stages=tiling_stages, - scanning_stages=scanning_stages, - focusing_stages=focusing_stages) + mocked_instrument.configure_mock( + config=instrument_config, + lasers=lasers, + tiling_stages=tiling_stages, + scanning_stages=scanning_stages, + focusing_stages=focusing_stages, + ) mocked_instrument_view = MagicMock() - mocked_instrument_view.configure_mock(instrument=mocked_instrument, - laser_widgets=laser_widgets, - focusing_stage_widgets=focusing_stage_widgets, - config=gui_config) + mocked_instrument_view.configure_mock( + instrument=mocked_instrument, + laser_widgets=laser_widgets, + focusing_stage_widgets=focusing_stage_widgets, + config=gui_config, + ) mocked_acquisition = MagicMock() - mocked_acquisition.configure_mock(instrument=mocked_instrument, - config=acquisition_config) + mocked_acquisition.configure_mock(instrument=mocked_instrument, config=acquisition_config) view = AcquisitionView(mocked_acquisition, mocked_instrument_view) view.volume_plan.rows.setValue(4) - view.channel_plan.add_channel('488') + view.channel_plan.add_channel("488") view.volume_plan.start = 1 - expected_tiles = [ - {'channel': '488', - 'position_mm': { - 'x': 0.0, - 'y': 0.5, - 'z': 0.0 - }, - 'tile_number': 1, - '488nm': { - 'power_setpoint_mw': 10.0 - }, - 'start_delay_time': 15.0, - 'repeats': 0, - 'example': 'example', - 'steps': 0, - 'step_size': 0.0, - 'prefix': ''}, - {'channel': '488', - 'position_mm': { - 'x': 0.0, - 'y': -0.5, - 'z': 0.0 - }, - 'tile_number': 2, - '488nm': { - 'power_setpoint_mw': 10.0 - }, - 'start_delay_time': 15.0, - 'repeats': 0, - 'example': 'example', - 'steps': 0, - 'step_size': 0.0, - 'prefix': ''}, - {'channel': '488', - 'position_mm': { - 'x': 0.0, - 'y': -1.5, - 'z': 0.0 - }, - 'tile_number': 3, - '488nm': { - 'power_setpoint_mw': 10.0 - }, - 'start_delay_time': 15.0, - 'repeats': 0, - 'example': 'example', - 'steps': 0, - 'step_size': 0.0, - 'prefix': ''}] + { + "channel": "488", + "position_mm": {"x": 0.0, "y": 0.5, "z": 0.0}, + "tile_number": 1, + "488nm": {"power_setpoint_mw": 10.0}, + "start_delay_time": 15.0, + "repeats": 0, + "example": "example", + "steps": 0, + "step_size": 0.0, + "prefix": "", + }, + { + "channel": "488", + "position_mm": {"x": 0.0, "y": -0.5, "z": 0.0}, + "tile_number": 2, + "488nm": {"power_setpoint_mw": 10.0}, + "start_delay_time": 15.0, + "repeats": 0, + "example": "example", + "steps": 0, + "step_size": 0.0, + "prefix": "", + }, + { + "channel": "488", + "position_mm": {"x": 0.0, "y": -1.5, "z": 0.0}, + "tile_number": 3, + "488nm": {"power_setpoint_mw": 10.0}, + "start_delay_time": 15.0, + "repeats": 0, + "example": "example", + "steps": 0, + "step_size": 0.0, + "prefix": "", + }, + ] actual_tiles = view.create_tile_list() self.assertEqual(expected_tiles, actual_tiles) @@ -399,46 +314,35 @@ def test_subset_write_tiles(self): view.volume_plan.stop = 3 expected_tiles = [ - {'channel': '488', - 'position_mm': { - 'x': 0.0, - 'y': 0.5, - 'z': 0.0 - }, - 'tile_number': 1, - '488nm': { - 'power_setpoint_mw': 10.0 - }, - 'start_delay_time': 15.0, - 'repeats': 0, - 'example': 'example', - 'steps': 0, - 'step_size': 0.0, - 'prefix': ''}, - {'channel': '488', - 'position_mm': { - 'x': 0.0, - 'y': -0.5, - 'z': 0.0 - }, - 'tile_number': 2, - '488nm': { - 'power_setpoint_mw': 10.0 - }, - 'start_delay_time': 15.0, - 'repeats': 0, - 'example': 'example', - 'steps': 0, - 'step_size': 0.0, - 'prefix': ''}, - ] + { + "channel": "488", + "position_mm": {"x": 0.0, "y": 0.5, "z": 0.0}, + "tile_number": 1, + "488nm": {"power_setpoint_mw": 10.0}, + "start_delay_time": 15.0, + "repeats": 0, + "example": "example", + "steps": 0, + "step_size": 0.0, + "prefix": "", + }, + { + "channel": "488", + "position_mm": {"x": 0.0, "y": -0.5, "z": 0.0}, + "tile_number": 2, + "488nm": {"power_setpoint_mw": 10.0}, + "start_delay_time": 15.0, + "repeats": 0, + "example": "example", + "steps": 0, + "step_size": 0.0, + "prefix": "", + }, + ] actual_tiles = view.create_tile_list() self.assertEqual(expected_tiles, actual_tiles) - - - if __name__ == "__main__": unittest.main() sys.exit(app.exec_()) diff --git a/tests/test_base_device_widget.py b/tests/test_base_device_widget.py index e7f1d5f..89c4598 100644 --- a/tests/test_base_device_widget.py +++ b/tests/test_base_device_widget.py @@ -1,58 +1,56 @@ -""" testing BaseDeviceWidget """ - +import sys import unittest -from view.widgets.base_device_widget import BaseDeviceWidget -from qtpy.QtTest import QTest, QSignalSpy -from qtpy.QtWidgets import QApplication, QWidget + from qtpy.QtCore import Qt -import sys +from qtpy.QtGui import QDoubleValidator, QIntValidator +from qtpy.QtTest import QSignalSpy, QTest +from qtpy.QtWidgets import QApplication + +from view.widgets.base_device_widget import BaseDeviceWidget from view.widgets.miscellaneous_widgets.q_scrollable_line_edit import QScrollableLineEdit -from qtpy.QtGui import QIntValidator, QDoubleValidator app = QApplication(sys.argv) class BaseDeviceWidgetTests(unittest.TestCase): - """tests for BaseDeviceWidget""" + """_summary_""" def test_string_properties(self): - """Test that BaseDeviceWidget can correctly handle properties that are strings""" - - properties = {'test_string': 'hello'} + """_summary_""" + properties = {"test_string": "hello"} widget = BaseDeviceWidget(properties, properties) - self.assertTrue(hasattr(widget, 'test_string')) - self.assertTrue(widget.test_string == 'hello') - self.assertTrue(hasattr(widget, 'test_string_widget')) + self.assertTrue(hasattr(widget, "test_string")) + self.assertTrue(widget.test_string == "hello") + self.assertTrue(hasattr(widget, "test_string_widget")) self.assertTrue(type(widget.test_string_widget) == QScrollableLineEdit) - self.assertTrue(widget.test_string_widget.text() == 'hello') + self.assertTrue(widget.test_string_widget.text() == "hello") # change value externally outside_signal_spy = QSignalSpy(widget.ValueChangedOutside) - widget.test_string = 'howdy' + widget.test_string = "howdy" self.assertEqual(len(outside_signal_spy), 1) # triggered once self.assertTrue(outside_signal_spy.isValid()) - self.assertTrue(widget.test_string == 'howdy') - self.assertTrue(widget.test_string_widget.text() == 'howdy') + self.assertTrue(widget.test_string == "howdy") + self.assertTrue(widget.test_string_widget.text() == "howdy") # change value internally inside_signal_spy = QSignalSpy(widget.ValueChangedInside) - widget.test_string_widget.setText('hello') + widget.test_string_widget.setText("hello") QTest.keyPress(widget.test_string_widget, Qt.Key_Enter) # press enter self.assertEqual(len(inside_signal_spy), 1) # triggered once self.assertTrue(inside_signal_spy.isValid()) - self.assertTrue(widget.test_string == 'hello') - self.assertTrue(widget.test_string_widget.text() == 'hello') + self.assertTrue(widget.test_string == "hello") + self.assertTrue(widget.test_string_widget.text() == "hello") def test_int_properties(self): - """Test that BaseDeviceWidget can correctly handle properties that are as ints""" - - properties = {'test_int': 1} + """_summary_""" + properties = {"test_int": 1} widget = BaseDeviceWidget(properties, properties) - self.assertTrue(hasattr(widget, 'test_int')) + self.assertTrue(hasattr(widget, "test_int")) self.assertTrue(widget.test_int == 1) - self.assertTrue(hasattr(widget, 'test_int_widget')) + self.assertTrue(hasattr(widget, "test_int_widget")) self.assertTrue(type(widget.test_int_widget) == QScrollableLineEdit) self.assertTrue(type(widget.test_int_widget.validator()) == QIntValidator) self.assertTrue(widget.test_int_widget.value() == 1) @@ -75,14 +73,13 @@ def test_int_properties(self): self.assertTrue(widget.test_int_widget.value() == 1) def test_float_properties(self): - """Test that BaseDeviceWidget can correctly handle properties that are floats""" - - properties = {'test_float': 1.5} + """_summary_""" + properties = {"test_float": 1.5} widget = BaseDeviceWidget(properties, properties) - self.assertTrue(hasattr(widget, 'test_float')) + self.assertTrue(hasattr(widget, "test_float")) self.assertTrue(widget.test_float == 1.5) - self.assertTrue(hasattr(widget, 'test_float_widget')) + self.assertTrue(hasattr(widget, "test_float_widget")) self.assertTrue(type(widget.test_float_widget) == QScrollableLineEdit) self.assertTrue(type(widget.test_float_widget.validator()) == QDoubleValidator) self.assertTrue(widget.test_float_widget.value() == 1.5) @@ -105,88 +102,88 @@ def test_float_properties(self): self.assertTrue(widget.test_float_widget.value() == 1.5) def test_list_properties(self): - """Test that BaseDeviceWidget can correctly handle properties that are list""" - - properties = {'test_list': ['hello', 'world']} + """_summary_""" + properties = {"test_list": ["hello", "world"]} widget = BaseDeviceWidget(properties, properties) - self.assertTrue(hasattr(widget, 'test_list')) - self.assertTrue(widget.test_list == ['hello', 'world']) + self.assertTrue(hasattr(widget, "test_list")) + self.assertTrue(widget.test_list == ["hello", "world"]) - self.assertTrue(hasattr(widget, 'test_list.0')) - self.assertTrue(getattr(widget, 'test_list.0') == 'hello') - self.assertTrue(hasattr(widget, 'test_list.0_widget')) - self.assertTrue(type(getattr(widget, 'test_list.0_widget')) == QScrollableLineEdit) - self.assertTrue(getattr(widget, 'test_list.0_widget').text() == 'hello') + self.assertTrue(hasattr(widget, "test_list.0")) + self.assertTrue(getattr(widget, "test_list.0") == "hello") + self.assertTrue(hasattr(widget, "test_list.0_widget")) + self.assertTrue(type(getattr(widget, "test_list.0_widget")) == QScrollableLineEdit) + self.assertTrue(getattr(widget, "test_list.0_widget").text() == "hello") - self.assertTrue(hasattr(widget, 'test_list.1')) - self.assertTrue(getattr(widget, 'test_list.1') == 'world') - self.assertTrue(hasattr(widget, 'test_list.1_widget')) - self.assertTrue(type(getattr(widget, 'test_list.1_widget')) == QScrollableLineEdit) - self.assertTrue(getattr(widget, 'test_list.1_widget').text() == 'world') + self.assertTrue(hasattr(widget, "test_list.1")) + self.assertTrue(getattr(widget, "test_list.1") == "world") + self.assertTrue(hasattr(widget, "test_list.1_widget")) + self.assertTrue(type(getattr(widget, "test_list.1_widget")) == QScrollableLineEdit) + self.assertTrue(getattr(widget, "test_list.1_widget").text() == "world") # change value internally - getattr(widget, 'test_list.0_widget').setText('howdy') - QTest.keyPress(getattr(widget, 'test_list.0_widget'), Qt.Key_Enter) # press enter - self.assertTrue(widget.test_list == ['howdy', 'world']) - self.assertTrue(getattr(widget, 'test_list.0') == 'howdy') + getattr(widget, "test_list.0_widget").setText("howdy") + QTest.keyPress(getattr(widget, "test_list.0_widget"), Qt.Key_Enter) # press enter + self.assertTrue(widget.test_list == ["howdy", "world"]) + self.assertTrue(getattr(widget, "test_list.0") == "howdy") def test_dict_properties(self): - """Test that BaseDeviceWidget can correctly handle properties that are dictionaries""" - - properties = {'test_dict': {'greeting': 'hello', 'directed_to': 'world'}} + """_summary_""" + properties = {"test_dict": {"greeting": "hello", "directed_to": "world"}} widget = BaseDeviceWidget(properties, properties) - self.assertTrue(hasattr(widget, 'test_dict')) - self.assertTrue(widget.test_dict == {'greeting': 'hello', 'directed_to': 'world'}) + self.assertTrue(hasattr(widget, "test_dict")) + self.assertTrue(widget.test_dict == {"greeting": "hello", "directed_to": "world"}) - self.assertTrue(hasattr(widget, 'test_dict.greeting')) - self.assertTrue(getattr(widget, 'test_dict.greeting') == 'hello') - self.assertTrue(hasattr(widget, 'test_dict.greeting_widget')) - self.assertTrue(type(getattr(widget, 'test_dict.greeting_widget')) == QScrollableLineEdit) - self.assertTrue(getattr(widget, 'test_dict.greeting_widget').text() == 'hello') + self.assertTrue(hasattr(widget, "test_dict.greeting")) + self.assertTrue(getattr(widget, "test_dict.greeting") == "hello") + self.assertTrue(hasattr(widget, "test_dict.greeting_widget")) + self.assertTrue(type(getattr(widget, "test_dict.greeting_widget")) == QScrollableLineEdit) + self.assertTrue(getattr(widget, "test_dict.greeting_widget").text() == "hello") - self.assertTrue(hasattr(widget, 'test_dict.directed_to')) - self.assertTrue(getattr(widget, 'test_dict.directed_to') == 'world') - self.assertTrue(hasattr(widget, 'test_dict.directed_to_widget')) - self.assertTrue(type(getattr(widget, 'test_dict.directed_to_widget')) == QScrollableLineEdit) - self.assertTrue(getattr(widget, 'test_dict.directed_to_widget').text() == 'world') + self.assertTrue(hasattr(widget, "test_dict.directed_to")) + self.assertTrue(getattr(widget, "test_dict.directed_to") == "world") + self.assertTrue(hasattr(widget, "test_dict.directed_to_widget")) + self.assertTrue(type(getattr(widget, "test_dict.directed_to_widget")) == QScrollableLineEdit) + self.assertTrue(getattr(widget, "test_dict.directed_to_widget").text() == "world") # change value internally - getattr(widget, 'test_dict.greeting_widget').setText('howdy') - QTest.keyPress(getattr(widget, 'test_dict.greeting_widget'), Qt.Key_Enter) # press enter - self.assertTrue(widget.test_dict == {'greeting': 'howdy', 'directed_to': 'world'}) - self.assertTrue(getattr(widget, 'test_dict.greeting') == 'howdy') + getattr(widget, "test_dict.greeting_widget").setText("howdy") + QTest.keyPress(getattr(widget, "test_dict.greeting_widget"), Qt.Key_Enter) # press enter + self.assertTrue(widget.test_dict == {"greeting": "howdy", "directed_to": "world"}) + self.assertTrue(getattr(widget, "test_dict.greeting") == "howdy") def test_nested_properties(self): - """Test that BaseDeviceWidget can correctly handle properties that are nested dictionaries""" - - properties = {'test_nest_dict': {'greeting_options': {'formal': 'hello', 'cowboy': 'howdy'}, - 'directed_to': 'world'}} + """_summary_""" + properties = { + "test_nest_dict": {"greeting_options": {"formal": "hello", "cowboy": "howdy"}, "directed_to": "world"} + } widget = BaseDeviceWidget(properties, properties) - self.assertTrue(hasattr(widget, 'test_nest_dict.greeting_options')) - self.assertTrue(getattr(widget, 'test_nest_dict.greeting_options') == {'formal': 'hello', 'cowboy': 'howdy'}) - self.assertTrue(hasattr(widget, 'test_nest_dict.greeting_options')) + self.assertTrue(hasattr(widget, "test_nest_dict.greeting_options")) + self.assertTrue(getattr(widget, "test_nest_dict.greeting_options") == {"formal": "hello", "cowboy": "howdy"}) + self.assertTrue(hasattr(widget, "test_nest_dict.greeting_options")) - self.assertTrue(hasattr(widget, 'test_nest_dict.greeting_options.formal')) - self.assertTrue(getattr(widget, 'test_nest_dict.greeting_options.formal') == 'hello') - self.assertTrue(hasattr(widget, 'test_nest_dict.greeting_options.formal_widget')) - self.assertTrue(type(getattr(widget, 'test_nest_dict.greeting_options.formal_widget')) == QScrollableLineEdit) - self.assertTrue(getattr(widget, 'test_nest_dict.greeting_options.formal_widget').text() == 'hello') + self.assertTrue(hasattr(widget, "test_nest_dict.greeting_options.formal")) + self.assertTrue(getattr(widget, "test_nest_dict.greeting_options.formal") == "hello") + self.assertTrue(hasattr(widget, "test_nest_dict.greeting_options.formal_widget")) + self.assertTrue(type(getattr(widget, "test_nest_dict.greeting_options.formal_widget")) == QScrollableLineEdit) + self.assertTrue(getattr(widget, "test_nest_dict.greeting_options.formal_widget").text() == "hello") - self.assertTrue(hasattr(widget, 'test_nest_dict.greeting_options.cowboy')) - self.assertTrue(getattr(widget, 'test_nest_dict.greeting_options.cowboy') == 'howdy') - self.assertTrue(hasattr(widget, 'test_nest_dict.greeting_options.cowboy_widget')) - self.assertTrue(type(getattr(widget, 'test_nest_dict.greeting_options.cowboy_widget')) == QScrollableLineEdit) - self.assertTrue(getattr(widget, 'test_nest_dict.greeting_options.cowboy_widget').text() == 'howdy') + self.assertTrue(hasattr(widget, "test_nest_dict.greeting_options.cowboy")) + self.assertTrue(getattr(widget, "test_nest_dict.greeting_options.cowboy") == "howdy") + self.assertTrue(hasattr(widget, "test_nest_dict.greeting_options.cowboy_widget")) + self.assertTrue(type(getattr(widget, "test_nest_dict.greeting_options.cowboy_widget")) == QScrollableLineEdit) + self.assertTrue(getattr(widget, "test_nest_dict.greeting_options.cowboy_widget").text() == "howdy") # change value internally - getattr(widget, 'test_nest_dict.greeting_options.formal_widget').setText('salutations') - QTest.keyPress(getattr(widget, 'test_nest_dict.greeting_options.formal_widget'), Qt.Key_Enter) # press enter - self.assertTrue(widget.test_nest_dict == {'greeting_options': {'formal': 'salutations', 'cowboy': 'howdy'}, - 'directed_to': 'world'}) - self.assertTrue(getattr(widget, 'test_nest_dict.greeting_options.formal') == 'salutations') + getattr(widget, "test_nest_dict.greeting_options.formal_widget").setText("salutations") + QTest.keyPress(getattr(widget, "test_nest_dict.greeting_options.formal_widget"), Qt.Key_Enter) # press enter + self.assertTrue( + widget.test_nest_dict + == {"greeting_options": {"formal": "salutations", "cowboy": "howdy"}, "directed_to": "world"} + ) + self.assertTrue(getattr(widget, "test_nest_dict.greeting_options.formal") == "salutations") if __name__ == "__main__": diff --git a/tests/test_camera_widget.py b/tests/test_camera_widget.py index 76b4195..e7e335e 100644 --- a/tests/test_camera_widget.py +++ b/tests/test_camera_widget.py @@ -1,28 +1,22 @@ -""" testing CameraWidget """ - -from types import SimpleNamespace import unittest from view.widgets.device_widgets.camera_widget import CameraWidget -from qtpy.QtTest import QTest, QSignalSpy from qtpy.QtWidgets import QApplication, QWidget -from qtpy.QtCore import Qt import sys import numpy as np -from unittest.mock import MagicMock, PropertyMock +from unittest.mock import MagicMock app = QApplication(sys.argv) class CameraWidgetTests(unittest.TestCase): - """Tests for CameraWidget""" + """_summary_""" def test_format(self): - """Test format of camera widget is correct""" - + """_summary_""" mock_camera = MagicMock() mock_camera.configure_mock( _binning=1, - _pixel_type='mono8', + _pixel_type="mono8", _exposure_time_ms=20.0, _sensor_width_px=100, _sensor_height_px=100, @@ -31,64 +25,78 @@ def test_format(self): _height_px=100, _height_offset_px=0, roi_widget=QWidget(), - _latest_frame=np.zeros((100, 100))) - - type(mock_camera).binning = property(fget=lambda inst: getattr(inst, '_binning'), - fset=lambda inst, val: setattr(inst, '_binning', val)) - type(mock_camera).frame_time_ms = property(fget=lambda inst: inst.height_px * inst.line_interval_us / 1000 + - inst.exposure_time_ms) + _latest_frame=np.zeros((100, 100)), + ) + + type(mock_camera).binning = property( + fget=lambda inst: getattr(inst, "_binning"), fset=lambda inst, val: setattr(inst, "_binning", val) + ) + type(mock_camera).frame_time_ms = property( + fget=lambda inst: inst.height_px * inst.line_interval_us / 1000 + inst.exposure_time_ms + ) type(mock_camera).line_interval_us = property(fget=lambda inst: 20.0) - type(mock_camera).binning = property(fget=lambda inst: getattr(inst, '_binning'), - fset=lambda inst, val: setattr(inst, '_binning', val)) - type(mock_camera).pixel_type = property(fget=lambda inst: getattr(inst, '_pixel_type'), - fset=lambda inst, val: setattr(inst, '_pixel_type', val)) - type(mock_camera).exposure_time_ms = property(fget=lambda inst: getattr(inst, '_exposure_time_ms'), - fset=lambda inst, v: setattr(inst, '_exposure_time_ms', v)) - type(mock_camera).sensor_width_px = property(fget=lambda inst: getattr(inst, '_sensor_width_px'), - fset=lambda inst, v: setattr(inst, '_sensor_width_px', v)) - type(mock_camera).sensor_height_px = property(fget=lambda inst: getattr(inst, '_sensor_height_px'), - fset=lambda inst, v: setattr(inst, '_sensor_height_px', - v)) - type(mock_camera).width_px = property(fget=lambda inst: getattr(inst, '_width_px'), - fset=lambda inst, v: setattr(inst, '_width_px', v)) - type(mock_camera).width_offset_px = property(fget=lambda inst: getattr(inst, '_width_offset_px'), - fset=lambda inst, v: setattr(inst, '_width_offset_px', v)) - type(mock_camera).height_px = property(fget=lambda inst: getattr(inst, '_height_px'), - fset=lambda inst, v: setattr(inst, '_height_px', v)) - type(mock_camera).height_offset_px = property(fget=lambda inst: getattr(inst, '_height_offset_px'), - fset=lambda inst, v: setattr(inst, '_height_offset_px', v)) - type(mock_camera).latest_frame = property(fget=lambda inst: getattr(inst, '_latest_frame')) - + type(mock_camera).binning = property( + fget=lambda inst: getattr(inst, "_binning"), fset=lambda inst, val: setattr(inst, "_binning", val) + ) + type(mock_camera).pixel_type = property( + fget=lambda inst: getattr(inst, "_pixel_type"), fset=lambda inst, val: setattr(inst, "_pixel_type", val) + ) + type(mock_camera).exposure_time_ms = property( + fget=lambda inst: getattr(inst, "_exposure_time_ms"), + fset=lambda inst, v: setattr(inst, "_exposure_time_ms", v), + ) + type(mock_camera).sensor_width_px = property( + fget=lambda inst: getattr(inst, "_sensor_width_px"), + fset=lambda inst, v: setattr(inst, "_sensor_width_px", v), + ) + type(mock_camera).sensor_height_px = property( + fget=lambda inst: getattr(inst, "_sensor_height_px"), + fset=lambda inst, v: setattr(inst, "_sensor_height_px", v), + ) + type(mock_camera).width_px = property( + fget=lambda inst: getattr(inst, "_width_px"), fset=lambda inst, v: setattr(inst, "_width_px", v) + ) + type(mock_camera).width_offset_px = property( + fget=lambda inst: getattr(inst, "_width_offset_px"), + fset=lambda inst, v: setattr(inst, "_width_offset_px", v), + ) + type(mock_camera).height_px = property( + fget=lambda inst: getattr(inst, "_height_px"), fset=lambda inst, v: setattr(inst, "_height_px", v) + ) + type(mock_camera).height_offset_px = property( + fget=lambda inst: getattr(inst, "_height_offset_px"), + fset=lambda inst, v: setattr(inst, "_height_offset_px", v), + ) + type(mock_camera).latest_frame = property(fget=lambda inst: getattr(inst, "_latest_frame")) widget = CameraWidget(mock_camera) children = widget.centralWidget().children() - groups = {'picture_buttons': children[1], - 'pixel_widgets': children[2], - 'timing_widgets': children[3], - 'sensor_size_widgets': children[5], - } + groups = { + "picture_buttons": children[1], + "pixel_widgets": children[2], + "timing_widgets": children[3], + "sensor_size_widgets": children[5], + } # check that picture buttons are correctly placed - self.assertEqual(groups['picture_buttons'].layout().itemAt(0).widget(), widget.live_button) - self.assertEqual(groups['picture_buttons'].layout().itemAt(1).widget(), widget.snapshot_button) + self.assertEqual(groups["picture_buttons"].layout().itemAt(0).widget(), widget.live_button) + self.assertEqual(groups["picture_buttons"].layout().itemAt(1).widget(), widget.snapshot_button) # check that pixel widgets are placed correctly - - - print(groups['pixel_widgets'].children()[1].layout().itemAt(1), widget.property_widgets['binning']) - self.assertEqual(groups['pixel_widgets'].layout().itemAt(0).widget(), widget.binning_widget) - self.assertEqual(groups['pixel_widgets'].layout().itemAt(1).widget(), widget.pixel_type_widget) + self.assertEqual(groups["pixel_widgets"].layout().itemAt(0).widget(), widget.binning_widget) + self.assertEqual(groups["pixel_widgets"].layout().itemAt(1).widget(), widget.pixel_type_widget) # check that timing widgets are placed correctly - self.assertEqual(groups['timing_widgets'].layout().itemAt(0).widget(), widget.exposure_time_ms_widget) - self.assertEqual(groups['timing_widgets'].layout().itemAt(1).widget(), widget.frame_time_ms_widget) - self.assertEqual(groups['timing_widgets'].layout().itemAt(2).widget(), widget.line_interval_us) + self.assertEqual(groups["timing_widgets"].layout().itemAt(0).widget(), widget.exposure_time_ms_widget) + self.assertEqual(groups["timing_widgets"].layout().itemAt(1).widget(), widget.frame_time_ms_widget) + self.assertEqual(groups["timing_widgets"].layout().itemAt(2).widget(), widget.line_interval_us) # check that sensor size widgets are placed correctly - self.assertEqual(groups['sensor_size_widgets'].layout().itemAt(0).widget(), widget.width_px_widget) - self.assertEqual(groups['sensor_size_widgets'].layout().itemAt(1).widget(), widget.width_offset_px) - self.assertEqual(groups['sensor_size_widgets'].layout().itemAt(2).widget(), widget.height_px) - self.assertEqual(groups['sensor_size_widgets'].layout().itemAt(3).widget(), widget.height_offset_px) + self.assertEqual(groups["sensor_size_widgets"].layout().itemAt(0).widget(), widget.width_px_widget) + self.assertEqual(groups["sensor_size_widgets"].layout().itemAt(1).widget(), widget.width_offset_px) + self.assertEqual(groups["sensor_size_widgets"].layout().itemAt(2).widget(), widget.height_px) + self.assertEqual(groups["sensor_size_widgets"].layout().itemAt(3).widget(), widget.height_offset_px) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_volume_plan_widget.py b/tests/test_volume_plan_widget.py index be72151..157dfbb 100644 --- a/tests/test_volume_plan_widget.py +++ b/tests/test_volume_plan_widget.py @@ -1,9 +1,7 @@ -""" testing VolumePlanWidget """ - import unittest from view.widgets.acquisition_widgets.volume_plan_widget import VolumePlanWidget from qtpy.QtTest import QTest, QSignalSpy -from qtpy.QtWidgets import QApplication, QWidget +from qtpy.QtWidgets import QApplication from qtpy.QtCore import Qt import sys import numpy as np @@ -12,13 +10,10 @@ class VolumePlanWidgetTests(unittest.TestCase): - """Tests for VolumePlanWidget""" - - # TODO: Test overlap, relative_to, order, apply_all + """_summary_""" def test_toggle_mode(self): - """Test functionality of number mode""" - + """_summary_""" plan = VolumePlanWidget() plan.show() @@ -27,7 +22,7 @@ def test_toggle_mode(self): # check clicking radio button works QTest.mouseClick(plan.area_button, Qt.LeftButton) self.assertTrue(plan.area_button.isChecked()) - self.assertEqual(plan.mode, 'area') + self.assertEqual(plan.mode, "area") self.assertEqual(len(valueChanged_spy), 1) # triggered once self.assertTrue(valueChanged_spy.isValid()) self.assertTrue(plan.area_widget.isEnabled()) @@ -35,7 +30,7 @@ def test_toggle_mode(self): self.assertFalse(plan.bounds_widget.isEnabled()) QTest.mouseClick(plan.number_button, Qt.LeftButton) - self.assertEqual(plan.mode, 'number') + self.assertEqual(plan.mode, "number") self.assertEqual(len(valueChanged_spy), 2) # triggered twice self.assertTrue(valueChanged_spy.isValid()) self.assertFalse(plan.area_widget.isEnabled()) @@ -43,7 +38,7 @@ def test_toggle_mode(self): self.assertFalse(plan.bounds_widget.isEnabled()) QTest.mouseClick(plan.bounds_button, Qt.LeftButton) - self.assertEqual(plan.mode, 'bounds') + self.assertEqual(plan.mode, "bounds") self.assertEqual(len(valueChanged_spy), 3) # triggered thrice self.assertTrue(valueChanged_spy.isValid()) self.assertFalse(plan.area_widget.isEnabled()) @@ -53,11 +48,10 @@ def test_toggle_mode(self): plan.close() def test_number_mode(self): - """Test functionality of number mode""" - + """_summary_""" plan = VolumePlanWidget() plan.show() - plan.mode = 'number' + plan.mode = "number" valueChanged_spy = QSignalSpy(plan.valueChanged) @@ -101,11 +95,10 @@ def test_number_mode(self): self.assertTrue(np.array_equal(expected_tile_pos, actual_tile_pos)) def test_area_mode(self): - """Test functionality of area mode""" - + """_summary_""" plan = VolumePlanWidget() plan.show() - plan.mode = 'area' + plan.mode = "area" valueChanged_spy = QSignalSpy(plan.valueChanged) @@ -162,11 +155,10 @@ def test_area_mode(self): self.assertTrue(plan.scan_ends.shape == (2, 2)) def test_bounds_mode(self): - """Test functionality of bounds mode""" - + """_summary_""" plan = VolumePlanWidget() plan.show() - plan.mode = 'bounds' + plan.mode = "bounds" valueChanged_spy = QSignalSpy(plan.valueChanged) @@ -208,8 +200,7 @@ def test_bounds_mode(self): self.assertTrue(np.array_equal(expected_tile_pos, actual_tile_pos)) def test_update_fov_position(self): - """Test functionality of moving fov mode""" - + """_summary_""" plan = VolumePlanWidget() plan.show() valueChanged_spy = QSignalSpy(plan.valueChanged) @@ -244,11 +235,8 @@ def test_update_fov_position(self): self.assertEqual(plan.grid_offset_widgets[2].value(), 3) self.assertEqual(plan.grid_offset, [1, 2, 3]) - - def test_grid_offset_widgets(self): - """Test functionality of grid_offset_widgets""" - + """_summary_""" plan = VolumePlanWidget() plan.show() valueChanged_spy = QSignalSpy(plan.valueChanged) @@ -285,7 +273,6 @@ def test_grid_offset_widgets(self): self.assertTrue(np.array_equal(expected_tiles, actual_tiles)) - if __name__ == "__main__": unittest.main() sys.exit(app.exec_()) From affc04fcfbb7045c4167fc2ba438820a4dab243e Mon Sep 17 00:00:00 2001 From: adamkglaser Date: Wed, 5 Mar 2025 07:11:11 -0800 Subject: [PATCH 2/2] adding more docstrings --- examples/camera_example.py | 5 +- examples/channel_plan_example.py | 4 +- examples/filter_wheel_example.py | 4 +- examples/filter_wheel_simulated_example.py | 22 +- examples/laser_example.py | 2 +- examples/ni_example.py | 17 +- src/view/acquisition_view.py | 237 +++++++++------- src/view/instrument_view.py | 193 +++++++------ .../channel_plan_widget.py | 121 ++++---- .../acquisition_widgets/metadata_widget.py | 23 +- .../acquisition_widgets/volume_model.py | 212 ++++++++------ .../acquisition_widgets/volume_plan_widget.py | 264 ++++++++++-------- .../device_widgets/filter_wheel_widget.py | 152 +++++----- 13 files changed, 705 insertions(+), 551 deletions(-) diff --git a/examples/camera_example.py b/examples/camera_example.py index b0dfc39..7c68a4e 100644 --- a/examples/camera_example.py +++ b/examples/camera_example.py @@ -1,4 +1,4 @@ -from voxel.devices.camera.simulated import Camera +from voxel.devices.camera.simulated import SimulatedCamera from view.widgets.device_widgets.camera_widget import CameraWidget from qtpy.QtWidgets import QApplication import sys @@ -43,12 +43,13 @@ def widget_property_changed(name, device, widget): if __name__ == "__main__": app = QApplication(sys.argv) - camera_object = Camera("") + camera_object = SimulatedCamera("") props = { "exposure_time_ms": 20.0, "pixel_type": "mono16", "width_px": 1152, "height_px": 1152, + "um_px": 1.0 } for k, v in props.items(): setattr(camera_object, k, v) diff --git a/examples/channel_plan_example.py b/examples/channel_plan_example.py index a3eb4ae..ad37377 100644 --- a/examples/channel_plan_example.py +++ b/examples/channel_plan_example.py @@ -7,7 +7,7 @@ from view.widgets.device_widgets.laser_widget import LaserWidget from view.widgets.device_widgets.stage_widget import StageWidget from voxel.devices.laser.simulated import SimulatedLaser -from voxel.devices.stage.simulated import Stage +from voxel.devices.stage.simulated import SimulatedStage if __name__ == "__main__": app = QApplication(sys.argv) @@ -41,7 +41,7 @@ lasers = {"488nm": SimulatedLaser(id="hello", wavelength=488), "639nm": SimulatedLaser(id="there", wavelength=639)} - focusing_stages = {"n": Stage(hardware_axis="n", instrument_axis="n")} + focusing_stages = {"n": SimulatedStage(hardware_axis="n", instrument_axis="n")} laser_widgets = {"488nm": LaserWidget(lasers["488nm"]), "639nm": LaserWidget(lasers["639nm"])} diff --git a/examples/filter_wheel_example.py b/examples/filter_wheel_example.py index 73b9eab..ac5bf69 100644 --- a/examples/filter_wheel_example.py +++ b/examples/filter_wheel_example.py @@ -7,7 +7,7 @@ from view.widgets.device_widgets.filter_wheel_widget import FilterWheelWidget from voxel.devices.filter.asi import Filter -from voxel.devices.filterwheel.asi import FilterWheel +from voxel.devices.filterwheel.asi.fw1000 import FW1000FilterWheel def move_filter(): @@ -41,7 +41,7 @@ def widget_property_changed(name, device, widget): app = QApplication(sys.argv) stage = TigerController("COM4") - filter_wheel = FilterWheel( + filter_wheel = FW1000FilterWheel( stage, 0, { diff --git a/examples/filter_wheel_simulated_example.py b/examples/filter_wheel_simulated_example.py index 22bdcbb..86fcb8c 100644 --- a/examples/filter_wheel_simulated_example.py +++ b/examples/filter_wheel_simulated_example.py @@ -5,7 +5,7 @@ from view.widgets.device_widgets.filter_wheel_widget import FilterWheelWidget from voxel.devices.filter.simulated import Filter -from voxel.devices.filterwheel.simulated import FilterWheel +from voxel.devices.filterwheel.simulated import SimulatedFilterWheel def move_filter(): @@ -39,7 +39,7 @@ def widget_property_changed(name, device, widget): if __name__ == "__main__": app = QApplication(sys.argv) - filter_wheel = FilterWheel( + filter_wheel = SimulatedFilterWheel( 0, { "BP405": 0, @@ -60,15 +60,15 @@ def widget_property_changed(name, device, widget): BP488_filter = Filter(filter_wheel, "BP488") colors = { - "BP405": "purple", - "BP488": "blue", - "BP561": "yellowgreen", - "LP638": "red", - "MB405/488/561/638": "pink", - "Empty1": "black", - "Empty2": "black", - "Empty3": "black", - "Empty4": "black", + "BP405": "#C875C4", + "BP488": "#1F77B4", + "BP561": "#2CA02C", + "LP638": "#D62768", + "MB405/488/561/638": "#17BECF", + "Empty1": "#262930", + "Empty2": "#262930", + "Empty3": "#262930", + "Empty4": "#262930", } widget = FilterWheelWidget(filter_wheel, colors) diff --git a/examples/laser_example.py b/examples/laser_example.py index 8ba39a5..8299b58 100644 --- a/examples/laser_example.py +++ b/examples/laser_example.py @@ -49,7 +49,7 @@ def widget_property_changed(name, device, widget): if __name__ == "__main__": app = QApplication(sys.argv) laser_object = SimulatedLaser(id='', wavelength=488) - laser = LaserWidget(laser_object, color='blue', advanced_user=False) + laser = LaserWidget(laser_object, color='#1F77B4', advanced_user=False) laser.show() laser.ValueChangedInside[str].connect( diff --git a/examples/ni_example.py b/examples/ni_example.py index ad053b4..c037e8a 100644 --- a/examples/ni_example.py +++ b/examples/ni_example.py @@ -1,4 +1,4 @@ -from voxel.devices.daq.ni import DAQ +from voxel.devices.daq.simulated import SimulatedDAQ from view.widgets.device_widgets.ni_widget import NIWidget from qtpy.QtWidgets import QApplication import sys @@ -16,16 +16,7 @@ def widget_property_changed(name, device, widget): """Slot to signal when widget has been changed :param name: name of attribute and widget""" - - name_lst = name.split('.') - # print('widget', name, ' changed to ', getattr(widget, name_lst[0])) - # value = getattr(widget, name_lst[0]) - # setattr(device, name_lst[0], value) - # print('Device', name, ' changed to ', getattr(device, name_lst[0])) - # for k, v in widget.property_widgets.items(): - # instrument_value = getattr(device, k) - # print(k, instrument_value) - #setattr(widget, k, instrument_value) + pass if __name__ == "__main__": @@ -38,7 +29,7 @@ def widget_property_changed(name, device, widget): ao_task = daq_tasks['ao_task'] co_task = daq_tasks['co_task'] - daq_object = DAQ("Dev2") + daq_object = SimulatedDAQ("Dev2") daq_object.tasks = daq_tasks daq_object.add_task('ao') daq_object.add_task('co') @@ -48,7 +39,7 @@ def widget_property_changed(name, device, widget): gui_config = YAML(typ='safe', pure=True).load(GUI_YAML) exposed = gui_config['instrument_view']['device_widgets']['PCIe-6738']['init']['exposed_branches'] - daq_widget = NIWidget(daq_object, exposed) + daq_widget = NIWidget(daq_object, exposed, advanced_user=False) daq_widget.show() daq_widget.ValueChangedInside[str].connect( lambda value, dev=daq_object, widget=daq_widget,: widget_property_changed(value, dev, widget)) diff --git a/src/view/acquisition_view.py b/src/view/acquisition_view.py index e61f63c..025ac39 100644 --- a/src/view/acquisition_view.py +++ b/src/view/acquisition_view.py @@ -2,7 +2,7 @@ import logging from pathlib import Path from time import sleep -from typing import Iterator, Literal, Union +from typing import Any, Dict, Iterator, List, Literal, Union import inflection import napari @@ -45,8 +45,6 @@ ) from view.widgets.base_device_widget import BaseDeviceWidget, create_widget, label_maker, scan_for_properties from view.widgets.miscellaneous_widgets.q_dock_widget_title_bar import QDockWidgetTitleBar -from view.widgets.miscellaneous_widgets.q_scrollable_float_slider import QScrollableFloatSlider -from view.widgets.miscellaneous_widgets.q_scrollable_line_edit import QScrollableLineEdit class AcquisitionView(QWidget): @@ -59,13 +57,17 @@ def __init__( acquisition, instrument_view, log_level: Literal["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO", - ): + ) -> None: """ - :param acquisition: voxel acquisition object - :param instrument_view: view object relating to instrument. Needed to lock stage - :param log_level: level to set logger at + Initialize the AcquisitionView. + + :param acquisition: Voxel acquisition object + :type acquisition: object + :param instrument_view: View object relating to instrument. Needed to lock stage + :type instrument_view: object + :param log_level: Level to set logger at + :type log_level: Literal["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], optional """ - super().__init__() self.log = logging.getLogger(f"{__name__}.{self.__class__.__name__}") self.log.setLevel(log_level) @@ -123,13 +125,13 @@ def __init__( scroll.setWidget(self.metadata_widget) scroll.setWindowTitle("Metadata") scroll.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) - dock = QDockWidget(scroll.windowTitle(), self) - dock.setWidget(scroll) - dock.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) - dock.setTitleBarWidget(QDockWidgetTitleBar(dock)) - dock.setWidget(scroll) - dock.setMinimumHeight(25) - splitter.addWidget(dock) + # dock = QDockWidget(scroll.windowTitle(), self) + # dock.setWidget(scroll) + # dock.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) + # dock.setTitleBarWidget(QDockWidgetTitleBar(dock)) + # dock.setWidget(scroll) + # dock.setMinimumHeight(25) + splitter.addWidget(scroll) # create dock widget for operations for i, operation in enumerate(["writer", "file_transfer", "process", "routine"]): @@ -140,12 +142,12 @@ def __init__( scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) scroll.setWidget(stack) scroll.setFixedWidth(self.metadata_widget.size().width()) - dock = QDockWidget(stack.windowTitle()) - dock.setTitleBarWidget(QDockWidgetTitleBar(dock)) - dock.setWidget(scroll) - dock.setMinimumHeight(25) - setattr(self, f"{operation}_dock", dock) - splitter.addWidget(dock) + # dock = QDockWidget(stack.windowTitle()) + # dock.setTitleBarWidget(QDockWidgetTitleBar(dock)) + # dock.setWidget(scroll) + # dock.setMinimumHeight(25) + setattr(self, f"{operation}_dock", scroll) + splitter.addWidget(scroll) self.main_layout.addWidget(splitter, 1, 3) self.setLayout(self.main_layout) self.setWindowTitle("Acquisition View") @@ -158,9 +160,10 @@ def __init__( app.lastWindowClosed.connect(self.close) # shut everything down when closing def create_start_button(self) -> QPushButton: - """_summary_ + """ + Create the start button. - :return: _description_ + :return: Start button :rtype: QPushButton """ start = QPushButton("Start") @@ -169,20 +172,22 @@ def create_start_button(self) -> QPushButton: return start def create_stop_button(self) -> QPushButton: - """_summary_ + """ + Create the stop button. - :return: _description_ + :return: Stop button :rtype: QPushButton """ stop = QPushButton("Stop") stop.clicked.connect(self.acquisition.stop_acquisition) stop.setStyleSheet("background-color: red") stop.setDisabled(True) - return stop def start_acquisition(self) -> None: - """_summary_""" + """ + Start the acquisition process. + """ # add tiles to acquisition config self.update_tiles() @@ -223,7 +228,9 @@ def start_acquisition(self) -> None: sleep(1) def acquisition_ended(self) -> None: - """_summary_""" + """ + Handle the end of the acquisition process. + """ # enable acquisition view self.start_button.setEnabled(True) self.metadata_widget.setEnabled(True) @@ -238,7 +245,6 @@ def acquisition_ended(self) -> None: daq.tasks = self.config["instrument_view"]["livestream_tasks"][daq_name]["tasks"] # unanchor grid in volume widget - # anchor grid in volume widget for anchor, widget in zip(self.volume_plan.anchor_widgets, self.volume_plan.grid_offset_widgets): anchor.setChecked(False) widget.setDisabled(False) @@ -255,11 +261,12 @@ def acquisition_ended(self) -> None: worker.pause() def stack_device_widgets(self, device_type: str) -> QWidget: - """_summary_ + """ + Stack device widgets. - :param device_type: _description_ + :param device_type: Type of device :type device_type: str - :return: _description_ + :return: Stacked device widgets :rtype: QWidget """ device_widgets = { @@ -286,12 +293,13 @@ def stack_device_widgets(self, device_type: str) -> QWidget: return overlap_widget @staticmethod - def hide_devices(text: str, device_widgets: dict) -> None: - """_summary_ + def hide_devices(text: str, device_widgets: Dict[str, QWidget]) -> None: + """ + Hide or show device widgets based on the selected text. - :param text: _description_ + :param text: Selected text :type text: str - :param device_widgets: _description_ + :param device_widgets: Dictionary of device widgets :type device_widgets: dict """ for name, widget in device_widgets.items(): @@ -301,9 +309,10 @@ def hide_devices(text: str, device_widgets: dict) -> None: widget.setVisible(True) def create_metadata_widget(self) -> MetadataWidget: - """_summary_ + """ + Create the metadata widget. - :return: _description_ + :return: Metadata widget :rtype: MetadataWidget """ metadata_widget = MetadataWidget(self.acquisition.metadata) @@ -316,10 +325,11 @@ def create_metadata_widget(self) -> MetadataWidget: return metadata_widget def create_acquisition_widget(self) -> QSplitter: - """_summary_ + """ + Create the acquisition widget. - :raises KeyError: _description_ - :return: _description_ + :raises KeyError: If the coordinate plane does not match instrument axes in tiling_stages + :return: Acquisition widget :rtype: QSplitter """ # find limits of all axes @@ -409,9 +419,10 @@ def create_acquisition_widget(self) -> QSplitter: return acquisition_widget def channel_plan_changed(self, channel: str) -> None: - """_summary_ + """ + Update the channel plan when a channel is changed. - :param channel: _description_ + :param channel: The name of the channel that was changed :type channel: str """ tile_order = [[t.row, t.col] for t in self.volume_plan.value()] @@ -420,9 +431,10 @@ def channel_plan_changed(self, channel: str) -> None: self.update_tiles() def volume_plan_changed(self, value: Union[GridRowsColumns, GridFromEdges, GridWidthHeight]) -> None: - """_summary_ + """ + Update the volume plan when it is changed. - :param value: _description_ + :param value: The new value of the volume plan :type value: Union[GridRowsColumns, GridFromEdges, GridWidthHeight] """ tile_volumes = self.volume_plan.scan_ends - self.volume_plan.scan_starts @@ -444,14 +456,17 @@ def volume_plan_changed(self, value: Union[GridRowsColumns, GridFromEdges, GridW self.update_tiles() def update_tiles(self) -> None: - """_summary_""" + """ + Update the tiles in the acquisition configuration. + """ self.acquisition.config["acquisition"]["tiles"] = self.create_tile_list() - def move_stage(self, fov_position: list[float, float, float]) -> None: - """_summary_ + def move_stage(self, fov_position: List[float]) -> None: + """ + Move the stage to the specified field of view position. - :param fov_position: _description_ - :type fov_position: list[float, float, float] + :param fov_position: The field of view position to move to + :type fov_position: list[float] """ scalar_coord_plane = [x.strip("-") for x in self.coordinate_plane] stage_names = {stage.instrument_axis: name for name, stage in self.instrument.tiling_stages.items()} @@ -462,7 +477,9 @@ def move_stage(self, fov_position: list[float, float, float]) -> None: scan_stage.move_absolute_mm(fov_position[2], wait=False) def stop_stage(self) -> None: - """_summary_""" + """ + Stop the stage movement. + """ for name, stage in { **getattr(self.instrument, "scanning_stages", {}), **getattr(self.instrument, "tiling_stages", {}), @@ -470,18 +487,21 @@ def stop_stage(self) -> None: stage.halt() def setup_fov_position(self) -> None: - """_summary_""" + """ + Set up the field of view position. + """ self.grab_fov_positions_worker = self.grab_fov_positions() self.grab_fov_positions_worker.yielded.connect(lambda pos: setattr(self.volume_plan, "fov_position", pos)) self.grab_fov_positions_worker.yielded.connect(lambda pos: setattr(self.volume_model, "fov_position", pos)) self.grab_fov_positions_worker.start() @thread_worker - def grab_fov_positions(self) -> Iterator[list[float, float, float]]: - """_summary_ + def grab_fov_positions(self) -> Iterator[List[float]]: + """ + Grab the field of view positions. - :yield: _description_ - :rtype: Iterator[list[float, float, float]] + :yield: The field of view positions + :rtype: Iterator[list[float]] """ scalar_coord_plane = [x.strip("-") for x in self.coordinate_plane] while True: # best way to do this or have some sort of break? @@ -502,13 +522,14 @@ def grab_fov_positions(self) -> Iterator[list[float, float, float]]: yield fov_pos def create_operation_widgets(self, device_name: str, operation_name: str, operation_specs: dict) -> None: - """_summary_ + """ + Create widgets for the specified operation. - :param device_name: _description_ + :param device_name: The name of the device :type device_name: str - :param operation_name: _description_ + :param operation_name: The name of the operation :type operation_name: str - :param operation_specs: _description_ + :param operation_specs: The specifications of the operation :type operation_specs: dict """ operation_type = operation_specs["type"] @@ -571,11 +592,12 @@ def create_operation_widgets(self, device_name: str, operation_name: str, operat labeled.show() def update_acquisition_layer(self, image: np.ndarray, camera_name: str) -> None: - """_summary_ + """ + Update the acquisition layer with the specified image. - :param image: _description_ + :param image: The image to update the layer with :type image: np.ndarray - :param camera_name: _description_ + :param camera_name: The name of the camera :type camera_name: str """ if image is not None: @@ -588,16 +610,17 @@ def update_acquisition_layer(self, image: np.ndarray, camera_name: str) -> None: layer = self.instrument_view.viewer.add_image(image, name=layer_name) @thread_worker - def grab_property_value(self, device: object, property_name: str, widget) -> Iterator: - """_summary_ + def grab_property_value(self, device: object, property_name: str, widget: Any) -> Iterator: + """ + Grab the value of the specified property from the device. - :param device: _description_ + :param device: The device to grab the property value from :type device: object - :param property_name: _description_ + :param property_name: The name of the property :type property_name: str - :param widget: _description_ - :type widget: _type_ - :yield: _description_ + :param widget: The widget to update with the property value + :type widget: Any + :yield: The property value and the widget :rtype: Iterator """ while True: # best way to do this or have some sort of break? @@ -605,13 +628,14 @@ def grab_property_value(self, device: object, property_name: str, widget) -> Ite value = getattr(device, property_name) yield value, widget - def update_property_value(self, value, widget) -> None: - """_summary_ + def update_property_value(self, value: Any, widget: Any) -> None: + """ + Update the widget with the specified property value. - :param value: _description_ - :type value: _type_ - :param widget: _description_ - :type widget: _type_ + :param value: The property value + :type value: Any + :param widget: The widget to update + :type widget: Any """ try: if type(widget) in [QLineEdit, QScrollableLineEdit]: @@ -627,15 +651,16 @@ def update_property_value(self, value, widget) -> None: pass @Slot(str) - def operation_property_changed(self, attr_name: str, operation: object, widget) -> None: - """_summary_ + def operation_property_changed(self, attr_name: str, operation: object, widget: Any) -> None: + """ + Handle changes to the operation property. - :param attr_name: _description_ + :param attr_name: The name of the attribute that changed :type attr_name: str - :param operation: _description_ + :param operation: The operation object :type operation: object - :param widget: _description_ - :type widget: _type_ + :param widget: The widget that changed + :type widget: Any """ name_lst = attr_name.split(".") self.log.debug(f"widget {attr_name} changed to {getattr(widget, name_lst[0])}") @@ -657,11 +682,12 @@ def operation_property_changed(self, attr_name: str, operation: object, widget) self.log.warning(f"{attr_name} can't be mapped into operation properties due to {e}") pass - def create_tile_list(self) -> list: - """_summary_ + def create_tile_list(self) -> List[Dict[str, Any]]: + """ + Create a list of tiles for the acquisition. - :return: _description_ - :rtype: list + :return: A list of tiles + :rtype: list[dict[str, Any]] """ tiles = [] tile_slice = slice(self.volume_plan.start, self.volume_plan.stop) @@ -677,15 +703,16 @@ def create_tile_list(self) -> list: tiles.append(self.write_tile(ch, tile)) return tiles - def write_tile(self, channel: str, tile) -> dict: - """_summary_ + def write_tile(self, channel: str, tile: Any) -> Dict[str, Any]: + """ + Write the tile information for the specified channel. - :param channel: _description_ + :param channel: The name of the channel :type channel: str - :param tile: _description_ - :type tile: _type_ - :return: _description_ - :rtype: dict + :param tile: The tile information + :type tile: Any + :return: A dictionary containing the tile information + :rtype: dict[str, Any] """ row, column = tile.row, tile.col table_row = self.volume_plan.tile_table.findItems(str([row, column]), Qt.MatchExactly)[0].row() @@ -728,17 +755,20 @@ def write_tile(self, channel: str, tile) -> dict: return tile_dict def update_config_on_quit(self) -> None: - """_summary_""" + """ + Update the acquisition configuration when quitting. + """ return_value = self.update_config_query() if return_value == QMessageBox.Ok: self.acquisition.update_current_state_config() self.acquisition.save_config(self.config_save_to) - def update_config_query(self) -> None: - """_summary_ + def update_config_query(self) -> int: + """ + Show a dialog to confirm updating the acquisition configuration. - :return: _description_ - :rtype: _type_ + :return: The result of the dialog + :rtype: int """ msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Question) @@ -756,11 +786,12 @@ def update_config_query(self) -> None: return msgBox.exec() def select_directory(self, pressed: bool, msgBox: QMessageBox) -> None: - """_summary_ + """ + Select a directory to save the configuration file. - :param pressed: _description_ + :param pressed: Whether the button was pressed :type pressed: bool - :param msgBox: _description_ + :param msgBox: The message box :type msgBox: QMessageBox """ fname = QFileDialog() @@ -773,7 +804,9 @@ def select_directory(self, pressed: bool, msgBox: QMessageBox) -> None: self.config_save_to = Path(folder[0]) def close(self) -> None: - """_summary_""" + """ + Close the acquisition view and all associated resources. + """ for worker in self.property_workers: worker.quit() self.grab_fov_positions_worker.quit() diff --git a/src/view/instrument_view.py b/src/view/instrument_view.py index a39b192..1c9fda9 100644 --- a/src/view/instrument_view.py +++ b/src/view/instrument_view.py @@ -4,7 +4,7 @@ import logging from pathlib import Path from time import sleep -from typing import Iterator, Literal, Union +from typing import Any, Iterator, Literal, Tuple, Type, Union import inflection import napari @@ -55,14 +55,15 @@ def __init__( instrument, config_path: Path, log_level: Literal["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO", - ): - """_summary_ + ) -> None: + """ + Initialize the InstrumentView. - :param instrument: _description_ - :type instrument: _type_ - :param config_path: _description_ + :param instrument: The instrument to be used + :type instrument: Instrument + :param config_path: The path to the configuration file :type config_path: Path - :param log_level: _description_, defaults to "INFO" + :param log_level: The logging level, defaults to "INFO" :type log_level: Literal["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], optional """ super().__init__() @@ -174,7 +175,9 @@ def setup_stage_widgets(self) -> None: self.viewer.window.add_dock_widget(joystick_scroll, area="left", name="Joystick") def setup_laser_widgets(self) -> None: - """_summary_""" + """ + Setup laser widgets. + """ laser_widgets = [] for name, widget in self.laser_widgets.items(): label = QLabel(name) @@ -210,11 +213,12 @@ def setup_daq_widgets(self) -> None: self.viewer.window.add_dock_widget(stacked, area="right", name="DAQs", add_vertical_stretch=False) def stack_device_widgets(self, device_type: str) -> QWidget: - """_summary_ + """ + Stack device widgets. - :param device_type: _description_ + :param device_type: Type of device :type device_type: str - :return: _description_ + :return: Stacked device widgets :rtype: QWidget """ device_widgets = getattr(self, f"{device_type}_widgets") @@ -236,11 +240,12 @@ def stack_device_widgets(self, device_type: str) -> QWidget: return overlap_widget def hide_devices(self, text: str, device_type: str) -> None: - """_summary_ + """ + Hide or show device widgets based on the selected text. - :param text: _description_ + :param text: Selected text :type text: str - :param device_type: _description_ + :param device_type: Type of device :type device_type: str """ device_widgets = getattr(self, f"{device_type}_widgets") @@ -251,9 +256,10 @@ def hide_devices(self, text: str, device_type: str) -> None: widget.setVisible(True) def write_waveforms(self, daq) -> None: - """_summary_ + """ + Write waveforms to the DAQ. - :param daq: _description_ + :param daq: Data acquisition device :type daq: _type_ """ if self.grab_frames_worker.is_running: # if currently livestreaming @@ -264,16 +270,17 @@ def write_waveforms(self, daq) -> None: daq.generate_waveforms("do", self.livestream_channel) daq.write_do_waveforms(rereserve_buffer=False) - def update_config_waveforms(self, daq_widget, daq_name: str, attr_name: str) -> None: - """_summary_ + def update_config_waveforms(self, daq_widget: Type, daq_name: str, attr_name: str) -> None: + """ + Update the configuration waveforms. - :param daq_widget: _description_ - :type daq_widget: _type_ - :param daq_name: _description_ + :param daq_widget: DAQ widget + :type daq_widget: Type + :param daq_name: Name of the DAQ :type daq_name: str - :param attr_name: _description_ + :param attr_name: Attribute name :type attr_name: str - :raises KeyError: _description_ + :raises KeyError: If the attribute path is not valid """ path = attr_name.split(".") value = getattr(daq_widget, attr_name) @@ -300,14 +307,14 @@ def update_config_waveforms(self, daq_widget, daq_name: str, attr_name: str) -> f"be reflected in acquisition" ) - def setup_filter_wheel_widgets(self): + def setup_filter_wheel_widgets(self) -> None: """ Stack filter wheels. """ stacked = self.stack_device_widgets("filter_wheel") self.viewer.window.add_dock_widget(stacked, area="bottom", name="Filter Wheels") - def setup_camera_widgets(self): + def setup_camera_widgets(self) -> None: """ Setup live view and snapshot button. """ @@ -329,9 +336,10 @@ def setup_camera_widgets(self): self.viewer.window.add_dock_widget(stacked, area="right", name="Cameras", add_vertical_stretch=False) def toggle_live_button(self, camera_name: str) -> None: - """_summary_ + """ + Toggle the live button for the camera. - :param camera_name: _description_ + :param camera_name: Name of the camera :type camera_name: str """ live_button = getattr(self.camera_widgets[camera_name], "live_button", QPushButton()) @@ -350,18 +358,19 @@ def toggle_live_button(self, camera_name: str) -> None: live_button.pressed.connect(lambda button=live_button: disable_button(button)) live_button.pressed.connect(lambda camera=camera_name: self.toggle_live_button(camera_name)) - def setup_live(self, camera_name: str, frames=float("inf")) -> None: - """_summary_ + def setup_live(self, camera_name: str, frames: float = float("inf")) -> None: + """ + Setup live view for the camera. - :param camera_name: _description_ + :param camera_name: Name of the camera :type camera_name: str - :param frames: _description_, defaults to float("inf") - :type frames: _type_, optional + :param frames: Number of frames to capture, defaults to float("inf") + :type frames: float, optional """ if self.grab_frames_worker.is_running: if frames == 1: # create snapshot layer with the latest image layer = self.viewer.layers[f"{camera_name} {self.livestream_channel}"] - image = layer.data[0] if layer.multiscale else image.data + image = layer.data[0] if layer.multiscale else layer.data self.update_layer((image, camera_name), snapshot=True) return @@ -377,6 +386,7 @@ def setup_live(self, camera_name: str, frames=float("inf")) -> None: self.instrument.cameras[camera_name].prepare() self.instrument.cameras[camera_name].start(frames) + print(f"Starting live view for {camera_name}") for laser in self.channels[self.livestream_channel].get("lasers", []): self.log.info(f"Enabling laser {laser}") @@ -402,9 +412,10 @@ def setup_live(self, camera_name: str, frames=float("inf")) -> None: daq.start() def dismantle_live(self, camera_name: str) -> None: - """_summary_ + """ + Dismantle the live view for the camera. - :param camera_name: _description_ + :param camera_name: Name of the camera :type camera_name: str """ self.instrument.cameras[camera_name].abort() @@ -414,15 +425,16 @@ def dismantle_live(self, camera_name: str) -> None: self.instrument.lasers[laser_name].disable() @thread_worker - def grab_frames(self, camera_name: str, frames=float("inf")) -> Iterator[tuple[np.ndarray, str]]: - """_summary_ + def grab_frames(self, camera_name: str, frames: float = float("inf")) -> Iterator[Tuple[np.ndarray, str]]: + """ + Grab frames from the camera. - :param camera_name: _description_ + :param camera_name: Name of the camera :type camera_name: str - :param frames: _description_, defaults to float("inf") - :type frames: _type_, optional - :yield: _description_ - :rtype: Iterator[tuple[np.ndarray, str]] + :param frames: Number of frames to capture, defaults to float("inf") + :type frames: float, optional + :yield: Tuple containing the image and camera name + :rtype: Iterator[Tuple[np.ndarray, str]] """ i = 0 while i < frames: # while loop since frames can == inf @@ -430,12 +442,13 @@ def grab_frames(self, camera_name: str, frames=float("inf")) -> Iterator[tuple[n yield self.instrument.cameras[camera_name].grab_frame(), camera_name i += 1 - def update_layer(self, args, snapshot: bool = False) -> None: - """_summary_ + def update_layer(self, args: Tuple[np.ndarray, str], snapshot: bool = False) -> None: + """ + Update the layer with the captured image. - :param args: _description_ - :type args: _type_ - :param snapshot: _description_, defaults to False + :param args: Tuple containing the image and camera name + :type args: Tuple[np.ndarray, str] + :param snapshot: Whether the image is a snapshot, defaults to False :type snapshot: bool, optional """ (image, camera_name) = args @@ -469,11 +482,12 @@ def update_layer(self, args, snapshot: bool = False) -> None: def save_image( layer: Union[napari.layers.image.image.Image, list[napari.layers.image.image.Image]], event: QMouseEvent ) -> None: - """_summary_ + """ + Save the image to a file. - :param layer: _description_ + :param layer: Image layer :type layer: Union[napari.layers.image.image.Image, list[napari.layers.image.image.Image]] - :param event: _description_ + :param event: Mouse event :type event: QMouseEvent """ if event.button == 2: # Left click @@ -495,7 +509,6 @@ def setup_channel_widget(self) -> None: """ Create widget to select which laser to livestream with. """ - widget = QWidget() widget_layout = QVBoxLayout() @@ -511,11 +524,12 @@ def setup_channel_widget(self) -> None: self.viewer.window.add_dock_widget(widget, area="bottom", name="Channels") def change_channel(self, checked: bool, channel: str) -> None: - """_summary_ + """ + Change the livestream channel. - :param checked: _description_ + :param checked: Whether the channel is checked :type checked: bool - :param channel: _description_ + :param channel: Name of the channel :type channel: str """ if checked: @@ -536,11 +550,12 @@ def change_channel(self, checked: bool, channel: str) -> None: self.instrument.filters[filter].enable() def create_device_widgets(self, device_name: str, device_specs: dict) -> None: - """_summary_ + """ + Create widgets for the specified device. - :param device_name: _description_ + :param device_name: Name of the device :type device_name: str - :param device_specs: _description_ + :param device_specs: Specifications of the device :type device_specs: dict """ device_type = device_specs["type"] @@ -580,17 +595,20 @@ def create_device_widgets(self, device_name: str, device_specs: dict) -> None: gui.setWindowTitle(f"{device_type} {device_name}") @thread_worker - def grab_property_value(self, device: object, property_name: str, device_widget) -> Iterator: - """_summary_ + def grab_property_value( + self, device: object, property_name: str, device_widget: Type + ) -> Iterator[Tuple[Any, Type, str]]: + """ + Grab the value of a property from a device. - :param device: _description_ + :param device: The device to grab the property value from :type device: object - :param property_name: _description_ + :param property_name: The name of the property to grab :type property_name: str - :param device_widget: _description_ - :type device_widget: _type_ - :yield: _description_ - :rtype: Iterator + :param device_widget: The widget associated with the device + :type device_widget: Type + :yield: The property value, device widget, and property name + :rtype: Iterator[Tuple[Any, Type, str]] """ while True: # best way to do this or have some sort of break? sleep(0.5) @@ -600,14 +618,15 @@ def grab_property_value(self, device: object, property_name: str, device_widget) value = None yield value, device_widget, property_name - def update_property_value(self, value, device_widget, property_name: str) -> None: - """_summary_ + def update_property_value(self, value: Any, device_widget: Type, property_name: str) -> None: + """ + Update the widget with the property value. - :param value: _description_ - :type value: _type_ - :param device_widget: _description_ - :type device_widget: _type_ - :param property_name: _description_ + :param value: The property value + :type value: Any + :param device_widget: The widget associated with the device + :type device_widget: Type + :param property_name: The name of the property :type property_name: str """ try: @@ -616,15 +635,16 @@ def update_property_value(self, value, device_widget, property_name: str) -> Non pass @Slot(str) - def device_property_changed(self, attr_name: str, device: object, widget) -> None: - """_summary_ + def device_property_changed(self, attr_name: str, device: object, widget: Type) -> None: + """ + Handle changes to the device property. - :param attr_name: _description_ + :param attr_name: The name of the attribute that changed :type attr_name: str - :param device: _description_ + :param device: The device object :type device: object - :param widget: _description_ - :type widget: _type_ + :param widget: The widget that changed + :type widget: Type """ name_lst = attr_name.split(".") self.log.debug(f"widget {attr_name} changed to {getattr(widget, name_lst[0])}") @@ -672,9 +692,10 @@ def add_undocked_widgets(self) -> None: undocked_widget.setVisible(False) def setDisabled(self, disable: bool) -> None: - """_summary_ + """ + Disable or enable all widgets. - :param disable: _description_ + :param disable: Whether to disable the widgets :type disable: bool """ widgets = [] @@ -697,9 +718,10 @@ def update_config_on_quit(self) -> None: self.instrument.save_config(self.config_save_to) def update_config_query(self) -> Literal[0, 1]: - """_summary_ + """ + Show a dialog to confirm updating the instrument configuration. - :return: _description_ + :return: The result of the dialog :rtype: Literal[0, 1] """ msgBox = QMessageBox() @@ -718,11 +740,12 @@ def update_config_query(self) -> Literal[0, 1]: return msgBox.exec() def select_directory(self, pressed: bool, msgBox: QMessageBox) -> None: - """_summary_ + """ + Select a directory to save the configuration file. - :param pressed: _description_ + :param pressed: Whether the button was pressed :type pressed: bool - :param msgBox: _description_ + :param msgBox: The message box :type msgBox: QMessageBox """ fname = QFileDialog() diff --git a/src/view/widgets/acquisition_widgets/channel_plan_widget.py b/src/view/widgets/acquisition_widgets/channel_plan_widget.py index 4e95913..0fca148 100644 --- a/src/view/widgets/acquisition_widgets/channel_plan_widget.py +++ b/src/view/widgets/acquisition_widgets/channel_plan_widget.py @@ -5,6 +5,7 @@ import pint from inflection import singularize from qtpy.QtCore import Qt, Signal +from qtpy.QtGui import QMouseEvent from qtpy.QtWidgets import ( QAction, QComboBox, @@ -19,6 +20,7 @@ QWidget, ) +from view.instrument_view import InstrumentView from view.widgets.base_device_widget import label_maker from view.widgets.miscellaneous_widgets.q_item_delegates import QComboItemDelegate, QSpinItemDelegate, QTextItemDelegate from view.widgets.miscellaneous_widgets.q_scrollable_line_edit import QScrollableLineEdit @@ -32,16 +34,17 @@ class ChannelPlanWidget(QTabWidget): channelAdded = Signal([str]) channelChanged = Signal() - def __init__(self, instrument_view, channels: dict, properties: dict, unit: str = "um"): - """_summary_ + def __init__(self, instrument_view: InstrumentView, channels: dict, properties: dict, unit: str = "um"): + """ + Initialize the ChannelPlanWidget. - :param instrument_view: _description_ - :type instrument_view: _type_ - :param channels: _description_ + :param instrument_view: The instrument view + :type instrument_view: InstrumentView + :param channels: Dictionary of channels :type channels: dict - :param properties: _description_ + :param properties: Dictionary of properties :type properties: dict - :param unit: _description_, defaults to 'um' + :param unit: Unit of measurement, defaults to 'um' :type unit: str, optional """ super().__init__() @@ -99,11 +102,12 @@ def __init__(self, instrument_view, channels: dict, properties: dict, unit: str ) self._apply_all = True # external flag to dictate behaviour of added tab - def initialize_tables(self, instrument_view) -> None: - """_summary_ + def initialize_tables(self, instrument_view: InstrumentView) -> None: + """ + Initialize the tables for each channel. - :param instrument_view: _description_ - :type instrument_view: _type_ + :param instrument_view: The instrument view + :type instrument_view: InstrumentView """ # TODO: Checks here if prop or device isn't part of the instrument? Or go in instrument validation? for channel in self.possible_channels: @@ -183,18 +187,20 @@ def initialize_tables(self, instrument_view) -> None: @property def apply_all(self) -> bool: - """_summary_ + """ + Get the apply_all property. - :return: _description_ + :return: Whether to apply all settings :rtype: bool """ return self._apply_all @apply_all.setter def apply_all(self, value: bool) -> None: - """_summary_ + """ + Set the apply_all property. - :param value: _description_ + :param value: Whether to apply all settings :type value: bool """ if self._apply_all != value: @@ -211,18 +217,20 @@ def apply_all(self, value: bool) -> None: @property def tile_volumes(self) -> np.ndarray: - """_summary_ + """ + Get the tile volumes. - :return: _description_ + :return: The tile volumes :rtype: np.ndarray """ return self._tile_volumes @tile_volumes.setter def tile_volumes(self, value: np.ndarray) -> None: - """_summary_ + """ + Set the tile volumes. - :param value: _description_ + :param value: The tile volumes :type value: np.ndarray """ self._tile_volumes = value @@ -244,11 +252,12 @@ def tile_volumes(self, value: np.ndarray) -> None: self.update_steps(tile_index, row, channel) def enable_item(self, item: QTableWidgetItem, enable: bool) -> None: - """_summary_ + """ + Enable or disable a table item. - :param item: _description_ + :param item: The table item :type item: QTableWidgetItem - :param enable: _description_ + :param enable: Whether to enable the item :type enable: bool """ flags = QTableWidgetItem().flags() @@ -261,9 +270,10 @@ def enable_item(self, item: QTableWidgetItem, enable: bool) -> None: item.setFlags(flags) def add_channel(self, channel: str) -> None: - """_summary_ + """ + Add a channel to the widget. - :param channel: _description_ + :param channel: The channel to add :type channel: str """ table = getattr(self, f"{channel}_table") @@ -312,11 +322,12 @@ def add_channel(self, channel: str) -> None: self.channelAdded.emit(channel) def add_channel_rows(self, channel: str, order: list) -> None: - """_summary_ + """ + Add rows to the channel table. - :param channel: _description_ + :param channel: The channel to add rows to :type channel: str - :param order: _description_ + :param order: The order of the rows :type order: list """ table = getattr(self, f"{channel}_table") @@ -348,9 +359,10 @@ def add_channel_rows(self, channel: str, order: list) -> None: table.blockSignals(False) def remove_channel(self, channel: str) -> None: - """_summary_ + """ + Remove a channel from the widget. - :param channel: _description_ + :param channel: The channel to remove :type channel: str """ self.channels.remove(channel) @@ -378,13 +390,14 @@ def remove_channel(self, channel: str) -> None: self.channelChanged.emit() def cell_edited(self, row: int, column: int, channel: str = None) -> None: - """_summary_ + """ + Handle cell edits in the table. - :param row: _description_ + :param row: The row of the edited cell :type row: int - :param column: _description_ + :param column: The column of the edited cell :type column: int - :param channel: _description_, defaults to None + :param channel: The channel of the edited cell, defaults to None :type channel: str, optional """ channel = self.tabText(self.currentIndex()) if channel is None else channel @@ -420,15 +433,16 @@ def cell_edited(self, row: int, column: int, channel: str = None) -> None: self.channelChanged.emit() def update_steps(self, tile_index: list[int], row: int, channel: str) -> list[float, int]: - """_summary_ + """ + Update the steps for a given tile. - :param tile_index: _description_ + :param tile_index: The index of the tile :type tile_index: list[int] - :param row: _description_ + :param row: The row of the tile :type row: int - :param channel: _description_ + :param channel: The channel of the tile :type channel: str - :return: _description_ + :return: The updated step size and steps :rtype: list[float, int] """ volume_um = (self.tile_volumes[*tile_index] * self.unit).to(self.micron) @@ -447,15 +461,16 @@ def update_steps(self, tile_index: list[int], row: int, channel: str) -> list[fl return step_size, steps def update_step_size(self, tile_index: list[int], row: int, channel: str) -> list[float, int]: - """_summary_ + """ + Update the step size for a given tile. - :param tile_index: _description_ + :param tile_index: The index of the tile :type tile_index: list[int] - :param row: _description_ + :param row: The row of the tile :type row: int - :param channel: _description_ + :param channel: The channel of the tile :type channel: str - :return: _description_ + :return: The updated step size and steps :rtype: list[float, int] """ volume_um = (self.tile_volumes[*tile_index] * self.unit).to(self.micron) @@ -478,26 +493,30 @@ class ChannelPlanTabBar(QTabBar): """ def __init__(self): - """_summary_""" + """ + Initialize the ChannelPlanTabBar. + """ super(ChannelPlanTabBar, self).__init__() self.tabMoved.connect(self.tab_index_check) def tab_index_check(self, prev_index: int, curr_index: int) -> None: - """_summary_ + """ + Ensure the add channel tab stays at the end. - :param prev_index: _description_ + :param prev_index: The previous index of the tab :type prev_index: int - :param curr_index: _description_ + :param curr_index: The current index of the tab :type curr_index: int """ if prev_index == self.count() - 1: self.moveTab(curr_index, prev_index) - def mouseMoveEvent(self, ev) -> None: - """_summary_ + def mouseMoveEvent(self, ev: QMouseEvent) -> None: + """ + Handle mouse move events. - :param ev: _description_ - :type ev: _type_ + :param ev: The mouse event + :type ev: QMouseEvent """ index = self.currentIndex() if index == self.count() - 1: # last tab is immovable diff --git a/src/view/widgets/acquisition_widgets/metadata_widget.py b/src/view/widgets/acquisition_widgets/metadata_widget.py index 58be99d..55488f3 100644 --- a/src/view/widgets/acquisition_widgets/metadata_widget.py +++ b/src/view/widgets/acquisition_widgets/metadata_widget.py @@ -10,12 +10,13 @@ class MetadataWidget(BaseDeviceWidget): Widget for handling metadata class. """ - def __init__(self, metadata_class, advanced_user: bool = True) -> None: - """_summary_ + def __init__(self, metadata_class) -> None: + """ + Initialize the MetadataWidget. - :param metadata_class: _description_ + :param metadata_class: The metadata class :type metadata_class: _type_ - :param advanced_user: _description_, defaults to True + :param advanced_user: Whether the user is advanced, defaults to True :type advanced_user: bool, optional """ properties = scan_for_properties(metadata_class) @@ -42,20 +43,22 @@ def __init__(self, metadata_class, advanced_user: bool = True) -> None: ) def name_property_change_wrapper(self, func: Callable) -> Callable: - """_summary_ + """ + Wrap the property setter to update the acquisition name. - :param func: _description_ + :param func: The property setter function :type func: Callable - :return: _description_ + :return: The wrapped function :rtype: Callable """ def wrapper(object, value): - """_summary_ + """ + Wrapper function to update the acquisition name. - :param object: _description_ + :param object: The object :type object: _type_ - :param value: _description_ + :param value: The value to set :type value: _type_ """ func(object, value) diff --git a/src/view/widgets/acquisition_widgets/volume_model.py b/src/view/widgets/acquisition_widgets/volume_model.py index 1b7d71d..a901d5e 100644 --- a/src/view/widgets/acquisition_widgets/volume_model.py +++ b/src/view/widgets/acquisition_widgets/volume_model.py @@ -1,10 +1,11 @@ from math import radians, sqrt, tan +from typing import List, Optional, Tuple import numpy as np from pyqtgraph import makeRGBA from pyqtgraph.opengl import GLImageItem from qtpy.QtCore import Qt, Signal -from qtpy.QtGui import QMatrix4x4, QQuaternion, QVector3D +from qtpy.QtGui import QMatrix4x4, QQuaternion, QVector3D, QKeyEvent, QMouseEvent, QWheelEvent from qtpy.QtWidgets import QButtonGroup, QCheckBox, QGridLayout, QLabel, QMessageBox, QPushButton, QRadioButton, QWidget from scipy import spatial @@ -14,38 +15,43 @@ class SignalChangeVar: - """_summary_""" + """ + Descriptor class to emit a signal when a variable is changed. + """ - def __set_name__(self, owner, name): - """_summary_ + def __set_name__(self, owner: type, name: str) -> None: + """ + Set the name of the variable. - :param owner: _description_ - :type owner: _type_ - :param name: _description_ - :type name: _type_ + :param owner: The owner class + :type owner: type + :param name: The name of the variable + :type name: str """ self.name = f"_{name}" - def __set__(self, instance, value): - """_summary_ + def __set__(self, instance: object, value: object) -> None: + """ + Set the value of the variable and emit a signal. - :param instance: _description_ - :type instance: _type_ - :param value: _description_ - :type value: _type_ + :param instance: The instance of the class + :type instance: object + :param value: The value to set + :type value: object """ setattr(instance, self.name, value) # initially setting attr instance.valueChanged.emit(self.name[1:]) - def __get__(self, instance, value): - """_summary_ - - :param instance: _description_ - :type instance: _type_ - :param value: _description_ - :type value: _type_ - :return: _description_ - :rtype: _type_ + def __get__(self, instance: object, owner: type) -> object: + """ + Get the value of the variable. + + :param instance: The instance of the class + :type instance: object + :param owner: The owner class + :type owner: type + :return: The value of the variable + :rtype: object """ return getattr(instance, self.name) @@ -53,7 +59,7 @@ def __get__(self, instance, value): class VolumeModel(GLOrthoViewWidget): """ Widget to display configured acquisition grid. Note that the x and y refer to the tiling - dimensions and z is the scanning dimension + dimensions and z is the scanning dimension. """ fov_dimensions = SignalChangeVar() @@ -69,10 +75,10 @@ class VolumeModel(GLOrthoViewWidget): def __init__( self, unit: str = "mm", - limits: list[[float, float], [float, float], [float, float]] = None, - fov_dimensions: list[float, float, float] = None, - fov_position: list[float, float, float] = None, - coordinate_plane: list[str, str, str] = None, + limits: Optional[List[Tuple[float, float]]] = None, + fov_dimensions: Optional[List[float]] = None, + fov_position: Optional[List[float]] = None, + coordinate_plane: Optional[List[str]] = None, fov_color: str = "yellow", fov_line_width: int = 2, fov_opacity: float = 0.15, @@ -89,50 +95,51 @@ def __init__( limits_line_width: int = 2, limits_color: str = "white", limits_opacity: float = 0.1, - ): - """_summary_ + ) -> None: + """ + Initialize the VolumeModel. - :param unit: _description_, defaults to "mm" + :param unit: Unit of measurement, defaults to "mm" :type unit: str, optional - :param limits: _description_, defaults to None + :param limits: Limits for the volume, defaults to None :type limits: list[[float, float], [float, float], [float, float]], optional - :param fov_dimensions: _description_, defaults to None + :param fov_dimensions: Dimensions of the field of view, defaults to None :type fov_dimensions: list[float, float, float], optional - :param fov_position: _description_, defaults to None + :param fov_position: Position of the field of view, defaults to None :type fov_position: list[float, float, float], optional - :param coordinate_plane: _description_, defaults to None + :param coordinate_plane: Coordinate plane, defaults to None :type coordinate_plane: list[str, str, str], optional - :param fov_color: _description_, defaults to "yellow" + :param fov_color: Color of the field of view, defaults to "yellow" :type fov_color: str, optional - :param fov_line_width: _description_, defaults to 2 + :param fov_line_width: Line width of the field of view, defaults to 2 :type fov_line_width: int, optional - :param fov_opacity: _description_, defaults to 0.15 + :param fov_opacity: Opacity of the field of view, defaults to 0.15 :type fov_opacity: float, optional - :param path_line_width: _description_, defaults to 2 + :param path_line_width: Line width of the path, defaults to 2 :type path_line_width: int, optional - :param path_arrow_size: _description_, defaults to 6.0 + :param path_arrow_size: Arrow size of the path, defaults to 6.0 :type path_arrow_size: float, optional - :param path_arrow_aspect_ratio: _description_, defaults to 4 + :param path_arrow_aspect_ratio: Arrow aspect ratio of the path, defaults to 4 :type path_arrow_aspect_ratio: int, optional - :param path_start_color: _description_, defaults to "magenta" + :param path_start_color: Start color of the path, defaults to "magenta" :type path_start_color: str, optional - :param path_end_color: _description_, defaults to "green" + :param path_end_color: End color of the path, defaults to "green" :type path_end_color: str, optional - :param active_tile_color: _description_, defaults to "cyan" + :param active_tile_color: Color of the active tile, defaults to "cyan" :type active_tile_color: str, optional - :param active_tile_opacity: _description_, defaults to 0.075 + :param active_tile_opacity: Opacity of the active tile, defaults to 0.075 :type active_tile_opacity: float, optional - :param inactive_tile_color: _description_, defaults to "red" + :param inactive_tile_color: Color of the inactive tile, defaults to "red" :type inactive_tile_color: str, optional - :param inactive_tile_opacity: _description_, defaults to 0.025 + :param inactive_tile_opacity: Opacity of the inactive tile, defaults to 0.025 :type inactive_tile_opacity: float, optional - :param tile_line_width: _description_, defaults to 2 + :param tile_line_width: Line width of the tile, defaults to 2 :type tile_line_width: int, optional - :param limits_line_width: _description_, defaults to 2 + :param limits_line_width: Line width of the limits, defaults to 2 :type limits_line_width: int, optional - :param limits_color: _description_, defaults to "white" + :param limits_color: Color of the limits, defaults to "white" :type limits_color: str, optional - :param limits_opacity: _description_, defaults to 0.1 + :param limits_opacity: Opacity of the limits, defaults to 0.1 :type limits_opacity: float, optional """ super().__init__(rotationMethod="quaternion") @@ -269,11 +276,12 @@ def __init__( self.widgets.setMaximumHeight(70) self.widgets.show() - def update_model(self, attribute_name) -> None: - """_summary_ + def update_model(self, attribute_name: str) -> None: + """ + Update the model based on the changed attribute. - :param attribute_name: _description_ - :type attribute_name: _type_ + :param attribute_name: The name of the changed attribute + :type attribute_name: str """ # update color of tiles based on z position flat_coords = self.grid_coords.reshape([-1, 3]) # flatten array @@ -356,19 +364,21 @@ def update_model(self, attribute_name) -> None: self._update_opts() - def toggle_view_plane(self, button) -> None: - """_summary_ + def toggle_view_plane(self, button: QRadioButton) -> None: + """ + Toggle the view plane based on the selected button. - :param button: _description_ - :type button: _type_ + :param button: The radio button that was clicked + :type button: QRadioButton """ view_plane = tuple(x for x in button.text() if x.isalpha()) self.view_plane = view_plane - def set_path_pos(self, coord_order: list) -> None: - """_summary_ + def set_path_pos(self, coord_order: List[List[float]]) -> None: + """ + Set the path position based on the coordinate order. - :param coord_order: _description_ + :param coord_order: The order of coordinates :type coord_order: list """ path = np.array( @@ -382,12 +392,13 @@ def set_path_pos(self, coord_order: list) -> None: ) self.path.setData(pos=path) - def add_fov_image(self, image: np.ndarray, levels: list[float]) -> None: - """_summary_ + def add_fov_image(self, image: np.ndarray, levels: List[float]) -> None: + """ + Add a field of view image. - :param image: _description_ + :param image: The image to add :type image: np.ndarray - :param levels: _description_ + :param levels: The levels for the image :type levels: list[float] """ image_rgba = makeRGBA(image, levels=levels) @@ -421,12 +432,13 @@ def add_fov_image(self, image: np.ndarray, levels: list[float]) -> None: if self.view_plane != (self.coordinate_plane[0], self.coordinate_plane[1]): gl_image.setVisible(False) - def adjust_glimage_contrast(self, image: np.ndarray, contrast_levels: list[float]) -> None: - """_summary_ + def adjust_glimage_contrast(self, image: np.ndarray, contrast_levels: List[float]) -> None: + """ + Adjust the contrast of a GL image. - :param image: _description_ + :param image: The image to adjust :type image: np.ndarray - :param contrast_levels: _description_ + :param contrast_levels: The contrast levels :type contrast_levels: list[float] """ if image.tobytes() in self.fov_images.keys(): # check if image has been deleted @@ -435,16 +447,19 @@ def adjust_glimage_contrast(self, image: np.ndarray, contrast_levels: list[float self.add_fov_image(image, contrast_levels) def toggle_fov_image_visibility(self, visible: bool) -> None: - """_summary_ + """ + Toggle the visibility of the field of view images. - :param visible: _description_ + :param visible: Whether the images should be visible :type visible: bool """ for image in self.fov_images.values(): image.setVisible(visible) def _update_opts(self) -> None: - """_summary_""" + """ + Update the options for the view. + """ view_plane = self.view_plane view_pol = [ self.polarity[self.coordinate_plane.index(view_plane[0])], @@ -540,13 +555,14 @@ def _update_opts(self) -> None: self.update() - def move_fov_query(self, new_fov_pos: list[float]) -> [int, bool]: - """_summary_ + def move_fov_query(self, new_fov_pos: List[float]) -> Tuple[int, bool]: + """ + Query the user to move the field of view. - :param new_fov_pos: _description_ + :param new_fov_pos: The new position of the field of view :type new_fov_pos: list[float] - :return: _description_ - :rtype: [int, bool] + :return: The result of the query and whether to move to the nearest tile + :rtype: tuple[int, bool] """ msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Question) @@ -564,12 +580,13 @@ def move_fov_query(self, new_fov_pos: list[float]) -> [int, bool]: return msgBox.exec(), checkbox.isChecked() - def delete_fov_image_query(self, fov_image_pos: list[float]) -> int: - """_summary_ + def delete_fov_image_query(self, fov_image_pos: List[float]) -> int: + """ + Query the user to delete a field of view image. - :param fov_image_pos: _description_ + :param fov_image_pos: The position of the field of view image :type fov_image_pos: list[float] - :return: _description_ + :return: The result of the query :rtype: int """ msgBox = QMessageBox() @@ -580,11 +597,12 @@ def delete_fov_image_query(self, fov_image_pos: list[float]) -> int: return msgBox.exec() - def mousePressEvent(self, event) -> None: - """_summary_ + def mousePressEvent(self, event: QMouseEvent) -> None: + """ + Handle mouse press events. - :param event: _description_ - :type event: _type_ + :param event: The mouse event + :type event: QMouseEvent """ plane = list(self.view_plane) + [ax for ax in self.coordinate_plane if ax not in self.view_plane] view_pol = [ @@ -651,26 +669,38 @@ def mousePressEvent(self, event) -> None: if delete_key is not None: del self.fov_images[delete_key] - def mouseMoveEvent(self, event): + def mouseMoveEvent(self, event: QMouseEvent) -> None: """ Override mouseMoveEvent so user can't change view. + + :param event: The mouse event + :type event: QMouseEvent """ pass - def wheelEvent(self, event): + def wheelEvent(self, event: QWheelEvent) -> None: """ Override wheelEvent so user can't change view. + + :param event: The wheel event + :type event: QWheelEvent """ pass - def keyPressEvent(self, event): + def keyPressEvent(self, event: QKeyEvent) -> None: """ Override keyPressEvent so user can't change view. + + :param event: The key event + :type event: QKeyEvent """ pass - def keyReleaseEvent(self, event): + def keyReleaseEvent(self, event: QKeyEvent) -> None: """ - Override keyPressEvent so user can't change view. + Override keyReleaseEvent so user can't change view. + + :param event: The key event + :type event: QKeyEvent """ pass diff --git a/src/view/widgets/acquisition_widgets/volume_plan_widget.py b/src/view/widgets/acquisition_widgets/volume_plan_widget.py index 6c05423..d6181cf 100644 --- a/src/view/widgets/acquisition_widgets/volume_plan_widget.py +++ b/src/view/widgets/acquisition_widgets/volume_plan_widget.py @@ -1,5 +1,5 @@ -from typing import Generator, Literal, Union - +from typing import Generator, Literal, Union, Optional, List + import numpy as np import useq from qtpy.QtCore import Qt, Signal @@ -32,10 +32,11 @@ class GridFromEdges(useq.GridFromEdges): reverse = property() # initialize property - def __init__(self, reverse=False, *args, **kwargs): - """_summary_ + def __init__(self, reverse: bool = False, *args, **kwargs) -> None: + """ + Initialize the GridFromEdges. - :param reverse: _description_, defaults to False + :param reverse: Whether to reverse the order, defaults to False :type reverse: bool, optional """ # rewrite property since pydantic doesn't allow to add attr @@ -44,9 +45,10 @@ def __init__(self, reverse=False, *args, **kwargs): @property def rows(self) -> int: - """_summary_ + """ + Get the number of rows. - :return: _description_ + :return: The number of rows :rtype: int """ dx, _ = self._step_size(self.fov_width, self.fov_height) @@ -54,18 +56,20 @@ def rows(self) -> int: @property def columns(self) -> int: - """_summary_ + """ + Get the number of columns. - :return: _description_ + :return: The number of columns :rtype: int """ _, dy = self._step_size(self.fov_width, self.fov_height) return self._ncolumns(dy) def iter_grid_positions(self, *args, **kwargs) -> Generator: - """_summary_ + """ + Iterate over grid positions. - :yield: _description_ + :yield: The grid positions :rtype: Generator """ if not self.reverse: @@ -83,10 +87,11 @@ class GridWidthHeight(useq.GridWidthHeight): reverse = property() - def __init__(self, reverse=False, *args, **kwargs): - """_summary_ + def __init__(self, reverse: bool = False, *args, **kwargs) -> None: + """ + Initialize the GridWidthHeight. - :param reverse: _description_, defaults to False + :param reverse: Whether to reverse the order, defaults to False :type reverse: bool, optional """ # rewrite property since pydantic doesn't allow to add attr @@ -95,9 +100,10 @@ def __init__(self, reverse=False, *args, **kwargs): @property def rows(self) -> int: - """_summary_ + """ + Get the number of rows. - :return: _description_ + :return: The number of rows :rtype: int """ dx, _ = self._step_size(self.fov_width, self.fov_height) @@ -105,18 +111,20 @@ def rows(self) -> int: @property def columns(self) -> int: - """_summary_ + """ + Get the number of columns. - :return: _description_ + :return: The number of columns :rtype: int """ _, dy = self._step_size(self.fov_width, self.fov_height) return self._ncolumns(dy) def iter_grid_positions(self, *args, **kwargs) -> Generator: - """_summary_ + """ + Iterate over grid positions. - :yield: _description_ + :yield: The grid positions :rtype: Generator """ if not self.reverse: @@ -134,19 +142,21 @@ class GridRowsColumns(useq.GridRowsColumns): reverse = property() - def __init__(self, reverse=False, *args, **kwargs): - """_summary_ + def __init__(self, reverse: bool = False, *args, **kwargs) -> None: + """ + Initialize the GridRowsColumns. - :param reverse: _description_, defaults to False + :param reverse: Whether to reverse the order, defaults to False :type reverse: bool, optional """ setattr(type(self), "reverse", property(fget=lambda x: reverse)) super().__init__(*args, **kwargs) def iter_grid_positions(self, *args, **kwargs) -> Generator: - """_summary_ + """ + Iterate over grid positions. - :yield: _description_ + :yield: The grid positions :rtype: Generator """ if not self.reverse: @@ -166,23 +176,24 @@ class VolumePlanWidget(QMainWindow): def __init__( self, - limits: list[[float, float], [float, float], [float, float]] = None, - fov_dimensions: list[float, float, float] = None, - fov_position: list[float, float, float] = None, - coordinate_plane: list[str, str, str] = None, + limits: Optional[List[List[float]]] = None, + fov_dimensions: Optional[List[float]] = None, + fov_position: Optional[List[float]] = None, + coordinate_plane: Optional[List[str]] = None, unit: str = "um", - ): - """_summary_ - - :param limits: _description_, defaults to None - :type limits: list[[float, float], [float, float], [float, float]], optional - :param fov_dimensions: _description_, defaults to None - :type fov_dimensions: list[float, float, float], optional - :param fov_position: _description_, defaults to None - :type fov_position: list[float, float, float], optional - :param coordinate_plane: _description_, defaults to None - :type coordinate_plane: list[str, str, str], optional - :param unit: _description_, defaults to "um" + ) -> None: + """ + Initialize the VolumePlanWidget. + + :param limits: The limits for the volume, defaults to None + :type limits: Optional[List[List[float]]], optional + :param fov_dimensions: The dimensions of the field of view, defaults to None + :type fov_dimensions: Optional[List[float]], optional + :param fov_position: The position of the field of view, defaults to None + :type fov_position: Optional[List[float]], optional + :param coordinate_plane: The coordinate plane, defaults to None + :type coordinate_plane: Optional[List[str]], optional + :param unit: The unit of measurement, defaults to "um" :type unit: str, optional """ super().__init__() @@ -412,9 +423,10 @@ def __init__( self.update_tile_table(self.value()) # initialize table def update_tile_table(self, value: Union[GridRowsColumns, GridFromEdges, GridWidthHeight]) -> None: - """_summary_ + """ + Update the tile table with the given value. - :param value: _description_ + :param value: The grid value to update the table with :type value: Union[GridRowsColumns, GridFromEdges, GridWidthHeight] """ # check if order changed @@ -450,7 +462,9 @@ def update_tile_table(self, value: Union[GridRowsColumns, GridFromEdges, GridWid # return def refill_table(self) -> None: - """_summary_""" + """ + Refill the tile table with the current grid values. + """ value = self.value() self.tile_table.clearContents() self.tile_table.setRowCount(0) @@ -464,11 +478,12 @@ def refill_table(self) -> None: self.header.blockSignals(False) def add_tile_to_table(self, row: int, column: int) -> None: - """_summary_ + """ + Add a tile to the table at the specified row and column. - :param row: _description_ + :param row: The row index :type row: int - :param column: _description_ + :param column: The column index :type column: int """ self.tile_table.blockSignals(True) @@ -515,13 +530,14 @@ def add_tile_to_table(self, row: int, column: int) -> None: self.tile_table.blockSignals(False) def toggle_visibility(self, checked: bool, row: int, column: int) -> None: - """_summary_ + """ + Toggle the visibility of a tile. - :param checked: _description_ + :param checked: Whether the tile is visible :type checked: bool - :param row: _description_ + :param row: The row index :type row: int - :param column: _description_ + :param column: The column index :type column: int """ self._tile_visibility[row, column] = checked @@ -534,9 +550,10 @@ def toggle_visibility(self, checked: bool, row: int, column: int) -> None: self.valueChanged.emit(self.value()) def tile_table_changed(self, item: QTableWidgetItem) -> None: - """_summary_ + """ + Handle changes to the tile table. - :param item: _description_ + :param item: The table widget item that changed :type item: QTableWidgetItem """ row, column = [int(x) for x in self.tile_table.item(item.row(), 0).text() if x.isdigit()] @@ -559,11 +576,12 @@ def tile_table_changed(self, item: QTableWidgetItem) -> None: self.grid_offset_widgets[2].setValue(value) def toggle_grid_position(self, enable: bool, index: Literal[0, 1, 2]) -> None: - """_summary_ + """ + Toggle the grid position. - :param enable: _description_ + :param enable: Whether to enable the grid position :type enable: bool - :param index: _description_ + :param index: The index of the grid position :type index: Literal[0, 1, 2] """ self.grid_offset_widgets[index].setEnabled(enable) @@ -575,18 +593,20 @@ def toggle_grid_position(self, enable: bool, index: Literal[0, 1, 2]) -> None: @property def apply_all(self) -> bool: - """_summary_ + """ + Get whether to apply all settings. - :return: _description_ + :return: Whether to apply all settings :rtype: bool """ return self._apply_all @apply_all.setter def apply_all(self, value: bool) -> None: - """_summary_ + """ + Set whether to apply all settings. - :param value: _description_ + :param value: Whether to apply all settings :type value: bool """ self._apply_all = value @@ -610,21 +630,23 @@ def apply_all(self, value: bool) -> None: self.refill_table() # order, pos, and visibilty doesn't change, so update table to reconfigure editablility @property - def fov_position(self) -> list[float, float, float]: - """_summary_ + def fov_position(self) -> List[float]: + """ + Get the field of view position. - :return: _description_ - :rtype: list[float, float, float] + :return: The field of view position + :rtype: List[float] """ return self._fov_position @fov_position.setter - def fov_position(self, value: list[float, float, float]) -> None: - """_summary_ + def fov_position(self, value: List[float]) -> None: + """ + Set the field of view position. - :param value: _description_ - :type value: list[float, float, float] - :raises ValueError: _description_ + :param value: The field of view position + :type value: List[float] + :raises ValueError: If the value is not a list of length 3 """ if type(value) is not list and len(value) != 3: raise ValueError @@ -639,21 +661,23 @@ def fov_position(self, value: list[float, float, float]) -> None: self._on_change() @property - def fov_dimensions(self) -> list[float, float, float]: - """_summary_ + def fov_dimensions(self) -> List[float]: + """ + Get the field of view dimensions. - :return: _description_ - :rtype: list[float, float, float] + :return: The field of view dimensions + :rtype: List[float] """ return self._fov_dimensions @fov_dimensions.setter - def fov_dimensions(self, value: list[float, float, float]) -> None: - """_summary_ + def fov_dimensions(self, value: List[float]) -> None: + """ + Set the field of view dimensions. - :param value: _description_ - :type value: list[float, float, float] - :raises ValueError: _description_ + :param value: The field of view dimensions + :type value: List[float] + :raises ValueError: If the value is not a list of length 2 """ if type(value) is not list and len(value) != 2: raise ValueError @@ -661,21 +685,23 @@ def fov_dimensions(self, value: list[float, float, float]) -> None: self._on_change() @property - def grid_offset(self) -> list[float, float, float]: - """_summary_ + def grid_offset(self) -> List[float]: + """ + Get the grid offset. - :return: _description_ - :rtype: list[float, float, float] + :return: The grid offset + :rtype: List[float] """ return self._grid_offset @grid_offset.setter - def grid_offset(self, value: list[float, float, float]) -> None: - """_summary_ + def grid_offset(self, value: List[float]) -> None: + """ + Set the grid offset. - :param value: _description_ - :type value: list[float, float, float] - :raises ValueError: _description_ + :param value: The grid offset + :type value: List[float] + :raises ValueError: If the value is not a list of length 3 """ if type(value) is not list and len(value) != 3: raise ValueError @@ -684,11 +710,12 @@ def grid_offset(self, value: list[float, float, float]) -> None: self._on_change() @property - def tile_positions(self) -> [[float, float, float]]: - """_summary_ + def tile_positions(self) -> List[List[float]]: + """ + Get the tile positions. - :return: _description_ - :rtype: [[float, float, float]] + :return: The tile positions + :rtype: List[List[float]] """ value = self.value() coords = np.zeros((value.rows, value.columns, 3)) @@ -706,36 +733,39 @@ def tile_positions(self) -> [[float, float, float]]: @property def tile_visibility(self) -> np.ndarray: - """_summary_ + """ + Get the tile visibility. - :return: _description_ + :return: The tile visibility :rtype: np.ndarray """ return self._tile_visibility @property def scan_starts(self) -> np.ndarray: - """_summary_ + """ + Get the scan start positions. - :return: _description_ + :return: The scan start positions :rtype: np.ndarray """ return self._scan_starts @property def scan_ends(self) -> np.ndarray: - """_summary_ + """ + Get the scan end positions. - :return: _description_ + :return: The scan end positions :rtype: np.ndarray """ return self._scan_ends def _on_change(self) -> None: - """_summary_ + """ + Handle changes to the grid. - :return: _description_ - :rtype: _type_ + :return: None """ if (val := self.value()) is None: return # pragma: no cover @@ -749,20 +779,22 @@ def _on_change(self) -> None: @property def mode(self) -> Literal["number", "area", "bounds"]: - """_summary_ + """ + Get the grid mode. - :return: _description_ - :rtype: _type_ + :return: The grid mode + :rtype: Literal["number", "area", "bounds"] """ return self._mode @mode.setter def mode(self, value: Literal["number", "area", "bounds"]) -> None: - """_summary_ + """ + Set the grid mode. - :param value: _description_ - :type value: Literal["number", "area", "bounds"] - :raises ValueError: _description_ + :param value: The grid mode + :type value: Literal["number", "area", "bounds"] + :raises ValueError: If the value is not a valid mode """ if value not in ["number", "area", "bounds"]: raise ValueError @@ -783,10 +815,11 @@ def mode(self, value: Literal["number", "area", "bounds"]) -> None: self._on_change() def value(self) -> Union[GridRowsColumns, GridFromEdges, GridWidthHeight]: - """_summary_ + """ + Get the current grid value. - :raises NotImplementedError: _description_ - :return: _description_ + :raises NotImplementedError: If the mode is not implemented + :return: The current grid value :rtype: Union[GridRowsColumns, GridFromEdges, GridWidthHeight] """ over = self.overlap.value() @@ -822,12 +855,13 @@ def value(self) -> Union[GridRowsColumns, GridFromEdges, GridWidthHeight]: raise NotImplementedError -def line(): - """_summary_ +def line() -> QFrame: + """ + Create a horizontal line. - :return: _description_ - :rtype: _type_ + :return: A horizontal line frame + :rtype: QFrame """ frame = QFrame() frame.setFrameShape(QFrame.HLine) - return frame + return frame \ No newline at end of file diff --git a/src/view/widgets/device_widgets/filter_wheel_widget.py b/src/view/widgets/device_widgets/filter_wheel_widget.py index 5205895..b90da09 100644 --- a/src/view/widgets/device_widgets/filter_wheel_widget.py +++ b/src/view/widgets/device_widgets/filter_wheel_widget.py @@ -1,33 +1,31 @@ from math import atan, cos, degrees, pi, radians, sin from typing import Callable, Union -from pyqtgraph import (PlotWidget, ScatterPlotItem, TextItem, mkBrush, mkPen, - setConfigOptions) +from pyqtgraph import PlotWidget, ScatterPlotItem, TextItem, mkBrush, mkPen, setConfigOptions from qtpy.QtCore import Property, QObject, QTimer, Signal, Slot from qtpy.QtGui import QColor, QFont from qtpy.QtWidgets import QComboBox, QGraphicsEllipseItem -from view.widgets.base_device_widget import (BaseDeviceWidget, - scan_for_properties) +from view.widgets.base_device_widget import BaseDeviceWidget, scan_for_properties setConfigOptions(antialias=True) class FilterWheelWidget(BaseDeviceWidget): - """_summary_""" + """Widget for controlling a filter wheel device.""" - def __init__(self, filter_wheel, colors: dict = None, advanced_user: bool = True): - """_summary_ + def __init__(self, filter_wheel: object, colors: dict = None, advanced_user: bool = True): + """ + Initialize the FilterWheelWidget. - :param filter_wheel: _description_ - :type filter_wheel: _type_ - :param colors: _description_, defaults to None + :param filter_wheel: The filter wheel device. + :type filter_wheel: object + :param colors: Dictionary of colors for the filters, defaults to None. :type colors: dict, optional - :param advanced_user: _description_, defaults to True + :param advanced_user: Flag to enable advanced user features, defaults to True. :type advanced_user: bool, optional """ properties = scan_for_properties(filter_wheel) - # wrap filterwheel filter property to emit signal when set filter_setter = getattr(type(filter_wheel).filter, "fset") filter_getter = getattr(type(filter_wheel).filter, "fget") @@ -66,21 +64,23 @@ def __init__(self, filter_wheel, colors: dict = None, advanced_user: bool = True self.filter_widget.setDisabled(True) def filter_change_wrapper(self, func: Callable) -> Callable: - """_summary_ + """ + Wrap the filter change function to emit a signal when the filter is changed. - :param func: _description_ + :param func: The original filter change function. :type func: Callable - :return: _description_ + :return: The wrapped function. :rtype: Callable """ - def wrapper(object, value): - """_summary_ + def wrapper(object: object, value: str) -> None: + """ + Wrapper function to emit signal on filter change. - :param object: _description_ - :type object: _type_ - :param value: _description_ - :type value: _type_ + :param object: The filter wheel object. + :type object: object + :param value: The new filter value. + :type value: str """ func(object, value) self.filter = value @@ -97,9 +97,10 @@ class FilterItem(ScatterPlotItem): pressed = Signal(str) def __init__(self, filter_name: str, *args, **kwargs): - """_summary_ + """ + Initialize the FilterItem. - :param filter_name: _description_ + :param filter_name: The name of the filter. :type filter_name: str """ self.filter_name = filter_name @@ -107,26 +108,29 @@ def __init__(self, filter_name: str, *args, **kwargs): def mousePressEvent(self, ev) -> None: """ - Emit signal containing filter_name when item is pressed - :param ev: QMousePressEvent triggered when item is clicked + Emit signal containing filter_name when item is pressed. + + :param ev: QMousePressEvent triggered when item is clicked. + :type ev: QMousePressEvent """ super().mousePressEvent(ev) self.pressed.emit(self.filter_name) class FilterWheelGraph(PlotWidget): - """_summary_""" + """Graphical representation of the filter wheel.""" ValueChangedInside = Signal((str,)) def __init__(self, filters: dict, colors: dict, diameter: float = 10.0, **kwargs): - """_summary_ + """ + Initialize the FilterWheelGraph. - :param filters: _description_ + :param filters: Dictionary of filters. :type filters: dict - :param colors: _description_ + :param colors: Dictionary of colors for the filters. :type colors: dict - :param diameter: _description_, defaults to 10.0 + :param diameter: Diameter of the filter wheel, defaults to 10.0. :type diameter: float, optional """ super().__init__(**kwargs) @@ -142,7 +146,7 @@ def __init__(self, filters: dict, colors: dict, diameter: float = 10.0, **kwargs # create wheel graphic wheel = QGraphicsEllipseItem(-self.diameter, -self.diameter, self.diameter * 2, self.diameter * 2) wheel.setPen(mkPen((0, 0, 0, 100))) # outline of wheel - wheel.setBrush(mkBrush((128, 128, 128))) # color of wheel + wheel.setBrush(mkBrush((65, 75, 70))) # color of wheel self.addItem(wheel) self.filter_path = self.diameter - 3 @@ -155,11 +159,21 @@ def __init__(self, filters: dict, colors: dict, diameter: float = 10.0, **kwargs angles = [pi / 2 + (2 * pi / l * i) for i in range(l)] self.points = {} for angle, (filter, i) in zip(angles, self.filters.items()): - color = QColor(colors.get(filter, "black")).getRgb() + color = colors.get(filter, "black") + if type(color) is str: + color = QColor(color).getRgb() + else: + color = QColor().fromRgb(*color).getRgb() + color = list(color) pos = [self.filter_path * cos(angle), self.filter_path * sin(angle)] # create scatter point filter point = FilterItem(filter_name=filter, size=filter_diameter, pxMode=False, pos=[pos]) - point.setPen(mkPen((0, 0, 0, 100), width=2)) # outline of filter + # update opacity of filter outline color + color[-1] = 255 + point.setBrush(mkBrush(tuple(color))) # color of filter + # update opacity of filter outline color + color[-1] = 128 + point.setPen(mkPen(tuple(color), width=3)) # outline of filter point.setBrush(mkBrush(color)) # color of filter point.pressed.connect(self.move_wheel) self.addItem(point) @@ -168,7 +182,7 @@ def __init__(self, filters: dict, colors: dict, diameter: float = 10.0, **kwargs # create label index = TextItem(text=str(i), anchor=(0.5, 0.5), color="white") font = QFont() - font.setPointSize(round(filter_diameter**2)) + font.setPointSize(round(filter_diameter**2 - 6)) index.setFont(font) index.setPos(*pos) self.addItem(index) @@ -186,9 +200,10 @@ def __init__(self, filters: dict, colors: dict, diameter: float = 10.0, **kwargs self.setAspectLocked(1) def move_wheel(self, name: str) -> None: - """_summary_ + """ + Move the wheel to the specified filter. - :param name: _description_ + :param name: The name of the filter to move to. :type name: str """ self.ValueChangedInside.emit(name) @@ -240,17 +255,18 @@ def move_wheel(self, name: str) -> None: @Slot(float) def move_point(self, angle: float, point: Union[FilterItem, TextItem]) -> None: - """_summary_ + """ + Move a point to a new angle. - :param angle: _description_ + :param angle: The angle to move the point to. :type angle: float - :param point: _description_ + :param point: The point to move. :type point: Union[FilterItem, TextItem] """ pos = [self.filter_path * cos(radians(angle)), self.filter_path * sin(radians(angle))] - if type(point) == FilterItem: + if type(point) is FilterItem: point.setData(pos=[pos]) - elif type(point) == TextItem: + elif type(point) is TextItem: point.setPos(*pos) @@ -261,17 +277,18 @@ class TimeLine(QObject): frameChanged = Signal(float) - def __init__(self, interval: int = 60, loopCount: int = 1, step_size: float = 1, parent=None): - """_summary_ + def __init__(self, interval: int = 60, loopCount: int = 1, step_size: float = 1, parent: QObject = None): + """ + Initialize the TimeLine. - :param interval: _description_, defaults to 60 + :param interval: Interval between steps in milliseconds, defaults to 60. :type interval: int, optional - :param loopCount: _description_, defaults to 1 + :param loopCount: Number of times to loop, defaults to 1. :type loopCount: int, optional - :param step_size: _description_, defaults to 1 + :param step_size: Size of each step, defaults to 1. :type step_size: float, optional - :param parent: _description_, defaults to None - :type parent: _type_, optional + :param parent: Parent QObject, defaults to None. + :type parent: QObject, optional """ super(TimeLine, self).__init__(parent) self._stepSize = step_size @@ -285,7 +302,7 @@ def __init__(self, interval: int = 60, loopCount: int = 1, step_size: float = 1, def on_timeout(self) -> None: """ - Function called by Qtimer that will trigger a step of current step_size and emit new counter value. + Function called by QTimer that will trigger a step of current step_size and emit new counter value. """ if (self._startFrame <= self._counter <= self._endFrame and self._stepSize > 0) or ( self._startFrame >= self._counter >= self._endFrame and self._stepSize < 0 @@ -300,47 +317,48 @@ def on_timeout(self) -> None: self._timer.stop() def setLoopCount(self, loopCount: int) -> None: - """_summary_ + """ + Set the number of times to loop. - :param loopCount: _description_ + :param loopCount: Number of times to loop. :type loopCount: int """ self._loopCount = loopCount def loopCount(self) -> int: - """_summary_ + """ + Get the number of times to loop. - :return: _description_ + :return: Number of times to loop. :rtype: int """ return self._loopCount - interval = Property(int, fget=loopCount, fset=setLoopCount) - def setInterval(self, interval: int) -> None: - """_summary_ + """ + Set the interval between steps. - :param interval: _description_ + :param interval: Interval between steps in milliseconds. :type interval: int """ self._timer.setInterval(interval) def interval(self) -> int: - """_summary_ + """ + Get the interval between steps. - :return: _description_ + :return: Interval between steps in milliseconds. :rtype: int """ return self._timer.interval() - interval = Property(int, fget=interval, fset=setInterval) - def setFrameRange(self, startFrame: float, endFrame: float) -> None: - """_summary_ + """ + Set the range of frames to step through. - :param startFrame: _description_ + :param startFrame: The starting frame. :type startFrame: float - :param endFrame: _description_ + :param endFrame: The ending frame. :type endFrame: float """ self._startFrame = startFrame @@ -349,12 +367,14 @@ def setFrameRange(self, startFrame: float, endFrame: float) -> None: @Slot() def start(self) -> None: """ - Function to start QTimer and begin emitting and stepping through value. + Start the QTimer and begin emitting and stepping through values. """ self._counter = self._startFrame self._loop_counter = 0 self._timer.start() def stop(self) -> None: - """Function to stop QTimer and stop stepping through values.""" + """ + Stop the QTimer and stop stepping through values. + """ self._timer.stop()