# pvtol.py - (planar) vertical takeoff and landing system model # RMM, 19 Jan 2022 # # This file contains a model and utility function for a (planar) # vertical takeoff and landing system, as described in FBS2e and OBC. # This system is approximately differentially flat and the flat system # mappings are included. # import numpy as np import matplotlib.pyplot as plt import control as ct import control.flatsys as fs from math import sin, cos from warnings import warn __all__ = ['pvtol', 'pvtol_windy', 'pvtol_noisy'] # PVTOL dynamics def _pvtol_update(t, x, u, params): # Get the parameter values m = params.get('m', 4.) # mass of aircraft J = params.get('J', 0.0475) # inertia around pitch axis r = params.get('r', 0.25) # distance to center of force g = params.get('g', 9.8) # gravitational constant c = params.get('c', 0.05) # damping factor (estimated) # Get the inputs and states x, y, theta, xdot, ydot, thetadot = x F1, F2 = u # Constrain the inputs F2 = np.clip(F2, 0, 1.5 * m * g) F1 = np.clip(F1, -0.1 * F2, 0.1 * F2) # Dynamics xddot = (F1 * cos(theta) - F2 * sin(theta) - c * xdot) / m yddot = (F1 * sin(theta) + F2 * cos(theta) - m * g - c * ydot) / m thddot = (r * F1) / J return np.array([xdot, ydot, thetadot, xddot, yddot, thddot]) def _pvtol_output(t, x, u, params): return x # PVTOL flat system mappings def _pvtol_flat_forward(states, inputs, params={}): # Get the parameter values m = params.get('m', 4.) # mass of aircraft J = params.get('J', 0.0475) # inertia around pitch axis r = params.get('r', 0.25) # distance to center of force g = params.get('g', 9.8) # gravitational constant c = params.get('c', 0.05) # damping factor (estimated) # Make sure that c is zero if c != 0: warn("System is only approximately flat (c != 0)") # Create a list of arrays to store the flat output and its derivatives zflag = [np.zeros(5), np.zeros(5)] # Store states and inputs in variables to make things easier to read x, y, theta, xdot, ydot, thdot = states F1, F2 = inputs # Use equations of motion for higher derivates x1ddot = (F1 * cos(theta) - F2 * sin(theta)) / m x2ddot = (F1 * sin(theta) + F2 * cos(theta) - m * g) / m thddot = (r * F1) / J # Flat output is a point above the vertical axis zflag[0][0] = x - (J / (m * r)) * sin(theta) zflag[1][0] = y + (J / (m * r)) * cos(theta) zflag[0][1] = xdot - (J / (m * r)) * cos(theta) * thdot zflag[1][1] = ydot - (J / (m * r)) * sin(theta) * thdot zflag[0][2] = (F1 * cos(theta) - F2 * sin(theta)) / m \ + (J / (m * r)) * sin(theta) * thdot**2 \ - (J / (m * r)) * cos(theta) * thddot zflag[1][2] = (F1 * sin(theta) + F2 * cos(theta) - m * g) / m \ - (J / (m * r)) * cos(theta) * thdot**2 \ - (J / (m * r)) * sin(theta) * thddot # For the third derivative, assume F1, F2 are constant (also thddot) zflag[0][3] = (-F1 * sin(theta) - F2 * cos(theta)) * (thdot / m) \ + (J / (m * r)) * cos(theta) * thdot**3 \ + 3 * (J / (m * r)) * sin(theta) * thdot * thddot zflag[1][3] = (F1 * cos(theta) - F2 * sin(theta)) * (thdot / m) \ + (J / (m * r)) * sin(theta) * thdot**3 \ - 3 * (J / (m * r)) * cos(theta) * thdot * thddot # For the fourth derivative, assume F1, F2 are constant (also thddot) zflag[0][4] = (-F1 * sin(theta) - F2 * cos(theta)) * (thddot / m) \ + (-F1 * cos(theta) + F2 * sin(theta)) * (thdot**2 / m) \ + 6 * (J / (m * r)) * cos(theta) * thdot**2 * thddot \ + 3 * (J / (m * r)) * sin(theta) * thddot**2 \ - (J / (m * r)) * sin(theta) * thdot**4 zflag[1][4] = (F1 * cos(theta) - F2 * sin(theta)) * (thddot / m) \ + (-F1 * sin(theta) - F2 * cos(theta)) * (thdot**2 / m) \ - 6 * (J / (m * r)) * sin(theta) * thdot**2 * thddot \ - 3 * (J / (m * r)) * cos(theta) * thddot**2 \ + (J / (m * r)) * cos(theta) * thdot**4 return zflag def _pvtol_flat_reverse(zflag, params={}): # Get the parameter values m = params.get('m', 4.) # mass of aircraft J = params.get('J', 0.0475) # inertia around pitch axis r = params.get('r', 0.25) # distance to center of force g = params.get('g', 9.8) # gravitational constant c = params.get('c', 0.05) # damping factor (estimated) # Given the flat variables, solve for the state theta = np.arctan2(-zflag[0][2], zflag[1][2] + g) x = zflag[0][0] + (J / (m * r)) * sin(theta) y = zflag[1][0] - (J / (m * r)) * cos(theta) # Solve for thdot using next derivative thdot = (zflag[0][3] * cos(theta) + zflag[1][3] * sin(theta)) \ / (zflag[0][2] * sin(theta) - (zflag[1][2] + g) * cos(theta)) # xdot and ydot can be found from first derivative of flat outputs xdot = zflag[0][1] + (J / (m * r)) * cos(theta) * thdot ydot = zflag[1][1] + (J / (m * r)) * sin(theta) * thdot # Solve for the input forces F2 = m * ((zflag[1][2] + g) * cos(theta) - zflag[0][2] * sin(theta) + (J / (m * r)) * thdot**2) F1 = (J / r) * \ (zflag[0][4] * cos(theta) + zflag[1][4] * sin(theta) - 2 * zflag[0][3] * sin(theta) * thdot \ + 2 * zflag[1][3] * cos(theta) * thdot \ - zflag[0][2] * cos(theta) * thdot**2 - (zflag[1][2] + g) * sin(theta) * thdot**2) \ / (zflag[0][2] * sin(theta) - (zflag[1][2] + g) * cos(theta)) return np.array([x, y, theta, xdot, ydot, thdot]), np.array([F1, F2]) pvtol = fs.FlatSystem( _pvtol_flat_forward, _pvtol_flat_reverse, name='pvtol', updfcn=_pvtol_update, outfcn=_pvtol_output, states = [f'x{i}' for i in range(6)], inputs = ['F1', 'F2'], outputs = [f'x{i}' for i in range(6)], params = { 'm': 4., # mass of aircraft 'J': 0.0475, # inertia around pitch axis 'r': 0.25, # distance to center of force 'g': 9.8, # gravitational constant 'c': 0.05, # damping factor (estimated) } ) # # PVTOL dynamics with wind # def _windy_update(t, x, u, params): # Get the input vector F1, F2, d = u # Get the system response from the original dynamics xdot, ydot, thetadot, xddot, yddot, thddot = \ _pvtol_update(t, x, u[0:2], params) # Now add the wind term m = params.get('m', 4.) # mass of aircraft xddot += d / m return np.array([xdot, ydot, thetadot, xddot, yddot, thddot]) pvtol_windy = ct.NonlinearIOSystem( _windy_update, _pvtol_output, name="pvtol_windy", states = [f'x{i}' for i in range(6)], inputs = ['F1', 'F2', 'd'], outputs = [f'x{i}' for i in range(6)] ) # # PVTOL dynamics with noise and disturbances # def _noisy_update(t, x, u, params): # Get the inputs F1, F2, Dx, Dy = u[:4] if u.shape[0] > 4: Nx, Ny, Nth = u[4:] else: Nx, Ny, Nth = 0, 0, 0 # Get the system response from the original dynamics xdot, ydot, thetadot, xddot, yddot, thddot = \ _pvtol_update(t, x, u[0:2], params) # Get the parameter values we need m = params.get('m', 4.) # mass of aircraft J = params.get('J', 0.0475) # inertia around pitch axis # Now add the disturbances xddot += Dx / m yddot += Dy / m return np.array([xdot, ydot, thetadot, xddot, yddot, thddot]) def _noisy_output(t, x, u, params): F1, F2, dx, Dy, Nx, Ny, Nth = u return x + np.array([Nx, Ny, Nth, 0, 0, 0]) pvtol_noisy = ct.NonlinearIOSystem( _noisy_update, _noisy_output, name="pvtol_noisy", states = [f'x{i}' for i in range(6)], inputs = ['F1', 'F2'] + ['Dx', 'Dy'] + ['Nx', 'Ny', 'Nth'], outputs = pvtol.state_labels ) # Add the linearitizations to the dynamics as an additional method def pvtol_noisy_A(x, u, params={}): # Get the parameter values we need m = params.get('m', 4.) # mass of aircraft J = params.get('J', 0.0475) # inertia around pitch axis c = params.get('c', 0.05) # damping factor (estimated) # Get the angle and compute sine and cosine theta = x[2] cth, sth = cos(theta), sin(theta) # Return the linearized dynamics matrix return np.array([ [0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 1], [0, 0, (-u[0] * sth - u[1] * cth)/m, -c/m, 0, 0], [0, 0, ( u[0] * cth - u[1] * sth)/m, 0, -c/m, 0], [0, 0, 0, 0, 0, 0] ]) pvtol.A = pvtol_noisy_A # Plot the trajectory in xy coordinates def plot_results(t, x, u): # Set the size of the figure plt.figure(figsize=(10, 6)) # Top plot: xy trajectory plt.subplot(2, 1, 1) plt.plot(x[0], x[1]) plt.xlabel('x [m]') plt.ylabel('y [m]') plt.axis('equal') # Time traces of the state and input plt.subplot(2, 4, 5) plt.plot(t, x[1]) plt.xlabel('Time t [sec]') plt.ylabel('y [m]') plt.subplot(2, 4, 6) plt.plot(t, x[2]) plt.xlabel('Time t [sec]') plt.ylabel('theta [rad]') plt.subplot(2, 4, 7) plt.plot(t, u[0]) plt.xlabel('Time t [sec]') plt.ylabel('$F_1$ [N]') plt.subplot(2, 4, 8) plt.plot(t, u[1]) plt.xlabel('Time t [sec]') plt.ylabel('$F_2$ [N]') plt.tight_layout() # # Additional functions for testing # # Check flatness calculations def _pvtol_check_flat(test_points=None, verbose=False): if test_points is None: # If no test points, use internal set mg = 9.8 * 4 test_points = [ ([0, 0, 0, 0, 0, 0], [0, mg]), ([1, 0, 0, 0, 0, 0], [0, mg]), ([0, 1, 0, 0, 0, 0], [0, mg]), ([1, 1, 0.1, 0, 0, 0], [0, mg]), ([0, 0, 0.1, 0, 0, 0], [0, mg]), ([0, 0, 0, 1, 0, 0], [0, mg]), ([0, 0, 0, 0, 1, 0], [0, mg]), ([0, 0, 0, 0, 0, 0.1], [0, mg]), ([0, 0, 0, 1, 1, 0.1], [0, mg]), ([0, 0, 0, 0, 0, 0], [1, mg]), ([0, 0, 0, 0, 0, 0], [0, mg + 1]), ([0, 0, 0, 0, 0, 0.1], [1, mg]), ([0.1, 0.2, 0.3, 0.4, 0.5, 0.6], [0.7, mg + 1]), ] elif isinstance(test_points, tuple): # If we only got one test point, convert to a list test_points = [test_points] for x, u in test_points: x, u = np.array(x), np.array(u) flag = _pvtol_flat_forward(x, u) xc, uc = _pvtol_flat_reverse(flag) print(f'({x}, {u}): ', end='') if verbose: print(f'\n flag: {flag}') print(f' check: ({xc}, {uc}): ', end='') if np.allclose(x, xc) and np.allclose(u, uc): print("OK") else: print("ERR")