Source code for discopy.para

# -*- coding: utf-8 -*-

"""
The category of parametric maps over a symmetric underlying `category`.

A parametric map from `x` to `y` with parameter space `p` is a morphism
`x @ p -> y` in the underlying category. Composition tensors the parameters
and tensor routes them to the right with a swap. Parametric maps first
appeared in the study of supervised learning :cite:p:`FongEtAl19`.

Summary
-------

.. autosummary::
    :template: class.rst
    :nosignatures:
    :toctree:

    Symmetric
    Traced
    Markov
    Closed
    Feedback
    Compact
    Hypergraph

Axioms
------

Composition tensors the parameter spaces:

>>> from discopy.symmetric import Ty, Box, Diagram
>>> x, y, z, w, p, q = map(Ty, "xyzwpq")
>>> f = Symmetric(x, y, Box('f', x @ p, y), p)
>>> g = Symmetric(y, z, Box('g', y @ q, z), q)
>>> assert (f >> g).param == p @ q
>>> assert (f >> g).inside == f.inside @ q >> g.inside
>>> (f >> g).inside.draw(doctest="docs/_static/para/then.svg")

.. image:: /_static/para/then.svg
    :align: center

So does the tensor, with a swap routing the parameters to the right:

>>> h = Symmetric(z, w, Box('h', z @ q, w), q)
>>> assert (f @ h).param == p @ q
>>> assert (f @ h).inside\\
...     == x @ Diagram.swap(z, p) @ q >> f.inside @ h.inside
>>> (f @ h).inside.draw(doctest="docs/_static/para/tensor.svg")

.. image:: /_static/para/tensor.svg
    :align: center

Reparametrisation precomposes the parameters, contravariantly:

>>> p_, p__ = Ty("p'"), Ty("p''")
>>> r, s = Box('r', p_, p), Box('s', p__, p_)
>>> assert f.reparam(r).param == p_
>>> assert f.reparam(s >> r) == f.reparam(r).reparam(s)

The identity and swap of :class:`Symmetric` are those of the underlying
category, with the empty parameter space:

>>> assert Symmetric.id(x) == Symmetric.lift(Diagram.id(x))
>>> assert Symmetric.swap(x, y) == Symmetric.lift(Diagram.swap(x, y))
>>> t = Traced(x @ y, z @ y, Box('t', x @ y @ p, z @ y), p)
>>> assert t.trace().dom == x and t.trace().param == p

The construction preserves each level of the hierarchy below symmetric:
:class:`Traced`, :class:`Markov`, :class:`Closed`, :class:`Feedback`,
:class:`Compact` and :class:`Hypergraph` lift the extra structure of their
underlying category with the empty parameter space, e.g.

>>> from discopy import frobenius
>>> X = frobenius.Ty('x')
>>> assert Hypergraph.spiders(1, 2, X)\\
...     == Hypergraph.lift(frobenius.Diagram.spiders(1, 2, X))

while the operations on morphisms swap the parameters out of the way,
the same as :meth:`Traced.trace`:

>>> from discopy import closed
>>> a, b, c, P = map(closed.Ty, "abcP")
>>> k = Closed(a @ b, c, closed.Box('k', a @ b @ P, c), P)
>>> assert k.curry(left=True).cod == c << b
>>> assert k.curry(left=False).cod == a >> c

A map may also carry a coparameter space `copar` on the codomain, i.e.
`inside : dom @ param -> cod @ copar`, empty by default — the type of one
time step of a stateful morphism sequence :cite:p:`DiLavoreEtAl22`, i.e. of
a :class:`Stream <discopy.stream.Stream>` with the delay forgotten.
Composition and tensor accumulate the hidden objects on both sides:

>>> m, n = Ty('m'), Ty('n')
>>> t = Symmetric(x, y, Box('t', x @ p, y @ m), p, m)
>>> u = Symmetric(y, z, Box('u', y @ q, z @ n), q, n)
>>> assert (t >> u).param == p @ q and (t >> u).copar == m @ n
>>> (t >> u).inside.draw(doctest="docs/_static/para/stateful-then.svg")

.. image:: /_static/para/stateful-then.svg
    :align: center

Coparametric maps, studied in categorical cybernetics
:cite:p:`CapucciEtAl21`, are the case of an empty `param`, composed by
accumulating the coparameters in forward order:

>>> f_ = Symmetric(x, y, Box("f'", x, y @ m), copar=m)
>>> g_ = Symmetric(y, z, Box("g'", y, z @ n), copar=n)
>>> assert (f_ >> g_).copar == m @ n
>>> (f_ >> g_).inside.draw(doctest="docs/_static/para/copara-then.svg")

.. image:: /_static/para/copara-then.svg
    :align: center

Recoparametrisation post-composes the coparameters, covariantly where
:meth:`Symmetric.reparam` is contravariant:

>>> assert t.recopar(Box('c', m, n)).copar == n

and the diagonal `param == copar` is closed under composition: it is the
free category with feedback of :cite:t:`KatisEtAl02`.

>>> s = Ty('s')
>>> v = Symmetric(x, y, Box('v', x @ s, y @ s), s, s)
>>> w = Symmetric(y, z, Box('w', y @ s, z @ s), s, s)
>>> assert (v >> w).param == (v >> w).copar == s @ s

Example
-------

Parametric maps compose like layers of a neural network, e.g. over
:class:`Function <discopy.python.Function>` with weight and bias parameters:

>>> from discopy.python import Function, Ty as PyTy
>>> layer = Symmetric[Function](PyTy(float), PyTy(float),
...     Function(lambda x, w, b: w * x + b, PyTy(float) ** 3, PyTy(float)),
...     param=PyTy(float, float))
>>> network = layer >> layer
>>> assert network.param == PyTy(float) ** 4
>>> network.inside(2., 3., 1., .5, 0.)
3.5
"""

from __future__ import annotations

from dataclasses import dataclass

from discopy import symmetric, markov, closed, compact, frobenius
from discopy.abc import (
    ClosedCategory, CompactCategory, FeedbackCategory, HypergraphCategory,
    MarkovCategory, NamedGeneric, SymmetricCategory, TracedCategory)
from discopy.feedback import Diagram as FeedbackDiagram
from discopy.utils import (
    assert_iscomposable, assert_isinstance, classproperty, unbiased)


[docs] @dataclass class Symmetric(SymmetricCategory, NamedGeneric['category']): """ A parametric map from `dom` to `cod` with parameter space `param` is a morphism `inside : dom @ param -> cod` in an underlying `category`, optionally with a coparameter space `copar` on the codomain, i.e. `inside : dom @ param -> cod @ copar`. Parameters: dom (category.ob) : The domain of the parametric map. cod (category.ob) : The codomain of the parametric map. inside (category) : The morphism ``dom @ param -> cod @ copar``. param (category.ob) : The parameter space, empty by default. copar (category.ob) : The coparameter space, empty by default. .. admonition:: Summary .. autosummary:: lift id then tensor swap reparam recopar """ category = symmetric.Diagram ob = classproperty(lambda cls: cls.category.ob) dom: ob cod: ob inside: category param: ob = None copar: ob = None def __post_init__(self): if self.param is None: self.param = self.ob() if self.copar is None: self.copar = self.ob() for obj in (self.dom, self.cod, self.param, self.copar): assert_isinstance(obj, self.ob) assert_isinstance(self.inside, self.category) assert_iscomposable( self.category.id(self.dom @ self.param), self.inside) assert_iscomposable( self.inside, self.category.id(self.cod @ self.copar))
[docs] @classmethod def lift(cls, inside: category) -> Symmetric: """ A morphism of the underlying category as a parametric map with the empty parameter space, i.e. the injection functor from a category into its category of parametric maps. Parameters: inside : The morphism to lift. """ return cls(inside.dom, inside.cod, inside)
[docs] @classmethod def id(cls, dom: ob = None) -> Symmetric: """ The identity parametric map on `dom`, with empty parameter space. Parameters: dom : The domain of the identity, also its codomain. """ return cls.lift(cls.category.id(cls.ob() if dom is None else dom))
[docs] @unbiased def then(self, other: Symmetric) -> Symmetric: """ Sequential composition tensors the hidden spaces on both sides, i.e. `(p, f) >> (q, g) == (p @ q, f @ q >> g)` for empty coparameters and the routing of :meth:`Stream.then <discopy.stream.Stream.then>` in general. Parameters: other : The parametric map to compose with. """ assert_isinstance(other, type(self)) assert_iscomposable(self, other) inside = self.inside @ other.param\ >> self.cod @ self.category.swap(self.copar, other.param)\ >> other.inside @ self.copar\ >> other.cod @ self.category.swap(other.copar, self.copar) return type(self)(self.dom, other.cod, inside, self.param @ other.param, self.copar @ other.copar)
[docs] @unbiased def tensor(self, other: Symmetric) -> Symmetric: """ Parallel composition tensors the hidden spaces on both sides, with swaps routing the parameters to the right of the domains and the coparameters to the right of the codomains. Parameters: other : The parametric map to tensor with. """ assert_isinstance(other, type(self)) inside = self.dom @ self.category.swap(other.dom, self.param)\ @ other.param >> self.inside @ other.inside >> self.cod\ @ self.category.swap(self.copar, other.cod) @ other.copar return type(self)(self.dom @ other.dom, self.cod @ other.cod, inside, self.param @ other.param, self.copar @ other.copar)
[docs] @classmethod def swap(cls, left: ob, right: ob) -> Symmetric: """ The swap of the underlying category, with empty parameter space. Parameters: left : The object on the left of the swap. right : The object on the right of the swap. """ return cls.lift(cls.category.swap(left, right))
[docs] def reparam(self, other: category) -> Symmetric: """ Precompose the parameter space with `other : q -> param`, i.e. the 2-cells of :class:`Symmetric`, kept as a method of the 1-cells the same way as :meth:`interchange <discopy.monoidal.Diagram.interchange>`. Parameters: other : The reparametrisation, a morphism into ``param``. Example ------- >>> from discopy.symmetric import Ty, Box >>> x, y, p, q = map(Ty, "xypq") >>> f = Symmetric(x, y, Box('f', x @ p, y), p) >>> r = Box('r', q, p) >>> f.reparam(r).inside.draw(doctest="docs/_static/para/reparam.svg") .. image:: /_static/para/reparam.svg :align: center """ assert_iscomposable(self.dom @ other, self.inside) return type(self)(self.dom, self.cod, self.dom @ other >> self.inside, other.dom, self.copar)
[docs] def recopar(self, other: category) -> Symmetric: """ Post-compose the coparameter space with `other : copar -> q`, covariantly where :meth:`reparam` is contravariant. Parameters: other : The recoparametrisation, a morphism out of ``copar``. """ assert_iscomposable(self.inside, self.cod @ other) return type(self)(self.dom, self.cod, self.inside >> self.cod @ other, self.param, other.cod)
[docs] class Traced(Symmetric, TracedCategory): """ Parametric maps over a traced symmetric underlying `category` form a traced category, with the parameters swapped out of the way. """
[docs] def trace(self, n: int = 1, left: bool = False) -> Traced: """ The trace of a parametric map is the trace of the underlying morphism, with the parameters swapped out of the way. Parameters: n : The number of objects to trace over. left : Whether to trace the wires on the left or right. """ if n == 0: return self if left: return type(self)(self.dom[n:], self.cod[n:], self.inside.trace(n, left=True), self.param, self.copar) inside = self.dom[:-n] @ self.category.swap( self.param, self.dom[-n:]) >> self.inside >> self.cod[:-n]\ @ self.category.swap(self.cod[-n:], self.copar) return type(self)(self.dom[:-n], self.cod[:-n], inside.trace(n), self.param, self.copar)
[docs] class Markov(Symmetric, MarkovCategory): """ Parametric maps over a Markov underlying `category` form a Markov category, with the copy of the underlying category as :meth:`copy`. """ category = markov.Diagram
[docs] @classmethod def copy(cls, x: Symmetric.ob, n: int = 2) -> Markov: """ The copy of the underlying category, with empty parameter space. Parameters: x : The object to copy. n : The number of copies. """ return cls.lift(cls.category.copy(x, n))
[docs] class Closed(Markov, ClosedCategory): """ Parametric maps over a closed underlying `category` form a closed category, currying with the parameters swapped out of the way. """ category = closed.Diagram
[docs] @classmethod def ev(cls, base: Symmetric.ob, exponent: Symmetric.ob, left: bool = True ) -> Closed: """ The evaluation of the underlying category, with empty parameters. Parameters: base : The base of the exponential type. exponent : The exponent of the exponential type. left : Whether to take the left or right evaluation. """ return cls.lift(cls.category.ev(base, exponent, left))
[docs] def curry(self, n: int = 1, left: bool = False) -> Closed: """ Curry the last `n` objects of the domain if `left` else the first, i.e. everything but the parameters, which a left currying swaps out of the way the same as :meth:`Traced.trace`. Parameters: n : The number of objects to curry. left : Whether to curry into a left or right exponential. """ if not left: inside = self.inside.curry(n, left=False) return type(self)(self.dom[n:], inside.cod, inside, self.param) inside = self.dom[:-n] @ self.category.swap( self.param, self.dom[-n:]) >> self.inside inside = inside.curry(n, left=True) return type(self)(self.dom[:-n], inside.cod, inside, self.param)
[docs] class Feedback(Markov, FeedbackCategory): """ Parametric maps over a feedback underlying `category` form a feedback category, with :meth:`delay` applied to all five components. """ category = FeedbackDiagram
[docs] def delay(self, n_steps: int = 1) -> Feedback: """ Delay a parametric map by delaying its underlying morphism together with its domain, codomain, parameter and coparameter spaces. Parameters: n_steps : The number of time steps to delay. """ return type(self)(*(x.delay(n_steps) for x in ( self.dom, self.cod, self.inside, self.param, self.copar)))
[docs] def feedback(self, dom: Symmetric.ob = None, cod: Symmetric.ob = None, mem: Symmetric.ob = None) -> Feedback: """ The feedback of the underlying category, with the parameters swapped out of the way the same as :meth:`Traced.trace`. Parameters: dom : The domain of the feedback. cod : The codomain of the feedback. mem : The memory type to trace over. """ mem = self.cod[-1:] if mem is None else mem dom = self.dom[:len(self.dom) - len(mem)] if dom is None else dom cod = self.cod[:len(self.cod) - len(mem)] if cod is None else cod inside = dom @ self.category.swap(self.param, mem.delay())\ >> self.inside >> cod @ self.category.swap(mem, self.copar) return type(self)( dom, cod, inside.feedback(dom @ self.param, cod @ self.copar, mem), self.param, self.copar)
[docs] class Compact(Traced, CompactCategory): """ Parametric maps over a compact underlying `category` form a compact category, with the cups and caps of the underlying category. """ category = compact.Diagram
[docs] @classmethod def cups(cls, left: Symmetric.ob, right: Symmetric.ob) -> Compact: """ The cups of the underlying category, with empty parameter space. Parameters: left : The left-hand side of the cups. right : Its adjoint, i.e. the right-hand side of the cups. """ return cls.lift(cls.category.cups(left, right))
[docs] @classmethod def caps(cls, left: Symmetric.ob, right: Symmetric.ob) -> Compact: """ The caps of the underlying category, with empty parameter space. Parameters: left : The left-hand side of the caps. right : Its adjoint, i.e. the right-hand side of the caps. """ return cls.lift(cls.category.caps(left, right))
ev = classmethod(Closed.ev.__func__) curry = Closed.curry
[docs] class Hypergraph(Compact, Markov, HypergraphCategory): """ Parametric maps over a hypergraph underlying `category` form a hypergraph category, with the spiders of the underlying category. """ category = frobenius.Diagram
[docs] @classmethod def spiders(cls, n_legs_in: int, n_legs_out: int, typ: Symmetric.ob ) -> Hypergraph: """ The spiders of the underlying category, with empty parameters. Parameters: n_legs_in : The number of legs in for each spider. n_legs_out : The number of legs out for each spider. typ : The type of the spiders. """ return cls.lift(cls.category.spiders(n_legs_in, n_legs_out, typ))