Sign in

    moonwind

    Weather computation in pure MoonBit: humidity, apparent temperature, pressure and altitude, the Beaufort scale, wind barbs, and winds-aloft (FD) flight calculations

    weather
    meteorology
    humidity
    apparent-temperature
    pressure-altitude
    beaufort
    wind-barb
    aviation
    winds-aloft
    wind-triangle
    flight-planning
    Download zip
    Author
    Version
    0.1.1
    License
    Apache-2.0
    Last updated
    7 hours ago
    Downloads
    4

    #moonwind

    Weather computation in pure MoonBit: humidity, apparent temperature, pressure and altitude, the Beaufort scale and wind barbs — the questions anyone asks of a weather report — plus the flight side, the winds and temperatures aloft (FD) bulletins and the wind-triangle, runway and great-circle route calculations that turn them into headings, ground speeds and crosswind components. The two halves share one set of units and one standard atmosphere, so a dew point, a density altitude and a flight plan all come out of the same numbers. cmd/main is a demo that plans a route from the command line.

    ///|
    let bulletin = @moonwind.parse_bulletin(product_text)

    ///|
    let station = bulletin.station("ABR").unwrap()

    ///|
    let level = station.level_at(24000).unwrap() // wind and temperature

    #Bulletins

    parse_bulletin reads a winds and temperatures aloft product — the fixed-column text products that aviation weather services publish — into Bulletin, StationWinds, Level and Wind values. format_bulletin writes it back; parsing is lossless, so a parsed bulletin re-formats byte for byte.

    The wire format compresses the numbers, and the parser handles all of it:

    FieldEncoding
    Windddss: tens of degrees and knots. 9900 is light and variable
    Wind ≥ 100 kt50 is added to the direction and 100 taken off the speed
    Temperature+TT / -TT in °C
    Temperature above 24,000 ftalways negative, so the sign is dropped
    3,000 ftno temperature at all

    The column layout is derived from the FT header line rather than hard-coded, so the same code reads nine-level low-level products (FD1US1) and two-level high-level ones (FD8US7):

    FT 3000 6000 9000 12000 18000 24000 30000 34000 39000 ABR 1519 1618+06 1614+03 1514+01 1509-13 1612-25 260739 311344 331151 ACK 0623 1108+05 2805+03 3010+00 2642-10 2648-20 266535 256945 257756

    which reads as: ABR at 3,000 ft, wind 150° at 19 kt; at 24,000 ft, 160° at 12 kt and −25 °C; at 30,000 ft, 260° at 7 kt and −39 °C.

    #Between the levels

    A bulletin forecasts a handful of levels, and a flight plan wants the altitude it will actually fly, which usually falls between two of them. A station answers for any altitude between the levels it reports:

    ///|
    let wind = station.vector_at(4500).unwrap()

    ///|
    let temperature = station.temperature_at(7500) // Some(4.5)

    ///|
    let reported = station.vector_at(4500).unwrap().to_wind() // rounded to whole numbers

    The wind is interpolated as a vector rather than as a direction and a speed, because a wind does not turn evenly between two levels: LOU goes from 70° at 13 kt at 3,000 ft to 260° at 9 kt at 6,000 ft, and half way between them the two nearly cancel, leaving 2.2 kt from 49° — not the 165° at 11 kt that averaging the numbers alone would give. A light and variable level counts as no wind at all.

    WindVector is a wind as a velocity vector, u east and v north in knots, with direction, speed and to_wind to read a report back off it.

    Nothing is extrapolated: an altitude above the highest or below the lowest level a station reports is answered with None, and stations do not all report the same levels. DEN, high in the Rockies, starts at 9,000 ft because the levels below it are under the ground; the 3,000 ft column never carries a temperature, so temperature_at starts at 6,000 ft.

    #Units and the standard atmosphere

    The data model keeps the units the bulletins are written in — knots, feet, degrees Celsius — and units.mbt converts them: knots to metres per second, kilometres per hour or miles per hour, feet to metres, Celsius to Fahrenheit. It also carries the standard atmosphere, which is what a reported temperature is read against:

    ///|
    let standard = @moonwind.isa_temperature(24000.0) // -32.5488

    ///|
    let deviation = @moonwind.isa_deviation(-25.0, 24000.0) // 7.5488, warmer than standard

    #Humidity

    humidity.mbt reads the water in the air: the saturation vapour pressure at a temperature, the dew point of air at a temperature and a humidity, the relative humidity of air at a temperature and a dew point, the frost point against ice, the wet-bulb temperature, and the water a cubic metre holds.

    They are all read off one Magnus form, from Alduchov and Eskridge (1996) — 6.1094 · exp(17.625·T / (T + 243.04)) over water and 6.1121 · exp(22.587·T / (T + 273.86)) over ice, both in hectopascals — so a vapour pressure and a dew point are exact inverses of each other. The saturation vapour pressure at a temperature is also the actual vapour pressure of air whose dew point that temperature is, which is how the rest are read off it.

    ///|
    let dew = @moonwind.dew_point(20.0, 50.0) // 9.26 °C, the classic Magnus example

    ///|
    let back = @moonwind.relative_humidity(20.0, dew) // 50 %

    ///|
    let frost = @moonwind.frost_point(-10.0, 50.0) // -16.52, above its -18.47 dew point

    ///|
    let wet = @moonwind.wet_bulb_temperature(20.0, 50.0) // Some(13.70)

    ///|
    let grams = @moonwind.absolute_humidity(
    @moonwind.saturation_vapor_pressure(20.0),
    20.0,
    ) // 17.25 g/m³

    The wet-bulb temperature is Stull's fit (2011), worked out over 5 % to 99 % humidity and −20 °C to 50 °C. Outside that the function answers None rather than extrapolating, and saturated air is outside it as well: the fit drifts there, and the wet-bulb temperature of saturated air is the air temperature itself.

    A relative humidity has to be above zero and at most 100, and air cannot have a dew point above its own temperature. Both are refused with HumidityError rather than answered with a number that does not exist.

    #Apparent temperature

    apparent_temperature.mbt answers the other half of what a forecast is read for: not how warm the air is, but how warm it feels. Moving air carries heat away from skin, so a cold day feels colder in a wind; humid air cannot take sweat away, so a warm day feels warmer.

    The wind chill is the formula the National Weather Service and Environment Canada adopted in 2001, in the two sets of units it is published in — Fahrenheit and miles per hour in the United States, Celsius and kilometres per hour in Canada:

    ///|
    let american = @moonwind.wind_chill_fahrenheit(0.0, 15.0) // Some(-19.4), the chart's -19

    ///|
    let canadian = @moonwind.wind_chill_celsius(0.0, 20.0) // Some(-5.2)

    wind_chill_from_wind reads a reported Wind instead, converting the knots it is kept in, and answers a light and variable wind — the one with no direction — with None, as it does any temperature above 10 °C.

    The heat index is the American regression over humidity, the humidex the Canadian index of the same thing, and the apparent temperature the Bureau of Meteorology's "feels like", which counts the wind as well as the humidity:

    ///|
    let heat = @moonwind.heat_index_celsius(30.0, 50.0) // 31.05, 86 F at 50 %

    ///|
    let humid = @moonwind.humidex(30.0, 15.0) // 33.9, Environment Canada's example

    ///|
    let feels = @moonwind.apparent_temperature(30.0, 80.0, 10.0 / 3.6) // 35.24, the Bureau's

    Each index is read only where it was fitted, and answers None outside that rather than extrapolating into a number no service publishes: the wind chill at or below 50 °F (10 °C) with a wind above 3 mph (4.8 km/h), the heat index from 80 °F to 112 °F. The humidex and the apparent temperature are defined wherever their inputs are.

    A relative humidity has to be between 0 and 100 and a wind speed cannot be negative; both are refused with ApparentTemperatureError. The humidex reads its vapour pressure from the humidity module, so a dew point above the air temperature is refused there, with HumidityError.

    #Pressure and altitude

    pressure.mbt does the barometry: the units a pressure is reported in, the reduction to sea level that makes two stations comparable, the altimeter setting an aircraft's altimeter is read against, and the two altitudes that come out of the standard atmosphere.

    ///|
    let sea_level = @moonwind.mean_sea_level_pressure(955.0, 500.0, 20.0) // 1011.98 hPa

    ///|
    let station = @moonwind.station_pressure(1012.0, 500.0, 20.0) // 955.02 hPa, its inverse

    ///|
    let setting = @moonwind.altimeter_setting_hpa(954.61, 500.0) // 1013.25 hPa, a standard station at 500 m

    ///|
    let inches = @moonwind.altimeter_setting_inhg(954.61, 500.0) // 29.92, the same setting

    The sea-level reduction takes the weight of the air between the station and the sea back out of the reading, along the column that the station's own temperature implies — the best a station can say about a column it has one reading from. The altimeter setting is the same idea written as the barometric formula and takes no temperature at all: it is defined so that an altimeter standing on the field reads the elevation of the field.

    The other two altitudes are heights in the standard atmosphere rather than reductions of a station — where that pressure is found, and where that air density is found:

    ///|
    let pressure_altitude = @moonwind.pressure_altitude(850.0) // 4781.17 ft, the 850 hPa level

    ///|
    let hot_day = @moonwind.density_altitude(1013.25, 25.0) // 1200.0 ft, 120 ft per degree

    ///|
    let damp_day = @moonwind.density_altitude_with_dew_point(1013.25, 25.0, 15.0) // 1377.0 ft, the vapour counted too

    density_altitude is the rule of thumb a pilot reads off a chart, 120 ft for every degree the air is warmer than the standard atmosphere, and it reads a little high: the exact answer for dry air on a 25 °C day at sea level is 1,162 ft. density_altitude_with_dew_point reads the density instead, with the vapour in it counted through the virtual temperature — 218 ft of the answer on a saturated 15 °C day at sea level, 109 ft at 50 % humidity.

    The four conversions at the top of the module read a pressure in hectopascals, inches of mercury or millimetres of mercury, an altimeter setting being read aloud in inches over much of the world. An elevation above 44330.77 m — where the standard troposphere would reach absolute zero — has no altimeter setting and is refused with PressureError, and a dew point above the air temperature is refused with HumidityError, as it is by relative_humidity.

    #Beaufort and the wind barb

    The last two modules write a wind the way a forecast writes it: as a force on the Beaufort scale, and as a barb drawn on a station plot.

    ///|
    let breeze = @moonwind.Beaufort::from_knots(14.0) // force 4, "Moderate breeze"

    ///|
    let force = breeze.description() // "Moderate breeze"

    ///|
    let band = breeze.upper_bound_knots() // Some(17.0), where force 5 begins

    ///|
    let reported = @moonwind.Beaufort::from_wind(wind) // 150 at 14 kt is the same breeze

    Each force covers a band of speeds, closed at the bottom, and the bands are published in knots, in metres per second and in kilometres per hour — rounded separately, so they do not quite agree at the edges. 3.4 m/s is 6.61 kt, which the metres-per-second table calls force 3 and the knots table force 2. from_knots, from_meters_per_second and from_kilometers_per_hour each read their own table rather than converting, so a report is answered in the scale it was written against.

    A wind barb counts the speed in the increments a station plot has room for — a pennant for 50 knots, a full barb for 10, a half barb for 5 — and a calm is a circle with no staff:

    ///|
    let barb = @moonwind.WindBarb::from_wind(wind) // 270 at 25 kt

    ///|
    let feathers = (barb.pennants(), barb.full_barbs(), barb.half_barbs()) // (0, 2, 1)

    ///|
    let drawn = barb.to_unicode() // "──╌│"

    ///|
    let plain = barb.to_ascii() // "==-|", for a font with no pennant glyph

    The direction is held in tens of degrees, 0 meaning the wind has none and 36 meaning north, so a direction that comes in as 0° reads back out as 0°. The speed is drawn to the nearest 5 knots, which is all the feathers can say — a 12 kt wind is drawn as 10 — so a barb read back with to_wind carries the rounded speed.

    A negative wind speed is refused by both modules, with BeaufortError and WindBarbError: a wind does not blow backwards.

    #The wind triangle

    The wind that is forecast is not the wind the aeroplane flies through: it carries the aircraft with it. WindTriangle::solve takes the course to make good, the true airspeed and the wind, and gives back the heading to steer, the ground speed that results, and the components a pilot reads a wind in:

    ///|
    let triangle = @moonwind.WindTriangle::solve(90.0, 120.0, wind)

    ///|
    let heading = triangle.heading() // 097.99°, course plus the correction

    ///|
    let ground_speed = triangle.ground_speed() // 111.01 kt

    A headwind is positive when it comes from ahead, so a tailwind is negative, and a crosswind is positive when it comes from the right of the track. The wind correction angle has the same sign as the crosswind, so steering into the wind is steering by the correction angle.

    A wind does not have to come from a bulletin: Wind::new(Some(150), 14) is 150 degrees at 14 kt, and WindVector::from_wind turns a reported wind into the vector a triangle solves. The other way round, WindVector::to_wind reads a wind back off a vector, as a rounded direction and speed.

    A crosswind stronger than the airspeed has no solution — the aircraft cannot hold the course at any heading — and solve reports it with FlightError::CrosswindExceedsAirspeed rather than returning a heading that does not exist.

    #Runways

    A runway is named for its magnetic direction in tens of degrees, and the wind across it is what a crosswind limit is written against:

    ///|
    let runway = @moonwind.Runway::parse("27").unwrap()

    ///|
    let crosswind = runway.crosswind(wind) // positive from the right

    ///|
    let headwind = runway.headwind(wind) // negative for a tailwind

    ///|
    let other_end = runway.reciprocal().unwrap() // runway 09

    #Routes

    A route is a list of waypoints, and the path between two of them is a great circle. The course of a great circle drifts as it is flown — the one from New York to London leaves on 051 and arrives on 108 — so a long hop is cut into shorter legs, each with a course of its own:

    ///|
    let route = @moonwind.Route::new([
    @moonwind.Coordinate::new(45.0, -98.0),
    @moonwind.Coordinate::new(44.0, -96.0),
    @moonwind.Coordinate::new(43.0, -94.0),
    ])

    ///|
    let distance = route.distance() // 210.38 nm along the waypoints

    ///|
    let segments = route.segments() // a distance and a course for every hop

    ///|
    let shorter = route.subdivided(8) // every hop flown as eight shorter ones

    ///|
    let middle = route.points()[0].midpoint(route.points()[1])

    Coordinate does the geometry: the great-circle distance_to another position, the initial_course_to it, and interpolate and midpoint for the positions along the way. Distances are nautical miles on a sphere of the Earth's mean radius, and a path across the date line is taken the short way, so the course from 170 east to 170 west is 270 rather than 90.

    A route is planned against a forecast at one altitude and airspeed. Each leg is flown at the wind over its own middle, where a leg is most of the way through it:

    ///|
    let plan = route.plan(station, 12000, 120.0)

    ///|
    let legs = plan.legs() // each with its wind, heading, ground speed and time

    ///|
    let hours = plan.total_time_hours()

    ///|
    let distance = plan.total_distance()

    Route::plan reads one station's forecast at the level it reports for that altitude. Route::plan_with takes the wind from a function of the position and the altitude instead, so a forecast that varies over a route — the station nearest each leg, or a grid of forecast winds — is used where the leg is.

    A leg that has no forecast at that altitude is reported with FlightError::NoForecast, and a leg the wind beats — a headwind stronger than the airspeed, so the ground speed comes out at or below zero — with FlightError::NoGroundSpeed, rather than being given a time that means nothing.

    #Running the demo

    cmd/main plans a route against one of the products it carries and prints it leg by leg:

    moon run cmd/main moon run cmd/main -- --station ABR --altitude 12000 --airspeed 120 \ --legs 4 45,-98 44,-96 43,-94

    moonwind — a flight plan from a winds and temperatures aloft product product fd1us1 FD1US1 valid 221800Z station ABR at 12000 ft: 150 at 14 kt, 1.0 C route 3 waypoints, 2 legs, 210.4 nm, at 120 kt leg 1 45.00N 98.00W -> 44.00N 96.00W 104.6 nm on course 124.3, wind 150 at 14 kt, heading 127.2 at 107.2 kt, 0.98 h 6.1 kt across, 12.6 kt along leg 2 44.00N 96.00W -> 43.00N 94.00W 105.8 nm on course 123.9, wind 150 at 14 kt, heading 126.8 at 107.3 kt, 0.99 h 6.2 kt across, 12.6 kt along total 210.4 nm in 1 h 58 min

    --help lists the options, --list lists the stations of a product, and --product fd8us7 plans at 45,000 and 53,000 ft with the high-level product. The products it carries are the real ones the tests run against.

    #Testing

    moon test

    The tests run against real products captured from the Aviation Weather Center text data API, stored in tests/fixtures/ and embedded into the build by scripts/gen_fixtures.py:

    python scripts/gen_fixtures.py > fd_fixtures_wbtest.mbt python scripts/gen_fixtures.py --cli > cmd/main/sample.mbt moon fmt

    They cover the decode rules, the golden values of specific stations, a byte-for-byte round trip of every product, the interpolation, conversion, standard-atmosphere, humidity, apparent-temperature, pressure, Beaufort, wind barb and great-circle values quoted above, and the command line demo's reading of its arguments.

    The package also carries benchmarks, run against the same real product:

    moon bench --release benchmarks

    one benchmark per thing the library does — parsing a product, writing one back, the wind at a flight level, a wind triangle, a great-circle hop and a position along it, cutting a route into legs, planning a whole route, a dew point, a wet-bulb temperature, a heat index, a wind chill, a density altitude, a Beaufort force and a wind barb — so a change to the parsing or the arithmetic shows up in the numbers rather than being argued about.

    ApparentTemperatureError

    pub suberror ApparentTemperatureError {
    BadRelativeHumidity(Double)
    BadWindSpeed(Double)
    }

    Air whose apparent temperature will not do: a percentage that is not one, or a wind that blows backwards.

    BeaufortError

    pub suberror BeaufortError {
    BadWindSpeed(Double)
    }

    A wind whose speed will not do: a wind does not blow backwards, and a negative speed is not one.

    FdError

    pub suberror FdError {
    MissingLevelHeader
    BadLevelHeader(String)
    BadStationLine(String)
    BadWindGroup(String)
    BadTemperature(String)
    }

    Errors raised while reading or writing winds-aloft (FD) bulletins.

    FlightError

    pub suberror FlightError {
    BadAirspeed(Double)
    CrosswindExceedsAirspeed(Double, Double)
    NoForecast(Int)
    NoGroundSpeed(Double)
    }

    A flight that cannot be planned: a wind triangle with no solution, or a leg with no forecast or no ground speed to time it by.

    HumidityError

    pub suberror HumidityError {
    BadRelativeHumidity(Double)
    DewPointAboveTemperature(Double, Double)
    }

    Air whose humidity will not do: a percentage that is not one, or a parcel that is already supersaturated.

    PressureError

    pub suberror PressureError {
    ElevationTooHigh(Double)
    }

    Air whose pressure will not do: a station standing above the standard troposphere, where the reduction has no answer.

    WindBarbError

    pub suberror WindBarbError {
    BadWindSpeed(Double)
    }

    A wind whose speed will not do: the feathers count a speed, and a negative one is not a speed.

    Beaufort

    pub struct Beaufort {
    force : Int
    }

    A wind on the Beaufort scale, held as the force alone: the band it falls in carries the description and the bounds.

    Beaufort::description

    fn Beaufort::description(self : Beaufort) -> String

    What the force is called, such as "Moderate breeze".

    Beaufort::force

    fn Beaufort::force(self : Beaufort) -> Int

    The force, from 0 to 12.

    Beaufort::from_kilometers_per_hour

    fn Beaufort::from_kilometers_per_hour(kilometers_per_hour : Double) -> Beaufort raise BeaufortError

    The Beaufort force of a wind of kilometers_per_hour, read from the scale as it is published in kilometres per hour.

    Beaufort::from_knots

    fn Beaufort::from_knots(knots : Double) -> Beaufort raise BeaufortError

    The Beaufort force of a wind of knots, read from the scale as it is published in knots.

    Beaufort::from_meters_per_second

    fn Beaufort::from_meters_per_second(meters_per_second : Double) -> Beaufort raise BeaufortError

    The Beaufort force of a wind of meters_per_second, read from the scale as it is published in metres per second.

    Beaufort::from_wind

    fn Beaufort::from_wind(wind : Wind) -> Beaufort raise BeaufortError

    The Beaufort force of a reported wind, read from the scale as it is published in knots, which is the unit the data model keeps. A light and variable wind is calm.

    Beaufort::lower_bound_knots

    fn Beaufort::lower_bound_knots(self : Beaufort) -> Double

    The speed the force begins at, in knots, inclusive: the knots table's, even for a force read from another unit's table.

    Beaufort::upper_bound_knots

    fn Beaufort::upper_bound_knots(self : Beaufort) -> Double?

    The speed the next force begins at, in knots, which is where this one ends: force 4 runs up to but not including force 5's 17 kt. A hurricane, force 12, is the last of them and answers None, since the scale has no bound above it.

    Bulletin

    pub struct Bulletin {
    header : Array[String]
    level_header : String
    altitudes : Array[Int]
    wmo_header : String?
    designator : String?
    based_on : String?
    valid : String?
    use_window : String?
    temps_negative_above : Int?
    stations : Array[StationWinds]
    index :
    HashMap
    [String, StationWinds]
    trailing : Array[String]
    }

    A parsed winds and temperatures aloft bulletin, such as the FD1US1 product.

    The header lines are kept verbatim so that format_bulletin reproduces the original text byte for byte.

    Bulletin::altitudes

    fn Bulletin::altitudes(self : Bulletin) -> Array[Int]

    The levels the product forecasts, in feet MSL, as the FT header lists them. Stations do not all report every one of them.

    Bulletin::based_on

    fn Bulletin::based_on(self : Bulletin) -> String?

    The observation time the forecast is based on, as the product writes it.

    Bulletin::designator

    fn Bulletin::designator(self : Bulletin) -> String?

    The product designator, such as FD1US1, when the product carries one.

    Bulletin::station

    fn Bulletin::station(self : Bulletin, id : String) -> StationWinds?

    The forecast for one station, by its three-character identifier.

    Bulletin::stations

    fn Bulletin::stations(self : Bulletin) -> Array[StationWinds]

    Bulletin::temps_negative_above

    fn Bulletin::temps_negative_above(self : Bulletin) -> Int?

    The altitude above which temperatures are reported without a sign, in feet MSL, from TEMPS NEG ABV n; 24,000 ft in current products.

    Bulletin::use_window

    fn Bulletin::use_window(self : Bulletin) -> String?

    The window the forecast is meant to be used in, as the product writes it.

    Bulletin::valid

    fn Bulletin::valid(self : Bulletin) -> String?

    The time the forecast is valid for, as the product writes it.

    Bulletin::wmo_header

    fn Bulletin::wmo_header(self : Bulletin) -> String?

    The WMO abbreviated heading of the product, such as FBUS31 KWNO 221358, when it carries one.

    Coordinate

    pub struct Coordinate {
    latitude : Double
    longitude : Double
    }

    A position on the globe: latitude and longitude in degrees, positive north and east.

    Coordinate::distance_to

    fn Coordinate::distance_to(self : Coordinate, other : Coordinate) -> Double

    The great-circle distance to other, in nautical miles.

    This is the haversine formula, written as an atan2 rather than as an arcsine so that it keeps its accuracy at both ends of its range: for the short hops a route is built from, and for the near-antipodal distances a long flight covers.

    Coordinate::initial_course_to

    fn Coordinate::initial_course_to(self : Coordinate, other : Coordinate) -> Double

    The initial true course from here to other, in degrees clockwise from north.

    A great circle is not a straight line on a chart, so its direction changes along the way, and this is the course at the start of the path: the one to steer as the leg begins. Over a long path the course drifts — a great circle from New York to London leaves on 051 and arrives on 108 — which is why a long hop is flown as several short ones, each with its own course.

    Coordinate::interpolate

    fn Coordinate::interpolate(self : Coordinate, other : Coordinate, fraction : Double) -> Coordinate

    The position fraction of the way along the great circle to other: 0 is here, 1 is there, and 0.5 is half way by distance.

    Coordinate::latitude

    fn Coordinate::latitude(self : Coordinate) -> Double

    Coordinate::longitude

    fn Coordinate::longitude(self : Coordinate) -> Double

    Coordinate::midpoint

    fn Coordinate::midpoint(self : Coordinate, other : Coordinate) -> Coordinate

    Half way along the great circle to other, by distance.

    Half way is where the two positions balance, which is a sum rather than a walk: the sum of two unit vectors points at the middle of the path between them, and only needs putting back on the surface. It is also the better behaved of the two at short range, where walking a path by its sine leaves the middle undetermined.

    Coordinate::new

    fn Coordinate::new(latitude : Double, longitude : Double) -> Coordinate

    A position from its latitude and longitude, in degrees.

    Level

    pub struct Level {
    altitude : Int
    wind : Wind
    temperature : Int?
    }

    One forecast level of a station: the altitude in feet above mean sea level, the wind, and the temperature in degrees Celsius. temperature is None when the product does not forecast one, which is always the case at 3,000 ft and at levels close to the station elevation.

    Level::altitude

    fn Level::altitude(self : Level) -> Int

    Level::temperature

    fn Level::temperature(self : Level) -> Int?

    Level::wind

    fn Level::wind(self : Level) -> Wind

    Route

    pub struct Route {
    points : Array[Coordinate]
    }

    A route: the waypoints in the order they are flown.

    Route::distance

    fn Route::distance(self : Route) -> Double

    The length of the route along its waypoints, in nautical miles.

    Only the hops are measured, so a route's length costs nothing it does not use: the courses along the way are left to segments.

    Route::new

    fn Route::new(points : Array[Coordinate]) -> Route

    A route from its waypoints, in order. A route needs two waypoints to have any length; one with fewer is simply empty of hops.

    Route::plan

    fn Route::plan(self : Route, station : StationWinds, altitude : Int, airspeed : Double) -> RoutePlan raise FlightError

    Plan the route at altitude feet and airspeed knots with one station's forecast, read at each leg's middle.

    One station stands in for the whole route, which is what a route flown within one station's area wants. A longer route is planned in parts, one call per part, each with the station nearest it; the parts add up.

    Route::plan_with

    fn Route::plan_with(self : Route, wind_at : (Coordinate, Int) -> WindVector?, altitude : Int, airspeed : Double) -> RoutePlan raise FlightError

    Plan the route at altitude feet and airspeed knots, reading the wind for each leg from wind_at, which is given the middle of the leg and the altitude.

    The wind is read at the middle of a leg because that is the wind that carries the aircraft over most of it, and the position is offered to the caller so that a forecast which varies over the route can be used: the station nearest each leg, or a grid of forecast winds.

    A leg that has no forecast at that altitude is reported with FlightError::NoForecast, and a leg the wind beats — one whose ground speed comes out at or below zero — with FlightError::NoGroundSpeed, rather than being planned with a time that means nothing.

    Route::points

    fn Route::points(self : Route) -> Array[Coordinate]

    The waypoints, in order.

    Route::segments

    fn Route::segments(self : Route) -> Array[RouteSegment]

    The hops between consecutive waypoints, in order, with their distances and courses.

    Route::subdivided

    fn Route::subdivided(self : Route, count : Int) -> Route

    The same route flown in shorter hops: every hop divided into count equal parts, so that the course of each part stays close to the course at its start. A count below one leaves the route as it is.

    The hop is walked from its two ends, which are read once for the whole hop: cutting a route into many legs costs the same trigonometry whether a hop is cut in two or in twenty.

    RouteLeg

    pub struct RouteLeg {
    segment : RouteSegment
    wind : WindVector
    triangle : WindTriangle
    }

    One leg of a planned route: the hop, the forecast wind that was used for it, and the wind triangle that comes out of the two.

    RouteLeg::crosswind

    fn RouteLeg::crosswind(self : RouteLeg) -> Double

    The wind component across the leg, in knots: positive from the right.

    RouteLeg::distance

    fn RouteLeg::distance(self : RouteLeg) -> Double

    The distance of the leg, in nautical miles.

    RouteLeg::ground_speed

    fn RouteLeg::ground_speed(self : RouteLeg) -> Double

    The ground speed the leg is flown at, in knots.

    RouteLeg::heading

    fn RouteLeg::heading(self : RouteLeg) -> Double

    The heading to steer along the leg, in degrees true.

    RouteLeg::headwind

    fn RouteLeg::headwind(self : RouteLeg) -> Double

    The wind component along the leg, in knots: positive against the aircraft, so a tailwind is negative.

    RouteLeg::segment

    fn RouteLeg::segment(self : RouteLeg) -> RouteSegment

    The hop this leg flies.

    RouteLeg::time_hours

    fn RouteLeg::time_hours(self : RouteLeg) -> Double

    The time the leg takes, in hours: its distance over its ground speed.

    RouteLeg::triangle

    fn RouteLeg::triangle(self : RouteLeg) -> WindTriangle

    The wind triangle of the leg: heading, ground speed and the components a pilot reads the wind in.

    RouteLeg::true_course

    fn RouteLeg::true_course(self : RouteLeg) -> Double

    The course of the leg, in degrees true.

    RouteLeg::wind

    fn RouteLeg::wind(self : RouteLeg) -> WindVector

    The forecast wind the leg was planned with, as a vector.

    RoutePlan

    pub struct RoutePlan {
    source : String?
    altitude : Int
    airspeed : Double
    legs : Array[RouteLeg]
    }

    A route planned at one altitude and airspeed: the legs in order, each with the wind it was planned with, and the totals over the whole route.

    RoutePlan::airspeed

    fn RoutePlan::airspeed(self : RoutePlan) -> Double

    The true airspeed the route is planned at, in knots.

    RoutePlan::altitude

    fn RoutePlan::altitude(self : RoutePlan) -> Int

    The altitude the route is planned at, in feet.

    RoutePlan::legs

    fn RoutePlan::legs(self : RoutePlan) -> Array[RouteLeg]

    The legs, in order.

    RoutePlan::source

    fn RoutePlan::source(self : RoutePlan) -> String?

    The station the forecast came from, when the plan was built on one station's winds rather than on a wind the caller supplies.

    RoutePlan::total_distance

    fn RoutePlan::total_distance(self : RoutePlan) -> Double

    The distance of the whole route, in nautical miles.

    RoutePlan::total_time_hours

    fn RoutePlan::total_time_hours(self : RoutePlan) -> Double

    The time the whole route takes, in hours.

    RouteSegment

    pub struct RouteSegment {
    from : Coordinate
    to : Coordinate
    distance : Double
    true_course : Double
    }

    One hop of a route: the two positions, the great-circle distance between them and the course it is flown on.

    RouteSegment::distance

    fn RouteSegment::distance(self : RouteSegment) -> Double

    The great-circle distance of the hop, in nautical miles.

    RouteSegment::from

    Where the hop starts.

    RouteSegment::midpoint

    fn RouteSegment::midpoint(self : RouteSegment) -> Coordinate

    Half way along the hop, where a leg reads its wind from the forecast.

    RouteSegment::new

    The hop from one position to another, with its distance and course.

    RouteSegment::to

    Where the hop ends.

    RouteSegment::true_course

    fn RouteSegment::true_course(self : RouteSegment) -> Double

    The true course at the start of the hop, in degrees. The course drifts as the hop is flown, so a long hop is not flown on one heading: cut it into shorter legs to keep the course of each one nearly constant.

    Runway

    pub struct Runway {
    designator : String
    heading : Int
    }

    One runway, by its designator and magnetic heading.

    Runway::crosswind

    fn Runway::crosswind(self : Runway, wind : Wind) -> Double

    The wind across the runway, in knots, positive from the right of the landing or take-off direction.

    Runway::designator

    fn Runway::designator(self : Runway) -> String

    Runway::heading

    fn Runway::heading(self : Runway) -> Int

    Runway::headwind

    fn Runway::headwind(self : Runway, wind : Wind) -> Double

    The wind along the runway, in knots, positive against the landing or take-off direction.

    Runway::parse

    fn Runway::parse(designator : String) -> Runway?

    A runway from its designator, such as 27, 09L or 36.

    The designator is a runway number from 01 to 36 — its heading in tens of degrees — optionally followed by L, C or R for one of a set of parallel runways. Anything else is not a runway, and parses to None.

    Runway::reciprocal

    fn Runway::reciprocal(self : Runway) -> Runway?

    The runway at the other end of the same strip, if it has one: runway 09 is the reciprocal of runway 27, and the other way round. Every runway pair has one, but some one-way strips carry no designator.

    StationWinds

    pub struct StationWinds {
    station : String
    levels : Array[Level]
    }

    The winds and temperatures aloft forecast for one station.

    levels holds only the altitudes the product actually reports for this station; a missing level is simply absent, exactly as a blank column in the bulletin.

    StationWinds::altitudes

    fn StationWinds::altitudes(self : StationWinds) -> Array[Int]

    The altitudes this station reports, in feet, in the order the bulletin lists them, which is ascending.

    A station does not necessarily report every level of the product: a column the bulletin leaves blank is simply absent.

    StationWinds::highest_altitude

    fn StationWinds::highest_altitude(self : StationWinds) -> Int?

    The highest altitude this station reports, in feet.

    StationWinds::level_at

    fn StationWinds::level_at(self : StationWinds, altitude : Int) -> Level?

    The level at exactly altitude feet, if the station reports one.

    StationWinds::levels

    fn StationWinds::levels(self : StationWinds) -> Array[Level]

    StationWinds::lowest_altitude

    fn StationWinds::lowest_altitude(self : StationWinds) -> Int?

    The lowest altitude this station reports, in feet.

    StationWinds::station

    fn StationWinds::station(self : StationWinds) -> String

    StationWinds::temperature_at

    fn StationWinds::temperature_at(self : StationWinds, altitude : Int) -> Double?

    The temperature at altitude feet, in degrees Celsius, interpolated between the levels that forecast one.

    None when no level on both sides of altitude carries a temperature, which is the case at 3,000 ft, where the product never forecasts one.

    StationWinds::vector_at

    fn StationWinds::vector_at(self : StationWinds, altitude : Int) -> WindVector?

    The wind at altitude feet, as a vector in knots.

    A reported level is returned as it was reported. An altitude between two reported levels is interpolated between them, component by component; a light and variable level counts as no wind at all. None when the station reports nothing on both sides of altitude.

    Wind

    pub struct Wind {
    direction : Int?
    speed : Int
    }

    One upper-air wind: the direction the wind blows from, in degrees true, and its speed in knots.

    direction is None for a light and variable wind (encoded 9900), which is a wind below 5 knots with no useful direction. Directions are always multiples of 10 degrees, as the wire format carries tens of degrees.

    Wind::direction

    fn Wind::direction(self : Wind) -> Int?

    Accessors.

    Wind::is_light_and_variable

    fn Wind::is_light_and_variable(self : Wind) -> Bool

    Whether this wind is a light and variable report (9900).

    Wind::new

    fn Wind::new(direction : Int?, speed : Int) -> Wind

    A wind from the direction it blows from and its speed, which is how one is reported: Wind::new(Some(150), 14) is 150 degrees at 14 kt, and Wind::new(None, 0) is light and variable.

    Wind::speed

    fn Wind::speed(self : Wind) -> Int

    WindBarb

    pub struct WindBarb {
    direction_tens : Int
    pennants : Int
    full_barbs : Int
    half_barbs : Int
    }

    A wind drawn as a barb: the direction the staff points along, in tens of degrees, and the pennants, full barbs and half barbs hanging off it.

    WindBarb::direction_tens

    fn WindBarb::direction_tens(self : WindBarb) -> Int

    The direction the staff points along, in tens of degrees: 0 when the wind has no direction, 36 for a wind out of the north, and otherwise the direction over ten.

    WindBarb::from_knots

    fn WindBarb::from_knots(direction : Int?, knots : Double) -> WindBarb raise WindBarbError

    The wind barb of a wind from direction degrees at knots, for a caller that has the two numbers rather than a Wind to report. A direction of None is written as no direction at all — tens of 0 — and a direction of 0 degrees, the north, as 36.

    WindBarb::from_wind

    fn WindBarb::from_wind(wind : Wind) -> WindBarb raise WindBarbError

    The wind barb of a reported wind: its direction in tens of degrees and its speed counted out in pennants, full barbs and half barbs.

    WindBarb::full_barbs

    fn WindBarb::full_barbs(self : WindBarb) -> Int

    How many full barbs hang off the staff, each of them 10 knots.

    WindBarb::half_barbs

    fn WindBarb::half_barbs(self : WindBarb) -> Int

    How many half barbs hang off the staff, each of them 5 knots.

    WindBarb::is_calm

    fn WindBarb::is_calm(self : WindBarb) -> Bool

    Whether the barb is a calm: no wind at all, which a station plot draws as a circle with no staff. A wind with a direction but too little speed to draw a feather is not a calm — it has a staff, and it points somewhere.

    WindBarb::pennants

    fn WindBarb::pennants(self : WindBarb) -> Int

    How many pennants hang off the staff, each of them 50 knots.

    WindBarb::speed_knots

    fn WindBarb::speed_knots(self : WindBarb) -> Double

    The speed the feathers count, in knots: the pennants at 50, the full barbs at 10 and the half barbs at 5, which is the speed the barb was drawn from rounded to the nearest 5 knots.

    WindBarb::to_ascii

    fn WindBarb::to_ascii(self : WindBarb) -> String

    The barb drawn with characters every font has — P for a pennant, = for a full barb, - for a half barb, | for the staff and O for a calm — for a terminal with no glyph for a pennant or a box-drawing line.

    WindBarb::to_unicode

    fn WindBarb::to_unicode(self : WindBarb) -> String

    The barb drawn with the characters a station plot uses: a pennant ⚑, a full barb ─, a half barb ╌, the staff │ they hang off, and a circle ○ for a calm. The feathers come in that order along the staff.

    WindBarb::to_wind

    fn WindBarb::to_wind(self : WindBarb) -> Wind

    The wind the barb was drawn from, as far as the drawing can say: the speed rounded to the nearest 5 knots, and the direction the tens stand for — none for 0, north for 36, and the tens of degrees otherwise.

    WindTriangle

    pub struct WindTriangle {
    course : Double
    airspeed : Double
    wind : WindVector
    heading : Double
    wind_correction_angle : Double
    ground_speed : Double
    headwind : Double
    crosswind : Double
    }

    The solution of one wind triangle: the inputs, and the heading, ground speed and wind components they give.

    WindTriangle::airspeed

    fn WindTriangle::airspeed(self : WindTriangle) -> Double

    WindTriangle::course

    fn WindTriangle::course(self : WindTriangle) -> Double

    WindTriangle::crosswind

    fn WindTriangle::crosswind(self : WindTriangle) -> Double

    WindTriangle::ground_speed

    fn WindTriangle::ground_speed(self : WindTriangle) -> Double

    WindTriangle::heading

    fn WindTriangle::heading(self : WindTriangle) -> Double

    WindTriangle::headwind

    fn WindTriangle::headwind(self : WindTriangle) -> Double

    WindTriangle::solve

    fn WindTriangle::solve(course : Double, airspeed : Double, wind : WindVector) -> WindTriangle raise FlightError

    Solve the wind triangle.

    The heading to steer is the course plus the wind correction angle, which is read off the two components of the wind along the track: the angle whose sine is the crosswind over the airspeed. When the crosswind exceeds the airspeed there is no such angle, and the triangle cannot be flown.

    WindTriangle::wind

    WindTriangle::wind_correction_angle

    fn WindTriangle::wind_correction_angle(self : WindTriangle) -> Double

    WindVector

    pub struct WindVector {
    u : Double
    v : Double
    }

    A horizontal wind as a velocity vector: u is the eastward component and v the northward one, both in knots.

    This is the direction the air is moving towards, the opposite of the direction a bulletin reports, since a wind direction names where the wind comes from.

    WindVector::crosswind_component

    fn WindVector::crosswind_component(self : WindVector, direction : Double) -> Double

    The wind across direction, in degrees true, in knots.

    Positive when the wind comes from the right, the side that makes an aircraft steer a positive wind correction angle.

    WindVector::direction

    fn WindVector::direction(self : WindVector) -> Double

    The direction the wind comes from, in degrees true, in [0, 360).

    Undefined for a vector of zero length, where it returns 0.

    WindVector::from_components

    fn WindVector::from_components(u : Double, v : Double) -> WindVector

    A wind vector from its eastward and northward components, in knots.

    WindVector::from_wind

    fn WindVector::from_wind(wind : Wind) -> WindVector

    The vector of a reported wind.

    A light and variable wind has no direction, and becomes the zero vector: it contributes nothing to an interpolated wind.

    WindVector::headwind_component

    fn WindVector::headwind_component(self : WindVector, direction : Double) -> Double

    The wind along direction, in degrees true, in knots.

    Positive when the wind comes from ahead, so a tailwind is negative. A light and variable wind has no direction, and contributes nothing.

    WindVector::interpolate

    fn WindVector::interpolate(a : WindVector, b : WindVector, fraction : Double) -> WindVector

    The vector fraction of the way from a to b, component by component.

    A wind forecast for two levels is taken to vary linearly between them, the same way its temperature does.

    WindVector::speed

    fn WindVector::speed(self : WindVector) -> Double

    The wind speed, in knots: the length of the vector.

    WindVector::to_wind

    fn WindVector::to_wind(self : WindVector) -> Wind

    The wind as a Wind, rounded to the nearest knot and degree, with 360° folded back to 0°.

    A vector below half a knot rounds to zero speed, and is reported the way the bulletins report a negligible wind: light and variable.

    WindVector::u

    fn WindVector::u(self : WindVector) -> Double

    WindVector::v

    fn WindVector::v(self : WindVector) -> Double

    absolute_humidity

    fn absolute_humidity(vapor_pressure_hpa : Double, celsius : Double) -> Double

    How much water a cubic metre of air at celsius holds when its vapour pressure is vapor_pressure_hpa, in grams per cubic metre.

    The vapour pressure is what the dew point is: saturation_vapor_pressure of it gives the vapour pressure of saturated air, and half of that is air at 50 % humidity.

    altimeter_setting_hpa

    fn altimeter_setting_hpa(station_pressure_hpa : Double, elevation_meters : Double) -> Double raise PressureError

    The altimeter setting of a station reporting station_pressure_hpa at elevation_meters, in hectopascals: the pressure the station would report if it stood at sea level in the standard atmosphere, which is what an aircraft's altimeter is set to.

    The value is the barometric formula, P·(288.15 / (288.15 − 0.0065·z))^5.25588, and it takes no temperature: the setting is defined so that an altimeter standing at the field reads the elevation of the field, and the two formulas cancel exactly when it does. A station in the standard atmosphere at 500 m reports 954.61 hPa and has a setting of 1013.25.

    An elevation of 44330.77 m or more is above the atmosphere the formula is written for, and is refused rather than answered with a number that does not exist.

    altimeter_setting_inhg

    fn altimeter_setting_inhg(station_pressure_hpa : Double, elevation_meters : Double) -> Double raise PressureError

    The altimeter setting of a station reporting station_pressure_hpa at elevation_meters, in inches of mercury, which is how it is read aloud in North America.

    apparent_temperature

    fn apparent_temperature(celsius : Double, relative_humidity : Double, wind_speed_meters_per_second : Double) -> Double raise ApparentTemperatureError

    The apparent temperature of air at celsius holding relative_humidity percent, with a wind of wind_speed_meters_per_second at 10 metres, in degrees Celsius: what the Bureau of Meteorology calls "feels like".

    The value is the Bureau's T + 0.33·e − 0.70·ws − 4.00, with e the part of the saturation vapour pressure that the humidity holds, both in the units above. The Bureau computes its own vapour pressure; this one is the Magnus form the rest of the humidity calculations use, which differs from it by at most 0.07 hPa at 30 °C, and by 0.02 °C in the result.

    celsius_to_fahrenheit

    fn celsius_to_fahrenheit(celsius : Double) -> Double

    Degrees Celsius to degrees Fahrenheit.

    decode_wind

    fn decode_wind(group : String) -> Wind raise FdError

    Decode a four-character wind group ddss.

    density_altitude

    fn density_altitude(pressure_hpa : Double, celsius : Double) -> Double

    The density altitude of air at pressure_hpa and celsius, in feet: the height in the standard atmosphere that has that air density, which is what an engine, a propeller and a wing are working in.

    The value is the rule of thumb a pilot reads off a chart, the pressure altitude plus 120 ft for every degree the air is warmer than the standard atmosphere is at that altitude, so standard air at sea level reads 0 ft and a 25 °C day at sea level reads 1,200. It reads a little high: the exact answer for dry air on that day is 1,162 ft. The water in the air is left out of this one as well — density_altitude_with_dew_point counts it, and a damp day is a few hundred feet higher again.

    density_altitude_with_dew_point

    fn density_altitude_with_dew_point(pressure_hpa : Double, celsius : Double, dew_point_celsius : Double) -> Double raise HumidityError

    The density altitude of air at pressure_hpa, celsius and a dew point of dew_point_celsius, in feet: the same altitude as density_altitude, read off the air's density rather than off the rule of thumb, and with the water in it counted.

    The vapour the dew point stands for lightens the air, so its density is read at the virtual temperature the dry air alone would have to be at to weigh the same, T / (1 − 0.378·e/P) with e the vapour pressure in hectopascals, and the altitude is where the standard atmosphere has that density. Saturated air at sea level on a 15 °C day reads 218 ft above the dry-air answer, and 109 ft at 50 % humidity.

    A dew point above the air temperature would make the air supersaturated and is refused, as it is by relative_humidity.

    dew_point

    fn dew_point(celsius : Double, relative_humidity : Double) -> Double raise HumidityError

    The dew point of air at celsius holding relative_humidity percent of the water it could, in degrees Celsius: the temperature it would have to be cooled to for the vapour to begin condensing.

    The relative humidity has to be above zero and at most 100.

    encode_level

    fn encode_level(level : Level) -> String raise FdError

    Encode one level as its column field, without padding.

    encode_temperature

    fn encode_temperature(temperature : Int, altitude : Int) -> String raise FdError

    Encode a temperature field for a level: unsigned above 24,000 ft, where temperatures are always negative, and signed below.

    encode_wind

    fn encode_wind(wind : Wind) -> String raise FdError

    Encode a wind as its four-character group.

    fahrenheit_to_celsius

    fn fahrenheit_to_celsius(fahrenheit : Double) -> Double

    Degrees Fahrenheit to degrees Celsius.

    feet_to_meters

    fn feet_to_meters(feet : Double) -> Double

    Feet to metres.

    field_layout

    fn field_layout(altitudes : Array[Int]) -> Array[(Int, Int)]

    The column of each forecast level in a station line, as (start, width).

    The first field starts at column 4 (after the three-character station identifier and a blank) and fields are separated by one blank. A field is four characters wide when no temperature is carried (the 3,000 ft level), six when the temperature is an unsigned magnitude above 24,000 ft, and seven otherwise.

    format_bulletin

    fn format_bulletin(bulletin : Bulletin) -> String raise FdError

    Write a parsed bulletin back as text.

    For a bulletin produced by parse_bulletin this reproduces the input byte for byte, apart from a final newline, which is always written.

    format_station

    fn format_station(station : StationWinds, altitudes : Array[Int]) -> String raise FdError

    Format one station line, placing each level in its column.

    frost_point

    fn frost_point(celsius : Double, relative_humidity : Double) -> Double raise HumidityError

    The frost point of air at celsius holding relative_humidity percent of the water it could, in degrees Celsius: the temperature at which the vapour would begin to deposit as ice.

    Below freezing this sits above the dew point of the same air, because the vapour pressure over ice is the lower of the two.

    heat_index_celsius

    fn heat_index_celsius(celsius : Double, relative_humidity : Double) -> Double? raise ApparentTemperatureError

    The heat index of air at celsius holding relative_humidity percent of the water it could, in degrees Celsius: heat_index_fahrenheit read in the units the rest of the world uses.

    The temperature is converted to Fahrenheit to be read against the range the regression was fitted over, so an air temperature just below 26.7 °C is outside it even though it looks like round numbers in Celsius.

    heat_index_fahrenheit

    fn heat_index_fahrenheit(fahrenheit : Double, relative_humidity : Double) -> Double? raise ApparentTemperatureError

    The heat index of air at fahrenheit holding relative_humidity percent of the water it could, in degrees Fahrenheit: how much hotter the humidity makes it feel.

    The value is the Rothfusz regression the National Weather Service uses, with the two corrections it applies at the ends of the humidity range — a subtraction in very dry air, an addition in very damp air — and its fallback of 0.5·(T + 61.0 + (T − 68.0)·1.2 + RH·0.094) when the regression comes out below 80 °F, where the index is the air temperature rather than something felt.

    It is defined from 80 °F to 112 °F, the range it was fitted over, and answers None outside it. A relative humidity outside 0 to 100 is refused.

    hectopascals_to_inches_of_mercury

    fn hectopascals_to_inches_of_mercury(hectopascals : Double) -> Double

    Hectopascals to inches of mercury, the unit an altimeter setting is published in over much of the world.

    hectopascals_to_millimeters_of_mercury

    fn hectopascals_to_millimeters_of_mercury(hectopascals : Double) -> Double

    Hectopascals to millimetres of mercury, which is the same unit as the torr.

    humidex

    fn humidex(celsius : Double, dew_point_celsius : Double) -> Double raise HumidityError

    The humidex of air at celsius whose dew point is dew_point_celsius, in degrees Celsius: Environment Canada's index of how hot the humidity makes it feel, T + (5/9)·(e − 10) with e the vapour pressure in hectopascals.

    The vapour pressure is the saturation one at the dew point, which is the vapour pressure of the air itself. A dew point above the air temperature would make the air supersaturated and is refused, as it is by relative_humidity.

    inches_of_mercury_to_hectopascals

    fn inches_of_mercury_to_hectopascals(inches_of_mercury : Double) -> Double

    Inches of mercury to hectopascals.

    isa_deviation

    fn isa_deviation(celsius : Double, feet : Double) -> Double

    How much warmer than the standard atmosphere celsius is at feet, in degrees Celsius: the ISA deviation a flight plan quotes, positive in warm air.

    isa_temperature

    fn isa_temperature(feet : Double) -> Double

    The temperature of the international standard atmosphere at feet, in degrees Celsius: 15 °C at sea level, falling by 1.9812 °C per thousand feet to the tropopause at 36,089 ft, and constant at −56.5 °C above it.

    kilometers_per_hour_to_knots

    fn kilometers_per_hour_to_knots(kilometers_per_hour : Double) -> Double

    Kilometres per hour to knots.

    knots_to_kilometers_per_hour

    fn knots_to_kilometers_per_hour(knots : Double) -> Double

    Knots to kilometres per hour.

    knots_to_meters_per_second

    fn knots_to_meters_per_second(knots : Double) -> Double

    Knots to metres per second.

    knots_to_miles_per_hour

    fn knots_to_miles_per_hour(knots : Double) -> Double

    Knots to statute miles per hour.

    mean_sea_level_pressure

    fn mean_sea_level_pressure(station_pressure_hpa : Double, elevation_meters : Double, celsius : Double) -> Double

    The sea-level pressure of a station reporting station_pressure_hpa at elevation_meters with an air temperature of celsius, in hectopascals: the reading with the weight of the air below it added back.

    The value is the reduction the World Meteorological Organization specifies, P·exp(g·z / (R·(T + L·z/2))), which takes the column of air between the station and the sea to warm at half the standard lapse rate — the best a station can say about a column it only has one temperature from. A station at sea level reduces to itself.

    The water in the air is left out of it: vapour lightens the column by a tenth of a percent at ordinary humidity, less than the temperature of the column moves the answer by.

    meters_per_second_to_knots

    fn meters_per_second_to_knots(meters_per_second : Double) -> Double

    Metres per second to knots.

    meters_to_feet

    fn meters_to_feet(meters : Double) -> Double

    Metres to feet.

    miles_per_hour_to_knots

    fn miles_per_hour_to_knots(miles_per_hour : Double) -> Double

    Statute miles per hour to knots.

    millimeters_of_mercury_to_hectopascals

    fn millimeters_of_mercury_to_hectopascals(millimeters_of_mercury : Double) -> Double

    Millimetres of mercury to hectopascals.

    parse_bulletin

    fn parse_bulletin(text : String) -> Bulletin raise FdError

    Parse a winds and temperatures aloft bulletin.

    Header lines, the FT line and any trailing lines are kept verbatim, so format_bulletin reproduces the input byte for byte.

    pressure_altitude

    fn pressure_altitude(pressure_hpa : Double) -> Double

    The pressure altitude of pressure_hpa, in feet: the height in the standard atmosphere that has that pressure.

    The value is 145442.16·(1 − (P/1013.25)^(1/5.25588)), so the standard sea level pressure reads 0 ft, the 850 hPa level reads 4,781 ft and the 700 hPa level 9,882. An altimeter set to the standard sea level pressure reads it directly.

    relative_humidity

    fn relative_humidity(celsius : Double, dew_point_celsius : Double) -> Double raise HumidityError

    The relative humidity of air at celsius whose dew point is dew_point_celsius, in percent: how much of the water it could hold it is holding.

    A dew point above the temperature would be supersaturated, which is refused rather than reported as a humidity above 100.

    saturation_vapor_pressure

    fn saturation_vapor_pressure(celsius : Double) -> Double

    The vapour pressure of air saturated with water at celsius, in hectopascals: how much pressure its water vapour would have if the air held all it can at that temperature.

    This is also the actual vapour pressure of air whose dew point is celsius, which is how the relative humidity and the dew point are read off it.

    saturation_vapor_pressure_over_ice

    fn saturation_vapor_pressure_over_ice(celsius : Double) -> Double

    The vapour pressure of air saturated with respect to ice at celsius, in hectopascals.

    Below freezing the air saturates against ice rather than water, and the pressure over ice is the lower of the two — 2.597 hPa against 2.868 hPa at −10 °C — so a frost point sits above the dew point at the same humidity.

    station_pressure

    fn station_pressure(sea_level_pressure_hpa : Double, elevation_meters : Double, celsius : Double) -> Double

    The pressure a station at elevation_meters reports when the sea-level pressure is sea_level_pressure_hpa and the air is at celsius, in hectopascals: the inverse of mean_sea_level_pressure.

    wet_bulb_temperature

    fn wet_bulb_temperature(celsius : Double, relative_humidity : Double) -> Double?

    The wet-bulb temperature of air at celsius holding relative_humidity percent of the water it could, in degrees Celsius: what a thermometer reads with a wet wick over it in moving air, which evaporation cools below the air temperature.

    The value is Stull's fit (2011), T·atan(0.151977·√(RH + 8.313659)) +atan(T + RH) − atan(RH − 1.676331) + 0.00391838·RH^1.5·atan(0.023101·RH) −4.686035, with the angles in radians. It is fitted over 5 % to 99 % humidity and −20 °C to 50 °C, and answers None outside that, where it is an extrapolation. Saturated air is outside it too: the fit drifts there, and the wet-bulb temperature of saturated air is the air temperature itself.

    wind_chill_celsius

    fn wind_chill_celsius(celsius : Double, kilometers_per_hour : Double) -> Double? raise ApparentTemperatureError

    The wind chill of air at celsius moving at kilometers_per_hour, in degrees Celsius: the same index as wind_chill_fahrenheit, written in the units Canada reads it in, 13.12 + 0.6215·T − 11.37·V^0.16 +0.3965·T·V^0.16.

    It is defined at or below 10 °C with a wind above 4.8 km/h, and answers None outside that.

    wind_chill_fahrenheit

    fn wind_chill_fahrenheit(fahrenheit : Double, miles_per_hour : Double) -> Double? raise ApparentTemperatureError

    The wind chill of air at fahrenheit moving at miles_per_hour, in degrees Fahrenheit: how much colder the wind makes exposed skin feel.

    The value is the formula the National Weather Service and Environment Canada adopted in 2001, 35.74 + 0.6215·T − 35.75·V^0.16 +0.4275·T·V^0.16, for a wind at 10 metres. It is defined at or below 50 °F with a wind above 3 mph, and answers None outside that rather than extrapolating into a chill the index does not stand behind.

    wind_chill_from_wind

    fn wind_chill_from_wind(wind : Wind, celsius : Double) -> Double? raise ApparentTemperatureError

    The wind chill of a reported wind at celsius, in degrees Celsius: the knots the data model keeps are converted to the kilometres per hour the metric index is written in.

    A light and variable wind — the one with no direction, a wind below 5 knots — is answered with None, and so is a temperature above 10 °C.