# Zero offset

Whether the profile's stored zero height and zero offset shift every come-up and windage, or the rifle is taken as zeroed exactly on the aim point.

## Generated table

**Setting:** Zero offset

| Choice | Used by |
|---|---|
| Zeroed on the aim point (Standard) | Standard, Physics, Classic, Spin |
| Use the profile’s zero height and offset | Custom only |

## What it does

A rifle is "zeroed" when the group lands where the scope's crosshair points at the zero range. In practice the group is often a little off: half an inch high, say, or a quarter inch left. This setting decides what the solver does with that.

- **Zeroed on the aim point (Standard):** the solver assumes the group is exactly on the aim point at the zero range. The profile's zero height and zero offset are ignored. This is the value in every preset.
- **Use the profile's zero height and offset:** the solver reads the two numbers from the profile. Zero height is how far above (plus) or below (minus) the aim point the group prints at the zero range, in inches. Zero offset is how far right (plus) or left (minus), in inches. Every come-up and every windage is then shifted by the angle those inches make at the zero range. It is chosen under the Custom model, or, under any model, by the profile's switch "Use my zero offsets in every solve", which the zero step turns on when it saves offsets measured from zero-check groups (since 2026-10-16).

With both numbers at 0, or not set, the two choices give the same answer.

The two numbers are edited in the profile screen's Zero section ("Zero height" and "Zero offset") and, by Claude, with the connector's `edit_profile` tool, each between -20 and 20 inches.

## When to choose it

Choose it when you know your zero is off the aim point and have measured by how much: a group measured at the zero range with the scope on its zero setting. It fits a rifle whose long-range come-ups run consistently high or low by the same angle at every distance, which is what a zero that is off the aim point does. A velocity error looks different: it grows with range, while this shift is the same angle everywhere, so one distance alone cannot tell the two apart. Leave it on Standard if you do not know the numbers; a guess here moves every come-up.

## How to picture it

Draw the line of sight and the bullet's arc crossing it at the zero range. With the standard zero the arc crosses the line exactly at the zero range. With a zero height of a small amount, the arc passes that much above the line there instead. To get there the barrel has to point a little higher, and that small extra angle is carried all the way out: at every distance the arc sits higher than before by about the same angle, so less elevation is needed to hit the same target. A zero offset is the same picture seen from above: the group sits to the right of the line at the zero range, the barrel points a little right, and every distance needs that angle of left correction.

## The math

**Standard.** The zeroed flight must pass through the line of sight at the zero range $R_z$. The bore angle $b$ is found by repeating

$$ b \leftarrow b - \frac{y(R_z; b)}{R_z} $$

until $\lvert y \rvert < 10^{-9}$ m (at most 50 passes), starting from $b = 0$. Here $y(R_z; b)$ is the height of the flight above the line of sight at the zero range, for bore angle $b$. The zero uses no wind, no Earth rotation, the zero air and the zero slope, if the profile has them.

**Use the profile's zero height and offset.** The target height is no longer 0 but $h$, the zero height converted to metres:

$$ h = 0.0254\, h_{\mathrm{in}} \qquad b \leftarrow b - \frac{y(R_z; b) - h}{R_z} $$

with the same stopping rule, now on $\lvert y - h \rvert$. The zeroed flight passes $h$ above the line of sight at $R_z$. If $h$ is 0 this is the Standard zero exactly.

The come-up for a range $R$ is read from the flight with that bore angle. With the Standard angle convention it is the drop divided by the range, in mil:

$$ E(R) = -\frac{y(R; b)}{R} \times 1000 $$

At $R = R_z$ in calm, level air this gives exactly

$$ E(R_z) = -\frac{h}{R_z} \times 1000 \quad\text{(before the scope's elevation tracking factor)} $$

**Away from the zero range.** Under the default `flight: 'zeroed'` every row is flown at the one bore angle $b$, which is higher by about $h/R_z$ than the Standard one. The come-up is then lower by very nearly that angle, not exactly: drag and gravity act on a flight that starts at a slightly different angle, and the solver does not use the approximation, it flies the whole path. Solved for this page (computed, not from the table): for 1 in of zero height at 100 yd the shift is within about 0.2 % of $h/R_z$ at 3,520 yd and much closer at 500 and 1,500 yd. Under `flight: 'dialed'` each row's dial is solved against the raised bore, so the total angle is fixed by the target and the elevation shift is $h/R_z$ at every range; measured, it agrees to about 4 parts in 10 million, the accuracy of the dial search, which interpolates between a few trial flights. Because of that interpolation neither relation is in the exact (1e-9) check file.

**The windage shift.** The offset is applied directly. With $o$ the zero offset converted to metres,

$$ o = 0.0254\, o_{\mathrm{in}} \qquad \Delta_w = \frac{o}{R_z} \times 1000 \ \text{mil} $$

and the windage correction for a wind segment is

$$ W = \frac{D(R) - D(R_z) - \Delta_w}{\mathrm{SSF}_w} $$

where $D(R)$ is the lateral drift in mil of the flight at the bore angle $b$ above (plus the spin drift and Earth-rotation terms the other options add), and $D(R_z)$ is the same drift at the zero range in calm air (the part the zero absorbs). Because $b$ rises with $h$, $D$ itself changes very slightly with the zero height; it does not change with $o$, so the offset's own contribution to $W$ is exactly $-\Delta_w/\mathrm{SSF}_w$ at every range. $\Delta_w$ is subtracted only when $o_{\mathrm{in}}$ is not 0. A group that prints right of the aim point lands right by this angle at every range, so the correction moves left by it. The same shift applies to both wind segments of a two-wind shot, and the elevation is not touched by the offset.

| Symbol | Meaning | Value / unit |
|---|---|---|
| $h_{\mathrm{in}}$ | zero height: group above (+) or below (-) the aim point at the zero range | inches, profile field `zero_height_in`, absent = 0 |
| $o_{\mathrm{in}}$ | zero offset: group right (+) or left (-) of the aim point at the zero range | inches, profile field `zero_offset_in`, absent = 0 |
| 0.0254 | inches to metres | m per in |
| $R_z$ | zero range | metres (the profile's yards times 0.9144) |
| $R$ | range of the row | metres |
| $b$ | bore angle relative to the line of sight | rad |
| $y(R; b)$ | height of the flight above the line of sight at range $R$ | m |
| $E$ | come-up before the scope tracking factor, jump and drop-scale factor | mil |
| $D$ | lateral drift of the flight with the other options' terms | mil, plus meaning drift to the right |
| $\Delta_w$ | angle of the group's sideways offset at the zero range | mil |
| $W$ | windage correction | mil, plus meaning a correction to the right |
| $\mathrm{SSF}_w$ | the profile's windage tracking factor | dimensionless |

Both fields must be finite numbers or the profile is refused. Absent, 0 and non-numeric values from a stored profile all mean "none" when the profile is converted for the solver.

**What other options change.** `angle: 'true'` replaces the small-angle form of $E$ with an arc tangent; the zero height is a height in metres either way and is not affected. With `flight: 'dialed'` each row is flown at the bore angle above plus its own dial, so the dial is solved against the raised bore and the elevation shift is the full $h/R_z$ at every range, as described above ([flight](flight.md)). The zero offset never reaches the zeroing flight; only the height does.

## The algorithm

Run once per solve, before the shot is flown. Only the first two lines read this option.

```
function solve(profile, env, target, options):
    useOffset = (options.zeroOffset == 'apply')
    hM = useOffset ? (profile.zero_height_in or 0) * 0.0254 : 0
    bore = zeroAngle(zero air, profile.zero_range_yd, profile.zero_incl_deg or 0, hM)
    if abs(bore) > pi/4: refuse ("zero needs a bore angle beyond 45 degrees")
    fly the shot at bore; fly the zero range in calm air at bore for the zero's own drift
    zeroDrift = drift(zero range sample)
    offsetMil = (useOffset and profile.zero_offset_in != 0)
                ? profile.zero_offset_in * 0.0254 / (zero_range_yd * 0.9144) * 1000 : 0
    for each row:
        elevation from the flight at bore            # carries the height shift
        windage = (drift(row) - zeroDrift - offsetMil) / ssf_wind
```

```
function zeroAngle(flight, zeroRangeYd, zeroInclDeg, heightM):
    b = 0
    repeat up to 50 times:
        y = flight at bore b, zero range, no wind, Earth rotation off, slope zeroInclDeg
        miss = heightM != 0 ? y - heightM : y
        b = b - miss / zeroRangeM
        if abs(miss) < 1e-9: stop
    return b
```

## On a real shot

The shot: a 350 gr 0.375 in bullet at 3048 fps muzzle velocity, fired at 3520 yd in air of 60 °F, 40 % humidity, 24.43 inHg at the firing line, latitude 42.1°, wind 10 mph from the right (90° to the line of fire), the same wind in every column; site altitude 5,500 ft. In the app, type it as "Site altitude" (shown when the profile's model reads one); left blank, the classic values count from sea level and can differ from this table. The profile is changed from the scenario's for this table: zero 1 in high, 0.5 in right. All other settings are the Standard model's. In the first column (North, level) the reference arc reaches 223 ft above the line of sight at 2160 yd. Compared with the first column, the zero range differs in "North, level, 730 yd zero".

| Value | Shown in the app as | North, level | East, level | North, level, 730 yd zero |
|---|---|---|---|---|
| `'ignore'` | Zeroed on the aim point (Standard) | reference | reference | reference |
| `'apply'` | Use the profile’s zero height and offset | -0.278 / -0.139 / 0.0 | -0.278 / -0.139 / 0.0 | -0.038 / -0.019 / 0.0 |

Each cell is the change from the reference row (`'ignore'`) in the same shot, as elevation (mil) / windage (mil) / velocity at the target (fps). Elevation: plus means more elevation must be dialed. Windage: plus means the windage correction moves to the right (dial further right); this is a correction, not where the bullet lands. Velocity: plus means the bullet arrives faster. A zero means no difference at the shown precision.

Read from the table: with a group 1 in high and 0.5 in right at the zero range, the `'apply'` row has an elevation change of -0.278 mil and a windage change of -0.139 mil (the correction moves left) on both the north and east shots, which have a 100 yd zero, and the arrival velocity does not change at the shown precision (the higher bore flies a slightly different path, so it moves by an amount too small to print; under `flight: 'dialed'` it is unchanged to within the dial search's accuracy). The 730 yd zero column changes by only -0.038 mil of elevation and -0.019 mil of windage. The reason is in the math section: the shift is the stored inches divided by the zero range, as an angle, so the same inches make a much smaller angle at 730 yd than at 100 yd. The windage change is half the elevation change in every column, as the 0.5 in offset is half the 1 in height.

---

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