All published worksheets from http://sagenb.org
Image: ubuntu2004
Coercion and Categories in Sage
David Roe
University of Washington
January 26, 2012
There are three levels of mathematical structures in Sage:
- Parents -- modeling sets with some mathematical structure.
- Elements -- elements of parents: frequently the subjects of our computations. Every element in Sage has a parent.
- Categories -- modeled on the mathematical notion of a category, they also serve to collect code for objects (parents) in the category.
The primers on categories and coercion are good places to learn more.
Parents
Here are some examples of parents in Sage.
Native python types like ints, lists and strings don't have parents, but we'd like to reason with them as if they did. So we define the parent of such an object to just be its type.
Note that for actual elements in Sage the type is distinct from the parent:
Sometimes you're interested in computing with parents directly:
But frequently your computations will require arithmetic with actual elements of these sets.
Elements
For a few kinds of elements there are ways to create them without first creating a parent.
But the most common idiom in Sage is to first create a parent and then provide some data to the __call__ method of that parent in order to create elements.
Behind the scenes the coercion model has entered the picture here. But the motivation for having a coercion system is more apparent in the other natural way to construct a polynomial:
The primary aim of the coercion system is to allow arithmetic between elements of different parents: between R and the integer ring in the example above.
In these two examples of creating polynomials we've already seen the two types of maps appearing in the coercion system: conversions and coercions. Conversions are invoked when creating an element of a parent out of some data. They are not intended to be canonical, but instead should strive to work as frequently as possible, making non-canonical choices if necessary.
They can work on some elements of a single parent and not others:
Coercions, on the other hand, should fit into a diagram of canonical homomorphisms. Frequently these homomorphisms are injections, but some are quotient maps instead. While conversions need to be explicitly invoked by the __call__ method of the parent, coercions are applied implicitly when doing arithmetic.
Behind the scenes, both coercions and conversions are implemented as maps, or morphisms.
So suppose you multiply two elements and with parents and respectively. After checking to see if is in fact the same object as , one of the ways Sage allows arithmetic between different parents is to check to see if there's a coercion map from to or from to . But there are a number of cases where this strategy is not sufficient.
Actions
There is no coercion from the parent of to the parent of or vice versa. Yet multiplication still succeeds. What is going on?
For multiplication and division, the first thing Sage checks after checking whether two elements have the same parent is to see if an action exists.
File: /Users/roed/sage/sage-4.8.alpha2/devel/sage/sage/structure/element.pyx
Source Code (starting at line 1344):
def __mul__(left, right): """ Top-level multiplication operator for ring elements. See extensive documentation at the top of element.pyx. AUTHOR: - Gonzalo Tornaria (2007-06-25) - write base-extending test cases and fix them TESTS: Here we test (scalar * vector) multiplication:: sage: x, y = var('x, y') sage: parent(ZZ(1)*vector(ZZ,[1,2])) Ambient free module of rank 2 over the principal ideal domain Integer Ring sage: parent(QQ(1)*vector(ZZ,[1,2])) Vector space of dimension 2 over Rational Field sage: parent(ZZ(1)*vector(QQ,[1,2])) Vector space of dimension 2 over Rational Field sage: parent(QQ(1)*vector(QQ,[1,2])) Vector space of dimension 2 over Rational Field sage: parent(QQ(1)*vector(ZZ[x],[1,2])) Ambient free module of rank 2 over the principal ideal domain Univariate Polynomial Ring in x over Rational Field sage: parent(ZZ[x](1)*vector(QQ,[1,2])) Ambient free module of rank 2 over the principal ideal domain Univariate Polynomial Ring in x over Rational Field sage: parent(QQ(1)*vector(ZZ[x][y],[1,2])) Ambient free module of rank 2 over the integral domain Univariate Polynomial Ring in y over Univariate Polynomial Ring in x over Rational Field sage: parent(ZZ[x][y](1)*vector(QQ,[1,2])) Ambient free module of rank 2 over the integral domain Univariate Polynomial Ring in y over Univariate Polynomial Ring in x over Rational Field sage: parent(QQ[x](1)*vector(ZZ[x][y],[1,2])) Ambient free module of rank 2 over the integral domain Univariate Polynomial Ring in y over Univariate Polynomial Ring in x over Rational Field sage: parent(ZZ[x][y](1)*vector(QQ[x],[1,2])) Ambient free module of rank 2 over the integral domain Univariate Polynomial Ring in y over Univariate Polynomial Ring in x over Rational Field sage: parent(QQ[y](1)*vector(ZZ[x][y],[1,2])) Ambient free module of rank 2 over the integral domain Univariate Polynomial Ring in y over Univariate Polynomial Ring in x over Rational Field sage: parent(ZZ[x][y](1)*vector(QQ[y],[1,2])) Ambient free module of rank 2 over the integral domain Univariate Polynomial Ring in y over Univariate Polynomial Ring in x over Rational Field sage: parent(ZZ[x](1)*vector(ZZ[y],[1,2])) Traceback (most recent call last): ... TypeError: unsupported operand parent(s) for '*': 'Univariate Polynomial Ring in x over Integer Ring' and 'Ambient free module of rank 2 over the integral domain Univariate Polynomial Ring in y over Integer Ring' sage: parent(ZZ[x](1)*vector(QQ[y],[1,2])) Traceback (most recent call last): ... TypeError: unsupported operand parent(s) for '*': 'Univariate Polynomial Ring in x over Integer Ring' and 'Ambient free module of rank 2 over the principal ideal domain Univariate Polynomial Ring in y over Rational Field' sage: parent(QQ[x](1)*vector(ZZ[y],[1,2])) Traceback (most recent call last): ... TypeError: unsupported operand parent(s) for '*': 'Univariate Polynomial Ring in x over Rational Field' and 'Ambient free module of rank 2 over the integral domain Univariate Polynomial Ring in y over Integer Ring' sage: parent(QQ[x](1)*vector(QQ[y],[1,2])) Traceback (most recent call last): ... TypeError: unsupported operand parent(s) for '*': 'Univariate Polynomial Ring in x over Rational Field' and 'Ambient free module of rank 2 over the principal ideal domain Univariate Polynomial Ring in y over Rational Field' Here we test (scalar * matrix) multiplication:: sage: parent(ZZ(1)*matrix(ZZ,2,2,[1,2,3,4])) Full MatrixSpace of 2 by 2 dense matrices over Integer Ring sage: parent(QQ(1)*matrix(ZZ,2,2,[1,2,3,4])) Full MatrixSpace of 2 by 2 dense matrices over Rational Field sage: parent(ZZ(1)*matrix(QQ,2,2,[1,2,3,4])) Full MatrixSpace of 2 by 2 dense matrices over Rational Field sage: parent(QQ(1)*matrix(QQ,2,2,[1,2,3,4])) Full MatrixSpace of 2 by 2 dense matrices over Rational Field sage: parent(QQ(1)*matrix(ZZ[x],2,2,[1,2,3,4])) Full MatrixSpace of 2 by 2 dense matrices over Univariate Polynomial Ring in x over Rational Field sage: parent(ZZ[x](1)*matrix(QQ,2,2,[1,2,3,4])) Full MatrixSpace of 2 by 2 dense matrices over Univariate Polynomial Ring in x over Rational Field sage: parent(QQ(1)*matrix(ZZ[x][y],2,2,[1,2,3,4])) Full MatrixSpace of 2 by 2 dense matrices over Univariate Polynomial Ring in y over Univariate Polynomial Ring in x over Rational Field sage: parent(ZZ[x][y](1)*matrix(QQ,2,2,[1,2,3,4])) Full MatrixSpace of 2 by 2 dense matrices over Univariate Polynomial Ring in y over Univariate Polynomial Ring in x over Rational Field sage: parent(QQ[x](1)*matrix(ZZ[x][y],2,2,[1,2,3,4])) Full MatrixSpace of 2 by 2 dense matrices over Univariate Polynomial Ring in y over Univariate Polynomial Ring in x over Rational Field sage: parent(ZZ[x][y](1)*matrix(QQ[x],2,2,[1,2,3,4])) Full MatrixSpace of 2 by 2 dense matrices over Univariate Polynomial Ring in y over Univariate Polynomial Ring in x over Rational Field sage: parent(QQ[y](1)*matrix(ZZ[x][y],2,2,[1,2,3,4])) Full MatrixSpace of 2 by 2 dense matrices over Univariate Polynomial Ring in y over Univariate Polynomial Ring in x over Rational Field sage: parent(ZZ[x][y](1)*matrix(QQ[y],2,2,[1,2,3,4])) Full MatrixSpace of 2 by 2 dense matrices over Univariate Polynomial Ring in y over Univariate Polynomial Ring in x over Rational Field sage: parent(ZZ[x](1)*matrix(ZZ[y],2,2,[1,2,3,4])) Traceback (most recent call last): ... TypeError: unsupported operand parent(s) for '*': 'Univariate Polynomial Ring in x over Integer Ring' and 'Full MatrixSpace of 2 by 2 dense matrices over Univariate Polynomial Ring in y over Integer Ring' sage: parent(ZZ[x](1)*matrix(QQ[y],2,2,[1,2,3,4])) Traceback (most recent call last): ... TypeError: unsupported operand parent(s) for '*': 'Univariate Polynomial Ring in x over Integer Ring' and 'Full MatrixSpace of 2 by 2 dense matrices over Univariate Polynomial Ring in y over Rational Field' sage: parent(QQ[x](1)*matrix(ZZ[y],2,2,[1,2,3,4])) Traceback (most recent call last): ... TypeError: unsupported operand parent(s) for '*': 'Univariate Polynomial Ring in x over Rational Field' and 'Full MatrixSpace of 2 by 2 dense matrices over Univariate Polynomial Ring in y over Integer Ring' sage: parent(QQ[x](1)*matrix(QQ[y],2,2,[1,2,3,4])) Traceback (most recent call last): ... TypeError: unsupported operand parent(s) for '*': 'Univariate Polynomial Ring in x over Rational Field' and 'Full MatrixSpace of 2 by 2 dense matrices over Univariate Polynomial Ring in y over Rational Field' """ # Try fast pathway if they are both RingElements and the parents match. # (We know at least one of the arguments is a RingElement. So if their # types are *equal* (fast to check) then they are both RingElements. # Otherwise use the slower test via PY_TYPE_CHECK.) if have_same_parent(left, right): if (<RefPyObject *>left).ob_refcnt < inplace_threshold: return (<RingElement>left)._imul_(<RingElement>right) else: return (<RingElement>left)._mul_(<RingElement>right) if PyInt_CheckExact(right): return (<ModuleElement>left)._mul_long(PyInt_AS_LONG(right)) elif PyInt_CheckExact(left): return (<ModuleElement>right)._mul_long(PyInt_AS_LONG(left)) return coercion_model.bin_op(left, right, mul)
File: /Users/roed/sage/sage-4.8.alpha2/devel/sage/sage/structure/coerce.pyx
Source Code (starting at line 651):
cpdef bin_op(self, x, y, op): """ Execute the operation op on x and y. It first looks for an action corresponding to op, and failing that, it tries to coerces x and y into the a common parent and calls op on them. If it cannot make sense of the operation, a TypeError is raised. INPUT: - ``x`` - the left operand - ``y`` - the right operand - ``op`` - a python function taking 2 arguments .. note:: op is often an arithmetic operation, but need not be so. EXAMPLES:: sage: cm = sage.structure.element.get_coercion_model() sage: cm.bin_op(1/2, 5, operator.mul) 5/2 The operator can be any callable:: set Rational Field Integer Ring <function <lambda> at 0xc0b2270> None None (Rational Field, Rational Field) sage: R.<x> = ZZ['x'] sage: cm.bin_op(x^2-1, x+1, gcd) x + 1 Actions are detected and performed:: sage: M = matrix(ZZ, 2, 2, range(4)) sage: V = vector(ZZ, [5,7]) sage: cm.bin_op(M, V, operator.mul) (7, 31) TESTS:: sage: class Foo: ... def __rmul__(self, left): ... return 'hello' ... sage: H = Foo() sage: print int(3)*H hello sage: print Integer(3)*H hello sage: print H*3 Traceback (most recent call last): ... TypeError: unsupported operand parent(s) for '*': '<type 'instance'>' and 'Integer Ring' sage: class Nonsense: ... def __init__(self, s): ... self.s = s ... def __repr__(self): ... return self.s ... def __mul__(self, x): ... return Nonsense(self.s + chr(x%256)) ... __add__ = __mul__ ... def __rmul__(self, x): ... return Nonsense(chr(x%256) + self.s) ... __radd__ = __rmul__ ... sage: a = Nonsense('blahblah') sage: a*80 blahblahP sage: 80*a Pblahblah sage: a+80 blahblahP sage: 80+a Pblahblah """ self._exceptions_cleared = False if (op is not sub) and (op is not isub): # Actions take preference over common-parent coercions. xp = parent_c(x) yp = parent_c(y) if xp is yp: return op(x,y) action = self.get_action(xp, yp, op) if action is not None: return (<Action>action)._call_(x, y) xy = None try: xy = self.canonical_coercion(x,y) return PyObject_CallObject(op, xy) except TypeError, err: if xy is not None: # The error was in calling, not coercing raise self._record_exception() if op is mul or op is imul: # elements may also act on non-elements # (e.g. sequences or parents) if not isinstance(y, Element) or not isinstance(x, Element): try: if hasattr(x, '_act_on_'): res = x._act_on_(y, True) if res is not None: return res except CoercionException: self._record_exception() try: if hasattr(x, '_acted_upon_'): res = x._acted_upon_(y, True) if res is not None: return res except CoercionException: self._record_exception() try: if hasattr(y, '_act_on_'): res = y._act_on_(x, False) if res is not None: return res except CoercionException: self._record_exception() try: if hasattr(y, '_acted_upon_'): res = y._acted_upon_(x, False) if res is not None: return res except CoercionException: self._record_exception() if not isinstance(y, Element): op_name = op.__name__ if op_name[0] == 'i': op_name = op_name[1:] mul_method = getattr3(y, '__r%s__'%op_name, None) if mul_method is not None: res = mul_method(x) if res is not None and res is not NotImplemented: return res # We should really include the underlying error. # This causes so much headache. raise TypeError, arith_error_message(x,y,op)
The second way in which our strategy of coercing each way is not sufficient is when there is no coercion in either direction, but we really want the operation to be defined. Here's an example:
File: /Users/roed/sage/sage-4.8.alpha2/devel/sage/sage/structure/coerce.pyx
Source Code (starting at line 799):
cpdef canonical_coercion(self, x, y): r""" Given two elements x and y, with parents S and R respectively, find a common parent Z such that there are coercions `f: S \mapsto Z` and `g: R \mapsto Z` and return `f(x), g(y)` which will have the same parent. Raises a type error if no such Z can be found. EXAMPLES:: sage: cm = sage.structure.element.get_coercion_model() sage: cm.canonical_coercion(mod(2, 10), 17) (2, 7) sage: x, y = cm.canonical_coercion(1/2, matrix(ZZ, 2, 2, range(4))) sage: x [1/2 0] [ 0 1/2] sage: y [0 1] [2 3] sage: parent(x) is parent(y) True There is some support for non-Sage datatypes as well:: sage: x, y = cm.canonical_coercion(int(5), 10) sage: type(x), type(y) (<type 'sage.rings.integer.Integer'>, <type 'sage.rings.integer.Integer'>) sage: x, y = cm.canonical_coercion(int(5), complex(3)) sage: type(x), type(y) (<type 'complex'>, <type 'complex'>) sage: class MyClass: ... def _sage_(self): ... return 13 sage: a, b = cm.canonical_coercion(MyClass(), 1/3) sage: a, b (13, 1/3) sage: type(a) <type 'sage.rings.rational.Rational'> We also make an exception for 0, even if $\ZZ$ does not map in:: sage: canonical_coercion(vector([1, 2, 3]), 0) ((1, 2, 3), (0, 0, 0)) """ xp = parent_c(x) yp = parent_c(y) if xp is yp: return x,y cdef Element x_elt, y_elt coercions = self.coercion_maps(xp, yp) if coercions is not None: x_map, y_map = coercions if x_map is not None: x_elt = (<Map>x_map)._call_(x) else: x_elt = x if y_map is not None: y_elt = (<Map>y_map)._call_(y) else: y_elt = y if x_elt is None: raise RuntimeError, "BUG in map, returned None %s %s %s" % (x, type(x_map), x_map) elif y_elt is None: raise RuntimeError, "BUG in map, returned None %s %s %s" % (y, type(y_map), y_map) if x_elt._parent is y_elt._parent: # We must verify this as otherwise we are prone to # getting into an infinite loop in c, and the above # maps may be written by (imperfect) users. return x_elt,y_elt elif x_elt._parent == y_elt._parent: # TODO: Non-uniqueness of parents strikes again! # print parent_c(x_elt), " is not ", parent_c(y_elt) y_elt = parent_c(x_elt)(y_elt) if x_elt._parent is y_elt._parent: return x_elt,y_elt self._coercion_error(x, x_map, x_elt, y, y_map, y_elt) cdef bint x_numeric = PY_IS_NUMERIC(x) cdef bint y_numeric = PY_IS_NUMERIC(y) if x_numeric and y_numeric: x_rank = _native_coercion_ranks[type(x)] y_rank = _native_coercion_ranks[type(y)] ty = _native_coercion_ranks_inv[max(x_rank, y_rank)] x = ty(x) y = ty(y) return x, y # Now handle the native python + sage object cases # that were not taken care of above. elif x_numeric: try: sage_parent = py_scalar_parent(type(x)) if sage_parent is None or sage_parent.has_coerce_map_from(yp): return x, x.__class__(y) else: return self.canonical_coercion(sage_parent(x), y) except (TypeError, ValueError): self._record_exception() elif y_numeric: try: sage_parent = py_scalar_parent(type(y)) if sage_parent is None or sage_parent.has_coerce_map_from(xp): return y.__class__(x), y else: return self.canonical_coercion(x, sage_parent(y)) except (TypeError, ValueError): self._record_exception() # See if the non-objects define a _sage_ method. if not PY_TYPE_CHECK(x, SageObject) or not PY_TYPE_CHECK(y, SageObject): try: x = x._sage_() y = y._sage_() except AttributeError: self._record_exception() else: return self.canonical_coercion(x, y) # Allow coercion of 0 even if no coercion from Z if is_Integer(x) and not x and not PY_TYPE_CHECK_EXACT(yp, type): try: return yp(0), y except: self._record_exception() if is_Integer(y) and not y and not PY_TYPE_CHECK_EXACT(xp, type): try: return x, xp(0) except: self._record_exception() raise TypeError, "no common canonical parent for objects with parents: '%s' and '%s'"%(xp, yp)
File: /Users/roed/sage/sage-4.8.alpha2/devel/sage/sage/structure/coerce.pyx
Source Code (starting at line 942):
cpdef coercion_maps(self, R, S): r""" Give two parents R and S, return a pair of coercion maps `f: R \rightarrow Z` and `g: S \rightarrow Z` , if such a `Z` can be found. In the (common) case that `R=Z` or `S=Z` then ``None`` is returned for `f` or `g` respectively rather than constructing (and subsequently calling) the identity morphism. If no suitable `f, g` can be found, a single None is returned. This result is cached. EXAMPLES:: sage: cm = sage.structure.element.get_coercion_model() sage: f, g = cm.coercion_maps(ZZ, QQ) sage: print f Natural morphism: From: Integer Ring To: Rational Field sage: print g None sage: f, g = cm.coercion_maps(ZZ['x'], QQ) sage: print f Conversion map: From: Univariate Polynomial Ring in x over Integer Ring To: Univariate Polynomial Ring in x over Rational Field sage: print g Polynomial base injection morphism: From: Rational Field To: Univariate Polynomial Ring in x over Rational Field sage: cm.coercion_maps(QQ, GF(7)) == None True Note that to break symmetry, if there is a coercion map in both directions, the parent on the left is used:: sage: V = QQ^3 sage: W = V.__class__(QQ, 3) sage: V == W True sage: V is W False sage: cm = sage.structure.element.get_coercion_model() sage: cm.coercion_maps(V, W) (None, Call morphism: From: Vector space of dimension 3 over Rational Field To: Vector space of dimension 3 over Rational Field) sage: cm.coercion_maps(W, V) (None, Call morphism: From: Vector space of dimension 3 over Rational Field To: Vector space of dimension 3 over Rational Field) sage: v = V([1,2,3]) sage: w = W([1,2,3]) sage: parent(v+w) is V True sage: parent(w+v) is W True """ try: return self._coercion_maps.get(R, S, None) except KeyError: homs = self.discover_coercion(R, S) if 0: # This breaks too many things that are going to change # in the new coercion model anyways. # COERCE TODO: Enable it then. homs = self.verify_coercion_maps(R, S, homs) else: if homs is not None: x_map, y_map = homs if x_map is not None and not isinstance(x_map, Map): raise RuntimeError, "BUG in coercion model: coerce_map_from must return a Map" if y_map is not None and not isinstance(y_map, Map): raise RuntimeError, "BUG in coercion model: coerce_map_from must return a Map" if homs is None: swap = None else: R_map, S_map = homs if R_map is None and PY_TYPE_CHECK(S, Parent) and (<Parent>S).has_coerce_map_from(R): swap = None, (<Parent>S).coerce_map_from(R) else: swap = S_map, R_map self._coercion_maps.set(R, S, None, homs) self._coercion_maps.set(S, R, None, swap) return homs
File: /Users/roed/sage/sage-4.8.alpha2/devel/sage/sage/structure/coerce.pyx
Source Code (starting at line 1101):
cpdef discover_coercion(self, R, S): """ This actually implements the finding of coercion maps as described in the ``coercion_maps`` method. EXAMPLES:: sage: cm = sage.structure.element.get_coercion_model() If R is S, then two identity morphisms suffice:: sage: cm.discover_coercion(SR, SR) (None, None) If there is a coercion map either direction, use that:: sage: cm.discover_coercion(ZZ, QQ) (Natural morphism: From: Integer Ring To: Rational Field, None) sage: cm.discover_coercion(RR, QQ) (None, Generic map: From: Rational Field To: Real Field with 53 bits of precision) Otherwise, try and compute an appropriate cover:: sage: cm.discover_coercion(ZZ['x,y'], RDF) (Call morphism: From: Multivariate Polynomial Ring in x, y over Integer Ring To: Multivariate Polynomial Ring in x, y over Real Double Field, Polynomial base injection morphism: From: Real Double Field To: Multivariate Polynomial Ring in x, y over Real Double Field) Sometimes there is a reasonable "cover," but no canonical coercion:: sage: sage.categories.pushout.pushout(QQ, QQ^3) Vector space of dimension 3 over Rational Field sage: print cm.discover_coercion(QQ, QQ^3) None """ from sage.categories.homset import Hom if R is S: return None, None # See if there is a natural coercion from R to S if PY_TYPE_CHECK(R, Parent): mor = (<Parent>R).coerce_map_from(S) if mor is not None: return None, mor # See if there is a natural coercion from S to R if PY_TYPE_CHECK(S, Parent): mor = (<Parent>S).coerce_map_from(R) if mor is not None: return mor, None # Try base extending if PY_TYPE_CHECK(R, Parent) and PY_TYPE_CHECK(S, Parent): from sage.categories.pushout import pushout try: Z = pushout(R, S) coerce_R = Z.coerce_map_from(R) coerce_S = Z.coerce_map_from(S) if coerce_R is None: raise TypeError, "No coercion from %s to pushout %s" % (R, Z) if coerce_S is None: raise TypeError, "No coercion from %s to pushout %s" % (S, Z) return coerce_R, coerce_S except: self._record_exception() return None
How does Sage know what to choose? The answer is in sage.categories.pushout and the construction method on parents.
Sage traces back along these construction functors until it finds a common ancestor: since much arithmetic is actually being performed on rings and many common rings can be constructed from the integers by applying a sequence of functors, finding a common ancestor is frequently possible. Sage then uses a collection of heuristics to merge these constructions.
File: /Users/roed/sage/sage-4.8.alpha2/local/lib/python2.6/site-packages/sage/categories/pushout.py
Source Code (starting at line 3097):
def pushout_lattice(R, S): r""" Given a pair of Objects $R$ and $S$, try and construct a reasonable object $Y$ and return maps such that canonically $R \leftarrow Y \rightarrow S$. ALGORITHM: This is based on the model that arose from much discussion at Sage Days 4. Going up the tower of constructions of $R$ and $S$ (e.g. the reals come from the rationals come from the integers) try and find a common parent, and then try and fill in a lattice with these two towers as sides with the top as the common ancestor and the bottom will be the desired ring. See the code for a specific worked-out example. EXAMPLES:: sage: from sage.categories.pushout import pushout_lattice sage: A, B = pushout_lattice(Qp(7), Frac(ZZ['x'])) sage: A.codomain() Fraction Field of Univariate Polynomial Ring in x over 7-adic Field with capped relative precision 20 sage: A.codomain() is B.codomain() True sage: A, B = pushout_lattice(ZZ, MatrixSpace(ZZ[['x']], 3, 3)) sage: B Identity endomorphism of Full MatrixSpace of 3 by 3 dense matrices over Power Series Ring in x over Integer Ring AUTHOR: - Robert Bradshaw """ R_tower = construction_tower(R) S_tower = construction_tower(S) Rs = [c[1] for c in R_tower] Ss = [c[1] for c in S_tower] # look for common ancestor start = None for Z in Rs: if Z in Ss: start = Z if start is None: # Should I test for a map between the tops of the towers? # Or, if they're both not ZZ, is it hopeless? return None # truncate at common ancestor R_tower = list(reversed(R_tower[:Rs.index(start)+1])) S_tower = list(reversed(S_tower[:Ss.index(start)+1])) Rs = [c[1] for c in R_tower] # the list of objects Ss = [c[1] for c in S_tower] Rc = [c[0] for c in R_tower] # the list of functors Sc = [c[0] for c in S_tower] # Here we try and construct a 2-dimensional lattice as follows. # Suppose our towers are Z -> Q -> Qp = R and Z -> Z[t] -> Frac(Z[t]) = S lattice = {} # First we fill in the sides # # Z # / \ # Q Z[t] # / \ # Qp Frac(Z[t]) # for i in range(len(Rs)): lattice[i,0] = Rs[i] for j in range(len(Ss)): lattice[0,j] = Ss[j] # Now we attempt to fill in the center, one (diagonal) row at a time, # one commuting square at a time. # # Z # / \ # Q Z[t] # / \ / \ # Qp Q[t] Frac(Z[t]) # \ / # Qp[t] # # There is always exactly one "correct" path/order in which to apply operations # from the top to the bottom. In our example, this is down the far left side. # We keep track of which that is by clearing out Rc and Sc as we go along. # # Note that when applying the functors in the correct order, base extension # is not needed (though it may occur in the resulting morphisms). # for i in range(len(Rc)-1): for j in range(len(Sc)-1): try: if lattice[i,j+1] == lattice[i+1,j]: # In this case we have R <- S -> R # We don't want to perform the operation twice # and all subsequent squares will come from objects # where the operation was already performed (either # to the left or right) Rc[i] = Sc[j] = None # IdentityConstructionFunctor() lattice[i+1,j+1] = lattice[i,j+1] elif Rc[i] is None and Sc[j] is None: lattice[i+1,j+1] = lattice[i,j+1] elif Rc[i] is None: lattice[i+1,j+1] = Sc[j](lattice[i+1,j]) elif Sc[j] is None: lattice[i+1,j+1] = Rc[i](lattice[i,j+1]) else: # For now, we just look at the rank. # TODO: be more sophisticated and query the functors themselves if Rc[i].rank < Sc[j].rank: lattice[i+1,j+1] = Sc[j](lattice[i+1,j]) Rc[i] = None # force us to use pre-applied Rc[i] else: lattice[i+1,j+1] = Rc[i](lattice[i,j+1]) Sc[j] = None # force us to use pre-applied Sc[i] except (AttributeError, NameError): # print i, j # pp(lattice) for i in range(100): for j in range(100): try: R = lattice[i,j] print i, j, R except KeyError: break raise CoercionException, "%s does not support %s" % (lattice[i,j], 'F') # If we are successful, we should have something that looks like this. # # Z # / \ # Q Z[t] # / \ / \ # Qp Q[t] Frac(Z[t]) # \ / \ / # Qp[t] Frac(Q[t]) # \ / # Frac(Qp[t]) # R_loc = len(Rs)-1 S_loc = len(Ss)-1 # Find the composition coercion morphisms along the bottom left... if S_loc > 0: R_map = lattice[R_loc,1].coerce_map_from(R) for i in range(1, S_loc): map = lattice[R_loc, i+1].coerce_map_from(lattice[R_loc, i]) # The functor used is implicit here, should it be? R_map = map * R_map else: R_map = R.coerce_map_from(R) # id # ... and bottom right if R_loc > 0: S_map = lattice[1, S_loc].coerce_map_from(S) for i in range(1, R_loc): map = lattice[i+1, S_loc].coerce_map_from(lattice[i, S_loc]) S_map = map * S_map else: S_map = S.coerce_map_from(S) # id return R_map, S_map
So in summary, Sage tries the following steps in order to execute an arithmetic operation.
- Check to see if the elements have the same parent. If so, call the _mul_, _add_, etc method defined in the class of the element.
- Check to see if there's an appropriate action by one parent on the other. If so, apply the action.
- Check to see if there's a coercion in either direction.
- Try to create a pushout to which both parents coerce using the construction methods on the parents. If appropriate coercions are detected, apply them to the two elements and do arithmetic in the "common" parent.
In order to make all of this fast, the coercions and actions that are discovered are cached. So while the first instance of arithmetic between different parents can be slow, later arithmetic will be much faster. And if the default coercion maps created are too slow for you, you can implement your own morphisms in Cython and use them (the coercion from ZZ to Zmod(N) uses this feature).
Categories
Categories in Sage are modeled on the mathematical concept of a category and one of their purposes is to allow users to model such objects in Sage. For example, you might want to know if a category is abelian, has an initial or terminal object, is closed under inverse limits....
But they also serve as a place to put code that should apply to any object in that category.
Every parent in Sage has a category.
Categories include information about what functions need to be implemented by parents in that category and elements of those parents.
If you write a new parent, Sage has a testing framework to check whether it implements these required methods, and implements them correctly.
So suppose you want to implement a new parent in Sage. You need to define a class for the parents, a class for the elements, and choose a category. We'll use the example of localization from the coercion primer.