-
-
Notifications
You must be signed in to change notification settings - Fork 264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adopt 8-Term (Error Box) Calibration for 1.5-port VNAs #656
Comments
This is my first attempt on solving this problem. It seems to be an impractical (or at least not elegant) solution to copy all 8-term, 2-port calibration classes in the entire codebase and redefine them repeatedly just for a 1.5-port VNA. Instead, I think we can have more flexibility if an arbitrary 2-port, 2-path calibration class can be automatically adopted for a 2-port, 1-path VNA dynamically, on the fly. I think this is possible using Python's metaprogramming capabilities like metaclasses, but for simplicity, I only used a function here to generate classes here. import pathlib
import skrf
from skrf.network import four_oneports_2_twoport
from skrf.calibration import TwoPortOnePath, UnknownThru
from skrf.media import DefinedGammaZ0
def ThreeReceiverCal(calibration):
"""
Generate a new calibration class dynamically in a form suitable for use
with a 1.5-port VNA, from an existing 2-port calibration class.
This is implemented in the following way. First, before the calibration
measurements are passed to the underlying class, 2-port, 2-path measurements
are forged by copying all forward S-parameters (S11, S21) to the backward
direction (S22, S12).
Next, the `apply_cal` function is modified to take two sets of measurements,
one in the forward direction, and another one in the backward direction
with the DUT physically flipped. These two sets of measurements is again
combined, forging full 2-port, 2-path measurements before it's passed into
the underlying calibration class.
"""
def single_one_half_ports_to_two_ports(network):
s11 = network.s11
s21 = network.s21
s22 = s11
s12 = s21
return four_oneports_2_twoport(s11, s12, s21, s22)
def two_one_half_ports_to_two_ports(dut_forward, dut_backward):
s11 = dut_forward.s11
s21 = dut_forward.s21
s22 = dut_backward.s11
s12 = dut_backward.s21
return four_oneports_2_twoport(s11, s12, s21, s22)
class ThreeReceiverCal(calibration):
def __init__(self, measured, ideals, *args, **kwargs):
for idx, network in enumerate(measured):
# FIXME: This modifies the user input, real code should copy the networks first.
measured[idx] = single_one_half_ports_to_two_ports(network)
super().__init__(measured, ideals, *args, **kwargs)
def apply_cal(self, ntwk_tuple):
if not isinstance(ntwk_tuple, tuple):
raise NotImplementedError
if not len(ntwk_tuple) == 2:
raise ValueError
dut = two_one_half_ports_to_two_ports(ntwk_tuple[0], ntwk_tuple[1])
return super().apply_cal(dut)
return ThreeReceiverCal
def main(solt_dir, unknown_thru_dir, measurement_dir, output_dir):
# Perform Two Port One Path 12-term calibration to extract the switch term
solt_short_std = skrf.Network(solt_dir / "short.s2p")
solt_open_std = skrf.Network(solt_dir / "open.s2p")
solt_load_std = skrf.Network(solt_dir / "load.s2p")
solt_thru_std = skrf.Network(solt_dir / "thru.s2p")
frequency = solt_open_std.frequency
line = DefinedGammaZ0(frequency=frequency)
ideals = [
line.short(nports=2), line.open(nports=2),
line.match(nports=2), line.thru()
]
measured = [
solt_short_std, solt_open_std,
solt_load_std, solt_thru_std
]
cal = TwoPortOnePath(measured=measured, ideals=ideals, n_thrus=1)
switch_terms = [
cal.coefs_8term_ntwks["forward switch term"],
cal.coefs_8term_ntwks["reverse switch term"]
]
assert switch_terms[0] == switch_terms[1]
# Perform Unknown Thru 8-term calibration, with the switch term added
short_std = skrf.Network(unknown_thru_dir / "short.s2p")
open_std = skrf.Network(unknown_thru_dir / "open.s2p")
load_std = skrf.Network(unknown_thru_dir / "load.s2p")
thru_std = skrf.Network(unknown_thru_dir / "thru.s2p")
frequency = open_std.frequency
line = DefinedGammaZ0(frequency=frequency)
ideals = [
line.short(nports=2), line.open(nports=2),
line.match(nports=2), line.line(d=1, unit='mm')
]
measured = [
short_std, open_std,
load_std, thru_std
]
cal_class = ThreeReceiverCal(UnknownThru)
cal = cal_class(measured=measured, ideals=ideals, n_thrus=1, switch_terms=switch_terms)
dut_forward = skrf.Network(measurement_dir / "coupler_forward.s2p")
dut_backward = skrf.Network(measurement_dir / "coupler_backward.s2p")
dut = cal.apply_cal((dut_forward, dut_backward))
dut.write_touchstone(output_dir / "coupler.s2p")
solt_dir = pathlib.Path("./cal/solt/")
unknown_thru_dir = pathlib.Path("./cal/unknown-thru/")
measurement_dir = pathlib.Path("./raw/")
output_dir = pathlib.Path("./data/")
main(solt_dir, unknown_thru_dir, measurement_dir, output_dir) The code is untested, currently I'm not sure if the calibration is really functional. I still need to do the testing. Nevertheless, I think it's self-explanatory and clearly demostrates my ideas... One way to test whether it's correct is to compare the result of |
i am pretty sure that not having 4 receivers makes it not possible to perfectly cal. (i think keysight had a TRL* algorithm with tried this and failed, but i cant remember exactly) . so you will have an approximate call . you would need to setup a test suit to ensure its working probably by making some simplifications. if you look at how the calibration test code is , you can see how to extend it to the 3-receiver case. |
I don't know much about network theory or calibration, so correct me if I get anything wrong. But... My impression is that the TRL* algorithm's biggest deficiency is neglecting the switch terms (or load reflection errors). On the other hand, if you can determine the switch terms in a SOLT calibration before doing the second-tier TRL calibration (and applying the previously-obtained switch terms on top of the TRL result), this error can be removed, and the calibration should be equivalent (?) to an ideal one. In the NIST paper Two-Tier Multiline TRL for Calibration of Low-Cost Network Analyzers, the authors said,
Conclusion:
Also, this statement is repeated in the NIST paper Formulations of the Basic Vector Network Analyzer Error Model including Switch-Terms:
This is basically what I'm trying do to here, as seen in the code snippet I posted in the previous comment. I did a conventional 12-term SOLT calibration first, followed by a 8-term Unknown Thru calibration and applied the previous switch terms on top of it. In my understanding is correct, this approach makes all 8-term error models usable on a Three Receiver VNA, using the switch terms obtained in a previous 12-trem calibration. |
Many useful calibration algorithms are based on the 8-term Error Box model, such as the Unknown Thru calibration or the TRL calibration. If these algorithms can be applied to a 1.5-port VNA as well, they will have great practical values. But currently in scikit-rf, the only calibration available for 1.5-port VNAs is the Two Port One Path, which is a variant of the 12-term SOLT calibration.
Is it possible to adopt these 8-term Error Box models to 1.5-port VNAs? If my understanding is correct:
If this is the case, is it possible to find the switch term via a full Two Port One Path SOLT calibration first, then use a 8-term Error Model and apply the same trick of copying the forward error coefficients to the backward direction, and finally apply the switch term?
The text was updated successfully, but these errors were encountered: