Mercurial > ~darius > hgwebdir.cgi > pyinst
comparison fsp7_phasenoise.py @ 81:1947d10f9395
- Search for signal frequency iteratively to improve accuracy.
- Use narrower bandwidths (via FFT filter) for close in measurements.
- Work around bug in firmware when using FFT filter.
- Actually _raise_ the exception when the signal power is too low.
author | Daniel O'Connor <doconnor@gsoft.com.au> |
---|---|
date | Fri, 27 Sep 2024 16:56:44 +0930 |
parents | 23c96322cfb6 |
children | 6ce8ed5ba76d |
comparison
equal
deleted
inserted
replaced
80:b280fe3b696a | 81:1947d10f9395 |
---|---|
25 # SUCH DAMAGE. | 25 # SUCH DAMAGE. |
26 # | 26 # |
27 | 27 |
28 import argparse | 28 import argparse |
29 import math | 29 import math |
30 import misc | |
30 import numpy | 31 import numpy |
31 import os | 32 import os |
32 import PIL | 33 import PIL |
33 import rsib | 34 import rsib |
34 import scipy | 35 import scipy |
35 import scpi | 36 import scpi |
36 import sys | 37 import sys |
37 import time | 38 import time |
38 | |
39 def sifmt(_v, dp = 3, unit = 'Hz', sp = ' '): | |
40 '''Format a number using SI prefixes''' | |
41 si_prefixes = ('T', 'G', 'M', 'k', '', 'm', 'ยต', 'n', 'p') | |
42 scale = 10 ** 12 | |
43 v = abs(_v) | |
44 if v == 0: | |
45 sip = "" | |
46 scale = 0 | |
47 for i, sip in enumerate(si_prefixes): | |
48 if v >= scale: | |
49 break | |
50 scale /= 1e3 | |
51 return ('%.' + str(dp) + 'f%s%s%s') % (_v / scale, sp, si_prefixes[i], unit) | |
52 | 39 |
53 def setupsweep(r, rbw, vbw, centre = None, span = None, start = None, stop = None): | 40 def setupsweep(r, rbw, vbw, centre = None, span = None, start = None, stop = None): |
54 '''Helper function to set various sweep parameters''' | 41 '''Helper function to set various sweep parameters''' |
55 if centre is not None: | 42 if centre is not None: |
56 r.write('SENSE1:FREQ:CENT %f Hz' % (centre)) | 43 r.write('SENSE1:FREQ:CENT %f Hz' % (centre)) |
59 if start is not None: | 46 if start is not None: |
60 r.write('SENSE1:FREQ:START %f Hz' % (start)) | 47 r.write('SENSE1:FREQ:START %f Hz' % (start)) |
61 if stop is not None: | 48 if stop is not None: |
62 r.write('SENSE1:FREQ:STOP %f Hz' % (stop)) | 49 r.write('SENSE1:FREQ:STOP %f Hz' % (stop)) |
63 if rbw is not None: | 50 if rbw is not None: |
51 # Need to use FFT filters below 10Hz BW | |
52 if rbw < 10: | |
53 r.write('BAND:TYPE FFT') | |
54 else: | |
55 r.write('BAND:TYPE NORM') | |
64 r.write('SENS1:BAND:RES %f Hz' % (rbw)) | 56 r.write('SENS1:BAND:RES %f Hz' % (rbw)) |
65 if vbw is not None: | 57 if vbw is not None: |
66 r.write('SENS1:BAND:VID %f Hz' % (vbw)) | 58 r.write('SENS1:BAND:VID %f Hz' % (vbw)) |
67 | 59 |
68 def dosweep(r, sweeps): | 60 def dosweep(r, sweeps): |
69 '''Helper function to trigger a sweep and wait for it to finish''' | 61 '''Helper function to trigger a sweep and wait for it to finish''' |
70 swt = float(r.ask('SWE:TIME?')) | 62 tout = getsweeptime(r) * 5 * sweeps |
71 tout = swt * 5 * sweeps | |
72 if tout < 1: | 63 if tout < 1: |
73 tout = 1 | 64 tout = 1 |
74 #print('Sweep time', swt) | 65 #print('Sweep time', swt) |
75 | 66 |
76 # Trigger the sweep | 67 # Trigger the sweep |
97 peaks_pwr.append(pwr) | 88 peaks_pwr.append(pwr) |
98 r.write('CALC:MARK1:MAX:NEXT') | 89 r.write('CALC:MARK1:MAX:NEXT') |
99 | 90 |
100 return peaks_f, peaks_pwr | 91 return peaks_f, peaks_pwr |
101 | 92 |
93 def getsweeptime(r): | |
94 '''Return sweep time in seconds''' | |
95 # According to the manual SWE:TIME works for both normal and FFT filters | |
96 # however in practise it doesn't get a response for FFT ones so we fake it | |
97 | |
98 if r.ask('BAND:TYPE?').decode('ascii') == 'FFT': | |
99 return 5 | |
100 else: | |
101 return float(r.ask('SWE:TIME?')) | |
102 | |
102 def phasenoise(r, nominal, sweeps, atten, rlev, yscale, ssprefix): | 103 def phasenoise(r, nominal, sweeps, atten, rlev, yscale, ssprefix): |
103 '''Main function to find a signal, do some phase noise measurements and wideband sweeps''' | 104 '''Main function to find a signal, do some phase noise measurements and wideband sweeps''' |
104 cpwrlim = -30 | 105 cpwrlim = -30 |
105 measurements = ( | 106 measurements = ( |
106 { 'offset' : 10, 'span' : 100, 'rbw' : 10, 'vbw' : 10 }, | 107 { 'offset' : 10, 'span' : 100, 'rbw' : 1, 'vbw' : 3 }, |
107 { 'offset' : 100, 'span' : 250, 'rbw' : 10, 'vbw' : 10 }, | 108 { 'offset' : 100, 'span' : 250, 'rbw' : 1, 'vbw' : 3 }, |
108 { 'offset' : 1e3, 'span' : 2.5e3, 'rbw' : 10, 'vbw' : 30 }, | 109 { 'offset' : 1e3, 'span' : 2.5e3, 'rbw' : 1, 'vbw' : 3 }, |
109 { 'offset' : 10e3, 'span' : 25e3, 'rbw' : 100, 'vbw' : 300 }, | 110 { 'offset' : 10e3, 'span' : 25e3, 'rbw' : 100, 'vbw' : 300 }, |
110 { 'offset' : 100e3, 'span' : 250e3, 'rbw' : 100, 'vbw' : 300 }, | 111 { 'offset' : 100e3, 'span' : 250e3, 'rbw' : 100, 'vbw' : 300 }, |
111 { 'offset' : 1e6, 'span' : 2.1e6, 'rbw' : 1e3, 'vbw' : 3e3 }, | 112 { 'offset' : 1e6, 'span' : 2.1e6, 'rbw' : 1e3, 'vbw' : 3e3 }, |
112 ) | 113 ) |
113 # Reset to defaults | 114 # Reset to defaults |
114 r.write('*RST') | 115 r.write('*RST') |
116 r.write('*CLS') | |
115 | 117 |
116 # Set to single sweep mode | 118 # Set to single sweep mode |
117 r.write('INIT:CONT OFF') | 119 r.write('INIT:CONT OFF') |
118 | 120 |
119 # Enable display updates | 121 # Enable display updates |
134 r.write('DISP:WIND:TRAC:Y:RLEV AUTO') | 136 r.write('DISP:WIND:TRAC:Y:RLEV AUTO') |
135 else: | 137 else: |
136 r.write('DISP:WIND:TRAC:Y:RLEV %f' % (rlev)) | 138 r.write('DISP:WIND:TRAC:Y:RLEV %f' % (rlev)) |
137 | 139 |
138 # | 140 # |
139 # Look for signal | 141 # Look from 100kHz down to 100Hz to narrow down the exact frequency |
140 # | 142 # |
141 # Set frequency range etc | 143 found = False |
142 setupsweep(r, 10, 30, centre = nominal, span = 1e3) | 144 cmeas = nominal |
143 | 145 for idx, span in enumerate((100e3, 10e3, 1e3, 100)): |
144 # Switch marker 1 on in screen A | 146 setupsweep(r, span / 50, span / 50 * 3, centre = cmeas, span = span) |
145 r.write('CALC:MARK1 ON') | 147 print('Looking for signal at %s+/-%s (%.1f seconds)' % ( |
146 print('Looking for signal (%.1f seconds)' % (float(r.ask('SWE:TIME?')))) | 148 misc.sifmt(nominal), misc.sifmt(span, dp = 1), |
147 | 149 getsweeptime(r))) |
148 # Do the sweep | 150 # Do the sweep |
149 dosweep(r, 1) | 151 dosweep(r, 1) |
150 | 152 |
151 # Check the instrument is happy | 153 # Check the instrument is happy |
152 status = int(r.ask('STAT:QUES:COND?')) | 154 status = int(r.ask('STAT:QUES:COND?')) |
153 pwrstat = int(r.ask('STAT:QUES:POW:COND?')) | 155 pwrstat = int(r.ask('STAT:QUES:POW:COND?')) |
154 if status != 0 or pwrstat != 0: | 156 if status != 0 or pwrstat != 0: |
155 print('Instrument warning, status %s power status %s' % | 157 # Dump a screenshot for debugging |
156 (bin(status), bin(pwrstat))) | 158 screenshot(r, '%s-sweep-inst-warnings.png' % (ssprefix)) |
157 # Find the peak | 159 raise Exception('Instrument warning, status %s power status %s' % |
158 r.write('CALC:MARK1:MAX') | 160 (bin(status), bin(pwrstat))) |
159 cpwr = float(r.ask('CALC:MARK1:Y?')) | 161 # Find the peak |
160 if cpwr < cpwrlim: | 162 r.write('CALC:MARK1 ON') |
161 Exception('Power too low / not found: %.1f dBm vs %.1f dBm' % (cpwr, cpwrlim)) | 163 r.write('CALC:MARK1:MAX') |
162 cmeas = float(r.ask('CALC:MARK1:X?')) | 164 cpwr = float(r.ask('CALC:MARK1:Y?')) |
163 print('Found signal at %s with power %.1f dBm' % (sifmt(cmeas, dp = 6), cpwr)) | 165 if cpwr < cpwrlim: |
164 | 166 # Dump a screenshot for debugging |
167 screenshot(r, '%s-sweep-no-power.png' % (ssprefix)) | |
168 raise Exception('Power too low / not found: %.1f dBm vs %.1f dBm' % (cpwr, cpwrlim)) | |
169 else: | |
170 found = True | |
171 cmeas = float(r.ask('CALC:MARK1:X?')) | |
172 print('Found signal at %s with power %.1f dBm' % (misc.sifmt(cmeas, dp = 6), cpwr)) | |
173 | |
174 if not found: | |
175 print('Unable to find signal') | |
165 # Centre peak | 176 # Centre peak |
166 r.write('CALC:MARK1:FUNC:CENT') | 177 r.write('CALC:MARK1:FUNC:CENT') |
167 | 178 |
168 # Set number of sweeps | 179 # Set number of sweeps |
169 r.write('SWE:COUN %d' % (sweeps)) | 180 r.write('SWE:COUN %d' % (sweeps)) |
173 r.write('CALC:DELT1:FUNC:PNO') | 184 r.write('CALC:DELT1:FUNC:PNO') |
174 | 185 |
175 for idx, m in enumerate(measurements): | 186 for idx, m in enumerate(measurements): |
176 # Setup measurement | 187 # Setup measurement |
177 setupsweep(r, m['rbw'], m['vbw'], span = m['span']) | 188 setupsweep(r, m['rbw'], m['vbw'], span = m['span']) |
178 | |
179 # Do the sweep | 189 # Do the sweep |
180 dosweep(r, sweeps) | 190 dosweep(r, sweeps) |
181 | 191 |
182 # Set offset of phase noise measurement | 192 # Set offset of phase noise measurement |
183 r.write('CALC:MARK1:MAX') | 193 r.write('CALC:MARK1:MAX') |
184 r.write('CALC:DELT1:MOD REL') | 194 r.write('CALC:DELT1:MOD REL') |
185 r.write('CALC:DELT1:X %f Hz' % (m['offset'])) | 195 r.write('CALC:DELT1:X %f Hz' % (m['offset'])) |
186 meas = float(r.ask('CALC:DELT:FUNC:PNO:RES?')) | 196 meas = float(r.ask('CALC:DELT:FUNC:PNO:RES?')) |
187 print('Offset %7s %7.2f dBc/Hz' % (sifmt(m['offset'], dp = 0), meas)) | 197 print('Offset %7s %7.2f dBc/Hz' % (misc.sifmt(m['offset'], dp = 0), meas)) |
188 | 198 |
189 # Take screen shot and save it | 199 # Take screen shot and save it |
190 if ssprefix is not None: | 200 if ssprefix is not None: |
191 screenshot(r, '%s-%d_%s_offset.png' % ( | 201 screenshot(r, '%s-%d_%s_offset.png' % ( |
192 ssprefix, idx, sifmt(m['offset'], dp = 0, sp = ''))) | 202 ssprefix, idx, misc.sifmt(m['offset'], dp = 0, sp = ''))) |
193 | 203 |
194 # Do a wide band scan | 204 # Do a wide band scan |
195 if ssprefix is not None: | 205 if ssprefix is not None: |
196 start = 1e6 | 206 start = 1e6 |
197 stop = 100e6 | 207 stop = 100e6 |
198 setupsweep(r, 1e3, 3e3, start = start, stop = stop) | 208 setupsweep(r, 1e3, 3e3, start = start, stop = stop) |
199 print('Scanning %s - %s (%.1f seconds)' % (sifmt(start, dp = 0), sifmt(stop, dp = 0), | 209 print('Scanning %s - %s (%.1f seconds)' % (misc.sifmt(start, dp = 0), misc.sifmt(stop, dp = 0), |
200 float(r.ask('SWE:TIME?')))) | 210 getsweeptime(r))) |
201 dosweep(r, sweeps) | 211 dosweep(r, sweeps) |
202 # Show peaks | 212 # Show peaks |
203 r.write('CALC:MARK:AOFF') | 213 r.write('CALC:MARK:AOFF') |
204 for i, (f, p) in enumerate(zip(*findpeaks(r, 4))): | 214 for i, (f, p) in enumerate(zip(*findpeaks(r, 4))): |
205 r.write('CALC:MARK%d:X %f Hz' % (i + 1, f)) | 215 r.write('CALC:MARK%d:X %f Hz' % (i + 1, f)) |
208 | 218 |
209 # Look from just before signal to just after second harmonic | 219 # Look from just before signal to just after second harmonic |
210 start = nominal * 0.9 | 220 start = nominal * 0.9 |
211 stop = 2 * nominal + nominal * 0.1 | 221 stop = 2 * nominal + nominal * 0.1 |
212 setupsweep(r, 1e3, 3e3, start = start, stop = stop) | 222 setupsweep(r, 1e3, 3e3, start = start, stop = stop) |
213 print('Scanning %s - %s (%.1f seconds)' % (sifmt(start, dp = 0), sifmt(stop, dp = 0), | 223 print('Scanning %s - %s (%.1f seconds)' % (misc.sifmt(start, dp = 0), misc.sifmt(stop, dp = 0), |
214 float(r.ask('SWE:TIME?')))) | 224 getsweeptime(r))) |
215 dosweep(r, sweeps) | 225 dosweep(r, sweeps) |
216 # Show peaks | 226 # Show peaks |
217 r.write('CALC:MARK:AOFF') | 227 r.write('CALC:MARK:AOFF') |
218 for i, (f, p) in enumerate(zip(*findpeaks(r, 4))): | 228 for i, (f, p) in enumerate(zip(*findpeaks(r, 4))): |
219 r.write('CALC:MARK%d:X %f Hz' % (i + 1, f)) | 229 r.write('CALC:MARK%d:X %f Hz' % (i + 1, f)) |