winds-engine 0.3.0

Deterministic wind profiles and radiosonde validation for trajectory simulation
Documentation

winds-engine

Deterministic wind profiles and radiosonde validation for trajectory simulation — the wind data pipeline for rocket modeling, consumed by rockets-engine. Kernel-side wind modeling (the WindField trait, profile types, analytic profiles) is staged here pending promotion to ballistics-engine's wind module; see docs/winds-engine-plan.md.

use winds_engine::{Sounding, WindField, validate};

// A measured sounding in the line-based interchange format. Heights are
// metres above sea level; this station's surface report is at 287 m.
let sounding = Sounding::parse("287 0.0 0\n305 3.09 125\n610 3.60 145\n")?;
// The profile keeps those heights, so it is read with y = height above sea
// level, the frame rockets-engine uses.
let profile = sounding.to_profile_field()?;
let wind = profile.wind(0.0, [0.0, 500.0, 0.0]); // 500 m above sea level
// Or score a candidate profile against the measurement.
let report = validate::validate(&profile, &sounding);
  • WindField trait: stateless, deterministic wind(time, position) — the same sampling pattern trajectory integrators already rely on.

  • ProfileField (piecewise-linear, circular direction interpolation), ConstantField, LogProfile, EkmanProfile.

  • Sounding: radiosonde interchange format (tested against a real 115-level KMPX sounding from the IEM RAOB archive). Heights are above sea level and stay that way in to_profile_field(), so the profile reads directly in a frame whose y is altitude above sea level, as in rockets-engine (with BallisticForces and in its flat-frame CLI, whose ground sits at --ground-altitude-m, sea level by default).

  • Datum conversion: LogProfile and EkmanProfile take y as height above the ground, where they are calm. This crate's staged types have no offset wrapper; ballistics-engine's HeightOffset (from its next release, which rockets-engine's =0.48.0 pin does not have) wraps only ballistics-engine's own WindField, not this crate's. So a caller in a sea-level frame either reads them at y - ground elevation at its single call site, or builds ballistics-engine's ProfileField/LogProfile/EkmanProfile from the same values and wraps those in HeightOffset (offset = the height of the caller's y = 0 in the field's datum: minus the ground elevation for a log or Ekman profile, none for a sea-level sounding). Convert in one place only: sounding heights stay above sea level, and shifting them as well would move every wind by the site elevation.

  • validate: speed/direction RMSE and per-layer errors vs a sounding.

  • Relationship to ballistics-engine: these types are this crate's own. ballistics-engine's wind fields (unreleased) were adapted from them and differ: WindField::wind takes and returns nalgebra::Vector3<f64> and there is no Wind type, ConstantField is a tuple struct over that vector, LogProfile/EkmanProfile keep their fields private behind getters, and constructors return WindFieldError. Moving winds-engine onto them changes this crate's public API (the example above, validate()'s signature), so it is a deliberate breaking release, not a drop-in re-export.

  • grib (feature): decode GRIB2 forecast files (GFS isobaric HGT/UGRD/VGRD) into a ProfileField at a point — bilinear-interpolated, offline and deterministic.

  • download (feature): fetch live forecasts — GFS via the NOAA NOMADS filter script (server-side subsetting) or HRRR's 3 km CONUS grids via byte-range requests against the AWS mirror's index files — the only networked code in the crate.

Zero dependencies by default (grib adds the grib crate, download adds ureq); no wall clock or unseeded randomness anywhere — cycle date and forecast hour are explicit parameters.

MIT licensed (see LICENSE).