measure-cal-data.py
The Script
Initially, the script sets a number of default arguments and processes the
command line. The main
function is then invoked passing the specified
command arguments or their defaults.
# # Generated from ad9913-measure-cal-data.org # <<imports>> <<globals>> <<serial-comms>> <<plot-fits>> <<plot-response-params>> <<main>> if __name__ == '__main__': defaultDevice = '/dev/tty.usbmodem14101' defaultBaud = '1500000' defaultOutfile = 'ad9913-cal-data.json' defaultCalplotfile = 'ad9913-cal-data.svg' defaultParamplotfile = 'ad9913-response-params.svg' parser = ArgumentParser(description= '''Calibrate AD9913 module output power.''') parser.add_argument("-d", "--device", default=defaultDevice, help="The serial device (default: {})".format(defaultDevice)) parser.add_argument("-C", "--cspin", default='D0', help="Chip select controller pin (default: D0)") parser.add_argument("-U", "--ioupdate", default='C4', help="DDS IO Update controller pin (default: C4)") parser.add_argument("-R", "--reset", help="DDS reset controller pin") parser.add_argument("-S", "--sa", choices=['DSA815', 'HP8560A'], default='DSA815', help="""The spectrum analyzer in use for the calibration. Default: DSA815""") parser.add_argument("-O", "--outfile", default=defaultOutfile, help="""Output file for measured calibration data. Default: {}""".format(defaultOutfile)) parser.add_argument("-I", "--infile", help="""Input data file for generating calibration data plots.""") parser.add_argument("--calplots", default=defaultCalplotfile, help="""Output file for calibration data plots. Default: {}""".format(defaultCalplotfile)) parser.add_argument("--paramplots", default=defaultParamplotfile, help="""Ouput file for response parameter plots. Default: {}""".format(defaultParamplotfile)) args = parser.parse_args() main(args.device, args.cspin, args.ioupdate, args.reset, args.sa, args.outfile, args.calplots, args.paramplots, args.infile)
The main
Function: <<main>>
The main
function proceeds in five stages:
Initialize the spectrum analyzer.
Initialize the DDS under test.
Take the output power measurements.
Fit lines to the linear portions of the output power for each of the output signal frequencies.
Save measured data and associated linear fits to a file.
def main(serdev, cspin, ioupdate, reset, sa_model, outfile, calplotfile, paramplotfile, infile): init_style() if infile: with open(infile, 'r') as fd: d = json.load(fd) #measured_data = {float(k): v for (k, v) in d['measured_data'].items()} #linear_fits = {float(k): v for (k, v) in d['linear_fits'].items()} plot_fits(d['measured_data'], d['linear_fits'], calplotfile) plot_response_params(d['linear_fits'], paramplotfile) return <<initialize-sa>> <<initialize-dds>> <<take-measurements>> <<fit-measurements>> data_out = {"measured_data" : vout_vs_freq, "linear_fits" : linear_fits} with open(outfile, 'w') as fd: json.dump(data_out, fd) plot_fits(vout_vs_freq, linear_fits, calplotfile) plot_response_params(linear_fits, paramplotfile)
The data is saved as a JSON formatted dictionary containing two entries for the measured data and linear fits. The format of the saved data is illustrated in the following code snippet:
import json with open('measuredData/ad9913/ad9913-cal-data.json') as fd: d = json.load(fd) # The measured voltages are lists keyed with the input signal frequency print(d['measured_data'].keys()) dict_keys(['5.0', '10.0', '15.0', '20.0', '25.0', '30.0', '35.0', '40.0', '45.0', '50.0', '55.0', '60.0', '65.0', '70.0', '75.0', '80.0', '85.0', '90.0', '95.0', '100.0']) # Peak signal output voltages are measured for each of the DDS DAC codes print(d['measured_data']['50.0']) [35.57821, 69.96692999999999, 104.4203, 139.1116, 173.3624, 207.709, 241.6632, 275.8893, 309.3714, 343.5909, 378.3162, 411.24699999999996, 445.5399, 480.29159999999996, 513.7374, 546.6367] # The linear fits are lists keyed with the output signal frequency print(d['linear_fits'].keys()) dict_keys(['5.0', '10.0', '15.0', '20.0', '25.0', '30.0', '35.0', '40.0', '45.0', '50.0', '55.0', '60.0', '65.0', '70.0', '75.0', '80.0', '85.0', '90.0', '95.0', '100.0']) # Linear fit data is saved in the following format: # # [slope, intercept, slope std. dev., intercept std. dev.] print(d['linear_fits']['50.0']) [0.532739564796775, 2.466679250003161, 0.0005501108480686453, 0.34043641607701375]
Initialize the spectrum analyzer: <<initialize-sa>>
The script currently supports either the Rigol DSA815 or the HP8560A. Depending on the which one is being used either GPIB or VISA is used for instrument communication and command.
if sa_model == 'DSA815': visa_rm = pyvisa.ResourceManager('@py') sa = DSA815(visa_rm, DSA815_ID) sa.initialize() elif sa_model == 'HP8560A': gpib = GPIB() gpib.initialize() sa = HP8560A(gpib, HP8560A_GPIBID) sa.initialize() else: print("ERROR: Unknown spectrum analyzer model: {}".format(sa_model)) sys.exit(-1) sa.vavg = 2 sa.ref_level = 10.0
Serial communication: <<serial-comms>>
Initialize the DDS: <<initialize-dds>>
Take measurements: <<take-measurements>>
For each of the DDS outout power levels specified in output_levels
measure
the actual DDS ouput power at each frequency specified in the
frequencies
list. The output power levels as reported by the
spectrum analyzer are saved in measured_levels
.
measured_levels = {} sa.unit = 'V' sa.fspan = 0.1 # MHz for freq in frequencies: print('{}: '.format(freq), end='', flush=True) cmd = dds.config_tuning_word(dds.tuning_word(float(freq))) write_dds_cmd(serdev, cmd) sleep(0.5) for lvl in output_levels: cmd = dds.config_output_level(lvl) write_dds_cmd(serdev, cmd) sleep(0.5) measured_vout, measured_freq = sa.measure_pwr(float(freq)) measured_vout *= 1e3 print(' {:5.2f}'.format(measured_vout), end='', flush=True) try: measured_levels[freq].append(measured_vout) except KeyError: measured_levels[freq] = [measured_vout] print() sa.unit = 'DBM' if sa_model == 'DSA815': visa_rm.close() else: gpib.close()
Fit measurements: <<fit-measurements>>
Lines are fitted to the DDS output response for each of the input
signal frequencies. The scipy
optimize.curve_fit
function is
used to fit a linear function to the response. The curve_fit
function returns the slope and intercept of the best fit line
together with the 2x2 array of covariances for the fit parameters.
One standard deviation errors for the fit parameters are calculated
from the trace of the covariances array.
Plot calibration data fits: <<plot-fits>>
Plot the measured data points together with the associated linear fits for each of the output signal frequencies.
def linear_func(x, a, b): return a*x + b def plot_fits(measured_data, linear_fits, plotfile): fig = plt.figure(num=None, figsize=(6.0, 8.5), dpi=72) for idx, freq in enumerate(frequencies): vout = measured_data[freq] slope = linear_fits[freq][0] intercept = linear_fits[freq][1] if idx == 0: ax = fig.add_subplot(5, 4, idx+1) sharey_ax = ax else: ax = fig.add_subplot(5, 4, idx+1, sharey=sharey_ax) if (idx+1) % 4 == 1: plt.setp(ax.get_yticklabels(), visible=True) else: plt.setp(ax.get_yticklabels(), visible=False) ax.plot(output_levels, vout, marker='o') ax.plot(output_levels, linear_func(np.array(output_levels), slope, intercept)) ax.text(250, 500, '{}'.format(freq)) fig.tight_layout() plt.savefig(plotfile)
Plot response parameters
def plot_response_params(linear_fits, plotfile): freq_array = np.array([float(f) for f in frequencies]) slopes = [v[0] for v in linear_fits.values()] intercepts = [v[1] for v in linear_fits.values()] fig = plt.figure(num=None, figsize=(8.0, 4.0), dpi=72) ax1 = fig.add_subplot(1, 2, 1) ax1.set_ylabel('Slope (mV/DAC unit)') ax1.set_xlabel('Frequency (MHz)') ax1.plot(freq_array, slopes) ax2 = fig.add_subplot(1, 2, 2) ax2.set_ylabel('Intercept (mV)') ax2.set_xlabel('Frequency (MHz)') ax2.plot(freq_array, intercepts) fig.tight_layout() plt.savefig(plotfile)
Globals: <<globals>>
Imports: <<imports>>
import sys from time import sleep from argparse import ArgumentParser import json import serial import numpy as np import matplotlib import matplotlib.pyplot as plt matplotlib.use('Agg') from dyadic.splot import init_style from scipy.optimize import curve_fit import pyvisa from tam import (GPIB, DSA815, DSA815_ID, HP8560A, HP8560A_GPIBID) from rfblocks import ad9913, create_serial, write_serial_cmd