Skip to content
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

Open
biergaizi opened this issue May 8, 2022 · 3 comments
Open

Adopt 8-Term (Error Box) Calibration for 1.5-port VNAs #656

biergaizi opened this issue May 8, 2022 · 3 comments
Labels

Comments

@biergaizi
Copy link
Contributor

biergaizi commented May 8, 2022

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:

  1. The 8-term error model is almost mathematical equivalent to the 12-term error model, with the exception of an additional switch term.
  2. The switch term represents the quality of the port termination inside the VNA, it's stable over time, and is independent from the external cabling.
  3. When doing a 12-term calibration, scikit-rf automatically calculates the equivalent 8-term coefficients as well, including the switch term.

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?

@biergaizi
Copy link
Contributor Author

biergaizi commented May 12, 2022

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 ThreeReceiverCal(SOLT) and TwoPortOnePath. If my assumption in the code is correct, both should produce identical calibration coefficients - since ThreeReceiverCal(SOLT) is a general function to transform an arbitrary 2-port calibration class to a form suitable for 1.5-port calibration, while TwoPortOnePath is nothing but a special case of SOLT.

@arsenovic
Copy link
Member

arsenovic commented May 12, 2022

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.

@biergaizi
Copy link
Contributor Author

biergaizi commented May 12, 2022

@arsenovic: 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.

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,

The three-sampler architecture precludes straightforward TRL because it provides no direct mechanism for assessing the load reflection errors due to nonideal terminations on the port opposite the one to which the incident signal is applied.

Since error occurs only if the switchable source and load have nonidentical reflection coefficients, the effect is generally known as "switching error." Users of three-sampler VNA find themselves forced to use lumped-element methods such as OSLT (open-short-load-thr). Recently, an approximate version of TRL intended for use on a three-sampler VNA has been introduced commercially [Note: this is a reference to Keysight's TRL*]. However, it simply neglects the load reflection errors and therefore may be unsuitably inaccurate [7,8].

Another way to overcome the limitations of a three-sampler system is to apply a two-tier calibration. In this case, the first tier may be an OSLT calibration in which only the load reflection terms will affect the measurements, since the second tier calibration will effectively overwrite all of the other calibration coefficients. Since the switch terms are determined in the first tier, the second tier can use TRL or a derivative. Two-tier calibrations in which the second tier is a TRL-based method have been used for many years [9]. More recently, this approach has been applied specifically to three-sampler VNAs [8,10]. The former documents that the accuracy of this method may be superior to TRL without load reflection correction.

Conclusion:

Multiline TRL can be easily implemented on a three-sampler VNA using external software. The accuracy can be nearly as good as a four-sampler VNA. Although an additional first-tier calibration using coaxial standards is required, this first tier need not be repeated frequently as long as the calibration factors are retained in the VNA. Alternatively, the load reflection coefficients could be extracted from the first-tier calibration and stored externally for later use.

Also, this statement is repeated in the NIST paper Formulations of the Basic Vector Network Analyzer Error Model including Switch-Terms:

Equations (30) and (31) [Note: the two equations for calculating the switch term from the 12-term error coefficients] have been used [9] to demonstratethat a TRL calibration could be implemented on a three-samplerVNA once Γ_F, and Γ_R were determined from a separate calibration. These switch terms were shown to be stable with respect to time, as we would expect of an electronic switch.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants