diff --git a/src/pymodaq_plugins_datamixer/app/custom_app_template.py b/src/pymodaq_plugins_datamixer/app/custom_app_template.py deleted file mode 100644 index ec46ad8..0000000 --- a/src/pymodaq_plugins_datamixer/app/custom_app_template.py +++ /dev/null @@ -1,125 +0,0 @@ -from qtpy import QtWidgets - -from pymodaq_gui import utils as gutils -from pymodaq_utils.config import Config -from pymodaq_utils.logger import set_logger, get_module_name - -# todo: replace here *pymodaq_plugins_template* by your plugin package name -from pymodaq_plugins_template.utils import Config as PluginConfig - -logger = set_logger(get_module_name(__file__)) - -main_config = Config() -plugin_config = PluginConfig() - - -# todo: modify the name of this class to reflect its application and change the name in the main -# method at the end of the script -class CustomAppTemplate(gutils.CustomApp): - - # todo: if you wish to create custom Parameter and corresponding widgets. These will be - # automatically added as children of self.settings. Morevover, the self.settings_tree will - # render the widgets in a Qtree. If you wish to see it in your app, add is into a Dock - params = [] - - def __init__(self, parent: gutils.DockArea): - super().__init__(parent) - - self.setup_ui() - - def setup_docks(self): - """Mandatory method to be subclassed to setup the docks layout - - Examples - -------- - >>>self.docks['ADock'] = gutils.Dock('ADock name') - >>>self.dockarea.addDock(self.docks['ADock']) - >>>self.docks['AnotherDock'] = gutils.Dock('AnotherDock name') - >>>self.dockarea.addDock(self.docks['AnotherDock'''], 'bottom', self.docks['ADock']) - - See Also - -------- - pyqtgraph.dockarea.Dock - """ - # todo: create docks and add them here to hold your widgets - # reminder, the attribute self.settings_tree will render the widgets in a Qtree. - # If you wish to see it in your app, add is into a Dock - raise NotImplementedError - - def setup_actions(self): - """Method where to create actions to be subclassed. Mandatory - - Examples - -------- - >>> self.add_action('quit', 'Quit', 'close2', "Quit program") - >>> self.add_action('grab', 'Grab', 'camera', "Grab from camera", checkable=True) - >>> self.add_action('load', 'Load', 'Open', "Load target file (.h5, .png, .jpg) or data from camera" - , checkable=False) - >>> self.add_action('save', 'Save', 'SaveAs', "Save current data", checkable=False) - - See Also - -------- - ActionManager.add_action - """ - raise NotImplementedError(f'You have to define actions here') - - def connect_things(self): - """Connect actions and/or other widgets signal to methods""" - raise NotImplementedError - - def setup_menu(self): - """Non mandatory method to be subclassed in order to create a menubar - - create menu for actions contained into the self._actions, for instance: - - Examples - -------- - >>>file_menu = self.mainwindow.menuBar().addMenu('File') - >>>self.affect_to('load', file_menu) - >>>self.affect_to('save', file_menu) - - >>>file_menu.addSeparator() - >>>self.affect_to('quit', file_menu) - - See Also - -------- - pymodaq.utils.managers.action_manager.ActionManager - """ - # todo create and populate menu using actions defined above in self.setup_actions - pass - - def value_changed(self, param): - """ Actions to perform when one of the param's value in self.settings is changed from the - user interface - - For instance: - if param.name() == 'do_something': - if param.value(): - print('Do something') - self.settings.child('main_settings', 'something_done').setValue(False) - - Parameters - ---------- - param: (Parameter) the parameter whose value just changed - """ - pass - - -def main(): - from pymodaq.utils.gui_utils.utils import mkQApp - app = mkQApp('CustomApp') - - mainwindow = QtWidgets.QMainWindow() - dockarea = gutils.DockArea() - mainwindow.setCentralWidget(dockarea) - - # todo: change the name here to be the same as your app class - prog = CustomAppTemplate(dockarea) - - mainwindow.show() - - app.exec() - - -if __name__ == '__main__': - main() diff --git a/src/pymodaq_plugins_datamixer/extensions/data_mixer.py b/src/pymodaq_plugins_datamixer/extensions/data_mixer.py index d21f4d0..6707856 100644 --- a/src/pymodaq_plugins_datamixer/extensions/data_mixer.py +++ b/src/pymodaq_plugins_datamixer/extensions/data_mixer.py @@ -1,20 +1,24 @@ from qtpy import QtWidgets, QtCore import numpy as np +from typing import Optional + from pymodaq_gui import utils as gutils from pymodaq_utils.config import Config, ConfigError from pymodaq_utils.logger import set_logger, get_module_name +from pymodaq_utils.utils import find_dict_in_list_from_key_val from pymodaq_data.data import DataToExport, DataWithAxes from pymodaq.utils.config import get_set_preset_path from pymodaq.extensions.utils import CustomExt -from pymodaq_gui.parameter.pymodaq_ptypes.itemselect import ItemSelect + from pymodaq_gui.plotting.data_viewers.viewer import ViewerDispatcher +from pymodaq_gui.utils.widgets.qled import QLED +from pymodaq_gui.parameter import utils as putils from pymodaq_plugins_datamixer.utils import Config as PluginConfig -from pymodaq_plugins_datamixer.extensions.utils.parser import ( - extract_data_names, split_formulae, replace_names_in_formula) from pymodaq.control_modules.utils import DAQTypesEnum +from pymodaq_plugins_datamixer.extensions.utils.model import get_models, DataMixerModel logger = set_logger(get_module_name(__file__)) @@ -28,19 +32,37 @@ class DataMixer(CustomExt): settings_name = 'DataMixerSettings' - params = [{'title': 'Edit Formula:', 'name': 'edit_formula', 'type': 'text', 'value': ''}] + models = get_models() + params = [ + {'title': 'Models', 'name': 'models', 'type': 'group', 'expanded': True, 'visible': True, + 'children': [ + {'title': 'Models class:', 'name': 'model_class', 'type': 'list', + 'limits': [d['name'] for d in models]}, + {'title': 'Ini Model', 'name': 'ini_model', 'type': 'action', }, + {'title': 'Model params:', 'name': 'model_params', 'type': 'group', 'children': []}, + + ]}] + dte_computed_signal = QtCore.Signal(DataToExport) def __init__(self, parent: gutils.DockArea, dashboard): super().__init__(parent, dashboard) - self.data0D_list_widget = ItemSelect() - self.data1D_list_widget = ItemSelect() - self.data2D_list_widget = ItemSelect() - self.dataND_list_widget = ItemSelect() + self.model_class: Optional[DataMixerModel] = None self.setup_ui() + self.settings.child('models', 'ini_model').sigActivated.connect( + self.get_action('ini_model').trigger) + + def get_set_model_params(self, model_name): + self.settings.child('models', 'model_params').clearChildren() + if len(self.models) > 0: + model_class = find_dict_in_list_from_key_val(self.models, 'name', model_name)['class'] + params = getattr(model_class, 'params') + self.settings.child('models', 'model_params').addChildren(params) + + def setup_docks(self): """Mandatory method to be subclassed to setup the docks layout @@ -61,20 +83,6 @@ def setup_docks(self): splitter.addWidget(self.settings_tree) - self.docks['data'] = gutils.Dock('Data List') - self.dockarea.addDock(self.docks['data'], 'right') - widget_data = QtWidgets.QWidget() - widget_data.setLayout(QtWidgets.QVBoxLayout()) - self.docks['data'].addWidget(widget_data) - widget_data.layout().addWidget(QtWidgets.QLabel('Data0D:')) - widget_data.layout().addWidget(self.data0D_list_widget) - widget_data.layout().addWidget(QtWidgets.QLabel('Data1D:')) - widget_data.layout().addWidget(self.data1D_list_widget) - widget_data.layout().addWidget(QtWidgets.QLabel('Data2D:')) - widget_data.layout().addWidget(self.data2D_list_widget) - widget_data.layout().addWidget(QtWidgets.QLabel('DataND:')) - widget_data.layout().addWidget(self.dataND_list_widget) - self.docks['computed'] = gutils.Dock('Computed data') self.dockarea.addDock(self.docks['computed'], 'right') @@ -83,15 +91,16 @@ def setup_docks(self): self.dte_computed_viewer = ViewerDispatcher(self.area_computed) + if len(self.models) != 0: + self.get_set_model_params(self.models[0]['name']) + def setup_actions(self): """Method where to create actions to be subclassed. Mandatory + """ self.add_action('quit', 'Quit', 'close2', "Quit program") - self.add_action('get_data', 'Get Data List', 'properties', - "Get the list of data from selected detectors") - self.add_action('compute', 'Compute Formulae', 'algo', - 'Compute the Formula when new data is available', - checkable=True) + self.add_action('ini_model', 'Init Model', 'ini') + self.add_widget('model_led', QLED, toolbar=self.toolbar) self.add_action('snap', 'Snap Detectors', 'snap', 'Snap all selected detectors') self.add_action('create_computed_detectors', 'Create Computed Detectors', 'Add_Step', @@ -100,13 +109,66 @@ def setup_actions(self): def connect_things(self): """Connect actions and/or other widgets signal to methods""" self.connect_action('quit', self.quit) - self.connect_action('get_data', self.show_data_list) + self.connect_action('ini_model', self.ini_model) self.modules_manager.det_done_signal.connect(self.process_data) - self.dte_computed_signal.connect(self.plot_formulae_results) + self.dte_computed_signal.connect(self.plot_computed_results) self.connect_action('snap', self.snap) self.modules_manager.detectors_changed.connect(self.update_connect_detectors) self.connect_action('create_computed_detectors', self.create_computed_detectors) + def process_data(self, dte: DataToExport): + if self.model_class is not None: + dte_computed = self.model_class.process_dte(dte) + self.dte_computed_signal.emit(dte_computed) + + def snap(self): + self.modules_manager.grab_data() + + def create_computed_detectors(self): + # Now that we have the module manager, load PID if it is checked in managers + try: + self.dashboard.add_det_from_extension('DataMixer', 'DAQ0D', 'DataMixer', self) + self.set_action_enabled('create_computed_detectors', False) + except Exception as e: + logger.exception(str(e)) + pass + + def update_connect_detectors(self): + try: + self.connect_detectors(False) + except : + pass + self.connect_detectors() + + def connect_detectors(self, connect=True): + """Connect detectors to DAQ_Logging do_save_continuous method + + Parameters + ---------- + connect: bool + If True make the connection else disconnect + """ + self.modules_manager.connect_detectors(connect=connect) + + def plot_computed_results(self, dte): + self.dte_computed_viewer.show_data(dte) + + def ini_model(self): + if self.model_class is None: + self.set_model() + + self.get_action('model_led').set_as_true() + self.set_action_enabled('ini_model', False) + self.settings.child('models', 'ini_model').setValue(True) + + self.update_connect_detectors() + + def set_model(self): + model_name = self.settings.child('models', 'model_class').value() + self.model_class = find_dict_in_list_from_key_val( + self.models, 'name', model_name)['class'](self) + self.model_class.ini_model_base() + def setup_menu(self): """Non mandatory method to be subclassed in order to create a menubar @@ -142,98 +204,15 @@ def value_changed(self, param): ---------- param: (Parameter) the parameter whose value just changed """ - pass + if param.name() == 'model_class': + self.get_set_model_params(param.value()) + elif param.name() in putils.iter_children(self.settings.child('models', 'model_params'), []): + if self.model_class is not None: + self.model_class.update_settings(param) def quit(self): self.mainwindow.close() - def snap(self): - self.modules_manager.grab_data() - - def create_computed_detectors(self): - # Now that we have the module manager, load PID if it is checked in managers - try: - detector_modules = [] - self.dashboard.add_det('DataMixer', None, [], [], detector_modules, - plug_type=DAQTypesEnum.DAQ0D.name, - plug_subtype='DataMixer') - self.dashboard.add_det_from_extension('DataMixer', 'DAQ0D', 'DataMixer', self) - self.set_action_enabled('create_computed_detectors', False) - except Exception as e: - logger.exception(str(e)) - pass - - def update_connect_detectors(self): - try: - self.connect_detectors(False) - except : - pass - self.connect_detectors() - - def connect_detectors(self, connect=True): - """Connect detectors to DAQ_Logging do_save_continuous method - - Parameters - ---------- - connect: bool - If True make the connection else disconnect - """ - self.modules_manager.connect_detectors(connect=connect) - - def plot_formulae_results(self, dte): - self.dte_computed_viewer.show_data(dte) - - def process_data(self, dte: DataToExport): - if self.is_action_checked('compute'): - formulae = split_formulae(self.get_formulae()) - dte_processed = DataToExport('Computed') - for ind, formula in enumerate(formulae): - try: - dwa = self.compute_formula(formula, dte, - name=f'Formula_{ind:03.0f}') - dte_processed.append(dwa) - except Exception as e: - pass - self.dte_computed_signal.emit(dte_processed) - - def compute_formula(self, formula: str, dte: DataToExport, - name: str) -> DataWithAxes: - """ Compute the operations in formula using data stored in dte - - Parameters - ---------- - formula: str - The mathematical formula using numpy and data fullnames within curly brackets - dte: DataToExport - name: str - The name to give to the produced DataWithAxes - - Returns - ------- - DataWithAxes: the results of the formula computation - """ - formula_to_eval, names = replace_names_in_formula(formula) - dwa = eval(formula_to_eval) - dwa.name = name - return dwa - - def get_formulae(self) -> str: - """ Read the content of the formula QTextEdit widget""" - return self.settings['edit_formula'] - - def show_data_list(self): - dte = self.modules_manager.get_det_data_list() - - data_list0D = dte.get_full_names('data0D') - data_list1D = dte.get_full_names('data1D') - data_list2D = dte.get_full_names('data2D') - data_listND = dte.get_full_names('dataND') - - self.data0D_list_widget.set_value(dict(all_items=data_list0D, selected=[])) - self.data1D_list_widget.set_value(dict(all_items=data_list1D, selected=[])) - self.data2D_list_widget.set_value(dict(all_items=data_list2D, selected=[])) - self.dataND_list_widget.set_value(dict(all_items=data_listND, selected=[])) - def main(): from pymodaq_gui.utils.utils import mkQApp diff --git a/src/pymodaq_plugins_datamixer/extensions/utils/model.py b/src/pymodaq_plugins_datamixer/extensions/utils/model.py new file mode 100644 index 0000000..0e4a888 --- /dev/null +++ b/src/pymodaq_plugins_datamixer/extensions/utils/model.py @@ -0,0 +1,94 @@ +from typing import List, TYPE_CHECKING +import importlib +import inspect +import pkgutil +import warnings +from pathlib import Path +from typing import Union, List + +import numpy as np # to be imported within models + +from pymodaq_utils.utils import find_dict_in_list_from_key_val, get_entrypoints +from pymodaq_utils.logger import set_logger, get_module_name + +from pymodaq_data.data import DataToExport + +from pymodaq_gui.managers.parameter_manager import ParameterManager, Parameter + +logger = set_logger(get_module_name(__file__)) + +if TYPE_CHECKING: + from pymodaq_plugins_datamixer.extensions.data_mixer import DataMixer + from pymodaq.utils.managers.modules_manager import ModulesManager + + +class DataMixerModel: + + detectors_name: List[str] = [] + params = [] + + def __init__(self, data_mixer: 'DataMixer'): + self.data_mixer = data_mixer + self.modules_manager: ModulesManager = data_mixer.dashboard.modules_manager + self.settings: Parameter = self.data_mixer.settings.child('models', 'model_params') + + def ini_model_base(self): + """ Method to add things that should be executed before instantiating the model""" + self.ini_model() + + def ini_model(self): + pass + + def update_settings(self, param: Parameter): + pass + + def process_dte(self, measurements: DataToExport) -> DataToExport: + """ + Convert the measurements in the units to be fed to the PID (same dimensionality as the setpoint) + Parameters + ---------- + measurements: DataToExport + DataToExport object from which the model extract a value of the same units as the setpoint + + Returns + ------- + DataToExport: the converted input as 0D DataCalculated stored in a DataToExport + """ + raise NotImplementedError + + +def get_models(model_name=None): + """ + Get DataMixer Models + + Returns + ------- + list: list of dict containting the name and python module of the found models + """ + models_import = [] + discovered_models = list(get_entrypoints(group='pymodaq.models')) + if len(discovered_models) > 0: + for pkg in discovered_models: + try: + module = importlib.import_module(pkg.value) + module_name = pkg.value + + for mod in pkgutil.iter_modules([str(Path(module.__file__).parent.joinpath('models'))]): + try: + model_module = importlib.import_module(f'{module_name}.models.{mod.name}', module) + classes = inspect.getmembers(model_module, inspect.isclass) + for name, klass in classes: + if klass.__base__ is DataMixerModel: + models_import.append({'name': mod.name, 'module': model_module, 'class': klass}) + break + + except Exception as e: # pragma: no cover + logger.warning(str(e)) + + except Exception as e: # pragma: no cover + logger.warning(f'Impossible to import the {pkg.value} extension: {str(e)}') + + if model_name is None: + return models_import + else: + return find_dict_in_list_from_key_val(models_import, 'name', model_name) diff --git a/src/pymodaq_plugins_datamixer/models/equation_model.py b/src/pymodaq_plugins_datamixer/models/equation_model.py new file mode 100644 index 0000000..45a16d8 --- /dev/null +++ b/src/pymodaq_plugins_datamixer/models/equation_model.py @@ -0,0 +1,81 @@ +from pymodaq_plugins_datamixer.extensions.utils.model import DataMixerModel, np # np will be used in method eval of the formula + +from pymodaq_data.data import DataToExport, DataWithAxes +from pymodaq_gui.parameter import Parameter + +from pymodaq_plugins_datamixer.extensions.utils.parser import ( + extract_data_names, split_formulae, replace_names_in_formula) + + +class DataMixerModelEquation(DataMixerModel): + params = [ + {'title': 'Get Data:', 'name': 'get_data', 'type': 'bool_push', 'value': False, + 'label': 'Get Data'}, + {'title': 'Edit Formula:', 'name': 'edit_formula', 'type': 'text', 'value': ''}, + {'title': 'Data0D:', 'name': 'data0D', 'type': 'itemselect', + 'value': dict(all_items=[], selected=[])}, + {'title': 'Data1D:', 'name': 'data1D', 'type': 'itemselect', + 'value': dict(all_items=[], selected=[])}, + {'title': 'Data2D:', 'name': 'data2D', 'type': 'itemselect', + 'value': dict(all_items=[], selected=[])}, + {'title': 'DataND:', 'name': 'dataND', 'type': 'itemselect', + 'value': dict(all_items=[], selected=[])}, + ] + + def ini_model(self): + self.show_data_list() + + def update_settings(self, param: Parameter): + if param.name() == 'get_data': + self.show_data_list() + + def get_formulae(self) -> str: + """ Read the content of the formula QTextEdit widget""" + return self.settings['edit_formula'] + + def show_data_list(self): + dte = self.modules_manager.get_det_data_list() + + data_list0D = dte.get_full_names('data0D') + data_list1D = dte.get_full_names('data1D') + data_list2D = dte.get_full_names('data2D') + data_listND = dte.get_full_names('dataND') + + self.settings.child('data0D').setValue(dict(all_items=data_list0D, selected=[])) + self.settings.child('data1D').setValue(dict(all_items=data_list1D, selected=[])) + self.settings.child('data2D').setValue(dict(all_items=data_list2D, selected=[])) + self.settings.child('dataND').setValue(dict(all_items=data_listND, selected=[])) + + def process_dte(self, dte: DataToExport): + formulae = split_formulae(self.get_formulae()) + dte_processed = DataToExport('Computed') + for ind, formula in enumerate(formulae): + try: + dwa = self.compute_formula(formula, dte, + name=f'Formula_{ind:03.0f}') + dte_processed.append(dwa) + except Exception as e: + pass + return dte_processed + + def compute_formula(self, formula: str, dte: DataToExport, + name: str) -> DataWithAxes: + """ Compute the operations in formula using data stored in dte + + Parameters + ---------- + formula: str + The mathematical formula using numpy and data fullnames within curly brackets + dte: DataToExport + name: str + The name to give to the produced DataWithAxes + + Returns + ------- + DataWithAxes: the results of the formula computation + """ + formula_to_eval, names = replace_names_in_formula(formula) + dwa = eval(formula_to_eval) + dwa.name = name + return dwa + diff --git a/src/pymodaq_plugins_datamixer/models/fit_model.py b/src/pymodaq_plugins_datamixer/models/fit_model.py new file mode 100644 index 0000000..8088957 --- /dev/null +++ b/src/pymodaq_plugins_datamixer/models/fit_model.py @@ -0,0 +1,48 @@ +import numpy as np + +from pymodaq_plugins_datamixer.extensions.utils.model import DataMixerModel, np # np will be used in method eval of the formula + +from pymodaq_utils.math_utils import gauss1D, my_moment + +from pymodaq_data.data import DataToExport, DataWithAxes +from pymodaq_gui.parameter import Parameter + +from pymodaq_plugins_datamixer.extensions.utils.parser import ( + extract_data_names, split_formulae, replace_names_in_formula) + + +def gaussian_fit(x, amp, x0, dx, offset): + return amp * gauss1D(x, x0, dx) + offset + + +class DataMixerModelFit(DataMixerModel): + params = [ + + ] + + def ini_model(self): + pass + + def update_settings(self, param: Parameter): + pass + + def process_dte(self, dte: DataToExport): + dte_processed = DataToExport('computed') + dwa = dte.get_data_from_full_name('Spectrum - ROI_00/Hlineout_ROI_00').deepcopy() + + dte_processed.append(dwa) + dte_processed.append(dwa.fit(gaussian_fit, self.get_guess(dwa))) + + return dte_processed + + @staticmethod + def get_guess(dwa): + offset = np.min(dwa).value() + moments = my_moment(dwa.axes[0].get_data(), dwa.data[0]) + amp = (np.max(dwa) - np.min(dwa)).value() + x0 = float(moments[0]) + dx = float(moments[1]) + + return amp, x0, dx, offset + +