1
|
|
|
# -*- coding: utf-8 -*- |
2
|
|
|
|
3
|
|
|
""" |
4
|
|
|
This file contains the Qudi data object classes needed for pulse sequence generation. |
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
|
|
|
import copy |
24
|
|
|
import os |
25
|
|
|
import sys |
26
|
|
|
import inspect |
27
|
|
|
import importlib |
28
|
|
|
from collections import OrderedDict |
29
|
|
|
|
30
|
|
|
from logic.pulsed.sampling_functions import SamplingFunctions as sf |
31
|
|
|
from core.util.modules import get_main_dir |
32
|
|
|
|
33
|
|
|
|
34
|
|
|
class PulseBlockElement(object): |
35
|
|
|
""" |
36
|
|
|
Object representing a single atomic element in a pulse block. |
37
|
|
|
|
38
|
|
|
This class can build waiting times, sine waves, etc. The pulse block may |
39
|
|
|
contain many Pulse_Block_Element Objects. These objects can be displayed in |
40
|
|
|
a GUI as single rows of a Pulse_Block. |
41
|
|
|
""" |
42
|
|
|
def __init__(self, init_length_s=10e-9, increment_s=0, pulse_function=None, digital_high=None): |
43
|
|
|
""" |
44
|
|
|
The constructor for a Pulse_Block_Element needs to have: |
45
|
|
|
|
46
|
|
|
@param float init_length_s: an initial length of the element, this parameters should not be |
47
|
|
|
zero but must have a finite value. |
48
|
|
|
@param float increment_s: the number which will be incremented during each repetition of |
49
|
|
|
this element. |
50
|
|
|
@param dict pulse_function: dictionary with keys being the qudi analog channel string |
51
|
|
|
descriptors ('a_ch1', 'a_ch2' etc.) and the corresponding |
52
|
|
|
objects being instances of the mathematical function objects |
53
|
|
|
provided by SamplingFunctions class. |
54
|
|
|
@param dict digital_high: dictionary with keys being the qudi digital channel string |
55
|
|
|
descriptors ('d_ch1', 'd_ch2' etc.) and the corresponding objects |
56
|
|
|
being boolean values describing if the channel should be logical |
57
|
|
|
low (False) or high (True). |
58
|
|
|
For 3 digital channel it may look like: |
59
|
|
|
{'d_ch1': True, 'd_ch2': False, 'd_ch5': False} |
60
|
|
|
""" |
61
|
|
|
# FIXME: Sanity checks need to be implemented here |
62
|
|
|
self.init_length_s = init_length_s |
63
|
|
|
self.increment_s = increment_s |
64
|
|
|
if pulse_function is None: |
65
|
|
|
self.pulse_function = OrderedDict() |
66
|
|
|
else: |
67
|
|
|
self.pulse_function = pulse_function |
68
|
|
|
if digital_high is None: |
69
|
|
|
self.digital_high = OrderedDict() |
70
|
|
|
else: |
71
|
|
|
self.digital_high = digital_high |
72
|
|
|
|
73
|
|
|
# determine set of used digital and analog channels |
74
|
|
|
self.analog_channels = set(self.pulse_function) |
75
|
|
|
self.digital_channels = set(self.digital_high) |
76
|
|
|
self.channel_set = self.analog_channels.union(self.digital_channels) |
77
|
|
|
|
78
|
|
|
def get_dict_representation(self): |
79
|
|
|
dict_repr = dict() |
80
|
|
|
dict_repr['init_length_s'] = self.init_length_s |
81
|
|
|
dict_repr['increment_s'] = self.increment_s |
82
|
|
|
dict_repr['digital_high'] = self.digital_high |
83
|
|
|
dict_repr['pulse_function'] = dict() |
84
|
|
|
for chnl, func in self.pulse_function.items(): |
85
|
|
|
dict_repr['pulse_function'][chnl] = func.get_dict_representation() |
86
|
|
|
return dict_repr |
87
|
|
|
|
88
|
|
|
@staticmethod |
89
|
|
|
def element_from_dict(element_dict): |
90
|
|
|
for chnl, sample_dict in element_dict['pulse_function'].items(): |
91
|
|
|
sf_class = getattr(sf, sample_dict['name']) |
92
|
|
|
element_dict['pulse_function'][chnl] = sf_class(**sample_dict['params']) |
93
|
|
|
return PulseBlockElement(**element_dict) |
94
|
|
|
|
95
|
|
|
|
96
|
|
|
class PulseBlock(object): |
97
|
|
|
""" |
98
|
|
|
Collection of Pulse_Block_Elements which is called a Pulse_Block. |
99
|
|
|
""" |
100
|
|
|
def __init__(self, name, element_list=None): |
101
|
|
|
""" |
102
|
|
|
The constructor for a Pulse_Block needs to have: |
103
|
|
|
|
104
|
|
|
@param str name: chosen name for the Pulse_Block |
105
|
|
|
@param list element_list: which contains the Pulse_Block_Element Objects forming a |
106
|
|
|
Pulse_Block, e.g. [Pulse_Block_Element, Pulse_Block_Element, ...] |
107
|
|
|
""" |
108
|
|
|
self.name = name |
109
|
|
|
self.element_list = list() if element_list is None else element_list |
110
|
|
|
self.init_length_s = 0.0 |
111
|
|
|
self.increment_s = 0.0 |
112
|
|
|
self.analog_channels = set() |
113
|
|
|
self.digital_channels = set() |
114
|
|
|
self.channel_set = set() |
115
|
|
|
self.refresh_parameters() |
116
|
|
|
return |
117
|
|
|
|
118
|
|
|
def refresh_parameters(self): |
119
|
|
|
""" Initialize the parameters which describe this Pulse_Block object. |
120
|
|
|
|
121
|
|
|
The information is gained from all the Pulse_Block_Element objects, |
122
|
|
|
which are attached in the element_list. |
123
|
|
|
""" |
124
|
|
|
# the Pulse_Block parameters |
125
|
|
|
self.init_length_s = 0.0 |
126
|
|
|
self.increment_s = 0.0 |
127
|
|
|
self.channel_set = set() |
128
|
|
|
|
129
|
|
|
for elem in self.element_list: |
130
|
|
|
self.init_length_s += elem.init_length_s |
131
|
|
|
self.increment_s += elem.increment_s |
132
|
|
|
|
133
|
|
|
if not self.channel_set: |
134
|
|
|
self.channel_set = elem.channel_set |
135
|
|
|
elif self.channel_set != elem.channel_set: |
136
|
|
|
raise ValueError('Usage of different sets of analog and digital channels in the ' |
137
|
|
|
'same PulseBlock is prohibited.\nPulseBlock creation failed!\n' |
138
|
|
|
'Used channel sets are:\n{0}\n{1}'.format(self.channel_set, |
139
|
|
|
elem.channel_set)) |
140
|
|
|
break |
141
|
|
|
self.analog_channels = {chnl for chnl in self.channel_set if chnl.startswith('a')} |
142
|
|
|
self.digital_channels = {chnl for chnl in self.channel_set if chnl.startswith('d')} |
143
|
|
|
return |
144
|
|
|
|
145
|
|
View Code Duplication |
def replace_element(self, position, element): |
|
|
|
|
146
|
|
|
if not isinstance(element, PulseBlockElement) or len(self.element_list) <= position: |
147
|
|
|
return -1 |
148
|
|
|
|
149
|
|
|
if element.channel_set != self.channel_set: |
150
|
|
|
raise ValueError('Usage of different sets of analog and digital channels in the ' |
151
|
|
|
'same PulseBlock is prohibited. Used channel sets are:\n{0}\n{1}' |
152
|
|
|
''.format(self.channel_set, element.channel_set)) |
153
|
|
|
return -1 |
154
|
|
|
|
155
|
|
|
self.init_length_s -= self.element_list[position].init_length_s |
156
|
|
|
self.increment_s -= self.element_list[position].increment_s |
157
|
|
|
self.init_length_s += element.init_length_s |
158
|
|
|
self.increment_s += element.increment_s |
159
|
|
|
self.element_list[position] = copy.deepcopy(element) |
160
|
|
|
return 0 |
161
|
|
|
|
162
|
|
View Code Duplication |
def delete_element(self, position): |
|
|
|
|
163
|
|
|
if len(self.element_list) <= position: |
164
|
|
|
return -1 |
165
|
|
|
|
166
|
|
|
self.init_length_s -= self.element_list[position].init_length_s |
167
|
|
|
self.increment_s -= self.element_list[position].increment_s |
168
|
|
|
del self.element_list[position] |
169
|
|
|
if len(self.element_list) == 0: |
170
|
|
|
self.init_length_s = 0.0 |
171
|
|
|
self.increment_s = 0.0 |
172
|
|
|
return 0 |
173
|
|
|
|
174
|
|
|
def insert_element(self, position, element): |
175
|
|
|
""" Insert a PulseBlockElement at the given position. The old elements at this position and |
176
|
|
|
all consecutive elements after that will be shifted to higher indices. |
177
|
|
|
|
178
|
|
|
@param int position: position in the element list |
179
|
|
|
@param PulseBlockElement element: PulseBlockElement instance |
180
|
|
|
""" |
181
|
|
|
if not isinstance(element, PulseBlockElement) or len(self.element_list) < position: |
182
|
|
|
return -1 |
183
|
|
|
|
184
|
|
|
if not self.channel_set: |
185
|
|
|
self.channel_set = element.channel_set |
186
|
|
|
self.analog_channels = {chnl for chnl in self.channel_set if chnl.startswith('a')} |
187
|
|
|
self.digital_channels = {chnl for chnl in self.channel_set if chnl.startswith('d')} |
188
|
|
|
elif element.channel_set != self.channel_set: |
189
|
|
|
raise ValueError('Usage of different sets of analog and digital channels in the ' |
190
|
|
|
'same PulseBlock is prohibited. Used channel sets are:\n{0}\n{1}' |
191
|
|
|
''.format(self.channel_set, element.channel_set)) |
192
|
|
|
return -1 |
193
|
|
|
|
194
|
|
|
self.init_length_s += element.init_length_s |
195
|
|
|
self.increment_s += element.increment_s |
196
|
|
|
|
197
|
|
|
self.element_list.insert(position, copy.deepcopy(element)) |
198
|
|
|
return 0 |
199
|
|
|
|
200
|
|
|
def append_element(self, element, at_beginning=False): |
201
|
|
|
""" |
202
|
|
|
""" |
203
|
|
|
position = 0 if at_beginning else len(self.element_list) |
204
|
|
|
return self.insert_element(position=position, element=element) |
205
|
|
|
|
206
|
|
|
def get_dict_representation(self): |
207
|
|
|
dict_repr = dict() |
208
|
|
|
dict_repr['name'] = self.name |
209
|
|
|
dict_repr['element_list'] = list() |
210
|
|
|
for element in self.element_list: |
211
|
|
|
dict_repr['element_list'].append(element.get_dict_representation()) |
212
|
|
|
return dict_repr |
213
|
|
|
|
214
|
|
|
@staticmethod |
215
|
|
|
def block_from_dict(block_dict): |
216
|
|
|
for ii, element_dict in enumerate(block_dict['element_list']): |
217
|
|
|
block_dict['element_list'][ii] = PulseBlockElement.element_from_dict(element_dict) |
218
|
|
|
return PulseBlock(**block_dict) |
219
|
|
|
|
220
|
|
|
|
221
|
|
|
class PulseBlockEnsemble(object): |
222
|
|
|
""" |
223
|
|
|
Represents a collection of PulseBlock objects which is called a PulseBlockEnsemble. |
224
|
|
|
|
225
|
|
|
This object is used as a construction plan to create one sampled file. |
226
|
|
|
""" |
227
|
|
|
def __init__(self, name, block_list=None, rotating_frame=True): |
228
|
|
|
""" |
229
|
|
|
The constructor for a Pulse_Block_Ensemble needs to have: |
230
|
|
|
|
231
|
|
|
@param str name: chosen name for the PulseBlockEnsemble |
232
|
|
|
@param list block_list: contains the PulseBlock names with their number of repetitions, |
233
|
|
|
e.g. [(name, repetitions), (name, repetitions), ...]) |
234
|
|
|
@param bool rotating_frame: indicates whether the phase should be preserved for all the |
235
|
|
|
functions. |
236
|
|
|
""" |
237
|
|
|
# FIXME: Sanity checking needed here |
238
|
|
|
self.name = name |
239
|
|
|
self.rotating_frame = rotating_frame |
240
|
|
|
if isinstance(block_list, list): |
241
|
|
|
self.block_list = block_list |
242
|
|
|
else: |
243
|
|
|
self.block_list = list() |
244
|
|
|
|
245
|
|
|
# Dictionary container to store information related to the actually sampled |
246
|
|
|
# Waveform like pulser settings used during sampling (sample_rate, activation_config etc.) |
247
|
|
|
# and additional information about the discretization of the waveform (timebin positions of |
248
|
|
|
# the PulseBlockElement transitions etc.) as well as the names of the created waveforms. |
249
|
|
|
# This container will be populated during sampling and will be emptied upon deletion of the |
250
|
|
|
# corresponding waveforms from the pulse generator |
251
|
|
|
self.sampling_information = dict() |
252
|
|
|
# Dictionary container to store additional information about for measurement settings |
253
|
|
|
# (ignore_lasers, controlled_variable, alternating etc.). |
254
|
|
|
# This container needs to be populated by the script creating the PulseBlockEnsemble |
255
|
|
|
# before saving it. (e.g. in generate methods in PulsedObjectGenerator class) |
256
|
|
|
self.measurement_information = dict() |
257
|
|
|
return |
258
|
|
|
|
259
|
|
|
def replace_block(self, position, block_name, reps=None): |
260
|
|
|
""" |
261
|
|
|
""" |
262
|
|
|
if not isinstance(block_name, str) or len(self.block_list) <= position: |
263
|
|
|
return -1 |
264
|
|
|
|
265
|
|
|
if reps is None: |
266
|
|
|
list_entry = (block_name, self.block_list[position][1]) |
267
|
|
|
self.block_list[position][0] = list_entry |
268
|
|
|
elif reps >= 0: |
269
|
|
|
self.block_list[position] = (block_name, reps) |
270
|
|
|
else: |
271
|
|
|
return -1 |
272
|
|
|
return 0 |
273
|
|
|
|
274
|
|
|
def delete_block(self, position): |
275
|
|
|
""" |
276
|
|
|
""" |
277
|
|
|
if len(self.block_list) <= position: |
278
|
|
|
return -1 |
279
|
|
|
|
280
|
|
|
del self.block_list[position] |
281
|
|
|
return 0 |
282
|
|
|
|
283
|
|
|
def insert_block(self, position, block_name, reps=0): |
284
|
|
|
""" Insert a PulseBlock at the given position. The old block at this position and all |
285
|
|
|
consecutive blocks after that will be shifted to higher indices. |
286
|
|
|
|
287
|
|
|
@param int position: position in the block list |
288
|
|
|
@param str block_name: PulseBlock name |
289
|
|
|
@param int reps: Block repetitions. Zero means single playback of the block. |
290
|
|
|
""" |
291
|
|
|
if not isinstance(block_name, str) or len(self.block_list) < position or reps < 0: |
292
|
|
|
return -1 |
293
|
|
|
|
294
|
|
|
self.block_list.insert(position, (block_name, reps)) |
295
|
|
|
return 0 |
296
|
|
|
|
297
|
|
|
def append_block(self, block_name, reps=0, at_beginning=False): |
298
|
|
|
""" Append either at the front or at the back. |
299
|
|
|
|
300
|
|
|
@param str block_name: PulseBlock name |
301
|
|
|
@param int reps: Block repetitions. Zero means single playback of the block. |
302
|
|
|
@param bool at_beginning: If False append to end (default), if True insert at beginning. |
303
|
|
|
""" |
304
|
|
|
position = 0 if at_beginning else len(self.block_list) |
305
|
|
|
return self.insert_block(position=position, block_name=block_name, reps=reps) |
306
|
|
|
|
307
|
|
|
def get_dict_representation(self): |
308
|
|
|
dict_repr = dict() |
309
|
|
|
dict_repr['name'] = self.name |
310
|
|
|
dict_repr['rotating_frame'] = self.rotating_frame |
311
|
|
|
dict_repr['block_list'] = self.block_list |
312
|
|
|
dict_repr['sampling_information'] = self.sampling_information |
313
|
|
|
dict_repr['measurement_information'] = self.measurement_information |
314
|
|
|
return dict_repr |
315
|
|
|
|
316
|
|
|
@staticmethod |
317
|
|
|
def ensemble_from_dict(ensemble_dict): |
318
|
|
|
new_ens = PulseBlockEnsemble(name=ensemble_dict['name'], |
319
|
|
|
block_list=ensemble_dict['block_list'], |
320
|
|
|
rotating_frame=ensemble_dict['rotating_frame']) |
321
|
|
|
new_ens.sampling_information = ensemble_dict['sampling_information'] |
322
|
|
|
new_ens.measurement_information = ensemble_dict['measurement_information'] |
323
|
|
|
return new_ens |
324
|
|
|
|
325
|
|
|
|
326
|
|
|
class PulseSequence(object): |
327
|
|
|
""" |
328
|
|
|
Higher order object for sequence capability. |
329
|
|
|
|
330
|
|
|
Represents a playback procedure for a number of PulseBlockEnsembles. Unused for pulse |
331
|
|
|
generator hardware without sequencing functionality. |
332
|
|
|
""" |
333
|
|
|
__default_seq_params = {'repetitions': 1, |
334
|
|
|
'go_to': -1, |
335
|
|
|
'event_jump_to': -1, |
336
|
|
|
'event_trigger': 'OFF', |
337
|
|
|
'wait_for': 'OFF', |
338
|
|
|
'flag_trigger': 'OFF', |
339
|
|
|
'flag_high': 'OFF'} |
340
|
|
|
|
341
|
|
|
def __init__(self, name, ensemble_list=None, rotating_frame=False): |
342
|
|
|
""" |
343
|
|
|
The constructor for a PulseSequence objects needs to have: |
344
|
|
|
|
345
|
|
|
@param str name: the actual name of the sequence |
346
|
|
|
@param list ensemble_list: list containing a tuple of two entries: |
347
|
|
|
[(PulseBlockEnsemble name, seq_param), |
348
|
|
|
(PulseBlockEnsemble name, seq_param), ...] |
349
|
|
|
The seq_param is a dictionary, where the various sequence |
350
|
|
|
parameters are saved with their keywords and the |
351
|
|
|
according parameter (as item). |
352
|
|
|
Available parameters are: |
353
|
|
|
'repetitions': The number of repetitions for that sequence |
354
|
|
|
step. (Default 0) |
355
|
|
|
0 meaning the step is played once. |
356
|
|
|
Set to -1 for infinite looping. |
357
|
|
|
'go_to': The sequence step index to jump to after |
358
|
|
|
having played all repetitions. (Default -1) |
359
|
|
|
Indices starting at 1 for first step. |
360
|
|
|
Set to 0 or -1 to follow up with the next step. |
361
|
|
|
'event_jump_to': The sequence step to jump to |
362
|
|
|
(starting from 1) in case of a trigger |
363
|
|
|
event (see event_trigger). |
364
|
|
|
Setting it to 0 or -1 means jump to next |
365
|
|
|
step. Ignored if event_trigger is 'OFF'. |
366
|
|
|
'event_trigger': The trigger input to listen to in order |
367
|
|
|
to perform sequence jumps. Set to 'OFF' |
368
|
|
|
(default) in order to ignore triggering. |
369
|
|
|
'wait_for': The trigger input to wait for before playing |
370
|
|
|
this sequence step. Set to 'OFF' (default) |
371
|
|
|
in order to play the current step immediately. |
372
|
|
|
'flag_trigger': The flag to trigger when this sequence |
373
|
|
|
step starts playing. Select 'OFF' |
374
|
|
|
(default) for no flag trigger. |
375
|
|
|
'flag_high': The flag to set to high while this step is |
376
|
|
|
playing. Select 'OFF' (default) to set all |
377
|
|
|
flags to low. |
378
|
|
|
|
379
|
|
|
If only 'repetitions' are in the dictionary, then the dict |
380
|
|
|
will look like: |
381
|
|
|
seq_param = {'repetitions': 41} |
382
|
|
|
and so the respective sequence step will play 42 times. |
383
|
|
|
@param bool rotating_frame: indicates, whether the phase has to be preserved in all |
384
|
|
|
analog signals ACROSS different waveforms |
385
|
|
|
""" |
386
|
|
|
self.name = name |
387
|
|
|
self.rotating_frame = rotating_frame |
388
|
|
|
self.ensemble_list = list() if ensemble_list is None else ensemble_list |
389
|
|
|
self.is_finite = True |
390
|
|
|
self.refresh_parameters() |
391
|
|
|
|
392
|
|
|
# self.sampled_ensembles = OrderedDict() |
393
|
|
|
# Dictionary container to store information related to the actually sampled |
394
|
|
|
# Waveforms like pulser settings used during sampling (sample_rate, activation_config etc.) |
395
|
|
|
# and additional information about the discretization of the waveform (timebin positions of |
396
|
|
|
# the PulseBlockElement transitions etc.) |
397
|
|
|
# This container is not necessary for the sampling process but serves only the purpose of |
398
|
|
|
# holding optional information for different modules. |
399
|
|
|
self.sampling_information = dict() |
400
|
|
|
# Dictionary container to store additional information about for measurement settings |
401
|
|
|
# (ignore_lasers, controlled_values, alternating etc.). |
402
|
|
|
# This container needs to be populated by the script creating the PulseSequence |
403
|
|
|
# before saving it. |
404
|
|
|
self.measurement_information = dict() |
405
|
|
|
return |
406
|
|
|
|
407
|
|
|
def refresh_parameters(self): |
408
|
|
|
self.is_finite = True |
409
|
|
|
for ensemble_name, params in self.ensemble_list: |
410
|
|
|
if params['repetitions'] < 0: |
411
|
|
|
self.is_finite = False |
412
|
|
|
break |
413
|
|
|
return |
414
|
|
|
|
415
|
|
|
def replace_ensemble(self, position, ensemble_name, seq_param=None): |
416
|
|
|
""" Replace a sequence step at a given position. |
417
|
|
|
|
418
|
|
|
@param int position: position in the ensemble list |
419
|
|
|
@param str ensemble_name: PulseBlockEnsemble name |
420
|
|
|
@param dict seq_param: Sequence step parameter dictionary. Use present one if None. |
421
|
|
|
""" |
422
|
|
|
if not isinstance(ensemble_name, str) or len(self.ensemble_list) <= position: |
423
|
|
|
return -1 |
424
|
|
|
|
425
|
|
|
if seq_param is None: |
426
|
|
|
list_entry = (ensemble_name, self.ensemble_list[position][1]) |
427
|
|
|
self.ensemble_list[position] = list_entry |
428
|
|
|
else: |
429
|
|
|
self.ensemble_list[position] = (ensemble_name, seq_param.copy()) |
430
|
|
|
if seq_param['repetitions'] < 0 and self.is_finite: |
431
|
|
|
self.is_finite = False |
432
|
|
|
elif seq_param['repetitions'] >= 0 and not self.is_finite: |
433
|
|
|
self.refresh_parameters() |
434
|
|
|
return 0 |
435
|
|
|
|
436
|
|
|
def delete_ensemble(self, position): |
437
|
|
|
""" Delete an ensemble at a given position |
438
|
|
|
|
439
|
|
|
@param int position: position within the list self.ensemble_list. |
440
|
|
|
""" |
441
|
|
|
if len(self.ensemble_list) <= position: |
442
|
|
|
return -1 |
443
|
|
|
|
444
|
|
|
refresh = True if self.ensemble_list[position][1]['repetitions'] < 0 else False |
445
|
|
|
|
446
|
|
|
del self.ensemble_list[position] |
447
|
|
|
|
448
|
|
|
if refresh: |
449
|
|
|
self.refresh_parameters() |
450
|
|
|
return 0 |
451
|
|
|
|
452
|
|
|
def insert_ensemble(self, position, ensemble_name, seq_param=None): |
453
|
|
|
""" Insert a sequence step at the given position. The old step at this position and all |
454
|
|
|
consecutive steps after that will be shifted to higher indices. |
455
|
|
|
|
456
|
|
|
@param int position: position in the ensemble list |
457
|
|
|
@param str ensemble_name: PulseBlockEnsemble name |
458
|
|
|
@param dict seq_param: Sequence step parameter dictionary. |
459
|
|
|
""" |
460
|
|
|
if not isinstance(ensemble_name, str) or len(self.ensemble_list) < position: |
461
|
|
|
return -1 |
462
|
|
|
|
463
|
|
|
if seq_param is None: |
464
|
|
|
seq_param = self.__default_seq_params |
465
|
|
|
|
466
|
|
|
self.ensemble_list.insert(position, (ensemble_name, seq_param.copy())) |
467
|
|
|
|
468
|
|
|
if seq_param['repetitions'] < 0: |
469
|
|
|
self.is_finite = False |
470
|
|
|
return 0 |
471
|
|
|
|
472
|
|
|
def append_ensemble(self, ensemble_name, seq_param=None, at_beginning=False): |
473
|
|
|
""" Append either at the front or at the back. |
474
|
|
|
|
475
|
|
|
@param str ensemble_name: PulseBlockEnsemble name |
476
|
|
|
@param dict seq_param: Sequence step parameter dictionary. |
477
|
|
|
@param bool at_beginning: If False append to end (default), if True insert at beginning. |
478
|
|
|
""" |
479
|
|
|
position = 0 if at_beginning else len(self.ensemble_list) |
480
|
|
|
return self.insert_ensemble(position=position, |
481
|
|
|
ensemble_name=ensemble_name, |
482
|
|
|
seq_param=seq_param) |
483
|
|
|
|
484
|
|
|
def get_dict_representation(self): |
485
|
|
|
dict_repr = dict() |
486
|
|
|
dict_repr['name'] = self.name |
487
|
|
|
dict_repr['rotating_frame'] = self.rotating_frame |
488
|
|
|
dict_repr['ensemble_list'] = self.ensemble_list |
489
|
|
|
dict_repr['sampling_information'] = self.sampling_information |
490
|
|
|
dict_repr['measurement_information'] = self.measurement_information |
491
|
|
|
return dict_repr |
492
|
|
|
|
493
|
|
|
@staticmethod |
494
|
|
|
def sequence_from_dict(sequence_dict): |
495
|
|
|
new_seq = PulseSequence(name=sequence_dict['name'], |
496
|
|
|
ensemble_list=sequence_dict['ensemble_list'], |
497
|
|
|
rotating_frame=sequence_dict['rotating_frame']) |
498
|
|
|
new_seq.sampling_information = sequence_dict['sampling_information'] |
499
|
|
|
new_seq.measurement_information = sequence_dict['measurement_information'] |
500
|
|
|
return new_seq |
501
|
|
|
|
502
|
|
|
|
503
|
|
|
class PredefinedGeneratorBase: |
504
|
|
|
""" |
505
|
|
|
Base class for PulseObjectGenerator and predefined generator classes containing the actual |
506
|
|
|
"generate_"-methods. |
507
|
|
|
|
508
|
|
|
This class holds a protected reference to the SequenceGeneratorLogic and provides read-only |
509
|
|
|
access via properties to various attributes of the logic module. |
510
|
|
|
SequenceGeneratorLogic logger is also accessible via this base class and can be used as in any |
511
|
|
|
qudi module (e.g. self.log.error(...)). |
512
|
|
|
Also provides helper methods to simplify sequence/ensemble generation. |
513
|
|
|
""" |
514
|
|
|
def __init__(self, sequencegeneratorlogic): |
515
|
|
|
# Keep protected reference to the SequenceGeneratorLogic |
516
|
|
|
self.__sequencegeneratorlogic = sequencegeneratorlogic |
517
|
|
|
|
518
|
|
|
@property |
519
|
|
|
def log(self): |
520
|
|
|
return self.__sequencegeneratorlogic.log |
521
|
|
|
|
522
|
|
|
@property |
523
|
|
|
def pulse_generator_settings(self): |
524
|
|
|
return self.__sequencegeneratorlogic.pulse_generator_settings |
525
|
|
|
|
526
|
|
|
@property |
527
|
|
|
def generation_parameters(self): |
528
|
|
|
return self.__sequencegeneratorlogic.generation_parameters |
529
|
|
|
|
530
|
|
|
@property |
531
|
|
|
def channel_set(self): |
532
|
|
|
channels = self.pulse_generator_settings.get('activation_config') |
533
|
|
|
if channels is None: |
534
|
|
|
channels = ('', set()) |
535
|
|
|
return channels[1] |
536
|
|
|
|
537
|
|
|
@property |
538
|
|
|
def analog_channels(self): |
539
|
|
|
return {chnl for chnl in self.channel_set if chnl.startswith('a')} |
540
|
|
|
|
541
|
|
|
@property |
542
|
|
|
def digital_channels(self): |
543
|
|
|
return {chnl for chnl in self.channel_set if chnl.startswith('d')} |
544
|
|
|
|
545
|
|
|
@property |
546
|
|
|
def laser_channel(self): |
547
|
|
|
return self.generation_parameters.get('laser_channel') |
548
|
|
|
|
549
|
|
|
@property |
550
|
|
|
def sync_channel(self): |
551
|
|
|
channel = self.generation_parameters.get('sync_channel') |
552
|
|
|
return None if channel == '' else channel |
553
|
|
|
|
554
|
|
|
@property |
555
|
|
|
def gate_channel(self): |
556
|
|
|
channel = self.generation_parameters.get('gate_channel') |
557
|
|
|
return None if channel == '' else channel |
558
|
|
|
|
559
|
|
|
@property |
560
|
|
|
def analog_trigger_voltage(self): |
561
|
|
|
return self.generation_parameters.get('analog_trigger_voltage') |
562
|
|
|
|
563
|
|
|
@property |
564
|
|
|
def laser_delay(self): |
565
|
|
|
return self.generation_parameters.get('laser_delay') |
566
|
|
|
|
567
|
|
|
@property |
568
|
|
|
def microwave_channel(self): |
569
|
|
|
channel = self.generation_parameters.get('microwave_channel') |
570
|
|
|
return None if channel == '' else channel |
571
|
|
|
|
572
|
|
|
@property |
573
|
|
|
def microwave_frequency(self): |
574
|
|
|
return self.generation_parameters.get('microwave_frequency') |
575
|
|
|
|
576
|
|
|
@property |
577
|
|
|
def microwave_amplitude(self): |
578
|
|
|
return self.generation_parameters.get('microwave_amplitude') |
579
|
|
|
|
580
|
|
|
@property |
581
|
|
|
def laser_length(self): |
582
|
|
|
return self.generation_parameters.get('laser_length') |
583
|
|
|
|
584
|
|
|
@property |
585
|
|
|
def wait_time(self): |
586
|
|
|
return self.generation_parameters.get('wait_time') |
587
|
|
|
|
588
|
|
|
@property |
589
|
|
|
def rabi_period(self): |
590
|
|
|
return self.generation_parameters.get('rabi_period') |
591
|
|
|
|
592
|
|
|
################################################################################################ |
593
|
|
|
# Helper methods #### |
594
|
|
|
################################################################################################ |
595
|
|
|
def _get_idle_element(self, length, increment): |
596
|
|
|
""" |
597
|
|
|
Creates an idle pulse PulseBlockElement |
598
|
|
|
|
599
|
|
|
@param float length: idle duration in seconds |
600
|
|
|
@param float increment: idle duration increment in seconds |
601
|
|
|
|
602
|
|
|
@return: PulseBlockElement, the generated idle element |
603
|
|
|
""" |
604
|
|
|
# Create idle element |
605
|
|
|
return PulseBlockElement(init_length_s=length, |
606
|
|
|
increment_s=increment, |
607
|
|
|
pulse_function={chnl: sf.Idle() for chnl in self.analog_channels}, |
608
|
|
|
digital_high={chnl: False for chnl in self.digital_channels}) |
609
|
|
|
|
610
|
|
|
def _get_trigger_element(self, length, increment, channels): |
611
|
|
|
""" |
612
|
|
|
Creates a trigger PulseBlockElement |
613
|
|
|
|
614
|
|
|
@param float length: trigger duration in seconds |
615
|
|
|
@param float increment: trigger duration increment in seconds |
616
|
|
|
@param str|list channels: The pulser channel(s) to be triggered. |
617
|
|
|
|
618
|
|
|
@return: PulseBlockElement, the generated trigger element |
619
|
|
|
""" |
620
|
|
|
if isinstance(channels, str): |
621
|
|
|
channels = [channels] |
622
|
|
|
|
623
|
|
|
# input params for element generation |
624
|
|
|
pulse_function = {chnl: sf.Idle() for chnl in self.analog_channels} |
625
|
|
|
digital_high = {chnl: False for chnl in self.digital_channels} |
626
|
|
|
|
627
|
|
|
# Determine analogue or digital trigger channel and set channels accordingly. |
628
|
|
|
for channel in channels: |
629
|
|
|
if channel.startswith('d'): |
630
|
|
|
digital_high[channel] = True |
631
|
|
|
else: |
632
|
|
|
pulse_function[channel] = sf.DC(voltage=self.analog_trigger_voltage) |
633
|
|
|
|
634
|
|
|
# return trigger element |
635
|
|
|
return PulseBlockElement(init_length_s=length, |
636
|
|
|
increment_s=increment, |
637
|
|
|
pulse_function=pulse_function, |
638
|
|
|
digital_high=digital_high) |
639
|
|
|
|
640
|
|
|
def _get_laser_element(self, length, increment): |
641
|
|
|
""" |
642
|
|
|
Creates laser trigger PulseBlockElement |
643
|
|
|
|
644
|
|
|
@param float length: laser pulse duration in seconds |
645
|
|
|
@param float increment: laser pulse duration increment in seconds |
646
|
|
|
|
647
|
|
|
@return: PulseBlockElement, two elements for laser and gate trigger (delay element) |
648
|
|
|
""" |
649
|
|
|
return self._get_trigger_element(length=length, |
650
|
|
|
increment=increment, |
651
|
|
|
channels=self.laser_channel) |
652
|
|
|
|
653
|
|
|
def _get_laser_gate_element(self, length, increment): |
654
|
|
|
""" |
655
|
|
|
""" |
656
|
|
|
laser_gate_element = self._get_laser_element(length=length, |
657
|
|
|
increment=increment) |
658
|
|
|
if self.gate_channel: |
659
|
|
|
if self.gate_channel.startswith('d'): |
660
|
|
|
laser_gate_element.digital_high[self.gate_channel] = True |
661
|
|
|
else: |
662
|
|
|
laser_gate_element.pulse_function[self.gate_channel] = sf.DC( |
663
|
|
|
voltage=self.analog_trigger_voltage) |
664
|
|
|
return laser_gate_element |
665
|
|
|
|
666
|
|
|
def _get_delay_element(self): |
667
|
|
|
""" |
668
|
|
|
Creates an idle element of length of the laser delay |
669
|
|
|
|
670
|
|
|
@return PulseBlockElement: The delay element |
671
|
|
|
""" |
672
|
|
|
return self._get_idle_element(length=self.laser_delay, |
673
|
|
|
increment=0) |
674
|
|
|
|
675
|
|
|
def _get_delay_gate_element(self): |
676
|
|
|
""" |
677
|
|
|
Creates a gate trigger of length of the laser delay. |
678
|
|
|
If no gate channel is specified will return a simple idle element. |
679
|
|
|
|
680
|
|
|
@return PulseBlockElement: The delay element |
681
|
|
|
""" |
682
|
|
|
if self.gate_channel: |
683
|
|
|
return self._get_trigger_element(length=self.laser_delay, |
684
|
|
|
increment=0, |
685
|
|
|
channels=self.gate_channel) |
686
|
|
|
else: |
687
|
|
|
return self._get_delay_element() |
688
|
|
|
|
689
|
|
|
def _get_sync_element(self): |
690
|
|
|
""" |
691
|
|
|
|
692
|
|
|
""" |
693
|
|
|
return self._get_trigger_element(length=50e-9, |
694
|
|
|
increment=0, |
695
|
|
|
channels=self.sync_channel) |
696
|
|
|
|
697
|
|
|
def _get_mw_element(self, length, increment, amp=None, freq=None, phase=None): |
698
|
|
|
""" |
699
|
|
|
Creates a MW pulse PulseBlockElement |
700
|
|
|
|
701
|
|
|
@param float length: MW pulse duration in seconds |
702
|
|
|
@param float increment: MW pulse duration increment in seconds |
703
|
|
|
@param float freq: MW frequency in case of analogue MW channel in Hz |
704
|
|
|
@param float amp: MW amplitude in case of analogue MW channel in V |
705
|
|
|
@param float phase: MW phase in case of analogue MW channel in deg |
706
|
|
|
|
707
|
|
|
@return: PulseBlockElement, the generated MW element |
708
|
|
|
""" |
709
|
|
|
if self.microwave_channel.startswith('d'): |
710
|
|
|
mw_element = self._get_trigger_element( |
711
|
|
|
length=length, |
712
|
|
|
increment=increment, |
713
|
|
|
channels=self.microwave_channel) |
714
|
|
|
else: |
715
|
|
|
mw_element = self._get_idle_element( |
716
|
|
|
length=length, |
717
|
|
|
increment=increment) |
718
|
|
|
mw_element.pulse_function[self.microwave_channel] = sf.Sin(amplitude=amp, |
719
|
|
|
frequency=freq, |
720
|
|
|
phase=phase) |
721
|
|
|
return mw_element |
722
|
|
|
|
723
|
|
|
def _get_multiple_mw_element(self, length, increment, amps=None, freqs=None, phases=None): |
724
|
|
|
""" |
725
|
|
|
Creates single, double or triple sine mw element. |
726
|
|
|
|
727
|
|
|
@param float length: MW pulse duration in seconds |
728
|
|
|
@param float increment: MW pulse duration increment in seconds |
729
|
|
|
@param amps: list containing the amplitudes |
730
|
|
|
@param freqs: list containing the frequencies |
731
|
|
|
@param phases: list containing the phases |
732
|
|
|
@return: PulseBlockElement, the generated MW element |
733
|
|
|
""" |
734
|
|
|
if isinstance(amps, (int, float)): |
735
|
|
|
amps = [amps] |
736
|
|
|
if isinstance(freqs, (int, float)): |
737
|
|
|
freqs = [freqs] |
738
|
|
|
if isinstance(phases, (int, float)): |
739
|
|
|
phases = [phases] |
740
|
|
|
|
741
|
|
|
if self.microwave_channel.startswith('d'): |
742
|
|
|
mw_element = self._get_trigger_element( |
743
|
|
|
length=length, |
744
|
|
|
increment=increment, |
745
|
|
|
channels=self.microwave_channel) |
746
|
|
|
else: |
747
|
|
|
mw_element = self._get_idle_element( |
748
|
|
|
length=length, |
749
|
|
|
increment=increment) |
750
|
|
|
|
751
|
|
|
sine_number = min(len(amps), len(freqs), len(phases)) |
752
|
|
|
|
753
|
|
|
if sine_number < 2: |
754
|
|
|
mw_element.pulse_function[self.microwave_channel] = sf.Sin(amplitude=amps[0], |
755
|
|
|
frequency=freqs[0], |
756
|
|
|
phase=phases[0]) |
757
|
|
|
elif sine_number == 2: |
758
|
|
|
mw_element.pulse_function[self.microwave_channel] = sf.DoubleSin( |
759
|
|
|
amplitude_1=amps[0], |
760
|
|
|
amplitude_2=amps[1], |
761
|
|
|
frequency_1=freqs[0], |
762
|
|
|
frequency_2=freqs[1], |
763
|
|
|
phase_1=phases[0], |
764
|
|
|
phase_2=phases[1]) |
765
|
|
|
else: |
766
|
|
|
mw_element.pulse_function[self.microwave_channel] = sf.TripleSin( |
767
|
|
|
amplitude_1=amps[0], |
768
|
|
|
amplitude_2=amps[1], |
769
|
|
|
amplitude_3=amps[2], |
770
|
|
|
frequency_1=freqs[0], |
771
|
|
|
frequency_2=freqs[1], |
772
|
|
|
frequency_3=freqs[2], |
773
|
|
|
phase_1=phases[0], |
774
|
|
|
phase_2=phases[1], |
775
|
|
|
phase_3=phases[2]) |
776
|
|
|
return mw_element |
777
|
|
|
|
778
|
|
|
def _get_mw_laser_element(self, length, increment, amp=None, freq=None, phase=None): |
779
|
|
|
""" |
780
|
|
|
|
781
|
|
|
@param length: |
782
|
|
|
@param increment: |
783
|
|
|
@param amp: |
784
|
|
|
@param freq: |
785
|
|
|
@param phase: |
786
|
|
|
@return: |
787
|
|
|
""" |
788
|
|
|
mw_laser_element = self._get_mw_element(length=length, |
789
|
|
|
increment=increment, |
790
|
|
|
amp=amp, |
791
|
|
|
freq=freq, |
792
|
|
|
phase=phase) |
793
|
|
|
if self.laser_channel.startswith('d'): |
794
|
|
|
mw_laser_element.digital_high[self.laser_channel] = True |
795
|
|
|
else: |
796
|
|
|
mw_laser_element.pulse_function[self.laser_channel] = sf.DC( |
797
|
|
|
voltage=self.analog_trigger_voltage) |
798
|
|
|
return mw_laser_element |
799
|
|
|
|
800
|
|
|
def _get_ensemble_count_length(self, ensemble, created_blocks): |
801
|
|
|
""" |
802
|
|
|
|
803
|
|
|
@param ensemble: |
804
|
|
|
@param created_blocks: |
805
|
|
|
@return: |
806
|
|
|
""" |
807
|
|
|
if self.gate_channel: |
808
|
|
|
length = self.laser_length + self.laser_delay |
809
|
|
|
else: |
810
|
|
|
blocks = {block.name: block for block in created_blocks} |
811
|
|
|
length = 0.0 |
812
|
|
|
for block_name, reps in ensemble.block_list: |
813
|
|
|
length += blocks[block_name].init_length_s * (reps + 1) |
814
|
|
|
length += blocks[block_name].increment_s * ((reps ** 2 + reps) / 2) |
815
|
|
|
return length |
816
|
|
|
|
817
|
|
|
|
818
|
|
|
class PulseObjectGenerator(PredefinedGeneratorBase): |
819
|
|
|
""" |
820
|
|
|
|
821
|
|
|
""" |
822
|
|
|
def __init__(self, sequencegeneratorlogic): |
823
|
|
|
# Initialize base class |
824
|
|
|
super().__init__(sequencegeneratorlogic) |
825
|
|
|
|
826
|
|
|
# dictionary containing references to all generation methods imported from generator class |
827
|
|
|
# modules. The keys are the method names excluding the prefix "generate_". |
828
|
|
|
self._generate_methods = dict() |
829
|
|
|
# nested dictionary with keys being the generation method names and values being a |
830
|
|
|
# dictionary containing all keyword arguments as keys with their default value |
831
|
|
|
self._generate_method_parameters = dict() |
832
|
|
|
|
833
|
|
|
# import path for generator modules from default dir (logic.predefined_generate_methods) |
834
|
|
|
path_list = [os.path.join(get_main_dir(), 'logic', 'pulsed', 'predefined_generate_methods')] |
835
|
|
|
# import path for generator modules from non-default directory if a path has been given |
836
|
|
|
if isinstance(sequencegeneratorlogic.additional_methods_dir, str): |
837
|
|
|
path_list.append(sequencegeneratorlogic.additional_methods_dir) |
838
|
|
View Code Duplication |
|
|
|
|
|
839
|
|
|
# Import predefined generator modules and get a list of generator classes |
840
|
|
|
generator_classes = self.__import_external_generators(paths=path_list) |
841
|
|
|
|
842
|
|
|
# create an instance of each class and put them in a temporary list |
843
|
|
|
generator_instances = [cls(sequencegeneratorlogic) for cls in generator_classes] |
844
|
|
|
|
845
|
|
|
# add references to all generate methods in each instance to a dict |
846
|
|
|
self.__populate_method_dict(instance_list=generator_instances) |
847
|
|
|
|
848
|
|
|
# populate parameters dictionary from generate method signatures |
849
|
|
|
self.__populate_parameter_dict() |
850
|
|
|
|
851
|
|
|
@property |
852
|
|
|
def predefined_generate_methods(self): |
853
|
|
|
return self._generate_methods |
854
|
|
|
|
855
|
|
|
@property |
856
|
|
|
def predefined_method_parameters(self): |
857
|
|
|
return self._generate_method_parameters.copy() |
858
|
|
|
|
859
|
|
|
def __import_external_generators(self, paths): |
860
|
|
|
""" |
861
|
|
|
Helper method to import all modules from directories contained in paths. |
862
|
|
|
Find all classes in those modules that inherit exclusively from PredefinedGeneratorBase |
863
|
|
|
class and return a list of them. |
864
|
|
|
|
865
|
|
|
@param iterable paths: iterable containing paths to import modules from |
866
|
|
|
@return list: A list of imported valid generator classes |
867
|
|
|
""" |
868
|
|
|
class_list = list() |
869
|
|
|
for path in paths: |
870
|
|
|
if not os.path.exists(path): |
871
|
|
|
self.log.error('Unable to import generate methods from "{0}".\n' |
872
|
|
|
'Path does not exist.'.format(path)) |
873
|
|
|
continue |
874
|
|
|
# Get all python modules to import from. |
875
|
|
|
# The assumption is that in the path, there are *.py files, |
876
|
|
|
# which contain only generator classes! |
877
|
|
|
module_list = [name[:-3] for name in os.listdir(path) if |
878
|
|
|
os.path.isfile(os.path.join(path, name)) and name.endswith('.py')] |
879
|
|
|
|
880
|
|
|
# append import path to sys.path |
881
|
|
|
sys.path.append(path) |
882
|
|
|
|
883
|
|
|
# Go through all modules and create instances of each class found. |
884
|
|
|
for module_name in module_list: |
885
|
|
|
# import module |
886
|
|
|
mod = importlib.import_module('{0}'.format(module_name)) |
887
|
|
|
importlib.reload(mod) |
888
|
|
|
# get all generator class references defined in the module |
889
|
|
|
tmp_list = [m[1] for m in inspect.getmembers(mod, self.is_generator_class)] |
890
|
|
|
# append to class_list |
891
|
|
|
class_list.extend(tmp_list) |
892
|
|
|
return class_list |
893
|
|
|
|
894
|
|
|
def __populate_method_dict(self, instance_list): |
895
|
|
|
""" |
896
|
|
|
Helper method to populate the dictionaries containing all references to callable generate |
897
|
|
|
methods contained in generator instances passed to this method. |
898
|
|
|
|
899
|
|
|
@param list instance_list: List containing instances of generator classes |
900
|
|
|
""" |
901
|
|
|
self._generate_methods = dict() |
902
|
|
|
for instance in instance_list: |
903
|
|
|
for method_name, method_ref in inspect.getmembers(instance, inspect.ismethod): |
904
|
|
|
if method_name.startswith('generate_'): |
905
|
|
|
self._generate_methods[method_name[9:]] = method_ref |
906
|
|
|
return |
907
|
|
|
|
908
|
|
|
def __populate_parameter_dict(self): |
909
|
|
|
""" |
910
|
|
|
Helper method to populate the dictionary containing all possible keyword arguments from all |
911
|
|
|
generate methods. |
912
|
|
|
""" |
913
|
|
|
self._generate_method_parameters = dict() |
914
|
|
|
for method_name, method in self._generate_methods.items(): |
915
|
|
|
method_signature = inspect.signature(method) |
916
|
|
|
param_dict = dict() |
917
|
|
|
for name, param in method_signature.parameters.items(): |
918
|
|
|
param_dict[name] = None if param.default is param.empty else param.default |
919
|
|
|
|
920
|
|
|
self._generate_method_parameters[method_name] = param_dict |
921
|
|
|
return |
922
|
|
|
|
923
|
|
|
@staticmethod |
924
|
|
|
def is_generator_class(obj): |
925
|
|
|
""" |
926
|
|
|
Helper method to check if an object is a valid generator class. |
927
|
|
|
|
928
|
|
|
@param object obj: object to check |
929
|
|
|
@return bool: True if obj is a valid generator class, False otherwise |
930
|
|
|
""" |
931
|
|
|
if inspect.isclass(obj): |
932
|
|
|
return PredefinedGeneratorBase in obj.__bases__ and len(obj.__bases__) == 1 |
933
|
|
|
return False |
934
|
|
|
|
935
|
|
|
|
936
|
|
|
|
937
|
|
|
|