___

Signal Generator Power Transfer Uncertainty.

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.

Contents¶

  • Consider a Realistic Signal Generator
  • Plot Return Loss Circle on a Smith Chart
  • Scalar Power Calibration of Signal Generator
  • Resulting Error Determined with a Monte Carlo Simulation
  • Conclusion

Import some libraries and Define some fuctions¶

Just a bunch of python code to import libraries and define useful functions.

Show Python Code
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
Show Python Code
def Gamma_2_RL(x):
    """
    Calculate return loss in dB
    """
    RL = -20*np.log10(abs(x))
    return RL
Show Python Code
def RL_2_Gamma(x):
    """
    Calculate the absolute value of the reflection coefficient
    """
    Gamma = 10**(x/(-20))
    return Gamma
Show Python Code
def Imp_2_Gamma(x):
    """
    Calculate the complex reflection coefficient from complex impedance
    """
    Gamma = (x-50)/(x+50)
    return Gamma
Show Python Code
def Gamma_2_Imp(x):
    """
    Calculate complex impedance from complex reflection coefficient
    """
    ZL = 50*(1+x)/(1-x)
    return ZL
Show Python Code
def Lin_2_dBm(x):
    """
    Convert linear power to dBm
    """
    dBm = 10*np.log10(abs(x/0.001))
    return dBm
Show Python Code
def dBm_2_Lin(x):
    """
    Convert dBm to linear power
    """
    lin = 0.001*10**(x/10)
    return lin
Show Python Code
def Lin_2_Volts(x, y):
    """
    Convert linear power to volts
    """
    volts = np.sqrt(x*y.real)
    return volts
Show Python Code
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
Show Python Code
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

Consider a Realistic Signal Generator¶

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

Show Python Code
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

Plot a Return Loss Circle on a Smith Chart¶

By way of an example, assign a return loss to the Signal Generator's source impedance and the eventual load impedance here:

Show Python Code
SG_RL = 14.5
Load_RL = 9.2

Now plot the signal generator's return loss circult on the Smith Chart.

Show Python Code
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

Scalar Power Calibration of the Signal Generator¶

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

Show Python Code
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
Perform the Scalar Power Calibration.¶

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

Resulting Error Determined with a Monte Carlo Simulation¶

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.

Show Python Code
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
Show Python Code
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
Show Python Code
#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

Conclusion¶

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.