Calibration Examples
One-Port Reflect Only
Calibrate the VNA from measurements of short, open and load standards using Agilent calibration kit models.
from libvna.cal import Calset, CalType, Solver
import numpy as np
# Set the calibration frequency points.
fmin = 1.0e+06
fmax = 1.0e+09
f_vector = np.logspace(np.log10(fmin), np.log10(fmax), num=10)
# Create the calibration container and error term solver.
calset = Calset()
solver = Solver(calset, CalType.E12, rows=1, columns=1,
frequency_vector=f_vector)
# Define the calibration standards for out calibration kit.
short_std = calset.short_standard(
offset_z0=51.259595,
offset_delay=33.340790e-12,
offset_loss=5.460953e+9,
fmax=8.5e+9,
L=[-119.006943e-12, -1.310397249e-21, 1.511773982e-31, -91.480400e-42]
)
open_std = calset.open_standard(
offset_z0=51.635682,
offset_delay=37.636850e-12,
offset_loss=5.611771e+9,
fmax=8.5e+9,
C=[-93.936119e-15, 151.860439e-27, -786.852853e-36, 46.121820e-45]
)
load_std = calset.load_standard(
offset_z0=50.0,
offset_delay=0.0,
offset_loss=0.0,
fmax=8.5e+9,
Zl=50.0
)
# Add measurement of the short standard.
m = [
[[-0.78096+0.04016j]],
[[-0.57075-0.06843j]],
[[-1.07720-0.69930j]],
[[-1.46208-0.18917j]],
[[-1.45114+0.12961j]],
[[-1.30609+0.47136j]],
[[-0.73713+0.81063j]],
[[+0.19019-0.03156j]],
[[-1.37703+0.41971j]],
[[-0.21701+0.72119j]]
]
solver.add_single_reflect(m, s11=short_std)
# Add measurement of the open standard.
m = [
[[+1.28991+0.00763j]],
[[+1.49690-0.13527j]],
[[+0.98587-0.84160j]],
[[+0.58278-0.49390j]],
[[+0.51175-0.51737j]],
[[+0.29281-0.83658j]],
[[-0.54331-1.24423j]],
[[-1.86889+0.04960j]],
[[+0.35237-0.68638j]],
[[-1.39939-0.92837j]]
]
solver.add_single_reflect(m, s11=open_std)
# Add measurement of the load standard.
m = [
[[+0.06199+0.02924j]],
[[+0.27374-0.09461j]],
[[-0.23403-0.75681j]],
[[-0.62627-0.31304j]],
[[-0.64863-0.13356j]],
[[-0.65105-0.06079j]],
[[-0.65147-0.02810j]],
[[-0.65156-0.01303j]],
[[-0.65157-0.00605j]],
[[-0.65158-0.00281j]]
]
solver.add_single_reflect(m, s11=load_std)
# Solve, add to Calset and save.
solver.solve()
solver.add_to_calset('1-port')
calset.save('1x1.vnacal')
Apply the calibration to a device under test.
from libvna.cal import Calset
# Load the calibration from file.
calset = Calset('1x1.vnacal')
calibration = calset.calibrations['1-port']
f_vector = calibration.frequency_vector
# Measured response:
measured = [
[[-0.52964+0.07031j]],
[[-0.31303-0.00464j]],
[[-0.79363-0.56351j]],
[[-1.05220+0.08788j]],
[[-0.36799+0.39266j]],
[[-1.17567-0.80949j]],
[[-1.21893+0.62688j]],
[[+0.15769+0.24262j]],
[[-1.46825+0.20584j]],
[[-0.53253+0.82947j]]
]
# Apply calibration and save in Touchstone format.
result = calibration.apply(f_vector, measured)
result.save('1x1-corrected.s1p')
One-Port Reflect Only with Measured Standards
Calibrate the VNA from measurements of short, open and load standards using measured values of the standards.
from libvna.cal import Calset, CalType, Solver
from libvna.data import NPData, PType
import numpy as np
# Set the calibration frequency points.
fmin = 1.0e+06
fmax = 1.0e+09
f_vector = np.logspace(np.log10(fmin), np.log10(fmax), num=10)
# Create the calibration container and error term solver.
calset = Calset()
solver = Solver(calset, CalType.E12, rows=1, columns=1,
frequency_vector=f_vector)
# Load the measurement files for the standards. We could use
# calset.data_standard() to explicitly convert these NPData objects
# to ParameterMatrix, but we don't have to because the solver_add_*
# methods will do it for us.
short_std = NPData(filename='short.s1p')
open_std = NPData(filename='open.s1p')
load_std = NPData(filename='load.s1p')
# Add measurement of the short standard.
m = [
[[-0.81564+0.05826j]],
[[-0.80665+0.12481j]],
[[-0.76615+0.26205j]],
[[-0.59922+0.50375j]],
[[-0.10496+0.69735j]],
[[+0.56939+0.34291j]],
[[+0.80222-0.70743j]],
[[-1.23162-2.12844j]],
[[-0.46290+0.73718j]],
[[-1.03275+0.86455j]]
]
solver.add_single_reflect(m, short_std)
# Add measurement of the open standard.
m = [
[[+0.99780-0.07396j]],
[[+0.99008-0.15897j]],
[[+0.95466-0.33884j]],
[[+0.79660-0.69444j]],
[[+0.18668-1.18116j]],
[[-0.96628-0.85516j]],
[[-0.85185+0.06671j]],
[[-0.28959-0.04330j]],
[[-1.17370-1.21542j]],
[[-1.01239-1.09588j]]
]
solver.add_single_reflect(m, open_std)
# Add measurement of the load standard.
m = [
[[-0.99975+0.00961j]],
[[-0.99913+0.02069j]],
[[-0.99636+0.04440j]],
[[-0.98395+0.09385j]],
[[-0.93151+0.18578j]],
[[-0.76080+0.28467j]],
[[-0.48971+0.20853j]],
[[-0.22567-0.09589j]],
[[-1.07909-1.21437j]],
[[-0.66130-0.88713j]]
]
solver.add_single_reflect(m, load_std)
# Solve, add to Calset and save.
solver.solve()
solver.add_to_calset('1-port')
calset.save('1x1m.vnacal')
Apply the calibration to a device under test.
from libvna.cal import Calset
# Load the calibration from file.
calset = Calset('1x1m.vnacal')
calibration = calset.calibrations['1-port']
f_vector = calibration.frequency_vector
# Measured response:
measured = [
[[-0.66426+0.04608j]],
[[-0.65565+0.09883j]],
[[-0.61598+0.20843j]],
[[-0.43624+0.40339j]],
[[+0.26546+0.38457j]],
[[-0.32590-1.26461j]],
[[-0.92757+0.01144j]],
[[-0.34861-0.00107j]],
[[-0.99717-1.20680j]],
[[-0.62859-0.85625j]]
]
# Apply calibration and save in Touchstone format.
result = calibration.apply(f_vector, measured)
result.save('1x1m-corrected.s1p')
2x1 SOLT
Example of SOLT calibration for a VNA that measures only \(S_{11}\) and \(S_{21}\). In this example, we use perfect standards: -1 for short, 1 for open, 0 for load, and solver.add_through() for a perfect through.
from libvna.cal import Calset, CalType, Solver
import numpy as np
# Set the calibration frequency points.
fmin = 1.0e+06
fmax = 1.0e+09
f_vector = np.logspace(np.log10(fmin), np.log10(fmax), num=10)
# Create the calibration container and error term solver.
calset = Calset()
solver = Solver(calset, CalType.E12, rows=2, columns=1,
frequency_vector=f_vector)
# Add measurement of the short standard.
m = [
[[-1.01824-0.06417j],
[-0.02363-0.03527j]],
[[-1.06978-0.11034j],
[-0.11220-0.06234j]],
[[-1.17746-0.11148j],
[-0.55861+0.06766j]],
[[-1.26256-0.01627j],
[+0.18205+2.33926j]],
[[-1.27559+0.13606j],
[+1.69014+0.83232j]],
[[-1.21099+0.35981j],
[+1.67578+0.32863j]],
[[-0.97062+0.70975j],
[+1.66486+0.14719j]],
[[-0.16957+1.01077j],
[+1.66224+0.06780j]],
[[+0.69978-0.35845j],
[+1.66166+0.03142j]],
[[-0.49864+0.95548j],
[+1.66153+0.01458j]]
]
solver.add_single_reflect(m, s11=-1)
# Add measurement of the open standard.
m = [
[[+0.98158-0.07944j],
[-0.02363-0.03527j]],
[[+0.92977-0.14323j],
[-0.11220-0.06234j]],
[[+0.82101-0.18232j],
[-0.55861+0.06766j]],
[[+0.73120-0.16877j],
[+0.18205+2.33926j]],
[[+0.69680-0.19129j],
[+1.69014+0.83232j]],
[[+0.66385-0.33378j],
[+1.67578+0.32863j]],
[[+0.47325-0.67191j],
[+1.66486+0.14719j]],
[[-0.31697-0.98110j],
[+1.66224+0.06780j]],
[[-1.13607+0.42159j],
[+1.66166+0.03142j]],
[[-0.06591-0.98338j],
[+1.66153+0.01458j]]
]
solver.add_single_reflect(m, s11=1)
# Add measurement of the load standard.
m = [
[[-0.01833-0.07175j],
[-0.02363-0.03527j]],
[[-0.07000-0.12666j],
[-0.11220-0.06234j]],
[[-0.17821-0.14664j],
[-0.55861+0.06766j]],
[[-0.26564-0.09196j],
[+0.18205+2.33926j]],
[[-0.28920-0.02642j],
[+1.69014+0.83232j]],
[[-0.27268+0.01546j],
[+1.67578+0.32863j]],
[[-0.24484+0.02301j],
[+1.66486+0.14719j]],
[[-0.23119+0.01417j],
[+1.66224+0.06780j]],
[[-0.22742+0.00702j],
[+1.66166+0.03142j]],
[[-0.22656+0.00331j],
[+1.66153+0.01458j]]
]
solver.add_single_reflect(m, s11=0)
# Add measurement of the through standard.
m = [
[[-0.06903-0.23630j],
[+0.97625-0.04409j]],
[[-0.15973-0.20673j],
[+0.88751-0.08134j]],
[[-0.27689-0.18154j],
[+0.44038+0.02676j]],
[[-0.36605-0.10220j],
[+1.17792+2.25123j]],
[[-0.38896-0.01829j],
[+2.67179+0.64359j]],
[[-0.36765+0.04647j],
[+2.59269-0.06904j]],
[[-0.31823+0.09069j],
[+2.30019-0.62390j]],
[[-0.22477+0.11374j],
[+1.33997-0.87735j]],
[[-0.13542-0.03136j],
[+1.08180+0.84320j]],
[[-0.24904+0.10025j],
[+0.84474-0.55477j]]
]
solver.add_through(m)
# Solve, add to Calset and save.
solver.solve()
solver.add_to_calset('cal2x1')
calset.save('2x1.vnacal')
Example of applying the calibration to a device under test, where we first measure the raw values of \(S_{11}\) and \(S_{21}\), then exchange the probes and measure the raw values of \(S_{22}\) and \(S_{12}\). With all four values and the calibration, we can solve for the full S-parameters of the device.
from libvna.cal import Calset
import numpy as np
# Load the calibration from file.
calset = Calset('2x1.vnacal')
calibration = calset.calibrations[0]
f_vector = calibration.frequency_vector
# Measured response with VNA port1 = DUT port 1, VNA port2 = DUT port 2:
m1 = np.asarray([
[[-0.07977-0.23276j],
[+0.97545-0.07557j]],
[[-0.17199-0.19365j],
[+0.88370-0.14911j]],
[[-0.29309-0.14964j],
[+0.42258-0.11850j]],
[[-0.39726-0.02181j],
[+1.09471+1.94626j]],
[[-0.43639+0.23960j],
[+2.28733+0.08400j]],
[[+0.05427+0.67277j],
[+1.42172-0.30696j]],
[[+0.73020-0.07176j],
[+1.45225+0.17829j]],
[[-0.01832-0.96433j],
[+1.66263+0.11608j]],
[[-1.18784+0.29378j],
[+1.66875+0.02373j]],
[[-0.00239-0.97477j],
[+1.66338+0.01588j]]
])
# Measured response with VNA port1 = DUT port 2, VNA port2 = DUT port 1:
m2 = np.asarray([
[[-0.07876-0.23282j],
[+0.97551-0.07542j]],
[[-0.16745-0.19410j],
[+0.88416-0.14880j]],
[[-0.27241-0.15354j],
[+0.42480-0.11842j]],
[[-0.30709-0.05914j],
[+1.10316+1.94103j]],
[[-0.15123-0.10240j],
[+2.28778+0.04046j]],
[[-0.51260-0.67585j],
[+1.33397-0.26085j]],
[[-1.22033+0.10430j],
[+1.48757+0.19477j]],
[[-0.46491+0.98049j],
[+1.66407+0.10736j]],
[[+0.73753-0.22745j],
[+1.66756+0.02531j]],
[[-0.55691+0.93303j],
[+1.66290+0.01577j]]
])
# Combine the measurements into a vector of 2x2 matrices,
# flipping the rows of m2 in the reversed measurement.
measured = np.concatenate((m1, np.flip(m2, axis=1)), axis=2)
# Apply calibration and save in Touchstone format.
result = calibration.apply(f_vector, measured)
result.save('2x1-corrected.s2p')
2x2 with A & B Measurements
In this example, our VNA measures full S parameters. In addition, it measures the transmitted power (a matrix) as well as the reflected power (b matrix). Having the a matrix makes it possible to compensate for errors in the RF switches without requiring separate calibration error terms for each switch setting. We use the TE10 calibration type, which consists of 8-term T error terms and two internal leakage terms. To reduce the number of calibration steps, we measure one-port standards two at a time: short-open, short-load and through.
from libvna.cal import Calset, CalType, Solver
import numpy as np
# Set the calibration frequency points.
fmin = 1.0e+06
fmax = 1.0e+09
f_vector = np.logspace(np.log10(fmin), np.log10(fmax), num=10)
# Create the calibration container and error term solver.
calset = Calset()
solver = Solver(calset, CalType.TE10, rows=2, columns=2,
frequency_vector=f_vector)
# Define the standards for our calibration kit.
short_std = calset.short_standard(
offset_z0=51.259595,
offset_delay=33.340790e-12,
offset_loss=5.460953e+9,
fmax=8.5e+9,
L=[-119.006943e-12, -1.310397249e-21, 1.511773982e-31, -91.480400e-42]
)
open_std = calset.open_standard(
offset_z0=51.635682,
offset_delay=37.636850e-12,
offset_loss=5.611771e+9,
fmax=8.5e+9,
C=[-93.936119e-15, 151.860439e-27, -786.852853e-36, 46.121820e-45]
)
load_std = calset.load_standard(
offset_z0=50.0,
offset_delay=0.0,
offset_loss=0.0,
fmax=8.5e+9,
Zl=50.0
)
through_std = calset.through_standard(
offset_z0=50.280012,
offset_delay=75.191505e-12,
offset_loss=6.731522e+9
)
# Add measurement of the short-open standard..
a = [
[[+0.99990-0.01414j, -0.00010+0.00000j],
[+0.00010-0.00000j, +0.99990-0.01414j]],
[[+0.99954-0.03047j, -0.00046+0.00001j],
[+0.00046-0.00001j, +0.99954-0.03047j]],
[[+0.99784-0.06564j, -0.00215+0.00014j],
[+0.00215-0.00014j, +0.99784-0.06564j]],
[[+0.98990-0.14141j, -0.00990+0.00141j],
[+0.00990-0.00141j, +0.98990-0.14141j]],
[[+0.95153-0.30403j, -0.04417+0.01411j],
[+0.04417-0.01411j, +0.95153-0.30403j]],
[[+0.74976-0.62730j, -0.16153+0.13515j],
[+0.16153-0.13515j, +0.74976-0.62730j]],
[[+0.00000-0.70711j, -0.00000+0.70711j],
[+0.00000-0.70711j, +0.00000-0.70711j]],
[[-0.16153-0.13515j, +0.74976+0.62730j],
[-0.74976-0.62730j, -0.16153-0.13515j]],
[[-0.04417-0.01411j, +0.95153+0.30403j],
[-0.95153-0.30403j, -0.04417-0.01411j]],
[[-0.00990-0.00141j, +0.98990+0.14141j],
[-0.98990-0.14141j, -0.00990-0.00141j]]
]
b = [
[[-0.97574+0.08783j, +0.00009-0.00078j],
[+0.07111+0.09941j, +1.00026-0.02482j]],
[[-0.89194+0.16186j, +0.00036-0.00173j],
[+0.18924+0.08122j, +1.00154-0.05362j]],
[[-0.62052+0.06760j, +0.00109-0.00370j],
[+0.26821-0.08665j, +1.00776-0.11666j]],
[[-1.17519-0.42619j, +0.01059-0.00332j],
[+0.15769-0.34680j, +1.04299-0.26490j]],
[[-1.19737+4.34464j, +0.05022-0.21730j],
[-0.21301-0.50449j, +1.35530-1.00482j]],
[[-0.67242+0.73937j, +0.12029-0.18612j],
[-0.71117-0.08149j, -0.10700-0.30657j]],
[[+0.19204+0.36781j, -0.29959-0.35738j],
[-0.54955+0.49518j, -0.19715-0.47243j]],
[[-0.03542+0.34233j, -0.19874-0.90807j],
[+0.35274+0.57472j, -0.41918-0.45205j]],
[[+0.05014+0.34564j, -0.21004-0.35795j],
[-0.23970+0.47211j, -0.70281-0.25878j]],
[[+0.37293+0.44903j, -0.39872+0.28616j],
[+0.53285+0.67023j, -0.72937-0.12494j]]
]
solver.add_double_reflect(a=a, b=b, s11=short_std, s22=open_std)
# Add measurement of the short-load stnadard.
a = [
[[+0.99990-0.01414j, -0.00010+0.00000j],
[+0.00010-0.00000j, +0.99990-0.01414j]],
[[+0.99954-0.03047j, -0.00046+0.00001j],
[+0.00046-0.00001j, +0.99954-0.03047j]],
[[+0.99784-0.06564j, -0.00215+0.00014j],
[+0.00215-0.00014j, +0.99784-0.06564j]],
[[+0.98990-0.14141j, -0.00990+0.00141j],
[+0.00990-0.00141j, +0.98990-0.14141j]],
[[+0.95153-0.30403j, -0.04417+0.01411j],
[+0.04417-0.01411j, +0.95153-0.30403j]],
[[+0.74976-0.62730j, -0.16153+0.13515j],
[+0.16153-0.13515j, +0.74976-0.62730j]],
[[+0.00000-0.70711j, -0.00000+0.70711j],
[+0.00000-0.70711j, +0.00000-0.70711j]],
[[-0.16153-0.13515j, +0.74976+0.62730j],
[-0.74976-0.62730j, -0.16153-0.13515j]],
[[-0.04417-0.01411j, +0.95153+0.30403j],
[-0.95153-0.30403j, -0.04417-0.01411j]],
[[-0.00990-0.00141j, +0.98990+0.14141j],
[-0.98990-0.14141j, -0.00990-0.00141j]]
]
b = [
[[-0.97574+0.08783j, +0.00009-0.00078j],
[+0.07101+0.09942j, -0.00002-0.00069j]],
[[-0.89194+0.16186j, +0.00036-0.00173j],
[+0.18877+0.08124j, -0.00013-0.00150j]],
[[-0.62052+0.06760j, +0.00109-0.00370j],
[+0.26603-0.08641j, -0.00076-0.00296j]],
[[-1.17519-0.42619j, +0.01059-0.00332j],
[+0.14723-0.34419j, -0.00232-0.00331j]],
[[-1.19737+4.34464j, +0.05022-0.21730j],
[-0.27551-0.45753j, +0.00880+0.00702j]],
[[-0.67242+0.73937j, +0.12029-0.18612j],
[-0.66163-0.02033j, +0.12296-0.02270j]],
[[+0.19204+0.36781j, -0.29959-0.35738j],
[-0.23146+0.47822j, +0.12094-0.48939j]],
[[-0.03542+0.34233j, -0.19874-0.90807j],
[-0.02048+0.29574j, -0.49959-0.51215j]],
[[+0.05014+0.34564j, -0.21004-0.35795j],
[+0.23530+0.38697j, -0.68076-0.26274j]],
[[+0.37293+0.44903j, -0.39872+0.28616j],
[+0.40962+0.19533j, -0.73061-0.12969j]]
]
solver.add_double_reflect(a=a, b=b, s11=short_std, s22=load_std)
# Add measurement of the through standard. Notice that we have to add
# the calkit standard as a 'line'.
a = [
[[+0.99990-0.01414j, -0.00010+0.00000j],
[+0.00010-0.00000j, +0.99990-0.01414j]],
[[+0.99954-0.03047j, -0.00046+0.00001j],
[+0.00046-0.00001j, +0.99954-0.03047j]],
[[+0.99784-0.06564j, -0.00215+0.00014j],
[+0.00215-0.00014j, +0.99784-0.06564j]],
[[+0.98990-0.14141j, -0.00990+0.00141j],
[+0.00990-0.00141j, +0.98990-0.14141j]],
[[+0.95153-0.30403j, -0.04417+0.01411j],
[+0.04417-0.01411j, +0.95153-0.30403j]],
[[+0.74976-0.62730j, -0.16153+0.13515j],
[+0.16153-0.13515j, +0.74976-0.62730j]],
[[+0.00000-0.70711j, -0.00000+0.70711j],
[+0.00000-0.70711j, +0.00000-0.70711j]],
[[-0.16153-0.13515j, +0.74976+0.62730j],
[-0.74976-0.62730j, -0.16153-0.13515j]],
[[-0.04417-0.01411j, +0.95153+0.30403j],
[-0.95153-0.30403j, -0.04417-0.01411j]],
[[-0.00990-0.00141j, +0.98990+0.14141j],
[-0.98990-0.14141j, -0.00990-0.00141j]]
]
b = [
[[+0.02521+0.05700j, +0.99915-0.03325j],
[+1.07018+0.06693j, -0.00082-0.00070j]],
[[+0.11327+0.09499j, +0.99699-0.07154j],
[+1.18586+0.01139j, -0.00436-0.00138j]],
[[+0.40505-0.08157j, +0.98658-0.15354j],
[+1.25370-0.23657j, -0.02136-0.00034j]],
[[-0.04799-0.80684j, +0.93832-0.31909j],
[+1.08523-0.66343j, -0.10250+0.03024j]],
[[-0.54508-0.51940j, +0.61221-0.44377j],
[+0.29399-0.88889j, -0.48197+0.45668j]],
[[-0.87490+0.95517j, -0.11108-0.42040j],
[-0.98001-0.26316j, -0.07839-0.27819j]],
[[-0.06984+0.52046j, -0.69818-0.24867j],
[-0.84953+0.62264j, +0.16333-0.60633j]],
[[+0.49294+0.31557j, -0.06253-0.93060j],
[+0.42964+0.51381j, -0.96032-0.43356j]],
[[+0.11605+0.90873j, -0.19605-0.40183j],
[-0.15983+0.54598j, -0.76567-0.81641j]],
[[+0.14410-0.12478j, -0.23372+0.17484j],
[+0.66081+0.57056j, -0.50092+0.44898j]]
]
solver.add_line(a=a, b=b, s=through_std)
# Solve, add to Calset and save.
solver.solve()
solver.add_to_calset('cal2x2')
calset.save('2x2ab.vnacal')
Apply the calibration to a device under test.
from libvna.cal import Calset
# Load the calibration from file.
calset = Calset('2x2ab.vnacal')
calibration = calset.calibrations[0]
f_vector = calibration.frequency_vector
# Measured response
a = [
[[+0.99990-0.01414j, -0.00010+0.00000j],
[+0.00010-0.00000j, +0.99990-0.01414j]],
[[+0.99954-0.03047j, -0.00046+0.00001j],
[+0.00046-0.00001j, +0.99954-0.03047j]],
[[+0.99784-0.06564j, -0.00215+0.00014j],
[+0.00215-0.00014j, +0.99784-0.06564j]],
[[+0.98990-0.14141j, -0.00990+0.00141j],
[+0.00990-0.00141j, +0.98990-0.14141j]],
[[+0.95153-0.30403j, -0.04417+0.01411j],
[+0.04417-0.01411j, +0.95153-0.30403j]],
[[+0.74976-0.62730j, -0.16153+0.13515j],
[+0.16153-0.13515j, +0.74976-0.62730j]],
[[+0.00000-0.70711j, -0.00000+0.70711j],
[+0.00000-0.70711j, +0.00000-0.70711j]],
[[-0.16153-0.13515j, +0.74976+0.62730j],
[-0.74976-0.62730j, -0.16153-0.13515j]],
[[-0.04417-0.01411j, +0.95153+0.30403j],
[-0.95153-0.30403j, -0.04417-0.01411j]],
[[-0.00990-0.00141j, +0.98990+0.14141j],
[-0.98990-0.14141j, -0.00990-0.00141j]]
]
b = [
[[+0.02484+0.05681j, +0.99784-0.06400j],
[+1.06887+0.03618j, -0.00050-0.00083j]],
[[+0.11203+0.09445j, +0.99040-0.13764j],
[+1.17927-0.05471j, -0.00233-0.00134j]],
[[+0.39900-0.08492j, +0.95552-0.29343j],
[+1.22264-0.37646j, -0.01014+0.00200j]],
[[-0.09645-0.82985j, +0.80228-0.59409j],
[+0.94946-0.93843j, -0.02696+0.05307j]],
[[-1.07837-0.52427j, +0.43817-1.13177j],
[+0.14427-1.56386j, +0.57552+0.74216j]],
[[-0.28945+0.70349j, -0.02569-0.32482j],
[-0.78902-0.18363j, -0.17371-0.10103j]],
[[-0.88457-0.31517j, +0.40003+0.51203j],
[-0.73700+1.17358j, -0.00761+0.01955j]],
[[+0.82068+0.27739j, -3.13187-1.28322j],
[+1.86492-0.83789j, -0.31756-0.61062j]],
[[+0.34119+0.50882j, -3.89943-1.79941j],
[-1.10494+4.83592j, -0.86278-0.15251j]],
[[+0.32777+0.45761j, -1.90183-3.70638j],
[+8.00354+4.77445j, -0.59448-0.05255j]]
]
# Apply calibration and save in Touchstone format.
result = calibration.apply(f_vector, a=a, b=b)
result.save('2x2ab-corrected.s2p')
Two-Port Reflect Only
In this example, we calibrate a two-port VNA for reflection measurements only, useful in cases where we don’t need to make through measurements. This example demonstrates using multiple solvers simultaneously and saving more than one calibration in the same calibration file. To keep the example simple, we’re using perfect standards.
from libvna.cal import Calset, CalType, Solver
import numpy as np
# Set the calibration frequency points.
fmin = 1.0e+06
fmax = 1.0e+09
f_vector = np.logspace(np.log10(fmin), np.log10(fmax), num=10)
# Create two error solvers.
calset = Calset()
solver1 = Solver(calset, CalType.E12, rows=1, columns=1,
frequency_vector=f_vector)
solver2 = Solver(calset, CalType.E12, rows=1, columns=1,
frequency_vector=f_vector)
# Define the standards for our calibration kit.
short_std = calset.short_standard(
offset_z0=51.259595,
offset_delay=33.340790e-12,
offset_loss=5.460953e+9,
fmax=8.5e+9,
L=[-119.006943e-12, -1.310397249e-21, 1.511773982e-31, -91.480400e-42]
)
open_std = calset.open_standard(
offset_z0=51.635682,
offset_delay=37.636850e-12,
offset_loss=5.611771e+9,
fmax=8.5e+9,
C=[-93.936119e-15, 151.860439e-27, -786.852853e-36, 46.121820e-45]
)
load_std = calset.load_standard(
offset_z0=50.0,
offset_delay=0.0,
offset_loss=0.0,
fmax=8.5e+9,
Zl=50.0
)
# Add measurement of load standard on port 1 and short standard on port 2.
m = np.asarray([
[[+0.21656+0.02026j, -0.99971+0.00469j]],
[[+0.21156+0.00901j, -0.99965+0.00993j]],
[[+0.21051+0.00414j, -0.99983+0.02110j]],
[[+0.21028+0.00192j, -1.00159+0.04449j]],
[[+0.21024+0.00089j, -1.01544+0.09003j]],
[[+0.21023+0.00041j, -1.17841+0.21871j]],
[[+0.21022+0.00019j, -0.80815+1.30306j]],
[[+0.21022+0.00009j, +0.89369+1.04472j]],
[[+0.21022+0.00004j, -0.23986-1.22135j]],
[[+0.21022+0.00002j, +0.58026-0.91606j]]
])
solver1.add_single_reflect(m[:, :, 0].reshape((len(f_vector), 1, 1)), s11=load_std)
solver2.add_single_reflect(m[:, :, 1].reshape((len(f_vector), 1, 1)), s11=short_std)
# Add measurement of short standard on port 1 and open standard on port 2.
m = np.asarray([
[[-0.78287+0.03979j, +0.99972-0.01629j]],
[[-0.78699+0.05093j, +0.99893-0.03507j]],
[[-0.78460+0.09410j, +0.99543-0.07544j]],
[[-0.76976+0.19436j, +0.97920-0.16124j]],
[[-0.70230+0.40538j, +0.90270-0.33145j]],
[[-0.41113+0.78046j, +0.62814-0.50883j]],
[[+0.56402+0.93123j, +0.36794-0.63165j]],
[[+0.72775-0.84899j, -0.46683-0.60725j]],
[[+1.10317+0.43536j, +0.04243+0.86291j]],
[[-0.67697+0.45907j, -0.53402+0.81509j]]
])
solver1.add_single_reflect(m[:, :, 0].reshape((len(f_vector), 1, 1)), s11=short_std)
solver2.add_single_reflect(m[:, :, 1].reshape((len(f_vector), 1, 1)), s11=open_std)
# Add measurement of open standard on port 1 and load standard on port 2.
m = np.asarray([
[[+1.21622+0.00090j, -0.00000+0.00012j]],
[[+1.21046-0.03269j, -0.00000+0.00027j]],
[[+1.20613-0.08559j, -0.00000+0.00058j]],
[[+1.19109-0.19042j, -0.00000+0.00125j]],
[[+1.12375-0.40399j, -0.00001+0.00268j]],
[[+0.83199-0.78123j, -0.00004+0.00578j]],
[[-0.14663-0.93181j, -0.00017+0.01250j]],
[[-0.30239+0.85442j, -0.00080+0.02731j]],
[[-0.68846-0.41966j, -0.00363+0.06296j]],
[[+1.06278-0.47780j, -0.00930+0.19720j]]
])
solver1.add_single_reflect(m[:, :, 0].reshape((len(f_vector), 1, 1)), s11=open_std)
solver2.add_single_reflect(m[:, :, 1].reshape((len(f_vector), 1, 1)), s11=load_std)
# Solve both calibrations, add to Calset and save.
solver1.solve()
solver2.solve()
solver1.add_to_calset('port 1')
solver2.add_to_calset('port 2')
calset.save('2PR.vnacal')
Apply the calibration to a device under test.
from libvna.cal import Calset
import numpy as np
# Load the calibration from file.
calset = Calset('2PR.vnacal')
calibration1 = calset.calibrations['port 1']
calibration2 = calset.calibrations['port 2']
f_vector = calibration1.frequency_vector[...]
# Measured response:
measured = np.asarray([
[[+1.21537-0.02508j, -0.99968+0.02919j]],
[[+1.20656-0.08851j, -0.99876+0.06289j]],
[[+1.18812-0.20460j, -0.99464+0.13558j]],
[[+1.10933-0.43462j, -0.97570+0.29312j]],
[[+0.78114-0.81884j, -0.88458+0.64837j]],
[[-0.18560-0.91551j, +0.02988+1.47087j]],
[[-0.61369+0.55947j, +0.90731+0.03885j]],
[[+1.17116-0.25390j, -0.00421-0.78695j]],
[[+0.77308+0.81758j, -0.28904+0.81441j]],
[[-0.77930-0.08387j, -0.74127+0.52192j]]
])
# Apply calibration to each port and save in Touchstone format.
m1 = measured[:, 0, 0].reshape((len(f_vector), 1, 1))
m2 = measured[:, 0, 1].reshape((len(f_vector), 1, 1))
result1 = calibration1.apply(f_vector, m1)
result1.save('2PR1-corrected.s1p')
result2 = calibration2.apply(f_vector, m2)
result2.save('2PR2-corrected.s1p')
TRL
Example of through, reflect, line (TRL) calibration. We need to know our reflect and line standards only approximately – the calibration process solves for the actual parameters of the standards as well as the error terms.
from libvna.cal import Calset, CalType, Solver
from libvna.data import NPData, PType
import math
import numpy as np
# Set the calibration frequency points.
fmin = 1.0e+09
fmax = 8.0e+09
f_vector = np.linspace(fmin, fmax, num=10)
# Create the calibration container and error term solver.
calset = Calset()
solver = Solver(calset, CalType.TE10, rows=2, columns=2,
frequency_vector=f_vector)
# Our line is a 1cm long trace of microstrip on FR4 at 50 ohms.
# We estimate εr_eff to be 2.75. From these, we can estimate the
# transmission parameter.
#
line_length = 0.01 # length in meters
εr_eff = 2.75 # effective permittivity
vf = 1 / math.sqrt(εr_eff) # velocity factor
c = 2.9979246e+8 # speed of light (m/s)
line_estimated = np.exp(-2.0j * math.pi / (c * vf) * line_length * f_vector)
# Create unknown parameters for the reflect and line, giving
# estimated values for each.
R = calset.unknown_parameter(-1)
L = calset.unknown_parameter((f_vector, line_estimated))
# Add measurement of the through standard.
m = [
[[+0.01225-0.00192j, +0.40879+0.32741j],
[+0.66817+0.33782j, -0.73177-0.05376j]],
[[+0.02647-0.02319j, +0.14407-0.89089j],
[+0.19088-1.06383j, -0.71191+0.16362j]],
[[+0.01560-0.06068j, -1.20029-0.47901j],
[-1.27700-0.60742j, -0.67702-0.10569j]],
[[-0.01789-0.07253j, -0.73301+0.87574j],
[-0.86337+0.81913j, -0.61514+0.14759j]],
[[-0.03759-0.06440j, +0.59389+0.34433j],
[+0.44819+0.35538j, -0.71956-0.09837j]],
[[-0.04470-0.05605j, -0.01768-0.94064j],
[-0.15763-0.87290j, -0.56263+0.11327j]],
[[-0.04640-0.05097j, -1.26108-0.24924j],
[-1.38383-0.13624j, -0.75242-0.06735j]],
[[-0.04581-0.04852j, -0.49829+0.94974j],
[-0.59793+1.09779j, -0.53317+0.06943j]],
[[-0.04412-0.04788j, +0.64914+0.12095j],
[+0.57519+0.29548j, -0.76874-0.02682j]],
[[-0.04176-0.04850j, -0.24391-0.97008j],
[-0.29158-0.77609j, -0.52294+0.02326j]]
]
solver.add_through(m)
# Add measurement of the unknown reflect standard.
m = [
[[+0.60962-0.43380j, -0.41143-0.22932j],
[-0.15205-0.21890j, -0.67826+0.85470j]],
[[+0.56917-0.46865j, -0.36039-0.04635j],
[-0.31359-0.21929j, -1.04853-0.71124j]],
[[+0.53509-0.51199j, -0.33451-0.02238j],
[-0.41122-0.15079j, -0.09925+0.61476j]],
[[+0.50112-0.55751j, -0.32443-0.01462j],
[-0.45479-0.07122j, -1.33856-0.37457j]],
[[+0.46644-0.60270j, -0.31954-0.01091j],
[-0.46523+0.00014j, +0.09599+0.22417j]],
[[+0.43105-0.64637j, -0.31680-0.00874j],
[-0.45675+0.05900j, -1.42696+0.00000j]],
[[+0.39521-0.68786j, -0.31512-0.00731j],
[-0.43786+0.10569j, +0.11177-0.16730j]],
[[+0.35926-0.72673j, -0.31401-0.00630j],
[-0.41365+0.14175j, -1.34732+0.37449j]],
[[+0.32360-0.76272j, -0.31324-0.00554j],
[-0.38719+0.16898j, -0.05064-0.51690j]],
[[+0.28867-0.79568j, -0.31268-0.00495j],
[-0.36035+0.18903j, -1.10614+0.66825j]]
]
solver.add_double_reflect(m, s11=R, s22=R)
# Add measurement of the unknown line standard.
m = [
[[+0.00690-0.00729j, +0.37065-0.01638j],
[+0.63004-0.00597j, -0.80218-0.03448j]],
[[-0.00347-0.02312j, -0.39675-0.80044j],
[-0.34994-0.97337j, -0.62442+0.07775j]],
[[-0.02801-0.01120j, -1.00372+0.23298j],
[-1.08043+0.10456j, -0.73410+0.04146j]],
[[-0.01651+0.01670j, +0.10772+0.51342j],
[-0.02264+0.45681j, -0.62006-0.03241j]],
[[+0.01033+0.01683j, +0.02972-0.55993j],
[-0.11597-0.54889j, -0.64540+0.07538j]],
[[+0.02602+0.00093j, -0.92058-0.16379j],
[-1.06053-0.09605j, -0.69029-0.02208j]],
[[+0.02961-0.01722j, -0.27740+0.59156j],
[-0.40014+0.70456j, -0.59907+0.01412j]],
[[+0.02522-0.03164j, +0.22498-0.22005j],
[+0.12534-0.07199j, -0.68435+0.03919j]],
[[+0.01704-0.04016j, -0.67218-0.43769j],
[-0.74614-0.26317j, -0.63808-0.03212j]],
[[+0.00871-0.04267j, -0.60278+0.45668j],
[-0.65046+0.65066j, -0.62862+0.04425j]]
]
solver.add_line(m, [[0, L], [L, 0]])
# Solve, add to Calset and save.
solver.solve()
solver.add_to_calset('mycal')
calset.save('TRL.vnacal')
# Save the solved R
calset.parameter_matrix([[R]]).to_npdata(f_vector).save("TRL-R.s1p")
# Save the solved L
calset.parameter_matrix([[0, L], [L, 0]]).to_npdata(f_vector).save("TRL-L.s2p")
Apply the calibration to a device under test.
from libvna.cal import Calset
# Load the calibration from file.
calset = Calset('TRL.vnacal')
calibration = calset.calibrations[0]
f_vector = calibration.frequency_vector
# Measured response
m = [
[[-1.05612+0.28485j, -0.41308-0.23083j],
[-0.15370-0.22042j, -0.89611+1.03984j]],
[[-1.08234+0.31254j, -0.36083-0.04574j],
[-0.31402-0.21868j, -0.78252-0.97811j]],
[[-1.09084+0.30815j, -0.33419-0.02220j],
[-0.41090-0.15062j, -0.36947+0.99689j]],
[[-1.09484+0.28970j, -0.32435-0.01481j],
[-0.45471-0.07141j, -1.10160-0.84872j]],
[[-1.09696+0.26421j, -0.31966-0.01095j],
[-0.46536+0.00009j, -0.09980+0.78792j]],
[[-1.09775+0.23472j, -0.31682-0.00865j],
[-0.45678+0.05909j, -1.29764-0.65847j]],
[[-1.09735+0.20267j, -0.31505-0.00730j],
[-0.43779+0.10570j, +0.08184+0.57870j]],
[[-1.09576+0.16890j, -0.31400-0.00635j],
[-0.41364+0.14170j, -1.44796-0.43935j]],
[[-1.09296+0.13387j, -0.31328-0.00555j],
[-0.38723+0.16898j, +0.20551+0.33529j]],
[[-1.08894+0.09792j, -0.31268-0.00492j],
[-0.36036+0.18907j, -1.53407-0.18589j]]
]
# Apply calibration and save in Touchstone format.
result = calibration.apply(f_vector, m)
result.save('TRL-corrected.s2p')
Unknown Through
Example of unknown through calibration. For this calibration, we need three reflect standards on each port and the unknown through between them. To reduce calibration steps, we measure two reflect standards at a time. We have arbitrarily selected them as short-open, open-load, and load-short.
from libvna.cal import Calset, CalType, Solver
from libvna.data import NPData, PType
import math
import numpy as np
# Set the calibration frequency points.
fmin = 1.0e+06
fmax = 1.0e+09
f_vector = np.logspace(np.log10(fmin), np.log10(fmax), num=10)
# Create the calibration container and error term solver.
calset = Calset()
solver = Solver(calset, CalType.TE10, rows=2, columns=2,
frequency_vector=f_vector)
# Define the short, open and load standards from our calibration kit.
short_std = calset.short_standard(
offset_z0=51.259595,
offset_delay=33.340790e-12,
offset_loss=5.460953e+9,
fmax=8.5e+9,
L=[-119.006943e-12, -1.310397249e-21, 1.511773982e-31, -91.480400e-42]
)
open_std = calset.open_standard(
offset_z0=51.635682,
offset_delay=37.636850e-12,
offset_loss=5.611771e+9,
fmax=8.5e+9,
C=[-93.936119e-15, 151.860439e-27, -786.852853e-36, 46.121820e-45]
)
load_std = calset.load_standard(
offset_z0=50.0,
offset_delay=0.0,
offset_loss=0.0,
fmax=8.5e+9,
Zl=50.0
)
# Create an unknown parameter for our through standard. As long as the
# phase shift in the through remains safely below 90 degrees, we can
# give 1 as the estimate. For a larger phase shift, we would need to
# give a more accurate estimate for each frequency.
T = calset.unknown_parameter(1)
# Add measurement of the short-open standard.
m = [
[[-0.99670+0.03392j, +0.01404-0.06817j],
[-0.00095-0.02105j, +0.78144-1.15178j]],
[[-0.98674+0.07003j, +0.01999-0.24018j],
[-0.00439-0.04507j, +0.06520-0.03879j]],
[[-0.95370+0.13013j, -0.41542-0.18505j],
[-0.01988-0.09430j, +0.29664-0.06132j]],
[[-0.89281+0.20574j, -0.33986-0.02676j],
[-0.08292-0.17852j, +0.31272-0.19034j]],
[[-0.81895+0.33642j, -0.31654-0.00853j],
[-0.25983-0.23204j, +0.22935-0.39589j]],
[[-0.62854+0.61079j, -0.31159-0.00361j],
[-0.45043-0.08388j, -0.00886-0.80176j]],
[[+0.05597+0.87823j, -0.31053-0.00164j],
[-0.39800+0.15894j, -1.13456-0.93273j]],
[[+0.83730-0.34890j, -0.31030-0.00076j],
[-0.18545+0.21787j, -0.89669+1.02739j]],
[[-0.18895+0.87454j, -0.31025-0.00035j],
[-0.06157+0.13731j, -1.67128+0.24119j]],
[[+0.70419-0.69883j, -0.31024-0.00016j],
[-0.02537+0.06858j, -1.04280-0.96755j]]
]
solver.add_double_reflect(m, s11=short_std, s22=open_std)
# Add measurement of the open-load standard.
m = [
[[+1.00193+0.00085j, +0.01404-0.06817j],
[-0.00095-0.02105j, -0.21808-1.13020j]],
[[+1.00898+0.00007j, +0.01999-0.24018j],
[-0.00439-0.04507j, -0.93306+0.00766j]],
[[+1.03518-0.01419j, -0.41542-0.18505j],
[-0.01988-0.09430j, -0.69611+0.03828j]],
[[+1.08852-0.10104j, -0.33986-0.02676j],
[-0.08292-0.17852j, -0.65537+0.01953j]],
[[+1.08788-0.35163j, -0.31654-0.00853j],
[-0.25983-0.23204j, -0.64703+0.00922j]],
[[+0.83842-0.79902j, -0.31159-0.00361j],
[-0.45043-0.08388j, -0.64526+0.00429j]],
[[-0.12032-1.14081j, -0.31053-0.00164j],
[-0.39800+0.15894j, -0.64487+0.00199j]],
[[-0.99463+0.47978j, -0.31030-0.00076j],
[-0.18545+0.21787j, -0.64479+0.00093j]],
[[+0.17195-1.09123j, -0.31025-0.00035j],
[-0.06157+0.13731j, -0.64477+0.00043j]],
[[-0.66061+0.74477j, -0.31024-0.00016j],
[-0.02537+0.06858j, -0.64477+0.00020j]]
]
solver.add_double_reflect(m, s11=open_std, s22=load_std)
# Add measurement of the load-short standard.
m = [
[[+0.00000-0.00004j, +0.01404-0.06817j],
[-0.00095-0.02105j, -1.21756-1.10999j]],
[[+0.00000-0.00009j, +0.01999-0.24018j],
[-0.00439-0.04507j, -1.93194+0.05106j]],
[[+0.00000-0.00019j, -0.41542-0.18505j],
[-0.01988-0.09430j, -1.69277+0.13173j]],
[[+0.00000-0.00041j, -0.33986-0.02676j],
[-0.08292-0.17852j, -1.64240+0.22265j]],
[[+0.00002-0.00088j, -0.31654-0.00853j],
[-0.25983-0.23204j, -1.57113+0.47281j]],
[[+0.00008-0.00190j, -0.31159-0.00361j],
[-0.45043-0.08388j, -1.14689+0.83091j]],
[[+0.00038-0.00410j, -0.31053-0.00164j],
[-0.39800+0.15894j, -0.16494+0.81492j]],
[[+0.00178-0.00893j, -0.31030-0.00076j],
[-0.18545+0.21787j, -0.43631-0.91575j]],
[[+0.00848-0.02015j, -0.31025-0.00035j],
[-0.06157+0.13731j, +0.26872-0.20863j]],
[[+0.04500-0.05519j, -0.31024-0.00016j],
[-0.02537+0.06858j, -0.31759+0.87572j]]
]
solver.add_double_reflect(m, s11=load_std, s22=short_std)
# Add measurement of the unknown through standard. Note that we
# have to add it as a "line".
m = [
[[-0.00010-0.00083j, +1.00863-0.08695j],
[+0.99363-0.03984j, -0.21552-1.11307j]],
[[-0.00046-0.00175j, +1.01144-0.28056j],
[+0.98706-0.08545j, -0.92190+0.04204j]],
[[-0.00212-0.00346j, +0.56932-0.27181j],
[+0.96487-0.18106j, -0.65495+0.09402j]],
[[-0.00943-0.00431j, +0.62476-0.21214j],
[+0.88171-0.36390j, -0.55863+0.06439j]],
[[-0.02547+0.02446j, +0.57389-0.39146j],
[+0.63060-0.61497j, -0.52380-0.01105j]],
[[+0.05903+0.01892j, +0.31077-0.73924j],
[+0.17194-0.81950j, -0.57058-0.09777j]],
[[+0.01568-0.05486j, -0.60406-0.89964j],
[-0.69153-0.73905j, -0.72075-0.08355j]],
[[-0.04172+0.00930j, -0.85469+0.72578j],
[-0.72984+0.94441j, -0.62650+0.08794j]],
[[+0.00372-0.06088j, -0.98137-0.51984j],
[-0.73269-0.38218j, -0.68719+0.04841j]],
[[+0.04664-0.02276j, +0.44307-0.02562j],
[+0.72795+0.04313j, -0.69001-0.00967j]]
]
solver.add_line(m, [[0, T], [T, 0]])
# Solve, add to Calset and save.
solver.solve()
solver.add_to_calset('mycal')
calset.save('UT.vnacal')
# Save the solved T
calset.parameter_matrix([[0, T], [T, 0]]).to_npdata(f_vector).save("UT-T.s2p")
Apply the calibration to a device under test.
from libvna.cal import Calset
# Load the calibration from file.
calset = Calset('UT.vnacal')
calibration = calset.calibrations[0]
f_vector = calibration.frequency_vector
# Measured response
m = [
[[-0.00064-0.00081j, +1.01267-0.11783j],
[+0.99768-0.07071j, -0.21394-1.11311j]],
[[-0.00297-0.00147j, +1.01405-0.34713j],
[+0.98967-0.15202j, -0.91493+0.04050j]],
[[-0.01339-0.00044j, +0.55700-0.41473j],
[+0.95255-0.32399j, -0.62929+0.07845j]],
[[-0.05355+0.02476j, +0.52844-0.50588j],
[+0.78538-0.65764j, -0.50544-0.02605j]],
[[-0.08795+0.21654j, +0.12057-0.84604j],
[+0.17728-1.06955j, -0.55366-0.31512j]],
[[+0.43256+0.53986j, -0.83008-0.48819j],
[-0.96892-0.56845j, -1.33225-0.34254j]],
[[+0.71310-0.83606j, -0.40271+0.19374j],
[-0.49018+0.35432j, -0.78745+0.93977j]],
[[-1.11505+0.01861j, -0.26984-0.02349j],
[-0.14499+0.19514j, -0.11540-0.78525j]],
[[+0.53635-0.97659j, -0.30558+0.00818j],
[-0.05690+0.14585j, +0.29432+0.08087j]],
[[-0.97085+0.31228j, -0.31189-0.00142j],
[-0.02702+0.06733j, -0.72685+0.93482j]]
]
# Apply calibration and save in Touchstone format.
result = calibration.apply(f_vector, m)
result.save('UT-corrected.s2p')
Test Fixture Embedding and De-Embedding
Calibrate the VNA, embedding a male-to-male coaxial adapter between VNA and standards.
from libvna.cal import Calset, CalType, Solver
import numpy as np
# Set the calibration frequency points.
fmin = 1.0e+08
fmax = 8.5e+09
f_vector = np.logspace(np.log10(fmin), np.log10(fmax), num=10)
# Create the calibration container
calset = Calset()
# Make calibration standards. These are 3.5mm female.
short_standard = calset.short_standard(
offset_z0=50.0,
offset_delay=33.356e-15,
offset_loss=2.36e+9,
fmax=8.5e+9,
L=[-44.000e-12, 3.7000e-21, -250.00e-33, 5.0000e-42]
)
open_standard = calset.open_standard(
offset_z0=50.0,
offset_delay=32.032e-15,
offset_loss=2.2e+9,
fmax=8.5e+9,
C=[-17.500e-15, -2.0000e-24, 140.00e-36, -2.7000e-45]
)
load_standard = calset.load_standard(
offset_z0=50.0,
offset_delay=0.0,
offset_loss=0.0,
fmax=8.5e+9,
Zl=50.0
)
# The VNA expects male standards, so we insert this male-male adapter,
# keeping the VNA reference plane on the VNA side of the adapter.
adapter = calset.through_standard(
offset_z0=50.0,
offset_delay=59.0e-12,
offset_loss=2.51e+9,
fmax=8.5e+9
)
# Embed the standards.
embedded_short = short_standard.embed(fixture=adapter)
embedded_open = open_standard.embed(fixture=adapter)
embedded_load = load_standard.embed(fixture=adapter)
# Create the error term solver.
solver = Solver(calset, CalType.E12, rows=1, columns=1,
frequency_vector=f_vector)
# Add measurement of the short_standard standard.
m = [
[[+0.21032+0.74378j]],
[[+0.84940-0.12228j]],
[[+0.07366-1.26448j]],
[[-1.43090-0.08847j]],
[[-0.45898-1.02260j]],
[[-0.97551-0.92650j]],
[[+0.08015-0.66386j]],
[[+0.34428-0.52096j]],
[[+0.44260+0.23022j]],
[[-0.03932+0.51845j]]
]
solver.add_single_reflect(m, s11=embedded_short)
# Add measurement of the open_standard standard.
m = [
[[+0.32966-1.31867j]],
[[-0.77399-1.39322j]],
[[-1.17867+0.36595j]],
[[+0.48086-0.81264j]],
[[-0.68831+0.98379j]],
[[+0.02500+0.77240j]],
[[-1.16898+0.78903j]],
[[-1.31770+0.51611j]],
[[-1.39533-0.42316j]],
[[-1.44447-0.87403j]]
]
solver.add_single_reflect(m, s11=embedded_open)
# Add measurement of the load_standard standard.
m = [
[[+0.27475-0.09570j]],
[[+0.20021-0.66035j]],
[[-0.46926-0.61810j]],
[[-0.62028-0.33292j]],
[[-0.64639-0.18877j]],
[[-0.65102-0.11061j]],
[[-0.65304-0.06932j]],
[[-0.65200-0.04398j]],
[[-0.64924-0.02534j]],
[[-0.65156-0.01534j]]
]
solver.add_single_reflect(m, s11=embedded_load)
# Solve, add to Calset and save.
solver.solve()
solver.add_to_calset('1-port')
calset.save('embed.vnacal')
Measure the DUT with the same coaxial adapter and de-embed the adapter from the result.
from libvna.cal import Calset
# Load the calibration from file.
calset = Calset('embed.vnacal')
calibration = calset.calibrations['1-port']
f_vector = calibration.frequency_vector
# 3.5mm male-to-male adapter to de-embed
adapter = calset.through_standard(
offset_z0=50.0,
offset_delay=59.0e-12,
offset_loss=2.51e+9,
fmax=8.5e+9
)
# Measured response:
measured = [
[[+0.63866+0.36112j]],
[[+0.61421-1.08132j]],
[[-0.87148+0.03290j]],
[[-1.05811-1.07821j]],
[[+0.01457-0.70373j]],
[[-0.63477-0.95584j]],
[[+0.16831-0.45311j]],
[[+0.38071-0.34991j]],
[[+0.43026+0.31522j]],
[[-0.04294+0.52263j]]
]
# Apply calibration.
result = calibration.apply(f_vector, measured)
# De-embed the adapter and save in Touchstone format.
result = calset.deembed_npdata(result, fixture=adapter)
result.save('embed-corrected.s1p')
Advanced 16 Term with Measurement Error Modeling
In this example we calibrate a two-port VNA using 16-term T parameters, taking into account both measurement noise and connection non-repeatability. We introduce four main new elements: the solver.set_m_error() method, the solver.et_tolerance and solver.p_tolerance attributes, and the CorrelatedParameter class.
The solver.set_m_error() method takes a frequency vector, a noise floor vector, and a tracking noise error. The noise floor vector describes the complex standard deviation in VNA measurements at each frequency due to noise in the VNA’s detectors when no signal from the DUT is applied. The optional tracking noise vector describes the complex standard deviation in VNA measurements at each frequency, proportional to the amplitude of the received signal due to noise in the VNA’s transmitter. It’s assumed that the two noise sources are Gaussian and independent.
The solver.et_tolerance and solver.p_tolerance attributes set the change in RMS value of the error terms and unknown parameters, respectively, sufficiently low to stop iteration. We set them about 10x smaller than smallest resolution we can realistically expect to attain given the limits of our calibration accuracy. Not shown is the solver.iteration_limit attribute that limits the number of iterations allowed before reporting convergence failure.
In the example, all parameters of the calibration standards are unknown. The transmission term of the through standard is an UnknownParameter as in the unknown-through example; all others are CorrelatedParameters, known only to be statistically related to other parameters, in this case, to known perfect short, open, and load standards. Calibration must not only solve for the 16 error terms (really 15, because one is a free variable), but also for the 12 unknown non-repeatable connection parameters of the calibration standards.
We start the calibration by connecting port 1 to a load standard and port 2 to a short standard. From there, we change one connection at a time, providing two measurements of most parameters. Finally, we connect the unknown through, not assuming perfect impedance matches.
from libvna.cal import Calset, CalType, Solver
from libvna.data import NPData, PType
import math
import numpy as np
# Set the calibration frequency points.
fmin = 1.0e+06
fmax = 1.0e+08
f_vector = np.linspace(fmin, fmax, num=10)
# Create the calibration container and error term solver.
calset = Calset()
solver = Solver(calset, CalType.T16, rows=2, columns=2,
frequency_vector=f_vector)
# Set the measurement error. First column is the noise floor of the
# receiver with no input signal; second column is noise in the generator,
# proportional to the received amplitude.
m_error = np.asarray([
(0.0001000, 0.0010000),
(0.0001111, 0.0011111),
(0.0001222, 0.0012222),
(0.0001333, 0.0013333),
(0.0001444, 0.0014444),
(0.0001556, 0.0015556),
(0.0001667, 0.0016667),
(0.0001778, 0.0017778),
(0.0001889, 0.0018889),
(0.0002000, 0.0020000)
])
solver.set_m_error(f_vector, m_error[:, 0], m_error[:, 1])
solver.et_tolerance = 1.0e-5 # set 10x smaller than error
solver.p_tolerance = 1.0e-5
# Get the (measured) connection non-repeatability error for each frequency
# and standard. For simplicity, we assume the error is the same for all
# our calibration standards; in practice, it's probably not. A better
# model would use different sigma values for each standard.
σ_vector = np.asarray([
0.0100000,
0.0111111,
0.0122222,
0.0133333,
0.0144444,
0.0155556,
0.0166667,
0.0177778,
0.0188889,
0.0200000,
])
# Create correlated parameters for each connection of the reflect
# standards. Here, we assume that the standards are perfect on average.
# The second parameter can be a vector parameter instead of a constant
# if we have a more trusted model of the standards.
L1 = calset.correlated_parameter( 0, f_vector, σ_vector)
S1 = calset.correlated_parameter(-1, f_vector, σ_vector)
O1 = calset.correlated_parameter(+1, f_vector, σ_vector)
L2 = calset.correlated_parameter( 0, f_vector, σ_vector)
S2 = calset.correlated_parameter(-1, f_vector, σ_vector)
O2 = calset.correlated_parameter(+1, f_vector, σ_vector)
L3 = calset.correlated_parameter( 0, f_vector, σ_vector)
S3 = calset.correlated_parameter(-1, f_vector, σ_vector)
O3 = calset.correlated_parameter(+1, f_vector, σ_vector)
# Create two impedance match errors and an unknown parameter for the
# through standard. If the phase shift in the through remains safely
# below 90 degrees, we can give 1 as the estimate. For a larger phase
# shift, we would have to provide a vector parameter instead of 1 as the
# initial guess and give a more accurate estimate for each frequency.
L4 = calset.correlated_parameter(0, f_vector, σ_vector)
L5 = calset.correlated_parameter(0, f_vector, σ_vector)
T = calset.unknown_parameter(1)
# Start with load on port 1 and short on port 2 (L-S).
m = NPData(ptype=PType.S, filename='T16-EM-MS1.s2p')
solver.add_double_reflect(m.data_array, s11=L1, s22=S1)
# Change port 1 to open (O-S).
m = NPData(ptype=PType.S, filename='T16-EM-OS2.s2p')
solver.add_double_reflect(m.data_array, s11=O1, s22=S1)
# Change port 2 to load (O-L).
m = NPData(ptype=PType.S, filename='T16-EM-OM3.s2p')
solver.add_double_reflect(m.data_array, s11=O1, s22=L2)
# Change port 1 to short (S-L).
m = NPData(ptype=PType.S, filename='T16-EM-SM4.s2p')
solver.add_double_reflect(m.data_array, s11=S2, s22=L2)
# Change port 2 to open (S-O).
m = NPData(ptype=PType.S, filename='T16-EM-SO5.s2p')
solver.add_double_reflect(m.data_array, s11=S2, s22=O2)
# Change port 1 to load (L-O).
m = NPData(ptype=PType.S, filename='T16-EM-MO6.s2p')
solver.add_double_reflect(m.data_array, s11=L3, s22=O2)
# Change port 2 to short (L-S).
m = NPData(ptype=PType.S, filename='T16-EM-MS7.s2p')
solver.add_double_reflect(m.data_array, s11=L3, s22=S3)
# Change port 1 to open (O-S).
m = NPData(ptype=PType.S, filename='T16-EM-OS8.s2p')
solver.add_double_reflect(m.data_array, s11=O3, s22=S3)
# Add measurement of the through standard.
m = NPData(ptype=PType.S, filename='T16-EM-T.s2p')
solver.add_line(m.data_array, [[L4, T], [T, L5]])
# Solve, add to Calset and save.
solver.solve()
solver.add_to_calset('mycal')
calset.save('T16-EM.vnacal')
Apply the calibration to a device under test.
from libvna.cal import Calset
# Load the calibration from file.
calset = Calset('T16-EM.vnacal')
calibration = calset.calibrations[0]
f_vector = calibration.frequency_vector
# Measured response
m = [
[[+0.34055-0.24663j, +3.26892+2.01818j],
[+0.97603-0.13328j, +0.40330+0.01610j]],
[[-1.12464-0.48291j, +1.34954-0.43587j],
[+0.57243-0.53912j, -0.87861+0.09846j]],
[[-1.31857-0.14438j, +1.19974-0.45799j],
[+0.20528-1.05226j, -0.48029+0.40278j]],
[[-1.60523+0.22822j, +1.38953-0.79020j],
[-0.51816-1.37328j, -0.07362+0.53082j]],
[[-0.32741+1.42067j, -0.40980-1.51797j],
[-1.22206-0.19197j, -0.24247-0.71180j]],
[[-0.14945+0.18954j, -0.01665-0.11219j],
[-0.84902-0.47930j, -0.83250+0.26880j]],
[[-0.28479-0.03291j, +0.28546+0.00031j],
[-1.16681-0.39048j, -0.60184+0.66266j]],
[[-0.32593-0.16384j, +0.41239+0.01457j],
[-1.39671-0.08966j, -0.33247+0.86948j]],
[[-0.34825-0.28684j, +0.47844+0.00919j],
[-1.47685+0.29482j, -0.03762+0.97695j]],
[[-0.37689-0.41281j, +0.51339-0.00248j],
[-1.42969+0.69255j, +0.27402+0.99239j]]
]
# Apply calibration and save in Touchstone format.
result = calibration.apply(f_vector, m)
result.save('T16-EM-corrected.s2p')