# Calibration

The record of the calibration last saved into a profile version (method, groups, model, solver version, result) and the records it replaced.

## Generated table

**Record:** profile · **Covers:** `calibration`

| Field | Type | Required | Limits in the schema | Solver reads | In a shared log |
|---|---|---|---|---|---|
| `calibration.aero_source` | object | no | - | no | as stored |
| `calibration.aero_source.id` | string | no | - | no | removed for every reader |
| `calibration.aero_source.kind` | `'set'` or `'estimated'` | yes, within | - | no | as stored |
| `calibration.chi2_reduced` | number or null | yes, within | - | no | as stored |
| `calibration.fitted_at` | string | yes, within | - | no | as stored |
| `calibration.from_version` | number | yes, within | - | no | as stored |
| `calibration.held_out` | array of string | yes, within | - | no | set by the server for each share |
| `calibration.held_out_residuals` | array of object | yes, within | - | no | as stored |
| `calibration.held_out_residuals[].observation_id` | string | yes, within | - | no | set by the server for each share |
| `calibration.held_out_residuals[].range_yd` | number | yes, within | - | no | as stored |
| `calibration.held_out_residuals[].residual_mil` | number | yes, within | - | no | as stored |
| `calibration.history` | array of object; each entry has the same fields as `calibration` | no | - | no | removed for every reader; set by the server for each share |
| `calibration.method` | `'ssf'` or `'mv_temp'` or `'mv_true'` or `'cdf'` or `'dsf'` or `'full'` or `'spin_trim'` | yes, within | - | yes | as stored |
| `calibration.min_mach` | number | no | - | yes | as stored |
| `calibration.options` | object (the option choices) | no | - | no | as stored |
| `calibration.params` | array of object | yes, within | - | no | as stored |
| `calibration.params[].name` | string | yes, within | - | no | as stored |
| `calibration.params[].observed` | boolean | yes, within | - | no | as stored |
| `calibration.params[].sigma` | number or null | yes, within | - | no | as stored |
| `calibration.params[].value` | number | yes, within | - | no | as stored |
| `calibration.preset` | Standard or Physics or Custom or Classic or Spin | yes, within | - | no | as stored |
| `calibration.prior` | object | no | - | no | as stored |
| `calibration.prior.id` | string | no | - | no | removed for every reader |
| `calibration.prior.kind` | `'outline'` or `'measured'` | yes, within | - | no | as stored |
| `calibration.prior.level` | number | no | - | no | as stored |
| `calibration.reapplied_from` | number | no | - | no | as stored |
| `calibration.solver_version` | string | yes, within | - | no | as stored |
| `calibration.used` | array of string | yes, within | - | no | set by the server for each share |
| `calibration.verdict` | `'ok'` or `'weak'` or `'few'` or `'clamped'` | no | - | no | as stored |

## What it holds

This optional block of a profile version is the record of the calibration last saved into it: which method ran, when, from which version and which logged groups, under which model and solver version, and what came out (the fitted parameters, their uncertainty, a fit-quality number and the misses on held-out groups). Its history keeps the records it replaced. It says what happened; it is not the fitted values. A profile with no `calibration` has never had one saved, or has had it reset.

## What to know first

**The fitted values are not here.** The solver flies the curve, factor and drop table on [drag](drag.md), the velocity and its table on [velocity](velocity.md), and the scope factor on [scope](scope.md). Here it reads only `calibration.method` and `calibration.min_mach`, as a stand-in floor for a curve saved without one.

**The top record is the newest save, not always the fit behind the curve.** A tall-target, velocity-temperature or Spin trim save replaces it while an earlier drag fit still acts. That fit is then in `calibration.history`, whose newest entry is last.

**Nothing sets it by hand.** Only a calibration save writes the record. `edit_profile` can only keep or reset it. A reset removes the record and its whole history, with the curve; it keeps a fitted velocity, the velocity table and the scope factors.

**An old record is never refused.** No solve and no fit checks `calibration.solver_version`. After a model change the old fit keeps acting; the app and `calibrate_profile` only say "calibrated under" the earlier model.

**The group ids are the owner's.** `calibration.used` and `calibration.held_out` name logged groups by id, and a group deleted later stays listed. A shared log shows that share's own ids in their place, and leaves out `calibration.prior.id` and `calibration.aero_source.id`.

## The fields

**What was fitted.**

**`calibration.method`** Which calibration this record is of. Seven values: `'mv_true'` (MV true), `'cdf'` (one drag factor), `'dsf'` (a drop table), `'full'` (the whole drag curve), `'ssf'` (the tall-target scope check), `'mv_temp'` (velocity against ammo temperature) and `'spin_trim'` (the Spin trim, a factor on the Spin model's lift, see [spin](spin.md)). Required. The table in "Who writes it" says what each writes.

**`calibration.params`** The fitted parameters, in the fitter's own order. Each has `calibration.params[].name`, `calibration.params[].value`, `calibration.params[].sigma` (one standard deviation, in the value's unit; null when the data did not reach the parameter, and for the tall-target factor) and `calibration.params[].observed` (false for a point the used groups never reached, which was set and not fitted). The names are fixed words: `mv_offset_fps`, `cdf`, `ssf_elev`, `slope_fps_per_F`, a drop-table point as `dsf@` and its Mach to two decimals, `lift` for a Spin trim, a curve point as `k@` and its Mach, and, for a full fit, one `mv_bias_fps[...]` for each session that had a used group: a small velocity correction for that session. Example: a name of `cdf`, a value of 1.031, a sigma of 0.004, observed true.

**`calibration.prior`** A full fit only, and only when it did not start from the standard shape: which starting shape the curve was pulled toward where the groups said little. `calibration.prior.kind` is `'outline'` (estimated from the bullet's outline, see [bullet shape](bullet-shape.md)) or `'measured'` (a shape held for that bullet by some accounts). `calibration.prior.level` is the fitted scale of that shape at speeds no group reached, near 1. `calibration.prior.id` names the measured shape; it is an identifier, present only with `'measured'`, and is not shared. Absent block: the standard shape.

**`calibration.aero_source`** Which Spin coefficients the fit flew with. `calibration.aero_source.kind` is `'set'` (a fitted set attached to the profile) or `'estimated'` (worked out from the bullet's size and twist). `calibration.aero_source.id` identifies the set or the estimate's rule set; it can be absent, and is not shared. Absent block: the fit ran as point mass, or was a tall-target or velocity-temperature save, or was saved before 2026-10-05.

**`calibration.verdict`** A Spin trim only: how far the groups could tell the factor from 1. `'ok'`: a signal worth applying. `'weak'`: they cannot tell it from 1. `'few'`: fewer than three groups. `'clamped'`: the groups wanted a factor outside 0.5 to 1.5, which points at something other than spin drift (wind, the zero, the windage factor). Absent on every other method and on records saved before the trim. A save whose verdict is not `'ok'` is made only after the shooter says they still want it. Example: `'weak'`.

**From what.**

**`calibration.fitted_at`** When the save was made, as an ISO date and time in UTC. "Use this one again" carries the older record's time over unchanged. Example: 2026-03-01T10:00:00.000Z.

**`calibration.from_version`** The profile version the calibration was run on. A fit of version 4 is saved as version 5 with `from_version` 4. A hand edit that keeps the calibration carries the record on unchanged, so the number can be several versions behind. "Use this one again" keeps the older record's own number and says where the record came back from in `calibration.reapplied_from` (since 2026-10-24; before, it overwrote this number with that version's).

**`calibration.reapplied_from`** Set only by "Use this one again" (since 2026-10-24): the profile version whose calibration was put back on. The rest of the record is that version's, unchanged. Absent on every fit and on records saved before. Example: 7.

**`calibration.used`** The ids of the logged groups the fit used. For MV true this leaves out a group marked "use" whose bullet was slower than Mach 1.2 at the target: the fit drops it. For velocity against temperature it lists the groups whose velocities made the line. Empty for a tall-target save. For a Spin trim it lists the calm-air groups the fit used.

**`calibration.held_out`** The ids of the groups kept out of the fit and scored against it. Empty when none were held out, and always for the two direct methods and the Spin trim. A group in neither list was skipped, left out as ineligible, or logged later.

**Under what.**

**`calibration.preset`** The model the profile had when the fit ran: Standard, Physics, Classic, Spin or Custom. A profile with no model is recorded as Standard. See [model](model.md).

**`calibration.options`** The profile's stored option choices at that moment, copied as they were. Present only when the profile had some (normally a Custom or Spin profile).

**`calibration.solver_version`** The solver version the app or the connector was running, as text. Example: `solver-11`.

**With what result.**

**`calibration.chi2_reduced`** The fit's chi-squared per degree of freedom: about 1 when the misses left over are as large as the groups' own scatter predicts, well above 1 when something else is wrong. Null for the two direct methods, for the Spin trim and when the fit had fewer data than it needs to judge itself (one group and one parameter, for example).

**`calibration.held_out_residuals`** One entry per held-out group: `calibration.held_out_residuals[].observation_id` (the group's id), `calibration.held_out_residuals[].range_yd` (its range, yards) and `calibration.held_out_residuals[].residual_mil` (the fitted profile's come-up for that group minus the come-up the group needed, in true mil; plus means the fit predicts too much elevation). Empty when nothing was held out. The misses of the used groups are not stored.

**`calibration.min_mach`** The slowest Mach at the target among the used groups, worked out with the profile as it was before the fit. Present for the four fits, absent for the two direct methods and the Spin trim. It is a record of what the fit saw. The floor the solver obeys is `ammo.drag.min_mach` on the [drag](drag.md) page; the two are equal after a drag fit and differ after an MV true (which never moves the curve's floor).

**History.**

**`calibration.history`** The records this one replaced, oldest first, at most 20. Each entry has every field above and no history of its own. Every fit and every direct save moves the old record to the end of the list; when the list is full the oldest entry is dropped, except that the newest MV true and the newest velocity-temperature entries are always kept (since 2026-10-24), so a velocity table is never shifted twice. A suggestion's `entry` has the same fields: see [suggestion](../log/suggestion.md).

**No limits by writer.** No screen and no connector input sets any of these fields, so there is no table of limits. The record schema fixes the types and the closed lists in the table above and nothing else: no range on a number and no length on a list or a name.

## Who writes it

**The fitter, through one piece of code.** Every save, in the app or by the connector, builds the record the same way: the method and what it found, the time, the version it ran on, the solver version, the profile's model, and the old record moved into the history.

| Method | In the app | Writes to the profile | Parameters recorded |
|---|---|---|---|
| `'mv_true'` | MV true | `ammo.mv_fps`, and a shift of every row of `ammo.mv_temp_table` | `mv_offset_fps` |
| `'cdf'` | CDF | `ammo.drag.cdf`, `ammo.drag.min_mach` | `cdf` |
| `'dsf'` | DSF | `ammo.dsf`, `ammo.drag.min_mach` | six drop-table points, Mach 0.8 to 1.3 |
| `'full'` | full fit | `ammo.drag.knots`, `ammo.drag.min_mach`; removes `ammo.drag.cdf` and `ammo.dsf` | 28 curve points, Mach 0.7 to 3.0, then one velocity correction per session |
| `'ssf'` | tall target (SSF) | `scope.ssf_elev`, rounded to four decimals | `ssf_elev`, not rounded |
| `'mv_temp'` | MV vs temperature | `ammo.mv_temp_table`, replaced by two rows | `slope_fps_per_F` |
| `'spin_trim'` | Spin trim | `ammo.aero_trim`, to three decimals | `lift`, with its sigma |

A save changes only what its row names and keeps the rest: a CDF or DSF fit on top of a full fit keeps the curve, and a tall-target save keeps the curve, the factor and the table. A Spin trim writes the factor and its record and nothing else; a newer trim replaces an older one at the head of the record, so the record it goes on top of is the one under the older trim. A fit that did not converge is not saved. The full fit does not write the velocity: its per-session corrections are only recorded.

**MV true and the velocity table.** The new `ammo.mv_fps` is the average of the used groups' measured averages plus the fitted offset. The table holds chronograph velocities, so it gets the same offset: each row moves by the new offset minus the offset an earlier MV true already added, rounded to 0.1 fps. The earlier offset is found in this record and its history, newest first, and counts as 0 once a velocity-temperature save is met, since that save rebuilds the table from measured velocities. Running MV true twice on the same groups leaves the table where it was.

**The app.** The Calibrate screen saves the six earlier methods, and on a Spin profile the Spin trim as a seventh, from the groups marked calm (and those typed with 2 mph of wind or less when its switch is on); the Workbench's Spin trim panel saves the same trim. Accepting a helper's suggestion runs the suggested fit again on the owner's own groups and saves it as any fit; nothing in the record says it came from a suggestion. "Use this one again" does not run a fit: it puts an older version's velocity, table, curve, factor, drop table, floor and both scope factors on the current version together with that version's record, marked with `calibration.reapplied_from` (the older version's number). Since 2026-10-24 the record it replaces and that record's history become the new history (the newest 20 kept), as after a fit, and an older version with no record is refused with "That version has no calibration to use again." and nothing is saved. The profile screen's sections write nothing here; a reset chosen there removes the block.

**The connector.** `calibrate_profile` runs MV true, CDF, DSF, the full fit, velocity against temperature or the Spin trim, and `tall_target` the scope check. Each only previews unless `save` is true, and a save needs the `profiles` token scope. A `calibrate_profile` save must repeat the preview's arguments and its fingerprint. The fingerprint is not stored anywhere: it is a short hash of the version's id, the method, the starting shape, the solver version and every group the fit could use with its last-changed time and role. `edit_profile` and `create_profile` have no input for this block or for the curve, the factor, the drop table, the floor or the velocity table; such an input is not used, and the reply names it ("I ignored: ..."). `edit_profile` takes `calibration` only as the words keep or reset. `calibrate_profile` with `remove` and `save` takes a saved Spin trim away (no fingerprint); `edit_profile`'s reset removes it with the rest.

**Copies and restores.** A copy of a shared profile carries the block as the reader saw it, and a restored profile as the deleted version held it; the [origin](origin.md) and [versions](versions.md) pages describe the two. The server does not write the block.

## Who reads it

**The solver.** Only when a profile has a curve, a factor or a drop table and no `ammo.drag.min_mach`: the conversion then takes `calibration.min_mach` as the floor if `calibration.method` is `'cdf'`, `'dsf'` or `'full'`. It looks at the top record only, never the history. What the floor does is on the [drag](drag.md) page.

**The fitter.** MV true reads the record and its history for the offset already in the velocity table. Nothing else in a fit reads the record: a fit starts from the profile's fitted values, not from `calibration.params`.

**The "calibrated under" check.** It compares `calibration.preset` and `calibration.options` with the profile's model now, by the options each resolves to. It reads the newest calibration record only, and a tall-target, velocity-against-temperature or Spin trim save writes a record of its own under the model then current, so after one of those saves the check says nothing. It answers with a short phrase when they differ, when the recorded model is not one it knows, or when the profile is a Spin profile and `calibration.solver_version` is older than `solver-8`, the version from which the Spin model's estimated coefficients are today's. That is the only use of the solver version. The check warns; it stops nothing.

**The app.** The Calculator's "Calibrated" line gives the method, the date, the number of used groups, a full fit's starting shape when it was not the standard one, and the earlier methods still in effect (read from the history; a Spin trim is named only while the profile still holds one), then the "calibrated under" phrase. The profile screen's Model section and the Calibrate screen show the same phrase. The Calibrate screen's "Earlier calibrations" lists each version that has a record, with a line for `calibration.aero_source`; that block exists only for a fit made with the bullet-flight choice [dynamics](../options/dynamics.md) off point mass, and its identifier names a rule set chosen by [estimatorRules](../options/estimatorRules.md) when `calibration.aero_source.kind` is `'estimated'`. The Workbench opens on the newest drag fit in the record or its history: that fit's method, its `calibration.used` and `calibration.held_out` as the starting roles (every other group skipped), and its starting shape; it says how many listed groups are no longer in the log. Guided calibration reviews groups against the same starting shape. No screen shows a saved record's parameters, chi-squared or held-out misses: those numbers are on screen for a fit just run, and in a suggestion.

**The connector.** `calibrate_profile` replies with the fit's parameters, chi-squared, each group's miss (on an MOA scope also in MOA, and the spoken worst miss in MOA, since 2026-10-16) and its warnings, among them "The existing calibration was calibrated under ..." from the check above. `explain_dope` adds the caveat that the profile's calibration was used as it is. `edit_profile` reports `calibration` in its details: `"reset"`; `"kept"` after any save of a profile that holds a fitted drag correction or a record, whatever was edited; or null when it holds neither. Its spoken line adds "calibration kept" only after a change that asked. The tools that return a whole profile show the block with its option choices in the solver's value names, and remove `calibration.aero_source.id` unless the account holds the fitted sets.

**Sharing.** The share filter reads the block to replace and remove identifiers; see "Sharing and privacy".

## Why it is stored

**Method, groups and time.** So the shooter, a helper and the Workbench can see which groups a curve rests on, and run it again with one group more or less.

**Model and solver version.** A fitted correction belongs to the model it was fitted under: under another model the same factor corrects the wrong thing. Recording the model lets the app say so.

**Parameters and quality numbers.** To keep what the fit found and how sure it was beside the values it wrote, since the values alone do not say which points the data reached. They also let a suggestion be checked: the owner's re-run must reproduce the helper's parameters.

**The floor.** A record of how slow the fit's groups got. It stands in as the solver's floor only for curves saved before the curve carried its own.

**The starting shape and the coefficient source.** A full fit depends on the shape it started from, and a Spin fit on the coefficients it flew with. Without them a later re-run could not start the same way.

**History.** Methods stack: a scope check, a true velocity, a Spin trim and a curve can all act at once, and only one can be the top record.

The Workbench's comparison ranges and air are display settings; they are kept with the account's interface preferences, not here, and never make a profile version.

## Changing it

An edit saves a new version and each logged group keeps the version it was shot with; the [versions](versions.md) page explains that once for every field.

**No field here is edited.** What a hand edit of other fields does to this block depends on the edit. A change to the bullet, the drag model or its coefficient, the twist or its direction, the sight height, the velocity, the ammo name or lot, or the attached Spin coefficients defaults to reset; a change to the zero or a scope factor defaults to keep; a change of model, and of any field not named, asks nothing and keeps. The app asks with the default chosen; the connector saves a keep-by-default edit without asking and refuses a reset-by-default edit until told which. Each field's page has its own case. A changed bullet also drops a saved Spin trim, whatever is answered, and takes a record headed by the trim with it (see [spin](spin.md)).

**What a reset removes.** The curve, the drag factor, the drop table, the curve's floor, the choice of what to use below the floor, the Spin trim, and this block with its history. It keeps `ammo.mv_fps` (also one that MV true wrote), the velocity table, both scope factors, the calibration goal and the Spin choices. In the app two boxes, off by default, add the velocity table and the scope factors (back to 1) to the reset. The connector's reset never removes those two. A kept calibration is carried to the new version exactly as it was.

**Groups.** Deleting or changing a logged group never changes a saved record: the id stays in its lists. A later fit sees the log as it is then. See [group](../log/group.md) and [observation](../log/observation.md).

## Sharing and privacy

A reader of a shared log sees the block on every profile version of the share, the same on a share with one person, with the public or with the admins, and whether or not location and notes are hidden.

- `calibration.used`, `calibration.held_out` and `calibration.held_out_residuals[].observation_id` hold ids the share made for that reader, matching the ids of the groups in the same shared log. The owner's own group ids are not shown.
- `calibration.prior.id` and `calibration.aero_source.id` are removed for every reader, admins included. The kind and the level stay.
- `calibration.history` gets both treatments in every entry.
- Everything else is as stored: the method, a Spin trim's verdict, the date and time, the versions (`calibration.from_version`, and `calibration.reapplied_from` on a record put back by "Use this one again"), the model and its option choices, the parameters with their names, the quality numbers and the floor.

What that lets a reader work out: when the owner calibrated, how many groups and which ones, how well the fit did, and from the floor roughly how far the slowest group was. A copy of a shared profile takes the block along, with ids that match no group of the copier's. A suggestion goes the other way: its `entry` is sent to the owner alone, with the ids turned back into the owner's; see [suggestion](../log/suggestion.md).

---

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