polylib
The package provides a class for concretely computing in the ring of polynomials with coefficients in a given ring or field. See below for example usage. After installation, additional documentation is made available by typing in your Python interpreter:
>>> import polylib
>>> help(polylib.Polynomial)
>>> help(polylib.FPolynomial)
Alternatively, type at your commandline: pydoc3 polylib.Polynomial. For recent versions of Python, pydoc3 is simply pydoc.
Installing this package also installs an executable program, bernoulli, that computes Bernoulli polynomials; type bernoulli at your commandline or, if ~/.local/bin/ is not in your PATH, type ~/.local/bin/bernoulli.
Contents
Installation
From your command line:
pip install polylib --user
or, for the latest dev version:
pip install git+https://github.com/sj-simmons/polylib.git --user
For older Python installations, pip might refer to its Python2 version (check this with pip -V), so use instead pip3.
Basic usage
In the interpreter, one can
>>> import polylib
>>> p = polylib.Polynomial([1, 0, -1]) # define a poly by specifying its coefficients
>>> p # Polynomial([1, 0, -1])
>>> print(p) # 1 - x^2
>>> print(p**5) # 1 - 5x^2 + 10x^4 - 10x^6 + 5x^8 - x^10
Equivalently, one can define polynomials in a more Sage-like manner:
>>> x = polylib.Polynomial([0, 1]) # an indeterminant from which to build polynomials
>>> p = 1 - x**2
>>> print(p**5) # 1 - 5x^2 + 10x^4 - 10x^6 + 5x^8 - x^10
If you are working over a field, you may wish to use FPolynomial — the difference is that, in the divmod, floordiv, and mod methods, one can divide an FPolynomial by any FPolynomial, whereas the divisor must be monic when dividing Polynomials.
>>> from fractions import Fraction as F
>>> from polylib import FPolynomial
>>>
>>> x = FPolynomial([0, F(1)]) # an indeterminant for polys over the rational numbers
>>> p = F(3) - F(7,2)*x - F(3,2)*x**2 - F(11,12)*x**3 - F(5,4)*x**4
>>> print(p) # 3 - 7/2x - 3/2x^2 - 11/12x^3 - 5/4x^4
>>>
>>> # looking at the constant and leading coefficient of
>>> 12*p # 36 - 42x - 18x^2 - 11x^3 - 15x^4,
>>> # a possible root of p is 3/5. Let us check:
>>> p.of(F(3,5)) == 0 # True
>>>
>>> # Let us divide p by x - 3/5 using long division:
>>> p1 = x - F(3,5)
>>> q, r = p.divmod(p1)
>>> print("quotient:", q) # quotient: 5 + 5/2x + 5/3x^2 + 5/4x^3
>>> print("remainder:", r) # remainder: 0
>>>
>>> #If you just want the quotient:
>>> print(p // p1) # 5 + 5/2x + 5/3x^2 + 5/4x^3
>>>
>>> #or, just the remainder:
>>> print(p % p1) # 0
Suppose we want a different name for the indeterminant:
>>> from polylib import Polynomial
>>> print(Polynomial([3, 4, 0, 2], 't')) # 3 + 2t + 4t^3
>>> # equivalently:
>>> t = Polynomial([0,1], 't')
>>> p = 3 + 2*t + 4*t**3 # 3 + 2t + 4t^3
Polynomials with polynomial coefficients
This library does not robustly handle polynomials in more than one variable; however, when working, say, over finite fields, we often want polynomials with polynomial coefficients.
We could do something like this:
>>> t = Polynomial([0, 1], 't')
>>> p = Polynomial([5-t, t**2+t-1, -2])
>>> print(p) # 5 - t + -1 + t + t^2x + -2x^2
Problem: the string output is ambiguous; instead, do this:
>>> t = Polynomial([0, 1], '(t)') # note the parentheses
>>> p = Polynomial([5-t, t**2+t-1, -2])
>>> print(p) # (5 - t) + (-1 + t + t^2)x + -2x^2
but that is incorrect since
>>> p # Polynomial((Polynomial((5, -1)), Polynomial((-1, 1, 1)), -2))
This is what we want:
>>> p = Polynomial([5-t, t**2+t-1, -2*t**0]) # note the use of t**0
>>> print(p) # (5 - t) + (-1 + t + t^2)x + (-2)x^2
>>> p # Polynomial((Polynomial((5, -1)), Polynomial((-1, 1, 1)), Polynomial((-2,))))
Alternatively, of course, one can type
>>> p=Polynomial((Polynomial((5,-1),'(t)'),Polynomial((-1,1,1),'(t)'),Polynomial((-2,),'(t)')))
but this might be better:
>>> x = Polynomial([0, 1])
>>> t = Polynomial([0, 1], '(t)') # note the parentheses
>>> p = (5-t) * x**0 + (t**2+t-1) * x - 2*t**0 * x**2 # note, now, both t**0 and x**0
>>> print(p)
(5 - t) + (-1 + t + t^2)x + (-2)x^2
These sorts of polynomials occur when working with elliptic curves over a finite field. Their string representations are more readable if the coefficient polynomials don't appear with spaces:
>>> x = Polynomial([0, 1])
>>> t = Polynomial([0, 1], '(t)', spaces=False)
>>> p = (5-t) * x**0 + (t**2+t-1) * x - 2*t**0 * x**2 # note, now, both t**0 and x**0
>>> print(p)
(5-t) + (-1+t+t^2)x + (-2)x^2
In some cases, wrapping in square brackets is more readable:
>>> t = Polynomial([0, 1],'[t]')
>>> p = Polynomial([complex(0,1), complex(2,3)-complex(0,1)*t])
>>> print(p) # 1j + [(2+3j) + (-0-1j)t]x
>>> print(p*2) # (-1+0j) + [(-6+4j) + (2+0j)t]x + [(-5+12j) + (6-4j)t + (-1+0j)t^2]x^2
You may prefer a capital x or some other symbol for the indeterminate:
>>> p = Polynomial([complex(0,1), complex(2,3)-complex(0,1)*t], '[X]')
>>> print(p*2) # (-1+0j) + [(-6+4j) + (2+0j)t]X + [(-5+12j) + (6-4j)t + (-1+0j)t^2]X^2
Cyclotomic polynomials
Next, suppose that we try to compute cyclotomic polynomials by means of their definition in terms of complex roots of unity:
from cmath import exp, pi
from math import gcd
from functools import reduce
from operator import mul
from polylib import Polynomial
def cyclotomic_poly(n):
omega = exp(complex(0,2*pi/n)) # e^(2pi/n i), a primitive n_th root of unity
x = Polynomial([0, 1])
return reduce(mul, [x - omega**j for j in range(n) if gcd(j, n) == 1], 1)
print(cyclotomic_poly(8))
The function cyclotomic_poly(n) computes, in standard math notation, , which is defined by
the terms in the product on the right involving only primitive roots of unity.
The output of the above program is
(1-1.5543122344752192e-15j) + (7.771561172376096e-16+1.1102230246251565e-16j)x + (3.3306690738754696e-16-2.220446049250313e-16j)x^2 + (6.661338147750939e-16+0j)x^3 + x^4
The cmath Python library, as well as the builtin type complex, represents complex numbers in rectangular form, as pairs of floats; hence the rounding error in the coefficients of the 8th cyclotomic polynomial. In fact, as we will see below, the coefficients of are integral for all
Since the complex primitive roots of unity occur in complex conjugate pairs. we can improve things by multiplying first the conjugate pairs:
def cyclotomic_poly(n):
omega = exp(complex(0,2*pi/n))
x = Polynomial([0, 1])
if n < 3:
poly = reduce(mul, [x - omega**j for j in range(n) if gcd(j, n) == 1], 1)
else:
poly = reduce(
mul,
[(x-omega**j)*(x-(omega**j).conjugate()) for j in range(n//2+1) if gcd(j, n) == 1],
1
)
return poly
Then cyclotomic_poly(8) becomes:
1 + (-4.440892098500626e-16+0j)x + (4.440892098500626e-16+0j)x^2 + (-4.440892098500626e-16+0j)x^3 + x^4
Assuming that the coefficients are integral, we can now simply round:
from cmath import exp, pi
from math import gcd
from functools import reduce
from operator import mul
from polylib import Polynomial
def cyclotomic_poly(n):
omega = exp(complex(0, 2 * pi / n))
x = Polynomial([0, 1])
if n < 3:
poly = reduce(mul, [x - omega ** j for j in range(n) if gcd(j, n) == 1], 1)
else:
poly = reduce(
mul,
[
(x - omega ** j) * (x - (omega ** j).conjugate())
for j in range(n // 2 + 1)
if gcd(j, n) == 1
],
1,
)
return Polynomial([round(coeff.real) for coeff in poly])
print(" n cyclotomic_poly(n)")
for n in range(1, 17):
print(f"{n:2} {cyclotomic_poly(n)}")
output:
n cyclotomic_poly(n)
1 -1 + x
2 1 + x
3 1 + x + x^2
4 1 + x^2
5 1 + x + x^2 + x^3 + x^4
6 1 - x + x^2
7 1 + x + x^2 + x^3 + x^4 + x^5 + x^6
8 1 + x^4
9 1 + x^3 + x^6
10 1 - x + x^2 - x^3 + x^4
11 1 + x + x^2 + x^3 + x^4 + x^5 + x^6 + x^7 + x^8 + x^9 + x^10
12 1 - x^2 + x^4
13 1 + x + x^2 + x^3 + x^4 + x^5 + x^6 + x^7 + x^8 + x^9 + x^10 + x^11 + x^12
14 1 - x + x^2 - x^3 + x^4 - x^5 + x^6
15 1 - x + x^3 - x^4 + x^5 - x^7 + x^8
16 1 + x^8
(For those interested in computing exactly in the real of complex numbers see, for example, Calcium and slides from a talk by its author.)
In the case of cyclotomic polynomials, it is better to reformulate their definition completely in terms of , polynomials over the integers. Before we do that, let us observe the floating point issues inherent in the above formulation of cyclotomic_poly(n) building up to the point of throwing off our computations.
It is fairly easy to establish (see below), for a prime and a positive integer, the following identity:
We can use polylib to compose polynomials. Let us verify the identity above for and :
x = Polynomial([0, 1])
cyclotomic_poly(3**4) == cyclotomic_poly(3).of(x**(3**3)) # True
cyclotomic_poly(3**5) == cyclotomic_poly(3).of(x**(3**4)) # False, but should be True
Cyclotomic polynomials as polynomials over the integers
Over any field with unit, one can construct cyclotomic polynomials (partially) characterized by the following:
(Here, and the exponent of is the smallest positive integer , if it exists, satisfying )
More correctly, the coefficients are in an isomorphic image of embedded in the prime field of in the case that has characteristic zero; if has positive characteristic , then the coefficients live in its prime field — isomorphic to — which is naturally the homomorphic image of in
If , notice that, by the Fundamental Theorem of Algebra, the cyclotomic polynomials defined above satisfy condition (1) since the primitive th roots of unity, for and are the complete list of complex numbers of exponent Also note that each primitive th root of unity is a zero of mutliplicity 1, so the in the definition above are the unique smallest-degree, monic polynomials that do the job.
How can we construct the cyclotomic polynomials using only polynomials of the integers?
First note that, over , we have the factorization
the product now involving all th roots of unity — the factorization holds since the th roots of unity are exactly the roots of
The th roots of unity together comprise a multiplicative group of order By Lagrange's Theorem, the order, , of any single th root of unity must divide Collecting the th primitive roots of unity for each that divides we can write
where are, for now, still defined, as above, in terms of the primitive th roots of unity; hence, we can write
where we have set In fact, we can view this last equality as a (recursive) definition of
We now see why has integer coefficients: inductively (with respect to ), if we assume that each — monic, we already know — with and has integral coefficients, the product in the denominator above is also monic with integer coefficients. The numerator, , has integer coefficients so that long division (we already know that the remainder is 0) can be executed using only integers.
Rather than writing out the induction proof in detail, let us rewrite the function cyclotomic_poly in a such a way that it uses only polynomials over the integers:
from functools import reduce
from operator import mul
from polylib import Polynomial
def cyclotomic_poly(n):
assert n > 0
x = Polynomial([0, 1])
return (x ** n - 1) // reduce(
mul, [cyclotomic_poly(d) for d in range(1, n) if n % d == 0], x ** 0
)
print(" n cyclotomic_poly(n)")
for n in range(1, 18):
print(f"{n:2} {cyclotomic_poly(n)}")
The output is, of course, the same as above,
n cyclotomic_poly(n)
1 -1 + x
2 1 + x
3 1 + x + x^2
4 1 + x^2
5 1 + x + x^2 + x^3 + x^4
...
And the two identities, the second of which we had trouble with above?
x = Polynomial([0, 1])
cyclotomic_poly(3**4) == cyclotomic_poly(3).of(x**(3**3)) # True
cyclotomic_poly(3**5) == cyclotomic_poly(3).of(x**(3**4)) # True
Before moving on, let us establish the identity in question, which is
First notice that, if n is prime, the recursive definition specializes to
More generally,
Cyclotomic polynomials over fields
Over , the polynomial has distinct roots and so each root has multiplicity 1; i.e., in its factored form, none of the linear factors of are repeated. It turns out the same is true over any field whose characteristic does not divide (this follows from the fact that and its formal derivative satisfy if the characteristic of does not divide and hence are co-prime over )1.
A natural question to consider is: can we use the recursive definition from the last section over any field? For completeness, here is the recursive definition:
At issue is whether we retain the property
over fields other than the complex numbers.
But the only way that can satisfy is if ; and if the reason is that for (assume without loss of generality that is the least such positive integer) then must divide (by the division algorithm). But then is a root of the denominator so that occurs as a factor in the denominator. This is a problem since such a factor in the denominator would cancel with an identical and non-repeated (if the characteristic of does not divide ) factor in the numerator. Hence no such exists, and the property above holds over any field.
A subtlety here is that we are now officially working over the prime field of Let denote the cyclotomic polynomial with coefficients now in the image of the natural ring homomorphism (which is reduction modulo a prime if the characteristic of is positive and essentially the identity in characteristic zero). Clearly, for if and only if Using the Euclidean algorithm, we can write, for or any where in fact, so that is a root if and only if divides (which, note, also implies that has at most roots in ).2
A useful summary of the preceding argument is that if, in the case that char() does not divide we write
then the cyclotomic polynomials on the right are pairwise coprime as polynomials in
If defined using complex th roots of unity, then clearly has degree where denotes Euler's phi-function (i.e., is defined to be the number of positive integers less than and coprime to ).
But over any field, the degree is the same, as follows inductively from the recursive definition and the identity
Let us reformulate the property above:
If char() does not divide , then
where denotes the non-zero elements (so, units) of the field .
We are now in a good position to observe the following:
This is true in any field Why? Suppose given a subgroup of order and consider the identity (in ):
The elements of the given group are exactly the solutions of the polynomials on the left and hence on the right. From (1'), we know that the roots of are the generators of the given group (of which there are we also know). More generally, each (also cyclic, of course) subgroup of the given group must have order dividing and so has the generators consisting exactly of the roots of 3
The picture is now complete. Over any field one has the analogue of the th complex roots of unity with exactly the same group structure. The roots of the cycolotomic polynomials are precisely the generators of of the subgroups of order dividing .
In a field of characteristic zero, one has finite groups of all orders and they are all cyclic. The same is true in finite field, at least for orders not divisible by the characteristic.
In any case, to find a generator, or primitive element, of a subgroup of order , one only need to find a root of .
The program below uses numlib to instantiate the prime field and then computes the first few cyclotomic polynomials with coefficients reduced modulo 2.
from functools import reduce
from operator import mul
from polylib import Polynomial
from numlib import Zmod
def cyclotomic_poly(n: int, x: Polynomial = Polynomial([0, 1])) -> Polynomial:
return (x ** n - 1) // reduce(
mul, [cyclotomic_poly(d) for d in range(1, n) if n % d == 0], x ** 0
)
Zp = Zmod(2)
x = Polynomial([0, Zp(1)])
print(" n n_th cyclotomic poly over", Zp)
for n in range(1, 8):
print(f"{n:2} {cyclotomic_poly(n, x)}")
output:
n n_th cyclotomic poly over Z/2Z
1 1 + x
2 1 + x
3 1 + x + x^2
4 1 + x^2
5 1 + x + x^2 + x^3 + x^4
6 1 + x + x^2
7 1 + x + x^2 + x^3 + x^4 + x^5 + x^6
Cyclotomic polynomials over fields
Cyclotomic polynomials are often in the mix when constructing finite (Galois) fields. Suppose is an irreducible polynomial over of degree Then, modulo the ideal generated by is a finite field of order . See, for example, numlib for the mathematical details and implementations.
Let us consider the case in which and . Using numlib we can construct the Galois field of order 16 using numlib.FPmod but we need a quartic that is irreducible over the prime field (more on finding such a polynomial below).
>>> from numlib import Zmod, FPmod
>>> from polylib import FPolynomial
>>>
>>> PF = Zmod(2) # the prime field
>>> t = FPolynomial([0, PF(1)], 't') # an indeterminant for Z/2Z[x]
>>> irred = 1 + t**3 + t**4 # this is irreducible over Z/2Z[x]
>>> GF = FPmod(irred) # representation of the Galois field of order 16
>>> print(GF) # Z/2Z [t] / <1 + t^3 + t^4>
We already know that the multiplicative group of units in GF is cyclic, and that its generators are exactly the roots of the cyclotomic polynomial . As a sanity check, let us brute force verify this. Continuing the previous interactive session:
>>> def order(elt): # return the multiplicative orde
... for j in range(1,16):
... if elt**j == 1:
... return j
>>>
>>> from itertools import product
>>>
>>> generators = []
>>> for coeffs in product(PF, repeat=4):
... elt = GF(coeffs)
... if order(elt) == 15:
... generators.append(elt)
>>> print(', '.join([str(gen).replace(" ","") for gen in generators]))
t^2, t^2+t^3, t, t+t^2, t+t^2+t^3, 1+t^3, 1+t^2+t^3, 1+t+t^2
Now for the verification:
>>> from polylib import cyclotomic
>>> cyclo15 = cyclotomic(15, t) # 1 + t + t^3 + t^4 + t^5 + t^7 + t^8
>>> all(map(lambda elt: cyclo15.of(elt) == 0, generators))
True
Serendipitously, the choice of for the irreducible when implementing led to the equivalence class being a generator. That happening is particularly convenient and we say that is a primitive polynomial for ; i.e., an irreducible is a primitive polynomial for if 's equivalence class is a generator for .
We know that we need an quartic irreducible over to even instantiate ; but how can we choose one so that turns out to be a generator? A hint is that the irreducible we chose above divides :
>>> print(cyclo15 % irred)
0
>>> print(cyclo15 // irred)
1 + t + t^4
Moreover, is also irreducible in .
But how do we find a candidate for a primitive polynomial in the first place? One clue is that, in the previous example, factors like
begin(align) 1+t+t^3+t^4+t^5+t^7+t^8 &= 1 + t^3 + t^4 + t + t^4(t + t^3 + t^4) \notag\ &= 1 + (1 + t^4)(t + t^3 + t^4) \notag\
Exercises
- Write a program that find an primitive polynomial for
$\operatorname{GF}(2^12).$
Footnotes
1 The formal (or algebraic) derivative which one first defines on monomials as and then extends linearly, satisfies the familiar product rule from Calculus. Hence, if then ; so that is a common divisor of of
Also, that the expression implies that and have no common factors tacitly uses the fact that is a unique factorization domain — more computationally, is a Euclidean domain and, in fact, one in which polynomial division can be used to implement the Euclidean algorithm.
2 The ring , where the cyclotomic polynomials live, is not a Euclidean domain, nor even an principal ideal domain; it is, however, a unique factorization domain in which the division algorithm can be used to write any in as where so that is a root of if and only if it is divisible by .
3 If is perfect and char() divides , then one can show that necessarily has repeated roots in . In other words, there are not enough element of with the right order to form a group of order — so over, for example, over a finite field, there is no subgroup with order divisible by the characteristic.
Resources
- Victor Shoup's A Computational Introduction to Number Theory and Algebra
- FLINT Sage uses this for polynomial arithmetic factoring over , , and .
- Arnold and Monagan's article Calculating Cyclotomic Polynomials
- Brent's On computing factors of cyclotomic polynomials
- Cao and Cao's Note on fast division algorithm for polynomials using Newton iteration
Todo
- Implement faster division algorithm (see Cao and Cao's paper linked to above).
- Implement multivariate polynomials (use hash?)
- Add option to Polynomial's constructor to print in decreasing degree order.