#!/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 sys
import argparse
import numpy as np
import matplotlib.pyplot as plt
from copy import copy, deepcopy
import LogClass
from Simulation import FarFieldDataClass
from Simulation.constants import _CSTInstallationPath, _CSTProjectPath
# import CST
sys.path.append(_CSTInstallationPath + "/LinuxAMD64/python_cst_libraries/cst/") # TODO this might be outdated
try :
import interface as csti
except :
print("No CST-interface, installation path should be :", _CSTInstallationPath)
csti = None
try:
import results as cstr
except :
print("No CST results, installation path should be :", _CSTInstallationPath)
cstr = None
# TODO separate method 2 (reading CST project data) into its own method
#############################################################################
# constants
#############################################################################
_CLASSNAME = "CSTData"
#############################################################################
# Class definition
#############################################################################
[docs]
class CST :
"""
This module provides two methods to read farfield data generated by CST (https://www.3ds.com/products/simulia/cst-studio-suite).
Method 1 (readFarFieldSources): Reading farfield files (.ffs) exported by CST
This method requires no additional libraries and no local installation of the CST suite.
It does require .ffs-files. CST has an export feature that allows farfield data to be stored in said format.
Method 2: Using the official CST Python library to access project files (.cst)
An installation of CST contains Python libraries which are able to read the proprietary project files.
This requires a local installation of CST and the project files containing your desired farfield data.
Be aware that the Python libraries provided by CST only support specific Python versions.
Make sure that you run this module with a supported version of Python when using the official CST Python library.
The PAF Frontend simulator requires some form of farfield data, though it doesn't have to be generated by CST.
For this reason, this module is optional and internal methods to generate farfield data may be used instead (see FarFieldDataClass.py).
"""
def __init__ ( self, log = None, project = "" ) :
"""
Initialize class.
Parameters :
project (string): Name of the CST project (folder name within the _CSTProjectPath folder). If empty, an empty object is created.
log (LogClass): Logging class to be used
"""
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.printOut("Initializing CST data-access :", self.log.FLOW_TYPE)
if cstr == None :
self.printOut("no CST-Lib present!", self.log.WARNING_TYPE, False)
self.printOut("actual CST installation path : %s" % _CSTInstallationPath, self.log.DEBUG_TYPE, False)
self.printOut("actual CST project path : %s" % _CSTProjectPath, self.log.DEBUG_TYPE, False)
self.printOut("opening CST project : %s" % project, self.log.DEBUG_TYPE, False)
self.folder = _CSTProjectPath + project
try :
self.ItemList = self._getItemList() # List of all electrical data available
except :
self.ItemList = []
self.printOut("found %d data-sets in 3D results" % len(self.ItemList), self.log.DEBUG_TYPE, False)
# extract reference impedance
self.Impedance = -1.
ZRef = self._getItem("ZRef 1")
if ZRef != None :
zref = ZRef.get_data()
if len(zref [0]) > 1 :
self.Impedance = np.absolute(zref[0][1])
self.printOut("extracted a reference impedance of %.1f Ohm" % self.Impedance, self.log.DEBUG_TYPE, False)
if self.Impedance < 0 :
self.Impedance = 50.
self.printOut("no reference impedance to extract, using %.1f Ohm" % self.Impedance, self.log.DEBUG_TYPE, False)
[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)
#############################################################################
# 1D results
#############################################################################
def _getItemList ( self ) :
"""
Returns all 1D results available for this project.
"""
self.printOut("Getting item list of CST-Project", self.log.FLOW_TYPE)
if cstr == None : return []
# get all items from item tree
project = cstr.ProjectFile(self.folder + ".cst", allow_interactive= True)
results3D = project.get_3d() # 3D-results available
items = results3D.get_tree_items()
iList = []
# filter item tree for electrical and field data
for i in items :
columns = i.split("\\")
if ( (len(columns) > 1) and
( columns[1] != "Optimizer") and
( columns[1] != "Materials") and
( columns[1] != "Port Information") and
( columns[1] != "Adaptive Meshing")
) :
iList.append(i)
#self.printOut(" found item : %s" % i, False)
return iList
def _getItem ( self, itemName ) :
"""
get the data-set of the item with name 'itemName'
Parameters :
itemName : full or part name of the item (first occurance of the string in the list will be returned).
Returns : cst.results.ResultItem or None
"""
# search for itemName
for i in self.ItemList :
if i.find(itemName) > -1 :
project = cstr.ProjectFile(self.folder + ".cst", allow_interactive= True)
return project.get_3d().get_result_item(i)
return None
[docs]
def getItemData ( self, itemName, minX = None, maxX = None ) :
"""
Get the data of the item with name 'itemName'.
Parameters:
itemName (string): Full or part name of the item (first occurance of the string in the list will be returned).
Returns:
List (list): of x-data, y-data, plot-title, x-axis label, y-axis label
or None
"""
result = self._getItem(itemName)
if result == None:
return None
xdata = result.get_xdata()
ydata = result.get_ydata()
xfiltered = []
yfiltered = []
if minX == None : minX = np.amin(xdata) - 1.
if maxX == None : maxX = np.amax(xdata) + 1.
for idx, x in enumerate(xdata) :
if ( x >= minX ) and ( x <= maxX ) :
xfiltered.append(x)
yfiltered.append(ydata[idx ])
return [xfiltered, yfiltered, result.title, result.xlabel, result.ylabel]
[docs]
def plotItem ( self, itemName, unit = "Mag", figname = "", show = False ) :
"""
Plot the data of the item with name 'itemName'.
Breakdown of the units:
"Mag" : using a linear scale of the magnutude of the value (if complex, amplitude is used)
"dB" : using dB scale (power) of the magnutude of the value (if complex, amplitude is used)
"Phase" : using the phase (only for complex y-data)
Parameters:
itemName (string): Full or part name of the item (first occurance of the string in the list will be returned).
unit (string): Identifier to switch between amplitude, phase and liner / dB scale
figname (string): File-name of the figure (if given figure is only shown on screen if show == True
show (boolean): Flag to show plot on screen even when being written to disk (True)
"""
self.printOut("Plotting data set belonging to item '%s'" % itemName, self.log.FLOW_TYPE)
data = self.getItemData ( itemName )
if data == None :
self.printOut("item with name '%s' does not exist" % itemName, self.log.ERROR_TYPE, False)
return
# transform result to numpy arrays
data[0] = np.asarray(data[0])
data[1] = np.asarray(data[1])
# check for complex numbers and use the absolute value in case
if isinstance(data[1][0], complex) or isinstance(data[1][0], np._complex) :
if unit == "Phase" :
data[1] = np.unwrap(np.angle(data[1]))*180./np.pi
data[4] = "Phase [deg]"
else :
data[1] = np.absolute(data[1])
if unit == "dB" :
data[1] = np.log10(data[1])*20.
data[4] = "power [dB]"
fig, ax = plt.subplots()
ax.plot(data[0], data[1], 'k-')
ax.set_title (data[2])
ax.set_xlabel(data[3])
ax.set_ylabel(data[4])
if figname != "" :
fig.savefig(figname)
if show :
plt.show()
else :
plt.show()
plt.close()
#############################################################################
# 3D results
#############################################################################
[docs]
def readFarFieldSources ( self, cstImpedance = -1) :
"""
Method to read a CST farfield source file (.ffs).
Currently, each farfield source file must contain only one frequency.
Parameters:
cstImpedance (int): Override CST simulation reference impedance (negative, use simulation setting)
Returns:
dataValid (boolean): error flag (true on success, false on error)
FarFields (list): list of far-field data classes
"""
self.printOut("Reading Far-Field Source data", self.log.FLOW_TYPE)
if cstImpedance <= 0. :
cstImpedance = self.Impedance
else :
self.printOut("overriding reference impedance with %.1f Ohm" % cstImpedance, self.log.DEBUG_TYPE, False)
FarFields = [] # delete all data
dataValid = False # data valid flag
lcount = 0 # line-counter
lnFreq = -1 # line with the number of frequency packages
lExp = -10 # line with experimental data (Power, frequency)
lNum = -10 # line with number of elements data
lCenter = -10 # line with center position
lzAxis = -10 # line with z-axis data
lxAxis = -10 # line with x-axis data
nPhi = 0 # number of Phi values
nTh = 0 # number of Theta values
dataSet = -1 # actual data set
freqList = []
with open(self.folder + "/Result/Farfield Source [1].ffs" , 'r') as f : # open file
for line in f : # and loop all lines in the file
line = line.strip() # remove CR / NL / EOF from line end
if line == "// #Frequencies" :
lnFreq = lcount + 1
dataValid = False
if line == "// Radiated/Accepted/Stimulated Power , Frequency" : # parse line contend for experimental data
lExp = lcount # set line-number of the frequency entry
dataValid = False
self.printOut("reading individal setting for each frequency", self.log.DEBUG_TYPE, False)
if line == "// Position" : # parse line contend for center position
lCenter = lcount+1 # set line-number of the center position
dataValid = False
if line == "// xAxis" : # parse line contend for center position
lxAxis = lcount+1 # set line-number of the center position
dataValid = False
if line == "// zAxis" : # parse line contend for center position
lzAxis = lcount+1 # set line-number of the center position
dataValid = False
if line == "// >> Total #phi samples, total #theta samples" : # parse line contend for number of Phi & Theta values
lNum = lcount + 1 # set line number of the sampling data
dataValid = False
if line == "// >> Phi, Theta, Re(E_Theta), Im(E_Theta), Re(E_Phi), Im(E_Phi):" : # parse line contend for start of the data block
dataValid = True # indicate start of the data-block
dataSet += 1
self.printOut("reading data set for frequency %.1f GHz" % freqList[dataSet], self.log.DEBUG_TYPE, False)
for i in range(nPhi): # pre-define 2d-array
row = []
for j in range(nTh):
row.append([])
FarFields[dataSet].FarField.append(row)
if dataValid : # if data are valid
columns = line.split() # extract values
if ( len(columns) == 0 ) or (columns[0] == "//") : continue
c0 = float(columns[0])
c1 = float(columns[1])
iPh = int ( (nPhi-1)*float(c0)/360.) # calculate array index for Phi and Theta
iTh = int ( (nTh -1)*float(c1)/180.)
FarFields[dataSet].FarField[iPh][iTh] = [ c0, c1, # compose complex Ephi and Eth-data
(float(columns[4])+1j*float(columns[5])) , # (Eth comes first in the data-line, but array
(float(columns[2])+1j*float(columns[3])) ] # is [Phi, Theta, Ephi, Eth]!)
# extract number of frequenciy data sets in the file
if lcount == lnFreq :
columns = line.split()
nF = int(columns[0])
self.printOut("found %d frequency settings" % nF, self.log.DEBUG_TYPE, False)
for f_idx in range(0, nF) :
FarFields.append(FarFieldDataClass.FarFieldData( log = self.log ))
# extract experimental data
if (lExp > 0) and (lcount > lExp) and (lcount < lExp+5*len(FarFields)) : # at correct line-number extract experimental data
f_idx = int((lcount-lExp) / 5)
columns = line.split()
if (lcount - lExp) % 5 == 1 :
FarFields[f_idx].S21 = float(columns[0]) # extract S21
if (lcount - lExp) % 5 == 2 :
FarFields[f_idx].S11 = float(columns[0]) # extract S11
if (lcount - lExp) % 5 == 3 :
FarFields[f_idx].I0 = np.sqrt(2.*float(columns[0])/cstImpedance) # extract I0
FarFields[f_idx].S21 = np.sqrt(FarFields[f_idx].S21 / float(columns[0]))
FarFields[f_idx].S11 = np.sqrt(1. - FarFields[f_idx].S11 / float(columns[0]))
if (lcount - lExp) % 5 == 4 :
freq = float(columns[0])/1e9
FarFields[f_idx].Wavelength = _C / float(columns[0]) * 1000. # or Wavelength in mm
freqList.append(freq)
self.printOut("found data for %.1f GHz" % freq, self.log.DEBUG_TYPE, False)
# extract number of data-points
if lcount == lNum : # at correct line-number extract array dimensions
columns = line.split()
nPhi = int(columns[0])
nTh = int(columns[1])
# extract center position
if lcount == lCenter : # at correct line-number extract center positon
columns = line.split()
PhaseCenter = [ float(columns[0])*1000., float(columns[1])*1000., float(columns[2])*1000. ]
for ff in FarFields :
ff.PhaseCenter = PhaseCenter
# extract x-axis orientation
if lcount == lxAxis : # at correct line-number extract x-axis
columns = line.split()
OrientationX = [ float(columns[0]), float(columns[1]), float(columns[2]) ]
for ff in FarFields :
ff.OrientationX = OrientationX
# extract z-axis orientation
if lcount == lzAxis : # at correct line-number extract z-axis
columns = line.split()
OrientationZ = [ float(columns[0]), float(columns[1]), float(columns[2]) ]
for ff in FarFields :
ff.OrientationZ = OrientationZ
lcount += 1 # increment line-counter
f.close()
# set internal variables of the FarFields
for ff in FarFields :
ff.moveCenter (0., 0., 0.) # absolute center not given, move it to zero
ff.movePhaseCenter(0., 0., 0.)
ff.ReferenceDist = 1000. # reference distance in CST is 1m
ff.Gain = 1.
ff.Phase = 0.
ff.Bandwidth = 1000000. # default CST calculation bandwidth (1MHz)
ff.Type = 'CST' # set calculation type
return dataValid, FarFields
#############################################################################
# Main function;
#
#############################################################################
if __name__=="__main__":
pass
# end