642 lines
19 KiB
Python
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.
|