|
1
|
|
|
# -*- coding: utf-8 -*- |
|
2
|
|
|
|
|
3
|
|
|
""" |
|
4
|
|
|
This file contains the Qudi hardware module for AWG7000 Series. |
|
5
|
|
|
|
|
6
|
|
|
Qudi is free software: you can redistribute it and/or modify |
|
7
|
|
|
it under the terms of the GNU General Public License as published by |
|
8
|
|
|
the Free Software Foundation, either version 3 of the License, or |
|
9
|
|
|
(at your option) any later version. |
|
10
|
|
|
|
|
11
|
|
|
Qudi is distributed in the hope that it will be useful, |
|
12
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
14
|
|
|
GNU General Public License for more details. |
|
15
|
|
|
|
|
16
|
|
|
You should have received a copy of the GNU General Public License |
|
17
|
|
|
along with Qudi. If not, see <http://www.gnu.org/licenses/>. |
|
18
|
|
|
|
|
19
|
|
|
Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the |
|
20
|
|
|
top-level directory of this distribution and at <https://github.com/Ulm-IQO/qudi/> |
|
21
|
|
|
""" |
|
22
|
|
|
|
|
23
|
|
|
|
|
24
|
|
|
import os |
|
25
|
|
|
import time |
|
26
|
|
|
import visa |
|
27
|
|
|
import numpy as np |
|
28
|
|
|
from ftplib import FTP |
|
29
|
|
|
from collections import OrderedDict |
|
30
|
|
|
|
|
31
|
|
|
from core.util.modules import get_home_dir |
|
32
|
|
|
from core.module import Base, ConfigOption |
|
33
|
|
|
from interface.pulser_interface import PulserInterface, PulserConstraints |
|
34
|
|
|
|
|
35
|
|
|
|
|
36
|
|
|
# TODO: add a method to sequencing to change from dynamic to jump in order to get triggers for odmr |
|
37
|
|
|
class AWG7122C(Base, PulserInterface): |
|
38
|
|
|
""" |
|
39
|
|
|
Unstable and under construction, Jochen Scheuer |
|
40
|
|
|
... but about to be become awesome, Nikolas Tomek |
|
41
|
|
|
""" |
|
42
|
|
|
|
|
43
|
|
|
_modclass = 'awg7122c' |
|
44
|
|
|
_modtype = 'hardware' |
|
45
|
|
|
|
|
46
|
|
|
# config options |
|
47
|
|
|
_tmp_work_dir = ConfigOption(name='tmp_work_dir', |
|
48
|
|
|
default=os.path.join(get_home_dir(), 'pulsed_files'), |
|
49
|
|
|
missing='warn') |
|
50
|
|
|
_visa_address = ConfigOption(name='awg_visa_address', missing='error') |
|
51
|
|
|
_ip_address = ConfigOption(name='awg_ip_address', missing='error') |
|
52
|
|
|
_ftp_dir = ConfigOption(name='ftp_root_dir', default='C:\\inetpub\\ftproot', missing='warn') |
|
53
|
|
|
_username = ConfigOption(name='ftp_login', default='anonymous', missing='warn') |
|
54
|
|
|
_password = ConfigOption(name='ftp_passwd', default='anonymous@', missing='warn') |
|
55
|
|
|
_default_sample_rate = ConfigOption(name='default_sample_rate', default=None, missing='warn') |
|
56
|
|
|
_visa_timeout = ConfigOption(name='timeout', default=30, missing='nothing') |
|
57
|
|
|
|
|
58
|
|
|
def __init__(self, config, **kwargs): |
|
59
|
|
|
super().__init__(config=config, **kwargs) |
|
60
|
|
|
|
|
61
|
|
|
# Get an instance of the visa resource manager |
|
62
|
|
|
self._rm = visa.ResourceManager() |
|
63
|
|
|
|
|
64
|
|
|
self.awg = None # This variable will hold a reference to the awg visa resource |
|
65
|
|
|
|
|
66
|
|
|
self.ftp_working_dir = 'waves' # subfolder of FTP root dir on AWG disk to work in |
|
67
|
|
|
|
|
68
|
|
|
self.installed_options = list() # will hold the encoded installed options available on awg |
|
69
|
|
|
self.__loaded_sequence = '' # Helper variable since a loaded sequence can not be queried :( |
|
70
|
|
|
self._marker_byte_dict = {0: b'\x00', 1: b'\x01', 2: b'\x02', 3: b'\x03'} |
|
71
|
|
|
|
|
72
|
|
|
def on_activate(self): |
|
73
|
|
|
""" Initialisation performed during activation of the module. |
|
74
|
|
|
""" |
|
75
|
|
|
# Create work directory if necessary |
|
76
|
|
|
if not os.path.exists(self._tmp_work_dir): |
|
77
|
|
|
os.makedirs(os.path.abspath(self._tmp_work_dir)) |
|
78
|
|
|
|
|
79
|
|
|
# connect to awg using PyVISA |
|
80
|
|
|
if self._visa_address not in self._rm.list_resources(): |
|
81
|
|
|
self.awg = None |
|
82
|
|
|
self.log.error( |
|
83
|
|
|
'VISA address "{0}" not found by the pyVISA resource manager.\nCheck ' |
|
84
|
|
|
'the connection by using for example "Agilent Connection Expert".' |
|
85
|
|
|
''.format(self._visa_address)) |
|
86
|
|
|
else: |
|
87
|
|
|
self.awg = self._rm.open_resource(self._visa_address) |
|
88
|
|
|
# set timeout by default to 30 sec |
|
89
|
|
|
self.awg.timeout = self._visa_timeout * 1000 |
|
90
|
|
|
|
|
91
|
|
|
# try connecting to AWG using FTP protocol |
|
92
|
|
|
with FTP(self._ip_address) as ftp: |
|
93
|
|
|
ftp.login(user=self._username, passwd=self._password) |
|
94
|
|
|
ftp.cwd(self.ftp_working_dir) |
|
95
|
|
|
|
|
96
|
|
|
# Options of AWG7000 series: |
|
97
|
|
|
# Option 01: Memory expansion to 64,8 MSamples (Million points) |
|
98
|
|
|
# Option 06: Interleave and extended analog output bandwidth |
|
99
|
|
|
# Option 08: Fast sequence switching |
|
100
|
|
|
# Option 09: Subsequence and Table Jump |
|
101
|
|
|
self.installed_options = self.query('*OPT?').split(',') |
|
102
|
|
|
# TODO: inclulde proper routine to check and change zeroing functionality |
|
103
|
|
|
|
|
104
|
|
|
# Set current directory on AWG |
|
105
|
|
|
self.write('MMEM:CDIR "{0}"'.format(os.path.join(self._ftp_dir, self.ftp_working_dir))) |
|
106
|
|
|
|
|
107
|
|
|
def on_deactivate(self): |
|
108
|
|
|
""" Deinitialisation performed during deactivation of the module. |
|
109
|
|
|
""" |
|
110
|
|
|
# Closes the connection to the AWG |
|
111
|
|
|
try: |
|
112
|
|
|
self.awg.close() |
|
113
|
|
|
except: |
|
114
|
|
|
self.log.debug('Closing AWG connection using pyvisa failed.') |
|
115
|
|
|
self.log.info('Closed connection to AWG') |
|
116
|
|
|
return |
|
117
|
|
|
|
|
118
|
|
|
# ========================================================================= |
|
119
|
|
|
# Below all the Pulser Interface routines. |
|
120
|
|
|
# ========================================================================= |
|
121
|
|
|
|
|
122
|
|
|
def get_constraints(self): |
|
123
|
|
|
""" |
|
124
|
|
|
Retrieve the hardware constrains from the Pulsing device. |
|
125
|
|
|
|
|
126
|
|
|
@return constraints object: object with pulser constraints as attributes. |
|
127
|
|
|
|
|
128
|
|
|
Provides all the constraints (e.g. sample_rate, amplitude, total_length_bins, |
|
129
|
|
|
channel_config, ...) related to the pulse generator hardware to the caller. |
|
130
|
|
|
|
|
131
|
|
|
SEE PulserConstraints CLASS IN pulser_interface.py FOR AVAILABLE CONSTRAINTS!!! |
|
132
|
|
|
|
|
133
|
|
|
If you are not sure about the meaning, look in other hardware files to get an impression. |
|
134
|
|
|
If still additional constraints are needed, then they have to be added to the |
|
135
|
|
|
PulserConstraints class. |
|
136
|
|
|
|
|
137
|
|
|
Each scalar parameter is an ScalarConstraints object defined in cor.util.interfaces. |
|
138
|
|
|
Essentially it contains min/max values as well as min step size, default value and unit of |
|
139
|
|
|
the parameter. |
|
140
|
|
|
|
|
141
|
|
|
PulserConstraints.activation_config differs, since it contain the channel |
|
142
|
|
|
configuration/activation information of the form: |
|
143
|
|
|
{<descriptor_str>: <channel_set>, |
|
144
|
|
|
<descriptor_str>: <channel_set>, |
|
145
|
|
|
...} |
|
146
|
|
|
|
|
147
|
|
|
If the constraints cannot be set in the pulsing hardware (e.g. because it might have no |
|
148
|
|
|
sequence mode) just leave it out so that the default is used (only zeros). |
|
149
|
|
|
""" |
|
150
|
|
|
# TODO: Check values for AWG7122c |
|
151
|
|
|
constraints = PulserConstraints() |
|
152
|
|
|
|
|
153
|
|
|
if self.get_interleave(): |
|
154
|
|
|
constraints.sample_rate.min = 12.0e9 |
|
155
|
|
|
constraints.sample_rate.max = 24.0e9 |
|
156
|
|
|
constraints.sample_rate.step = 5.0e2 |
|
157
|
|
|
constraints.sample_rate.default = 24.0e9 |
|
158
|
|
|
else: |
|
159
|
|
|
constraints.sample_rate.min = 10.0e6 |
|
160
|
|
|
constraints.sample_rate.max = 12.0e9 |
|
161
|
|
|
constraints.sample_rate.step = 10.0e6 |
|
162
|
|
|
constraints.sample_rate.default = 12.0e9 |
|
163
|
|
|
|
|
164
|
|
|
constraints.a_ch_amplitude.max = 1.0 |
|
165
|
|
|
constraints.a_ch_amplitude.step = 0.001 |
|
166
|
|
|
constraints.a_ch_amplitude.default = 1.0 |
|
167
|
|
|
if self._zeroing_enabled(): |
|
168
|
|
|
constraints.a_ch_amplitude.min = 0.25 |
|
169
|
|
|
else: |
|
170
|
|
|
constraints.a_ch_amplitude.min = 0.5 |
|
171
|
|
|
|
|
172
|
|
|
constraints.d_ch_low.min = -1.4 |
|
173
|
|
|
constraints.d_ch_low.max = 0.9 |
|
174
|
|
|
constraints.d_ch_low.step = 0.01 |
|
175
|
|
|
constraints.d_ch_low.default = 0.0 |
|
176
|
|
|
|
|
177
|
|
|
constraints.d_ch_high.min = -0.9 |
|
178
|
|
|
constraints.d_ch_high.max = 1.4 |
|
179
|
|
|
constraints.d_ch_high.step = 0.01 |
|
180
|
|
|
constraints.d_ch_high.default = 1.4 |
|
181
|
|
|
|
|
182
|
|
|
constraints.waveform_length.min = 1 |
|
183
|
|
|
constraints.waveform_length.step = 1 |
|
184
|
|
|
constraints.waveform_length.default = 80 |
|
185
|
|
|
if '01' in self.installed_options: |
|
186
|
|
|
constraints.waveform_length.max = 64800000 |
|
187
|
|
|
else: |
|
188
|
|
|
constraints.waveform_length.max = 32000000 |
|
189
|
|
|
|
|
190
|
|
|
constraints.waveform_num.min = 1 |
|
191
|
|
|
constraints.waveform_num.max = 32000 |
|
192
|
|
|
constraints.waveform_num.step = 1 |
|
193
|
|
|
constraints.waveform_num.default = 1 |
|
194
|
|
|
|
|
195
|
|
|
constraints.sequence_num.min = 1 |
|
196
|
|
|
constraints.sequence_num.max = 16000 |
|
197
|
|
|
constraints.sequence_num.step = 1 |
|
198
|
|
|
constraints.sequence_num.default = 1 |
|
199
|
|
|
|
|
200
|
|
|
constraints.subsequence_num.min = 1 |
|
201
|
|
|
constraints.subsequence_num.max = 8000 |
|
202
|
|
|
constraints.subsequence_num.step = 1 |
|
203
|
|
|
constraints.subsequence_num.default = 1 |
|
204
|
|
|
|
|
205
|
|
|
# If sequencer mode is available then these should be specified |
|
206
|
|
|
constraints.repetitions.min = 0 |
|
207
|
|
|
constraints.repetitions.max = 65539 |
|
208
|
|
|
constraints.repetitions.step = 1 |
|
209
|
|
|
constraints.repetitions.default = 0 |
|
210
|
|
|
|
|
211
|
|
|
# ToDo: Check how many external triggers this device has |
|
212
|
|
|
constraints.event_triggers = ['A', 'B'] |
|
213
|
|
|
constraints.flags = list() |
|
214
|
|
|
|
|
215
|
|
|
constraints.sequence_steps.min = 0 |
|
216
|
|
|
constraints.sequence_steps.max = 8000 |
|
217
|
|
|
constraints.sequence_steps.step = 1 |
|
218
|
|
|
constraints.sequence_steps.default = 0 |
|
219
|
|
|
|
|
220
|
|
|
# the name a_ch<num> and d_ch<num> are generic names, which describe UNAMBIGUOUSLY the |
|
221
|
|
|
# channels. Here all possible channel configurations are stated, where only the generic |
|
222
|
|
|
# names should be used. The names for the different configurations can be customary chosen. |
|
223
|
|
|
activation_config = OrderedDict() |
|
224
|
|
|
activation_config['All'] = {'a_ch1', 'd_ch1', 'd_ch2', 'a_ch2', 'd_ch3', 'd_ch4'} |
|
225
|
|
|
# Usage of channel 1 only: |
|
226
|
|
|
activation_config['A1_M1_M2'] = {'a_ch1', 'd_ch1', 'd_ch2'} |
|
227
|
|
|
# Usage of channel 2 only: |
|
228
|
|
|
activation_config['A2_M3_M4'] = {'a_ch2', 'd_ch3', 'd_ch4'} |
|
229
|
|
|
# Only both analog channels |
|
230
|
|
|
activation_config['Two_Analog'] = {'a_ch1', 'a_ch2'} |
|
231
|
|
|
# Usage of one analog channel without digital channel |
|
232
|
|
|
activation_config['Analog1'] = {'a_ch1'} |
|
233
|
|
|
# Usage of one analog channel without digital channel |
|
234
|
|
|
activation_config['Analog2'] = {'a_ch2'} |
|
235
|
|
|
constraints.activation_config = activation_config |
|
236
|
|
|
return constraints |
|
237
|
|
|
|
|
238
|
|
|
def pulser_on(self): |
|
239
|
|
|
""" Switches the pulsing device on. |
|
240
|
|
|
|
|
241
|
|
|
@return int: error code (0:OK, -1:error, higher number corresponds to |
|
242
|
|
|
current status of the device. Check then the |
|
243
|
|
|
class variable status_dic.) |
|
244
|
|
|
""" |
|
245
|
|
|
# do nothing if AWG is already running |
|
246
|
|
|
if not self._is_output_on(): |
|
247
|
|
|
self.write('AWGC:RUN') |
|
248
|
|
|
# wait until the AWG is actually running |
|
249
|
|
|
while not self._is_output_on(): |
|
250
|
|
|
time.sleep(0.2) |
|
251
|
|
|
return self.get_status() |
|
252
|
|
|
|
|
253
|
|
|
def pulser_off(self): |
|
254
|
|
|
""" Switches the pulsing device off. |
|
255
|
|
|
|
|
256
|
|
|
@return int: error code (0:OK, -1:error, higher number corresponds to |
|
257
|
|
|
current status of the device. Check then the |
|
258
|
|
|
class variable status_dic.) |
|
259
|
|
|
""" |
|
260
|
|
|
# do nothing if AWG is already idle |
|
261
|
|
|
if self._is_output_on(): |
|
262
|
|
|
self.write('AWGC:STOP') |
|
263
|
|
|
# wait until the AWG has actually stopped |
|
264
|
|
|
while self._is_output_on(): |
|
265
|
|
|
time.sleep(0.2) |
|
266
|
|
|
return self.get_status() |
|
267
|
|
|
|
|
268
|
|
View Code Duplication |
def load_waveform(self, load_dict): |
|
|
|
|
|
|
269
|
|
|
""" Loads a waveform to the specified channel of the pulsing device. |
|
270
|
|
|
For devices that have a workspace (i.e. AWG) this will load the waveform from the device |
|
271
|
|
|
workspace into the channel. |
|
272
|
|
|
For a device without mass memory this will make the waveform/pattern that has been |
|
273
|
|
|
previously written with self.write_waveform ready to play. |
|
274
|
|
|
|
|
275
|
|
|
@param load_dict: dict|list, a dictionary with keys being one of the available channel |
|
276
|
|
|
index and values being the name of the already written |
|
277
|
|
|
waveform to load into the channel. |
|
278
|
|
|
Examples: {1: rabi_ch1, 2: rabi_ch2} or |
|
279
|
|
|
{1: rabi_ch2, 2: rabi_ch1} |
|
280
|
|
|
If just a list of waveform names if given, the channel |
|
281
|
|
|
association will be invoked from the channel |
|
282
|
|
|
suffix '_ch1', '_ch2' etc. |
|
283
|
|
|
|
|
284
|
|
|
@return dict: Dictionary containing the actually loaded waveforms per channel. |
|
285
|
|
|
""" |
|
286
|
|
|
if isinstance(load_dict, list): |
|
287
|
|
|
new_dict = dict() |
|
288
|
|
|
for waveform in load_dict: |
|
289
|
|
|
channel = int(waveform.rsplit('_ch', 1)[1]) |
|
290
|
|
|
new_dict[channel] = waveform |
|
291
|
|
|
load_dict = new_dict |
|
292
|
|
|
|
|
293
|
|
|
# Get all active channels |
|
294
|
|
|
chnl_activation = self.get_active_channels() |
|
295
|
|
|
analog_channels = sorted( |
|
296
|
|
|
chnl for chnl in chnl_activation if chnl.startswith('a') and chnl_activation[chnl]) |
|
297
|
|
|
|
|
298
|
|
|
# Check if all channels to load to are active |
|
299
|
|
|
channels_to_set = {'a_ch{0:d}'.format(chnl_num) for chnl_num in load_dict} |
|
300
|
|
|
if not channels_to_set.issubset(analog_channels): |
|
301
|
|
|
self.log.error('Unable to load all waveforms into channels.\n' |
|
302
|
|
|
'One or more channels to set are not active.') |
|
303
|
|
|
return self.get_loaded_assets() |
|
304
|
|
|
|
|
305
|
|
|
# Check if all waveforms to load are present on device memory |
|
306
|
|
|
if not set(load_dict.values()).issubset(self.get_waveform_names()): |
|
307
|
|
|
self.log.error('Unable to load waveforms into channels.\n' |
|
308
|
|
|
'One or more waveforms to load are missing on device memory.') |
|
309
|
|
|
return self.get_loaded_assets() |
|
310
|
|
|
|
|
311
|
|
|
# Load waveforms into channels |
|
312
|
|
|
for chnl_num, waveform in load_dict.items(): |
|
313
|
|
|
# load into channel |
|
314
|
|
|
self.write('SOUR{0:d}:WAV "{1}"'.format(chnl_num, waveform)) |
|
315
|
|
|
while self.query('SOUR{0:d}:WAV?'.format(chnl_num)) != waveform: |
|
316
|
|
|
time.sleep(0.1) |
|
317
|
|
|
|
|
318
|
|
|
self.__loaded_sequence = '' |
|
319
|
|
|
return self.get_loaded_assets() |
|
320
|
|
|
|
|
321
|
|
|
def load_sequence(self, sequence_name): |
|
322
|
|
|
""" Loads a sequence to the channels of the device in order to be ready for playback. |
|
323
|
|
|
For devices that have a workspace (i.e. AWG) this will load the sequence from the device |
|
324
|
|
|
workspace into the channels. |
|
325
|
|
|
For a device without mass memory this will make the waveform/pattern that has been |
|
326
|
|
|
previously written with self.write_waveform ready to play. |
|
327
|
|
|
|
|
328
|
|
|
@param sequence_name: dict|list, a dictionary with keys being one of the available channel |
|
329
|
|
|
index and values being the name of the already written |
|
330
|
|
|
waveform to load into the channel. |
|
331
|
|
|
Examples: {1: rabi_ch1, 2: rabi_ch2} or |
|
332
|
|
|
{1: rabi_ch2, 2: rabi_ch1} |
|
333
|
|
|
If just a list of waveform names if given, the channel |
|
334
|
|
|
association will be invoked from the channel |
|
335
|
|
|
suffix '_ch1', '_ch2' etc. |
|
336
|
|
|
|
|
337
|
|
|
@return dict: Dictionary containing the actually loaded waveforms per channel. |
|
338
|
|
|
""" |
|
339
|
|
|
if sequence_name not in self.get_sequence_names(): |
|
340
|
|
|
self.log.error('Unable to load sequence.\n' |
|
341
|
|
|
'Sequence to load is missing on device memory.') |
|
342
|
|
|
return self.get_loaded_assets() |
|
343
|
|
|
|
|
344
|
|
|
# Load sequence |
|
345
|
|
|
file_name = sequence_name + '{0}.seq'.format(sequence_name) |
|
346
|
|
|
|
|
347
|
|
|
# self.tell('MMEMORY:IMPORT "{0}","{1}",SEQ \n'.format(asset_name , asset_name + '.seq')) |
|
348
|
|
|
self.write('SOUR1:FUNC:USER "{0!s}"'.format(file_name)) |
|
349
|
|
|
print(self.query('SOUR1:FUNC:USER?')) |
|
350
|
|
|
# while self.query('SOUR1:FUNC:USER?') != sequence_name: |
|
351
|
|
|
# time.sleep(0.2) |
|
352
|
|
|
|
|
353
|
|
|
# set the AWG to the event jump mode: |
|
354
|
|
|
self.write('AWGC:EVENT:JMODE EJUMP') |
|
355
|
|
|
|
|
356
|
|
|
self.__loaded_sequence = sequence_name |
|
357
|
|
|
return self.get_loaded_assets() |
|
358
|
|
|
|
|
359
|
|
|
def get_loaded_assets(self): |
|
360
|
|
|
""" |
|
361
|
|
|
Retrieve the currently loaded asset names for each active channel of the device. |
|
362
|
|
|
The returned dictionary will have the channel numbers as keys. |
|
363
|
|
|
In case of loaded waveforms the dictionary values will be the waveform names. |
|
364
|
|
|
In case of a loaded sequence the values will be the sequence name appended by a suffix |
|
365
|
|
|
representing the track loaded to the respective channel (i.e. '<sequence_name>_1'). |
|
366
|
|
|
|
|
367
|
|
|
@return (dict, str): Dictionary with keys being the channel number and values being the |
|
368
|
|
|
respective asset loaded into the channel, |
|
369
|
|
|
string describing the asset type ('waveform' or 'sequence') |
|
370
|
|
|
""" |
|
371
|
|
|
# Get all active channels |
|
372
|
|
|
chnl_activation = self.get_active_channels() |
|
373
|
|
|
channel_numbers = sorted(int(chnl.split('_ch')[1]) for chnl in chnl_activation if |
|
374
|
|
|
chnl.startswith('a') and chnl_activation[chnl]) |
|
375
|
|
|
|
|
376
|
|
|
# Get assets per channel |
|
377
|
|
|
loaded_assets = dict() |
|
378
|
|
|
current_type = None |
|
379
|
|
|
for chnl_num in channel_numbers: |
|
380
|
|
|
# Ask AWG for currently loaded waveform or sequence. The answer for a waveform will |
|
381
|
|
|
# look like |
|
382
|
|
|
# FIXME: What does an AWG7000 return with this query? |
|
383
|
|
|
asset_name = self.query('SOUR1:FUNC:USER?') |
|
384
|
|
|
|
|
385
|
|
|
return loaded_assets, current_type |
|
386
|
|
|
|
|
387
|
|
|
def clear_all(self): |
|
388
|
|
|
""" Clears all loaded waveforms from the pulse generators RAM/workspace. |
|
389
|
|
|
|
|
390
|
|
|
@return int: error code (0:OK, -1:error) |
|
391
|
|
|
""" |
|
392
|
|
|
self.write('WLIS:WAV:DEL ALL') |
|
393
|
|
|
self.__loaded_sequence = '' |
|
394
|
|
|
return 0 |
|
395
|
|
|
|
|
396
|
|
|
def get_status(self): |
|
397
|
|
|
""" Retrieves the status of the pulsing hardware |
|
398
|
|
|
|
|
399
|
|
|
@return (int, dict): inter value of the current status with the |
|
400
|
|
|
corresponding dictionary containing status |
|
401
|
|
|
description for all the possible status variables |
|
402
|
|
|
of the pulse generator hardware |
|
403
|
|
|
""" |
|
404
|
|
|
status_dic = {-1: 'Failed Request or Communication', |
|
405
|
|
|
0: 'Device has stopped, but can receive commands', |
|
406
|
|
|
1: 'Device is active and running', |
|
407
|
|
|
2: 'Device is waiting for trigger.'} |
|
408
|
|
|
current_status = -1 if self.awg is None else int(self.query('AWGC:RST?')) |
|
409
|
|
|
return current_status, status_dic |
|
410
|
|
|
|
|
411
|
|
|
def get_sample_rate(self): |
|
412
|
|
|
""" Get the sample rate of the pulse generator hardware |
|
413
|
|
|
|
|
414
|
|
|
@return float: The current sample rate of the device (in Hz) |
|
415
|
|
|
|
|
416
|
|
|
Do not return a saved sample rate from an attribute, but instead retrieve the current |
|
417
|
|
|
sample rate directly from the device. |
|
418
|
|
|
""" |
|
419
|
|
|
return float(self.query('SOUR1:FREQ?')) |
|
420
|
|
|
|
|
421
|
|
|
def set_sample_rate(self, sample_rate): |
|
422
|
|
|
""" Set the sample rate of the pulse generator hardware. |
|
423
|
|
|
|
|
424
|
|
|
@param float sample_rate: The sampling rate to be set (in Hz) |
|
425
|
|
|
|
|
426
|
|
|
@return float: the sample rate returned from the device (in Hz). |
|
427
|
|
|
|
|
428
|
|
|
Note: After setting the sampling rate of the device, use the actually set return value for |
|
429
|
|
|
further processing. |
|
430
|
|
|
""" |
|
431
|
|
|
self.write('SOUR1:FREQ {0:.4G}MHz\n'.format(sample_rate / 1e6)) |
|
432
|
|
|
while int(self.query('*OPC?')) != 1: |
|
433
|
|
|
time.sleep(0.1) |
|
434
|
|
|
# Here we need to wait, because when the sampling rate is changed AWG is busy |
|
435
|
|
|
# and therefore the ask in get_sample_rate will return an empty string. |
|
436
|
|
|
time.sleep(1) |
|
437
|
|
|
return self.get_sample_rate() |
|
438
|
|
|
|
|
439
|
|
|
# def load_asset(self, asset_name, load_dict=None): |
|
440
|
|
|
# """ Loads a sequence or waveform to the specified channel of the pulsing |
|
441
|
|
|
# device. |
|
442
|
|
|
# |
|
443
|
|
|
# @param str asset_name: The name of the asset to be loaded |
|
444
|
|
|
# |
|
445
|
|
|
# @param dict load_dict: a dictionary with keys being one of the |
|
446
|
|
|
# available channel numbers and items being the |
|
447
|
|
|
# name of the already sampled |
|
448
|
|
|
# waveform/sequence files. |
|
449
|
|
|
# Examples: {1: rabi_Ch1, 2: rabi_Ch2} |
|
450
|
|
|
# {1: rabi_Ch2, 2: rabi_Ch1} |
|
451
|
|
|
# This parameter is optional. If none is given |
|
452
|
|
|
# then the channel association is invoked from |
|
453
|
|
|
# the sequence generation, |
|
454
|
|
|
# i.e. the filename appendix (_Ch1, _Ch2 etc.) |
|
455
|
|
|
# |
|
456
|
|
|
# @return int: error code (0:OK, -1:error) |
|
457
|
|
|
# |
|
458
|
|
|
# Unused for digital pulse generators without sequence storage capability |
|
459
|
|
|
# (PulseBlaster, FPGA). |
|
460
|
|
|
# """ |
|
461
|
|
|
# if load_dict is None: |
|
462
|
|
|
# load_dict = {} |
|
463
|
|
|
# path = self.ftp_path + self.get_asset_dir_on_device() |
|
464
|
|
|
# |
|
465
|
|
|
# # Find all files associated with the specified asset name |
|
466
|
|
|
# file_list = self._get_filenames_on_device() |
|
467
|
|
|
# filename = [] |
|
468
|
|
|
# |
|
469
|
|
|
# # Get current channel activation state to be restored after loading the asset |
|
470
|
|
|
# chnl_activation = self.get_active_channels() |
|
471
|
|
|
# |
|
472
|
|
|
# if (asset_name + '.seq') in file_list: |
|
473
|
|
|
# file_name = asset_name + '.seq' |
|
474
|
|
|
# |
|
475
|
|
|
# # self.tell('MMEMORY:IMPORT "{0}","{1}",SEQ \n'.format(asset_name , asset_name + '.seq')) |
|
476
|
|
|
# self.tell('SOUR1:FUNC:USER "{0!s}/{1!s}"\n'.format(path, file_name)) |
|
477
|
|
|
# # self.tell('SOUR1:FUNC:USER "{0}/{1}"\n'.format(path, file_name)) |
|
478
|
|
|
# # set the AWG to the event jump mode: |
|
479
|
|
|
# self.tell('AWGCONTROL:EVENT:JMODE EJUMP') |
|
480
|
|
|
# |
|
481
|
|
|
# self.current_loaded_asset = asset_name |
|
482
|
|
|
# else: |
|
483
|
|
|
# |
|
484
|
|
|
# for file in file_list: |
|
485
|
|
|
# |
|
486
|
|
|
# if file == asset_name + '_ch1.wfm': |
|
487
|
|
|
# #load into workspace |
|
488
|
|
|
# self.tell('MMEMORY:IMPORT "{0}","{1}",WFM \n'.format(asset_name +'_ch1', asset_name + '_ch1.wfm')) |
|
489
|
|
|
# #load into channel |
|
490
|
|
|
# self.tell('SOUR1:WAVEFORM "{0}"\n'.format(asset_name + '_ch1')) |
|
491
|
|
|
# self.log.debug('Ch1 loaded: "{0}"'.format(asset_name)) |
|
492
|
|
|
# filename.append(file) |
|
493
|
|
|
# elif file == asset_name + '_ch2.wfm': |
|
494
|
|
|
# self.tell('MMEMORY:IMPORT "{0}","{1}",WFM \n'.format(asset_name + '_ch2', asset_name + '_ch2.wfm')) |
|
495
|
|
|
# self.tell('SOUR2:WAVEFORM "{0}"\n'.format(asset_name + '_ch2')) |
|
496
|
|
|
# self.log.debug('Ch2 loaded: "{0}"'.format(asset_name)) |
|
497
|
|
|
# filename.append(file) |
|
498
|
|
|
# |
|
499
|
|
|
# if load_dict == {} and filename == []: |
|
500
|
|
|
# self.log.warning('No file and channel provided for load!\nCorrect that!\n' |
|
501
|
|
|
# 'Command will be ignored.') |
|
502
|
|
|
# |
|
503
|
|
|
# # for channel_num in list(load_dict): |
|
504
|
|
|
# #asset_name = str(load_dict[channel_num]) |
|
505
|
|
|
# #self.tell('MMEMORY:IMPORT "{0}","{1}",WFM \n'.format(asset_name + '_ch{0}'.format(int(channel_num)), asset_name + '_ch{0}.wfm'.format(int(channel_num)))) |
|
506
|
|
|
# #self.tell('SOUR1:WAVEFORM "{0}"\n'.format(asset_name + '_ch{0}'.format(int(channel_num)))) |
|
507
|
|
|
# |
|
508
|
|
|
# #if len(list(load_dict)) > 0: |
|
509
|
|
|
# self.current_loaded_asset = asset_name |
|
510
|
|
|
# |
|
511
|
|
|
# # Restore channel activation state |
|
512
|
|
|
# self.set_active_channels(chnl_activation) |
|
513
|
|
|
# return 0 |
|
514
|
|
|
|
|
515
|
|
|
|
|
516
|
|
|
|
|
517
|
|
|
# file_list = self._get_filenames_on_device() |
|
518
|
|
|
# filename = [] |
|
519
|
|
|
# |
|
520
|
|
|
# for file in file_list: |
|
521
|
|
|
# if file == asset_name+'_ch1.wfm' or file == asset_name+'_ch2.wfm': |
|
522
|
|
|
# filename.append(file) |
|
523
|
|
|
# |
|
524
|
|
|
# |
|
525
|
|
|
# # Check if something could be found |
|
526
|
|
|
# if len(filename) == 0: |
|
527
|
|
|
# self.log.error('No files associated with asset {0} were found on AWG7122c.' |
|
528
|
|
|
# 'Load to channels failed!'.format(asset_name) |
|
529
|
|
|
# ) # if asset.split("_")[-1][:3] == 'ch1': |
|
530
|
|
|
# self.tell('SOUR1:WAVEFORM "{0}"\n'.format(asset[:-4])) |
|
531
|
|
|
# if asset.split("_")[-1][:3] == 'ch2': |
|
532
|
|
|
# self.tell('SOUR2:WAVEFORM "{0}"\n'.format(asset[:-4])) |
|
533
|
|
|
# self.current_loaded_asset = asset_name |
|
534
|
|
|
# else: |
|
535
|
|
|
# for channel in load_dict: |
|
536
|
|
|
# return -1 |
|
537
|
|
|
# |
|
538
|
|
|
# self.log.info('The following files associated with the asset {0} were found on AWG7122c:\n' |
|
539
|
|
|
# '"{1}"'.format(asset_name, filename)) |
|
540
|
|
|
# |
|
541
|
|
|
# # load files in AWG Waveform list |
|
542
|
|
|
# for asset in filename: |
|
543
|
|
|
# if asset.endswith('.wfm'): |
|
544
|
|
|
# self.tell('MMEMORY:IMPORT "{0}","{1}",WFM \n'.format(asset[:-4], asset)) |
|
545
|
|
|
# else: |
|
546
|
|
|
# self.log.error('Could not load asset {0} to AWG7122c:\n' |
|
547
|
|
|
# '"{1}"'.format(asset_name, filename)) |
|
548
|
|
|
# |
|
549
|
|
|
# file_path = self.ftp_path + self.get_asset_dir_on_device() |
|
550
|
|
|
# # simply use the channel association of the filenames if no load_dict is given |
|
551
|
|
|
# if load_dict == {}: |
|
552
|
|
|
# for asset in filename: |
|
553
|
|
|
# # load waveforms into channels as given in filename |
|
554
|
|
|
|
|
555
|
|
|
# # load waveforms into channels |
|
556
|
|
|
# name = load_dict[channel] |
|
557
|
|
|
# self.tell('SOUR'+str(channel)+':FUNC:USER "{0}/{1}"\n'.format(file_path, name)) |
|
558
|
|
|
# self.current_loaded_asset = name |
|
559
|
|
|
# |
|
560
|
|
|
# return 0 |
|
561
|
|
|
|
|
562
|
|
|
def get_analog_level(self, amplitude=None, offset=None): |
|
563
|
|
|
""" Retrieve the analog amplitude and offset of the provided channels. |
|
564
|
|
|
|
|
565
|
|
|
@param list amplitude: optional, if the amplitude value (in Volt peak to peak, i.e. the |
|
566
|
|
|
full amplitude) of a specific channel is desired. |
|
567
|
|
|
@param list offset: optional, if the offset value (in Volt) of a specific channel is |
|
568
|
|
|
desired. |
|
569
|
|
|
|
|
570
|
|
|
@return: (dict, dict): tuple of two dicts, with keys being the channel descriptor string |
|
571
|
|
|
(i.e. 'a_ch1') and items being the values for those channels. |
|
572
|
|
|
Amplitude is always denoted in Volt-peak-to-peak and Offset in volts. |
|
573
|
|
|
|
|
574
|
|
|
Note: Do not return a saved amplitude and/or offset value but instead retrieve the current |
|
575
|
|
|
amplitude and/or offset directly from the device. |
|
576
|
|
|
|
|
577
|
|
|
If nothing (or None) is passed then the levels of all channels will be returned. If no |
|
578
|
|
|
analog channels are present in the device, return just empty dicts. |
|
579
|
|
|
|
|
580
|
|
|
Example of a possible input: |
|
581
|
|
|
amplitude = ['a_ch1', 'a_ch4'], offset = None |
|
582
|
|
|
to obtain the amplitude of channel 1 and 4 and the offset of all channels |
|
583
|
|
|
{'a_ch1': -0.5, 'a_ch4': 2.0} {'a_ch1': 0.0, 'a_ch2': 0.0, 'a_ch3': 1.0, 'a_ch4': 0.0} |
|
584
|
|
|
""" |
|
585
|
|
|
# FIXME: No sanity checking done here with constraints |
|
586
|
|
|
amp = dict() |
|
587
|
|
|
off = dict() |
|
588
|
|
|
|
|
589
|
|
|
chnl_list = self._get_all_analog_channels() |
|
590
|
|
|
|
|
591
|
|
|
# get pp amplitudes |
|
592
|
|
View Code Duplication |
if amplitude is None: |
|
|
|
|
|
|
593
|
|
|
for ch_num, chnl in enumerate(chnl_list): |
|
594
|
|
|
amp[chnl] = float(self.query('SOUR{0:d}:VOLT:AMPL?'.format(ch_num + 1))) |
|
595
|
|
|
else: |
|
596
|
|
|
for chnl in amplitude: |
|
597
|
|
|
if chnl in chnl_list: |
|
598
|
|
|
ch_num = int(chnl.rsplit('_ch', 1)[1]) |
|
599
|
|
|
amp[chnl] = float(self.query('SOUR{0:d}:VOLT:AMPL?'.format(ch_num))) |
|
600
|
|
|
else: |
|
601
|
|
|
self.log.warning('Get analog amplitude from AWG7122c channel "{0}" failed. ' |
|
602
|
|
|
'Channel non-existent.'.format(chnl)) |
|
603
|
|
|
|
|
604
|
|
|
# get voltage offsets |
|
605
|
|
|
no_offset = '02' in self.installed_options or '06' in self.installed_options |
|
606
|
|
View Code Duplication |
if offset is None: |
|
|
|
|
|
|
607
|
|
|
for ch_num, chnl in enumerate(chnl_list): |
|
608
|
|
|
off[chnl] = 0.0 if no_offset else float( |
|
609
|
|
|
self.query('SOUR{0:d}:VOLT:OFFS?'.format(ch_num))) |
|
610
|
|
|
else: |
|
611
|
|
|
for chnl in offset: |
|
612
|
|
|
if chnl in chnl_list: |
|
613
|
|
|
ch_num = int(chnl.rsplit('_ch', 1)[1]) |
|
614
|
|
|
off[chnl] = 0.0 if no_offset else float( |
|
615
|
|
|
self.query('SOUR{0:d}:VOLT:OFFS?'.format(ch_num))) |
|
616
|
|
|
else: |
|
617
|
|
|
self.log.warning('Get analog offset from AWG7122c channel "{0}" failed. ' |
|
618
|
|
|
'Channel non-existent.'.format(chnl)) |
|
619
|
|
|
return amp, off |
|
620
|
|
|
|
|
621
|
|
View Code Duplication |
def set_analog_level(self, amplitude=None, offset=None): |
|
|
|
|
|
|
622
|
|
|
""" Set amplitude and/or offset value of the provided analog channel(s). |
|
623
|
|
|
|
|
624
|
|
|
@param dict amplitude: dictionary, with key being the channel descriptor string |
|
625
|
|
|
(i.e. 'a_ch1', 'a_ch2') and items being the amplitude values |
|
626
|
|
|
(in Volt peak to peak, i.e. the full amplitude) for the desired |
|
627
|
|
|
channel. |
|
628
|
|
|
@param dict offset: dictionary, with key being the channel descriptor string |
|
629
|
|
|
(i.e. 'a_ch1', 'a_ch2') and items being the offset values |
|
630
|
|
|
(in absolute volt) for the desired channel. |
|
631
|
|
|
|
|
632
|
|
|
@return (dict, dict): tuple of two dicts with the actual set values for amplitude and |
|
633
|
|
|
offset for ALL channels. |
|
634
|
|
|
|
|
635
|
|
|
If nothing is passed then the command will return the current amplitudes/offsets. |
|
636
|
|
|
|
|
637
|
|
|
Note: After setting the amplitude and/or offset values of the device, use the actual set |
|
638
|
|
|
return values for further processing. |
|
639
|
|
|
""" |
|
640
|
|
|
# Check the inputs by using the constraints... |
|
641
|
|
|
constraints = self.get_constraints() |
|
642
|
|
|
# ...and the available analog channels |
|
643
|
|
|
analog_channels = self._get_all_analog_channels() |
|
644
|
|
|
|
|
645
|
|
|
# amplitude sanity check |
|
646
|
|
|
if amplitude is not None: |
|
647
|
|
|
for chnl in amplitude: |
|
648
|
|
|
ch_num = int(chnl.rsplit('_ch', 1)[1]) |
|
649
|
|
|
if chnl not in analog_channels: |
|
650
|
|
|
self.log.warning('Channel to set (a_ch{0}) not available in AWG.\nSetting ' |
|
651
|
|
|
'analogue voltage for this channel ignored.'.format(chnl)) |
|
652
|
|
|
del amplitude[chnl] |
|
653
|
|
|
if amplitude[chnl] < constraints.a_ch_amplitude.min: |
|
654
|
|
|
self.log.warning('Minimum Vpp for channel "{0}" is {1}. Requested Vpp of {2}V ' |
|
655
|
|
|
'was ignored and instead set to min value.' |
|
656
|
|
|
''.format(chnl, constraints.a_ch_amplitude.min, |
|
657
|
|
|
amplitude[chnl])) |
|
658
|
|
|
amplitude[chnl] = constraints.a_ch_amplitude.min |
|
659
|
|
|
elif amplitude[chnl] > constraints.a_ch_amplitude.max: |
|
660
|
|
|
self.log.warning('Maximum Vpp for channel "{0}" is {1}. Requested Vpp of {2}V ' |
|
661
|
|
|
'was ignored and instead set to max value.' |
|
662
|
|
|
''.format(chnl, constraints.a_ch_amplitude.max, |
|
663
|
|
|
amplitude[chnl])) |
|
664
|
|
|
amplitude[chnl] = constraints.a_ch_amplitude.max |
|
665
|
|
|
# offset sanity check |
|
666
|
|
|
if offset is not None: |
|
667
|
|
|
for chnl in offset: |
|
668
|
|
|
ch_num = int(chnl.rsplit('_ch', 1)[1]) |
|
669
|
|
|
if chnl not in analog_channels: |
|
670
|
|
|
self.log.warning('Channel to set (a_ch{0}) not available in AWG.\nSetting ' |
|
671
|
|
|
'offset voltage for this channel ignored.'.format(chnl)) |
|
672
|
|
|
del offset[chnl] |
|
673
|
|
|
if offset[chnl] < constraints.a_ch_offset.min: |
|
674
|
|
|
self.log.warning('Minimum offset for channel "{0}" is {1}. Requested offset of ' |
|
675
|
|
|
'{2}V was ignored and instead set to min value.' |
|
676
|
|
|
''.format(chnl, constraints.a_ch_offset.min, offset[chnl])) |
|
677
|
|
|
offset[chnl] = constraints.a_ch_offset.min |
|
678
|
|
|
elif offset[chnl] > constraints.a_ch_offset.max: |
|
679
|
|
|
self.log.warning('Maximum offset for channel "{0}" is {1}. Requested offset of ' |
|
680
|
|
|
'{2}V was ignored and instead set to max value.' |
|
681
|
|
|
''.format(chnl, constraints.a_ch_offset.max, |
|
682
|
|
|
offset[chnl])) |
|
683
|
|
|
offset[chnl] = constraints.a_ch_offset.max |
|
684
|
|
|
|
|
685
|
|
|
if amplitude is not None: |
|
686
|
|
|
for a_ch in amplitude: |
|
687
|
|
|
ch_num = int(chnl.rsplit('_ch', 1)[1]) |
|
688
|
|
|
self.write('SOUR{0:d}:VOLT:AMPL {1}'.format(ch_num, amplitude[a_ch])) |
|
689
|
|
|
while int(self.query('*OPC?')) != 1: |
|
690
|
|
|
time.sleep(0.1) |
|
691
|
|
|
|
|
692
|
|
|
no_offset = '02' in self.installed_options or '06' in self.installed_options |
|
693
|
|
|
if offset is not None and not no_offset: |
|
694
|
|
|
for a_ch in offset: |
|
695
|
|
|
ch_num = int(chnl.rsplit('_ch', 1)[1]) |
|
696
|
|
|
self.write('SOUR{0:d}:VOLT:OFFSET {1}'.format(ch_num, offset[a_ch])) |
|
697
|
|
|
while int(self.query('*OPC?')) != 1: |
|
698
|
|
|
time.sleep(0.1) |
|
699
|
|
|
return self.get_analog_level() |
|
700
|
|
|
|
|
701
|
|
View Code Duplication |
def get_digital_level(self, low=None, high=None): |
|
|
|
|
|
|
702
|
|
|
""" Retrieve the digital low and high level of the provided/all channels. |
|
703
|
|
|
|
|
704
|
|
|
@param list low: optional, if the low value (in Volt) of a specific channel is desired. |
|
705
|
|
|
@param list high: optional, if the high value (in Volt) of a specific channel is desired. |
|
706
|
|
|
|
|
707
|
|
|
@return: (dict, dict): tuple of two dicts, with keys being the channel descriptor strings |
|
708
|
|
|
(i.e. 'd_ch1', 'd_ch2') and items being the values for those |
|
709
|
|
|
channels. Both low and high value of a channel is denoted in volts. |
|
710
|
|
|
|
|
711
|
|
|
Note: Do not return a saved low and/or high value but instead retrieve |
|
712
|
|
|
the current low and/or high value directly from the device. |
|
713
|
|
|
|
|
714
|
|
|
If nothing (or None) is passed then the levels of all channels are being returned. |
|
715
|
|
|
If no digital channels are present, return just an empty dict. |
|
716
|
|
|
|
|
717
|
|
|
Example of a possible input: |
|
718
|
|
|
low = ['d_ch1', 'd_ch4'] |
|
719
|
|
|
to obtain the low voltage values of digital channel 1 an 4. A possible answer might be |
|
720
|
|
|
{'d_ch1': -0.5, 'd_ch4': 2.0} {'d_ch1': 1.0, 'd_ch2': 1.0, 'd_ch3': 1.0, 'd_ch4': 4.0} |
|
721
|
|
|
Since no high request was performed, the high values for ALL channels are returned (here 4). |
|
722
|
|
|
""" |
|
723
|
|
|
low_val = {} |
|
724
|
|
|
high_val = {} |
|
725
|
|
|
|
|
726
|
|
|
digital_channels = self._get_all_digital_channels() |
|
727
|
|
|
|
|
728
|
|
|
if low is None: |
|
729
|
|
|
low = digital_channels |
|
730
|
|
|
if high is None: |
|
731
|
|
|
high = digital_channels |
|
732
|
|
|
|
|
733
|
|
|
# get low marker levels |
|
734
|
|
|
for chnl in low: |
|
735
|
|
|
if chnl not in digital_channels: |
|
736
|
|
|
continue |
|
737
|
|
|
d_ch_number = int(chnl.rsplit('_ch', 1)[1]) |
|
738
|
|
|
a_ch_number = (1 + d_ch_number) // 2 |
|
739
|
|
|
marker_index = 2 - (d_ch_number % 2) |
|
740
|
|
|
low_val[chnl] = float( |
|
741
|
|
|
self.query('SOUR{0:d}:MARK{1:d}:VOLT:LOW?'.format(a_ch_number, marker_index))) |
|
742
|
|
|
# get high marker levels |
|
743
|
|
|
for chnl in high: |
|
744
|
|
|
if chnl not in digital_channels: |
|
745
|
|
|
continue |
|
746
|
|
|
d_ch_number = int(chnl.rsplit('_ch', 1)[1]) |
|
747
|
|
|
a_ch_number = (1 + d_ch_number) // 2 |
|
748
|
|
|
marker_index = 2 - (d_ch_number % 2) |
|
749
|
|
|
high_val[chnl] = float( |
|
750
|
|
|
self.query('SOUR{0:d}:MARK{1:d}:VOLT:HIGH?'.format(a_ch_number, marker_index))) |
|
751
|
|
|
|
|
752
|
|
|
return low_val, high_val |
|
753
|
|
|
|
|
754
|
|
|
def set_digital_level(self, low=None, high=None): |
|
755
|
|
|
""" Set low and/or high value of the provided digital channel. |
|
756
|
|
|
|
|
757
|
|
|
@param dict low: dictionary, with key being the channel and items being |
|
758
|
|
|
the low values (in volt) for the desired channel. |
|
759
|
|
|
@param dict high: dictionary, with key being the channel and items being |
|
760
|
|
|
the high values (in volt) for the desired channel. |
|
761
|
|
|
|
|
762
|
|
|
@return (dict, dict): tuple of two dicts where first dict denotes the |
|
763
|
|
|
current low value and the second dict the high |
|
764
|
|
|
value. |
|
765
|
|
|
|
|
766
|
|
|
If nothing is passed then the command will return two empty dicts. |
|
767
|
|
|
|
|
768
|
|
|
Note: After setting the high and/or low values of the device, retrieve |
|
769
|
|
|
them again for obtaining the actual set value(s) and use that |
|
770
|
|
|
information for further processing. |
|
771
|
|
|
|
|
772
|
|
|
The major difference to analog signals is that digital signals are |
|
773
|
|
|
either ON or OFF, whereas analog channels have a varying amplitude |
|
774
|
|
|
range. In contrast to analog output levels, digital output levels are |
|
775
|
|
|
defined by a voltage, which corresponds to the ON status and a voltage |
|
776
|
|
|
which corresponds to the OFF status (both denoted in (absolute) voltage) |
|
777
|
|
|
|
|
778
|
|
|
In general there is no bijective correspondence between |
|
779
|
|
|
(amplitude, offset) and (value high, value low)! |
|
780
|
|
|
""" |
|
781
|
|
|
# If you want to check the input use the constraints: |
|
782
|
|
|
# constraints = self.get_constraints() |
|
783
|
|
|
# |
|
784
|
|
|
# for d_ch, value in low.items(): |
|
785
|
|
|
# #FIXME: Tell the device the proper digital voltage low value: |
|
786
|
|
|
# # self.tell('SOURCE1:MARKER{0}:VOLTAGE:LOW {1}'.format(d_ch, low[d_ch])) |
|
787
|
|
|
# pass |
|
788
|
|
|
# |
|
789
|
|
|
# for d_ch, value in high.items(): |
|
790
|
|
|
# #FIXME: Tell the device the proper digital voltage high value: |
|
791
|
|
|
# # self.tell('SOURCE1:MARKER{0}:VOLTAGE:HIGH {1}'.format(d_ch, high[d_ch])) |
|
792
|
|
|
# pass |
|
793
|
|
|
return self.get_digital_level() |
|
794
|
|
|
|
|
795
|
|
|
def get_active_channels(self, ch=None): |
|
796
|
|
|
""" Get the active channels of the pulse generator hardware. |
|
797
|
|
|
|
|
798
|
|
|
@param list ch: optional, if specific analog or digital channels are needed to be asked |
|
799
|
|
|
without obtaining all the channels. |
|
800
|
|
|
|
|
801
|
|
|
@return dict: where keys denoting the channel string and items boolean expressions whether |
|
802
|
|
|
channel are active or not. |
|
803
|
|
|
|
|
804
|
|
|
Example for an possible input (order is not important): |
|
805
|
|
|
ch = ['a_ch2', 'd_ch2', 'a_ch1', 'd_ch5', 'd_ch1'] |
|
806
|
|
|
then the output might look like |
|
807
|
|
|
{'a_ch2': True, 'd_ch2': False, 'a_ch1': False, 'd_ch5': True, 'd_ch1': False} |
|
808
|
|
|
|
|
809
|
|
|
If no parameter (or None) is passed to this method all channel states will be returned. |
|
810
|
|
|
""" |
|
811
|
|
|
# If you want to check the input use the constraints: |
|
812
|
|
|
# constraints = self.get_constraints() |
|
813
|
|
|
|
|
814
|
|
|
analog_channels = self._get_all_analog_channels() |
|
815
|
|
|
|
|
816
|
|
|
active_ch = dict() |
|
817
|
|
|
for ch_num, a_ch in enumerate(analog_channels): |
|
818
|
|
|
ch_num = ch_num + 1 |
|
819
|
|
|
# check what analog channels are active |
|
820
|
|
|
active_ch[a_ch] = bool(int(self.query('OUTPUT{0:d}:STATE?'.format(ch_num)))) |
|
821
|
|
|
# check how many markers are active on each channel, i.e. the DAC resolution |
|
822
|
|
View Code Duplication |
if active_ch[a_ch]: |
|
|
|
|
|
|
823
|
|
|
digital_mrk = 10 - int(self.query('SOUR{0:d}:DAC:RES?'.format(ch_num))) |
|
824
|
|
|
if digital_mrk == 2: |
|
825
|
|
|
active_ch['d_ch{0:d}'.format(ch_num * 2)] = True |
|
826
|
|
|
active_ch['d_ch{0:d}'.format(ch_num * 2 - 1)] = True |
|
827
|
|
|
else: |
|
828
|
|
|
active_ch['d_ch{0:d}'.format(ch_num * 2)] = False |
|
829
|
|
|
active_ch['d_ch{0:d}'.format(ch_num * 2 - 1)] = False |
|
830
|
|
|
else: |
|
831
|
|
|
active_ch['d_ch{0:d}'.format(ch_num * 2)] = False |
|
832
|
|
|
active_ch['d_ch{0:d}'.format(ch_num * 2 - 1)] = False |
|
833
|
|
|
|
|
834
|
|
|
# return either all channel information or just the one asked for. |
|
835
|
|
|
if ch is not None: |
|
836
|
|
|
chnl_to_delete = [chnl for chnl in active_ch if chnl not in ch] |
|
837
|
|
|
for chnl in chnl_to_delete: |
|
838
|
|
|
del active_ch[chnl] |
|
839
|
|
|
return active_ch |
|
840
|
|
|
|
|
841
|
|
|
def set_active_channels(self, ch=None): |
|
842
|
|
|
""" Set the active channels for the pulse generator hardware. |
|
843
|
|
|
|
|
844
|
|
|
@param dict ch: dictionary with keys being the analog or digital string generic names for |
|
845
|
|
|
the channels (i.e. 'd_ch1', 'a_ch2') with items being a boolean value. |
|
846
|
|
|
True: Activate channel, False: Deactivate channel |
|
847
|
|
|
|
|
848
|
|
|
@return dict: with the actual set values for ALL active analog and digital channels |
|
849
|
|
|
|
|
850
|
|
|
If nothing is passed then the command will simply return the unchanged current state. |
|
851
|
|
|
|
|
852
|
|
|
Note: After setting the active channels of the device, |
|
853
|
|
|
use the returned dict for further processing. |
|
854
|
|
|
|
|
855
|
|
|
Example for possible input: |
|
856
|
|
|
ch={'a_ch2': True, 'd_ch1': False, 'd_ch3': True, 'd_ch4': True} |
|
857
|
|
|
to activate analog channel 2 digital channel 3 and 4 and to deactivate |
|
858
|
|
|
digital channel 1. |
|
859
|
|
|
|
|
860
|
|
|
The hardware itself has to handle, whether separate channel activation is possible. |
|
861
|
|
|
""" |
|
862
|
|
|
current_channel_state = self.get_active_channels() |
|
863
|
|
|
|
|
864
|
|
|
if ch is None: |
|
865
|
|
|
return current_channel_state |
|
866
|
|
|
|
|
867
|
|
|
if not set(current_channel_state).issuperset(ch): |
|
868
|
|
|
self.log.error('Trying to (de)activate channels that are not present in AWG.\n' |
|
869
|
|
|
'Setting of channel activation aborted.') |
|
870
|
|
|
return current_channel_state |
|
871
|
|
|
|
|
872
|
|
|
# Determine new channel activation states |
|
873
|
|
|
new_channels_state = current_channel_state.copy() |
|
874
|
|
|
for chnl in ch: |
|
875
|
|
|
new_channels_state[chnl] = ch[chnl] |
|
876
|
|
|
|
|
877
|
|
|
# check if the channels to set are part of the activation_config constraints |
|
878
|
|
|
constraints = self.get_constraints() |
|
879
|
|
|
new_active_channels = {chnl for chnl in new_channels_state if new_channels_state[chnl]} |
|
880
|
|
|
if new_active_channels not in constraints.activation_config.values(): |
|
881
|
|
|
self.log.error('activation_config to set ({0}) is not allowed according to constraints.' |
|
882
|
|
|
''.format(new_active_channels)) |
|
883
|
|
|
return current_channel_state |
|
884
|
|
|
|
|
885
|
|
|
# get lists of all analog channels |
|
886
|
|
|
analog_channels = self._get_all_analog_channels() |
|
887
|
|
|
|
|
888
|
|
|
# calculate dac resolution for each analog channel and set it in hardware. |
|
889
|
|
|
# Also (de)activate the analog channels accordingly |
|
890
|
|
|
for a_ch in analog_channels: |
|
891
|
|
|
ach_num = int(a_ch.rsplit('_ch', 1)[1]) |
|
892
|
|
|
# determine number of markers for current a_ch |
|
893
|
|
|
if new_channels_state['d_ch{0:d}'.format(2 * ach_num)]: |
|
894
|
|
|
marker_num = 2 |
|
895
|
|
|
else: |
|
896
|
|
|
marker_num = 0 |
|
897
|
|
|
# set DAC resolution for this channel |
|
898
|
|
|
dac_res = 10 - marker_num |
|
899
|
|
|
self.write('SOUR{0:d}:DAC:RES {1:d}'.format(ach_num, dac_res)) |
|
900
|
|
|
# (de)activate the analog channel |
|
901
|
|
|
if new_channels_state[a_ch]: |
|
902
|
|
|
self.write('OUTPUT{0:d}:STATE ON'.format(ach_num)) |
|
903
|
|
|
else: |
|
904
|
|
|
self.write('OUTPUT{0:d}:STATE OFF'.format(ach_num)) |
|
905
|
|
|
return self.get_active_channels() |
|
906
|
|
|
|
|
907
|
|
|
def write_waveform(self, name, analog_samples, digital_samples, is_first_chunk, is_last_chunk, |
|
908
|
|
|
total_number_of_samples): |
|
909
|
|
|
""" |
|
910
|
|
|
Write a new waveform or append samples to an already existing waveform on the device memory. |
|
911
|
|
|
The flags is_first_chunk and is_last_chunk can be used as indicator if a new waveform should |
|
912
|
|
|
be created or if the write process to a waveform should be terminated. |
|
913
|
|
|
|
|
914
|
|
|
@param name: str, the name of the waveform to be created/append to |
|
915
|
|
|
@param analog_samples: numpy.ndarray of type float32 containing the voltage samples |
|
916
|
|
|
@param digital_samples: numpy.ndarray of type bool containing the marker states |
|
917
|
|
|
(if analog channels are active, this must be the same length as |
|
918
|
|
|
analog_samples) |
|
919
|
|
|
@param is_first_chunk: bool, flag indicating if it is the first chunk to write. |
|
920
|
|
|
If True this method will create a new empty wavveform. |
|
921
|
|
|
If False the samples are appended to the existing waveform. |
|
922
|
|
|
@param is_last_chunk: bool, flag indicating if it is the last chunk to write. |
|
923
|
|
|
Some devices may need to know when to close the appending wfm. |
|
924
|
|
|
@param total_number_of_samples: int, The number of sample points for the entire waveform |
|
925
|
|
|
(not only the currently written chunk) |
|
926
|
|
|
|
|
927
|
|
|
@return: (int, list) number of samples written (-1 indicates failed process) and list of |
|
928
|
|
|
created waveform names |
|
929
|
|
|
""" |
|
930
|
|
|
waveforms = list() |
|
931
|
|
|
|
|
932
|
|
|
# Sanity checks |
|
933
|
|
|
constraints = self.get_constraints() |
|
934
|
|
|
|
|
935
|
|
|
if len(analog_samples) == 0: |
|
936
|
|
|
self.log.error('No analog samples passed to write_waveform method in awg7122c.') |
|
937
|
|
|
return -1, waveforms |
|
938
|
|
|
|
|
939
|
|
|
if total_number_of_samples < constraints.waveform_length.min: |
|
940
|
|
|
self.log.error('Unable to write waveform.\nNumber of samples to write ({0:d}) is ' |
|
941
|
|
|
'smaller than the allowed minimum waveform length ({1:d}).' |
|
942
|
|
|
''.format(total_number_of_samples, constraints.waveform_length.min)) |
|
943
|
|
|
return -1, waveforms |
|
944
|
|
|
if total_number_of_samples > constraints.waveform_length.max: |
|
945
|
|
|
self.log.error('Unable to write waveform.\nNumber of samples to write ({0:d}) is ' |
|
946
|
|
|
'greater than the allowed maximum waveform length ({1:d}).' |
|
947
|
|
|
''.format(total_number_of_samples, constraints.waveform_length.max)) |
|
948
|
|
|
return -1, waveforms |
|
949
|
|
|
|
|
950
|
|
|
# determine active channels |
|
951
|
|
|
activation_dict = self.get_active_channels() |
|
952
|
|
|
active_channels = {chnl for chnl in activation_dict if activation_dict[chnl]} |
|
953
|
|
|
active_analog = sorted(chnl for chnl in active_channels if chnl.startswith('a')) |
|
954
|
|
|
|
|
955
|
|
|
# Sanity check of channel numbers |
|
956
|
|
|
if active_channels != set(analog_samples.keys()).union(set(digital_samples.keys())): |
|
957
|
|
|
self.log.error('Mismatch of channel activation and sample array dimensions for ' |
|
958
|
|
|
'waveform creation.\nChannel activation is: {0}\nSample arrays have: ' |
|
959
|
|
|
''.format(active_channels, |
|
960
|
|
|
set(analog_samples.keys()).union(set(digital_samples.keys())))) |
|
961
|
|
|
return -1, waveforms |
|
962
|
|
|
|
|
963
|
|
|
# Write waveforms. One for each analog channel. |
|
964
|
|
|
for a_ch in active_analog: |
|
965
|
|
|
# Get the integer analog channel number |
|
966
|
|
|
a_ch_num = int(a_ch.rsplit('ch', 1)[1]) |
|
967
|
|
|
# Get the digital channel specifiers belonging to this analog channel markers |
|
968
|
|
|
mrk_ch_1 = 'd_ch{0:d}'.format(a_ch_num * 2 - 1) |
|
969
|
|
|
mrk_ch_2 = 'd_ch{0:d}'.format(a_ch_num * 2) |
|
970
|
|
|
|
|
971
|
|
|
start = time.time() |
|
972
|
|
|
# Encode marker information in an array of bytes (uint8). Avoid intermediate copies!!! |
|
973
|
|
|
if mrk_ch_1 in digital_samples and mrk_ch_2 in digital_samples: |
|
974
|
|
|
mrk_bytes = digital_samples[mrk_ch_2].view('uint8') |
|
975
|
|
|
tmp_bytes = digital_samples[mrk_ch_1].view('uint8') |
|
976
|
|
|
np.left_shift(mrk_bytes, 7, out=mrk_bytes) |
|
977
|
|
|
np.left_shift(tmp_bytes, 6, out=tmp_bytes) |
|
978
|
|
|
np.add(mrk_bytes, tmp_bytes, out=mrk_bytes) |
|
979
|
|
|
else: |
|
980
|
|
|
mrk_bytes = None |
|
981
|
|
|
print('Prepare digital channel data: {0}'.format(time.time() - start)) |
|
982
|
|
|
|
|
983
|
|
|
# Create waveform name string |
|
984
|
|
|
wfm_name = '{0}_ch{1:d}'.format(name, a_ch_num) |
|
985
|
|
|
|
|
986
|
|
|
# Write WFM file for waveform |
|
987
|
|
|
start = time.time() |
|
988
|
|
|
self._write_wfm(filename=wfm_name, |
|
989
|
|
|
analog_samples=analog_samples[a_ch], |
|
990
|
|
|
digital_samples=mrk_bytes, |
|
991
|
|
|
is_first_chunk=is_first_chunk, |
|
992
|
|
|
is_last_chunk=is_last_chunk, |
|
993
|
|
|
total_number_of_samples=total_number_of_samples) |
|
994
|
|
|
|
|
995
|
|
|
print('Write WFM file: {0}'.format(time.time() - start)) |
|
996
|
|
|
|
|
997
|
|
|
# transfer waveform to AWG and load into workspace |
|
998
|
|
|
start = time.time() |
|
999
|
|
|
self._send_file(filename=wfm_name + '.wfm') |
|
1000
|
|
|
print('Send WFM file: {0}'.format(time.time() - start)) |
|
1001
|
|
|
|
|
1002
|
|
|
start = time.time() |
|
1003
|
|
|
self.write('MMEM:IMP "{0}","{1}",WFM'.format(wfm_name, wfm_name + '.wfm')) |
|
1004
|
|
|
# Wait for everything to complete |
|
1005
|
|
|
while int(self.query('*OPC?')) != 1: |
|
1006
|
|
|
time.sleep(0.2) |
|
1007
|
|
|
# Just to make sure |
|
1008
|
|
|
while wfm_name not in self.get_waveform_names(): |
|
1009
|
|
|
time.sleep(0.2) |
|
1010
|
|
|
print('Load WFM file into workspace: {0}'.format(time.time() - start)) |
|
1011
|
|
|
|
|
1012
|
|
|
# Append created waveform name to waveform list |
|
1013
|
|
|
waveforms.append(wfm_name) |
|
1014
|
|
|
return total_number_of_samples, waveforms |
|
1015
|
|
|
|
|
1016
|
|
|
def write_sequence(self, name, sequence_parameters): |
|
1017
|
|
|
""" |
|
1018
|
|
|
Write a new sequence on the device memory. |
|
1019
|
|
|
|
|
1020
|
|
|
@param name: str, the name of the waveform to be created/append to |
|
1021
|
|
|
@param sequence_parameters: dict, dictionary containing the parameters for a sequence |
|
1022
|
|
|
|
|
1023
|
|
|
@return: int, number of sequence steps written (-1 indicates failed process) |
|
1024
|
|
|
""" |
|
1025
|
|
|
# Check if device has sequencer option installed |
|
1026
|
|
|
if not self.has_sequence_mode(): |
|
1027
|
|
|
self.log.error('Direct sequence generation in AWG not possible. Sequencer option not ' |
|
1028
|
|
|
'installed.') |
|
1029
|
|
|
return -1 |
|
1030
|
|
|
# FIXME: I can not possibly implement that without the hardware to test it. |
|
1031
|
|
|
return -1 |
|
1032
|
|
|
|
|
1033
|
|
|
def get_waveform_names(self): |
|
1034
|
|
|
""" Retrieve the names of all uploaded waveforms on the device. |
|
1035
|
|
|
|
|
1036
|
|
|
@return list: List of all uploaded waveform name strings in the device workspace. |
|
1037
|
|
|
""" |
|
1038
|
|
|
wfm_list_len = int(self.query('WLIS:SIZE?')) |
|
1039
|
|
|
wfm_list = list() |
|
1040
|
|
|
for index in range(1, wfm_list_len + 1): |
|
1041
|
|
|
wfm_list.append(self.query('WLIS:NAME? {0:d}'.format(index))) |
|
1042
|
|
|
return sorted(wfm_list) |
|
1043
|
|
|
|
|
1044
|
|
|
def get_sequence_names(self): |
|
1045
|
|
|
""" Retrieve the names of all uploaded sequence on the device. |
|
1046
|
|
|
|
|
1047
|
|
|
@return list: List of all uploaded sequence name strings in the device workspace. |
|
1048
|
|
|
""" |
|
1049
|
|
|
# FIXME: No idea without hardware to test |
|
1050
|
|
|
return list() |
|
1051
|
|
|
|
|
1052
|
|
|
def delete_waveform(self, waveform_name): |
|
1053
|
|
|
""" Delete the waveform with name "waveform_name" from the device memory. |
|
1054
|
|
|
|
|
1055
|
|
|
@param str waveform_name: The name of the waveform to be deleted |
|
1056
|
|
|
Optionally a list of waveform names can be passed. |
|
1057
|
|
|
|
|
1058
|
|
|
@return list: a list of deleted waveform names. |
|
1059
|
|
|
""" |
|
1060
|
|
|
if isinstance(waveform_name, str): |
|
1061
|
|
|
waveform_name = [waveform_name] |
|
1062
|
|
|
|
|
1063
|
|
|
avail_waveforms = self.get_waveform_names() |
|
1064
|
|
|
deleted_waveforms = list() |
|
1065
|
|
|
for waveform in waveform_name: |
|
1066
|
|
|
if waveform in avail_waveforms: |
|
1067
|
|
|
self.write('WLIS:WAV:DEL "{0}"'.format(waveform)) |
|
1068
|
|
|
deleted_waveforms.append(waveform) |
|
1069
|
|
|
return sorted(deleted_waveforms) |
|
1070
|
|
|
|
|
1071
|
|
|
def delete_sequence(self, sequence_name): |
|
1072
|
|
|
""" Delete the sequence with name "sequence_name" from the device memory. |
|
1073
|
|
|
|
|
1074
|
|
|
@param str sequence_name: The name of the sequence to be deleted |
|
1075
|
|
|
Optionally a list of sequence names can be passed. |
|
1076
|
|
|
|
|
1077
|
|
|
@return list: a list of deleted sequence names. |
|
1078
|
|
|
""" |
|
1079
|
|
|
# FIXME: Again... no idea without hardware to play with |
|
1080
|
|
|
return list() |
|
1081
|
|
|
|
|
1082
|
|
|
def get_interleave(self): |
|
1083
|
|
|
""" Check whether Interleave is ON or OFF in AWG. |
|
1084
|
|
|
|
|
1085
|
|
|
@return bool: True: ON, False: OFF |
|
1086
|
|
|
|
|
1087
|
|
|
Will always return False for pulse generator hardware without interleave. |
|
1088
|
|
|
""" |
|
1089
|
|
|
return bool(int(self.query('AWGC:INT:STAT?'))) |
|
1090
|
|
|
|
|
1091
|
|
|
def set_interleave(self, state=False): |
|
1092
|
|
|
""" Turns the interleave of an AWG on or off. |
|
1093
|
|
|
|
|
1094
|
|
|
@param bool state: The state the interleave should be set to |
|
1095
|
|
|
(True: ON, False: OFF) |
|
1096
|
|
|
|
|
1097
|
|
|
@return bool: actual interleave status (True: ON, False: OFF) |
|
1098
|
|
|
|
|
1099
|
|
|
Note: After setting the interleave of the device, retrieve the |
|
1100
|
|
|
interleave again and use that information for further processing. |
|
1101
|
|
|
|
|
1102
|
|
|
Unused for pulse generator hardware other than an AWG. |
|
1103
|
|
|
""" |
|
1104
|
|
|
if not isinstance(state, bool): |
|
1105
|
|
|
return self.get_interleave() |
|
1106
|
|
|
|
|
1107
|
|
|
# if the interleave state should not be changed from the current state, do nothing. |
|
1108
|
|
|
if state is self.get_interleave(): |
|
1109
|
|
|
return state |
|
1110
|
|
|
|
|
1111
|
|
|
self.write('AWGC:INT:STAT {0:d}'.format(int(state))) |
|
1112
|
|
|
while int(self.query('*OPC?')) != 1: |
|
1113
|
|
|
time.sleep(0.1) |
|
1114
|
|
|
return self.get_interleave() |
|
1115
|
|
|
|
|
1116
|
|
|
def write(self, command): |
|
1117
|
|
|
""" Sends a command string to the device. |
|
1118
|
|
|
|
|
1119
|
|
|
@param string command: string containing the command |
|
1120
|
|
|
|
|
1121
|
|
|
@return int: error code (0:OK, -1:error) |
|
1122
|
|
|
""" |
|
1123
|
|
|
bytes_written, enum_status_code = self.awg.write(command) |
|
1124
|
|
|
return int(enum_status_code) |
|
1125
|
|
|
|
|
1126
|
|
|
def query(self, question): |
|
1127
|
|
|
""" Asks the device a 'question' and receive and return an answer from it. |
|
1128
|
|
|
|
|
1129
|
|
|
@param string question: string containing the command |
|
1130
|
|
|
|
|
1131
|
|
|
@return string: the answer of the device to the 'question' in a string |
|
1132
|
|
|
""" |
|
1133
|
|
|
answer = self.awg.query(question) |
|
1134
|
|
|
answer = answer.strip() |
|
1135
|
|
|
answer = answer.rstrip('\n') |
|
1136
|
|
|
answer = answer.rstrip() |
|
1137
|
|
|
answer = answer.strip('"') |
|
1138
|
|
|
return answer |
|
1139
|
|
|
|
|
1140
|
|
|
def reset(self): |
|
1141
|
|
|
""" Reset the device. |
|
1142
|
|
|
|
|
1143
|
|
|
@return int: error code (0:OK, -1:error) |
|
1144
|
|
|
""" |
|
1145
|
|
|
self.write('*RST') |
|
1146
|
|
|
self.write('*WAI') |
|
1147
|
|
|
return 0 |
|
1148
|
|
|
|
|
1149
|
|
|
def has_sequence_mode(self): |
|
1150
|
|
|
""" Asks the pulse generator whether sequence mode exists. |
|
1151
|
|
|
|
|
1152
|
|
|
@return: bool, True for yes, False for no. |
|
1153
|
|
|
""" |
|
1154
|
|
|
return True |
|
1155
|
|
|
|
|
1156
|
|
|
def set_lowpass_filter(self, a_ch, cutoff_freq): |
|
1157
|
|
|
""" Set a lowpass filter to the analog channels of the AWG. |
|
1158
|
|
|
|
|
1159
|
|
|
@param int a_ch: To which channel to apply, either 1 or 2. |
|
1160
|
|
|
@param cutoff_freq: Cutoff Frequency of the lowpass filter in Hz. |
|
1161
|
|
|
""" |
|
1162
|
|
|
if a_ch not in (1, 2): |
|
1163
|
|
|
return |
|
1164
|
|
|
self.write('OUTPUT{0:d}:FILTER:LPASS:FREQUENCY {1:f}MHz'.format(a_ch, cutoff_freq / 1e6)) |
|
1165
|
|
|
|
|
1166
|
|
|
def set_jump_timing(self, synchronous=False): |
|
1167
|
|
|
"""Sets control of the jump timing in the AWG. |
|
1168
|
|
|
|
|
1169
|
|
|
@param bool synchronous: if True the jump timing will be set to synchornous, otherwise the |
|
1170
|
|
|
jump timing will be set to asynchronous. |
|
1171
|
|
|
|
|
1172
|
|
|
If the Jump timing is set to asynchornous the jump occurs as quickly as possible after an |
|
1173
|
|
|
event occurs (e.g. event jump tigger), if set to synchornous the jump is made after the |
|
1174
|
|
|
current waveform is output. The default value is asynchornous. |
|
1175
|
|
|
""" |
|
1176
|
|
|
timing = 'SYNC' if synchronous else 'ASYNC' |
|
1177
|
|
|
self.write('EVEN:JTIM {0}'.format(timing)) |
|
1178
|
|
|
|
|
1179
|
|
|
def set_mode(self, mode): |
|
1180
|
|
|
"""Change the output mode of the AWG5000 series. |
|
1181
|
|
|
|
|
1182
|
|
|
@param str mode: Options for mode (case-insensitive): |
|
1183
|
|
|
continuous - 'C' |
|
1184
|
|
|
triggered - 'T' |
|
1185
|
|
|
gated - 'G' |
|
1186
|
|
|
sequence - 'S' |
|
1187
|
|
|
|
|
1188
|
|
|
""" |
|
1189
|
|
|
look_up = {'C': 'CONT', |
|
1190
|
|
|
'T': 'TRIG', |
|
1191
|
|
|
'G': 'GAT', |
|
1192
|
|
|
'E': 'ENH', |
|
1193
|
|
|
'S': 'SEQ'} |
|
1194
|
|
|
self.write('AWGC:RMOD {0!s}'.format(look_up[mode.upper()])) |
|
1195
|
|
|
|
|
1196
|
|
|
# works |
|
1197
|
|
|
def get_sequencer_mode(self, output_as_int=False): |
|
1198
|
|
|
""" Asks the AWG which sequencer mode it is using. |
|
1199
|
|
|
|
|
1200
|
|
|
@param: bool output_as_int: optional boolean variable to set the output |
|
1201
|
|
|
@return: str or int with the following meaning: |
|
1202
|
|
|
'HARD' or 0 indicates Hardware Mode |
|
1203
|
|
|
'SOFT' or 1 indicates Software Mode |
|
1204
|
|
|
'Error' or -1 indicates a failure of request |
|
1205
|
|
|
|
|
1206
|
|
|
It can be either in Hardware Mode or in Software Mode. The optional |
|
1207
|
|
|
variable output_as_int sets if the returned value should be either an |
|
1208
|
|
|
integer number or string. |
|
1209
|
|
|
""" |
|
1210
|
|
|
message = self.query('AWGC:SEQ:TYPE?') |
|
1211
|
|
|
if 'HARD' in message: |
|
1212
|
|
|
return 0 if output_as_int else 'Hardware-Sequencer' |
|
1213
|
|
|
elif 'SOFT' in message: |
|
1214
|
|
|
return 1 if output_as_int else 'Software-Sequencer' |
|
1215
|
|
|
return -1 if output_as_int else 'Request-Error' |
|
1216
|
|
|
|
|
1217
|
|
|
def _delete_file(self, filename): |
|
1218
|
|
|
""" |
|
1219
|
|
|
|
|
1220
|
|
|
@param str filename: The full filename to delete from FTP cwd |
|
1221
|
|
|
""" |
|
1222
|
|
|
if filename in self._get_filenames_on_device(): |
|
1223
|
|
|
with FTP(self._ip_address) as ftp: |
|
1224
|
|
|
ftp.login(user=self._username, passwd=self._password) |
|
1225
|
|
|
ftp.cwd(self.ftp_working_dir) |
|
1226
|
|
|
ftp.delete(filename) |
|
1227
|
|
|
return |
|
1228
|
|
|
|
|
1229
|
|
View Code Duplication |
def _send_file(self, filename): |
|
|
|
|
|
|
1230
|
|
|
""" |
|
1231
|
|
|
|
|
1232
|
|
|
@param filename: |
|
1233
|
|
|
@return: |
|
1234
|
|
|
""" |
|
1235
|
|
|
# check input |
|
1236
|
|
|
if not filename: |
|
1237
|
|
|
self.log.error('No filename provided for file upload to awg!\nCommand will be ignored.') |
|
1238
|
|
|
return -1 |
|
1239
|
|
|
|
|
1240
|
|
|
filepath = os.path.join(self._tmp_work_dir, filename) |
|
1241
|
|
|
if not os.path.isfile(filepath): |
|
1242
|
|
|
self.log.error('No file "{0}" found in "{1}". Unable to upload!' |
|
1243
|
|
|
''.format(filename, self._tmp_work_dir)) |
|
1244
|
|
|
return -1 |
|
1245
|
|
|
|
|
1246
|
|
|
# Delete old file on AWG by the same filename |
|
1247
|
|
|
self._delete_file(filename) |
|
1248
|
|
|
|
|
1249
|
|
|
# Transfer file |
|
1250
|
|
|
with FTP(self._ip_address) as ftp: |
|
1251
|
|
|
ftp.login(user=self._username, passwd=self._password) |
|
1252
|
|
|
ftp.cwd(self.ftp_working_dir) |
|
1253
|
|
|
with open(filepath, 'rb') as file: |
|
1254
|
|
|
ftp.storbinary('STOR ' + filename, file) |
|
1255
|
|
|
return 0 |
|
1256
|
|
|
|
|
1257
|
|
View Code Duplication |
def _get_filenames_on_device(self): |
|
|
|
|
|
|
1258
|
|
|
""" |
|
1259
|
|
|
|
|
1260
|
|
|
@return list: filenames found in <ftproot>\\waves |
|
1261
|
|
|
""" |
|
1262
|
|
|
filename_list = list() |
|
1263
|
|
|
with FTP(self._ip_address) as ftp: |
|
1264
|
|
|
ftp.login(user=self._username, passwd=self._password) |
|
1265
|
|
|
ftp.cwd(self.ftp_working_dir) |
|
1266
|
|
|
# get only the files from the dir and skip possible directories |
|
1267
|
|
|
log = list() |
|
1268
|
|
|
ftp.retrlines('LIST', callback=log.append) |
|
1269
|
|
|
for line in log: |
|
1270
|
|
|
if '<DIR>' not in line: |
|
1271
|
|
|
# that is how a potential line is looking like: |
|
1272
|
|
|
# '05-10-16 05:22PM 292 SSR aom adjusted.seq' |
|
1273
|
|
|
# The first part consists of the date information. Remove this information and |
|
1274
|
|
|
# separate the first number, which indicates the size of the file. This is |
|
1275
|
|
|
# necessary if the filename contains whitespaces. |
|
1276
|
|
|
size_filename = line[18:].lstrip() |
|
1277
|
|
|
# split after the first appearing whitespace and take the rest as filename. |
|
1278
|
|
|
# Remove for safety all trailing and leading whitespaces: |
|
1279
|
|
|
filename = size_filename.split(' ', 1)[1].strip() |
|
1280
|
|
|
filename_list.append(filename) |
|
1281
|
|
|
return filename_list |
|
1282
|
|
|
|
|
1283
|
|
|
def _get_all_channels(self): |
|
1284
|
|
|
""" |
|
1285
|
|
|
Helper method to return a sorted list of all technically available channel descriptors |
|
1286
|
|
|
(e.g. ['a_ch1', 'a_ch2', 'd_ch1', 'd_ch2']) |
|
1287
|
|
|
|
|
1288
|
|
|
@return list: Sorted list of channels |
|
1289
|
|
|
""" |
|
1290
|
|
|
avail_channels = ['a_ch1', 'd_ch1', 'd_ch2'] |
|
1291
|
|
|
if not self.get_interleave(): |
|
1292
|
|
|
avail_channels.extend(['a_ch2', 'd_ch3', 'd_ch4']) |
|
1293
|
|
|
return sorted(avail_channels) |
|
1294
|
|
|
|
|
1295
|
|
|
def _get_all_analog_channels(self): |
|
1296
|
|
|
""" |
|
1297
|
|
|
Helper method to return a sorted list of all technically available analog channel |
|
1298
|
|
|
descriptors (e.g. ['a_ch1', 'a_ch2']) |
|
1299
|
|
|
|
|
1300
|
|
|
@return list: Sorted list of analog channels |
|
1301
|
|
|
""" |
|
1302
|
|
|
return sorted(chnl for chnl in self._get_all_channels() if chnl.startswith('a')) |
|
1303
|
|
|
|
|
1304
|
|
|
def _get_all_digital_channels(self): |
|
1305
|
|
|
""" |
|
1306
|
|
|
Helper method to return a sorted list of all technically available digital channel |
|
1307
|
|
|
descriptors (e.g. ['d_ch1', 'd_ch2']) |
|
1308
|
|
|
|
|
1309
|
|
|
@return list: Sorted list of digital channels |
|
1310
|
|
|
""" |
|
1311
|
|
|
return sorted(chnl for chnl in self._get_all_channels() if chnl.startswith('d')) |
|
1312
|
|
|
|
|
1313
|
|
|
def _is_output_on(self): |
|
1314
|
|
|
""" |
|
1315
|
|
|
Aks the AWG if the output is enabled, i.e. if the AWG is running |
|
1316
|
|
|
|
|
1317
|
|
|
@return bool: True: output on, False: output off |
|
1318
|
|
|
""" |
|
1319
|
|
|
return bool(int(self.query('AWGC:RST?'))) |
|
1320
|
|
|
|
|
1321
|
|
|
def _zeroing_enabled(self): |
|
1322
|
|
|
""" |
|
1323
|
|
|
Checks if the zeroing option is enabled. Only available on devices with option '06'. |
|
1324
|
|
|
|
|
1325
|
|
|
@return bool: True: enabled, False: disabled |
|
1326
|
|
|
""" |
|
1327
|
|
|
if '06' not in self.installed_options: |
|
1328
|
|
|
return False |
|
1329
|
|
|
return bool(int(self.query('AWGC:INT:ZER?'))) |
|
1330
|
|
|
|
|
1331
|
|
|
def _write_wfm(self, filename, analog_samples, marker_bytes, is_first_chunk, is_last_chunk, |
|
1332
|
|
|
total_number_of_samples): |
|
1333
|
|
|
""" |
|
1334
|
|
|
Appends a sampled chunk of a whole waveform to a wfm-file. Create the file |
|
1335
|
|
|
if it is the first chunk. |
|
1336
|
|
|
If both flags (is_first_chunk, is_last_chunk) are set to TRUE it means |
|
1337
|
|
|
that the whole ensemble is written as a whole in one big chunk. |
|
1338
|
|
|
|
|
1339
|
|
|
@param filename: string, represents the name of the sampled waveform |
|
1340
|
|
|
@param analog_samples: dict containing float32 numpy ndarrays, contains the |
|
1341
|
|
|
samples for the analog channels that |
|
1342
|
|
|
are to be written by this function call. |
|
1343
|
|
|
@param marker_bytes: np.ndarray containing bool numpy ndarrays, contains the samples |
|
1344
|
|
|
for the digital channels that |
|
1345
|
|
|
are to be written by this function call. |
|
1346
|
|
|
@param total_number_of_samples: int, The total number of samples in the |
|
1347
|
|
|
entire waveform. Has to be known in advance. |
|
1348
|
|
|
@param is_first_chunk: bool, indicates if the current chunk is the |
|
1349
|
|
|
first write to this file. |
|
1350
|
|
|
@param is_last_chunk: bool, indicates if the current chunk is the last |
|
1351
|
|
|
write to this file. |
|
1352
|
|
|
""" |
|
1353
|
|
|
# The memory overhead of the tmp file write/read process in bytes. |
|
1354
|
|
|
tmp_bytes_overhead = 104857600 # 100 MB |
|
1355
|
|
|
tmp_samples = tmp_bytes_overhead // 5 |
|
1356
|
|
|
if tmp_samples > len(analog_samples): |
|
1357
|
|
|
tmp_samples = len(analog_samples) |
|
1358
|
|
|
|
|
1359
|
|
|
if not filename.endswith('.wfm'): |
|
1360
|
|
|
filename += '.wfm' |
|
1361
|
|
|
wfm_path = os.path.join(self._tmp_work_dir, filename) |
|
1362
|
|
|
|
|
1363
|
|
|
# if it is the first chunk, create the WFM file with header. |
|
1364
|
|
|
if is_first_chunk: |
|
1365
|
|
|
with open(wfm_path, 'wb') as wfm_file: |
|
1366
|
|
|
# write the first line, which is the header file, if first chunk is passed: |
|
1367
|
|
|
num_bytes = str(int(total_number_of_samples * 5)) |
|
1368
|
|
|
num_digits = str(len(num_bytes)) |
|
1369
|
|
|
header = 'MAGIC 1000\r\n#{0}{1}'.format(num_digits, num_bytes) |
|
1370
|
|
|
wfm_file.write(header.encode()) |
|
1371
|
|
|
|
|
1372
|
|
|
# For the WFM file format unfortunately we need to write the digital sampels together |
|
1373
|
|
|
# with the analog samples. Therefore we need a temporary copy of all samples for each |
|
1374
|
|
|
# analog channel. |
|
1375
|
|
|
write_array = np.zeros(tmp_samples, dtype='float32, uint8') |
|
1376
|
|
|
|
|
1377
|
|
|
# Consecutively prepare and write chunks of maximal size tmp_bytes_overhead to file |
|
1378
|
|
|
samples_written = 0 |
|
1379
|
|
|
with open(wfm_path, 'ab') as wfm_file: |
|
1380
|
|
|
while samples_written < len(analog_samples): |
|
1381
|
|
|
write_end = samples_written + write_array.size |
|
1382
|
|
|
# Prepare tmp write array |
|
1383
|
|
|
write_array['f0'] = analog_samples[samples_written:write_end] |
|
1384
|
|
|
if marker_bytes is not None: |
|
1385
|
|
|
write_array['f1'] = marker_bytes[samples_written:write_end] |
|
1386
|
|
|
# Write to file |
|
1387
|
|
|
wfm_file.write(write_array) |
|
1388
|
|
|
# Increment write counter |
|
1389
|
|
|
samples_written = write_end |
|
1390
|
|
|
# Reduce write array size if |
|
1391
|
|
|
if 0 < total_number_of_samples - samples_written < write_array.size: |
|
1392
|
|
|
write_array.resize(total_number_of_samples - samples_written) |
|
1393
|
|
|
|
|
1394
|
|
|
del write_array |
|
1395
|
|
|
|
|
1396
|
|
|
# append footer if it's the last chunk to write |
|
1397
|
|
|
if is_last_chunk: |
|
1398
|
|
|
# the footer encodes the sample rate, which was used for that file: |
|
1399
|
|
|
footer = 'CLOCK {0:16.10E}\r\n'.format(self.get_sample_rate()) |
|
1400
|
|
|
with open(wfm_path, 'ab') as wfm_file: |
|
1401
|
|
|
wfm_file.write(footer.encode()) |
|
1402
|
|
|
return |
|
1403
|
|
|
|