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