rewalt
(archaic) to overturn, throw down
a library for rewriting, algebra, and topology, developed in Tallinn (aka Reval)

rewalt
is a toolkit for higher-dimensional diagram rewriting, with applications in
higher and monoidal category theory,
homotopical algebra,
combinatorial topology,
and more. Thanks to its visualisation features, it can also be used as a structure-aware string diagram editor, supporting TikZ output so the string diagrams can be directly embedded in your LaTeX files.

It implements diagrammatic sets which, by the “higher-dimensional rewriting” paradigm, double as a model of
higher-dimensional rewrite systems, and of
directed cell complexes.
This model is “topologically sound”: a diagrammatic set built in rewalt
presents a finite CW complex, and a diagram constructed in the diagrammatic set presents a valid homotopy in this CW complex.
A diagrammatic set can be seen as a generalisation of a simplicial set or of a cubical set with many more “cell shapes”. As a result, rewalt
also contains a full implementation of finitely presented simplicial sets and cubical sets with connections.
Installation
rewalt
is available for Python 3.7 and higher. You can install it with the command
pip install rewalt
If you want the bleeding edge, you can check out the GitHub repository.
Getting started
To get started, we recommend you check the Notebooks, which contain a number of worked examples from category theory, algebra, and homotopy theory.
Further reading
For a first introduction to the ideas of higher-dimensional rewriting, diagrammatic sets, and “topological soundness”, you may want to watch these presentations at the CIRM meeting on Higher Structures and at the GETCO 2022 conference.
A nice overview of the general landscape of higher-dimensional rewriting is Yves Guiraud’s mémoire d’habilitation.
So far there are two papers on the theory of diagrammatic sets: the first one containing the foundations, the second one containing some developments applied to categorical universal algebra.
A description and complexity analysis of some of the data structures and algorithms behind rewalt
will be published in the proceedings of ACT 2022.
License
rewalt
is distributed under the BSD 3-clause license.
Contributing
Currently, the only active developer of rewalt
is Amar Hadzihasanovic.
Contributions are welcome. Please reach out either by sending me an email, or by opening an issue.
The theory of monoids
In this notebook, we will construct a presentation of the theory of monoids or associative algebras in rewalt
. Depending on your favourite gadget, you may see this as the data presenting a monoidal category (PRO) or an operad.
Adding the sorts and operations
Let’s first import rewalt
and create an empty diagrammatic set — an object of class DiagSet
— that we will call Mon
.
[1]:
import rewalt
Mon = rewalt.DiagSet()
You know how a monoidal category can be seen as a one-object bicategory (its delooping)? This is how we do it in rewalt
too: the sorts of a monoidal theory are 1-cells going to and from a single 0-cell.
So first of all, we add a single 0-dimensional generator to our diagrammatic set.
[2]:
pt = Mon.add('pt')
This adds a 0-dimensional generator to Mon
, assigns it the name 'pt'
and returns the Diagram
object that “picks” that generator only; we assign this diagram to the variable pt
.
Next, we add a single 1-dimensional generator, corresponding to the single sort of our theory.
[3]:
a = Mon.add('a', pt, pt)
The two extra arguments that we gave to add
specify the input, or source boundary of the new generator, and the output, or target boundary of the new generator, respectively. In this case they are both equal to the unique “point”.
By the way, if you fail to assign the output of add
to a variable, you can always retrieve it later by giving the generator’s name to Mon
’s indexer.
[4]:
assert a == Mon['a']
There is not much that we can do with 0-cells… but with 1-cells, we can create larger diagrams by pasting.
The paste
method pastes together diagrams along the k-dimensional output boundary of one and the k-dimensional input boundary of the other, when these match each other.
For a 1-cell, the only non-trivial boundary is the 0-dimensional one; pasting along it corresponds to “concatenation of paths”. We can concatenate a
to itself as many times as we want. Let’s also visualise the result as a “1-dimensional string diagram”.
[5]:
a.draw()

[6]:
a.paste(a).draw()

[7]:
a.paste(a).paste(a).draw()

And so on. Note that paste
can also take an integer argument specifying the dimension of the boundary along which to paste; it defaults to the minimum of the two diagrams’ dimensions, minus 1. In this case the minimum of 1 and 1 is 1, which minus 1 equals 0, and that’s the boundary we want.
Now that we have the sorts, let’s add the operations. The monoid multiplication takes two inputs and returns one output. This corresponds to a 2-dimensional generator, whose input is a.paste(a)
, and output a
.
[8]:
m = Mon.add('m', a.paste(a), a)
And let’s picture this as a string diagram.
[9]:
m.draw()

(As you can see, string diagrams by default go from bottom to top. If you prefer left-to-right, or top-to-bottom, or right-to-left orientation, you can pass it as an argument to draw
; or to change the default setting, reassign rewalt.strdiags.DEFAULT['orientation']
.)
[10]:
m.draw(orientation='lr')

Since we have a single sort, it is a little pointless to label the wires. Same for labelling the unique point. Let’s switch labels off for these generators.
[11]:
Mon.update('a', draw_label=False)
Mon.update('pt', draw_label=False)
m.draw()

Next, we want to add the monoid unit, which is a “nullary” operation. Here things get a little more subtle.
Cells in rewalt
are not allowed to have “strictly lower-dimensional” inputs or outputs: if we try to add a 2-dimensional generator whose input is a 0-dimensional diagram, we will get an error.
[12]:
try:
u = Mon.add('u', pt, a)
except ValueError:
print('Nope')
Nope
Instead, we have to use “weak units”, in the form of degenerate diagrams. (This may seem like a hassle in dimension 2, where “everything can be strictified”, but pays off in higher dimensions.)
A simple constructor for degenerate diagrams is the unit
method, which creates a “unit diagram”, one dimension higher.
[13]:
assert pt.dim == 0
assert not pt.isdegenerate
assert pt.unit().dim == 1
assert pt.unit().isdegenerate
So to add the monoid unit, we make pt.unit()
its input.
In string diagrams, degenerate cells are represented as translucent wires (when wires), or as “node-less nodes” (when nodes).
[14]:
u = Mon.add('u', pt.unit(), a)
u.draw()

Adding “oriented equations”
Now we can compose diagrams with paste
in two directions, along the 0-boundary (“horizontally”) or the 1-boundary (“vertically”)…
[15]:
u.paste(m, 0).draw() # "horizontal" pasting

[16]:
u.paste(m, 0).paste(m).draw() # ...and now "vertical" pasting

A useful alternative to paste
(especially in an “operadic” setting) are the methods to_inputs
and to_outputs
, which allow us to paste a diagram only to some inputs and outputs of another diagram.
To use these in practice, one must know that every node and wire in a string diagram have a unique position. We can use the keyword arguments positions
(both nodes and wires), nodepositions
, and wirepositions
to enable positions in string diagram output.
[17]:
m.draw(positions=True)

Now, we can paste another multiplication either to the input in position 0, or the input in position 1.
[18]:
m.to_inputs(0, m).draw()

[19]:
m.to_inputs(1, m).draw()

These two diagrams happen to be the two sides of the associativity equation, so let’s add this equation to our presentation!
Or rather, we add an oriented associativity equation, or associativity rewrite, or “associator”, as a 3-dimensional generator. All the cells in diagrammatic sets have a direction.
[20]:
assoc = Mon.add('assoc', m.to_inputs(0, m), m.to_inputs(1, m))
assoc.draw()

You can see that, when we draw a 3-dimensional diagram, we obtain a “2-dimensional slice” string diagram, where nodes correspond to 3-cells and wires to 2-cells. (In general, for an n-dimensional diagram, nodes are n-dimensional cells and wires are (n-1)-dimensional cells).
Here, assoc
is a 3-dimensional cell that has two m
2-cells in its input, and two m
2-cells in its output.
To see the two “sides” of the rewrite, we can either use the draw_boundaries
method, or first call input
/output
and only then draw
.
[21]:
assoc.draw_boundaries()


Next, let’s add left unitality and right unitality equations/rewrites. The left-hand side of the left unitality equation is this.
[22]:
m.to_inputs(0, u).draw()

This diagram is supposed to be equal to “the identity operation” on our sort (which would be the unit on a
)… but not quite, because it contains a weak unit in the input; instead we want to equate to another degenerate cell called the left unitor on a
. We build it like this.
[23]:
a.lunitor('-').draw()

The argument '-'
specifies that the unit should appear in the input, and not the output.
Now we can add the “left unitality” generator.
[24]:
lunit = Mon.add('lunit', m.to_inputs(0, u), a.lunitor('-'))
lunit.draw()

We proceed similarly for the “right unitality” generator.
[25]:
runit = Mon.add('runit', m.to_inputs(1, u), a.runitor('-'))
runit.draw()
runit.draw_boundaries()



Making the equations go both ways
That’s it, we now have a presentation of the theory of monoids!
Except our “equations” are really directed rewrites. What if we want to use them in both directions? Luckily, we have methods for “weakly inverting” a generator. Let’s try it on assoc
.
[26]:
Mon.invert('assoc')
[26]:
(<rewalt.diagrams.Diagram at 0x7f72f7faf100>,
<rewalt.diagrams.Diagram at 0x7f72f7faeb60>,
<rewalt.diagrams.Diagram at 0x7f72f843f250>)
This returned 3 diagrams, which corresponds to the fact that 3 new generators were added. Let’s see what happened. We can see a list of the generators, ordered by dimension, with the DiagSet
method by_dim
.
[27]:
Mon.by_dim
[27]:
{0: {'pt'},
1: {'a'},
2: {'m', 'u'},
3: {'assoc', 'assoc⁻¹', 'lunit', 'runit'},
4: {'inv(assoc, assoc⁻¹)', 'inv(assoc⁻¹, assoc)'}}
So, first of all, there’s a new 3-dimensional generator, assoc⁻¹
.
[28]:
Mon['assoc⁻¹'].draw()
Mon['assoc⁻¹'].draw_boundaries()



This is the “weak inverse” of assoc
: a generator with the same boundaries as assoc
, but going in the reverse direction. If a generator has a weak inverse, we can get it with the inverse
attribute.
[29]:
assert assoc.inverse == Mon['assoc⁻¹']
Then, we have two new 4-dimensional generators, inv(assoc, assoc⁻¹)
and inv(assoc⁻¹, assoc)
.
[30]:
Mon['inv(assoc, assoc⁻¹)'].draw()
Mon['inv(assoc, assoc⁻¹)'].draw_boundaries()



This generator “exhibits” the fact that assoc⁻¹
is a right inverse (right in diagrammatic order; left in composition order) for assoc
: it goes from the pasting of assoc
and assoc⁻¹
, to a weak unit on the input of assoc
.
We call this a right invertor for assoc
, and can get it with the rinvertor
attribute.
Similarly, inv(assoc, assoc⁻¹)
exhibits the fact that assoc⁻¹
is a left inverse for assoc
. We call this a left invertor for assoc
, and can retrieve it with the linvertor
attribute.
Note that the left invertor for assoc
is the right invertor for assoc⁻¹
, and vice versa!
[31]:
assert assoc.rinvertor == Mon['inv(assoc, assoc⁻¹)']
assert assoc.linvertor == assoc.inverse.rinvertor
In the theory of diagrammatic sets, these two “witnesses” should, themselves, be weakly invertible cells; since this would require an infinite number of generators, we leave it to the user to invert them when/if needed.
[32]:
Mon['inv(assoc⁻¹, assoc)'].draw()
Mon['inv(assoc⁻¹, assoc)'].draw_boundaries()



Computing with diagrammatic rewrites
Let’s start using our presentation to make some diagrammatic computations. First, we create a 2-dimensional diagram.
[33]:
start = m.to_inputs(0, m).to_inputs(0, m)
start.draw(nodepositions=True)

In traditional algebraic notation, this would correspond to the term \(m(m(m(x, y), z), w)\).
We see that we can apply an associativity rewrite/equation in two places, corresponding to the nodes in positions (0, 1) and to the nodes in positions (1, 2).
We can “apply rewrites” with the rewrite
method. The result of rewrite
is not going to be the “rewritten” 2-dimensional diagram. Instead, it will be a 3-dimensional diagram whose input is the original diagram, and output is the rewritten diagram: an “embodiment” of the rewrite operation.
(The rewrite
method is, in fact, a special instance of to_outputs
; once you understand the principles of higher-dimensional rewriting, you should be able to see why).
[34]:
rew1 = start.rewrite([0,1], assoc)
rew1.draw()
rew1.output.draw(nodepositions=True)


In the rewritten diagram, we can only apply assoc
to the nodes (0, 2).
[35]:
rew2 = rew1.output.rewrite([0, 2], assoc)
rew2.output.draw(nodepositions=True)

Now, we can apply assoc
to the nodes (1, 2).
[36]:
rew3 = rew2.output.rewrite([1, 2], assoc)
rew3.output.draw()

We cannot apply assoc
anywhere else. (Of course we could start applying assoc⁻¹
).
Let’s put together our sequence of rewrites.
[37]:
seq1 = rewalt.Diagram.with_layers(rew1, rew2, rew3)
seq1.draw()

(We could have equally defined seq1
as rew1.paste(rew2).paste(rew3)
).
We can use the method rewrite_steps
to get all our rewrite steps… and we can even produce a little gif animation with all the steps. (We’ll make it loop backwards as well so it doesn’t end too soon.)
[38]:
rewalt.strdiags.to_gif(*seq1.rewrite_steps, loop=True, path='monoids_1.gif')
Let’s go back to the start
and pick a different rewrite, the one on nodes (1, 2).
[39]:
rew4 = start.rewrite([1, 2], assoc)
rew4.output.draw(nodepositions=True)

[40]:
rew5 = rew4.output.rewrite([0, 2], assoc)
rew5.output.draw()

[41]:
seq2 = rew4.paste(rew5)
seq2.draw()
rewalt.strdiags.to_gif(*seq2.rewrite_steps, loop=True, path='monoids_2.gif')

You can see that seq1
and seq2
are two different sequences of rewrites with the same starting and ending point.
If you are familiar with the characterisation of monoidal categories as pseudomonoids in the monoidal 2-category of categories with cartesian product, you may recognise the two sides of Mac Lane’s pentagon equation!
Indeed, we can add a 4-dimensional generator between the two, embodying Mac Lane’s pentagon.
[42]:
pentagon = Mon.add('pentagon', seq1, seq2)
pentagon.draw()
pentagon.draw_boundaries()



We could go on and add generators corresponding to Mac Lane’s triangle… but this was supposed to be about the theory of monoids, not of lax or pseudomonoids, so let’s stop here instead.
Generating string diagrams
For any higher-dimensional diagram that we can create in rewalt
, we can output a string diagram representation both as an image (with the Matplotlib backend), or as TikZ code that we can include in our LaTeX files.
Thus, one of the intended applications of rewalt
is also to be a structure-aware, type-aware string diagram generator: we can build our string diagrams the way we build the morphisms/homotopies/operations/rewrites that they represent, and let rewalt
do the typesetting for us.
In this notebook, we will work out one example, and explore the customisation options that we have.
Note that the placement and general style of nodes and wires is not currently customisable (except for the choice of orientation). However, rewalt
is open source software and everyone is welcome to modify the algorithm to suit their aesthetic preferences.
A presentation of adjunctions
As an example, we will construct a presentation of the “theory of adjunctions”, or “walking adjunction”, whose models in a bicategory are adjunctions internal to that bicategory. (This has “dualities in monoidal categories” as a special case.) The triangle/zigzag/snake equations of adjunctions are some of the most well-known and recognisable in string diagrams.
The theory of adjunctions has two 0-cells and two 1-cells between them, going in opposite directions.
[1]:
import rewalt
Adj = rewalt.DiagSet()
x = Adj.add('x')
y = Adj.add('y')
l = Adj.add('l', x, y)
r = Adj.add('r', y, x)
Then, we need to add two 2-cells, the unit and counit of the adjunction.
[2]:
eta = Adj.add('η', x.unit(), l.paste(r)) # unit
eps = Adj.add('ε', r.paste(l), y.unit()) # counit
This is how rewalt
draws the unit and counit by default.
[3]:
eta.draw()
eps.draw()


We can use the picture as a visual aid to see how to paste the unit and counit together to get the left-hand side of the triangle equations. For example, if we add an l
to the right of eta
…
[4]:
eta.paste(l).draw(wirepositions=True)

… we can plug an eps
to the wires in positions (3, 1).
[5]:
lhs1 = eta.paste(l).to_outputs([3, 1], eps)
lhs1.draw()

This needs to be equated to “the identity on l
”, except we have weak units on x
in the input and on y
in the output.
We can in fact obtain the degenerate 2-cell with the right type as one of the cubical degeneracies on l
.
[6]:
rhs1 = l.cube_degeneracy(1)
rhs1.draw()

We can now add our first “oriented equation”.
[7]:
eq1 = Adj.add('eq1', lhs1, rhs1)
eq1.draw()

For the second one, we can proceed symmetrically. We add an r
to the left of eta
…
[8]:
r.paste(eta).draw(wirepositions=True)

… and we plug an eps
to the wires in positions (0, 2) to get the left-hand side of the second equation.
[9]:
lhs2 = r.paste(eta).to_outputs([0, 2], eps)
lhs2.draw()

To get the right-hand-side, we use a different cubical degeneracy on r
.
[10]:
rhs2 = r.cube_degeneracy(0)
rhs2.draw()

And finally, we add the second triangle equation.
[11]:
eq2 = Adj.add('eq2', lhs2, rhs2)
eq2.draw()

That’s it, we have a presentation. (We could also invert eq1
and eq2
but that’s besides the point of this exercise).
Customising string diagrams
Let’s return to the first triangle equation. The default string diagram representation of its left-hand side is this.
[12]:
eq1.input.draw()

Let’s make it a bit nicer.
First of all, it is quite common to draw units and counits as “bent wires” (aka “cups and caps”), without a node, so that the triangle equations look like topological trivialities.
We can do this by disabling node drawing for these generators of Adj
.
[13]:
Adj.update('ε', draw_node=False)
Adj.update('η', draw_node=False)
eq1.input.draw()

Then, since we have only two 1-cells, why not also colour-code them?
[14]:
Adj.update('l', color='blue')
Adj.update('r', color='magenta')
eq1.input.draw()

When we are working in rewalt
, it is good to see the weak units, because we need to take them into account to know that everything typechecks.
However, we may want to “hide them away” if, for example, our diagrams are to be interpreted in a strict 2-category. We can do this by changing the alpha factor for degenerate wires to 0.
[15]:
eq1.input.draw(degenalpha=0)

Note that this still shows the weak unit labels, which is actually helpful in this setting because it reminds us of the type of l
and r
. If we wanted to get rid of them, we could deactivate labels for these generators.
[16]:
Adj.update('x', draw_label=False)
Adj.update('y', draw_label=False)
eq1.input.draw(degenalpha=0)

[17]:
Adj.update('x', draw_label=True)
Adj.update('y', draw_label=True)
There are different factions on what the “correct” orientation of string diagrams is. In rewalt
, the default is bottom-to-top, but it can be changed.
[18]:
eq1.input.draw(degenalpha=0, orientation='lr')
eq1.input.draw(degenalpha=0, orientation='rl')
eq1.input.draw(degenalpha=0, orientation='tb')



We can change the default settings by reassigning the values of rewalt.strdiags.DEFAULT
. Let’s say we want all our string diagrams to be top-to-bottom with no degenerate wires.
[19]:
rewalt.strdiags.DEFAULT['orientation'] = 'tb'
rewalt.strdiags.DEFAULT['degenalpha'] = 0
Now, how about a dark theme?
[20]:
Adj.update('l', color='cyan')
eq1.input.draw(bgcolor='0.2', fgcolor='white')

Let’s see what the sides of our two triangle equations look like now.
[21]:
eq1.draw_boundaries(bgcolor='0.2', fgcolor='white')
eq2.draw_boundaries(bgcolor='0.2', fgcolor='white')




If we are happy with the look, we can output TikZ code. Note that both labels and colour settings are passed to the TikZ output as they are, so we should change the background colour setting to something that LaTeX can recognise.
TikZ output uses coordinates in \([0, 1] \times [0, 1]\). Since this is quite small, the output is scaled 3x by default; this can be changed with the scale
, xscale
, and yscale
keyword arguments.
Also, by default, all wires are drawn with a contour, which is useful in higher dimensions when wires can overlap. Since we are in 2d and this doesn’t happen, we can avoid drawing contours by setting the depth
keyword argument to False
.
[22]:
eq1.input.draw(
bgcolor='darkgray', fgcolor='white', depth=False,
tikz=True, xscale=8, yscale=6, path='stringdiagrams_1.tex')
Here’s the generated TikZ code and the output PDF compiled with pdflatex.
Fun with higher-dimensional shapes
We can have string diagram representations not only of “diagrams in a DiagSet
”, but also of shapes and maps of shapes of diagrams.
For example, this is the shape of the diagram we have been using as example.
[23]:
eq1.input.shape.draw()

Every wire and node corresponds to a unique face of the diagram shape, specified by its dimension (2 for nodes, 1 for wires) and position. We can match them to elements of the oriented face poset of the diagram shape.
[24]:
eq1.input.shape.hasse(labels=False)

A quick way to get some interesting higher-dimensional diagrams, and see some of the things that happen with string diagram representations in higher dimensions, is to use some of the constructors for special higher-dimensional shapes, such as simplices and cubes.
For example, these are the string diagrams for the 3-dimensional boundaries of the oriented 4-cube.
[25]:
tesseract = rewalt.Shape.cube(4)
tesseract.draw_boundaries(labels=False)


You can see that wires can cross each other in 3-dimensional diagrams.
For something even more complicated, let’s look at a cubical connection map on the 4-cube, which is a surjective map from the 5-cube.
(Since this will contain many degenerate cells, we will reinstate weak units in string diagrams.)
[26]:
connection = tesseract.cube_connection(1, '-')
connection.draw_boundaries(labels=False, degenalpha=0.1)


And let’s play a little bit with colours.
[27]:
connection.draw_boundaries(
labels=False, bgcolor='0.2', fgcolor='0.9', degenalpha=0.4,
nodecolor='gold', nodestroke='white')


Looks nice, no?
Exploring simplices and cubes
Diagrammatic sets — the structure implemented by rewalt
’s DiagSet
class — support a wide variety of “shapes of diagrams”, while remaining “topologically sound”. This makes them a convenient tool for diagrammatic reasoning in higher category, higher algebra, and homotopy theory.
Among these shapes are some subclasses that are widely used on their own: in particular, the simplices and the cubes. Indeed, both simplicial sets and cubical sets with connections are special instances of diagrammatic sets (their categories are full subcategories of the category of diagrammatic sets).
Reflecting this, rewalt
contains a full implementation of (finitely presented) simplicial sets and of (finitely presented) cubical sets with connections. These are nothing more than diagrammatic sets whose generators all have simplicial and cubical shapes! The Diagram
objects that have simplicial or cubical shapes come with special methods for constructing simplicial and cubical faces, degeneracies, and connections.
Since all our shapes have a “globular” orientation (half a boundary is “input”, half a boundary is “output”), our simplices are in fact Street’s oriented simplices. Similarly our cubes are “oriented” as in cubical ω-categories.
Understanding higher-dimensional oriented simplices and cubes can be difficult. In this notebook, we will try to use rewalt
and its visualisation methods to get a grip on some low-but-not-too-low-dimensional examples.
Oriented simplices
Oriented simplices of any dimension are built with the Shapes.simplex
constructor. Let’s start with the lowest possible dimension: -1.
[1]:
import rewalt
empty = rewalt.Shape.simplex(-1)
This is just the empty diagram shape.
[2]:
len(empty)
[2]:
0
The 0-dimensional simplex is a point.
[3]:
point = rewalt.Shape.simplex(0)
point.draw()

The 1-dimensional oriented simplex is an arrow.
[4]:
arrow = rewalt.Shape.simplex(1)
arrow.draw()

Things get a little more interesting in dimension 2. The oriented 2-simplex is a triangle with two output sides and one input side. In string diagrams, it is, for example, the shape of a comonoid comultiplication.
[5]:
triangle = rewalt.Shape.simplex(2)
triangle.draw()

Let’s go one dimension higher. The oriented 3-simplex is a tetrahedron with two output faces and two input faces, each of them shaped as an oriented 2-simplex.
Let’s draw both its top-dimensional “slice” string diagram, and its input and output boundaries.
[6]:
tetrahedron = rewalt.Shape.simplex(3)
tetrahedron.draw()

[7]:
tetrahedron.draw_boundaries()


If we stick to the interpretation of the oriented 2-simplex as “the shape of a comultiplication”, then the oriented 3-simplex is “the shape of a (co)associativity equation”, or “the shape of a coassociator”!
What happens if we go to dimension 4?
[8]:
pentachoron = rewalt.Shape.simplex(4)
pentachoron.draw()

This is a pentachoron, also known as the 5-cell, with three output tetrahedral faces and two input tetrahedral faces.
Let’s see what its boundaries look like, starting from the input.
[9]:
penta_input = pentachoron.input
penta_input.draw()

This is a slice of a 3-dimensional diagram with two 3-dimensional cells.
This is still hard to visualise directly in three dimensions; instead, we are going to try to visualise it as a sequence of rewrites on 2-dimensional diagrams.
For that purpose, we use the generate_layering
method, which creates a “layering” of a diagram into a sequence of rewrites, one for each one of its top-dimensional cells. Then, we can
get a list of the layers with the
layers
attribute, orget a list of the corresponding “rewrite steps” with the
rewrite_steps
attribute.
[10]:
penta_input.generate_layering()
rewalt.strdiags.draw(*penta_input.layers)


[11]:
rewalt.strdiags.draw(*penta_input.rewrite_steps)



So, we can see that
first the 3-dimensional face
El(3, 0)
“rewrites” the trianglesEl(2, 0)
andEl(2, 1)
into the trianglesEl(2, 3)
andEl(2, 4)
,then the 3-dimensional face
El(3, 1)
“rewrites” the trianglesEl(2, 2)
andEl(2, 3)
into the trianglesEl(2, 5)
andEl(2, 6)
.
We can also create a gif “movie” of the rewrite steps (and make it loop backwards so it doesn’t stop too soon).
[12]:
rewalt.strdiags.to_gif(
*penta_input.rewrite_steps,
loop=True, path='simplicescubes_1.gif')
Now, let’s look at the output boundary of the oriented 4-simplex.
[13]:
penta_output = pentachoron.output
penta_output.draw()

This is the slice of a 3-dimensional diagram with three 3-dimensional cells. Let’s proceed as with the input.
[14]:
penta_output.generate_layering()
rewalt.strdiags.draw(*penta_output.layers)



[15]:
rewalt.strdiags.draw(*penta_output.rewrite_steps)




Let’s also make a movie of these.
[16]:
rewalt.strdiags.to_gif(
*penta_output.rewrite_steps, loop=True,
path='simplicescubes_2.gif')
The two sides of the oriented 4-simplex are, in fact, the two sides of an equation dual to Mac Lane’s pentagon. This was featured at the end of this other notebook.
Maps of simplices
So far we have only looked at the oriented simplices “in isolation”. Let’s see how we can use rewalt
to understand their face and degeneracy maps.
Faces are quite simple; let’s look at the example of the 2-simplex. This has 3 faces.
[17]:
triangle.draw()
for n in range(3):
triangle.simplex_face(n).draw()




By comparing labels, we can see that
the 0th face of the 2-simplex is the rightmost output,
the 1st face of the 2-simplex is the only input, and
the 2nd face of the 2-simplex is the leftmost output.
In general, the faces of an oriented simplex alternate between inputs and outputs, always starting with an output at index 0.
Let’s look at degeneracies; these are somewhat more interesting. There are two degeneracies on the 1-simplex.
[18]:
arrow.draw()
for n in range(2):
arrow.simplex_degeneracy(n).draw()



The two diagrams represent two surjective (“collapsing”) maps from the 2-simplex to the 1-simplex. The string diagrams tell us that
the 0th degeneracy sends the 2-cell, its input, and the rightmost output of the 2-simplex onto the 1-cell of the 1-simplex, and collapses the leftmost output onto its input 0-cell;
the 1st degeneracy sends the 2-cell, its input, and the leftmost output of the 2-simplex onto the 1-cell of the 1-simplex, and collapses the rightmost output onto its output 0-cell.
Now, let’s take a look at one degeneracy of the 2-simplex.
[19]:
triangle.simplex_degeneracy(0).draw()

This represents a collapsing map from the 3-simplex onto the 2-simplex; the string diagram tells us which input and which output of the 3-simplex are collapsed, and which are sent to the 2-cell of the 2-simplex.
Let’s obtain some more information by looking at the boundaries.
[20]:
triangle.simplex_degeneracy(0).draw_boundaries()


This tells us exactly how the two collapsed 2-dimensional faces of the 3-simplex are collapsed: we can tell that, in both cases, it is the leftmost output that is collapsed, hence the 0-th degeneracy of the 1-simplex is used.
By the way, if we want a precise (but not very intuitive) description of a map, we can use the Hasse diagram visualisation:
[21]:
triangle.simplex_degeneracy(0).hasse()

This shows us the “oriented face poset” of the source of the map — here, the 3-simplex — with each element labelled with its image through the map. For example, the third element of the third row from the bottom is labelled with El(1, 0)
; this means that the map sends El(2, 2)
to El(1, 0)
(we are counting from 0).
Constructing a simplicial set
Let’s briefly look at how we can use rewalt
to construct a simplicial set. As a simple example, we will construct the 3-dimensional real projective space \(\mathbb{R}P^3\), with its cell structure made up of a single cell in each dimension.
The first step is to create an empty diagrammatic set.
[22]:
RP3 = rewalt.DiagSet()
To ensure that this is really a simplicial set, we only add generators with the add_simplex
method, taking, as arguments, the simplicial faces of the new generator in the same order as given by simplex_face
.
(In dimension 0 and 1, there’s no substantial difference between add
and add_simplex
).
[23]:
c0 = RP3.add_simplex('c0')
c1 = RP3.add_simplex('c1', c0, c0)
We construct degenerate simplices over the generators with the simplex_degeneracy
method.
[24]:
c2 = RP3.add_simplex('c2', c1, c0.simplex_degeneracy(0), c1)
c2.draw()

[25]:
c3 = RP3.add_simplex(
'c3',
c2, c1.simplex_degeneracy(0), c1.simplex_degeneracy(1), c2)
c3.draw()

[26]:
c3.draw_boundaries()


There we go; RP3
is now a simplicial model of the 3-dimensional real projective space. We can check that this is “really” a simplicial set:
[27]:
RP3.issimplicial
[27]:
True
In future releases, we plan to add features that will allow us to automatically compute some topological invariants of cell complexes constructed as DiagSet
objects.
Oriented cubes
Let’s move on from simplices to cubes; these can be obtained with the Shape.cube
constructor. Unlike in simplices, there is no (-1)-cube. The 0-cube and the 1-cube are, in fact, the same as the 0-simplex and the 1-simplex.
[28]:
assert point == rewalt.Shape.cube(0)
assert arrow == rewalt.Shape.cube(1)
So the first interesting case is the oriented 2-cube: this is a square with two output faces and two input faces.
[29]:
square = rewalt.Shape.cube(2)
square.draw()

Next, the oriented 3-cube has three output faces and three input faces. (In fact, the oriented n-cube always has n inputs and n outputs.)
[30]:
cube = rewalt.Shape.cube(3)
cube.draw()

[31]:
cube.draw_boundaries()


You may see the 2-dimensional boundaries of the oriented 3-cube, in string diagrams, as the shapes of the two sides of the Yang-Baxter equation, or the two sides of the third Reidemeister move.
Let’s move on to the 4-dimensional cube.
[32]:
tesseract = rewalt.Shape.cube(4)
tesseract.draw()

As expected, it has four input faces and four output faces. Let’s proceed as we did with the 4-simplex to understand what is happening.
[33]:
tess_input = tesseract.input
tess_input.draw(wirelabels=False)

(We have deactivated wire labels to make the image less crowded.)
[34]:
tess_input.generate_layering()
rewalt.strdiags.draw(*tess_input.layers)




[35]:
rewalt.strdiags.draw(*tess_input.rewrite_steps, wirelabels=False)





Now we turn the sequence of rewrite steps into a gif.
[36]:
rewalt.strdiags.to_gif(
*tess_input.rewrite_steps, loop=True,
wirelabels=False,
path='simplicescubes_3.gif')
Next we focus on the output of the 4-cube.
[37]:
tess_output = tesseract.output
tess_output.draw(wirelabels=False)

[38]:
tess_output.generate_layering()
rewalt.strdiags.draw(*tess_output.layers)




[39]:
rewalt.strdiags.to_gif(
*tess_output.rewrite_steps, loop=True,
wirelabels=False,
path='simplicescubes_4.gif')
In the two rewrite sequences corresponding to the input and output boundary of the 4-cube, you may recognise the shapes of the two sides of the Zamolodchikov tetrahedron equation.
(Why “tetrahedron equation” if its shape is a 4-cube? Not sure!)
Maps of cubes
In contrast to simplices, faces of cubes are specified by two arguments: thinking of the n-cube as \([0, 1]^n\), one argument is an integer ranging from 0 to (n-1), specifying which coordinate to fix, and the other is a bit (for us, a sign: '-'
or '+'
) specifying whether to set the coordinate to 0 or to 1.
[40]:
for n in range(2):
for sign in ('-', '+'):
square.cube_face(n, sign).draw()




Cubes also have two different kinds of “collapse” maps:
degeneracies, which collapse the cube along a single coordinate (specified by an integer argument), and
connections, which “fold” the cube along a pair of consecutive coordinates (specified by an integer argument), in two different ways (specified by a “sign” argument).
In rewalt
, we can get a string-diagrammatic picture of these collapse maps.
[41]:
for n in range(2):
arrow.cube_degeneracy(n).draw()


[42]:
for sign in ('-', '+'):
arrow.cube_connection(0, sign).draw()


As we saw in another notebook, being familiar with these degeneracies, which are neither “units” or “unitors”, can be handy when constructing presentations of monoidal or higher algebraic theories.
Constructing a cubical set
Constructing a cubical set with connections is just like constructing a simplicial set, except we use the add_cube
method instead of the add_simplex
method when adding generators.
Let’s construct a simple cubical model of the torus, with one 0-cell, two 1-cells, and one 2-cell.
[43]:
T = rewalt.DiagSet()
pt = T.add_cube('pt')
a = T.add_cube('a', pt, pt)
b = T.add_cube('b', pt, pt)
s = T.add_cube('s', a, a, b, b)
s.draw()

That’s all! T
is a torus.
We can check that the diagrammatic set we constructed is, indeed, a cubical set:
[44]:
T.iscubical
[44]:
True
Notice that if we look at this diagrammatic set as string rewrite system instead, it is a presentation of the free commutative monoid on the 2 generators a
and b
. Of course, the free abelian group on two generators is the first homology group of the torus.
Mixing them together
One of the reasons why simplices and cubes are “nice” families of shapes is that both are generated by the iteration of a binary operation, which defines a monoidal structure on their respective shape categories:
simplices are iterated joins of points;
cubes are iterated products of intervals.
In fact, both joins and products have “oriented” counterparts, and all shapes of rewalt
are closed under both of these operations:
the join of shapes, accessed either with the
join
method, or with the shift operators>>
and<<
, andthe Gray product of shapes, accessed either with the
gray
method, or with the multiplication operator*
.
Indeed, this is how rewalt
constructs oriented simplices and oriented cubes.
[45]:
assert arrow == point >> point
assert triangle == arrow >> point
assert square == arrow * arrow
assert cube == arrow * square
Joins are useful, for instance, for constructing cones, while products are useful for constructing cylinders. So the first operation is natural in a simplicial context, but not in a cubical context; while the second operation is natural in a cubical context but not in a simplicial context.
One nice thing about diagrammatic sets is that we do not need to choose! We can build a cylinder on a simplex…
[46]:
cylinder = arrow * triangle
cylinder.draw()

[47]:
cylinder.draw_boundaries()


… and we can build a cone on a cube.
[48]:
cone = square >> point
cone.draw()

[49]:
cone.draw_boundaries()


The Eckmann–Hilton argument
A nice theoretical feature of rewalt
is “topological soundness”: a diagrammatic set can be geometrically realised as a CW complex with one cell for each of its generators, and every diagram that we construct in the diagrammatic set corresponds to a valid homotopy in its realisation.
One of the first non-trivial homotopies that one encounters in algebraic topology are the “braiding” homotopies between two 2-cells, exhibiting the fact that \(\pi_2\) of a space is always an abelian group. The construction of these homotopies is known as Eckmann–Hilton argument, and is also the basis of the identification of braided monoidal categories with “doubly degenerate” tricategories.
In this notebook, we will implement the Eckmann–Hilton argument in rewalt
, by constructing both homotopies in a diagrammatic set with a single 0-dimensional generator and two 2-dimensional generators. Thanks to topological soundness, you can also see this as a formal proof of the usual homotopical Eckmann–Hilton.
First of all, let’s create a diagrammatic set, and add all the generators. We will colour-code the two 2-cells, one in blue and one in magenta.
[1]:
import rewalt
EH = rewalt.DiagSet()
pt = EH.add('pt', draw_label=False)
a = EH.add('a', pt.unit(), pt.unit(), color='blue')
b = EH.add('b', pt.unit(), pt.unit(), color='magenta')
First braiding
The “braiding homotopies” will be made of degenerate cells, starting from the pasting “b
after a
”, and ending in the pasting “a
after b
”.
Our construction of these homotopies will be, essentially, an implementation of the “train tracks” proof by André Joyal and Joachim Kock. Let’s start from the beginning.
[2]:
start = a.paste(b)
start.draw(nodepositions=True)

Let’s introduce some weak units between a
and b
; one would be sufficient, but we’ll do two for reasons of symmetry.
[3]:
rew1 = start.rewrite(0, a.runitor('+'))
rew1.output.draw(nodepositions=True)

[4]:
rew2 = rew1.output.rewrite(2, b.lunitor('+'))
rew2.output.draw(nodepositions=True)

Now, we want to “split” the units in positions (1, 2) into two “train tracks”. This can be done with a “fully degenerate” cell over pt
, of the appropriate shape:
[5]:
globe = rewalt.Shape.globe(2)
triangle = rewalt.Shape.simplex(2)
track_split_shape = globe.paste(globe).atom(triangle.paste(triangle.dual()))
track_split_shape.draw_boundaries()


You can see that track_split_shape
is a 3-dimensional shape with input and output of the shape we desire, going from “single track” (pasting of two 2-globes) to “double track” (pasting of a 2-simplex with its dual).
To get a “fully degenerate” cell over pt
of shape track_split_shape
, we use the degeneracy
method.
[6]:
track_split = pt.degeneracy(track_split_shape)
track_split.draw_boundaries()


[7]:
rew3 = rew2.output.rewrite([1, 2], track_split)
rew3.output.draw(nodepositions=True)

Now, our goal is to “move a
to the right track, and move b
to the left track”. This can be done with appropriate degenerate cells over a
and b
.
These degenerate cells are neither units or unitors. However, just like units and unitors, they can be obtained from pullbacks of a
and b
over particular collapse maps from a “partially collapsed cylinder” on their shape, as provided by the inflate
method of the Shape
class.
(I do not expect that this is particulary intuitive; you should try fiddling with inflate
to get an idea of the collapses you can get.)
This, for example, is the map we can use to move a
from the bottom to the right track.
[8]:
switch_br_map = globe.inflate(globe.all().boundary('+', 0))
switch_br_map.draw_boundaries()


Every other “switch” map we will get as a dual of this one. For example, the “top-to-left” that we need for b
is the dual in dimensions 1 and 2 (“horizontal and vertical flip”).
[9]:
switch_tl_map = switch_br_map.dual(1, 2)
switch_tl_map.draw_boundaries()


[10]:
a_switch_br = a.pullback(switch_br_map)
rew4 = rew3.output.rewrite([0, 1], a_switch_br)
rew4.output.draw(nodepositions=True)

[11]:
b_switch_tl = b.pullback(switch_tl_map)
rew5 = rew4.output.rewrite([1, 3], b_switch_tl)
rew5.output.draw(nodepositions=True)

Now we will move a
to the top, then b
to the bottom. For that, we use pullbacks along other duals of our original “switch” map.
[12]:
switch_rt_map = switch_br_map.dual(2, 3)
a_switch_rt = a.pullback(switch_rt_map)
rew6 = rew5.output.rewrite([2, 3], a_switch_rt)
rew6.output.draw(nodepositions=True)

[13]:
switch_lb_map = switch_br_map.dual(1, 3)
b_switch_lb = b.pullback(switch_lb_map)
rew7 = rew6.output.rewrite([0, 1], b_switch_lb)
rew7.output.draw(nodepositions=True)

The relative positions of a
and b
have been exchanged! Now we only need to get rid of the “train tracks” and other units between them.
We used degenerate cells to introduce them, and degenerate cells are always “weakly invertible”, so we can just use their “weak inverses”, obtained with the inverse
method.
[14]:
rew8 = rew7.output.rewrite([1, 2], track_split.inverse)
rew8.output.draw(nodepositions=True)

[15]:
rew9 = rew8.output.rewrite([0, 1], b.runitor('-'))
rew9.output.draw(nodepositions=True)

[16]:
rew10 = rew9.output.rewrite([1, 2], a.lunitor('-'))
rew10.output.draw(nodepositions=True)

We are done! Let’s put all our rewrites together, and see what our proof looks like as a slice of a 3-dimensional diagram.
[17]:
eh1 = rewalt.Diagram.with_layers(
rew1, rew2, rew3, rew4, rew5, rew6, rew7, rew8, rew9, rew10)
eh1.draw()

See? It’s a braiding where the b
strand is passing over the a
strand.
We can also assemble all our rewrites into a gif animation. We will also make it loop backwards.
[18]:
rewalt.strdiags.to_gif(
*eh1.rewrite_steps, degenalpha=0.2,
loop=True, path='eckmannhilton_1.gif')
Second braiding
In our proof, we made the choice of moving a
onto the right track, and b
onto the left track; but we might as well have made a different choice. This would have led to a non-equivalent homotopy, the dual braiding.
Let’s go back to the step where we had the choice, and make a different one. This corresponds to “horizontally flipping” all the maps we used the first time.
[19]:
rew3.output.draw(nodepositions=True)

[20]:
switch_bl_map = switch_br_map.dual(1)
a_switch_bl = a.pullback(switch_bl_map)
rew4d = rew3.output.rewrite([0, 1], a_switch_bl)
rew4d.output.draw(nodepositions=True)

[21]:
switch_tr_map = switch_tl_map.dual(1)
b_switch_tr = b.pullback(switch_tr_map)
rew5d = rew4d.output.rewrite([2, 3], b_switch_tr)
rew5d.output.draw(nodepositions=True)

[22]:
switch_lt_map = switch_rt_map.dual(1)
a_switch_lt = a.pullback(switch_lt_map)
rew6d = rew5d.output.rewrite([1, 3], a_switch_lt)
rew6d.output.draw(nodepositions=True)

[23]:
switch_rb_map = switch_lb_map.dual(1)
b_switch_rb = b.pullback(switch_rb_map)
rew7d = rew6d.output.rewrite([0, 2], b_switch_rb)
rew7d.output.draw(nodepositions=True)

That’s it; the last few steps are the same as the first time. Let’s put the whole sequence together.
[24]:
eh2 = rewalt.Diagram.with_layers(
rew1, rew2, rew3, rew4d, rew5d, rew6d, rew7d, rew8, rew9, rew10)
eh2.draw()

See? Now it is the blue (a
) strand that crosses over the magenta (b
) strand.
And let’s make another animation.
[25]:
rewalt.strdiags.to_gif(
*eh2.rewrite_steps, degenalpha=0.2,
loop=True, path='eckmannhilton_2.gif')
The diagrams eh1
and eh2
have the same input and output; they could, in principle, be the input and output of another cell.
By topological soundness, however, we know that there isn’t a diagram between eh1
and eh2
: the geometric realisation of EH
is a bouquet of two 2-spheres, and in this space there isn’t a homotopy between the two “braidings”.
You are welcome to add one by hand, if you really want.
[26]:
symmetriser = EH.add('symmetriser', eh1, eh2)
symmetriser.draw()

Presenting a category
The “higher-dimensional rewrite systems” that we construct in rewalt
are interpretable in higher-dimensional categories, but they are, in general, different from higher-dimensional categories, in that they have no notion of composition of diagrams; that is, there’s no way, in general, to “turn a diagram with many n-cells into a single n-cell”.
Nevertheless, rewalt
contains an implementation of a model of higher categories, in the form of diagrammatic sets with weak composites. This allows us to “declare” a cell to be the composite of a diagram; the composition is exhibited by a higher-dimensional compositor cell.
In this notebook, we will use the dedicated methods to construct a presentation of a simple finite category, consisting of a commuting square of four morphisms.
Adding all objects and morphisms
We start by creating an empty DiagSet
, and adding all the objects and morphisms of our category. We have four objects (0-generators).
[1]:
import rewalt
C = rewalt.DiagSet()
x0 = C.add('x0')
x1 = C.add('x1')
x2 = C.add('x2')
x3 = C.add('x3')
Then we add the four morphisms (1-generators) that form the boundary of our commuting square.
[2]:
f0 = C.add('f0', x0, x1)
f1 = C.add('f1', x1, x3)
g0 = C.add('g0', x0, x2)
g1 = C.add('g1', x2, x3)
Now we have two parallel diagrams of two 1-cells: f0.paste(f1)
and g0.paste(g1)
. We add the “diagonal” morphism that will be the composite of both diagrams.
[3]:
h = C.add('h', x0, x3)
That’s it; now we move on to the compositors.
Adding compositors
We declare a generator to be the “weak composite” of a diagram with the make_composite
method. This will add a “compositor” 2-cell, and return it as a Diagram
object.
[4]:
c_f = C.make_composite('h', f0.paste(f1))
c_f.draw()

[5]:
c_g = C.make_composite('h', g0.paste(g1))
c_g.draw()

We can check that a diagram has a composite with the hascomposite
attribute; if a diagram has a composite, we can retrieve it with the composite
attribute.
[6]:
f0.paste(f1).hascomposite
[6]:
True
[7]:
f0.paste(f1).composite == h
[7]:
True
A compositor allows us to rewrite a diagram into a cell. Now, according to the theory, to exhibit a genuine weak composite, the compositor would need to be weakly invertible.
As we saw in another notebook, since weak invertibility requires an infinite “tower” of cells, the approach of rewalt
is to “invert only when needed”. That also applies to compositors, which are created in “one direction only”, and must be explicitly inverted if needed.
(Another reason to not invert by default is that one may want to use DiagSet
objects to implement different kinds of higher structures, such as representable multicategories or “lax” versions thereof, where it is important that compositors only go “one way”.)
[8]:
c_f_inv, c_f_rinvertor, c_f_linvertor = C.invert(c_f)
c_f_inv.draw()

Now that we have an inverse compositor, we can “rewrite” g0.paste(g1)
into f0.paste(f1)
via their shared composite.
[9]:
g_to_f = c_g.paste(c_f_inv)
g_to_f.draw()

To go the other way around, we need to invert the compositor for g0.paste(g1)
.
[10]:
c_g_inv, c_g_rinvertor, c_g_linvertor = C.invert(c_g)
f_to_g = c_f.paste(c_g_inv)
f_to_g.draw()

This pair of diagrams “embodies” the commuting square with sides f0
, f1
, g0
, g1
.
We can use the “invertors” to show that the two diagrams are each other’s weak inverse.
[11]:
f_to_g.paste(g_to_f).draw(nodepositions=True)

[12]:
rew1 = f_to_g.paste(g_to_f).rewrite([1, 2], c_g_linvertor)
rew1.output.draw(nodepositions=True)

[13]:
rew2 = rew1.output.rewrite([0, 1], c_f.runitor('-'))
rew2.output.draw(nodepositions=True)

[14]:
rew3 = rew2.output.rewrite([0, 1], c_f_rinvertor)
rew3.output.draw()

Composites involving units
Now in C
all 1-dimensional diagrams have composites, so we can see C
as a category.
Except, in fact, not all 1-dimensional diagrams have composites that C
knows of!
[15]:
x0.unit().paste(f0).hascomposite
[15]:
False
Nevertheless, we can certainly turn this diagram into a single cell, using the left unitor for f0
.
[16]:
f0.lunitor('-').draw()

This is even already “weakly invertible”, as all degenerate cells are.
[17]:
f0.lunitor('-').inverse.draw()

So why does rewalt
not consider unitors to be compositors?
There is a good reason: rewalt
does not make a distinction between presentations of categories, bicategories, or n-categories for any other n. And there are certainly non-strict bicategories in which the composite of a 1-cell with a unit is not equal to the 1-cell.
So if we want C
to know that f0
is, indeed, the composite of x0.unit()
and f0
, we need to make it explicit.
[18]:
c_x0_f0 = C.make_composite('f0', x0.unit().paste(f0))
This will add a compositor which is not the same as the left unitor on f0
.
(The reason you cannot declare an existing degenerate cell to be a compositor is that rewalt
wants compositors to be generators, so it can remember which compositors a DiagSet
contains just by their list of names).
So if we want to “equate” the compositor to the unitor, we have to do it “weakly”, by adding a 3-cell between them.
[19]:
comp_to_lu = C.add('comp_to_lu', c_x0_f0, f0.lunitor('-'))
comp_to_lu.draw()

diagrams
Implements diagrammatic sets and diagrams.
Class for diagrammatic sets, a model of higher-dimensional rewrite systems and/or directed cell complexes. |
|
|
Class for diagrams, that is, mappings from a shape to an "ambient" diagrammatic set. |
|
Subclass of |
|
Subclass of |
|
Subclass of |
shapes
Implements shapes of cells and diagrams.
Inductive subclass of |
|
|
An overlay of |
Subclass of |
|
Subclass of |
ogposets
Implements oriented graded posets, their elements, subsets, and maps.
|
Class for oriented graded posets, that is, finite graded posets with an orientation, defined as a |
|
Class for (partial) maps of oriented graded posets, compatible with boundaries. |
|
Class for elements of an oriented graded poset. |
|
Class for sets of elements of an oriented graded poset, graded by their dimension. |
|
Class for graded subsets, that is, pairs of a |
|
Subclass of |
|
Class for pairs of maps of oriented graded posets. |
strdiags
Implements string diagram visualisations.
|
Class for string diagram visualisations of diagrams and shapes. |
|
Given any number of diagrams, generates their string diagrams and draws them. |
|
Given a diagram, generates the string diagram of its input and output boundaries of a given dimension, and draws them. |
|
Given a non-zero number of diagrams, generates their string diagrams and outputs a GIF animation of the sequence of their visualisations. |
hasse
Implements oriented Hasse diagram visualisation.
|
Class for "oriented Hasse diagrams" of oriented graded posets. |
|
Given any number of oriented graded posets, or maps, or diagrams, generates their Hasse diagrams and draws them. |
drawing
Drawing backends.
|
Abstract drawing backend for placing nodes, wires, arrows, and labels on a canvas. |
|
Drawing backend outputting Matplotlib figures. |
|
Drawing backend outputting TikZ code that can be embedded in a LaTeX document. |