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

PredefinedGeneratorBase.laser_length()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
c 2
b 0
f 0
dl 0
loc 3
rs 10
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
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 __repr__(self):
79
        repr_str = 'PulseBlockElement(init_length_s={0}, increment_s={1}, pulse_function='.format(
80
            self.init_length_s, self.increment_s)
81
        repr_str += '{'
82
        for ind, (channel, sampling_func) in enumerate(self.pulse_function.items()):
83
            repr_str += '\'{0}\': {1}'.format(channel, 'SamplingFunctions.' + repr(sampling_func))
84
            if ind < len(self.pulse_function) - 1:
85
                repr_str += ', '
86
        repr_str += '}, '
87
        repr_str += 'digital_high={0})'.format(repr(dict(self.digital_high)))
88
        return repr_str
89
90
    def __str__(self):
91
        pulse_func_dict = {chnl: type(func).__name__ for chnl, func in self.pulse_function.items()}
92
        return_str = 'PulseBlockElement\n\tinitial length: {0}s\n\tlength increment: {1}s\n\t' \
93
                     'analog channels: {2}\n\tdigital channels: {3}'.format(self.init_length_s,
94
                                                                            self.increment_s,
95
                                                                            pulse_func_dict,
96
                                                                            dict(self.digital_high))
97
        return return_str
98
99
    def get_dict_representation(self):
100
        dict_repr = dict()
101
        dict_repr['init_length_s'] = self.init_length_s
102
        dict_repr['increment_s'] = self.increment_s
103
        dict_repr['digital_high'] = self.digital_high
104
        dict_repr['pulse_function'] = dict()
105
        for chnl, func in self.pulse_function.items():
106
            dict_repr['pulse_function'][chnl] = func.get_dict_representation()
107
        return dict_repr
108
109
    @staticmethod
110
    def element_from_dict(element_dict):
111
        for chnl, sample_dict in element_dict['pulse_function'].items():
112
            sf_class = getattr(SamplingFunctions, sample_dict['name'])
113
            element_dict['pulse_function'][chnl] = sf_class(**sample_dict['params'])
114
        return PulseBlockElement(**element_dict)
115
116
117
class PulseBlock(object):
118
    """
119
    Collection of Pulse_Block_Elements which is called a Pulse_Block.
120
    """
121
    def __init__(self, name, element_list=None):
122
        """
123
        The constructor for a Pulse_Block needs to have:
124
125
        @param str name: chosen name for the Pulse_Block
126
        @param list element_list: which contains the Pulse_Block_Element Objects forming a
127
                                  Pulse_Block, e.g. [Pulse_Block_Element, Pulse_Block_Element, ...]
128
        """
129
        self.name = name
130
        self.element_list = list() if element_list is None else element_list
131
        self.init_length_s = 0.0
132
        self.increment_s = 0.0
133
        self.analog_channels = set()
134
        self.digital_channels = set()
135
        self.channel_set = set()
136
        self.refresh_parameters()
137
        return
138
139
    def __repr__(self):
140
        repr_str = 'PulseBlock(name=\'{0}\', element_list=['.format(self.name)
141
        repr_str += ', '.join((repr(elem) for elem in self.element_list)) + '])'
142
        return repr_str
143
144
    def __str__(self):
145
        return_str = 'PulseBlock "{0}"\n\tnumber of elements: {1}\n\t'.format(
146
            self.name, len(self.element_list))
147
        return_str += 'initial length: {0}s\n\tlength increment: {1}s\n\t'.format(
148
            self.init_length_s, self.increment_s)
149
        return_str += 'active analog channels: {0}\n\tactive digital channels: {1}'.format(
150
            sorted(self.analog_channels), sorted(self.digital_channels))
151
        return return_str
152
153
    def __len__(self):
154
        return len(self.element_list)
155
156
    def __getitem__(self, key):
157
        if not isinstance(key, (slice, int)):
158
            raise TypeError('PulseBlock indices must be int or slice, not {0}'.format(type(key)))
159
        return self.element_list[key]
160
161
    def __setitem__(self, key, value):
162
        if isinstance(key, int):
163
            if not isinstance(value, PulseBlockElement):
164
                raise TypeError('PulseBlock element list entries must be of type PulseBlockElement,'
165
                                ' not {0}'.format(type(value)))
166
            if not self.channel_set:
167
                self.channel_set = value.channel_set.copy()
168
                self.analog_channels = {chnl for chnl in self.channel_set if chnl.startswith('a')}
169
                self.digital_channels = {chnl for chnl in self.channel_set if chnl.startswith('d')}
170
            elif value.channel_set != self.channel_set:
171
                raise ValueError('Usage of different sets of analog and digital channels in the '
172
                                 'same PulseBlock is prohibited. Used channel sets are:\n{0}\n{1}'
173
                                 ''.format(self.channel_set, value.channel_set))
174
175
            self.init_length_s -= self.element_list[key].init_length_s
176
            self.increment_s -= self.element_list[key].increment_s
177
            self.init_length_s += value.init_length_s
178
            self.increment_s += value.increment_s
179
        elif isinstance(key, slice):
180
            add_length = 0
181
            add_increment = 0
182
            for element in value:
183
                if not isinstance(element, PulseBlockElement):
184
                    raise TypeError('PulseBlock element list entries must be of type '
185
                                    'PulseBlockElement, not {0}'.format(type(value)))
186
                if not self.channel_set:
187
                    self.channel_set = element.channel_set.copy()
188
                    self.analog_channels = {chnl for chnl in self.channel_set if
189
                                            chnl.startswith('a')}
190
                    self.digital_channels = {chnl for chnl in self.channel_set if
191
                                             chnl.startswith('d')}
192
                elif element.channel_set != self.channel_set:
193
                    raise ValueError(
194
                        'Usage of different sets of analog and digital channels in the '
195
                        'same PulseBlock is prohibited. Used channel sets are:\n{0}\n{1}'
196
                        ''.format(self.channel_set, element.channel_set))
197
198
                add_length += element.init_length_s
199
                add_increment += element.increment_s
200
201
            for element in self.element_list[key]:
202
                self.init_length_s -= element.init_length_s
203
                self.increment_s -= element.increment_s
204
205
            self.init_length_s += add_length
206
            self.increment_s += add_increment
207
        else:
208
            raise TypeError('PulseBlock indices must be int or slice, not {0}'.format(type(key)))
209
        self.element_list[key] = copy.deepcopy(value)
210
        return
211
212
    def __delitem__(self, key):
213
        if not isinstance(key, (slice, int)):
214
            raise TypeError('PulseBlock indices must be int or slice, not {0}'.format(type(key)))
215
216
        if isinstance(key, int):
217
            items_to_delete = [self.element_list[key]]
218
        else:
219
            items_to_delete = self.element_list[key]
220
221
        for element in items_to_delete:
222
            self.init_length_s -= element.init_length_s
223
            self.increment_s -= element.increment_s
224
        del self.element_list[key]
225
        if len(self.element_list) == 0:
226
            self.init_length_s = 0.0
227
            self.increment_s = 0.0
228
        return
229
230
    def refresh_parameters(self):
231
        """ Initialize the parameters which describe this Pulse_Block object.
232
233
        The information is gained from all the Pulse_Block_Element objects,
234
        which are attached in the element_list.
235
        """
236
        # the Pulse_Block parameters
237
        self.init_length_s = 0.0
238
        self.increment_s = 0.0
239
        self.channel_set = set()
240
241
        for elem in self.element_list:
242
            self.init_length_s += elem.init_length_s
243
            self.increment_s += elem.increment_s
244
245
            if not self.channel_set:
246
                self.channel_set = elem.channel_set
247
            elif self.channel_set != elem.channel_set:
248
                raise ValueError('Usage of different sets of analog and digital channels in the '
249
                                 'same PulseBlock is prohibited.\nPulseBlock creation failed!\n'
250
                                 'Used channel sets are:\n{0}\n{1}'.format(self.channel_set,
251
                                                                           elem.channel_set))
252
                break
253
        self.analog_channels = {chnl for chnl in self.channel_set if chnl.startswith('a')}
254
        self.digital_channels = {chnl for chnl in self.channel_set if chnl.startswith('d')}
255
        return
256
257
    def pop(self, position=None):
258
        if len(self.element_list) == 0:
259
            raise IndexError('pop from empty PulseBlock')
260
261
        if position is None:
262
            self.init_length_s -= self.element_list[-1].init_length_s
263
            self.increment_s -= self.element_list[-1].increment_s
264
            return self.element_list.pop()
265
266
        if not isinstance(position, int):
267
            raise TypeError('PulseBlock.pop position argument expects integer, not {0}'
268
                            ''.format(type(position)))
269
270
        if position < 0:
271
            position = len(self.element_list) + position
272
273
        if len(self.element_list) <= position or position < 0:
274
            raise IndexError('PulseBlock element list index out of range')
275
276
        self.init_length_s -= self.element_list[position].init_length_s
277
        self.increment_s -= self.element_list[position].increment_s
278
        return self.element_list.pop(position)
279
280
    def insert(self, position, element):
281
        """ Insert a PulseBlockElement at the given position. The old element at this position and
282
        all consecutive elements after that will be shifted to higher indices.
283
284
        @param int position: position in the element list
285
        @param PulseBlockElement element: PulseBlockElement instance
286
        """
287
        if not isinstance(element, PulseBlockElement):
288
            raise ValueError('PulseBlock elements must be of type PulseBlockElement, not {0}'
289
                             ''.format(type(element)))
290
291
        if position < 0:
292
            position = len(self.element_list) + position
293
294
        if len(self.element_list) < position or position < 0:
295
            raise IndexError('PulseBlock element list index out of range')
296
297
        if not self.channel_set:
298
            self.channel_set = element.channel_set.copy()
299
            self.analog_channels = {chnl for chnl in self.channel_set if chnl.startswith('a')}
300
            self.digital_channels = {chnl for chnl in self.channel_set if chnl.startswith('d')}
301
        elif element.channel_set != self.channel_set:
302
            raise ValueError('Usage of different sets of analog and digital channels in the '
303
                             'same PulseBlock is prohibited. Used channel sets are:\n{0}\n{1}'
304
                             ''.format(self.channel_set, element.channel_set))
305
306
        self.init_length_s += element.init_length_s
307
        self.increment_s += element.increment_s
308
309
        self.element_list.insert(position, copy.deepcopy(element))
310
        return
311
312
    def append(self, element):
313
        """
314
        """
315
        self.insert(position=len(self.element_list), element=element)
316
        return
317
318
    def extend(self, iterable):
319
        for element in iterable:
320
            self.append(element=element)
321
        return
322
323
    def clear(self):
324
        del self.element_list[:]
325
        self.init_length_s = 0.0
326
        self.increment_s = 0.0
327
        self.analog_channels = set()
328
        self.digital_channels = set()
329
        self.channel_set = set()
330
        return
331
332
    def reverse(self):
333
        self.element_list.reverse()
334
        return
335
336
    def get_dict_representation(self):
337
        dict_repr = dict()
338
        dict_repr['name'] = self.name
339
        dict_repr['element_list'] = list()
340
        for element in self.element_list:
341
            dict_repr['element_list'].append(element.get_dict_representation())
342
        return dict_repr
343
344
    @staticmethod
345
    def block_from_dict(block_dict):
346
        for ii, element_dict in enumerate(block_dict['element_list']):
347
            block_dict['element_list'][ii] = PulseBlockElement.element_from_dict(element_dict)
348
        return PulseBlock(**block_dict)
349
350
351
class PulseBlockEnsemble(object):
352
    """
353
    Represents a collection of PulseBlock objects which is called a PulseBlockEnsemble.
354
355
    This object is used as a construction plan to create one sampled file.
356
    """
357
    def __init__(self, name, block_list=None, rotating_frame=True):
358
        """
359
        The constructor for a Pulse_Block_Ensemble needs to have:
360
361
        @param str name: chosen name for the PulseBlockEnsemble
362
        @param list block_list: contains the PulseBlock names with their number of repetitions,
363
                                e.g. [(name, repetitions), (name, repetitions), ...])
364
        @param bool rotating_frame: indicates whether the phase should be preserved for all the
365
                                    functions.
366
        """
367
        # FIXME: Sanity checking needed here
368
        self.name = name
369
        self.rotating_frame = rotating_frame
370
        if isinstance(block_list, list):
371
            self.block_list = block_list
372
        else:
373
            self.block_list = list()
374
375
        # Dictionary container to store information related to the actually sampled
376
        # Waveform like pulser settings used during sampling (sample_rate, activation_config etc.)
377
        # and additional information about the discretization of the waveform (timebin positions of
378
        # the PulseBlockElement transitions etc.) as well as the names of the created waveforms.
379
        # This container will be populated during sampling and will be emptied upon deletion of the
380
        # corresponding waveforms from the pulse generator
381
        self.sampling_information = dict()
382
        # Dictionary container to store additional information about for measurement settings
383
        # (ignore_lasers, controlled_variable, alternating etc.).
384
        # This container needs to be populated by the script creating the PulseBlockEnsemble
385
        # before saving it. (e.g. in generate methods in PulsedObjectGenerator class)
386
        self.measurement_information = dict()
387
        return
388
389
    def __repr__(self):
390
        repr_str = 'PulseBlockEnsemble(name=\'{0}\', block_list={1}, rotating_frame={2})'.format(
391
            self.name, repr(self.block_list), self.rotating_frame)
392
        return repr_str
393
394
    def __str__(self):
395
        return_str = 'PulseBlockEnsemble "{0}"\n\trotating frame: {1}\n\t' \
396
                     'has been sampled: {2}\n\t<block name>\t<repetitions>\n\t'.format(
397
                         self.name, self.rotating_frame, bool(self.sampling_information))
398
        return_str += '\n\t'.join(('{0}\t{1}'.format(name, reps) for name, reps in self.block_list))
399
        return return_str
400
401
    def __len__(self):
402
        return len(self.block_list)
403
404
    def __getitem__(self, key):
405
        if not isinstance(key, (slice, int)):
406
            raise TypeError('PulseBlockEnsemble indices must be int or slice, not {0}'
407
                            ''.format(type(key)))
408
        return self.block_list[key]
409
410
    def __setitem__(self, key, value):
411
        if isinstance(key, int):
412
            if not isinstance(value, (tuple, list)) or len(value) != 2:
413
                raise TypeError('PulseBlockEnsemble block list entries must be a tuple or list of '
414
                                'length 2')
415
            elif not isinstance(value[0], str):
416
                raise ValueError('PulseBlockEnsemble element tuple index 0 must contain str, '
417
                                 'not {0}'.format(type(value[0])))
418
            elif not isinstance(value[1], int) or value[1] < 0:
419
                raise ValueError('PulseBlockEnsemble element tuple index 1 must contain int >= 0')
420
        elif isinstance(key, slice):
421
            for element in value:
422
                if not isinstance(element, (tuple, list)) or len(value) != 2:
423
                    raise TypeError('PulseBlockEnsemble block list entries must be a tuple or list '
424
                                    'of length 2')
425
                elif not isinstance(element[0], str):
426
                    raise ValueError('PulseBlockEnsemble element tuple index 0 must contain str, '
427
                                     'not {0}'.format(type(element[0])))
428
                elif not isinstance(element[1], int) or element[1] < 0:
429
                    raise ValueError('PulseBlockEnsemble element tuple index 1 must contain int >= '
430
                                     '0')
431
        else:
432
            raise TypeError('PulseBlockEnsemble indices must be int or slice, not {0}'
433
                            ''.format(type(key)))
434
        self.block_list[key] = tuple(value)
435
        self.sampling_information = dict()
436
        self.measurement_information = dict()
437
        return
438
439
    def __delitem__(self, key):
440
        if not isinstance(key, (slice, int)):
441
            raise TypeError('PulseBlockEnsemble indices must be int or slice, not {0}'
442
                            ''.format(type(key)))
443
444
        del self.block_list[key]
445
        self.sampling_information = dict()
446
        self.measurement_information = dict()
447
        return
448
449
    def pop(self, position=None):
450
        if len(self.block_list) == 0:
451
            raise IndexError('pop from empty PulseBlockEnsemble')
452
453
        if position is None:
454
            self.sampling_information = dict()
455
            self.measurement_information = dict()
456
            return self.block_list.pop()
457
458
        if not isinstance(position, int):
459
            raise TypeError('PulseBlockEnsemble.pop position argument expects integer, not {0}'
460
                            ''.format(type(position)))
461
462
        if position < 0:
463
            position = len(self.block_list) + position
464
465
        if len(self.block_list) <= position or position < 0:
466
            raise IndexError('PulseBlockEnsemble block list index out of range')
467
468
        self.sampling_information = dict()
469
        self.measurement_information = dict()
470
        return self.block_list.pop(position)
471
472
    def insert(self, position, element):
473
        """ Insert a (PulseBlock.name, repetitions) tuple at the given position. The old element
474
        at this position and all consecutive elements after that will be shifted to higher indices.
475
476
        @param int position: position in the element list
477
        @param tuple element: (PulseBlock name (str), repetitions (int))
478
        """
479
        if not isinstance(element, (tuple, list)) or len(element) != 2:
480
            raise TypeError('PulseBlockEnsemble block list entries must be a tuple or list of '
481
                            'length 2')
482
        elif not isinstance(element[0], str):
483
            raise ValueError('PulseBlockEnsemble element tuple index 0 must contain str, '
484
                             'not {0}'.format(type(element[0])))
485
        elif not isinstance(element[1], int) or element[1] < 0:
486
            raise ValueError('PulseBlockEnsemble element tuple index 1 must contain int >= 0')
487
488
        if position < 0:
489
            position = len(self.block_list) + position
490
        if len(self.block_list) < position or position < 0:
491
            raise IndexError('PulseBlockEnsemble block list index out of range')
492
493
        self.block_list.insert(position, tuple(element))
494
        self.sampling_information = dict()
495
        self.measurement_information = dict()
496
        return
497
498
    def append(self, element):
499
        """
500
        """
501
        self.insert(position=len(self), element=element)
502
        return
503
504
    def extend(self, iterable):
505
        for element in iterable:
506
            self.append(element=element)
507
        return
508
509
    def clear(self):
510
        del self.block_list[:]
511
        self.sampling_information = dict()
512
        self.measurement_information = dict()
513
        return
514
515
    def reverse(self):
516
        self.block_list.reverse()
517
        self.sampling_information = dict()
518
        self.measurement_information = dict()
519
        return
520
521
    def get_dict_representation(self):
522
        dict_repr = dict()
523
        dict_repr['name'] = self.name
524
        dict_repr['rotating_frame'] = self.rotating_frame
525
        dict_repr['block_list'] = self.block_list
526
        dict_repr['sampling_information'] = self.sampling_information
527
        dict_repr['measurement_information'] = self.measurement_information
528
        return dict_repr
529
530
    @staticmethod
531
    def ensemble_from_dict(ensemble_dict):
532
        new_ens = PulseBlockEnsemble(name=ensemble_dict['name'],
533
                                     block_list=ensemble_dict['block_list'],
534
                                     rotating_frame=ensemble_dict['rotating_frame'])
535
        new_ens.sampling_information = ensemble_dict['sampling_information']
536
        new_ens.measurement_information = ensemble_dict['measurement_information']
537
        return new_ens
538
539
540
class PulseSequence(object):
541
    """
542
    Higher order object for sequence capability.
543
544
    Represents a playback procedure for a number of PulseBlockEnsembles. Unused for pulse
545
    generator hardware without sequencing functionality.
546
    """
547
    __default_seq_params = {'repetitions': 0,
548
                            'go_to': -1,
549
                            'event_jump_to': -1,
550
                            'event_trigger': 'OFF',
551
                            'wait_for': 'OFF',
552
                            'flag_trigger': 'OFF',
553
                            'flag_high': 'OFF'}
554
555
    def __init__(self, name, ensemble_list=None, rotating_frame=False):
556
        """
557
        The constructor for a PulseSequence objects needs to have:
558
559
        @param str name: the actual name of the sequence
560
        @param list ensemble_list: list containing a tuple of two entries:
561
                                          [(PulseBlockEnsemble name, seq_param),
562
                                           (PulseBlockEnsemble name, seq_param), ...]
563
                                          The seq_param is a dictionary, where the various sequence
564
                                          parameters are saved with their keywords and the
565
                                          according parameter (as item).
566
                                          Available parameters are:
567
                                          'repetitions': The number of repetitions for that sequence
568
                                                         step. (Default 0)
569
                                                         0 meaning the step is played once.
570
                                                         Set to -1 for infinite looping.
571
                                          'go_to':   The sequence step index to jump to after
572
                                                     having played all repetitions. (Default -1)
573
                                                     Indices starting at 1 for first step.
574
                                                     Set to 0 or -1 to follow up with the next step.
575
                                          'event_jump_to': The sequence step to jump to
576
                                                           (starting from 1) in case of a trigger
577
                                                           event (see event_trigger).
578
                                                           Setting it to 0 or -1 means jump to next
579
                                                           step. Ignored if event_trigger is 'OFF'.
580
                                          'event_trigger': The trigger input to listen to in order
581
                                                           to perform sequence jumps. Set to 'OFF'
582
                                                           (default) in order to ignore triggering.
583
                                          'wait_for': The trigger input to wait for before playing
584
                                                      this sequence step. Set to 'OFF' (default)
585
                                                      in order to play the current step immediately.
586
                                          'flag_trigger': The flag to trigger when this sequence
587
                                                          step starts playing. Select 'OFF'
588
                                                          (default) for no flag trigger.
589
                                          'flag_high': The flag to set to high while this step is
590
                                                       playing. Select 'OFF' (default) to set all
591
                                                       flags to low.
592
593
                                          If only 'repetitions' are in the dictionary, then the dict
594
                                          will look like:
595
                                            seq_param = {'repetitions': 41}
596
                                          and so the respective sequence step will play 42 times.
597
        @param bool rotating_frame: indicates, whether the phase has to be preserved in all
598
                                    analog signals ACROSS different waveforms
599
        """
600
        self.name = name
601
        self.rotating_frame = rotating_frame
602
        self.ensemble_list = list() if ensemble_list is None else ensemble_list
603
        self.is_finite = True
604
        self.refresh_parameters()
605
606
        # self.sampled_ensembles = OrderedDict()
607
        # Dictionary container to store information related to the actually sampled
608
        # Waveforms like pulser settings used during sampling (sample_rate, activation_config etc.)
609
        # and additional information about the discretization of the waveform (timebin positions of
610
        # the PulseBlockElement transitions etc.)
611
        # This container is not necessary for the sampling process but serves only the purpose of
612
        # holding optional information for different modules.
613
        self.sampling_information = dict()
614
        # Dictionary container to store additional information about for measurement settings
615
        # (ignore_lasers, controlled_values, alternating etc.).
616
        # This container needs to be populated by the script creating the PulseSequence
617
        # before saving it.
618
        self.measurement_information = dict()
619
        return
620
621
    def refresh_parameters(self):
622
        self.is_finite = True
623
        for ensemble_name, params in self.ensemble_list:
624
            if params['repetitions'] < 0:
625
                self.is_finite = False
626
                break
627
        return
628
629
    def __repr__(self):
630
        repr_str = 'PulseSequence(name=\'{0}\', ensemble_list={1}, rotating_frame={2})'.format(
631
            self.name, self.ensemble_list, self.rotating_frame)
632
        return repr_str
633
634
    def __str__(self):
635
        return_str = 'PulseSequence "{0}"\n\trotating frame: {1}\n\t' \
636
                     'has finite length: {2}\n\thas been sampled: {3}\n\t<ensemble name>\t' \
637
                     '<sequence parameters>\n\t'.format(self.name,
638
                                                        self.rotating_frame,
639
                                                        self.is_finite,
640
                                                        bool(self.sampling_information))
641
        return_str += '\n\t'.join(('{0}\t{1}'.format(name, param) for name, param in self))
642
        return return_str
643
644
    def __len__(self):
645
        return len(self.ensemble_list)
646
647
    def __getitem__(self, key):
648
        if not isinstance(key, (slice, int)):
649
            raise TypeError('PulseSequence indices must be int or slice, not {0}'.format(type(key)))
650
        return self.ensemble_list[key]
651
652
    def __setitem__(self, key, value):
653
        stage_refresh = False
654
        if isinstance(key, int):
655
            if isinstance(value, str):
656
                value = (value, self.__default_seq_params.copy())
657
            if not isinstance(value, (tuple, list)) or len(value) != 2:
658
                raise TypeError('PulseSequence ensemble list entries must be a tuple or list of '
659
                                'length 2')
660
            elif not isinstance(value[0], str):
661
                raise ValueError('PulseSequence element tuple index 0 must contain str, not {0}'
662
                                 ''.format(type(value[0])))
663
            elif not isinstance(value[1], dict):
664
                raise ValueError('PulseSequence element tuple index 1 must contain dict, not {0}'
665
                                 ''.format(type(value[1])))
666
667
            if value[1]['repetitions'] < 0:
668
                self.is_finite = False
669
            elif not self.is_finite and self[key][1]['repetitions'] < 0:
670
                stage_refresh = True
671
        elif isinstance(key, slice):
672
            if isinstance(value[0], str):
673
                tmp_value = list()
674
                for element in value:
675
                    tmp_value.append((element, self.__default_seq_params.copy()))
676
                value = tmp_value
677
            for element in value:
678
                if not isinstance(element, (tuple, list)) or len(value) != 2:
679
                    raise TypeError('PulseSequence block list entries must be a tuple or list '
680
                                    'of length 2')
681
                elif not isinstance(element[0], str):
682
                    raise ValueError('PulseSequence element tuple index 0 must contain str, not {0}'
683
                                     ''.format(type(element[0])))
684
                elif not isinstance(element[1], dict):
685
                    raise ValueError('PulseSequence element tuple index 1 must contain dict, not '
686
                                     '{0}'.format(type(element[1])))
687
688
                if element[1]['repetitions'] < 0:
689
                    self.is_finite = False
690
                elif not self.is_finite:
691
                    stage_refresh = True
692
        else:
693
            raise TypeError('PulseSequence indices must be int or slice, not {0}'.format(type(key)))
694
        self.ensemble_list[key] = tuple(value)
695
        self.sampling_information = dict()
696
        self.measurement_information = dict()
697
        if stage_refresh:
698
            self.refresh_parameters()
699
        return
700
701
    def __delitem__(self, key):
702
        if isinstance(key, slice):
703
            stage_refresh = False
704
            for element in self.ensemble_list[key]:
705
                if element[1]['repetitions'] < 0:
706
                    stage_refresh = True
707
                    break
708
        elif isinstance(key, int):
709
            stage_refresh = self.ensemble_list[key][1]['repetitions'] < 0
710
        else:
711
            raise TypeError('PulseSequence indices must be int or slice, not {0}'.format(type(key)))
712
        del self.ensemble_list[key]
713
        self.sampling_information = dict()
714
        self.measurement_information = dict()
715
        if stage_refresh:
716
            self.refresh_parameters()
717
        return
718
719
    def pop(self, position=None):
720
        if len(self.ensemble_list) == 0:
721
            raise IndexError('pop from empty PulseSequence')
722
723
        if position is None:
724
            position = len(self.ensemble_list) - 1
725
726
        if not isinstance(position, int):
727
            raise TypeError('PulseSequence.pop position argument expects integer, not {0}'
728
                            ''.format(type(position)))
729
730
        if position < 0:
731
            position = len(self.ensemble_list) + position
732
733
        if len(self.ensemble_list) <= position or position < 0:
734
            raise IndexError('PulseSequence ensemble list index out of range')
735
736
        self.sampling_information = dict()
737
        self.measurement_information = dict()
738
        if self.ensemble_list[-1][1]['repetitions'] < 0:
739
            popped_element = self.ensemble_list.pop(position)
740
            self.refresh_parameters()
741
            return popped_element
742
        return self.ensemble_list.pop(position)
743
744
    def insert(self, position, element):
745
        """ Insert a (PulseSequence.name, parameters) tuple at the given position. The old element
746
        at this position and all consecutive elements after that will be shifted to higher indices.
747
748
        @param int position: position in the ensemble list
749
        @param tuple|str element: PulseBlock name (str)[, seq_parameters (dict)]
750
        """
751
        if isinstance(element, str):
752
            element = (element, self.__default_seq_params.copy())
753
754
        if not isinstance(element, (tuple, list)) or len(element) != 2:
755
            raise TypeError('PulseSequence ensemble list entries must be a tuple or list of '
756
                            'length 2')
757
        elif not isinstance(element[0], str):
758
            raise ValueError('PulseSequence element tuple index 0 must contain str, '
759
                             'not {0}'.format(type(element[0])))
760
        elif not isinstance(element[1], dict):
761
            raise ValueError('PulseSequence element tuple index 1 must contain dict')
762
763
        if position < 0:
764
            position = len(self.ensemble_list) + position
765
        if len(self.ensemble_list) < position or position < 0:
766
            raise IndexError('PulseSequence ensemble list index out of range')
767
768
        self.ensemble_list.insert(position, tuple(element))
769
        if element[1]['repetitions'] < 0:
770
            self.is_finite = False
771
        self.sampling_information = dict()
772
        self.measurement_information = dict()
773
        return
774
775
    def append(self, element):
776
        """
777
        """
778
        self.insert(position=len(self), element=element)
779
        return
780
781
    def extend(self, iterable):
782
        for element in iterable:
783
            self.append(element=element)
784
        return
785
786
    def clear(self):
787
        del self.ensemble_list[:]
788
        self.sampling_information = dict()
789
        self.measurement_information = dict()
790
        self.is_finite = True
791
        return
792
793
    def reverse(self):
794
        self.ensemble_list.reverse()
795
        self.sampling_information = dict()
796
        self.measurement_information = dict()
797
        return
798
799
    def get_dict_representation(self):
800
        dict_repr = dict()
801
        dict_repr['name'] = self.name
802
        dict_repr['rotating_frame'] = self.rotating_frame
803
        dict_repr['ensemble_list'] = self.ensemble_list
804
        dict_repr['sampling_information'] = self.sampling_information
805
        dict_repr['measurement_information'] = self.measurement_information
806
        return dict_repr
807
808
    @staticmethod
809
    def sequence_from_dict(sequence_dict):
810
        new_seq = PulseSequence(name=sequence_dict['name'],
811
                                ensemble_list=sequence_dict['ensemble_list'],
812
                                rotating_frame=sequence_dict['rotating_frame'])
813
        new_seq.sampling_information = sequence_dict['sampling_information']
814
        new_seq.measurement_information = sequence_dict['measurement_information']
815
        return new_seq
816
817
818
class PredefinedGeneratorBase:
819
    """
820
    Base class for PulseObjectGenerator and predefined generator classes containing the actual
821
    "generate_"-methods.
822
823
    This class holds a protected reference to the SequenceGeneratorLogic and provides read-only
824
    access via properties to various attributes of the logic module.
825
    SequenceGeneratorLogic logger is also accessible via this base class and can be used as in any
826
    qudi module (e.g. self.log.error(...)).
827
    Also provides helper methods to simplify sequence/ensemble generation.
828
    """
829
    def __init__(self, sequencegeneratorlogic):
830
        # Keep protected reference to the SequenceGeneratorLogic
831
        self.__sequencegeneratorlogic = sequencegeneratorlogic
832
833
    @property
834
    def log(self):
835
        return self.__sequencegeneratorlogic.log
836
837
    @property
838
    def pulse_generator_settings(self):
839
        return self.__sequencegeneratorlogic.pulse_generator_settings
840
841
    @property
842
    def generation_parameters(self):
843
        return self.__sequencegeneratorlogic.generation_parameters
844
845
    @property
846
    def channel_set(self):
847
        channels = self.pulse_generator_settings.get('activation_config')
848
        if channels is None:
849
            channels = ('', set())
850
        return channels[1]
851
852
    @property
853
    def analog_channels(self):
854
        return {chnl for chnl in self.channel_set if chnl.startswith('a')}
855
856
    @property
857
    def digital_channels(self):
858
        return {chnl for chnl in self.channel_set if chnl.startswith('d')}
859
860
    @property
861
    def laser_channel(self):
862
        return self.generation_parameters.get('laser_channel')
863
864
    @property
865
    def sync_channel(self):
866
        channel = self.generation_parameters.get('sync_channel')
867
        return None if channel == '' else channel
868
869
    @property
870
    def gate_channel(self):
871
        channel = self.generation_parameters.get('gate_channel')
872
        return None if channel == '' else channel
873
874
    @property
875
    def analog_trigger_voltage(self):
876
        return self.generation_parameters.get('analog_trigger_voltage')
877
878
    @property
879
    def laser_delay(self):
880
        return self.generation_parameters.get('laser_delay')
881
882
    @property
883
    def microwave_channel(self):
884
        channel = self.generation_parameters.get('microwave_channel')
885
        return None if channel == '' else channel
886
887
    @property
888
    def microwave_frequency(self):
889
        return self.generation_parameters.get('microwave_frequency')
890
891
    @property
892
    def microwave_amplitude(self):
893
        return self.generation_parameters.get('microwave_amplitude')
894
895
    @property
896
    def laser_length(self):
897
        return self.generation_parameters.get('laser_length')
898
899
    @property
900
    def wait_time(self):
901
        return self.generation_parameters.get('wait_time')
902
903
    @property
904
    def rabi_period(self):
905
        return self.generation_parameters.get('rabi_period')
906
907
    ################################################################################################
908
    #                                   Helper methods                                          ####
909
    ################################################################################################
910
    def _get_idle_element(self, length, increment):
911
        """
912
        Creates an idle pulse PulseBlockElement
913
914
        @param float length: idle duration in seconds
915
        @param float increment: idle duration increment in seconds
916
917
        @return: PulseBlockElement, the generated idle element
918
        """
919
        # Create idle element
920
        return PulseBlockElement(
921
            init_length_s=length,
922
            increment_s=increment,
923
            pulse_function={chnl: SamplingFunctions.Idle() for chnl in self.analog_channels},
924
            digital_high={chnl: False for chnl in self.digital_channels})
925
926
    def _get_trigger_element(self, length, increment, channels):
927
        """
928
        Creates a trigger PulseBlockElement
929
930
        @param float length: trigger duration in seconds
931
        @param float increment: trigger duration increment in seconds
932
        @param str|list channels: The pulser channel(s) to be triggered.
933
934
        @return: PulseBlockElement, the generated trigger element
935
        """
936
        if isinstance(channels, str):
937
            channels = [channels]
938
939
        # input params for element generation
940
        pulse_function = {chnl: SamplingFunctions.Idle() for chnl in self.analog_channels}
941
        digital_high = {chnl: False for chnl in self.digital_channels}
942
943
        # Determine analogue or digital trigger channel and set channels accordingly.
944
        for channel in channels:
945
            if channel.startswith('d'):
946
                digital_high[channel] = True
947
            else:
948
                pulse_function[channel] = SamplingFunctions.DC(voltage=self.analog_trigger_voltage)
949
950
        # return trigger element
951
        return PulseBlockElement(init_length_s=length,
952
                                 increment_s=increment,
953
                                 pulse_function=pulse_function,
954
                                 digital_high=digital_high)
955
956
    def _get_laser_element(self, length, increment):
957
        """
958
        Creates laser trigger PulseBlockElement
959
960
        @param float length: laser pulse duration in seconds
961
        @param float increment: laser pulse duration increment in seconds
962
963
        @return: PulseBlockElement, two elements for laser and gate trigger (delay element)
964
        """
965
        return self._get_trigger_element(length=length,
966
                                         increment=increment,
967
                                         channels=self.laser_channel)
968
969
    def _get_laser_gate_element(self, length, increment):
970
        """
971
        """
972
        laser_gate_element = self._get_laser_element(length=length,
973
                                                     increment=increment)
974
        if self.gate_channel:
975
            if self.gate_channel.startswith('d'):
976
                laser_gate_element.digital_high[self.gate_channel] = True
977
            else:
978
                laser_gate_element.pulse_function[self.gate_channel] = SamplingFunctions.DC(
979
                    voltage=self.analog_trigger_voltage)
980
        return laser_gate_element
981
982
    def _get_delay_element(self):
983
        """
984
        Creates an idle element of length of the laser delay
985
986
        @return PulseBlockElement: The delay element
987
        """
988
        return self._get_idle_element(length=self.laser_delay,
989
                                      increment=0)
990
991
    def _get_delay_gate_element(self):
992
        """
993
        Creates a gate trigger of length of the laser delay.
994
        If no gate channel is specified will return a simple idle element.
995
996
        @return PulseBlockElement: The delay element
997
        """
998
        if self.gate_channel:
999
            return self._get_trigger_element(length=self.laser_delay,
1000
                                             increment=0,
1001
                                             channels=self.gate_channel)
1002
        else:
1003
            return self._get_delay_element()
1004
1005
    def _get_sync_element(self):
1006
        """
1007
1008
        """
1009
        return self._get_trigger_element(length=50e-9,
1010
                                         increment=0,
1011
                                         channels=self.sync_channel)
1012
1013
    def _get_mw_element(self, length, increment, amp=None, freq=None, phase=None):
1014
        """
1015
        Creates a MW pulse PulseBlockElement
1016
1017
        @param float length: MW pulse duration in seconds
1018
        @param float increment: MW pulse duration increment in seconds
1019
        @param float freq: MW frequency in case of analogue MW channel in Hz
1020
        @param float amp: MW amplitude in case of analogue MW channel in V
1021
        @param float phase: MW phase in case of analogue MW channel in deg
1022
1023
        @return: PulseBlockElement, the generated MW element
1024
        """
1025
        if self.microwave_channel.startswith('d'):
1026
            mw_element = self._get_trigger_element(
1027
                length=length,
1028
                increment=increment,
1029
                channels=self.microwave_channel)
1030
        else:
1031
            mw_element = self._get_idle_element(
1032
                length=length,
1033
                increment=increment)
1034
            mw_element.pulse_function[self.microwave_channel] = SamplingFunctions.Sin(
1035
                amplitude=amp,
1036
                frequency=freq,
1037
                phase=phase)
1038
        return mw_element
1039
1040
    def _get_multiple_mw_element(self, length, increment, amps=None, freqs=None, phases=None):
1041
        """
1042
        Creates single, double or triple sine mw element.
1043
1044
        @param float length: MW pulse duration in seconds
1045
        @param float increment: MW pulse duration increment in seconds
1046
        @param amps: list containing the amplitudes
1047
        @param freqs: list containing the frequencies
1048
        @param phases: list containing the phases
1049
        @return: PulseBlockElement, the generated MW element
1050
        """
1051
        if isinstance(amps, (int, float)):
1052
            amps = [amps]
1053
        if isinstance(freqs, (int, float)):
1054
            freqs = [freqs]
1055
        if isinstance(phases, (int, float)):
1056
            phases = [phases]
1057
1058
        if self.microwave_channel.startswith('d'):
1059
            mw_element = self._get_trigger_element(
1060
                length=length,
1061
                increment=increment,
1062
                channels=self.microwave_channel)
1063
        else:
1064
            mw_element = self._get_idle_element(
1065
                length=length,
1066
                increment=increment)
1067
1068
            sine_number = min(len(amps), len(freqs), len(phases))
1069
1070
            if sine_number < 2:
1071
                mw_element.pulse_function[self.microwave_channel] = SamplingFunctions.Sin(
1072
                    amplitude=amps[0],
1073
                    frequency=freqs[0],
1074
                    phase=phases[0])
1075
            elif sine_number == 2:
1076
                mw_element.pulse_function[self.microwave_channel] = SamplingFunctions.DoubleSin(
1077
                    amplitude_1=amps[0],
1078
                    amplitude_2=amps[1],
1079
                    frequency_1=freqs[0],
1080
                    frequency_2=freqs[1],
1081
                    phase_1=phases[0],
1082
                    phase_2=phases[1])
1083
            else:
1084
                mw_element.pulse_function[self.microwave_channel] = SamplingFunctions.TripleSin(
1085
                    amplitude_1=amps[0],
1086
                    amplitude_2=amps[1],
1087
                    amplitude_3=amps[2],
1088
                    frequency_1=freqs[0],
1089
                    frequency_2=freqs[1],
1090
                    frequency_3=freqs[2],
1091
                    phase_1=phases[0],
1092
                    phase_2=phases[1],
1093
                    phase_3=phases[2])
1094
        return mw_element
1095
1096
    def _get_mw_laser_element(self, length, increment, amp=None, freq=None, phase=None):
1097
        """
1098
1099
        @param length:
1100
        @param increment:
1101
        @param amp:
1102
        @param freq:
1103
        @param phase:
1104
        @return:
1105
        """
1106
        mw_laser_element = self._get_mw_element(length=length,
1107
                                                increment=increment,
1108
                                                amp=amp,
1109
                                                freq=freq,
1110
                                                phase=phase)
1111
        if self.laser_channel.startswith('d'):
1112
            mw_laser_element.digital_high[self.laser_channel] = True
1113
        else:
1114
            mw_laser_element.pulse_function[self.laser_channel] = SamplingFunctions.DC(
1115
                voltage=self.analog_trigger_voltage)
1116
        return mw_laser_element
1117
1118
    def _get_ensemble_count_length(self, ensemble, created_blocks):
1119
        """
1120
1121
        @param ensemble:
1122
        @param created_blocks:
1123
        @return:
1124
        """
1125
        if self.gate_channel:
1126
            length = self.laser_length + self.laser_delay
1127
        else:
1128
            blocks = {block.name: block for block in created_blocks}
1129
            length = 0.0
1130
            for block_name, reps in ensemble.block_list:
1131
                length += blocks[block_name].init_length_s * (reps + 1)
1132
                length += blocks[block_name].increment_s * ((reps ** 2 + reps) / 2)
1133
        return length
1134
1135
1136
class PulseObjectGenerator(PredefinedGeneratorBase):
1137
    """
1138
1139
    """
1140
    def __init__(self, sequencegeneratorlogic):
1141
        # Initialize base class
1142
        super().__init__(sequencegeneratorlogic)
1143
1144
        # dictionary containing references to all generation methods imported from generator class
1145
        # modules. The keys are the method names excluding the prefix "generate_".
1146
        self._generate_methods = dict()
1147
        # nested dictionary with keys being the generation method names and values being a
1148
        # dictionary containing all keyword arguments as keys with their default value
1149
        self._generate_method_parameters = dict()
1150
1151
        # import path for generator modules from default dir (logic.predefined_generate_methods)
1152
        path_list = [os.path.join(get_main_dir(), 'logic', 'pulsed', 'predefined_generate_methods')]
1153
        # import path for generator modules from non-default directory if a path has been given
1154
        if isinstance(sequencegeneratorlogic.additional_methods_dir, str):
1155
            path_list.append(sequencegeneratorlogic.additional_methods_dir)
1156
1157
        # Import predefined generator modules and get a list of generator classes
1158
        generator_classes = self.__import_external_generators(paths=path_list)
1159
1160
        # create an instance of each class and put them in a temporary list
1161
        generator_instances = [cls(sequencegeneratorlogic) for cls in generator_classes]
1162
1163
        # add references to all generate methods in each instance to a dict
1164
        self.__populate_method_dict(instance_list=generator_instances)
1165
1166
        # populate parameters dictionary from generate method signatures
1167
        self.__populate_parameter_dict()
1168
1169
    @property
1170
    def predefined_generate_methods(self):
1171
        return self._generate_methods
1172
1173
    @property
1174
    def predefined_method_parameters(self):
1175
        return self._generate_method_parameters.copy()
1176
1177 View Code Duplication
    def __import_external_generators(self, paths):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1178
        """
1179
        Helper method to import all modules from directories contained in paths.
1180
        Find all classes in those modules that inherit exclusively from PredefinedGeneratorBase
1181
        class and return a list of them.
1182
1183
        @param iterable paths: iterable containing paths to import modules from
1184
        @return list: A list of imported valid generator classes
1185
        """
1186
        class_list = list()
1187
        for path in paths:
1188
            if not os.path.exists(path):
1189
                self.log.error('Unable to import generate methods from "{0}".\n'
1190
                               'Path does not exist.'.format(path))
1191
                continue
1192
            # Get all python modules to import from.
1193
            # The assumption is that in the path, there are *.py files,
1194
            # which contain only generator classes!
1195
            module_list = [name[:-3] for name in os.listdir(path) if
1196
                           os.path.isfile(os.path.join(path, name)) and name.endswith('.py')]
1197
1198
            # append import path to sys.path
1199
            sys.path.append(path)
1200
1201
            # Go through all modules and create instances of each class found.
1202
            for module_name in module_list:
1203
                # import module
1204
                mod = importlib.import_module('{0}'.format(module_name))
1205
                importlib.reload(mod)
1206
                # get all generator class references defined in the module
1207
                tmp_list = [m[1] for m in inspect.getmembers(mod, self.is_generator_class)]
1208
                # append to class_list
1209
                class_list.extend(tmp_list)
1210
        return class_list
1211
1212
    def __populate_method_dict(self, instance_list):
1213
        """
1214
        Helper method to populate the dictionaries containing all references to callable generate
1215
        methods contained in generator instances passed to this method.
1216
1217
        @param list instance_list: List containing instances of generator classes
1218
        """
1219
        self._generate_methods = dict()
1220
        for instance in instance_list:
1221
            for method_name, method_ref in inspect.getmembers(instance, inspect.ismethod):
1222
                if method_name.startswith('generate_'):
1223
                    self._generate_methods[method_name[9:]] = method_ref
1224
        return
1225
1226
    def __populate_parameter_dict(self):
1227
        """
1228
        Helper method to populate the dictionary containing all possible keyword arguments from all
1229
        generate methods.
1230
        """
1231
        self._generate_method_parameters = dict()
1232
        for method_name, method in self._generate_methods.items():
1233
            method_signature = inspect.signature(method)
1234
            param_dict = dict()
1235
            for name, param in method_signature.parameters.items():
1236
                param_dict[name] = None if param.default is param.empty else param.default
1237
1238
            self._generate_method_parameters[method_name] = param_dict
1239
        return
1240
1241
    @staticmethod
1242
    def is_generator_class(obj):
1243
        """
1244
        Helper method to check if an object is a valid generator class.
1245
1246
        @param object obj: object to check
1247
        @return bool: True if obj is a valid generator class, False otherwise
1248
        """
1249
        if inspect.isclass(obj):
1250
            return PredefinedGeneratorBase in obj.__bases__ and len(obj.__bases__) == 1
1251
        return False
1252
1253
1254
1255