Problem Statement

What is the Circular Error Probable (CEP) for the upcoming drop test of the Electromechanical Recovery System (ERS) under a range of possible scenarios?

Definition

Loosely speaking, CEP is the mean distance of a sample of points from their mean impact point. That is, a circle around the average point that contains 50% of the sample points. Here, we are defining CEP as the square root of the mean square error, i.e. the sum of range and azimuth variances and their covariance (ignoring bias for now).

Situation

The ERS will be dropped out of a helicopter somewhere near the McMinnville Airport and for safety and liability reasons, we must have an expectation of the area it may land in. The primary variables we are considering is a range of possible helicopter trajectories (in terms of velocity and heading, with positions randomized around a certain mean) as well as a range of likely wind conditions (also as velocities and headings). GPS error is accounted for in the final calculation.

There are four operational modes (that is, three failure modes and a successful mission) to consider. There are also two helicopter airspeeds, 15.5 and 30 knots, to consider, thus we have eight cases of simulations in total.

For our operational modes, we consider the case that the mission is successful. We consider the two cases that only the drogue parachute fails and that the ERS system fails completely to be identical, given the distance we drop from. We consider the case that the drogue deploys correctly but remains attached while the main chute does not deploy. We consider the case that the main chute somehow deploys when the drogue is supposed to, which we expect to provide our worst case in terms of CEP spread.

In all scenarios, we consider the wind in a normal distribution about one direction with a standard deviation of 10 degrees. Thus, all simulations are contained within a 60 degree cone, and a large majority are within a 40 degree cone. The helicopter is considered to be going against that mean wind direction at the specified airspeed with some noise injected for realism. On the plots, the wind blows downwards and the helicopter flies upwards. It should also be noted that we may wish to discard any simulations with wind speeds above 5.15 m/s (that is approximately 10 knots) in order to lower CEP, but we will not do so here.

Since the simulations are conducted relative to an arbitrary direction, similar results should hold in any absolute direction by symmetry. The utility of this approach is that on the day of the test, given the current wind velocity, we should have an idea of how where the system is likely to drift to relative to its release point. This could allow us to select our release point to ensure that the possible impact zone is bounded, and in the case of an irregularly shaped field to drop in, we would have a better sense of which wind speeds are acceptable in any given direction.

Methodology

Another Notebook contains a trajectory simulator and our documentation of it. We have reason to believe our model is approximately representative of the actual system and environment, assuming wind may be held constant for up to several minutes. The trajectory is available both as a numerical integration of a system of ordinary differential equations and as a linearization of that integration. In either case, the interaction of the drogue parachute with air must be integrated, but the phases prior to that and after that may be considered as steady-states without loss of significant precision. Hence, we use the numerical integration of our trajectory model in this Notebook which is much slower to compute, but much smoother.

Since CEP relies on observational evidence to calculate, we selected a Monte Carlo simulation of our trajectory model to generate data for statistical analysis. For each of our eight scenarios, we randomly sample 1,250 trajectories (with random variables considered independently) and we collect every final impact point, along with their initial conditions.

Conclusions


In [1]:
%run drop_test_simulator.ipynb
import pandas as pd

from bokeh.io import output_file, output_notebook, show
from bokeh.models import (
  GMapPlot, GMapOptions, ColumnDataSource, Circle, LogColorMapper, BasicTicker, ColorBar,
    DataRange1d, PanTool, WheelZoomTool, BoxSelectTool, Range1d
)
from bokeh.models.mappers import ColorMapper, LinearColorMapper
from bokeh.palettes import Viridis5

In [2]:
def convert_to_lat(y):
    lat_in_ft = 364618.1186706298
    return y/lat_in_ft

def convert_to_long(x):
    long_in_ft = 257742.06079152823
    return x/long_in_ft

Note that this code has been written with a lot of generality to make it easier to specify later simulations of our various scenarios completely with single lines of code.


In [3]:
heli_direction = 230
# note that if dt=0.25 the horizontal distance may be off by about 8 m (compared to dt=0.001)
def mc_init(num_sims=500, dt=0.25, mode='', airspeed=15.43334):
    all_coords = []
    v_wind_rndm = []
    v_plane_rndm = []
    init_pos_offset = []

    deg_wind_mean, deg_wind_std_dev  = (50, 10) # 20-40-60 degree cone (with probability 68-95-99.7)
    
    for i in range(num_sims):
        v_wind_rndm.append([np.random.rayleigh(2.47344), # rayleigh distro good for wind magnitude
                            np.random.normal(deg_wind_mean, deg_wind_std_dev)])
        
        v_plane_rndm.append(np.random.normal(
                airspeed + v_wind_rndm[-1][0] * np.cos(np.radians(v_wind_rndm[-1][1] - heli_direction)),
                1))
        
        init_pos_offset.append([np.random.normal(0, 30 * 0.3048), # converting feet to meters
                                np.random.normal(0, 30 * 0.3048),
                                np.random.normal(0, 15 * 0.3048)]) # account for the imperfections of reality
    print('random wind and helicopter vectors obtained\n')

    for i, vw in enumerate(v_wind_rndm):
        all_coords.append(trajectory(v_plane=v_plane_rndm[i],
                                          deg_plane=heli_direction,
                                          v_wind=vw[0],
                                          deg_wind=vw[1],
                                          x_0_offset=init_pos_offset[i],
                                          mode=mode,
                                          dt=dt)[0])
        if i % 250 == 0: print('iterations:', i)
    print('done simulations!\n')
    
    return (all_coords, v_wind_rndm, v_plane_rndm)

In [4]:
def polar_coord(df):
    # magnitude of landing coordinate
    df['norm'] = df.loc[:,['x','y']].apply(np.linalg.norm, axis=1, raw=True)
    # azimuth angle of landing (note that this is on the unit circle)
    df['theta'] = df.apply(lambda pos: np.arctan2(pos[1], pos[0]), axis=1, raw=True)

def lat_long(df):
    df['latitude'] = df.apply(lambda tmp: convert_to_lat(tmp[1]), axis=1, raw=True)
    df['longitude'] = df.apply(lambda tmp: convert_to_long(tmp[0]), axis=1, raw=True)
      
def handle_data(all_coords, v_wind_rndm, v_plane_rndm, wind_limit=None):
    def wind_envelope(df, cutoff):
        bool_list = df['v_w'] <= cutoff
        return df[bool_list]
    # landing coordinates
    results_df = pd.DataFrame.from_records([rotate(np.array([m_to_ft(Xi) for Xi in X]),
                                                   np.radians(heli_direction) - np.pi/2)
                                            for X in all_coords],
                                           columns=['x', 'y'])
    #results_df = pd.DataFrame.from_records([np.array([m_to_ft(Xi) for Xi in X])
    #                                        for X in all_coords],
    #                                       columns=['x','y'])
    polar_coord(results_df)
    # plane velocities
    results_df['v_plane'] = [ms_to_knots(vel) for vel in v_plane_rndm]
    # wind magnitude
    results_df['v_w'] = [ms_to_knots(vel[0]) for vel in v_wind_rndm]
    # wind direction (is this on unit circle? need to think more...)
    results_df['deg_w'] = [vel[1] for vel in v_wind_rndm]
    lat_long(results_df)
    
    if wind_limit != None:
        print("Cutting out all runs with winds above "+str(wind_limit)+" m/s.")
        results_df = wind_envelope(results_df, wind_limit)
        
    print(results_df.describe())
    return results_df

def translate_cluster(df, offset):
    df['x'] = df['x'].apply(lambda x: x + offset[0])
    df['y'] = df['y'].apply(lambda y: y + offset[1])
    polar_coord(df)
    lat_long(df)
    
# i don't want to talk about how ugly this code block is lol
def rotate_cluster(df, theta):
    array = df.loc[:, ['x', 'y']]
    X = [x for x in array['x']]
    Y = [y for y in array['y']]
    new = [rotate(np.array([x, Y[i]]), theta) for i, x in enumerate(X)]
    X = [x[0] for x in new]
    Y = [y[1] for y in new]
    df['x'] = X
    df['y'] = Y
    polar_coord(df)
    lat_long(df)

def get_mean(df):
    mean_x = df.apply(lambda pos: pos[2] * np.cos(pos[3]), axis=1, raw=True).describe()['mean']
    mean_y = df.apply(lambda pos: pos[2] * np.sin(pos[3]), axis=1, raw=True).describe()['mean']
    return mean_x, mean_y

In [5]:
def visualize(results_df):
    # these are not the means of the coordinates, taken individually
    # this is the coordinate of the mean impact point, hence the polar coordinates
    mean_x, mean_y = get_mean(results_df)
    print('Mean Impact: (', mean_x, ',', mean_y, ')')

    # assume that the bias is 0 until we have an actual target point
    # so CEP = sqrt(MSE) = sqrt(var_norm + var_theta + cov_norm_theta)
    # this is the mean distance from the mean impact point
    variance = results_df.var(axis=0)[2:4].sum()
    covariance = results_df.cov().iloc[2, 3]
    # GPS is 95% inside a 2x2 m box, and 95% of values on one axis are within 2*RMS of ideal
    # thus RMS = 1/2, since 2* 1/2 =1, and 1 on each side gives a 2 m bound. Therefore MSE = 1/4, which we double
    # because there are two axes to account for
    GPS_MSE = m_to_ft(m_to_ft(1/4))
    bias = 0
    total_MSE = variance + covariance + bias + 2*GPS_MSE
    CEP = np.sqrt(total_MSE)
    print('CEP:', CEP.round(3), 'ft')

    plt.figure(1)
    plt.hist2d(results_df['v_w'], results_df['deg_w'], 25)
    plt.title('wind vectors')
    plt.xlabel('v (knots)')
    plt.ylabel('direction (degrees)')
    plt.show()

    plt.figure(2)
    plt.hist(results_df['v_w'], 30)
    plt.xlabel('wind magnitude (knots)')
    plt.ylabel('count')
    plt.title('wind speed distribution')
    plt.show()

    plt.figure(3)
    plt.hist(results_df['norm'], 30)
    plt.title('distances from origin')
    plt.xlabel('radius (ft)')
    plt.ylabel('count')
    plt.show()

    plt.figure(4)
    plt.hist2d(results_df['x'],results_df['y'], 30)
    plt.plot(mean_x, mean_y, 'r^')
    plt.title('landing sites')
    plt.xlabel('x (ft)')
    plt.ylabel('y (ft)')
    plt.show()

    plt.figure(5)
    results_df.plot.scatter('x', 'y', s=1, c='v_plane', colormap='inferno')
    plt.axis('equal')
    circle1 = plt.Circle((mean_x, mean_y), CEP, color='r', fill=False)
    circle2 = plt.Circle((mean_x, mean_y), 2*CEP, color='tab:orange', fill=False)
    circle3 = plt.Circle((mean_x, mean_y), 3*CEP, color='y', fill=False)
    plt.gca().add_artist(circle1)
    plt.gca().add_artist(circle2)
    plt.gca().add_artist(circle3)
    plt.title('impact points (and initial helicopter velocities)')
    plt.show()

    plt.figure(6)
    results_df.plot.scatter('x', 'y', s=1, c='v_w', colormap='inferno')
    plt.axis('equal')
    circle1 = plt.Circle((mean_x, mean_y), CEP, color='r', fill=False)
    circle2 = plt.Circle((mean_x, mean_y), 2*CEP, color='tab:orange', fill=False)
    circle3 = plt.Circle((mean_x, mean_y), 3*CEP, color='y', fill=False)
    plt.gca().add_artist(circle1)
    plt.gca().add_artist(circle2)
    plt.gca().add_artist(circle3)
    plt.title('impact points (and initial wind velocities)')
    plt.show()
    
    return ((mean_x, mean_y), CEP)

In [6]:
def plot_on_map(df, CEP):
    lat_0 = 45.208778
    long_0 = -123.138722
    mean_x, mean_y = get_mean(df)
    
    mean_long = convert_to_long(mean_x) + long_0
    mean_lat = convert_to_lat(mean_y) + lat_0
    avg_CEP_est = (convert_to_lat(CEP) + convert_to_long(CEP)) / 2
    
    map_options = GMapOptions(lat=lat_0, lng=long_0, map_type="satellite", zoom=16)
    
    plot = GMapPlot(x_range=Range1d(), y_range=Range1d(), map_options=map_options)
    plot.title.text = "Impact Points"
    plot.api_key = "AIzaSyCYsl3277rp6nR3BUUx2q3Z9R4fyI2J1qo"
    
    #this block doesn't work :(
    circle1 = Circle(x=mean_long, y=mean_lat, radius=avg_CEP_est, line_width=2, line_color='red')
    circle2 = Circle(x=mean_long, y=mean_lat, radius=avg_CEP_est*2, line_width=2, line_color='green')
    circle3 = Circle(x=mean_long, y=mean_lat, radius=avg_CEP_est*3, line_width=2, line_color='blue')
    plot.add_glyph(circle1)
    plot.add_glyph(circle2)
    plot.add_glyph(circle3)
    
    source = ColumnDataSource(
    data=dict(
        lat=df.latitude.apply(lambda y: y + lat_0).tolist(),
        lon=df.longitude.apply(lambda x: x + long_0).tolist(),
        v_h=df.v_plane.tolist(),
        v_w=df.v_w.tolist()))
    color_mapper = LinearColorMapper(palette=Viridis5)
    circle = Circle(x="lon", y="lat", size="v_w", fill_color={'field': 'v_h', 'transform': color_mapper},
                    fill_alpha=0.5, line_color=None)
    plot.add_glyph(source, circle)
    
    color_bar = ColorBar(color_mapper=color_mapper, ticker=BasicTicker(),
                     label_standoff=12, border_line_color=None, location=(0,0))
    plot.add_layout(color_bar, 'right')
    plot.add_tools(PanTool(), WheelZoomTool())
    output_notebook()

    show(plot)

In [7]:
amount = 1150
# mission success
succ_coords, succ_wind, succ_plane = mc_init(num_sims=amount, dt=0.05, airspeed=15.43334)

# main chute failure
coords_A, wind_A, plane_A = mc_init(num_sims=amount, dt=0.05, mode='A', airspeed=15.43334)

# drogue failure
coords_B, wind_B, plane_B = mc_init(num_sims=amount, dt=0.05, mode='B', airspeed=15.43334)

# early deployment
coords_C, wind_C, plane_C = mc_init(num_sims=amount, dt=0.05, mode='C', airspeed=15.43334)


random wind and helicopter vectors obtained

iterations: 0
iterations: 250
iterations: 500
iterations: 750
iterations: 1000
done simulations!

random wind and helicopter vectors obtained

iterations: 0
iterations: 250
iterations: 500
iterations: 750
iterations: 1000
done simulations!

random wind and helicopter vectors obtained

iterations: 0
iterations: 250
iterations: 500
iterations: 750
iterations: 1000
done simulations!

random wind and helicopter vectors obtained

iterations: 0
iterations: 250
iterations: 500
iterations: 750
iterations: 1000
done simulations!

Note that the following plots are not normalized so visual comparison must take their differing scales into account. Please recall that the helicopter is travelling in the positive Y direction.

Analysis of Mission Success


In [8]:
succ_data = handle_data(succ_coords, succ_wind, succ_plane, wind_limit=None)
succ_pos, succ_CEP = visualize(succ_data)
plot_on_map(succ_data, succ_CEP)


                 x            y         norm        theta      v_plane  \
count  1150.000000  1150.000000  1150.000000  1150.000000  1150.000000   
mean    378.505932   460.478568   635.239866     0.667438    24.227679   
std     330.528550   392.031673   463.328772     0.915781     3.601488   
min    -218.284340  -246.553530     2.333411    -3.103887    10.869476   
25%     129.496198   169.865883   246.711335     0.630961    22.083558   
50%     329.194716   404.585589   554.211394     0.859396    24.550094   
75%     581.123778   693.685789   910.206444     1.063299    26.711542   
max    1833.141994  2116.385379  2551.359360     3.129973    33.977995   

               v_w        deg_w     latitude    longitude  
count  1150.000000  1150.000000  1150.000000  1150.000000  
mean      5.852341    50.161635     0.001263     0.001469  
std       3.110355     9.811278     0.001075     0.001282  
min       0.075232    17.520200    -0.000676    -0.000847  
25%       3.510831    43.409131     0.000466     0.000502  
50%       5.507432    50.325977     0.001110     0.001277  
75%       7.740284    57.319479     0.001902     0.002255  
max      18.002148    82.365748     0.005804     0.007112  
Mean Impact: ( 378.5059319140015 , 460.4785680330383 )
CEP: 463.451 ft
<matplotlib.figure.Figure at 0x7f8830ef4390>
<matplotlib.figure.Figure at 0x7f8833423208>
Loading BokehJS ...

In [9]:
#rotate_cluster(succ_data, np.pi)
#translate_cluster(succ_data, (-500, -500))
#succ_pos, succ_CEP = visualize(succ_data)
#plot_on_map(succ_data, succ_CEP)

Analysis of Main Chute Failure (with successful Drogue Deployment)


In [10]:
data_A = handle_data(coords_A, wind_A, plane_A, wind_limit=None)
pos_A, CEP_A = visualize(data_A)
plot_on_map(data_A, CEP_A)


                 x            y         norm        theta      v_plane  \
count  1150.000000  1150.000000  1150.000000  1150.000000  1150.000000   
mean     87.901600   102.304539   224.508843     0.039189    24.044109   
std     164.202197   184.018708   169.073657     1.533659     3.731100   
min    -231.819867  -256.341999     3.290254    -3.125080     9.195663   
25%     -35.773065   -36.459149   102.797610    -1.684176    21.696789   
50%      60.960631    81.090461   180.217393     0.658293    24.321069   
75%     186.937516   215.693728   303.478201     1.067141    26.761098   
max     825.855604   795.743285  1131.048674     3.121588    34.307117   

               v_w        deg_w     latitude    longitude  
count  1150.000000  1150.000000  1150.000000  1150.000000  
mean      6.152431    49.957380     0.000281     0.000341  
std       3.134546    10.289073     0.000505     0.000637  
min       0.407572    14.922127    -0.000703    -0.000899  
25%       3.798640    43.360504    -0.000100    -0.000139  
50%       5.845800    50.048184     0.000222     0.000237  
75%       8.100896    56.737102     0.000592     0.000725  
max      19.573362    81.502412     0.002182     0.003204  
Mean Impact: ( 87.90159951500604 , 102.30453919634076 )
CEP: 169.314 ft
<matplotlib.figure.Figure at 0x7f8830393f98>
<matplotlib.figure.Figure at 0x7f88302adcf8>
Loading BokehJS ...

Analysis of Drogue Failure


In [11]:
data_B = handle_data(coords_B, wind_B, plane_B, wind_limit=None)
pos_B, CEP_B = visualize(data_B)
plot_on_map(data_B, CEP_B)


                 x            y         norm        theta      v_plane  \
count  1150.000000  1150.000000  1150.000000  1150.000000  1150.000000   
mean   -274.040590  -326.829940   427.591521    -2.268279    24.255449   
std      54.439314    61.127717    76.032779     0.072960     3.510566   
min    -414.032104  -496.268501   151.974445    -2.477894    11.821262   
25%    -312.236814  -369.424777   377.641856    -2.320389    21.903875   
50%    -277.546959  -330.859271   431.872423    -2.268175    24.466640   
75%    -238.357004  -287.439733   481.912537    -2.220220    26.850658   
max     -76.720002  -109.204693   626.629293    -1.954055    32.847291   

               v_w        deg_w     latitude    longitude  
count  1150.000000  1150.000000  1150.000000  1150.000000  
mean      5.896968    50.066555    -0.000896    -0.001063  
std       3.091631     9.899701     0.000168     0.000211  
min       0.113178    19.731858    -0.001361    -0.001606  
25%       3.645027    42.949273    -0.001013    -0.001211  
50%       5.445931    49.852484    -0.000907    -0.001077  
75%       7.869084    56.293705    -0.000788    -0.000925  
max      15.886538    85.185373    -0.000300    -0.000298  
Mean Impact: ( -274.0405902218037 , -326.8299401396373 )
CEP: 76.067 ft
<matplotlib.figure.Figure at 0x7f88300c2240>
<matplotlib.figure.Figure at 0x7f882af1ed30>
Loading BokehJS ...

Analysis of Main Chute Early Deployment (with Drogue Failure)


In [12]:
data_C = handle_data(coords_C, wind_C, plane_C, wind_limit=None)
pos_C, CEP_C = visualize(data_C)
plot_on_map(data_C, CEP_C)


                 x            y         norm        theta      v_plane  \
count  1150.000000  1150.000000  1150.000000  1150.000000  1150.000000   
mean    770.106161   923.222592  1235.782147     0.797611    24.079927   
std     534.506155   633.944709   778.304253     0.544129     3.732791   
min    -191.108102  -175.530761    15.096178    -3.023255    10.600518   
25%     371.964234   448.925385   648.685547     0.707698    21.776459   
50%     663.049282   809.318519  1111.125587     0.867846    24.533394   
75%    1096.015880  1260.746640  1734.508961     1.020441    26.720337   
max    3135.467695  3397.230995  4017.562396     3.088765    35.124697   

               v_w        deg_w     latitude    longitude  
count  1150.000000  1150.000000  1150.000000  1150.000000  
mean      6.077533    50.000101     0.002532     0.002988  
std       3.261194    10.222185     0.001739     0.002074  
min       0.071772     7.530418    -0.000481    -0.000741  
25%       3.627374    43.223613     0.001231     0.001443  
50%       5.537530    50.086124     0.002220     0.002573  
75%       8.253077    57.040599     0.003458     0.004252  
max      17.521316    86.003081     0.009317     0.012165  
Mean Impact: ( 770.1061607897028 , 923.2225924630526 )
CEP: 778.364 ft
<matplotlib.figure.Figure at 0x7f8830226b70>
<matplotlib.figure.Figure at 0x7f88303b7a20>
Loading BokehJS ...

Saving data and Aggregate Results


In [13]:
succ_data.to_csv(path_or_buf='./drop_test_sample_data/succ_data.csv')
data_A.to_csv(path_or_buf='./drop_test_sample_data/data_A.csv')
data_B.to_csv(path_or_buf='./drop_test_sample_data/data_B.csv')
data_C.to_csv(path_or_buf='./drop_test_sample_data/data_C.csv')

In [14]:
aggregate = pd.concat(map(pd.read_csv, ['./drop_test_sample_data/succ_data.csv',
                                        './drop_test_sample_data/data_A.csv',
                                        './drop_test_sample_data/data_B.csv', 
                                        './drop_test_sample_data/data_C.csv']))

In [15]:
aggregate.drop('Unnamed: 0', axis='columns', inplace=True)
print(aggregate.describe())


                 x            y         norm        theta      v_plane  \
count  4600.000000  4600.000000  4600.000000  4600.000000  4600.000000   
mean    240.618275   289.793940   630.780594    -0.191010    24.151791   
std     503.062898   599.731931   597.234064     1.547058     3.645141   
min    -414.032104  -496.268501     2.333411    -3.125080     9.195663   
25%    -161.379671  -192.517991   246.386034    -2.206264    21.880355   
50%     135.488549   173.333323   443.491018     0.671420    24.467609   
75%     479.195200   579.517875   778.010854     0.967970    26.759342   
max    3135.467695  3397.230995  4017.562396     3.129973    35.124697   

               v_w        deg_w     latitude    longitude  
count  4600.000000  4600.000000  4600.000000  4600.000000  
mean      5.994818    50.046418     0.000795     0.000934  
std       3.151545    10.054640     0.001645     0.001952  
min       0.071772     7.530418    -0.001361    -0.001606  
25%       3.635772    43.262015    -0.000528    -0.000626  
50%       5.559955    50.054997     0.000475     0.000526  
75%       7.965982    56.878638     0.001589     0.001859  
max      19.573362    86.003081     0.009317     0.012165  

In [16]:
agg, agg_cep = visualize(aggregate)


Mean Impact: ( 240.61827549922668 , 289.7939398881987 )
CEP: 597.491 ft
<matplotlib.figure.Figure at 0x7f882ae56da0>
<matplotlib.figure.Figure at 0x7f8830342a20>

In [ ]: