doppy 0.0.3__tar.gz → 0.0.5__tar.gz

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.

Potentially problematic release.


This version of doppy might be problematic. Click here for more details.

Files changed (36) hide show
  1. {doppy-0.0.3 → doppy-0.0.5}/Cargo.lock +2 -2
  2. {doppy-0.0.3 → doppy-0.0.5}/Cargo.toml +1 -1
  3. {doppy-0.0.3 → doppy-0.0.5}/PKG-INFO +1 -1
  4. doppy-0.0.5/src/doppy/product/__init__.py +4 -0
  5. {doppy-0.0.3 → doppy-0.0.5}/src/doppy/product/stare.py +1 -5
  6. doppy-0.0.5/src/doppy/product/wind.py +244 -0
  7. {doppy-0.0.3 → doppy-0.0.5}/src/doppy/raw/halo_hpl.py +1 -1
  8. doppy-0.0.3/src/doppy/product/__init__.py +0 -3
  9. {doppy-0.0.3 → doppy-0.0.5}/LICENSE +0 -0
  10. {doppy-0.0.3 → doppy-0.0.5}/README.md +0 -0
  11. {doppy-0.0.3 → doppy-0.0.5}/crates/doppy_rs/Cargo.toml +0 -0
  12. {doppy-0.0.3 → doppy-0.0.5}/crates/doppy_rs/src/lib.rs +0 -0
  13. {doppy-0.0.3 → doppy-0.0.5}/crates/doppy_rs/src/raw/halo_hpl.rs +0 -0
  14. {doppy-0.0.3 → doppy-0.0.5}/crates/doppy_rs/src/raw.rs +0 -0
  15. {doppy-0.0.3 → doppy-0.0.5}/crates/doprs/.gitignore +0 -0
  16. {doppy-0.0.3 → doppy-0.0.5}/crates/doprs/Cargo.toml +0 -0
  17. {doppy-0.0.3 → doppy-0.0.5}/crates/doprs/src/lib.rs +0 -0
  18. {doppy-0.0.3 → doppy-0.0.5}/crates/doprs/src/raw/error.rs +0 -0
  19. {doppy-0.0.3 → doppy-0.0.5}/crates/doprs/src/raw/halo_hpl.rs +0 -0
  20. {doppy-0.0.3 → doppy-0.0.5}/crates/doprs/src/raw.rs +0 -0
  21. {doppy-0.0.3 → doppy-0.0.5}/pyproject.toml +0 -0
  22. {doppy-0.0.3 → doppy-0.0.5}/src/doppy/__init__.py +0 -0
  23. {doppy-0.0.3 → doppy-0.0.5}/src/doppy/__main__.py +0 -0
  24. {doppy-0.0.3 → doppy-0.0.5}/src/doppy/bench.py +0 -0
  25. {doppy-0.0.3 → doppy-0.0.5}/src/doppy/data/__init__.py +0 -0
  26. {doppy-0.0.3 → doppy-0.0.5}/src/doppy/data/api.py +0 -0
  27. {doppy-0.0.3 → doppy-0.0.5}/src/doppy/data/cache.py +0 -0
  28. {doppy-0.0.3 → doppy-0.0.5}/src/doppy/data/exceptions.py +0 -0
  29. {doppy-0.0.3 → doppy-0.0.5}/src/doppy/defaults.py +0 -0
  30. {doppy-0.0.3 → doppy-0.0.5}/src/doppy/exceptions.py +0 -0
  31. {doppy-0.0.3 → doppy-0.0.5}/src/doppy/netcdf.py +0 -0
  32. {doppy-0.0.3 → doppy-0.0.5}/src/doppy/options.py +0 -0
  33. {doppy-0.0.3 → doppy-0.0.5}/src/doppy/py.typed +0 -0
  34. {doppy-0.0.3 → doppy-0.0.5}/src/doppy/raw/__init__.py +0 -0
  35. {doppy-0.0.3 → doppy-0.0.5}/src/doppy/raw/halo_bg.py +0 -0
  36. {doppy-0.0.3 → doppy-0.0.5}/src/doppy/raw/halo_sys_params.py +0 -0
@@ -106,7 +106,7 @@ checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
106
106
 
107
107
  [[package]]
108
108
  name = "doppy_rs"
109
- version = "0.0.3"
109
+ version = "0.0.5"
110
110
  dependencies = [
111
111
  "doprs",
112
112
  "numpy",
@@ -115,7 +115,7 @@ dependencies = [
115
115
 
116
116
  [[package]]
117
117
  name = "doprs"
118
- version = "0.0.3"
118
+ version = "0.0.5"
119
119
  dependencies = [
120
120
  "chrono",
121
121
  "rayon",
@@ -4,6 +4,6 @@ resolver = "2"
4
4
 
5
5
  [workspace.package]
6
6
  edition = "2021"
7
- version = "0.0.3"
7
+ version = "0.0.5"
8
8
  authors = ["Niko Leskinen <niko.leskinen@fmi.fi>"]
9
9
  license-file = "LICENSE"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: doppy
3
- Version: 0.0.3
3
+ Version: 0.0.5
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.10
@@ -0,0 +1,4 @@
1
+ from doppy.product.stare import Stare
2
+ from doppy.product.wind import Wind
3
+
4
+ __all__ = ["Stare", "Wind"]
@@ -508,11 +508,7 @@ def _select_raws_for_stare(
508
508
  raise doppy.exceptions.NoDataError("No data to select from")
509
509
 
510
510
  # Select files that stare
511
- raws_stare = [
512
- raw
513
- for raw in raws
514
- if len(raw.azimuth_angles) == 1 or raw.azimuth_angles == {0, 360}
515
- ]
511
+ raws_stare = [raw for raw in raws if len(raw.azimuth_angles) == 1]
516
512
  if len(raws_stare) == 0:
517
513
  raise doppy.exceptions.NoDataError(
518
514
  "No data suitable for stare product. Data is probably from scans"
@@ -0,0 +1,244 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ from collections import defaultdict
5
+ from dataclasses import dataclass
6
+ from io import BufferedIOBase
7
+ from pathlib import Path
8
+ from typing import Sequence, TypeAlias
9
+
10
+ import numpy as np
11
+ import numpy.typing as npt
12
+ from scipy.ndimage import generic_filter
13
+ from sklearn.cluster import KMeans
14
+
15
+ import doppy
16
+
17
+ # ngates, elevation angle, tuple of sorted azimuth angles
18
+ SelectionGroupKeyType: TypeAlias = tuple[int, int, tuple[int, ...]]
19
+
20
+
21
+ @dataclass
22
+ class Wind:
23
+ time: npt.NDArray[np.datetime64]
24
+ height: npt.NDArray[np.float64]
25
+ zonal_wind: npt.NDArray[np.float64]
26
+ meridional_wind: npt.NDArray[np.float64]
27
+ vertical_wind: npt.NDArray[np.float64]
28
+ mask: npt.NDArray[np.bool_]
29
+
30
+ @functools.cached_property
31
+ def horizontal_wind_speed(self) -> npt.NDArray[np.float64]:
32
+ return np.sqrt(self.zonal_wind**2 + self.meridional_wind**2)
33
+
34
+ @functools.cached_property
35
+ def horizontal_wind_direction(self) -> npt.NDArray[np.float64]:
36
+ direction = np.arctan2(self.zonal_wind, self.meridional_wind)
37
+ direction[direction < 0] += 2 * np.pi
38
+ return np.array(np.degrees(direction), dtype=np.float64)
39
+
40
+ @classmethod
41
+ def from_halo_data(
42
+ cls,
43
+ data: Sequence[str]
44
+ | Sequence[Path]
45
+ | Sequence[bytes]
46
+ | Sequence[BufferedIOBase],
47
+ ) -> Wind:
48
+ raws = doppy.raw.HaloHpl.from_srcs(data)
49
+
50
+ if len(raws) == 0:
51
+ raise doppy.exceptions.NoDataError("HaloHpl data missing")
52
+
53
+ raw = (
54
+ doppy.raw.HaloHpl.merge(_select_raws_for_wind(raws))
55
+ .sorted_by_time()
56
+ .non_strictly_increasing_timesteps_removed()
57
+ )
58
+
59
+ groups = _group_scans(raw)
60
+ time_list = []
61
+ elevation_list = []
62
+ wind_list = []
63
+ rmse_list = []
64
+
65
+ for group_index in set(groups):
66
+ pick = group_index == groups
67
+ time_, elevation_, wind_, rmse_ = _compute_wind(raw[pick])
68
+ time_list.append(time_)
69
+ elevation_list.append(elevation_)
70
+ wind_list.append(wind_[np.newaxis, :, :])
71
+ rmse_list.append(rmse_[np.newaxis, :])
72
+ time = np.array(time_list)
73
+ elevation = np.array(elevation_list)
74
+ wind = np.concatenate(wind_list)
75
+ rmse = np.concatenate(rmse_list)
76
+ if not np.allclose(elevation, elevation[0]):
77
+ raise ValueError("Elevation is expected to stay same")
78
+ height = raw.radial_distance * np.sin(np.deg2rad(elevation[0]))
79
+ mask = _compute_mask(wind, rmse)
80
+ return Wind(
81
+ time=time,
82
+ height=height,
83
+ zonal_wind=wind[:, :, 0],
84
+ meridional_wind=wind[:, :, 1],
85
+ vertical_wind=wind[:, :, 2],
86
+ mask=mask,
87
+ )
88
+
89
+
90
+ def _compute_wind(
91
+ raw: doppy.raw.HaloHpl,
92
+ ) -> tuple[float, float, npt.NDArray[np.float64], npt.NDArray[np.float64]]:
93
+ """
94
+ Returns
95
+ -------
96
+ time
97
+
98
+ elevation
99
+
100
+
101
+ wind (range,component):
102
+ Wind components for each range gate.
103
+ Components:
104
+ 0: zonal wind
105
+ 1: meridional wind
106
+ 2: vertical wind
107
+
108
+ rmse (range,):
109
+ Root-mean-square error of radial velocity fit for each range gate.
110
+ """
111
+ elevation = np.deg2rad(raw.elevation)
112
+ azimuth = np.deg2rad(raw.azimuth)
113
+ radial_velocity = raw.radial_velocity
114
+
115
+ cos_elevation = np.cos(elevation)
116
+ A = np.hstack(
117
+ (
118
+ (np.sin(azimuth) * cos_elevation).reshape(-1, 1),
119
+ (np.cos(azimuth) * cos_elevation).reshape(-1, 1),
120
+ (np.sin(elevation)).reshape(-1, 1),
121
+ )
122
+ )
123
+ A_inv = np.linalg.pinv(A)
124
+
125
+ w = A_inv @ radial_velocity
126
+ r_appr = A @ w
127
+ rmse = np.sqrt(np.sum((r_appr - radial_velocity) ** 2, axis=0) / r_appr.shape[0])
128
+ wind = w.T
129
+ time = raw.time[len(raw.time) // 2]
130
+ elevation = np.round(raw.elevation)
131
+ if not np.allclose(elevation, elevation[0]):
132
+ raise ValueError("Elevations in the scan differ")
133
+ return time, elevation[0], wind, rmse
134
+
135
+
136
+ def _compute_mask(
137
+ wind: npt.NDArray[np.float64], rmse: npt.NDArray[np.float64]
138
+ ) -> npt.NDArray[np.bool_]:
139
+ """
140
+ Parameters
141
+ ----------
142
+
143
+ wind (time,range,component)
144
+ intensty (time,range)
145
+ rmse (time,range)
146
+ """
147
+
148
+ def neighbour_diff(X: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
149
+ mdiff = np.max(np.abs(X - X[len(X) // 2]))
150
+ return np.array(mdiff, dtype=np.float64)
151
+
152
+ WIND_NEIGHBOUR_DIFFERENCE = 20
153
+ neighbour_mask = np.any(
154
+ generic_filter(wind, neighbour_diff, size=(1, 3, 1))
155
+ > WIND_NEIGHBOUR_DIFFERENCE,
156
+ axis=2,
157
+ )
158
+
159
+ rmse_th = 5
160
+ return np.array((rmse > rmse_th) | neighbour_mask, dtype=np.bool_)
161
+
162
+
163
+ def _group_scans(raw: doppy.raw.HaloHpl) -> npt.NDArray[np.int64]:
164
+ if len(raw.time) < 4:
165
+ raise ValueError("Expected at least 4 profiles to compute wind profile")
166
+ if raw.time.dtype != "<M8[us]":
167
+ raise TypeError("time expected to be in numpy datetime[us]")
168
+ time = raw.time.astype(np.float64) * 1e-6
169
+ timediff_in_seconds = np.diff(time)
170
+ kmeans = KMeans(n_clusters=2, n_init="auto").fit(timediff_in_seconds.reshape(-1, 1))
171
+ centers = kmeans.cluster_centers_.flatten()
172
+ scanstep_timediff = centers[np.argmin(centers)]
173
+
174
+ if scanstep_timediff < 0.1 or scanstep_timediff > 30:
175
+ raise ValueError(
176
+ "Time difference between profiles in one scan "
177
+ "expected to be between 0.1 and 30 seconds"
178
+ )
179
+ scanstep_timediff_upperbound = 2 * scanstep_timediff
180
+ groups_by_time = -1 * np.ones_like(time, dtype=np.int64)
181
+ groups_by_time[0] = 0
182
+ scan_index = 0
183
+ for i, (t_prev, t) in enumerate(zip(time[:-1], time[1:]), start=1):
184
+ if t - t_prev > scanstep_timediff_upperbound:
185
+ scan_index += 1
186
+ groups_by_time[i] = scan_index
187
+
188
+ return _subgroup_scans(raw, groups_by_time)
189
+
190
+
191
+ def _subgroup_scans(
192
+ raw: doppy.raw.HaloHpl, time_groups: npt.NDArray[np.int64]
193
+ ) -> npt.NDArray[np.int64]:
194
+ """
195
+ Groups scans further based on the azimuth angles
196
+ """
197
+ group = -1 * np.ones_like(raw.time, dtype=np.int64)
198
+ i = -1
199
+ for time_group in set(time_groups):
200
+ i += 1
201
+ (pick,) = np.where(time_group == time_groups)
202
+ raw_group = raw[pick]
203
+ first_azimuth_angle = int(np.round(raw_group.azimuth[0])) % 360
204
+ group[pick[0]] = i
205
+ for j, azi in enumerate(
206
+ (int(np.round(azi)) % 360 for azi in raw_group.azimuth[1:]), start=1
207
+ ):
208
+ if azi == first_azimuth_angle:
209
+ i += 1
210
+ group[pick[j]] = i
211
+ return group
212
+
213
+
214
+ def _select_raws_for_wind(
215
+ raws: Sequence[doppy.raw.HaloHpl],
216
+ ) -> Sequence[doppy.raw.HaloHpl]:
217
+ raws_wind = [
218
+ raw
219
+ for raw in raws
220
+ if len(raw.elevation_angles) == 1
221
+ and next(iter(raw.elevation_angles)) < 80
222
+ and len(raw.azimuth_angles) > 3
223
+ ]
224
+ groups: dict[SelectionGroupKeyType, int] = defaultdict(int)
225
+
226
+ for raw in raws_wind:
227
+ groups[_selection_key(raw)] += len(raw.time)
228
+
229
+ def key_func(key: SelectionGroupKeyType) -> int:
230
+ return groups[key]
231
+
232
+ select_tuple = max(groups, key=key_func)
233
+
234
+ return [raw for raw in raws_wind if _selection_key(raw) == select_tuple]
235
+
236
+
237
+ def _selection_key(raw: doppy.raw.HaloHpl) -> SelectionGroupKeyType:
238
+ if len(raw.elevation_angles) != 1:
239
+ raise ValueError("Expected only one elevation angle")
240
+ return (
241
+ raw.header.ngates,
242
+ next(iter(raw.elevation_angles)),
243
+ tuple(sorted(raw.azimuth_angles)),
244
+ )
@@ -149,7 +149,7 @@ class HaloHpl:
149
149
 
150
150
  @functools.cached_property
151
151
  def azimuth_angles(self) -> set[int]:
152
- return set(int(x) for x in np.round(self.azimuth))
152
+ return set(int(x) % 360 for x in np.round(self.azimuth))
153
153
 
154
154
  @functools.cached_property
155
155
  def elevation_angles(self) -> set[int]:
@@ -1,3 +0,0 @@
1
- from doppy.product.stare import Stare
2
-
3
- __all__ = ["Stare"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes