Thursday, October 22, 2015

Building Better Python User Interfaces with PyQt and pyqtgraph

2 ways to build the same user interface

This post illustrates 2 ways to build the same user interface. The first is the 'introductory' level user interface where each component and functionality is in one script. This is fine for a simple user interface like this one. But as one progresses into much larger UI's with many widgets (graphics windows, dock widgets, tables, lists, etc.), the need to separate the components from the functionality becomes critical. So the second way to build the UI is constructed with this in mind.


The user interface we are going to build is a simple one with a plot of a sinc function and a slider that allows the user to change the width of the coda (and the dominant frequency).

What is the function plotted?

$$f(t) = \frac{sin(\alpha \pi t)}{\alpha \pi t}$$

The slider controls the alpha parameter. In the code, this is 'self.a'.

Method 1: a single script to build the UI:

CODE: SincFunction.py

__author__ = 'Anthony Torlucci'
__version__ = '0.0.1'

# import python standard modules
import sys

# import 3rd party libraries
from PyQt4 import QtCore, QtGui
import numpy
import pyqtgraph

#import local python

class Window(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(Window, self).__init__(parent)
        self.setWindowTitle('Simple Sinc Function UI')
        self.window = pyqtgraph.GraphicsWindow()
        self.window.setBackground('w')
        self.sinc_plot = self.window.addPlot(title='Sinc Function')
        self.sinc_plot.showGrid(x=True, y=True, alpha=0.5)
        self.setCentralWidget(self.window)
        # add a slider to change the coda of the sinc function
        self.slider = QtGui.QSlider(QtCore.Qt.Horizontal)
        # initialize the time axis (this will not change)
        self.t = numpy.linspace(-0.500, 0.500, num=1000, endpoint=True)
        # a will change the coda of the sinc function
        self.a = 1
        #
        # add the menubar with the method createMenuBar()
        self.createMenuBar()
        # add the dock widget with the method createDockWidget()
        self.createDockWidget()
        #
        # first set the default value to a
        self.slider.setValue(self.a)
        # when the slider is changed, it emits a signal and sends an integer value
        # we send that value to a method called slider value changed that updates the value a
        self.slider.valueChanged.connect(self.sliderValueChanged)
        # finally draw the curve



    def createMenuBar(self):
        # file menu actions
        exit_action = QtGui.QAction('&Exit', self)
        exit_action.triggered.connect(self.close)
        # create an instance of menu bar
        menubar = self.menuBar()
        # add file menu and file menu actions
        file_menu = menubar.addMenu('&File')
        file_menu.addAction(exit_action)

    def createDockWidget(self):
        my_dock_widget = QtGui.QDockWidget()
        my_dock_widget.setObjectName('Control Panel')
        my_dock_widget.setAllowedAreas(QtCore.Qt.TopDockWidgetArea | QtCore.Qt.BottomDockWidgetArea)
        # create a widget to house user control widgets like sliders
        my_house_widget = QtGui.QWidget()
        # every widget should have a layout, right?
        my_house_layout = QtGui.QVBoxLayout()
        # add the slider initialized in __init__() to the layout
        my_house_layout.addWidget(self.slider)
        # apply the 'house' layout to the 'house' widget
        my_house_widget.setLayout(my_house_layout)
        # set the house widget 'inside' the dock widget
        my_dock_widget.setWidget(my_house_widget)
        # now add the dock widget to the main window
        self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, my_dock_widget)

    def sliderValueChanged(self, int_value):
        self.a = int_value
        self.drawCurve()

    def drawCurve(self):
        self.sinc_plot.clear()
        self.my_sinc = numpy.sin(self.a * numpy.pi * self.t) / (self.a * numpy.pi * self.t)
        self.sinc_plot.plot(self.t, self.my_sinc, pen='b')

def main():
    app = QtGui.QApplication(sys.argv)
    app.setApplicationName('Simple Sinc Function UI')
    window = Window()
    window.show()
    app.exec_()

if __name__ == '__main__':
    main()

What does the code do?

If you ran the code above, you will see an empty plot window at first. Move the slider and the plot appears. As you move the slider to the right, the alpha parameter (self.a) increases and the function becomes narrower (an increase in the dominant frequency).

Method 2: Breaking up the UI into components / objects

First, a disclaimer. I am a self taught, intermediate level python programmer. My primary area of study is computational geophysics and I began programming to better understand the concepts. That being said, I would like to share a little bit of what I have learned building some user interfaces that illustrate basic concepts in computational geophysics.

Below is a tree structure of the code:

  • /ui
    • __init__.py
    • ui_dock_widget.py
    • ui_main_window.py
    • ui_menubar.py
  • __init__.py
  • main.py
  • main_window.py

We start with the main function. This is required for all pyqt apps. For all my simple user interfaces, I re-use this exact same code. The only thing I have to change is the application name.

CODE: main.py

__author__ = 'Anthony Torlucci'
__version__ = '0.0.1'

# import python standard modules
import sys

# import 3rd party libraries
from PyQt4.QtGui import QApplication

# import local python
from main_window import Window

def main():
    app = QApplication(sys.argv)
    app.setApplicationName('Simple Sinc Function UI')
    window = Window()
    window.show()
    app.exec_()

if __name__ == '__main__':
    main()

In the code above, we import Window from main_window. Before looking at main_window.py, lets look at the ui components which are constructed in ui_main_main_window and ui_dock_widget.

CODE: ui_main_window.py

__author__ = 'Anthony Torlucci'
__version__ = '0.0.1'

# import python standard modules


# import 3rd party libraries
from PyQt4 import QtGui, QtCore
import pyqtgraph

# import local python
from ui_menubar import Ui_Menubar

class Ui_MainWindow(QtCore.QObject):

    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        self.ui_central_widget = QtGui.QWidget(MainWindow)
        #
        self.ui_window = pyqtgraph.GraphicsWindow()
        self.ui_window.setBackground('w')
        self.ui_sinc_plot = self.ui_window.addPlot(title='Sinc Function')
        self.ui_sinc_plot.showGrid(x=True, y=True, alpha=0.5)
        # LAYOUT
        self.ui_central_layout = QtGui.QVBoxLayout()
        self.ui_central_layout.addWidget(self.ui_window)
        self.ui_central_widget.setLayout(self.ui_central_layout)
        #
        MainWindow.setCentralWidget(self.ui_central_widget)
        #
        # MENUBAR
        self.ui_menubar = Ui_Menubar()
        self.ui_menubar.setupUi(self)

The components of our user interface are a graphics window for plotting the function (from which we will use pyqtgraph), and a slider. The graphics window (self.ui_window) will be the central component (and for this simple case, the only component) of our main window. Every main window in qt has a central widget. What I have done here is create a widget, gave it a layout, and added the graphics window to that widget. If you would like to add more windows and widgets, it's easy. Just create an instance of the widget here and add it to the central widget layout. Next, we add a menubar to our main window.


CODE: ui_menubar.py

__author__ = 'Anthony Torlucci'
__version__ = '0.0.1'

# import python standard modules

# import 3rd party libraries
from PyQt4 import QtGui

# import local python

class Ui_Menubar(QtGui.QMenuBar):
    def __init__(self, parent=None):
        super(Ui_Menubar, self).__init__(parent)
        self.exit_action = QtGui.QAction(QtGui.QIcon('exit.png'), '&Exit', self)

    def setupUi(self, Ui_Menubar):
        self.ui_menubar = QtGui.QMenuBar()
        #
        # file menu actions:
        # add file menu and file menu actions
        self.file_menu = self.ui_menubar.addMenu('&File')
        self.file_menu.addAction(self.exit_action)

This menubar only has one menu 'File' and one menu action for that menu 'Exit'. The reason it is constructed this way is so I can add many more menus and actions and keep them contained in this file. We would still have to add the functionality to the main window, but that would be only a few lines of code. Next, lets look at the dock widget that holds the slider.


CODE: ui_dock_widget.py

__author__ = 'Anthony Torlucci'
__version__ = '0.0.1'

# import python standard modules

# import 3rd party libraries
from PyQt4 import QtCore, QtGui

# import local python scripts

class Ui_ControlsBoxDockWidget(QtCore.QObject):

    def setupUi(self, ControlsBox):
        ControlsBox.setObjectName('Controls Box')
        self.ui_controls_box_widget = QtGui.QDockWidget(ControlsBox)
        self.ui_controls_box_widget.setAllowedAreas(QtCore.Qt.BottomDockWidgetArea)
        #
        self.slider = QtGui.QSlider(QtCore.Qt.Horizontal)
        self.slider.setRange(1,100) # this avoids dividing by zero
        #
        self.house_layout = QtGui.QVBoxLayout()
        self.house_layout.addWidget(self.slider)
        self.house_widget = QtGui.QWidget()
        self.house_widget.setLayout(self.house_layout)
        #
        self.ui_controls_box_widget.setWidget(self.house_widget)

So I'm not sure if this is the best way to do it, but I always create a 'house' widget. This is the widget that will hold all my other widgets. The only widget here is a slider. But if I wanted to create a dial, labels, or any other widgets, I could just add them to my house widget layout. You may have to change to a grid layout and add spacers, but all that is applied and contained within the house widget. Then just set the dock widget's widget to house widget.

Now for the meat and potatoes. We give our widgets functionality in main_window.py. That's it. Notice we create all of our main window components in 2 lines: self.ui = Ui_MainWindow() and self.ui.setupUi(self). Same for the menubar and the dock widget. I think everything else is self explanatory.

CODE: main_window.py

__author__ = 'Anthony Torlucci'
__version__ = '0.0.1'

# import python standard modules


# import 3rd party libraries
from PyQt4 import QtGui, QtCore
import numpy
import pyqtgraph

# import local python
from ui.ui_main_window import Ui_MainWindow
from ui.ui_dock_widget import Ui_ControlsBoxDockWidget

class Window(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(Window, self).__init__(parent)
        self.setWindowTitle('Simple Sinc Function UI')
        #
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        #
        self.mbar = self.setMenuBar(self.ui.ui_menubar.ui_menubar)
        self.ui.ui_menubar.exit_action.triggered.connect(self.close)
        #
        self.dck_widget = Ui_ControlsBoxDockWidget()
        self.dck_widget.setupUi(self)
        self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.dck_widget.ui_controls_box_widget)
        #
        # DATA
        # initialize the time axis (this will not change)
        self.t = numpy.linspace(-0.500, 0.500, num=1000, endpoint=True)
        # a will change the coda of the sinc function
        self.a = 1
        #
        # CONTROLS
        # first set the default value to a
        self.dck_widget.slider.setValue(self.a)
        # when the slider is changed, it emits a signal and sends an integer value
        # we send that value to a method called slider value changed that updates the value a
        self.dck_widget.slider.valueChanged.connect(self.sliderValueChanged)
        # finally draw the curve
        self.drawCurve()

    def sliderValueChanged(self, int_value):
        self.a = int_value
        self.drawCurve()

    def drawCurve(self):
        self.ui.ui_sinc_plot.clear()
        self.my_sinc = numpy.sin(self.a * numpy.pi * self.t) / (self.a * numpy.pi * self.t)
        self.ui.ui_sinc_plot.plot(self.t, self.my_sinc, pen='b')

I hope this helps some of you learning to develop with python, pyqt, and pyqtgraph to build your own user interfaces to explore computational science. Good luck!

Syntax Highlighter Test

Testing Python Syntax Highlighter.

Here is an example of a Ricker Wavelet function.


CODE:

__author__ = 'Anthony Torlucci'
__version__ = '0.0.1'

# REFERENCE: http://wiki.seg.org/wiki/Dictionary:Ricker_wavelet

from numpy import pi, exp

def segRicker(f, t):
    '''
    Simple Ricker Wavelet
    :param f: frequency (float)
    :param t: time (1D numpy array)
    :return: ricker wavelet, length = len(t)
    '''
    r = (1 - 2*pi**2 * f**2 * t**2) * exp(-pi**2 * f**2 * t**2)
    return r

Useful Links:

Sunday, July 19, 2015

Interactive Cross Plots

Another simple user interface: SimpleLasXPlot!

This cross plotting application is an extension of the SimpleLasCurveViewer which adds a cross plot. The data being plotted is only the region highlighted. You can resize this region and slide the region up or down log interactive changing the data being plotted in the cross plot space. I hope this will help those having trouble visualizing cross plots. As usual, I have included a few screen caps below.


Code can be found at my github page.

Sunday, July 12, 2015

SimpleLasCurveViewer Updated

I have added crosshairs and another dock widget to the viewer that shows the value of the depth and the curve for which you are hovering over in the zoomed plot on the left. Take a look at the screen shot below.

The code can be found at my github page under simpleUIs/simpleLasCurveViewer.

Tuesday, July 7, 2015

Simple Sum 2 Cosines UI

Another user interface added to github very similar to SimpleCosineWaveUI, except there are now 2 cosine plots and a third plot that is the sum of the first two.

Below are some images.

Saturday, July 4, 2015

Problems & Solutions: Partial Differentiation 001

The first example is from: Widder, David V., "Advanced Calculus", Dover Books on Mathematics

Prerequisites: single-variable calculus, specifically u-substitution

Example 1:

Find $$\frac{\partial }{\partial x}\frac{sin(xy)}{cos(x+y)}$$

Solution:

We will need the quotient rule: $$\left ( \frac{f}{g} \right )' = \frac{f(x)g'(x) - f'(x)g(x)}{[g(x)]^{2}}$$

Setting $$f(x) = sin(xy)$$ and $$g(x) = cos(x+y)$$ we will need to find the derivatives of each.

Using the rule: $$\frac{\mathrm{d} }{\mathrm{d} x}sin(u(x)) = cos(u(x))\frac{\mathrm{d} u}{\mathrm{d} x}$$ $$f'(x) = cos(xy)\frac{\partial }{\partial x}(xy)$$ $$f'(x) = ycos(xy)$$

And conversely, setting $$g(x) = cos(x+y)$$

Using the rule: $$\frac{\mathrm{d} }{\mathrm{d} x}cos(u(x)) = -sin(u(x))\frac{\mathrm{d} u}{\mathrm{d} x}$$ $$g'(x) = -sin(x+y)\frac{\partial }{\partial x}(x+y)$$ $$g'(x) = -sin(x+y)(1+0)$$ $$g'(x) = -sin(x+y)$$

Putting the parts together: $$\frac{\partial }{\partial x}\frac{sin(xy)}{cos(x+y)} = \frac{sin(xy)(-sin(x+y))-(ycos(xy)cos(x+y))}{[cos(x+y)]^{2}}$$


This is usually sufficient for most professors. It is assumed that if you can get this far, you can simplify.

Problems & Solutions: About Page

Problems & Solutions is a series of posts about just that: problems and solutions in mathematics, physics, geophysics, and anything else I feel strongly needs writing about. My experience as an undergraduate was there were never enough examples to fully illustrate the concepts of [insert course name here]. My intention is to provide examples with clear explicit solutions and incorporate numerical methods with analytic solutions.

Simple Cosine Wave UI: illustrating frequency and phase

A new simple user interface has been added to github: SimpleCosineWaveUI! It's not terribly advanced, but it does illustrate two fundamental concepts in exploration geophysics: frequency and phase. The UI graphs a plot of: $$ f(t) = cos(2\pi ft+\phi ) $$ The two sliders at the bottom of the page allow the user to change the frequency and the phase of the signal and interactively view how those changes affect the plot. A "RESET" button is added that sets the phase and frequency of the signal back to zero. Finally, if you would like to see the waves with the area under of the curve filled in, click the check box "add fill". It does make the plot visually more interesting and allows the user to see more in my opinion.

A few of the details for those not interested in scanning through the code:

The minimum time and maximum time of the plot is set to zero and one second, respectively. The interval between samples is 0.001 seconds or one millisecond. Many of you will immediately jump to the fact that that means the sampling frequency (or sampling rate) is 1000 Hz (Fs = 1/dt), and thus the Nyquist frequency is 500 Hz. Although, technically you could reconstruct any frequency (NOT THE SIGNAL ITSELF NECESSARILY, a discussion tabled for another UI) up to Nyquist frequency, I have limited the frequency range from 0 to 100 Hz.

There are 2 interesting things that will notice after playing with the UI:(1) a cosine wave with zero frequency is a linear function with zero slope. In other words a constant: $ f(t) = C $. (2) an increase in phase (moving the phase slider to the right) causes the function to appear to move to the left. The function has undergone a negative translation or time delay. Both of these phenomena will be discussed in a later post and accompanying UI or IPython notebook. For now, it is important to recognize them.

Below are some screenshots. I hope you enjoy this tool and maybe even learn something.

Wednesday, June 17, 2015

Daily Dose of Exploration Geophysics: Gardners Expression as a Linear Regression

Gardner's Coefficients Revealed!


I have a added a new IPython notebook to my github account that shows how you can find the coefficients in Gardner's expression for the basin you happen to be working in. follow the link to myIPythonNotebooks - Gardner's Expression Regressed. For some reason, github is not showing the mathjax equations well, but if you download the notebook and open it locally, you should have no problem viewing them. I'll write them here just in case to give you a sample of what can be found in the notebook:


********************************************************************************************************************* One option is to linearize the equation and do a regression for the 2 parameters. This ipython notebook will show you how. Start with the general equation: $$\rho = \alpha * V_{P}^\beta$$ Take the natural log of both sides: $$ln(\rho) = ln(\alpha * V_{P}^\beta)$$ Use the properties of logarithms to make a linear equation: $$ln\rho = ln\alpha + ln(V_{P}^\beta)$$ $$ln\rho = ln\alpha + \beta*lnV_{P}$$ Rewrite: $$D = A + BV$$ where A is the intercept, B is the slope, V is the independent variable, and D is the dependent variable. *********************************************************************************************************************


Finally, below is a screen shot of the final regression:


GREAT LINK to SUBSURF WIKI: Gardner's equation

Tuesday, June 2, 2015

The best method I have found for setting up my python environment

I am running Scientific Linux 6 which comes with python version 2.6.6.  However, I really like using version 2.7.9 mainly because of all the compatible third party libraries associated with it.  Also, I prefer to use PyCharm - my favorite python IDE.

I have found that anaconda by Continuum Analytics to be the best python package manager and, after surfing the web, it seems I am not alone.  Using 'conda' to add additional packages is simple and for those new to python programming and third party modules or libraries, conda is well documented.  Also, conda installs to its own directory, so it does not interfere with your package python interpreter.


Once you have conda installed, install PyCharm.  PyCharm does not include a python interpreter (which is why you need conda or you can use the system default).  When you create a new project, tell PyCharm you want to use the anaconda interpreter (~/anaconda/bin/python).


That's it.  Post any questions to the comment portion and I will do my best to answer them if I can.

Monday, June 1, 2015

First Github Contribution: SimpleLasCurveViewer

Simple las Curve Viewer

Check out my first contribution to the open source community. It won't revolutionize the industry, but I think its a good start. It's a little program called 'Simple las Curve Viewer' and it's exactly that. The GUI is written with PyQt4 developed by Riverbank and it uses the plotting library pyqtgraph.

I've built and tested it using the L-30.las file from the Penobscot survey. You can find it at the SEG Github page under tutorials/1406_Make_a_synthetic.

The code can be found at github.

Thank you Warren Weckesser for allowing others to use your las python script and Luke Campagnola for developing pyqtgraph.

Below are some screenshots of the application.

First when the application opens:

After importing the las: (Notice that the curve list is populated with all the names of the curves in the las file)

A gamma ray log:

A resistivity log:

Saturday, April 18, 2015

I'm Back!!!

After a long time of not blogging, I'm back.  The blog title has changed from 'MathCompGeoPhys' to just 'CompGeoPhys', but the content will be the same.

Here's what you can expect to see in the future:

  • Solved problems in advanced calculus and differential equations
  • cool python (and C) code for exploring and visualizing the world of computational geophysics
  • and other cool things related to my favorite subjects: mathematics and computational geophysics!

Thanks for reading.  Stay tuned...