diff --git a/demo/F16/F16_waypoint_scene.py b/demo/F16/F16_waypoint_scene.py new file mode 100644 index 0000000000000000000000000000000000000000..8984fc182bcfbf622e6bd3a7f989986003fd161a --- /dev/null +++ b/demo/F16/F16_waypoint_scene.py @@ -0,0 +1,96 @@ +''' +Sayan Mitra +Derived from Stanley Bak's version +''' + + +from typing import Tuple, List + +import numpy as np +from scipy.integrate import ode + +from dryvr_plus_plus.scene_verifier.agents.base_agent import BaseAgent +from dryvr_plus_plus.scene_verifier.map.lane_map import LaneMap +from dryvr_plus_plus.scene_verifier.code_parser.pythonparser import EmptyAst + + +import math + +from numpy import deg2rad +import matplotlib.pyplot as plt + +from aerobench.run_f16_sim import run_f16_sim + +from aerobench.visualize import plot + +from waypoint_autopilot import WaypointAutopilot + +def main(): + 'main function' + + ### Initial Conditions ### + power = 9 # engine power level (0-10) + + # Default alpha & beta + alpha = deg2rad(2.1215) # Trim Angle of Attack (rad) + beta = 0 # Side slip angle (rad) + + # Initial Attitude + alt = 3800 # altitude (ft) + vt = 540 # initial velocity (ft/sec) + phi = 0 # Roll angle from wings level (rad) + theta = 0 # Pitch angle from nose level (rad) + psi = math.pi/8 # Yaw angle from North (rad) + + # Build Initial Condition Vectors + # state = [vt, alpha, beta, phi, theta, psi, P, Q, R, pn, pe, h, pow] + init = [vt, alpha, beta, phi, theta, psi, 0, 0, 0, 0, 0, alt, power] + tmax = 70 # simulation time + + # make waypoint list + e_pt = 1000 + n_pt = 3000 + h_pt = 4000 + + waypoints = [[e_pt, n_pt, h_pt], + [e_pt + 2000, n_pt + 5000, h_pt - 100], + [e_pt - 2000, n_pt + 15000, h_pt - 250], + [e_pt - 500, n_pt + 25000, h_pt]] + + ap = WaypointAutopilot(waypoints, stdout=True) + + step = 1/30 + extended_states = True + res = run_f16_sim(init, tmax, ap, step=step, extended_states=extended_states, integrator_str='rk45') + + print(f"Simulation Completed in {round(res['runtime'], 2)} seconds (extended_states={extended_states})") + + plot.plot_single(res, 'alt', title='Altitude (ft)') + filename = 'alt.png' + plt.savefig(filename) + print(f"Made {filename}") + + plot.plot_overhead(res, waypoints=waypoints) + filename = 'overhead.png' + plt.savefig(filename) + print(f"Made {filename}") + + plot.plot_attitude(res) + filename = 'attitude.png' + plt.savefig(filename) + print(f"Made {filename}") + + # plot inner loop controls + references + plot.plot_inner_loop(res) + filename = 'inner_loop.png' + plt.savefig(filename) + print(f"Made {filename}") + + # plot outer loop controls + references + plot.plot_outer_loop(res) + filename = 'outer_loop.png' + plt.savefig(filename) + print(f"Made {filename}") + +if __name__ == '__main__': + main() diff --git a/demo/F16/aerobench/examples/waypoint/F16_waypoint_scene.py b/demo/F16/aerobench/examples/waypoint/F16_waypoint_scene.py new file mode 100644 index 0000000000000000000000000000000000000000..8984fc182bcfbf622e6bd3a7f989986003fd161a --- /dev/null +++ b/demo/F16/aerobench/examples/waypoint/F16_waypoint_scene.py @@ -0,0 +1,96 @@ +''' +Sayan Mitra +Derived from Stanley Bak's version +''' + + +from typing import Tuple, List + +import numpy as np +from scipy.integrate import ode + +from dryvr_plus_plus.scene_verifier.agents.base_agent import BaseAgent +from dryvr_plus_plus.scene_verifier.map.lane_map import LaneMap +from dryvr_plus_plus.scene_verifier.code_parser.pythonparser import EmptyAst + + +import math + +from numpy import deg2rad +import matplotlib.pyplot as plt + +from aerobench.run_f16_sim import run_f16_sim + +from aerobench.visualize import plot + +from waypoint_autopilot import WaypointAutopilot + +def main(): + 'main function' + + ### Initial Conditions ### + power = 9 # engine power level (0-10) + + # Default alpha & beta + alpha = deg2rad(2.1215) # Trim Angle of Attack (rad) + beta = 0 # Side slip angle (rad) + + # Initial Attitude + alt = 3800 # altitude (ft) + vt = 540 # initial velocity (ft/sec) + phi = 0 # Roll angle from wings level (rad) + theta = 0 # Pitch angle from nose level (rad) + psi = math.pi/8 # Yaw angle from North (rad) + + # Build Initial Condition Vectors + # state = [vt, alpha, beta, phi, theta, psi, P, Q, R, pn, pe, h, pow] + init = [vt, alpha, beta, phi, theta, psi, 0, 0, 0, 0, 0, alt, power] + tmax = 70 # simulation time + + # make waypoint list + e_pt = 1000 + n_pt = 3000 + h_pt = 4000 + + waypoints = [[e_pt, n_pt, h_pt], + [e_pt + 2000, n_pt + 5000, h_pt - 100], + [e_pt - 2000, n_pt + 15000, h_pt - 250], + [e_pt - 500, n_pt + 25000, h_pt]] + + ap = WaypointAutopilot(waypoints, stdout=True) + + step = 1/30 + extended_states = True + res = run_f16_sim(init, tmax, ap, step=step, extended_states=extended_states, integrator_str='rk45') + + print(f"Simulation Completed in {round(res['runtime'], 2)} seconds (extended_states={extended_states})") + + plot.plot_single(res, 'alt', title='Altitude (ft)') + filename = 'alt.png' + plt.savefig(filename) + print(f"Made {filename}") + + plot.plot_overhead(res, waypoints=waypoints) + filename = 'overhead.png' + plt.savefig(filename) + print(f"Made {filename}") + + plot.plot_attitude(res) + filename = 'attitude.png' + plt.savefig(filename) + print(f"Made {filename}") + + # plot inner loop controls + references + plot.plot_inner_loop(res) + filename = 'inner_loop.png' + plt.savefig(filename) + print(f"Made {filename}") + + # plot outer loop controls + references + plot.plot_outer_loop(res) + filename = 'outer_loop.png' + plt.savefig(filename) + print(f"Made {filename}") + +if __name__ == '__main__': + main() diff --git a/demo/F16/aerobench/examples/waypoint/run_u_turn.py b/demo/F16/aerobench/examples/waypoint/run_u_turn.py new file mode 100644 index 0000000000000000000000000000000000000000..f7f77701f1497be07008a68aeb41bc34d3286825 --- /dev/null +++ b/demo/F16/aerobench/examples/waypoint/run_u_turn.py @@ -0,0 +1,81 @@ +''' +Stanley Bak + +should match 'u_turn' scenario from matlab version +''' + +import math + +from numpy import deg2rad +import matplotlib.pyplot as plt + +from aerobench.run_f16_sim import run_f16_sim + +from aerobench.visualize import plot + +from waypoint_autopilot import WaypointAutopilot + +def main(): + 'main function' + + ### Initial Conditions ### + power = 9 # engine power level (0-10) + + # Default alpha & beta + alpha = deg2rad(2.1215) # Trim Angle of Attack (rad) + beta = 0 # Side slip angle (rad) + + # Initial Attitude + alt = 1500 # altitude (ft) + vt = 540 # initial velocity (ft/sec) + phi = 0 # Roll angle from wings level (rad) + theta = 0 # Pitch angle from nose level (rad) + psi = 0 # Yaw angle from North (rad) + + # Build Initial Condition Vectors + # state = [vt, alpha, beta, phi, theta, psi, P, Q, R, pn, pe, h, pow] + init = [vt, alpha, beta, phi, theta, psi, 0, 0, 0, 0, 0, alt, power] + tmax = 150 # simulation time + + # make waypoint list + waypoints = [[-5000, -7500, alt], + [-15000, -7500, alt], + [-20000, 0, alt+500]] + + ap = WaypointAutopilot(waypoints, stdout=True) + + step = 1/30 + extended_states = True + res = run_f16_sim(init, tmax, ap, step=step, extended_states=extended_states, integrator_str='rk45') + + print(f"Simulation Completed in {round(res['runtime'], 2)} seconds (extended_states={extended_states})") + + plot.plot_single(res, 'alt', title='Altitude (ft)') + filename = 'alt.png' + plt.savefig(filename) + print(f"Made {filename}") + + plot.plot_overhead(res, waypoints=waypoints) + filename = 'overhead.png' + plt.savefig(filename) + print(f"Made {filename}") + + plot.plot_attitude(res) + filename = 'attitude.png' + plt.savefig(filename) + print(f"Made {filename}") + + # plot inner loop controls + references + plot.plot_inner_loop(res) + filename = 'inner_loop.png' + plt.savefig(filename) + print(f"Made {filename}") + + # plot outer loop controls + references + plot.plot_outer_loop(res) + filename = 'outer_loop.png' + plt.savefig(filename) + print(f"Made {filename}") + +if __name__ == '__main__': + main() diff --git a/demo/F16/aerobench/examples/waypoint/run_waypoint.py b/demo/F16/aerobench/examples/waypoint/run_waypoint.py new file mode 100644 index 0000000000000000000000000000000000000000..e2952a6b5034a24e045e8ffba2ade2db21a6f38b --- /dev/null +++ b/demo/F16/aerobench/examples/waypoint/run_waypoint.py @@ -0,0 +1,86 @@ +''' +Stanley Bak + +should match 'waypoint' scenario from matlab version +''' + +import math + +from numpy import deg2rad +import matplotlib.pyplot as plt + +from aerobench.run_f16_sim import run_f16_sim + +from aerobench.visualize import plot + +from waypoint_autopilot import WaypointAutopilot + +def main(): + 'main function' + + ### Initial Conditions ### + power = 9 # engine power level (0-10) + + # Default alpha & beta + alpha = deg2rad(2.1215) # Trim Angle of Attack (rad) + beta = 0 # Side slip angle (rad) + + # Initial Attitude + alt = 3800 # altitude (ft) + vt = 540 # initial velocity (ft/sec) + phi = 0 # Roll angle from wings level (rad) + theta = 0 # Pitch angle from nose level (rad) + psi = math.pi/8 # Yaw angle from North (rad) + + # Build Initial Condition Vectors + # state = [vt, alpha, beta, phi, theta, psi, P, Q, R, pn, pe, h, pow] + init = [vt, alpha, beta, phi, theta, psi, 0, 0, 0, 0, 0, alt, power] + tmax = 70 # simulation time + + # make waypoint list + e_pt = 1000 + n_pt = 3000 + h_pt = 4000 + + waypoints = [[e_pt, n_pt, h_pt], + [e_pt + 2000, n_pt + 5000, h_pt - 100], + [e_pt - 2000, n_pt + 15000, h_pt - 250], + [e_pt - 500, n_pt + 25000, h_pt]] + + ap = WaypointAutopilot(waypoints, stdout=True) + + step = 1/30 + extended_states = True + res = run_f16_sim(init, tmax, ap, step=step, extended_states=extended_states, integrator_str='rk45') + + print(f"Simulation Completed in {round(res['runtime'], 2)} seconds (extended_states={extended_states})") + + plot.plot_single(res, 'alt', title='Altitude (ft)') + filename = 'alt.png' + plt.savefig(filename) + print(f"Made {filename}") + + plot.plot_overhead(res, waypoints=waypoints) + filename = 'overhead.png' + plt.savefig(filename) + print(f"Made {filename}") + + plot.plot_attitude(res) + filename = 'attitude.png' + plt.savefig(filename) + print(f"Made {filename}") + + # plot inner loop controls + references + plot.plot_inner_loop(res) + filename = 'inner_loop.png' + plt.savefig(filename) + print(f"Made {filename}") + + # plot outer loop controls + references + plot.plot_outer_loop(res) + filename = 'outer_loop.png' + plt.savefig(filename) + print(f"Made {filename}") + +if __name__ == '__main__': + main() diff --git a/demo/F16/aerobench/examples/waypoint/waypoint_autopilot.py b/demo/F16/aerobench/examples/waypoint/waypoint_autopilot.py new file mode 100644 index 0000000000000000000000000000000000000000..0c9575ff51826eb2452f965af05e6cf155fa2c11 --- /dev/null +++ b/demo/F16/aerobench/examples/waypoint/waypoint_autopilot.py @@ -0,0 +1,307 @@ +'''waypoint autopilot + +ported from matlab v2 +''' + +from math import pi, atan2, sqrt, sin, cos, asin + +import numpy as np + +from aerobench.highlevel.autopilot import Autopilot +from aerobench.util import StateIndex +from aerobench.lowlevel.low_level_controller import LowLevelController + +class WaypointAutopilot(Autopilot): + '''waypoint follower autopilot''' + + def __init__(self, waypoints, gain_str='old', stdout=False): + 'waypoints is a list of 3-tuples' + + self.stdout = stdout + self.waypoints = waypoints + self.waypoint_index = 0 + + # waypoint config + self.cfg_slant_range_threshold = 250 + + # default control when not waypoint tracking + self.cfg_u_ol_default = (0, 0, 0, 0.3) + + # control config + # Gains for speed control + self.cfg_k_vt = 0.25 + self.cfg_airspeed = 550 + + # Gains for altitude tracking + self.cfg_k_alt = 0.005 + self.cfg_k_h_dot = 0.02 + + # Gains for heading tracking + self.cfg_k_prop_psi = 5 + self.cfg_k_der_psi = 0.5 + + # Gains for roll tracking + self.cfg_k_prop_phi = 0.75 + self.cfg_k_der_phi = 0.5 + self.cfg_max_bank_deg = 65 # maximum bank angle setpoint + # v2 was 0.5, 0.9 + + # Ranges for Nz + self.cfg_max_nz_cmd = 4 + self.cfg_min_nz_cmd = -1 + + self.done_time = 0.0 + + llc = LowLevelController(gain_str=gain_str) + + Autopilot.__init__(self, 'Waypoint 1', llc=llc) + + def log(self, s): + 'print to terminal if stdout is true' + + if self.stdout: + print(s) + + def get_u_ref(self, _t, x_f16): + '''get the reference input signals''' + + if self.mode != "Done": + psi_cmd = self.get_waypoint_data(x_f16)[0] + + # Get desired roll angle given desired heading + phi_cmd = self.get_phi_to_track_heading(x_f16, psi_cmd) + ps_cmd = self.track_roll_angle(x_f16, phi_cmd) + + nz_cmd = self.track_altitude(x_f16) + throttle = self.track_airspeed(x_f16) + else: + # Waypoint Following complete: fly level. + throttle = self.track_airspeed(x_f16) + ps_cmd = self.track_roll_angle(x_f16, 0) + nz_cmd = self.track_altitude_wings_level(x_f16) + + # trim to limits + nz_cmd = max(self.cfg_min_nz_cmd, min(self.cfg_max_nz_cmd, nz_cmd)) + throttle = max(min(throttle, 1), 0) + + # Create reference vector + rv = [nz_cmd, ps_cmd, 0, throttle] + + return rv + + def track_altitude(self, x_f16): + 'get nz to track altitude, taking turning into account' + + h_cmd = self.waypoints[self.waypoint_index][2] + + h = x_f16[StateIndex.ALT] + phi = x_f16[StateIndex.PHI] + + # Calculate altitude error (positive => below target alt) + h_error = h_cmd - h + nz_alt = self.track_altitude_wings_level(x_f16) + nz_roll = get_nz_for_level_turn_ol(x_f16) + + if h_error > 0: + # Ascend wings level or banked + nz = nz_alt + nz_roll + elif abs(phi) < np.deg2rad(15): + # Descend wings (close enough to) level + nz = nz_alt + nz_roll + else: + # Descend in bank (no negative Gs) + nz = max(0, nz_alt + nz_roll) + + return nz + + def get_phi_to_track_heading(self, x_f16, psi_cmd): + 'get phi from psi_cmd' + + # PD Control on heading angle using phi_cmd as control + + # Pull out important variables for ease of use + psi = wrap_to_pi(x_f16[StateIndex.PSI]) + r = x_f16[StateIndex.R] + + # Calculate PD control + psi_err = wrap_to_pi(psi_cmd - psi) + + phi_cmd = psi_err * self.cfg_k_prop_psi - r * self.cfg_k_der_psi + + # Bound to acceptable bank angles: + max_bank_rad = np.deg2rad(self.cfg_max_bank_deg) + + phi_cmd = min(max(phi_cmd, -max_bank_rad), max_bank_rad) + + return phi_cmd + + def track_roll_angle(self, x_f16, phi_cmd): + 'get roll angle command (ps_cmd)' + + # PD control on roll angle using stability roll rate + + # Pull out important variables for ease of use + phi = x_f16[StateIndex.PHI] + p = x_f16[StateIndex.P] + + # Calculate PD control + ps = (phi_cmd-phi) * self.cfg_k_prop_phi - p * self.cfg_k_der_phi + + return ps + + def track_airspeed(self, x_f16): + 'get throttle command' + + vt_cmd = self.cfg_airspeed + + # Proportional control on airspeed using throttle + throttle = self.cfg_k_vt * (vt_cmd - x_f16[StateIndex.VT]) + + return throttle + + def track_altitude_wings_level(self, x_f16): + 'get nz to track altitude' + + i = self.waypoint_index if self.waypoint_index < len(self.waypoints) else -1 + + h_cmd = self.waypoints[i][2] + + vt = x_f16[StateIndex.VT] + h = x_f16[StateIndex.ALT] + + # Proportional-Derivative Control + h_error = h_cmd - h + gamma = get_path_angle(x_f16) + h_dot = vt * sin(gamma) # Calculated, not differentiated + + # Calculate Nz command + nz = self.cfg_k_alt*h_error - self.cfg_k_h_dot*h_dot + + return nz + + def is_finished(self, t, x_f16): + 'is the maneuver done?' + + rv = self.waypoint_index >= len(self.waypoints) and self.done_time + 5.0 < t + + return rv + + def advance_discrete_mode(self, t, x_f16): + ''' + advance the discrete state based on the current aircraft state. Returns True iff the discrete state + has changed. + ''' + + if self.waypoint_index < len(self.waypoints): + slant_range = self.get_waypoint_data(x_f16)[-1] + + if slant_range < self.cfg_slant_range_threshold: + self.waypoint_index += 1 + + if self.waypoint_index >= len(self.waypoints): + self.done_time = t + + premode = self.mode + + if self.waypoint_index >= len(self.waypoints): + self.mode = 'Done' + else: + self.mode = f'Waypoint {self.waypoint_index + 1}' + + rv = premode != self.mode + + if rv: + self.log(f"Waypoint transition {premode} -> {self.mode} at time {t}") + + return rv + + def get_waypoint_data(self, x_f16): + '''returns current waypoint data tuple based on the current waypoint: + + (heading, inclination, horiz_range, vert_range, slant_range) + + heading = heading to tgt, equivalent to psi (rad) + inclination = polar angle to tgt, equivalent to theta (rad) + horiz_range = horizontal range to tgt (ft) + vert_range = vertical range to tgt (ft) + slant_range = total range to tgt (ft) + ''' + + waypoint = self.waypoints[self.waypoint_index] + + e_pos = x_f16[StateIndex.POSE] + n_pos = x_f16[StateIndex.POSN] + alt = x_f16[StateIndex.ALT] + + delta = [waypoint[i] - [e_pos, n_pos, alt][i] for i in range(3)] + + _, inclination, slant_range = cart2sph(delta) + + heading = wrap_to_pi(pi/2 - atan2(delta[1], delta[0])) + + horiz_range = np.linalg.norm(delta[0:2]) + vert_range = np.linalg.norm(delta[2]) + + return heading, inclination, horiz_range, vert_range, slant_range + +def get_nz_for_level_turn_ol(x_f16): + 'get nz to do a level turn' + + # Pull g's to maintain altitude during bank based on trig + + # Calculate theta + phi = x_f16[StateIndex.PHI] + + if abs(phi): # if cos(phi) ~= 0, basically + nz = 1 / cos(phi) - 1 # Keeps plane at altitude + else: + nz = 0 + + return nz + +def get_path_angle(x_f16): + 'get the path angle gamma' + + alpha = x_f16[StateIndex.ALPHA] # AoA (rad) + beta = x_f16[StateIndex.BETA] # Sideslip (rad) + phi = x_f16[StateIndex.PHI] # Roll anle (rad) + theta = x_f16[StateIndex.THETA] # Pitch angle (rad) + + gamma = asin((cos(alpha)*sin(theta)- \ + sin(alpha)*cos(theta)*cos(phi))*cos(beta) - \ + (cos(theta)*sin(phi))*sin(beta)) + + return gamma + +def wrap_to_pi(psi_rad): + '''handle angle wrapping + + returns equivelent angle in range [-pi, pi] + ''' + + rv = psi_rad % (2 * pi) + + if rv > pi: + rv -= 2 * pi + + return rv + +def cart2sph(pt3d): + ''' + Cartesian to spherical coordinates + + returns az, elev, r + ''' + + x, y, z = pt3d + + h = sqrt(x*x + y*y) + r = sqrt(h*h + z*z) + + elev = atan2(z, h) + az = atan2(y, x) + + return az, elev, r + +if __name__ == '__main__': + print("Autopulot script not meant to be run directly.") diff --git a/demo/F16/aerobench/run_f16_sim.py b/demo/F16/aerobench/run_f16_sim.py new file mode 100644 index 0000000000000000000000000000000000000000..027303baa88be7b27303d02bdc1dc9188ac804a9 --- /dev/null +++ b/demo/F16/aerobench/run_f16_sim.py @@ -0,0 +1,227 @@ +''' +Stanley Bak +run_f16_sim python version +''' + +import time + +import numpy as np +from scipy.integrate import RK45 + +from aerobench.highlevel.controlled_f16 import controlled_f16 +from aerobench.util import get_state_names, Euler + +from dryvr_plus_plus.scene_verifier.agents.base_agent import BaseAgent +from dryvr_plus_plus.scene_verifier.map.lane_map import LaneMap +from dryvr_plus_plus.scene_verifier.code_parser.pythonparser import EmptyAst + + +class F16Agent(BaseAgent): + '''Dynamics of an F16 aircraft + derived from Stanley Bak's python library''' + def __init__(self, id, code = None, file_name = None): + '''Contructor for tha agent + EXACTLY one of the following should be given + file_name: name of the controller + code: pyhton string ddefning the controller + ''' + # Calling the constructor of tha base class + super().__init__(id, code, file_name) + +def run_f16_sim(initial_state, tmax, ap, step=1/30, extended_states=False, model_str='morelli', + integrator_str='rk45', v2_integrators=False): + '''Simulates and analyzes autonomous F-16 maneuvers + + if multiple aircraft are to be simulated at the same time, + initial_state should be the concatenated full (including integrators) initial state. + + returns a dict with the following keys: + + 'status': integration status, should be 'finished' if no errors, or 'autopilot finished' + 'times': time history + 'states': state history at each time step + 'modes': mode history at each time step + + if extended_states was True, result also includes: + 'xd_list' - derivative at each time step + 'ps_list' - ps at each time step + 'Nz_list' - Nz at each time step + 'Ny_r_list' - Ny_r at each time step + 'u_list' - input at each time step, input is 7-tuple: throt, ele, ail, rud, Nz_ref, ps_ref, Ny_r_ref + These are tuples if multiple aircraft are used + ''' + + start = time.perf_counter() + + initial_state = np.array(initial_state, dtype=float) + llc = ap.llc + + num_vars = len(get_state_names()) + llc.get_num_integrators() + + if initial_state.size < num_vars: + # append integral error states to state vector + x0 = np.zeros(num_vars) + x0[:initial_state.shape[0]] = initial_state + else: + x0 = initial_state + + assert x0.size % num_vars == 0, f"expected initial state ({x0.size} vars) to be multiple of {num_vars} vars" + + # run the numerical simulation + times = [0] + states = [x0] + + # mode can change at time 0 + ap.advance_discrete_mode(times[-1], states[-1]) + + modes = [ap.mode] + + if extended_states: + xd, u, Nz, ps, Ny_r = get_extended_states(ap, times[-1], states[-1], model_str, v2_integrators) + + xd_list = [xd] + u_list = [u] + Nz_list = [Nz] + ps_list = [ps] + Ny_r_list = [Ny_r] + + der_func = make_der_func(ap, model_str, v2_integrators) + + if integrator_str == 'rk45': + integrator_class = RK45 + kwargs = {} + else: + assert integrator_str == 'euler' + integrator_class = Euler + kwargs = {'step': step} + + # note: fixed_step argument is unused by rk45, used with euler + integrator = integrator_class(der_func, times[-1], states[-1], tmax, **kwargs) + + while integrator.status == 'running': + integrator.step() + + if integrator.t >= times[-1] + step: + dense_output = integrator.dense_output() + + while integrator.t >= times[-1] + step: + t = times[-1] + step + #print(f"{round(t, 2)} / {tmax}") + + times.append(t) + states.append(dense_output(t)) + + updated = ap.advance_discrete_mode(times[-1], states[-1]) + modes.append(ap.mode) + + # re-run dynamics function at current state to get non-state variables + if extended_states: + xd, u, Nz, ps, Ny_r = get_extended_states(ap, times[-1], states[-1], model_str, v2_integrators) + + xd_list.append(xd) + u_list.append(u) + + Nz_list.append(Nz) + ps_list.append(ps) + Ny_r_list.append(Ny_r) + + if ap.is_finished(times[-1], states[-1]): + # this both causes the outer loop to exit and sets res['status'] appropriately + integrator.status = 'autopilot finished' + break + + if updated: + # re-initialize the integration class on discrete mode switches + integrator = integrator_class(der_func, times[-1], states[-1], tmax, **kwargs) + break + + assert 'finished' in integrator.status + + res = {} + res['status'] = integrator.status + res['times'] = times + res['states'] = np.array(states, dtype=float) + res['modes'] = modes + + if extended_states: + res['xd_list'] = xd_list + res['ps_list'] = ps_list + res['Nz_list'] = Nz_list + res['Ny_r_list'] = Ny_r_list + res['u_list'] = u_list + + res['runtime'] = time.perf_counter() - start + + return res + +def make_der_func(ap, model_str, v2_integrators): + 'make the combined derivative function for integration' + + def der_func(t, full_state): + 'derivative function, generalized for multiple aircraft' + + u_refs = ap.get_checked_u_ref(t, full_state) + + num_aircraft = u_refs.size // 4 + num_vars = len(get_state_names()) + ap.llc.get_num_integrators() + assert full_state.size // num_vars == num_aircraft + + xds = [] + + for i in range(num_aircraft): + state = full_state[num_vars*i:num_vars*(i+1)] + u_ref = u_refs[4*i:4*(i+1)] + + xd = controlled_f16(t, state, u_ref, ap.llc, model_str, v2_integrators)[0] + xds.append(xd) + + rv = np.hstack(xds) + + return rv + + return der_func + +def get_extended_states(ap, t, full_state, model_str, v2_integrators): + '''get xd, u, Nz, ps, Ny_r at the current time / state + + returns tuples if more than one aircraft + ''' + + llc = ap.llc + num_vars = len(get_state_names()) + llc.get_num_integrators() + num_aircraft = full_state.size // num_vars + + xd_tup = [] + u_tup = [] + Nz_tup = [] + ps_tup = [] + Ny_r_tup = [] + + u_refs = ap.get_checked_u_ref(t, full_state) + + for i in range(num_aircraft): + state = full_state[num_vars*i:num_vars*(i+1)] + u_ref = u_refs[4*i:4*(i+1)] + + xd, u, Nz, ps, Ny_r = controlled_f16(t, state, u_ref, llc, model_str, v2_integrators) + + xd_tup.append(xd) + u_tup.append(u) + Nz_tup.append(Nz) + ps_tup.append(ps) + Ny_r_tup.append(Ny_r) + + if num_aircraft == 1: + rv_xd = xd_tup[0] + rv_u = u_tup[0] + rv_Nz = Nz_tup[0] + rv_ps = ps_tup[0] + rv_Ny_r = Ny_r_tup[0] + else: + rv_xd = tuple(xd_tup) + rv_u = tuple(u_tup) + rv_Nz = tuple(Nz_tup) + rv_ps = tuple(ps_tup) + rv_Ny_r = tuple(Ny_r_tup) + + return rv_xd, rv_u, rv_Nz, rv_ps, rv_Ny_r diff --git a/demo/F16/alt.png b/demo/F16/alt.png new file mode 100644 index 0000000000000000000000000000000000000000..2ce1015d7fd9d284e062ff5b3ae13a9a994d2c7f Binary files /dev/null and b/demo/F16/alt.png differ diff --git a/demo/F16/attitude.png b/demo/F16/attitude.png new file mode 100644 index 0000000000000000000000000000000000000000..e45ef4cfde0c619b767e5ffc6e142b6255aaa41f Binary files /dev/null and b/demo/F16/attitude.png differ diff --git a/demo/F16/inner_loop.png b/demo/F16/inner_loop.png new file mode 100644 index 0000000000000000000000000000000000000000..35a1669f956d0fa4cc17af0cd5c16927d0508bb7 Binary files /dev/null and b/demo/F16/inner_loop.png differ diff --git a/demo/F16/outer_loop.png b/demo/F16/outer_loop.png new file mode 100644 index 0000000000000000000000000000000000000000..7689d3aa137f10331f44a1c9db6faa2c75a9bec4 Binary files /dev/null and b/demo/F16/outer_loop.png differ diff --git a/demo/F16/overhead.png b/demo/F16/overhead.png new file mode 100644 index 0000000000000000000000000000000000000000..349b8404228524ce0096682069cef50f7cfab78c Binary files /dev/null and b/demo/F16/overhead.png differ diff --git a/demo/F16/waypoint_autopilot.py b/demo/F16/waypoint_autopilot.py new file mode 100644 index 0000000000000000000000000000000000000000..0c9575ff51826eb2452f965af05e6cf155fa2c11 --- /dev/null +++ b/demo/F16/waypoint_autopilot.py @@ -0,0 +1,307 @@ +'''waypoint autopilot + +ported from matlab v2 +''' + +from math import pi, atan2, sqrt, sin, cos, asin + +import numpy as np + +from aerobench.highlevel.autopilot import Autopilot +from aerobench.util import StateIndex +from aerobench.lowlevel.low_level_controller import LowLevelController + +class WaypointAutopilot(Autopilot): + '''waypoint follower autopilot''' + + def __init__(self, waypoints, gain_str='old', stdout=False): + 'waypoints is a list of 3-tuples' + + self.stdout = stdout + self.waypoints = waypoints + self.waypoint_index = 0 + + # waypoint config + self.cfg_slant_range_threshold = 250 + + # default control when not waypoint tracking + self.cfg_u_ol_default = (0, 0, 0, 0.3) + + # control config + # Gains for speed control + self.cfg_k_vt = 0.25 + self.cfg_airspeed = 550 + + # Gains for altitude tracking + self.cfg_k_alt = 0.005 + self.cfg_k_h_dot = 0.02 + + # Gains for heading tracking + self.cfg_k_prop_psi = 5 + self.cfg_k_der_psi = 0.5 + + # Gains for roll tracking + self.cfg_k_prop_phi = 0.75 + self.cfg_k_der_phi = 0.5 + self.cfg_max_bank_deg = 65 # maximum bank angle setpoint + # v2 was 0.5, 0.9 + + # Ranges for Nz + self.cfg_max_nz_cmd = 4 + self.cfg_min_nz_cmd = -1 + + self.done_time = 0.0 + + llc = LowLevelController(gain_str=gain_str) + + Autopilot.__init__(self, 'Waypoint 1', llc=llc) + + def log(self, s): + 'print to terminal if stdout is true' + + if self.stdout: + print(s) + + def get_u_ref(self, _t, x_f16): + '''get the reference input signals''' + + if self.mode != "Done": + psi_cmd = self.get_waypoint_data(x_f16)[0] + + # Get desired roll angle given desired heading + phi_cmd = self.get_phi_to_track_heading(x_f16, psi_cmd) + ps_cmd = self.track_roll_angle(x_f16, phi_cmd) + + nz_cmd = self.track_altitude(x_f16) + throttle = self.track_airspeed(x_f16) + else: + # Waypoint Following complete: fly level. + throttle = self.track_airspeed(x_f16) + ps_cmd = self.track_roll_angle(x_f16, 0) + nz_cmd = self.track_altitude_wings_level(x_f16) + + # trim to limits + nz_cmd = max(self.cfg_min_nz_cmd, min(self.cfg_max_nz_cmd, nz_cmd)) + throttle = max(min(throttle, 1), 0) + + # Create reference vector + rv = [nz_cmd, ps_cmd, 0, throttle] + + return rv + + def track_altitude(self, x_f16): + 'get nz to track altitude, taking turning into account' + + h_cmd = self.waypoints[self.waypoint_index][2] + + h = x_f16[StateIndex.ALT] + phi = x_f16[StateIndex.PHI] + + # Calculate altitude error (positive => below target alt) + h_error = h_cmd - h + nz_alt = self.track_altitude_wings_level(x_f16) + nz_roll = get_nz_for_level_turn_ol(x_f16) + + if h_error > 0: + # Ascend wings level or banked + nz = nz_alt + nz_roll + elif abs(phi) < np.deg2rad(15): + # Descend wings (close enough to) level + nz = nz_alt + nz_roll + else: + # Descend in bank (no negative Gs) + nz = max(0, nz_alt + nz_roll) + + return nz + + def get_phi_to_track_heading(self, x_f16, psi_cmd): + 'get phi from psi_cmd' + + # PD Control on heading angle using phi_cmd as control + + # Pull out important variables for ease of use + psi = wrap_to_pi(x_f16[StateIndex.PSI]) + r = x_f16[StateIndex.R] + + # Calculate PD control + psi_err = wrap_to_pi(psi_cmd - psi) + + phi_cmd = psi_err * self.cfg_k_prop_psi - r * self.cfg_k_der_psi + + # Bound to acceptable bank angles: + max_bank_rad = np.deg2rad(self.cfg_max_bank_deg) + + phi_cmd = min(max(phi_cmd, -max_bank_rad), max_bank_rad) + + return phi_cmd + + def track_roll_angle(self, x_f16, phi_cmd): + 'get roll angle command (ps_cmd)' + + # PD control on roll angle using stability roll rate + + # Pull out important variables for ease of use + phi = x_f16[StateIndex.PHI] + p = x_f16[StateIndex.P] + + # Calculate PD control + ps = (phi_cmd-phi) * self.cfg_k_prop_phi - p * self.cfg_k_der_phi + + return ps + + def track_airspeed(self, x_f16): + 'get throttle command' + + vt_cmd = self.cfg_airspeed + + # Proportional control on airspeed using throttle + throttle = self.cfg_k_vt * (vt_cmd - x_f16[StateIndex.VT]) + + return throttle + + def track_altitude_wings_level(self, x_f16): + 'get nz to track altitude' + + i = self.waypoint_index if self.waypoint_index < len(self.waypoints) else -1 + + h_cmd = self.waypoints[i][2] + + vt = x_f16[StateIndex.VT] + h = x_f16[StateIndex.ALT] + + # Proportional-Derivative Control + h_error = h_cmd - h + gamma = get_path_angle(x_f16) + h_dot = vt * sin(gamma) # Calculated, not differentiated + + # Calculate Nz command + nz = self.cfg_k_alt*h_error - self.cfg_k_h_dot*h_dot + + return nz + + def is_finished(self, t, x_f16): + 'is the maneuver done?' + + rv = self.waypoint_index >= len(self.waypoints) and self.done_time + 5.0 < t + + return rv + + def advance_discrete_mode(self, t, x_f16): + ''' + advance the discrete state based on the current aircraft state. Returns True iff the discrete state + has changed. + ''' + + if self.waypoint_index < len(self.waypoints): + slant_range = self.get_waypoint_data(x_f16)[-1] + + if slant_range < self.cfg_slant_range_threshold: + self.waypoint_index += 1 + + if self.waypoint_index >= len(self.waypoints): + self.done_time = t + + premode = self.mode + + if self.waypoint_index >= len(self.waypoints): + self.mode = 'Done' + else: + self.mode = f'Waypoint {self.waypoint_index + 1}' + + rv = premode != self.mode + + if rv: + self.log(f"Waypoint transition {premode} -> {self.mode} at time {t}") + + return rv + + def get_waypoint_data(self, x_f16): + '''returns current waypoint data tuple based on the current waypoint: + + (heading, inclination, horiz_range, vert_range, slant_range) + + heading = heading to tgt, equivalent to psi (rad) + inclination = polar angle to tgt, equivalent to theta (rad) + horiz_range = horizontal range to tgt (ft) + vert_range = vertical range to tgt (ft) + slant_range = total range to tgt (ft) + ''' + + waypoint = self.waypoints[self.waypoint_index] + + e_pos = x_f16[StateIndex.POSE] + n_pos = x_f16[StateIndex.POSN] + alt = x_f16[StateIndex.ALT] + + delta = [waypoint[i] - [e_pos, n_pos, alt][i] for i in range(3)] + + _, inclination, slant_range = cart2sph(delta) + + heading = wrap_to_pi(pi/2 - atan2(delta[1], delta[0])) + + horiz_range = np.linalg.norm(delta[0:2]) + vert_range = np.linalg.norm(delta[2]) + + return heading, inclination, horiz_range, vert_range, slant_range + +def get_nz_for_level_turn_ol(x_f16): + 'get nz to do a level turn' + + # Pull g's to maintain altitude during bank based on trig + + # Calculate theta + phi = x_f16[StateIndex.PHI] + + if abs(phi): # if cos(phi) ~= 0, basically + nz = 1 / cos(phi) - 1 # Keeps plane at altitude + else: + nz = 0 + + return nz + +def get_path_angle(x_f16): + 'get the path angle gamma' + + alpha = x_f16[StateIndex.ALPHA] # AoA (rad) + beta = x_f16[StateIndex.BETA] # Sideslip (rad) + phi = x_f16[StateIndex.PHI] # Roll anle (rad) + theta = x_f16[StateIndex.THETA] # Pitch angle (rad) + + gamma = asin((cos(alpha)*sin(theta)- \ + sin(alpha)*cos(theta)*cos(phi))*cos(beta) - \ + (cos(theta)*sin(phi))*sin(beta)) + + return gamma + +def wrap_to_pi(psi_rad): + '''handle angle wrapping + + returns equivelent angle in range [-pi, pi] + ''' + + rv = psi_rad % (2 * pi) + + if rv > pi: + rv -= 2 * pi + + return rv + +def cart2sph(pt3d): + ''' + Cartesian to spherical coordinates + + returns az, elev, r + ''' + + x, y, z = pt3d + + h = sqrt(x*x + y*y) + r = sqrt(h*h + z*z) + + elev = atan2(z, h) + az = atan2(y, x) + + return az, elev, r + +if __name__ == '__main__': + print("Autopulot script not meant to be run directly.") diff --git a/demo/ball_bounces.py b/demo/ball/ball_bounces.py similarity index 100% rename from demo/ball_bounces.py rename to demo/ball/ball_bounces.py