Source code for skrf.vi.vna.nanovna.nanovna

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from collections.abc import Sequence

import functools
from enum import Enum

import numpy as np
import pyvisa

import skrf
from skrf.vi import vna


class OP(bytes, Enum):
    NOP = b"\x00"
    INDICATE = b"\x0d"
    READ = b"\x10"
    READ2 = b"\x11"
    READ4 = b"\x12"
    READFIFO = b"\x18"
    WRITE = b"\x20"
    WRITE2 = b"\x21"
    WRITE4 = b"\x22"
    WRITE8 = b"\x23"
    WRITEFIFO = b"\x28"


class REG_ADDR(bytes, Enum):
    SWEEP_START = b"\x00"
    SWEEP_STEP = b"\x10"
    SWEEP_POINTS = b"\x20"
    VALS_PER_FREQ = b"\x22"
    RAW_SAMPLES_MODE = b"\x26"
    VALS_FIFO = b"\x30"
    DEVICE_VARIANT = b"\xf0"
    PROTOCOL_VERSION = b"\xf1"
    HARDWARE_REV = b"\xf2"
    FIRMWARE_MAJOR = b"\xf3"
    FIRMWARE_MINOR = b"\xf4"


[docs] class NanoVNAv2(vna.VNA): """NanoVNAv2. This class connects to the NanoVNA V2 using a binary protocol over USB. It should also be compatible with other devices, if they use the same `protocol <https://nanorfe.com/nanovna-v2-user-manual.html#__RefHeading___Toc2537_2953165397>`_. Some variants of the NanoVNA use a text protocol and are not supported. .. warning:: The NanoVNA V2 only returns uncalibrated networks, regardless of the calibration on the device itself. See section `Examples`_. Notes ----- Tested devices: * `NanoVNA V2 <https://nanorfe.com/de/nanovna-v2.html>`_ * `LiteVNA <https://www.zeenko.tech/litevna>`_ .. _Examples: Examples -------- Basic S11 and S21 measurement: .. code-block:: python import skrf from skrf.vi.vna.nanovna import NanoVNAv2 vna = NanoVNAv2("ASRL/dev/ttyACM0::INSTR") # Linux # vna = NanoVNAv2("ASRL1::INSTR") # Windows freq = skrf.Frequency(start=1, stop=2, unit='GHz', npoints=101) s11, s21 = vna.get_s11_s21() 1-port calibration assuming ideal reference standards: .. code-block:: python import skrf # load uncalibrated measurement raw_s1p = skrf.Network("measurement.s1p") # load s1p networks of measured standards cal_measured = [skrf.Network("short.s1p"), skrf.Network("open.s1p"), skrf.Network("load.s1p"] # get ideal standards line = skrf.DefinedGammaZ0(frequency=cal_measured[0].frequency, z0=50) cal_ideals = [line.short(nports=1), line.open(nports=1), line.match(nports=1)] # run calibration cal = skrf.OnePort(ideals=cal_ideals, measured=cal_measured) cal.run() # apply calibration calibrated_s1p = cal.apply_cal(raw_s1p) """ _scpi = False
[docs] def __init__(self, address, backend: str = "@py"): super().__init__(address, backend) if not isinstance(self._resource, pyvisa.resources.SerialInstrument): raise RuntimeError( "NanoVNAv2 can only be a serial instrument. " f"{address} yields a {self._resource.__class__.__name__}" ) self.read_bytes = self._resource.read_bytes self.write_raw = self._resource.write_raw self._reset_protocol() self.frequency = skrf.Frequency(start=1e6, stop=10e6, npoints=201)
def _reset_protocol(self): self.write_raw(b"\x00\x00\x00\x00\x00\x00\x00\x00")
[docs] def query(self, op: OP, addr: REG_ADDR | bytes, nbytes: int) -> bytes: cmd = op + addr self.write_raw(cmd) return self.read_bytes(nbytes)
[docs] def write(self, op: OP, addr: REG_ADDR | bytes, nbytes: int, arg: int) -> None: arg = int(arg).to_bytes(nbytes, byteorder="little", signed=False) if op == OP.WRITEFIFO: cmd = op + addr + nbytes.to_bytes(1) + arg else: cmd = op + addr + arg self.write_raw(cmd)
@property def id(self) -> str: var = self.query(OP.READ, REG_ADDR.DEVICE_VARIANT, 1) var = int.from_bytes(var, 'little') return str(var) @property def device_info(self) -> str: variant = self.id protocol = self.query(OP.READ, REG_ADDR.PROTOCOL_VERSION, 1) protocol = int.from_bytes(protocol, 'little') hardware = self.query(OP.READ, REG_ADDR.HARDWARE_REV, 1) hardware = int.from_bytes(hardware, 'little') fw_major = self.query(OP.READ, REG_ADDR.FIRMWARE_MAJOR, 1) fw_major = int.from_bytes(fw_major, 'little') fw_minor = self.query(OP.READ, REG_ADDR.FIRMWARE_MAJOR, 1) fw_minor = int.from_bytes(fw_minor, 'little') return ( f"NanoVNAv2\n" f"\tVariant:{variant}\n" f"\tProtocol Version:{protocol}\n" f"\tHardware Version: {hardware}\n" f"\tFirmware Version: {fw_major}.{fw_minor}" ) @property def freq_start(self) -> float: return self._freq.start @freq_start.setter def freq_start(self, f: int) -> None: self.write(OP.WRITE8, REG_ADDR.SWEEP_START, 8, f) self._freq = skrf.Frequency(start=f, stop=self._freq.stop, npoints=self._freq.npoints) @property def freq_stop(self) -> float: return self._freq.stop @freq_stop.setter def freq_stop(self, f: int) -> None: self._freq = skrf.Frequency(start=self._freq.start, stop=f, npoints=self._freq.npoints) self.write(OP.WRITE8, REG_ADDR.SWEEP_STEP, 8, self._freq.step) @property def freq_step(self) -> float: return self._freq.step @freq_step.setter def freq_step(self, f: int) -> None: npoints = (self._freq.stop - self._freq.start + f) / f npoints = int(npoints.round()) self._freq = skrf.Frequency(start=self._freq.start, stop=self._freq.stop, npoints=npoints) self.write(OP.WRITE2, REG_ADDR.SWEEP_POINTS, 2, npoints) @property def npoints(self) -> int: return self._freq.npoints @npoints.setter def npoints(self, n: int) -> None: self.write(OP.WRITE2, REG_ADDR.SWEEP_POINTS, 2, n) self._freq = skrf.Frequency(start=self._freq.start, stop=self._freq.stop, npoints=n) @property def frequency(self) -> skrf.Frequency: return self._freq @frequency.setter def frequency(self, f: skrf.Frequency): self.write(OP.WRITE8, REG_ADDR.SWEEP_START, 8, f.start) self.write(OP.WRITE8, REG_ADDR.SWEEP_STEP, 8, f.step) self.write(OP.WRITE2, REG_ADDR.SWEEP_POINTS, 2, f.npoints) self._freq = f
[docs] def clear_fifo(self) -> None: self.write(OP.WRITE, REG_ADDR.VALS_FIFO, 1, 0)
def _convert_bytes_to_sparams(n: int, raw: bytearray) -> tuple[np.ndarray, np.ndarray]: s11 = np.zeros(n, dtype=complex) s21 = np.zeros_like(s11) from_bytes = functools.partial(int.from_bytes, byteorder='little', signed=True) for i in range(n): start = i * 32 stop = (i+1) * 32 chunk = raw[start:stop] fwd0re = from_bytes(chunk[0:4]) fwd0im = from_bytes(chunk[4:8]) rev0re = from_bytes(chunk[8:12]) rev0im = from_bytes(chunk[12:16]) rev1re = from_bytes(chunk[16:20]) rev1im = from_bytes(chunk[20:24]) freqIndex = from_bytes(chunk[24:26]) a1 = complex(fwd0re, fwd0im) b1 = complex(rev0re, rev0im) b2 = complex(rev1re, rev1im) s11[freqIndex] = b1 / a1 s21[freqIndex] = b2 / a1 return s11, s21
[docs] def get_s11_s21(self) -> tuple[skrf.Network, skrf.Network]: n = self._freq.npoints self.clear_fifo() raw = bytearray() n_remaining = n while n_remaining > 0: len_segment = 255 if n_remaining > 255 else n_remaining n_remaining = n_remaining - len_segment self.write(OP.READFIFO, REG_ADDR.VALS_FIFO, 1, len_segment) raw.extend(self.read_bytes(32 * len_segment)) s11, s21 = skrf.Network(), skrf.Network() s11.frequency = self._freq.copy() s21.frequency = self._freq.copy() s11.s, s21.s = NanoVNAv2._convert_bytes_to_sparams(n, raw) return s11, s21
[docs] def get_sdata(self, a: int = 1, b: int = 1) -> skrf.Network: """ Get S-parameter as 1-port :class:`skrf.Network`. Parameters ---------- a: Input port of VNA. Can be 1 or 2, default is 1. b: Output port of VNA. Can only be 1, default is 1. Returns ------- :class:`skrf.Network` Measured S-parameter """ if a not in (1, 2) or b != 1: raise RuntimeError( "NanoVNA V2 can only measure S11 and S21" ) return self.get_s11_s21()[a-1]
[docs] def get_snp_network(self, ports: Sequence[int | None]) -> skrf.Network: """ Get custom n-port :class:`skrf.Network`. Parameters ---------- ports: Port mapping. Can be any sequence of 1, 2 and None Returns ------- :class:`skrf.Network` Measured S-parameters in custom network """ nwk_s11, nwk_s21 = self.get_s11_s21() frequency = self._freq.copy() s_parameters = np.zeros((len(frequency), len(ports), len(ports)), dtype=complex) snp_network = skrf.Network(frequency=frequency, s=s_parameters) for i, i_port in enumerate(ports): if i_port is not None: for j, j_port in enumerate(ports): if i_port == 1 and j_port == 1: snp_network.s[:, i, j] = nwk_s11.s[:, 0, 0] elif i_port == 2 and j_port == 1: snp_network.s[:, i, j] = nwk_s21.s[:, 0, 0] return snp_network