Searching for the SS Central America

On Tuesday, September 8th 1857, the steamboat SS Central America left Havana at 9 AM for New York, carrying about 600 passengers and crew members. Inside of this vessel, there was stowed a very precious cargo: a set of manuscripts by John James Audubon, and three tons of gold bars and coins. The manuscripts documented an expedition through the yet uncharted southwestern United States and California, and contained 200 sketches and paintings of its wildlife. The gold, fruit of many years of prospecting and mining during the California Gold Rush, was meant to start anew the lives of many of the passengers aboard.

On the 9th, the vessel ran into a storm which developed into a hurricane. The steamboat endured four hard days at sea, and by Saturday morning the ship was doomed. The captain arranged to have women and children taken off to the brig Marine, which offered them assistance at about noon. In spite of the efforts of the remaining crew and passengers to save the ship, the inevitable happened at about 8 PM that same day. The wreck claimed the lives of 425 men, and carried the valuable cargo to the bottom of the sea.

It was not until late 1980s that technology allowed recovery of shipwrecks at deep sea. But no technology would be of any help without an accurate location of the site. In the following paragraphs we would like to illustrate the power of the scipy stack by performing a simple simulation, that ultimately creates a dataset of possible locations for the wreck of the SS Central America, and mines the data to attempt to pinpoint the most probable target.

We simulate several possible paths of the steamboat (say 10,000 randomly generated possibilities), between 7:00 AM on Saturday, and 13 hours later, at 8:00 pm on Sunday. At 7:00 AM on that Saturday the ship’s captain, William Herndon, took a celestial fix and verbally relayed the position to the schooner El Dorado. The fix was 31º25’ North, 77º10’ West. Because the ship was not operative at that point—no engine, no sails—, for the next thirteen hours its course was solely subjected to the effect of ocean current and winds. With enough information, it is possible to model the drift and leeway on different possible paths.

We start by creating a data frame—a computational structure that will hold all the values we need in a very efficient way. We do so with the help of the pandas libraries.

1
2
3
4
5
6
7
8
9
10
11
12
from datetime import datetime, timedelta
from dateutil.parser import parse
import numpy as np, pandas as pd

interval = [parse("9/12/1857 7 am")]
for k in range(14*2-1):
    if k % 2 == 0:
        interval.append(interval[-1])
    else:
        interval.append(interval[-1] + timedelta(hours=1))

herndon = pd.DataFrame(np.zeros((28, 10000)), index = [interval, ['Lat', 'Lon']*14])

Each column of the data frame herndon is to hold the latitude and longitude of a possible path of the SS Central America, sampled every hour. Let us populate this data following a similar analysis to the one followed by the Columbus America Discovery Group, as explained by Lawrence D. Stone in the article Revisiting the SS Central America Search, from the 2010 Conference on Information Fusion.

The celestial fix obtained by Capt. Herndon at 7:00 AM was taken with a sextant in the middle of a storm. There are some uncertainties in the estimation of latitude and longitude with this method and under those weather conditions, which are modeled by a bivariate normally distributed random variable with mean (0,0) and standard deviations of 0.9 nautical miles (for latitude), and 3.9 nautical miles (for longitude). We create first a random variable with those characteristics. Let us use this idea to populate the dataframe with several random initial locations.

1
2
from scipy.stats import multivariate_normal
celestial_fix = multivariate_normal(cov = np.diag((0.9, 3.9)))

To estimate the corresponding celestial fixes, as well as all further geodetic computations, we will use the accurate formulas of Vincenty for ellipsoids, assuming a radius at the Equator of \(a = 63781374\) meters, and a flattening of the ellipsoid of \(f = 1/298.257223563\) (these figures are regarded as one of the standards for use in cartography, geodesy and navigation, and are referred by the community as the World Geodetic System WGS-84 ellipsoid)

A very good set of Vincenty’s formulas coded in python can be found at wegener.mechanik.tu-darmstadt.de/GMT-Help/Archiv/att-8710/Geodetic_py.

In particular, for this example we will be using Vincenty’s direct formula, that computes the resulting latitude \(\phi_2\), longitude \(\lambda_2\), and azimuth \(\alpha_2\) of an object starting at latitude \(\phi_1\), longitude \(\lambda_1\), and traveling \(s\) meters with initial azimuth \(\alpha_1\). Latitudes, longitudes and azimuths are given in degrees, and distances in meters. We also use the convention of assigning negative values to the latitudes to the West. To apply the conversion from nautical miles or knots to their respective units in SI, we employ the system of units in scipy.constants.

1
2
3
4
5
6
7
8
9
10
from Geodetic_py import vinc_pt
from scipy.constants import nautical_mile
a = 6378137.0
f = 1./298.257223563
for k in range(10000):
    lat_delta, lon_delta = celestial_fix.rvs() * nautical_mile
    azimuth = 90 - np.angle(lat_delta + 1j*lon_delta, deg=True)
    distance = np.hypot(lat_delta, lon_delta)
    output = vinc_pt(f, a, 31+25./60, -77-10./60, azimuth, distance)
    herndon.ix['1857-09-12 07:00:00',:][k] = output[0:2]

Issuing now the command herndon.ix['1857-09-12 07:00:00',:] gives us the following output:

--------------------------------------------------------------------------------
          0          1          2          3          4          5     \
Lat  31.455345  31.452572  31.439491  31.444000  31.462029  31.406287
Lon -77.148860 -77.168941 -77.173416 -77.163484 -77.169911 -77.168462   
          6          7          8          9       ...           9990  \
Lat  31.390807  31.420929  31.441248  31.367623    ...      31.405862
Lon -77.178367 -77.187680 -77.176924 -77.172941    ...     -77.146794   
          9991       9992       9993       9994       9995       9996  \
Lat  31.394365  31.428827  31.415392  31.443225  31.350158  31.392087
Lon -77.179720 -77.182885 -77.159965 -77.186102 -77.183292 -77.168586   
          9997       9998       9999
Lat  31.443154  31.438852  31.401723
Lon -77.169504 -77.151137 -77.134298  
[2 rows x 10000 columns]
--------------------------------------------------------------------------------

We simulate the drift according to the formula \(D = (V + \gamma W)\). In this formula \(V\) (the ocean current) is modeled as vector pointing about North-East (around 45 degrees of azimuth) and a variable speed between 1 and 1.5 knots. The other random variable, \(W\), represents the action of the winds in the area during the hurricane, which we choose to represent by directions ranging between South and East, and speeds with a mean 0.2 knots, standard deviation 1/30 knots. Both random variables are coded as bivariate normal. Finally, we have accounted for the leeway factor. According to a study performed on the blueprints of the SS Central America, we have estimated this leeway \(\gamma\) to be about 3%.

This choice of random variables to represent the ocean current and wind differs from the ones used in the aforementioned paper. In our version we have not used the actual covariance matrices as computed by Stone from data received from the Naval Oceanographic Data Center. Rather, we have presented a very simplified version.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
current = multivariate_normal((np.pi/4, 1.25), cov=np.diag((np.pi/270, .25/3)))
wind    = multivariate_normal((np.pi/4, .3), cov=np.diag((np.pi/12, 1./30)))
leeway  = 3./100
for date in pd.date_range('1857-9-12 08:00:00', periods=13, freq='1h'):
    before  = herndon.ix[date-timedelta(hours=1)]
    for k in range(10000):
        angle, speed = current.rvs()
        current_v = speed * nautical_mile * (np.cos(angle) + 1j * np.sin(angle))
        angle, speed  = wind.rvs()
        wind_v = speed * nautical_mile * (np.cos(angle) + 1j*np.sin(angle))
        drift = current_v + leeway * wind_v
        azimuth = 90 - np.angle(drift, deg=True)
        distance = abs(drift)
        output = vinc_pt(f, a, before.ix['Lat'][k], before.ix['Lon'][k], azimuth, distance)
        herndon.ix[date,:][k] = output[:2]

Let us plot the first three of those simulated paths:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap 
m = Basemap(llcrnrlon=-77.4, llcrnrlat=31.2, urcrnrlon=-76.6,
            urcrnrlat=31.8, projection='lcc', lat_0 = 31.5,
            lon_0=-77, resolution='l', area_thresh=1000.)
m.drawmeridians(np.arange(-77.4,-76.6,0.1), labels=[0,0,1,1])
m.drawparallels(np.arange(31.2,32.8,0.1), labels=[1,1,0,0])
colors = ['r', 'b', 'k']
styles = ['-', '--', ':']
for k in range(3):
    longitudes = herndon[k][:,'Lon'].values
    latitudes  = herndon[k][:,'Lat'].values
    longitudes, latitudes = m(longitudes, latitudes)
    m.plot(longitudes, latitudes, color=colors[k], lw=3, ls=styles[k])

plt.show()

As expected they observe a North-Easterly general direction, in occasion showing deviations from the effect of the strong winds.

The advantage of storing all the different steps in these paths becomes apparent if we need to perform some further study—and maybe filtering—on the data obtained. We could impose additional conditions to paths, and use only those filtered according to the extra rules, for example. Another advantage is the possibility of performing different analysis on the paths with very little coding. By issuing the command herndon.loc(axis=0)[:,'Lat'].describe(), we obtain quick statistics on the computed latitudes for all 10000 paths (number of items, mean, standard deviation, min, max and the quartiles of the data).

--------------------------------------------------------------------------------
            0          1          2          3          4          5     \
count  14.000000  14.000000  14.000000  14.000000  14.000000  14.000000
mean   31.474706  31.489831  31.479797  31.551953  31.543533  31.516511
std     0.058218   0.060026   0.060504   0.060204   0.060290   0.065008
min    31.388410  31.400693  31.387974  31.457087  31.446786  31.421331
25%    31.431773  31.439437  31.429712  31.506764  31.498132  31.465424
50%    31.470768  31.491100  31.481918  31.551521  31.546649  31.510613
75%    31.515063  31.541353  31.527317  31.599800  31.588251  31.568368
max    31.571535  31.580176  31.575999  31.641745  31.639944  31.619281   
            6          7          8          9       ...           9990  \
count  14.000000  14.000000  14.000000  14.000000    ...      14.000000
mean   31.463063  31.515181  31.510682  31.448281    ...      31.541765
std     0.064530   0.060904   0.064621   0.061350    ...       0.059633
min    31.369973  31.410879  31.412623  31.353806    ...      31.445757
25%    31.410827  31.473212  31.460292  31.398931    ...      31.500659
50%    31.460968  31.516328  31.511749  31.448647    ...      31.542774
75%    31.512171  31.565814  31.555854  31.492793    ...      31.583822
max    31.564134  31.601316  31.620487  31.547686    ...      31.632320   
            9991       9992       9993       9994       9995       9996  \
count  14.000000  14.000000  14.000000  14.000000  14.000000  14.000000
mean   31.481608  31.501862  31.509630  31.495412  31.557487  31.491508
std     0.064426   0.061343   0.057857   0.068578   0.058520   0.055164
min    31.384021  31.398002  31.408542  31.387861  31.452803  31.402979
25%    31.422987  31.457732  31.468419  31.440465  31.518770  31.450746
50%    31.489742  31.509546  31.515301  31.503460  31.565790  31.493993
75%    31.532992  31.549224  31.553803  31.545975  31.596499  31.532340
max    31.576120  31.589048  31.591815  31.599973  31.645622  31.577492   
            9997       9998       9999
count  14.000000  14.000000  14.000000
mean   31.522756  31.509904  31.461305
std     0.055411   0.064045   0.066058
min    31.435634  31.408430  31.363115
25%    31.482015  31.458449  31.405953
50%    31.523253  31.519991  31.463235
75%    31.567399  31.555133  31.516251
max    31.605647  31.605175  31.556127  
[8 rows x 10000 columns]
--------------------------------------------------------------------------------

The focus of this simulation is, nonetheless, on the final location of all these paths. Let us plot them all on the same map first, for a quick visual evaluation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
latitudes, longitudes = herndon.ix['1857-9-12 20:00:00'].values
m = Basemap(llcrnrlon=-82., llcrnrlat=31, urcrnrlon=-76,
            urcrnrlat=32.5, projection='lcc', lat_0 = 31.5,
            lon_0=-78, resolution='h', area_thresh=1000.)
longitudes, latitudes = m(longitudes, latitudes)
x, y = m(-81.2003759, 32.0405369)  # Coordinates of Savannah, GA
m.plot(longitudes, latitudes, 'ko', markersize=1)
m.plot(x,y,'bo')
plt.text(x-10000, y+10000, 'Savannah, GA')
m.drawmeridians(np.arange(-82,-76,1), labels=[1,1,1,1])
m.drawparallels(np.arange(31,32.5,0.25), labels=[1,1,0,0])
m.drawcoastlines()
m.drawcountries()
m.fillcontinents(color='coral')
m.drawmapboundary()
plt.show()