Open RF Prototyping

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:

  1. Initialize the spectrum analyzer.

  2. Initialize the DDS under test.

  3. Take the output power measurements.

  4. Fit lines to the linear portions of the output power for each of the output signal frequencies.

  5. 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>>

def write_dds_cmd(serdev, cmd):
    try:
        with create_serial(serdev) as ser:
            write_serial_cmd(ser, cmd)
    except serial.serialutil.SerialException as se:
        print(se)
        sys.exit(-1)

Initialize the DDS: <<initialize-dds>>

dds = ad9913(cs=cspin, io_update=ioupdate, reset=reset)
write_dds_cmd(serdev, dds.pin_config())
write_dds_cmd(serdev, dds.power_control(
    ad9913.POWER_UP, ad9913.Subsystem.DAC))

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.

linear_fits = {}
vout_vs_freq = {}
for freq, vout in measured_levels.items():
    vout_vs_freq[freq] = vout

    popt, pcov = curve_fit(linear_func, output_levels, vout)
    perr = np.sqrt(np.diag(pcov))
    slope = popt[0]
    intercept = popt[1]
    linear_fits[freq] = [slope, intercept, perr[0], perr[1]]

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>>

output_levels = list(range(64, 1025, 64))
frequencies = ['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']

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