#!/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
from matplotlib import pyplot as plt
from copy import copy, deepcopy
import LogClass
from Simulation.constants import _Impedance, _Tiso
from Simulation import SnPDataClass
#############################################################################
# constants
#############################################################################
_CLASSNAME = "SignalChain"
#############################################################################
# classes
#############################################################################
[docs]
class SignalChain :
"""
Class to handle signal chain data.
It is also able to read (readFile) and write (writeFile) signal chain data in its own format (PAF-Simulator signal chain noise data file).
Alternatively, LNA data can be read if formatted as a Touchstone file.
In this case, the module SnPDataClass is used.
If no input file is provided, the LNA data must be passed as lists to the internal method "makeDataset".
"""
#####################################
# Keywords for the file
__KEYS = {
"Comment" : '#' ,
"FileType" : 'SIGNAL-CHAIN:' ,
"Name" : 'NAME:' ,
"T0" : 'T0:' ,
"Zs" : 'Zs' ,
"Estimate" : 'ESTIMATE:' ,
"Data" : 'DATA:'
}
#####################################
# internal data and init method
# 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.Name = "--" # Name of the signal chain
self.T0 = _Tiso # signal chain reference temperature
self.Zs = _Impedance # nominal input impedance of the signal chain [Ohm]
self.Frequency = [] # array with frequencies (sorted) [GHz]
self.Tmin = [] # array with minimum noise temperatures for each frequency point [K]
self.Rn = [] # array with noise resistance per frequency point [Ohm]
self.Estimate = False # is Rn estimated by 0.5*Re(Zopt)*(Fmin -1); Fmin = Tmin/T0+1
self.Zopt = [] # array with the optimum input impedance [Ohm]
self.S11 = [] # array with S-11 parameters (just as reference)
self.S12 = [] # array with S-12 parameters (just as reference)
self.S21 = [] # array with S-21 parameters (just as reference)
self.S22 = [] # array with S-22 parameters (just as reference)
[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)
#####################################
# File-IO methods
# write file
[docs]
def writeFile ( self, filename):
"""
Method to write the signal chain data to a file.
Parameters:
filename (string): File to be written.
"""
self.printOut("Writing File %s" % filename, self.log.FLOW_TYPE)
fh = open(filename, 'w') # open file for writing
# write header block
fh.write(self.__KEYS['Comment'] + " PAF-Simulator signal chain noise data file\n" )
fh.write(self.__KEYS['Comment'] + " \n" )
fh.write(self.__KEYS['Comment'] + " File-Header\n" )
fh.write(self.__KEYS['FileType'] + " # Type Identifier (only lines after Type-ID are parsed during reading\n" )
fh.write(self.__KEYS['Comment'] + " \n" )
fh.write(self.__KEYS['Comment'] + " Signal chain identifier and header data\n")
fh.write(self.__KEYS['Name'] + " %s \n" % self.Name)
fh.write(self.__KEYS['Comment'] + " Reference temperature \n")
fh.write(self.__KEYS['T0'] + " %.2f K\n" % self.T0 )
fh.write(self.__KEYS['Comment'] + " Nominal input impedance \n")
fh.write(self.__KEYS['Zs'] + " %.2f Ohm\n" % self.Zs )
fh.write(self.__KEYS['Comment'] + " Flag if Rn is etsimated \n")
fh.write(self.__KEYS['Estimate'] + " %s \n" % self.Estimate )
fh.write(self.__KEYS['Comment'] + " \n" )
fh.write(self.__KEYS['Comment'] + " Data-block: complex numbers written in amplitude(amp) and phase (ph) [radian]\n")
fh.write(self.__KEYS['Comment'] + " Data-Block (Frequency[GHz] Tmin[K] Rn[Ohm] Zopt-amp Zopt-ph S11-amp S11-ph S12-amp S12-ph S21-amp S21-ph S22-amp S22-ph) :\n")
fh.write(self.__KEYS['Comment'] + " \n" )
# write data block
for i in range(0, len(self.Frequency) ) :
fh.write(self.__KEYS['Data'] + " %.2f %.2f %.2f %.2f %.2f %.2f %.2f %.2f %.2f %.2f %.2f %.2f %.2f\n"
% ( self.Frequency[i],
self.Tmin [i],
self.Rn [i],
np.absolute(self.Zopt[i]), np.angle(self.Zopt[i]),
np.absolute(self.S11 [i]), np.angle(self.S11 [i]),
np.absolute(self.S12 [i]), np.angle(self.S12 [i]),
np.absolute(self.S21 [i]), np.angle(self.S21 [i]),
np.absolute(self.S22 [i]), np.angle(self.S22 [i])
) )
# write end block
fh.write(self.__KEYS['Comment'] + " \n" )
fh.write(self.__KEYS['Comment'] + " end of file\n" )
fh.close()
[docs]
def readFile ( self, filename) :
"""
Method to read an signal-chain noise data file.
Parameters:
filename (string): Name of the input file
Returns:
idOK (boolean): error flag (true on success, false on error)
"""
self.printOut("Reading File %s" % filename, self.log.FLOW_TYPE)
# delete all data
self.Frequency = []
self.Tmin = []
self.Rn = []
self.Zopt = []
self.S11 = []
self.S12 = []
self.S21 = []
self.S22 = []
idOK = False
with open(filename, 'r') as f : # open file for reading
for line in f : # and loop all lines in the file
line = line.strip() # remove CR / NL / EOF from line end
columns = line.split() # split line
if ( len(columns) == 0) : continue
if columns[0] == self.__KEYS['FileType'] :# check for correct type-ID
idOK = True
if not idOK : continue # continue with next line if type-ID was not set so far
# Parse header line
if columns[0] == self.__KEYS['Name'] : # Name of the amplifier
self.Name = columns[1]
continue
if columns[0] == self.__KEYS['T0'] : # reference temperature
self.T0 = float(columns[1])
continue
if columns[0] == self.__KEYS['Zs'] : # nominal input impedance
self.Zs = float(columns[1])
continue
if columns[0] == self.__KEYS['Estimate'] : # is the Rn-Data estimated?
self.Estimate = bool(columns[1])
continue
if ( columns[0] == self.__KEYS['Data'] ) :
self.Frequency.append(float(columns[ 1])) # frequency
self.Tmin.append (float(columns[ 2])) # Tmin
self.Rn.append (float(columns[ 3])) # Rn
self.Zopt.append (float(columns[ 4]) * np.exp(1j*float(columns[ 5])) ) # Zopt
self.S11.append (float(columns[ 6]) * np.exp(1j*float(columns[ 7])) ) # S11
self.S12.append (float(columns[ 8]) * np.exp(1j*float(columns[ 9])) ) # S12
self.S21.append (float(columns[10]) * np.exp(1j*float(columns[11])) ) # S21
self.S22.append (float(columns[12]) * np.exp(1j*float(columns[13])) ) # S22
f.close()
if not idOK :
snp = SnPDataClass.SnPData( log = self.log )
idOK = snp.readSnP(filename)
if not idOK :
self.printOut("Error while reading", self.log.ERROR_TYPE, False)
return idOK
self.Estimate = False
self.Name = filename.split("/")[-1].split(".")[0] # using the file-name without path and extension as signal chain name
for f_idx, f in enumerate(snp.freq):
self.Frequency.append(f)
self.S11.append (snp.Param[f_idx][0, 0])
self.S12.append (snp.Param[f_idx][0, 1])
self.S21.append (snp.Param[f_idx][1, 0])
self.S22.append (snp.Param[f_idx][1, 1])
self.T0 = _Tiso
self.Tmin.append (_Tiso * (10.**(np.absolute(snp.Noise[f_idx][0])/10.)-1.)) # calculate minimum noise temp.
self.Zopt.append ((1.-snp.Noise[f_idx][1])/(1.+snp.Noise[f_idx][1])*snp.RefR) # calculate optimum input impedance
self.Zs = snp.RefR # use reference impedance as char. impedance
self.Rn.append ( np.absolute(snp.Noise[f_idx][2])*snp.RefR) # transfer normalized Rn to noise resistance
self.Frequency = np.array(self.Frequency)
self.Tmin = np.array(self.Tmin)
self.Rn = np.array(self.Rn)
self.Zopt = np.array(self.Zopt)
self.S11 = np.array(self.S11)
self.S12 = np.array(self.S12)
self.S21 = np.array(self.S21)
self.S22 = np.array(self.S22)
return idOK
#####################################
# modify data
[docs]
def makeDataset(self, Name, F, Tmin = [], Zopt = [], Rn =[], S11 = [], S12 = [], S21 = [], S22 = []) :
"""
Make a data-set from pre-defined data points.
Parameters:
Name (string): Name of the signal-chain
F (list): All other data will be interpolated to these frequencies [GHz] and are assumed to be equally distributed in the range Fmin to Fmax
Tmin (list): List of minimum tempertures [K]
Rn (list): List of noise resistances [Ohm]
Zopt (list): List of input impedances [Ohm]
S11 (list): List of S11 data [dB]
S12 (list): List of S12 data [dB]
S21 (list): List of S21 data [dB]
S22 (list): List of S22 data [dB]
"""
self.printOut("Creating new data-set for %s" % Name, self.log.FLOW_TYPE)
self.Name = Name
F = np.asarray(F)
if len(F) < 2 :
self.printOut("Not enough frequency parameters, at least minimum and maximum frequency must be given", self.log.ERROR_TYPE, False)
elif Tmin == [] :
self.printOut("Need Tmin to proceed", self.log.ERROR_TYPE, False)
else:
if len(Tmin) < 2 :
Tmin = [Tmin[0], Tmin[0]]
if Zopt == [] : # set Zopt to 50Ohms if nothing else is given
self.printOut("Assuming Zopt to be 50 Ohm", self.log.DEBUG_TYPE, False)
Zopt = [50 +1j*0]
self.Frequency = np.array(F) # set frequency array
self.Tmin = np.interp(F, np.mgrid[F.min():F.max():(1j*len(Tmin))], Tmin ) # set Tmin array
self.Zopt = np.interp(F, np.mgrid[F.min():F.max():(1j*len(Zopt))], Zopt ) # set Zopt array
if len(Rn) > 0 :
self.Rn = np.interp(F, np.mgrid[F.min():F.max():(1j*len(Rn) )], Rn ) # set Rn array if given
else :
self.printOut("Estimating noise resistance:", self.log.DEBUG_TYPE, False) # otherwise estimate data
self.Estimate = True
self.Rn = np.zeros(len(Tmin), dtype = np.complex128) # set up empty array
fList = np.mgrid[F.min():F.max():(1j*(len(Tmin)))] # get frequencies belonging to Tmin data
for t_idx, tmin in enumerate(self.Tmin) : # loop Tmin
zopt = np.interp(fList[t_idx], self.Frequency, self.Zopt) # get zopt belonging to Tmin
self.Rn[t_idx] = 0.5*np.real(zopt)*(tmin/self.T0) # and calculate estimated Rn
# S-Parameters (for completness only) # TODO move this into dedicated method?
if len(S11) == 0 :
S11 = [0.]
self.S11 = np.interp(F, np.mgrid[F.min():F.max():(1j*len(S11) )], S11 )
if len(S12) == 0 :
S12 = [-30]
self.S12 = np.interp(F, np.mgrid[F.min():F.max():(1j*len(S12) )], S12 )
if len(S21) == 0 :
S21 = [30.]
self.S21 = np.interp(F, np.mgrid[F.min():F.max():(1j*len(S21) )], S21 )
if len(S22) == 0 :
S22 = [0.]
self.S22 = np.interp(F, np.mgrid[F.min():F.max():(1j*len(S22) )], S22 )
[docs]
def selectFreq (self, startF = -1., stopF = -1.) :
"""
Selects a frequency band of the data.
Parameters:
startF (float): Lower band end (negative value: lowest available frequency)
stopF (float): Higher band end (negative value: highest available frequency)
"""
if startF < 0 : startF = self.Frequency[0]
if stopF < 0 : stopF = self.Frequency[-1]
self.printOut("Selecting frequency Range (%.1f to %.1fGHz)" %(startF, stopF), self.log.FLOW_TYPE )
freq = []
Tmin = []
Rn = []
Zopt = []
S11 = []
S12 = []
S21 = []
S22 = []
for f_idx, f in enumerate(self.Frequency) :
if (f >= startF) and (f <= stopF) :
freq.append(f)
Tmin.append(self.Tmin[f_idx])
Rn.append( self.Rn [f_idx])
Zopt.append(self.Zopt[f_idx])
S11.append( self.S11 [f_idx])
S12.append( self.S12 [f_idx])
S21.append( self.S21 [f_idx])
S22.append( self.S22 [f_idx])
self.Frequency = np.array(freq)
self.Tmin = np.array(Tmin)
self.Zopt = np.array(Zopt)
self.Rn = np.array(Rn)
self.S11 = np.array(S11)
self.S12 = np.array(S12)
self.S21 = np.array(S21)
self.S22 = np.array(S22)
#####################################
# accessing datas
[docs]
def getData (self, f) :
"""
Linear interpolation of the data set to get value for frequency f.
Parameters:
f (float): Frequency [GHz]
Returns:
tuple (tuple): tmin, rn, zopt, s11, s12, s21, s22
"""
tmin = np.interp(f, self.Frequency, self.Tmin)
rn = np.interp(f, self.Frequency, self.Rn )
zopt = np.interp(f, self.Frequency, self.Zopt)
s11 = np.interp(f, self.Frequency, self.S11 )
s12 = np.interp(f, self.Frequency, self.S12 )
s21 = np.interp(f, self.Frequency, self.S21 )
s22 = np.interp(f, self.Frequency, self.S22 )
return tmin, rn, zopt, s11, s12, s21, s22
#####################################
# plotting methods
[docs]
def plot (self, figname = "", show = False, onlyNoise = False, Zmatch = -1.) :
"""
Plot the actual data set.
Parameters:
figname (string): Name of the graphics file written
show (boolean): If True, plot will be shown on screen without being written to file
onlyNoise (boolean): Plot only noise data
Zmatch (float): Signal impedance. If > 0, the noise temperature belonging to this impedance is plotted as well
"""
self.printOut("Plotting signal chain data: Noise Parameter", self.log.FLOW_TYPE)
legend = []
fig, ax1 = plt.subplots()
plt.title("Noise parameters for signal chain : " + self.Name)
ax1.plot(self.Frequency, self.Tmin, lw=1, linestyle = 'dashed', color="red")
legend.append('Tmin')
ax1.set_ylabel("Noise-temperature [K]", color="red")
ax1.set_xlabel("Frequency [GHz]")
for label in ax1.get_yticklabels():
label.set_color("red")
if ( isinstance(Zmatch, float) or isinstance(Zmatch, complex) or isinstance(Zmatch, int ) ) and (Zmatch > 0.) :
Zmatch = [Zmatch]
else:
Zmatch = []
if len(Zmatch) > 0 :
colorList =["red", "green", "orange", "brown", "yellow", "purple"]
while len(colorList) < len(Zmatch) :
colorList += colorList
for zidx, zmatch in enumerate(Zmatch) :
Ys = 1./ zmatch
T = []
for idx, Rn in enumerate(self.Rn):
zopt = self.Zopt[idx]
tmin = self.Tmin[idx]
T.append(tmin + (Rn/np.real(Ys)*np.absolute(Ys-1/zopt)**2.)*self.T0)
ax1.plot(self.Frequency, T, lw=1, color=colorList[zidx])
legend.append('$T_{LNA}$ @ $Z_S$ = %.1f$\\Omega$' % np.absolute(zmatch))
if not onlyNoise :
ax2 = ax1.twinx()
ax2.plot(self.Frequency, np.absolute(self.Rn), "--b")
legend.append('Rn')
ax2.plot(self.Frequency, np.absolute(self.Zopt), "-b")
legend.append('Zopt')
ax2.set_ylabel("Impedance (amplitude) [Ohm]", color="blue")
for label in ax2.get_yticklabels():
label.set_color("blue")
fig.legend(legend, loc='upper left', bbox_to_anchor=(0.65, 0.85))
if figname != "" :
fig.savefig(figname)
if show :
plt.show()
else :
plt.show()
plt.close()
[docs]
def plotSParam (self, figname = "", show = False) :
"""
Plot the actual S-Parameter data.
Parameters:
figname (string): Name of the graphics file written
show (boolean): If True, plot will be shown on screen without being written to file
"""
self.printOut("Plotting signal chain data -- S-Parameter", self.log.FLOW_TYPE)
fig, ax1 = plt.subplots()
plt.title("S-parameters for signal chain : " + self.Name)
ax1.plot(self.Frequency, np.log10(np.absolute(self.S11))*20., lw=1, color="blue", linestyle = "solid")
ax1.plot(self.Frequency, np.log10(np.absolute(self.S12))*20., lw=1, color="blue", linestyle = "dotted")
ax1.plot(self.Frequency, np.log10(np.absolute(self.S22))*20., lw=1, color="blue", linestyle = "dashed")
ax1.set_ylabel("S-Parameter [dB]", color="blue")
ax1.set_xlabel("Frequency [GHz]")
for label in ax1.get_yticklabels():
label.set_color("blue")
ax2 = ax1.twinx()
ax2.plot(self.Frequency, np.log10(np.absolute(self.S21))*20., lw=1, color="red", linestyle = "solid")
ax2.set_ylabel("Gain ($|S21|^2$ [dB])", color="red")
for label in ax2.get_yticklabels():
label.set_color("red")
fig.legend(['$|S11|^2$', '$|S12|^2$', '$|S22|^2$', '$|S21|^2$'], loc='upper left', bbox_to_anchor=(0.75, 0.85))
if figname != "" :
fig.savefig(figname)
if show :
plt.show()
else :
plt.show()
plt.close()