Author Topic: Open source VNA calibration, conversion, touchstone read-write from python or C!  (Read 1171 times)

0 Members and 1 Guest are viewing this topic.

Offline scott_guthridgeTopic starter

  • Newbie
  • Posts: 7
  • Country: us
Hi everyone.  For folks working on open hardware vector network analyzers, I'd like to put in a plug for my open source VNA software library, libvna, available in python or C.  It supports Linux, Windows and MacOS.  The library contains extensive VNA calibration including SOLT, TRL, TXYZ (a.k.a "unknown through"), as well as all the others: LRL, TRM, LRM, LXYZ, LRRM, etc. If it doesn't have a specific solver for the calibration, it uses a general solver -- all that's necessary is that there are a sufficient number of calibration conditions.  It supports 8, 12 and 16 error term models.  It can also model measurement and connection non-repeatability errors.  The library also has full conversion routines between S, Y, Z, H, G, A (ABCD), B (inverse ABCD), T (scattering transfer) and U (inverse scattering transfer) parameters.  It loads and saves Touchstone v1 and v2 file formats as well as a more general space-separated value .npd (network parameter data) format.

From python3:
pip install libvna

Python documentation is here: https://libvna.readthedocs.io/latest/   There are extensive examples in the docs.
Python/Cython source is here: https://github.com/scott-guthridge/pylibvna
C source is here: https://github.com/scott-guthridge/libvna

There is definitely some overlap with scikit-rf, but we've taken fairly different approaches and I believe that this library is complementary to that one.  Please try it out and let me know what you think.
 

Offline shabaz

  • Frequent Contributor
  • **
  • Posts: 286
Hi Scott,

This is great, thanks for sharing!

I gave it a shot, but wanted to run it by you, in case I’m doing something wrong. I’m not a calibration expert, so I apologize if I’m making basic mistakes here. On the other hand, if I can use it, then almost anyone probably can! : )

I have an OSL kit called SDR-Kits Female Calibration Kit of Rosenberger parts (PDF datasheet) and it comes with offset delay (or electrical length) values. Ordinarily, I would plug those values into the VNA, but I recently got a Nano-VNA (the H4 variant), and I’m completely new to these Nano instruments. I don’t think I can configure those values into the instrument.

The NanoVNA appears to be applying some default calibration, I left that calibration enabled (with hindsight I would have switched the default calibration off, I can do that if you think it’s needed). I left the NanoVNA at all the default settings for now, and it is set to a span of 50 kHz – 900 MHz.

Next, I connected a short length of cable (~ 90 mm) to the NanoVNA, and then attached the open/short/load, and obtained the .s1p files for the Open/Short/Load standards in that kit.
I named the files open_meas.s1p, short_meas.s1p and load_meas.s1p (attached in the zip file).

Next, I ran the program cal_create.py, and it generated a file called 1x1.vnacal

Then, I attached a DIY 100-ohm load (two 200-ohm resistors in parallel across the back of a SMA female connector) and saved the measurement as dut_meas.s1p

I ran the program cal_apply.py, and it converted to a file called dut_meas_corrected.s1p

It certainly looks spot-on (Smith chart output attached). Have I done it right? It feels odd not having typed the calibration standard offset delay (or electrical length) values anywhere, so I was wondering if I've done something wrong.

Many thanks.
 

Offline scott_guthridgeTopic starter

  • Newbie
  • Posts: 7
  • Country: us
One problem I see is that the frequencies were linearly spaced in your measurements, but given as log spaced to the libvna.cal.Solver object.  It's better to grab the actual frequency vector from one of the input files than to use linspace or logspace to compute it.  It's interesting that you used by scipy.rf and libvna together -- nothing wrong with that -- but you can write it with fewer lines if you use libvna.data.NPData to read the files.  I'm attaching a modified version of cal_create.py that fixes the frequency vector mismatch and loads using libvna.data.NPData instead of scikit.rf.

As for the offset delay values of your standards, those suggest that the standards may have differing lengths relative to the reference plane.  It's possible to compensate for those offsets by altering the -1, 1, and 0 s11 values given to the add_single_reflect calls using:

    s11_measured = exp(-4 pi j x) s11_actual

Where x is the electrical length (in wavelengths) in one direction.  You'd pass the s11_measured value to the add_single_reflect methods, where s11_actual is -1, 1 or 0.  For the match (assuming it's perfect), the offset shouldn't matter.  A negative value of x would indicate that the standard lies in front of the reference plane.  You might want to understand how the maker of the standards defines the location of the reference plane because you want your DUT to lie exactly in the same place.

I should add a method to add length offsets to the ports of any arbitrary standard.

Otherwise, your code looks good.
 
The following users thanked this post: shabaz

Offline shabaz

  • Frequent Contributor
  • **
  • Posts: 286
Thanks for taking the time to check it out, spotting the log/lin mistake, and updating the file.
I have tested it, and it runs.

Regarding the offsets, the open and short are of different lengths, I asked an expert once, and they
mentioned that usually, the values are almost similar, but the offset length of the Short is usually very slightly (not a lot) longer than the offset length of the Open. That's for a high-end cal kit, whereas the SDR-Kits cal kit is low-cost, and the offset length of the Short is shorter than the Open offset length. In any case, both are normal positive values.

In the table screenshot earlier, I had written the values to be plugged into different VNAs in different columns. Some VNAs want the values as negative, apparently, even though the distance from the reference plane to the Open or Short is still in the usual direction, i.e., further away from the VNA.


The lengths of the open and short are (respectively) 42.35ps and 26.91ps, i.e. in millimeters they are
(by multiplying by 0.3 approx) 12.69 and 8.07 mm.

I have converted those lengths to units of wavelengths in the code, into two lists, called open_length[] and short_length[], both of the length of f_vector. My code is ugly there, I didn't know if there was an easier way to do it.

In any event, I now have the two lists and have built up s11_open_measured[] and s11_short_measured[] complex lists using the formula you mentioned (I may have got it wrong!).

However, it's not in a format that the add_single_reflect function understands, I know I somehow have to get it into a tuple with frequency_vector, but my Python knowledge is pretty basic.

I hate to take up anyone's time but if you get a chance to look at it some time, no hurry, (file attached), it would be gratefully appreciated.

It could help a lot of people since that calibration kit is very popular (since it's low-cost), and there are probably very many newcomers to VNAs (not just NanoVNA) and VNA calibration) who would benefit from your library. It's difficult enough to use a complex instrument, so being able to execute through easy-to-use Python software could be very attractive.

Once it's all working well, I plan to write beginners' documentation with screenshots, photos, etc., trial it with some guinea pigs here (and record a video while I do that), and I hope it will be useful for anyone to use for S11 purposes, even if they are Python beginners.
 

Offline scott_guthridgeTopic starter

  • Newbie
  • Posts: 7
  • Country: us
> s11_open_measured = complex(0, math.exp(-4 * math.pi * open_length))

You can't pull the j out of the exponential like this.  Instead import "cmath" and use: s11_open_measured = cmath.exp(-4j * math.pi * open_length).  Given a complex argument, the exponential function gives cos and sin values: exp(j x) = cos(x) + j sin(x)


To pass the vector to add_single_reflect, there are two ways, both equivalent:

    from libvna.cal import VectorParameter

    s11 = VectorParameter(calset, f_vector, s11_open_measured)
    solver.add_single_reflect(ntwk.data_array, s11=s11)

or more simply, you can pass a tuple:

    solver.add_single_reflect(ntwk.data_array, s11=(f_vector, s11_open_measured))


On whether or not to keep the Nano VNA's calibration in place, it doesn't matter as long as you're consistent.  Whatever configuration you calibrate in is the configuration you have to measure in.
« Last Edit: July 23, 2024, 05:55:29 pm by scott_guthridge »
 

Offline scott_guthridgeTopic starter

  • Newbie
  • Posts: 7
  • Country: us
One more thing to check: are the delay values of your standards the delay between the reference plane and the standard in one direction?  Or is it the delay from the reference plane to the standard and back.  If the later, change the -4j pi in the formula to -2j pi.
« Last Edit: July 23, 2024, 03:06:11 pm by scott_guthridge »
 

Offline shabaz

  • Frequent Contributor
  • **
  • Posts: 286
Hi Scott,

Thank you I believe it's working now!
Even though I didn't know what to do, that was still pretty dumb of me pulling it out of the exponent - it was late when I was coding! : (

I've attached the code I'm using. Now the result looks (I believe) realistic, given the 100 ohm load is very crude (two 100 ohm resistors that have not been soldered flat, and are large 0805; I used this load at a few tens of MHz in the past, so didn't make a lot of effort at the time). Now I'll try to make a better load with 0603 upside-down and with a high-quality SMA instead (this was discussed on another thread where someone was trying to make a 50 ohm load).

I'll report back with the measurements and new results once I've done that.

EDIT: The offset values I'm using are one-way, the code will print it out to prevent any confusion in future:
Code: [Select]
python ./cal_create_v1a.py
One-way Open delay : 42.35 picosec / 12.697 mm
One-way Short delay: 26.91 picosec / 8.068 mm
Done, generated 1x1.vnacal
« Last Edit: July 23, 2024, 05:42:54 pm by shabaz »
 

Offline scott_guthridgeTopic starter

  • Newbie
  • Posts: 7
  • Country: us
Is blue the raw measurement from the Nano VNA and orange the corrected measurement?  DUT may have a bit of capacitance to ground.
 

Offline shabaz

  • Frequent Contributor
  • **
  • Posts: 286
Hi,
As you say, blue is raw, orange is corrected. I'm searching for a decent end-launch style RF connector and will construct the load better, to see if it improves.
 

Offline shabaz

  • Frequent Contributor
  • **
  • Posts: 286
Just realized, (at least a significant part of) the reason for the curve is that I still need to do electrical length compensation.

The calibration is to the reference plane (i.e. approx the mating surface of the SMA connector), but my 100 ohm load is at a position further away from that reference plane.

I'll make a better 100 ohm load anyway, since the current one is so crude, and then perform the cal_apply.py operation, and then do the electrical length compensation (I have some Python code to do that provided I have an open at the same distance).

Once all that's done, I'll put it all into one Python file, so that it's easy to do all this without the risk of missing steps like this!
« Last Edit: July 23, 2024, 06:59:16 pm by shabaz »
 

Offline shabaz

  • Frequent Contributor
  • **
  • Posts: 286
Good news. Change of plan, I was impatient and curiosity got the better of me so I ran with the current poor-quality 100-ohm load and did the electrical length compensation anyway. The photo shows the open that I used for the compensation (it's got some epoxy resin on it which is not great either).
Anyway, the result (orange in the attached chart) is great! It seems, even with the poor quality SMA and 0805 resistors, it's pretty good to the 900 MHz limit of the NanoVNA-H4 I'm using.

To summarize, the steps that were done are:
(a) Use the VNA to capture S11 for the calibration kit's Open, Short and Load
(b) Run that latest cal_create program to generate the .vnacal file
(c) Capture S11 for the DUT
(d) Capture S11 for an Open at the same position as the DUT
(e) Run the cal_apply for both (c) and (d), resulting in two files with suffix _corrected.py
(f) Run the electrical length compensation on the two files in (e), generating an output file

The code I'm using for step (f) is here, it uses scikit-rf, but if it can be converted to using libvna I can test that too (or that part could be kept as-is, since I believe it currently works).

I'll put everything together and write some notes, etc, so anyone can do it with little risk of error.
« Last Edit: July 23, 2024, 07:49:58 pm by shabaz »
 

Offline scott_guthridgeTopic starter

  • Newbie
  • Posts: 7
  • Country: us
If the DUT isn't on the reference plane, you have two options: (1) change the calibration delay parameters to move the reference plane to where you want it, or (2) postprocess the corrected result in the other direction (remove the minus sign from the formula) to move the DUT to the reference plane.

Thanks for trying out the library.  It gives me good feedback.  I will try to make it possible to specify delay parameters to the add* methods.  Something tricky I have to figure out is what to do if someone specifies a delay parameter for an UnknownParameter or CorrelatedParameter.  It's especially problematic for the later.  I'll probably just disallow delays on unknown parameters.
 

Offline shabaz

  • Frequent Contributor
  • **
  • Posts: 286
Hi Scott,

As a use-case, if there was some way for the user to supply not just open/short/load, but open/short/load/testjig_open (or testjig_short) S11 data, then if your library could auto-calculate the delay, and somehow stick it in the .vnacal file, then from the user perspective, all they need to know is that a single vnacal file contains everything for their setup.

It could also be interesting to extend the vnacal file format, so that it stores two calibrations; one with the delay, and one without, so users can choose to switch off the delay at the cal_apply stage. Not sure why they may want to do that, but maybe it's useful when slightly modifying a test jig and not re-measuring the open/delay/short standards.

Anyway, this is just a nice-to-have. Even if the auto-calculation just automatically calculated the delay value (maybe call it testjig_elec_compensation_delay (or something more succinct!) to make it clear what it is doing, that it's not a calibration-standard delay/offset) and made it available to be added to the add* methods as you say, would be great.
 

Offline shabaz

  • Frequent Contributor
  • **
  • Posts: 286
Just to throw out another suggestion (I don't know how useful it is, and unfortunately, I don't know what math is involved for it), could be to allow users to plug in the DC resistance for their 50-ohm load in case that makes a difference (in practice it might not make much difference). Some of the low-cost cal kits have a discrepancy of more than an ohm. The low-cost cal kit that I purchased had a load with value 48.5 ohm. I think such a feature if worthwhile, would still be low-priority though, since there's always the workaround of replacing with a better 50-ohm load if desired, e.g. a MiniCircuits one costs about $20.
 

Offline scott_guthridgeTopic starter

  • Newbie
  • Posts: 7
  • Country: us
You already can save multiple calibrations in a single .vnacal file.  See this example: https://libvna.readthedocs.io/latest/cal-examples.html#two-port-reflect-only  So it's possible to create one with a delay and one without.

If your match standard is 48.5 instead of 50 ohms, you can do this:

    from libvna.conv import ztos

    s = ztos([[48.5]], z0=50]
    s11 = s[0, 0]

then use s11 instead of 0 in the add_single_reflect for the match.
« Last Edit: July 24, 2024, 07:40:56 pm by scott_guthridge »
 
The following users thanked this post: shabaz


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf