hv-chemical-bubble
Keller-Miksis bubble reservoir computing with a parity certificate — a 16-function capability test that classifies any reservoir substrate as linear, single-threshold-nonlinear, or multi-layer-nonlinear. NumPy only.
Size: ~26 KB source, no weights. Runtime: ~30 s for the full benchmark (multi-bubble batteries). Dependencies: NumPy only.
The finding
The SKILLS archive makes a specific quantitative prediction: a capability battery of 16 boolean functions of 2 inputs, scored by a linear readout, classifies any reservoir substrate into three regimes:
| fitness | capability class |
|---|---|
| 16/16 | multi-layer nonlinear |
| 12–14/16 | single threshold layer |
| ≤8/16 | linear |
The bubble reservoir reproduces all three regimes exactly.
| config | fitness | class |
|---|---|---|
| 1 bubble, 0.15 atm drive | 8/16 | linear |
| 1 bubble, 1.0 atm drive | 14/16 | single threshold layer |
| 3+ bubbles, 1.0 atm drive | 16/16 | multi-layer nonlinear |
The parity certificate is the contribution. The bubble is the demonstration.
The primitive
A gas bubble in water is a nonlinear oscillator. Its radial dynamics follow the Keller-Miksis equation:
(1 − Ṙ/c) R R̈ + (3/2)(1 − Ṙ/(3c)) Ṙ²
= (1 + Ṙ/c)(p_B − p_∞)/ρ + (R/ρc) dp_B/dt
where R is radius, c the speed of sound, p_B the internal pressure, p_∞ the acoustic drive, ρ the liquid density.
The state (R(t), Ṙ(t)) is a rich response to the drive. That makes the bubble a reservoir — its trajectory is the feature space, and a linear readout on top of it computes nonlinear functions of the input.
The parity certificate
The test:
- Encode boolean inputs as acoustic drive patterns. Bit 1 → high amplitude, bit 0 → low amplitude, both at the Minnaert frequency.
- Run the bubble reservoir for each of the 2^(2^n) boolean functions.
- Fit a linear readout on the reservoir's final state.
- Fitness = fraction of functions solved with accuracy ≥ 0.8.
The classification thresholds are structural:
- 8/16 — the number of linearly separable boolean functions of 2 inputs. A linear reservoir cannot exceed this.
- 14/16 — the signature of a single threshold layer.
- 16/16 — full nonlinearity (multi-layer capability).
The three diagnostics
Parity certificate. Classifies computational capability. Runs in 1.6 s per bubble for the 16-function battery.
Return-map Lyapunov exponent. Samples R once per drive period, fits nearest-neighbor divergence on the return map. Robust during collapse events where tangent propagation would blow up.
Results at three drive conditions:
| frequency | amp | LLE | regime |
|---|---|---|---|
| 0.5× Minnaert | 0.5 | −0.14 | periodic |
| 0.5× Minnaert | 1.5 | −0.01 | near-critical |
| 1.0× Minnaert | 0.5 | +0.13 | chaotic |
| 2.0× Minnaert | 1.0 | +0.21 | chaotic |
At 0.5× Minnaert the bubble is periodic. Above Minnaert it enters chaos. The transition happens near the Minnaert frequency itself.
Shared normalization check. Reproduces the fake "+10 dB" result from the SKILLS archive. Independent normalization of input and target inflates R². The test verifies that shared normalization is at least as good as independent.
Headline numbers
| diagnostic | result |
|---|---|
| Free oscillation | R/R0 ∈ [0.98, 1.02] |
| Driven at 1.0 atm | R/R0 ∈ [0.69, 1.28] |
| Minnaert frequency (R0 = 1 μm) | 3.28 MHz |
| Minnaert freq scaling | exactly ∝ 1/R0 |
| LLE chaotic threshold | near Minnaert frequency |
| Multi-bubble channel count | 3 channels for 3 bubbles |
| 13/13 consistency checks pass |
How to use
from hv_chemical_bubble import (
BubbleParams, KellerMiksis, BubbleReservoir,
capability_battery, run_single_task,
return_map_lle, shared_normalization_check,
interpret_fitness, make_drive_signal,
)
import math
# Single bubble simulation
params = BubbleParams()
bubble = KellerMiksis(params)
freq = params.minnaert_freq()
drive = lambda t: params.p_inf * (1.0 + 1.0 * math.sin(2 * math.pi * freq * t))
result = bubble.simulate(drive, n_steps=5000, dt=1e-9)
print(result['R_over_R0'].min(), result['R_over_R0'].max())
# Parity certificate
battery = capability_battery(n_inputs=2, n_steps_per_bit=500,
drive_amp=1.0, n_bubbles=5)
print(battery['fitness']) # 1.0
print(interpret_fitness(battery['fitness']))
# 'multi-layer nonlinear (16/16)'
# Return-map Lyapunov exponent
lle = return_map_lle(drive_freq=freq, drive_amp=1.0,
n_periods=300, steps_per_period=150)
print(lle['lle']) # ~+0.06
# Shared normalization check
sn = shared_normalization_check()
print(sn['r2_independent'], sn['r2_shared'])
- Downloads last month
- 21