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 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 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.
"""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.
"""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 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 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.