Tutorial

Complex numbers

Choose ExactComplex for arithmetic on exact rational components, or Complex for rounded components and elementary functions. For uncertain components with guaranteed bounds, use ComplexBall.

Use Complex for complex arithmetic with a precision you choose for each Float component. Arithmetic rounds each component once. Multiplication, for example, rounds ac - bd and ad + bc without first rounding the four products.

Values and components

Construct a complex number from real numeric components or from text such as "3-4j". A ComplexContext sets separate precisions, rounding modes, and traps for the two parts. An ArithmeticContext applies the same settings to both. The component formats must share exponent bounds.

complex_values.mojo Download
"""Complex values with independent component formats."""

from apn_mojo import (
    Complex,
    ComplexContext,
    FloatFormat,
    ArithmeticContext,
    Rational,
    RoundingMode,
)


def main() raises:
    var z = Complex(3, -4)
    print(z)
    print(z.conjugate())
    var part = z.imag()
    part += 1
    print(z.imag(), part)
    var settings = ComplexContext(
        real=ArithmeticContext(format=FloatFormat(3)),
        imag=ArithmeticContext(
            format=FloatFormat(5), rounding=RoundingMode.toward_positive
        ),
    )
    var fraction = Complex(
        Rational(1, 3), Rational(-1, 3), context=settings
    )
    print(fraction)
    var saved = fraction
    fraction = Complex(fraction, context=ArithmeticContext(format=FloatFormat(256)))
    print(fraction == saved, fraction.real_format().precision())

Run from the repository root pixi run mojo run -I src docs/examples/complex_values.mojo

Output

Complex(3.0, -4.0)
Complex(3.0, 4.0)
-4.0 -3.0
Complex(0.3, -0.33)
True 256

Components print as Floats do, in decimal: Complex(3.0, -4.0). Use real() and imag() to read the components. Each returns an independent value that you can change without affecting the complex number.

Arithmetic

You can mix complex numbers with integers, rationals, and floats in either operand order. Real operands retain their meaning: adding a real zero to a complex number preserves the imaginary component, including its zero sign.

complex_arithmetic.mojo Download
"""Complex arithmetic rounded per component."""

from apn_mojo import (
    Complex,
    Rational,
    ArithmeticContext,
    FloatFormat,
    divide,
)


def main() raises:
    var z = Complex(3, 4)
    print(z * Complex(1, -2))
    print(25 / z)
    print(z + Rational(1, 2))
    var saved = z
    z *= z
    print(z)
    print(saved)
    var narrow = ArithmeticContext(format=FloatFormat(8))
    var result = divide(saved, 3, context=narrow)
    print(result)

Run from the repository root pixi run mojo run -I src docs/examples/complex_arithmetic.mojo

Output

Complex(11.0, -2.0)
Complex(3.0, -4.0)
Complex(3.5, 4.0)
Complex(-7.0, 24.0)
Complex(3.0, 4.0)
Complex(1.0, 1.336)

When the result fits the destination formats, (3+4j) * (1-2j) is exactly 11-2j, and 25 / (3+4j) is 3-4j. Division rounds each quotient component once, including the denominator's contribution. z *= z keeps the formats of z; a named function with context= can choose other formats.

Magnitudes and square roots

abs(z) returns the magnitude, and norm_sqr(z) returns its square. Both return a Float. Use sqrt(z) for the principal square root.

complex_functions.mojo Download
"""Magnitudes and square roots."""

from apn_mojo import (
    Complex,
    Float,
    FloatFormat,
    ArithmeticContext,
    abs,
    norm_sqr,
    sqrt,
)


def main() raises:
    var z = Complex(3, -4)
    print(norm_sqr(z))
    print(abs(z))
    print(sqrt(z))
    print(sqrt(Complex(-4, Float.zero())))
    print(sqrt(Complex(-4, Float.zero(negative=True))))
    var result = sqrt(
        Complex(1, 2),
        context=ArithmeticContext(format=FloatFormat(8)),
    )
    print(result)

Run from the repository root pixi run mojo run -I src docs/examples/complex_functions.mojo

Output

25.0
5.0
Complex(2.0, -1.0)
Complex(0.0, 2.0)
Complex(0.0, -2.0)
Complex(1.27, 0.785)

These functions account for both components before rounding. On the negative real axis, the imaginary zero's sign selects the side of the branch cut: sqrt(-4+0j) is 2j, and sqrt(-4-0j) is -2j.

Powers and phase cycling

z ** n and pow_int(z, n) accept positive, zero, and negative integer exponents, including exponents too large for a native integer.

complex_powers.mojo Download
"""Integral powers."""

from apn_mojo import Complex, Integer, pow_int


def main() raises:
    var z = Complex(2, 3)
    print(z**2)
    print(z**3)
    print(pow_int(Complex(1, 1), -2))
    var turns = (Integer(1) << 256) + 1
    print(Complex(0, 1) ** turns)
    var saved = z
    z **= 2
    print(z)
    print(saved)

Run from the repository root pixi run mojo run -I src docs/examples/complex_powers.mojo

Output

Complex(-5.0, 12.0)
Complex(-46.0, 9.0)
Complex(0.0, -0.5)
Complex(0.0, 1.0)
Complex(-5.0, 12.0)
Complex(2.0, 3.0)

For example, (1+1j) ** -2 is exactly -0.5j when the format can hold it. Powers of the imaginary unit repeat every four steps, so even a huge exponent can be inexpensive. Other powers may need more work and storage.

Elementary functions

exp, log, trigonometric and hyperbolic functions, their inverses, and general pow accept Complex inputs. Each result component rounds once. Use a context to choose the destination precision, just as for arithmetic.

complex_elementary.mojo Download
"""Complex exponentials and the principal logarithm."""

from apn_mojo import ArithmeticContext, Complex, FloatFormat, exp, log


def main() raises:
    var context = ArithmeticContext(format=FloatFormat(128))
    var exponential = exp(Complex(1, 1), context=context)
    print("exp(1 + i), real and imaginary:",
          exponential.real().to_string(digits=12), exponential.imag().to_string(digits=12))
    var logarithm = log(Complex(-2), context=context)
    print("principal log(-2), real and imaginary:",
          logarithm.real().to_string(digits=12), logarithm.imag().to_string(digits=12))

Run from the repository root pixi run mojo run -I src docs/examples/complex_elementary.mojo

Output

exp(1 + i), real and imaginary: 1.46869393992 2.28735528718
principal log(-2), real and imaginary: 0.693147180560 3.14159265359

The logarithm in the example is the principal value: log(Complex(-2)) has real part log(2) and imaginary part pi. Branch cuts matter when moving between these functions and their inverses; see the branch conventions. Real Float functions keep their real domain: explicitly construct a Complex when a calculation needs a complex result.

Keep complex fractions exact

ExactComplex stores two Rationals. Arithmetic and integer powers stay exact; division by zero raises. sqrt_exact returns an Optional value, succeeding only when both components of the principal square root are rational.

exact_complex_values.mojo Download
"""Exact complex fractions, square roots and conversion."""

from apn_mojo import ArithmeticContext, ComplexContext, ExactComplex, FloatFormat, Rational
from apn_mojo.exact_complex import sqrt_exact


def main() raises:
    var z = ExactComplex(Rational(1, 3), Rational(2, 3))
    print("square:", z * z)
    print("quotient:", z / z)
    print("JSON round trip:", ExactComplex.from_json(z.to_json()) == z)
    var root = sqrt_exact(ExactComplex(3, 4))
    if root:
        print("exact root:", root.value())
    print("sqrt(i) has rational parts:", Bool(sqrt_exact(ExactComplex(0, 1))))
    var context = ComplexContext(ArithmeticContext(format=FloatFormat.binary64()))
    print("rounded components:", z.to_complex(context=context))

Run from the repository root pixi run mojo run -I src docs/examples/exact_complex_values.mojo

Output

square: ExactComplex(-1/3, 4/9)
quotient: ExactComplex(1, 0)
JSON round trip: True
exact root: ExactComplex(2, 1)
sqrt(i) has rational parts: False
rounded components: Complex(0.3333333333333333, 0.6666666666666666)

Call to_complex(context=...) when you are ready to round the components or use elementary functions. Text, JSON, and dictionary keys preserve exact values. ExactComplex currently has no Batch or vmap support; convert the elements to Complex or ComplexBall before constructing those batches. See the ExactComplex reference for the full API.

See the Complex reference for special values and conversion rules, and the architecture chapter for the algorithms that establish correct rounding.