plot-cal-data-linearity.py
Initially, the script processes the command line and then invokes the
main
function passing the specified command arguments or their
defaults.
<<imports>> <<globals>> <<main>> if __name__ == '__main__': defaultInfile = 'pwrdetect-cal-data.json' defaultOutfile = 'pwrdetect-cal-data.svg' defaultPlotfile = 'pwrdetect-lin-fits.svg' defaultLinerrfile = 'pwrdetect-linerr.svg' defaultLinlimitsfile = 'pwrdetect-linlimits.json' parser = ArgumentParser(description= '''Plot power detector calibration data linearity.''') parser.add_argument("-I", "--infile", default=defaultInfile, help="""Input file containing calibration data. Default: {}""".format(defaultInfile)) parser.add_argument("-O", "--outfile", default=defaultOutfile, help="""Output file for plot. Default: {}""".format( defaultOutfile)) parser.add_argument("-P", "--plotfile", default=defaultPlotfile, help="""Output file for slope/intercept plots. Default: {}""".format(defaultPlotfile)) parser.add_argument("-L", "--linlimitsfile", default=defaultLinlimitsfile, help="""Output file for linearity limits. Default: {}""".format(defaultLinlimitsfile)) parser.add_argument("-E", "--linerrfile", default=defaultLinerrfile, help="""Output file for linearity error plots. Default: {}""".format(defaultLinerrfile)) args = parser.parse_args() main(args.infile, args.outfile, args.plotfile, args.linerrfile, args.linlimitsfile)
The main
function: <<main>>
The main
function proceeds in the following steps:
Read the measured calibration data and associated linear fit parameters acquired using the measure-cal-data.py script.
Plot measured calibration data points together with the associated linear fit.
Plot the linear fit slope and intercept parameters as a function of input signal frequency.
Calculate and plot the linearity errors.
Calculate and plot the detector linearity limits. Fit cubic functions to the lower and upper linearity limit values.
Save the linearity limit data to a JSON format file.
def main(infile, outfile1, outfile2, outfile3, linlimitsfile): with open(infile) as fd: d = json.load(fd) try: detector_type = d['detector_type'] except KeyError: detector_type = 0 measured_data = d['measured_data'] linear_fits = d['linear_fits'] annotation_color = 'darkgrey' init_style() <<plot-fits>> <<plot-parameters>> <<plot-linearity>> <<plot-linearity-limits>> linearity_data = { 'measured_limits': lin_limits, 'min_limit_coeffs': list(min_limit_coeffs), 'max_limit_coeffs': list(max_limit_coeffs), 'linerr_fits': linerr_fits } # Save the linearity limits information with open(linlimitsfile, 'w') as fd: json.dump(linearity_data, fd)
Plot calibration data fits: <<plot-fits>>
Plot the measured data points together with the associated linear fits for each of the input signal frequencies. Figure 5 shows a set of example plots.
def linear_func(x, a, b): return a*x + b fig = plt.figure(num=None, figsize=(6.0, 6.0), dpi=72) levels = pwr_levels[detector_type] for idx, freq in enumerate(frequencies[detector_type][1:]): vout = measured_data[freq] slope = linear_fits[freq][0] intercept = linear_fits[freq][1] if (idx+1) % 4 == 1: ax = fig.add_subplot(4, 4, idx+1) sharey_ax = ax else: ax = fig.add_subplot(4, 4, idx+1, sharey=sharey_ax) plt.setp(ax.get_yticklabels(), visible=False) ax.plot(levels, vout, marker='o') ax.plot(levels, linear_func(np.array(levels), slope, intercept)) ax.text(-75.0, 2500.0, '{}'.format(freq)) fig.tight_layout() plt.savefig(outfile1)
Plot linear fit parameters: <<plot-parameters>>
Plot the linear fit slopes and log intercepts as functions of signal input frequency.
freq_array = np.array([float(f) for f in frequencies[detector_type]]) slopes = np.array([v[0] for v in d['linear_fits'].values()]) intercepts = np.array([-v[1]/v[0] for v in d['linear_fits'].values()]) fig = plt.figure(num=None, figsize=(8.0, 4.0), dpi=72) ax1 = fig.add_subplot(1, 2, 1) ax1.set_ylim(np.floor(np.min(slopes)), np.ceil(np.max(slopes))) ax1.set_xlim(*frequency_limits[detector_type]) ax1.set_ylabel('Slope (mV/dB)') ax1.set_xlabel('Frequency (MHz)') ax1.plot(freq_array, slopes) ax2 = fig.add_subplot(1, 2, 2) ax2.set_ylim(np.floor(np.min(intercepts)), np.ceil(np.max(intercepts))) ax2.set_xlim(*frequency_limits[detector_type]) ax2.set_ylabel('Intercept (dBm)') ax2.set_xlabel('Frequency (MHz)') ax2.plot(freq_array, intercepts) fig.tight_layout() plt.savefig(outfile2)
Plot calibration data linearity: <<plot-linearity>>
A panel of four plots showing aspects of the detector linearity are produced. Here the top two plots are generated. The linearity error of the detector is calculated for each of the input signal frequencies. The linearity error is the difference between the expected power (as calculated using the linear fit) and the power which was actually measured. This linearity error is plotted in the first panel as a function of the input signal power. Horizontal dashed lines indicating the linearity error levels of \(\pm 0.5\) dB are also drawn on the plot.
A polynomial function is now fitted to the calculated linearity error points for each of the input signal frequencies. The functions are plotted in the second panel along with horizontal dashed lines indicating linearity error levels of \(\pm 0.5\) dB.
Examples of these plots are shown in the upper two panels of Figure 7.
Linearity limits at each of the input signal frequencies can now be found from the intersection of each of the polynomial function fits with the horizontal lines of \(\pm 0.5\) dB linearity error.
# ----------------------------------------------------------- # outfile3: # # This produces 4 plots: # 1. The measured linearity errors # 2. Polynomial fits to the measured linearity errors # 3. The lower linearity limits as a function of frequency # 4. The upper linearity limits as a function of frequency # if detector_type == LogDetector.LTC5582: lower_pwr_limit = -55.0 upper_pwr_limit = 0.0 else: lower_pwr_limit = -80.0 upper_pwr_limit = 10.0 fig = plt.figure(num=None, figsize=(6.0, 6.0), dpi=72) ax1 = fig.add_subplot(2, 2, 1) ax1.set_xlabel('RF Input Power (dBm)') ax1.set_ylabel('Linearity Error (dB)') ax1.set_ylim(-5.0, 5.0) ax1.set_xlim(lower_pwr_limit, upper_pwr_limit) for idx, freq in enumerate(frequencies[detector_type]): vout = measured_data[freq] slope = linear_fits[freq][0] intercept = linear_fits[freq][1] linerr = (np.array(vout) - linear_func(np.array(levels), slope, intercept)) linerr_db = linerr / slope ax1.plot(levels, linerr_db) ax1.text(-30.0, 4.0, 'Measured') ax1.hlines([0.5, -0.5], lower_pwr_limit, upper_pwr_limit, linestyles='--', colors=annotation_color) # Generate polynomial fits to the measured linearity errors # and plot the resulting curves. # In addition, find the +/- 0.5 dB linearity limits. Write these # to a JSON format file. def poly_func(x, a, b, c, d, e, f, g, h): return ((((((a*x + b)*x + c)*x + d)*x + e)*x + f)*x + g)*x + h ax2 = fig.add_subplot(2, 2, 2) ax2.set_xlabel('RF Input Power (dBm)') ax2.set_ylim(-5.0, 5.0) ax2.set_xlim(lower_pwr_limit, upper_pwr_limit) lin_limits = {} linerr_fits = {} for idx, freq in enumerate(frequencies[detector_type]): vout = measured_data[freq] slope = linear_fits[freq][0] intercept = linear_fits[freq][1] linerr = (np.array(vout) - linear_func(np.array(levels), slope, intercept)) linerr_db = linerr / slope popt, _ = curve_fit(poly_func, np.array(levels), linerr_db) # Determine the intersection of the linearity error curves with # the +/- 0.5 dB linearity limits. def poly_func2(a, b, c, d, e, f, g, h, x): return ((((((a*x + b)*x + c)*x + d)*x + e)*x + f)*x + g)*x + h ff = partial(poly_func2, *popt) min_db = fsolve(lambda x: ff(x) - 0.5, lower_pwr_limit) max_db = fsolve(lambda x: ff(x) + 0.5, upper_pwr_limit) lin_limits[freq] = [min_db[0], max_db[0]] linerr_fits[freq] = list(popt) ax2.plot(levels, poly_func(np.array(levels), *popt)) ax2.text(-40.0, 4.0, 'Polynomial Fits') ax2.hlines([0.5, -0.5], lower_pwr_limit, upper_pwr_limit, linestyles='--', colors=annotation_color)
Plot calibration data linearity limits: <<plot-linearity-limits>>
Based on the linearity error plots in the LTC5582 data sheet, the calculated linearity limits are extrapolated to include values for an input signal frequency of 6 GHz. A cubic function is now fitted to both the lower and upper linearity limits. In effect this gives us a polynomial which provides the detector linearity limits as function of input signal frequency. The calculated linearity limits and the associated cubic fit are plotted in the lower panels.
Examples of these plots are shown in the lower two panels of Figure 7.
def cubic_func(x, a, b, c, d): return ((a*x + b)*x + c)*x + d if detector_type == LogDetector.LTC5582: # Extrapolate the linearity limits to include values for 6000 MHz. lin_limits['6000'] = [ lin_limits['4250'][0] + 5.0, lin_limits['4250'][1] + 1.0] freqs = [250.0, 500.0, 750.0, 1000.0, 1250.0, 1500.0, 1750.0, 2000.0, 2250.0, 2500.0, 2750.0, 3000.0, 3250.0, 3500.0, 3750.0, 4000.0, 4250.0, 6000.0] _f = np.arange(250,6050,50) limits = lin_limits.values() else: freqs = [float(f) for f in frequencies[detector_type]][1:] _f = np.arange(50,605,25) limits = list(lin_limits.values())[1:] min_limit_coeffs, _ = curve_fit(cubic_func, freqs, [v[1] for v in limits]) max_limit_coeffs, _ = curve_fit(cubic_func, freqs, [v[0] for v in limits]) # Plot the linearity limit fits together with the measured # limit data points. ax3 = fig.add_subplot(2, 2, 3) ax3.set_xlabel('Frequency (MHz)') ax3.set_ylabel('Lower Linearity Limit (dBm)') ax3.plot(_f, cubic_func(_f, *max_limit_coeffs)) ax3.scatter(freqs, [v[0] for v in limits]) ax4 = fig.add_subplot(2, 2, 4) ax4.set_xlabel('Frequency (MHz)') ax4.set_ylabel('Upper Linearity Limit (dBm)') ax4.plot(_f, cubic_func(_f, *min_limit_coeffs)) ax4.scatter(freqs, [v[1] for v in limits]) fig.tight_layout() plt.savefig(outfile3)
Globals: <<globals>>
pwr_levels = [ [-75.0, -70.0, -65.0, -60.0, -55.0, -50.0, -45.0, -40.0, -35.0, -30.0, -25.0, -20.0, -15.0, -10.0, -5.0, 0.0, 5.0, 10.0], [-80.0, -75.0, -70.0, -65.0, -60.0, -55.0, -50.0, -45.0, -40.0, -35.0, -30.0, -25.0, -20.0, -15.0, -10.0, -5.0, 0.0, 5.0, 10.0] ] frequencies = [ ['250.0', '500.0', '750.0', '1000', '1250', '1500', '1750', '2000', '2250', '2500', '2750', '3000', '3250', '3500', '3750', '4000', '4250'], ['5.0', '50.0', '100.0', '150.0', '200.0', '250.0', '300.0', '350.0', '400.0', '425.0', '450.0', '475.0', '500.0', '525.0', '550.0', '575.0', '600.0'] ] frequency_limits = [ [0.0, 6000.0], [0.0, 600.0] ]