2019-11-15 11:43:18 +00:00
|
|
|
# NanoVNASaver
|
|
|
|
# A python program to view and export Touchstone data from a NanoVNA
|
2019-10-18 22:58:31 +00:00
|
|
|
# Copyright (C) 2019. Rune B. Broberg
|
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
import math
|
2019-11-13 17:55:06 +00:00
|
|
|
import cmath
|
|
|
|
from typing import List, NamedTuple
|
2020-06-21 18:54:23 +00:00
|
|
|
from NanoVNASaver.SITools import Format, clamp_value
|
2019-10-18 22:58:31 +00:00
|
|
|
|
2019-12-06 18:50:06 +00:00
|
|
|
FMT_FREQ = Format()
|
|
|
|
FMT_SHORT = Format(max_nr_digits=4)
|
|
|
|
FMT_SWEEP = Format(max_nr_digits=9, allow_strip=True)
|
|
|
|
|
2019-11-15 11:43:18 +00:00
|
|
|
|
2019-12-07 22:22:54 +00:00
|
|
|
def parallel_to_serial(z: complex) -> complex:
|
|
|
|
"""Convert parallel impedance to serial impedance equivalent"""
|
|
|
|
z_sq_sum = z.real ** 2 + z.imag ** 2
|
2020-01-06 20:46:56 +00:00
|
|
|
# TODO: Fix divide by zero
|
2019-12-07 22:22:54 +00:00
|
|
|
return complex(z.real * z.imag ** 2 / z_sq_sum,
|
|
|
|
z.real ** 2 * z.imag / z_sq_sum)
|
|
|
|
|
|
|
|
|
|
|
|
def serial_to_parallel(z: complex) -> complex:
|
|
|
|
"""Convert serial impedance to parallel impedance equivalent"""
|
|
|
|
z_sq_sum = z.real ** 2 + z.imag ** 2
|
2020-01-22 16:54:34 +00:00
|
|
|
if z.real == 0 and z.imag == 0:
|
2020-01-06 20:46:56 +00:00
|
|
|
return complex(math.inf, math.inf)
|
2020-01-22 16:54:34 +00:00
|
|
|
if z_sq_sum == 0:
|
|
|
|
return complex(0, 0)
|
|
|
|
if z.imag == 0:
|
|
|
|
return complex(z_sq_sum / z.real, math.copysign(math.inf, z_sq_sum))
|
|
|
|
if z.real == 0:
|
2020-02-10 09:30:19 +00:00
|
|
|
return complex(math.copysign(math.inf, z_sq_sum), z_sq_sum / z.real)
|
2020-01-22 16:54:34 +00:00
|
|
|
return complex(z_sq_sum / z.real, z_sq_sum / z.imag)
|
2019-12-07 22:22:54 +00:00
|
|
|
|
|
|
|
|
2019-12-08 15:32:13 +00:00
|
|
|
def impedance_to_capacitance(z: complex, freq: float) -> float:
|
|
|
|
"""Calculate capacitive equivalent for reactance"""
|
2019-12-07 22:22:54 +00:00
|
|
|
if freq == 0:
|
|
|
|
return -math.inf
|
|
|
|
if z.imag == 0:
|
|
|
|
return math.inf
|
|
|
|
return -(1 / (freq * 2 * math.pi * z.imag))
|
|
|
|
|
|
|
|
|
|
|
|
def impedance_to_inductance(z: complex, freq: float) -> float:
|
|
|
|
"""Calculate inductive equivalent for reactance"""
|
|
|
|
if freq == 0:
|
|
|
|
return 0
|
|
|
|
return z.imag * 1 / (freq * 2 * math.pi)
|
|
|
|
|
|
|
|
|
2019-11-29 12:07:47 +00:00
|
|
|
def impedance_to_norm(z: complex, ref_impedance: float = 50) -> complex:
|
|
|
|
"""Calculate normalized z from impedance"""
|
|
|
|
return z / ref_impedance
|
|
|
|
|
|
|
|
|
2019-12-01 16:59:15 +00:00
|
|
|
def norm_to_impedance(z: complex, ref_impedance: float = 50) -> complex:
|
|
|
|
"""Calculate impedance from normalized z"""
|
|
|
|
return z * ref_impedance
|
|
|
|
|
|
|
|
|
2019-11-23 10:45:34 +00:00
|
|
|
def reflection_coefficient(z: complex, ref_impedance: float = 50) -> complex:
|
|
|
|
"""Calculate reflection coefficient for z"""
|
|
|
|
return (z - ref_impedance) / (z + ref_impedance)
|
|
|
|
|
2019-11-25 10:00:30 +00:00
|
|
|
|
2019-11-23 10:45:34 +00:00
|
|
|
def gamma_to_impedance(gamma: complex, ref_impedance: float = 50) -> complex:
|
2019-11-29 12:07:47 +00:00
|
|
|
"""Calculate impedance from gamma"""
|
2020-02-15 19:12:49 +00:00
|
|
|
try:
|
|
|
|
return ((-gamma - 1) / (gamma - 1)) * ref_impedance
|
|
|
|
except ZeroDivisionError:
|
|
|
|
return math.inf
|
2019-10-18 22:58:31 +00:00
|
|
|
|
2019-11-25 10:00:30 +00:00
|
|
|
|
2019-11-13 17:55:06 +00:00
|
|
|
class Datapoint(NamedTuple):
|
|
|
|
freq: int
|
|
|
|
re: float
|
|
|
|
im: float
|
|
|
|
|
|
|
|
@property
|
2019-12-02 08:39:30 +00:00
|
|
|
def z(self) -> complex:
|
2019-11-20 15:53:56 +00:00
|
|
|
""" return the datapoint impedance as complex number """
|
2019-11-13 17:55:06 +00:00
|
|
|
return complex(self.re, self.im)
|
|
|
|
|
2019-11-15 11:55:54 +00:00
|
|
|
@property
|
2019-12-02 08:39:30 +00:00
|
|
|
def phase(self) -> float:
|
2019-11-20 15:53:56 +00:00
|
|
|
""" return the datapoint's phase value """
|
2019-11-15 11:55:54 +00:00
|
|
|
return cmath.phase(self.z)
|
|
|
|
|
2019-11-17 11:55:45 +00:00
|
|
|
@property
|
|
|
|
def gain(self) -> float:
|
2019-11-15 11:43:18 +00:00
|
|
|
mag = abs(self.z)
|
|
|
|
if mag > 0:
|
|
|
|
return 20 * math.log10(mag)
|
2020-02-10 09:30:19 +00:00
|
|
|
return -math.inf
|
|
|
|
|
2019-11-17 11:53:59 +00:00
|
|
|
@property
|
|
|
|
def vswr(self) -> float:
|
2019-11-15 11:43:18 +00:00
|
|
|
mag = abs(self.z)
|
|
|
|
if mag == 1:
|
|
|
|
return 1
|
|
|
|
return (1 + mag) / (1 - mag)
|
|
|
|
|
2019-11-17 13:13:37 +00:00
|
|
|
def impedance(self, ref_impedance: float = 50) -> complex:
|
2019-11-23 10:45:34 +00:00
|
|
|
return gamma_to_impedance(self.z, ref_impedance)
|
2019-11-15 11:43:18 +00:00
|
|
|
|
2019-11-20 15:53:56 +00:00
|
|
|
def qFactor(self, ref_impedance: float = 50) -> float:
|
2019-11-17 13:13:37 +00:00
|
|
|
imp = self.impedance(ref_impedance)
|
2019-11-15 11:43:18 +00:00
|
|
|
if imp.real == 0.0:
|
|
|
|
return -1
|
|
|
|
return abs(imp.imag / imp.real)
|
|
|
|
|
2019-11-20 15:53:56 +00:00
|
|
|
def capacitiveEquivalent(self, ref_impedance: float = 50) -> float:
|
2019-12-17 19:13:28 +00:00
|
|
|
return impedance_to_capacitance(self.impedance(ref_impedance), self.freq)
|
2019-11-15 11:43:18 +00:00
|
|
|
|
2019-11-20 15:53:56 +00:00
|
|
|
def inductiveEquivalent(self, ref_impedance: float = 50) -> float:
|
2019-12-17 19:13:28 +00:00
|
|
|
return impedance_to_inductance(self.impedance(ref_impedance), self.freq)
|
2019-11-15 11:43:18 +00:00
|
|
|
|
2019-11-25 10:00:30 +00:00
|
|
|
|
2019-11-23 10:45:34 +00:00
|
|
|
def groupDelay(data: List[Datapoint], index: int) -> float:
|
|
|
|
idx0 = clamp_value(index - 1, 0, len(data) - 1)
|
|
|
|
idx1 = clamp_value(index + 1, 0, len(data) - 1)
|
2019-11-24 16:35:35 +00:00
|
|
|
delta_angle = data[idx1].phase - data[idx0].phase
|
|
|
|
delta_freq = data[idx1].freq - data[idx0].freq
|
|
|
|
if delta_freq == 0:
|
|
|
|
return 0
|
2019-11-23 10:45:34 +00:00
|
|
|
if abs(delta_angle) > math.tau:
|
|
|
|
if delta_angle > 0:
|
|
|
|
delta_angle = delta_angle % math.tau
|
|
|
|
else:
|
|
|
|
delta_angle = -1 * (delta_angle % math.tau)
|
2019-11-24 16:35:35 +00:00
|
|
|
val = -delta_angle / math.tau / delta_freq
|
2019-11-23 10:45:34 +00:00
|
|
|
return val
|
|
|
|
|
2019-11-15 11:43:18 +00:00
|
|
|
|
2020-03-12 19:14:22 +00:00
|
|
|
def corrAttData(data: Datapoint, att: float):
|
|
|
|
"""Correct the ratio for a given attenuation on s21 input"""
|
|
|
|
|
|
|
|
if att <= 0:
|
|
|
|
return data
|
|
|
|
else:
|
|
|
|
att = 10**(att/20)
|
|
|
|
|
|
|
|
ndata = []
|
|
|
|
for i in range(len(data)):
|
|
|
|
freq, re, im = data[i]
|
|
|
|
orig = complex(re, im)
|
|
|
|
corrected = orig * att
|
|
|
|
ndata.append(Datapoint(freq, corrected.real, corrected.imag))
|
|
|
|
|
|
|
|
return ndata
|