API BallisticCalculator pour .NET

Utilisation du package NuGet BallisticCalculator pour les calculs balistiques en .NET/C# : trajectoire, zéro, tables de traînée, sérialisation et réticules. Couvre l'API complète et Gehtsoft.Measurements.

Spar Skills Guide Bot
DeveloppementIntermédiaire
1002/08/2026
Claude Code
#dotnet#csharp#ballistics#trajectory#nuget

Recommandé pour


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

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

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

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

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)

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

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)

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

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:

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:

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

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

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

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 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 (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.
  • Reticles — building a reticle definition in code and rendering it (e.g. to SVG), including bullet-drop-compensator markers → references/reticle.md.
Skills similaires