1. Pulse sequences and shapes¶
Synthesis of pulse sequences in Exopy is centered around the notion of sequence and pulse. Those two elements are used describe the sequence to synthetyse, while the actual synthesis is actually carried out by the context.
The following sections will present how to contribute new shapes for analogical pulses, new sequences and new configuration objects for sequences. The case of context will be treated in detail in another setion.
Contents
1.1. Object with evaluable fields¶
Sequence, pulses, shapes and contexts can all have formula fileds that can be either formatted or formatted and evaluated. In those fields, variables such as the start, stop, and duration of any pulse can be referenced in between curly brackets.
The handling of the evaluation is automated by their common base class HasEvaluableFields
.
All members tagged with either ‘fmt’ or ‘feval’ will be automatically formatted/formatted and
evaluated when the eval_entries method is called. In case of success, the result will be
stored in the _cache dictionary of the object. For formatted values, the value of the
‘fmt’ metadata should be either True or False depending on whether the
formatted value should be stored in the global variables avalaible to other object
when evaluating their entries or not. For feval values, the value of the ‘feval’
metadata should be a Feval
instance (which is reminiscent of the system used
for tasks). Feval
can be subclassed to customize when to perform the evaluation
and how to check the resulting value. By default, a tuple of types can be
specified. For values which should be stored as globals, the store_global
member should be set to True.
Note
Shapes and contexts cannot store values in the global namespace.
Note
Even if the name is identical to the objects used for tasks, the Feval object sused in both cases are different. For pulses related object, it should be imported from exopy_pulses.pulses.api.
1.2. Creating a new shape¶
Creating a new shape is a three step process :
first the shape itself which holds the logic must be created.
to allow a user to correctly parametrize the shape a dedicated widget or view should also be created.
finally the shape must be declared in the manifest of the plugin contributing it.
1.2.1. Implementing the logic¶
The shape itself should be a subclass of AbstractShape
.
The shape parameters should be declared using the appropriate member and tagged with ‘pref’ in order to be correctly saved. If the default way of saving/restoring (repr/literal_eval) is enough simply use True as a value otherwise you can specify the function to use to serialize/desarialize should be passed as a tuple/list.
from numbers import Real
from atom.api import Unicode, Int
from exopy_pulses.pulses.api import Feval
class MyShape(AbstractShape):
"""MyShape description.
Use Numpy style docstrings.
"""
#: my_int description
my_int = Int(1).tag(pref=True) # Integer with a default value of 1
#: my_formula description
my_formula = Unicode().tag(pref=True, feval=Feval(types=Real))
You will also need to implement one method :
compute : this method should evaluate and return the shape of the pulse at the provided times (taking into account the unit in which the time is expressed). It should return an array. As mentioned before values computed by eval_entries can be found in the _cache dictionary.
1.2.2. Creating the view¶
All shape views should inherit from AbstractShapeView
which is nothing more than
a customized SplitItem (Which it should have a single Container child). The view
will always have a reference to the shape it is used to edit under shape and
to the pulse using the shape item. From there you are free to design your UI the
way you want.
To edit member corresponding to formulas with access to the sequence variables,
note that the QtLineCompleter
and QtTextCompleter
widgets give
auto-completion for the sequence variables after a ‘{’. You need to set the
entries_updater attribute to item.parent.get_accessible_vars. If you do
so you may also want to use EVALUATER_TOOLTIP
as a tool tip (tool_tip member)
so that your user get a nice explanation about what he can and cannot write in
this field. From a general point of view it is a good idea to provide
meaningful tool tips.
enamldef MyShapeView(AbstractShapeView):
QtLineCompleter:
text := shape.my_formula
entries_updater = item.parent.get_accessible_vars
tool_tip = EVALUATER_TOOLTIP
For more informations about the Enaml syntax please give a look at the relevant section in the Exopy documentation.
At this point your shape is ready to be registered in Exopy, however writing a bunch of unit tests for your shape making sure it works as expected and will go on doing so is good idea. Give a look at ExopyPulses tests for more details about writing tests and checking that your tests do cover all the possible cases.
1.2.3. Registering your shape¶
The last thing you need to do is to declare your shape in a plugin manifest so
that the main application can find it. To do so your plugin should contribute
an extension to ‘exopy.pulses.shapes’ providing Shapes
and/or Shape
objects.
Let’s say we need to declare a single shape named ‘MyShape’. The name of our extension package (see the glossary section in Exopy documentation) is named ‘my_exopy_plugin’. Let’s look at the example below:
enamldef MyPluginManifest(PluginManifest):
id = 'my_plugin_id'
Extension:
point = 'exopy.pulses.shapes'
Shapes:
path = 'my_exopy_plugin'
Shape:
shape = 'my_shape:MyShape'
view = 'views.my_shape:MyView'
We declare a single child for the extension a Shapes
object. Shapes
does
nothing by themselves they are simply container for grouping shapes
declarations. They have a single attribute:
‘path’: when declaring a shape you must specify in which module it is defined as a ‘.’ sperated path. When declaring a path in a
Shapes
it will be prepended to any path-like declaration in all children.
We then declare our shape using a Shape
object. A Shape
has three attributes
but only two of them must be given non-default values :
‘shape’: this is the path (‘.’ separated) to the module defining the shape. The actual name of the shape is specified after a colon (‘:’). As mentioned above the path of all parent
Shapes
is preprended to this path.‘view’: this identic to the shape attribute but used for the view definition. Once again the path of all parent
Shapes
is preprended to this path.‘metadata’: Any additional informations about the shape. Those should be specified as a dictionary.
This is it. Now when starting Exopy your new shape should be listed.
1.3. Creating a new sequence¶
Creating a new sequence is very similar to creating a new shape and the same three steps exists :
first the sequence itself which holds the logic must be created.
to allow a user to correctly parametrize the sequence one view should also be created.
finally the sequence must be declared in the manifest of the plugin contributing it.
1.3.1. Minimal methods to implement¶
The sequence should be a subclass either of BaseSequence
or AbstractSequence
.
The second case is reserved to cases where a higher level of control over the
children item is required which should be quite uncommon.
The declaration of parameters is similar to the one used for shape. If the sequence wish to share a computed value with the other items, it should set the value of the linkable_vars member.
from numbers import Real
from atom.api import Unicode, Int
from exopy_pulses.pulses.api import Feval
class MySequence(BaseSequence):
"""MySequence description.
Use Numpy style docstrings.
"""
#: my_int description
my_int = Int(1).tag(pref=True) # Integer with a default value of 1
#: my_text description
my_val = Unicode().tag(pref=True, feval=Feval(store_global=True))
linkable_vars = set_default(['my_val'])
Sequences should at least implement the following method :
simplify_sequence: this method should a return a list of items that the context declares it can handle as declared in its supported_sequences member (pulses are implicitely handled).
1.3.2. Creating the view(s)¶
Just like for a shape, you need to provide a widget to edit the sequence
parameters. The view should subclass either BaseSequence
or AbstractSequenceView
.
AbstractSequenceView
is pretty muchh bare, while BaseSequence
handle the display
of the child items, the edition of the local variables and the possibility to fix
the duration of the sequence. In both cases, the view is a custom Groupbox which has a
reference to the edited sequence and to the root view which itself provide access to the
core plugin and several useful methods.
enamldef MySequenceView(BaseSequenceView):
- constraints << ([vbox(hbox(t_bool, cond_lab, cond_val),
hbox(*t_def.items), nb)]
if t_def.condition else [vbox(hbox(t_bool, cond_lab, cond_val), nb)])
- Label: cond_lab:
text = ‘Condition’
- QtLineCompleter: cond_val:
text := item.condition entries_updater = item.get_accessible_vars tool_tip = EVALUATER_TOOLTIP
1.3.3. Registering your sequence¶
Registering a sequence is quite similar to registering a shape.
Let’s say we need to declare a sequence named MySequence. The name of our extension package (see the glossary section in Exopy documentation) is ‘my_exopy_plugin’. Let’s look at the example below:
enamldef MyPluginManifest(PluginManifest):
id = 'my_plugin_id'
Extension:
point = 'exopy.pulses.sequences'
Sequences:
path = 'my_exopy_plugin'
Sequence:
sequence = 'my_sequence:MySequence'
view = 'views.my_sequence:MyView'
We declare a single child for the extension a Sequences
object. Sequences
does
nothing by themselves they are simply container for grouping sequences declarations.
They have a single attribute:
‘path’: when declaring a sequence you must specify in which module it is defined as a ‘.’ sperated path. When declaring a path in a
Sequences
it will be prepended to any path-like declaration in all children.
We then declare our sequence using a Sequence
object. A Sequence
has three attributes
but only two of them must be given non-default values :
‘sequence’: this is the path (‘.’ separated) to the module defining the sequence. The actual name of the sequence is specified after a colon (‘:’). As mentioned above the path of all parent
Sequences
is preprended to this path.‘view’: this identic to the sequence attribute but used for the view definition. Once again the path of all parent
Sequences
is preprended to this path.‘metadata’: Any additional informations about the sequence. Those should be specified as a dictionary.
This is it. Now when starting Exopy your new sequence should be listed.
1.4. Creating your own sequence filter¶
As the number of sequences available in Exopy grows, finding the sequence you need might become a bit tedious. To make searching through tasks easier Exopy can filter the sequences from which to choose from. A number a basic filters are built-in but one can easily add more.
To add a new filter you simply need to contribute a SequenceFilter
to the
‘exopy.pulses.filters’ extension point, as in the following example :
enamldef MyPluginManifest(PluginManifest):
id = 'my_plugin_id'
Extension:
point = 'exopy.pulses.filters'
SequenceFilter:
id = 'MySequenceFilter'
filter_tasks => (sequences, templates):
return sorted(sequences)[::2]
A filter need a unique id (basically its name) and a method to filter through sequences. This method receives two dictionaries: the first ones contains the known sequences and their associated infos, the second the templates names and their path. Here we override the filter_tasks method, we could also have used one of the following specialized filters:
SubclassSequenceFilter
: filter the sequences (exclude the templates) looking for a common subclass (declared in the subclass attribute)MetadataSequenceFilter
: filter the sequences (exclude the templates) based on the value of a metadata (meta_key is the metadata entry to look for, meta_value the value looked for).
1.5. Creating your own sequence configurer¶
In some cases, the default way to configure a sequence before inserting it in a sequence hierarchy (ie simply specifying its name) is not enough. The sequence configurers exist to make possible to customize the creation of a new sequence. Creating one is once again similar to creating a new shape.
Note
Sequence configurers are not meant to fully parametrize a sequence, the sequence view is already there for that purpose. It is rather meant to provide essential informations necessary before including the sequence in a hierarchy or parameters not meant to change afterwards.
Note
When a sequence configurer is specified for a sequence it is by default used form all its subclasses too.
1.5.1. Minimal methods to implement¶
All sequence configurers need to inherit from AbstractConfig
, which defines the
expected interface of all configurers. When creating a new configurer two
methods need to be overwritten :
build_sequence : this method is supposed to return when called a new instance of the sequence being configured correctly initialized. The configurer holds a refrence to the class of the sequence it is configuring.
check_parameters : this method should set the ready flag to True if all the parameters required by the configurer have been provided and False otherwise. It should be called each time the value of a parameter change (using a _post_settattr_* method).
class MySequenceConfig(AbstractConfig):
"""Config for MySequence.
"""
#: My parameter description
parameter = Int()
def check_parameters(self):
"""Ensure that parameter is positive and task has a name.
"""
self.ready = self.parameter
def build_task(self):
"""Build an instance of MySequence.
"""
return self.sequence_class(parameter=self.parameter)
def _post_setattr_parameter(self, old, new):
"""Check parameters each time parameter is updated.
"""
self.check_parameters()
1.5.2. Creating the view¶
Just like for tasks and interfaces, you need to create a custom widget to
allow the user to parametrize the configurer. Your widget should inherit from
AbstractConfigView
. This widget is simple container with a reference to the
configurer being edited (model)
1.5.3. Declaring the configurer¶
Finally you must declare the config in a manifest by contributing an
extension to the ‘exopy.pulses.configs’ extension point. This is identical to
how shapes are declared but relies on the SequenceConfigs
(instead of Shapes
) and
SequenceConfig
(instead of Shape
) objects. The base sequence class for which the
configurer is meant should be returned by the get_sequence_class method.