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

OkFpgaPulser   F

Complexity

Total Complexity 64

Size/Duplication

Total Lines 678
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 678
rs 2.9441
wmc 64

35 Methods

Rating   Name   Duplication   Size   Complexity  
A get_constraints() 0 68 1
A _recover_current_waveform() 0 3 1
A query() 0 9 1
A on_activate() 0 6 1
A set_analog_level() 0 20 1
A get_status() 0 14 1
A write_sequence() 0 12 1
A set_sample_rate() 0 23 2
A delete_waveform() 0 9 1
A __init__() 0 7 1
A load_sequence() 0 21 1
B get_active_channels() 0 28 3
A reset() 0 9 1
A get_waveform_names() 0 6 1
A has_sequence_mode() 0 6 1
A set_active_channels() 0 22 1
A get_sample_rate() 0 6 1
C write_waveform() 0 74 7
A pulser_off() 0 6 1
A write() 0 12 2
A set_digital_level() 0 21 1
A pulser_on() 0 6 1
A clear_all() 0 9 1
A get_sequence_names() 0 6 1
A get_loaded_assets() 0 15 3
A set_interleave() 0 17 2
A delete_sequence() 0 9 1
C get_digital_level() 0 33 7
A _convert_current_waveform() 0 3 1
A get_interleave() 0 8 1
A on_deactivate() 0 2 1
F load_waveform() 0 82 10
B get_analog_level() 0 24 1
A _disconnect_fpga() 0 9 1
A _connect_fpga() 0 15 2

How to fix   Complexity   

Complex Class

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

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

1
# -*- coding: utf-8 -*-
2
"""
3
Use OK FPGA as a digital pulse sequence generator.
4
5
Qudi is free software: you can redistribute it and/or modify
6
it under the terms of the GNU General Public License as published by
7
the Free Software Foundation, either version 3 of the License, or
8
(at your option) any later version.
9
10
Qudi is distributed in the hope that it will be useful,
11
but WITHOUT ANY WARRANTY; without even the implied warranty of
12
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
GNU General Public License for more details.
14
15
You should have received a copy of the GNU General Public License
16
along with Qudi. If not, see <http://www.gnu.org/licenses/>.
17
18
Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the
19
top-level directory of this distribution and at <https://github.com/Ulm-IQO/qudi/>
20
"""
21
22
from core.module import Base, ConfigOption, StatusVar
23
from core.util.modules import get_main_dir
24
from interface.pulser_interface import PulserInterface, PulserConstraints
25
import okfrontpanel as ok
26
import numpy as np
27
import time
28
import os
29
from collections import OrderedDict
30
31
32
class OkFpgaPulser(Base, PulserInterface):
33
    """Methods to control Pulse Generator running on OK FPGA.
34
35
    Chan   PIN
36
    ----------
37
    Ch1    A3
38
    Ch2    C5
39
    Ch3    D6
40
    Ch4    B6
41
    Ch5    C7
42
    Ch6    B8
43
    Ch7    D9
44
    Ch8    C9
45
    """
46
    _modclass = 'pulserinterface'
47
    _modtype = 'hardware'
48
49
    _fpga_serial = ConfigOption(name='fpga_serial', missing='error')
50
    _fpga_type = ConfigOption(name='fpga_type', default='XEM6310_LX150', missing='warn')
51
52
    __current_waveform = StatusVar(name='current_waveform', default=np.zeros(1, dtype='uint8'))
53
    __current_waveform_name = StatusVar(name='current_waveform_name', default='')
54
    __sample_rate = StatusVar(name='sample_rate', default=950e6)
55
 
56
    def __init__(self, config, **kwargs):
57
        super().__init__(config=config, **kwargs)
58
59
        self.__current_status = -1
60
        self.__currently_loaded_waveform = ''  # loaded and armed waveform name
61
        self.__samples_written = 0
62
        self.fpga = None  # Reference to the OK FrontPanel instance
63
64
    def on_activate(self):
65
        self.__samples_written = 0
66
        self.__currently_loaded_waveform = ''
67
        self.fpga = ok.FrontPanel()
68
        self._connect_fpga()
69
        self.set_sample_rate(self.__sample_rate)
70
71
    def on_deactivate(self):
72
        self._disconnect_fpga()
73
74
    @__current_waveform.representer
75
    def _convert_current_waveform(self, waveform_bytearray):
76
        return np.frombuffer(waveform_bytearray, dtype='uint8')
77
78
    @__current_waveform.constructor
79
    def _recover_current_waveform(self, waveform_nparray):
80
        return bytearray(waveform_nparray.tobytes())
81
82
    def get_constraints(self):
83
        """
84
        Retrieve the hardware constrains from the Pulsing device.
85
86
        @return constraints object: object with pulser constraints as attributes.
87
88
        Provides all the constraints (e.g. sample_rate, amplitude, total_length_bins,
89
        channel_config, ...) related to the pulse generator hardware to the caller.
90
91
            SEE PulserConstraints CLASS IN pulser_interface.py FOR AVAILABLE CONSTRAINTS!!!
92
93
        If you are not sure about the meaning, look in other hardware files to get an impression.
94
        If still additional constraints are needed, then they have to be added to the
95
        PulserConstraints class.
96
97
        Each scalar parameter is an ScalarConstraints object defined in cor.util.interfaces.
98
        Essentially it contains min/max values as well as min step size, default value and unit of
99
        the parameter.
100
101
        PulserConstraints.activation_config differs, since it contain the channel
102
        configuration/activation information of the form:
103
            {<descriptor_str>: <channel_set>,
104
             <descriptor_str>: <channel_set>,
105
             ...}
106
107
        If the constraints cannot be set in the pulsing hardware (e.g. because it might have no
108
        sequence mode) just leave it out so that the default is used (only zeros).
109
        """
110
        constraints = PulserConstraints()
111
112
        constraints.sample_rate.min = 500e6
113
        constraints.sample_rate.max = 950e6
114
        constraints.sample_rate.step = 450e6
115
        constraints.sample_rate.default = 950e6
116
117
        constraints.a_ch_amplitude.min = 0.0
118
        constraints.a_ch_amplitude.max = 0.0
119
        constraints.a_ch_amplitude.step = 0.0
120
        constraints.a_ch_amplitude.default = 0.0
121
122
        constraints.a_ch_offset.min = 0.0
123
        constraints.a_ch_offset.max = 0.0
124
        constraints.a_ch_offset.step = 0.0
125
        constraints.a_ch_offset.default = 0.0
126
127
        constraints.d_ch_low.min = 0.0
128
        constraints.d_ch_low.max = 0.0
129
        constraints.d_ch_low.step = 0.0
130
        constraints.d_ch_low.default = 0.0
131
132
        constraints.d_ch_high.min = 3.3
133
        constraints.d_ch_high.max = 3.3
134
        constraints.d_ch_high.step = 0.0
135
        constraints.d_ch_high.default = 3.3
136
137
        constraints.waveform_length.min = 1024
138
        constraints.waveform_length.max = 134217728
139
        constraints.waveform_length.step = 1
140
        constraints.waveform_length.default = 1024
141
142
        # the name a_ch<num> and d_ch<num> are generic names, which describe UNAMBIGUOUSLY the
143
        # channels. Here all possible channel configurations are stated, where only the generic
144
        # names should be used. The names for the different configurations can be customary chosen.
145
        activation_config = OrderedDict()
146
        activation_config['all'] = {'d_ch1', 'd_ch2', 'd_ch3', 'd_ch4',
147
                                    'd_ch5', 'd_ch6', 'd_ch7', 'd_ch8'}
148
        constraints.activation_config = activation_config
149
        return constraints
150
151
    def pulser_on(self):
152
        """ Switches the pulsing device on.
153
154
        @return int: error code (0:OK, -1:error)
155
        """
156
        return self.write(0x01)
157
158
    def pulser_off(self):
159
        """ Switches the pulsing device off.
160
161
        @return int: error code (0:OK, -1:error)
162
        """
163
        return self.write(0x00)
164
165
    def load_waveform(self, load_dict):
166
        """ Loads a waveform to the specified channel of the pulsing device.
167
        For devices that have a workspace (i.e. AWG) this will load the waveform from the device
168
        workspace into the channel.
169
        For a device without mass memory this will make the waveform/pattern that has been
170
        previously written with self.write_waveform ready to play.
171
172
        @param load_dict:  dict|list, a dictionary with keys being one of the available channel
173
                                      index and values being the name of the already written
174
                                      waveform to load into the channel.
175
                                      Examples:   {1: rabi_ch1, 2: rabi_ch2} or
176
                                                  {1: rabi_ch2, 2: rabi_ch1}
177
                                      If just a list of waveform names if given, the channel
178
                                      association will be invoked from the channel
179
                                      suffix '_ch1', '_ch2' etc.
180
181
        @return dict: Dictionary containing the actually loaded waveforms per channel.
182
        """
183
        # Since only one waveform can be present at a time check if only a single name is given
184
        if isinstance(load_dict, list):
185
            waveforms = list(set(load_dict))
186
        elif isinstance(load_dict, dict):
187
            waveforms = list(set(load_dict.values()))
188
        else:
189
            self.log.error('Method load_waveform expects a list of waveform names or a dict.')
190
            return self.get_loaded_assets()
191
192
        if len(waveforms) != 1:
193
            self.log.error('FPGA pulser expects exactly one waveform name for load_waveform.')
194
            return self.get_loaded_assets()
195
196
        waveform = waveforms[0]
197
        if waveform != self.__current_waveform_name:
198
            self.log.error('No waveform by the name "{0}" generated for FPGA pulser.\n'
199
                           'Only one waveform at a time can be held.'.format(waveform))
200
            return self.get_loaded_assets()
201
202
        # calculate size of the two bytearrays to be transmitted. The biggest part is tranfered
203
        # in 1024 byte blocks and the rest is transfered in 32 byte blocks
204
        big_bytesize = (len(self.__current_waveform) // 1024) * 1024
205
        small_bytesize = len(self.__current_waveform) - big_bytesize
206
207
        # try repeatedly to upload the samples to the FPGA RAM
208
        # stop if the upload was successful
209
        loop_count = 0
210
        while True:
211
            loop_count += 1
212
            # reset FPGA
213
            self.reset()
214
            # upload sequence
215
            if big_bytesize != 0:
216
                # enable sequence write mode in FPGA
217
                self.write((255 << 24) + 2)
218
                # write to FPGA DDR2-RAM
219
                self.fpga.WriteToBlockPipeIn(0x80, 1024, self.__current_waveform[0:big_bytesize])
220
            if small_bytesize != 0:
221
                # enable sequence write mode in FPGA
222
                self.write((8 << 24) + 2)
223
                # write to FPGA DDR2-RAM
224
                self.fpga.WriteToBlockPipeIn(0x80, 32, self.__current_waveform[big_bytesize:])
225
226
            # check if upload was successful
227
            self.write(0x00)
228
            # start the pulse sequence
229
            self.write(0x01)
230
            # wait for 600ms
231
            time.sleep(0.6)
232
            # get status flags from FPGA
233
            flags = self.query()
234
            self.write(0x00)
235
            # check if the memory readout works.
236
            if flags == 0:
237
                self.log.info('Loading of waveform "{0}" to FPGA was successful.\n'
238
                              'Upload attempts needed: {1}'.format(waveform, loop_count))
239
                self.__currently_loaded_waveform = waveform
240
                break
241
            if loop_count == 10:
242
                self.log.error('Unable to upload waveform to FPGA.\n'
243
                               'Abort loading after 10 failed attempts.')
244
                self.reset()
245
                break
246
        return self.get_loaded_assets()[0]
247
248
    def load_sequence(self, sequence_name):
249
        """ Loads a sequence to the channels of the device in order to be ready for playback.
250
        For devices that have a workspace (i.e. AWG) this will load the sequence from the device
251
        workspace into the channels.
252
        For a device without mass memory this will make the waveform/pattern that has been
253
        previously written with self.write_waveform ready to play.
254
255
        @param sequence_name:  dict|list, a dictionary with keys being one of the available channel
256
                                      index and values being the name of the already written
257
                                      waveform to load into the channel.
258
                                      Examples:   {1: rabi_ch1, 2: rabi_ch2} or
259
                                                  {1: rabi_ch2, 2: rabi_ch1}
260
                                      If just a list of waveform names if given, the channel
261
                                      association will be invoked from the channel
262
                                      suffix '_ch1', '_ch2' etc.
263
264
        @return dict: Dictionary containing the actually loaded waveforms per channel.
265
        """
266
        self.log.warning('FPGA digital pulse generator has no sequencing capabilities.\n'
267
                         'load_sequence call ignored.')
268
        return
269
270
    def get_loaded_assets(self):
271
        """
272
        Retrieve the currently loaded asset names for each active channel of the device.
273
        The returned dictionary will have the channel numbers as keys.
274
        In case of loaded waveforms the dictionary values will be the waveform names.
275
        In case of a loaded sequence the values will be the sequence name appended by a suffix
276
        representing the track loaded to the respective channel (i.e. '<sequence_name>_1').
277
278
        @return (dict, str): Dictionary with keys being the channel number and values being the
279
                             respective asset loaded into the channel,
280
                             string describing the asset type ('waveform' or 'sequence')
281
        """
282
        asset_type = 'waveform' if self.__currently_loaded_waveform else None
283
        asset_dict = {chnl_num: self.__currently_loaded_waveform for chnl_num in range(1, 9)}
284
        return asset_dict, asset_type
285
286
    def clear_all(self):
287
        """ Clears all loaded waveforms from the pulse generators RAM/workspace.
288
289
        @return int: error code (0:OK, -1:error)
290
        """
291
        self.__currently_loaded_waveform = ''
292
        self.__current_waveform_name = ''
293
        self.__current_waveform = bytearray([0])
294
        return 0
295
296
    def get_status(self):
297
        """ Retrieves the status of the pulsing hardware
298
299
        @return (int, dict): tuple with an interger value of the current status
300
                             and a corresponding dictionary containing status
301
                             description for all the possible status variables
302
                             of the pulse generator hardware.
303
        """
304
        status_dic = dict()
305
        status_dic[-1] = 'Failed Request or Failed Communication with device.'
306
        status_dic[0] = 'Device has stopped, but can receive commands.'
307
        status_dic[1] = 'Device is active and running.'
308
309
        return self.__current_status, status_dic
310
311
    def get_sample_rate(self):
312
        """ Get the sample rate of the pulse generator hardware
313
314
        @return float: The current sample rate of the device (in Hz)
315
        """
316
        return self.__sample_rate
317
318
    def set_sample_rate(self, sample_rate):
319
        """ Set the sample rate of the pulse generator hardware.
320
321
        @param float sample_rate: The sampling rate to be set (in Hz)
322
323
        @return float: the sample rate returned from the device (in Hz).
324
325
        Note: After setting the sampling rate of the device, use the actually set return value for
326
              further processing.
327
        """
328
        # Round sample rate either to 500MHz or 950MHz since no other values are possible.
329
        if sample_rate < 725e6:
330
            self.__sample_rate = 500e6
331
            bitfile_name = 'pulsegen_8chnl_500MHz_{0}.bit'.format(self._fpga_type.split('_')[1])
332
        else:
333
            self.__sample_rate = 950e6
334
            bitfile_name = 'pulsegen_8chnl_950MHz_{0}.bit'.format(self._fpga_type.split('_')[1])
335
336
        bitfile_path = os.path.join(get_main_dir(), 'thirdparty', 'qo_fpga', bitfile_name)
337
338
        self.fpga.ConfigureFPGA(bitfile_path)
339
        self.log.debug('FPGA pulse generator configured with {0}'.format(bitfile_path))
340
        return self.__sample_rate
341
342
    def get_analog_level(self, amplitude=None, offset=None):
343
        """ Retrieve the analog amplitude and offset of the provided channels.
344
345
        @param list amplitude: optional, if the amplitude value (in Volt peak to peak, i.e. the
346
                               full amplitude) of a specific channel is desired.
347
        @param list offset: optional, if the offset value (in Volt) of a specific channel is
348
                            desired.
349
350
        @return: (dict, dict): tuple of two dicts, with keys being the channel descriptor string
351
                               (i.e. 'a_ch1') and items being the values for those channels.
352
                               Amplitude is always denoted in Volt-peak-to-peak and Offset in volts.
353
354
        Note: Do not return a saved amplitude and/or offset value but instead retrieve the current
355
              amplitude and/or offset directly from the device.
356
357
        If nothing (or None) is passed then the levels of all channels will be returned. If no
358
        analog channels are present in the device, return just empty dicts.
359
360
        Example of a possible input:
361
            amplitude = ['a_ch1', 'a_ch4'], offset = None
362
        to obtain the amplitude of channel 1 and 4 and the offset of all channels
363
            {'a_ch1': -0.5, 'a_ch4': 2.0} {'a_ch1': 0.0, 'a_ch2': 0.0, 'a_ch3': 1.0, 'a_ch4': 0.0}
364
        """
365
        return dict(), dict()
366
367
    def set_analog_level(self, amplitude=None, offset=None):
368
        """ Set amplitude and/or offset value of the provided analog channel(s).
369
370
        @param dict amplitude: dictionary, with key being the channel descriptor string
371
                               (i.e. 'a_ch1', 'a_ch2') and items being the amplitude values
372
                               (in Volt peak to peak, i.e. the full amplitude) for the desired
373
                               channel.
374
        @param dict offset: dictionary, with key being the channel descriptor string
375
                            (i.e. 'a_ch1', 'a_ch2') and items being the offset values
376
                            (in absolute volt) for the desired channel.
377
378
        @return (dict, dict): tuple of two dicts with the actual set values for amplitude and
379
                              offset for ALL channels.
380
381
        If nothing is passed then the command will return the current amplitudes/offsets.
382
383
        Note: After setting the amplitude and/or offset values of the device, use the actual set
384
              return values for further processing.
385
        """
386
        return {}, {}
387
388
    def get_digital_level(self, low=None, high=None):
389
        """ Retrieve the digital low and high level of the provided/all channels.
390
391
        @param list low: optional, if the low value (in Volt) of a specific channel is desired.
392
        @param list high: optional, if the high value (in Volt) of a specific channel is desired.
393
394
        @return: (dict, dict): tuple of two dicts, with keys being the channel descriptor strings
395
                               (i.e. 'd_ch1', 'd_ch2') and items being the values for those
396
                               channels. Both low and high value of a channel is denoted in volts.
397
398
        Note: Do not return a saved low and/or high value but instead retrieve
399
              the current low and/or high value directly from the device.
400
401
        If nothing (or None) is passed then the levels of all channels are being returned.
402
        If no digital channels are present, return just an empty dict.
403
404
        Example of a possible input:
405
            low = ['d_ch1', 'd_ch4']
406
        to obtain the low voltage values of digital channel 1 an 4. A possible answer might be
407
            {'d_ch1': -0.5, 'd_ch4': 2.0} {'d_ch1': 1.0, 'd_ch2': 1.0, 'd_ch3': 1.0, 'd_ch4': 4.0}
408
        Since no high request was performed, the high values for ALL channels are returned (here 4).
409
        """
410
        if low:
411
            low_dict = {chnl: 0.0 for chnl in low}
412
        else:
413
            low_dict = {'d_ch{0:d}'.format(chnl + 1): 0.0 for chnl in range(8)}
414
415
        if high:
416
            high_dict = {chnl: 3.3 for chnl in high}
417
        else:
418
            high_dict = {'d_ch{0:d}'.format(chnl + 1): 3.3 for chnl in range(8)}
419
420
        return low_dict, high_dict
421
422
    def set_digital_level(self, low=None, high=None):
423
        """ Set low and/or high value of the provided digital channel.
424
425
        @param dict low: dictionary, with key being the channel descriptor string
426
                         (i.e. 'd_ch1', 'd_ch2') and items being the low values (in volt) for the
427
                         desired channel.
428
        @param dict high: dictionary, with key being the channel descriptor string
429
                          (i.e. 'd_ch1', 'd_ch2') and items being the high values (in volt) for the
430
                          desired channel.
431
432
        @return (dict, dict): tuple of two dicts where first dict denotes the current low value and
433
                              the second dict the high value for ALL digital channels.
434
                              Keys are the channel descriptor strings (i.e. 'd_ch1', 'd_ch2')
435
436
        If nothing is passed then the command will return the current voltage levels.
437
438
        Note: After setting the high and/or low values of the device, use the actual set return
439
              values for further processing.
440
        """
441
        self.log.warning('FPGA pulse generator logic level cannot be adjusted!')
442
        return self.get_digital_level()
443
444
    def get_active_channels(self,  ch=None):
445
        """ Get the active channels of the pulse generator hardware.
446
447
        @param list ch: optional, if specific analog or digital channels are needed to be asked
448
                        without obtaining all the channels.
449
450
        @return dict:  where keys denoting the channel string and items boolean expressions whether
451
                       channel are active or not.
452
453
        Example for an possible input (order is not important):
454
            ch = ['a_ch2', 'd_ch2', 'a_ch1', 'd_ch5', 'd_ch1']
455
        then the output might look like
456
            {'a_ch2': True, 'd_ch2': False, 'a_ch1': False, 'd_ch5': True, 'd_ch1': False}
457
458
        If no parameter (or None) is passed to this method all channel states will be returned.
459
        """
460
        if ch:
461
            d_ch_dict = {chnl: True for chnl in ch}
462
        else:
463
            d_ch_dict = {'d_ch1': True,
464
                         'd_ch2': True,
465
                         'd_ch3': True,
466
                         'd_ch4': True,
467
                         'd_ch5': True,
468
                         'd_ch6': True,
469
                         'd_ch7': True,
470
                         'd_ch8': True}
471
        return d_ch_dict
472
473
    def set_active_channels(self, ch=None):
474
        """ Set the active channels for the pulse generator hardware.
475
476
        @param dict ch: dictionary with keys being the analog or digital string generic names for
477
                        the channels (i.e. 'd_ch1', 'a_ch2') with items being a boolean value.
478
                        True: Activate channel, False: Deactivate channel
479
480
        @return dict: with the actual set values for ALL active analog and digital channels
481
482
        If nothing is passed then the command will simply return the unchanged current state.
483
484
        Note: After setting the active channels of the device,
485
              use the returned dict for further processing.
486
487
        Example for possible input:
488
            ch={'a_ch2': True, 'd_ch1': False, 'd_ch3': True, 'd_ch4': True}
489
        to activate analog channel 2 digital channel 3 and 4 and to deactivate
490
        digital channel 1.
491
492
        The hardware itself has to handle, whether separate channel activation is possible.
493
        """
494
        return self.get_active_channels()
495
496
    def write_waveform(self, name, analog_samples, digital_samples, is_first_chunk, is_last_chunk,
497
                       total_number_of_samples):
498
        """
499
        Write a new waveform or append samples to an already existing waveform on the device memory.
500
        The flags is_first_chunk and is_last_chunk can be used as indicator if a new waveform should
501
        be created or if the write process to a waveform should be terminated.
502
503
        @param name: str, the name of the waveform to be created/append to
504
        @param analog_samples: numpy.ndarray of type float32 containing the voltage samples
505
        @param digital_samples: numpy.ndarray of type bool containing the marker states
506
                                (if analog channels are active, this must be the same length as
507
                                analog_samples)
508
        @param is_first_chunk: bool, flag indicating if it is the first chunk to write.
509
                                     If True this method will create a new empty wavveform.
510
                                     If False the samples are appended to the existing waveform.
511
        @param is_last_chunk: bool, flag indicating if it is the last chunk to write.
512
                                    Some devices may need to know when to close the appending wfm.
513
        @param total_number_of_samples: int, The number of sample points for the entire waveform
514
                                        (not only the currently written chunk)
515
516
        @return: (int, list) number of samples written (-1 indicates failed process) and list of
517
                             created waveform names
518
        """
519
        if analog_samples:
520
            self.log.error('FPGA pulse generator is purely digital and does not support waveform '
521
                           'generation with analog samples.')
522
            return -1, list()
523
        if not digital_samples:
524
            if total_number_of_samples > 0:
525
                self.log.warning('No samples handed over for waveform generation.')
526
                return -1, list()
527
            else:
528
                self.__current_waveform = bytearray([0])
529
                self.__current_waveform_name = ''
530
                return 0, list()
531
532
        # Initialize waveform array if this is the first chunk to write
533
        # Also append zero-timebins to waveform if the length is no integer multiple of 32
534
        if is_first_chunk:
535
            self.__samples_written = 0
536
            self.__current_waveform_name = name
537
            if total_number_of_samples % 32 != 0:
538
                number_of_zeros = 32 - (total_number_of_samples % 32)
539
                self.__current_waveform = np.zeros(total_number_of_samples + number_of_zeros,
540
                                                   dtype='uint8')
541
                self.log.warning('FPGA pulse sequence length is no integer multiple of 32 samples.'
542
                                 '\nAppending {0:d} zero-samples to the sequence.'
543
                                 ''.format(number_of_zeros))
544
            else:
545
                self.__current_waveform = np.zeros(total_number_of_samples, dtype='uint8')
546
547
        # Determine which part of the waveform array should be written
548
        chunk_length = len(digital_samples[list(digital_samples)[0]])
549
        write_end_index = self.__samples_written + chunk_length
550
551
        # Encode samples for each channel in bit mask and create waveform array
552
        for chnl, samples in digital_samples.items():
553
            # get channel index in range 0..7
554
            chnl_ind = int(chnl.rsplit('ch', 1)[1]) - 1
555
            # Represent bool values as np.uint8
556
            uint8_samples = samples.view('uint8')
557
            # left shift 0/1 values to bit position corresponding to channel index
558
            np.left_shift(uint8_samples, chnl_ind, out=uint8_samples)
559
            # Add samples to waveform array
560
            np.add(self.__current_waveform[self.__samples_written:write_end_index],
561
                   uint8_samples,
562
                   out=self.__current_waveform[self.__samples_written:write_end_index])
563
564
        # Convert numpy array to bytearray
565
        self.__current_waveform = bytearray(self.__current_waveform.tobytes())
566
567
        # increment the current write index
568
        self.__samples_written += chunk_length
569
        return chunk_length, [self.__current_waveform_name]
570
571
    def write_sequence(self, name, sequence_parameters):
572
        """
573
        Write a new sequence on the device memory.
574
575
        @param name: str, the name of the waveform to be created/append to
576
        @param sequence_parameters: dict, dictionary containing the parameters for a sequence
577
578
        @return: int, number of sequence steps written (-1 indicates failed process)
579
        """
580
        self.log.warning('FPGA digital pulse generator has no sequencing capabilities.\n'
581
                         'write_sequence call ignored.')
582
        return -1
583
584
    def get_waveform_names(self):
585
        """ Retrieve the names of all uploaded waveforms on the device.
586
587
        @return list: List of all uploaded waveform name strings in the device workspace.
588
        """
589
        return
590
591
    def get_sequence_names(self):
592
        """ Retrieve the names of all uploaded sequence on the device.
593
594
        @return list: List of all uploaded sequence name strings in the device workspace.
595
        """
596
        return list()
597
598
    def delete_waveform(self, waveform_name):
599
        """ Delete the waveform with name "waveform_name" from the device memory.
600
601
        @param str waveform_name: The name of the waveform to be deleted
602
                                  Optionally a list of waveform names can be passed.
603
604
        @return list: a list of deleted waveform names.
605
        """
606
        return
607
608
    def delete_sequence(self, sequence_name):
609
        """ Delete the sequence with name "sequence_name" from the device memory.
610
611
        @param str sequence_name: The name of the sequence to be deleted
612
                                  Optionally a list of sequence names can be passed.
613
614
        @return list: a list of deleted sequence names.
615
        """
616
        return list()
617
618
    def get_interleave(self):
619
        """ Check whether Interleave is ON or OFF in AWG.
620
621
        @return bool: True: ON, False: OFF
622
623
        Will always return False for pulse generator hardware without interleave.
624
        """
625
        return False
626
627
    def set_interleave(self, state=False):
628
        """ Turns the interleave of an AWG on or off.
629
630
        @param bool state: The state the interleave should be set to
631
                           (True: ON, False: OFF)
632
633
        @return bool: actual interleave status (True: ON, False: OFF)
634
635
        Note: After setting the interleave of the device, retrieve the
636
              interleave again and use that information for further processing.
637
638
        Unused for pulse generator hardware other than an AWG.
639
        """
640
        if state:
641
            self.log.error('No interleave functionality available in FPGA pulser.\n'
642
                           'Interleave state is always False.')
643
        return False
644
645
    def write(self, command):
646
        """ Sends a command string to the device.
647
648
        @param string command: string containing the command
649
650
        @return int: error code (0:OK, -1:error)
651
        """
652
        if not isinstance(command, int):
653
            return -1
654
        self.fpga.SetWireInValue(0x00, command)
655
        self.fpga.UpdateWireIns()
656
        return 0
657
658
    def query(self, question=None):
659
        """ Asks the device a 'question' and receive and return an answer from it.
660
661
        @param string question: string containing the command
662
663
        @return string: the answer of the device to the 'question' in a string
664
        """
665
        self.fpga.UpdateWireOuts()
666
        return self.fpga.GetWireOutValue(0x20)
667
668
    def reset(self):
669
        """ Reset the device.
670
671
        @return int: error code (0:OK, -1:error)
672
        """
673
        self.write(0x04)
674
        self.write(0x00)
675
        self.clear_all()
676
        return 0
677
678
    def has_sequence_mode(self):
679
        """ Asks the pulse generator whether sequence mode exists.
680
681
        @return: bool, True for yes, False for no.
682
        """
683
        return False
684
685
    def _connect_fpga(self):
686
        # connect to FPGA by serial number
687
        self.fpga.OpenBySerial(self._fpga_serial)
688
        # upload configuration bitfile to FPGA
689
        self.set_sample_rate(self.sample_rate)
690
691
        # Check connection
692
        if not self.fpga.IsFrontPanelEnabled():
693
            self.current_status = -1
694
            self.log.error('ERROR: FrontPanel is not enabled in FPGA pulse generator!')
695
            return -1
696
        else:
697
            self.current_status = 0
698
            self.log.info('FPGA pulse generator connected')
699
            return 0
700
701
    def _disconnect_fpga(self):
702
        """
703
        stop FPGA and disconnect
704
        """
705
        # set FPGA in reset state
706
        self.write(0x04)
707
        self.current_status = -1
708
        del self.fpga
709
        return 0
710