Teaching a biological neural system to compose music
using only methods from a Numerical Analysis course
Vahid Ghayoomie · wormuse project · 2026
From a hammer strike to a symphony — the physics of music
When you press a piano key:
Low, bass register. String vibrates 110 times per second.
The universal tuning reference. 440 vibrations/second.
Very high. Each octave up = ×2 in frequency.
Frequency formula: \( f_n = n \cdot f_1 \cdot \sqrt{1 + B n^2} \)
where \(f_1\) = fundamental, \(n\) = harmonic number, \(B\) = inharmonicity (bending stiffness)
The \(B n^2\) term makes real piano strings slightly sharp at high harmonics — giving the piano its characteristic bright, rich timbre. This is the inharmonicity we model with modal synthesis in PyANNOW.
The string's motion \(u(x,t)\) obeys a 4th-order PDE (Chabassier 2014):
We skip the FEM solver and use modal synthesis instead:
Sum 40 decaying sinusoids at their inharmonic frequencies.
0.03 s per note (vs minutes for FEM). Same physics, different math.
From single cells to intelligence — how nature computes
How a neuron "decides":
The entire nervous system. Every connection is mapped. Capable of: feeding, moving, reproducing, learning.
Complete brain recently mapped (2023). Capable of: flight, complex navigation, courtship.
With ~100 trillion synaptic connections. Capable of: language, music, mathematics, consciousness.
Key insight: complexity of behaviour does not simply scale with neuron count. The worm's 302 neurons produce sophisticated movement patterns — and with the right mathematical tools, we can translate those patterns into music.
We record the worm's neural activity (biological), compress it with SVD (L06), and feed it into an artificial neural network (L16) trained with Adam (L20) to produce notes. Both worlds in one pipeline.
The simplest nervous system on Earth — and the first to be simulated
Why C. elegans for music?
The travelling body wave activates muscle segments sequentially from head to tail at ~0.4–2 Hz. In PyANNOW, each contraction = one piano note at a phase-specific pitch.
OpenWorm (2011–present) is an open-source project to recreate every cell of C. elegans in software:
The Docker image openworm/openworm:latest (already on your machine) contains both Sibernetic and C302. The docker-compose.yml in the repo orchestrates them alongside the MK scientific toolchain.
Sibernetic outputs WCON (Worm Configuration Object Notation) for body pose. C302 outputs spike trains in JSON. PyANNOW's Step 1 compresses these into a low-dimensional latent code.
Numerical Analysis for Machine Learning — our mathematical instruments
The PyANNOW progression uses one new NAML concept per step — a direct mapping from the course to a musical improvement.
Every constraint in the worm biology has a structural counterpart in the piano and in the NAML algorithm.
Full 20-row table: docs/EQUIVALENCE_TABLE.md
| C. elegans biology | Piano physics | NAML / PyANNOW | Lecture |
|---|---|---|---|
| 302 neurons → 4 motor modes | ∞ modes → 40 dominant modes | Rank-k SVD X_k = U_k Σ_k V_k^T | L06 |
| EGL-19 Ca²⁺ threshold V_half_Ca | Hammer contact δ > 0 gate | tanh activation; Ca_THRESH |
L15 |
| τ_Ca activation speed | Hammer contact duration ~1-4 ms | τ_Ca in PINN; most sensitive param (§7) | L27 |
| EXP-2 K⁺ repolarisation | String damping σ₀ e^{-σ₀t} | g_EXP2 in params; sets note sustain | L27 |
| Locomotion frequency ~0.4 Hz | Tempo (BPM) | drive_freq_hz; master clock | L18 GD |
| Bending wave propagation | Inharmonicity B (string stiffness) | 1D conv kernel; temporal spread | L23 |
| Body ODE: ÿ+2γẏ+ω²y=F | String PDE: ρü−Tu_xx+σ₀u̇=F | PINN residual loss (ODE + PDE) | L27 |
| 95 BWM cells (4 quadrants) | 88 keys (4 registers) | out_dim=95 MLP + pitch_map | L16 |
Teaching the worm to play Chopin — step by step
In Lab01 we compressed the Tarantula Nebula image with SVD. Here we compress the worm's neural activity. Same maths — different data.
# Step 1: RSVD + Procrustes (from Lab01 + L06/L09) — v1.0.0
from pyannow.step1_svd.encoder import rsvd, neural_scores
from pyannow.step1_svd.procrustes import procrustes_align, build_chopin_features
# Compress 302-D neural trajectory to k=4
U_k, s_k, Vt_k = rsvd(X_neural, k=4)
Z_worm = neural_scores(X_neural, U_k) # (T, 4)
# Auto-select k_chopin at 90% variance (ISSUE-034 fix)
C_chopin = build_chopin_features(events, duration_s=15, k_chopin=None)
# Standardize W_k before SVD (ISSUE-032 fix: PC1/PC4 ratio was 6.9x)
result = procrustes_align(Z_worm, C_chopin, standardize=True)
R = result['R'] # (4, k_chopin)
Z_aligned = result['W_k_scaled'] @ R # worm in Chopin's space
# v1.0.0: residual 89.807 -> 4.059
print(f"Alignment residual: {result['residual']:.3f}")
print(f"Scale factors: {result['scale']}")
Ridge shrinks each singular direction by σ/(σ²+λ). Small σ = noisy neural mode = suppressed. Exactly like the pseudoinverse truncation in L09.
# Step 2: K-means (Lab02 / L08 / L10 pattern)
from pyannow.step2_clustering.motor_primitives import (
pca_reduce, find_motor_primitives, cluster_to_notes
)
scores, pca = pca_reduce(X_neural, n_components=4)
labels, km = find_motor_primitives(scores, k=4)
# Each cluster transition → note onset
note_events = cluster_to_notes(labels, t_arr_ms)
# Step 3: Ridge (Lab07 / L07 / L11 pattern)
from pyannow.step3_regression.ridge_composer import RidgeComposer
ridge = RidgeComposer(alpha=None) # CV chooses λ
ridge.fit(Z_worm, C_chopin)
C_pred = ridge.predict(Z_worm)
eval_res = ridge.evaluate(Z_test, C_test)
print(f"Ridge R² = {eval_res['r2']:.3f}")
jax.grad (L14)
# Step 4-6: MLP + Adam + L-BFGS (Lab06/Lab10 pattern)
from pyannow.step4_ffnn.jax_composer import (
create_model, init_params)
from pyannow.step5_training.adam_trainer import train_adam
from pyannow.step6_lbfgs.lbfgs_polish import polish_lbfgs
# Build MLP (same architecture as Lab10 MNIST)
model = create_model(k_worm=4, k_chopin=8, hidden=32)
params = init_params(model, k_worm=4)
# Stage 1: Adam warmup (Lab10 / L20)
params, h_adam = train_adam(
model, params, Z_worm, C_chopin,
lr=1e-3, epochs=500, verbose=True
)
# Stage 2: L-BFGS polish (Lab08 / L22)
params, h_lbfgs = polish_lbfgs(
model, params, Z_worm, C_chopin,
max_steps=100
)
print(f"Final loss: {h_lbfgs.train_loss[-1]:.5f}")
Physics (ODE): each muscle group is a point oscillator
Residual = q̈ + 2γq̇ + ω²q − F via jax.jacfwd twice. Random collocation in time only. Exact recipe from L27 slides.
Fast (~30s training). Easy to understand. Baseline for comparison.
Each muscle is independent. No spatial coupling — the body wave is not captured.
# Step 8a: ODE PINN (L14 + L27)
from pyannow.step8_pinn.locomotion_pinn import (
PhysicsComposer, ode_physics_loss, train_pinn
)
import jax, jax.numpy as jnp
model_ode = PhysicsComposer(hidden=32, depth=2, out_dim=8)
params_ode = model_ode.init(
jax.random.PRNGKey(0), jnp.zeros((1, 1+k_worm)))
# Collocation in TIME only (ODE)
t_colloc = jnp.linspace(0, 15, 200)
z_colloc = Z_worm[::T//200]
params_ode, hist_ode = train_pinn(
model_ode, params_ode,
inp_ode, # (T, 1+k_worm) ← time + neural code
C_chopin,
phys_loss_fn = ode_physics_loss,
phys_args = (t_colloc, z_colloc),
lam_phys = 0.1,
adam_steps=500, lbfgs_steps=50,
label="ODE"
)
Physics (PDE): the worm body as a 1D elastic rod
Piano (§B.2): ρ_s ü − T_s u_xx + ESκ² u_xxxx = F_hammer
Worm (§A.6): ρ q_tt − μ q_xx + γ q_t = F_neural
The same PDE family drives both!
This is the Structural Correspondence 2 from SCIENTIFIC_FOUNDATION.md §C.2.
# Step 8b: PDE PINN — 1D wave eq (L14 + L27)
from pyannow.step8_pinn.locomotion_pinn import (
PhysicsComposer, pde_physics_loss,
compare_ode_vs_pde
)
# Collocation in SPACE × TIME (PDE)
x_colloc = jnp.linspace(0, 1, 200)
t_colloc = jnp.linspace(0, 15, 200)
# One call runs BOTH and compares
results = compare_ode_vs_pde(
Z_worm, C_chopin, t_arr,
lam_phys=0.1,
adam_steps=500, lbfgs_steps=50
)
print(f"ODE loss: {results['final_loss_ode']:.5f}")
print(f"PDE loss: {results['final_loss_pde']:.5f}")
# PDE is typically better — it knows the wave propagates!
Captures the spatial travelling wave. Adjacent segments are coupled → smoother melodies. Closer to real worm mechanics.
| Criterion | ODE | PDE |
|---|---|---|
| Physics dimension | time only | x + time |
| Collocation space | 1D | 2D grid |
| Captures body wave? | ✗ | ✓ |
| Piano symmetry? | partial | full (§C.2) |
| Training time | ~30s | ~90s |
| Expected final loss | lower bound | slightly lower |
The only difference: collocation points are 1D (t) for ODE and 2D (x,t) for PDE. The loss, optimizer, and autodiff are identical.
The loss decomposition tells the story:
ODE physics loss: \(\|\ddot q + 2\gamma\dot q + \omega^2 q - F\|^2\) sampled at random times
PDE physics loss: \(\|\rho q_{tt} - \mu q_{xx} + \gamma q_t - F\|^2\) sampled at random (x, t) pairs
For real-time applications (ODE is 3× faster) or when spatial resolution is irrelevant (e.g., if you only care about total energy, not per-segment dynamics).
RF discovers non-linear interactions between worm motor modes and Chopin's timing. Ridge/MLP (Steps 3-6) lacked temporal structure — no time embedding, so they couldn't capture rhythmic patterns.
# Step 7: Random Forest (AppStat Lec07)
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler
# Features: worm PCA (k) + beat position
X_rf = np.hstack([Z_worm_10s, beat_phase.reshape(-1,1)])
y_rf = onset_labels_10s # binary onset labels
rf = RandomForestClassifier(
n_estimators=100, max_depth=12,
oob_score=True, random_state=0, n_jobs=-1)
rf.fit(X_rf_s, y_rf)
probs_rf = rf.predict_proba(X_rf_s)[:,1]
onsets_rf = calibrated_onset_detect(probs_rf, ...)
F1_7 = musical_f1(onsets_rf, t_on_target)
print(f"Step 7 RF: F1={F1_7['f1']:.6f}")
print(f" OOB score: {rf.oob_score_:.3f}")
# Feature importance: worm PC1 > beat_phase > PC2 …
# Step 9: Worm+Time MLP (NB04 recipe + AppStat MLP)
from sklearn.neural_network import MLPClassifier
def fourier_feats(t, T, n_harmonics=12):
phase = t / T
cols = [phase]
for k in range(1, n_harmonics+1):
cols += [np.cos(2*np.pi*k*phase),
np.sin(2*np.pi*k*phase)]
return np.column_stack(cols) # (T, 25-D)
T_feats = fourier_feats(t_arr, T=DURATION) # 25-D
X9 = np.hstack([T_feats, Z_worm_scaled]) # 25+k
mlp9 = MLPClassifier(
hidden_layer_sizes=(128,64), activation='relu',
max_iter=300, early_stopping=True, random_state=0)
mlp9.fit(X9_sub, y9_sub, sample_weight=sw9)
F1_9 = musical_f1(onsets9, t_on_target)
# F1 = 0.933 (NB04 time-only ceiling: 0.858)
# Step 9b adds ODE residual → F1 ≈ 0.933 (tied)
NB04 (04_chopin_score_net.ipynb) trains a Fourier time-net on the full piece: F1=0.858. Adding worm biology pushes to 0.933 — 8.7pp above the pure-time ceiling. The worm's body wave carries real musical timing signal.
Problem with Steps 9/9b: trained on 10s window, features extrapolate beyond training range for the full 229s piece → performance degrades.
worm_mlp_full.wav · chopin_synth_full.wav
Both ≈229s — compare in Part VII ↓
# Step 10: NB04+Step9b full 229s piece — v1.0.0
t_on_all = note_onsets(events_chopin, clip_s=10000)
T_full_s = float(t_on_all[-1] + 2.0) # ≈ 229s
# Run full Boyle model simulation
result_s10 = run_worm_simulation(T_full_s)
Z10 = result_s10['neural_pca'] # (T_pts, k_worm)
# Fourier features normalised over FULL piece
# → t/T_full ∈ [0,1] everywhere, never extrapolates
T_feats_full = fourier_feats(t_full, T=T_full_s)
# Stack: Fourier + worm PCA + ODE residual
X10 = np.hstack([T_feats_full, Z10, ode_res_full])
mlp10.fit(X10_sub, y10_sub, sample_weight=sw10)
# Synthesise and save full-length WAVs
audio_worm, fs = synthesise_melody(events10_full,
duration_s=T_full_s)
wavfile.write('worm_mlp_full.wav', fs,
(audio_worm * 28000).astype('int16'))
How far can a worm with 302 neurons go toward Chopin?
musical_f1 — onset matched within ±50 ms. Step 0 floor = 0.217. NB04 ceiling (pure time): F1=0.858. Step 9 (worm+time): F1=0.933 — adding C. elegans biology to time features gives +8.7pp above NB04 ceiling, proving the worm carries genuine musical timing signal.
| Constraint | Biological source | Musical limit |
|---|---|---|
| 8 voices | 8 BWM muscle segments | Max 8 simultaneous notes (Chopin uses more) |
| ~100% rate-reachable | 8–95 voices × 65 ms refractory (EGL-19 + EXP-2) | All Chopin notes achievable by rate — real limit is timing |
| Regular rhythm | Body wave is periodic (locomotion circuit oscillator) | Worm plays a regular grid; Chopin is syncopated |
| 96 pitches ✅ | Boyle 4×24 model → MIDI 24-119, all 12 pitch classes (v0.7.0+) | 100% pitch coverage — every Chopin note is reachable |
What NAML CAN improve: note density (SVD threshold), velocity/dynamics (ridge/MLP), temporal clustering (K-means), attack/release shaping (PINN).
Full 229s side-by-side — Step 10 NB04+Step9b model vs Chopin synthesised
96-cell Boyle model · 229s full-piece training · MLP(128,64) · onset detection → piano synthesis
step_outputs/worm_mlp_full.wav · ~229 s
Fourier(27-D) + worm PCA + ODE residual · 30k training pts
Original Chopin MIDI → same piano string model (modal synthesis, 40 modes) → 229 s reference
step_outputs/chopin_synth_full.wav · ~229 s · 1000 notes
Nocturne in C# minor Op. posth. · MIDI public domain
Note (v1.0.0): Audio hosted on
GitHub Releases v1.0.0
· or run Step 10 in 03_pyannow_naml_progression.ipynb to generate locally.
Worm uses 96 pitches (MIDI 24–119, all 12 pitch classes · Boyle 4×24 model).
Remaining bottleneck: periodic body wave vs Chopin's expressive syncopation.
Two course skills that extend PyANNOW toward production-grade biology
The gap today: PyANNOW uses synthetic neural activity (repeated muscle voltages). The real loop requires the actual Sibernetic SPH body simulator and C302 neural network running in a tight optimization cycle.
unique_ptr<WormSimulator> + shared-library C-ABI binding from PythonMPI_Allreducenmpde-projects/container-config.yamlThe current learning loop (678s for 10s synthetic worm) becomes 10-60s per iteration on the Azure GPU, enabling real parameter search over the ion-channel space.
AMSC lectures directly applied: L03 (C++17), L05 (RAII subprocess), L09 (shared library), L11 (MPI reduce), L12 (OpenMP piano FEM), PC L5 (CUDA port of SPH).
The gap today: the PINN predicts (m_∞, τ_m, h_∞, τ_h) that minimise the PINN loss — but there is no statistical guarantee these predictions are biologically plausible relative to measured C. elegans channel values.
(g_EGL19, V_half, τ_Ca, g_EXP2) → find the "musical cluster" where Chopin onset loss is lowUse the AppStat regression as a prior over ion-channel parameters. Add a Mahalanobis distance penalty to the PINN loss:
PINN predictions become statistically validated: "this parameter set lies within the 95% CI of biologically measured EGL-19 conductances in C. elegans."
The user's existing work in AppStat/project/ (bootstrap CI, PCA scree plot, OpenWorm research proposals) is the direct starting point.
NAML provides the learning algorithm. AMSC provides the simulation engine and HPC backbone. AppStat provides the statistical validation. Each alone is insufficient; together they form a scientifically rigorous, computationally feasible, and biologically grounded pipeline.
The NAML tools improved the music:
The biology sets the ceiling:
With 8–95 independent voices and 65 ms refractory, the worm can match all of Chopin's notes by rate (~100% ceiling). The irreducible constraint is the periodic travelling wave — Chopin's timing is syncopated; the worm's is not.
A 1 mm worm with 302 neurons, simulated in software, can — with the right mathematical tools from a university course — approximate a Chopin piano piece. Nature's simplest brain can be a composer.
• MIDI mislabeled Raindrop → Nocturne No. 20 (ISSUE-001)
• Ceiling formula single-voice artefact 57.7% → ~100% (ISSUE-002)
• 95-cell model 38 notes/s too dense → selective gating (ISSUE-005)
• JAX not PyTorch · NAML course stack only · HPC shell config
• Could not download correct MIDI → Vahid sourced the file
• PDE Jacobian shape bug → caught during execution
• pca_reduce transpose bug → caught by the test suite
• FDM instability for high strings → replaced with modal synthesis
• Single-voice ceiling bug → caught by Vahid (ISSUE-002)
Nelder-Mead showed near-zero improvement on ion-channel parameters — not an AI failure, but a real biological result: timing is structural (body-wave), not parametric. Vahid confirmed this interpretation.
Full breakdown: AI_CONTRIBUTIONS.md
Prof. Miglio & NAML 2025-26 faculty — SVD → MLP → PINN arc that drives every step of PyANNOW (L06 Eckart-Young, L14-17 NNs, L20 Adam, L22 L-BFGS, L27 PINNs).
Prof. Formaggia (AMSC) — C++ + HPC + MPI backbone for the Sibernetic integration.
Profs. Beraha & Andre (AppStat 2026) — Random Forest, MLP classification, statistical validation of the ML pipeline.
Questions & comments
github.com/VahidGh/wormuse
PyANNOW v1.0.0 · module + notebook
PyANNOW/notebooks/
03_pyannow_naml_progression.ipynb
PyANNOW/docs/
PyANNOW_NAML_progression.md
Built with: Python · JAX · Flax · Optax · NumPy · scikit-learn · Reveal.js
Physics: Chabassier (2014) piano model · OpenWorm Sibernetic + C302
Music: Chopin Nocturne in C# minor Op. posth. · MIDI public domain
Thanks: Prof. Miglio (NAML) · Prof. Formaggia (AMSC) · Profs. Beraha & Andre (AppStat 2026)