From 475f2fa6aa93ae7c30548b2c3cb1b055867e31ce Mon Sep 17 00:00:00 2001 From: "J.A. de Jong" Date: Mon, 4 Jun 2018 15:00:27 +0200 Subject: [PATCH] Added bar plots, different figures, and lots of things have been changed --- lasp/lasp_reverb.py | 1 + lasp/lasp_slm.py | 95 ++++++++++--- lasp/plot/__init__.py | 3 + lasp/plot/bar.py | 307 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 388 insertions(+), 18 deletions(-) create mode 100644 lasp/plot/__init__.py create mode 100644 lasp/plot/bar.py diff --git a/lasp/lasp_reverb.py b/lasp/lasp_reverb.py index ada61d0..6c476b0 100644 --- a/lasp/lasp_reverb.py +++ b/lasp/lasp_reverb.py @@ -13,6 +13,7 @@ class ReverbTime: """ Tool to estimate the reverberation time """ + def __init__(self, fs, level): """ Initialize Reverberation time computer. diff --git a/lasp/lasp_slm.py b/lasp/lasp_slm.py index 7a4ed4b..df651b7 100644 --- a/lasp/lasp_slm.py +++ b/lasp/lasp_slm.py @@ -12,7 +12,7 @@ from .lasp_common import (FreqWeighting, sens, calfile, TimeWeighting, getTime, P_REF) from .lasp_weighcal import WeighCal from .lasp_gui_tools import wait_cursor -from .lasp_figure import FigureDialog, PlotOptions +from .lasp_figure import PlotOptions, Plotable from .ui_slmwidget import Ui_SlmWidget __all__ = ['SLM', 'SlmWidget'] @@ -96,16 +96,25 @@ class SLM: class SlmWidget(ComputeWidget, Ui_SlmWidget): - def __init__(self): + def __init__(self, parent=None): """ Initialize the SlmWidget. """ - super().__init__() + super().__init__(parent) self.setupUi(self) - FreqWeighting.fillComboBox(self.freqweighting) + FreqWeighting.fillComboBox(self.tfreqweighting) + FreqWeighting.fillComboBox(self.eqfreqweighting) self.setMeas(None) + def init(self, fm): + """ + Register combobox of the figure dialog to plot to in the FigureManager + """ + super().init(fm) + fm.registerCombo(self.tfigure) + fm.registerCombo(self.eqfigure) + def setMeas(self, meas): """ Set the current measurement for this widget. @@ -119,27 +128,64 @@ class SlmWidget(ComputeWidget, Ui_SlmWidget): else: self.setEnabled(True) rt = meas.recTime - self.starttime.setRange(0, rt, 0) - self.stoptime.setRange(0, rt, rt) + self.tstarttime.setRange(0, rt, 0) + self.tstoptime.setRange(0, rt, rt) - self.channel.clear() + self.eqstarttime.setRange(0, rt, 0) + self.eqstoptime.setRange(0, rt, rt) + + self.tchannel.clear() + self.eqchannel.clear() for i in range(meas.nchannels): - self.channel.addItem(str(i)) - self.channel.setCurrentIndex(0) + self.tchannel.addItem(str(i)) + self.eqchannel.addItem(str(i)) + self.tchannel.setCurrentIndex(0) + self.eqchannel.setCurrentIndex(0) - def compute(self): + def computeEq(self): """ - Compute Sound Level using settings. This method is - called whenever the Compute button is pushed in the SLM tab + Compute equivalent levels for a piece of time """ meas = self.meas fs = meas.samplerate - channel = self.channel.currentIndex() - tw = TimeWeighting.getCurrent(self.timeweighting) - fw = FreqWeighting.getCurrent(self.freqweighting) + channel = self.eqchannel.currentIndex() + fw = FreqWeighting.getCurrent(self.eqfreqweighting) + + startpos = self.eqstarttime.value + stoppos = self.eqstoptime.value + N = meas.N + istart = int(startpos*fs) + if istart >= N: + raise ValueError("Invalid start position") + istop = int(stoppos*fs) + if istart > N: + raise ValueError("Invalid stop position") + + + with wait_cursor(): + # This one exctracts the calfile and sensitivity from global + # variables defined at the top. # TODO: Change this to a more + # robust variant. + weighcal = WeighCal(fw, nchannels=1, + fs=fs, calfile=calfile, + sens=sens) + praw = meas.praw()[istart:istop, [channel]] + pto = PlotOptions() + fig, new = self.getFigure(self.eqfigure, pto, 'bar') + fig.show() + + def computeT(self): + """ + Compute sound levels as a function of time. + """ + meas = self.meas + fs = meas.samplerate + channel = self.tchannel.currentIndex() + tw = TimeWeighting.getCurrent(self.ttimeweighting) + fw = FreqWeighting.getCurrent(self.tfreqweighting) # Downsampling factor of result - dsf = self.downsampling.value() + dsf = self.tdownsampling.value() # gb = self.slmFre with wait_cursor(): @@ -162,8 +208,11 @@ class SlmWidget(ComputeWidget, Ui_SlmWidget): pto = PlotOptions() pto.ylabel = f'L{fw[0]} [dB({fw[0]})]' pto.xlim = (time[0], time[-1]) - fig, new = self.getFigure(pto) - fig.fig.plot(time, filtered) + + pta = Plotable(time, filtered) + + fig, new = self.getFigure(self.tfigure, pto, 'line') + fig.fig.add(pta) fig.show() stats = f"""Statistical results: @@ -175,3 +224,13 @@ Maximum level (L{fw[0]} max): {Lmax:4.4} [dB({fw[0]})] """ self.results.setPlainText(stats) + + def compute(self): + """ + Compute Sound Level using settings. This method is + called whenever the Compute button is pushed in the SLM tab + """ + if self.ttab.isVisible(): + self.computeT() + elif self.eqtab.isVisible(): + self.computeEq() diff --git a/lasp/plot/__init__.py b/lasp/plot/__init__.py new file mode 100644 index 0000000..60d45b9 --- /dev/null +++ b/lasp/plot/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from .bar import BarScene diff --git a/lasp/plot/bar.py b/lasp/plot/bar.py new file mode 100644 index 0000000..3b3a190 --- /dev/null +++ b/lasp/plot/bar.py @@ -0,0 +1,307 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Author: J.A. de Jong - ASCEE + +Description: +Class for plotting bars on a QGraphicsScene. + +""" +from ..lasp_gui_tools import ASCEEColors +from PySide.QtGui import ( + QGraphicsScene, QGraphicsView, QPen, QBrush, QGraphicsRectItem, + QGraphicsTextItem, QPainter, QImage + ) + +# from PySide.QtOpenGL import +from PySide.QtCore import Qt, QRectF, QLineF, QSize, QRect, QPointF +import numpy as np +import os + + +leftoffset = .1 # Left offset of the figure +rightoffset = 0 +topoffset = .05 +bottomoffset = .1 +nyticks = 6 +ticklength = .01 + +# Distance between two bar groups in units of bar thicknesses +dxbars = 2 + +DEFAULT_COLORS = [ASCEEColors.blue, ASCEEColors.green, Qt.red] + + +class BarScene(QGraphicsScene): + """ + Graphhics Scene for plotting bars + """ + + def __init__(self, parent, xvals, G, ylim=(0, 1), + grid=True, + xlabel=None, + ylabel=None, + title=None, + colors=DEFAULT_COLORS, size=(800, 600), + legend=None): + """ + Initialize a bar scene + + Args: + xvals: labels and x positions of the bars + G: Number of bars per x value + ylim: y limits of the figure + + + """ + super().__init__(parent=parent) + + # self.setBackgroundBrush(ASCEEColors.bgBrush(0, size[0])) + self.ylim = ylim + N = len(xvals) + self.N = N + self.G = G + self.bgs = [] + + self.size = size + self.colors = colors + + # Size of the frame + Lx = 1 - rightoffset - leftoffset + Ly = 1 - topoffset - bottomoffset + + # The main frame where the bars are in. + mainframe = self.createRect(leftoffset, + bottomoffset, + Lx, + Ly) + self.addItem(mainframe) + + # Set the y ticks and ticklabels + self.yticks = [] + txtmaxwidth = 0 + for i in range(nyticks): + y = bottomoffset+Ly*i/(nyticks-1) + + ytick = self.addLine(leftoffset, + y, + leftoffset-ticklength, + y) + if grid: + ygrid = self.addLine(leftoffset, + y, + 1-rightoffset, + y,pen=QPen(Qt.gray)) + + range_ = ylim[1]-ylim[0] + ytickval = i/(nyticks-1)*range_ + ylim[0] + yticklabel = f'{ytickval:2}' + txt = QGraphicsTextItem(yticklabel) + txtwidth = txt.boundingRect().width() + txtmaxwidth = max(txtmaxwidth, txtwidth) + txt.setPos((leftoffset-.03)*self.xscale-txtwidth, + (1-y-.022)*self.yscale) + self.addItem(txt) + self.yticks.append(ytick) + + # Create the bars + for g in range(G): + bg = [] + for n in range(N): + barrect = self.getBarRect(n, g, 0) + baritem = QGraphicsRectItem(barrect, brush=QBrush(Qt.blue)) + + self.addItem(baritem) + bg.append(baritem) + + self.bgs.append(bg) + + # Add x ticks and ticklabels + for n in range(N): + xticklabel = f'{xvals[n]}' + txt = QGraphicsTextItem(xticklabel) + txtxpos = self.getBarGroupMidPos(n)-0.01*self.xscale + txt.setPos(txtxpos, + self.yscale*(1-bottomoffset+.1)) + txt.rotate(-90) + self.addItem(txt) + + # Set xlabel + if xlabel is not None: + xlabel = QGraphicsTextItem(xlabel) + width = xlabel.boundingRect().width() + txtxpos = self.xscale/2-width/2 + txtypos = .998*self.yscale + xlabel.setPos(txtxpos, txtypos) + self.addItem(xlabel) + + # Set ylabel + if ylabel is not None: + ylabel = QGraphicsTextItem(ylabel) + ylabel.setPos((leftoffset-.01)*self.xscale-txtmaxwidth, + ((1-topoffset-bottomoffset)/2+topoffset)*self.yscale) + ylabel.rotate(-90) + self.addItem(ylabel) + + + # Set title + if title is not None: + title = QGraphicsTextItem(title) + width = xlabel.boundingRect().width() + txtxpos = self.xscale/2-width/2 + txtypos = (1-.998)*self.yscale + title.setPos(txtxpos, txtypos) + self.addItem(title) + + legpos = (1-rightoffset-.3, 1-topoffset-.05) + + dyleg = 0.03 + dylegtxt = dyleg + Lyleg = .02 + Lxleg = .05 + legrectmarginpix = 5 + boxtopleft = QPointF(legpos[0]*self.xscale-legrectmarginpix, + (1-legpos[1]-Lyleg)*self.yscale-legrectmarginpix) + + + if legend is not None: + nlegs = len(legend) + maxlegtxtwidth = 0 + for i,leg in enumerate(legend): + leglabel = legend[i] + + # The position of the legend, in our coordinates + pos = (legpos[0], legpos[1] - i*dyleg) + color = self.colors[i] + + legrect = self.createRect(*pos,Lxleg,Lyleg) + + legrectwidth = legrect.boundingRect().width() + + legrect.setBrush(QBrush(color)) + legtxt = QGraphicsTextItem(leglabel) + maxlegtxtwidth = max(maxlegtxtwidth, + legtxt.boundingRect().width()) + + self.addItem(legrect) + self.addItem(legtxt) + + legtxt.setPos(legpos[0]*self.xscale+legrectwidth, + (1-pos[1]-dylegtxt)*self.yscale) + + boxbottomright = legtxt.boundingRect().topRight() + + legboxsize = QSize(maxlegtxtwidth+legrectwidth+2*legrectmarginpix, + (i+1)*dyleg*self.yscale+legrectmarginpix) + + legboxrect = QRectF(boxtopleft,legboxsize) + legbox = self.addRect(legboxrect) + + + def saveAsPng(self, fn, force=False): + """ + Save bar image as a jpg file. + + https://stackoverflow.com/questions/7451183/how-to-create-image-file\ + -from-qgraphicsscene-qgraphicsview#11642517 + + """ + if os.path.exists(fn) and not force: + raise RuntimeError(f"File {fn} already exists in filesystem.") + + # self.clearSelection() + image = QImage(*self.size, + QImage.Format_ARGB32_Premultiplied) + # image = QImage() + painter = QPainter(image) + # painter.begin() + painter.setRenderHint(QPainter.Antialiasing) + painter.setBrush(Qt.white) + painter.setPen(Qt.white) + painter.drawRect(QRect(0,0,*self.size)) + + targetrect = QRectF(0,0,*self.size) + sourcerect = QRectF(0,0,*self.size) + self.render(painter,targetrect,sourcerect) + painter.end() + # print('saving image') + image.save(fn) + + def getBarGroupMidPos(self,n): + """ + Returns the mid x position below each bar group + """ + Lx = 1-rightoffset-leftoffset + Ly = 1 - topoffset - bottomoffset + + start = .05 + S = Lx - 2*start + L = S/(self.N*self.G+dxbars*(self.N-1)) + xL = leftoffset+start + return (n*(self.G*L+dxbars*L) + xL + self.G*L/2)*self.xscale + + def getBarRect(self, n, g, yval): + Lx = 1-rightoffset-leftoffset + Ly = 1 - topoffset - bottomoffset + + start = .05 + S = Lx - 2*start + L = S/(self.N*self.G+dxbars*(self.N-1)) + xL = leftoffset+start + x = g*L + n*(self.G*L+dxbars*L) + xL + + return QRectF(x*self.xscale, + (1-bottomoffset-yval*Ly)*self.yscale, + L*self.xscale, + yval*Ly*self.yscale) + + def addLine(self, x1, y1, x2, y2, pen=QPen(), brush=QBrush()): + line = QLineF(x1*self.xscale, + (1-y1)*self.yscale, + (x2)*self.xscale, + (1-y2)*self.yscale) + return super().addLine(line, pen=pen, brush=brush) + + def createRect(self, x, y, Lx, Ly, pen=QPen(), brush=QBrush()): + """ + Create a rectangle somewhere, in relative coordinates originating + from the lower left position. + """ + x1 = x + + # Y-position from the top, these are the coordinates used to create a + # rect item. + y1 = 1-y-Ly + return QGraphicsRectItem(x1*self.xscale, + y1*self.yscale, + Lx*self.xscale, + Ly*self.yscale, + pen=pen, + brush=brush) + + @property + def xscale(self): + return self.size[0] + + @property + def yscale(self): + return self.size[1] + + def set_ydata(self, newydata): + G = len(self.bgs) + N = len(self.bgs[0]) + + assert newydata.shape[0] == N + assert newydata.shape[1] == G + + # Crop values to be between 0 and 1 + scalefac = self.ylim[1]-self.ylim[0] + yvals = np.clip(newydata, self.ylim[0], self.ylim[1])/scalefac + + for g in range(G): + color = self.colors[g] + for n in range(N): + bar = self.bgs[g][n] + bar.setRect(self.getBarRect(n, g, yvals[n, g])) + bar.setBrush(color)