Source code for src.Libs.Simulation.SignalChainDataClass

#!/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()