# src/algolib/maths/complex/complex.py
"""
A lightweight Complex number implementation (without using Python's built-in ``complex``).
This module provides a small, well-documented ``Complex`` class suitable for learning
and algorithmic implementations. It supports algebraic form (a + bi) and polar form
(r·e^{iθ}), with common operations.
Notes
-----
- This class uses plain floats and is immutable.
- We intentionally avoid Python's built-in :class:`complex` to practice fundamentals.
Examples
--------
>>> z1 = Complex(3, 4)
>>> z1.modulus()
5.0
>>> z2 = Complex.from_polar(2, math.pi/2)
>>> (z1 + z2).re # doctest: +ELLIPSIS
3.0
>>> (z1.conjugate()).im
-4.0
"""
from __future__ import annotations
import math
from dataclasses import dataclass
from typing import Iterable, Tuple
from algolib.exceptions import InvalidTypeError, InvalidValueError
Number = float # for readability
[docs]
@dataclass(frozen=True)
class Complex:
"""
Complex number in algebraic form :math:`a + b \\mathrm{i}`.
Parameters
----------
re : float
Real part :math:`a`.
im : float
Imaginary part :math:`b`.
Raises
------
InvalidTypeError
If either part is not a real number (int/float).
"""
re: Number
im: Number
# ------------------------------- constructors -------------------------------
def __post_init__(self) -> None:
"""
Post-initialization validation and normalization.
Ensures that the real and imaginary parts are valid numbers (int or float)
and converts them to floats for consistency.
Raises
------
InvalidTypeError
If `re` or `im` is not a real number.
"""
if not isinstance(self.re, (int, float)) or not isinstance(self.im, (int, float)):
raise InvalidTypeError("re and im must be real numbers (int or float).")
# freeze as floats (even if ints given)
object.__setattr__(self, "re", float(self.re))
object.__setattr__(self, "im", float(self.im))
[docs]
@staticmethod
def from_polar(r: Number, theta: Number) -> "Complex":
r"""
Construct a complex number from polar coordinates.
Parameters
----------
r : float
The modulus (radius) of the complex number. Must be non-negative.
theta : float
The argument (angle in radians) of the complex number.
Returns
-------
Complex
The complex number corresponding to the polar coordinates.
Raises
------
InvalidTypeError
If `r` or `theta` is not a real number.
InvalidValueError
If `r` is negative.
Examples
--------
>>> z = Complex.from_polar(2, math.pi / 2)
>>> z
Complex(re=1.2246467991473532e-16, im=2.0)
"""
if not isinstance(r, (int, float)) or not isinstance(theta, (int, float)):
raise InvalidTypeError("r and theta must be real numbers (int or float).")
if r < 0:
raise InvalidValueError(f"radius r must be non-negative, got {r}")
return Complex(r * math.cos(theta), r * math.sin(theta))
[docs]
@staticmethod
def from_cartesian(re: Number, im: Number) -> "Complex":
"""
Construct a complex number from Cartesian coordinates.
Parameters
----------
re : float
The real part of the complex number.
im : float
The imaginary part of the complex number.
Returns
-------
Complex
The complex number corresponding to the Cartesian coordinates.
Examples
--------
>>> z = Complex.from_cartesian(3, 4)
>>> z
Complex(re=3.0, im=4.0)
"""
return Complex(re, im)
[docs]
@staticmethod
def from_iterable(pair: Iterable[Number]) -> "Complex":
r"""
Construct a complex number from an iterable of two numbers.
Parameters
----------
pair : Iterable[float]
An iterable containing exactly two elements: the real and imaginary parts.
Returns
-------
Complex
The complex number constructed from the iterable.
Raises
------
InvalidTypeError
If the iterable does not contain exactly two numeric elements.
Examples
--------
>>> z = Complex.from_iterable([3, 4])
>>> z
Complex(re=3.0, im=4.0)
"""
try:
re, im = pair # type: ignore[misc]
except Exception as e: # noqa: BLE001
raise InvalidTypeError("pair must be an iterable of length 2 (re, im).") from e
return Complex(re, im)
# ------------------------------- properties ---------------------------------
[docs]
def to_tuple(self) -> Tuple[Number, Number]:
r"""
Convert the complex number to a tuple of its real and imaginary parts.
Returns
-------
tuple of float
A tuple `(re, im)` representing the real and imaginary parts.
Examples
--------
>>> z = Complex(3, 4)
>>> z.to_tuple()
(3.0, 4.0)
"""
return (self.re, self.im)
# --------------------------------- queries ----------------------------------
[docs]
def modulus(self) -> float:
r"""
Compute the modulus (absolute value) of the complex number.
Returns
-------
float
The modulus :math:`\sqrt{a^2 + b^2}`.
Examples
--------
>>> z = Complex(3, 4)
>>> z.modulus()
5.0
"""
x, y = abs(self.re), abs(self.im)
if x < y:
x, y = y, x
if x == 0.0: # 避免 0/0
return 0.0
r = y / x
return x * (1.0 + r * r) ** 0.5
[docs]
def argument(self) -> float:
r"""
Compute the principal argument of the complex number.
Returns
-------
float
The argument (angle in radians) in the range :math:`(-\pi, \pi]`.
Examples
--------
>>> z = Complex(0, 1)
>>> z.argument()
1.5707963267948966
"""
return math.atan2(self.im, self.re)
[docs]
def conjugate(self) -> "Complex":
r"""
Compute the complex conjugate of the number.
Returns
-------
Complex
The conjugate :math:`a - b \mathrm{i}`.
Examples
--------
>>> z = Complex(3, 4)
>>> z.conjugate()
Complex(re=3.0, im=-4.0)
"""
return Complex(self.re, -self.im)
[docs]
def normalized(self) -> "Complex":
r"""
Normalize the complex number by :math:`z/|z|` to have a modulus of 1.
Returns
-------
Complex
The normalized complex number.
Raises
------
InvalidValueError
If the complex number is zero.
Examples
--------
>>> z = Complex(3, 4)
>>> z.normalized()
Complex(re=0.6, im=0.8)
"""
r = math.hypot(self.re, self.im)
if r == 0.0:
raise InvalidValueError("cannot normalize 0+0i.")
# 先标准化
x, y = self.re / r, self.im / r
# 计算(标准化后)的模长平方,理论应为 1,但会有舍入
m2 = x * x + y * y
# 用一次性的缩放把模长“微校准”到 1
# 注意:sqrt 和乘法的舍入会再带来极小误差,但会压到 ~1e-15 量级
if m2 != 1.0 and m2 > 0.0 and math.isfinite(m2):
adj = 1.0 / math.sqrt(m2)
x, y = x * adj, y * adj
return Complex(x, y)
# ---------------------------------- algebra ---------------------------------
def __add__(self, other: "Complex") -> "Complex":
"""
Add two complex numbers.
Parameters
----------
other : :class:`Complex`
The complex number to add.
Returns
-------
:class:`Complex`
The sum of the two complex numbers.
Raises
------
InvalidTypeError
If ``other`` is not :class:`Complex`.
Examples
--------
>>> z1 = Complex(3, 4)
>>> z2 = Complex(1, -1)
>>> z1 + z2
Complex(re=4.0, im=3.0)
"""
if not isinstance(other, Complex):
raise InvalidTypeError("other must be Complex.")
return Complex(self.re + other.re, self.im + other.im)
def __sub__(self, other: "Complex") -> "Complex":
"""
Subtract two complex numbers.
"""
if not isinstance(other, Complex):
raise InvalidTypeError("other must be Complex.")
return Complex(self.re - other.re, self.im - other.im)
def __mul__(self, other: "Complex") -> "Complex":
r"""
Multiply two complex numbers.
Formula
-------
:math:`(a+bi)(c+di) = (ac - bd) + (ad + bc)i`.
"""
if not isinstance(other, Complex):
raise InvalidTypeError("other must be Complex.")
a, b, c, d = self.re, self.im, other.re, other.im
return Complex(a * c - b * d, a * d + b * c)
def __truediv__(self, other: "Complex") -> "Complex":
r"""
Robust complex division using scaling + Smith's algorithm to avoid overflow/underflow.
For z = a+bi, w = c+di:
1) scale = max(|a|,|b|,|c|,|d|) ; divide all by scale (if scale>0)
2) use Smith's branch on the scaled values.
"""
if not isinstance(other, Complex):
raise InvalidTypeError("other must be Complex.")
a, b = self.re, self.im
c, d = other.re, other.im
if c == 0.0 and d == 0.0:
raise InvalidValueError("division by zero complex.")
# ---- 统一缩放,避免中间量溢出/下溢 ----
scale = max(abs(a), abs(b), abs(c), abs(d))
if scale > 0.0:
a /= scale;
b /= scale;
c /= scale;
d /= scale
# 若 scale==0,这里意味着 self==0 且 other!=0,直接走下面公式也安全
ac = abs(c)
ad = abs(d)
if ac >= ad:
# |c| >= |d|
t = d / c # |t| <= 1
denom = c + d * t # 数值上 ~ (c^2 + d^2)/c
re = (a + b * t) / denom
im = (b - a * t) / denom
else:
t = c / d # |t| <= 1
denom = d + c * t # 数值上 ~ (c^2 + d^2)/d
re = (a * t + b) / denom
im = (b * t - a) / denom
return Complex(re, im)
def __neg__(self) -> "Complex":
"""Unary minus."""
return Complex(-self.re, -self.im)
def __abs__(self) -> float:
"""Builtin ``abs(z)`` -> modulus."""
return self.modulus()
# ------------------------------- comparisons --------------------------------
[docs]
def almost_equal(self, other: "Complex", tol: float = 1e-12) -> bool:
"""
Return True if each component differs by at most ``tol`` (absolute).
This is safer than exact float equality.
"""
if not isinstance(other, Complex):
return False
return (abs(self.re - other.re) <= tol) and (abs(self.im - other.im) <= tol)
def __eq__(self, other: object) -> bool: # exact equality
if not isinstance(other, Complex):
return NotImplemented
return self.re == other.re and self.im == other.im
# --------------------------------- polar ------------------------------------
[docs]
def to_polar(self) -> Tuple[float, float]:
r"""
Convert the complex number to polar coordinates.
Returns
-------
tuple of float
A tuple ``(r, theta)`` where ``r`` is the modulus and ``theta`` is the argument.
Examples
--------
>>> z = Complex(3, 4)
>>> z.to_polar()
(5.0, 0.9272952180016122)
"""
return (self.modulus(), self.argument())
# --------------------------------- display ----------------------------------
def __repr__(self) -> str:
return f"Complex(re={self.re:.12g}, im={self.im:.12g})"
def __str__(self) -> str:
sign = "+" if self.im >= 0 else "-"
return f"{self.re:.12g} {sign} {abs(self.im):.12g}i"