a collection of generative art scripts
1
2import math
3
4tau = math.pi * 2
5
6mod = 12
7
8valid_states = set()
9collapsed_states = set()
10collapse_map = {}
11
12def contains(L,R,P):
13 l_positions = range(mod-L, mod + 1)
14 r_positions = range(1, R+1)
15 valid_P = set(l_positions) | set(r_positions)
16 return P in valid_P
17
18# Step 0: build valid states
19for L in range(0, mod):
20 for R in range(0, mod):
21 if L + R < mod - 1:
22 l_positions = range(mod-L, mod + 1)
23 r_positions = range(1, R+1)
24 valid_P = set(l_positions) | set(r_positions)
25
26 collapsed_states.add((L, R, L + R + 1))
27 for P in valid_P:
28 valid_states.add((L, R, P))
29 collapse_map[(L, R, P)] = (L, R, L + R + 1)
30 elif L + R == mod - 1:
31 for P in range(1, mod):
32 valid_states.add(("done", P))
33
34print("Number of valid states:", len(valid_states))
35
36#print(valid_states)
37
38# Step 1: build directed edges
39edges = {} # key: state, value: list of next states
40collapsed_edges = {}
41
42for state in valid_states:
43 if state[0] == "done":
44 edges[state] = (state, []) # terminal
45 continue
46
47 L, R, P = state
48 out = []
49
50 # clockwise move
51 P1 = (P - 2) % mod + 1
52
53 L1 = L if contains(L,R,P1) else L+1
54
55 if L1 + R < mod - 1:
56 if (L1, R, P1) in valid_states:
57 out.append((L1, R, P1))
58 else:
59 print("AAAAAAAA", (L1, R, P1), (L, R, P))
60 else:
61 out.append(("done", P1))
62
63 # counterclockwise move
64 P2 = (P % mod) + 1
65
66 R1 = R if contains(L,R,P2) else R+1
67 if L + R1 < mod - 1:
68 if (L, R1, P2) in valid_states:
69 out.append((L, R1, P2))
70 else:
71 print("BBBBBBBB", (L, R1, P2), (L, R, P))
72 else:
73 out.append(("done", P2))
74
75 edges[state] = (state, out)
76
77# Sanity check
78outdegrees = [len(v) for (u,v) in edges.values()]
79terminal_count = sum(1 for (u,v) in edges.values() if len(v) == 0)
80print("Max outdegree:", max(outdegrees)) # should be 2
81print("Min outdegree (non-terminal):",
82 min([d for s,d in zip(edges.keys(), outdegrees) if s[0] != "done"])) # should be 2
83print("Number of terminal states:", terminal_count) # 12
84
85
86
87
88for state in valid_states:
89 outs = edges[state]
90 if len(outs[1]) < 2:
91 collapsed_edges[state] = outs
92 continue
93 outs_of_outs = [edges[out] for out in outs[1]]
94 dests_of_ooo = [dests for src, dests in outs_of_outs]
95 is_isomorph = [state in dests for dests in dests_of_ooo]
96 srcs_of_ooo = [src for src, dests in outs_of_outs]
97 if is_isomorph == [True, True]:
98 pass
99 elif state in collapse_map:
100 collapsed = collapse_map[state]
101 for dest, is_iso in zip([o for o in outs[1]], is_isomorph):
102 if not is_iso:
103 if dest in collapse_map:
104 if collapsed in collapsed_edges:
105 collapsed_edges[collapsed][1].append(collapse_map[dest])
106 else:
107 collapsed_edges[collapsed] = (collapsed, [collapse_map[dest]])
108 else:
109 if collapsed in collapsed_edges:
110 collapsed_edges[collapsed][1].append(dest)
111 else:
112 collapsed_edges[collapsed] = (collapsed, [dest])
113 else:
114 print(state, outs, "b")
115
116#print(collapsed_states)
117#print(collapsed_edges)
118
119
120
121import networkx as nx
122import matplotlib
123matplotlib.use("Agg")
124import matplotlib.pyplot as plt
125
126G = nx.DiGraph()
127
128# Add nodes and edges
129
130collapse = False
131
132drawn_edges = collapsed_edges if collapse else edges
133
134
135def collapsed_rep(state):
136 if state[0] == "done":
137 return state
138 return collapse_map.get(state, state)
139
140for src, outs in drawn_edges.items():
141 G.add_node(src)
142 for dst in outs[1]:
143 #print(src, dst)
144 G.add_edge(src, dst)
145
146import matplotlib.cm as cm
147import matplotlib.colors as mcolors
148
149reps = [collapsed_rep(s) for s in G.nodes]
150unique_reps = list(dict.fromkeys(reps)) # stable order
151
152cmap = cm.get_cmap("tab20", len(unique_reps))
153rep_to_color = {
154 r: mcolors.to_hex(cmap(i))
155 for i, r in enumerate(unique_reps)
156}
157
158# Build layout
159pos = {}
160
161def collapsed_pos(state):
162 if state[0] == "done":
163 _, P = state
164 x = (mod+1) * (mod) * mod
165 y = (mod+1) * mod * (P + mod // 2) + mod // 2
166 else:
167 (L, R, P) = state
168 x = (mod+1) * (mod) * P
169 y = (mod+1) * mod * ((P)/2) + (mod+1) * (mod * (mod - L) - mod//2)
170 return (x,y)
171
172if collapse:
173 for state in G.nodes:
174 px, py = collapsed_pos(state)
175 pos[state] = py, px
176else:
177 for state in G.nodes:
178 #if state[0] == "done":
179 # _, P = state
180 # x = (mod+1) * (P + (mod//2)) % mod
181 # y = (mod - 1) * (mod + 1)
182 #else:
183 # L, R, P = state
184 # x = (mod+1) * (P + (mod//2)) % mod
185 # y = (mod+1) * (L + R) + R
186 if state[0] == "done":
187 px, py = collapsed_pos(state)
188 pos[state] = py, px
189 elif state in collapse_map:
190 px, py = collapsed_pos(collapse_map[state])
191 p = state[2]
192 start = mod - state[0]
193 end = state[1]
194 p_rel = p - start if p >= start else p + state[0]
195 portion = 0.5
196 if state[0] + end > 0:
197 portion = (p_rel) / (state[0] + end)
198 pos[state] = (
199 #py + (mod // 2 * math.cos(portion * tau / 2)) * (mod - 2),
200 py + (portion - 0.5) * mod * (mod - 2),
201 px + (mod * 0.4) * mod * math.sin(portion * tau / 2)
202 )
203 print(px, py, state[0])
204 else:
205 print(state, "aaa")
206 pos[state] = (0, 0)
207
208#pos[(0,0,mod)] = ((mod+1) * (mod + (mod // 2)) % mod, (mod - 1) * (mod + 1))
209
210# Labels
211labels = {}
212collapsed_labels = {}
213for state in G.nodes:
214 if state[0] == "done":
215 labels[state] = f"end at {state[1]}"
216 collapsed_labels[state] = f"end at {state[1]}"
217 else:
218 L, R, P = state
219 if state in collapsed_states:
220 nL = 12 - L
221 nR = R if R != 0 else 12
222 collapsed_labels[state] = f"{nL} to {nR}"
223 else:
224 collapsed_labels[state] = f"({L}, {R}, {P})"
225 labels[state] = f"{P}" #f"({L}, {R}, {P}"
226
227spring = False
228
229done_states = [("done",n) for n in range(1,mod)]
230
231init_state = (0, 0, 1) if collapse else (0,0,mod)
232
233fixed_states = done_states + [init_state]
234
235if spring:
236 pos = nx.spring_layout(
237 G,
238 pos=pos, # your existing layout
239 fixed=fixed_states, # or list of nodes to keep fixed
240 #center=(6 * 13, 11 * 13 / 2),
241 k=1,
242 iterations=10000
243 )
244else:
245 #pos = nx.kamada_kawai_layout(G)
246 #pos = nx.bfs_layout(G, init_state)
247 #pos = nx.planar_layout(G)
248 pass
249
250node_colors = "lightblue"
251
252if not collapse:
253 node_colors = [rep_to_color[collapsed_rep(s)] for s in G.nodes]
254else:
255 node_colors = [rep_to_color[s] for s in G.nodes]
256
257frame_count = 512
258
259probs = {}
260
261for state in valid_states:
262 probs[state] = 0
263
264probs[(0,0,mod)] = 1
265
266
267for frame_index in range(frame_count):
268 # Draw
269 sizes = []
270 if collapse:
271 for s in G.nodes:
272 size = 0
273 for key in collapse_map:
274 if s == collapse_map[key]:
275 size += probs[key]
276 #print(s, key, collapse_map[key])
277 if s in done_states:
278 size += probs[s]
279 sizes.append(30 + size * 50000)
280 else:
281 sizes = [30 + probs[s] * 50000 for s in G.nodes]
282
283 plt.figure(figsize=(14, 10))
284 nx.draw(
285 G,
286 pos,
287 node_size=sizes,
288 node_color=node_colors,
289 edge_color="gray",
290 arrowsize=10 if collapse else 5,
291 with_labels=False
292 )
293
294 used_labels = collapsed_labels if collapse else labels
295
296 for state in done_states:
297 p = probs[state] * 100
298 used_labels[state] = f"{state[1]}: {p:.2f}%"
299
300 nx.draw_networkx_labels(G, pos, used_labels, font_size=6)
301
302
303 plt.tight_layout()
304
305 plt.savefig(f"{frame_index:06d}.png", dpi=100, bbox_inches="tight")
306 plt.close()
307
308 probs_next = {}
309 for state in valid_states:
310 if not state in done_states:
311 probs_next[state] = 0
312 for state in done_states:
313 probs_next[state] = probs[state]
314 for state in valid_states:
315 if state in done_states:
316 continue
317 else:
318 for dst in edges[state][1]:
319 probs_next[dst] += probs[state] / 2
320
321 probs = probs_next
322
323
324#plt.tight_layout()
325#plt.show()