The aim of this tutorial is to show what needs to be implemented in order to enable arithmetic operations between new and existing algebraic structures in Sage.
In this tutorial, we are implementing the set of m by n matrices over a given commutative ring, subject to the usual multiplication rules of matrices. Our implementation is very slow and naive, and it will of course not replace the existing implementation of matrix spaces. Our toy implementation only aims to demonstrate how one can implement
Note that the existing implementation of matrix spaces in Sage does not implement coercion in the same way as demonstrated here.
This is not a talk about Sage's category framework, but we will need "categorial notions" in various places.
Def:
A Parent is an object of a sub-category of the category of sets. In particular, it contains elements.
Sage provides convenient base classes for a number of algebraic parent structures and their elements, and they should be used.
In addition, Sage provides a "category framework", and one should indicate the category which a parent belongs to---in addition to using algebraic base classes!
Matrix spaces are modules, and they are algebras if the matrices are square. Hence, we build our matrix spaces on top of a base class sage.modules.module.Module, and refine the class using the category framework in the case of square matrices.
It may seem strange that we build non-square matrices on top of a base class for ring elements, but the base class of module elements strictly disallows n internal multiplication (which I think is a bug).
The following tentative class definitions will be refined later.
{{{id=1| from sage.modules.module import Module from sage.structure.parent import Parent class MyMatrixSpace(Module): def __init__(self, B, m, n=None): # the base ring must be commutative if B not in CommutativeRings(): raise ValueError, "commutative ring required" if n is None: n = m self.m = m self.n = n # Module.__init__ should accept an argument "category" and pass it through, # but it currently doesn'tSometimes our matrix spaces are just modules and not algebras. However, Sage's base class <ModuleElements> raises an error if an internal multiplication is attempted. Hence, we start with <RingElement> and square matrices, and cope with the absence of internal multiplication for non-square matrices later. First, we only take care of the data structure and the string representation:
{{{id=142| from sage.rings.ring_element import RingElement class MyMatrix(RingElement): def __init__(self, parent, data): self.data = [list(r) for r in data] RingElement.__init__(self, parent) def _repr_(self): return os.linesep.join([repr(r) for r in self.data]) def __iter__(self): for r in self.data: yield r def __getitem__(self, key): try: return self.data[key] except TypeError: if len(key)==2: if isinstance(key[0], slice): return [row[key[1]] for row in self.data[key[0]]] return self.data[key[0]][key[1]] raise ValueError, "invalid indices" /// }}}The first lesson of this lecture is: Using the category framework to create elements is easy!
Slicing works (we will use it in the multiplication code below):
{{{id=151| M1[:,1] /// [2, 4] }}} {{{id=152| M1[0,:] /// [1, 2] }}}We now come to the arithmetic methods. You certainly know about Python's "magical" double underscore methods. Usually, the base classes in Sage have default implementations. Here is the default multiplication code for ring elements (I have removed the long doc string from the output):
{{{id=72| from sage.misc.sageinspect import sage_getsourcelines sources = sage_getsourcelines(RingElement.__mul__)[0] pretty_print(''.join([sources[0]]+sources[-10:])) /// }}}The second lesson of this lecture is: Do not override __mul__ (nor __add__, __sub__, __call__) unless you are ready to bear the consequences of what you are doing, since the default implementations are essential for Sage's coercion framework!
Generally, you should provide the following single underscore methods:
Sage's coercion model guarantees that
Recreate the matrix space and define some elements:
{{{id=25| MyMatrixSpace.Element = MyMatrix MS = MyMatrixSpace(QQ,2) M1 = MS([[1,2],[3,4]]) M2 = MS([[0,1],[2,1]]) /// }}}Addition works, due to _add_:
{{{id=27| M1 + M2 /// [1, 3] [5, 5] }}}Square matrix multiplication in _mul_ works as well:
{{{id=35| M1*M2 /// [4, 3] [8, 7] }}}Verify this multiplication with Sage's matrices:
{{{id=38| m1 = matrix([[1,2],[3,4]]) m2 = matrix([[0,1],[2,1]]) m1*m2 /// [4 3] [8 7] }}}Multiplication by base ring elements does not work, yet.
{{{id=39| M1*2 /// Traceback (most recent call last): File "We fix it by providing _lmul_ and _rmul_.
{{{id=41| class MyMatrix(MyMatrix): def _lmul_(self, c): return self.parent()([[c*a for a in row] for row in self]) def _rmul_(self, c): return self.parent()([[a*c for a in row] for row in self]) /// }}} {{{id=43| MyMatrixSpace.Element = MyMatrix MS = MyMatrixSpace(QQ,2) M1 = MS([[1,2],[3,4]]) M2 = MS([[0,1],[2,1]]) /// }}} {{{id=44| M1*2 /// [2, 4] [6, 8] }}} {{{id=45| 3*M2 /// [0, 3] [6, 3] }}}Generally, a conversion is a procedure that turns given data into an element of a parent.
Normally, one wants to convert elements of the base ring into diagonal matrices. This is not implemented, yet:
{{{id=46| MS(1) /// Traceback (most recent call last): File "We could fix this by making MyMatrix.__init__ accept a base ring element as input argument. However, the preferred way is to implement a method _element_constructor_ for the parent.
The third lesson of this lecture is: Do not override the default __call__ method, but implement _element_constructor_!!
{{{id=47| class MyMatrixSpace(MyMatrixSpace): def _element_constructor_(self, data): B = self.base() z = B.zero() if data is None: data = z if data in B: return self.element_class(self, [[z]*k+[data]+[z]*(self.n-k-1) for k in range(self.m)]) return self.element_class(self, data) /// }}} {{{id=48| MS = MyMatrixSpace(QQ,2) /// }}}Conversions of lists of lists still work:
{{{id=51| M1 = MS([[1,2],[3,4]]) M2 = MS([[0,1],[2,1]]) M1; isinstance(M1, MS.element_class) /// [1, 2] [3, 4] True }}}In addition, we can convert elements of the base ring intor diagonal matrices:
{{{id=52| MS(2) /// [2, 0] [0, 2] }}}We can also convert the usual Sage matrices into our matrix space (this is because MyMatrix.__init__ expects arguments that are iterables of iterables):
{{{id=56| SageMatQQ = matrix(QQ, [[1,2],[3,4]]) MS(SageMatQQ) /// [1, 2] [3, 4] }}}Of course, it would be a very bad idea to automatically apply an arbitrary conversions when doing arithmetic. Sage would by default not apply the conversion of SageMatQQ into MS for doing arithmetic.
Hence, currently the addition of M1 and SageMatQQ fails, even though it would "mathematically reasonably" work:
{{{id=59| M1+SageMatQQ /// Traceback (most recent call last): File "Why would this addition be "mathematically reasonable"? This is related with the notion of a coercion.
Def:
Let $P_1, P_2, P_3$ be parents.
1. A coercion $\phi$ from $P_1$ to $P_2$ is a morphism in the meet $\mathcal C$ of the categories of $P_1$ and $P_2$:
$\phi\in \mathrm{Hom}_{\mathcal C}(P_1,P_2)$
2. There is at most one coercion from one parent to another, and if it exists, then it coincides with conversion.
3. The identity morphism is a coercion.
4. If $\phi$ is a coercion from $P_1$ to $P_2$ and $\psi$ is a coercion from $P_2$ to $P_3$, then $\psi\circ\phi$ is a (the!) coercion from $P_1$ to $P_3$
Coercions will implicitly be used in arithmetic operations!
Warning/Example:
If $P_1$ is an additive group and $P_2$ is a ring, and $P_1$ coerces into $P_2$ by a morphism in the category of additive groups, then this coercion will also be used to multiply elements of $P_1$ with elements of $P_2$ (the result lives in $P_2$)!
By now, Sage knows that there is a conversion from Sage matrices into our matrix space, but it doesn't know that it is a coercion:
{{{id=58| MS.convert_map_from(SageMatQQ.parent()) /// Conversion map: From: Full MatrixSpace of 2 by 2 dense matrices over Rational Field To: Our space of 2x2 matrices over Rational Field }}} {{{id=62| MS.has_coerce_map_from(SageMatQQ.parent()) /// False }}}In contrast, Sage knows that the base ring of MS coerces into MS. This is automatically done for any parent that is initialised in the category of algebras over a base ring: We see the category framework at work.
{{{id=63| MS.coerce_map_from(ZZ) /// Composite map: From: Integer Ring To: Our space of 2x2 matrices over Rational Field Defn: Natural morphism: From: Integer Ring To: Rational Field then Generic morphism: From: Rational Field To: Our space of 2x2 matrices over Rational Field }}}Hence, we can already add an integer to a square matrix: The integer is implicitly converted into a diagonal matrix.
{{{id=67| 1+M1 /// [2, 2] [3, 5] }}}In order to make Sage aware of a coercion, one can implement a _coerce_map_from_() method. If it returns True, then an existing conversion is used as coercion. If it returns a map, then this map is used for coercion.
{{{id=74| class MyMatrixSpace(MyMatrixSpace): def _coerce_map_from_(self, P): # Coercion from the base ring has already been taken care of. # We want coercion from other matrix spaces of compatible parameters. # Use duck-typing! try: return self.m==P.nrows() and self.n==P.ncols() and self.base().has_coerce_map_from(P.base()) except AttributeError: return False /// }}} {{{id=66| MS = MyMatrixSpace(QQ,2) M1 = MS([[1,2],[3,4]]) M2 = MS([[0,1],[2,1]]) /// }}}Now, Sage detects coercions from "usual" matrix spaces into "our" matrix space.
{{{id=65| MSSage = MatrixSpace(ZZ,2) MS.coerce_map_from(MSSage) /// Conversion map: From: Full MatrixSpace of 2 by 2 dense matrices over Integer Ring To: Our space of 2x2 matrices over Rational Field }}} {{{id=77| M1 * MSSage(2) # Note that the result is using "our" implementation /// [2, 4] [6, 8] }}} {{{id=157| MSSage(1) + M1 /// [2, 2] [3, 5] }}}The material of this section is treated in more detail in the thematic tutorial on "coercions and categories".
Many parents in Sage can tell how they are constructed:
{{{id=78| FSage, R = MSSage.construction() FSage, R /// (MatrixFunctor, Integer Ring) }}} {{{id=80| FSage(R) == MSSage /// True }}}Here, we see a so-called construction functor. This is a covariant functor $\mathcal F:\mathcal C_1\to \mathcal C_2$, so that applying $\mathcal F$ to any coercion map $\phi\in\mathrm{Hom}_{\mathcal C_1}(P,Q)$ yields a coercion map from $\mathcal F(P)$ to $\mathcal F(Q)$. Examples include:
Often, there is a coercion from $R$ to $\mathcal F(R)$, for any object $R$ of $\mathcal C_1$. Here is an exception of this rule:
{{{id=81| MatrixSpace(ZZ,2,3).has_coerce_map_from(ZZ) /// False }}}Every construction functor has a "rank":
{{{id=93| FC.rank, FF.rank, FP.rank, FSage.rank /// (3, 5, 9, 10) }}}If two parents $P_1$, $P_2$ are obtained from a common "ancestor" by two sequences of construction functors, then Sage will try to "shuffle" the two construction sequences, applying first a functor with lower rank. The result is called the pushout of $P_1$ and $P_2$, and is a reasonable candidate for performing arithmetic operations involving elements from both $P_1$ and $P_2$:
{{{id=158| FSage(FF(ZZ)) /// Full MatrixSpace of 2 by 2 dense matrices over Rational Field }}} {{{id=159| FSage(FP(ZZ)) /// Full MatrixSpace of 2 by 2 dense matrices over Univariate Polynomial Ring in x over Integer Ring }}} {{{id=85| from sage.categories.pushout import pushout pushout(FSage(FF(ZZ)), FSage(FP(ZZ))) # FF has lower rank than FP and is applied first in the pushout /// Full MatrixSpace of 2 by 2 dense matrices over Univariate Polynomial Ring in x over Rational Field }}}When two different functors of the same rank clash when shuffling, Sage will raise an error, unless it is asserted that one can apply the functors in any order, or Sage is told how to merge the two functors into one.
Here, we create a new matrix construction functor, imposing the rule that Sage's matrix functors will henceforth merge into our matrix functors:
{{{id=94| from sage.categories.pushout import MatrixFunctor class MyMatrixFunctor(MatrixFunctor): def _apply_functor(self, R): return MyMatrixSpace(R,self.nrows,self.ncols) def merge(self, other): if not isinstance(other, MatrixFunctor): return if other.nrows!=self.nrows or other.ncols!=self.ncols: return return self /// }}} {{{id=96| class MyMatrixSpace(MyMatrixSpace): def construction(self): return MyMatrixFunctor(self.m,self.n), self.base() /// }}} {{{id=97| MS = MyMatrixSpace(QQ,2) M1 = MS([[1,2],[3,4]]) M2 = MS([[0,1],[2,1]]) /// }}} {{{id=104| MyF, R = MS.construction() MyF(R) == MS /// True }}}Now, if Sage can not find a coercion from one parent into another, then it constructs a pushout and coerces into it:
{{{id=98| MSPolySage = MatrixSpace(ZZ['x'],2) MS.has_coerce_map_from(MSPolySage), MSPolySage.has_coerce_map_from(MS) /// (False, False) }}} {{{id=100| pushout(MS, MSPolySage) # this is because merging prefers *our* implementation! /// Our space of 2x2 matrices over Univariate Polynomial Ring in x over Rational Field }}} {{{id=103| MSPolySage('x') /// [x 0] [0 x] }}} {{{id=106| MSPolySage('x') + MS(1/2) /// [x + 1/2, 0] [0, x + 1/2] }}}One way of implementing an action of anything on an element is: Implement a method _acted_upon_ of the element.
{{{id=109| RingElement._acted_upon_?? ///File: /home/king/Sage/prerelease/sage-5.12.beta5/devel/sage/sage/structure/element.pyx
Source Code (starting at line 716):
cpdef _acted_upon_(self, x, bint self_on_left):
"""
Use this method to implement self acted on by x.
Return None or raise a CoercionException if no
such action is defined here.
"""
return None
Similarly, an action of the element on anything can be defined by a method _act_on_ of the element.
We implement a left action of a permutation group on our matrices, by permuting rows.
{{{id=110| class MyMatrix(MyMatrix): def _acted_upon_(self, x, self_on_left): if self_on_left: return None # Here, we mainly use that permutations are callable return self.parent()([self.data[x(i)] for i in range(self.parent().nrows())]) /// }}}Refresh our matrix space definition, and let permutations act:
{{{id=112| MyMatrixSpace.Element = MyMatrix MS = MyMatrixSpace(QQ,4) M1 = MS(1) M2 = MS([[1,2,3,4], [5,6,7,8], [8,7,6,5], [4,3,2,1]]) /// }}} {{{id=114| M1 /// [1, 0, 0, 0] [0, 1, 0, 0] [0, 0, 1, 0] [0, 0, 0, 1] }}} {{{id=115| P = Permutation([1,3,2,4]) /// }}}*@#/}!!!
Someone has killed Sage's category framework for permutations, by overloading __mul__! You should not do that!
{{{id=118| P * M1 /// Traceback (most recent call last): File "File: /home/king/Sage/prerelease/sage-5.12.beta5/local/lib/python2.7/site-packages/sage/combinat/permutation.py
Source Code (starting at line 1244):
def __mul__(self, rp):
"""
TESTS::
sage: p213 = Permutation([2,1,3])
sage: p312 = Permutation([3,1,2])
sage: PermutationOptions(mult='l2r')
sage: p213*p312
[1, 3, 2]
sage: PermutationOptions(mult='r2l')
sage: p213*p312
[3, 2, 1]
sage: PermutationOptions(mult='l2r')
"""
if self.parent().global_options['mult'] == 'l2r':
return self._left_to_right_multiply_on_right(rp)
else:
return self._left_to_right_multiply_on_left(rp)
So, we better work with an element of the symmetric group, which complies with Sage's coercion framework:
{{{id=117| p = SymmetricGroup(4)([(3,2)]); p /// (2,3) }}}Now, multiplication from the left works, but (on purpose) not from the right:
{{{id=121| p*M1 /// [1, 0, 0, 0] [0, 1, 0, 0] [0, 0, 0, 1] [0, 0, 1, 0] }}} {{{id=122| M1*p /// Traceback (most recent call last): File "Sage assumes that the action of the symmetric group on our matrix space will always use _acted_upon_:
{{{id=160| A = MS.get_action(p.parent(), self_on_left=False); A; type(A) /// Left action by Symmetric group of order 4! as a permutation group on Our space of 4x4 matrices over Rational FieldSometimes _acted_upon_/_act_on_ on the level of elements is not good enough. It is also possible to let the parent take care of creating an action.
Of course, there is a base class for actions. To create a new type of actions, implement a _call_ method (not __call__) that take two elements and returns the result of the action of these elements.
{{{id=126| from sage.categories.action import Action class MyMatrixAction(Action): def __init__(self, P1,P2): Action.__init__(self, P1, P2, is_left=True, op=operator.mul) self.P = MyMatrixSpace(pushout(P1.base(),P2.base()), P1.nrows(), P2.ncols()) self.range = P1.ncols() def _repr_name_(self): # helps for string representation return "multiplication action of our matrices" def _call_(self, left,right): return self.P([[add([a*b for a,b in zip(left[m], [right[i,k] for i in range(self.range)]) ]) for k in range(self.P.ncols())] for m in range(self.P.nrows())]) /// }}}A parent can use _get_action_ to indicate the action being used:
{{{id=127| class MyMatrixSpace(MyMatrixSpace): def _get_action_(self, P, op, self_on_left): if op!=operator.mul: return Module._get_action_(self, P, op, self_on_left) try: # duck type matrix spaces: Pcol = P.ncols() Prow = P.nrows() except AttributeError: return Module._get_action_(self, P, op, self_on_left) if self_on_left: if self.ncols()!=Prow: return Module._get_action_(self, P, op, self_on_left) else: return MyMatrixAction(self,P) else: if Pcol!=self.nrows(): return Module._get_action_(self, P, op, self_on_left) else: return MyMatrixAction(P, self) /// }}} {{{id=129| MS1 = MyMatrixSpace(QQ,2,4); MS2 = MyMatrixSpace(QQ,4,2) /// }}} {{{id=130| MS1.get_action(MS2) /// Left multiplication action of our matrices by Our space of 2x4 matrices over Rational Field on Our space of 4x2 matrices over Rational Field }}}Now, we define matrices both in our and in Sage's implementation, and see that by our definition of a multiplication action, we can multiply the rectangular matrices, even if different implementations are involved:
{{{id=131| M1 = MS1([[1,2,3,4],[5,6,7,8]]); M2 = MS2([[1,2],[3,4],[5,6],[7,8]]) m1 = matrix(QQ, [[1,2,3,4],[5,6,7,8]]); m2 = matrix(QQ, [[1,2],[3,4],[5,6],[7,8]]) /// }}} {{{id=132| M1*M2 /// [50, 60] [114, 140] }}} {{{id=133| m1*m2 # Check our result against Sage's result /// [ 50 60] [114 140] }}} {{{id=155| m1*M2 # Sage's matrix times our matrix /// [50, 60] [114, 140] }}} {{{id=156| M1*m2 # our matrix times Sage's matrix /// [50, 60] [114, 140] }}}And verify if the old stuff still works:
{{{id=141| 2*M1 /// [2, 4, 6, 8] [10, 12, 14, 16] }}} {{{id=138| M2*3 /// [3, 6] [9, 12] [15, 18] [21, 24] }}} {{{id=139| p*M2 /// [1, 2] [3, 4] [7, 8] [5, 6] }}}