欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Lesson on complex number object

程序员文章站 2022-03-17 11:28:56
...

Complex numbers, such as 2 + 5i where i = sqrt(-1), have extensive applications in math and physics. So it deserves an another look itself. In this note, I'll go through the decisions I make in the design/modeling. These decisions, I believe, not only work in this case, but also in other common cases, such as rational numbers p / q where p and q are integers, money(cash, with amount and currency), etc. The code is in C#, while I briefly mention Java as well.

 

In C#, for performance reason, I model it as a struct because struct data is allocated on the stack, not on the heap.

 

struct ComplexNumber
{
     double real;
     double imag;

     public ComplexNumber(double real, double imag)
     {
         this.real = real;
         this.imag = imag;
     }
}

 

Here I use the type of double for both real and imaginary parts. I don't find applications that warrants other types, such as float, long, or int. However, with a little bit imagination, we should switch the types if the application needs, such as the following case,

  • if memory is tight and float can do the job.
  • if integer(int or long) is good enough to do the job, because the arithmetics of integers are faster.

Though we could utilize generics to parametize the types of real and imaginary parts, I feel this decision is more close to the general definition of complex numbers.

 

Another consideration is the copy constructor, do we need it? My preference is not having it, because there are only two fields and it's clearer when we explicitly initiate a complex number with its real and imaginary parts.

 

Since we model complex numbers as value objects, namely two complex numbers are equal if their real parts are equal and imaginary parts are equal. So we override the Equals() and GetHashCode() methods

 

public override bool Equals(object obj)
{
    if (obj == null) return false;
    if (!(obj is ComplexNumber)) return false;

    ComplexNumber c = (ComplexNumber)obj;
    return real == c.real && imag == c.imag;
}

public override int GetHashCode()
{
    return real.GetHashCode() + 19 * imag.GetHashCode();
}

 

Here 19 is just a prime number arbitrarily picked to generate not-so-often-*ed hash code. For convenience, we override ToString() as well.

 

public override string ToString()
{
    if (imag == 0) return "" + real;
    else if (imag > 0) return "" + real + "+" + imag + "i";
    else return "" + real + imag + "i";
}

 

 There are a few basic operations for complex numbers, norm(), arg(), conjugate(). And for convenience, I provide an extra method, reciprocal()

 

public double norm()
{
    return Math.Sqrt(real * real + imag * imag);
}

public double arg()
{
    if (real == 0)
    {
        if (imag == 0) return 0;
        if (imag > 0) return Math.PI / 2;
        else return -Math.PI / 2;
    }

    return Math.Atan(imag / real);
}

public ComplexNumber conjugate()
{
    return new ComplexNumber(real, -imag);
}

public ComplexNumber reciprocal()
{
    double r2 = real * real + imag * imag;
    if (r2 == 0) // should we check precision
        throw new DivideByZeroException("there is no reciprocal for zero!");

    return new ComplexNumber(real / r2, -imag / r2);
}

 

Next, since C# has operator overloading, so I add them here

 

public static ComplexNumber operator +(ComplexNumber complexNumber1, ComplexNumber complexNumber2)
{
        return new ComplexNumber(complexNumber1.real + complexNumber2.real, 
                                complexNumber1.imag + complexNumber2.imag);
}

public static ComplexNumber operator -(ComplexNumber complexNumber1, ComplexNumber complexNumber2)
{
    return new ComplexNumber(complexNumber1.real - complexNumber2.real, 
                                 complexNumber1.imag - complexNumber2.imag);
}

public static ComplexNumber operator *(ComplexNumber complexNumber1, ComplexNumber complexNumber2)
{
    double r = complexNumber1.real * complexNumber2.real - 
               complexNumber1.imag * complexNumber2.imag;
    double i = complexNumber1.imag * complexNumber2.real - 
               complexNumber1.real * complexNumber2.imag;
    return new ComplexNumber(r, i);
}

public static ComplexNumber operator /(ComplexNumber complexNumber1, ComplexNumber complexNumber2)
{
    double r2 = complexNumber2.real * complexNumber2.real +
                complexNumber2.imag * complexNumber2.imag;
    if (r2 == 0) // should we check precision
        throw new DivideByZeroException("divided by zero!");

    double r = complexNumber1.real * complexNumber2.real + 
               complexNumber1.imag * complexNumber2.imag;
    double i = complexNumber1.imag * complexNumber2.real - 
               complexNumber1.real * complexNumber2.imag;
    return new ComplexNumber(r / r2, i / r2);
}

 

And we shouldn't forget the following since we override the Equals() method

 

public static bool operator ==(ComplexNumber complexNumber1, ComplexNumber complexNumber2)
{
    // we need cast this to object, otherwise this == is a recursive call and end up an infinite loop
    if ((object)complexNumber1 == null) return (object)complexNumber2 == null;
    return complexNumber1.Equals(complexNumber2);
}

public static bool operator !=(ComplexNumber complexNumber1, ComplexNumber complexNumber2)
{
    // we need cast this to object, otherwise this == is a recursive call and end up an infinite loop
    if ((object)complexNumber1 == null) return (object)complexNumber2 != null;
    return !complexNumber1.Equals(complexNumber2);
}

 

So far so good, we can write a testcase, I just use console printout.

 

ComplexNumber a = new ComplexNumber(5, 12);
ComplexNumber b = new ComplexNumber(9, 11);
ComplexNumber c = new ComplexNumber(0, 0);
ComplexNumber d = new ComplexNumber(5, 12);

// test equalities
Console.WriteLine("a is not null: " + (a.Equals(null)) + ", " + (a == null));
Console.WriteLine("a is not a string: " + (a.Equals("abc")));
Console.WriteLine("a is not a real number: " + (a.Equals(2)));
Console.WriteLine("value object equal: " + (a.Equals(d)) + ", " + (a == d));
Console.WriteLine("value object not equal: " + (a == b));

// test behaviors
Console.WriteLine("a=" + a);
Console.WriteLine("a.norm=" + a.norm());
Console.WriteLine("a.arg=" + a.arg());
Console.WriteLine("a.conj=" + a.conjugate());
Console.WriteLine("a.reci=" + a.reciprocal());
try { c.reciprocal(); }
catch (DivideByZeroException dze)
{
    Console.WriteLine("divided by zero: " + dze.Message);
}

 

 Now the question arise, what if we have

 

a + 6

 

we are adding a complex number to a double. So we need something extra. There are two ways to accomplish this. The first way is to add an implicit operator

 

public static implicit operator ComplexNumber(double d)
{
    return new ComplexNumber(d, 0);
}

 

This will convert a double to a ComplexNumber in the arithmetic operations. The second way is to add explicitly code the methods, for instance,

 

public static ComplexNumber operator +(ComplexNumber complexNumber, double real)
{
    return new ComplexNumber(complexNumber.real + real, complexNumber.imag);
}

public static ComplexNumber operator +(double real, ComplexNumber complexNumber)
{
    return new ComplexNumber(complexNumber.real + real, complexNumber.imag);
}

 

This method is more straight forward and thus faster (without creating extra ComplexNumber object). We should do this for all 4 arithmetic operations.

 

 Now let's go back to implicit conversion(i.e., the first method), do we really need this method? The only usage of this method is the case where we have something special in complex number context but not in the real(double) context. From my experience I know this is indeed the case, so we keep this method. In addition to implicit conversion, we have explicit conversion too(namely, cast). And there are two directions in each conversion, so totally we have 4 cases:

 

  • implicit operator ComplexNumber(double d)
  • implicit operator double(ComplexNumber c)
  • explicit operator ComplexNumber(double d)
  • explicit operator double(ComplexNumber c)

 First we can have 1, 3 at the same time, or 2, 4 at the same time(complier error). Next, do we really want to convert a ComplexNumber object to a double? I saw some code around that returns the real part when converting, it is not the case in general. So I classify this as an invalid case and rule out 2, 4. Now the question is whether we want it to be explicit or implicit. Since there is no point to do explicitly, such as (ComplexNumber)d, we just keep the implicit version.

 

 Again, we test it

 

            Console.WriteLine("explicit cast:" + ((ComplexNumber)3.0));
            Console.WriteLine("neg=" + (-a));
            Console.WriteLine("sum=" + (a + b));
            Console.WriteLine("diff=" + (a - b));
            Console.WriteLine("prod=" + (a * b));
            Console.WriteLine("divi=" + (a / b));
            Console.WriteLine("add=" + (a + 3.5));
            Console.WriteLine("add=" + (3.5 + a));
            Console.WriteLine("minus=" + (a - 3.5));
            Console.WriteLine("minus=" + (3.5 - a));
            Console.WriteLine("multi=" + (a * 2));
            Console.WriteLine("multi=" + (2 * a));
            Console.WriteLine("div=" + (a / 2));
            Console.WriteLine("div=" + (2 / a));

 

At this point, we are done with the ComplexNumber struct. Let's now see how useful this is.

 

The first case we are interested in is the array of ComplexNumber. Our testing shows this approach is not performant well. Here is the testing code.

 

            // initialize data
            ComplexNumber[] cnums = new ComplexNumber[1000000];
            double[] re = new double[1000000];
            double[] im = new double[1000000];
            for (int i = 0; i < cnums.Length; i++)
            {
                cnums[i] = new ComplexNumber(i + 3, i + 7);
                re[i] = i + 3;
                im[i] = i + 7;
            }

            ComplexNumber sum = new ComplexNumber(0, 0);
            DateTime startTime = DateTime.Now;
            for (int i = 0; i < cnums.Length; i++)
            {
                sum += cnums[i];
            }
            DateTime stopTime = DateTime.Now;
            TimeSpan duration = stopTime - startTime;
            Console.WriteLine(duration);

            double resum = 0, imsum = 0;
            startTime = DateTime.Now;
            for (int i = 0; i < cnums.Length; i++)
            {
                resum += re[i];
                imsum += im[i];
            }
            stopTime = DateTime.Now;
            duration = stopTime - startTime;
            Console.WriteLine(duration);

 

We initialize a million complex numbers and then take the sum. We compare this method with the approach where we use two double arrays. The result is this:

 

00:00:00.0312502
00:00:00.0156251

 

The difference is 100%. Though individual object is working fine, the composition of an array of the same struct works poorly when the size of the array is large.

 

The second case is matrix of complex numbers. Ideally we want to have something like this:

 

Matrix<double> md = new Matrix<double>(100, 200);

Matrix<ComplexNumber> mc = new Matrix<ComplexNumber>(100, 200);

 

and then carry out whatever operation we need, such as LU decomposition. Since the LU decomposition is the same for both real number and complex number, we would like to write one piece of code for the decomposition, no matter what the underlying number system is. However, in C#, there is no super class/struct/interface for operators(+, -, *, /), we can't code something like this

 

public class Matrix<T> { ... }

 

to work with all number systems.

 

Here we show two cases where C# (as well as Java) and other strong types languages don't work well with numerical computing which demands high performance. C++ can work out through weak typed templates, but the error message is very ugly to digest if we pass in a wrong object.

 

The lack of support from the language level suggests that it is not appropriate to abstract the details at the number system level. Instead, we should abstract at a higher level in the application (e.g., at the array of complex numbers in the first case).

 

In the object oriented world, we do have a lot of cases where we need a lot of small objects, especially when we build a foundation for a certain application, one good book is Analysis Patterns written by Martin Fowler. Efficient composition of these small objects is crucial to successful usage of an object oriented language.

 

 A side note is that once we create the new complex number, we need to create corresponding functions, such as complex versions of sin, cos, etc. Though I don't foresee any problem with it, it's pretty tedious. By the way, the way that Java and C# build Math libs is not appropriate massive composition, but this is a different topics.