---
name: ballistic-calculator
description: >
  Use whenever writing, reviewing, or debugging .NET/C# code that uses the BallisticCalculator
  NuGet package (namespace BallisticCalculator) or its Gehtsoft.Measurements unit types — even
  when the user doesn't name the package explicitly but is clearly doing external/interior
  ballistics in .NET (bullet drop, velocity, energy, windage, spin drift, time of flight, zeroing
  a scope) for rifles, air rifles, bows, or artillery. Covers the full public API (Ammunition,
  Rifle, Atmosphere, Wind, ShotParameters, TrajectoryCalculator, TrajectoryPoint); standard,
  custom, .drg, and multi-BC drag tables; saving/loading data and building your own file format
  around it (BXml and System.Text.Json serialization); and building or rendering scope reticles
  (e.g. to SVG). Also use it for this library's Gehtsoft.Measurements Measurement<T> unit types
  (DistanceUnit, VelocityUnit, AngularUnit, WeightUnit, PressureUnit, …). Skip for: generic physics
  or projectile-motion exercises, a different ballistics library (e.g. GNU/JBM), game-engine or Unity
  projectile motion, plain unit conversions, and non-code ballistics questions (load-data or
  forensics advice). Self-contained: no access to the library source or binaries is required —
  prefer this over scanning the package to rediscover the API. Describes 1.1.13+, including the named
  ZeroRangeCantBeReachedException / TrajectoryCannotBeCalculatedException failures.
---

# BallisticCalculator

A .NET library (`netstandard2.0`) that models the trajectory of a projectile through the
atmosphere using a 3DOF (point-mass) model.

- NuGet: **`BallisticCalculator`** (namespace `BallisticCalculator`). This describes **1.1.13+**; where
  behaviour changed, the version that changed it is named.
- Depends on **`Gehtsoft.Measurements`** (namespace `Gehtsoft.Measurements`) — every physical
  quantity is a strongly-typed `Measurement<TUnit>`.
- License: LGPL 2.1.

```csharp
using BallisticCalculator;
using Gehtsoft.Measurements;
```

The workflow is always: **(1)** describe the ammunition, rifle, and atmosphere → **(2)** compute the
zero (sight) correction with `CalculateZeroParameters` and `Apply` it to the `ShotParameters` →
**(3)** call `Calculate` → **(4)** read the returned `TrajectoryPoint[]`.

---

## 1. Measurements (the value type used everywhere)

Every dimensional value is a `Measurement<TUnit>` struct — a `double` value plus a unit. You pick
the unit at construction and again when you read it; metric and US units interoperate freely.

```csharp
var d = new Measurement<DistanceUnit>(100, DistanceUnit.Yard); // value + unit
var d2 = new Measurement<DistanceUnit>("100yd");               // parse "<number><unit>"
double meters = d.In(DistanceUnit.Meter);                      // convert & read as double
double raw    = d.Value;                                       // value in its own unit (100)
DistanceUnit u = d.Unit;                                       // DistanceUnit.Yard
var zero = Measurement<DistanceUnit>.ZERO;                     // 0
double mm = Measurement<DistanceUnit>.Convert(1, DistanceUnit.Inch, DistanceUnit.Meter); // static double->double

var d3 = 100.As(DistanceUnit.Yard);                            // extension sugar: (double|int).As(unit)
var d4 = DistanceUnit.Yard.New(100);                           // unit-first sugar: unit.New(value)
```

Operators: `+` `-` (same unit), `*` `/` by a scalar, and comparisons. Nullable `Measurement<T>?`
marks optional inputs. The three construction forms above (`new`, `.As`, `.New`) are equivalent —
use whichever reads best.

**Unit enums** (use the exact member names):

| Enum | Members |
|------|---------|
| `DistanceUnit` | Millimeter, Centimeter, Meter, Kilometer, Inch, Foot, Yard, Mile, NauticalMile, Line, RussianLine, Point, Pica |
| `VelocityUnit` | MetersPerSecond, KilometersPerHour, FeetPerSecond, MilesPerHour, Knot |
| `AngularUnit` | Radian, Degree, MOA, Mil, MRad, Thousand, InchesPer100Yards, CmPer100Meters, Percent, Turn, Gradian |
| `WeightUnit` | Grain, Ounce, Gram, Pound, Kilogram, Neuton, Dram, TroyOz, Tonne, USTonne, UKTonne |
| `PressureUnit` | Pascal, KiloPascal, Bar, Millibar, Atmosphere, TechincalAtmosphere, MillimetersOfMercury, InchesOfMercury, PoundsPerSquareInch, MillimetersOfWater |
| `TemperatureUnit` | Fahrenheit, Celsius, Kelvin, Rankin, Reaumur, Delisle |
| `EnergyUnit` | FootPound, Joule, BTU, HpH, Wh |
| `DensityUnit` | GramPerCubicCentimeter, KilogramPerCubicMeter, PoundsPerCubicInch, OuncesPerCubicFeet, PoundsPerCubicFoot |

(`TechincalAtmosphere` is spelled that way in the library.)

---

## 2. Describing the shot (inputs)

### Ammunition
```csharp
public Ammunition(
    Measurement<WeightUnit> weight,
    BallisticCoefficient ballisticCoefficient,
    Measurement<VelocityUnit> muzzleVelocity,
    Measurement<DistanceUnit>? bulletDiameter = null,  // required only for spin drift
    Measurement<DistanceUnit>? bulletLength   = null)  // required only for spin drift
```
Properties: `Weight`, `BallisticCoefficient`, `MuzzleVelocity`, `BulletDiameter?`, `BulletLength?`,
`CustomTableFileName`. `double GetBallisticCoefficient()` returns the effective BC.

### BallisticCoefficient (struct) and drag tables
```csharp
new BallisticCoefficient(0.325, DragTableId.G7)                                   // a coefficient
new BallisticCoefficient(1.0, DragTableId.GC, BallisticCoefficientValueType.FormFactor)
new BallisticCoefficient("0.325G7")                                              // parse text form
```
- `DragTableId`: `G1, G2, G5, G6, G7, G8, GI, GS, RA4` (standard curves) and `GC` (custom — see §6).
- `BallisticCoefficientValueType`: `Coefficient` (a BC number) or `FormFactor`.
- Text form: `"0.325G7"`, or `"F1GC"` (leading `F` = form factor; last two chars = table id).

### Sight, Rifling, ZeroingParameters, Rifle
```csharp
new Sight(Measurement<DistanceUnit> sightHeight,
          Measurement<AngularUnit> verticalClick,
          Measurement<AngularUnit> horizontalClick)          // clicks may be Measurement<AngularUnit>.ZERO

new Rifling(Measurement<DistanceUnit> riflingStep, TwistDirection direction)  // step = distance per turn; TwistDirection.Left|Right

new ZeroingParameters(Measurement<DistanceUnit> distance, Ammunition ammunition, Atmosphere atmosphere)
// ammunition/atmosphere may be null; when set they override the shot's ammo/atmo FOR ZEROING ONLY.
// Optional property: VerticalOffset (Measurement<DistanceUnit>?, + is up) shifts the zero impact point.

new Rifle(Sight sight, ZeroingParameters zero, Rifling rifling = null)         // rifling optional
```

### Atmosphere (immutable; derived values computed in the constructor)
```csharp
new Atmosphere()                                              // sea level standard
new Atmosphere(Measurement<DistanceUnit> altitude,
               Measurement<PressureUnit> pressure,            // STATION pressure at that altitude
               Measurement<TemperatureUnit> temperature,
               double humidity)                               // humidity is a FRACTION 0..1, not %
new Atmosphere(Measurement<DistanceUnit> altitude,
               Measurement<PressureUnit> pressure,
               bool pressureAtSeaLevel,                       // true => pressure is sea-level-corrected
               Measurement<TemperatureUnit> temperature,
               double humidity)
Atmosphere.CreateICAOAtmosphere(Measurement<DistanceUnit> altitude, double humidity = 0)
```
Read-only properties: `Altitude`, `Pressure`, `Temperature`, `Humidity`, `SoundVelocity`, `Density`,
`DensityAltitude` (1.1.12 — the standard-atmosphere altitude matching this air's density, humidity included).
Static: `Atmosphere.StandardDensity`.

`Density` is computed from the resolved station `Pressure`, so it is consistent with what the engine uses (this
was wrong before 1.1.12 — up to 21% high at 5000 ft with `pressureAtSeaLevel: true`). `DensityAltitude` is
baselined on the ICAO sea-level density, **not** `Atmosphere.StandardDensity` — they differ by ~0.005% (~1.9 ft)
and are not interchangeable.

### Wind
```csharp
new Wind(Measurement<VelocityUnit> velocity,
         Measurement<AngularUnit> direction,
         Measurement<DistanceUnit>? maximumRange = null)
```
- **Direction:** 0° blows toward the target, 90° blows from the right (right-to-left across the
  path), 180° toward the shooter, 270°/−90° from the left.
- `Calculate` takes a `Wind[]`. Each wind applies out to its `MaximumRange`; the last one (or one
  with a null range) applies to the end. **Sort the array by ascending `MaximumRange`.**

### ShotParameters (output granularity + geometry)
```csharp
var shot = new ShotParameters
{
    Step            = new Measurement<DistanceUnit>(100, DistanceUnit.Yard),  // row spacing
    MaximumDistance = new Measurement<DistanceUnit>(1000, DistanceUnit.Yard), // extent
    ShotAngle       = null,   // Measurement<AngularUnit>? line-of-sight incline, + up / - down
    CantAngle       = null,   // Measurement<AngularUnit>?
    BarrelAzimuth   = null,   // Measurement<AngularUnit>?
    Latitude        = null,   // Measurement<AngularUnit>? shooter latitude (Coriolis); optional
};
// The zero (sight) correction is REQUIRED but not set by hand — compute it and copy it in
// (see §3). Apply fills ZeroDropAdjustment / ZeroWindageAdjustment on the shot:
shot.Apply(calc.CalculateZeroParameters(ammo, atmosphere, rifle, rifle.Zero));
```
`Apply` sets the `ZeroDropAdjustment` / `ZeroWindageAdjustment` properties; the incline-adjusted
`ShotDropAdjustment` / `ShotWindageAdjustment` are also exposed. There is no longer a `SightAngle`
property.

---

## 3. Running the calculation

```csharp
public class TrajectoryCalculator
{
    // Compute the zero (sight) correction for the rifle's zero distance. `zero` is usually
    // rifle.Zero. Pass dragTable only when the BC uses table GC. NOTE the argument order:
    // atmosphere comes BEFORE rifle here (the opposite of Calculate).
    ZeroCalculatedParameters CalculateZeroParameters(
        Ammunition ammunition, Atmosphere atmosphere, Rifle rifle, ZeroingParameters zero,
        ShotParameters shot = null, Wind[] wind = null, DragTable dragTable = null,
        Measurement<DistanceUnit>? accuracy = null);   // accuracy default 0.1 mm

    TrajectoryPoint[] Calculate(Ammunition ammunition, Rifle rifle, Atmosphere atmosphere,
                                ShotParameters shot, Wind[] wind = null, DragTable dragTable = null);

    Measurement<DistanceUnit> MaximumCalculationStepSize { get; set; } // default 10 cm
    IntegrationMethod Integrator { get; set; }                         // Euler | MidpointRK2 (default)
    static Measurement<DistanceUnit> MaximumDrop  { get; }             // 10000 ft — stop condition
    static Measurement<VelocityUnit> MinimumVelocity { get; }          // 50 ft/s — stop condition
}

// Returned by CalculateZeroParameters; copy it into the shot with ShotParameters.Apply.
public class ZeroCalculatedParameters
{
    Measurement<AngularUnit>  ZeroDropAdjustment    { get; }  // vertical sight correction
    Measurement<AngularUnit>? ZeroWindageAdjustment { get; }  // horizontal, when computed
}
```

Compute the zero correction first, `Apply` it to the `ShotParameters`, then call `Calculate`:

```csharp
var zero = calc.CalculateZeroParameters(ammo, atmosphere, rifle, rifle.Zero);
shot.Apply(zero);   // sets shot.ZeroDropAdjustment (+ windage) — REQUIRED before Calculate
var trajectory = calc.Calculate(ammo, rifle, atmosphere, shot);
```

`ShotAngle`, if set, is added to the barrel elevation inside `Calculate`.

The result array can be **shorter** than `MaximumDistance/Step + 1`: the run stops early if velocity
drops below 50 ft/s or drop exceeds 10000 ft. Iterate the returned array; don't assume its length.

### Failures the engine names

Both calls throw rather than returning nonsense, and the two conditions a *user* can fix have their own
exception types:

```csharp
// namespace BallisticCalculator; both : InvalidOperationException
public class ZeroRangeCantBeReachedException : InvalidOperationException { }        // CalculateZeroParameters
public class TrajectoryCannotBeCalculatedException : InvalidOperationException { }  // Calculate
```

| Type | Raised when | What the user changes |
|---|---|---|
| `ZeroRangeCantBeReachedException` | the load cannot carry to the zero distance — a subsonic pistol load asked for a 1,000 yd zero | zero closer, more muzzle velocity, better BC |
| `TrajectoryCannotBeCalculatedException` | the numbers leave nothing to integrate — a zero or absurd BC, weight or muzzle velocity | fix the offending ammunition field |

Two rules follow:

- **Catch them before any broader handler.** They derive from `InvalidOperationException`, so a
  `catch (InvalidOperationException)` earlier in the chain swallows them.
- **Treat them as input errors, not crashes.** Show a plain message saying what to change; keep the stack
  trace for exceptions the engine has *not* named, which are bugs worth reporting. The other throws are
  ordinary argument faults and stay in that second category: `ArgumentException` for a form factor with no
  bullet diameter (or a non-positive form factor/weight), and `ArgumentNullException` when the BC table is
  `GC` and no `dragTable` was supplied.

Where a field can be checked up front — a non-positive BC, weight or muzzle velocity — validate instead of
catching. `TrajectoryCannotBeCalculatedException` is the net under that, not a substitute for it.

---

## 4. Reading the output (`TrajectoryPoint`, read-only)

| Property | Type | Meaning |
|----------|------|---------|
| `Time` | `TimeSpan` | Time of flight to this point. |
| `Distance` | `Measurement<DistanceUnit>` | Distance along the line of sight. |
| `DistanceFlat` | `Measurement<DistanceUnit>` | Horizontal distance from the muzzle. |
| `Velocity` | `Measurement<VelocityUnit>` | Projectile speed. |
| `Mach` | `double` | Speed relative to the local speed of sound. |
| `Drop` | `Measurement<DistanceUnit>` | Vertical position vs the line of sight. At the muzzle = −sight height. |
| `DropFlat` | `Measurement<DistanceUnit>` | Vertical position vs the muzzle (bore line). |
| `Windage` | `Measurement<DistanceUnit>` | Horizontal deflection. **Left +, right −.** Includes spin drift when modelled. |
| `Energy` | `Measurement<EnergyUnit>` | Kinetic energy. |
| `DropAdjustment` | `Measurement<AngularUnit>` | Angular scope correction for the drop at this distance. |
| `WindageAdjustment` | `Measurement<AngularUnit>` | Angular correction for the windage at this distance. |
| `LineOfSightElevation` | `Measurement<DistanceUnit>` | Height of the sight line at this distance. |
| `LineOfDepartureElevation` | `Measurement<DistanceUnit>` | Height of the bore line at this distance. |
| `OptimalGameWeight` | `Measurement<WeightUnit>` | Litz optimal game weight estimate. |

```csharp
foreach (var p in trajectory)
    Console.WriteLine($"{p.Distance.In(DistanceUnit.Yard):N0} yd  " +
                      $"{p.Velocity.In(VelocityUnit.FeetPerSecond):N0} fps  " +
                      $"drop {p.Drop.In(DistanceUnit.Inch):N1} in  " +
                      $"wind {p.Windage.In(DistanceUnit.Inch):N1} in");
```

---

## 5. Complete examples

### Minimal
```csharp
var ammo = new Ammunition(
    weight: new Measurement<WeightUnit>(168, WeightUnit.Grain),
    ballisticCoefficient: new BallisticCoefficient(0.223, DragTableId.G7),
    muzzleVelocity: new Measurement<VelocityUnit>(2700, VelocityUnit.FeetPerSecond));

var rifle = new Rifle(
    sight: new Sight(new Measurement<DistanceUnit>(1.5, DistanceUnit.Inch),
                     Measurement<AngularUnit>.ZERO, Measurement<AngularUnit>.ZERO),
    zero: new ZeroingParameters(new Measurement<DistanceUnit>(100, DistanceUnit.Yard), null, null));

var atmosphere = new Atmosphere();
var calc = new TrajectoryCalculator();

var shot = new ShotParameters
{
    MaximumDistance = new Measurement<DistanceUnit>(1000, DistanceUnit.Yard),
    Step = new Measurement<DistanceUnit>(100, DistanceUnit.Yard),
};
shot.Apply(calc.CalculateZeroParameters(ammo, atmosphere, rifle, rifle.Zero));

TrajectoryPoint[] trajectory = calc.Calculate(ammo, rifle, atmosphere, shot);
```

### Non-standard atmosphere + wind + spin drift
Spin drift is modelled only when the rifle has `Rifling` **and** the ammunition has both a bullet
diameter and length; it is folded into `Windage` (there is no separate output).
```csharp
var ammo = new Ammunition(
    weight: new Measurement<WeightUnit>(168, WeightUnit.Grain),
    ballisticCoefficient: new BallisticCoefficient(0.223, DragTableId.G7),
    muzzleVelocity: new Measurement<VelocityUnit>(2700, VelocityUnit.FeetPerSecond),
    bulletDiameter: new Measurement<DistanceUnit>(0.308, DistanceUnit.Inch),
    bulletLength: new Measurement<DistanceUnit>(1.2, DistanceUnit.Inch));

var rifle = new Rifle(
    sight: new Sight(new Measurement<DistanceUnit>(2.0, DistanceUnit.Inch),
                     Measurement<AngularUnit>.ZERO, Measurement<AngularUnit>.ZERO),
    zero: new ZeroingParameters(new Measurement<DistanceUnit>(100, DistanceUnit.Yard), null, null),
    rifling: new Rifling(new Measurement<DistanceUnit>(11.25, DistanceUnit.Inch), TwistDirection.Right));

var atmosphere = new Atmosphere(
    altitude: new Measurement<DistanceUnit>(5000, DistanceUnit.Foot),
    pressure: new Measurement<PressureUnit>(24.9, PressureUnit.InchesOfMercury),
    temperature: new Measurement<TemperatureUnit>(40, TemperatureUnit.Fahrenheit),
    humidity: 0.30);

var wind = new[]
{
    new Wind(new Measurement<VelocityUnit>(10, VelocityUnit.MilesPerHour),
             new Measurement<AngularUnit>(90, AngularUnit.Degree),
             new Measurement<DistanceUnit>(500, DistanceUnit.Yard)),   // out to 500 yd
    new Wind(new Measurement<VelocityUnit>(5, VelocityUnit.MilesPerHour),
             new Measurement<AngularUnit>(45, AngularUnit.Degree)),    // beyond 500 yd
};

var calc = new TrajectoryCalculator();
var shot = new ShotParameters
{
    MaximumDistance = new Measurement<DistanceUnit>(1000, DistanceUnit.Yard),
    Step = new Measurement<DistanceUnit>(100, DistanceUnit.Yard),
};
shot.Apply(calc.CalculateZeroParameters(ammo, atmosphere, rifle, rifle.Zero));
var trajectory = calc.Calculate(ammo, rifle, atmosphere, shot, wind);
```

---

## 6. Custom drag curves

Standard `G1..RA4` curves are applied automatically when the BC names a standard table — nothing
extra to do. **If, and only if, the task involves a *non-standard* drag curve — a custom/measured
drag table, a radar `.drg` file, or a multi-BC (BC-vs-Mach) profile — read
[`references/custom-drag.md`](references/custom-drag.md) and follow it.** That file has the full,
copy-pasteable recipes for all three (`DragTable` subclass, `DrgDragTable.Open`, and
`DrgDragTableFactory.Build` with `BcAtMach`), including the exact signatures. Do not reconstruct
this API from memory or hand-roll a drag table — the helpers exist and the reference has them.

All three techniques use table id **`GC`** and require passing the `DragTable` instance to **both**
`CalculateZeroParameters` and `Calculate` (`DragTable.Get(GC)` throws — you supply the instance).

---

## 7. Conventions & gotchas

- **Humidity is a fraction 0..1**, not a percentage (0.5 = 50 %).
- **Pressure** in the 4-argument `Atmosphere` constructor is the station pressure *at that altitude*;
  use the 5-argument overload with `pressureAtSeaLevel: true` to pass a sea-level value.
- **The zero correction must be applied before `Calculate`:** compute it with `CalculateZeroParameters`
  and copy it into the shot via `ShotParameters.Apply(...)`. There is no `SightAngle` property anymore.
- **Argument order differs between the two calls:** `CalculateZeroParameters(ammunition, atmosphere,
  rifle, zero)` puts atmosphere before rifle, whereas `Calculate(ammunition, rifle, atmosphere, shot)`
  puts rifle before atmosphere. Easy to transpose.
- **Windage sign:** left is positive, right is negative. **`Drop` at the muzzle = −sight height.**
- **Spin drift** needs `Rifling` **and** `BulletDiameter` **and** `BulletLength`; otherwise windage is
  wind-only. It is folded into `Windage` (no separate value). Right twist drifts right, left drifts left.
- **`GC` (custom) drag** requires passing the `DragTable` to **both** `CalculateZeroParameters` and
  `Calculate`; `DragTable.Get(DragTableId.GC)` throws — you must supply the instance.
- The returned array may contain **fewer rows** than requested (subsonic/steep runs stop early).
- **`ZeroRangeCantBeReachedException` / `TrajectoryCannotBeCalculatedException`** both derive from
  `InvalidOperationException` — catch them *before* any broader handler, and treat them as input errors the
  user can fix rather than crashes. See §3.
- **`AngularUnit.Mil` is the military mil (1/6400), not the milliradian** (`MRad`, 1/6283.19) — ~1.9 % apart.
  A mil-dot reticle is a milliradian instrument, and `MilDotReticle` is built in `MRad`.
- **Crosswind aerodynamic jump IS modelled** (Litz, Applied Ballistics Eq 5.4), so a pure crosswind moves the
  point of impact **vertically** as well as horizontally. Like spin drift it needs `Rifling` **and** the bullet
  diameter **and** length; without them the term is absent. Right twist + wind from the right lifts the impact.

---

## 8. Specialized topics (reference files)

These live in `references/` and load only when the task needs them — read the matching file and follow
it rather than reconstructing the API from memory:

- **Custom / non-standard drag curves** (custom `DragTable`, radar `.drg`, multi-BC synthesis) →
  [`references/custom-drag.md`](references/custom-drag.md) (see §6).
- **Serialization & persistence** — saving/loading `Ammunition`/`Rifle`/`Atmosphere`/libraries via BXml
  or `System.Text.Json`, embedding library objects in your own file format, and decorating your own
  classes for the BXml serializer → [`references/serialization.md`](references/serialization.md).
- **Reticles** — building a reticle definition in code and rendering it (e.g. to SVG), including
  bullet-drop-compensator markers → [`references/reticle.md`](references/reticle.md).
