"""
The category of labeled progressive plane graphs.
This was first defined in :cite:t:`JoyalStreet88`.
Summary
-------
.. autosummary::
:template: class.rst
:nosignatures:
:toctree:
Point
PlaneGraph
Drawing
Equation
Axioms
------
* Associativity and unit
>>> from discopy.monoidal import Ty, Box
>>> x, y, z, w = map(Ty, "xyzw")
>>> f = Box('f', x, y).to_drawing()
>>> g = Box('g', y, z).to_drawing()
>>> h = Box('h', z, w).to_drawing()
>>> assert (f >> g) >> h == f >> (g >> h)
>>> assert (f @ g) @ h == f @ (g @ h)
>>> assert f >> Drawing.id(f.cod) == f == Drawing.id(f.dom) >> f
>>> assert f @ Drawing.id() == f == Drawing.id() @ f
* Interchanger
>>> f0, f1 = (Box(f'f{i}', f'x{i}', f'y{i}').to_drawing() for i in (0, 1))
>>> g0, g1 = (Box(f'g{i}', f'y{i}', f'z{i}').to_drawing() for i in (0, 1))
>>> Equation(f0 @ f1 >> g0 @ g1, (f0 >> g0) @ (f1 >> g1)).draw(
... path="docs/_static/drawing/interchanger-1.png")
.. image:: /_static/drawing/interchanger-1.png
:align: center
>>> Equation(f @ g.dom >> f.cod @ g, f @ g, f.dom @ g >> f @ g.cod).draw(
... path="docs/_static/drawing/interchanger-2.png")
.. image:: /_static/drawing/interchanger-2.png
:align: center
"""
from __future__ import annotations
from typing import NamedTuple, TYPE_CHECKING
from dataclasses import dataclass
import networkx as nx
from discopy.drawing import backend, Node, Point
from discopy.config import DRAWING_ATTRIBUTES
from discopy.utils import (
Composable, Whiskerable, assert_isinstance, assert_iscomposable, unbiased)
if TYPE_CHECKING:
from discopy import monoidal
[docs]
class PlaneGraph(NamedTuple):
""" A plane graph is a graph with a mapping from nodes to points. """
graph: nx.DiGraph
positions: dict[Node, Point]
[docs]
@dataclass
class Drawing(Composable, Whiskerable):
"""
A drawing is a plane graph with designated input and output types.
Parameters:
inside (PlaneGraph) : The plane graph underlying the drawing.
dom (monoidal.Ty) : The domain of the drawing, i.e. its input type.
cod (monoidal.Ty) : The codomain of the drawing, i.e. its output type.
boxes (tuple[monoidal.Box, ...]) : The boxes inside the drawing.
width (float) : The width of the drawing.
height (float) : The height of the drawing.
_check (bool) : Whether to call :meth:`validate_attributes`.
.. admonition:: Summary
.. autosummary::
validate_attributes
draw
from_box
id
then
tensor
dagger
bubble
frame
"""
inside: PlaneGraph
dom: "monoidal.Ty"
cod: "monoidal.Ty"
boxes: tuple["monoidal.Box", ...] = ()
width: float = 0
height: float = 0
graph = property(lambda self: self.inside.graph)
nodes = property(lambda self: self.graph.nodes)
edges = property(lambda self: self.graph.edges)
positions = property(lambda self: self.inside.positions)
def nodes_of_kind(self, kind):
return [node for node in self.nodes if node.kind == kind]
box_nodes = property(lambda self: self.nodes_of_kind("box"))
dom_nodes = property(lambda self: self.nodes_of_kind("dom"))
cod_nodes = property(lambda self: self.nodes_of_kind("cod"))
box_dom_nodes = property(lambda self: self.nodes_of_kind("box_dom"))
box_cod_nodes = property(lambda self: self.nodes_of_kind("box_cod"))
def __init__(
self, inside, dom, cod, boxes=(), width=0, height=0, _check=True):
self.inside, self.dom, self.cod = inside, dom, cod
self.boxes, self.width, self.height = boxes, width, height
if _check:
self.validate_attributes()
[docs]
def validate_attributes(self):
"""
Check that the attributes of a drawing are consistent.
>>> from discopy.monoidal import Ty, Id
>>> x = Ty('x')
>>> drawing = Id(x).to_drawing()
>>> drawing.add_edges([(Node("cod", i=0, x=x), Node("dom", i=0, x=x))])
>>> drawing.validate_attributes()
Traceback (most recent call last):
...
ValueError: Wrong edge Node('cod', i=0, x=x) -> Node('dom', i=0, x=x)
"""
from discopy.monoidal import Ty, Box
assert_isinstance(self.dom, Ty)
assert_isinstance(self.cod, Ty)
for box in self.boxes:
assert_isinstance(box, Box)
assert self.dom_nodes == [
Node("dom", i=i, x=x) for i, x in enumerate(self.dom)]
assert self.cod_nodes == [
Node("cod", i=i, x=x) for i, x in enumerate(self.cod)]
assert self.box_nodes == [
Node("box", j=j, box=box) for j, box in enumerate(self.boxes)]
for j, box in enumerate(self.boxes):
box_node = self.box_nodes[j]
box_dom_nodes, box_cod_nodes = ([
Node(f"box_{kind}", i=i, j=j, x=x)
for i, x in enumerate(xs)] for kind, xs in [
("dom", box.dom), ("cod", box.cod)])
assert list(self.graph.predecessors(box_node)) == box_dom_nodes
assert list(self.graph.successors(box_node)) == box_cod_nodes
for source, target in self.edges:
if source.kind == "box":
assert target.kind == "box_cod"
elif source.kind == "box_dom":
assert target.kind == "box"
elif source.kind in ("dom", "box_cod"):
assert target.kind in ("cod", "box_dom")
else:
raise ValueError(f"Wrong edge {source} -> {target}")
assert self.height >= (1 if self.boxes else 0)
assert self.width >= (1 if self.boxes else 0)
assert self.width >= max(x for (x, _) in self.positions.values())
assert self.height >= max(y for (_, y) in self.positions.values())
assert set(self.positions.keys()) == set(self.nodes) == set(
self.dom_nodes + self.cod_nodes) + set(
self.box_dom_nodes + self.box_nodes + self.box_cod_nodes)
assert all(isinstance(x, Point) for x in self.positions.values())
def __eq__(self, other):
if not isinstance(other, Drawing):
return False
return self.is_parallel(other) and self.positions == other.positions
[docs]
def draw(self, **params):
""" Call :meth:`add_box_corners` then :func:`backend.draw`. """
asymmetry = params.pop("asymmetry", 0.125 * any(
box.is_conjugate or box.is_transpose or (
box.is_dagger and not box.draw_as_braid)
for box in self.boxes))
self.add_box_corners()
return backend.draw(self, asymmetry=asymmetry, **params)
[docs]
def add_box_corners(self):
""" Recenter boxes w.r.t their wires then draw the corners. """
for j, box in enumerate(self.boxes):
box_node = Node("box", j=j, box=box)
box_x, box_y = self.positions[box_node]
box_dom_nodes, box_cod_nodes = ([
Node(kind, i=i, j=j, x=x) for i, x in enumerate(xs)]
for kind, xs in [("box_dom", box.dom), ("box_cod", box.cod)])
xs = [self.positions[n].x for n in box_dom_nodes + box_cod_nodes]
left, right = min(xs + [box_x]) - 0.25, max(xs + [box_x]) + 0.25
self.add_nodes({
Node(f"box-corner-{a}{b}", j=j): Point(x, box_y + y)
for a, x in enumerate([left, right])
for b, y in enumerate([-0.25, 0.25])})
if box.draw_as_wires or box.draw_as_spider:
if len(box.dom) == 1 or len(box.cod) == 1:
continue
self.positions[box_node] = Point(
(right + left) / 2, self.positions[box_node].y)
[docs]
def union(self, other, dom, cod, width=None, height=None, _check=True):
""" Take the union of two drawings, assuming nodes are distinct. """
graph = nx.union(self.inside.graph, other.inside.graph)
inside = PlaneGraph(graph, self.positions | other.positions)
boxes = self.boxes + other.boxes
width = width or max(self.width, other.width)
height = height or max(self.height, other.height)
return Drawing(inside, dom, cod, boxes, width, height, _check)
[docs]
def add_nodes(self, positions: dict[Node, Point]):
""" Add nodes to the graph given their positions. """
if not positions:
return
self.graph.add_nodes_from(positions)
self.positions.update(positions)
self.width = max(self.width, max(i for (i, _) in positions.values()))
self.height = max(self.height, max(j for (_, j) in positions.values()))
[docs]
def add_edges(self, edges: list[tuple[Node, Node]]):
""" Add edges from a list. """
self.graph.add_edges_from(edges)
[docs]
def relabel_nodes(
self, mapping=dict(), positions=dict(), copy=True, _check=False):
""" Relabel nodes and/or their positions. """
graph = nx.relabel_nodes(self.graph, mapping, copy)
if not copy:
self.positions.update(positions)
return self
positions = {mapping.get(node, node): positions.get(node, pos)
for node, pos in self.positions.items()}
inside = PlaneGraph(graph, positions)
dom, cod, boxes = self.dom, self.cod, self.boxes
x, y = self.width, self.height
return Drawing(inside, dom, cod, boxes, x, y, _check=_check)
[docs]
def make_space(self, space, x,
y_min=None, y_max=None, exclusive=False, copy=False):
"""
Make some horizontal space after position x
for all nodes between y_min and y_max (inclusive).
Example
-------
>>> from discopy.monoidal import Ty, Box
>>> x, y, z = map(Ty, "xyz")
>>> f = Drawing.from_box(Box('f', x @ y, z))
>>> f.make_space(2, 0.5, 0.75, 1.0, copy=True).draw(
... aspect='equal', path="docs/_static/drawing/make-space.png")
.. image:: /_static/drawing/make-space.png
:align: center
"""
y_min = 0 if y_min is None else y_min
y_max = self.height if y_max is None else y_max
result = self.relabel_nodes(copy=copy, positions={
n: p.shift(x=((p.x > x if exclusive else p.x >= x) * space))
for n, p in self.positions.items() if y_min <= p.y <= y_max})
result.width += space
return result
[docs]
def reposition_box_dom(self, j=0):
""" Recenter dom nodes, used when drawing frames. """
box_dom = self.boxes[j].dom
xs = [self.positions[n].x for n in self.nodes
if n.kind == "box_cod" and n.j == j]
box_x = self.positions[self.box_nodes[j]].x
left, right = min(xs + [box_x]), max(xs + [box_x])
for i, x in enumerate(box_dom):
target = Node("box_dom", i=i, j=j, x=x)
source, = self.graph.predecessors(target)
for n in (source, target):
x = (right + left - len(box_dom) + 1) / 2 + i
self.positions[n] = Point(x, self.positions[n].y)
[docs]
def reposition_box_cod(self, j=-1):
""" Recenter cod nodes to recover legacy behaviour for layers. """
j = j if j > 0 else len(self.boxes) + j
box = self.boxes[j]
if box.bubble_closing and len(box.dom[1:-1]) == len(box.cod):
return # Otherwise the wires would bend when coming out.
xs = [self.positions[n].x for n in self.nodes
if n.kind == "box_dom" and n.j == j]
box_x = self.positions[self.box_nodes[j]].x
left, right = min(xs + [box_x]), max(xs + [box_x])
for i, x in enumerate(box.cod):
source = Node("box_cod", i=i, j=j, x=x)
target, = self.graph.successors(source)
if target.kind != "cod":
return # Otherwise we would have to reposition everything.
for n in (source, target):
x = (right + left - len(box.cod) + 1) / 2 + i
self.positions[n] = Point(x, self.positions[n].y)
if box.draw_as_spider and len(box.cod) == 1:
box_node = Node("box", box=box, j=j)
self.positions[box_node] = Point(x, self.positions[box_node].y)
[docs]
def align_box_cod(self, j=-1):
""" Align outputs with inputs when they have equal number of wires. """
j = j if j > 0 else len(self.boxes) + j
box = self.boxes[j]
for i, (x_dom, x_cod) in enumerate(zip(box.dom, box.cod)):
dom_node = Node("box_dom", i=i, j=j, x=x_dom)
cod_node = Node("box_cod", i=i, j=j, x=x_cod)
target, = self.graph.successors(cod_node)
if target.kind != "cod":
return # Otherwise we would have to reposition everything.
x, _ = self.positions[dom_node]
_, y = self.positions[cod_node]
self.positions[cod_node] = Point(x, y)
@property
def is_identity(self):
""" A drawing with no boxes is the identity. """
return not self.boxes
@property
def is_empty(self):
""" A drawing with no boxes and no wires is empty. """
return self.is_identity and not self.dom
@property
def is_box(self):
""" Whether the drawing is just one box. """
return len(self.boxes) == 1 and self.is_parallel(self.boxes[0])
@property
def is_layer(self):
""" Whether the drawing is just one box with wires on both sides. """
return len(self.boxes) == 1
@property
def box(self):
""" Syntactic sugar for self.boxes[0] when self.is_box """
if not self.is_layer:
raise ValueError
return self.boxes[0]
@property
def left_is_whiskered(self):
""" Whether `self = x @ f` for some non-empty type `x`. """
if len(self.dom) == 0:
return False
left_dom = Node("dom", i=0, x=self.dom[0])
target, = self.graph.successors(left_dom)
return target.kind == "cod" and target.i == 0
@property
def right_is_whiskered(self):
""" Whether `self = f @ x` for some non-empty type `x`. """
if len(self.dom) == 0:
return False
right_dom = Node("dom", i=len(self.dom) - 1, x=self.dom[-1])
target, = self.graph.successors(right_dom)
return target.kind == "cod" and target.i == len(self.cod) - 1
[docs]
@staticmethod
def from_box(box: "monoidal.Box") -> Drawing:
"""
Draw a diagram with just one box.
>>> from discopy.monoidal import Ty, Box
>>> x, y, z = map(Ty, "xyz")
>>> f = Box('f', x, y @ z)
>>> assert f.to_drawing() == Drawing.from_box(f)
>>> for ps in f.to_drawing().positions.items(): print(*ps)
Node('box', box=f, j=0) Point(x=1.0, y=0.5)
Node('dom', i=0, x=x) Point(x=1.0, y=1)
Node('box_dom', i=0, j=0, x=x) Point(x=1.0, y=0.75)
Node('box_cod', i=0, j=0, x=y) Point(x=0.5, y=0.25)
Node('box_cod', i=1, j=0, x=z) Point(x=1.5, y=0.25)
Node('cod', i=0, x=y) Point(x=0.5, y=0)
Node('cod', i=1, x=z) Point(x=1.5, y=0)
>>> f.draw(path="docs/_static/drawing/box.png")
.. image:: /_static/drawing/box.png
:align: center
"""
from discopy.monoidal import Box
box_dom, box_cod = box.dom.to_drawing(), box.cod.to_drawing()
old_box, box = box, Box(
box.name, box_dom, box_cod, is_dagger=box.is_dagger)
for attr, default in DRAWING_ATTRIBUTES.items():
setattr(box, attr, getattr(old_box, attr, default(box)))
if box.draw_as_wires and not box.frame_boundary:
for i, obj in enumerate(box.cod.inside):
obj.reposition_label = 0.5 if (
box.bubble_closing or box.bubble_opening and i) else 0.25
if box.bubble_opening:
width = max(1, len(box.dom), len(box.cod) - 2) + 0.5
elif box.bubble_closing:
width = max(1, len(box.dom) - 2, len(box.cod)) + 0.5
elif len(box.dom) <= 1 and len(box.cod) <= 1:
width = 1
else:
width = max(len(box.dom), len(box.cod))
height = box.height
left, right = 0.25, width - 0.25
inside = PlaneGraph(nx.DiGraph(), dict())
result = Drawing(
inside, box.dom, box.cod, (box, ), width, height, _check=False)
box_node = Node("box", box=box, j=0)
result.add_nodes({box_node: Point(width / 2, height / 2)})
dom = [Node("dom", i=i, x=x) for i, x in enumerate(box.dom)]
cod = [Node("cod", i=i, x=x) for i, x in enumerate(box.cod)]
box_dom = [Node("box_dom", i=i, j=0, x=x.x) for i, x in enumerate(dom)]
box_cod = [Node("box_cod", i=i, j=0, x=x.x) for i, x in enumerate(cod)]
result.add_edges(list(zip(dom, box_dom)))
result.add_edges([(x, box_node) for x in box_dom])
result.add_edges([(box_node, x) for x in box_cod])
result.add_edges(list(zip(box_cod, cod)))
if box.bubble_opening:
result.add_nodes({
cod[0]: Point(left, 0),
box_cod[0]: Point(left, 0),
box_cod[-1]: Point(right, 0),
cod[-1]: Point(right, 0)})
cod, box_cod = cod[1:-1], box_cod[1:-1]
elif box.bubble_closing:
result.add_nodes({
dom[0]: Point(left, height),
box_dom[0]: Point(left, height),
box_dom[-1]: Point(right, height),
dom[-1]: Point(right, height)})
dom, box_dom = dom[1:-1], box_dom[1:-1]
result.add_nodes({
x: Point(i + (width - len(xs) + 1) / 2, y) for xs, y in [
(dom, height),
(box_dom, height if box.draw_as_wires else height - 0.25),
(box_cod, 0 if box.draw_as_wires else 0.25),
(cod, 0)]
for i, x in enumerate(xs)})
return result
[docs]
@staticmethod
def id(dom: "monoidal.Ty" = None, length=0) -> Drawing:
"""
Draw the identity diagram.
>>> from discopy.monoidal import Ty
>>> Drawing.id(Ty()).draw(path="docs/_static/drawing/empty.png")
.. image:: /_static/drawing/empty.png
:align: center
>>> Drawing.id(Ty('x')).draw(path="docs/_static/drawing/idx.png")
.. image:: /_static/drawing/idx.png
:align: center
>>> Drawing.id(Ty('x', 'y')).draw(path="docs/_static/drawing/idxy.png")
.. image:: /_static/drawing/idxy.png
:align: center
"""
from discopy.monoidal import Ty
dom = Ty() if dom is None else dom
inside = PlaneGraph(nx.DiGraph(), dict())
height, width = 0.5, len(dom) - 0.5 if len(dom) > 1 else 0.5
result = Drawing(inside, dom, dom, (), width, height, _check=False)
dom_nodes = [Node("dom", i=i, x=x) for i, x in enumerate(dom)]
cod_nodes = [Node("cod", i=i, x=x) for i, x in enumerate(dom)]
result.add_nodes({
x: Point(i + 0.25, 1) for i, x in enumerate(dom_nodes)})
result.add_nodes({
x: Point(i + 0.25, 0) for i, x in enumerate(cod_nodes)})
result.add_edges(list(zip(dom_nodes, cod_nodes)))
return result
[docs]
@unbiased
def then(self, other: Drawing, draw_step_by_step=False) -> Drawing:
"""
Draw one diagram composed with another.
This is done by calling :meth:`make_space` to align the output wires of
`self` and the input wires of `other` while keeping them straight.
Example
-------
>>> from discopy.monoidal import Ty, Box, Diagram
>>> x = Ty('x')
>>> f = Drawing.from_box(Box('f', x, x @ x @ x))
>>> g = Drawing.from_box(Box('g', x @ x, x))
>>> u = Drawing.from_box(Box('u', Ty(), x ** 3))
>>> v = Drawing.from_box(Box('v', x ** 7, Ty()))
>>> top, bottom = u >> g @ f, g @ f @ f >> v
>>> Diagram.to_gif(
... *top.then(bottom, draw_step_by_step=True), loop=True,
... wire_labels=False, draw_box_labels=False,
... path="docs/_static/drawing/composition.gif")
<IPython.core.display.HTML object>
.. image:: /_static/drawing/composition.gif
:align: center
"""
assert_iscomposable(self, other)
if self.is_identity:
return other
if other.is_identity:
return self
dom, cod = self.dom, other.cod
tmp_cod = [Node("tmp_cod", i=i) for i, n in enumerate(self.cod_nodes)]
mapping = dict(zip(self.cod_nodes, tmp_cod))
positions = {
n: p.shift(y=other.height + 1) for n, p in self.positions.items()}
tmp_dom = [Node("tmp_dom", i=i) for i, n in enumerate(other.dom_nodes)]
other_mapping = dict(zip(other.dom_nodes, tmp_dom))
other_mapping.update({
n: n.shift_j(len(self.boxes))
for n in other.nodes if "box" in n.kind})
result = self.relabel_nodes(mapping, positions, _check=False).union(
other.relabel_nodes(other_mapping), dom, cod, _check=False)
result.height = self.height + other.height + 1
cut, top_width, bot_width = other.height + 0.5, self.width, other.width
if draw_step_by_step:
steps = [result.relabel_nodes(copy=True)]
for i, (u, v) in enumerate(zip(self.cod_nodes, other.dom_nodes)):
top = result.positions[tmp_cod[i]].x
bot = result.positions[tmp_dom[i]].x
if top > bot:
bot_width += top - bot
result.make_space(top - bot, (i > 0) * bot, 0, cut)
elif top < bot:
top_width += bot - top
result.make_space(bot - top, (i > 0) * top, cut, result.height)
source, = self.graph.predecessors(u)
target, = other.graph.successors(v)
result.add_edges([(source, other_mapping.get(target, target))])
if draw_step_by_step:
steps.append(result.relabel_nodes(copy=True))
result.graph.remove_nodes_from(tmp_dom + tmp_cod)
[result.positions.pop(n) for n in tmp_dom + tmp_cod]
result.relabel_nodes(copy=False, positions={
n: p.shift(y=-1)
for n, p in result.positions.items() if p.y > other.height})
result.height = self.height + other.height
result.width = max(top_width, bot_width)
for j, box in enumerate(other.boxes): # Recover legacy behaviour.
if len(box.dom) == len(box.cod) and not box.frame_boundary:
result.align_box_cod(len(self.boxes) + j)
else:
result.reposition_box_cod(len(self.boxes) + j)
if draw_step_by_step:
for step in steps:
step.width = result.width
return steps if draw_step_by_step else result
[docs]
def stretch(self, y, copy=True):
"""
Stretch input and output wires to increase the height of a diagram
by a given length.
.
Example
-------
>>> from discopy.monoidal import Box
>>> f = Drawing.from_box(Box('f', 'x', 'x'))
>>> f.stretch(2).draw(path="docs/_static/drawing/stretch.png")
.. image:: /_static/drawing/stretch.png
:align: center
"""
result = self.relabel_nodes(copy=copy, positions={n: p.shift(y=(
y if n.kind == "dom" else 0 if n.kind == "cod" else y / 2))
for n, p in self.positions.items()})
result.height += y
return result
[docs]
@unbiased
def tensor(self, other: Drawing) -> Drawing:
"""
Draw two diagrams side by side.
Example
-------
>>> from discopy.monoidal import Box
>>> f = Drawing.from_box(Box('f', 'x', 'x'))
>>> d = (f >> f >> f) @ (f >> f)
>>> d.draw(path="docs/_static/drawing/tensor.png")
.. image:: /_static/drawing/tensor.png
:align: center
"""
if self.is_empty:
return other
if other.is_empty:
return self
mapping = {
n: n.shift_j(len(self.boxes))
for n in other.nodes if "box" in n.kind}
mapping.update({n: n.shift_i(len(self.dom)) for n in other.dom_nodes})
mapping.update({n: n.shift_i(len(self.cod)) for n in other.cod_nodes})
if self.height < other.height:
self = self.stretch(other.height - self.height)
elif self.height > other.height:
other = other.stretch(self.height - other.height)
x_shift = self.width + (
0.25 if self.right_is_whiskered or other.left_is_whiskered else 0)
result = self.union(other.relabel_nodes(mapping, positions={
n: p.shift(x=x_shift) for n, p in other.positions.items()}),
dom=self.dom @ other.dom, cod=self.cod @ other.cod, _check=False)
result.width = x_shift + other.width
return result
[docs]
def dagger(self) -> Drawing:
""" The reflection of a drawing along the the horizontal axis. """
def box_dagger(box):
result = box.dagger()
for attr in DRAWING_ATTRIBUTES:
setattr(result, attr, getattr(box, attr))
return result
if self.is_box:
return Drawing.from_box(box_dagger(self.box))
mapping = {
n: Node("box", box=box_dagger(n.box), j=len(self.boxes) - n.j - 1)
for n in self.nodes if n.kind == "box"}
mapping.update({
n: Node(kd, i=n.i, j=len(self.boxes) - n.j - 1, x=n.x)
for (k, kd) in [("box_dom", "box_cod"), ("box_cod", "box_dom")]
for n in self.nodes if n.kind == k})
mapping.update({
n: Node("cod", i=i, x=n.x) for i, n in enumerate(self.dom_nodes)})
mapping.update({
n: Node("dom", i=i, x=n.x) for i, n in enumerate(self.cod_nodes)})
graph = nx.relabel_nodes(self.graph, mapping).reverse(copy=False)
inside = PlaneGraph(graph, positions={
mapping[n]: Point(x, self.height - y)
for n, (x, y) in self.positions.items()})
dom, cod = self.cod, self.dom
boxes = tuple(map(box_dagger, self.boxes[::-1]))
return Drawing(inside, dom, cod, boxes, self.width, self.height)
[docs]
@staticmethod
def bubble_opening(dom, arg_dom, left, right, frame_boundary=False):
"""
Construct the opening of a bubble, i.e. a box drawn as wires.
>>> from discopy.monoidal import Ty
>>> x, y, z = map(Ty, "xyz")
>>> Drawing.bubble_opening(x, y, z, Ty("")).draw(
... path="docs/_static/drawing/bubble-opening.png")
.. image:: /_static/drawing/bubble-opening.png
:align: center
"""
from discopy.monoidal import Box
return Box(
"top", dom, left @ arg_dom @ right,
bubble_opening=True, frame_boundary=frame_boundary,
height=(0.5 if frame_boundary else 1)).to_drawing()
[docs]
@staticmethod
def bubble_closing(arg_cod, cod, left, right, frame_boundary=False):
"""
Construct the closing of a bubble, i.e. a box drawn as wires.
>>> from discopy.monoidal import Ty
>>> x, y, z = map(Ty, "xyz")
>>> Drawing.bubble_closing(x, y, z, Ty("")).draw(
... path="docs/_static/drawing/bubble-closing.png")
.. image:: /_static/drawing/bubble-closing.png
:align: center
"""
from discopy.monoidal import Box
return Box(
"bot", left @ arg_cod @ right, cod,
bubble_closing=True, frame_boundary=frame_boundary,
height=(0.5 if frame_boundary else 1)).to_drawing()
[docs]
@staticmethod
def frame_opening(dom, arg_dom, left, right):
"""
Construct the opening of a frame as the opening of a bubble squashed to
zero height so that it looks like the upper half of a rectangle.
>>> from discopy.monoidal import Ty
>>> x, y, z = map(Ty, "xyz")
>>> Drawing.frame_opening(x, y, z, Ty("")).draw(
... path="docs/_static/drawing/frame-opening.png")
.. image:: /_static/drawing/frame-opening.png
:align: center
"""
result = Drawing.bubble_opening(
dom, arg_dom, left, right, frame_boundary=True)
box_dom_nodes = result.box_dom_nodes
box_cod_nodes = result.box_cod_nodes
result.relabel_nodes(copy=False, positions={
n: result.positions[n].shift(y=-0.25) for n in box_dom_nodes})
result.relabel_nodes(copy=False, positions={
n: result.positions[n].shift(y=0.25) for n in box_cod_nodes})
result.graph.remove_edges_from([
(u, v) for u in box_dom_nodes for v in result.box_nodes] + [
(u, v) for u in result.box_nodes for v in box_cod_nodes[1:-1]])
return result
[docs]
@staticmethod
def frame_closing(arg_cod, cod, left, right):
"""
Construct the closing of a frame as the closing of a bubble squashed to
zero height so that it looks like the lower half of a rectangle.
>>> from discopy.monoidal import Ty
>>> x, y, z = map(Ty, "xyz")
>>> Drawing.frame_closing(x, y, z, Ty("")).draw(
... path="docs/_static/drawing/frame-closing.png")
.. image:: /_static/drawing/frame-closing.png
:align: center
"""
result = Drawing.bubble_closing(
arg_cod, cod, left, right, frame_boundary=True)
box_dom_nodes = result.box_dom_nodes
box_cod_nodes = result.box_cod_nodes
result.relabel_nodes(copy=False, positions={
n: result.positions[n].shift(y=-0.25) for n in box_dom_nodes})
result.relabel_nodes(copy=False, positions={
n: result.positions[n].shift(y=0.25) for n in box_cod_nodes})
result.graph.remove_edges_from([
(u, v) for u in box_dom_nodes[1:-1] for v in result.box_nodes] + [
(u, v) for u in result.box_nodes for v in box_cod_nodes])
return result
[docs]
def bubble(self, dom=None, cod=None, name=None,
width=None, height=None, draw_as_square=False) -> Drawing:
"""
Draw a closed line around a drawing, with some wires coming in and out.
Parameters:
dom (monoidal.Ty) : The wires coming into the bubble.
cod (monoidal.Ty) : The wires coming out of the bubble.
name (str) : The label of the bubble, drawn on the top left.
width
>>> from discopy.symmetric import *
>>> a, b, c, d = map(Ty, "abcd")
>>> f = Box('f', a @ b, c @ d).to_drawing()
>>> f.bubble(d @ c @ c, b @ a @ a, name="g").draw(
... path="docs/_static/drawing/bubble-drawing.png")
.. image:: /_static/drawing/bubble-drawing.png
:align: center
"""
dom = self.dom if dom is None else dom
cod = self.cod if cod is None else cod
arg_dom, arg_cod = self.dom, self.cod
left, right = type(dom)(name or ""), type(dom)("")
left[0].always_draw_label = True
wires_can_go_straight = (
len(dom), len(cod)) == (len(arg_dom), len(arg_cod))
if draw_as_square or not wires_can_go_straight:
top = Drawing.frame_opening(dom, arg_dom, left, right)
bot = Drawing.frame_closing(arg_cod, cod, left, right)
else:
top = Drawing.bubble_opening(dom, arg_dom, left, right)
bot = Drawing.bubble_closing(arg_cod, cod, left, right)
middle = self if height is None else self.stretch(height - self.height)
result = top >> left @ middle @ right >> bot
result.make_space(-0.25, 0.25, exclusive=True)
result.make_space(-0.25, result.width - 0.25)
if width is not None and result.width != width:
if result.width > width:
raise ValueError
space = (width - result.width) / 2
result.make_space(space, 0.25, exclusive=True)
result.make_space(space, result.width - 0.25)
if len(dom) == len(arg_dom):
dom_nodes, arg_dom_nodes = ([
Node(kind, x=x, i=i + off, j=0)
for i, x in enumerate(xs)]
for (kind, xs, off) in [
("box_dom", dom, 0), ("box_cod", arg_dom, 1)])
node = Node("box", j=0, box=top.box)
result.graph.add_edges_from(zip(dom_nodes, arg_dom_nodes))
result.graph.remove_edges_from([(x, node) for x in dom_nodes])
result.graph.remove_edges_from([(node, x) for x in arg_dom_nodes])
if len(cod) == len(arg_cod):
arg_cod_nodes, cod_nodes = ([
Node(kind, x=x, i=i + off, j=len(result.boxes) - 1)
for i, x in enumerate(xs)]
for (kind, xs, off) in [
("box_dom", arg_cod, 1), ("box_cod", cod, 0)])
node = Node("box", j=len(result.boxes) - 1, box=bot.box)
result.graph.add_edges_from(zip(arg_cod_nodes, cod_nodes))
result.graph.remove_edges_from([(x, node) for x in arg_cod_nodes])
result.graph.remove_edges_from([(node, x) for x in cod_nodes])
return result
[docs]
def frame(self, *others: Drawing,
dom=None, cod=None, name=None, draw_vertically=False) -> Drawing:
"""
>>> from discopy.monoidal import *
>>> x, y = Ty('x'), Ty('y')
>>> f, g, h = Box('f', x, y ** 3), Box('g', y, y @ y), Box('h', x, y)
>>> f.bubble(dom=x @ x, cod=y @ y, name="b", draw_as_frame=True
... ).draw(path="docs/_static/drawing/single-frame.png")
.. image:: /_static/drawing/single-frame.png
:align: center
>>> Bubble(f, g, h >> h[::-1], dom=x, cod=y @ y
... ).draw(path="docs/_static/drawing/horizontal-frame.png")
.. image:: /_static/drawing/horizontal-frame.png
:align: center
>>> Bubble(f, g, h, dom=x, cod=y @ y, draw_vertically=True
... ).draw(path="docs/_static/drawing/vertical-frame.png")
.. image:: /_static/drawing/vertical-frame.png
:align: center
"""
from discopy.monoidal import Ty
args = (self, ) + others
method = "then" if draw_vertically else "tensor"
params = dict(
width=max([arg.width for arg in args] + [0]) + 1
) if draw_vertically else dict(
height=max([arg.height for arg in args] + [0]))
result = getattr(Drawing.id(), method)(*(arg.bubble(
Ty(), Ty(), draw_as_square=True, **params)
for arg in args)).bubble(dom, cod, name, draw_as_square=True)
result.reposition_box_dom()
result.reposition_box_cod()
return result
def zero(dom, cod):
from discopy.monoidal import Box
result = Box("zero", dom, cod).to_drawing()
result.zero_drawing = True
return result
[docs]
def add(self, other: Drawing, symbol="+", space=1):
""" Concatenate two drawings with a symbol in between. """
from discopy.monoidal import Ty, Box
if getattr(self, "zero_drawing", False):
return other
if getattr(other, "zero_drawing", False):
return self
scalar = Box(symbol, Ty(), Ty(), draw_as_spider=True, color="white")
result = self @ scalar.to_drawing() @ other
result.make_space(space - 1, self.width + 1) # Right of the scalar.
result.make_space(space - 1, self.width) # Left of the scalar.
return result
__add__ = add
def to_drawing(self):
return self
[docs]
class Equation:
"""
An equation is a list of diagrams with a dedicated draw method.
Parameters:
terms : The terms of the equation.
symbol : The symbol between the terms.
space : The space between the terms.
Example
-------
>>> from discopy.tensor import Spider, Swap, Dim, Id
>>> dim = Dim(2)
>>> mu, eta = Spider(2, 1, dim), Spider(0, 1, dim)
>>> delta, upsilon = Spider(1, 2, dim), Spider(1, 0, dim)
>>> special = Equation(mu >> delta, Id(dim))
>>> frobenius = Equation(
... delta @ Id(dim) >> Id(dim) @ mu,
... mu >> delta,
... Id(dim) @ delta >> mu @ Id(dim))
>>> Equation(special, frobenius, symbol=', ').draw(
... aspect='equal', wire_labels=False,
... path='docs/_static/drawing/frobenius-axioms.png')
.. image:: /_static/drawing/frobenius-axioms.png
:align: center
"""
def __init__(self, *terms: "monoidal.Diagram", symbol="=", space=1):
self.terms, self.symbol, self.space = terms, symbol, space
def __repr__(self):
return f"Equation({', '.join(map(repr, self.terms))})"
def __str__(self):
return f" {self.symbol} ".join(map(str, self.terms))
def to_drawing(self):
result = self.terms[0].to_drawing()
for term in self.terms[1:]:
result = result.add(term.to_drawing(), self.symbol, self.space)
return result
[docs]
def draw(self, path=None, **params):
"""
Drawing an equation.
Parameters:
path : Where to save the drawing.
params : Passed to :meth:`discopy.monoidal.Diagram.draw`.
"""
return self.to_drawing().draw(path=path, **params)
def __bool__(self):
return all(term == self.terms[0] for term in self.terms)