Merge pull request #100 from mihtjel/feature/touchstone_rewrite

Feature/touchstone rewrite
pull/101/head
mihtjel 2019-11-19 12:35:55 +01:00 zatwierdzone przez GitHub
commit d84168b1da
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
7 zmienionych plików z 2234 dodań i 115 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

1011
test/data/valid.s1p 100644

Plik diff jest za duży Load Diff

1027
test/data/valid.s2p 100644

Plik diff jest za duży Load Diff

Wyświetl plik

@ -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