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

SequenceGeneratorLogic   F

Complexity

Total Complexity 298

Size/Duplication

Total Lines 1679
Duplicated Lines 8.46 %

Importance

Changes 31
Bugs 0 Features 0
Metric Value
dl 142
loc 1679
rs 0.8
c 31
b 0
f 0
wmc 298

56 Methods

Rating   Name   Duplication   Size   Complexity  
A saved_pulse_blocks() 0 3 1
A pulse_generator_constraints() 0 3 1
A sampled_sequences() 0 3 1
A sampled_waveforms() 0 3 1
A generation_parameters() 0 3 2
A saved_pulse_block_ensembles() 0 3 1
A analog_channels() 0 3 3
A digital_channels() 0 3 3
A on_deactivate() 0 4 1
A saved_pulse_sequences() 0 3 1
A generate_method_params() 0 3 1
A generate_methods() 0 3 1
F set_pulse_generator_settings() 0 110 30
B load_ensemble() 37 37 7
A delete_ensemble() 22 22 4
A save_sequence() 0 12 1
B _update_blocks_from_file() 0 17 7
A _save_block_to_file() 0 13 3
A _save_sequence_to_file() 0 13 3
A _delete_sequence() 0 9 4
C _update_ensembles_from_file() 0 25 9
B analyze_sequence() 0 56 6
F sample_pulse_block_ensemble() 0 221 30
A _load_block_from_file() 0 17 4
A _save_ensembles_to_file() 0 7 2
B __init__() 0 37 4
A _sampling_sequence_sanity_check() 0 17 5
B _sampling_ensemble_sanity_check() 0 25 7
C _update_sequences_from_file() 0 28 10
D get_ensemble_info() 13 62 12
C generate_predefined_sequence() 0 33 9
C loaded_asset() 0 19 9
A delete_block() 0 16 3
A get_block() 0 10 2
A _apply_activation_config() 0 14 5
A clear_pulser() 0 18 3
F sample_pulse_sequence() 0 132 12
A _save_sequences_to_file() 0 7 2
A get_ensemble() 0 10 2
A on_activate() 0 29 3
B delete_sequence() 24 24 5
A _save_blocks_to_file() 0 7 2
B load_sequence() 37 37 8
A save_block() 0 9 1
B _read_settings_from_device() 0 41 5
A _delete_waveform_by_nametag() 0 13 5
A _load_sequence_from_file() 0 17 4
F analyze_block_ensemble() 9 116 16
A _load_ensemble_from_file() 0 17 4
A _delete_waveform() 0 9 4
B get_sequence_info() 0 25 6
A get_sequence() 0 10 2
A pulse_generator_settings() 0 9 2
A save_ensemble() 0 9 1
A _save_ensemble_to_file() 0 14 3
D set_generation_parameters() 0 64 13

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SequenceGeneratorLogic 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
                if laser_channel.startswith('a'):
1052
                    tmp_digital_high = type(
1053
                        block.element_list[-1].pulse_function[laser_channel]).__name__ != 'Idle'
1054
                else:
1055
                    tmp_digital_high = block.element_list[-1].digital_high[laser_channel]
1056
        else:
1057
            return ensemble_length_s, ensemble_length_bins, number_of_lasers
1058
1059
        # Loop over all blocks in the ensemble
1060
        for block_name, reps in ensemble.block_list:
1061
            block = self.get_block(block_name)
1062
            # Iterate over all repetitions of the current block
1063
            for rep_no in range(reps + 1):
1064
                # ideal end time for the sequence up until this point in sec
1065
                ensemble_length_s += block.init_length_s + rep_no * block.increment_s
1066
                if laser_channel in channel_set:
1067
                    # Iterate over the Block_Elements inside the current block
1068
                    for block_element in block.element_list:
1069
                        # save bin position if transition from low to high has occured in
1070
                        # laser channel
1071
                        if laser_channel.startswith('a'):
1072
                            is_high = type(
1073
                                block_element.pulse_function[laser_channel]).__name__ != 'Idle'
1074
                        else:
1075
                            is_high = block_element.digital_high[laser_channel]
1076
1077
                        if is_high and not tmp_digital_high:
1078
                            number_of_lasers += 1
1079
                        tmp_digital_high = is_high
1080
1081
        # Nearest possible match including the discretization in bins
1082
        ensemble_length_bins = int(np.rint(ensemble_length_s * self.__sample_rate))
1083
        return ensemble_length_s, ensemble_length_bins, number_of_lasers
1084
1085
    def get_sequence_info(self, sequence):
1086
        """
1087
        This helper method will analyze a PulseSequence and return information like length in
1088
        seconds and bins (with currently set sampling rate), number of laser pulses (with currently
1089
        selected laser/gate channel)
1090
1091
        @param PulseSequence sequence: The PulseSequence instance to analyze
1092
        @return (float, int, int): length in seconds, length in bins, number of laser/gate pulses
1093
        """
1094
        length_bins = 0
1095
        length_s = 0 if sequence.is_finite else np.inf
1096
        number_of_lasers = 0 if sequence.is_finite else -1
1097
        for ensemble_name, seq_params in sequence.ensemble_list:
1098
            ensemble = self.get_ensemble(name=ensemble_name)
1099
            if ensemble is None:
1100
                length_bins = -1
1101
                length_s = np.inf
1102
                number_of_lasers = -1
1103
                break
1104
            ens_length, ens_bins, ens_lasers = self.get_ensemble_info(ensemble=ensemble)
1105
            length_bins += ens_bins
1106
            if sequence.is_finite:
1107
                length_s += ens_length * (seq_params['repetitions'] + 1)
1108
                number_of_lasers += ens_lasers * (seq_params['repetitions'] + 1)
1109
        return length_s, length_bins, number_of_lasers
1110
1111
    def analyze_block_ensemble(self, ensemble):
1112
        """
1113
        This helper method runs through each element of a PulseBlockEnsemble object and extracts
1114
        important information about the Waveform that can be created out of this object.
1115
        Especially the discretization due to the set self.sample_rate is taken into account.
1116
        The positions in time (as integer time bins) of the PulseBlockElement transitions are
1117
        determined here (all the "rounding-to-best-match-value").
1118
        Additional information like the total number of samples, total number of PulseBlockElements
1119
        and the timebins for digital channel low-to-high transitions get returned as well.
1120
1121
        This method assumes that sanity checking has been already performed on the
1122
        PulseBlockEnsemble (via _sampling_ensemble_sanity_check). Meaning it assumes that all
1123
        PulseBlocks are actually present in saved blocks and the channel activation matches the
1124
        current pulse settings.
1125
1126
        @param ensemble: A PulseBlockEnsemble object (see logic.pulse_objects.py)
1127
        @return: number_of_samples (int): The total number of samples in a Waveform provided the
1128
                                              current sample_rate and PulseBlockEnsemble object.
1129
                 total_elements (int): The total number of PulseBlockElements (incl. repetitions) in
1130
                                       the provided PulseBlockEnsemble.
1131
                 elements_length_bins (1D numpy.ndarray[int]): Array of number of timebins for each
1132
                                                               PulseBlockElement in chronological
1133
                                                               order (incl. repetitions).
1134
                 digital_rising_bins (dict): Dictionary with keys being the digital channel
1135
                                             descriptor string and items being arrays of
1136
                                             chronological low-to-high transition positions
1137
                                             (in timebins; incl. repetitions) for each digital
1138
                                             channel.
1139
        """
1140
        # memorize the channel state of the previous element
1141
        tmp_digital_high = dict()
1142
        # Set of used analog and digital channels
1143
        digital_channels = set()
1144
        analog_channels = set()
1145
        # check for active channels and initialize tmp_digital_high with the state of the very last
1146
        # element in the ensemble
1147 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...
1148
            block = self.get_block(ensemble.block_list[0][0])
1149
            digital_channels = block.digital_channels
1150
            analog_channels = block.analog_channels
1151
            block = self.get_block(ensemble.block_list[-1][0])
1152
            if len(block.element_list) > 0:
1153
                tmp_digital_high = block.element_list[-1].digital_high.copy()
1154
            else:
1155
                tmp_digital_high = {chnl: False for chnl in digital_channels}
1156
1157
        # dicts containing the bins where the digital channels are rising/falling
1158
        digital_rising_bins = {chnl: list() for chnl in digital_channels}
1159
        digital_falling_bins = {chnl: list() for chnl in digital_channels}
1160
1161
        # Array to store the length in bins for all elements including repetitions in the order
1162
        # they are occuring in the waveform later on. (Must be int64 or it will overflow eventually)
1163
        elements_length_bins = np.empty(0, dtype='int64')
1164
1165
        # variables to keep track of the current timeframe
1166
        current_end_time = 0.0
1167
        current_start_bin = 0
1168
1169
        # Loop through all blocks in the ensemble block_list
1170
        for block_name, reps in ensemble.block_list:
1171
            # Get the stored PulseBlock instance
1172
            block = self.get_block(block_name)
1173
1174
            # Temporary array to hold the length in bins for all elements in the block (incl. reps)
1175
            tmp_length_bins = np.zeros((reps + 1) * len(block.element_list), dtype='int64')
1176
1177
            # Iterate over all repetitions of the current block while keeping track of the
1178
            # current element index
1179
            unrolled_element_index = 0
1180
            for rep_no in range(reps + 1):
1181
                # Iterate over the Block_Elements inside the current block
1182
                for element in block.element_list:
1183
                    # save bin position if a transition from low to high or vice versa has occured
1184
                    # in a digital channel
1185
                    if tmp_digital_high != element.digital_high:
1186
                        for chnl, state in element.digital_high.items():
1187
                            if not tmp_digital_high[chnl] and state:
1188
                                digital_rising_bins[chnl].append(current_start_bin)
1189
                            elif tmp_digital_high[chnl] and not state:
1190
                                digital_falling_bins[chnl].append(current_start_bin)
1191
                        tmp_digital_high = element.digital_high.copy()
1192
1193
                    # Calculate length of the current element with current repetition count in sec
1194
                    # and add this to the ideal end time for the sequence up until this point.
1195
                    current_end_time += element.init_length_s + rep_no * element.increment_s
1196
1197
                    # Nearest possible match including the discretization in bins
1198
                    current_end_bin = int(np.rint(current_end_time * self.__sample_rate))
1199
1200
                    # append current element length in discrete bins to temporary array
1201
                    tmp_length_bins[unrolled_element_index] = current_end_bin - current_start_bin
1202
1203
                    # advance bin offset for next element
1204
                    current_start_bin = current_end_bin
1205
                    # increment element counter
1206
                    unrolled_element_index += 1
1207
1208
            # append element lengths (in bins) for this block to array
1209
            elements_length_bins = np.append(elements_length_bins, tmp_length_bins)
1210
1211
        # convert digital rising/falling indices to numpy.ndarrays
1212
        for chnl in digital_channels:
1213
            digital_rising_bins[chnl] = np.array(digital_rising_bins[chnl], dtype='int64')
1214
            digital_falling_bins[chnl] = np.array(digital_falling_bins[chnl], dtype='int64')
1215
1216
        return_dict = dict()
1217
        return_dict['number_of_samples'] = np.sum(elements_length_bins)
1218
        return_dict['number_of_elements'] = len(elements_length_bins)
1219
        return_dict['elements_length_bins'] = elements_length_bins
1220
        return_dict['digital_rising_bins'] = digital_rising_bins
1221
        return_dict['digital_falling_bins'] = digital_falling_bins
1222
        return_dict['analog_channels'] = analog_channels
1223
        return_dict['digital_channels'] = digital_channels
1224
        return_dict['channel_set'] = analog_channels.union(digital_channels)
1225
        return_dict['generation_parameters'] = self.generation_parameters.copy()
1226
        return return_dict
1227
1228
    def analyze_sequence(self, sequence):
1229
        """
1230
        This helper method runs through each step of a PulseSequence object and extracts
1231
        important information about the Sequence that can be created out of this object.
1232
        Especially the discretization due to the set self.sample_rate is taken into account.
1233
        The positions in time (as integer time bins) of the PulseBlockElement transitions are
1234
        determined here (all the "rounding-to-best-match-value").
1235
        Additional information like the total number of samples, total number of PulseBlockElements
1236
        and the timebins for digital channel low-to-high transitions get returned as well.
1237
1238
        This method assumes that sanity checking has been already performed on the
1239
        PulseSequence (via _sampling_ensemble_sanity_check). Meaning it assumes that all
1240
        PulseBlocks are actually present in saved blocks and the channel activation matches the
1241
        current pulse settings.
1242
1243
        @param sequence: A PulseSequence object (see logic.pulse_objects.py)
1244
        @return: number_of_samples (int): The total number of samples in a Waveform provided the
1245
                                              current sample_rate and PulseBlockEnsemble object.
1246
                 total_elements (int): The total number of PulseBlockElements (incl. repetitions) in
1247
                                       the provided PulseBlockEnsemble.
1248
                 elements_length_bins (1D numpy.ndarray[int]): Array of number of timebins for each
1249
                                                               PulseBlockElement in chronological
1250
                                                               order (incl. repetitions).
1251
                 digital_rising_bins (dict): Dictionary with keys being the digital channel
1252
                                             descriptor string and items being arrays of
1253
                                             chronological low-to-high transition positions
1254
                                             (in timebins; incl. repetitions) for each digital
1255
                                             channel.
1256
        """
1257
        # Determine channel activation
1258
        digital_channels = set()
1259
        analog_channels = set()
1260
        if len(sequence.ensemble_list) > 0:
1261
            ensemble = self.get_ensemble(sequence.ensemble_list[0][0])
1262
            if len(ensemble.block_list) > 0:
1263
                block = self.get_block(ensemble.block_list[0][0])
1264
                digital_channels = block.digital_channels
1265
                analog_channels = block.analog_channels
1266
1267
        # If the sequence does not contain infinite loop steps, determine the remaining parameters
1268
        # TODO: Implement this!
1269
        length_bins = 0
1270
        length_s = 0 if sequence.is_finite else np.inf
1271
        for ensemble_name, seq_params in sequence.ensemble_list:
1272
            ensemble = self.get_ensemble(name=ensemble_name)
1273
            ens_length, ens_bins, ens_lasers = self.get_ensemble_info(ensemble=ensemble)
1274
            length_bins += ens_bins
1275
            if sequence.is_finite:
1276
                length_s += ens_length * (seq_params['repetitions'] + 1)
1277
1278
        return_dict = dict()
1279
        return_dict['digital_channels'] = digital_channels
1280
        return_dict['analog_channels'] = analog_channels
1281
        return_dict['channel_set'] = analog_channels.union(digital_channels)
1282
        return_dict['generation_parameters'] = self.generation_parameters.copy()
1283
        return return_dict
1284
1285
    def _sampling_ensemble_sanity_check(self, ensemble):
1286
        blocks_missing = set()
1287
        channel_activation_mismatch = False
1288
        for block_name, reps in ensemble.block_list:
1289
            block = self._saved_pulse_blocks.get(block_name)
1290
            # Check if block is present
1291
            if block is None:
1292
                blocks_missing.add(block_name)
1293
                continue
1294
            # Check for matching channel activation
1295
            if block.channel_set != self.__activation_config[1]:
1296
                channel_activation_mismatch = True
1297
1298
        # print error messages
1299
        if len(blocks_missing) > 0:
1300
            self.log.error('Sampling of PulseBlockEnsemble "{0}" failed. Not all PulseBlocks found.'
1301
                           '\nPlease generate the following PulseBlocks: {1}'
1302
                           ''.format(ensemble.name, blocks_missing))
1303
        if channel_activation_mismatch:
1304
            self.log.error('Sampling of PulseBlockEnsemble "{0}" failed!\nMismatch of activation '
1305
                           'config in logic ({1}) and used channels in PulseBlockEnsemble.'
1306
                           ''.format(ensemble.name, self.__activation_config[1]))
1307
1308
        # Return error code
1309
        return -1 if blocks_missing or channel_activation_mismatch else 0
1310
1311
    def _sampling_sequence_sanity_check(self, sequence):
1312
        ensembles_missing = set()
1313
        for ensemble_name, seq_params in sequence.ensemble_list:
1314
            ensemble = self._saved_pulse_block_ensembles.get(ensemble_name)
1315
            # Check if ensemble is present
1316
            if ensemble is None:
1317
                ensembles_missing.add(ensemble_name)
1318
                continue
1319
1320
        # print error messages
1321
        if len(ensembles_missing) > 0:
1322
            self.log.error('Sampling of PulseSequence "{0}" failed. Not all PulseBlockEnsembles '
1323
                           'found.\nPlease generate the following PulseBlockEnsembles: {1}'
1324
                           ''.format(sequence.name, ensembles_missing))
1325
1326
        # Return error code
1327
        return -1 if ensembles_missing else 0
1328
1329
    @QtCore.Slot(str)
1330
    def sample_pulse_block_ensemble(self, ensemble, offset_bin=0, name_tag=None):
1331
        """ General sampling of a PulseBlockEnsemble object, which serves as the construction plan.
1332
1333
        @param str|PulseBlockEnsemble ensemble: PulseBlockEnsemble instance or name of a saved
1334
                                                PulseBlockEnsemble to sample
1335
        @param int offset_bin: If many pulse ensembles are samples sequentially, then the
1336
                               offset_bin of the previous sampling can be passed to maintain
1337
                               rotating frame across pulse_block_ensembles
1338
        @param str name_tag: a name tag, which is used to keep the sampled files together, which
1339
                             where sampled from the same PulseBlockEnsemble object but where
1340
                             different offset_bins were used.
1341
1342
        @return tuple: of length 3 with
1343
                       (offset_bin, created_waveforms, ensemble_info).
1344
                        offset_bin:
1345
                            integer, which is used for maintaining the rotation frame
1346
                        created_waveforms:
1347
                            list, a list of created waveform names
1348
                        ensemble_info:
1349
                            dict, information about the ensemble returned by analyze_block_ensemble
1350
1351
        This method is creating the actual samples (voltages and logic states) for each time step
1352
        of the analog and digital channels specified in the PulseBlockEnsemble.
1353
        Therefore it iterates through all blocks, repetitions and elements of the ensemble and
1354
        calculates the exact voltages (float64) according to the specified math_function. The
1355
        samples are later on stored inside a float32 array.
1356
        So each element is calculated with high precision (float64) and then down-converted to
1357
        float32 to be stored.
1358
1359
        To preserve the rotating frame, an offset counter is used to indicate the absolute time
1360
        within the ensemble. All calculations are done with time bins (dtype=int) to avoid rounding
1361
        errors. Only in the last step when a single PulseBlockElement object is sampled  these
1362
        integer bin values are translated into a floating point time.
1363
1364
        The chunkwise write mode is used to save memory usage at the expense of time.
1365
        In other words: The whole sample arrays are never created at any time. This results in more
1366
        function calls and general overhead causing much longer time to complete.
1367
1368
        In addition the pulse_block_ensemble gets analyzed and important parameters used during
1369
        sampling get stored in the ensemble object "sampling_information" attribute.
1370
        It is a dictionary containing:
1371
        TODO: Add parameters that are stored
1372
        """
1373
        # Get PulseBlockEnsemble from saved ensembles if string has been passed as argument
1374
        if isinstance(ensemble, str):
1375
            ensemble = self.get_ensemble(ensemble)
1376
            if not ensemble:
1377
                self.log.error('Unable to sample PulseBlockEnsemble. Not found in saved ensembles.')
1378
                self.sigSampleEnsembleComplete.emit(None)
1379
                return -1, list()
1380
1381
        # Perform sanity checks on ensemble and corresponding blocks
1382
        if self._sampling_ensemble_sanity_check(ensemble) < 0:
1383
            self.sigSampleEnsembleComplete.emit(None)
1384
            return -1, list()
1385
1386
        # lock module if it's not already locked (sequence sampling in progress)
1387
        if self.module_state() == 'idle':
1388
            self.module_state.lock()
1389
        elif not self.__sequence_generation_in_progress:
1390
            self.sigSampleEnsembleComplete.emit(None)
1391
            return -1, list()
1392
1393
        # Set the waveform name (excluding the device specific channel naming suffix, i.e. '_ch1')
1394
        waveform_name = name_tag if name_tag else ensemble.name
1395
1396
        # check for old waveforms associated with the ensemble and delete them from pulse generator.
1397
        self._delete_waveform_by_nametag(waveform_name)
1398
1399
        # Take current time
1400
        start_time = time.time()
1401
1402
        # get important parameters from the ensemble
1403
        ensemble_info = self.analyze_block_ensemble(ensemble)
1404
1405
        # Calculate the byte size per sample.
1406
        # One analog sample per channel is 4 bytes (np.float32) and one digital sample per channel
1407
        # is 1 byte (np.bool).
1408
        bytes_per_sample = len(ensemble_info['analog_channels']) * 4 + len(
1409
            ensemble_info['digital_channels'])
1410
1411
        # Calculate the bytes estimate for the entire ensemble
1412
        bytes_per_ensemble = bytes_per_sample * ensemble_info['number_of_samples']
1413
1414
        # Determine the size of the sample arrays to be written as a whole.
1415
        if bytes_per_ensemble <= self._overhead_bytes or self._overhead_bytes == 0:
1416
            array_length = ensemble_info['number_of_samples']
1417
        else:
1418
            array_length = self._overhead_bytes // bytes_per_sample
1419
1420
        # Allocate the sample arrays that are used for a single write command
1421
        analog_samples = dict()
1422
        digital_samples = dict()
1423
        try:
1424
            for chnl in ensemble_info['analog_channels']:
1425
                analog_samples[chnl] = np.empty(array_length, dtype='float32')
1426
            for chnl in ensemble_info['digital_channels']:
1427
                digital_samples[chnl] = np.empty(array_length, dtype=bool)
1428
        except MemoryError:
1429
            self.log.error('Sampling of PulseBlockEnsemble "{0}" failed due to a MemoryError.\n'
1430
                           'The sample array needed is too large to allocate in memory.\n'
1431
                           'Try using the overhead_bytes ConfigOption to limit memory usage.'
1432
                           ''.format(ensemble.name))
1433
            if not self.__sequence_generation_in_progress:
1434
                self.module_state.unlock()
1435
            self.sigSampleEnsembleComplete.emit(None)
1436
            return -1, list()
1437
1438
        # integer to keep track of the sampls already processed
1439
        processed_samples = 0
1440
        # Index to keep track of the samples written into the preallocated samples array
1441
        array_write_index = 0
1442
        # Keep track of the number of elements already written
1443
        element_count = 0
1444
        # set of written waveform names on the device
1445
        written_waveforms = set()
1446
        # Iterate over all blocks within the PulseBlockEnsemble object
1447
        for block_name, reps in ensemble.block_list:
1448
            block = self.get_block(block_name)
1449
            # Iterate over all repetitions of the current block
1450
            for rep_no in range(reps + 1):
1451
                # Iterate over the PulseBlockElement instances inside the current block
1452
                for element in block.element_list:
1453
                    digital_high = element.digital_high
1454
                    pulse_function = element.pulse_function
1455
                    element_length_bins = ensemble_info['elements_length_bins'][element_count]
1456
1457
                    # Indicator on how many samples of this element have been written already
1458
                    element_samples_written = 0
1459
1460
                    while element_samples_written != element_length_bins:
1461
                        samples_to_add = min(array_length - array_write_index,
1462
                                             element_length_bins - element_samples_written)
1463
                        # create floating point time array for the current element inside rotating
1464
                        # frame if analog samples are to be calculated.
1465
                        if pulse_function:
1466
                            time_arr = (offset_bin + np.arange(
1467
                                samples_to_add, dtype='float64')) / self.__sample_rate
1468
1469
                        # Calculate respective part of the sample arrays
1470
                        for chnl in digital_high:
1471
                            digital_samples[chnl][array_write_index:array_write_index+samples_to_add] = digital_high[chnl]
1472
                        for chnl in pulse_function:
1473
                            analog_samples[chnl][array_write_index:array_write_index+samples_to_add] = pulse_function[chnl].get_samples(time_arr)/self.__analog_levels[0][chnl]
1474
1475
                        # Free memory
1476
                        if pulse_function:
1477
                            del time_arr
1478
1479
                        element_samples_written += samples_to_add
1480
                        array_write_index += samples_to_add
1481
                        processed_samples += samples_to_add
1482
                        # if the rotating frame should be preserved (default) increment the offset
1483
                        # counter for the time array.
1484
                        if ensemble.rotating_frame:
1485
                            offset_bin += samples_to_add
1486
1487
                        # Check if the temporary sample array is full and write to the device if so.
1488
                        if array_write_index == array_length:
1489
                            # Set first/last chunk flags
1490
                            is_first_chunk = array_write_index == processed_samples
1491
                            is_last_chunk = processed_samples == ensemble_info['number_of_samples']
1492
                            written_samples, wfm_list = self.pulsegenerator().write_waveform(
1493
                                name=waveform_name,
1494
                                analog_samples=analog_samples,
1495
                                digital_samples=digital_samples,
1496
                                is_first_chunk=is_first_chunk,
1497
                                is_last_chunk=is_last_chunk,
1498
                                total_number_of_samples=ensemble_info['number_of_samples'])
1499
1500
                            # Update written waveforms set
1501
                            written_waveforms.update(wfm_list)
1502
1503
                            # check if write process was successful
1504
                            if written_samples != array_length:
1505
                                self.log.error('Sampling of block "{0}" in ensemble "{1}" failed. '
1506
                                               'Write to device was unsuccessful.'
1507
                                               ''.format(block_name, ensemble.name))
1508
                                if not self.__sequence_generation_in_progress:
1509
                                    self.module_state.unlock()
1510
                                self.sigAvailableWaveformsUpdated.emit(self.sampled_waveforms)
1511
                                self.sigSampleEnsembleComplete.emit(None)
1512
                                return -1, list()
1513
1514
                            # Reset array write start pointer
1515
                            array_write_index = 0
1516
1517
                            # check if the temporary write array needs to be truncated for the next
1518
                            # part. (because it is the last part of the ensemble to write which can
1519
                            # be shorter than the previous chunks)
1520
                            if array_length > ensemble_info['number_of_samples'] - processed_samples:
1521
                                array_length = ensemble_info['number_of_samples'] - processed_samples
1522
                                analog_samples = dict()
1523
                                digital_samples = dict()
1524
                                for chnl in ensemble_info['analog_channels']:
1525
                                    analog_samples[chnl] = np.empty(array_length, dtype='float32')
1526
                                for chnl in ensemble_info['digital_channels']:
1527
                                    digital_samples[chnl] = np.empty(array_length, dtype=bool)
1528
1529
                    # Increment element index
1530
                    element_count += 1
1531
1532
        # Save sampling related parameters to the sampling_information container within the
1533
        # PulseBlockEnsemble.
1534
        # This step is only performed if the resulting waveforms are named by the PulseBlockEnsemble
1535
        # and not by a sequence nametag
1536
        if waveform_name == ensemble.name:
1537
            ensemble.sampling_information = dict()
1538
            ensemble.sampling_information.update(ensemble_info)
1539
            ensemble.sampling_information['pulse_generator_settings'] = self.pulse_generator_settings
1540
            ensemble.sampling_information['waveforms'] = sorted(written_waveforms)
1541
            self.save_ensemble(ensemble)
1542
1543
        self.log.info('Time needed for sampling and writing PulseBlockEnsemble to device: {0} sec'
1544
                      ''.format(int(np.rint(time.time() - start_time))))
1545
        if not self.__sequence_generation_in_progress:
1546
            self.module_state.unlock()
1547
        self.sigAvailableWaveformsUpdated.emit(self.sampled_waveforms)
1548
        self.sigSampleEnsembleComplete.emit(ensemble)
1549
        return offset_bin, sorted(written_waveforms), ensemble_info
1550
1551
    @QtCore.Slot(str)
1552
    def sample_pulse_sequence(self, sequence):
1553
        """ Samples the PulseSequence object, which serves as the construction plan.
1554
1555
        @param str|PulseSequence sequence: Name or instance of the PulseSequence to be sampled.
1556
1557
        The sequence object is sampled by call subsequently the sampling routine for the
1558
        PulseBlockEnsemble objects and passing if needed the rotating frame option.
1559
1560
        Right now two 'simple' methods of sampling where implemented, which reuse the sample
1561
        function for the Pulse_Block_Ensembles. One, which samples by preserving the phase (i.e.
1562
        staying in the rotating frame) and the other which samples without keep a phase
1563
        relationship between the different entries of the PulseSequence object.
1564
        ATTENTION: The phase preservation within a single PulseBlockEnsemble is NOT affected by
1565
                   this method.
1566
1567
        More sophisticated sequence sampling method can be implemented here.
1568
        """
1569
        # Get PulseSequence from saved sequences if string has been passed as argument
1570
        if isinstance(sequence, str):
1571
            sequence = self.get_sequence(sequence)
1572
            if not sequence:
1573
                self.log.error('Unable to sample PulseSequence. Not found in saved sequences.')
1574
                self.sigSampleSequenceComplete.emit(None)
1575
                return
1576
1577
        # Perform sanity checks on sequence and corresponding ensembles
1578
        if self._sampling_sequence_sanity_check(sequence) < 0:
1579
            self.sigSampleSequenceComplete.emit(None)
1580
            return
1581
1582
        # lock module and set sequence-generation-in-progress flag
1583
        if self.module_state() == 'idle':
1584
            self.__sequence_generation_in_progress = True
1585
            self.module_state.lock()
1586
        else:
1587
            self.log.error('Cannot sample sequence "{0}" because the SequenceGeneratorLogic is '
1588
                           'still busy (locked).\nFunction call ignored.'.format(sequence.name))
1589
            self.sigSampleSequenceComplete.emit(None)
1590
            return
1591
1592
        self._saved_pulse_sequences[sequence.name] = sequence
1593
1594
        # delete already written sequences on the device memory.
1595
        if sequence.name in self.sampled_sequences:
1596
            self.pulsegenerator().delete_sequence(sequence.name)
1597
1598
        # Make sure the PulseSequence is contained in the saved sequences dict
1599
        sequence.sampling_information = dict()
1600
        self.save_sequence(sequence)
1601
1602
        # Take current time
1603
        start_time = time.time()
1604
1605
        # Produce a set of created waveforms
1606
        written_waveforms = set()
1607
        # Keep track of generated PulseBlockEnsembles and their corresponding ensemble_info dict
1608
        generated_ensembles = dict()
1609
1610
        # Create a list in the process with each element holding the created waveform names as a
1611
        # tuple and the corresponding sequence parameters as defined in the PulseSequence object
1612
        # Example: [(('waveform1', 'waveform2'), seq_param_dict1),
1613
        #           (('waveform3', 'waveform4'), seq_param_dict2)]
1614
        sequence_param_dict_list = list()
1615
1616
        # if all the Pulse_Block_Ensembles should be in the rotating frame, then each ensemble
1617
        # will be created in general with a different offset_bin. Therefore, in order to keep track
1618
        # of the sampled Pulse_Block_Ensembles one has to introduce a running number as an
1619
        # additional name tag, so keep the sampled files separate.
1620
        offset_bin = 0  # that will be used for phase preservation
1621
        for sequence_step, (ensemble_name, seq_param) in enumerate(sequence.ensemble_list):
1622
            if sequence.rotating_frame:
1623
                # to make something like 001
1624
                name_tag = sequence.name + '_' + str(sequence_step).zfill(3)
1625
            else:
1626
                name_tag = None
1627
                offset_bin = 0  # Keep the offset at 0
1628
1629
            # Only sample ensembles if they have not already been sampled
1630
            if sequence.rotating_frame or ensemble_name not in generated_ensembles:
1631
                offset_bin, waveform_list, ensemble_info = self.sample_pulse_block_ensemble(
1632
                    ensemble=ensemble_name,
1633
                    offset_bin=offset_bin,
1634
                    name_tag=name_tag)
1635
1636
                if len(waveform_list) == 0:
1637
                    self.log.error('Sampling of PulseBlockEnsemble "{0}" failed during sampling of '
1638
                                   'PulseSequence "{1}".\nFailed to create waveforms on device.'
1639
                                   ''.format(ensemble_name, sequence.name))
1640
                    self.module_state.unlock()
1641
                    self.__sequence_generation_in_progress = False
1642
                    self.sigSampleSequenceComplete.emit(None)
1643
                    return
1644
1645
                # Add to generated ensembles
1646
                ensemble_info['waveforms'] = waveform_list
1647
                generated_ensembles[name_tag] = ensemble_info
1648
1649
                # Add created waveform names to the set
1650
                written_waveforms.update(waveform_list)
1651
1652
            # Append written sequence step to sequence_param_dict_list
1653
            sequence_param_dict_list.append(
1654
                (tuple(generated_ensembles[name_tag]['waveforms']), seq_param))
1655
1656
        # pass the whole information to the sequence creation method:
1657
        steps_written = self.pulsegenerator().write_sequence(sequence.name,
1658
                                                             sequence_param_dict_list)
1659
        if steps_written != len(sequence_param_dict_list):
1660
            self.log.error('Writing PulseSequence "{0}" to the device memory failed.\n'
1661
                           'Returned number of sequence steps ({1:d}) does not match desired '
1662
                           'number of steps ({2:d}).'.format(sequence.name,
1663
                                                             steps_written,
1664
                                                             len(sequence_param_dict_list)))
1665
1666
        # get important parameters from the sequence and save them to the sequence object
1667
        sequence.sampling_information.update(self.analyze_sequence(sequence))
1668
        sequence.sampling_information['ensemble_info'] = generated_ensembles
1669
        sequence.sampling_information['pulse_generator_settings'] = self.pulse_generator_settings
1670
        sequence.sampling_information['waveforms'] = sorted(written_waveforms)
1671
        sequence.sampling_information['step_parameters'] = sequence_param_dict_list
1672
        self.save_sequence(sequence)
1673
1674
        self.log.info('Time needed for sampling and writing PulseSequence to device: {0} sec.'
1675
                      ''.format(int(np.rint(time.time() - start_time))))
1676
1677
        # unlock module
1678
        self.module_state.unlock()
1679
        self.__sequence_generation_in_progress = False
1680
        self.sigAvailableSequencesUpdated.emit(self.sampled_sequences)
1681
        self.sigSampleSequenceComplete.emit(sequence)
1682
        return
1683
1684
    def _delete_waveform(self, names):
1685
        if isinstance(names, str):
1686
            names = [names]
1687
        current_waveforms = self.sampled_waveforms
1688
        for wfm in names:
1689
            if wfm in current_waveforms:
1690
                self.pulsegenerator().delete_waveform(wfm)
1691
        self.sigAvailableWaveformsUpdated.emit(self.sampled_waveforms)
1692
        return
1693
1694
    def _delete_waveform_by_nametag(self, nametag):
1695
        if not isinstance(nametag, str):
1696
            return
1697
        wfm_to_delete = [wfm for wfm in self.sampled_waveforms if
1698
                         wfm.rsplit('_', 1)[0] == nametag]
1699
        self._delete_waveform(wfm_to_delete)
1700
        # Erase sampling information if a PulseBlockEnsemble by the same name can be found in saved
1701
        # ensembles
1702
        if nametag in self.saved_pulse_block_ensembles:
1703
            ensemble = self.saved_pulse_block_ensembles[nametag]
1704
            ensemble.sampling_information = dict()
1705
            self.save_ensemble(ensemble)
1706
        return
1707
1708
    def _delete_sequence(self, names):
1709
        if isinstance(names, str):
1710
            names = [names]
1711
        current_sequences = self.sampled_sequences
1712
        for seq in names:
1713
            if seq in current_sequences:
1714
                self.pulsegenerator().delete_sequence(seq)
1715
        self.sigAvailableSequencesUpdated.emit(self.sampled_sequences)
1716
        return
1717