structural-biology.com

ButtFold: A protein folds in your browser and the trajectory becomes music: PhoneFold’s web sibling

ButtFold

A protein folds in your browser and the trajectory becomes music.

live python flask gunicorn nginx sqlite WebAssembly three.js audio numpy pytest node:test sonifier parity data licence author

🌐 Appbuttfold.mdeller.com✉️ Contactmarc@marcdeller.com🐙 GitHubbellcheddar/ButtFold

Ubiquitin part way through folding: a magenta helix and cyan beta strands packing against a slate coil, with secondary-structure and radius-of-gyration traces beside the viewer

ButtFold takes a named protein, starts it as a self-avoiding random coil, and folds it with a C-alpha structure-based (Gō) model until it reaches its native structure. The trajectory is the instrument: contacts forming are note onsets, sequence separation sets register, hydropathy sets pitch, secondary structure sets texture (helix a sustained pad, sheet a staccato figure, coil an arpeggio), and compaction drives an accelerando. It is the web version of PhoneFold, and it plays the same piece: Sonifier.js is held to note-for-note agreement with the shipped Swift, 15,536 notes across two proteins and all five styles, zero differences.

Why it matters: most things that show a protein “folding” are showing you a structure prediction being refined, which is a picture of a computation rather than of a molecule, and the ones that sonify proteins almost always sonify the sequence, which is a lookup table set to music. This does neither. The physics is a real dynamical trajectory computed from scratch in your browser, and every note in the piece is caused by something the chain actually did, which makes the music a reading of the fold instead of a decoration on it. It is useful for: teaching what a folding funnel sounds like, showing a coarse-grained simulation to people who will never open a terminal, and hearing the difference between a protein that packs a hydrophobic core and one that never resolves.

What it is not. This is a funnelled coarse-grained simulation of a protein whose structure is already known, in the tradition of Clementi, Nymeyer and Onuchic (J. Mol. Biol. 298:937, 2000). It is not a structure prediction, it is not an unbiased physical force field, and no protein folds this way. The app says so on the page, next to the thing it is describing, rather than in an About box.

🧱 Three ways to get a trajectory, one player

Everything downstream of “a stream of CA frames” is shared. The three sources produce byte-identical frame objects, which is tested rather than asserted.

 What it isComputeWhen it runs
GallerySix folds precomputed on a Mac and served flatnoneAlways. First paint, no capability check, no wait
Fold it liveThe same C compiled to WebAssembly, in a Web Workeryour CPUThe primary interactive path where the browser can run it
On the serverThe same C compiled natively, one job at a timedroplet, boundedThe fallback, and anything too heavy for a phone

A fold the server computes is baked into the gallery’s own artefact format, so a queued result plays through the identical player with no code path of its own, and converges the cache: a protein folded once is served from disk forever after.

🎹 How a trajectory becomes music

The mapping is PhoneFold’s, row for row, because the point is that someone who plays with this and then downloads the app hears the same piece.

Trajectory featureMusical parameter
New contact eventNote onset; sequence separation sets the register
Long-range hydrophobic contactBass note (core packing)
Helix contentSustained pad, stacked fourths
Sheet contentStaccato interlocking figure
Coil contentArpeggiation between chord tones
Per-residue confidenceNote velocity for that residue
Mean confidenceLow-pass cutoff, detune, reverb
Radius of gyrationTempo and register
ConvergenceCadence, resolving to the tonic

“Confidence” here is the fraction of a residue’s own native contacts that have formed, which is real per-residue information: the hydrophobic core locks in first and completely while the termini are still loose, so a region that never resolves stays a detuned wash for the whole piece.

Five styles ship (Fantasy, Jazz, Pop, Rock, Surf) and are the app’s own JSON files, byte-identical. Each note is placed in space at its residue’s live 3D position through a PannerNode with HRTF, so the fold collapses around the listener.

🚀 Usage

Open https://buttfold.mdeller.com. Press Play. Drag the structure to turn it, scroll to zoom, double-click to reframe.

To run it locally:

git clone https://github.com/bellcheddar/ButtFold.git
cd ButtFold
python3 -m venv .venv
./.venv/bin/pip install -r requirements-dev.txt
./.venv/bin/python app.py                    # http://127.0.0.1:8007/
./.venv/bin/python -m buttfold.worker        # optional: the fold queue

requirements.txt is the web layer (Flask and gunicorn, no numpy) and requirements-queue.txt is the worker (numpy, for the bake). They are deliberately disjoint: the droplet runs both processes under separate systemd budgets so a fold can never starve the six other apps on the box.

🔧 Rebuilding the artefacts

Everything the app serves is committed. These regenerate it, and all of them run on the Mac.

CommandWhat it does
tools/extract_natives.pyVendors the native structures and starting coils from PhoneFold into data/natives/
tools/bake_gallery.pyFolds the six gallery proteins and bakes them, with the collapse assertions
tools/bake_gallery.py --frame-fixturesAlso writes the exact coordinates the JS parity test compares against
tools/build_wasm.shCompiles the Gō model to WebAssembly, emsdk pinned at 4.0.7
tools/swift_score_dump/run.shRuns PhoneFold’s own Sonifier over this gallery to produce the reference scores
tools/psea.py --fixtures, tools/contacts.py --fixturesReference outputs for the JS geometry ports
tools/check_all.shEvery machine-verifiable gate in one command

🧪 Testing: verify the artefact, not the intent

tools/check_all.sh runs seven gates. Four of them drive a real browser, because the failures that matter here are the ones where every unit test passes and the page does nothing.

GateWhat it proves
audit_wiring.pyEvery route, module, style, card and element id is reachable. Anything declared and never reached fails the build
pytest, 51 testsRoutes, caching, the queue’s caps and cache, the honesty strings, and the committed artefact’s own assertions
node --test, 26 testsThe WASM module against the CLI, the JS geometry ports against the Python, the sonifier against the Swift, and the camera’s interaction model
stage rendersA headless screenshot of the stage mid-fold is non-uniform, the three colour modes render differently, and the Play bar is above the fold at four common screen sizes
drag, zoom, reframeReal pointer input through the DevTools protocol reaches the camera
soundThe score reaches a running AudioContext from a real click, and the animation follows the audio clock
live fold, and the queueA browser folds trp-cage to Q >= 0.95, and the droplet returns a fold and then serves it from cache

Two assertions run at bake time and are repeated against the committed file, because they are the ones that catch a trajectory which is not doing the thing the animation is about: the chain must collapse (Rg_end / Rg_start <= 0.8) and it must not start folded (under 25% of contacts on the first frame). PhoneFold threw away a whole bake for want of them.

📊 Measured, not estimated

Full detail with machines and dates in METRICS.md. Nothing is quoted anywhere in this project until it is a measured row there.

  
trp-cage, folded live in Chrome11.2 s, final Q 1.000, 152 frames streamed
the same fold, module alone, no frame building7.4 s (1.4x the native binary)
trp-cage, folded on the droplet25.7 s under its CPU quota (16.6 s unconstrained)
ubiquitin, on the droplet7 min 07 s, which sets the 76-residue cap
WASM against native, forcesagree to 2.9e-15 relative, against a 1e-9 bar
Sonifier.js against the shipped Swift15,536 notes, 0 differences
Live path against baked path300 frames, 5 fields each, byte-identical
Baked gallery payload842 kB for six folds and 900 frames

One number that changed a plan: the parity bar was written as “RMSD within 0.1 Å of the native build”. Measured, the same build folding the same coil lands anywhere from 0.84 to 1.16 Å depending only on the random-force seed, so that bar is narrower than the measurement’s own noise and no correct implementation can meet it. Q over the same five seeds was 0.993 every time, so Q carries the weight and the RMSD bar is taken from the measured spread.

✅ To Do

Roadmap for ButtFold, roughly in dependency order. Suggestions welcome.

  • ✅ Measure before building. Droplet fold times, WASM speed in three browsers, WASM parity against native, and the baked payload size, all in METRICS.md before a line of the app existed. Two of the plan’s own numbers had to change as a result
  • ✅ The baked gallery. Six Gō folds with the collapse and first-frame-contact assertions green, served by Flask with explicit Cache-Control because Flask sends none on a template and heuristic caching then makes a deploy invisible
  • ✅ The 3D stage. three.js tube along the CA trace, coloured by secondary structure, with live readouts and two time series. A degenerate initial spline gave the tube a NaN bounding sphere and three.js frustum-culled it forever behind a page that otherwise looked perfect
  • ✅ Note-for-note sonifier parity with the shipped app. Float32 is emulated with Math.fround wherever the Swift uses Float: mean confidence accumulates over 76 residues in single precision on the phone, and the difference reaches note velocity and the cadence
  • ✅ The Web Audio engine. All five style profiles unchanged, per-note HRTF panning, a lookahead scheduler, and the audio clock driving the animation rather than the reverse
  • ✅ The live WebAssembly fold. wasm_api.c is additive: it #includes the vendored C rather than editing it, and reproduces the CLI’s trajectory bitwise
  • ✅ The droplet queue. One worker at nice 19 behind a residue cap, a depth cap, a per-IP cap and a timeout of three times the measured worst case, with a cache that converges
  • ✅ The honesty layer. The app’s own disclosure strings byte for byte, an engine badge that never scrolls away, and a disclosure paragraph as body text rather than behind a link
  • ✅ PhoneFold’s camera. Ported after drag-to-rotate proved broken the same way the app’s once was: yaw and pitch with a clamp, which makes a vertical drag die mid-gesture. A quaternion attitude on the subject has no pole to protect
  • ⬜ Cartoon secondary structure. The ribbon is a plain tube, so a helix reads as a coiled tube and a strand as a straight one. The app draws a flat helical ribbon and an arrowed strand, and this is the biggest remaining visual gap
  • ⬜ Genie 2 gallery entries. Two to four backbones precomputed on the Mac, carrying the generative disclosure on both the card and the stage. Genie 2 needs Core ML or a GPU, so these can only ever be baked, never folded live
  • ⬜ Mobile Safari’s fold time. The one measurement no machine here can take, and the one that decides whether phones fold live at all or route to the queue
  • ⬜ The PhoneFold store link. static/links.json carries a null app_store_url, so the card honestly says “in review”. One field flips it when the app clears review

📚 Provenance and licences

The Gō model, the five style profiles, the native structures and the geometry and audio ports all come from PhoneFold (MIT), vendored with provenance headers naming the source commit and re-vendored rather than patched. The C is unedited. THIRD-PARTY.md lists everything, with the papers behind the model, the secondary-structure assignment, the hydropathy scale and both radius-of-gyration scaling laws.

ButtFold’s own code is MIT: see LICENSE.

The name is a joke about an accidental butt-dial. The page says so once, in the footer, and never again.


👤 Author

Marc C. Deller, D.Phil.
Structural biologist & drug discovery scientist

🌐marcdeller.com✉️marc@marcdeller.com🐙github.com/bellcheddar/ButtFold