use core::f64::consts::TAU;
pub const GUST_TERMS: usize = 12;
pub const GUST_INTENSITY: f32 = 0.25;
pub const GUST_TIMESCALE: f32 = 2.0;
const MIN_TIMESCALE: f32 = 0.05;
const LOWEST: f64 = 0.125;
const RATIO: f64 = core::f64::consts::SQRT_2;
fn mix(mut x: u32) -> u32 {
x ^= x >> 16;
x = x.wrapping_mul(0x7feb_352d);
x ^= x >> 15;
x = x.wrapping_mul(0x846c_a68b);
x ^ (x >> 16)
}
pub fn gust_unit(t: f64, seed: u32, timescale: f32) -> f32 {
if t.is_nan() || timescale.is_nan() {
return 0.0;
}
let corner = 1.0 / (TAU * f64::from(timescale.max(MIN_TIMESCALE)));
let mut total = 0.0;
let mut sum = 0.0;
let mut freq = LOWEST;
let mut tones = [(0.0, 0.0); GUST_TERMS];
for (k, tone) in tones.iter_mut().enumerate() {
let weight = freq / (1.0 + freq * freq);
let phase = f64::from(mix(seed ^ mix(k as u32 + 1))) / f64::from(u32::MAX) * TAU;
*tone = (weight, phase);
sum += weight;
freq *= RATIO;
}
let mut freq = LOWEST;
for (weight, phase) in tones {
let amplitude = (2.0 * weight / sum).sqrt();
total += amplitude * (TAU * freq * corner * t + phase).sin();
freq *= RATIO;
}
total as f32
}
pub fn gust_velocity(mean: (f32, f32), intensity: f32, timescale: f32, seed: u32, t: f64) -> (f32, f32) {
let speed = mean.0.hypot(mean.1);
if !speed.is_finite() || intensity.is_nan() || speed == 0.0 {
return (0.0, 0.0);
}
let intensity = intensity.max(0.0);
if intensity == 0.0 {
return mean;
}
let along = gust_unit(t, seed, timescale);
let across = gust_unit(t, seed ^ 0x9e37_79b9, timescale);
let speed = (speed * (1.0 + intensity * along)).max(0.0);
let heading = mean.1.atan2(mean.0) + intensity * across;
(speed * heading.cos(), speed * heading.sin())
}
#[cfg(test)]
mod tests {
use super::*;
fn samples(seed: u32, timescale: f32, seconds: f32, rate: f32) -> Vec<f32> {
let n = (seconds * rate) as usize;
(0..n).map(|i| gust_unit(i as f64 / f64::from(rate), seed, timescale)).collect()
}
fn mean_var(v: &[f32]) -> (f32, f32) {
let mean = v.iter().sum::<f32>() / v.len() as f32;
(mean, v.iter().map(|x| (x - mean).powi(2)).sum::<f32>() / v.len() as f32)
}
#[test]
fn the_process_is_zero_mean_and_unit_variance_for_every_seed() {
for seed in [0, 1, 7, 42, 1234, 99_999] {
let (mean, var) = mean_var(&samples(seed, 2.0, 2000.0, 20.0));
assert!(mean.abs() < 0.25, "seed {seed}: mean {mean}");
assert!((0.6..1.4).contains(&var), "seed {seed}: variance {var}");
}
}
#[test]
fn it_is_smooth_at_frame_rate() {
for seed in [3, 11] {
let v = samples(seed, 2.0, 600.0, 60.0);
let worst = v.windows(2).map(|w| (w[1] - w[0]).abs()).fold(0.0, f32::max);
assert!(worst < 0.1, "seed {seed}: a frame moved the gust by {worst} sigma");
}
}
#[test]
fn the_correlation_time_is_the_timescale() {
for timescale in [1.0_f32, 2.0, 4.0] {
let rate = 10.0;
let v = samples(5, timescale, 4000.0, rate);
let (mean, var) = mean_var(&v);
let lag = (timescale * rate) as usize;
let c = v.iter().zip(&v[lag..]).map(|(a, b)| (a - mean) * (b - mean)).sum::<f32>()
/ (v.len() - lag) as f32
/ var;
assert!((c - (-1.0f32).exp()).abs() < 0.2, "timescale {timescale}: r(T) = {c}");
}
}
#[test]
fn no_turbulence_is_the_mean_and_a_gust_never_reverses_the_speed() {
for i in 0..200 {
let t = i as f64 * 0.37;
assert_eq!(gust_velocity((2.0, 0.0), 0.0, 2.0, 9, t), (2.0, 0.0));
let (x, z) = gust_velocity((1.0, 1.0), 5.0, 2.0, 9, t);
assert!(x.is_finite() && z.is_finite());
let v = x.hypot(z);
assert!(v >= 0.0 && v < 1.0 + 1.5 * 2.0_f32.sqrt() * 5.0 * 10.0);
}
}
#[test]
fn the_heading_wanders_by_about_the_intensity() {
let intensity = 0.25;
let angles: Vec<f32> = (0..20_000)
.map(|i| {
let (x, z) = gust_velocity((3.0, 0.0), intensity, 2.0, 4, i as f64 * 0.1);
z.atan2(x)
})
.collect();
let (mean, var) = mean_var(&angles);
assert!(mean.abs() < 0.1, "mean heading {mean}");
assert!((var.sqrt() - intensity).abs() < 0.1, "heading sigma {}", var.sqrt());
}
#[test]
fn degenerate_inputs_give_still_air_not_nan() {
assert_eq!(gust_velocity((0.0, 0.0), 0.3, 2.0, 1, 1.0), (0.0, 0.0));
assert_eq!(gust_velocity((f32::NAN, 0.0), 0.3, 2.0, 1, 1.0), (0.0, 0.0));
assert_eq!(gust_velocity((1.0, 0.0), f32::NAN, 2.0, 1, 1.0), (0.0, 0.0));
assert_eq!(gust_unit(f64::NAN, 1, 2.0), 0.0);
assert!(gust_unit(1.0, 1, 0.0).is_finite(), "a zero timescale is clamped");
}
#[test]
fn a_long_session_is_as_smooth_as_the_start() {
let t0 = 40.0 * 3600.0_f64;
let worst = (0..600)
.map(|i| {
let a = gust_unit(t0 + f64::from(i) / 60.0, 3, 2.0);
let b = gust_unit(t0 + f64::from(i + 1) / 60.0, 3, 2.0);
(b - a).abs()
})
.fold(0.0, f32::max);
assert!(worst < 0.15, "{worst}");
}
}