{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Storm Surge Alerts Feeds\n", "\n", "## Development Notes\n", "\n", "Notes about development of ATOM/RSS feeds for storm surge alerts.\n", "Started in the process of working out the details of how to provide\n", "a feed to Port Metro Vancouver during the 2015/2016 storm surge season.\n", "Consideration is also given to the possibility of providing a wider collection\n", "of feeds in the future." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Port Metro Vancouver presently consumes [Environment Canada feeds](https://weather.gc.ca/business/index_e.html),\n", "so we use that as our pattern.\n", "\n", "Feeds are generically referred to as RSS,\n", "but there are 2 current standards for them:\n", "[ATOM](https://en.wikipedia.org/wiki/Atom_%28standard%29)\n", "and [RSS-2.0](https://en.wikipedia.org/wiki/RSS).\n", "EC uses ATOM,\n", "and it seems to be somewhat more favourable technically,\n", "so that's what we'll focus on initially.\n", "\n", "The IETF standard for ATOM is [RFC 4287](https://tools.ietf.org/html/rfc4287).\n", "\n", "The [Vancouver weather forecast web page](http://weather.gc.ca/city/pages/bc-74_metric_e.html) \n", "has links to the [weather forecast feed](http://weather.gc.ca/rss/city/bc-74_e.xml),\n", "and the [weather alerts feed](http://weather.gc.ca/rss/warning/bc-74_e.xml).\n", "Using the Firefox \"View Page Source\" function on the page that loads from either of those\n", "feed links lets you see the structure of the feed content.\n", "\n", "Other useful links:\n", "\n", "* A blog post about [creating ATOM ID elements](http://web.archive.org/web/20110514113830/http://diveintomark.org/archives/2004/05/28/howto-atom-id)\n", "\n", "* A blog post (linked from the above one) about [using tag URIs as ATOM IDs](http://web.archive.org/web/20110514113830/http://www.taguri.org/)\n", "\n", "* The Python `feedgen` package [documentation](http://lkiesow.github.io/python-feedgen/),\n", "[Github repository](https://github.com/lkiesow/python-feedgen),\n", "and [PyPI page](https://pypi.python.org/pypi/feedgen/)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### URLs\n", "\n", "The storm surge alerts feeds will be located under http://salishsea.eos.ubc.ca/storm-surge/.\n", "\n", "The initial ATOM feed for Port Metro Vancouver (PMV) will be http://salishsea.eos.ubc.ca/storm-surge/atom/pmv.xml.\n", "\n", "That pattern can be extended to provide other feed like:\n", "* One containing alert messages for the entire model domain\n", "based on Ben's narrative template developed during the Aug-2015 sprint:\n", "http://salishsea.eos.ubc.ca/storm-surge/atom/alerts.xml\n", "* Feeds for specific locations: http://salishsea.eos.ubc.ca/storm-surge/atom/PointAtkinson.xml\n", "* Feeds customized for other stakeholders\n", "\n", "This URL structure also allows for the possibility of generating RSS-2.0 feeds in the future\n", "at locations like:\n", "* http://salishsea.eos.ubc.ca/storm-surge/rss/pmv.xml\n", "* http://salishsea.eos.ubc.ca/storm-surge/rss/alerts.xml\n", "* http://salishsea.eos.ubc.ca/storm-surge/rss/PointAtkinson.xml\n", "* etc.\n", "\n", "Even though the PMV and Point Atkinson feeds are for the same geolocation \n", "and are expressions of the same model results,\n", "they are treated separately so that the PMV feed can be customized with\n", "units,\n", "threshold levels,\n", "etc.\n", "requested by that stakeholder." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Generating the PMV Feed\n", "\n", "EC appears to generate a new feed each time the [forecast page](http://weather.gc.ca/city/pages/bc-74_metric_e.html)\n", "is updated\n", "(so, hourly, at least)\n", "rather than appeding new feed entries to an existing feed.\n", "We will adopt the same practice for the PMV feed:\n", "* A new feed will be generated each day after the forecast and forecast2 runs\n", "* In contrast to EC, who always have at least new current conditions to report on,\n", "we will only generate feed entries when the Point Atkinson water level is forecast to\n", "exceed the risk and extreme risk thresholds\n", "\n", "We'll use the [`feedgen` package](http://lkiesow.github.io/python-feedgen/)\n", "to construct the feed and its entries and store them to disk." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from pprint import pprint\n", "\n", "import arrow\n", "from feedgen.feed import FeedGenerator" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(\"\\n\"\n", " '\\n'\n", " ' '\n", " 'tag:salishsea.eos.ubc.ca,2015-12-12:/storm-surge/atom/pmv/20151222160527\\n'\n", " ' Salish Sea NEMO Model Storm Surge Alerts for Port Metro '\n", " 'Vancouver\\n'\n", " ' 2015-12-22T16:05:27.801607+00:00\\n'\n", " ' \\n'\n", " ' Salish Sea MEOPAR Project\\n'\n", " ' http://salishsea.eos.ubc.ca/\\n'\n", " ' \\n'\n", " ' \\n'\n", " ' \\n'\n", " ' python-feedgen\\n'\n", " ' Copyright 2015, Salish Sea MEOPAR Project Contributors and The '\n", " 'University of British Columbia\\n'\n", " '\\n')\n" ] } ], "source": [ "fg = FeedGenerator()\n", "\n", "utcnow = arrow.utcnow()\n", "\n", "fg.title('Salish Sea NEMO Model Storm Surge Alerts for Port Metro Vancouver')\n", "fg.id(\n", " 'tag:salishsea.eos.ubc.ca,2015-12-12:/storm-surge/atom/pmv/{utcnow}'\n", " .format(utcnow=utcnow.format('YYYYMMDDHHmmss')))\n", "fg.language('en-ca')\n", "fg.author(name='Salish Sea MEOPAR Project', uri='http://salishsea.eos.ubc.ca/')\n", "fg.rights(\n", " 'Copyright {this_year}, Salish Sea MEOPAR Project Contributors and The University of British Columbia'\n", " .format(this_year=utcnow.year))\n", "fg.link(href='http://salishsea.eos.ubc.ca/storm-surge/atom/pmv.xml', rel='self', type='application/atom+xml')\n", "fg.link(href='http://salishsea.eos.ubc.ca/storm-surge/forecast.html', rel='related', type='text/html')\n", "\n", "pprint(fg.atom_str(pretty=True).decode('ascii'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Instead of pretty-printing the ASCII version of the feed,\n", "the production code will save it as binary data in a file with:\n", "```\n", "fg.atom_file(os.path.join(path_to_storm_surge, 'atom', 'pmv.xml')\n", "```\n", "\n", "The only really interesting bit in the code above is the calculation of the `id` element for the feed.\n", "Quoting [Mark Pilgrim's blog post](http://web.archive.org/web/20110514113830/http://diveintomark.org/archives/2004/05/28/howto-atom-id):\n", "\n", "> There are three requirements for an Atom ID:\n", ">\n", "> 1. The ID must be a valid URI, as defined by RFC 2396.\n", "> 2. The ID must be globally unique, across all Atom feeds, everywhere, for all time. This part is actually easier than it sounds.\n", "> 3. The ID must never, ever change.\n", "\n", "We use the [tag URI](http://web.archive.org/web/20110514113830/http://www.taguri.org/)\n", "technique to create our `id` with the EC tactic of appending the UTC feed create date/time\n", "as a string of digits:\n", "\n", "`tag:salishsea.eos.ubc.ca,2015-12-12:/storm-surge/atom/pmv/20151221020629`\n", "\n", "This tag is composed of:\n", "* Our domain name: `salishsea.eos.ubc.ca`\n", "* A comma\n", "* The date on which this scheme was conceived: `2015-12-12`\n", "* A colon\n", "* The path to the feed file, excluding the `.xml` extension: `/storm-surge/atom/pmv`\n", "* The UTC date/time when the feed was created as a string of digits, prepended with a '/': `/20151221020629`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Generating a PMV Feed Entry\n", "\n", "The feed above will be generated after every forecast and forecast2 run\n", "by the `make_feeds` worker between the successful completion of the \n", "`make_site_page forecast publish` worker and the launch of the `push_to_web` worker.\n", "\n", "If the Point Atkinson water level is forecast to exceed the risk and extreme risk thresholds,\n", "the `make_feeds` worker will also add an entry to the feed.\n", "Apart from the calculation of the alert text for the entry,\n", "its creation looks like:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(\"\\n\"\n", " '\\n'\n", " ' '\n", " 'tag:salishsea.eos.ubc.ca,2015-12-12:/storm-surge/atom/pmv/20151222160527\\n'\n", " ' Salish Sea NEMO Model Storm Surge Alerts for Port Metro '\n", " 'Vancouver\\n'\n", " ' 2015-12-22T16:05:27.801607+00:00\\n'\n", " ' \\n'\n", " ' Salish Sea MEOPAR Project\\n'\n", " ' http://salishsea.eos.ubc.ca/\\n'\n", " ' \\n'\n", " ' \\n'\n", " ' \\n'\n", " ' python-feedgen\\n'\n", " ' Copyright 2015, Salish Sea MEOPAR Project Contributors and The '\n", " 'University of British Columbia\\n'\n", " ' \\n'\n", " ' '\n", " 'tag:salishsea.eos.ubc.ca,2015-12-22:/storm-surge/atom/pmv/20151222080553\\n'\n", " ' Storm Surge Alert for Point Atkinson\\n'\n", " ' 2015-12-22T16:05:53.366853+00:00\\n'\n", " ' \\n'\n", " ' Salish Sea MEOPAR Project\\n'\n", " ' http://salishsea.eos.ubc.ca/\\n'\n", " ' \\n'\n", " ' \\n'\n", " ' \\n'\n", " ' \\n'\n", " '\\n')\n" ] } ], "source": [ "fe = fg.add_entry()\n", "\n", "now = arrow.now()\n", "\n", "fe.title('Storm Surge Alert for Point Atkinson')\n", "fe.id(\n", " 'tag:salishsea.eos.ubc.ca,{today}:/storm-surge/atom/pmv/{now}'\n", " .format(\n", " today=now.format('YYYY-MM-DD'),\n", " now=now.format('YYYYMMDDHHmmss')))\n", "fe.author(name='Salish Sea MEOPAR Project', uri='http://salishsea.eos.ubc.ca/')\n", "fe.content('', type='html')\n", "fe.link(href='salishsea.eos.ubc.ca/nemo/results/forecast/publish_20dec15.html', rel='alternate', type='text/html')\n", "\n", "pprint(fg.atom_str(pretty=True).decode('ascii'))" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "Each entry also requires a unique `id`.\n", "Here we use a similar algorithm to the feed `id` except that the date/time is local rather than UTC:\n", "\n", "`tag:salishsea.eos.ubc.ca,2015-12-20:/storm-surge/atom/pmv/20151220180702`\n", "\n", "This tag is composed of:\n", "* Our domain name: `salishsea.eos.ubc.ca`\n", "* A comma\n", "* The date on which the entry was created: `2015-12-20`\n", "* A colon\n", "* The path to the feed file, excluding the `.xml` extension: `/storm-surge/atom/pmv`\n", "* The local date/time when the entry was created as a string of digits, prepended with a '/': `/20151220180702`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Calculation of the Alert Text for the Entry `summary` Element\n", "\n", "The `summary` element of the feed entry for a forecast for which there is\n", "a water level alert condition will be something like:\n", "```\n", "STORM SURGE ADVISORY!\n", "Extreme sea levels expected for the marine areas of Vancouver\n", "\n", "Synopsis:\n", "Strong winds over the northeast Pacific Ocean are expected \n", "to produce elevated sea levels near Vancouver early Sunday morning.\n", "These elevated sea levels may present a flood risk to \n", "coastal structures and communities at high tide.\n", "\n", "Point Atkinson\n", "Maximum Water Level: 5.37 m above chart datum\n", "Wind: 33.67 km/hr (18.18 knots) from the W (276°)\n", "Time: Dec 13, 2015 07:22 [PST]\n", "\n", "Wind speed and direction are average over the 4 hours preceding\n", "the maximum water level to give information regarding wave setup\n", "that may augment flood risks.\n", "```\n", "\n", "Things that need to be calculated for this message:\n", "* The risk level: risk or extreme risk to reflect the words used in the \n", "storm surge forecast page graphic\n", "* Maximum water level above chart datum in metres\n", "* Average wind speed in km/hr and knots over the 4 hours preceding the maximum water level\n", "* Average wind direction as a compass point (e.g. SW) and a bearing in degrees that the wind is coming from\n", "* Date and time of the maximum water level with the appropriate timezone indicated\n", "\n", "The `summary` element can be either plain text for HTML.\n", "We probably need to use the latter to get formatting like above.\n", "That opens the possibility of font faces (bold, italic, ...) \n", "and inclusion of a link to the appropriate forecast page,\n", "although the `rel=\"alternate\"` `link` element may provide that.\n", "\n", "Most of the code that we need to calculate that text is available in the `SalishSeaNowcast` package,\n", "in particular,\n", "in the `nowcast.figures` module.\n", "There is also a Mako template that generates RST in `SalishSeaNowcast/nowcast/www/templates/surgetext.mako`." ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import datetime\n", "import os\n", "\n", "import netCDF4 as nc\n", "import numpy as np\n", "\n", "from salishsea_tools import (\n", " nc_tools,\n", " stormtools,\n", " unit_conversions,\n", " wind_tools,\n", ")\n", "from salishsea_tools.places import PLACES\n", "\n", "from nowcast import figures" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The next cell mounts the `/results` filesystem on `skookum` locally.\n", "It is intended for use if when this notebook is run on a laptop or other\n", "non-Waterhole machine that has `sshfs` installed.\n", "Don't execute the cell if that doesn't describe your situation." ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": false }, "outputs": [], "source": [ "!sshfs skookum:/results results" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Calculating the Values to use in the Template\n", "\n", "The `nowcast.figures.plot_threshold_website()` has code\n", "(reproduced below) that calculates:\n", "* The maximum sea surface height in a `grid_T` run results file\n", "* The time at which it occurs" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": false }, "outputs": [], "source": [ "site_name = 'Point Atkinson'\n", "grid_T_15m = nc.Dataset('results/SalishSea/forecast/20dec15/{}.nc'.format(site_name.replace(' ', '')))\n", "tidal_predictions = 'results/nowcast-sys/tools/SalishSeaNowcast/nowcast/tidal_predictions/'\n", "weather_path = 'results/forcing/atmospheric/GEM2.5/operational/fcst'\n", "\n", "ssh_model, t_model = figures.load_model_ssh(grid_T_15m)\n", "ttide = figures.get_tides(site_name, tidal_predictions)\n", "ssh_corr = figures.correct_model_ssh(ssh_model, t_model, ttide)\n", "residual = figures.compute_residual(ssh_corr, t_model, ttide)\n", "max_ssh, _, max_ssh_time, _, max_ssh_wind, i_max_ssh_wind = figures.get_maxes(\n", " ssh_corr, t_model, residual,\n", " figures.SITES[site_name]['lon'], figures.SITES[site_name]['lat'], \n", " weather_path)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "During development of the `nowcast.workers.make_feeds()` worker\n", "the above code was refactored to:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": false }, "outputs": [], "source": [ "site_name = 'Point Atkinson'\n", "grid_T_15m = nc.Dataset('results/SalishSea/forecast/20dec15/{}.nc'.format(site_name.replace(' ', '')))\n", "tidal_predictions = 'results/nowcast-sys/tools/SalishSeaNowcast/nowcast/tidal_predictions/'\n", "\n", "ssh_model, t_model = nc_tools.ssh_timeseries_at_point(grid_T_15m, 0, 0, datetimes=True)\n", "ttide = figures.get_tides(site_name, tidal_predictions)\n", "ssh_corr = figures.correct_model_ssh(ssh_model, t_model, ttide)\n", "max_ssh = np.max(ssh_corr) + figures.SITES[site_name]['msl']\n", "max_ssh_time = t_model[np.argmax(ssh_corr)]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "From those results we can calculate:\n", "* Maximum water level above chart datum in metres\n", "* Date and time of the maximum water level with the appropriate timezone indicated" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "(4.997162480377197, )" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "max_ssh, arrow.get(max_ssh_time).to('local')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Formating the date/time would be easy if it weren't for adding the timezone name:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "'Mon Dec 21, 2015 13:07 [PST]'" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "a = arrow.get(max_ssh_time).to('local')\n", "'{datetime} [{tzname}]'.format(datetime=a.format('ddd MMM DD, YYYY HH:mm'), tzname=a.tzinfo.tzname(a.datetime))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We also want to \"humanize\" the date/time into a phrase like \"early Monday afternoon\".\n", "There is code in the \n", "[surge_warning notebook](http://nbviewer.ipython.org/urls/bitbucket.org/salishsea/tools/raw/tip/SalishSeaNowcast/nowcast/notebooks/surge_warning.ipynb)\n", "that forms the basis for a function to do that." ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def humanize_time_of_day(date_time):\n", " day_of_week = date_time.format('dddd')\n", " if date_time.hour < 12:\n", " part_of_day = 'morning'\n", " early_late = 'early' if date_time.hour < 8 else 'late'\n", " elif date_time.hour >= 12 and date_time.hour < 17:\n", " part_of_day = 'afternoon'\n", " early_late = 'early' if date_time.hour < 15 else 'late'\n", " else:\n", " part_of_day = 'evening'\n", " early_late = 'early' if date_time.hour < 20 else 'late'\n", " return ' '.join((early_late, day_of_week, part_of_day))" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "'early Monday afternoon'" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "humanize_time_of_day(a)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "During development of the `nowcast.workers.make_feeds()` worker\n", "a slightly different version of that function that uses the same\n", "time of day descriptions as Environment Canada does in the public\n", "weather forecasts was implemented as\n", "`salishsea_tools.unit_conversions.humanize_time_of_day()`:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "'early Monday afternoon'" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "unit_conversions.humanize_time_of_day(a)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Some code adapted from `nowcast.figures.plot_threshold_map()`\n", "provides the risk level:" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def storm_surge_risk_level(max_ssh, site_msl, max_historic_ssh, ttide):\n", " max_tide_ssh = max(ttide.pred_all) + site_msl\n", " extreme_threshold = max_tide_ssh + (max_historic_ssh - max_tide_ssh) / 2\n", "\n", " risk_level = (\n", " None if max_ssh < max_tide_ssh\n", " else 'extreme risk' if max_ssh > extreme_threshold\n", " else 'moderate risk')\n", " return risk_level" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The above function has been added to the `stormtools` module." ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "moderate risk\n" ] } ], "source": [ "risk_level = stormtools.storm_surge_risk_level(site_name, max_ssh, ttide)\n", "print(risk_level)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There is code in `nowcast.figures.get_model_winds()` that finds the wind component arrays\n", "at the grid point in the weather forcing dataset at the lat-lon-time point that is closest\n", "to the tide gauge site at the time of the maximum water level:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "collapsed": true }, "outputs": [], "source": [ "weather_path = 'results/forcing/atmospheric/GEM2.5/operational/fcst'\n", "weather = nc.Dataset(os.path.join(weather_path, '{:ops_y%Ym%md%d.nc}'.format(max_ssh_time)))\n", "weather_lats = weather.variables['nav_lat'][:]\n", "weather_lons = weather.variables['nav_lon'][:] - 360\n", "\n", "j, i = figures.find_model_point(\n", " figures.SITES[site_name]['lon'],\n", " figures.SITES[site_name]['lat'],\n", " weather_lons, weather_lats)\n", "u_wind = weather.variables['u_wind'][:, j, i]\n", "v_wind = weather.variables['v_wind'][:, j, i]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Since we regularly need to get wind component values at the weather grid\n", "points closest to tide gauge stations,\n", "we'll put the grid indices in the `salishsea_tools.places.PLACES` data structure." ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[146] [155]\n", "(146, 155)\n" ] } ], "source": [ "print(j, i)\n", "print(PLACES[site_name]['wind grid ji'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Having done that we can use the `salishsea_tools.nc_tools.uv_wind_timeseries_at_point()`\n", "function to get the wind component time series:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "collapsed": false }, "outputs": [], "source": [ "wind = nc_tools.uv_wind_timeseries_at_point(weather, *PLACES[site_name]['wind grid ji'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`nowcast.figures.get_maxes()` above gave us `i_max_ssh_wind`, the index in the wind component arrays \n", "of the hour in which the maximum water level occurs." ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "collapsed": true }, "outputs": [], "source": [ "i_max_ssh_wind = np.asscalar(i_max_ssh_wind)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As a result of the development refactoring we're no longer using\n", "`nowcast.figures.get_maxes()` to provide `i_max_ssh_wind`.\n", "Instead we'll just grab the code that calculates it:" ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "21\n" ] } ], "source": [ "i_max_ssh_wind = np.asscalar(\n", " np.where(\n", " wind.time == arrow.get(\n", " max_ssh_time.year, max_ssh_time.month, max_ssh_time.day, max_ssh_time.hour))[0])\n", "print(i_max_ssh_wind)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We use that to calculate the average wind components in the 4 hours preceding that hour:" ] }, { "cell_type": "code", "execution_count": 20, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "(-4.4057636, -0.36821717)" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "u_wind_4h_avg = np.mean(u_wind[i_max_ssh_wind-4:i_max_ssh_windax_ssh_wind])\n", "v_wind_4h_avg = np.mean(v_wind[i_max_ssh_wind-4:i_max_ssh_wind])\n", "\n", "u_wind_4h_avg, v_wind_4h_avg" ] }, { "cell_type": "code", "execution_count": 23, "metadata": { "collapsed": true }, "outputs": [], "source": [ "# Calculating speed and direction arrays from components values or arrays\n", "# should probably be a library function in the planned `salishsea_tools.wind_tools` module\n", "\n", "def wind_speed_dir(u_wind, v_wind):\n", " wind_speed = np.sqrt(u_wind**2 + v_wind**2)\n", " wind_dir = np.arctan2(v_wind, u_wind)\n", " wind_dir = np.rad2deg(wind_dir + (wind_dir < 0) * 2 * np.pi)\n", " return wind_speed, wind_dir" ] }, { "cell_type": "code", "execution_count": 24, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "(4.4211239536471059, 184.77746642865748)" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wind_speed_4h_avg, wind_dir_4h_avg = wind_speed_dir(u_wind_4h_avg, v_wind_4h_avg)\n", "\n", "wind_speed_4h_avg, wind_dir_4h_avg" ] }, { "cell_type": "code", "execution_count": 22, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "(4.4211239536471059, 184.77746642865748)" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wind_speed_4h_avg, wind_dir_4h_avg = wind_tools.wind_speed_dir(u_wind_4h_avg, v_wind_4h_avg)\n", "\n", "wind_speed_4h_avg, wind_dir_4h_avg" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "Conversion of the wind speed from m/s to km/hr and knots is easily\n", "accomplished with some conversion factors and functions that should be\n", "added to a module in the `SalishSeaTools` package:" ] }, { "cell_type": "code", "execution_count": 25, "metadata": { "collapsed": true }, "outputs": [], "source": [ "M_PER_S__KM_PER_HR = 3600 / 1000\n", "M_PER_S__KNOTS = 3600 / 1852\n", "\n", "def mps_kph(m_per_s):\n", " return m_per_s * M_PER_S__KM_PER_HR\n", "\n", "def mps_knots(m_per_s):\n", " return m_per_s * M_PER_S__KNOTS" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Those conversion factors and functions are implemented in the `salishsea_tools.unit_conversions` module." ] }, { "cell_type": "code", "execution_count": 26, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "(15.916046233129581, 8.5939774476941579)" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "unit_conversions.mps_kph(wind_speed_4h_avg), unit_conversions.mps_knots(wind_speed_4h_avg)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We need a function that converts wind bearings from\n", "the \"wind to\" orientation that physicists use to the\n", "\"wind from\" orientation that people are used to:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def wind_to_from(wind_to):\n", " return 270 - wind_to if wind_to <= 270 else 270 - wind_to + 360" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This function is also now implemented as `salishsea_tools.unit_converstions.wind_to_from()`." ] }, { "cell_type": "code", "execution_count": 27, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "85.222533571342524" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "unit_conversions.wind_to_from(wind_dir_4h_avg)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We also need a function that converts compass bearings to compass headings:" ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def bearing_heading(\n", " bearing, \n", " headings=['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW', 'N'],\n", "):\n", " return headings[int(round(bearing * (len(headings) - 1) / 360, 0))]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This function is available as `salishsea_tools.unit_conversions.bearing_heading()`:" ] }, { "cell_type": "code", "execution_count": 29, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "'E'" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "unit_conversions.bearing_heading(unit_conversions.wind_to_from(wind_dir_4h_avg))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Rendering the Template for the `summary` Element\n", "\n", "We'll start with a reStructuredText template based on `SalishSeaNowcast/nowcast/www/templates/surgetext.mako`:" ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import docutils.core\n", "import IPython.display\n", "import mako.template" ] }, { "cell_type": "code", "execution_count": 25, "metadata": { "collapsed": true }, "outputs": [], "source": [ "template = \"\"\"\n", "**STORM SURGE ADVISORY**\n", "\n", "${'Extreme' if 'extreme' in conditions[tide_gauge_stn]['risk_level'] else 'Elevated'} sea levels expected for the marine areas of ${city}\n", "\n", "**Synopsis**:\n", "Strong winds over the northeast Pacific Ocean are expected \n", "to produce elevated sea levels near ${city} ${conditions[tide_gauge_stn]['humanized_max_ssh_time']}.\n", "These elevated sea levels may present a flood risk to \n", "coastal structures and communities at high tide.\n", "\n", "${tide_gauge_stn_info(tide_gauge_stn, conditions)}\n", "\n", "Wind speed and direction are averages over the 4 hours preceding\n", "the maximum water level to give information regarding wave setup\n", "that may augment flood risks.\n", "\n", "<%def name=\"tide_gauge_stn_info(stn, conditions)\">\n", "**${stn}**\n", "\n", "**Risk Level:** ${conditions[stn]['risk_level'].title()}\n", "\n", "**Maximum Water Level:** ${round(conditions[stn]['max_ssh_msl'], 1)} m above chart datum\n", "\n", "**Wind:** ${int(round(conditions[stn]['wind_speed_4h_avg_kph'], 0))} km/hr (${int(round(conditions[stn]['wind_speed_4h_avg_knots'], 0))} knots) from the ${conditions[stn]['wind_dir_4h_avg_heading']} (${int(round(conditions[stn]['wind_dir_4h_avg_bearing'], 0))}°)\n", "\n", "**Time:** ${conditions[stn]['max_ssh_time'].format('ddd MMM DD, YYYY HH:mm')} [${conditions[stn]['max_ssh_time_tzname']}]\n", "\n", "\"\"\"" ] }, { "cell_type": "code", "execution_count": 26, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "**STORM SURGE ADVISORY**\n", "\n", "Elevated sea levels expected for the marine areas of Vancouver\n", "\n", "**Synopsis**:\n", "Strong winds over the northeast Pacific Ocean are expected \n", "to produce elevated sea levels near Vancouver early Monday afternoon.\n", "These elevated sea levels may present a flood risk to \n", "coastal structures and communities at high tide.\n", "\n", "\n", "**Point Atkinson**\n", "\n", "**Risk Level:** Moderate Risk\n", "\n", "**Maximum Water Level:** 5.0 m above chart datum\n", "\n", "**Wind:** 16 km/hr (9 knots) from the E (85°)\n", "\n", "**Time:** Mon Dec 21, 2015 13:07 [PST]\n", "\n", "\n", "Wind speed and direction are averages over the 4 hours preceding\n", "the maximum water level to give information regarding wave setup\n", "that may augment flood risks.\n", "\n", "\n", "\n" ] } ], "source": [ "max_ssh_time_local = arrow.get(max_ssh_time).to('local')\n", "values = {\n", " 'city': 'Vancouver',\n", " 'tide_gauge_stn': 'Point Atkinson',\n", " 'conditions': {\n", " 'Point Atkinson': {\n", " 'risk_level': risk_level,\n", " 'max_ssh_msl': max_ssh_msl,\n", " 'wind_speed_4h_avg_kph': mps_kph(wind_speed_4h_avg),\n", " 'wind_speed_4h_avg_knots': mps_knots(wind_speed_4h_avg),\n", " 'wind_dir_4h_avg_heading': bearing_heading(wind_to_from(wind_dir_4h_avg)),\n", " 'wind_dir_4h_avg_bearing': wind_to_from(wind_dir_4h_avg),\n", " 'max_ssh_time': max_ssh_time_local,\n", " 'max_ssh_time_tzname': max_ssh_time_local.tzinfo.tzname(max_ssh_time_local.datetime),\n", " 'humanized_max_ssh_time': humanize_time_of_day(max_ssh_time_local),\n", " },\n", " },\n", "}\n", "rendered_rst = mako.template.Template(template).render(**values)\n", "\n", "print(rendered_rst)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Rendering the RST to HTML:" ] }, { "cell_type": "code", "execution_count": 27, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ "

STORM SURGE ADVISORY

\n", "

Elevated sea levels expected for the marine areas of Vancouver

\n", "

Synopsis:\n", "Strong winds over the northeast Pacific Ocean are expected\n", "to produce elevated sea levels near Vancouver early Monday afternoon.\n", "These elevated sea levels may present a flood risk to\n", "coastal structures and communities at high tide.

\n", "

Point Atkinson

\n", "

Risk Level: Moderate Risk

\n", "

Maximum Water Level: 5.0 m above chart datum

\n", "

Wind: 16 km/hr (9 knots) from the E (85°)

\n", "

Time: Mon Dec 21, 2015 13:07 [PST]

\n", "

Wind speed and direction are averages over the 4 hours preceding\n", "the maximum water level to give information regarding wave setup\n", "that may augment flood risks.

\n" ], "text/plain": [ "" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "parts = docutils.core.publish_parts(rendered_rst, writer_name='html')\n", "\n", "IPython.display.HTML(parts['body'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, we have everything we need to generate the feed and an entry if the model\n", "is predicting a storm surge risk:" ] }, { "cell_type": "code", "execution_count": 36, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(\"\\n\"\n", " '\\n'\n", " ' '\n", " 'tag:salishsea.eos.ubc.ca,2015-12-12:/storm-surge/atom/pmv/20151221053237\\n'\n", " ' Salish Sea NEMO Model Storm Surge Alerts for Port Metro '\n", " 'Vancouver\\n'\n", " ' 2015-12-21T05:32:37.988672+00:00\\n'\n", " ' \\n'\n", " ' Salish Sea MEOPAR Project\\n'\n", " ' http://salishsea.eos.ubc.ca/\\n'\n", " ' \\n'\n", " ' \\n'\n", " ' \\n'\n", " ' python-feedgen\\n'\n", " ' Copyright 2015, Salish Sea MEOPAR Project Contributors and The '\n", " 'University of British Columbia\\n'\n", " ' \\n'\n", " ' '\n", " 'tag:salishsea.eos.ubc.ca,2015-12-20:/storm-surge/atom/pmv/20151220213237\\n'\n", " ' Storm Surge Alert for Point Atkinson\\n'\n", " ' 2015-12-21T05:32:38.000038+00:00\\n'\n", " ' \\n'\n", " ' Salish Sea MEOPAR Project\\n'\n", " ' http://salishsea.eos.ubc.ca/\\n'\n", " ' \\n'\n", " ' <p><strong>STORM SURGE '\n", " 'ADVISORY</strong></p>\\n'\n", " '<p>Elevated sea levels expected for the marine areas of '\n", " 'Vancouver</p>\\n'\n", " '<p><strong>Synopsis</strong>:\\n'\n", " 'Strong winds over the northeast Pacific Ocean are expected\\n'\n", " 'to produce elevated sea levels near Vancouver early Monday afternoon.\\n'\n", " 'These elevated sea levels may present a flood risk to\\n'\n", " 'coastal structures and communities at high tide.</p>\\n'\n", " '<p><strong>Point Atkinson</strong></p>\\n'\n", " '<p><strong>Risk Level:</strong> Moderate Risk</p>\\n'\n", " '<p><strong>Maximum Water Level:</strong> 5.0 m above chart '\n", " 'datum</p>\\n'\n", " '<p><strong>Wind:</strong> 16 km/hr (9 knots) from the E '\n", " '(85°)</p>\\n'\n", " '<p><strong>Time:</strong> Mon Dec 21, 2015 13:07 '\n", " '[PST]</p>\\n'\n", " '<p>Wind speed and direction are averages over the 4 hours preceding\\n'\n", " 'the maximum water level to give information regarding wave setup\\n'\n", " 'that may augment flood risks.</p>\\n'\n", " '\\n'\n", " ' \\n'\n", " ' \\n'\n", " '\\n')\n" ] } ], "source": [ "fg = FeedGenerator()\n", "\n", "utcnow = arrow.utcnow()\n", "\n", "fg.title('Salish Sea NEMO Model Storm Surge Alerts for Port Metro Vancouver')\n", "fg.id(\n", " 'tag:salishsea.eos.ubc.ca,2015-12-12:/storm-surge/atom/pmv/{utcnow}'\n", " .format(utcnow=utcnow.format('YYYYMMDDHHmmss')))\n", "fg.language('en-ca')\n", "fg.author(name='Salish Sea MEOPAR Project', uri='http://salishsea.eos.ubc.ca/')\n", "fg.rights(\n", " 'Copyright {this_year}, Salish Sea MEOPAR Project Contributors and The University of British Columbia'\n", " .format(this_year=utcnow.year))\n", "fg.link(href='http://salishsea.eos.ubc.ca/storm-surge/atom/pmv.xml', rel='self', type='application/atom+xml')\n", "fg.link(href='http://salishsea.eos.ubc.ca/storm-surge/forecast.html', rel='related', type='text/html')\n", "\n", "if risk_level is not None:\n", " rendered_rst = mako.template.Template(template).render(**values)\n", " html = docutils.core.publish_parts(rendered_rst, writer_name='html')\n", " now = arrow.now()\n", " \n", " fe = fg.add_entry()\n", " fe.title('Storm Surge Alert for Point Atkinson')\n", " fe.id(\n", " 'tag:salishsea.eos.ubc.ca,{today}:/storm-surge/atom/pmv/{now}'\n", " .format(\n", " today=now.format('YYYY-MM-DD'),\n", " now=now.format('YYYYMMDDHHmmss')))\n", " fe.author(name='Salish Sea MEOPAR Project', uri='http://salishsea.eos.ubc.ca/')\n", " fe.content(html['body'], type='html')\n", " fe.link(href='http://salishsea.eos.ubc.ca/nemo/results/forecast/publish_20dec15.html', rel='alternate', type='text/html')\n", "\n", "pprint(fg.atom_str(pretty=True).decode('utf8'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To store the feed in a file instead of rendering it to a string use:\n", "```\n", "fg.atom_file('pmv.xml')\n", "```\n", "\n", "Now the code in this notebook will be used to create a `nowcast.worker` module that\n", "will generate feeds and store them in the appropriate locations after every\n", "forecast and forecast2 run." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.5.1" } }, "nbformat": 4, "nbformat_minor": 0 }