
primer_on_scientific_programming_with_python
.pdf


C.2 Program Development and Testing |
635 |
|
|
step i, so the t+=dt update must come after S[i+1] is computed. The same is important in the special formula for the first time step as well.
A main program will typically first set some default values of the 10 input parameters, then call init_prms to let the user adjust the default values, and then call solve to compute the Si values:
# default values: from math import pi
m = 1; b = 2; L = 10; k = 1; beta = 0; S0 = 1; dt = 2*pi/40; g = 9.81; w_formula = ’0’; N = 80;
m, b, L, k, beta, S0, dt, g, w, N = \
init_prms(m, b, L, k, beta, S0, dt, g, w_formula, N) S = solve(m, k, beta, S0, dt, g, w, N)
So, what shall we do with the solution S? We can write out the values of this list, but the numbers do not give an immediate feeling for how the box moves. It will be better to graphically illustrate the S(t) function, or even better, the Y (t) function. This is straightforward with the techniques from Chapter 4 and is treated in Appendix C.3. In Chapter 9.5, we develop a drawing tool for drawing figures like Figure C.1. By drawing the box, string, and plate at every time level we compute Si, we can use this tool to make a moving figure that illustrates the dynamics of the oscillations. Already now you can play around with a program doing that (box_spring_figure_anim.py).
C.2.2 Callback Functionality
It would be nice to make some graphics of the system while the computations take place, not only after the S list is ready. The user must then put some relevant statements in between the statements in the algorithm. However, such modifications will depend on what type of analysis the user wants to do. It is a bad idea to mix user-specific statements with general statements in a general algorithm. We therefore let the user provide a function that the algorithm can call after each Si value is computed. This is commonly called a callback function (because a general function calls back to the user’s program to do a user-specific task). To this callback function we send three key quantities: the S list, the point of time (t), and the time step number (i + 1), so that the user’s code gets access to these important data.
If we just want to print the solution to the screen, the callback function can be as simple as
def print_S(S, t, step):
print ’t=%.2f S[%d]=%+g’ % (t, step, S[step])
In the solve function we take the callback function as a keyword argument user_action. The default value can be an empty function, which we can define separately:

636 |
C A Complete Project |
|
|
def empty_func(S, time, time_step_no): return None
def solve(m, k, beta, S0, dt, g, w, N, user_action=empty_func):
...
However, it is quicker to just use a lambda function (see Chapter 2.2.11):
def solve(m, k, beta, S0, dt, g, w, N,
user_action=lambda S, time, time_step_no: None):
The new solve function has a call to user_action each time a new S value has been computed:
def solve(m, k, beta, S0, dt, g, w, N,
user_action=lambda S, time, time_step_no: None):
"""Calculate N steps |
forward. Return list S.""" |
S = [0.0]*(N+1) |
# output list |
gamma = beta*dt/2.0 |
# short form |
t = 0 |
|
S[0] = S0 |
|
user_action(S, t, 0) |
|
#special formula for first time step: i = 0
S[i+1] = (1/(2.0*m))*(2*m*S[i] - dt**2*k*S[i] + m*(w(t+dt) - 2*w(t) + w(t-dt)) + dt**2*m*g)
t = dt
user_action(S, t, i+1)
#time loop:
for i in range(1,N):
S[i+1] = (1/(m + gamma))*(2*m*S[i] - m*S[i-1] + gamma*dt*S[i-1] - dt**2*k*S[i] + m*(w(t+dt) - 2*w(t) + w(t-dt)) + dt**2*m*g)
t += dt user_action(S, t, i+1)
return S
The two last arguments to user_action must be carefully set: these should be time value and index for the most recently computed S value.
C.2.3 Making a Module
The init_prms and solve functions can now be combined with many di erent types of main programs and user_action functions. It is therefore preferable to have the general init_prms and solve functions in a module box_spring and import these functions in more user-specific programs. Making a module out of init_prms and solve is, according to Chapter 3.5, quite trivial as we just need to put the functions in a file box_spring.py.
It is always a good habit to include a test block in module files. To make the test block small, we place the statements in a separate

C.2 Program Development and Testing |
637 |
|
|
function _test and just call _test in the test block. The initial underscore in the name _test prevents this function from being imported by a from box_spring import * statement. Our test here simply prints solution at each time level. The following code snippet is then added to the module file to include a test block:
def _test():
def print_S(S, t, step):
print ’t=%.2f S[%d]=%+g’ % (t, step, S[step])
# default values: from math import pi
m = 1; b = 2; L = 10; k = 1; beta = 0; S0 = 1; dt = 2*pi/40; g = 9.81; w_formula = ’0’; N = 80;
m, b, L, k, beta, S0, dt, g, w, N = \
init_prms(m, b, L, k, beta, S0, dt, g, w_formula, N) S = solve(m, k, beta, S0, dt, g, w, N,
user_action=print_S)
if __name__ == ’__main__’: _test()
C.2.4 Verification
To check that the program works correctly, we need a series of problems where the solution is known. These test cases must be specified by someone with a good physical and mathematical understanding of the problem being solved. We already have a solution formula (C.10) that we can compare the computations with, and more tests can be made in the case w = 0as well.
However, before we even think of checking the program against the formula (C.10), we should perform some much simpler tests. The simplest test is to see what happens if we do nothing with the system. This solution is of course not very exciting – the box is at rest, but it is in fact exciting to see if our program reproduces the boring solution. Many bugs in the program can be found this way! So, let us run the program box_spring.py with -S0 0 as the only command-line argument. The output reads
t=0.00 S[0]=+0
t=0.16 S[1]=+0.121026 t=0.31 S[2]=+0.481118 t=0.47 S[3]=+1.07139 t=0.63 S[4]=+1.87728 t=0.79 S[5]=+2.8789 t=0.94 S[6]=+4.05154 t=1.10 S[7]=+5.36626
...
Something happens! All S[1], S[2], and so forth should be zero. What is the error?
There are two directions to follow now: we can either visualize the solution to understand more of what the computed S(t) function looks

638 |
C A Complete Project |
|
|
like (perhaps this explains what is wrong), or we can dive into the algorithm and compute S[1] by hand to understand why it does not become zero. Let us follow both paths.
First we print out all terms on the right-hand side of the statement that computes S[1]. All terms except the last one (Δt2 mg) are zero. The gravity term causes the spring to be stretched downward, which causes oscillations. We can see this from the governing equation (C.8) too: If there is no motion, S(t) = 0, the derivatives are zero (and w = 0 is default in the program), and then we are left with
kS = mg : S = |
m |
g . |
(C.22) |
|
|||
|
k |
|
This result means that if the box is at rest, the spring is stretched (which is reasonable!). Either we have to start with S(0) = mk g in the equilibrium position, or we have to turn o the gravity force by setting -g 0 on the command line. Setting either -S0 0 -g 0 or -S0 9.81 shows that the whole S list contains either zeros or 9.81 values (recall that m = k = 1 so S0 = g). This constant solution is correct, and the coding looks promising.
We can also plot the solution using the program box_spring_plot:
Terminal
box_spring_plot.py --S0 0 --N 200
Figure C.2 shows the function Y (t) for this case where the initial stretch is zero, but gravity is causing a motion. With some mathematical analysis of this problem we can establish that the solution is correct. We have that m = k = 1 and w = β = 0, which implies that the governing equation is
d2S |
+ S = g, S(0) = 0, dS/dt(0) = 0 . |
dt2 |
Without the g term this equation is simple enough to be solved by basic techniques you can find in most introductory books on di erential equations. Let us therefore get rid of the g term by a little trick: we introduce a new variable T = S − g, and by inserting S = T + g in the equation, the g is gone:
d2T |
+ T = 0, |
T (0) = −g, |
dT |
(0) = 0 . |
(C.23) |
dt2 |
dt |
This equation is of a very well-known type and the solution reads T (t) = −g cos t, which means that S(t) = g(1 − cos t) and
Y (t) = −L − g(1 − cos t) − 2b .

C.3 Visualization |
639 |
|
|
With L = 10, g ≈ 10, and b = 2 we get oscillations around y ≈ 21 with a period of 2π and a start value Y (0) = −L −b/2 = 11. A rough visual inspection of the plot shows that this looks right. A more thorough analysis would be to make a test of the numerical values in a new callback function (the program is found in box_spring_test1.py):
from box_spring import init_prms, solve from math import cos
def exact_S_solution(t): return g*(1 - cos(t))
def check_S(S, t, step):
error = exact_S_solution(t) - S[step]
print ’t=%.2f S[%d]=%+g error=%g’ % (t, step, S[step], error)
# fixed values for a test: from math import pi
m = 1; b = 2; L = 10; k = 1; beta = 0; S0 = 0 dt = 2*pi/40; g = 9.81; N = 200
def w(t): return 0
S = solve(m, k, beta, S0, dt, g, w, N, user_action=check_S)
The output from this program shows increasing errors with time, up as large values as 0.3. The di culty is to judge whether this is the error one must expect because the program computes an approximate solution, or if this error points to a bug in the program – or a wrong mathematical formula.
From these sessions on program testing you will probably realize that verification of mathematical software is challenging. In particular, the design of the test problems and the interpretation of the numerical output require quite some experience with the interplay between physics (or another application discipline), mathematics, and programming.
C.3 Visualization
The purpose of this section is to add graphics to the oscillating system application developed in Appendix C.2. Recall that the function solve solves the problem and returns a list S with indices from 0 to N. Our aim is to plot this list and various physical quantities computed from it.
C.3.1 Simultaneous Computation and Plotting
The solve function makes a call back to the user’s code through a callback function (the user_action argument to solve) at each time level. The callback function has three arguments: S, the time, and the



642 |
C A Complete Project |
|
|
from box_spring import init_prms, solve |
|
from scitools.std import * |
|
def plot_S(S, t, step): |
|
first_plot_step = 10 |
# skip the first steps |
if step < first_plot_step: |
|
return |
|
tcoor = linspace(0, t, step+1) |
# t = dt*step |
S = array(S[:len(tcoor)]) |
|
Y = w(tcoor) - L - S - b/2.0 |
# (w, L, b are global vars.) |
plot(tcoor, Y,
axis=[0, N*dt, min(Y), max(Y)], xlabel=’time’, ylabel=’Y’)
# default values:
m = 1; b = 2; L = 10; k = 1; beta = 0; S0 = 1
dt = 2*pi/40; g = 9.81; w_formula = ’0’; N = 200
m, b, L, k, beta, S0, dt, g, w, N = \
init_prms(m, b, L, k, beta, S0, dt, g, w_formula, N)
# vectorize the StringFunction w:
w_formula = str(w) # keep this to see if w=0 later
if ’ else ’ in w_formula: |
|
w = vectorize(w) |
# general vectorization |
else: |
|
w.vectorize(globals()) |
# more efficient (when no if) |
S = solve(m, k, beta, S0, dt, g, w, N, user_action=plot_S)
# first make a hardcopy of the the last plot of Y: hardcopy(’tmp_Y.eps’)
C.3.2 Some Applications
What if we suddenly, right after t = 0, move the plate upward from y = 0 to y = 1? This will set the system in motion, and the task is to find out what the motion looks like.
There is no initial stretch in the spring, so the initial condition becomes S0 = 0. We turn o gravity for simplicity and try a w = 1 function since the plate has the position y = w = 1 for t > 0:
Terminal
box_spring_plot.py --w ’1’ --S 0 --g 0
Nothing happens. The reason is that we specify w(t) = 1, but in the equation only d2w/dt2 has an e ect and this quantity is zero. What we need to specify is a step function: w = 0 for t ≤ 0 and w = 1 for t > 0. In Python such a function can be specified as a string expression ’1 if t>0 else 0’. With a step function we obtain the right initial jump of the plate:
Terminal