diff --git a/exopy_pulses/pulses/manifest.enaml b/exopy_pulses/pulses/manifest.enaml index dce3427..3439036 100644 --- a/exopy_pulses/pulses/manifest.enaml +++ b/exopy_pulses/pulses/manifest.enaml @@ -432,6 +432,12 @@ enamldef PulsesManagerManifest(PluginManifest): Shape: shape = 'slope_shape:SlopeShape' view = 'views.slope_shape_view:SlopeShapeView' + Shape: + shape = 'gaussian_shape:GaussianShape' + view = 'views.gaussian_shape_view:GaussianShapeView' + Shape: + shape = 'tanh_shape:TanhShape' + view = 'views.tanh_shape_view:TanhShapeView' PulsesBuildingDependenciesExtension: pass diff --git a/exopy_pulses/pulses/shapes/gaussian_shape.py b/exopy_pulses/pulses/shapes/gaussian_shape.py new file mode 100644 index 0000000..9136870 --- /dev/null +++ b/exopy_pulses/pulses/shapes/gaussian_shape.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2021 by ExopyPulses Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Most basic shape for an analogical pulse. + +""" +from numbers import Real + +import numpy as np +from atom.api import Str + +from ..utils.validators import Feval + +from .base_shape import AbstractShape + + +class GaussianShape(AbstractShape): + """ Gaussian pulse with a variable amplitude and sigma. + + """ + #: Amplitude of the pulse this should be a number between -1.0 and 1.0 + amplitude = Str('1.0').tag(pref=True, feval=Feval(types=Real)) + + #: Sigma of gaussian pulse, units are AWG context units + sigma = Str('10.0').tag(pref=True, feval=Feval(types=Real)) + + def eval_entries(self, root_vars, sequence_locals, missing, errors): + """ Evaluate the amplitude of the pulse. + + Parameters + ---------- + root_vars : dict + Global variables. As shapes and modulation cannot update them an + empty dict is passed. + + sequence_locals : dict + Known locals variables for the pulse sequence. + + missing : set + Set of variables missing to evaluate some entries in the sequence. + + errors : dict + Errors which occurred when trying to compile the pulse sequence. + + Returns + ------- + result : bool + Flag indicating whether or not the evaluation succeeded. + + """ + res = super(GaussianShape, self).eval_entries(root_vars, sequence_locals, + missing, errors) + + if res: + if not -1.0 <= self._cache['amplitude'] <= 1.0: + msg = 'Shape amplitude must be between -1 and 1.' + errors[self.format_error_id('amplitude')] = msg + res = False + + return res + + def compute(self, time, unit): + """ Computes the shape of the pulse at a given time. + + Parameters + ---------- + time : ndarray + Times at which to compute the modulation. + + unit : str + Unit in which the time is expressed. + + Returns + ------- + shape : ndarray + Amplitude of the pulse. + + """ + amp = self._cache['amplitude'] + sigma = self._cache['sigma'] + t0 = (time[0]+time[-1])/2 + pulse_shape = [amp*np.exp(-(t-t0)**2/2/sigma**2) for t in time] + return np.asarray(pulse_shape) diff --git a/exopy_pulses/pulses/shapes/tanh_shape.py b/exopy_pulses/pulses/shapes/tanh_shape.py new file mode 100644 index 0000000..dc45f4a --- /dev/null +++ b/exopy_pulses/pulses/shapes/tanh_shape.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2015-2021 by ExopyPulses Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Most basic shape for an analogical pulse. + +""" +from numbers import Real + +import numpy as np +from atom.api import Str + +from ..utils.validators import Feval + +from .base_shape import AbstractShape + + +class TanhShape(AbstractShape): + """ Tanh pulse with a variable amplitude and sigma. + + """ + #: Amplitude of the pulse this should be a number between -1.0 and 1.0 + amplitude = Str('1.0').tag(pref=True, feval=Feval(types=Real)) + + #: Sigma of tanh pulse, basically raising time, units are AWG context units + sigma = Str('10.0').tag(pref=True, feval=Feval(types=Real)) + + def eval_entries(self, root_vars, sequence_locals, missing, errors): + """ Evaluate the amplitude of the pulse. + + Parameters + ---------- + root_vars : dict + Global variables. As shapes and modulation cannot update them an + empty dict is passed. + + sequence_locals : dict + Known locals variables for the pulse sequence. + + missing : set + Set of variables missing to evaluate some entries in the sequence. + + errors : dict + Errors which occurred when trying to compile the pulse sequence. + + Returns + ------- + result : bool + Flag indicating whether or not the evaluation succeeded. + + """ + res = super(TanhShape, self).eval_entries(root_vars, sequence_locals, + missing, errors) + + if res: + if not -1.0 <= self._cache['amplitude'] <= 1.0: + msg = 'Shape amplitude must be between -1 and 1.' + errors[self.format_error_id('amplitude')] = msg + res = False + + return res + + def compute(self, time, unit): + """ Computes the shape of the pulse at a given time. + + Parameters + ---------- + time : ndarray + Times at which to compute the modulation. + + unit : str + Unit in which the time is expressed. + + Returns + ------- + shape : ndarray + Amplitude of the pulse. + + """ + amp = self._cache['amplitude'] + sigma = self._cache['sigma'] + t0 = (time[0]+time[-1])/2 + duration = time[-1]-time[0] + func = lambda t:(0.5+0.5*np.tanh((t+duration/2)/sigma*2*np.pi-np.pi)) + pulse_shape = [amp*func(t-t0)*func(-(t-t0)) for t in time] + return np.asarray(pulse_shape) diff --git a/exopy_pulses/pulses/shapes/views/gaussian_shape_view.enaml b/exopy_pulses/pulses/shapes/views/gaussian_shape_view.enaml new file mode 100644 index 0000000..2f0ace0 --- /dev/null +++ b/exopy_pulses/pulses/shapes/views/gaussian_shape_view.enaml @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2015-2021 by ExopyPulses Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""View for the Gaussian Shape. + +""" +from enaml.layout.api import hbox, vbox, align +from enaml.widgets.api import (Label, GroupBox) + +from exopy.utils.widgets.qt_completers import QtLineCompleter +from ...utils.entry_eval import EVALUATER_TOOLTIP + +from .base_shape_view import AbstractShapeView + +enamldef GaussianShapeView(AbstractShapeView): view: + """ View for a Gaussian pulse. + + """ + GroupBox: + title = 'Gaussian' + constraints = [hbox(amp_lab, amp_val, sigma_lab, sigma_val), + align('v_center', amp_lab, amp_val, sigma_lab, sigma_val)] + + Label: amp_lab: + text = 'Amplitude' + QtLineCompleter: amp_val: + text := shape.amplitude + entries_updater = item.parent.get_accessible_vars + tool_tip = ('Relative amplitude of the pulse (should be between ' + '-1.0 and 1.0)') + + Label: sigma_lab: + text = 'Sigma' + QtLineCompleter: sigma_val: + text := shape.sigma + entries_updater = item.parent.get_accessible_vars + tool_tip = ('Sigma of gaussian pulse, units are AWG context units') diff --git a/exopy_pulses/pulses/shapes/views/tanh_shape_view.enaml b/exopy_pulses/pulses/shapes/views/tanh_shape_view.enaml new file mode 100644 index 0000000..6e74e71 --- /dev/null +++ b/exopy_pulses/pulses/shapes/views/tanh_shape_view.enaml @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2015-2021 by ExopyPulses Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""View for the Gaussian Shape. + +""" +from enaml.layout.api import hbox, vbox, align +from enaml.widgets.api import (Label, GroupBox) + +from exopy.utils.widgets.qt_completers import QtLineCompleter +from ...utils.entry_eval import EVALUATER_TOOLTIP + +from .base_shape_view import AbstractShapeView + +enamldef TanhShapeView(AbstractShapeView): view: + """ View for a Tanh pulse. + + """ + GroupBox: + title = 'Tanh' + constraints = [hbox(amp_lab, amp_val, sigma_lab, sigma_val), + align('v_center', amp_lab, amp_val, sigma_lab, sigma_val)] + + Label: amp_lab: + text = 'Amplitude' + QtLineCompleter: amp_val: + text := shape.amplitude + entries_updater = item.parent.get_accessible_vars + tool_tip = ('Relative amplitude of the pulse (should be between ' + '-1.0 and 1.0)') + + Label: sigma_lab: + text = 'Sigma' + QtLineCompleter: sigma_val: + text := shape.sigma + entries_updater = item.parent.get_accessible_vars + tool_tip = ('Sigma of tanh pulse, basically raising time, units are AWG context units') diff --git a/exopy_pulses/tasks/manifest.enaml b/exopy_pulses/tasks/manifest.enaml index 685e16e..450bcb6 100644 --- a/exopy_pulses/tasks/manifest.enaml +++ b/exopy_pulses/tasks/manifest.enaml @@ -31,3 +31,11 @@ enamldef PulsesTasksManifest(PluginManifest): # Way to declare instrument dependencies without specifying # any instrument. instruments = [None] + path = 'exopy_pulses.tasks.tasks.instrs' + Task: + task = 'transfer_pulse_loop_task:TransferPulseLoopTask' + view = ('views.transfer_pulse_loop_task_view:' + 'TransferPulseLoopView') + # Way to declare instrument dependencies without specifying + # any instrument. + instruments = [None] diff --git a/exopy_pulses/tasks/tasks/instrs/transfer_pulse_loop_task.py b/exopy_pulses/tasks/tasks/instrs/transfer_pulse_loop_task.py new file mode 100644 index 0000000..bfd4489 --- /dev/null +++ b/exopy_pulses/tasks/tasks/instrs/transfer_pulse_loop_task.py @@ -0,0 +1,321 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2015-2021 by ExopyHqcLegacy Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Task to transfer a sequence on an AWG. + +""" +import os +from traceback import format_exc +from pprint import pformat +from collections import OrderedDict + +import numpy as np +from atom.api import (Value, Str, Float, Typed, Bool, set_default, Enum) + +from exopy.tasks.api import (InstrumentTask) +from exopy.utils.atom_util import ordered_dict_from_pref, ordered_dict_to_pref + + +class TransferPulseLoopTask(InstrumentTask): + """Build and transfer a pulse sequence to an instrument. + + """ + #: Sequence path for the case of sequence simply referenced. + sequence_path = Str().tag(pref=True) + + #: Time stamp of the last modification of the sequence file. + sequence_timestamp = Float().tag(pref=True) + + #: Sequence of pulse to compile and transfer to the instrument. + sequence = Value() + + #: Global variable to use for the sequence. + sequence_vars = Typed(OrderedDict, ()).tag(pref=(ordered_dict_to_pref, + ordered_dict_from_pref)) + + #: operation mode of the awg + operation = Enum( + 'Clean, Load & Enqueue', + 'Clean & Load', + 'Load', + 'Load & Enqueue all').tag( + pref=True) + + #: Loop variables: channels on which the loop will be done, loop parameters + #: names, start value, stop value and number of points per loop + + loop_start = Str('0').tag(pref=True) + + loop_stop = Str('0').tag(pref=True) + + loop_points = Str('2').tag(pref=True) + + #: run mode of the awg + run_mode = Enum('Ext Trig', 'Int Trig', 'Continuous').tag(pref=True) + + #: Internal trigger period in mus + trigger_period = Str('20').tag(pref=True) + + parameters = Typed(OrderedDict, ()).tag(pref=[ordered_dict_to_pref, + ordered_dict_from_pref]) + + database_entries = set_default({'num_loop': 1}) + + def check(self, *args, **kwargs): + """Check that the sequence can be compiled. + + """ + test, traceback = super(TransferPulseLoopTask, + self).check(*args, **kwargs) + err_path = self.path + '/' + self.name + '-' + + msg = 'Failed to evaluate {} ({}): {}' + seq = self.sequence + for k, v in self.sequence_vars.items(): + try: + seq.external_vars[k] = self.format_and_eval_string(v) + except Exception: + test = False + traceback[err_path + k] = msg.format(k, v, format_exc()) + + if not test: + return test, traceback + + context = seq.context + res, infos, errors = context.compile_and_transfer_sequence(seq) + + if not res: + traceback[err_path + 'compil'] = errors + return False, traceback + + for k, v in infos.items(): + self.write_in_database(k, v) + + if self.sequence_path: + if not (self.sequence_timestamp == + os.path.getmtime(self.sequence_path)): + msg = 'The sequence is outdated, consider refreshing it.' + traceback[err_path + 'outdated'] = msg + + return test, traceback + + def perform(self): + """Compile the sequence. + + Uses the following properties and methods from the driver: + - internal_trigger + - internal_trigger_period + - clear_all_sequences() + - delete_all_waveforms() + - get_waveform_number() + - get_waveform_name() + - run_mode + - get_channel() + - set_jump_pos() + - set_goto_pos() + + """ + operation = self.operation + seq = self.sequence + context = seq.context + if self.run_mode == 'Int Trig': + internal_trigger = True + else: + internal_trigger = False + + self.driver.internal_trigger = internal_trigger + if internal_trigger: + self.driver.internal_trigger_period = int( + float(self.trigger_period) * 1000) + + self.driver.clear_all_sequences() + if operation in ['Clean, Load & Enqueue', 'Clean & Load']: + self.driver.delete_all_waveforms() + first_index = 1 + else: + n_remaining_wf = self.driver.get_waveform_number() + last_wf = str(self.driver.get_waveform_name(n_remaining_wf - 1)) + last_wf = last_wf.split('_') + first_index = int(last_wf[1]) + 1 + + _used_channels = [] + loops = [] + name_parameters = [] + n_loops = len(self.parameters) + if n_loops > 0: + context.run_after_transfer = False + context.select_after_transfer = False + self.driver.run_mode = 'SEQUENCE' + for params in self.parameters.items(): + loop_start = float(self.format_and_eval_string(params[1][0])) + loop_stop = float(self.format_and_eval_string(params[1][1])) + loop_points = int(self.format_and_eval_string(params[1][2])) + loops.append(np.linspace(loop_start, loop_stop, loop_points)) + name_parameters.append(params[0]) + self.write_in_database( + params[0] + '_loop', + np.linspace( + loop_start, + loop_stop, + loop_points)) + + loop_values = np.moveaxis( + np.array(np.meshgrid(*loops)), 0, -1).reshape((-1, n_loops)) + if operation == 'Clean, Load & Enqueue': + self.write_in_database('num_loop', len(loop_values)) + for nn, loop_value in enumerate(loop_values): + for ii, name_parameter in enumerate(name_parameters): + self.write_in_database(name_parameter, loop_value[ii]) + for k, v in self.sequence_vars.items(): + seq.external_vars[k] = self.format_and_eval_string(v) + context.sequence_name = '{}_{}'.format('', nn + first_index) + res, infos, errors = context.compile_and_transfer_sequence( + seq, + driver=self.driver) + if operation == 'Clean, Load & Enqueue': + for cc in range(4): + _seq = 'sequence_ch' + str(cc + 1) + if infos[_seq]: + self.driver.get_channel( + cc + + 1).set_sequence_pos( + infos[_seq], + nn + + 1) + _used_channels.append(cc + 1) + self.driver.set_jump_pos(nn + 1, 1) + self.driver.set_goto_pos(len(loop_values), 1) + for cc in set(_used_channels): + self.driver.get_channel(cc).output_state = 'on' + + if not res: + raise Exception('Failed to compile sequence :\n' + + pformat(errors)) + self.write_in_database(name_parameter, loop_value[ii]) + + else: + for k, v in self.sequence_vars.items(): + seq.external_vars[k] = self.format_and_eval_string(v) + if self.run_mode == 'Continuous': + self.driver.run_mode = 'CONT' + else: + self.driver.run_mode = 'TRIG' + context.sequence_name = '{}_{}'.format('', first_index) + res, infos, errors = context.compile_and_transfer_sequence( + seq, self.driver) + + if not res: + raise Exception('Failed to compile sequence :\n' + + pformat(errors)) + + for k, v in infos.items(): + self.write_in_database(k, v) + + if operation == 'Load & Enqueue all': + n_wf = self.driver.get_waveform_number() - 25 + channels_to_turn_on = set() + for ii in range(n_wf): + index = ii + 25 + current_wf = str(self.driver.get_waveform_name(index)) + current_ch = int(current_wf[-1]) + current_index = int(current_wf.split('_')[1]) + channels_to_turn_on.add(current_ch) + self.driver.get_channel(current_ch).set_sequence_pos( + current_wf, current_index) + self.driver.set_jump_pos(current_index, 1) + self.driver.set_goto_pos(current_index, 1) + self.write_in_database('num_loop', current_index) + for cc in channels_to_turn_on: + self.driver.get_channel(cc).output_state = 'on' + + def register_preferences(self): + """Register the task preferences into the preferences system. + + """ + super(TransferPulseLoopTask, self).register_preferences() + + if self.sequence: + self.preferences['sequence'] =\ + self.sequence.preferences_from_members() + + update_preferences_from_members = register_preferences + + def traverse(self, depth=-1): + """Reimplemented to also yield the sequence + + """ + infos = super(TransferPulseLoopTask, self).traverse(depth) + + for i in infos: + yield i + + for item in self.sequence.traverse(): + yield item + + @classmethod + def build_from_config(cls, config, dependencies): + """Rebuild the task and the sequence from a config file. + + """ + builder = cls.mro()[1].build_from_config.__func__ + task = builder(cls, config, dependencies) + + if 'sequence' in config: + pulse_dep = dependencies['exopy.pulses.item'] + builder = pulse_dep['exopy_pulses.RootSequence'] + conf = config['sequence'] + seq = builder.build_from_config(conf, dependencies) + task.sequence = seq + + return task + + def _post_setattr_sequence(self, old, new): + """Set up n observer on the sequence context to properly update the + database entries. + + """ + entries = self.database_entries.copy() + if old: + old.unobserve('context', self._update_database_entries) + if old.context: + for k in old.context.list_sequence_infos(): + del entries[k] + if new: + new.observe('context', self._update_database_entries) + if new.context: + entries.update(new.context.list_sequence_infos()) + + if entries != self.database_entries: + self.database_entries = entries + + def _post_setattr_parameters(self, old, new): + """Observer keeping the database entries in sync with the declared + definitions. + + """ + entries = self.database_entries.copy() + for e in old: + del entries[e] + del entries[e + '_loop'] + for e in new: + entries.update({key: 0.0 for key in new}) + entries.update({key + '_loop': 0.0 for key in new}) + self.database_entries = entries + + def _update_database_entries(self, change): + """Reflect in the database the sequence infos of the context. + + """ + entries = self.database_entries.copy() + if change.get('oldvalue'): + for k in change['oldvalue'].list_sequence_infos(): + del entries[k] + if change['value']: + context = change['value'] + entries.update(context.list_sequence_infos()) + self.database_entries = entries diff --git a/exopy_pulses/tasks/tasks/instrs/transfer_sequence_task.py b/exopy_pulses/tasks/tasks/instrs/transfer_sequence_task.py index 45a0908..c8dea69 100644 --- a/exopy_pulses/tasks/tasks/instrs/transfer_sequence_task.py +++ b/exopy_pulses/tasks/tasks/instrs/transfer_sequence_task.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- -# Copyright 2015-2018 by ExopyHqcLegacy Authors, see AUTHORS for more details. +# Copyright 2015-2021 by ExopyHqcLegacy Authors, see AUTHORS for more details. # # Distributed under the terms of the BSD license. # @@ -18,6 +18,8 @@ from exopy.utils.atom_util import ordered_dict_from_pref, ordered_dict_to_pref from exopy.utils.traceback import format_exc +import matplotlib.pyplot as plt +import numpy as np class TransferPulseSequenceTask(InstrumentTask): """Build and transfer a pulse sequence to an instrument. @@ -96,6 +98,48 @@ def perform(self): for k, v in infos.items(): self.write_in_database(k, v) + def compile_and_plot(self, variables): + """Compile the sequence and plot it. + + """ + seq = self.sequence + context = seq.context + for k, v in variables.items(): + seq.external_vars[k] = self.format_and_eval_string(v) + + table, marker1, marker2, errors = context.compile_sequence(seq) + if not table: + raise Exception('Failed to compile sequence :\n' + + pformat(errors)) + freq = context.list_sequence_infos()['sampling_frequency'] + + channel_num = len(table) + fig, axs = plt.subplots(channel_num, 1, figsize=(15, 2.5*channel_num), + sharex=True) + fig.subplots_adjust(hspace = 0.5, wspace=.001) + + x = np.arange(len(table[list(table.keys())[0]]))/freq*10**6 + + if len(list(table.keys())) == 1: + key = list(table.keys())[0] + axs.plot(x,table[key], label = 'wvfm') + axs.plot(x,marker1[key], label = 'M1') + axs.plot(x,marker2[key], label = 'M2') + axs.set_xlabel('time (us)') + axs.set_ylabel(key) + axs.axis(xmin = 0, xmax = x[-1],ymin = -1.2, ymax = 1.2) + axs.legend(loc=6) + else: + for i, key in enumerate(np.sort(list(table.keys()))): + axs[i].plot(x,table[key], label = 'wvfm') + axs[i].plot(x,marker1[key], label = 'M1') + axs[i].plot(x,marker2[key], label = 'M2') + axs[i].set_xlabel('time (us)') + axs[i].set_ylabel(key) + axs[i].axis(xmin = 0, xmax = x[-1],ymin = -1.2, ymax = 1.2) + axs[i].legend(loc=6) + return fig + def register_preferences(self): """Register the task preferences into the preferences system. diff --git a/exopy_pulses/tasks/tasks/instrs/views/transfer_pulse_loop_task_view.enaml b/exopy_pulses/tasks/tasks/instrs/views/transfer_pulse_loop_task_view.enaml new file mode 100644 index 0000000..a3e7fae --- /dev/null +++ b/exopy_pulses/tasks/tasks/instrs/views/transfer_pulse_loop_task_view.enaml @@ -0,0 +1,400 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2015-2021 by ExopyPulses Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""View of the task used to transfer a pulse sequence on an arbitrary +waveform generator. + +""" +import os +import logging +from traceback import format_exc +from textwrap import fill +from inspect import cleandoc +from collections import OrderedDict + +from enaml.styling import StyleSheet, Style, Setter +from enaml.layout.api import factory, hbox, vbox, spacer, align +from enaml.core.api import Conditional, Include +from enaml.widgets.api import (GroupBox, Label, Field, ObjectCombo, CheckBox, + Notebook, Page, PushButton, Menu, Action, + FileDialogEx, Container) +from enaml.stdlib.message_box import information, question, warning + +from exopy.utils.widgets.qt_completers import QtLineCompleter +from exopy.utils.widgets.dict_editor import DictEditor +from exopy.utils.widgets.dict_list_editor import DictListEditor +from exopy.tasks.tasks.instr_view import InstrTaskView + +from exopy_pulses.pulses.utils.entry_eval import EVALUATER_TOOLTIP +from exopy_pulses.pulses.utils.sequences_io import save_sequence_prefs +from exopy_pulses.pulses.sequences.views.base_sequences_views\ + import instantiate_context_view + +def fc(txt): + return fill(cleandoc(txt)) + +enamldef RedButton(StyleSheet): + """Style sheet allowing to use a large font for the save confirmation popup + + """ + Style: + style_class = 'red-button' + Setter: + field = 'background' + value = 'red' + + +enamldef VarEditor(Container): + """ Fields allowing top edit the global variables of a sequence. + + """ + #: Model object describing the key/value pair. + attr model + + padding = 1 + constraints = [hbox(lab, val), align('v_center', lab, val)] + Label: lab: + hug_width = 'strong' + text << model.key + QtLineCompleter: val: + text := model.value + entries_updater << model.task.list_accessible_database_entries + tool_tip = EVALUATER_TOOLTIP + +enamldef LoopEditor(Container): + """ Fields allowing to edit the sweep of a sequence. + + """ + #: Model object describing the key/value pair. + attr model + + padding = 1 + constraints = [hbox(loop_name, + param_start, param_start_val, param_stop, + param_stop_val, param_points, param_points_val), + align('v_center', loop_name, param_start)] + + Field: loop_name: + text := model.key + + Label: param_start: + text = 'Start value' + QtLineCompleter: param_start_val: + text := model.value0 + entries_updater << model.task.list_accessible_database_entries + tool_tip = fc('''Start value of the loop parameter to be called in the + variables. If several loops are needed, separate the + values by a comma. Do not put a space + after the commas. + ''') + + Label: param_stop: + text = 'Stop value' + QtLineCompleter: param_stop_val: + text := model.value1 + entries_updater << model.task.list_accessible_database_entries + tool_tip = fc('''Stop value of the loop parameter to be called in the + variables. If several loops are needed, separate the + values by a comma. Do not put a space + after the commas. + ''') + + Label: param_points: + text = 'Number of points' + QtLineCompleter: param_points_val: + text := model.value2 + entries_updater << model.task.list_accessible_database_entries + tool_tip = fc('''Number of points of the loop. If several loops + are needed, separate the values by a comma. + Do not put a space after the commas. + ''') + + +enamldef ObjectComboFieldCompleterEditor(Container): + """ + + """ + #: Reference to the object holding the completion information. + attr model + constraints = [hbox(k, v), 2*k.width <= v.width] + padding = 1 + + ObjectCombo: k: + items << list(model.task.sequence_vars.keys()) + selected := model.task.loop_name + tool_tip = ("Select external sequence variable to add") + QtLineCompleter: v: + text := model.value + entries_updater << list(model.task.sequence_vars.keys()) + tool_tip = EVALUATER_TOOLTIP + +def validate_context_driver_pair(core, context, task, parent=None): + """Validate that the context can work in conjunction with the selected + driver. + + """ + if task.selected_instrument and task.selected_instrument[0]: + cmd = 'exopy.pulses.get_context_infos' + c_id = context.context_id + c_infos = core.invoke_command(cmd, dict(context_id=c_id)) + driver_id = task.selected_instrument[1] + if driver_id not in c_infos.instruments: + del task.selected_instrument + information(parent, title='Invalid context/driver association', + text=fill('The context of the loaded sequence does ' + 'not support the selected driver, please ' + ' elect a different driver.')) + + +def load_sequence(core, task, parent=None, path=''): + """Open a dialog and load a pulse sequence. + + Parameters + ---------- + core : CorePlugin + Refrence to the core plugin of the application. + task : TransferPulseSequenceTask + Task for which to load the sequence. + parent : enaml.Widget + Parent for the dialog. + path : unicode, optional + Path of the sequence to load. If this does not point to a real + file the path is used as a hint for the file selection dialog. + + """ + if not os.path.isfile(path): + path = FileDialogEx.get_open_file_name(parent, current_path=path, + name_filters=['*.pulse.ini']) + if path: + cmd = 'exopy.pulses.build_sequence' + try: + seq = core.invoke_command(cmd, {'path': path}) + except Exception: + cmd = 'exopy.app.errors.signal' + msg = 'Failed to load sequence {}: {}'.format(path, format_exc()) + core.invoke_command(cmd, dict(kind='error', message=msg)) + return + + if seq.context: + validate_context_driver_pair(core, seq.context, task, parent) + + task.sequence = seq + new = OrderedDict.fromkeys(seq.external_vars, '') + for k in (e for e in task.sequence_vars if e in new): + new[k] = task.sequence_vars[k] + task.sequence_vars = new + task.sequence_path = path + task.sequence_timestamp = os.path.getmtime(path) + + +enamldef TransferPulseLoopView(InstrTaskView): view: + """View for the TransferPulseSequenceTask. + + """ + constraints << [vbox(hbox(seq, seq_name, seq_re, seq_sav, spacer, operation_lab, operation_val, + instr_label, instr_selection), parameter, + hbox(run_mode_lab, run_mode_val, trig_period_lab, trig_period_val), + nb)] + + initialized :: + + task.observe('sequence', _install_context_observer) + + if task.sequence and os.path.isfile(task.sequence_path): + if os.path.getmtime(task.sequence_path) != task.sequence_timestamp: + seq_re.style_class = 'red-button' + seq_re.tool_tip = fill('The sequence appears to have been ' + 'edited since it has been reload. ' + 'Consider refreshing the sequence.') + + if task.sequence.context: + validate_context_driver_pair(root.core, task.sequence.context, + task, self) + + task.sequence.observe('context', + _check_context_driver_compatibility) + + filter_profiles => (profiles): + """Only allow profile whose at least one driver can be used by the + context. + + """ + if not task.sequence or not task.sequence.context: + return profiles + + cmd = 'exopy.pulses.get_context_infos' + c_id = task.sequence.context.context_id + c_infos = self.root.core.invoke_command(cmd, dict(context_id=c_id)) + + return [p for p, v in profiles.items() + if any([d.id in c_infos.instruments + for d in v.model.drivers])] + + filter_drivers => (drivers): + """Only allow drivers supported by the context. + + """ + if not task.sequence or not task.sequence.context: + return drivers + + cmd = 'exopy.pulses.get_context_infos' + c_id = task.sequence.context.context_id + c_infos = self.root.core.invoke_command(cmd, dict(context_id=c_id)) + + return [d for d in drivers if d.id in c_infos.instruments] + + instr_selection.enabled << bool(task.sequence and task.sequence.context) + instr_selection.tool_tip << ('Please first select a context' + if not instr_selection.enabled else + '') + + PushButton: seq: + text = 'Select sequence' + clicked :: + dir_path = os.path.dirname(task.sequence_path) + load_sequence(view.root.core, task, parent=view, path=dir_path) + seq_re.style_class = '' + + Field: seq_name: + visible << bool(task.sequence_path) + read_only = True + text << os.path.basename(task.sequence_path).rstrip('.pulse.ini') + + PushButton: seq_re: + enabled << bool(task.sequence_path and + os.path.isfile(task.sequence_path)) + text = 'Refresh' + tool_tip << ('Referenced file does not exist anymore.' + if not self.enabled else '') + clicked :: + btn = question(self, title='Confirm refresh', + text='If you refresh any local modification to the ' + 'sequence will be lost.\nConfirm refresh ?') + if btn and btn.action == 'accept': + try: + load_sequence(view.root.core, task, + path=task.sequence_path) + except Exception: + warning(self, 'Failed to refresh sequence', format_exc()) + + self.tool_tip = 'Reload the sequence from file.' + self.style_class = '' + + PushButton: seq_sav: + enabled << bool(task.sequence) + text = 'Save' + Menu: + Action: + text = 'Save' + enabled << bool(task.sequence_path and + os.path.isfile(task.sequence_path)) + tool_tip << ('Referenced file does not exist anymore.' + if not self.enabled else '') + triggered :: + btn = question(seq_sav, title='Confirm save', + text='If you save any local modification will ' + 'override the original sequence.\n' + 'Confirm save ?') + if btn and btn.action == 'accept': + seq = task.sequence + prefs = seq.preferences_from_members() + ext_vars = OrderedDict.fromkeys(seq.external_vars, '') + prefs['external_vars'] = repr(list(ext_vars.items())) + save_sequence_prefs(task.sequence_path, prefs) + tstmp = os.path.getmtime(task.sequence_path) + task.sequence_timestamp = tstmp + Action: + text = 'Save as' + triggered :: + explore = FileDialogEx.get_save_file_name + path = explore(seq_sav, current_path=task.sequence_path, + name_filters=['*.pulse.ini']) + if path: + if not path.endswith('.pulse.ini'): + path += '.pulse.ini' + seq = task.sequence + prefs = seq.preferences_from_members() + ext_vars = OrderedDict.fromkeys(seq.external_vars, '') + prefs['external_vars'] = repr(list(ext_vars.items())) + save_sequence_prefs(path, prefs) + tstmp = os.path.getmtime(path) + task.sequence_timestamp = tstmp + task.sequence_path = path + + Label: operation_lab: + text = 'Operation :' + ObjectCombo: operation_val: + items << list(task.get_member('operation').items) + selected := task.operation + tool_tip = ("Clean: delete all previously loaded waveforms\n" + "Load: load waveforms in the waveform list of the AWG \n" + "Enqueue: build a sequence from loaded waveforms") + + DictListEditor(LoopEditor): parameter: + parameter.mapping := task.parameters + parameter.operations = ['add','remove'] + parameter.attributes = {'task' : task} + + Label: run_mode_lab: + text = 'Running mode' + ObjectCombo: run_mode_val: + items << list(task.get_member('run_mode').items) + selected := task.run_mode + + Label: trig_period_lab: + text = 'Int trigger period (us)' + QtLineCompleter: trig_period_val: + entries_updater << task.list_accessible_database_entries + text := task.trigger_period + tool_tip = EVALUATER_TOOLTIP + enabled << (task.run_mode=='Int Trig') + + Notebook: nb: + tabs_closable = False + visible << bool(task.sequence) + Page: + title = 'Variables (Name: values)' + DictEditor(VarEditor): ed: + ed.mapping := task.sequence_vars + ed.attributes << {'task': task} + Page: + title = 'Context' + Include: + objects << ([instantiate_context_view(view.root.core, + task.sequence, + task.sequence.context + )] + if task.sequence and task.sequence.context else []) + + # ========================================================================= + # --- Private API --------------------------------------------------------- + # ========================================================================= + + func _install_context_observer(change): + """Setup an observer to validate the driver/context match on context + change. + + """ + if 'oldvalue' in change and change['oldvalue']: + change['oldvalue'].unobserve('context', + _check_context_driver_compatibility) + if change['value']: + sequence = change['value'] + sequence.observe('context', _check_context_driver_compatibility) + if sequence.context: + validate_context_driver_pair(view.root.core, + task.sequence.context, task, view) + + func _check_context_driver_compatibility(change): + """Check whether the selected driver can be used with the selected + context. + + """ + if task.sequence.context: + validate_context_driver_pair(view.root.core, task.sequence.context, + task, view) diff --git a/exopy_pulses/tasks/tasks/instrs/views/transfer_sequence_task_view.enaml b/exopy_pulses/tasks/tasks/instrs/views/transfer_sequence_task_view.enaml index f4203bb..7e77b30 100644 --- a/exopy_pulses/tasks/tasks/instrs/views/transfer_sequence_task_view.enaml +++ b/exopy_pulses/tasks/tasks/instrs/views/transfer_sequence_task_view.enaml @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- -# Copyright 2015-2018 by ExopyPulses Authors, see AUTHORS for more details. +# Copyright 2015-2021 by ExopyPulses Authors, see AUTHORS for more details. # # Distributed under the terms of the BSD license. # @@ -20,7 +20,8 @@ from enaml.layout.api import factory, hbox, vbox, spacer, align from enaml.core.api import Conditional, Include from enaml.widgets.api import (GroupBox, Label, Field, ObjectCombo, CheckBox, Notebook, Page, PushButton, Menu, Action, - FileDialogEx, Container) + FileDialogEx, Container, MPLCanvas, Window, + ScrollArea) from enaml.stdlib.message_box import information, question, warning from exopy.utils.traceback import format_exc @@ -33,6 +34,9 @@ from exopy_pulses.pulses.utils.sequences_io import save_sequence_prefs from exopy_pulses.pulses.sequences.views.base_sequences_views\ import instantiate_context_view +import matplotlib.pyplot as plt +import numpy as np + enamldef RedButton(StyleSheet): """Style sheet allowing to use a large font for the save confirmation popup @@ -63,6 +67,35 @@ enamldef VarEditor(Container): tool_tip = EVALUATER_TOOLTIP +enamldef SequenceWindow(Window): seqwin: + #attr task + + title = 'Sequences' + + initial_size = (1500,1000) + + Container: + ScrollArea: + Container: + constraints = [vbox(ed, update, check, plot)] + + DictEditor(VarEditor): ed: + ed.mapping = task.sequence_vars + ed.attributes << {'task': task} + + PushButton: update: + text = 'Update Plot' + clicked :: + variables = ed.mapping + canvas.figure = task.compile_and_plot(variables) + + CheckBox: check: + text = 'Toolbar Visible' + checked := canvas.toolbar_visible + Container: plot: + MPLCanvas: canvas: + figure = task.compile_and_plot(ed.mapping) + def validate_context_driver_pair(core, context, task, parent=None): """Validate that the context can work in conjunction with the selected driver. @@ -126,7 +159,7 @@ enamldef TransferPulseSequenceView(InstrTaskView): view: """View for the TransferPulseSequenceTask. """ - constraints << [vbox(hbox(seq, seq_name, seq_re, seq_sav, spacer, + constraints << [vbox(hbox(seq, seq_name, seq_re, seq_sav, seq_plot, spacer, instr_label, instr_selection), nb)] @@ -255,6 +288,13 @@ enamldef TransferPulseSequenceView(InstrTaskView): view: task.sequence_timestamp = tstmp task.sequence_path = path + PushButton: seq_plot: + enabled << bool(task.sequence) + text = 'Plot sequence' + clicked :: + win = SequenceWindow(self) + win.show() + Notebook: nb: tabs_closable = False visible << bool(task.sequence) @@ -287,9 +327,9 @@ enamldef TransferPulseSequenceView(InstrTaskView): view: if change['value']: sequence = change['value'] sequence.observe('context', _check_context_driver_compatibility) - if sequence.context: - validate_context_driver_pair(view.root.core, - task.sequence.context, task, view) + #if sequence.context: + # validate_context_driver_pair(view.root.core, + # task.sequence.context, task, view) func _check_context_driver_compatibility(change): """Check whether the selected driver can be used with the selected diff --git a/exopy_pulses/testing/fixtures.py b/exopy_pulses/testing/fixtures.py index 39e4200..b2b93cd 100644 --- a/exopy_pulses/testing/fixtures.py +++ b/exopy_pulses/testing/fixtures.py @@ -76,8 +76,9 @@ def template_sequence(pulses_plugin): """ from exopy_pulses.pulses.pulse import Pulse from exopy_pulses.pulses.sequences.base_sequences import (RootSequence, - BaseSequence) + BaseSequence) from exopy_pulses.pulses.shapes.square_shape import SquareShape + from exopy_pulses.pulses.shapes.gaussian_shape import GaussianShape from exopy_pulses.pulses.contexts.template_context import TemplateContext root = RootSequence() diff --git a/setup.py b/setup.py index d5ea0ac..c2c2cc2 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ def long_description(): packages=find_packages(exclude=['tests', 'tests.*']), package_data={'': ['*.enaml']}, setup_requires=['setuptools'], - install_requires=['exopy', 'numpy'], + install_requires=['exopy', 'numpy', 'matplotlib'], entry_points={ 'exopy_package_extension': 'exopy_pulses = %s:list_manifests' % PROJECT_NAME}