use crate::atmosphere::{self, AtmosphericState};
use crate::error::{BadalError, Result};
use pravash::grid::{BoundaryCondition, FluidGrid, GridConfig};
use pravash::shallow::ShallowWater;
#[derive(Debug, Clone, Copy)]
pub struct AtmoFlowConfig {
pub cell_size_m: f64,
pub dt: f64,
pub viscosity: f64,
pub vorticity_confinement: f64,
pub smagorinsky_cs: f64,
}
impl Default for AtmoFlowConfig {
fn default() -> Self {
Self {
cell_size_m: 1000.0,
dt: 1.0,
viscosity: 1.5e-5,
vorticity_confinement: 0.3,
smagorinsky_cs: 0.15,
}
}
}
#[must_use]
pub fn grid_config(state: &AtmosphericState, config: &AtmoFlowConfig) -> GridConfig {
let mut gc = GridConfig::default();
gc.dt = config.dt;
gc.viscosity = config.viscosity;
gc.boundary = BoundaryCondition::FreeSlip;
gc.vorticity_confinement = config.vorticity_confinement;
gc.buoyancy_alpha = -0.01; gc.ambient_density = state.temperature_k();
gc.use_bfecc = true;
gc.smagorinsky_cs = config.smagorinsky_cs;
gc.use_multigrid = true;
gc
}
pub fn atmospheric_grid(
nx: usize,
ny: usize,
state: &AtmosphericState,
config: &AtmoFlowConfig,
) -> Result<FluidGrid> {
let grid = FluidGrid::new(nx, ny, config.cell_size_m).map_err(|e| {
BadalError::ComputationError(format!("failed to create atmospheric grid: {e}"))
})?;
let mut grid = grid;
for y in 0..ny {
let altitude = state.altitude_m() + (y as f64) * config.cell_size_m;
let temp = atmosphere::standard_temperature(altitude);
for x in 0..nx {
let i = y * nx + x;
grid.density[i] = temp;
}
}
Ok(grid)
}
pub fn apply_coriolis(grid: &mut FluidGrid, latitude_rad: f64, dt: f64) {
let f = crate::wind::coriolis_parameter(latitude_rad);
let n = grid.vx.len();
for i in 0..n {
let u = grid.vx[i];
let v = grid.vy[i];
grid.vx[i] += f * v * dt;
grid.vy[i] -= f * u * dt;
}
}
pub fn apply_pressure_gradient(
grid: &mut FluidGrid,
pressure_field: &[f64],
density: f64,
dt: f64,
) -> Result<()> {
let nx = grid.nx;
let ny = grid.ny;
if pressure_field.len() != nx * ny {
return Err(BadalError::ComputationError(format!(
"pressure field size {} != grid size {}",
pressure_field.len(),
nx * ny
)));
}
if density <= 0.0 {
return Err(BadalError::InvalidPressure(
"density must be positive".into(),
));
}
let dx = grid.dx;
let inv_rho_dx = 1.0 / (density * dx);
for y in 1..ny - 1 {
for x in 1..nx - 1 {
let i = y * nx + x;
let dp_dx = (pressure_field[i + 1] - pressure_field[i - 1]) * 0.5;
let dp_dy = (pressure_field[i + nx] - pressure_field[i - nx]) * 0.5;
grid.vx[i] -= dp_dx * inv_rho_dx * dt;
grid.vy[i] -= dp_dy * inv_rho_dx * dt;
}
}
Ok(())
}
pub fn flood_from_rainfall(
nx: usize,
ny: usize,
cell_size_m: f64,
rain_rate_mm_hr: &[f64],
duration_hours: f64,
) -> Result<ShallowWater> {
if rain_rate_mm_hr.len() != nx * ny {
return Err(BadalError::ComputationError(format!(
"rain field size {} != grid size {}",
rain_rate_mm_hr.len(),
nx * ny
)));
}
let mut sw = ShallowWater::new(nx, ny, cell_size_m, 0.0).map_err(|e| {
BadalError::ComputationError(format!("failed to create shallow water grid: {e}"))
})?;
for (h, &rate) in sw.height.iter_mut().zip(rain_rate_mm_hr.iter()) {
*h = rate.max(0.0) * duration_hours / 1000.0;
}
Ok(sw)
}
pub fn add_rainfall(sw: &mut ShallowWater, rain_rate_mm_hr: &[f64], dt_hours: f64) -> Result<()> {
let n = sw.nx * sw.ny;
if rain_rate_mm_hr.len() != n {
return Err(BadalError::ComputationError(format!(
"rain field size {} != grid size {}",
rain_rate_mm_hr.len(),
n
)));
}
for (h, &rate) in sw.height.iter_mut().zip(rain_rate_mm_hr.iter()) {
*h += rate.max(0.0) * dt_hours / 1000.0;
}
Ok(())
}
#[must_use]
pub fn extract_wind_field(grid: &FluidGrid) -> (Vec<f64>, Vec<f64>) {
(grid.vx.clone(), grid.vy.clone())
}
#[must_use]
pub fn extract_wind_speed_direction(grid: &FluidGrid) -> (Vec<f64>, Vec<f64>) {
let n = grid.vx.len();
let mut speed = Vec::with_capacity(n);
let mut direction = Vec::with_capacity(n);
for i in 0..n {
speed.push(crate::wind::wind_speed(grid.vx[i], grid.vy[i]));
direction.push(crate::wind::wind_direction(grid.vx[i], grid.vy[i]));
}
(speed, direction)
}
#[cfg(test)]
mod tests {
use super::*;
fn sea_level() -> AtmosphericState {
AtmosphericState::sea_level()
}
#[test]
fn grid_config_creates_valid() {
let state = sea_level();
let cfg = grid_config(&state, &AtmoFlowConfig::default());
assert_eq!(cfg.dt, 1.0);
assert!(cfg.use_multigrid);
assert!(cfg.use_bfecc);
}
#[test]
fn atmospheric_grid_creates_valid() {
let state = sea_level();
let grid = atmospheric_grid(16, 16, &state, &AtmoFlowConfig::default()).unwrap();
assert_eq!(grid.nx, 16);
assert_eq!(grid.ny, 16);
assert!((grid.density[0] - 288.15).abs() < 1.0);
}
#[test]
fn atmospheric_grid_temperature_decreases_with_height() {
let state = sea_level();
let grid = atmospheric_grid(8, 8, &state, &AtmoFlowConfig::default()).unwrap();
let bottom = grid.density[0]; let top = grid.density[7 * 8]; assert!(
top < bottom,
"temperature should decrease with height: bottom={bottom}, top={top}"
);
}
#[test]
fn atmospheric_grid_too_small_errors() {
let state = sea_level();
assert!(atmospheric_grid(2, 2, &state, &AtmoFlowConfig::default()).is_err());
}
#[test]
fn coriolis_deflects_velocity() {
let state = sea_level();
let mut grid = atmospheric_grid(8, 8, &state, &AtmoFlowConfig::default()).unwrap();
grid.vx.fill(10.0);
grid.vy.fill(0.0);
let vy_before = grid.vy[0];
apply_coriolis(&mut grid, 45.0_f64.to_radians(), 1.0);
assert!(
grid.vy[0] < vy_before,
"Coriolis should deflect eastward wind southward in NH"
);
}
#[test]
fn pressure_gradient_accelerates_flow() {
let state = sea_level();
let mut grid = atmospheric_grid(8, 8, &state, &AtmoFlowConfig::default()).unwrap();
let nx = grid.nx;
let ny = grid.ny;
let mut pfield = vec![0.0; nx * ny];
for y in 0..ny {
for x in 0..nx {
pfield[y * nx + x] = 101_325.0 - (x as f64) * 100.0;
}
}
let vx_before = grid.vx[grid.nx / 2 + (grid.ny / 2) * grid.nx];
apply_pressure_gradient(&mut grid, &pfield, 1.225, 1.0).unwrap();
let vx_after = grid.vx[grid.nx / 2 + (grid.ny / 2) * grid.nx];
assert!(
vx_after > vx_before,
"pressure gradient should accelerate flow from high to low pressure"
);
}
#[test]
fn pressure_gradient_wrong_size_errors() {
let state = sea_level();
let mut grid = atmospheric_grid(8, 8, &state, &AtmoFlowConfig::default()).unwrap();
let pfield = vec![0.0; 10]; assert!(apply_pressure_gradient(&mut grid, &pfield, 1.225, 1.0).is_err());
}
#[test]
fn flood_from_rainfall_creates_valid() {
let rain = vec![10.0; 16]; let sw = flood_from_rainfall(4, 4, 100.0, &rain, 2.0).unwrap();
assert!((sw.height[0] - 0.02).abs() < 1e-6);
}
#[test]
fn flood_wrong_size_errors() {
let rain = vec![10.0; 10]; assert!(flood_from_rainfall(4, 4, 100.0, &rain, 1.0).is_err());
}
#[test]
fn add_rainfall_accumulates() {
let mut sw = ShallowWater::new(4, 4, 100.0, 0.0).unwrap();
let rain = vec![5.0; 16]; add_rainfall(&mut sw, &rain, 1.0).unwrap();
assert!((sw.height[0] - 0.005).abs() < 1e-6);
add_rainfall(&mut sw, &rain, 1.0).unwrap();
assert!((sw.height[0] - 0.01).abs() < 1e-6);
}
#[test]
fn extract_wind_field_returns_correct_size() {
let state = sea_level();
let grid = atmospheric_grid(8, 8, &state, &AtmoFlowConfig::default()).unwrap();
let (u, v) = extract_wind_field(&grid);
assert_eq!(u.len(), 64);
assert_eq!(v.len(), 64);
}
#[test]
fn extract_wind_speed_direction_works() {
let state = sea_level();
let mut grid = atmospheric_grid(8, 8, &state, &AtmoFlowConfig::default()).unwrap();
grid.vx.fill(5.0); grid.vy.fill(0.0);
let (speed, dir) = extract_wind_speed_direction(&grid);
assert!((speed[0] - 5.0).abs() < f64::EPSILON);
assert!((dir[0] - 270.0).abs() < 1.0);
}
}