ballistics-ng 0.1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/ballistics.rb ADDED
@@ -0,0 +1,77 @@
1
+ require 'ballistics/ext'
2
+
3
+ module Ballistics
4
+ TABLE_FIELDS = {
5
+ range: {
6
+ label: "Range",
7
+ format: "%i",
8
+ },
9
+ time: {
10
+ label: "Time",
11
+ format: "%0.3f",
12
+ },
13
+ velocity: {
14
+ label: "FPS",
15
+ format: "%0.1f",
16
+ },
17
+ moa_correction: {
18
+ label: "MOA",
19
+ format: "%0.1f",
20
+ },
21
+ path: {
22
+ label: "Path",
23
+ format: "%0.1f",
24
+ },
25
+ }
26
+
27
+ def self.table(trajectory: nil, fields: nil, opts: {})
28
+ trajectory ||= self.trajectory(opts)
29
+ fields ||= [:range, :time, :velocity, :path]
30
+
31
+ # Create an array of field labels and format strings once
32
+ labels = []
33
+ formats = []
34
+ fields.each { |sym|
35
+ cfg = TABLE_FIELDS.fetch(sym)
36
+ labels << cfg.fetch(:label)
37
+ formats << cfg.fetch(:format)
38
+ }
39
+
40
+ # Iterate over trajectory structure and return a multiline string
41
+ labels.join("\t") + "\n" + trajectory.map { |hsh|
42
+ formats.join("\t") % fields.map { |sym| hsh.fetch(sym.to_s) }
43
+ }.join("\n")
44
+ end
45
+
46
+ def self.zero_angle(opts = {})
47
+ opts[:y_intercept] ||= 0
48
+ args = [
49
+ :drag_number,
50
+ :ballistic_coefficient,
51
+ :velocity,
52
+ :sight_height,
53
+ :zero_range,
54
+ :y_intercept,
55
+ ].map { |arg| opts.fetch(arg) }
56
+
57
+ Ballistics::Ext.zero_angle(*args)
58
+ end
59
+
60
+ def self.trajectory(opts = {})
61
+ opts[:zero_angle] ||= self.zero_angle(opts)
62
+ args = [
63
+ :drag_number,
64
+ :ballistic_coefficient,
65
+ :velocity,
66
+ :sight_height,
67
+ :shooting_angle,
68
+ :zero_angle,
69
+ :wind_speed,
70
+ :wind_angle,
71
+ :max_range,
72
+ :interval,
73
+ ].map { |arg| opts.fetch(arg) }
74
+
75
+ Ballistics::Ext.trajectory(*args)
76
+ end
77
+ end
@@ -0,0 +1,110 @@
1
+ require 'bigdecimal'
2
+ require 'bigdecimal/util'
3
+ require 'ballistics/yaml'
4
+
5
+ # http://www.exteriorballistics.com/ebexplained/4th/51.cfm
6
+
7
+ class Ballistics::Atmosphere
8
+ MANDATORY = {
9
+ "altitude" => :float, # feet
10
+ "humidity" => :percent, # float between 0 and 1
11
+ "pressure" => :float, # inches of mercury
12
+ "temp" => :float, # degrees fahrenheit
13
+ }
14
+
15
+ # US Army standard, used by most commercial ammunition specs
16
+ ARMY = {
17
+ "altitude" => 0,
18
+ "humidity" => 0.78,
19
+ "pressure" => 29.5275,
20
+ "temp" => 59,
21
+ }
22
+
23
+ # International Civil Aviation Organization
24
+ ICAO = {
25
+ "altitude" => 0,
26
+ "humidity" => 0,
27
+ "pressure" => 29.9213,
28
+ "temp" => 59,
29
+ }
30
+
31
+ # altitude coefficients
32
+ ALTITUDE_A = -4e-15.to_d
33
+ ALTITUDE_B = 4e-10.to_d
34
+ ALTITUDE_C = -3e-5.to_d
35
+ ALTITUDE_D = 1.to_d
36
+
37
+ # humidity coefficients
38
+ VAPOR_A = 4e-6.to_d
39
+ VAPOR_B = -0.0004.to_d
40
+ VAPOR_C = 0.0234.to_d
41
+ VAPOR_D = -0.2517.to_d
42
+
43
+ DRY_AIR_INCREASE = 0.3783.to_d
44
+ DRY_AIR_REDUCTION = 0.9950.to_d
45
+ RANKLINE_CORRECTION = 459.4.to_d
46
+ TEMP_ALTITUDE_CORRECTION = -0.0036.to_d # degrees per foot
47
+
48
+ def self.altitude_factor(altitude)
49
+ altitude = altitude.to_d
50
+ 1 / (ALTITUDE_A * altitude ** 3 +
51
+ ALTITUDE_B * altitude ** 2 +
52
+ ALTITUDE_C * altitude +
53
+ ALTITUDE_D)
54
+ end
55
+
56
+ def self.humidity_factor(temp, pressure, humidity)
57
+ vpw = VAPOR_A * temp ** 3 +
58
+ VAPOR_B * temp ** 2 +
59
+ VAPOR_C * temp +
60
+ VAPOR_D
61
+ DRY_AIR_REDUCTION * pressure /
62
+ (pressure - DRY_AIR_INCREASE * humidity * vpw)
63
+ end
64
+
65
+ def self.pressure_factor(pressure)
66
+ pressure = pressure.to_d
67
+ (pressure - ARMY["pressure"]) / ARMY["pressure"]
68
+ end
69
+
70
+ def self.temp_factor(temp, altitude)
71
+ std_temp = ARMY["temp"] + altitude * TEMP_ALTITUDE_CORRECTION
72
+ (temp - std_temp) / (RANKLINE_CORRECTION + std_temp)
73
+ end
74
+
75
+ def self.translate(bc, altitude:, humidity:, pressure:, temp:)
76
+ bc.to_d *
77
+ self.altitude_factor(altitude) *
78
+ self.humidity_factor(temp, pressure, humidity) *
79
+ (self.temp_factor(temp, altitude) -
80
+ self.pressure_factor(pressure) + 1)
81
+ end
82
+
83
+ def self.icao
84
+ self.new ICAO
85
+ end
86
+
87
+ def self.army
88
+ self.new ARMY
89
+ end
90
+
91
+ attr_reader(*MANDATORY.keys)
92
+ attr_reader(:yaml_data, :extra)
93
+
94
+ def initialize(hsh = ARMY.dup)
95
+ @yaml_data = hsh
96
+ MANDATORY.each { |field, type|
97
+ val = hsh[field] || hsh[field.to_sym] or raise("#{field} not provided")
98
+ Ballistics::YAML.check_type!(val, type)
99
+ self.instance_variable_set("@#{field}", val.to_d)
100
+ }
101
+ end
102
+
103
+ def translate(ballistic_coefficient)
104
+ self.class.translate(ballistic_coefficient,
105
+ altitude: @altitude,
106
+ humidity: @humidity,
107
+ pressure: @pressure,
108
+ temp: @temp)
109
+ end
110
+ end
@@ -0,0 +1,177 @@
1
+ require 'ballistics/yaml'
2
+
3
+ class Ballistics::Cartridge
4
+ MANDATORY = {
5
+ "name" => :string,
6
+ "case" => :string,
7
+ "projectile" => :reference,
8
+ }
9
+ OPTIONAL = {
10
+ "desc" => :string,
11
+ "powder_grains" => :float,
12
+ "powder_type" => :string,
13
+ }
14
+
15
+ # used to guesstimate muzzle velocities for unknown barrel lengths
16
+ BURN_LENGTH = {
17
+ '300 BLK' => 9,
18
+ '5.56' => 20,
19
+ '.223' => 20,
20
+ '.308' => 24,
21
+ }
22
+
23
+ # Load a built-in YAML file and instantiate cartridge objects
24
+ # Return a hash of cartridge objects keyed by the cartridge id as in the YAML
25
+ #
26
+ def self.find(short_name)
27
+ objects = {}
28
+ Ballistics::YAML.load_built_in('cartridges', short_name).each { |id, hsh|
29
+ obj = self.new(hsh)
30
+ if block_given?
31
+ objects[id] = obj if yield obj
32
+ else
33
+ objects[id] = obj
34
+ end
35
+ }
36
+ objects
37
+ end
38
+
39
+ # This is a helper method to perform loading of cartridges and projectiles
40
+ # and to perform the cross ref. This works only with built in cartridge and
41
+ # projectile data. To do this for user-supplied data files, the user should
42
+ # perform the cross_ref explicitly
43
+ #
44
+ def self.find_with_projectiles(chamber)
45
+ require 'ballistics/projectile'
46
+
47
+ cartridges = self.find(chamber)
48
+ projectiles = Ballistics::Projectile.find(chamber)
49
+ self.cross_ref(cartridges, projectiles)
50
+ cartridges
51
+ end
52
+
53
+ # A cartridge object starts with a string identifier for its projectile
54
+ # Given a hash of cartridge objects (keyed by cartridge id)
55
+ # and a hash of projectile objects (keyed by projectile id)
56
+ # Set the cartridge's projectile to the projectile object identified
57
+ # by the string value
58
+ # The cartridge objects in the cartridges hash are updated in place
59
+ # The return value reports what was updated
60
+ #
61
+ def self.cross_ref(cartridges, projectiles)
62
+ retval = {}
63
+ cartridges.values.each { |c|
64
+ if c.projectile.is_a?(String)
65
+ proj_id = c.projectile
66
+ proj = projectiles[proj_id]
67
+ if proj
68
+ c.projectile = proj
69
+ retval[:updated] ||= []
70
+ retval[:updated] << proj_id
71
+ else
72
+ warn "#{proj_id} not found in projectiles"
73
+ end
74
+ else
75
+ warn "c.projectile is not a string"
76
+ end
77
+ }
78
+ retval
79
+ end
80
+
81
+ # Given a single data point (barrel_length, muzzle_velocity)
82
+ # and a known burn length
83
+ # Guess a muzzle velocity for an unknown length
84
+ #
85
+ def self.guess_mv(known_length, known_mv, burn_length, unknown_length)
86
+ inch_diff = known_length - unknown_length
87
+ known_bf = burn_length.to_f / known_length
88
+ unknown_bf = burn_length.to_f / unknown_length
89
+ # assume 1% FPS per inch; adjust for burn_length and take the average
90
+ fps_per_inch = known_mv * (known_bf + unknown_bf) / 2 / 100
91
+ known_mv - inch_diff * fps_per_inch
92
+ end
93
+
94
+ # Match and extract e.g. "16" from "16_inch_fps"
95
+ BARREL_LENGTH_REGEX = /([0-9]+)_inch_fps/i
96
+
97
+ attr_reader(*MANDATORY.keys)
98
+ attr_reader(*OPTIONAL.keys)
99
+ attr_reader :muzzle_velocity, :yaml_data, :extra
100
+ attr_writer :projectile
101
+
102
+ def initialize(hsh)
103
+ @yaml_data = hsh
104
+ MANDATORY.each { |field, type|
105
+ val = hsh.fetch(field)
106
+ Ballistics::YAML.check_type!(val, type)
107
+ self.instance_variable_set("@#{field}", val)
108
+ }
109
+
110
+ OPTIONAL.each { |field, type|
111
+ if hsh.key?(field)
112
+ val = hsh[field]
113
+ Ballistics::YAML.check_type!(val, type)
114
+ self.instance_variable_set("@#{field}", val)
115
+ end
116
+ }
117
+
118
+ # Keep track of fields that we don't expect
119
+ @extra = {}
120
+ (hsh.keys - MANDATORY.keys - OPTIONAL.keys).each { |k| @extra[k] = hsh[k] }
121
+
122
+ # Extract muzzle velocities per barrel length and remove from @extra
123
+ # We need at least one
124
+ #
125
+ @muzzle_velocity = {}
126
+ extracted = []
127
+ @extra.each { |key, val|
128
+ matches = key.match(BARREL_LENGTH_REGEX)
129
+ if matches
130
+ bl = matches[1].to_f.round
131
+ @muzzle_velocity[bl] = val
132
+ extracted << key
133
+ end
134
+ }
135
+ extracted.each { |k| @extra.delete(k) }
136
+ raise "no valid muzzle velocity" if @muzzle_velocity.empty?
137
+ end
138
+
139
+ # estimate muzzle velocity for a given barrel length
140
+ def mv(barrel_length, burn_length = nil)
141
+ [barrel_length, barrel_length.floor, barrel_length.ceil].each { |candidate|
142
+ mv = @muzzle_velocity[candidate]
143
+ return mv if mv
144
+ }
145
+ burn_length ||= BURN_LENGTH.fetch(@case)
146
+ known_lengths = @muzzle_velocity.keys
147
+
148
+ case known_lengths.length
149
+ when 0
150
+ raise "no muzzle velocities available"
151
+ when 1
152
+ known_length = known_lengths.first
153
+ self.class.guess_mv(known_length,
154
+ @muzzle_velocity[known_length],
155
+ burn_length,
156
+ barrel_length)
157
+ else
158
+ # ok, now we need to interpolate if we can
159
+ raise "not implemented yet"
160
+ end
161
+ end
162
+
163
+ def multiline
164
+ lines = ["CARTRIDGE: #{@name}", "========="]
165
+ fields = {
166
+ "Case" => @case,
167
+ }
168
+ @muzzle_velocity.keys.sort.each { |bar_len|
169
+ fields["MV @ #{bar_len}"] = @muzzle_velocity[bar_len]
170
+ }
171
+ fields["Desc"] = @desc if @desc
172
+ fields.each { |name, val|
173
+ lines << [name.rjust(9, ' '), val].join(': ')
174
+ }
175
+ lines.join("\n")
176
+ end
177
+ end
@@ -0,0 +1,159 @@
1
+ # MANDATORY
2
+ # $id:
3
+ # name:
4
+ # case:
5
+ # projectile: # ref to projectiles.yaml
6
+ # AT LEAST ONE OF
7
+ # 16_inch_fps:
8
+ # 20_inch_fps:
9
+ # 24_inch_fps:
10
+ # OPTIONAL
11
+ # desc:
12
+ # powder_type:
13
+ # powder_grains:
14
+ # $n_inch_fps:
15
+
16
+ # FLAT BASE PROJECTILES
17
+
18
+ barnes_90_range_ar:
19
+ name: Barnes 300 BLK 90gr Range AR (90gr OTFB)
20
+ case: 300 BLK
21
+ projectile: barnes_90_otfb
22
+ 16_inch_fps: 2550
23
+ desc: Target, training, competition
24
+
25
+ barnes_110_vor_tx:
26
+ name: Barnes 300 BLK 110gr VOR-TX
27
+ case: 300 BLK
28
+ projectile: barnes_110_tac_tx_300_blk
29
+ 16_inch_fps: 2350
30
+ 10_inch_fps: 2180
31
+ desc: Hunting round for penetration and expansion
32
+
33
+ hornady_110_v_max_black:
34
+ name: Hornady 300 BLK 110gr V-MAX BLACK
35
+ case: 300 BLK
36
+ projectile: hornady_110_v_max_black
37
+ 16_inch_fps: 2375
38
+ 10_inch_fps: 2250
39
+ desc: Varmint round for a black rifle
40
+
41
+ hpr_110_tac_tx:
42
+ name: HPR 300 BLK 110gr TAC-TX
43
+ case: 300 BLK
44
+ projectile: barnes_110_tac_tx_300_blk
45
+ 10_inch_fps: 2170
46
+ 16_inch_fps: 2300
47
+ desc: Accurate and consistent load
48
+
49
+ wilson_110_tac_tx:
50
+ name: Wilson Combat 300 BLK 110gr TAC-TX
51
+ case: 300 BLK
52
+ projectile: barnes_110_tac_tx_300_blk
53
+ 10_inch_fps: 2240
54
+ 16_inch_fps: 2400
55
+ desc: Fast, accurate, and consistent load
56
+
57
+ remington_120_umc_otfb:
58
+ name: Remington 300 BLK 120gr UMC OTFB
59
+ case: 300 BLK
60
+ projectile: remington_120_umc_otfb
61
+ 16_inch_fps: 2200
62
+ 11_inch_fps: 2080
63
+ 9_inch_fps: 2000
64
+ desc: Value ammo
65
+
66
+ hornady_125_hp:
67
+ name: Hornady 300 BLK 125gr HP American Gunner
68
+ case: 300 BLK
69
+ projectile: hornady_125_hp_match
70
+ 24_inch_fps: 2175
71
+ desc: Target, hunting, self defense
72
+
73
+ remington_125_premier_mkfb:
74
+ name: Remington Premier Match 300 BLK 125gr MatchKing Flat Base
75
+ case: 300 BLK
76
+ projectile: sierra_125_mk
77
+ 16_inch_fps: 2215
78
+ desc: Match ammo
79
+
80
+ hornady_135_ftx:
81
+ name: Hornady 300 BLK 135gr FTX
82
+ case: 300 BLK
83
+ projectile: hornady_135_ftx
84
+ 24_inch_fps: 2085
85
+ desc: Expanding round for hunting, personal defense
86
+
87
+ hornady_208_a_max_black:
88
+ name: Hornady 300 BLK 208gr A-MAX BLACK
89
+ case: 300 BLK
90
+ projectile: hornady_208_a_max_black
91
+ 16_inch_fps: 1020
92
+ 10_inch_fps: 915
93
+ desc: Optimized for accuracy in a black rifle
94
+
95
+ remington_220_umc_otfb:
96
+ name: Remington 300 BLK 220gr UMC OTFB
97
+ case: 300 BLK
98
+ projectile: remington_220_umc_otfb
99
+ 16_inch_fps: 1050
100
+ 10_inch_fps: 1005
101
+ desc: Value / training round
102
+
103
+ remington_220_high_perf_otfb:
104
+ name: Remington 300 BLK 220gr High Performance OTFB
105
+ case: 300 BLK
106
+ projectile: remington_220_otfb
107
+ 16_inch_fps: 1015
108
+ desc: Match style round
109
+
110
+ # BOAT TAIL PROJECTILES
111
+
112
+ hornady_110_gmx:
113
+ name: Hornady 300 BLK 110gr GMX Full Boar
114
+ case: 300 BLK
115
+ projectile: hornady_110_gmx
116
+ 24_inch_fps: 2350
117
+ desc: Penetrating hunting round
118
+
119
+ barnes_120_vor_tx:
120
+ name: Barnes 300 BLK 120gr VOR-TX
121
+ case: 300 BLK
122
+ projectile: barnes_120_tac_tx
123
+ 16_inch_fps: 2150
124
+ desc: Hunting round for penetration and expansion
125
+
126
+ remington_130_hog_hammer:
127
+ name: Remington 300 BLK 130gr Hog Hammer
128
+ case: 300 BLK
129
+ projectile: barnes_130_tsx
130
+ 24_inch_fps: 2075
131
+ desc: Hog hunting round for penetration and expansion
132
+
133
+ remington_130_htp:
134
+ name: Remington 300 BLK 130gr HTP
135
+ case: 300 BLK
136
+ projectile: barnes_130_tsx
137
+ 16_inch_fps: 2075
138
+ desc: Hunting round for penetration and expansion
139
+
140
+ remington_220_premier_mkbthp:
141
+ name: Remington 300 BLK 220gr Premier Match SMK
142
+ case: 300 BLK
143
+ projectile: sierra_220_hpbt_mk
144
+ 16_inch_fps: 1015
145
+ desc: Match round
146
+
147
+ silencerco_220_smk:
148
+ name: SilencerCo 300 BLK 220gr Sierra MatchKing
149
+ case: 300 BLK
150
+ projectile: sierra_220_hpbt_mk
151
+ 16_inch_fps: 1030
152
+ desc: Match round
153
+
154
+ wilson_220_smk:
155
+ name: Wilson 300 BLK 220gr Sierra MatchKing
156
+ case: 300 BLK
157
+ projectile: sierra_220_hpbt_mk
158
+ 16_inch_fps: 1025
159
+ desc: Match round with factory new Remington 300 BLK case