From ce3385e65f67511a9fec2f9b56641af2c6939e19 Mon Sep 17 00:00:00 2001
From: sayanmitracode <sayan.mitra@gmail.com>
Date: Mon, 4 Jul 2022 12:18:32 -0500
Subject: [PATCH] made F16 stanalone

---
 demo/F16/README.md                            |   3 +-
 demo/F16/aerobench/highlevel/autopilot.py     |  97 +++++
 .../F16/aerobench/highlevel/controlled_f16.py |  60 +++
 demo/F16/aerobench/lowlevel/adc.py            |  37 ++
 demo/F16/aerobench/lowlevel/cl.py             |  53 +++
 demo/F16/aerobench/lowlevel/clf16.py          |  85 ++++
 demo/F16/aerobench/lowlevel/cm.py             |  50 +++
 demo/F16/aerobench/lowlevel/cn.py             |  54 +++
 demo/F16/aerobench/lowlevel/conf16.py         |  58 +++
 demo/F16/aerobench/lowlevel/cx.py             |  50 +++
 demo/F16/aerobench/lowlevel/cy.py             |   9 +
 demo/F16/aerobench/lowlevel/cz.py             |  31 ++
 demo/F16/aerobench/lowlevel/dampp.py          |  42 ++
 demo/F16/aerobench/lowlevel/dlda.py           |  50 +++
 demo/F16/aerobench/lowlevel/dldr.py           |  52 +++
 demo/F16/aerobench/lowlevel/dnda.py           |  51 +++
 demo/F16/aerobench/lowlevel/dndr.py           |  49 +++
 .../lowlevel/low_level_controller.py          | 109 +++++
 demo/F16/aerobench/lowlevel/morellif16.py     | 189 ++++++++
 demo/F16/aerobench/lowlevel/pdot.py           |  29 ++
 demo/F16/aerobench/lowlevel/rtau.py           |  18 +
 demo/F16/aerobench/lowlevel/subf16_model.py   | 201 +++++++++
 demo/F16/aerobench/lowlevel/tgear.py          |  14 +
 demo/F16/aerobench/lowlevel/thrust.py         |  76 ++++
 demo/F16/aerobench/util.py                    | 287 +++++++++++++
 demo/F16/aerobench/visualize/anim3d.py        | 404 ++++++++++++++++++
 .../visualize/bak_matplotlib.mlpstyle         |   8 +
 demo/F16/aerobench/visualize/f-16.mat         | Bin 0 -> 32222 bytes
 demo/F16/aerobench/visualize/plot.py          | 294 +++++++++++++
 29 files changed, 2459 insertions(+), 1 deletion(-)
 create mode 100644 demo/F16/aerobench/highlevel/autopilot.py
 create mode 100644 demo/F16/aerobench/highlevel/controlled_f16.py
 create mode 100644 demo/F16/aerobench/lowlevel/adc.py
 create mode 100644 demo/F16/aerobench/lowlevel/cl.py
 create mode 100644 demo/F16/aerobench/lowlevel/clf16.py
 create mode 100644 demo/F16/aerobench/lowlevel/cm.py
 create mode 100644 demo/F16/aerobench/lowlevel/cn.py
 create mode 100644 demo/F16/aerobench/lowlevel/conf16.py
 create mode 100644 demo/F16/aerobench/lowlevel/cx.py
 create mode 100644 demo/F16/aerobench/lowlevel/cy.py
 create mode 100644 demo/F16/aerobench/lowlevel/cz.py
 create mode 100644 demo/F16/aerobench/lowlevel/dampp.py
 create mode 100644 demo/F16/aerobench/lowlevel/dlda.py
 create mode 100644 demo/F16/aerobench/lowlevel/dldr.py
 create mode 100644 demo/F16/aerobench/lowlevel/dnda.py
 create mode 100644 demo/F16/aerobench/lowlevel/dndr.py
 create mode 100644 demo/F16/aerobench/lowlevel/low_level_controller.py
 create mode 100644 demo/F16/aerobench/lowlevel/morellif16.py
 create mode 100644 demo/F16/aerobench/lowlevel/pdot.py
 create mode 100644 demo/F16/aerobench/lowlevel/rtau.py
 create mode 100644 demo/F16/aerobench/lowlevel/subf16_model.py
 create mode 100644 demo/F16/aerobench/lowlevel/tgear.py
 create mode 100644 demo/F16/aerobench/lowlevel/thrust.py
 create mode 100644 demo/F16/aerobench/util.py
 create mode 100644 demo/F16/aerobench/visualize/anim3d.py
 create mode 100644 demo/F16/aerobench/visualize/bak_matplotlib.mlpstyle
 create mode 100644 demo/F16/aerobench/visualize/f-16.mat
 create mode 100644 demo/F16/aerobench/visualize/plot.py

diff --git a/demo/F16/README.md b/demo/F16/README.md
index 654c1430..d1cc352b 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 00000000..d369cdc8
--- /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 00000000..c7ee1159
--- /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 00000000..a70390eb
--- /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 00000000..6bff3591
--- /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 00000000..a09addfe
--- /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 00000000..4c00a175
--- /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 00000000..b06e3729
--- /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 00000000..f322a6de
--- /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 00000000..c23a96cb
--- /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 00000000..fac8c8c3
--- /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 00000000..66f57695
--- /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 00000000..ffca4495
--- /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 00000000..1f6b424b
--- /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 00000000..8d753484
--- /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 00000000..d7e62355
--- /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 00000000..805f1709
--- /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 00000000..d472bf30
--- /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 00000000..4632b78c
--- /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 00000000..dbfb1c32
--- /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 00000000..a2f00d6e
--- /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 00000000..b807b165
--- /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 00000000..8c0fe473
--- /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 00000000..3de5f8b1
--- /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 00000000..cf2c4796
--- /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 00000000..9042af44
--- /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 00000000..58c7faed
--- /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
GIT binary patch
literal 32222
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

literal 0
HcmV?d00001

diff --git a/demo/F16/aerobench/visualize/plot.py b/demo/F16/aerobench/visualize/plot.py
new file mode 100644
index 00000000..c6901452
--- /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()
-- 
GitLab