python4oceanographers

Turning ripples into waves

Tidal analysis and prediction

There are few very modules for tidal analysis and prediction in python. In fact I can come up with just one name: tappy (Tidal Analysis Program in PYthon). Tappy has a command line interface and a syntax that is specific to its file format. In addition to that tappy is not being developed anymore. Luckily Sam Cox started pytides, in his own words:

Pytides is small Python package for the analysis and prediction of tides. Pytides can be used to extrapolate the tidal behaviour at a given location from its previous behaviour. The method used is that of harmonic constituents, in particular as presented by P. Schureman in Special Publication 98. The fitting of amplitudes and phases is handled by Scipy's leastsq minimisation function. Pytides currently supports the constituents used by NOAA, with plans to add more constituent sets. It is therefore possible to use the amplitudes and phases published by NOAA directly, without the need to perform the analysis again (although there may be slight discrepancies for some constituents).

It is recommended that all interactions with pytides which require times to be specified are in the format of naive UTC datetime instances. In particular, note that pytides makes no adjustment for summertime or any other civil variations within timezones.

This post is a quick example on how to perform tidal analysis and how to generate a prediction using pytides. First let's load some saved data that we will use as a basis for the tidal analysis.

(The data was downloaded from here).

In [2]:
from pandas import read_csv

df = read_csv('data/CO-OPS__8516945__hr.csv', index_col=0, parse_dates=True)

water_level = df[' Water Level']['2014-06-01':]

ax = water_level.plot(figsize=(13, 3.5))

Now let's perform the tidal analysis:

In [3]:
from pytides.tide import Tide

demeaned = water_level.values - water_level.values.mean()

tide = Tide.decompose(demeaned, water_level.index.to_datetime())

We can use pandas to create a nice table with the analysis results:

In [4]:
import numpy as np
from pandas import DataFrame

constituent = [c.name for c in tide.model['constituent']]

df = DataFrame(tide.model, index=constituent).drop('constituent', axis=1)

df.sort('amplitude', ascending=False).head(10)
Out[4]:
amplitude phase
S2 25.869209 60.953990
R2 25.203304 220.922496
K2 10.771259 235.999320
T2 8.921256 80.975323
S1 6.726733 358.386925
K1 3.680642 177.060124
P1 3.617312 184.508192
M2 1.296401 117.577471
N2 0.729587 114.433668
lambda2 0.603989 211.056662

The tide object has some handy methods for computing the Form Number and classifying the tide for us.

In [5]:
print('Form number %s, the tide is %s.' %
      (tide.form_number()[0], tide.classify()))
Form number 0.137759844397, the tide is semidiurnal.

With just a few lines we can generate an one-week prediction.

In [6]:
from pandas import Series, read_csv, date_range

dates = date_range(start='2014-07-01', end='2014-07-08', freq='6T')

hours = np.cumsum(np.r_[0, [t.total_seconds() / 3600.0
                            for t in np.diff(dates.to_pydatetime())]])

times = Tide._times(dates[0], hours)

prediction = Series(tide.at(times) + water_level.values.mean(), index=dates)

ax = water_level.plot(figsize=(13, 3.5), label='Observed data')
ax = prediction.plot(ax=ax, color='red', label='Prediction')
leg = ax.legend(loc='best')

In my opnion pytides is a promising module, but it does not calculate the analysis errors in the amplitude and phase, nor infers the tidal constituents. At least for now!

Last, but not least, let's augment Sam's tidal prediction table example with IPython's jinja2 rendered magic.

In [7]:
from IPython import display
from IPython.core.magic import register_cell_magic, Magics, magics_class, cell_magic
import jinja2

@magics_class
class JinjaMagics(Magics):
    '''Magics class containing the jinja2 magic and state'''
    
    def __init__(self, shell):
        super(JinjaMagics, self).__init__(shell)
        
        # create a jinja2 environment to use for rendering
        # this can be modified for desired effects (ie: using different variable syntax)
        self.env = jinja2.Environment(loader=jinja2.FileSystemLoader('.'))
        
        # possible output types
        self.display_functions = dict(html=display.HTML, 
                                      latex=display.Latex,
                                      json=display.JSON,
                                      pretty=display.Pretty,
                                      display=display.display)

    
    @cell_magic
    def jinja(self, line, cell):
        '''
        jinja2 cell magic function.  Contents of cell are rendered by jinja2, and 
        the line can be used to specify output type.

        ie: "%%jinja html" will return the rendered cell wrapped in an HTML object.
        '''
        f = self.display_functions.get(line.lower().strip(), display.display)
        
        tmp = self.env.from_string(cell)
        rend = tmp.render(dict((k,v) for (k,v) in self.shell.user_ns.items() 
                                        if not k.startswith('_') and k not in self.shell.user_ns_hidden))
        
        return f(rend)
        
    
ip = get_ipython()
ip.register_magics(JinjaMagics)
In [8]:
import calendar
from pytz import timezone
from datetime import datetime, timedelta
from jinja2 import Environment, DictLoader

# Prepare our variables for the template
location = "King's Point"
tzname = "US/Eastern"
tz = timezone(tzname)
utc = timezone('UTC')
datum = "MLLW"
units = "meters"
year = 2014
month = 7
rows = []
for day in range(1, calendar.monthrange(year, month)[1] + 1):
    start = tz.localize(datetime(year, month, day))
    end = start + timedelta(days=1)
    startUTC = utc.normalize(start.astimezone(utc))
    endUTC = utc.normalize(end.astimezone(utc))
    extremaUTC = tide.extrema(startUTC, endUTC)
    date = {'date': day, 'day': calendar.day_abbr[start.weekday()]}
    extrema = []
    for e in extremaUTC:
        time = tz.normalize(e[0].astimezone(tz))
        # Round the time to the nearest minute.
        time = time + timedelta(minutes=time.second > 30)
        height = e[1]
        extrema.append({'time': time.strftime('%H:%M'), 'height': "{0:.2f}".format(height)})
    # This is just for nicer formatting of days with only three tides.
    for _ in range(4 - len(extrema)):
        extrema.append({'time': '', 'height': ''})
    rows.append([date, extrema])
In [9]:
%%jinja html
<html>
<head>
<style>
h3 {font: sans-serif; color: #002277}
.datagrid table { border-collapse: collapse; text-align: left; width: 500px; }
.datagrid {font: normal 12px/150% Arial, Helvetica, sans-serif; background: #fff; overflow: hidden; border: 1px solid #006699; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; width:500px; }
.datagrid table td, .datagrid table th { padding: 3px 10px; }
.datagrid table thead th {background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #006699), color-stop(1, #00557F) );background:-moz-linear-gradient( center top, #006699 5%, #00557F 100% );filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#006699', endColorstr='#00557F');background-color:#006699; color:#FFFFFF; font-size: 15px; font-weight: bold; border-left: 1px solid #0070A8; }
.datagrid table thead th:first-child { border: none; }
.datagrid table tbody td { color: #00557F; border-left: 1px solid #E1EEF4;font-size: 12px;font-weight: normal; }
.datagrid table tbody .alt td { background: #E1EEf4; color: #00557F; }
.datagrid table tbody td:first-child { border-left: none; }
.datagrid table tbody tr:last-child td { border-bottom: none; }
.datagrid table tfoot td div { border-top: 1px solid #006699;background: #E1EEf4;}
.datagrid table tfoot td { padding: 0; font-size: 12px }
.datagrid table tfoot td div{ padding: 2px; }
.date {float:left}
.day {float:right}
.time {float:left}
.height {float:right}
</style>
</head>
<body>
<h3>Tide Table for {{location}} ({{month}} {{year}})</h3>
<div class="datagrid">
<table>
<thead><tr><th>Date</th><th colspan="4">Predictions</th></thead>
<tfoot><tr><td colspan="5"><div id="no-paging">Predictions given in {{units}} and {{tzname}} relative to {{datum}}</div></tr></tfoot>
<tbody>
{% for row in rows %}
<tr class="{{ loop.cycle('', 'alt') }}">
    <td><strong class="date">{{row[0]['date']}}</strong><strong class="day">{{row[0]['day']}}</strong></td>
    {% for e in row[1] %}
    <td><span class="time">{{ e['time'] }}</span><span class="height">{{ e['height'] }}</span></td>
    {% endfor %}
</tr>
{% endfor %}
</tbody>
</table></div>
</body>
</html>
Out[9]:

Tide Table for King's Point (7 2014)

DatePredictions
Predictions given in meters and US/Eastern relative to MLLW
1Tue 01:581.11 08:24-1.18 14:290.98 20:27-1.00
2Wed 02:321.17 09:01-1.19 15:040.94 21:06-1.00
3Thu 03:161.26 09:50-1.19 15:490.89 21:53-1.02
4Fri 04:091.38 10:58-1.22 16:490.85 22:51-1.06
5Sat 05:181.55 12:47-1.35 18:230.89
6Sun 00:07-1.15 06:451.79 14:02-1.59 20:001.06
7Mon 01:45-1.35 08:052.09 14:59-1.94 21:021.40
8Tue 02:59-1.67 09:052.43 15:47-2.37 21:511.84
9Wed 03:54-2.04 09:532.75 16:29-2.81 22:342.31
10Thu 04:41-2.38 10:352.97 17:07-3.20 23:132.73
11Fri 05:24-2.58 11:113.05 17:40-3.47 23:503.01
12Sat 06:02-2.57 11:422.96 18:10-3.59
13Sun 00:243.10 06:35-2.33 12:102.73 18:34-3.53
14Mon 00:552.97 07:00-1.88 12:332.38 18:55-3.31
15Tue 01:222.63 07:17-1.30 12:531.97 19:12-2.95
16Wed 01:442.15 07:29-0.68 13:081.53 19:24-2.53
17Thu 02:041.59 07:31-0.12 12:571.13 19:18-2.11
18Fri 02:161.03 05:550.11 11:151.53 18:19-2.00
19Sat 00:160.69 05:39-0.31 11:291.99 18:11-2.26
20Sun 00:161.18 05:47-0.68 11:412.25 18:15-2.52
21Mon 00:281.58 05:45-0.94 11:342.38 18:08-2.76
22Tue 00:311.86 05:25-1.21 11:192.61 17:47-3.19
23Wed 00:142.13 05:31-1.51 11:242.95 17:50-3.80
24Thu 00:122.48 05:47-1.75 11:353.31 18:04-4.36
25Fri 00:222.77 06:03-1.90 11:453.62 18:18-4.76
26Sat 00:342.93 06:12-1.96 11:483.90 18:26-4.98
27Sun 00:412.98 06:05-2.06 11:464.16 18:22-5.12
28Mon 00:373.04 05:59-2.29 11:514.41 18:21-5.29
29Tue 00:343.25 06:09-2.50 12:024.52 18:29-5.42
30Wed 00:423.53 06:22-2.53 12:144.42 18:38-5.45
31Thu 00:573.73 06:35-2.29 12:204.10 18:43-5.35
In [10]:
HTML(html)
Out[10]:

This post was written as an IPython notebook. It is available for download or as a static html.

Creative Commons License
python4oceanographers by Filipe Fernandes is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
Based on a work at https://ocefpaf.github.io/.

Comments