Skip to content

Commit 09f00b9

Browse files
committed
docs: world-class documentation, tutorials, and examples
1 parent 13ceee2 commit 09f00b9

4 files changed

Lines changed: 600 additions & 52 deletions

File tree

README.md

Lines changed: 253 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,300 @@
11
# wave-conservation
22

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.**
44

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.*
66
7-
## What This Gives You
7+
---
88

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+
```
1667

1768
## Quick Start
1869

1970
```rust
2071
use wave_conservation::wave::WaveState;
2172
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());
2282

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();
2687

2788
for _ in 0..1000 {
2889
wave.step(0.01);
2990
}
91+
92+
println!("Energy conservation: {:.4}", wave.conservation_ratio(e0));
3093
```
3194

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+
}
34116
```
35117

36-
## Experiments
118+
### 2. Wave Speed = √λ₂ (Fiedler Eigenvalue)
37119

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:
46121

47-
## API Reference
122+
```rust
123+
use wave_conservation::experiments;
48124

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:
50141

51142
```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);
55153
```
56154

57-
### `spectral`
155+
### 4. Frequency Sweep — Eigenvalues from Wave Response
156+
157+
Sweep frequencies and find peaks that correspond to eigenvalues:
58158

59159
```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+
}
63178
```
64179

65-
### Graph generators (`experiments`)
180+
### 5. Conservation Ratio Predicts Coherence
181+
182+
Higher CR → longer coherence halflife:
66183

67184
```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
73194
```
74195

75-
## Testing
196+
### 6. Energy Conservation (Symplectic Integrator)
197+
198+
The velocity Verlet integrator preserves energy:
76199

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);
80216
```
81217

82-
## Installation
218+
### 7. Graph Zoo — Different Topologies, Different Spectra
83219

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+
}
87237
```
88238

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 |
90280

91-
Part of the SuperInstance ecosystem:
281+
## Ecosystem Role
282+
283+
`wave-conservation` is the **wave dynamics layer** of the SuperInstance ecosystem:
92284

93285
- **[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+
```
96298

97299
## License
98300

0 commit comments

Comments
 (0)