# Scope

The scope's turret units, its tracking factors, its elevation travel and the words used for a turn of each turret.

## Generated table

**Record:** profile · **Covers:** `scope.units`, `scope.ssf_elev`, `scope.ssf_wind`, `scope.name`, `scope.reticle`, `scope.elev_travel_mil`, `scope.turret`, `scope.click`

| Field | Type | Required | Limits in the schema | Solver reads | In a shared log |
|---|---|---|---|---|---|
| `scope.click` | object | no | - | no | as stored |
| `scope.click.elev` | number | yes, within | max 5 | no | as stored |
| `scope.click.wind` | number | yes, within | max 5 | no | as stored |
| `scope.elev_travel_mil` | number | no | - | no | as stored |
| `scope.name` | string | no | - | no | as stored |
| `scope.reticle` | string | no | - | no | as stored |
| `scope.ssf_elev` | number | yes | min 0.5; max 1.5 | yes | as stored |
| `scope.ssf_wind` | number | yes | min 0.5; max 1.5 | yes | as stored |
| `scope.turret` | object | no | - | no | as stored |
| `scope.turret.elev_up` | `'cw'` or `'ccw'` | no | - | no | as stored |
| `scope.turret.wind_right` | `'cw'` or `'ccw'` | no | - | no | as stored |
| `scope.turret.wording` | `'lr'` or `'clock'` | yes, within | - | no | as stored |
| `scope.units` | `'MRAD'` or `'MOA'` | yes | - | no | as stored |

## What it holds

These fields of a profile version describe the scope on the rifle: whether its turrets are marked in mil or MOA, how truly each turret tracks, how much elevation the turret and mount can give, a make and model, a reticle name, and, if the shooter wants it, which way each turret turns to move the impact. The two tracking factors are always present and 1 means "true". The name, the reticle, the travel and the turret wording are optional, and an absent one means "not said". Sight height is not here: it is a [rifle](rifle.md) field, although guided setup asks for it on the Scope step.

## What to know first

**A tracking factor divides the answer.** The solver finds the true angle, then divides the come-up by `scope.ssf_elev` and the drift by `scope.ssf_wind`. Above 1, a dialed mil moves the impact more than a mil, so you dial less. 1 changes nothing.

**Only elevation is ever measured.** The tall-target test writes `scope.ssf_elev`. Nothing measures `scope.ssf_wind`; it is typed. A fit changes neither, and reads only the elevation factor.

**Units are for talking, not for the numbers.** Every stored and solved number is in mil, whatever `scope.units` says. On an MOA scope the app's screens, the connector and the observations CSV show and take MOA (see "Who reads it"); the JSON backup stays in mil.

**A turret direction is never guessed.** Clockwise wording needs at least one of `elev_up` and `wind_right` (since 2026-10-22; before that, both); a turret whose direction is not given keeps up, left and right. Only the words change; typed and stored values keep their signs (up and right are plus).

**A reset keeps the factors unless asked.** By default, editing a scope field keeps the calibration. A reset removes the fitted curve and record, not the factors. Only the app can also reset them.

## The fields

**`scope.units`** Which units the turrets are marked in: `'MRAD'` (mil) or `'MOA'`. Required; a new profile starts at `'MRAD'`. It decides how `scope.elev_travel_mil` is shown and typed and which unit the connector's tools speak and read in (see "Who reads it"). It does not convert anything stored: changing it later leaves every stored number as it was. In guided setup, changing Turret units converts the number in the travel box to the new unit, so it still means the same mil; a travel only converted that way saves its stored mil unchanged, and a travel typed after the change is read in the new unit.

**`scope.ssf_elev`** The elevation tracking factor, a plain ratio: the fraction of a dialed mil that the impact moves. Required; 1 (the default) means the turret is true. 0.99 means a dial of 10 mil moves the impact 9.9 mil. The solver divides the come-up by it, so a factor below 1 means more dial. Example: 0.99.

**`scope.ssf_wind`** The same for the windage turret. Required; 1 by default and usually left there. It is only typed: nothing measures it. The solver divides the drift by it.

**`scope.name`** The scope's make and model, as the shooter wrote it. Optional free text; no code uses it, and an empty text removes it. Example: "Acme 5-25x56".

**`scope.reticle`** The reticle's name, as written. Optional free text, like `scope.name`. Example: "Acme mil grid".

**`scope.elev_travel_mil`** The most elevation you can dial from the zero, counting any tilt built into the rail or mount, in mil whatever `scope.units` is. Optional; absent means "not known". The app takes and shows it in the scope's units and stores mil (1 MOA is 0.290888209 mil); the connector's `elev_travel_mil` input is in its `travel_unit`, else the scope's unit, and is stored in mil. Example: 30.5.

**`scope.turret`** How a turn of each turret is worded, one object with three members: `scope.turret.wording` (`'clock'` for clockwise and counter-clockwise, `'lr'` for up, left and right), `scope.turret.elev_up` and `scope.turret.wind_right` (`'cw'` or `'ccw'`: which way that turret turns, seen from its top, to move the impact up or right). `wording` and at least one direction are required when the object is present (since 2026-10-22: a shooter may know only one turret). Absent (the default) means up, left and right. Nothing writes `'lr'`: the app and the connector remove the object instead. Example: `wording` `'clock'`, `elev_up` `'ccw'`, `wind_right` `'cw'`.

**`scope.click`** The size of one click of each turret, in the scope's unit: **`scope.click.elev`** and **`scope.click.wind`**, both required when the object is present, each above 0 and at most 5. Optional; absent means not set. Only the zero step and `zero_check` read it, to give a turret correction in whole clicks (half a click rounds away from zero); no solve uses it. The profile screen's Scope section has the two boxes, "Elevation click size" and "Windage click size", in the turret units as chosen there; both blank removes the object. Example: 0.1 and 0.1 on a mil scope, 0.25 and 0.25 on an MOA one.

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

| Field | App | Connector | Conversion | Solve |
|---|---|---|---|---|
| `scope.ssf_elev` | 0.5 to 1.5 | 0.5 to 1.5 | > 0 | > 0 |
| `scope.ssf_wind` | 0.5 to 1.5 | 0.5 to 1.5 | > 0 | > 0 |
| `scope.elev_travel_mil` | > 0 | > 0 to 1032 | none | n/a |
| `scope.click.elev` | > 0 to 5 | not settable | n/a | n/a |
| `scope.click.wind` | > 0 to 5 | not settable | n/a | n/a |

The factors are ratios and the travel is in mil. Since 2026-10-13 the record schema holds each factor to 0.5 to 1.5 as well, so the server refuses a pushed profile outside it; the travel it asks only to be a number. A typed factor of 0 or less is refused by all four: the app says "must be a number greater than 0", the input check of `edit_profile` and `create_profile` refuses it, and a profile that carries one fails the conversion, so it cannot be solved and `edit_profile` saves nothing on it unless the same call sets the factor right. Between 0 and 0.5, and above 1.5, the app saves it; `edit_profile` and `create_profile` do not take it as an input, but every tool solves a profile that already holds one. The tall-target save is outside this table, and has one rule for the app and the connector (since 2026-10-10): a factor that, rounded to four places, is below 0.5 or above 1.5 is not saved (the range `edit_profile` takes; both ends are saved). Ten mil dialed at 100 yd with 10 in measured gives 0.2778: the app's screen shows the number, says it is outside the range and turns its Save button off, and `tall_target` previews it, says it will not be saved, and refuses the save. The connector's text inputs for the name and the reticle take at most 80 characters; the app and the schema set no length. The conversion does not look at the travel, the name, the reticle, the units or the turret.

## Who writes it

**The app.** The profile screen's Scope section has the scope's name, the units, the reticle, the sight height, the travel (typed in the scope's units), the two factors and the turret: a switch, "Say clockwise / counter-clockwise instead of up, L and R", and two questions, "To move the impact up, the elevation turret turns..." and "To move the impact right, the windage turret turns...". Each question offers "Not known" (keep up / down, keep L / R); with the switch on and neither direction chosen, it does not save. Turning the switch off removes the object. It also has the two click sizes. A new profile starts with `'MRAD'` and both factors at 1. The sample profiles a fresh install starts with also carry a scope name and a reticle; the [origin](origin.md) page describes them. The Calibrate screen's tall-target test saves the elevation factor, and its "Use this one again" button puts an older version's two factors on the current one with the rest of that calibration.

**The connector.** `edit_profile` takes `scope_name`, `reticle`, `scope_units`, `elev_travel_mil` (in `travel_unit`, `mil` or `moa`, else the scope's unit after the call; up to 1032 in the input, then refused above 300 mil once converted; null removes it), `turret` (the two directions, both required in its input, which sets `'clock'`; a stored profile with one direction is read and kept as it is; null removes the object; a `wording` given inside `turret` is ignored, so giving the two directions always means clockwise wording), `ssf_elev` and `ssf_wind`. An empty text removes the name or the reticle. `create_profile` takes the same and starts from the app's defaults. Give only what changes: a value equal to the stored one is not a change. Unknown inputs are not used, and the reply names them. `tall_target` computes the elevation factor from three numbers (the amount dialed in its `unit` input, `mil` or `moa`, else the scope's unit, since 2026-10-15), only previews unless `save` is true, and saves under the `profiles` token scope, a factor of 0.5 to 1.5 only.

**The fitter.** The tall-target test is the only calibration that writes a scope field. From the distance, the mils dialed and the inches the impact moved, the factor is inches moved divided by (mils dialed times 3.6 times distance in yards over 100): a mil is 3.6 in at 100 yd. A made-up example: 20 mil dialed at 100 yd should move 72 in, and 71.28 in measured gives 0.99. The saved value is rounded to four decimals. The calculation checks nothing about the result; the save does: the app's screen and `tall_target` both refuse a factor outside 0.5 to 1.5, and the shared save itself throws on one. The save also replaces the profile's calibration record with one of method `ssf` and moves the old record to the history; it does not touch the drag curve. No other fit changes any scope field.

**Copies and restores.** Copying a shared profile writes the scope exactly as the sharer's record held it, including their turret directions and tracking factors, and restoring a deleted profile writes it as the deleted version held it; neither applies the limits above. The [origin](origin.md) and [versions](versions.md) pages describe the two. The server does not set these fields.

## Who reads it

**The solver.** The conversion passes both factors on (and refuses one that is not above 0). For each range the solver works out the come-up and the drift in true mils, and the factors are the last step, so nothing else in the answer depends on them:

- Elevation: the angle from the drop, minus the aerodynamic jump from the crosswind, times the drop scale factor of the [drag](drag.md) curve, then divided by `scope.ssf_elev`.
- Windage: the drift (spin drift and Earth rotation included), minus the calm drift at the zero range and any zero offset, then divided by `scope.ssf_wind`. The drop scale factor and the jump are not in the windage.

The flight itself is flown without the factors: the drop in inches, the time of flight and the speed do not change with them, and the Classic model's spin drift, which is a fraction of the come-up, uses the value before the division.

A run for this page, in the Standard model with a made-up 250 gr, 0.338 in bullet (G7 0.33, 1.7 in long, 2950 fps, 1:10 right-hand twist, 2 in sight height, 100 yd zero, 59 degrees F, 50 %, 29.92 inHg, latitude 40, firing north, 10 mph from 3 o'clock; these numbers are my own run, not from a generated table):

| Range | Elevation, factor 1 | Elevation, factor 1.02 | Windage, factor 1 | Windage, factor 1.02 |
|---|---|---|---|---|
| 500 yd | 2.19 mil | 2.15 mil | 0.62 mil | 0.61 mil |
| 1000 yd | 6.99 mil | 6.85 mil | 1.41 mil | 1.38 mil |
| 1500 yd | 14.56 mil | 14.27 mil | 2.48 mil | 2.43 mil |

At each range the 1.02 column is the 1 column divided by 1.02. Setting only one factor changes only its own axis. Even at 100 yd the come-up (-0.11 mil in this run, from the crosswind jump) is divided by the factor, which shows the jump is inside the division.

**The fitter.** A group records the mils dialed, and the fitter turns that into the true mils it needed: dial times the factor of the version the group was shot with, minus where the group printed. A hold read off the reticle is not scaled. Guided calibration leaves out a candidate target when its true come-up divided by the profile's factor is more than `scope.elev_travel_mil`, and says "more than your scope travel"; the Guide card prints that come-up as the dial, the true come-up divided by the factor. The fit's own solves run with both factors at 1.

**The app.** The Calculator, the range card and the Session's "Calculated" line read `scope.turret` through one function that the connector shares. With clock wording a number shows with `CW` or `CCW` after it (for example "0.63 CW"), and the sign is dropped because the direction carries it. A value that shows as 0.00 has no direction. Without clock wording, windage shows `R` or `L` and elevation shows the plain signed number. For a profile whose `elev_up` is `'ccw'` and `wind_right` is `'cw'`, +12.41 mil of elevation shows as "12.41 CCW", -0.8 as "0.80 CW", and +0.63 of windage as "0.63 CW". A turret with no direction keeps its usual words: with only `elev_up` `'ccw'`, the same windage shows as "0.63 R", and the connector says "12.41 mils counter-clockwise on elevation, 0.63 right on windage". The editor and guided setup show the other fields for editing. Typed fields (dialed wind, group centre, hold) keep their signs.

**The app's angle unit (since 2026-10-16).** Every angle the Calculator shows (elevation, Wind 1 and Wind 2, the other model's line, the curvature line, the velocity spread line), the range card's Elev and Wind columns (headed "(MOA)" on an MOA scope), the Guide card's spreads, goal and travel lines, its error-target box, the guide's own sentences and group flags, the calibration screen's group rows and fit result, the tall-target test's amount dialed, the measured-velocity notice, a received suggestion's tables and the saved-group editor are shown in the scope's unit: the solved mil divided by 0.290888209 on an MOA scope. It is display only: the solve, the stored records and the hand-off to the range log (which converts it in the form) are unchanged. A shared profile picked in the Calculator shows in the sharer's unit.

**The connector.** `solve` and `range_card` say the dial as a turn when the profile has clock wording: the spoken line becomes, for example, "6.99 mils counter-clockwise on elevation, 1.41 clockwise on windage", and the structured result gains `elev_turn` and `wind1_turn` (range card rows have both). The numbers themselves stay signed. Since 2026-10-15 the spoken come-up of `solve`, and of `set_calculator` and `log_group`'s predictions, is in the scope's unit ("Up 51.02 MOA, right 2.17" on a MOA scope), and `solve` and `range_card` add `elev_moa` and `wind1_moa` beside the mil fields; on a mil scope they say "mils" as before. `log_group` and `tall_target` read their amounts in the scope's unit unless `unit` says otherwise, and say so when they read MOA. `explain_dope` takes its confirmed come-ups in the scope's unit unless told otherwise, and its click defaults to 0.25 MOA or 0.1 mil to match. `sensitivity` words its answer in MOA or mils by `scope.units` and adds MOA figures for a MOA scope. Since 2026-10-16 every other angle a reply carries on an MOA scope also comes as an MOA figure beside the mil one (`get_session`, `get_context`, `set_calculator`, `calibrate_profile`, `compare_models`, `next_target`), the replies about the phone or a session name the scope's unit, and the spoken lines (the curvature line, the fit's worst miss, the guide's goal, the travel `edit_profile` saved) use the scope's unit. `calibrate_profile` warns when used groups were logged under a different elevation factor than the profile has now.

`check_profile` names a scope field in two cases. When a factor is 0 or less it reports that the solver cannot use the profile, in the conversion's words ("SSF elevation must be greater than 0"). And it says "Not measured yet: elevation travel" when `scope.elev_travel_mil` is on the profile's list of unmeasured values; no screen or tool puts it there today, so this shows only for a record that came from elsewhere.

Nothing reads `scope.name` or `scope.reticle`.

## Why it is stored

**The factors.** A turret that tracks 1 % off puts a long shot off by 1 % of the come-up: at 20 mil dialed that is 0.2 mil. The tall-target test measures it once, and every answer then dials for it.

**Units.** To speak to the shooter in the turret's own units: the connector, the travel field, the range log and, since 2026-10-16, the Calculator, the range card, the Guide card, the calibration screen and the desktop workbench.

**Travel.** To keep guided calibration from suggesting a target the scope cannot reach.

**Name and reticle.** Notes for the shooter, and for a person reading a shared profile. No calculation uses them.

**Turret wording.** Which way is clockwise depends on the scope and on each turret, so a dial value is only a useful instruction when the shooter has said how that scope turns. It lives on the profile, not on the device, because it describes one scope.

## 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.

**The edit category is "ssf" for the two factors and none for the other five fields.** A change to a factor keeps the calibration by default. A change to the units, the name, the reticle, the travel or the turret is in no category, so it never asks, and the app always keeps the calibration. In the connector the `calibration` input acts on its own: `edit_profile` with `calibration: "reset"` removes the fitted curve and record whatever else the call changes, a scope name included. In the app, a profile with something a reset could remove (a fitted curve, a record, velocity-temperature rows, or a factor other than 1) gets the keep-or-reset question with "keep" chosen, and the reset offers a separate box, "Also reset SSF to 1.000", that is off by default and can be ticked only when the reset is. So a reset leaves both factors as they are unless that box is ticked. The connector saves a factor edit without asking and its reset never touches the factors. An edit that also changes something in the "shape" or "ammo" category defaults to reset. What a reset removes is on the [calibration](calibration.md) page.

**Older groups.** No scope field makes a logged group ineligible for a later fit. The fit takes each group's own version's elevation factor, so a group shot before a factor change keeps the factor it was dialed with. When the used groups were shot under factors other than the profile's present one, the Calibrate screen says so ("logged with SSF 0.98, but this profile's SSF is now 1") and the connector's fit warns "logged under a different SSF". The Workbench's windage panels do the same with the windage factor (since 2026-10-24): each group is read with the factor of the version it was shot with.

## Sharing and privacy

Every field on this page reads "as stored" in the table: a reader of a shared log sees the scope exactly as the owner does, on a share with one person, with the public or with the admins. Hiding notes empties the profile's `notes`, the session notes and the groups' notes, and removes each confirmed come-up's note; it does not touch `scope.name` or `scope.reticle`, so a make and model written there reaches every reader. The factors tell a reader how true the scope tracks, and the travel hints at the mount. Nothing here carries a place or a date. A reader who saves a copy gets the owner's turret directions and factors with it.

---

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