1
|
|
|
# Copyright (c) 2016 MetPy Developers. |
2
|
|
|
# Distributed under the terms of the BSD 3-Clause License. |
3
|
|
|
# SPDX-License-Identifier: BSD-3-Clause |
4
|
|
|
|
5
|
|
|
from io import BytesIO |
6
|
|
|
import json |
7
|
|
|
try: |
8
|
|
|
from urllib.request import urlopen |
9
|
|
|
except ImportError: |
10
|
|
|
from urllib2 import urlopen |
11
|
|
|
|
12
|
|
|
import numpy as np |
13
|
|
|
|
14
|
|
|
from ..calc import get_wind_components |
15
|
|
|
from .cdm import Dataset |
16
|
|
|
from .tools import UnitLinker |
17
|
|
|
from ..package_tools import Exporter |
18
|
|
|
|
19
|
|
|
exporter = Exporter(globals()) |
20
|
|
|
|
21
|
|
|
|
22
|
|
|
@exporter.export |
23
|
|
|
def get_upper_air_data(time, site_id, source='wyoming', **kwargs): |
24
|
|
|
r"""Download and parse upper air observations from an online archive. |
25
|
|
|
|
26
|
|
|
Parameters |
27
|
|
|
---------- |
28
|
|
|
time : datetime |
29
|
|
|
The date and time of the desired observation. |
30
|
|
|
|
31
|
|
|
site_id : str |
32
|
|
|
The three letter ICAO identifier of the station for which data should be |
33
|
|
|
downloaded. |
34
|
|
|
|
35
|
|
|
source : str |
36
|
|
|
The archive to use as a source of data. Current can be one of 'wyoming' or 'iastate'. |
37
|
|
|
Defaults to 'wyoming'. |
38
|
|
|
|
39
|
|
|
kwargs |
40
|
|
|
Arbitrary keyword arguments to use to initialize source |
41
|
|
|
|
42
|
|
|
Returns |
43
|
|
|
------- |
44
|
|
|
:class:`metpy.io.cdm.Dataset` containing the data |
45
|
|
|
""" |
46
|
|
|
sources = dict(wyoming=WyomingUpperAir, iastate=IAStateUpperAir) |
47
|
|
|
src = sources.get(source) |
48
|
|
|
if src is None: |
49
|
|
|
raise ValueError('Unknown source for data: {0}'.format(str(source))) |
50
|
|
|
|
51
|
|
|
fobj = src.get_data(time, site_id, **kwargs) |
52
|
|
|
info = src.parse(fobj) |
53
|
|
|
|
54
|
|
|
ds = Dataset() |
55
|
|
|
ds.createDimension('pressure', len(info['p'][0])) |
56
|
|
|
|
57
|
|
|
# Simplify the process of creating variables that wrap the parsed arrays and can |
58
|
|
|
# return appropriate units attached to data |
59
|
|
|
def add_unit_var(name, std_name, arr, unit): |
60
|
|
|
var = ds.createVariable(name, arr.dtype, ('pressure',), wrap_array=arr) |
61
|
|
|
var.standard_name = std_name |
62
|
|
|
var.units = unit |
63
|
|
|
ds.variables[name] = UnitLinker(var) |
64
|
|
|
return var |
65
|
|
|
|
66
|
|
|
# Add variables for all the data columns |
67
|
|
|
for key, name, std_name in [('p', 'pressure', 'air_pressure'), |
68
|
|
|
('t', 'temperature', 'air_temperature'), |
69
|
|
|
('td', 'dewpoint', 'dew_point_temperature')]: |
70
|
|
|
data, units = info[key] |
71
|
|
|
add_unit_var(name, std_name, data, units) |
72
|
|
|
|
73
|
|
|
direc, spd, spd_units = info['wind'] |
74
|
|
|
u, v = get_wind_components(spd, np.deg2rad(direc)) |
75
|
|
|
add_unit_var('u_wind', 'eastward_wind', u, spd_units) |
76
|
|
|
add_unit_var('v_wind', 'northward_wind', v, spd_units) |
77
|
|
|
|
78
|
|
|
return ds |
79
|
|
|
|
80
|
|
|
|
81
|
|
|
class UseSampleData(object): |
82
|
|
|
r"""Class to temporarily point to local sample data instead of downloading.""" |
83
|
|
|
url_map = {r'http://weather.uwyo.edu/cgi-bin/sounding?region=naconf&TYPE=TEXT%3ALIST' |
84
|
|
|
r'&YEAR=1999&MONTH=05&FROM=0400&TO=0400&STNM=OUN': 'may3_sounding.txt', |
85
|
|
|
r'http://weather.uwyo.edu/cgi-bin/sounding?region=naconf&TYPE=TEXT%3ALIST' |
86
|
|
|
r'&YEAR=2013&MONTH=01&FROM=2012&TO=2012&STNM=OUN': 'sounding_data.txt', |
87
|
|
|
r'http://mesonet.agron.iastate.edu/json/raob.py?ts=201607301200' |
88
|
|
|
r'&station=KDEN': 'sounding_iastate.txt'} |
89
|
|
|
|
90
|
|
|
def __init__(self): |
91
|
|
|
r"""Initialize the wrapper.""" |
92
|
|
|
self._urlopen = urlopen |
93
|
|
|
|
94
|
|
|
def _wrapped_urlopen(self, url): |
95
|
|
|
r"""Method to wrap urlopen and look to see if the request should be redirected.""" |
96
|
|
|
from metpy.cbook import get_test_data |
97
|
|
|
|
98
|
|
|
filename = self.url_map.get(url) |
99
|
|
|
|
100
|
|
|
if filename is None: |
101
|
|
|
return self._urlopen(url) |
102
|
|
|
else: |
103
|
|
|
return open(get_test_data(filename, False), 'rb') |
104
|
|
|
|
105
|
|
|
def __enter__(self): |
106
|
|
|
global urlopen |
107
|
|
|
urlopen = self._wrapped_urlopen |
108
|
|
|
|
109
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb): |
110
|
|
|
global urlopen |
111
|
|
|
urlopen = self._urlopen |
112
|
|
|
|
113
|
|
|
|
114
|
|
|
class WyomingUpperAir(object): |
115
|
|
|
r"""Download and parse data from the University of Wyoming's upper air archive.""" |
116
|
|
|
|
117
|
|
|
@staticmethod |
118
|
|
|
def get_data(time, site_id, region='naconf'): |
119
|
|
|
r"""Download data from the University of Wyoming's upper air archive. |
120
|
|
|
|
121
|
|
|
Parameters |
122
|
|
|
---------- |
123
|
|
|
time : datetime |
124
|
|
|
Date and time for which data should be downloaded |
125
|
|
|
site_id : str |
126
|
|
|
Site id for which data should be downloaded |
127
|
|
|
region : str |
128
|
|
|
The region in which the station resides. Defaults to `naconf`. |
129
|
|
|
|
130
|
|
|
Returns |
131
|
|
|
------- |
132
|
|
|
a file-like object from which to read the data |
133
|
|
|
""" |
134
|
|
|
url = ('http://weather.uwyo.edu/cgi-bin/sounding?region={region}&TYPE=TEXT%3ALIST' |
135
|
|
|
'&YEAR={time:%Y}&MONTH={time:%m}&FROM={time:%d%H}&TO={time:%d%H}' |
136
|
|
|
'&STNM={stid}').format(region=region, time=time, stid=site_id) |
137
|
|
|
fobj = urlopen(url) |
138
|
|
|
data = fobj.read() |
139
|
|
|
|
140
|
|
|
# Since the archive text format is embedded in HTML, look for the <PRE> tags |
141
|
|
|
data_start = data.find(b'<PRE>') |
142
|
|
|
data_end = data.find(b'</PRE>', data_start) |
143
|
|
|
|
144
|
|
|
# Grab the stuff *between* the <PRE> tags -- 6 below is len('<PRE>\n') |
145
|
|
|
buf = data[data_start + 6:data_end] |
146
|
|
|
return BytesIO(buf.strip()) |
147
|
|
|
|
148
|
|
|
@staticmethod |
149
|
|
|
def parse(fobj): |
150
|
|
|
r"""Parse Wyoming Upper Air Data. |
151
|
|
|
|
152
|
|
|
This parses the particular tabular layout of upper air data used by the University of |
153
|
|
|
Wyoming upper air archive. |
154
|
|
|
|
155
|
|
|
Parameters |
156
|
|
|
---------- |
157
|
|
|
fobj : file-like object |
158
|
|
|
The file-like object from which the data should be read. This needs to be set up |
159
|
|
|
to return bytes when read, not strings. |
160
|
|
|
|
161
|
|
|
Returns |
162
|
|
|
------- |
163
|
|
|
dict of information used by :func:`get_upper_air_data` |
164
|
|
|
""" |
165
|
|
|
def to_float(s): |
166
|
|
|
# Remove all whitespace and replace empty values with NaN |
167
|
|
|
if not s.strip(): |
168
|
|
|
s = 'nan' |
169
|
|
|
return float(s) |
170
|
|
|
|
171
|
|
|
# Skip the row of dashes and column names |
172
|
|
|
for _ in range(2): |
173
|
|
|
fobj.readline() |
174
|
|
|
|
175
|
|
|
# Parse the actual data, only grabbing the columns for pressure, T/Td, and wind |
176
|
|
|
unit_strs = ['degC' if u == 'C' else u |
177
|
|
|
for u in fobj.readline().decode('ascii').split()] |
178
|
|
|
|
179
|
|
|
# Skip last header row of dashes |
180
|
|
|
fobj.readline() |
181
|
|
|
|
182
|
|
|
# Initiate lists for variables |
183
|
|
|
arr_data = [] |
184
|
|
|
|
185
|
|
|
# Read all lines of data and append to lists |
186
|
|
|
for row in fobj.readlines(): |
187
|
|
|
arr_data.append((to_float(row[0:7]), to_float(row[14:21]), |
188
|
|
|
to_float(row[21:28]), to_float(row[42:49]), |
189
|
|
|
to_float(row[49:56]))) |
190
|
|
|
|
191
|
|
|
p, t, td, direc, spd = np.array(arr_data).T |
192
|
|
|
|
193
|
|
|
return dict(p=(p, unit_strs[0]), t=(t, unit_strs[2]), td=(td, unit_strs[3]), |
194
|
|
|
wind=(direc, spd, unit_strs[7])) |
195
|
|
|
|
196
|
|
|
|
197
|
|
|
class IAStateUpperAir(object): |
198
|
|
|
r"""Download and parse data from the Iowa State's upper air archive.""" |
199
|
|
|
@staticmethod |
200
|
|
|
def get_data(time, site_id): |
201
|
|
|
r"""Download data from the Iowa State's upper air archive. |
202
|
|
|
|
203
|
|
|
Parameters |
204
|
|
|
---------- |
205
|
|
|
time : datetime |
206
|
|
|
Date and time for which data should be downloaded |
207
|
|
|
site_id : str |
208
|
|
|
Site id for which data should be downloaded |
209
|
|
|
|
210
|
|
|
Returns |
211
|
|
|
------- |
212
|
|
|
a file-like object from which to read the data |
213
|
|
|
""" |
214
|
|
|
url = ('http://mesonet.agron.iastate.edu/json/raob.py?ts={time:%Y%m%d%H}00' |
215
|
|
|
'&station={stid}').format(time=time, stid=site_id) |
216
|
|
|
|
217
|
|
|
return urlopen(url) |
218
|
|
|
|
219
|
|
|
@staticmethod |
220
|
|
|
def parse(fobj): |
221
|
|
|
r"""Parse Iowa State Upper Air Data. |
222
|
|
|
|
223
|
|
|
This parses the JSON formatted data returned by the Iowa State upper air data archive. |
224
|
|
|
|
225
|
|
|
Parameters |
226
|
|
|
---------- |
227
|
|
|
fobj : file-like object |
228
|
|
|
The file-like object from which the data should be read. This needs to be set up |
229
|
|
|
to return bytes when read, not strings. |
230
|
|
|
|
231
|
|
|
Returns |
232
|
|
|
------- |
233
|
|
|
dict of information used by :func:`get_upper_air_data` |
234
|
|
|
""" |
235
|
|
|
json_data = json.loads(fobj.read().decode('utf-8'))['profiles'][0]['profile'] |
236
|
|
|
|
237
|
|
|
data = dict() |
238
|
|
|
for pt in json_data: |
239
|
|
|
for field in ('drct', 'dwpc', 'pres', 'sknt', 'tmpc'): |
240
|
|
|
data.setdefault(field, []).append(np.nan if pt[field] is None else pt[field]) |
241
|
|
|
|
242
|
|
|
ret = dict(p=(np.array(data['pres']), 'mbar'), t=(np.array(data['tmpc']), 'degC'), |
243
|
|
|
td=(np.array(data['dwpc']), 'degC'), |
244
|
|
|
wind=(np.array(data['drct']), np.array(data['sknt']), 'knot')) |
245
|
|
|
|
246
|
|
|
return ret |
247
|
|
|
|