Completed
Push — pulsed_with_queued_connections ( 6b1460...30fbbf )
by
unknown
02:58
created

SequenceGeneratorLogic.get_pulse_block()   C

Complexity

Conditions 7

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
c 1
b 0
f 0
dl 0
loc 31
rs 5.5

1 Method

Rating   Name   Duplication   Size   Complexity  
A SequenceGeneratorLogic.save_ensemble() 0 14 1
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 pickle
25
import os
26
import time
27
from qtpy import QtCore
28
from collections import OrderedDict
29
import inspect
30
import importlib
31
32
from logic.pulse_objects import PulseBlockElement
33
from logic.pulse_objects import PulseBlock
34
from logic.pulse_objects import PulseBlockEnsemble
35
from logic.pulse_objects import PulseSequence
36
from logic.generic_logic import GenericLogic
37
from logic.sampling_functions import SamplingFunctions
38
from logic.samples_write_methods import SamplesWriteMethods
39
40
41
class SequenceGeneratorLogic(GenericLogic, SamplingFunctions, SamplesWriteMethods):
42
    """unstable: Nikolas Tomek
43
    This is the Logic class for the pulse (sequence) generation.
44
45
    The basis communication with the GUI should be done as follows:
46
    The logic holds all the created objects in its internal lists. The GUI is
47
    able to view this list and get the element of this list.
48
49
    How the logic will contruct its objects according to configuration dicts.
50
    The configuration dicts contain essentially, which parameters of either the
51
    PulseBlockElement objects or the PulseBlock objects can be changed and
52
    set via the GUI.
53
54
    In the end the information transfer happend through lists (read by the GUI)
55
    and dicts (set by the GUI). The logic sets(creats) the objects in the list
56
    and read the dict, which tell it which parameters to expect from the GUI.
57
    """
58
59
    _modclass = 'sequencegeneratorlogic'
60
    _modtype = 'logic'
61
62
    ## declare connectors
63
    _out = {'sequencegenerator': 'SequenceGeneratorLogic'}
64
65
66
    # define signals
67
    sigBlockDictUpdated = QtCore.Signal(dict)
68
    sigEnsembleDictUpdated = QtCore.Signal(dict)
69
    sigSequenceDictUpdated = QtCore.Signal(dict)
70
    sigSampleEnsembleComplete = QtCore.Signal(str)
71
    sigSampleSequenceComplete = QtCore.Signal(str)
72
    sigCurrentBlockUpdated = QtCore.Signal(object)
73
    sigCurrentEnsembleUpdated = QtCore.Signal(object)
74
    sigCurrentSequenceUpdated = QtCore.Signal(object)
75
    sigSettingsUpdated = QtCore.Signal(list, str, float, dict)
76
77
    def __init__(self, config, **kwargs):
78
        super().__init__(config=config, **kwargs)
79
80
        self.log.info('The following configuration was found.')
81
82
        # checking for the right configuration
83
        for key in config.keys():
84
            self.log.info('{0}: {1}'.format(key,config[key]))
85
86
        # Get all the attributes from the SamplingFunctions module:
87
        SamplingFunctions.__init__(self)
88
        # Get all the attributes from the SamplesWriteMethods module:
89
        SamplesWriteMethods.__init__(self)
90
91
        # here the currently shown data objects of the editors should be stored
92
        self.current_block = None
93
        self.current_ensemble = None
94
        self.current_sequence = None
95
96
        # The created PulseBlock objects are saved in this dictionary. The keys are the names.
97
        self.saved_pulse_blocks = OrderedDict()
98
        # The created PulseBlockEnsemble objects are saved in this dictionary.
99 View Code Duplication
        # The keys are the names.
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
100
        self.saved_pulse_block_ensembles = OrderedDict()
101
        # The created Sequence objects are saved in this dictionary. The keys are the names.
102
        self.saved_pulse_sequences = OrderedDict()
103
104
        if 'pulsed_file_dir' in config.keys():
105
            self.pulsed_file_dir = config['pulsed_file_dir']
106
            if not os.path.exists(self.pulsed_file_dir):
107
                homedir = self.get_home_dir()
108
                self.pulsed_file_dir = os.path.join(homedir, 'pulsed_files')
109
                self.log.warning('The directort defined in "pulsed_file_dir" '
110
                        'in the config for SequenceGeneratorLogic class does '
111
                        'not exist!\n'
112
                        'The default home directory\n{0}\n will be '
113
                        'taken instead.'.format(self.pulsed_file_dir))
114
        else:
115
            homedir = self.get_home_dir()
116
            self.pulsed_file_dir = os.path.join(homedir, 'pulsed_files')
117
            self.log.warning('No directory with the attribute '
118
                    '"pulsed_file_dir" is defined for the '
119
                    'SequenceGeneratorLogic!\n'
120
                    'The default home directory\n{0}\n will be taken '
121
                    'instead.'.format(self.pulsed_file_dir))
122
123
124
        self.block_dir = self._get_dir_for_name('pulse_block_objects')
125
        self.ensemble_dir = self._get_dir_for_name('pulse_ensemble_objects')
126
        self.sequence_dir = self._get_dir_for_name('sequence_objects')
127
        self.waveform_dir = self._get_dir_for_name('sampled_hardware_files')
128
        self.temp_dir = self._get_dir_for_name('temporary_files')
129
130
        # Information on used channel configuration for sequence generation
131
        # IMPORTANT: THIS CONFIG DOES NOT REPRESENT THE ACTUAL SETTINGS ON THE HARDWARE
132
        self.analog_channels = 2
133
        self.digital_channels = 4
134
        self.activation_config = ['a_ch1', 'd_ch1', 'd_ch2', 'a_ch2', 'd_ch3', 'd_ch4']
135
        self.laser_channel = 'd_ch1'
136
        self.amplitude_dict = OrderedDict()
137
        self.amplitude_dict['a_ch1'] = 0.5
138
        self.amplitude_dict['a_ch2'] = 0.5
139
        self.amplitude_dict['a_ch3'] = 0.5
140
        self.amplitude_dict['a_ch4'] = 0.5
141
        self.sample_rate = 25e9
142
        # The file format for the sampled hardware-compatible waveforms and sequences
143
        self.waveform_format = 'wfmx' # can be 'wfmx', 'wfm' or 'fpga'
144
        self.sequence_format = 'seqx' # can be 'seqx' or 'seq'
145
146
    def on_activate(self, e):
147
        """ Initialisation performed during activation of the module.
148
149
        @param object e: Event class object from Fysom.
150
                         An object created by the state machine module Fysom,
151
                         which is connected to a specific event (have a look in
152
                         the Base Class). This object contains the passed event,
153
                         the state before the event happened and the destination
154
                         of the state which should be reached after the event
155
                         had happened.
156
        """
157
        self._get_blocks_from_file()
158
        self._get_ensembles_from_file()
159
        self._get_sequences_from_file()
160
161
        self._attach_predefined_methods()
162
163
        if 'activation_config' in self._statusVariables:
164
            self.activation_config = self._statusVariables['activation_config']
165
        if 'laser_channel' in self._statusVariables:
166
            self.laser_channel = self._statusVariables['laser_channel']
167
        if 'amplitude_dict' in self._statusVariables:
168
            self.amplitude_dict = self._statusVariables['amplitude_dict']
169
        if 'sample_rate' in self._statusVariables:
170
            self.sample_rate = self._statusVariables['sample_rate']
171
        if 'waveform_format' in self._statusVariables:
172
            self.waveform_format = self._statusVariables['waveform_format']
173
        if 'sequence_format' in self._statusVariables:
174
            self.sequence_format = self._statusVariables['sequence_format']
175
        self.sigSettingsUpdated.emit(self.activation_config, self.laser_channel, self.sample_rate,
176
                                     self.amplitude_dict)
177
178
    def on_deactivate(self, e):
179
        """ Deinitialisation performed during deactivation of the module.
180
181
        @param object e: Event class object from Fysom. A more detailed
182
                         explanation can be found in method activation.
183
        """
184
        self._statusVariables['activation_config'] = self.activation_config
185
        self._statusVariables['laser_channel'] = self.laser_channel
186
        self._statusVariables['amplitude_dict'] = self.amplitude_dict
187
        self._statusVariables['sample_rate'] = self.sample_rate
188
        self._statusVariables['waveform_format'] = self.waveform_format
189
        self._statusVariables['sequence_format'] = self.sequence_format
190
191
    def _attach_predefined_methods(self):
192
        """
193
        Retrieve in the folder all files for predefined methods and attach their methods to the
194
195
        @return:
196
        """
197
        self.predefined_method_list = []
198
        filename_list = []
199
        # The assumption is that in the directory predefined_methods, there are
200
        # *.py files, which contain only methods!
201
        path = os.path.join(self.get_main_dir(), 'logic', 'predefined_methods')
202
        for entry in os.listdir(path):
203
            if os.path.isfile(os.path.join(path, entry)) and entry.endswith('.py'):
204
                filename_list.append(entry[:-3])
205
206
        for filename in filename_list:
207
            mod = importlib.import_module('logic.predefined_methods.{0}'.format(filename))
208
209
            for method in dir(mod):
210
                try:
211
                    # Check for callable function or method:
212
                    ref = getattr(mod, method)
213
                    if callable(ref) and (inspect.ismethod(ref) or inspect.isfunction(ref)):
214
                        # Bind the method as an attribute to the Class
215
                        setattr(SequenceGeneratorLogic, method, getattr(mod, method))
216
217
                        self.predefined_method_list.append(eval('self.'+method))
218
                except:
219
                    self.log.error('It was not possible to import element {0} from {1} into '
220
                                   'SequenceGenerationLogic.'.format(method,filename))
221
        return
222
223
    def _get_dir_for_name(self, name):
224
        """ Get the path to the pulsed sub-directory 'name'.
225
226
        @param str name: name of the folder
227
        @return: str, absolute path to the directory with folder 'name'.
228
        """
229
        path = os.path.join(self.pulsed_file_dir, name)
230
        if not os.path.exists(path):
231
            os.makedirs(os.path.abspath(path))
232
        return os.path.abspath(path)
233
234
    def request_init_values(self):
235
        """
236
237
        @return:
238
        """
239
        self.sigBlockDictUpdated.emit(self.saved_pulse_blocks)
240
        self.sigEnsembleDictUpdated.emit(self.saved_pulse_block_ensembles)
241
        self.sigSequenceDictUpdated.emit(self.saved_pulse_sequences)
242
        self.sigSampleEnsembleComplete.emit('')
243
        self.sigSampleSequenceComplete.emit('')
244
        self.sigCurrentBlockUpdated.emit(self.current_block)
245
        self.sigCurrentEnsembleUpdated.emit(self.current_ensemble)
246
        self.sigCurrentSequenceUpdated.emit(self.current_sequence)
247
        self.sigSettingsUpdated.emit(self.activation_config, self.laser_channel, self.sample_rate,
248
                                     self.amplitude_dict)
249
        return
250
251
    def set_settings(self, activation_config, laser_channel, sample_rate, amplitude_dict):
252
        """
253
        Sets all settings for the generator logic.
254
255
        @param activation_config:
256
        @param laser_channel:
257
        @param sample_rate:
258
        @param amplitude_dict:
259
        @return:
260
        """
261
        # check if the currently chosen laser channel is part of the config and adjust if this
262
        # is not the case. Choose first digital channel in that case.
263
        if laser_channel not in activation_config:
264
            laser_channel = None
265
            for channel in activation_config:
266
                if 'd_ch' in channel:
267
                    laser_channel = channel
268
                    break
269
            if laser_channel is None:
270
                self.log.warning('No digital channel present in sequence generator activation '
271
                                 'config.')
272
        self.laser_channel = laser_channel
273
        self.activation_config = activation_config
274
        self.analog_channels = len([chnl for chnl in activation_config if 'a_ch' in chnl])
275
        self.digital_channels = len([chnl for chnl in activation_config if 'd_ch' in chnl])
276
        self.amplitude_dict = amplitude_dict
277
        self.sample_rate = sample_rate
278
        self.sigSettingsUpdated.emit(activation_config, laser_channel, sample_rate, amplitude_dict)
279
        return self.activation_config, self.laser_channel, self.sample_rate, self.amplitude_dict
280
281
# -----------------------------------------------------------------------------
282
#                    BEGIN sequence/block generation
283
# -----------------------------------------------------------------------------
284
    def get_saved_asset(self, name):
285
        """
286
        Returns the data object for a saved Ensemble/Sequence with name "name". Searches in the
287
        saved assets for a Sequence object first. If no Sequence by that name could be found search
288
        for Ensembles instead. If neither could be found return None.
289
        @param name: Name of the Sequence/Ensemble
290
        @return: PulseSequence | PulseBlockEnsemble | None
291
        """
292
        if name == '':
293
            asset_obj = None
294
        elif name in list(self.saved_pulse_sequences):
295
            asset_obj = self.saved_pulse_sequences[name]
296
        elif name in list(self.saved_pulse_block_ensembles):
297
            asset_obj = self.saved_pulse_block_ensembles[name]
298
        else:
299
            asset_obj = None
300
            self.log.warning('No PulseSequence or PulseBlockEnsemble by the name "{0}" could be '
301
                             'found in saved assets. Returning None.'.format(name))
302
        return asset_obj
303
304
305
    def save_block(self, name, block):
306
        """ Serialize a PulseBlock object to a *.blk file.
307
308
        @param name: string, name of the block to save
309
        @param block: PulseBlock object which will be serialized
310
        """
311
        # TODO: Overwrite handling
312
        block.name = name
313
        self.current_block = block
314
        self.saved_pulse_blocks[name] = block
315
        self._save_blocks_to_file()
316
        self.sigBlockDictUpdated.emit(self.saved_pulse_blocks)
317
        self.sigCurrentBlockUpdated.emit(self.current_block)
318
        return
319
320
    def load_block(self, name):
321
        """
322
323
        @param name:
324
        @return:
325
        """
326
        if name not in self.saved_pulse_blocks:
327
            self.log.error('PulseBlock "{0}" could not be found in saved pulse blocks. Load failed.'
328
                           ''.format(name))
329
            return
330
        block = self.saved_pulse_blocks[name]
331
        self.current_block = block
332
        self.sigCurrentBlockUpdated.emit(self.current_block)
333
        return
334
335
    def delete_block(self, name):
336
        """ Remove the serialized object "name" from the block list and HDD.
337
338
        @param name: string, name of the PulseBlock object to be removed.
339
        """
340
        if name in list(self.saved_pulse_blocks):
341
            del(self.saved_pulse_blocks[name])
342
            if hasattr(self.current_block, 'name'):
343
                if self.current_block.name == name:
344
                    self.current_block = None
345
                    self.sigCurrentBlockUpdated.emit(self.current_block)
346
            self._save_blocks_to_file()
347
            self.sigBlockDictUpdated.emit(self.saved_pulse_blocks)
348
        else:
349
            self.log.warning('PulseBlock object with name "{0}" not found in saved '
350
                             'blocks.\nTherefore nothing is removed.'.format(name))
351
        return
352
353 View Code Duplication
    def _get_blocks_from_file(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
354
        """ Update the saved_pulse_block dict from file """
355
        block_files = [f for f in os.listdir(self.block_dir) if 'block_dict.blk' in f]
356
        if len(block_files) == 0:
357
            self.log.warning('No serialized block dict was found in {0}.'.format(self.block_dir))
358
            self.saved_pulse_blocks = OrderedDict()
359
            self.sigBlockDictUpdated.emit(self.saved_pulse_blocks)
360
            return
361
        # raise error if more than one file is present
362
        if len(block_files) > 1:
363
            self.log.error('More than one serialized block dict was found in {0}.\n'
364
                           'Using {1}.'.format(self.block_dir, block_files[-1]))
365
        block_files = block_files[-1]
366
        try:
367
            with open(os.path.join(self.block_dir, block_files), 'rb') as infile:
368
                self.saved_pulse_blocks = pickle.load(infile)
369
        except:
370
            self.saved_pulse_blocks = OrderedDict()
371
            self.log.error('Failed to deserialize ensemble dict "{0}" from "{1}".'
372
                           ''.format(block_files, self.block_dir))
373
        self.sigBlockDictUpdated.emit(self.saved_pulse_blocks)
374
        return
375
376 View Code Duplication
    def _save_blocks_to_file(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
377
        """ Saves the saved_pulse_block dict to file """
378
        try:
379
            with open(os.path.join(self.block_dir, 'block_dict.blk.tmp'), 'wb') as outfile:
380
                pickle.dump(self.saved_pulse_blocks, outfile)
381
        except:
382
            self.log.error('Failed to serialize ensemble dict in "{0}".'
383
                           ''.format(os.path.join(self.block_dir, 'block_dict.blk.tmp')))
384
            return
385
        # remove old file and rename temp file
386
        try:
387
            os.rename(os.path.join(self.block_dir, 'block_dict.blk.tmp'),
388
                      os.path.join(self.block_dir, 'block_dict.blk'))
389
        except WindowsError:
390
            os.remove(os.path.join(self.block_dir, 'block_dict.blk'))
391
            os.rename(os.path.join(self.block_dir, 'block_dict.blk.tmp'),
392
                      os.path.join(self.block_dir, 'block_dict.blk'))
393
        return
394
395
    def save_ensemble(self, name, ensemble):
396
        """ Saves a PulseBlockEnsemble with name name to file.
397
398
        @param str name: name of the ensemble, which will be serialized.
399
        @param obj ensemble: a PulseBlockEnsemble object
400
        """
401
        # TODO: Overwrite handling
402
        ensemble.name = name
403
        self.current_ensemble = ensemble
404
        self.saved_pulse_block_ensembles[name] = ensemble
405
        self._save_ensembles_to_file()
406
        self.sigEnsembleDictUpdated.emit(self.saved_pulse_block_ensembles)
407
        self.sigCurrentEnsembleUpdated.emit(self.current_ensemble)
408
        return
409
410 View Code Duplication
    def load_ensemble(self, name):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
411
        """
412
413
        @param name:
414
        @return:
415
        """
416
        if name not in self.saved_pulse_block_ensembles:
417
            self.log.error('PulseBlockEnsemble "{0}" could not be found in saved pulse block '
418
                           'ensembles. Load failed.'.format(name))
419
            return
420
        ensemble = self.saved_pulse_block_ensembles[name]
421
        # set generator settings if found in ensemble metadata
422
        if ensemble.sample_rate is not None:
423
            self.sample_rate = ensemble.sample_rate
424
        if ensemble.amplitude_dict is not None:
425
            self.amplitude_dict = ensemble.amplitude_dict
426
        if ensemble.activation_config is not None:
427
            self.activation_config = ensemble.activation_config
428
        if ensemble.laser_channel is not None:
429
            self.laser_channel = ensemble.laser_channel
430
        self.sigSettingsUpdated.emit(self.activation_config, self.laser_channel, self.sample_rate,
431
                                     self.amplitude_dict)
432
        self.current_ensemble = ensemble
433
        self.sigCurrentEnsembleUpdated.emit(ensemble)
434
        return
435
436
    def delete_ensemble(self, name):
437
        """ Remove the ensemble with 'name' from the ensemble list and HDD. """
438
        if name in list(self.saved_pulse_block_ensembles):
439
            del(self.saved_pulse_block_ensembles[name])
440
            if hasattr(self.current_ensemble, 'name'):
441
                if self.current_ensemble.name == name:
442
                    self.current_ensemble = None
443
                    self.sigCurrentEnsembleUpdated.emit(self.current_ensemble)
444
            self._save_ensembles_to_file()
445
            self.sigEnsembleDictUpdated.emit(self.saved_pulse_block_ensembles)
446
        else:
447
            self.log.warning('PulseBlockEnsemble object with name "{0}" not found in saved '
448
                             'ensembles.\nTherefore nothing is removed.'.format(name))
449
        return
450
451 View Code Duplication
    def _get_ensembles_from_file(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
452
        """ Update the saved_pulse_block_ensembles dict from file """
453
        ensemble_files = [f for f in os.listdir(self.ensemble_dir) if 'ensemble_dict.ens' in f]
454
        if len(ensemble_files) == 0:
455
            self.log.warning('No serialized ensembles dict was found in {0}.'
456
                             ''.format(self.ensemble_dir))
457
            self.saved_pulse_block_ensembles = OrderedDict()
458
            self.sigEnsembleDictUpdated.emit(self.saved_pulse_block_ensembles)
459
            return
460
        # raise error if more than one file is present
461
        if len(ensemble_files) > 1:
462
            self.log.error('More than one serialized ensemble dict was found in {0}.\n'
463
                           'Using {1}.'.format(self.ensemble_dir, ensemble_files[-1]))
464
        ensemble_files = ensemble_files[-1]
465
        try:
466
            with open(os.path.join(self.ensemble_dir, ensemble_files), 'rb') as infile:
467
                self.saved_pulse_block_ensembles = pickle.load(infile)
468
        except:
469
            self.saved_pulse_block_ensembles = OrderedDict()
470
            self.log.error('Failed to deserialize ensemble dict "{0}" from "{1}".'
471
                           ''.format(ensemble_files, self.ensemble_dir))
472
        self.sigEnsembleDictUpdated.emit(self.saved_pulse_block_ensembles)
473
        return
474
475 View Code Duplication
    def _save_ensembles_to_file(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
476
        """ Saves the saved_pulse_block_ensembles dict to file """
477
        try:
478
            with open(os.path.join(self.ensemble_dir, 'ensemble_dict.ens.tmp'), 'wb') as outfile:
479
                pickle.dump(self.saved_pulse_block_ensembles, outfile)
480
        except:
481
            self.log.error('Failed to serialize ensemble dict in "{0}".'
482
                           ''.format(os.path.join(self.ensemble_dir, 'ensemble_dict.ens.tmp')))
483
            return
484
        # remove old file and rename temp file
485
        try:
486
            os.rename(os.path.join(self.ensemble_dir, 'ensemble_dict.ens.tmp'),
487
                      os.path.join(self.ensemble_dir, 'ensemble_dict.ens'))
488
        except WindowsError:
489
            os.remove(os.path.join(self.ensemble_dir, 'ensemble_dict.ens'))
490
            os.rename(os.path.join(self.ensemble_dir, 'ensemble_dict.ens.tmp'),
491
                      os.path.join(self.ensemble_dir, 'ensemble_dict.ens'))
492
        return
493
494
    def save_sequence(self, name, sequence):
495
        """ Serialize the PulseSequence object with name 'name' to file.
496
497
        @param str name: name of the sequence object.
498
        @param object sequence: a PulseSequence object, which is going to be
499
                                serialized to file.
500
501
        @return: str: name of the serialized object, if needed.
502
        """
503
        # TODO: Overwrite handling
504
        sequence.name = name
505
        self.current_sequence = sequence
506
        self.saved_pulse_sequences[name] = sequence
507
        self._save_sequences_to_file()
508
        self.sigSequenceDictUpdated.emit(self.saved_pulse_sequences)
509
        self.sigCurrentSequenceUpdated.emit(self.current_sequence)
510
511 View Code Duplication
    def load_sequence(self, name):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
512
        """
513
514
        @param name:
515
        @return:
516
        """
517
        if name not in self.saved_pulse_sequences:
518
            self.log.error('PulseSequence "{0}" could not be found in saved pulse sequences. '
519
                           'Load failed.'.format(name))
520
            return
521
        sequence = self.saved_pulse_sequences[name]
522
        # set generator settings if found in seqeunce metadata
523
        if sequence.sample_rate is not None:
524
            self.sample_rate = sequence.sample_rate
525
        if sequence.amplitude_dict is not None:
526
            self.amplitude_dict = sequence.amplitude_dict
527
        if sequence.activation_config is not None:
528
            self.activation_config = sequence.activation_config
529
        if sequence.laser_channel is not None:
530
            self.laser_channel = sequence.laser_channel
531
        self.sigSettingsUpdated.emit(self.activation_config, self.laser_channel, self.sample_rate,
532
                                     self.amplitude_dict)
533
        self.current_sequence = sequence
534
        self.sigCurrentSequenceUpdated.emit(sequence)
535
        return
536
537
    def delete_sequence(self, name):
538
        """ Remove the sequence "name" from the sequence list and HDD.
539
540
        @param str name: name of the sequence object, which should be deleted.
541
        """
542
        if name in list(self.saved_pulse_sequences):
543
            del(self.saved_pulse_sequences[name])
544
            if hasattr(self.current_sequence, 'name'):
545
                if self.current_sequence.name == name:
546
                    self.current_sequence = None
547
                    self.sigCurrentSequenceUpdated.emit(self.current_sequence)
548
            self._save_sequences_to_file()
549
            self.sigSequenceDictUpdated.emit(self.saved_pulse_sequences)
550
        else:
551
            self.log.warning('PulseBlockEnsemble object with name "{0}" not found in saved '
552
                             'ensembles.\nTherefore nothing is removed.'.format(name))
553
        return
554
555 View Code Duplication
    def _get_sequences_from_file(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
556
        """ Update the saved_pulse_sequences dict from file """
557
        sequence_files = [f for f in os.listdir(self.sequence_dir) if 'sequence_dict.sequ' in f]
558
        if len(sequence_files) == 0:
559
            self.log.warning('No serialized sequence dict was found in {0}.'
560
                             ''.format(self.sequence_dir))
561
            self.saved_pulse_sequences = OrderedDict()
562
            self.sigSequenceDictUpdated.emit(self.saved_pulse_sequences)
563
            return
564
        # raise error if more than one file is present
565
        if len(sequence_files) > 1:
566
            self.log.error('More than one serialized sequence dict was found in {0}.\n'
567
                           'Using {1}.'.format(self.sequence_dir, sequence_files[-1]))
568
            sequence_files = sequence_files[-1]
569
        try:
570
            with open(os.path.join(self.sequence_dir, sequence_files), 'rb') as infile:
571
                self.saved_pulse_sequences = pickle.load(infile)
572
        except:
573
            self.saved_pulse_sequences = OrderedDict()
574
            self.log.error('Failed to deserialize sequence dict "{0}" from "{1}".'
575
                           ''.format(sequence_files, self.sequence_dir))
576
        self.sigSequenceDictUpdated.emit(self.saved_pulse_sequences)
577
        return
578
579 View Code Duplication
    def _save_sequences_to_file(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
580
        """ Saves the saved_pulse_sequences dict to file """
581
        try:
582
            with open(os.path.join(self.sequence_dir, 'sequence_dict.sequ.tmp'), 'wb') as outfile:
583
                pickle.dump(self.saved_pulse_sequences, outfile)
584
        except:
585
            self.log.error('Failed to serialize ensemble dict in "{0}".'
586
                           ''.format(os.path.join(self.sequence_dir, 'sequence_dict.sequ.tmp')))
587
            return
588
        # remove old file and rename temp file
589
        try:
590
            os.rename(os.path.join(self.sequence_dir, 'sequence_dict.sequ.tmp'),
591
                      os.path.join(self.sequence_dir, 'sequence_dict.sequ'))
592
        except WindowsError:
593
            os.remove(os.path.join(self.sequence_dir, 'sequence_dict.sequ'))
594
            os.rename(os.path.join(self.sequence_dir, 'sequence_dict.sequ.tmp'),
595
                      os.path.join(self.sequence_dir, 'sequence_dict.sequ'))
596
        return
597
598
    #---------------------------------------------------------------------------
599
    #                    END sequence/block generation
600
    #---------------------------------------------------------------------------
601
602
603
    #---------------------------------------------------------------------------
604
    #                    BEGIN sequence/block sampling
605
    #---------------------------------------------------------------------------
606
    def _analyze_block_ensemble(self, ensemble):
607
        """
608
609
        @param ensemble:
610
        @return:
611
        """
612
        state_length_bins_arr = np.array([], dtype=int)
613
        number_of_elements = 0
614
        for block, reps in ensemble.block_list:
615
            number_of_elements += (reps+1)*len(block.element_list)
616
            num_state_changes = (reps+1) * len(block.element_list)
617
            tmp_length_bins = np.zeros(num_state_changes, dtype=int)
618
            # Iterate over all repertitions of the current block
619
            for rep_no in range(reps+1):
620
                # Iterate over the Block_Elements inside the current block
621
                for elem_index, block_element in enumerate(block.element_list):
622
                    state_index = rep_no + elem_index
623
                    init_length_s = block_element.init_length_s
624
                    increment_s = block_element.increment_s
625
                    element_length_s = init_length_s + (rep_no * increment_s)
626
                    tmp_length_bins[state_index] = int(np.rint(element_length_s * self.sample_rate))
627
            state_length_bins_arr = np.append(state_length_bins_arr, tmp_length_bins)
628
        number_of_samples = np.sum(state_length_bins_arr)
629
        number_of_states = len(state_length_bins_arr)
630
        return number_of_samples, number_of_elements, number_of_states, state_length_bins_arr
631
632
    def sample_pulse_block_ensemble(self, ensemble_name, write_to_file=True, chunkwise=True,
633
                                    offset_bin=0, name_tag=''):
634
        """ General sampling of a PulseBlockEnsemble object, which serves as the construction plan.
635
636
        @param str ensemble_name: Name, which should correlate with the name of on of the displayed
637
                                  ensembles.
638
        @param bool write_to_file: Write either to RAM or to File (depends on the available space
639
                                   in RAM). If set to FALSE, this method will return the samples
640
                                   (digital and analog) as numpy arrays
641
        @param bool chunkwise: Decide, whether you want to write chunkwise, which will reduce
642
                               memory usage but will increase vastly the amount of time needed.
643
        @param int offset_bin: If many pulse ensembles are samples sequentially, then the
644
                               offset_bin of the previous sampling can be passed to maintain
645
                               rotating frame across pulse_block_ensembles
646
        @param str name_tag: a name tag, which is used to keep the sampled files together, which
647
                             where sampled from the same PulseBlockEnsemble object but where
648
                             different offset_bins were used.
649
650
        @return tuple: of length 4 with
651
                       (analog_samples, digital_samples, [<created_files>], offset_bin).
652
                        analog_samples:
653
                            numpy arrays containing the sampled voltages
654
                        digital_samples:
655
                            numpy arrays containing the sampled logic levels
656
                        [<created_files>]:
657
                            list of strings, with the actual created files through the pulsing
658
                            device
659
                        offset_bin:
660
                            integer, which is used for maintaining the rotation frame.
661
662
        This method is creating the actual samples (voltages and logic states) for each time step
663
        of the analog and digital channels specified in the PulseBlockEnsemble.
664
        Therefore it iterates through all blocks, repetitions and elements of the ensemble and
665
        calculates the exact voltages (float64) according to the specified math_function. The
666
        samples are later on stored inside a float32 array.
667
        So each element is calculated with high precision (float64) and then down-converted to
668
        float32 to be stored.
669
670
        To preserve the rotating frame, an offset counter is used to indicate the absolute time
671
        within the ensemble. All calculations are done with time bins (dtype=int) to avoid rounding
672
        errors. Only in the last step when a single PulseBlockElement object is sampled  these
673
        integer bin values are translated into a floating point time.
674
675
        The chunkwise write mode is used to save memory usage at the expense of time. Here for each
676
        PulseBlockElement the write_to_file method in the HW module is called to avoid large
677
        arrays inside the memory. In other words: The whole sample arrays are never created at any
678
        time. This results in more function calls and general overhead causing the much longer time
679
        to complete.
680
        """
681
        # lock module if it's not already locked (sequence sampling in progress)
682
        if self.getState() == 'idle':
683
            self.lock()
684
            sequence_sampling_in_progress = False
685
        else:
686
            sequence_sampling_in_progress = True
687
        # check for old files associated with the new ensemble and delete them from host PC
688
        if write_to_file:
689
            # get sampled filenames on host PC referring to the same ensemble
690
            filename_list = [f for f in os.listdir(self.waveform_dir) if
691
                             f.startswith(ensemble_name + '_ch')]
692
            # delete all filenames in the list
693
            for file in filename_list:
694
                os.remove(os.path.join(self.waveform_dir, file))
695
696
            if len(filename_list) != 0:
697
                self.log.info('Found old sampled ensembles for name "{0}". Files deleted before '
698
                              'sampling: {1}'.format(ensemble_name, filename_list))
699
700
        start_time = time.time()
701
        # get ensemble
702
        ensemble = self.saved_pulse_block_ensembles[ensemble_name]
703
        # Ensemble parameters to determine the shape of sample arrays
704
        ana_channels = ensemble.analog_channels
705
        dig_channels = ensemble.digital_channels
706
        ana_chnl_names = [chnl for chnl in self.activation_config if 'a_ch' in chnl]
707
        if self.digital_channels != dig_channels or self.analog_channels != ana_channels:
708
            self.log.error('Sampling of PulseBlockEnsemble "{0}" failed!\nMismatch in number of '
709
                           'analog and digital channels between logic ({1}, {2}) and '
710
                           'PulseBlockEnsemble ({3}, {4}).'
711
                           ''.format(ensemble_name, self.analog_channels, self.digital_channels,
712
                                     ana_channels, dig_channels))
713
            return [], [], [''], 0
714
715
        number_of_samples, number_of_elements, number_of_states, state_length_bins_arr = self._analyze_block_ensemble(ensemble)
716
        # The time bin offset for each element to be sampled to preserve rotating frame.
717
        if chunkwise and write_to_file:
718
            # Flags and counter for chunkwise writing
719
            is_first_chunk = True
720
            is_last_chunk = False
721
            element_count = 0
722
        else:
723
            # Allocate huge sample arrays if chunkwise writing is disabled.
724
            analog_samples = np.empty([ana_channels, number_of_samples], dtype = 'float32')
725
            digital_samples = np.empty([dig_channels, number_of_samples], dtype = bool)
726
            # Starting index for the sample array entrys
727
            entry_ind = 0
728
729
        # Iterate over all blocks within the PulseBlockEnsemble object
730
        for block, reps in ensemble.block_list:
731
            # Iterate over all repertitions of the current block
732
            for rep_no in range(reps+1):
733
                # Iterate over the Block_Elements inside the current block
734
                for elem_ind, block_element in enumerate(block.element_list):
735
                    parameters = block_element.parameters
736
                    init_length_s = block_element.init_length_s
737
                    increment_s = block_element.increment_s
738
                    digital_high = block_element.digital_high
739
                    pulse_function = block_element.pulse_function
740
                    element_length_s = init_length_s + (rep_no*increment_s)
741
                    element_length_bins = int(np.rint(element_length_s * self.sample_rate))
742
743
                    # create floating point time array for the current element inside rotating frame
744
                    time_arr = (offset_bin + np.arange(element_length_bins, dtype='float64')) / self.sample_rate
745
746
                    if chunkwise and write_to_file:
747
                        # determine it the current element is the last one to be sampled.
748
                        # Toggle the is_last_chunk flag accordingly.
749
                        element_count += 1
750
                        if element_count == number_of_elements:
751
                            is_last_chunk = True
752
753
                        # allocate temporary sample arrays to contain the current element
754
                        analog_samples = np.empty([ana_channels, element_length_bins], dtype='float32')
755
                        digital_samples = np.empty([dig_channels, element_length_bins], dtype=bool)
756
757
                        # actually fill the allocated sample arrays with values.
758
                        for i, state in enumerate(digital_high):
759
                            digital_samples[i] = np.full(element_length_bins, state, dtype=bool)
760
                        for i, func_name in enumerate(pulse_function):
761
                            analog_samples[i] = np.float32(self._math_func[func_name](time_arr, parameters[i])/self.amplitude_dict[ana_chnl_names[i]])
762
763
                        # write temporary sample array to file
764
                        created_files = self._write_to_file[self.waveform_format](
765
                            ensemble.name + name_tag, analog_samples, digital_samples,
766
                            number_of_samples, is_first_chunk, is_last_chunk)
767
                        # set flag to FALSE after first write
768
                        is_first_chunk = False
769
                    else:
770
                        # if the ensemble should be sampled as a whole (chunkwise = False) fill the
771
                        # entries in the huge sample arrays
772
                        for i, state in enumerate(digital_high):
773
                            digital_samples[i, entry_ind:entry_ind+element_length_bins] = np.full(element_length_bins, state, dtype=bool)
774
                        for i, func_name in enumerate(pulse_function):
775
                            analog_samples[i, entry_ind:entry_ind+element_length_bins] = np.float32(self._math_func[func_name](time_arr, parameters[i])/self.amplitude_dict[ana_chnl_names[i]])
776
777
                        # increment the index offset of the overall sample array for the next
778
                        # element
779
                        entry_ind += element_length_bins
780
781
                    # if the rotating frame should be preserved (default) increment the offset
782
                    # counter for the time array.
783
                    if ensemble.rotating_frame:
784
                        offset_bin += element_length_bins
785
786
        if not write_to_file:
787
            # return a status message with the time needed for sampling the entire ensemble as a
788
            # whole without writing to file.
789
            self.log.info('Time needed for sampling and writing PulseBlockEnsemble to file as a '
790
                          'whole: {0} sec.'.format(int(np.rint(time.time() - start_time))))
791
            # return the sample arrays for write_to_file was set to FALSE
792
            if not sequence_sampling_in_progress:
793
                self.unlock()
794
                self.sigSampleEnsembleComplete.emit(ensemble_name)
795
            return analog_samples, digital_samples, created_files, offset_bin
796
        elif chunkwise:
797
            # return a status message with the time needed for sampling and writing the ensemble
798
            # chunkwise.
799
            self.log.info('Time needed for sampling and writing to file chunkwise: {0} sec'
800
                          ''.format(int(np.rint(time.time()-start_time))))
801
            if not sequence_sampling_in_progress:
802
                self.unlock()
803
                self.sigSampleEnsembleComplete.emit(ensemble_name)
804
            return [], [], created_files, offset_bin
805
        else:
806
            # If the sampling should not be chunkwise and write to file is enabled call the
807
            # write_to_file method only once with both flags set to TRUE
808
            is_first_chunk = True
809
            is_last_chunk = True
810
            created_files = self._write_to_file[self.waveform_format](ensemble.name + name_tag,
811
                                                                      analog_samples,
812
                                                                      digital_samples,
813
                                                                      number_of_samples,
814
                                                                      is_first_chunk, is_last_chunk)
815
            # return a status message with the time needed for sampling and writing the ensemble as
816
            # a whole.
817
            self.log.info('Time needed for sampling and writing PulseBlockEnsemble to file as a '
818
                          'whole: {0} sec'.format(int(np.rint(time.time()-start_time))))
819
            if not sequence_sampling_in_progress:
820
                self.unlock()
821
                self.sigSampleEnsembleComplete.emit(ensemble_name)
822
            return [], [], created_files, offset_bin
823
824
    def sample_pulse_sequence(self, sequence_name, write_to_file=True, chunkwise=True):
825
        """ Samples the PulseSequence object, which serves as the construction plan.
826
827
        @param str ensemble_name: Name, which should correlate with the name of on of the displayed
828
                                  ensembles.
829
        @param bool write_to_file: Write either to RAM or to File (depends on the available space
830
                                   in RAM). If set to FALSE, this method will return the samples
831
                                   (digital and analog) as numpy arrays
832
        @param bool chunkwise: Decide, whether you want to write chunkwise, which will reduce
833
                               memory usage but will increase vastly the amount of time needed.
834
835
        The sequence object is sampled by call subsequently the sampling routine for the
836
        PulseBlockEnsemble objects and passing if needed the rotating frame option.
837
838
        Only those PulseBlockEnsemble object where sampled that are different! These can be
839
        directly obtained from the internal attribute different_ensembles_dict of a PulseSequence.
840
841
        Right now two 'simple' methods of sampling where implemented, which reuse the sample
842
        function for the Pulse_Block_Ensembles. One, which samples by preserving the phase (i.e.
843
        staying in the rotating frame) and the other which samples without keep a phase
844
        relationship between the different entries of the PulseSequence object.
845
846
        More sophisticated sequence sampling method can be implemented here.
847
        """
848
        # lock module
849
        if self.getState() == 'idle':
850
            self.lock()
851
        else:
852
            self.log.error('Cannot sample sequence "{0}" because the sequence generator logic is '
853
                           'still busy (locked).\nFunction call ignored.'.format(sequence_name))
854
            return
855
        if write_to_file:
856
            # get sampled filenames on host PC referring to the same ensemble
857
            filename_list = [f for f in os.listdir(self.sequence_dir) if
858
                             f.startswith(sequence_name + '.seq')]
859
            # delete all filenames in the list
860
            for file in filename_list:
861
                os.remove(os.path.join(self.sequence_dir, file))
862
863
            if len(filename_list) != 0:
864
                self.log.warning('Found old sequence for name "{0}". Files deleted before '
865
                                 'sampling: {1}'.format(sequence_name, filename_list))
866
867
        start_time = time.time()
868
        # get ensemble
869
        sequence_obj = self.saved_pulse_sequences[sequence_name]
870
        sequence_param_dict_list = []
871
872
        # Here all the sampled ensembles with their result file name will be locally stored:
873
        sampled_ensembles = OrderedDict()
874
875
        # if all the Pulse_Block_Ensembles should be in the rotating frame, then each ensemble
876
        # will be created in general with a different offset_bin. Therefore, in order to keep track
877
        # of the sampled Pulse_Block_Ensembles one has to introduce a running number as an
878
        # additional name tag, so keep the sampled files separate.
879
        if sequence_obj.rotating_frame:
880
            ensemble_index = 0  # that will indicate the ensemble index
881
            offset_bin = 0      # that will be used for phase preserving
882
            for ensemble_obj, seq_param in sequence_obj.ensemble_param_list:
883
                # to make something like 001
884
                name_tag = '_' + str(ensemble_index).zfill(3)
885
886
                dummy1, \
887
                dummy2, \
888
                created_files, \
889
                offset_bin_return = self.sample_pulse_block_ensemble(ensemble_obj.name,
890
                                                                     write_to_file,
891
                                                                     chunkwise,
892
                                                                     offset_bin=offset_bin,
893
                                                                     name_tag=name_tag)
894
895
                # the temp_dict is a format how the sequence parameter will be saved
896
                temp_dict = dict()
897
                temp_dict['name'] = created_files
898
899
                # relate the created_files to a name identifier. Maybe this information will be
900
                # needed later on about that sequence object
901
                sampled_ensembles[ensemble_obj.name + name_tag] = created_files
902
                # update the sequence parameter to the temp dict:
903
                temp_dict.update(seq_param)
904
                # add the whole dict to the list of dicts, containing information about how to
905
                # write the sequence properly in the hardware file:
906
                sequence_param_dict_list.append(temp_dict)
907
908
                # for the next run, the returned offset_bin will serve as starting point for
909
                # phase preserving.
910
                offset_bin = offset_bin_return
911
                ensemble_index += 1
912
        else:
913
            # if phase prevervation between the sequence entries is not needed, then only the
914
            # different ensembles will be sampled, since the offset_bin does not matter for them:
915
            for ensemble_name in sequence_obj.different_ensembles_dict:
916
                ensemble_obj = self.saved_pulse_block_ensembles[ensemble_name]
917
918
                dummy1, \
919
                dummy2, \
920
                created_files, \
921
                offset_bin = self.sample_pulse_block_ensemble(ensemble_name, write_to_file,
922
                                                              chunkwise, offset_bin=0, name_tag='')
923
924
                # contains information about which file(s) was/were created for the specified
925
                # ensemble:
926
                sampled_ensembles[ensemble_name] = created_files
927
928
            # go now through the sequence list and replace all the entries with the output of the
929
            # sampled ensemble file:
930
            for ensemble_obj, seq_param  in sequence_obj.ensemble_param_list:
931
932
                temp_dict = dict()
933
                temp_dict['name'] = sampled_ensembles[ensemble_obj.name]
934
                # update the sequence parameter to the temp dict:
935
                temp_dict.update(seq_param)
936
937
                sequence_param_dict_list.append(temp_dict)
938
939
        # FIXME: That is most propably not a good idea!!! But let's see whether that will work out
940
        #        and whether it will be necessary (for the upload method it is!)
941
942
        sequence_obj.sampled_ensembles = sampled_ensembles
943
        # save the current object, since it has now a different attribute:
944
        self.save_sequence(sequence_name, sequence_obj)
945
946
        # pass the whole information to the sequence creation method:
947
        self._write_to_file[self.sequence_format](sequence_name, sequence_param_dict_list)
948
949
        self.log.info('Time needed for sampling and writing Pulse Sequence to file as a whole: '
950
                      '{0} sec.'.format(int(np.rint(time.time() - start_time))))
951
        self.sigSampleSequenceComplete.emit(sequence_name)
952
        # unlock module
953
        self.unlock()
954
        return
955
956
    #---------------------------------------------------------------------------
957
    #                    END sequence/block sampling
958
    #---------------------------------------------------------------------------