# Air

The temperature, humidity, station pressure and latitude stored with one logged group, the density altitude worked out from them, and where they came from.

## Generated table

**Record:** observation · **Covers:** `environment`

| Field | Type | Required | Limits in the schema | Solver reads | In a shared log |
|---|---|---|---|---|---|
| `environment.altitude_ft` | number | no | min -1500; max 20000 | no | removed when location is hidden |
| `environment.assumed` | array of `'rh'` or `'lat'` | no | - | no | as stored |
| `environment.captured_at` | string | yes | - | no | as stored |
| `environment.da_ft` | number | yes | - | no | worked out again from the rounded pressure (removed when the air is incomplete) when location is hidden |
| `environment.lat_deg` | number | yes | - | no | rounded to a whole degree when location is hidden |
| `environment.rh_pct` | number | yes | - | no | as stored |
| `environment.source` | `'manual'` or `'kestrel'` or `'phone'` or `'rangefinder'` | yes | - | no | as stored |
| `environment.station_pressure_inHg` | number | yes | - | no | rounded to 0.1 inHg when location is hidden |
| `environment.temp_F` | number | yes | - | no | as stored |

## What it holds

The `environment` block is the air a group was shot in: temperature, humidity, station pressure and the shooter's latitude. It also holds the density altitude the app worked out from them, a word for where the numbers came from, and the time the group was saved. It is a copy made when the group was logged, and it is always there. It has no field for height above sea level or for a place name. When the group is solved again, these four numbers are its weather, and the group's [target](target.md) is where it was fired.

## What to know first

**Pressure is station pressure.** It is what a weather meter shows where you stand, not a weather report's sea-level number. A sea-level value reads as thick air: 29.92 inHg for a true 25 raised a 1,500 yd elevation from 10.20 to 11.28 mil in a run.

**The source word says where the air came from.** `rangefinder` when a connected rangefinder gave the three values and none was changed, `phone` when the app's weather lookup gave them and none was typed over, `manual` otherwise. A lookup also puts "air: weather model" in the notes.

**The density altitude is stored for show.** `environment.da_ft` uses the usual formula. The fit never reads it; only the guided calibration compares it with the weather.

**Latitude is the shooter's own number, and the connector can supply a default.** A voice log with no latitude stores 45 and marks it in `environment.assumed`.

**Hiding the location rounds, it does not remove.** The pressure is shared to 0.1 inHg, the latitude to the whole degree and the density altitude worked out again; the temperature and humidity stay exact.

## The fields

**`environment.temp_F`** The air temperature at the firing point in degrees Fahrenheit. Required; negative values are fine. Example: 62.

**`environment.rh_pct`** The relative humidity in percent, 0 to 100. Required. The solver refuses a value outside that range. Example: 48.

**`environment.station_pressure_inHg`** The pressure at the firing point in inches of mercury, as a weather meter shows it, not corrected to sea level. Required, above 0. A pressure typed as sea-level at a high range makes the air too thick; a slip such as 2.9 for 29 makes it far too thin (density altitude about 62,000 ft in a run). The Weather button in the range log and the Calculator gives the weather model's pressure at its own grid height, which the button's note states in feet. Example: 25.10.

**`environment.lat_deg`** The latitude of the range in degrees, positive north. Required, within 90 degrees. Only the solver's latitude-dependent effects read it: the Coriolis choice and, in the Physics and Spin models, gravity by latitude. The Classic model reads no latitude at all (its Coriolis choice is Off and its gravity is standard). In a run on the Standard model at 1,500 yd, going from latitude 0 to 60 moved the windage by 0.12 mil, and at 40 degrees firing east lowered the elevation by about 0.12 mil against firing north. The range log fills it from the phone's GPS fix when the field is blank; the Weather button does not set it. Example: 40.6.

**`environment.da_ft`** The density altitude in feet, worked out when the group is saved from temperature, humidity and pressure with the Standard vapour choice, whatever the profile's model. The Calculator shows the same number, worked the same way, whatever the model. Example: 5,100.

**`environment.source`** Where the numbers came from. The type allows `manual`, `kestrel` (a Kestrel weather meter), `phone` and `rangefinder`. The app writes `rangefinder` for a group logged from the Calculator while its temperature, humidity and pressure are all still the values a connected rangefinder gave; changing any one of the three, in the Calculator or in the log form, before the group is added makes it `manual`. Since 2026-10-24 the app writes `phone` when the air came from its weather lookup (the Weather button, or a group carried from one whose air did) and was not typed over. Everything else the app writes, and everything the connector writes, is `manual`; `kestrel` exists only in the type. The mark names the kind of device and nothing about the unit: no serial number, address or name is stored. Groups from before the rangefinder link have `manual`.

**`environment.captured_at`** The time the group was saved, as an ISO date and time. It is not the time the air was measured. A group logged by voice from stale phone data says so in its notes, not here.

**`environment.assumed`** Which of the air values nobody gave, so that a default was stored. Optional: a list of `rh` (stored 50 percent) and `lat` (stored 45 degrees). The connector's `log_group` writes it for a value that was neither said nor on the phone; since 2026-10-19 the app's group form writes it too, for a humidity or latitude it filled in (no value from the Calculator, and for the latitude no phone fix) and the shooter left as filled. Absent means nothing is said: every group the app's form logged before 2026-10-19, and every group logged before 2026-10-13. The Session screen shows "Not given, a default is stored" under such a group. Editing the group in the app drops the entry of each value that was changed. No fit reads it. Example: `["rh"]`.

**`environment.altitude_ft`** The site altitude of the firing point, in feet above sea level, when one was given. Optional; absent means not given, and a solve then counts from sea level, as for every group before 2026-10-13. Only a model that reads a site altitude uses it (the three classic values of the [lapse](../options/lapse.md) option and the classic temperature taper); the Standard, Physics and Spin models never do, because their air comes from the station pressure and temperature alone. The app shows the box "Site altitude" in the group form only when the profile's model reads one, and saves the value only then; the form offers the firing point's ground elevation with one tap and never fills it by itself. `log_group` takes it as `altitude_ft`, else the phone's Calculator value, else leaves it out. The form and the connector hold it to -1,500 to 20,000 ft, and so does the record schema. A fit under a model that reads it flies each group at its own altitude. Example: 5480.

**Limits, by writer.** A log field has no conversion step, so the limits are listed rather than tabled. Units are the field's own.

- App, the log form: each number must be readable as a number (digits with an optional decimal mark, a comma works; no exponent), and a blank air value is refused. The solver then checks the four numbers when the group is added: humidity 0 to 100, pressure above 0, latitude within 90, and a temperature above absolute zero, and the form shows its message. Since 2026-10-13 the form also refuses air from which no density altitude can be worked out, and since 2026-10-24 it says humidity 0 to 100 and latitude within 90 in its own words. There is no other upper or lower limit, so 2.9 inHg and 300 degrees Fahrenheit are saved (limits there await the owner).
- Stored record, checked when the group syncs: all four numbers and `environment.da_ft` must be numbers, and `environment.source` one of the four words. No range is checked, so a record with humidity 900 passes the sync, yet the fit refuses it.
- Connector, `log_group`: `temp_F` -40 to 130, `rh_pct` 0 to 100, `station_pressure_inHg` 15 to 32, `lat_deg` -90 to 90. A phone value for temperature or pressure outside the same bounds, or not a number, makes the tool refuse and name it. A phone humidity or latitude that is outside them, blank or not a number is replaced by the default (50 percent, 45 degrees) with a note.
- Solve and fit: humidity outside 0 to 100, pressure of 0 or less, latitude beyond 90 degrees, a temperature at or below absolute zero or a value that is not a number stops the solve with a message. A group with such a value stops any fit it is part of (used or held out) with that message; the fit does not leave it out by itself, but it runs once the shooter skips that group. The guided calibration's review of the groups stops on it the same way.

## Who writes it

**The app.** The range log's group form has "Temperature", "Humidity", "Station pressure" and "Latitude". "Add group" saves them through `buildObservation`, which also stores the density altitude, the word `manual` and the time. The form starts a group from the air of the session's newest group and keeps the air after each add, so a session's first group starts blank (unless a draft is restored or the group comes from the Calculator) and later ones carry the last air. The Weather button fills temperature, humidity and pressure from a weather service at the phone's position (a model, not a measurement) and sets a mark so that every group saved while it stands gets "air: weather model" added to its notes; typing any of the three clears the mark. Since 2026-10-20 it asks first when one of the three was typed since its last fill: it shows what it found with "Use these" and "Keep mine", and after a fill "Undo" puts the three back for a minute. For a model that does not read a site altitude, "From altitude…" under the pressure fills a standard-day station pressure from a typed altitude (5,000 ft gives 24.90 inHg), with a note until the pressure is typed. "Use phone GPS" fills the latitude only when it is blank. "Log this shot" in the Calculator hands over its four air fields, the weather-model mark and, when all three air values are still a connected rangefinder's, a rangefinder mark; the form shows "Air is from the rangefinder" while it stands, and typing in any of the three clears it. The air stays in the form after a group is added, and so does its mark, until one of the three is changed. A session reopened later starts its first group with a copy of the last group's air and no mark, and a half-typed draft restored after a reload keeps the mark it had. A Calculator that was never set starts at 59 degrees, 50 percent, 29.92 inHg and latitude 42, and those numbers are carried into a group like measured ones.

**The connector.** `log_group` needs a temperature and a station pressure, from what was said or from the phone's Calculator, or it refuses ("I don't know the atmosphere"). Humidity defaults to 50 percent and latitude to 45 degrees. The group's notes list each value that was not said by its word, under "from the phone" or "default", never with its number (since 2026-10-15; until 2026-10-12 the latitude's value was written too). The connector does not carry the Calculator's weather-model mark, so a group logged by voice from lookup air is not tagged.

**Never fitted.** No fit writes this block. Since 2026-10-13 the app can edit a saved group, its air included: the density altitude is worked out again, and the air source is kept while the temperature, humidity and pressure are unchanged; otherwise it is the edit's own (`phone` after a weather lookup in the editor, else `manual`) (what an edit keeps and works out again is on the [group](group.md) page). A backup restore brings records back as they were exported.

## Who reads it

**The fitter.** A calibration copies the four air numbers into each calibration group and solves the group with them and the profile's current model. A group with no `environment` block is left out as an incomplete record; no other air value excludes a group. A group's air is also the weather of the zero shot when the profile records no zero air; when it does, the group's latitude is still used for it (see [zero](../profile/zero.md)). The fit never reads the density altitude, the source or the time, and the temperature of the ammunition is a different field, covered on the [muzzle velocity](muzzle-velocity.md) page.

**The solver options.** Three options decide how these numbers are used when a group is solved again:

- [vapour](../options/vapour.md): the temperature and humidity give the water vapour in the air, and the Classic choice holds less of it above 80 degrees Fahrenheit.
- [gravity](../options/gravity.md): the latitude gives gravity only in the choice by latitude.
- [coriolis](../options/coriolis.md): the latitude sets the strength of the Earth's rotation effect, and the Off choice ignores it.

Four more options use the same air without a choice about the fields. [soundSpeed](../options/soundSpeed.md) picks the dry or the moist speed of sound, and only the moist one changes with the humidity and the pressure. [airDensity](../options/airDensity.md) picks the density the drag uses at the rifle, and [lapse](../options/lapse.md) and [tempClamp](../options/tempClamp.md) say how that air changes with height. A logged group with no site altitude is solved from sea level.

**The app.** The guided calibration reads the newest group's air as the conditions for the shot it suggests next. It flags a group as an air jump when its pressure is more than 0.3 inHg, or else its temperature more than 15 degrees Fahrenheit, away from the median of the same session's other groups logged within 2 hours of it. It flags a group whose stored density altitude differs by more than 200 ft from the one worked out from its weather with the profile's model. Both flags are advice. The Workbench and the chart panels solve each group again with its air. The CSV export writes the four numbers and the density altitude, rounded to whole feet.

**The connector.** `calibrate_profile` fits and previews through the same conversion, so a group's air decides whether it counts and how it is solved, and a bad value ends the preview with the message above.

**The server.** The admins' pooled export lists the four numbers and the density altitude of each group of the one share exported per profile (use permission: anything). Community and the pool pages estimate each group's Mach number at the target from the temperature and the pressure alone, and show the lowest and highest.

## Why it is stored

**Temperature, humidity, pressure and latitude** are everything the solver needs to redo the shot in the same air, besides the target, the muzzle velocity and the profile. They are saved with the group because the air changes by the hour, and a fit to a group needs the air it was fired in.

**The density altitude** is the number a shooter can compare with a weather meter's own density altitude and with the day before. The guided calibration uses it to spot a typo in the other three.

**The source and the time** were part of the first design, which expected a weather meter to write them. Only the rangefinder link and the weather lookup set the source to anything but `manual`, and nothing reads it: it is there so that a later look at the group can tell a measured air from a typed one. The time is read by the guided calibration.

## Changing it

The record is not versioned: a group is added, edited in place on the Session screen (since 2026-10-13), or deleted by setting its deleted mark. A deleted group is left out of every fit. The air does not belong to the profile, so changing the profile's zero air changes nothing here. A zero edit can still remove a group from a later fit; that is covered on the [observation](observation.md) page. The edit category is "none".

## Sharing and privacy

Every field on this page reads "as stored" for a reader of a shared log, except the latitude, the station pressure, the density altitude and the site altitude, each when location is hidden. With location hidden, `environment.altitude_ft` is removed: it is the same quantity as the firing point's ground elevation, which goes with the positions. A reader who fits such a log under a model that reads a site altitude then flies those groups from sea level. The source word travels with the values it belongs to: a reader sees `rangefinder` on a group whose air was a rangefinder's, and since the air is never hidden, the word is not either. With location hidden, `environment.lat_deg` is rounded to the whole degree, halves away from zero, and a result of -0 becomes 0; `environment.station_pressure_inHg` is rounded to 0.1 inHg (about 90 ft of altitude either way); and `environment.da_ft` is worked out again from the rounded pressure with the standard formula, to the whole foot, because with the temperature and humidity it would give the exact pressure back. A group whose air has no humidity loses its density altitude. With location shown, all of them are as stored. The rounding moves a come-up at 2,000 yd by at most about 0.03 mil, so a helper's fit or the pooled fit loses almost nothing. That holds for a share with one person, with the public or with the admins. The block holds no text, so hiding notes changes nothing here. The notes are a different field with their own switch; the [group record](observation.md) page says what a reader gets of them. The notes of an app group may say "air: weather model".

A reader can therefore always see the temperature and humidity, and the station pressure and density altitude, exact with location shown and rounded with it hidden. The rounded pressure with the temperature still gives roughly how high the range lies above sea level, and the latitude to a degree places it in a band about 70 miles wide when the location is hidden. The time a group was saved is also visible.

---

[All reference pages](../index.md)
