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.
- checksums.yaml +7 -0
- data/README.md +12 -0
- data/Rakefile +51 -0
- data/examples/table.rb +10 -0
- data/ext/ballistics/ext.c +136 -0
- data/ext/ballistics/extconf.rb +3 -0
- data/ext/ballistics/gnu_ballistics.h +577 -0
- data/lib/ballistics.rb +77 -0
- data/lib/ballistics/atmosphere.rb +110 -0
- data/lib/ballistics/cartridge.rb +177 -0
- data/lib/ballistics/cartridges/300_blk.yaml +159 -0
- data/lib/ballistics/gun.rb +123 -0
- data/lib/ballistics/guns/pistols.yaml +6 -0
- data/lib/ballistics/guns/rifles.yaml +34 -0
- data/lib/ballistics/guns/shotguns.yaml +6 -0
- data/lib/ballistics/problem.rb +102 -0
- data/lib/ballistics/projectile.rb +165 -0
- data/lib/ballistics/projectiles/300_blk.yaml +321 -0
- data/lib/ballistics/util.rb +67 -0
- data/lib/ballistics/yaml.rb +57 -0
- data/test/atmosphere.rb +26 -0
- data/test/ballistics.rb +31 -0
- data/test/cartridge.rb +121 -0
- data/test/gun.rb +85 -0
- data/test/problem.rb +137 -0
- data/test/projectile.rb +163 -0
- data/test/util.rb +28 -0
- metadata +102 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
require 'bigdecimal/util'
|
3
|
+
|
4
|
+
module Ballistics
|
5
|
+
# weight in grains
|
6
|
+
# velocity in ft / s
|
7
|
+
# acceleration in ft / s^2
|
8
|
+
# caliber in inches
|
9
|
+
|
10
|
+
GRAINS_PER_LB = 7000.to_d
|
11
|
+
G = 32.175.to_d # gravitational acceleration
|
12
|
+
|
13
|
+
def self.lbs(grains)
|
14
|
+
grains.to_d / GRAINS_PER_LB
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.grains(lbs)
|
18
|
+
lbs.to_d * GRAINS_PER_LB
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.mass(weight)
|
22
|
+
weight.to_d / G
|
23
|
+
end
|
24
|
+
|
25
|
+
# Sectional density, according to the SpeerReloading Manual No. 13,
|
26
|
+
# is defined as: "A bullet's weight in pounds divided by the square of
|
27
|
+
# its diameter in inches." Note that SD is independent of a bullet's
|
28
|
+
# shape. All bullets of the same caliber and weight will have the same
|
29
|
+
# SD, regardless of their shape or composition.
|
30
|
+
#
|
31
|
+
def self.sectional_density(weight, caliber)
|
32
|
+
self.lbs(weight) / caliber.to_d ** 2
|
33
|
+
end
|
34
|
+
|
35
|
+
# 1/2 m v^2
|
36
|
+
#
|
37
|
+
def self.kinetic_energy(velocity, weight)
|
38
|
+
self.mass(self.lbs(weight)) * velocity.to_d ** 2 / 2
|
39
|
+
end
|
40
|
+
|
41
|
+
# http://www.chuckhawks.com/taylor_KO_factor.htm
|
42
|
+
# tl;dr don't use this
|
43
|
+
#
|
44
|
+
def self.taylor_ko(velocity, weight, caliber)
|
45
|
+
self.lbs(weight) * velocity.to_d * caliber.to_d
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.recoil_impulse(proj_weight, charge_weight, proj_v, charge_v=4000)
|
49
|
+
self.mass(self.lbs(proj_weight.to_d * proj_v.to_d +
|
50
|
+
charge_weight.to_d * charge_v.to_d))
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.free_recoil(proj_weight, charge_weight,
|
54
|
+
proj_v, gun_lbs, charge_v=4000)
|
55
|
+
self.recoil_impulse(proj_weight, charge_weight, proj_v, charge_v) /
|
56
|
+
self.mass(gun_lbs)
|
57
|
+
end
|
58
|
+
|
59
|
+
# 1/2 m(gun) * fr^2
|
60
|
+
#
|
61
|
+
def self.recoil_energy(proj_weight, charge_weight,
|
62
|
+
proj_v, gun_lbs, charge_v=4000)
|
63
|
+
fr = self.free_recoil(proj_weight, charge_weight,
|
64
|
+
proj_v, gun_lbs, charge_v)
|
65
|
+
self.mass(gun_lbs) * fr ** 2 / 2
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
# We don't depend on the Ballistics module, so it may not have been loaded yet
|
4
|
+
module Ballistics; end
|
5
|
+
|
6
|
+
module Ballistics::YAML
|
7
|
+
class UnknownType < RuntimeError; end
|
8
|
+
class TypeMismatch < RuntimeError; end
|
9
|
+
class LoadError < RuntimeError; end
|
10
|
+
class MandatoryFieldError; end
|
11
|
+
|
12
|
+
# Return a hash keyed by subdir with array values
|
13
|
+
# Array contains short names for the subdir's yaml files
|
14
|
+
# e.g. { 'cartridges' => ['300_blk'] }
|
15
|
+
#
|
16
|
+
BUILT_IN = {}
|
17
|
+
Dir[File.join(__dir__, '*')].each { |fn|
|
18
|
+
if File.directory? fn
|
19
|
+
yaml_files = Dir[File.join(fn, '*.yaml')]
|
20
|
+
if !yaml_files.empty?
|
21
|
+
BUILT_IN[File.basename fn] =
|
22
|
+
yaml_files.map { |y| File.basename(y, '.yaml') }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
}
|
26
|
+
|
27
|
+
def self.load_built_in(dir, short_name)
|
28
|
+
files = BUILT_IN[dir] or raise(LoadError, "unknown dir: #{dir}")
|
29
|
+
filename = [short_name, 'yaml'].join('.')
|
30
|
+
if files.include?(short_name)
|
31
|
+
::YAML.load_file(File.join(__dir__, dir, filename))
|
32
|
+
else
|
33
|
+
raise(LoadError, "unknown short name: #{short_name}")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.check_type?(val, type)
|
38
|
+
case type
|
39
|
+
when :string, :reference
|
40
|
+
val.is_a?(String)
|
41
|
+
when :float
|
42
|
+
val.is_a?(Numeric)
|
43
|
+
when :percent
|
44
|
+
val.is_a?(Numeric) and val >= 0 and val <= 1
|
45
|
+
when :count
|
46
|
+
val.is_a?(1.class) and val >= 0
|
47
|
+
when :int
|
48
|
+
val.is_a?(1.class)
|
49
|
+
else
|
50
|
+
raise UnknownType, type
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.check_type!(val, type)
|
55
|
+
self.check_type?(val, type) or raise(TypeMismatch, [val, type].join(' '))
|
56
|
+
end
|
57
|
+
end
|
data/test/atmosphere.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'ballistics/atmosphere'
|
3
|
+
|
4
|
+
include Ballistics
|
5
|
+
|
6
|
+
describe Atmosphere do
|
7
|
+
before do
|
8
|
+
@altitude = 5430
|
9
|
+
@humidity = 0.48
|
10
|
+
@pressure = 29.93
|
11
|
+
@temp = 40
|
12
|
+
end
|
13
|
+
|
14
|
+
it "translates a ballistic coefficient" do
|
15
|
+
Atmosphere.translate(0.338,
|
16
|
+
altitude: @altitude,
|
17
|
+
humidity: @humidity,
|
18
|
+
pressure: @pressure,
|
19
|
+
temp: @temp).round(3).must_equal 0.392
|
20
|
+
|
21
|
+
Atmosphere.new("altitude" => @altitude,
|
22
|
+
"humidity" => @humidity,
|
23
|
+
"pressure" => @pressure,
|
24
|
+
"temp" => @temp).translate(0.338).round(3).must_equal 0.392
|
25
|
+
end
|
26
|
+
end
|
data/test/ballistics.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'ballistics'
|
3
|
+
|
4
|
+
describe Ballistics do
|
5
|
+
it "calculates the expected zero angle" do
|
6
|
+
opts = {
|
7
|
+
drag_number: 1,
|
8
|
+
ballistic_coefficient: 0.5,
|
9
|
+
velocity: 1200,
|
10
|
+
sight_height: 1.6,
|
11
|
+
zero_range: 100,
|
12
|
+
}
|
13
|
+
Ballistics.zero_angle(opts).round(6).must_equal 0.227188
|
14
|
+
end
|
15
|
+
|
16
|
+
it "calculates the expected trajectory" do
|
17
|
+
opts = {
|
18
|
+
drag_number: 1,
|
19
|
+
ballistic_coefficient: 0.5,
|
20
|
+
velocity: 2250,
|
21
|
+
sight_height: 2.6,
|
22
|
+
zero_range: 50,
|
23
|
+
wind_speed: 10,
|
24
|
+
wind_angle: 90,
|
25
|
+
max_range: 1000,
|
26
|
+
interval: 25,
|
27
|
+
shooting_angle: 0,
|
28
|
+
}
|
29
|
+
Ballistics.trajectory(opts).must_be_kind_of Array
|
30
|
+
end
|
31
|
+
end
|
data/test/cartridge.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'ballistics/cartridge'
|
3
|
+
|
4
|
+
include Ballistics
|
5
|
+
|
6
|
+
describe Cartridge do
|
7
|
+
before do
|
8
|
+
@test_data = {
|
9
|
+
"name" => "Test Cartridge",
|
10
|
+
"case" => "test case",
|
11
|
+
"projectile" => "test_projectile",
|
12
|
+
"20_inch_fps" => 2000,
|
13
|
+
"desc" => "Test Cartridge for test purposes",
|
14
|
+
}
|
15
|
+
@extra_data = {
|
16
|
+
"foo" => "bar",
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "BARREL_LENGTH_REGEX" do
|
21
|
+
it "must match and extract barrel lengths" do
|
22
|
+
["16_inch_fps", "1_inch_fps", "21_inch_fps"].each { |valid|
|
23
|
+
rgx = Cartridge::BARREL_LENGTH_REGEX
|
24
|
+
matches = rgx.match valid
|
25
|
+
matches.wont_be_nil
|
26
|
+
matches.captures.wont_be_empty
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "new instance" do
|
32
|
+
before do
|
33
|
+
@cart = Cartridge.new(@test_data)
|
34
|
+
@cart_ex = Cartridge.new(@test_data.merge(@extra_data))
|
35
|
+
end
|
36
|
+
|
37
|
+
it "must raise with insufficient parameters" do
|
38
|
+
params = {}
|
39
|
+
proc { Cartridge.new params }.must_raise Exception
|
40
|
+
# Accumulate the mandatory fields in params
|
41
|
+
# Note, a field matching Cartridge::BARREL_LENGTH_REGEX is also mandatory
|
42
|
+
Cartridge::MANDATORY.keys.each { |mfield|
|
43
|
+
params[mfield] = @test_data[mfield]
|
44
|
+
proc { Cartridge.new params }.must_raise Exception
|
45
|
+
}
|
46
|
+
mv = { "15_inch_fps" => 15 }
|
47
|
+
proc { Cartridge.new mv }.must_raise Exception
|
48
|
+
Cartridge.new(params.merge(mv)).must_be_kind_of Cartridge
|
49
|
+
end
|
50
|
+
|
51
|
+
it "must have a name" do
|
52
|
+
@cart.name.wont_be_nil
|
53
|
+
@cart.name.must_equal @test_data["name"]
|
54
|
+
end
|
55
|
+
|
56
|
+
it "must have a case" do
|
57
|
+
@cart.case.wont_be_nil
|
58
|
+
@cart.case.must_equal @test_data["case"]
|
59
|
+
end
|
60
|
+
|
61
|
+
it "must have a projectile" do
|
62
|
+
@cart.projectile.wont_be_nil
|
63
|
+
@cart.projectile.must_equal @test_data["projectile"]
|
64
|
+
end
|
65
|
+
|
66
|
+
it "must have a muzzle velocity" do
|
67
|
+
mv = @cart.muzzle_velocity
|
68
|
+
mv.wont_be_nil
|
69
|
+
mv.must_be_kind_of Hash
|
70
|
+
mv.wont_be_empty
|
71
|
+
["16", "20"].each { |barrel_length|
|
72
|
+
test_mv = @test_data[barrel_length]
|
73
|
+
if test_mv
|
74
|
+
mv[barrel_length].must_equal test_mv
|
75
|
+
else
|
76
|
+
mv[barrel_length].must_be_nil
|
77
|
+
end
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
it "must accept optional fields" do
|
82
|
+
Cartridge::OPTIONAL.keys.each { |k|
|
83
|
+
if @test_data.key? k
|
84
|
+
@cart.send(k).must_equal @test_data[k]
|
85
|
+
else
|
86
|
+
@cart.send(k).must_be_nil
|
87
|
+
end
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
it "must retain initializing data" do
|
92
|
+
@cart.yaml_data.must_equal @test_data
|
93
|
+
@cart_ex.yaml_data.must_equal @test_data.merge(@extra_data)
|
94
|
+
end
|
95
|
+
|
96
|
+
it "must retain extra data" do
|
97
|
+
@cart.extra.must_be_empty
|
98
|
+
@cart_ex.extra.wont_be_empty
|
99
|
+
@cart_ex.extra.must_equal @extra_data
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "muzzle velocity" do
|
104
|
+
before do
|
105
|
+
# we need a valid case -- e.g. 300 BLK
|
106
|
+
@cart = Cartridge.new(@test_data.merge("case" => "300 BLK"))
|
107
|
+
end
|
108
|
+
|
109
|
+
it "must estimate an unknown muzzle velocity" do
|
110
|
+
# sanity checks
|
111
|
+
Cartridge::BURN_LENGTH[@cart.case].wont_be_nil
|
112
|
+
@test_data["20_inch_fps"].wont_be_nil
|
113
|
+
@test_data["19_inch_fps"].must_be_nil
|
114
|
+
@test_data["21_inch_fps"].must_be_nil
|
115
|
+
|
116
|
+
@cart.mv(20).must_equal @test_data["20_inch_fps"]
|
117
|
+
@cart.mv(20.9).must_equal @test_data["20_inch_fps"]
|
118
|
+
@cart.mv(19.1).must_equal @test_data["20_inch_fps"]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
data/test/gun.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'ballistics/gun'
|
3
|
+
|
4
|
+
include Ballistics
|
5
|
+
|
6
|
+
describe Gun do
|
7
|
+
before do
|
8
|
+
@test_data = {
|
9
|
+
"name" => "Test Gun",
|
10
|
+
"chamber" => "test chamber",
|
11
|
+
"barrel_length" => 16,
|
12
|
+
"sight_height" => 1.5,
|
13
|
+
"zero_range" => 100,
|
14
|
+
}
|
15
|
+
|
16
|
+
@extra_data = {
|
17
|
+
"foo" => "bar",
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "new instance" do
|
22
|
+
before do
|
23
|
+
@gun = Gun.new(@test_data)
|
24
|
+
@gun_ex = Gun.new(@test_data.merge(@extra_data))
|
25
|
+
end
|
26
|
+
|
27
|
+
it "must have a name" do
|
28
|
+
@gun.name.wont_be_nil
|
29
|
+
@gun.name.must_equal @test_data["name"]
|
30
|
+
end
|
31
|
+
|
32
|
+
it "must have a chamber" do
|
33
|
+
@gun.chamber.wont_be_nil
|
34
|
+
@gun.chamber.must_equal @test_data["chamber"]
|
35
|
+
end
|
36
|
+
|
37
|
+
it "must have a barrel_length" do
|
38
|
+
@gun.barrel_length.wont_be_nil
|
39
|
+
@gun.barrel_length.must_equal @test_data["barrel_length"]
|
40
|
+
end
|
41
|
+
|
42
|
+
it "must have a sight_height" do
|
43
|
+
@gun.sight_height.wont_be_nil
|
44
|
+
@gun.sight_height.must_equal @test_data["sight_height"]
|
45
|
+
end
|
46
|
+
|
47
|
+
it "must accept optional fields" do
|
48
|
+
Gun::OPTIONAL.keys.each { |k|
|
49
|
+
if @test_data.key?(k)
|
50
|
+
@gun.send(k).must_equal @test_data[k]
|
51
|
+
else
|
52
|
+
@gun.send(k).must_be_nil
|
53
|
+
end
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
it "must retain initializing data" do
|
58
|
+
@gun.yaml_data.must_equal @test_data
|
59
|
+
@gun_ex.yaml_data.must_equal @test_data.merge(@extra_data)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "must retain extra data" do
|
63
|
+
@gun.extra.must_be_empty
|
64
|
+
@gun_ex.extra.wont_be_empty
|
65
|
+
@gun_ex.extra.must_equal @extra_data
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "cartridges" do
|
70
|
+
it "must recognize known cartridges" do
|
71
|
+
["300 BLK"].each { |valid|
|
72
|
+
# sanity check
|
73
|
+
Gun::CHAMBER_CARTRIDGE.key?(valid).must_equal true
|
74
|
+
gun = Gun.new @test_data.merge("chamber" => valid)
|
75
|
+
gun.cartridges.must_be_kind_of Hash
|
76
|
+
}
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
it "must reject an unknown cartridge" do
|
81
|
+
invalid = { "chamber" => "501 Levis" }
|
82
|
+
gun = Gun.new @test_data.merge(invalid)
|
83
|
+
proc { gun.cartridges }.must_raise Gun::ChamberNotFound
|
84
|
+
end
|
85
|
+
end
|
data/test/problem.rb
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'ballistics/problem'
|
3
|
+
|
4
|
+
include Ballistics
|
5
|
+
|
6
|
+
describe Problem do
|
7
|
+
describe "enrich" do
|
8
|
+
it "tolerates empty input" do
|
9
|
+
hsh = Problem.new.enrich
|
10
|
+
hsh.wont_be_empty
|
11
|
+
hsh.must_equal Problem::DEFAULTS
|
12
|
+
end
|
13
|
+
|
14
|
+
it "infers projectile params" do
|
15
|
+
prj = Projectile.new("name" => "test proj",
|
16
|
+
"cal" => 0.223,
|
17
|
+
"grains" => 100,
|
18
|
+
"g7" => 0.445)
|
19
|
+
hsh = Problem.new(projectile: prj).enrich
|
20
|
+
hsh.wont_be_empty
|
21
|
+
prj.params.each { |sym, val| hsh[sym].must_equal val }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "trajectory" do
|
26
|
+
@problem = Problem.new(atmosphere: Atmosphere.new("altitude" => 5430,
|
27
|
+
"humidity" => 0.48,
|
28
|
+
"pressure" => 29.93,
|
29
|
+
"temp" => 40))
|
30
|
+
@opts = {
|
31
|
+
drag_number: 1,
|
32
|
+
ballistic_coefficient: 0.5,
|
33
|
+
velocity: 2850,
|
34
|
+
sight_height: 1.6,
|
35
|
+
wind_speed: 10,
|
36
|
+
wind_angle: 90,
|
37
|
+
zero_range: 200,
|
38
|
+
max_range: 1000,
|
39
|
+
interval: 25,
|
40
|
+
}
|
41
|
+
|
42
|
+
def self.trajectory
|
43
|
+
@trajectory ||= @problem.trajectory(@opts)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "raises with insufficient params" do
|
47
|
+
proc { Problem.trajectory(drag_function: 'G1',
|
48
|
+
drag_number: 1,
|
49
|
+
ballistic_coefficient: 0.5,
|
50
|
+
velocity: 2850) }.must_raise Exception
|
51
|
+
end
|
52
|
+
|
53
|
+
it "has the expected range" do
|
54
|
+
t = self.class.trajectory
|
55
|
+
Hash[2 => 50,
|
56
|
+
4 => 100,
|
57
|
+
8 => 200,
|
58
|
+
12 => 300,
|
59
|
+
16 => 400,
|
60
|
+
20 => 500,
|
61
|
+
40 => 1000,
|
62
|
+
].each { |i, val|
|
63
|
+
t[i]['range'].must_equal val
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
it "has the expected path" do
|
68
|
+
t = self.class.trajectory
|
69
|
+
Hash[2 => 0.6,
|
70
|
+
4 => 1.6,
|
71
|
+
8 => 0,
|
72
|
+
12 => -7,
|
73
|
+
16 => -20.1,
|
74
|
+
20 => -40.1,
|
75
|
+
40 => -282.5,
|
76
|
+
].each { |i, val|
|
77
|
+
t[i]['path'].round(1).must_equal val
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
it "has the expected velocity" do
|
82
|
+
t = self.class.trajectory
|
83
|
+
Hash[2 => 2769,
|
84
|
+
4 => 2691,
|
85
|
+
8 => 2538,
|
86
|
+
12 => 2390,
|
87
|
+
16 => 2246,
|
88
|
+
20 => 2108,
|
89
|
+
40 => 1500,
|
90
|
+
].each { |i, val|
|
91
|
+
t[i]['velocity'].to_i.must_equal val
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
it "has the expected MOA correction" do
|
96
|
+
t = self.class.trajectory
|
97
|
+
Hash[2 => -1.1,
|
98
|
+
4 => -1.5,
|
99
|
+
8 => 0,
|
100
|
+
12 => 2.2,
|
101
|
+
16 => 4.8,
|
102
|
+
20 => 7.7,
|
103
|
+
40 => 27,
|
104
|
+
].each { |i, val|
|
105
|
+
t[i]['moa_correction'].round(1).must_equal val
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
it "has the expected windage" do
|
110
|
+
t = self.class.trajectory
|
111
|
+
Hash[2 => 0.2,
|
112
|
+
4 => 0.6,
|
113
|
+
8 => 2.3,
|
114
|
+
12 => 5.2,
|
115
|
+
16 => 9.4,
|
116
|
+
20 => 15.2,
|
117
|
+
40 => 71.3,
|
118
|
+
].each { |i, val|
|
119
|
+
t[i]['windage'].round(1).must_equal val
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
it "has the expected MOA correction for windage" do
|
124
|
+
t = self.class.trajectory
|
125
|
+
Hash[2 => 0.3,
|
126
|
+
4 => 0.5,
|
127
|
+
8 => 1.1,
|
128
|
+
12 => 1.6,
|
129
|
+
16 => 2.3,
|
130
|
+
20 => 2.9,
|
131
|
+
40 => 6.8,
|
132
|
+
].each { |i, val|
|
133
|
+
t[i]['moa_windage'].round(1).must_equal val
|
134
|
+
}
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|