[C# Helper]
Index Books FAQ Contact About Rod
[Essential Algorithms, Second Edition]

[The Modern C# Challenge]

[WPF 3d, Three-Dimensional Graphics with WPF and C#]

[The C# Helper Top 100]

[Interview Puzzles Dissected]

[C# 24-Hour Trainer]

[Beginning Software Engineering]

[C# 5.0 Programmer's Reference]

[MCSD Certification Toolkit (Exam 70-483): Programming in C#]

Title: Draw the Weierstrass function in C#

[Draw the Weierstrass function in C#]

The Weierstrass function was discovered by Karl Weierstrass in 1872. It's an odd function that is continuous everywhere but differentiable nowhere. I'll explain what that means shortly. Here's the function:

[Draw the Weierstrass function in C#]

Here:

[Draw the Weierstrass function in C#]

The following two sections explain what it means to be continuous and differentiable. The section after those explains the example program.

Continuous

Intuitively, a function is continuous if there are no breaks or jumps in its values. More technically, a function is continuous at a point (x, F(x)) if a small change in the x value leads to an arbitrarily small change in Y value.

[Draw the Weierstrass function in C#]

To think of this in another way, suppose you pick a small change in Y value dy. Then I can find a small enough value dx such that |F(x - dx) - F(x + dx)| < dy. In other words, I can find a small enough region around the point (x, F(x)) so the function varies by no more than your desired amount dy.

Most of the "normal" functions that you're used to dealing with are continuous. An example of a function that is not continuous is y = 1/x, which is graphed on the right. This function is discontinuous at x = 0 because its value jumps from minus infinity to plus infinity at that point.

Because the Weierstrass function is continuous everywhere, that means if you zoom in closely enough on any point, the function's values are limited. They don't tend towards infinity and they don't jump from one value to another.

Differentiable

Intuitively, a function is differentiable at a point if it varies smoothly there. Another way to think about it is that, if you zoom in closely enough on the point, the function is approximately linear.

More technically, a function is differentiable at a point if it has a derivative at that point.

[Draw the Weierstrass function in C#]

Functions like lines, parabolas, and other polynomial functions are continuous. The function y = |x|, which is graphed on the right, is not differentiable at the point x = 0 because its slope is undefined at x = 0.

Note that a function that is differentiable at point x must also be continuous there, but as the Weierstrass function shows, the converse is not true.

The Weierstrass function is weird because it is everywhere continuous but nowhere differentiable. Basically, the function is infinitely spiky. If you zoom in on part of the curve, you'll find spikes that were too small to notice at the zoomed-out scale.

The function is also fractal. If you zoom in, you'll find a similar zigzaggy up and down structure superimposed with smaller zigzags.

The Example

The example program is fairly simple. The following code calculates the Weierstrass function.

// Return a value of the Weierstrass function. private float F(float x, float A, float B) { const int iterations = 100; double total = 0; for (int n = 0; n < iterations; n++) { double cos = Math.Cos(Math.Pow(B, n) * Math.PI * x); if (cos > 1) cos = 0; else if (cos < -1) cos = 0; total += Math.Pow(A, n) * cos; } return (float)total; }

In theory, the cosine function should give values between 1 and -1. Unfortunately if its input is too big, the function has some sort of overflow error and returns a nonsensical result. The code protects itself against that by checking for values smaller than -1 and larger than 1, and setting the cosine to 0 in those cases.

The cosine is multiplied by a to the nth power. Because 0 < a < 1, that value becomes smaller as n grows larger. For example, if a = 0.9, Math.Pow(a, 100) is around 0.000027, so that term adds little to the total. Because terms with larger values of n don't contribute much to the total, the program saves time by only adding up the first 100 terms. (This works as long as a isn't too big. If a is 0.999, you should probably use more terms.)

The following code draws the function's graph.

// Draw the graph. private void DrawGraph() { const float wxmin = -2; const float wxmax = 2; const float wymin = -2; const float wymax = 2; int wid = picGraph.ClientSize.Width; int hgt = picGraph.ClientSize.Height; Bitmap bm = new Bitmap(wid, hgt); using (Graphics gr = Graphics.FromImage(bm)) { gr.SmoothingMode = SmoothingMode.AntiAlias; RectangleF rect = new RectangleF( wxmin, wymin, (wxmax - wxmin), (wymax - wymin)); PointF[] points = { new PointF(0, hgt), new PointF(wid, hgt), new PointF(0, 0), }; gr.Transform = new Matrix(rect, points); Matrix inverse = gr.Transform; inverse.Invert(); // Draw the axes. using (Pen pen = new Pen(Color.Red, 0)) { gr.DrawLine(pen, wxmin, 0, wxmax, 0); gr.DrawLine(pen, 0, wymin, 0, wymax); } // Plot the function. // Convert X coordinates for each pixel into world coordinates. PointF[] values = new PointF[wid]; for (int i = 0; i < wid; i++) values[i].X = i; inverse.TransformPoints(values); // Generate Y values. for (int i = 0; i < wid; i++) values[i].Y = F(values[i].X, A, B); // Plot. using (Pen pen = new Pen(Color.Black, 0)) { gr.DrawLines(pen, values); } } picGraph.Image = bm; }

The code first defines the X and Y values within which it will draw the graph. It then defines a transformation to map those coordinate bounds onto the program's PictureBox.

The code then draws the X and Y axes. It makes an array holding X values for each of the pixels in the PictureBox, and then uses the inverse transformation matrix to map those points into world coordinates. The code uses the function to fill in the corresponding Y values, and then draws the curve that they define.

Download the example to experiment with it and to see additional details.

© 2009-2022 Rocky Mountain Computer Consulting, Inc. All rights reserved.