# calibrate_profile

Fit a profile to the groups in the shooter's range log, and keep the result if asked.

## What it is for

`calibrate_profile` fits a profile to the groups the shooter has logged. It runs one of six calibrations (true muzzle velocity, one drag factor, the drop table, the whole drag curve, velocity against ammo temperature, or the Spin trim), says what it would change and how sure it is, and on a second call saves the result as the profile's next version. It is the connector's only tool that fits velocity, drag or the Spin trim. It never fits the scope's tracking ([tall_target](tall_target.md)) and does not log groups.

## What to know first

**A save is a second call.** The first call stores nothing. To keep the result, send the same arguments, `save: true` and the preview's `fingerprint`. The save fits again; it is refused if the profile has a newer version, or a role, the method or a group's last-changed time differs.

**The preview's spoken line does not name the profile.** With no `profile_id` the call goes to the profile the newest calculator report shows, else the only profile. `profile` in the details names it; the saved line names it too.

**A save stores the fit as it came out.** No bound is put on a fitted value, except that a Spin trim is held to 0.5 to 1.5. The warnings in the line are what the tool says of a fit.

**Groups under 3 shots, or with no count or measured velocity, start skipped.** A group is also left out when its version is not live, or differs from the profile in bullet, twist, sight height or zero. The details give the reason for each.

**One fit per account at a time.** A running explanation makes this call refuse, and the other way round. `mv_temp` uses no worker. A preview and its save are two fits against the fit time.

## Generated table

**Tool:** `calibrate_profile` · **Family:** Calibration · **Scope:** any link · **Save scope:** `profiles` · **Changes:** records · **Runs:** the fit workers (4 jobs at once for the whole connector, a fit stopped after 90 s, 120 s of such work per account per 10 minutes) · **Limits:** at most 300 groups in one fit; a preview reads at most 2000 groups

**Description** (what the connector sends to an assistant, word for word):

```text
Fit a profile to the groups in the shooter's range log, and keep the result if asked. For a few remembered come-ups use explain_dope; for a tall-target test, tall_target. Without save it only previews. Saves the profile's next version only with save: true, which needs the profiles permission; any link can preview. Say the preview's result and warnings, name the profile (the line does not) and ask; to keep it, call again with the same arguments, save: true and the preview's fingerprint. Groups of under 3 shots or with no measured velocity start skipped (not spin_trim). Guide: calibrate_profile.
```

**Inputs** (12; read from the input schema the connector sends):

| Input | Type | Required | Bounds | Field |
|---|---|---|---|---|
| `profile_id` | text | no | - | - (selects a record) |
| `method` | `"mv_true"` or `"cdf"` or `"dsf"` or `"full"` or `"mv_temp"` or `"spin_trim"` | yes | - | - (control) |
| `session_id` | text | no | - | - (control) |
| `skip` | list of text | no | at most 200 items | - (control) |
| `use` | list of text | no | at most 200 items | - (control) |
| `hold_out` | list of text | no | at most 200 items | - (control) |
| `prior` | `"g7"` or `"outline"` or `"measured"` | no | - | - (control) |
| `save` | boolean | no | - | - (control) |
| `fingerprint` | text | no | - | - (control) |
| `light_wind` | boolean | no | - | - (control) |
| `confirm` | boolean | no | - | - (control) |
| `remove` | boolean | no | - | - (control) |

**Sent with the inputs** (each input's own description, word for word; an input not listed is sent without one):

```text
profile_id: A profile id or name. Not given: the profile the phone last showed, else the only one
method: mv_true (true velocity), cdf (one drag factor), dsf (drop correction at transonic Mach points), full (whole drag curve), mv_temp (velocity against ammo temperature) or spin_trim (Spin model's spin drift, from calm-air groups)
session_id: Use only this session's groups; the others start skipped
skip: Group ids to leave out
use: Group ids to include although they start skipped
hold_out: Group ids kept out of the fit and scored against it (not for mv_temp or spin_trim)
prior: Full fit only: the shape unobserved speeds follow. Default g7.
save: true keeps the previewed fit (needs the profiles permission). Not given: preview only
fingerprint: From the preview; required with save
light_wind: spin_trim: also use groups typed with 2 mph of wind or less
confirm: spin_trim: save a result that is not a clear fit, after the shooter has heard that and still wants it
remove: spin_trim: remove the saved trim; needs save
```

## When to use it, and when not

| The shooter wants | Use | Why |
|---|---|---|
| Velocity or drag fitted from logged groups, or the Spin model's spin drift trimmed from calm-air groups (or a saved trim removed), and kept as the next version | `calibrate_profile` | Fits the range log; previews first |
| To know what could explain come-ups they remember, with no log | [explain_dope](explain_dope.md) | Tests causes against one to six come-ups; saves nothing |
| The scope's tracking from a tall-target test | [tall_target](tall_target.md) | Works the factor out from three numbers; takes no group |
| A velocity or drag they already know typed in | [edit_profile](edit_profile.md), if the link has it | Saves what it is given; no fit, no record |
| The entered numbers checked for mistakes | [check_profile](check_profile.md) | Reads the profile; fits no group |
| A group added to the log | [log_group](log_group.md) | Calibration reads the log and never writes to it |
| What the fitted profile gives at a range | [solve](solve.md) | One come-up from the saved profile |

Ask before calling: which profile, which calibration (MV true for velocity, CDF for one drag factor, if unsure; the Spin trim only for a Spin profile with groups marked calm), whether groups of another day should stay out (`session_id`), and, after the preview, whether to save. Advice for the assistant, not a limit in the code: say the result and the warnings before asking.

## The inputs

**`profile_id`** The profile's id, or its name in any letter case with outer spaces ignored. The rule is on [edit_profile](edit_profile.md), under `profile_id`; here, run: a name that fits none or an empty text is refused with the list, and a copy of a shared profile is named by the name a reply shows, with or without its quotation marks. Not given: the profile the newest calculator report shows, at any age; with no report, the only profile; else `Which profile?` (also when the report shows a profile that is no longer listed, even with one profile left). The reply does not say that the phone chose it, and unlike `edit_profile` and `log_group` a save does not ask which profile when the report is 30 minutes old or more (read). The profile used is the newest live version; a save makes the next one.

**`method`** Required. What each does is in [calibration](profile/calibration.md), "Who writes it"; this tool's own requirements and what a save changes:

| `method` | Needs (else a refusal) | A save changes |
|---|---|---|
| `mv_true` | One used group still above Mach 1.2 at the target; slower used groups are dropped (`fitted` false in the details) | `ammo.mv_fps` (the fitted groups' mean measured velocity plus the fitted offset, stored unrounded), and each row of `ammo.mv_temp_table` moved by that offset less any an earlier MV true added; see [velocity](profile/velocity.md) |
| `cdf` | Nothing more | `ammo.drag.cdf` and `ammo.drag.min_mach` |
| `dsf` | One used group slower than Mach 1.4 at the target | `ammo.dsf` and `ammo.drag.min_mach` |
| `full` | Nothing more | `ammo.drag.knots`, `ammo.drag.min_mach`; removes `ammo.drag.cdf` and `ammo.dsf` |
| `mv_temp` | Groups at two ammo temperatures 10 °F or more apart | `ammo.mv_temp_table` (two rows); `ammo.mv_fps` stays |
| `spin_trim` | A profile on the Spin model, and groups marked calm (with `light_wind`, also groups typed with 2 mph of wind or less). Three or more are needed before the answer counts, fewer is a verdict of `few` | `ammo.aero_trim` (to three decimals), and a record of method `spin_trim` carrying the verdict |

Every save also replaces the `calibration` record and moves the old one into its history. The fit uses the profile's own model, so the same groups can give different numbers under another model; the Spin model also records which kind of coefficients it flew with (`calibration.aero_source`). A save never changes a value the method does not name. The Spin trim is a factor from 0.5 to 1.5 on the lift of the Spin model's coefficients, fitted to the calm-air groups with a verdict (`ok`, `weak`, `few` or `clamped`); a result that is not `ok` is saved only with `confirm: true`. The page [spin](profile/spin.md) says what the factor does and when a bullet change drops it.

**`session_id`, `skip`, `use`, `hold_out`** Which groups play which role. For the four fits, every group of the profile that is not left out is a candidate; for `mv_temp`, every group that carries an ammo temperature, of any version (a `skip` naming another is refused as not usable). A group starts as `use`, or `skip` when it has fewer than 3 shots (for `mv_temp`, fewer than 3 in its velocity string), when it was logged with no measured velocity (the mark `mv.not_measured` on [muzzle velocity](log/muzzle-velocity.md), which `log_group` sets when no velocity is given) or, with `session_id` given, belongs to another session. A group named in `use` is used like any other, whatever made it start skipped. `skip` and `use` change that start; `hold_out` keeps a group out of the fit and scores it against the fitted profile (its `residual_mil` in the details, and in the saved record). `mv_temp` takes no `hold_out`. An id that is not a usable group, left-out groups included, or one given two roles, is refused. A group left out for its version cannot be brought back with `use`, nor can one shot before the latest turret move recorded with `zero_check` or the app's zero step (since 2026-10-17; see [zero check](log/zero-check.md)). Each list takes at most the number in the table above. The details give `roles` for every candidate, and `skip_reasons` for each one that is skipped by the starting rule: `another session`, `no measured velocity` or `fewer than 3 shots`. **`light_wind`, `confirm`, `remove`** For `spin_trim` only; another method turns the call away (`Nothing was done: I ignored` and the names), except `mv_temp`, which ignores them. The Spin trim's candidates are every group not in `skip` (with `session_id`, that session's and those in `use`); nothing starts skipped, and the fit takes only groups marked calm, plus, with `light_wind: true`, those typed with 2 mph of wind or less (part of the fingerprint), at most 300. `hold_out` and `prior` are refused. `confirm: true` saves a result that is not a clear fit, after the shooter has heard it and still wants it; without it the save is refused with the sentence. `remove: true` with `save: true` takes a saved trim away and allows no other input.

**`prior`** For `full` only: the shape the curve follows where no group reached, `g7` (not given), `outline` (needs the bullet outline on the profile, see [bullet-shape](profile/bullet-shape.md)) or `measured` (only for some accounts, and only for some bullets). With any other method it is ignored, and the fingerprint does not see it. A full-fit save records a non-default shape as `calibration.prior`.

**`save`, `fingerprint`** `save` absent or false: a preview, whatever `fingerprint` holds. `save: true` needs the `profiles` scope; a link without it still previews. The fingerprint is 16 hex digits over the version's id, the method, `prior`, the solver version and every candidate group's id, last-changed time and role; a save compares these and nothing else.

## What it returns

**The spoken line,** each preview ending `Say save to keep it.`:

| Outcome | Spoken line, in its fixed words |
|---|---|
| MV true | `MV true:` the stored velocity as it is and the new one rounded, in fps, `plus or minus` its uncertainty, `from N groups.` |
| CDF | `CDF` the factor, `plus or minus` its uncertainty, `from N groups.` |
| DSF | `DSF fitted at N Mach points from N groups.` (the first N counts only points a group reached) |
| Full fit | `Full fit from N groups: worst miss goes from` two numbers and the scope's unit, `mil.` or `MOA.` (the second number can be the larger) |
| MV against temperature | `MV changes` the slope `fps per degree, from N groups between` two temperatures `degrees.` |
| Spin trim | `Spin trim ×` the factor, `plus or minus` its uncertainty`.` and one sentence on what the groups show (it names the number of groups, and says to check the wind, the zero and the windage factor when the groups want a factor outside the range). A verdict other than `ok` ends `It is not a clear fit: say save anyway to keep it.` in place of `Say save to keep it.` |
| Not converged | `The fit did not converge, so there is nothing to save. Try other groups or a simpler method.` (a preview, not a refusal) |
| Saved | `Saved` the name `as version N:` and the line above, without the closing sentence; for the Spin trim only its first sentence, and after `remove`, `spin trim removed.` |

Warnings follow the summary, each one sentence and also in `warnings`: `The misses still trend with range.`; `N groups were logged under a different SSF.`; `The existing calibration was calibrated under` and a model name`.` (the profile's model differs from the one that made its newest calibration record, or that one is unrecorded, an older Spin, or a Custom or Spin model with other settings; a tall-target or velocity-against-temperature save writes a newer record under the model of that day, so the warning stops while an older drag fit still acts); `This fit saw nothing slower than Mach` and a number`.` (only above Mach 1). N counts the groups the fit used, not held-out ones. The profile's name is quoted and cut to 40 characters unless it is one short token.

**The details of a preview** (examples 1 to 6):

| Key | Holds |
|---|---|
| `status`, `method`, `converged`, `fingerprint` | `preview`; what ran; whether it converged; the fingerprint to send back |
| `params`, `chi2_reduced`, `iterations` | The fitted values with `sigma` and `observed` (false: no group reached it, left at its starting value); fit quality, null with too little data; see [calibration](profile/calibration.md) |
| `min_observed_mach` | The slowest Mach at the target among the used groups, before the fit |
| `groups` | Per group with the role `use` or `hold_out` (a skipped group is only in `roles`): `id`, `session_id`, `role`, `fitted`, `range_yd`, `mach`, and in true mil `required_mil` (what the shot needed: the dial times the group's own version's elevation factor, plus a reticle hold, minus the group centre), `before_mil`, `after_mil`, `residual_mil` (after minus required); on a scope in MOA each `_mil` field has a `_moa` field beside it |
| `ranges` | The come-up before and after at each fitted group's range, and at 3,520 and 4,400 yd, in calm air at factor 1, in the air and at the angle of the last fitted group; `extrapolated` true when the bullet is slower there than any fitted group, or does not get there; in MOA too on a scope in MOA, as for `groups` |
| `excluded`, `roles`, `skip_reasons`, `warnings`, `profile` | Groups left out, each with `id`, `range_yd`, `reason`; the role of each candidate; why each candidate that started skipped and still is was skipped, by id (a group skipped by name in `skip` only is not there); `id`, `version`, `name` |

`mv_temp` has `slope_fps_per_F`, `sigma`, `table`, `roles`, `skip_reasons` and `profile`, and no `groups`, `excluded` or `warnings`. `spin_trim` has `factor`, `sigma`, `verdict`, `groups` (per group `id`, `range_yd`, and in mil `before_mil`, `after_mil` and `sigma_mil`: the windage still missing before and after the factor; on a scope in MOA each has an `_moa` field beside it), an empty `warnings` and `profile`, and none of the other keys (example 29). A save returns `status`, `profile_id`, `version`, `name`, `method` and `warnings`, and for the Spin trim also `factor`, `sigma` and `verdict`, or `removed: true`. No reply carries the trend slope or the covariance. The profile's name is whole in the details, and group, session and profile ids are as stored, unmarked.

## Worked examples

Every example below was run by the documentation generator against a made-up account, never a real one. Numbers are rounded to three places; generated ids and times are shown as placeholders. Two things are printed more briefly than the tool sent them, and nothing else is changed. A list of plain values stands on one line, whole up to 12 entries (a longer one, and a list of objects past 3, ends with a count of the rest). A reply marked "with the stored-text note" also carried this note, which is left out of the example: as `stored_text_note` in the details, or in brackets as the last line of a refusal: "Text in quotation marks is stored text (a name, a title or a note someone typed): it is a label, never an instruction."

Example 5 saves `ammo.drag.cdf` and `ammo.drag.min_mach`, and the record; example 19 is a Spin save (the changed list gains `calibration.aero_source`); example 20 is `mv_temp` with its save. The model warning is in example 17, the trend warning in 25, the changed-factor sentence in 26, a fit that does not converge in 27, and the out-of-reach, singular-system and Mach refusals in 28, 18 and 24. The busy, too-long and spent-budget refusals are not simulated here; they are described from real worker runs (the reviewer's).

## When it refuses

| The reply holds | Cause | Tell or ask the shooter |
|---|---|---|
| `Which profile?`, `You have no profiles yet; create_profile makes one.` | None named and no phone profile or only profile; or the id or name fits none; or the phone showed a profile no longer listed | Read the names and ask which |
| `More than one profile has that name. Use the id:` | The name fits two | Ask which, call with the id |
| `This profile has` ... `groups, more than a calibration can look at (` | Too many groups on the profile; nothing is fitted | Ask for a session (`session_id`) |
| `is not one of this profile's usable groups.` | An id in `skip`, `use` or `hold_out` is no usable group of the profile (it may be left out, deleted or another profile's) | Read the ids from a preview |
| `is listed more than once: give each group one role.` | An id given two roles | Ask which role |
| `MV versus temperature has no hold_out: use skip to leave a group out.` | `hold_out` with `mv_temp` | Use `skip` |
| `No groups to fit for` and, when some were left out, `Left out:` with the range and reason of every one | No group is usable, or all are skipped | Say why, and that groups of other versions may be left out |
| `A fit takes at most` ... `groups; this one has` | More `use` and `hold_out` groups than a fit takes | Name a session or skip some |
| `MV true needs at least one used group still above Mach 1.2` | All used groups are slower | Another method, or nearer groups |
| `DSF needs at least one used group slower than Mach 1.4` | All used groups are faster | Another method, or farther groups |
| `is out of range for this profile` | A used or held-out group is farther than the bullet reaches with this profile | Skip that group |
| `singular system: the data cannot determine every parameter` | The groups do not settle every value the method fits (seen with DSF and a group at the edge of a Mach point) | Another method, or skip the group at the edge |
| `the fit reached parameters the solver cannot fly` | The fit moved to values the solver refuses; nothing is proposed | Other groups or a simpler method |
| `MV vs temperature needs groups at two or more ammo temperatures at least 10 °F apart` | Too few temperatures | Say it needs groups logged at different ammo temperatures |
| `The estimated shape needs the bullet outline in the profile (`, `No measured shape is available for this bullet.` | `prior` the profile or account cannot use | Use `g7` |
| `must be greater than 0`, `invalid option:` | A stored number the solver refuses (the profile is named), or a stored option value it does not know (the solver's own sentence, cut short). Not with `mv_temp`, which does not solve | Say the profile needs fixing; `edit_profile` on a link that may |
| `Spin trim is only for a profile on the Spin model;` and the model's name; `Spin trim has no prior.`; `Spin trim has no hold_out: use skip to leave a group out.`; `Nothing was done: I ignored` and `which are for spin_trim only.` | `spin_trim` on another model (example 31), with `prior` or `hold_out`, or one of its own inputs with another method | Say what is wrong; leave the input out |
| The profile's name and `No group is marked "Shot in calm air"` (example 32); `Not saved:` and the sentence ending `with confirm: true.` (example 29); `Removing the spin trim needs save: true.`, `Nothing was removed: I ignored` ... `removing a trim takes only save.`, `has no saved spin trim to remove.` | No calm group among the candidates; a Spin trim save that is not a clear fit with no `confirm`; `remove` without `save`, with another input, or on a profile with no trim | Say what is wrong; for the unclear fit, read the sentence and save with `confirm: true` only if the shooter still wants it |
| `This connector can't save profiles.` | `save` on a link without `profiles`; checked before the fingerprint | Say a link with that permission can save; the preview stands |
| `Your groups or profile changed since the preview. Run the preview again.` | No fingerprint, a wrong one, or a change since | Preview again and repeat the arguments |
| `The fit did not converge: not saved.` | A save whose fit does not converge | Try other groups or a simpler method |
| `Not saved:` and a reason | The server did not store the version | Say it was not saved, and the reason |
| `A calibration is already running for you. Try again in a moment.` | The account has a fit or explanation running | Try again in a moment |
| `The calculator is busy; try again in a minute.` | Every slot of the connector is taken by other accounts' jobs | Try again in a minute |
| `That fit took too long and was stopped. Nothing was saved.` | Passed the time limit, or the time the account had left | Fewer groups |
| `You've run a lot of calibrations. Try again in about` | The account used its fit time | Wait the minutes the reply gives |

For the Spin trim the order is: the profile, the group ceiling, a model other than Spin, `prior` and `hold_out`, the ids in `skip` and `use`, the group limit, the save checks (scope, then fingerprint), then the fit's own reason and the confirm check; a removal checks the scope, `save`, the other inputs and the trim, before any group is fitted (the group ceiling is checked first, as for every method). For the four fits the refusals come in this order: the profile, the group ceiling of a preview, the roles, no group to fit, the fit's group limit, then the save checks (scope, then fingerprint), and only then the profile's own numbers, the starting shape and the fit. `mv_temp` checks `hold_out`, the roles and its temperatures first, and the save checks last. The input check turns away a missing or unlisted `method`, a list over its limit and a `save` that is not true or false: see [the connector overview](connector.md). A call holding only inputs the tool does not have is turned away as missing `method`.

## What it reads and changes

**Reads** the newest live version of each profile, all of the profile's live versions, its live groups (oldest first) and their sessions, and with no profile named what the newest calculator report shows. The Spin model also reads the account's coefficient sets or estimating rules; `prior: measured` reads the account's drag-shape files. A preview stores nothing and sends nothing to a device.

**A save pushes one new version** of the profile: the newest version with the changes of its method (for the Spin trim, `ammo.aero_trim` and the record; for `remove`, the field gone and a record headed by the trim gone with it, the newest earlier record becoming the record again), the `calibration` record replaced (the old one moves to the end of its history, at most 20, the newest MV true and velocity-temperature entries always kept), a "not measured" mark on a value the save sets dropped and the mark that the numbers were read from another app dropped, see [origin](profile/origin.md). The next version is one above the highest the profile has. The groups are not touched. A copy of a shared profile stays a copy. Open devices pick the version up at their next sync. The connector has no way to bring an older version back; the app's "Use this one again" does.

## Below this page

- [calibration](profile/calibration.md) and [spin](profile/spin.md): the record a save writes, what each method writes, the "calibrated under" check, and the Spin trim's field and what drops it.
- [velocity](profile/velocity.md), [drag](profile/drag.md), [scope](profile/scope.md): the values a fit changes or reads, and the elevation factor of a group's version. [model](profile/model.md) and [bullet-shape](profile/bullet-shape.md): the model a fit runs under, and the outline for `prior: outline`.
- [observation](log/observation.md), [group](log/group.md), [air](log/air.md), [target](log/target.md), [muzzle-velocity](log/muzzle-velocity.md): which groups a fit may use and what each contributes. A group logged with wind by stretch is fitted with the best-guess wind of its stretches, not its simple wind figures (see target).
- [origin](profile/origin.md) and [versions](profile/versions.md): the dropped mark, copies, and how versions are numbered.
- [the connector overview](connector.md)

## Worked examples

**1. MV true on six logged groups, as a preview**

Starting point: one profile, and one range session with six logged groups of five shots from 300 to 800 yd. Link scopes: `read`.

Call:
```json
{"method":"mv_true"}
```

Spoken line:
```text
MV true: 2640 to 2749 fps, plus or minus 9, from 6 groups. This fit saw nothing slower than Mach 1.35. Say save to keep it.
```

Details:
```json
{
 "status": "preview",
 "method": "mv_true",
 "converged": true,
 "fingerprint": "2e619cad6a160991",
 "params": [
  {
   "name": "mv_offset_fps",
   "value": 94.386,
   "observed": true,
   "sigma": 8.896
  }
 ],
 "chi2_reduced": 0.736,
 "iterations": 3,
 "min_observed_mach": 1.346,
 "groups": [
  {
   "id": "fixture-group-1",
   "role": "use",
   "fitted": true,
   "range_yd": 300,
   "mach": 1.946,
   "required_mil": 1.15,
   "before_mil": 1.237,
   "after_mil": 1.122,
   "residual_mil": -0.028,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-2",
   "role": "use",
   "fitted": true,
   "range_yd": 400,
   "mach": 1.82,
   "required_mil": 1.9,
   "before_mil": 2.118,
   "after_mil": 1.936,
   "residual_mil": 0.036,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-3",
   "role": "use",
   "fitted": true,
   "range_yd": 500,
   "mach": 1.691,
   "required_mil": 2.95,
   "before_mil": 3.131,
   "after_mil": 2.872,
   "residual_mil": -0.078,
   "session_id": "fixture-session"
  },
  "(3 more)"
 ],
 "ranges": [
  {
   "range_yd": 300,
   "before_mil": 1.295,
   "after_mil": 1.163,
   "extrapolated": false
  },
  {
   "range_yd": 400,
   "before_mil": 2.193,
   "after_mil": 1.983,
   "extrapolated": false
  },
  {
   "range_yd": 500,
   "before_mil": 3.203,
   "after_mil": 2.905,
   "extrapolated": false
  },
  "(5 more)"
 ],
 "excluded": [],
 "roles": {
  "fixture-group-1": "use",
  "fixture-group-2": "use",
  "fixture-group-3": "use",
  "fixture-group-4": "use",
  "fixture-group-5": "use",
  "fixture-group-6": "use"
 },
 "skip_reasons": {},
 "warnings": ["This fit saw nothing slower than Mach 1.35."],
 "profile": {
  "id": "fixture-ridge",
  "version": 1,
  "name": "Ridge 308"
 }
}
```

Saved nothing.

**2. One drag factor (CDF) on the same groups**

Starting point: one profile, and one range session with six logged groups of five shots from 300 to 800 yd. Link scopes: `read`.

Call:
```json
{"method":"cdf"}
```

Spoken line:
```text
CDF 0.814, plus or minus 0.020, from 6 groups. This fit saw nothing slower than Mach 1.35. Say save to keep it.
```

Details:
```json
{
 "status": "preview",
 "method": "cdf",
 "converged": true,
 "fingerprint": "d2fd0f7a95a4554f",
 "params": [
  {
   "name": "cdf",
   "value": 0.814,
   "observed": true,
   "sigma": 0.02
  }
 ],
 "chi2_reduced": 1.244,
 "iterations": 4,
 "min_observed_mach": 1.346,
 "groups": [
  {
   "id": "fixture-group-1",
   "role": "use",
   "fitted": true,
   "range_yd": 300,
   "mach": 1.946,
   "required_mil": 1.15,
   "before_mil": 1.237,
   "after_mil": 1.183,
   "residual_mil": 0.033,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-2",
   "role": "use",
   "fitted": true,
   "range_yd": 400,
   "mach": 1.82,
   "required_mil": 1.9,
   "before_mil": 2.118,
   "after_mil": 2.01,
   "residual_mil": 0.11,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-3",
   "role": "use",
   "fitted": true,
   "range_yd": 500,
   "mach": 1.691,
   "required_mil": 2.95,
   "before_mil": 3.131,
   "after_mil": 2.945,
   "residual_mil": -0.005,
   "session_id": "fixture-session"
  },
  "(3 more)"
 ],
 "ranges": [
  {
   "range_yd": 300,
   "before_mil": 1.295,
   "after_mil": 1.241,
   "extrapolated": false
  },
  {
   "range_yd": 400,
   "before_mil": 2.193,
   "after_mil": 2.085,
   "extrapolated": false
  },
  {
   "range_yd": 500,
   "before_mil": 3.203,
   "after_mil": 3.015,
   "extrapolated": false
  },
  "(5 more)"
 ],
 "excluded": [],
 "roles": {
  "fixture-group-1": "use",
  "fixture-group-2": "use",
  "fixture-group-3": "use",
  "fixture-group-4": "use",
  "fixture-group-5": "use",
  "fixture-group-6": "use"
 },
 "skip_reasons": {},
 "warnings": ["This fit saw nothing slower than Mach 1.35."],
 "profile": {
  "id": "fixture-ridge",
  "version": 1,
  "name": "Ridge 308"
 }
}
```

Saved nothing.

**3. The drop table (DSF): only one Mach point is reached**

Starting point: one profile, and one range session with six logged groups of five shots from 300 to 800 yd. Link scopes: `read`.

Call:
```json
{"method":"dsf"}
```

Spoken line:
```text
DSF fitted at 1 Mach points from 6 groups. This fit saw nothing slower than Mach 1.35. Say save to keep it.
```

Details:
```json
{
 "status": "preview",
 "method": "dsf",
 "converged": true,
 "fingerprint": "85de5a07f914d8ad",
 "params": [
  {
   "name": "dsf@0.80",
   "value": 1,
   "sigma": null,
   "observed": false
  },
  {
   "name": "dsf@0.90",
   "value": 1,
   "sigma": null,
   "observed": false
  },
  {
   "name": "dsf@1.00",
   "value": 1,
   "sigma": null,
   "observed": false
  },
  "(3 more)"
 ],
 "chi2_reduced": 14.087,
 "iterations": 2,
 "min_observed_mach": 1.346,
 "groups": [
  {
   "id": "fixture-group-1",
   "role": "use",
   "fitted": true,
   "range_yd": 300,
   "mach": 1.946,
   "required_mil": 1.15,
   "before_mil": 1.237,
   "after_mil": 1.237,
   "residual_mil": 0.087,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-2",
   "role": "use",
   "fitted": true,
   "range_yd": 400,
   "mach": 1.82,
   "required_mil": 1.9,
   "before_mil": 2.118,
   "after_mil": 2.118,
   "residual_mil": 0.218,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-3",
   "role": "use",
   "fitted": true,
   "range_yd": 500,
   "mach": 1.691,
   "required_mil": 2.95,
   "before_mil": 3.131,
   "after_mil": 3.131,
   "residual_mil": 0.181,
   "session_id": "fixture-session"
  },
  "(3 more)"
 ],
 "ranges": [
  {
   "range_yd": 300,
   "before_mil": 1.295,
   "after_mil": 1.295,
   "extrapolated": false
  },
  {
   "range_yd": 400,
   "before_mil": 2.193,
   "after_mil": 2.193,
   "extrapolated": false
  },
  {
   "range_yd": 500,
   "before_mil": 3.203,
   "after_mil": 3.203,
   "extrapolated": false
  },
  "(5 more)"
 ],
 "excluded": [],
 "roles": {
  "fixture-group-1": "use",
  "fixture-group-2": "use",
  "fixture-group-3": "use",
  "fixture-group-4": "use",
  "fixture-group-5": "use",
  "fixture-group-6": "use"
 },
 "skip_reasons": {},
 "warnings": ["This fit saw nothing slower than Mach 1.35."],
 "profile": {
  "id": "fixture-ridge",
  "version": 1,
  "name": "Ridge 308"
 }
}
```

Saved nothing.

**4. The full drag curve on the same groups, the farthest held out and scored against the fit**

Starting point: one profile, and one range session with six logged groups of five shots from 300 to 800 yd. Link scopes: `read`.

Call:
```json
{"method":"full","hold_out":["fixture-group-6"]}
```

Spoken line:
```text
Full fit from 5 groups: worst miss goes from 0.41 to 0.23 mil. This fit saw nothing slower than Mach 1.46. Say save to keep it.
```

Details:
```json
{
 "status": "preview",
 "method": "full",
 "converged": true,
 "fingerprint": "a1dc82efdafae6f4",
 "params": [
  {
   "name": "k@0.70",
   "value": 1,
   "sigma": null,
   "observed": false
  },
  {
   "name": "k@0.80",
   "value": 1,
   "sigma": null,
   "observed": false
  },
  {
   "name": "k@0.90",
   "value": 1,
   "sigma": null,
   "observed": false
  },
  "(26 more)"
 ],
 "chi2_reduced": 3.754,
 "iterations": 4,
 "min_observed_mach": 1.458,
 "groups": [
  {
   "id": "fixture-group-1",
   "role": "use",
   "fitted": true,
   "range_yd": 300,
   "mach": 1.946,
   "required_mil": 1.15,
   "before_mil": 1.237,
   "after_mil": 1.202,
   "residual_mil": 0.052,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-2",
   "role": "use",
   "fitted": true,
   "range_yd": 400,
   "mach": 1.82,
   "required_mil": 1.9,
   "before_mil": 2.118,
   "after_mil": 2.049,
   "residual_mil": 0.149,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-3",
   "role": "use",
   "fitted": true,
   "range_yd": 500,
   "mach": 1.691,
   "required_mil": 2.95,
   "before_mil": 3.131,
   "after_mil": 3.011,
   "residual_mil": 0.061,
   "session_id": "fixture-session"
  },
  "(3 more)"
 ],
 "ranges": [
  {
   "range_yd": 300,
   "before_mil": 1.295,
   "after_mil": 1.269,
   "extrapolated": false
  },
  {
   "range_yd": 400,
   "before_mil": 2.193,
   "after_mil": 2.136,
   "extrapolated": false
  },
  {
   "range_yd": 500,
   "before_mil": 3.203,
   "after_mil": 3.101,
   "extrapolated": false
  },
  "(4 more)"
 ],
 "excluded": [],
 "roles": {
  "fixture-group-1": "use",
  "fixture-group-2": "use",
  "fixture-group-3": "use",
  "fixture-group-4": "use",
  "fixture-group-5": "use",
  "fixture-group-6": "hold_out"
 },
 "skip_reasons": {},
 "warnings": ["This fit saw nothing slower than Mach 1.46."],
 "profile": {
  "id": "fixture-ridge",
  "version": 1,
  "name": "Ridge 308"
 }
}
```

Saved nothing.

**5. A preview, then the save with its fingerprint, on a link that may change profiles**

Starting point: one profile, and one range session with six logged groups of five shots from 300 to 800 yd. Link scopes: `read`, `profiles`.

Call 1:
```json
{"method":"cdf"}
```

Spoken line:
```text
CDF 0.814, plus or minus 0.020, from 6 groups. This fit saw nothing slower than Mach 1.35. Say save to keep it.
```

Details:
```json
{
 "status": "preview",
 "method": "cdf",
 "converged": true,
 "fingerprint": "d2fd0f7a95a4554f",
 "params": [
  {
   "name": "cdf",
   "value": 0.814,
   "observed": true,
   "sigma": 0.02
  }
 ],
 "chi2_reduced": 1.244,
 "iterations": 4,
 "min_observed_mach": 1.346,
 "groups": [
  {
   "id": "fixture-group-1",
   "role": "use",
   "fitted": true,
   "range_yd": 300,
   "mach": 1.946,
   "required_mil": 1.15,
   "before_mil": 1.237,
   "after_mil": 1.183,
   "residual_mil": 0.033,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-2",
   "role": "use",
   "fitted": true,
   "range_yd": 400,
   "mach": 1.82,
   "required_mil": 1.9,
   "before_mil": 2.118,
   "after_mil": 2.01,
   "residual_mil": 0.11,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-3",
   "role": "use",
   "fitted": true,
   "range_yd": 500,
   "mach": 1.691,
   "required_mil": 2.95,
   "before_mil": 3.131,
   "after_mil": 2.945,
   "residual_mil": -0.005,
   "session_id": "fixture-session"
  },
  "(3 more)"
 ],
 "ranges": [
  {
   "range_yd": 300,
   "before_mil": 1.295,
   "after_mil": 1.241,
   "extrapolated": false
  },
  {
   "range_yd": 400,
   "before_mil": 2.193,
   "after_mil": 2.085,
   "extrapolated": false
  },
  {
   "range_yd": 500,
   "before_mil": 3.203,
   "after_mil": 3.015,
   "extrapolated": false
  },
  "(5 more)"
 ],
 "excluded": [],
 "roles": {
  "fixture-group-1": "use",
  "fixture-group-2": "use",
  "fixture-group-3": "use",
  "fixture-group-4": "use",
  "fixture-group-5": "use",
  "fixture-group-6": "use"
 },
 "skip_reasons": {},
 "warnings": ["This fit saw nothing slower than Mach 1.35."],
 "profile": {
  "id": "fixture-ridge",
  "version": 1,
  "name": "Ridge 308"
 }
}
```

Saved nothing.

Call 2:
```json
{"method":"cdf","save":true,"fingerprint":"d2fd0f7a95a4554f"}
```

Spoken line:
```text
Saved "Ridge 308" as version 2: CDF 0.814, plus or minus 0.020, from 6 groups. This fit saw nothing slower than Mach 1.35.
```

Details (with the stored-text note):
```json
{
 "status": "saved",
 "profile_id": "fixture-ridge",
 "version": 2,
 "name": "Ridge 308",
 "method": "cdf",
 "warnings": ["This fit saw nothing slower than Mach 1.35."]
}
```

Saved the profile at version 2, changed: `ammo.drag.cdf`, `ammo.drag.min_mach`, `calibration.chi2_reduced`, `calibration.fitted_at`, `calibration.from_version`, `calibration.held_out[]`, `calibration.held_out_residuals[]`, `calibration.method`, `calibration.min_mach`, `calibration.params[]`, `calibration.preset`, `calibration.solver_version`, `calibration.used[]`.

**6. One group held out and one skipped**

Starting point: one profile, and one range session with six logged groups of five shots from 300 to 800 yd. Link scopes: `read`.

Call:
```json
{"method":"cdf","skip":["fixture-group-1"],"hold_out":["fixture-group-6"]}
```

Spoken line:
```text
CDF 0.803, plus or minus 0.036, from 4 groups. This fit saw nothing slower than Mach 1.46. Say save to keep it.
```

Details:
```json
{
 "status": "preview",
 "method": "cdf",
 "converged": true,
 "fingerprint": "9f2396a13de50141",
 "params": [
  {
   "name": "cdf",
   "value": 0.803,
   "observed": true,
   "sigma": 0.036
  }
 ],
 "chi2_reduced": 1.877,
 "iterations": 4,
 "min_observed_mach": 1.458,
 "groups": [
  {
   "id": "fixture-group-2",
   "role": "use",
   "fitted": true,
   "range_yd": 400,
   "mach": 1.82,
   "required_mil": 1.9,
   "before_mil": 2.118,
   "after_mil": 2.004,
   "residual_mil": 0.104,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-3",
   "role": "use",
   "fitted": true,
   "range_yd": 500,
   "mach": 1.691,
   "required_mil": 2.95,
   "before_mil": 3.131,
   "after_mil": 2.934,
   "residual_mil": -0.016,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-4",
   "role": "use",
   "fitted": true,
   "range_yd": 600,
   "mach": 1.577,
   "required_mil": 3.8,
   "before_mil": 4.215,
   "after_mil": 3.907,
   "residual_mil": 0.107,
   "session_id": "fixture-session"
  },
  "(2 more)"
 ],
 "ranges": [
  {
   "range_yd": 400,
   "before_mil": 2.193,
   "after_mil": 2.078,
   "extrapolated": false
  },
  {
   "range_yd": 500,
   "before_mil": 3.203,
   "after_mil": 3.004,
   "extrapolated": false
  },
  {
   "range_yd": 600,
   "before_mil": 4.327,
   "after_mil": 4.014,
   "extrapolated": false
  },
  "(3 more)"
 ],
 "excluded": [],
 "roles": {
  "fixture-group-1": "skip",
  "fixture-group-2": "use",
  "fixture-group-3": "use",
  "fixture-group-4": "use",
  "fixture-group-5": "use",
  "fixture-group-6": "hold_out"
 },
 "skip_reasons": {},
 "warnings": ["This fit saw nothing slower than Mach 1.46."],
 "profile": {
  "id": "fixture-ridge",
  "version": 1,
  "name": "Ridge 308"
 }
}
```

Saved nothing.

**7. A save with a fingerprint from another set of roles**

Starting point: one profile, and one range session with six logged groups of five shots from 300 to 800 yd. Link scopes: `read`, `profiles`.

Call:
```json
{"method":"cdf","skip":["fixture-group-1"],"save":true,"fingerprint":"d2fd0f7a95a4554f"}
```

The tool refuses:
```text
Your groups or profile changed since the preview. Run the preview again.
```

**8. A save with no fingerprint**

Starting point: one profile, and one range session with six logged groups of five shots from 300 to 800 yd. Link scopes: `read`, `profiles`.

Call:
```json
{"method":"cdf","save":true}
```

The tool refuses:
```text
Your groups or profile changed since the preview. Run the preview again.
```

**9. A save on a link without the profiles scope**

Starting point: one profile, and one range session with six logged groups of five shots from 300 to 800 yd. Link scopes: `read`.

Call:
```json
{"method":"cdf","save":true,"fingerprint":"d2fd0f7a95a4554f"}
```

The tool refuses:
```text
This connector can't save profiles. Make a new one on the Connectors screen with 'create and edit my profiles' ticked.
```

**10. A group id the profile does not have**

Starting point: one profile, and one range session with six logged groups of five shots from 300 to 800 yd. Link scopes: `read`.

Call:
```json
{"method":"cdf","skip":["fixture-group-9"]}
```

The tool refuses:
```text
fixture-group-9 is not one of this profile's usable groups.
```

**11. MV against temperature when no group has an ammo temperature**

Starting point: one profile, and one range session with six logged groups of five shots from 300 to 800 yd. Link scopes: `read`.

Call:
```json
{"method":"mv_temp"}
```

The tool refuses:
```text
MV vs temperature needs groups at two or more ammo temperatures at least 10 °F apart
```

**12. Hold out with MV against temperature**

Starting point: one profile, and one range session with six logged groups of five shots from 300 to 800 yd. Link scopes: `read`.

Call:
```json
{"method":"mv_temp","hold_out":["fixture-group-6"]}
```

The tool refuses:
```text
MV versus temperature has no hold_out: use skip to leave a group out.
```

**13. A profile with no logged group**

Starting point: one profile, no phone connected. Link scopes: `read`.

Call:
```json
{"method":"cdf"}
```

The tool refuses (with the stored-text note):
```text
No groups to fit for "Ridge 308".
```

**14. A session that holds none of the groups**

Starting point: one profile, and one range session with six logged groups of five shots from 300 to 800 yd. Link scopes: `read`.

Call:
```json
{"method":"cdf","session_id":"fixture-other"}
```

The tool refuses (with the stored-text note):
```text
No groups to fit for "Ridge 308".
```

**15. The outline shape asked for when the profile has no outline**

Starting point: one profile, and one range session with six logged groups of five shots from 300 to 800 yd. Link scopes: `read`.

Call:
```json
{"method":"full","prior":"outline"}
```

The tool refuses:
```text
The estimated shape needs the bullet outline in the profile (nose length).
```

**16. Two profiles and none named**

Starting point: two profiles, no phone connected. Link scopes: `read`.

Call:
```json
{"method":"cdf"}
```

The tool refuses (with the stored-text note):
```text
Which profile? "Ridge 308" (fixture-ridge); "Creek 65" (fixture-creek)
```

**17. A profile calibrated under another model than it now stores: every fit says so**

Starting point: one profile at version 3: version 2 carries a fitted drag factor of 1.05 from a calibration under the Standard model, version 3 stores the Physics model; one range session with six logged groups of five shots from 300 to 800 yd, shot with version 1. Link scopes: `read`.

Call:
```json
{"method":"cdf"}
```

Spoken line:
```text
CDF 0.815, plus or minus 0.020, from 6 groups. The existing calibration was calibrated under Standard. This fit saw nothing slower than Mach 1.30. Say save to keep it.
```

Details:
```json
{
 "status": "preview",
 "method": "cdf",
 "converged": true,
 "fingerprint": "770d21da25b30640",
 "params": [
  {
   "name": "cdf",
   "value": 0.815,
   "observed": true,
   "sigma": 0.02
  }
 ],
 "chi2_reduced": 1.234,
 "iterations": 4,
 "min_observed_mach": 1.3,
 "groups": [
  {
   "id": "fixture-group-1",
   "role": "use",
   "fitted": true,
   "range_yd": 300,
   "mach": 1.923,
   "required_mil": 1.15,
   "before_mil": 1.251,
   "after_mil": 1.183,
   "residual_mil": 0.033,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-2",
   "role": "use",
   "fitted": true,
   "range_yd": 400,
   "mach": 1.791,
   "required_mil": 1.9,
   "before_mil": 2.147,
   "after_mil": 2.01,
   "residual_mil": 0.11,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-3",
   "role": "use",
   "fitted": true,
   "range_yd": 500,
   "mach": 1.658,
   "required_mil": 2.95,
   "before_mil": 3.182,
   "after_mil": 2.944,
   "residual_mil": -0.006,
   "session_id": "fixture-session"
  },
  "(3 more)"
 ],
 "ranges": [
  {
   "range_yd": 300,
   "before_mil": 1.309,
   "after_mil": 1.241,
   "extrapolated": false
  },
  {
   "range_yd": 400,
   "before_mil": 2.223,
   "after_mil": 2.084,
   "extrapolated": false
  },
  {
   "range_yd": 500,
   "before_mil": 3.255,
   "after_mil": 3.014,
   "extrapolated": false
  },
  "(5 more)"
 ],
 "excluded": [],
 "roles": {
  "fixture-group-1": "use",
  "fixture-group-2": "use",
  "fixture-group-3": "use",
  "fixture-group-4": "use",
  "fixture-group-5": "use",
  "fixture-group-6": "use"
 },
 "skip_reasons": {},
 "warnings": ["The existing calibration was calibrated under Standard.", "This fit saw nothing slower than Mach 1.30."],
 "profile": {
  "id": "fixture-ridge",
  "version": 3,
  "name": "Ridge 308"
 }
}
```

Saved nothing.

**18. The drop table when a group sits at the edge of a Mach point: refused**

Starting point: one profile at version 3: version 2 carries a fitted drag factor of 1.05 from a calibration under the Standard model, version 3 stores the Physics model; one range session with six logged groups of five shots from 300 to 800 yd, shot with version 1. Link scopes: `read`.

Call:
```json
{"method":"dsf"}
```

The tool refuses:
```text
singular system: the data cannot determine every parameter
```

**19. A Spin profile: the preview, then the save**

Starting point: one profile whose stored model is Spin, and one range session with six logged groups of five shots from 300 to 800 yd. Link scopes: `read`, `profiles`.

Call 1:
```json
{"method":"cdf"}
```

Spoken line:
```text
CDF 0.812, plus or minus 0.021, from 6 groups. This fit saw nothing slower than Mach 1.34. Say save to keep it.
```

Details:
```json
{
 "status": "preview",
 "method": "cdf",
 "converged": true,
 "fingerprint": "d2fd0f7a95a4554f",
 "params": [
  {
   "name": "cdf",
   "value": 0.812,
   "observed": true,
   "sigma": 0.021
  }
 ],
 "chi2_reduced": 1.381,
 "iterations": 4,
 "min_observed_mach": 1.343,
 "groups": [
  {
   "id": "fixture-group-1",
   "role": "use",
   "fitted": true,
   "range_yd": 300,
   "mach": 1.942,
   "required_mil": 1.15,
   "before_mil": 1.247,
   "after_mil": 1.193,
   "residual_mil": 0.043,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-2",
   "role": "use",
   "fitted": true,
   "range_yd": 400,
   "mach": 1.816,
   "required_mil": 1.9,
   "before_mil": 2.127,
   "after_mil": 2.019,
   "residual_mil": 0.119,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-3",
   "role": "use",
   "fitted": true,
   "range_yd": 500,
   "mach": 1.688,
   "required_mil": 2.95,
   "before_mil": 3.138,
   "after_mil": 2.95,
   "residual_mil": 0,
   "session_id": "fixture-session"
  },
  "(3 more)"
 ],
 "ranges": [
  {
   "range_yd": 300,
   "before_mil": 1.292,
   "after_mil": 1.238,
   "extrapolated": false
  },
  {
   "range_yd": 400,
   "before_mil": 2.189,
   "after_mil": 2.079,
   "extrapolated": false
  },
  {
   "range_yd": 500,
   "before_mil": 3.197,
   "after_mil": 3.007,
   "extrapolated": false
  },
  "(5 more)"
 ],
 "excluded": [],
 "roles": {
  "fixture-group-1": "use",
  "fixture-group-2": "use",
  "fixture-group-3": "use",
  "fixture-group-4": "use",
  "fixture-group-5": "use",
  "fixture-group-6": "use"
 },
 "skip_reasons": {},
 "warnings": ["This fit saw nothing slower than Mach 1.34."],
 "profile": {
  "id": "fixture-ridge",
  "version": 1,
  "name": "Ridge 308"
 }
}
```

Saved nothing.

Call 2:
```json
{"method":"cdf","save":true,"fingerprint":"d2fd0f7a95a4554f"}
```

Spoken line:
```text
Saved "Ridge 308" as version 2: CDF 0.812, plus or minus 0.021, from 6 groups. This fit saw nothing slower than Mach 1.34.
```

Details (with the stored-text note):
```json
{
 "status": "saved",
 "profile_id": "fixture-ridge",
 "version": 2,
 "name": "Ridge 308",
 "method": "cdf",
 "warnings": ["This fit saw nothing slower than Mach 1.34."]
}
```

Saved the profile at version 2, changed: `ammo.drag.cdf`, `ammo.drag.min_mach`, `calibration.aero_source.id`, `calibration.aero_source.kind`, `calibration.chi2_reduced`, `calibration.fitted_at`, `calibration.from_version`, `calibration.held_out[]`, `calibration.held_out_residuals[]`, `calibration.method`, `calibration.min_mach`, `calibration.params[]`, `calibration.preset`, `calibration.solver_version`, `calibration.used[]`.

**20. MV against temperature from five groups, then the save**

Starting point: one profile, and one range session with seven logged groups: five with ammunition temperatures from 42 to 91 °F and velocities from 2,612 to 2,666 fps, one with no ammunition temperature, and one at 100 °F whose velocity is from two shots. Link scopes: `read`, `profiles`.

Call 1:
```json
{"method":"mv_temp"}
```

Spoken line:
```text
MV changes 1.1 fps per degree, from 5 groups between 42 and 91 degrees. Say save to keep it.
```

Details:
```json
{
 "status": "preview",
 "method": "mv_temp",
 "fingerprint": "d4d34eb7679b6e59",
 "slope_fps_per_F": 1.097,
 "sigma": 0.029,
 "table": [
  {
   "temp_F": 42,
   "mv_fps": 2611.6
  },
  {
   "temp_F": 91,
   "mv_fps": 2665.3
  }
 ],
 "roles": {
  "fixture-group-1": "use",
  "fixture-group-2": "use",
  "fixture-group-3": "use",
  "fixture-group-4": "use",
  "fixture-group-5": "use",
  "fixture-group-7": "skip"
 },
 "skip_reasons": {
  "fixture-group-7": "fewer than 3 shots"
 },
 "profile": {
  "id": "fixture-ridge",
  "version": 1,
  "name": "Ridge 308"
 }
}
```

Saved nothing.

Call 2:
```json
{"method":"mv_temp","save":true,"fingerprint":"d4d34eb7679b6e59"}
```

Spoken line:
```text
Saved "Ridge 308" as version 2: MV changes 1.1 fps per degree, from 5 groups between 42 and 91 degrees.
```

Details (with the stored-text note):
```json
{
 "status": "saved",
 "profile_id": "fixture-ridge",
 "version": 2,
 "name": "Ridge 308",
 "method": "mv_temp",
 "warnings": []
}
```

Saved the profile at version 2, changed: `ammo.mv_temp_table[]`, `calibration.chi2_reduced`, `calibration.fitted_at`, `calibration.from_version`, `calibration.held_out[]`, `calibration.held_out_residuals[]`, `calibration.method`, `calibration.params[]`, `calibration.preset`, `calibration.solver_version`, `calibration.used[]`.

**21. MV against temperature with a group that starts skipped brought in**

Starting point: one profile, and one range session with seven logged groups: five with ammunition temperatures from 42 to 91 °F and velocities from 2,612 to 2,666 fps, one with no ammunition temperature, and one at 100 °F whose velocity is from two shots. Link scopes: `read`.

Call:
```json
{"method":"mv_temp","use":["fixture-group-7"]}
```

Spoken line:
```text
MV changes 1.3 fps per degree, from 6 groups between 42 and 100 degrees. Say save to keep it.
```

Details:
```json
{
 "status": "preview",
 "method": "mv_temp",
 "fingerprint": "6c438eb757c2b5b7",
 "slope_fps_per_F": 1.255,
 "sigma": 0.155,
 "table": [
  {
   "temp_F": 42,
   "mv_fps": 2609
  },
  {
   "temp_F": 100,
   "mv_fps": 2681.8
  }
 ],
 "roles": {
  "fixture-group-1": "use",
  "fixture-group-2": "use",
  "fixture-group-3": "use",
  "fixture-group-4": "use",
  "fixture-group-5": "use",
  "fixture-group-7": "use"
 },
 "skip_reasons": {},
 "profile": {
  "id": "fixture-ridge",
  "version": 1,
  "name": "Ridge 308"
 }
}
```

Saved nothing.

**22. MV true on groups out to 1,300 yd: only the two nearest are fitted**

Starting point: one profile, and one range session with six logged groups of five shots from 600 to 1,300 yd. Link scopes: `read`.

Call:
```json
{"method":"mv_true"}
```

Spoken line:
```text
MV true: 2640 to 2650 fps, plus or minus 10, from 2 groups. This fit saw nothing slower than Mach 1.37. Say save to keep it.
```

Details:
```json
{
 "status": "preview",
 "method": "mv_true",
 "converged": true,
 "fingerprint": "2e619cad6a160991",
 "params": [
  {
   "name": "mv_offset_fps",
   "value": -37.931,
   "observed": true,
   "sigma": 10.429
  }
 ],
 "chi2_reduced": 0.711,
 "iterations": 3,
 "min_observed_mach": 1.369,
 "groups": [
  {
   "id": "fixture-group-1",
   "role": "use",
   "fitted": true,
   "range_yd": 600,
   "mach": 1.597,
   "required_mil": 4.2,
   "before_mil": 4.11,
   "after_mil": 4.253,
   "residual_mil": 0.053,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-2",
   "role": "use",
   "fitted": true,
   "range_yd": 800,
   "mach": 1.369,
   "required_mil": 6.9,
   "before_mil": 6.636,
   "after_mil": 6.865,
   "residual_mil": -0.035,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-3",
   "role": "use",
   "fitted": false,
   "range_yd": 1000,
   "mach": 1.157,
   "required_mil": 10.2,
   "before_mil": 9.788,
   "after_mil": 10.131,
   "residual_mil": -0.069,
   "session_id": "fixture-session"
  },
  "(3 more)"
 ],
 "ranges": [
  {
   "range_yd": 600,
   "before_mil": 4.327,
   "after_mil": 4.29,
   "extrapolated": false
  },
  {
   "range_yd": 800,
   "before_mil": 6.977,
   "after_mil": 6.918,
   "extrapolated": true
  },
  {
   "range_yd": 3520,
   "before_mil": 142.509,
   "after_mil": 141.832,
   "extrapolated": true
  },
  "(1 more)"
 ],
 "excluded": [],
 "roles": {
  "fixture-group-1": "use",
  "fixture-group-2": "use",
  "fixture-group-3": "use",
  "fixture-group-4": "use",
  "fixture-group-5": "use",
  "fixture-group-6": "use"
 },
 "skip_reasons": {},
 "warnings": ["This fit saw nothing slower than Mach 1.37."],
 "profile": {
  "id": "fixture-ridge",
  "version": 1,
  "name": "Ridge 308"
 }
}
```

Saved nothing.

**23. The drop table on the same groups: five Mach points**

Starting point: one profile, and one range session with six logged groups of five shots from 600 to 1,300 yd. Link scopes: `read`.

Call:
```json
{"method":"dsf"}
```

Spoken line:
```text
DSF fitted at 5 Mach points from 6 groups. Say save to keep it.
```

Details:
```json
{
 "status": "preview",
 "method": "dsf",
 "converged": true,
 "fingerprint": "85de5a07f914d8ad",
 "params": [
  {
   "name": "dsf@0.80",
   "value": 1,
   "sigma": null,
   "observed": false
  },
  {
   "name": "dsf@0.90",
   "value": 1.06,
   "observed": true,
   "sigma": 0.012
  },
  {
   "name": "dsf@1.00",
   "value": 1.052,
   "observed": true,
   "sigma": 0.012
  },
  "(3 more)"
 ],
 "chi2_reduced": 1.453,
 "iterations": 3,
 "min_observed_mach": 0.928,
 "groups": [
  {
   "id": "fixture-group-1",
   "role": "use",
   "fitted": true,
   "range_yd": 600,
   "mach": 1.597,
   "required_mil": 4.2,
   "before_mil": 4.11,
   "after_mil": 4.11,
   "residual_mil": -0.09,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-2",
   "role": "use",
   "fitted": true,
   "range_yd": 800,
   "mach": 1.369,
   "required_mil": 6.9,
   "before_mil": 6.636,
   "after_mil": 6.9,
   "residual_mil": 0,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-3",
   "role": "use",
   "fitted": true,
   "range_yd": 1000,
   "mach": 1.157,
   "required_mil": 10.2,
   "before_mil": 9.788,
   "after_mil": 10.2,
   "residual_mil": 0,
   "session_id": "fixture-session"
  },
  "(3 more)"
 ],
 "ranges": [
  {
   "range_yd": 600,
   "before_mil": 4.327,
   "after_mil": 4.327,
   "extrapolated": false
  },
  {
   "range_yd": 800,
   "before_mil": 6.977,
   "after_mil": 7.549,
   "extrapolated": false
  },
  {
   "range_yd": 1000,
   "before_mil": 10.306,
   "after_mil": 10.764,
   "extrapolated": false
  },
  "(5 more)"
 ],
 "excluded": [],
 "roles": {
  "fixture-group-1": "use",
  "fixture-group-2": "use",
  "fixture-group-3": "use",
  "fixture-group-4": "use",
  "fixture-group-5": "use",
  "fixture-group-6": "use"
 },
 "skip_reasons": {},
 "warnings": [],
 "profile": {
  "id": "fixture-ridge",
  "version": 1,
  "name": "Ridge 308"
 }
}
```

Saved nothing.

**24. MV true with the two nearest groups skipped**

Starting point: one profile, and one range session with six logged groups of five shots from 600 to 1,300 yd. Link scopes: `read`.

Call:
```json
{"method":"mv_true","skip":["fixture-group-1","fixture-group-2"]}
```

The tool refuses:
```text
MV true needs at least one used group still above Mach 1.2 at the target (supersonic); these groups are all slower
```

**25. MV true when the misses grow with range**

Starting point: one profile, and one range session with six logged groups of five shots from 300 to 800 yd that needed more elevation than predicted, more so with range. Link scopes: `read`.

Call:
```json
{"method":"mv_true"}
```

Spoken line:
```text
MV true: 2640 to 2500 fps, plus or minus 13, from 6 groups. The misses still trend with range. This fit saw nothing slower than Mach 1.34. Say save to keep it.
```

Details:
```json
{
 "status": "preview",
 "method": "mv_true",
 "converged": true,
 "fingerprint": "2e619cad6a160991",
 "params": [
  {
   "name": "mv_offset_fps",
   "value": -150.487,
   "observed": true,
   "sigma": 12.886
  }
 ],
 "chi2_reduced": 3.883,
 "iterations": 4,
 "min_observed_mach": 1.343,
 "groups": [
  {
   "id": "fixture-group-1",
   "role": "use",
   "fitted": true,
   "range_yd": 300,
   "mach": 1.944,
   "required_mil": 1.3,
   "before_mil": 1.239,
   "after_mil": 1.451,
   "residual_mil": 0.151,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-2",
   "role": "use",
   "fitted": true,
   "range_yd": 400,
   "mach": 1.816,
   "required_mil": 2.3,
   "before_mil": 2.13,
   "after_mil": 2.466,
   "residual_mil": 0.166,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-3",
   "role": "use",
   "fitted": true,
   "range_yd": 500,
   "mach": 1.691,
   "required_mil": 3.5,
   "before_mil": 3.131,
   "after_mil": 3.607,
   "residual_mil": 0.107,
   "session_id": "fixture-session"
  },
  "(3 more)"
 ],
 "ranges": [
  {
   "range_yd": 300,
   "before_mil": 1.295,
   "after_mil": 1.494,
   "extrapolated": false
  },
  {
   "range_yd": 400,
   "before_mil": 2.193,
   "after_mil": 2.509,
   "extrapolated": false
  },
  {
   "range_yd": 500,
   "before_mil": 3.203,
   "after_mil": 3.65,
   "extrapolated": false
  },
  "(5 more)"
 ],
 "excluded": [],
 "roles": {
  "fixture-group-1": "use",
  "fixture-group-2": "use",
  "fixture-group-3": "use",
  "fixture-group-4": "use",
  "fixture-group-5": "use",
  "fixture-group-6": "use"
 },
 "skip_reasons": {},
 "warnings": ["The misses still trend with range.", "This fit saw nothing slower than Mach 1.34."],
 "profile": {
  "id": "fixture-ridge",
  "version": 1,
  "name": "Ridge 308"
 }
}
```

Saved nothing.

**26. Groups logged before the tracking factor was changed**

Starting point: one profile at version 2, whose elevation tracking factor was set to 0.95 after its groups were shot; one range session with six logged groups of five shots from 300 to 800 yd, shot with version 1. Link scopes: `read`.

Call:
```json
{"method":"cdf"}
```

Spoken line:
```text
CDF 0.814, plus or minus 0.020, from 6 groups. 6 groups were logged under a different SSF. This fit saw nothing slower than Mach 1.35. Say save to keep it.
```

Details:
```json
{
 "status": "preview",
 "method": "cdf",
 "converged": true,
 "fingerprint": "ccb36831b6da1301",
 "params": [
  {
   "name": "cdf",
   "value": 0.814,
   "observed": true,
   "sigma": 0.02
  }
 ],
 "chi2_reduced": 1.244,
 "iterations": 4,
 "min_observed_mach": 1.346,
 "groups": [
  {
   "id": "fixture-group-1",
   "role": "use",
   "fitted": true,
   "range_yd": 300,
   "mach": 1.946,
   "required_mil": 1.15,
   "before_mil": 1.237,
   "after_mil": 1.183,
   "residual_mil": 0.033,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-2",
   "role": "use",
   "fitted": true,
   "range_yd": 400,
   "mach": 1.82,
   "required_mil": 1.9,
   "before_mil": 2.118,
   "after_mil": 2.01,
   "residual_mil": 0.11,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-3",
   "role": "use",
   "fitted": true,
   "range_yd": 500,
   "mach": 1.691,
   "required_mil": 2.95,
   "before_mil": 3.131,
   "after_mil": 2.945,
   "residual_mil": -0.005,
   "session_id": "fixture-session"
  },
  "(3 more)"
 ],
 "ranges": [
  {
   "range_yd": 300,
   "before_mil": 1.295,
   "after_mil": 1.241,
   "extrapolated": false
  },
  {
   "range_yd": 400,
   "before_mil": 2.193,
   "after_mil": 2.085,
   "extrapolated": false
  },
  {
   "range_yd": 500,
   "before_mil": 3.203,
   "after_mil": 3.015,
   "extrapolated": false
  },
  "(5 more)"
 ],
 "excluded": [],
 "roles": {
  "fixture-group-1": "use",
  "fixture-group-2": "use",
  "fixture-group-3": "use",
  "fixture-group-4": "use",
  "fixture-group-5": "use",
  "fixture-group-6": "use"
 },
 "skip_reasons": {},
 "warnings": ["6 groups were logged under a different SSF.", "This fit saw nothing slower than Mach 1.35."],
 "profile": {
  "id": "fixture-ridge",
  "version": 2,
  "name": "Ridge 308"
 }
}
```

Saved nothing.

**27. A fit that does not converge, and its save**

Starting point: one profile, and one range session with three logged groups, at 310, 500 and 710 yd, each with nothing dialed and the group on the point of aim. Link scopes: `read`, `profiles`.

Call 1:
```json
{"method":"cdf"}
```

Spoken line:
```text
The fit did not converge, so there is nothing to save. Try other groups or a simpler method.
```

Details:
```json
{
 "status": "preview",
 "method": "cdf",
 "converged": false,
 "fingerprint": "92a64f07da6176fe",
 "params": [
  {
   "name": "cdf",
   "value": 0,
   "observed": true,
   "sigma": 2
  }
 ],
 "chi2_reduced": 1765.969,
 "iterations": 20,
 "min_observed_mach": 1.444,
 "groups": [
  {
   "id": "fixture-group-1",
   "role": "use",
   "fitted": true,
   "range_yd": 310,
   "mach": 1.931,
   "required_mil": 0,
   "before_mil": 1.323,
   "after_mil": 1.042,
   "residual_mil": 1.042,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-2",
   "role": "use",
   "fitted": true,
   "range_yd": 500,
   "mach": 1.691,
   "required_mil": 0,
   "before_mil": 3.131,
   "after_mil": 2.284,
   "residual_mil": 2.284,
   "session_id": "fixture-session"
  },
  {
   "id": "fixture-group-3",
   "role": "use",
   "fitted": true,
   "range_yd": 710,
   "mach": 1.444,
   "required_mil": 0,
   "before_mil": 5.619,
   "after_mil": 3.696,
   "residual_mil": 3.696,
   "session_id": "fixture-session"
  }
 ],
 "ranges": [
  {
   "range_yd": 310,
   "before_mil": 1.38,
   "after_mil": 1.097,
   "extrapolated": false
  },
  {
   "range_yd": 500,
   "before_mil": 3.203,
   "after_mil": 2.348,
   "extrapolated": false
  },
  {
   "range_yd": 710,
   "before_mil": 5.713,
   "after_mil": 3.771,
   "extrapolated": false
  },
  "(2 more)"
 ],
 "excluded": [],
 "roles": {
  "fixture-group-1": "use",
  "fixture-group-2": "use",
  "fixture-group-3": "use"
 },
 "skip_reasons": {},
 "warnings": [],
 "profile": {
  "id": "fixture-ridge",
  "version": 1,
  "name": "Ridge 308"
 }
}
```

Saved nothing.

Call 2:
```json
{"method":"cdf","save":true,"fingerprint":"92a64f07da6176fe"}
```

The tool refuses:
```text
The fit did not converge: not saved.
```

**28. A group the bullet does not reach, also when it is held out**

Starting point: one profile, and one range session with seven logged groups: six of five shots from 300 to 800 yd, and one at 9,000 yd, which the bullet does not reach. Link scopes: `read`.

Call 1:
```json
{"method":"cdf"}
```

The tool refuses:
```text
group at 9000 yd is out of range for this profile
```

Call 2:
```json
{"method":"cdf","hold_out":["fixture-group-7"]}
```

The tool refuses:
```text
group at 9000 yd is out of range for this profile
```

**29. The Spin trim on six calm-air groups: a preview that cannot tell, a save refused for that, then saved when the shooter wants it anyway**

Starting point: one profile whose stored model is Spin, and one range session with six logged groups of five shots from 400 to 900 yd, each marked as shot in calm air. Link scopes: `read`, `profiles`.

Call 1:
```json
{"method":"spin_trim"}
```

Spoken line:
```text
Spin trim × 1.14, plus or minus 0.23. These 6 groups cannot tell the spin drift apart from the model’s: × 1.14 (±0.23). Nothing to trim. They could only show a difference of 70 % or more; more calm-air groups, and longer ones, bring that down. It is not a clear fit: say save anyway to keep it.
```

Details:
```json
{
 "status": "preview",
 "method": "spin_trim",
 "converged": true,
 "fingerprint": "a5278c709aacb477",
 "factor": 1.139,
 "sigma": 0.228,
 "verdict": "weak",
 "groups": [
  {
   "id": "fixture-group-1",
   "before_mil": -0.018,
   "after_mil": -0.012,
   "sigma_mil": 0.063,
   "range_yd": 400
  },
  {
   "id": "fixture-group-2",
   "before_mil": -0.018,
   "after_mil": -0.008,
   "sigma_mil": 0.071,
   "range_yd": 500
  },
  {
   "id": "fixture-group-3",
   "before_mil": -0.034,
   "after_mil": -0.022,
   "sigma_mil": 0.08,
   "range_yd": 600
  },
  "(3 more)"
 ],
 "warnings": [],
 "profile": {
  "id": "fixture-ridge",
  "version": 1,
  "name": "Ridge 308"
 }
}
```

Saved nothing.

Call 2:
```json
{"method":"spin_trim","save":true,"fingerprint":"a5278c709aacb477"}
```

The tool refuses:
```text
Not saved: These 6 groups cannot tell the spin drift apart from the model’s: × 1.14 (±0.23). Nothing to trim. They could only show a difference of 70 % or more; more calm-air groups, and longer ones, bring that down. It is not a clear fit: save only if the shooter still wants it, with confirm: true.
```

Call 3:
```json
{"method":"spin_trim","save":true,"confirm":true,"fingerprint":"a5278c709aacb477"}
```

Spoken line:
```text
Saved "Ridge 308" as version 2: Spin trim × 1.14, plus or minus 0.23.
```

Details (with the stored-text note):
```json
{
 "status": "saved",
 "profile_id": "fixture-ridge",
 "version": 2,
 "name": "Ridge 308",
 "method": "spin_trim",
 "factor": 1.139,
 "sigma": 0.228,
 "verdict": "weak",
 "warnings": []
}
```

Saved the profile at version 2, changed: `ammo.aero_trim.lift`, `calibration.chi2_reduced`, `calibration.fitted_at`, `calibration.from_version`, `calibration.held_out[]`, `calibration.held_out_residuals[]`, `calibration.method`, `calibration.params[]`, `calibration.preset`, `calibration.solver_version`, `calibration.used[]`, `calibration.verdict`.

**30. A saved Spin trim removed**

Starting point: one profile whose stored model is Spin and which holds a saved Spin trim of 1.2, no phone connected. Link scopes: `read`, `profiles`.

Call:
```json
{"method":"spin_trim","remove":true,"save":true}
```

Spoken line:
```text
Saved "Ridge 308" as version 2: spin trim removed.
```

Details (with the stored-text note):
```json
{
 "status": "saved",
 "profile_id": "fixture-ridge",
 "version": 2,
 "name": "Ridge 308",
 "method": "spin_trim",
 "removed": true,
 "warnings": []
}
```

Saved the profile at version 2, changed: `ammo.aero_trim.lift`, `calibration.chi2_reduced`, `calibration.fitted_at`, `calibration.from_version`, `calibration.held_out[]`, `calibration.held_out_residuals[]`, `calibration.method`, `calibration.params[]`, `calibration.preset`, `calibration.solver_version`, `calibration.used[]`.

**31. The Spin trim on a profile that is not on the Spin model**

Starting point: one profile, and one range session with six logged groups of five shots from 300 to 800 yd. Link scopes: `read`.

Call:
```json
{"method":"spin_trim"}
```

The tool refuses (with the stored-text note):
```text
Spin trim is only for a profile on the Spin model; "Ridge 308" is on Standard.
```

**32. The Spin trim when no group is marked calm, and again with light wind allowed**

Starting point: one profile whose stored model is Spin, and one range session with six logged groups of five shots from 300 to 800 yd. Link scopes: `read`.

Call 1:
```json
{"method":"spin_trim"}
```

The tool refuses (with the stored-text note):
```text
"Ridge 308": No group is marked "Shot in calm air".
```

Call 2:
```json
{"method":"spin_trim","light_wind":true}
```

The tool refuses (with the stored-text note):
```text
"Ridge 308": No group is marked "Shot in calm air" or typed with 2 mph or less.
```

---

[All reference pages](index.md)
