Skip to main content

proof_engine/weather/
atmosphere.rs

1//! Atmospheric simulation subsystem.
2//!
3//! Models pressure layers, humidity fields, temperature profiles, 3-D wind vector grids,
4//! jet streams, fog density, visibility calculation, and barometric gradients.
5
6use std::collections::HashMap;
7use super::{Vec3, lerp, smoothstep, fbm_2d, value_noise_2d};
8
9// ── Constants ────────────────────────────────────────────────────────────────
10
11/// ISA sea-level pressure in Pascals.
12pub const ISA_PRESSURE_PA: f32 = 101_325.0;
13/// ISA sea-level temperature in Kelvin.
14pub const ISA_TEMP_K: f32 = 288.15;
15/// Lapse rate K/m (ISA troposphere).
16pub const LAPSE_RATE: f32 = 0.006_5;
17/// Specific gas constant for dry air.
18pub const R_DRY: f32 = 287.058;
19/// Gravity m/s².
20pub const GRAVITY: f32 = 9.807;
21/// Troposphere top altitude (m).
22pub const TROPOPAUSE_ALT: f32 = 11_000.0;
23/// Stratosphere temperature (K) above tropopause.
24pub const STRATO_TEMP_K: f32 = 216.65;
25
26// ── Configuration ─────────────────────────────────────────────────────────────
27
28/// Configuration for the atmospheric simulator.
29#[derive(Debug, Clone)]
30pub struct AtmosphereConfig {
31    /// Number of vertical layers to simulate.
32    pub layer_count: usize,
33    /// Maximum altitude of the simulation (metres).
34    pub max_altitude_m: f32,
35    /// Horizontal grid resolution in world units.
36    pub grid_resolution: f32,
37    /// Grid width (cells in X).
38    pub grid_width: usize,
39    /// Grid depth (cells in Z).
40    pub grid_depth: usize,
41    /// Turbulence strength multiplier.
42    pub turbulence_scale: f32,
43    /// Enable large-scale jet-stream simulation.
44    pub enable_jet_streams: bool,
45    /// Fog scattering coefficient (higher = denser fog).
46    pub fog_scatter_coeff: f32,
47}
48
49impl Default for AtmosphereConfig {
50    fn default() -> Self {
51        Self {
52            layer_count: 8,
53            max_altitude_m: 12_000.0,
54            grid_resolution: 200.0,
55            grid_width: 32,
56            grid_depth: 32,
57            turbulence_scale: 0.4,
58            enable_jet_streams: true,
59            fog_scatter_coeff: 0.02,
60        }
61    }
62}
63
64// ── Pressure ──────────────────────────────────────────────────────────────────
65
66/// A barometric pressure cell covering a horizontal region.
67#[derive(Debug, Clone)]
68pub struct PressureCell {
69    /// Centre of the cell in world-space (x, z).
70    pub centre: [f32; 2],
71    /// Radius of influence in metres.
72    pub radius: f32,
73    /// Pressure at the centre in Pascals.
74    pub pressure_pa: f32,
75    /// Type of system.
76    pub kind: PressureCellKind,
77    /// Drift velocity [m/s] in world-space (x, z).
78    pub drift: [f32; 2],
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum PressureCellKind {
83    HighPressure,
84    LowPressure,
85    Neutral,
86}
87
88impl PressureCell {
89    pub fn new_high(cx: f32, cz: f32, radius: f32) -> Self {
90        Self {
91            centre: [cx, cz],
92            radius,
93            pressure_pa: ISA_PRESSURE_PA + 2_000.0,
94            kind: PressureCellKind::HighPressure,
95            drift: [1.5, 0.5],
96        }
97    }
98
99    pub fn new_low(cx: f32, cz: f32, radius: f32) -> Self {
100        Self {
101            centre: [cx, cz],
102            radius,
103            pressure_pa: ISA_PRESSURE_PA - 3_000.0,
104            kind: PressureCellKind::LowPressure,
105            drift: [2.0, -0.3],
106        }
107    }
108
109    /// Sample pressure contribution at world pos (x, z).
110    pub fn sample(&self, x: f32, z: f32) -> f32 {
111        let dx = x - self.centre[0];
112        let dz = z - self.centre[1];
113        let dist = (dx * dx + dz * dz).sqrt();
114        if dist >= self.radius { return ISA_PRESSURE_PA; }
115        let t = smoothstep(self.radius, 0.0, dist);
116        lerp(ISA_PRESSURE_PA, self.pressure_pa, t)
117    }
118
119    /// Advance cell position by `dt` seconds.
120    pub fn tick(&mut self, dt: f32) {
121        self.centre[0] += self.drift[0] * dt;
122        self.centre[1] += self.drift[1] * dt;
123    }
124}
125
126/// Barometric pressure gradient — the spatial derivative of pressure.
127#[derive(Debug, Clone, Copy)]
128pub struct BarometricGradient {
129    /// Gradient vector [Pa/m] in (x, z).
130    pub grad_x: f32,
131    pub grad_z: f32,
132}
133
134impl BarometricGradient {
135    pub fn magnitude(&self) -> f32 {
136        (self.grad_x * self.grad_x + self.grad_z * self.grad_z).sqrt()
137    }
138
139    /// Derive geostrophic wind from gradient (simplified, f=Coriolis param).
140    pub fn geostrophic_wind(&self, air_density: f32, coriolis: f32) -> Vec3 {
141        // Geostrophic: V = (1/(ρ·f)) × ∇p rotated 90°
142        let factor = 1.0 / (air_density * coriolis.max(1e-5));
143        Vec3::new(
144             self.grad_z * factor,
145            0.0,
146            -self.grad_x * factor,
147        )
148    }
149}
150
151// ── Humidity ──────────────────────────────────────────────────────────────────
152
153/// A 2-D humidity map over the simulation grid.
154#[derive(Debug, Clone)]
155pub struct HumidityMap {
156    pub width: usize,
157    pub depth: usize,
158    /// Relative humidity [0.0, 1.0] stored row-major (z * width + x).
159    pub data: Vec<f32>,
160}
161
162impl HumidityMap {
163    pub fn new(width: usize, depth: usize, base: f32) -> Self {
164        Self {
165            width,
166            depth,
167            data: vec![base; width * depth],
168        }
169    }
170
171    /// Sample humidity at continuous grid coords using bilinear interpolation.
172    pub fn sample(&self, gx: f32, gz: f32) -> f32 {
173        let x0 = (gx.floor() as usize).min(self.width.saturating_sub(2));
174        let z0 = (gz.floor() as usize).min(self.depth.saturating_sub(2));
175        let fx = gx - gx.floor();
176        let fz = gz - gz.floor();
177        let v00 = self.data[z0       * self.width + x0    ];
178        let v10 = self.data[z0       * self.width + x0 + 1];
179        let v01 = self.data[(z0 + 1) * self.width + x0    ];
180        let v11 = self.data[(z0 + 1) * self.width + x0 + 1];
181        lerp(lerp(v00, v10, fx), lerp(v01, v11, fx), fz)
182    }
183
184    /// Set humidity at integer grid cell.
185    pub fn set(&mut self, gx: usize, gz: usize, val: f32) {
186        if gx < self.width && gz < self.depth {
187            self.data[gz * self.width + gx] = val.clamp(0.0, 1.0);
188        }
189    }
190
191    /// Advect humidity by a uniform wind (dx, dz) in grid cells/step.
192    pub fn advect(&mut self, dx: f32, dz: f32) {
193        let old = self.data.clone();
194        for z in 0..self.depth {
195            for x in 0..self.width {
196                let src_x = x as f32 - dx;
197                let src_z = z as f32 - dz;
198                let clamped_x = src_x.clamp(0.0, (self.width  - 1) as f32);
199                let clamped_z = src_z.clamp(0.0, (self.depth  - 1) as f32);
200                let x0 = clamped_x.floor() as usize;
201                let z0 = clamped_z.floor() as usize;
202                let x1 = (x0 + 1).min(self.width  - 1);
203                let z1 = (z0 + 1).min(self.depth  - 1);
204                let fx = clamped_x - clamped_x.floor();
205                let fz = clamped_z - clamped_z.floor();
206                let v = lerp(
207                    lerp(old[z0 * self.width + x0], old[z0 * self.width + x1], fx),
208                    lerp(old[z1 * self.width + x0], old[z1 * self.width + x1], fx),
209                    fz,
210                );
211                self.data[z * self.width + x] = v.clamp(0.0, 1.0);
212            }
213        }
214    }
215
216    /// Evaporation — increase humidity by `rate` everywhere (capped at 1.0).
217    pub fn evaporate(&mut self, rate: f32) {
218        for v in &mut self.data {
219            *v = (*v + rate).min(1.0);
220        }
221    }
222
223    /// Precipitation sink — decrease humidity proportional to excess above `sat`.
224    pub fn precipitate(&mut self, sat: f32, coeff: f32) {
225        for v in &mut self.data {
226            if *v > sat {
227                *v -= (*v - sat) * coeff;
228            }
229        }
230    }
231}
232
233// ── Temperature Profile ───────────────────────────────────────────────────────
234
235/// Vertical temperature profile — one value per simulation layer.
236#[derive(Debug, Clone)]
237pub struct TemperatureProfile {
238    /// Altitude of each layer's base (m).
239    pub altitudes: Vec<f32>,
240    /// Temperature (K) at each layer.
241    pub temps_k: Vec<f32>,
242}
243
244impl TemperatureProfile {
245    /// Build a standard ISA-based profile for `layer_count` layers up to `max_alt_m`.
246    pub fn isa(layer_count: usize, max_alt_m: f32, surface_temp_k: f32) -> Self {
247        let mut altitudes = Vec::with_capacity(layer_count);
248        let mut temps_k   = Vec::with_capacity(layer_count);
249        for i in 0..layer_count {
250            let alt = i as f32 * max_alt_m / (layer_count - 1).max(1) as f32;
251            let t = if alt <= TROPOPAUSE_ALT {
252                surface_temp_k - LAPSE_RATE * alt
253            } else {
254                STRATO_TEMP_K
255            };
256            altitudes.push(alt);
257            temps_k.push(t);
258        }
259        Self { altitudes, temps_k }
260    }
261
262    /// Sample temperature at arbitrary altitude via linear interpolation.
263    pub fn sample_at(&self, alt_m: f32) -> f32 {
264        if alt_m <= self.altitudes[0] { return self.temps_k[0]; }
265        let last = self.altitudes.len() - 1;
266        if alt_m >= self.altitudes[last] { return self.temps_k[last]; }
267        for i in 0..last {
268            if alt_m >= self.altitudes[i] && alt_m <= self.altitudes[i + 1] {
269                let t = (alt_m - self.altitudes[i])
270                    / (self.altitudes[i + 1] - self.altitudes[i]);
271                return lerp(self.temps_k[i], self.temps_k[i + 1], t);
272            }
273        }
274        self.temps_k[last]
275    }
276
277    /// Compute air density (kg/m³) at a given altitude using the ideal gas law.
278    pub fn density_at(&self, alt_m: f32) -> f32 {
279        let temp = self.sample_at(alt_m);
280        let pressure = isa_pressure_at_altitude(alt_m);
281        pressure / (R_DRY * temp)
282    }
283
284    /// Update surface temperature, scaling the whole profile accordingly.
285    pub fn set_surface_temp(&mut self, new_temp_k: f32) {
286        if self.temps_k.is_empty() { return; }
287        let old_surface = self.temps_k[0];
288        let delta = new_temp_k - old_surface;
289        // Surface perturbation decays exponentially with altitude
290        for (i, t) in self.temps_k.iter_mut().enumerate() {
291            let alt = self.altitudes[i];
292            let decay = (-alt / 3_000.0_f32).exp();
293            *t += delta * decay;
294        }
295    }
296}
297
298/// ISA pressure at altitude using barometric formula.
299pub fn isa_pressure_at_altitude(alt_m: f32) -> f32 {
300    if alt_m <= TROPOPAUSE_ALT {
301        let base_ratio = 1.0 - LAPSE_RATE * alt_m / ISA_TEMP_K;
302        ISA_PRESSURE_PA * base_ratio.powf(GRAVITY / (LAPSE_RATE * R_DRY))
303    } else {
304        let p_tropo = isa_pressure_at_altitude(TROPOPAUSE_ALT);
305        let delta_alt = alt_m - TROPOPAUSE_ALT;
306        p_tropo * (-(GRAVITY * delta_alt) / (R_DRY * STRATO_TEMP_K)).exp()
307    }
308}
309
310// ── Atmospheric Layers ────────────────────────────────────────────────────────
311
312/// A single simulated atmospheric layer.
313#[derive(Debug, Clone)]
314pub struct AtmosphericLayer {
315    pub altitude_base_m: f32,
316    pub altitude_top_m:  f32,
317    pub pressure_pa:     f32,
318    pub temperature_k:   f32,
319    pub density_kg_m3:   f32,
320    pub humidity:        f32,   // relative humidity 0–1
321    pub cloud_fraction:  f32,   // 0–1 coverage
322    pub wind:            Vec3,
323}
324
325impl AtmosphericLayer {
326    pub fn from_isa(alt_base: f32, alt_top: f32, surface_temp_k: f32, humidity: f32) -> Self {
327        let mid = (alt_base + alt_top) * 0.5;
328        let temp_k = if mid <= TROPOPAUSE_ALT {
329            surface_temp_k - LAPSE_RATE * mid
330        } else {
331            STRATO_TEMP_K
332        };
333        let pressure = isa_pressure_at_altitude(mid);
334        let density  = pressure / (R_DRY * temp_k.max(1.0));
335        Self {
336            altitude_base_m: alt_base,
337            altitude_top_m:  alt_top,
338            pressure_pa:     pressure,
339            temperature_k:   temp_k,
340            density_kg_m3:   density,
341            humidity,
342            cloud_fraction:  0.0,
343            wind:            Vec3::ZERO,
344        }
345    }
346
347    pub fn thickness(&self) -> f32 {
348        self.altitude_top_m - self.altitude_base_m
349    }
350
351    /// Compute dew point (K) using Magnus approximation.
352    pub fn dew_point_k(&self) -> f32 {
353        let tc = self.temperature_k - 273.15;
354        let rh = self.humidity.clamp(0.001, 1.0);
355        let a = 17.625_f32;
356        let b = 243.04_f32;
357        let alpha = (a * tc / (b + tc)) + rh.ln();
358        let dp_c = b * alpha / (a - alpha);
359        dp_c + 273.15
360    }
361
362    /// True if temperature is at or below dew point (condensation occurs).
363    pub fn is_saturated(&self) -> bool {
364        self.temperature_k <= self.dew_point_k() + 0.5
365    }
366
367    /// Cloud likelihood based on humidity and instability.
368    pub fn update_cloud_fraction(&mut self) {
369        if self.is_saturated() {
370            self.cloud_fraction = (self.cloud_fraction + 0.1).min(1.0);
371        } else {
372            self.cloud_fraction = (self.cloud_fraction - 0.05).max(0.0);
373        }
374    }
375}
376
377// ── Wind Field ────────────────────────────────────────────────────────────────
378
379/// A single wind sample.
380#[derive(Debug, Clone, Copy)]
381pub struct WindVector {
382    pub velocity: Vec3,  // m/s in world space
383    pub turbulence: f32, // local turbulence intensity 0–1
384}
385
386/// 3-D wind field stored as a grid of [WindVector].
387#[derive(Debug, Clone)]
388pub struct WindField {
389    pub grid_w: usize,
390    pub grid_h: usize, // vertical layers
391    pub grid_d: usize,
392    pub resolution: f32,     // world units per cell
393    pub layer_height: f32,   // altitude per vertical cell
394    pub data: Vec<WindVector>,
395}
396
397impl WindField {
398    pub fn new(w: usize, h: usize, d: usize, resolution: f32, layer_height: f32) -> Self {
399        let default_wv = WindVector { velocity: Vec3::ZERO, turbulence: 0.0 };
400        Self {
401            grid_w: w,
402            grid_h: h,
403            grid_d: d,
404            resolution,
405            layer_height,
406            data: vec![default_wv; w * h * d],
407        }
408    }
409
410    fn index(&self, x: usize, y: usize, z: usize) -> usize {
411        (y * self.grid_d + z) * self.grid_w + x
412    }
413
414    pub fn set(&mut self, x: usize, y: usize, z: usize, wv: WindVector) {
415        if x < self.grid_w && y < self.grid_h && z < self.grid_d {
416            let i = self.index(x, y, z);
417            self.data[i] = wv;
418        }
419    }
420
421    /// Sample wind at world-space position using trilinear interpolation.
422    pub fn sample_world(&self, wx: f32, wy: f32, wz: f32) -> WindVector {
423        let gx = wx / self.resolution;
424        let gy = wy / self.layer_height;
425        let gz = wz / self.resolution;
426
427        let x0 = (gx.floor() as usize).min(self.grid_w.saturating_sub(2));
428        let y0 = (gy.floor() as usize).min(self.grid_h.saturating_sub(2));
429        let z0 = (gz.floor() as usize).min(self.grid_d.saturating_sub(2));
430        let x1 = (x0 + 1).min(self.grid_w - 1);
431        let y1 = (y0 + 1).min(self.grid_h - 1);
432        let z1 = (z0 + 1).min(self.grid_d - 1);
433
434        let fx = gx - gx.floor();
435        let fy = gy - gy.floor();
436        let fz = gz - gz.floor();
437
438        let lerp_wv = |a: WindVector, b: WindVector, t: f32| -> WindVector {
439            WindVector {
440                velocity:   a.velocity.lerp(b.velocity, t),
441                turbulence: lerp(a.turbulence, b.turbulence, t),
442            }
443        };
444
445        let c000 = self.data[self.index(x0, y0, z0)];
446        let c100 = self.data[self.index(x1, y0, z0)];
447        let c010 = self.data[self.index(x0, y1, z0)];
448        let c110 = self.data[self.index(x1, y1, z0)];
449        let c001 = self.data[self.index(x0, y0, z1)];
450        let c101 = self.data[self.index(x1, y0, z1)];
451        let c011 = self.data[self.index(x0, y1, z1)];
452        let c111 = self.data[self.index(x1, y1, z1)];
453
454        let c00 = lerp_wv(c000, c100, fx);
455        let c10 = lerp_wv(c010, c110, fx);
456        let c01 = lerp_wv(c001, c101, fx);
457        let c11 = lerp_wv(c011, c111, fx);
458
459        let c0 = lerp_wv(c00, c10, fy);
460        let c1 = lerp_wv(c01, c11, fy);
461
462        lerp_wv(c0, c1, fz)
463    }
464
465    /// Fill the entire field with a base directional wind that varies by layer.
466    pub fn fill_from_layers(&mut self, layers: &[AtmosphericLayer]) {
467        for y in 0..self.grid_h {
468            let alt = y as f32 * self.layer_height;
469            // Find the matching layer
470            let layer_wind = layers.iter()
471                .find(|l| alt >= l.altitude_base_m && alt < l.altitude_top_m)
472                .map(|l| l.wind)
473                .unwrap_or(Vec3::ZERO);
474
475            for z in 0..self.grid_d {
476                for x in 0..self.grid_w {
477                    let turb_noise = fbm_2d(
478                        x as f32 * 0.3 + alt * 0.001,
479                        z as f32 * 0.3,
480                        3, 2.0, 0.5,
481                    ) * 0.5 - 0.25;
482                    let turb_vec = Vec3::new(turb_noise, 0.0, turb_noise * 0.7);
483                    let wv = WindVector {
484                        velocity:   layer_wind.add(turb_vec),
485                        turbulence: turb_noise.abs() * 2.0,
486                    };
487                    self.set(x, y, z, wv);
488                }
489            }
490        }
491    }
492}
493
494// ── Jet Stream ────────────────────────────────────────────────────────────────
495
496/// A jet stream — a narrow band of fast upper-level wind.
497#[derive(Debug, Clone)]
498pub struct JetStream {
499    /// Core altitude (m).
500    pub altitude_m: f32,
501    /// Core wind speed (m/s).
502    pub core_speed: f32,
503    /// Direction (radians from +X axis).
504    pub direction: f32,
505    /// Vertical half-width (m) — Gaussian roll-off.
506    pub vertical_width_m: f32,
507    /// Horizontal sinusoidal amplitude for Rossby wave undulation.
508    pub wave_amplitude: f32,
509    /// Rossby wave number (radians per metre along the stream).
510    pub wave_number: f32,
511    /// Phase offset (radians).
512    pub wave_phase: f32,
513    /// Phase advance speed (radians/second).
514    pub wave_phase_speed: f32,
515}
516
517impl JetStream {
518    pub fn polar(northern_hemisphere: bool) -> Self {
519        Self {
520            altitude_m:        10_000.0,
521            core_speed:        if northern_hemisphere { 60.0 } else { 55.0 },
522            direction:         if northern_hemisphere { 0.0 } else { std::f32::consts::PI },
523            vertical_width_m:  2_000.0,
524            wave_amplitude:    400_000.0,
525            wave_number:       1.0 / 6_000_000.0,
526            wave_phase:        0.0,
527            wave_phase_speed:  1e-5,
528        }
529    }
530
531    pub fn subtropical() -> Self {
532        Self {
533            altitude_m:        12_000.0,
534            core_speed:        45.0,
535            direction:         0.1,
536            vertical_width_m:  1_500.0,
537            wave_amplitude:    200_000.0,
538            wave_number:       1.0 / 4_000_000.0,
539            wave_phase:        1.0,
540            wave_phase_speed:  8e-6,
541        }
542    }
543
544    pub fn tick(&mut self, dt: f32) {
545        self.wave_phase += self.wave_phase_speed * dt;
546    }
547
548    /// Sample jet-stream wind contribution at world pos (wx, wy, wz).
549    pub fn sample(&self, wx: f32, wy: f32, wz: f32) -> Vec3 {
550        // Vertical Gaussian roll-off from core altitude
551        let dalt = wy - self.altitude_m;
552        let vert_factor = (-0.5 * (dalt / self.vertical_width_m).powi(2)).exp();
553        if vert_factor < 1e-4 { return Vec3::ZERO; }
554
555        // Rossby wave undulation shifts the stream laterally
556        let undulation = (wx * self.wave_number + self.wave_phase).sin() * self.wave_amplitude;
557        let _effective_z = wz - undulation;
558
559        let speed = self.core_speed * vert_factor;
560        Vec3::new(
561            speed * self.direction.cos(),
562            0.0,
563            speed * self.direction.sin(),
564        )
565    }
566}
567
568// ── Fog ───────────────────────────────────────────────────────────────────────
569
570/// A fog layer — horizontally uniform, vertically parameterised.
571#[derive(Debug, Clone)]
572pub struct FogLayer {
573    /// Altitude of fog base (m).
574    pub base_m: f32,
575    /// Altitude of fog top (m).
576    pub top_m: f32,
577    /// Peak extinction coefficient (m⁻¹).
578    pub extinction: f32,
579    /// Fog type.
580    pub kind: FogKind,
581    /// Current visibility through the peak (metres).
582    pub visibility_m: f32,
583}
584
585#[derive(Debug, Clone, Copy, PartialEq, Eq)]
586pub enum FogKind {
587    Radiation,
588    Advection,
589    Upslope,
590    Freezing,
591}
592
593impl FogLayer {
594    pub fn radiation(base: f32, top: f32, extinction: f32) -> Self {
595        let vis = if extinction > 0.0 { 3.912 / extinction } else { 50_000.0 };
596        Self { base_m: base, top_m: top, extinction, kind: FogKind::Radiation, visibility_m: vis }
597    }
598
599    pub fn advection(base: f32, top: f32, extinction: f32) -> Self {
600        let vis = if extinction > 0.0 { 3.912 / extinction } else { 50_000.0 };
601        Self { base_m: base, top_m: top, extinction, kind: FogKind::Advection, visibility_m: vis }
602    }
603
604    /// Sample extinction at altitude `y`.
605    pub fn extinction_at(&self, y: f32) -> f32 {
606        if y < self.base_m || y > self.top_m { return 0.0; }
607        let t = (y - self.base_m) / (self.top_m - self.base_m).max(0.1);
608        // Bell-shaped profile peaking in the middle
609        let bell = smoothstep(0.0, 0.5, t) * smoothstep(1.0, 0.5, t) * 2.0;
610        self.extinction * bell
611    }
612
613    /// Dissipate fog over time.
614    pub fn dissipate(&mut self, rate: f32) {
615        self.extinction = (self.extinction - rate).max(0.0);
616        self.visibility_m = if self.extinction > 0.0 { 3.912 / self.extinction } else { 50_000.0 };
617    }
618
619    /// Intensify fog.
620    pub fn intensify(&mut self, rate: f32) {
621        self.extinction = (self.extinction + rate).min(0.5);
622        self.visibility_m = if self.extinction > 0.0 { 3.912 / self.extinction } else { 50_000.0 };
623    }
624}
625
626// ── Visibility ────────────────────────────────────────────────────────────────
627
628/// Result of a visibility computation.
629#[derive(Debug, Clone, Copy)]
630pub struct VisibilityResult {
631    /// Meteorological visibility (distance at which contrast = 0.05).
632    pub distance_m: f32,
633    /// Optical depth along the line of sight.
634    pub optical_depth: f32,
635    /// Dominant limiting factor.
636    pub limiting_factor: VisibilityLimiter,
637}
638
639#[derive(Debug, Clone, Copy, PartialEq, Eq)]
640pub enum VisibilityLimiter {
641    Clear,
642    Fog,
643    Precipitation,
644    Haze,
645    Smoke,
646    Dust,
647}
648
649// ── Main Atmosphere Struct ────────────────────────────────────────────────────
650
651/// The primary atmospheric simulation state.
652#[derive(Debug, Clone)]
653pub struct Atmosphere {
654    pub config: AtmosphereConfig,
655    pub layers: Vec<AtmosphericLayer>,
656    pub temperature_profile: TemperatureProfile,
657    pub humidity_map: HumidityMap,
658    pub wind_field: WindField,
659    pub pressure_cells: Vec<PressureCell>,
660    pub jet_streams: Vec<JetStream>,
661    pub fog_layers: Vec<FogLayer>,
662    pub surface_altitude_m: f32,
663    /// Background aerosol extinction (haze), m⁻¹.
664    pub haze_extinction: f32,
665    /// Time accumulator for slow dynamics.
666    time_accum: f32,
667    /// Noise offset drifting over time for turbulence variety.
668    noise_offset: f32,
669}
670
671impl Atmosphere {
672    pub fn new(surface_altitude_m: f32) -> Self {
673        Self::with_config(surface_altitude_m, AtmosphereConfig::default())
674    }
675
676    pub fn with_config(surface_altitude_m: f32, config: AtmosphereConfig) -> Self {
677        let n = config.layer_count;
678        let max_alt = config.max_altitude_m;
679        let surface_temp_k = ISA_TEMP_K;
680
681        // Build vertical layers
682        let layers: Vec<AtmosphericLayer> = (0..n).map(|i| {
683            let base = i as f32 * max_alt / n as f32;
684            let top  = (i + 1) as f32 * max_alt / n as f32;
685            AtmosphericLayer::from_isa(base, top, surface_temp_k, 0.5)
686        }).collect();
687
688        let temp_profile = TemperatureProfile::isa(n, max_alt, surface_temp_k);
689
690        let humidity_map = HumidityMap::new(
691            config.grid_width,
692            config.grid_depth,
693            0.55,
694        );
695
696        let layer_height = max_alt / n as f32;
697        let mut wind_field = WindField::new(
698            config.grid_width,
699            n,
700            config.grid_depth,
701            config.grid_resolution,
702            layer_height,
703        );
704        wind_field.fill_from_layers(&layers);
705
706        let pressure_cells = vec![
707            PressureCell::new_high(0.0, 0.0, 500_000.0),
708            PressureCell::new_low(200_000.0, 100_000.0, 300_000.0),
709        ];
710
711        let jet_streams = if config.enable_jet_streams {
712            vec![JetStream::polar(true), JetStream::subtropical()]
713        } else {
714            vec![]
715        };
716
717        Self {
718            config,
719            layers,
720            temperature_profile: temp_profile,
721            humidity_map,
722            wind_field,
723            pressure_cells,
724            jet_streams,
725            fog_layers: vec![],
726            surface_altitude_m,
727            haze_extinction: 0.002,
728            time_accum: 0.0,
729            noise_offset: 0.0,
730        }
731    }
732
733    // ── Tick ─────────────────────────────────────────────────────────────────
734
735    pub fn tick(&mut self, dt: f32, surface_temp_k: f32, time_of_day: f32) {
736        self.time_accum  += dt;
737        self.noise_offset += dt * 0.003;
738
739        // Update temperature profile
740        self.temperature_profile.set_surface_temp(surface_temp_k);
741
742        // Sync layer temperatures
743        for layer in &mut self.layers {
744            let mid = (layer.altitude_base_m + layer.altitude_top_m) * 0.5;
745            layer.temperature_k = self.temperature_profile.sample_at(mid);
746            layer.pressure_pa   = isa_pressure_at_altitude(mid);
747            layer.density_kg_m3 = layer.pressure_pa / (R_DRY * layer.temperature_k.max(1.0));
748            layer.update_cloud_fraction();
749        }
750
751        // Advance pressure cells
752        for cell in &mut self.pressure_cells {
753            cell.tick(dt);
754        }
755
756        // Advance jet streams
757        for js in &mut self.jet_streams {
758            js.tick(dt);
759        }
760
761        // Wind: build layer winds from pressure gradient + jet stream
762        self.rebuild_layer_winds(time_of_day);
763
764        // Refill wind field every few seconds
765        if self.time_accum >= 5.0 {
766            self.time_accum = 0.0;
767            let layers_snapshot = self.layers.clone();
768            self.wind_field.fill_from_layers(&layers_snapshot);
769        }
770
771        // Humidity advection — use surface wind
772        let surf_wind = self.surface_wind();
773        let adv_scale = dt / self.config.grid_resolution;
774        self.humidity_map.advect(surf_wind.x * adv_scale, surf_wind.z * adv_scale);
775
776        // Evaporation from surface (stronger in daytime)
777        let day_factor = smoothstep(6.0, 12.0, time_of_day) * smoothstep(20.0, 15.0, time_of_day);
778        self.humidity_map.evaporate(0.00002 * day_factor * dt);
779
780        // Precipitation sink where humidity is high
781        self.humidity_map.precipitate(0.85, 0.001 * dt);
782
783        // Fog lifecycle
784        self.update_fog(surface_temp_k, dt);
785    }
786
787    fn rebuild_layer_winds(&mut self, _time_of_day: f32) {
788        let sample_x = 0.0_f32;
789        let sample_z = 0.0_f32;
790
791        // Compute pressure gradient at origin
792        let dp_dx = self.pressure_gradient_x(sample_x, sample_z);
793        let dp_dz = self.pressure_gradient_z(sample_x, sample_z);
794        let grad = BarometricGradient { grad_x: dp_dx, grad_z: dp_dz };
795
796        for layer in &mut self.layers {
797            let alt = (layer.altitude_base_m + layer.altitude_top_m) * 0.5;
798            let density = layer.density_kg_m3.max(0.01);
799            // Simplified Coriolis parameter (mid-latitude)
800            let f_cor = 1e-4_f32;
801            let geo_wind = grad.geostrophic_wind(density, f_cor);
802
803            // Add turbulence noise
804            let t_scale = alt * 0.0001 + 0.5;
805            let turb = value_noise_2d(t_scale, 0.1) * 2.0 - 1.0;
806
807            layer.wind = Vec3::new(
808                geo_wind.x + turb * 1.5,
809                0.0,
810                geo_wind.z + turb * 0.8,
811            );
812        }
813
814        // Add jet-stream contribution to upper layers
815        for layer in &mut self.layers {
816            let alt = (layer.altitude_base_m + layer.altitude_top_m) * 0.5;
817            let mut js_contrib = Vec3::ZERO;
818            for js in &self.jet_streams {
819                js_contrib = js_contrib.add(js.sample(sample_x, alt, sample_z));
820            }
821            layer.wind = layer.wind.add(js_contrib);
822        }
823    }
824
825    fn pressure_gradient_x(&self, x: f32, z: f32) -> f32 {
826        let dx = 1_000.0_f32;
827        let p1 = self.total_pressure_at(x + dx, z);
828        let p0 = self.total_pressure_at(x - dx, z);
829        (p1 - p0) / (2.0 * dx)
830    }
831
832    fn pressure_gradient_z(&self, x: f32, z: f32) -> f32 {
833        let dz = 1_000.0_f32;
834        let p1 = self.total_pressure_at(x, z + dz);
835        let p0 = self.total_pressure_at(x, z - dz);
836        (p1 - p0) / (2.0 * dz)
837    }
838
839    fn total_pressure_at(&self, x: f32, z: f32) -> f32 {
840        let mut p = ISA_PRESSURE_PA;
841        for cell in &self.pressure_cells {
842            let sample = cell.sample(x, z);
843            p += sample - ISA_PRESSURE_PA;
844        }
845        // Add noise-based mesoscale variation
846        p += (fbm_2d(x * 0.000_01, z * 0.000_01, 3, 2.0, 0.5) - 0.5) * 400.0;
847        p
848    }
849
850    fn update_fog(&mut self, surface_temp_k: f32, dt: f32) {
851        let surface_humidity = self.surface_humidity();
852        let dew_point = {
853            let tc = surface_temp_k - 273.15;
854            let rh = surface_humidity.clamp(0.001, 1.0);
855            let a = 17.625_f32;
856            let b = 243.04_f32;
857            let alpha = (a * tc / (b + tc)) + rh.ln();
858            b * alpha / (a - alpha) + 273.15
859        };
860
861        // Radiation fog forms when surface temp ≈ dew point at night
862        let temp_spread = surface_temp_k - dew_point;
863        if temp_spread < 2.5 && surface_humidity > 0.8 {
864            if self.fog_layers.is_empty() {
865                self.fog_layers.push(FogLayer::radiation(0.0, 150.0, 0.01));
866            } else {
867                for fog in &mut self.fog_layers {
868                    fog.intensify(0.0005 * dt);
869                }
870            }
871        } else {
872            for fog in &mut self.fog_layers {
873                fog.dissipate(0.0003 * dt);
874            }
875            self.fog_layers.retain(|f| f.extinction > 1e-5);
876        }
877    }
878
879    // ── Public Query Methods ──────────────────────────────────────────────────
880
881    /// Return surface (layer 0) relative humidity [0,1].
882    pub fn surface_humidity(&self) -> f32 {
883        self.humidity_map.sample(
884            (self.config.grid_width / 2) as f32,
885            (self.config.grid_depth / 2) as f32,
886        )
887    }
888
889    /// Return surface pressure (Pa).
890    pub fn surface_pressure(&self) -> f32 {
891        self.total_pressure_at(0.0, 0.0)
892    }
893
894    /// Return surface wind velocity (m/s).
895    pub fn surface_wind(&self) -> Vec3 {
896        if self.layers.is_empty() { return Vec3::ZERO; }
897        self.layers[0].wind
898    }
899
900    /// Sample wind at world-space position.
901    pub fn wind_at(&self, wx: f32, wy: f32, wz: f32) -> [f32; 3] {
902        let base_wv = self.wind_field.sample_world(wx, wy, wz);
903        let mut wind = base_wv.velocity;
904        // Add jet stream
905        for js in &self.jet_streams {
906            wind = wind.add(js.sample(wx, wy, wz));
907        }
908        // Turbulence
909        let t = self.noise_offset;
910        let turb = Vec3::new(
911            (fbm_2d(wx * 0.0005 + t, wz * 0.0005, 3, 2.0, 0.5) * 2.0 - 1.0) * self.config.turbulence_scale,
912            0.0,
913            (fbm_2d(wz * 0.0005 + t + 100.0, wx * 0.0005, 3, 2.0, 0.5) * 2.0 - 1.0) * self.config.turbulence_scale * 0.5,
914        );
915        wind = wind.add(turb);
916        [wind.x, wind.y, wind.z]
917    }
918
919    /// Compute meteorological visibility from the surface.
920    pub fn compute_visibility(&self) -> VisibilityResult {
921        // Total extinction coefficient = haze + fog
922        let mut ext = self.haze_extinction;
923        let mut limiter = VisibilityLimiter::Haze;
924
925        for fog in &self.fog_layers {
926            let fog_ext = fog.extinction_at(self.surface_altitude_m + 1.5); // observer height
927            if fog_ext > ext {
928                ext = fog_ext;
929                limiter = VisibilityLimiter::Fog;
930            }
931        }
932
933        if ext < 1e-8 {
934            return VisibilityResult {
935                distance_m: 50_000.0,
936                optical_depth: 0.0,
937                limiting_factor: VisibilityLimiter::Clear,
938            };
939        }
940
941        // Koschmieder's law: V = 3.912 / σ
942        let dist = (3.912 / ext).min(50_000.0);
943        let opt_depth = ext * dist;
944        VisibilityResult { distance_m: dist, optical_depth: opt_depth, limiting_factor: limiter }
945    }
946
947    /// Return the cloud fraction at a given altitude.
948    pub fn cloud_fraction_at(&self, alt_m: f32) -> f32 {
949        for layer in &self.layers {
950            if alt_m >= layer.altitude_base_m && alt_m < layer.altitude_top_m {
951                return layer.cloud_fraction;
952            }
953        }
954        0.0
955    }
956
957    /// Integrate optical depth between two altitudes (for sky rendering).
958    pub fn optical_depth_between(&self, alt_low: f32, alt_high: f32) -> f32 {
959        let steps = 8usize;
960        let dh = (alt_high - alt_low) / steps as f32;
961        let mut od = 0.0_f32;
962        for i in 0..steps {
963            let h = alt_low + (i as f32 + 0.5) * dh;
964            let cloud = self.cloud_fraction_at(h) * 0.05;
965            let haze = self.haze_extinction * (-h / 8_500.0_f32).exp();
966            let fog_ext: f32 = self.fog_layers.iter().map(|f| f.extinction_at(h)).sum();
967            od += (cloud + haze + fog_ext) * dh;
968        }
969        od
970    }
971
972    /// Return temperature (K) at world altitude.
973    pub fn temperature_at(&self, alt_m: f32) -> f32 {
974        self.temperature_profile.sample_at(alt_m + self.surface_altitude_m)
975    }
976}
977
978// ── AtmosphereSimulator ───────────────────────────────────────────────────────
979
980/// Higher-level simulator that manages an Atmosphere and exposes query helpers.
981#[derive(Debug, Clone)]
982pub struct AtmosphereSimulator {
983    pub atmo: Atmosphere,
984    /// Cached per-frame visibility result.
985    pub last_visibility: VisibilityResult,
986    /// Accumulated precipitation potential from humidity excess.
987    pub precip_potential: f32,
988    /// Named weather stations for point queries.
989    stations: HashMap<String, [f32; 3]>,
990}
991
992impl AtmosphereSimulator {
993    pub fn new(surface_alt: f32) -> Self {
994        let atmo = Atmosphere::new(surface_alt);
995        let vis  = atmo.compute_visibility();
996        Self {
997            atmo,
998            last_visibility: vis,
999            precip_potential: 0.0,
1000            stations: HashMap::new(),
1001        }
1002    }
1003
1004    pub fn tick(&mut self, dt: f32, surface_temp_k: f32, time_of_day: f32) {
1005        self.atmo.tick(dt, surface_temp_k, time_of_day);
1006        self.last_visibility = self.atmo.compute_visibility();
1007        // Accumulate precip potential
1008        let hum = self.atmo.surface_humidity();
1009        if hum > 0.85 {
1010            self.precip_potential = (self.precip_potential + (hum - 0.85) * dt * 0.1).min(1.0);
1011        } else {
1012            self.precip_potential = (self.precip_potential - dt * 0.005).max(0.0);
1013        }
1014    }
1015
1016    /// Register a named weather station.
1017    pub fn add_station(&mut self, name: impl Into<String>, x: f32, y: f32, z: f32) {
1018        self.stations.insert(name.into(), [x, y, z]);
1019    }
1020
1021    /// Query all weather parameters at a named station.
1022    pub fn station_report(&self, name: &str) -> Option<StationReport> {
1023        let pos = self.stations.get(name)?;
1024        let [x, y, z] = *pos;
1025        let wind = self.atmo.wind_at(x, y, z);
1026        let temp_k = self.atmo.temperature_at(y);
1027        let pressure = isa_pressure_at_altitude(y + self.atmo.surface_altitude_m);
1028        let vis = self.last_visibility.distance_m;
1029        let cloud = self.atmo.cloud_fraction_at(y);
1030        Some(StationReport { x, y, z, wind, temp_k, pressure_pa: pressure, visibility_m: vis, cloud_fraction: cloud })
1031    }
1032
1033    /// Return true if conditions favour precipitation.
1034    pub fn precipitation_likely(&self) -> bool {
1035        self.precip_potential > 0.3 && self.atmo.surface_humidity() > 0.75
1036    }
1037}
1038
1039/// A weather report at a named station.
1040#[derive(Debug, Clone, Copy)]
1041pub struct StationReport {
1042    pub x: f32, pub y: f32, pub z: f32,
1043    pub wind: [f32; 3],
1044    pub temp_k: f32,
1045    pub pressure_pa: f32,
1046    pub visibility_m: f32,
1047    pub cloud_fraction: f32,
1048}
1049
1050impl StationReport {
1051    pub fn temp_celsius(&self) -> f32 { self.temp_k - 273.15 }
1052    pub fn wind_speed_ms(&self) -> f32 {
1053        let [wx, wy, wz] = self.wind;
1054        (wx * wx + wy * wy + wz * wz).sqrt()
1055    }
1056    pub fn beaufort_number(&self) -> u8 {
1057        let spd = self.wind_speed_ms();
1058        match spd as u32 {
1059            0     => 0,
1060            1..=5  => 1,
1061            6..=11 => 2,
1062            12..=19 => 3,
1063            20..=28 => 4,
1064            29..=38 => 5,
1065            39..=49 => 6,
1066            50..=61 => 7,
1067            62..=74 => 8,
1068            75..=88 => 9,
1069            89..=102 => 10,
1070            103..=117 => 11,
1071            _ => 12,
1072        }
1073    }
1074}