<div class="controls">
<label>
Ratio (clauses / variables): <strong id="ratio-label">4.27</strong>
<input type="range" id="ratio-slider" min="1" max="8" step="0.05" value="4.27">
</label>
<label>
Variables (n): <strong id="n-label">20</strong>
<input type="range" id="n-slider" min="8" max="30" step="1" value="20">
</label>
<button id="run-btn" type="button">Sample & Solve 20 instances</button>
</div>
<canvas id="chart" width="560" height="200"></canvas>
<div id="stats" class="stats"></div>
<div class="legend">
<span class="dot sat"></span> SAT (satisfiable)
<span class="dot unsat"></span> UNSAT (unsatisfiable)
<span class="bar hard"></span> Backtracks (hardness)
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.controls { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .8rem; }
label { font-size: .88rem; display: flex; flex-direction: column; gap: .2rem; }
input[type=range] { width: 100%; accent-color: #1d3557; }
button { font: 600 14px system-ui; padding: .45rem 1rem; background: #1d3557;
color: #fff; border: none; border-radius: 8px; cursor: pointer;
margin-top: .3rem; align-self: flex-start; }
canvas { width: 100%; max-width: 560px; border: 1px solid #cdd9e3;
border-radius: 8px; background: #f8fafc; display: block; }
.stats { font-size: .85rem; color: #444; margin: .5rem 0; min-height: 2.8em; }
.legend { font-size: .8rem; color: #555; display: flex; gap: 1.2rem; flex-wrap: wrap; align-items: center; }
.dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%;
vertical-align: middle; margin-right: 3px; }
.dot.sat { background: #2a9d8f; }
.dot.unsat { background: #e63946; }
.bar { display: inline-block; width: 12px; height: 12px; background: #e9c46a;
border: 1px solid #c9a820; vertical-align: middle; margin-right: 3px; border-radius: 2px; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// 3-SAT phase-transition demo.
// Generates random 3-SAT instances at a chosen clause/variable ratio,
// solves them with DPLL (unit propagation + backtracking), and counts
// backtracks as a proxy for hardness. The phase transition near ratio 4.27
// is a classical and empirically confirmed result in combinatorics.
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = { t: 20, r: 20, b: 34, l: 46 };
// --- Random 3-SAT generator ---
// Returns array of clauses; each clause = array of 3 literals.
// A literal is a signed variable index: +v means x_v is true, -v means x_v is false.
function randomSAT(n, m) {
const clauses = [];
for (let i = 0; i < m; i++) {
const vars = [];
while (vars.length < 3) {
const v = Math.floor(Math.random() * n) + 1;
if (!vars.includes(v)) vars.push(v);
}
clauses.push(vars.map(v => Math.random() < 0.5 ? v : -v));
}
return clauses;
}
// --- DPLL solver with correct unit propagation ---
// assign[v] = 0 (unset), 1 (true), -1 (false) for v = 1..n
function dpll(clauses, n) {
let backtracks = 0;
const assign = new Int8Array(n + 1); // 0-indexed slot 0 unused
// Unit propagation: repeatedly find unit clauses and force their literal.
// Returns false on conflict (empty clause), true when no more propagation possible.
function unitProp() {
let changed = true;
while (changed) {
changed = false;
for (const c of clauses) {
let unsetLit = null, unsetCount = 0, satisfied = false;
for (const lit of c) {
const v = Math.abs(lit), sign = lit > 0 ? 1 : -1;
if (assign[v] === sign) { satisfied = true; break; } // clause satisfied
if (assign[v] === 0) { unsetCount++; unsetLit = lit; }
}
if (satisfied) continue;
if (unsetCount === 0) return false; // all literals false: conflict
if (unsetCount === 1) { // unit clause: force assignment
assign[Math.abs(unsetLit)] = unsetLit > 0 ? 1 : -1;
changed = true;
}
}
}
return true;
}
function solve(depth) {
const snapshot = assign.slice(); // save state before propagation
if (!unitProp()) { assign.set(snapshot); return false; }
// Find first unset variable to branch on
let choice = -1;
for (let v = 1; v <= n; v++) if (assign[v] === 0) { choice = v; break; }
if (choice === -1) return true; // all variables assigned, no conflict
// Branch: try true first
assign[choice] = 1;
if (solve(depth + 1)) return true;
backtracks++;
assign.set(snapshot); // restore and try false
assign[choice] = -1;
if (solve(depth + 1)) return true;
backtracks++;
assign.set(snapshot);
return false; // both branches failed
}
const sat = solve(0);
return { sat, backtracks };
}
let results = [];
function runExperiment() {
const ratio = parseFloat(document.getElementById('ratio-slider').value);
const n = parseInt(document.getElementById('n-slider').value);
const m = Math.round(ratio * n);
const RUNS = 20;
results = [];
let totalBT = 0, satCount = 0;
for (let i = 0; i < RUNS; i++) {
const clauses = randomSAT(n, m);
const { sat, backtracks } = dpll(clauses, n);
results.push({ sat, backtracks });
totalBT += backtracks;
if (sat) satCount++;
}
drawChart();
const avgBT = (totalBT / RUNS).toFixed(1);
const zone = ratio < 3.0 ? '(easy — almost all SAT)' :
ratio > 6.0 ? '(easy — almost all UNSAT)' :
'⚡ near phase transition — hardness spike';
document.getElementById('stats').innerHTML =
fmt('<b>n=%n%, m=%m%, ratio=%ratio%</b> | SAT: %satCount%/%RUNS% | Avg backtracks: <b>%avgBT%</b> %zone%', { n: n, m: m, ratio: ratio.toFixed(2), satCount: satCount, RUNS: RUNS, avgBT: avgBT, zone: zone });
console.log('Ratio:', ratio.toFixed(2), '| n:', n, '| SAT:', satCount + '/' + RUNS,
'| AvgBacktracks:', avgBT);
}
function drawChart() {
ctx.clearRect(0, 0, W, H);
if (!results.length) return;
const maxBT = Math.max(...results.map(r => r.backtracks), 1);
const RUNS = results.length;
const barW = (W - PAD.l - PAD.r) / RUNS;
const plotH = H - PAD.t - PAD.b;
// Draw axes
ctx.strokeStyle = '#adb1b8'; ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.l, PAD.t); ctx.lineTo(PAD.l, H - PAD.b);
ctx.lineTo(W - PAD.r, H - PAD.b);
ctx.stroke();
// Axis labels
ctx.fillStyle = '#666'; ctx.font = '11px system-ui';
ctx.textAlign = 'right';
ctx.fillText(maxBT, PAD.l - 4, PAD.t + 8);
ctx.fillText(0, PAD.l - 4, H - PAD.b + 4);
ctx.textAlign = 'center';
ctx.fillText('Instance #', PAD.l + (W - PAD.l - PAD.r) / 2, H - 4);
ctx.save(); ctx.translate(13, PAD.t + plotH / 2); ctx.rotate(-Math.PI / 2);
ctx.fillText('Backtracks', 0, 0); ctx.restore();
results.forEach((r, i) => {
const x = PAD.l + i * barW;
const bh = maxBT > 0 ? (r.backtracks / maxBT) * plotH : 0;
// Backtrack bar
ctx.fillStyle = '#e9c46a';
ctx.fillRect(x + 2, H - PAD.b - bh, barW - 4, bh);
// SAT/UNSAT dot below axis
ctx.fillStyle = r.sat ? '#2a9d8f' : '#e63946';
ctx.beginPath();
ctx.arc(x + barW / 2, H - PAD.b + 16, 5, 0, 2 * Math.PI);
ctx.fill();
});
}
// Wire controls
document.getElementById('ratio-slider').addEventListener('input', e => {
document.getElementById('ratio-label').textContent =
parseFloat(e.target.value).toFixed(2);
});
document.getElementById('n-slider').addEventListener('input', e => {
document.getElementById('n-label').textContent = e.target.value;
});
document.getElementById('run-btn').addEventListener('click', runExperiment);
// Initial run
runExperiment();
<div class="controls">
<label>
Ratio (clauses / variables): <strong id="ratio-label">4.27</strong>
<input type="range" id="ratio-slider" min="1" max="8" step="0.05" value="4.27">
</label>
<label>
Variables (n): <strong id="n-label">20</strong>
<input type="range" id="n-slider" min="8" max="30" step="1" value="20">
</label>
<button id="run-btn" type="button">Sample & Solve 20 instances</button>
</div>
<canvas id="chart" width="560" height="200"></canvas>
<div id="stats" class="stats"></div>
<div class="legend">
<span class="dot sat"></span> SAT (satisfiable)
<span class="dot unsat"></span> UNSAT (unsatisfiable)
<span class="bar hard"></span> Backtracks (hardness)
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.controls { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .8rem; }
label { font-size: .88rem; display: flex; flex-direction: column; gap: .2rem; }
input[type=range] { width: 100%; accent-color: #1d3557; }
button { font: 600 14px system-ui; padding: .45rem 1rem; background: #1d3557;
color: #fff; border: none; border-radius: 8px; cursor: pointer;
margin-top: .3rem; align-self: flex-start; }
canvas { width: 100%; max-width: 560px; border: 1px solid #cdd9e3;
border-radius: 8px; background: #f8fafc; display: block; }
.stats { font-size: .85rem; color: #444; margin: .5rem 0; min-height: 2.8em; }
.legend { font-size: .8rem; color: #555; display: flex; gap: 1.2rem; flex-wrap: wrap; align-items: center; }
.dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%;
vertical-align: middle; margin-right: 3px; }
.dot.sat { background: #2a9d8f; }
.dot.unsat { background: #e63946; }
.bar { display: inline-block; width: 12px; height: 12px; background: #e9c46a;
border: 1px solid #c9a820; vertical-align: middle; margin-right: 3px; border-radius: 2px; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// 3-SAT phase-transition demo.
// Generates random 3-SAT instances at a chosen clause/variable ratio,
// solves them with DPLL (unit propagation + backtracking), and counts
// backtracks as a proxy for hardness. The phase transition near ratio 4.27
// is a classical and empirically confirmed result in combinatorics.
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = { t: 20, r: 20, b: 34, l: 46 };
// --- Random 3-SAT generator ---
// Returns array of clauses; each clause = array of 3 literals.
// A literal is a signed variable index: +v means x_v is true, -v means x_v is false.
function randomSAT(n, m) {
const clauses = [];
for (let i = 0; i < m; i++) {
const vars = [];
while (vars.length < 3) {
const v = Math.floor(Math.random() * n) + 1;
if (!vars.includes(v)) vars.push(v);
}
clauses.push(vars.map(v => Math.random() < 0.5 ? v : -v));
}
return clauses;
}
// --- DPLL solver with correct unit propagation ---
// assign[v] = 0 (unset), 1 (true), -1 (false) for v = 1..n
function dpll(clauses, n) {
let backtracks = 0;
const assign = new Int8Array(n + 1); // 0-indexed slot 0 unused
// Unit propagation: repeatedly find unit clauses and force their literal.
// Returns false on conflict (empty clause), true when no more propagation possible.
function unitProp() {
let changed = true;
while (changed) {
changed = false;
for (const c of clauses) {
let unsetLit = null, unsetCount = 0, satisfied = false;
for (const lit of c) {
const v = Math.abs(lit), sign = lit > 0 ? 1 : -1;
if (assign[v] === sign) { satisfied = true; break; } // clause satisfied
if (assign[v] === 0) { unsetCount++; unsetLit = lit; }
}
if (satisfied) continue;
if (unsetCount === 0) return false; // all literals false: conflict
if (unsetCount === 1) { // unit clause: force assignment
assign[Math.abs(unsetLit)] = unsetLit > 0 ? 1 : -1;
changed = true;
}
}
}
return true;
}
function solve(depth) {
const snapshot = assign.slice(); // save state before propagation
if (!unitProp()) { assign.set(snapshot); return false; }
// Find first unset variable to branch on
let choice = -1;
for (let v = 1; v <= n; v++) if (assign[v] === 0) { choice = v; break; }
if (choice === -1) return true; // all variables assigned, no conflict
// Branch: try true first
assign[choice] = 1;
if (solve(depth + 1)) return true;
backtracks++;
assign.set(snapshot); // restore and try false
assign[choice] = -1;
if (solve(depth + 1)) return true;
backtracks++;
assign.set(snapshot);
return false; // both branches failed
}
const sat = solve(0);
return { sat, backtracks };
}
let results = [];
function runExperiment() {
const ratio = parseFloat(document.getElementById('ratio-slider').value);
const n = parseInt(document.getElementById('n-slider').value);
const m = Math.round(ratio * n);
const RUNS = 20;
results = [];
let totalBT = 0, satCount = 0;
for (let i = 0; i < RUNS; i++) {
const clauses = randomSAT(n, m);
const { sat, backtracks } = dpll(clauses, n);
results.push({ sat, backtracks });
totalBT += backtracks;
if (sat) satCount++;
}
drawChart();
const avgBT = (totalBT / RUNS).toFixed(1);
const zone = ratio < 3.0 ? '(easy — almost all SAT)' :
ratio > 6.0 ? '(easy — almost all UNSAT)' :
'⚡ near phase transition — hardness spike';
document.getElementById('stats').innerHTML =
fmt('<b>n=%n%, m=%m%, ratio=%ratio%</b> | SAT: %satCount%/%RUNS% | Avg backtracks: <b>%avgBT%</b> %zone%', { n: n, m: m, ratio: ratio.toFixed(2), satCount: satCount, RUNS: RUNS, avgBT: avgBT, zone: zone });
console.log('Ratio:', ratio.toFixed(2), '| n:', n, '| SAT:', satCount + '/' + RUNS,
'| AvgBacktracks:', avgBT);
}
function drawChart() {
ctx.clearRect(0, 0, W, H);
if (!results.length) return;
const maxBT = Math.max(...results.map(r => r.backtracks), 1);
const RUNS = results.length;
const barW = (W - PAD.l - PAD.r) / RUNS;
const plotH = H - PAD.t - PAD.b;
// Draw axes
ctx.strokeStyle = '#adb1b8'; ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.l, PAD.t); ctx.lineTo(PAD.l, H - PAD.b);
ctx.lineTo(W - PAD.r, H - PAD.b);
ctx.stroke();
// Axis labels
ctx.fillStyle = '#666'; ctx.font = '11px system-ui';
ctx.textAlign = 'right';
ctx.fillText(maxBT, PAD.l - 4, PAD.t + 8);
ctx.fillText(0, PAD.l - 4, H - PAD.b + 4);
ctx.textAlign = 'center';
ctx.fillText('Instance #', PAD.l + (W - PAD.l - PAD.r) / 2, H - 4);
ctx.save(); ctx.translate(13, PAD.t + plotH / 2); ctx.rotate(-Math.PI / 2);
ctx.fillText('Backtracks', 0, 0); ctx.restore();
results.forEach((r, i) => {
const x = PAD.l + i * barW;
const bh = maxBT > 0 ? (r.backtracks / maxBT) * plotH : 0;
// Backtrack bar
ctx.fillStyle = '#e9c46a';
ctx.fillRect(x + 2, H - PAD.b - bh, barW - 4, bh);
// SAT/UNSAT dot below axis
ctx.fillStyle = r.sat ? '#2a9d8f' : '#e63946';
ctx.beginPath();
ctx.arc(x + barW / 2, H - PAD.b + 16, 5, 0, 2 * Math.PI);
ctx.fill();
});
}
// Wire controls
document.getElementById('ratio-slider').addEventListener('input', e => {
document.getElementById('ratio-label').textContent =
parseFloat(e.target.value).toFixed(2);
});
document.getElementById('n-slider').addEventListener('input', e => {
document.getElementById('n-label').textContent = e.target.value;
});
document.getElementById('run-btn').addEventListener('click', runExperiment);
// Initial run
runExperiment();
Comments
Loading comments...