Numbers · Lesson 16.12

Complex

Source
Do arithmetic on complex numbers with the Math package's Complex and its four operators, read one as a length and an angle with Abs and Arg, build one with FromPolar, and solve a quadratic whose roots are not real.

Math ended with Sqrt(-1) giving NaN: no real number squares to a negative one. That is why x * x + 2 * x + 5 = 0 has no real solution, and why a program that solves quadratics has to stop there, or work the answer out by hand. Complex numbers add the missing square root, i, with i * i = -1, and with it every quadratic has its two roots.

They are more than a trick for quadratics. A complex number is also a point in the plane, and multiplying by one turns and stretches — which is why electrical engineering, signal processing and graphics use them all the time.

import Math::{ Complex, DegToRad, Pi, RadToDeg };

A pair of floats

A Complex is real + imaginary·i, held as two public float64 fields, real and imaginary. Complex::New makes one, and Complex32 is the same over float32.

The type defines the four operators, so complex arithmetic reads like any other:

let a = Complex::New(3.0, 4.0);
let b = Complex::New(1.0, -2.0);
Show("a + b", a + b);
Show("a * b", a * b);
Show("a / b", a / b);

Multiplying works out the brackets and uses i * i = -1: (3 + 4i)(1 - 2i) = 3 - 6i + 4i + 8 = 11 - 2i. Both sides of an operator must be Complex; to multiply by a plain float, use Scale.

MethodGivesFor 3 + 4i
Conjugate()the imaginary part's sign flipped3 - 4i
Negate()both signs flipped-3 - 4i
Scale(2.0)both parts times a float6 + 8i
Abs()the magnitude: the length of the arrow5
Norm()the magnitude squared25
Arg()the angle, in radians, between −π and π0.927…

A Complex does not implement Display, so the program has a Show function that prints a + bi or a - bi with three decimals.

Length and angle

Draw 3 + 4i as an arrow from the origin to the point (3, 4). Its length is Abs(), and its direction is Arg(), the angle from the positive real axis, in radians:

PrintLine("{:<14} {:.3}", "magnitude", a.Abs());
PrintLine("{:<14} {:.3} degrees", "angle", RadToDeg(a.Arg()));

That is the polar form: 5 long, at about 53.13°. FromPolar(length, angle) goes the other way, so FromPolar(2.0, DegToRad(60.0)) is 1 + 1.732i.

The polar form explains multiplication. Multiplying two complex numbers multiplies their lengths and adds their angles. i is 1 long at 90°, so multiplying by i turns a number a quarter turn anticlockwise and leaves its length alone:

flowchart LR
    a["`3 + 4i`<br/>length 5, 53.13°"] -- "times `i`<br/>length 1, 90°" --> t["`-4 + 3i`<br/>length 5, 143.13°"]

Solving a quadratic

The quadratic formula needs the square root of the discriminant, q * q - 4 * p * r. For x² + 2x + 5 that is -16, and the real Sqrt gives NaN. Made complex, -16 has the square root 4i:

let root = Complex::New(q * q - 4.0 * p * r, 0.0).Sqrt();
let first = (Complex::New(-q, 0.0) + root).Scale(1.0 / (2.0 * p));
let second = (Complex::New(-q, 0.0) - root).Scale(1.0 / (2.0 * p));

The roots are -1 + 2i and -1 - 2i. They come as a conjugate pair, as the roots of every quadratic with real coefficients do. Putting one back into the equation gives zero.

Sqrt, like Exp, Log and Pow, returns the principal value: of the two square roots of -16, 4i and -4i, it picks the one with a positive real part, or on the imaginary axis the one above zero. That is why the formula adds and subtracts it.

Rounding still applies

Euler's identity says e^(iπ) = -1, and Exp agrees — almost. The imaginary part comes out as about 1.22e-16, because Pi is only the nearest float64 to π. Every float caveat from Math carries over: compare complex results part by part, within a tolerance.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// No real number squares to -1, so `x * x + 1 = 0` has no real solution. Complex numbers add
// one: `i`, with `i * i = -1`. A complex number is a pair, `real + imaginary·i`, and with it
// every quadratic has its two roots, and a rotation in the plane becomes a multiplication.
//
// The Math package's `Complex` holds the pair as two `float64` fields, `real` and `imaginary`,
// and gives it the four operators, so complex arithmetic is written as ordinary arithmetic.
// (`Complex32` is the same over `float32`.)
//
// The pair can also be read as a point in the plane, or as an arrow from the origin to it. The
// arrow's length is the magnitude, `Abs()`, and its direction is the angle, `Arg()`, measured in
// radians from the positive real axis. `FromPolar` builds a number from those two instead.
import Io::PrintLine;
import Math::{ Complex, DegToRad, Pi, RadToDeg };

// A Complex does not implement Display, so `PrintLine("{}", a)` is refused:
//
//     error: no matching overload for 'PrintLine' with argument types (char8[..], Complex)
//
// This writes it as `a + bi` or `a - bi` instead.
func Show(label: char8[..], z: Complex) {
    if z.imaginary < 0.0 {
        PrintLine("{:<14} {:.3} - {:.3}i", label, z.real, -z.imaginary);
    } else {
        PrintLine("{:<14} {:.3} + {:.3}i", label, z.real, z.imaginary);
    }
}

func Main() -> int {
    // Arithmetic. Multiplying uses i·i = -1: (3 + 4i)(1 - 2i) = 3 - 6i + 4i + 8 = 11 - 2i.
    let a = Complex::New(3.0, 4.0);
    let b = Complex::New(1.0, -2.0);
    Show("a + b", a + b);
    Show("a * b", a * b);
    Show("a / b", a / b);
    Show("conjugate a", a.Conjugate());

    // The operators take a Complex on both sides; a plain float is refused:
    //
    //     error: operator '*' cannot combine left operand 'Complex' with right operand 'float64'
    //
    // `Scale` multiplies by a real number instead.
    Show("a scaled by 2", a.Scale(2.0));

    // Polar form: 3 + 4i is 5 long and points about 53 degrees above the real axis.
    PrintLine("{:<14} {:.3}", "magnitude", a.Abs());
    PrintLine("{:<14} {:.3} degrees", "angle", RadToDeg(a.Arg()));

    // Multiplying by i turns a number a quarter turn anticlockwise, keeping its length.
    let i = Complex::New(0.0, 1.0);
    let turned = a * i;
    Show("a * i", turned);
    PrintLine("{:<14} {:.3}, {:.3} degrees", "turned polar", turned.Abs(), RadToDeg(turned.Arg()));

    // `FromPolar` goes the other way: length 2 at 60 degrees.
    Show("polar 2, 60", Complex::FromPolar(2.0, DegToRad(60.0)));

    // x^2 + 2x + 5 = 0. The discriminant, 4 - 20, is negative, so the real square root would be
    // NaN; the complex one is 4i, and the two roots are -1 + 2i and -1 - 2i.
    let p = 1.0;
    let q = 2.0;
    let r = 5.0;
    let root = Complex::New(q * q - 4.0 * p * r, 0.0).Sqrt();
    let first = (Complex::New(-q, 0.0) + root).Scale(1.0 / (2.0 * p));
    let second = (Complex::New(-q, 0.0) - root).Scale(1.0 / (2.0 * p));
    Show("first root", first);
    Show("second root", second);

    // Putting a root back in gives zero, as far as floats allow.
    let check = first * first + first.Scale(q) + Complex::New(r, 0.0);
    Show("check", check);

    // Euler's identity, e^(i·pi) = -1. The imaginary part comes out as about 1.2e-16, not 0,
    // because `Pi` is only the nearest float to pi; at three places it shows as 0.000.
    let euler = Complex::New(0.0, Pi).Exp();
    Show("e^(i pi)", euler);
    PrintLine("{:<14} {}", "imaginary part", euler.imaginary);
    return 0;
}

Besides Io, its Rux.toml lists Math under [Dependencies].

Run it

cd Examples/Numbers/Complex
rux run
a + b          4.000 + 2.000i
a * b          11.000 - 2.000i
a / b          -1.000 + 2.000i
conjugate a    3.000 - 4.000i
a scaled by 2  6.000 + 8.000i
magnitude      5.000
angle          53.130 degrees
a * i          -4.000 + 3.000i
turned polar   5.000, 143.130 degrees
polar 2, 60    1.000 + 1.732i
first root     -1.000 + 2.000i
second root    -1.000 - 2.000i
check          0.000 + 0.000i
e^(i pi)       -1.000 + 0.000i
imaginary part 1.2246467991473532e-16

Common mistakes

Try it yourself

  1. Solve x² - 2x + 5 = 0 and x² - 5x + 6 = 0. What does the second give for the imaginary parts?
  2. Multiply a by i four times in a loop, printing each result. Where does it end up?
  3. Print the twelve points FromPolar(1.0, DegToRad(30.0 * k)) for k from 0 to 11 — the twelve hours of a clock face.
  4. Write Close(left: Complex, right: Complex) -> bool, true when both parts differ by less than 1e-9, and use it to check that euler is -1.

Learn more