-
Notifications
You must be signed in to change notification settings - Fork 75
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
Changes from 14 commits
795a439
e5df7d1
90d52dd
6a23567
5ef7f3a
bea518c
5a5a44c
f333710
7d00b16
1bb8b4b
43a51e3
17860de
c62483f
67227f4
fd8bee4
4dee3bc
fab1c2a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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': | ||
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() |
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': | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe
|
||
continue | ||
# Skip stations with missing data | ||
if stn['smry'][0] == 'M' or stn['smry'][1] == 'M': | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() |
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/ |
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') | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about:
?