Completed
Pull Request — master (#384)
by
unknown
02:02
created

SequenceGeneratorLogic.analyze_block_ensemble()   F

Complexity

Conditions 16

Size

Total Lines 116

Duplication

Lines 9
Ratio 7.76 %

Importance

Changes 0
Metric Value
cc 16
c 0
b 0
f 0
dl 9
loc 116
rs 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like SequenceGeneratorLogic.analyze_block_ensemble() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# -*- coding: utf-8 -*-
2
3
"""
4
This file contains the Qudi sequence generator logic for general sequence structure.
5
6
Qudi is free software: you can redistribute it and/or modify
7
it under the terms of the GNU General Public License as published by
8
the Free Software Foundation, either version 3 of the License, or
9
(at your option) any later version.
10
11
Qudi is distributed in the hope that it will be useful,
12
but WITHOUT ANY WARRANTY; without even the implied warranty of
13
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
GNU General Public License for more details.
15
16
You should have received a copy of the GNU General Public License
17
along with Qudi. If not, see <http://www.gnu.org/licenses/>.
18
19
Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the
20
top-level directory of this distribution and at <https://github.com/Ulm-IQO/qudi/>
21
"""
22
23
import numpy as np
24
import os
25
import pickle
26
import time
27
28
from qtpy import QtCore
29
from collections import OrderedDict
30
from core.module import StatusVar, Connector, ConfigOption
31
from core.util.modules import get_main_dir, get_home_dir
32
from logic.generic_logic import GenericLogic
33
from logic.pulsed.pulse_objects import PulseBlock, PulseBlockEnsemble, PulseSequence
34
from logic.pulsed.pulse_objects import PulseObjectGenerator
35
from logic.pulsed.sampling_functions import SamplingFunctions
36
37
38
class SequenceGeneratorLogic(GenericLogic):
39
    """
40
    This is the Logic class for the pulse (sequence) generation.
41
42
    It is responsible for creating the theoretical (ideal) contruction plan for a pulse sequence or
43
    waveform (digital and/or analog) by creating PulseBlockElements, PulseBlocks,
44
    PulseBlockEnsembles and PulseSequences.
45
    Based on these objects the logic can sample waveforms according to the underlying hardware
46
    constraints (especially the sample rate) and upload these samples to the connected pulse
47
    generator hardware.
48
49
    This logic is also responsible to manipulate and read back hardware settings for
50
    waveform/sequence playback (pp-amplitude, sample rate, active channels etc.).
51
    """
52
53
    _modclass = 'sequencegeneratorlogic'
54
    _modtype = 'logic'
55
56
    # declare connectors
57
    pulsegenerator = Connector(interface='PulserInterface')
58
59
    # configuration options
60
    _assets_storage_dir = ConfigOption(name='assets_storage_path',
61
                                       default=os.path.join(get_home_dir(), 'saved_pulsed_assets'),
62
                                       missing='warn')
63
    _overhead_bytes = ConfigOption(name='overhead_bytes', default=0, missing='nothing')
64
    # Optional additional paths to import from
65
    additional_methods_dir = ConfigOption(name='additional_predefined_methods_path',
66
                                          default=None,
67
                                          missing='nothing')
68
    _sampling_functions_import_path = ConfigOption(name='additional_sampling_functions_path',
69
                                                   default=None,
70
                                                   missing='nothing')
71
72
    # status vars
73
    # Global parameters describing the channel usage and common parameters used during pulsed object
74
    # generation for predefined methods.
75
    _generation_parameters = StatusVar(default=OrderedDict([('laser_channel', 'd_ch1'),
76
                                                            ('sync_channel', ''),
77
                                                            ('gate_channel', ''),
78
                                                            ('microwave_channel', 'a_ch1'),
79
                                                            ('microwave_frequency', 2.87e9),
80
                                                            ('microwave_amplitude', 0.0),
81
                                                            ('rabi_period', 100e-9),
82
                                                            ('laser_length', 3e-6),
83
                                                            ('laser_delay', 500e-9),
84
                                                            ('wait_time', 1e-6),
85
                                                            ('analog_trigger_voltage', 0.0)]))
86
87
    # The created pulse objects (PulseBlock, PulseBlockEnsemble, PulseSequence) are saved in
88
    # these dictionaries. The keys are the names.
89
    # _saved_pulse_blocks = StatusVar(default=OrderedDict())
90
    # _saved_pulse_block_ensembles = StatusVar(default=OrderedDict())
91
    # _saved_pulse_sequences = StatusVar(default=OrderedDict())
92
93
    # define signals
94
    sigBlockDictUpdated = QtCore.Signal(dict)
95
    sigEnsembleDictUpdated = QtCore.Signal(dict)
96
    sigSequenceDictUpdated = QtCore.Signal(dict)
97
    sigSampleEnsembleComplete = QtCore.Signal(object)
98
    sigSampleSequenceComplete = QtCore.Signal(object)
99
    sigLoadedAssetUpdated = QtCore.Signal(str, str)
100
    sigGeneratorSettingsUpdated = QtCore.Signal(dict)
101
    sigSamplingSettingsUpdated = QtCore.Signal(dict)
102
    sigAvailableWaveformsUpdated = QtCore.Signal(list)
103
    sigAvailableSequencesUpdated = QtCore.Signal(list)
104
105
    sigPredefinedSequenceGenerated = QtCore.Signal(object)
106
107
    def __init__(self, config, **kwargs):
108
        super().__init__(config=config, **kwargs)
109
110
        self.log.debug('The following configuration was found.')
111
        for key in config.keys():
112
            self.log.debug('{0}: {1}'.format(key, config[key]))
113
114
        # directory for additional generate methods to import
115
        # (other than logic.predefined_generate_methods)
116
        if 'additional_methods_dir' in config.keys():
117
            if not os.path.exists(config['additional_methods_dir']):
118
                self.log.error('Specified path "{0}" for import of additional generate methods '
119
                               'does not exist.'.format(config['additional_methods_dir']))
120
                self.additional_methods_dir = None
121
122
        # current pulse generator settings that are frequently used by this logic.
123
        # Save them here since reading them from device every time they are used may take some time.
124
        self.__activation_config = ('', set())  # Activation config name and set of active channels
125
        self.__sample_rate = 0.0  # Sample rate in samples/s
126
        self.__analog_levels = (dict(), dict())  # Tuple of two dict (<pp_amplitude>, <offset>)
127
                                                 # Dict keys are analog channel descriptors
128
        self.__digital_levels = (dict(), dict())  # Tuple of two dict (<low_volt>, <high_volt>)
129
                                                  # Dict keys are digital channel descriptors
130
        self.__interleave = False  # Flag to indicate use of interleave
131
132
        # A flag indicating if sampling of a sequence is in progress
133
        self.__sequence_generation_in_progress = False
134
135
        # Get instance of PulseObjectGenerator which takes care of collecting all predefined methods
136
        self._pog = None
137
138
        # The created pulse objects (PulseBlock, PulseBlockEnsemble, PulseSequence) are saved in
139
        # these dictionaries. The keys are the names.
140
        self._saved_pulse_blocks = OrderedDict()
141
        self._saved_pulse_block_ensembles = OrderedDict()
142
        self._saved_pulse_sequences = OrderedDict()
143
        return
144
145
    def on_activate(self):
146
        """ Initialisation performed during activation of the module.
147
        """
148
        if not os.path.exists(self._assets_storage_dir):
149
            os.makedirs(self._assets_storage_dir)
150
151
        # Initialize SamplingFunctions class by handing over a list of paths to import
152
        # sampling functions from.
153
        sf_path_list = [os.path.join(get_main_dir(), 'logic', 'pulsed', 'sampling_function_defs')]
154
        if isinstance(self._sampling_functions_import_path, str):
155
            sf_path_list.append(self._sampling_functions_import_path)
156
        SamplingFunctions.import_sampling_functions(sf_path_list)
157
158
        # Read back settings from device and update instance variables accordingly
159
        self._read_settings_from_device()
160
161
        # Update saved blocks/ensembles/sequences from serialized files
162
        self._saved_pulse_blocks = OrderedDict()
163
        self._saved_pulse_block_ensembles = OrderedDict()
164
        self._saved_pulse_sequences = OrderedDict()
165
        self._update_blocks_from_file()
166
        self._update_ensembles_from_file()
167
        self._update_sequences_from_file()
168
169
        # Get instance of PulseObjectGenerator which takes care of collecting all predefined methods
170
        self._pog = PulseObjectGenerator(sequencegeneratorlogic=self)
171
172
        self.__sequence_generation_in_progress = False
173
        return
174
175
    def on_deactivate(self):
176
        """ Deinitialisation performed during deactivation of the module.
177
        """
178
        return
179
180
    # @_saved_pulse_blocks.constructor
181
    # def _restore_saved_blocks(self, block_list):
182
    #     return_block_dict = OrderedDict()
183
    #     if block_list is not None:
184
    #         for block_dict in block_list:
185
    #             return_block_dict[block_dict['name']] = PulseBlock.block_from_dict(block_dict)
186
    #     return return_block_dict
187
    #
188
    #
189
    # @_saved_pulse_blocks.representer
190
    # def _convert_saved_blocks(self, block_dict):
191
    #     if block_dict is None:
192
    #         return None
193
    #     else:
194
    #         block_list = list()
195
    #         for block in block_dict.values():
196
    #             block_list.append(block.get_dict_representation())
197
    #         return block_list
198
    #
199
    # @_saved_pulse_block_ensembles.constructor
200
    # def _restore_saved_ensembles(self, ensemble_list):
201
    #     return_ensemble_dict = OrderedDict()
202
    #     if ensemble_list is not None:
203
    #         for ensemble_dict in ensemble_list:
204
    #             return_ensemble_dict[ensemble_dict['name']] = PulseBlockEnsemble.ensemble_from_dict(
205
    #                 ensemble_dict)
206
    #     return return_ensemble_dict
207
    #
208
    # @_saved_pulse_block_ensembles.representer
209
    # def _convert_saved_ensembles(self, ensemble_dict):
210
    #     if ensemble_dict is None:
211
    #         return None
212
    #     else:
213
    #         ensemble_list = list()
214
    #         for ensemble in ensemble_dict.values():
215
    #             ensemble_list.append(ensemble.get_dict_representation())
216
    #         return ensemble_list
217
    #
218
    # @_saved_pulse_sequences.constructor
219
    # def _restore_saved_sequences(self, sequence_list):
220
    #     return_sequence_dict = OrderedDict()
221
    #     if sequence_list is not None:
222
    #         for sequence_dict in sequence_list:
223
    #             return_sequence_dict[sequence_dict['name']] = PulseBlockEnsemble.ensemble_from_dict(
224
    #                 sequence_dict)
225
    #     return return_sequence_dict
226
    #
227
    # @_saved_pulse_sequences.representer
228
    # def _convert_saved_sequences(self, sequence_dict):
229
    #     if sequence_dict is None:
230
    #         return None
231
    #     else:
232
    #         sequence_list = list()
233
    #         for sequence in sequence_dict.values():
234
    #             sequence_list.append(sequence.get_dict_representation())
235
    #         return sequence_list
236
237
    ############################################################################
238
    # Pulse generator control methods and properties
239
    ############################################################################
240
    @property
241
    def pulse_generator_settings(self):
242
        settings_dict = dict()
243
        settings_dict['activation_config'] = tuple(self.__activation_config)
244
        settings_dict['sample_rate'] = float(self.__sample_rate)
245
        settings_dict['analog_levels'] = tuple(self.__analog_levels)
246
        settings_dict['digital_levels'] = tuple(self.__digital_levels)
247
        settings_dict['interleave'] = bool(self.__interleave)
248
        return settings_dict
249
250
    @pulse_generator_settings.setter
251
    def pulse_generator_settings(self, settings_dict):
252
        if isinstance(settings_dict, dict):
253
            self.set_pulse_generator_settings(settings_dict)
254
        return
255
256
    @property
257
    def pulse_generator_constraints(self):
258
        return self.pulsegenerator().get_constraints()
259
260
    @property
261
    def sampled_waveforms(self):
262
        return self.pulsegenerator().get_waveform_names()
263
264
    @property
265
    def sampled_sequences(self):
266
        return self.pulsegenerator().get_sequence_names()
267
268
    @property
269
    def analog_channels(self):
270
        return {chnl for chnl in self.__activation_config[1] if chnl.startswith('a_ch')}
271
272
    @property
273
    def digital_channels(self):
274
        return {chnl for chnl in self.__activation_config[1] if chnl.startswith('d_ch')}
275
276
    @property
277
    def loaded_asset(self):
278
        asset_names, asset_type = self.pulsegenerator().get_loaded_assets()
279
        name_list = list(asset_names.values())
280
        if asset_type == 'waveform' and len(name_list) > 0:
281
            return_type = 'PulseBlockEnsemble'
282
            return_name = name_list[0].rsplit('_', 1)[0]
283
            for name in name_list:
284
                if name.rsplit('_', 1)[0] != return_name:
285
                    return '', ''
286
        elif asset_type == 'sequence' and len(name_list) > 0:
287
            return_type = 'PulseSequence'
288
            return_name = name_list[0].rsplit('_', 1)[0]
289
            for name in name_list:
290
                if name.rsplit('_', 1)[0] != return_name:
291
                    return '', ''
292
        else:
293
            return '', ''
294
        return return_name, return_type
295
296
    @QtCore.Slot(dict)
297
    def set_pulse_generator_settings(self, settings_dict=None, **kwargs):
298
        """
299
        Either accept a settings dictionary as positional argument or keyword arguments.
300
        If both are present both are being used by updating the settings_dict with kwargs.
301
        The keyword arguments take precedence over the items in settings_dict if there are
302
        conflicting names.
303
304
        @param settings_dict:
305
        @param kwargs:
306
        @return:
307
        """
308
        # Check if pulse generator is running and do nothing if that is the case
309
        pulser_status, status_dict = self.pulsegenerator().get_status()
310
        if pulser_status == 0:
311
            # Determine complete settings dictionary
312
            if not isinstance(settings_dict, dict):
313
                settings_dict = kwargs
314
            else:
315
                settings_dict.update(kwargs)
316
317
            # Set parameters if present
318
            if 'activation_config' in settings_dict:
319
                activation_config = settings_dict['activation_config']
320
                available_configs = self.pulse_generator_constraints.activation_config
321
                set_config = None
322
                # Allow argument types str, set and tuple
323
                if isinstance(activation_config, str):
324
                    if activation_config in available_configs.keys():
325
                        set_config = self._apply_activation_config(
326
                            available_configs[activation_config])
327
                        self.__activation_config = (activation_config, set_config)
328
                    else:
329
                        self.log.error('Unable to set activation config by name.\n'
330
                                       '"{0}" not found in pulser constraints.'
331
                                       ''.format(activation_config))
332
                elif isinstance(activation_config, set):
333
                    if activation_config in available_configs.values():
334
                        set_config = self._apply_activation_config(activation_config)
335
                        config_name = list(available_configs)[
336
                            list(available_configs.values()).index(activation_config)]
337
                        self.__activation_config = (config_name, set_config)
338
                    else:
339
                        self.log.error('Unable to set activation config "{0}".\n'
340
                                       'Not found in pulser constraints.'.format(activation_config))
341
                elif isinstance(activation_config, tuple):
342
                    if activation_config in available_configs.items():
343
                        set_config = self._apply_activation_config(activation_config[1])
344
                        self.__activation_config = (activation_config[0], set_config)
345
                    else:
346
                        self.log.error('Unable to set activation config "{0}".\n'
347
                                       'Not found in pulser constraints.'.format(activation_config))
348
                # Check if the ultimately set config is part of the constraints
349
                if set_config is not None and set_config not in available_configs.values():
350
                    self.log.error('Something went wrong while setting new activation config.')
351
                    self.__activation_config = ('', set_config)
352
353
                # search the generation_parameters for channel specifiers and adjust them if they
354
                # are no longer valid
355
                changed_settings = dict()
356
                ana_chnls = sorted(self.analog_channels)
357
                digi_chnls = sorted(self.digital_channels)
358
                for name in [setting for setting in self.generation_parameters if
359
                             setting.endswith('_channel')]:
360
                    channel = self.generation_parameters[name]
361
                    if isinstance(channel, str) and channel not in self.__activation_config[1]:
362
                        if channel.startswith('a'):
363
                            new_channel = ana_chnls[0] if ana_chnls else digi_chnls[0]
364
                        elif channel.startswith('d'):
365
                            new_channel = digi_chnls[0] if digi_chnls else ana_chnls[0]
366
                        else:
367
                            continue
368
369
                        if new_channel is not None:
370
                            self.log.warning('Change of activation config caused sampling_setting '
371
                                             '"{0}" to be changed to "{1}".'.format(name,
372
                                                                                    new_channel))
373
                            changed_settings[name] = new_channel
374
375
            if 'sample_rate' in settings_dict:
376
                self.__sample_rate = self.pulsegenerator().set_sample_rate(
377
                    float(settings_dict['sample_rate']))
378
379
            if 'analog_levels' in settings_dict:
380
                self.__analog_levels = self.pulsegenerator().set_analog_level(
381
                    *settings_dict['analog_levels'])
382
383
            if 'digital_levels' in settings_dict:
384
                self.__digital_levels = self.pulsegenerator().set_digital_level(
385
                    *settings_dict['digital_levels'])
386
387
            if 'interleave' in settings_dict:
388
                self.__interleave = self.pulsegenerator().set_interleave(
389
                    bool(settings_dict['interleave']))
390
391
        elif len(kwargs) != 0 or isinstance(settings_dict, dict):
392
            # Only throw warning when arguments have been passed to this method
393
            self.log.warning('Pulse generator is not idle (status: {0:d}, "{1}").\n'
394
                             'Unable to apply new settings.'.format(pulser_status,
395
                                                                    status_dict[pulser_status]))
396
397
        # emit update signal for master (GUI or other logic module)
398
        self.sigGeneratorSettingsUpdated.emit(self.pulse_generator_settings)
399
        # Apply potential changes to generation_parameters
400
        try:
401
            if changed_settings:
402
                self.generation_parameters = changed_settings
403
        except UnboundLocalError:
404
            pass
405
        return self.pulse_generator_settings
406
407
    @QtCore.Slot()
408
    def clear_pulser(self):
409
        """
410
        """
411
        self.pulsegenerator().clear_all()
412
        # Delete all sampling information from all PulseBlockEnsembles and PulseSequences
413
        for seq_name in self.saved_pulse_sequences:
414
            seq = self.saved_pulse_sequences[seq_name]
415
            seq.sampling_information = dict()
416
            self.save_sequence(seq)
417
        for ens_name in self.saved_pulse_block_ensembles:
418
            ens = self.saved_pulse_block_ensembles[ens_name]
419
            ens.sampling_information = dict()
420
            self.save_ensemble(ens)
421
        self.sigAvailableWaveformsUpdated.emit(self.sampled_waveforms)
422
        self.sigAvailableSequencesUpdated.emit(self.sampled_sequences)
423
        self.sigLoadedAssetUpdated.emit('', '')
424
        return
425
426 View Code Duplication
    @QtCore.Slot(str)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
427
    @QtCore.Slot(object)
428
    def load_ensemble(self, ensemble):
429
        """
430
431
        @param str|PulseBlockEnsemble ensemble:
432
        """
433
        # If str has been passed, get the ensemble object from saved ensembles
434
        if isinstance(ensemble, str):
435
            ensemble = self.saved_pulse_block_ensembles[ensemble]
436
            if ensemble is None:
437
                self.sigLoadedAssetUpdated.emit(*self.loaded_asset)
438
                return
439
        if not isinstance(ensemble, PulseBlockEnsemble):
440
            self.log.error('Unable to load PulseBlockEnsemble into pulser channels.\nArgument ({0})'
441
                           ' is no instance of PulseBlockEnsemble.'.format(type(ensemble)))
442
            self.sigLoadedAssetUpdated.emit(*self.loaded_asset)
443
            return
444
445
        # Check if the PulseBlockEnsemble has been sampled already.
446
        if ensemble.sampling_information:
447
            # Check if the corresponding waveforms are present in the pulse generator memory
448
            ready_waveforms = self.sampled_waveforms
449
            for waveform in ensemble.sampling_information['waveforms']:
450
                if waveform not in ready_waveforms:
451
                    self.log.error('Waveform "{0}" associated with PulseBlockEnsemble "{1}" not '
452
                                   'found on pulse generator device.\nPlease re-generate the '
453
                                   'PulseBlockEnsemble.'.format(waveform, ensemble.name))
454
                    self.sigLoadedAssetUpdated.emit(*self.loaded_asset)
455
                    return
456
            # Actually load the waveforms to the generic channels
457
            self.pulsegenerator().load_waveform(ensemble.sampling_information['waveforms'])
458
        else:
459
            self.log.error('Loading of PulseBlockEnsemble "{0}" failed.\n'
460
                           'It has not been generated yet.'.format(ensemble.name))
461
        self.sigLoadedAssetUpdated.emit(*self.loaded_asset)
462
        return
463
464 View Code Duplication
    @QtCore.Slot(str)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
465
    @QtCore.Slot(object)
466
    def load_sequence(self, sequence):
467
        """
468
469
        @param str|PulseSequence sequence:
470
        """
471
        # If str has been passed, get the sequence object from saved sequences
472
        if isinstance(sequence, str):
473
            sequence = self.saved_pulse_sequences[sequence]
474
            if sequence is None:
475
                self.sigLoadedAssetUpdated.emit(*self.loaded_asset)
476
                return
477
        if not isinstance(sequence, PulseSequence):
478
            self.log.error('Unable to load PulseSequence into pulser channels.\nArgument ({0})'
479
                           ' is no instance of PulseSequence.'.format(type(sequence)))
480
            self.sigLoadedAssetUpdated.emit(*self.loaded_asset)
481
            return
482
483
        # Check if the PulseSequence has been sampled already.
484
        if sequence.sampling_information and sequence.name in self.sampled_sequences:
485
            # Check if the corresponding waveforms are present in the pulse generator memory
486
            ready_waveforms = self.sampled_waveforms
487
            for waveform in sequence.sampling_information['waveforms']:
488
                if waveform not in ready_waveforms:
489
                    self.log.error('Waveform "{0}" associated with PulseSequence "{1}" not '
490
                                   'found on pulse generator device.\nPlease re-generate the '
491
                                   'PulseSequence.'.format(waveform, sequence.name))
492
                    self.sigLoadedAssetUpdated.emit(*self.loaded_asset)
493
                    return
494
            # Actually load the sequence to the generic channels
495
            self.pulsegenerator().load_sequence(sequence.name)
496
        else:
497
            self.log.error('Loading of PulseSequence "{0}" failed.\n'
498
                           'It has not been generated yet.'.format(sequence.name))
499
        self.sigLoadedAssetUpdated.emit(*self.loaded_asset)
500
        return
501
502
    def _read_settings_from_device(self):
503
        """
504
        """
505
        # Read activation_config from device.
506
        channel_state = self.pulsegenerator().get_active_channels()
507
        current_config = {chnl for chnl in channel_state if channel_state[chnl]}
508
509
        # Check if the read back config is a valid config in constraints
510
        avail_configs = self.pulse_generator_constraints.activation_config
511
        if current_config in avail_configs.values():
512
            # Read config found in constraints
513
            config_name = list(avail_configs)[list(avail_configs.values()).index(current_config)]
514
            self.__activation_config = (config_name, current_config)
515
        else:
516
            # Set first valid config if read config is not valid.
517
            config_to_set = list(avail_configs.items())[0]
518
            set_config = self._apply_activation_config(config_to_set[1])
519
            if set_config != config_to_set[1]:
520
                self.__activation_config = ('', set_config)
521
                self.log.error('Error during activation.\n'
522
                               'Unable to set activation_config that was taken from pulse '
523
                               'generator constraints.\n'
524
                               'Probably one or more activation_configs in constraints invalid.')
525
            else:
526
                self.__activation_config = config_to_set
527
528
        # Read sample rate from device
529
        self.__sample_rate = float(self.pulsegenerator().get_sample_rate())
530
531
        # Read analog levels from device
532
        self.__analog_levels = self.pulsegenerator().get_analog_level()
533
534
        # Read digital levels from device
535
        self.__digital_levels = self.pulsegenerator().get_digital_level()
536
537
        # Read interleave flag from device
538
        self.__interleave = self.pulsegenerator().get_interleave()
539
540
        # Notify new settings to listening module
541
        self.set_pulse_generator_settings()
542
        return
543
544
    def _apply_activation_config(self, activation_config):
545
        """
546
547
        @param set activation_config: A set of channels to set active (all others inactive)
548
        """
549
        channel_state = self.pulsegenerator().get_active_channels()
550
        for chnl in channel_state:
551
            if chnl in activation_config:
552
                channel_state[chnl] = True
553
            else:
554
                channel_state[chnl] = False
555
        set_state = self.pulsegenerator().set_active_channels(channel_state)
556
        set_config = set([chnl for chnl in set_state if set_state[chnl]])
557
        return set_config
558
559
    ############################################################################
560
    # Waveform/Sequence generation control methods and properties
561
    ############################################################################
562
    @property
563
    def generate_methods(self):
564
        return self._pog.predefined_generate_methods
565
566
    @property
567
    def generate_method_params(self):
568
        return self._pog.predefined_method_parameters
569
570
    @property
571
    def generation_parameters(self):
572
        return self._generation_parameters.copy()
573
574
    @generation_parameters.setter
575
    def generation_parameters(self, settings_dict):
576
        if isinstance(settings_dict, dict):
577
            self.set_generation_parameters(settings_dict)
578
        return
579
580
    @property
581
    def saved_pulse_blocks(self):
582
        return self._saved_pulse_blocks
583
584
    @property
585
    def saved_pulse_block_ensembles(self):
586
        return self._saved_pulse_block_ensembles
587
588
    @property
589
    def saved_pulse_sequences(self):
590
        return self._saved_pulse_sequences
591
592
    @QtCore.Slot(dict)
593
    def set_generation_parameters(self, settings_dict=None, **kwargs):
594
        """
595
        Either accept a settings dictionary as positional argument or keyword arguments.
596
        If both are present both are being used by updating the settings_dict with kwargs.
597
        The keyword arguments take precedence over the items in settings_dict if there are
598
        conflicting names.
599
600
        @param settings_dict:
601
        @param kwargs:
602
        @return:
603
        """
604
        # Check if generation is in progress and do nothing if that is the case
605
        if self.module_state() != 'locked':
606
            # Determine complete settings dictionary
607
            if not isinstance(settings_dict, dict):
608
                settings_dict = kwargs
609
            else:
610
                settings_dict.update(kwargs)
611
612
            # Notify if new keys have been added
613
            for key in settings_dict:
614
                if key not in self._generation_parameters:
615
                    self.log.warning('Setting by name "{0}" not present in generation_parameters.\n'
616
                                     'Will add it but this could lead to unwanted effects.'
617
                                     ''.format(key))
618
            # Sanity checks
619
            if 'laser_channel' in settings_dict:
620
                if settings_dict['laser_channel'] not in self.__activation_config[1]:
621
                    self.log.error('Unable to set laser channel "{0}".\nChannel to set is not part '
622
                                   'of the current channel activation config ({1}).'
623
                                   ''.format(settings_dict['laser_channel'],
624
                                             self.__activation_config[1]))
625
                    del settings_dict['laser_channel']
626
            if settings_dict.get('sync_channel'):
627
                if settings_dict['sync_channel'] not in self.__activation_config[1]:
628
                    self.log.error('Unable to set sync channel "{0}".\nChannel to set is not part '
629
                                   'of the current channel activation config ({1}).'
630
                                   ''.format(settings_dict['sync_channel'],
631
                                             self.__activation_config[1]))
632
                    del settings_dict['sync_channel']
633
            if settings_dict.get('gate_channel'):
634
                if settings_dict['gate_channel'] not in self.__activation_config[1]:
635
                    self.log.error('Unable to set gate channel "{0}".\nChannel to set is not part '
636
                                   'of the current channel activation config ({1}).'
637
                                   ''.format(settings_dict['gate_channel'],
638
                                             self.__activation_config[1]))
639
                    del settings_dict['gate_channel']
640
            if settings_dict.get('microwave_channel'):
641
                if settings_dict['microwave_channel'] not in self.__activation_config[1]:
642
                    self.log.error('Unable to set microwave channel "{0}".\nChannel to set is not '
643
                                   'part of the current channel activation config ({1}).'
644
                                   ''.format(settings_dict['microwave_channel'],
645
                                             self.__activation_config[1]))
646
                    del settings_dict['microwave_channel']
647
648
            # update settings dict
649
            self._generation_parameters.update(settings_dict)
650
        else:
651
            self.log.error('Unable to apply new sampling settings.\n'
652
                           'SequenceGeneratorLogic is busy generating a waveform/sequence.')
653
654
        self.sigSamplingSettingsUpdated.emit(self.generation_parameters)
655
        return self.generation_parameters
656
657
    def save_block(self, block):
658
        """ Saves a PulseBlock instance
659
660
        @param PulseBlock block: PulseBlock instance to save
661
        """
662
        self._saved_pulse_blocks[block.name] = block
663
        self._save_block_to_file(block)
664
        self.sigBlockDictUpdated.emit(self._saved_pulse_blocks)
665
        return
666
667
    def get_block(self, name):
668
        """
669
670
        @param str name:
671
        @return PulseBlock:
672
        """
673
        if name not in self._saved_pulse_blocks:
674
            self.log.warning('PulseBlock "{0}" could not be found in saved pulse blocks.\n'
675
                             'Returning None.'.format(name))
676
        return self._saved_pulse_blocks.get(name)
677
678
    def delete_block(self, name):
679
        """ Remove the serialized object "name" from the block list and HDD.
680
681
        @param name: string, name of the PulseBlock object to be removed.
682
        """
683
        # Delete from dict
684
        if name in self.saved_pulse_blocks:
685
            del(self._saved_pulse_blocks[name])
686
687
        # Delete from disk
688
        filepath = os.path.join(self._assets_storage_dir, '{0}.block'.format(name))
689
        if os.path.exists(filepath):
690
            os.remove(filepath)
691
692
        self.sigBlockDictUpdated.emit(self.saved_pulse_blocks)
693
        return
694
695
    def _load_block_from_file(self, block_name):
696
        """
697
        De-serializes a PulseBlock instance from file.
698
699
        @param str block_name: The name of the PulseBlock instance to de-serialize
700
        @return PulseBlock: The de-serialized PulseBlock instance
701
        """
702
        block = None
703
        filepath = os.path.join(self._assets_storage_dir, '{0}.block'.format(block_name))
704
        if os.path.exists(filepath):
705
            try:
706
                with open(filepath, 'rb') as file:
707
                    block = pickle.load(file)
708
            except:
709
                self.log.error('Failed to de-serialize PulseBlock "{0}" from file.'
710
                               ''.format(block_name))
711
        return block
712
713
    def _update_blocks_from_file(self):
714
        """
715
        Update the saved_pulse_blocks dict by de-serializing stored file.
716
        """
717
        # Get all files in asset directory ending on ".block" and extract a sorted list of
718
        # PulseBlock names
719
        with os.scandir(self._assets_storage_dir) as scan:
720
            names = sorted(f.name[:-6] for f in scan if f.is_file and f.name.endswith('.block'))
721
722
        # Load all blocks from file
723
        for block_name in names:
724
            block = self._load_block_from_file(block_name)
725
            if block is not None:
726
                self._saved_pulse_blocks[block_name] = block
727
728
        self.sigBlockDictUpdated.emit(self._saved_pulse_blocks)
729
        return
730
731
    def _save_block_to_file(self, block):
732
        """
733
        Saves a single PulseBlock instance to file by serialization using pickle.
734
735
        @param PulseBlock block: The PulseBlock instance to be saved
736
        """
737
        filename = '{0}.block'.format(block.name)
738
        try:
739
            with open(os.path.join(self._assets_storage_dir, filename), 'wb') as file:
740
                pickle.dump(block, file)
741
        except:
742
            self.log.error('Failed to serialize PulseBlock "{0}" to file.'.format(block.name))
743
        return
744
745
    def _save_blocks_to_file(self):
746
        """
747
        Saves the saved_pulse_blocks dict items to files.
748
        """
749
        for block in self._saved_pulse_blocks.values():
750
            self._save_block_to_file(block)
751
        return
752
753
    def save_ensemble(self, ensemble):
754
        """ Saves a PulseBlockEnsemble instance
755
756
        @param PulseBlockEnsemble ensemble: PulseBlockEnsemble instance to save
757
        """
758
        self._saved_pulse_block_ensembles[ensemble.name] = ensemble
759
        self._save_ensemble_to_file(ensemble)
760
        self.sigEnsembleDictUpdated.emit(self.saved_pulse_block_ensembles)
761
        return
762
763
    def get_ensemble(self, name):
764
        """
765
766
        @param name:
767
        @return:
768
        """
769
        if name not in self._saved_pulse_block_ensembles:
770
            self.log.warning('PulseBlockEnsemble "{0}" could not be found in saved pulse block '
771
                             'ensembles.\nReturning None.'.format(name))
772
        return self._saved_pulse_block_ensembles.get(name)
773
774 View Code Duplication
    def delete_ensemble(self, name):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
775
        """
776
        Remove the ensemble with 'name' from the ensemble dict and all associated waveforms
777
        from the pulser memory.
778
        """
779
        # Delete from dict
780
        if name in self.saved_pulse_block_ensembles:
781
            # check if ensemble has already been sampled and delete associated waveforms
782
            if self.saved_pulse_block_ensembles[name].sampling_information:
783
                self._delete_waveform(
784
                    self.saved_pulse_block_ensembles[name].sampling_information['waveforms'])
785
                self.sigAvailableWaveformsUpdated.emit(self.sampled_waveforms)
786
            # delete PulseBlockEnsemble
787
            del self._saved_pulse_block_ensembles[name]
788
789
        # Delete from disk
790
        filepath = os.path.join(self._assets_storage_dir, '{0}.ensemble'.format(name))
791
        if os.path.exists(filepath):
792
            os.remove(filepath)
793
794
        self.sigEnsembleDictUpdated.emit(self.saved_pulse_block_ensembles)
795
        return
796
797
    def _load_ensemble_from_file(self, ensemble_name):
798
        """
799
        De-serializes a PulseBlockEnsemble instance from file.
800
801
        @param str ensemble_name: The name of the PulseBlockEnsemble instance to de-serialize
802
        @return PulseBlockEnsemble: The de-serialized PulseBlockEnsemble instance
803
        """
804
        ensemble = None
805
        filepath = os.path.join(self._assets_storage_dir, '{0}.ensemble'.format(ensemble_name))
806
        if os.path.exists(filepath):
807
            try:
808
                with open(filepath, 'rb') as file:
809
                    ensemble = pickle.load(file)
810
            except:
811
                self.log.error('Failed to de-serialize PulseBlockEnsemble "{0}" from file.'
812
                               ''.format(ensemble_name))
813
        return ensemble
814
815
    def _update_ensembles_from_file(self):
816
        """
817
        Update the saved_pulse_block_ensembles dict from temporary file.
818
        """
819
        # Get all files in asset directory ending on ".ensemble" and extract a sorted list of
820
        # PulseBlockEnsemble names
821
        with os.scandir(self._assets_storage_dir) as scan:
822
            names = sorted(f.name[:-9] for f in scan if f.is_file and f.name.endswith('.ensemble'))
823
824
        # Get all waveforms currently stored on pulser hardware in order to delete outdated
825
        # sampling_information dicts
826
        sampled_waveforms = set(self.sampled_waveforms)
827
828
        # Load all ensembles from file
829
        for ensemble_name in names:
830
            ensemble = self._load_ensemble_from_file(ensemble_name)
831
            if ensemble is not None:
832
                if ensemble.sampling_information.get('waveforms'):
833
                    waveform_set = set(ensemble.sampling_information['waveforms'])
834
                    if not sampled_waveforms.issuperset(waveform_set):
835
                        ensemble.sampling_information = dict()
836
                self._saved_pulse_block_ensembles[ensemble_name] = ensemble
837
838
        self.sigEnsembleDictUpdated.emit(self.saved_pulse_block_ensembles)
839
        return
840
841
    def _save_ensemble_to_file(self, ensemble):
842
        """
843
        Saves a single PulseBlockEnsemble instance to file by serialization using pickle.
844
845
        @param PulseBlockEnsemble ensemble: The PulseBlockEnsemble instance to be saved
846
        """
847
        filename = '{0}.ensemble'.format(ensemble.name)
848
        try:
849
            with open(os.path.join(self._assets_storage_dir, filename), 'wb') as file:
850
                pickle.dump(ensemble, file)
851
        except:
852
            self.log.error('Failed to serialize PulseBlockEnsemble "{0}" to file.'
853
                           ''.format(ensemble.name))
854
        return
855
856
    def _save_ensembles_to_file(self):
857
        """
858
        Saves the saved_pulse_block_ensembles dict items to files.
859
        """
860
        for ensemble in self.saved_pulse_block_ensembles.values():
861
            self._save_ensemble_to_file(ensemble)
862
        return
863
864
    def save_sequence(self, sequence):
865
        """ Saves a PulseSequence instance
866
867
        @param object sequence: a PulseSequence object, which is going to be
868
                                serialized to file.
869
870
        @return: str: name of the serialized object, if needed.
871
        """
872
        self._saved_pulse_sequences[sequence.name] = sequence
873
        self._save_sequence_to_file(sequence)
874
        self.sigSequenceDictUpdated.emit(self.saved_pulse_sequences)
875
        return
876
877
    def get_sequence(self, name):
878
        """
879
880
        @param name:
881
        @return:
882
        """
883
        if name not in self._saved_pulse_sequences:
884
            self.log.warning('PulseSequence "{0}" could not be found in saved pulse sequences.\n'
885
                             'Returning None.'.format(name))
886
        return self._saved_pulse_sequences.get(name)
887
888 View Code Duplication
    def delete_sequence(self, name):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
889
        """
890
        Remove the sequence with 'name' from the sequence dict and all associated waveforms
891
        from the pulser memory.
892
        """
893
        if name in self.saved_pulse_sequences:
894
            # check if sequence has already been sampled and delete associated sequence from pulser.
895
            # Also delete associated waveforms if sequence has been sampled within rotating frame.
896
            if self.saved_pulse_sequences[name].sampling_information:
897
                self._delete_sequence(name)
898
                if self.saved_pulse_sequences[name].rotating_frame:
899
                    self._delete_waveform(
900
                        self.saved_pulse_sequences[name].sampling_information['waveforms'])
901
                    self.sigAvailableWaveformsUpdated.emit(self.sampled_waveforms)
902
            # delete PulseSequence
903
            del self._saved_pulse_sequences[name]
904
905
        # Delete from disk
906
        filepath = os.path.join(self._assets_storage_dir, '{0}.sequence'.format(name))
907
        if os.path.exists(filepath):
908
            os.remove(filepath)
909
910
        self.sigSequenceDictUpdated.emit(self.saved_pulse_sequences)
911
        return
912
913
    def _load_sequence_from_file(self, sequence_name):
914
        """
915
        De-serializes a PulseSequence instance from file.
916
917
        @param str sequence_name: The name of the PulseSequence instance to de-serialize
918
        @return PulseSequence: The de-serialized PulseSequence instance
919
        """
920
        sequence = None
921
        filepath = os.path.join(self._assets_storage_dir, '{0}.sequence'.format(sequence_name))
922
        if os.path.exists(filepath):
923
            try:
924
                with open(filepath, 'rb') as file:
925
                    sequence = pickle.load(file)
926
            except:
927
                self.log.error('Failed to de-serialize PulseSequence "{0}" from file.'
928
                               ''.format(sequence_name))
929
        return sequence
930
931
    def _update_sequences_from_file(self):
932
        """
933
        Update the saved_pulse_sequences dict from files.
934
        """
935
        # Get all files in asset directory ending on ".sequence" and extract a sorted list of
936
        # PulseSequence names
937
        with os.scandir(self._assets_storage_dir) as scan:
938
            names = sorted(f.name[:-9] for f in scan if f.is_file and f.name.endswith('.sequence'))
939
940
        # Get all waveforms and sequences currently stored on pulser hardware in order to delete
941
        # outdated sampling_information dicts
942
        sampled_waveforms = set(self.sampled_waveforms)
943
        sampled_sequences = set(self.sampled_sequences)
944
945
        # Load all sequences from file
946
        for sequence_name in names:
947
            sequence = self._load_sequence_from_file(sequence_name)
948
            if sequence is not None:
949
                if sequence.name not in sampled_sequences:
950
                    sequence.sampling_information = dict()
951
                elif sequence.sampling_information:
952
                    waveform_set = set(sequence.sampling_information['waveforms'])
953
                    if not sampled_waveforms.issuperset(waveform_set):
954
                        sequence.sampling_information = dict()
955
                self._saved_pulse_sequences[sequence_name] = sequence
956
957
        self.sigSequenceDictUpdated.emit(self.saved_pulse_sequences)
958
        return
959
960
    def _save_sequence_to_file(self, sequence):
961
        """
962
        Saves a single PulseSequence instance to file by serialization using pickle.
963
964
        @param PulseSequence sequence: The PulseSequence instance to be saved
965
        """
966
        filename = '{0}.sequence'.format(sequence.name)
967
        try:
968
            with open(os.path.join(self._assets_storage_dir, filename), 'wb') as file:
969
                pickle.dump(sequence, file)
970
        except:
971
            self.log.error('Failed to serialize PulseSequence "{0}" to file.'.format(sequence.name))
972
        return
973
974
    def _save_sequences_to_file(self):
975
        """
976
        Saves the saved_pulse_sequences dict items to files.
977
        """
978
        for sequence in self.saved_pulse_sequences.values():
979
            self._save_sequence_to_file(sequence)
980
        return
981
982
    def generate_predefined_sequence(self, predefined_sequence_name, kwargs_dict):
983
        """
984
985
        @param predefined_sequence_name:
986
        @param kwargs_dict:
987
        @return:
988
        """
989
        gen_method = self.generate_methods[predefined_sequence_name]
990
        gen_params = self.generate_method_params[predefined_sequence_name]
991
        # match parameters to method and throw out unwanted ones
992
        thrown_out_params = [param for param in kwargs_dict if param not in gen_params]
993
        for param in thrown_out_params:
994
            del kwargs_dict[param]
995
        if thrown_out_params:
996
            self.log.debug('Unused params during predefined sequence generation "{0}":\n'
997
                           '{1}'.format(predefined_sequence_name, thrown_out_params))
998
        try:
999
            blocks, ensembles, sequences = gen_method(**kwargs_dict)
1000
        except:
1001
            self.log.error('Generation of predefined sequence "{0}" failed.'
1002
                           ''.format(predefined_sequence_name))
1003
            raise
1004
        # Save objects
1005
        for block in blocks:
1006
            self.save_block(block)
1007
        for ensemble in ensembles:
1008
            ensemble.sampling_information = dict()
1009
            self.save_ensemble(ensemble)
1010
        for sequence in sequences:
1011
            sequence.sampling_information = dict()
1012
            self.save_sequence(sequence)
1013
        self.sigPredefinedSequenceGenerated.emit(predefined_sequence_name)
1014
        return
1015
    # ---------------------------------------------------------------------------
1016
    #                    END sequence/block generation
1017
    # ---------------------------------------------------------------------------
1018
1019
    # ---------------------------------------------------------------------------
1020
    #                    BEGIN sequence/block sampling
1021
    # ---------------------------------------------------------------------------
1022
    def get_ensemble_info(self, ensemble):
1023
        """
1024
        This helper method will analyze a PulseBlockEnsemble and return information like length in
1025
        seconds and bins (with currently set sampling rate), number of laser pulses (with currently
1026
        selected laser/gate channel)
1027
1028
        @param PulseBlockEnsemble ensemble: The PulseBlockEnsemble instance to analyze
1029
        @return (float, int, int): length in seconds, length in bins, number of laser/gate pulses
1030
        """
1031
        # variables to keep track of the current timeframe and number of laser/gate pulses
1032
        ensemble_length_s = 0.0
1033
        ensemble_length_bins = 0
1034
        number_of_lasers = 0
1035
        # memorize the channel state of the previous element.
1036
        tmp_digital_high = False
1037
1038
        # Determine the right laser channel to choose. For gated counting it should be the gate
1039
        # channel instead of the laser trigger.
1040
        laser_channel = self.generation_parameters['gate_channel'] if self.generation_parameters[
1041
            'gate_channel'] else self.generation_parameters['laser_channel']
1042
1043
        # check for active channels in last block and take the laser_channel state of the very last
1044
        # element as initial state for the tmp_digital_high. Return if the ensemble is empty
1045 View Code Duplication
        if len(ensemble.block_list) > 0:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1046
            block = self.get_block(ensemble.block_list[-1][0])
1047
            digital_channels = block.digital_channels
1048
            analog_channels = block.analog_channels
1049
            channel_set = analog_channels.union(digital_channels)
1050
            if laser_channel in channel_set:
1051
                tmp_digital_high = block.element_list[-1].digital_high[laser_channel]
1052
        else:
1053
            return ensemble_length_s, ensemble_length_bins, number_of_lasers
1054
1055
        # Loop over all blocks in the ensemble
1056
        for block_name, reps in ensemble.block_list:
1057
            block = self.get_block(block_name)
1058
            # Iterate over all repetitions of the current block
1059
            for rep_no in range(reps + 1):
1060
                # ideal end time for the sequence up until this point in sec
1061
                ensemble_length_s += block.init_length_s + rep_no * block.increment_s
1062
                if laser_channel in channel_set:
1063
                    # Iterate over the Block_Elements inside the current block
1064
                    for block_element in block.element_list:
1065
                        # save bin position if transition from low to high has occured in laser channel
1066
                        if block_element.digital_high[laser_channel] and not tmp_digital_high:
1067
                            number_of_lasers += 1
1068
                        tmp_digital_high = block_element.digital_high[laser_channel]
1069
1070
        # Nearest possible match including the discretization in bins
1071
        ensemble_length_bins = int(np.rint(ensemble_length_s * self.__sample_rate))
1072
        return ensemble_length_s, ensemble_length_bins, number_of_lasers
1073
1074
    def get_sequence_info(self, sequence):
1075
        """
1076
        This helper method will analyze a PulseSequence and return information like length in
1077
        seconds and bins (with currently set sampling rate), number of laser pulses (with currently
1078
        selected laser/gate channel)
1079
1080
        @param PulseSequence sequence: The PulseSequence instance to analyze
1081
        @return (float, int, int): length in seconds, length in bins, number of laser/gate pulses
1082
        """
1083
        length_bins = 0
1084
        length_s = 0 if sequence.is_finite else np.inf
1085
        number_of_lasers = 0 if sequence.is_finite else -1
1086
        for ensemble_name, seq_params in sequence.ensemble_list:
1087
            ensemble = self.get_ensemble(name=ensemble_name)
1088
            if ensemble is None:
1089
                length_bins = -1
1090
                length_s = np.inf
1091
                number_of_lasers = -1
1092
                break
1093
            ens_length, ens_bins, ens_lasers = self.get_ensemble_info(ensemble=ensemble)
1094
            length_bins += ens_bins
1095
            if sequence.is_finite:
1096
                length_s += ens_length * (seq_params['repetitions'] + 1)
1097
                number_of_lasers += ens_lasers * (seq_params['repetitions'] + 1)
1098
        return length_s, length_bins, number_of_lasers
1099
1100
    def analyze_block_ensemble(self, ensemble):
1101
        """
1102
        This helper method runs through each element of a PulseBlockEnsemble object and extracts
1103
        important information about the Waveform that can be created out of this object.
1104
        Especially the discretization due to the set self.sample_rate is taken into account.
1105
        The positions in time (as integer time bins) of the PulseBlockElement transitions are
1106
        determined here (all the "rounding-to-best-match-value").
1107
        Additional information like the total number of samples, total number of PulseBlockElements
1108
        and the timebins for digital channel low-to-high transitions get returned as well.
1109
1110
        This method assumes that sanity checking has been already performed on the
1111
        PulseBlockEnsemble (via _sampling_ensemble_sanity_check). Meaning it assumes that all
1112
        PulseBlocks are actually present in saved blocks and the channel activation matches the
1113
        current pulse settings.
1114
1115
        @param ensemble: A PulseBlockEnsemble object (see logic.pulse_objects.py)
1116
        @return: number_of_samples (int): The total number of samples in a Waveform provided the
1117
                                              current sample_rate and PulseBlockEnsemble object.
1118
                 total_elements (int): The total number of PulseBlockElements (incl. repetitions) in
1119
                                       the provided PulseBlockEnsemble.
1120
                 elements_length_bins (1D numpy.ndarray[int]): Array of number of timebins for each
1121
                                                               PulseBlockElement in chronological
1122
                                                               order (incl. repetitions).
1123
                 digital_rising_bins (dict): Dictionary with keys being the digital channel
1124
                                             descriptor string and items being arrays of
1125
                                             chronological low-to-high transition positions
1126
                                             (in timebins; incl. repetitions) for each digital
1127
                                             channel.
1128
        """
1129
        # memorize the channel state of the previous element
1130
        tmp_digital_high = dict()
1131
        # Set of used analog and digital channels
1132
        digital_channels = set()
1133
        analog_channels = set()
1134
        # check for active channels and initialize tmp_digital_high with the state of the very last
1135
        # element in the ensemble
1136 View Code Duplication
        if len(ensemble.block_list) > 0:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1137
            block = self.get_block(ensemble.block_list[0][0])
1138
            digital_channels = block.digital_channels
1139
            analog_channels = block.analog_channels
1140
            block = self.get_block(ensemble.block_list[-1][0])
1141
            if len(block.element_list) > 0:
1142
                tmp_digital_high = block.element_list[-1].digital_high.copy()
1143
            else:
1144
                tmp_digital_high = {chnl: False for chnl in digital_channels}
1145
1146
        # dicts containing the bins where the digital channels are rising/falling
1147
        digital_rising_bins = {chnl: list() for chnl in digital_channels}
1148
        digital_falling_bins = {chnl: list() for chnl in digital_channels}
1149
1150
        # Array to store the length in bins for all elements including repetitions in the order
1151
        # they are occuring in the waveform later on. (Must be int64 or it will overflow eventually)
1152
        elements_length_bins = np.empty(0, dtype='int64')
1153
1154
        # variables to keep track of the current timeframe
1155
        current_end_time = 0.0
1156
        current_start_bin = 0
1157
1158
        # Loop through all blocks in the ensemble block_list
1159
        for block_name, reps in ensemble.block_list:
1160
            # Get the stored PulseBlock instance
1161
            block = self.get_block(block_name)
1162
1163
            # Temporary array to hold the length in bins for all elements in the block (incl. reps)
1164
            tmp_length_bins = np.zeros((reps + 1) * len(block.element_list), dtype='int64')
1165
1166
            # Iterate over all repetitions of the current block while keeping track of the
1167
            # current element index
1168
            unrolled_element_index = 0
1169
            for rep_no in range(reps + 1):
1170
                # Iterate over the Block_Elements inside the current block
1171
                for element in block.element_list:
1172
                    # save bin position if a transition from low to high or vice versa has occured
1173
                    # in a digital channel
1174
                    if tmp_digital_high != element.digital_high:
1175
                        for chnl, state in element.digital_high.items():
1176
                            if not tmp_digital_high[chnl] and state:
1177
                                digital_rising_bins[chnl].append(current_start_bin)
1178
                            elif tmp_digital_high[chnl] and not state:
1179
                                digital_falling_bins[chnl].append(current_start_bin)
1180
                        tmp_digital_high = element.digital_high.copy()
1181
1182
                    # Calculate length of the current element with current repetition count in sec
1183
                    # and add this to the ideal end time for the sequence up until this point.
1184
                    current_end_time += element.init_length_s + rep_no * element.increment_s
1185
1186
                    # Nearest possible match including the discretization in bins
1187
                    current_end_bin = int(np.rint(current_end_time * self.__sample_rate))
1188
1189
                    # append current element length in discrete bins to temporary array
1190
                    tmp_length_bins[unrolled_element_index] = current_end_bin - current_start_bin
1191
1192
                    # advance bin offset for next element
1193
                    current_start_bin = current_end_bin
1194
                    # increment element counter
1195
                    unrolled_element_index += 1
1196
1197
            # append element lengths (in bins) for this block to array
1198
            elements_length_bins = np.append(elements_length_bins, tmp_length_bins)
1199
1200
        # convert digital rising/falling indices to numpy.ndarrays
1201
        for chnl in digital_channels:
1202
            digital_rising_bins[chnl] = np.array(digital_rising_bins[chnl], dtype='int64')
1203
            digital_falling_bins[chnl] = np.array(digital_falling_bins[chnl], dtype='int64')
1204
1205
        return_dict = dict()
1206
        return_dict['number_of_samples'] = np.sum(elements_length_bins)
1207
        return_dict['number_of_elements'] = len(elements_length_bins)
1208
        return_dict['elements_length_bins'] = elements_length_bins
1209
        return_dict['digital_rising_bins'] = digital_rising_bins
1210
        return_dict['digital_falling_bins'] = digital_falling_bins
1211
        return_dict['analog_channels'] = analog_channels
1212
        return_dict['digital_channels'] = digital_channels
1213
        return_dict['channel_set'] = analog_channels.union(digital_channels)
1214
        return_dict['generation_parameters'] = self.generation_parameters.copy()
1215
        return return_dict
1216
1217
    def analyze_sequence(self, sequence):
1218
        """
1219
        This helper method runs through each step of a PulseSequence object and extracts
1220
        important information about the Sequence that can be created out of this object.
1221
        Especially the discretization due to the set self.sample_rate is taken into account.
1222
        The positions in time (as integer time bins) of the PulseBlockElement transitions are
1223
        determined here (all the "rounding-to-best-match-value").
1224
        Additional information like the total number of samples, total number of PulseBlockElements
1225
        and the timebins for digital channel low-to-high transitions get returned as well.
1226
1227
        This method assumes that sanity checking has been already performed on the
1228
        PulseSequence (via _sampling_ensemble_sanity_check). Meaning it assumes that all
1229
        PulseBlocks are actually present in saved blocks and the channel activation matches the
1230
        current pulse settings.
1231
1232
        @param sequence: A PulseSequence object (see logic.pulse_objects.py)
1233
        @return: number_of_samples (int): The total number of samples in a Waveform provided the
1234
                                              current sample_rate and PulseBlockEnsemble object.
1235
                 total_elements (int): The total number of PulseBlockElements (incl. repetitions) in
1236
                                       the provided PulseBlockEnsemble.
1237
                 elements_length_bins (1D numpy.ndarray[int]): Array of number of timebins for each
1238
                                                               PulseBlockElement in chronological
1239
                                                               order (incl. repetitions).
1240
                 digital_rising_bins (dict): Dictionary with keys being the digital channel
1241
                                             descriptor string and items being arrays of
1242
                                             chronological low-to-high transition positions
1243
                                             (in timebins; incl. repetitions) for each digital
1244
                                             channel.
1245
        """
1246
        # Determine channel activation
1247
        digital_channels = set()
1248
        analog_channels = set()
1249
        if len(sequence.ensemble_list) > 0:
1250
            ensemble = self.get_ensemble(sequence.ensemble_list[0][0])
1251
            if len(ensemble.block_list) > 0:
1252
                block = self.get_block(ensemble.block_list[0][0])
1253
                digital_channels = block.digital_channels
1254
                analog_channels = block.analog_channels
1255
1256
        # If the sequence does not contain infinite loop steps, determine the remaining parameters
1257
        # TODO: Implement this!
1258
        length_bins = 0
1259
        length_s = 0 if sequence.is_finite else np.inf
1260
        for ensemble_name, seq_params in sequence.ensemble_list:
1261
            ensemble = self.get_ensemble(name=ensemble_name)
1262
            ens_length, ens_bins, ens_lasers = self.get_ensemble_info(ensemble=ensemble)
1263
            length_bins += ens_bins
1264
            if sequence.is_finite:
1265
                length_s += ens_length * (seq_params['repetitions'] + 1)
1266
1267
        return_dict = dict()
1268
        return_dict['digital_channels'] = digital_channels
1269
        return_dict['analog_channels'] = analog_channels
1270
        return_dict['channel_set'] = analog_channels.union(digital_channels)
1271
        return_dict['generation_parameters'] = self.generation_parameters.copy()
1272
        return return_dict
1273
1274
    def _sampling_ensemble_sanity_check(self, ensemble):
1275
        blocks_missing = set()
1276
        channel_activation_mismatch = False
1277
        for block_name, reps in ensemble.block_list:
1278
            block = self._saved_pulse_blocks.get(block_name)
1279
            # Check if block is present
1280
            if block is None:
1281
                blocks_missing.add(block_name)
1282
                continue
1283
            # Check for matching channel activation
1284
            if block.channel_set != self.__activation_config[1]:
1285
                channel_activation_mismatch = True
1286
1287
        # print error messages
1288
        if len(blocks_missing) > 0:
1289
            self.log.error('Sampling of PulseBlockEnsemble "{0}" failed. Not all PulseBlocks found.'
1290
                           '\nPlease generate the following PulseBlocks: {1}'
1291
                           ''.format(ensemble.name, blocks_missing))
1292
        if channel_activation_mismatch:
1293
            self.log.error('Sampling of PulseBlockEnsemble "{0}" failed!\nMismatch of activation '
1294
                           'config in logic ({1}) and used channels in PulseBlockEnsemble.'
1295
                           ''.format(ensemble.name, self.__activation_config[1]))
1296
1297
        # Return error code
1298
        return -1 if blocks_missing or channel_activation_mismatch else 0
1299
1300
    def _sampling_sequence_sanity_check(self, sequence):
1301
        ensembles_missing = set()
1302
        for ensemble_name, seq_params in sequence.ensemble_list:
1303
            ensemble = self._saved_pulse_block_ensembles.get(ensemble_name)
1304
            # Check if ensemble is present
1305
            if ensemble is None:
1306
                ensembles_missing.add(ensemble_name)
1307
                continue
1308
1309
        # print error messages
1310
        if len(ensembles_missing) > 0:
1311
            self.log.error('Sampling of PulseSequence "{0}" failed. Not all PulseBlockEnsembles '
1312
                           'found.\nPlease generate the following PulseBlockEnsembles: {1}'
1313
                           ''.format(sequence.name, ensembles_missing))
1314
1315
        # Return error code
1316
        return -1 if ensembles_missing else 0
1317
1318
    @QtCore.Slot(str)
1319
    def sample_pulse_block_ensemble(self, ensemble, offset_bin=0, name_tag=None):
1320
        """ General sampling of a PulseBlockEnsemble object, which serves as the construction plan.
1321
1322
        @param str|PulseBlockEnsemble ensemble: PulseBlockEnsemble instance or name of a saved
1323
                                                PulseBlockEnsemble to sample
1324
        @param int offset_bin: If many pulse ensembles are samples sequentially, then the
1325
                               offset_bin of the previous sampling can be passed to maintain
1326
                               rotating frame across pulse_block_ensembles
1327
        @param str name_tag: a name tag, which is used to keep the sampled files together, which
1328
                             where sampled from the same PulseBlockEnsemble object but where
1329
                             different offset_bins were used.
1330
1331
        @return tuple: of length 3 with
1332
                       (offset_bin, created_waveforms, ensemble_info).
1333
                        offset_bin:
1334
                            integer, which is used for maintaining the rotation frame
1335
                        created_waveforms:
1336
                            list, a list of created waveform names
1337
                        ensemble_info:
1338
                            dict, information about the ensemble returned by analyze_block_ensemble
1339
1340
        This method is creating the actual samples (voltages and logic states) for each time step
1341
        of the analog and digital channels specified in the PulseBlockEnsemble.
1342
        Therefore it iterates through all blocks, repetitions and elements of the ensemble and
1343
        calculates the exact voltages (float64) according to the specified math_function. The
1344
        samples are later on stored inside a float32 array.
1345
        So each element is calculated with high precision (float64) and then down-converted to
1346
        float32 to be stored.
1347
1348
        To preserve the rotating frame, an offset counter is used to indicate the absolute time
1349
        within the ensemble. All calculations are done with time bins (dtype=int) to avoid rounding
1350
        errors. Only in the last step when a single PulseBlockElement object is sampled  these
1351
        integer bin values are translated into a floating point time.
1352
1353
        The chunkwise write mode is used to save memory usage at the expense of time.
1354
        In other words: The whole sample arrays are never created at any time. This results in more
1355
        function calls and general overhead causing much longer time to complete.
1356
1357
        In addition the pulse_block_ensemble gets analyzed and important parameters used during
1358
        sampling get stored in the ensemble object "sampling_information" attribute.
1359
        It is a dictionary containing:
1360
        TODO: Add parameters that are stored
1361
        """
1362
        # Get PulseBlockEnsemble from saved ensembles if string has been passed as argument
1363
        if isinstance(ensemble, str):
1364
            ensemble = self.get_ensemble(ensemble)
1365
            if not ensemble:
1366
                self.log.error('Unable to sample PulseBlockEnsemble. Not found in saved ensembles.')
1367
                self.sigSampleEnsembleComplete.emit(None)
1368
                return -1, list()
1369
1370
        # Perform sanity checks on ensemble and corresponding blocks
1371
        if self._sampling_ensemble_sanity_check(ensemble) < 0:
1372
            self.sigSampleEnsembleComplete.emit(None)
1373
            return -1, list()
1374
1375
        # lock module if it's not already locked (sequence sampling in progress)
1376
        if self.module_state() == 'idle':
1377
            self.module_state.lock()
1378
        elif not self.__sequence_generation_in_progress:
1379
            self.sigSampleEnsembleComplete.emit(None)
1380
            return -1, list()
1381
1382
        # Set the waveform name (excluding the device specific channel naming suffix, i.e. '_ch1')
1383
        waveform_name = name_tag if name_tag else ensemble.name
1384
1385
        # check for old waveforms associated with the ensemble and delete them from pulse generator.
1386
        self._delete_waveform_by_nametag(waveform_name)
1387
1388
        # Take current time
1389
        start_time = time.time()
1390
1391
        # get important parameters from the ensemble
1392
        ensemble_info = self.analyze_block_ensemble(ensemble)
1393
1394
        # Calculate the byte size per sample.
1395
        # One analog sample per channel is 4 bytes (np.float32) and one digital sample per channel
1396
        # is 1 byte (np.bool).
1397
        bytes_per_sample = len(ensemble_info['analog_channels']) * 4 + len(
1398
            ensemble_info['digital_channels'])
1399
1400
        # Calculate the bytes estimate for the entire ensemble
1401
        bytes_per_ensemble = bytes_per_sample * ensemble_info['number_of_samples']
1402
1403
        # Determine the size of the sample arrays to be written as a whole.
1404
        if bytes_per_ensemble <= self._overhead_bytes or self._overhead_bytes == 0:
1405
            array_length = ensemble_info['number_of_samples']
1406
        else:
1407
            array_length = self._overhead_bytes // bytes_per_sample
1408
1409
        # Allocate the sample arrays that are used for a single write command
1410
        analog_samples = dict()
1411
        digital_samples = dict()
1412
        try:
1413
            for chnl in ensemble_info['analog_channels']:
1414
                analog_samples[chnl] = np.empty(array_length, dtype='float32')
1415
            for chnl in ensemble_info['digital_channels']:
1416
                digital_samples[chnl] = np.empty(array_length, dtype=bool)
1417
        except MemoryError:
1418
            self.log.error('Sampling of PulseBlockEnsemble "{0}" failed due to a MemoryError.\n'
1419
                           'The sample array needed is too large to allocate in memory.\n'
1420
                           'Try using the overhead_bytes ConfigOption to limit memory usage.'
1421
                           ''.format(ensemble.name))
1422
            if not self.__sequence_generation_in_progress:
1423
                self.module_state.unlock()
1424
            self.sigSampleEnsembleComplete.emit(None)
1425
            return -1, list()
1426
1427
        # integer to keep track of the sampls already processed
1428
        processed_samples = 0
1429
        # Index to keep track of the samples written into the preallocated samples array
1430
        array_write_index = 0
1431
        # Keep track of the number of elements already written
1432
        element_count = 0
1433
        # set of written waveform names on the device
1434
        written_waveforms = set()
1435
        # Iterate over all blocks within the PulseBlockEnsemble object
1436
        for block_name, reps in ensemble.block_list:
1437
            block = self.get_block(block_name)
1438
            # Iterate over all repetitions of the current block
1439
            for rep_no in range(reps + 1):
1440
                # Iterate over the PulseBlockElement instances inside the current block
1441
                for element in block.element_list:
1442
                    digital_high = element.digital_high
1443
                    pulse_function = element.pulse_function
1444
                    element_length_bins = ensemble_info['elements_length_bins'][element_count]
1445
1446
                    # Indicator on how many samples of this element have been written already
1447
                    element_samples_written = 0
1448
1449
                    while element_samples_written != element_length_bins:
1450
                        samples_to_add = min(array_length - array_write_index,
1451
                                             element_length_bins - element_samples_written)
1452
                        # create floating point time array for the current element inside rotating
1453
                        # frame if analog samples are to be calculated.
1454
                        if pulse_function:
1455
                            time_arr = (offset_bin + np.arange(
1456
                                samples_to_add, dtype='float64')) / self.__sample_rate
1457
1458
                        # Calculate respective part of the sample arrays
1459
                        for chnl in digital_high:
1460
                            digital_samples[chnl][array_write_index:array_write_index+samples_to_add] = digital_high[chnl]
1461
                        for chnl in pulse_function:
1462
                            analog_samples[chnl][array_write_index:array_write_index+samples_to_add] = pulse_function[chnl].get_samples(time_arr)/self.__analog_levels[0][chnl]
1463
1464
                        # Free memory
1465
                        if pulse_function:
1466
                            del time_arr
1467
1468
                        element_samples_written += samples_to_add
1469
                        array_write_index += samples_to_add
1470
                        processed_samples += samples_to_add
1471
                        # if the rotating frame should be preserved (default) increment the offset
1472
                        # counter for the time array.
1473
                        if ensemble.rotating_frame:
1474
                            offset_bin += samples_to_add
1475
1476
                        # Check if the temporary sample array is full and write to the device if so.
1477
                        if array_write_index == array_length:
1478
                            # Set first/last chunk flags
1479
                            is_first_chunk = array_write_index == processed_samples
1480
                            is_last_chunk = processed_samples == ensemble_info['number_of_samples']
1481
                            written_samples, wfm_list = self.pulsegenerator().write_waveform(
1482
                                name=waveform_name,
1483
                                analog_samples=analog_samples,
1484
                                digital_samples=digital_samples,
1485
                                is_first_chunk=is_first_chunk,
1486
                                is_last_chunk=is_last_chunk,
1487
                                total_number_of_samples=ensemble_info['number_of_samples'])
1488
1489
                            # Update written waveforms set
1490
                            written_waveforms.update(wfm_list)
1491
1492
                            # check if write process was successful
1493
                            if written_samples != array_length:
1494
                                self.log.error('Sampling of block "{0}" in ensemble "{1}" failed. '
1495
                                               'Write to device was unsuccessful.'
1496
                                               ''.format(block_name, ensemble.name))
1497
                                if not self.__sequence_generation_in_progress:
1498
                                    self.module_state.unlock()
1499
                                self.sigAvailableWaveformsUpdated.emit(self.sampled_waveforms)
1500
                                self.sigSampleEnsembleComplete.emit(None)
1501
                                return -1, list()
1502
1503
                            # Reset array write start pointer
1504
                            array_write_index = 0
1505
1506
                            # check if the temporary write array needs to be truncated for the next
1507
                            # part. (because it is the last part of the ensemble to write which can
1508
                            # be shorter than the previous chunks)
1509
                            if array_length > ensemble_info['number_of_samples'] - processed_samples:
1510
                                array_length = ensemble_info['number_of_samples'] - processed_samples
1511
                                analog_samples = dict()
1512
                                digital_samples = dict()
1513
                                for chnl in ensemble_info['analog_channels']:
1514
                                    analog_samples[chnl] = np.empty(array_length, dtype='float32')
1515
                                for chnl in ensemble_info['digital_channels']:
1516
                                    digital_samples[chnl] = np.empty(array_length, dtype=bool)
1517
1518
                    # Increment element index
1519
                    element_count += 1
1520
1521
        # Save sampling related parameters to the sampling_information container within the
1522
        # PulseBlockEnsemble.
1523
        # This step is only performed if the resulting waveforms are named by the PulseBlockEnsemble
1524
        # and not by a sequence nametag
1525
        if waveform_name == ensemble.name:
1526
            ensemble.sampling_information = dict()
1527
            ensemble.sampling_information.update(ensemble_info)
1528
            ensemble.sampling_information['pulse_generator_settings'] = self.pulse_generator_settings
1529
            ensemble.sampling_information['waveforms'] = sorted(written_waveforms)
1530
            self.save_ensemble(ensemble)
1531
1532
        self.log.info('Time needed for sampling and writing PulseBlockEnsemble to device: {0} sec'
1533
                      ''.format(int(np.rint(time.time() - start_time))))
1534
        if not self.__sequence_generation_in_progress:
1535
            self.module_state.unlock()
1536
        self.sigAvailableWaveformsUpdated.emit(self.sampled_waveforms)
1537
        self.sigSampleEnsembleComplete.emit(ensemble)
1538
        return offset_bin, list(written_waveforms), ensemble_info
1539
1540
    @QtCore.Slot(str)
1541
    def sample_pulse_sequence(self, sequence):
1542
        """ Samples the PulseSequence object, which serves as the construction plan.
1543
1544
        @param str|PulseSequence sequence: Name or instance of the PulseSequence to be sampled.
1545
1546
        The sequence object is sampled by call subsequently the sampling routine for the
1547
        PulseBlockEnsemble objects and passing if needed the rotating frame option.
1548
1549
        Right now two 'simple' methods of sampling where implemented, which reuse the sample
1550
        function for the Pulse_Block_Ensembles. One, which samples by preserving the phase (i.e.
1551
        staying in the rotating frame) and the other which samples without keep a phase
1552
        relationship between the different entries of the PulseSequence object.
1553
        ATTENTION: The phase preservation within a single PulseBlockEnsemble is NOT affected by
1554
                   this method.
1555
1556
        More sophisticated sequence sampling method can be implemented here.
1557
        """
1558
        # Get PulseSequence from saved sequences if string has been passed as argument
1559
        if isinstance(sequence, str):
1560
            sequence = self.get_sequence(sequence)
1561
            if not sequence:
1562
                self.log.error('Unable to sample PulseSequence. Not found in saved sequences.')
1563
                self.sigSampleSequenceComplete.emit(None)
1564
                return
1565
1566
        # Perform sanity checks on sequence and corresponding ensembles
1567
        if self._sampling_sequence_sanity_check(sequence) < 0:
1568
            self.sigSampleSequenceComplete.emit(None)
1569
            return
1570
1571
        # lock module and set sequence-generation-in-progress flag
1572
        if self.module_state() == 'idle':
1573
            self.__sequence_generation_in_progress = True
1574
            self.module_state.lock()
1575
        else:
1576
            self.log.error('Cannot sample sequence "{0}" because the SequenceGeneratorLogic is '
1577
                           'still busy (locked).\nFunction call ignored.'.format(sequence.name))
1578
            self.sigSampleSequenceComplete.emit(None)
1579
            return
1580
1581
        self._saved_pulse_sequences[sequence.name] = sequence
1582
1583
        # delete already written sequences on the device memory.
1584
        if sequence.name in self.sampled_sequences:
1585
            self.pulsegenerator().delete_sequence(sequence.name)
1586
1587
        # Make sure the PulseSequence is contained in the saved sequences dict
1588
        sequence.sampling_information = dict()
1589
        self.save_sequence(sequence)
1590
1591
        # Take current time
1592
        start_time = time.time()
1593
1594
        # Produce a set of created waveforms
1595
        written_waveforms = set()
1596
        # Keep track of generated PulseBlockEnsembles and their corresponding ensemble_info dict
1597
        generated_ensembles = dict()
1598
1599
        # Create a list in the process with each element holding the created waveform names as a
1600
        # tuple and the corresponding sequence parameters as defined in the PulseSequence object
1601
        # Example: [(('waveform1', 'waveform2'), seq_param_dict1),
1602
        #           (('waveform3', 'waveform4'), seq_param_dict2)]
1603
        sequence_param_dict_list = list()
1604
1605
        # if all the Pulse_Block_Ensembles should be in the rotating frame, then each ensemble
1606
        # will be created in general with a different offset_bin. Therefore, in order to keep track
1607
        # of the sampled Pulse_Block_Ensembles one has to introduce a running number as an
1608
        # additional name tag, so keep the sampled files separate.
1609
        offset_bin = 0  # that will be used for phase preservation
1610
        for sequence_step, (ensemble_name, seq_param) in enumerate(sequence.ensemble_list):
1611
            if sequence.rotating_frame:
1612
                # to make something like 001
1613
                name_tag = sequence.name + '_' + str(sequence_step).zfill(3)
1614
            else:
1615
                name_tag = None
1616
                offset_bin = 0  # Keep the offset at 0
1617
1618
            # Only sample ensembles if they have not already been sampled
1619
            if sequence.rotating_frame or ensemble_name not in generated_ensembles:
1620
                offset_bin, waveform_list, ensemble_info = self.sample_pulse_block_ensemble(
1621
                    ensemble=ensemble_name,
1622
                    offset_bin=offset_bin,
1623
                    name_tag=name_tag)
1624
1625
                if len(waveform_list) == 0:
1626
                    self.log.error('Sampling of PulseBlockEnsemble "{0}" failed during sampling of '
1627
                                   'PulseSequence "{1}".\nFailed to create waveforms on device.'
1628
                                   ''.format(ensemble_name, sequence.name))
1629
                    self.module_state.unlock()
1630
                    self.__sequence_generation_in_progress = False
1631
                    self.sigSampleSequenceComplete.emit(None)
1632
                    return
1633
1634
                # Add to generated ensembles
1635
                ensemble_info['waveforms'] = waveform_list
1636
                generated_ensembles[name_tag] = ensemble_info
1637
1638
                # Add created waveform names to the set
1639
                written_waveforms.update(waveform_list)
1640
1641
            # Append written sequence step to sequence_param_dict_list
1642
            sequence_param_dict_list.append(
1643
                (tuple(generated_ensembles[name_tag]['waveforms']), seq_param))
1644
1645
        # pass the whole information to the sequence creation method:
1646
        steps_written = self.pulsegenerator().write_sequence(sequence.name,
1647
                                                             sequence_param_dict_list)
1648
        if steps_written != len(sequence_param_dict_list):
1649
            self.log.error('Writing PulseSequence "{0}" to the device memory failed.\n'
1650
                           'Returned number of sequence steps ({1:d}) does not match desired '
1651
                           'number of steps ({2:d}).'.format(sequence.name,
1652
                                                             steps_written,
1653
                                                             len(sequence_param_dict_list)))
1654
1655
        # get important parameters from the sequence and save them to the sequence object
1656
        sequence.sampling_information.update(self.analyze_sequence(sequence))
1657
        sequence.sampling_information['ensemble_info'] = generated_ensembles
1658
        sequence.sampling_information['pulse_generator_settings'] = self.pulse_generator_settings
1659
        sequence.sampling_information['waveforms'] = sorted(written_waveforms)
1660
        sequence.sampling_information['step_parameters'] = sequence_param_dict_list
1661
        self.save_sequence(sequence)
1662
1663
        self.log.info('Time needed for sampling and writing PulseSequence to device: {0} sec.'
1664
                      ''.format(int(np.rint(time.time() - start_time))))
1665
1666
        # unlock module
1667
        self.module_state.unlock()
1668
        self.__sequence_generation_in_progress = False
1669
        self.sigAvailableSequencesUpdated.emit(self.sampled_sequences)
1670
        self.sigSampleSequenceComplete.emit(sequence)
1671
        return
1672
1673
    def _delete_waveform(self, names):
1674
        if isinstance(names, str):
1675
            names = [names]
1676
        current_waveforms = self.sampled_waveforms
1677
        for wfm in names:
1678
            if wfm in current_waveforms:
1679
                self.pulsegenerator().delete_waveform(wfm)
1680
        self.sigAvailableWaveformsUpdated.emit(self.sampled_waveforms)
1681
        return
1682
1683
    def _delete_waveform_by_nametag(self, nametag):
1684
        if not isinstance(nametag, str):
1685
            return
1686
        wfm_to_delete = [wfm for wfm in self.sampled_waveforms if
1687
                         wfm.rsplit('_', 1)[0] == nametag]
1688
        self._delete_waveform(wfm_to_delete)
1689
        # Erase sampling information if a PulseBlockEnsemble by the same name can be found in saved
1690
        # ensembles
1691
        if nametag in self.saved_pulse_block_ensembles:
1692
            ensemble = self.saved_pulse_block_ensembles[nametag]
1693
            ensemble.sampling_information = dict()
1694
            self.save_ensemble(ensemble)
1695
        return
1696
1697
    def _delete_sequence(self, names):
1698
        if isinstance(names, str):
1699
            names = [names]
1700
        current_sequences = self.sampled_sequences
1701
        for seq in names:
1702
            if seq in current_sequences:
1703
                self.pulsegenerator().delete_sequence(seq)
1704
        self.sigAvailableSequencesUpdated.emit(self.sampled_sequences)
1705
        return
1706