+
+ +
+

METAR main

+

Notebook +.. container:

+
.. container::
+
+.. rubric:: METAR Staion Plot with MetPy
+   :name: metar-staion-plot-with-metpy
+
+**Python-AWIPS Tutorial Notebook**
+
+.. container::
+
+
+
+
+

Objectives

+
    +
  • Use python-awips to connect to an edex server

  • +
  • Define and filter data request for METAR surface obs

  • +
  • Extract necessary data and reformat it for plotting

  • +
  • Stylize and plot METAR station data using Cartopy, Matplotlib, and +MetPy

  • +
+
+

Table of Contents

+

+
  • 1  Imports

    +
  • 2  Function: get_cloud_cover()

    +
  • 3  Initial Setup

    +
    • 3.1  Initial EDEX Connection

      +
    • 3.2  Setting Connection Location Names

      +
  • 4  Filter by Time

    +
  • 5  Use the Data!

    +
    • 5.1  Get the Data!

      +
    • 5.2  Extract all Parameters

      +
    • 5.3  Populate the Data Dictionary

      +
  • 6  Plot the Data!

    +
  • 7  See Also

    +
    • 7.1  Related Notebooks

      +
    • 7.2  Additional Documentation

      +
+
+

Imports

+

The imports below are used throughout the notebook. Note the first two +imports are coming directly from python-awips and allow us to connect to +an EDEX server, and define a timrange used for filtering the data. The +subsequent imports are for data manipulation and visualization.

+
from awips.dataaccess import DataAccessLayer
+from dynamicserialize.dstypes.com.raytheon.uf.common.time import TimeRange
+from datetime import datetime, timedelta
+import numpy as np
+import cartopy.crs as ccrs
+import cartopy.feature as cfeature
+import matplotlib.pyplot as plt
+from metpy.calc import wind_components
+from metpy.plots import StationPlot, StationPlotLayout, sky_cover
+from metpy.units import units
+
+
+

Top

+
+
+
+

Function: get_cloud_cover()

+

Returns the cloud fraction values as integer codes (0 through 8).

+
def get_cloud_cover(code):
+    if 'OVC' in code:
+        return 8
+    elif 'BKN' in code:
+        return 6
+    elif 'SCT' in code:
+        return 4
+    elif 'FEW' in code:
+        return 2
+    else:
+        return 0
+
+
+

Top

+
+
+
+

Initial Setup

+
+

Initial EDEX Connection

+

First we establish a connection to Unidata’s public EDEX server. With +that connection made, we can create a new data request +object +and set the data type to obs.

+

Then, because we’re going to uses MetPy’s +StationPlot +and +StationPlotLayout +we need to define several parameters, and then set them on the data +request object.

+
# EDEX Request
+edexServer = "edex-cloud.unidata.ucar.edu"
+DataAccessLayer.changeEDEXHost(edexServer)
+request = DataAccessLayer.newDataRequest("obs")
+
+# define desired parameters
+single_value_params = ["timeObs", "stationName", "longitude", "latitude",
+                       "temperature", "dewpoint", "windDir",
+                       "windSpeed"]
+multi_value_params = ["skyCover"]
+
+params = single_value_params + multi_value_params
+
+# set all parameters on the request
+request.setParameters(*(params))
+
+
+
+
+

Setting Connection Location Names

+

We are also going to define specific station IDs so that our plot is not +too cluttered.

+
# Define a list of station IDs to plot
+selected = ['KPDX', 'KOKC', 'KICT', 'KGLD', 'KMEM', 'KBOS', 'KMIA', 'KMOB', 'KABQ', 'KPHX', 'KTTF',
+            'KORD', 'KBIL', 'KBIS', 'KCPR', 'KLAX', 'KATL', 'KMSP', 'KSLC', 'KDFW', 'KNYC', 'KPHL',
+            'KPIT', 'KIND', 'KOLY', 'KSYR', 'KLEX', 'KCHS', 'KTLH', 'KHOU', 'KGJT', 'KLBB', 'KLSV',
+            'KGRB', 'KCLT', 'KLNK', 'KDSM', 'KBOI', 'KFSD', 'KRAP', 'KRIC', 'KJAN', 'KHSV', 'KCRW',
+            'KSAT', 'KBUY', 'K0CO', 'KZPC', 'KVIH', 'KBDG', 'KMLF', 'KELY', 'KWMC', 'KOTH', 'KCAR',
+            'KLMT', 'KRDM', 'KPDT', 'KSEA', 'KUIL', 'KEPH', 'KPUW', 'KCOE', 'KMLP', 'KPIH', 'KIDA',
+            'KMSO', 'KACV', 'KHLN', 'KBIL', 'KOLF', 'KRUT', 'KPSM', 'KJAX', 'KTPA', 'KSHV', 'KMSY',
+            'KELP', 'KRNO', 'KFAT', 'KSFO', 'KNYL', 'KBRO', 'KMRF', 'KDRT', 'KFAR', 'KBDE', 'KDLH',
+            'KHOT', 'KLBF', 'KFLG', 'KCLE', 'KUNV']
+
+# set the location names to the desired station IDs
+request.setLocationNames(*(selected))
+
+
+

Top

+
+
+
+
+

Filter by Time

+

Here we decide how much data we want to pull from EDEX. By default we’ll +request 1 hour, but that value can easily be modified by adjusting the +``timedelta(hours = 1)` <https://docs.python.org/3/library/datetime.html#timedelta-objects>`__ +in line 2. The more data we request, the longer this section will +take to run.

+
# Time range
+lastHourDateTime = datetime.utcnow() - timedelta(hours = 1)
+start = lastHourDateTime.strftime('%Y-%m-%d %H')
+beginRange = datetime.strptime( start + ":00:00", "%Y-%m-%d %H:%M:%S")
+endRange = datetime.strptime( start + ":59:59", "%Y-%m-%d %H:%M:%S")
+timerange = TimeRange(beginRange, endRange)
+
+
+

Top

+
+
+
+

Use the Data!

+
+

Get the Data!

+

Now that we have our request and TimeRange timerange objects +ready, we’re ready to get the data array from EDEX.

+
# Get response
+response = DataAccessLayer.getGeometryData(request,timerange)
+
+
+
+
+

Extract all Parameters

+

In this section we start gathering all the information we’ll need to +properly display our data. First we create an empty dictionary and array +to keep track of all data and unique station IDs. We also create a +boolean to help us only grab the first entry for skyCover related to +a station id.

+
+
<b>Note:</b>  The way the data responses are returned, we recieve many <code>skyCover</code> entries for each station ID, but we only want to keep track of the most recent one (first one returned).
+
+
+
+

After defining these variables, we are ready to start looping through +our response data. If the response is an entry of skyCover, and this +is a new station id, then set the skyCover value in the obs dictionary. +If this is not a skyCover entry, then explicitly set the timeObs +variable (because we have to manipulate it slightly), and dynamically +set all the remaining parameters.

+
# define a dictionary and array that will be populated from our for loop below
+obs = dict({params: [] for params in params})
+station_names = []
+time_title = ""
+
+# cycle through all the data in the response, in reverse order to get the most recent data first
+for ob in reversed(response):
+    avail_params = ob.getParameters()
+    #print(avail_params)
+    # if it has cloud information, we want the last of the 6 entries (most recent)
+    if "skyCover" in avail_params:
+        if i == 5:
+            # store the associated cloud cover int for the skyCover string
+            obs['skyCover'].append(get_cloud_cover(ob.getString("skyCover")))
+        i = i + 1
+    elif "stationName" in avail_params:
+        # If we already have a record for this stationName, skip
+        if ob.getString('stationName') not in station_names:
+            station_names.append(ob.getString('stationName'))
+            i = 0
+            for param in single_value_params:
+                if param in avail_params:
+                    if param == 'timeObs':
+                        obs[param].append(datetime.fromtimestamp(ob.getNumber(param)/1000.0))
+                        if time_title == "":
+                            time_title = str(ob.getDataTime())
+                    else:
+                        try:
+                            obs[param].append(ob.getNumber(param))
+                        except TypeError:
+                            obs[param].append(ob.getString(param))
+                else:
+                    obs[param].append(None)
+
+
+
+
+

Populate the Data Dictionary

+

Next grab the variables out of the obs dictionary we just populated, +attach correct units, (calculate their components, in the instance of +wind) and put them into a new dictionary that we will hand the plotting +function later.

+
data = dict()
+data['stid'] = np.array(obs['stationName'])
+data['latitude']  = np.array(obs['latitude'])
+data['longitude'] = np.array(obs['longitude'])
+data['air_temperature'] = np.array(obs['temperature'], dtype=float)* units.degC
+data['dew_point_temperature'] = np.array(obs['dewpoint'], dtype=float)* units.degC
+
+direction = np.array(obs['windDir'])
+direction[direction == -9999.0] = 'nan'
+
+u, v = wind_components(np.array(obs['windSpeed']) * units('knots'),
+                           direction * units.degree)
+data['eastward_wind'], data['northward_wind'] = u, v
+data['cloud_coverage'] = np.array(obs['skyCover'])
+
+
+

Top

+
+
+
+
+

Plot the Data!

+

Now we have all the data we need to create our plot! First we’ll assign +a projection and create our figure and axes.

+

Next, we use Cartopy to add common features (land, ocean, lakes, +borders, etc) to help give us a more contextual map of the United States +to plot the METAR stations on. We create and add a title for our figure +as well.

+

Additionally, we use MetPy’s +StationPlotLayout +to instantiate a custom layout and define all the attributes we want +displayed. We need to then set the data dictionary (containing all of +our data values) on the custom layout so it knows what to draw.

+

Finally, we display the plot!

+
proj = ccrs.LambertConformal(central_longitude=-95, central_latitude=35,
+                             standard_parallels=[35])
+# Create the figure
+fig = plt.figure(figsize=(20, 10))
+ax = fig.add_subplot(1, 1, 1, projection=proj)
+
+# Add various map elements
+ax.add_feature(cfeature.LAND)
+ax.add_feature(cfeature.OCEAN)
+ax.add_feature(cfeature.LAKES)
+ax.add_feature(cfeature.COASTLINE)
+ax.add_feature(cfeature.STATES)
+ax.add_feature(cfeature.BORDERS, linewidth=2)
+
+# Set plot bounds
+ax.set_extent((-118, -73, 23, 50))
+ax.set_title(time_title + " | METAR | " + edexServer)
+
+# Winds, temps, dewpoint, station id
+custom_layout = StationPlotLayout()
+custom_layout.add_barb('eastward_wind', 'northward_wind', units='knots')
+custom_layout.add_value('NW', 'air_temperature', fmt='.0f', units='degF', color='darkred')
+custom_layout.add_value('SW', 'dew_point_temperature', fmt='.0f', units='degF', color='darkgreen')
+custom_layout.add_symbol('C', 'cloud_coverage', sky_cover)
+
+stationplot = StationPlot(ax, data['longitude'], data['latitude'], clip_on=True,
+                          transform=ccrs.PlateCarree(), fontsize=10)
+stationplot.plot_text((2, 0), data['stid'])
+custom_layout.plot(stationplot, data)
+
+plt.show()
+
+
+../../_images/METAR_main_25_0.png +

Top

+
+
+
+

See Also

+ + +
+

Additional Documentation

+

python-awips:

+ +

datetime:

+ +

numpy:

+ +

cartopy:

+ +

matplotlib:

+ +

metpy:

+ +

Top

+
+
+
+
+
+ + +
+