Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

std.complex

from std.complex import Complex provides complex numbers with f64 real and imaginary parts. The module is ordinary embedded Plenty: arithmetic is written with operator hooks and the functions use the f64 numeric methods. Nothing in it allocates or fails.

from std.complex import Complex, from_polar

def main() -> Result[(), IoError | AllocError | FormatError]:
    a = Complex(1.5, 2.0)
    b = Complex(-0.5, 4.0)
    print(f"{a + b} {a - b} {a * b} {-a}"?)?
    print(f"{a / b}"?)?
    print(f"{b.conj()} {Complex(3.0, 4.0).abs()} {Complex(3.0, 4.0).norm()}"?)?
    print(a == Complex(1.5, 2.0), a != b, a.re, a.im)?
    print(a)?
    Ok(())
1.0+6.0i 2.0-2.0i -8.75+5.0i -1.5-2.0i
0.4461538461538462-0.4307692307692308i
-0.5-4.0i 5.0 25.0
True True 1.5 2.0
1.5+2.0i

Complex(re, im) is the generated constructor, and re and im are public fields. Complex is a copy class: assignment, by-value argument passing, return, and collection insertion copy its two f64 parts and leave the source usable. Copying never allocates. Operators and methods borrow their operands, which reads the parts in place instead of copying them into the call. There are no in-place hooks; accumulate with total = total + term on a mut binding.

from std.complex import Complex

def double(z: Complex) -> Complex:
    z + z

def main() -> Result[(), AllocError | IoError]:
    a = Complex(1.0, 2.0)
    mut b = a
    b.im = -2.0
    zs = [a, b, double(a)]?
    print(a == zs[0], b == zs[1], zs[2].re, len(zs))?
    Ok(())
True True 2.0 3

Operations

OperationResultContract
a + b, a - b, a * b, a / bComplexArithmetic on two Complex operands
-aComplexNegate both parts
a == b, a != bboolStructural: both parts compare as f64
z.scale(factor: f64)ComplexMultiply both parts by a real factor
z.conj()ComplexComplex conjugate
z.abs()f64Magnitude, scaled like hypot
z.norm()f64Squared magnitude re * re + im * im
z.arg()f64Phase angle im.atan2(re), in [-pi, pi]
z.to_polar()(f64, f64)(z.abs(), z.arg())
from_polar(r: f64, theta: f64)ComplexComplex(r * cos(theta), r * sin(theta))
z.exp()ComplexComplex exponential
z.log()ComplexPrincipal natural logarithm: abs().log() + arg() i
z.sqrt()ComplexPrincipal square root: nonnegative real part
z.pow(exponent: &Complex)Complexz.powi(n) for a real integral exponent n with abs(n) <= 100; otherwise the principal value of exp(exponent * log(z))
z.powi(n: i64)Complexz multiplied by itself n times by repeated squaring; a negative n gives 1 / z.powi(-n)

Both operands of an operator must be Complex; there are no mixed operations with f64. Operator hooks dispatch on the left operand’s type and cannot be overloaded, so z * 2.0 cannot share __mul__ with z * w. Use z.scale(2.0) for a real factor, or Complex(2.0, 0.0) as an operand. scale is also the more exact choice for non-finite values: `Complex(inf, 1.0)

  • Complex(2.0, 0.0)computesinf * 0.0for its imaginary part and gives NaN, whilescale(2.0)givesinf+2.0i`.

Multiplication uses the textbook formula. Division uses Smith’s algorithm, which scales by the larger divisor part so that c * c + d * d never overflows or underflows. abs scales the same way, so Complex(1e300, 1e300).abs() is finite. norm does not scale and overflows first.

from std.complex import Complex, from_polar
import std.math as math

def main() -> Result[(), IoError | AllocError | FormatError | math.MathError]:
    z = from_polar(2.0, 0.5)
    r, theta = z.to_polar()
    print(math.isclose(r, 2.0, 1e-15, 0.0)?, math.isclose(theta, 0.5, 1e-15, 0.0)?)?
    print(f"{Complex(-4.0, 0.0).sqrt()} {Complex(3.0, 4.0).sqrt()}"?)?
    print(f"{Complex(-1.0, 0.0).log()}"?)?
    i = Complex(0.0, 1.0)
    print(f"{i.pow(i)} {Complex(0.0, 0.0).pow(Complex(2.0, 0.0))}"?)?
    print(f"{Complex(1.0, 1.0).pow(Complex(2.0, 0.0))} {Complex(1.0, 1.0).powi(-2)}"?)?
    print(f"{Complex(0.0, f64.PI).exp()}"?)?
    Ok(())
True True
0.0+2.0i 2.0+1.0i
0.0+3.141592653589793i
0.20787957635076193+0.0i 0.0+0.0i
0.0+2.0i 0.0-0.5i
-1.0+1.2246467991473532e-16i

The functions are computed from f64 primitives, so results carry their rounding: exp(pi i) has a tiny imaginary part. Compare results with a tolerance, for example math.isclose on each part.

pow uses the method of Python’s complex.__pow__. A real integral exponent n with abs(n) <= 100 uses powi, which multiplies by repeated squaring, so small powers of exactly representable values are exact: (1+1i).pow(2) is 0+2i. Each squaring can roughly double the relative error, so larger or non-integral exponents use exp(exponent * log(z)) and inherit the rounding of exp and log. A zero exponent gives 1+0i, including for a NaN base, as f64.pow does. A zero base gives 0+0i for a real positive exponent; otherwise it follows f64 division and log, so 0.pow(-2) is inf+NaNi rather than an error. Call powi directly for an i64 exponent of any size, including i64.MIN. Its negative-exponent magnitude is formed without signed overflow; the result remains an infallible Complex under checked defaults.

Floating-point behavior

Complex arithmetic follows f64 arithmetic: it never traps and never returns Result. Overflow gives infinity and invalid operations give NaN, which then propagates. This matches the / operator on f64 and the numeric methods, and lets operators compose. It differs from the std.math functions, which report domain and overflow errors; check parts with math.isfinite when a result must be finite.

Dividing by Complex(0.0, 0.0) divides each part of the numerator by the real zero, exactly as f64 division does: (1+2i) / 0 is inf+infi, and 0 / 0 is NaN+NaNi. Python raises ZeroDivisionError instead.

Signed zero is preserved where the formulas define it. log of zero is -inf plus the zero’s arg, and sqrt of a signed zero keeps the sign of the imaginary part. Complex(-1.0, 0.0).arg() is pi and Complex(-1.0, -0.0).arg() is -pi, so the branch cut of log, sqrt and pow along the negative real axis follows the sign of the imaginary zero. abs returns infinity when either part is infinite, even if the other is NaN.

from std.complex import Complex

def main() -> Result[(), IoError | AllocError | FormatError]:
    print(f"{Complex(1.0, 2.0) / Complex(0.0, 0.0)} {Complex(0.0, 0.0) / Complex(0.0, 0.0)}"?)?
    print(f"{Complex(-4.0, 0.0).sqrt()} {Complex(-4.0, -0.0).sqrt()}"?)?
    print(f"{Complex(0.0, 0.0).log()} {Complex(f64.INF, f64.NAN).abs()}"?)?
    print(Complex(0.0, 0.0) == Complex(-0.0, 0.0), Complex(f64.NAN, 0.0) == Complex(f64.NAN, 0.0))?
    Ok(())
inf+infi NaN+NaNi
0.0+2.0i 0.0-2.0i
-inf+0.0i inf
True False

Equality is the language’s structural class equality, not a hook: 0.0 equals -0.0, and a NaN part makes values unequal, as for f64.

Formatting

Complex has a public display method, so print, f-strings and std.fmt.to_string write re+imi or re-imi, each part in the f64 print spelling: 1.5+2.0i, 1.0-0.5i, NaN+infi. A negative zero imaginary part is written -0.0i. It has no debug method, so str.repr, !r and list elements use the structural spelling std.complex.Complex(re=1.5, im=2.0). A format spec applies to the whole text; format the parts directly, as in f"{z.re:.3f}{z.im:+.3f}i", to control precision.

Acknowledgements