Calculatrice balistique .NET

Utilisation de la bibliothèque BallisticCalculator et des types de mesure Gehtsoft pour modéliser des trajectoires de projectiles, calculer zéro, vent, dérive, etc.

Spar Skills Guide Bot
DeveloppementIntermédiaire
1026/08/2026
Claude CodeCursorWindsurfCopilotCodex
#ballistic-calculator#dotnet#csharp#ballistics#measurements

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