Asen Georgiev
Asen Georgiev

Reputation: 43

Why does this C# function to draw a sine wave using System.Drawing overflows?

So I made this little windows forms program that draws a sine wave but when I put certain functions like sin(x) ^ x it overflows. Please review this code and help me figure this out!

    private void button1_Click(object sender, EventArgs e)
    {
        if (Completed) return;
        Completed = true;
        Graphics g = this.CreateGraphics();
        Pen pen = new Pen(Brushes.Black, 2.0F);

        float x1 = 0;
        float y1 = 0;

        float y2 = 0;

        float yEx = 300;
        float eF = 50;


        for (float x = 0; x < Width; x += 0.005F)
        {
            y2 = (float)Math.Pow(Math.Sin(x) , x);

            g.DrawLine(pen, x1 * eF, y1 * eF + yEx, x * eF, y2 * eF + yEx);
            x1 = x;
            y1 = y2;
        }

    }

When I execute, it runs for a bit and then displays this:
what is exactly displayed

Any contribution is appreciated!

Upvotes: 1

Views: 6481

Answers (3)

Abion47
Abion47

Reputation: 24726

Alright, so it turns out the solution is a mix of TyCobb's answer and John Wu's answer. When you use Math.Sin on an increasing value like x, roughly half of the output from Math.Sin(x) is going to be negative. Math.Pow doesn't like it when you give it a negative number in the first parameter and a non-integer as the second parameter (it will return NaN in this case). The reason for this is because when you raise a negative number by a non-integer, the result is a complex number.

Now Graphics.DrawLine doesn't care if you pass NaN as either of the second pair of float parameters (x2, y2) as it will just return without doing anything. But if you pass NaN as one of the first pair (x1, y1), it will throw an OverflowException. Why it does this, I'm not sure, but I'd be willing to guess that it was an oversight on the part of the .NET team back in the day.

Now from the first discovery, you can see the problem is mainly whenever x is negative. The easy solution to that would be to replace x with Math.Pow(x), but that might alter the output in a way that doesn't reflect the intentions of the original function. A second solution would be to simply skip those iterations, but that will leave holes in your graph.

A third solution you may have guessed already, but that is to replace Math.Pow with Complex.Pow. This will give you the support you need for the potential inputs you may get to your power function by returning a complex number. Once you've done that, you can choose what you want to do with the result. (I just took the Real part of the result as my y and discarded the Imaginary portion.)

for (float x = 0; x < Width; x += 0.005F)
{
    y2 = (float)Complex.Pow(Math.Sin(x), x).Real;

    g.DrawLine(pen, x1 * eF, y1 * eF + yEx, x * eF, y2 * eF + yEx);
    x1 = x;
    y1 = y2;
}

The result is this:

enter image description here

Upvotes: 2

John Wu
John Wu

Reputation: 52280

The problem is that Math.Pow sometimes returns a float that is not a number or NaN. Specifically:

x < 0 but not NegativeInfinity; y is not an integer, NegativeInfinity, or PositiveInfinity. = NaN

Since Sin(x) will draw a curve that is both above and below 0, some of its values will be negative, thus the Math.Pow call will yield NaN, which of course cannot be drawn on Cartesian space.

I suggest you temporarily modify your code as follows: Wrap the draw call with a try block.

try
{
    g.DrawLine(pen, x1 * eF, y1 * eF + yEx, x * eF, y2 * eF + yEx);
}
catch {}  //Not usually a great idea to eat all exceptions, but for troubleshooting purposes this'll do

Then run your code and view the line. The breaks in the line will tell you which parts of the domain/range are problematic. Once you have a holistic view of the computational issues, you may be able to transform/map your Cartesian space into a domain where the overflows don't happen, e.g. by adding a constant to the result of the SIN function.

Upvotes: 0

TyCobb
TyCobb

Reputation: 9089

Validate your values for the DrawLine.

You do a check for the Width at the loop, but you do not validate your calculations before you call DrawLine. Here's an example without the draw:

float x1 = 0;
float y1 = 0;

float y2 = 0;

float yEx = 300;
float eF = 50;

const int width = 1024;
const int height = 1024;

for (float x = 0; x < width; x += 0.005F)
{
    y2 = (float)Math.Pow(Math.Sin(x) , x);

    var a = x1 * eF;
    var b = y1 * eF + yEx; 
    var c = x * eF;
    var d = y2 * eF + yEx;

    if(a < 0 || a >= width || b < 0 || b >= height || c < 0 || c >= width || d < 0 || d > height)
        Console.WriteLine("Snap! {0} | {1} | {2} | {3}", a, b, c, d);
    x1 = x;
    y1 = y2;
}

Here's an online example

GDI will not help you out when you try to draw off the canvas, in fact it will normally go boom. You should always limit the values to be within the height and width.

Here's some highlights of the example:

Snap! 1101,215 | NaN | 1101,465 | NaN
Snap! 1101,465 | NaN | 1101,715 | NaN
Snap! 1101,715 | NaN | 1101,965 | NaN
Snap! 1101,965 | NaN | 1102,215 | NaN
Snap! 1102,215 | NaN | 1102,465 | NaN
Snap! 1102,465 | NaN | 1102,715 | NaN
Snap! 1102,715 | NaN | 1102,965 | NaN
Snap! 1102,965 | NaN | 1103,215 | NaN
Snap! 1103,215 | NaN | 1103,465 | NaN
Snap! 1103,465 | NaN | 1103,715 | NaN
Snap! 1103,715 | NaN | 1103,965 | NaN
Snap! 1103,965 | NaN | 1104,215 | NaN
Snap! 1104,215 | NaN | 1104,465 | NaN
Snap! 1104,465 | NaN | 1104,715 | NaN
Snap! 1104,715 | NaN | 1104,965 | NaN
Snap! 1104,965 | NaN | 1105,215 | NaN

Upvotes: 0

Related Questions