tasmet/tasmet_postprocessor.py

642 lines
19 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Created on Thu Feb 2 08:33:53 2017
@author: anne
"""
qt = 5
# Here we provide the necessary imports.
# The basic GUI widgets are located in QtGui module.
import sys, h5py
import numpy as np
if qt==4:
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4 import NavigationToolbar2QT as NavigationToolbar
else:
from PyQt5.QtWidgets import *
from PyQt5.QtGui import QIcon,QDoubleValidator
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5 import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
import time
from matplotlib.animation import FuncAnimation
from gui.about_dialog import Ui_about_dialog
scale = [('nano',-9,'n'),
('micro',-6,'$\mu$'),
('milli:',-3,'m'),
('centi',-2,'c'),
('deci',-1,'d'),
('-',0,''),
('deca',1,'da'),
('hecto',2,'h'),
('kilo',3,'k'),
('mega',6,'M'),
('giga',9,'G'),
('tera',12,'T')]
appname = 'TaSMET Postprocessor'
def getGuiSettings():
return QSettings(appname,appname)
class MaxCounter(object):
def __init__(self,max=-1):
self.a = 0
self.max = max
def __call__(self):
rval = self.a
self.a += 1
if self.a == self.max+1:
self.a = 0
return rval
class StepCounter(object):
def __init__(self,step=1):
self.a = 0
self.step = step
self.inner = 0
def __call__(self):
rval = self.a
self.inner += 1
if self.inner == self.step:
self.inner = 0
self.a += 1
return rval
class TaSMETFigure(Figure):
def __init__(self, width=5, height=4, dpi=100):
Figure.__init__(self, figsize=(width, height), dpi=dpi)
self._axes = self.add_subplot(111)
# We want the axes cleared every time plot() is called
self._plots = []
self._grid = False
def setCanvas(self,canvas):
self._canvas = canvas
def addPlot(self,plot,one):
if one:
self._plots = [plot]
else:
self._plots.append(plot)
self.rePlot()
def rePlot(self):
self._axes.clear()
for plot in self._plots:
self._axes.plot(plot['x'],plot['y'],label=plot['ylabel'])
self._axes.set_ylabel(plot['ylabel'])
self._axes.set_xlabel(plot['xlabel'])
self._axes.grid(self._grid,'both')
self.canvas.draw()
def setGrid(self,grid):
self._grid = grid
self.rePlot()
def clearLastPlot(self):
if len(self._plots) > 0:
del self._plots[-1]
self.rePlot()
def clearAllPlot(self):
self._plots = []
self.rePlot()
def animate(self, data):
line = self._axes.get_lines()[0]
max_val = np.max(data)
min_val = np.min(data)
self._axes.set_ylim((min_val,max_val))
# Number of frames available in a period
frames_per_period = data.shape[1]
# Time to pass to show one period
time_per_period = 1.0
# interval in seconds
interval = time_per_period/frames_per_period
# We need a max, here because due to a bug intervals lower than 200
# are going plain wrong in matplotlib
interval_ms = max(int(interval*1000),200)
def animate_i(i):
line.set_ydata(data[:,i%frames_per_period])
return line,
ani = FuncAnimation(self,
animate_i,
interval = max(200,interval_ms),
frames = frames_per_period,
blit=True,
repeat = True,
save_count = 300 # High value here as animate_i repeats itself
)
self.canvas.draw()
# for i in range(100):
# print(i)
# animate_i(i)
#
class TaSMETCanvas(FigureCanvas):
def __init__(self, parent, figure):
FigureCanvas.__init__(self, figure)
self.setParent(parent)
FigureCanvas.setSizePolicy(self,
QSizePolicy.Expanding,
QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)
figure.setCanvas(self)
class MainWindow(QMainWindow):
def __init__(self, file_):
QMainWindow.__init__(self)
# Open the postprocessing file
try:
self._file = h5py.File(file_,'r')
except:
QMessageBox.warning(self,
'Error opening file',
'File %s is not a valid TaSMET Postprocessing file. Closing.' %file_)
exit(1)
# Suppress triggering of the GUI changed() callbacks
self._suppressed = True
self.setWindowTitle(appname)
# Window icon
self.setWindowIcon(QIcon('images/tasmet_small.png'))
self._figure = TaSMETFigure()
self._canvas= TaSMETCanvas(self,figure=self._figure)
self._seg_box = QComboBox()
self._seg_box.currentIndexChanged.connect(self.segItemChanged)
self._qty_box = QComboBox()
self._qty_box.currentIndexChanged.connect(self.qtyItemChanged)
# Horizontal offset
self._hor_offset = QLineEdit()
self._hor_offset.setAlignment(Qt.AlignRight)
self._hor_offset.setText('0.0')
self._hor_offset.setMinimumWidth(40)
# Vertical offset
self._ver_offset = QLineEdit()
self._ver_offset.setAlignment(Qt.AlignRight)
self._ver_offset.setText('0.0')
self._ver_offset.setMinimumWidth(40)
offset_validator = QDoubleValidator()
self._hor_offset.setValidator(offset_validator)
self._ver_offset.setValidator(offset_validator)
self._scale = QComboBox()
for val in scale:
self._scale.addItem(val[0] + ' (%0.0e)' %(10**val[1]))
self._scale.setCurrentIndex(5)
self._hold_button = QPushButton('Hold on')
self._hold_button.setCheckable(True)
self._vstime = QRadioButton('Plot vs time')
self._vstime.clicked.connect(self.qtyItemChanged)
self._vspos = QRadioButton('Plot vs position')
self._vspos.clicked.connect(self.qtyItemChanged)
self._vspos.setChecked(True)
self._vs_instance = QSpinBox()
self._plot_button = QPushButton('Plot')
self._plot_button.clicked.connect(self.plot)
self._output_button = QPushButton('Output')
self._output_button.clicked.connect(self.output)
self._animate_button = QPushButton('Animate')
self._animate_button.clicked.connect(self.animate)
top = QGridLayout()
col = StepCounter(step=2)
row = MaxCounter(max=1)
top.addWidget(QLabel('Segment:'),row(),col())
top.addWidget(QLabel('Quantity:'),row(),col())
top.addWidget(self._seg_box,row(),col())
top.addWidget(self._qty_box,row(),col())
top.addWidget(QLabel('Horizontal offset'),row(),col())
top.addWidget(QLabel('Vertical offset'),row(),col())
top.addWidget(self._hor_offset,row(),col())
top.addWidget(self._ver_offset,row(),col())
top.addWidget(QLabel('Scale:'),row(),col())
row(); col()
top.addWidget(self._scale,row(),col())
row(); col()
radiobox = QHBoxLayout()
radiobox.addWidget(self._vstime)
radiobox.addWidget(self._vspos)
top.addLayout(radiobox,row(),col())
instance_box = QHBoxLayout()
instance_box.addWidget(QLabel('Time/position instance:'))
instance_box.addWidget(self._vs_instance)
top.addLayout(instance_box,row(),col())
top.addWidget(self._plot_button,row(),col())
top.addWidget(self._hold_button,row(),col())
top.addWidget(self._output_button,row(),col())
top.addWidget(self._animate_button,row(),col())
htop = QHBoxLayout()
htop.addLayout(top)
htop.addStretch()
vlayout = QVBoxLayout()
vlayout.addLayout(htop)
navbar = NavigationToolbar(self._canvas, self)
vlayout.addWidget(navbar)
vlayout.addWidget(self._canvas)
hlayout = QHBoxLayout()
output_layout = QVBoxLayout()
output_layout.addWidget(QLabel('Output'))
self._output = QPlainTextEdit()
self._output.setFixedWidth(300)
self._output.setStyleSheet('font-family: monospace; background-color: white; color: black;')
output_layout.addWidget(self._output)
hlayout.addLayout(vlayout)
hlayout.addLayout(output_layout)
# Put stuff in a widget and put it in the main window
widget = QWidget()
widget.setLayout(hlayout)
self.setCentralWidget(widget)
# File menu
file_menu = QMenu('&File', self)
file_menu.addAction('&Open',self.loadFile)
file_menu.addAction('&Exit',self.close)
# Plot menu
plot_menu = QMenu('&Plot', self)
self._grid_option = QAction('Enable grid',plot_menu)
self._grid_option.setCheckable(True)
self._grid_option.triggered.connect(self._figure.setGrid)
plot_menu.addAction(self._grid_option)
plot_menu.addAction('&Delete last line',self._figure.clearLastPlot)
plot_menu.addAction('&Delete all lines',self._figure.clearAllPlot)
# Options menu
options_menu = QMenu('&Options', self)
options_menu.addAction('Clear output',self.clearOutput)
options_menu.addAction('Default output',self.defaultOutput)
# Help menu
help_menu = QMenu('&Help', self)
help_menu.addAction('&About', self.about)
# Add menus to the menubar
self.menuBar().addMenu(file_menu)
self.menuBar().addMenu(plot_menu)
self.menuBar().addMenu(options_menu)
self.menuBar().addMenu(help_menu)
# Set window sizes
settings = getGuiSettings()
if settings.value('Geometry') is not None:
self.restoreGeometry(settings.value("Geometry"))
self.initGui()
self._suppressed = False
def clearOutput(self):
self._output.setPlainText('')
def defaultOutput(self):
txt = ''
nsegments = 0
for x in self._file:
if isinstance(self._file[x], h5py.Group):
nsegments+=1
basestr = '{:22}: {:>12} [{:2}]\n'
txt += basestr.format('Number of segments',nsegments,'-')
txt += basestr.format('Fundamental frequency',self._file.attrs['freq'][0],'Hz')
txt += basestr.format('Numer of time samples',self._file.attrs['Ns'][0],'-')
txt += basestr.format('Reference temperature',self._file.attrs['T0'][0],'K')
txt += basestr.format('Reference pressure',self._file.attrs['p0'][0],'Pa')
txt += basestr.format('Gas type',u''+self._file.attrs['gas'].decode('utf-8'),'-')
tinst = self._file.attrs['t']
txt += '\nTime instances:\n'
for i in range(tinst.shape[0]):
txt += '{:2}: {:2e}\n'.format(i,tinst[i])
self._output.setPlainText(txt)
def output(self):
print('output')
def disableAll(self):
self._seg_box.setEnabled(False)
self._qty_box.setEnabled(False)
self._vspos.setEnabled(False)
self._vstime.setEnabled(False)
self._vs_instance.setEnabled(False)
self._plot_button.setEnabled(False)
self._output_button.setEnabled(False)
self._animate_button.setEnabled(False)
def animate(self):
self.disableAll()
self._hold_button.setChecked(False)
self.plot()
try:
data,datatype = self.getCurrentData()
except:
return
self._figure.animate(data[:,:])
self.qtyItemChanged()
def getCurrentData(self):
"""
Returns the current datatype, None if invalid or not selected
"""
segitemno = self._seg_box.currentText()
itemtxt = self._qty_box.currentText()
if len(segitemno) == 0:
return None
if len(itemtxt)==0:
return None
else:
data = self._file[segitemno][itemtxt]
datatype = data.attrs['datatype'].decode('utf-8')
return data,datatype
def segItemChanged(self,item=None):
"""
Update the combobox with quantities
"""
self._suppressed = True
self._qty_box.clear()
one = False
for dataset in self._file[str(item)]:
self._qty_box.addItem(dataset)
one = True
# Not at least one plottable item in the list of the selected segment
self._plot_button.setEnabled(one)
self._output_button.setEnabled(one)
self._suppressed = False
self._qty_box.setCurrentIndex(0)
self.qtyItemChanged()
def qtyItemChanged(self,item=None):
"""
The quantity item has been changed
"""
if self._suppressed: return
try:
data,datatype = self.getCurrentData()
except:
data = None
segitemno = self._seg_box.currentText()
self.disableAll()
self._seg_box.setEnabled(True)
self._qty_box.setEnabled(True)
if data is None:
return
self._plot_button.setEnabled(True)
self._output_button.setEnabled(True)
if datatype == 'time':
self._vstime.setChecked(True)
self._vspos.setEnabled(False)
elif datatype == 'pos':
self._vspos.setChecked(True)
self._vspos.setEnabled(False)
self._vstime.setEnabled(False)
self._vs_instance.setEnabled(False)
self._animate_button.setEnabled(False)
elif datatype == 'postime':
vspos = self._vspos.isChecked()
if vspos:
t = self._file.attrs['t']
self._vs_instance.setRange(0,len(t)-1)
else:
x = self._file[segitemno]['x']
self._vs_instance.setRange(0,len(x)-1)
self._vs_instance.setValue(0)
self._vspos.setEnabled(True)
self._vstime.setEnabled(True)
self._vs_instance.setEnabled(True)
self._animate_button.setEnabled(True)
def initGui(self):
self.defaultOutput()
self._suppressed = True
for x in self._file:
if isinstance(self._file[x], h5py.Group):
self._seg_box.addItem(x)
else:
print(self._file[x])
self._suppressed = False
self.segItemChanged(0)
def loadFile(self, file=None):
pass
def getx(self):
"""
Returns the x-axis for the current data selected. Returns None if invalid.
Returns a tuple containing: x-values, x-legend
"""
try:
data,datatype = self.getCurrentData()
except:
return None
segitemno = self._seg_box.currentText()
vspos = self._vspos.isChecked()
x = None
if datatype == 'time' or (datatype == 'postime' and not vspos):
x = self._file.attrs['t']
xsymbol = 't'
xunit = 's'
elif datatype == 'pos' or (datatype == 'postime' and vspos):
xset = self._file[segitemno]['x']
x = xset[:]
xsymbol = xset.attrs['symbol'].decode('utf-8')
xunit = xset.attrs['unit'].decode('utf-8')
xlabel = xsymbol + ' [' + xunit + ']'
return x,xlabel
def gety(self):
"""
Returns the y-axis for the current data selected. Returns None if invalid.
Returns a tuple containing x-values, x-legend
"""
try:
data,datatype = self.getCurrentData()
except:
return None
symbol = data.attrs['symbol'].decode('utf-8')
unit = data.attrs['unit'].decode('utf-8')
vspos = self._vspos.isChecked()
ylabel = symbol + ' [' + unit + ']'
if datatype == 'time' or datatype == 'pos':
y = data[:]
elif datatype == 'postime':
data_inst = self._vs_instance.value()
if self._vspos.isChecked():
y = data[:,data_inst]
else:
y = data[data_inst,:]
return y,ylabel
def plot(self):
plot = {}
hor_offset = float(self._hor_offset.text())
ver_offset = float(self._ver_offset.text())
try:
x,xlabel = self.getx()
except TypeError:
return
try:
y,ylabel = self.gety()
except TypeError:
return
plot['x'] = x+hor_offset
plot['xlabel'] = xlabel
thescale = scale[self._scale.currentIndex()]
scaleval = thescale[1]
scaleylabel = '' if scaleval == 0 else ' x $10^{%i}$' %scaleval
plot['y'] = (y+ver_offset)/(10**scaleval)
plot['ylabel'] = ylabel + ' ' + scaleylabel
self._figure.addPlot(plot, not self._hold_button.isChecked())
def about(self):
dialog = QDialog(self)
about_dialog = Ui_about_dialog()
about_dialog.setupUi(dialog)
dialog.exec_()
def closeEvent(self,event):
settings = getGuiSettings()
settings.setValue("Geometry", self.saveGeometry())
event.accept()
def usage():
print("Usage:")
if hasattr(sys,'frozen'):
pass
else:
print('python ' + sys.argv[0] + ' <file>.tasmet.h5')
if __name__ == '__main__':
if(len(sys.argv) < 2):
usage()
exit(-1)
else:
file_ = sys.argv[1]
# Every PyQt4 application must create an application object.
# The application object is located in the QtGui module.
# try:
# app = QApplication.instance()
# except:
app = QApplication(sys.argv)
# The QWidget widget is the base class of all user interface objects in PyQt4.
# We provide the default constructor for QWidget. The default constructor has no parent.
# A widget with no parent is called a window.
# w = MainWindow(sys.argv[1])
w = MainWindow(file_)
w.resize(640, 480) # The resize() method resizes the widget.
w.show() # The show() method displays the widget on the screen.
sys.exit(app.exec_()) # Finally, we enter the mainloop of the application.