Stern-Brocot tree

This Jupyter notebook provides an example of using the Python package gravis. The .ipynb file can be found here.

It constructs a Stern-Brocot tree with the help of 2x2 matrices and visualizes the tree as directed graph with different layouts. There is also a connection to continued fractions.

References

[1]:
import networkx as nx
import gravis as gv

Standard Stern-Brocot tree

A class for 2x2 matrices

as a tool for simpler construction of the Stern-Brocot tree

[2]:
class Matrix:
    """A simple class for 2x2 matrices with methods for creating a Stern-Brocot tree."""

    def __init__(self, a, b, c, d):
        """Define a 2x2 matrix by providing its 4 elements.

        M = |a b|
            |c d|
        """
        self.a = a
        self.b = b
        self.c = c
        self.d = d

    def __repr__(self):
        """Get a string representation of the matrix."""
        return '|{}  {}|\n|{}  {}|'.format(*self.elements)

    def __add__(self, other):
        """Calculate matrix addition."""
        elements = [e1 + e2 for e1, e2 in zip(self.elements, other.elements)]
        matrix = self.__class__(elements)
        return matrix

    def __mul__(self, other):
        """Calculate matrix multiplication."""
        # https://youtu.be/qPeD87HJ0UA?t=230
        new_matrix = self.__class__(
            self.a * other.a + self.b * other.c, self.a * other.b + self.b * other.d,
            self.c * other.a + self.d * other.c, self.c * other.b + self.d * other.d)
        return new_matrix

    def __eq__(self, other):
        """Check if two matrices are equal."""
        return self.elements == other.elements

    @property
    def lc(self):
        """Create the left child in a Stern-Brocot tree with matrix elements.

        References
        ----------
        - https://youtu.be/qPeD87HJ0UA?t=419
        """
        return self.__class__(
            self.a + self.b, self.b,
            self.c + self.d, self.d)

    @property
    def rc(self):
        """Create the right child in a Stern-Brocot tree with matrix elements."""
        return Matrix(self.a, self.a + self.b, self.c, self.c + self.d)

    @property
    def elements(self):
        """Get the matrix elements as tuple."""
        return self.a, self.b, self.c, self.d

    @property
    def numerator(self):
        """Get the numerator of the fraction."""
        return self.a + self.b

    @property
    def denominator(self):
        """Get the denominator of the fraction."""
        return self.c + self.d

    @property
    def fraction(self):
        """Reconstruct the fraction that corresponds to the matrix."""
        return self.numerator, self.denominator

    @property
    def fraction_string(self):
        """Get a string representation of the fraction."""
        return '{}/{}'.format(*self.fraction)

    @property
    def decimal(self):
        """Get a decimal representation of the fraction."""
        return self.numerator / self.denominator
[3]:
# Identity matrix and two operators for creating left and right children
I = Matrix(1, 0, 0, 1)
L = Matrix(1, 0, 1, 1)
R = Matrix(1, 1, 0, 1)

# equivalence of matrix multiplication (star notation) and branching methods (dot notation)
M1 = I * L * L * R * L * R
M2 = I.lc.lc.rc.lc.rc
assert M1 == M2

print('Matrix')
print(M1)
print()
print('Fraction')
print('- as tuple:', M1.fraction)
print('- as numerator and denominator:', M1.numerator, M1.denominator)
print('- as string:', M1.fraction_string)
print('- as decimal number:', M1.decimal)
Matrix
|2  3|
|5  8|

Fraction
- as tuple: (5, 13)
- as numerator and denominator: 5 13
- as string: 5/13
- as decimal number: 0.38461538461538464
[4]:
# Level 0
I.fraction_string
[4]:
'1/1'
[5]:
# Level 1
I.lc.fraction_string, I.rc.fraction_string
[5]:
('1/2', '2/1')
[6]:
# Level 2
I.lc.lc.fraction_string, I.lc.rc.fraction_string, I.rc.lc.fraction_string, I.rc.rc.fraction_string
[6]:
('1/3', '2/3', '3/2', '3/1')

Construction of the tree

[7]:
num_levels = 7
dg = nx.DiGraph()

def to_hover_message(matrix):
    return 'Matrix:\n{}<br><br>Fraction: {} = {}'.format(matrix, matrix.fraction_string, matrix.decimal)

root = Matrix(1, 0,
              0, 1)
sequence = [root]
for level in range(1, num_levels):
    new_sequence = []
    for i, item in enumerate(sequence):
        # Stern-Brocot tree construction
        child1, child2 = item.lc, item.rc
        new_sequence.append(child1)
        new_sequence.append(child2)

        # Directed graph construction
        dg.add_edge(item.fraction_string, child1.fraction_string)
        dg.add_edge(item.fraction_string, child2.fraction_string)
        edge1 = dg.edges[(item.fraction_string, child1.fraction_string)]
        edge2 = dg.edges[(item.fraction_string, child2.fraction_string)]
        edge1['label'] = 'L'
        edge1['color'] = 'red'
        edge1['label_color'] = 'red'
        edge2['label'] = 'R'
        # Add node attributes
        parent_node = dg.nodes[item.fraction_string]
        child_node1 = dg.nodes[child1.fraction_string]
        child_node2 = dg.nodes[child2.fraction_string]
        parent_node['fraction'] = item.fraction
        child_node1['fraction'] = child1.fraction
        child_node2['fraction'] = child2.fraction
        parent_node['elements'] = item.elements
        child_node1['elements'] = child1.elements
        child_node2['elements'] = child2.elements
        parent_node['hover'] = to_hover_message(item)
        child_node1['hover'] = to_hover_message(child1)
        child_node2['hover'] = to_hover_message(child2)
        parent_node['x'] = (-len(sequence) / 2 + i) * 50
        parent_node['y'] = (level - 1) * 50
        child_node1['color'] = 'red'
        child_node1['label_color'] = 'red'
    sequence = new_sequence

# Node attributes for lowest level
for i, item in enumerate(sequence):
    parent_node = dg.nodes[item.fraction_string]
    parent_node['x'] = (-len(sequence) / 2 + i) * 50
    parent_node['y'] = level * 50

Visualization of the tree

The following shows different embeddings of the same Stern-Brocot tree.

[8]:
gv.vis(dg, zoom_factor=0.45, show_edge_label=True, edge_label_data_source='label')
[8]:
Details for selected element
General
App state
Display mode
Export
Data selection
Graph
Node label text
Edge label text
Node size
Minimum
Maximum
Edge size
Minimum
Maximum
Nodes
Visibility
Size
Scaling factor
Position
Drag behavior
Hover behavior
Node images
Visibility
Size
Scaling factor
Node labels
Visibility
Size
Scaling factor
Rotation
Angle
Edges
Visibility
Size
Scaling factor
Form
Curvature
Hover behavior
Edge labels
Visibility
Size
Scaling factor
Rotation
Angle
Layout algorithm
Simulation
Algorithm
Parameters
Gravitational constant
Spring length
Spring constant
Avoid overlap
Central gravity
[9]:
# Assign different coordinates to each node: numerator as x, denominator as y
# - see https://youtu.be/qPeD87HJ0UA?t=1272
dg2 = dg.copy()
for node_id in dg2.nodes:
    node = dg2.nodes[node_id]
    node['x'] = node['fraction'][0] * 50 - 900
    node['y'] = -node['fraction'][1] * 50 + 900

gv.vis(dg2, show_edge_label=True, edge_label_data_source='label')
[9]:
Details for selected element
General
App state
Display mode
Export
Data selection
Graph
Node label text
Edge label text
Node size
Minimum
Maximum
Edge size
Minimum
Maximum
Nodes
Visibility
Size
Scaling factor
Position
Drag behavior
Hover behavior
Node images
Visibility
Size
Scaling factor
Node labels
Visibility
Size
Scaling factor
Rotation
Angle
Edges
Visibility
Size
Scaling factor
Form
Curvature
Hover behavior
Edge labels
Visibility
Size
Scaling factor
Rotation
Angle
Layout algorithm
Simulation
Algorithm
Parameters
Gravitational constant
Spring length
Spring constant
Avoid overlap
Central gravity
[10]:
# Assign different coordinates to each node: sum of left column as x, sum of right column as y
dg3 = dg.copy()
for node_id in dg3.nodes:
    node = dg3.nodes[node_id]
    node['x'] = (node['elements'][0] + node['elements'][2]) * 50 - 900
    node['y'] = -(node['elements'][1] + node['elements'][3]) * 50 +  900

gv.vis(dg3, show_edge_label=True, edge_label_data_source='label')
[10]:
Details for selected element
General
App state
Display mode
Export
Data selection
Graph
Node label text
Edge label text
Node size
Minimum
Maximum
Edge size
Minimum
Maximum
Nodes
Visibility
Size
Scaling factor
Position
Drag behavior
Hover behavior
Node images
Visibility
Size
Scaling factor
Node labels
Visibility
Size
Scaling factor
Rotation
Angle
Edges
Visibility
Size
Scaling factor
Form
Curvature
Hover behavior
Edge labels
Visibility
Size
Scaling factor
Rotation
Angle
Layout algorithm
Simulation
Algorithm
Parameters
Gravitational constant
Spring length
Spring constant
Avoid overlap
Central gravity

Connection to continued fractions and mathematical constants

See book p. 122

Euler’s number e as continued fraction

[11]:
# Continued fraction representations of the irrational number e by an integer sequence with a simple pattern
e_sequence = [
    2, 1,
    2, 1, 1,
    4, 1, 1,
    6, 1, 1,
    8, 1, 1,
    10, 1, 1,
    12, 1, 1,
    14, 1, 1,
]
[12]:
# Continued fraction
# - Using the integers of the sequence as denominators in a continued fraction

result = None
for element in reversed(e_sequence):
    if result is None:
        result = 1.0 / element
    else:
        result = 1.0 / (element + result)

print(1.0 / result)
2.7182818284590455

Euler’s number e as walk in the Stern-Brocot tree

[13]:
# Stern-Brocot system
# - Using the integers of the sequence as a series of L and R operations to perform a walk in the tree

def change_or_add_edge(graph, M1, M2, operator_name):
    try:
        edge = graph.edges[(M1.fraction_string, M2.fraction_string)]
    except Exception:
        graph.add_edge(M1.fraction_string, M2.fraction_string)
        edge = graph.edges[(M1.fraction_string, M2.fraction_string)]
    color = 'black' if operator_name == 'R' else 'red'
    edge['label'] = operator_name
    edge['size'] = 10
    edge['color'] = color
    edge['label_color'] = 'green'
    edge['label_size'] = 20
    for M in [M1, M2]:
        node = graph.nodes[M.fraction_string]
        node['size'] = 18
        node['shape'] = 'rectangle'
        node['fraction'] = M.fraction
        node['elements'] = M.elements
        node['hover'] = to_hover_message(M)
        if M is M2:
            node['color'] = color
            node['label_color'] = color


I = Matrix(1, 0,
           0, 1)
M = I
operator = R
for element in e_sequence:
    for _ in range(element):
        M_new = M * operator
        op_name = 'R' if operator is R else 'L'
        print(op_name, end='')
        change_or_add_edge(dg2, M, M_new, op_name)  # add the walk into the digraph
        M = M_new
    operator = L if operator == R else R

print('\n\n{} = {}'.format(M.fraction_string, M.decimal))
RRLRRLRLLLLRLRRRRRRLRLLLLLLLLRLRRRRRRRRRRLRLLLLLLLLLLLLRLRRRRRRRRRRRRRRLR

1286807394/473389985 = 2.718281828459045
[14]:
gv.vis(dg2, show_edge_label=True, edge_label_data_source='label')
[14]:
Details for selected element
General
App state
Display mode
Export
Data selection
Graph
Node label text
Edge label text
Node size
Minimum
Maximum
Edge size
Minimum
Maximum
Nodes
Visibility
Size
Scaling factor
Position
Drag behavior
Hover behavior
Node images
Visibility
Size
Scaling factor
Node labels
Visibility
Size
Scaling factor
Rotation
Angle
Edges
Visibility
Size
Scaling factor
Form
Curvature
Hover behavior
Edge labels
Visibility
Size
Scaling factor
Rotation
Angle
Layout algorithm
Simulation
Algorithm
Parameters
Gravitational constant
Spring length
Spring constant
Avoid overlap
Central gravity

Generalized Stern-Brocot tree

This is an attempt to generalize the Stern-Brocot tree by replacing 2d matrices with 3d matrices and keeping the operations as close to the 2d case as possible. The resulting tree and its embeddings apparently share some similarity to the 2d case. Of course there is more freedom of interpretation due to the higher number of constituents, therefore I’ll leave speculation about possible meanings to the reader.

[15]:
class Matrix3d:
    """A simple class for 3x3 matrices with methods for creating a generalized Stern-Brocot tree."""

    def __init__(self, a, b, c, d, e, f, g, h, i):
        """Define a 3x3 matrix by providing its 9 elements.

        M = |a b c|
            |d e f|
            |g h i|
        """
        self.a = a
        self.b = b
        self.c = c
        self.d = d
        self.e = e
        self.f = f
        self.g = g
        self.h = h
        self.i = i

    def __repr__(self):
        """Get a string representation of the matrix."""
        return '|{}  {}  {}|\n|{}  {}  {}|\n|{}  {}  {}|'.format(*self.elements)

    def __add__(self, other):
        """Calculate matrix addition."""
        elements = [e1 + e2 for e1, e2 in zip(self.elements, other.elements)]
        matrix = self.__class__(elements)
        return matrix

    def __eq__(self, other):
        """Check if two matrices are equal."""
        return self.elements == other.elements

    @property
    def elements(self):
        """Get the matrix elements as tuple."""
        return self.a, self.b, self.c, self.d, self.e, self.f, self.g, self.h, self.i

    @property
    def lc(self):
        """Create the left child in a generalized Stern-Brocot tree with matrix elements."""
        new_matrix = self.__class__(
            self.a + self.b + self.c, self.b, self.c,
            self.d + self.e + self.f, self.e, self.f,
            self.g + self.h + self.i, self.h, self.i)
        return new_matrix

    @property
    def mc(self):
        """Create the middle child in a generalized Stern-Brocot tree with matrix elements."""
        new_matrix = self.__class__(
            self.a, self.b + self.a + self.c, self.c,
            self.d, self.e + self.d + self.f, self.f,
            self.g, self.h + self.g + self.i, self.i)
        return new_matrix

    @property
    def rc(self):
        """Create the right child in a generalized Stern-Brocot tree with matrix elements."""
        new_matrix = self.__class__(
            self.a, self.b, self.c + self.b + self.a,
            self.d, self.e, self.f + self.e + self.d,
            self.g, self.h, self.i + self.h + self.g)
        return new_matrix

    @property
    def fractions_string(self):
        """Get a string representation of both fractions."""
        return '{}, {}'.format(self.fraction1_string, self.fraction2_string)

    @property
    def numerator1(self):
        """Get the numerator of the first fraction."""
        return self.a + self.b + self.c

    @property
    def denominator1(self):
        """Get the denominator of the first fraction."""
        return self.d + self.e + self.f

    @property
    def fraction1(self):
        """Reconstruct the first fraction that is contained in the matrix."""
        return self.numerator1, self.denominator1

    @property
    def fraction1_string(self):
        """Get a string representation of the first fraction."""
        return '{}/{}'.format(*self.fraction1)

    @property
    def decimal1(self):
        """Get a decimal representation of the first fraction."""
        return self.numerator1 / self.denominator1

    @property
    def numerator2(self):
        """Get the numerator of the second fraction."""
        return self.d + self.e + self.f

    @property
    def denominator2(self):
        """Get the denominator of the second fraction."""
        return self.g + self.h + self.i

    @property
    def fraction2(self):
        """Reconstruct the second fraction that is contained in the matrix."""
        return self.numerator2, self.denominator2

    @property
    def fraction2_string(self):
        """Get a string representation of the second fraction."""
        return '{}/{}'.format(*self.fraction2)

    @property
    def decimal2(self):
        """Get a decimal representation of the second fraction."""
        return self.numerator2 / self.denominator2
[16]:
num_levels = 5
dg = nx.DiGraph()

def to_hover_message(matrix):
    return 'Matrix:\n{}<br><br>Fraction1: {} = {}<br>Fraction2: {} = {}'.format(
        matrix, matrix.fraction1_string, matrix.decimal1, matrix.fraction2_string, matrix.decimal2)

root = Matrix3d(
    1, 0, 0,
    0, 1, 0,
    0, 0, 1)
sequence = [root]
for level in range(1, num_levels):
    new_sequence = []
    for i, item in enumerate(sequence):
        # Stern-Brocot tree construction
        child1, child2, child3 = item.lc, item.mc, item.rc
        new_sequence.append(child1)
        new_sequence.append(child2)
        new_sequence.append(child3)

        # Directed graph construction
        dg.add_edge(item.fractions_string, child1.fractions_string)
        dg.add_edge(item.fractions_string, child2.fractions_string)
        dg.add_edge(item.fractions_string, child3.fractions_string)
        edge1 = dg.edges[(item.fractions_string, child1.fractions_string)]
        edge2 = dg.edges[(item.fractions_string, child2.fractions_string)]
        edge3 = dg.edges[(item.fractions_string, child3.fractions_string)]
        edge1['label'] = 'L'
        edge1['color'] = 'red'
        edge1['label_color'] = 'red'
        edge2['label'] = 'M'
        edge2['color'] = 'blue'
        edge2['label_color'] = 'blue'
        edge3['label'] = 'R'
        # Add node attributes
        parent_node = dg.nodes[item.fractions_string]
        child_node1 = dg.nodes[child1.fractions_string]
        child_node2 = dg.nodes[child2.fractions_string]
        child_node3 = dg.nodes[child3.fractions_string]
        parent_node['elements'] = item.elements
        child_node1['elements'] = child1.elements
        child_node2['elements'] = child2.elements
        child_node3['elements'] = child3.elements
        parent_node['hover'] = to_hover_message(item)
        child_node1['hover'] = to_hover_message(child1)
        child_node2['hover'] = to_hover_message(child2)
        child_node3['hover'] = to_hover_message(child3)
        parent_node['x'] = (-len(sequence) / 2 + i) * 100
        parent_node['y'] = (level - 1) * 50
        child_node1['color'] = 'red'
        child_node1['label_color'] = 'red'
        child_node2['color'] = 'blue'
        child_node2['label_color'] = 'blue'
    sequence = new_sequence

# Node attributes for lowest level
for i, item in enumerate(sequence):
    parent_node = dg.nodes[item.fractions_string]
    parent_node['x'] = (-len(sequence) / 2 + i) * 100
    parent_node['y'] = level * 50
[17]:
gv.vis(dg, zoom_factor=0.45, show_edge_label=True, edge_label_data_source='label')
[17]:
Details for selected element
General
App state
Display mode
Export
Data selection
Graph
Node label text
Edge label text
Node size
Minimum
Maximum
Edge size
Minimum
Maximum
Nodes
Visibility
Size
Scaling factor
Position
Drag behavior
Hover behavior
Node images
Visibility
Size
Scaling factor
Node labels
Visibility
Size
Scaling factor
Rotation
Angle
Edges
Visibility
Size
Scaling factor
Form
Curvature
Hover behavior
Edge labels
Visibility
Size
Scaling factor
Rotation
Angle
Layout algorithm
Simulation
Algorithm
Parameters
Gravitational constant
Spring length
Spring constant
Avoid overlap
Central gravity
[18]:
# Assign different 3d coordinates to each node: sum of row 1 as x, row 2 as y, row 3 as z
dg2 = dg.copy()
for node_id in dg2.nodes:
    node = dg2.nodes[node_id]
    m = node['elements']
    x = m[0] + m[1] + m[2]
    y = m[3] + m[4] + m[5]
    z = m[6] + m[7] + m[8]
    node['x'] = x * 50 - 200
    node['y'] = -y * 50 + 200
    node['z'] = -z * 50 + 200

gv.three(dg2, show_edge_label=True, edge_label_data_source='label', large_graph_threshold=1000)
[18]:
Details for selected element
General
App state
Display mode
Export
Data selection
Graph
Node label text
Node size
Minimum
Maximum
Edge size
Minimum
Maximum
Nodes
Visibility
Size
Scaling factor
Position
Drag behavior
Hover behavior
Node images
Visibility
Size
Scaling factor
Node labels
Visibility
Size
Scaling factor
Edges
Visibility
Size
Scaling factor
Form
Curvature
Hover behavior
Layout algorithm
Simulation
Many-body force
Strength
Theta
Min
Max
Links force
x-positioning force
Strength
y-positioning force
Strength
z-positioning force
Strength
Centering force
[19]:
# Assign different 3d coordinates to each node: sum of column 1 as x, column 2 as y, column 3 as z
dg2 = dg.copy()
for node_id in dg2.nodes:
    node = dg2.nodes[node_id]
    m = node['elements']
    x = m[0] + m[3] + m[6]
    y = m[1] + m[4] + m[7]
    z = m[2] + m[5] + m[8]
    node['x'] = x * 50 - 200
    node['y'] = -y * 50 + 200
    node['z'] = -z * 50 + 200

gv.three(dg2, show_edge_label=True, edge_label_data_source='label', large_graph_threshold=1000)
[19]:
Details for selected element
General
App state
Display mode
Export
Data selection
Graph
Node label text
Node size
Minimum
Maximum
Edge size
Minimum
Maximum
Nodes
Visibility
Size
Scaling factor
Position
Drag behavior
Hover behavior
Node images
Visibility
Size
Scaling factor
Node labels
Visibility
Size
Scaling factor
Edges
Visibility
Size
Scaling factor
Form
Curvature
Hover behavior
Layout algorithm
Simulation
Many-body force
Strength
Theta
Min
Max
Links force
x-positioning force
Strength
y-positioning force
Strength
z-positioning force
Strength
Centering force
[20]:
# Assign different 2d coordinates to each node: fraction1 as x, fraction2 as y
dg2 = dg.copy()
for node_id in dg2.nodes:
    node = dg2.nodes[node_id]
    m = node['elements']
    r1 = m[0] + m[1] + m[2]
    r2 = m[3] + m[4] + m[5]
    r3 = m[6] + m[7] + m[8]
    node['x'] = r1 / r2 * 700
    node['y'] = -r2 / r3 * 700

gv.vis(dg2, show_edge_label=True, edge_label_data_source='label', large_graph_threshold=1000)
[20]:
Details for selected element
General
App state
Display mode
Export
Data selection
Graph
Node label text
Edge label text
Node size
Minimum
Maximum
Edge size
Minimum
Maximum
Nodes
Visibility
Size
Scaling factor
Position
Drag behavior
Hover behavior
Node images
Visibility
Size
Scaling factor
Node labels
Visibility
Size
Scaling factor
Rotation
Angle
Edges
Visibility
Size
Scaling factor
Form
Curvature
Hover behavior
Edge labels
Visibility
Size
Scaling factor
Rotation
Angle
Layout algorithm
Simulation
Algorithm
Parameters
Gravitational constant
Spring length
Spring constant
Avoid overlap
Central gravity
[21]:
# Assign different 2d coordinates to each node: row1 * row as x, fraction2 as y
dg2 = dg.copy()
for node_id in dg2.nodes:
    node = dg2.nodes[node_id]
    m = node['elements']
    r1 = m[0] + m[1] + m[2]
    r2 = m[3] + m[4] + m[5]
    r3 = m[6] + m[7] + m[8]
    node['x'] = (r1 / r2) * r3 * 100
    node['y'] = -r1 * (r2 / r3) * 100

gv.vis(dg2, show_edge_label=True, edge_label_data_source='label', large_graph_threshold=1000)
[21]:
Details for selected element
General
App state
Display mode
Export
Data selection
Graph
Node label text
Edge label text
Node size
Minimum
Maximum
Edge size
Minimum
Maximum
Nodes
Visibility
Size
Scaling factor
Position
Drag behavior
Hover behavior
Node images
Visibility
Size
Scaling factor
Node labels
Visibility
Size
Scaling factor
Rotation
Angle
Edges
Visibility
Size
Scaling factor
Form
Curvature
Hover behavior
Edge labels
Visibility
Size
Scaling factor
Rotation
Angle
Layout algorithm
Simulation
Algorithm
Parameters
Gravitational constant
Spring length
Spring constant
Avoid overlap
Central gravity

Euler’s number e plus 1 as walk in the generalized Stern-Brocot tree

[22]:
# Using the integers of the e sequence as a series of L and R operations
# to perform a walk in the generalized tree

def change_or_add_edge(graph, M1, M2, operator_name):
    try:
        edge = graph.edges[(M1.fractions_string, M2.fractions_string)]
    except Exception:
        graph.add_edge(M1.fractions_string, M2.fractions_string)
        edge = graph.edges[(M1.fractions_string, M2.fractions_string)]
    color = 'black' if operator_name == 'R' else 'red'
    edge['label'] = operator_name
    edge['size'] = 10
    edge['color'] = color
    edge['label_color'] = 'green'
    edge['label_size'] = 20
    for M in [M1, M2]:
        node = graph.nodes[M.fractions_string]
        node['size'] = 18
        node['shape'] = 'rectangle'
        node['fraction1'] = M.fraction1
        node['fraction2'] = M.fraction2
        node['elements'] = M.elements
        node['hover'] = to_hover_message(M)
        if M is M2:
            node['color'] = color
            node['label_color'] = color


I = Matrix3d(1, 0, 0,
             0, 1, 0,
             0, 0, 1)
M = I
operator = 'R'
for element in e_sequence:
    for _ in range(element):
        if operator == 'R':
            M_new = M.rc
        else:
            M_new = M.lc
        print(operator, end='')
        change_or_add_edge(dg2, M, M_new, operator)  # add the walk into the digraph
        M = M_new
    operator = 'L' if operator == 'R' else 'R'

print('\n\nMatrix:\n{}\n\nFraction 1: {} = {}\nFraction 2: {} = {}'.format(
    M, M.fraction1_string, M.decimal1, M.fraction2_string, M.decimal2))
RRLRRLRLLLLRLRRRRRRLRLLLLLLLLRLRRRRRRRRRRLRLLLLLLLLLLLLRLRRRRRRRRRRRRRRLR

Matrix:
|438351041  0  848456353|
|599611376  1  1160586001|
|161260336  0  312129649|

Fraction 1: 1286807394/1760197378 = 0.7310585790453324
Fraction 2: 1760197378/473389985 = 3.718281826346622
[23]:
# How can this be interpreted?
# - Initial observations
#   - Apparent: Fraction 2 = 2.7182818 is e + 1
#   - Web search: Fraction 1 = 0.7310585 is output of logistic function e(t) / (1 + e(t)) at t=1
# - Deduced
#   - Fraction 1 can be expressed with fraction 2
#   - Fraction 1 times fraction 2 gives e
#   - Numerator of fraction 1 together with denominator of fraction 2 gives e
# - Other observation
#   - Web search: 1.7310585 => "tables of angular spheroidal wave functions", "Non-Classical Hydrogen Bonding"

import math

b = math.e + 1               # fraction1 is e+1
a = math.e / (1 + math.e)    # fraction2 is the logistic function with t=1: e/(1+e)
assert a == math.e / b       # fraction 1 expressed by fraction 2
assert b == math.e / a       # fraction 2 expressed by fraction 1

e1 = a * b                   # e calculated by multiplication of fraction 1 and 2
e2 = 1286807394 / 473389985  # e calculated by using numerator1 and denominator 2

a, b, e1, e2
[23]:
(0.7310585786300049, 3.718281828459045, 2.718281828459045, 2.718281828459045)
[24]:
gv.vis(dg2, show_edge_label=True, edge_label_data_source='label')
[24]:
Details for selected element
General
App state
Display mode
Export
Data selection
Graph
Node label text
Edge label text
Node size
Minimum
Maximum
Edge size
Minimum
Maximum
Nodes
Visibility
Size
Scaling factor
Position
Drag behavior
Hover behavior
Node images
Visibility
Size
Scaling factor
Node labels
Visibility
Size
Scaling factor
Rotation
Angle
Edges
Visibility
Size
Scaling factor
Form
Curvature
Hover behavior
Edge labels
Visibility
Size
Scaling factor
Rotation
Angle
Layout algorithm
Simulation
Algorithm
Parameters
Gravitational constant
Spring length
Spring constant
Avoid overlap
Central gravity

Further generalization attempts

This seems not provide additional structure compared to the 3x3 case.

[25]:
class MatrixGeneral:
    """A simple class for nxn matrices with methods for creating a generalized Stern-Brocot tree."""

    def __init__(self, element_list):
        """Define a nxn matrix by providing its 9 elements."""
        from math import sqrt
        root = int(sqrt(len(element_list)))
        if root**2 != len(element_list):
            raise ValueError('Invalid number of elements, not an integer sqrt(): {}'.format(
                len(elements)))
        self.element_list = element_list
        self.n = root

    def __repr__(self):
        """Get a string representation of the matrix."""
        row = '|{}|'.format('  '.join(['{}'] * self.n))
        matrix = '\n'.join([row] * self.n)
        return matrix.format(*self.elements)

    def __add__(self, other):
        """Calculate matrix addition."""
        new_elements = [e1 + e2 for e1, e2 in zip(self.element_list, other.element_list)]
        matrix = self.__class__(new_elements)
        return matrix

    def __eq__(self, other):
        """Check if two matrices are equal."""
        return self.element_list == other.element_list

    @property
    def elements(self):
        """Get the matrix elements as tuple."""
        return self.element_list

    @property
    def lc(self):
        """Create the left child in a generalized Stern-Brocot tree with matrix elements."""
        new_elements = []
        for i in range(self.n):
            for j in range(self.n):
                if j == 0:
                    new_element = 0
                    for k in range(self.n):
                        new_element += self.element_list[i * self.n + k]
                else:
                    new_element = self.element_list[i * self.n + j]
                new_elements.append(new_element)
        new_matrix = self.__class__(new_elements)
        return new_matrix

    @property
    def rc(self):
        """Create the right child in a generalized Stern-Brocot tree with matrix elements."""
        new_elements = []
        for i in range(self.n):
            for j in range(self.n):
                if j == (self.n - 1):
                    new_element = 0
                    for k in range(self.n):
                        new_element += self.element_list[i * self.n + k]
                else:
                    new_element = self.element_list[i * self.n + j]
                new_elements.append(new_element)
        new_matrix = self.__class__(new_elements)
        return new_matrix

    @property
    def fractions(self):
        pass
[26]:
# Euler sequence with 5x5 matrix ... seems not to add information in comparison to the 3x3 case
I = MatrixGeneral([1, 0, 0, 0,
                   0, 1, 0, 0,
                   0, 0, 1, 0,
                   0, 0, 0, 1])
M = I
operator = 'R'
for element in e_sequence:
    for _ in range(element):
        if operator == 'R':
            M_new = M.rc
        else:
            M_new = M.lc
        print(operator, end='')
        M = M_new
    operator = 'L' if operator == 'R' else 'R'

print('\n\nMatrix:\n{}'.format(M))
RRLRRLRLLLLRLRRRRRRLRLLLLLLLLRLRRRRRRRRRRLRLLLLLLLLLLLLRLRRRRRRRRRRRRRRLR

Matrix:
|438351041  0  0  848456353|
|599611376  1  0  1160586001|
|599611376  0  1  1160586001|
|161260336  0  0  312129649|
[27]:
# Euler sequence with 5x5 matrix ... seems not to add information in comparison to the 3x3 case
I = MatrixGeneral([1, 0, 0, 0, 0,
                   0, 1, 0, 0, 0,
                   0, 0, 1, 0, 0,
                   0, 0, 0, 1, 0,
                   0, 0, 0, 0, 1])
M = I
operator = 'R'
for element in e_sequence:
    for _ in range(element):
        if operator == 'R':
            M_new = M.rc
        else:
            M_new = M.lc
        print(operator, end='')
        M = M_new
    operator = 'L' if operator == 'R' else 'R'

print('\n\nMatrix:\n{}'.format(M))
RRLRRLRLLLLRLRRRRRRLRLLLLLLLLRLRRRRRRRRRRLRLLLLLLLLLLLLRLRRRRRRRRRRRRRRLR

Matrix:
|438351041  0  0  0  848456353|
|599611376  1  0  0  1160586001|
|599611376  0  1  0  1160586001|
|599611376  0  0  1  1160586001|
|161260336  0  0  0  312129649|