<!-- LTL to Büchi demo — builds the automaton for G(not-p), the negation of F p -->
<div class="demo-wrap">
<div class="formula-bar">
<span class="label">Formula:</span>
<span class="formula">F p</span>
<span class="label sep">→</span>
<span class="formula neg">G ¬p</span>
<span class="info-pill">we build the automaton for the negation</span>
</div>
<div class="step-controls">
<button id="btn-prev" type="button" class="ghost">← Prev</button>
<span id="step-counter" class="step-counter">Step 1 / 5</span>
<button id="btn-next" type="button">Next →</button>
<button id="btn-reset" type="button" class="ghost sm">Reset</button>
</div>
<div class="stage">
<svg id="automaton-svg" viewBox="0 0 480 220" width="100%" aria-label="Büchi automaton diagram being built step by step">
<!-- automaton SVG is drawn programmatically by JS -->
</svg>
</div>
<div class="explanation-box" id="explanation"></div>
<div class="legend">
<span class="leg-item"><span class="leg-dot init-accept"></span>Initial + accepting state (q₀)</span>
<span class="leg-item"><span class="leg-dot sink"></span>Sink state (q₁, non-accepting)</span>
</div>
</div>
/* demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.demo-wrap { max-width: 520px; margin: 0 auto; padding: .5rem; }
.formula-bar { display: flex; align-items: center; flex-wrap: wrap; gap: .4rem .7rem;
margin-bottom: .8rem; background: #f0f4f8; border-radius: 8px; padding: .5rem .8rem; }
.formula { font: 700 1.15rem ui-monospace, monospace; color: #1d3557; }
.formula.neg { color: #c92f3c; }
.label { font-size: .82rem; color: #555; }
.label.sep { font-size: 1.1rem; color: #888; }
.info-pill { font-size: .75rem; background: #dde8f5; color: #1d3557;
border-radius: 12px; padding: .15rem .55rem; }
.step-controls { display: flex; align-items: center; gap: .5rem; margin-bottom: .6rem; }
.step-counter { font-size: .9rem; color: #555; flex: 1; text-align: center; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button.sm { font-size: 12px; padding: .3rem .6rem; }
button:disabled { opacity: .4; cursor: default; }
.stage { background: #f7f9fb; border: 1px solid #d6dde5; border-radius: 10px;
padding: .5rem; margin-bottom: .7rem; }
/* SVG element classes */
.state-circle { fill: #fff; stroke: #1d3557; stroke-width: 2; }
.state-inner { fill: none; stroke: #c92f3c; stroke-width: 1.8; }
.state-label { font: 700 14px ui-monospace, monospace; fill: #1d3557; text-anchor: middle; dominant-baseline: middle; }
.arrow { fill: none; stroke: #1d3557; stroke-width: 1.8; marker-end: url(#arrowhead); opacity: 0; transition: opacity .4s; }
.arrow.visible { opacity: 1; }
.arrow-label { font-size: 12px; fill: #444; text-anchor: middle; opacity: 0; transition: opacity .4s; }
.arrow-label.visible { opacity: 1; }
.init-arrow { fill: none; stroke: #2a7d4f; stroke-width: 2; marker-end: url(#arrowhead-init); opacity: 0; transition: opacity .4s; }
.init-arrow.visible { opacity: 1; }
.accept-ring { fill: none; stroke: #c92f3c; stroke-width: 1.8; opacity: 0; transition: opacity .4s; }
.accept-ring.visible { opacity: 1; }
.explanation-box { font-size: .88rem; line-height: 1.55; min-height: 3.5em;
padding: .55rem .8rem; background: #fff8e1; border-left: 3px solid #f0a500;
border-radius: 0 6px 6px 0; margin-bottom: .6rem; }
.legend { display: flex; gap: .9rem; flex-wrap: wrap; font-size: .78rem; color: #555; }
.leg-item { display: flex; align-items: center; gap: .3rem; }
.leg-dot { width: 14px; height: 14px; border-radius: 50%; border: 2px solid #c92f3c; position: relative; }
.leg-dot.init-accept { border-color: #c92f3c; background: #fde8e8; }
.leg-dot.sink { border-color: #1d3557; background: #fff; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Each step reveals one more element of the automaton.
// show flags control which SVG elements are visible at each step.
const STEPS = [
{
// Step 1: bare states, no transitions, no markings
show: { states: true, init: false, q0ring: false, q0loop: false, q0q1: false, q1loop: false },
explKey: 'exp_step1'
},
{
// Step 2: mark q0 as initial state
show: { states: true, init: true, q0ring: false, q0loop: false, q0q1: false, q1loop: false },
explKey: 'exp_step2'
},
{
// Step 3: mark q0 as accepting (inner ring)
show: { states: true, init: true, q0ring: true, q0loop: false, q0q1: false, q1loop: false },
explKey: 'exp_step3'
},
{
// Step 4: add q0 self-loop labelled not-p
show: { states: true, init: true, q0ring: true, q0loop: true, q0q1: false, q1loop: false },
explKey: 'exp_step4'
},
{
// Step 5: add q0->q1 on p and q1 self-loop on true (sink)
show: { states: true, init: true, q0ring: true, q0loop: true, q0q1: true, q1loop: true },
explKey: 'exp_step5'
}
];
const TOTAL = STEPS.length;
let currentStep = 0;
const svg = document.getElementById('automaton-svg');
const NS = 'http://www.w3.org/2000/svg';
// SVG helper to create elements in the SVG namespace
function mkEl(tag, attrs) {
const el = document.createElementNS(NS, tag);
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
return el;
}
function buildSVG() {
svg.innerHTML = '';
// define arrowhead markers for both arrow colors
const defs = mkEl('defs', {});
const mkMarker = (id, color) => {
const m = mkEl('marker', { id, markerWidth: '8', markerHeight: '6', refX: '7', refY: '3', orient: 'auto' });
const p = mkEl('polygon', { points: '0 0, 8 3, 0 6', fill: color });
m.appendChild(p); return m;
};
defs.appendChild(mkMarker('arrowhead', '#1d3557'));
defs.appendChild(mkMarker('arrowhead-init', '#2a7d4f'));
svg.appendChild(defs);
// state center positions and radius
const q0x = 140, q0y = 110, q1x = 340, q1y = 110, R = 32;
// draw q0 with its accepting inner ring element (hidden until step 3)
const g0 = mkEl('g', {});
g0.appendChild(mkEl('circle', { cx: q0x, cy: q0y, r: R, class: 'state-circle' }));
const ring0 = mkEl('circle', { cx: q0x, cy: q0y, r: R - 6, id: 'q0-ring', class: 'accept-ring' });
g0.appendChild(ring0);
const t0 = mkEl('text', { x: q0x, y: q0y, class: 'state-label' });
t0.textContent = 'q₀';
g0.appendChild(t0);
svg.appendChild(g0);
// draw q1 — plain circle, non-accepting sink
const g1 = mkEl('g', {});
g1.appendChild(mkEl('circle', { cx: q1x, cy: q1y, r: R, class: 'state-circle' }));
const t1 = mkEl('text', { x: q1x, y: q1y, class: 'state-label' });
t1.textContent = 'q₁';
g1.appendChild(t1);
svg.appendChild(g1);
// short arrow entering q0 from the left to mark it as initial
const initArr = mkEl('path', { d: 'M' + (q0x - 65) + ',' + q0y + ' L' + (q0x - R - 2) + ',' + q0y, class: 'init-arrow', id: 'init-arrow' });
svg.appendChild(initArr);
// curved self-loop on q0 labelled not-p
const q0Loop = mkEl('path', { d: 'M' + (q0x - 20) + ',' + (q0y - R) + ' C' + (q0x - 60) + ',' + (q0y - 82) + ' ' + (q0x + 60) + ',' + (q0y - 82) + ' ' + (q0x + 20) + ',' + (q0y - R), class: 'arrow', id: 'q0-loop' });
svg.appendChild(q0Loop);
const q0LoopLbl = mkEl('text', { x: q0x, y: q0y - 77, class: 'arrow-label', id: 'q0-loop-lbl' });
q0LoopLbl.textContent = '¬p';
svg.appendChild(q0LoopLbl);
// curved arc from q0 to q1 labelled p
const q0q1 = mkEl('path', { d: 'M' + (q0x + R + 2) + ',' + (q0y - 8) + ' C' + (q0x + 80) + ',' + (q0y - 42) + ' ' + (q1x - 80) + ',' + (q0y - 42) + ' ' + (q1x - R - 2) + ',' + (q1y - 8), class: 'arrow', id: 'q0-q1' });
svg.appendChild(q0q1);
const q0q1Lbl = mkEl('text', { x: (q0x + q1x) / 2, y: q0y - 52, class: 'arrow-label', id: 'q0-q1-lbl' });
q0q1Lbl.textContent = 'p';
svg.appendChild(q0q1Lbl);
// curved self-loop on q1 labelled true (top symbol — absorbs all runs)
const q1Loop = mkEl('path', { d: 'M' + (q1x - 20) + ',' + (q1y - R) + ' C' + (q1x - 60) + ',' + (q1y - 82) + ' ' + (q1x + 60) + ',' + (q1y - 82) + ' ' + (q1x + 20) + ',' + (q1y - R), class: 'arrow', id: 'q1-loop' });
svg.appendChild(q1Loop);
const q1LoopLbl = mkEl('text', { x: q1x, y: q1y - 77, class: 'arrow-label', id: 'q1-loop-lbl' });
q1LoopLbl.textContent = '⊤';
svg.appendChild(q1LoopLbl);
}
function applyStep(idx) {
const s = STEPS[idx].show;
// show/hide each SVG element according to current step flags
const toggle = (id, vis) => {
const el = document.getElementById(id);
if (el) el.classList.toggle('visible', vis);
};
toggle('init-arrow', s.init);
toggle('q0-ring', s.q0ring);
toggle('q0-loop', s.q0loop);
toggle('q0-loop-lbl', s.q0loop);
toggle('q0-q1', s.q0q1);
toggle('q0-q1-lbl', s.q0q1);
toggle('q1-loop', s.q1loop);
toggle('q1-loop-lbl', s.q1loop);
document.getElementById('explanation').textContent = i18nData[STEPS[idx].explKey] || '';
document.getElementById('step-counter').textContent =
i18nFmt('Step %cur% / %total%', { cur: idx + 1, total: TOTAL });
document.getElementById('btn-prev').disabled = idx === 0;
document.getElementById('btn-next').disabled = idx === TOTAL - 1;
}
// read i18n strings injected by the kipu host page
let i18nData = {};
function loadI18n() {
const lang = (document.documentElement.lang || 'en').slice(0, 2);
const raw = document.getElementById('i18n-data');
if (!raw) return;
try {
const all = JSON.parse(raw.textContent);
i18nData = all[lang] || all['en'] || {};
} catch (e) { i18nData = {}; }
}
buildSVG();
loadI18n();
applyStep(currentStep);
document.getElementById('btn-next').addEventListener('click', () => {
if (currentStep < TOTAL - 1) { currentStep++; applyStep(currentStep); }
});
document.getElementById('btn-prev').addEventListener('click', () => {
if (currentStep > 0) { currentStep--; applyStep(currentStep); }
});
document.getElementById('btn-reset').addEventListener('click', () => {
currentStep = 0; applyStep(currentStep);
});
<!-- LTL to Büchi demo — builds the automaton for G(not-p), the negation of F p -->
<div class="demo-wrap">
<div class="formula-bar">
<span class="label">Formula:</span>
<span class="formula">F p</span>
<span class="label sep">→</span>
<span class="formula neg">G ¬p</span>
<span class="info-pill">we build the automaton for the negation</span>
</div>
<div class="step-controls">
<button id="btn-prev" type="button" class="ghost">← Prev</button>
<span id="step-counter" class="step-counter">Step 1 / 5</span>
<button id="btn-next" type="button">Next →</button>
<button id="btn-reset" type="button" class="ghost sm">Reset</button>
</div>
<div class="stage">
<svg id="automaton-svg" viewBox="0 0 480 220" width="100%" aria-label="Büchi automaton diagram being built step by step">
<!-- automaton SVG is drawn programmatically by JS -->
</svg>
</div>
<div class="explanation-box" id="explanation"></div>
<div class="legend">
<span class="leg-item"><span class="leg-dot init-accept"></span>Initial + accepting state (q₀)</span>
<span class="leg-item"><span class="leg-dot sink"></span>Sink state (q₁, non-accepting)</span>
</div>
</div>
/* demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.demo-wrap { max-width: 520px; margin: 0 auto; padding: .5rem; }
.formula-bar { display: flex; align-items: center; flex-wrap: wrap; gap: .4rem .7rem;
margin-bottom: .8rem; background: #f0f4f8; border-radius: 8px; padding: .5rem .8rem; }
.formula { font: 700 1.15rem ui-monospace, monospace; color: #1d3557; }
.formula.neg { color: #c92f3c; }
.label { font-size: .82rem; color: #555; }
.label.sep { font-size: 1.1rem; color: #888; }
.info-pill { font-size: .75rem; background: #dde8f5; color: #1d3557;
border-radius: 12px; padding: .15rem .55rem; }
.step-controls { display: flex; align-items: center; gap: .5rem; margin-bottom: .6rem; }
.step-counter { font-size: .9rem; color: #555; flex: 1; text-align: center; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button.sm { font-size: 12px; padding: .3rem .6rem; }
button:disabled { opacity: .4; cursor: default; }
.stage { background: #f7f9fb; border: 1px solid #d6dde5; border-radius: 10px;
padding: .5rem; margin-bottom: .7rem; }
/* SVG element classes */
.state-circle { fill: #fff; stroke: #1d3557; stroke-width: 2; }
.state-inner { fill: none; stroke: #c92f3c; stroke-width: 1.8; }
.state-label { font: 700 14px ui-monospace, monospace; fill: #1d3557; text-anchor: middle; dominant-baseline: middle; }
.arrow { fill: none; stroke: #1d3557; stroke-width: 1.8; marker-end: url(#arrowhead); opacity: 0; transition: opacity .4s; }
.arrow.visible { opacity: 1; }
.arrow-label { font-size: 12px; fill: #444; text-anchor: middle; opacity: 0; transition: opacity .4s; }
.arrow-label.visible { opacity: 1; }
.init-arrow { fill: none; stroke: #2a7d4f; stroke-width: 2; marker-end: url(#arrowhead-init); opacity: 0; transition: opacity .4s; }
.init-arrow.visible { opacity: 1; }
.accept-ring { fill: none; stroke: #c92f3c; stroke-width: 1.8; opacity: 0; transition: opacity .4s; }
.accept-ring.visible { opacity: 1; }
.explanation-box { font-size: .88rem; line-height: 1.55; min-height: 3.5em;
padding: .55rem .8rem; background: #fff8e1; border-left: 3px solid #f0a500;
border-radius: 0 6px 6px 0; margin-bottom: .6rem; }
.legend { display: flex; gap: .9rem; flex-wrap: wrap; font-size: .78rem; color: #555; }
.leg-item { display: flex; align-items: center; gap: .3rem; }
.leg-dot { width: 14px; height: 14px; border-radius: 50%; border: 2px solid #c92f3c; position: relative; }
.leg-dot.init-accept { border-color: #c92f3c; background: #fde8e8; }
.leg-dot.sink { border-color: #1d3557; background: #fff; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Each step reveals one more element of the automaton.
// show flags control which SVG elements are visible at each step.
const STEPS = [
{
// Step 1: bare states, no transitions, no markings
show: { states: true, init: false, q0ring: false, q0loop: false, q0q1: false, q1loop: false },
explKey: 'exp_step1'
},
{
// Step 2: mark q0 as initial state
show: { states: true, init: true, q0ring: false, q0loop: false, q0q1: false, q1loop: false },
explKey: 'exp_step2'
},
{
// Step 3: mark q0 as accepting (inner ring)
show: { states: true, init: true, q0ring: true, q0loop: false, q0q1: false, q1loop: false },
explKey: 'exp_step3'
},
{
// Step 4: add q0 self-loop labelled not-p
show: { states: true, init: true, q0ring: true, q0loop: true, q0q1: false, q1loop: false },
explKey: 'exp_step4'
},
{
// Step 5: add q0->q1 on p and q1 self-loop on true (sink)
show: { states: true, init: true, q0ring: true, q0loop: true, q0q1: true, q1loop: true },
explKey: 'exp_step5'
}
];
const TOTAL = STEPS.length;
let currentStep = 0;
const svg = document.getElementById('automaton-svg');
const NS = 'http://www.w3.org/2000/svg';
// SVG helper to create elements in the SVG namespace
function mkEl(tag, attrs) {
const el = document.createElementNS(NS, tag);
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
return el;
}
function buildSVG() {
svg.innerHTML = '';
// define arrowhead markers for both arrow colors
const defs = mkEl('defs', {});
const mkMarker = (id, color) => {
const m = mkEl('marker', { id, markerWidth: '8', markerHeight: '6', refX: '7', refY: '3', orient: 'auto' });
const p = mkEl('polygon', { points: '0 0, 8 3, 0 6', fill: color });
m.appendChild(p); return m;
};
defs.appendChild(mkMarker('arrowhead', '#1d3557'));
defs.appendChild(mkMarker('arrowhead-init', '#2a7d4f'));
svg.appendChild(defs);
// state center positions and radius
const q0x = 140, q0y = 110, q1x = 340, q1y = 110, R = 32;
// draw q0 with its accepting inner ring element (hidden until step 3)
const g0 = mkEl('g', {});
g0.appendChild(mkEl('circle', { cx: q0x, cy: q0y, r: R, class: 'state-circle' }));
const ring0 = mkEl('circle', { cx: q0x, cy: q0y, r: R - 6, id: 'q0-ring', class: 'accept-ring' });
g0.appendChild(ring0);
const t0 = mkEl('text', { x: q0x, y: q0y, class: 'state-label' });
t0.textContent = 'q₀';
g0.appendChild(t0);
svg.appendChild(g0);
// draw q1 — plain circle, non-accepting sink
const g1 = mkEl('g', {});
g1.appendChild(mkEl('circle', { cx: q1x, cy: q1y, r: R, class: 'state-circle' }));
const t1 = mkEl('text', { x: q1x, y: q1y, class: 'state-label' });
t1.textContent = 'q₁';
g1.appendChild(t1);
svg.appendChild(g1);
// short arrow entering q0 from the left to mark it as initial
const initArr = mkEl('path', { d: 'M' + (q0x - 65) + ',' + q0y + ' L' + (q0x - R - 2) + ',' + q0y, class: 'init-arrow', id: 'init-arrow' });
svg.appendChild(initArr);
// curved self-loop on q0 labelled not-p
const q0Loop = mkEl('path', { d: 'M' + (q0x - 20) + ',' + (q0y - R) + ' C' + (q0x - 60) + ',' + (q0y - 82) + ' ' + (q0x + 60) + ',' + (q0y - 82) + ' ' + (q0x + 20) + ',' + (q0y - R), class: 'arrow', id: 'q0-loop' });
svg.appendChild(q0Loop);
const q0LoopLbl = mkEl('text', { x: q0x, y: q0y - 77, class: 'arrow-label', id: 'q0-loop-lbl' });
q0LoopLbl.textContent = '¬p';
svg.appendChild(q0LoopLbl);
// curved arc from q0 to q1 labelled p
const q0q1 = mkEl('path', { d: 'M' + (q0x + R + 2) + ',' + (q0y - 8) + ' C' + (q0x + 80) + ',' + (q0y - 42) + ' ' + (q1x - 80) + ',' + (q0y - 42) + ' ' + (q1x - R - 2) + ',' + (q1y - 8), class: 'arrow', id: 'q0-q1' });
svg.appendChild(q0q1);
const q0q1Lbl = mkEl('text', { x: (q0x + q1x) / 2, y: q0y - 52, class: 'arrow-label', id: 'q0-q1-lbl' });
q0q1Lbl.textContent = 'p';
svg.appendChild(q0q1Lbl);
// curved self-loop on q1 labelled true (top symbol — absorbs all runs)
const q1Loop = mkEl('path', { d: 'M' + (q1x - 20) + ',' + (q1y - R) + ' C' + (q1x - 60) + ',' + (q1y - 82) + ' ' + (q1x + 60) + ',' + (q1y - 82) + ' ' + (q1x + 20) + ',' + (q1y - R), class: 'arrow', id: 'q1-loop' });
svg.appendChild(q1Loop);
const q1LoopLbl = mkEl('text', { x: q1x, y: q1y - 77, class: 'arrow-label', id: 'q1-loop-lbl' });
q1LoopLbl.textContent = '⊤';
svg.appendChild(q1LoopLbl);
}
function applyStep(idx) {
const s = STEPS[idx].show;
// show/hide each SVG element according to current step flags
const toggle = (id, vis) => {
const el = document.getElementById(id);
if (el) el.classList.toggle('visible', vis);
};
toggle('init-arrow', s.init);
toggle('q0-ring', s.q0ring);
toggle('q0-loop', s.q0loop);
toggle('q0-loop-lbl', s.q0loop);
toggle('q0-q1', s.q0q1);
toggle('q0-q1-lbl', s.q0q1);
toggle('q1-loop', s.q1loop);
toggle('q1-loop-lbl', s.q1loop);
document.getElementById('explanation').textContent = i18nData[STEPS[idx].explKey] || '';
document.getElementById('step-counter').textContent =
i18nFmt('Step %cur% / %total%', { cur: idx + 1, total: TOTAL });
document.getElementById('btn-prev').disabled = idx === 0;
document.getElementById('btn-next').disabled = idx === TOTAL - 1;
}
// read i18n strings injected by the kipu host page
let i18nData = {};
function loadI18n() {
const lang = (document.documentElement.lang || 'en').slice(0, 2);
const raw = document.getElementById('i18n-data');
if (!raw) return;
try {
const all = JSON.parse(raw.textContent);
i18nData = all[lang] || all['en'] || {};
} catch (e) { i18nData = {}; }
}
buildSVG();
loadI18n();
applyStep(currentStep);
document.getElementById('btn-next').addEventListener('click', () => {
if (currentStep < TOTAL - 1) { currentStep++; applyStep(currentStep); }
});
document.getElementById('btn-prev').addEventListener('click', () => {
if (currentStep > 0) { currentStep--; applyStep(currentStep); }
});
document.getElementById('btn-reset').addEventListener('click', () => {
currentStep = 0; applyStep(currentStep);
});
A Büchi automaton accepts an infinite run if and only if it visits an accepting state infinitely often. Runs looping on q0 forever visit it at every step — accepted, matching G ¬p. Runs that eventually reach q1 visit q0 only finitely many times — rejected, matching the traces that do satisfy F p.
Step through the builder to see each element added one by one. The automaton is just 2 states yet it compactly encodes all infinite counter-examples to F p.
Comments
Loading comments...