Mercurial > ~darius > hgwebdir.cgi > pyinst
comparison rs_fsp7_noisetest.py @ 84:4b4ae555067b
Use RMS detector and fix the sweep time for more accuracy.
Add links to app notes discussing theory.
Run forever without pause and print a summary on ctrl-c
author | Daniel O'Connor <doconnor@gsoft.com.au> |
---|---|
date | Thu, 03 Oct 2024 08:57:10 +0930 |
parents | 23c96322cfb6 |
children | 60ad91b4c67c |
comparison
equal
deleted
inserted
replaced
83:4cc3d0706dd1 | 84:4b4ae555067b |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 | 2 |
3 # Copyright (c) 2012 | 3 # Copyright (c) 2024 |
4 # Daniel O'Connor <darius@dons.net.au>. All rights reserved. | 4 # Daniel O'Connor <darius@dons.net.au>. All rights reserved. |
5 # | 5 # |
6 # Redistribution and use in source and binary forms, with or without | 6 # Redistribution and use in source and binary forms, with or without |
7 # modification, are permitted provided that the following conditions | 7 # modification, are permitted provided that the following conditions |
8 # are met: | 8 # are met: |
23 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY | 23 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY |
24 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF | 24 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF |
25 # SUCH DAMAGE. | 25 # SUCH DAMAGE. |
26 # | 26 # |
27 | 27 |
28 # This script uses the "Y Factor Method" as discussed here: | |
29 # https://scdn.rohde-schwarz.com/ur/pws/dl_downloads/dl_application/application_notes/1ma178/1MA178_5e_NoiseFigure.pdf | |
30 # https://www.analog.com/en/resources/technical-articles/noise-figure-measurement-methods-and-formulas--maxim-integrated.html | |
31 # | |
32 # It requires an ENR and has hard coded values for the source we have. | |
33 | |
34 import argparse | |
28 import math | 35 import math |
36 import misc | |
29 import numpy | 37 import numpy |
30 import optparse | |
31 import rsib | 38 import rsib |
32 import scipy | 39 import scipy |
33 import scpi | 40 import scpi |
34 import sys | 41 import sys |
35 | 42 |
40 | 47 |
41 # Convert back to linear values | 48 # Convert back to linear values |
42 enr = 10 ** (enrdb / 10) | 49 enr = 10 ** (enrdb / 10) |
43 | 50 |
44 # Interpolate | 51 # Interpolate |
45 rtn = scipy.interp([frq], enrfrq, enr) | 52 rtn = numpy.interp([frq], enrfrq, enr) |
46 | 53 |
47 # Convert to dB | 54 # Convert to dB |
48 rtndb = 10 * math.log10(rtn) | 55 rtndb = 10 * math.log10(rtn[0]) |
49 | 56 |
50 return rtndb | 57 return rtndb |
51 | 58 |
52 def setup(r, freq, span, sweeps, bw): | 59 def setup(r, freq, span, sweeps, bw, atten, time): |
53 # Reset to defaults | 60 # Reset to defaults |
54 r.write("*RST") | 61 r.write('*RST') |
55 | 62 |
56 # Set to single sweep mode | 63 # Set to single sweep mode |
57 r.write("INIT:CONT OFF") | 64 r.write('INIT:CONT OFF') |
58 | 65 |
59 # Enable display updates | 66 # Enable display updates |
60 r.write("SYST:DISP:UPD ON") | 67 r.write('SYST:DISP:UPD ON') |
61 | 68 |
62 # Set frequency range | 69 # Set frequency range |
63 r.write("SENSE1:FREQ:CENT %f Hz" % (freq)) | 70 r.write('SENSE1:FREQ:CENT %f Hz' % (freq)) |
64 r.write("SENSE1:FREQ:SPAN %f Hz" % (span)) | 71 r.write('SENSE1:FREQ:SPAN %f Hz' % (span)) |
65 | 72 |
66 # Switch marker 1 on in screen A | 73 # Switch marker 1 on in screen A |
67 r.write("CALC:MARK1 ON") | 74 r.write('CALC:MARK1 ON') |
75 | |
76 # Set marker to centre frequency | |
77 r.write('CALC:MARK1:X %f Hz ' % (freq)) | |
68 | 78 |
69 # Enable noise measurement | 79 # Enable noise measurement |
70 r.write("CALC:MARK1:FUNC:NOIS ON") | 80 r.write('CALC:MARK1:FUNC:NOIS ON') |
71 | 81 |
72 # Turn averaging on | 82 # Turn averaging on |
73 r.write("AVER:STAT ON") | 83 r.write('AVER:STAT ON') |
74 | 84 |
75 # Set number of sweeps | 85 # Set number of sweeps |
76 r.write("SWE:COUN %d" % (sweeps)) | 86 r.write('SWE:COUN %d' % (sweeps)) |
87 | |
88 # Use RMS detector | |
89 r.write('SENS:DET RMS') | |
90 | |
91 # Set sweep time | |
92 r.write('SWE:TIME %f' % (time)) | |
77 | 93 |
78 # Set resolution bandwidth | 94 # Set resolution bandwidth |
79 r.write("SENS1:BAND:RES %f Hz" % (bw)) | 95 r.write('SENS1:BAND:RES %f Hz' % (bw)) |
80 | 96 |
81 # Set video bandwidth (10x res BW) | 97 # Set video bandwidth (10x res BW) |
82 r.write("SENS1:BAND:VID %f Hz" % (bw * 10)) | 98 r.write('SENS1:BAND:VID %f Hz' % (bw * 10)) |
99 | |
100 r.write('INP:ATT %s' % atten) | |
83 | 101 |
84 def getnoise(r): | 102 def getnoise(r): |
85 # Trigger the sweep | 103 # Trigger the sweep |
86 r.write("INIT;*WAI") | 104 r.write('INIT;*WAI') |
87 | 105 |
88 # Wait for it to be done | 106 # Wait for it to be done |
89 r.write("*OPC?") | 107 r.ask('*OPC?') |
90 opc = scpi.getdata(r.read(None), int) | |
91 #print "OPC - %d" % (opc) | |
92 assert(opc == 1) | |
93 | 108 |
94 # Set data format | 109 # Set data format |
95 r.write("FORM:DATA ASC") | 110 r.write('FORM:DATA ASC') |
96 | 111 |
97 # Read noise value | 112 # Read noise value |
98 r.write("CALC:MARK1:FUNC:NOIS:RES?") | 113 data = r.ask('CALC:MARK1:FUNC:NOIS:RES?') |
99 data = r.read(10) | 114 #print 'Data - ' + data |
100 #print "Data - " + data | |
101 | 115 |
102 return float(data) | 116 return float(data) |
103 | 117 |
104 def setnoise(r, en): | 118 def setnoise(r, en): |
105 if en: | 119 if en: |
106 val = "ON" | 120 val = 'ON' |
107 else: | 121 else: |
108 val = "OFF" | 122 val = 'OFF' |
109 r.write("DIAG:SERV:NSO " + val) | 123 r.write('DIAG:SERV:NSO ' + val) |
110 | 124 |
111 def calcnf(enrdb, offdb, ondb): | 125 def calcnf(enrdb, offdb, ondb): |
112 # Not possible but noisy results may result in it happening | 126 # Not possible but noisy results may result in it happening |
113 if ondb <= offdb: | 127 if ondb <= offdb: |
114 return 0 | 128 return None |
115 ydb = ondb - offdb | 129 ydb = ondb - offdb |
116 y = 10 ** (ydb / 10) | 130 y = 10 ** (ydb / 10) |
117 enr = 10 ** (enrdb / 10) | 131 enr = 10 ** (enrdb / 10) |
118 nf = 10 * math.log10(enr / (y - 1)) | 132 nf = 10 * math.log10(enr / (y - 1)) |
119 return nf | 133 return nf |
120 | 134 |
121 def donoisetest(r, enr): | 135 def donoisetest(r, enr, sweeps): |
122 print("Acquiring with noise off..") | 136 swt = float(r.ask('SWE:TIME?')) |
137 print('Acquiring with noise off.. (%.1f x %d = %.1f sec)' % (swt, sweeps, swt * sweeps)) | |
123 setnoise(r, False) | 138 setnoise(r, False) |
124 off = getnoise(r) | 139 off = getnoise(r) |
125 print("Acquiring with noise on..") | 140 print('Acquiring with noise on.. (%.1f x %d = %.1f sec)' % (swt, sweeps, swt * sweeps)) |
126 setnoise(r, True) | 141 setnoise(r, True) |
127 on = getnoise(r) | 142 on = getnoise(r) |
128 return off, on, calcnf(enr, off, on) | 143 nf = calcnf(enr, off, on) |
144 #print(nf) | |
145 return off, on, nf | |
129 | 146 |
130 if __name__ == '__main__': | 147 if __name__ == '__main__': |
131 parser = optparse.OptionParser(usage = '%prog [options] address frequency', | 148 parser = argparse.ArgumentParser(description = 'Configures a Rohde Schwarz FSP7 spectrum analyser to do a noise figure test', |
132 description = 'Configures a Rohde Schwarz FSP7 spectrum analyser to do a noise figure test', | 149 epilog = 'Note: video bandwidth is set to 10 times the resolution bandwidth') |
133 epilog = 'video bandwidth is set to 10 times the resolution bandwidth') | 150 parser.add_argument('-a', '--atten', default = 10, help = 'Input attenuation in dB (default: %(default).0f dB)', type = float) |
134 parser.add_option('-s', '--span', dest = 'span', default = 1e6, help = 'Span frequency in Hz (default: %default)', type = float) | 151 parser.add_argument('-b', '--bw', default = 1000, help = 'Resolution bandwidth in Hz (default: %(default).1f Hz)', type = float) |
135 parser.add_option('-i', '--input', dest = 'input', default = None, help = 'Frequency used to compute ENR (defaults to frequency)', type = float) | 152 parser.add_argument('-i', '--input', default = None, help = 'Frequency used to compute ENR (defaults to frequency)', type = float) |
136 parser.add_option('-w', '--sweeps', dest = 'sweeps', default = 20, help = 'Number of sweeps (default: %default)', type = int) | 153 parser.add_argument('-p', '--pause', default = False, action = 'store_true', |
137 parser.add_option('-b', '--bw', dest = 'bw', default = 1000, help = 'Resolution bandwidth in Hz (default: %default)', type = float) | 154 help = 'Wait between measurements (when not doing N repeats, default: %(default)s)') |
138 parser.add_option('-r', '--repeat', dest = 'repeat', help = 'Number of repetitions, if not specified do one and ask to continue', type = int) | 155 parser.add_argument('-r', '--repeat', help = 'Number of repetitions, if not specified do one and ask to continue', type = int) |
139 | 156 parser.add_argument('-s', '--span', default = 1e6, help = 'Span frequency in Hz (default: %(default).0f Hz)', type = float) |
140 (options, args) = parser.parse_args() | 157 parser.add_argument('-t', '--time', default = 30, help = 'Sweep time (default: %(default)f sec)', type = float) |
141 | 158 parser.add_argument('-w', '--sweeps', default = 3, help = 'Number of sweeps to average (default: %(default)d)', type = int) |
142 if len(args) != 2: | 159 parser.add_argument('address', help = 'Spectrum analyser address', type = str) |
143 parser.error('Must supply the specan address and centre frequency') | 160 parser.add_argument('centre', help = 'Centre frequency (Hz)', type = float) |
144 | 161 |
145 addr = args[0] | 162 args = parser.parse_args() |
146 try: | 163 |
147 freq = float(args[1]) | 164 if args.input == None: |
148 except ValueError: | 165 args.input = args.centre |
149 parser.error('Unable to parse frequency') | 166 |
150 | 167 if args.time is not None and args.time <= 0: |
151 if options.input == None: | 168 parser.error('Sweep time must be >0') |
152 options.input = freq | |
153 | 169 |
154 # Compute ENR at frequency of interest | 170 # Compute ENR at frequency of interest |
155 enr = findenr(options.input) | 171 enr = findenr(args.input) |
156 | 172 |
157 # Connect to the analyser | 173 # Connect to the analyser |
158 r = rsib.RSIBDevice(addr) | 174 r = rsib.RSIBDevice(args.address) |
159 | 175 |
160 # ID instrument | 176 # ID instrument |
161 r.write('*IDN?') | 177 print('Device ID is ' + r.ask('*IDN?').decode('ascii')) |
162 print("ID is " + r.read(5)) | |
163 | 178 |
164 # Setup parameters | 179 # Setup parameters |
165 setup(r, freq, options.span, options.sweeps, options.bw) | 180 setup(r, args.centre, args.span, args.sweeps, args.bw, args.atten, args.time) |
166 | |
167 r.write("INIT:CONT OFF") | |
168 | 181 |
169 nfs = [] | 182 nfs = [] |
170 print("Centre: %.1f Mhz, Span %.1f Mhz, Input %.1f MHz, BW %.1f kHz, %d sweeps, ENR %.2f dB" % (freq / 1e6, options.span / 1e6, options.input / 1e6, options.bw / 1e3, options.sweeps, enr)) | 183 print('Centre: %s, Span %s, Input %s, RBW %s, Sweeps: %d, Sweep time: %.1f sec, ENR %.2f dB' % ( |
171 while options.repeat == None or options.repeat > 0: | 184 misc.sifmt(args.centre), misc.sifmt(args.span), |
172 off, on, nf = donoisetest(r, enr) | 185 misc.sifmt(args.input), misc.sifmt(args.bw), |
173 print("Off %.3f dBm/Hz, on %.3f dBm/Hz, NF %.2f dB" % (off, on, nf)) | 186 args.sweeps, args.time, enr)) |
174 nfs.append(nf) | 187 while args.repeat == None or args.repeat > 0: |
175 if options.repeat == None: | 188 try: |
176 print("Press enter to perform a new measurement") | 189 off, on, nf = donoisetest(r, enr, args.sweeps) |
177 sys.stdin.readline() | 190 if nf is None or nf < 0: |
178 else: | 191 nfstr = 'Invalid' |
179 options.repeat -= 1 | 192 else: |
193 nfstr = '%.2f dB' % (nf) | |
194 nfs.append(nf) | |
195 print('Off %.3f dBm/Hz, on %.3f dBm/Hz, NF %s' % (off, on, nfstr)) | |
196 if args.repeat == None: | |
197 if args.pause: | |
198 print('Press enter to perform a new measurement') | |
199 sys.stdin.readline() | |
200 else: | |
201 args.repeat -= 1 | |
202 except KeyboardInterrupt: | |
203 print('') | |
204 break | |
180 | 205 |
181 if len(nfs) > 1: | 206 if len(nfs) > 1: |
182 nfs = numpy.array(nfs) | 207 nfs = numpy.array(nfs) |
183 print("NF min: %.1f dBm/Hz, max: %.1f dBm/Hz, avg: %.1f dBm/hz, stddev: %.1f" % ( | 208 |
184 nfs.min(), nfs.max(), nfs.sum() / len(nfs), numpy.std(nfs))) | 209 # XXX: not sure mean/std of dBm/Hz values is really meaningful |
210 print('N: %d, NF min: %.1f dBm/Hz, max: %.1f dBm/Hz, avg: %.1f dBm/Hz, stddev: %.1f' % ( | |
211 len(nfs), nfs.min(), nfs.max(), nfs.mean(), numpy.std(nfs))) |