← Docs

How a windgram is computed

A windgram is a time–height chart of a soaring day: hours across, altitude up, and at every intersection the wind, temperature, moisture, and derived lift a pilot cares about. This pipeline builds one per catalogued launch from each new run of a numerical weather model and publishes it as JSON for anyone to render.

The derivation is a port of canadarasp's windgram, with our thanks — constant for constant, deliberately. Pilots calibrate against the original, and a windgram that disagreed with it for the same site and hour would read as broken rather than rigorous. This page explains both halves: how to read the picture, and how the numbers behind it are made. The ground truth for everything below is windgrams/windgram.py.

Reading a windgram

The model's ground is not your ground

Every profile starts from a weather model on a grid — 2.5 km cells for the HRDPS baseline. At that resolution mountains keep their shape but lose their sharpness: the model's ground at a launch sits below the real launch because the grid smooths the mountain down. The chart floor and every "surface" value live at that model ground, which is why they can read a couple hundred metres under where you are standing. Each profile carries both numbers — modelElevationM and siteAltitudeM — so a renderer can show the discrepancy instead of hiding it. (For a concrete example of how much this matters across resolutions, see the model bestiary.)

The derived quantities

The model does not forecast "lift". Every soaring number on the chart is derived from raw model fields, the same way canadarasp derives them:

Why usable lift rides above the boundary layer

At first glance it looks like a bug: the usable-lift line sits consistently above the boundary-layer top, as if lift outlives the layer that makes it. It is deliberate.

In the 1980s, Lenschow and Stephens flew instrumented aircraft through convective boundary layers and fit a curve to the updrafts they measured. The average updraft at height z inside a boundary layer of depth D came out near:

w = w* × 1.34 × (z/D)^⅓ × (1 − 0.8·z/D)

canadarasp multiplies that coefficient by roughly three — giving the 4.0 in the code — because the core of a thermal is about three times stronger than the average across it. Their "usable lift top" (hcrit) is then the height where even that best core no longer out-climbs a 1 m/s sink rate, capped at cloud base because good pilots don't fly in cloud.

So the two lines answer different questions. The boundary-layer line says where average mixing stops — where a dry parcel from the surface loses its buoyancy. The usable-lift line says how high the best core of the day stays worth turning in — and cores genuinely overshoot the mixed-layer top before they die, so lift-top above boundary-layer is the model's honest opinion, not an error. Read it accordingly: an average pilot centring imperfect lift should expect the truth to sit between the two lines, closer to the boundary-layer top on an ordinary day.

Open question. Neither the average profile nor the ×3 core factor has been validated against any particular valley. Pilots record IGC tracks; a season of forecast lift-top against actually-achieved altitudes would give a site its own correction factor — at which point nobody would be choosing between forty-year-old curve fits. The history archives exist so that study is possible.

The computation, step by step

Inputs

Each build collects, for every catalogued site and every forecast hour:

That is 54 field–level combinations per hour. How they are fetched differs per model — WCS crops, GRIB2 byte ranges, whole files — but every builder ends at the same source-hour shape and calls the same derive_windgram_profile.

Level filtering

Pressure levels are kept only when their height is finite and at least 20 m above the model's terrain. In mountain terrain the 925 hPa surface (and sometimes more) is underground; it is discarded rather than plotted below the chart floor.

Lapse rate and cloud flags

For each kept level, the lapse rate to the next level up is (T_next − T) / (z_next − z) × 304.8 — °C per 1000 ft, negative when temperature falls with height in the plotted convention. A level is flagged cloud when its dew point depression is under 0.5 °C: the model has that layer at or near saturation.

Cloud base

The lifted condensation level, by the classic surface approximation: 121 m of climb per degree of surface dew point depression, above the model terrain.

cloudBaseM = modelElevationM + max(0, dewPointDepressionC) × 121

Boundary-layer depth

A parcel at the surface temperature is lifted dry-adiabatically (0.0098 °C/m). Walking up the sorted levels, the first level where the parcel is no longer warmer than the environment brackets the boundary-layer top; the exact crossing height is solved by intersecting the parcel line with the environmental lapse between that level and the one below. If the parcel is warmer than every level, the top of the sounding is used — a deliberately honest degenerate case for very unstable profiles over a shallow sounding.

Thermal velocity w*

Deardorff's velocity scale, with canadarasp's exact constants:

Q_v  = SHTFL + 0.000245268 × T_K × LHTFL          # virtual heat flux, W/m²
θ    = T_K × (1015 / p_first)^0.28482              # potential temperature
w*   = ((0.0075516 / θ) × Q_v × D)^(1/3)           # D = BL depth in metres

The odd-looking constants are physical quantities folded together: 0.000245268 ≈ 0.61·c_p/L_v converts latent heat flux into its buoyancy contribution (moist air is lighter), 0.0075516 ≈ g/(ρ·c_p) converts W/m² into a kinematic flux, and the exponent approximates R/c_p for dry air. When the virtual heat flux is zero or negative — night, overcast, rain — w* is zero and there is no thermal forecast, by construction.

Usable lift top (hcrit)

Starting from w* and the boundary-layer depth D:

  1. If the peak core updraft w* × 2.02 cannot beat the 1 m/s sink rate, there is no usable lift; the field is null. (2.02 is just the maximum of the ×3-core Lenschow–Stephens profile 4·x^⅓·(1 − 0.8x).)
  2. Otherwise walk up the levels from 0.25·D, evaluating the core updraft w* × 4 × (z/D)^⅓ × (1 − 0.8·z/D) at each level's height.
  3. The first level where the core drops to the sink rate brackets hcrit; linear interpolation against the previous evaluation pins it down.
  4. The result is capped at cloud base — and if the walk reaches cloud base first, cloud base is the answer.
  5. If the core never drops below the sink rate within the sounding, the usable top is the boundary-layer top itself (still capped at cloud base).

Smoothing

cloudBaseM and usableLiftTopM are the two derived series noisy enough to flicker hour-to-hour, so each interior hour is smoothed with a 1‑2‑1 kernel — (previous + 2×current + next) / 4 — applied only where the neighbours are exactly one hour away and none of the three values is missing. Everything else is published raw.

The flyable-day window

Profiles carry only hours a pilot can fly: 07:00–21:00 in the sites' local time (America/Vancouver for the founding catalogue), and only for days that have at least five such hours in the run — a run's ragged first or last day is dropped rather than shown as a two-hour stub. If the filter would empty the profile entirely, the unfiltered hours are kept: a degenerate run is published honestly rather than not at all.

Provenance of the constants

Every constant above — the 121 m/°C cloud base, the 0.28482 exponent, the 4.0 core coefficient, the 1 m/s sink rate, the 2.02 threshold — is canadarasp's, ported exactly. The Python module rebuilt the committed output of the original TypeScript implementation to within one double ULP before it replaced it, and the test suite keeps that parity (see engineering the pipeline). Improving on these constants is a real prospect — but it belongs in a new product with its own name, not in silent drift of this one. See where this goes next.