1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
//! # Stochastic Resonance Module
//!
//! Demonstrates the counterintuitive phenomenon where adding an *optimal*
//! level of noise to a nonlinear system actually *improves* signal detection.
//!
//! ## Theory
//!
//! A weak sub-threshold signal s(t) is normally too small to cross the
//! detection threshold of a nonlinear element (e.g., a bistable potential).
//! Adding Gaussian noise of amplitude σ can help the signal cross the
//! threshold at the right moments, effectively amplifying it.
//!
//! The signal-to-noise ratio (SNR) as a function of noise amplitude σ shows
//! a characteristic inverted-U shape with a maximum at σ_opt.
//!
//! ## Audio mapping
//!
//! - Detected signal crossings → rhythmic clicks / triggers.
//! - Noise amplitude σ → brightness / spectral centroid of background texture.
//! - SNR curve peak → loudest / most rhythmically coherent output.
//!
//! ## Usage
//!
//! ```rust
//! use math_sonify::stochastic_resonance::{StochasticResonance, SRConfig};
//!
//! let cfg = SRConfig::default();
//! let mut sr = StochasticResonance::new(cfg);
//! let (state, crossings) = sr.step(0.0);
//! let _ = sr.snr_curve(20);
//! ```
/// Configuration for the stochastic resonance experiment.
#[derive(Debug, Clone)]
pub struct SRConfig {
/// Weak sinusoidal signal amplitude (should be sub-threshold, i.e., < 1.0).
pub signal_amplitude: f64,
/// Signal frequency (Hz equivalent in simulation time).
pub signal_frequency: f64,
/// Current noise standard deviation σ (tunable in real-time).
pub noise_amplitude: f64,
/// Double-well potential barrier height.
pub barrier_height: f64,
/// Bistable system damping coefficient.
pub damping: f64,
/// Integration timestep.
pub dt: f64,
}
impl Default for SRConfig {
fn default() -> Self {
Self {
signal_amplitude: 0.3,
signal_frequency: 0.05,
noise_amplitude: 0.5,
barrier_height: 1.0,
damping: 0.1,
dt: 0.01,
}
}
}
/// State of the stochastic resonance system.
pub struct StochasticResonance {
pub config: SRConfig,
/// Current position in the bistable potential.
x: f64,
/// Simulation time.
t: f64,
/// Last well the particle was in (+1 or -1).
last_well: i32,
/// Count of threshold crossings in current measurement window.
crossing_count: u32,
/// Deterministic pseudo-random state (LCG).
rng_state: u64,
/// Accumulated SNR data: (sigma, snr) pairs.
pub snr_data: Vec<(f64, f64)>,
}
impl StochasticResonance {
pub fn new(config: SRConfig) -> Self {
Self {
config,
x: -1.0,
t: 0.0,
last_well: -1,
crossing_count: 0,
rng_state: 12345678901234567,
snr_data: Vec::new(),
}
}
/// Advance the system by one timestep.
///
/// Returns `(x, crossed)` where `x` is the current position and
/// `crossed` is true if the particle switched wells this step.
pub fn step(&mut self, _external_signal: f64) -> (f64, bool) {
let dt = self.config.dt;
let a = self.config.barrier_height;
let gamma = self.config.damping;
let amp = self.config.signal_amplitude;
let freq = self.config.signal_frequency;
// Driving signal s(t)
let s = amp * (2.0 * std::f64::consts::PI * freq * self.t).sin();
// Drift: -dV/dx for V(x) = -ax²/2 + x⁴/4 → dV/dx = -ax + x³
let drift = a * self.x - self.x.powi(3) + s;
// Gaussian noise via Box-Muller
let noise = self.gaussian_noise() * self.config.noise_amplitude / dt.sqrt();
// Euler-Maruyama step
self.x += (drift - gamma * self.x) * dt + noise * dt.sqrt();
self.t += dt;
// Detect well crossing
let current_well = if self.x > 0.0 { 1 } else { -1 };
let crossed = current_well != self.last_well;
if crossed {
self.last_well = current_well;
self.crossing_count += 1;
}
(self.x, crossed)
}
/// Compute the SNR curve by sweeping noise amplitudes.
///
/// Returns a vector of `(sigma, snr_db)` pairs.
/// The SNR is estimated as the ratio of signal-correlated crossings to
/// noise-only crossings.
pub fn snr_curve(&mut self, num_sigma_points: usize) -> Vec<(f64, f64)> {
let mut result = Vec::with_capacity(num_sigma_points);
let freq = self.config.signal_frequency;
let amp = self.config.signal_amplitude;
let barrier = self.config.barrier_height;
for i in 0..num_sigma_points {
let sigma = 0.05 + 3.0 * i as f64 / (num_sigma_points as f64 - 1.0).max(1.0);
// Theoretical SNR for bistable system (Kramers' rate approximation)
// r_K = (barrier * sqrt(2)) * exp(-barrier^2 / (2 * sigma^2))
let kramers_rate =
barrier * 2.0_f64.sqrt() * (-(barrier * barrier) / (2.0 * sigma * sigma)).exp();
// SNR ∝ amp^2 / (sigma^2 * r_K) for weak-signal limit, normalised
let snr_linear = if kramers_rate > 1e-12 {
(amp * amp) / (sigma * sigma) * kramers_rate
/ (kramers_rate * kramers_rate + (2.0 * std::f64::consts::PI * freq).powi(2))
} else {
0.0
};
// Convert to dB
let snr_db = if snr_linear > 1e-15 {
10.0 * snr_linear.log10()
} else {
-60.0
};
result.push((sigma, snr_db));
}
self.snr_data = result.clone();
result
}
/// Optimal noise amplitude (σ at which SNR is maximised).
pub fn optimal_noise(&mut self, resolution: usize) -> f64 {
let curve = self.snr_curve(resolution);
curve
.iter()
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
.map(|(sigma, _)| *sigma)
.unwrap_or(self.config.noise_amplitude)
}
/// Map system state to audio parameters.
///
/// Returns `(frequency_hz, amplitude, spectral_tilt)`.
pub fn audio_params(&self) -> (f64, f64, f64) {
// Map position to frequency: well -1 → 220 Hz, well +1 → 440 Hz
let base_freq = if self.x > 0.0 { 440.0 } else { 220.0 };
let freq_wobble = 1.0 + 0.05 * self.x.abs().min(2.0);
let frequency = base_freq * freq_wobble;
// Amplitude proportional to distance from barrier (x=0)
let amplitude = (self.x.abs() / 2.0).min(1.0);
// Spectral tilt: high noise → brighter (more high-frequency content)
let tilt = self.config.noise_amplitude.clamp(0.0, 3.0) / 3.0;
(frequency, amplitude, tilt)
}
// ── Internal ─────────────────────────────────────────────────────────────
fn gaussian_noise(&mut self) -> f64 {
// Box-Muller transform from two uniform samples
let u1 = self.lcg_uniform();
let u2 = self.lcg_uniform();
let u1 = u1.max(1e-15); // avoid log(0)
(-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
}
fn lcg_uniform(&mut self) -> f64 {
self.rng_state = self
.rng_state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(self.rng_state >> 11) as f64 / (1u64 << 53) as f64
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn step_does_not_nan() {
let mut sr = StochasticResonance::new(SRConfig::default());
for _ in 0..100 {
let (x, _) = sr.step(0.0);
assert!(x.is_finite(), "x must be finite");
}
}
#[test]
fn snr_curve_length() {
let mut sr = StochasticResonance::new(SRConfig::default());
let curve = sr.snr_curve(10);
assert_eq!(curve.len(), 10);
}
#[test]
fn snr_has_interior_maximum() {
let mut sr = StochasticResonance::new(SRConfig::default());
let curve = sr.snr_curve(30);
let max_idx = curve
.iter()
.enumerate()
.max_by(|a, b| a.1 .1.partial_cmp(&b.1 .1).unwrap())
.map(|(i, _)| i)
.unwrap();
// Max should not be at the extremes (pure stochastic resonance behaviour)
assert!(max_idx > 0 && max_idx < curve.len() - 1,
"SNR maximum should be at an interior point, got idx={max_idx}");
}
#[test]
fn audio_params_in_range() {
let sr = StochasticResonance::new(SRConfig::default());
let (freq, amp, tilt) = sr.audio_params();
assert!(freq > 0.0);
assert!((0.0..=1.0).contains(&));
assert!((0.0..=1.0).contains(&tilt));
}
#[test]
fn optimal_noise_positive() {
let mut sr = StochasticResonance::new(SRConfig::default());
let opt = sr.optimal_noise(20);
assert!(opt > 0.0);
}
}