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
|
|
|
|