This note attempts to quantify the uncertainty involved with using an "ideal" power sensor to calibrate the output power of a signal generator that has a realistic output impdedance, referred to as the source impedance.
The ideal power sensor presents a perfect 50 $\Omega$ load impedance.
The ideal power sensor measures power with no errors or uncertainty.
Then a realisic load is attached in place of the power sensor, and the actual power transfered to the load is analyzed to identify the power transfer uncertainty due to realistic source impedance and realistic load impedance.
Just a bunch of python code to import libraries and define useful functions.
import ziaplot as zp
import schemdraw
from schemdraw import dsp
import schemdraw.elements as elm
import schemdraw.elements.cables as cbl
import cairosvg
from PIL import Image
import math
import cmath
import numpy as np
def Gamma_2_RL(x):
"""
Calculate return loss in dB
"""
RL = -20*np.log10(abs(x))
return RL
def RL_2_Gamma(x):
"""
Calculate the absolute value of the reflection coefficient
"""
Gamma = 10**(x/(-20))
return Gamma
def Imp_2_Gamma(x):
"""
Calculate the complex reflection coefficient from complex impedance
"""
Gamma = (x-50)/(x+50)
return Gamma
def Gamma_2_Imp(x):
"""
Calculate complex impedance from complex reflection coefficient
"""
ZL = 50*(1+x)/(1-x)
return ZL
def Lin_2_dBm(x):
"""
Convert linear power to dBm
"""
dBm = 10*np.log10(abs(x/0.001))
return dBm
def dBm_2_Lin(x):
"""
Convert dBm to linear power
"""
lin = 0.001*10**(x/10)
return lin
def Lin_2_Volts(x, y):
"""
Convert linear power to volts
"""
volts = np.sqrt(x*y.real)
return volts
def SG_Imp_2_Volts(x):
"""
Convert SG impedance into voltage when calibrating with ideal power sensor
So perfect 50 ohm load
dissipating a perfect 0.001 watts
"""
load_volts = Lin_2_Volts(0.001, 50)
SigGen_volts = load_volts*(50+x)/50
return SigGen_volts
def Load_Cal_Power(SG_imp, Load_imp):
"""
Calibrated power for various SG and load impendaces
Assume calibration of a realistic SG with an ideal power sensor
Then connect a realistic load
"""
PM_volts = np.sqrt(0.001 * 50) # convert 1 mW in a 50 ohm load to voltage
SigGen_volts = PM_volts*(50+SG_imp)/50 # PM volts related to SG volts with voltage divider
Load_volts = SigGen_volts*Load_imp.real/(SG_imp+Load_imp) # replace PM with load, calculate power delivered to load
# NOTE: voltage across real part of the load...
Load_power_lin = abs(Load_volts)**2 / Load_imp.real # linear load power in real part of the load
Load_power_dBm = 10*np.log10(abs(Load_power_lin/0.001)) # convert power to dBm
return Load_power_dBm # NOTE: target power was 0 dBm, so this value is also dB of error...
___ back to contents
Our signal generator has a realisistic output impedance, called the source impedance. We will represent this source impedance with a return loss value. This means the actual source impedance at any given operating frequency is located inside the constant return loss circle on the Smith Chart.
The figure below shows the realistic signal generator with an source impedance of "x dB Return Loss," followed by a coaxial cable. The output of the cable is the "calibration plane."
PM_cal = schemdraw.Drawing()
PM_cal += (S1 := dsp.Oscillator().fill('lightgreen'))
PM_cal += dsp.Line().down().at(S1.S).length(PM_cal.unit/4)
PM_cal += elm.GroundSignal().right
PM_cal += dsp.Line().up().at(S1.N).length(PM_cal.unit/2)
PM_cal += dsp.Line().right().length(PM_cal.unit/4)
#elm.style(elm.STYLE_IEC)
PM_cal += elm.RBOX().right().label('x dB Return Loss')
PM_cal += cbl.Coax().color('brown')
PM_cal.move(dy = -1.5)
PM_cal += dsp.Line().up().color('red').linestyle('--').label('Calibration Plane', 'right')
#PM_cal.draw()
PM_cal.save('PM.svg')
cairosvg.svg2png(file_obj=open("PM.svg"), write_to="PM.png", scale=1.5)
Figure = Image.open("PM.png")
Figure
___ back to contents
By way of an example, assign a return loss to the Signal Generator's source impedance and the eventual load impedance here:
SG_RL = 14.5
Load_RL = 9.2
Now plot the signal generator's return loss circult on the Smith Chart.
SG_Gamma = RL_2_Gamma(SG_RL)
Imp_left = Gamma_2_Imp(cmath.rect(SG_Gamma, math.pi))
Imp_right = Gamma_2_Imp(cmath.rect(SG_Gamma, 0))
Mag = zp.linspace(SG_Gamma, SG_Gamma, 201)
Phs = zp.linspace(0, 2*math.pi, 201)
p = zp.Smith(grid='extrafine', title=r'Signal Generator Source Impedance is Inside this Circle...')
p += zp.LinePolar(Mag, Phs).color('green')
p += zp.Text(0.0, 0.4, 'Return Loss Circle = %0.1f' %SG_RL +' dB', halign = 'center')
p += zp.Text(-SG_Gamma*1.5, -0.1, f'{Imp_left: .1f}', halign = 'right', valign = 'top')
p += zp.Arrow( (-SG_Gamma*1.1, -0.02), (-SG_Gamma*1.4, -0.1)).color('black')
p += zp.Text(SG_Gamma*1.5, -0.1, f'{Imp_right: .1f}', halign = 'left', valign = 'top')
p += zp.Arrow( (SG_Gamma*1.1, -0.02), (SG_Gamma*1.4, -0.1)).color('black')
figure = zp.Hlayout(p, height=600, width = 600)
figure.save('return_loss_SC.svg')
cairosvg.svg2png(file_obj=open("return_loss_SC.svg"), write_to="return_loss_SC.png", scale=1)
Figure = Image.open("return_loss_SC.png")
Figure
___ back to contents
Start by assuming we have an ideal power sensor. This means that the load impedance looking into the power sensor is a perfect 50 ohms. And the power sensor can perfectly measure the power dissipated by it's 50 ohm load.
The figure below shows an ideal power sensor being connected to the end of a coaxial cable connected to our realistic signal generator with an source impedance of "x dB RL".
PM_cal = schemdraw.Drawing()
PM_cal += (S1 := dsp.Oscillator().fill('lightgreen'))
PM_cal += dsp.Line().down().at(S1.S).length(PM_cal.unit/4)
PM_cal += elm.GroundSignal().right
PM_cal += dsp.Line().up().at(S1.N).length(PM_cal.unit/2)
PM_cal += dsp.Line().right().length(PM_cal.unit/4)
#elm.style(elm.STYLE_IEC)
PM_cal += elm.RBOX().right().label('%0.1f dB \nReturn Loss' %SG_RL)
PM_cal += cbl.Coax().color('brown')
PM_cal += dsp.Line().right().length(PM_cal.unit/8)
PM_cal.push()
PM_cal += dsp.Line().right().length(PM_cal.unit/8)
PM_cal += (PM:= elm.Triax(shieldofstend = 0, leadlen = 0.0, length = 2).color('blue'))
elm.style(elm.STYLE_IEEE)
PM_cal.move(dx = 0.35, dy = 0.55)
PM_cal += (Load_start := dsp.Line().theta(-90).length(0.1))
PM_cal += dsp.Line().theta(-35).length(0.1)
PM_cal += dsp.Line().theta(-145).length(0.25)
PM_cal += dsp.Line().theta(-35).length(0.25)
PM_cal += dsp.Line().theta(-145).length(0.25)
PM_cal += dsp.Line().theta(-35).length(0.25)
PM_cal += dsp.Line().theta(-145).length(0.25)
PM_cal += dsp.Line().theta(-35).length(0.25)
PM_cal += dsp.Line().theta(-145).length(0.1)
PM_cal += (Load_end := dsp.Line().theta(-90).length(0.1))
PM_cal.pop()
PM_cal.move(dy = -3)
PM_cal.push()
PM_cal += dsp.Line().up().length(PM_cal.unit*1.5).color('red').linestyle('--').label('Calibration Plane', 'right')
PM_cal.pop()
PM_cal += elm.Line().left().length(0).color('black').label('adjust for 0 dBm', ofst = -0.75).color('red')
PM_cal += elm.EncircleBox([PM, Load_start, Load_end], pady=.6).linestyle('--').color('royalblue').fill('aliceblue').zorder(0).label('ideal 50 Ω \n power sensor', 'bottom', ofst = 0.25)
#PM_cal.draw()
PM_cal.save('PM_cal.svg')
cairosvg.svg2png(file_obj=open("PM_cal.svg"), write_to="PM_cal.png", scale=1.5)
Figure = Image.open("PM_cal.png")
Figure
This means that at each frequency, we will adjust the output of the signal generator so that the ideal 50 ohm power sensor indicates it is measuring 0 dBm.
Since the signal generator does not have a perfect 50 ohm source impedance, this means that some power is reflected at the calibration plane, due to the mismatch.
The calibration routine overcomes this reflected power by (internally) increasing the voltage of the signal generator.
The end result is that any DUT which also has a perfect 50 ohm impedance, will also dissipate exactly 0 dBm.
But since real world DUTs will not have perfect 50 ohm impedances, the actual dissipated power will be higher or lower than 0 dBm.
Why higher? For some signal generator source impedances, the signal generator voltage had to be increaased during the calibration process, resulting in more than 0 dBm of available power. The way to get more than 0 dBm is to connect a load to the signal generator that is the complex conjugate of the signal generator's source impedance.
Why lower? There is also a region of signal generator source impedances that the DUT can present which result in an even larger reflection coefficent than what was observed during calibration.
___ back to contents
We do NOT know the actual signal generator source impedace; and we do not know the input impedance to the DUT. We only know that each impedance is located inside their respective return loss circles on the Smith Chart.
Further, at each output frequency, both of these impedances change. This is especially true when you consider the impact of the coaxial cable. Electrical length in both the signal generator and the DUT will cause the impedances to rotate around the Smith Chart, within the return loss circle.
So the Monte Carlo simulation will assume a rectangular distribution of phase values over the range of 0 to 2$\pi$.
It is tempting to also use a rectangular distribution for the reflection coefficent. But that does skew the results. In reality, the magnitude of the return loss is seldom better than 30 or 40 dB.
So a Rayleigh distribution is used for the distribution of the reflection coeficiant.
The distributions of the source and load impedances are plotted in the Smith Chart, so the actual distribution can be observed.
trials = 20000
modevalue = .25
Rayleigh_Dist_SG_Gamma = abs((1 - np.random.rayleigh(modevalue, trials)))
SG_Gamma = RL_2_Gamma(SG_RL)
SG_Gamma_array_random = Rayleigh_Dist_SG_Gamma * SG_Gamma * np.exp(-1j*np.random.random(trials) * 2 * np.pi)
SG_Imp_array_random = Gamma_2_Imp(SG_Gamma_array_random)
Rayleigh_Dist_SG_Gamma_plot = abs(SG_Gamma_array_random).tolist()
SG_Mag = zp.linspace(SG_Gamma, SG_Gamma, 201)
SG_Phs = zp.linspace(0, 2*math.pi, 201)
Rayleigh_Dist_Load_Gamma = abs((1 - np.random.rayleigh(modevalue, trials)))
Load_Gamma = RL_2_Gamma(Load_RL)
Load_Gamma_array_random = Rayleigh_Dist_Load_Gamma * Load_Gamma * np.exp(-1j*np.random.random(trials) * 2 * np.pi)
Load_Imp_array_random = Gamma_2_Imp(Load_Gamma_array_random)
Rayleigh_Dist_Load_Gamma_plot = abs(Load_Gamma_array_random).tolist()
Load_Mag = zp.linspace(Load_Gamma, Load_Gamma, 201)
Load_Phs = zp.linspace(0, 2*math.pi, 201)
Power_Variation_random_random = Load_Cal_Power(SG_Imp_array_random, Load_Imp_array_random)
Power_error = Power_Variation_random_random.tolist()
src_gamma_hist = zp.XyPlot( title=r'Distribution of Source Gamma, Sig Gen Return Loss = %0.1f dB' %SG_RL)
src_gamma_hist += zp.Histogram(Rayleigh_Dist_SG_Gamma_plot, binrange=(0, 1, .01)).color('green')
load_gamma_hist = zp.XyPlot( title=r'Distribution of Load Gamma, Load Return Loss = %0.1f dB' %Load_RL, xname=r'Reflection Coefficent')
load_gamma_hist += zp.Histogram(Rayleigh_Dist_Load_Gamma_plot, binrange=(0, 1, .01)).color('blue')
figure = zp.Vlayout(src_gamma_hist, load_gamma_hist, height=400, width = 800)
figure.save('gamma_dist.svg')
cairosvg.svg2png(file_obj=open("gamma_dist.svg"), write_to="gamma_dist.png", scale=1)
Figure = Image.open("gamma_dist.png")
Figure
src_smith = zp.Smith(grid='extrafine', title=r'Random Source Impedance')
load_smith = zp.Smith(grid='extrafine', title=r'Random Load Impedance')
error_hist = zp.XyPlot( title=r'Error in Power Delivered to the Load after Calibration with an Ideal Power Sensor', xname=r'Error in dB')
src_smith += zp.Line(SG_Gamma_array_random.real, SG_Gamma_array_random.imag).marker('round', radius=1.5).color('green').stroke('none')
src_smith += zp.LinePolar(SG_Mag, SG_Phs).color('green')
src_smith += zp.Text(0.0, 0.6, 'SG Return Loss Circle = %0.1f' %SG_RL +' dB', halign = 'center')
load_smith += zp.Line(Load_Gamma_array_random.real, Load_Gamma_array_random.imag).marker('round', radius=1.5).color('blue').stroke('none')
load_smith += zp.LinePolar(Load_Mag, Load_Phs).color('blue')
load_smith += zp.Text(0.0, 0.6, 'Load Return Loss Circle = %0.1f' %Load_RL +' dB', halign = 'center')
error_hist += zp.Histogram(Power_error, bins = 101)
error_hist.style.tick.xsrformat = '0.1f'
error_hist.style.tick.ysrformat = 'none'
smith_row = zp.Hlayout(src_smith, load_smith)
error_row = zp.Hlayout(error_hist)
figure = zp.Vlayout(smith_row, error_row, height=800, width = 800)
figure.save('error_plots.svg')
cairosvg.svg2png(file_obj=open("error_plots.svg"), write_to="error_plots.png", scale=1)
Figure = Image.open("error_plots.png")
Figure
#plt.xkcd()
DUT = schemdraw.Drawing()
DUT += (S1 := dsp.Oscillator().fill('lightgreen'))
DUT += dsp.Line().down().at(S1.S).length(DUT.unit/4)
DUT += elm.GroundSignal().right
DUT += dsp.Line().up().at(S1.N).length(DUT.unit/2)
DUT += dsp.Line().right().length(DUT.unit/4)
#elm.style(elm.STYLE_IEC)
DUT += elm.RBOX().right().label('%.1f dB \n Return Loss' %SG_RL)
DUT += cbl.Coax().color('brown')
DUT.push()
DUT += cbl.Coax().color('brown')
DUT += dsp.Line().right().length(DUT.unit/4)
DUT += dsp.Line().down().length(DUT.unit/8)
DUT += elm.RBOX().down().label(' %.1f dB \n Return Loss' %Load_RL, 'bottom')
DUT += elm.GroundSignal().right
DUT.pop()
DUT.move(dy = -1.5)
DUT += dsp.Line().up().color('red').linestyle('--').label('Calibration Plane', 'right')
#DUT.draw()
DUT.save('schem1.svg')
cairosvg.svg2png(file_obj=open("schem1.svg"), write_to="schem1.png", scale=1.5)
Figure = Image.open("schem1.png")
Figure
___ back to contents
In this example, we expected the load to dissipate 0 dBm. The actual power dissipated in the load has a range of values, as displayed in the plot. In reality, if the signals are distribted over frequency, it is analagous to running this experiment over and over. Each frequency will have its own source and load impedances, and we are likely to observe the full range of this error distribution.