Completed
Pull Request — master (#250)
by
unknown
01:51
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
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