1 · Exact arithmetic, or the library refuses to lie¶
quiverlab computes with finite-dimensional algebras over two kinds of ground field, and it never touches a floating-point number on the way. Over the complex numbers it works in the exact algebraic subfield your data generate; over a finite field it works with integers and coefficient tuples. Either way, every scalar that enters is exact, and a float that tries to enter is turned away at the door.
The public field surface is three names:
| name | meaning |
|---|---|
CC |
"the complex numbers", exactly (an exact algebraic subfield of $\mathbb{C}$) |
GF(q) |
the field with $q = p^n$ elements |
E(n) |
the primitive $n$-th root of unity $\exp(2\pi i/n)$, exact |
This notebook tours the field zoo and then states the exactness contract: why a float is treated as an error rather than a number.
from quiverlab import CC, GF, E
from quiverlab import ExactnessError, FieldError
CC, GF(7), GF(4)
(CC, GF(7), GF(2^2))
CC: the complex numbers, exactly¶
CC is not IEEE-754. Its admissible entries are exact algebraic expressions
— rationals, the imaginary unit i, radicals like sqrt(2), and roots of
unity E(n). When you build an algebra, quiverlab inspects the entries and
constructs the exact subfield of $\mathbb{C}$ they generate (a sympy algebraic
number field $\mathbb{Q}(\alpha)$). All arithmetic then happens there. By flat
base change the dimensions computed in that subfield equal the answers over
$\mathbb{C}$, so nothing is lost by staying exact.
CC.make_domain(entries) is the step that picks the working field.
dom = CC.make_domain(["sqrt(2)"])
print("working field:", dom.name)
s = dom.coerce("sqrt(2)")
prod = dom.mul(s, s)
print("sqrt(2) * sqrt(2) =", dom.to_str(prod), " equals 2 exactly?", dom.eq(prod, dom.coerce(2)))
working field: CC (computing exactly in QQ<sqrt(2)>) sqrt(2) * sqrt(2) = 2 equals 2 exactly? True
There is no rounding here: $\sqrt{2}\cdot\sqrt{2}$ is the integer $2$ on the nose, because the arithmetic lives in $\mathbb{Q}(\sqrt2)$ where $\sqrt2$ is a formal generator with $\sqrt2^{\,2}=2$.
Roots of unity, the GAP way¶
E(n) denotes $\exp(2\pi i/n)$, a primitive $n$-th root of unity (the same
convention as GAP). The cyclotomic identities hold exactly.
domE = CC.make_domain([E(3)])
w = domE.coerce(E(3))
cyclotomic = domE.add(domE.add(domE.one(), w), domE.mul(w, w)) # 1 + w + w^2
print("field:", domE.name)
print("1 + E(3) + E(3)^2 =", domE.to_str(cyclotomic), " (is zero?", domE.is_zero(cyclotomic), ")")
domi = CC.make_domain(["i"])
i = domi.coerce("i")
print("i^2 =", domi.to_str(domi.mul(i, i)))
field: CC (computing exactly in QQ<exp(2*I*pi/3)>) 1 + E(3) + E(3)^2 = 0 (is zero? True ) i^2 = -1
$1+\omega+\omega^2=0$ for a primitive cube root $\omega$, and $i^2=-1$: both are exact facts in the generated field, not numerical near-misses.
GF(p): prime fields¶
GF(p) for $p$ prime is the field $\mathbb{F}_p$, implemented on plain Python
integers reduced mod $p$. Division is exact: $1/3$ is the genuine modular
inverse, an element of the field, not the decimal $0.333\ldots$.
f = GF(7)
print("10 in GF(7) :", f.coerce(10))
print("3^{-1} in GF(7) :", f.inv(3), " check 3 * 3^{-1} =", f.mul(3, f.inv(3)))
print("'1/3' in GF(7) :", f.coerce("1/3"), " (because 3 * 5 = 15 = 1 mod 7)")
10 in GF(7) : 3
3^{-1} in GF(7) : 5 check 3 * 3^{-1} = 1
'1/3' in GF(7) : 5 (because 3 * 5 = 15 = 1 mod 7)
GF(p^n): extension fields with validated moduli¶
For a prime power $q=p^n$ with $n\ge 2$, GF(q) is $\mathbb{F}_{p^n}$,
represented as coefficient tuples modulo a monic irreducible polynomial over
$\mathbb{F}_p$. The modulus comes from a bundled table (Lübeck's Conway
polynomials) — and is machine-verified irreducible at construction, so a
corrupted table entry cannot silently produce a non-field.
g = GF(4) # F_4 = F_2[x] / (x^2 + x + 1)
print("GF(4) elements:", [g.to_str(e) for e in g.elements()])
a = g.gen() # the class of x
defining = g.add(g.add(g.mul(a, a), a), g.one()) # a^2 + a + 1
print("generator a = x, and a^2 + a + 1 =", g.to_str(defining), " (the defining relation)")
print("larger extensions exist too:", GF(9), GF(27))
GF(4) elements: ['0', 'x^1', '1', '1 + x^1'] generator a = x, and a^2 + a + 1 = 0 (the defining relation) larger extensions exist too: GF(3^2) GF(3^3)
The table validates itself¶
quiverlab refuses to build a "field" from a reducible modulus, and refuses to
guess a modulus it does not have tabulated. Both refusals are loud
FieldErrors carrying a fix-it hint.
for call, thunk in [
("GF(4, modulus=[0,0,1]) # x^2, reducible", lambda: GF(4, modulus=[0, 0, 1])),
("GF(3**7) # beyond the bundled table", lambda: GF(3 ** 7)),
]:
try:
thunk()
except FieldError as err:
print(call, "\n ->", err, "\n")
GF(4, modulus=[0,0,1]) # x^2, reducible -> modulus [0, 0, 1] is reducible over GF(2) [hint: pick an irreducible polynomial (see Lübeck's Conway tables)] GF(3**7) # beyond the bundled table -> GF(3^7) is beyond the bundled modulus table [hint: pass an irreducible monic modulus: GF(3**7, modulus=[c0, ..., 1])]
Exact linear algebra rests on exact arithmetic¶
Every dimension quiverlab prints — ranks, kernels, Hochschild numbers — is the output of Gaussian elimination performed over one of these domains, without a single rounding step. The matrix primitives themselves are internal in this preview; what the public surface guarantees is the exact field arithmetic they run on. Here is that arithmetic doing a $2\times2$ determinant, and noticing that whether a matrix is singular can depend on the characteristic:
$$\det\begin{pmatrix}2&3\\1&4\end{pmatrix}=2\cdot4-3\cdot1=5.$$
def det2(dom, a, b, c, d):
a, b, c, d = (dom.coerce(x) for x in (a, b, c, d))
return dom.sub(dom.mul(a, d), dom.mul(b, c))
qq = CC.make_domain([2, 3, 1, 4]) # exact rationals
print("det over", qq.name, "=", qq.to_str(det2(qq, 2, 3, 1, 4)),
" singular?", qq.is_zero(det2(qq, 2, 3, 1, 4)))
g5 = GF(5)
print("det over GF(5) =", g5.to_str(det2(g5, 2, 3, 1, 4)),
" singular?", g5.is_zero(det2(g5, 2, 3, 1, 4)))
det over CC (computing exactly in QQ) = 5 singular? False det over GF(5) = 0 singular? True
The determinant is $5$: invertible over $\mathbb{Q}$, but exactly $0$ in characteristic $5$. An exact library reports this as a hard fact; a floating approximation would see $5.0$ in both and miss the collapse entirely. This characteristic-sensitivity returns, with teeth, in notebook 3.
The exactness contract¶
A float is not an approximation quiverlab is willing to round on your behalf — it is missing information about which exact number you meant. So a float fails immediately and loudly, with a hint, rather than contaminating a computation whose whole value is that you can trust it. This is a feature.
for call, thunk in [
("CC.parse_entry(0.1)", lambda: CC.parse_entry(0.1)),
("GF(7).coerce(0.5)", lambda: GF(7).coerce(0.5)),
]:
try:
thunk()
except ExactnessError as err:
print(call, "->", type(err).__name__)
print(" ", err, "\n")
CC.parse_entry(0.1) -> ExactnessError
0.1 is a float [hint: quiverlab is exact-only: write '1/3' or Fraction(1, 3), never 0.333]
GF(7).coerce(0.5) -> ExactnessError
0.5 is a float [hint: quiverlab is exact-only: write '1/3' or Fraction(1, 3), never 0.333]
0.1 is not $1/10$ in binary floating point, and quiverlab will not pretend
otherwise. Write "1/10", Fraction(1, 10), "sqrt(2)", or E(3) —
say exactly what you mean, and the answer you get back is exact all the way
down. That guarantee is what makes the dimensions in the next two notebooks
worth trusting.
Next: 02 · Quivers and algebras.