def plot(): # Thrust = Thrust('A320', 'CFM56-5B4') thrust = Thrust('A320', 'V2500-A1') fig = plt.figure(figsize=(10,8)) ax = fig.add_subplot(111, projection='3d') tas = np.arange(0, 500, 20) alt = np.arange(0, 35000, 2000) x, y = np.meshgrid(tas, alt) thr_to = thrust.takeoff(x, y) thr_cl = thrust.climb(x, y, 2000) c1, c2, c3 = .14231E+06, .51680E+05, .56809E-10 thr_bada = c1 * (1 - y / c2 + c3 * y**2) plt.title('inflight') ax.plot_wireframe(x, y, thr_to, color='r', label='OpenAP-Thrust-TO') ax.plot_wireframe(x, y, thr_cl, color='g', label='Open-Thrust-CL') ax.plot_wireframe(x, y, thr_bada, color='b', label='BADA3') ax.set_xlabel('tas (kts)') ax.set_ylabel('alt (ft)') ax.set_zlabel('thr (N)') # ax.view_init(20, 40) ax.legend() plt.tight_layout() plt.show()
def state_update(X, dt, mdl, eng): nrow, ncol = X.shape m, eta, x, y, z, vax, vay, vz, vwx, vwy, tau = X vgx = vax + vwx vgy = vay + vwy vg = np.sqrt(vgx**2 + vgy**2) va = np.sqrt(vax**2 + vay**2) psi = np.arctan2(vax, vay) gamma = np.arcsin(vz / va) thrust = Thrust(mdl, eng) T = thrust.climb(va / aero.kts, z / aero.ft, vz / aero.fpm) drag = Drag(mdl) D = drag.clean(m, va / aero.kts, z / aero.ft) a = (eta * T - D) / m - aero.g0 * np.sin(gamma) m1 = m eta1 = eta x1 = x + vgx * dt y1 = y + vgy * dt z1 = z + vz * dt va1 = va + a * dt vax1 = va1 * np.sin(psi) vay1 = va1 * np.cos(psi) vz1 = vz vwx1 = vwx vwy1 = vwy tau1 = aero.temperature(z) X = np.array([m1, eta1, x1, y1, z1, vax1, vay1, vz1, vwx1, vwy1, tau1]) return X
class FuelFlow(object): """Fuel flow model based on ICAO emmision databank.""" def __init__(self, ac, eng=None): """Initialize FuelFlow object. Args: ac (string): ICAO aircraft type (for example: A320). eng (string): Engine type (for example: CFM56-5A3). Leave empty to use the default engine specified by in the aircraft database. """ self.aircraft = prop.aircraft(ac) if eng is None: eng = self.aircraft["engine"]["default"] self.engine = prop.engine(eng) self.thrust = Thrust(ac, eng) self.drag = Drag(ac) c3, c2, c1 = ( self.engine["fuel_c3"], self.engine["fuel_c2"], self.engine["fuel_c1"], ) # print(c3,c2,c1) self.fuel_flow_model = lambda x: c3 * x**3 + c2 * x**2 + c1 * x @ndarrayconvert def at_thrust(self, acthr, alt=0): """Compute the fuel flow at a given total thrust. Args: acthr (int or ndarray): The total net thrust of the aircraft (unit: N). alt (int or ndarray): Aircraft altitude (unit: ft). Returns: float: Fuel flow (unit: kg/s). """ n_eng = self.aircraft["engine"]["number"] engthr = acthr / n_eng ratio = engthr / self.engine["max_thrust"] ff_sl = self.fuel_flow_model(ratio) ff_corr_alt = self.engine["fuel_ch"] * (engthr / 1000) * (alt * aero.ft) ff_eng = ff_sl + ff_corr_alt fuelflow = ff_eng * n_eng return fuelflow @ndarrayconvert def takeoff(self, tas, alt=None, throttle=1): """Compute the fuel flow at takeoff. The net thrust is first estimated based on the maximum thrust model and throttle setting. Then FuelFlow.at_thrust() is called to compted the thrust. Args: tas (int or ndarray): Aircraft true airspeed (unit: kt). alt (int or ndarray): Altitude of airport (unit: ft). Defaults to sea-level. throttle (float or ndarray): The throttle setting, between 0 and 1. Defaults to 1, which is at full thrust. Returns: float: Fuel flow (unit: kg/s). """ Tmax = self.thrust.takeoff(tas=tas, alt=alt) fuelflow = throttle * self.at_thrust(Tmax) return fuelflow @ndarrayconvert def enroute(self, mass, tas, alt, path_angle=0): """Compute the fuel flow during climb, cruise, or descent. The net thrust is first estimated based on the dynamic equation. Then FuelFlow.at_thrust() is called to compted the thrust. Assuming no flap deflection and no landing gear extended. Args: mass (int or ndarray): Aircraft mass (unit: kg). tas (int or ndarray): Aircraft true airspeed (unit: kt). alt (int or ndarray): Aircraft altitude (unit: ft). path_angle (float or ndarray): Flight path angle (unit: degrees). Returns: float: Fuel flow (unit: kg/s). """ D = self.drag.clean(mass=mass, tas=tas, alt=alt, path_angle=path_angle) # Convert angles from degrees to radians. gamma = np.radians(path_angle) T = D + mass * aero.g0 * np.sin(gamma) T_idle = self.thrust.descent_idle(tas=tas, alt=alt) T = np.where(T < 0, T_idle, T) fuelflow = self.at_thrust(T, alt) # do not return value outside performance boundary, with a margin of 20% T_max = self.thrust.climb(tas=0, alt=alt, roc=0) fuelflow = np.where(T > 1.20 * T_max, np.nan, fuelflow) return fuelflow def plot_model(self, plot=True): """Plot the engine fuel model, or return the pyplot object. Args: plot (bool): Display the plot or return an object. Returns: None or pyplot object. """ import matplotlib.pyplot as plt xx = np.linspace(0, 1, 50) yy = self.fuel_flow_model(xx) # plt.scatter(self.x, self.y, color='k') plt.plot(xx, yy, "--", color="gray") if plot: plt.show() else: return plt
class SIR(): def __init__(self, **kwargs): self.ac = kwargs.get('ac') self.eng = kwargs.get('eng') self.time = kwargs.get('time') self.Y = kwargs.get('obs') # slightly increase the cov matrix (factor of 0.2), for better convergency self.noise = kwargs.get('noise') self.stdn = stdns[self.noise] * 1.2 self.R = np.zeros((nY, nY)) np.fill_diagonal(self.R, self.stdn**2) self.thrust = Thrust(self.ac, self.eng) self.drag = Drag(self.ac) aircraft = prop.aircraft(self.ac) self.mmin = aircraft['limits']['OEW'] self.mmax = aircraft['limits']['MTOW'] self.mrange = (self.mmin, self.mmax) self.eta_min = kwargs.get('eta_min', 0.80) self.eta_max = kwargs.get('eta_max', 1) self.kstd_m = kpm * (self.mmax - self.mmin) self.kstd_eta = kpe * (1 - self.eta_min) self.X = None self.nP = None self.now = 0 self.neff = None self.X_true = None logfn = kwargs.get('logfn', None) self.xlabels = [ 'm (kg)', '$\eta$ (-)', 'x (m)', 'y (m)', 'z (m)', '$v_{ax}$ (m/s)', '$v_{ay}$ (m/s)', '$v_z$ (m/s)', '$v_{wx}$ (m/s)', '$v_{wy}$ (m/s)', '$\\tau$ (K)' ] self.ylabels = [ 'x', 'y', 'z', '$v_{gx}$', '$v_{gy}$', '$v_z$', '$v_{wx}$', '$v_{wy}$', '$\\tau$' ] if (logfn is not None): if ('.log' not in logfn): raise RuntimeError('Log file must end with .log') self.log = root + '/smclog/' + logfn print('writing to log:', self.log) header = ['time'] + self.ylabels \ + [l+" avg" for l in self.xlabels] \ + [l+" med" for l in self.xlabels] \ + [l+" min" for l in self.xlabels] \ + [l+" max" for l in self.xlabels] with open(self.log, 'wt') as fcsv: writer = csv.writer(fcsv, delimiter=',') writer.writerow(header) else: self.log = None def state_update(self, X, dt): nrow, ncol = X.shape m, eta, x, y, z, vax, vay, vz, vwx, vwy, tau = np.split( X, X.shape[1], 1) vgx = vax + vwx vgy = vay + vwy vg = np.sqrt(vgx**2 + vgy**2) va = np.sqrt(vax**2 + vay**2) psi = np.arctan2(vax, vay) gamma = np.arcsin(vz / va) T = self.thrust.climb(va / aero.kts, z / aero.ft, vz / aero.fpm) D = self.drag.clean(m, va / aero.kts, z / aero.ft) a = (eta * T - D) / m - aero.g0 * np.sin(gamma) print(np.mean(a)) m1 = m eta1 = eta x1 = x + vgx * dt y1 = y + vgy * dt z1 = z + vz * dt va1 = va + a * dt # va1 = va + a * np.cos(gamma) * dt vax1 = va1 * np.sin(psi) vay1 = va1 * np.cos(psi) evz = np.random.normal(0, sigma_vz, nrow) vz1 = alpha_vz * vz + evz.reshape(-1, 1) * dt # vz1 = alpha_vz * vz + a * np.sin(gamma) * dt + evz.reshape(-1, 1) * dt evwx = np.random.normal(0, sigma_vwx, nrow) vwx1 = alpha_vwx * vwx + evwx.reshape(-1, 1) * dt evwy = np.random.normal(0, sigma_vwy, nrow) vwy1 = alpha_vwy * vwy + evwy.reshape(-1, 1) * dt etau = np.random.normal(0, sigma_tau, nrow) tau1 = alpha_tau * tau + etau.reshape(-1, 1) * dt X = np.hstack( [m1, eta1, x1, y1, z1, vax1, vay1, vz1, vwx1, vwy1, tau1]) return X def logsm(self): if self.log is None: return t = self.now if t in self.time: idx = list(self.time).index(t) measurement = self.Y[:, idx] else: measurement = np.ones(self.Y.shape[0]) * np.nan state_avgs = np.average(self.X, weights=self.W.T, axis=0) state_meds = np.median(self.X, axis=0) # state_mins = np.percentile(self.X, 2.5, axis=0) # state_maxs = np.percentile(self.X, 95.5, axis=0) state_mins = np.min(self.X, axis=0) state_maxs = np.max(self.X, axis=0) row = np.hstack([[t], measurement, state_avgs, state_meds, state_mins, state_maxs]) with open(self.log, 'at') as fcsv: writer = csv.writer(fcsv, delimiter=',') writer.writerow(row) def compute_neff(self): self.neff = 1 / np.sum(np.square(self.W)) def printstates(self, t): # ---- Debug ---- obsv = Y2X(self.Y[:, t]) obsv[0:2] = ['*****', '****'] avgs = np.average(self.X, weights=self.W, axis=0) meds = np.median(self.X, axis=0) # mins = np.percentile(self.X, 2.5, axis=0) # maxs = np.percentile(self.X, 95.5, axis=0) mins = np.min(self.X, axis=0) maxs = np.max(self.X, axis=0) printarray(obsv, 'obsv') printarray(avgs, 'avgs') # printarray(meds, 'meds') printarray(mins, 'mins') printarray(maxs, 'maxs') def pickle_particles(self, fname): import os, pickle root = os.path.dirname(os.path.realpath(__file__)) fpkl = open(root + '/data/' + fname, 'wb') pickle.dump({'X': self.X, 'W': self.W}, fpkl) def init_particles(self, at=0, n_particles=50000): Mu0 = Y2X(self.Y[:, at]) Mu0[0:2] = [0, 0] Var0 = np.zeros((nX, nX)) np.fill_diagonal(Var0, (np.append([0, 0], self.stdn))**2) printarray(Mu0, 'Mu0') printarray(np.diag(Var0), 'Var0') self.X = np.random.multivariate_normal(Mu0, Var0, n_particles) self.nP = n_particles m_inits = np.random.uniform(self.mmin, self.mmax, n_particles) # mass-related initialization (recommended) er_mins = 1 - (1 - self.eta_min) * (self.mmax - m_inits) / (self.mmax - self.mmin) eta_inits = np.random.uniform(er_mins, self.eta_max, n_particles) # # Uniform initialization # eta_inits = np.random.uniform(self.eta_min, self.eta_max, n_particles) self.X[:, 0] = m_inits self.X[:, 1] = eta_inits self.W = np.ones(n_particles) / (n_particles) return def resample(self): """ References: J. S. Liu and R. Chen. Sequential Monte Carlo methods for dynamic systems. Journal of the American Statistical Association, 93(443):1032–1044, 1998. """ N = self.nP W = self.W idx = np.zeros(N, 'i') # take int(N*w) copies of each weight, which ensures particles with the # same weight are drawn uniformly copyn = (np.floor(N * W)).astype(int) a = np.where(copyn > 0)[0] b = copyn[a] c = np.append(0, np.cumsum(b)) for i, (i0, i1) in enumerate(zip(c[:-1], c[1:])): idx[i0:i1] = a[i] # use multinormal resample on the residual to fill up the rest. This # maximizes the variance of the samples k = c[-1] residual = W - copyn residual /= sum(residual) cumulative_sum = np.cumsum(residual) cumulative_sum[ -1] = 1. # avoid round-off errors: ensures sum is exactly one idx[k:N] = np.searchsorted(cumulative_sum, np.random.random(N - k)) return idx def run(self, processdt=1, use_bada=True): if self.X is None: raise RuntimeError( 'Particles not initialized. Run SIR.init_particles() first.') self.now = self.time[0] self.logsm() for it, tm in enumerate(self.time): print('-' * 100) self.now = tm # ===== SIR update ===== print("SIR / measurement update, time", self.now) self.printstates(it) # ---- weight update ---- Y_true = X2Y(self.X) Yt = self.Y[:, it] Y = np.ones(Y_true.shape) * Yt DY = Y - Y_true Simga_inv = np.linalg.inv(self.R) self.W *= np.exp(-0.5 * np.einsum('ij,ij->i', np.dot(DY, Simga_inv), DY)) self.W = np.where(self.X[:, 1] < self.eta_min, 0, self.W) self.W = np.where(self.X[:, 1] > self.eta_max, 0, self.W) self.W = np.where(self.X[:, 0] < self.mmin, 0, self.W) self.W = np.where(self.X[:, 0] > self.mmax, 0, self.W) self.W = self.W / np.sum(self.W) # normalize # ---- resample ---- idx = self.resample() self.X = self.X[idx, :] self.W = np.ones(self.nP) / self.nP if tm == self.time[-1]: break # ---- apply kernel ---- print((bcolors.OKGREEN + "Kernel applied: [m:%d, eta:%f]" + bcolors.ENDC) % (self.kstd_m, self.kstd_eta)) self.X[:, 0] = self.X[:, 0] + np.random.normal( 0, self.kstd_m, self.nP) self.X[:, 1] = self.X[:, 1] + np.random.normal( 0, self.kstd_eta, self.nP) epsi = np.random.normal(0, kpsi, self.nP) va = np.sqrt(self.X[:, 5]**2 + self.X[:, 6]**2) psi = np.arctan2(self.X[:, 5], self.X[:, 6]) self.X[:, 5] = va * np.sin(psi + epsi) self.X[:, 6] = va * np.cos(psi + epsi) # ===== states evolution ===== dtm = self.time[it + 1] - self.time[it] dtp = min(processdt, dtm) # process update time interval for j in range(int(dtm / dtp)): # number of process update print("process update / state evolution, time", self.now) self.now = tm + (j + 1) * dtp self.X = self.state_update(self.X, dtp) self.logsm() return
import numpy as np from openap import Thrust from matplotlib import pyplot as plt from mpl_toolkits.mplot3d.axes3d import Axes3D thrust = Thrust(ac='A320', eng='CFM56-5B4') print('-'*70) T = thrust.takeoff(tas=100, alt=0) print("thrust.takeoff(tas=100, alt=0)") print(T) print('-'*70) T = thrust.climb(tas=200, alt=20000, roc=1000) print("thrust.climb(tas=200, alt=20000, roc=1000)") print(T) print('-'*70) T = thrust.cruise(tas=230, alt=32000) print("thrust.cruise(tas=230, alt=32000)") print(T) print('-'*70) T = thrust.climb(tas=[200], alt=[20000], roc=[1000]) print("thrust.climb(tas=[200], alt=[20000], roc=[1000])") print(T) print('-'*70)