Fast, integer-backed algebraic number types for exact arithmetic in imaginary quadratic integer rings.
-
complexint: a Gaussian integer type that mirrors Python’scomplex, but storesintcomponents (no floating-point drift). -
QuadInt/QuadraticRing: a general quadratic-integer implementation for elements of the form
$(a + b\sqrt{D}) / \mathrm{den}$ with$den ∈ {1,2}$ .- By default,
QuadraticRing(D)choosesden = 2whenD % 4 == 1, otherwiseden = 1(and you can override withQuadraticRing(D, den=1)/den=2to work in a non-default order).
- By default,
-
eisensteinint: Eisenstein integers in the ω-basis (a + bω, where$ω = (-1 + \sqrt{-3})/2$ ). -
dualint: dual integers of the forma + bεwhereε² = 0andε != 0. -
splitint: split-complex (hyperbolic) integers of the forma + bjwherej² = 1andj != 1.
Designed for discrete math, number theory tooling, and high-throughput exact computations (this project is built to compile cleanly with mypyc).
New helper methods on every quadratic integer value:
x.content()— largest positive integernsuch thatx = n*yin the same ring.x.factor_detail()— structured factorization asFactorization(unit, primes).x.factor()— a plain{prime_like_factor: exponent}mapping whose product is exactlyx.x.basis,x.basis_a,x.basis_b— public/user-facing basis coordinates, which may differ from the internal(a, b)numerator coordinates.
python -m pip install quadintfrom quadint import complexint
a = complexint(1, 2)
b = complexint(3, 6)
c = a * b
print(c) # "(-9+12j)" (exact, integer-backed)
print(c.real) # -9
print(c.imag) # 12
print(type(c.real)) # <class 'int'>
print(abs(a)) # 1^2 + 2^2 = 5 (norm)complexint is ideal when you want something that feels like complex, but with infinite-precision integer components.
Create a ring instance for a chosen discriminant parameter D, then construct values in that ring:
from quadint import QuadraticRing
Q2 = QuadraticRing(-2) # Z[√-2]
x = Q2(1, 2) # (1 + 2*sqrt(-2))
y = Q2(3, 6)
print(x * y) # "(-21+12*sqrt(-2))"
print(abs(x)) # norm: 1^2 - (-2)*2^2 = 9Common operations include +, -, *, ** (non-negative powers), conjugate(), and abs() (the norm).
from quadint.eisenstein import eisensteinint
z = eisensteinint(2, 3) # 2 + 3ω
w = eisensteinint(1, -1) # 1 - ω
print(z)
print(z * w) # exact product in Z[ω]
print(abs(z)) # norm (integer)Use real and omega to access the ω-basis components.
from quadint import dualint
z = dualint(2, 3) # 2 + 3ε
w = dualint(1, -1) # 1 - ε
print(z)
print(z * w) # (2+1ε)Use real and dual (or epsilon) to access the ε-basis components.
Split-complex (a.k.a. hyperbolic) integers behave like complexint, except the generator satisfies j² = 1 instead of j² = -1.
Unlike complex numbers, split-complex numbers have an indefinite norm and zero divisors (e.g. (1+j)*(1-j) == 0).
from quadint.split import splitint
z = splitint(1, 1) # 1 + 1j
w = splitint(1, -1) # 1 - 1j
print(z * w) # 0j (zero divisor behavior)- This package is primarily intended for exact, discrete arithmetic (
+,-,*,**, conjugation, norms). - Division helpers (
divmod,//,%,/) are implemented for the finite set of norm-Euclidean quadratic rings at the default/maximal denominator, and also for dual (D=0) and split-complex integers (D=1).- New: division is also available in selected Euclidean-but-not-norm-Euclidean real quadratic maximal orders via a Harper-style method (weighted Euclidean score + local quotient search). This currently covers:
D=14,22,23,31,43,46,47,53,59,61,62,67,71,77,83,86,89,93,94,97- and
D=69via a dedicated Clark-style Euclidean function implementation. - Without
cypari, Harper-style support is limited to the built-in hard-coded cases above (withD < 100); withcypariinstalled, additional admissible Harper-like cases may be discoverable.
- New: division is also available in selected Euclidean-but-not-norm-Euclidean real quadratic maximal orders via a Harper-style method (weighted Euclidean score + local quotient search). This currently covers:
- Factorization (
factor/factor_detail) is currently implemented for:complexint(D=-1, den=1),QuadraticRing(-2, den=1),eisensteinint(D=-3, den=2),- and the Heegner maximal orders for
D=-7andD=-11. Other rings may raiseNotImplementedError.
- Floats and Python
complexare accepted in some operations but are converted viaint(...), which truncates toward zero. If you care about rationals, avoid mixing infloat.
Example of truncation behavior:
from quadint import complexint
a = complexint(3, 6)
print(a / 3) # "(1+2j)"
print(a / 3.5) # "(1+2j)" (3.5 -> 3 by int(...) conversion)
print(a + 1) # "(4+6j)"
print(a + 1.5) # "(4+6j)" (1.5 -> 1)quadint can work with integral ideals of quadratic orders.
from quadint import QuadraticRing
O = QuadraticRing(-5)
I = O.ideal(3, O(1, 1))
assert not I.is_principal()
assert O.class_number == 2QuadInt separates public basis coordinates from the internal numerator coordinates used by the arithmetic engine.
Internally, every value is still stored as (a, b) numerators for
For most quadratic integer types, the public basis is the identity basis, so the coordinates you pass to the constructor are the same coordinates used internally. Subclasses can override that by defining conversion matrices:
BASIS_TO_INTERNALmaps constructor/user coordinates(x, y)into internal numerator coordinates(a, b).INTERNAL_TO_BASISandINTERNAL_TO_BASIS_DENmap internal numerator coordinates back to public basis coordinates.
This is mainly useful when the natural mathematical notation for a type is not the raw 1, √D basis.
Eisenstein integers are the motivating example. Users write them as a + bω, where QuadraticRing(-3) as
So eisensteinint(x, y) converts from the public ω-basis to the internal numerator basis as:
x + yω = ((2x - y) + y√-3) / 2
Example:
from quadint.eisenstein import eisensteinint
z = eisensteinint(2, 3)
print(z) # (2+3ω)
print(z.real) # 2
print(z.omega) # 3
print(z.basis) # (2, 3)
print(tuple(z)) # (2, 3)
# Internal numerator coordinates are still available, but usually only useful
# for implementing rings/subclasses or debugging low-level arithmetic.
print(z.a, z.b, z.ring.den) # 1 3 2Prefer basis, basis_a, basis_b, and type-specific aliases such as real / omega when presenting values to users. Prefer the internal .a and .b fields only when implementing arithmetic, division, factorization, or another low-level ring operation.
quadint.sums provides small number-theory helpers for decomposing primes and integers into non-negative integer solutions of:
x^2 + d*y^2 = n
The default d=1 gives the classic sum-of-two-squares problem.
from quadint.sums import decompose_prime, decompose_number
print(decompose_prime(19889))
# (17, 140)
print(decompose_number(19890))
# {(69, 123), (57, 129), (3, 141), (87, 111)}Use d to solve related forms:
from quadint.sums import decompose_prime, decompose_number
print(decompose_prime(19, d=3))
# (4, 1) because 4^2 + 3*1^2 == 19
print(decompose_number(12, d=3, no_trivial_solutions=False))
# {(0, 2), (3, 1)} because 0^2 + 3*2^2 == 12 and 3^2 + 3*1^2 == 12Return a non-negative pair (x, y) for a prime-like input where:
x^2 + d*y^2 = den^2 * p
For normal public use, leave den=1. Passing den=2 is mainly useful when working with denominator-2 quadratic orders, where the returned pair is in numerator coordinates.
from quadint.sums import decompose_prime
print(decompose_prime(5))
# (1, 2)
print(decompose_prime(7, d=3))
# (2, 1)
print(decompose_prime(2, d=7, den=2))
# (1, 1) because 1^2 + 7*1^2 == 2^2 * 2Return all canonical non-negative integer pairs (x, y) satisfying:
x^2 + d*y^2 = n
from quadint.sums import decompose_number
print(decompose_number(325, no_trivial_solutions=False))
# {(1, 18), (6, 17), (10, 15)}decompose_number accepts either an integer or a precomputed factorization dictionary:
from quadint.sums import decompose_number
print(decompose_number({2: 1, 3: 2, 5: 1, 13: 1, 17: 1}))
# same result as decompose_number(19890)Useful options:
d=1by default; use another positive integer forx^2 + d*y^2 = n.no_trivial_solutions=Trueby default; set it toFalseto include solutions with a zero coordinate and symmetricd=1solutions such as(0, 2)forn=4.check_count=Nreturns an empty set early when the predicted number of solutions is belowN.
Completeness is best-supported for the class-number-one Heegner values used by the package: d in {1, 2, 3, 7, 11, 19, 43, 67, 163}. Other d values may work, and results are still validated as true solutions, but completeness is not guaranteed.
There is also a small companion module for decomposing Eisenstein norms of the form:
a^2 - a*b + b^2 = n
This is mostly a fun helper built on the Eisenstein integer machinery rather than a central part of the package. It mirrors the main quadint.sums API:
from quadint.sums.eisenstein import decompose_prime, decompose_number
print(decompose_prime(7))
# (1, 3) # because 1^2 - 1*3 + 3^2 == 7
print(decompose_number(91, no_trivial_solutions=False))
# returns canonical pairs (a, b) with a^2 - a*b + b^2 == 91no_trivial_solutions=True filters the obvious square-like rays where a == 0, b == 0, or a == b. As with the rest of quadint.sums, a factorization dictionary may be passed instead of an integer.
complexint(a: int = 0, b: int = 0)eisensteinint(a: int = 0, b: int = 0)wherea + bωdualint(a: int = 0, b: int = 0)splitint(a: int = 0, b: int = 0)QuadraticRing(D: int = 0, den: int = None)- If
denis omitted (None), it defaults to2whenD % 4 == 1, otherwise1.
- If
Q(a: int = 0, b: int = 0) -> QuadInt(constructs using the ring’s internal basis)Q.from_ab(a: int, b: int) -> QuadInt(construct with user coords, respectingden)Q.from_obj(x) -> QuadInt(embedint/float, andcomplexonly whenD == -1)
x.conjugate()abs(x)(norm)x.units(finite torsion unit subgroup exposed as a tuple)x.content()x.factor_detail()(returnsFactorization(unit, primes))x.factor()(returns plaindict[QuadInt, int])divmod(x, y),x // y,x % y(where supported)- Iteration/indexing over the stored coefficients:
list(x),x[0],x[1]
decompose_prime(p: int, d: int = 1, den: int = 1) -> tuple[int, int]decompose_number(n: int | dict[int, int], d: int = 1, check_count: int | None = None, *, limited_checks: bool = False, no_trivial_solutions: bool = True, warn: bool = True) -> set[tuple[int, int]]