pn.py
Note that this page is automatically generated from an org-mode source file. The Python code for this reference design is also automatically generated from this file using org-babel-tangle.
The Script
The main
Function
The asyncio
framework is used to coordinate communications over GPIB
without blocking user interface events. For this purpose we make use
of QEventLoop
and QThreadExecutor
from the qasync package.
def main(): parser = ArgumentParser(description= '''Measure phase noise.''') parser.add_argument("--sg", choices=['SMHU58', 'DSG815'], default="SMHU58", help="Reference signal generator in use. Default=SMHU58") parser.add_argument("-H", "--harmonics", action='store_true', help="Don't suppress power supply harmonics at 150 and 250Hz") args = parser.parse_args() app = QApplication(sys.argv) loop = QEventLoop(app) loop.set_default_executor(QThreadExecutor(1)) asyncio.set_event_loop(loop) appWindow = PhaseNoiseApp(sg_model=args.sg) appWindow.show() with loop: loop.set_debug(True) sys.exit(loop.run_forever())
Phase Noise App Class
Measuring the phase noise of a given device (DUT) proceeds in the following steps:
Measure the approximate frequency and power of the DUT signal: Measure DUT signal
Calibrate the test setup using a known FM modulated signal source: Calibration
Ensure that the DUT signal is locked to the reference signal: Capture
Acquire phase noise measurements: Measure and display
class PhaseNoiseApp(QMainWindow): GPIB_CONTROLLER_LIST = [PROLOGIX_IPADDR] FREQ_SUFFIX = ' MHz' MIN_DUT_FREQUENCY = 5.0 MAX_DUT_FREQUENCY = 3200.0 DEFAULT_DUT_FREQUENCY = 1000.0 OFFSET_START = {'100': 1e2, '1K': 1e3, '10K': 1e4} OFFSET_END = {'100K': 1e5, '1M': 1e6, '10M': 1e7} SA_SMOOTHING = {"0.003": .003, "0.01": .01, "0.03": .03, "0.1": .1, "0.3": .3, "1.0": 1.0, "3.0": 3.0} # FILTER_BANDS = {1: [5.0, 1280.0], 2: [1280.0, 3200.0], 3: [3200.0, 5760.0], # 4: [5760, 8320], 5: [8320, 10880], 6: [10880, 13440], # 7: [13440, 16000], 8: [16000, 18000]} FILTER_BANDS = {1: [5.0, 1280.0], 2: [1280.0, 3200.0]} LOCK_BANDWIDTHS = {"1": 1, "10": 2, "100": 3, "1K": 4, "10K": 5} FMDEV_RANGE = [10.0, 50000.0] CAL_ATTEN = -40.0 CAL_FREQ_OFFSET = 0.05 DEFAULT_FMDEV = 5000 # Tuple format: (start_freq, stop_freq, rbw, wn) # An rbw entry of 0 implies 'AUTO' # wn is the PN_BANDS = [ (100, 2e3, 10, 15/300), (2e3, 1e4, 0, 20/300), (1e4, 1e5, 0, 25/300), (1e5, 1e6, 0, 25/300), (1e6, 1e7, 0, 25/300)] Lf_CONV_FACTOR = 6.0 LOG_CONV_FACTOR = 2.5 def __init__(self, sg_model='SMHU58'): super().__init__() self.gpib = None self.sa = None self.sg = None self.pn = None self._gpib_ctl_addr = PROLOGIX_IPADDR self._band = 1 self._if_freq = PhaseNoiseApp.DEFAULT_DUT_FREQUENCY self._dut_freq = PhaseNoiseApp.DEFAULT_DUT_FREQUENCY self._sa_avg = 10 self._sa_smoothing = 0.1 self._pn_data = [] self.build_ui() def enable_ui(self, s): self._dut_gbox.setEnabled(s) self._bb_gbox.setEnabled(s) self._meas_gbox.setEnabled(s) def build_ui(self): """Build the on-screen UI for the phase noise app.""" vbox = QVBoxLayout() hbox = QHBoxLayout() gpib_combo = QComboBox() gpib_combo.addItems(PhaseNoiseApp.GPIB_CONTROLLER_LIST) gpib_combo.currentIndexChanged.connect( lambda idx, w=gpib_combo: self.gpib_changed(w, idx)) line_edit = QLineEdit() gpib_combo.setLineEdit(line_edit) hbox.addWidget(QLabel("GPIB Controller:")) hbox.addWidget(gpib_combo) hbox.addStretch(1) initialize_btn = QPushButton("Initialize") initialize_btn.clicked.connect(self.initialize) hbox.addWidget(initialize_btn) vbox.addLayout(hbox) self._dut_gbox = QGroupBox("DUT and Reference") vbox2 = QVBoxLayout() self._dut_gbox.setLayout(vbox2) hbox2 = QHBoxLayout() self._dutf_box = QDoubleSpinBox() self._dutf_box.setRange(PhaseNoiseApp.MIN_DUT_FREQUENCY, PhaseNoiseApp.MAX_DUT_FREQUENCY) self._dutf_box.setDecimals(3) self._dutf_box.setStyleSheet("""QDoubleSpinBox { font-size: 25pt; }""") self._dutf_box.setValue(PhaseNoiseApp.DEFAULT_DUT_FREQUENCY) self._dutf_box.setSuffix(PhaseNoiseApp.FREQ_SUFFIX) self._dutf_box.valueChanged.connect(self.set_dut_freq) hbox2.addWidget(QLabel("DUT Freq:")) hbox2.addWidget(self._dutf_box) hbox2.addStretch(1) self._measure_dutf_btn = QPushButton("Measure DUT Sig") self._measure_dutf_btn.clicked.connect(self.connect_dut_signal) hbox2.addWidget(self._measure_dutf_btn) vbox2.addLayout(hbox2) hbox2 = QHBoxLayout() self._ref_box = QLabel() self._ref_box.setStyleSheet("""QLabel { font-size: 25pt; }""") self._ref_box.setText('{:.3f} MHz'.format(self.if_freq)) hbox2.addWidget(QLabel("Ref. Freq:")) hbox2.addWidget(self._ref_box) hbox2.addStretch(1) self._measure_if_btn = QPushButton("Measure IF Sig") self._measure_if_btn.clicked.connect(self.connect_if_signal) hbox2.addWidget(self._measure_if_btn) vbox2.addLayout(hbox2) hbox2 = QHBoxLayout() self._cal_btn = QPushButton("Calibrate") self._cal_btn.clicked.connect(self.calibrate) hbox2.addWidget(self._cal_btn) hbox2.addStretch(1) vbox2.addLayout(hbox2) vbox.addWidget(self._dut_gbox) self._bb_gbox = QGroupBox("Baseband") fbox1 = QFormLayout() self._bb_gbox.setLayout(fbox1) hbox = QHBoxLayout() offsetStartGroup = QButtonGroup(hbox) self._offset_start_btns = [] for offset in PhaseNoiseApp.OFFSET_START.keys(): rb = QRadioButton("{}".format(offset)) rb.offset = offset rb.toggled.connect( lambda state, w=rb: self.set_offset_start(state, w.offset)) offsetStartGroup.addButton(rb) self._offset_start_btns.append(rb) hbox.addWidget(rb) self._offset_start_btns[0].setChecked(True) fbox1.addRow(QLabel('Offset Start Freq:'), hbox) hbox = QHBoxLayout() offsetStopGroup = QButtonGroup(hbox) self._offset_stop_btns = [] for offset in PhaseNoiseApp.OFFSET_END.keys(): rb = QRadioButton("{}".format(offset)) rb.offset = offset rb.toggled.connect( lambda state, w=rb: self.set_offset_stop(state, w.offset)) offsetStopGroup.addButton(rb) self._offset_stop_btns.append(rb) hbox.addWidget(rb) self._offset_stop_btns[-1].setChecked(True) fbox1.addRow(QLabel('Offset Stop Freq:'), hbox) hbox = QHBoxLayout() smoothingGroup = QButtonGroup(hbox) self._sa_smooth_btns = [] for ratio in PhaseNoiseApp.SA_SMOOTHING.keys(): rb = QRadioButton("{}".format(ratio)) rb.ratio = ratio rb.toggled.connect( lambda state, w=rb: self.set_sa_smoothing(state, w.ratio)) smoothingGroup.addButton(rb) self._sa_smooth_btns.append(rb) hbox.addWidget(rb) self._sa_smooth_btns[3].setChecked(True) fbox1.addRow(QLabel("SA Smoothing Ratio:"), hbox) self._sa_avg_box = QSpinBox() self._sa_avg_box.setRange(1, 999) self._sa_avg_box.valueChanged.connect(self.set_sa_avg) self._sa_avg_box.setValue(10) fbox1.addRow(QLabel("SA Averaging:"), self._sa_avg_box) vbox.addWidget(self._bb_gbox) self._meas_gbox = QGroupBox("Phase Locking") vbox2 = QVBoxLayout() fbox1 = QFormLayout() vbox2.addLayout(fbox1) self._meas_gbox.setLayout(vbox2) hbox = QHBoxLayout() methodGroup = QButtonGroup(hbox) self._method_btns = [] for method in ["Phase Det.", "Manual Lock", "Freq. Discrim."]: rb = QRadioButton("{}".format(method)) rb.method = method rb.toggled.connect( lambda state, w=rb: self.set_method(state, w.method)) methodGroup.addButton(rb) self._method_btns.append(rb) hbox.addWidget(rb) self._method_btns[0].setChecked(True) self._method_btns[2].setEnabled(False) fbox1.addRow(QLabel("Method:"), hbox) hbox = QHBoxLayout() lockbwGroup = QButtonGroup(hbox) self._lockbw_btns = [] for bw in PhaseNoiseApp.LOCK_BANDWIDTHS.keys(): rb = QRadioButton(bw) rb.bw = PhaseNoiseApp.LOCK_BANDWIDTHS[bw] rb.toggled.connect( lambda state, w=rb: self.set_lockbw(state, w.bw)) lockbwGroup.addButton(rb) self._lockbw_btns.append(rb) hbox.addWidget(rb) self._lockbw_btns[1].setChecked(True) fbox1.addRow(QLabel("Lock BW Factor:"), hbox) self._fmdev_box = QDoubleSpinBox() self._fmdev_box.setRange(*PhaseNoiseApp.FMDEV_RANGE) self._fmdev_box.setDecimals(1) self._fmdev_box.setSingleStep(1000) self._fmdev_box.valueChanged.connect(self.set_fmdev) self._fmdev_box.setValue(PhaseNoiseApp.DEFAULT_FMDEV) fbox1.addRow(QLabel("FM Deviation:"), self._fmdev_box) hbox = QHBoxLayout() self._capture_btn = QPushButton("Capture") self._capture_btn.clicked.connect(self.capture) hbox.addWidget(self._capture_btn) hbox.addStretch(1) vbox2.addLayout(hbox) vbox.addWidget(self._meas_gbox) self.enable_ui(False) hbox = QHBoxLayout() self._meas_btn = QPushButton("Measurements...") self._meas_btn.clicked.connect(self.show_measure_dialog) hbox.addWidget(self._meas_btn) hbox.addStretch(1) vbox.addLayout(hbox) self.container = QWidget() self.container.setLayout(vbox) self.setCentralWidget(self.container) self.setWindowTitle('Phase Noise Measurement') @asyncSlot() async def initialize(self): try: self.initialize_ui() loop = asyncio.get_event_loop() await loop.run_in_executor(None, self.initialize_hw) self.enable_ui(True) except RuntimeError as re: error_dialog = QErrorMessage(self) error_dialog.showMessage(str(re)) def initialize_ui(self): pass def initialize_hw(self): try: self.gpib = GPIB(gpib_ip=self.gpib_ctl_addr) self.gpib.initialize() except socket.timeout as te: self.gpib = None raise RuntimeError('GPIB controller is offline or powered down') try: self.sa = HP8560A(self.gpib, HP8560A_GPIBID) sa_id = self.sa.ident self.sa.initialize() except socket.timeout as te: self.sa = None raise RuntimeError('HP8560A is offline or powered down') try: self.sg = SMHU58(self.gpib, SMHU58_GPIBID) sg_id = self.sg.ident self.sg.initialize() except socket.timeout as te: self.sg = None raise RuntimeError('SMHU58 is offline or powered down') try: self.pn = HP11729C(self.gpib, HP11729C_GPIBID) #pn_id = self.pn.ident self.pn.initialize() except socket.timeout as te: self.pn = None raise RuntimeError('HP11729C is offline or powered down') @property def gpib_ctl_addr(self): return self._gpib_ctl_addr def gpib_changed(self, combo, idx): self._gpib_ctl_addr = combo.itemText(idx) @property def offset_start(self): return self._offset_start def set_offset_start(self, state, offset_str): if state: self._offset_start = PhaseNoiseApp.OFFSET_START[offset_str] @property def offset_stop(self): return self._offset_stop def set_offset_stop(self, state, offset_str): if state: self._offset_stop = PhaseNoiseApp.OFFSET_END[offset_str] @property def sa_smoothing(self): return self._sa_smoothing def set_sa_smoothing(self, state, value): if state: self._sa_smoothing = PhaseNoiseApp.SA_SMOOTHING[value] @property def sa_avg(self): return self._sa_avg def set_sa_avg(self, avg): self._sa_avg = avg @property def method(self): return self._method def set_method(self, state, m): if state: self._method = m @property def band(self): return self._band @property def band_center(self): l = PhaseNoiseApp.FILTER_BANDS[self.band] return l[1] - l[0] def set_band(self, f): for band, l in PhaseNoiseApp.FILTER_BANDS.items(): if f > l[0] and f <= l[1]: self._band = band return raise RuntimeError( 'DUT freq. {} outside of available filter bands'.format(f)) @property def lockbw(self): return self._lockbw def set_lockbw(self, state, bw): if state: self._lockbw = bw @property def fmdev(self): return self._fm_dev def set_fmdev(self, value): self._fm_dev = value @property def dut_freq(self): return self._dut_freq def set_dut_freq(self, f): self._dut_freq = f self.set_band(f) if self.band == 1: self.if_freq = self.dut_freq else: self.if_freq = abs(self.band_center - self.dut_freq) self._ref_box.setText('{:.3f} MHz'.format(self.if_freq)) @property def if_freq(self): return self._if_freq @if_freq.setter def if_freq(self, f): self._if_freq = f @property def carrier_pwr(self): return self._carrier_pwr @property def pn_data(self): return self._pn_data def append_pn_data(self, pn_freq, pn_pwr, label=None): if not label: label = "PN-{}".format(len(self.pn_data)+1) data_item = PNDataItem(label, self.dut_freq, list(zip(pn_freq, pn_pwr)), self.lockbw, self.fmdev, self.sa_avg, self.sa_smoothing) self.pn_data.append(data_item) return self._pn_data def clear_pn_data(self): self._pn_data.clear() @asyncSlot() async def connect_dut_signal(self): msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Information) msgBox.setText("Connect DUT signal to SA input.") msgBox.setWindowTitle("Connect DUT Signal") msgBox.setStandardButtons(QMessageBox.Ok) returnValue = msgBox.exec() self._measure_dutf_btn.setEnabled(False) self._measure_if_btn.setEnabled(False) self._cal_btn.setEnabled(False) loop = asyncio.get_event_loop() f, pwr = await loop.run_in_executor(None, self.measure_signal, self.dut_freq) self._dutf_box.setValue(f) self._measure_dutf_btn.setEnabled(True) self._measure_if_btn.setEnabled(True) self._cal_btn.setEnabled(True) @asyncSlot() async def connect_if_signal(self): msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Information) msgBox.setText("Connect HP11729C IF output signal to SA input.") msgBox.setWindowTitle("Connect IF Signal") msgBox.setStandardButtons(QMessageBox.Ok) returnValue = msgBox.exec() print("band: {}, lockbw: {}".format(self.band, self.lockbw)) self.pn.band = self.band self.pn.lock_range = self.lockbw self._measure_dutf_btn.setEnabled(False) self._measure_if_btn.setEnabled(False) self._cal_btn.setEnabled(False) loop = asyncio.get_event_loop() f, pwr = await loop.run_in_executor(None, self.measure_signal, self.if_freq) self.if_freq = f print("if_freq: {:f}, pwr: {:f}".format(self.if_freq, pwr)) if pwr < -30.0: msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Warning) msgBox.setText("IF output power is {}. Check DUT signal.".format(pwr)) msgBox.setWindowTitle("Check DUT Signal") msgBox.setStandardButtons(QMessageBox.Ok) returnValue = msgBox.exec() self._ref_box.setText('{:.3f} MHz'.format(self.if_freq)) self._measure_dutf_btn.setEnabled(True) self._measure_if_btn.setEnabled(True) self._cal_btn.setEnabled(True) def set_ref_level(self, freq, span): self.sa.freq = freq self.sa.fspan = span while True: self.sa.sweep() pwr, f = self.sa.measure_pwr(freq) rl = self.sa.ref_level if rl > self.sa.max_ref_level - 5.0: break if pwr > rl: self.sa.ref_level = rl + 10 elif abs(rl - pwr) > 10.0: self.sa.ref_level = rl - 10 else: break return f, pwr def measure_signal(self, freq, span=2.0): f, pwr = self.set_ref_level(freq, span) self.sa.fspan = 0.05 self.sa.sweep() try: pwr, f = self.sa.measure_pwr(f) self.sa.cmd('MKCF;') self.sa.fspan = 0.01 self.sa.sweep() pwr, f = self.sa.measure_pwr(f) except ValueError as ve: print(str(ve)) return f, pwr @asyncSlot() async def calibrate(self): msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Information) msgBox.setWindowTitle("Connect uW Signal") msgBox.setTextFormat(Qt.RichText) msgBox.setText("""<p>Connect HP11729C < 10 MHz output to SA input.</p><p><span style="color: red;">IMPORTANT: Use a 100uF DC block on the SA input!</span></p>""") msgBox.setStandardButtons(QMessageBox.Ok) returnValue = msgBox.exec() self.pn.band = self.band self.pn.lock_range = self.lockbw self.sa.cmd("COUPLE DC;") self.sg.cmd("FM:OFF") self.sg.set_signal_output(self.if_freq + PhaseNoiseApp.CAL_FREQ_OFFSET, PhaseNoiseApp.CAL_ATTEN) self._cal_btn.setEnabled(False) self._capture_btn.setEnabled(False) loop = asyncio.get_event_loop() f, pwr = await loop.run_in_executor(None, self.set_ref_level, PhaseNoiseApp.CAL_FREQ_OFFSET, 0.06) self.sa.cmd("COUPLE AC;") self._carrier_pwr = pwr self._ref_level = self.sa.ref_level self._atten = self.sa.atten self._capture_btn.setEnabled(True) self._cal_btn.setEnabled(True) print("f: {}, pwr: {}, sa.ref_level: {}".format(f, pwr, self.sa.ref_level)) @asyncSlot() async def capture(self): # 1. Set the reference signal (SMHU58) to self.if_freq and output # power of 0.0 # 2. Ensure that the HP11729C FREQ-CONT DC-FM output (back panel) is # connected to the SMHU58 FM EXT input (front panel) # 3. Initially, set the SMHU58 max FM deviation to 5 kHz # 4. Set the HP11729C initial lock bandwidth factor to 10 # 5. Set the HP11729C to 'capture' and sleep for some period # 6. Test the HP11729C for phase lock # 7. If phase lock set HP11729C for inactive capture and sleep for # some period else go to 10 # 8. Test the HP11729C for phase lock # 9. If phase lock set HP11729C capture inactive and set SA for # first measurement band and take sweep # 10. If no phase lock increment FM DEV by 1kHz and go to (5) # If FM DEV == 10kHz stop auto capture msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Information) msgBox.setWindowTitle("Connect uW Signal") msgBox.setTextFormat(Qt.RichText) msgBox.setText("""<p>Connect HP11729C < 10 MHz output to SA input.</p><p><span style="color: red;">IMPORTANT: Use a 100uF DC block on the SA input!</span></p>""") msgBox.setStandardButtons(QMessageBox.Ok) returnValue = msgBox.exec() self._capture_btn.setEnabled(False) self.pn.band = self.band self.pn.lock_range = self.lockbw self.sg.set_signal_output(self.if_freq, 0.0) self.sg.cmd("FM:EXT:DC {}".format(self.fmdev)) self._capture_btn.setEnabled(True) msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Information) msgBox.setWindowTitle("Initiate Phase Lock") msgBox.setTextFormat(Qt.RichText) msgBox.setText("""<p>Place the HP11729C in 'Local' mode and then press and release the 'Capture' button.</p><p>After a short period of time the Phase lock indicator above the Capture button should indicate phase lock by illuminating the green LED in the center of the display. If this doesn't occur some adjustments to the Phase Locking parameters may be required.</p>""") msgBox.setStandardButtons(QMessageBox.Ok) returnValue = msgBox.exec() def sweep_band(self, start, stop, rbw): self.sa.cmd("COUPLE DC;") self.sa.vbr = self.sa_smoothing self.sa.stop_freq = stop / 1e6 stopFreq = self.sa.stop_freq self.sa.start_freq = start / 1e6 startFreq = self.sa.start_freq if rbw > 0: self.sa.rbw = rbw else: self.sa.rbw_auto = True reflevel = self.sa.ref_level ampl_scale = self.sa.scale self.sa.vavg = self.sa_avg self.sa.sweep() if isinstance(self.sa, HP8560A): raw_data = np.array(self.sa.trace_raw_data()) sdata = reflevel + (ampl_scale * (raw_data - 600)/60) else: sdata = np.array(self.sa.trace_data()) self.sa.vavg = 1 res_bw = self.sa.rbw self.sa.cmd("COUPLE AC;") correction = - self.carrier_pwr + PhaseNoiseApp.CAL_ATTEN \ - PhaseNoiseApp.Lf_CONV_FACTOR \ - (10.0 * log10(1.2 * res_bw)) + PhaseNoiseApp.LOG_CONV_FACTOR band_pn = sdata band_pn += correction band_freq = np.linspace(startFreq, stopFreq, len(band_pn)) band_freq *= 1e6 # Frequency in Hz return band_pn, band_freq def show_measure_dialog(self): self._pnmeas_dialog = PlotDialog(self) self._pnmeas_dialog.show() @asyncClose async def closeEvent(self, event): if self.pn: self.pn.reset() self.pn.local() if self.sg: self.sg.reset() self.sg.local() if self.sa: self.sa.reset() self.sa.local() if self.gpib: self.gpib.close()
Measure DUT signal
@asyncSlot() async def connect_dut_signal(self): msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Information) msgBox.setText("Connect DUT signal to SA input.") msgBox.setWindowTitle("Connect DUT Signal") msgBox.setStandardButtons(QMessageBox.Ok) returnValue = msgBox.exec() self._measure_dutf_btn.setEnabled(False) self._measure_if_btn.setEnabled(False) self._cal_btn.setEnabled(False) loop = asyncio.get_event_loop() f, pwr = await loop.run_in_executor(None, self.measure_signal, self.dut_freq) self._dutf_box.setValue(f) self._measure_dutf_btn.setEnabled(True) self._measure_if_btn.setEnabled(True) self._cal_btn.setEnabled(True) @asyncSlot() async def connect_if_signal(self): msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Information) msgBox.setText("Connect HP11729C IF output signal to SA input.") msgBox.setWindowTitle("Connect IF Signal") msgBox.setStandardButtons(QMessageBox.Ok) returnValue = msgBox.exec() print("band: {}, lockbw: {}".format(self.band, self.lockbw)) self.pn.band = self.band self.pn.lock_range = self.lockbw self._measure_dutf_btn.setEnabled(False) self._measure_if_btn.setEnabled(False) self._cal_btn.setEnabled(False) loop = asyncio.get_event_loop() f, pwr = await loop.run_in_executor(None, self.measure_signal, self.if_freq) self.if_freq = f print("if_freq: {:f}, pwr: {:f}".format(self.if_freq, pwr)) if pwr < -30.0: msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Warning) msgBox.setText("IF output power is {}. Check DUT signal.".format(pwr)) msgBox.setWindowTitle("Check DUT Signal") msgBox.setStandardButtons(QMessageBox.Ok) returnValue = msgBox.exec() self._ref_box.setText('{:.3f} MHz'.format(self.if_freq)) self._measure_dutf_btn.setEnabled(True) self._measure_if_btn.setEnabled(True) self._cal_btn.setEnabled(True) def set_ref_level(self, freq, span): self.sa.freq = freq self.sa.fspan = span while True: self.sa.sweep() pwr, f = self.sa.measure_pwr(freq) rl = self.sa.ref_level if rl > self.sa.max_ref_level - 5.0: break if pwr > rl: self.sa.ref_level = rl + 10 elif abs(rl - pwr) > 10.0: self.sa.ref_level = rl - 10 else: break return f, pwr def measure_signal(self, freq, span=2.0): f, pwr = self.set_ref_level(freq, span) self.sa.fspan = 0.05 self.sa.sweep() try: pwr, f = self.sa.measure_pwr(f) self.sa.cmd('MKCF;') self.sa.fspan = 0.01 self.sa.sweep() pwr, f = self.sa.measure_pwr(f) except ValueError as ve: print(str(ve)) return f, pwr
Calibration
The calibration proceeds as follows:
Ensure that the
< 10 MHz
output from the HP11729C is connected to the spectrum analyzer via a \(100\mu F\) DC block.Ensure that the FM modulation facility is disabled on the reference signal generator.
Set the output of the reference signal generator to the previously measured/calculated IF frequency plus an offset of 50 kHz.
Ensure that the spectrum analyzer input coupling is DC.
Set the signal generator output level to be -40 dBm.
Set the spectrum analyzer reference level so that the reference signal is just below the top of the SA screen. Measure the frequency and power of the reference level.
@asyncSlot() async def calibrate(self): msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Information) msgBox.setWindowTitle("Connect uW Signal") msgBox.setTextFormat(Qt.RichText) msgBox.setText("""<p>Connect HP11729C < 10 MHz output to SA input.</p><p><span style="color: red;">IMPORTANT: Use a 100uF DC block on the SA input!</span></p>""") msgBox.setStandardButtons(QMessageBox.Ok) returnValue = msgBox.exec() self.pn.band = self.band self.pn.lock_range = self.lockbw self.sa.cmd("COUPLE DC;") self.sg.cmd("FM:OFF") self.sg.set_signal_output(self.if_freq + PhaseNoiseApp.CAL_FREQ_OFFSET, PhaseNoiseApp.CAL_ATTEN) self._cal_btn.setEnabled(False) self._capture_btn.setEnabled(False) loop = asyncio.get_event_loop() f, pwr = await loop.run_in_executor(None, self.set_ref_level, PhaseNoiseApp.CAL_FREQ_OFFSET, 0.06) self.sa.cmd("COUPLE AC;") self._carrier_pwr = pwr self._ref_level = self.sa.ref_level self._atten = self.sa.atten self._capture_btn.setEnabled(True) self._cal_btn.setEnabled(True) print("f: {}, pwr: {}, sa.ref_level: {}".format(f, pwr, self.sa.ref_level))
Capture
@asyncSlot() async def capture(self): # 1. Set the reference signal (SMHU58) to self.if_freq and output # power of 0.0 # 2. Ensure that the HP11729C FREQ-CONT DC-FM output (back panel) is # connected to the SMHU58 FM EXT input (front panel) # 3. Initially, set the SMHU58 max FM deviation to 5 kHz # 4. Set the HP11729C initial lock bandwidth factor to 10 # 5. Set the HP11729C to 'capture' and sleep for some period # 6. Test the HP11729C for phase lock # 7. If phase lock set HP11729C for inactive capture and sleep for # some period else go to 10 # 8. Test the HP11729C for phase lock # 9. If phase lock set HP11729C capture inactive and set SA for # first measurement band and take sweep # 10. If no phase lock increment FM DEV by 1kHz and go to (5) # If FM DEV == 10kHz stop auto capture msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Information) msgBox.setWindowTitle("Connect uW Signal") msgBox.setTextFormat(Qt.RichText) msgBox.setText("""<p>Connect HP11729C < 10 MHz output to SA input.</p><p><span style="color: red;">IMPORTANT: Use a 100uF DC block on the SA input!</span></p>""") msgBox.setStandardButtons(QMessageBox.Ok) returnValue = msgBox.exec() self._capture_btn.setEnabled(False) self.pn.band = self.band self.pn.lock_range = self.lockbw self.sg.set_signal_output(self.if_freq, 0.0) self.sg.cmd("FM:EXT:DC {}".format(self.fmdev)) self._capture_btn.setEnabled(True) msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Information) msgBox.setWindowTitle("Initiate Phase Lock") msgBox.setTextFormat(Qt.RichText) msgBox.setText("""<p>Place the HP11729C in 'Local' mode and then press and release the 'Capture' button.</p><p>After a short period of time the Phase lock indicator above the Capture button should indicate phase lock by illuminating the green LED in the center of the display. If this doesn't occur some adjustments to the Phase Locking parameters may be required.</p>""") msgBox.setStandardButtons(QMessageBox.Ok) returnValue = msgBox.exec()
Application user interface
def enable_ui(self, s): self._dut_gbox.setEnabled(s) self._bb_gbox.setEnabled(s) self._meas_gbox.setEnabled(s) def build_ui(self): """Build the on-screen UI for the phase noise app.""" vbox = QVBoxLayout() hbox = QHBoxLayout() gpib_combo = QComboBox() gpib_combo.addItems(PhaseNoiseApp.GPIB_CONTROLLER_LIST) gpib_combo.currentIndexChanged.connect( lambda idx, w=gpib_combo: self.gpib_changed(w, idx)) line_edit = QLineEdit() gpib_combo.setLineEdit(line_edit) hbox.addWidget(QLabel("GPIB Controller:")) hbox.addWidget(gpib_combo) hbox.addStretch(1) initialize_btn = QPushButton("Initialize") initialize_btn.clicked.connect(self.initialize) hbox.addWidget(initialize_btn) vbox.addLayout(hbox) self._dut_gbox = QGroupBox("DUT and Reference") vbox2 = QVBoxLayout() self._dut_gbox.setLayout(vbox2) hbox2 = QHBoxLayout() self._dutf_box = QDoubleSpinBox() self._dutf_box.setRange(PhaseNoiseApp.MIN_DUT_FREQUENCY, PhaseNoiseApp.MAX_DUT_FREQUENCY) self._dutf_box.setDecimals(3) self._dutf_box.setStyleSheet("""QDoubleSpinBox { font-size: 25pt; }""") self._dutf_box.setValue(PhaseNoiseApp.DEFAULT_DUT_FREQUENCY) self._dutf_box.setSuffix(PhaseNoiseApp.FREQ_SUFFIX) self._dutf_box.valueChanged.connect(self.set_dut_freq) hbox2.addWidget(QLabel("DUT Freq:")) hbox2.addWidget(self._dutf_box) hbox2.addStretch(1) self._measure_dutf_btn = QPushButton("Measure DUT Sig") self._measure_dutf_btn.clicked.connect(self.connect_dut_signal) hbox2.addWidget(self._measure_dutf_btn) vbox2.addLayout(hbox2) hbox2 = QHBoxLayout() self._ref_box = QLabel() self._ref_box.setStyleSheet("""QLabel { font-size: 25pt; }""") self._ref_box.setText('{:.3f} MHz'.format(self.if_freq)) hbox2.addWidget(QLabel("Ref. Freq:")) hbox2.addWidget(self._ref_box) hbox2.addStretch(1) self._measure_if_btn = QPushButton("Measure IF Sig") self._measure_if_btn.clicked.connect(self.connect_if_signal) hbox2.addWidget(self._measure_if_btn) vbox2.addLayout(hbox2) hbox2 = QHBoxLayout() self._cal_btn = QPushButton("Calibrate") self._cal_btn.clicked.connect(self.calibrate) hbox2.addWidget(self._cal_btn) hbox2.addStretch(1) vbox2.addLayout(hbox2) vbox.addWidget(self._dut_gbox) self._bb_gbox = QGroupBox("Baseband") fbox1 = QFormLayout() self._bb_gbox.setLayout(fbox1) hbox = QHBoxLayout() offsetStartGroup = QButtonGroup(hbox) self._offset_start_btns = [] for offset in PhaseNoiseApp.OFFSET_START.keys(): rb = QRadioButton("{}".format(offset)) rb.offset = offset rb.toggled.connect( lambda state, w=rb: self.set_offset_start(state, w.offset)) offsetStartGroup.addButton(rb) self._offset_start_btns.append(rb) hbox.addWidget(rb) self._offset_start_btns[0].setChecked(True) fbox1.addRow(QLabel('Offset Start Freq:'), hbox) hbox = QHBoxLayout() offsetStopGroup = QButtonGroup(hbox) self._offset_stop_btns = [] for offset in PhaseNoiseApp.OFFSET_END.keys(): rb = QRadioButton("{}".format(offset)) rb.offset = offset rb.toggled.connect( lambda state, w=rb: self.set_offset_stop(state, w.offset)) offsetStopGroup.addButton(rb) self._offset_stop_btns.append(rb) hbox.addWidget(rb) self._offset_stop_btns[-1].setChecked(True) fbox1.addRow(QLabel('Offset Stop Freq:'), hbox) hbox = QHBoxLayout() smoothingGroup = QButtonGroup(hbox) self._sa_smooth_btns = [] for ratio in PhaseNoiseApp.SA_SMOOTHING.keys(): rb = QRadioButton("{}".format(ratio)) rb.ratio = ratio rb.toggled.connect( lambda state, w=rb: self.set_sa_smoothing(state, w.ratio)) smoothingGroup.addButton(rb) self._sa_smooth_btns.append(rb) hbox.addWidget(rb) self._sa_smooth_btns[3].setChecked(True) fbox1.addRow(QLabel("SA Smoothing Ratio:"), hbox) self._sa_avg_box = QSpinBox() self._sa_avg_box.setRange(1, 999) self._sa_avg_box.valueChanged.connect(self.set_sa_avg) self._sa_avg_box.setValue(10) fbox1.addRow(QLabel("SA Averaging:"), self._sa_avg_box) vbox.addWidget(self._bb_gbox) self._meas_gbox = QGroupBox("Phase Locking") vbox2 = QVBoxLayout() fbox1 = QFormLayout() vbox2.addLayout(fbox1) self._meas_gbox.setLayout(vbox2) hbox = QHBoxLayout() methodGroup = QButtonGroup(hbox) self._method_btns = [] for method in ["Phase Det.", "Manual Lock", "Freq. Discrim."]: rb = QRadioButton("{}".format(method)) rb.method = method rb.toggled.connect( lambda state, w=rb: self.set_method(state, w.method)) methodGroup.addButton(rb) self._method_btns.append(rb) hbox.addWidget(rb) self._method_btns[0].setChecked(True) self._method_btns[2].setEnabled(False) fbox1.addRow(QLabel("Method:"), hbox) hbox = QHBoxLayout() lockbwGroup = QButtonGroup(hbox) self._lockbw_btns = [] for bw in PhaseNoiseApp.LOCK_BANDWIDTHS.keys(): rb = QRadioButton(bw) rb.bw = PhaseNoiseApp.LOCK_BANDWIDTHS[bw] rb.toggled.connect( lambda state, w=rb: self.set_lockbw(state, w.bw)) lockbwGroup.addButton(rb) self._lockbw_btns.append(rb) hbox.addWidget(rb) self._lockbw_btns[1].setChecked(True) fbox1.addRow(QLabel("Lock BW Factor:"), hbox) self._fmdev_box = QDoubleSpinBox() self._fmdev_box.setRange(*PhaseNoiseApp.FMDEV_RANGE) self._fmdev_box.setDecimals(1) self._fmdev_box.setSingleStep(1000) self._fmdev_box.valueChanged.connect(self.set_fmdev) self._fmdev_box.setValue(PhaseNoiseApp.DEFAULT_FMDEV) fbox1.addRow(QLabel("FM Deviation:"), self._fmdev_box) hbox = QHBoxLayout() self._capture_btn = QPushButton("Capture") self._capture_btn.clicked.connect(self.capture) hbox.addWidget(self._capture_btn) hbox.addStretch(1) vbox2.addLayout(hbox) vbox.addWidget(self._meas_gbox) self.enable_ui(False) hbox = QHBoxLayout() self._meas_btn = QPushButton("Measurements...") self._meas_btn.clicked.connect(self.show_measure_dialog) hbox.addWidget(self._meas_btn) hbox.addStretch(1) vbox.addLayout(hbox) self.container = QWidget() self.container.setLayout(vbox) self.setCentralWidget(self.container) self.setWindowTitle('Phase Noise Measurement')
Measure and display
def sweep_band(self, start, stop, rbw): self.sa.cmd("COUPLE DC;") self.sa.vbr = self.sa_smoothing self.sa.stop_freq = stop / 1e6 stopFreq = self.sa.stop_freq self.sa.start_freq = start / 1e6 startFreq = self.sa.start_freq if rbw > 0: self.sa.rbw = rbw else: self.sa.rbw_auto = True reflevel = self.sa.ref_level ampl_scale = self.sa.scale self.sa.vavg = self.sa_avg self.sa.sweep() if isinstance(self.sa, HP8560A): raw_data = np.array(self.sa.trace_raw_data()) sdata = reflevel + (ampl_scale * (raw_data - 600)/60) else: sdata = np.array(self.sa.trace_data()) self.sa.vavg = 1 res_bw = self.sa.rbw self.sa.cmd("COUPLE AC;") correction = - self.carrier_pwr + PhaseNoiseApp.CAL_ATTEN \ - PhaseNoiseApp.Lf_CONV_FACTOR \ - (10.0 * log10(1.2 * res_bw)) + PhaseNoiseApp.LOG_CONV_FACTOR band_pn = sdata band_pn += correction band_freq = np.linspace(startFreq, stopFreq, len(band_pn)) band_freq *= 1e6 # Frequency in Hz return band_pn, band_freq
class PlotDialog(QDialog): """ """ X_MAJOR_TICKS = [(2, '100'), (3, '1K'), (4, '10K'), (5, '100K'), (6, '1M'), (7, '10M')] X_MINOR_TICKS = [(log10(n), '') for n in np.arange(2e2, 1e3, 1e2)] + \ [(log10(n), '') for n in np.arange(2e3, 1e4, 1e3)] + \ [(log10(n), '') for n in np.arange(2e4, 1e5, 1e4)] + \ [(log10(n), '') for n in np.arange(2e5, 1e6, 1e5)] + \ [(log10(n), '') for n in np.arange(2e6, 1e7, 1e6)] PLOT_COLORS = [(52,138,189), (183,67,49), (142,186,66), (251,193,94), (152,142,213), (255,181,184), (119,119,119)] def __init__(self, pnApp, parent=None): super().__init__(parent) self._app = pnApp self.setWindowTitle("Phase Noise Plots") self.build_ui() self.plot_pn_data_items() def lowpass_filter(self, wn, arr): i, u = signal.butter(3, wn, btype='lowpass') filtered = signal.filtfilt(i, u, arr) return filtered def filter_ps_harmonics(self, wn, arr): # Note that the indices used here will depend crucially # on the band start and stop frequencies as well as the # RBW used to acquire the data. # The current numbers are for the following: # start: 100 Hz # stop: 2.0 kHz # rbw: 10 Hz for i in range(8, 25): arr[i] = np.average(arr[i-5:i]) for i in range(8, 25): arr[i] = np.average(arr[i:i+5]) for i in range(37, 58): arr[i] = np.average(arr[i-5:i]) for i in range(37, 58): arr[i] = np.average(arr[i:i+5]) for i in range(73, 90): arr[i] = np.average(arr[i-5:i]) for i in range(73, 90): arr[i] = np.average(arr[i:i+5]) # Apply a low pass filter to attenuate remaining # power supply harmonics return self.lowpass_filter(wn, arr) @asyncSlot() async def acquire_pn_sweep(self): start_band = int(log10(self._app.offset_start)) - 2 stop_band = int(log10(self._app.offset_stop)) - 3 loop = asyncio.get_event_loop() pn_pwr = np.array([]) pn_freq = np.array([]) # Create an empty PlotDataItem... plot = self._pn_widget.plot() plot.setPen(pg.mkPen(PlotDialog.PLOT_COLORS[ len(self._app._pn_data) % len(PlotDialog.PLOT_COLORS)])) self.acquire_enable(False) for band in range(start_band, stop_band+1): f_start = PhaseNoiseApp.PN_BANDS[band][0] f_stop = PhaseNoiseApp.PN_BANDS[band][1] rbw = PhaseNoiseApp.PN_BANDS[band][2] wn = PhaseNoiseApp.PN_BANDS[band][3] print("f_start={}, f_stop={}, rbw={}".format( f_start, f_stop, rbw), flush=True) band_pwr, band_freq = await loop.run_in_executor( None, self._app.sweep_band, f_start, f_stop, rbw) if band == 0: # Filter out the power supply harmonics at 150 and 250Hz band_pwr = self.filter_ps_harmonics(wn, band_pwr) else: band_pwr = self.lowpass_filter(wn, band_pwr) if len(pn_freq) > 0 and band_freq[0] == pn_freq[-1]: pn_pwr = np.concatenate([pn_pwr, band_pwr[1:]]) pn_freq = np.concatenate([pn_freq, band_freq[1:]]) else: pn_pwr = np.concatenate([pn_pwr, band_pwr]) pn_freq = np.concatenate([pn_freq, band_freq]) plot.setData(np.log10(pn_freq), pn_pwr) pn_data = self._app.append_pn_data(pn_freq, pn_pwr) self._app.sa.cmd("IP;SNGLS;") self.acquire_enable(True) def acquire_enable(self, en): self._acquire_btn.setEnabled(en) self._clear_btn.setEnabled(en) self._load_btn.setEnabled(en) self._save_btn.setEnabled(en) self._bbox.button(QDialogButtonBox.Close).setEnabled(en) def clear_pn_sweep(self): self._pn_widget.clear() def load_pn_data(self): filter_str = "JSON Data (*.json)" name, _ = QFileDialog.getOpenFileName(self, 'Open file', '', filter_str) if name: filename = str(name).strip() if len(filename): with open(filename, 'r') as fd: self._app._pn_data = [PNDataItem.from_json(pn) for pn in json.load(fd)] self.plot_pn_data_items() def save_pn_data(self): filter_str = "JSON Data (*.json)" name, _ = QFileDialog.getSaveFileName(self, 'Save configuration', '', filter_str) if name: output_file = str(name).strip() if len(output_file) == 0: return with open(output_file, 'w') as fd: json.dump(self._app.pn_data, fd, indent=4, default=lambda o: o.__dict__) def close(self): QDialog.reject(self) def build_ui(self): vbox = QVBoxLayout() pg.setConfigOptions(foreground='k') self._pn_widget = pg.PlotWidget(name='Plot1', background='w', enableMenu=True) self._pn_widget.getPlotItem().showAxis('right', True) self._pn_widget.getAxis('right').setStyle(showValues=False) self._pn_widget.getPlotItem().showAxis('top', True) self._pn_widget.getAxis('top').setStyle(showValues=False) x_start = log10(self._app.offset_start) x_stop = log10(self._app.offset_stop) self._pn_widget.setRange(yRange=(-170, -40), xRange=(x_start, x_stop)) self._pn_widget.setMouseEnabled(False, False) self._pn_widget.getPlotItem().showGrid(True, True, alpha=0.15) tickFont = QFont("Times", 14) labelFont = QFont("Times", 16) x_axis = self._pn_widget.getAxis('bottom') x_axis.setStyle(tickFont=tickFont) x_ticks = [ PlotDialog.X_MAJOR_TICKS[int(x_start)-2:int(x_stop)-1], PlotDialog.X_MINOR_TICKS[(int(x_start)-2)*8:(int(x_stop)-2)*8] ] x_axis.setTicks(x_ticks) x_axis.setLabel("Frequency (Hz)") x_axis.label.setFont(labelFont) y_axis = self._pn_widget.getAxis('left') y_axis.setStyle(tickFont=tickFont) y_axis.setLabel("Phase Noise (dBc/Hz)") y_axis.label.setFont(labelFont) vbox.addWidget(self._pn_widget) self._bbox = QDialogButtonBox(QDialogButtonBox.Close) self._acquire_btn = QPushButton("Acquire") self._clear_btn = QPushButton("Clear") self._load_btn = QPushButton("Load...") self._save_btn = QPushButton("Save...") self._bbox.addButton(self._acquire_btn, QDialogButtonBox.ActionRole) self._bbox.addButton(self._clear_btn, QDialogButtonBox.ActionRole) self._bbox.addButton(self._load_btn, QDialogButtonBox.ActionRole) self._bbox.addButton(self._save_btn, QDialogButtonBox.ActionRole) self._acquire_btn.clicked.connect(self.acquire_pn_sweep) self._clear_btn.clicked.connect(self.clear_pn_sweep) self._load_btn.clicked.connect(self.load_pn_data) self._save_btn.clicked.connect(self.save_pn_data) self._bbox.rejected.connect(self.close) vbox.addWidget(self._bbox) self.setLayout(vbox) def plot_pn_data_items(self): if len(self._app.pn_data) > 0: for i, pn_data in enumerate(self._app.pn_data): plot = self._pn_widget.plot() plot.setPen(pg.mkPen(PlotDialog.PLOT_COLORS[ i % len(PlotDialog.PLOT_COLORS)])) plot.setData(np.log10(pn_data.freq), pn_data.pwr)
Data storage
class PNDataItem(object): """Stores phase noise data""" def __init__(self, label, carrier_freq, data, lock_bw, fm_dev, sa_avg, sa_smoothing, descr=''): self._label = label self._carrier_freq = carrier_freq self._pn_data = data.copy() self._lock_bw = lock_bw self._fm_dev = fm_dev self._sa_avg = sa_avg self._sa_smoothing = sa_smoothing self._descr = descr @classmethod def from_json(cls, data): return cls(data['_label'], data['_carrier_freq'], data['_pn_data'], data['_lock_bw'], data['_fm_dev'], data['_sa_avg'], data['_sa_smoothing'], data['_descr']) @property def label(self): return self._label @property def carrier_freq(self): return self.carrier_freq @property def pwr(self): """Phase noise power data (in dBc)""" return [t[1] for t in self._pn_data] @property def freq(self): """Phase noise frequency data (in Hz)""" return [t[0] for t in self._pn_data] @property def lock_bw(self): """HP11729C lock BW factor used when acquiring data""" return self._lock_pw @property def fmdev(self): """Peak FM deviation of ref. signal gen used when acquiring data""" return self._fm_dev @property def sa_avg(self): """Number of successive SA traces averaged when acquiring data""" return self._sa_avg @property def sa_smoothing(self): """SA video BW to resolution BW used when acquiring data""" return self._sa_smoothing @property def description(self): return self._descr @description.setter def description(self, descr): self._descr = descr
Initialization
@asyncSlot() async def initialize(self): try: self.initialize_ui() loop = asyncio.get_event_loop() await loop.run_in_executor(None, self.initialize_hw) self.enable_ui(True) except RuntimeError as re: error_dialog = QErrorMessage(self) error_dialog.showMessage(str(re)) def initialize_ui(self): pass def initialize_hw(self): try: self.gpib = GPIB(gpib_ip=self.gpib_ctl_addr) self.gpib.initialize() except socket.timeout as te: self.gpib = None raise RuntimeError('GPIB controller is offline or powered down') try: self.sa = HP8560A(self.gpib, HP8560A_GPIBID) sa_id = self.sa.ident self.sa.initialize() except socket.timeout as te: self.sa = None raise RuntimeError('HP8560A is offline or powered down') try: self.sg = SMHU58(self.gpib, SMHU58_GPIBID) sg_id = self.sg.ident self.sg.initialize() except socket.timeout as te: self.sg = None raise RuntimeError('SMHU58 is offline or powered down') try: self.pn = HP11729C(self.gpib, HP11729C_GPIBID) #pn_id = self.pn.ident self.pn.initialize() except socket.timeout as te: self.pn = None raise RuntimeError('HP11729C is offline or powered down')
Process command line arguments
parser = ArgumentParser(description= '''Measure phase noise.''') parser.add_argument("--sg", choices=['SMHU58', 'DSG815'], default="SMHU58", help="Reference signal generator in use. Default=SMHU58") parser.add_argument("-H", "--harmonics", action='store_true', help="Don't suppress power supply harmonics at 150 and 250Hz") args = parser.parse_args()
Imports
import sys import copy import time from math import sqrt, log10 from enum import Enum import socket import asyncio from argparse import ArgumentParser import json import numpy as np from scipy import signal from tam import ( GPIB, PROLOGIX_IPADDR, HP8560A, HP8560A_GPIBID, HP11729C, HP11729C_GPIBID, SMHU58, SMHU58_GPIBID ) from qasync import ( QEventLoop, QThreadExecutor, asyncSlot, asyncClose ) from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont from PyQt5.QtWidgets import ( QWidget, QLabel, QAbstractSpinBox, QDoubleSpinBox, QVBoxLayout, QLineEdit, QHBoxLayout, QGroupBox, QMainWindow, QComboBox, QCheckBox, QPushButton, QRadioButton, QButtonGroup, QMessageBox, QFormLayout, QErrorMessage, QApplication, QScrollArea, QMenu, QFileDialog, QSpinBox, QDialog, QDialogButtonBox, QAction, QActionGroup, QStyleFactory ) import pyqtgraph as pg