Repository navigation
Arithmetic Operations
Arithmetic on complex numbers is, ironically, not very complex.
To add two complex numbers, we simply add the real part to the real part and the imaginary part to the imaginary part. This should hopefully make sense, as since addition is commutative and associative, (a+b*i)+(c+d*i) = a+b*i+c+d*i = a+c+b*i+d*i = a+c + (b+d)i. To subtract two complex numbers, we simply subtract the real part from the real part and the imaginary part from the imaginary part. This should also make sense, since (a+b*i)-(c+d*i) = a+b*i-c-d*i = a-c+b*i-d*i = a-c+(b-d)i. In fact, addition and subtraction work the exact same on complex numbers as they do on vectors, the only difference being instead of x and y we have real and imaginary.
Multiplication between two complex numbers isn't quite as simple, but it's still very easy to compute. Complex numbers obey the distributive property just as well as real numbers, so if we want to solve (a+bi)*(c+di), we can just use FOIL: (a+bi)*(c+di) = ac+adi+bci+bdi² = ac+adi+bci+bd(-1) = ac+(ad+bc)i-bd = (ac-bd) + (ad+bc)i. Now, at first, this might not seem as neat as addition and subtraction. Plus and minus has a nice geometric interpretation involving vectors, while multiplication just seems to multiply components in a random order, right? Well, actually, this isn't the whole story. First, we rewrite a+bi and c+di in polar notation as r1∠θ1 and r2∠θ2, where r1 and r2 are the absolute values of the first and second complex number (their Pythagorean distances from 0), and θ1 and θ2 are their arguments (their angle measured counterclockwise from the positive real axis). If we convert (a+bi)*(c+di) into polar notation, we find the answer is r1*r2 ∠ (θ1+θ2). That's right, their distances from the center multiply, while their angles add. Which, in my opinion at least, is much more interesting than what we got for addition or subtraction. Also, as to be expected, if we multiply a complex number a+bi by a real number c, we get a*c + b*ci, which is ultimately the same as scaling up a+bi by c. In that sense, multiplying by a real number is analogous to a multiplying a vector by a scalar. This also follows our definition, as the absolute value multiplies by c, and the angle adds 0. Or, if c was negative, the absolute value multiplies by -c, and the angle adds π radians / 180° (the angle of a negative number).
Division between two complex numbers is quite a bit more complicated than multiplication. To compute (a+bi)/(c+di), we first have to multiply by (c-di)/(c-di), yielding (a+bi)(c-di)/((c+di)(c-di)) = (a+bi)(c-di)/(c²+d²). In other words, dividing by a complex number is the same as multiplying by its complex conjugate, then dividing by the square of its absolute value (AKA its "absolute square"). Just like how multiplying by a real number is like multiplying a vector by a scalar, dividing by one is like dividing a vector by a scalar. We simply scale down the magnitude by the real number, in this case that real number is the absolute square. Likewise, if we were to represent this in polar notation, we'd find that (r1∠θ1) / (r2∠θ2) = r1/r2 ∠ (θ1-θ2). That's right, their distances from the center divide and their angles subtract.
This library comes with a variety of arithmetic operations.
If the operation ends in "eq", that means it applies the operation to whichever Complex instance this was applied on. For instance, the statement:
z.addeq(new Complex(1,1))
is equivalent to:
z += 1+i
On the other hand, if the operation doesn't end in "eq", then the inputs are completely unaffected by the operation and the result is recorded on a new instance of the Complex class. So the statement:
Complex z2 = z1.add(new Complex(1,1))
is equivalent to:
z2 = z1+1+i.
So, in this sense, addeq is equivalent to +=, while add is equivalent to +.
add - returns the sum of two numbers
sub - returns the difference between two numbers
mul - returns the product between two numbers
div - returns the quotient between two numbers
addeq - adds the second number to the first number and returns the sum
subeq - subtracts the second number from the first and returns the result
muleq - multiplies the first number by the second and returns the result
diveq - divides the first number by the second and returns the result
All of these functions have 4 variations in the complex class: one between a complex and a complex (instance.operation(complex)), one between a complex and a real (instance.operation(double)), one between a complex and 2 doubles which represent the real and imaginary parts of another complex number (instance.operation(double, double)), and one between a complex and an imaginary double (instance.operationI(double)). The fourth of those functions is special, as it ends with a capital i right before the left parenthesis, like so:
addI
subI
mulI
divI
addeqI
subeqI
muleqI
diveqI
All of these operations use very primitive implementations, except for the division based ones.
Addition and subtraction are extremely primitive, as they literally just involve adding/subtracting the components.
add: return new Complex(re+z.re, im+z.im);
sub: return new Complex(re-z.re, im-z.im);
addeq: re+=z.re; im+=z.im; return this;
subeq: re-=z.re; im-=z.im; return this;
As for multiplication, while the geometric interpretation involving multiplying the magnitudes and adding the angles is much more elegant and beautiful, that's not how it's implemented. Doing it that way would require converting two numbers to polar notation, then converting the result back to rectangular a+bi form. Which requires quite a bit of square roots and trigonometry, which is extremely inefficient. Hence, multiplication is instead implemented using the distributive property and FOILing everything out.
mul: return new Complex(re*z.re-im*z.im, re*z.im+im*z.re);
muleq: set(re*z.re-im*z.im, re*z.im+im*z.re); return this;
It's much faster this way and only requires addition, subtraction, and multiplication. The only downside is that it's a bit more prone to round-off errors than add and sub. Also, multiplication by a real or imaginary double is even easier, since there's only one component involved:
mul: return new Complex(re*a, im*a);
mulI: return new Complex(-im*a, re*a);
muleq: re*=a; im*=a; return this;
muleqI: set(-im*a, re*a); return this;
Finally, division. As I said, this one's a bit more complicated than the other ones, so we'll analyze this case by case. When dividing a Complex by a double, the computer first checks to see if the double is a denormal. If not, we simply calculate the reciprocal of the double and multiply each component by that reciprocal. If it is a denormal, however, the reciprocal overflows, and we instead have to divide each component by that double. The reason we don't just do the second one for all cases is that division isn't as computationally cheap as multiplication. As for division with an imaginary double, the same thing happens, but the components are swapped and one of them is negated.
When dividing a Complex by a Complex, the divide operation becomes even more complicated. We first have to compute the inverse of the divisor, then multiply that by the dividend. Computing the reciprocal of the divisor requires utilizing the inv function. Which, TLDR, is the complex conjugate divided by the absolute square, but with some special cases to protect against overflow and underflow.
The add, sub, mul, and div functions are also defined within the Cpx class, this time defined statically and with all inputs as parameters. All 4 of them have the form operation(Complex, Complex), operation(Complex, double), and operation(double, Complex). The third of those is unique to Cpx, and is helpful as it allows us to subtract double - Complex or to divide double / Complex.
Cpx also has variations of add and mul with unlimited inputs. Since addition and multiplication are commutative and associative, the library makes use of varargs to allow for an unlimited input add and mul function, so the user doesn't have to chain together a long series of adds and muls.
Both functions can take any number of inputs, provided there are at least 2. Only 0 or 1 of the inputs can be doubles, the rest have to be Complexes. If you want to add / multiply more doubles, simply have a parameter equal "a+b+c+..." or "a*b*c*...". As well, if there is a double input, it must be the first parameter. Unless there are 2 or 3 parameters, in which case it can also be the last parameter. This is because varargs in java have to be the last parameter.
The unlimited input add and multiply work simply by adding / multiplying the first 2 parameters, then repeatedly adding / multiplying that product by the remaining parameters.