Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Visualizing Complex Functions

4.95/5 (77 votes)
23 May 2010Ms-PL14 min read 115.4K   4.1K  
A program to produce beautiful and informative images of complex functions.

Introduction

Complex numbers are a subject simple enough to be taught in high school math, but subtle enough to continue to be investigated through college mathematics and beyond. A complex function is a function that accepts a complex number as its argument and returns a complex number as its value. Complex functions are the bread and butter of complex analysis, which plays an important role in algebra, geometry, number theory, and in a host of practical applications of mathematics such as physics and engineering.

Despite the critical role they play in the mathematical sciences, there is no widely accepted way to "draw" a complex function. Real functions can be easily represented by 2-D plots: the argument goes on the horizontal axis, the function value goes on the vertical axis. The interpretation of such plots has become so natural to us that we "get" a function faster by looking at its plot than by looking at any other representation. Unfortunately, the plot technique does not generalize to complex functions. We need the two dimensions of a page just to represent the function argument. Living in three spatial dimensions doesn't save us; since we need an additional two dimensions to represent the functions' value, we would need to live in four spatial dimensions in order to "plot" a complex function.

A number of poor substitutes for the 4-D plots we need have become common. At Wolfram's pages on the Gamma function, you can peruse separate "3-D perspective" plots of the function's real part, imaginary part, and absolute value, or 2-D "heat maps" of the same. For me at least, these images do more to show how poor these visualization techniques are than to give me much of a feel for the behavior of the Gamma function.

This article outlines a different visualization technique, and implements it in a simple WinForms application we call "Complex Explorer". Our technique produces images of complex functions that are beautiful and rich in information.

Image 1

The drop-down menu allows you to choose the function to visualize. The Save button allows you to save the currently displayed image to a PNG file. If you click on a point in the image, the Complex Explorer will tell you the argument and function values at that point.

Complex Explorer's Approach

Our visualization technique uses a form of domain coloring I first saw implemented by Claudio Rocchini.

Image 2

The first principal of the technique is taken from topographic mapping. Topographic maps show elevations by drawing contour lines, which are lines of equal altitude. If you walk along a contour line, you go neither up nor down. If you turn and move at right angles to a contour line, you are moving up or down, and will eventually reach another contour line. Where contour lines are close together, elevation changes rapidly over a short distance, that is, the slope is steep; where contour lines are widely spaced, the slope is gentle. "3-D Perspective" maps may give a slightly more immediate feel of a landscape, but they unavoidably hide some areas behind others, and it is much easier to read an accurate estimate of the elevation at a particular point from a contour line map than from a perspective map. For this reason, "topo maps" using contour lines have become a standard tool for hikers worldwide. The same principal is used in the isobar and isotherm maps used by meteorologists. With just a little practice, you can very quickly get the "lay of the land" by looking at contour lines.

Image 3

The second principal of our technique is to use color to encode information. This is a long-standing technique used in many maps; for example, using blue for water and green for forest in geographic maps. We will specify colors using hue-saturation-value (HSV) triplets, rather than the red-green-blue (RGB) triplets most familiar in computer graphics, because the axes of the HSV system correlate more directly with the human-perceived qualities of colors. In the HSV system, as illustrated, hue is specified by the angle along a color wheel, and saturation and value specify the intensity and brightness of the color. Black has zero value and white has zero saturation.

The final principal of our visualization technique is to think of complex function values in terms of magnitude and phase rather than in terms of real and imaginary parts. If you are used to thinking of complex numbers only in terms of real and imaginary parts, now is the time to expand your horizons. Just as different spatial coordinate systems highlight different aspects of the relationships between points, different complex coordinate systems highlight different aspects of complex numbers. For example, as we near a singularity of a complex function, we can't say anything in general about the function's real and imaginary parts x and y, but we can say that its magnitude ρ is increasing. The relationships between a complex number z, its real and imaginary parts x and y, and its magnitude ρ and phase θ, are illustrated in the diagram below.

Image 4

With contour lines and colors, we have the two degrees of freedom we need to represent a complex function's magnitude and phase. Since the natural measure of a complex function's "height" is its absolute value ρ, we will use contour lines to represent the "landscape" |f(z)|. Since both the phase θ of a complex number and the hues on a color wheel are naturally periodic, we will use hue to represent the phase arg(f(z)). The hues on a color wheel run from red through yellow, green, blue, violet, and back to red. Note: this is the same order as the colors of a rainbow. With this convention, positive real points are red, and negative real points are cyan (light blue), which lies opposite to red on the color wheel. To generate contour lines, we will adjust the saturation and value so that our pixels become white near |f(z)| = 0, |f(z)| = 1, and at exponentially increasing regular values thereafter, and black at regular intervals in between. Thus our images will consist of alternating white and black contour lines indicating magnitude, with fully saturated hues indicating phase in the areas in between.

We will discuss the details of our implementation later. For now, let's fire up Complex Explorer and get to know a few complex functions.

Examples

z

Image 5

Let's begin with the very simple function that Complex Explorer shows when first started: f(z)=z. Since this function is its argument, by studying it, you can get a feel for how our technique represents a complex number. Since |z| is the distance from the origin, the contour lines are concentric circles centered at the origin: a white dot in the middle where z=0, another white circle at |z|=1, and a third white circle for a yet larger |z|. Since the phase of z is the angle between it and the positive real axis, the hues in this plot are constant along radial lines, but change as we sweep around the complex plane.

z2-1

Image 6

Now let's look at a simple polynomial. Looked along the real axis, this would be a parabola, with zeros at x = ±1, negative between those roots, positive outside of them. And looking at our visualization of its complex generalization, we see exactly that behavior along the real axis: zeros of f(z) at z = ±1, with cyan points (negative real numbers) between them and red points (positive real numbers) beyond them. But we also see much richer behavior in the complex plane.

We see that the small, isolated valleys around each root join at the |f(z)| = 1 white contour line, which has a beautiful "infinity symbol" shape enclosing both roots. We see that for larger |z|, there is a single oval valley, which becomes increasingly circular as |z| increases. We see that f(z) is negative and real (cyan) along the entire imaginary axis. By tracing one orbit around the origin, we see that the phase of f(z) completes two full cycles (red to cyan to red to cyan to red) as z makes one. We see the z -> -z symmetry of the equation embodied in the reflection symmetry of our image around the vertical axis.

z3+1

Image 7

This cubic polynomial has only one real root, at z = -1, but our figure immediately shows that it has three complex roots, as the fundamental theorem of algebra says it must. (The others are z = (1 ± i sqrt(3))/2.) We see that, again, the small valleys of |f(z)| around each root join together to produce one large valley as |z| increases.

The phase structure is more complicated. We see three "channels" of positive real numbers (red) that flow into a circle in the center, whose boundaries are defined by the roots. Areas with other phases are not similarly connected. Instead, the three positive real channels are separated by three disconnected regions in which the phase of f(z) goes through its cycle as we move from one positive real channel to the next.

Notice that, while our previous example had a two-fold visual symmetry, this example has a three-fold visual symmetry. The visual symmetry in the previous example was induced by the algebraic symmetry z -> -z. Can you write down the algebraic symmetry that induces the visual symmetry in this example?

1/z

Image 8

Lest I be accused of over-selling this visualization technique, let me give an example which illustrates a problem. At right, you see the image of f(z) = 1/z, and you will immediately notice that it bears a striking resemblance to the image of f(z) = z with which we began. Yet the behavior of these functions could hardly be more different: while f(z) = z is zero at z=0 and grows larger as |z| increases, f(z) = 1/z is infinite at z=0 and falls off as |z| increases.

The problem you see here is exactly the same problem that plagues users of "topo" maps: as a collection of unlabeled contour lines, a valley looks the same as a mountain. Without contour line labels, there is no way to know whether the slope is going up or down. So you do have to know a little bit about the "lay of the land" (or read it from the labels) to see what a "topo" map is telling you, and you do have to know a little bit about the function you are visualizing (or deduce it from the functional form), in order to correctly interpret these images.

Notice, by the way, that the colors circulate around our simple pole in the opposite way they do around a simple root. That's because the phase arg(z-1) = -arg(z) is the opposite of the phase of z.

exp(z)

Image 9

Our image of the exponential function may look boring at first glance, but the reason for its surprising simplicity can give us some insight. It appears from our plot that the magnitude (elevation) of ez depends only on the real part of z, while its phase (hue) depends only on the imaginary part of z. How does that happen? By writing z = x + i y, we find f(z) = ex ei y. That is, |f(z)| = ex depends only on x while arg(f(z)) = y mod 2π depends only on y, which is precisely what the image tells us.

Γ(z)

Image 10

Recall that we began by critiquing Wolfram's visualization of the Gamma function. Let us see whether ours, shown above, does any better.

In the right complex plane, we see the saddle point at z ≈ 1.5; contour lines show the function increasing as we move outward from that point to the "east" or "west", decreasing as we move outward from that point to the "north" or "south". In the left half of the complex plane, we see singularities at the integer values 0, -1, -2, etc. Note that the colors circulate each pole in the same sense as in our 1/z example above. From the density of contour lines, we see that the poles nearer the origin are stronger (that is, rise higher faster) than the poles at higher negative integers. On the real axis, the function's sign alternates between positive (red) and negative (cyan) at intervals separated by the poles. Note, while the magnitude |f(z)| falls off as we move away from a pole in any direction, the behavior of the phase is not so uniform: as we move outward from a positive real interval, the function stays positive real, but as we move outward from a negative interval, the function can become either positive imaginary or negative imaginary, depending on the exact direction we move.

All this could be read off our image. And it's pretty to boot!

We have so far explored only six complex functions. Nearly that many again are built in to Complex Explorer, and you can easily add more of your own. Take a look at log(z) or sqrt(z) to see a discontinuity in the phase of a complex function, called a cut. Take a look at Rocchini's example function, whose beautiful visualization inspired me to create Complex Explorer. Take a look at ψ(z) and try to understand how it is related to Γ(z).

Implementation Details

Complex Explorer is a simple WinForms app. The core logic for producing an image from a complex function simply iterates over each pixel, computes the corresponding z value, computes the corresponding f(z) value, and maps that value to a color.

C#
public void DrawImage () {

    // get the function to evaluate
    Function<complex,complex> f = 
      functions[functionList.SelectedIndex].Function;

    Bitmap image = new Bitmap(imageBox.Width, imageBox.Height);

    // iterate over all image pixels
    for (int x = 0; x < imageBox.Width; x++) {
        double re = re_min + x * (re_max - re_min) / imageBox.Width;
        for (int y = 0; y < imageBox.Height; y++) {
            double im = im_max - y * (im_max - im_min) / imageBox.Height;

            // form a complex number based on the pixel value
            Complex z = new Complex(re, im);

            // compute the value of the current complex function
            // for that complex number
            Complex fz = f(z);

            // don't try to plot non-numeric values (e.g. at poles)
            if (Double.IsInfinity(fz.Re) || Double.IsNaN(fz.Re) || 
                Double.IsInfinity(fz.Im) || Double.IsNaN(fz.Im)) continue;

            // convert the complex function value to a HSV color triplet
            ColorTriplet hsv = ColorMap.ComplexToHsv(fz);

            // convert the HSV color triplet to an RBG color triplet
            ColorTriplet rgb = ColorMap.HsvToRgb(hsv);
            int r = (int) Math.Truncate(255.0 * rgb.X);
            int g = (int) Math.Truncate(255.0 * rgb.Y);
            int b = (int) Math.Truncate(255.0 * rgb.Z);
            Color color = Color.FromArgb(r, g, b);

            // plot the point
            image.SetPixel(x, y, color);
        }
    }

    // put the image in the image box control
    imageBox.Image = image;
}

The mapping of a complex value to an HSV color triplet is the key operation for our visualization technique. Determining the hue is easy; we just get the phase from the ComplexMath.Arg function and express it as a fraction of 2π. Determining the saturation and value is harder. We need our algorithm to fade to black or white near the desired contour line values, but keep those regions small enough that the figure does not become "washed out" with large regions devoid of color.

C#
public static ColorTriplet ComplexToHsv (Complex z) {

    // extract a phase 0 <= t < 2 pi
    double t = ComplexMath.Arg(z);
    while (t < 0.0) t += TwoPI;
    while (t >= TwoPI) t -= TwoPI;

    // the hue is determined by the phase
    double h = t / TwoPI;

    // extract a magnitude m >= 0
    double m = ComplexMath.Abs(z);

    // map the magnitude logrithmicly into the repeating interval 0 < r < 1
    // this is essentially where we are between countour lines
    double r0 = 0.0;
    double r1 = 1.0;
    while (m > r1) {
        r0 = r1;
        r1 = r1 * Math.E;
    }
    double r = (m - r0) / (r1 - r0);
    // this puts contour lines at 0, 1, e, e^2, e^3, ...

    // determine saturation and value based on r
    // p and q are complementary distances from a countour line
    double p = r < 0.5 ? 2.0 * r : 2.0 * (1.0 - r);
    double q = 1.0 - p;
    // only let p and q go to zero very close to zero;
    // otherwise they should stay nearly 1
    // this keep the countour lines from getting thick
    double p1 = 1 - q * q * q;
    double q1 = 1 - p * p * p;
    // fix s and v from p1 and q1
    double s = 0.4 + 0.6 * p1;
    double v = 0.6 + 0.4 * q1;

    return (new ColorTriplet() {X = h, Y = s, Z = v} );

}

This mapping is due to Claudio Rocchini. Notice that the variables p and q measure the distance from and to the nearest contour lines. A naive approach would be to map those quantities linearly into s and v, but that produces large regions that are nearly white or nearly black instead of sharply defined contour lines. (Try it for yourself!) Rocchini's trick for avoiding that problem is to use the variables p1 and q1 in place of p and q. p1 and q1 are produced from p and q via a simple function (1 - (1-x)3) that preserves ordering (that is, as the input rises from 0 to 1, the output also rises from 0 to 1), but stays closer to one over a larger ranger (the output is already ~0.9 by the time the input reaches ~0.5). This ensures that our contour lines are thin.

The .NET Framework does not provide direct support for HSV color specification, so we need to translate our HSV triplet to an RGB triplet. That computation is standard, and we will not go over it in detail. You can learn about this and other straightforward aspects of Complex Explorer's logic, such as its representation of complex functions and image save functionality, by studying the code.

Complex Explorer uses the Meta.Numerics library for its advanced complex functions like the Gamma function. As an added bonus, Meta.Numerics defines its own Complex type (just introduced in .NET 4.0) and function delegate (introduced in .NET 3.5), enabling Complex Explorer to be compiled with and work on .NET Framework versions all the way back to 2.0.

There are a lot of ways that Complex Explorer could be expanded. It would be nice to have a plug-in mechanism so that new functions could be added without having to change the program's code. It would be nice to be able to zoom in or out on different regions of the complex plane. Complex Explorer is free software under the MS-PL license, and I encourage anyone interested to take it and run with it, adding these and other cool features.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)