ballistics-ng 0.1.0.1

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