From 452dddc19cad85b6bdaae6e88225e6d20b8e80bb Mon Sep 17 00:00:00 2001 From: Brendan Haines Date: Mon, 7 Jul 2025 23:22:36 -0600 Subject: [PATCH] functional 1 port calibration --- charon_vna/vna.py | 46 +++++++++++++++++++++++++++++++++++++++++++ charon_vna/vna_dev.py | 37 ++++++++++++++++++++++++++++++---- 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/charon_vna/vna.py b/charon_vna/vna.py index 3ea3552..fb2bc99 100644 --- a/charon_vna/vna.py +++ b/charon_vna/vna.py @@ -1,5 +1,6 @@ # %% imports import copy +import pickle from enum import IntEnum, unique from pathlib import Path from typing import Any, Callable, Dict, List, Literal, Tuple @@ -360,6 +361,51 @@ class Charon: return s + def calibrate_sol(self, prompt: Callable[[str], None] | None = None, **kwargs) -> rf.calibration.Calibration: + if len(self.ports) != 1: + raise ValueError( + f"SOL calibration needs only one port but {len(self.ports)} ports are enabled. " + "Did you mean to use SOLT?" + ) + + if prompt is None: + prompt = lambda s: input(f"{s}\nENTER to continue...") + + ideal = rf.media.DefinedGammaZ0(frequency=rf.media.Frequency.from_f(self.frequency, unit="Hz")) + ideals = [ideal.short(), ideal.open(), ideal.load(0)] + + names = ["short", "open", "load"] + + measured = list() + for name in names: + prompt(f"Connect standard: {name} to port {self.ports[0]}") + measured.append(self.capture(**kwargs)) + + cal = rf.OnePort(ideals=ideals, measured=[s2net(m) for m in measured]) + + self.calibration = cal + + return cal + + def save_calibration(self, path: Path | str): + path = Path(path) + if path.suffix.lower() == ".pkl": + with open(str(path), "wb") as f: + pickle.dump(self.calibration, f) + else: + raise NotImplementedError(f"Unknown calibration file extension: {path.suffix}") + + def load_calibration(self, path: Path | str): + path = Path(path) + if path.suffix.lower() == ".pkl": + with open(str(path), "rb") as f: + cal = pickle.load(f) + if not isinstance(cal, rf.calibration.Calibration): + raise ValueError(f"Expected {rf.calibration.Calibration}, got {type(cal)}") + self.calibration = cal + else: + raise NotImplementedError(f"Unknown calibration file extension: {path.suffix}") + # %% if __name__ == "__main__": diff --git a/charon_vna/vna_dev.py b/charon_vna/vna_dev.py index 3acff13..9f51118 100644 --- a/charon_vna/vna_dev.py +++ b/charon_vna/vna_dev.py @@ -2,23 +2,52 @@ import numpy as np from matplotlib import pyplot as plt +from charon_vna.util import db20, net2s, s2net from charon_vna.vna import Charon # %% -frequency = np.linspace(80e6, 280e6, 31) +frequency = np.linspace(80e6, 280e6, 301) # %% -vna = Charon(frequency=frequency, ports=2) +vna = Charon(frequency=frequency, ports=1) # %% -s = vna.capture(measurements=[(2, 1), (2, 2)]) +s = vna.capture() # %% for m in s.m.data: for n in s.n.data: - plt.plot(s.frequency, 20 * np.log10(np.abs(s.sel(m=m, n=n))), label="$S_{" + str(m) + str(n) + "}$") + plt.plot(s.frequency, db20(s.sel(m=m, n=n)), label="$S_{" + str(m) + str(n) + "}$") plt.grid(True) plt.legend() +plt.show() +# %% +vna.calibrate_sol() + +# %% +vna.load_calibration("./calibration.pkl") + +# %% +s2 = net2s(vna.calibration.apply_cal(s2net(s))) + +for m in s.m.data: + for n in s.n.data: + plt.plot(s.frequency, db20(s.sel(m=m, n=n)), label="$S_{" + str(m) + str(n) + "}$ (uncalibrated)") + plt.plot(s2.frequency, db20(s2.sel(m=m, n=n)), label="$S_{" + str(m) + str(n) + "}$ (calibrated)") +plt.grid(True) +plt.legend() +plt.ylabel("Magnitude [dB]") +plt.show() + +for m in s.m.data: + for n in s.n.data: + plt.plot(s.frequency, np.angle(s.sel(m=m, n=n), deg=True), label="$S_{" + str(m) + str(n) + "}$ (uncalibrated)") + plt.plot(s2.frequency, np.angle(s2.sel(m=m, n=n), deg=True), label="$S_{" + str(m) + str(n) + "}$ (calibrated)") +plt.grid(True) +plt.legend() +plt.ylabel("Phase [deg]") +plt.show() + # %%