Nathan Grigg

Drawing a slope field in SVG using Python

I am teaching a differential equations course this quarter. Most of my students find slope fields very useful to visualize the set of solutions to a first order differential equation. Here is an example:

-1 -0.8 -0.6 -0.4 -0.2 0 0.2 0.4 0.6 0.8 1 y'=t-y -1 -0.8 -0.6 -0.4 -0.2 0 0.2 0.4 0.6 0.8 1

Since I know y’ as a function of y and t, I draw a short line at each point (t,y) with slope y’. Then every solution to the differential equation must run tangent to all of these little lines. If you think of the lines as the flow direction of a river, the solutions are the lines traced out by dropping a stick in the river and watching it flow.

Slope fields are incredibly easy to draw, you just have to plug in 900 or so combinations of t and y. But it isn’t something you want to do by hand. There are quite a few slope field generators out on the internet. Most of them are written as Java applets, like this one that I have pointed my students to in the past. Java applets are always painful to use and don’t work at all on the iPad, so I put together a Python script to draw the slope field.

Here are some of the different technologies that go into making this work. There is something for everyone.

Scalable Vector Graphics (SVG)

Until now, my only real experience the SVG format is the SVG gnuplot terminal, which I use to monitor the weather near my house. It is a vector format, which makes it great for line-based drawings. It is based on XML, which makes it easy to manipulate. Even better, it is human-readable. Here is the code for one tick mark:

<line x1 = "47.2" y1 = "121.5" x2 = "54.3" y2 = "127.1" />

You can imagine how easy this makes it to generate the graph using Python.

In HTML5, you can use the svg tag to embed the SVG code directly into the html file, and all current versions of major browsers support this. So if you want to see what the code looks like for the slope field above, just view this page’s source.

Python generator functions

Most of the time, I’m writing scripts to do stuff on my personal computer. I make excessive use of long lists and giant strings, never worrying about memory. This is analogous to using UNIX commands one at a time, writing the output to a temporary file each time.

Generator functions can be used to implement a pipeline structure, like when you chain UNIX commands. You can link several generator functions so that each one uses the output of another. Similar to a UNIX pipeline, the functions run in parallel, and there is no need to temporarily store the output.

Here is an example, a modified version of what I used to make the slopefield.

 1 def slopefield(fn,tmin,tmax,dt,ymin,ymax,dy):
 2     """Generator for the slopefield ticks"""
 3     t = tmin + 0.5 * dt
 4     while t < tmax:
 5         y = ymin + 0.5 * dy
 6         while y < ymax:
 7             # tick calculates the endpoints of the tick
 8             yield tick(t,y,fn)
 9             y += dy
10         t += dt
11 
12 for tick in slopefield(fn,tmin,tmax,dt,ymin,ymax,dy):
13     # svg generates one line of svg code
14     print svg(tick)

The yield statement makes slopefield a generator function. You could actually do this whole thing without a generator by replacing yield tick(t,y,f) with print svg(tick(t,y,f)). But when I was testing my code, I found it very useful to be able to access the intermediate data, e.g.

for tick in slopefield(fn,tmin,tmax,dt,ymin,ymax,dy):
    print tick

The great thing is that slopefield only generates its output as needed. There is no need to store it in memory somewhere.

You can only use a generator one time through.

Input Sanitization

Originally, I thought that I would parse the equations by myself, because the eval command can be used to execute arbitrary code, and that sounds pretty scary. I thought about using pyparser, but decided I was being ridiculous.

I convinced myself that it would be safe to use eval with the following sanitization, which checks every word of the string against a whitelist.

def sanitize(fn_str):
    """Sanitizes fn_str, evaluates, and returns a function"""

    VALID_WORDS = ['','sin','cos','tan','t','y','abs','sqrt','e',
        'pi','log','ln','acos','asin','atan','cosh','sinh','tanh']

    # separate into words on number and operator boundaries
    words = re.split(r'[0-9.+\-*/^ ()]+',fn_str)

    for word in words:
            if word not in VALID_WORDS:
            error('Unrecognized expression in function: %s' % word)

    s = fn_str.replace('^','**')

    # replace 1.232 with float(1.234)
    s = re.sub(r'[0-9.]+', r'float(\g<0>)', s)

    return eval("lambda t,y: " + s)

I wrap the numbers in a float command because Python does arbitrary-precision integer arithmetic, and I don’t want people to be able to type stuff like 9^9^9^9^9^9^9^9 and set the processor going crazy. That, and we avoid any integer division issues.

As of this writing, you can try it out for yourself. The code is also on GitHub.