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 ADDED
@@ -0,0 +1,6 @@
1
+ from doppy import data, netcdf, options, product, raw
2
+ from doppy.rs import __version__
3
+
4
+ from . import bench
5
+
6
+ __all__ = ["raw", "options", "product", "netcdf", "bench", "data", "__version__"]
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()
@@ -0,0 +1,6 @@
1
+ class ApiRequestError(Exception):
2
+ pass
3
+
4
+
5
+ class CliArgumentError(Exception):
6
+ pass
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
@@ -0,0 +1,14 @@
1
+ class DoppyException(Exception):
2
+ pass
3
+
4
+
5
+ class RawParsingError(DoppyException):
6
+ pass
7
+
8
+
9
+ class NoDataError(DoppyException):
10
+ pass
11
+
12
+
13
+ class ShapeError(DoppyException):
14
+ pass
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,13 @@
1
+ from enum import Enum
2
+
3
+
4
+ class BgCorrectionMethod(Enum):
5
+ FIT = "fit"
6
+ MEAN = "mean"
7
+ PRE_COMPUTED = "pre_computed"
8
+
9
+
10
+ class BgFitMethod(Enum):
11
+ LIN = "lin"
12
+ EXP = "exp"
13
+ EXPLIN = "explin"
@@ -0,0 +1,6 @@
1
+ from doppy.product.stare import Stare
2
+ from doppy.product.stare_depol import StareDepol
3
+ from doppy.product.wind import Options as WindOptions
4
+ from doppy.product.wind import Wind
5
+
6
+ __all__ = ["Stare", "StareDepol", "Wind", "WindOptions"]
@@ -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)