ballistics-engine 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,6 +10,15 @@ A high-performance ballistics trajectory calculation engine with comprehensive p
10
10
  - **Advanced Drag Models** - Full standard-projectile family (G1, G2, G5, G6, G7, G8, GI, GS, and the British RA4 reference function), each backed by its own real Mach-indexed table with automatic transonic corrections, plus user-supplied custom Cd(Mach) drag tables (`--drag-table`, used as-is with endpoint hold outside their measured domain, no transonic correction applied — see [CLI_USAGE.md](CLI_USAGE.md#custom-drag-tables); `bc_value` is ignored while a custom table is active), with an optional `--cd-scale <FACTOR>` whole-curve truing multiplier (Hornady AFF / AB CDF style; `1.0` = neutral, typical range 0.90-1.10; requires `--drag-table` — see [CLI_USAGE.md](CLI_USAGE.md#whole-curve-drag-scale---cd-scale)). `true-velocity`/`plan-truing`'s forward model is deliberately G1/G7 only. GL is out of scope — its only public source is velocity-domain data, which doesn't fit this engine's Mach-indexed table format.
11
11
  - **Automatic Zeroing** - Calculate sight adjustments and apply zero angles automatically; `trajectory` echoes the solved bore angle (degrees, additive across table/JSON/CSV) whenever auto-zero ran, and `zero --from-angle <DEGREES>` solves the zero RANGE(S) a previously solved/stored bore angle produces — a bore angle generally implies two zeros (the classic 25/300-yard relationship), so both are reported rather than one being silently picked — so the angle can be captured once and reused later, independent of the day it was solved — see [CLI_USAGE.md](CLI_USAGE.md#solving-range-from-a-stored-angle---from-angle)
12
12
  - **Canted-Rifle Modeling** - Model a rifle zeroed level but fired canted (`--cant <DEGREES>`, alias `--cant-angle`, on `trajectory`/`monte-carlo`); clockwise cant shifts point of impact right and low downrange for a rifle with an upward zero correction — see [CLI_USAGE.md](CLI_USAGE.md#canted-shooting)
13
+ - **Deliberate Zero POI Offset** - Record that the rifle is deliberately zeroed off — e.g. 0.1 in high / 0.2 in left at the zero range (Kestrel ZH/ZO semantics) — and shift the whole solution by the equivalent angular bias (`--zero-poi-up`/`--zero-poi-right`, inches imperial / cm metric, on `trajectory` and every zero-solving subcommand; solve-json `shot.zero_poi_up_m`/`zero_poi_right_m`; saved-profile fields; `.a7p` zero click counts convertible via `profile import --zero-click`) — see [CLI_USAGE.md](CLI_USAGE.md#sight-geometry-and-zero-state)
14
+ - **Lateral Sight Offset** - Model offset-mounted optics (`--sight-offset`, inches imperial / mm metric, positive = sight right of bore): the bullet starts that far left of the sight line and the windage zero converges it onto the sight line at the zero range — physical mount geometry, distinct from and additive with the zero POI offset (solve-json `rifle.sight_offset_lateral_m`; saved-profile field; also on `monte-carlo`/WEZ mirroring `--cant`) — see [CLI_USAGE.md](CLI_USAGE.md#sight-geometry-and-zero-state)
15
+ - **Multiple Named Zeroes / Per-Load Offsets in Profiles** - Store alternate zero conditions on a saved profile (Lapua Sight-In POI / ATrag zero-zone class): each named set carries an optional zero distance plus constant per-load dial corrections in mils (`profile zero-set add|remove|list`; `--zero-set NAME` on `trajectory`/`come-ups`/`wind-card`/`range-table`/`dsf`/`plan-truing`; corrections are added to total-correction dial outputs before the tracking-CF division; profile-CSV `V_OFFSET_MIL`/`H_OFFSET_MIL` columns and `.a7p --zero-click` import feed sets automatically; unknown names fail loudly listing the available sets) — see [CLI_USAGE.md](CLI_USAGE.md#named-zero-sets-and-per-load-offsets-profile-zero-set---zero-set)
16
+ - **Drops Reference: LOS vs Target Plane** - Reference sampled drops to the line of sight (default, unchanged) or to the vertical target plane on steep inclined shots (`trajectory --drops-reference target`: drop ÷ cos(shooting angle), JBM's "target plane" checkbox; sampled table/CSV column relabeled `Drop (target)`/`drop_target_in`; solve-json `shot.drops_reference`) — see [CLI_USAGE.md](CLI_USAGE.md#drops-reference-los-vs-target-plane---drops-reference--mba-1403)
17
+ - **Scope Tracking Correction Factors** - Compensate turrets that don't track their nominal click value: derive `CF = actual/dialed` from a tall-target test (`tall-target` subcommand, pure arithmetic), then `--elevation-cf`/`--windage-cf` (or validated saved-profile fields) DIVIDE every dial-unit output once at the shared conversion boundary — come-ups, range/compare/wind cards, mover lead/Ring, PDF dope card, zero MOA/mrad — never raw inches (an under-tracking scope, CF < 1, needs more dial); `true-velocity` multiplies dialed observations by the CF (scope-dial to true angular) so scope error is not baked into trued MV/BC — see [CLI_USAGE.md](CLI_USAGE.md#scope-tracking-correction-factors---elevation-cf---windage-cf-tall-target--mba-1358)
18
+ - **Equivalent Horizontal Range (BDC shoot-to)** - Inclined zeroed shots print the flat range whose angular correction against the same zero matches the inclined solution (SIG AMR / Leica EHR / Gunwerks style, angular-match inversion over one flat re-solve — not the rifleman's-rule cosine), so fixed BDC turrets/reticles can dial as if flat (`trajectory` summary line; solve-json `summary.equivalent_horizontal_range_m`; public `TrajectorySolver::equivalent_horizontal_range`) — see [CLI_USAGE.md](CLI_USAGE.md#equivalent-horizontal-range-bdc-shoot-to--mba-1395)
19
+ - **Reticle Hold Points** - Place a firing solution where you actually read it: a point in your own reticle, FFP/SFP aware with second-focal-plane subtensions rescaled for the magnification in use (`reticle hold`, native + browser terminal + a new appended C ABI export; `reticle generate mil-grid|tree|bdc` builds descriptions in one shared serde schema that saved profiles, `--reticle-json` and solve-json's optional `reticle` block all speak). Horus/TREMOR grid layouts, wind-dot calibration and any vendor catalog are deliberately excluded — see [CLI_USAGE.md](CLI_USAGE.md#reticle-hold-points-reticle--mba-1361)
20
+ - **Reticle/BDC Inverse Solvers** - Three read-only solvers over an existing load, sharing one drop-vs-range root find: `mark-to-range` maps each reticle subtension to the range where it lands (Nightforce / Nikon Spot On / Swarovski / TRACT), reporting marks the load cannot reach rather than dropping them; `bdc-match` fits the magnification that makes an SFP BDC reticle match the load (Zeiss Rapid-Z; closed-form least squares, with a residual warning when nothing fits); `optimal-zero` min-max searches the one zero that minimizes the largest hold a whole target list needs and reports whether a dead-center hold keeps each inside its vital zone (GeoBallistics HDZ). CLI-only this train — see [CLI_USAGE.md](CLI_USAGE.md#reticlebdc-inverse-solvers--mba-1362)
21
+ - **Robust Hold Corridors** - Solve a bounded set of NAMED segmented-wind scenarios at once (`hold-corridor --scenarios set.json --ranges 200,400,600 [--target rect:WxH|circle:D]`) and get, at every range, each scenario's hold, the min/max corridor they span, the minimax (Chebyshev-center) hold, the worst-case miss from it, and whether one hold keeps every scenario inside the target — with a versioned `RobustHoldReportV1` JSON form. Caps (≤8 scenarios, ≤64 ranges) and malformed segments are structured errors raised *before* any solving, and reordering scenarios cannot change the answer. No probabilities are assigned anywhere: the corridor is the span of the hypotheses you supplied, not a confidence interval — see [CLI_USAGE.md](CLI_USAGE.md#robust-hold-corridors-hold-corridor--mba-1349)
13
22
  - **Moving-Target Lead** - Wind-aware hold tables for targets moving at a constant speed/angle, with iterative intercept-range correction for non-perpendicular motion (`lead` subcommand; public `ballistics_engine::calculate_lead` API) — see [CLI_USAGE.md](CLI_USAGE.md#moving-target-lead)
14
23
  - **Mover Ring** - Field-tested alternative for engaging movers: a per-point ring radius (`target_speed × time-of-flight`) falls out of an already-solved trajectory with no second command or re-entered ballistic data (`trajectory --target-speed`, additive across table/JSON/CSV output); `lead` also gained `trajectory`'s powder-temperature flags for muzzle-velocity parity between the two — see [CLI_USAGE.md](CLI_USAGE.md#mover-ring---target-speed)
15
24
  - **Side-by-Side Load Comparison** - Compare 2-8 loads at identical conditions with per-load independent zeroing (`compare --load "NAME:DRAG:BC:MASS:VELOCITY[:DIAMETER]"`, mixable with saved profiles); JSON/CSV output carries per-row deltas against the first load — see [CLI_USAGE.md](CLI_USAGE.md#load-comparison-compare)
@@ -17,6 +26,8 @@ A high-performance ballistics trajectory calculation engine with comprehensive p
17
26
  - **Unit Conversion** - Seamless switching between Imperial (default) and Metric units
18
27
  - **BC Segmentation** - Velocity-dependent ballistic coefficient modeling with automatic estimation
19
28
  - **Atmospheric Modeling** - Temperature, pressure, humidity, and altitude effects with ICAO standard atmosphere; also accepts a single **density altitude** reading (`trajectory --density-altitude`, feet imperial / meters metric) as a direct alternative to entering altitude/pressure/temperature separately — back-solves an ISA-equivalent atmosphere (preserving Mach/lapse-rate/segmented-atmosphere behavior, not a density-only shortcut) and supersedes `--altitude`/`--pressure`/`--pressure-type` entirely, with an explicit `--temperature` still honored for correct powder-temperature sensitivity — see [CLI_USAGE.md](CLI_USAGE.md#density-altitude-as-a-direct-input---density-altitude)
29
+ - **Clock-Position Wind Entry** - Enter wind direction as the dominant field convention: marked clock positions (`--wind-direction 3oc`, `10h30`, or `10:30`; 12 o'clock = headwind, minutes count 0.5°) alongside plain degrees on every wind-direction flag and the WASM terminal; inside `--wind-segment` the colon-free forms apply (`10:3oc:400`) while `10:30:400` keeps its numeric SPEED:ANGLE:DIST meaning; bare numbers stay degrees everywhere — see [CLI_USAGE.md](CLI_USAGE.md#wind-direction-entry-degrees-clock-positions--mba-1367)
30
+ - **Earth-Fixed Compass Wind Bearings** - Store wind as absolute compass bearings and let the solver re-reference them against the shot azimuth (`--wind-ref compass` + `--shot-direction` on `trajectory`/`monte-carlo`; covers the single direction, location-CSV WIND_DIR, and every `--wind-segment` angle; Monte Carlo converts before dispersion sampling; solve-json `wind.wind_reference`; WASM builder `setWindReference`/`setShotDirection`; wind FROM north on a shot due north = pure headwind, pinned) — see [CLI_USAGE.md](CLI_USAGE.md#earth-fixed-compass-bearings---wind-ref-compass--mba-1368)
20
31
  - **Wind Effects** - 3D wind calculations with altitude-dependent wind shear modeling, **downrange-segmented wind** (`--wind-segment SPEED:ANGLE:DIST[:VERTICAL]`, repeatable — model wind that varies along the path, e.g. muzzle plus downrange sensor readings), and **vertical wind** (`--wind-vertical <SPEED>` on `trajectory`/`monte-carlo`, or the segment's optional 4th field; positive = updraft, raises point of impact) — see [CLI_USAGE.md](CLI_USAGE.md#vertical-wind)
21
32
  - **Oblique Wind-Drift Cards** - Wind dope cards at any wind-FROM angle, not just full-value 90° crosswind (`wind-card --wind-angle <DEG>` or `--wind-angles <CSV>` for one card per angle); each cell is a real trajectory solve, default (no flags) unchanged from the classic full-value 90° card — see [CLI_USAGE.md](CLI_USAGE.md#wind-card)
22
33
  - **Monte Carlo Simulations** - Statistical analysis with parameter uncertainties
@@ -433,6 +444,31 @@ propagated drop bands. Declared sigmas are treated as absolute known errors, so
433
444
  covariance is not rescaled by residual RMS. With no uncertainty flags, the existing
434
445
  point estimate and output schema are unchanged.
435
446
 
447
+ ### Wind-Call Truing (`true-wind`)
448
+
449
+ `true-velocity` trues the vertical axis; `true-wind` (MBA-1392) trues the other one.
450
+ Give it where your groups actually landed left/right of the aim point and it back-solves
451
+ the crosswind that reproduces that miss through the real forward model, plus a wind-call
452
+ correction factor against the wind you *called*:
453
+
454
+ ```bash
455
+ ./ballistics true-wind \
456
+ --miss 500:14.0 --miss 700:29.5 \
457
+ -v 2700 -b 0.475 -m 168 -d 0.308 --drag-model g7 \
458
+ --twist-rate 11 --called-wind 9
459
+ ```
460
+
461
+ A horizontal miss is not purely wind, so the command separates it: `--twist-rate` is
462
+ **required** and gyroscopic spin drift is always modelled and subtracted (a 1:11" .308
463
+ drifts ~3.5 in right at 700 yd — read as wind, that alone is several mph of error), and
464
+ `--latitude` with `--shot-direction` adds Coriolis. Anything the model had no data for
465
+ stays absorbed in the solved wind and is named in the report, so a contaminated number is
466
+ never presented as pure wind. Signs are documented in one block: `--miss` positive =
467
+ impact **right** of aim, solved wind positive = wind **from the shooter's left**
468
+ (9 o'clock) pushing impacts right. `--miss` values are linear inches off the target, not
469
+ dial readings, so scope tracking correction factors deliberately do not apply.
470
+ See [CLI_USAGE.md](CLI_USAGE.md#wind-call-truing-true-wind--mba-1392).
471
+
436
472
  ### DSF (Drop-Scale-Factor) Truing
437
473
 
438
474
  Second stage of the Applied Ballistics-style two-stage truing workflow (MBA-1357).
@@ -45,10 +45,24 @@ export class Calculator {
45
45
  setMass(mass_grains: number): Calculator;
46
46
  setMaxRange(range_yards: number): Calculator;
47
47
  setPressure(pressure_inhg: number): Calculator;
48
+ /**
49
+ * MBA-1368: compass bearing of the shot (degrees, 0 = north, 90 = east) —
50
+ * required by `setWindReference("compass")`; also feeds Coriolis when enabled.
51
+ */
52
+ setShotDirection(bearing_deg: number): Calculator;
48
53
  setSightHeight(height_inches: number): Calculator;
49
54
  setTemperature(temp_f: number): Calculator;
50
55
  setVelocity(velocity_fps: number): Calculator;
51
56
  setWind(speed_mph: number, direction_deg: number): Calculator;
57
+ /**
58
+ * MBA-1368: choose the wind entry frame — "shooter" (default; wind-FROM angles
59
+ * relative to the line of fire) or "compass" (earth-fixed bearings, 0 = north,
60
+ * covering `setWind` AND every `addWindSegment` direction, re-referenced against
61
+ * the shot azimuth at solve time). Compass mode requires `setShotDirection`;
62
+ * invalid values surface when the command runs. Additive method — no existing
63
+ * signature changed.
64
+ */
65
+ setWindReference(mode: string): Calculator;
52
66
  setZeroRange(range_yards: number): Calculator;
53
67
  }
54
68
 
@@ -150,10 +164,12 @@ export interface InitOutput {
150
164
  readonly calculator_setMass: (a: number, b: number) => number;
151
165
  readonly calculator_setMaxRange: (a: number, b: number) => number;
152
166
  readonly calculator_setPressure: (a: number, b: number) => number;
167
+ readonly calculator_setShotDirection: (a: number, b: number) => number;
153
168
  readonly calculator_setSightHeight: (a: number, b: number) => number;
154
169
  readonly calculator_setTemperature: (a: number, b: number) => number;
155
170
  readonly calculator_setVelocity: (a: number, b: number) => number;
156
171
  readonly calculator_setWind: (a: number, b: number, c: number) => number;
172
+ readonly calculator_setWindReference: (a: number, b: number, c: number) => number;
157
173
  readonly calculator_setZeroRange: (a: number, b: number) => number;
158
174
  readonly wasmballistics_clearDragTable: (a: number) => number;
159
175
  readonly wasmballistics_hasBc5dTable: (a: number) => number;
@@ -175,6 +175,17 @@ export class Calculator {
175
175
  const ret = wasm.calculator_setPressure(ptr, pressure_inhg);
176
176
  return Calculator.__wrap(ret);
177
177
  }
178
+ /**
179
+ * MBA-1368: compass bearing of the shot (degrees, 0 = north, 90 = east) —
180
+ * required by `setWindReference("compass")`; also feeds Coriolis when enabled.
181
+ * @param {number} bearing_deg
182
+ * @returns {Calculator}
183
+ */
184
+ setShotDirection(bearing_deg) {
185
+ const ptr = this.__destroy_into_raw();
186
+ const ret = wasm.calculator_setShotDirection(ptr, bearing_deg);
187
+ return Calculator.__wrap(ret);
188
+ }
178
189
  /**
179
190
  * @param {number} height_inches
180
191
  * @returns {Calculator}
@@ -212,6 +223,23 @@ export class Calculator {
212
223
  const ret = wasm.calculator_setWind(ptr, speed_mph, direction_deg);
213
224
  return Calculator.__wrap(ret);
214
225
  }
226
+ /**
227
+ * MBA-1368: choose the wind entry frame — "shooter" (default; wind-FROM angles
228
+ * relative to the line of fire) or "compass" (earth-fixed bearings, 0 = north,
229
+ * covering `setWind` AND every `addWindSegment` direction, re-referenced against
230
+ * the shot azimuth at solve time). Compass mode requires `setShotDirection`;
231
+ * invalid values surface when the command runs. Additive method — no existing
232
+ * signature changed.
233
+ * @param {string} mode
234
+ * @returns {Calculator}
235
+ */
236
+ setWindReference(mode) {
237
+ const ptr = this.__destroy_into_raw();
238
+ const ptr0 = passStringToWasm0(mode, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
239
+ const len0 = WASM_VECTOR_LEN;
240
+ const ret = wasm.calculator_setWindReference(ptr, ptr0, len0);
241
+ return Calculator.__wrap(ret);
242
+ }
215
243
  /**
216
244
  * @param {number} range_yards
217
245
  * @returns {Calculator}
Binary file
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "Alex Jokela <email@tinycomputers.io>"
6
6
  ],
7
7
  "description": "High-performance ballistics trajectory engine with professional physics",
8
- "version": "0.30.0",
8
+ "version": "0.31.0",
9
9
  "license": "MIT OR Apache-2.0",
10
10
  "repository": {
11
11
  "type": "git",