|
1 | 1 | # wave-conservation |
2 | 2 |
|
3 | | -**Spectral wave propagation on graphs — wave speed = √λ₂, standing waves reveal the eigenvalue spectrum, conservation ratio predicts coherence.** |
| 3 | +**Spectral wave propagation on graphs — wave speed = √λ₂, standing waves reveal the eigenvalue spectrum, and the conservation ratio predicts how long coherence survives.** |
4 | 4 |
|
5 | | -Pure Rust implementation of the discrete wave equation on graph structures. Propagates waves through path, cycle, complete, star, and barbell graphs, verifying that graph spectral properties (eigenvalues, Fiedler vector) directly govern wave behavior. |
| 5 | +> *Send a wave through a graph. Watch how fast it travels, how long it stays coherent, where it resonates. The wave doesn't know about eigenvalues — but it computes them anyway. The spectrum hides in the wave. You just have to know where to look.* |
6 | 6 |
|
7 | | -## What This Gives You |
| 7 | +--- |
8 | 8 |
|
9 | | -- **Wave speed verification** — confirms that wave speed on graphs equals √λ₂ (Fiedler eigenvalue) |
10 | | -- **Standing wave detection** — driving at eigenfrequencies produces resonance peaks |
11 | | -- **CR vs coherence** — maps conservation ratio to wave coherence halflife |
12 | | -- **Frequency sweep** — recovers the full eigenvalue spectrum from wave response alone |
13 | | -- **Fiedler reflection** — waves reflected at the algebraic connectivity boundary |
14 | | -- **Wave interference** — constructive/destructive patterns on graph structures |
15 | | -- **Zero dependencies** — pure Rust, no external math crates |
| 9 | +## The Problem |
| 10 | + |
| 11 | +Graphs have spectral structure encoded in their Laplacian eigenvalues. But eigenvalues are abstract — you compute them with linear algebra, not physics. What if you could *measure* the spectrum by sending waves through the graph and watching what happens? |
| 12 | + |
| 13 | +It turns out you can. The wave equation on a graph `d²u/dt² = -L·u` (where L is the graph Laplacian) produces wave behavior that is *governed entirely by the eigenvalue spectrum*. The wave speed equals √λ₂ (the Fiedler eigenvalue). The resonance frequencies are √λᵢ for each eigenvalue. And the conservation ratio λ₂/λₙ predicts how long wave coherence survives. |
| 14 | + |
| 15 | +This crate implements the discrete wave equation on graphs and provides experiments that verify these spectral-wave correspondences. No external math dependencies — everything from eigenvalue computation to wave simulation is pure Rust. |
| 16 | + |
| 17 | +## The Key Insight |
| 18 | + |
| 19 | +**The wave equation on a graph IS a spectral probe. You don't need to compute eigenvalues separately — just send a wave and read off the spectrum from the response.** |
| 20 | + |
| 21 | +Here's why. The Laplacian L has eigenvectors φ₁, φ₂, ..., φₙ with eigenvalues 0 = λ₁ < λ₂ ≤ ... ≤ λₙ. Any displacement u can be written as a superposition of eigenvectors: u = Σ cᵢφᵢ. Under the wave equation, each mode oscillates independently at frequency √λᵢ. So: |
| 22 | + |
| 23 | +1. **Wave speed** — The lowest non-trivial mode (Fiedler) travels at speed √λ₂. Measure how fast a pulse crosses the graph, and you've measured the Fiedler eigenvalue. |
| 24 | + |
| 25 | +2. **Standing waves** — Drive the graph at frequency ω = √λᵢ, and mode i resonates. A frequency sweep reveals all eigenvalues as peaks. |
| 26 | + |
| 27 | +3. **Coherence** — The conservation ratio CR = λ₂/λₙ bounds how long waves stay coherent. High CR (well-connected graph) → long coherence. Low CR (path graph) → fast decoherence. |
| 28 | + |
| 29 | +4. **Fiedler reflection** — The Fiedler vector partitions the graph into two communities. Waves launched from one side reflect at the boundary, like light at an interface. |
| 30 | + |
| 31 | +The velocity Verlet integrator ensures symplectic energy conservation — the simulation respects the physics, not just the numerics. |
| 32 | + |
| 33 | +## Architecture |
| 34 | + |
| 35 | +``` |
| 36 | + ┌──────────────────────────────────────────────────────┐ |
| 37 | + │ WaveState │ |
| 38 | + │ d²u/dt² = -L·u - γ·du/dt │ |
| 39 | + │ │ |
| 40 | + │ displacement[N] ──▶ velocity Verlet ──▶ step(dt) │ |
| 41 | + │ velocity[N] integrator │ |
| 42 | + │ adjacency[N×N] │ │ |
| 43 | + │ damping γ │ │ |
| 44 | + └──────────┬───────────────────┼──────────────────────┘ |
| 45 | + │ │ |
| 46 | + ▼ ▼ |
| 47 | + ┌──────────────────┐ ┌─────────────────────────┐ |
| 48 | + │ spectral │ │ experiments │ |
| 49 | + │ │ │ │ |
| 50 | + │ eigenvalues() │ │ verify_wave_speed() │ |
| 51 | + │ fiedler_vector() │ │ cr_vs_coherence() │ |
| 52 | + │ conservation_ │ │ standing_waves() │ |
| 53 | + │ ratio() │ │ fiedler_reflection() │ |
| 54 | + │ resonance_ │ │ interference_pattern() │ |
| 55 | + │ frequencies() │ │ │ |
| 56 | + │ frequency_ │ │ Graph generators: │ |
| 57 | + │ sweep() │ │ path, cycle, complete, │ |
| 58 | + │ find_peaks() │ │ star, barbell │ |
| 59 | + └──────────────────┘ └─────────────────────────┘ |
| 60 | +
|
| 61 | + Flow: adjacency → WaveState → simulate → measure speed/coherence/energy |
| 62 | + ↓ |
| 63 | + spectral → eigenvalues → predict speed, find resonances |
| 64 | + ↓ |
| 65 | + compare predicted vs measured → conservation verified |
| 66 | +``` |
16 | 67 |
|
17 | 68 | ## Quick Start |
18 | 69 |
|
19 | 70 | ```rust |
20 | 71 | use wave_conservation::wave::WaveState; |
21 | 72 | use wave_conservation::spectral; |
| 73 | +use wave_conservation::experiments; |
| 74 | + |
| 75 | +// Build a path graph |
| 76 | +let adj = experiments::path_graph(20); |
| 77 | + |
| 78 | +// Compute eigenvalues (power iteration, no external deps) |
| 79 | +let eigs = spectral::eigenvalues(&adj); |
| 80 | +println!("λ₂ = {:.4}", eigs[1]); // Fiedler eigenvalue |
| 81 | +println!("Wave speed = √λ₂ = {:.4}", eigs[1].sqrt()); |
22 | 82 |
|
23 | | -let adj = vec![vec![/* adjacency matrix */]]; |
24 | | -let mut wave = WaveState::new(adj).with_damping(0.01); |
25 | | -wave.pulse(0, 1.0); // inject at node 0 |
| 83 | +// Launch a wave |
| 84 | +let mut wave = WaveState::new(adj).with_damping(0.001); |
| 85 | +wave.pulse(0, 1.0); |
| 86 | +let e0 = wave.energy(); |
26 | 87 |
|
27 | 88 | for _ in 0..1000 { |
28 | 89 | wave.step(0.01); |
29 | 90 | } |
| 91 | + |
| 92 | +println!("Energy conservation: {:.4}", wave.conservation_ratio(e0)); |
30 | 93 | ``` |
31 | 94 |
|
32 | | -```bash |
33 | | -cargo run # runs all 6 experiments on a path graph |
| 95 | +## Tutorial |
| 96 | + |
| 97 | +### 1. Wave Propagation on a Path Graph |
| 98 | + |
| 99 | +Send a pulse from one end and watch it travel: |
| 100 | + |
| 101 | +```rust |
| 102 | +use wave_conservation::wave::WaveState; |
| 103 | +use wave_conservation::experiments; |
| 104 | + |
| 105 | +let adj = experiments::path_graph(20); |
| 106 | +let mut wave = WaveState::new(adj); |
| 107 | +wave.pulse(0, 1.0); // inject energy at node 0 |
| 108 | + |
| 109 | +// Step forward and track displacement |
| 110 | +for step in 0..5000 { |
| 111 | + wave.step(0.01); |
| 112 | + if step % 500 == 0 { |
| 113 | + println!("Step {}: node[10] = {:.4}", step, wave.displacement[10]); |
| 114 | + } |
| 115 | +} |
34 | 116 | ``` |
35 | 117 |
|
36 | | -## Experiments |
| 118 | +### 2. Wave Speed = √λ₂ (Fiedler Eigenvalue) |
37 | 119 |
|
38 | | -| # | Experiment | What It Shows | |
39 | | -|---|-----------|---------------| |
40 | | -| 1 | Wave Speed | Measured speed ≈ √λ₂ (predicted from spectrum) | |
41 | | -| 2 | CR vs Coherence | Higher conservation ratio → longer coherence halflife | |
42 | | -| 3 | Standing Waves | Resonance at λ₂ frequency confirms eigenvalue spectrum | |
43 | | -| 4 | Fiedler Reflection | Energy reflected at the algebraic connectivity boundary | |
44 | | -| 5 | Frequency Sweep | Response peaks recover all eigenvalues | |
45 | | -| 6 | Wave Interference | Multi-source interference patterns | |
| 120 | +The most fundamental correspondence — verify it experimentally: |
46 | 121 |
|
47 | | -## API Reference |
| 122 | +```rust |
| 123 | +use wave_conservation::experiments; |
48 | 124 |
|
49 | | -### `WaveState` |
| 125 | +let adj = experiments::path_graph(30); |
| 126 | +let report = experiments::verify_wave_speed(&adj); |
| 127 | + |
| 128 | +println!("Predicted speed (√λ₂): {:.4}", report.predicted_speed); |
| 129 | +println!("Measured speed: {:.4}", report.wave_speed); |
| 130 | +println!("Error: {:.4} ({:.1}%)", |
| 131 | + report.speed_error, |
| 132 | + if report.predicted_speed > 0.0 { |
| 133 | + 100.0 * report.speed_error / report.predicted_speed |
| 134 | + } else { 0.0 } |
| 135 | +); |
| 136 | +``` |
| 137 | + |
| 138 | +### 3. Standing Waves Reveal the Spectrum |
| 139 | + |
| 140 | +Drive at eigenvalue frequencies and observe resonance: |
50 | 141 |
|
51 | 142 | ```rust |
52 | | -let mut ws = WaveState::new(adjacency_matrix); |
53 | | -ws.pulse(node_index, amplitude); // inject energy |
54 | | -ws.step(dt); // advance one timestep (velocity Verlet) |
| 143 | +use wave_conservation::{spectral, experiments}; |
| 144 | + |
| 145 | +let adj = experiments::path_graph(10); |
| 146 | +let eigs = spectral::eigenvalues(&adj); |
| 147 | + |
| 148 | +// Drive at √λ₂ — should produce resonance |
| 149 | +let freq = eigs[1].sqrt(); |
| 150 | +let response = experiments::standing_waves(&adj, freq, 10000); |
| 151 | +let max_amp = response.iter().map(|x| x.abs()).fold(0.0_f64, f64::max); |
| 152 | +println!("Resonance at √λ₂={:.4}: max amplitude = {:.4}", freq, max_amp); |
55 | 153 | ``` |
56 | 154 |
|
57 | | -### `spectral` |
| 155 | +### 4. Frequency Sweep — Eigenvalues from Wave Response |
| 156 | + |
| 157 | +Sweep frequencies and find peaks that correspond to eigenvalues: |
58 | 158 |
|
59 | 159 | ```rust |
60 | | -let eigs = spectral::eigenvalues(&adj); // all eigenvalues (power iteration) |
61 | | -let response = spectral::frequency_sweep(&adj, f_min, f_max, steps); |
62 | | -let peaks = spectral::find_peaks(&response, threshold); |
| 160 | +use wave_conservation::{spectral, experiments}; |
| 161 | + |
| 162 | +let adj = experiments::path_graph(10); |
| 163 | +let eigs = spectral::eigenvalues(&adj); |
| 164 | + |
| 165 | +// Sweep from 0.1 to 3.0 Hz |
| 166 | +let sweep = spectral::frequency_sweep(&adj, 0.1, 3.0, 200); |
| 167 | +let peaks = spectral::find_peaks(&sweep, 0.3); |
| 168 | + |
| 169 | +println!("Frequency sweep peaks (should match √λᵢ):"); |
| 170 | +for (freq, resp) in &peaks { |
| 171 | + println!(" f={:.4}, response={:.4} (λ ≈ {:.4})", freq, resp, freq * freq); |
| 172 | +} |
| 173 | + |
| 174 | +println!("\nActual eigenvalues:"); |
| 175 | +for (i, &e) in eigs.iter().enumerate().take(5) { |
| 176 | + println!(" λ[{}] = {:.4}, √λ = {:.4}", i, e, e.sqrt()); |
| 177 | +} |
63 | 178 | ``` |
64 | 179 |
|
65 | | -### Graph generators (`experiments`) |
| 180 | +### 5. Conservation Ratio Predicts Coherence |
| 181 | + |
| 182 | +Higher CR → longer coherence halflife: |
66 | 183 |
|
67 | 184 | ```rust |
68 | | -let adj = experiments::path_graph(n); |
69 | | -let adj = experiments::cycle_graph(n); |
70 | | -let adj = experiments::complete_graph(n); |
71 | | -let adj = experiments::star_graph(n); |
72 | | -let adj = experiments::barbell_graph(k); |
| 185 | +use wave_conservation::experiments; |
| 186 | + |
| 187 | +let data = experiments::cr_vs_coherence(); |
| 188 | +println!("CR vs Coherence Halflife:"); |
| 189 | +for (cr, halflife) in &data { |
| 190 | + println!(" CR={:.4} halflife={} steps", cr, halflife); |
| 191 | +} |
| 192 | +// Complete graph: CR ≈ 1.0, longest coherence |
| 193 | +// Path graph: CR ≈ 0.1, shortest coherence |
73 | 194 | ``` |
74 | 195 |
|
75 | | -## Testing |
| 196 | +### 6. Energy Conservation (Symplectic Integrator) |
| 197 | + |
| 198 | +The velocity Verlet integrator preserves energy: |
76 | 199 |
|
77 | | -```bash |
78 | | -cargo test |
79 | | -cargo run # interactive demo with all experiments |
| 200 | +```rust |
| 201 | +use wave_conservation::wave::WaveState; |
| 202 | +use wave_conservation::experiments; |
| 203 | + |
| 204 | +let adj = experiments::path_graph(20); |
| 205 | +let mut wave = WaveState::new(adj).with_damping(0.0); // no damping! |
| 206 | +wave.pulse(0, 1.0); |
| 207 | +let e0 = wave.energy(); |
| 208 | + |
| 209 | +for _ in 0..10000 { |
| 210 | + wave.step(0.002); // small dt for good conservation |
| 211 | +} |
| 212 | + |
| 213 | +let ef = wave.energy(); |
| 214 | +println!("Energy drift: {:.4}% ({:.6} → {:.6})", |
| 215 | + 100.0 * (ef - e0) / e0, e0, ef); |
80 | 216 | ``` |
81 | 217 |
|
82 | | -## Installation |
| 218 | +### 7. Graph Zoo — Different Topologies, Different Spectra |
83 | 219 |
|
84 | | -```toml |
85 | | -[dependencies] |
86 | | -wave-conservation = { git = "https://github.com/SuperInstance/wave-conservation" } |
| 220 | +```rust |
| 221 | +use wave_conservation::{spectral, experiments}; |
| 222 | + |
| 223 | +let graphs = [ |
| 224 | + ("Path(10)", experiments::path_graph(10)), |
| 225 | + ("Cycle(10)", experiments::cycle_graph(10)), |
| 226 | + ("Star(10)", experiments::star_graph(10)), |
| 227 | + ("Complete(10)", experiments::complete_graph(10)), |
| 228 | + ("Barbell(5)", experiments::barbell_graph(5)), |
| 229 | +]; |
| 230 | + |
| 231 | +for (name, adj) in &graphs { |
| 232 | + let eigs = spectral::eigenvalues(adj); |
| 233 | + let cr = spectral::conservation_ratio(adj); |
| 234 | + println!("{}: λ₂={:.4}, λₙ={:.4}, CR={:.4}", |
| 235 | + name, eigs[1], eigs[eigs.len()-1], cr); |
| 236 | +} |
87 | 237 | ``` |
88 | 238 |
|
89 | | -## How It Fits |
| 239 | +## API Reference |
| 240 | + |
| 241 | +### `wave` Module |
| 242 | + |
| 243 | +| Type/Method | Description | |
| 244 | +|-------------|-------------| |
| 245 | +| `WaveState::new(adj)` | Create wave state from adjacency matrix | |
| 246 | +| `.with_damping(γ)` | Set damping coefficient (builder pattern) | |
| 247 | +| `.pulse(node, amplitude)` | Inject energy at a node | |
| 248 | +| `.step(dt)` | Advance one timestep (velocity Verlet) | |
| 249 | +| `.energy()` | Total kinetic + potential energy | |
| 250 | +| `.coherence()` | Average correlation of displacement between neighbors | |
| 251 | +| `.conservation_ratio(e₀)` | Ratio E_now / E_initial | |
| 252 | +| `WaveReport` | Full experiment results (speed, coherence, energy) | |
| 253 | + |
| 254 | +### `spectral` Module |
| 255 | + |
| 256 | +| Function | Description | |
| 257 | +|----------|-------------| |
| 258 | +| `eigenvalues(&adj)` | All eigenvalues via power iteration + deflation | |
| 259 | +| `fiedler_vector(&adj)` | Eigenvector of λ₂ (graph partition) | |
| 260 | +| `conservation_ratio(&adj)` | CR = λ₂/λₙ | |
| 261 | +| `resonance_frequencies(&adj)` | √λᵢ for each eigenvalue | |
| 262 | +| `frequency_response(&adj, freq)` | Steady-state amplitude at given frequency | |
| 263 | +| `frequency_sweep(&adj, min, max, steps)` | Response curve over frequency range | |
| 264 | +| `find_peaks(&sweep, threshold)` | Locate resonance peaks in sweep data | |
| 265 | + |
| 266 | +### `experiments` Module |
| 267 | + |
| 268 | +| Function | Description | |
| 269 | +|----------|-------------| |
| 270 | +| `path_graph(n)` | Path graph adjacency | |
| 271 | +| `cycle_graph(n)` | Cycle graph adjacency | |
| 272 | +| `complete_graph(n)` | Complete graph adjacency | |
| 273 | +| `star_graph(n)` | Star graph adjacency | |
| 274 | +| `barbell_graph(k)` | Two k-cliques joined by a bridge | |
| 275 | +| `verify_wave_speed(&adj)` | Experiment 1: measure vs predicted speed | |
| 276 | +| `cr_vs_coherence()` | Experiment 2: CR vs coherence across graph types | |
| 277 | +| `standing_waves(&adj, freq, steps)` | Experiment 3: drive at eigenfrequency | |
| 278 | +| `fiedler_reflection(&adj)` | Experiment 4: wave at Fiedler boundary | |
| 279 | +| `interference_pattern(&adj)` | Experiment 5: two-source interference | |
90 | 280 |
|
91 | | -Part of the SuperInstance ecosystem: |
| 281 | +## Ecosystem Role |
| 282 | + |
| 283 | +`wave-conservation` is the **wave dynamics layer** of the SuperInstance ecosystem: |
92 | 284 |
|
93 | 285 | - **[heat-spectral](https://github.com/SuperInstance/heat-spectral)** — Heat diffusion (parabolic PDE) on graphs |
94 | | -- **wave-conservation** — Wave propagation (hyperbolic PDE) on graphs (this repo) |
95 | | -- **[graph-neural](https://github.com/SuperInstance/graph-neural)** — Graph neural network spectral primitives |
| 286 | +- **[analog-spectral](https://github.com/SuperInstance/analog-spectral)** — Physical dials and eigenvalue estimation |
| 287 | +- **[dial-ecology](https://github.com/SuperInstance/dial-ecology)** — Lotka-Volterra competition for traditions |
| 288 | +- **wave-conservation** — Wave propagation and spectral analysis on graphs (this crate) |
| 289 | + |
| 290 | +The conservation ratio computed here connects to the spectral thermostat in `analog-spectral`, while the graph eigenvalue analysis provides structural insights for `dial-ecology`'s niche-based competition models. |
| 291 | + |
| 292 | +## Installation |
| 293 | + |
| 294 | +```toml |
| 295 | +[dependencies] |
| 296 | +wave-conservation = { git = "https://github.com/SuperInstance/wave-conservation" } |
| 297 | +``` |
96 | 298 |
|
97 | 299 | ## License |
98 | 300 |
|
|
0 commit comments