Try it — with a classic toy version, the HP model. The chain has two bead types: H (hydrophobic, "oily") and P (polar). H beads like to huddle together away from water, so every time two H beads end up neighbors on the grid (without being neighbors in the chain), the fold gets one unit more stable.
<p class="hint">The chain has <span class="h">H</span> (oily) and <span class="p">P</span> (polar)
beads. Click a bead to bend the chain there. Pack the <span class="h">H</span> beads so they
touch on the grid — each H–H contact lowers the <b>energy</b>.</p>
<svg id="plot" viewBox="0 0 300 240" class="plot"></svg>
<div class="meter">
<div>Energy: <b id="energy" class="en">0</b></div>
<div>H–H contacts: <b id="contacts">0</b></div>
<div id="state" class="state"></div>
</div>
<div class="btns">
<button id="best" type="button">Best fold</button>
<button id="reset" type="button" class="ghost">Straighten</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.hint .h { color: #c0392b; font-weight: 800; } .hint .p { color: #457b9d; font-weight: 800; }
.plot { width: 100%; max-width: 380px; background: #f4f7f9; border: 1px solid #e2e6eb; border-radius: 10px; display: block; }
.bond { stroke: #6a7b88; stroke-width: 3; }
.contact { stroke: #e9b44c; stroke-width: 3; stroke-dasharray: 3 3; }
.bead { cursor: pointer; stroke: #fff; stroke-width: 2; }
.bead.h { fill: #c0392b; } .bead.p { fill: #457b9d; }
.blbl { fill: #fff; font: 800 11px ui-monospace, monospace; text-anchor: middle; dominant-baseline: central; pointer-events: none; }
.meter { display: flex; gap: 1.3rem; align-items: center; margin: .8rem 0 .6rem; font-size: 1rem; flex-wrap: wrap; }
.meter b { color: #1d3557; font-family: ui-monospace, monospace; }
.en { color: #0a7d33 !important; }
.state { font-weight: 700; }
.state.bad { color: #c0392b; }
.btns { display: flex; gap: .5rem; }
button { font: 600 14px system-ui, sans-serif; padding: .5rem 1rem; border: 1px solid #457b9d; background: #457b9d; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #457b9d; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// HP lattice model of protein folding. Beads are H (hydrophobic) or P (polar).
// Energy = -(number of H-H lattice contacts that aren't chain neighbors).
// Finding the minimum-energy fold is NP-hard; here we brute-force a length-12 chain.
const SEQ = 'HPHPPHHPHPPH';
const N = SEQ.length;
const DIRS = [[1, 0], [0, -1], [-1, 0], [0, 1]]; // E, N, W, S
let turns = new Array(N - 2).fill(0); // 0 straight, 1 left, 2 right
function positions(turns) {
const pos = [[0, 0]]; let d = 0; // start East
for (let k = 0; k < N - 1; k++) {
if (k >= 1) { const t = turns[k - 1]; if (t === 1) d = (d + 1) % 4; else if (t === 2) d = (d + 3) % 4; }
const [x, y] = pos[pos.length - 1]; pos.push([x + DIRS[d][0], y + DIRS[d][1]]);
}
return pos;
}
function valid(pos) { const seen = new Set(); for (const [x, y] of pos) { const k = x + ',' + y; if (seen.has(k)) return false; seen.add(k); } return true; }
function contacts(pos) {
let c = 0;
for (let i = 0; i < N; i++) for (let j = i + 2; j < N; j++) {
if (SEQ[i] === 'H' && SEQ[j] === 'H' && Math.abs(pos[i][0] - pos[j][0]) + Math.abs(pos[i][1] - pos[j][1]) === 1) c++;
}
return c;
}
function render() {
const pos = positions(turns);
const ok = valid(pos);
// fit to view
const xs = pos.map(p => p[0]), ys = pos.map(p => p[1]);
const minX = Math.min(...xs), maxX = Math.max(...xs), minY = Math.min(...ys), maxY = Math.max(...ys);
const span = Math.max(maxX - minX, maxY - minY, 1);
const S = Math.min(200 / span, 40); const ox = 150 - (minX + maxX) / 2 * S, oy = 120 - (minY + maxY) / 2 * S;
const sx = x => ox + x * S, sy = y => oy + y * S;
let s = '';
for (let k = 0; k < N - 1; k++) s += '<line class="bond" x1="' + sx(pos[k][0]) + '" y1="' + sy(pos[k][1]) + '" x2="' + sx(pos[k + 1][0]) + '" y2="' + sy(pos[k + 1][1]) + '"/>';
if (ok) for (let i = 0; i < N; i++) for (let j = i + 2; j < N; j++) if (SEQ[i] === 'H' && SEQ[j] === 'H' && Math.abs(pos[i][0] - pos[j][0]) + Math.abs(pos[i][1] - pos[j][1]) === 1) s += '<line class="contact" x1="' + sx(pos[i][0]) + '" y1="' + sy(pos[i][1]) + '" x2="' + sx(pos[j][0]) + '" y2="' + sy(pos[j][1]) + '"/>';
pos.forEach(([x, y], i) => { s += '<circle class="bead ' + (SEQ[i] === 'H' ? 'h' : 'p') + '" cx="' + sx(x) + '" cy="' + sy(y) + '" r="' + (S * 0.32) + '" data-i="' + i + '"/>'; s += '<text class="blbl" x="' + sx(x) + '" y="' + sy(y) + '">' + SEQ[i] + '</text>'; });
document.getElementById('plot').innerHTML = s;
document.getElementById('plot').querySelectorAll('.bead').forEach(el => el.addEventListener('click', () => { const i = +el.dataset.i; if (i >= 1 && i <= N - 2) { turns[i - 1] = (turns[i - 1] + 1) % 3; render(); } }));
const c = ok ? contacts(pos) : 0;
document.getElementById('contacts').textContent = c;
document.getElementById('energy').textContent = ok ? (-c) : '—';
const st = document.getElementById('state');
if (!ok) { st.className = 'state bad'; st.textContent = '✗ chain crosses itself — invalid'; }
else if (c === BEST) { st.className = 'state'; st.style.color = '#0a7d33'; st.textContent = '✓ optimal fold!'; }
else { st.className = 'state'; st.style.color = '#777'; st.textContent = fmt('best possible is %v% (click beads to bend)', { v: -BEST }); }
}
let BEST = 0, BESTTURNS = null;
(function solve() {
const tot = Math.pow(3, N - 2); const t = new Array(N - 2).fill(0);
for (let m = 0; m < tot; m++) {
let mm = m; for (let i = 0; i < N - 2; i++) { t[i] = mm % 3; mm = Math.floor(mm / 3); }
const pos = positions(t); if (!valid(pos)) continue; const c = contacts(pos);
if (c > BEST) { BEST = c; BESTTURNS = t.slice(); }
}
})();
document.getElementById('best').addEventListener('click', () => { turns = BESTTURNS.slice(); render(); });
document.getElementById('reset').addEventListener('click', () => { turns = new Array(N - 2).fill(0); render(); });
render();
<p class="hint">The chain has <span class="h">H</span> (oily) and <span class="p">P</span> (polar)
beads. Click a bead to bend the chain there. Pack the <span class="h">H</span> beads so they
touch on the grid — each H–H contact lowers the <b>energy</b>.</p>
<svg id="plot" viewBox="0 0 300 240" class="plot"></svg>
<div class="meter">
<div>Energy: <b id="energy" class="en">0</b></div>
<div>H–H contacts: <b id="contacts">0</b></div>
<div id="state" class="state"></div>
</div>
<div class="btns">
<button id="best" type="button">Best fold</button>
<button id="reset" type="button" class="ghost">Straighten</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.hint .h { color: #c0392b; font-weight: 800; } .hint .p { color: #457b9d; font-weight: 800; }
.plot { width: 100%; max-width: 380px; background: #f4f7f9; border: 1px solid #e2e6eb; border-radius: 10px; display: block; }
.bond { stroke: #6a7b88; stroke-width: 3; }
.contact { stroke: #e9b44c; stroke-width: 3; stroke-dasharray: 3 3; }
.bead { cursor: pointer; stroke: #fff; stroke-width: 2; }
.bead.h { fill: #c0392b; } .bead.p { fill: #457b9d; }
.blbl { fill: #fff; font: 800 11px ui-monospace, monospace; text-anchor: middle; dominant-baseline: central; pointer-events: none; }
.meter { display: flex; gap: 1.3rem; align-items: center; margin: .8rem 0 .6rem; font-size: 1rem; flex-wrap: wrap; }
.meter b { color: #1d3557; font-family: ui-monospace, monospace; }
.en { color: #0a7d33 !important; }
.state { font-weight: 700; }
.state.bad { color: #c0392b; }
.btns { display: flex; gap: .5rem; }
button { font: 600 14px system-ui, sans-serif; padding: .5rem 1rem; border: 1px solid #457b9d; background: #457b9d; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #457b9d; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// HP lattice model of protein folding. Beads are H (hydrophobic) or P (polar).
// Energy = -(number of H-H lattice contacts that aren't chain neighbors).
// Finding the minimum-energy fold is NP-hard; here we brute-force a length-12 chain.
const SEQ = 'HPHPPHHPHPPH';
const N = SEQ.length;
const DIRS = [[1, 0], [0, -1], [-1, 0], [0, 1]]; // E, N, W, S
let turns = new Array(N - 2).fill(0); // 0 straight, 1 left, 2 right
function positions(turns) {
const pos = [[0, 0]]; let d = 0; // start East
for (let k = 0; k < N - 1; k++) {
if (k >= 1) { const t = turns[k - 1]; if (t === 1) d = (d + 1) % 4; else if (t === 2) d = (d + 3) % 4; }
const [x, y] = pos[pos.length - 1]; pos.push([x + DIRS[d][0], y + DIRS[d][1]]);
}
return pos;
}
function valid(pos) { const seen = new Set(); for (const [x, y] of pos) { const k = x + ',' + y; if (seen.has(k)) return false; seen.add(k); } return true; }
function contacts(pos) {
let c = 0;
for (let i = 0; i < N; i++) for (let j = i + 2; j < N; j++) {
if (SEQ[i] === 'H' && SEQ[j] === 'H' && Math.abs(pos[i][0] - pos[j][0]) + Math.abs(pos[i][1] - pos[j][1]) === 1) c++;
}
return c;
}
function render() {
const pos = positions(turns);
const ok = valid(pos);
// fit to view
const xs = pos.map(p => p[0]), ys = pos.map(p => p[1]);
const minX = Math.min(...xs), maxX = Math.max(...xs), minY = Math.min(...ys), maxY = Math.max(...ys);
const span = Math.max(maxX - minX, maxY - minY, 1);
const S = Math.min(200 / span, 40); const ox = 150 - (minX + maxX) / 2 * S, oy = 120 - (minY + maxY) / 2 * S;
const sx = x => ox + x * S, sy = y => oy + y * S;
let s = '';
for (let k = 0; k < N - 1; k++) s += '<line class="bond" x1="' + sx(pos[k][0]) + '" y1="' + sy(pos[k][1]) + '" x2="' + sx(pos[k + 1][0]) + '" y2="' + sy(pos[k + 1][1]) + '"/>';
if (ok) for (let i = 0; i < N; i++) for (let j = i + 2; j < N; j++) if (SEQ[i] === 'H' && SEQ[j] === 'H' && Math.abs(pos[i][0] - pos[j][0]) + Math.abs(pos[i][1] - pos[j][1]) === 1) s += '<line class="contact" x1="' + sx(pos[i][0]) + '" y1="' + sy(pos[i][1]) + '" x2="' + sx(pos[j][0]) + '" y2="' + sy(pos[j][1]) + '"/>';
pos.forEach(([x, y], i) => { s += '<circle class="bead ' + (SEQ[i] === 'H' ? 'h' : 'p') + '" cx="' + sx(x) + '" cy="' + sy(y) + '" r="' + (S * 0.32) + '" data-i="' + i + '"/>'; s += '<text class="blbl" x="' + sx(x) + '" y="' + sy(y) + '">' + SEQ[i] + '</text>'; });
document.getElementById('plot').innerHTML = s;
document.getElementById('plot').querySelectorAll('.bead').forEach(el => el.addEventListener('click', () => { const i = +el.dataset.i; if (i >= 1 && i <= N - 2) { turns[i - 1] = (turns[i - 1] + 1) % 3; render(); } }));
const c = ok ? contacts(pos) : 0;
document.getElementById('contacts').textContent = c;
document.getElementById('energy').textContent = ok ? (-c) : '—';
const st = document.getElementById('state');
if (!ok) { st.className = 'state bad'; st.textContent = '✗ chain crosses itself — invalid'; }
else if (c === BEST) { st.className = 'state'; st.style.color = '#0a7d33'; st.textContent = '✓ optimal fold!'; }
else { st.className = 'state'; st.style.color = '#777'; st.textContent = fmt('best possible is %v% (click beads to bend)', { v: -BEST }); }
}
let BEST = 0, BESTTURNS = null;
(function solve() {
const tot = Math.pow(3, N - 2); const t = new Array(N - 2).fill(0);
for (let m = 0; m < tot; m++) {
let mm = m; for (let i = 0; i < N - 2; i++) { t[i] = mm % 3; mm = Math.floor(mm / 3); }
const pos = positions(t); if (!valid(pos)) continue; const c = contacts(pos);
if (c > BEST) { BEST = c; BESTTURNS = t.slice(); }
}
})();
document.getElementById('best').addEventListener('click', () => { turns = BESTTURNS.slice(); render(); });
document.getElementById('reset').addEventListener('click', () => { turns = new Array(N - 2).fill(0); render(); });
render();
Click a joint to bend the chain; keep it from crossing itself. Try to pack the H beads together to make the energy as low (negative) as possible, then hit Best fold to see the optimum. Even on this tiny grid, finding it by hand is fiddly — and the real problem, in 3D with 20 amino acids, is NP-hard.
Comments
Loading comments...