diff --git a/doc/src/modules/categories.txt b/doc/src/modules/categories.txt new file mode 100644 index 000000000000..61d7445a050f --- /dev/null +++ b/doc/src/modules/categories.txt @@ -0,0 +1,46 @@ +Category Theory Module +====================== + +.. module:: sympy.categories + +Introduction +------------ + +The category theory module for SymPy will allow manipulating diagrams +within a single category, including drawing them in TikZ and deciding +whether they are commutative or not. + +The general reference work this module tries to follow is + + [JoyOfCats] J. Adamek, H. Herrlich. G. E. Strecker: Abstract and + Concrete Categories. The Joy of Cats. + +The latest version of this book should be available for free download +from + + katmat.math.uni-bremen.de/acc/acc.pdf + +The module is still in its pre-embryonic stage. + +Base Class Reference +-------------------- +.. autoclass:: Object + :members: + +.. autoclass:: Morphism + :members: + +.. autoclass:: NamedMorphism + :members: + +.. autoclass:: CompositeMorphism + :members: + +.. autoclass:: IdentityMorphism + :members: + +.. autoclass:: Category + :members: + +.. autoclass:: Diagram + :members: diff --git a/doc/src/modules/index.txt b/doc/src/modules/index.txt index 93d71fccc675..1f54ccab9d97 100644 --- a/doc/src/modules/index.txt +++ b/doc/src/modules/index.txt @@ -47,6 +47,7 @@ access any SymPy module, or use this contens: utilities/index.txt parsing.txt physics/index.txt + categories.txt Contributions to docs --------------------- diff --git a/sympy/categories/__init__.py b/sympy/categories/__init__.py new file mode 100644 index 000000000000..d3ef52468477 --- /dev/null +++ b/sympy/categories/__init__.py @@ -0,0 +1,22 @@ +""" +Category Theory module. + +Provides some of the fundamental category-theory-related classes, +including categories, morphisms, diagrams. Functors are not +implemented yet. + +The general reference work this module tries to follow is + + [JoyOfCats] J. Adamek, H. Herrlich. G. E. Strecker: Abstract and + Concrete Categories. The Joy of Cats. + +The latest version of this book should be available for free download +from + + katmat.math.uni-bremen.de/acc/acc.pdf + +""" + +from baseclasses import (Object, Morphism, IdentityMorphism, + NamedMorphism, CompositeMorphism, Category, + Diagram) diff --git a/sympy/categories/baseclasses.py b/sympy/categories/baseclasses.py new file mode 100644 index 000000000000..cd29ab91aff8 --- /dev/null +++ b/sympy/categories/baseclasses.py @@ -0,0 +1,815 @@ +from sympy.core import (Set, Basic, FiniteSet, EmptySet, Dict, Symbol, + Tuple) + +class Class(Set): + r""" + The base class for any kind of class in the set-theoretic sense. + + In axiomatic set theories, everything is a class. A class which + can be a member of another class is a set. A class which is not a + member of another class is a proper class. The class `\{1, 2\}` + is a set; the class of all sets is a proper class. + + This class is essentially a synonym for :class:`sympy.core.Set`. + The goal of this class is to assure easier migration to the + eventual proper implementation of set theory. + """ + is_proper = False + +class Object(Symbol): + """ + The base class for any kind of object in an abstract category. + + While technically any instance of :class:`Basic` will do, this + class is the recommended way to create abstract objects in + abstract categories. + """ + +class Morphism(Basic): + """ + The base class for any morphism in an abstract category. + + In abstract categories, a morphism is an arrow between two + category objects. The object where the arrow starts is called the + domain, while the object where the arrow ends is called the + codomain. + + Two morphisms between the same pair of objects are considered to + be the same morphisms. To distinguish between morphisms between + the same objects use :class:`NamedMorphism`. + + It is prohibited to instantiate this class. Use one of the + derived classes instead. + + See Also + ======== + + IdentityMorphism, NamedMorphism, CompositeMorphism + """ + def __new__(cls, domain, codomain): + raise(NotImplementedError( + "Cannot instantiate Morphism. Use derived classes instead.")) + + @property + def domain(self): + """ + Returns the domain of the morphism. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> f = NamedMorphism(A, B, "f") + >>> f.domain + Object("A") + + """ + return self.args[0] + + @property + def codomain(self): + """ + Returns the codomain of the morphism. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> f = NamedMorphism(A, B, "f") + >>> f.codomain + Object("B") + + """ + return self.args[1] + + def compose(self, other): + r""" + Composes self with the supplied morphism. + + The order of elements in the composition is the usual order, + i.e., to construct `g\circ f` use ``g.compose(f)``. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> g * f + CompositeMorphism((NamedMorphism(Object("A"), Object("B"), "f"), + NamedMorphism(Object("B"), Object("C"), "g"))) + >>> (g * f).domain + Object("A") + >>> (g * f).codomain + Object("C") + + """ + return CompositeMorphism(other, self) + + def __mul__(self, other): + r""" + Composes self with the supplied morphism. + + The semantics of this operation is given by the following + equation: ``g * f == g.compose(f)`` for composable morphisms + ``g`` and ``f``. + + See Also + ======== + + compose + """ + return self.compose(other) + +class IdentityMorphism(Morphism): + """ + Represents an identity morphism. + + An identity morphism is a morphism with equal domain and codomain, + which acts as an identity with respect to composition. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism, IdentityMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> f = NamedMorphism(A, B, "f") + >>> id_A = IdentityMorphism(A) + >>> id_B = IdentityMorphism(B) + >>> f * id_A == f + True + >>> id_B * f == f + True + + See Also + ======== + + Morphism + """ + def __new__(cls, domain): + return Basic.__new__(cls, domain, domain) + +class NamedMorphism(Morphism): + """ + Represents a morphism which has a name. + + Names are used to distinguish between morphisms which have the + same domain and codomain: two named morphisms are equal if they + have the same domains, codomains, and names. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> f = NamedMorphism(A, B, "f") + >>> f + NamedMorphism(Object("A"), Object("B"), "f") + >>> f.name + 'f' + + See Also + ======== + + Morphism + """ + def __new__(cls, domain, codomain, name): + if not name: + raise ValueError("Empty morphism names not allowed.") + + return Basic.__new__(cls, domain, codomain, Symbol(name)) + + @property + def name(self): + """ + Returns the name of the morphism. + + Examples + ======== + >>> from sympy.categories import Object, NamedMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> f = NamedMorphism(A, B, "f") + >>> f.name + 'f' + + """ + return self.args[2].name + +class CompositeMorphism(Morphism): + r""" + Represents a morphism which is a composition of other morphisms. + + Two composite morphisms are equal if the morphisms they were + obtained from (components) are the same and were listed in the + same order. + + The arguments to the constructor for this class should be listed + in diagram order: to obtain the composition `g\circ f` from the + instances of :class:`Morphism` ``g`` and ``f`` use + ``CompositeMorphism(f, g)``. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism, CompositeMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> g * f + CompositeMorphism((NamedMorphism(Object("A"), Object("B"), "f"), + NamedMorphism(Object("B"), Object("C"), "g"))) + >>> CompositeMorphism(f, g) == g * f + True + + """ + @staticmethod + def _add_morphism(t, morphism): + """ + Intelligently adds ``morphism`` to tuple ``t``. + + If ``morphism`` is a composite morphism, its components are + added to the tuple. If ``morphism`` is an identity, nothing + is added to the tuple. + + No composability checks are performed. + """ + if isinstance(morphism, CompositeMorphism): + # ``morphism`` is a composite morphism; we have to + # denest its components. + return t + morphism.components + elif isinstance(morphism, IdentityMorphism): + # ``morphism`` is an identity. Nothing happens. + return t + else: + return t + Tuple(morphism) + + def __new__(cls, *components): + if components and not isinstance(components[0], Morphism): + # Maybe the user has explicitly supplied a list of + # morphisms. + return CompositeMorphism.__new__(cls, *components[0]) + + normalised_components = Tuple() + + # TODO: Fix the unpythonicity. + for i in xrange(len(components) - 1): + current = components[i] + following = components[i + 1] + + if not isinstance(current, Morphism) or \ + not isinstance(following, Morphism): + raise TypeError("All components must be morphisms.") + + if current.codomain != following.domain: + raise ValueError("Uncomposable morphisms.") + + normalised_components = CompositeMorphism._add_morphism( + normalised_components, current) + + # We haven't added the last morphism to the list of normalised + # components. Add it now. + normalised_components = CompositeMorphism._add_morphism( + normalised_components, components[-1]) + + if not normalised_components: + # If ``normalised_components`` is empty, only identities + # were supplied. Since they all were composable, they are + # all the same identities. + return components[0] + elif len(normalised_components) == 1: + # No sense to construct a whole CompositeMorphism. + return normalised_components[0] + + return Basic.__new__(cls, normalised_components) + + @property + def components(self): + """ + Returns the components of this composite morphism. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism, CompositeMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> (g * f).components + (NamedMorphism(Object("A"), Object("B"), "f"), + NamedMorphism(Object("B"), Object("C"), "g")) + + """ + return self.args[0] + + @property + def domain(self): + """ + Returns the domain of this composite morphism. + + The domain of the composite morphism is the domain of its + first component. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism, CompositeMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> (g * f).domain + Object("A") + + """ + return self.components[0].domain + + @property + def codomain(self): + """ + Returns the codomain of this composite morphism. + + The codomain of the composite morphism is the codomain of its + last component. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism, CompositeMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> (g * f).codomain + Object("C") + + """ + return self.components[-1].codomain + + def flatten(self, new_name): + """ + Forgets the composite structure of this morphism. + + If ``new_name`` is not empty, returns a :class:`NamedMorphism` + with the supplied name, otherwise returns a :class:`Morphism`. + In both cases the domain of the new morphism is the domain of + this composite morphism and the codomain of the new morphism + is the codomain of this composite morphism. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism, CompositeMorphism + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> (g * f).flatten("h") + NamedMorphism(Object("A"), Object("C"), "h") + + """ + return NamedMorphism(self.domain, self.codomain, new_name) + +class Category(Basic): + r""" + An (abstract) category. + + A category [JoyOfCats] is a quadruple `\mbox{K} = (O, \hom, id, + \circ)` consisting of + + * a (set-theoretical) class `O`, whose members are called + `K`-objects, + + * for each pair `(A, B)` of `K`-objects, a set `\hom(A, B)` whose + members are called `K`-morphisms from `A` to `B`, + + * for a each `K`-object `A`, a morphism `id:A\rightarrow A`, + called the `K`-identity of `A`, + + * a composition law `\circ` associating with every `K`-morphisms + `f:A\rightarrow B` and `g:B\rightarrow C` a `K`-morphism `g\circ + f:A\rightarrow C`, called the composite of `f` and `g`. + + Composition is associative, `K`-identities are identities with + respect to composition, and the sets `\hom(A, B)` are pairwise + disjoint. + + This class knows nothing about its objects and morphisms. + Concrete cases of (abstract) categories should be implemented as + classes derived from this one. + + Certain instances of :class:`Diagram` can be asserted to be + commutative in a :class:`Category` by supplying the argument + ``commutative_diagrams`` in the constructor. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism, Diagram, Category + >>> from sympy import FiniteSet + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> d = Diagram([f, g]) + >>> K = Category("K", commutative_diagrams=[d]) + >>> K.commutative_diagrams == FiniteSet(d) + True + + See Also + ======== + Diagram + """ + def __new__(cls, name, objects=EmptySet(), commutative_diagrams=EmptySet()): + if not name: + raise ValueError("A Category cannot have an empty name.") + + new_category = Basic.__new__(cls, Symbol(name), Class(objects), + FiniteSet(commutative_diagrams)) + return new_category + + @property + def name(self): + """ + Returns the name of this category. + + Examples + ======== + + >>> from sympy.categories import Category + >>> K = Category("K") + >>> K.name + 'K' + + """ + return self.args[0].name + + @property + def objects(self): + """ + Returns the class of objects of this category. + + Examples + ======== + + >>> from sympy.categories import Object, Category + >>> from sympy import FiniteSet + >>> A = Object("A") + >>> B = Object("B") + >>> K = Category("K", FiniteSet(A, B)) + >>> K.objects + Class({Object("B"), Object("A")}) + + """ + return self.args[1] + + @property + def commutative_diagrams(self): + """ + Returns the :class:`FiniteSet` of diagrams which are known to + be commutative in this category. + + >>> from sympy.categories import Object, NamedMorphism, Diagram, Category + >>> from sympy import FiniteSet + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> d = Diagram([f, g]) + >>> K = Category("K", commutative_diagrams=[d]) + >>> K.commutative_diagrams == FiniteSet(d) + True + + """ + return self.args[2] + + def hom(self, A, B): + raise NotImplementedError( + "hom-sets are not implemented in Category.") + + def all_morphisms(self): + raise NotImplementedError( + "Obtaining the class of morphisms is not implemented in Category.") + +class Diagram(Basic): + r""" + Represents a diagram in a certain category. + + Informally, a diagram is a collection of objects of a category and + certain morphisms between them. A diagram is still a monoid with + respect to morphism composition; i.e., identity morphisms, as well + as all composites of morphisms included in the diagram belong to + the diagram. For a more formal approach to this notion see + [Pare1970]. + + A commutative diagram is often accompanied by a statement of the + following kind: "if such morphisms with such properties exist, + then such morphisms which such properties exist and the diagram is + commutative". To represent this, an instance of :class:`Diagram` + includes a collection of morphisms which are the premises and + another collection of conclusions. ``premises`` and + ``conclusions`` associate morphisms belonging to the corresponding + categories with the :class:`FiniteSet`'s of their properties. + + The set of properties of a composite morphism is the intersection + of the sets of properties of its components. The domain and + codomain of a conclusion morphism should be among the domains and + codomains of the morphisms listed as the premises of a diagram. + + No checks are carried out of whether the supplied object and + morphisms do belong to one and the same category. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism, Diagram + >>> from sympy import FiniteSet, pprint + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> d = Diagram([f, g]) + >>> pprint(d.premises.keys(), use_unicode=False) + [f:A-->B, id:B-->B, g*f:A-->C, g:B-->C, id:C-->C, id:A-->A] + >>> pprint(d.premises, use_unicode=False) + {g*f:A-->C: EmptySet(), id:A-->A: EmptySet(), id:B-->B: EmptySet(), id:C-->C: + EmptySet(), f:A-->B: EmptySet(), g:B-->C: EmptySet()} + >>> d = Diagram([f, g], {g * f:"unique"}) + >>> pprint(d.conclusions) + {g*f:A-->C: {unique}} + + References + ========== + [Pare1970] B. Pareigis: Categories and functors. Academic Press, + 1970. + """ + @staticmethod + def _set_dict_union(dictionary, key, value): + """ + If ``key`` is in ``dictionary``, set the new value of ``key`` + to be the union between the old value and ``value``. + Otherwise, set the value of ``key`` to ``value. + + Returns ``True`` if the key already was in the dictionary and + ``False`` otherwise. + """ + if key in dictionary: + dictionary[key] = dictionary[key] | value + return True + else: + dictionary[key] = value + return False + + @staticmethod + def _add_morphism(morphisms, morphism, props, add_identities=True): + """ + Adds a morphism and its attributes to the supplied dictionary + ``morphisms``. If ``add_identities`` is True, also adds the + identity morphisms for the domain and the codomain of + ``morphism``. + """ + if not Diagram._set_dict_union(morphisms, morphism, props): + # We have just added a new morphism. + + if isinstance(morphism, IdentityMorphism): + return + + if add_identities: + empty = EmptySet() + + id_dom = IdentityMorphism(morphism.domain) + id_cod = IdentityMorphism(morphism.codomain) + + Diagram._set_dict_union(morphisms, id_dom, empty) + Diagram._set_dict_union(morphisms, id_cod, empty) + + for existing_morphism, existing_props in morphisms.items(): + new_props = existing_props & props + if morphism.domain == existing_morphism.codomain: + left = morphism * existing_morphism + Diagram._set_dict_union(morphisms, left, new_props) + if morphism.codomain == existing_morphism.domain: + right = existing_morphism * morphism + Diagram._set_dict_union(morphisms, right, new_props) + + def __new__(cls, *args): + """ + Construct a new instance of Diagram. + + If no arguments are supplied, an empty diagram is created. + + If at least an argument is supplied, ``args[0]`` is + interpreted as the premises of the diagram. If ``args[0]`` is + a list, it is interpreted as a list of :class:`Morphism`'s, in + which each :class:`Morphism` has an empty set of properties. + If ``args[0]`` is a Python dictionary or a :class:`Dict`, it + is interpreted as a dictionary associating to some + :class:`Morphism`'s some properties. + + If at least two arguments are supplied ``args[1]`` is + interpreted as the conclusions of the diagram. The type of + ``args[1]`` is interpreted in exactly the same way as the type + of ``args[0]``. If only one argument is supplied, the diagram + has no conclusions. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism + >>> from sympy.categories import IdentityMorphism, Diagram + >>> from sympy import FiniteSet + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> d = Diagram([f, g]) + >>> IdentityMorphism(A) in d.premises.keys() + True + >>> g * f in d.premises.keys() + True + >>> d = Diagram([f, g], {g * f:"unique"}) + >>> d.conclusions[g * f] + {unique} + + """ + premises = {} + conclusions = {} + + # Here we will keep track of the objects which appear in the + # premises. + objects = EmptySet() + + if len(args) >= 1: + # We've got some premises in the arguments. + premises_arg = args[0] + + if isinstance(premises_arg, list): + # The user has supplied a list of morphisms, none of + # which have any attributes. + empty = EmptySet() + + for morphism in premises_arg: + objects |= FiniteSet(morphism.domain, morphism.codomain) + Diagram._add_morphism(premises, morphism, empty) + elif isinstance(premises_arg, dict) or isinstance(premises_arg, Dict): + # The user has supplied a dictionary of morphisms and + # their properties. + for morphism, props in premises_arg.items(): + objects |= FiniteSet(morphism.domain, morphism.codomain) + Diagram._add_morphism(premises, morphism, FiniteSet(props)) + + if len(args) >= 2: + # We also have some conclusions. + conclusions_arg = args[1] + + if isinstance(conclusions_arg, list): + # The user has supplied a list of morphisms, none of + # which have any attributes. + empty = EmptySet() + + for morphism in conclusions_arg: + # Check that no new objects appear in conclusions. + if (morphism.domain in objects) and \ + (morphism.codomain in objects): + # No need to add identities this time. + Diagram._add_morphism(conclusions, morphism, empty, False) + elif isinstance(conclusions_arg, dict) or \ + isinstance(conclusions_arg, Dict): + # The user has supplied a dictionary of morphisms and + # their properties. + for morphism, props in conclusions_arg.items(): + # Check that no new objects appear in conclusions. + if (morphism.domain in objects) and \ + (morphism.codomain in objects): + # No need to add identities this time. + Diagram._add_morphism(conclusions, morphism, + FiniteSet(props), False) + + return Basic.__new__(cls, Dict(premises), Dict(conclusions), objects) + + @property + def premises(self): + """ + Returns the premises of this diagram. + + Examples + ======== + >>> from sympy.categories import Object, NamedMorphism + >>> from sympy.categories import IdentityMorphism, Diagram + >>> from sympy import EmptySet, Dict, pretty + >>> A = Object("A") + >>> B = Object("B") + >>> f = NamedMorphism(A, B, "f") + >>> id_A = IdentityMorphism(A) + >>> id_B = IdentityMorphism(B) + >>> d = Diagram([f]) + >>> print pretty(d.premises, use_unicode=False) + {id:A-->A: EmptySet(), id:B-->B: EmptySet(), f:A-->B: EmptySet()} + + """ + return self.args[0] + + @property + def conclusions(self): + """ + Returns the conclusions of this diagram. + + Examples + ======== + >>> from sympy.categories import Object, NamedMorphism + >>> from sympy.categories import IdentityMorphism, Diagram + >>> from sympy import FiniteSet + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> d = Diagram([f, g]) + >>> IdentityMorphism(A) in d.premises.keys() + True + >>> g * f in d.premises.keys() + True + >>> d = Diagram([f, g], {g * f:"unique"}) + >>> d.conclusions[g * f] == FiniteSet("unique") + True + + """ + return self.args[1] + + @property + def objects(self): + """ + Returns the :class:`FiniteSet` of objects that appear in this + diagram. + + Examples + ======== + >>> from sympy.categories import Object, NamedMorphism, Diagram + >>> from sympy import FiniteSet + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> d = Diagram([f, g]) + >>> d.objects + {Object("C"), Object("B"), Object("A")} + + """ + return self.args[2] + + def hom(self, A, B): + """ + Returns a 2-tuple of sets of morphisms between objects A and + B: one set of morphisms listed as premises, and the other set + of morphisms listed as conclusions. + + Examples + ======== + + >>> from sympy.categories import Object, NamedMorphism, Diagram + >>> from sympy import FiniteSet, pretty + >>> A = Object("A") + >>> B = Object("B") + >>> C = Object("C") + >>> f = NamedMorphism(A, B, "f") + >>> g = NamedMorphism(B, C, "g") + >>> d = Diagram([f, g], {g * f: "unique"}) + >>> print pretty(d.hom(A, C), use_unicode=False) + ({g*f:A-->C}, {g*f:A-->C}) + + See Also + ======== + Object, Morphism + """ + premises = EmptySet() + conclusions = EmptySet() + + for morphism in self.premises.keys(): + if (morphism.domain == A) and (morphism.codomain == B): + premises |= FiniteSet(morphism) + for morphism in self.conclusions.keys(): + if (morphism.domain == A) and (morphism.codomain == B): + conclusions |= FiniteSet(morphism) + + return (premises, conclusions) diff --git a/sympy/categories/tests/test_baseclasses.py b/sympy/categories/tests/test_baseclasses.py new file mode 100644 index 000000000000..9ef95bfc78d5 --- /dev/null +++ b/sympy/categories/tests/test_baseclasses.py @@ -0,0 +1,171 @@ +from sympy.categories import (Object, Morphism, IdentityMorphism, + NamedMorphism, CompositeMorphism, + Diagram, Category) +from sympy.categories.baseclasses import Class +from sympy.utilities.pytest import XFAIL, raises +from sympy import FiniteSet, EmptySet, Dict, Tuple + +def test_morphisms(): + A = Object("A") + B = Object("B") + C = Object("C") + D = Object("D") + + # Test the base morphism. + f = NamedMorphism(A, B, "f") + assert f.domain == A + assert f.codomain == B + assert f == NamedMorphism(A, B, "f") + + # Test identities. + id_A = IdentityMorphism(A) + id_B = IdentityMorphism(B) + assert id_A.domain == A + assert id_A.codomain == A + assert id_A == IdentityMorphism(A) + assert id_A != id_B + + # Test named morphisms. + g = NamedMorphism(B, C, "g") + assert g.name == "g" + assert g != f + assert g == NamedMorphism(B, C, "g") + assert g != NamedMorphism(B, C, "f") + + # Test composite morphisms. + assert f == CompositeMorphism(f) + + k = g.compose(f) + assert k.domain == A + assert k.codomain == C + assert k.components == Tuple(f, g) + assert g * f == k + assert CompositeMorphism(f, g) == k + + assert CompositeMorphism(g * f) == g * f + + # Test the associativity of composition. + h = NamedMorphism(C, D, "h") + + p = h * g + u = h * g * f + + assert h * k == u + assert p * f == u + assert CompositeMorphism(f, g, h) == u + + # Test flattening. + u2 = u.flatten("u") + assert isinstance(u2, NamedMorphism) + assert u2.name == "u" + assert u2.domain == A + assert u2.codomain == D + + # Test identities. + assert f * id_A == f + assert id_B * f == f + assert id_A * id_A == id_A + assert CompositeMorphism(id_A) == id_A + + # Test bad compositions. + raises(ValueError, lambda: f * g) + + raises(TypeError, lambda: f.compose(None)) + raises(TypeError, lambda: id_A.compose(None)) + raises(TypeError, lambda: f * None) + raises(TypeError, lambda: id_A * None) + + raises(TypeError, lambda: CompositeMorphism(f, None, 1)) + + raises(ValueError, lambda: NamedMorphism(A, B, "")) + raises(NotImplementedError, lambda: Morphism(A, B)) + +def test_diagram(): + A = Object("A") + B = Object("B") + C = Object("C") + + f = NamedMorphism(A, B, "f") + g = NamedMorphism(B, C, "g") + id_A = IdentityMorphism(A) + id_B = IdentityMorphism(B) + + empty = EmptySet() + + # Test the addition of identities. + d1 = Diagram([f]) + + assert d1.objects == FiniteSet(A, B) + assert d1.hom(A, B) == (FiniteSet(f), empty) + assert d1.hom(A, A) == (FiniteSet(id_A), empty) + assert d1.hom(B, B) == (FiniteSet(id_B), empty) + + assert d1 == Diagram([id_A, f]) + assert d1 == Diagram([f, f]) + + # Test the addition of composites. + d2 = Diagram([f, g]) + homAC = d2.hom(A, C)[0] + + assert d2.objects == FiniteSet(A, B, C) + assert g * f in d2.premises.keys() + assert homAC == FiniteSet(g * f) + + # Test equality, inequality and hash. + d11 = Diagram([f]) + + assert d1 == d11 + assert d1 != d2 + assert hash(d1) == hash(d11) + + d11 = Diagram({f:"unique"}) + assert d1 != d11 + + # Make sure that (re-)adding composites (with new properties) + # works as expected. + d = Diagram([f, g], {g * f:"unique"}) + assert d.conclusions[g * f] == FiniteSet("unique") + + # Check the hom-sets when there are premises and conclusions. + assert d.hom(A, C) == (FiniteSet(g * f), FiniteSet(g * f)) + d = Diagram([f, g], [g * f]) + assert d.hom(A, C) == (FiniteSet(g * f), FiniteSet(g * f)) + + # Check how the properties of composite morphisms are computed. + d = Diagram({f:["unique", "isomorphism"], g:"unique"}) + assert d.premises[g * f] == FiniteSet("unique") + + # Check that conclusion morphisms with new objects are not allowed. + d = Diagram([f], [g]) + assert d.conclusions == Dict({}) + + # Test an empty diagram. + d = Diagram() + assert d.premises == Dict({}) + assert d.conclusions == Dict({}) + assert d.objects == empty + + # Check a SymPy Dict object. + d = Diagram(Dict({f:FiniteSet("unique", "isomorphism"), g:"unique"})) + assert d.premises[g * f] == FiniteSet("unique") + +def test_category(): + A = Object("A") + B = Object("B") + C = Object("C") + + f = NamedMorphism(A, B, "f") + g = NamedMorphism(B, C, "g") + + d1 = Diagram([f, g]) + d2 = Diagram([f]) + + objects = d1.objects | d2.objects + + K = Category("K", objects, commutative_diagrams=[d1, d2]) + + assert K.name == "K" + assert K.objects == Class(objects) + assert K.commutative_diagrams == FiniteSet(d1, d2) + + raises(ValueError, lambda: Category("")) diff --git a/sympy/core/tests/test_args.py b/sympy/core/tests/test_args.py index dec126030ed0..b551dd0a9a5f 100644 --- a/sympy/core/tests/test_args.py +++ b/sympy/core/tests/test_args.py @@ -1937,3 +1937,54 @@ def test_sympy__differential_geometry__differential_geometry__VectorField(): from sympy.differential_geometry import Manifold, Patch, CoordSystem, VectorField cs = CoordSystem('name', Patch('name', Manifold('name', 3))) assert _test_args(VectorField(cs, [x, y], [x, y])) + +def test_sympy__categories__baseclasses__Class(): + from sympy.categories.baseclasses import Class + assert _test_args(Class()) + +def test_sympy__categories__baseclasses__Object(): + from sympy.categories import Object + assert _test_args(Object("A")) + +@XFAIL +def test_sympy__categories__baseclasses__Morphism(): + from sympy.categories import Object, Morphism + assert _test_args(Morphism(Object("A"), Object("B"))) + +def test_sympy__categories__baseclasses__IdentityMorphism(): + from sympy.categories import Object, IdentityMorphism + assert _test_args(IdentityMorphism(Object("A"))) + +def test_sympy__categories__baseclasses__NamedMorphism(): + from sympy.categories import Object, NamedMorphism + assert _test_args(NamedMorphism(Object("A"), Object("B"), "f")) + +def test_sympy__categories__baseclasses__CompositeMorphism(): + from sympy.categories import Object, NamedMorphism, CompositeMorphism + A = Object("A") + B = Object("B") + C = Object("C") + f = NamedMorphism(A, B, "f") + g = NamedMorphism(B, C, "g") + assert _test_args(CompositeMorphism(f, g)) + +def test_sympy__categories__baseclasses__Diagram(): + from sympy.categories import Object, NamedMorphism, Diagram, Category + A = Object("A") + B = Object("B") + C = Object("C") + f = NamedMorphism(A, B, "f") + d = Diagram([f]) + assert _test_args(d) + +def test_sympy__categories__baseclasses__Category(): + from sympy.categories import Object, NamedMorphism, Diagram, Category + A = Object("A") + B = Object("B") + C = Object("C") + f = NamedMorphism(A, B, "f") + g = NamedMorphism(B, C, "g") + d1 = Diagram([f, g]) + d2 = Diagram([f]) + K = Category("K", commutative_diagrams=[d1, d2]) + assert _test_args(K) diff --git a/sympy/printing/latex.py b/sympy/printing/latex.py index 8ff628d64459..069dad0543c4 100644 --- a/sympy/printing/latex.py +++ b/sympy/printing/latex.py @@ -2,7 +2,7 @@ A Printer which converts an expression into its LaTeX equivalent. """ -from sympy.core import S, C, Add +from sympy.core import S, C, Add, Symbol from sympy.core.function import _coeff_isneg from printer import Printer from conventions import split_super_sub @@ -1181,6 +1181,51 @@ def _print_DMP(self, p): def _print_DMF(self, p): return self._print_DMP(p) + def _print_Object(self, object): + return self._print(Symbol(object.name)) + + def _print_Morphism(self, morphism): + domain = self._print(morphism.domain) + codomain = self._print(morphism.codomain) + return "%s\\rightarrow %s" % (domain, codomain) + + def _print_NamedMorphism(self, morphism): + pretty_name = self._print(Symbol(morphism.name)) + pretty_morphism = self._print_Morphism(morphism) + return "%s:%s" % (pretty_name, pretty_morphism) + + def _print_IdentityMorphism(self, morphism): + from sympy.categories import NamedMorphism + return self._print_NamedMorphism(NamedMorphism( + morphism.domain, morphism.codomain, "id")) + + def _print_CompositeMorphism(self, morphism): + from sympy.categories import NamedMorphism + + # All components of the morphism have names and it is thus + # possible to build the name of the composite. + component_names_list = [self._print(Symbol(component.name)) for \ + component in morphism.components] + component_names_list.reverse() + component_names = "\\circ ".join(component_names_list) + ":" + + pretty_morphism = self._print_Morphism(morphism) + return component_names + pretty_morphism + + def _print_Category(self, morphism): + return "\\mathbf{%s}" % self._print(Symbol(morphism.name)) + + def _print_Diagram(self, diagram): + if not diagram.premises: + # This is an empty diagram. + return self._print(S.EmptySet) + + latex_result = self._print(diagram.premises) + if diagram.conclusions: + latex_result += "\\Longrightarrow %s" % \ + self._print(diagram.conclusions) + + return latex_result def latex(expr, **settings): r""" diff --git a/sympy/printing/pretty/pretty.py b/sympy/printing/pretty/pretty.py index f1d324aaa836..8c9a3735d21e 100644 --- a/sympy/printing/pretty/pretty.py +++ b/sympy/printing/pretty/pretty.py @@ -1446,6 +1446,67 @@ def _print_DMP(self, p): def _print_DMF(self, p): return self._print_DMP(p) + def _print_Object(self, object): + return self._print(pretty_symbol(object.name)) + + def _print_Morphism(self, morphism): + arrow = "-->" + if self._use_unicode: + arrow = u"\u27f6 " + + domain = self._print(morphism.domain) + codomain = self._print(morphism.codomain) + tail = domain.right(arrow, codomain)[0] + + return prettyForm(tail) + + def _print_NamedMorphism(self, morphism): + pretty_name = self._print(pretty_symbol(morphism.name)) + pretty_morphism = self._print_Morphism(morphism) + return prettyForm(pretty_name.right(":", pretty_morphism)[0]) + + def _print_IdentityMorphism(self, morphism): + from sympy.categories import NamedMorphism + return self._print_NamedMorphism( + NamedMorphism(morphism.domain, morphism.codomain, "id")) + + def _print_CompositeMorphism(self, morphism): + from sympy.categories import NamedMorphism + + circle = "*" + if self._use_unicode: + circle = u"\u2218" + + # All components of the morphism have names and it is thus + # possible to build the name of the composite. + component_names_list = [pretty_symbol(component.name) for \ + component in morphism.components] + component_names_list.reverse() + component_names = circle.join(component_names_list) + ":" + + pretty_name = self._print(component_names) + pretty_morphism = self._print_Morphism(morphism) + return prettyForm(pretty_name.right(pretty_morphism)[0]) + + def _print_Category(self, category): + return self._print(pretty_symbol(category.name)) + + def _print_Diagram(self, diagram): + if not diagram.premises: + # This is an empty diagram. + return self._print(S.EmptySet) + + pretty_result = self._print(diagram.premises) + if diagram.conclusions: + results_arrow = " ==> " + if self._use_unicode: + results_arrow = u" \u27f9 " + + pretty_conclusions = self._print(diagram.conclusions)[0] + pretty_result = pretty_result.right(results_arrow, pretty_conclusions) + + return prettyForm(pretty_result[0]) + def pretty(expr, **settings): """Returns a string containing the prettified form of expr. diff --git a/sympy/printing/pretty/tests/test_pretty.py b/sympy/printing/pretty/tests/test_pretty.py index fa738da9b254..137cef293c27 100644 --- a/sympy/printing/pretty/tests/test_pretty.py +++ b/sympy/printing/pretty/tests/test_pretty.py @@ -3691,3 +3691,54 @@ def test_issue_3186(): def test_complicated_symbol_unchanged(): for symb_name in ["dexpr2_d1tau", "dexpr2^d1tau"]: assert pretty(Symbol(symb_name)) == symb_name + +def test_categories(): + from sympy.categories import (Object, Morphism, IdentityMorphism, + NamedMorphism, CompositeMorphism, + Category, Diagram) + + A1 = Object("A1") + A2 = Object("A2") + A3 = Object("A3") + + f1 = NamedMorphism(A1, A2, "f1") + f2 = NamedMorphism(A2, A3, "f2") + id_A1 = IdentityMorphism(A1) + + K1 = Category("K1") + + assert pretty(A1) == "A1" + assert upretty(A1) == u"A₁" + + assert pretty(f1) == "f1:A1-->A2" + assert upretty(f1) == u"f₁:A₁⟶ A₂" + assert pretty(id_A1) == "id:A1-->A1" + assert upretty(id_A1) == u"id:A₁⟶ A₁" + + assert pretty(f2*f1) == "f2*f1:A1-->A3" + assert upretty(f2*f1) == u"f₂∘f₁:A₁⟶ A₃" + + assert pretty(K1) == "K1" + assert upretty(K1) == u"K₁" + + # Test how diagrams are printed. + d = Diagram() + assert pretty(d) == "EmptySet()" + assert upretty(d) == u"∅" + + d = Diagram({f1:"unique", f2:S.EmptySet}) + assert pretty(d) == "{f2*f1:A1-->A3: EmptySet(), id:A1-->A1: " \ + "EmptySet(), id:A2-->A2: EmptySet(), id:A3-->A3: " \ + "EmptySet(), f1:A1-->A2: {unique}, f2:A2-->A3: EmptySet()}" + + assert upretty(d) == u"{f₂∘f₁:A₁⟶ A₃: ∅, id:A₁⟶ A₁: ∅, " \ + u"id:A₂⟶ A₂: ∅, id:A₃⟶ A₃: ∅, f₁:A₁⟶ A₂: {unique}, f₂:A₂⟶ A₃: ∅}" + + d = Diagram({f1:"unique", f2:S.EmptySet}, {f2 * f1: "unique"}) + assert pretty(d) == "{f2*f1:A1-->A3: EmptySet(), id:A1-->A1: " \ + "EmptySet(), id:A2-->A2: EmptySet(), id:A3-->A3: " \ + "EmptySet(), f1:A1-->A2: {unique}, f2:A2-->A3: EmptySet()}" \ + " ==> {f2*f1:A1-->A3: {unique}}" + assert upretty(d) == u"{f₂∘f₁:A₁⟶ A₃: ∅, id:A₁⟶ A₁: ∅, id:A₂⟶ A₂: " \ + u"∅, id:A₃⟶ A₃: ∅, f₁:A₁⟶ A₂: {unique}, f₂:A₂⟶ A₃: ∅}" \ + u" ⟹ {f₂∘f₁:A₁⟶ A₃: {unique}}" diff --git a/sympy/printing/str.py b/sympy/printing/str.py index f17c180b92e0..974a48caf669 100644 --- a/sympy/printing/str.py +++ b/sympy/printing/str.py @@ -526,6 +526,18 @@ def _print_DMP(self, p): def _print_DMF(self, expr): return self._print_DMP(expr) + def _print_Object(self, object): + return 'Object("%s")' % object.name + + def _print_IdentityMorphism(self, morphism): + return 'IdentityMorphism(%s)' % morphism.domain + + def _print_NamedMorphism(self, morphism): + return 'NamedMorphism(%s, %s, "%s")' % \ + (morphism.domain, morphism.codomain, morphism.name) + + def _print_Category(self, category): + return 'Category("%s")' % category.name def sstr(expr, **settings): diff --git a/sympy/printing/tests/test_latex.py b/sympy/printing/tests/test_latex.py index b9f7e4bbbf6e..77e1f6a7a9f2 100644 --- a/sympy/printing/tests/test_latex.py +++ b/sympy/printing/tests/test_latex.py @@ -569,3 +569,46 @@ def test_PolynomialRing(): assert latex(QQ[x, y]) == r"\mathbb{Q}\left[x, y\right]" assert latex(QQ.poly_ring(x, y, order="ilex")) == \ r"S_<^{-1}\mathbb{Q}\left[x, y\right]" + +def test_categories(): + from sympy.categories import (Object, Morphism, IdentityMorphism, + NamedMorphism, CompositeMorphism, + Category, Diagram) + + A1 = Object("A1") + A2 = Object("A2") + A3 = Object("A3") + + f1 = NamedMorphism(A1, A2, "f1") + f2 = NamedMorphism(A2, A3, "f2") + id_A1 = IdentityMorphism(A1) + + K1 = Category("K1") + + assert latex(A1) == "A_{1}" + assert latex(f1) == "f_{1}:A_{1}\\rightarrow A_{2}" + assert latex(id_A1) == "id:A_{1}\\rightarrow A_{1}" + assert latex(f2*f1) == "f_{2}\\circ f_{1}:A_{1}\\rightarrow A_{3}" + + assert latex(K1) == "\mathbf{K_{1}}" + + d = Diagram() + assert latex(d) == "\emptyset" + + d = Diagram({f1:"unique", f2:S.EmptySet}) + assert latex(d) == "\\begin{Bmatrix}f_{2}\\circ f_{1}:A_{1}" \ + "\\rightarrow A_{3} : \\emptyset, & id:A_{1}\\rightarrow " \ + "A_{1} : \\emptyset, & id:A_{2}\\rightarrow A_{2} : " \ + "\\emptyset, & id:A_{3}\\rightarrow A_{3} : \\emptyset, " \ + "& f_{1}:A_{1}\\rightarrow A_{2} : \\left\\{unique\\right\\}, " \ + "& f_{2}:A_{2}\\rightarrow A_{3} : \\emptyset\\end{Bmatrix}" + + d = Diagram({f1:"unique", f2:S.EmptySet}, {f2 * f1: "unique"}) + assert latex(d) == "\\begin{Bmatrix}f_{2}\\circ f_{1}:A_{1}" \ + "\\rightarrow A_{3} : \\emptyset, & id:A_{1}\\rightarrow " \ + "A_{1} : \\emptyset, & id:A_{2}\\rightarrow A_{2} : " \ + "\\emptyset, & id:A_{3}\\rightarrow A_{3} : \\emptyset, " \ + "& f_{1}:A_{1}\\rightarrow A_{2} : \\left\\{unique\\right\\}," \ + " & f_{2}:A_{2}\\rightarrow A_{3} : \\emptyset\\end{Bmatrix}" \ + "\\Longrightarrow \\begin{Bmatrix}f_{2}\\circ f_{1}:A_{1}" \ + "\\rightarrow A_{3} : \\left\\{unique\\right\\}\\end{Bmatrix}" diff --git a/sympy/printing/tests/test_str.py b/sympy/printing/tests/test_str.py index 2176e3d20394..814451673340 100644 --- a/sympy/printing/tests/test_str.py +++ b/sympy/printing/tests/test_str.py @@ -471,3 +471,21 @@ def test_PrettyPoly(): R = QQ[x, y] assert sstr(F.convert(x/(x + y))) == sstr(x/(x + y)) assert sstr(R.convert(x + y)) == sstr(x + y) + +def test_categories(): + from sympy.categories import (Object, Morphism, NamedMorphism, + IdentityMorphism, Category) + + A = Object("A") + B = Object("B") + + f = NamedMorphism(A, B, "f") + id_A = IdentityMorphism(A) + + K = Category("K") + + assert str(A) == 'Object("A")' + assert str(f) == 'NamedMorphism(Object("A"), Object("B"), "f")' + assert str(id_A) == 'IdentityMorphism(Object("A"))' + + assert str(K) == 'Category("K")'