winds-engine 0.4.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](docs/winds-engine-plan.md).

```rust
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.

- `terrain`: a diagnostic mass-consistent terrain wind — a `HeightGrid`
  (ASCII DEM + raw arrays) plus `TerrainFlowField`, which adjusts a base
  field to satisfy 2-D horizontal mass conservation over terrain,
  optionally constrained by surface wind monitors.

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).