# Velocity

The one muzzle velocity the solver uses, the chronograph strings kept as evidence behind it, and the table of velocity against ammo temperature.

## Generated table

**Record:** profile · **Covers:** `ammo.mv_fps`, `ammo.mv_strings`, `ammo.mv_temp_table`

| Field | Type | Required | Limits in the schema | Solver reads | In a shared log |
|---|---|---|---|---|---|
| `ammo.mv_fps` | number | yes | min 300; max 5000 | yes | as stored |
| `ammo.mv_strings` | array of object | no | at most 20 items | no | as stored |
| `ammo.mv_strings[].date` | string | no | - | no | as stored |
| `ammo.mv_strings[].device` | string | no | - | no | as stored |
| `ammo.mv_strings[].es_fps` | number | no | - | no | as stored |
| `ammo.mv_strings[].mv_fps` | number | yes, within | - | no | as stored |
| `ammo.mv_strings[].n` | number | no | - | no | as stored |
| `ammo.mv_strings[].sd_fps` | number | no | - | no | as stored |
| `ammo.mv_strings[].shots_fps` | array of number | no | at most 50 items | no | as stored |
| `ammo.mv_strings[].temp_F` | number | no | - | no | as stored |
| `ammo.mv_temp_table` | array of object | yes | - | yes | as stored |
| `ammo.mv_temp_table[].mv_fps` | number | yes | - | yes | as stored |
| `ammo.mv_temp_table[].temp_F` | number | yes | - | yes | as stored |

## What it holds

These fields of a profile version describe how fast the bullet leaves the muzzle. `ammo.mv_fps` is the one velocity every solve uses. `ammo.mv_strings` holds the chronograph strings the shooter entered, kept as evidence: the solver never reads them. `ammo.mv_temp_table` says how the velocity changes with the temperature of the ammunition. The velocity is always present, and so is the table (an empty list is the usual state). The strings are optional: a profile without them is complete.

## What to know first

**One number drives every answer.** `ammo.mv_fps` is what the solver flies. A string never changes it by itself. Only a person does (the "Use as the muzzle velocity" button in guided setup, or a typed value) or a "MV true" calibration.

**The table counts only when an ammo temperature is given.** The app gives one when typed; the connector never does, so its answers ignore the table. The "not measured" mark is cleared only by an edit in the app: the connector's edit and a "MV true" fit leave it.

**Outside the table the line goes on.** Between its rows the velocity is interpolated in a straight line, and beyond its first and last row the same line is extended. Far from the rows it can reach zero or go negative, and the profile is still accepted.

**Editing the velocity leaves the table alone.** The table is not shifted, and the "reset" choice does not clear it unless "Also clear the MV-temperature table" is ticked. A stale table then contradicts the new velocity whenever a temperature is typed.

## The fields

**`ammo.mv_fps`** The velocity the solver uses, in feet per second: the average of a chronograph string from this rifle and this load, not the number on a box. Required. A new profile in the app starts at 2700. Example: 3048. How much it matters: for a 350 gr, G7 0.414 bullet near 3050 fps, 10 fps more velocity lowers the come-up by about 0.05 mil at 1,000 yd and about 0.16 mil at 2,000 yd. Guided setup lets the shooter tick "Don't know yet" for it; the profile then lists the velocity as not measured. Changing the value in the app removes that mark; changing it through the connector's edit_profile does not, so check_profile keeps saying the velocity is not measured.

**`ammo.mv_strings`** A list of at most 20 chronograph strings, in the order entered. Absent means none were entered. Each string has these members, only the first required: `ammo.mv_strings[].mv_fps` (the string's average, in feet per second), `ammo.mv_strings[].sd_fps` (standard deviation, feet per second), `ammo.mv_strings[].es_fps` (extreme spread, feet per second), `ammo.mv_strings[].n` (number of shots, a whole number above 0), `ammo.mv_strings[].temp_F` (the air or ammo temperature that day, degrees Fahrenheit; the screen does not say which) and `ammo.mv_strings[].device` and `ammo.mv_strings[].date` (free text: the chronograph and the day, kept as typed). Example: an average of 3041, SD 6, ES 18, 10 shots, 62 degrees, "radar", "2026-09-20". A string may also keep its shots, `ammo.mv_strings[].shots_fps` (at most 50 velocities in feet per second, in the order typed); when it does, the average, SD (n−1), ES and count beside them were computed from those shots, and a hand edit of any of the four in the app drops the shots, so the two always agree.

**`ammo.mv_temp_table`** The velocity at two or more ammo temperatures, as a list of rows sorted or not (the order is not required). Empty, the default, means the velocity does not depend on temperature. Each row has `ammo.mv_temp_table[].temp_F` (ammo temperature, degrees Fahrenheit) and `ammo.mv_temp_table[].mv_fps` (velocity at that temperature, feet per second), both required. Example: 3030 fps at 40 degrees and 3050 fps at 80 degrees, which is 0.5 fps per degree. Between rows the solver interpolates on the two rows either side of the temperature, beyond the ends it extends the line through the nearest two rows, and a table of one row is that row's velocity at every temperature. A table of two rows or more is what the "MV vs temperature" calibration saves (always two rows, at the coldest and the warmest group), but a table can have more rows.

**Limits, by writer.** App: the save check the profile screen's sections share. Connector: the input of `edit_profile` and `create_profile`. Conversion: the step every solve goes through first. Solve: what the solver itself refuses for one answer.

| Field | App | Connector | Conversion | Solve |
|---|---|---|---|---|
| `ammo.mv_fps` | 300 to 5000 | 300 to 5000 | > 0 | > 0 |

The velocity is in feet per second. The strings and the table have no row above: the connector cannot set either (`not settable`). The app's save check limits the strings to at most 20, requires each average above 0, each SD and ES to be 0 or more, each shot count to be a whole number above 0, each temperature to be a number, and a string's typed shots to be at most 50, each above 0; it checks nothing in the table beyond that the list exists. The conversion looks at no string and no table row, only at the stored `ammo.mv_fps` (so a table that gives a velocity at or below 0 for an ammo temperature is not a profile error). The solver refuses a velocity of 0 or less as "muzzle velocity must be > 0", the same kind of answer as a target that is too far, not as a profile error. The connector's `what_if` for `solve` and `range_card` takes a velocity above 0 and up to 6000.

## Who writes it

**The app.** The Calculator shows the velocity under the profile picker with "change", which opens the Today section in a sheet with its own save (the same save, keep-or-reset included), and "What if…" (also opened by tapping the model line), which solves with another velocity or model without saving anything ("Trying …: not saved", Reset; Log this shot still logs at the profile's own come-up). Quick setup has "Type shots from a chronograph" under its velocity box: the shots fill the velocity and are saved as a string with them. The profile screen's Today section has the velocity, the strings and, when the groups say the velocity is off, the range log's verdict with its one fix: one row per string, "Type shots" on each row (the shots are typed one at a time with "Add shot", fill the row's average, SD, ES and count as they go, and a tap on a shot removes it), a button "Use as the muzzle velocity" on each row (it says the velocity is set until the section is saved) (it fills the velocity box; nothing is saved until the step is saved), and, when the strings include two different temperatures, a tick "Fill the velocity-by-temperature table from these". That tick builds two rows or more, one per distinct temperature, each the rounded average of that temperature's strings; with fewer than two temperatures there is nothing to fill. The Calibrate screen's "MV vs temperature" method saves the table, and its "MV true" method changes the velocity. "Use this one again" on an older version copies that version's velocity and table.

**The connector.** `edit_profile` and `create_profile` take `mv_fps` (`create_profile` requires it). Neither takes the strings or the table; an input with another name, `mv_strings` among them, is not used, and the reply names it ("I ignored: ..."). `calibrate_profile` previews and, with the right scope, saves "MV true" (the velocity) and "MV vs temperature" (the table). Copying a shared profile and restoring a deleted one write these fields as the earlier record held them, without the limits above; the [origin](origin.md) and [versions](versions.md) pages describe both.

**The fitter.** Two calibration methods write these fields; see [calibration](calibration.md).

- *MV vs temperature* fits a straight line through the groups that carry an ammo temperature and saves a table of exactly two rows, at the coldest and the warmest of those groups, rounded to 0.1 fps. It needs groups at two temperatures at least 10 degrees apart. It leaves `ammo.mv_fps` as it is.
- *MV true* sets `ammo.mv_fps` to the average velocity of the groups it used plus a fitted offset, saved unrounded (the next MV true line says it as stored). If the profile has a table, the table moves by the new offset minus the offset an earlier MV true already added, so fitting again on the same data leaves it where it is. An "MV vs temperature" save after it rebuilds the table from the groups' own velocities, which starts the count again.

## Who reads it

**The solver.** The conversion passes `ammo.mv_fps` always. When the caller gives an ammo temperature that is a number and the table is not empty, it passes the table's velocity at that temperature instead: the two rows either side of that temperature (the nearest two when it is past an end), a straight line through them, and the same line extended past the first and last row. A table of one row gives that row's velocity at any temperature. If the two nearest rows share a temperature, the one nearer to the asked temperature is used. The velocity does not depend on any option: the Spin model's estimate of the bullet's behaviour also takes it, but through the same field.

**Who gives an ammo temperature.** The Calculator does, when its "Ammo temp" box has a number; the box is shown only when the selected profile's table has rows. The Session screen's calculated dope does the same with the form's ammo temperature. Nothing else does: the Workbench, the Calibrate screen, the range card made by the connector, `solve`, `sensitivity` and the pooled fit all use `ammo.mv_fps`. Typing an ammo temperature in the Calculator and then logging the shot carries that temperature into the group.

**Today's measured velocity replaces both.** When the active range log's switch to solve with the velocity measured in this session is on for the selected profile (the session's `measured_mv`, on the [session](../log/session.md) page), the Calculator's solve takes that velocity in place of `ammo.mv_fps` and does not read the table: a measured number is the velocity at today's temperature already. The stored profile is not changed.

**The fitter.** A fit does not use the profile's velocity for a group. It solves each group at that group's own measured average (the group's `mv`), so the profile's velocity and table do not change what a fit finds; the only fit that writes the velocity is MV true, from those averages. Guided calibration plans its next shots with `ammo.mv_fps`. The stored prediction on a logged group also uses the group's average, not the profile's. How a group records its velocity is on the [muzzle velocity](../log/muzzle-velocity.md) page.

**The app.** The Profiles screen and the shared-log header show the velocity. Guided setup's summary lists the strings. The printable worksheet has a blank table for them. The Calculator shows the "Ammo temp" box as above.

**The connector.** `solve`, `range_card`, `sensitivity` and `explain_dope` read the velocity through the conversion, without a temperature. `solve` and `range_card` take a `what_if` of `mv_fps`, tried for that one answer and not saved. `sensitivity` always reports what 10 fps is worth. `explain_dope` offers "a different velocity" as one explanation of confirmed come-ups. `check_profile` compares the velocity with the strings: when the strings, weighted by shot count (a missing count counts as one), average more than 10 fps from `ammo.mv_fps`, it reports both numbers. It also notes that there is no table when the table is empty, and lists a velocity marked not measured. `log_group` with no velocity given records `ammo.mv_fps` as the group's average and notes in the group that it was not measured.

## Why it is stored

**The velocity** is the one input every come-up depends on, and the only one a shooter measures directly. **The strings** exist so that the number is not a bare guess: they say how many shots, how steady (SD, ES), when, with what chronograph and at what temperature. The profile audit compares them with the velocity, and a second shooter reading a shared profile can judge how well the velocity is known. The solver does not need them. **The table** exists because powder velocity changes with temperature, so a shot in cold air is slower than the same load in the heat. Storing the line lets the Calculator follow the ammo temperature without a new profile for each day.

## Changing it

An edit saves a new version; the [versions](versions.md) page explains that once for every field.

**The edit category depends on the field.** A change to `ammo.mv_fps` is in the "ammo" category, with the ammo name and lot: the default is to reset the calibration. In the app a profile with anything a reset could remove gets the keep-or-reset question with "reset" already chosen, and the connector refuses to save until it is told `calibration: keep` or `calibration: reset`. A reset never clears the table unless the app's box "Also clear the MV-temperature table" is ticked (the connector cannot tick it). A change to the strings or the table is in no category: it never asks and never touches the calibration. Both are checked by `editEffect`.

**A calibration reset does not undo a fitted velocity.** MV true wrote `ammo.mv_fps`; resetting the calibration keeps it, along with the table. What a reset removes is on the [calibration](calibration.md) page.

**Older groups.** Neither a new velocity nor a new table makes older groups ineligible for a fit: eligibility compares the zero, not the velocity. Each group keeps its own measured velocity, so a velocity change leaves old groups meaning what they meant.

## Sharing and privacy

Every field on this page reads "as stored" in the table: a reader of a shared log sees the velocity, the strings and the table exactly as the owner does, on a share with one person, with the public or with the admins, and whether or not location and notes are hidden. That includes each string's date, temperature and chronograph name, which are free text the owner typed; the "hide notes" switch does not cover them. The table and the strings let a reader work out the load's temperature response and roughly when and in what weather it was chronographed.

---

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