kopia lustrzana https://github.com/NanoVNA-Saver/nanovna-saver
Merge pull request #100 from mihtjel/feature/touchstone_rewrite
Feature/touchstone rewritepull/101/head
commit
d84168b1da
|
@ -1,4 +1,5 @@
|
|||
# NanoVNASaver - a python program to view and export Touchstone data from a NanoVNA
|
||||
# NanoVNASaver
|
||||
# A python program to view and export Touchstone data from a NanoVNA
|
||||
# Copyright (C) 2019. Rune B. Broberg
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
|
@ -13,132 +14,167 @@
|
|||
#
|
||||
# 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 collections
|
||||
import logging
|
||||
import math
|
||||
import re
|
||||
from typing import List
|
||||
from .RFTools import Datapoint
|
||||
import cmath
|
||||
import io
|
||||
from NanoVNASaver.RFTools import Datapoint
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Touchstone:
|
||||
s11data: List[Datapoint] = []
|
||||
s21data: List[Datapoint] = []
|
||||
comments = []
|
||||
filename = ""
|
||||
class Options:
|
||||
# Fun fact: In Touchstone 1.1 spec all params are optional unordered.
|
||||
# Just the line has to start with "#"
|
||||
UNIT_TO_FACTOR = {
|
||||
"ghz": 10**9,
|
||||
"mhz": 10**6,
|
||||
"khz": 10**3,
|
||||
"hz": 10**0,
|
||||
}
|
||||
VALID_UNITS = UNIT_TO_FACTOR.keys()
|
||||
VALID_PARAMETERS = "syzgh"
|
||||
VALID_FORMATS = ("ma", "db", "ri")
|
||||
|
||||
def __init__(self, filename):
|
||||
def __init__(self,
|
||||
unit: str = "GHZ",
|
||||
parameter: str = "S",
|
||||
t_format: str = "ma",
|
||||
resistance: int = 50):
|
||||
# set defaults
|
||||
assert unit.lower() in Options.VALID_UNITS
|
||||
assert parameter.lower() in Options.VALID_PARAMETERS
|
||||
assert t_format.lower() in Options.VALID_FORMATS
|
||||
assert resistance > 0
|
||||
self.unit = unit.lower()
|
||||
self.parameter = parameter.lower()
|
||||
self.format = t_format.lower()
|
||||
self.resistance = resistance
|
||||
|
||||
@property
|
||||
def factor(self) -> int:
|
||||
return Options.UNIT_TO_FACTOR[self.unit]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f"# {self.unit} {self.parameter}"
|
||||
f" {self.format} r {self.resistance}"
|
||||
).upper()
|
||||
|
||||
def parse(self, line: str):
|
||||
if not line.startswith("#"):
|
||||
raise TypeError("Not an option line: " + line)
|
||||
pfact = pparam = pformat = presist = False
|
||||
params = iter(line[1:].lower().split())
|
||||
for p in params:
|
||||
if p in Options.VALID_UNITS and not pfact:
|
||||
self.factor = Options.UNIT_TO_FACTOR[p]
|
||||
pfact = True
|
||||
elif p in Options.VALID_PARAMETERS and not pparam:
|
||||
self.parameter = p
|
||||
pparam = True
|
||||
elif p in Options.VALID_FORMATS and not pformat:
|
||||
self.format = p
|
||||
pformat = True
|
||||
elif p == "r" and not presist:
|
||||
self.resistance = int(next(params))
|
||||
else:
|
||||
raise TypeError("Illegal option line: " + line)
|
||||
|
||||
|
||||
class Touchstone:
|
||||
|
||||
def __init__(self, filename: str):
|
||||
self.filename = filename
|
||||
self.sdata = [[], [], [], []] # at max 4 data pairs
|
||||
self.comments = []
|
||||
self.opts = Options()
|
||||
|
||||
@property
|
||||
def s11data(self) -> list:
|
||||
return self.sdata[0]
|
||||
|
||||
@property
|
||||
def s21data(self) -> list:
|
||||
return self.sdata[1]
|
||||
|
||||
@property
|
||||
def s12data(self) -> list:
|
||||
return self.sdata[2]
|
||||
|
||||
@property
|
||||
def s22data(self) -> list:
|
||||
return self.sdata[3]
|
||||
|
||||
def _parse_comments(self, fp) -> str:
|
||||
for line in fp:
|
||||
line = line.strip()
|
||||
if line.startswith("!"):
|
||||
logger.info(line)
|
||||
self.comments.append(line)
|
||||
continue
|
||||
return line
|
||||
|
||||
def _append_line_data(self, freq: float, data: list):
|
||||
data_list = iter(self.sdata)
|
||||
vals = iter(data)
|
||||
for v in vals:
|
||||
if self.opts.format == "ri":
|
||||
next(data_list).append(
|
||||
Datapoint(freq, float(v), float(next(vals))))
|
||||
if self.opts.format == "ma":
|
||||
z = cmath.polar(float(v),
|
||||
math.radians(float(next(vals))))
|
||||
next(data_list).append(Datapoint(freq, z.real, z.imag))
|
||||
if self.opts.format == "db":
|
||||
z = cmath.polar(math.exp(float(v) / 20),
|
||||
math.radians(float(next(vals))))
|
||||
next(data_list).append(Datapoint(freq, z.real, z.imag))
|
||||
|
||||
def load(self):
|
||||
self.s11data = []
|
||||
self.s21data = []
|
||||
|
||||
realimaginary = False
|
||||
magnitudeangle = False
|
||||
|
||||
factor = 1
|
||||
logger.info("Attempting to open file %s", self.filename)
|
||||
try:
|
||||
logger.info("Attempting to open file %s", self.filename)
|
||||
file = open(self.filename, "r")
|
||||
|
||||
lines = file.readlines()
|
||||
parsed_header = False
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line.startswith("!"):
|
||||
logger.info(line)
|
||||
self.comments.append(line)
|
||||
continue
|
||||
if line.startswith("#") and not parsed_header:
|
||||
pattern = "^# (.?HZ) (S )?RI( R 50)?$"
|
||||
match = re.match(pattern, line.upper())
|
||||
if match:
|
||||
logger.debug("Found header for RealImaginary and %s", match.group(1))
|
||||
match = match.group(1)
|
||||
parsed_header = True
|
||||
realimaginary = True
|
||||
if match == "HZ":
|
||||
factor = 1
|
||||
elif match == "KHZ":
|
||||
factor = 10**3
|
||||
elif match == "MHZ":
|
||||
factor = 10**6
|
||||
elif match == "GHZ":
|
||||
factor = 10**9
|
||||
else:
|
||||
factor = 10**9 # Default Touchstone frequency unit is GHz
|
||||
continue
|
||||
|
||||
pattern = "^# (.?HZ) (S )?MA( R 50)?$"
|
||||
match = re.match(pattern, line.upper())
|
||||
if match:
|
||||
logger.debug("Found header for MagnitudeAngle and %s", match.group(1))
|
||||
match = match.group(1)
|
||||
parsed_header = True
|
||||
magnitudeangle = True
|
||||
if match == "HZ":
|
||||
factor = 1
|
||||
elif match == "KHZ":
|
||||
factor = 10**3
|
||||
elif match == "MHZ":
|
||||
factor = 10**6
|
||||
elif match == "GHZ":
|
||||
factor = 10**9
|
||||
else:
|
||||
factor = 10**9 # Default Touchstone frequency unit is GHz
|
||||
continue
|
||||
|
||||
# else:
|
||||
# This is some other comment line
|
||||
logger.debug("Comment line: %s", line)
|
||||
continue
|
||||
if not parsed_header:
|
||||
logger.warning("Read line without having read header: %s", line)
|
||||
continue
|
||||
|
||||
try:
|
||||
if realimaginary:
|
||||
values = line.split(maxsplit=5)
|
||||
freq = values[0]
|
||||
re11 = values[1]
|
||||
im11 = values[2]
|
||||
freq = int(float(freq) * factor)
|
||||
re11 = float(re11)
|
||||
im11 = float(im11)
|
||||
self.s11data.append(Datapoint(freq, re11, im11))
|
||||
if len(values) > 3:
|
||||
re21 = values[3]
|
||||
im21 = values[4]
|
||||
re21 = float(re21)
|
||||
im21 = float(im21)
|
||||
self.s21data.append(Datapoint(freq, re21, im21))
|
||||
elif magnitudeangle:
|
||||
values = line.split(maxsplit=5)
|
||||
freq = values[0]
|
||||
mag11 = float(values[1])
|
||||
angle11 = float(values[2])
|
||||
freq = int(float(freq) * factor)
|
||||
re11 = float(mag11) * math.cos(math.radians(angle11))
|
||||
im11 = float(mag11) * math.sin(math.radians(angle11))
|
||||
self.s11data.append(Datapoint(freq, re11, im11))
|
||||
if len(values) > 3:
|
||||
mag21 = float(values[3])
|
||||
angle21 = float(values[4])
|
||||
re21 = float(mag21) * math.cos(math.radians(angle21))
|
||||
im21 = float(mag21) * math.sin(math.radians(angle21))
|
||||
self.s21data.append(Datapoint(freq, re21, im21))
|
||||
|
||||
continue
|
||||
except ValueError as e:
|
||||
logger.exception("Failed to parse line: %s (%s)", line, e)
|
||||
|
||||
file.close()
|
||||
with open(self.filename) as infile:
|
||||
self.loads(infile.read())
|
||||
except TypeError as e:
|
||||
logger.exception("Failed to parse %s: %s", self.filename, e)
|
||||
except IOError as e:
|
||||
logger.exception("Failed to open %s: %s", self.filename, e)
|
||||
return
|
||||
|
||||
def setFilename(self, filename):
|
||||
def loads(self, s: str):
|
||||
"""Parse touchstone 1.1 string input
|
||||
appends to existing sdata if Touchstone object exists
|
||||
"""
|
||||
with io.StringIO(s) as file:
|
||||
opts_line = self._parse_comments(file)
|
||||
self.opts.parse(opts_line)
|
||||
|
||||
prev_freq = 0.0
|
||||
prev_len = 0
|
||||
for line in file:
|
||||
# ignore empty lines (even if not specified)
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
# ignore comments at data end
|
||||
data = line.split('!')[0]
|
||||
data = data.split()
|
||||
freq, data = float(data[0]) * self.opts.factor, data[1:]
|
||||
data_len = len(data)
|
||||
|
||||
# consistency checks
|
||||
if freq <= prev_freq:
|
||||
raise TypeError("Frequency not ascending: " + line)
|
||||
prev_freq = freq
|
||||
|
||||
if prev_len == 0:
|
||||
prev_len = data_len
|
||||
if data_len % 2:
|
||||
raise TypeError("Data values aren't pairs: " + line)
|
||||
elif data_len != prev_len:
|
||||
raise TypeError("Inconsistent number of pairs: " + line)
|
||||
|
||||
self._append_line_data(freq, data)
|
||||
|
||||
def setFilename(self, filename: str):
|
||||
self.filename = filename
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# Hz S RI R 50
|
||||
140000000 -0.720544874 -0.074467673
|
||||
140307234 -0.707615315 -0.045678697
|
||||
140614468 -0.694235622 -0.017205553
|
||||
140921702 -0.679476678 0.011064857
|
||||
141228936 0.037949264
|
||||
141536169 -0.645231842 0.06495472
|
||||
141843404 -0.625548779 0.090901531
|
||||
142150638 -0.605278372 0.116493001
|
|
@ -0,0 +1,16 @@
|
|||
! Vector Network Analyzer VNA R2
|
||||
! Tucson Amateur Packet Radio
|
||||
! Saturday, 9 November, 2019 17:48:47
|
||||
! Frequency S11 S21 S12 S22
|
||||
! ListType=Lin
|
||||
# HZ S RI R 50
|
||||
000500000 -3.33238E-001 1.80018E-004 6.74780E-001 -8.19510E-007 6.75290E-001 -8.20129E-007 -3.33238E-001 3.08078E-004
|
||||
001382728 -3.33017E-001 6.89580E-004 6.74251E-001 -3.70855E-004 6.74761E-001 -5.04361E-004 -3.33016E-001 9.45694E-004
|
||||
002265456 -3.33136E-001 1.06095E-003 6.74766E-001 -1.00228E-003 6.75276E-001 -1.00304E-003 -3.33136E-001 1.06095E-003
|
||||
003148184 -3.33120E-001 1.97467E-003 6.74773E-001 -1.65230E-003 6.74773E-001 -1.65230E-003 -3.33121E-001 1.91064E-003
|
||||
004030912 -3.32847E-001 2.45743E-003 6.74777E-001 -2.28839E-003 6.75288E-001 -2.15679E-003
|
||||
004913640 -3.32746E-001 2.93382E-003 6.75260E-001 -2.94645E-003 6.75261E-001 -2.81312E-003 -3.32990E-001 3.06364E-003
|
||||
005796368 -3.33479E-001 3.06528E-003 6.75798E-001 -2.32365E-003 6.76309E-001 -2.32540E-003 -3.33479E-001 3.06528E-003
|
||||
006679097 -3.32609E-001 3.80377E-003 6.74764E-001 -4.08250E-003 6.74764E-001 -4.08250E-003 -3.32854E-001 3.80608E-003
|
||||
007561825 -3.32448E-001 4.35906E-003 6.75247E-001 -4.96650E-003 6.75249E-001 -4.69986E-003 -3.32692E-001 4.36169E-003
|
||||
008444553 -3.32510E-001 4.94361E-003 6.74737E-001 -5.33508E-003 6.75248E-001 -5.20579E-003 -3.32508E-001 5.13540E-003
|
|
@ -0,0 +1,8 @@
|
|||
# Hz S RI R 50
|
||||
140000000 -0.720544874 -0.074467673
|
||||
140307234 -0.707615315 -0.045678697
|
||||
140921702 -0.679476678 0.011064857
|
||||
140614468 -0.694235622 -0.017205553
|
||||
141536169 -0.645231842 0.06495472
|
||||
141843404 -0.625548779 0.090901531
|
||||
142150638 -0.605278372 0.116493001
|
Plik diff jest za duży
Load Diff
Plik diff jest za duży
Load Diff
|
@ -0,0 +1,12 @@
|
|||
# Hz S RI R 50
|
||||
140000000 -0.720544874 -0.074467673
|
||||
140307234 -0.707615315 -0.045678697
|
||||
140614468 -0.694235622 -0.017205553
|
||||
140921702 -0.679476678 0.011064857
|
||||
141228936 -0.662805676 0.037949264
|
||||
141536169 -0.645231842 0.06495472 ! just a test comment
|
||||
141843404 -0.625548779 0.090901531
|
||||
142150638 -0.605278372 0.116493001
|
||||
142457872 -0.583680212 0.140287563
|
||||
142765106 -0.560637235 0.16401714
|
||||
143072339 -0.536502182 0.186390563
|
Ładowanie…
Reference in New Issue