Eyob-Sol commited on
Commit
dbc8c36
·
verified ·
1 Parent(s): 8de43d6

Upload 24 files

Browse files
app.py ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hugging Face / local Gradio app for exploring Collatz structures.
3
+
4
+ Row 1:
5
+ - Inverse tree controls
6
+ - Minimal subtree controls
7
+ - Statistics for the currently displayed graph
8
+
9
+ Row 2:
10
+ - Image display area (Zoom & Scroll or Fit to Width)
11
+ """
12
+
13
+ from __future__ import annotations
14
+ import io
15
+ import matplotlib.pyplot as plt
16
+
17
+ from typing import Any
18
+ from pathlib import Path
19
+ import base64
20
+
21
+ import gradio as gr
22
+
23
+ from src.utils import (
24
+ build_and_render_collatz_tree,
25
+ build_and_render_minimal_subtree,
26
+ safe_int,
27
+ )
28
+ from src.collatz.metrics import compute_basic_graph_stats, format_stats_markdown
29
+
30
+
31
+ # ============================================================
32
+ # Helpers
33
+ # ============================================================
34
+
35
+ def image_file_to_html(
36
+ path: str,
37
+ mode: str = "Zoom & Scroll",
38
+ box_height: int = 650,
39
+ ) -> str:
40
+ """
41
+ Convert an image file into an HTML block.
42
+
43
+ Modes:
44
+ - "Zoom & Scroll": full resolution inside fixed-height scroll-box
45
+ - "Fit to Width" : scaled to column width, whole graph visible
46
+ """
47
+ img_path = Path(path)
48
+ if not img_path.is_file():
49
+ return "<p style='color:red;'>Error: image file not found.</p>"
50
+
51
+ data = img_path.read_bytes()
52
+ encoded = base64.b64encode(data).decode("ascii")
53
+
54
+ if mode == "Fit to Width":
55
+ # Show whole graph scaled to container width
56
+ html = f"""
57
+ <div style="
58
+ border:1px solid #ddd;
59
+ border-radius:6px;
60
+ padding:4px;
61
+ background-color:#fafafa;
62
+ ">
63
+ <img src="data:image/png;base64,{encoded}"
64
+ style="display:block; max-width:100%; height:auto; margin:0 auto;" />
65
+ </div>
66
+ """
67
+ else:
68
+ # Zoom & scroll (full resolution)
69
+ html = f"""
70
+ <div style="
71
+ display:flex;
72
+ justify-content:center;
73
+ width:100%;
74
+ ">
75
+ <div style="
76
+ height:{box_height}px;
77
+ overflow:auto;
78
+ border:1px solid #ddd;
79
+ border-radius:6px;
80
+ padding:4px;
81
+ background-color:#fafafa;
82
+ width:fit-content;
83
+ max-width:100%;
84
+ ">
85
+ <img src="data:image/png;base64,{encoded}"
86
+ style="display:block; max-width:none; max-height:none;" />
87
+ </div>
88
+ </div>
89
+ """
90
+
91
+ return html
92
+
93
+ def parity_histogram_html(stats: dict) -> str:
94
+ """
95
+ Create a small odd vs even histogram as an embedded PNG <img> tag.
96
+ """
97
+ num_odd = stats.get("num_odd", 0)
98
+ num_even = stats.get("num_even", 0)
99
+
100
+ # If no nodes, nothing to plot
101
+ if num_odd == 0 and num_even == 0:
102
+ return "<p>_No nodes to plot._</p>"
103
+
104
+ labels = ["Odd", "Even"]
105
+ values = [num_odd, num_even]
106
+
107
+ fig, ax = plt.subplots(figsize=(3.5, 2.5))
108
+ ax.bar(labels, values)
109
+ ax.set_ylabel("Count")
110
+ ax.set_title("Odd vs Even Nodes")
111
+ fig.tight_layout()
112
+
113
+ buf = io.BytesIO()
114
+ fig.savefig(buf, format="png")
115
+ plt.close(fig)
116
+ encoded = base64.b64encode(buf.getvalue()).decode("ascii")
117
+
118
+ return f'<img src="data:image/png;base64,{encoded}" style="max-width:100%; height:auto;" />'
119
+
120
+ # ============================================================
121
+ # Callbacks
122
+ # ============================================================
123
+
124
+ def inverse_tree_callback(
125
+ backbone_length: Any,
126
+ branch_length: Any,
127
+ max_depth: Any,
128
+ view_mode: str,
129
+ ):
130
+ """
131
+ Generate the inverse structural tree and return (image_html, stats_md).
132
+ """
133
+
134
+ b_len = safe_int(backbone_length, default=8)
135
+ r_len = safe_int(branch_length, default=4)
136
+ depth = safe_int(max_depth, default=2)
137
+
138
+ # clamp for demo
139
+ b_len = max(4, min(b_len, 10))
140
+ r_len = max(1, min(r_len, 7))
141
+ depth = max(0, min(depth, 4))
142
+
143
+ image_path, df_edges = build_and_render_collatz_tree(
144
+ backbone_length=b_len,
145
+ branch_length=r_len,
146
+ max_depth=depth,
147
+ return_edges=True,
148
+ )
149
+
150
+ html_block = image_file_to_html(image_path, view_mode, 650)
151
+
152
+ stats = compute_basic_graph_stats(df_edges)
153
+ stats_md = format_stats_markdown(stats)
154
+
155
+ hist_html = parity_histogram_html(stats)
156
+
157
+ return html_block, stats_md, hist_html
158
+
159
+
160
+ def minimal_subtree_callback(
161
+ N: Any,
162
+ view_mode: str,
163
+ ):
164
+ """
165
+ Generate the minimal subtree up to N and return (image_html, stats_md).
166
+ """
167
+
168
+ N = safe_int(N, default=7)
169
+ # Cap N for demo to prevent huge graphs
170
+ N = max(1, min(N, 2000))
171
+
172
+ image_path, df_edges = build_and_render_minimal_subtree(
173
+ N,
174
+ return_edges=True,
175
+ filename=f"minimal_subtree",
176
+ )
177
+
178
+ html_block = image_file_to_html(image_path, view_mode, 650)
179
+
180
+ stats = compute_basic_graph_stats(df_edges)
181
+ stats_md = format_stats_markdown(stats)
182
+ hist_html = parity_histogram_html(stats)
183
+
184
+ return html_block, stats_md, hist_html
185
+
186
+
187
+ # ============================================================
188
+ # Build UI
189
+ # ============================================================
190
+
191
+ def build_demo() -> gr.Blocks:
192
+
193
+ with gr.Blocks(title="Collatz Explorer") as demo:
194
+
195
+ gr.Markdown(
196
+ """
197
+ <h1 style="text-align:center; margin-bottom:20px;">
198
+ 🔷 <span style="font-weight:700;">Collatz Structural Explorer</span> 🔷
199
+ </h1>
200
+
201
+ <div style="text-align:justify;">
202
+
203
+ <div style="margin-left:20px; margin-bottom:15px;">
204
+ The <em>Collatz Structural Explorer</em> accompanies the research article
205
+ <a href="https://www.tandfonline.com/doi/full/10.1080/27684830.2025.2542052" target="_blank" style="color:#1a73e8; text-decoration:none; font-weight:600;">
206
+ Unfolding the Collatz Tree: An Indirect Structural Proof of the Collatz Conjecture
207
+ </a>, published in the <em>Journal of Experimental Mathematics</em> (Taylor and Francis).
208
+ This interactive demonstration is intended to visually illustrate key structural ideas from the paper using a dynamic inverse-tree perspective.
209
+ </div>
210
+
211
+ <div style="margin-left:20px;">
212
+ It highlights how the inverse Collatz map, structural branch rules, and the minimal subtree containing all natural numbers up to a chosen bound N collectively reconstruct the forward Collatz dynamics in an organized and interpretable way.
213
+ Through real-time visualization and graph statistics, readers can explore the hierarchical structure of the Collatz process and gain an intuitive understanding of the theoretical insights developed in the publication.
214
+ </div>
215
+
216
+ </div>
217
+ <div style="height:50px;"></div>
218
+ """
219
+ )
220
+
221
+ # ============================
222
+ # Row 1: controls + stats
223
+ # ============================
224
+ with gr.Row():
225
+ # Inverse tree controls
226
+ with gr.Column(scale=1, min_width=260):
227
+ gr.Markdown("### Inverse Collatz Tree")
228
+
229
+ backbone_input = gr.Slider(
230
+ 4, 10, value=8, step=1,
231
+ label="Backbone length (powers of 2)",
232
+ )
233
+ branch_input = gr.Slider(
234
+ 1, 7, value=4, step=1,
235
+ label="Branch length",
236
+ )
237
+ depth_input = gr.Slider(
238
+ 0, 4, value=2, step=1,
239
+ label="Branch recursion depth",
240
+ )
241
+
242
+ view_mode_inverse = gr.Radio(
243
+ ["Zoom & Scroll", "Fit to Width"],
244
+ value="Zoom & Scroll",
245
+ label="View mode for inverse tree",
246
+ )
247
+
248
+ gen_inverse = gr.Button("Generate Inverse Tree")
249
+
250
+ # Minimal subtree controls
251
+ with gr.Column(scale=1, min_width=260):
252
+ gr.Markdown("### Minimal Subtree up to N")
253
+
254
+ N_input = gr.Number(
255
+ value=7, precision=0,
256
+ label="Upper bound N (includes all 1..N)",
257
+ info="Demo max = 2000",
258
+ )
259
+
260
+ view_mode_minimal = gr.Radio(
261
+ ["Zoom & Scroll", "Fit to Width"],
262
+ value="Zoom & Scroll",
263
+ label="View mode for minimal subtree",
264
+ )
265
+
266
+ gen_minimal = gr.Button("Generate Minimal Subtree")
267
+
268
+ # Stats panel
269
+ # Stats + histogram (side by side)
270
+ with gr.Column(scale=1):
271
+ gr.Markdown("### Current Graph Statistics")
272
+
273
+ with gr.Row():
274
+ # Column for text statistics
275
+ with gr.Column(scale=2, min_width=140):
276
+ stats_output = gr.Markdown(
277
+ value="_No graph generated yet._"
278
+ )
279
+
280
+ # Column for histogram (right side)
281
+ with gr.Column(scale=2, min_width=140):
282
+ hist_output = gr.HTML(
283
+ value="",
284
+ label="Odd vs Even Histogram",
285
+ )
286
+
287
+ # ============================
288
+ # Row 2: image display area
289
+ # ============================
290
+ with gr.Row():
291
+ with gr.Column():
292
+ image_output = gr.HTML(
293
+ label="Current Collatz Graph",
294
+ )
295
+ gr.Markdown(
296
+ """
297
+ **Display tips:**
298
+ - In **Zoom & Scroll** mode, use the scrollbars to explore large graphs.
299
+ - In **Fit to Width** mode, the graph is scaled to the available width.
300
+ - You can right-click the image to open it in a new tab or save it.
301
+ """
302
+ )
303
+
304
+ # Wire buttons: both update the same image + stats
305
+ gen_inverse.click(
306
+ fn=inverse_tree_callback,
307
+ inputs=[backbone_input, branch_input, depth_input, view_mode_inverse],
308
+ outputs=[image_output, stats_output, hist_output],
309
+ )
310
+
311
+ gen_minimal.click(
312
+ fn=minimal_subtree_callback,
313
+ inputs=[N_input, view_mode_minimal],
314
+ outputs=[image_output, stats_output, hist_output],
315
+ )
316
+
317
+ return demo
318
+
319
+
320
+ demo = build_demo()
321
+
322
+ if __name__ == "__main__":
323
+ demo.launch()
outputs/.DS_Store ADDED
Binary file (6.15 kB). View file
 
outputs/collatz_tree.png ADDED
outputs/minimal_subtree.png ADDED
requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core numerical & data utilities
2
+ pandas
3
+ numpy
4
+
5
+ # Graph algorithms & rendering
6
+ graphviz
7
+ networkx
8
+
9
+ # Visualizations
10
+ matplotlib
11
+
12
+ # HF Space interface
13
+ gradio
14
+
15
+ # System utilities (optional but requested)
16
+ psutil
17
+ matplotlib
src/.DS_Store ADDED
Binary file (6.15 kB). View file
 
src/__init__.py ADDED
File without changes
src/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (185 Bytes). View file
 
src/__pycache__/utils.cpython-312.pyc ADDED
Binary file (3.69 kB). View file
 
src/collatz/.DS_Store ADDED
Binary file (6.15 kB). View file
 
src/collatz/__init__.py ADDED
File without changes
src/collatz/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (193 Bytes). View file
 
src/collatz/__pycache__/inverse_tree.cpython-312.pyc ADDED
Binary file (3.97 kB). View file
 
src/collatz/__pycache__/metrics.cpython-312.pyc ADDED
Binary file (4.22 kB). View file
 
src/collatz/__pycache__/minimal_subtree.cpython-312.pyc ADDED
Binary file (6.1 kB). View file
 
src/collatz/inverse_tree.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Tuple
4
+
5
+ import pandas as pd
6
+
7
+
8
+ def generate_collatz_tree(
9
+ backbone_length: int = 11,
10
+ branch_length: int = 5,
11
+ max_depth: int = 3,
12
+ ) -> pd.DataFrame:
13
+ """
14
+ Generate an inverse Collatz tree structure starting from 1, with controlled
15
+ depth and branch expansion, based on the reverse Collatz rule:
16
+
17
+ If (n - 1) % 3 == 0 and n is even, then (n - 1) / 3 is a valid preimage.
18
+
19
+ The construction uses:
20
+ - a "backbone" of powers of 2 starting at 1,
21
+ - branches that grow from specific backbone nodes and from later branch nodes,
22
+ - recursive expansion up to exactly `max_depth` levels.
23
+
24
+ Parameters
25
+ ----------
26
+ backbone_length : int, default=11
27
+ Length of the backbone (powers of 2).
28
+ Backbone nodes are exactly: 1, 2, 4, ..., 2^(backbone_length - 1).
29
+ branch_length : int, default=5
30
+ Length of each branch created from valid reverse steps.
31
+ Branch nodes are formed by repeated doubling from the branch root.
32
+ Branches always have exactly `branch_length` nodes.
33
+ max_depth : int, default=3
34
+ Number of recursive branch-expansion levels.
35
+
36
+ Returns
37
+ -------
38
+ pd.DataFrame
39
+ DataFrame with two columns:
40
+ - "Source": child node (preimage under the Collatz map),
41
+ - "Target": parent node (image under the Collatz map).
42
+
43
+ These edges define a directed tree (or forest) rooted at 1.
44
+ """
45
+
46
+ if backbone_length < 2:
47
+ raise ValueError("backbone_length must be at least 2.")
48
+
49
+ if branch_length < 1:
50
+ raise ValueError("branch_length must be at least 1.")
51
+
52
+ if max_depth < 0:
53
+ raise ValueError("max_depth must be non-negative.")
54
+
55
+ # Backbone nodes: 1, 2, 4, ..., 2^(backbone_length - 1)
56
+ branches: List[List[int]] = [[2 ** i for i in range(backbone_length)]]
57
+
58
+ # Backbone edges: 2 -> 1, 4 -> 2, 8 -> 4, ...
59
+ edges: List[Tuple[int, int]] = [(2 ** i, 2 ** (i - 1)) for i in range(1, backbone_length)] + [(1,4)]
60
+
61
+ # Backbone nodes that will create branches: 2^4, 2^6, 2^8, ...
62
+ branch_creating_numbers: List[int] = [
63
+ 2 ** i for i in range(4, backbone_length, 2)
64
+ ]
65
+
66
+ # Recursive branch expansion with EXACT branch_length and max_depth
67
+ for _ in range(max_depth):
68
+ next_level_branch_creators: List[int] = []
69
+
70
+ for branch_num in branch_creating_numbers:
71
+ # Reverse Collatz: (branch_num - 1) / 3
72
+ parent = (branch_num - 1) // 3
73
+ edges.append((parent, branch_num))
74
+
75
+ # Create a branch of EXACT length = branch_length via repeated doubling
76
+ new_branch = [parent * (2 ** i) for i in range(branch_length)]
77
+ branches.append(new_branch)
78
+
79
+ # Link within the branch (2a -> a, 4a -> 2a, ...)
80
+ edges.extend(
81
+ (new_branch[i + 1], new_branch[i])
82
+ for i in range(len(new_branch) - 1)
83
+ )
84
+
85
+ # Candidates for further branching on next level
86
+ next_level_branch_creators.extend(
87
+ n for n in new_branch[1:] if (n - 1) % 3 == 0
88
+ )
89
+
90
+ branch_creating_numbers = next_level_branch_creators
91
+
92
+ df_edges = pd.DataFrame(edges, columns=["Source", "Target"]).drop_duplicates()
93
+
94
+ return df_edges
src/collatz/metrics.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Metrics and basic statistics for Collatz graphs.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Dict, Any, Iterable
8
+
9
+ import pandas as pd
10
+
11
+
12
+ def _to_int_nodes(nodes: Iterable[Any]) -> list[int]:
13
+ """
14
+ Safely convert an iterable of node labels to a list of integers.
15
+ Non-convertible labels are skipped.
16
+ """
17
+ int_nodes: list[int] = []
18
+ for n in nodes:
19
+ try:
20
+ int_nodes.append(int(n))
21
+ except Exception:
22
+ continue
23
+ return int_nodes
24
+
25
+
26
+ def compute_basic_graph_stats(df_edges: pd.DataFrame) -> Dict[str, Any]:
27
+ """
28
+ Compute basic statistics for a Collatz graph represented
29
+ as a DataFrame of edges with columns ["Source", "Target"].
30
+
31
+ Returns a dictionary with:
32
+ - num_nodes: total number of distinct nodes
33
+ - num_edges: total number of edges
34
+ - num_odd: number of odd-valued nodes
35
+ - num_even: number of even-valued nodes
36
+ - min_node: minimum node value (if any)
37
+ - max_node: maximum node value (if any)
38
+ - num_cycles: number of cycles (here always 1: the trivial 1–2–4–1 cycle)
39
+ """
40
+ if not {"Source", "Target"}.issubset(df_edges.columns):
41
+ raise ValueError("df_edges must contain 'Source' and 'Target' columns.")
42
+
43
+ raw_nodes = set(df_edges["Source"]).union(set(df_edges["Target"]))
44
+ nodes = _to_int_nodes(raw_nodes)
45
+
46
+ num_nodes = len(nodes)
47
+ num_edges = len(df_edges)
48
+
49
+ num_odd = sum(1 for n in nodes if n % 2 == 1)
50
+ num_even = sum(1 for n in nodes if n % 2 == 0)
51
+
52
+ min_node = min(nodes) if nodes else None
53
+ max_node = max(nodes) if nodes else None
54
+
55
+ # By construction, your graphs contain only the trivial 1–2–4–1 cycle.
56
+ num_cycles = 1 if num_nodes > 0 else 0
57
+
58
+ return {
59
+ "num_nodes": num_nodes,
60
+ "num_edges": num_edges,
61
+ "num_odd": num_odd,
62
+ "num_even": num_even,
63
+ "min_node": min_node,
64
+ "max_node": max_node,
65
+ "num_cycles": num_cycles,
66
+ }
67
+
68
+
69
+ def format_stats_markdown(stats: Dict[str, Any]) -> str:
70
+ """
71
+ Format the statistics dictionary returned by compute_basic_graph_stats
72
+ into a human-readable Markdown string for display in the UI.
73
+ """
74
+ if not stats:
75
+ return "_No statistics available._"
76
+
77
+ num_nodes = stats.get("num_nodes", 0)
78
+ num_edges = stats.get("num_edges", 0)
79
+ num_odd = stats.get("num_odd", 0)
80
+ num_even = stats.get("num_even", 0)
81
+ min_node = stats.get("min_node", None)
82
+ max_node = stats.get("max_node", None)
83
+ num_cycles = stats.get("num_cycles", 0)
84
+
85
+ lines = [
86
+ "### Graph Statistics",
87
+ "",
88
+ f"- **Nodes:** {num_nodes}",
89
+ f"- **Edges:** {num_edges}",
90
+ f"- **Odd nodes:** {num_odd}",
91
+ f"- **Even nodes:** {num_even}",
92
+ ]
93
+
94
+ if min_node is not None and max_node is not None:
95
+ lines.append(f"- **Node value range:** {min_node} to {max_node}")
96
+
97
+ # Trivial cycle information
98
+ if num_cycles:
99
+ lines.append(f"- **Cycles:** {num_cycles} (trivial cycle 1 → 2 → 4 → 1)")
100
+
101
+ return "\n".join(lines)
src/collatz/minimal_subtree.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Minimal Collatz subtree construction up to N, using the structural
3
+ branch algorithm from the paper.
4
+
5
+ This file implements:
6
+
7
+ - Algorithm 5: GenerateCollatzBranches
8
+ - Algorithm 6: BuildCollatzSubtreeEdges
9
+
10
+ The combined effect is to construct the minimal Collatz subtree
11
+ (containing all natural numbers 1..N, plus necessary ancestors)
12
+ as a directed graph with edges (child -> parent) following the
13
+ forward Collatz map:
14
+
15
+ T(n) = n/2 if n is even
16
+ 3n + 1 if n is odd
17
+
18
+ Edges are of the form (n, T(n)), including the structural edge (1, 4).
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from typing import Dict, List, Tuple
24
+
25
+ import pandas as pd
26
+
27
+
28
+ # ============================================================
29
+ # Algorithm 5: GenerateCollatzBranches
30
+ # ============================================================
31
+
32
+ def generate_collatz_branches(N: int) -> Dict[int, List[int]]:
33
+ """
34
+ Implement Algorithm 5: GenerateCollatzBranches.
35
+
36
+ Parameters
37
+ ----------
38
+ N : int
39
+ Natural number N (upper bound for starting odd numbers).
40
+
41
+ Returns
42
+ -------
43
+ Dict[int, List[int]]
44
+ Dictionary `Tree` mapping each odd root y to an increasing list
45
+ of values of the form y * 2^m. Initially, each odd x ≤ N is mapped
46
+ to its doubling sequence up to N, then extended as needed when
47
+ exploring Collatz branches from all odd x in [3, N].
48
+
49
+ Notes
50
+ -----
51
+ Structural summary:
52
+
53
+ 1) For each odd x ≤ N, create the base branch:
54
+ Tree[x] = [x, 2x, 4x, ..., 2^k x] (all ≤ N).
55
+
56
+ 2) For each odd x from 3 to N, repeatedly perform:
57
+ b = 3x + 1
58
+ m = b & (-b) # largest power of 2 dividing b
59
+ y = b // m # odd part of b
60
+
61
+ and extend the branch for y so that it contains b (and any
62
+ intermediate doublings) if needed. Then set x <- y and repeat
63
+ until x = 1 or we break due to an already-covered b.
64
+ """
65
+
66
+ if N < 1:
67
+ raise ValueError("N must be a positive integer.")
68
+
69
+ Tree: Dict[int, List[int]] = {}
70
+
71
+ # ------------------------------------------------------------
72
+ # Lines 2–7: For all odd numbers x ≤ N, initialize base branches
73
+ # ------------------------------------------------------------
74
+ for x in range(1, N + 1, 2): # odd x: 1, 3, 5, ...
75
+ seq: List[int] = []
76
+ power = 1 # corresponds to 2^0, 2^1, 2^2, ...
77
+ while x * power <= N:
78
+ seq.append(x * power)
79
+ power *= 2
80
+ Tree[x] = seq
81
+
82
+ # ------------------------------------------------------------
83
+ # Lines 8–33: Extend branches using the 3x+1 structure
84
+ # ------------------------------------------------------------
85
+ for start_x in range(3, N + 1, 2): # x from 3 to N, odd
86
+ x = start_x
87
+
88
+ # while x ≠ 1 do
89
+ while x != 1:
90
+ # b ← 3x + 1
91
+ b = 3 * x + 1
92
+
93
+ # m ← b ∧ (−b) (least significant 1 bit, largest power of 2 dividing b)
94
+ m = b & -b
95
+
96
+ # y ← b / m (odd part)
97
+ y = b // m
98
+
99
+ # if y ∈ Tree then
100
+ if y in Tree:
101
+ # if b ∉ Tree[y] then
102
+ if b not in Tree[y]:
103
+ # power ← 2^{len(Tree[y])}
104
+ power = 1 << len(Tree[y])
105
+
106
+ # while y · power ≤ b do
107
+ while y * power <= b:
108
+ # Append y · power to Tree[y]
109
+ Tree[y].append(y * power)
110
+ # power ← power · 2
111
+ power *= 2
112
+ else:
113
+ # else break
114
+ break
115
+ else:
116
+ # else:
117
+ # power ← 1, seq ← []
118
+ power = 1
119
+ seq: List[int] = []
120
+
121
+ # while y · power ≤ b do
122
+ while y * power <= b:
123
+ # Append y · power to seq
124
+ seq.append(y * power)
125
+ # power ← power · 2
126
+ power *= 2
127
+
128
+ # Tree[y] ← seq
129
+ Tree[y] = seq
130
+
131
+ # x ← y
132
+ x = y
133
+
134
+ # Line 34: return Tree
135
+ return Tree
136
+
137
+
138
+ # ============================================================
139
+ # Algorithm 6: BuildCollatzSubtreeEdges
140
+ # ============================================================
141
+
142
+ def build_collatz_subtree_edges(N: int, Tree: Dict[int, List[int]]) -> pd.DataFrame:
143
+ """
144
+ Implement Algorithm 6: BuildCollatzSubtreeEdges.
145
+
146
+ Parameters
147
+ ----------
148
+ N : int
149
+ Upper bound N (not explicitly used inside, but kept for interface
150
+ consistency with the paper).
151
+ Tree : Dict[int, List[int]]
152
+ Dictionary produced by generate_collatz_branches(N).
153
+
154
+ Returns
155
+ -------
156
+ pd.DataFrame
157
+ DataFrame with columns ["Source", "Target"], encoding directed
158
+ edges (child -> parent) of a connected minimal Collatz subtree
159
+ containing all integers up to N.
160
+
161
+ The pseudocode is followed line by line:
162
+
163
+ 1) For each branch list S in Tree, add edges S[i] -> S[i-1].
164
+ 2) For each odd root x > 1, compute:
165
+ b = 3x + 1
166
+ m = b & (-b)
167
+ y = b / m
168
+ find the index t of b in Tree[y], and add edge:
169
+ Tree[x][0] -> Tree[y][t]
170
+ which corresponds structurally to x -> 3x + 1.
171
+ 3) Finally, add the structural edge (1, 4).
172
+ """
173
+
174
+ edges: List[Tuple[int, int]] = []
175
+
176
+ # ------------------------------------------------------------
177
+ # Lines 2–6: For all branches (r, S) in Tree
178
+ # ------------------------------------------------------------
179
+ for _, S in Tree.items():
180
+ # S = [r, 2r, 4r, ...]; add edges S[i] -> S[i-1] for i = 1..|S|-1
181
+ for i in range(1, len(S)):
182
+ child = S[i]
183
+ parent = S[i - 1]
184
+ edges.append((child, parent))
185
+
186
+ # ------------------------------------------------------------
187
+ # Lines 7–13: Link branches via the 3x+1 structure
188
+ # ------------------------------------------------------------
189
+ for x in sorted(Tree.keys()):
190
+ if x > 1: # only odd roots greater than 1
191
+ # b = 3x + 1
192
+ b = 3 * x + 1
193
+
194
+ # m = b ∧ (-b)
195
+ m = b & -b
196
+
197
+ # y = b / m
198
+ y = b // m
199
+
200
+ # Let i ← index of b in Tree[y]
201
+ if y not in Tree:
202
+ # If Tree[y] does not exist, structure is inconsistent; skip.
203
+ continue
204
+
205
+ try:
206
+ t = Tree[y].index(b)
207
+ except ValueError:
208
+ # If b is not present in Tree[y], skip this link.
209
+ continue
210
+
211
+ # Add edge (Tree[x][0], Tree[y][i]) to edges
212
+ root_x = Tree[x][0] # odd root x
213
+ match_y = Tree[y][t] # this is b itself
214
+ edges.append((root_x, match_y))
215
+
216
+ # ------------------------------------------------------------
217
+ # Line 14: Add edge (1, 4)
218
+ # ------------------------------------------------------------
219
+ edges.append((1, 4))
220
+
221
+ # ------------------------------------------------------------
222
+ # Line 15: return edges as DataFrame
223
+ # ------------------------------------------------------------
224
+ df = pd.DataFrame(edges, columns=["Source", "Target"]).drop_duplicates()
225
+ return df
226
+
227
+
228
+ # ============================================================
229
+ # Convenience wrapper: minimal subtree edges up to N
230
+ # ============================================================
231
+
232
+ def generate_minimal_collatz_subtree_edges(N: int) -> pd.DataFrame:
233
+ """
234
+ High-level helper that runs Algorithm 5 and Algorithm 6:
235
+
236
+ Tree = GenerateCollatzBranches(N)
237
+ edges = BuildCollatzSubtreeEdges(N, Tree)
238
+
239
+ and returns the resulting edge DataFrame.
240
+
241
+ Parameters
242
+ ----------
243
+ N : int
244
+ Upper bound on the natural numbers to be included (1..N).
245
+
246
+ Returns
247
+ -------
248
+ pd.DataFrame
249
+ Edge list with columns ["Source", "Target"].
250
+ """
251
+ Tree = generate_collatz_branches(N)
252
+ df_edges = build_collatz_subtree_edges(N, Tree)
253
+ return df_edges
src/utils.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ High-level utilities for Collatz operations.
3
+
4
+ This module provides glue functions used by the web interface (app.py),
5
+ combining steps such as:
6
+
7
+ - generating an inverse Collatz tree,
8
+ - generating the minimal subtree up to N,
9
+ - computing basic statistics,
10
+ - rendering graphs to Graphviz PNG images.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Optional, Tuple
16
+ from pathlib import Path
17
+
18
+ import pandas as pd
19
+
20
+ from src.collatz.inverse_tree import generate_collatz_tree
21
+ from src.collatz.minimal_subtree import generate_minimal_collatz_subtree_edges
22
+ from src.visual.render import render_collatz_tree_graphviz
23
+ from src.collatz.metrics import compute_basic_graph_stats
24
+
25
+
26
+ def build_and_render_collatz_tree(
27
+ backbone_length: int,
28
+ branch_length: int,
29
+ max_depth: int,
30
+ *,
31
+ filename: str = "collatz_tree",
32
+ output_dir: Optional[str] = "outputs",
33
+ return_edges: bool = False,
34
+ ) -> Tuple[str, Optional[pd.DataFrame]]:
35
+ """
36
+ Generate an inverse Collatz tree, render it as a PNG image, and optionally
37
+ return the underlying edge DataFrame.
38
+ """
39
+
40
+ output_path = Path(output_dir)
41
+ output_path.mkdir(parents=True, exist_ok=True)
42
+
43
+ df_edges: pd.DataFrame = generate_collatz_tree(
44
+ backbone_length=backbone_length,
45
+ branch_length=branch_length,
46
+ max_depth=max_depth,
47
+ )
48
+
49
+ image_path = render_collatz_tree_graphviz(
50
+ df_edges=df_edges,
51
+ filename=filename,
52
+ directory=output_path,
53
+ image_format="png",
54
+ )
55
+
56
+ if return_edges:
57
+ return image_path, df_edges
58
+
59
+ return image_path, None
60
+
61
+
62
+ def build_and_render_minimal_subtree(
63
+ N: int,
64
+ *,
65
+ filename: str = "collatz_minimal_subtree",
66
+ output_dir: Optional[str] = "outputs",
67
+ return_edges: bool = False,
68
+ ) -> Tuple[str, Optional[pd.DataFrame]]:
69
+ """
70
+ Generate the minimal Collatz subtree containing all integers 1..N,
71
+ render it as a PNG image, and optionally return the underlying
72
+ edge DataFrame.
73
+
74
+ This uses the structural branch construction (Algorithms 5 and 6)
75
+ implemented in `generate_minimal_collatz_subtree_edges`.
76
+ """
77
+
78
+ output_path = Path(output_dir)
79
+ output_path.mkdir(parents=True, exist_ok=True)
80
+
81
+ df_edges: pd.DataFrame = generate_minimal_collatz_subtree_edges(N)
82
+
83
+ image_path = render_collatz_tree_graphviz(
84
+ df_edges=df_edges,
85
+ filename=filename,
86
+ directory=output_path,
87
+ image_format="png",
88
+ )
89
+
90
+ if return_edges:
91
+ return image_path, df_edges
92
+
93
+ return image_path, None
94
+
95
+
96
+ def compute_collatz_tree_stats(
97
+ backbone_length: int,
98
+ branch_length: int,
99
+ max_depth: int,
100
+ ) -> dict:
101
+ """
102
+ Convenience helper: generate the inverse Collatz tree and return
103
+ basic graph stats (number of nodes, edges, parity counts, etc.).
104
+ """
105
+ df_edges = generate_collatz_tree(
106
+ backbone_length=backbone_length,
107
+ branch_length=branch_length,
108
+ max_depth=max_depth,
109
+ )
110
+ return compute_basic_graph_stats(df_edges)
111
+
112
+
113
+ def safe_int(x, default: int = 1) -> int:
114
+ """
115
+ Convert a value to int safely.
116
+ This protects the Gradio app from crashing when users type invalid input.
117
+ """
118
+ try:
119
+ return int(x)
120
+ except Exception:
121
+ return default
src/visual/.DS_Store ADDED
Binary file (6.15 kB). View file
 
src/visual/__init__.py ADDED
File without changes
src/visual/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (192 Bytes). View file
 
src/visual/__pycache__/render.cpython-312.pyc ADDED
Binary file (6.62 kB). View file
 
src/visual/render.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Graph rendering utilities for Collatz trees using Graphviz.
3
+
4
+ Main entry point:
5
+
6
+ render_collatz_tree_graphviz(df_edges, ...)
7
+
8
+ It takes a DataFrame of edges with columns ["Source", "Target"]
9
+ and renders a PNG image using Graphviz. The function returns the
10
+ absolute path to the generated image file, which is convenient
11
+ for Gradio's image components.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from pathlib import Path
17
+ from typing import Optional, Union
18
+
19
+ import pandas as pd
20
+ from graphviz import Digraph
21
+
22
+
23
+ PathLike = Union[str, Path]
24
+
25
+
26
+ def render_collatz_tree_graphviz(
27
+ df_edges: pd.DataFrame,
28
+ filename: str = "collatz_tree",
29
+ directory: Optional[PathLike] = None,
30
+ *,
31
+ font_size: int = 12,
32
+ node_size: float = 0.05,
33
+ arrow_size: float = 0.5,
34
+ nodesep: float = 0.2,
35
+ ranksep: float = 0.15,
36
+ dpi: int = 100,
37
+ image_format: str = "png",
38
+ ) -> str:
39
+ """
40
+ Render a Collatz inverse tree from a DataFrame of directed edges using Graphviz,
41
+ and save it as a single image file.
42
+
43
+ Parameters
44
+ ----------
45
+ df_edges : pd.DataFrame
46
+ DataFrame with at least two columns: "Source" and "Target".
47
+ Each row defines a directed edge: Source -> Target.
48
+ filename : str, default="collatz_tree"
49
+ Base file name (without extension) for the generated image.
50
+ directory : str or Path, optional
51
+ Directory where the image will be written. If None, uses the current
52
+ working directory.
53
+ font_size : int, default=11
54
+ Fixed font size used for node labels. This stays constant.
55
+ node_size : float, default=0.40
56
+ Minimum diameter of node circles in inches. Nodes will grow
57
+ automatically if labels are larger.
58
+ arrow_size : float, default=0.5
59
+ Arrowhead size.
60
+ nodesep : float, default=0.2
61
+ Minimum space between nodes on the same rank/level.
62
+ ranksep : float, default=0.15
63
+ Minimum space between different ranks/levels in the tree.
64
+ dpi : int, default=200
65
+ Render resolution in dots-per-inch.
66
+ image_format : str, default="png"
67
+ Output image format supported by Graphviz (e.g. "png", "svg").
68
+
69
+ Returns
70
+ -------
71
+ str
72
+ Absolute filesystem path to the generated image file.
73
+
74
+ Notes
75
+ -----
76
+ - Layout direction is Bottom-to-Top ("BT"), so the root (1) appears near
77
+ the bottom and branches extend upwards.
78
+ - Font size is fixed; node circles automatically expand when labels
79
+ require more space, because fixedsize="false".
80
+ """
81
+
82
+ required_columns = {"Source", "Target"}
83
+ if not required_columns.issubset(df_edges.columns):
84
+ missing = required_columns.difference(df_edges.columns)
85
+ raise ValueError(
86
+ f"df_edges must contain columns {required_columns}, missing: {missing}"
87
+ )
88
+
89
+ # Output directory
90
+ if directory is None:
91
+ directory_path = Path(".").resolve()
92
+ else:
93
+ directory_path = Path(directory).resolve()
94
+
95
+ directory_path.mkdir(parents=True, exist_ok=True)
96
+
97
+ dot = Digraph(
98
+ comment="Collatz Inverse Tree",
99
+ format=image_format,
100
+ )
101
+
102
+ # Collect labels (just to ensure clean string formatting)
103
+ raw_nodes = set(df_edges["Source"]).union(set(df_edges["Target"]))
104
+ labels = []
105
+ for node in raw_nodes:
106
+ if isinstance(node, (int, float)) and int(node) == node:
107
+ label = str(int(node))
108
+ else:
109
+ label = str(node)
110
+ labels.append(label)
111
+
112
+ # Global graph layout: Bottom-to-Top tree
113
+ dot.attr(
114
+ rankdir="BT",
115
+ dpi=str(dpi),
116
+ nodesep=str(nodesep),
117
+ ranksep=str(ranksep),
118
+ )
119
+
120
+ # Node style:
121
+ # - fontsize is fixed (for consistent readability),
122
+ # - width/height is the *minimum* size,
123
+ # - fixedsize="false" lets Graphviz enlarge nodes as needed to fit labels.
124
+ dot.attr(
125
+ "node",
126
+ shape="circle",
127
+ fontsize=str(font_size),
128
+ width=str(node_size),
129
+ height=str(node_size),
130
+ fixedsize="false",
131
+ )
132
+
133
+ # Edge style
134
+ dot.attr("edge", arrowsize=str(arrow_size))
135
+ dot.attr(splines="true")
136
+
137
+ # Add nodes with parity-based coloring and special styling for 1, 2, 4
138
+ for label in labels:
139
+ attrs = {}
140
+
141
+ # Try to interpret the label as an integer to check parity
142
+ try:
143
+ n = int(label)
144
+ is_int = True
145
+ except Exception:
146
+ n = None
147
+ is_int = False
148
+
149
+ if is_int:
150
+ # Base colors by parity
151
+ if n % 2 == 0:
152
+ # Even nodes: light blue
153
+ attrs.update(style="filled", fillcolor="#ddeeff")
154
+ else:
155
+ # Odd nodes: light orange
156
+ attrs.update(style="filled", fillcolor="#ffe5cc")
157
+
158
+ # Special highlight for the trivial cycle 1 → 2 → 4 → 1
159
+ if n == 1:
160
+ attrs.update(
161
+ style="filled,bold",
162
+ fillcolor="#fff2cc", # brighter yellow
163
+ penwidth="2",
164
+ )
165
+ elif n in (2, 4):
166
+ attrs.update(
167
+ style="filled",
168
+ fillcolor="#d0e3ff", # slightly stronger blue
169
+ )
170
+ else:
171
+ # Non-integer labels, if any, keep default styling
172
+ pass
173
+
174
+ dot.node(label, **attrs)
175
+
176
+ # Add edges
177
+ for source, target in df_edges[["Source", "Target"]].itertuples(index=False):
178
+ # Normalize labels to nice integer strings when possible
179
+ if isinstance(source, (int, float)) and int(source) == source:
180
+ src_label = str(int(source))
181
+ else:
182
+ src_label = str(source)
183
+
184
+ if isinstance(target, (int, float)) and int(target) == target:
185
+ tgt_label = str(int(target))
186
+ else:
187
+ tgt_label = str(target)
188
+
189
+ # Special case: cycle-closing edge 1 -> 4
190
+ if src_label == "1" and tgt_label == "4":
191
+ dot.edge(
192
+ src_label,
193
+ tgt_label,
194
+ constraint="false", # do not affect layout / ranks
195
+ color="gray40",
196
+ penwidth="1.6",
197
+ style="curved", # request a curved spline
198
+ arrowsize="0.8",
199
+ )
200
+ else:
201
+ dot.edge(src_label, tgt_label)
202
+
203
+ # Render to file. Graphviz's render() returns the path including extension.
204
+ output_path = dot.render(
205
+ filename=filename,
206
+ directory=str(directory_path),
207
+ cleanup=True,
208
+ )
209
+
210
+ return str(Path(output_path).resolve())