Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download

All published worksheets from http://sagenb.org

Views: 168736
Image: ubuntu2004

How to implement basic algebraic structures

Simon King
Friedrich-Schiller-Universität Jena
E-mail: simon dot king at uni hyphen jena dot de
© 2011

The aim of this tutorial is to explain how one can benefit from Sage's category framework and coercion model when implementing new algebraic structures.

On the one hand, we try to explain the theoretical background. On the other hand, we beef up the theory with a sample implementation of fraction fields. The example is quite thorough and detailed. It covers Sage's category framework, the construction of unique parents, and both simple and advanced parts of Sage's coercion model.

The example is developed step by step, so that it should be easy to follow. Not all applications will require all details that we expose here, so that the reader may pick the cherries that are needed for her or his own work.

For the impatient

Start by inheriting from a sub-class of sage.structure.parent.Parent resp. of sage.structure.element.Element

These classes provide default methods that are essential for using Sage's coercion system. There are base classes for typical algebraic structures. Arithmetic operations should be implemented by single underscore methods, such as _add_, _mul_, _div_.

Make your parent structures unique objects

Two parent structures should be equal if and only if they are one and the same object. Use the existing tools for unique parents, such as UniqueFactory or UniqueRepresentation.

Declare your parent structure as an object of a category

Your parent structures will inherit further useful methods and consistency tests.

Assign your element class to an attribute called "Element" of your parent class

The elements will inherit further useful methods from the category. In addition, some basic conversions will immediately work.

If further conversions are needed: Implement a method _element_constructor_

Do not override a parent's call method!

Declare coercions

A coercion is a conversion that is implicitly used in arithmetic operations.

Run the automatic test suites, add test methods

Each method should be documented and provide a doc test (we are not giving examples here). In addition, any method defined for a category should be supported by a test method that is executed when running the test suite.

Optional: Define construction functors for your parent structure

That should be done if you need more complicated coercions, e.g., such that new parents are automatically created for you, or if you use different implementations of the same algebraic structure and want to smoothly do arithmetic involving the different implementations.

 

Chosing a base class

In Sage, objects that "contain" other objects and belong to a mathematical category (such as the category of sets or the category of algebras over the rational field) are called Parent. Hence, a parent in Sage and an object of a category are very similar notions. A member of a parent is called an element.

When implementing a parent in Sage, one should start with a class that inherits from :class:`sage.structure.parent.Parent`. Note that there exist some very old parents in Sage that are derived from other classes. We aim at refactoring them, but there needs more work to be done (volunteers welcome). Similarly, when implementing an element in Sage, one should start with a class that inherits from :class:' sage.structure.element.Element`.

Sage also has certain  base classes (derived from Parent and Element) that provide a basic framework for a variety of more concrete algebraic structures, such as groups, rings, or fields, and of their elements.

The parent

We want to implement a field, and we should thus start with the base class :class:`sage.rings.field.Field`.

from sage.rings.field import Field

Let us compare the methods provided by that class with the methods provided by a general parent:

[p for p in dir(Field) if p not in dir(Parent)]
['__div__', '__fraction_field', '__ideal_monoid', '__iter__', '__mul__', '__pow__', '__rdiv__', '__rmul__', '__rpow__', '__rxor__', '__xor__', '_an_element', '_an_element_c', '_an_element_impl', '_coerce_', '_coerce_c', '_coerce_impl', '_coerce_self', '_coerce_try', '_gens', '_gens_dict', '_has_coerce_map_from', '_ideal_class_', '_latex_names', '_list', '_one_element', '_pseudo_fraction_field', '_random_nonzero_element', '_richcmp', '_unit_ideal', '_zero_element', '_zero_ideal', 'algebraic_closure', 'base_extend', 'cardinality', 'characteristic', 'class_group', 'coerce_map_from_c', 'coerce_map_from_impl', 'content', 'divides', 'extension', 'fraction_field', 'gcd', 'gen', 'gens', 'get_action_c', 'get_action_impl', 'has_coerce_map_from_c', 'has_coerce_map_from_impl', 'ideal', 'ideal_monoid', 'integral_closure', 'is_atomic_repr', 'is_commutative', 'is_field', 'is_finite', 'is_integral_domain', 'is_integrally_closed', 'is_noetherian', 'is_prime_field', 'is_ring', 'is_subring', 'is_zero', 'krull_dimension', 'list', 'ngens', 'one', 'one_element', 'order', 'prime_subfield', 'principal_ideal', 'quo', 'quotient', 'quotient_ring', 'random_element', 'unit_ideal', 'zero', 'zero_element', 'zero_ideal', 'zeta', 'zeta_order']

Any ring in Sage has a so-called base and a base ring (sometimes the base ring, the base and the ring itself coincide). In our implementation of fraction fields, we use R as base of the fraction field of R, and we use the base ring of R as the base ring of its fraction field. The usual implementation of fraction fields in Sage does the same:

Frac(QQ['x']).base(), Frac(QQ['x']).base_ring()
(Univariate Polynomial Ring in x over Rational Field, Rational Field)

Note that some rings coincide with their base. And also note that not all quotient fields in Sage use base() to tell what ring they were obtained from. A notorious exception is the rational field:

QQ.base() is QQ is QQ.base_ring()
True

It is very easy to declare a ring as the base of a field: One just passes the ring as argument to the field constructor.

Field(ZZ['x']).base()
Univariate Polynomial Ring in x over Integer Ring

Hence, in our first approach, we simply write a class inheriting from Field, and can use the default initialisation. We provide our class with a custom string representation, such that it prints like a fraction field. In Python, the string representation is given by a method __repr__. Sage has a default implementation of __repr__, that relies on another method: _repr_, with single underscore. Hence, one implements _repr_, not __repr__.

Field.__repr__??

File: /sagenb/flask/sage-4.6.2/devel/sage/sage/structure/sage_object.pyx

Source Code (starting at line 112):

def __repr__(self):
    """
    Default method for string representation.

    NOTE:

    Do not overwrite this method. Instead, implement
    a ``_repr_`` (single underscore) method.

    EXAMPLES:

    By default, the string representation coincides with
    the output of the single underscore ``_repr_``::

        sage: P.<x> = QQ[]
        sage: repr(P) == P._repr_()  #indirect doctest
        True

    Using :meth:`rename`, the string representation can
    be customized::

        sage: P.rename('A polynomial ring')
        sage: repr(P) == P._repr_()
        False

    The original behaviour is restored with :meth:`reset_name`.

        sage: P.reset_name()
        sage: repr(P) == P._repr_()
        True
    """
    try:
        name = self.__custom_name
        if name is not None:
            return name
    except:
        pass
    try:
        repr_func = self._repr_
    except AttributeError:
        return str(type(self))
    else:
        return repr_func()

We also add a base_ring method (as mentioned above) and a method that returns the characteristic of the field (it is implicitly used in a test below).

class MyFrac(Field): def _repr_(self): return "NewFrac(%s)"%repr(self.base()) def base_ring(self): return self.base().base_ring() def characteristic(self): return self.base().characteristic()
MyFrac(ZZ['x'])
NewFrac(Univariate Polynomial Ring in x over Integer Ring)

Note that some parts of our example depend on fixes that are provided in trac tickets #9944 and #9138. We test whether they are merged into the Sage version you are using, such that we can work around, if needed.

try: Field('bla') is_9138_merged = False except: is_9138_merged = ZZ['x'] in IntegralDomains() is_9138_merged; ZZ['x'] in IntegralDomains()
False False

It is automatically tested whether the base ring is actually a ring (if #9138 is merged). But it is not tested whether it is a integral domain; we will see to that later.

MyFrac('bla') # it should raise an error, if #9138 is merged
NewFrac('bla')
MyFrac(Integers(15)) # We would like that an error is raised
NewFrac(Ring of integers modulo 15)
Integers(15) in IntegralDomains()
False

We remark that sometimes it is impossible to capture the mathematics by the inheritance of Python classes. For example, the set of m by n  matrices over a field (aka. MatrixSpace) form, in general, not more than a vector space. In particular, it is not a good idea to implement Matrix Spaces as a sub class of sage.rings.ring.Ring. However, if m=n then the matrix space is an algebra, thus, in particular is a ring. From the point of view of Python classes, both are the same:

MS1 = MatrixSpace(QQ,2,3) isinstance(MS1, Ring)
False
MS2 = MatrixSpace(QQ,2) isinstance(MS2, Ring)
False

However, the category framework can differentiate them, and is providing the matrix space with additional methods, if it happens to be a ring:

Rings()
Category of rings
MS1 in Rings()
False
MS2 in Rings()
True
import inspect len([s for s in dir(MS1) if inspect.ismethod(getattr(MS1,s,None))])
47
len([s for s in dir(MS2) if inspect.ismethod(getattr(MS2,s,None))])
58
MS1.__class__ is MS2.__class__
True

Some methods are in fact not inherited from the class but from somewhere else (the category framework - see below!):

len([s for s in dir(MS1.__class__) if inspect.ismethod(getattr(MS2.__class__,s,None))])
30

Below, we will explain how this can be taken advantage of.

The elements

We use the base class :class:`sage.structure.element.FieldElement`. Note that field elements do not require that their parent is a field:

from sage.structure.element import FieldElement
FieldElement(ZZ)
Generic element of a structure

An element of a fraction field is determined by a numerator and a denominator, which both live in the base ring of the fraction field, and the denominator must not be zero. We take care of that during initialisation, where we also normalise the denominator. By default, the denominator is one.

We provide methods that return numerator and denominator, and we provide a string representation that differs from the usual representation of fraction field elements in Sage, in order to see more easily whether an example uses the existing fraction fields or our new implementation.

class MyElement(FieldElement): def __init__(self, n,d=None, parent=None): if parent is None: raise ValueError, "The parent must be provided" B = parent.base() if d is None: d = B.one_element() if n not in B or d not in B: raise ValueError, "Numerator and denominator must be elements of %s"%B if d==0: raise ZeroDivisionError, "The denominator must not be zero" if d<0: self.n = -n self.d = -d else: self.n = n self.d = d FieldElement.__init__(self,parent) def numerator(self): return self.n def denominator(self): return self.d def _repr_(self): return "(%s):(%s)"%(self.n,self.d)
MyElement(4,-3,ZZ)
(-4):(3)
MyElement(4,-3.1, ZZ)
Traceback (most recent call last): File "<stdin>", line 1, in <module> File "_sage_input_28.py", line 10, in <module> exec compile(u'open("___code___.py","w").write("# -*- coding: utf-8 -*-\\n" + _support_.preparse_worksheet_cell(base64.b64decode("TXlFbGVtZW50KDQsLTMuMSwgWlop"),globals())+"\\n"); execfile(os.path.abspath("___code___.py"))' + '\n', '', 'single') File "", line 1, in <module> File "/tmp/tmpfHrDT3/___code___.py", line 3, in <module> exec compile(u'MyElement(_sage_const_4 ,-_sage_const_3p1 , ZZ)' + '\n', '', 'single') File "", line 1, in <module> File "", line 9, in __init__ ValueError: Numerator and denominator must be elements of Integer Ring
MyElement(4, 0, ZZ)
Traceback (most recent call last): File "<stdin>", line 1, in <module> File "_sage_input_29.py", line 10, in <module> exec compile(u'open("___code___.py","w").write("# -*- coding: utf-8 -*-\\n" + _support_.preparse_worksheet_cell(base64.b64decode("TXlFbGVtZW50KDQsIDAsIFpaKQ=="),globals())+"\\n"); execfile(os.path.abspath("___code___.py"))' + '\n', '', 'single') File "", line 1, in <module> File "/tmp/tmpe0fGN6/___code___.py", line 3, in <module> exec compile(u'MyElement(_sage_const_4 , _sage_const_0 , ZZ)' + '\n', '', 'single') File "", line 1, in <module> File "", line 11, in __init__ ZeroDivisionError: The denominator must not be zero
MyElement(3, parent=ZZ)
(3):(1)

Implementing arithmetic operations

In Python and Cython, arithmetic operations are implemented using double underscore methods such as __add__, __mul__, __div__, __invert__. Again, for most of them, Sage provides a default implementation that makes use of a single underscore method to be provided by the user. The default implementation uses Sage's powerful coercion model (see below). Therefore, one must not override the default arithmetic methods.

Before a single underscore method is called, the default implementation of the double underscore methods makes sure that all arguments belong to the same parent. So, when implementing the single underscore methods, we can assume that both arguments belong to the same parent.

Let us now refine our element class, so that we can do some arithmetic. Note that when constructing a new element, we do not directly name our class MyElement. Instead, we return an instance of the same class as self. The reason is that later we will in fact work with sub-classes of MyElement.

We also implement comparison with the method __cmp__, where we can again assume that both arguments belong to the same parent. Note that if you program in Cython, you must use a slightly different way, that is explained in the module sage.structure.element.

class MyElement(MyElement): def __cmp__(self, other): return cmp(self.n*other.denominator(), other.numerator()*self.d) def _add_(self, other): C = self.__class__ D = self.d*other.denominator() return C(self.n*other.denominator()+self.d*other.numerator(),D, self.parent()) def _sub_(self, other): C = self.__class__ D = self.d*other.denominator() return C(self.n*other.denominator()-self.d*other.numerator(),D, self.parent()) def _mul_(self, other): C = self.__class__ return C(self.n*other.numerator(), self.d*other.denominator(), self.parent()) def _div_(self, other): C = self.__class__ return C(self.n*other.denominator(), self.d*other.numerator(), self.parent())
P = MyFrac(ZZ) a = MyElement(3,4,P) b = MyElement(1,2,P)
a+b, a-b, a*b, a/b
((10):(8), (2):(8), (3):(8), (6):(4))

Note that there is a default implementation of exponentiation. Hence, unless you want to provide a more efficient exponentiation algorithm, you can rely on the default:

a^3
(27):(64)

We verify that the comparison does as we wish:

a-b == MyElement(1,4,P)
True

Also note that there is a default implementation of element tests. We can already do

a in P
True

since a is defined as an element of P. But we can, for example, not verify yet that the integers are contained in the fraction field of the ring of integers:

1 in P
Traceback (most recent call last): File "<stdin>", line 1, in <module> File "_sage_input_37.py", line 10, in <module> exec compile(u'open("___code___.py","w").write("# -*- coding: utf-8 -*-\\n" + _support_.preparse_worksheet_cell(base64.b64decode("MSBpbiBQ"),globals())+"\\n"); execfile(os.path.abspath("___code___.py"))' + '\n', '', 'single') File "", line 1, in <module> File "/tmp/tmpeD_BbF/___code___.py", line 3, in <module> exec compile(u'_sage_const_1 in P' + '\n', '', 'single') File "", line 1, in <module> File "parent.pyx", line 972, in sage.structure.parent.Parent.__contains__ (sage/structure/parent.c:6841) File "parent.pyx", line 895, in sage.structure.parent.Parent.__call__ (sage/structure/parent.c:6489) NotImplementedError

We will take care of that later.

Chosing a category

As we have mentioned above, a parent can be understood as an object of a category (in the sense of algebra). It is no surprise that our parent P defined above knows that it belongs to the category of fields, as it is derived from the base class of fields.

P.category()
Category of fields

However, we could choose a smaller category, namely the category of quotient fields.

Why should one choose a category?

As usual in Python, one can define base classes, and then all classes derived from them inherit certain default methods; we have seen examples above. In addition to that, one can provide default methods for all objects of a category, and for all elements of such objects. In other words, there is a different way of inheriting useful stuff. It does not rely on implementation details (such as class inheritance), but it relies on mathematical concepts.

In addition, the categories define test suites for their objects and elements. Hence, one also gets basic sanity tests for free.

How does this so-called category framework work?

Any category defines a base class for their objects ("parent class") and of the elements of their objects ("element class"). If the category is declared during initialisation of a parent, then the class of that parent is automatically dynamically changed into a sub-class of the category's parent class. Hence, it inherits additional methods. Note that dynamic classes do not work in Cython. Nevertheless, if one writes a parent in Cython, it still inherits the methods from the category: It is not by class inheritance but by virtue of a default implementation of __getattr__ that mimmicks the class inheritance.

So, it is strongly recommended to use the category framework both in Python and in Cython.

Let us see whether there is any gain in chosing the category of quotient fields instead of the category of fields:

QuotientFields().parent_class, QuotientFields().element_class
(<class 'sage.categories.quotient_fields.QuotientFields.parent_class'>, <class 'sage.categories.quotient_fields.QuotientFields.element_class'>)
[p for p in dir(QuotientFields().parent_class) if p not in dir(Fields().parent_class)]; [p for p in dir(QuotientFields().element_class) if p not in dir(Fields().element_class)]
[] ['_derivative', 'denominator', 'derivative', 'factor', 'numerator', 'partial_fraction_decomposition']

So, there is no immediate gain for the parent, but there is more structure for the elements.

Category framework for the parent

Let us start by modifying the parent's initialisation, such that the correct category is chosen resp. the correct category can be provided by the user.

from sage.categories.quotient_fields import QuotientFields class MyFrac(MyFrac): def __init__(self, base, category=None): Field.__init__(self, base, category=category or QuotientFields())

We demonstrate that by declaring the category, when constructing instances of MyFrac, their class is dynamically changed into a new class MyFrac_with_category. It is a common sub-class of MyFrac and of the category's parent class.

P = MyFrac(ZZ) type(P); isinstance(P,MyFrac); isinstance(P,QuotientFields().parent_class)
<class '__main__.MyFrac_with_category'> True True

We remark that the class of P is a so-called dynamic class, that uses some meta-class:

type(type(P))
<class 'sage.structure.dynamic_class.DynamicMetaclass'>

We can now demonstrate that P inherits additional methods. The base class Field does not have a method `sum`. But the parent class of the category of fields does have such method (in fact, it is defined for the category of commutative additive monoids), and it is available for P:

P.sum.__module__
'sage.categories.commutative_additive_monoids'

However, it does not work, yet, because it relies on some other stuff that we will implement later:

a = MyElement(3,4,P) b = MyElement(1,2,P) c = MyElement(-1,2,P) P.sum([a,b,c])
Traceback (most recent call last): File "<stdin>", line 1, in <module> File "_sage_input_45.py", line 10, in <module> exec compile(u'open("___code___.py","w").write("# -*- coding: utf-8 -*-\\n" + _support_.preparse_worksheet_cell(base64.b64decode("YSA9IE15RWxlbWVudCgzLDQsUCkKYiA9IE15RWxlbWVudCgxLDIsUCkKYyA9IE15RWxlbWVudCgtMSwyLFApClAuc3VtKFthLGIsY10p"),globals())+"\\n"); execfile(os.path.abspath("___code___.py"))' + '\n', '', 'single') File "", line 1, in <module> File "/tmp/tmpfXpKTO/___code___.py", line 6, in <module> exec compile(u'P.sum([a,b,c])' + '\n', '', 'single') File "", line 1, in <module> File "/sagenb/flask/sage-4.6.2/local/lib/python2.6/site-packages/sage/categories/commutative_additive_monoids.py", line 137, in sum return sum(args, self.zero()) File "ring.pyx", line 502, in sage.rings.ring.Ring.zero_element (sage/rings/ring.c:4994) File "parent.pyx", line 895, in sage.structure.parent.Parent.__call__ (sage/structure/parent.c:6489) NotImplementedError

Category framework for the elements

The basic idea remains the same: A Python class is dynamically changed into a sub-class of the element class of a category, and that behaviour is mimmicked for Cython classes as well. However, the category framework is invoked in a different way for elements than for parents.

One has to provide the parent with an attribute called "Element", whose value is the class that is used to implement all elements of that parent. Then, there is a :class:`~sage.misc.lazy_attribute.lazy_attribute` of the name "element_class", that automatically mixes the class provided by "Element" with the element class of the parent's category. Note that this holds if the parent class is written in Python. For parents written in Cython, lazy attributes will only be available if trac ticket #11115 is merged.

For providing our fraction fields with their own element classes, we just need to add a single line to our class:

class MyFrac(MyFrac): Element = MyElement

That little change has dramatic consequences. We can now create elements by simply calling the parent, we can suddenly use a method zero_element, and the "sum" method works:

P = MyFrac(ZZ) P(1), P(2,3)
((1):(1), (2):(3))
P.zero_element()
(0):(1)
a = MyElement(9,4,P) b = MyElement(1,2,P) c = MyElement(-1,2,P) P.sum([a,b,c])
(36):(16)

How did that happen?

Since we provided the attribute "Element", P has now also a lazy attribute "element_class". It provides another dynamic class (having a meta-class), which  is a sub-class of both MyElement and of `P.category().element_class`:

P.__class__.element_class
<sage.misc.lazy_attribute.lazy_attribute object at 0x10a72d0>
P.element_class; type(P.element_class)
<class '__main__.MyFrac_with_category.element_class'> <class 'sage.structure.dynamic_class.DynamicMetaclass'>
issubclass(P.element_class, MyElement); issubclass(P.element_class,P.category().element_class)
True True

The default __call__ method of a parent P simply passes the arguments to P.element_class, adding a named argument "parent=P". That is the reason for the choice of argument names in MyElement.__init__, and for their order.

In particular, elements obtained by calling P are instances of that new automatically created class.

type(P(2,3))
<class '__main__.MyFrac_with_category.element_class'>

That is the reason why we did not explicitly name the class in the arithmetic methods of MyElement. If we create one element by calling P (this is the case, e.g., for P.zero_element()), and add any other instance of MyElement to it, then the sum will also be an instance of P.element_class. Since P.zero_element() is an instance of P.element_class and since P.sum([...]) starts with P.zero_element(), we thus have:

type(a); isinstance(a,P.element_class); type(P.sum([a,b,c]))
<class '__main__.MyElement'> False <class '__main__.MyFrac_with_category.element_class'>

We have already seen above that the element class of P.category() provides a method factor(). Let us try if it works:

a; a.factor()
(9):(4) 2^-2 * 3^2

It does! That may come as a surprise, since a is actually not an instance of P.element_class, and its class does not know about the factor method. But, as we have mentioned earlier, the __getattr__ method works around a missing class inheritance.

hasattr(type(a), 'factor'); hasattr(P.element_class, 'factor'); hasattr(a, 'factor')
False True True

Note, however, that the access to the factor method is much faster if one has an actual instance of P.element_class:

timeit('a.factor'); type(a)
625 loops, best of 3: 4.43 µs per loop <class '__main__.MyElement'>
a2 = P(9,4) a2 == a
True
timeit('a2.factor'); type(a2)
625 loops, best of 3: 309 ns per loop <class '__main__.MyFrac_with_category.element_class'>

So, for efficiency, it is better to use the category  framework properly.

Uniqueness of parents

In sage, all parents should be globally unique. That means: If two parents are equal then they should actually be the same objects - even if their definition looks different. For example:

QQ['x'] is PolynomialRing(QQ,names=['x'])
True
Frac(QQ['x']) is PolynomialRing(QQ,names=['x']).fraction_field()
True
QQ.algebraic_closure().completion(infinity,20) is QQ.completion(infinity,20).algebraic_closure() is ComplexField(20)
True

That requirement is not met by our implementation, yet:

MyFrac(ZZ) is MyFrac(ZZ)
False

There are several tools in Sage to construct unique parents. See, for instance, :class:`sage.structure.factory.UniqueFactory`, which can be used for rather complicated cases, where very different input may result in the same objects.

Here, we have a simpler case, and we use Python, so that we can use :class:`sage.structure.unique_representation.UniqueRepresentation`. The simplemost usage would be to make MyFrac inherit first from UniqueRepresentation and then from sage.rings.field.Field.

However, as we have announced above, we want that our fraction fields test whether the given ring is an integral domain. Therefore, we want to preprocess the arguments before passing them to the __init__ method. That can be done in a __classcall__ method, that needs to be a static method.

from sage.categories.integral_domains import IntegralDomains from sage.structure.unique_representation import UniqueRepresentation class MyFracUnique(UniqueRepresentation, MyFrac): @staticmethod def __classcall__(cls, base, category=None): if (base not in Rings()) or (is_9138_merged and base not in IntegralDomains()): raise TypeError, "%s is no integral domain"%base return super(MyFracUnique, cls).__classcall__(cls, base, category)

Now, uniqueness holds, and rings that are no integral domains are refused.

MyFracUnique(ZZ) is MyFracUnique(ZZ)
True
MyFracUnique(ZZ['x']) is MyFracUnique(ZZ['x'])
True
MyFracUnique(Integers(15)) # should raise an error if #9138 is merged
NewFrac(Ring of integers modulo 15)
MyFracUnique(SymmetricGroup(5)) # should raise the error even without #9138
Traceback (most recent call last): File "<stdin>", line 1, in <module> File "_sage_input_68.py", line 10, in <module> exec compile(u'open("___code___.py","w").write("# -*- coding: utf-8 -*-\\n" + _support_.preparse_worksheet_cell(base64.b64decode("TXlGcmFjVW5pcXVlKFN5bW1ldHJpY0dyb3VwKDUpKSAjIHNob3VsZCByYWlzZSB0aGUgZXJyb3IgZXZlbiB3aXRob3V0ICM5MTM4"),globals())+"\\n"); execfile(os.path.abspath("___code___.py"))' + '\n', '', 'single') File "", line 1, in <module> File "/tmp/tmpEUVAqR/___code___.py", line 3, in <module> exec compile(u'MyFracUnique(SymmetricGroup(_sage_const_5 )) # should raise the error even without #9138' + '\n', '', 'single') File "", line 1, in <module> File "/sagenb/flask/sage-4.6.2/local/lib/python2.6/site-packages/sage/misc/classcall_metaclass.py", line 258, in __call__ return cls.__classcall__(cls, *args, **options) File "", line 5, in __classcall__ TypeError: SymmetricGroup(5) is no integral domain

Note that Sage's tools for creating unique parents also take care of pickling (at least to some extent):

loads(dumps(MyFracUnique(ZZ))) is MyFracUnique(ZZ)
True

Coercion - the basics

Coercion is involved if one wants to be able to do arithmetic, comparisons, etc. between elements of distinct parents.

Theoretical background

Dissociation from type conversion

Perhaps the reader is familiar with the concept of coercion in the programming language C, which is more or less a synonyme for "automatic type conversion". A very basic example of an automatic type conversion is the sum of an integer with a floating point number: The integer is automatically converted into a floating point number, and then the summation takes place.

However, coercion in Sage is not about a change of types, but about a change of parents. Elements of the same type may have very different parents, such as here:

P1 = QQ['v,w']; P2 = ZZ['v,w']
type(P1.gen()) == type(P2.gen()); P1==P2
True False

Every element of P2 can be naturally interpreted as an element of P1. So, it makes sense to be able to add elements of the two rings - the result should then live in P1:

(P1.gen()+P2.gen()).parent() is P1
True

It would be rather inconvenient if one needed to explicitly convert an element of P2 into P1 before adding. The coercion system does that conversion automatically.

Of course, one must not overdo the automatic conversion. There are situations in which a conversion is possible, but one would not want to use that conversion for arithmetics. For example, while every element of a finite prime field can be converted into an integer (by chosing a representative), one would certainly not want to add elements of GF(2) with elements of GF(3); in particular one would not want that the result is an integer.

Coercion versus conversion

A coercion happens implicitly, without being explicitly requested by the user. Hence, coercion must be based on mathematical rigour. The user may want to convert a finite field element into an integer, and can do so explicitly - but such conversion must never happen implicitly. We elaborate on the conditions that a conversion needs to satisfy in order to be eligible as a coercion.

A coercion is a map, a conversion may be a partial map

Let P1 and P2 be two parents. It is possible that some elements of P1 can be converted into P2, but others can't. For example, a polynomial of degree zero over the integers can be interpreted as an integer - but there is no general conversion of a polynomial into an integer.

The first requirement is:

The coercion of elements of P1 into P2 must be defined for all elements of P1. There may be a conversion of some elements of P1 into P2, but if it is not globally defined, then it is no coercion.

Hence, coercion maps are defined on the level of parents, and they can be requested with the following methods:

P1.has_coerce_map_from(P2)
True
P1.coerce_map_from(P2)
Call morphism: From: Multivariate Polynomial Ring in v, w over Integer Ring To: Multivariate Polynomial Ring in v, w over Rational Field
P2.has_coerce_map_from(P1)
False
P2.coerce_map_from(P1) is None
True
A coercion is structure preserving

Any real number can be converted to an integer, namely by rounding. However, such a conversion is not useful in arithmetic operations:

int(1.6)+int(2.7) == int(1.6+2.7)
False

The reason is that rounding real numbers is no ring homomorphism. So, the next requirement is:

Coercion maps are morphisms, i.e., they are structure preserving.

It depends on the parents which structure is to be preserved. For example, if one parent is a ring and the other parent is a field, then a coercion between the two must be a ring homomorphism. The coercion from the integers into the rational field, for instance, is a homomorphism of euclidean domains:

QQ.coerce_map_from(ZZ).category_for()
Category of euclidean domains
There is at most one coercion map between two parents

There are, in general, many different homomorphisms from a ring P1 to a ring P2. It is possible that none of them is used for coercion; however, it must never occur that two different coercions are simultaneously used. In addition, if there is a coercion from P1 to P2 and  x is an element of P1, then the conversion P2(x) has to coincide with the coercion of x into P2.

Coercion maps can be composed

If there is a coercion φ from P1 to P2 and another coercion ψ from P2 to P3, then the composition of φ followed by ψ must yield a (the) coercion from P1 to P3.

The identity is a coercion

There is a coercion from any parent into itself, namely the identity map. Together with the two preceding requirements, it follows: If there are conversions from P1 to P2 and from P2 to P1, then they are mutually inverse.

How to meet the above requirements, and preserve common sense

As an illustration, we discuss here the rationale behind the choice of coercions between multivariate polynomial rings in Sage. Let R be a ring.

Obviously, there can be many ring homomorphisms between two polynomial rings over R. So, we have to fix a canonical choice. If the polynomial ring P2 has at least as many generators as P1, then one could map generator number n of P1 to generator number n of P2. Such "place preserving" conversions would certainly qualify as a coercion, since the composition of two place preserving conversions is a place preserving conversion.

However, common sense would would more likely take the names of the generators into account: When mapping R[x,y] to R[y,x], one would rather map x to x and y to y, not the first generator x of R[x,y] to the first generator y of R[y,x].

Sage tries to balance common sense and generality. If two polynomial rings have the same generators (but potentially in a different order), then there is a name preserving coercion:

P1.<x,y> = QQ[]; P2 = QQ['y,x,z']
f = P2.coerce_map_from(P1); f
Call morphism: From: Multivariate Polynomial Ring in x, y over Rational Field To: Multivariate Polynomial Ring in y, x, z over Rational Field
f(x);f(y)
x y

However, if a name preserving map is not possible, then there is no coercion.

P3 = QQ['z,x'] P1.has_coerce_map_from(P3)
False

Assume that we would use a place preserving map from P3 to P1 for coercion. Then, the composition with the name preserving coercion from P1 to P2 must be a coercion, too. But that composition is not name preserving and is thus different from the name preserving coercion from P3 to P2 that we already chose:

g = P2.coerce_map_from(P3) P3.0, g(P3.0); P3.1,g(P3.1)
(z, z) (x, x)

By the requested uniqueness of coercions, a place preserving coercion from P3 to P1 is thus impossible.

One may use the place preserving map from P3 to P1 explicitly as a conversion, but it would not be used implicitly by Sage.

h = P1.convert_map_from(P3); h
Call morphism: From: Multivariate Polynomial Ring in z, x over Rational Field To: Multivariate Polynomial Ring in x, y over Rational Field
P3.0;h(P3.0)
z x
P3.1;h(P3.1)
x y

Implementing conversion

We return to our example: Fraction fields. We were able to implement some conversions by simply providing the attribute "Element": We can convert from the base ring into the fraction field. However, we can not convert elements of a fraction field into elements of another fraction field, yet.

 

P(2/3)
Traceback (most recent call last): File "<stdin>", line 1, in <module> File "_sage_input_87.py", line 10, in <module> exec compile(u'open("___code___.py","w").write("# -*- coding: utf-8 -*-\\n" + _support_.preparse_worksheet_cell(base64.b64decode("UCgyLzMp"),globals())+"\\n"); execfile(os.path.abspath("___code___.py"))' + '\n', '', 'single') File "", line 1, in <module> File "/tmp/tmp8F9p4r/___code___.py", line 3, in <module> exec compile(u'P(_sage_const_2 /_sage_const_3 )' + '\n', '', 'single') File "", line 1, in <module> File "parent.pyx", line 915, in sage.structure.parent.Parent.__call__ (sage/structure/parent.c:6668) File "coerce_maps.pyx", line 82, in sage.structure.coerce_maps.DefaultConvertMap_unique._call_ (sage/structure/coerce_maps.c:3119) File "coerce_maps.pyx", line 77, in sage.structure.coerce_maps.DefaultConvertMap_unique._call_ (sage/structure/coerce_maps.c:3022) File "/sagenb/flask/sage-4.6.2/local/lib/python2.6/site-packages/sage/categories/sets_cat.py", line 307, in _element_constructor_from_element_class return self.element_class(parent = self, *args, **keywords) File "", line 9, in __init__ ValueError: Numerator and denominator must be elements of Integer Ring

Readers familiar with Python may assume that the conversion is implemented by overriding the __call__ method of a parent. However, the default __call__ method should never be overridden. Again, some old parents violate that rule, and we'd appreciate help to refactor them.

Conversions should be implemented by providing the method _element_constructor_, that accepts arguments to be converted, perhaps accepts optional arguments, and returns an element of the parent. The returned element should be an instance of the parent's element class.

We test whether the given argument is an element of a fraction field by testing the category of its parent. If it is, then we can use numerator and denominator. If it has no parent or the parent is no fraction field, then we simply hope that our element class can deal with the input.

Note that we return to the old name of the class (MyFrac, not MyFracUnique), because otherwise an infinite recursion would occur in the __classcall__ method. But uniqueness of the parent is still granted.

class MyFrac(MyFracUnique): def _element_constructor_(self, *args,**kwds): if len(args)!=1: return self.element_class(*args,parent=self,**kwds) x = args[0] try: P = x.parent() except AttributeError: return self.element_class(x,parent=self,**kwds) if P in QuotientFields() and P != self.base(): return self.element_class(x.numerator(),x.denominator(),parent=self,**kwds) return self.element_class(x,parent=self,**kwds)
P = MyFrac(ZZ) P is MyFrac(ZZ)
True

In addition to the conversion from the base ring and from pairs of base ring elements, we now also have a conversion from the rationals to our fraction field of ZZ:

P(2); P(2,3); P(3/4)
(2):(1) (2):(3) (3):(4)

Recall that above, the test "1 in P" failed with an error. Since we can now convert the integer 1 into P, the error has vanished. But there is still a negative outcome:

1 in P
False

Establishing a coercion

We have a conversion from the base ring and even from the rational field to P. But it is not known as a coercion:

P.has_coerce_map_from(ZZ), P.has_coerce_map_from(QQ)
(False, False)

We could use P.register_coercion during initialisation (see documentation of that method) for declaring that there is a coercion from a ring to its fraction field. A more flexible way is to provide a method _coerce_map_from_ (with leading and trailing underscore). It accepts a parent as argument. If it returns an actual map, then this map is used for coercion. Otherwise, if it returns something that evaluates to "True", then conversion is used for coercion.

Note that we need a special case for the rational field, since QQ.base() is not the ring of integers.

class MyFrac(MyFrac): def _coerce_map_from_(self, S): if self.base().has_coerce_map_from(S): return True if S in QuotientFields(): if self.base().has_coerce_map_from(S.base()): return True if hasattr(S,'ring_of_integers') and self.base().has_coerce_map_from(S.ring_of_integers()): return True

By the method above, a parent coercing into the base ring will also coerce into the fraction field, and a fraction field coerces into another fraction field if there is a coercion of the corresponding base rings. Now, we have:

P = MyFrac(QQ['x']) P.has_coerce_map_from(ZZ['x']), P.has_coerce_map_from(Frac(ZZ['x'])), P.has_coerce_map_from(QQ)
(True, True, True)

We can now use coercion from ZZ['x'] and from QQ into P for arithmetic operations between the two rings:

3/4+P(2)+ZZ['x'].gen(), (P(2)+ZZ['x'].gen()).parent() is P
((4*x + 11):(4), True)

Equality and element containment

Recall that above, the test "1 in P" failed. Let us repeat the test now:

1 in P
True

Why is that?

The default element containment test "x in P" is based on the interplay of three building blocks: conversion, coercion and equality test.

  1. Clearly, if the conversion P(x) raises an error, then x can not be seen as an element of P. On the other hand, a conversion P(x) can in general do very nasty things. So, the fact that P(x) works does not mean that x is in P.
  2. If x.parent() is P, then the conversion P(x) will not change x (at least, that's the default). Hence, we will have x==P(x).
  3. Sage uses coercion not only for arithmetic operations, but also for comparison. Hence, if there is a coercion between the parent of x and P, then it will be applied. If there is a coercion from x.parent() to P then x==P(x) reduces to P(x)==P(x). Otherwise, x==P(x) will evaluate as false.

That leads to the following default implementation of element containment testing:

"x in P" holds if and only if "x==P(x)" does not raise an error and evaluates as true

If the user is not happy with that behaviour, the "magical" Python method __contains__ can be overridden.

Coercion - the advanced parts

So far, we are able to add integers and rational numbers to elements of our new implementation of the fraction field of ZZ.

P = MyFrac(ZZ)
1/2+P(2,3)+1
(13):(6)

Surprisingly, we can even add a polynomial over the integers to an element of P, even though the result lives in a new parent, namely in a polynomial ring over P:

P(1/2) + ZZ['x'].gen(), (P(1/2) + ZZ['x'].gen()).parent() is P['x']
((1):(1)*x + (1):(2), True)

That positive surprise may make us confident enough to try another, seemingly more easy example. There obviously is a coercion from the fraction field of ZZ to the fraction field of ZZ['x']. However, Sage does not know enough about our new implementation of fraction fields. Hence, it does not recognise the coercion:

Frac(ZZ['x']).has_coerce_map_from(P)
False

The aim of this section is to explain why and how a new ring was constructed when adding and element of P to an integral polynomial, and how we can establish a coercion from P to Frac(ZZ['x']).

Most parents can be constructed from simpler pieces. For example, the polynomial ring in variables x and y over the rational field can be obtained from the ring of integers by first applying the fraction field construction, then constructing a polynomial ring with generator x, and then constructing a polynomial ring with generator y (or: constructing a bivariate ring after the fraction field construction).

These constructions are functorial: The fraction field is a functor from the category of integral domains to the category of fields, and the construction of a polynomial ring with generator x is a functor from the category of rings into itself.

The advanced parts of Sage's coercion model are based on these functors, which are referred to as construction functors. If a parent is obtained from a simpler parent by applying a construction functor, then it should reveal it by a method `construction()`:

Poly,R = QQ[x].construction() Poly,R
(Poly[x], Rational Field)
Poly(R) is QQ['x']; Poly(ZZ) is ZZ['x']; Poly(P) is P['x']
True True True
Fract,R = QQ.construction() Fract,R
(FractionField, Integer Ring)
Poly*Fract
Poly[x](FractionField(...))
(Poly*Fract)(ZZ) is QQ[x]
True

Note that construction functors do not necessarily commute: The polynomial ring over the fraction field of the integers is not the same as the fraction field of the polynomial ring over the integers.

(Fract*Poly)(ZZ)
Fraction Field of Univariate Polynomial Ring in x over Integer Ring

Now, let P1 and P2 be two parents that are obtained from the same parent R by applying two different construction functors F1, F2.

It is often possible to combine F1 and F2 to a new construction functor F3, such that F3(R) is a new parent P3 which both P1 and P2 coerce into. In analogy to a notion of category theory, P3 is called the pushout of P1 and P2; and similarly F3 is called the pushout of F1 and F2.

from sage.categories.pushout import pushout pushout(Fract(ZZ),Poly(ZZ))
Univariate Polynomial Ring in x over Rational Field

Obviously, both F1*F2 and F2*F1 are natural candidates for the pushout of F1 and F2. But the pushout must not depend on the order in which F1 and F2 are provided. Hence, there must be a consistent choice.

Any construction functor has an attribute `rank`, and the convention is that if F1.rank is smaller than F2.rank, then the pushout is F2*F1 (hence, F1 is applied first, since the multiplication is in functorial notation). We have

Fract.rank, Poly.rank
(5, 9)

and thus the pushout is

Fract.pushout(Poly), Poly.pushout(Fract)
(Poly[x](FractionField(...)), Poly[x](FractionField(...)))

regardless of the order in which Fract and Poly are provided. That is why the sum of a rational number and a polynomial over the integers is a polynomial over the rationals, and not an element of the fraction field of the polynomials over the integers.

However, only "elementary" construction functors have a rank:

(Fract*Poly).rank
Traceback (most recent call last): File "<stdin>", line 1, in <module> File "_sage_input_114.py", line 10, in <module> exec compile(u'open("___code___.py","w").write("# -*- coding: utf-8 -*-\\n" + _support_.preparse_worksheet_cell(base64.b64decode("KEZyYWN0KlBvbHkpLnJhbms="),globals())+"\\n"); execfile(os.path.abspath("___code___.py"))' + '\n', '', 'single') File "", line 1, in <module> File "/tmp/tmp3Kv5Ap/___code___.py", line 2, in <module> exec compile(u'(Fract*Poly).rank' + '\n', '', 'single') File "", line 1, in <module> AttributeError: 'CompositeConstructionFunctor' object has no attribute 'rank'

So, what do we do if P1 and P2 are obtained from R by two chains of construction functors?

The two functor chains will be shuffled: If the rank of the next unused functor F1 of the first chain is smaller (or greater) than the rank of the next unused functor F2 of the second chain, then F1 (or F2) is applied, and the next functor in the first (or second) chain is considered. If the ranks of F1 and F2 coincide, then we need another idea that is explained below.

As an illustration, we first get us some functors and then see how chains of functors are shuffled.

AlgClos, R = CC.construction(); AlgClos
AlgebraicClosureFunctor
Compl, R = RR.construction(); Compl
CompletionFunctor
Matr, R = (MatrixSpace(ZZ,3)).construction(); Matr
MatrixFunctor
AlgClos.rank, Compl.rank, Fract.rank, Poly.rank, Matr.rank
(3, 4, 5, 9, 10)

When we apply Fract, AlgClos, Poly and Fract to the ring of integers, we obtain

(Fract*Poly*AlgClos*Fract)(ZZ)
Fraction Field of Univariate Polynomial Ring in x over Algebraic Field

When we apply Compl, Matr and Poly to the ring of integers, we obtain

(Poly*Matr*Compl)(ZZ)
Univariate Polynomial Ring in x over Full MatrixSpace of 3 by 3 dense matrices over Real Field with 53 bits of precision

When we shuffle Fract,AlgClos,Poly,Fract with Compl,Matr,Poly according to the ranks, we start with Compl (second chain), since Compl.rank<Fract.rank. Then we proceed with the complete first chain, since Fract.rank, AlgClos.rank and Poly.rank are all smaller than Matr.rank. We finish with the remaining functors of the second chain, Matr and Poly.

So, we expect that the pushout of the above parents is obtained by applying Compl, Fract, AlgClos, Poly, Fract, Matr and Poly to the ring of integers. That is indeed the case:

(Poly*Matr*Fract*Poly*AlgClos*Fract*Compl)(ZZ)
Univariate Polynomial Ring in x over Full MatrixSpace of 3 by 3 dense matrices over Fraction Field of Univariate Polynomial Ring in x over Complex Field with 53 bits of precision
pushout((Fract*Poly*AlgClos*Fract)(ZZ), (Poly*Matr*Compl)(ZZ))
Univariate Polynomial Ring in x over Full MatrixSpace of 3 by 3 dense matrices over Fraction Field of Univariate Polynomial Ring in x over Complex Field with 53 bits of precision

There remains the case that the next unused functor F1 of the first chain has the same rank as the next unused functor F2 of the second chain. Sage's pushout constructions offers two ways to proceed:

  1. Construction functors have a method `merge()` that either returns None or returns a construction functor. By default, F1.merge(F2) is F1 if F1==F2, and is None otherwise. If F1.merge(F2) returns a functor F3, then F3 is applied, and both F1 and F2 are discarded.
  2. Construction functors have a method `commutes()`. If F1.commutes(F2) or F2.commutes(F1) return True, then it is guaranteed that it does not matter in which order F1 and F2 are applied to any given parent R. So, we apply both F1 and F2, in any order.

We remark that the `commutes` method exists, but it seems that so far nobody has implemented two functors of the same rank that commute, although some functors of different ranks do commute:

Compl.commutes(Fract), Compl.rank==Fract.rank # Should be (True, False), if #8807 is merged
(False, False)

The `merge` method, however, plays an important role. Typically it is used to provide coercion between different implementations of the same algebraic structure: For example, F1(R) may be the dense implementation of a polynomial ring and and F2(R) a sparse implementation. Then, F1.merge(F2) should be a functor F3, such that F3(R) is the same algebraic structure as F1(R) and F2(R), in a default implementation.

Here, we have two implementations of fraction fields, namely the one that already existed in Sage, and the new implementation that is the main example of our tutorial. We are now creating a construction functor `MyFracFunctor`, and merge it with the usual fraction field functor in such a way that the new functor becomes the default.

Since functors will only be merged if their ranks coincide, our new functor must have the same rank as the usual fraction field construction functor, namely 5. It is a functor from the category of integral domains to the category of fields.

Note that since sage-4.7, the application of a functor to an object of a category should be implemented using the method `_apply_functor`. Do not override the default __call__ method (unless you are working with an old version of Sage)!

from sage.categories.pushout import ConstructionFunctor class MyFracFunctor(ConstructionFunctor): rank = 5 def __init__(self): if is_9138_merged: ConstructionFunctor.__init__(self, IntegralDomains(), Fields()) else: ConstructionFunctor.__init__(self, Rings(), Fields()) def _apply_functor(self, R): return MyFrac(R) def merge(self, other): if isinstance(other, (type(self), sage.categories.pushout.FractionField)): return self
MyFracFunctor()
MyFracFunctor

However, at the time of writing this tutorial, the main sage notebook server was still using sage-4.6. In that version, MyFracFunctor would return none, which is a bug fixed in trac ticket #8800. If that is the case, we use a little work-around.

print MyFracFunctor()(ZZ)
NewFrac(Integer Ring)
if MyFracFunctor()(ZZ) is None: class MyFracFunctor(MyFracFunctor): def __call__(self, R): return self._apply_functor(R)

We verify that our functor can really be used to construct our implementation of fraction fields, and that it can be merged with either itself or the usual fraction field constructor:

MyFracFunctor()(ZZ)
NewFrac(Integer Ring)
MyFracFunctor().merge(MyFracFunctor())
MyFracFunctor
MyFracFunctor().merge(Fract)
MyFracFunctor

There remains to let our new fraction fields know about the new construction functor:

class MyFrac(MyFrac): def construction(self): return MyFracFunctor(), self.base()
MyFrac(ZZ['x']).construction()
(MyFracFunctor, Univariate Polynomial Ring in x over Integer Ring)

Due to merging, we have:

pushout(MyFrac(ZZ['x']), Frac(QQ['x']))
NewFrac(Univariate Polynomial Ring in x over Rational Field)

Note that merging of functors really occurs here: There is no coercion from QQ['x'] to ZZ['x'].

The Test Suites

We have already mentioned that the category framework does not only provide functionality but  also a test framework. The aim of this last section is to show how one can add more tests.

Parents or elements that belong to a certain category must provide certain methods, in order to smoothly blend with the methods that already exist in Sage. The methods that ought to be provided are called "abstract methods", and they can be required or optional. Let us see what methods are needed for quotient fields and their elements:

from sage.misc.abstract_method import abstract_methods_of_class
abstract_methods_of_class(QuotientFields().parent_class)
{'required': ['__contains__'], 'optional': []}

Hence, the only required method (that is actually required for all parents that belong to the category of sets) is an element containment test. That's fine, because the base class `sage.structure.parent.Parent` provides a default containment test.

The elements have to provide more:

abstract_methods_of_class(QuotientFields().element_class)
{'required': ['denominator', 'numerator'], 'optional': ['_add_', '_mul_']}

Hence, the elements must provide denominator() and numerator() methods, may provide Sage's single underscore arithmetic methods (actually any ring element should provide them).

Then, there are test methods defined for the parent or element class of a category. Their names start with "_test_". They test, for example, whether a parent P actually is an instance of the parent class of the category of P. Or, if the definition of category involves properties such as commutativity, then that property is tested. Or the existence of required abstract methods is tested.

Here are the tests that form the test suite of quotient fields:

[t for t in dir(QuotientFields().parent_class) if t.startswith('_test_')]
['_test_additive_associativity', '_test_an_element', '_test_associativity', '_test_distributivity', '_test_elements', '_test_elements_eq', '_test_one', '_test_prod', '_test_some_elements', '_test_zero']

Since we have implemented all abstract methods, we are confident that the test suite runs without complaining. In fact, it does!

P = MyFrac(ZZ['x'])
TestSuite(P).run()

Let us see what tests are actually performed:

TestSuite(P).run(verbose=True)
running ._test_additive_associativity() . . . pass running ._test_an_element() . . . pass running ._test_associativity() . . . pass running ._test_category() . . . pass running ._test_distributivity() . . . pass running ._test_elements() . . . Running the test suite of self.an_element() running ._test_category() . . . pass running ._test_eq() . . . pass running ._test_not_implemented_methods() . . . pass running ._test_pickling() . . . pass pass running ._test_elements_eq() . . . pass running ._test_eq() . . . pass running ._test_not_implemented_methods() . . . pass running ._test_one() . . . pass running ._test_pickling() . . . pass running ._test_prod() . . . pass running ._test_some_elements() . . . pass running ._test_zero() . . . pass

As one can see, tests are also performed on elements. There are methods that return one element or a list of some elements, relying on "typical" elements that can be found in most algebraic structures.

P.an_element(); P.some_elements()
(2):(1) [(2):(1)]

Unfortunately, the list of elements that is returned by the default method is of length one, and that single element could also be a bit more interesting.

The method an_element relies on a method _an_element_, so, we implement that. We also override the some_elements method.

class MyFrac(MyFrac): def _an_element_(self): a = self.base().an_element() b = self.base_ring().an_element() if (a+b)!=0: return self(a)**2/(self(a+b)**3) if b != 0: return self(a)/self(b)**2 return self(a)**2*self(b)**3 def some_elements(self): return [self.an_element(),self(self.base().an_element()),self(self.base_ring().an_element())]
P = MyFrac(ZZ['x']) P.an_element(); P.some_elements()
(x^2):(x^3 + 3*x^2 + 3*x + 1) [(x^2):(x^3 + 3*x^2 + 3*x + 1), (x):(1), (1):(1)]

Now, as we have more  interesting elements, we may also add a test for the "factor" method. Recall that the method was inherited from the category, but it appears that it is not tested. The tests that form the test suites should be implemented as a parent or element method of a category (and in addition, each method should have a documentation with tests, but that's a different topic).

Normally, a test for a method defined by a category should be provided by the same category. Hence, since `factor()` is defined in the category of quotient fields, a test should be added there. But we won't change source code here and will instead create a sub-category.

Apparently, the product of the factors returned be `e.factor()` should be equal to `e`. For forming the product, we use the `prod()` method, that, no surprise, is inherited from another category.

P.prod.__module__
'sage.categories.monoids'

When we want to create a sub-category, we need to provide a method `super_categories`, that returns a list of all immediate super categories (here: category of quotient fields); it should be a cached method, for efficiency (at least it will be more efficient if trac ticket #11115 is merged). The parent and element methods of a category are provided as methods of a class `ParentMethods` and `Element Methods`, as follows:

from sage.categories.category import Category class QuotientFieldsWithTest(Category): @cached_method def super_categories(self): return [QuotientFields()] class ParentMethods: pass class ElementMethods: def _test_factorisation(self, **options): P = self.parent() assert self == P.prod([P(b)**e for b,e in self.factor()])

We provide an instance of our quotient field implementation with that new category. Note that categories have a default _repr_ method, that guesses a good string representation from the name of the class: QuotientFieldsWithTest becomes "quotient fields with test".

P = MyFrac(ZZ['x'], category=QuotientFieldsWithTest()) P.category()
Category of quotient fields with test

The new test is inherited from the category. Since an_element() is returning a complicated element, _test_factorisation is a serious test.

P.an_element()._test_factorisation
<bound method MyFrac_with_category.element_class._test_factorisation of (x^2):(x^3 + 3*x^2 + 3*x + 1)>
P.an_element().factor()
(x + 1)^-3 * x^2

Last, we observe that the new test has automatically become part of the test suite. We remark that the existing test became more serious as well, by an_element() returning something more interesting.

TestSuite(P).run(verbose=True)
running ._test_additive_associativity() . . . pass running ._test_an_element() . . . pass running ._test_associativity() . . . pass running ._test_category() . . . pass running ._test_distributivity() . . . pass running ._test_elements() . . . Running the test suite of self.an_element() running ._test_category() . . . pass running ._test_eq() . . . pass running ._test_factorisation() . . . pass running ._test_not_implemented_methods() . . . pass running ._test_pickling() . . . pass pass running ._test_elements_eq() . . . pass running ._test_eq() . . . pass running ._test_not_implemented_methods() . . . pass running ._test_one() . . . pass running ._test_pickling() . . . pass running ._test_prod() . . . pass running ._test_some_elements() . . . pass running ._test_zero() . . . pass