Skip to main content

Section 4.14 Session 15: Objectification of Properties, Part 5

Last week I stopped partway through Exercise 7 because I was struggling to find all the last 2 maps, but I had "an idea" about where to find them. This week the plan is work on verifying that hypothesis by using Python. I’m going to begin with Exercise 8 to get a better feel for how these "presentations" work.
This is going to get some what code-heavy, so see this Jupyter notebook
 1 
github.com/rruff82/LS-Categories/blob/main/assets/notebooks/ls-ses15-ex7-before.ipynb
if you want to follow along.

Example 4.14.1. Exercise 8:.

Draw some simple dynamical systems...
Solution.
Rather than manually finding these presentations, I’m going to build a Python script that does it for me. I leveraged some common libraries, includeing NetworkX
 2 
networkx.org
for the "digraph" object, NumPy
 3 
numpy.org
for linear algebra. I used Matplotlib
 4 
matplotlib.org
for visuals during development, but I’ll map to for this document. Our endomap \(\alpha\) might be defined as follows:
import numpy as np
import networkx as nx
import matplotlib.pyplot as plt

x_nodes = ["X"+str(i) for i in range(9)]
x_edges = [
    (x_nodes[0],x_nodes[4]),
    (x_nodes[1],x_nodes[5]),
    (x_nodes[2],x_nodes[6]),
    (x_nodes[3],x_nodes[8]),
    (x_nodes[4],x_nodes[5]),
    (x_nodes[5],x_nodes[6]),
    (x_nodes[6],x_nodes[7]),
    (x_nodes[7],x_nodes[5]),
    (x_nodes[8],x_nodes[3])
]
XG = nx.DiGraph()
XG.add_nodes_from(x_nodes)
XG.add_edges_from(x_edges)
Since this directed graph represents an endomap, each node should have precisely one "successor". We can use this property to implent our endomap as a function on the graph:
def follow_endomap(G,n):
    return list(G.successors(n))[0]
We can then use the algorithm described in the text to produce a "presentation" from a given endomap. I tried
def make_presentation(G):
    G_nodes = list(G.nodes())
    ones_G = np.full((len(G_nodes),),1)
    GA =  nx.adjacency_matrix(G)
    G_arrows_in =  np.matmul(ones_G,GA.toarray())
    G_arrows_by_node = sorted(zip(range(len(G_nodes)),G_arrows_in), key=lambda x: x[1])
    G_presentation = {}
    G_relations = {}
    for k,v in G_arrows_by_node:
      print(f"Checking node {k} with {v} arrows in")
      if v == 0:
        print(f"Adding root generator {G_nodes[k]}")
        G_presentation[G_nodes[k]] = (G_nodes[k],v)
        next_node = follow_endomap(G,G_nodes[k])
        steps = 1
        while not next_node in G_presentation:
          print(f"Adding follow up node {next_node}: {steps} from {G_nodes[k]}")
          G_presentation[next_node] = (G_nodes[k],steps)
          next_node = follow_endomap(G,next_node)
          steps += 1
        print(f"next node {next_node} already has presentation")
        G_relations[G_nodes[k]] = ((G_nodes[k],steps),G_presentation[next_node])
      elif v == 1:
        print(f"Only 1 way into node {k}")
        if G_nodes[k] in G_presentation:
          print("already has a presentation")
        else:
          print("Found potential loop")
          G_presentation[G_nodes[k]] = (G_nodes[k],0)
          next_node = follow_endomap(G,G_nodes[k])
          steps = 1
          while not next_node in G_presentation:
            print(f"Adding follow up node {next_node}: {steps} from {G_nodes[k]}")
            G_presentation[next_node] = (G_nodes[k],steps)
            next_node = follow_endomap(G,next_node)
            steps += 1
          print(f"next node {next_node} already has presentation")
          G_relations[G_nodes[k]] = ((G_nodes[k],steps),G_presentation[next_node])
      else:
        print(f"More than 1 way in")
  
    return {
        "generators":list(G_relations.keys()),
        "relations":list(G_relations.values())
    }
endomap_alpha = make_presentation(XG)
print(endomap_alpha)
This code produced the following output:
Checking node 0 with 0 arrows in
Adding root generator X0
Adding follow up node X4: 1 from X0
Adding follow up node X5: 2 from X0
Adding follow up node X6: 3 from X0
Adding follow up node X7: 4 from X0
next node X5 already has presentation
Checking node 1 with 0 arrows in
Adding root generator X1
next node X5 already has presentation
Checking node 2 with 0 arrows in
Adding root generator X2
next node X6 already has presentation
Checking node 3 with 1 arrows in
Only 1 way into node 3
Found potential loop
Adding follow up node X8: 1 from X3
next node X3 already has presentation
Checking node 4 with 1 arrows in
Only 1 way into node 4
already has a presentation
Checking node 7 with 1 arrows in
Only 1 way into node 7
already has a presentation
Checking node 8 with 1 arrows in
Only 1 way into node 8
already has a presentation
Checking node 6 with 2 arrows in
More than 1 way in
Checking node 5 with 3 arrows in
More than 1 way in
{
    'generators': ['X0', 'X1', 'X2', 'X3'], 
    'relations': [
        (('X0', 5), ('X0', 2)), 
        (('X1', 1), ('X0', 2)), 
        (('X2', 1), ('X0', 3)), 
        (('X3', 2), ('X3', 0))
    ]
}
This matches up with our expected presentation for \(X^{\circlearrowright \alpha}\) with the substitions \(a = X_1\text{,}\) \(b = X_2\text{,}\) \(c = X_3\text{,}\) and \(d = X_4\text{.}\) The relations are expressed as a ordered pair based on the number of "presses" from a specified generator. This the equation \(\alpha^5 a = \alpha^2 a\) becomes (('X0', 5), ('X0', 2)), \(\alpha b = \alpha^2 a\) becomes (('X1', 1), ('X0', 2)), \(\alpha c = \alpha^3 a\) becomes (('X2', 1), ('X0', 3)), and \(\alpha^2 d = d\) becomes (('X3', 2), ('X3', 0)).
We can use a similar process to find a presentation for \(Y^{\circlearrowright \beta}\) as well with the following code.
y_nodes = ["Y"+y for y in ["p", "q", "r", "s", "t", "v", "u", "m", "l", "w", "x", "y", "z"]]

y_edges = [
    (y_nodes[0],y_nodes[2]),
    (y_nodes[1],y_nodes[2]),
    (y_nodes[2],y_nodes[4]),
    (y_nodes[3],y_nodes[4]),
    (y_nodes[4],y_nodes[5]),
    (y_nodes[5],y_nodes[6]),
    (y_nodes[6],y_nodes[3]),
    (y_nodes[7],y_nodes[8]),
    (y_nodes[8],y_nodes[7]),
    (y_nodes[9],y_nodes[10]),
    (y_nodes[10],y_nodes[11]),
    (y_nodes[11],y_nodes[9]),
    (y_nodes[12],y_nodes[11])
]
YG = nx.DiGraph()
YG.add_nodes_from(y_nodes)
YG.add_edges_from(y_edges)

endomap_beta = make_presentation(YG)
print(endomap_beta)
The output of the above code is the following presentation:
{
    'generators': ['p', 'q', 'z', 'm'], 
    'relations': [
        (('p', 6), ('p', 2)), 
        (('q', 1), ('p', 1)), 
        (('z', 4), ('z', 1)), 
        (('m', 2), ('m', 0))
    ]
}
Using the notation in the text, our corresponding relations would be \(\beta^6 p = \beta^2 p\text{,}\) \(\beta q = \beta p\text{,}\) \(\beta^4 z = \beta z\text{,}\) and \(\beta^2 m = m\text{.}\)
It seems like this method would be characterized as depth first search for the presentation. Depending on what order the nodes of the nodes used to express the maps, the above algorithm may produce a different result.
For example, suppose we defined X with the nodes in reverse:
XG2 = nx.DiGraph()
x2_nodes = [x_nodes[-1-i] for i in range(len(x_nodes))]
print(x2_nodes)
XG2.add_nodes_from(x2_nodes)
XG2.add_edges_from(x_edges)
endomap_alpha_prime = make_presentation(XG2)
print(endomap_alpha_prime)
We get the following output:
['X8', 'X7', 'X6', 'X5', 'X4', 'X3', 'X2', 'X1', 'X0']
Checking node 6 with 0 arrows in
Adding root generator X2
Adding follow up node X6: 1 from X2
Adding follow up node X7: 2 from X2
Adding follow up node X5: 3 from X2
next node X6 already has presentation
Checking node 7 with 0 arrows in
Adding root generator X1
next node X5 already has presentation
Checking node 8 with 0 arrows in
Adding root generator X0
Adding follow up node X4: 1 from X0
next node X5 already has presentation
Checking node 0 with 1 arrows in
Only 1 way into node 0
Found potential loop
Adding follow up node X3: 1 from X8
next node X8 already has presentation
Checking node 1 with 1 arrows in
Only 1 way into node 1
already has a presentation
Checking node 4 with 1 arrows in
Only 1 way into node 4
already has a presentation
Checking node 5 with 1 arrows in
Only 1 way into node 5
already has a presentation
Checking node 2 with 2 arrows in
More than 1 way in
Checking node 3 with 3 arrows in
More than 1 way in
{
    'generators': ['X2', 'X1', 'X0', 'X8'], 
    'relations': [
        (('X2', 4), ('X2', 1)),
        (('X1', 1), ('X2', 3)),
        (('X0', 2), ('X2', 3)),
        (('X8', 2), ('X8', 0))
    ]
}
This is an "equivalent presentation" for \(X^{\circlearrowright \alpha}\text{.}\) We have the same generators still, but they’re listed in different order and our relations are slightly different. The equivalent expressions are \(\alpha^4 c = \alpha^1 c\text{,}\) \(\alpha b = \alpha^3 c\text{,}\) \(\alpha^2 a = \alpha^3 c\text{,}\) and \(\alpha^2 d = d\text{.}\)
I’m thinking that my task for Exercise 9 will be a little easier if I can standardize my relations and generators in a unified manner that makes these "equivalent presentations" easier to identify. I’m wondering if using a "breadth first search" instead of a "depth first search" might allow me to more readily identify the cylces of each length.

Example 4.14.2. Exercise 7 (Part 2):.

Given my code above, let’s review where I got stuck last week and what I think my roadblock is. As mentioned in Exercise 8 above, part of that difficulty was matching up the cycles of matching lengths.
Solution.
My code from my previous attempt tried to follow my pen-and-paper process as closely as possible. The relevant Python code was as follows:
def get_cycles(endomap):
    cycles_by_length = {}
    cycles_by_generator = {}
    for k,v in enumerate(endomap["relations"]):
        if v[0][0] == v[1][0]:
        cl = abs(v[0][1] - v[1][1])
        mins = min(v[0][1],v[1][1])
        print(f"{v[0][0]} leads to cycle of length {cl} after {mins} steps")
        if not cl in cycles_by_length:
            cycles_by_length[cl] = {
                "primary_generators":[k],
                "secondary_generators":[]
            }
        else:
            cycles_by_length[cl]['primary_generators'].append(k)
        cycles_by_generator[v[0][0]] = cl
        else:
        cl = cycles_by_generator[v[1][0]]
        cycles_by_length[cl]['secondary_generators'].append(k)
        cycles_by_generator[v[0][0]] = cl
        return {
            "cycles_by_length":cycles_by_length, 
            "cycles_by_generator":cycles_by_generator
        }

def iterate_endomap(endomap, start, steps):
    out_val = start
    for i in range(steps):
        out_val = follow_endomap(endomap,out_val)
    return out_val

def make_assignments(gen,poss_vals):
    output = []
    for v in poss_vals:
      output.append({gen:v})
    return output

def join_assignments(ass1,ass2):
    if len(ass1) == 0:
      return ass2
    if len(ass2)<len(ass1):
      return join_assignments(ass2,ass1)
    output = []
    for a1 in ass1:
      for a2 in ass2:
        new_a1 = a1.copy()
        for k,v in zip(a2.keys(),a2.values()):
          new_a1[k] = v
        output.append(new_a1)
    return output

def find_predecessors(endomap,target,steps=1):
    preds = list(endomap.predecessors(target))
    if steps > 1 and len(preds) > 0:
      r_preds = []
      for t in preds:
        r_preds.extend(find_predecessors(endomap,t,steps-1))
      return r_preds
    else:
      return preds

assignments = []

for idx,rel in enumerate(endomap_alpha['relations']):
print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
x_gen = rel[0][0]
print(f'Attempting to match relation on: {x_gen}')
cl = alpha_cycles['cycles_by_generator'][x_gen]
print(f'Cycle Length: {cl}')
y_cycles = alpha_cycles['cycles_by_length'][cl]
poss_y_gen = y_cycles['primary_generators']+y_cycles['secondary_generators']
print(f"Possible Generators: {poss_y_gen}")

    
print(f"Attempting to satisfy: {rel}")
rel_lhs = iterate_endomap(XG,rel[0][0],rel[0][1])
rel_rhs = iterate_endomap(XG,rel[1][0],rel[1][1])
print(f"In X: {rel_lhs} = {rel_rhs}")
    
for gen in poss_y_gen:
    print(f"Testing generator: {gen}")
    if rel[0][0]==rel[1][0]:
    print("Trying to match up a loop")
    seen_in_Y = set([])
    eligible_subs = {}
    cur_y = endomap_beta['generators'][gen]
    y_steps = 0
    found_pairs = []
    while not cur_y in seen_in_Y:
        lhs = iterate_endomap(YG,cur_y,rel[0][1])
        rhs = iterate_endomap(YG,cur_y,rel[1][1])
        if lhs == rhs:
        print(f"Potential match at {cur_y}! {y_steps} from {gen}")
        found_pairs.append(cur_y)
        seen_in_Y.add(cur_y)
        y_steps += 1
        cur_y = iterate_endomap(YG,cur_y,1)

    print(f"Potential matches for {x_gen}: {found_pairs}")
    new_assignments = make_assignments(x_gen,found_pairs)
    print(f"New assignments: {new_assignments}")
    assignments = join_assignments(assignments,new_assignments)
    print(f"Joined assignments: {assignments}")
    else:
    print("Trying to match up a chain")
    new_assignments = []
    print(f"Existing Assignments: {assignments}")
    for ass in assignments:
        print(f"* Checking {ass}")
        if rel[1][0] in ass.keys():
        print(f"\t Found assignment from {rel[1][0]} to {ass[rel[1][0]]}")
        y_target = ass[rel[1][0]]

        x_lhs = iterate_endomap(XG,rel[0][0],rel[0][1])
        print(f"\tIn X: {rel[0][1]} steps from {rel[0][0]} is {x_lhs}")
        x_rhs = iterate_endomap(XG,rel[1][0],rel[1][1])
        print(f"\tIn X: {rel[1][1]} steps from {rel[1][0]} is {x_rhs}")


        y_rhs = iterate_endomap(YG,y_target,rel[1][1])
        print(f"\tIn Y: {rel[1][1]} steps from {y_target} is {y_rhs}")

        print(f"\tThis means {rel[0][0]} needs to map to {y_rhs} after {rel[0][1]} steps")
        poss_vals = find_predecessors(YG,y_rhs,rel[0][1])
        print(f"\tPossible Predecessors: {poss_vals}")

        for v in poss_vals:
            new_ass = ass.copy()
            new_ass[rel[0][0]] = v
            print(f"\tAdding new assignment: {new_ass}")
            new_assignments.append(new_ass)
    print("Done checking old assignments")
    print(f"New assignments({len(new_assignments)}): {new_assignments}")

    assignments = new_assignments

print(f"Pre-dupe removal: {len(assignments)}")
    
remove_dupes = set([str(x) for x in assignments])    
print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
print(f"Final assignment count: {len(remove_dupes)}")
print(remove_dupes)
The relevant output of which gives me:
Final assignment count: 12
{
    "{'X3': 'Ym', 'X0': 'Yw', 'X1': 'Yx', 'X2': 'Yy'}",
    "{'X3': 'Yl', 'X0': 'Yy', 'X1': 'Yw', 'X2': 'Yx'}", 
    "{'X3': 'Yl', 'X0': 'Yw', 'X1': 'Yz', 'X2': 'Yy'}", 
    "{'X3': 'Ym', 'X0': 'Yy', 'X1': 'Yw', 'X2': 'Yz'}", 
    "{'X3': 'Ym', 'X0': 'Yx', 'X1': 'Yy', 'X2': 'Yw'}", 
    "{'X3': 'Yl', 'X0': 'Yy', 'X1': 'Yw', 'X2': 'Yz'}", 
    "{'X3': 'Ym', 'X0': 'Yy', 'X1': 'Yw', 'X2': 'Yx'}", 
    "{'X3': 'Ym', 'X0': 'Yw', 'X1': 'Yz', 'X2': 'Yy'}", 
    "{'X3': 'Yl', 'X0': 'Yw', 'X1': 'Yx', 'X2': 'Yy'}", 
    "{'X3': 'Ym', 'X0': 'Yz', 'X1': 'Yy', 'X2': 'Yw'}", 
    "{'X3': 'Yl', 'X0': 'Yz', 'X1': 'Yy', 'X2': 'Yw'}", 
    "{'X3': 'Yl', 'X0': 'Yx', 'X1': 'Yy', 'X2': 'Yw'}"
}
These are precisely the 12 maps I came up with by hand. Not satisfied, I took this one step further with a more exhaustive search on a smaller "subgraph" of \(X\text{.}\) Specifically, between the cluster of points around my 3-cycle in \(X\) to the respective 3-cycle in \(Y\text{.}\)
sm_x_nodes = ["a", "b", "c", "α a", "α^2 a", "α^3 a", "α^4 a"]
sm_x_edges = [
    (sm_x_nodes[0],sm_x_nodes[3]),
    (sm_x_nodes[1],sm_x_nodes[4]),
    (sm_x_nodes[2],sm_x_nodes[5]),
    (sm_x_nodes[3],sm_x_nodes[4]),
    (sm_x_nodes[4],sm_x_nodes[5]),
    (sm_x_nodes[5],sm_x_nodes[6]),
    (sm_x_nodes[6],sm_x_nodes[4])
    
]
sm_XG = nx.DiGraph()
sm_XG.add_nodes_from(sm_x_nodes)
sm_XG.add_edges_from(sm_x_edges)

sm_y_nodes = ["w","x","y","z"]
sm_y_edges = [
    (sm_y_nodes[0],sm_y_nodes[1]),
    (sm_y_nodes[1],sm_y_nodes[2]),
    (sm_y_nodes[2],sm_y_nodes[0]),
    (sm_y_nodes[3],sm_y_nodes[2])
]
sm_YG = nx.DiGraph()
sm_YG.add_nodes_from(sm_y_nodes)
sm_YG.add_edges_from(sm_y_edges)

sm_X_alpha = make_presentation(sm_XG)
sm_Y_beta = make_presentation(sm_YG)

print(sm_X_alpha)
print("~~~~~~~~~~~~~~~~~~~")
print(sm_Y_beta)
This gives me the following (expected) presentations:
{'generators': ['a', 'b', 'c'], 'relations': [(('a', 5), ('a', 2)), (('b', 1), ('a', 2)), (('c', 1), ('a', 3))]}
~~~~~~~~~~~~~~~~~~~
{'generators': ['z'], 'relations': [(('z', 4), ('z', 1))]}
Given that this aligned with my expectation, I tried to generate ALL maps in \(\mathcal{S}\text{.}\) I know that since I’m mapping from a 7 element set to a 4 element set there are \(4^7 = 16384\) possible maps all together. I can then methodically test each map to see if they "preserve structure".
def make_all_maps(xs,ys):
    x_size = len(xs)
    y_size = len(ys)
    total = y_size**x_size
    print(total)
    all_assignments = []
    for x in xs:
        new_maps = []
        for y in ys:
            new_maps.append({x:y})
        all_assignments = join_assignments(all_assignments,new_maps) 
    return all_assignments
sm_maps = make_all_maps(sm_x_nodes,sm_y_nodes)

def preserves_structure(f,x_graph,y_graph):
    for k,v in zip(f.keys(),f.values()):
        #print(f"f({k}) = {v}")
        alpha_x = follow_endomap(x_graph,k)
        f_alpha_x = f[alpha_x]
        #print(f"alpha({k}) = {alpha_x}")
        #print(f"f({alpha_x}) = {f_alpha_x}")
        beta_y = follow_endomap(y_graph,v)
        #print(f"beta({v}) = {beta_y}")
        if beta_y != f_alpha_x:
              #print("fails to preserve structure")
              return False
    return True

sm_evaluations = list(filter(lambda x: preserves_structure(x,sm_XG,sm_YG),sm_maps))

print(len(sm_evaluations))
print(sm_evaluations)
Since each of these 6 maps could have either \(\bar{d} \rightarrow m\) and \(\bar{d} \rightarrow l\) as options, this essentially confirms the 12 maps I saw earlier.
Where this problem became interesting was when I stopped to consider the question: "how could I possibly get 7 maps?". After testing out some variations I came across the following graph which finally "worked".
sm_z_nodes = ["X"+str(i) for i in range(8)]
sm_z_edges = [
    (sm_z_nodes[0],sm_z_nodes[3]),
    (sm_z_nodes[1],sm_z_nodes[4]),
    (sm_z_nodes[2],sm_z_nodes[5]),
    (sm_z_nodes[3],sm_z_nodes[4]),
    (sm_z_nodes[4],sm_z_nodes[5]),
    (sm_z_nodes[5],sm_z_nodes[6]),
    (sm_z_nodes[6],sm_z_nodes[4]),
    (sm_z_nodes[7],sm_z_nodes[0])
    
]
sm_ZG = nx.DiGraph()
sm_ZG.add_nodes_from(sm_z_nodes)
sm_ZG.add_edges_from(sm_z_edges)

# make my latex easier
print("\n".join(["("+e[0]+") edge ("+e[1]+")" for e in sm_z_edges]))

nx.draw(sm_ZG, with_labels=True, font_weight='bold')

my_missing_maps = make_all_maps(sm_z_nodes,sm_y_nodes)
missing_map_evaluations = list(filter(lambda x: preserves_structure(x,sm_ZG,sm_YG),my_missing_maps))

print(len(missing_map_evaluations))
print(missing_map_evaluations)
Figure 4.14.3.
Once run, this gave me the following 7 structure preserving maps:
[
    {'X8': 'w', 'X7': 'y', 'X6': 'x', 'X5': 'w', 'X4': 'y', 'X3': 'w', 'X1': 'x', 'X2': 'y'}, 
    {'X8': 'x', 'X7': 'w', 'X6': 'y', 'X5': 'x', 'X4': 'w', 'X3': 'x', 'X1': 'y', 'X2': 'w'}, 
    {'X8': 'x', 'X7': 'w', 'X6': 'y', 'X5': 'x', 'X4': 'w', 'X3': 'z', 'X1': 'y', 'X2': 'w'},
    {'X8': 'y', 'X7': 'x', 'X6': 'w', 'X5': 'y', 'X4': 'x', 'X3': 'y', 'X1': 'w', 'X2': 'x'}, 
    {'X8': 'y', 'X7': 'x', 'X6': 'w', 'X5': 'y', 'X4': 'x', 'X3': 'y', 'X1': 'w', 'X2': 'z'}, 
    {'X8': 'z', 'X7': 'w', 'X6': 'y', 'X5': 'x', 'X4': 'w', 'X3': 'x', 'X1': 'y', 'X2': 'w'}, 
    {'X8': 'z', 'X7': 'w', 'X6': 'y', 'X5': 'x', 'X4': 'w', 'X3': 'z', 'X1': 'y', 'X2': 'w'}
]
This got me wondering if I had an object "0" with the property needed in the above map and that’s when I got really excited by this idea of the objects "0" and "1" being defined as maps.
The first time I started asking "is the text wrong" was way back in Article 1 2.1 when we first started counting maps. The discrepancy had to do with the way I was allowing things to be "undecided" in my model of a map in a way that the authors had explicitly not. Essentially, I had left \(0^0\) as "undefined" while the authors had defined \(0^0 = 1\text{,}\) so there was an off-by-one error in how I was counting my maps.
I think I’m implicitly doing the something similar again here. I feel like this mystery point \(0\) in my map comes from the fact that I have a map that assigns \(0\) to my generators. You can see this in my code where I use the adjacency matrix of my graph to count the incoming arrows at each node to find potential generators. From there, I had to go through a great deal of messy code to pull a generator out of the loop by iterating through points with exactly one arrow in. I’ve started to wonder if I should be thinking of "0" as a map from the "empty set" to "my generators" and treat my cycle of two more like a single element.
I’ve also started thinking about using \(\mathbb{N}\) as an intermediary step between \(X\) and \(Y\text{.}\)
Figure 4.14.4.
Part of my reasoning is that it seems like there are certain "natural maps" based on the structure of the endomap. Once such map is the "number of incoming arrows map", as established by the product of my "ones vector" with the "adjacency matrix". This is an "objective value" that can be assigned to each node.
Figure 4.14.5.
Then there’s a second "natural map" that assigns a value to each node based on "the number of steps to stabilize". That is, for any given point \(x \in X\text{,}\) how many times can we apply \(\alpha\) until we see a point that we’ve seen before.
Figure 4.14.6.
It seems like these two graphs labelings are somehow connected with the presentation of \(X^{\circlearrowright \alpha}\) in a way I can’t quite articulate yet.
There’s one last idea I want to get out my head for the week, and it’s this idea that one of my 12 maps is "special". Namely, I think the map that assigns \(\bar{a} \rightarrow z\) has a different interpretation that the others do not. Specifically, I think this map has an "isomorphism" with a subset of \(X\) that would describe as "the image of \(X\) under \(\alpha\)". Where does it say that the points in \(Y\) can’t also be points in \(X\text{?}\) Maybe that’s the missing binary question that produces my two missing maps.
Figure 4.14.7.
Once I apply \(\alpha\) to the points in \(X\text{,}\) each one of the "tails on the loop" shortens by 1. If I apply \(\alpha\) a second time, the tails disappear entirely. Maybe I should be taking exponents of my adjacency matrix instead of trying to break things apart into cycles.
I’m going to stop there for the week. Having to manually format the output of my Juypter notebooks is exhausting. If I’m going to continue solving questions with code it might be worthwhile to automate the process.