Debugging¶
Aims:¶
Understand the importance of debugging
Learn some debugging techniques
Learn the basics of
pdb
Software prerequisites¶
Contact details:
Dr. Rhodri Nelson
Room 4.85 RSM building
email: rnelson@ic.ac.uk
Teams: @Nelson, Rhodri B in #ACSE1 or #General, or DM me.
Notes¶
Much of the material presented in today’s lecture has been adapted from
SciPy
’s debugging lecture found here.Scripts for todays lecture can be found in the
files
folder withinLecture10
.
When working on technical software development projects it’s often the case that you’ll spend as much (if not more) time debugging, compared to writing, ‘actual’ code. This statement alone should make one thing abundantly clear, debugging is important and in order to increase your productivity you need to get good at it.
Over the course of time as you develop/encounter code with bugs, fix them and repeat this process countless times these are skills you will be ‘forced’ to acquire naturally. But as with everything, when starting out it doesn’t hurt to get a few tips from sources who have spent too much time searching for that elusive typo in the middle of a million lines of code. Thus, this lecture aims to provide you with a few tips and tricks to help you on your journey to becoming a debugging guru.
Learn what works in what language¶
This lecture focuses on debugging in Python. While the general skill set is transferable to any coding language, some of the tools are not. When working in an unfamiliar language it is therefore important to quickly find the right tools to suit your debugging style - for example we’ll first look at a couple of manual debugging strategies utilizing print
statements and IPython
, how would we transfer similar techniques to c++
?
Avoiding bugs¶
We all, even the great Fabio Luporini, write buggy code.
Write your code with testing and debugging in mind.
Keep It Simple, Stupid (KISS):
What is the simplest thing that could possibly work?
Don’t Repeat Yourself (DRY):
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
Constants, algorithms, etc.
Try to limit interdependencies of your code. (Loose Coupling).
Give your variables, functions and modules meaningful names (not cryptic/mathematics names).
There have been some infamous ‘bugs’ throughout history, here are a few:
The world’s first computer bug?¶
On September 9th, 1947 at 3:45 p.m., Grace Murray Hopper logged in her log book the first computer bug! Her log read: “First actual case of bug being found”. The term “bug” in computer science is now, of course, not taken literally. It is used to describe a flaw or failure in a computer program that causes it to produce an unexpected result or crash.
The Y2K bug¶
The year 2000 problem spawned fears of worldwide economic collapse, nuclear war, and led to an industry of consultants providing last-minute fixes.
Scary stuff, but clearly a bit over exaggerated. Nevertheless, the bug was real and billions of dollars were spend world wide fixing the bug. Indeed, not all systems were fixed in time and in Spain some parking meters failed, the French meteorological institute published the weather for 19100 and in Australia some ticket validation machines failed.
The Dhahran Missile¶
But on a more serious note, a bug in the software of an MIM-104 Patriot caused its system clock to drift by one third of a second over a period of one hundred hours - resulting in failure to locate and intercept an incoming Iraqi Al Hussein missile, which then struck Dharan barracks, Saudi Arabia (February 25, 1991), killing 28 Americans.
And here are some of the more ‘interesting’ bugs you may encounter while developing software:
The Heisenbug¶
From Wikipedia:
In computer programming jargon, a heisenbug is a software bug that seems to disappear or alter its behavior when one attempts to study it. The term is a pun on the name of Werner Heisenberg, the physicist who first asserted the observer effect of quantum mechanics, which states that the act of observing a system inevitably alters its state. In electronics the traditional term is probe effect, where attaching a test probe to a device changes its behavior.
Mandelbug¶
Mandelbugs do not change their properties or vanish like Heisenbugs. Instead, they are so unusual and complex that there is typically no practical solution to fix them. In fact, Mandelbugs may be so nondeterministic that many scholarly developers believe that fractal mathematics inventor Benoit Mandelbrot developed the Mandelbug to drive developers insane.
Bohrbug¶
A software bug which manifests reliably under a well-defined, but possibly unknown, set of conditions.
Schroedinbug¶
A software bug which manifests only when somebody debugging it finds out that it shouldn’t work at all.
pyflakes: fast static analysis¶
They are several static analysis tools in Python (most of which you’re already familiar with); to name a few:
The simplest of these tools is probably pyflakes
:
Fast, simple
Detects syntax errors, missing imports, typos on names.
Another good recommendation is the flake8
tool which is a combination of pyflakes
and pep8
. Thus, in addition to the types of errors that pyflakes
catches, flake8
detects violations of the recommendation in PEP8
style guide.
Integrating pyflakes
(or flake8
) in your editor or IDE is highly recommended, it does yield productivity gains.
Debugging workflow¶
If you do have a non trivial bug, this is when debugging strategies kick in. There is no silver bullet. Yet, strategies help:
For debugging a given problem, the favorable situation is when the problem is isolated in a small number of lines of code, outside framework or application code, with short modify-run-fail cycles
Make it fail reliably. Find a test case that makes the code fail every time.
Divide and Conquer. Once you have a failing test case, isolate the failing code.
Which module.
Which function.
Which line of code.
=> isolate a small reproducible failure: a test case/minimum failing example
Change one thing at a time and re-run the failing test case.
Use the debugger to understand what is going wrong.
Take notes and be patient. It may take a while.
Note: Once you have gone through this process: isolated a tight piece of code reproducing the bug and fix the bug using this piece of code, add the corresponding code to your test suite.
Minimum failing examples¶
Given the occurrence of a particular bug, a minimum failing example (MFE) is the simplest piece of code that can reproduce an instance of the particular bug in question (which was probably discovered through building a larger more complex script).
When approaching someone for help debugging a problem, it is considered good ‘manners’ to provide them with an MFE. This will often drastically reduce the time taken for them to debug the problem. Further, when contributing to an open source project and reporting a problem (e.g. opening a bug report issue on Github), the developers will frequently ask ‘Please could you provide an MFE’.
Example:¶
This is a ‘real’ example. (Don’t worry about the details, this is just to illustrate and idea). I was once sent this long bit of code:
import numpy as np
import matplotlib.pyplot as plot
import math as mt
import matplotlib.ticker as mticker
from mpl_toolkits.axes_grid1 import make_axes_locatable
from matplotlib import cm
from scipy.interpolate import CubicSpline
from scipy.interpolate import interp1d
from devito import *
from examples.seismic import TimeAxis
from examples.seismic import RickerSource
from examples.seismic import Receiver
nptx = 861
nptz = 311
x0 = 13650.
x1 = 56650.
compx = x1-x0
z0 = 0.
z1 = 9920.
compz = z1-z0;
hxv = (x1-x0)/(nptx-1)
hzv = (z1-z0)/(nptz-1)
npmlx = 30
npmlz = 31
lx = npmlx*hxv
lz = npmlz*hzv
origin = (x0,z0)
extent = (compx,compz)
shape = (nptx,nptz)
spacing = (hxv,hzv)
class d0domain(SubDomain):
name = 'd0'
def define(self, dimensions):
x, z = dimensions
return {x: ('middle', npmlx, npmlx), z: ('middle', 0, npmlz)}
d0_domain = d0domain()
class d1domain(SubDomain):
name = 'd1'
def define(self, dimensions):
x, z = dimensions
return {x: ('left',npmlx), z: z}
d1_domain = d1domain()
class d2domain(SubDomain):
name = 'd2'
def define(self, dimensions):
x, z = dimensions
return {x: ('right',npmlx), z: z}
d2_domain = d2domain()
class d3domain(SubDomain):
name = 'd3'
def define(self, dimensions):
x, z = dimensions
return {x: ('middle', npmlx, npmlx), z: ('right',nptz)}
d3_domain = d3domain()
grid = Grid(origin=origin,extent=extent,shape=shape,subdomains=(d0_domain,d1_domain,d2_domain,d3_domain),dtype=np.float64)
nptxvel = 1407
nptzvel = 311
x0vel = 0.
x1vel = 70300.
z0vel = 0.
z1vel = 9920.
hxvel = (x1vel-x0vel)/(nptxvel-1)
hzvel = (z1vel-z0vel)/(nptzvel-1)
Xvel = np.linspace(x0vel,x1vel,nptxvel)
Zvel = np.linspace(z0vel,z1vel,nptzvel)
velarq = np.fromfile('velocity_data/profilevel.bin',dtype='float32')
velarq = velarq.reshape((nptxvel,nptzvel))
fscale = 10**(-3)
velarq = fscale*velarq
X0 = np.linspace(x0,x1,nptx)
Z0 = np.linspace(z0,z1,nptz)
X1 = np.linspace((x0+0.5*hxv),(x1-0.5*hxv),nptx-1)
Z1 = np.linspace((z0+0.5*hzv),(z1-0.5*hzv),nptz-1)
v0 = np.zeros((nptx,nptz))
v1 = np.zeros((nptx-1,nptz-1))
C0x = np.zeros((nptx,nptzvel))
for j in range(nptzvel):
x = Xvel
z = velarq[0:nptxvel,j]
#cs = CubicSpline(x,z)
cs = interp1d(x,z,kind='linear')
xs = X0
C0x[0:nptx,j] = cs(xs)
for i in range(nptx):
x = Zvel
z = C0x[i,0:nptzvel]
#cs = CubicSpline(x,z)
cs = interp1d(x,z,kind='linear')
xs = Z0
v0[i,0:nptz] = cs(xs)
C11x = np.zeros((nptx-1,nptzvel))
for j in range(nptzvel):
x = Xvel
z = velarq[0:nptxvel,j]
cs = CubicSpline(x,z)
xs = X1
C11x[0:nptx-1,j] = cs(xs)
for i in range(nptx-1):
x = Zvel
z = C11x[i,0:nptzvel]
#cs = CubicSpline(x,z)
cs = interp1d(x,z,kind='linear')
xs = Z1
v1[i,0:nptz-1] = cs(xs)
t0 = 0.
tn = 20000.
CFL = 0.4
vmax = np.amax(v0)
dtmax = np.float64((min(hxv,hzv)*CFL)/(vmax))
ntmax = int((tn-t0)/dtmax)+1
dt0 = np.float64((tn-t0)/ntmax)
time_range = TimeAxis(start=t0,stop=tn,num=ntmax+1)
nt = time_range.num - 1
(hx,hz) = grid.spacing_map
(x, z) = grid.dimensions
t = grid.stepping_dim
dt = grid.stepping_dim.spacing
f0 = 0.005
nfonte = 1
xposf = 35150.
zposf = 32.
src = RickerSource(name='src',grid=grid,f0=f0,npoint=nfonte,time_range=time_range,staggered=NODE,dtype=np.float64)
src.coordinates.data[:, 0] = xposf
src.coordinates.data[:, 1] = zposf
nrec = nptx
nxpos = np.linspace(x0,x1,nrec)
nzpos = 32.
rec = Receiver(name='rec',grid=grid,npoint=nrec,time_range=time_range,staggered=NODE,dtype=np.float64)
rec.coordinates.data[:, 0] = nxpos
rec.coordinates.data[:, 1] = nzpos
u = TimeFunction(name="u",grid=grid,time_order=2,space_order=2,staggered=NODE,dtype=np.float64)
phi1 = TimeFunction(name="phi1",grid=grid,time_order=2,space_order=2,staggered=(x,z),dtype=np.float64)
phi2 = TimeFunction(name="phi2",grid=grid,time_order=2,space_order=2,staggered=(x,z),dtype=np.float64)
vel0 = Function(name="vel0",grid=grid,space_order=2,staggered=NODE,dtype=np.float64)
vel0.data[:,:] = v0[:,:]
vel1 = Function(name="vel1", grid=grid,space_order=2,staggered=(x,z),dtype=np.float64)
vel1.data[0:nptx-1,0:nptz-1] = v1
vel1.data[nptx-1,0:nptz-1] = vel1.data[nptx-2,0:nptz-1]
vel1.data[0:nptx,nptz-1] = vel1.data[0:nptx,nptz-2]
src_term = src.inject(field=u.forward,expr=src*dt**2*vel0**2)
rec_term = rec.interpolate(expr=u)
x0pml = x0 + npmlx*hxv
x1pml = x1 - npmlx*hxv
z0pml = z0
z1pml = z1 - npmlz*hzv
def fdamp(x,z,i):
quibar = 0.05
if(i==1):
a = np.where(x<=x0pml,(np.abs(x-x0pml)/lx),np.where(x>=x1pml,(np.abs(x-x1pml)/lx),0.))
fdamp = quibar*(a-(1./(2.*np.pi))*np.sin(2.*np.pi*a))
if(i==2):
a = np.where(z<=z0pml,(np.abs(z-z0pml)/lz),np.where(z>=z1pml,(np.abs(z-z1pml)/lz),0.))
fdamp = quibar*(a-(1./(2.*np.pi))*np.sin(2.*np.pi*a))
return fdamp
def generatemdamp():
X0 = np.linspace(x0,x1,nptx)
Z0 = np.linspace(z0,z1,nptz)
X0grid,Z0grid = np.meshgrid(X0,Z0)
X1 = np.linspace((x0+0.5*hxv),(x1-0.5*hxv),nptx-1)
Z1 = np.linspace((z0+0.5*hzv),(z1-0.5*hzv),nptz-1)
X1grid,Z1grid = np.meshgrid(X1,Z1)
D01 = np.zeros((nptx,nptz))
D02 = np.zeros((nptx,nptz))
D11 = np.zeros((nptx,nptz))
D12 = np.zeros((nptx,nptz))
D01 = np.transpose(fdamp(X0grid,Z0grid,1))
D02 = np.transpose(fdamp(X0grid,Z0grid,2))
D11 = np.transpose(fdamp(X1grid,Z1grid,1))
D12 = np.transpose(fdamp(X1grid,Z1grid,2))
return D01, D02, D11, D12
D01, D02, D11, D12 = generatemdamp();
dampx0 = Function(name="dampx0", grid=grid,space_order=2,staggered=NODE ,dtype=np.float64)
dampz0 = Function(name="dampz0", grid=grid,space_order=2,staggered=NODE ,dtype=np.float64)
dampx0.data[:,:] = D01
dampz0.data[:,:] = D02
dampx1 = Function(name="dampx1", grid=grid,space_order=2,staggered=(x,z),dtype=np.float64)
dampz1 = Function(name="dampz1", grid=grid,space_order=2,staggered=(x,z),dtype=np.float64)
dampx1.data[0:nptx-1,0:nptz-1] = D11
dampz1.data[0:nptx-1,0:nptz-1] = D12
dampx1.data[nptx-1,0:nptz-1] = dampx1.data[nptx-2,0:nptz-1]
dampx1.data[0:nptx,nptz-1] = dampx1.data[0:nptx,nptz-2]
dampz1.data[nptx-1,0:nptz-1] = dampz1.data[nptx-2,0:nptz-1]
dampz1.data[0:nptx,nptz-1] = dampz1.data[0:nptx,nptz-2]
# White Region
pde01 = Eq(u.dt2-u.laplace*vel0*vel0)
# Blue Region
pde02a = u.dt2 + (dampx0+dampz0)*u.dtc + (dampx0*dampz0)*u - u.laplace*vel0*vel0
pde02b = - (0.5/hx)*(phi1[t,x,z-1]+phi1[t,x,z]-phi1[t,x-1,z-1]-phi1[t,x-1,z])
pde02c = - (0.5/hz)*(phi2[t,x-1,z]+phi2[t,x,z]-phi2[t,x-1,z-1]-phi2[t,x,z-1])
pde02 = Eq(pde02a + pde02b + pde02c)
pde10 = phi1.dt + dampx1*0.5*(phi1.forward+phi1)
a1 = u[t+1,x+1,z] + u[t+1,x+1,z+1] - u[t+1,x,z] - u[t+1,x,z+1]
a2 = u[t,x+1,z] + u[t,x+1,z+1] - u[t,x,z] - u[t,x,z+1]
pde11 = -(dampz1-dampx1)*0.5*(0.5/hx)*(a1+a2)*vel1*vel1
pde1 = Eq(pde10+pde11)
pde20 = phi2.dt + dampz1*0.5*(phi2.forward+phi2)
b1 = u[t+1,x,z+1] + u[t+1,x+1,z+1] - u[t+1,x,z] - u[t+1,x+1,z]
b2 = u[t,x,z+1] + u[t,x+1,z+1] - u[t,x,z] - u[t,x+1,z]
pde21 = -(dampx1-dampz1)*0.5*(0.5/hz)*(b1+b2)*vel1*vel1
pde2 = Eq(pde20+pde21)
stencil01 = Eq(u.forward,solve(pde01,u.forward) ,subdomain = grid.subdomains['d0'])
subds = ['d1','d2','d3']
stencil02 = [Eq(u.forward,solve(pde02, u.forward),subdomain = grid.subdomains[subds[i]]) for i in range(0,len(subds))]
stencil1 = [Eq(phi1.forward, solve(pde1,phi1.forward),subdomain = grid.subdomains[subds[i]]) for i in range(0,len(subds))]
stencil2 = [Eq(phi2.forward, solve(pde2,phi2.forward),subdomain = grid.subdomains[subds[i]]) for i in range(0,len(subds))]
bc = [Eq(u[t+1,0,z],0.),Eq(u[t+1,nptx-1,z],0.),Eq(u[t+1,x,nptz-1],0.),Eq(u[t+1,x,0],u[t+1,x,1])]
op = Operator([stencil01,stencil02] + src_term + bc + [stencil1,stencil2] + rec_term,subs=grid.spacing_map)
u.data[:] = 0.
phi1.data[:] = 0.
phi2.data[:] = 0.
op(time=nt,dt=dt0)
def graph2d(U):
plot.figure()
fscale = 10**(-3)
scale = np.amax(U[npmlx:-npmlx,0:-npmlz])/50.
extent = [fscale*x0pml,fscale*x1pml,fscale*z1pml,fscale*z0pml]
fig = plot.imshow(np.transpose(U[npmlx:-npmlx,0:-npmlz]),vmin=-scale, vmax=scale, cmap=cm.gray, extent=extent)
plot.gca().xaxis.set_major_formatter(mticker.FormatStrFormatter('%d km'))
plot.gca().yaxis.set_major_formatter(mticker.FormatStrFormatter('%d km'))
plot.axis('equal')
plot.title('Map - Acoustic Problem with PML - Devito')
plot.grid()
ax = plot.gca()
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.05)
cbar = plot.colorbar(fig, cax=cax, format='%.2e')
cbar.set_label('Displacement [km]')
plot.draw()
plot.savefig('acoustic_map_pml_devito.png',dpi=100)
plot.show()
graph2d(u.data[0,:,:])
def graph2drec(rec):
fig1 = plot.figure()
fscale = 10**(-3)
scale = np.amax(rec[:,npmlx:-npmlx])/50.
extent = [fscale*x0pml,fscale*x1pml, fscale*tn, fscale*t0]
fig = plot.imshow(rec[:,npmlx:-npmlx], vmin=-scale, vmax=scale, cmap=cm.gray, extent=extent)
plot.gca().xaxis.set_major_formatter(mticker.FormatStrFormatter('%d km'))
plot.gca().yaxis.set_major_formatter(mticker.FormatStrFormatter('%d s'))
plot.axis('equal')
plot.title('Receivers Signal Profile with PML - Devito')
ax = plot.gca()
divider = make_axes_locatable(ax)
plot.savefig('rec_map_pml_devito.png')
plot.show()
graph2drec(rec.data)
which was now producing an incorrect result, but a couple of software versions ago the result was correct.
The above piece of code is long and the offender has not been pin-pointed, thus making it difficult to deduce where the bug is coming from. After some work, I managed to produce an MFE:
import numpy as np
from devito import *
class d0domain(SubDomain):
name = 'd0'
def define(self, dimensions):
x, y = dimensions
return {x: ('middle', 2, 2), y: ('right', 10)}
sdom = d0domain()
grid = Grid(shape=(10, 10), extent=(10, 10), subdomains=(sdom))
u = Function(name='u', grid=grid)
eq = Eq(u, u+1, subdomain = grid.subdomains['d0'])
op = Operator(eq)
op.apply()
This piece of code is short and simple an allowed the bug to be tracked down and fixed in less than an hour!
Manual debugging¶
Here are a couple of ‘manual’ debugging techniques:
Print statements Adding print statements to your code can often be a quick and effective method of pin-pointing problematic lines of code, e.g.,
...
x = some_function(inputs)
print(x.proprty1, x.property2)
...
from IPython import embed; embed()
When possible, utilizing this command can be an incredibly useful tool for debugging sections of code:
...
x = some_function(inputs)
from IPython import embed; embed()
...
I’ll now show you a live example!
Using the Python debugger¶
The python debugger, pdb: https://docs.python.org/library/pdb.html, allows you to inspect your code interactively.
Specifically it allows you to:
View the source code.
Walk up and down the call stack.
Inspect values of variables.
Modify values of variables.
Set breakpoints.
Invoking the debugger¶
Ways to launch the debugger:
Postmortem, launch debugger after module errors.
Launch the module with the debugger.
Call the debugger inside the module
Postmortem¶
Example 1: You’re working in IPython
and you get a traceback.
Here we debug the file index_error.py
. When running it, an IndexError
is raised. Type %debug
and drop into the debugger.
In [1]: %run index_error.py --------------------------------------------------------------------------- IndexError Traceback (most recent call last) /home/varoquau/dev/scipy-lecture-notes/advanced/debugging/index_error.py in <module>() 6 7 if __name__ == '__main__': ----> 8 index_error() 9 /home/varoquau/dev/scipy-lecture-notes/advanced/debugging/index_error.py in index_error() 3 def index_error(): 4 lst = list('foobar') ----> 5 print lst[len(lst)] 6 7 if __name__ == '__main__': IndexError: list index out of range In [2]: %debug > /home/varoquau/dev/scipy-lecture-notes/advanced/debugging/index_error.py(5)index_error() 4 lst = list('foobar') ----> 5 print lst[len(lst)] 6 ipdb> list 1 """Small snippet to raise an IndexError.""" 2 3 def index_error(): 4 lst = list('foobar') ----> 5 print lst[len(lst)] 6 7 if __name__ == '__main__': 8 index_error() 9 ipdb> len(lst) 6 ipdb> print(lst[len(lst)-1]) r ipdb> quit In [3]:
Example 2: Post-mortem debugging without IPython
In some situations you cannot use IPython, for instance to debug a script that wants to be called from the command line. In this case, you can call the script with python -m pdb script.py
:
$ python -m pdb index_error.py
> /home/varoquau/dev/scipy-lecture-notes/advanced/optimizing/index_error.py(1)<module>()
-> """Small snippet to raise an IndexError."""
(Pdb) continue
Traceback (most recent call last):
File "/usr/lib/python2.6/pdb.py", line 1296, in main
pdb._runscript(mainpyfile)
File "/usr/lib/python2.6/pdb.py", line 1215, in _runscript
self.run(statement)
File "/usr/lib/python2.6/bdb.py", line 372, in run
exec cmd in globals, locals
File "<string>", line 1, in <module>
File "index_error.py", line 8, in <module>
index_error()
File "index_error.py", line 5, in index_error
print lst[len(lst)]
IndexError: list index out of range
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> /home/varoquau/dev/scipy-lecture-notes/advanced/optimizing/index_error.py(5)index_error()
-> print(lst[len(lst)])
(Pdb)
Step-by-step execution¶
Example: You believe a bug exists in a module but are not sure where.
For instance we are trying to debug wiener_filtering.py
. Indeed the code runs, but the filtering does not work well.
Run the script in
IPython
with the debugger using%run -d wiener_filtering.py
:
In [1]: %run -d wiener_filtering.py *** Blank or comment *** Blank or comment *** Blank or comment Breakpoint 1 at /home/varoquau/dev/scipy-lecture-notes/advanced/optimizing/wiener_filtering.py:4 NOTE: Enter 'c' at the ipdb> prompt to start your script. > <string>(1)<module>()
Set a break point at line 34 using
b 34
:
ipdb> n
> /home/varoquau/dev/scipy-lecture-notes/advanced/optimizing/wiener_filtering.py(4)<module>()
3
1---> 4 import numpy as np
5 import scipy as sp
ipdb> b 34
Breakpoint 2 at /home/varoquau/dev/scipy-lecture-notes/advanced/optimizing/wiener_filtering.py:34
Continue execution to next breakpoint with
c(ont(inue))
:
ipdb> c
> /home/varoquau/dev/scipy-lecture-notes/advanced/optimizing/wiener_filtering.py(34)iterated_wiener()
33 """
2--> 34 noisy_img = noisy_img
35 denoised_img = local_mean(noisy_img, size=size)
Step into code with
n(ext)
ands(tep)
: next jumps to the next statement in the current execution context, while step will go across execution contexts, i.e. enable exploring inside function calls:
ipdb> s
> /home/varoquau/dev/scipy-lecture-notes/advanced/optimizing/wiener_filtering.py(35)iterated_wiener()
2 34 noisy_img = noisy_img
---> 35 denoised_img = local_mean(noisy_img, size=size)
36 l_var = local_var(noisy_img, size=size)
ipdb> n
> /home/varoquau/dev/scipy-lecture-notes/advanced/optimizing/wiener_filtering.py(36)iterated_wiener()
35 denoised_img = local_mean(noisy_img, size=size)
---> 36 l_var = local_var(noisy_img, size=size)
37 for i in range(3):
Step a few lines and explore the local variables:
ipdb> n
> /home/varoquau/dev/scipy-lecture-notes/advanced/optimizing/wiener_filtering.py(37)iterated_wiener()
36 l_var = local_var(noisy_img, size=size)
---> 37 for i in range(3):
38 res = noisy_img - denoised_img
ipdb> print(l_var)
[[5868 5379 5316 ..., 5071 4799 5149]
[5013 363 437 ..., 346 262 4355]
[5379 410 344 ..., 392 604 3377]
...,
[ 435 362 308 ..., 275 198 1632]
[ 548 392 290 ..., 248 263 1653]
[ 466 789 736 ..., 1835 1725 1940]]
ipdb> print(l_var.min())
0
Oh dear, nothing but integers, and 0
variation. Here is our bug, we are doing integer arithmetic.
When we run the wiener_filtering.py
file, the following warnings are raised:
In [2]: %run wiener_filtering.py
wiener_filtering.py:40: RuntimeWarning: divide by zero encountered in divide
noise_level = (1 - noise/l_var )
We can turn these warnings into exceptions, which enables us to do post-mortem debugging on them, and find our problem quicker:
In [3]: np.seterr(all='raise')
Out[3]: {'divide': 'print', 'invalid': 'print', 'over': 'print', 'under': 'ignore'}
In [4]: %run wiener_filtering.py
---------------------------------------------------------------------------
FloatingPointError Traceback (most recent call last)
/home/esc/anaconda/lib/python2.7/site-packages/IPython/utils/py3compat.pyc in execfile(fname, *where)
176 else:
177 filename = fname
--> 178 __builtin__.execfile(filename, *where)
/home/esc/physique-cuso-python-2013/scipy-lecture-notes/advanced/debugging/wiener_filtering.py in <module>()
55 pl.matshow(noisy_face[cut], cmap=pl.cm.gray)
56
---> 57 denoised_face = iterated_wiener(noisy_face)
58 pl.matshow(denoised_face[cut], cmap=pl.cm.gray)
59
/home/esc/physique-cuso-python-2013/scipy-lecture-notes/advanced/debugging/wiener_filtering.py in iterated_wiener(noisy_img, size)
38 res = noisy_img - denoised_img
39 noise = (res**2).sum()/res.size
---> 40 noise_level = (1 - noise/l_var )
41 noise_level[noise_level<0] = 0
42 denoised_img += noise_level*res
FloatingPointError: divide by zero encountered in divide
Other ways of starting a debugger¶
Raising an exception as a poor man’s break point¶
If you find it tedious to note the line number to set a break point, you can simply raise an exception at the point that you want to inspect and use IPython
’s %debug
. Note that in this case you cannot step or continue the execution.
Debugging test failures using nosetests
¶
You can run, e.g., nosetests --pdb my_test.py
to drop in post-mortem debugging on exceptions, and nosetests --pdb-failure
to inspect test failures using the debugger. (That that the default nose
package does not work with parameterised tests).
In addition, you can use the IPython
interface for the debugger in nose
by installing the nose
plugin ipdbplugin. You can than pass the --ipdb
and --ipdb-failure
options to nosetests
.
Calling the debugger explicitly¶
Insert the following line where you want to drop in the debugger:
import pdb; pdb.set_trace()
When running nosetests
, the output is captured, and thus it seems that the debugger does not work. Simply run the nosetests with the -s
flag.
Graphical debuggers and alternatives¶
pudb
is a good semi-graphical debugger with a text user interface in the console.The Visual Studio Code integrated development environment includes a debugging mode.
The Mu editor is a simple Python editor that includes a debugging mode.
Debugger commands and interaction¶
Command |
Description |
---|---|
l(list) |
Lists the code at the current position |
u(p) |
Walk up the call stack |
d(own) |
Walk down the call stack |
n(ext) |
Execute the next line (does not go down in new functions) |
s(tep) |
Execute the next statement (goes down in new functions) |
bt |
Print the call stack |
a |
Print the local variables |
!command |
Execute the given Python command (by opposition to pdb commands |
Debugger commands are not Python code¶
You cannot name the variables the way you want. For instance, you cannot override the variables in the current frame with the same name: use different names than your local variable when typing code in the debugger.
Getting help when in the debugger¶
Type h
or help
to access the interactive help:
ipdb> help
Documented commands (type help <topic>):
========================================
EOF bt cont enable jump pdef r tbreak w
a c continue exit l pdoc restart u whatis
alias cl d h list pinfo return unalias where
args clear debug help n pp run unt
b commands disable ignore next q s until
break condition down j p quit step up
Miscellaneous help topics:
==========================
exec pdb
Undocumented commands:
======================
retval rv
Debugging segmentation faults using gdb
¶
Note:¶
The following example makes use of python3
compiled with debugging symbols. On Unix systems this can be installed via the package manager, e.g. sudo apt install python3-dbg python3-dev
on Ubuntu systems. (I’m sorry, I did not get chance to check how this should be installed in Windows).
If you have a segmentation fault, you cannot debug it with pdb, as it crashes the Python interpreter before it can drop in the debugger. Similarly, if you have a bug in C code embedded in Python, pdb is useless. For this we turn to the gnu debugger, gdb, available on Linux, MacOS and Windows.
Before we start with gdb, let us add a few Python-specific tools to it.
For this we add a few macros to our ~/.gdbinit
. The optimal choice of
macro depends on your Python version and your gdb version. A
simplified version has been added in .gdbinit
located in the files
folder, but feel free to read
DebuggingWithGdb.
NOTE: Some of the paths specified in .gdbinit
are specific to my machine. You’ll need to modify these.
To debug with gdb the Python script segfault.py
, we can run the
script in gdb as follows
(gdb) run segfault.py
Starting program: /home/rhodri/debugging/bin/python segfault.py
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff4732700 (LWP 43662)]
[New Thread 0x7ffff1f31700 (LWP 43663)]
[New Thread 0x7ffff1730700 (LWP 43664)]
[New Thread 0x7fffecf2f700 (LWP 43665)]
[New Thread 0x7fffec72e700 (LWP 43666)]
[New Thread 0x7fffe7f2d700 (LWP 43667)]
[New Thread 0x7fffe572c700 (LWP 43668)]
[New Thread 0x7fffe4f2b700 (LWP 43669)]
[New Thread 0x7fffe072a700 (LWP 43670)]
[New Thread 0x7fffddf29700 (LWP 43671)]
[New Thread 0x7fffdb728700 (LWP 43672)]
[New Thread 0x7fffd8f27700 (LWP 43673)]
[New Thread 0x7fffd6726700 (LWP 43674)]
[New Thread 0x7fffd3f25700 (LWP 43675)]
[New Thread 0x7fffd1724700 (LWP 43676)]
Thread 1 "python" received signal SIGSEGV, Segmentation fault.
0x00007ffff6e9a4c9 in _aligned_strided_to_contig_size8 () from /home/rhodri/debugging/lib/python3.8/site-packages/numpy/core/_multiarray_umath.cpython-38-x86_64-linux-gnu.so
(gdb)
Then, to get a back-trace we can utilize the py-bt
command:
(gdb) py-bt
Traceback (most recent call first):
<built-in method concatenate of module object at remote 0x7ffff744aef0>
<built-in method implement_array_function of module object at remote 0x7ffff744aef0>
File "<__array_function__ internals>", line 261, in concatenate
File "/home/rhodri/debugging/lib/python3.8/site-packages/numpy/core/arrayprint.py", line 848, in _leading_trailing
self.floatmode = floatmode
File "/home/rhodri/debugging/lib/python3.8/site-packages/numpy/core/arrayprint.py", line 337, in _leading_trailing
_leading_trailing(a, edgeitems, index + np.index_exp[ :edgeitems]),
File "/home/rhodri/debugging/lib/python3.8/site-packages/numpy/core/arrayprint.py", line 489, in _array2string
data = _leading_trailing(data, options['edgeitems'])
File "/home/rhodri/debugging/lib/python3.8/site-packages/numpy/core/arrayprint.py", line 468, in wrapper
return f(self, *args, **kwargs)
File "/home/rhodri/debugging/lib/python3.8/site-packages/numpy/core/arrayprint.py", line 1204, in array2string
File "/home/rhodri/debugging/lib/python3.8/site-packages/numpy/core/arrayprint.py", line 2002, in _array_str_implementation
<built-in method print of module object at remote 0x7ffff761efb0>
File "segfault.py", line 16, in print_big_array
print(big_array[-10:])
File "segfault.py", line 23, in <module>
l.append(print_big_array(a))
And there we have it, our segmentation fault is being caused by line 23 in segfault.py
, specifically the command l.append(print_big_array(a))
.
Note¶
For a list of Python-specific commands defined in the .gdbinit
, read
the source of this file.
Debugging distributed (e.g. MPI) computations¶
Later in this course you will encounter the mpi4py package. Debugging MPI based programs can be a real pain and tools such as pdb
are often less useful. Utilising manual debugging techniques in tandem with tools such as tmpi can however be an effective method.
Wrap up exercise¶
The following script is well documented and hopefully legible. It seeks to answer a problem of actual interest for numerical computing, but it does not work… Can you debug it?
Python source code of to_debug.py
"""
A script to compare different root-finding algorithms.
This version of the script is buggy and does not execute. It is your task
to find an fix these bugs.
The output of the script sould look like:
Benching 1D root-finder optimizers from scipy.optimize:
brenth: 604678 total function calls
brentq: 594454 total function calls
ridder: 778394 total function calls
bisect: 2148380 total function calls
"""
from itertools import product
import numpy as np
from scipy import optimize
FUNCTIONS = (np.tan, # Dilating map
np.tanh, # Contracting map
lambda x: x**3 + 1e-4*x, # Almost null gradient at the root
lambda x: x+np.sin(2*x), # Non monotonous function
lambda x: 1.1*x+np.sin(4*x), # Fonction with several local maxima
)
OPTIMIZERS = (optimize.brenth, optimize.brentq, optimize.ridder,
optimize.bisect)
def apply_optimizer(optimizer, func, a, b):
""" Return the number of function calls given an root-finding optimizer,
a function and upper and lower bounds.
"""
return optimizer(func, a, b, full_output=True)[1].function_calls,
def bench_optimizer(optimizer, param_grid):
""" Find roots for all the functions, and upper and lower bounds
given and return the total number of function calls.
"""
return sum(apply_optimizer(optimizer, func, a, b)
for func, a, b in param_grid)
def compare_optimizers(optimizers):
""" Compare all the optimizers given on a grid of a few different
functions all admitting a signle root in zero and a upper and
lower bounds.
"""
random_a = -1.3 + np.random.random(size=100)
random_b = .3 + np.random.random(size=100)
param_grid = product(FUNCTIONS, random_a, random_b)
print("Benching 1D root-finder optimizers from scipy.optimize:")
for optimizer in OPTIMIZERS:
print('% 20s: % 8i total function calls' % (
optimizer.__name__,
bench_optimizer(optimizer, param_grid)
))
if __name__ == '__main__':
compare_optimizers(OPTIMIZERS)