[1]:
import semilattices as SL
%matplotlib inline

Coordinate semilattices

We start by constructing a semilattice, where we manually keep track of the nodes created. Note, it is not required to assign a new vertex (the return of the method call sl.new_vertex) to a variable, but this becomes useful for manual defining the graph dependence structure of the semilattice, as we do throughout this tutorial.

[2]:
sl = SL.CoordinateSemilattice(dims=2)     # Creates a semilattice of dimension 2
print(sl.properties)
root = sl.new_vertex()                    # Add the root vertex to the semilattice
c1 = sl.new_vertex(edge=1, parent=root)   # Add the child along coordinate 1 to the root vertex
Properties(VertexConstructor=<class 'semilattices._vertices.CoordinateVertex'>, VertexSetConstructor=<class 'sortedcontainers.sortedset.SortedSet'>, VertexSetConstructorKwargs={}, l1_vertices_partition_flag=True, l1_frontier_partition_flag=True, l1_childless_partition_flag=True, dims=2)

Next we plot the current semilattice.

[3]:
sl.draw(cartesian=True, show=False)
../_images/CoordinateSemilattice_5_0.png

Then, let us add some new nodes…

[4]:
c0 = sl.new_vertex(edge=0, parent=root)   # Add the child along coordinate 0 to the root vertex
c0c0 = sl.new_vertex(edge=0, parent=c0)   # Add the child along coordinate 0 to the c0 vertex
c0c1 = sl.new_vertex(edge=1, parent=c0)   # Add the child along coordinate 1 to the c0 node
sl.draw(cartesian=True, show=False)
../_images/CoordinateSemilattice_7_0.png

Note that in line 3 above, we add the child of c0 along coordinate 1. This child not only has c0 as its parent, but also the vertex c1 which was inserted previously. The function new_vertex detects the fact that the parent c1 exists and connects it correctly.

Let us add some more vertices.

[5]:
c0c1c1 = sl.new_vertex(edge=1, parent=c0c1)
c0c1c0 = sl.new_vertex(edge=0, parent=c0c1)
c0c0c0 = sl.new_vertex(edge=0, parent=c0c0)
c0c0c0c0 = sl.new_vertex(edge=0, parent=c0c0c0)
c0c0c0c1 = sl.new_vertex(edge=1, parent=c0c0c0)
sl.draw(cartesian=True, show=False)
../_images/CoordinateSemilattice_9_0.png

Adding vertices that already exist raises exceptions.

[6]:
try:
    c0c0c0 = sl.new_vertex(edge=0, parent=c0c0)
except SL.ChildAlreadyExists:
    print("Catched exception of child already existing...")
sl.draw(cartesian=True, show=False)
Catched exception of child already existing...
../_images/CoordinateSemilattice_11_1.png

It is also possible to remove nodes, while preserving the connectivity of the semilattice.

[7]:
sl.delete_vertex(c0c0)
sl.draw(cartesian=True, show=False)
../_images/CoordinateSemilattice_13_0.png

The frontier

The frontier is the subset of vertices \(\mathcal{F} \subset \mathcal{V}\) that admit additional children. As we state in the definition of coordinate semilattice, every vertex can have at most \(d\) children. Therefore a vertex is in the frontier if it has less than \(d\) children.

The frontier can be accessed by the following field.

[8]:
sl.frontier
[8]:
SortedSet([<semilattices._vertices.CoordinateVertex object at 0x7f2b99faef28>, <semilattices._vertices.CoordinateVertex object at 0x7f2b99d80780>, <semilattices._vertices.CoordinateVertex object at 0x7f2b99d80748>, <semilattices._vertices.CoordinateVertex object at 0x7f2b99bda1d0>, <semilattices._vertices.CoordinateVertex object at 0x7f2b99e21940>])

We may iterate over the frontier and print out the coordinates of the vertices belonging to it.

[9]:
for v in sl.frontier:
    print(v.coordinates)
{0: 1}
{0: 1, 1: 2}
{0: 2, 1: 1}
{0: 3, 1: 1}
{1: 1}

Or, we could visualize it using different markers (squares for the nodes in the frontier).

[10]:
sl.draw(cartesian=True, mark_frontier=True, show=False)
../_images/CoordinateSemilattice_19_0.png

Let’s delete a vertex!

[11]:
sl.delete_vertex(c1)
sl.draw(cartesian=True, mark_frontier=True, show=False)
../_images/CoordinateSemilattice_21_0.png
[12]:
for v in sl.frontier:
    print(v.coordinates)
print("or another way:")
for v in SL.LevelsIterable(sl, iter_attribute='_l1_frontier_partition'):
    print(v.coordinates)
{}
{0: 1}
{0: 1, 1: 2}
{0: 2, 1: 1}
{0: 3, 1: 1}
or another way:
{0: 1}
{0: 1, 1: 2}
{0: 2, 1: 1}
{0: 3, 1: 1}
{}
[13]:
for lvl, vertices in sl.l1_frontier_partition.items():
    print("lvl", lvl)
    for v in vertices:
        print(v.coordinates)
    print("\n")
lvl 1
{0: 1}


lvl 3
{0: 1, 1: 2}
{0: 2, 1: 1}


lvl 4
{0: 3, 1: 1}


lvl 0
{}


Childless vertices

Childless vertices are the subset of vertices \(\mathcal{F} \subset \mathcal{V}\) that have no children. This set is a subset of the frontier.

The childless vertices can be accessed by the following field.

[14]:
sl.childless
[14]:
SortedSet([<semilattices._vertices.CoordinateVertex object at 0x7f2b99d80780>, <semilattices._vertices.CoordinateVertex object at 0x7f2b99bda1d0>])

As with the frontier, we may iterate over the childless vertices and print out the coordinates of the vertices belonging to it.

[15]:
for v in sl.childless:
    print(v.coordinates)
{0: 1, 1: 2}
{0: 3, 1: 1}

Or, as with the frontier, we could visualize it using different markers (♢ for the nodes in the childless set). Note, if one also marks the frontier, the childless vertices will default to marking with ♢, even though they are a subset of the frontier as well.

[16]:
sl.draw(cartesian=True, mark_frontier=True, mark_childless=True,  show=False)
../_images/CoordinateSemilattice_29_0.png

The levels structure

The sets \(\{\mathcal{L}_i\}\) of \(\mathcal{V}\) partition \(\mathcal{V}\) in into vertices that belong to level \(i\). These correspond to vertices whose coordinates sum to \(i\), or in other words whose \(\ell^1\)-norm of the coordinates is \(i\).

These sets can be accessed through the following data structure.

[17]:
sl.l1_vertices_partition
[17]:
<semilattices._datastructures.LevelsPartition at 0x7f2bcace2318>

We can for example access all the vertices in level \(1\) as follows.

[18]:
for v in sl.l1_vertices_partition[1]:
    print(v.coordinates)
{0: 1}

Or we could visualize the semilattice with vertices colored according to their l1 norm.

[19]:
sl.draw(cartesian=True, color='l1_norm', show=False)
../_images/CoordinateSemilattice_35_0.png

We can also visualize the semilattice with vertices colored according to their ‘average dimension.’

[20]:
sl.draw(cartesian=True, color='dims', show=False)
../_images/CoordinateSemilattice_37_0.png

Iterating over a semilattice

There are a number of ways for iterating over a semilattice. The simplest way is using the semilattice itself, which does not guarantee any specific ordering.

[21]:
for v in sl:
    print(v.coordinates)
{}
{0: 1}
{0: 1, 1: 1}
{0: 1, 1: 2}
{0: 2, 1: 1}
{0: 3, 1: 1}

Alternatively one may be interested in iterating breadth first.

[22]:
for v in SL.BreadthFirstSemilatticeIterable(sl):
    print(v.coordinates)
{}
{0: 1}
{0: 1, 1: 1}
{0: 2, 1: 1}
{0: 1, 1: 2}
{0: 3, 1: 1}

Or depth first.

[23]:
for v in SL.DepthFirstSemilatticeIterable(sl):
    print(v.coordinates)
{}
{0: 1}
{0: 1, 1: 1}
{0: 1, 1: 2}
{0: 2, 1: 1}
{0: 3, 1: 1}

One could also to iterate level-by-level.

[24]:
for v in SL.LevelsIterable(sl):
    print(v.coordinates)
{}
{0: 1}
{0: 1, 1: 1}
{0: 1, 1: 2}
{0: 2, 1: 1}
{0: 3, 1: 1}

Union and intersection of semilattices

Semilattices are closed under union and intersection operations. So, two semilattices can be merged into one by taking the union or the intersection of the two. These operations could be performed in-place or not-in-place.

In order to show this feature, let us first generate another semilattice.

[25]:
sl2 = SL.CoordinateSemilattice(dims=2)
root = sl2.new_vertex()
c0 = sl2.new_vertex(edge=0, parent=root)
c1 = sl2.new_vertex(edge=1, parent=root)
c0c1 = sl2.new_vertex(edge=0, parent=c1)
c1c1 = sl2.new_vertex(edge=1, parent=c1)
c1c1c1 = sl2.new_vertex(edge=1, parent=c1c1)
c1c1c1c0 = sl2.new_vertex(edge=0, parent=c1c1c1)
c0c0 = sl2.new_vertex(edge=0, parent=c0)
sl2.draw(cartesian=True, show=False)
../_images/CoordinateSemilattice_47_0.png

Then let’s take the not-in-place union with the previous semilattice.

[26]:
usl = sl | sl2
usl.draw(cartesian=True, show=False)
../_images/CoordinateSemilattice_49_0.png

The corresponding in-place operation is performed as follows:

[27]:
sl_copy = sl.copy()    # We copy sl in order to be able to re-use it later
sl_copy |= sl2
sl_copy.draw(cartesian=True, show=False)
../_images/CoordinateSemilattice_51_0.png

Let us take the not-in-place intersection with the previous semilattice.

[28]:
isl = sl & sl2
isl.draw(cartesian=True, show=False)
../_images/CoordinateSemilattice_53_0.png

The corresponding in-place operation is:

[29]:
sl_copy = sl.copy()    # We copy sl in order to be able to re-use it later
sl_copy &= sl2
sl_copy.draw(cartesian=True, show=False)
../_images/CoordinateSemilattice_55_0.png

Visualization

In the previous examples we have seen several usages of the function CoordinateSemilattice.draw. There are two main plotting projections available, the cartesian, which works in 2d and 3d, and the “default” which project everything in 2 dimensions.

We construct a 3 dimensional semilattice to show their differences.

[30]:
sl = SL.CoordinateSemilattice(dims=3)
root = sl.new_vertex()
c0 = sl.new_vertex(edge=0, parent=root)
c1 = sl.new_vertex(edge=1, parent=root)
c2 = sl.new_vertex(edge=2, parent=root)
c0c0 = sl.new_vertex(edge=0, parent=c0)
c0c1 = sl.new_vertex(edge=1, parent=c0)
c0c2 = sl.new_vertex(edge=2, parent=c0)
c0c0c0 = sl.new_vertex(edge=0, parent=c0c0)
c0c0c1 = sl.new_vertex(edge=1, parent=c0c0)
c1c2 = sl.new_vertex(edge=2, parent=c1)
c0c1c2 = sl.new_vertex(edge=2, parent=c0c1)
c1c1 = sl.new_vertex(edge=1, parent=c1)

In general the cartesian projection provides a better spacing of the nodes, but can be used only in 2 dimensions or 3 dimensions.

[31]:
sl.draw(cartesian=True, show=False)
../_images/CoordinateSemilattice_59_0.png

The “default” projection instead puts the different axis of the dimensions along different branches of the semilattice projection and, as a result of plotting a high-dimensional semilattice in 2 dimesnions, clusters the nodes towards these branches.

[32]:
sl.draw(show=False)
/home/docs/checkouts/readthedocs.org/user_builds/semilattices/envs/latest/lib/python3.6/site-packages/networkx/drawing/nx_pylab.py:579: MatplotlibDeprecationWarning:
The iterable function was deprecated in Matplotlib 3.1 and will be removed in 3.3. Use np.iterable instead.
  if not cb.iterable(width):
../_images/CoordinateSemilattice_61_1.png

As a result some nodes and some edges may be crossing and overlapping, which is an unavoidable side effect. On the other hand this projection is still useful to get a general overview of the semilattice in high-dimensions.