File Coverage

lib/Physics/Ballistics/Internal.pm
Criterion Covered Total %
statement 162 185 87.5
branch 29 58 50.0
condition 3 7 42.8
subroutine 19 19 100.0
pod 8 15 53.3
total 221 284 77.8


line stmt bran cond sub pod time code
1             package Physics::Ballistics::Internal;
2 1     1   153406 use strict;
  1         4  
  1         32  
3 1     1   7 use warnings;
  1         3  
  1         92  
4             require Exporter;
5             our @ISA = qw(Exporter);
6             our @EXPORT = qw(cartridge_capacity empty_brass gunfire powley recoil_mbt cup2psi cup2psi_linear ogival_volume recoil_mbt);
7             our $VERSION = '1.03';
8              
9 1     1   348 use Physics::Ballistics;
  1         6  
  1         48  
10 1     1   13 use Math::Trig;
  1         3  
  1         4129  
11              
12             =head1 NAME
13              
14             Physics::Ballistics::Internal -- Various internal ballistics formulae.
15              
16             =cut
17              
18             =head1 ABSTRACT
19              
20             Internal ballistics is the study of what happens while a projectile is being
21             launched from a barrel, from the time the propellant ignites to the moment
22             after the projectile leaves the muzzle.
23              
24             This module implements a variety of functions and mathematical formulae useful
25             in the analysis and prediction of internal ballistic effects.
26              
27             It also branches out somewhat into the closely-related matters of bullet and
28             cartridge composition and characteristics.
29              
30             =head1 REGARDING BULLET DIAMETERS
31              
32             Some of these functions require the diameter of a projectile as a parameter.
33             Please note that bullet diameters are usually different from the names of
34             their calibers. NATO 5.56mm bullets are actually 5.70mm in diameter, while
35             Russian 5.45mm bullets are actually 5.62mm. .308 caliber bullets really are
36             0.308 inches in diameter (7.82mm), but .22 Long Rifle bullets are 0.222
37             inches across.
38              
39             Making assumptions can hurt! It's better to look it up before plugging it in.
40              
41             =head1 ANNOTATIONS OF SOURCES
42              
43             Regarding their source, these functions fall into three categories: Some are
44             simple encodings of basic physics (like energy = 1/2 * mass * velocity**2),
45             and these will not be cited. Others are from published works, such as books
46             or trade journals, and these will be cited when possible. A few are products
47             of my own efforts, and will be thus annotated.
48              
49             =head1 OOP INTERFACE
50              
51             A more integrated, object-oriented interface for these functions is under
52             development.
53              
54             =head1 CONSTANTS
55              
56             =head2 %CASE_CAPACITY_GRAINS
57              
58             This hash table maps the names of various cartridges to their volumes, in grains of water.
59              
60             It is based on the table at: http://kwk.us/cases.html
61              
62             Powder capacities will vary considerably depending on powder type, but multiplying water capacity by 0.85 comes pretty close for most powder types.
63              
64             =cut
65              
66             # Actual, measured case capacities, in grains of water, based on http://kwk.us/cases.html
67             # Powder capacities will vary considerably depending on powder type, but multiplying by 0.85 comes pretty close for most powder types.
68             our %CASE_CAPACITY_GRAINS = (
69             '.14 Hornet' => 12,
70             '.17 Hornet' => 14,
71             '.17 Remington' => 27,
72             '.204 Ruger' => 33,
73             '.22 Hornet' => 14,
74             '.22 Hornet Impr' => 16,
75             '.218 Bee' => 18,
76             '.22 Remington Jet' => 18,
77             '.221 Remington' => 21,
78             '.222 Remington' => 27,
79             '.223 Remington' => 31,
80             '5.56x45mm NATO' => 31,
81             '.222 Remington Magnum' => 32,
82             '5.6x50R' => 34,
83             '.219 Zipper' => 34,
84             '.225 Winchester' => 41,
85             '.22-250 Remington' => 43,
86             '.220 Swift' => 48,
87             '.223 WSSM' => 53,
88             '.22-06' => 65,
89             '.22-15 Stevens' => 17,
90             '.22 Sav' => 35,
91             '6x47' => 33,
92             '6x52R Bret' => 36,
93             '6 BR' => 39,
94             '6x70R' => 39,
95             '.243 Winchester' => 54,
96             '.243 WSSM' => 54,
97             '6 Remington' => 55,
98             '6 USN' => 51,
99             '.240 Weatherby Magnum' => 65,
100             '6x62R' => 67,
101             '.240 Fl NE' => 58,
102             '.25-20 WCF' => 19,
103             '.256 Winchester' => 22,
104             '.25-21 Stevens' => 25,
105             '.25-25 Stevens' => 29,
106             '.25-36 Marlin' => 37,
107             '.25-35 WCF' => 37,
108             '.25 Remington' => 42,
109             '.250 Sav' => 46,
110             '.257 Roberts' => 56,
111             '.25-06 Remington' => 66,
112             '.257 Weatherby Magnum' => 84,
113             '6.5x70R' => 39,
114             '6.5J' => 48,
115             '6.5x52 Carcano' => 49,
116             '6.5x53R' => 49,
117             '6.5x54 MS .256' => 50,
118             '.260 Remington' => 53,
119             '6.5x55' => 57,
120             '6.5x57R' => 58,
121             '6.5 Grendel' => 35,
122             '6.5 Remington Magnum' => 68,
123             '.264 Winchester Magnum' => 82,
124             '.270 REN' => 16,
125             '.270 Winchester' => 68,
126             '.270 Weatherby' => 83,
127             '.28-30 Stevens' => 37,
128             '7-30 Waters' => 45,
129             '7x72R' => 54,
130             '7-08 Remington' => 56,
131             '7x57R Mauser' => 59,
132             '.284 Winchester' => 66,
133             '.280 Remington' => 67,
134             '7x65R' => 68,
135             '7 WSM' => 81,
136             '7 Remington Magnum' => 84,
137             '.30 Carbine' => 21,
138             '.30-357 AeT' => 25,
139             '.30 Dasher' => 43,
140             '.30 Remington AR' => 44,
141             '.30-30' => 45,
142             '.30 Remington' => 46,
143             '.303 Sav' => 48,
144             '.300 Sav' => 52,
145             '.307 Winchester' => 54,
146             '7.62x51mm NATO' => 54,
147             '.308 Winchester' => 56,
148             '.30 Fl.NE Purdey' => 58,
149             '.30-40 US' => 58,
150             '.30-06 US' => 69,
151             '.300 HH' => 86,
152             '.300 Winchester Magnum' => 89,
153             '.30 Fl HH' => 90,
154             '.300 Weatherby Magnum' => 99,
155             '.30-378' => 130,
156             '7.62x39mm' => 35,
157             '7.62x54R' => 64,
158             '.303 Brit' => 57,
159             '.375/303 WR' => 62,
160             '.32-20 WCF' => 22,
161             '7.65 Mauser' => 58,
162             '8x72R' => 59,
163             '32-40 Ballard' => 41,
164             '8x50R Lebel' => 66,
165             '8x57R Mauser' => 62,
166             '8-06' => 70,
167             '8 Remington Magnum' => 98,
168             '.318 WR' => 69,
169             '.333 Jeffery' => 86,
170             '.33 WCF' => 63,
171             '.338-06' => 70,
172             '.338 Winchester Magnum' => 86,
173             '.340 Weatherby Magnum' => 98,
174             '.338 Laupa Magnum' => 114,
175             '.338-378' => 132,
176             '.348 Winchester' => 75,
177             '9x57R Mauser' => 62,
178             '.357 Magnum' => 27,
179             '.357 Max' => 34,
180             '.357/44 BD' => 35,
181             '.400/350 Rigby' => 78,
182             '.350 ME Guide 2' => 49,
183             '.35 Remington' => 51,
184             '.356 Winchester' => 57,
185             '.358 Winchester' => 57,
186             '.35 WCF' => 69,
187             '.35 Whelen' => 71,
188             '.35 Greevy' => 72,
189             '.350 Remington Magnum' => 73,
190             '.358 Norma Magnum' => 88,
191             '.358 STA' => 105,
192             '9.3x57 Mauser' => 64,
193             '9.3x54R Finn' => 65,
194             '9.3x72R' => 67,
195             '9.3x62' => 77,
196             '9.3x74R' => 82,
197             '.360 No2 NE' => 111,
198             '.375 Winchester' => 49,
199             '.38-56 Winchester' => 62,
200             '.375 2.5 NE' => 67,
201             '.375-06' => 73,
202             '.375 HH' => 95,
203             '.375 Fl Magnum' => 97,
204             '.375 Ruger' => 100,
205             '.369 NE' => 102,
206             '.378 Weatherby Magnum' => 136,
207             '.38-55 Ballard' => 52,
208             '.38-72 Winchester' => 74,
209             '.38-40 WCF' => 39,
210             '.400 Whelen' => 75,
211             '.405 Winchester' => 78,
212             '.400 Jeffery' => 117,
213             '.450/400 NE 3.25' => 123,
214             '.416 Taylor' => 92,
215             '.416 Remington Magnum' => 107,
216             '.416 Rigby' => 130,
217             '.416 Weatherby Magnum' => 134,
218             '.423 OKH' => 77,
219             '.404 Jeffery' => 113,
220             '.44-40 WCF' => 40,
221             '.44 Spl' => 34,
222             '.44 Remington Magnum' => 39,
223             '.444 Marlin' => 69,
224             '.45 Colt' => 42,
225             '.454 Casull' => 47,
226             '.45-70 US' => 79,
227             '.450 Marlin' => 74,
228             '.45-90 2.4"' => 90,
229             '.458 Winchester Magnum' => 94,
230             '.458 Lott' => 108,
231             '.450 3.25 NE' => 129,
232             '.460 Weatherby Magnum' => 140,
233             '.465 NE' => 144,
234             '.470 NE' => 146,
235             '.475 3.25 NE' => 137,
236             '.50-110' => 109,
237             '.50 BMG' => 293,
238             '12.7x99mm NATO' => 293 # Same cartridge as '.50 BMG'
239             );
240              
241             =head1 FUNCTIONS
242              
243             =head2 cartridge_capacity (bullet_diameter_mm, base_diameter_mm, case_len_mm, psi, [want_powder_bool])
244              
245             This function estimates the internal capacity (volume) of a rifle cartridge, assuming typical brass material construction.
246              
247             It is not very accurate (+/- 6%) and actual volumes will vary between different manufacturers of the same cartridge anyway.
248              
249             Its main advantage over a Powley calculator is ease of use, as it requires fewer and easier-to-obtain parameters.
250              
251             Unlike the Powley calculator, it is only valid for typically-tapered rifle casings for calibers in the range of about 5mm to 14mm.
252              
253             This function is the original work of the author.
254              
255             DISCLAIMER: Do not use this as a guide for handloading! Get a Speer Reloading Manual or similar. If you must use this function, start at least 15% lower than the function indicates and work your way up slowly. Author is not responsible for idiots blasting copper case-bits into their own faces.
256              
257             =over 4
258              
259             parameter: (float) bullet_diameter_mm is the width of the bullet (in mm)
260              
261             parameter: (float) base_diameter_mm is the width of the case at its base, NOT its rim (in mm)
262              
263             parameter: (float) case_len_mm is the overall length of the case, including rim and neck (in mm)
264              
265             parameter: (float) psi is the peak pressure tolerated by the cartridge (in psi)
266              
267             parameter: (boolean) OPTIONAL: set want_powder_bool to a True value to approximate powder capacity instead of water capacity. This is intrinsically inaccurate, since different powders have different weights, but it does okay for typical powders.
268              
269             returns: (int) cartridge capacity (in grains)
270              
271             =back
272              
273             =cut
274              
275             # Estimate the internal capacity (volume) of a rifle cartridge, assuming brass material.
276             # NOTES:
277             # Not very accurate (+/- 6%), will vary between manufacturers
278             # Only valid for tapered rifle casings for calibers around 5mm to 14mm.
279             #
280             sub cartridge_capacity {
281 3     3 1 1613 my ($bullet_diameter_mm, $base_diameter_mm, $case_len_mm, $psi, $want_powder_bool) = @_;
282 3 50       12 die("mass_grains = cartridge_capacity(bullet_diameter_mm, base_diameter_mm, case_len_mm, max_pressure_psi[, want_powder_not_water])") unless(defined($psi));
283 3         24 my $ff = ($bullet_diameter_mm / 5.56)**0.0585;
284 3         16 my $capacity = $base_diameter_mm**2 * $case_len_mm * $psi * $ff / 8240000; # capacity in water
285 3 50       8 $capacity *= 0.85 if ($want_powder_bool); # converts to approx capacity in powder. "safe" load is about 5% lower than this.
286 3         20 return int($capacity + 0.5);
287             }
288              
289             =head2 empty_brass (bullet_diameter_mm, base_diameter_mm, case_len_mm, psi)
290              
291             This function estimates the weight of an empty rifle cartridge, assuming typical brass material construction. This is just the weight of the case metal, without primer, bullet, or powder.
292              
293             It is moderately accurate, with the occasional winger, but actual weights will vary between different manufacturers of the same cartridge anyway.
294              
295             Its main advantage over a Powley calculator is ease of use, as it requires fewer and easier-to-obtain parameters.
296              
297             Unlike the Powley calculator, it is only valid for reasonably-tapered rifle casings for calibers in the range of about 5mm to 14mm.
298              
299             This function is the original work of the author.
300              
301             Compare to known dimensions and weights:
302              
303             5.56x45mm: 95 gr, empty_brass predicts 95 = 1.000 actual, 0.0% error
304             7.62x39mm: 100 gr, empty_brass predicts 105 = 1.050 actual, 5.0% error
305             6.8mm SPC: 107 gr, empty_brass predicts 100 = 0.935 actual, 6.5% error
306             7.62x51mm: 182 gr, empty_brass predicts 181 = 0.995 actual, 0.5% error
307             12.7x99mm: 847 gr, empty_brass predicts 830 = 0.980 actual, 2.0% error
308              
309             =over 4
310              
311             parameter: (float) bullet_diameter_mm is the width of the bullet (in mm)
312              
313             parameter: (float) base_diameter_mm is the width of the case at its base, NOT its rim (in mm)
314              
315             parameter: (float) case_len_mm is the overall length of the case, including rim and neck (in mm)
316              
317             parameter: (float) psi is the peak pressure tolerated by the cartridge (in psi)
318              
319             parameter: (boolean) OPTIONAL: set want_powder_bool to a True value to approximate powder capacity instead of water capacity. This is intrinsically inaccurate, since different powders have different weights, but it does okay for typical powders.
320              
321             returns: (int) cartridge weight (in grains)
322              
323             =back
324              
325             =cut
326              
327             # Estimate the weight of an empty rifle cartridge, assuming brass material.
328             # NOTES:
329             # Not very accurate (+/- 3% most cases), will vary between manufacturers.
330             # Only valid for tapered rifle casings for calibers around 5mm to 14mm.
331             #
332             # compare to known dimensions and weights:
333             # 5.56mm NATO: 5.70 mm bullet diam, 9.58 mm base diam, 44.70 mm case len, 62366 psi, 95 grains (predicts 95 = 1.000)
334             # 6mm PPC: 6.17 mm bullet diam, 11.20 mm base diam, 38.50 mm case len, 55000 psi, ?? grains (predicts 99 = ?,???)
335             # 6.8mm SPC: 7.00 mm bullet diam, 10.70 mm base diam, 42.60 mm case len, 52000 psi, 107 grains (predicts 100 = 1.070)
336             # 6.5x47mm Lapua: 6.71 mm bullet diam, 11.95 mm base diam, 47.00 mm case len, 63090 psi, ??? grains (predicts 166 = ?.???)
337             # 7.62mm NATO: 7.82 mm bullet diam, 11.94 mm base diam, 51.18 mm case len, 60200 psi, 182 grains (predicts 181 = 0.995)
338             # .243 WSSM: 6.20 mm bullet diam, 14.10 mm base diam, 42.40 mm case len, 65000 psi, 212 grains (predicts 207 = 0.976)
339             # 12.7mm NATO: 12.90 mm bullet diam 20.40 mm base diam, 99.30 mm case len, 54800 psi, 847 grains (predicts 830 = 0.980)
340             #
341             sub empty_brass {
342 3     3 1 1600 my ($bullet_diameter_mm, $base_diameter_mm, $case_len_mm, $psi) = @_;
343 3 50       12 die("mass_grains = empty_brass(bullet_diameter, base_diameter_mm, case_len_mm, max_pressure_psi)") unless(defined($psi));
344 3         11 my $bltf = abs($bullet_diameter_mm - 7.82) * 2.2;
345 3         12 my $cvol = $base_diameter_mm**2 * ($case_len_mm - $bltf);
346 3         27 return int($cvol * $psi / 2420484 + 0.5);
347             }
348              
349             ################### BEGIN functions used for gunfire()
350              
351             # firespec2throw: trying to improve on spec2throw (qv) to account for
352             # more physical factors. Returns:
353             # * the time taken to cross that distance in seconds,
354             # * its velocity at that point in m/s,
355             # * its velocity in ft/s,
356             # * its kinetic energy in (kg*m**2)/(s**2).
357             # returns: ( $t, $vms, $vfs, $ke )
358             sub firespec2throw {
359 1     1 0 4 my ($force, $brl_diam_mm, $brl_len_inches, $case_diam_mm, $case_len_mm, $mass_grains) = @_;
360             # print ("firespec2throw: force=$force brl_diam_mm=$brl_diam_mm brl_len_inches=$brl_len_inches case_diam_mm=$case_diam_mm case_len_mm=$case_len_mm mass_grains=$mass_grains\n");
361 1         4 my $cdm_m = $case_diam_mm / 1000.0;
362 1         3 my $cln_m = $case_len_mm / 1000.0;
363 1         2 my $bdm_m = $brl_diam_mm / 1000.0;
364 1         4 my $equiv_dist = (($case_diam_mm/$brl_diam_mm)**2) * $cln_m;
365 1         3 my $mass_kg = $mass_grains / 15432.358;
366 1         3 my $accel = $force / $mass_kg;
367             # s = 1/2 at**2, so t = sqrt(2s/a)
368 1         8 my $t = (2 * $equiv_dist / $accel)**0.5;
369             # v = at
370 1         25 my $vms = $accel * $t;
371             # gain factor corrects observed error, which appears to be very nonlinearly proportional to bore diameter
372 1         6 my $gain_factor = 0.866 * ($brl_diam_mm**0.093);
373 1         4 $vms *= $gain_factor / ($case_len_mm / $brl_diam_mm)**0.5 * 2.77531;
374 1         3 my $brl_diam_inches = $brl_diam_mm / 25.4;
375 1         3 my $case_diam_inches = $case_diam_mm / 25.4;
376 1         3 my $case_len_inches = $case_len_mm / 25.4;
377             # this equation is normalized on l/d=55, so correct via powley():
378 1         6 $vms *= powley ($brl_diam_inches, $case_diam_inches, $case_len_inches, $brl_diam_inches * 55, $brl_len_inches);
379             # print ("firespec2throw: powley: vms=$vms brl_diam_inches=$brl_diam_inches case_diam_inches=$case_diam_inches case_len_inches=$case_len_inches brl_diam_inches=$brl_diam_inches\n");
380             # re-derive the time it would have taken to achieve this velocity:
381 1         4 $t = $vms / $accel;
382 1         3 my $vfs = $vms * 3.2808399;
383             # energy = mass * velocity**2
384 1         4 my $ke = 0.5 * $mass_kg * ($vms**2); # Joules
385             # print ("firespec2throw: returning: accel=$accel t=$t vms=$vms vfs=$vfs ke=$ke\n");
386 1         10 return ($t, $vms, $vfs, $ke);
387             }
388              
389             sub rndto {
390 5     5 0 12 my ( $x, $n ) = @_;
391 5         8 my ( $ret );
392 5         15 $ret = int(($x*(10**$n))+0.5) / (10**$n);
393 5 100       13 if ( $n == 0 ) { return ( $ret ); }
  2         5  
394 3 50       25 if ( $ret !~ /\./ ) { return ("$ret.".("0"x$n)); }
  0         0  
395 3         26 while ( $ret !~ /\.../ ) { $ret .= '0'; }
  0         0  
396 3         10 return ( $ret );
397             }
398              
399             # spec2force(psi, diameter_mm)
400             # returns force generated when a cylinder of the given diameter is charged to
401             # the given pressure.
402             # Returns a float representing force in Newtons (ie, (kg*m)/(s**2)).
403             sub spec2force {
404 1     1 0 3 my ($psi, $diameter_mm) = @_;
405 1         3 my ($area, $ret);
406             # convert PSI to N/mm**2
407 1         3 $psi /= 145;
408             # calculate surface area from diameter
409 1         5 $area = pi * (($diameter_mm / 2)**2);
410             # calculate force
411 1         3 $ret = $psi * $area;
412 1         5 return $ret;
413             }
414              
415             sub fireglob {
416 1     1 0 4 my ( $psi, $brl_diam_mm, $brl_len_inches, $case_diam_mm, $case_len_mm, $mass_gr ) = @_;
417 1         5 my ( $t, $vms, $vfs, $ke ) = firespec2throw(spec2force($psi, $brl_diam_mm), $brl_diam_mm, $brl_len_inches, $case_diam_mm, $case_len_mm, $mass_gr );
418             # print ("firespec2throw: t=$t vms=$vms vfs=$vfs ke=$ke\n");
419 1         5 my $xvel = ($vms**2 / 2)**0.5;
420 1         3 my $xt = $xvel / 4.9;
421 1         3 my $dis = $vms * $xt;
422 1         2 my $lob = $dis;
423 1         5 my $rng = int ((($vms ** 1.5) / 2.5) + 0.5);
424 1         3 $lob += $rng;
425 1         2 $lob /= 2; # TODO: better formulation of $lob, taking sectional density into account
426 1         4 $t = rndto($t,6);
427 1         6 $vms = rndto($vms,2);
428 1         4 $vfs = rndto($vfs,2);
429 1         3 $ke = rndto($ke,0);
430 1         4 $lob = rndto($lob,0);
431 1         3 my $ret = {};
432 1         4 $ret->{'tm'} = $t;
433 1         3 $ret->{'m/s'} = $vms;
434 1         3 $ret->{'f/s'} = $vfs;
435 1         3 $ret->{'N*m'} = $ke;
436 1         3 $ret->{'r,m'} = $lob;
437 1         3 return $ret;
438             }
439              
440             =head2 gunfire (psi, bullet_diameter_mm, barrel_length_inches, cartridge_diameter_mm, cartridge_length_mm, bullet_mass_gr)
441              
442             The gunfire function attempts to approximate the performance of a cartridge/bullet/barrel combination in terms of muzzle velocity (and some related attributes).
443              
444             It does a tolerable job, despite grossly oversimplifying the problem. Its principle charms are that it is easy to use, and there isn't much else available for solving this kind of problem. Improving this function is on my to-do list.
445              
446             This function is the original work of the author.
447              
448             Compare to known cartridge/bullet/barrel performance:
449              
450             270 Winchester Short Magnum, 140gr, 24" barrel:
451             actual velocity: 3250 ft/s
452             gunfire() predicts: 3489 ft/s (7% error)
453              
454             .300 Lapua Magnum, 185gr, 27" barrel:
455             actual velocity: 3300 ft/s
456             gunfire() predicts: 3527 ft/s (6% error)
457              
458             .25-06 Remington, 120gr, 24" barrel:
459             actual velocity: 2990 ft/s
460             gunfire() predicts: 3056 ft/s (2% error)
461              
462             .308 Winchester, 150gr, 24" barrel:
463             actual velocity: 2820 ft/s
464             gunfire() predicts: 2840 ft/s (1% error)
465              
466             12.7x99mm NATO, 655gr, 45" barrel:
467             actual velocity: 3029 ft/s
468             gunfire() predicts: 3033 ft/s (0.1% error)
469              
470             .223 Remington, 55gr, 24" barrel:
471             actual velocity: 3240 ft/s
472             gunfire() predicts: 3170 ft/s (2% error)
473              
474             .30-06, 180gr, 24" barrel:
475             actual velocity: 2700 ft/s
476             gunfire() predicts: 2580 ft/s (5% error)
477              
478             .375 Ruger, 300gr, 23" barrel:
479             actual velocity: 2660 ft/s
480             gunfire() predicts: 2431 ft/s (9% error)
481              
482             In particular, take the "r,m" output field with a huge grain of salt. For a more accurate number, use Physics::Ballistics::External::flight_simulator(). The only advantage of "r,m" is that it is much much faster to derive (25 microseconds for gunfire(), vs an eighth of a second for flight_simulator() on my hardware).
483              
484             =over 4
485              
486             parameter: (float) peak chamber pressure (in psi, NOT cup!)
487              
488             parameter: (float) bullet diameter (in mm)
489              
490             parameter: (float) barrel length (in inches)
491              
492             parameter: (float) cartridge base diameter (in mm)
493              
494             parameter: (float) cartridge overall length (in mm)
495              
496             parameter: (float) bullet mass (in grains)
497              
498             returns: a reference to a hash, with the following fields:
499              
500             N*m: (int) muzzle energy (in joules)
501             f/s: (float) muzzle velocity (in feet per second)
502             m/s: (float) muzzle velocity (in meters per second)
503             r,m: (int) approx range achieved when fired at a 45 degree angle (in meters)
504             tm: (float) time elapsed from ignition to bullet's egress from barrel (in seconds)
505              
506             =back
507              
508             =cut
509              
510             sub gunfire {
511 1     1 1 516 my ( $psi, $brl_diam_mm, $brl_len_inches, $cart_base_diam_mm, $cart_len_mm, $bullet_mass_gr, $minimize ) = @_;
512 1 50       5 die("usage: psi, barrel diam mm, barrel len inches, cart diam mm, cart len mm, proj mass, [minimize]") unless (defined($bullet_mass_gr));
513 1         5 my $res = fireglob($psi, $brl_diam_mm, $brl_len_inches, $cart_base_diam_mm, $cart_len_mm, $bullet_mass_gr);
514 1 50 33     5 if (defined($minimize) && $minimize != 0) {
515 0         0 delete ($res->{'N*m'});
516 0         0 delete ($res->{'tm'} );
517             }
518 1         4 return $res;
519             }
520              
521             ################### END functions used for gunfire()
522              
523             ################### BEGIN functions used for ogival_volume
524              
525             # Given a cone of a given base diameter and height, return its volume in cubic centimeters.
526             #
527             sub vcone {
528 360000     360000 0 587493 my ($base_mm, $ht_mm) = @_;
529 360000         546190 my $base_cm = $base_mm / 10;
530 360000         501440 my $ht_cm = $ht_mm / 10;
531 360000         499923 my $radius_cm = $base_cm / 2;
532 360000         629370 my $volume = pi * ($radius_cm**2) * $ht_cm / 3.0;
533 360000         603104 return $volume;
534             }
535              
536             # Given a truncated cone with a base diameter, a top diameter, and a distance between the two, return its volume in cubic centimeters.
537             #
538             sub vcone_trunc {
539 180000     180000 0 321165 my ($base_mm, $top_mm, $ht_mm) = @_;
540 180000         283906 my $base_cm = $base_mm / 10;
541 180000         263025 my $top_cm = $top_mm / 10;
542 180000         263932 my $ht_cm = $ht_mm / 10;
543 180000         251892 my $radius1_cm = $base_cm / 2;
544 180000         252105 my $radius2_cm = $top_cm / 2;
545 180000         262906 my $delta_x_cm = $radius1_cm - $radius2_cm;
546 180000         264968 my $ht2_cm = $ht_cm * ($radius2_cm / $delta_x_cm);
547 180000         254168 my $ht1_cm = $ht2_cm + $ht_cm;
548 180000         352846 my $vcone_1 = vcone($radius1_cm * 20, $ht1_cm * 10);
549 180000         354981 my $vcone_2 = vcone($radius2_cm * 20, $ht2_cm * 10);
550 180000         365245 return $vcone_1 - $vcone_2;
551             }
552              
553             # Given a distance (in mm) from the tip of a haak ogival nose shape, its overall length (in mm), its radius at its base (in mm), and its bluntness factor, return the area of its cross-section at that distance in square centimeters.
554             # NOTE: $x=0 is the TIP of the ogive, and $x=$len is the BASE
555             # qv: http://en.wikipedia.org/wiki/Nose_cone_design#Haack_series
556             # From that article: "when C = 0, the notation LD signifies minimum drag for the given length and diameter,
557             # and when C = 1/3, LV indicates minimum drag for a given length and volume. The Haack series nose cones are
558             # not perfectly tangent to the body at their base except for case where C = 2/3. However, the discontinuity
559             # is usually so slight as to be imperceptible. For C > 2/3, Haack nose cones bulge to a maximum diameter
560             # greater than the base diameter. Haack nose tips do not come to a sharp point, but are slightly rounded."
561             # NOTE: To make best use of this function to reverse-engineer ogival-nose projectiles, it would be very nice
562             # to have a function which derived a best-fit value for C given a set of sample diameters at various distances
563             # from the nose tip. TODO.
564             #
565             sub haak_ogive {
566 180000     180000 0 316548 my ($x, $len, $radius, $C) = @_; # valid range for $C is 0..2/3, and larger C make more blunt ogival nose.
567 180000 50       337971 $C = 2/3 unless (defined($C)); # 2/3 seems to fit M791
568 180000         468827 my $theta = acos(1-(2*$x/$len));
569 180000         1525320 my $y = $radius * ($theta - sin(2*$theta)/2 + $C * sin($theta)**3)**0.5 / pi**0.5;
570 180000         338621 return $y;
571             }
572              
573             =head2 ogival_volume (length_mm, radius_mm, [C,] [granularity_mm])
574              
575             This function calculates the volume of a Haak-series ogival nose shape, as often used for areodynamically
576             streamlined projectiles. It is useful (for instance) for determining the mass of a nose, when nose
577             composition (and therefore density) is known.
578              
579             qv: http://en.wikipedia.org/wiki/Nose_cone_design#Haack_series
580              
581             Quoting from that article:
582              
583             While the series is a continuous set of shapes determined by the value of
584             C in the equations below, two values of C have particular significance:
585             when C = 0, the notation LD signifies minimum drag for the given length
586             and diameter, and when C = 1/3, LV indicates minimum drag for a given
587             length and volume. The Haack series nose cones are not perfectly tangent
588             to the body at their base except for case where C = 2/3. However, the
589             discontinuity is usually so slight as to be imperceptible. For C > 2/3,
590             Haack nose cones bulge to a maximum diameter greater than the base diameter.
591             Haack nose tips do not come to a sharp point, but are slightly rounded.
592              
593             =over 4
594              
595             parameter: (float) the length of the ogive, from base to tip (in mm)
596              
597             parameter: (float) the radius of the cross-section of the ogive (in mm)
598              
599             parameter: (float) OPTIONAL: the sharpness factor of the ogive, higher values providing a more fat, blunt nose shape (in range 0..2/3, default=2/3)
600              
601             parameter: (float) OPTIONAL: the granularity at which the volume will be calculated, lower values providing more accuracy but requiring more processing time (in mm, default=1/10000, provides < 0.1% error)
602              
603             returns: (float) volume (in cc)
604              
605             =back
606              
607             =cut
608              
609             # Given length, base radius, and bluntness factor of an ogival nose shape, returns its internal volume in cubic centimeters.
610             # NOTE: valid range for $C is 0..2/3, and larger C make more blunt ogival nose.
611             #
612             sub ogival_volume {
613 3     3 1 3442 my ($len_mm, $radius_mm, $C, $k) = @_;
614 3 50       13 $C = 2/3 unless (defined($C));
615 3 50       11 $k = 1/10000 unless (defined($k));
616 3         7 my $volume_total = 0;
617 3         12 my $prev_y = $radius_mm;
618 3         14 for (my $x_mm = $k; $x_mm < $len_mm; $x_mm += $k) {
619 180000         327069 my $y_mm = haak_ogive($x_mm, $len_mm, $radius_mm, $C);
620 180000         394168 $volume_total += vcone_trunc(2*$prev_y, 2*$y_mm, $k);
621 180000         481138 $prev_y = $y_mm;
622             }
623 3         32 return $volume_total;
624             }
625              
626             ################### END functions used for ogival_volume
627              
628             =head2 powley (bore_diameter_inches, case_base_diameter_inches, case_length_inches, barrel_1_length_inches, barrel_2_length_inches)
629              
630             This function implements Powley's formula for approximating the projectile velocity gained or lost from a change in barrel length.
631              
632             Example of use:
633              
634             It is known that the muzzle velocity of a .223 Remington, 55gr bullet from a 24" barrel is 3240 ft/s.
635             We want to know its muzzle velocity from a 16" barrel.
636              
637             powley (0.224, 0.378, 1.77, 24, 16) = 0.9205
638             3240 ft/s * 0.9205 = 2982 ft/s
639              
640             =over 4
641              
642             parameter: (float) barrel's bore diameter (in inches)
643              
644             parameter: (float) cartridge's base case diameter (in inches)
645              
646             parameter: (float) cartridge's overall length (in inches)
647              
648             parameter: (float) the length of the barrel for which muzzle velocity is known (in inches)
649              
650             parameter: (float) the length of the barrel for which muzzle velocity is not known (in inches)
651              
652             returns: (float) the ratio of the muzzle velocities (unitless)
653              
654             =back
655              
656             =cut
657              
658             # $vms *= powley ($brl_diam_inches, $case_diam_inches, $case_len_inches, $brl_diam_inches * 55, $brl_len_inches);
659             sub powley {
660 2     2 1 909 my ($barrel_diam_inches, $case_diam_inches, $case_len_inches, $blen1, $blen2) = @_; # blen1 = original length, blen2 = new length
661 2 50       10 die('powley(bore diam (inches), cart diam (inches), cart len (inches), brl len orig (inches), brl len new (inches))') unless (defined($blen2));
662             # print ("powley: called: barrel_diam_inches=$barrel_diam_inches case_diam_inches=$case_diam_inches case_len_inches=$case_len_inches blen1=$blen1 blen2=$blen2\n");
663 2         7 my $b_rad = $barrel_diam_inches / 2;
664 2         7 my $c_rad = $case_diam_inches / 2;
665 2         8 my $c_vol = ($c_rad**2) * $case_len_inches;
666 2         7 my $b_vol = ($b_rad**2) * $blen1;
667 2         7 my $r1 = ($c_vol + $b_vol) / $c_vol;
668 2         5 $b_vol = ($b_rad**2) * $blen2;
669 2         5 my $r2 = ($c_vol + $b_vol) / $c_vol;
670 2         16 my $factor = ((1 - ($r2**(-0.25))) / (1-($r1**(-0.25)))) ** 0.5;
671             # print ("powley: returning: factor=$factor b_rad=$b_rad c_vol=$c_vol b_vol=$b_vol r1=$r1 r2=$r2\n");
672 2         9 return $factor;
673             }
674              
675             =head2 cup2psi_linear (cup[, want_range[, fractional_deviation]])
676              
677             Approximates peak chamber pressure, in psi, given peak chamber CUP (copper crush test). Since there is a degree of error present in both kinds of pressure tests, this will often disagree with published measurements. To offset this, a range may be requested by passing a non-false second parameter. This will cause three values to be returned: A low-end psi estimate, the median psi estimate (which is the same as the value returned when called without a want_range parameter), and a high-end psi estimate. The degree of variation may be adjusted by passing a value between 0 and 1 as the third argument (default is 0.05).
678              
679             Based on linear formula from Denton Bramwell's _Correlating PSI and CUP_, with curve-fitting enhancements by module author.
680              
681             =cut
682              
683             sub cup2psi_linear {
684 1     1 1 473 my ($cup, $want_range, $range_wobble) = @_;
685 1 50       5 die("usage:\npeak_pressure_psi = cup2psi_linear(peak_pressure_cup)\n(low_peak_psi, median_peak_psi, high_peak_psi) = cup2psi_linear(peak_cup, 1[, fraction_variation])") unless (defined($cup));
686 1 50       4 $want_range = 0 unless (defined($want_range));
687 1 50       3 $range_wobble = 0.05 unless (defined($range_wobble));
688 1         4 my $median_psi = 1.51586 * $cup - 17902.0;
689 1 50       6 return int($median_psi) unless ($want_range);
690 0         0 my $low_wob = 1.0 - $range_wobble;
691 0         0 my $high_wob = 1.0 + $range_wobble;
692 0         0 my $low_psi = 1.51586 * ($low_wob * $cup) - (17902.0 * $high_wob);
693 0         0 my $high_psi = 1.51586 * ($high_wob * $cup) - (17902.0 * $low_wob);
694 0         0 return (int($low_psi), int($median_psi), int($high_psi));
695             }
696              
697             =head2 cup2psi (cup[, want_range[, fractional_deviation]])
698              
699             Approximates peak chamber pressure, in psi, given peak chamber CUP (copper crush test). Since there is a degree of error present in both kinds of pressure tests, this will often disagree with published measurements. To offset this, a range may be requested by passing a non-false second parameter. This will cause three values to be returned: A low-end psi estimate, the median psi estimate (which is the same as the value returned when called without a want_range parameter), and a high-end psi estimate. The degree of variation may be adjusted by passing a value between 0 and 1 as the third argument (default is 0.04).
700              
701             Based on exponential formula from http://kwk.us/pressures.html, with enhancements by module author.
702              
703             =cut
704              
705             sub cup2psi {
706 1     1 1 407 my ($cup, $want_range, $range_wobble) = @_;
707 1 50       8 die("usage:\npeak_pressure_psi = cup2psi(peak_pressure_cup)\n(low_peak_psi, median_peak_psi, high_peak_psi) = cup2psi(peak_cup, 1[, fraction_variation])") unless (defined($cup));
708 1 50       5 $want_range = 0 unless (defined($want_range));
709 1 50       20 $range_wobble = 0.04 unless (defined($range_wobble));
710 1 50       7 $cup /= 1000 unless ($cup < 100); # Assuming user is providing CUP/1000, which is a common convention.
711 1         8 my $median_psi = $cup * (1 + $cup**2.2 / 31000); # NOTE: Original formula used 30000 here. Adjusted it to be more accurate for common military cartridges.
712 1 50       6 if ($cup > 60000) {
713 0         0 $median_psi = $cup + ($cup - 20)**2.3 / 210;
714             }
715 1 50       9 return int($median_psi * 1000) unless ($want_range);
716 0         0 my $low_wob = 1.0 - $range_wobble;
717 0         0 my $high_wob = 1.0 + $range_wobble;
718 0         0 my $low_psi = $cup * $low_wob * (1 + ($cup * $low_wob )**2.2 / 31000);
719 0         0 my $high_psi = $cup * $high_wob * (1 + ($cup * $high_wob)**2.2 / 31000);
720 0 0       0 if ($cup > 60000) {
721 0         0 $low_psi = $cup * $low_wob + (($cup - 20) * $low_wob )**2.3 / 210;
722 0         0 $high_psi = $cup * $high_wob + (($cup - 20) * $high_wob)**2.3 / 210;
723             }
724 0         0 return (int($low_psi * 1000), int($median_psi * 1000), int($high_psi * 1000));
725             }
726              
727             =head2 recoil_mbt (gun_mass_kg, projectile_mass_kg, projectile_velocity_mps, [gas_mass_kg,] [gas_velocity_mps,] [recoil_distance_cm,] [english_or_metric_str])
728              
729             Approximates the recoil force of a battletank's large-bore main gun (or any other large-bore, high-velocity gun).
730              
731             Based on formula from Ogorkiewicz's _Design and Development of Fighting Vehicles_, page 58.
732              
733             As a rule of thumb, the recoil force of an MBT-proportioned vehicle's main gun should not exceed twice the vehicle's mass.
734              
735             If combustion gas mass and velocity are absent, they will be estimated from the projectile mass and velocity.
736              
737             The gun mass includes all of the parts moving against the vechicle's recoil mechanism (principally, the barrel and breech).
738              
739             =over 4
740              
741             parameter: (float) gun mass (in kg)
742              
743             parameter: (float) projectile mass (in kg)
744              
745             parameter: (float) projectile muzzle velocity (in meters per second)
746              
747             parameter: (float) OPTIONAL: combustion gas mass, equal to the propellant mass, usually between one and one half the projectile mass (in kg)
748              
749             parameter: (float) OPTIONAL: combustion gas velocity (in meters per second, usually 1450).
750              
751             parameter: (float) OPTIONAL: recoil distance (in cm, default=20)
752              
753             returns: (float) recoil force exerted on the vehicle (in tonnes)
754              
755             =back
756              
757             =cut
758              
759             # Main gun recoil forces, lifted from Ogorkiewicz's _Design and Development of Fighting Vehicles_, page 58
760             # This should not exceed twice the vehicle's mass, or it might roll over when firing.
761             # Propellant mass and velocity will be estimated relative to projectile mass if not actually specified (TODO: factor in projectile velocity), but this will likely be somewhat inaccurate.
762             sub recoil_mbt
763             {
764 1     1 1 567 my ($w_g, $w_p, $v_p, $w_e, $v_e, $rl, $engmet) = @_;
765 1 50       8 die ("usage: recoil (gun_mass, proj_mass, proj_vel, [gas_mass], [gas_vel], [recoil|{20 cm}], [units:{e,m}]") unless (defined ($v_p));
766 1   50     10 $rl //= 20;
767 1   50     6 $engmet //= 'm';
768 1         3 my $gees = 32.1740486; # gravity in feet/second**2
769 1         3 $rl *= 0.032808399; # converting cm to feet
770 1         3 $w_p *= 2.2046226; # converting kg to pounds
771 1         4 $v_p *= 3.2808399; # converting meters/second to feet/second
772 1         3 $w_g *= 2.2046226; # converting kg to pounds
773 1 50       6 if (defined($w_e)) {
774 0         0 $w_e *= 2.2046226; # converting kg to pounds
775             }
776             else {
777 1         3 $w_e = $w_p / 2.0;
778             }
779 1 50       5 if (defined($v_e)) {
780 0         0 $v_e *= 3.2808399; # converting meters/second to feet/second
781             }
782             else {
783 1         4 my $vp_1 = $v_p * 1.5;
784 1         3 my $vp_2 = 3280.84; # 1000 meters per second, in feet per second
785 1         2 $v_e = $vp_1;
786 1 50       4 $v_e = $vp_2 if ($vp_2 > $vp_1);
787             }
788             # print (" ((($w_p*$v_p)+($w_e*$v_e))**2) / (2*$gees*$w_g*$rl);\n");
789 1         8 my $force = ((($w_p*$v_p)+($w_e*$v_e))**2) / (2*$gees*$w_g*$rl); # force, pounds
790 1 50       6 if ($engmet eq 'e') { $force /= 2000.0000; } # english measure: tons
  0         0  
791 1         2 else { $force /= 2204.6226; } # metric measure: tonnes
792 1 50       7 if ($force < 10) { $force = int ($force * 100 + 0.5) / 100; }
  0 50       0  
793 1         5 elsif ($force < 100) { $force = int ($force * 10 + 0.5) / 10; }
794 0         0 else { $force = int ($force * 1 + 0.5) / 1; }
795 1         21 return $force; # returns tons or tonnes
796             }
797              
798             1;
799              
800             =head1 TODO
801              
802             The accuracy of these estimating functions can be improved, and I intend to improve them.
803              
804             In particular, empty_brass should be made to take a "parent case" option, because it tends to underestimate the weight of cartridges which are based on other cartridges which have been trimmed or necked down.
805              
806             =cut