Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds acis.py for ACIS Web Services functionality #177

Merged
merged 17 commits into from
Jan 30, 2018
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions examples/acis/Basic_Overview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""
=============================
Basic ACIS Web Services Usage
=============================

Siphon's simplewebservice support also includes the ability to query the
Regional Climate Centers' ACIS data servers. ACIS data provides daily records
for most station networks in the U.S. and is updated hourly.

In this example we will be querying the service for 20 years of temperature data
from Denver International Airport.
"""

import matplotlib.pyplot as plt

from siphon.simplewebservice.acis import acis_request

###########################################
# First, we need to assemble a dictionary containing the information we want.
# For this example we want the average temperature at Denver International (KDEN)
# from January 1, 1997 to December 1, 2017. While we could get the daily data,
# we will instead request the monthly averages, which the remote service will
# find for us.
parameters = {'sid': 'KDEN', 'sdate': '19970101', 'edate': '20171231', 'elems': [
{'name': 'avgt', 'interval': 'mly', 'duration': 'mly', 'reduce': 'mean'}]}

###########################################
# These parameters are used to specify what kind of data we want. We are
# formatting this as a dictionary, the acis_request function will handle the
# conversion of this into a JSON string for us!
#
# As we explain how this dictionary is formatted, feel free to follow along
# using the API documentation here :http://www.rcc-acis.org/docs_webservices.html
#
# The first section of the parameters dictionary is focused on the station and
# period of interest. We have a 'sid' element where the airport identifier is,
# and sdate/edate which correspond to the starting and ending dates of the
# period of interest.
#
# The 'elems' list contains individual dictionaries of elements (variables) of
# interest. In this example we are requesting the average monthly temperature.
# If we also wanted the minimum temperature, we would simply add an additional
# dictionary to the 'elems' list.
#
# Now that we have assembled our dictionary, we need to decide what type of
# request we are making. You can request meta data (information about the
# station), station data (data from an individual station), data from multiple
# stations, or even images of pre-prepared data.
#
# In this case we are interested in a single station, so we will be using the
# method set aside for this called, 'StnData'.

method = 'StnData'

###########################################
# Now that we have our request information ready, we can call the acis_request
# function and recieve our data!

myData = acis_request(method, parameters)

###########################################
# The data is also returned in a dictionary format, decoded from a JSON string.

print(myData)

###########################################
# We can see there are two parts to this data: The metadata, and the data. The
# metadata can be useful in mapping the observations (We'll do this in a later
# example).
#
# To wrap this example up, we are going to do a simple line graph of this 30
# year temperature data using MatPlotLib! Notice that the data is decoded as
# a string, so you should convert those back into numbers before use.
#
# *Note: Missing data is recorded as M!

stnName = myData['meta']['name']

avgt = []
dates = []
for obs in myData['data']:
if obs[0][-2:] == '01':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about:

If obs[0].endswith(‘01’)

?

dates.append(obs[0])
else:
dates.append('')
avgt.append(float(obs[1]))

X = list(range(len(avgt)))

plt.title(stnName)
plt.ylabel('Average Temperature (F)')
plt.plot(X, avgt)
plt.xticks(X, dates, rotation=45)

plt.show()
149 changes: 149 additions & 0 deletions examples/acis/Mapping_Example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"""
===============================
Multi Station Calls and Mapping
===============================

In this example we will be using Siphon's simplewebservice support to query
ACIS Web Services for multiple stations. We will plot precipitation
values recorded in Colorado and Wyoming during the 2013 flooding event.
"""

import cartopy.crs as ccrs
import cartopy.feature as feat
import matplotlib.pyplot as plt

from siphon.simplewebservice.acis import acis_request

###########################################
# First, we need to assemble a dictionary containing the information we want.
# In this example we want multiple station information, which indicates we
# need a MultiStnData call. Our event period spans from September 9 through
# September 12, 2013. We know we are interested in precipitation totals,
# but we are also going to take advantage of the long-term record in ACIS
# and ask it to return what the departure from normal precipitation was on
# this day.

parameters = {'state': 'co', 'sdate': '20130909', 'edate': '20130912', 'elems': [
{'name': 'pcpn', 'interval': 'dly'},
{'name': 'pcpn', 'interval': 'dly', 'normal': 'departure'}]}

method = 'MultiStnData'
###########################################
# In this case, rather than using station ID's, we are able to specify a new
# parameter called 'state'. If we were interested in other states, we could just
# add another to the list like this: 'co,wy'. Also notice how we are getting
# both the precipitation and departure from normal within one variable. We'll
# see how this changes the final data dictionary. Now let's make our call and
# review our data.

myData = acis_request(method, parameters)

print(myData)

###########################################
# MultiStnData calls take longer to return than single stations, especially when
# you request multiple states. We can see the data is divided by station, with
# each station having it's own meta and data components. This time we also have
# multiple values in each data list. Each value corresponds to the variable we
# requested, in the order we requested it. So in this case, we have the
# precipitation value, followed by the departure from normal value. Before we
# plot this information, we need to add up the precipitation sums. But rather
# than doing it in Python, let's make another ACIS call that prepares this for
# us.

parameters = {'state': 'co', 'sdate': '20130909', 'edate': '20130912', 'elems': [
{'name': 'pcpn', 'interval': 'dly', 'smry': 'sum', 'smry_only': 1},
{'name': 'pcpn', 'interval': 'dly', 'smry': 'sum', 'smry_only': 1, 'normal': 'departure'}]}

myData = acis_request(method, parameters)

print(myData)

###########################################
# First of all, we have two new components to our elements: 'smry' and 'smry_only'.
# 'smry' allows us to summarize the data over our time period. There are a few
# options for this, including being able to count the number of records exceeding
# a threshold (something we will explore in the next example). The other parameter,
# 'smry_only', allows us to only return the summary value and not the intermediate
# data.
#
# Now let's look at how our data has changed. Rather than having a just a 'meta'
# and 'data' component, we have a new one called 'smry'. As you've guessed,
# this contains our summary information (also in the order we requested it).
# By specifying 'smry_only', there is no 'data' component. If we also wanted
# all 4 days of data, we would simply remove that parameter.
#
# To wrap up this example, we will finally plot our precipitation sums and
# departures onto a map using CartoPy. To do this we will utilize
# the meta data that is provided with each station's data. Within the metadata
# is a 'll' element that contains the latitude and longitude, which is perfect
# for plotting!
#
# One final thing to note is that not all stations have location information.
# Stations from the ThreadEx network cover general areas, and thus aren't
# packaged with precise latitudes and longitudes. We will skip them by
# identifying their network ID of 9 in the ACIS metadata. Don't worry about
# lost information though! These summarize stations that already exist within
# their areas!

lat = []
lon = []
pcpn = []
pcpn_dep = []

for stn in myData['data']:
# Skip threaded stations! They have no lat/lons
if stn['meta']['sids'][-1][-1] == '9':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe

if stn[‘meta’][‘sids’][-1].endswith(‘9’)

continue
# Skip stations with missing data
if stn['smry'][0] == 'M' or stn['smry'][1] == 'M':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What comes before “M” if it’s at index 1? White space? If so, what about:

If stn[‘smry’].lstrip().startswith(‘M’)

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case 'smry' is actually a list of summary values. So in this case the 'smry' would have one value for the total precip and another for the departure from normal.

continue

lat.append(stn['meta']['ll'][1])
lon.append(stn['meta']['ll'][0])
pcpn.append(float(stn['smry'][0]))
pcpn_dep.append(float(stn['smry'][1]))
###########################################
# Now we setup our map and plot the data! We are going to plot the station
# locations with a '+' symbol and label them with the precipitation value.
# We will use the departures to set the departure from normal values where:
# * Departure < 0 is Red
# * Departure > 0 is Green
# * Departure > 2 is Magenta
#
# This should help us visualize where the precipitation event was strongest!

proj = ccrs.LambertConformal(central_longitude=-105, central_latitude=0,
standard_parallels=[35])

fig = plt.figure(figsize=(20, 10))
ax = fig.add_subplot(1, 1, 1, projection=proj)

state_boundaries = feat.NaturalEarthFeature(category='cultural',
name='admin_1_states_provinces_lines',
scale='110m', facecolor='none')

ax.add_feature(feat.LAND, zorder=-1)
ax.add_feature(feat.OCEAN, zorder=-1)
ax.add_feature(feat.LAKES, zorder=-1)
ax.coastlines(resolution='110m', zorder=2, color='black')
ax.add_feature(state_boundaries, edgecolor='black')
ax.add_feature(feat.BORDERS, linewidth=2, edgecolor='black')

# Set plot bounds
ax.set_extent((-109.9, -101.8, 36.5, 41.3))

# Plot each station, labeling based on departure
for stn in range(len(pcpn)):
if pcpn_dep[stn] >= 0 and pcpn_dep[stn] < 2:
ax.plot(lon[stn], lat[stn], 'g+', markersize=7, transform=ccrs.Geodetic())
ax.text(lon[stn], lat[stn], pcpn[stn], transform=ccrs.Geodetic())
elif pcpn_dep[stn] >= 2:
ax.plot(lon[stn], lat[stn], 'm+', markersize=7, transform=ccrs.Geodetic())
ax.text(lon[stn], lat[stn], pcpn[stn], transform=ccrs.Geodetic())
elif pcpn_dep[stn] < 0:
ax.plot(lon[stn], lat[stn], 'r+', markersize=7, transform=ccrs.Geodetic())
ax.text(lon[stn], lat[stn], pcpn[stn], transform=ccrs.Geodetic())
ax.plot(pcpn)

plt.show()
27 changes: 27 additions & 0 deletions examples/acis/readme.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.. _acis_examples:

ACIS Web Services
-----------------

Examples of using Siphon's simplewebservice support to access and use data from
the Applied Climate Information System (ACIS) Web Services API provided by the
Regional Climate Centers.

The ACIS Web Service API provides daily records for stations from the following
networks:
* WBAN
* COOP
* FAA
* WMO
* ICAO
* GHCN
* NWSLI/SHEF
* ThreadEx
* CoCoRahs
* California Multi-station Index

Data is updated hourly and documentation on the API is available at
http://www.rcc-acis.org/docs_webservices.html

A useful web tool for constructing query parameters is available here:
http://builder.rcc-acis.org/
65 changes: 65 additions & 0 deletions siphon/simplewebservice/acis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Requests data from the ACIS Web Services API."""

import requests

from ..http_util import create_http_session


def acis_request(method, params):
"""Requests data from the ACIS Web Services API.

Makes a request from the ACIS Web Services API for data
based on a given method (StnMeta,StnData,MultiStnData,GridData,General)
and parameters string. Information about the parameters can be obtained at:
http://www.rcc-acis.org/docs_webservices.html

If a connection to the API fails, then it will raise an exception. Some bad
calls will also return empty dictionaries.

ACIS Web Services is a distributed system! A call to the main URL can be
delivered to any climate center running a public instance of the service.
This makes the calls efficient, but also occasionaly results in failed
calls when a server you are directed to is having problems. Generally,
reconnecting after waiting a few seconds will resolve a problem. If problems
are persistent, contact ACIS developers at the High Plains Regional Climate
Center or Northeast Regional Climate Center who will look into server
issues.

Parameters
----------
method : str
The Web Services request method (StnMeta, StnData, MultiStnData, GridData, General)
params : dict
A JSON array of parameters (See Web Services API)

Returns
-------
A dictionary of data based on the JSON parameters

Raises
------
:class: `ACIS_API_Exception`
When the API is unable to establish a connection or returns
unparsable data.

"""
base_url = 'http://data.rcc-acis.org/' # ACIS Web API URL

timeout = 300 if method == 'MultiStnData' else 60

try:
response = create_http_session().post(base_url + method, json=params, timeout=timeout)
return response.json()
except requests.exceptions.Timeout:
raise AcisApiException('Connection Timeout')
except requests.exceptions.TooManyRedirects:
raise AcisApiException('Bad URL. Check your ACIS connection method string.')
except ValueError:
raise AcisApiException('No data returned! The ACIS parameter dictionary'
'may be incorrectly formatted')


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you kill the blank line?

class AcisApiException(Exception):
"""This class handles exceptions raised by the acis_request function."""

pass
Loading