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
| Operation | Result | Contract |
|---|---|---|
a + b, a - b, a * b, a / b | Complex | Arithmetic on two Complex operands |
-a | Complex | Negate both parts |
a == b, a != b | bool | Structural: both parts compare as f64 |
z.scale(factor: f64) | Complex | Multiply both parts by a real factor |
z.conj() | Complex | Complex conjugate |
z.abs() | f64 | Magnitude, scaled like hypot |
z.norm() | f64 | Squared magnitude re * re + im * im |
z.arg() | f64 | Phase angle im.atan2(re), in [-pi, pi] |
z.to_polar() | (f64, f64) | (z.abs(), z.arg()) |
from_polar(r: f64, theta: f64) | Complex | Complex(r * cos(theta), r * sin(theta)) |
z.exp() | Complex | Complex exponential |
z.log() | Complex | Principal natural logarithm: abs().log() + arg() i |
z.sqrt() | Complex | Principal square root: nonnegative real part |
z.pow(exponent: &Complex) | Complex | z.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) | Complex | z 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
- API modelled on Python’s
complexandcmath;normfollows C++std::norm. - Division uses Robert L. Smith’s Algorithm 116: Complex division (Communications of the ACM, 1962).