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

SamplesWriteMethods._write_wfmx()   F

Complexity

Conditions 25

Size

Total Lines 143

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 25
c 1
b 0
f 0
dl 0
loc 143
rs 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

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

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

1
# -*- coding: utf-8 -*-
2
3
"""
4
This file contains the Qudi methods to create hardware compatible files out of sampled pulse
5
sequences or pulse ensembles.
6
7
Qudi is free software: you can redistribute it and/or modify
8
it under the terms of the GNU General Public License as published by
9
the Free Software Foundation, either version 3 of the License, or
10
(at your option) any later version.
11
12
Qudi is distributed in the hope that it will be useful,
13
but WITHOUT ANY WARRANTY; without even the implied warranty of
14
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
GNU General Public License for more details.
16
17
You should have received a copy of the GNU General Public License
18
along with Qudi. If not, see <http://www.gnu.org/licenses/>.
19
20
Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the
21
top-level directory of this distribution and at <https://github.com/Ulm-IQO/qudi/>
22
"""
23
24
import os
25
import numpy as np
26
from collections import OrderedDict
27
from lxml import etree as ET
28
29
30
class SamplesWriteMethods:
31
    """
32
    Collection of write-to-file methods used to create hardware compatible files for the pulse
33
    generator out of sample arrays.
34
    """
35
    def __init__(self):
36
        # If you want to define a new file format, make a new method and add the
37
        # reference to this method to the _write_to_file dictionary:
38
        self._write_to_file = OrderedDict()
39
        self._write_to_file['wfm'] = self._write_wfm
40
        self._write_to_file['wfmx'] = self._write_wfmx
41
        self._write_to_file['seq'] = self._write_seq
42
        self._write_to_file['seqx'] = self._write_seqx
43
        self._write_to_file['fpga'] = self._write_fpga
44
        self._write_to_file['pstream'] = self._write_pstream
45
        return
46
47
    def _write_wfmx(self, name, analog_samples, digital_samples, total_number_of_samples,
48
                    is_first_chunk, is_last_chunk):
49
        """
50
        Appends a sampled chunk of a whole waveform to a wfmx-file. Create the file
51
        if it is the first chunk.
52
        If both flags (is_first_chunk, is_last_chunk) are set to TRUE it means
53
        that the whole ensemble is written as a whole in one big chunk.
54
55
        @param name: string, represents the name of the sampled ensemble
56
        @param analog_samples: dict containing float32 numpy ndarrays, contains the
57
                                       samples for the analog channels that
58
                                       are to be written by this function call.
59
        @param digital_samples: dict containing bool numpy ndarrays, contains the samples
60
                                      for the digital channels that
61
                                      are to be written by this function call.
62
        @param total_number_of_samples: int, The total number of samples in the
63
                                        entire waveform. Has to be known in advance.
64
        @param is_first_chunk: bool, indicates if the current chunk is the
65
                               first write to this file.
66
        @param is_last_chunk: bool, indicates if the current chunk is the last
67
                              write to this file.
68
69
        @return list: the list contains the string names of the created files for the passed
70
                      presampled arrays
71
        """
72
        # record the name of the created files
73
        created_files = []
74
75
        # The overhead of the write process in bytes.
76
        # Making this value bigger will result in a faster write process
77
        # but consumes more memory
78
        write_overhead_bytes = 1024*1024*256 # 256 MB
79
        # The overhead of the write process in number of samples
80
        write_overhead_samples = write_overhead_bytes//4
81
82
        # if it is the first chunk, create the .WFMX file with header.
83
        if is_first_chunk:
84
            # create header
85
            self._create_xml_file(total_number_of_samples, self.temp_dir)
86
            # read back the header xml-file and delete it afterwards
87
            temp_file = os.path.join(self.temp_dir, 'header.xml')
88
            with open(temp_file, 'r') as header:
89
                header_lines = header.readlines()
90
            os.remove(temp_file)
91
92
            # create wfmx-file for each analog channel
93
            for channel in analog_samples:
94
                filename = name + channel[1:] + '.wfmx'
95
                created_files.append(filename)
96
97
                filepath = os.path.join(self.waveform_dir, filename)
98
99
                with open(filepath, 'wb') as wfmxfile:
100
                    # write header
101
                    for line in header_lines:
102
                        wfmxfile.write(bytes(line, 'UTF-8'))
103
104
        # append analog samples to the .WFMX files of each channel. Write
105
        # digital samples in temporary files.
106
        for channel in analog_samples:
107
            # get analog channel number as integer from string
108
            a_chnl_number = int(channel.strip('a_ch'))
109
            # get marker string descriptors for this analog channel
110
            markers = ['d_ch'+str((a_chnl_number*2)-1), 'd_ch'+str(a_chnl_number*2)]
111
            # append analog samples chunk to .WFMX file
112
            filepath = os.path.join(self.waveform_dir, name + channel[1:] + '.wfmx')
113
            with open(filepath, 'ab') as wfmxfile:
114
                # append analog samples in binary format. One sample is 4
115
                # bytes (np.float32). Write in chunks if array is very big to
116
                # avoid large temporary copys in memory
117
                number_of_full_chunks = int(analog_samples[channel].size//write_overhead_samples)
118
                for start_ind in np.arange(0, number_of_full_chunks * write_overhead_samples,
119
                                           write_overhead_samples):
120
                    stop_ind = start_ind+write_overhead_samples
121
                    wfmxfile.write(analog_samples[channel][start_ind:stop_ind])
122
                # write rest
123
                rest_start_ind = number_of_full_chunks*write_overhead_samples
124
                wfmxfile.write(analog_samples[channel][rest_start_ind:])
125
126
            # create the byte values corresponding to the marker states
127
            # (\x01 for marker 1, \x02 for marker 2, \x03 for both)
128
            # and write them into a temporary file
129
            filepath = os.path.join(self.temp_dir, name + channel[1:] + '_digi' + '.tmp')
130
            with open(filepath, 'ab') as tmpfile:
131
                if markers[0] not in digital_samples and markers[1] not in digital_samples:
132
                    # no digital channels to write for this analog channel
133
                    pass
134
                elif markers[0] in digital_samples and markers[1] not in digital_samples:
135
                    # Only marker one is active for this channel
136
                    for start_ind in np.arange(0, number_of_full_chunks * write_overhead_bytes,
137
                                               write_overhead_bytes):
138
                        stop_ind = start_ind + write_overhead_bytes
139
                        # append digital samples in binary format. One sample is 1 byte (np.uint8).
140
                        tmpfile.write(digital_samples[markers[0]][start_ind:stop_ind])
141
                    # write rest of digital samples
142
                    rest_start_ind = number_of_full_chunks * write_overhead_bytes
143
                    tmpfile.write(digital_samples[markers[0]][rest_start_ind:])
144
                elif markers[0] not in digital_samples and markers[1] in digital_samples:
145
                    # Only marker two is active for this channel
146
                    for start_ind in np.arange(0, number_of_full_chunks * write_overhead_bytes,
147
                                               write_overhead_bytes):
148
                        stop_ind = start_ind + write_overhead_bytes
149
                        # append digital samples in binary format. One sample is 1 byte (np.uint8).
150
                        tmpfile.write(np.left_shift(
151
                            digital_samples[markers[1]][start_ind:stop_ind].astype('uint8'),
152
                            1))
153
                    # write rest of digital samples
154
                    rest_start_ind = number_of_full_chunks * write_overhead_bytes
155
                    tmpfile.write(np.left_shift(
156
                        digital_samples[markers[1]][rest_start_ind:].astype('uint8'), 1))
157
                else:
158
                    # Both markers are active for this channel
159
                    for start_ind in np.arange(0, number_of_full_chunks * write_overhead_bytes,
160
                                               write_overhead_bytes):
161
                        stop_ind = start_ind + write_overhead_bytes
162
                        # append digital samples in binary format. One sample is 1 byte (np.uint8).
163
                        tmpfile.write(np.add(np.left_shift(
164
                            digital_samples[markers[1]][start_ind:stop_ind].astype('uint8'), 1),
165
                            digital_samples[markers[0]][start_ind:stop_ind]))
166
                    # write rest of digital samples
167
                    rest_start_ind = number_of_full_chunks * write_overhead_bytes
168
                    tmpfile.write(np.add(np.left_shift(
169
                        digital_samples[markers[1]][rest_start_ind:].astype('uint8'), 1),
170
                        digital_samples[markers[0]][rest_start_ind:]))
171
172
        # append the digital sample tmp file to the .WFMX file and delete the
173
        # .tmp files if it was the last chunk to write.
174
        if is_last_chunk:
175
            for channel in analog_samples:
176
                tmp_filepath = os.path.join(self.temp_dir, name + channel[1:] + '_digi' + '.tmp')
177
                wfmx_filepath = os.path.join(self.waveform_dir, name + channel[1:] + '.wfmx')
178
                with open(wfmx_filepath, 'ab') as wfmxfile:
179
                    with open(tmp_filepath, 'rb') as tmpfile:
180
                        # read and write files in max. write_overhead_bytes chunks to reduce
181
                        # memory usage
182
                        while True:
183
                            tmp_data = tmpfile.read(write_overhead_bytes)
184
                            if not tmp_data:
185
                                break
186
                            wfmxfile.write(tmp_data)
187
                # delete tmp file
188
                os.remove(tmp_filepath)
189
        return created_files
190
191
    def _write_wfm(self, name, analog_samples, digital_samples, total_number_of_samples,
192
                    is_first_chunk, is_last_chunk):
193
        """
194
        Appends a sampled chunk of a whole waveform to a wfm-file. Create the file
195
        if it is the first chunk.
196
        If both flags (is_first_chunk, is_last_chunk) are set to TRUE it means
197
        that the whole ensemble is written as a whole in one big chunk.
198
199
        @param name: string, represents the name of the sampled ensemble
200
        @param analog_samples: dict containing float32 numpy ndarrays, contains the
201
                                       samples for the analog channels that
202
                                       are to be written by this function call.
203
        @param digital_samples: dict containing bool numpy ndarrays, contains the samples
204
                                      for the digital channels that
205
                                      are to be written by this function call.
206
        @param total_number_of_samples: int, The total number of samples in the
207
                                        entire waveform. Has to be known it advance.
208
        @param is_first_chunk: bool, indicates if the current chunk is the
209
                               first write to this file.
210
        @param is_last_chunk: bool, indicates if the current chunk is the last
211
                              write to this file.
212
213
        @return list: the list contains the string names of the created files for the passed
214
                      presampled arrays
215
        """
216
        # record the name of the created files
217
        created_files = []
218
219
        # IMPORTANT: These numbers build the header in the wfm file. Needed
220
        # by the device program to understand wfm file. If it is wrong,
221
        # AWG will not be able to understand the written file.
222
223
        # The pure waveform has the number 1000, indicating that it is a
224
        # *.wfm file. For sequence mode e.g. the number would be 3001 or
225
        # 3002, depending on the number of channels in the sequence mode.
226
        # (The last number indicates the channel numbers).
227
        # Next line after the header tells the number of bins of the
228
        # waveform file.
229
        # After this number a 14bit binary representation of the channel
230
        # and the marker are followed.
231
        for channel in analog_samples:
232
            # get analog channel number as integer from string
233
            a_chnl_number = int(channel.strip('a_ch'))
234
            # get marker string descriptors for this analog channel
235
            markers = ['d_ch' + str((a_chnl_number * 2) - 1), 'd_ch' + str(a_chnl_number * 2)]
236
237
            filename = name + channel[1:] + '.wfm'
238
            created_files.append(filename)
239
            filepath = os.path.join(self.waveform_dir, filename)
240
241
            if is_first_chunk:
242
                with open(filepath, 'wb') as wfm_file:
243
                    # write the first line, which is the header file, if first chunk is passed:
244
                    num_bytes = str(int(total_number_of_samples * 5))
245
                    num_digits = str(len(num_bytes))
246
                    header = str.encode('MAGIC 1000\r\n#' + num_digits + num_bytes)
247
                    wfm_file.write(header)
248
249
            # now write the samples chunk in binary representation:
250
            # First we create a structured numpy array representing one byte (numpy uint8)
251
            # for the markers and 4 byte (numpy float32) for the analog samples.
252
            write_array = np.empty(analog_samples[channel].size, dtype='float32, uint8')
253
254
            # now we determine which markers are active for this channel and write them to
255
            # write_array.
256
            if markers[0] in digital_samples and markers[1] in digital_samples:
257
                # both markers active for this channel
258
                write_array['f1'] = np.add(
259
                    np.left_shift(digital_samples[markers[1]][:].astype('uint8'), 1),
260
                    digital_samples[markers[0]][:].astype('uint8'))
261
            elif markers[0] in digital_samples and markers[1] not in digital_samples:
262
                # only marker 1 active for this channel
263
                write_array['f1'] = digital_samples[markers[0]][:].astype('uint8')
264
            elif markers[0] not in digital_samples and markers[1] in digital_samples:
265
                # only marker 2 active for this channel
266
                write_array['f1'] = np.left_shift(digital_samples[markers[1]][:].astype('uint8'), 1)
267
            else:
268
                # no markers active for this channel
269
                write_array['f1'] = np.zeros(analog_samples[channel].size, dtype='uint8')
270
            # Write analog samples into the write_array
271
            write_array['f0'] = analog_samples[channel][:]
272
273
            # Write write_array to file
274
            with open(filepath, 'ab') as wfm_file:
275
                wfm_file.write(write_array)
276
277
                # append footer if it's the last chunk to write
278
                if is_last_chunk:
279
                    # the footer encodes the sample rate, which was used for that file:
280
                    footer = str.encode('CLOCK {0:16.10E}\r\n'.format(self.sample_rate))
281
                    wfm_file.write(footer)
282
        return created_files
283
284
    def _write_fpga(self, name, analog_samples, digital_samples, total_number_of_samples,
285
                    is_first_chunk, is_last_chunk):
286
        """
287
        Appends a sampled chunk of a whole waveform to a fpga-file. Create the file
288
        if it is the first chunk.
289
        If both flags (is_first_chunk, is_last_chunk) are set to TRUE it means
290
        that the whole ensemble is written as a whole in one big chunk.
291
292
        @param name: string, represents the name of the sampled ensemble
293
        @param analog_samples: dict containing float32 numpy ndarrays, contains the
294
                                       samples for the analog channels that
295
                                       are to be written by this function call.
296
        @param digital_samples: dict containing bool numpy ndarrays, contains the samples
297
                                      for the digital channels that
298
                                      are to be written by this function call.
299
        @param total_number_of_samples: int, The total number of samples in the
300
                                        entire waveform. Has to be known it advance.
301
        @param is_first_chunk: bool, indicates if the current chunk is the
302
                               first write to this file.
303
        @param is_last_chunk: bool, indicates if the current chunk is the last
304
                              write to this file.
305
306
        @return list: the list contains the string names of the created files for the passed
307
                      presampled arrays
308
        """
309
        # record the name of the created files
310
        created_files = []
311
312
        if len(digital_samples) != 8:
313
            self.log.warning('FPGA pulse generator needs 8 digital channels. ({0} given)\n'
314
                             'All not specified channels will be set to logical low.'
315
                             ''.format(len(digital_samples)))
316
            return -1
317
318
        chunk_length_bins = len(digital_samples[list(digital_samples)[0]])
319
320
        # encode channels into FPGA samples (bytes)
321
        # check if the sequence length is an integer multiple of 32 bins
322
        if is_last_chunk and (total_number_of_samples % 32 != 0):
323
            # calculate number of zero timeslots to append
324
            number_of_zeros = 32 - (total_number_of_samples % 32)
325
            encoded_samples = np.zeros(chunk_length_bins + number_of_zeros, dtype='uint8')
326
            self.log.warning('FPGA pulse sequence length is no integer multiple of 32 samples. '
327
                             'Appending {0} zero-samples to the sequence.'.format(number_of_zeros))
328
        else:
329
            encoded_samples = np.zeros(chunk_length_bins, dtype='uint8')
330
331
        for chnl_num in range(1, 9):
332
            chnl_str = 'd_ch' + str(chnl_num)
333
            if chnl_str in digital_samples:
334
                encoded_samples[:chunk_length_bins] += (2 ** chnl_num) * np.uint8(
335
                    digital_samples[chnl_str])
336
337
        del digital_samples  # no longer needed
338
339
        # append samples to file
340
        filename = name + '.fpga'
341
        created_files.append(filename)
342
343
        filepath = os.path.join(self.waveform_dir, filename)
344
        with open(filepath, 'wb') as fpgafile:
345
            fpgafile.write(encoded_samples)
346
347
        return created_files
348
349
    def _write_pstream(self, name, analog_samples, digital_samples, total_number_of_samples,
350
                       is_first_chunk, is_last_chunk):
351
        """
352
        Appends a sampled chunk of a whole waveform to a fpga-file. Create the file
353
        if it is the first chunk.
354
        If both flags (is_first_chunk, is_last_chunk) are set to TRUE it means
355
        that the whole ensemble is written as a whole in one big chunk.
356
357
        The PulseStreamer programming interface is based on a sequence of <Pulse> elements,
358
        with the following C++ datatype (taken from the documentation available at 
359
        https://www.swabianinstruments.com/static/documentation/PulseStreamer/sections/interface.html):
360
            struct Pulse {
361
                unsigned int ticks; // duration in ns
362
                unsigned char digi; // bit mask
363
                short ao0;
364
                short ao1;
365
            };
366
367
        Currently the access to the analog channels is not exposed by the Qudi implementation
368
        of the PulseStreamer hardware, so we need only deal with the digital side. Thus a new 
369
        Pulse element is required every time the digital channels (8 available) change, with a
370
        corresponding length computed for that Pulse element. For example, the sequence
371
        
372
            Channel 01234567
373
                    01000000
374
                    01000000
375
                    01000100
376
                    01000100
377
                    00000000
378
                
379
        will be compressed to three Pulse elements with duration 2, 2, 1 and with the correct
380
        respective bitmasks for the active channels. 
381
        
382
        This function traverses the digital_samples array to identify where the active digital
383
        channels are modified and compresses it down to a sequence of pulse elements each with 
384
        a bitmask and a length. The file is then written to disk. 
385
        
386
        TODO: This is inefficient, as the original PulseElement representation inside Qudi is
387
        first decompressed into a sample stream, then recompressed into the PulseStreamer 
388
        representation. Work is required to enable the bypass of the interim stage. 
389
390
        @param name: string, represents the name of the sampled ensemble
391
        @param analog_samples: dict containing float32 numpy ndarrays, contains the
392
                                       samples for the analog channels that
393
                                       are to be written by this function call.
394
        @param digital_samples: dict containing bool numpy ndarrays, contains the samples
395
                                      for the digital channels that
396
                                      are to be written by this function call.
397
        @param total_number_of_samples: int, The total number of samples in the
398
                                        entire waveform. Has to be known it advance.
399
        @param is_first_chunk: bool, indicates if the current chunk is the
400
                               first write to this file.
401
        @param is_last_chunk: bool, indicates if the current chunk is the last
402
                              write to this file.
403
404
        @return list: the list contains the string names of the created files for the passed
405
                      presampled arrays
406
        """
407
        # FIXME: This method needs to be adapted to "digital_samples" now being a dictionary
408
        import dill
409
410
        # record the name of the created files
411
        created_files = []
412
413
        channel_number = len(digital_samples)
414
415
        if channel_number != 8:
416
            self.log.error('Pulse streamer needs 8 digital channels. {0} is not allowed!'
417
                           ''.format(channel_number))
418
            return -1
419
420
        # fetch locations where digital channel states change, and eliminate duplicates
421
        new_channel_indices = np.where(digital_samples[:-1,:] != digital_samples[1:,:])[0]
422
        new_channel_indices = np.unique(new_channel_indices)
423
424
        # add in indices for the start and end of the sequence to simplify iteration
425
        new_channel_indices = np.insert(new_channel_indices, 0, [-1])
426
        new_channel_indices = np.insert(new_channel_indices, new_channel_indices.size, [digital_samples.shape[0]-1])
427
428
        pulses = []
429
        for new_channel_index in range(1, new_channel_indices.size):
430
            pulse = [new_channel_indices[new_channel_index] - new_channel_indices[new_channel_index - 1], _convert_to_bitmask(digital_samples[new_channel_indices[new_channel_index - 1] + 1,:])]
431
            pulses.append(pulse)
432
433
        # append samples to file
434
        filename = name + '.pstream'
435
        created_files.append(filename)
436
437
        filepath = os.path.join(self.waveform_dir, filename)
438
        dill.dump(pulses, open(filepath, 'wb'))
439
440
        return created_files
441
442
    def _write_seq(self, sequence_obj):
443
        """
444
        Write a sequence to a seq-file.
445
446
        @param str name: name of the sequence to be created
447
        @param object sequence_obj: PulseSequence instance
448
449
        for AWG5000/7000 Series the following parameter will be used (are also present in the
450
        hardware constraints for the pulser):
451
            { 'name' : [<list_of_str_names>],
452
              'repetitions' : 0=infinity reps; int_num in [1:65536],
453
              'trigger_wait' : 0=False or 1=True,
454
              'go_to': 0=Nothing happens; int_num in [1:8000]
455
              'event_jump_to' : -1=to next; 0= nothing happens; int_num in [1:8000]
456
        """
457
        filepath = os.path.join(self.waveform_dir, sequence_obj.name + '.seq')
458
459
        with open(filepath, 'wb') as seq_file:
460
            # write the header:
461
            # determine the used channels according to how much files were created:
462
            channels = len(sequence_obj.analog_channels)
463
            lines = len(sequence_obj.ensemble_list)
464
            seq_file.write('MAGIC 300{0:d}\r\n'.format(channels).encode('UTF-8'))
465
            seq_file.write('LINES {0:d}\r\n'.format(lines).encode('UTF-8'))
466
467
            # write main part:
468
            # in this order: 'waveform_name', repeat, wait, Goto, ejump
469
            for step_num, (ensemble_obj, seq_param) in enumerate(sequence_obj.ensemble_list):
470
                repeat = seq_param['repetitions'] + 1
471
                event_jump_to = seq_param['event_jump_to']
472
                go_to = seq_param['go_to']
473
                trigger_wait = 0
474
475
476
                line_str = ''
477
                # Put waveform filenames in the line string for the current sequence step
478
                # In case of rotating frame preservation the waveforms are not named after the
479
                # ensemble but after the sequence with a running number suffix.
480
                for chnl in sequence_obj.analog_channels:
481
                    if sequence_obj.rotating_frame:
482
                        line_str += '"{0}", '.format(sequence_obj.name + '_' +
483
                                                     str(step_num).zfill(3) + chnl[1:] + '.' +
484
                                                     self.waveform_format)
485
                    else:
486
                        line_str += '"{0}", '.format(
487
                            ensemble_obj.name + chnl[1:] + '.' + self.waveform_format)
488
489
                # append sequence step parameters to line string
490
                line_str += '{0:d}, {1:d}, {2:d}, {3:d}\r\n'.format(repeat, trigger_wait, go_to,
491
                                                                    event_jump_to)
492
                seq_file.write(line_str.encode('UTF-8'))
493
494
            # write the footer:
495
            footer = ''
496
            footer += 'TABLE_JUMP' + 16 * ' 0,' + '\r\n'
497
            footer += 'LOGIC_JUMP -1, -1, -1, -1,\r\n'
498
            footer += 'JUMP_MODE TABLE\r\n'
499
            footer += 'JUMP_TIMING ASYNC\r\n'
500
            footer += 'STROBE 0\r\n'
501
502
            seq_file.write(footer.encode('UTF-8'))
503
504
    # TODO: Implement this method.
505
    def _write_seqx(self, name, sequence_param):
506
        """
507
        Write a sequence to a seqx-file.
508
509
        @param str name: name of the sequence to be created
510
        @param list sequence_param: a list of dict, which contains all the information, which
511
                                    parameters are to be taken to create a sequence. The dict will
512
                                    have at least the entry
513
                                        {'name': [<list_of_sampled_file_names>] }
514
                                    All other parameters, which can be used in the sequence are
515
                                    determined in the get_constraints method in the category
516
                                    'sequence_param'.
517
518
        In order to write sequence files a completely new method with respect to
519
        write_samples_to_file is needed.
520
521
        for AWG5000/7000 Series the following parameter will be used (are also present in the
522
        hardware constraints for the pulser):
523
            { 'name' : [<list_of_str_names>],
524
              'repetitions' : 0=infinity reps; int_num in [1:65536],
525
              'trigger_wait' : 0=False or 1=True,
526
              'go_to': 0=Nothing happens; int_num in [1:8000]
527
              'event_jump_to' : -1=to next; 0= nothing happens; int_num in [1:8000]
528
        """
529
        pass
530
531
    def _create_xml_file(self, number_of_samples, temp_dir=''):
532
        """
533
        This function creates an xml file containing the header for the wfmx-file format using
534
        etree.
535
        """
536
        root = ET.Element('DataFile', offset='xxxxxxxxx', version="0.1")
537
        DataSetsCollection = ET.SubElement(root, 'DataSetsCollection',
538
                                           xmlns="http://www.tektronix.com")
539
        DataSets = ET.SubElement(DataSetsCollection, 'DataSets', version="1",
540
                                 xmlns="http://www.tektronix.com")
541
        DataDescription = ET.SubElement(DataSets, 'DataDescription')
542
        NumberSamples = ET.SubElement(DataDescription, 'NumberSamples')
543
        NumberSamples.text = str(int(number_of_samples))
544
        SamplesType = ET.SubElement(DataDescription, 'SamplesType')
545
        SamplesType.text = 'AWGWaveformSample'
546
        MarkersIncluded = ET.SubElement(DataDescription, 'MarkersIncluded')
547
        MarkersIncluded.text = 'true'
548
        NumberFormat = ET.SubElement(DataDescription, 'NumberFormat')
549
        NumberFormat.text = 'Single'
550
        Endian = ET.SubElement(DataDescription, 'Endian')
551
        Endian.text = 'Little'
552
        Timestamp = ET.SubElement(DataDescription, 'Timestamp')
553
        Timestamp.text = '2014-10-28T12:59:52.9004865-07:00'
554
        ProductSpecific = ET.SubElement(DataSets, 'ProductSpecific', name="")
555
        ReccSamplingRate = ET.SubElement(ProductSpecific, 'ReccSamplingRate', units="Hz")
556
        ReccSamplingRate.text = str(self.sample_rate)
557
        ReccAmplitude = ET.SubElement(ProductSpecific, 'ReccAmplitude', units="Volts")
558
        ReccAmplitude.text = str(0.5)
559
        ReccOffset = ET.SubElement(ProductSpecific, 'ReccOffset', units="Volts")
560
        ReccOffset.text = str(0)
561
        SerialNumber = ET.SubElement(ProductSpecific, 'SerialNumber')
562
        SoftwareVersion = ET.SubElement(ProductSpecific, 'SoftwareVersion')
563
        SoftwareVersion.text = '4.0.0075'
564
        UserNotes = ET.SubElement(ProductSpecific, 'UserNotes')
565
        OriginalBitDepth = ET.SubElement(ProductSpecific, 'OriginalBitDepth')
566
        OriginalBitDepth.text = 'EightBit'
567
        Thumbnail = ET.SubElement(ProductSpecific, 'Thumbnail')
568
        CreatorProperties = ET.SubElement(ProductSpecific, 'CreatorProperties',
569
                                          name='Basic Waveform')
570
        Setup = ET.SubElement(root, 'Setup')
571
572
        filepath = os.path.join(temp_dir, 'header.xml')
573
574
        ##### This command creates the first version of the file
575
        tree = ET.ElementTree(root)
576
        tree.write(filepath, pretty_print=True, xml_declaration=True)
577
578
        # Calculates the length of the header:
579
        # 40 is subtracted since the first line of the above created file has a length of 39 and is
580
        # not included later and the last endline (\n) is also not neccessary.
581
        # The for loop is needed to give a nine digit length: xxxxxxxxx
582
        length_of_header = ''
583
        size = str(os.path.getsize(filepath) - 40)
584
585
        for ii in range(9 - len(size)):
586
            length_of_header += '0'
587
        length_of_header += size
588
589
        # The header length is written into the file
590
        # The first line is not included since it is redundant
591
        # Also the last endline (\n) is excluded
592
        text = open(filepath, "U").read()
593
        text = text.replace("xxxxxxxxx", length_of_header)
594
        text = bytes(text, 'UTF-8')
595
        f = open(filepath, "wb")
596
        f.write(text[39:-1])
597
        f.close()
598
599
def _convert_to_bitmask(active_channels):
600
    """ Convert a list of channels into a bitmask.
601
    @param numpy.array active_channels: the list of active channels like
602
                        e.g. [0,4,7]. Note that the channels start from 0.
603
    @return int: The channel-list is converted into a bitmask (an sequence
604
                 of 1 and 0). The returned integer corresponds to such a
605
                 bitmask.
606
    Note that you can get a binary representation of an integer in python
607
    if you use the command bin(<integer-value>). All higher unneeded digits
608
    will be dropped, i.e. 0b00100 is turned into 0b100. Examples are
609
        bin(0) =    0b0
610
        bin(1) =    0b1
611
        bin(8) = 0b1000
612
    Each bit value (read from right to left) corresponds to the fact that a
613
    channel is on or off. I.e. if you have
614
        0b001011
615
    then it would mean that only channel 0, 1 and 3 are switched to on, the
616
    others are off.
617
    Helper method for write_pulse_form.
618
    """
619
    bits = 0  # that corresponds to: 0b0
620
    active_channels = np.where(active_channels == True)
621
    for channel in active_channels[0]:
622
        # go through each list element and create the digital word out of
623
        # 0 and 1 that represents the channel configuration. In order to do
624
        # that a bitwise shift to the left (<< operator) is performed and
625
        # the current channel configuration is compared with a bitwise OR
626
        # to check whether the bit was already set. E.g.:
627
        #   0b1001 | 0b0110: compare elementwise:
628
        #           1 | 0 => 1
629
        #           0 | 1 => 1
630
        #           0 | 1 => 1
631
        #           1 | 1 => 1
632
        #                   => 0b1111
633
        bits = bits | (1 << channel)
634
    return bits
635