<p class="hint">Build a directed graph, choose a <b>source node</b>, then press <b>Run LFP</b> to watch the least-fixed-point iteration compute reachability step by step.</p>
<div id="controls">
<button id="btn-edge" type="button" class="active">Add edge</button>
<button id="btn-reset" type="button" class="ghost">Reset graph</button>
<label class="src-label">Source node: <input id="src-input" type="number" min="0" max="7" value="0"></label>
<button id="btn-run" type="button" class="run">Run LFP</button>
</div>
<div id="canvas-wrap">
<canvas id="gc" width="480" height="300"></canvas>
</div>
<div id="log-wrap">
<div id="log"></div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
#controls { display: flex; flex-wrap: wrap; gap: .45rem; align-items: center; margin-bottom: .6rem; }
.src-label { font-size: .88rem; color: #555; display: flex; align-items: center; gap: .3rem; }
#src-input { width: 48px; padding: .2rem .3rem; border: 1px solid #ccc; border-radius: 6px; font-size: .88rem; }
button { font: 600 13px system-ui, sans-serif; padding: .38rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; transition: background .12s; }
button.ghost { background: #fff; color: #1d3557; }
button.run { background: #0a7d33; border-color: #0a7d33; }
button.active { background: #e63946; border-color: #c92f3c; }
#canvas-wrap { border: 1px solid #dde4ea; border-radius: 10px; overflow: hidden; background: #f8fafb; margin-bottom: .6rem; }
canvas { display: block; cursor: crosshair; }
#log-wrap { min-height: 64px; max-height: 140px; overflow-y: auto; border: 1px solid #dde4ea; border-radius: 8px; padding: .45rem .6rem; background: #f0f4f8; }
#log { font: 13px ui-monospace, monospace; color: #1d3557; line-height: 1.65; white-space: pre-wrap; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// โโ Descriptive Complexity: LFP Reachability Demo โโโโโโโโโโโโโโโโโโโโโโโโโโ
// Nodes 0..7 are fixed in a circle. The user adds directed edges.
// "Run LFP" simulates the least-fixed-point iteration for graph reachability:
// R_0 = {source}
// R_{i+1} = R_i โช { v : โu โ R_i, edge(u,v) }
// Terminates when R_{i+1} = R_i (fixed point reached).
// This exactly matches the FO+LFP formula for reachability (ImmermanโVardi).
const N = 8; // number of nodes
const canvas = document.getElementById('gc');
const ctx = canvas.getContext('2d');
const log = document.getElementById('log');
const W = canvas.width, H = canvas.height;
const CX = W / 2, CY = H / 2, R = 105; // layout circle
// Preloaded example edges so it works out of the box
const edges = new Set(['0,1','1,2','2,4','3,5','5,3','4,6','6,7']);
let pendingFrom = null; // first click for edge-add mode
let lfpSets = []; // snapshots for animation
let animFrame = null;
// โโ Geometry โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function nodePos(i) {
const angle = (2 * Math.PI * i / N) - Math.PI / 2;
return { x: CX + R * Math.cos(angle), y: CY + R * Math.sin(angle) };
}
function hitNode(mx, my) {
for (let i = 0; i < N; i++) {
const p = nodePos(i);
if ((mx - p.x) ** 2 + (my - p.y) ** 2 < 16 * 16) return i;
}
return -1;
}
// โโ Drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function drawArrow(x1, y1, x2, y2, color) {
const nodeR = 14;
const dx = x2 - x1, dy = y2 - y1;
const len = Math.sqrt(dx*dx + dy*dy) || 1;
const ux = dx/len, uy = dy/len;
// start/end offset by node radius
const sx = x1 + ux * nodeR, sy = y1 + uy * nodeR;
const ex = x2 - ux * nodeR, ey = y2 - uy * nodeR;
const hLen = 10, hWid = 5;
const bx = ex - ux * hLen, by = ey - uy * hLen;
const px = -uy * hWid, py = ux * hWid;
ctx.beginPath();
ctx.moveTo(sx, sy); ctx.lineTo(ex, ey);
ctx.strokeStyle = color; ctx.lineWidth = 1.8;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(ex, ey);
ctx.lineTo(bx + px, by + py);
ctx.lineTo(bx - px, by - py);
ctx.closePath();
ctx.fillStyle = color; ctx.fill();
}
function draw(reachSet) {
ctx.clearRect(0, 0, W, H);
// edges
edges.forEach(e => {
const [a, b] = e.split(',').map(Number);
const pa = nodePos(a), pb = nodePos(b);
drawArrow(pa.x, pa.y, pb.x, pb.y, '#8fa8bf');
});
// nodes
for (let i = 0; i < N; i++) {
const p = nodePos(i);
const inR = reachSet && reachSet.has(i);
ctx.beginPath();
ctx.arc(p.x, p.y, 14, 0, 2 * Math.PI);
ctx.fillStyle = inR ? '#0a7d33' : (pendingFrom === i ? '#e63946' : '#e8eef3');
ctx.fill();
ctx.strokeStyle = inR ? '#074f21' : '#8fa8bf';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = inR ? '#fff' : '#1d3557';
ctx.font = 'bold 13px system-ui, sans-serif';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(i, p.x, p.y);
}
}
// โโ LFP iteration โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function runLFP() {
if (animFrame) cancelAnimationFrame(animFrame);
const src = parseInt(document.getElementById('src-input').value, 10);
if (src < 0 || src >= N || isNaN(src)) { logLine('Invalid source node.'); return; }
log.textContent = '';
logLine(fmt('โโ Starting LFP for reachability from node %src% โโ', { src: src }));
logLine(fmt('Formula: R = lfp ฮปR. {%src%} โช {v : โuโR, edge(u,v)}', { src: src }));
logLine('');
lfpSets = [];
let current = new Set([src]);
lfpSets.push(new Set(current));
for (let round = 0; round < N + 1; round++) {
const next = new Set(current);
current.forEach(u => {
edges.forEach(e => {
const [a, b] = e.split(',').map(Number);
if (a === u) next.add(b);
});
});
const changed = next.size !== current.size || ![...next].every(x => current.has(x));
if (!changed) {
lfpSets.push(null); // sentinel: fixed point reached
break;
}
current = next;
lfpSets.push(new Set(current));
}
let step = 0;
function animate() {
if (step >= lfpSets.length) return;
const snap = lfpSets[step];
if (snap === null) {
logLine('Fixed point reached โ no new nodes added.');
logLine(fmt('Reachable from %src%: R = { %sort% }', { src: src, sort: [...lfpSets[step-1]].sort((a,b)=>a-b).join(', ') }));
draw(lfpSets[step - 1]);
} else {
if (step === 0) {
logLine(fmt('Rโ = { %src% } (seed: just the source)', { src: src }));
} else {
const prev = lfpSets[step - 1];
const added = [...snap].filter(x => !prev.has(x));
logLine('R' + step + ' = { ' + [...snap].sort((a,b)=>a-b).join(', ') + ' }' + (added.length ? ' (+' + added.join(',') + ')' : ''));
}
draw(snap);
step++;
animFrame = setTimeout(animate, 700);
}
}
animate();
}
function logLine(s) { log.textContent += s + '\n'; log.scrollTop = log.scrollHeight; }
// โโ Edge-add interaction โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
canvas.addEventListener('click', e => {
const rect = canvas.getBoundingClientRect();
const mx = (e.clientX - rect.left) * (W / rect.width);
const my = (e.clientY - rect.top) * (H / rect.height);
const hit = hitNode(mx, my);
if (hit < 0) { pendingFrom = null; draw(null); return; }
if (pendingFrom === null) {
pendingFrom = hit;
draw(null);
logLine(fmt('Selected node %hit% as edge source. Now click target node.', { hit: hit }));
} else {
if (pendingFrom !== hit) {
const key = pendingFrom + ',' + hit;
if (!edges.has(key)) { edges.add(key); logLine(fmt('Added edge %pendingFrom% โ %hit%', { pendingFrom: pendingFrom, hit: hit })); }
else logLine(fmt('Edge %pendingFrom% โ %hit% already exists.', { pendingFrom: pendingFrom, hit: hit }));
}
pendingFrom = null;
draw(null);
}
});
document.getElementById('btn-run').addEventListener('click', runLFP);
document.getElementById('btn-edge').addEventListener('click', () => {
pendingFrom = null;
logLine('Click a source node, then a target node to add a directed edge.');
draw(null);
});
document.getElementById('btn-reset').addEventListener('click', () => {
edges.clear(); pendingFrom = null;
if (animFrame) clearTimeout(animFrame);
lfpSets = [];
log.textContent = 'Graph cleared. Add edges, then Run LFP.\n';
draw(null);
});
// โโ Initial render โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
logLine('Graph loaded with example edges (0โ1โ2โ4โ6โ7, 3โ5).');
logLine('Press "Run LFP" to compute reachability from node 0, or edit the graph first.');
draw(null);
<p class="hint">Build a directed graph, choose a <b>source node</b>, then press <b>Run LFP</b> to watch the least-fixed-point iteration compute reachability step by step.</p>
<div id="controls">
<button id="btn-edge" type="button" class="active">Add edge</button>
<button id="btn-reset" type="button" class="ghost">Reset graph</button>
<label class="src-label">Source node: <input id="src-input" type="number" min="0" max="7" value="0"></label>
<button id="btn-run" type="button" class="run">Run LFP</button>
</div>
<div id="canvas-wrap">
<canvas id="gc" width="480" height="300"></canvas>
</div>
<div id="log-wrap">
<div id="log"></div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
#controls { display: flex; flex-wrap: wrap; gap: .45rem; align-items: center; margin-bottom: .6rem; }
.src-label { font-size: .88rem; color: #555; display: flex; align-items: center; gap: .3rem; }
#src-input { width: 48px; padding: .2rem .3rem; border: 1px solid #ccc; border-radius: 6px; font-size: .88rem; }
button { font: 600 13px system-ui, sans-serif; padding: .38rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; transition: background .12s; }
button.ghost { background: #fff; color: #1d3557; }
button.run { background: #0a7d33; border-color: #0a7d33; }
button.active { background: #e63946; border-color: #c92f3c; }
#canvas-wrap { border: 1px solid #dde4ea; border-radius: 10px; overflow: hidden; background: #f8fafb; margin-bottom: .6rem; }
canvas { display: block; cursor: crosshair; }
#log-wrap { min-height: 64px; max-height: 140px; overflow-y: auto; border: 1px solid #dde4ea; border-radius: 8px; padding: .45rem .6rem; background: #f0f4f8; }
#log { font: 13px ui-monospace, monospace; color: #1d3557; line-height: 1.65; white-space: pre-wrap; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// โโ Descriptive Complexity: LFP Reachability Demo โโโโโโโโโโโโโโโโโโโโโโโโโโ
// Nodes 0..7 are fixed in a circle. The user adds directed edges.
// "Run LFP" simulates the least-fixed-point iteration for graph reachability:
// R_0 = {source}
// R_{i+1} = R_i โช { v : โu โ R_i, edge(u,v) }
// Terminates when R_{i+1} = R_i (fixed point reached).
// This exactly matches the FO+LFP formula for reachability (ImmermanโVardi).
const N = 8; // number of nodes
const canvas = document.getElementById('gc');
const ctx = canvas.getContext('2d');
const log = document.getElementById('log');
const W = canvas.width, H = canvas.height;
const CX = W / 2, CY = H / 2, R = 105; // layout circle
// Preloaded example edges so it works out of the box
const edges = new Set(['0,1','1,2','2,4','3,5','5,3','4,6','6,7']);
let pendingFrom = null; // first click for edge-add mode
let lfpSets = []; // snapshots for animation
let animFrame = null;
// โโ Geometry โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function nodePos(i) {
const angle = (2 * Math.PI * i / N) - Math.PI / 2;
return { x: CX + R * Math.cos(angle), y: CY + R * Math.sin(angle) };
}
function hitNode(mx, my) {
for (let i = 0; i < N; i++) {
const p = nodePos(i);
if ((mx - p.x) ** 2 + (my - p.y) ** 2 < 16 * 16) return i;
}
return -1;
}
// โโ Drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function drawArrow(x1, y1, x2, y2, color) {
const nodeR = 14;
const dx = x2 - x1, dy = y2 - y1;
const len = Math.sqrt(dx*dx + dy*dy) || 1;
const ux = dx/len, uy = dy/len;
// start/end offset by node radius
const sx = x1 + ux * nodeR, sy = y1 + uy * nodeR;
const ex = x2 - ux * nodeR, ey = y2 - uy * nodeR;
const hLen = 10, hWid = 5;
const bx = ex - ux * hLen, by = ey - uy * hLen;
const px = -uy * hWid, py = ux * hWid;
ctx.beginPath();
ctx.moveTo(sx, sy); ctx.lineTo(ex, ey);
ctx.strokeStyle = color; ctx.lineWidth = 1.8;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(ex, ey);
ctx.lineTo(bx + px, by + py);
ctx.lineTo(bx - px, by - py);
ctx.closePath();
ctx.fillStyle = color; ctx.fill();
}
function draw(reachSet) {
ctx.clearRect(0, 0, W, H);
// edges
edges.forEach(e => {
const [a, b] = e.split(',').map(Number);
const pa = nodePos(a), pb = nodePos(b);
drawArrow(pa.x, pa.y, pb.x, pb.y, '#8fa8bf');
});
// nodes
for (let i = 0; i < N; i++) {
const p = nodePos(i);
const inR = reachSet && reachSet.has(i);
ctx.beginPath();
ctx.arc(p.x, p.y, 14, 0, 2 * Math.PI);
ctx.fillStyle = inR ? '#0a7d33' : (pendingFrom === i ? '#e63946' : '#e8eef3');
ctx.fill();
ctx.strokeStyle = inR ? '#074f21' : '#8fa8bf';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = inR ? '#fff' : '#1d3557';
ctx.font = 'bold 13px system-ui, sans-serif';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(i, p.x, p.y);
}
}
// โโ LFP iteration โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function runLFP() {
if (animFrame) cancelAnimationFrame(animFrame);
const src = parseInt(document.getElementById('src-input').value, 10);
if (src < 0 || src >= N || isNaN(src)) { logLine('Invalid source node.'); return; }
log.textContent = '';
logLine(fmt('โโ Starting LFP for reachability from node %src% โโ', { src: src }));
logLine(fmt('Formula: R = lfp ฮปR. {%src%} โช {v : โuโR, edge(u,v)}', { src: src }));
logLine('');
lfpSets = [];
let current = new Set([src]);
lfpSets.push(new Set(current));
for (let round = 0; round < N + 1; round++) {
const next = new Set(current);
current.forEach(u => {
edges.forEach(e => {
const [a, b] = e.split(',').map(Number);
if (a === u) next.add(b);
});
});
const changed = next.size !== current.size || ![...next].every(x => current.has(x));
if (!changed) {
lfpSets.push(null); // sentinel: fixed point reached
break;
}
current = next;
lfpSets.push(new Set(current));
}
let step = 0;
function animate() {
if (step >= lfpSets.length) return;
const snap = lfpSets[step];
if (snap === null) {
logLine('Fixed point reached โ no new nodes added.');
logLine(fmt('Reachable from %src%: R = { %sort% }', { src: src, sort: [...lfpSets[step-1]].sort((a,b)=>a-b).join(', ') }));
draw(lfpSets[step - 1]);
} else {
if (step === 0) {
logLine(fmt('Rโ = { %src% } (seed: just the source)', { src: src }));
} else {
const prev = lfpSets[step - 1];
const added = [...snap].filter(x => !prev.has(x));
logLine('R' + step + ' = { ' + [...snap].sort((a,b)=>a-b).join(', ') + ' }' + (added.length ? ' (+' + added.join(',') + ')' : ''));
}
draw(snap);
step++;
animFrame = setTimeout(animate, 700);
}
}
animate();
}
function logLine(s) { log.textContent += s + '\n'; log.scrollTop = log.scrollHeight; }
// โโ Edge-add interaction โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
canvas.addEventListener('click', e => {
const rect = canvas.getBoundingClientRect();
const mx = (e.clientX - rect.left) * (W / rect.width);
const my = (e.clientY - rect.top) * (H / rect.height);
const hit = hitNode(mx, my);
if (hit < 0) { pendingFrom = null; draw(null); return; }
if (pendingFrom === null) {
pendingFrom = hit;
draw(null);
logLine(fmt('Selected node %hit% as edge source. Now click target node.', { hit: hit }));
} else {
if (pendingFrom !== hit) {
const key = pendingFrom + ',' + hit;
if (!edges.has(key)) { edges.add(key); logLine(fmt('Added edge %pendingFrom% โ %hit%', { pendingFrom: pendingFrom, hit: hit })); }
else logLine(fmt('Edge %pendingFrom% โ %hit% already exists.', { pendingFrom: pendingFrom, hit: hit }));
}
pendingFrom = null;
draw(null);
}
});
document.getElementById('btn-run').addEventListener('click', runLFP);
document.getElementById('btn-edge').addEventListener('click', () => {
pendingFrom = null;
logLine('Click a source node, then a target node to add a directed edge.');
draw(null);
});
document.getElementById('btn-reset').addEventListener('click', () => {
edges.clear(); pendingFrom = null;
if (animFrame) clearTimeout(animFrame);
lfpSets = [];
log.textContent = 'Graph cleared. Add edges, then Run LFP.\n';
draw(null);
});
// โโ Initial render โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
logLine('Graph loaded with example edges (0โ1โ2โ4โ6โ7, 3โ5).');
logLine('Press "Run LFP" to compute reachability from node 0, or edit the graph first.');
draw(null);
Notice that the iteration always terminates (the set can only grow, and the graph is finite). The number of rounds is at most |V|, so the whole process runs in polynomial time โ matching the FO+LFP = PTIME theorem perfectly.
Comments
Loading comments...