Here is a short chain of residues, each one of a few "types." A hidden target assigns the best type to every position. Your score is how well the chain matches what the target wants given its neighbors — exactly the kind of local energy a real design tool minimizes. Click a residue to cycle its type, or let the computer search.
<p class="hint">Each tile is a <b>residue</b> with a type (color). A hidden target wants a specific type at every position. Your <b>score</b> rewards each residue that matches the target, plus a small bonus where neighbors differ — a tiny stand-in for folding energy. Click a residue to cycle its type, or let the computer search.</p>
<div id="chain" class="chain"></div>
<div class="bar"><div id="fill" class="fill"></div></div>
<div class="status" id="status">Score: 0 / 0</div>
<div class="btns">
<button id="mutate" type="button">Mutate one residue (hill-climb)</button>
<button id="search" type="button">Exhaustive search</button>
<button id="reset" type="button" class="ghost">Reset</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; }
.chain { display: flex; gap: 6px; flex-wrap: wrap; margin: .5rem 0; }
.res { width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
font: 700 16px ui-monospace, monospace; color: #fff; border-radius: 50%;
cursor: pointer; user-select: none; transition: transform .08s; border: 2px solid rgba(0,0,0,.15); }
.res:hover { transform: scale(1.08); }
.res.t0 { background: #2a9d8f; }
.res.t1 { background: #e76f51; }
.res.t2 { background: #457b9d; }
.res.ok { box-shadow: 0 0 0 2px #0a7d33; }
.bar { height: 12px; background: #e8eef3; border-radius: 6px; overflow: hidden; margin: .4rem 0; }
.fill { height: 100%; width: 0; background: #2a9d8f; transition: width .2s; }
.status { font-size: 1rem; font-weight: 600; margin: .4rem 0; min-height: 1.4em; }
.status.win { color: #0a7d33; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Inverse folding, in miniature. A chain of residues, each of K types. A hidden
// TARGET assigns the ideal type per position. The "energy" we MAXIMIZE rewards
// matching the target (+MATCH) and adds a contrast bonus where adjacent residues differ (+DIFF) — a local pairwise score, the shape real design tools optimize. Scoring a chain is a single linear pass; finding the optimum is a search over K^n sequences. The target is the unique best sequence (verified by brute force).
const K = 3; // residue types (0,1,2)
const TARGET = [0,1,1,2,0,2,1,0]; // hidden ideal sequence
const N = TARGET.length;
const MATCH = 3, DIFF = 1;
const LETTERS = ['H','P','C']; // hydrophobic / polar / charged (flavor only)
function score(seq){
let t = 0;
for (let i=0;i<N;i++) if (seq[i] === TARGET[i]) t += MATCH;
for (let i=0;i<N-1;i++) if (seq[i] !== seq[i+1]) t += DIFF;
return t;
}
const MAX = score(TARGET); // best possible = the target itself
let seq = new Array(N).fill(0); // player's chain; start all type 0
function render(){
const c = document.getElementById('chain'); c.innerHTML='';
for(let i=0;i<N;i++){
const d = document.createElement('div');
const ok = seq[i] === TARGET[i];
d.className = 'res t'+seq[i]+(ok?' ok':'');
d.textContent = LETTERS[seq[i]];
d.title = fmt('Position %i%%v%', { i: i, v: 1 });
d.addEventListener('click', ()=>{ seq[i]=(seq[i]+1)%K; render(); });
c.appendChild(d);
}
const sc = score(seq);
document.getElementById('fill').style.width = (100*sc/MAX)+'%';
const st = document.getElementById('status');
if (sc === MAX){ st.textContent=fmt('Score: %sc% / %MAX% — perfect fold! Every residue matches the target.', { sc: sc, MAX: MAX }); st.className='status win'; }
else { st.textContent=fmt('Score: %sc% / %MAX%', { sc: sc, MAX: MAX }); st.className='status'; }
}
// Greedy hill-climb: try every single-residue mutation, keep the best improving one.
function mutate(){
const cur = score(seq); let best = cur, bi=-1, bt=-1;
for(let i=0;i<N;i++) for(let t=0;t<K;t++){
if(t===seq[i]) continue;
const old=seq[i]; seq[i]=t; const s=score(seq); seq[i]=old;
if(s>best){ best=s; bi=i; bt=t; }
}
if(bi>=0){ seq[bi]=bt; render(); }
else { render(); document.getElementById('status').textContent=fmt('Score: %cur% / %MAX% — no single mutation improves it from here.', { cur: cur, MAX: MAX }); }
}
// Exhaustive search over all K^N sequences — guaranteed optimal, but K^N grows fast.
function search(){
let best=-1, arg=null, steps=0;
const total = Math.pow(K,N), cand=new Array(N).fill(0);
for(let m=0;m<total;m++){ steps++;
let x=m; for(let i=0;i<N;i++){ cand[i]=x%K; x=Math.floor(x/K); }
const s=score(cand); if(s>best){ best=s; arg=cand.slice(); }
}
seq=arg; render();
console.log('Exhaustive search checked',steps,'of',total,'sequences; best score; ',best);
const st=document.getElementById('status');
st.textContent=fmt('Score: %best% / %MAX% — optimum found after checking all %total% sequences.', { best: best, MAX: MAX, total: total });
st.className = best===MAX ? 'status win' : 'status';
}
document.getElementById('mutate').addEventListener('click', mutate);
document.getElementById('search').addEventListener('click', search);
document.getElementById('reset').addEventListener('click', ()=>{ seq=new Array(N).fill(0); render(); });
render();
<p class="hint">Each tile is a <b>residue</b> with a type (color). A hidden target wants a specific type at every position. Your <b>score</b> rewards each residue that matches the target, plus a small bonus where neighbors differ — a tiny stand-in for folding energy. Click a residue to cycle its type, or let the computer search.</p>
<div id="chain" class="chain"></div>
<div class="bar"><div id="fill" class="fill"></div></div>
<div class="status" id="status">Score: 0 / 0</div>
<div class="btns">
<button id="mutate" type="button">Mutate one residue (hill-climb)</button>
<button id="search" type="button">Exhaustive search</button>
<button id="reset" type="button" class="ghost">Reset</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; }
.chain { display: flex; gap: 6px; flex-wrap: wrap; margin: .5rem 0; }
.res { width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
font: 700 16px ui-monospace, monospace; color: #fff; border-radius: 50%;
cursor: pointer; user-select: none; transition: transform .08s; border: 2px solid rgba(0,0,0,.15); }
.res:hover { transform: scale(1.08); }
.res.t0 { background: #2a9d8f; }
.res.t1 { background: #e76f51; }
.res.t2 { background: #457b9d; }
.res.ok { box-shadow: 0 0 0 2px #0a7d33; }
.bar { height: 12px; background: #e8eef3; border-radius: 6px; overflow: hidden; margin: .4rem 0; }
.fill { height: 100%; width: 0; background: #2a9d8f; transition: width .2s; }
.status { font-size: 1rem; font-weight: 600; margin: .4rem 0; min-height: 1.4em; }
.status.win { color: #0a7d33; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Inverse folding, in miniature. A chain of residues, each of K types. A hidden
// TARGET assigns the ideal type per position. The "energy" we MAXIMIZE rewards
// matching the target (+MATCH) and adds a contrast bonus where adjacent residues differ (+DIFF) — a local pairwise score, the shape real design tools optimize. Scoring a chain is a single linear pass; finding the optimum is a search over K^n sequences. The target is the unique best sequence (verified by brute force).
const K = 3; // residue types (0,1,2)
const TARGET = [0,1,1,2,0,2,1,0]; // hidden ideal sequence
const N = TARGET.length;
const MATCH = 3, DIFF = 1;
const LETTERS = ['H','P','C']; // hydrophobic / polar / charged (flavor only)
function score(seq){
let t = 0;
for (let i=0;i<N;i++) if (seq[i] === TARGET[i]) t += MATCH;
for (let i=0;i<N-1;i++) if (seq[i] !== seq[i+1]) t += DIFF;
return t;
}
const MAX = score(TARGET); // best possible = the target itself
let seq = new Array(N).fill(0); // player's chain; start all type 0
function render(){
const c = document.getElementById('chain'); c.innerHTML='';
for(let i=0;i<N;i++){
const d = document.createElement('div');
const ok = seq[i] === TARGET[i];
d.className = 'res t'+seq[i]+(ok?' ok':'');
d.textContent = LETTERS[seq[i]];
d.title = fmt('Position %i%%v%', { i: i, v: 1 });
d.addEventListener('click', ()=>{ seq[i]=(seq[i]+1)%K; render(); });
c.appendChild(d);
}
const sc = score(seq);
document.getElementById('fill').style.width = (100*sc/MAX)+'%';
const st = document.getElementById('status');
if (sc === MAX){ st.textContent=fmt('Score: %sc% / %MAX% — perfect fold! Every residue matches the target.', { sc: sc, MAX: MAX }); st.className='status win'; }
else { st.textContent=fmt('Score: %sc% / %MAX%', { sc: sc, MAX: MAX }); st.className='status'; }
}
// Greedy hill-climb: try every single-residue mutation, keep the best improving one.
function mutate(){
const cur = score(seq); let best = cur, bi=-1, bt=-1;
for(let i=0;i<N;i++) for(let t=0;t<K;t++){
if(t===seq[i]) continue;
const old=seq[i]; seq[i]=t; const s=score(seq); seq[i]=old;
if(s>best){ best=s; bi=i; bt=t; }
}
if(bi>=0){ seq[bi]=bt; render(); }
else { render(); document.getElementById('status').textContent=fmt('Score: %cur% / %MAX% — no single mutation improves it from here.', { cur: cur, MAX: MAX }); }
}
// Exhaustive search over all K^N sequences — guaranteed optimal, but K^N grows fast.
function search(){
let best=-1, arg=null, steps=0;
const total = Math.pow(K,N), cand=new Array(N).fill(0);
for(let m=0;m<total;m++){ steps++;
let x=m; for(let i=0;i<N;i++){ cand[i]=x%K; x=Math.floor(x/K); }
const s=score(cand); if(s>best){ best=s; arg=cand.slice(); }
}
seq=arg; render();
console.log('Exhaustive search checked',steps,'of',total,'sequences; best score; ',best);
const st=document.getElementById('status');
st.textContent=fmt('Score: %best% / %MAX% — optimum found after checking all %total% sequences.', { best: best, MAX: MAX, total: total });
st.className = best===MAX ? 'status win' : 'status';
}
document.getElementById('mutate').addEventListener('click', mutate);
document.getElementById('search').addEventListener('click', search);
document.getElementById('reset').addEventListener('click', ()=>{ seq=new Array(N).fill(0); render(); });
render();
Notice the asymmetry. Scoring a candidate sequence is instant: walk the chain and add up how happy each residue is. Finding the sequence that maximizes the score is the hard part. "Mutate one residue and accept it if the score rises" is greedy hill-climbing — fast, and on this tiny chain it climbs straight to the answer; but on real, frustrated landscapes it gets stuck on local peaks. "Try every sequence" is exact but explodes: only a few types across a dozen positions is already millions of combinations, and real proteins have hundreds of positions and twenty types.
Comments
Loading comments...