Open RF Prototyping

plot-cal-data-fits.py

Initially, the script processes the command line and then invokes the main function passing the specified command arguments or their defaults.

from argparse import ArgumentParser
import json
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
matplotlib.use('Agg')
from dyadic.splot import init_style

DS_SLOPE_VS_FREQ = {
    '25': { 'Freq': [0.00, 454.55, 659.09, 818.18, 1011.36, 1397.73,
                     1727.27, 2011.36, 2272.73, 2511.36, 2772.73, 2988.64,
                     3227.27, 3534.09, 3840.91, 4136.36, 4454.55, 4818.18,
                     5193.18, 5568.18, 6000.00],
            'Slope': [29.46, 29.45, 29.36, 29.33, 29.37, 29.49, 29.61,
                      29.72, 29.79, 29.81, 29.86, 29.95, 30.06, 30.19,
                      30.30, 30.40, 30.51, 30.62, 30.75, 30.87, 31.01] },
    '85': { 'Freq': [0.00, 454.55, 613.64, 795.45, 1000.00, 1306.82,
                     1613.64, 1897.73, 2170.45, 2340.91, 2579.55, 2806.82,
                     3045.45, 3340.91, 3613.64, 3897.73, 4193.18, 4625.00,
                     5045.45, 5409.09, 6000.00],
            'Slope': [28.84, 28.82, 28.73, 28.68, 28.72, 28.82, 28.93,
                      29.03, 29.11, 29.13, 29.11, 29.15, 29.23, 29.31,
                      29.35, 29.34, 29.30, 29.21, 29.11, 29.02, 28.89] },
    '-40': { 'Freq': [0.00, 443.18, 590.91, 772.73, 965.91, 1329.55, 1659.09,
                      2000.00, 2238.64, 2477.27, 2670.45, 2886.36, 3170.45,
                      3397.73, 3659.09, 4000.00, 4363.64, 4818.18, 5318.18,
                      5772.73, 6000.00],
             'Slope': [29.41, 29.41, 29.33, 29.28, 29.30, 29.41, 29.53,
                       29.65, 29.71, 29.73, 29.75, 29.82, 29.94, 30.03,
                       30.09, 30.11, 30.10, 30.07, 30.03, 30.00, 29.98] }
}

DS_INTERCEPT_VS_FREQ = {
    '25': { 'Freq': [0.00, 444.02, 637.57, 876.66, 1559.77, 2026.57,
                     2288.43, 2527.51, 2823.53, 3153.70, 3552.18, 3973.43,
                     4371.92, 4872.87, 5282.73, 5612.90, 5851.99, 6000.00],
            'Intercept': [-86.15, -86.18, -86.42, -86.42, -85.53, -84.89,
                          -84.51, -84.20, -83.56, -82.77, -81.72, -80.49,
                          -79.30, -77.66, -76.36, -75.31, -74.49, -73.98] },
    '85': { 'Freq': [0.00, 465.91, 715.91, 886.36, 1204.55, 1590.91, 2011.36,
                     2318.18, 2556.82, 2750.00, 3147.73, 3420.45, 3761.36,
                     4147.73, 4534.09, 4909.09, 5545.45, 6000.00],
            'Intercept': [-87.34, -87.41, -87.68, -87.65, -87.27, -86.73,
                          -86.15, -85.70, -85.43, -85.06, -84.10, -83.42,
                          -82.64, -81.85, -81.10, -80.35, -79.12, -78.17] },
    '-40': { 'Freq': [0.00, 454.55, 670.45, 863.64, 1272.73, 1602.27, 2011.36,
                      2227.27, 2488.64, 2750.00, 3045.45, 3306.82, 3636.36,
                      4011.36, 4306.82, 4840.91, 5420.45, 6000.00],
             'Intercept': [-86.05, -86.18, -86.45, -86.45, -85.98, -85.57,
                           -85.06, -84.78, -84.61, -84.27, -83.76, -83.25,
                           -82.57, -81.65, -80.83, -79.40, -77.80, -76.09] }
}

frequencies = ['250.0', '500.0', '750.0', '1000', '1250', '1500', '1750',
               '2000', '2250', '2500', '2750', '3000', '3250', '3500',
               '3750', '4000', '4250']

def main(infiles, plotfile, outfile, extrapolate):
    linear_fits = []

    for infile in infiles:
        with open(infile) as fd:
            d = json.load(fd)

        linear_fits.append(d['linear_fits'])

    # ------------------------------------------------------------
    #
    # plotfile:
    #
    # Plot linear fit slopes and log intercepts as functions of
    # RF input frequency.  If the 'extrapolate' flag is True
    # then compute extrapolations of the slope and intercept
    # up to 6000 MHz and down to 0 MHz.
    #
    init_style()
    freq_array = np.array([float(f) for f in frequencies])
    fig = plt.figure(num=None, figsize=(8.0, 4.0), dpi=72)
    ax1 = fig.add_subplot(1, 2, 1)
    ax1.set_ylim(28.0, 33.0)
    ax1.set_xlim(0.0, 6000.0)
    ax1.set_ylabel('Slope (mV/dB)')
    ax1.set_xlabel('Frequency (MHz)')
    ax2 = fig.add_subplot(1, 2, 2)
    ax2.set_ylim(-90.0, -70.0)
    ax2.set_xlim(0.0, 6000.0)
    ax2.set_ylabel('Intercept (dBm)')
    ax2.set_xlabel('Frequency (MHz)')

    # Average slopes and intercepts across the specified cal. data sets.
    slopes = np.zeros(len(freq_array))
    intercepts = np.zeros(len(freq_array))
    for fit in linear_fits:
        slopes += np.array([v[0] for v in fit.values()])
        intercepts += np.array([-v[1]/v[0] for v in fit.values()])
    avg_slopes = slopes / len(linear_fits)
    avg_intercepts = intercepts / len(linear_fits)
    ax1.plot(freq_array, avg_slopes, label='measured')
    ax2.plot(freq_array, avg_intercepts, label='measured')

    # Plot the data sheet cal. graphs for comparison
    freq_array = np.array(DS_SLOPE_VS_FREQ['25']['Freq'])
    slopes_25 = DS_SLOPE_VS_FREQ['25']['Slope']
    ax1.plot(freq_array, slopes_25, linestyle='dashed',
             label='datasheet, 25C')
    freq_array = np.array(DS_SLOPE_VS_FREQ['-40']['Freq'])
    slopes_40 = DS_SLOPE_VS_FREQ['-40']['Slope']
    ax1.plot(freq_array, slopes_40, linestyle='dashed',
             label='datasheet, -40C')
    freq_array = np.array(DS_SLOPE_VS_FREQ['85']['Freq'])
    slopes_85 = DS_SLOPE_VS_FREQ['85']['Slope']
    ax1.plot(freq_array, slopes_85, linestyle='dashed',
             label='datasheet, 85C')
    freq_array = np.array(DS_INTERCEPT_VS_FREQ['25']['Freq'])
    intercepts_25 = DS_INTERCEPT_VS_FREQ['25']['Intercept']
    ax2.plot(freq_array, intercepts_25, linestyle='dashed',
             label='datasheet, 25C')
    freq_array = np.array(DS_INTERCEPT_VS_FREQ['-40']['Freq'])
    intercepts_40 = DS_INTERCEPT_VS_FREQ['-40']['Intercept']
    ax2.plot(freq_array, intercepts_40, linestyle='dashed',
             label='datasheet, -40C')
    freq_array = np.array(DS_INTERCEPT_VS_FREQ['85']['Freq'])
    intercepts_85 = DS_INTERCEPT_VS_FREQ['85']['Intercept']
    ax2.plot(freq_array, intercepts_85, linestyle='dashed',
             label='datasheet, 85C')

    if extrapolate:

        ext_freqs = np.append(np.array([float(f) for f in frequencies])[:-2], 6000.0)
        ext_freqs = np.insert(ext_freqs, 0, 0.0)
        freq_array = np.array(DS_SLOPE_VS_FREQ['25']['Freq'])
        delta_slope = ((slopes_25[-1] - slopes_25[14])
               / (freq_array[-1] - freq_array[14])) * (ext_freqs[-1] - ext_freqs[-2])
        ext_slopes = np.append(avg_slopes[:-2],
                               avg_slopes[-3] + delta_slope)
        ext_slopes = np.insert(ext_slopes, 0, ext_slopes[0])

        freq_array = np.array(DS_INTERCEPT_VS_FREQ['25']['Freq'])
        delta_intercept = ((intercepts_25[-1] - intercepts_25[10])
               / (freq_array[-1] - freq_array[10])) * (ext_freqs[-1] - ext_freqs[-2])
        ext_intercepts = np.append(avg_intercepts[:-2],
                                   avg_intercepts[-3] + delta_intercept)
        ext_intercepts = np.insert(ext_intercepts, 0, ext_intercepts[0])

        ax1.plot(ext_freqs, ext_slopes, linestyle='dotted', label='extrapolated')
        ax2.plot(ext_freqs, ext_intercepts, linestyle='dotted',
                 label='extrapolated')

        cal_data = {}
        for freq, slope, intercept in zip(ext_freqs, ext_slopes, ext_intercepts):
            cal_data['{:.1f}'.format(freq)] = [ slope, intercept ]

        with open(outfile, 'w') as fd:
            json.dump(cal_data, fd)

    ax1.legend(loc='upper left')
    ax2.legend(loc='upper left')
    fig.tight_layout()
    plt.savefig(plotfile)

if __name__ == '__main__':

    defaultInfile = ['ltc5582-unit1-cal-data.json']
    defaultPlotfile = 'static/tmp-ltc5582-unit1-cal-fits.svg'
    defaultOutfile = 'ltc5582-unit1-extrapolated-cal-data.json'

    parser = ArgumentParser(description=
      '''Plot LTC5582 detector calibration data fits.''')

    parser.add_argument("-E", "--extrapolate", action='store_true',
                        help="""Extrapolate cal. data fits linearly to 6 GHz.""")
    parser.add_argument("-I", "--infiles", nargs='+',
                        default=defaultInfile,
                        help="""Input file(s) containing calibration data.
                        Default: {}""".format(defaultInfile))
    parser.add_argument("-O", "--outfile",
                        default=defaultOutfile,
                        help="""Output file for extrapolated cal. data.
                        Default: {}""".format(defaultOutfile))
    parser.add_argument("-P", "--plotfile",
                        default=defaultPlotfile,
                        help="""Output file for slope/intercept plots.
                        Default: {}""".format(defaultPlotfile))

    args = parser.parse_args()

    main(args.infiles, args.plotfile, args.outfile, args.extrapolate)

The main function: <<main>>

The main function proceeds in the following steps:

  1. Read the calibration measurements and associated linear fit parameters acquired using the measure-cal-data.py script.

  2. Produces plots of the linear fit slope and intercept parameters.

  3. If required, extrapolate the linear fit parameters up to 6 GHz. The extrapolated points are also included in the parameter plots.

  4. Save the parameter plots and, if required, the updated, extrapolated linear fit parameters.

def main(infiles, plotfile, outfile, extrapolate):
    linear_fits = []

    for infile in infiles:
        with open(infile) as fd:
            d = json.load(fd)

        linear_fits.append(d['linear_fits'])

    <<plot-fit-parameters>>

    if extrapolate:

        <<extrapolate-fit-parameters>>

    ax1.legend(loc='upper left')
    ax2.legend(loc='upper left')
    fig.tight_layout()
    plt.savefig(plotfile)

Plot linear fit parameters: <<plot-fit-parameters>>

Plot the linear fit slopes and intercepts as functions of the signal input frequency. If more than one set of calibration data are specified the fit parameters will be averaged. For comparison purposes, slope and intercept data taken from the LTC5582 data sheet are also shown in the same plot (as dashed curves). The data sheet slope and intercept data is contained in the DS_SLOPE_VS_FREQ and DS_INTERCEPT_VS_FREQ global variables.

Example slope and intercept plots are shown in Figure 6.

# ------------------------------------------------------------
#
# plotfile:
#
# Plot linear fit slopes and log intercepts as functions of
# RF input frequency.  If the 'extrapolate' flag is True
# then compute extrapolations of the slope and intercept
# up to 6000 MHz and down to 0 MHz.
#
init_style()
freq_array = np.array([float(f) for f in frequencies])
fig = plt.figure(num=None, figsize=(8.0, 4.0), dpi=72)
ax1 = fig.add_subplot(1, 2, 1)
ax1.set_ylim(28.0, 33.0)
ax1.set_xlim(0.0, 6000.0)
ax1.set_ylabel('Slope (mV/dB)')
ax1.set_xlabel('Frequency (MHz)')
ax2 = fig.add_subplot(1, 2, 2)
ax2.set_ylim(-90.0, -70.0)
ax2.set_xlim(0.0, 6000.0)
ax2.set_ylabel('Intercept (dBm)')
ax2.set_xlabel('Frequency (MHz)')

# Average slopes and intercepts across the specified cal. data sets.
slopes = np.zeros(len(freq_array))
intercepts = np.zeros(len(freq_array))
for fit in linear_fits:
    slopes += np.array([v[0] for v in fit.values()])
    intercepts += np.array([-v[1]/v[0] for v in fit.values()])
avg_slopes = slopes / len(linear_fits)
avg_intercepts = intercepts / len(linear_fits)
ax1.plot(freq_array, avg_slopes, label='measured')
ax2.plot(freq_array, avg_intercepts, label='measured')

# Plot the data sheet cal. graphs for comparison
freq_array = np.array(DS_SLOPE_VS_FREQ['25']['Freq'])
slopes_25 = DS_SLOPE_VS_FREQ['25']['Slope']
ax1.plot(freq_array, slopes_25, linestyle='dashed',
         label='datasheet, 25C')
freq_array = np.array(DS_SLOPE_VS_FREQ['-40']['Freq'])
slopes_40 = DS_SLOPE_VS_FREQ['-40']['Slope']
ax1.plot(freq_array, slopes_40, linestyle='dashed',
         label='datasheet, -40C')
freq_array = np.array(DS_SLOPE_VS_FREQ['85']['Freq'])
slopes_85 = DS_SLOPE_VS_FREQ['85']['Slope']
ax1.plot(freq_array, slopes_85, linestyle='dashed',
         label='datasheet, 85C')
freq_array = np.array(DS_INTERCEPT_VS_FREQ['25']['Freq'])
intercepts_25 = DS_INTERCEPT_VS_FREQ['25']['Intercept']
ax2.plot(freq_array, intercepts_25, linestyle='dashed',
         label='datasheet, 25C')
freq_array = np.array(DS_INTERCEPT_VS_FREQ['-40']['Freq'])
intercepts_40 = DS_INTERCEPT_VS_FREQ['-40']['Intercept']
ax2.plot(freq_array, intercepts_40, linestyle='dashed',
         label='datasheet, -40C')
freq_array = np.array(DS_INTERCEPT_VS_FREQ['85']['Freq'])
intercepts_85 = DS_INTERCEPT_VS_FREQ['85']['Intercept']
ax2.plot(freq_array, intercepts_85, linestyle='dashed',
         label='datasheet, 85C')

Extrapolate fit parameters: <<extrapolate-fit-parameters>>

If extrapolation of the fit parameters is required (as indicated by the script extrapolate flag) extra slope and intercept parameter points are calculated using the LTC5582 data sheet slope and intercept curves as guides. Data points taken from the data sheet curves are contained in the DS_SLOPE_VS_FREQ and DS_INTERCEPT_VS_FREQ global variables.

Parameter values are extrapolated upward by extending the measured value using the slopes of the curves taken from data sheet values. Parameter values are extrapolated downward (to 0 Hz) by duplicating the lowest frequency measured values.

ext_freqs = np.append(np.array([float(f) for f in frequencies])[:-2], 6000.0)
ext_freqs = np.insert(ext_freqs, 0, 0.0)
freq_array = np.array(DS_SLOPE_VS_FREQ['25']['Freq'])
delta_slope = ((slopes_25[-1] - slopes_25[14])
       / (freq_array[-1] - freq_array[14])) * (ext_freqs[-1] - ext_freqs[-2])
ext_slopes = np.append(avg_slopes[:-2],
                       avg_slopes[-3] + delta_slope)
ext_slopes = np.insert(ext_slopes, 0, ext_slopes[0])

freq_array = np.array(DS_INTERCEPT_VS_FREQ['25']['Freq'])
delta_intercept = ((intercepts_25[-1] - intercepts_25[10])
       / (freq_array[-1] - freq_array[10])) * (ext_freqs[-1] - ext_freqs[-2])
ext_intercepts = np.append(avg_intercepts[:-2],
                           avg_intercepts[-3] + delta_intercept)
ext_intercepts = np.insert(ext_intercepts, 0, ext_intercepts[0])

ax1.plot(ext_freqs, ext_slopes, linestyle='dotted', label='extrapolated')
ax2.plot(ext_freqs, ext_intercepts, linestyle='dotted',
         label='extrapolated')

cal_data = {}
for freq, slope, intercept in zip(ext_freqs, ext_slopes, ext_intercepts):
    cal_data['{:.1f}'.format(freq)] = [ slope, intercept ]

with open(outfile, 'w') as fd:
    json.dump(cal_data, fd)

Globals: <<globals>>

The DS_SLOPE_VS_FREQ and DS_INTERCEPT_VS_FREQ global variables contain data taken from the LTC5582 data sheet. They are:

DS_SLOPE_VS_FREQ

Values taken from the LTC5582 data sheet 'Slope vs Frequency' graph.

DS_INTERCEPT_VS_FREQ

Values taken from the LTC5582 data sheet ('Logarithmic Intercept vs Frequency' graph.

DS_SLOPE_VS_FREQ = {
    '25': { 'Freq': [0.00, 454.55, 659.09, 818.18, 1011.36, 1397.73,
                     1727.27, 2011.36, 2272.73, 2511.36, 2772.73, 2988.64,
                     3227.27, 3534.09, 3840.91, 4136.36, 4454.55, 4818.18,
                     5193.18, 5568.18, 6000.00],
            'Slope': [29.46, 29.45, 29.36, 29.33, 29.37, 29.49, 29.61,
                      29.72, 29.79, 29.81, 29.86, 29.95, 30.06, 30.19,
                      30.30, 30.40, 30.51, 30.62, 30.75, 30.87, 31.01] },
    '85': { 'Freq': [0.00, 454.55, 613.64, 795.45, 1000.00, 1306.82,
                     1613.64, 1897.73, 2170.45, 2340.91, 2579.55, 2806.82,
                     3045.45, 3340.91, 3613.64, 3897.73, 4193.18, 4625.00,
                     5045.45, 5409.09, 6000.00],
            'Slope': [28.84, 28.82, 28.73, 28.68, 28.72, 28.82, 28.93,
                      29.03, 29.11, 29.13, 29.11, 29.15, 29.23, 29.31,
                      29.35, 29.34, 29.30, 29.21, 29.11, 29.02, 28.89] },
    '-40': { 'Freq': [0.00, 443.18, 590.91, 772.73, 965.91, 1329.55, 1659.09,
                      2000.00, 2238.64, 2477.27, 2670.45, 2886.36, 3170.45,
                      3397.73, 3659.09, 4000.00, 4363.64, 4818.18, 5318.18,
                      5772.73, 6000.00],
             'Slope': [29.41, 29.41, 29.33, 29.28, 29.30, 29.41, 29.53,
                       29.65, 29.71, 29.73, 29.75, 29.82, 29.94, 30.03,
                       30.09, 30.11, 30.10, 30.07, 30.03, 30.00, 29.98] }
}

DS_INTERCEPT_VS_FREQ = {
    '25': { 'Freq': [0.00, 444.02, 637.57, 876.66, 1559.77, 2026.57,
                     2288.43, 2527.51, 2823.53, 3153.70, 3552.18, 3973.43,
                     4371.92, 4872.87, 5282.73, 5612.90, 5851.99, 6000.00],
            'Intercept': [-86.15, -86.18, -86.42, -86.42, -85.53, -84.89,
                          -84.51, -84.20, -83.56, -82.77, -81.72, -80.49,
                          -79.30, -77.66, -76.36, -75.31, -74.49, -73.98] },
    '85': { 'Freq': [0.00, 465.91, 715.91, 886.36, 1204.55, 1590.91, 2011.36,
                     2318.18, 2556.82, 2750.00, 3147.73, 3420.45, 3761.36,
                     4147.73, 4534.09, 4909.09, 5545.45, 6000.00],
            'Intercept': [-87.34, -87.41, -87.68, -87.65, -87.27, -86.73,
                          -86.15, -85.70, -85.43, -85.06, -84.10, -83.42,
                          -82.64, -81.85, -81.10, -80.35, -79.12, -78.17] },
    '-40': { 'Freq': [0.00, 454.55, 670.45, 863.64, 1272.73, 1602.27, 2011.36,
                      2227.27, 2488.64, 2750.00, 3045.45, 3306.82, 3636.36,
                      4011.36, 4306.82, 4840.91, 5420.45, 6000.00],
             'Intercept': [-86.05, -86.18, -86.45, -86.45, -85.98, -85.57,
                           -85.06, -84.78, -84.61, -84.27, -83.76, -83.25,
                           -82.57, -81.65, -80.83, -79.40, -77.80, -76.09] }
}

frequencies = ['250.0', '500.0', '750.0', '1000', '1250', '1500', '1750',
               '2000', '2250', '2500', '2750', '3000', '3250', '3500',
               '3750', '4000', '4250']

Imports: <<imports>>

from argparse import ArgumentParser
import json
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
matplotlib.use('Agg')
from dyadic.splot import init_style