-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathpropNav.py
More file actions
2427 lines (2144 loc) · 95.6 KB
/
Copy pathpropNav.py
File metadata and controls
2427 lines (2144 loc) · 95.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
# pylint: disable=trailing-whitespace,bad-whitespace,invalid-name
# pylint: disable=anomalous-backslash-in-string,bad-continuation
# pylint: disable=multiple-statements,redefined-outer-name,global-statement
""" File Header Comment Block
#
# File: propNav.py
# Auth: Gary E. Deschaines
# Date: 25 Oct 2023
# Prog: Proportional navigation guidance missile flyout model
# Desc: Application of selectable proportional navigation guidance laws
# (True, Pure, ZEM, or Augmented PN) for missile engagement
# of a target. 3-DOF point mass kinematic model for missile and
# target. Ideal missile seeker and control: no sensor range or
# field-of-view (FOV) limits, no measurement errors, no lag with
# 100% effective, but bounded commanded acceleration, and perfect
# command response.
#
# Note: Translated from a Mathcad 3-DOF kinematic ideal proportional
# navigation guidance missile flyout model developed in 1997.
# The inertial (fixed) reference frame Cartesian (+X, +Y, +Z)
# coordinate system in the Mathcad model correlates with (East,
# North, Up), while translational/rotational missile and target
# body frame (+x, +y, +z) axes follow the (forward, right, down)
# convention. Care must be taken in transforming displacement
# and directional vectors between these coordinate frames, and
# describing rotational directions. Specifically, since missile
# and target body frame +x axes are aligned with their respective
# inertial velocity vectors, positive azimuth rotation is negative
# body yaw, while positive elevation rotation is positive body
# pitch. Thus, positive accelerations normal to body frame +x
# axis are those resulting from positive yaw or pitch rates
# crossed with the body frame inertial velocity vector. The +/-
# missile line-of-sigt (LOS) rates and normal accelerations will
# be evident in associated profile plots.
#
# Refs:
#
# [1] Paul Zarchan and A. Richard Seebass (Editor-in-Chief),
# "Tactical and Strategic Missile Guidance (Progress in
# Astronautics and Aeronautics, Vol 124)", American
# Institute of Aeronautics and Astronautics, Washington,
# D.C., 1990.
#
# [2] Donald T. Greenwood, "Principles of Dynamics", Prentice-Hall,
# Inc. of Englewood Clifts, New Jersey, 1965.
#
# [3] Neil F. Palumbo, Ross A. Blauwkamp, and Justin M. Lloyd,
# "Basic Principles of Homing Guidance", rev 2018, Johns
# Hopkins APL Technical Digest, VOL 29, No 1, 2010. Web
# available at secwww.jhuapl.edu/techdigest:
# https://secwww.jhuapl.edu/techdigest/Content/techdigest/pdf/V29-N01/29-01-Palumbo_Principles_Rev2018.pdf
#
# [4] Ben Dickinson, "Missile Guidance Fundamentals Tutorial",
# last updated Oct. 15, 2023. Web available at www.youtube.com:
# https://www.youtube.com/playlist?list=PLcmbTy9X3gXt02z1wNy4KF5ui0tKxdQm7
#
# [5] Ben Dickinson, "Guidance from Optimal Control",
# last updated Apr. 2, 2023. Web available at www.youtube.com:
# https://www.youtube.com/playlist?list=PLcmbTy9X3gXsh-o1W60E7rEA35igNj__q
#
# [6] Farham A. Faruqi, "Integrated Navigation, Guidance, and
# Control of Missile Systems: 3-D Dynamic Model", Weapon
# Systems Division DSTO, DSTO-TR-2805, Feb., 2013. Web
# available at www.dst.defence.gov.au:
# https://www.dst.defence.gov.au/publication/integrated-navigation-guidance-and-control-missile-systems-3-d-dynamic-model
#
# [7] David Hosier, "Avoiding Gimbal Lock in a Trajectory Simulation",
# U.S. Army Armament Research, Development and Engineering Center
# ARDEC, METC, Technical Report ARMET-TR-17051, Picatinny Arsenal, New
# Jersey, July 2018. Web Available at discover.dtic.mil:
# https://apps.dtic.mil/sti/pdfs/AD1055301.pdf
#
# [8] Neil F. Palumbo, Ross A. Blauwkamp, and Justin M. Lloyd,
# "Modern Homing Missile Guidance Theory and Techniques", rev 2018, Johns
# Hopkins APL Technical Digest, VOL 29, No 1, 2010. Web
# available at secwww.jhuapl.edu/techdigest:
# https://secwww.jhuapl.edu/techdigest/Content/techdigest/pdf/V29-N01/29-01-Palumbo_Homing.pdf
#
# [9] Ben Dickinson, "Time to Go Estimation - Guidance Fundamentals II - Section 1.1",
# last updated Jan. 6, 2024. Web available at www.youtube.com:
# https://youtu.be/sbcPfnm30vA?si=nngS_KMwzqJyxMv3
#
# Disclaimer:
#
# See DISCLAIMER file.
#
"""
###
### Module Imports
###
import sys
import time
from math import ceil, floor, cos, sin, acos, asin, atan, atan2, pi, sqrt
from io import StringIO
#from locale import format_string
import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)
try:
import numpy as np
import numpy.linalg as la
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from mpl_toolkits.mplot3d.art3d import Line3D
except ImportError:
print("* Error: NumPy and Matplotlib packages required.")
sys.exit()
try:
from RK4_Solver import RK4_Solver
except ImportError:
print("* Error: RK4_Solver class required.")
sys.exit()
###
### File/Module Scope Constants and Globals
###
RPD = atan(1.0)/45.0 # radians per degree
DPR = 1.0/RPD # degrees per radian
g = 9.80665 # gravitational acceleration at sea-level (meters/s/s)
# Unit vectors for inertial Cartesian frame X,Y,Z axes.
global Uxi, Uyi, Uzi
Uxi = np.array([1.0, 0.0, 0.0])
Uyi = np.array([0.0, 1.0, 0.0])
Uzi = np.array([0.0, 0.0, 1.0])
###
### Processing Control Options - Output Flags, PN Guidance Type,
### Missile/Target Engagement Options and Initial States
###
### To best utilize this program from within an IDE such as Spyder, keep
### all code blocks following this section folded. Edit contents of this
### section to specify:
###
### + Processing output flags (PRINT_DATA, PLOT_DATA, SHOW_ANIM,
### SAVE_ANIM, PRINT_TXYZ) and plot figure flags (PLOT_FIGS)
### + Missile type (SAM or AAM)
### + Proportional navigation guidance type (True, Pure, ZEM, ATPN,
### APPN or AZEM) and navigation constant (Nm)
### + Target turning or weave acceleration (Nt)
### + Target initial position and velocity (Pt0, Vt0)
### + Missile initial position (Pm0), velocity magnitude (magVm),
### and launch lead or heading error angles (maz, mel)
### + Target rotation direction vector or weave rate (UWt, Wt)
### + Integration step size and stop time (T_STEP, T_STOP)
###
# Set Processing output control flags.
PRINT_DATA = False # Controls printing of collected data (for debugging)
PLOT_DATA = False # Controls plotting of collected data
SHOW_ANIM = True # Controls showing interactive 3D engagement animation
SAVE_ANIM = False # Controls saving/showing 3D engagement animation
PRINT_TXYZ = False # Controls printing TXYZ.OUT file
PLOT_FIGS = { 1:True, # Closing distance at tStop
2:True, # XY plan view of intercept geometry at tStop
3:True, # XZ profile view of intercept geometry at tStop
4:True, # XY plan view of missile/target engagement
5:True, # XZ profile view of missile/target engagement
6:True, # Msl/Tgt velocity magnitude vs time of flight
7:True, # Msl/Tgt acceleration vs time of flight
8:True, # Line-of-Sight rate vs time of flight
9:True, # Closing velocity vs time of flight
10:True, # Zero Effort Miss distance vs time of flight
11:True, # Target offset sines wrt missile +x axis
12:True, # Rotational energy vs time of flight plots
13:True, # 3D missile/target engagement trajectories plot
}
"""
# Plot only selected figures.
for ifig in PLOT_FIGS.keys():
PLOT_FIGS[ifig] = False
PLOT_FIGS[4] = True
PLOT_FIGS[5] = True
PLOT_FIGS[6] = True
PLOT_FIGS[7] = True
PLOT_FIGS[8] = True
PLOT_FIGS[9] = True
PLOT_FIGS[10] = True
PLOT_FIGS[12] = True
"""
# Set missile type and acceleration maximum.
SAM = 1 # For engagements described in Sample Cases section of propNav README.
AAM = 2 # For engagements presented in Section 3, Modules 3 & 4, Section 4,
# Module 4 of ref [4], and Section 2, Module 3 of ref [5].
MSL = SAM
global Ammax
Gmmax = {SAM:8, AAM:30} # maximum missile acceleration (g's)
Ammax = Gmmax[MSL]*g # maximum missile acceleration (meters/s/s)
# Set minimum miss distance (meters).
global MinMissDist
if MSL == SAM:
MinMissDist = 3.0
else:
MinMissDist = 6.0
# Proportional Navigation law (method) selection.
PN_TRUE = 1 # With guidance command preservation (GCP1) per ref [3]
PN_PURE = 2
PN_ZEM = 3 # Zero Effort Miss per Section 3, Module 4 of ref [4]
PN_ATPN = 4 # Augmented True Proportional Navigation per ref [6]
PN_APPN = 5 # Augmented Pure Proportional Navigation per ref [6]
PN_AZEM = 6 # Augmented Zero Effort Miss per Section 2, Module 2 of ref [5]
PN_LAWS = {PN_TRUE:'True', PN_PURE:'Pure', PN_ZEM:'ZEM',
PN_ATPN:'ATPN', PN_APPN:'APPN', PN_AZEM:'AZEM'}
PNAV = PN_PURE
global Nm, Nt, Wt
Nm = 4 # proportional navigation constant
Nt = 3.0 # target turning acceleration (g's)
Wt = 0.0 # target weave rate (rad/sec)
# Define target and missile initial states.
if MSL == SAM:
Pt0 = np.array([ 2000.0, 0.0, 500.0]) # 0.0k offset and
Vt0 = np.array([ 0.0, 200.0, 0.0]) # crossing
#Pt0 = np.array([ 2000.0, 500.0, 500.0]) # 0.5k offset and
#Vt0 = np.array([ -200.0, 0.0, 0.0]) # inbound
Pm0 = np.array([ 0.0, 0.0, 2.0])
magVm = 450.0
else:
if Wt > 0.0:
# For Section 5, Module 2 of ref [4].
Pt0 = np.array([12192.0, 6096.0, 3048.0])
Vt0 = np.array([ -304.8, -Nt*g/Wt, 0.0])
Pm0 = np.array([ 0.0, 6096.0, 3048.0])
magVm = 914.4
elif ((int(Nt) == 3) and (int(Nm) >= 2)) and \
((PNAV == PN_TRUE) or (PNAV == PN_ATPN) or \
(PNAV == PN_APPN) or (PNAV == PN_AZEM)):
# for Section 2, Module 3 of ref [5].
Pt0 = np.array([ 9144.0, 6096.0, 3048.0])
Vt0 = np.array([ -304.8, 0.0, 0.0])
Pm0 = np.array([ 0.0, 6096.0, 3048.0])
magVm = 457.2
else:
# For Section 3, Modules 3 & 4, Section 4, Module 4 of ref [4],
# and Section 1.1 of ref [9].
Pt0 = np.array([12192.0, 6096.0, 3048.0])
Vt0 = np.array([ -304.8, 0.0, 0.0])
Pm0 = np.array([ 0.0, 6096.0, 3048.0])
magVm = 914.4
# Verify missile is faster than target.
magVt = la.norm(Vt0)
if magVm <= magVt:
print("Error: magVm= %8.2f <= magVt= %8.2f;\n %s" % \
(magVm, magVt, "Missile must be faster than target."))
sys.exit()
# Missile lead azimuth and elevation angles in degrees.
# Set to None for calculation of lead angles based on
# estimation of time-to-intercept of a non-maneuvering
# target with constant velocity and heading.
if MSL == SAM:
# Point missile at target's current position.
maz = atan2((Pt0[1]-Pm0[1]),(Pt0[0]-Pm0[0]))*DPR
mel = atan((Pt0[2]-Pm0[2])/sqrt((Pt0[0]-Pm0[0])**2 + (Pt0[1]-Pm0[1])**2))*DPR
# Add lead angles.
#maz += 4.0
#mel += 2.0
maz = 10.0
mel = 12.0
#maz = None
#mel = None
else:
maz = 0.0
mel = 0.0
# Define target turning/climbing rotation axis unit vector.
global UWt
if MSL == SAM:
UWt = Uzi # for level leftward turn in XY plane
#UWt = -Uzi # for level rightward turn in XY plane
#UWt = np.array([-0.2418, -0.2418, -0.9397]) # shallow diving leftward turn
#UWt = np.array([-0.2418, -0.2418, 0.9397]) # shallow diving rightward turn
#UWt = np.array([-0.5657, 0.5657, -0.6000]) # steep climbing leftward turn
#UWt = np.array([-0.5657, 0.5657, 0.6000]) # steep climbing rightward turn
else:
if Wt > 0.0:
UWt = -Uxi # for weave maneuver
else:
UWt = Uyi # for climbing turn in XZ plane
#UWt = -Uyi # for diving turn in XZ plane
#UWt = np.array([0.5657, 0.6000, 0.5657]) # steep rightward climbing turn
#UWt = np.array([0.0000, 0.2500, 0.9682]) # shallow rightward climbing turn
#UWt = np.array([0.5657,-0.6000,-0.5657]) # steep leftward diving turn
#UWt = np.array([0.0000,-0.2500,-0.9682]) # shallow leftward diving turn
UWt = UWt/la.norm(UWt)
# Set integration time step size and simulation stop time (sec).
T_STEP = 0.005
if MSL == SAM:
T_STOP = 8.0
else:
if Wt > 0.0:
# For Section 5, Module 2 of ref [4].
T_STOP = 11.0
elif ((int(Nt) == 3) and (int(Nm) >= 2)) and \
((PNAV == PN_TRUE) or (PNAV == PN_ATPN) or \
(PNAV == PN_APPN) or (PNAV == PN_AZEM)):
# For Section 2, Module 3 of ref [5].
T_STOP = 15.5
elif (int(Nt) == 6) and \
((PNAV == PN_ATPN) or (PNAV == PN_APPN) or (PNAV == PN_AZEM)):
# For Section 1.1 of ref [9].
T_STOP = 13.5
else:
# For Section 3, Modules 3 & 4, Section 4, Module 4 of ref [4].
T_STOP = 13.0
###
### Procedures for 3-DOF Kinematic Relative Position, Orientation
### and Motion in 3D Cartesian Frames
###
def Uvec(V):
"""
Returns unit direction vector of given vector V.
Parameters
----------
V : float 3-vector
Vector.
Returns
-------
U : float 3-vector
Unit direction vector of V.
"""
magV = la.norm(V)
if magV > 0.0: U = V / magV
else: U = np.array([0.0, 0.0, 0.0])
return U
def Prel(Pt, Pm):
return Pt - Pm
def Vrel(Vt, Vm):
return Vt - Vm
def Mrot(psi, tht, phi):
"""
Euler angle (yaw,pitch,roll) rotation transformation matrix
for inertial frame Cartesian coordinates (Xi,Yi,Zi) to body
frame coordinates (xb,yb,zb) derived from equation (7-166)
on pg 335 of ref [2] (same as that presented in Figure 3 on
pg 6 of ref [6]). In the derivation below, consider the
xyz inertial frame depicted in Figure 3 on pg 6 of ref [6]
rotated 180 deg counter-clockwise about the +yi axis so the
+zi axis points toward top of the page, then the following
relationships exist between azimuth, elevation and bank
angles in propNav inertial frame and psi, theta, phi in
the figure:
psi = azimuth
theta = -elevation
phi = bank angle
Note: Yaw, pitch and roll are expected to be in radians,
NOT degrees!
Use as: [xb, yb, zb] = Numpy.matmul(Mbi, [Xi, Yi, Zi])
"""
cpsi = cos(psi)
spsi = sin(psi)
ctht = cos(-tht)
stht = sin(-tht)
cphi = cos(phi)
sphi = sin(phi)
M = np.zeros([3,3])
M[0,0] = cpsi*ctht
M[0,1] = spsi*ctht
M[0,2] = -stht
M[1,0] = -spsi*cphi + cpsi*stht*sphi
M[1,1] = cpsi*cphi + spsi*stht*sphi
M[1,2] = ctht*sphi
M[2,0] = spsi*sphi + cpsi*stht*cphi
M[2,1] = -cpsi*sphi + spsi*stht*cphi
M[2,2] = ctht*cphi
# Account for +yb = -Yi and +zb = -Zi
Mbi = np.matmul(np.array([[1.0, 0.0, 0.0],
[0.0, -1.0, 0.0],
[0.0, 0.0, -1.0]]), M)
return Mbi
def leadAngle(Pt, Vt, Pm, magVm):
#
# Assuming constant velocity magnitudes and
# headings, at some future intercept point the
# triangle formed by missile velocity magVm and
# target velocity magVt toward that point from
# their current positions Pm and Pt, and the
# LOS distance between current positions (i.e.,
# Rlos = Pt - Pm) must satisfy the following
# relationship:
#
# Vm*sin(alpha) = Vt*sin(beta)
#
# where alpha (lead angle) is the angle between
# Vm and Rlos, and beta is the angle between Vt
# and Rlos. Using definition of inner product:
#
# cos(beta) = <Vt, Rlos> / |Vt||Rlos|
#
# a value for beta can determined, and then the
# above relationship solved for alpha.
#
magVt = la.norm(Vt)
Ptm = Prel(Pt, Pm)
cosb = np.dot(Vt, Ptm)/(magVt*la.norm(Ptm))
alpha = asin((magVt/magVm)*sin(acos(cosb)))
return alpha # Note: alpha in radians.
def az_el_of_V(V):
# Note: DPR is global.
U = Uvec(V)
az = atan2(U[1], U[0])*DPR
el = atan2(U[2], la.norm([U[0], U[1]]))*DPR
return az, el # Note: az and el in degrees.
def setVm(vmag, az, el):
# Note: az and el in radians.
vx = vmag*cos(el)*cos(az)
vy = vmag*cos(el)*sin(az)
vz = vmag*sin(el)
Vm = np.array([vx, vy, vz])
return Vm
###
### Procedures for 3-DOF Kinematic Proportional Navigation Guidance
### Laws of Ideal Missile
###
def Vclose(Vt, Vm, Ulos, collision=False):
# Note: Closing velocity is defined as -d(Rlos)/dt.
if collision == True:
# Calculate collision closing velocity (see calcVcTgo).
vt = la.norm(Vt)
vm = la.norm(Vm)
betat = acos(np.dot(Vt, Ulos)/vt)
betam = asin((vt/vm)*sin(betat))
Vc = (vm*cos(betam) - vt*cos(betat))*Ulos
else:
# Calculate standard closing velocity which assumes
# Vt, Vm and Ulos are all within the same plane.
Vc = -np.dot(Vrel(Vt,Vm), Ulos)*Ulos
return Vc
def timeToGo(Rlos, Vt, Vm):
# Note: Uses collision closing velocity (see calcVcTgo).
vcc = la.norm(Vclose(Vt, Vm, Uvec(Rlos), True))
tgo = la.norm(Rlos)/vcc
return tgo
def calcVcTgo(Pt, Vt, Pm, Vm):
#
# Calculates Vc as collision course relative (closing)
# velocity of missile wrt target, and Tgo as time-to-go
# (to intercept) using equations presented on pgs 25-26
# in sections C.1.1, C.1.2 and C.1.3 of ref [6].
#
# NOTE: Following two expressions assume elt != +/-90
# and ||[Prel[0], Prel[1]]|| > zero.
azt, elt = az_el_of_V(Vt)
aztm, eltm = az_el_of_V(Prel(Pt,Pm))
thtt = elt*RPD
psit = -azt*RPD
thttm = eltm*RPD
psitm = -aztm*RPD
# Unit vector (evt) along target body, and unit Rlos (estm) vector;
# equations (A2.1) and (A2.2).
evt = np.array([cos(thtt)*cos(psit), cos(thtt)*sin(psit), -sin(thtt)])
estm = np.array([cos(thttm)*cos(psitm), cos(thttm)*sin(psitm), -sin(thttm)])
# Angle between target velocity vector Vt and the Rlos vector measured
# in Vt X Rlos X Vm plane; equations (A2.3) and (A2.4).
Betatm = acos(np.dot(evt, estm))
# Angle between missile collision course velocity vector and the
# RLos vector measured in Vt X Rlos X Vm plane; equation (A2.6).
Betaccmt = asin((la.norm(Vt)/la.norm(Vm))*sin(Betatm))
# Collision course closing velocity of missile wrt target along
# Rmt; equation (A2.7).
VCccmt = la.norm(Vm)*cos(Betaccmt) - la.norm(Vt)*cos(Betatm)
# Target/missile range-to-go; equation (A2.8).
Rmt = la.norm(Prel(Pm, Pt))
# Time-to-go; equation (A2.9).
Tgo = Rmt/VCccmt
return VCccmt, Tgo
def ZEMn(Rlos, Vtm, tgo):
# Zero Effort Miss normal to line-of-sight (LOS) vector Rlos
# towards a non-maneuvering target at time-to-go tgo.
# See derivation of equation (22) on pg 48 in ref [8] and
# discussion of ZEM in Section 4, Module 3 ref [4].
Ulos = Uvec(Rlos)
ZEM = Rlos + Vtm*tgo
ZEMr = np.dot(ZEM, Ulos)*Ulos
ZEMn = ZEM - ZEMr
"""
try:
azd, eld = az_el_of_V(Rlos)
Mli = Mrot(azd*RPD, eld*RPD, 0.0)
ZEMl = np.matmul(Mli, ZEM)
ZEMln = np.array([0.0, ZEMl[1], ZEMl[2]])
ZEMi = np.matmul(Mli.transpose(), ZEMln)
np.testing.assert_almost_equal(la.norm(ZEMn - ZEMi), 0.0, 6)
except:
print("ZEMn: (%10.3f, %10.3f, %10.3f)" %\
(ZEMn[0], ZEMn[1], ZEMn[2]))
print("ZEMi: (%10.3f, %10.3f, %10.3f)" %\
(ZEMi[0], ZEMi[1], ZEMi[2]))
sys.exit()
"""
return ZEMn
def AZEMn(Rlos, Vtm, At, tgo):
# Augmented Zero Effort Miss normal to line-of-sight (LOS) vector
# Rlos towards an accelerating target at time-to-go tgo.
# See derivation of equation (27) on pg 51 in ref [8] and
# discussion of APN in Section 2, Module 2 of ref [5].
Ulos = Uvec(Rlos)
ZEMA = Rlos + Vtm*tgo + (At/2.0)*tgo**2
ZEMAr = np.dot(ZEMA, Ulos)*Ulos
ZEMAn = ZEMA - ZEMAr
return ZEMAn
def Wlos(Vt, Vm, Rlos, Ulos):
# Calculates line-of-sight (LOS) rotation rate Ws in the direction
# of Ulos x (Ws x Ulos) perpendicular to the instantaneous rotation
# plane of LOS (IRPL), and represents angular velocity of Rlos (i.e.,
# Vrel = W x Rlos) without the component in the direction of Ulos.
#
# This could be derived from the slew rate(s) of a missile's seeker
# as it follows the target, or as rate of change in target angular
# offset within the sensor FOV for a fixed seeker. Naturally, the
# contribution of seeker orientation, missile attitude angles and
# rotation rates to Ws would need to be accounted for (see ref [3]).
#
# Note: Following expressions for calculating Wlos
#
Vnrm = Vrel(Vt, Vm) + Vclose(Vt, Vm, Ulos) # Vrel component normal to -Vclose
Wlos = np.cross(-Vnrm, Ulos)/la.norm(Rlos) # Wlos is normal to Ulos and Vnrm
#
# is equivalent to:
#
# Wlos = np.cross(Rlos, Vrel(Vt, Vm))/np.dot(Rlos, Rlos)
"""
Wlos2 = np.cross(Rlos, Vrel(Vt, Vm))/np.dot(Rlos, Rlos)
try:
np.testing.assert_almost_equal(Wlos2, Wlos, 6)
except:
print("Wlos: (%8.3f, %8.3f, %8.3f)" % (Wlos[0], Wlos[1], Wlos[2]))
print("Wlos2: (%8.3f, %8.3f, %8.3f)" % (Wlos2[0], Wlos2[1], Wlos2[2]))
"""
#
# which can be reduced to:
#
# Wlos = np.array([Rlos[1]*Vtm[2] - Rlos[2]*Vtm[1],
# Rlos[2]*Vtm[0] - Rlos[0]*Vtm[2],
# Rlos[0]*Vtm[1] - Rlos[1]*Vtm[0]
# ])/np.dot(Rlos, Rlos)
#
# as shown in derivation of equation (2.18) in ref [6].
return Wlos
def applyGCP(Ac, Ulos, Vm):
"""
Apply guidance command preservation per eq (45) on pg 38 of ref [3].
Parameters
----------
Ac : float 3-vector
Commanded missile guidance acceleration (inertial).
Ulos : float 3-vector
Unit vector along LOS from missile to target (inertial).
Vm : float 3-vector
Velocity (inertial) of missile.
Returns
-------
Acmd : float 3-Vector
Acceleration commanded.
"""
UVm = Uvec(Vm)
Am = Ac - np.dot(Ac, UVm)*UVm
dot_Ulos_UVm = np.dot(Ulos, UVm)
if abs(dot_Ulos_UVm) > 0.0:
Agcp = ((Am[0] - np.dot(Ac, UVm))/dot_Ulos_UVm)*Ulos + Ac
Acmd = Agcp - np.dot(Agcp, UVm)*UVm # no thrust control
"""
try:
np.testing.assert_almost_equal(np.dot(Agcp, UVm), Am[0], 6)
np.testing.assert_almost_equal(np.dot(Agcp, Uvec(Ac)), la.norm(Ac), 6)
except:
print("\nAc: (%8.3f, %8.3f, %8.3f)" % (Ac[0], Ac[1], Ac[2]))
print("Agcp: (%8.3f, %8.3f, %8.3f)" % (Agcp[0], Agcp[1], Agcp[2]))
print("Acmd: (%8.3f, %8.3f, %8.3f)" % (Acmd[0], Acmd[1], Acmd[2]))
print("||Ac||, ||Agcp||, ||Acmd||: %8.3f %8.3f %8.3f" % \
(la.norm(Ac), la.norm(Agcp), la.norm(Acmd)))
"""
else:
# Missile body x-axis (uVM) perpendicular to LOS (Ulos).
Acmd = Ac - np.dot(Ac, UVm)*UVm
return Acmd
def Amslc(Rlos, Vt, At, Vm, N):
"""
This routine is the application of selected proportional
navigation method - True, Pure, ZEM, ATPN, APPN or AZEM.
Globals
-------
PNAV : integer constant
Proportional Navigation law selected identifier
RPD : float constant
Radians per degree
Parameters
----------
Rlos : float 3-vector
Range along LOS from missile to target.
Vt : float 3-vector
Velocity (inertial) of target.
At : float 3-vector
Acceleration (inertial) of target.
Vm : float 3-vector
Velocity (inertial) of missile.
N : float
Proportional navigation constant (or gain).
Returns
-------
Acmd : float 3-Vector
Acceleration commanded.
"""
Ulos = Uvec(Rlos)
Vtm = Vrel(Vt, Vm)
if PNAV == PN_APPN or PNAV == PN_ATPN:
# Create inertial to missile body rotation matrix
maz, mel = az_el_of_V(Vm)
Mbi = Mrot(maz*RPD, mel*RPD, 0.0)
"""
try:
np.testing.assert_almost_equal(la.norm(np.matmul(Mbi, Mbi.transpose())),
sqrt(3.0), 6)
except:
print('Msl:\n', np.matmul(Mbi, Mbi.transpose()))
print(la.norm(np.matmul(Mbi, Mbi.transpose())))
sys.exit()
try:
Atb = np.matmul(Mbi, At)
Atx = np.matmul(Mbi.transpose(), Atb)
np.testing.assert_almost_equal(la.norm(At - Atx), 0.0, 6)
except:
print("At orig: (%8.3f, %8.3f, %8.3f)" % (At[0], At[1], At[2]))
print("At xfrm: (%8.3f, %8.3f, %8.3f)" % (Atx[0], Atx[1], Atx[2]))
sys.exit()
"""
# See derivation of equation (3.8) in ref [6].
#
Ws = Wlos(Vt, Vm, Rlos, Ulos)
##UWs = Uvec(Ws) # Used to calculate Ats below.
##UVm = Uvec(Vm) # Used in the assert statements below.
# Vector Atn is the rejection of At with vector Ulos and is normal
# to Ulos, and represents the components of target acceleration At
# which can contribute to change in line-of-sight (LOS) rotation
# rate Ws (i.e., d(Ws)/dt).
Atn = At - np.dot(At, Ulos)*Ulos
# Vector Ats is the rejection of Atn with vector UWs and is in the
# plane containing both vector UWs x Ulos and vector Ulos; thus
# omitting the component of Atn which could change the direction
# of Ws. Not applied; using Atn instead of Ats for APPN and ATPN
# below.
##Ats = Atn - np.dot(Atn, UWs)*UWs
if PNAV == PN_APPN:
# 3.1.1 Version 1 (PN-1) Pure PN equations (3.2)-(3.4).
Atsb = np.matmul(Mbi, Atn)
Atsb[0] = 0.0
Atsi = np.matmul(Mbi.transpose(), Atsb)
# Vector Ac normal to UVm.
Ac = N*np.cross(Ws, Vm) + (N/2)*Atsi # eqs (3.2) & (3.8) inertial
"""
PN_1b = np.matmul(Mbi, Ac)
# Eq. (3.4) not required since Ac dot Vm is zero by definition.
# PN_1b[0] = 0.0 # eq. (3.4)
np.testing.assert_almost_equal(np.dot(Ac,UVm), 0.0, 6)
np.testing.assert_almost_equal(PN_1b[0], 0.0, 6)
"""
Acmd = Ac
else: # PNAV == PN_ATPN
# 3.1.2 Version 2 (PN-2) True PN equations (3.5)-(3.7).
Vc = la.norm(Vclose(Vt, Vm, Ulos))
# Vector Ac is normal to Ulos.
Ac = N*Vc*np.cross(Ws, Ulos) + (N/2)*Atn # eqs (3.5) & (3.8) inertial.
Agcp = applyGCP(Ac, Ulos, Vm)
"""
PN_2b = np.matmul(Mbi, Agcp)
# Eq. (3.7) not required since Agcp dot Vm is zero by definition.
# PN_2b[0] = 0.0 # eq. (3.7)
np.testing.assert_almost_equal(np.dot(Agcp,UVm), 0.0, 6)
np.testing.assert_almost_equal(PN_2b[0], 0.0, 6)
"""
Acmd = Agcp
elif PNAV == PN_AZEM:
# See derivation of equation (27) on pg 51 in ref [8].
# NOTE: Time-to-go calculated here assumes non-accelerating target.
tgo = timeToGo(Rlos, Vt, Vm)
Ac = N*AZEMn(Rlos, Vtm, At, tgo)/(tgo**2)
Acmd = applyGCP(Ac, Ulos, Vm)
elif PNAV == PN_ZEM:
# See derivation of equation (22) on pg 48 in ref [8].
# NOTE: Time-to-go calculated here assumes non-accelerating target.
tgo = timeToGo(Rlos, Vt, Vm)
Ac = N*ZEMn(Rlos, Vtm, tgo)/(tgo**2)
Acmd = applyGCP(Ac, Ulos, Vm)
elif PNAV == PN_PURE:
Acmd = N*np.cross(Wlos(Vt, Vm, Rlos, Ulos), Vm)
else: # PNAV == PN_TRUE
Vc = la.norm(Vclose(Vt, Vm, Ulos))
Ac = N*Vc*np.cross(Wlos(Vt, Vm, Rlos, Ulos), Ulos)
Acmd = applyGCP(Ac, Ulos, Vm)
return Acmd
def Amsla(Amcmd, Ammax):
"""
Applies Ammax bound to given commanded missile acceleration.
Parameters
----------
Amcmd : float 3-vector
Missile inertial linear acceleration commanded.
Ammax : float constant
Maximum missile linear acceleration.
Returns
-------
float 3-Vector
Missile inertial acceleration achieved (actual).
"""
if la.norm(Amcmd) > Ammax:
return np.dot(Ammax, Uvec(Amcmd))
return Amcmd
###
### Procedures for 3-DOF Kinematic Equations of Motion of Fixed-Wing Target
###
def Atgt(t, UWt, Pt, Vt, n, TgtTheta):
"""
This routine calculates target inertial linear acceleration
and pitch rate for given inertial angular velocity direction
unit vector, inertial linear velocity, turning g's, and pitch
angle.
Globals
-------
g : float (read only)
gravitional acceleration magnitude (meters/sec/sec)
DPR: float (read only)
degrees per radian
RPD: float (read only)
radians per degree
MSL: integer (read only)
Missile type code
Wt: float (read only)
Target weave angular velocity in UWt direction (rad/sec)
Parameters
----------
t : float
Time (sec)
UWt : float 3-vector
Target angular velocity direction unit vector (i.e.,
direction frame rotation axis points in inertial space).
Pt : float 3-vector
Target inertial position.
Vt : float 3-vector
Target inertial velocity.
n : float
Target turning acceleration (normal to UWt) magnitude
in g's.
TgtTheta : float
Target pitch angle in radians.
Returns
-------
At : float 3-vector
Target inertial acceleration.
TgtThetaDot : float
Target pitch angle rotation rate in radians/sec.
"""
global Wt
if n != 0.0:
if MSL == AAM and Wt > 0.0:
# Weave maneuver
At = np.array([0.0, n*g*sin(Wt*t), n*g*cos(Wt*t)])
return At, 0.0
magVt = la.norm(Vt)
# Calculate target az and el in degrees.
taz, tel = az_el_of_V(Vt)
# Set target body frame rotation axis.
if abs(np.dot(UWt,Uyi)) < 0.0001:
UWtb = -UWt # +yawing about -zb axis
else:
UWtb = UWt # +pitching about +yb axis
# Compute inertial to body transformation matrix
if (abs(UWtb[1]) > 0.9999 and TgtTheta*DPR > 85.0) or \
(abs(UWtb[1]) < 0.9999 and (tel > 85.0 and TgtTheta*DPR > 85.0)):
# Approaching gimbal lock; form direction cosine matrix
# using position pointing vector as derived in eqs (25)
# thru (30) on pg 5 of ref [7].
i = Uvec(Vt)
r = np.array([20000.0, Pt[1], 20000.0]) - Pt
j = Uvec(np.cross(r, i))
k = Uvec(np.cross(i, j))
Mbi = np.zeros([3,3])
Mbi[0,0] = i[0]
Mbi[0,1] = i[1]
Mbi[0,2] = i[2]
Mbi[1,0] = j[0]
Mbi[1,1] = j[1]
Mbi[1,2] = j[2]
Mbi[2,0] = k[0]
Mbi[2,1] = k[1]
Mbi[2,2] = k[2]
UWtb = np.matmul(Mbi, UWt)
# Calculate lift g's loss due to pitch angle.
loss = cos(TgtTheta)
else:
# Use ENU coordinate transformation matrix.
Mbi = Mrot(taz*RPD, tel*RPD, 0.0)
# Calculate lift g's loss due to pitch angle.
theta = pitchAngle(Vt)
loss = cos(theta)
"""
try:
np.testing.assert_almost_equal(la.norm(np.matmul(Mbi, Mbi.transpose())),
sqrt(3.0), 6)
except:
print('Tgt:\n', np.matmul(Mbi, Mbi.transpose()))
print(la.norm(np.matmul(Mbi, Mbi.transpose())))
sys.exit()
"""
# Rotate inertial Vt into target body frame.
Vtb = np.matmul(Mbi, Vt)
# Calculate aircraft body rotational rate (rad/sec).
OmegaDot = (n*g)/magVt # total rate
## NOTE: Set loss to zero for constant target rotation rate as modeled
## in AAM engagement cases presented in refs [4], [5] and [9].
loss = 0.0
TgtThetaDot = (((n-loss)*g)/magVt)*UWt[1] # pitch rate
# Calculate inertial acceleration in target body frame.
Atb = np.cross(OmegaDot*UWtb, Vtb.flatten())
# Rotate inertial acceleration into inertial space frame.
At = np.matmul(Mbi.transpose(), Atb)
else:
TgtThetaDot = 0.0
At = np.array([0.0, 0.0, 0.0])
return At, TgtThetaDot
def pitchAngle(Vt):
# Note: The following expression returns values in the
# range [-90.0, 90.0], and does not account for
# instances where the target has pitched past
# +/-90 degrees.
theta = atan2(Vt[2], la.norm([Vt[0], Vt[1]]))
return theta # Note: theta in radians.
def bankAngle(At, Vt):
# Note: RPD and g are global.
UVt = Uvec(Vt)
# Calculate turning acceleration normal to Vt.
Atn = At - np.dot(At, UVt)*UVt
# Calculate target az and el in degrees.
taz, tel = az_el_of_V(Vt)
# Rotate inertial Atn into target body frame.
Mbi = Mrot(taz*RPD, tel*RPD, 0.0)
Atb = np.matmul(Mbi, Atn)
# Only use Y component of turning acceleration normal.
phi = atan(Atb[1]/g)
return phi # Note: phi in radians.
###
### Differential Equations of Motion State Integration Data Structures
### and Procedures
###
# Initial values for state variables array.
nSvar = 14 # number of state variables
S = np.zeros(nSvar) # state variables
dS = np.zeros(nSvar) # state derivatives
def setS(S, Vt, Pt, Vm, Pm, TgtTheta):
# Note: S[0] is t (time).
S[1] = Vt[0]
S[2] = Vt[1]
S[3] = Vt[2]
S[4] = Pt[0]
S[5] = Pt[1]
S[6] = Pt[2]
S[7] = Vm[0]
S[8] = Vm[1]
S[9] = Vm[2]
S[10] = Pm[0]
S[11] = Pm[1]
S[12] = Pm[2]
S[13] = TgtTheta
return S
def getVtOfS(S):
return np.array([S[1], S[2], S[3]])
def getPtOfS(S):
return np.array([S[4], S[5], S[6]])
def getVmOfS(S):
return np.array([S[7], S[8], S[9]])
def getPmOfS(S):
return np.array([S[10], S[11], S[12]])
def getS(S):
Vt = getVtOfS(S)
Pt = getPtOfS(S)
Vm = getVmOfS(S)
Pm = getPmOfS(S)
TgtTheta = S[13]
return Vt, Pt, Vm, Pm, TgtTheta
def setSdot(Sdot, At, Vt, Am, Vm, TgtThetaDot):
# Note: Sdot[0] is dt (1.0).
Sdot[1] = At[0]
Sdot[2] = At[1]
Sdot[3] = At[2]
Sdot[4] = Vt[0]
Sdot[5] = Vt[1]
Sdot[6] = Vt[2]
Sdot[7] = Am[0]
Sdot[8] = Am[1]
Sdot[9] = Am[2]
Sdot[10] = Vm[0]
Sdot[11] = Vm[1]
Sdot[12] = Vm[2]
Sdot[13] = TgtThetaDot
return Sdot
def getAtOfSdot(Sdot):
return np.array([Sdot[1], Sdot[2], Sdot[3]])
def getVtOfSdot(Sdot):
return np.array([Sdot[4], Sdot[5], Sdot[6]])
def getAmOfSdot(Sdot):
return np.array([Sdot[7], Sdot[8], Sdot[9]])
def getVmOfSdot(Sdot):