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

PulseSequence.replace_ensemble()   C

Complexity

Conditions 8

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 8
dl 0
loc 20
rs 6.6666
c 4
b 0
f 0
1
# -*- coding: utf-8 -*-
2
3
"""
4
This file contains the Qudi data object classes needed for pulse sequence generation.
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 copy
24
import os
25
import sys
26
import inspect
27
import importlib
28
from collections import OrderedDict
29
30
from logic.pulsed.sampling_functions import SamplingFunctions as sf
31
from core.util.modules import get_main_dir
32
33
34
class PulseBlockElement(object):
35
    """
36
    Object representing a single atomic element in a pulse block.
37
38
    This class can build waiting times, sine waves, etc. The pulse block may
39
    contain many Pulse_Block_Element Objects. These objects can be displayed in
40
    a GUI as single rows of a Pulse_Block.
41
    """
42
    def __init__(self, init_length_s=10e-9, increment_s=0, pulse_function=None, digital_high=None):
43
        """
44
        The constructor for a Pulse_Block_Element needs to have:
45
46
        @param float init_length_s: an initial length of the element, this parameters should not be
47
                                    zero but must have a finite value.
48
        @param float increment_s: the number which will be incremented during each repetition of
49
                                  this element.
50
        @param dict pulse_function: dictionary with keys being the qudi analog channel string
51
                                    descriptors ('a_ch1', 'a_ch2' etc.) and the corresponding
52
                                    objects being instances of the mathematical function objects
53
                                    provided by SamplingFunctions class.
54
        @param dict digital_high: dictionary with keys being the qudi digital channel string
55
                                  descriptors ('d_ch1', 'd_ch2' etc.) and the corresponding objects
56
                                  being boolean values describing if the channel should be logical
57
                                  low (False) or high (True).
58
                                  For 3 digital channel it may look like:
59
                                  {'d_ch1': True, 'd_ch2': False, 'd_ch5': False}
60
        """
61
        # FIXME: Sanity checks need to be implemented here
62
        self.init_length_s = init_length_s
63
        self.increment_s = increment_s
64
        if pulse_function is None:
65
            self.pulse_function = OrderedDict()
66
        else:
67
            self.pulse_function = pulse_function
68
        if digital_high is None:
69
            self.digital_high = OrderedDict()
70
        else:
71
            self.digital_high = digital_high
72
73
        # determine set of used digital and analog channels
74
        self.analog_channels = set(self.pulse_function)
75
        self.digital_channels = set(self.digital_high)
76
        self.channel_set = self.analog_channels.union(self.digital_channels)
77
78
    def get_dict_representation(self):
79
        dict_repr = dict()
80
        dict_repr['init_length_s'] = self.init_length_s
81
        dict_repr['increment_s'] = self.increment_s
82
        dict_repr['digital_high'] = self.digital_high
83
        dict_repr['pulse_function'] = dict()
84
        for chnl, func in self.pulse_function.items():
85
            dict_repr['pulse_function'][chnl] = func.get_dict_representation()
86
        return dict_repr
87
88
    @staticmethod
89
    def element_from_dict(element_dict):
90
        for chnl, sample_dict in element_dict['pulse_function'].items():
91
            sf_class = getattr(sf, sample_dict['name'])
92
            element_dict['pulse_function'][chnl] = sf_class(**sample_dict['params'])
93
        return PulseBlockElement(**element_dict)
94
95
96
class PulseBlock(object):
97
    """
98
    Collection of Pulse_Block_Elements which is called a Pulse_Block.
99
    """
100
    def __init__(self, name, element_list=None):
101
        """
102
        The constructor for a Pulse_Block needs to have:
103
104
        @param str name: chosen name for the Pulse_Block
105
        @param list element_list: which contains the Pulse_Block_Element Objects forming a
106
                                  Pulse_Block, e.g. [Pulse_Block_Element, Pulse_Block_Element, ...]
107
        """
108
        self.name = name
109
        self.element_list = list() if element_list is None else element_list
110
        self.init_length_s = 0.0
111
        self.increment_s = 0.0
112
        self.analog_channels = set()
113
        self.digital_channels = set()
114
        self.channel_set = set()
115
        self.refresh_parameters()
116
        return
117
118
    def refresh_parameters(self):
119
        """ Initialize the parameters which describe this Pulse_Block object.
120
121
        The information is gained from all the Pulse_Block_Element objects,
122
        which are attached in the element_list.
123
        """
124
        # the Pulse_Block parameters
125
        self.init_length_s = 0.0
126
        self.increment_s = 0.0
127
        self.channel_set = set()
128
129
        for elem in self.element_list:
130
            self.init_length_s += elem.init_length_s
131
            self.increment_s += elem.increment_s
132
133
            if not self.channel_set:
134
                self.channel_set = elem.channel_set
135
            elif self.channel_set != elem.channel_set:
136
                raise ValueError('Usage of different sets of analog and digital channels in the '
137
                                 'same PulseBlock is prohibited.\nPulseBlock creation failed!\n'
138
                                 'Used channel sets are:\n{0}\n{1}'.format(self.channel_set,
139
                                                                           elem.channel_set))
140
                break
141
        self.analog_channels = {chnl for chnl in self.channel_set if chnl.startswith('a')}
142
        self.digital_channels = {chnl for chnl in self.channel_set if chnl.startswith('d')}
143
        return
144
145 View Code Duplication
    def replace_element(self, position, element):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
146
        if not isinstance(element, PulseBlockElement) or len(self.element_list) <= position:
147
            return -1
148
149
        if element.channel_set != self.channel_set:
150
            raise ValueError('Usage of different sets of analog and digital channels in the '
151
                             'same PulseBlock is prohibited. Used channel sets are:\n{0}\n{1}'
152
                             ''.format(self.channel_set, element.channel_set))
153
            return -1
154
155
        self.init_length_s -= self.element_list[position].init_length_s
156
        self.increment_s -= self.element_list[position].increment_s
157
        self.init_length_s += element.init_length_s
158
        self.increment_s += element.increment_s
159
        self.element_list[position] = copy.deepcopy(element)
160
        return 0
161
162 View Code Duplication
    def delete_element(self, position):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
163
        if len(self.element_list) <= position:
164
            return -1
165
166
        self.init_length_s -= self.element_list[position].init_length_s
167
        self.increment_s -= self.element_list[position].increment_s
168
        del self.element_list[position]
169
        if len(self.element_list) == 0:
170
            self.init_length_s = 0.0
171
            self.increment_s = 0.0
172
        return 0
173
174
    def insert_element(self, position, element):
175
        """ Insert a PulseBlockElement at the given position. The old elements at this position and
176
        all consecutive elements after that will be shifted to higher indices.
177
178
        @param int position: position in the element list
179
        @param PulseBlockElement element: PulseBlockElement instance
180
        """
181
        if not isinstance(element, PulseBlockElement) or len(self.element_list) < position:
182
            return -1
183
184
        if not self.channel_set:
185
            self.channel_set = element.channel_set
186
            self.analog_channels = {chnl for chnl in self.channel_set if chnl.startswith('a')}
187
            self.digital_channels = {chnl for chnl in self.channel_set if chnl.startswith('d')}
188
        elif element.channel_set != self.channel_set:
189
            raise ValueError('Usage of different sets of analog and digital channels in the '
190
                             'same PulseBlock is prohibited. Used channel sets are:\n{0}\n{1}'
191
                             ''.format(self.channel_set, element.channel_set))
192
            return -1
193
194
        self.init_length_s += element.init_length_s
195
        self.increment_s += element.increment_s
196
197
        self.element_list.insert(position, copy.deepcopy(element))
198
        return 0
199
200
    def append_element(self, element, at_beginning=False):
201
        """
202
        """
203
        position = 0 if at_beginning else len(self.element_list)
204
        return self.insert_element(position=position, element=element)
205
206
    def get_dict_representation(self):
207
        dict_repr = dict()
208
        dict_repr['name'] = self.name
209
        dict_repr['element_list'] = list()
210
        for element in self.element_list:
211
            dict_repr['element_list'].append(element.get_dict_representation())
212
        return dict_repr
213
214
    @staticmethod
215
    def block_from_dict(block_dict):
216
        for ii, element_dict in enumerate(block_dict['element_list']):
217
            block_dict['element_list'][ii] = PulseBlockElement.element_from_dict(element_dict)
218
        return PulseBlock(**block_dict)
219
220
221
class PulseBlockEnsemble(object):
222
    """
223
    Represents a collection of PulseBlock objects which is called a PulseBlockEnsemble.
224
225
    This object is used as a construction plan to create one sampled file.
226
    """
227
    def __init__(self, name, block_list=None, rotating_frame=True):
228
        """
229
        The constructor for a Pulse_Block_Ensemble needs to have:
230
231
        @param str name: chosen name for the PulseBlockEnsemble
232
        @param list block_list: contains the PulseBlock names with their number of repetitions,
233
                                e.g. [(name, repetitions), (name, repetitions), ...])
234
        @param bool rotating_frame: indicates whether the phase should be preserved for all the
235
                                    functions.
236
        """
237
        # FIXME: Sanity checking needed here
238
        self.name = name
239
        self.rotating_frame = rotating_frame
240
        if isinstance(block_list, list):
241
            self.block_list = block_list
242
        else:
243
            self.block_list = list()
244
245
        # Dictionary container to store information related to the actually sampled
246
        # Waveform like pulser settings used during sampling (sample_rate, activation_config etc.)
247
        # and additional information about the discretization of the waveform (timebin positions of
248
        # the PulseBlockElement transitions etc.) as well as the names of the created waveforms.
249
        # This container will be populated during sampling and will be emptied upon deletion of the
250
        # corresponding waveforms from the pulse generator
251
        self.sampling_information = dict()
252
        # Dictionary container to store additional information about for measurement settings
253
        # (ignore_lasers, controlled_variable, alternating etc.).
254
        # This container needs to be populated by the script creating the PulseBlockEnsemble
255
        # before saving it. (e.g. in generate methods in PulsedObjectGenerator class)
256
        self.measurement_information = dict()
257
        return
258
259
    def replace_block(self, position, block_name, reps=None):
260
        """
261
        """
262
        if not isinstance(block_name, str) or len(self.block_list) <= position:
263
            return -1
264
265
        if reps is None:
266
            list_entry = (block_name, self.block_list[position][1])
267
            self.block_list[position][0] = list_entry
268
        elif reps >= 0:
269
            self.block_list[position] = (block_name, reps)
270
        else:
271
            return -1
272
        return 0
273
274
    def delete_block(self, position):
275
        """
276
        """
277
        if len(self.block_list) <= position:
278
            return -1
279
280
        del self.block_list[position]
281
        return 0
282
283
    def insert_block(self, position, block_name, reps=0):
284
        """ Insert a PulseBlock at the given position. The old block at this position and all
285
        consecutive blocks after that will be shifted to higher indices.
286
287
        @param int position: position in the block list
288
        @param str block_name: PulseBlock name
289
        @param int reps: Block repetitions. Zero means single playback of the block.
290
        """
291
        if not isinstance(block_name, str) or len(self.block_list) < position or reps < 0:
292
            return -1
293
294
        self.block_list.insert(position, (block_name, reps))
295
        return 0
296
297
    def append_block(self, block_name, reps=0, at_beginning=False):
298
        """ Append either at the front or at the back.
299
300
        @param str block_name: PulseBlock name
301
        @param int reps: Block repetitions. Zero means single playback of the block.
302
        @param bool at_beginning: If False append to end (default), if True insert at beginning.
303
        """
304
        position = 0 if at_beginning else len(self.block_list)
305
        return self.insert_block(position=position, block_name=block_name, reps=reps)
306
307
    def get_dict_representation(self):
308
        dict_repr = dict()
309
        dict_repr['name'] = self.name
310
        dict_repr['rotating_frame'] = self.rotating_frame
311
        dict_repr['block_list'] = self.block_list
312
        dict_repr['sampling_information'] = self.sampling_information
313
        dict_repr['measurement_information'] = self.measurement_information
314
        return dict_repr
315
316
    @staticmethod
317
    def ensemble_from_dict(ensemble_dict):
318
        new_ens = PulseBlockEnsemble(name=ensemble_dict['name'],
319
                                     block_list=ensemble_dict['block_list'],
320
                                     rotating_frame=ensemble_dict['rotating_frame'])
321
        new_ens.sampling_information = ensemble_dict['sampling_information']
322
        new_ens.measurement_information = ensemble_dict['measurement_information']
323
        return new_ens
324
325
326
class PulseSequence(object):
327
    """
328
    Higher order object for sequence capability.
329
330
    Represents a playback procedure for a number of PulseBlockEnsembles. Unused for pulse
331
    generator hardware without sequencing functionality.
332
    """
333
    __default_seq_params = {'repetitions': 1,
334
                            'go_to': -1,
335
                            'event_jump_to': -1,
336
                            'event_trigger': 'OFF',
337
                            'wait_for': 'OFF',
338
                            'flag_trigger': 'OFF',
339
                            'flag_high': 'OFF'}
340
341
    def __init__(self, name, ensemble_list=None, rotating_frame=False):
342
        """
343
        The constructor for a PulseSequence objects needs to have:
344
345
        @param str name: the actual name of the sequence
346
        @param list ensemble_list: list containing a tuple of two entries:
347
                                          [(PulseBlockEnsemble name, seq_param),
348
                                           (PulseBlockEnsemble name, seq_param), ...]
349
                                          The seq_param is a dictionary, where the various sequence
350
                                          parameters are saved with their keywords and the
351
                                          according parameter (as item).
352
                                          Available parameters are:
353
                                          'repetitions': The number of repetitions for that sequence
354
                                                         step. (Default 0)
355
                                                         0 meaning the step is played once.
356
                                                         Set to -1 for infinite looping.
357
                                          'go_to':   The sequence step index to jump to after
358
                                                     having played all repetitions. (Default -1)
359
                                                     Indices starting at 1 for first step.
360
                                                     Set to 0 or -1 to follow up with the next step.
361
                                          'event_jump_to': The sequence step to jump to
362
                                                           (starting from 1) in case of a trigger
363
                                                           event (see event_trigger).
364
                                                           Setting it to 0 or -1 means jump to next
365
                                                           step. Ignored if event_trigger is 'OFF'.
366
                                          'event_trigger': The trigger input to listen to in order
367
                                                           to perform sequence jumps. Set to 'OFF'
368
                                                           (default) in order to ignore triggering.
369
                                          'wait_for': The trigger input to wait for before playing
370
                                                      this sequence step. Set to 'OFF' (default)
371
                                                      in order to play the current step immediately.
372
                                          'flag_trigger': The flag to trigger when this sequence
373
                                                          step starts playing. Select 'OFF'
374
                                                          (default) for no flag trigger.
375
                                          'flag_high': The flag to set to high while this step is
376
                                                       playing. Select 'OFF' (default) to set all
377
                                                       flags to low.
378
379
                                          If only 'repetitions' are in the dictionary, then the dict
380
                                          will look like:
381
                                            seq_param = {'repetitions': 41}
382
                                          and so the respective sequence step will play 42 times.
383
        @param bool rotating_frame: indicates, whether the phase has to be preserved in all
384
                                    analog signals ACROSS different waveforms
385
        """
386
        self.name = name
387
        self.rotating_frame = rotating_frame
388
        self.ensemble_list = list() if ensemble_list is None else ensemble_list
389
        self.is_finite = True
390
        self.refresh_parameters()
391
392
        # self.sampled_ensembles = OrderedDict()
393
        # Dictionary container to store information related to the actually sampled
394
        # Waveforms like pulser settings used during sampling (sample_rate, activation_config etc.)
395
        # and additional information about the discretization of the waveform (timebin positions of
396
        # the PulseBlockElement transitions etc.)
397
        # This container is not necessary for the sampling process but serves only the purpose of
398
        # holding optional information for different modules.
399
        self.sampling_information = dict()
400
        # Dictionary container to store additional information about for measurement settings
401
        # (ignore_lasers, controlled_values, alternating etc.).
402
        # This container needs to be populated by the script creating the PulseSequence
403
        # before saving it.
404
        self.measurement_information = dict()
405
        return
406
407
    def refresh_parameters(self):
408
        self.is_finite = True
409
        for ensemble_name, params in self.ensemble_list:
410
            if params['repetitions'] < 0:
411
                self.is_finite = False
412
                break
413
        return
414
415
    def replace_ensemble(self, position, ensemble_name, seq_param=None):
416
        """ Replace a sequence step at a given position.
417
418
        @param int position: position in the ensemble list
419
        @param str ensemble_name: PulseBlockEnsemble name
420
        @param dict seq_param: Sequence step parameter dictionary. Use present one if None.
421
        """
422
        if not isinstance(ensemble_name, str) or len(self.ensemble_list) <= position:
423
            return -1
424
425
        if seq_param is None:
426
            list_entry = (ensemble_name, self.ensemble_list[position][1])
427
            self.ensemble_list[position] = list_entry
428
        else:
429
            self.ensemble_list[position] = (ensemble_name, seq_param.copy())
430
            if seq_param['repetitions'] < 0 and self.is_finite:
431
                self.is_finite = False
432
            elif seq_param['repetitions'] >= 0 and not self.is_finite:
433
                self.refresh_parameters()
434
        return 0
435
436
    def delete_ensemble(self, position):
437
        """ Delete an ensemble at a given position
438
439
        @param int position: position within the list self.ensemble_list.
440
        """
441
        if len(self.ensemble_list) <= position:
442
            return -1
443
444
        refresh = True if self.ensemble_list[position][1]['repetitions'] < 0 else False
445
446
        del self.ensemble_list[position]
447
448
        if refresh:
449
            self.refresh_parameters()
450
        return 0
451
452
    def insert_ensemble(self, position, ensemble_name, seq_param=None):
453
        """ Insert a sequence step at the given position. The old step at this position and all
454
        consecutive steps after that will be shifted to higher indices.
455
456
        @param int position: position in the ensemble list
457
        @param str ensemble_name: PulseBlockEnsemble name
458
        @param dict seq_param: Sequence step parameter dictionary.
459
        """
460
        if not isinstance(ensemble_name, str) or len(self.ensemble_list) < position:
461
            return -1
462
463
        if seq_param is None:
464
            seq_param = self.__default_seq_params
465
466
        self.ensemble_list.insert(position, (ensemble_name, seq_param.copy()))
467
468
        if seq_param['repetitions'] < 0:
469
            self.is_finite = False
470
        return 0
471
472
    def append_ensemble(self, ensemble_name, seq_param=None, at_beginning=False):
473
        """ Append either at the front or at the back.
474
475
        @param str ensemble_name: PulseBlockEnsemble name
476
        @param dict seq_param: Sequence step parameter dictionary.
477
        @param bool at_beginning: If False append to end (default), if True insert at beginning.
478
        """
479
        position = 0 if at_beginning else len(self.ensemble_list)
480
        return self.insert_ensemble(position=position,
481
                                    ensemble_name=ensemble_name,
482
                                    seq_param=seq_param)
483
484
    def get_dict_representation(self):
485
        dict_repr = dict()
486
        dict_repr['name'] = self.name
487
        dict_repr['rotating_frame'] = self.rotating_frame
488
        dict_repr['ensemble_list'] = self.ensemble_list
489
        dict_repr['sampling_information'] = self.sampling_information
490
        dict_repr['measurement_information'] = self.measurement_information
491
        return dict_repr
492
493
    @staticmethod
494
    def sequence_from_dict(sequence_dict):
495
        new_seq = PulseSequence(name=sequence_dict['name'],
496
                                ensemble_list=sequence_dict['ensemble_list'],
497
                                rotating_frame=sequence_dict['rotating_frame'])
498
        new_seq.sampling_information = sequence_dict['sampling_information']
499
        new_seq.measurement_information = sequence_dict['measurement_information']
500
        return new_seq
501
502
503
class PredefinedGeneratorBase:
504
    """
505
    Base class for PulseObjectGenerator and predefined generator classes containing the actual
506
    "generate_"-methods.
507
508
    This class holds a protected reference to the SequenceGeneratorLogic and provides read-only
509
    access via properties to various attributes of the logic module.
510
    SequenceGeneratorLogic logger is also accessible via this base class and can be used as in any
511
    qudi module (e.g. self.log.error(...)).
512
    Also provides helper methods to simplify sequence/ensemble generation.
513
    """
514
    def __init__(self, sequencegeneratorlogic):
515
        # Keep protected reference to the SequenceGeneratorLogic
516
        self.__sequencegeneratorlogic = sequencegeneratorlogic
517
518
    @property
519
    def log(self):
520
        return self.__sequencegeneratorlogic.log
521
522
    @property
523
    def pulse_generator_settings(self):
524
        return self.__sequencegeneratorlogic.pulse_generator_settings
525
526
    @property
527
    def generation_parameters(self):
528
        return self.__sequencegeneratorlogic.generation_parameters
529
530
    @property
531
    def channel_set(self):
532
        channels = self.pulse_generator_settings.get('activation_config')
533
        if channels is None:
534
            channels = ('', set())
535
        return channels[1]
536
537
    @property
538
    def analog_channels(self):
539
        return {chnl for chnl in self.channel_set if chnl.startswith('a')}
540
541
    @property
542
    def digital_channels(self):
543
        return {chnl for chnl in self.channel_set if chnl.startswith('d')}
544
545
    @property
546
    def laser_channel(self):
547
        return self.generation_parameters.get('laser_channel')
548
549
    @property
550
    def sync_channel(self):
551
        channel = self.generation_parameters.get('sync_channel')
552
        return None if channel == '' else channel
553
554
    @property
555
    def gate_channel(self):
556
        channel = self.generation_parameters.get('gate_channel')
557
        return None if channel == '' else channel
558
559
    @property
560
    def analog_trigger_voltage(self):
561
        return self.generation_parameters.get('analog_trigger_voltage')
562
563
    @property
564
    def laser_delay(self):
565
        return self.generation_parameters.get('laser_delay')
566
567
    @property
568
    def microwave_channel(self):
569
        channel = self.generation_parameters.get('microwave_channel')
570
        return None if channel == '' else channel
571
572
    @property
573
    def microwave_frequency(self):
574
        return self.generation_parameters.get('microwave_frequency')
575
576
    @property
577
    def microwave_amplitude(self):
578
        return self.generation_parameters.get('microwave_amplitude')
579
580
    @property
581
    def laser_length(self):
582
        return self.generation_parameters.get('laser_length')
583
584
    @property
585
    def wait_time(self):
586
        return self.generation_parameters.get('wait_time')
587
588
    @property
589
    def rabi_period(self):
590
        return self.generation_parameters.get('rabi_period')
591
592
    ################################################################################################
593
    #                                   Helper methods                                          ####
594
    ################################################################################################
595
    def _get_idle_element(self, length, increment):
596
        """
597
        Creates an idle pulse PulseBlockElement
598
599
        @param float length: idle duration in seconds
600
        @param float increment: idle duration increment in seconds
601
602
        @return: PulseBlockElement, the generated idle element
603
        """
604
        # Create idle element
605
        return PulseBlockElement(init_length_s=length,
606
                                 increment_s=increment,
607
                                 pulse_function={chnl: sf.Idle() for chnl in self.analog_channels},
608
                                 digital_high={chnl: False for chnl in self.digital_channels})
609
610
    def _get_trigger_element(self, length, increment, channels):
611
        """
612
        Creates a trigger PulseBlockElement
613
614
        @param float length: trigger duration in seconds
615
        @param float increment: trigger duration increment in seconds
616
        @param str|list channels: The pulser channel(s) to be triggered.
617
618
        @return: PulseBlockElement, the generated trigger element
619
        """
620
        if isinstance(channels, str):
621
            channels = [channels]
622
623
        # input params for element generation
624
        pulse_function = {chnl: sf.Idle() for chnl in self.analog_channels}
625
        digital_high = {chnl: False for chnl in self.digital_channels}
626
627
        # Determine analogue or digital trigger channel and set channels accordingly.
628
        for channel in channels:
629
            if channel.startswith('d'):
630
                digital_high[channel] = True
631
            else:
632
                pulse_function[channel] = sf.DC(voltage=self.analog_trigger_voltage)
633
634
        # return trigger element
635
        return PulseBlockElement(init_length_s=length,
636
                                 increment_s=increment,
637
                                 pulse_function=pulse_function,
638
                                 digital_high=digital_high)
639
640
    def _get_laser_element(self, length, increment):
641
        """
642
        Creates laser trigger PulseBlockElement
643
644
        @param float length: laser pulse duration in seconds
645
        @param float increment: laser pulse duration increment in seconds
646
647
        @return: PulseBlockElement, two elements for laser and gate trigger (delay element)
648
        """
649
        return self._get_trigger_element(length=length,
650
                                         increment=increment,
651
                                         channels=self.laser_channel)
652
653
    def _get_laser_gate_element(self, length, increment):
654
        """
655
        """
656
        laser_gate_element = self._get_laser_element(length=length,
657
                                                     increment=increment)
658
        if self.gate_channel:
659
            if self.gate_channel.startswith('d'):
660
                laser_gate_element.digital_high[self.gate_channel] = True
661
            else:
662
                laser_gate_element.pulse_function[self.gate_channel] = sf.DC(
663
                    voltage=self.analog_trigger_voltage)
664
        return laser_gate_element
665
666
    def _get_delay_element(self):
667
        """
668
        Creates an idle element of length of the laser delay
669
670
        @return PulseBlockElement: The delay element
671
        """
672
        return self._get_idle_element(length=self.laser_delay,
673
                                      increment=0)
674
675
    def _get_delay_gate_element(self):
676
        """
677
        Creates a gate trigger of length of the laser delay.
678
        If no gate channel is specified will return a simple idle element.
679
680
        @return PulseBlockElement: The delay element
681
        """
682
        if self.gate_channel:
683
            return self._get_trigger_element(length=self.laser_delay,
684
                                             increment=0,
685
                                             channels=self.gate_channel)
686
        else:
687
            return self._get_delay_element()
688
689
    def _get_sync_element(self):
690
        """
691
692
        """
693
        return self._get_trigger_element(length=50e-9,
694
                                         increment=0,
695
                                         channels=self.sync_channel)
696
697
    def _get_mw_element(self, length, increment, amp=None, freq=None, phase=None):
698
        """
699
        Creates a MW pulse PulseBlockElement
700
701
        @param float length: MW pulse duration in seconds
702
        @param float increment: MW pulse duration increment in seconds
703
        @param float freq: MW frequency in case of analogue MW channel in Hz
704
        @param float amp: MW amplitude in case of analogue MW channel in V
705
        @param float phase: MW phase in case of analogue MW channel in deg
706
707
        @return: PulseBlockElement, the generated MW element
708
        """
709
        if self.microwave_channel.startswith('d'):
710
            mw_element = self._get_trigger_element(
711
                length=length,
712
                increment=increment,
713
                channels=self.microwave_channel)
714
        else:
715
            mw_element = self._get_idle_element(
716
                length=length,
717
                increment=increment)
718
            mw_element.pulse_function[self.microwave_channel] = sf.Sin(amplitude=amp,
719
                                                                       frequency=freq,
720
                                                                       phase=phase)
721
        return mw_element
722
723
    def _get_multiple_mw_element(self, length, increment, amps=None, freqs=None, phases=None):
724
        """
725
        Creates single, double or triple sine mw element.
726
727
        @param float length: MW pulse duration in seconds
728
        @param float increment: MW pulse duration increment in seconds
729
        @param amps: list containing the amplitudes
730
        @param freqs: list containing the frequencies
731
        @param phases: list containing the phases
732
        @return: PulseBlockElement, the generated MW element
733
        """
734
        if isinstance(amps, (int, float)):
735
            amps = [amps]
736
        if isinstance(freqs, (int, float)):
737
            freqs = [freqs]
738
        if isinstance(phases, (int, float)):
739
            phases = [phases]
740
741
        if self.microwave_channel.startswith('d'):
742
            mw_element = self._get_trigger_element(
743
                length=length,
744
                increment=increment,
745
                channels=self.microwave_channel)
746
        else:
747
            mw_element = self._get_idle_element(
748
                length=length,
749
                increment=increment)
750
751
            sine_number = min(len(amps), len(freqs), len(phases))
752
753
            if sine_number < 2:
754
                mw_element.pulse_function[self.microwave_channel] = sf.Sin(amplitude=amps[0],
755
                                                                           frequency=freqs[0],
756
                                                                           phase=phases[0])
757
            elif sine_number == 2:
758
                mw_element.pulse_function[self.microwave_channel] = sf.DoubleSin(
759
                    amplitude_1=amps[0],
760
                    amplitude_2=amps[1],
761
                    frequency_1=freqs[0],
762
                    frequency_2=freqs[1],
763
                    phase_1=phases[0],
764
                    phase_2=phases[1])
765
            else:
766
                mw_element.pulse_function[self.microwave_channel] = sf.TripleSin(
767
                    amplitude_1=amps[0],
768
                    amplitude_2=amps[1],
769
                    amplitude_3=amps[2],
770
                    frequency_1=freqs[0],
771
                    frequency_2=freqs[1],
772
                    frequency_3=freqs[2],
773
                    phase_1=phases[0],
774
                    phase_2=phases[1],
775
                    phase_3=phases[2])
776
        return mw_element
777
778
    def _get_mw_laser_element(self, length, increment, amp=None, freq=None, phase=None):
779
        """
780
781
        @param length:
782
        @param increment:
783
        @param amp:
784
        @param freq:
785
        @param phase:
786
        @return:
787
        """
788
        mw_laser_element = self._get_mw_element(length=length,
789
                                                increment=increment,
790
                                                amp=amp,
791
                                                freq=freq,
792
                                                phase=phase)
793
        if self.laser_channel.startswith('d'):
794
            mw_laser_element.digital_high[self.laser_channel] = True
795
        else:
796
            mw_laser_element.pulse_function[self.laser_channel] = sf.DC(
797
                voltage=self.analog_trigger_voltage)
798
        return mw_laser_element
799
800
    def _get_ensemble_count_length(self, ensemble, created_blocks):
801
        """
802
803
        @param ensemble:
804
        @param created_blocks:
805
        @return:
806
        """
807
        if self.gate_channel:
808
            length = self.laser_length + self.laser_delay
809
        else:
810
            blocks = {block.name: block for block in created_blocks}
811
            length = 0.0
812
            for block_name, reps in ensemble.block_list:
813
                length += blocks[block_name].init_length_s * (reps + 1)
814
                length += blocks[block_name].increment_s * ((reps ** 2 + reps) / 2)
815
        return length
816
817
818
class PulseObjectGenerator(PredefinedGeneratorBase):
819
    """
820
821
    """
822
    def __init__(self, sequencegeneratorlogic):
823
        # Initialize base class
824
        super().__init__(sequencegeneratorlogic)
825
826
        # dictionary containing references to all generation methods imported from generator class
827
        # modules. The keys are the method names excluding the prefix "generate_".
828
        self._generate_methods = dict()
829
        # nested dictionary with keys being the generation method names and values being a
830
        # dictionary containing all keyword arguments as keys with their default value
831
        self._generate_method_parameters = dict()
832
833
        # import path for generator modules from default dir (logic.predefined_generate_methods)
834
        path_list = [os.path.join(get_main_dir(), 'logic', 'pulsed', 'predefined_generate_methods')]
835
        # import path for generator modules from non-default directory if a path has been given
836
        if isinstance(sequencegeneratorlogic.additional_methods_dir, str):
837
            path_list.append(sequencegeneratorlogic.additional_methods_dir)
838 View Code Duplication
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
839
        # Import predefined generator modules and get a list of generator classes
840
        generator_classes = self.__import_external_generators(paths=path_list)
841
842
        # create an instance of each class and put them in a temporary list
843
        generator_instances = [cls(sequencegeneratorlogic) for cls in generator_classes]
844
845
        # add references to all generate methods in each instance to a dict
846
        self.__populate_method_dict(instance_list=generator_instances)
847
848
        # populate parameters dictionary from generate method signatures
849
        self.__populate_parameter_dict()
850
851
    @property
852
    def predefined_generate_methods(self):
853
        return self._generate_methods
854
855
    @property
856
    def predefined_method_parameters(self):
857
        return self._generate_method_parameters.copy()
858
859
    def __import_external_generators(self, paths):
860
        """
861
        Helper method to import all modules from directories contained in paths.
862
        Find all classes in those modules that inherit exclusively from PredefinedGeneratorBase
863
        class and return a list of them.
864
865
        @param iterable paths: iterable containing paths to import modules from
866
        @return list: A list of imported valid generator classes
867
        """
868
        class_list = list()
869
        for path in paths:
870
            if not os.path.exists(path):
871
                self.log.error('Unable to import generate methods from "{0}".\n'
872
                               'Path does not exist.'.format(path))
873
                continue
874
            # Get all python modules to import from.
875
            # The assumption is that in the path, there are *.py files,
876
            # which contain only generator classes!
877
            module_list = [name[:-3] for name in os.listdir(path) if
878
                           os.path.isfile(os.path.join(path, name)) and name.endswith('.py')]
879
880
            # append import path to sys.path
881
            sys.path.append(path)
882
883
            # Go through all modules and create instances of each class found.
884
            for module_name in module_list:
885
                # import module
886
                mod = importlib.import_module('{0}'.format(module_name))
887
                importlib.reload(mod)
888
                # get all generator class references defined in the module
889
                tmp_list = [m[1] for m in inspect.getmembers(mod, self.is_generator_class)]
890
                # append to class_list
891
                class_list.extend(tmp_list)
892
        return class_list
893
894
    def __populate_method_dict(self, instance_list):
895
        """
896
        Helper method to populate the dictionaries containing all references to callable generate
897
        methods contained in generator instances passed to this method.
898
899
        @param list instance_list: List containing instances of generator classes
900
        """
901
        self._generate_methods = dict()
902
        for instance in instance_list:
903
            for method_name, method_ref in inspect.getmembers(instance, inspect.ismethod):
904
                if method_name.startswith('generate_'):
905
                    self._generate_methods[method_name[9:]] = method_ref
906
        return
907
908
    def __populate_parameter_dict(self):
909
        """
910
        Helper method to populate the dictionary containing all possible keyword arguments from all
911
        generate methods.
912
        """
913
        self._generate_method_parameters = dict()
914
        for method_name, method in self._generate_methods.items():
915
            method_signature = inspect.signature(method)
916
            param_dict = dict()
917
            for name, param in method_signature.parameters.items():
918
                param_dict[name] = None if param.default is param.empty else param.default
919
920
            self._generate_method_parameters[method_name] = param_dict
921
        return
922
923
    @staticmethod
924
    def is_generator_class(obj):
925
        """
926
        Helper method to check if an object is a valid generator class.
927
928
        @param object obj: object to check
929
        @return bool: True if obj is a valid generator class, False otherwise
930
        """
931
        if inspect.isclass(obj):
932
            return PredefinedGeneratorBase in obj.__bases__ and len(obj.__bases__) == 1
933
        return False
934
935
936
937