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