doppy 0.5.9__cp310-abi3-macosx_10_12_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- doppy/__init__.py +6 -0
- doppy/bench.py +13 -0
- doppy/data/__init__.py +0 -0
- doppy/data/api.py +58 -0
- doppy/data/cache.py +43 -0
- doppy/data/exceptions.py +6 -0
- doppy/defaults.py +18 -0
- doppy/exceptions.py +14 -0
- doppy/netcdf.py +134 -0
- doppy/options.py +13 -0
- doppy/product/__init__.py +6 -0
- doppy/product/noise_utils.py +106 -0
- doppy/product/stare.py +807 -0
- doppy/product/stare_depol.py +308 -0
- doppy/product/turbulence.py +264 -0
- doppy/product/utils.py +12 -0
- doppy/product/wind.py +460 -0
- doppy/py.typed +0 -0
- doppy/raw/__init__.py +16 -0
- doppy/raw/halo_bg.py +173 -0
- doppy/raw/halo_hpl.py +480 -0
- doppy/raw/halo_sys_params.py +135 -0
- doppy/raw/utils.py +14 -0
- doppy/raw/windcube.py +477 -0
- doppy/raw/wls70.py +175 -0
- doppy/raw/wls77.py +163 -0
- doppy/rs.abi3.so +0 -0
- doppy/utils.py +24 -0
- doppy-0.5.9.dist-info/METADATA +144 -0
- doppy-0.5.9.dist-info/RECORD +33 -0
- doppy-0.5.9.dist-info/WHEEL +4 -0
- doppy-0.5.9.dist-info/entry_points.txt +2 -0
- doppy-0.5.9.dist-info/licenses/LICENSE +21 -0
doppy/__init__.py
ADDED
doppy/bench.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Timer:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.start: float | None = None
|
|
7
|
+
|
|
8
|
+
def __enter__(self):
|
|
9
|
+
self.start = time.time()
|
|
10
|
+
|
|
11
|
+
def __exit__(self, type, value, traceback):
|
|
12
|
+
if isinstance(self.start, float):
|
|
13
|
+
print(f"Elapsed time: {time.time() - self.start:.2f}")
|
doppy/data/__init__.py
ADDED
|
File without changes
|
doppy/data/api.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import gzip
|
|
2
|
+
import io
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
from requests.adapters import HTTPAdapter
|
|
6
|
+
from urllib3.util.retry import Retry
|
|
7
|
+
|
|
8
|
+
from doppy.data import exceptions
|
|
9
|
+
from doppy.data.cache import cached_record
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Api:
|
|
13
|
+
def __init__(self, cache: bool = False) -> None:
|
|
14
|
+
retries = Retry(total=10, backoff_factor=0.2)
|
|
15
|
+
adapter = HTTPAdapter(max_retries=retries)
|
|
16
|
+
session = requests.Session()
|
|
17
|
+
session.mount("https://", adapter)
|
|
18
|
+
session.mount("http://", adapter)
|
|
19
|
+
self.session = session
|
|
20
|
+
self.api_endpoint = "https://cloudnet.fmi.fi/api"
|
|
21
|
+
self.cache = cache
|
|
22
|
+
|
|
23
|
+
def get(self, path: str, params: dict[str, str | list[str]]) -> list:
|
|
24
|
+
res = self.session.get(
|
|
25
|
+
f"{self.api_endpoint}/{path}", params=params, timeout=1800
|
|
26
|
+
)
|
|
27
|
+
if res.ok:
|
|
28
|
+
data = res.json()
|
|
29
|
+
if isinstance(data, list):
|
|
30
|
+
return data
|
|
31
|
+
raise exceptions.ApiRequestError(
|
|
32
|
+
f"Unexpected response type from api: {type(data)}"
|
|
33
|
+
)
|
|
34
|
+
raise exceptions.ApiRequestError(f"Api request error: {res.status_code}")
|
|
35
|
+
|
|
36
|
+
def get_raw_records(self, site: str, date: str) -> list:
|
|
37
|
+
return self.get(
|
|
38
|
+
"raw-files",
|
|
39
|
+
params={
|
|
40
|
+
"instrument": [
|
|
41
|
+
"halo-doppler-lidar",
|
|
42
|
+
"wls100s",
|
|
43
|
+
"wls200s",
|
|
44
|
+
"wls400s",
|
|
45
|
+
"wls70",
|
|
46
|
+
],
|
|
47
|
+
"site": site,
|
|
48
|
+
"date": date,
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def get_record_content(self, rec: dict) -> io.BytesIO:
|
|
53
|
+
if self.cache:
|
|
54
|
+
return cached_record(rec, self.session)
|
|
55
|
+
content = self.session.get(rec["downloadUrl"]).content
|
|
56
|
+
if rec["filename"].endswith(".gz"):
|
|
57
|
+
content = gzip.decompress(content)
|
|
58
|
+
return io.BytesIO(content)
|
doppy/data/cache.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import gzip
|
|
2
|
+
import logging
|
|
3
|
+
import shutil
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from requests import Session
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def cached_record(
|
|
11
|
+
record: dict, session: Session, check_disk_usage: bool = True
|
|
12
|
+
) -> BytesIO:
|
|
13
|
+
cache_dir = Path("cache")
|
|
14
|
+
path = cache_dir / record["uuid"]
|
|
15
|
+
|
|
16
|
+
if check_disk_usage:
|
|
17
|
+
HUNDRED_GIGABYTES_AS_BYTES = 100 * 1024 * 1024 * 1024
|
|
18
|
+
|
|
19
|
+
_, _, disk_free = shutil.disk_usage("./")
|
|
20
|
+
if disk_free < HUNDRED_GIGABYTES_AS_BYTES:
|
|
21
|
+
_clear_dir(cache_dir)
|
|
22
|
+
|
|
23
|
+
if path.is_file():
|
|
24
|
+
return BytesIO(path.read_bytes())
|
|
25
|
+
else:
|
|
26
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
content = session.get(record["downloadUrl"]).content
|
|
28
|
+
if record["filename"].endswith(".gz"):
|
|
29
|
+
try:
|
|
30
|
+
content = gzip.decompress(content)
|
|
31
|
+
except EOFError:
|
|
32
|
+
logging.error(f"Failed to recompress {record['filename']}")
|
|
33
|
+
raise
|
|
34
|
+
|
|
35
|
+
with path.open("wb") as f:
|
|
36
|
+
f.write(content)
|
|
37
|
+
return BytesIO(content)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _clear_dir(path: Path) -> None:
|
|
41
|
+
for f in path.glob("**/*"):
|
|
42
|
+
if f.is_file():
|
|
43
|
+
f.unlink()
|
doppy/data/exceptions.py
ADDED
doppy/defaults.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
DEFAULT_BEAM_ENERGY = 1e-5
|
|
2
|
+
DEFAULT_EFFECTIVE_DIAMETER = 25e-3
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Halo:
|
|
6
|
+
wavelength = 1.565e-6 # [m]
|
|
7
|
+
receiver_bandwidth = 50e6 # [Hz]
|
|
8
|
+
beam_energy = DEFAULT_BEAM_ENERGY
|
|
9
|
+
effective_diameter = DEFAULT_EFFECTIVE_DIAMETER
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WindCube:
|
|
13
|
+
# https://doi.org/10.5194/essd-13-3539-2021
|
|
14
|
+
wavelength = 1.54e-6 # [m]
|
|
15
|
+
receiver_bandwidth = 55e6 # [Hz]
|
|
16
|
+
beam_energy = DEFAULT_BEAM_ENERGY
|
|
17
|
+
effective_diameter = 50e-3 # [m]
|
|
18
|
+
focus = 1e3 # [m]
|
doppy/exceptions.py
ADDED
doppy/netcdf.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pathlib
|
|
4
|
+
import warnings
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
from typing import Literal, TypeAlias
|
|
7
|
+
|
|
8
|
+
import netCDF4
|
|
9
|
+
import numpy as np
|
|
10
|
+
import numpy.typing as npt
|
|
11
|
+
|
|
12
|
+
NetCDFDataType: TypeAlias = Literal["f4", "f8", "i4", "i8", "u4", "u8"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Dataset:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
filename: str | pathlib.Path,
|
|
19
|
+
format: Literal["NETCDF4", "NETCDF4_CLASSIC"] = "NETCDF4",
|
|
20
|
+
) -> None:
|
|
21
|
+
self.nc = netCDF4.Dataset(filename, mode="w", format=format)
|
|
22
|
+
|
|
23
|
+
def __enter__(self) -> Dataset:
|
|
24
|
+
return self
|
|
25
|
+
|
|
26
|
+
def __exit__(
|
|
27
|
+
self,
|
|
28
|
+
exc_type: type[BaseException] | None,
|
|
29
|
+
exc_val: BaseException | None,
|
|
30
|
+
exc_tb: TracebackType | None,
|
|
31
|
+
) -> None:
|
|
32
|
+
self.close()
|
|
33
|
+
|
|
34
|
+
def add_dimension(self, dim: str, size: int | None = None) -> Dataset:
|
|
35
|
+
self.nc.createDimension(dim, size)
|
|
36
|
+
return self
|
|
37
|
+
|
|
38
|
+
def add_attribute(self, key: str, val: str) -> Dataset:
|
|
39
|
+
setattr(self.nc, key, val)
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def add_atribute(self, key: str, val: str) -> Dataset:
|
|
43
|
+
warnings.warn("Use add_attribute", DeprecationWarning, stacklevel=2)
|
|
44
|
+
return self.add_attribute(key, val)
|
|
45
|
+
|
|
46
|
+
def add_time(
|
|
47
|
+
self,
|
|
48
|
+
name: str,
|
|
49
|
+
dimensions: tuple[str, ...],
|
|
50
|
+
data: npt.NDArray[np.datetime64],
|
|
51
|
+
dtype: NetCDFDataType,
|
|
52
|
+
standard_name: str | None = None,
|
|
53
|
+
long_name: str | None = None,
|
|
54
|
+
) -> Dataset:
|
|
55
|
+
time, units, calendar = _convert_time(data)
|
|
56
|
+
var = self.nc.createVariable(name, dtype, dimensions, compression="zlib")
|
|
57
|
+
var.units = units
|
|
58
|
+
var.calendar = calendar
|
|
59
|
+
var.axis = "T"
|
|
60
|
+
var[:] = time
|
|
61
|
+
if standard_name is not None:
|
|
62
|
+
var.standard_name = standard_name
|
|
63
|
+
if long_name is not None:
|
|
64
|
+
var.long_name = long_name
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def add_variable(
|
|
68
|
+
self,
|
|
69
|
+
name: str,
|
|
70
|
+
dimensions: tuple[str, ...],
|
|
71
|
+
units: str,
|
|
72
|
+
data: npt.NDArray[np.float64],
|
|
73
|
+
dtype: NetCDFDataType,
|
|
74
|
+
standard_name: str | None = None,
|
|
75
|
+
long_name: str | None = None,
|
|
76
|
+
mask: npt.NDArray[np.bool_] | None = None,
|
|
77
|
+
) -> Dataset:
|
|
78
|
+
fill_value = netCDF4.default_fillvals[dtype] if mask is not None else None
|
|
79
|
+
var = self.nc.createVariable(
|
|
80
|
+
name, dtype, dimensions, fill_value=fill_value, compression="zlib"
|
|
81
|
+
)
|
|
82
|
+
var.units = units
|
|
83
|
+
if mask is not None:
|
|
84
|
+
var[:] = np.ma.masked_array(data, mask) # type: ignore
|
|
85
|
+
else:
|
|
86
|
+
var[:] = data
|
|
87
|
+
if standard_name is not None:
|
|
88
|
+
var.standard_name = standard_name
|
|
89
|
+
if long_name is not None:
|
|
90
|
+
var.long_name = long_name
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
def add_scalar_variable(
|
|
94
|
+
self,
|
|
95
|
+
name: str,
|
|
96
|
+
units: str,
|
|
97
|
+
data: np.float64 | np.int64 | float | int,
|
|
98
|
+
dtype: NetCDFDataType,
|
|
99
|
+
standard_name: str | None = None,
|
|
100
|
+
long_name: str | None = None,
|
|
101
|
+
mask: npt.NDArray[np.bool_] | None = None,
|
|
102
|
+
) -> Dataset:
|
|
103
|
+
var = self.nc.createVariable(name, dtype)
|
|
104
|
+
var.units = units
|
|
105
|
+
var[:] = data
|
|
106
|
+
if standard_name is not None:
|
|
107
|
+
var.standard_name = standard_name
|
|
108
|
+
if long_name is not None:
|
|
109
|
+
var.long_name = long_name
|
|
110
|
+
return self
|
|
111
|
+
|
|
112
|
+
def close(self) -> None:
|
|
113
|
+
self.nc.close()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _convert_time(
|
|
117
|
+
time: npt.NDArray[np.datetime64],
|
|
118
|
+
) -> tuple[npt.NDArray[np.float64], str, str]:
|
|
119
|
+
"""
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
time : npt.NDArray[np.datetime64["us"]]
|
|
123
|
+
Must be represented in UTC
|
|
124
|
+
"""
|
|
125
|
+
if time.dtype != "<M8[us]":
|
|
126
|
+
raise TypeError("time must be datetime64[us]")
|
|
127
|
+
MICROSECONDS_TO_HOURS = 1 / (1e6 * 3600)
|
|
128
|
+
start_of_day = time.min().astype("datetime64[D]")
|
|
129
|
+
hours_since_start_of_day = (time - start_of_day).astype(
|
|
130
|
+
np.float64
|
|
131
|
+
) * MICROSECONDS_TO_HOURS
|
|
132
|
+
units = f"hours since {np.datetime_as_string(start_of_day)} 00:00:00 +00:00"
|
|
133
|
+
calendar = "standard"
|
|
134
|
+
return hours_since_start_of_day, units, calendar
|
doppy/options.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import numpy.typing as npt
|
|
5
|
+
import scipy
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def detect_wind_noise(
|
|
9
|
+
w: npt.NDArray[np.float64],
|
|
10
|
+
height: npt.NDArray[np.float64],
|
|
11
|
+
mask: npt.NDArray[np.bool_],
|
|
12
|
+
window: float = 150,
|
|
13
|
+
stride: int = 1,
|
|
14
|
+
) -> npt.NDArray[np.bool_]:
|
|
15
|
+
"""
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
w
|
|
19
|
+
vertical velocity, dims: (time,height)
|
|
20
|
+
|
|
21
|
+
height
|
|
22
|
+
dims: (height,)
|
|
23
|
+
|
|
24
|
+
mask
|
|
25
|
+
old mask that still contains noisy velocity data,
|
|
26
|
+
mask[t,h] = True iff w[t,h] is noise
|
|
27
|
+
|
|
28
|
+
window
|
|
29
|
+
size of window used to compute rolling median in meters
|
|
30
|
+
|
|
31
|
+
stride
|
|
32
|
+
stride used to compute rolling median
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
improved noise mask such that new_mask[t,h] = True iff w[t,h] is noise
|
|
37
|
+
"""
|
|
38
|
+
warnings.simplefilter("ignore", RuntimeWarning)
|
|
39
|
+
v = _rolling_median_over_range(
|
|
40
|
+
height,
|
|
41
|
+
w,
|
|
42
|
+
mask,
|
|
43
|
+
window=window, # meters
|
|
44
|
+
stride=stride,
|
|
45
|
+
fill_gaps=True,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
th = 2
|
|
49
|
+
diff = np.abs(v - w)
|
|
50
|
+
new_mask = (diff > th) | mask
|
|
51
|
+
new_mask = _remove_one_hot(new_mask)
|
|
52
|
+
return np.array(new_mask, dtype=np.bool_)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _remove_one_hot(m: npt.NDArray[np.bool_]) -> npt.NDArray[np.bool_]:
|
|
56
|
+
if m.ndim != 2:
|
|
57
|
+
raise ValueError
|
|
58
|
+
if m.shape[1] < 3:
|
|
59
|
+
return m
|
|
60
|
+
x = ~m
|
|
61
|
+
y = np.full(x.shape, np.False_)
|
|
62
|
+
y[:, 0] = x[:, 0] & x[:, 1]
|
|
63
|
+
y[:, 1:-1] = x[:, 1:-1] & (x[:, 2:] | x[:, :-2])
|
|
64
|
+
y[:, -1] = x[:, -1] & x[:, -2]
|
|
65
|
+
return np.array(~y, dtype=np.bool_)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _rolling_median_over_range(
|
|
69
|
+
range_: npt.NDArray[np.float64],
|
|
70
|
+
arr: npt.NDArray[np.float64],
|
|
71
|
+
mask: npt.NDArray[np.bool_],
|
|
72
|
+
window: float,
|
|
73
|
+
stride: int = 1,
|
|
74
|
+
fill_gaps: bool = False,
|
|
75
|
+
) -> npt.NDArray[np.float64]:
|
|
76
|
+
"""
|
|
77
|
+
window
|
|
78
|
+
range window in meters
|
|
79
|
+
"""
|
|
80
|
+
X = arr.T.copy()
|
|
81
|
+
X[mask.T] = np.nan
|
|
82
|
+
|
|
83
|
+
half_window = window / 2
|
|
84
|
+
|
|
85
|
+
i = 0
|
|
86
|
+
j = 0
|
|
87
|
+
n = len(range_)
|
|
88
|
+
med = np.full(X.shape, np.nan, dtype=np.float64)
|
|
89
|
+
for k in range(0, n, stride):
|
|
90
|
+
r = range_[k]
|
|
91
|
+
while i + 1 < n and r - range_[i + 1] >= half_window:
|
|
92
|
+
i += 1
|
|
93
|
+
while j + 1 < n and range_[j] - r < half_window:
|
|
94
|
+
j += 1
|
|
95
|
+
if i > k or j < k:
|
|
96
|
+
raise ValueError
|
|
97
|
+
med[k] = np.nanmedian(X[i : j + 1], axis=0)
|
|
98
|
+
|
|
99
|
+
if stride != 1 and fill_gaps:
|
|
100
|
+
ind = list(range(0, n, stride))
|
|
101
|
+
f_interp = scipy.interpolate.interp1d(
|
|
102
|
+
range_[ind], med[ind], axis=0, fill_value="extrapolate"
|
|
103
|
+
)
|
|
104
|
+
med_all = f_interp(range_)
|
|
105
|
+
return np.array(med_all.T.copy(), dtype=np.float64)
|
|
106
|
+
return np.array(med.T.copy(), dtype=np.float64)
|