<!-- Demo layout — canvas for the state graph, bars for the probability distribution -->
<p class="hint">A toy protein with 5 states: Unfolded (U), two Intermediates (I1, I2), Near-native (N*), and Folded (F). Each step multiplies the probability vector by the transition matrix — watch the distribution converge to the equilibrium regardless of where it starts.</p>
<div class="msm-layout">
<canvas id="msm-canvas" width="340" height="220"></canvas>
<div class="info-panel">
<div class="state-bars" id="state-bars"></div>
<div class="step-counter">Step: <span id="step-num">0</span></div>
<div class="status" id="status">Start from the unfolded state (U = 100%).</div>
</div>
</div>
<div class="btns">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 4px; }
.hint { font-size: .85rem; color: #444; margin: 0 0 .6rem; line-height: 1.4; }
.msm-layout { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; background: #f7f9fb; flex-shrink: 0; }
.info-panel { flex: 1; min-width: 140px; }
.state-bars { display: flex; flex-direction: column; gap: 5px; margin-bottom: 8px; }
.bar-row { display: flex; align-items: center; gap: 6px; font-size: .82rem; }
.bar-label { width: 26px; font-weight: 700; text-align: right; color: #1d3557; }
.bar-track { flex: 1; height: 14px; background: #e0e6ec; border-radius: 4px; overflow: hidden; }
.bar-fill { height: 100%; border-radius: 4px; transition: width .25s; }
.bar-val { width: 38px; font-size: .78rem; color: #555; text-align: right; }
.step-counter { font-size: .82rem; color: #666; margin-bottom: 4px; }
.status { font-size: .88rem; font-weight: 600; min-height: 1.3em; color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: 8px; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .8rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Markov State Model demo: 5 metastable states mimicking protein folding.
// States in order — U=Unfolded I1=Intermediate1 I2=Intermediate2 N*=Near-native F=Folded
const STATES = ['U', 'I1', 'I2', 'N*', 'F'];
const COLORS = ['#e63946', '#f4a261', '#2a9d8f', '#457b9d', '#1d3557'];
// Row-stochastic transition matrix T[i][j] = prob of going from state i to state j.
// Each row sums to 1.0 (verified below).
const T = [
// U I1 I2 N* F
[0.60, 0.25, 0.10, 0.03, 0.02], // U
[0.20, 0.50, 0.20, 0.07, 0.03], // I1
[0.10, 0.15, 0.45, 0.25, 0.05], // I2
[0.02, 0.05, 0.15, 0.55, 0.23], // N*
[0.01, 0.02, 0.04, 0.18, 0.75], // F
];
// Canvas positions [x, y] for each state node (pixels).
const POS = [
[40, 110],
[110, 55],
[110, 165],
[230, 75],
[300, 110],
];
// Compute stationary distribution by power iteration (left eigenvector for eigenvalue 1).
function powerIterate(T, steps) {
let v = Array(T.length).fill(1 / T.length);
for (let s = 0; s < steps; s++) {
const next = Array(T.length).fill(0);
for (let i = 0; i < T.length; i++)
for (let j = 0; j < T.length; j++)
next[j] += v[i] * T[i][j];
v = next;
}
return v;
}
const STATIONARY = powerIterate(T, 200);
let prob = Array(STATES.length).fill(0);
prob[0] = 1.0; // Start fully unfolded.
let stepCount = 0;
let running = false;
let animId = null;
const canvas = document.getElementById('msm-canvas');
const ctx = canvas.getContext('2d');
// Draw a weighted directed arrow between two state nodes.
function drawArrow(x1, y1, x2, y2, weight, color) {
const r = 18;
const dx = x2 - x1, dy = y2 - y1;
const len = Math.sqrt(dx * dx + dy * dy);
const ux = dx / len, uy = dy / len;
const sx = x1 + ux * r, sy = y1 + uy * r;
const ex = x2 - ux * r, ey = y2 - uy * r;
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(ex, ey);
ctx.strokeStyle = color;
ctx.lineWidth = Math.max(0.5, weight * 5);
ctx.globalAlpha = 0.35 + weight * 0.5;
ctx.stroke();
// Draw arrowhead at destination.
const ax = ex - ux * 7 - uy * 4;
const ay = ey - uy * 7 + ux * 4;
const bx = ex - ux * 7 + uy * 4;
const by = ey - uy * 7 - ux * 4;
ctx.beginPath();
ctx.moveTo(ex, ey);
ctx.lineTo(ax, ay);
ctx.lineTo(bx, by);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
ctx.globalAlpha = 1;
}
function drawScene() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw transition arrows (skip very thin ones for clarity).
for (let i = 0; i < STATES.length; i++) {
for (let j = 0; j < STATES.length; j++) {
if (i === j) continue;
if (T[i][j] < 0.03) continue; // Skip arrows below 3% weight to reduce visual clutter.
drawArrow(POS[i][0], POS[i][1], POS[j][0], POS[j][1], T[i][j], '#888');
}
}
// Draw state nodes — radius grows with current probability mass.
for (let i = 0; i < STATES.length; i++) {
const [x, y] = POS[i];
const r = 14 + prob[i] * 18; // Larger radius = more probability mass in this state.
ctx.beginPath();
ctx.arc(x, y, r, 0, 2 * Math.PI);
ctx.fillStyle = COLORS[i];
ctx.globalAlpha = 0.25 + prob[i] * 0.75;
ctx.fill();
ctx.globalAlpha = 1;
ctx.strokeStyle = COLORS[i];
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = `bold ${prob[i] > 0.3 ? 13 : 11}px system-ui`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(STATES[i], x, y);
}
}
function updateBars() {
const container = document.getElementById('state-bars');
container.innerHTML = '';
for (let i = 0; i < STATES.length; i++) {
const pct = (prob[i] * 100).toFixed(1);
const row = document.createElement('div');
row.className = 'bar-row';
row.innerHTML =
`<span class="bar-label">${STATES[i]}</span>` +
`<span class="bar-track"><span class="bar-fill" style="width:${pct}%;background:${COLORS[i]}"></span></span>` +
`<span class="bar-val">${pct}%</span>`;
container.appendChild(row);
}
}
function setStatus(msg) {
document.getElementById('status').textContent = msg;
}
function updateStepCounter() {
document.getElementById('step-num').textContent = stepCount;
}
function step() {
// One Markov step: new probability = old probability * transition matrix.
const next = Array(STATES.length).fill(0);
for (let i = 0; i < STATES.length; i++)
for (let j = 0; j < STATES.length; j++)
next[j] += prob[i] * T[i][j];
prob = next;
stepCount++;
updateStepCounter();
drawScene();
updateBars();
// Stop automatically when the distribution is within 0.5% of stationary.
const maxDiff = STATES.reduce((m, _, i) => Math.max(m, Math.abs(prob[i] - STATIONARY[i])), 0);
if (maxDiff < 0.005) {
setStatus(i18nFmt('Converged at step %step% — stationary distribution reached.', { step: stepCount }));
stopRun();
} else {
setStatus(i18nFmt('Step %step% — probability spreading across states.', { step: stepCount }));
}
}
function stopRun() {
running = false;
if (animId) { clearInterval(animId); animId = null; }
document.getElementById('btn-run').textContent = 'Run';
document.getElementById('btn-step').disabled = false;
}
document.getElementById('btn-step').addEventListener('click', () => {
if (!running) step();
});
document.getElementById('btn-run').addEventListener('click', () => {
if (running) {
stopRun();
} else {
running = true;
document.getElementById('btn-run').textContent = 'Pause';
document.getElementById('btn-step').disabled = true;
animId = setInterval(step, 320);
}
});
document.getElementById('btn-reset').addEventListener('click', () => {
stopRun();
prob = Array(STATES.length).fill(0);
prob[0] = 1.0;
stepCount = 0;
updateStepCounter();
drawScene();
updateBars();
setStatus('Start from the unfolded state (U = 100%).');
});
drawScene();
updateBars();
setStatus('Start from the unfolded state (U = 100%).');
<!-- Demo layout — canvas for the state graph, bars for the probability distribution -->
<p class="hint">A toy protein with 5 states: Unfolded (U), two Intermediates (I1, I2), Near-native (N*), and Folded (F). Each step multiplies the probability vector by the transition matrix — watch the distribution converge to the equilibrium regardless of where it starts.</p>
<div class="msm-layout">
<canvas id="msm-canvas" width="340" height="220"></canvas>
<div class="info-panel">
<div class="state-bars" id="state-bars"></div>
<div class="step-counter">Step: <span id="step-num">0</span></div>
<div class="status" id="status">Start from the unfolded state (U = 100%).</div>
</div>
</div>
<div class="btns">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 4px; }
.hint { font-size: .85rem; color: #444; margin: 0 0 .6rem; line-height: 1.4; }
.msm-layout { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; background: #f7f9fb; flex-shrink: 0; }
.info-panel { flex: 1; min-width: 140px; }
.state-bars { display: flex; flex-direction: column; gap: 5px; margin-bottom: 8px; }
.bar-row { display: flex; align-items: center; gap: 6px; font-size: .82rem; }
.bar-label { width: 26px; font-weight: 700; text-align: right; color: #1d3557; }
.bar-track { flex: 1; height: 14px; background: #e0e6ec; border-radius: 4px; overflow: hidden; }
.bar-fill { height: 100%; border-radius: 4px; transition: width .25s; }
.bar-val { width: 38px; font-size: .78rem; color: #555; text-align: right; }
.step-counter { font-size: .82rem; color: #666; margin-bottom: 4px; }
.status { font-size: .88rem; font-weight: 600; min-height: 1.3em; color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: 8px; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .8rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Markov State Model demo: 5 metastable states mimicking protein folding.
// States in order — U=Unfolded I1=Intermediate1 I2=Intermediate2 N*=Near-native F=Folded
const STATES = ['U', 'I1', 'I2', 'N*', 'F'];
const COLORS = ['#e63946', '#f4a261', '#2a9d8f', '#457b9d', '#1d3557'];
// Row-stochastic transition matrix T[i][j] = prob of going from state i to state j.
// Each row sums to 1.0 (verified below).
const T = [
// U I1 I2 N* F
[0.60, 0.25, 0.10, 0.03, 0.02], // U
[0.20, 0.50, 0.20, 0.07, 0.03], // I1
[0.10, 0.15, 0.45, 0.25, 0.05], // I2
[0.02, 0.05, 0.15, 0.55, 0.23], // N*
[0.01, 0.02, 0.04, 0.18, 0.75], // F
];
// Canvas positions [x, y] for each state node (pixels).
const POS = [
[40, 110],
[110, 55],
[110, 165],
[230, 75],
[300, 110],
];
// Compute stationary distribution by power iteration (left eigenvector for eigenvalue 1).
function powerIterate(T, steps) {
let v = Array(T.length).fill(1 / T.length);
for (let s = 0; s < steps; s++) {
const next = Array(T.length).fill(0);
for (let i = 0; i < T.length; i++)
for (let j = 0; j < T.length; j++)
next[j] += v[i] * T[i][j];
v = next;
}
return v;
}
const STATIONARY = powerIterate(T, 200);
let prob = Array(STATES.length).fill(0);
prob[0] = 1.0; // Start fully unfolded.
let stepCount = 0;
let running = false;
let animId = null;
const canvas = document.getElementById('msm-canvas');
const ctx = canvas.getContext('2d');
// Draw a weighted directed arrow between two state nodes.
function drawArrow(x1, y1, x2, y2, weight, color) {
const r = 18;
const dx = x2 - x1, dy = y2 - y1;
const len = Math.sqrt(dx * dx + dy * dy);
const ux = dx / len, uy = dy / len;
const sx = x1 + ux * r, sy = y1 + uy * r;
const ex = x2 - ux * r, ey = y2 - uy * r;
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(ex, ey);
ctx.strokeStyle = color;
ctx.lineWidth = Math.max(0.5, weight * 5);
ctx.globalAlpha = 0.35 + weight * 0.5;
ctx.stroke();
// Draw arrowhead at destination.
const ax = ex - ux * 7 - uy * 4;
const ay = ey - uy * 7 + ux * 4;
const bx = ex - ux * 7 + uy * 4;
const by = ey - uy * 7 - ux * 4;
ctx.beginPath();
ctx.moveTo(ex, ey);
ctx.lineTo(ax, ay);
ctx.lineTo(bx, by);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
ctx.globalAlpha = 1;
}
function drawScene() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw transition arrows (skip very thin ones for clarity).
for (let i = 0; i < STATES.length; i++) {
for (let j = 0; j < STATES.length; j++) {
if (i === j) continue;
if (T[i][j] < 0.03) continue; // Skip arrows below 3% weight to reduce visual clutter.
drawArrow(POS[i][0], POS[i][1], POS[j][0], POS[j][1], T[i][j], '#888');
}
}
// Draw state nodes — radius grows with current probability mass.
for (let i = 0; i < STATES.length; i++) {
const [x, y] = POS[i];
const r = 14 + prob[i] * 18; // Larger radius = more probability mass in this state.
ctx.beginPath();
ctx.arc(x, y, r, 0, 2 * Math.PI);
ctx.fillStyle = COLORS[i];
ctx.globalAlpha = 0.25 + prob[i] * 0.75;
ctx.fill();
ctx.globalAlpha = 1;
ctx.strokeStyle = COLORS[i];
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = `bold ${prob[i] > 0.3 ? 13 : 11}px system-ui`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(STATES[i], x, y);
}
}
function updateBars() {
const container = document.getElementById('state-bars');
container.innerHTML = '';
for (let i = 0; i < STATES.length; i++) {
const pct = (prob[i] * 100).toFixed(1);
const row = document.createElement('div');
row.className = 'bar-row';
row.innerHTML =
`<span class="bar-label">${STATES[i]}</span>` +
`<span class="bar-track"><span class="bar-fill" style="width:${pct}%;background:${COLORS[i]}"></span></span>` +
`<span class="bar-val">${pct}%</span>`;
container.appendChild(row);
}
}
function setStatus(msg) {
document.getElementById('status').textContent = msg;
}
function updateStepCounter() {
document.getElementById('step-num').textContent = stepCount;
}
function step() {
// One Markov step: new probability = old probability * transition matrix.
const next = Array(STATES.length).fill(0);
for (let i = 0; i < STATES.length; i++)
for (let j = 0; j < STATES.length; j++)
next[j] += prob[i] * T[i][j];
prob = next;
stepCount++;
updateStepCounter();
drawScene();
updateBars();
// Stop automatically when the distribution is within 0.5% of stationary.
const maxDiff = STATES.reduce((m, _, i) => Math.max(m, Math.abs(prob[i] - STATIONARY[i])), 0);
if (maxDiff < 0.005) {
setStatus(i18nFmt('Converged at step %step% — stationary distribution reached.', { step: stepCount }));
stopRun();
} else {
setStatus(i18nFmt('Step %step% — probability spreading across states.', { step: stepCount }));
}
}
function stopRun() {
running = false;
if (animId) { clearInterval(animId); animId = null; }
document.getElementById('btn-run').textContent = 'Run';
document.getElementById('btn-step').disabled = false;
}
document.getElementById('btn-step').addEventListener('click', () => {
if (!running) step();
});
document.getElementById('btn-run').addEventListener('click', () => {
if (running) {
stopRun();
} else {
running = true;
document.getElementById('btn-run').textContent = 'Pause';
document.getElementById('btn-step').disabled = true;
animId = setInterval(step, 320);
}
});
document.getElementById('btn-reset').addEventListener('click', () => {
stopRun();
prob = Array(STATES.length).fill(0);
prob[0] = 1.0;
stepCount = 0;
updateStepCounter();
drawScene();
updateBars();
setStatus('Start from the unfolded state (U = 100%).');
});
drawScene();
updateBars();
setStatus('Start from the unfolded state (U = 100%).');
Comments
Loading comments...