diff --git a/demo/F16/README.md b/demo/F16/README.md index 654c1430aa3be833e9cbd4d3d8ca07aa55fa828a..d1cc352b13d48ec3069c2cf90d7420ca8df97911 100644 --- a/demo/F16/README.md +++ b/demo/F16/README.md @@ -1,3 +1,4 @@ # Adaptation of F16 model -This is an adaptation of the Python F16 model from (Stanley Bak)[https://github.com/stanleybak/AeroBenchVVPython]. +This is an adaptation of the Python F16 model from [Stanley Bak](https://github.com/stanleybak/AeroBenchVVPython). +All the files are copied to make this example a standalone executable. diff --git a/demo/F16/aerobench/highlevel/autopilot.py b/demo/F16/aerobench/highlevel/autopilot.py new file mode 100644 index 0000000000000000000000000000000000000000..d369cdc82cbd69c46ffcb9fc6769b407ca78570e --- /dev/null +++ b/demo/F16/aerobench/highlevel/autopilot.py @@ -0,0 +1,97 @@ +''' +Stanley Bak +Autopilot State-Machine Logic + +There is a high-level advance_discrete_state() function, which checks if we should change the current discrete state, +and a get_u_ref(f16_state) function, which gets the reference inputs at the current discrete state. +''' + +import abc +from math import pi + +import numpy as np +from numpy import deg2rad + +from aerobench.lowlevel.low_level_controller import LowLevelController +from aerobench.util import Freezable + +class Autopilot(Freezable): + '''A container object for the hybrid automaton logic for a particular autopilot instance''' + + def __init__(self, init_mode, llc=None): + + assert isinstance(init_mode, str), 'init_mode should be a string' + + if llc is None: + # use default + llc = LowLevelController() + + self.llc = llc + self.xequil = llc.xequil + self.uequil = llc.uequil + + self.mode = init_mode # discrete state, this should be overwritten by subclasses + + self.freeze_attrs() + + def advance_discrete_mode(self, t, x_f16): + ''' + advance the discrete mode based on the current aircraft state. Returns True iff the discrete mode + has changed. It's also suggested to update self.mode to the current mode name. + ''' + + return False + + def is_finished(self, t, x_f16): + ''' + returns True if the simulation should stop (for example, after maneuver completes) + + this is called after advance_discrete_state + ''' + + return False + + @abc.abstractmethod + def get_u_ref(self, t, x_f16): + ''' + for the current discrete state, get the reference inputs signals. Override this one + in subclasses. + + returns four values per aircraft: Nz, ps, Ny_r, throttle + ''' + + return + + def get_checked_u_ref(self, t, x_f16): + ''' + for the current discrete state, get the reference inputs signals and check them against ctrl limits + ''' + + rv = np.array(self.get_u_ref(t, x_f16), dtype=float) + + assert rv.size % 4 == 0, "get_u_ref should return Nz, ps, Ny_r, throttle for each aircraft" + + for i in range(rv.size //4): + Nz, _ps, _Ny_r, _throttle = rv[4*i:4*(i+1)] + + l, u = self.llc.ctrlLimits.NzMin, self.llc.ctrlLimits.NzMax + assert l <= Nz <= u, f"autopilot commanded invalid Nz ({Nz}). Not in range [{l}, {u}]" + + return rv + +class FixedSpeedAutopilot(Autopilot): + '''Simple Autopilot that gives a fixed speed command using proportional control''' + + def __init__(self, setpoint, p_gain): + self.setpoint = setpoint + self.p_gain = p_gain + + init_mode = 'tracking speed' + Autopilot.__init__(self, init_mode) + + def get_u_ref(self, t, x_f16): + '''for the current discrete state, get the reference inputs signals''' + + x_dif = self.setpoint - x_f16[0] + + return 0, 0, 0, self.p_gain * x_dif diff --git a/demo/F16/aerobench/highlevel/controlled_f16.py b/demo/F16/aerobench/highlevel/controlled_f16.py new file mode 100644 index 0000000000000000000000000000000000000000..c7ee115954ed94673ddbf8de28c9dd0eb7083937 --- /dev/null +++ b/demo/F16/aerobench/highlevel/controlled_f16.py @@ -0,0 +1,60 @@ +''' +Stanley Bak +Python Version of F-16 GCAS +ODE derivative code (controlled F16) +''' + +from math import sin, cos + +import numpy as np +from numpy import deg2rad + +from aerobench.lowlevel.subf16_model import subf16_model +from aerobench.lowlevel.low_level_controller import LowLevelController + +def controlled_f16(t, x_f16, u_ref, llc, f16_model='morelli', v2_integrators=False): + 'returns the LQR-controlled F-16 state derivatives and more' + + assert isinstance(x_f16, np.ndarray) + assert isinstance(llc, LowLevelController) + assert u_ref.size == 4 + + assert f16_model in ['stevens', 'morelli'], 'Unknown F16_model: {}'.format(f16_model) + + x_ctrl, u_deg = llc.get_u_deg(u_ref, x_f16) + + # Note: Control vector (u) for subF16 is in units of degrees + xd_model, Nz, Ny, _, _ = subf16_model(x_f16[0:13], u_deg, f16_model) + + if v2_integrators: + # integrators from matlab v2 model + ps = xd_model[6] * cos(xd_model[1]) + xd_model[8] * sin(xd_model[1]) + + Ny_r = Ny + xd_model[8] + else: + # Nonlinear (Actual): ps = p * cos(alpha) + r * sin(alpha) + ps = x_ctrl[4] * cos(x_ctrl[0]) + x_ctrl[5] * sin(x_ctrl[0]) + + # Calculate (side force + yaw rate) term + Ny_r = Ny + x_ctrl[5] + + xd = np.zeros((x_f16.shape[0],)) + xd[:len(xd_model)] = xd_model + + # integrators from low-level controller + start = len(xd_model) + end = start + llc.get_num_integrators() + int_der = llc.get_integrator_derivatives(t, x_f16, u_ref, Nz, ps, Ny_r) + xd[start:end] = int_der + + # Convert all degree values to radians for output + u_rad = np.zeros((7,)) # throt, ele, ail, rud, Nz_ref, ps_ref, Ny_r_ref + + u_rad[0] = u_deg[0] # throttle + + for i in range(1, 4): + u_rad[i] = deg2rad(u_deg[i]) + + u_rad[4:7] = u_ref[0:3] # inner-loop commands are 4-7 + + return xd, u_rad, Nz, ps, Ny_r diff --git a/demo/F16/aerobench/lowlevel/adc.py b/demo/F16/aerobench/lowlevel/adc.py new file mode 100644 index 0000000000000000000000000000000000000000..a70390eb25f9e91855152adf31799e17297e34e3 --- /dev/null +++ b/demo/F16/aerobench/lowlevel/adc.py @@ -0,0 +1,37 @@ +''' +Stanley Bak +adc.py for F-16 model +''' + +from math import sqrt + +def adc(vt, alt): + '''converts velocity (vt) and altitude (alt) to mach number (amach) and dynamic pressure (qbar) + + See pages 63-65 of Stevens & Lewis, "Aircraft Control and Simulation", 2nd edition + ''' + + # vt = freestream air speed + + ro = 2.377e-3 + tfac = 1 - .703e-5 * alt + + if alt >= 35000: # in stratosphere + t = 390 + else: + t = 519 * tfac # 3 rankine per atmosphere (3 rankine per 1000 ft) + + # rho = freestream mass density + rho = ro * tfac**4.14 + + # a = speed of sound at the ambient conditions + # speed of sound in a fluid is the sqrt of the quotient of the modulus of elasticity over the mass density + a = sqrt(1.4 * 1716.3 * t) + + # amach = mach number + amach = vt / a + + # qbar = dynamic pressure + qbar = .5 * rho * vt * vt + + return amach, qbar diff --git a/demo/F16/aerobench/lowlevel/cl.py b/demo/F16/aerobench/lowlevel/cl.py new file mode 100644 index 0000000000000000000000000000000000000000..6bff35919a610317771baa8bc22ffcfacfe48973 --- /dev/null +++ b/demo/F16/aerobench/lowlevel/cl.py @@ -0,0 +1,53 @@ +''' +Stanley Bak +F-16 GCAS in Python +cl function +''' + +import numpy as np + +from aerobench.util import fix, sign + +def cl(alpha, beta): + 'cl function' + + a = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], \ + [-.001, -.004, -.008, -.012, -.016, -.022, -.022, -.021, -.015, -.008, -.013, -.015], \ + [-.003, -.009, -.017, -.024, -.030, -.041, -.045, -.040, -.016, -.002, -.010, -.019], \ + [-.001, -.010, -.020, -.030, -.039, -.054, -.057, -.054, -.023, -.006, -.014, -.027], \ + [.000, -.010, -.022, -.034, -.047, -.060, -.069, -.067, -.033, -.036, -.035, -.035], \ + [.007, -.010, -.023, -.034, -.049, -.063, -.081, -.079, -.060, -.058, -.062, -.059], \ + [.009, -.011, -.023, -.037, -.050, -.068, -.089, -.088, -.091, -.076, -.077, -.076]], dtype=float).T + + s = .2 * alpha + k = fix(s) + + if k <= -2: + k = -1 + + if k >= 9: + k = 8 + + da = s - k + l = k + fix(1.1 * sign(da)) + s = .2 * abs(beta) + m = fix(s) + if m == 0: + m = 1 + + if m >= 6: + m = 5 + + db = s - m + n = m + fix(1.1 * sign(db)) + l = l + 3 + k = k + 3 + m = m + 1 + n = n + 1 + t = a[k-1, m-1] + u = a[k-1, n-1] + v = t + abs(da) * (a[l-1, m-1] - t) + w = u + abs(da) * (a[l-1, n-1] - u) + dum = v + (w - v) * abs(db) + + return dum * sign(beta) diff --git a/demo/F16/aerobench/lowlevel/clf16.py b/demo/F16/aerobench/lowlevel/clf16.py new file mode 100644 index 0000000000000000000000000000000000000000..a09addfe2445456f98efb89c6fbcee662be911e7 --- /dev/null +++ b/demo/F16/aerobench/lowlevel/clf16.py @@ -0,0 +1,85 @@ +''' +Stanley Bak +clf16.py for F-16 model + +This is the objective function for finding the trim condition of the initial states +''' + +from math import asin, sin + +from tgear import tgear +from conf16 import conf16 +from subf16_model import subf16_model + +def clf16(s, x, u, const, model='stevens', adjust_cy=True): + ''' + objective function of the optimization to find the trim conditions + + x and u get modified in-place + returns the cost + ''' + + _, singam, _, _, tr, _, _, _, thetadot, _, _, orient = const + gamm = asin(singam) + + if len(s) == 3: + u[0] = s[0] + u[1] = s[1] + x[1] = s[2] + else: + u[0] = s[0] + u[1] = s[1] + u[2] = s[2] + u[3] = s[3] + x[1] = s[4] + x[3] = s[5] + x[4] = s[6] + + # + # Get the current power and constraints + # + x[12] = tgear(u[0]) + [x, u] = conf16(x, u, const) + + # we just want the derivative + subf16 = lambda x, u: subf16_model(x, u, model, adjust_cy)[0] + + xd = subf16(x, u) + + # + # Steady Level flight + # + if orient == 1: + r = 100.0*(xd[0]**2 + xd[1]**2 + xd[2]**2 + xd[6]**2 + xd[7]**2 + xd[8]**2) + + # + # Steady Climb + # + if orient == 2: + r = 500.0*(xd[11]-x[0]*sin(gamm))**2 + xd[0]**2 + 100.0*(xd[1]**2 + xd[2]**2) + \ + 10.0*(xd[6]**2 + xd[7]**2 + xd[8]**2) + + + + # + # Coord Turn + # + if orient == 3: + r = xd[0]*xd[0] + 100.0 * (xd[1] * xd[1] + xd[2]*xd[2] + xd[11]*xd[11]) + 10.0*(xd[6]*xd[6] + \ + xd[7]*xd[7]+xd[8]*xd[8]) + 500.0*(xd[5] - tr)**2 + + # + # Pitch Pull Up + # + + + if orient == 4: + r = 500.0*(xd[4]-thetadot)**2 + xd[0]**2 + 100.0*(xd[1]**2 + xd[2]**2) + 10.0*(xd[6]**2 + xd[7]**2 + xd[8]**2) + + # + # Scale r if it is less than 1 + # + if r < 1.0: + r = r**0.5 + + return r diff --git a/demo/F16/aerobench/lowlevel/cm.py b/demo/F16/aerobench/lowlevel/cm.py new file mode 100644 index 0000000000000000000000000000000000000000..4c00a17596797b95076272f079f7e81502396b30 --- /dev/null +++ b/demo/F16/aerobench/lowlevel/cm.py @@ -0,0 +1,50 @@ +''' +Stanley Bak +F-16 GCAS Python +''' + +import numpy as np +from aerobench.util import fix, sign + +def cm(alpha, el): + 'cm function' + + a = np.array([[.205, .168, .186, .196, .213, .251, .245, .238, .252, .231, .198, .192], \ + [.081, .077, .107, .110, .110, .141, .127, .119, .133, .108, .081, .093], \ + [-.046, -.020, -.009, -.005, -.006, .010, .006, -.001, .014, .000, -.013, .032], \ + [-.174, -.145, -.121, -.127, -.129, -.102, -.097, -.113, -.087, -.084, -.069, -.006], \ + [-.259, -.202, -.184, -.193, -.199, -.150, -.160, -.167, -.104, -.076, -.041, -.005]], dtype=float).T + + s = .2 * alpha + k = fix(s) + + if k <= -2: + k = -1 + + if k >= 9: + k = 8 + + da = s - k + l = k + fix(1.1 * sign(da)) + s = el / 12 + m = fix(s) + + if m <= -2: + m = -1 + + if m >= 2: + m = 1 + + de = s - m + n = m + fix(1.1 * sign(de)) + k = k + 3 + l = l + 3 + m = m + 3 + n = n + 3 + t = a[k-1, m-1] + u = a[k-1, n-1] + v = t + abs(da) * (a[l-1, m-1] - t) + w = u + abs(da) * (a[l-1, n-1] - u) + + return v + (w - v) * abs(de) + diff --git a/demo/F16/aerobench/lowlevel/cn.py b/demo/F16/aerobench/lowlevel/cn.py new file mode 100644 index 0000000000000000000000000000000000000000..b06e3729c43572920230f410ef80555c3b766ee8 --- /dev/null +++ b/demo/F16/aerobench/lowlevel/cn.py @@ -0,0 +1,54 @@ +''' +Stanley Bak +F16 GCAS in Python +cn function +''' + +import numpy as np +from aerobench.util import fix, sign + +def cn(alpha, beta): + 'cn function' + + a = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], \ + [.018, .019, .018, .019, .019, .018, .013, .007, .004, -.014, -.017, -.033], \ + [.038, .042, .042, .042, .043, .039, .030, .017, .004, -.035, -.047, -.057], \ + [.056, .057, .059, .058, .058, .053, .032, .012, .002, -.046, -.071, -.073], \ + [.064, .077, .076, .074, .073, .057, .029, .007, .012, -.034, -.065, -.041], \ + [.074, .086, .093, .089, .080, .062, .049, .022, .028, -.012, -.002, -.013], \ + [.079, .090, .106, .106, .096, .080, .068, .030, .064, .015, .011, -.001]], dtype=float).T + + s = .2 * alpha + k = fix(s) + + if k <= -2: + k = -1 + + if k >= 9: + k = 8 + + da = s - k + l = k + fix(1.1 * sign(da)) + s = .2 * abs(beta) + m = fix(s) + + if m == 0: + m = 1 + + if m >= 6: + m = 5 + + db = s - m + n = m + fix(1.1 * sign(db)) + l = l + 3 + k = k + 3 + m = m + 1 + n = n + 1 + t = a[k-1, m-1] + u = a[k-1, n-1] + + v = t + abs(da) * (a[l-1, m-1] - t) + w = u + abs(da) * (a[l-1, n-1] - u) + dum = v + (w - v) * abs(db) + + return dum * sign(beta) diff --git a/demo/F16/aerobench/lowlevel/conf16.py b/demo/F16/aerobench/lowlevel/conf16.py new file mode 100644 index 0000000000000000000000000000000000000000..f322a6def618aa96f0b5163f1c73cd4ca75cd83f --- /dev/null +++ b/demo/F16/aerobench/lowlevel/conf16.py @@ -0,0 +1,58 @@ +''' +Stanley Bak +Python F-16 + +Apply constraints to x variable +used when finding trim conditions +''' + +from math import sin, cos, asin +from tgear import tgear + +def conf16(x, u, const): + 'apply constraints to x' + + radgam, singam, rr, pr, tr, phi, cphi, sphi, thetadot, coord, stab, orient = const + gamm = asin(singam) + + # + # Steady Level Flight + # + if orient == 1: + x[3] = phi # Phi + x[4] = x[1] # Theta + x[6] = rr # Roll Rate + x[7] = pr # Pitch Rate + x[8] = 0.0 # Yaw Rate + + # + # Steady Climb + # + if orient == 2: + x[3] = phi # Phi + x[4] = x[1] + radgam # Theta + x[6] = rr # Roll Rate + x[7] = pr # Pitch Rate + x[8] = 0.0 # Yaw Rate + + # + # orient=3 implies coordinated turn + # + if orient == 3: + x[6] = -tr * sin(x[4]) # Roll Rate + x[7] = tr * cos(x[4]) * sin(x[3]) # Pitch Rate + x[8] = tr * cos(x[4]) * cos(x[3]) # Yaw Rate + + # + # Pitch Pull Up + # + if orient == 4: + x[4] = x[1] # Theta = alpha + x[3] = phi # Phi + x[6] = rr # Roll Rate + x[7] = thetadot # Pitch Rate + x[8] = 0.0 # Yaw Rate + + x[12] = tgear(u[0]) + + return x, u diff --git a/demo/F16/aerobench/lowlevel/cx.py b/demo/F16/aerobench/lowlevel/cx.py new file mode 100644 index 0000000000000000000000000000000000000000..c23a96cb6ab44c14e48dce145e1ff9bb4179e8fd --- /dev/null +++ b/demo/F16/aerobench/lowlevel/cx.py @@ -0,0 +1,50 @@ +''' +Stanley Bak +Python F-16 GCAS +cx +''' + +import numpy as np + +from aerobench.util import fix, sign + +def cx(alpha, el): + 'cx definition' + + a = np.array([[-.099, -.081, -.081, -.063, -.025, .044, .097, .113, .145, .167, .174, .166], \ + [-.048, -.038, -.040, -.021, .016, .083, .127, .137, .162, .177, .179, .167], \ + [-.022, -.020, -.021, -.004, .032, .094, .128, .130, .154, .161, .155, .138], \ + [-.040, -.038, -.039, -.025, .006, .062, .087, .085, .100, .110, .104, .091], \ + [-.083, -.073, -.076, -.072, -.046, .012, .024, .025, .043, .053, .047, .040]], dtype=float).T + + s = .2 * alpha + k = fix(s) + if k <= -2: + k = -1 + + if k >= 9: + k = 8 + + da = s - k + l = k + fix(1.1 * sign(da)) + s = el / 12 + m = fix(s) + if m <= -2: + m = -1 + + if m >= 2: + m = 1 + + de = s - m + n = m + fix(1.1 * sign(de)) + k = k + 3 + l = l + 3 + m = m + 3 + n = n + 3 + t = a[k-1, m-1] + u = a[k-1, n-1] + v = t + abs(da) * (a[l-1, m-1] - t) + w = u + abs(da) * (a[l-1, n-1] - u) + cxx = v + (w - v) * abs(de) + + return cxx diff --git a/demo/F16/aerobench/lowlevel/cy.py b/demo/F16/aerobench/lowlevel/cy.py new file mode 100644 index 0000000000000000000000000000000000000000..fac8c8c34ae7f1ea1307a9332e30774a146e760c --- /dev/null +++ b/demo/F16/aerobench/lowlevel/cy.py @@ -0,0 +1,9 @@ +''' +Stanley Bak +Python F-16 GCAS +''' + +def cy(beta, ail, rdr): + 'cy function' + + return -.02 * beta + .021 * (ail / 20) + .086 * (rdr / 30) diff --git a/demo/F16/aerobench/lowlevel/cz.py b/demo/F16/aerobench/lowlevel/cz.py new file mode 100644 index 0000000000000000000000000000000000000000..66f5769545f3fdcefb1c010dcd74483b9c0a53e5 --- /dev/null +++ b/demo/F16/aerobench/lowlevel/cz.py @@ -0,0 +1,31 @@ +''' +Stanley Bak +Python F-16 GCAS +Cz function +''' + +import numpy as np +from aerobench.util import fix, sign + +def cz(alpha, beta, el): + 'cz function' + + a = np.array([.770, .241, -.100, -.415, -.731, -1.053, -1.355, -1.646, -1.917, -2.120, -2.248, -2.229], \ + dtype=float).T + + s = .2 * alpha + k = fix(s) + + if k <= -2: + k = -1 + + if k >= 9: + k = 8 + + da = s - k + l = k + fix(1.1 * sign(da)) + l = l + 3 + k = k + 3 + s = a[k-1] + abs(da) * (a[l-1] - a[k-1]) + + return s * (1 - (beta / 57.3)**2) - .19 * (el / 25) diff --git a/demo/F16/aerobench/lowlevel/dampp.py b/demo/F16/aerobench/lowlevel/dampp.py new file mode 100644 index 0000000000000000000000000000000000000000..ffca449504156a1f2a1d50f98eb2b207c114d127 --- /dev/null +++ b/demo/F16/aerobench/lowlevel/dampp.py @@ -0,0 +1,42 @@ +''' +Stanley Bak +F16 GCAS in Python +dampp function +''' + +import numpy as np +from aerobench.util import fix, sign + +def dampp(alpha): + 'dampp functon' + + a = np.array([[-.267, -.110, .308, 1.34, 2.08, 2.91, 2.76, 2.05, 1.50, 1.49, 1.83, 1.21], \ + [.882, .852, .876, .958, .962, .974, .819, .483, .590, 1.21, -.493, -1.04], \ + [-.108, -.108, -.188, .110, .258, .226, .344, .362, .611, .529, .298, -2.27], \ + [-8.80, -25.8, -28.9, -31.4, -31.2, -30.7, -27.7, -28.2, -29.0, -29.8, -38.3, -35.3], \ + [-.126, -.026, .063, .113, .208, .230, .319, .437, .680, .100, .447, -.330], \ + [-.360, -.359, -.443, -.420, -.383, -.375, -.329, -.294, -.230, -.210, -.120, -.100], \ + [-7.21, -.540, -5.23, -5.26, -6.11, -6.64, -5.69, -6.00, -6.20, -6.40, -6.60, -6.00], \ + [-.380, -.363, -.378, -.386, -.370, -.453, -.550, -.582, -.595, -.637, -1.02, -.840], \ + [.061, .052, .052, -.012, -.013, -.024, .050, .150, .130, .158, .240, .150]], dtype=float).T + + s = .2 * alpha + k = fix(s) + + if k <= -2: + k = -1 + + if k >= 9: + k = 8 + + da = s - k + l = k + fix(1.1 * sign(da)) + k = k + 3 + l = l + 3 + + d = np.zeros((9,)) + + for i in range(9): + d[i] = a[k-1, i] + abs(da) * (a[l-1, i] - a[k-1, i]) + + return d diff --git a/demo/F16/aerobench/lowlevel/dlda.py b/demo/F16/aerobench/lowlevel/dlda.py new file mode 100644 index 0000000000000000000000000000000000000000..1f6b424b85147182a2c6021978932c9f06d2071f --- /dev/null +++ b/demo/F16/aerobench/lowlevel/dlda.py @@ -0,0 +1,50 @@ +''' +Stanley Bak +F-16 GCAS in Python +dlda function +''' + +import numpy as np +from aerobench.util import fix, sign + +def dlda(alpha, beta): + 'dlda function' + + a = np.array([[-.041, -.052, -.053, -.056, -.050, -.056, -.082, -.059, -.042, -.038, -.027, -.017], \ + [-.041, -.053, -.053, -.053, -.050, -.051, -.066, -.043, -.038, -.027, -.023, -.016], \ + [-.042, -.053, -.052, -.051, -.049, -.049, -.043, -.035, -.026, -.016, -.018, -.014], \ + [-.040, -.052, -.051, -.052, -.048, -.048, -.042, -.037, -.031, -.026, -.017, -.012], \ + [-.043, -.049, -.048, -.049, -.043, -.042, -.042, -.036, -.025, -.021, -.016, -.011], \ + [-.044, -.048, -.048, -.047, -.042, -.041, -.020, -.028, -.013, -.014, -.011, -.010], \ + [-.043, -.049, -.047, -.045, -.042, -.037, -.003, -.013, -.010, -.003, -.007, -.008]], dtype=float).T + + s = .2 * alpha + k = fix(s) + if k <= -2: + k = -1 + + if k >= 9: + k = 8 + + da = s - k + l = k + fix(1.1 * sign(da)) + s = .1 * beta + m = fix(s) + if m <= -3: + m = -2 + + if m >= 3: + m = 2 + + db = s - m + n = m + fix(1.1 * sign(db)) + l = l + 3 + k = k + 3 + m = m + 4 + n = n + 4 + t = a[k-1, m-1] + u = a[k-1, n-1] + v = t + abs(da) * (a[l-1, m-1] - t) + w = u + abs(da) * (a[l-1, n-1] - u) + + return v + (w - v) * abs(db) diff --git a/demo/F16/aerobench/lowlevel/dldr.py b/demo/F16/aerobench/lowlevel/dldr.py new file mode 100644 index 0000000000000000000000000000000000000000..8d75348462f36b83800feb01659e7477df40f327 --- /dev/null +++ b/demo/F16/aerobench/lowlevel/dldr.py @@ -0,0 +1,52 @@ +''' +Stanley Bak +Python GCAS F - 16 +dldr function +''' + +import numpy as np +from aerobench.util import sign, fix + +def dldr(alpha, beta): + 'dldr function' + + a = np.array([[.005, .017, .014, .010, -.005, .009, .019, .005, -.000, -.005, -.011, .008], \ + [.007, .016, .014, .014, .013, .009, .012, .005, .000, .004, .009, .007], \ + [.013, .013, .011, .012, .011, .009, .008, .005, -.002, .005, .003, .005], \ + [.018, .015, .015, .014, .014, .014, .014, .015, .013, .011, .006, .001], \ + [.015, .014, .013, .013, .012, .011, .011, .010, .008, .008, .007, .003], \ + [.021, .011, .010, .011, .010, .009, .008, .010, .006, .005, .000, .001], \ + [.023, .010, .011, .011, .011, .010, .008, .010, .006, .014, .020, .000]], dtype=float).T + + s = .2 * alpha + k = fix(s) + if k <= -2: + k = -1 + + if k >= 9: + k = 8 + + da = s - k + l = k + fix(1.1 * sign(da)) + s = .1 * beta + m = fix(s) + + if m <= -3: + m = -2 + + if m >= 3: + m = 2 + + db = s - m + n = m + fix(1.1 * sign(db)) + l = l + 3 + k = k + 3 + m = m + 4 + n = n + 4 + t = a[k-1, m-1] + u = a[k-1, n-1] + + v = t + abs(da) * (a[l-1, m-1] - t) + w = u + abs(da) * (a[l-1, n-1] - u) + + return v + (w - v) * abs(db) diff --git a/demo/F16/aerobench/lowlevel/dnda.py b/demo/F16/aerobench/lowlevel/dnda.py new file mode 100644 index 0000000000000000000000000000000000000000..d7e623554415d52300ec68953f92aadc14debf78 --- /dev/null +++ b/demo/F16/aerobench/lowlevel/dnda.py @@ -0,0 +1,51 @@ +''' +Stanley Bak +F16 GCAS in Python +dnda function +''' + +import numpy as np +from aerobench.util import fix, sign + +def dnda(alpha, beta): + 'dnda function' + + a = np.array([[.001, -.027, -.017, -.013, -.012, -.016, .001, .017, .011, .017, .008, .016], \ + [.002, -.014, -.016, -.016, -.014, -.019, -.021, .002, .012, .016, .015, .011], \ + [-.006, -.008, -.006, -.006, -.005, -.008, -.005, .007, .004, .007, .006, .006], \ + [-.011, -.011, -.010, -.009, -.008, -.006, .000, .004, .007, .010, .004, .010], \ + [-.015, -.015, -.014, -.012, -.011, -.008, -.002, .002, .006, .012, .011, .011], \ + [-.024, -.010, -.004, -.002, -.001, .003, .014, .006, -.001, .004, .004, .006], \ + [-.022, .002, -.003, -.005, -.003, -.001, -.009, -.009, -.001, .003, -.002, .001]], dtype=float).T + + s = .2 * alpha + k = fix(s) + + if k <= -2: + k = -1 + + if k >= 9: + k = 8 + + da = s - k + l = k + fix(1.1 * sign(da)) + s = .1 * beta + m = fix(s) + if m <= -3: + m = -2 + + if m >= 3: + m = 2 + + db = s - m + n = m + fix(1.1 * sign(db)) + l = l + 3 + k = k + 3 + m = m + 4 + n = n + 4 + t = a[k-1, m-1] + u = a[k-1, n-1] + v = t + abs(da) * (a[l-1, m-1] - t) + w = u + abs(da) * (a[l-1, n-1] - u) + + return v + (w - v) * abs(db) diff --git a/demo/F16/aerobench/lowlevel/dndr.py b/demo/F16/aerobench/lowlevel/dndr.py new file mode 100644 index 0000000000000000000000000000000000000000..805f170958e9a55df40addd68e2ab00566de0538 --- /dev/null +++ b/demo/F16/aerobench/lowlevel/dndr.py @@ -0,0 +1,49 @@ +''' +Stanley Bak +F16 GCAS in Python +dndr function +''' + +import numpy as np +from aerobench.util import fix, sign + +def dndr(alpha, beta): + 'dndr function' + + a = np.array([[-.018, -.052, -.052, -.052, -.054, -.049, -.059, -.051, -.030, -.037, -.026, -.013], \ + [-.028, -.051, -.043, -.046, -.045, -.049, -.057, -.052, -.030, -.033, -.030, -.008], \ + [-.037, -.041, -.038, -.040, -.040, -.038, -.037, -.030, -.027, -.024, -.019, -.013], \ + [-.048, -.045, -.045, -.045, -.044, -.045, -.047, -.048, -.049, -.045, -.033, -.016], \ + [-.043, -.044, -.041, -.041, -.040, -.038, -.034, -.035, -.035, -.029, -.022, -.009], \ + [-.052, -.034, -.036, -.036, -.035, -.028, -.024, -.023, -.020, -.016, -.010, -.014], \ + [-.062, -.034, -.027, -.028, -.027, -.027, -.023, -.023, -.019, -.009, -.025, -.010]], dtype=float).T + + s = .2 * alpha + k = fix(s) + if k <= -2: + k = -1 + + if k >= 9: + k = 8 + + da = s - k + l = k + fix(1.1 * sign(da)) + s = .1 * beta + m = fix(s) + if m <= -3: + m = -2 + + if m >= 3: + m = 2 + + db = s - m + n = m + fix(1.1 * sign(db)) + l = l + 3 + k = k + 3 + m = m + 4 + n = n + 4 + t = a[k-1, m-1] + u = a[k-1, n-1] + v = t + abs(da) * (a[l-1, m-1] - t) + w = u + abs(da) * (a[l-1, n-1] - u) + return v + (w - v) * abs(db) diff --git a/demo/F16/aerobench/lowlevel/low_level_controller.py b/demo/F16/aerobench/lowlevel/low_level_controller.py new file mode 100644 index 0000000000000000000000000000000000000000..d472bf309937035d4cef72ba5c7fcf0de6c94400 --- /dev/null +++ b/demo/F16/aerobench/lowlevel/low_level_controller.py @@ -0,0 +1,109 @@ +''' +Stanley Bak +Low-level flight controller +''' + +import numpy as np +from aerobench.util import Freezable + +class CtrlLimits(Freezable): + 'Control Limits' + + def __init__(self): + self.ThrottleMax = 1 # Afterburner on for throttle > 0.7 + self.ThrottleMin = 0 + self.ElevatorMaxDeg = 25 + self.ElevatorMinDeg = -25 + self.AileronMaxDeg = 21.5 + self.AileronMinDeg = -21.5 + self.RudderMaxDeg = 30 + self.RudderMinDeg = -30 + + self.NzMax = 6 + self.NzMin = -1 + + self.freeze_attrs() + +class LowLevelController(Freezable): + '''low level flight controller + ''' + + old_k_long = np.array([[-156.8801506723475, -31.037008068526642, -38.72983346216317]], dtype=float) + old_k_lat = np.array([[37.84483, -25.40956, -6.82876, -332.88343, -17.15997], + [-23.91233, 5.69968, -21.63431, 64.49490, -88.36203]], dtype=float) + + old_xequil = np.array([502.0, 0.0389, 0.0, 0.0, 0.0389, 0.0, 0.0, 0.0, \ + 0.0, 0.0, 0.0, 1000.0, 9.0567], dtype=float).transpose() + old_uequil = np.array([0.1395, -0.7496, 0.0, 0.0], dtype=float).transpose() + + def __init__(self, gain_str='old'): + # Hard coded LQR gain matrix from matlab version + + assert gain_str == 'old' + + # Longitudinal Gains + K_long = LowLevelController.old_k_long + K_lat = LowLevelController.old_k_lat + + self.K_lqr = np.zeros((3, 8)) + self.K_lqr[:1, :3] = K_long + self.K_lqr[1:, 3:] = K_lat + + # equilibrium points from BuildLqrControllers.py + self.xequil = LowLevelController.old_xequil + self.uequil = LowLevelController.old_uequil + + self.ctrlLimits = CtrlLimits() + + self.freeze_attrs() + + def get_u_deg(self, u_ref, f16_state): + 'get the reference commands for the control surfaces' + + # Calculate perturbation from trim state + x_delta = f16_state.copy() + x_delta[:len(self.xequil)] -= self.xequil + + ## Implement LQR Feedback Control + # Reorder states to match controller: + # [alpha, q, int_e_Nz, beta, p, r, int_e_ps, int_e_Ny_r] + x_ctrl = np.array([x_delta[i] for i in [1, 7, 13, 2, 6, 8, 14, 15]], dtype=float) + + # Initialize control vectors + u_deg = np.zeros((4,)) # throt, ele, ail, rud + + # Calculate control using LQR gains + u_deg[1:4] = np.dot(-self.K_lqr, x_ctrl) # Full Control + + # Set throttle as directed from output of getOuterLoopCtrl(...) + u_deg[0] = u_ref[3] + + # Add in equilibrium control + u_deg[0:4] += self.uequil + + ## Limit controls to saturation limits + ctrlLimits = self.ctrlLimits + + # Limit throttle from 0 to 1 + u_deg[0] = max(min(u_deg[0], ctrlLimits.ThrottleMax), ctrlLimits.ThrottleMin) + + # Limit elevator from -25 to 25 deg + u_deg[1] = max(min(u_deg[1], ctrlLimits.ElevatorMaxDeg), ctrlLimits.ElevatorMinDeg) + + # Limit aileron from -21.5 to 21.5 deg + u_deg[2] = max(min(u_deg[2], ctrlLimits.AileronMaxDeg), ctrlLimits.AileronMinDeg) + + # Limit rudder from -30 to 30 deg + u_deg[3] = max(min(u_deg[3], ctrlLimits.RudderMaxDeg), ctrlLimits.RudderMinDeg) + + return x_ctrl, u_deg + + def get_num_integrators(self): + 'get the number of integrators in the low-level controller' + + return 3 + + def get_integrator_derivatives(self, t, x_f16, u_ref, Nz, ps, Ny_r): + 'get the derivatives of the integrators in the low-level controller' + + return [Nz - u_ref[0], ps - u_ref[1], Ny_r - u_ref[2]] diff --git a/demo/F16/aerobench/lowlevel/morellif16.py b/demo/F16/aerobench/lowlevel/morellif16.py new file mode 100644 index 0000000000000000000000000000000000000000..4632b78cdcd91e06777a1f4680db9a44f8623770 --- /dev/null +++ b/demo/F16/aerobench/lowlevel/morellif16.py @@ -0,0 +1,189 @@ +''' +Stanley Bak +F16 GCAS in Python + +Morelli dynamics (Polynomial interpolation) +''' + +def Morellif16(alpha, beta, de, da, dr, p, q, r, cbar, b, V, xcg, xcgref): + 'desc' + + #alpha=max(-10*pi/180,min(45*pi/180,alpha)) # bounds alpha between -10 deg and 45 deg + #beta = max( - 30 * pi / 180, min(30 * pi / 180, beta)) #bounds beta between -30 deg and 30 deg + #de = max( - 25 * pi / 180, min(25 * pi / 180, de)) #bounds elevator deflection between -25 deg and 25 deg + #da = max( - 21.5 * pi / 180, min(21.5 * pi / 180, da)) #bounds aileron deflection between -21.5 deg and 21.5 deg + #dr = max( - 30 * pi / 180, min(30 * pi / 180, dr)) #bounds rudder deflection between -30 deg and 30 deg + + # xcgref = 0.35 + #reference longitudinal cg position in Morelli f16 model + + + phat = p * b / (2 * V) + qhat = q * cbar / (2 * V) + rhat = r * b / (2 * V) + ## + a0 = -1.943367e-2 + a1 = 2.136104e-1 + a2 = -2.903457e-1 + a3 = -3.348641e-3 + a4 = -2.060504e-1 + a5 = 6.988016e-1 + a6 = -9.035381e-1 + + b0 = 4.833383e-1 + b1 = 8.644627 + b2 = 1.131098e1 + b3 = -7.422961e1 + b4 = 6.075776e1 + + c0 = -1.145916 + c1 = 6.016057e-2 + c2 = 1.642479e-1 + + d0 = -1.006733e-1 + d1 = 8.679799e-1 + d2 = 4.260586 + d3 = -6.923267 + + e0 = 8.071648e-1 + e1 = 1.189633e-1 + e2 = 4.177702 + e3 = -9.162236 + + f0 = -1.378278e-1 + f1 = -4.211369 + f2 = 4.775187 + f3 = -1.026225e1 + f4 = 8.399763 + f5 = -4.354000e-1 + + g0 = -3.054956e1 + g1 = -4.132305e1 + g2 = 3.292788e2 + g3 = -6.848038e2 + g4 = 4.080244e2 + + h0 = -1.05853e-1 + h1 = -5.776677e-1 + h2 = -1.672435e-2 + h3 = 1.357256e-1 + h4 = 2.172952e-1 + h5 = 3.464156 + h6 = -2.835451 + h7 = -1.098104 + + i0 = -4.126806e-1 + i1 = -1.189974e-1 + i2 = 1.247721 + i3 = -7.391132e-1 + + j0 = 6.250437e-2 + j1 = 6.067723e-1 + j2 = -1.101964 + j3 = 9.100087 + j4 = -1.192672e1 + + k0 = -1.463144e-1 + k1 = -4.07391e-2 + k2 = 3.253159e-2 + k3 = 4.851209e-1 + k4 = 2.978850e-1 + k5 = -3.746393e-1 + k6 = -3.213068e-1 + + l0 = 2.635729e-2 + l1 = -2.192910e-2 + l2 = -3.152901e-3 + l3 = -5.817803e-2 + l4 = 4.516159e-1 + l5 = -4.928702e-1 + l6 = -1.579864e-2 + + m0 = -2.029370e-2 + m1 = 4.660702e-2 + m2 = -6.012308e-1 + m3 = -8.062977e-2 + m4 = 8.320429e-2 + m5 = 5.018538e-1 + m6 = 6.378864e-1 + m7 = 4.226356e-1 + + n0 = -5.19153 + n1 = -3.554716 + n2 = -3.598636e1 + n3 = 2.247355e2 + n4 = -4.120991e2 + n5 = 2.411750e2 + + o0 = 2.993363e-1 + o1 = 6.594004e-2 + o2 = -2.003125e-1 + o3 = -6.233977e-2 + o4 = -2.107885 + o5 = 2.141420 + o6 = 8.476901e-1 + + p0 = 2.677652e-2 + p1 = -3.298246e-1 + p2 = 1.926178e-1 + p3 = 4.013325 + p4 = -4.404302 + + q0 = -3.698756e-1 + q1 = -1.167551e-1 + q2 = -7.641297e-1 + + r0 = -3.348717e-2 + r1 = 4.276655e-2 + r2 = 6.573646e-3 + r3 = 3.535831e-1 + r4 = -1.373308 + r5 = 1.237582 + r6 = 2.302543e-1 + r7 = -2.512876e-1 + r8 = 1.588105e-1 + r9 = -5.199526e-1 + + s0 = -8.115894e-2 + s1 = -1.156580e-2 + s2 = 2.514167e-2 + s3 = 2.038748e-1 + s4 = -3.337476e-1 + s5 = 1.004297e-1 + + ## + Cx0 = a0 + a1 * alpha + a2 * de**2 + a3 * de + a4 * alpha * de + a5 * alpha**2 + a6 * alpha**3 + Cxq = b0 + b1 * alpha + b2 * alpha**2 + b3 * alpha**3 + b4 * alpha**4 + Cy0 = c0 * beta + c1 * da + c2 * dr + Cyp = d0 + d1 * alpha + d2 * alpha**2 + d3 * alpha**3 + Cyr = e0 + e1 * alpha + e2 * alpha**2 + e3 * alpha**3 + Cz0 = (f0 + f1 * alpha + f2 * alpha**2 + f3 * alpha**3 + f4 * alpha**4) * (1 - beta**2) + f5 * de + Czq = g0 + g1 * alpha + g2 * alpha**2 + g3 * alpha**3 + g4 * alpha**4 + Cl0 = h0 * beta + h1 * alpha * beta + h2 * alpha**2 * beta + h3 * beta**2 + h4 * alpha * beta**2 + h5 * \ + alpha**3 * beta + h6 * alpha**4 * beta + h7 * alpha**2 * beta**2 + Clp = i0 + i1 * alpha + i2 * alpha**2 + i3 * alpha**3 + Clr = j0 + j1 * alpha + j2 * alpha**2 + j3 * alpha**3 + j4 * alpha**4 + Clda = k0 + k1 * alpha + k2 * beta + k3 * alpha**2 + k4 * alpha * beta + k5 * alpha**2 * beta + k6 * alpha**3 + Cldr = l0 + l1 * alpha + l2 * beta + l3 * alpha * beta + l4 * alpha**2 * beta + l5 * alpha**3 * beta + l6 * beta**2 + Cm0 = m0 + m1 * alpha + m2 * de + m3 * alpha * de + m4 * de**2 + m5 * alpha**2 * de + m6 * de**3 + m7 * \ + alpha * de**2 + + + Cmq = n0 + n1 * alpha + n2 * alpha**2 + n3 * alpha**3 + n4 * alpha**4 + n5 * alpha**5 + Cn0 = o0 * beta + o1 * alpha * beta + o2 * beta**2 + o3 * alpha * beta**2 + o4 * alpha**2 * beta + o5 * \ + alpha**2 * beta**2 + o6 * alpha**3 * beta + Cnp = p0 + p1 * alpha + p2 * alpha**2 + p3 * alpha**3 + p4 * alpha**4 + Cnr = q0 + q1 * alpha + q2 * alpha**2 + Cnda = r0 + r1 * alpha + r2 * beta + r3 * alpha * beta + r4 * alpha**2 * beta + r5 * alpha**3 * beta + r6 * \ + alpha**2 + r7 * alpha**3 + r8 * beta**3 + r9 * alpha * beta**3 + Cndr = s0 + s1 * alpha + s2 * beta + s3 * alpha * beta + s4 * alpha**2 * beta + s5 * alpha**2 + ## + + Cx = Cx0 + Cxq * qhat + Cy = Cy0 + Cyp * phat + Cyr * rhat + Cz = Cz0 + Czq * qhat + Cl = Cl0 + Clp * phat + Clr * rhat + Clda * da + Cldr * dr + Cm = Cm0 + Cmq * qhat + Cz * (xcgref - xcg) + Cn = Cn0 + Cnp * phat + Cnr * rhat + Cnda * da + Cndr * dr - Cy * (xcgref - xcg) * (cbar / b) + + return Cx, Cy, Cz, Cl, Cm, Cn diff --git a/demo/F16/aerobench/lowlevel/pdot.py b/demo/F16/aerobench/lowlevel/pdot.py new file mode 100644 index 0000000000000000000000000000000000000000..dbfb1c3200aba86e69fcf4a34090768df018a323 --- /dev/null +++ b/demo/F16/aerobench/lowlevel/pdot.py @@ -0,0 +1,29 @@ +''' +Stanley Bak +Python F-16 +power derivative (pdot) +''' + +from aerobench.lowlevel.rtau import rtau + +def pdot(p3, p1): + 'pdot function' + + if p1 >= 50: + if p3 >= 50: + t = 5 + p2 = p1 + else: + p2 = 60 + t = rtau(p2 - p3) + else: + if p3 >= 50: + t = 5 + p2 = 40 + else: + p2 = p1 + t = rtau(p2 - p3) + + pd = t * (p2 - p3) + + return pd diff --git a/demo/F16/aerobench/lowlevel/rtau.py b/demo/F16/aerobench/lowlevel/rtau.py new file mode 100644 index 0000000000000000000000000000000000000000..a2f00d6e32fc1182722af5c9a8c025b5e88138db --- /dev/null +++ b/demo/F16/aerobench/lowlevel/rtau.py @@ -0,0 +1,18 @@ +''' +Stanley Bak +Python F-16 + +Rtau function +''' + +def rtau(dp): + 'rtau function' + + if dp <= 25: + rt = 1.0 + elif dp >= 50: + rt = .1 + else: + rt = 1.9 - .036 * dp + + return rt diff --git a/demo/F16/aerobench/lowlevel/subf16_model.py b/demo/F16/aerobench/lowlevel/subf16_model.py new file mode 100644 index 0000000000000000000000000000000000000000..b807b16520a8b735bef9bc17890c9c0219b4c69d --- /dev/null +++ b/demo/F16/aerobench/lowlevel/subf16_model.py @@ -0,0 +1,201 @@ +''' +Stanley Bak +Python F-16 subf16 +outputs aircraft state vector deriative +''' + +# x[0] = air speed, VT (ft/sec) +# x[1] = angle of attack, alpha (rad) +# x[2] = angle of sideslip, beta (rad) +# x[3] = roll angle, phi (rad) +# x[4] = pitch angle, theta (rad) +# x[5] = yaw angle, psi (rad) +# x[6] = roll rate, P (rad/sec) +# x[7] = pitch rate, Q (rad/sec) +# x[8] = yaw rate, R (rad/sec) +# x[9] = northward horizontal displacement, pn (feet) +# x[10] = eastward horizontal displacement, pe (feet) +# x[11] = altitude, h (feet) +# x[12] = engine thrust dynamics lag state, pow +# +# u[0] = throttle command 0.0 < u(1) < 1.0 +# u[1] = elevator command in degrees +# u[2] = aileron command in degrees +# u[3] = rudder command in degrees +# + +from math import sin, cos, pi + +from aerobench.lowlevel.adc import adc +from aerobench.lowlevel.tgear import tgear +from aerobench.lowlevel.pdot import pdot +from aerobench.lowlevel.thrust import thrust +from aerobench.lowlevel.cx import cx +from aerobench.lowlevel.cy import cy +from aerobench.lowlevel.cz import cz +from aerobench.lowlevel.cl import cl +from aerobench.lowlevel.dlda import dlda +from aerobench.lowlevel.dldr import dldr +from aerobench.lowlevel.cm import cm +from aerobench.lowlevel.cn import cn +from aerobench.lowlevel.dnda import dnda +from aerobench.lowlevel.dndr import dndr +from aerobench.lowlevel.dampp import dampp + +from aerobench.lowlevel.morellif16 import Morellif16 + +def subf16_model(x, u, model, adjust_cy=True): + '''output aircraft state vector derivative for a given input + + The reference for the model is Appendix A of Stevens & Lewis + ''' + + assert model in ['stevens', 'morelli'] + assert len(x) == 13 + assert len(u) == 4 + + xcg = 0.35 + + thtlc, el, ail, rdr = u + + s = 300 + b = 30 + cbar = 11.32 + rm = 1.57e-3 + xcgr = .35 + he = 160.0 + c1 = -.770 + c2 = .02755 + c3 = 1.055e-4 + c4 = 1.642e-6 + c5 = .9604 + c6 = 1.759e-2 + c7 = 1.792e-5 + c8 = -.7336 + c9 = 1.587e-5 + rtod = 57.29578 + g = 32.17 + + xd = x.copy() + vt = x[0] + alpha = x[1]*rtod + beta = x[2]*rtod + phi = x[3] + theta = x[4] + psi = x[5] + p = x[6] + q = x[7] + r = x[8] + alt = x[11] + power = x[12] + + # air data computer and engine model + amach, qbar = adc(vt, alt) + cpow = tgear(thtlc) + + xd[12] = pdot(power, cpow) + + t = thrust(power, alt, amach) + dail = ail/20 + drdr = rdr/30 + + # component build up + + if model == 'stevens': + # stevens & lewis (look up table version) + cxt = cx(alpha, el) + cyt = cy(beta, ail, rdr) + czt = cz(alpha, beta, el) + + clt = cl(alpha, beta) + dlda(alpha, beta) * dail + dldr(alpha, beta) * drdr + cmt = cm(alpha, el) + cnt = cn(alpha, beta) + dnda(alpha, beta) * dail + dndr(alpha, beta) * drdr + else: + # morelli model (polynomial version) + cxt, cyt, czt, clt, cmt, cnt = Morellif16(alpha*pi/180, beta*pi/180, el*pi/180, ail*pi/180, rdr*pi/180, \ + p, q, r, cbar, b, vt, xcg, xcgr) + + # add damping derivatives + + tvt = .5 / vt + b2v = b * tvt + cq = cbar * q * tvt + + # get ready for state equations + d = dampp(alpha) + cxt = cxt + cq * d[0] + cyt = cyt + b2v * (d[1] * r + d[2] * p) + czt = czt + cq * d[3] + clt = clt + b2v * (d[4] * r + d[5] * p) + cmt = cmt + cq * d[6] + czt * (xcgr-xcg) + cnt = cnt + b2v * (d[7] * r + d[8] * p)-cyt * (xcgr-xcg) * cbar/b + cbta = cos(x[2]) + u = vt * cos(x[1]) * cbta + v = vt * sin(x[2]) + w = vt * sin(x[1]) * cbta + sth = sin(theta) + cth = cos(theta) + sph = sin(phi) + cph = cos(phi) + spsi = sin(psi) + cpsi = cos(psi) + qs = qbar * s + qsb = qs * b + rmqs = rm * qs + gcth = g * cth + qsph = q * sph + ay = rmqs * cyt + az = rmqs * czt + + # force equations + udot = r * v-q * w-g * sth + rm * (qs * cxt + t) + vdot = p * w-r * u + gcth * sph + ay + wdot = q * u-p * v + gcth * cph + az + dum = (u * u + w * w) + + xd[0] = (u * udot + v * vdot + w * wdot)/vt + xd[1] = (u * wdot-w * udot)/dum + xd[2] = (vt * vdot-v * xd[0]) * cbta/dum + + # kinematics + xd[3] = p + (sth/cth) * (qsph + r * cph) + xd[4] = q * cph-r * sph + xd[5] = (qsph + r * cph)/cth + + # moments + xd[6] = (c2 * p + c1 * r + c4 * he) * q + qsb * (c3 * clt + c4 * cnt) + + xd[7] = (c5 * p-c7 * he) * r + c6 * (r * r-p * p) + qs * cbar * c7 * cmt + xd[8] = (c8 * p-c2 * r + c9 * he) * q + qsb * (c4 * clt + c9 * cnt) + + # navigation + t1 = sph * cpsi + t2 = cph * sth + t3 = sph * spsi + s1 = cth * cpsi + s2 = cth * spsi + s3 = t1 * sth-cph * spsi + s4 = t3 * sth + cph * cpsi + s5 = sph * cth + s6 = t2 * cpsi + t3 + s7 = t2 * spsi-t1 + s8 = cph * cth + xd[9] = u * s1 + v * s3 + w * s6 # north speed + xd[10] = u * s2 + v * s4 + w * s7 # east speed + xd[11] = u * sth-v * s5-w * s8 # vertical speed + + # outputs + + xa = 15.0 # sets distance normal accel is in front of the c.g. (xa = 15.0 at pilot) + az = az-xa * xd[7] # moves normal accel in front of c.g. + + #################################### + ###### peter additions below ###### + if adjust_cy: + ay = ay+xa*xd[8] # moves side accel in front of c.g. + + # For extraction of Nz + Nz = (-az / g) - 1 # zeroed at 1 g, positive g = pulling up + Ny = ay / g + + return xd, Nz, Ny, az, ay diff --git a/demo/F16/aerobench/lowlevel/tgear.py b/demo/F16/aerobench/lowlevel/tgear.py new file mode 100644 index 0000000000000000000000000000000000000000..8c0fe4735b581a4e947099e12c2a671f68b7ab59 --- /dev/null +++ b/demo/F16/aerobench/lowlevel/tgear.py @@ -0,0 +1,14 @@ +''' +Stanley Bak +Python F-16 GCAS +''' + +def tgear(thtl): + 'tgear function' + + if thtl <= .77: + tg = 64.94 * thtl + else: + tg = 217.38 * thtl - 117.38 + + return tg diff --git a/demo/F16/aerobench/lowlevel/thrust.py b/demo/F16/aerobench/lowlevel/thrust.py new file mode 100644 index 0000000000000000000000000000000000000000..3de5f8b18481376a24d7031c2b1c96af08e75e91 --- /dev/null +++ b/demo/F16/aerobench/lowlevel/thrust.py @@ -0,0 +1,76 @@ +''' +Stanle Bak +Python F-16 +Thrust function +''' + +import numpy as np + +from aerobench.util import fix + +def thrust(power, alt, rmach): + 'thrust lookup-table version' + + a = np.array([[1060, 670, 880, 1140, 1500, 1860], \ + [635, 425, 690, 1010, 1330, 1700], \ + [60, 25, 345, 755, 1130, 1525], \ + [-1020, -170, -300, 350, 910, 1360], \ + [-2700, -1900, -1300, -247, 600, 1100], \ + [-3600, -1400, -595, -342, -200, 700]], dtype=float).T + + b = np.array([[12680, 9150, 6200, 3950, 2450, 1400], \ + [12680, 9150, 6313, 4040, 2470, 1400], \ + [12610, 9312, 6610, 4290, 2600, 1560], \ + [12640, 9839, 7090, 4660, 2840, 1660], \ + [12390, 10176, 7750, 5320, 3250, 1930], \ + [11680, 9848, 8050, 6100, 3800, 2310]], dtype=float).T + + c = np.array([[20000, 15000, 10800, 7000, 4000, 2500], \ + [21420, 15700, 11225, 7323, 4435, 2600], \ + [22700, 16860, 12250, 8154, 5000, 2835], \ + [24240, 18910, 13760, 9285, 5700, 3215], \ + [26070, 21075, 15975, 11115, 6860, 3950], \ + [28886, 23319, 18300, 13484, 8642, 5057]], dtype=float).T + + if alt < 0: + alt = 0.01 # uh, why not 0? + + h = .0001 * alt + + i = fix(h) + + if i >= 5: + i = 4 + + dh = h - i + rm = 5 * rmach + m = fix(rm) + + if m >= 5: + m = 4 + elif m <= 0: + m = 0 + + dm = rm - m + cdh = 1 - dh + + # do not increment these, since python is 0-indexed while matlab is 1-indexed + #i = i + 1 + #m = m + 1 + + s = b[i, m] * cdh + b[i + 1, m] * dh + t = b[i, m + 1] * cdh + b[i + 1, m + 1] * dh + tmil = s + (t - s) * dm + + if power < 50: + s = a[i, m] * cdh + a[i + 1, m] * dh + t = a[i, m + 1] * cdh + a[i + 1, m + 1] * dh + tidl = s + (t - s) * dm + thrst = tidl + (tmil - tidl) * power * .02 + else: + s = c[i, m] * cdh + c[i + 1, m] * dh + t = c[i, m + 1] * cdh + c[i + 1, m + 1] * dh + tmax = s + (t - s) * dm + thrst = tmil + (tmax - tmil) * (power - 50) * .02 + + return thrst diff --git a/demo/F16/aerobench/util.py b/demo/F16/aerobench/util.py new file mode 100644 index 0000000000000000000000000000000000000000..cf2c4796629e0f3416452ab99ef538d9c973ce57 --- /dev/null +++ b/demo/F16/aerobench/util.py @@ -0,0 +1,287 @@ +''' +Utilities for F-16 GCAS +''' + +from math import floor, ceil +import numpy as np + +class StateIndex: + 'list of static state indices' + + VT = 0 + VEL = 0 # alias + + ALPHA = 1 + BETA = 2 + PHI = 3 # roll angle + THETA = 4 # pitch angle + PSI = 5 # yaw angle + + P = 6 + Q = 7 + R = 8 + + POSN = 9 + POS_N = 9 + + POSE = 10 + POS_E = 10 + + ALT = 11 + H = 11 + + POW = 12 + +class Freezable(): + 'a class where you can freeze the fields (prevent new fields from being created)' + + _frozen = False + + def freeze_attrs(self): + 'prevents any new attributes from being created in the object' + self._frozen = True + + def __setattr__(self, key, value): + if self._frozen and not hasattr(self, key): + raise TypeError("{} does not contain attribute '{}' (object was frozen)".format(self, key)) + + object.__setattr__(self, key, value) + +class Euler(Freezable): + '''fixed step euler integration + + loosely based on scipy.integrate.RK45 + ''' + + def __init__(self, der_func, tstart, ystart, tend, step=0, time_tol=1e-9): + assert step > 0, "arg step > 0 required in Euler integrator" + assert tend > tstart + + self.der_func = der_func # signature (t, x) + self.tstep = step + self.t = tstart + self.y = ystart.copy() + self.yprev = None + self.tprev = None + self.tend = tend + + self.status = 'running' + + self.time_tol = time_tol + + self.freeze_attrs() + + def step(self): + 'take one step' + + if self.status == 'running': + self.yprev = self.y.copy() + self.tprev = self.t + yd = self.der_func(self.t, self.y) + + self.t += self.tstep + + if self.t + self.time_tol >= self.tend: + self.t = self.tend + + dt = self.t - self.tprev + self.y += dt * yd + + if self.t == self.tend: + self.status = 'finished' + + def dense_output(self): + 'return a function of time' + + assert self.tprev is not None + + dy = self.y - self.yprev + dt = self.t - self.tprev + + dydt = dy / dt + + def fun(t): + 'return state at time t (linear interpolation)' + + deltat = t - self.tprev + + return self.yprev + dydt * deltat + + return fun + +def get_state_names(): + 'returns a list of state variable names' + + return ['vt', 'alpha', 'beta', 'phi', 'theta', 'psi', 'P', 'Q', 'R', 'pos_n', 'pos_e', 'alt', 'pow'] + +def printmat(mat, main_label, row_label_str, col_label_str): + 'print a matrix' + + if isinstance(row_label_str, list) and len(row_label_str) == 0: + row_label_str = None + + assert isinstance(main_label, str) + assert row_label_str is None or isinstance(row_label_str, str) + assert isinstance(col_label_str, str) + + mat = np.array(mat) + if len(mat.shape) == 1: + mat.shape = (1, mat.shape[0]) # one-row matrix + + print("{main_label} =") + + row_labels = None if row_label_str is None else row_label_str.split(' ') + col_labels = col_label_str.split(' ') + + width = 7 + + width = max(width, max([len(l) for l in col_labels])) + + if row_labels is not None: + width = max(width, max([len(l) for l in row_labels])) + + width += 1 + + # add blank space for row labels + if row_labels is not None: + print("{: <{}}".format('', width), end='') + + # print col lables + for col_label in col_labels: + if len(col_label) > width: + col_label = col_label[:width] + + print("{: >{}}".format(col_label, width), end='') + + print('') + + if row_labels is not None: + assert len(row_labels) == mat.shape[0], \ + "row labels (len={}) expected one element for each row of the matrix ({})".format( \ + len(row_labels), mat.shape[0]) + + for r in range(mat.shape[0]): + row = mat[r] + + if row_labels is not None: + label = row_labels[r] + + if len(label) > width: + label = label[:width] + + print("{:<{}}".format(label, width), end='') + + for num in row: + #print("{:#<{}}".format(num, width), end='') + print("{:{}.{}g}".format(num, width, width-3), end='') + + print('') + + +def fix(ele): + 'round towards zero' + + assert isinstance(ele, float) + + if ele > 0: + rv = int(floor(ele)) + else: + rv = int(ceil(ele)) + + return rv + +def sign(ele): + 'sign of a number' + + if ele < 0: + rv = -1 + elif ele == 0: + rv = 0 + else: + rv = 1 + + return rv + +def extract_single_result(res, index, llc): + 'extract a res object for a sinlge aircraft from a multi-aircraft simulation' + + num_vars = len(get_state_names()) + llc.get_num_integrators() + num_aircraft = res['states'][0].size // num_vars + + if num_aircraft == 1: + assert index == 0 + rv = res + else: + rv = {} + rv['status'] = res['status'] + rv['times'] = res['times'] + rv['modes'] = res['modes'] + + full_states = res['states'] + rv['states'] = full_states[:, num_vars*index:num_vars*(index+1)] + + if 'xd_list' in res: + # extended states + key_list = ['xd_list', 'ps_list', 'Nz_list', 'Ny_r_list', 'u_list'] + + for key in key_list: + rv[key] = [tup[index] for tup in res[key]] + + return rv + +class SafetyLimits(Freezable): + 'a class for holding a set of safety limits.' + + def __init__(self, **kwargs): + self.altitude = kwargs['altitude'] if 'altitude' in kwargs and kwargs['altitude'] is not None else None + self.Nz = kwargs['Nz'] if 'Nz' in kwargs and kwargs['Nz'] is not None else None + self.v = kwargs['v'] if 'v' in kwargs and kwargs['v'] is not None else None + self.alpha = kwargs['alpha'] if 'alpha' in kwargs and kwargs['alpha'] is not None else None + + self.psMaxAccelDeg = kwargs['psMaxAccelDeg'] if 'psMaxAccelDeg' in kwargs and kwargs['psMaxAccelDeg'] is not None else None + self.betaMaxDeg = kwargs['betaMaxDeg'] if 'betaMaxDeg' in kwargs and kwargs['betaMaxDeg'] is not None else None + + self.freeze_attrs() + +class SafetyLimitsVerifier(Freezable): + 'given some limits (in a SafetyLimits) and optional low-level controller (in a LowLevelController), verify whether the simulation results are safe.' + + def __init__(self, safety_limits, llc=None): + self.safety_limits = safety_limits + self.llc = llc + + def verify(self, results): + # Determine the number of state variables per tick of the simulation. + if self.llc is not None: + num_state_vars = len(get_state_names()) + \ + self.llc.get_num_integrators() + else: + num_state_vars = len(get_state_names()) + # Check whether the results are sane. + assert (results['states'].size % num_state_vars) == 0, \ + "Wrong number of state variables." + + # Go through each tick of the simulation and determine whether + # the object(s) was (were) in a safe state. + for i in range(results['states'].size // num_state_vars): + _vt, alpha, beta, _phi, \ + _theta, _psi, _p, _q, \ + _r, _pos_n, _pos_e, alt, \ + _, _, _, _ = results['states'][i] + nz = results['Nz_list'][i] + ps = results['ps_list'][i] + + if self.safety_limits.altitude is not None: + assert self.safety_limits.altitude[0] <= alt <= self.safety_limits.altitude[1], "Altitude ({}) is not within the specified limits ({}, {}).".format(alt, self.safety_limits.altitude[0], self.safety_limits.altitude[1]) + + if self.safety_limits.Nz is not None: + assert self.safety_limits.Nz[0] <= nz <= self.safety_limits.Nz[1], "Nz ({}) is not within the specified limits ({}, {}).".format(nz, self.safety_limits.Nz[0], self.safety_limits.Nz[1]) + + if self.safety_limits.alpha is not None: + assert self.safety_limits.alpha[0] <= alpha <= self.safety_limits.alpha[1], "alpha ({}) is not within the specified limits ({}, {}).".format(nz, self.safety_limits.alpha[0], self.safety_limits.alpha[1]) + + if self.safety_limits.psMaxAccelDeg is not None: + assert ps <= self.safety_limits.psMaxAccelDeg, "Ps is not less than the specified max." + + if self.safety_limits.betaMaxDeg is not None: + assert beta <= self.safety_limits.betaMaxDeg, "Beta is not less than the specified max." diff --git a/demo/F16/aerobench/visualize/anim3d.py b/demo/F16/aerobench/visualize/anim3d.py new file mode 100644 index 0000000000000000000000000000000000000000..9042af44979c1f5ef92f4da2ccf967047526fe18 --- /dev/null +++ b/demo/F16/aerobench/visualize/anim3d.py @@ -0,0 +1,404 @@ +''' +3d plotting utilities for aerobench +''' + +import math +import time +import os +import traceback + +from scipy.io import loadmat + +import numpy as np +from numpy import rad2deg + +# these imports are needed for 3d plotting +from mpl_toolkits.mplot3d import Axes3D +from mpl_toolkits.mplot3d.art3d import Line3D, Poly3DCollection + +import matplotlib +import matplotlib.animation as animation +import matplotlib.pyplot as plt + +from aerobench.visualize import plot +from aerobench.util import StateIndex + +def get_script_path(filename=__file__): + '''get the path this script''' + return os.path.dirname(os.path.realpath(filename)) + +def make_anim(res, filename, viewsize=1000, viewsize_z=1000, f16_scale=30, trail_pts=60, + elev=30, azim=45, skip_frames=None, chase=False, fixed_floor=False, + init_extra=None, update_extra=None): + ''' + make a 3d plot of the GCAS maneuver. + + see examples/anim3d folder for examples on usage + ''' + + plot.init_plot() + start = time.time() + + if not isinstance(res, list): + res = [res] + + if not isinstance(viewsize, list): + viewsize = [viewsize] + + if not isinstance(viewsize_z, list): + viewsize_z = [viewsize_z] + + if not isinstance(f16_scale, list): + f16_scale = [f16_scale] + + if not isinstance(trail_pts, list): + trail_pts = [trail_pts] + + if not isinstance(elev, list): + elev = [elev] + + if not isinstance(azim, list): + azim = [azim] + + if not isinstance(skip_frames, list): + skip_frames = [skip_frames] + + if not isinstance(chase, list): + chase = [chase] + + if not isinstance(fixed_floor, list): + fixed_floor = [fixed_floor] + + if not isinstance(init_extra, list): + init_extra = [init_extra] + + if not isinstance(update_extra, list): + update_extra = [update_extra] + + ##### + # fill in defaults + if filename == '': + full_plot = False + else: + full_plot = True + + for i, skip in enumerate(skip_frames): + if skip is not None: + continue + + if filename == '': # plot to the screen + skip_frames[i] = 5 + elif filename.endswith('.gif'): + skip_frames[i] = 2 + else: + skip_frames[i] = 1 # plot every frame + + if filename == '': + filename = None + + ## + all_times = [] + all_states = [] + all_modes = [] + all_ps_list = [] + all_Nz_list = [] + + for r, skip in zip(res, skip_frames): + t = r['times'] + s = r['states'] + m = r['modes'] + ps = r['ps_list'] + Nz = r['Nz_list'] + + t = t[0::skip] + s = s[0::skip] + m = m[0::skip] + ps = ps[0::skip] + Nz = Nz[0::skip] + + all_times.append(t) + all_states.append(s) + all_modes.append(m) + all_ps_list.append(ps) + all_Nz_list.append(Nz) + + ## + + fig = plt.figure(figsize=(8, 7)) + ax = fig.add_subplot(111, projection='3d') + + ## + + parent = get_script_path() + plane_point_data = os.path.join(parent, 'f-16.mat') + + data = loadmat(plane_point_data) + f16_pts = data['V'] + f16_faces = data['F'] + + plane_polys = Poly3DCollection([], color=None if full_plot else 'k') + ax.add_collection3d(plane_polys) + + ax.set_xlabel('X [ft]', fontsize=14) + ax.set_ylabel('Y [ft]', fontsize=14) + ax.set_zlabel('Altitude [ft]', fontsize=14) + + # text + fontsize = 14 + time_text = ax.text2D(0.05, 0.97, "", transform=ax.transAxes, fontsize=fontsize) + mode_text = ax.text2D(0.95, 0.97, "", transform=ax.transAxes, fontsize=fontsize, horizontalalignment='right') + + alt_text = ax.text2D(0.05, 0.93, "", transform=ax.transAxes, fontsize=fontsize) + v_text = ax.text2D(0.95, 0.93, "", transform=ax.transAxes, fontsize=fontsize, horizontalalignment='right') + + alpha_text = ax.text2D(0.05, 0.89, "", transform=ax.transAxes, fontsize=fontsize) + beta_text = ax.text2D(0.95, 0.89, "", transform=ax.transAxes, fontsize=fontsize, horizontalalignment='right') + + nz_text = ax.text2D(0.05, 0.85, "", transform=ax.transAxes, fontsize=fontsize) + ps_text = ax.text2D(0.95, 0.85, "", transform=ax.transAxes, fontsize=fontsize, horizontalalignment='right') + + ang_text = ax.text2D(0.5, 0.81, "", transform=ax.transAxes, fontsize=fontsize, horizontalalignment='center') + + trail_line, = ax.plot([], [], [], color='r', lw=2, zorder=50) + + extra_lines = [] + + for func in init_extra: + if func is not None: + extra_lines.append(func(ax)) + else: + extra_lines.append([]) + + first_frames = [] + frames = 0 + + for t in all_times: + first_frames.append(frames) + frames += len(t) + + def anim_func(global_frame): + 'updates for the animation frame' + + index = 0 + first_frame = False + + for i, f in enumerate(first_frames): + if global_frame >= f: + index = i + + if global_frame == f: + first_frame = True + break + + frame = global_frame - first_frames[index] + states = all_states[index] + times = all_times[index] + modes = all_modes[index] + Nz_list = all_Nz_list[index] + ps_list = all_ps_list[index] + + print(f"Frame: {global_frame}/{frames} - Index {index} frame {frame}/{len(times)}") + + speed = states[frame][0] + alpha = states[frame][1] + beta = states[frame][2] + alt = states[frame][11] + + phi = states[frame][StateIndex.PHI] + theta = states[frame][StateIndex.THETA] + psi = states[frame][StateIndex.PSI] + + dx = states[frame][StateIndex.POS_E] + dy = states[frame][StateIndex.POS_N] + dz = states[frame][StateIndex.ALT] + + if first_frame: + ax.view_init(elev[index], azim[index]) + + for i, lines in enumerate(extra_lines): + for line in lines: + line.set_visible(i == index) + + time_text.set_text('t = {:.2f} sec'.format(times[frame])) + + if chase[index]: + ax.view_init(elev[index], rad2deg(-psi) - 90.0) + + colors = ['red', 'blue', 'green', 'magenta'] + + mode_names = [] + + for mode in modes: + if not mode in mode_names: + mode_names.append(mode) + + mode = modes[frame] + mode_index = modes.index(mode) + col = colors[mode_index % len(colors)] + mode_text.set_color(col) + mode_text.set_text('Mode: {}'.format(mode.capitalize())) + + alt_text.set_text('h = {:.2f} ft'.format(alt)) + v_text.set_text('V = {:.2f} ft/sec'.format(speed)) + + alpha_text.set_text('$\\alpha$ = {:.2f} deg'.format(rad2deg(alpha))) + beta_text.set_text('$\\beta$ = {:.2f} deg'.format(rad2deg(beta))) + + nz_text.set_text('$N_z$ = {:.2f} g'.format(Nz_list[frame])) + ps_text.set_text('$p_s$ = {:.2f} deg/sec'.format(rad2deg(ps_list[frame]))) + + ang_text.set_text('[$\\phi$, $\\theta$, $\\psi$] = [{:.2f}, {:.2f}, {:.2f}] deg'.format(\ + rad2deg(phi), rad2deg(theta), rad2deg(psi))) + + s = f16_scale[index] + s = 25 if s is None else s + pts = scale3d(f16_pts, [-s, s, s]) + + pts = rotate3d(pts, theta, psi - math.pi/2, -phi) + + size = viewsize[index] + size = 1000 if size is None else size + minx = dx - size + maxx = dx + size + miny = dy - size + maxy = dy + size + + vz = viewsize_z[index] + vz = 1000 if vz is None else vz + + if fixed_floor[index]: + minz = 0 + maxz = vz + else: + minz = dz - vz + maxz = dz + vz + + ax.set_xlim([minx, maxx]) + ax.set_ylim([miny, maxy]) + ax.set_zlim([minz, maxz]) + + verts = [] + fc = [] + ec = [] + count = 0 + + # draw ground + if minz <= 0 <= maxz: + z = 0 + verts.append([(minx, miny, z), (maxx, miny, z), (maxx, maxy, z), (minx, maxy, z)]) + fc.append('0.8') + ec.append('0.8') + + # draw f16 + for face in f16_faces: + face_pts = [] + + count = count + 1 + + if not full_plot and count % 10 != 0: + continue + + for findex in face: + face_pts.append((pts[findex-1][0] + dx, \ + pts[findex-1][1] + dy, \ + pts[findex-1][2] + dz)) + + verts.append(face_pts) + fc.append('0.2') + ec.append('0.2') + + plane_polys.set_verts(verts) + plane_polys.set_facecolor(fc) + plane_polys.set_edgecolor(ec) + + # do trail + t = trail_pts[index] + t = 200 if t is None else t + trail_len = t // skip_frames[index] + start_index = max(0, frame-trail_len) + + pos_xs = [pt[StateIndex.POS_E] for pt in states] + pos_ys = [pt[StateIndex.POS_N] for pt in states] + pos_zs = [pt[StateIndex.ALT] for pt in states] + + trail_line.set_data(np.asarray(pos_xs[start_index:frame]), np.asarray(pos_ys[start_index:frame])) + trail_line.set_3d_properties(np.asarray(pos_zs[start_index:frame])) + + if update_extra[index] is not None: + update_extra[index](frame) + + plt.tight_layout() + + interval = 30 + + if filename.endswith('.gif'): + interval = 60 + + anim_obj = animation.FuncAnimation(fig, anim_func, frames, interval=interval, \ + blit=False, repeat=True) + + if filename is not None: + + if filename.endswith('.gif'): + print("\nSaving animation to '{}' using 'imagemagick'...".format(filename)) + anim_obj.save(filename, dpi=60, writer='imagemagick') # dpi was 80 + print("Finished saving to {} in {:.1f} sec".format(filename, time.time() - start)) + else: + fps = 40 + codec = 'libx264' + + print("\nSaving '{}' at {:.2f} fps using ffmpeg with codec '{}'.".format(filename, fps, codec)) + + # if this fails do: 'sudo apt-get install ffmpeg' + try: + extra_args = [] + + if codec is not None: + extra_args += ['-vcodec', str(codec)] + + anim_obj.save(filename, fps=fps, extra_args=extra_args) + print("Finished saving to {} in {:.1f} sec".format(filename, time.time() - start)) + except AttributeError: + traceback.print_exc() + print("\nSaving video file failed! Is ffmpeg installed? Can you run 'ffmpeg' in the terminal?") + else: + plt.show() + +def scale3d(pts, scale_list): + 'scale a 3d ndarray of points, and return the new ndarray' + + assert len(scale_list) == 3 + + rv = np.zeros(pts.shape) + + for i in range(pts.shape[0]): + for d in range(3): + rv[i, d] = scale_list[d] * pts[i, d] + + return rv + +def rotate3d(pts, theta, psi, phi): + 'rotates an ndarray of 3d points, returns new list' + + sinTheta = math.sin(theta) + cosTheta = math.cos(theta) + sinPsi = math.sin(psi) + cosPsi = math.cos(psi) + sinPhi = math.sin(phi) + cosPhi = math.cos(phi) + + transform_matrix = np.array([ \ + [cosPsi * cosTheta, -sinPsi * cosTheta, sinTheta], \ + [cosPsi * sinTheta * sinPhi + sinPsi * cosPhi, \ + -sinPsi * sinTheta * sinPhi + cosPsi * cosPhi, \ + -cosTheta * sinPhi], \ + [-cosPsi * sinTheta * cosPhi + sinPsi * sinPhi, \ + sinPsi * sinTheta * cosPhi + cosPsi * sinPhi, \ + cosTheta * cosPhi]], dtype=float) + + rv = np.zeros(pts.shape) + + for i in range(pts.shape[0]): + rv[i] = np.dot(pts[i], transform_matrix) + + return rv diff --git a/demo/F16/aerobench/visualize/bak_matplotlib.mlpstyle b/demo/F16/aerobench/visualize/bak_matplotlib.mlpstyle new file mode 100644 index 0000000000000000000000000000000000000000..58c7faed1401e5334c5489c2343574ae9036ae4e --- /dev/null +++ b/demo/F16/aerobench/visualize/bak_matplotlib.mlpstyle @@ -0,0 +1,8 @@ +font.family : serif +xtick.labelsize : 14 +ytick.labelsize : 14 + +axes.labelsize : 20 +axes.titlesize : 28 + +path.simplify : False diff --git a/demo/F16/aerobench/visualize/f-16.mat b/demo/F16/aerobench/visualize/f-16.mat new file mode 100644 index 0000000000000000000000000000000000000000..dd42bdfb56ac815ef1c1cb6259d92a7d7b8c484d Binary files /dev/null and b/demo/F16/aerobench/visualize/f-16.mat differ diff --git a/demo/F16/aerobench/visualize/plot.py b/demo/F16/aerobench/visualize/plot.py new file mode 100644 index 0000000000000000000000000000000000000000..c690145292ed4fdb1fba6ea08dbcf0bfeacd73a0 --- /dev/null +++ b/demo/F16/aerobench/visualize/plot.py @@ -0,0 +1,294 @@ +''' +Stanley Bak +Python code for F-16 animation video output +''' + +import math +import os + +import numpy as np +from numpy import rad2deg + +import matplotlib +import matplotlib.animation as animation +import matplotlib.pyplot as plt + +from aerobench.util import get_state_names, StateIndex + +def get_script_path(filename=__file__): + '''get the path this script''' + return os.path.dirname(os.path.realpath(filename)) + +def init_plot(): + 'initialize plotting style' + + matplotlib.use('TkAgg') # set backend + + parent = get_script_path() + p = os.path.join(parent, 'bak_matplotlib.mlpstyle') + + plt.style.use(['bmh', p]) + +def plot_overhead(run_sim_result, waypoints=None, llc=None): + '''altitude over time plot from run_f16_sum result object + + note: call plt.show() afterwards to have plot show up + ''' + + init_plot() + + res = run_sim_result + fig = plt.figure(figsize=(7, 5)) + + ax = fig.add_subplot(1, 1, 1) + + full_states = res['states'] + + if llc is not None: + num_vars = len(get_state_names()) + llc.get_num_integrators() + num_aircraft = full_states[0, :].size // num_vars + else: + num_vars = full_states[0, :].size + num_aircraft = 1 + + for i in range(num_aircraft): + states = full_states[:, i*num_vars:(i+1)*num_vars] + + ys = states[:, StateIndex.POSN] # 9: n/s position (ft) + xs = states[:, StateIndex.POSE] # 10: e/w position (ft) + + ax.plot(xs, ys, '-') + + label = 'Start' if i == 0 else None + ax.plot([xs[0]], [ys[1]], 'k*', ms=8, label=label) + + if waypoints is not None: + xs = [wp[0] for wp in waypoints] + ys = [wp[1] for wp in waypoints] + + ax.plot(xs, ys, 'ro', label='Waypoints') + + ax.set_ylabel('North / South Position (ft)') + ax.set_xlabel('East / West Position (ft)') + + ax.set_title('Overhead Plot') + + ax.axis('equal') + + ax.legend() + + plt.tight_layout() + +def plot_attitude(run_sim_result, title='Attitude History', skip_yaw=True, figsize=(7, 5)): + 'plot a single variable over time' + + init_plot() + + res = run_sim_result + fig = plt.figure(figsize=figsize) + + ax = fig.add_subplot(1, 1, 1) + ax.ticklabel_format(useOffset=False) + + times = res['times'] + states = res['states'] + + indices = [StateIndex.PHI, StateIndex.THETA, StateIndex.PSI, StateIndex.P, StateIndex.Q, StateIndex.R] + labels = ['Roll (Phi)', 'Pitch (Theta)', 'Yaw (Psi)', 'Roll Rate (P)', 'Pitch Rate (Q)', 'Yaw Rate (R)'] + colors = ['r-', 'g-', 'b-', 'r--', 'g--', 'b--'] + + rad_to_deg_factor = 180 / math.pi + + for index, label, color in zip(indices, labels, colors): + if skip_yaw and index == StateIndex.PSI: + continue + + ys = states[:, index] # 11: altitude (ft) + + ax.plot(times, ys * rad_to_deg_factor, color, label=label) + + ax.set_ylabel('Attitudes & Rates (deg, deg/s)') + ax.set_xlabel('Time (sec)') + + if title is not None: + ax.set_title(title) + + ax.legend() + plt.tight_layout() + +def plot_outer_loop(run_sim_result, title='Outer Loop Controls'): + 'plot a single variable over time' + + init_plot() + + res = run_sim_result + assert 'u_list' in res, "Simulation must be run with extended_states=True" + fig = plt.figure(figsize=(7, 5)) + + ax = fig.add_subplot(1, 1, 1) + ax.ticklabel_format(useOffset=False) + + times = res['times'] + u_list = res['u_list'] + ps_list = res['ps_list'] + nz_list = res['Nz_list'] + ny_r_list = res['Ny_r_list'] + + # u is: throt, ele, ail, rud, Nz_ref, ps_ref, Ny_r_ref + # u_ref is: Nz, ps, Ny + r, throttle + ys_list = [] + + ys_list.append(nz_list) + ys_list.append([u[4] for u in u_list]) + + ys_list.append(ps_list) + ys_list.append([u[5] for u in u_list]) + + ys_list.append(ny_r_list) + ys_list.append([u[6] for u in u_list]) + + # throttle reference is not included... although it's just a small offset so probably less important + ys_list.append([u[0] for u in u_list]) + + labels = ['N_z', 'N_z,ref', 'P_s', 'P_s,ref', 'N_yr', 'N_yr,ref', 'Throttle'] + colors = ['r', 'r', 'lime', 'lime', 'b', 'b', 'c'] + + for i, (ys, label, color) in enumerate(zip(ys_list, labels, colors)): + lt = '-' if i % 2 == 0 else ':' + lw = 1 if i % 2 == 0 else 3 + + ax.plot(times, ys, lt, lw=lw, color=color, label=label) + + ax.set_ylabel('Autopilot (deg & percent)') + ax.set_xlabel('Time (sec)') + + if title is not None: + ax.set_title(title) + + ax.legend() + plt.tight_layout() + +def plot_inner_loop(run_sim_result, title='Inner Loop Controls'): + 'plot inner loop controls over time' + + init_plot() + + res = run_sim_result + assert 'u_list' in res, "Simulation must be run with extended_states=True" + fig = plt.figure(figsize=(7, 5)) + + ax = fig.add_subplot(1, 1, 1) + ax.ticklabel_format(useOffset=False) + + times = res['times'] + u_list = res['u_list'] + + # u is throt, ele, ail, rud, Nz_ref, ps_ref, Ny_r_ref + ys_list = [] + + rad_to_deg_factor = 180 / math.pi + + for i in range(4): + factor = 1.0 if i == 0 else rad_to_deg_factor + ys_list.append([u[i] * factor for u in u_list]) + + labels = ['Throttle', 'Elevator', 'Aileron', 'Rudder'] + colors = ['b-', 'r-', '#FFA500', 'm-'] + + for ys, label, color in zip(ys_list, labels, colors): + ax.plot(times, ys, color, label=label) + + ax.set_ylabel('Controls (deg & percent)') + ax.set_xlabel('Time (sec)') + + if title is not None: + ax.set_title(title) + + ax.legend() + plt.tight_layout() + +def plot_single(run_sim_result, state_name, title=None): + 'plot a single variable over time' + + init_plot() + + res = run_sim_result + fig = plt.figure(figsize=(7, 5)) + + ax = fig.add_subplot(1, 1, 1) + ax.ticklabel_format(useOffset=False) + + times = res['times'] + states = res['states'] + + index = get_state_names().index(state_name) + ys = states[:, index] # 11: altitude (ft) + + ax.plot(times, ys, '-') + + ax.set_ylabel(state_name) + ax.set_xlabel('Time') + + if title is not None: + ax.set_title(title) + + plt.tight_layout() + +def plot_altitude(run_sim_result): + '''altitude over time plot from run_f16_sum result object + + note: call plt.show() afterwards to have plot show up + ''' + + plot_single(run_sim_result, 'alt') + + +def plot2d(filename, times, plot_data_list): + '''plot state variables in 2d + + plot data list of is a list of (values_list, var_data), + where values_list is an 2-d array, the first is time step, the second is a state vector + and each var_data is a list of tuples: (state_index, label) + ''' + + num_plots = sum([len(var_data) for _, var_data in plot_data_list]) + + fig = plt.figure(figsize=(7, 5)) + + for plot_index in range(num_plots): + ax = fig.add_subplot(num_plots, 1, plot_index + 1) + ax.tick_params(axis='both', which='major', labelsize=16) + + sum_plots = 0 + states = None + state_var_data = None + + for values_list, var_data in plot_data_list: + if plot_index < sum_plots + len(var_data): + states = values_list + state_var_data = var_data + break + + sum_plots += len(var_data) + + state_index, label = state_var_data[plot_index - sum_plots] + + if state_index == 0 and isinstance(states[0], float): # state is just a single number + ys = states + else: + ys = [state[state_index] for state in states] + + ax.plot(times, ys, '-') + + ax.set_ylabel(label, fontsize=16) + + # last one gets an x axis label + if plot_index == num_plots - 1: + ax.set_xlabel('Time', fontsize=16) + + plt.tight_layout() + + if filename is not None: + plt.savefig(filename, bbox_inches='tight') + else: + plt.show()