-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ENH: Add a reader for nexrad level2 files #147
Changes from 47 commits
c9d88ec
f7e42d2
34b52b0
9330b17
fbd942c
3f8f8cf
e042f1f
0c2d0dc
4f1d06c
1af4649
2e37130
7ae79bd
ec2261c
568e65b
0a56163
a904c03
14aa01c
888fb1b
44647ac
2b8d9b9
0550e2c
78fada3
cb94350
c3824d6
5e0da73
d16e2e3
3a0a76f
62db259
2edb91c
f0ec0e7
f1af9cd
6c7b824
f162928
9592148
045ba99
2f26565
d7d65b2
3b49bc3
9c11f0b
e1a59df
98090ad
528a16d
1b0d97b
bae3d19
aee82ed
fc03120
c33d521
5c7bd7e
091a7c0
2a1c46e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,4 +30,5 @@ jobs: | |
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} | ||
run: | | ||
python -m build | ||
python setup.py build_ext --inplace | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This wont be needed anymore. |
||
twine upload dist/* |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,4 +8,5 @@ recursive-include tests * | |
recursive-exclude * __pycache__ | ||
recursive-exclude * *.py[co] | ||
|
||
global-include *.pyx *pxd | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be removed too. |
||
recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,12 +39,14 @@ gamic = "xradar.io.backends:GamicBackendEntrypoint" | |
iris = "xradar.io.backends:IrisBackendEntrypoint" | ||
odim = "xradar.io.backends:OdimBackendEntrypoint" | ||
rainbow = "xradar.io.backends:RainbowBackendEntrypoint" | ||
nexradlevel2 = "xradar.io.backends:NexradLevel2BackendEntrypoint" | ||
|
||
[build-system] | ||
requires = [ | ||
"setuptools>=45", | ||
"wheel", | ||
"setuptools_scm[toml]>=7.0", | ||
"numpy" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
] | ||
build-backend = "setuptools.build_meta" | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from xradar.io.backends import common | ||
|
||
|
||
def test_lazy_dict(): | ||
d = common.LazyLoadDict({"key1": "value1", "key2": "value2"}) | ||
assert d["key1"] == "value1" | ||
lazy_func = lambda: 999 | ||
d.set_lazy("lazykey1", lazy_func) | ||
assert d["lazykey1"] == 999 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
#!/usr/bin/env python | ||
# Copyright (c) 2024, openradar developers. | ||
# Distributed under the MIT License. See LICENSE for more info. | ||
|
||
"""Tests for `xradar.io.nexrad_archive` module.""" | ||
|
||
import xarray as xr | ||
|
||
from xradar.io.backends import open_nexradlevel2_datatree | ||
|
||
|
||
def test_open_nexradlevel2_datatree(nexradlevel2_file): | ||
dtree = open_nexradlevel2_datatree(nexradlevel2_file) | ||
ds = dtree["sweep_0"] | ||
assert ds.attrs["instrument_name"] == "KATX" | ||
assert ds.attrs["nsweeps"] == 16 | ||
assert ds.attrs["Conventions"] == "CF/Radial instrument_parameters" | ||
assert ds["DBZH"].shape == (719, 1832) | ||
assert ds["DBZH"].dims == ("azimuth", "range") | ||
assert int(ds.sweep_number.values) == 0 | ||
|
||
|
||
def test_open_nexrad_level2_backend(nexradlevel2_file): | ||
ds = xr.open_dataset(nexradlevel2_file, engine="nexradlevel2") | ||
assert ds.attrs["instrument_name"] == "KATX" | ||
assert ds.attrs["nsweeps"] == 16 | ||
assert ds.attrs["Conventions"] == "CF/Radial instrument_parameters" | ||
assert ds["DBZH"].shape == (719, 1832) | ||
assert ds["DBZH"].dims == ("azimuth", "range") | ||
assert int(ds.sweep_number.values) == 0 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,7 +13,24 @@ | |
.. automodule:: xradar.io.export | ||
|
||
""" | ||
from .backends import * # noqa | ||
from .backends import ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can do this, but we should be sure, that we do not need any other things exported from the subpackages. At least for wradlib, I need some of the sigmet/iris defines. Sure, I can import those directly using the deep path to the definition. |
||
CfRadial1BackendEntrypoint, # noqa | ||
FurunoBackendEntrypoint, # noqa | ||
GamicBackendEntrypoint, # noqa | ||
IrisBackendEntrypoint, # noqa | ||
NexradLevel2BackendEntrypoint, # noqa | ||
OdimBackendEntrypoint, # noqa | ||
RainbowBackendEntrypoint, # noqa | ||
open_cfradial1_datatree, # noqa | ||
open_furuno_datatree, # noqa | ||
open_gamic_datatree, # noqa | ||
open_iris_datatree, # noqa | ||
open_nexradlevel2_datatree, # noqa | ||
open_odim_datatree, # noqa | ||
open_rainbow_datatree, # noqa | ||
) | ||
|
||
# noqa | ||
from .export import * # noqa | ||
|
||
__all__ = [s for s in dir() if not s.startswith("_")] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,7 @@ | |
.. automodule:: xradar.io.backends.furuno | ||
.. automodule:: xradar.io.backends.rainbow | ||
.. automodule:: xradar.io.backends.iris | ||
.. automodule:: xradar.io.backends.nexrad_level2 | ||
|
||
""" | ||
|
||
|
@@ -24,5 +25,9 @@ | |
from .iris import * # noqa | ||
from .odim import * # noqa | ||
from .rainbow import * # noqa | ||
from .nexrad_level2 import ( | ||
NexradLevel2BackendEntrypoint, # noqa | ||
open_nexradlevel2_datatree, # noqa | ||
) | ||
|
||
__all__ = [s for s in dir() if not s.startswith("_")] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why remove the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure... it is back in. thanks!! |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,10 +12,15 @@ | |
|
||
""" | ||
|
||
import bz2 | ||
import gzip | ||
import io | ||
import itertools | ||
import struct | ||
from collections import OrderedDict | ||
from collections.abc import MutableMapping | ||
|
||
import fsspec | ||
import h5netcdf | ||
import numpy as np | ||
import xarray as xr | ||
|
@@ -229,3 +234,167 @@ | |
UINT1 = {"fmt": "B", "dtype": "unit8"} | ||
UINT2 = {"fmt": "H", "dtype": "uint16"} | ||
UINT4 = {"fmt": "I", "dtype": "unint32"} | ||
|
||
|
||
def prepare_for_read(filename, storage_options={"anon": True}): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a very nice add-on. 👍 We should advertise this more prominently (eg. docs?). We can do this in follow-up PR. |
||
""" | ||
Return a file like object read for reading. | ||
|
||
Open a file for reading in binary mode with transparent decompression of | ||
Gzip and BZip2 files. The resulting file-like object should be closed. | ||
|
||
Parameters | ||
---------- | ||
filename : str or file-like object | ||
Filename or file-like object which will be opened. File-like objects | ||
will not be examined for compressed data. | ||
|
||
storage_options : dict, optional | ||
Parameters passed to the backend file-system such as Google Cloud Storage, | ||
Amazon Web Service S3. | ||
|
||
Returns | ||
------- | ||
file_like : file-like object | ||
File like object from which data can be read. | ||
|
||
""" | ||
# if a file-like object was provided, return | ||
if hasattr(filename, "read"): # file-like object | ||
return filename | ||
|
||
# look for compressed data by examining the first few bytes | ||
fh = fsspec.open(filename, mode="rb", compression="infer", **storage_options).open() | ||
magic = fh.read(3) | ||
fh.close() | ||
|
||
# If the data is still compressed, use gunzip/bz2 to uncompress the data | ||
if magic.startswith(b"\x1f\x8b"): | ||
return gzip.GzipFile(filename, "rb") | ||
|
||
if magic.startswith(b"BZh"): | ||
return bz2.BZ2File(filename, "rb") | ||
|
||
return fsspec.open( | ||
filename, mode="rb", compression="infer", **storage_options | ||
).open() | ||
|
||
|
||
def make_time_unit_str(dtobj): | ||
"""Return a time unit string from a datetime object.""" | ||
return "seconds since " + dtobj.strftime("%Y-%m-%dT%H:%M:%SZ") | ||
|
||
|
||
class LazyLoadDict(MutableMapping): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This also neat feature. We should check, if that could be used by other readers. We should open an issue when this is merged. |
||
""" | ||
A dictionary-like class supporting lazy loading of specified keys. | ||
|
||
Keys which are lazy loaded are specified using the set_lazy method. | ||
The callable object which produces the specified key is provided as the | ||
second argument to this method. This object gets called when the value | ||
of the key is loaded. After this initial call the results is cached | ||
in the traditional dictionary which is used for supplemental access to | ||
this key. | ||
|
||
Testing for keys in this dictionary using the "key in d" syntax will | ||
result in the loading of a lazy key, use "key in d.keys()" to prevent | ||
this evaluation. | ||
|
||
The comparison methods, __cmp__, __ge__, __gt__, __le__, __lt__, __ne__, | ||
nor the view methods, viewitems, viewkeys, viewvalues, are implemented. | ||
Neither is the the fromkeys method. | ||
mgrover1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
This is from Py-ART. | ||
|
||
Parameters | ||
---------- | ||
dic : dict | ||
Dictionary containing key, value pairs which will be stored and | ||
evaluated traditionally. This dictionary referenced not copied into | ||
the LazyLoadDictionary and hence changed to this dictionary may change | ||
the original. If this behavior is not desired copy dic in the | ||
initalization. | ||
|
||
Examples | ||
-------- | ||
>>> d = LazyLoadDict({'key1': 'value1', 'key2': 'value2'}) | ||
>>> d.keys() | ||
['key2', 'key1'] | ||
>>> lazy_func = lambda : 999 | ||
>>> d.set_lazy('lazykey1', lazy_func) | ||
>>> d.keys() | ||
['key2', 'key1', 'lazykey1'] | ||
>>> d['lazykey1'] | ||
999 | ||
|
||
""" | ||
|
||
def __init__(self, dic): | ||
"""initalize.""" | ||
self._dic = dic | ||
self._lazyload = {} | ||
|
||
# abstract methods | ||
def __setitem__(self, key, value): | ||
"""Set a key which will not be stored and evaluated traditionally.""" | ||
self._dic[key] = value | ||
if key in self._lazyload: | ||
del self._lazyload[key] | ||
|
||
def __getitem__(self, key): | ||
"""Get the value of a key, evaluating a lazy key if needed.""" | ||
if key in self._lazyload: | ||
value = self._lazyload[key]() | ||
self._dic[key] = value | ||
del self._lazyload[key] | ||
return self._dic[key] | ||
|
||
def __delitem__(self, key): | ||
"""Remove a lazy or traditional key from the dictionary.""" | ||
if key in self._lazyload: | ||
del self._lazyload[key] | ||
else: | ||
del self._dic[key] | ||
|
||
def __iter__(self): | ||
"""Iterate over all lazy and traditional keys.""" | ||
return itertools.chain(self._dic.copy(), self._lazyload.copy()) | ||
|
||
def __len__(self): | ||
"""Return the number of traditional and lazy keys.""" | ||
return len(self._dic) + len(self._lazyload) | ||
|
||
# additional class to mimic dict behavior | ||
def __str__(self): | ||
"""Return a string representation of the object.""" | ||
if len(self._dic) == 0 or len(self._lazyload) == 0: | ||
seperator = "" | ||
else: | ||
seperator = ", " | ||
lazy_reprs = [(repr(k), repr(v)) for k, v in self._lazyload.items()] | ||
lazy_strs = ["{}: LazyLoad({})".format(*r) for r in lazy_reprs] | ||
lazy_str = ", ".join(lazy_strs) + "}" | ||
return str(self._dic)[:-1] + seperator + lazy_str | ||
|
||
def has_key(self, key): | ||
"""True if dictionary has key, else False.""" | ||
return key in self | ||
|
||
def copy(self): | ||
""" | ||
Return a copy of the dictionary. | ||
|
||
Lazy keys are not evaluated in the original or copied dictionary. | ||
""" | ||
dic = self.__class__(self._dic.copy()) | ||
# load all lazy keys into the copy | ||
for key, value_callable in self._lazyload.items(): | ||
dic.set_lazy(key, value_callable) | ||
return dic | ||
|
||
# lazy dictionary specific methods | ||
def set_lazy(self, key, value_callable): | ||
"""Set a lazy key to load from a callable object.""" | ||
if key in self._dic: | ||
del self._dic[key] | ||
self._lazyload[key] = value_callable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIK we would need to enable jupyter notebook linting/formatting for ruff in pyproject.toml