6.4. Extending the Sequencer
This page describes how the Sequencer extension works under the hood, and how to write your own choice models and elements. The classes involved are described in the API section, see The Sequencer Extension.
6.4.1. How it works
Each element derives from the SeqEltBase class
(pymodaq.extensions.sequencer.utilities.element_factory) and owns a CompositeState (its mstate
attribute), a Qt state made of three sub-states, see Fig. 6.6:
the
execute_state: when entered, the elementexecutemethod is calledthe
children_state: contains the composite states of the element’s children (for container elements)the
done_state: a final state, ending this element
The element drives its state through Qt signals:
done_signal: the element has finished, the sequence moves on to the next elementchildren_signal: the children of a container element should be executed. Once the last child has finished, theexecute_stateis entered again so that the container decides whether to loop again (emittingchildren_signal) or to finish (emittingdone_signal)go_to_signal: used by the Choice element to jump to its True or False targetdata_to_log_signal: emitted with the data to be logged (use thesave_datamethod)
When a sequence is started, Sequence.recursive_connect_elts connects the composite states of all elements: the end
of an element leads to the next sibling, or back to its parent’s execute_state for the last child. The Pause and
Stop actions are also connected to each element’s state.
Fig. 6.6 The composite state of an element and its transitions.
Each element is serialized in the .seq files using its to_dict method: the elt_name and id common
fields, completed by the element specific fields returned by its to_dict_custom method (see
Sequence files (.seq)). The SeqEltFactory is then used to recreate the element from its elt_name when
loading a file.
6.4.2. Writing a custom choice model
A choice model is a class deriving from ChoiceModelBase
(pymodaq.extensions.sequencer.utilities.choice_models.model) and registered with the
ChoiceModelFactory.register_choice decorator. It is a ParameterManager: its params class attribute
defines the settings displayed in the Choice element editor. The model has access to the Choice element
(parent_elt) and to a ModulesManager holding the Dashboard modules (modules_manager).
The execute method is called when the Choice element is executed. It must end by emitting the
go_to_signal of the parent element with a boolean.
Below is an example of a model randomly choosing the True target with a given probability:
import random
from typing import Any
from pymodaq_data import DataToExport
from pymodaq.extensions.sequencer.utilities.choice_models.factory import ChoiceModelFactory
from pymodaq.extensions.sequencer.utilities.choice_models.model import ChoiceModelBase
from pymodaq.extensions.sequencer.utilities.element_factory import ElementError
@ChoiceModelFactory.register_choice()
class RandomChoiceModel(ChoiceModelBase):
model_name = 'random' # the name displayed in the Choice element editor
params = [
{'title': 'Probability of True:', 'name': 'probability', 'type': 'float',
'value': 0.5, 'min': 0., 'max': 1.},
]
def execute(self, dte: DataToExport):
self.parent_elt.go_to_signal.emit(random.random() < self.settings['probability'])
def check_set_is_valid(self):
if not 0 <= self.settings['probability'] <= 1:
raise ElementError(f'Element {self.parent_elt}: the probability should be within [0, 1]')
def to_dict(self) -> dict[str, Any]:
return {'probability': self.settings['probability']}
def from_dict(self, dict_config: dict[str, Any]):
self.settings['probability'] = dict_config.pop('probability')
The methods you may reimplement are:
execute(mandatory): evaluates the condition and emitsself.parent_elt.go_to_signalwithTrueorFalsecheck_set_is_valid: called before the sequence starts, raise anElementErrorif the settings are not validto_dict/from_dict: save and restore the model settings in the.seqfiles. The returned keys are added to the fields of the Choice elementupdated_module_manager: called when the Dashboard modules change (a new experiment is applied), for instance to update a list of detectors in the settings, see thethresholdmodel
Note
For a model to be “seen” by PyMoDAQ either place it in the pymodaq.extensions.sequencer.models module or in the
models module/folder of a PyMoDAQ plugin declaring the pymodaq.models entry point.
6.4.3. Writing a custom element
An element is a class deriving from SeqEltBase and registered with both the SeqEltFactory.register_elt and
SerializableFactory.register_decorator decorators. Below is an example of an element writing a message in the
PyMoDAQ log:
from typing import Any
from qtpy import QtWidgets
from serializall import SerializableFactory
from pymodaq_data import DataToExport
from pymodaq_utils.logger import set_logger, get_module_name
from pymodaq.extensions.sequencer.utilities.element_factory import SeqEltBase, SeqEltFactory, ElementError
from pymodaq.extensions.sequencer.utilities.widget_with_toolbar import WidgetWithToolbar
logger = set_logger(get_module_name(__file__))
@SerializableFactory.register_decorator()
@SeqEltFactory.register_elt()
class MessageElt(SeqEltBase):
elt_name = 'message' # unique name, displayed in the Add Element menus
children_allowed = False # True for a container element
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.message: str = ''
def _create_widget(self, base_widget: WidgetWithToolbar) -> WidgetWithToolbar:
line_edit = QtWidgets.QLineEdit(self.message, parent=base_widget)
line_edit.textChanged.connect(self.set_message)
base_widget.add_widget_top(line_edit)
base_widget.give_focus_to(line_edit)
return base_widget
def set_message(self, message: str):
self.message = message
def _execute(self, dte: DataToExport = None):
logger.info(self.message)
self.done_signal.emit() # mandatory, otherwise the sequence will hang here
def check_set_is_valid(self):
if self.message == '':
raise ElementError(f'Element {self}: the message is empty')
def to_dict_custom(self) -> dict[str, Any]:
return {'message': self.message}
def from_dict_custom(self, dict_config: dict[str, Any]):
self.message = dict_config.pop('message')
def _eq(self, other: 'MessageElt') -> bool:
return self.message == other.message
def __repr__(self):
return f'{super().__repr__()} - {self.message}'
The class attributes and methods to define are:
elt_name: the unique name of the element, also used as theelt_namefield in the.seqfileschildren_allowed:Truefor a container element_create_widget: adds the widgets allowing to edit the element on the base widget (aWidgetWithToolbaralready containing the id, the name and the Execute button)_execute: performs the action. It must eventually emitdone_signal(orchildren_signalto execute the children of a container, see theRepeatEltclass). Long actions should be asynchronous (signals, callbacks, timers) so the GUI is not blocked: thedone_signalis then emitted later, as in theWaitEltclasscheck_set_is_valid: called before the sequence starts, raise anElementErrorif the element is not validto_dict_custom/from_dict_custom: save and restore the element configuration in the.seqfiles_eq: tests if two elements have the same configuration
and optionally:
initialize_element: called each time the element is entered from outside (not when looping over its children). Used to reset loop countersdo_things_with_dashboard: called once the element has access to the Dashboard (self.dashboard)_save_data: custom processing of the data passed tosave_data, which also emits them for loggingsize_hint: the size of the editor popup__repr__: the summary displayed in the tree
Note
The elements are registered when their module is imported. The Sequencer automatically imports all the modules of
the pymodaq.extensions.sequencer.utilities.elements package. An element defined elsewhere (in a plugin for
instance) is not discovered automatically: its module has to be imported before the Sequencer is started.