Skip to content

Commit 060a4ff

Browse files
committed
Correct mathematical claims and add verification
1 parent 5e53b9b commit 060a4ff

5 files changed

Lines changed: 458 additions & 184 deletions

File tree

‎.github/workflows/test.yml‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Verify mathematics
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
node-tests:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Check out repository
15+
uses: actions/checkout@v4
16+
17+
- name: Set up Node.js
18+
uses: actions/setup-node@v4
19+
with:
20+
node-version: 22
21+
22+
- name: Run mathematical verification
23+
run: node --test tests/collatz-math.test.js
24+
25+
- name: Check JavaScript syntax
26+
run: |
27+
node --check collatz-math.js
28+
node --check tests/collatz-math.test.js

‎Collatz reverse tree.html‎

Lines changed: 33 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<title>Collatz Reverse Tree – Interactive Visualizer with (m,k) toggle & PNG export</title>
77
<!-- vis-network core library -->
88
<script type="text/javascript" src="https://unpkg.com/vis-network@9.1.6/dist/vis-network.min.js"></script>
9+
<script type="text/javascript" src="collatz-math.js"></script>
910
<style>
1011
* {
1112
box-sizing: border-box;
@@ -192,14 +193,14 @@
192193
<div class="toolbar">
193194
<div class="control-group">
194195
<label>🔢 Max steps (depth):</label>
195-
<input type="number" id="stepInput" min="1" max="12" value="4" step="1" style="width: 100px;">
196+
<input type="number" id="stepInput" min="1" max="30" value="4" step="1" style="width: 100px;">
196197
<button id="generateBtn">⟳ Generate / Refresh Tree</button>
197198
</div>
198199
<button id="toggleDisplayBtn" class="toggle-btn">🔄 Toggle (m,k) Coordinates</button>
199-
<button id="exportPngBtn" class="export-btn">📸 Export HD PNG (Full View)</button>
200+
<button id="exportPngBtn" class="export-btn">📸 Export PNG (Fitted View)</button>
200201
<div class="info-panel" id="infoPanel">
201-
📘 <strong>Collatz Reverse Tree</strong> · growing backwards from 1<br>
202-
🟢 Click any node to show its path · Switch to (m,k) mode
202+
📘 <strong>Cycle-pruned Collatz Reverse Tree</strong> · growing backwards from 1<br>
203+
🟢 Click any node to show its forward trajectory to 1 · Switch to (m,k) mode
203204
</div>
204205
<div class="stats" id="statsArea">
205206
Nodes: — | Edges: —
@@ -222,7 +223,7 @@
222223
</div>
223224
</div>
224225
<div class="footer">
225-
💡 Reverse rules: predecessors are 2x and (x‑1)/3 (if odd >1) | <span class="warning">⚠️ Depth ≥8 may produce >1500 nodes, export may be slow | Toggle (m,k): for any n, m = floor(n/6), k = n mod 6</span>
226+
💡 Reverse rules: predecessors are 2x and (x−1)/3 when the latter is a positive odd integer. The edge 4 → 1 is omitted to break the 1–4–2 cycle. | <span class="warning">⚠️ Depth 27 produces 1,748 nodes in this implementation; deeper trees may render slowly. PNG export captures the fitted viewport. | (m,k): m = floor(n/6), k = n mod 6</span>
226227
</div>
227228
</div>
228229

@@ -232,42 +233,9 @@
232233
// Returns { nodes, edges, parentMap, stepsMap }
233234
// --------------------------------------------------------------
234235
function generateCollatzTree(maxSteps) {
235-
let stepsMap = new Map(); // number -> step count from root 1
236-
let parentMap = new Map(); // child -> parent
237-
238-
if (maxSteps < 0) return { nodes: [], edges: [], parentMap, stepsMap };
239-
240-
stepsMap.set(1, 0);
241-
parentMap.set(1, null);
242-
let queue = [1];
243-
244-
while (queue.length > 0) {
245-
let current = queue.shift();
246-
let currentStep = stepsMap.get(current);
247-
if (currentStep >= maxSteps) continue;
248-
249-
let pred1 = 2 * current;
250-
let pred2 = null;
251-
if ((current - 1) % 3 === 0) {
252-
let candidate = (current - 1) / 3;
253-
if (candidate % 2 === 1 && candidate !== 1) {
254-
pred2 = candidate;
255-
}
256-
}
257-
258-
[pred1, pred2].forEach(pred => {
259-
if (pred === null) return;
260-
if (!stepsMap.has(pred)) {
261-
stepsMap.set(pred, currentStep + 1);
262-
parentMap.set(pred, current);
263-
queue.push(pred);
264-
} else if (stepsMap.get(pred) > currentStep + 1) {
265-
stepsMap.set(pred, currentStep + 1);
266-
parentMap.set(pred, current);
267-
queue.push(pred);
268-
}
269-
});
270-
}
236+
const { stepsMap, parentMap } = CollatzMath.generateReverseTree(1, maxSteps, {
237+
pruneRootCycle: true
238+
});
271239

272240
let nodes = [];
273241
let edges = [];
@@ -282,7 +250,7 @@
282250
for (let [child, parent] of parentMap.entries()) {
283251
if (parent !== null && nodeSet.has(child) && nodeSet.has(parent)) {
284252
if (stepsMap.get(child) <= maxSteps && stepsMap.get(parent) <= maxSteps) {
285-
edges.push({ from: parent, to: child });
253+
edges.push({ id: `${parent}->${child}`, from: parent, to: child });
286254
}
287255
}
288256
}
@@ -312,8 +280,7 @@
312280

313281
// ---------- Helper: format number as (m,k) ----------
314282
function formatNumberToMK(n) {
315-
const m = Math.floor(n / 6);
316-
const k = n % 6;
283+
const { m, k } = CollatzMath.toMK(n);
317284
return `(${m},${k})`;
318285
}
319286

@@ -633,7 +600,7 @@
633600
}
634601

635602
// --------------------------------------------------------------
636-
// Show path from node to root 1 (always raw numbers)
603+
// Show the forward Collatz trajectory from the selected node to root 1
637604
// --------------------------------------------------------------
638605
function showNodePath(nodeId) {
639606
if (!currentParentMap || !currentStepsMap) {
@@ -652,30 +619,36 @@
652619
cur = currentParentMap.get(cur);
653620
if (cur === null) break;
654621
}
655-
path.reverse();
656622
const stepCount = currentStepsMap.get(nodeId) || (path.length - 1);
657623

658624
let pathStr = path.join(" → ");
659625
const isLong = pathStr.length > 260;
660626
if (isLong) {
661-
pathStr = pathStr.slice(0, 240) + "… → " + nodeId;
627+
pathStr = pathStr.slice(0, 240) + "… → 1";
662628
}
663629

664630
const infoDiv = document.getElementById('infoPanel');
665631
if (infoDiv) {
666-
infoDiv.innerHTML = `🔍 <strong>Path details</strong> · Number ${nodeId} to 1<br>
632+
infoDiv.innerHTML = `🔍 <strong>Forward Collatz trajectory</strong> · ${nodeId} to 1<br>
667633
📌 Steps: ${stepCount}<br>
668634
🧩 Trajectory: ${pathStr}<br>
669-
<span style="font-size:0.7rem;">✨ Forward Collatz: odd→3x+1, even→x/2</span>
670-
<br><span style="font-size:0.65rem; color:#8b5a2b;">💡 Tip: Toggle (m,k) display on the graph – path always shows raw numbers</span>`;
635+
<span style="font-size:0.7rem;">✨ Rule: odd → 3x+1, even → x/2</span>
636+
<br><span style="font-size:0.65rem; color:#8b5a2b;">💡 Graph arrows show reverse expansion; this trajectory follows them back toward the root.</span>`;
671637
if (network) {
672-
network.selectNodes([nodeId]);
673-
setTimeout(() => { if(network) network.selectNodes([]); }, 1600);
638+
const pathEdges = [];
639+
for (let index = 0; index < path.length - 1; index += 1) {
640+
pathEdges.push(`${path[index + 1]}->${path[index]}`);
641+
}
642+
network.setSelection(
643+
{ nodes: path, edges: pathEdges },
644+
{ unselectAll: true, highlightEdges: false }
645+
);
646+
setTimeout(() => { if (network) network.unselectAll(); }, 2400);
674647
}
675648
} else {
676-
alert(`Number ${nodeId} → 1 path (${stepCount} steps):\n${pathStr}`);
649+
alert(`Forward Collatz trajectory ${nodeId} → 1 (${stepCount} steps):\n${pathStr}`);
677650
}
678-
showStatusMessage(`Displayed path for node ${nodeId} to root`, false);
651+
showStatusMessage(`Displayed forward trajectory for node ${nodeId}`, false);
679652
}
680653

681654
// --------------------------------------------------------------
@@ -698,16 +671,12 @@
698671
showStatusMessage('Please enter a valid step count (≥1)', true);
699672
return;
700673
}
701-
if (maxSteps > 12) {
702-
if (!confirm(`Depth ${maxSteps} may create >5000 nodes and freeze the browser. Recommended ≤10. Continue anyway?`)) {
703-
stepInput.value = "6";
704-
renderTree(6);
705-
return;
706-
}
707-
} else if (maxSteps >= 8) {
708-
if (!confirm(`Depth ${maxSteps} will generate many nodes (>1500), rendering and export will be slower. Continue?`)) {
709-
stepInput.value = "6";
710-
renderTree(6);
674+
if (maxSteps > 30) {
675+
showStatusMessage('Please enter a depth from 1 to 30; larger depths are disabled to keep integer arithmetic and rendering manageable', true);
676+
return;
677+
}
678+
if (maxSteps >= 27) {
679+
if (!confirm(`Depth 27 produces 1,748 nodes, and deeper values grow quickly. Rendering and export may be slow. Continue with depth ${maxSteps}?`)) {
711680
return;
712681
}
713682
}

0 commit comments

Comments
 (0)