Skip to content
This repository has been archived by the owner on Aug 11, 2022. It is now read-only.

Commit

Permalink
modularize, document API
Browse files Browse the repository at this point in the history
  • Loading branch information
scivision committed Aug 17, 2018
1 parent 576536d commit fa722d3
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 268 deletions.
122 changes: 79 additions & 43 deletions README.md
Expand Up @@ -8,14 +8,15 @@

# Nexrad Quick-plot

Easy Python download and plot NEXRAD N0Q compositive reflectivity.
Easy Python download and plot NEXRAD N0Q compositive reflectivity.
Uses RGB high resolution PNG images of North America.

Tested with `pytest`, `flake8` and `mypy` type checking.

## Install

python -m pip install -e .
```sh
python -m pip install -e .
```

## Usage

Expand All @@ -27,61 +28,96 @@ RGB data scaling: NEXRAD N0Q base reflectivity maps.

![NEXRAD N0Q RGB scaling](doc/n0q_ramp.png)

### Download NEXRAD data

Get
[NEXRAD reflectivity data](https://mesonet.agron.iastate.edu/docs/nexrad_composites/)
with parallel download:

download-nexrad start stop outdir

example: download from 2018-01-01 to 2018-01-02 to `~/data/nexrad`:

download-nexrad 2018-01-01T00 2018-01-03T00 ~/data/nexrad

### Plot NEXRAD reflectivity data
These data are reduced fidelity RGB images.
We use `xarray.DataArray` and plot image by image.
For high-fidelity science data, the lower level data are needed--contact us if interested.

```python
import nexrad_quickplot as nq

dat = nq.load('~/data/2017-08-21/nexrad/nexrad2017-08-19T00:00:00.png')

>>> dat
<xarray.DataArray (lat: 540, lon: 1220, color: 3)>
array([[[255, 255, 255],
[255, 255, 255],
...,
[255, 255, 255],
[255, 255, 255]]], dtype=uint8)
Coordinates:
* lat (lat) float64 23.0 23.05 23.1 23.15 23.2 23.25 23.3 23.35 23.4 ...
* lon (lon) float64 -126.0 -125.9 -125.9 -125.8 -125.8 -125.7 -125.7 ...
* color (color) 'R' 'G' 'B'
Attributes:
filename: ~/data/nexrad2015-01-19T01:23:00.png
wldfn: None
time: 2017-08-19 00:00:00
```

(georegistered via Cartopy)
`.lat` and `.lon` are vectors of geodetic latitude and longitude respectively, computed based on the `.wld` file corresponding to the images.

Plot all data in directory:

plot-nexrad ~/data/nexrad/
### Download NEXRAD data

Plot a specific file (subplots if multiple files specified):
Get
[NEXRAD reflectivity data](https://mesonet.agron.iastate.edu/docs/nexrad_composites/)
with parallel download:
```sh
download-nexrad start stop outdir
```

plot-nexrad ~/data/nexrad/2018-01-01T12:35:00.png
example:
download from 2018-01-01 to 2018-01-02 to `~/data/nexrad`:
```sh
download-nexrad 2018-01-01T00 2018-01-03T00 ~/data/nexrad
```

Plot via file glob match:
### Plot NEXRAD reflectivity data

plot-nexrad ~/data/nexrad/ -pat 2018-01-01T12*.png

Keogram (specify lat or lon and value):
NEXRAD QuickPlot plots are georegistered via
[Cartopy](https://pypi.org/project/Cartopy/),
which is the replacement for
[deprecated Basemap](https://www.scivision.co/cartopy-replace-deprecated-basemap/)

* Plot all data in directory:
```sh
plot-nexrad ~/data/nexrad/
```
* Plot a specific file (subplots if multiple files specified):
```sh
plot-nexrad ~/data/nexrad/2018-01-01T12:35:00.png
```
* Plot via file glob match:
```sh
plot-nexrad ~/data/nexrad/ -pat 2018-01-01T12*.png
```
* Keogram (specify lat or lon and value):
```sh
plot-nexrad ~/data/2017-08-21/nexrad/ -keo lat 40
```

plot-nexrad ~/data/2017-08-21/nexrad/ -keo lat 40
## Notes

## Coordinates
### Coordinates

EPSG:4326 coordinates (WGS84) are in .wld files, which are generally the
same for wide time spans of data. The [.wld
format](https://mesonet.agron.iastate.edu/docs/radmapserver/howto.html#toc3.3)
EPSG:4326 coordinates (WGS84) are in `.wld` files, which are generally the
same for wide time spans of data. The
[.wld format](https://mesonet.agron.iastate.edu/docs/radmapserver/howto.html#toc3.3)
is like:

0.005 (size of pixel in x direction)
0.0 (rotation of row) (Typically zero)
0.0 (rotation of column) (Typically zero)
-0.005 (size of pixel in y direction)
-126.0 (x coordinate of centre of upper left pixel in map units--here it's WGS84 longitude)
50.0 (y coordinate of centre of upper left pixel in map units--here it's WGS84 latitude)

## Notes
```
0.005 (size of pixel in x direction)
0.0 (rotation of row) (Typically zero)
0.0 (rotation of column) (Typically zero)
-0.005 (size of pixel in y direction)
-126.0 (x coordinate of centre of upper left pixel in map units--here it's WGS84 longitude)
50.0 (y coordinate of centre of upper left pixel in map units--here it's WGS84 latitude)
```


### Mass image downscaling

For initial analysis, the original Nexrad image size of 12200 x 5400
pixels may be too high to complete in a reasonable time. I choose to
downsize by a factor of 10, which takes a long time, but is a one-time
process.
For initial analysis, the original Nexrad image size of 12200 x 5400 pixels may be too high to complete in a reasonable time.
I choose to downsize by a factor of 10, which takes a long time, but is a one-time process.

```bash
mkdir orig
Expand Down
141 changes: 4 additions & 137 deletions nexrad_quickplot/__init__.py
@@ -1,141 +1,8 @@
#!/usr/bin/env python
import os
from pathlib import Path
from typing import List, Tuple
import datetime
from dateutil.parser import parse
import requests
import numpy as np
import xarray
import imageio
import functools
try:
import skimage.transform as st
except ImportError:
st = None
from datetime import datetime, timedelta
from typing import List
from .io import load, loadkeogram, download # noqa: F401


@functools.lru_cache()
def wld2mesh(fn: Path, nxy: tuple) -> np.ndarray:
"""converts .wld to lat/lon mesh for Cartopy/Matplotlib plots
assumes the .wld file is EPSG:4326 coordinates (WGS84)
"""
wld = np.loadtxt(fn)

# ny, nx = nxy
ny, nx = (5400, 12200) # FIXME has to be from original image size

lat = np.linspace(wld[5] + ny * wld[3], wld[5], nxy[0])
lon = np.linspace(wld[4], wld[4] + nx * wld[0], nxy[1])
# trailing index fixes occasional off by one due to floating point error
# lat = np.arange(wld[5]-wld[3] + ny*wld[3], wld[5]-wld[3], -wld[3])[:ny]
# lon = np.arange(wld[4], wld[4]+nx*wld[0], wld[0])[:nx]

return lat, lon


def datetimerange(start: datetime.datetime, stop: datetime.datetime, step: datetime.timedelta) -> List[datetime.datetime]:
def datetimerange(start: datetime, stop: datetime, step: timedelta) -> List[datetime]:
return [start + i * step for i in range((stop - start) // step)]


def download(t: datetime.datetime, outdir: os.PathLike, clobber: bool=False) -> Path:
"""download NEXRAD file for this time
https://mesonet.agron.iastate.edu/archive/data/2018/02/12/GIS/uscomp/n0q_201802120000.png
"""
STEM = 'https://mesonet.agron.iastate.edu/archive/data/'
outdir = Path(outdir).expanduser()

if isinstance(t, datetime.date) and not isinstance(t, datetime.datetime):
t = datetime.datetime.combine(t, datetime.time.min)

if os.name == 'nt':
fn = outdir / f"nexrad{t.isoformat().replace(':','-')}.png"
else:
fn = outdir / f"nexrad{t.isoformat()}.png"
# %%
if not clobber and fn.is_file() and fn.stat().st_size > 0: # no clobber
print(fn, 'SKIPPED', end='\r')
return fn

url: str = (f'{STEM}{t.year}/{t.month:02d}/{t.day:02d}/GIS/uscomp/n0q_'
f'{t.year}{t.month:02d}{t.day:02d}{t.hour:02d}{t.minute:02d}.png')

print(fn, end='\r')
with fn.open('wb') as f:
f.write(requests.get(url, allow_redirects=True, timeout=10).content)

return fn


def load(fn: Path, wld: Path, downsample: int=None, keo: bool=False) -> xarray.DataArray:
"""
loads and modifies NEXRAD image for plotting
"""
if not fn.is_file():
raise FileNotFoundError(f'{fn} is not a file.')

img = imageio.imread(fn)

assert img.ndim == 3 and img.shape[2] in (3, 4), 'unexpected NEXRAD image format'

if downsample is not None:
assert isinstance(downsample, int)
if st is None:
raise ImportError('you need to install scikit-image pip install skimage')
img = st.resize(img, (img.shape[0] // downsample, img.shape[1] // downsample),
mode='constant', cval=255,
preserve_range=True).astype(img.dtype)

# %% make transparent
if not keo:
img = img[..., :3]

mask = img[..., :3].all(axis=2) == 0
img[mask, :3] = 255 # make no signal be white

# %% collect output
lat, lon = wld2mesh(wld, img.shape[:2])

img = xarray.DataArray(img,
coords=[('lat', lat), ('lon', lon), ('color', ['R', 'G', 'B'])],
attrs={'filename': fn, 'wldfn': wld, 'time': parse(fn.stem[6:])})

assert img.dtype in (np.uint8, np.uint16)

return img


def keogram(flist: List[Path], llslice: Tuple[str, float], wld: Path) -> xarray.DataArray:
# %% generate slices
ilat = None
ilon = None
if llslice[0] == 'lat':
ilat = llslice[1]
elif llslice[0] == 'lon':
ilon = llslice[1]
else:
raise ValueError(f'unknown keogram slice {llslice}')

if ilat is None and ilon is None:
raise ValueError('must slice in lat or lon')

assert ilat is not None, 'FIXME: currently handling latitude cut (longitude keogram) only'
# %% setup arrays
img = load(flist[0], wld, keo=False)
coords = ('lat', img.lat) if ilon is not None else ('lon', img.lon)
time = [parse(f.stem[6:]) for f in flist]

keo = xarray.DataArray(np.empty((img.lon.size, len(flist), img.color.size), dtype=img.dtype),
coords=(coords, ('time', time), ('color', img.color)))
# %% load and stack slices
for f in flist:
print(f, end='\r')
img = load(f, wld, keo=False)
if ilat is not None:
keo.loc[:, img.time, :] = img.sel(lat=ilat, method='nearest', tolerance=0.1)
keo.attrs['lat'] = ilat
elif ilon is not None:
keo.loc[:, img.time, :] = img.sel(lon=ilon, method='nearest', tolerance=0.1)
keo.attrs['lon'] = ilon

return keo
File renamed without changes.

0 comments on commit fa722d3

Please sign in to comment.