Completed
Push — master ( 39c3e6...8fc031 )
by Ryan
01:17
created

WyomingUpperAir.to_float()   A

Complexity

Conditions 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
c 1
b 0
f 0
dl 0
loc 5
rs 9.4285
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
               r'http://weather.uwyo.edu/cgi-bin/sounding?region=naconf&TYPE=TEXT%3ALIST'
90
               r'&YEAR=2010&MONTH=12&FROM=0912&TO=0912&STNM=BOI': 'sounding_wyoming_upper.txt'}
91
92
    def __init__(self):
93
        r"""Initialize the wrapper."""
94
        self._urlopen = urlopen
95
96
    def _wrapped_urlopen(self, url):
97
        r"""Method to wrap urlopen and look to see if the request should be redirected."""
98
        from metpy.cbook import get_test_data
99
100
        filename = self.url_map.get(url)
101
102
        if filename is None:
103
            return self._urlopen(url)
104
        else:
105
            return open(get_test_data(filename, False), 'rb')
106
107
    def __enter__(self):
108
        global urlopen
109
        urlopen = self._wrapped_urlopen
110
111
    def __exit__(self, exc_type, exc_val, exc_tb):
112
        global urlopen
113
        urlopen = self._urlopen
114
115
116
class WyomingUpperAir(object):
117
    r"""Download and parse data from the University of Wyoming's upper air archive."""
118
119
    @staticmethod
120
    def get_data(time, site_id, region='naconf'):
121
        r"""Download data from the University of Wyoming's upper air archive.
122
123
        Parameters
124
        ----------
125
        time : datetime
126
            Date and time for which data should be downloaded
127
        site_id : str
128
            Site id for which data should be downloaded
129
        region : str
130
            The region in which the station resides. Defaults to `naconf`.
131
132
        Returns
133
        -------
134
        a file-like object from which to read the data
135
        """
136
        url = ('http://weather.uwyo.edu/cgi-bin/sounding?region={region}&TYPE=TEXT%3ALIST'
137
               '&YEAR={time:%Y}&MONTH={time:%m}&FROM={time:%d%H}&TO={time:%d%H}'
138
               '&STNM={stid}').format(region=region, time=time, stid=site_id)
139
        fobj = urlopen(url)
140
        data = fobj.read()
141
142
        # Since the archive text format is embedded in HTML, look for the <PRE> tags
143
        data_start = data.find(b'<PRE>')
144
        data_end = data.find(b'</PRE>', data_start)
145
146
        # Grab the stuff *between* the <PRE> tags -- 6 below is len('<PRE>\n')
147
        buf = data[data_start + 6:data_end]
148
        return BytesIO(buf.strip())
149
150
    @staticmethod
151
    def parse(fobj):
152
        r"""Parse Wyoming Upper Air Data.
153
154
        This parses the particular tabular layout of upper air data used by the University of
155
        Wyoming upper air archive.
156
157
        Parameters
158
        ----------
159
        fobj : file-like object
160
            The file-like object from which the data should be read. This needs to be set up
161
            to return bytes when read, not strings.
162
163
        Returns
164
        -------
165
        dict of information used by :func:`get_upper_air_data`
166
        """
167
        def to_float(s):
168
            # Remove all whitespace and replace empty values with NaN
169
            if not s.strip():
170
                s = 'nan'
171
            return float(s)
172
173
        # Skip the row of dashes and column names
174
        for _ in range(2):
175
            fobj.readline()
176
177
        # Parse the actual data, only grabbing the columns for pressure, T/Td, and wind
178
        unit_strs = ['degC' if u == 'C' else u
179
                     for u in fobj.readline().decode('ascii').split()]
180
181
        # Skip last header row of dashes
182
        fobj.readline()
183
184
        # Initiate lists for variables
185
        arr_data = []
186
187
        # Read all lines of data and append to lists only if there is some data
188
        for row in fobj:
189
            level = to_float(row[0:7])
190
            values = (to_float(row[14:21]), to_float(row[21:28]), to_float(row[42:49]),
191
                      to_float(row[49:56]))
192
193
            if any(np.invert(np.isnan(values))):
194
                arr_data.append((level,) + values)
195
196
        p, t, td, direc, spd = np.array(arr_data).T
197
198
        return dict(p=(p, unit_strs[0]), t=(t, unit_strs[2]), td=(td, unit_strs[3]),
199
                    wind=(direc, spd, unit_strs[7]))
200
201
202
class IAStateUpperAir(object):
203
    r"""Download and parse data from the Iowa State's upper air archive."""
204
    @staticmethod
205
    def get_data(time, site_id):
206
        r"""Download data from the Iowa State's upper air archive.
207
208
        Parameters
209
        ----------
210
        time : datetime
211
            Date and time for which data should be downloaded
212
        site_id : str
213
            Site id for which data should be downloaded
214
215
        Returns
216
        -------
217
        a file-like object from which to read the data
218
        """
219
        url = ('http://mesonet.agron.iastate.edu/json/raob.py?ts={time:%Y%m%d%H}00'
220
               '&station={stid}').format(time=time, stid=site_id)
221
222
        return urlopen(url)
223
224
    @staticmethod
225
    def parse(fobj):
226
        r"""Parse Iowa State Upper Air Data.
227
228
        This parses the JSON formatted data returned by the Iowa State upper air data archive.
229
230
        Parameters
231
        ----------
232
        fobj : file-like object
233
            The file-like object from which the data should be read. This needs to be set up
234
            to return bytes when read, not strings.
235
236
        Returns
237
        -------
238
        dict of information used by :func:`get_upper_air_data`
239
        """
240
        json_data = json.loads(fobj.read().decode('utf-8'))['profiles'][0]['profile']
241
242
        data = dict()
243
        for pt in json_data:
244
            for field in ('drct', 'dwpc', 'pres', 'sknt', 'tmpc'):
245
                data.setdefault(field, []).append(np.nan if pt[field] is None else pt[field])
246
247
        ret = dict(p=(np.array(data['pres']), 'mbar'), t=(np.array(data['tmpc']), 'degC'),
248
                   td=(np.array(data['dwpc']), 'degC'),
249
                   wind=(np.array(data['drct']), np.array(data['sknt']), 'knot'))
250
251
        return ret
252