ballistics-engine 0.22.0 → 0.22.11
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 +4 -4
- data/Cargo.lock +57 -184
- data/Cargo.toml +2 -2
- data/README.md +91 -136
- data/lib/ballistics_engine.rb +2 -0
- data/src/lib.rs +379 -117
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7fd3109e80b8fb96d2647e072eea4f8060caa3df7e661823879362caa732775b
|
|
4
|
+
data.tar.gz: 48a989637d61de7da186012d14af798611eb3803ed80f209f80294e6ed3c2b32
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e2d28d89a751928ba6b36f81150765ea94b81f050078ddba105bd287d41d27838cb32d6cda89c964cedae21bf49ed3aff7b7f573052d06b98f6e9a013f12331a
|
|
7
|
+
data.tar.gz: a49d7bab609e3077481659ad5180d36290d5f24e23519acd53ead7ccbca0467fa4008500c33f535df63cc8c58540261648ba700054f5c9b88bb98dcb2b037c87
|
data/Cargo.lock
CHANGED
|
@@ -93,15 +93,15 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
|
|
93
93
|
|
|
94
94
|
[[package]]
|
|
95
95
|
name = "ballistics-engine"
|
|
96
|
-
version = "0.22.
|
|
96
|
+
version = "0.22.11"
|
|
97
97
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
98
|
-
checksum = "
|
|
98
|
+
checksum = "76e5814dcee860b6d400ef5bd432833afad4750328ab3666befe73fa2fe0eae4"
|
|
99
99
|
dependencies = [
|
|
100
100
|
"clap",
|
|
101
101
|
"clap_complete",
|
|
102
102
|
"csv",
|
|
103
103
|
"dirs",
|
|
104
|
-
"getrandom 0.3
|
|
104
|
+
"getrandom 0.4.3",
|
|
105
105
|
"js-sys",
|
|
106
106
|
"nalgebra",
|
|
107
107
|
"ndarray",
|
|
@@ -121,7 +121,7 @@ dependencies = [
|
|
|
121
121
|
|
|
122
122
|
[[package]]
|
|
123
123
|
name = "ballistics_engine_rb"
|
|
124
|
-
version = "0.22.
|
|
124
|
+
version = "0.22.11"
|
|
125
125
|
dependencies = [
|
|
126
126
|
"ballistics-engine",
|
|
127
127
|
"magnus",
|
|
@@ -193,6 +193,17 @@ version = "1.0.4"
|
|
|
193
193
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
194
194
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|
195
195
|
|
|
196
|
+
[[package]]
|
|
197
|
+
name = "chacha20"
|
|
198
|
+
version = "0.10.1"
|
|
199
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
200
|
+
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
|
|
201
|
+
dependencies = [
|
|
202
|
+
"cfg-if",
|
|
203
|
+
"cpufeatures 0.3.0",
|
|
204
|
+
"rand_core",
|
|
205
|
+
]
|
|
206
|
+
|
|
196
207
|
[[package]]
|
|
197
208
|
name = "clang-sys"
|
|
198
209
|
version = "1.8.1"
|
|
@@ -268,6 +279,15 @@ dependencies = [
|
|
|
268
279
|
"libc",
|
|
269
280
|
]
|
|
270
281
|
|
|
282
|
+
[[package]]
|
|
283
|
+
name = "cpufeatures"
|
|
284
|
+
version = "0.3.0"
|
|
285
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
286
|
+
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
|
287
|
+
dependencies = [
|
|
288
|
+
"libc",
|
|
289
|
+
]
|
|
290
|
+
|
|
271
291
|
[[package]]
|
|
272
292
|
name = "crc32fast"
|
|
273
293
|
version = "1.5.0"
|
|
@@ -446,113 +466,41 @@ dependencies = [
|
|
|
446
466
|
|
|
447
467
|
[[package]]
|
|
448
468
|
name = "getrandom"
|
|
449
|
-
version = "0.3
|
|
469
|
+
version = "0.4.3"
|
|
450
470
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
451
|
-
checksum = "
|
|
471
|
+
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
|
452
472
|
dependencies = [
|
|
453
473
|
"cfg-if",
|
|
454
474
|
"js-sys",
|
|
455
475
|
"libc",
|
|
456
476
|
"r-efi",
|
|
457
|
-
"
|
|
477
|
+
"rand_core",
|
|
458
478
|
"wasm-bindgen",
|
|
459
479
|
]
|
|
460
480
|
|
|
461
481
|
[[package]]
|
|
462
482
|
name = "glam"
|
|
463
|
-
version = "0.
|
|
464
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
465
|
-
checksum = "333928d5eb103c5d4050533cec0384302db6be8ef7d3cebd30ec6a35350353da"
|
|
466
|
-
|
|
467
|
-
[[package]]
|
|
468
|
-
name = "glam"
|
|
469
|
-
version = "0.15.2"
|
|
470
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
471
|
-
checksum = "3abb554f8ee44336b72d522e0a7fe86a29e09f839a36022fa869a7dfe941a54b"
|
|
472
|
-
|
|
473
|
-
[[package]]
|
|
474
|
-
name = "glam"
|
|
475
|
-
version = "0.16.0"
|
|
476
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
477
|
-
checksum = "4126c0479ccf7e8664c36a2d719f5f2c140fbb4f9090008098d2c291fa5b3f16"
|
|
478
|
-
|
|
479
|
-
[[package]]
|
|
480
|
-
name = "glam"
|
|
481
|
-
version = "0.17.3"
|
|
482
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
483
|
-
checksum = "e01732b97afd8508eee3333a541b9f7610f454bb818669e66e90f5f57c93a776"
|
|
484
|
-
|
|
485
|
-
[[package]]
|
|
486
|
-
name = "glam"
|
|
487
|
-
version = "0.18.0"
|
|
488
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
489
|
-
checksum = "525a3e490ba77b8e326fb67d4b44b4bd2f920f44d4cc73ccec50adc68e3bee34"
|
|
490
|
-
|
|
491
|
-
[[package]]
|
|
492
|
-
name = "glam"
|
|
493
|
-
version = "0.19.0"
|
|
494
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
495
|
-
checksum = "2b8509e6791516e81c1a630d0bd7fbac36d2fa8712a9da8662e716b52d5051ca"
|
|
496
|
-
|
|
497
|
-
[[package]]
|
|
498
|
-
name = "glam"
|
|
499
|
-
version = "0.20.5"
|
|
500
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
501
|
-
checksum = "f43e957e744be03f5801a55472f593d43fabdebf25a4585db250f04d86b1675f"
|
|
502
|
-
|
|
503
|
-
[[package]]
|
|
504
|
-
name = "glam"
|
|
505
|
-
version = "0.21.3"
|
|
506
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
507
|
-
checksum = "518faa5064866338b013ff9b2350dc318e14cc4fcd6cb8206d7e7c9886c98815"
|
|
508
|
-
|
|
509
|
-
[[package]]
|
|
510
|
-
name = "glam"
|
|
511
|
-
version = "0.22.0"
|
|
512
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
513
|
-
checksum = "12f597d56c1bd55a811a1be189459e8fad2bbc272616375602443bdfb37fa774"
|
|
514
|
-
|
|
515
|
-
[[package]]
|
|
516
|
-
name = "glam"
|
|
517
|
-
version = "0.23.0"
|
|
518
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
519
|
-
checksum = "8e4afd9ad95555081e109fe1d21f2a30c691b5f0919c67dfa690a2e1eb6bd51c"
|
|
520
|
-
|
|
521
|
-
[[package]]
|
|
522
|
-
name = "glam"
|
|
523
|
-
version = "0.24.2"
|
|
524
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
525
|
-
checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945"
|
|
526
|
-
|
|
527
|
-
[[package]]
|
|
528
|
-
name = "glam"
|
|
529
|
-
version = "0.25.0"
|
|
530
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
531
|
-
checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3"
|
|
532
|
-
|
|
533
|
-
[[package]]
|
|
534
|
-
name = "glam"
|
|
535
|
-
version = "0.27.0"
|
|
483
|
+
version = "0.30.10"
|
|
536
484
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
537
|
-
checksum = "
|
|
485
|
+
checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9"
|
|
538
486
|
|
|
539
487
|
[[package]]
|
|
540
488
|
name = "glam"
|
|
541
|
-
version = "0.
|
|
489
|
+
version = "0.31.1"
|
|
542
490
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
543
|
-
checksum = "
|
|
491
|
+
checksum = "556f6b2ea90b8d15a74e0e7bb41671c9bdf38cd9f78c284d750b9ce58a2b5be7"
|
|
544
492
|
|
|
545
493
|
[[package]]
|
|
546
494
|
name = "glam"
|
|
547
|
-
version = "0.
|
|
495
|
+
version = "0.32.1"
|
|
548
496
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
549
|
-
checksum = "
|
|
497
|
+
checksum = "f70749695b063ecbf6b62949ccccde2e733ec3ecbbd71d467dca4e5c6c97cca0"
|
|
550
498
|
|
|
551
499
|
[[package]]
|
|
552
500
|
name = "glam"
|
|
553
|
-
version = "0.
|
|
501
|
+
version = "0.33.2"
|
|
554
502
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
555
|
-
checksum = "
|
|
503
|
+
checksum = "7f22fb22f065b308be0d8724e3706c7fa3fc2a6c7d6899df4cad7860e7a75436"
|
|
556
504
|
|
|
557
505
|
[[package]]
|
|
558
506
|
name = "glob"
|
|
@@ -713,27 +661,15 @@ dependencies = [
|
|
|
713
661
|
|
|
714
662
|
[[package]]
|
|
715
663
|
name = "nalgebra"
|
|
716
|
-
version = "0.
|
|
664
|
+
version = "0.35.0"
|
|
717
665
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
718
|
-
checksum = "
|
|
666
|
+
checksum = "adc43a60c217b0c6ff46e47f26911015ad8d2e5a8be1af668c67e370d99a4346"
|
|
719
667
|
dependencies = [
|
|
720
668
|
"approx",
|
|
721
|
-
"glam 0.14.0",
|
|
722
|
-
"glam 0.15.2",
|
|
723
|
-
"glam 0.16.0",
|
|
724
|
-
"glam 0.17.3",
|
|
725
|
-
"glam 0.18.0",
|
|
726
|
-
"glam 0.19.0",
|
|
727
|
-
"glam 0.20.5",
|
|
728
|
-
"glam 0.21.3",
|
|
729
|
-
"glam 0.22.0",
|
|
730
|
-
"glam 0.23.0",
|
|
731
|
-
"glam 0.24.2",
|
|
732
|
-
"glam 0.25.0",
|
|
733
|
-
"glam 0.27.0",
|
|
734
|
-
"glam 0.28.0",
|
|
735
|
-
"glam 0.29.3",
|
|
736
669
|
"glam 0.30.10",
|
|
670
|
+
"glam 0.31.1",
|
|
671
|
+
"glam 0.32.1",
|
|
672
|
+
"glam 0.33.2",
|
|
737
673
|
"matrixmultiply",
|
|
738
674
|
"nalgebra-macros",
|
|
739
675
|
"num-complex",
|
|
@@ -860,12 +796,6 @@ version = "0.2.0"
|
|
|
860
796
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
861
797
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
|
862
798
|
|
|
863
|
-
[[package]]
|
|
864
|
-
name = "paste"
|
|
865
|
-
version = "1.0.15"
|
|
866
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
867
|
-
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
|
868
|
-
|
|
869
799
|
[[package]]
|
|
870
800
|
name = "pest"
|
|
871
801
|
version = "2.8.5"
|
|
@@ -936,15 +866,6 @@ dependencies = [
|
|
|
936
866
|
"portable-atomic",
|
|
937
867
|
]
|
|
938
868
|
|
|
939
|
-
[[package]]
|
|
940
|
-
name = "ppv-lite86"
|
|
941
|
-
version = "0.2.21"
|
|
942
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
943
|
-
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
|
944
|
-
dependencies = [
|
|
945
|
-
"zerocopy",
|
|
946
|
-
]
|
|
947
|
-
|
|
948
869
|
[[package]]
|
|
949
870
|
name = "proc-macro2"
|
|
950
871
|
version = "1.0.106"
|
|
@@ -978,44 +899,32 @@ dependencies = [
|
|
|
978
899
|
|
|
979
900
|
[[package]]
|
|
980
901
|
name = "r-efi"
|
|
981
|
-
version = "
|
|
902
|
+
version = "6.0.0"
|
|
982
903
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
983
|
-
checksum = "
|
|
904
|
+
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
|
984
905
|
|
|
985
906
|
[[package]]
|
|
986
907
|
name = "rand"
|
|
987
|
-
version = "0.
|
|
908
|
+
version = "0.10.2"
|
|
988
909
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
989
|
-
checksum = "
|
|
910
|
+
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
|
990
911
|
dependencies = [
|
|
991
|
-
"
|
|
992
|
-
"
|
|
993
|
-
]
|
|
994
|
-
|
|
995
|
-
[[package]]
|
|
996
|
-
name = "rand_chacha"
|
|
997
|
-
version = "0.9.0"
|
|
998
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
999
|
-
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
|
1000
|
-
dependencies = [
|
|
1001
|
-
"ppv-lite86",
|
|
912
|
+
"chacha20",
|
|
913
|
+
"getrandom 0.4.3",
|
|
1002
914
|
"rand_core",
|
|
1003
915
|
]
|
|
1004
916
|
|
|
1005
917
|
[[package]]
|
|
1006
918
|
name = "rand_core"
|
|
1007
|
-
version = "0.
|
|
919
|
+
version = "0.10.1"
|
|
1008
920
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1009
|
-
checksum = "
|
|
1010
|
-
dependencies = [
|
|
1011
|
-
"getrandom 0.3.4",
|
|
1012
|
-
]
|
|
921
|
+
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
|
1013
922
|
|
|
1014
923
|
[[package]]
|
|
1015
924
|
name = "rand_distr"
|
|
1016
|
-
version = "0.
|
|
925
|
+
version = "0.6.0"
|
|
1017
926
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1018
|
-
checksum = "
|
|
927
|
+
checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8"
|
|
1019
928
|
dependencies = [
|
|
1020
929
|
"num-traits",
|
|
1021
930
|
"rand",
|
|
@@ -1137,9 +1046,9 @@ checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
|
|
1137
1046
|
|
|
1138
1047
|
[[package]]
|
|
1139
1048
|
name = "safe_arch"
|
|
1140
|
-
version = "0.
|
|
1049
|
+
version = "1.0.0"
|
|
1141
1050
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1142
|
-
checksum = "
|
|
1051
|
+
checksum = "1f7caad094bd561859bcd467734a720c3c1f5d1f338995351fefe2190c45efed"
|
|
1143
1052
|
dependencies = [
|
|
1144
1053
|
"bytemuck",
|
|
1145
1054
|
]
|
|
@@ -1211,7 +1120,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
|
1211
1120
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
|
1212
1121
|
dependencies = [
|
|
1213
1122
|
"cfg-if",
|
|
1214
|
-
"cpufeatures",
|
|
1123
|
+
"cpufeatures 0.2.17",
|
|
1215
1124
|
"digest",
|
|
1216
1125
|
]
|
|
1217
1126
|
|
|
@@ -1229,14 +1138,13 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
|
|
1229
1138
|
|
|
1230
1139
|
[[package]]
|
|
1231
1140
|
name = "simba"
|
|
1232
|
-
version = "0.
|
|
1141
|
+
version = "0.10.0"
|
|
1233
1142
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1234
|
-
checksum = "
|
|
1143
|
+
checksum = "8f45c644a9f3a386f9288625d9f0c1e999e1acf07a37df35d0516c7f199d9cb2"
|
|
1235
1144
|
dependencies = [
|
|
1236
1145
|
"approx",
|
|
1237
1146
|
"num-complex",
|
|
1238
1147
|
"num-traits",
|
|
1239
|
-
"paste",
|
|
1240
1148
|
"wide",
|
|
1241
1149
|
]
|
|
1242
1150
|
|
|
@@ -1325,15 +1233,6 @@ version = "0.11.1+wasi-snapshot-preview1"
|
|
|
1325
1233
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1326
1234
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
|
1327
1235
|
|
|
1328
|
-
[[package]]
|
|
1329
|
-
name = "wasip2"
|
|
1330
|
-
version = "1.0.4+wasi-0.2.12"
|
|
1331
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1332
|
-
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
|
|
1333
|
-
dependencies = [
|
|
1334
|
-
"wit-bindgen",
|
|
1335
|
-
]
|
|
1336
|
-
|
|
1337
1236
|
[[package]]
|
|
1338
1237
|
name = "wasm-bindgen"
|
|
1339
1238
|
version = "0.2.108"
|
|
@@ -1405,9 +1304,9 @@ dependencies = [
|
|
|
1405
1304
|
|
|
1406
1305
|
[[package]]
|
|
1407
1306
|
name = "wide"
|
|
1408
|
-
version = "
|
|
1307
|
+
version = "1.5.0"
|
|
1409
1308
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1410
|
-
checksum = "
|
|
1309
|
+
checksum = "dfdfe6a32973f2d1b268b8895845a8a96cac2f0191e72c27cc929036060dbf89"
|
|
1411
1310
|
dependencies = [
|
|
1412
1311
|
"bytemuck",
|
|
1413
1312
|
"safe_arch",
|
|
@@ -1428,32 +1327,6 @@ dependencies = [
|
|
|
1428
1327
|
"windows-link",
|
|
1429
1328
|
]
|
|
1430
1329
|
|
|
1431
|
-
[[package]]
|
|
1432
|
-
name = "wit-bindgen"
|
|
1433
|
-
version = "0.57.1"
|
|
1434
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1435
|
-
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
|
1436
|
-
|
|
1437
|
-
[[package]]
|
|
1438
|
-
name = "zerocopy"
|
|
1439
|
-
version = "0.8.33"
|
|
1440
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1441
|
-
checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
|
|
1442
|
-
dependencies = [
|
|
1443
|
-
"zerocopy-derive",
|
|
1444
|
-
]
|
|
1445
|
-
|
|
1446
|
-
[[package]]
|
|
1447
|
-
name = "zerocopy-derive"
|
|
1448
|
-
version = "0.8.33"
|
|
1449
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1450
|
-
checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
|
|
1451
|
-
dependencies = [
|
|
1452
|
-
"proc-macro2",
|
|
1453
|
-
"quote",
|
|
1454
|
-
"syn",
|
|
1455
|
-
]
|
|
1456
|
-
|
|
1457
1330
|
[[package]]
|
|
1458
1331
|
name = "zip"
|
|
1459
1332
|
version = "6.0.0"
|
data/Cargo.toml
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# rake-compiler/rb-sys cargo-metadata lookup resolves (newer rb-sys does not
|
|
4
4
|
# normalize hyphens). The published gem name is set in the gemspec, independent.
|
|
5
5
|
name = "ballistics_engine_rb"
|
|
6
|
-
version = "0.22.
|
|
6
|
+
version = "0.22.11"
|
|
7
7
|
edition = "2021"
|
|
8
8
|
authors = ["Alex Jokela <email@tinycomputers.io>"]
|
|
9
9
|
description = "Ruby bindings for ballistics-engine"
|
|
@@ -15,7 +15,7 @@ name = "ballistics_engine_rb"
|
|
|
15
15
|
crate-type = ["cdylib"]
|
|
16
16
|
|
|
17
17
|
[dependencies]
|
|
18
|
-
ballistics-engine = { version = "0.22.
|
|
18
|
+
ballistics-engine = { version = "0.22.11", default-features = false }
|
|
19
19
|
magnus = "0.8"
|
|
20
20
|
|
|
21
21
|
[profile.release]
|
data/README.md
CHANGED
|
@@ -33,165 +33,120 @@ $ gem install ballistics-engine
|
|
|
33
33
|
|
|
34
34
|
## Quick Start
|
|
35
35
|
|
|
36
|
+
The API is hash-in / hash-out — pass a hash of imperial-unit parameters, get a hash back.
|
|
37
|
+
|
|
36
38
|
```ruby
|
|
37
39
|
require 'ballistics_engine'
|
|
38
40
|
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
0.223,
|
|
42
|
-
168.0,
|
|
43
|
-
2650.0,
|
|
44
|
-
0.308,
|
|
45
|
-
1.2,
|
|
46
|
-
1.5,
|
|
47
|
-
100.0,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
# Optional: Add wind conditions
|
|
54
|
-
wind = BallisticsEngine::WindConditions.new(
|
|
55
|
-
10.0, # speed (mph)
|
|
56
|
-
90.0 # direction (degrees, 90 = from right)
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
# Optional: Add atmospheric conditions
|
|
60
|
-
atmosphere = BallisticsEngine::AtmosphericConditions.new(
|
|
61
|
-
59.0, # temperature (F)
|
|
62
|
-
29.92, # pressure (inHg)
|
|
63
|
-
50.0, # humidity (%)
|
|
64
|
-
0.0 # altitude (feet)
|
|
41
|
+
# 168gr .308 with a 10 mph crosswind
|
|
42
|
+
result = BallisticsEngine.solve(
|
|
43
|
+
"bc" => 0.223, # G7 BC
|
|
44
|
+
"bullet_weight_grains" => 168.0,
|
|
45
|
+
"muzzle_velocity_fps" => 2650.0,
|
|
46
|
+
"bullet_diameter_inches" => 0.308,
|
|
47
|
+
"bullet_length_inches" => 1.2,
|
|
48
|
+
"sight_height_inches" => 1.5,
|
|
49
|
+
"zero_distance_yards" => 100.0,
|
|
50
|
+
"drag_model" => "G7", # G1 | G7 | G8
|
|
51
|
+
"wind" => { "speed_mph" => 10.0, "direction_degrees" => 90.0 },
|
|
52
|
+
"atmosphere" => { "temperature_f" => 59.0, "pressure_inhg" => 29.92,
|
|
53
|
+
"humidity_percent" => 50.0, "altitude_feet" => 0.0 }
|
|
65
54
|
)
|
|
66
55
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
#
|
|
72
|
-
puts "Max range: #{result.max_range_yards.round(1)} yards"
|
|
73
|
-
puts "Time of flight: #{result.time_of_flight.round(3)} seconds"
|
|
74
|
-
puts "Impact velocity: #{result.impact_velocity_fps.round(1)} fps"
|
|
75
|
-
puts "Impact energy: #{result.impact_energy_ftlbs.round(1)} ft-lbs"
|
|
76
|
-
|
|
77
|
-
# Iterate through trajectory points
|
|
78
|
-
result.points.each do |point|
|
|
79
|
-
puts "Range: #{point.x.round(1)}yd, Drop: #{point.y.round(2)}yd, Velocity: #{point.velocity_fps.round(1)}fps"
|
|
56
|
+
puts "Max range: #{result['max_range_yards'].round(1)} yd"
|
|
57
|
+
puts "Time of flight: #{result['time_of_flight'].round(3)} s"
|
|
58
|
+
puts "Impact velocity: #{result['impact_velocity_fps'].round(1)} fps"
|
|
59
|
+
result["points"].each do |p|
|
|
60
|
+
puts " range #{p['x'].round(1)}yd drop #{p['y'].round(2)}yd vel #{p['velocity_fps'].round(1)}fps"
|
|
80
61
|
end
|
|
81
62
|
```
|
|
82
63
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
### Classes
|
|
64
|
+
### Velocity-dependent BC (`bc_segments`)
|
|
86
65
|
|
|
87
|
-
|
|
66
|
+
Supply your own velocity:BC ladder (velocities in fps). The BC applies while the bullet's
|
|
67
|
+
current speed is in `[velocity_min_fps, velocity_max_fps)`:
|
|
88
68
|
|
|
89
|
-
Ballistic calculation parameters.
|
|
90
|
-
|
|
91
|
-
**Constructor:**
|
|
92
69
|
```ruby
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
twist_rate_inches, # Barrel twist rate in inches
|
|
103
|
-
is_right_twist # Right-hand twist? (boolean)
|
|
104
|
-
)
|
|
70
|
+
BallisticsEngine.solve(inputs.merge(
|
|
71
|
+
"use_bc_segments" => true,
|
|
72
|
+
"bc_segments_data" => [
|
|
73
|
+
{ "velocity_min_fps" => 1800.0, "velocity_max_fps" => 4000.0, "bc" => 0.243 },
|
|
74
|
+
{ "velocity_min_fps" => 1500.0, "velocity_max_fps" => 1800.0, "bc" => 0.228 },
|
|
75
|
+
{ "velocity_min_fps" => 1200.0, "velocity_max_fps" => 1500.0, "bc" => 0.205 }
|
|
76
|
+
]
|
|
77
|
+
))
|
|
78
|
+
# Also accepts [vmin_fps, vmax_fps, bc] triples.
|
|
105
79
|
```
|
|
106
80
|
|
|
107
|
-
|
|
108
|
-
- All constructor parameters are accessible as read/write attributes
|
|
81
|
+
### Segmented wind (`wind_segments`)
|
|
109
82
|
|
|
110
|
-
|
|
83
|
+
Distance-keyed wind (independent of the velocity-keyed BC above) — e.g. downrange sensors:
|
|
111
84
|
|
|
112
|
-
Wind parameters.
|
|
113
|
-
|
|
114
|
-
**Constructor:**
|
|
115
85
|
```ruby
|
|
116
|
-
|
|
117
|
-
speed_mph,
|
|
118
|
-
|
|
119
|
-
)
|
|
86
|
+
BallisticsEngine.solve(inputs.merge(
|
|
87
|
+
# [speed_mph, angle_degrees, until_yards] (angle 90 = from the right)
|
|
88
|
+
"wind_segments" => [[5.0, 90.0, 150.0], [7.0, 90.0, 300.0], [9.0, 90.0, 1000.0]]
|
|
89
|
+
))
|
|
120
90
|
```
|
|
121
91
|
|
|
122
|
-
|
|
123
|
-
- `speed_mph` - Wind speed in mph (read/write)
|
|
124
|
-
- `direction_degrees` - Wind direction in degrees (read/write)
|
|
92
|
+
### Zero angle and Monte Carlo
|
|
125
93
|
|
|
126
|
-
#### `AtmosphericConditions`
|
|
127
|
-
|
|
128
|
-
Atmospheric parameters.
|
|
129
|
-
|
|
130
|
-
**Constructor:**
|
|
131
94
|
```ruby
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
95
|
+
z = BallisticsEngine.calculate_zero_angle(inputs.merge("target_distance_yards" => 1000.0))
|
|
96
|
+
puts "zero: #{z['zero_angle_moa'].round(2)} MOA"
|
|
97
|
+
|
|
98
|
+
mc = BallisticsEngine.monte_carlo(inputs.merge(
|
|
99
|
+
"num_simulations" => 1000,
|
|
100
|
+
"velocity_std_dev_fps" => 10.0,
|
|
101
|
+
"bc_std_dev" => 0.01,
|
|
102
|
+
"target_distance_yards"=> 1000.0
|
|
103
|
+
))
|
|
104
|
+
puts "hit probability: #{(mc['hit_probability'] * 100).round(1)}%"
|
|
138
105
|
```
|
|
139
106
|
|
|
140
|
-
|
|
141
|
-
- `temperature_f` - Temperature in Fahrenheit (read/write)
|
|
142
|
-
- `pressure_inhg` - Pressure in inHg (read/write)
|
|
143
|
-
- `humidity_percent` - Humidity percentage (read/write)
|
|
144
|
-
- `altitude_feet` - Altitude in feet (read/write)
|
|
145
|
-
|
|
146
|
-
#### `TrajectorySolver`
|
|
147
|
-
|
|
148
|
-
Trajectory calculation engine.
|
|
149
|
-
|
|
150
|
-
**Constructor:**
|
|
151
|
-
```ruby
|
|
152
|
-
TrajectorySolver.new(
|
|
153
|
-
inputs, # BallisticInputs object
|
|
154
|
-
wind = nil, # WindConditions object (optional)
|
|
155
|
-
atmosphere = nil # AtmosphericConditions object (optional)
|
|
156
|
-
)
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
**Methods:**
|
|
160
|
-
- `solve()` - Calculate trajectory, returns `TrajectoryResult`
|
|
161
|
-
|
|
162
|
-
#### `TrajectoryResult`
|
|
163
|
-
|
|
164
|
-
Trajectory calculation results.
|
|
165
|
-
|
|
166
|
-
**Attributes:**
|
|
167
|
-
- `max_range_yards` - Maximum range in yards
|
|
168
|
-
- `max_height_yards` - Maximum height in yards
|
|
169
|
-
- `time_of_flight` - Time of flight in seconds
|
|
170
|
-
- `impact_velocity_fps` - Impact velocity in fps
|
|
171
|
-
- `impact_energy_ftlbs` - Impact energy in ft-lbs
|
|
172
|
-
- `points` - Array of `TrajectoryPoint` objects
|
|
173
|
-
|
|
174
|
-
#### `TrajectoryPoint`
|
|
175
|
-
|
|
176
|
-
Individual point along trajectory.
|
|
177
|
-
|
|
178
|
-
**Attributes:**
|
|
179
|
-
- `time` - Time in seconds
|
|
180
|
-
- `x` - Downrange distance in yards
|
|
181
|
-
- `y` - Vertical position in yards (relative to line of sight)
|
|
182
|
-
- `z` - Lateral drift in yards
|
|
183
|
-
- `velocity_fps` - Velocity in fps
|
|
184
|
-
- `energy_ftlbs` - Energy in ft-lbs
|
|
185
|
-
|
|
186
|
-
#### `DragModel`
|
|
187
|
-
|
|
188
|
-
Ballistic coefficient drag model.
|
|
189
|
-
|
|
190
|
-
**Class Methods:**
|
|
191
|
-
- `DragModel.g1()` - G1 drag model
|
|
192
|
-
- `DragModel.g7()` - G7 drag model
|
|
193
|
-
- `DragModel.g8()` - G8 drag model
|
|
107
|
+
## API Reference
|
|
194
108
|
|
|
109
|
+
All functions live on the `BallisticsEngine` module and take a single hash with **string
|
|
110
|
+
keys** in imperial units. Unknown keys are ignored; only the seven marked required are needed.
|
|
111
|
+
|
|
112
|
+
### `BallisticsEngine.solve(hash) => hash`
|
|
113
|
+
|
|
114
|
+
**Required:** `bc`, `bullet_weight_grains`, `muzzle_velocity_fps`, `bullet_diameter_inches`,
|
|
115
|
+
`bullet_length_inches`, `sight_height_inches`, `zero_distance_yards`.
|
|
116
|
+
|
|
117
|
+
**Optional (selected):**
|
|
118
|
+
- Aim/geometry: `drag_model`("G7"), `shooting_angle_degrees`, `muzzle_angle_degrees`,
|
|
119
|
+
`twist_rate_inches`(10), `is_right_twist`(true), `muzzle_height_inches`, `target_height_inches`.
|
|
120
|
+
- Wind: `wind` = `{speed_mph, direction_degrees}`; `wind_segments` = `[[speed_mph, angle_deg, until_yards], ...]`.
|
|
121
|
+
- Atmosphere: `atmosphere` = `{temperature_f, pressure_inhg, humidity_percent, altitude_feet}`.
|
|
122
|
+
- BC: `use_bc_segments`, `bc_segments_data` (see above), `bc_segments` = `[[mach, bc], ...]`.
|
|
123
|
+
- Coriolis: `enable_coriolis`, `latitude_degrees`, `shot_direction_degrees`.
|
|
124
|
+
- Physics flags: `enable_advanced_effects`, `enable_magnus`, `enable_aerodynamic_jump`,
|
|
125
|
+
`use_enhanced_spin_drift`, `use_form_factor`, `use_cluster_bc`, `enable_pitch_damping`,
|
|
126
|
+
`enable_precession_nutation`, `enable_wind_shear` (+ `wind_shear_model`), `use_rk4`, `use_adaptive_rk45`.
|
|
127
|
+
- Powder: `use_powder_sensitivity`, `powder_temp_sensitivity`, `powder_temp_f`,
|
|
128
|
+
`powder_temp_curve` = `[[temp_f, velocity_fps], ...]`, `powder_curve_temp_f`.
|
|
129
|
+
- Sampling/solver: `enable_trajectory_sampling`, `sample_interval_yards`, `max_range_yards`, `time_step_seconds`.
|
|
130
|
+
|
|
131
|
+
**Returns:** `max_range_yards`, `max_height_yards`, `time_of_flight`, `impact_velocity_fps`,
|
|
132
|
+
`impact_energy_ftlbs`, `points` (array of `{time, x, y, z, velocity_fps, energy_ftlbs}` — x=range,
|
|
133
|
+
y=drop, z=windage, in yards). Present when enabled: `sampled_points`, `aerodynamic_jump`,
|
|
134
|
+
`angular_state`, `max_yaw_angle_rad`, `max_precession_angle_rad`, `min_pitch_damping`, `transonic_mach`.
|
|
135
|
+
|
|
136
|
+
### `BallisticsEngine.calculate_zero_angle(hash) => hash`
|
|
137
|
+
|
|
138
|
+
Same bullet keys as `solve`, plus **required** `target_distance_yards` (and optional
|
|
139
|
+
`target_height_inches`, `wind`, `atmosphere`). Returns `zero_angle_radians`,
|
|
140
|
+
`zero_angle_degrees`, `zero_angle_moa`. Raises on non-convergence.
|
|
141
|
+
|
|
142
|
+
### `BallisticsEngine.monte_carlo(hash) => hash`
|
|
143
|
+
|
|
144
|
+
Bullet keys (optionally nested under `base_inputs`) plus std-dev params:
|
|
145
|
+
`num_simulations`(1000), `velocity_std_dev_fps`, `angle_std_dev_radians`, `bc_std_dev`,
|
|
146
|
+
`wind_speed_std_dev_mph`, `azimuth_std_dev_radians`, `target_distance_yards`,
|
|
147
|
+
`base_wind_speed_mph`, `base_wind_direction_degrees`, `hit_radius_inches`(≈11.8).
|
|
148
|
+
Returns `ranges_yards`, `impact_velocities_fps`, `impact_positions`
|
|
149
|
+
(`[{vertical_inches, lateral_inches}, ...]`), `hit_probability`, `num_simulations`.
|
|
195
150
|
## Development
|
|
196
151
|
|
|
197
152
|
After checking out the repo, run `bundle install` to install dependencies.
|
data/lib/ballistics_engine.rb
CHANGED
data/src/lib.rs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
use magnus::{Error, RHash, function};
|
|
1
|
+
use magnus::{Error, RArray, RHash, function};
|
|
2
2
|
use ballistics_engine::{
|
|
3
|
-
|
|
3
|
+
AtmosphericConditions, BCSegmentData, BallisticInputs, DragModel, MonteCarloParams,
|
|
4
|
+
TrajectorySolver, WindConditions, calculate_zero_angle_with_conditions, run_monte_carlo,
|
|
4
5
|
};
|
|
5
6
|
|
|
6
7
|
// Unit conversion constants
|
|
@@ -9,149 +10,410 @@ const FPS_TO_MPS: f64 = 0.3048;
|
|
|
9
10
|
const YARDS_TO_METERS: f64 = 0.9144;
|
|
10
11
|
const INCHES_TO_METERS: f64 = 0.0254;
|
|
11
12
|
const MPH_TO_MPS: f64 = 0.44704;
|
|
13
|
+
const MPH_TO_KMH: f64 = 1.609344;
|
|
14
|
+
const INHG_TO_HPA: f64 = 33.8639;
|
|
12
15
|
const DEGREES_TO_RADIANS: f64 = std::f64::consts::PI / 180.0;
|
|
16
|
+
const JOULES_TO_FTLBS: f64 = 0.737562;
|
|
17
|
+
const METERS_TO_INCHES: f64 = 39.37007874;
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
let is_right_twist: bool = inputs_hash.lookup2("is_right_twist", true)?;
|
|
29
|
-
let enable_aerodynamic_jump: bool = inputs_hash.lookup2("enable_aerodynamic_jump", false)?;
|
|
30
|
-
|
|
31
|
-
// Coriolis (Earth-rotation) inputs. Requires latitude_degrees to take effect; with
|
|
32
|
-
// enable_coriolis + a latitude, shot_direction_degrees (compass bearing, 0=N, 90=E)
|
|
33
|
-
// drives the lateral drift and the vertical Eotvos term (east lifts, west depresses).
|
|
34
|
-
let enable_coriolis: bool = inputs_hash.lookup2("enable_coriolis", false)?;
|
|
35
|
-
let latitude_degrees: Option<f64> = inputs_hash.lookup::<_, Option<f64>>("latitude_degrees")?;
|
|
36
|
-
let shot_direction_degrees: f64 = inputs_hash.lookup2("shot_direction_degrees", 0.0)?;
|
|
37
|
-
|
|
38
|
-
// Drag model (default to G7)
|
|
39
|
-
let drag_model_str: String = inputs_hash.lookup2("drag_model", "G7")?;
|
|
40
|
-
let drag_model = match drag_model_str.to_uppercase().as_str() {
|
|
41
|
-
"G1" => DragModel::G1,
|
|
42
|
-
"G7" => DragModel::G7,
|
|
43
|
-
"G8" => DragModel::G8,
|
|
44
|
-
_ => return Err(Error::new(ruby.exception_arg_error(), "Invalid drag_model, must be G1, G7, or G8")),
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// Create ballistic inputs using defaults and override specific fields
|
|
48
|
-
let ballistic_inputs = BallisticInputs {
|
|
49
|
-
bc_type: drag_model,
|
|
50
|
-
bc_value: bc,
|
|
51
|
-
bullet_diameter: bullet_diameter_inches * INCHES_TO_METERS,
|
|
52
|
-
bullet_mass: bullet_weight_grains * GRAINS_TO_KG,
|
|
53
|
-
bullet_length: bullet_length_inches * INCHES_TO_METERS,
|
|
54
|
-
muzzle_velocity: muzzle_velocity_fps * FPS_TO_MPS,
|
|
55
|
-
sight_height: sight_height_inches * INCHES_TO_METERS,
|
|
56
|
-
target_distance: zero_distance_yards * YARDS_TO_METERS,
|
|
57
|
-
shooting_angle: shooting_angle_degrees * DEGREES_TO_RADIANS,
|
|
58
|
-
twist_rate: twist_rate_inches, // Already in inches per the struct definition
|
|
59
|
-
is_twist_right: is_right_twist,
|
|
60
|
-
caliber_inches: bullet_diameter_inches,
|
|
61
|
-
weight_grains: bullet_weight_grains,
|
|
62
|
-
enable_aerodynamic_jump,
|
|
63
|
-
enable_coriolis,
|
|
64
|
-
latitude: latitude_degrees,
|
|
65
|
-
shot_azimuth: shot_direction_degrees * DEGREES_TO_RADIANS,
|
|
66
|
-
..Default::default()
|
|
67
|
-
};
|
|
19
|
+
// Typed wrappers around RHash::lookup2 — magnus's lookup2<T,U,V> can't infer the output
|
|
20
|
+
// type V from the default U, so give each call site a concrete return type here.
|
|
21
|
+
fn opt_f64(h: &RHash, key: &str, default: f64) -> Result<f64, Error> {
|
|
22
|
+
h.lookup2(key, default)
|
|
23
|
+
}
|
|
24
|
+
fn opt_bool(h: &RHash, key: &str, default: bool) -> Result<bool, Error> {
|
|
25
|
+
h.lookup2(key, default)
|
|
26
|
+
}
|
|
27
|
+
fn opt_str(h: &RHash, key: &str, default: &str) -> Result<String, Error> {
|
|
28
|
+
h.lookup2(key, default.to_string())
|
|
29
|
+
}
|
|
30
|
+
fn opt_usize(h: &RHash, key: &str, default: usize) -> Result<usize, Error> {
|
|
31
|
+
h.lookup2(key, default)
|
|
32
|
+
}
|
|
68
33
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
34
|
+
fn drag_from_str(ruby: &magnus::Ruby, s: &str) -> Result<DragModel, Error> {
|
|
35
|
+
match s.to_uppercase().as_str() {
|
|
36
|
+
"G1" => Ok(DragModel::G1),
|
|
37
|
+
"G7" => Ok(DragModel::G7),
|
|
38
|
+
"G8" => Ok(DragModel::G8),
|
|
39
|
+
_ => Err(Error::new(
|
|
40
|
+
ruby.exception_arg_error(),
|
|
41
|
+
"Invalid drag_model, must be G1, G7, or G8",
|
|
42
|
+
)),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
73
45
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
46
|
+
/// Build WindConditions from an optional `wind` sub-hash (mph & degrees -> m/s & radians).
|
|
47
|
+
fn build_wind(h: &RHash) -> Result<WindConditions, Error> {
|
|
48
|
+
if let Some(w) = h.lookup::<_, Option<RHash>>("wind")? {
|
|
49
|
+
Ok(WindConditions {
|
|
50
|
+
speed: opt_f64(&w, "speed_mph", 0.0)? * MPH_TO_MPS,
|
|
51
|
+
direction: opt_f64(&w, "direction_degrees", 0.0)? * DEGREES_TO_RADIANS,
|
|
52
|
+
})
|
|
78
53
|
} else {
|
|
79
|
-
WindConditions {
|
|
54
|
+
Ok(WindConditions {
|
|
80
55
|
speed: 0.0,
|
|
81
56
|
direction: 0.0,
|
|
82
|
-
}
|
|
83
|
-
}
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
}
|
|
84
60
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
let
|
|
90
|
-
let
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
let altitude_m = altitude_ft * 0.3048;
|
|
98
|
-
|
|
99
|
-
AtmosphericConditions {
|
|
100
|
-
temperature: temp_c,
|
|
101
|
-
pressure: pressure_hpa,
|
|
102
|
-
humidity,
|
|
103
|
-
altitude: altitude_m,
|
|
104
|
-
}
|
|
61
|
+
/// Build AtmosphericConditions from an optional `atmosphere` sub-hash. Note:
|
|
62
|
+
/// AtmosphericConditions.humidity is PERCENT (0-100), pressure is hPa, temp is Celsius.
|
|
63
|
+
fn build_atmosphere(h: &RHash) -> Result<AtmosphericConditions, Error> {
|
|
64
|
+
if let Some(a) = h.lookup::<_, Option<RHash>>("atmosphere")? {
|
|
65
|
+
let temp_f = opt_f64(&a, "temperature_f", 59.0)?;
|
|
66
|
+
let pressure_inhg = opt_f64(&a, "pressure_inhg", 29.92)?;
|
|
67
|
+
Ok(AtmosphericConditions {
|
|
68
|
+
temperature: (temp_f - 32.0) * 5.0 / 9.0,
|
|
69
|
+
pressure: pressure_inhg * INHG_TO_HPA,
|
|
70
|
+
humidity: opt_f64(&a, "humidity_percent", 50.0)?,
|
|
71
|
+
altitude: opt_f64(&a, "altitude_feet", 0.0)? * FPS_TO_MPS,
|
|
72
|
+
})
|
|
105
73
|
} else {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
pressure: 1013.25, // 1 atm in hPa (NOT Pa)
|
|
74
|
+
Ok(AtmosphericConditions {
|
|
75
|
+
temperature: 15.0,
|
|
76
|
+
pressure: 1013.25,
|
|
110
77
|
humidity: 50.0,
|
|
111
78
|
altitude: 0.0,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// `bc_segments` = array of [mach, bc] pairs (Mach-keyed). Converts directly.
|
|
84
|
+
fn extract_bc_segments(h: &RHash) -> Result<Option<Vec<(f64, f64)>>, Error> {
|
|
85
|
+
match h.lookup::<_, Option<Vec<(f64, f64)>>>("bc_segments")? {
|
|
86
|
+
Some(v) if !v.is_empty() => Ok(Some(v)),
|
|
87
|
+
_ => Ok(None),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// `bc_segments_data` = array of {velocity_min_fps, velocity_max_fps, bc} hashes
|
|
92
|
+
/// OR [vmin_fps, vmax_fps, bc] triples. Velocities stay in FPS: the solver compares
|
|
93
|
+
/// BCSegmentData bounds directly against the bullet's velocity_fps (not m/s).
|
|
94
|
+
/// RHash is not TryConvertOwned, so iterate by index and try the hash form first.
|
|
95
|
+
fn extract_bc_segments_data(h: &RHash) -> Result<Vec<BCSegmentData>, Error> {
|
|
96
|
+
let mut out = Vec::new();
|
|
97
|
+
if let Some(arr) = h.lookup::<_, Option<RArray>>("bc_segments_data")? {
|
|
98
|
+
for i in 0..arr.len() {
|
|
99
|
+
if let Ok(seg) = arr.entry::<RHash>(i as isize) {
|
|
100
|
+
let bc = match seg.lookup::<_, Option<f64>>("bc")? {
|
|
101
|
+
Some(v) => v,
|
|
102
|
+
None => opt_f64(&seg, "bc_value", 0.0)?,
|
|
103
|
+
};
|
|
104
|
+
out.push(BCSegmentData {
|
|
105
|
+
velocity_min: seg.fetch::<_, f64>("velocity_min_fps")?,
|
|
106
|
+
velocity_max: seg.fetch::<_, f64>("velocity_max_fps")?,
|
|
107
|
+
bc_value: bc,
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
let (vmin, vmax, bc): (f64, f64, f64) = arr.entry(i as isize)?;
|
|
111
|
+
out.push(BCSegmentData {
|
|
112
|
+
velocity_min: vmin,
|
|
113
|
+
velocity_max: vmax,
|
|
114
|
+
bc_value: bc,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
112
117
|
}
|
|
118
|
+
}
|
|
119
|
+
Ok(out)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// `wind_segments` = array of [speed_mph, angle_degrees, until_yards] -> (km/h, deg, meters).
|
|
123
|
+
fn extract_wind_segments(h: &RHash) -> Result<Vec<(f64, f64, f64)>, Error> {
|
|
124
|
+
match h.lookup::<_, Option<Vec<(f64, f64, f64)>>>("wind_segments")? {
|
|
125
|
+
Some(v) => Ok(v
|
|
126
|
+
.into_iter()
|
|
127
|
+
.map(|(mph, ang, until_yd)| (mph * MPH_TO_KMH, ang, until_yd * YARDS_TO_METERS))
|
|
128
|
+
.collect()),
|
|
129
|
+
None => Ok(Vec::new()),
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// `powder_temp_curve` = array of [temp_f, velocity_fps] -> [(celsius, m/s)].
|
|
134
|
+
fn extract_powder_curve(h: &RHash) -> Result<Option<Vec<(f64, f64)>>, Error> {
|
|
135
|
+
match h.lookup::<_, Option<Vec<(f64, f64)>>>("powder_temp_curve")? {
|
|
136
|
+
Some(v) if !v.is_empty() => Ok(Some(
|
|
137
|
+
v.into_iter()
|
|
138
|
+
.map(|(tf, vfps)| ((tf - 32.0) * 5.0 / 9.0, vfps * FPS_TO_MPS))
|
|
139
|
+
.collect(),
|
|
140
|
+
)),
|
|
141
|
+
_ => Ok(None),
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Build a fully-populated BallisticInputs from an imperial Ruby hash (imperial -> SI).
|
|
146
|
+
/// Shared by solve, calculate_zero_angle, and monte_carlo.
|
|
147
|
+
fn build_inputs(ruby: &magnus::Ruby, h: &RHash) -> Result<BallisticInputs, Error> {
|
|
148
|
+
let drag_model = drag_from_str(ruby, &opt_str(h, "drag_model", "G7")?)?;
|
|
149
|
+
let bullet_diameter_inches: f64 = h.fetch("bullet_diameter_inches")?;
|
|
150
|
+
let bullet_weight_grains: f64 = h.fetch("bullet_weight_grains")?;
|
|
151
|
+
|
|
152
|
+
let bc_seg_data = extract_bc_segments_data(h)?;
|
|
153
|
+
|
|
154
|
+
let mut inputs = BallisticInputs {
|
|
155
|
+
bc_type: drag_model,
|
|
156
|
+
bc_value: h.fetch("bc")?,
|
|
157
|
+
bullet_diameter: bullet_diameter_inches * INCHES_TO_METERS,
|
|
158
|
+
bullet_mass: bullet_weight_grains * GRAINS_TO_KG,
|
|
159
|
+
bullet_length: h.fetch::<_, f64>("bullet_length_inches")? * INCHES_TO_METERS,
|
|
160
|
+
muzzle_velocity: h.fetch::<_, f64>("muzzle_velocity_fps")? * FPS_TO_MPS,
|
|
161
|
+
sight_height: h.fetch::<_, f64>("sight_height_inches")? * INCHES_TO_METERS,
|
|
162
|
+
target_distance: h.fetch::<_, f64>("zero_distance_yards")? * YARDS_TO_METERS,
|
|
163
|
+
// geometry / aim
|
|
164
|
+
shooting_angle: opt_f64(h, "shooting_angle_degrees", 0.0)? * DEGREES_TO_RADIANS,
|
|
165
|
+
muzzle_angle: opt_f64(h, "muzzle_angle_degrees", 0.0)? * DEGREES_TO_RADIANS,
|
|
166
|
+
azimuth_angle: opt_f64(h, "azimuth_angle_degrees", 0.0)? * DEGREES_TO_RADIANS,
|
|
167
|
+
twist_rate: opt_f64(h, "twist_rate_inches", 10.0)?, // inches/turn, NOT SI
|
|
168
|
+
is_twist_right: opt_bool(h, "is_right_twist", true)?,
|
|
169
|
+
caliber_inches: bullet_diameter_inches,
|
|
170
|
+
weight_grains: bullet_weight_grains,
|
|
171
|
+
muzzle_height: opt_f64(h, "muzzle_height_inches", 0.0)? * INCHES_TO_METERS,
|
|
172
|
+
target_height: opt_f64(h, "target_height_inches", 0.0)? * INCHES_TO_METERS,
|
|
173
|
+
// physics flags
|
|
174
|
+
enable_aerodynamic_jump: opt_bool(h, "enable_aerodynamic_jump", false)?,
|
|
175
|
+
enable_advanced_effects: opt_bool(h, "enable_advanced_effects", false)?,
|
|
176
|
+
enable_magnus: opt_bool(h, "enable_magnus", false)?,
|
|
177
|
+
enable_coriolis: opt_bool(h, "enable_coriolis", false)?,
|
|
178
|
+
latitude: h.lookup::<_, Option<f64>>("latitude_degrees")?,
|
|
179
|
+
shot_azimuth: opt_f64(h, "shot_direction_degrees", 0.0)? * DEGREES_TO_RADIANS,
|
|
180
|
+
use_enhanced_spin_drift: opt_bool(h, "use_enhanced_spin_drift", false)?,
|
|
181
|
+
use_form_factor: opt_bool(h, "use_form_factor", false)?,
|
|
182
|
+
use_cluster_bc: opt_bool(h, "use_cluster_bc", false)?,
|
|
183
|
+
enable_pitch_damping: opt_bool(h, "enable_pitch_damping", false)?,
|
|
184
|
+
enable_precession_nutation: opt_bool(h, "enable_precession_nutation", false)?,
|
|
185
|
+
use_rk4: opt_bool(h, "use_rk4", true)?,
|
|
186
|
+
use_adaptive_rk45: opt_bool(h, "use_adaptive_rk45", true)?,
|
|
187
|
+
tipoff_yaw: opt_f64(h, "tipoff_yaw_degrees", 0.0)? * DEGREES_TO_RADIANS,
|
|
188
|
+
tipoff_decay_distance: opt_f64(h, "tipoff_decay_distance_yards", 50.0)? * YARDS_TO_METERS,
|
|
189
|
+
// powder sensitivity
|
|
190
|
+
use_powder_sensitivity: opt_bool(h, "use_powder_sensitivity", false)?,
|
|
191
|
+
powder_temp_sensitivity: opt_f64(h, "powder_temp_sensitivity", 0.0)?, // (m/s)/C
|
|
192
|
+
powder_temp: (opt_f64(h, "powder_temp_f", 59.0)? - 32.0) * 5.0 / 9.0,
|
|
193
|
+
powder_temp_curve: extract_powder_curve(h)?,
|
|
194
|
+
powder_curve_temp_c: h
|
|
195
|
+
.lookup::<_, Option<f64>>("powder_curve_temp_f")?
|
|
196
|
+
.map(|tf| (tf - 32.0) * 5.0 / 9.0),
|
|
197
|
+
// wind shear
|
|
198
|
+
enable_wind_shear: opt_bool(h, "enable_wind_shear", false)?,
|
|
199
|
+
wind_shear_model: opt_str(h, "wind_shear_model", "none")?,
|
|
200
|
+
// sampling
|
|
201
|
+
enable_trajectory_sampling: opt_bool(h, "enable_trajectory_sampling", false)?,
|
|
202
|
+
// BC segments
|
|
203
|
+
use_bc_segments: opt_bool(h, "use_bc_segments", false)?,
|
|
204
|
+
bc_segments: extract_bc_segments(h)?,
|
|
205
|
+
bc_segments_data: if bc_seg_data.is_empty() {
|
|
206
|
+
None
|
|
207
|
+
} else {
|
|
208
|
+
Some(bc_seg_data)
|
|
209
|
+
},
|
|
210
|
+
// metadata
|
|
211
|
+
manufacturer: h.lookup::<_, Option<String>>("manufacturer")?,
|
|
212
|
+
bullet_model: h.lookup::<_, Option<String>>("bullet_model")?,
|
|
213
|
+
bullet_id: h.lookup::<_, Option<String>>("bullet_id")?,
|
|
214
|
+
bullet_cluster: h.lookup::<_, Option<usize>>("bullet_cluster")?,
|
|
215
|
+
..Default::default()
|
|
113
216
|
};
|
|
114
217
|
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
let
|
|
118
|
-
.
|
|
218
|
+
// ground_threshold / sample_interval have non-trivial engine defaults (-100 m / 10 m);
|
|
219
|
+
// only override them if the caller supplied a value (in yards -> meters).
|
|
220
|
+
if let Some(gt) = h.lookup::<_, Option<f64>>("ground_threshold_yards")? {
|
|
221
|
+
inputs.ground_threshold = gt * YARDS_TO_METERS;
|
|
222
|
+
}
|
|
223
|
+
if let Some(si) = h.lookup::<_, Option<f64>>("sample_interval_yards")? {
|
|
224
|
+
inputs.sample_interval = si * YARDS_TO_METERS;
|
|
225
|
+
}
|
|
119
226
|
|
|
120
|
-
|
|
121
|
-
|
|
227
|
+
Ok(inputs)
|
|
228
|
+
}
|
|
122
229
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
230
|
+
/// Calculate a trajectory from an imperial Ruby hash. Returns a Ruby hash.
|
|
231
|
+
fn solve_trajectory(ruby: &magnus::Ruby, h: RHash) -> Result<RHash, Error> {
|
|
232
|
+
let inputs = build_inputs(ruby, &h)?;
|
|
233
|
+
let wind = build_wind(&h)?;
|
|
234
|
+
let atmosphere = build_atmosphere(&h)?;
|
|
128
235
|
|
|
129
|
-
|
|
130
|
-
let
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
236
|
+
let mut solver = TrajectorySolver::new(inputs, wind, atmosphere);
|
|
237
|
+
let segs = extract_wind_segments(&h)?;
|
|
238
|
+
if !segs.is_empty() {
|
|
239
|
+
solver.set_wind_segments(segs);
|
|
240
|
+
}
|
|
241
|
+
if let Some(mr) = h.lookup::<_, Option<f64>>("max_range_yards")? {
|
|
242
|
+
solver.set_max_range(mr * YARDS_TO_METERS);
|
|
243
|
+
}
|
|
244
|
+
if let Some(ts) = h.lookup::<_, Option<f64>>("time_step_seconds")? {
|
|
245
|
+
solver.set_time_step(ts);
|
|
246
|
+
}
|
|
137
247
|
|
|
138
|
-
|
|
139
|
-
|
|
248
|
+
let result = solver
|
|
249
|
+
.solve()
|
|
250
|
+
.map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
|
|
140
251
|
|
|
141
|
-
|
|
142
|
-
|
|
252
|
+
let out = ruby.hash_new();
|
|
253
|
+
out.aset("max_range_yards", result.max_range / YARDS_TO_METERS)?;
|
|
254
|
+
out.aset("max_height_yards", result.max_height / YARDS_TO_METERS)?;
|
|
255
|
+
out.aset("time_of_flight", result.time_of_flight)?;
|
|
256
|
+
out.aset("impact_velocity_fps", result.impact_velocity / FPS_TO_MPS)?;
|
|
257
|
+
out.aset("impact_energy_ftlbs", result.impact_energy * JOULES_TO_FTLBS)?;
|
|
143
258
|
|
|
144
|
-
|
|
259
|
+
let points = ruby.ary_new();
|
|
260
|
+
for point in &result.points {
|
|
261
|
+
let ph = ruby.hash_new();
|
|
262
|
+
ph.aset("time", point.time)?;
|
|
263
|
+
ph.aset("x", point.position.x / YARDS_TO_METERS)?;
|
|
264
|
+
ph.aset("y", point.position.y / YARDS_TO_METERS)?;
|
|
265
|
+
ph.aset("z", point.position.z / YARDS_TO_METERS)?;
|
|
266
|
+
ph.aset("velocity_fps", point.velocity_magnitude / FPS_TO_MPS)?;
|
|
267
|
+
ph.aset("energy_ftlbs", point.kinetic_energy * JOULES_TO_FTLBS)?;
|
|
268
|
+
points.push(ph)?;
|
|
145
269
|
}
|
|
270
|
+
out.aset("points", points)?;
|
|
146
271
|
|
|
147
|
-
|
|
272
|
+
// Rich output (present only when the corresponding feature produced Some).
|
|
273
|
+
if let Some(samples) = &result.sampled_points {
|
|
274
|
+
let arr = ruby.ary_new();
|
|
275
|
+
for s in samples {
|
|
276
|
+
let sh = ruby.hash_new();
|
|
277
|
+
sh.aset("distance_yards", s.distance_m / YARDS_TO_METERS)?;
|
|
278
|
+
sh.aset("drop_inches", s.drop_m * METERS_TO_INCHES)?;
|
|
279
|
+
sh.aset("wind_drift_inches", s.wind_drift_m * METERS_TO_INCHES)?;
|
|
280
|
+
sh.aset("velocity_fps", s.velocity_mps / FPS_TO_MPS)?;
|
|
281
|
+
sh.aset("energy_ftlbs", s.energy_j * JOULES_TO_FTLBS)?;
|
|
282
|
+
sh.aset("time", s.time_s)?;
|
|
283
|
+
let flags = ruby.ary_new();
|
|
284
|
+
for f in &s.flags {
|
|
285
|
+
flags.push(format!("{:?}", f))?;
|
|
286
|
+
}
|
|
287
|
+
sh.aset("flags", flags)?;
|
|
288
|
+
arr.push(sh)?;
|
|
289
|
+
}
|
|
290
|
+
out.aset("sampled_points", arr)?;
|
|
291
|
+
}
|
|
292
|
+
if let Some(aj) = &result.aerodynamic_jump {
|
|
293
|
+
let jh = ruby.hash_new();
|
|
294
|
+
jh.aset("vertical_jump_moa", aj.vertical_jump_moa)?;
|
|
295
|
+
jh.aset("horizontal_jump_moa", aj.horizontal_jump_moa)?;
|
|
296
|
+
jh.aset("jump_angle_rad", aj.jump_angle_rad)?;
|
|
297
|
+
jh.aset("magnus_component_moa", aj.magnus_component_moa)?;
|
|
298
|
+
jh.aset("yaw_component_moa", aj.yaw_component_moa)?;
|
|
299
|
+
jh.aset("stabilization_factor", aj.stabilization_factor)?;
|
|
300
|
+
out.aset("aerodynamic_jump", jh)?;
|
|
301
|
+
}
|
|
302
|
+
if let Some(a) = &result.angular_state {
|
|
303
|
+
let ah = ruby.hash_new();
|
|
304
|
+
ah.aset("pitch_angle", a.pitch_angle)?;
|
|
305
|
+
ah.aset("yaw_angle", a.yaw_angle)?;
|
|
306
|
+
ah.aset("pitch_rate", a.pitch_rate)?;
|
|
307
|
+
ah.aset("yaw_rate", a.yaw_rate)?;
|
|
308
|
+
ah.aset("precession_angle", a.precession_angle)?;
|
|
309
|
+
ah.aset("nutation_phase", a.nutation_phase)?;
|
|
310
|
+
out.aset("angular_state", ah)?;
|
|
311
|
+
}
|
|
312
|
+
if let Some(v) = result.max_yaw_angle {
|
|
313
|
+
out.aset("max_yaw_angle_rad", v)?;
|
|
314
|
+
}
|
|
315
|
+
if let Some(v) = result.max_precession_angle {
|
|
316
|
+
out.aset("max_precession_angle_rad", v)?;
|
|
317
|
+
}
|
|
318
|
+
if let Some(v) = result.min_pitch_damping {
|
|
319
|
+
out.aset("min_pitch_damping", v)?;
|
|
320
|
+
}
|
|
321
|
+
if let Some(v) = result.transonic_mach {
|
|
322
|
+
out.aset("transonic_mach", v)?;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
Ok(out)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/// Solve for the launch angle that zeroes at a target distance.
|
|
329
|
+
fn calculate_zero_angle(ruby: &magnus::Ruby, h: RHash) -> Result<RHash, Error> {
|
|
330
|
+
let inputs = build_inputs(ruby, &h)?;
|
|
331
|
+
let target_yd: f64 = h.fetch("target_distance_yards")?;
|
|
332
|
+
let target_h_in: f64 = h.lookup2("target_height_inches", 0.0)?;
|
|
333
|
+
let wind = build_wind(&h)?;
|
|
334
|
+
let atmosphere = build_atmosphere(&h)?;
|
|
335
|
+
|
|
336
|
+
let rad = calculate_zero_angle_with_conditions(
|
|
337
|
+
inputs,
|
|
338
|
+
target_yd * YARDS_TO_METERS,
|
|
339
|
+
target_h_in * INCHES_TO_METERS,
|
|
340
|
+
wind,
|
|
341
|
+
atmosphere,
|
|
342
|
+
)
|
|
343
|
+
.map_err(|e| {
|
|
344
|
+
Error::new(
|
|
345
|
+
ruby.exception_runtime_error(),
|
|
346
|
+
format!("Unable to find zero angle for target distance {target_yd}: {e}"),
|
|
347
|
+
)
|
|
348
|
+
})?;
|
|
148
349
|
|
|
149
|
-
|
|
350
|
+
let out = ruby.hash_new();
|
|
351
|
+
out.aset("zero_angle_radians", rad)?;
|
|
352
|
+
out.aset("zero_angle_degrees", rad / DEGREES_TO_RADIANS)?;
|
|
353
|
+
out.aset("zero_angle_moa", rad / DEGREES_TO_RADIANS * 60.0)?;
|
|
354
|
+
Ok(out)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/// Monte Carlo dispersion. Reads `base_inputs` sub-hash (or the top-level hash) for the
|
|
358
|
+
/// bullet, plus std-dev params, and returns per-shot ranges/velocities/positions + hit prob.
|
|
359
|
+
fn monte_carlo(ruby: &magnus::Ruby, h: RHash) -> Result<RHash, Error> {
|
|
360
|
+
let base = match h.lookup::<_, Option<RHash>>("base_inputs")? {
|
|
361
|
+
Some(b) => b,
|
|
362
|
+
None => h,
|
|
363
|
+
};
|
|
364
|
+
let inputs = build_inputs(ruby, &base)?;
|
|
365
|
+
|
|
366
|
+
let params = MonteCarloParams {
|
|
367
|
+
num_simulations: opt_usize(&h, "num_simulations", 1000)?,
|
|
368
|
+
velocity_std_dev: opt_f64(&h, "velocity_std_dev_fps", 0.0)? * FPS_TO_MPS,
|
|
369
|
+
angle_std_dev: opt_f64(&h, "angle_std_dev_radians", 0.001)?,
|
|
370
|
+
bc_std_dev: opt_f64(&h, "bc_std_dev", 0.01)?,
|
|
371
|
+
wind_speed_std_dev: opt_f64(&h, "wind_speed_std_dev_mph", 0.0)? * MPH_TO_MPS,
|
|
372
|
+
target_distance: h
|
|
373
|
+
.lookup::<_, Option<f64>>("target_distance_yards")?
|
|
374
|
+
.map(|y| y * YARDS_TO_METERS),
|
|
375
|
+
base_wind_speed: opt_f64(&h, "base_wind_speed_mph", 0.0)? * MPH_TO_MPS,
|
|
376
|
+
base_wind_direction: opt_f64(&h, "base_wind_direction_degrees", 0.0)? * DEGREES_TO_RADIANS,
|
|
377
|
+
azimuth_std_dev: opt_f64(&h, "azimuth_std_dev_radians", 0.001)?,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
let res = run_monte_carlo(inputs, params)
|
|
381
|
+
.map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
|
|
382
|
+
|
|
383
|
+
let out = ruby.hash_new();
|
|
384
|
+
out.aset(
|
|
385
|
+
"ranges_yards",
|
|
386
|
+
res.ranges
|
|
387
|
+
.iter()
|
|
388
|
+
.map(|m| m / YARDS_TO_METERS)
|
|
389
|
+
.collect::<Vec<f64>>(),
|
|
390
|
+
)?;
|
|
391
|
+
out.aset(
|
|
392
|
+
"impact_velocities_fps",
|
|
393
|
+
res.impact_velocities
|
|
394
|
+
.iter()
|
|
395
|
+
.map(|v| v / FPS_TO_MPS)
|
|
396
|
+
.collect::<Vec<f64>>(),
|
|
397
|
+
)?;
|
|
398
|
+
let positions = ruby.ary_new();
|
|
399
|
+
for p in &res.impact_positions {
|
|
400
|
+
let ph = ruby.hash_new();
|
|
401
|
+
ph.aset("vertical_inches", p.y * METERS_TO_INCHES)?; // McCoy Y = vertical
|
|
402
|
+
ph.aset("lateral_inches", p.z * METERS_TO_INCHES)?; // McCoy Z = lateral
|
|
403
|
+
positions.push(ph)?;
|
|
404
|
+
}
|
|
405
|
+
out.aset("impact_positions", positions)?;
|
|
406
|
+
let radius_m = opt_f64(&h, "hit_radius_inches", 0.3 * METERS_TO_INCHES)? / METERS_TO_INCHES;
|
|
407
|
+
out.aset("hit_probability", res.hit_probability(radius_m))?;
|
|
408
|
+
out.aset("num_simulations", res.ranges.len())?;
|
|
409
|
+
Ok(out)
|
|
150
410
|
}
|
|
151
411
|
|
|
152
412
|
#[magnus::init]
|
|
153
413
|
fn init(ruby: &magnus::Ruby) -> Result<(), Error> {
|
|
154
414
|
let module = ruby.define_module("BallisticsEngine")?;
|
|
155
415
|
module.define_module_function("solve", function!(solve_trajectory, 1))?;
|
|
416
|
+
module.define_module_function("calculate_zero_angle", function!(calculate_zero_angle, 1))?;
|
|
417
|
+
module.define_module_function("monte_carlo", function!(monte_carlo, 1))?;
|
|
156
418
|
Ok(())
|
|
157
419
|
}
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ballistics-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.22.
|
|
4
|
+
version: 0.22.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex Jokela
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|