Source code for discopy.balanced

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

"""
The free balanced category, i.e. diagrams with braids and a twist.

Summary
-------

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

    Diagram
    Box
    Braid
    Twist
    Sum
    Functor

Axioms
------
The axiom for the twist holds on the nose.

>>> x, y = Ty('x'), Ty('y')
>>> assert Diagram.twist(x @ y) == (Braid(x, y)
...     >> Twist(y) @ Twist(x) >> Braid(y, x))
>>> Diagram.twist(x @ y).draw(path="docs/_static/balanced/twist.svg")

.. image:: /_static/balanced/twist.svg
"""

from __future__ import annotations

from copy import copy

from discopy import config, monoidal, braided, traced, hypergraph
from discopy.abc import BalancedCategory
from discopy.cat import factory
from discopy.monoidal import Ty  # noqa: F401
from discopy.utils import factory_name, assert_isatomic


def set_rail_margins(typ: monoidal.Ty, width: float = None) -> monoidal.Ty:
    """
    Sets the :attr:`min_right_margin` of each object of an already-doubled type
    by position, so that the two rails of every ribbon are drawn ``width``
    apart. This is re-applied after rotation, which reverses the rails and
    drops the margin (a per-object attribute that cannot know its pair-mate).

    Parameters:
        typ : An already-doubled type, i.e. with an even number of objects.
        width : The gap between the two rails, defaults to the ``ribbon_width``
            in :data:`discopy.config.DRAWING_DEFAULT`.
    """
    width = config.DRAWING_DEFAULT["ribbon_width"] if width is None else width
    for i, ob in enumerate(typ.inside):
        ob.min_right_margin = width - 1 if i % 2 == 0 else 0
    return typ


def double_rail(typ: monoidal.Ty, width: float = None) -> monoidal.Ty:
    """
    Doubles every object of a type into the two rails of a ribbon ``width``
    apart, copying each object so the two rails hold independent margins.

    Parameters:
        typ : The type to double.
        width : The gap between the two rails, defaults to the ``ribbon_width``
            in :data:`discopy.config.DRAWING_DEFAULT`.
    """
    return set_rail_margins(
        type(typ)(*[copy(ob) for ob in typ.inside for _ in range(2)]), width)


[docs] @factory class Diagram(braided.Diagram, traced.Diagram, BalancedCategory): """ A balanced diagram is a braided diagram with :class:`Twist`. Parameters: inside(Layer) : The layers inside the diagram. dom (monoidal.Ty) : The domain of the diagram, i.e. its input. cod (monoidal.Ty) : The codomain of the diagram, i.e. its output. Note ---- By default, our balanced diagrams are traced. Although not every balanced category embeds faithfully into a traced one (see the `nLab`_), the free balanced category does have the desired cancellation property and it does in fact embed faithfully into the free balanced traced category. .. _nLab: https://ncatlab.org/nlab/show/traced+monoidal+category) """
[docs] @classmethod def twist(cls, dom: monoidal.Ty) -> Diagram: """ The twist on an object. Parameters: dom : The domain of the twist. Note ---- This calls :attr:`twist_factory`. """ if len(dom) == 0: return cls.id() return cls.braid(dom[0], dom[1:])\ >> cls.twist(dom[1:]) @ cls.twist_factory(dom[0])\ >> cls.braid(dom[1:], dom[0])
[docs] def to_braided(self, width: float = None): """ Doubles every object and sends the twist to the braid. Parameters: width : The width of a ribbon, i.e. the gap between the two wires encoding each object, defaults to the ``ribbon_width`` in :data:`discopy.config.DRAWING_DEFAULT`. Set to ``0`` to return the diagram as is, i.e. without doubling it into dual rails. Example ------- >>> x = Ty('x') >>> braided_twist = Diagram.twist(x).to_braided() >>> Equation(Twist(x), braided_twist, symbol='$\\\\mapsto$').draw( ... wire_labels=False, ... path="docs/_static/balanced/twist_dual_rail.svg") .. image:: /_static/balanced/twist_dual_rail.svg """ width = config.DRAWING_DEFAULT["ribbon_width"]\ if width is None else width return self if not width else self.dual_rail_factory(width)(self)
[docs] class Box(braided.Box, traced.Box, Diagram): """ A braided box is a monoidal box in a braided diagram. Parameters: name (str) : The name of the box. dom (monoidal.Ty) : The domain of the box, i.e. its input. cod (monoidal.Ty) : The codomain of the box, i.e. its output. """
[docs] class Braid(braided.Braid, Box): """ Braid in a balanced category. """
class DualRailBraid(braided.Box): """ The crossing of two ribbons in the dual rail encoding of a swap. Unlike the braid of the doubled types (which decomposes into four wire crossings via the hexagon equation), this box is drawn as the two ribbons crossing as a whole. It is only used by :meth:`Diagram.to_braided`. Parameters: left : The ribbon (doubled type) on the top left and bottom right. right : The ribbon on the top right and bottom left. is_dagger (bool) : Which ribbon goes over the other. """ def __init__(self, left: monoidal.Ty, right: monoidal.Ty, is_dagger=False): self.left, self.right = left, right name = type(self).__name__ + f"({left}, {right})" braided.Box.__init__( self, name, left @ right, right @ left, is_dagger=is_dagger, draw_as_dual_rail_braid=True) def __repr__(self): str_is_dagger = ", is_dagger=True" if self.is_dagger else "" return factory_name(type(self))\ + f"({self.left!r}, {self.right!r}{str_is_dagger})" def dagger(self): return type(self)(self.right, self.left, not self.is_dagger) class DualRailTwist(braided.Box): """ The twist of a ribbon in the dual rail encoding, i.e. its two rails crossing each other twice. It is only used by :meth:`Diagram.to_braided`. Parameters: dom : The ribbon (doubled type) being twisted. is_dagger (bool) : Which way the rails twist. """ def __init__(self, dom: monoidal.Ty, is_dagger=False): name = type(self).__name__ + f"({dom})" braided.Box.__init__( self, name, dom, dom, is_dagger=is_dagger, draw_as_dual_rail_twist=True) def __repr__(self): str_is_dagger = ", is_dagger=True" if self.is_dagger else "" return factory_name(type(self)) + f"({self.dom!r}{str_is_dagger})" def dagger(self): return type(self)(self.dom, not self.is_dagger) class Trace(traced.Trace, Box): """ A trace in a balanced category. Parameters: arg : The diagram to trace. left : Whether to trace the wires on the left or right. See also -------- :meth:`Diagram.trace` """
[docs] class Twist(Box): """ The twist on atomic type :code:`dom`. Parameters: dom : the domain of the twist. phase: the phase of the twist in integer multiples of ``2 * pi``. Important --------- :class:`Twist` is only defined for atomic types (i.e. of length 1). For complex types, use :meth:`Diagram.twist` instead. """ drawing_name = "Twist" def __init__(self, dom: monoidal.Ty, is_dagger=False): assert_isatomic(dom, monoidal.Ty) name = type(self).__name__ + f"({dom})" Box.__init__(self, name, dom, dom, is_dagger=is_dagger) def __repr__(self): if self.is_dagger: return repr(self.dagger()) + ".dagger()" return factory_name(type(self)) + f"({self.dom!r})" def dagger(self): return type(self)(self.dom, not self.is_dagger)
[docs] class Sum(braided.Sum, Box): """ A balanced sum is a braided sum and a balanced box. Parameters: terms (tuple[Diagram, ...]) : The terms of the formal sum. dom (Ty) : The domain of the formal sum. cod (Ty) : The codomain of the formal sum. """
[docs] class Functor(braided.Functor, traced.Functor): """ A balanced functor is a braided functor that twists. Parameters: ob_map (Mapping[monoidal.Ty, monoidal.Ty]) : Map from :class:`monoidal.Ty` to :code:`cod.ob`. ar_map (Mapping[Box, Diagram]) : Map from :class:`Box` to :code:`cod`. cod (Category) : The codomain, :code:`Diagram` by default. """ dom = cod = Diagram def __call__(self, other): if isinstance(other, Twist): return self.cod.twist(self(other.dom)) if isinstance(other, Trace): return traced.Functor.__call__(self, other) return braided.Functor.__call__(self, other)
class DualRail(Functor): """ The functor sending a balanced diagram to its dual rail encoding in :class:`discopy.braided.Diagram`, i.e. doubling every object into the two rails of a ribbon and sending every :class:`Twist` and :class:`Braid` to a single box crossing the two ribbons of a wire as a whole. Parameters: width : The gap between the two rails of each ribbon, defaults to the ``ribbon_width`` in :data:`discopy.config.DRAWING_DEFAULT`. See also -------- :meth:`Diagram.to_braided` """ cod = braided.Diagram dual_rail_twist_factory = DualRailTwist dual_rail_braid_factory = DualRailBraid def __init__(self, width: float = None): self.width = config.DRAWING_DEFAULT["ribbon_width"]\ if width is None else width super().__init__( ob_map=lambda x: double_rail(x, self.width), ar_map=lambda f: f.name) def __call__(self, other): if isinstance(other, monoidal.Ty): return set_rail_margins(super().__call__(other), self.width) if isinstance(other, Twist): return self.dual_rail_twist_factory(self(other.dom)) if isinstance(other, Braid): return self.dual_rail_braid_factory( self(other.left), self(other.right), other.is_dagger) return super().__call__(other) Diagram.functor_factory = Functor Diagram.map_factory = traced.CMap Hypergraph = hypergraph.Hypergraph[Diagram] Diagram.braid_factory = Braid Diagram.twist_factory = Twist Diagram.trace_factory = Trace Diagram.sum_factory = Sum Diagram.dual_rail_factory = DualRail Id = Diagram.id class Equation(braided.Equation): """ The :class:`braided.Equation` of balanced diagrams. """