# Copyright (c) 2016,2017 MetPy Developers.
# Distributed under the terms of the BSD 3-Clause License.
# SPDX-License-Identifier: BSD-3-Clause
"""Read upper air data from remote archives."""
import contextlib
from io import BytesIO
import json
try:
from urllib.request import urlopen
except ImportError:
from urllib2 import urlopen
import numpy as np
import numpy.ma as ma
from ._tools import UnitLinker
from .cdm import Dataset
from ..calc import get_wind_components
from ..deprecation import deprecated
from ..package_tools import Exporter
exporter = Exporter(globals())
[docs]@deprecated('0.6', addendum=' This function is being moved to the Siphon package.',
pending=False)
@exporter.export
def get_upper_air_data(time, site_id, source='wyoming', **kwargs):
r"""Download and parse upper air observations from an online archive.
Parameters
----------
time : datetime
The date and time of the desired observation.
site_id : str
The three letter ICAO identifier of the station for which data should be
downloaded.
source : str
The archive to use as a source of data. Current can be one of 'wyoming' or 'iastate'.
Defaults to 'wyoming'.
kwargs
Arbitrary keyword arguments to use to initialize source
Returns
-------
:class:`metpy.io.cdm.Dataset` containing the data
.. deprecated:: 0.6.0
Function has been moved to the Siphon library and will be removed from MetPy in 0.8.0.
"""
sources = {'wyoming': WyomingUpperAir, 'iastate': IAStateUpperAir}
src = sources.get(source)
if src is None:
raise ValueError('Unknown source for data: {0}'.format(str(source)))
fobj = src.get_data(time, site_id, **kwargs)
info = src.parse(fobj)
ds = Dataset()
ds.createDimension('pressure', len(info['p'][0]))
# Simplify the process of creating variables that wrap the parsed arrays and can
# return appropriate units attached to data
def add_unit_var(name, std_name, arr, unit):
var = ds.createVariable(name, arr.dtype, ('pressure',), wrap_array=arr)
var.standard_name = std_name
var.units = unit
ds.variables[name] = UnitLinker(var)
return var
# Add variables for all the data columns
for key, name, std_name in [('p', 'pressure', 'air_pressure'),
('z', 'height', 'geopotential_height'),
('t', 'temperature', 'air_temperature'),
('td', 'dewpoint', 'dew_point_temperature')]:
data, units = info[key]
add_unit_var(name, std_name, data, units)
direc, spd, spd_units = info['wind']
u, v = get_wind_components(spd, np.deg2rad(direc))
add_unit_var('u_wind', 'eastward_wind', u, spd_units)
add_unit_var('v_wind', 'northward_wind', v, spd_units)
add_unit_var('speed', 'wind_speed', spd, spd_units)
add_unit_var('direction', 'wind_from_direction', direc, 'deg')
return ds
class UseSampleData(object):
r"""Class to temporarily point to local sample data instead of downloading."""
url_map = {r'http://weather.uwyo.edu/cgi-bin/sounding?region=naconf&TYPE=TEXT%3ALIST'
r'&YEAR=1999&MONTH=05&FROM=0400&TO=0400&STNM=OUN': 'may4_sounding.txt',
r'http://weather.uwyo.edu/cgi-bin/sounding?region=naconf&TYPE=TEXT%3ALIST'
r'&YEAR=2013&MONTH=01&FROM=2012&TO=2012&STNM=OUN': 'sounding_data.txt',
r'http://mesonet.agron.iastate.edu/json/raob.py?ts=201607301200'
r'&station=KDEN': 'sounding_iastate.txt',
r'http://weather.uwyo.edu/cgi-bin/sounding?region=naconf&TYPE=TEXT%3ALIST'
r'&YEAR=2010&MONTH=12&FROM=0912&TO=0912&STNM=BOI': 'sounding_wyoming_upper.txt',
r'http://weather.uwyo.edu/cgi-bin/sounding?region=naconf&TYPE=TEXT%3ALIST'
r'&YEAR=2016&MONTH=05&FROM=2200&TO=2200&STNM=DDC': 'may22_sounding.txt',
r'http://weather.uwyo.edu/cgi-bin/sounding?region=naconf&TYPE=TEXT%3ALIST'
r'&YEAR=2002&MONTH=11&FROM=1100&TO=1100&STNM=BNA': 'nov11_sounding.txt'}
def __init__(self):
r"""Initialize the wrapper."""
self._urlopen = urlopen
def _wrapped_urlopen(self, url):
r"""Wrap urlopen and look to see if the request should be redirected."""
from metpy.cbook import get_test_data
filename = self.url_map.get(url)
if filename is None:
return self._urlopen(url)
else:
return open(get_test_data(filename, False), 'rb')
def __enter__(self):
"""Set up our custom `urlopen` wrapper."""
global urlopen
urlopen = self._wrapped_urlopen
def __exit__(self, exc_type, exc_val, exc_tb):
"""Restore the normal `urlopen` functions."""
global urlopen
urlopen = self._urlopen
class WyomingUpperAir(object):
r"""Download and parse data from the University of Wyoming's upper air archive."""
@staticmethod
def get_data(time, site_id, region='naconf'):
r"""Download data from the University of Wyoming's upper air archive.
Parameters
----------
time : datetime
Date and time for which data should be downloaded
site_id : str
Site id for which data should be downloaded
region : str
The region in which the station resides. Defaults to `naconf`.
Returns
-------
a file-like object from which to read the data
"""
url = ('http://weather.uwyo.edu/cgi-bin/sounding?region={region}&TYPE=TEXT%3ALIST'
'&YEAR={time:%Y}&MONTH={time:%m}&FROM={time:%d%H}&TO={time:%d%H}'
'&STNM={stid}').format(region=region, time=time, stid=site_id)
with contextlib.closing(urlopen(url)) as fobj:
data = fobj.read()
# See if the return is valid, but has no data
if data.find(b'Can\'t') != -1:
raise ValueError('No data available for {time:%Y-%m-%d %HZ} from region {region} '
'for station {stid}.'.format(time=time, region=region,
stid=site_id))
# Since the archive text format is embedded in HTML, look for the <PRE> tags
data_start = data.find(b'<PRE>')
data_end = data.find(b'</PRE>', data_start)
# Grab the stuff *between* the <PRE> tags -- 6 below is len('<PRE>\n')
buf = data[data_start + 6:data_end]
return BytesIO(buf.strip())
@staticmethod
def parse(fobj):
r"""Parse Wyoming Upper Air Data.
This parses the particular tabular layout of upper air data used by the University of
Wyoming upper air archive.
Parameters
----------
fobj : file-like object
The file-like object from which the data should be read. This needs to be set up
to return bytes when read, not strings.
Returns
-------
dict of information used by :func:`get_upper_air_data`
"""
def to_float(s):
# Remove all whitespace and replace empty values with NaN
if not s.strip():
s = 'nan'
return float(s)
# Skip the row of dashes and column names
for _ in range(2):
fobj.readline()
# Parse the actual data, only grabbing the columns for pressure, T/Td, and wind
unit_strs = ['degC' if u == 'C' else u
for u in fobj.readline().decode('ascii').split()]
# Skip last header row of dashes
fobj.readline()
# Initiate lists for variables
arr_data = []
# Read all lines of data and append to lists only if there is some data
for row in fobj:
level = to_float(row[0:7])
values = (to_float(row[7:14]), to_float(row[14:21]), to_float(row[21:28]),
to_float(row[42:49]), to_float(row[49:56]))
if any(np.invert(np.isnan(values[1:]))):
arr_data.append((level,) + values)
p, z, t, td, direc, spd = np.array(arr_data).T
return {'p': (p, unit_strs[0]), 'z': (z, unit_strs[1]), 't': (t, unit_strs[2]),
'td': (td, unit_strs[3]), 'wind': (direc, spd, unit_strs[7])}
class IAStateUpperAir(object):
r"""Download and parse data from the Iowa State's upper air archive."""
@staticmethod
def get_data(time, site_id):
r"""Download data from the Iowa State's upper air archive.
Parameters
----------
time : datetime
Date and time for which data should be downloaded
site_id : str
Site id for which data should be downloaded
Returns
-------
list of json data
"""
url = ('http://mesonet.agron.iastate.edu/json/raob.py?ts={time:%Y%m%d%H}00'
'&station={stid}').format(time=time, stid=site_id)
with contextlib.closing(urlopen(url)) as fobj:
json_data = json.loads(fobj.read().decode('utf-8'))['profiles'][0]['profile']
# See if the return is valid, but has no data
if not json_data:
raise ValueError('No data available for {time:%Y-%m-%d %HZ} '
'for station {stid}.'.format(time=time, stid=site_id))
return json_data
@staticmethod
def parse(json_data):
r"""Parse Iowa State Upper Air Data.
This parses the JSON formatted data returned by the Iowa State upper air data archive.
Parameters
----------
json_data : list
list containing json data
Returns
-------
dict of information used by :func:`get_upper_air_data`
"""
data = {}
for pt in json_data:
for field in ('drct', 'dwpc', 'hght', 'pres', 'sknt', 'tmpc'):
data.setdefault(field, []).append(np.nan if pt[field] is None else pt[field])
# Make sure that the first entry has a valid temperature and dewpoint
idx = np.argmax(~(np.isnan(data['tmpc']) | np.isnan(data['tmpc'])))
ret = {'p': (ma.masked_invalid(data['pres'][idx:]), 'mbar'),
'z': (ma.masked_invalid(data['hght'][idx:]), 'meter'),
't': (ma.masked_invalid(data['tmpc'][idx:]), 'degC'),
'td': (ma.masked_invalid(data['dwpc'][idx:]), 'degC'),
'wind': (ma.masked_invalid(data['drct'][idx:]),
ma.masked_invalid(data['sknt'][idx:]), 'knot')}
return ret