The project is in a healthy, maintained state
Ruby bindings for ballistics-engine - A high-performance Rust-based ballistics calculations library with 4-DOF trajectory modeling, wind deflection, atmospheric effects, and more.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 13.0

Runtime

~> 0.9
 Project Readme

Ballistics Engine - Ruby Bindings

High-performance ballistics calculations library for Ruby, powered by Rust.

Features

  • 4-DOF Trajectory Modeling - Complete trajectory calculations with realistic physics
  • Multiple Drag Models - G1, G7, and G8 ballistic coefficients
  • Wind Deflection - Accurate wind drift calculations
  • Atmospheric Effects - Temperature, pressure, humidity, and altitude compensation
  • Unit Conversion - Automatic handling of imperial/metric conversions
  • High Performance - Rust-based calculations for maximum speed
  • JSON Command Bridge - bridge_call reaches every engine command (solve, dope cards, profiles, truing) through one versioned JSON envelope

Installation

Add this line to your application's Gemfile:

gem 'ballistics-engine'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install ballistics-engine

Quick Start

The API is hash-in / hash-out — pass a hash of imperial-unit parameters, get a hash back.

require 'ballistics_engine'

# 168gr .308 with a 10 mph crosswind
result = BallisticsEngine.solve(
  "bc"                     => 0.223,   # G7 BC
  "bullet_weight_grains"   => 168.0,
  "muzzle_velocity_fps"    => 2650.0,
  "bullet_diameter_inches" => 0.308,
  "bullet_length_inches"   => 1.2,
  "sight_height_inches"    => 1.5,
  "zero_distance_yards"    => 100.0,
  "drag_model"             => "G7",    # G1 | G7 | G8
  "wind"       => { "speed_mph" => 10.0, "direction_degrees" => 90.0 },
  # Readings: pass the ones you have and leave the rest out (see "Atmosphere" below).
  "atmosphere" => { "temperature_f" => 72.0, "pressure_inhg" => 29.31,
                    "humidity_percent" => 50.0, "altitude_feet" => 600.0 }
)

puts "Max range:       #{result['max_range_yards'].round(1)} yd"
puts "Time of flight:  #{result['time_of_flight'].round(3)} s"
puts "Impact velocity: #{result['impact_velocity_fps'].round(1)} fps"
result["points"].each do |p|
  puts "  range #{p['x'].round(1)}yd  drop #{p['y'].round(2)}yd  vel #{p['velocity_fps'].round(1)}fps"
end

Velocity-dependent BC (bc_segments)

Supply your own velocity:BC ladder (velocities in fps). The BC applies while the bullet's current speed is in [velocity_min_fps, velocity_max_fps):

BallisticsEngine.solve(inputs.merge(
  "use_bc_segments"  => true,
  "bc_segments_data" => [
    { "velocity_min_fps" => 1800.0, "velocity_max_fps" => 4000.0, "bc" => 0.243 },
    { "velocity_min_fps" => 1500.0, "velocity_max_fps" => 1800.0, "bc" => 0.228 },
    { "velocity_min_fps" => 1200.0, "velocity_max_fps" => 1500.0, "bc" => 0.205 }
  ]
))
# Also accepts [vmin_fps, vmax_fps, bc] triples.

Segmented wind (wind_segments)

Distance-keyed wind (independent of the velocity-keyed BC above) — e.g. downrange sensors:

BallisticsEngine.solve(inputs.merge(
  # [speed_mph, angle_degrees, until_yards]  (angle 90 = from the right)
  "wind_segments" => [[5.0, 90.0, 150.0], [7.0, 90.0, 300.0], [9.0, 90.0, 1000.0]]
))

Zero angle and Monte Carlo

z = BallisticsEngine.calculate_zero_angle(inputs.merge("target_distance_yards" => 1000.0))
puts "zero: #{z['zero_angle_moa'].round(2)} MOA"

mc = BallisticsEngine.monte_carlo(inputs.merge(
  "num_simulations"      => 1000,
  "velocity_std_dev_fps" => 10.0,
  "bc_std_dev"           => 0.01,
  "target_distance_yards"=> 1000.0
))
puts "hit probability: #{(mc['hit_probability'] * 100).round(1)}%"

API Reference

All functions live on the BallisticsEngine module and take a single hash with string keys in imperial units. Unknown keys are ignored; only the seven marked required are needed.

BallisticsEngine.solve(hash) => hash

Required: bc, bullet_weight_grains, muzzle_velocity_fps, bullet_diameter_inches, bullet_length_inches, sight_height_inches, zero_distance_yards.

Optional (selected):

  • Aim/geometry: drag_model("G7"), shooting_angle_degrees, muzzle_angle_degrees, twist_rate_inches(10), is_right_twist(true), muzzle_height_inches, target_height_inches.
  • Wind: wind = {speed_mph, direction_degrees}; wind_segments = [[speed_mph, angle_deg, until_yards], ...].
  • Atmosphere: atmosphere = {temperature_f, pressure_inhg, humidity_percent, altitude_feet}. pressure_inhg is absolute station pressure, not a sea-level-corrected altimeter setting. See "Atmosphere readings" below.
  • BC: use_bc_segments, bc_segments_data (see above), bc_segments = [[mach, bc], ...].
  • Coriolis: enable_coriolis, latitude_degrees, shot_direction_degrees.
  • Physics flags: enable_advanced_effects, enable_magnus, enable_aerodynamic_jump, use_enhanced_spin_drift, use_form_factor, use_cluster_bc, enable_pitch_damping, enable_precession_nutation, enable_wind_shear (+ wind_shear_model), use_rk4, use_adaptive_rk45. Leaving wind_shear_model out picks "power_law" (the CLI's default model) when enable_wind_shear is true and "none" when it is false; a model you pass, "none" included, is used exactly as given.
  • Powder: use_powder_sensitivity, powder_temp_sensitivity, powder_temp_f, powder_temp_curve = [[temp_f, velocity_fps], ...], powder_curve_temp_f.
  • Sampling/solver: enable_trajectory_sampling, sample_interval_yards, max_range_yards, time_step_seconds.

Returns: max_range_yards, max_height_yards, time_of_flight, impact_velocity_fps, impact_energy_ftlbs, points (array of {time, x, y, z, velocity_fps, energy_ftlbs} — x=range, y=drop, z=windage, in yards). Present when enabled: sampled_points, aerodynamic_jump, angular_state, max_yaw_angle_rad, max_precession_angle_rad, min_pitch_damping, transonic_mach.

Atmosphere readings

A temperature_f or pressure_inhg key in the atmosphere hash is a reading and flies exactly as given, whatever its value and whatever the altitude. A real 59 °F or 29.92 inHg at 5000 ft flies as 59 °F or 29.92 inHg. Leave a key out (or set it to nil) when you have no reading. It then defaults to 59 °F / 29.92 inHg, and above 1 m of altitude it flies the ICAO standard air at altitude_feet instead (about 41 °F and 24.9 inHg at 5000 ft). With no atmosphere hash at all the shot is at sea level in standard air.

Before engine 0.49.0 the binding could not tell the two apart. At any altitude above 1 m, a temperature within 0.1 °C of 15 °C (58.82 to 59.18 °F) or a pressure within 0.5 hPa of 1013.25 hPa (about 29.906 to 29.936 inHg) was treated as omitted and replaced with standard air, even when it was a real reading.

BallisticsEngine.bridge_call(request_json) => String

Raw pass-through to the engine's versioned JSON command bridge: one full request envelope in as a String, one full response envelope out as a String. Nothing is parsed and nothing is raised — the bridge never panics and reports every failure in band as {"ok":false,"error":{...}}, so the caller owns the envelope.

require "json"

def bridge(command, request)
  JSON.parse(BallisticsEngine.bridge_call(
    JSON.generate("api_version" => 1, "command" => command, "request" => request)
  ))
end

bridge("meta.version", {})
# => {"ok"=>true, "api_version"=>1, "engine_version"=>"0.50.1",
#     "command"=>"meta.version", "result"=>{"engine_version"=>"0.50.1"}}

# Every command reachable in this build:
bridge("meta.capabilities", {})["result"]["commands"]
# => ["meta.capabilities", "meta.version", "solve", "card.come_ups", "card.range_table",
#     "card.wind", "profile.validate", "profile.normalize", "true.fit", "true.wind",
#     "true.tall_target", "true.dsf", "true.plan", "true.dial_plan", "bc5d.info"]

command is one of the names meta.capabilities reports; request is that command's own document. solve takes a solve-json v1 document — strict SI units, unknown fields rejected with a JSON path — and is the only way to reach fields the hash API above has no key for, such as atmosphere.pressure_reference (QNH altimeter settings) and corrections.bc5d_table_path:

r = bridge("solve", {
  "schema_version" => 1,
  "projectile" => { "mass_kg" => 0.01134, "diameter_m" => 0.00782,
                    "drag_model" => "G7", "ballistic_coefficient" => 0.243 },
  "rifle"      => { "muzzle_velocity_mps" => 823.0, "sight_height_m" => 0.05 },
  "shot"       => { "max_range_m" => 1000.0, "zero_distance_m" => 100.0 },
  "atmosphere" => { "altitude_m" => 1200.0, "pressure_pa" => 101_325.0,
                    "pressure_reference" => "qnh" },
  "wind"       => { "speed_mps" => 4.4704, "direction_from_rad" => Math::PI / 2 },
  "solver"     => { "method" => "rk45" },
  "effects"    => { "wind_shear_model" => "power_law" },
  "sampling"   => { "interval_m" => 100.0 }
})
r["ok"]                                   # => true
r["result"]["samples"].last["windage_m"]  # => -2.7677...

The pdf / card.pdf and profile.import_a7p commands are compiled out of this gem (it builds the engine with default-features = false); meta.capabilities lists only what this build can actually run.

BallisticsEngine.calculate_zero_angle(hash) => hash

Same bullet keys as solve, plus required target_distance_yards (and optional target_height_inches, wind, atmosphere; atmosphere readings work as for solve). Returns zero_angle_radians, zero_angle_degrees, zero_angle_moa. Raises on non-convergence.

BallisticsEngine.monte_carlo(hash) => hash

Bullet keys (optionally nested under base_inputs) plus std-dev params: num_simulations(1000), velocity_std_dev_fps, angle_std_dev_radians, bc_std_dev, wind_speed_std_dev_mph, azimuth_std_dev_radians, target_distance_yards, base_wind_speed_mph, base_wind_direction_degrees, hit_radius_inches(≈11.8). Returns ranges_yards, impact_velocities_fps, impact_positions ([{vertical_inches, lateral_inches}, ...]), hit_probability, num_simulations. It has no atmosphere input: every simulation flies sea-level standard air, and an atmosphere hash is ignored.

Development

After checking out the repo, run bundle install to install dependencies.

To build the native extension:

bundle exec rake compile

To run tests:

bundle exec rake test

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/ajokela/ballistics-engine.

License

This project is licensed under either of:

at your option.