From c5dc320989b0504ee1aa44e3709e7246ed36043f Mon Sep 17 00:00:00 2001 From: Brendan Haines Date: Tue, 8 Jul 2025 00:02:03 -0600 Subject: [PATCH] 2-port SOLT cal --- charon_vna/vna.py | 49 +++++++++++++++++++++++++++++++++++++++---- charon_vna/vna_dev.py | 24 ++++++++++++++++++--- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/charon_vna/vna.py b/charon_vna/vna.py index fb2bc99..1a1954e 100644 --- a/charon_vna/vna.py +++ b/charon_vna/vna.py @@ -361,7 +361,7 @@ class Charon: return s - def calibrate_sol(self, prompt: Callable[[str], None] | None = None, **kwargs) -> rf.calibration.Calibration: + def calibrate_sol(self, prompt: Callable[[str], None] | None = None, **kwargs) -> None: if len(self.ports) != 1: raise ValueError( f"SOL calibration needs only one port but {len(self.ports)} ports are enabled. " @@ -378,14 +378,55 @@ class Charon: measured = list() for name in names: - prompt(f"Connect standard: {name} to port {self.ports[0]}") + 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]) + cal = rf.OnePort(measured=[s2net(m) for m in measured], ideals=ideals) self.calibration = cal - return cal + def calibrate_solt(self, prompt: Callable[[str], None] | None = None, **kwargs) -> None: + if len(self.ports) < 2: + raise ValueError( + f"SOLT calibration needs at least two ports but {len(self.ports)} ports are enabled. " + "Did you mean to use SOL?" + ) + + if len(self.ports) > 2: + raise NotImplementedError("SOLT calibration with more than two ports not yet supported") + + 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)] + ideals = [rf.two_port_reflect(id, id) for id in ideals] + + thru = np.zeros((len(self.frequency), 2, 2), dtype=np.complex128) + thru[:, 0, 1] = 1 + thru[:, 1, 0] = 1 + thru = rf.Network(frequency=self.frequency, f_unit="Hz", s=thru) + + ideals.append(thru) + + names_1p = ["short", "open", "load"] + names_2p = ["thru"] + + measured = list() + for name in names_1p: + measured_param = list() + for port in self.ports: + prompt(f"Connect standard {name} to port {port}") + measured_param.append(self.capture(measurements=[(port, port)], **kwargs).sel(m=port, n=port)) + measured.append(rf.two_port_reflect(*[s2net(m) for m in measured_param])) + + for name in names_2p: + prompt(f"Connect standard {name} between ports {self.ports[0]} and {self.ports[1]}") + measured.append(s2net(self.capture(**kwargs))) + + cal = rf.SOLT(measured=measured, ideals=ideals) + + self.calibration = cal def save_calibration(self, path: Path | str): path = Path(path) diff --git a/charon_vna/vna_dev.py b/charon_vna/vna_dev.py index 9f51118..d8880f0 100644 --- a/charon_vna/vna_dev.py +++ b/charon_vna/vna_dev.py @@ -9,7 +9,7 @@ from charon_vna.vna import Charon frequency = np.linspace(80e6, 280e6, 301) # %% -vna = Charon(frequency=frequency, ports=1) +vna = Charon(frequency=frequency, ports=2) # %% s = vna.capture() @@ -26,11 +26,19 @@ plt.show() # %% vna.calibrate_sol() +# %% +vna.calibrate_solt() + +# %% +vna.save_calibration("./calibration.pkl") + # %% vna.load_calibration("./calibration.pkl") # %% s2 = net2s(vna.calibration.apply_cal(s2net(s))) +# s2.coords["m"] = s.m +# s2.coords["n"] = s.n for m in s.m.data: for n in s.n.data: @@ -39,12 +47,22 @@ for m in s.m.data: plt.grid(True) plt.legend() plt.ylabel("Magnitude [dB]") +# plt.ylim(-30, 5) 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)") + if m != n: + 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]")