Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge pull request #1338 from scolobb/ct0-base-classes

Implement CT Base Classes
  • Loading branch information...
commit d1ff27566192a57948784dcd6934335d22073fdd 2 parents 1052b45 + 9e0f2a1
@ness01 ness01 authored
View
46 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:
View
1  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
---------------------
View
22 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)
View
815 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)
View
171 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(""))
View
51 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)
View
47 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"""
View
61 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.
View
51 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}}"
View
12 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):
View
43 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}"
View
18 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")'
Please sign in to comment.
Something went wrong with that request. Please try again.