Spaces:
Running
Running
r"""Modules in number fields. | |
The classes defined here allow us to work with finitely generated, free | |
modules, whose generators are algebraic numbers. | |
There is an abstract base class called :py:class:`~.Module`, which has two | |
concrete subclasses, :py:class:`~.PowerBasis` and :py:class:`~.Submodule`. | |
Every module is defined by its basis, or set of generators: | |
* For a :py:class:`~.PowerBasis`, the generators are the first $n$ powers | |
(starting with the zeroth) of an algebraic integer $\theta$ of degree $n$. | |
The :py:class:`~.PowerBasis` is constructed by passing either the minimal | |
polynomial of $\theta$, or an :py:class:`~.AlgebraicField` having $\theta$ | |
as its primitive element. | |
* For a :py:class:`~.Submodule`, the generators are a set of | |
$\mathbb{Q}$-linear combinations of the generators of another module. That | |
other module is then the "parent" of the :py:class:`~.Submodule`. The | |
coefficients of the $\mathbb{Q}$-linear combinations may be given by an | |
integer matrix, and a positive integer denominator. Each column of the matrix | |
defines a generator. | |
from sympy.polys import Poly, cyclotomic_poly, ZZ | |
from sympy.abc import x | |
from sympy.polys.matrices import DomainMatrix, DM | |
from sympy.polys.numberfields.modules import PowerBasis | |
T = Poly(cyclotomic_poly(5, x)) | |
A = PowerBasis(T) | |
print(A) | |
PowerBasis(x**4 + x**3 + x**2 + x + 1) | |
B = A.submodule_from_matrix(2 * DomainMatrix.eye(4, ZZ), denom=3) | |
print(B) | |
Submodule[[2, 0, 0, 0], [0, 2, 0, 0], [0, 0, 2, 0], [0, 0, 0, 2]]/3 | |
print(B.parent) | |
PowerBasis(x**4 + x**3 + x**2 + x + 1) | |
Thus, every module is either a :py:class:`~.PowerBasis`, | |
or a :py:class:`~.Submodule`, some ancestor of which is a | |
:py:class:`~.PowerBasis`. (If ``S`` is a :py:class:`~.Submodule`, then its | |
ancestors are ``S.parent``, ``S.parent.parent``, and so on). | |
The :py:class:`~.ModuleElement` class represents a linear combination of the | |
generators of any module. Critically, the coefficients of this linear | |
combination are not restricted to be integers, but may be any rational | |
numbers. This is necessary so that any and all algebraic integers be | |
representable, starting from the power basis in a primitive element $\theta$ | |
for the number field in question. For example, in a quadratic field | |
$\mathbb{Q}(\sqrt{d})$ where $d \equiv 1 \mod{4}$, a denominator of $2$ is | |
needed. | |
A :py:class:`~.ModuleElement` can be constructed from an integer column vector | |
and a denominator: | |
U = Poly(x**2 - 5) | |
M = PowerBasis(U) | |
e = M(DM([[1], [1]], ZZ), denom=2) | |
print(e) | |
[1, 1]/2 | |
print(e.module) | |
PowerBasis(x**2 - 5) | |
The :py:class:`~.PowerBasisElement` class is a subclass of | |
:py:class:`~.ModuleElement` that represents elements of a | |
:py:class:`~.PowerBasis`, and adds functionality pertinent to elements | |
represented directly over powers of the primitive element $\theta$. | |
Arithmetic with module elements | |
=============================== | |
While a :py:class:`~.ModuleElement` represents a linear combination over the | |
generators of a particular module, recall that every module is either a | |
:py:class:`~.PowerBasis` or a descendant (along a chain of | |
:py:class:`~.Submodule` objects) thereof, so that in fact every | |
:py:class:`~.ModuleElement` represents an algebraic number in some field | |
$\mathbb{Q}(\theta)$, where $\theta$ is the defining element of some | |
:py:class:`~.PowerBasis`. It thus makes sense to talk about the number field | |
to which a given :py:class:`~.ModuleElement` belongs. | |
This means that any two :py:class:`~.ModuleElement` instances can be added, | |
subtracted, multiplied, or divided, provided they belong to the same number | |
field. Similarly, since $\mathbb{Q}$ is a subfield of every number field, | |
any :py:class:`~.ModuleElement` may be added, multiplied, etc. by any | |
rational number. | |
from sympy import QQ | |
from sympy.polys.numberfields.modules import to_col | |
T = Poly(cyclotomic_poly(5)) | |
A = PowerBasis(T) | |
C = A.submodule_from_matrix(3 * DomainMatrix.eye(4, ZZ)) | |
e = A(to_col([0, 2, 0, 0]), denom=3) | |
f = A(to_col([0, 0, 0, 7]), denom=5) | |
g = C(to_col([1, 1, 1, 1])) | |
e + f | |
[0, 10, 0, 21]/15 | |
e - f | |
[0, 10, 0, -21]/15 | |
e - g | |
[-9, -7, -9, -9]/3 | |
e + QQ(7, 10) | |
[21, 20, 0, 0]/30 | |
e * f | |
[-14, -14, -14, -14]/15 | |
e ** 2 | |
[0, 0, 4, 0]/9 | |
f // g | |
[7, 7, 7, 7]/15 | |
f * QQ(2, 3) | |
[0, 0, 0, 14]/15 | |
However, care must be taken with arithmetic operations on | |
:py:class:`~.ModuleElement`, because the module $C$ to which the result will | |
belong will be the nearest common ancestor (NCA) of the modules $A$, $B$ to | |
which the two operands belong, and $C$ may be different from either or both | |
of $A$ and $B$. | |
A = PowerBasis(T) | |
B = A.submodule_from_matrix(2 * DomainMatrix.eye(4, ZZ)) | |
C = A.submodule_from_matrix(3 * DomainMatrix.eye(4, ZZ)) | |
print((B(0) * C(0)).module == A) | |
True | |
Before the arithmetic operation is performed, copies of the two operands are | |
automatically converted into elements of the NCA (the operands themselves are | |
not modified). This upward conversion along an ancestor chain is easy: it just | |
requires the successive multiplication by the defining matrix of each | |
:py:class:`~.Submodule`. | |
Conversely, downward conversion, i.e. representing a given | |
:py:class:`~.ModuleElement` in a submodule, is also supported -- namely by | |
the :py:meth:`~sympy.polys.numberfields.modules.Submodule.represent` method | |
-- but is not guaranteed to succeed in general, since the given element may | |
not belong to the submodule. The main circumstance in which this issue tends | |
to arise is with multiplication, since modules, while closed under addition, | |
need not be closed under multiplication. | |
Multiplication | |
-------------- | |
Generally speaking, a module need not be closed under multiplication, i.e. need | |
not form a ring. However, many of the modules we work with in the context of | |
number fields are in fact rings, and our classes do support multiplication. | |
Specifically, any :py:class:`~.Module` can attempt to compute its own | |
multiplication table, but this does not happen unless an attempt is made to | |
multiply two :py:class:`~.ModuleElement` instances belonging to it. | |
A = PowerBasis(T) | |
print(A._mult_tab is None) | |
True | |
a = A(0)*A(1) | |
print(A._mult_tab is None) | |
False | |
Every :py:class:`~.PowerBasis` is, by its nature, closed under multiplication, | |
so instances of :py:class:`~.PowerBasis` can always successfully compute their | |
multiplication table. | |
When a :py:class:`~.Submodule` attempts to compute its multiplication table, | |
it converts each of its own generators into elements of its parent module, | |
multiplies them there, in every possible pairing, and then tries to | |
represent the results in itself, i.e. as $\mathbb{Z}$-linear combinations | |
over its own generators. This will succeed if and only if the submodule is | |
in fact closed under multiplication. | |
Module Homomorphisms | |
==================== | |
Many important number theoretic algorithms require the calculation of the | |
kernel of one or more module homomorphisms. Accordingly we have several | |
lightweight classes, :py:class:`~.ModuleHomomorphism`, | |
:py:class:`~.ModuleEndomorphism`, :py:class:`~.InnerEndomorphism`, and | |
:py:class:`~.EndomorphismRing`, which provide the minimal necessary machinery | |
to support this. | |
""" | |
from sympy.core.intfunc import igcd, ilcm | |
from sympy.core.symbol import Dummy | |
from sympy.polys.polyclasses import ANP | |
from sympy.polys.polytools import Poly | |
from sympy.polys.densetools import dup_clear_denoms | |
from sympy.polys.domains.algebraicfield import AlgebraicField | |
from sympy.polys.domains.finitefield import FF | |
from sympy.polys.domains.rationalfield import QQ | |
from sympy.polys.domains.integerring import ZZ | |
from sympy.polys.matrices.domainmatrix import DomainMatrix | |
from sympy.polys.matrices.exceptions import DMBadInputError | |
from sympy.polys.matrices.normalforms import hermite_normal_form | |
from sympy.polys.polyerrors import CoercionFailed, UnificationFailed | |
from sympy.polys.polyutils import IntegerPowerable | |
from .exceptions import ClosureFailure, MissingUnityError, StructureError | |
from .utilities import AlgIntPowers, is_rat, get_num_denom | |
def to_col(coeffs): | |
r"""Transform a list of integer coefficients into a column vector.""" | |
return DomainMatrix([[ZZ(c) for c in coeffs]], (1, len(coeffs)), ZZ).transpose() | |
class Module: | |
""" | |
Generic finitely-generated module. | |
This is an abstract base class, and should not be instantiated directly. | |
The two concrete subclasses are :py:class:`~.PowerBasis` and | |
:py:class:`~.Submodule`. | |
Every :py:class:`~.Submodule` is derived from another module, referenced | |
by its ``parent`` attribute. If ``S`` is a submodule, then we refer to | |
``S.parent``, ``S.parent.parent``, and so on, as the "ancestors" of | |
``S``. Thus, every :py:class:`~.Module` is either a | |
:py:class:`~.PowerBasis` or a :py:class:`~.Submodule`, some ancestor of | |
which is a :py:class:`~.PowerBasis`. | |
""" | |
def n(self): | |
"""The number of generators of this module.""" | |
raise NotImplementedError | |
def mult_tab(self): | |
""" | |
Get the multiplication table for this module (if closed under mult). | |
Explanation | |
=========== | |
Computes a dictionary ``M`` of dictionaries of lists, representing the | |
upper triangular half of the multiplication table. | |
In other words, if ``0 <= i <= j < self.n``, then ``M[i][j]`` is the | |
list ``c`` of coefficients such that | |
``g[i] * g[j] == sum(c[k]*g[k], k in range(self.n))``, | |
where ``g`` is the list of generators of this module. | |
If ``j < i`` then ``M[i][j]`` is undefined. | |
Examples | |
======== | |
>>> from sympy.polys import Poly, cyclotomic_poly | |
>>> from sympy.polys.numberfields.modules import PowerBasis | |
>>> T = Poly(cyclotomic_poly(5)) | |
>>> A = PowerBasis(T) | |
>>> print(A.mult_tab()) # doctest: +SKIP | |
{0: {0: [1, 0, 0, 0], 1: [0, 1, 0, 0], 2: [0, 0, 1, 0], 3: [0, 0, 0, 1]}, | |
1: {1: [0, 0, 1, 0], 2: [0, 0, 0, 1], 3: [-1, -1, -1, -1]}, | |
2: {2: [-1, -1, -1, -1], 3: [1, 0, 0, 0]}, | |
3: {3: [0, 1, 0, 0]}} | |
Returns | |
======= | |
dict of dict of lists | |
Raises | |
====== | |
ClosureFailure | |
If the module is not closed under multiplication. | |
""" | |
raise NotImplementedError | |
def parent(self): | |
""" | |
The parent module, if any, for this module. | |
Explanation | |
=========== | |
For a :py:class:`~.Submodule` this is its ``parent`` attribute; for a | |
:py:class:`~.PowerBasis` this is ``None``. | |
Returns | |
======= | |
:py:class:`~.Module`, ``None`` | |
See Also | |
======== | |
Module | |
""" | |
return None | |
def represent(self, elt): | |
r""" | |
Represent a module element as an integer-linear combination over the | |
generators of this module. | |
Explanation | |
=========== | |
In our system, to "represent" always means to write a | |
:py:class:`~.ModuleElement` as a :ref:`ZZ`-linear combination over the | |
generators of the present :py:class:`~.Module`. Furthermore, the | |
incoming :py:class:`~.ModuleElement` must belong to an ancestor of | |
the present :py:class:`~.Module` (or to the present | |
:py:class:`~.Module` itself). | |
The most common application is to represent a | |
:py:class:`~.ModuleElement` in a :py:class:`~.Submodule`. For example, | |
this is involved in computing multiplication tables. | |
On the other hand, representing in a :py:class:`~.PowerBasis` is an | |
odd case, and one which tends not to arise in practice, except for | |
example when using a :py:class:`~.ModuleEndomorphism` on a | |
:py:class:`~.PowerBasis`. | |
In such a case, (1) the incoming :py:class:`~.ModuleElement` must | |
belong to the :py:class:`~.PowerBasis` itself (since the latter has no | |
proper ancestors) and (2) it is "representable" iff it belongs to | |
$\mathbb{Z}[\theta]$ (although generally a | |
:py:class:`~.PowerBasisElement` may represent any element of | |
$\mathbb{Q}(\theta)$, i.e. any algebraic number). | |
Examples | |
======== | |
>>> from sympy import Poly, cyclotomic_poly | |
>>> from sympy.polys.numberfields.modules import PowerBasis, to_col | |
>>> from sympy.abc import zeta | |
>>> T = Poly(cyclotomic_poly(5)) | |
>>> A = PowerBasis(T) | |
>>> a = A(to_col([2, 4, 6, 8])) | |
The :py:class:`~.ModuleElement` ``a`` has all even coefficients. | |
If we represent ``a`` in the submodule ``B = 2*A``, the coefficients in | |
the column vector will be halved: | |
>>> B = A.submodule_from_gens([2*A(i) for i in range(4)]) | |
>>> b = B.represent(a) | |
>>> print(b.transpose()) # doctest: +SKIP | |
DomainMatrix([[1, 2, 3, 4]], (1, 4), ZZ) | |
However, the element of ``B`` so defined still represents the same | |
algebraic number: | |
>>> print(a.poly(zeta).as_expr()) | |
8*zeta**3 + 6*zeta**2 + 4*zeta + 2 | |
>>> print(B(b).over_power_basis().poly(zeta).as_expr()) | |
8*zeta**3 + 6*zeta**2 + 4*zeta + 2 | |
Parameters | |
========== | |
elt : :py:class:`~.ModuleElement` | |
The module element to be represented. Must belong to some ancestor | |
module of this module (including this module itself). | |
Returns | |
======= | |
:py:class:`~.DomainMatrix` over :ref:`ZZ` | |
This will be a column vector, representing the coefficients of a | |
linear combination of this module's generators, which equals the | |
given element. | |
Raises | |
====== | |
ClosureFailure | |
If the given element cannot be represented as a :ref:`ZZ`-linear | |
combination over this module. | |
See Also | |
======== | |
.Submodule.represent | |
.PowerBasis.represent | |
""" | |
raise NotImplementedError | |
def ancestors(self, include_self=False): | |
""" | |
Return the list of ancestor modules of this module, from the | |
foundational :py:class:`~.PowerBasis` downward, optionally including | |
``self``. | |
See Also | |
======== | |
Module | |
""" | |
c = self.parent | |
a = [] if c is None else c.ancestors(include_self=True) | |
if include_self: | |
a.append(self) | |
return a | |
def power_basis_ancestor(self): | |
""" | |
Return the :py:class:`~.PowerBasis` that is an ancestor of this module. | |
See Also | |
======== | |
Module | |
""" | |
if isinstance(self, PowerBasis): | |
return self | |
c = self.parent | |
if c is not None: | |
return c.power_basis_ancestor() | |
return None | |
def nearest_common_ancestor(self, other): | |
""" | |
Locate the nearest common ancestor of this module and another. | |
Returns | |
======= | |
:py:class:`~.Module`, ``None`` | |
See Also | |
======== | |
Module | |
""" | |
sA = self.ancestors(include_self=True) | |
oA = other.ancestors(include_self=True) | |
nca = None | |
for sa, oa in zip(sA, oA): | |
if sa == oa: | |
nca = sa | |
else: | |
break | |
return nca | |
def number_field(self): | |
r""" | |
Return the associated :py:class:`~.AlgebraicField`, if any. | |
Explanation | |
=========== | |
A :py:class:`~.PowerBasis` can be constructed on a :py:class:`~.Poly` | |
$f$ or on an :py:class:`~.AlgebraicField` $K$. In the latter case, the | |
:py:class:`~.PowerBasis` and all its descendant modules will return $K$ | |
as their ``.number_field`` property, while in the former case they will | |
all return ``None``. | |
Returns | |
======= | |
:py:class:`~.AlgebraicField`, ``None`` | |
""" | |
return self.power_basis_ancestor().number_field | |
def is_compat_col(self, col): | |
"""Say whether *col* is a suitable column vector for this module.""" | |
return isinstance(col, DomainMatrix) and col.shape == (self.n, 1) and col.domain.is_ZZ | |
def __call__(self, spec, denom=1): | |
r""" | |
Generate a :py:class:`~.ModuleElement` belonging to this module. | |
Examples | |
======== | |
>>> from sympy.polys import Poly, cyclotomic_poly | |
>>> from sympy.polys.numberfields.modules import PowerBasis, to_col | |
>>> T = Poly(cyclotomic_poly(5)) | |
>>> A = PowerBasis(T) | |
>>> e = A(to_col([1, 2, 3, 4]), denom=3) | |
>>> print(e) # doctest: +SKIP | |
[1, 2, 3, 4]/3 | |
>>> f = A(2) | |
>>> print(f) # doctest: +SKIP | |
[0, 0, 1, 0] | |
Parameters | |
========== | |
spec : :py:class:`~.DomainMatrix`, int | |
Specifies the numerators of the coefficients of the | |
:py:class:`~.ModuleElement`. Can be either a column vector over | |
:ref:`ZZ`, whose length must equal the number $n$ of generators of | |
this module, or else an integer ``j``, $0 \leq j < n$, which is a | |
shorthand for column $j$ of $I_n$, the $n \times n$ identity | |
matrix. | |
denom : int, optional (default=1) | |
Denominator for the coefficients of the | |
:py:class:`~.ModuleElement`. | |
Returns | |
======= | |
:py:class:`~.ModuleElement` | |
The coefficients are the entries of the *spec* vector, divided by | |
*denom*. | |
""" | |
if isinstance(spec, int) and 0 <= spec < self.n: | |
spec = DomainMatrix.eye(self.n, ZZ)[:, spec].to_dense() | |
if not self.is_compat_col(spec): | |
raise ValueError('Compatible column vector required.') | |
return make_mod_elt(self, spec, denom=denom) | |
def starts_with_unity(self): | |
"""Say whether the module's first generator equals unity.""" | |
raise NotImplementedError | |
def basis_elements(self): | |
""" | |
Get list of :py:class:`~.ModuleElement` being the generators of this | |
module. | |
""" | |
return [self(j) for j in range(self.n)] | |
def zero(self): | |
"""Return a :py:class:`~.ModuleElement` representing zero.""" | |
return self(0) * 0 | |
def one(self): | |
""" | |
Return a :py:class:`~.ModuleElement` representing unity, | |
and belonging to the first ancestor of this module (including | |
itself) that starts with unity. | |
""" | |
return self.element_from_rational(1) | |
def element_from_rational(self, a): | |
""" | |
Return a :py:class:`~.ModuleElement` representing a rational number. | |
Explanation | |
=========== | |
The returned :py:class:`~.ModuleElement` will belong to the first | |
module on this module's ancestor chain (including this module | |
itself) that starts with unity. | |
Examples | |
======== | |
>>> from sympy.polys import Poly, cyclotomic_poly, QQ | |
>>> from sympy.polys.numberfields.modules import PowerBasis | |
>>> T = Poly(cyclotomic_poly(5)) | |
>>> A = PowerBasis(T) | |
>>> a = A.element_from_rational(QQ(2, 3)) | |
>>> print(a) # doctest: +SKIP | |
[2, 0, 0, 0]/3 | |
Parameters | |
========== | |
a : int, :ref:`ZZ`, :ref:`QQ` | |
Returns | |
======= | |
:py:class:`~.ModuleElement` | |
""" | |
raise NotImplementedError | |
def submodule_from_gens(self, gens, hnf=True, hnf_modulus=None): | |
""" | |
Form the submodule generated by a list of :py:class:`~.ModuleElement` | |
belonging to this module. | |
Examples | |
======== | |
>>> from sympy.polys import Poly, cyclotomic_poly | |
>>> from sympy.polys.numberfields.modules import PowerBasis | |
>>> T = Poly(cyclotomic_poly(5)) | |
>>> A = PowerBasis(T) | |
>>> gens = [A(0), 2*A(1), 3*A(2), 4*A(3)//5] | |
>>> B = A.submodule_from_gens(gens) | |
>>> print(B) # doctest: +SKIP | |
Submodule[[5, 0, 0, 0], [0, 10, 0, 0], [0, 0, 15, 0], [0, 0, 0, 4]]/5 | |
Parameters | |
========== | |
gens : list of :py:class:`~.ModuleElement` belonging to this module. | |
hnf : boolean, optional (default=True) | |
If True, we will reduce the matrix into Hermite Normal Form before | |
forming the :py:class:`~.Submodule`. | |
hnf_modulus : int, None, optional (default=None) | |
Modulus for use in the HNF reduction algorithm. See | |
:py:func:`~sympy.polys.matrices.normalforms.hermite_normal_form`. | |
Returns | |
======= | |
:py:class:`~.Submodule` | |
See Also | |
======== | |
submodule_from_matrix | |
""" | |
if not all(g.module == self for g in gens): | |
raise ValueError('Generators must belong to this module.') | |
n = len(gens) | |
if n == 0: | |
raise ValueError('Need at least one generator.') | |
m = gens[0].n | |
d = gens[0].denom if n == 1 else ilcm(*[g.denom for g in gens]) | |
B = DomainMatrix.zeros((m, 0), ZZ).hstack(*[(d // g.denom) * g.col for g in gens]) | |
if hnf: | |
B = hermite_normal_form(B, D=hnf_modulus) | |
return self.submodule_from_matrix(B, denom=d) | |
def submodule_from_matrix(self, B, denom=1): | |
""" | |
Form the submodule generated by the elements of this module indicated | |
by the columns of a matrix, with an optional denominator. | |
Examples | |
======== | |
>>> from sympy.polys import Poly, cyclotomic_poly, ZZ | |
>>> from sympy.polys.matrices import DM | |
>>> from sympy.polys.numberfields.modules import PowerBasis | |
>>> T = Poly(cyclotomic_poly(5)) | |
>>> A = PowerBasis(T) | |
>>> B = A.submodule_from_matrix(DM([ | |
... [0, 10, 0, 0], | |
... [0, 0, 7, 0], | |
... ], ZZ).transpose(), denom=15) | |
>>> print(B) # doctest: +SKIP | |
Submodule[[0, 10, 0, 0], [0, 0, 7, 0]]/15 | |
Parameters | |
========== | |
B : :py:class:`~.DomainMatrix` over :ref:`ZZ` | |
Each column gives the numerators of the coefficients of one | |
generator of the submodule. Thus, the number of rows of *B* must | |
equal the number of generators of the present module. | |
denom : int, optional (default=1) | |
Common denominator for all generators of the submodule. | |
Returns | |
======= | |
:py:class:`~.Submodule` | |
Raises | |
====== | |
ValueError | |
If the given matrix *B* is not over :ref:`ZZ` or its number of rows | |
does not equal the number of generators of the present module. | |
See Also | |
======== | |
submodule_from_gens | |
""" | |
m, n = B.shape | |
if not B.domain.is_ZZ: | |
raise ValueError('Matrix must be over ZZ.') | |
if not m == self.n: | |
raise ValueError('Matrix row count must match base module.') | |
return Submodule(self, B, denom=denom) | |
def whole_submodule(self): | |
""" | |
Return a submodule equal to this entire module. | |
Explanation | |
=========== | |
This is useful when you have a :py:class:`~.PowerBasis` and want to | |
turn it into a :py:class:`~.Submodule` (in order to use methods | |
belonging to the latter). | |
""" | |
B = DomainMatrix.eye(self.n, ZZ) | |
return self.submodule_from_matrix(B) | |
def endomorphism_ring(self): | |
"""Form the :py:class:`~.EndomorphismRing` for this module.""" | |
return EndomorphismRing(self) | |
class PowerBasis(Module): | |
"""The module generated by the powers of an algebraic integer.""" | |
def __init__(self, T): | |
""" | |
Parameters | |
========== | |
T : :py:class:`~.Poly`, :py:class:`~.AlgebraicField` | |
Either (1) the monic, irreducible, univariate polynomial over | |
:ref:`ZZ`, a root of which is the generator of the power basis, | |
or (2) an :py:class:`~.AlgebraicField` whose primitive element | |
is the generator of the power basis. | |
""" | |
K = None | |
if isinstance(T, AlgebraicField): | |
K, T = T, T.ext.minpoly_of_element() | |
# Sometimes incoming Polys are formally over QQ, although all their | |
# coeffs are integral. We want them to be formally over ZZ. | |
T = T.set_domain(ZZ) | |
self.K = K | |
self.T = T | |
self._n = T.degree() | |
self._mult_tab = None | |
def number_field(self): | |
return self.K | |
def __repr__(self): | |
return f'PowerBasis({self.T.as_expr()})' | |
def __eq__(self, other): | |
if isinstance(other, PowerBasis): | |
return self.T == other.T | |
return NotImplemented | |
def n(self): | |
return self._n | |
def mult_tab(self): | |
if self._mult_tab is None: | |
self.compute_mult_tab() | |
return self._mult_tab | |
def compute_mult_tab(self): | |
theta_pow = AlgIntPowers(self.T) | |
M = {} | |
n = self.n | |
for u in range(n): | |
M[u] = {} | |
for v in range(u, n): | |
M[u][v] = theta_pow[u + v] | |
self._mult_tab = M | |
def represent(self, elt): | |
r""" | |
Represent a module element as an integer-linear combination over the | |
generators of this module. | |
See Also | |
======== | |
.Module.represent | |
.Submodule.represent | |
""" | |
if elt.module == self and elt.denom == 1: | |
return elt.column() | |
else: | |
raise ClosureFailure('Element not representable in ZZ[theta].') | |
def starts_with_unity(self): | |
return True | |
def element_from_rational(self, a): | |
return self(0) * a | |
def element_from_poly(self, f): | |
""" | |
Produce an element of this module, representing *f* after reduction mod | |
our defining minimal polynomial. | |
Parameters | |
========== | |
f : :py:class:`~.Poly` over :ref:`ZZ` in same var as our defining poly. | |
Returns | |
======= | |
:py:class:`~.PowerBasisElement` | |
""" | |
n, k = self.n, f.degree() | |
if k >= n: | |
f = f % self.T | |
if f == 0: | |
return self.zero() | |
d, c = dup_clear_denoms(f.rep.to_list(), QQ, convert=True) | |
c = list(reversed(c)) | |
ell = len(c) | |
z = [ZZ(0)] * (n - ell) | |
col = to_col(c + z) | |
return self(col, denom=d) | |
def _element_from_rep_and_mod(self, rep, mod): | |
""" | |
Produce a PowerBasisElement representing a given algebraic number. | |
Parameters | |
========== | |
rep : list of coeffs | |
Represents the number as polynomial in the primitive element of the | |
field. | |
mod : list of coeffs | |
Represents the minimal polynomial of the primitive element of the | |
field. | |
Returns | |
======= | |
:py:class:`~.PowerBasisElement` | |
""" | |
if mod != self.T.rep.to_list(): | |
raise UnificationFailed('Element does not appear to be in the same field.') | |
return self.element_from_poly(Poly(rep, self.T.gen)) | |
def element_from_ANP(self, a): | |
"""Convert an ANP into a PowerBasisElement. """ | |
return self._element_from_rep_and_mod(a.to_list(), a.mod_to_list()) | |
def element_from_alg_num(self, a): | |
"""Convert an AlgebraicNumber into a PowerBasisElement. """ | |
return self._element_from_rep_and_mod(a.rep.to_list(), a.minpoly.rep.to_list()) | |
class Submodule(Module, IntegerPowerable): | |
"""A submodule of another module.""" | |
def __init__(self, parent, matrix, denom=1, mult_tab=None): | |
""" | |
Parameters | |
========== | |
parent : :py:class:`~.Module` | |
The module from which this one is derived. | |
matrix : :py:class:`~.DomainMatrix` over :ref:`ZZ` | |
The matrix whose columns define this submodule's generators as | |
linear combinations over the parent's generators. | |
denom : int, optional (default=1) | |
Denominator for the coefficients given by the matrix. | |
mult_tab : dict, ``None``, optional | |
If already known, the multiplication table for this module may be | |
supplied. | |
""" | |
self._parent = parent | |
self._matrix = matrix | |
self._denom = denom | |
self._mult_tab = mult_tab | |
self._n = matrix.shape[1] | |
self._QQ_matrix = None | |
self._starts_with_unity = None | |
self._is_sq_maxrank_HNF = None | |
def __repr__(self): | |
r = 'Submodule' + repr(self.matrix.transpose().to_Matrix().tolist()) | |
if self.denom > 1: | |
r += f'/{self.denom}' | |
return r | |
def reduced(self): | |
""" | |
Produce a reduced version of this submodule. | |
Explanation | |
=========== | |
In the reduced version, it is guaranteed that 1 is the only positive | |
integer dividing both the submodule's denominator, and every entry in | |
the submodule's matrix. | |
Returns | |
======= | |
:py:class:`~.Submodule` | |
""" | |
if self.denom == 1: | |
return self | |
g = igcd(self.denom, *self.coeffs) | |
if g == 1: | |
return self | |
return type(self)(self.parent, (self.matrix / g).convert_to(ZZ), denom=self.denom // g, mult_tab=self._mult_tab) | |
def discard_before(self, r): | |
""" | |
Produce a new module by discarding all generators before a given | |
index *r*. | |
""" | |
W = self.matrix[:, r:] | |
s = self.n - r | |
M = None | |
mt = self._mult_tab | |
if mt is not None: | |
M = {} | |
for u in range(s): | |
M[u] = {} | |
for v in range(u, s): | |
M[u][v] = mt[r + u][r + v][r:] | |
return Submodule(self.parent, W, denom=self.denom, mult_tab=M) | |
def n(self): | |
return self._n | |
def mult_tab(self): | |
if self._mult_tab is None: | |
self.compute_mult_tab() | |
return self._mult_tab | |
def compute_mult_tab(self): | |
gens = self.basis_element_pullbacks() | |
M = {} | |
n = self.n | |
for u in range(n): | |
M[u] = {} | |
for v in range(u, n): | |
M[u][v] = self.represent(gens[u] * gens[v]).flat() | |
self._mult_tab = M | |
def parent(self): | |
return self._parent | |
def matrix(self): | |
return self._matrix | |
def coeffs(self): | |
return self.matrix.flat() | |
def denom(self): | |
return self._denom | |
def QQ_matrix(self): | |
""" | |
:py:class:`~.DomainMatrix` over :ref:`QQ`, equal to | |
``self.matrix / self.denom``, and guaranteed to be dense. | |
Explanation | |
=========== | |
Depending on how it is formed, a :py:class:`~.DomainMatrix` may have | |
an internal representation that is sparse or dense. We guarantee a | |
dense representation here, so that tests for equivalence of submodules | |
always come out as expected. | |
Examples | |
======== | |
>>> from sympy.polys import Poly, cyclotomic_poly, ZZ | |
>>> from sympy.abc import x | |
>>> from sympy.polys.matrices import DomainMatrix | |
>>> from sympy.polys.numberfields.modules import PowerBasis | |
>>> T = Poly(cyclotomic_poly(5, x)) | |
>>> A = PowerBasis(T) | |
>>> B = A.submodule_from_matrix(3*DomainMatrix.eye(4, ZZ), denom=6) | |
>>> C = A.submodule_from_matrix(DomainMatrix.eye(4, ZZ), denom=2) | |
>>> print(B.QQ_matrix == C.QQ_matrix) | |
True | |
Returns | |
======= | |
:py:class:`~.DomainMatrix` over :ref:`QQ` | |
""" | |
if self._QQ_matrix is None: | |
self._QQ_matrix = (self.matrix / self.denom).to_dense() | |
return self._QQ_matrix | |
def starts_with_unity(self): | |
if self._starts_with_unity is None: | |
self._starts_with_unity = self(0).equiv(1) | |
return self._starts_with_unity | |
def is_sq_maxrank_HNF(self): | |
if self._is_sq_maxrank_HNF is None: | |
self._is_sq_maxrank_HNF = is_sq_maxrank_HNF(self._matrix) | |
return self._is_sq_maxrank_HNF | |
def is_power_basis_submodule(self): | |
return isinstance(self.parent, PowerBasis) | |
def element_from_rational(self, a): | |
if self.starts_with_unity(): | |
return self(0) * a | |
else: | |
return self.parent.element_from_rational(a) | |
def basis_element_pullbacks(self): | |
""" | |
Return list of this submodule's basis elements as elements of the | |
submodule's parent module. | |
""" | |
return [e.to_parent() for e in self.basis_elements()] | |
def represent(self, elt): | |
""" | |
Represent a module element as an integer-linear combination over the | |
generators of this module. | |
See Also | |
======== | |
.Module.represent | |
.PowerBasis.represent | |
""" | |
if elt.module == self: | |
return elt.column() | |
elif elt.module == self.parent: | |
try: | |
# The given element should be a ZZ-linear combination over our | |
# basis vectors; however, due to the presence of denominators, | |
# we need to solve over QQ. | |
A = self.QQ_matrix | |
b = elt.QQ_col | |
x = A._solve(b)[0].transpose() | |
x = x.convert_to(ZZ) | |
except DMBadInputError: | |
raise ClosureFailure('Element outside QQ-span of this basis.') | |
except CoercionFailed: | |
raise ClosureFailure('Element in QQ-span but not ZZ-span of this basis.') | |
return x | |
elif isinstance(self.parent, Submodule): | |
coeffs_in_parent = self.parent.represent(elt) | |
parent_element = self.parent(coeffs_in_parent) | |
return self.represent(parent_element) | |
else: | |
raise ClosureFailure('Element outside ancestor chain of this module.') | |
def is_compat_submodule(self, other): | |
return isinstance(other, Submodule) and other.parent == self.parent | |
def __eq__(self, other): | |
if self.is_compat_submodule(other): | |
return other.QQ_matrix == self.QQ_matrix | |
return NotImplemented | |
def add(self, other, hnf=True, hnf_modulus=None): | |
""" | |
Add this :py:class:`~.Submodule` to another. | |
Explanation | |
=========== | |
This represents the module generated by the union of the two modules' | |
sets of generators. | |
Parameters | |
========== | |
other : :py:class:`~.Submodule` | |
hnf : boolean, optional (default=True) | |
If ``True``, reduce the matrix of the combined module to its | |
Hermite Normal Form. | |
hnf_modulus : :ref:`ZZ`, None, optional | |
If a positive integer is provided, use this as modulus in the | |
HNF reduction. See | |
:py:func:`~sympy.polys.matrices.normalforms.hermite_normal_form`. | |
Returns | |
======= | |
:py:class:`~.Submodule` | |
""" | |
d, e = self.denom, other.denom | |
m = ilcm(d, e) | |
a, b = m // d, m // e | |
B = (a * self.matrix).hstack(b * other.matrix) | |
if hnf: | |
B = hermite_normal_form(B, D=hnf_modulus) | |
return self.parent.submodule_from_matrix(B, denom=m) | |
def __add__(self, other): | |
if self.is_compat_submodule(other): | |
return self.add(other) | |
return NotImplemented | |
__radd__ = __add__ | |
def mul(self, other, hnf=True, hnf_modulus=None): | |
""" | |
Multiply this :py:class:`~.Submodule` by a rational number, a | |
:py:class:`~.ModuleElement`, or another :py:class:`~.Submodule`. | |
Explanation | |
=========== | |
To multiply by a rational number or :py:class:`~.ModuleElement` means | |
to form the submodule whose generators are the products of this | |
quantity with all the generators of the present submodule. | |
To multiply by another :py:class:`~.Submodule` means to form the | |
submodule whose generators are all the products of one generator from | |
the one submodule, and one generator from the other. | |
Parameters | |
========== | |
other : int, :ref:`ZZ`, :ref:`QQ`, :py:class:`~.ModuleElement`, :py:class:`~.Submodule` | |
hnf : boolean, optional (default=True) | |
If ``True``, reduce the matrix of the product module to its | |
Hermite Normal Form. | |
hnf_modulus : :ref:`ZZ`, None, optional | |
If a positive integer is provided, use this as modulus in the | |
HNF reduction. See | |
:py:func:`~sympy.polys.matrices.normalforms.hermite_normal_form`. | |
Returns | |
======= | |
:py:class:`~.Submodule` | |
""" | |
if is_rat(other): | |
a, b = get_num_denom(other) | |
if a == b == 1: | |
return self | |
else: | |
return Submodule(self.parent, | |
self.matrix * a, denom=self.denom * b, | |
mult_tab=None).reduced() | |
elif isinstance(other, ModuleElement) and other.module == self.parent: | |
# The submodule is multiplied by an element of the parent module. | |
# We presume this means we want a new submodule of the parent module. | |
gens = [other * e for e in self.basis_element_pullbacks()] | |
return self.parent.submodule_from_gens(gens, hnf=hnf, hnf_modulus=hnf_modulus) | |
elif self.is_compat_submodule(other): | |
# This case usually means you're multiplying ideals, and want another | |
# ideal, i.e. another submodule of the same parent module. | |
alphas, betas = self.basis_element_pullbacks(), other.basis_element_pullbacks() | |
gens = [a * b for a in alphas for b in betas] | |
return self.parent.submodule_from_gens(gens, hnf=hnf, hnf_modulus=hnf_modulus) | |
return NotImplemented | |
def __mul__(self, other): | |
return self.mul(other) | |
__rmul__ = __mul__ | |
def _first_power(self): | |
return self | |
def reduce_element(self, elt): | |
r""" | |
If this submodule $B$ has defining matrix $W$ in square, maximal-rank | |
Hermite normal form, then, given an element $x$ of the parent module | |
$A$, we produce an element $y \in A$ such that $x - y \in B$, and the | |
$i$th coordinate of $y$ satisfies $0 \leq y_i < w_{i,i}$. This | |
representative $y$ is unique, in the sense that every element of | |
the coset $x + B$ reduces to it under this procedure. | |
Explanation | |
=========== | |
In the special case where $A$ is a power basis for a number field $K$, | |
and $B$ is a submodule representing an ideal $I$, this operation | |
represents one of a few important ways of reducing an element of $K$ | |
modulo $I$ to obtain a "small" representative. See [Cohen00]_ Section | |
1.4.3. | |
Examples | |
======== | |
>>> from sympy import QQ, Poly, symbols | |
>>> t = symbols('t') | |
>>> k = QQ.alg_field_from_poly(Poly(t**3 + t**2 - 2*t + 8)) | |
>>> Zk = k.maximal_order() | |
>>> A = Zk.parent | |
>>> B = (A(2) - 3*A(0))*Zk | |
>>> B.reduce_element(A(2)) | |
[3, 0, 0] | |
Parameters | |
========== | |
elt : :py:class:`~.ModuleElement` | |
An element of this submodule's parent module. | |
Returns | |
======= | |
elt : :py:class:`~.ModuleElement` | |
An element of this submodule's parent module. | |
Raises | |
====== | |
NotImplementedError | |
If the given :py:class:`~.ModuleElement` does not belong to this | |
submodule's parent module. | |
StructureError | |
If this submodule's defining matrix is not in square, maximal-rank | |
Hermite normal form. | |
References | |
========== | |
.. [Cohen00] Cohen, H. *Advanced Topics in Computational Number | |
Theory.* | |
""" | |
if not elt.module == self.parent: | |
raise NotImplementedError | |
if not self.is_sq_maxrank_HNF(): | |
msg = "Reduction not implemented unless matrix square max-rank HNF" | |
raise StructureError(msg) | |
B = self.basis_element_pullbacks() | |
a = elt | |
for i in range(self.n - 1, -1, -1): | |
b = B[i] | |
q = a.coeffs[i]*b.denom // (b.coeffs[i]*a.denom) | |
a -= q*b | |
return a | |
def is_sq_maxrank_HNF(dm): | |
r""" | |
Say whether a :py:class:`~.DomainMatrix` is in that special case of Hermite | |
Normal Form, in which the matrix is also square and of maximal rank. | |
Explanation | |
=========== | |
We commonly work with :py:class:`~.Submodule` instances whose matrix is in | |
this form, and it can be useful to be able to check that this condition is | |
satisfied. | |
For example this is the case with the :py:class:`~.Submodule` ``ZK`` | |
returned by :py:func:`~sympy.polys.numberfields.basis.round_two`, which | |
represents the maximal order in a number field, and with ideals formed | |
therefrom, such as ``2 * ZK``. | |
""" | |
if dm.domain.is_ZZ and dm.is_square and dm.is_upper: | |
n = dm.shape[0] | |
for i in range(n): | |
d = dm[i, i].element | |
if d <= 0: | |
return False | |
for j in range(i + 1, n): | |
if not (0 <= dm[i, j].element < d): | |
return False | |
return True | |
return False | |
def make_mod_elt(module, col, denom=1): | |
r""" | |
Factory function which builds a :py:class:`~.ModuleElement`, but ensures | |
that it is a :py:class:`~.PowerBasisElement` if the module is a | |
:py:class:`~.PowerBasis`. | |
""" | |
if isinstance(module, PowerBasis): | |
return PowerBasisElement(module, col, denom=denom) | |
else: | |
return ModuleElement(module, col, denom=denom) | |
class ModuleElement(IntegerPowerable): | |
r""" | |
Represents an element of a :py:class:`~.Module`. | |
NOTE: Should not be constructed directly. Use the | |
:py:meth:`~.Module.__call__` method or the :py:func:`make_mod_elt()` | |
factory function instead. | |
""" | |
def __init__(self, module, col, denom=1): | |
""" | |
Parameters | |
========== | |
module : :py:class:`~.Module` | |
The module to which this element belongs. | |
col : :py:class:`~.DomainMatrix` over :ref:`ZZ` | |
Column vector giving the numerators of the coefficients of this | |
element. | |
denom : int, optional (default=1) | |
Denominator for the coefficients of this element. | |
""" | |
self.module = module | |
self.col = col | |
self.denom = denom | |
self._QQ_col = None | |
def __repr__(self): | |
r = str([int(c) for c in self.col.flat()]) | |
if self.denom > 1: | |
r += f'/{self.denom}' | |
return r | |
def reduced(self): | |
""" | |
Produce a reduced version of this ModuleElement, i.e. one in which the | |
gcd of the denominator together with all numerator coefficients is 1. | |
""" | |
if self.denom == 1: | |
return self | |
g = igcd(self.denom, *self.coeffs) | |
if g == 1: | |
return self | |
return type(self)(self.module, | |
(self.col / g).convert_to(ZZ), | |
denom=self.denom // g) | |
def reduced_mod_p(self, p): | |
""" | |
Produce a version of this :py:class:`~.ModuleElement` in which all | |
numerator coefficients have been reduced mod *p*. | |
""" | |
return make_mod_elt(self.module, | |
self.col.convert_to(FF(p)).convert_to(ZZ), | |
denom=self.denom) | |
def from_int_list(cls, module, coeffs, denom=1): | |
""" | |
Make a :py:class:`~.ModuleElement` from a list of ints (instead of a | |
column vector). | |
""" | |
col = to_col(coeffs) | |
return cls(module, col, denom=denom) | |
def n(self): | |
"""The length of this element's column.""" | |
return self.module.n | |
def __len__(self): | |
return self.n | |
def column(self, domain=None): | |
""" | |
Get a copy of this element's column, optionally converting to a domain. | |
""" | |
if domain is None: | |
return self.col.copy() | |
else: | |
return self.col.convert_to(domain) | |
def coeffs(self): | |
return self.col.flat() | |
def QQ_col(self): | |
""" | |
:py:class:`~.DomainMatrix` over :ref:`QQ`, equal to | |
``self.col / self.denom``, and guaranteed to be dense. | |
See Also | |
======== | |
.Submodule.QQ_matrix | |
""" | |
if self._QQ_col is None: | |
self._QQ_col = (self.col / self.denom).to_dense() | |
return self._QQ_col | |
def to_parent(self): | |
""" | |
Transform into a :py:class:`~.ModuleElement` belonging to the parent of | |
this element's module. | |
""" | |
if not isinstance(self.module, Submodule): | |
raise ValueError('Not an element of a Submodule.') | |
return make_mod_elt( | |
self.module.parent, self.module.matrix * self.col, | |
denom=self.module.denom * self.denom) | |
def to_ancestor(self, anc): | |
""" | |
Transform into a :py:class:`~.ModuleElement` belonging to a given | |
ancestor of this element's module. | |
Parameters | |
========== | |
anc : :py:class:`~.Module` | |
""" | |
if anc == self.module: | |
return self | |
else: | |
return self.to_parent().to_ancestor(anc) | |
def over_power_basis(self): | |
""" | |
Transform into a :py:class:`~.PowerBasisElement` over our | |
:py:class:`~.PowerBasis` ancestor. | |
""" | |
e = self | |
while not isinstance(e.module, PowerBasis): | |
e = e.to_parent() | |
return e | |
def is_compat(self, other): | |
""" | |
Test whether other is another :py:class:`~.ModuleElement` with same | |
module. | |
""" | |
return isinstance(other, ModuleElement) and other.module == self.module | |
def unify(self, other): | |
""" | |
Try to make a compatible pair of :py:class:`~.ModuleElement`, one | |
equivalent to this one, and one equivalent to the other. | |
Explanation | |
=========== | |
We search for the nearest common ancestor module for the pair of | |
elements, and represent each one there. | |
Returns | |
======= | |
Pair ``(e1, e2)`` | |
Each ``ei`` is a :py:class:`~.ModuleElement`, they belong to the | |
same :py:class:`~.Module`, ``e1`` is equivalent to ``self``, and | |
``e2`` is equivalent to ``other``. | |
Raises | |
====== | |
UnificationFailed | |
If ``self`` and ``other`` have no common ancestor module. | |
""" | |
if self.module == other.module: | |
return self, other | |
nca = self.module.nearest_common_ancestor(other.module) | |
if nca is not None: | |
return self.to_ancestor(nca), other.to_ancestor(nca) | |
raise UnificationFailed(f"Cannot unify {self} with {other}") | |
def __eq__(self, other): | |
if self.is_compat(other): | |
return self.QQ_col == other.QQ_col | |
return NotImplemented | |
def equiv(self, other): | |
""" | |
A :py:class:`~.ModuleElement` may test as equivalent to a rational | |
number or another :py:class:`~.ModuleElement`, if they represent the | |
same algebraic number. | |
Explanation | |
=========== | |
This method is intended to check equivalence only in those cases in | |
which it is easy to test; namely, when *other* is either a | |
:py:class:`~.ModuleElement` that can be unified with this one (i.e. one | |
which shares a common :py:class:`~.PowerBasis` ancestor), or else a | |
rational number (which is easy because every :py:class:`~.PowerBasis` | |
represents every rational number). | |
Parameters | |
========== | |
other : int, :ref:`ZZ`, :ref:`QQ`, :py:class:`~.ModuleElement` | |
Returns | |
======= | |
bool | |
Raises | |
====== | |
UnificationFailed | |
If ``self`` and ``other`` do not share a common | |
:py:class:`~.PowerBasis` ancestor. | |
""" | |
if self == other: | |
return True | |
elif isinstance(other, ModuleElement): | |
a, b = self.unify(other) | |
return a == b | |
elif is_rat(other): | |
if isinstance(self, PowerBasisElement): | |
return self == self.module(0) * other | |
else: | |
return self.over_power_basis().equiv(other) | |
return False | |
def __add__(self, other): | |
""" | |
A :py:class:`~.ModuleElement` can be added to a rational number, or to | |
another :py:class:`~.ModuleElement`. | |
Explanation | |
=========== | |
When the other summand is a rational number, it will be converted into | |
a :py:class:`~.ModuleElement` (belonging to the first ancestor of this | |
module that starts with unity). | |
In all cases, the sum belongs to the nearest common ancestor (NCA) of | |
the modules of the two summands. If the NCA does not exist, we return | |
``NotImplemented``. | |
""" | |
if self.is_compat(other): | |
d, e = self.denom, other.denom | |
m = ilcm(d, e) | |
u, v = m // d, m // e | |
col = to_col([u * a + v * b for a, b in zip(self.coeffs, other.coeffs)]) | |
return type(self)(self.module, col, denom=m).reduced() | |
elif isinstance(other, ModuleElement): | |
try: | |
a, b = self.unify(other) | |
except UnificationFailed: | |
return NotImplemented | |
return a + b | |
elif is_rat(other): | |
return self + self.module.element_from_rational(other) | |
return NotImplemented | |
__radd__ = __add__ | |
def __neg__(self): | |
return self * -1 | |
def __sub__(self, other): | |
return self + (-other) | |
def __rsub__(self, other): | |
return -self + other | |
def __mul__(self, other): | |
""" | |
A :py:class:`~.ModuleElement` can be multiplied by a rational number, | |
or by another :py:class:`~.ModuleElement`. | |
Explanation | |
=========== | |
When the multiplier is a rational number, the product is computed by | |
operating directly on the coefficients of this | |
:py:class:`~.ModuleElement`. | |
When the multiplier is another :py:class:`~.ModuleElement`, the product | |
will belong to the nearest common ancestor (NCA) of the modules of the | |
two operands, and that NCA must have a multiplication table. If the NCA | |
does not exist, we return ``NotImplemented``. If the NCA does not have | |
a mult. table, ``ClosureFailure`` will be raised. | |
""" | |
if self.is_compat(other): | |
M = self.module.mult_tab() | |
A, B = self.col.flat(), other.col.flat() | |
n = self.n | |
C = [0] * n | |
for u in range(n): | |
for v in range(u, n): | |
c = A[u] * B[v] | |
if v > u: | |
c += A[v] * B[u] | |
if c != 0: | |
R = M[u][v] | |
for k in range(n): | |
C[k] += c * R[k] | |
d = self.denom * other.denom | |
return self.from_int_list(self.module, C, denom=d) | |
elif isinstance(other, ModuleElement): | |
try: | |
a, b = self.unify(other) | |
except UnificationFailed: | |
return NotImplemented | |
return a * b | |
elif is_rat(other): | |
a, b = get_num_denom(other) | |
if a == b == 1: | |
return self | |
else: | |
return make_mod_elt(self.module, | |
self.col * a, denom=self.denom * b).reduced() | |
return NotImplemented | |
__rmul__ = __mul__ | |
def _zeroth_power(self): | |
return self.module.one() | |
def _first_power(self): | |
return self | |
def __floordiv__(self, a): | |
if is_rat(a): | |
a = QQ(a) | |
return self * (1/a) | |
elif isinstance(a, ModuleElement): | |
return self * (1//a) | |
return NotImplemented | |
def __rfloordiv__(self, a): | |
return a // self.over_power_basis() | |
def __mod__(self, m): | |
r""" | |
Reduce this :py:class:`~.ModuleElement` mod a :py:class:`~.Submodule`. | |
Parameters | |
========== | |
m : int, :ref:`ZZ`, :ref:`QQ`, :py:class:`~.Submodule` | |
If a :py:class:`~.Submodule`, reduce ``self`` relative to this. | |
If an integer or rational, reduce relative to the | |
:py:class:`~.Submodule` that is our own module times this constant. | |
See Also | |
======== | |
.Submodule.reduce_element | |
""" | |
if is_rat(m): | |
m = m * self.module.whole_submodule() | |
if isinstance(m, Submodule) and m.parent == self.module: | |
return m.reduce_element(self) | |
return NotImplemented | |
class PowerBasisElement(ModuleElement): | |
r""" | |
Subclass for :py:class:`~.ModuleElement` instances whose module is a | |
:py:class:`~.PowerBasis`. | |
""" | |
def T(self): | |
"""Access the defining polynomial of the :py:class:`~.PowerBasis`.""" | |
return self.module.T | |
def numerator(self, x=None): | |
"""Obtain the numerator as a polynomial over :ref:`ZZ`.""" | |
x = x or self.T.gen | |
return Poly(reversed(self.coeffs), x, domain=ZZ) | |
def poly(self, x=None): | |
"""Obtain the number as a polynomial over :ref:`QQ`.""" | |
return self.numerator(x=x) // self.denom | |
def is_rational(self): | |
"""Say whether this element represents a rational number.""" | |
return self.col[1:, :].is_zero_matrix | |
def generator(self): | |
""" | |
Return a :py:class:`~.Symbol` to be used when expressing this element | |
as a polynomial. | |
If we have an associated :py:class:`~.AlgebraicField` whose primitive | |
element has an alias symbol, we use that. Otherwise we use the variable | |
of the minimal polynomial defining the power basis to which we belong. | |
""" | |
K = self.module.number_field | |
return K.ext.alias if K and K.ext.is_aliased else self.T.gen | |
def as_expr(self, x=None): | |
"""Create a Basic expression from ``self``. """ | |
return self.poly(x or self.generator).as_expr() | |
def norm(self, T=None): | |
"""Compute the norm of this number.""" | |
T = T or self.T | |
x = T.gen | |
A = self.numerator(x=x) | |
return T.resultant(A) // self.denom ** self.n | |
def inverse(self): | |
f = self.poly() | |
f_inv = f.invert(self.T) | |
return self.module.element_from_poly(f_inv) | |
def __rfloordiv__(self, a): | |
return self.inverse() * a | |
def _negative_power(self, e, modulo=None): | |
return self.inverse() ** abs(e) | |
def to_ANP(self): | |
"""Convert to an equivalent :py:class:`~.ANP`. """ | |
return ANP(list(reversed(self.QQ_col.flat())), QQ.map(self.T.rep.to_list()), QQ) | |
def to_alg_num(self): | |
""" | |
Try to convert to an equivalent :py:class:`~.AlgebraicNumber`. | |
Explanation | |
=========== | |
In general, the conversion from an :py:class:`~.AlgebraicNumber` to a | |
:py:class:`~.PowerBasisElement` throws away information, because an | |
:py:class:`~.AlgebraicNumber` specifies a complex embedding, while a | |
:py:class:`~.PowerBasisElement` does not. However, in some cases it is | |
possible to convert a :py:class:`~.PowerBasisElement` back into an | |
:py:class:`~.AlgebraicNumber`, namely when the associated | |
:py:class:`~.PowerBasis` has a reference to an | |
:py:class:`~.AlgebraicField`. | |
Returns | |
======= | |
:py:class:`~.AlgebraicNumber` | |
Raises | |
====== | |
StructureError | |
If the :py:class:`~.PowerBasis` to which this element belongs does | |
not have an associated :py:class:`~.AlgebraicField`. | |
""" | |
K = self.module.number_field | |
if K: | |
return K.to_alg_num(self.to_ANP()) | |
raise StructureError("No associated AlgebraicField") | |
class ModuleHomomorphism: | |
r"""A homomorphism from one module to another.""" | |
def __init__(self, domain, codomain, mapping): | |
r""" | |
Parameters | |
========== | |
domain : :py:class:`~.Module` | |
The domain of the mapping. | |
codomain : :py:class:`~.Module` | |
The codomain of the mapping. | |
mapping : callable | |
An arbitrary callable is accepted, but should be chosen so as | |
to represent an actual module homomorphism. In particular, should | |
accept elements of *domain* and return elements of *codomain*. | |
Examples | |
======== | |
>>> from sympy import Poly, cyclotomic_poly | |
>>> from sympy.polys.numberfields.modules import PowerBasis, ModuleHomomorphism | |
>>> T = Poly(cyclotomic_poly(5)) | |
>>> A = PowerBasis(T) | |
>>> B = A.submodule_from_gens([2*A(j) for j in range(4)]) | |
>>> phi = ModuleHomomorphism(A, B, lambda x: 6*x) | |
>>> print(phi.matrix()) # doctest: +SKIP | |
DomainMatrix([[3, 0, 0, 0], [0, 3, 0, 0], [0, 0, 3, 0], [0, 0, 0, 3]], (4, 4), ZZ) | |
""" | |
self.domain = domain | |
self.codomain = codomain | |
self.mapping = mapping | |
def matrix(self, modulus=None): | |
r""" | |
Compute the matrix of this homomorphism. | |
Parameters | |
========== | |
modulus : int, optional | |
A positive prime number $p$ if the matrix should be reduced mod | |
$p$. | |
Returns | |
======= | |
:py:class:`~.DomainMatrix` | |
The matrix is over :ref:`ZZ`, or else over :ref:`GF(p)` if a | |
modulus was given. | |
""" | |
basis = self.domain.basis_elements() | |
cols = [self.codomain.represent(self.mapping(elt)) for elt in basis] | |
if not cols: | |
return DomainMatrix.zeros((self.codomain.n, 0), ZZ).to_dense() | |
M = cols[0].hstack(*cols[1:]) | |
if modulus: | |
M = M.convert_to(FF(modulus)) | |
return M | |
def kernel(self, modulus=None): | |
r""" | |
Compute a Submodule representing the kernel of this homomorphism. | |
Parameters | |
========== | |
modulus : int, optional | |
A positive prime number $p$ if the kernel should be computed mod | |
$p$. | |
Returns | |
======= | |
:py:class:`~.Submodule` | |
This submodule's generators span the kernel of this | |
homomorphism over :ref:`ZZ`, or else over :ref:`GF(p)` if a | |
modulus was given. | |
""" | |
M = self.matrix(modulus=modulus) | |
if modulus is None: | |
M = M.convert_to(QQ) | |
# Note: Even when working over a finite field, what we want here is | |
# the pullback into the integers, so in this case the conversion to ZZ | |
# below is appropriate. When working over ZZ, the kernel should be a | |
# ZZ-submodule, so, while the conversion to QQ above was required in | |
# order for the nullspace calculation to work, conversion back to ZZ | |
# afterward should always work. | |
# TODO: | |
# Watch <https://github.com/sympy/sympy/issues/21834>, which calls | |
# for fraction-free algorithms. If this is implemented, we can skip | |
# the conversion to `QQ` above. | |
K = M.nullspace().convert_to(ZZ).transpose() | |
return self.domain.submodule_from_matrix(K) | |
class ModuleEndomorphism(ModuleHomomorphism): | |
r"""A homomorphism from one module to itself.""" | |
def __init__(self, domain, mapping): | |
r""" | |
Parameters | |
========== | |
domain : :py:class:`~.Module` | |
The common domain and codomain of the mapping. | |
mapping : callable | |
An arbitrary callable is accepted, but should be chosen so as | |
to represent an actual module endomorphism. In particular, should | |
accept and return elements of *domain*. | |
""" | |
super().__init__(domain, domain, mapping) | |
class InnerEndomorphism(ModuleEndomorphism): | |
r""" | |
An inner endomorphism on a module, i.e. the endomorphism corresponding to | |
multiplication by a fixed element. | |
""" | |
def __init__(self, domain, multiplier): | |
r""" | |
Parameters | |
========== | |
domain : :py:class:`~.Module` | |
The domain and codomain of the endomorphism. | |
multiplier : :py:class:`~.ModuleElement` | |
The element $a$ defining the mapping as $x \mapsto a x$. | |
""" | |
super().__init__(domain, lambda x: multiplier * x) | |
self.multiplier = multiplier | |
class EndomorphismRing: | |
r"""The ring of endomorphisms on a module.""" | |
def __init__(self, domain): | |
""" | |
Parameters | |
========== | |
domain : :py:class:`~.Module` | |
The domain and codomain of the endomorphisms. | |
""" | |
self.domain = domain | |
def inner_endomorphism(self, multiplier): | |
r""" | |
Form an inner endomorphism belonging to this endomorphism ring. | |
Parameters | |
========== | |
multiplier : :py:class:`~.ModuleElement` | |
Element $a$ defining the inner endomorphism $x \mapsto a x$. | |
Returns | |
======= | |
:py:class:`~.InnerEndomorphism` | |
""" | |
return InnerEndomorphism(self.domain, multiplier) | |
def represent(self, element): | |
r""" | |
Represent an element of this endomorphism ring, as a single column | |
vector. | |
Explanation | |
=========== | |
Let $M$ be a module, and $E$ its ring of endomorphisms. Let $N$ be | |
another module, and consider a homomorphism $\varphi: N \rightarrow E$. | |
In the event that $\varphi$ is to be represented by a matrix $A$, each | |
column of $A$ must represent an element of $E$. This is possible when | |
the elements of $E$ are themselves representable as matrices, by | |
stacking the columns of such a matrix into a single column. | |
This method supports calculating such matrices $A$, by representing | |
an element of this endomorphism ring first as a matrix, and then | |
stacking that matrix's columns into a single column. | |
Examples | |
======== | |
Note that in these examples we print matrix transposes, to make their | |
columns easier to inspect. | |
>>> from sympy import Poly, cyclotomic_poly | |
>>> from sympy.polys.numberfields.modules import PowerBasis | |
>>> from sympy.polys.numberfields.modules import ModuleHomomorphism | |
>>> T = Poly(cyclotomic_poly(5)) | |
>>> M = PowerBasis(T) | |
>>> E = M.endomorphism_ring() | |
Let $\zeta$ be a primitive 5th root of unity, a generator of our field, | |
and consider the inner endomorphism $\tau$ on the ring of integers, | |
induced by $\zeta$: | |
>>> zeta = M(1) | |
>>> tau = E.inner_endomorphism(zeta) | |
>>> tau.matrix().transpose() # doctest: +SKIP | |
DomainMatrix( | |
[[0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1], [-1, -1, -1, -1]], | |
(4, 4), ZZ) | |
The matrix representation of $\tau$ is as expected. The first column | |
shows that multiplying by $\zeta$ carries $1$ to $\zeta$, the second | |
column that it carries $\zeta$ to $\zeta^2$, and so forth. | |
The ``represent`` method of the endomorphism ring ``E`` stacks these | |
into a single column: | |
>>> E.represent(tau).transpose() # doctest: +SKIP | |
DomainMatrix( | |
[[0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, -1, -1, -1, -1]], | |
(1, 16), ZZ) | |
This is useful when we want to consider a homomorphism $\varphi$ having | |
``E`` as codomain: | |
>>> phi = ModuleHomomorphism(M, E, lambda x: E.inner_endomorphism(x)) | |
and we want to compute the matrix of such a homomorphism: | |
>>> phi.matrix().transpose() # doctest: +SKIP | |
DomainMatrix( | |
[[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], | |
[0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, -1, -1, -1, -1], | |
[0, 0, 1, 0, 0, 0, 0, 1, -1, -1, -1, -1, 1, 0, 0, 0], | |
[0, 0, 0, 1, -1, -1, -1, -1, 1, 0, 0, 0, 0, 1, 0, 0]], | |
(4, 16), ZZ) | |
Note that the stacked matrix of $\tau$ occurs as the second column in | |
this example. This is because $\zeta$ is the second basis element of | |
``M``, and $\varphi(\zeta) = \tau$. | |
Parameters | |
========== | |
element : :py:class:`~.ModuleEndomorphism` belonging to this ring. | |
Returns | |
======= | |
:py:class:`~.DomainMatrix` | |
Column vector equalling the vertical stacking of all the columns | |
of the matrix that represents the given *element* as a mapping. | |
""" | |
if isinstance(element, ModuleEndomorphism) and element.domain == self.domain: | |
M = element.matrix() | |
# Transform the matrix into a single column, which should reproduce | |
# the original columns, one after another. | |
m, n = M.shape | |
if n == 0: | |
return M | |
return M[:, 0].vstack(*[M[:, j] for j in range(1, n)]) | |
raise NotImplementedError | |
def find_min_poly(alpha, domain, x=None, powers=None): | |
r""" | |
Find a polynomial of least degree (not necessarily irreducible) satisfied | |
by an element of a finitely-generated ring with unity. | |
Examples | |
======== | |
For the $n$th cyclotomic field, $n$ an odd prime, consider the quadratic | |
equation whose roots are the two periods of length $(n-1)/2$. Article 356 | |
of Gauss tells us that we should get $x^2 + x - (n-1)/4$ or | |
$x^2 + x + (n+1)/4$ according to whether $n$ is 1 or 3 mod 4, respectively. | |
>>> from sympy import Poly, cyclotomic_poly, primitive_root, QQ | |
>>> from sympy.abc import x | |
>>> from sympy.polys.numberfields.modules import PowerBasis, find_min_poly | |
>>> n = 13 | |
>>> g = primitive_root(n) | |
>>> C = PowerBasis(Poly(cyclotomic_poly(n, x))) | |
>>> ee = [g**(2*k+1) % n for k in range((n-1)//2)] | |
>>> eta = sum(C(e) for e in ee) | |
>>> print(find_min_poly(eta, QQ, x=x).as_expr()) | |
x**2 + x - 3 | |
>>> n = 19 | |
>>> g = primitive_root(n) | |
>>> C = PowerBasis(Poly(cyclotomic_poly(n, x))) | |
>>> ee = [g**(2*k+2) % n for k in range((n-1)//2)] | |
>>> eta = sum(C(e) for e in ee) | |
>>> print(find_min_poly(eta, QQ, x=x).as_expr()) | |
x**2 + x + 5 | |
Parameters | |
========== | |
alpha : :py:class:`~.ModuleElement` | |
The element whose min poly is to be found, and whose module has | |
multiplication and starts with unity. | |
domain : :py:class:`~.Domain` | |
The desired domain of the polynomial. | |
x : :py:class:`~.Symbol`, optional | |
The desired variable for the polynomial. | |
powers : list, optional | |
If desired, pass an empty list. The powers of *alpha* (as | |
:py:class:`~.ModuleElement` instances) from the zeroth up to the degree | |
of the min poly will be recorded here, as we compute them. | |
Returns | |
======= | |
:py:class:`~.Poly`, ``None`` | |
The minimal polynomial for alpha, or ``None`` if no polynomial could be | |
found over the desired domain. | |
Raises | |
====== | |
MissingUnityError | |
If the module to which alpha belongs does not start with unity. | |
ClosureFailure | |
If the module to which alpha belongs is not closed under | |
multiplication. | |
""" | |
R = alpha.module | |
if not R.starts_with_unity(): | |
raise MissingUnityError("alpha must belong to finitely generated ring with unity.") | |
if powers is None: | |
powers = [] | |
one = R(0) | |
powers.append(one) | |
powers_matrix = one.column(domain=domain) | |
ak = alpha | |
m = None | |
for k in range(1, R.n + 1): | |
powers.append(ak) | |
ak_col = ak.column(domain=domain) | |
try: | |
X = powers_matrix._solve(ak_col)[0] | |
except DMBadInputError: | |
# This means alpha^k still isn't in the domain-span of the lower powers. | |
powers_matrix = powers_matrix.hstack(ak_col) | |
ak *= alpha | |
else: | |
# alpha^k is in the domain-span of the lower powers, so we have found a | |
# minimal-degree poly for alpha. | |
coeffs = [1] + [-c for c in reversed(X.to_list_flat())] | |
x = x or Dummy('x') | |
if domain.is_FF: | |
m = Poly(coeffs, x, modulus=domain.mod) | |
else: | |
m = Poly(coeffs, x, domain=domain) | |
break | |
return m | |