-
Notifications
You must be signed in to change notification settings - Fork 7
/
index.html
4613 lines (4321 loc) · 205 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<meta name="generator" content="pdoc 0.9.2" />
<title>tektronix_func_gen API documentation</title>
<meta name="description" content="Tektronix arbitrary function generator control …" />
<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/sanitize.min.css" integrity="sha256-PK9q560IAAa6WVRRh76LtCaI8pjTJ2z11v0miyNNjrs=" crossorigin>
<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/typography.min.css" integrity="sha256-7l/o7C8jubJiy74VsKTidCy1yBkRtiUGbVkYBylBqUg=" crossorigin>
<link rel="stylesheet preload" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/github.min.css" crossorigin>
<style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:30px;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:1em 0 .50em 0}h3{font-size:1.4em;margin:25px 0 10px 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .3s ease-in-out}a:hover{color:#e82}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900}pre code{background:#f8f8f8;font-size:.8em;line-height:1.4em}code{background:#f2f2f1;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{background:#f8f8f8;border:0;border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0;padding:1ex}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-weight:bold;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em .5em;margin-bottom:1em}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.item .name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul{padding-left:1.5em}.toc > ul > li{margin-top:.5em}}</style>
<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js" integrity="sha256-Uv3H6lx7dJmRfRvH8TH6kJD1TSK1aFcwgx+mdg3epi8=" crossorigin></script>
<script>window.addEventListener('DOMContentLoaded', () => hljs.initHighlighting())</script>
</head>
<body>
<main>
<article id="content">
<header>
<h1 class="title">Module <code>tektronix_func_gen</code></h1>
</header>
<section id="section-intro">
<h2 id="tektronix-arbitrary-function-generator-control">Tektronix arbitrary function generator control</h2>
<p><a href="https://www.codefactor.io/repository/github/asvela/tektronix-func-gen"><img alt="CodeFactor Grade" src="https://img.shields.io/codefactor/grade/github/asvela/tektronix-func-gen?style=flat-square"></a>
<a href="https://github.com/asvela/dlc-control/blob/main/LICENSE"><img alt="MIT License" src="https://img.shields.io/github/license/asvela/dlc-control?style=flat-square"></a></p>
<p>Provides basic control of AFG1000 and AFG3000 series Tektronix Arbitrary Function
Generators, possibly also others. This includes setting basic settings such as
selecting functions, transferring or selecting custom waveforms, amplitude and offset
control, phase syncronisation and frequency locking.</p>
<p>API documentation available <a href="https://asvela.github.io/tektronix-func-gen/">here</a>,
or in the repository <a href="docs/index.html">docs/index.html</a>. (To build the documentation
yourself use <a href="https://pdoc3.github.io/pdoc/">pdoc3</a> and run
<code>$ python3 pdoc --html -o ./docs/ tektronix_func_gen</code>.)</p>
<p>Tested on Win10 with NI-VISA and PyVISA v1.11 (if using PyVISA <v1.11 use <v0.4
of this module).</p>
<h3 id="known-issues">Known issues</h3>
<ul>
<li><strong>For TekVISA users:</strong> a <code>pyvisa.errors.VI_ERROR_IO</code> is raised unless the
Call Monitor application that comes with TekVISA is open and capturing
(see issue <a href="https://github.com/asvela/tektronix-func-gen/issues/1">#1</a>).
NI-VISA does not have this issue.</li>
<li>The offset of the built-in DC (flat) function cannot be controlled directly. A
workaround is to transfer a flat custom waveform to a memory location,
see <a href="#flat-function-offset-control">Flat function offset control</a> in this readme.</li>
<li>The frequency limits can in practice be stricter than what is set by the module,
as the module is using the limits for a sine, where as other functions, such as
ramp might have lower limit</li>
</ul>
<h3 id="installation">Installation</h3>
<p>Put the module file in the folder wherein the Python file you will import it
from resides.</p>
<p><strong>Dependencies:</strong></p>
<ul>
<li>The package needs VISA to be installed. It is tested with NI-VISA,
<em>TekVISA might not work</em>, see <code>Known issues</code></li>
<li>The Python packages <code>numpy</code> and <code>pyvisa</code> (>=v1.11) are required</li>
</ul>
<h3 id="usage-through-examples">Usage (through examples)</h3>
<p>An example of basic control</p>
<pre><code class="python">import tektronix_func_gen as tfg
with tfg.FuncGen('VISA ADDRESS OF YOUR INSTRUMENT') as fgen:
fgen.ch1.set_function("SIN")
fgen.ch1.set_frequency(25, unit="Hz")
fgen.ch1.set_offset(50, unit="mV")
fgen.ch1.set_amplitude(0.002)
fgen.ch1.set_output("ON")
fgen.ch2.set_output("OFF")
# alternatively fgen.ch1.print_settings() to show from one channel only
fgen.print_settings()
</code></pre>
<p>yields something like (depending on the settings already in use)</p>
<pre><code>Connected to TEKTRONIX model AFG1022, serial XXXXX
Current settings for TEKTRONIX AFG1022 XXXXX
Setting Ch1 Ch2 Unit
==========================
output ON OFF
function SIN RAMP
amplitude 0.002 1 Vpp
offset 0.05 -0.45 V
frequency 25.0 10.0 Hz
</code></pre>
<p>Settings can also be stored and restored:</p>
<pre><code class="python">"""Example showing how to connect, get the current settings of
the instrument, store them, change a setting and then restore the
initial settings"""
import tektronix_func_gen as tfg
with tfg.FuncGen('VISA ADDRESS OF YOUR INSTRUMENT') as fgen:
fgen.print_settings()
print("Saving these settings..")
settings = fgen.get_settings()
print("Change to 1Vpp amplitude for channel 1..")
fgen.ch1.set_amplitude(1)
fgen.print_settings()
print("Reset back to initial settings..")
fgen.set_settings(settings)
fgen.print_settings()
</code></pre>
<h4 id="syncronisation-and-frequency-lock">Syncronisation and frequency lock</h4>
<p>The phase of the two channels can be syncronised with <code>syncronise_waveforms()</code>.
Frequency lock can also be enabled/disabled with <code>set_frequency_lock()</code>:</p>
<pre><code class="python">"""Example showing the frequency being set to 10Hz and then the frequency
lock enabled, using the frequency at ch1 as the common frequency"""
import tektronix_func_gen as tfg
with tfg.FuncGen('VISA ADDRESS OF YOUR INSTRUMENT', verbose=False) as fgen:
fgen.ch1.set_frequency(10)
fgen.set_frequency_lock("ON", use_channel=1)
</code></pre>
<h4 id="arbitrary-waveforms">Arbitrary waveforms</h4>
<p>14 bit vertical resolution arbitrary waveforms can be transferred to the 256
available user defined functions on the function generator.
The length of the waveform must be between 2 and 8192 points.</p>
<pre><code class="python">import numpy as np
import tektronix_func_gen as tfg
with tfg.FuncGen('VISA ADDRESS OF YOUR INSTRUMENT') as fgen:
# create waveform
x = np.linspace(0, 4*np.pi, 8000)
waveform = np.sin(x)+x/5
# transfer the waveform (normalises to the vertical waveform range)
fgen.set_custom_waveform(waveform, memory_num=5, verify=True)
# done, but let's have a look at the waveform catalogue ..
print("New waveform catalogue:")
for i, wav in enumerate(fgen.get_waveform_catalogue()): print(" {}: {}".format(i, wav))
# .. and set the waveform to channel 1
print("Set new wavefrom to channel 1..", end=" ")
fgen.ch1.set_output("OFF")
fgen.ch1.set_function("USER5")
print("ok")
# print current settings
fgen.print_settings()
</code></pre>
<h5 id="flat-function-offset-control">Flat function offset control</h5>
<p>The offset of the built-in DC function cannot be controlled (the offset command
simply does not work, an issue from Tektronix). A workaround is to transfer a
flat custom waveform (two or more points of half the vertical range
(<code>arbitrary_waveform_resolution</code>)) to a memory location:</p>
<pre><code class="python">with tfg.FuncGen('VISA ADDRESS OF YOUR INSTRUMENT') as fgen:
flat_wfm = int(fgen.arbitrary_waveform_resolution/2)*np.ones(2).astype(np.int32)
fgen.set_custom_waveform(flat_wfm, memory_num=255, normalise=False)
fgen.ch1.set_function("USER255")
fgen.ch1.set_offset(2)
</code></pre>
<p>Note the <code>normalise=False</code> argument.</p>
<h4 id="set-voltage-and-frequency-limits">Set voltage and frequency limits</h4>
<p>Limits for amplitude, voltage and frequency for each channel are kept in a
dictionary <code><a title="tektronix_func_gen.FuncGenChannel.channel_limits" href="#tektronix_func_gen.FuncGenChannel.channel_limits">FuncGenChannel.channel_limits</a></code> (these are the standard limits
for AFG1022)</p>
<pre><code class="python">channel_limits = {
"frequency lims": ({"min": 1e-6, "max": 25e6}, "Hz"),
"voltage lims": ({"50ohm": {"min": -5, "max": 5},
"highZ": {"min": -10, "max": 10}}, "V"),
"amplitude lims": ({"50ohm": {"min": 0.001, "max": 10},
"highZ": {"min": 0.002, "max": 20}}, "Vpp")}
</code></pre>
<p>They chan be changed by <code><a title="tektronix_func_gen.FuncGenChannel.set_limit" href="#tektronix_func_gen.FuncGenChannel.set_limit">FuncGenChannel.set_limit()</a></code>, or by using the
<code><a title="tektronix_func_gen.FuncGenChannel.set_stricter_limits" href="#tektronix_func_gen.FuncGenChannel.set_stricter_limits">FuncGenChannel.set_stricter_limits()</a></code> for a series of prompts.</p>
<pre><code class="python">import tektronix_func_gen as tfg
"""Example showing how limits can be read and changed"""
with tfg.FuncGen('VISA ADDRESS OF YOUR INSTRUMENT') as fgen:
lims = fgen.ch1.get_frequency_lims()
print("Channel 1 frequency limits: {}".format(lims))
print("Change the lower limit to 2Hz..")
fgen.ch1.set_limit("frequency lims", "min", 2)
lims = fgen.ch1.get_frequency_lims()
print("Channel 1 frequency limits: {}".format(lims))
print("Try to set ch1 frequency to 1Hz..")
try:
fgen.ch1.set_frequency(1)
except NotSetError as err:
print(err)
</code></pre>
<h4 id="impedance">Impedance</h4>
<p>Unfortunately the impedance (50Ω or high Z) cannot be controlled or read remotely.
Which setting is in use affects the limits of the output voltage. Use the optional
impedance keyword in the initialisation of the <code><a title="tektronix_func_gen.FuncGen" href="#tektronix_func_gen.FuncGen">FuncGen</a></code> object to make the object
aware what limits applies: <code>FuncGen('VISA ADDRESS OF YOUR INSTRUMENT', impedance=("highZ", "50ohm"))</code>.</p>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python"># -*- coding: utf-8 -*-
"""
.. include:: ./README.md
"""
import copy
import pyvisa
import numpy as np
from typing import Tuple, List, Union
_VISA_ADDRESS = "USB0::0x0699::0x0353::1731975::INSTR"
def _SI_prefix_to_factor(unit):
"""Convert an SI prefix to a numerical factor
Parameters
----------
unit : str
The unit whose first character is checked against the list of
prefactors {"M": 1e6, "k": 1e3, "m": 1e-3}
Returns
-------
factor : float or `None`
The appropriate factor or 1 if not found in the list, or `None`
if the unit string is empty
"""
# SI prefix to numerical value
SI_conversion = {"M": 1e6, "k": 1e3, "m": 1e-3}
try: # using the unit's first character as key in the dictionary
factor = SI_conversion[unit[0]]
except KeyError: # if the entry does not exist
factor = 1
except IndexError: # if the unit string is empty
factor = None
return factor
## ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ERROR CLASSES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ##
class NotSetError(Exception):
"""Error for when a value cannot be written to the instrument"""
class NotCompatibleError(Exception):
"""Error for when the instrument is not compatible with this module"""
## ~~~~~~~~~~~~~~~~~~~~~ FUNCTION GENERATOR CLASS ~~~~~~~~~~~~~~~~~~~~~~~~~~ ##
class FuncGen:
"""Class for interacting with Tektronix function generator
Parameters
----------
visa_address : str
VISA address of the insrument
impedance : tuple of {"highZ", "50ohm"}, default ("highZ",)*2
Determines voltage limits associated with high impedance (whether the
instrument is using 50ohm or high Z cannot be controlled through VISA).
For example `("highZ", "50ohm")` for to use high Z for ch1 and
50 ohm for ch2
timeout : int, default 1000
Timeout in milliseconds of instrument connection
verify_param_set : bool, default False
Verify that a value is successfully set after executing a set function
verbose : bool, default `True`
Choose whether to print information such as model upon connecting etc
override_compatibility : str, default `""`
If the instrument limits for the model connected to are not known
`NotCompatibleError` will be raised. To override and use either of
AFG1022, AFG1062, or AFG3022 limits, use their respecive model names as
argument. Note that this might lead to unexpected behaviour for custom
waveforms and 'MIN'/'MAX' keywords.
Attributes
----------
_visa_address : str
The VISA address of the instrument
_id : str
Comma separated string with maker, model, serial and firmware of
the instrument
_inst : `pyvisa.resources.Resource`
The PyVISA resource
_arbitrary_waveform_length : list
The permitted minimum and maximum length of an arbitrary waveform,
e.g. [2, 8192]
_arbitrary_waveform_resolution : int
The vertical resolution of the arbitrary waveform, for instance 14 bit
=> 2**14-1 = 16383
_max_waveform_memory_user_locations : int
The number of the last user memory location available
Raises
------
pyvisa.Error
If the supplied VISA address cannot be connected to
NotCompatibleError
If the instrument limits for the model connected to are not known
(Call the class with `override_compatibility=True` to override and
use AFG1022 limits)
"""
_is_connected = False
"""bool: Keeping track of whether the PYVISA connection has been established"""
instrument_limits = {}
"""dict: Contains the following keys with subdictionaries
- `frequency lims`
Containing the frequency limits for the instrument where the keys
"min" and "max" have values corresponding to minimum and maximum
frequencies in Hz
- `voltage lims`
Contains the maximum absolute voltage the instrument can output
for the keys "50ohm" and "highZ" according to the impedance setting
- `amplitude lims`
Contains the smallest and largest possible amplitudes where the
keys "50ohm" and "highZ" will have subdictionaries with keys
"min" and "max"
"""
def __init__(
self,
visa_address: str,
impedance: Tuple[str, str] = ("highZ",) * 2,
timeout: int = 1000,
verify_param_set: bool = False,
override_compatibility: str = "",
verbose: bool = True,
):
self._override_compat = override_compatibility
self._visa_address = visa_address
self.verify_param_set = verify_param_set
"""bool: Verify that a value is successfully set after executing a set function"""
self.verbose = verbose
"""bool: Choose whether to print information such as model upon connecting etc"""
self.open(visa_address, timeout)
self._initialise_model_properties()
self.channels = (
self._spawn_channel(1, impedance[0]),
self._spawn_channel(2, impedance[1]),
)
"""tuple of `FuncGenChannel`: Objects to control the channels"""
self.ch1 = self.channels[0]
"""`FuncGenChannel`: Short hand for `channels[0]` Object to control channel 1"""
self.ch2 = self.channels[1]
"""`FuncGenChannel`: Short hand for `channels[1]` Object to control channel 2"""
def __enter__(self, **kwargs):
# The kwargs will be passed on to __init__
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
def __del__(self):
self.close()
def open(self, visa_address: str, timeout: int):
try:
rm = pyvisa.ResourceManager()
self._inst = rm.open_resource(visa_address)
except pyvisa.Error:
print(f"\nVisaError: Could not connect to '{visa_address}'")
raise
self._is_connected = True
self.timeout = timeout
# Clear all the event registers and queues used in the instrument
# status and event reporting system
self.write("*CLS")
# Get information about the connected device
self._id = self.query("*IDN?")
# Second query might be needed due to unknown reason
if self._id == "":
self._id = self.query("*IDN?")
self._maker, self._model, self._serial = self._id.split(",")[:3]
if self.verbose:
print(
f"Connected to {self._maker} model {self._model}, "
f"serial {self._serial}"
)
def close(self):
"""Close the connection to the instrument"""
if self._is_connected:
self._inst.close()
self._is_connected = False
@property
def timeout(self) -> int:
"""The timeout of the PYVISA connection in milliseconds"""
return self._inst.timeout
@timeout.setter
def timeout(self, ms: int):
self._inst.timeout = ms
def _initialise_model_properties(self):
"""Initialises the limits of what the instrument can handle according
to the instrument model
Raises
------
NotCompatibleError
If the connected model is not necessarily compatible with this
package, slimits are not known.
"""
if np.any(["AFG1022" in a for a in [self._model, self._override_compat]]):
self.instrument_limits = {
"frequency lims": ({"min": 1e-6, "max": 25e6}, "Hz"),
"voltage lims": (
{"50ohm": {"min": -5, "max": 5}, "highZ": {"min": -10, "max": 10}},
"V",
),
"amplitude lims": (
{
"50ohm": {"min": 0.001, "max": 10},
"highZ": {"min": 0.002, "max": 20},
},
"Vpp",
),
}
self._arbitrary_waveform_length = [2, 8192] # min length, max length
self._arbitrary_waveform_resolution = 16383 # 14 bit
self._max_waveform_memory_user_locations = 255
elif np.any(["AFG1062" in a for a in [self._model, self._override_compat]]):
self.instrument_limits = {
"frequency lims": ({"min": 1e-6, "max": 60e6}, "Hz"),
"voltage lims": (
{"50ohm": {"min": -5, "max": 5}, "highZ": {"min": -10, "max": 10}},
"V",
),
"amplitude lims": (
{
"50ohm": {"min": 0.001, "max": 10},
"highZ": {"min": 0.002, "max": 20},
},
"Vpp",
),
}
self._arbitrary_waveform_length = [2, 1e6] # min length, max length
self._arbitrary_waveform_resolution = 16383 # 14 bit
self._max_waveform_memory_user_locations = 31
elif np.any(["AFG3022" in a for a in [self._model, self._override_compat]]):
self.instrument_limits = {
"frequency lims": ({"min": 1e-6, "max": 25e6}, "Hz"),
"voltage lims": (
{"50ohm": {"min": -5, "max": 5}, "highZ": {"min": -10, "max": 10}},
"V",
),
"amplitude lims": (
{
"50ohm": {"min": 0.01, "max": 10},
"highZ": {"min": 0.02, "max": 20},
},
"Vpp",
),
}
self._arbitrary_waveform_length = [2, 65536] # min length, max length
self._arbitrary_waveform_resolution = 16383 # 14 bit
self._max_waveform_memory_user_locations = 4
else:
msg = (
f"Model {self._model} might not be fully supported!\n"
" The module has been tested with AFG1022, AFG1062, and AFG3022.\n"
" To initiate and use the module as any of these, call the\n"
" class with for instance `override_compatibility='AFG1022'`\n"
" Note that this might lead to unexpected behaviour\n"
" for custom waveforms and 'MIN'/'MAX' keywords."
)
raise NotCompatibleError(msg)
def write(self, command: str, custom_err_message: str = None) -> int:
"""Write a VISA command to the instrument
Parameters
----------
command : str
The VISA command to be written to the instrument
custom_err_message : str, default `None`
When `None`, the RuntimeError message is "Writing/querying command
{command} failed: pyvisa returned StatusCode ..".
Otherwise, if a message is supplied "Could not {message}:
pyvisa returned StatusCode .."
Returns
-------
bytes : int
Number of bytes tranferred
Raises
------
RuntimeError
If status returned by PyVISA write command is not
`pyvisa.constants.StatusCode.success`
"""
num_bytes = self._inst.write(command)
self._check_pyvisa_status(command, custom_err_message=custom_err_message)
return num_bytes
def query(self, command: str, custom_err_message: str = None) -> str:
"""Query the instrument
Parameters
----------
command : str
The VISA query command
custom_err_message : str, default `None`
When `None`, the RuntimeError message is "Writing/querying command
{command} failed: pyvisa returned StatusCode ..".
Otherwise, if a message is supplied "Could not {message}:
pyvisa returned StatusCode .."
Returns
-------
str
The instrument's response
Raises
------
RuntimeError
If status returned by PyVISA write command is not
`pyvisa.constants.StatusCode.success`
"""
response = self._inst.query(command).strip()
self._check_pyvisa_status(command, custom_err_message=custom_err_message)
return response
def _check_pyvisa_status(self, command: str, custom_err_message: str = None):
"""Check the last status code of PyVISA
Parameters
----------
command : str
The VISA write/query command
Returns
-------
status : pyvisa.constants.StatusCode
Return value of the library call
Raises
------
RuntimeError
If status returned by PyVISA write command is not
`pyvisa.constants.StatusCode.success`
"""
status = self._inst.last_status
if not status == pyvisa.constants.StatusCode.success:
if custom_err_message is not None:
msg = (
f"Could not {custom_err_message}: pyvisa returned "
f"StatusCode {status} ({str(status)})"
)
raise RuntimeError(msg)
msg = (
f"Writing/querying command {command} failed: pyvisa returned "
f"StatusCode {status} ({str(status)})"
)
raise RuntimeError(msg)
return status
def get_error(self) -> str:
"""Get the contents of the Error/Event queue on the device
Returns
-------
str
Error/event number, description of error/event
"""
return self.query("SYSTEM:ERROR:NEXT?")
def _spawn_channel(self, channel: int, impedance: str) -> "FuncGenChannel":
"""Wrapper function to create a `FuncGenChannel` object for
a channel -- see the class docstring"""
return FuncGenChannel(self, channel, impedance)
def get_settings(self) -> List[dict]:
"""Get dictionaries of the current settings of the two channels
Returns
-------
settings : list of dicts
[ch1_dict, ch2_dict]: Settings currently in use as a dictionary
with keys output, function, amplitude, offset, and frequency with
corresponding values
"""
return [ch.get_settings() for ch in self.channels]
def print_settings(self):
"""Prints table of the current setting for both channels"""
settings = self.get_settings()
# Find the necessary padding for the table columns
# by evaluating the maximum length of the entries
key_padding = max([len(key) for key in settings[0].keys()])
ch_paddings = [
max([len(str(val[0])) for val in ch_settings.values()])
for ch_settings in settings
]
padding = [key_padding] + ch_paddings
print(f"\nCurrent settings for {self._maker} {self._model} {self._serial}\n")
row_format = "{:>{padd[0]}s} {:{padd[1]}s} {:{padd[2]}s} {}"
table_header = row_format.format("Setting", "Ch1", "Ch2", "Unit", padd=padding)
print(table_header)
print("=" * len(table_header))
for (ch1key, (ch1val, unit)), (_, (ch2val, _)) in zip(
settings[0].items(), settings[1].items()
):
print(
row_format.format(ch1key, str(ch1val), str(ch2val), unit, padd=padding)
)
def set_settings(self, settings: List[dict]):
"""Set the settings of both channels with settings dictionaries
(Each channel is turned off before applying the changes to avoid
potentially harmful combinations)
Parameteres
-----------
settings : list of dicts
List of settings dictionaries as returned by `get_settings`, first
entry for channel 1, second for channel 2. The dictionaries should
have keys output, function, amplitude, offset, and frequency
"""
for ch, s in zip(self.channels, settings):
ch.set_settings(s)
def syncronise_waveforms(self):
"""Syncronise waveforms of the two channels when using the same frequency
Note: Does NOT enable the frequency lock that can be enabled on the
user interface of the instrument)
"""
self.write(":PHAS:INIT", custom_err_message="syncronise waveforms")
def get_frequency_lock(self) -> bool:
"""Check if frequency lock is enabled
Returns
-------
bool
`True` if frequency lock enabled
"""
# If one is locked so is the other, so just need to check one
return int(self.query("SOURCE1:FREQuency:CONCurrent?")) == 1
def set_frequency_lock(self, state: str, use_channel: int = 1):
"""Enable the frequency lock to make the two channels have the same
frequency and phase of their signals, also after adjustments.
See also `FuncGen.syncronise_waveforms` for one-time sync only.
Parameters
----------
state : {"ON", "OFF"}
ON to enable, OFF to disable the lock
use_channel : int, default 1
Only relevant if turning the lock ON: The channel whose frequency
shall be used as the common freqency
"""
if self.verbose:
if state.lower() == "off" and not self.get_frequency_lock():
print(
f"(!) {self._model}: Tried to disable frequency lock, but "
f"frequency lock was not enabled"
)
return
if state.lower() == "on" and self.get_frequency_lock():
print(
f"(!) {self._model}: Tried to enable frequency lock, but "
f"frequency lock was already enabled"
)
return
# (Sufficient to disable for only one of the channels)
cmd = f"SOURCE{use_channel}:FREQuency:CONCurrent {state}"
msg = f"turn frequency lock {state}"
self.write(cmd, custom_err_message=msg)
def software_trig(self):
"""NOT TESTED: sends a trigger signal to the device
(for bursts or modulations)"""
self.write("*TRG", custom_err_message="send trigger signal")
## ~~~~~~~~~~~~~~~~~~~~~ CUSTOM WAVEFORM FUNCTIONS ~~~~~~~~~~~~~~~~~~~~~ ##
def get_waveform_catalogue(self) -> List[str]:
"""Get list of the waveforms that are in use (not empty)
Returns
-------
catalogue : list
Strings with the names of the user functions that are not empty
"""
catalogue = self.query("DATA:CATalog?").split(",")
catalogue = [wf[1:-1] for wf in catalogue] # strip off extra quotes
return catalogue
def get_custom_waveform(self, memory_num: int) -> np.ndarray:
"""Get the waveform currently stored in USER<memory_num>
Parameters
----------
memory_num : str or int {0,...,255}, default 0
Select which user memory to compare with
Returns
-------
waveform : ndarray
Waveform as ints spanning the resolution of the function gen or
and empty array if waveform not in use
"""
# Find the wavefroms in use
waveforms_in_use = self.get_waveform_catalogue()
if f"USER{memory_num}" in waveforms_in_use:
# Copy the waveform to edit memory
self.write(f"DATA:COPY EMEMory,USER{memory_num}")
# Get the length of the waveform
waveform_length = int(self.query("DATA:POINts? EMEMory"))
# Get the waveform (returns binary values)
waveform = self._inst.query_binary_values(
"DATA:DATA? EMEMory",
datatype="H",
is_big_endian=True,
container=np.ndarray,
)
msg = (
f"Waveform length from native length command (DATA:POINts?) "
f"and the processed binary values do not match, "
f"{waveform_length} and {len(waveform)} respectively"
)
assert len(waveform) == waveform_length, msg
return waveform
print(f"Waveform USER{memory_num} is not in use")
return np.array([])
def set_custom_waveform(
self,
waveform: np.ndarray,
normalise: bool = True,
memory_num: int = 0,
verify: bool = True,
print_progress: bool = True,
):
"""Transfer waveform data to edit memory and then user memory.
NOTE: Will overwrite without warnings
Parameters
----------
waveform : ndarray
Either unnormalised arbitrary waveform (then use `normalise=True`),
or ints spanning the resolution of the function generator
normalise : bool
Choose whether to normalise the waveform to ints over the
resolution span of the function generator
memory_num : str or int {0,...,255}, default 0
Select which user memory to copy to
verify : bool, default `True`
Verify that the waveform has been transferred and is what was sent
print_progress : bool, default `True`
Returns
-------
waveform : ndarray
The normalised waveform transferred
Raises
------
ValueError
If the waveform is not within the permitted length or value range
RuntimeError
If the waveform transferred to the instrument is of a different
length than the waveform supplied
"""
if not 0 <= memory_num <= self._max_waveform_memory_user_locations:
raise ValueError(
f"The memory location {memory_num} is not a valid "
"memory location for this model"
)
# Check if waveform data is suitable
if print_progress:
print("Check if waveform data is suitable..", end=" ")
self._check_arb_waveform_length(waveform)
try:
self._check_arb_waveform_type_and_range(waveform)
except ValueError as err:
if print_progress:
print(f"\n {err}")
print("Trying again normalising the waveform..", end=" ")
waveform = self._normalise_to_waveform(waveform)
if print_progress:
print("ok")
print("Transfer waveform to function generator..", end=" ")
# Transfer waveform
self._inst.write_binary_values(
"DATA:DATA EMEMory,", waveform, datatype="H", is_big_endian=True
)
# Check for errors and check lengths are matching
transfer_error = self.get_error()
emem_wf_length = self.query("DATA:POINts? EMEMory")
if emem_wf_length == "" or not int(emem_wf_length) == len(waveform):
msg = (
f"Waveform in temporary EMEMory has a length of {emem_wf_length}"
f", not of the same length as the waveform ({len(waveform)})."
f"\nError from the instrument: {transfer_error}"
)
raise RuntimeError(msg)
if print_progress:
print("ok")
print(f"Copy waveform to USER{memory_num}..", end=" ")
self.write(f"DATA:COPY USER{memory_num},EMEMory")
if print_progress:
print("ok")
if verify:
if print_progress:
print(f"Verify waveform USER{memory_num}..")
if f"USER{memory_num}" in self.get_waveform_catalogue():
verif = self._verify_waveform(
waveform,
memory_num,
normalise=normalise,
print_result=print_progress,
)
if not verif[0]:
raise RuntimeError(
f"USER{memory_num} does not contain the waveform"
)
else:
raise RuntimeError(f"USER{memory_num} is empty")
return waveform
def _normalise_to_waveform(self, shape: np.ndarray) -> np.ndarray:
"""Normalise a shape of any discretisation and range to a waveform that
can be transmitted to the function generator
.. note::
If you are transferring a flat/constant waveform, do not use this
normaisation function. Transfer a waveform like
`int(self._arbitrary_waveform_resolution/2)*np.ones(2).astype(np.int32)`
without normalising for a well behaved flat function.
Parameters
----------
shape : array_like
Array to be transformed to waveform, can be ints or floats,
any normalisation or discretisation
Returns
-------
waveform : ndarray
Waveform as ints spanning the resolution of the function gen
"""
# Check if waveform data is suitable
self._check_arb_waveform_length(shape)
# Normalise
waveform = shape - np.min(shape)
normalisation_factor = np.max(waveform)
waveform = waveform / normalisation_factor * self._arbitrary_waveform_resolution
return waveform.astype(np.uint16)
def _verify_waveform(
self,
waveform: np.ndarray,
memory_num: int,
normalise: bool = True,
print_result: bool = True,
) -> Tuple[bool, np.ndarray, list]:
"""Compare a waveform in user memory to argument waveform
Parameters
----------
waveform : ndarray
Waveform as ints spanning the resolution of the function gen
memory_num : str or int {0,...,255}, default 0
Select which user memory to compare with
normalise : bool, default `True`
Normalise test waveform
Returns
-------
bool
Boolean according to equal/not equal
instrument_waveform
The waveform on the instrument
list or `None`
List of the indices where the waveforms are not equal or `None` if
the waveforms were of different lengths
"""
if normalise: # make sure test waveform is normalised
waveform = self._normalise_to_waveform(waveform)
# Get the waveform on the instrument
instrument_waveform = self.get_custom_waveform(memory_num)
# Compare lengths
len_inst_wav, len_wav = len(instrument_waveform), len(waveform)
if not len_inst_wav == len_wav:
if print_result:
print(
f"The waveform in USER{memory_num} and the compared "
f"waveform are not of same length (instrument "
f"{len_inst_wav} vs {len_wav})"
)
return False, instrument_waveform, None
# Compare each element
not_equal = []
for i in range(len_wav):
if not instrument_waveform[i] == waveform[i]:
not_equal.append(i)
# Return depending of whether list is empty or not
if not not_equal: # if list is empty
if print_result:
print(
f"The waveform in USER{memory_num} and the compared "
f"waveform are equal"
)
return True, instrument_waveform, not_equal
if print_result:
print(
f"The waveform in USER{memory_num} and the compared "
f"waveform are NOT equal"
)
return False, instrument_waveform, not_equal
def _check_arb_waveform_length(self, waveform: np.ndarray):
"""Checks if waveform is within the acceptable length
Parameters
----------
waveform : array_like
Waveform or voltage list to be checked
Raises
------
ValueError
If the waveform is not within the permitted length
"""
if (len(waveform) < self._arbitrary_waveform_length[0]) or (
len(waveform) > self._arbitrary_waveform_length[1]
):
msg = (
"The waveform is of length {}, which is not within the "
"acceptable length {} < len < {}"
"".format(len(waveform), *self._arbitrary_waveform_length)
)
raise ValueError(msg)
def _check_arb_waveform_type_and_range(self, waveform: np.ndarray):
"""Checks if waveform is of int/np.int32 type and within the resolution
of the function generator
Parameters
----------
waveform : array_like
Waveform or voltage list to be checked
Raises
------
ValueError
If the waveform values are not int, np.uint16 or np.int32, or the
values are not within the permitted range
"""
for value in waveform:
if not isinstance(value, (int, np.uint16, np.int32)):
raise ValueError(
"The waveform contains values that are not"
"int, np.uint16 or np.int32"
)
if (value < 0) or (value > self._arbitrary_waveform_resolution):
raise ValueError(
f"The waveform contains values out of range "
f"({value} is not within the resolution "
f"[0, {self._arbitrary_waveform_resolution}])"
)
## ~~~~~~~~~~~~~~~~~~~~~~~ CHANNEL CLASS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ##
class FuncGenChannel:
"""Class for controlling a channel on a function generator object
Parameters
----------
fgen : `FuncGen`
The function generator object
channel : {1, 2}
The channel to be controlled
impedance : {"50ohm", "highZ"}
Determines voltage limits associated with high impedance (whether the
instrument is using 50ohm or high Z cannot be controlled through VISA)
Attributes
----------
_fgen : `FuncGen`
The function generator object for which the channel exists
_channel : {1, 2}
The number of the channel this object is addressing
_source : str
"SOURce{i}:" where {i} is the channel number
"""
_state_to_str = {"1": "ON", "0": "OFF", 1: "ON", 0: "OFF"}
"""Dictionary for converting output states to "ON" and "OFF" """
def __init__(self, fgen: FuncGen, channel: int, impedance: str):
self._fgen = fgen
self._channel = channel
self._source = f"SOURce{channel}:"
self.impedance = impedance
"""{"50ohm", "highZ"}: Determines voltage limits associated with high
impedance (whether the instrument is using 50ohm or high Z cannot be