#!/usr/bin/env python3
"""
This file is part of the PAFFrontendSim.
The PAFFrontendSim is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License v3 as published by the Free Software Foundation.
The PAFFrontendSim 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 the PAFFrontendSim. If not, see https://www.gnu.org/licenses/.
"""
#############################################################################
# import libs
#############################################################################
import numpy as np
import matplotlib.pyplot as plt
from copy import deepcopy
import LogClass
from Simulation import ImpedanceMatrixDataClass
from Simulation.constants import _C
#############################################################################
# constants
#############################################################################
_CLASSNAME = "S-Parameter"
#############################################################################
# classes
#############################################################################
[docs]
class SnPData :
"""
Class to read LNA data input files.
The expected format is a Touchstone file for a 2 port network (.s2p).
The data must be blocks of S-Parameters (9 columns) and/or Noise parameters (5 columns).
The header of the file must specify the units as well as the impedance ("# ghz S db R 50" as an example).
"""
#####################################
# internal data and init method
#####################################
# Keywords for the file
__UNITS = {
"HZ" : 1. ,
"KHZ" : 1.e3 ,
"MHZ" : 1.e6 ,
"GHZ" : 1.e9
}
# init method
def __init__ ( self, log = None):
"""
Initialize class.
"""
if log == None:
self.log = LogClass.LogClass(_CLASSNAME)
else:
self.log = deepcopy(log) # logging class used for all in and output messages
self.log.addLabel(self)
self.Type = "S" # Type of the data (S, Y, Z, H, G)
self.noPorts = 2 # number of ports
self.RefR = 50. # reference resistance
self.Param = [] # array with arrays of s-parameter values (matrix wise)
self.freq = [] # array of belonging frequencies
self.Noise = [] # noise data arra with arrays of
# [min Tnoise, mag. S11_min_noise, angle S11_min_noise, noise Resistance/Rimpedance]
[docs]
def printOut ( self, line, message_type, noSpace = True, lineFeed = True ) :
"""
Wrapper for the printOut method of the actual log class
"""
self.log.printOut(line, message_type, noSpace, lineFeed)
[docs]
def readSnP ( self, filename ) :
"""
Method to read an TOUCHSTONE data file
Parameters:
filename : Name of the input file
Returns error flag (true on success, false on error)
"""
def _extractData(allCols, setField):
"""
extract data from string array to matrix using the appropriate conversion function setField
"""
if len(allCols) > 1:
self.printOut("Starting data-point %d" % len(self.Param), self.log.DEBUG_TYPE, False, False)
freq = float(allCols[0])
# create and extract S-parameter matrix
m = np.zeros((self.noPorts, self.noPorts), dtype = np.complex128)
for p_idx in range(1, self.noPorts**2*2 +1, 2) :
y = int(0.5*(p_idx-1) / self.noPorts)
x = int(0.5*(p_idx-1) - y*self.noPorts)
m[x,y] = setField(allCols[p_idx], allCols[p_idx+1])
self.Param.append(m)
self.freq.append(freq)
self.Param = [] # delete existing data
self.freq = []
self.Noise = []
freqN = []
self.noPorts = 0
line = 0.
self.printOut("Reading TOUCHSTONE File %s" % filename, self.log.FLOW_TYPE)
getData = False # flag that the data body is reached
getNoise = False # flag that data comming are noise data
allCols = []
lastLength = -1
# format conversion fuction
setField = lambda x,y : x*np.exp(1j*np.pi*y/180.)
with open(filename, 'r', encoding='latin-1') as fh : # open file for reading
for line in fh : # and loop all lines in the file
line = line.strip() # remove CR / NL / EOF from line end
columns = line.split() # split line
if len(line) > 0 :
if line[0] == '#' : # Read option line
self.printOut(" Using the following data to interprete the file contend:", self.log.DATA_TYPE, False)
getData = True # start data taking flag
# establish unit conversion to GHz
if len(columns) < 2 :
ffscale = 1/1.e9
self.printOut(" Frequency units : Hz (default)", self.log.DATA_TYPE, False)
else :
ffscale = self.__UNITS.get(columns[1].upper(), 1.)/1.e9
self.printOut(" Frequency units : %s" % columns[1], self.log.DATA_TYPE, False)
#selct data type (default: Magnitude)
if len(columns) < 3 :
self.Type = "S"
self.printOut(" Data-Type : S (default)", self.log.DATA_TYPE, False)
else :
self.Type = columns[2].upper()
self.printOut(" Data-Type : %s" % self.Type, self.log.DATA_TYPE, False)
# select format conversion fuction
if len(columns) > 3 :
if columns[3].upper() == "DB" :
setField = lambda x,y : (10**(x/20.))*np.exp(1j*np.pi*y/180.)
self.printOut(" Data-Format : DB power", self.log.DATA_TYPE, False)
elif columns[3].upper == "RI" :
setField = lambda x, y : x+ 1j*y
self.printOut(" Data-Format : Real / Imaginaty", self.log.DATA_TYPE, False)
else :
setField = lambda x,y : x*np.exp(1j*np.pi*y/180.)
self.printOut(" Data-Format : Magnitude Angle", self.log.DATA_TYPE, False)
else :
self.printOut(" Data-Format : Magnitude Angle (default)", self.log.DATA_TYPE, False)
# get reference impedance
self.RefR = 50.
if len(columns) > 5 :
self.RefR = float(columns[5])
self.printOut(" Reference Impedance : %.1f Ohm" % self.RefR, self.log.DATA_TYPE, False)
else :
self.printOut(" Reference Impedance : 50 Ohm (default)", self.log.DATA_TYPE, False)
continue
if line[0] == '!' : line = "" #continue # comment
if not getData : continue
# filter '!' sign
cols = []
for c in columns :
if c[0] == '!' : break
cols.append(float(c))
# getting noise data ?
if getNoise == True :
if len(freqN) == 0:
freqN.append(float(allCols[0]))
self.Noise.append([float(allCols[1]), float(allCols[2])*np.exp(1j*float(allCols[3])/180.*np.pi), float(allCols[4])])
freqN.append(float(cols[0]))
self.Noise.append([float(cols[1]), float(cols[2])*np.exp(1j*float(cols[3])/180.*np.pi), float(cols[4])])
continue
if (len(cols) <= lastLength-1) and ( len(cols) > 0) :
allCols += cols
continue
# detect first line
if lastLength < 0 :
lastLength = len(cols)
allCols = cols
continue
# determine number of ports
if (len(allCols) > 0) and ( self.noPorts == 0) : # set number of ports
self.noPorts = int(np.sqrt((len(allCols) - 1)/2.))
self.printOut("Found %d ports" % self.noPorts, self.log.DEBUG_TYPE, False)
#extract data-point
_extractData(allCols, setField)
allCols = cols
lastLength = len(cols)
# check if noise data are starting
if (len(self.freq) > 0) and (len(cols) > 0) and (self.freq[-1] > float(cols[0])) :
getNoise = True
if not getNoise : _extractData(allCols, setField)
self.freq = np.asarray(self.freq) * ffscale
fh.close()
self.printOut("", self.log.DEBUG_TYPE, False)
# interpolate frequency axis
if len(freqN) < 2 : return getData
freqN = np.asarray(freqN) * ffscale
Noise = np.zeros((len(self.freq), 3), dtype = complex)
self.Noise = np.asarray(self.Noise)
Noise[:,0] = np.interp(self.freq, freqN, self.Noise[:,0])
Noise[:,1] = np.interp(self.freq, freqN, self.Noise[:,1])
Noise[:,2] = np.interp(self.freq, freqN, self.Noise[:,2])
self.Noise = Noise
return getData
[docs]
def export2ImpedanceMatrix ( self, frequency = -1.) :
"""
exports the s-parameters as impedance matrix
"""
# get index of closest frequency in list
idx = np.argmin(np.abs(self.freq - frequency))
bw = 1000000.
if len(self.freq) > 1 :
bw = (self.freq[1]-self.freq[0])*1e9
M = ImpedanceMatrixDataClass.ImpedanceMatrix(wl = _C/self.freq[idx]*1e-6, z= self.RefR, bw = bw, log = self.log)
M.Data = self.Param[idx]**2.
M.noElements = self.noPorts
return M