1. Basic Tutorial: Reeb graph class

In this notebook, we’ll walk through the basic functionality of the cereeberus.ReebGraph class. See here for full documentation.

[1]:
import networkx as nx
import matplotlib.pyplot as plt
import numpy as np

# The main class for the Reeb Graph
from cereeberus import ReebGraph

# Some built in example graphs
import cereeberus.data.ex_reebgraphs as ex_rg

A Reeb graph consists of a graph \(G\) with a function \(f:G \to \mathbb{R}\) which is monotone on the edges. For our encoding purposes, we are going to assume that the function is defined by providing the function value on the vertices

\[f:V(G) \to \mathbb{R}\]

with the property that for any edge \(e = (u,v)\), \(f(u) \neq f(v)\).

In this tutorial, we go over the basic structure of the cereeberus.ReebGraph class.

1.1. Basic operations on a Reeb graph

We’re going to start with an empty Reeb graph.

[2]:
R = ReebGraph()
print(R)
ReebGraph with 0 nodes and 0 edges.

Let’s add some vertices. Of course, we have to be sure to specify the function value at the same time.

[3]:
R.add_node('A', f_vertex = 3.2)
R.add_node('B', f_vertex = 1.7)
R.add_node('C', f_vertex = 0)
R.add_node('D', f_vertex = -2)
R.add_node('E', f_vertex = np.pi)

print(R)
ReebGraph with 5 nodes and 0 edges.

We can also add some edges to our Reeb graph. Note that the Reeb graph allows for multiple edges between two vertices.

[4]:
R.add_edge('A','B')
R.add_edge('B','C')
R.add_edge('A','D')
R.add_edge('D','E')
R.add_edge('D','B')
R.add_edge('D','B')


print(R)
ReebGraph with 5 nodes and 6 edges.

The function values are stored in the dictionary R.f. Below we can see function values for some of the vertices we just added.

[5]:
print(f"f(A) = {R.f['A']}")
print(f"f(B) = {R.f['B']}")
print(f"f(E) = {R.f['E']}")
f(A) = 3.2
f(B) = 1.7
f(E) = 3.141592653589793

We can take a look at the Reeb graph using the built in drawing function. The cpx input controls loopiness for the drawing. The R.set_pos_from_f randomly sets the drawing positions. In this case, we picked seed = 2 to make a reasonable graph, however, this command is unnecessary unless you want more control over drawing locations.

[6]:
R.set_pos_from_f(seed = 2)
R.draw(cpx = 3)
../_images/notebooks_reeb_basics_13_0.png

You can manually control the drawing locations for the vertices if you want to. The drawing locations are held in the dictionary R.pos_f. So I can just move vertex A over a bit to the left to make my drawing a touch nicer. The second coordinate is always the function value R.f['A'], but we can change the first coordinate. Note that later when adding vertices and edges, this will likely be reset so you won’t likely want to use this tweaking much.

[7]:
print(f'Drawing location for A: {R.pos_f["A"]}')
R.pos_f['A'] = (-.5,R.f['A'])
R.draw(cpx=3)
Drawing location for A: (0.14133460941762915, 3.2)
../_images/notebooks_reeb_basics_15_1.png

We can also subdivde edges. Note that in a Reeb graph, because we think of an edge as being a copy of the unit interval, the result is considered the same Reeb graph. So, we can add a vertex to an edge \((u,v)\) at any function value \(f(u) < a< f(b)\).

[8]:
R.subdivide_edge('A','D','F',1)
R.subdivide_edge('B','D','G',0)

# Just for drawing purposes
R.set_pos_from_f(seed = 42)
R.draw(cpx=3)
../_images/notebooks_reeb_basics_17_0.png

These so-called regular vertices can also be removed. Either by using the command R.remove_regular_vertex(v) (note the difference between removing a regular vertex and the R.remove_node command which deletes a vertex and all adjacent edges from the Reeb graph), or by removing all at once as follows.

[9]:
R.remove_all_regular_vertices()

R.set_pos_from_f(seed = 1)
R.draw(cpx=3)
../_images/notebooks_reeb_basics_19_0.png

Note that if you add an edge between two vertices at the same function value, the Reeb graph will actually merge the two vertices into one. First I’m going to put back in an extra vertex on the edge \((D,E)\) at the same function value as \(C\).

[10]:
R.subdivide_edge('D','E','Z',R.f['C'])
R.draw()
../_images/notebooks_reeb_basics_21_0.png

Then if I try to add the edge \((Z,C)\), it will merge the two endpoints, keeping the name as the first vertex I passed in.

[11]:
R.add_edge('Z','C')
R.draw(cpx = 3)
../_images/notebooks_reeb_basics_23_0.png

1.2. Subgraphs and slices of the Reeb graph

Let’s get a new Reeb graph to work with. In this case, we’re going to use one of the example Reeb graphs provided in cereeberus.data.ex_reebgraphs. I’m also going to add a few edges for fun.

[12]:
R = ex_rg.dancing_man()
R.add_node(8,4)
R.add_edges_from(((6,0), (6,0),(7,1), (5,1), (8,2)))
R.set_pos_from_f(seed = 0)
R.draw(cpx = 3)
../_images/notebooks_reeb_basics_25_0.png

We can get the induced sub-Reeb-graph by providing a collection of vertices. This returns the portion of the Reeb graph with vertices from the set, and edges are included only if both endpoints are in the set.

[13]:
v_list = [0,1,8,6,3]
H = R.induced_subgraph(v_list)
H.draw(cpx = 3)
../_images/notebooks_reeb_basics_27_0.png

We can also take a slice of the Reeb graph, defined as the inverse image \(f^{-1}(a,b)\) for some interval \((a,b) \subset \mathbb{R}\). Note that this is not the same thing as taking the induced graph of the set of vertices \(\{v \mid f(v) \in (a,b)\}\) since we treat edges as being topologically equivalent to an interval. So taking the slice is done by adding spare vertices for edges that cross this interval.

[14]:
H = R.slice(4.5,6.5)
H.set_pos_from_f(seed = 0)
H.draw()
../_images/notebooks_reeb_basics_29_0.png

Although the drawing doesn’t make it particularly obvious, we can do things like check the number of connected components.

[15]:
print(f'There are {H.number_connected_components()} components in this Reeb Graph')
There are 3 components in this Reeb Graph

Or we can extract them individually:

[16]:
H_components = [H.induced_subgraph(c) for c in H.connected_components()]

H_components[0].draw()
../_images/notebooks_reeb_basics_33_0.png