Source code for pymodaq.utils.scanner.scanner

from __future__ import annotations
from typing import Tuple, List, TYPE_CHECKING, Any
from collections import OrderedDict

from serializall import SerializableFactory, SerializableBase
from qtpy.QtCore import QObject, Signal
from qtpy import QtWidgets

from pymodaq_gui.messenger import messagebox
from pymodaq_utils.enums import StrEnum

from pymodaq_utils.logger import set_logger, get_module_name
from pymodaq_utils.config import GlobalConfig as Config
import pymodaq_utils.utils as utils

from pymodaq_gui.managers.parameter_manager import ParameterManager
from pymodaq_gui.parameter import Parameter

from pymodaq.utils.scanner.scan_factory import ScannerFactory, ScannerBase
from pymodaq.utils.scanner.utils import ScanInfo
from pymodaq.utils.scanner.scan_selector import Selector
from pymodaq.utils.data import DataToExport, DataActuator
from pymodaq_utils.utils import find_objects_in_list_from_attr_name_val

if TYPE_CHECKING:
    from pymodaq.control_modules.daq_move import DAQ_Move


logger = set_logger(get_module_name(__file__))
ser_factory = SerializableFactory()
config = Config()
scanner_factory = ScannerFactory()


class Orientation(StrEnum):
    VERTICAl = 'vertical'
    HORIZONTAL = 'horizontal'


[docs] class Scanner(QObject, ParameterManager): """Main Object to define a PyMoDAQ scan and create a UI to set it Parameters ---------- parent_widget: QtWidgets.QWidget scanner_items: list of GraphicItems used by ScanSelector for chosing scan area or linear traces actuators: List[DAQ_Move] list actuators names See Also -------- ScanSelector, ScannerBase, TableModelSequential, TableModelTabular, pymodaq_types.TableViewCustom """ scanner_updated_signal = Signal() scanner_changed_signal = Signal() # emitted when the scanner implementation (type, subtype, actuators) changes settings_name = 'scanner' params = [ {'title': 'Actuators:', 'name': 'actuators', 'type': 'itemselect', 'checkbox': True, 'visible': False}, {'title': 'Calculate positions:', 'name': 'calculate_positions', 'type': 'bool_push', 'label': 'Calculate positions'}, {'title': 'N steps:', 'name': 'n_steps', 'type': 'int', 'value': 0, 'readonly': True}, {'title': 'Scan type:', 'name': 'scan_type', 'type': 'list', 'limits': scanner_factory.scan_types()}, {'title': 'Scan subtype:', 'name': 'scan_sub_type', 'type': 'list', 'limits': scanner_factory.scan_sub_types(scanner_factory.scan_types()[0])}, {'title': 'Units handling', 'name': 'units_handling', 'type': 'group', 'children': [ {'title': 'Display units', 'name': 'display_units', 'type': 'bool', 'value': True}, {'title': 'Default units', 'name': 'common_units', 'type': 'str', 'value': '', 'visible': False, 'readonly': True}, ]}, ] def __init__(self, parent_widget: QtWidgets.QWidget = None, actuators: List[DAQ_Move] = None, selected_actuators: list[DAQ_Move] = None, orientation: Orientation = Orientation.VERTICAl): if actuators is None: actuators = [] if selected_actuators is None: selected_actuators = [] QObject.__init__(self) ParameterManager.__init__(self) self._actuators: list[DAQ_Move] = selected_actuators self._actuators_all: list[DAQ_Move] = actuators self.orientation: Orientation = orientation if parent_widget is None: parent_widget = QtWidgets.QWidget() self.parent_widget = parent_widget self._scanner_settings_widget = None self.connect_things() self._scanner: ScannerBase = None self.setup_ui() self.actuators = selected_actuators if self._scanner is not None: self.settings.child('n_steps').setValue(self._scanner.evaluate_steps()) def __repr__(self): return f"Scanner {self.scan_type}/{self.scan_sub_type} {self.settings['n_steps']} steps of {self.actuators}"
[docs] def to_dict(self, use_real_actuators = False) -> dict[str, Any]: """ Dictionary representation of the scanner object if use_real_actuators is True, populate the dictionary with real actuator objects else their title This distinction allows to simply serialize Scanner when using strings while serializing DAQ_Move is not that simple """ return dict(actuators=self.actuators_all if use_real_actuators else [act.title for act in self.actuators_all], selected=self.actuators if use_real_actuators else [act.title for act in self.actuators], scan_type=self.scan_type, scan_sub_type=self.scan_sub_type, display_units = self.settings['units_handling', 'display_units'], n_steps = self.settings['n_steps'], scanner=self.scanner.to_dict())
def from_dict(self, scanner_dict: dict): self.actuators_all = scanner_dict['actuators'] self.actuators = scanner_dict['selected'] # should DAQ_Move instances, this is not symmetric wrt to_dict() except if you use the real_actuators argument in to_dict() self.set_scan_type_and_subtypes(scanner_dict['scan_type'], scanner_dict['scan_sub_type']) self.settings['units_handling', 'display_units'] = scanner_dict['display_units'] self.settings['n_steps'] = scanner_dict['n_steps'] self.set_scanner() self.scanner.from_dict(scanner_dict['scanner']) def setup_ui(self): self.parent_widget.setLayout(QtWidgets.QVBoxLayout() if self.orientation == Orientation.VERTICAl else QtWidgets.QHBoxLayout()) self.parent_widget.layout().setContentsMargins(0, 0, 0, 0) self._settings_tree.collapsible_widget.setVisible(False) self.parent_widget.layout().addWidget(self.settings_tree) self._scanner_settings_widget = QtWidgets.QWidget() self._scanner_settings_widget.setLayout(QtWidgets.QVBoxLayout()) self._scanner_settings_widget.layout().setContentsMargins(0, 0, 0, 0) self.parent_widget.layout().addWidget(self._scanner_settings_widget) self.settings_tree.setMinimumHeight(110) def set_scanner(self): try: self._scanner: ScannerBase = scanner_factory.get( self.settings['scan_type'], self.settings['scan_sub_type'], actuators=self.actuators, settings = self.settings ) while True: child = self._scanner_settings_widget.layout().takeAt(0) if not child: break child.widget().deleteLater() QtWidgets.QApplication.processEvents() self._scanner._settings_tree.collapsible_widget.setVisible(False) self._scanner_settings_widget.layout().addWidget(self._scanner.settings_tree) self._scanner.settings.sigTreeStateChanged.connect(self._update_steps) self.scanner_changed_signal.emit() except ValueError as e: pass @property def scanner(self) -> ScannerBase: return self._scanner
[docs] def get_scanner_sub_settings(self): """Get the current ScannerBase implementation's settings""" return self._scanner.settings
def value_changed(self, param: Parameter): if param.name() == 'calculate_positions': if param.value(): param.setValue(False) if param.name() == 'scan_type': self.settings.child('scan_sub_type').setOpts( limits=scanner_factory.scan_sub_types(param.value())) if param.name() in ['scan_type', 'scan_sub_type']: self.settings.child('units_handling', 'display_units').show() self.set_scanner() self.settings.child('scan_type').setOpts(tip=self._scanner.__doc__) self.settings.child('scan_sub_type').setOpts(tip=self._scanner.__doc__) elif param.name() == 'display_units': if not param.value() and len(self.actuators) > 0: units = set([act.units for act in self.actuators]) if len(units) > 1: messagebox(title='Info', text='Could not use the same units for all settings as units are not compatible') param.setValue(True) self.settings.child('units_handling', 'common_units').show(False) else: self.settings.child('units_handling', 'common_units').setValue(list(units)[0]) self.settings.child('units_handling', 'common_units').show(True) else: self.settings.child('units_handling', 'common_units').show(False) self.set_scanner() elif param.name() == 'actuators': self.actuators = [act for act in self.actuators_all if act.title in param.value()['selected']] if self._scanner is not None: self.settings.child('n_steps').setValue(self._scanner.evaluate_steps()) @property def actuators_all(self) -> list[DAQ_Move]: """list of DAQ_Move: Returns as a list the name of the selected actuators to describe the actual scan""" return self._actuators_all @actuators_all.setter def actuators_all(self, actuators: list[DAQ_Move]): self._actuators_all = actuators self.settings.child('actuators').setValue({'all_items': [actuator.title for actuator in actuators], 'selected': [actuator.title for actuator in actuators]}) @property def actuators(self) -> list[DAQ_Move]: """list of DAQ_Move: Returns as a list the name of the selected actuators to describe the actual scan""" return self._actuators @actuators.setter def actuators(self, act_list: list[DAQ_Move]): self._actuators = act_list for act in act_list: if act not in self.actuators_all: self._actuators_all.append(act) self.settings.child('actuators').setValue({'all_items': [actuator.title for actuator in self._actuators_all], 'selected': [actuator.title for actuator in act_list]}) self.set_scanner() def set_actuators(self, actuators: list[DAQ_Move]): self.actuators = actuators
[docs] def set_scan_type_and_subtypes(self, scan_type: str, scan_subtype: str = None): """Convenience function to set the main scan type Parameters ---------- scan_type: str one of registered Scanner main identifier scan_subtype: list of str or None one of registered Scanner second identifier for a given main identifier See Also -------- ScannerFactory """ if scan_type in scanner_factory.scan_types(): self.settings.child('scan_type').setValue(scan_type) QtWidgets.QApplication.processEvents() if scan_subtype is not None: if scan_subtype in scanner_factory.scan_sub_types(scan_type): self.settings.child('scan_sub_type').setValue(scan_subtype)
def set_scan_from_settings(self, settings: Parameter, scanner_settings: Parameter): self.set_scan_type_and_subtypes(settings['scan_type'], settings['scan_sub_type']) self.settings = settings self._scanner.settings = scanner_settings @property def scan_type(self) -> str: return self.settings['scan_type'] @property def scan_sub_type(self) -> str: return self.settings['scan_sub_type'] def connect_things(self): self.scanner_updated_signal.connect(self.save_scanner_settings) def save_scanner_settings(self): self._scanner.save_scan_parameters()
[docs] def get_scan_info(self) -> ScanInfo: """Get a summary of the configured scan as a ScanInfo object""" return ScanInfo(self._scanner.n_steps, positions=self._scanner.positions, axes_indexes=self._scanner.axes_indexes, axes_unique=self._scanner.axes_unique, selected_actuators=[act.title for act in self.actuators])
def get_nav_axes(self): return self._scanner.get_nav_axes() def get_scan_shape(self): return self._scanner.get_scan_shape()
[docs] def get_indexes_from_scan_index(self, scan_index: int) -> Tuple[int]: """To be reimplemented. Calculations of indexes within the scan""" return self._scanner.get_indexes_from_scan_index(scan_index)
def _update_steps(self): self.settings.child('n_steps').setValue(self.n_steps) @property def n_steps(self): return self._scanner.evaluate_steps() @property def n_axes(self): return self._scanner.n_axes @property def positions(self): return self._scanner.positions
[docs] def positions_at(self, index: int) -> DataToExport: """ Extract the actuators positions at a given index in the scan as a DataToExport of DataActuators""" dte = DataToExport('scanner') self.scanner.current_scan_index = index if len(self.positions[index]) == len(self.actuators): for axis_index, pos in enumerate(self.positions[index]): dte.append(self._scanner.data_actuator_at(scan_index=index, axis_index=axis_index)) return dte
@property def axes_indexes(self): return self._scanner.axes_indexes @property def axes_unique(self): return self._scanner.axes_unique @property def distribution(self): return self._scanner.distribution
[docs] def set_scan(self): """Process the settings options to calculate the scan positions Returns ------- bool: True if the processed number of steps if **higher** than the configured number of steps """ oversteps = config('pymodaq', 'scan', 'steps_limit') if self._scanner.evaluate_steps() > oversteps: return True self._scanner.set_scan() self.settings.child('n_steps').setValue(self.n_steps) self.scanner_updated_signal.emit() return False
def update_from_scan_selector(self, scan_selector: Selector): self._scanner.update_from_scan_selector(scan_selector)
def main(): from pymodaq.utils.parameter import ParameterTree app = QtWidgets.QApplication(sys.argv) units = ['nm', 'kW', 'ms', '°C', ] class MoveMock: def __init__(self, ind: int = 0): self.title = f'act_{ind}_{units[ind]}' self.units = units[ind] actuators = [MoveMock(ind) for ind in range(len(units))] params = [{'title': 'Actuators', 'name': 'actuators', 'type': 'itemselect', 'value': dict(all_items=[act.title for act in actuators], selected=[]),'checkbox':True}, {'title': 'Set Scan', 'name': 'set_scan', 'type': 'action'}, ] settings = Parameter.create(name='settings', type='group', children=params) settings_tree = ParameterTree() settings_tree.setParameters(settings) widget_main = QtWidgets.QWidget() widget_main.setLayout(QtWidgets.QHBoxLayout()) #widget_main.layout().setContentsMargins(0, 0, 0, 0) widget_scanner = QtWidgets.QWidget() widget_main.layout().addWidget(settings_tree) widget_main.layout().addWidget(widget_scanner) scanner = Scanner(widget_scanner, actuators=actuators) scanner.settings.child('actuators').show() def update_actuators(param): scanner.actuators = [utils.find_objects_in_list_from_attr_name_val(actuators, 'title', act_str, return_first=True)[0] for act_str in param.value()['selected']] def print_info(): print('info:') print(scanner.get_scan_info()) print('positions:') print(scanner.positions) print('nav:') print(scanner.get_nav_axes()) settings.child('actuators').sigValueChanged.connect(update_actuators) settings.child('set_scan').sigActivated.connect(scanner.set_scan) scanner.scanner_updated_signal.connect(print_info) widget_main.show() sys.exit(app.exec()) if __name__ == '__main__': import sys from qtpy import QtWidgets main()