<p class="hint">The classic <b>Sprinkler network</b>. Each node shows P(True). Click a node to cycle it through <em>unknown → observed True → observed False</em> and watch the other beliefs update via Bayes' theorem.</p>
<div id="network"></div>
<div id="legend">
<span class="leg-item"><span class="leg-dot unknown"></span>Unknown</span>
<span class="leg-item"><span class="leg-dot true"></span>Observed True</span>
<span class="leg-item"><span class="leg-dot false"></span>Observed False</span>
</div>
<button id="reset" type="button">Reset evidence</button>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
#network { position: relative; width: 100%; height: 320px; }
svg.edges { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; }
svg.edges path { fill: none; stroke: #8fa3b2; stroke-width: 2; marker-end: url(#arr); }
.node { position: absolute; width: 110px; text-align: center; cursor: pointer; user-select: none; transform: translate(-50%, -50%); }
.node-box { border-radius: 12px; padding: .5rem .4rem .4rem; border: 2px solid #8fa3b2; background: #eef2f5; transition: background .2s, border-color .2s; }
.node.obs-true .node-box { background: #d4edda; border-color: #28a745; }
.node.obs-false .node-box { background: #f8d7da; border-color: #dc3545; }
.node-box:hover { filter: brightness(0.96); }
.node-name { font-size: .78rem; font-weight: 700; color: #1d3557; margin-bottom: .25rem; }
.node-prob { font-size: 1.05rem; font-weight: 800; color: #1d3557; }
.node-state { font-size: .7rem; color: #555; margin-top: .15rem; min-height: .85rem; }
#legend { display: flex; gap: 1rem; font-size: .8rem; color: #555; margin: .6rem 0 .5rem; flex-wrap: wrap; }
.leg-item { display: flex; align-items: center; gap: .3rem; }
.leg-dot { width: 12px; height: 12px; border-radius: 50%; border: 2px solid; }
.leg-dot.unknown { background: #eef2f5; border-color: #8fa3b2; }
.leg-dot.true { background: #d4edda; border-color: #28a745; }
.leg-dot.false { background: #f8d7da; border-color: #dc3545; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Bayesian Network: classic Sprinkler example (Pearl 1988)
// Nodes: Cloudy (C), Sprinkler (S), Rain (R), WetGrass (W)
// Edges: C->S, C->R, S->W, R->W
//
// CPTs (all represent P(node=True | parents)):
// P(C=T) = 0.5
// P(S=T|C=T)=0.10, P(S=T|C=F)=0.50
// P(R=T|C=T)=0.80, P(R=T|C=F)=0.20
// P(W=T|S=T,R=T)=0.99, P(W=T|S=T,R=F)=0.90
// P(W=T|S=F,R=T)=0.90, P(W=T|S=F,R=F)=0.01
//
// Inference: enumerate all 16 joint assignments, apply CPT products,
// then marginalize + condition on observed evidence.
const CPT = {
C: { '': 0.5 },
S: { T: 0.10, F: 0.50 },
R: { T: 0.80, F: 0.20 },
W: { TT: 0.99, TF: 0.90, FT: 0.90, FF: 0.01 }
};
function jointProb(c, s, r, w) {
const pC = c ? CPT.C[''] : 1 - CPT.C[''];
const pS = s ? CPT.S[c ? 'T' : 'F'] : 1 - CPT.S[c ? 'T' : 'F'];
const pR = r ? CPT.R[c ? 'T' : 'F'] : 1 - CPT.R[c ? 'T' : 'F'];
const key = (s ? 'T' : 'F') + (r ? 'T' : 'F');
const pW = w ? CPT.W[key] : 1 - CPT.W[key];
return pC * pS * pR * pW;
}
// evidence: {C: true/false/null, S: ..., R: ..., W: ...}
function infer(evidence) {
const vars = ['C','S','R','W'];
let totals = { C: 0, S: 0, R: 0, W: 0 };
let Z = 0;
for (let mask = 0; mask < 16; mask++) {
const vals = {
C: !!(mask & 8), S: !!(mask & 4), R: !!(mask & 2), W: !!(mask & 1)
};
// Check evidence consistency
let consistent = true;
for (const v of vars) {
if (evidence[v] !== null && evidence[v] !== vals[v]) { consistent = false; break; }
}
if (!consistent) continue;
const p = jointProb(vals.C, vals.S, vals.R, vals.W);
Z += p;
for (const v of vars) if (vals[v]) totals[v] += p;
}
const result = {};
for (const v of vars) result[v] = Z > 0 ? totals[v] / Z : 0.5;
return result;
}
// Layout positions (percentage of container)
const POSITIONS = {
C: { x: 50, y: 14 },
S: { x: 25, y: 48 },
R: { x: 75, y: 48 },
W: { x: 50, y: 82 }
};
const LABELS = { C: 'Cloudy', S: 'Sprinkler', R: 'Rain', W: 'Wet Grass' };
const EDGES = [['C','S'],['C','R'],['S','W'],['R','W']];
let evidence = { C: null, S: null, R: null, W: null };
let probs = infer(evidence);
function stateClass(v) {
if (evidence[v] === true) return 'obs-true';
if (evidence[v] === false) return 'obs-false';
return '';
}
function stateLabel(v) {
if (evidence[v] === true) return 'Observed: True';
if (evidence[v] === false) return 'Observed: False';
return '';
}
function pct(v) { return (probs[v] * 100).toFixed(1) + '%'; }
function render() {
probs = infer(evidence);
const net = document.getElementById('network');
net.innerHTML = '';
// SVG for edges with arrowhead
const svg = document.createElementNS('http://www.w3.org/2000/svg','svg');
svg.setAttribute('class','edges');
svg.innerHTML = `<defs><marker id="arr" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#8fa3b2"/></marker></defs>`;
const W = net.offsetWidth || 300;
const H = 320;
EDGES.forEach(([a, b]) => {
const ax = POSITIONS[a].x / 100 * W;
const ay = POSITIONS[a].y / 100 * H;
const bx = POSITIONS[b].x / 100 * W;
const by = POSITIONS[b].y / 100 * H;
// Shorten line so it doesn't overlap nodes
const dx = bx - ax, dy = by - ay;
const len = Math.sqrt(dx*dx + dy*dy);
const trim = 44;
const x1 = ax + dx/len*trim, y1 = ay + dy/len*trim;
const x2 = bx - dx/len*trim, y2 = by - dy/len*trim;
const path = document.createElementNS('http://www.w3.org/2000/svg','path');
path.setAttribute('d', `M${x1},${y1} L${x2},${y2}`);
svg.appendChild(path);
});
net.appendChild(svg);
// Node divs
Object.keys(POSITIONS).forEach(v => {
const pos = POSITIONS[v];
const div = document.createElement('div');
div.className = 'node ' + stateClass(v);
div.style.left = pos.x + '%';
div.style.top = pos.y + '%';
div.innerHTML = `<div class="node-box">
<div class="node-name">${LABELS[v]}</div>
<div class="node-prob">${pct(v)}</div>
<div class="node-state">${stateLabel(v)}</div>
</div>`;
div.addEventListener('click', () => {
// cycle: null -> true -> false -> null
if (evidence[v] === null) evidence[v] = true;
else if (evidence[v] === true) evidence[v] = false;
else evidence[v] = null;
render();
});
net.appendChild(div);
});
}
document.getElementById('reset').addEventListener('click', () => {
evidence = { C: null, S: null, R: null, W: null };
render();
});
// Initial render + re-render on resize
render();
window.addEventListener('resize', render);
// Verify: P(W=T) = 0.65 with these CPTs
const pw = infer({C:null,S:null,R:null,W:null}).W;
console.assert(Math.abs(pw - 0.65) < 0.001,
fmt('P(W=T) baseline should be 0.65, got %pw%', { pw: pw.toFixed(4) }));
// With W observed True: P(R=T|W=T) should be ~0.7048
const pRgivenW = infer({C:null,S:null,R:null,W:true}).R;
console.assert(Math.abs(pRgivenW - 0.7048) < 0.002,
fmt('P(R=T|W=T) should be ~0.7048, got %pRgivenW%', { pRgivenW: pRgivenW.toFixed(4) }));
// "Explaining away": P(S=T|W=T,R=T) should drop vs P(S=T|W=T)
const pSgivenW = infer({C:null,S:null,R:null,W:true}).S;
const pSgivenWR = infer({C:null,S:null,R:true,W:true}).S;
console.assert(pSgivenWR < pSgivenW,
fmt('Explaining away: P(S|W,R) should be less than P(S|W): %pSgivenWR% vs %pSgivenW%', { pSgivenWR: pSgivenWR.toFixed(4), pSgivenW: pSgivenW.toFixed(4) }));
// Conditional independence: C independent of W given S and R
const pCgivenST = infer({C:null,S:true,R:null,W:null}).C;
console.assert(pCgivenST < 0.5,
fmt('P(C=T|S=T) should be < 0.5 since sprinkler is rare given cloudy, got %pCgivenST%', { pCgivenST: pCgivenST.toFixed(4) }));
console.log('Bayesian network demo verification passed.');
console.log('P(W=T):', pw.toFixed(4));
console.log('P(R=T|W=T):', pRgivenW.toFixed(4));
console.log('P(S=T|W=T):', pSgivenW.toFixed(4), ' P(S=T|W=T,R=T):', pSgivenWR.toFixed(4), '← explaining away');
<p class="hint">The classic <b>Sprinkler network</b>. Each node shows P(True). Click a node to cycle it through <em>unknown → observed True → observed False</em> and watch the other beliefs update via Bayes' theorem.</p>
<div id="network"></div>
<div id="legend">
<span class="leg-item"><span class="leg-dot unknown"></span>Unknown</span>
<span class="leg-item"><span class="leg-dot true"></span>Observed True</span>
<span class="leg-item"><span class="leg-dot false"></span>Observed False</span>
</div>
<button id="reset" type="button">Reset evidence</button>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
#network { position: relative; width: 100%; height: 320px; }
svg.edges { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; }
svg.edges path { fill: none; stroke: #8fa3b2; stroke-width: 2; marker-end: url(#arr); }
.node { position: absolute; width: 110px; text-align: center; cursor: pointer; user-select: none; transform: translate(-50%, -50%); }
.node-box { border-radius: 12px; padding: .5rem .4rem .4rem; border: 2px solid #8fa3b2; background: #eef2f5; transition: background .2s, border-color .2s; }
.node.obs-true .node-box { background: #d4edda; border-color: #28a745; }
.node.obs-false .node-box { background: #f8d7da; border-color: #dc3545; }
.node-box:hover { filter: brightness(0.96); }
.node-name { font-size: .78rem; font-weight: 700; color: #1d3557; margin-bottom: .25rem; }
.node-prob { font-size: 1.05rem; font-weight: 800; color: #1d3557; }
.node-state { font-size: .7rem; color: #555; margin-top: .15rem; min-height: .85rem; }
#legend { display: flex; gap: 1rem; font-size: .8rem; color: #555; margin: .6rem 0 .5rem; flex-wrap: wrap; }
.leg-item { display: flex; align-items: center; gap: .3rem; }
.leg-dot { width: 12px; height: 12px; border-radius: 50%; border: 2px solid; }
.leg-dot.unknown { background: #eef2f5; border-color: #8fa3b2; }
.leg-dot.true { background: #d4edda; border-color: #28a745; }
.leg-dot.false { background: #f8d7da; border-color: #dc3545; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Bayesian Network: classic Sprinkler example (Pearl 1988)
// Nodes: Cloudy (C), Sprinkler (S), Rain (R), WetGrass (W)
// Edges: C->S, C->R, S->W, R->W
//
// CPTs (all represent P(node=True | parents)):
// P(C=T) = 0.5
// P(S=T|C=T)=0.10, P(S=T|C=F)=0.50
// P(R=T|C=T)=0.80, P(R=T|C=F)=0.20
// P(W=T|S=T,R=T)=0.99, P(W=T|S=T,R=F)=0.90
// P(W=T|S=F,R=T)=0.90, P(W=T|S=F,R=F)=0.01
//
// Inference: enumerate all 16 joint assignments, apply CPT products,
// then marginalize + condition on observed evidence.
const CPT = {
C: { '': 0.5 },
S: { T: 0.10, F: 0.50 },
R: { T: 0.80, F: 0.20 },
W: { TT: 0.99, TF: 0.90, FT: 0.90, FF: 0.01 }
};
function jointProb(c, s, r, w) {
const pC = c ? CPT.C[''] : 1 - CPT.C[''];
const pS = s ? CPT.S[c ? 'T' : 'F'] : 1 - CPT.S[c ? 'T' : 'F'];
const pR = r ? CPT.R[c ? 'T' : 'F'] : 1 - CPT.R[c ? 'T' : 'F'];
const key = (s ? 'T' : 'F') + (r ? 'T' : 'F');
const pW = w ? CPT.W[key] : 1 - CPT.W[key];
return pC * pS * pR * pW;
}
// evidence: {C: true/false/null, S: ..., R: ..., W: ...}
function infer(evidence) {
const vars = ['C','S','R','W'];
let totals = { C: 0, S: 0, R: 0, W: 0 };
let Z = 0;
for (let mask = 0; mask < 16; mask++) {
const vals = {
C: !!(mask & 8), S: !!(mask & 4), R: !!(mask & 2), W: !!(mask & 1)
};
// Check evidence consistency
let consistent = true;
for (const v of vars) {
if (evidence[v] !== null && evidence[v] !== vals[v]) { consistent = false; break; }
}
if (!consistent) continue;
const p = jointProb(vals.C, vals.S, vals.R, vals.W);
Z += p;
for (const v of vars) if (vals[v]) totals[v] += p;
}
const result = {};
for (const v of vars) result[v] = Z > 0 ? totals[v] / Z : 0.5;
return result;
}
// Layout positions (percentage of container)
const POSITIONS = {
C: { x: 50, y: 14 },
S: { x: 25, y: 48 },
R: { x: 75, y: 48 },
W: { x: 50, y: 82 }
};
const LABELS = { C: 'Cloudy', S: 'Sprinkler', R: 'Rain', W: 'Wet Grass' };
const EDGES = [['C','S'],['C','R'],['S','W'],['R','W']];
let evidence = { C: null, S: null, R: null, W: null };
let probs = infer(evidence);
function stateClass(v) {
if (evidence[v] === true) return 'obs-true';
if (evidence[v] === false) return 'obs-false';
return '';
}
function stateLabel(v) {
if (evidence[v] === true) return 'Observed: True';
if (evidence[v] === false) return 'Observed: False';
return '';
}
function pct(v) { return (probs[v] * 100).toFixed(1) + '%'; }
function render() {
probs = infer(evidence);
const net = document.getElementById('network');
net.innerHTML = '';
// SVG for edges with arrowhead
const svg = document.createElementNS('http://www.w3.org/2000/svg','svg');
svg.setAttribute('class','edges');
svg.innerHTML = `<defs><marker id="arr" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#8fa3b2"/></marker></defs>`;
const W = net.offsetWidth || 300;
const H = 320;
EDGES.forEach(([a, b]) => {
const ax = POSITIONS[a].x / 100 * W;
const ay = POSITIONS[a].y / 100 * H;
const bx = POSITIONS[b].x / 100 * W;
const by = POSITIONS[b].y / 100 * H;
// Shorten line so it doesn't overlap nodes
const dx = bx - ax, dy = by - ay;
const len = Math.sqrt(dx*dx + dy*dy);
const trim = 44;
const x1 = ax + dx/len*trim, y1 = ay + dy/len*trim;
const x2 = bx - dx/len*trim, y2 = by - dy/len*trim;
const path = document.createElementNS('http://www.w3.org/2000/svg','path');
path.setAttribute('d', `M${x1},${y1} L${x2},${y2}`);
svg.appendChild(path);
});
net.appendChild(svg);
// Node divs
Object.keys(POSITIONS).forEach(v => {
const pos = POSITIONS[v];
const div = document.createElement('div');
div.className = 'node ' + stateClass(v);
div.style.left = pos.x + '%';
div.style.top = pos.y + '%';
div.innerHTML = `<div class="node-box">
<div class="node-name">${LABELS[v]}</div>
<div class="node-prob">${pct(v)}</div>
<div class="node-state">${stateLabel(v)}</div>
</div>`;
div.addEventListener('click', () => {
// cycle: null -> true -> false -> null
if (evidence[v] === null) evidence[v] = true;
else if (evidence[v] === true) evidence[v] = false;
else evidence[v] = null;
render();
});
net.appendChild(div);
});
}
document.getElementById('reset').addEventListener('click', () => {
evidence = { C: null, S: null, R: null, W: null };
render();
});
// Initial render + re-render on resize
render();
window.addEventListener('resize', render);
// Verify: P(W=T) = 0.65 with these CPTs
const pw = infer({C:null,S:null,R:null,W:null}).W;
console.assert(Math.abs(pw - 0.65) < 0.001,
fmt('P(W=T) baseline should be 0.65, got %pw%', { pw: pw.toFixed(4) }));
// With W observed True: P(R=T|W=T) should be ~0.7048
const pRgivenW = infer({C:null,S:null,R:null,W:true}).R;
console.assert(Math.abs(pRgivenW - 0.7048) < 0.002,
fmt('P(R=T|W=T) should be ~0.7048, got %pRgivenW%', { pRgivenW: pRgivenW.toFixed(4) }));
// "Explaining away": P(S=T|W=T,R=T) should drop vs P(S=T|W=T)
const pSgivenW = infer({C:null,S:null,R:null,W:true}).S;
const pSgivenWR = infer({C:null,S:null,R:true,W:true}).S;
console.assert(pSgivenWR < pSgivenW,
fmt('Explaining away: P(S|W,R) should be less than P(S|W): %pSgivenWR% vs %pSgivenW%', { pSgivenWR: pSgivenWR.toFixed(4), pSgivenW: pSgivenW.toFixed(4) }));
// Conditional independence: C independent of W given S and R
const pCgivenST = infer({C:null,S:true,R:null,W:null}).C;
console.assert(pCgivenST < 0.5,
fmt('P(C=T|S=T) should be < 0.5 since sprinkler is rare given cloudy, got %pCgivenST%', { pCgivenST: pCgivenST.toFixed(4) }));
console.log('Bayesian network demo verification passed.');
console.log('P(W=T):', pw.toFixed(4));
console.log('P(R=T|W=T):', pRgivenW.toFixed(4));
console.log('P(S=T|W=T):', pSgivenW.toFixed(4), ' P(S=T|W=T,R=T):', pSgivenWR.toFixed(4), '← explaining away');
Comments
Loading comments...