<p class="hint">A small <b>Kripke structure</b>: five states connected by transitions. Each state carries
labels <b>R</b> (request) and/or <b>G</b> (grant). Pick a formula and press <b>Check</b> to see
which states satisfy it โ green = satisfied, red = violated.</p>
<div id="canvas-wrap"><canvas id="kripke" width="480" height="230"></canvas></div>
<div class="controls">
<label for="formula">Formula:</label>
<select id="formula">
<option value="GRF">G(R โ F G) โ LTL: every request is eventually granted</option>
<option value="FG">F G โ LTL: grant is eventually seen</option>
<option value="GnotR">G ยฌR โ LTL: no request ever</option>
<option value="EFG">EF G โ CTL: there exists a path reaching a grant state</option>
<option value="AGR">AG R โ CTL: on all paths, R holds everywhere</option>
</select>
<button id="check" type="button">Check</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div id="result" class="result"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
#canvas-wrap { border: 1px solid #cdd9e3; border-radius: 10px; overflow: hidden;
background: #f5f8fa; margin-bottom: .6rem; }
canvas { display: block; max-width: 100%; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-bottom: .5rem; }
label { font-size: .88rem; font-weight: 600; }
select { font: 14px system-ui,sans-serif; padding: .35rem .5rem; border: 1px solid #adb1b8;
border-radius: 7px; background: #fff; flex: 1; min-width: 200px; }
button { font: 600 13px system-ui,sans-serif; padding: .4rem .85rem; border-radius: 7px;
border: 1px solid #1d3557; background: #1d3557; color: #fff; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.result { font-size: .93rem; font-weight: 600; min-height: 1.4em; padding: .3rem 0; }
.result.ok { color: #0a7d33; }
.result.bad { color: #c92f3c; }
.result.info { color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Kripke structure: 5 states, transitions, labels.
// Label R = request, G = grant.
// s0 = initial state (double border).
const STATES = [
{ id: 0, name: 's0', x: 60, y: 115, R: false, G: false },
{ id: 1, name: 's1', x: 160, y: 55, R: true, G: false },
{ id: 2, name: 's2', x: 280, y: 55, R: true, G: true },
{ id: 3, name: 's3', x: 390, y: 115, R: false, G: true },
{ id: 4, name: 's4', x: 220, y: 175, R: false, G: false }
];
// Directed transitions (every state has at least one successor)
const TRANS = [
[0,1],[0,4], // s0 โ s1, s4
[1,2], // s1 โ s2
[2,3],[2,1], // s2 โ s3, s1 (can loop back: request repeats)
[3,0],[3,4], // s3 โ s0, s4
[4,4] // s4 โ s4 (sink: no R, no G, loops forever)
];
const succ = STATES.map(() => []);
TRANS.forEach(([a,b]) => succ[a].push(b));
let highlight = null; // null = unchecked; [{id, ok}] after check
// โโ Drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const canvas = document.getElementById('kripke');
const ctx = canvas.getContext('2d');
const RN = 22; // node radius
function stateColor(s) {
if (!highlight) return '#e8eef3';
const h = highlight.find(h => h.id === s.id);
return h ? (h.ok ? '#c8f0d3' : '#fcd0d0') : '#e8eef3';
}
function arrowHead(x, y, angle) {
const sz = 7;
ctx.beginPath();
ctx.fillStyle = '#778';
ctx.moveTo(x, y);
ctx.lineTo(x - sz * Math.cos(angle - 0.38), y - sz * Math.sin(angle - 0.38));
ctx.lineTo(x - sz * Math.cos(angle + 0.38), y - sz * Math.sin(angle + 0.38));
ctx.closePath();
ctx.fill();
}
function drawEdge(a, b) {
ctx.beginPath();
ctx.strokeStyle = '#778';
ctx.lineWidth = 1.4;
if (a === b) {
// self-loop: small circle above the node
const lx = STATES[a].x, ly = STATES[a].y - RN - 13;
ctx.arc(lx, ly, 13, 0.25, Math.PI - 0.25, false);
ctx.stroke();
const ex = lx + 13 * Math.sin(0.25), ey = ly + 13 * (1 - Math.cos(0.25));
arrowHead(ex, ey, Math.PI / 2 - 0.25);
return;
}
const sx = STATES[a].x, sy = STATES[a].y;
const ex = STATES[b].x, ey = STATES[b].y;
const dx = ex - sx, dy = ey - sy, len = Math.hypot(dx, dy);
const ux = dx / len, uy = dy / len;
// shorten by radius
const x1 = sx + ux * RN, y1 = sy + uy * RN;
const x2 = ex - ux * RN, y2 = ey - uy * RN;
// slight curve (offset perpendicular)
const cx = (x1 + x2) / 2 - uy * 16, cy = (y1 + y2) / 2 + ux * 16;
ctx.moveTo(x1, y1);
ctx.quadraticCurveTo(cx, cy, x2, y2);
ctx.stroke();
arrowHead(x2, y2, Math.atan2(y2 - cy, x2 - cx));
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// edges
TRANS.forEach(([a, b]) => drawEdge(a, b));
// nodes
STATES.forEach(s => {
ctx.beginPath();
ctx.arc(s.x, s.y, RN, 0, 2 * Math.PI);
ctx.fillStyle = stateColor(s);
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = s.id === 0 ? 2.6 : 1.6;
ctx.fill(); ctx.stroke();
// label
ctx.fillStyle = '#1d3557';
ctx.font = 'bold 11px system-ui,sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText((s.R ? 'R' : '') + (s.G ? 'G' : '') || 'โ
', s.x, s.y);
// state name below node
ctx.font = '10px system-ui,sans-serif';
ctx.fillStyle = '#555';
ctx.fillText(s.name, s.x, s.y + RN + 11);
});
// initial arrow into s0
ctx.beginPath();
ctx.moveTo(STATES[0].x - RN - 18, STATES[0].y);
ctx.lineTo(STATES[0].x - RN, STATES[0].y);
ctx.strokeStyle = '#1d3557'; ctx.lineWidth = 1.6; ctx.stroke();
arrowHead(STATES[0].x - RN, STATES[0].y, 0);
}
// โโ LTL model checking via lassos โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Every infinite path in a finite Kripke structure is a lasso:
// prefix[0..p-1] followed by cycle[0..c-1] repeated forever.
// We enumerate all lassos from a given start state via DFS.
function allLassos(start) {
const lassos = [];
function dfs(path, onStack) {
const cur = path[path.length - 1];
for (const n of succ[cur]) {
if (onStack.has(n)) {
// back-edge: cycle starts at first occurrence of n
const ci = path.indexOf(n);
lassos.push({ prefix: path.slice(0, ci), cycle: path.slice(ci) });
} else {
onStack.add(n);
path.push(n);
dfs(path, onStack);
path.pop();
onStack.delete(n);
}
}
}
const stack = new Set([start]);
dfs([start], stack);
return lassos;
}
// Check G(R โ F G) on a lasso: on the infinite path, every R is followed by G.
// We look ahead into the cycle (โฅ2 repetitions) to handle R at cycle tail.
function lassoGRFG({ prefix, cycle }) {
const full = [...prefix, ...cycle, ...cycle]; // 2 cycle unrolls covers all cases
for (let i = 0; i < prefix.length + cycle.length; i++) {
if (STATES[full[i]].R) {
const found = full.slice(i).some(id => STATES[id].G);
if (!found) return false;
}
}
return true;
}
// F G: grant appears somewhere on the lasso
function lassoFG({ prefix, cycle }) {
return [...prefix, ...cycle].some(id => STATES[id].G);
}
// G ยฌR: no request anywhere on the lasso
function lassoGnotR({ prefix, cycle }) {
return ![...prefix, ...cycle].some(id => STATES[id].R);
}
// LTL check: formula holds at startId iff it holds on ALL lassos from startId
function ltlCheck(startId, lassoFn) {
return allLassos(startId).every(l => lassoFn(l));
}
// โโ CTL model checking via fixpoint / reachability โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// EF G: exists a path eventually reaching a G-state (BFS reachability)
function checkEFG(startId) {
const vis = new Set([startId]);
const q = [startId];
while (q.length) {
const cur = q.shift();
if (STATES[cur].G) return true;
succ[cur].forEach(n => { if (!vis.has(n)) { vis.add(n); q.push(n); } });
}
return false;
}
// AG R: on all reachable states, R holds (if any reachable state lacks R โ false)
function checkAGR(startId) {
const vis = new Set([startId]);
const q = [startId];
while (q.length) {
const cur = q.shift();
if (!STATES[cur].R) return false;
succ[cur].forEach(n => { if (!vis.has(n)) { vis.add(n); q.push(n); } });
}
return true;
}
const CHECKERS = {
GRF: id => ltlCheck(id, lassoGRFG),
FG: id => ltlCheck(id, lassoFG),
GnotR: id => ltlCheck(id, lassoGnotR),
EFG: id => checkEFG(id),
AGR: id => checkAGR(id)
};
const FORMULA_LABELS = {
GRF: 'G(R โ F G)',
FG: 'F G',
GnotR: 'G ยฌR',
EFG: 'EF G',
AGR: 'AG R'
};
function runCheck() {
const fid = document.getElementById('formula').value;
const fname = FORMULA_LABELS[fid];
highlight = STATES.map(s => ({ id: s.id, ok: CHECKERS[fid](s.id) }));
const sat = highlight.filter(h => h.ok).map(h => STATES[h.id].name);
const vio = highlight.filter(h => !h.ok).map(h => STATES[h.id].name);
const res = document.getElementById('result');
if (vio.length === 0) {
res.textContent = fmt('โ %fname% is satisfied at every state.', { fname: fname });
res.className = 'result ok';
} else if (sat.length === 0) {
res.textContent = fmt('โ %fname% is violated at every state.', { fname: fname });
res.className = 'result bad';
} else {
res.textContent = fmt('%fname% holds at: %sat% | fails at: %vio%', { fname: fname, sat: sat.join(', '), vio: vio.join(', ') });
res.className = 'result info';
}
console.log('Formula:', fname, '| sat:', sat, '| violated:', vio);
draw();
}
document.getElementById('check').addEventListener('click', runCheck);
document.getElementById('reset').addEventListener('click', () => {
highlight = null;
document.getElementById('result').textContent = '';
document.getElementById('result').className = 'result';
draw();
});
draw();
<p class="hint">A small <b>Kripke structure</b>: five states connected by transitions. Each state carries
labels <b>R</b> (request) and/or <b>G</b> (grant). Pick a formula and press <b>Check</b> to see
which states satisfy it โ green = satisfied, red = violated.</p>
<div id="canvas-wrap"><canvas id="kripke" width="480" height="230"></canvas></div>
<div class="controls">
<label for="formula">Formula:</label>
<select id="formula">
<option value="GRF">G(R โ F G) โ LTL: every request is eventually granted</option>
<option value="FG">F G โ LTL: grant is eventually seen</option>
<option value="GnotR">G ยฌR โ LTL: no request ever</option>
<option value="EFG">EF G โ CTL: there exists a path reaching a grant state</option>
<option value="AGR">AG R โ CTL: on all paths, R holds everywhere</option>
</select>
<button id="check" type="button">Check</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div id="result" class="result"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
#canvas-wrap { border: 1px solid #cdd9e3; border-radius: 10px; overflow: hidden;
background: #f5f8fa; margin-bottom: .6rem; }
canvas { display: block; max-width: 100%; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-bottom: .5rem; }
label { font-size: .88rem; font-weight: 600; }
select { font: 14px system-ui,sans-serif; padding: .35rem .5rem; border: 1px solid #adb1b8;
border-radius: 7px; background: #fff; flex: 1; min-width: 200px; }
button { font: 600 13px system-ui,sans-serif; padding: .4rem .85rem; border-radius: 7px;
border: 1px solid #1d3557; background: #1d3557; color: #fff; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.result { font-size: .93rem; font-weight: 600; min-height: 1.4em; padding: .3rem 0; }
.result.ok { color: #0a7d33; }
.result.bad { color: #c92f3c; }
.result.info { color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Kripke structure: 5 states, transitions, labels.
// Label R = request, G = grant.
// s0 = initial state (double border).
const STATES = [
{ id: 0, name: 's0', x: 60, y: 115, R: false, G: false },
{ id: 1, name: 's1', x: 160, y: 55, R: true, G: false },
{ id: 2, name: 's2', x: 280, y: 55, R: true, G: true },
{ id: 3, name: 's3', x: 390, y: 115, R: false, G: true },
{ id: 4, name: 's4', x: 220, y: 175, R: false, G: false }
];
// Directed transitions (every state has at least one successor)
const TRANS = [
[0,1],[0,4], // s0 โ s1, s4
[1,2], // s1 โ s2
[2,3],[2,1], // s2 โ s3, s1 (can loop back: request repeats)
[3,0],[3,4], // s3 โ s0, s4
[4,4] // s4 โ s4 (sink: no R, no G, loops forever)
];
const succ = STATES.map(() => []);
TRANS.forEach(([a,b]) => succ[a].push(b));
let highlight = null; // null = unchecked; [{id, ok}] after check
// โโ Drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const canvas = document.getElementById('kripke');
const ctx = canvas.getContext('2d');
const RN = 22; // node radius
function stateColor(s) {
if (!highlight) return '#e8eef3';
const h = highlight.find(h => h.id === s.id);
return h ? (h.ok ? '#c8f0d3' : '#fcd0d0') : '#e8eef3';
}
function arrowHead(x, y, angle) {
const sz = 7;
ctx.beginPath();
ctx.fillStyle = '#778';
ctx.moveTo(x, y);
ctx.lineTo(x - sz * Math.cos(angle - 0.38), y - sz * Math.sin(angle - 0.38));
ctx.lineTo(x - sz * Math.cos(angle + 0.38), y - sz * Math.sin(angle + 0.38));
ctx.closePath();
ctx.fill();
}
function drawEdge(a, b) {
ctx.beginPath();
ctx.strokeStyle = '#778';
ctx.lineWidth = 1.4;
if (a === b) {
// self-loop: small circle above the node
const lx = STATES[a].x, ly = STATES[a].y - RN - 13;
ctx.arc(lx, ly, 13, 0.25, Math.PI - 0.25, false);
ctx.stroke();
const ex = lx + 13 * Math.sin(0.25), ey = ly + 13 * (1 - Math.cos(0.25));
arrowHead(ex, ey, Math.PI / 2 - 0.25);
return;
}
const sx = STATES[a].x, sy = STATES[a].y;
const ex = STATES[b].x, ey = STATES[b].y;
const dx = ex - sx, dy = ey - sy, len = Math.hypot(dx, dy);
const ux = dx / len, uy = dy / len;
// shorten by radius
const x1 = sx + ux * RN, y1 = sy + uy * RN;
const x2 = ex - ux * RN, y2 = ey - uy * RN;
// slight curve (offset perpendicular)
const cx = (x1 + x2) / 2 - uy * 16, cy = (y1 + y2) / 2 + ux * 16;
ctx.moveTo(x1, y1);
ctx.quadraticCurveTo(cx, cy, x2, y2);
ctx.stroke();
arrowHead(x2, y2, Math.atan2(y2 - cy, x2 - cx));
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// edges
TRANS.forEach(([a, b]) => drawEdge(a, b));
// nodes
STATES.forEach(s => {
ctx.beginPath();
ctx.arc(s.x, s.y, RN, 0, 2 * Math.PI);
ctx.fillStyle = stateColor(s);
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = s.id === 0 ? 2.6 : 1.6;
ctx.fill(); ctx.stroke();
// label
ctx.fillStyle = '#1d3557';
ctx.font = 'bold 11px system-ui,sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText((s.R ? 'R' : '') + (s.G ? 'G' : '') || 'โ
', s.x, s.y);
// state name below node
ctx.font = '10px system-ui,sans-serif';
ctx.fillStyle = '#555';
ctx.fillText(s.name, s.x, s.y + RN + 11);
});
// initial arrow into s0
ctx.beginPath();
ctx.moveTo(STATES[0].x - RN - 18, STATES[0].y);
ctx.lineTo(STATES[0].x - RN, STATES[0].y);
ctx.strokeStyle = '#1d3557'; ctx.lineWidth = 1.6; ctx.stroke();
arrowHead(STATES[0].x - RN, STATES[0].y, 0);
}
// โโ LTL model checking via lassos โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Every infinite path in a finite Kripke structure is a lasso:
// prefix[0..p-1] followed by cycle[0..c-1] repeated forever.
// We enumerate all lassos from a given start state via DFS.
function allLassos(start) {
const lassos = [];
function dfs(path, onStack) {
const cur = path[path.length - 1];
for (const n of succ[cur]) {
if (onStack.has(n)) {
// back-edge: cycle starts at first occurrence of n
const ci = path.indexOf(n);
lassos.push({ prefix: path.slice(0, ci), cycle: path.slice(ci) });
} else {
onStack.add(n);
path.push(n);
dfs(path, onStack);
path.pop();
onStack.delete(n);
}
}
}
const stack = new Set([start]);
dfs([start], stack);
return lassos;
}
// Check G(R โ F G) on a lasso: on the infinite path, every R is followed by G.
// We look ahead into the cycle (โฅ2 repetitions) to handle R at cycle tail.
function lassoGRFG({ prefix, cycle }) {
const full = [...prefix, ...cycle, ...cycle]; // 2 cycle unrolls covers all cases
for (let i = 0; i < prefix.length + cycle.length; i++) {
if (STATES[full[i]].R) {
const found = full.slice(i).some(id => STATES[id].G);
if (!found) return false;
}
}
return true;
}
// F G: grant appears somewhere on the lasso
function lassoFG({ prefix, cycle }) {
return [...prefix, ...cycle].some(id => STATES[id].G);
}
// G ยฌR: no request anywhere on the lasso
function lassoGnotR({ prefix, cycle }) {
return ![...prefix, ...cycle].some(id => STATES[id].R);
}
// LTL check: formula holds at startId iff it holds on ALL lassos from startId
function ltlCheck(startId, lassoFn) {
return allLassos(startId).every(l => lassoFn(l));
}
// โโ CTL model checking via fixpoint / reachability โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// EF G: exists a path eventually reaching a G-state (BFS reachability)
function checkEFG(startId) {
const vis = new Set([startId]);
const q = [startId];
while (q.length) {
const cur = q.shift();
if (STATES[cur].G) return true;
succ[cur].forEach(n => { if (!vis.has(n)) { vis.add(n); q.push(n); } });
}
return false;
}
// AG R: on all reachable states, R holds (if any reachable state lacks R โ false)
function checkAGR(startId) {
const vis = new Set([startId]);
const q = [startId];
while (q.length) {
const cur = q.shift();
if (!STATES[cur].R) return false;
succ[cur].forEach(n => { if (!vis.has(n)) { vis.add(n); q.push(n); } });
}
return true;
}
const CHECKERS = {
GRF: id => ltlCheck(id, lassoGRFG),
FG: id => ltlCheck(id, lassoFG),
GnotR: id => ltlCheck(id, lassoGnotR),
EFG: id => checkEFG(id),
AGR: id => checkAGR(id)
};
const FORMULA_LABELS = {
GRF: 'G(R โ F G)',
FG: 'F G',
GnotR: 'G ยฌR',
EFG: 'EF G',
AGR: 'AG R'
};
function runCheck() {
const fid = document.getElementById('formula').value;
const fname = FORMULA_LABELS[fid];
highlight = STATES.map(s => ({ id: s.id, ok: CHECKERS[fid](s.id) }));
const sat = highlight.filter(h => h.ok).map(h => STATES[h.id].name);
const vio = highlight.filter(h => !h.ok).map(h => STATES[h.id].name);
const res = document.getElementById('result');
if (vio.length === 0) {
res.textContent = fmt('โ %fname% is satisfied at every state.', { fname: fname });
res.className = 'result ok';
} else if (sat.length === 0) {
res.textContent = fmt('โ %fname% is violated at every state.', { fname: fname });
res.className = 'result bad';
} else {
res.textContent = fmt('%fname% holds at: %sat% | fails at: %vio%', { fname: fname, sat: sat.join(', '), vio: vio.join(', ') });
res.className = 'result info';
}
console.log('Formula:', fname, '| sat:', sat, '| violated:', vio);
draw();
}
document.getElementById('check').addEventListener('click', runCheck);
document.getElementById('reset').addEventListener('click', () => {
highlight = null;
document.getElementById('result').textContent = '';
document.getElementById('result').className = 'result';
draw();
});
draw();
Comments
Loading comments...