winds-engine v0.1.0

Deterministic wind profiles and radiosonde validation for trajectory simulation, in Rust.

# Cargo.toml (not on crates.io yet)
winds-engine = { git = "https://github.com/ajokela/winds-engine" }

What it does

Zero dependencies. No network, no wall clock, no unseeded randomness.

Conventions

Heights

Sounding heights are metres above sea level, and to_profile_field() keeps them there, so the profile reads directly in a frame whose y is altitude above sea level, like rockets-engine's. The layers in validate use the same sea-level heights.

LogProfile and EkmanProfile take y as height above the ground, where they are calm. In a sea-level frame, read them at y − ground elevation. This crate has no offset wrapper, so convert in one place only: shifting the sounding too would move every wind by the site elevation.

Example

use winds_engine::analytic::LogProfile;
use winds_engine::{validate, ProfileField, ProfileLevel, Sounding, WindField};

fn main() -> winds_engine::Result<()> {
    // altitude_m speed_mps direction_from_deg; heights above sea level
    let sounding = Sounding::parse("287 0.0 0\n305 3.09 125\n610 3.60 145\n")?;

    // The profile keeps those heights: read it at y = altitude above sea level.
    let measured = sounding.to_profile_field()?;
    let wind = measured.wind(0.0, [0.0, 500.0, 0.0]);
    let (speed, from) = (wind.speed_mps(), wind.direction_from_deg());
    println!("{speed:.2} m/s from {from:.0} deg");

    // Score a candidate profile, on the same sea-level heights, against it.
    let candidate = ProfileField::new(vec![
        ProfileLevel { altitude_m: 287.0, speed_mps: 0.0, direction_from_deg: 0.0 },
        ProfileLevel { altitude_m: 610.0, speed_mps: 4.0, direction_from_deg: 140.0 },
    ])?;
    let report = validate::validate(&candidate, &sounding);
    println!("speed RMSE {:.2} m/s", report.speed_rmse_mps);

    // A log law is calm at the ground: convert the datum here, once.
    let ground_m = 287.0;
    let log = LogProfile::new(8.0, 10.0, 0.03, 270.0)?;
    let low = log.wind(0.0, [0.0, 500.0 - ground_m, 0.0]);
    println!("{:.2} m/s", low.speed_mps());
    Ok(())
}

Needs Rust 1.89 or newer. The README walks through the same sounding.

Where it fits

Status

Version 0.1.0, staging. Not published on crates.io, so there is no docs.rs page; use it as a git dependency (above). It is a library only, with no CLI and no browser terminal.

Planned, not built: GRIB2 forecast ingestion (HRRR/GFS), with only the download step behind an opt-in feature, and more sounding validation days.

Get it