Spaces:
Running
Running
""" | |
Computations with homomorphisms of modules and rings. | |
This module implements classes for representing homomorphisms of rings and | |
their modules. Instead of instantiating the classes directly, you should use | |
the function ``homomorphism(from, to, matrix)`` to create homomorphism objects. | |
""" | |
from sympy.polys.agca.modules import (Module, FreeModule, QuotientModule, | |
SubModule, SubQuotientModule) | |
from sympy.polys.polyerrors import CoercionFailed | |
# The main computational task for module homomorphisms is kernels. | |
# For this reason, the concrete classes are organised by domain module type. | |
class ModuleHomomorphism: | |
""" | |
Abstract base class for module homomoprhisms. Do not instantiate. | |
Instead, use the ``homomorphism`` function: | |
>>> from sympy import QQ | |
>>> from sympy.abc import x | |
>>> from sympy.polys.agca import homomorphism | |
>>> F = QQ.old_poly_ring(x).free_module(2) | |
>>> homomorphism(F, F, [[1, 0], [0, 1]]) | |
Matrix([ | |
[1, 0], : QQ[x]**2 -> QQ[x]**2 | |
[0, 1]]) | |
Attributes: | |
- ring - the ring over which we are considering modules | |
- domain - the domain module | |
- codomain - the codomain module | |
- _ker - cached kernel | |
- _img - cached image | |
Non-implemented methods: | |
- _kernel | |
- _image | |
- _restrict_domain | |
- _restrict_codomain | |
- _quotient_domain | |
- _quotient_codomain | |
- _apply | |
- _mul_scalar | |
- _compose | |
- _add | |
""" | |
def __init__(self, domain, codomain): | |
if not isinstance(domain, Module): | |
raise TypeError('Source must be a module, got %s' % domain) | |
if not isinstance(codomain, Module): | |
raise TypeError('Target must be a module, got %s' % codomain) | |
if domain.ring != codomain.ring: | |
raise ValueError('Source and codomain must be over same ring, ' | |
'got %s != %s' % (domain, codomain)) | |
self.domain = domain | |
self.codomain = codomain | |
self.ring = domain.ring | |
self._ker = None | |
self._img = None | |
def kernel(self): | |
r""" | |
Compute the kernel of ``self``. | |
That is, if ``self`` is the homomorphism `\phi: M \to N`, then compute | |
`ker(\phi) = \{x \in M | \phi(x) = 0\}`. This is a submodule of `M`. | |
Examples | |
======== | |
>>> from sympy import QQ | |
>>> from sympy.abc import x | |
>>> from sympy.polys.agca import homomorphism | |
>>> F = QQ.old_poly_ring(x).free_module(2) | |
>>> homomorphism(F, F, [[1, 0], [x, 0]]).kernel() | |
<[x, -1]> | |
""" | |
if self._ker is None: | |
self._ker = self._kernel() | |
return self._ker | |
def image(self): | |
r""" | |
Compute the image of ``self``. | |
That is, if ``self`` is the homomorphism `\phi: M \to N`, then compute | |
`im(\phi) = \{\phi(x) | x \in M \}`. This is a submodule of `N`. | |
Examples | |
======== | |
>>> from sympy import QQ | |
>>> from sympy.abc import x | |
>>> from sympy.polys.agca import homomorphism | |
>>> F = QQ.old_poly_ring(x).free_module(2) | |
>>> homomorphism(F, F, [[1, 0], [x, 0]]).image() == F.submodule([1, 0]) | |
True | |
""" | |
if self._img is None: | |
self._img = self._image() | |
return self._img | |
def _kernel(self): | |
"""Compute the kernel of ``self``.""" | |
raise NotImplementedError | |
def _image(self): | |
"""Compute the image of ``self``.""" | |
raise NotImplementedError | |
def _restrict_domain(self, sm): | |
"""Implementation of domain restriction.""" | |
raise NotImplementedError | |
def _restrict_codomain(self, sm): | |
"""Implementation of codomain restriction.""" | |
raise NotImplementedError | |
def _quotient_domain(self, sm): | |
"""Implementation of domain quotient.""" | |
raise NotImplementedError | |
def _quotient_codomain(self, sm): | |
"""Implementation of codomain quotient.""" | |
raise NotImplementedError | |
def restrict_domain(self, sm): | |
""" | |
Return ``self``, with the domain restricted to ``sm``. | |
Here ``sm`` has to be a submodule of ``self.domain``. | |
Examples | |
======== | |
>>> from sympy import QQ | |
>>> from sympy.abc import x | |
>>> from sympy.polys.agca import homomorphism | |
>>> F = QQ.old_poly_ring(x).free_module(2) | |
>>> h = homomorphism(F, F, [[1, 0], [x, 0]]) | |
>>> h | |
Matrix([ | |
[1, x], : QQ[x]**2 -> QQ[x]**2 | |
[0, 0]]) | |
>>> h.restrict_domain(F.submodule([1, 0])) | |
Matrix([ | |
[1, x], : <[1, 0]> -> QQ[x]**2 | |
[0, 0]]) | |
This is the same as just composing on the right with the submodule | |
inclusion: | |
>>> h * F.submodule([1, 0]).inclusion_hom() | |
Matrix([ | |
[1, x], : <[1, 0]> -> QQ[x]**2 | |
[0, 0]]) | |
""" | |
if not self.domain.is_submodule(sm): | |
raise ValueError('sm must be a submodule of %s, got %s' | |
% (self.domain, sm)) | |
if sm == self.domain: | |
return self | |
return self._restrict_domain(sm) | |
def restrict_codomain(self, sm): | |
""" | |
Return ``self``, with codomain restricted to to ``sm``. | |
Here ``sm`` has to be a submodule of ``self.codomain`` containing the | |
image. | |
Examples | |
======== | |
>>> from sympy import QQ | |
>>> from sympy.abc import x | |
>>> from sympy.polys.agca import homomorphism | |
>>> F = QQ.old_poly_ring(x).free_module(2) | |
>>> h = homomorphism(F, F, [[1, 0], [x, 0]]) | |
>>> h | |
Matrix([ | |
[1, x], : QQ[x]**2 -> QQ[x]**2 | |
[0, 0]]) | |
>>> h.restrict_codomain(F.submodule([1, 0])) | |
Matrix([ | |
[1, x], : QQ[x]**2 -> <[1, 0]> | |
[0, 0]]) | |
""" | |
if not sm.is_submodule(self.image()): | |
raise ValueError('the image %s must contain sm, got %s' | |
% (self.image(), sm)) | |
if sm == self.codomain: | |
return self | |
return self._restrict_codomain(sm) | |
def quotient_domain(self, sm): | |
""" | |
Return ``self`` with domain replaced by ``domain/sm``. | |
Here ``sm`` must be a submodule of ``self.kernel()``. | |
Examples | |
======== | |
>>> from sympy import QQ | |
>>> from sympy.abc import x | |
>>> from sympy.polys.agca import homomorphism | |
>>> F = QQ.old_poly_ring(x).free_module(2) | |
>>> h = homomorphism(F, F, [[1, 0], [x, 0]]) | |
>>> h | |
Matrix([ | |
[1, x], : QQ[x]**2 -> QQ[x]**2 | |
[0, 0]]) | |
>>> h.quotient_domain(F.submodule([-x, 1])) | |
Matrix([ | |
[1, x], : QQ[x]**2/<[-x, 1]> -> QQ[x]**2 | |
[0, 0]]) | |
""" | |
if not self.kernel().is_submodule(sm): | |
raise ValueError('kernel %s must contain sm, got %s' % | |
(self.kernel(), sm)) | |
if sm.is_zero(): | |
return self | |
return self._quotient_domain(sm) | |
def quotient_codomain(self, sm): | |
""" | |
Return ``self`` with codomain replaced by ``codomain/sm``. | |
Here ``sm`` must be a submodule of ``self.codomain``. | |
Examples | |
======== | |
>>> from sympy import QQ | |
>>> from sympy.abc import x | |
>>> from sympy.polys.agca import homomorphism | |
>>> F = QQ.old_poly_ring(x).free_module(2) | |
>>> h = homomorphism(F, F, [[1, 0], [x, 0]]) | |
>>> h | |
Matrix([ | |
[1, x], : QQ[x]**2 -> QQ[x]**2 | |
[0, 0]]) | |
>>> h.quotient_codomain(F.submodule([1, 1])) | |
Matrix([ | |
[1, x], : QQ[x]**2 -> QQ[x]**2/<[1, 1]> | |
[0, 0]]) | |
This is the same as composing with the quotient map on the left: | |
>>> (F/[(1, 1)]).quotient_hom() * h | |
Matrix([ | |
[1, x], : QQ[x]**2 -> QQ[x]**2/<[1, 1]> | |
[0, 0]]) | |
""" | |
if not self.codomain.is_submodule(sm): | |
raise ValueError('sm must be a submodule of codomain %s, got %s' | |
% (self.codomain, sm)) | |
if sm.is_zero(): | |
return self | |
return self._quotient_codomain(sm) | |
def _apply(self, elem): | |
"""Apply ``self`` to ``elem``.""" | |
raise NotImplementedError | |
def __call__(self, elem): | |
return self.codomain.convert(self._apply(self.domain.convert(elem))) | |
def _compose(self, oth): | |
""" | |
Compose ``self`` with ``oth``, that is, return the homomorphism | |
obtained by first applying then ``self``, then ``oth``. | |
(This method is private since in this syntax, it is non-obvious which | |
homomorphism is executed first.) | |
""" | |
raise NotImplementedError | |
def _mul_scalar(self, c): | |
"""Scalar multiplication. ``c`` is guaranteed in self.ring.""" | |
raise NotImplementedError | |
def _add(self, oth): | |
""" | |
Homomorphism addition. | |
``oth`` is guaranteed to be a homomorphism with same domain/codomain. | |
""" | |
raise NotImplementedError | |
def _check_hom(self, oth): | |
"""Helper to check that oth is a homomorphism with same domain/codomain.""" | |
if not isinstance(oth, ModuleHomomorphism): | |
return False | |
return oth.domain == self.domain and oth.codomain == self.codomain | |
def __mul__(self, oth): | |
if isinstance(oth, ModuleHomomorphism) and self.domain == oth.codomain: | |
return oth._compose(self) | |
try: | |
return self._mul_scalar(self.ring.convert(oth)) | |
except CoercionFailed: | |
return NotImplemented | |
# NOTE: _compose will never be called from rmul | |
__rmul__ = __mul__ | |
def __truediv__(self, oth): | |
try: | |
return self._mul_scalar(1/self.ring.convert(oth)) | |
except CoercionFailed: | |
return NotImplemented | |
def __add__(self, oth): | |
if self._check_hom(oth): | |
return self._add(oth) | |
return NotImplemented | |
def __sub__(self, oth): | |
if self._check_hom(oth): | |
return self._add(oth._mul_scalar(self.ring.convert(-1))) | |
return NotImplemented | |
def is_injective(self): | |
""" | |
Return True if ``self`` is injective. | |
That is, check if the elements of the domain are mapped to the same | |
codomain element. | |
Examples | |
======== | |
>>> from sympy import QQ | |
>>> from sympy.abc import x | |
>>> from sympy.polys.agca import homomorphism | |
>>> F = QQ.old_poly_ring(x).free_module(2) | |
>>> h = homomorphism(F, F, [[1, 0], [x, 0]]) | |
>>> h.is_injective() | |
False | |
>>> h.quotient_domain(h.kernel()).is_injective() | |
True | |
""" | |
return self.kernel().is_zero() | |
def is_surjective(self): | |
""" | |
Return True if ``self`` is surjective. | |
That is, check if every element of the codomain has at least one | |
preimage. | |
Examples | |
======== | |
>>> from sympy import QQ | |
>>> from sympy.abc import x | |
>>> from sympy.polys.agca import homomorphism | |
>>> F = QQ.old_poly_ring(x).free_module(2) | |
>>> h = homomorphism(F, F, [[1, 0], [x, 0]]) | |
>>> h.is_surjective() | |
False | |
>>> h.restrict_codomain(h.image()).is_surjective() | |
True | |
""" | |
return self.image() == self.codomain | |
def is_isomorphism(self): | |
""" | |
Return True if ``self`` is an isomorphism. | |
That is, check if every element of the codomain has precisely one | |
preimage. Equivalently, ``self`` is both injective and surjective. | |
Examples | |
======== | |
>>> from sympy import QQ | |
>>> from sympy.abc import x | |
>>> from sympy.polys.agca import homomorphism | |
>>> F = QQ.old_poly_ring(x).free_module(2) | |
>>> h = homomorphism(F, F, [[1, 0], [x, 0]]) | |
>>> h = h.restrict_codomain(h.image()) | |
>>> h.is_isomorphism() | |
False | |
>>> h.quotient_domain(h.kernel()).is_isomorphism() | |
True | |
""" | |
return self.is_injective() and self.is_surjective() | |
def is_zero(self): | |
""" | |
Return True if ``self`` is a zero morphism. | |
That is, check if every element of the domain is mapped to zero | |
under self. | |
Examples | |
======== | |
>>> from sympy import QQ | |
>>> from sympy.abc import x | |
>>> from sympy.polys.agca import homomorphism | |
>>> F = QQ.old_poly_ring(x).free_module(2) | |
>>> h = homomorphism(F, F, [[1, 0], [x, 0]]) | |
>>> h.is_zero() | |
False | |
>>> h.restrict_domain(F.submodule()).is_zero() | |
True | |
>>> h.quotient_codomain(h.image()).is_zero() | |
True | |
""" | |
return self.image().is_zero() | |
def __eq__(self, oth): | |
try: | |
return (self - oth).is_zero() | |
except TypeError: | |
return False | |
def __ne__(self, oth): | |
return not (self == oth) | |
class MatrixHomomorphism(ModuleHomomorphism): | |
r""" | |
Helper class for all homomoprhisms which are expressed via a matrix. | |
That is, for such homomorphisms ``domain`` is contained in a module | |
generated by finitely many elements `e_1, \ldots, e_n`, so that the | |
homomorphism is determined uniquely by its action on the `e_i`. It | |
can thus be represented as a vector of elements of the codomain module, | |
or potentially a supermodule of the codomain module | |
(and hence conventionally as a matrix, if there is a similar interpretation | |
for elements of the codomain module). | |
Note that this class does *not* assume that the `e_i` freely generate a | |
submodule, nor that ``domain`` is even all of this submodule. It exists | |
only to unify the interface. | |
Do not instantiate. | |
Attributes: | |
- matrix - the list of images determining the homomorphism. | |
NOTE: the elements of matrix belong to either self.codomain or | |
self.codomain.container | |
Still non-implemented methods: | |
- kernel | |
- _apply | |
""" | |
def __init__(self, domain, codomain, matrix): | |
ModuleHomomorphism.__init__(self, domain, codomain) | |
if len(matrix) != domain.rank: | |
raise ValueError('Need to provide %s elements, got %s' | |
% (domain.rank, len(matrix))) | |
converter = self.codomain.convert | |
if isinstance(self.codomain, (SubModule, SubQuotientModule)): | |
converter = self.codomain.container.convert | |
self.matrix = tuple(converter(x) for x in matrix) | |
def _sympy_matrix(self): | |
"""Helper function which returns a SymPy matrix ``self.matrix``.""" | |
from sympy.matrices import Matrix | |
c = lambda x: x | |
if isinstance(self.codomain, (QuotientModule, SubQuotientModule)): | |
c = lambda x: x.data | |
return Matrix([[self.ring.to_sympy(y) for y in c(x)] for x in self.matrix]).T | |
def __repr__(self): | |
lines = repr(self._sympy_matrix()).split('\n') | |
t = " : %s -> %s" % (self.domain, self.codomain) | |
s = ' '*len(t) | |
n = len(lines) | |
for i in range(n // 2): | |
lines[i] += s | |
lines[n // 2] += t | |
for i in range(n//2 + 1, n): | |
lines[i] += s | |
return '\n'.join(lines) | |
def _restrict_domain(self, sm): | |
"""Implementation of domain restriction.""" | |
return SubModuleHomomorphism(sm, self.codomain, self.matrix) | |
def _restrict_codomain(self, sm): | |
"""Implementation of codomain restriction.""" | |
return self.__class__(self.domain, sm, self.matrix) | |
def _quotient_domain(self, sm): | |
"""Implementation of domain quotient.""" | |
return self.__class__(self.domain/sm, self.codomain, self.matrix) | |
def _quotient_codomain(self, sm): | |
"""Implementation of codomain quotient.""" | |
Q = self.codomain/sm | |
converter = Q.convert | |
if isinstance(self.codomain, SubModule): | |
converter = Q.container.convert | |
return self.__class__(self.domain, self.codomain/sm, | |
[converter(x) for x in self.matrix]) | |
def _add(self, oth): | |
return self.__class__(self.domain, self.codomain, | |
[x + y for x, y in zip(self.matrix, oth.matrix)]) | |
def _mul_scalar(self, c): | |
return self.__class__(self.domain, self.codomain, [c*x for x in self.matrix]) | |
def _compose(self, oth): | |
return self.__class__(self.domain, oth.codomain, [oth(x) for x in self.matrix]) | |
class FreeModuleHomomorphism(MatrixHomomorphism): | |
""" | |
Concrete class for homomorphisms with domain a free module or a quotient | |
thereof. | |
Do not instantiate; the constructor does not check that your data is well | |
defined. Use the ``homomorphism`` function instead: | |
>>> from sympy import QQ | |
>>> from sympy.abc import x | |
>>> from sympy.polys.agca import homomorphism | |
>>> F = QQ.old_poly_ring(x).free_module(2) | |
>>> homomorphism(F, F, [[1, 0], [0, 1]]) | |
Matrix([ | |
[1, 0], : QQ[x]**2 -> QQ[x]**2 | |
[0, 1]]) | |
""" | |
def _apply(self, elem): | |
if isinstance(self.domain, QuotientModule): | |
elem = elem.data | |
return sum(x * e for x, e in zip(elem, self.matrix)) | |
def _image(self): | |
return self.codomain.submodule(*self.matrix) | |
def _kernel(self): | |
# The domain is either a free module or a quotient thereof. | |
# It does not matter if it is a quotient, because that won't increase | |
# the kernel. | |
# Our generators {e_i} are sent to the matrix entries {b_i}. | |
# The kernel is essentially the syzygy module of these {b_i}. | |
syz = self.image().syzygy_module() | |
return self.domain.submodule(*syz.gens) | |
class SubModuleHomomorphism(MatrixHomomorphism): | |
""" | |
Concrete class for homomorphism with domain a submodule of a free module | |
or a quotient thereof. | |
Do not instantiate; the constructor does not check that your data is well | |
defined. Use the ``homomorphism`` function instead: | |
>>> from sympy import QQ | |
>>> from sympy.abc import x | |
>>> from sympy.polys.agca import homomorphism | |
>>> M = QQ.old_poly_ring(x).free_module(2)*x | |
>>> homomorphism(M, M, [[1, 0], [0, 1]]) | |
Matrix([ | |
[1, 0], : <[x, 0], [0, x]> -> <[x, 0], [0, x]> | |
[0, 1]]) | |
""" | |
def _apply(self, elem): | |
if isinstance(self.domain, SubQuotientModule): | |
elem = elem.data | |
return sum(x * e for x, e in zip(elem, self.matrix)) | |
def _image(self): | |
return self.codomain.submodule(*[self(x) for x in self.domain.gens]) | |
def _kernel(self): | |
syz = self.image().syzygy_module() | |
return self.domain.submodule( | |
*[sum(xi*gi for xi, gi in zip(s, self.domain.gens)) | |
for s in syz.gens]) | |
def homomorphism(domain, codomain, matrix): | |
r""" | |
Create a homomorphism object. | |
This function tries to build a homomorphism from ``domain`` to ``codomain`` | |
via the matrix ``matrix``. | |
Examples | |
======== | |
>>> from sympy import QQ | |
>>> from sympy.abc import x | |
>>> from sympy.polys.agca import homomorphism | |
>>> R = QQ.old_poly_ring(x) | |
>>> T = R.free_module(2) | |
If ``domain`` is a free module generated by `e_1, \ldots, e_n`, then | |
``matrix`` should be an n-element iterable `(b_1, \ldots, b_n)` where | |
the `b_i` are elements of ``codomain``. The constructed homomorphism is the | |
unique homomorphism sending `e_i` to `b_i`. | |
>>> F = R.free_module(2) | |
>>> h = homomorphism(F, T, [[1, x], [x**2, 0]]) | |
>>> h | |
Matrix([ | |
[1, x**2], : QQ[x]**2 -> QQ[x]**2 | |
[x, 0]]) | |
>>> h([1, 0]) | |
[1, x] | |
>>> h([0, 1]) | |
[x**2, 0] | |
>>> h([1, 1]) | |
[x**2 + 1, x] | |
If ``domain`` is a submodule of a free module, them ``matrix`` determines | |
a homomoprhism from the containing free module to ``codomain``, and the | |
homomorphism returned is obtained by restriction to ``domain``. | |
>>> S = F.submodule([1, 0], [0, x]) | |
>>> homomorphism(S, T, [[1, x], [x**2, 0]]) | |
Matrix([ | |
[1, x**2], : <[1, 0], [0, x]> -> QQ[x]**2 | |
[x, 0]]) | |
If ``domain`` is a (sub)quotient `N/K`, then ``matrix`` determines a | |
homomorphism from `N` to ``codomain``. If the kernel contains `K`, this | |
homomorphism descends to ``domain`` and is returned; otherwise an exception | |
is raised. | |
>>> homomorphism(S/[(1, 0)], T, [0, [x**2, 0]]) | |
Matrix([ | |
[0, x**2], : <[1, 0] + <[1, 0]>, [0, x] + <[1, 0]>, [1, 0] + <[1, 0]>> -> QQ[x]**2 | |
[0, 0]]) | |
>>> homomorphism(S/[(0, x)], T, [0, [x**2, 0]]) | |
Traceback (most recent call last): | |
... | |
ValueError: kernel <[1, 0], [0, 0]> must contain sm, got <[0,x]> | |
""" | |
def freepres(module): | |
""" | |
Return a tuple ``(F, S, Q, c)`` where ``F`` is a free module, ``S`` is a | |
submodule of ``F``, and ``Q`` a submodule of ``S``, such that | |
``module = S/Q``, and ``c`` is a conversion function. | |
""" | |
if isinstance(module, FreeModule): | |
return module, module, module.submodule(), lambda x: module.convert(x) | |
if isinstance(module, QuotientModule): | |
return (module.base, module.base, module.killed_module, | |
lambda x: module.convert(x).data) | |
if isinstance(module, SubQuotientModule): | |
return (module.base.container, module.base, module.killed_module, | |
lambda x: module.container.convert(x).data) | |
# an ordinary submodule | |
return (module.container, module, module.submodule(), | |
lambda x: module.container.convert(x)) | |
SF, SS, SQ, _ = freepres(domain) | |
TF, TS, TQ, c = freepres(codomain) | |
# NOTE this is probably a bit inefficient (redundant checks) | |
return FreeModuleHomomorphism(SF, TF, [c(x) for x in matrix] | |
).restrict_domain(SS).restrict_codomain(TS | |
).quotient_codomain(TQ).quotient_domain(SQ) | |