cloudnetpy 1.55.20__py3-none-any.whl → 1.55.22__py3-none-any.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.
- cloudnetpy/categorize/atmos.py +46 -14
- cloudnetpy/categorize/atmos_utils.py +11 -1
- cloudnetpy/categorize/categorize.py +38 -21
- cloudnetpy/categorize/classify.py +31 -9
- cloudnetpy/categorize/containers.py +19 -7
- cloudnetpy/categorize/droplet.py +24 -8
- cloudnetpy/categorize/falling.py +17 -7
- cloudnetpy/categorize/freezing.py +19 -5
- cloudnetpy/categorize/insects.py +27 -14
- cloudnetpy/categorize/lidar.py +38 -36
- cloudnetpy/categorize/melting.py +19 -9
- cloudnetpy/categorize/model.py +28 -9
- cloudnetpy/categorize/mwr.py +4 -2
- cloudnetpy/categorize/radar.py +58 -22
- cloudnetpy/cloudnetarray.py +15 -6
- cloudnetpy/concat_lib.py +39 -16
- cloudnetpy/constants.py +7 -0
- cloudnetpy/datasource.py +39 -19
- cloudnetpy/instruments/basta.py +6 -2
- cloudnetpy/instruments/campbell_scientific.py +33 -16
- cloudnetpy/instruments/ceilo.py +30 -13
- cloudnetpy/instruments/ceilometer.py +76 -37
- cloudnetpy/instruments/cl61d.py +8 -3
- cloudnetpy/instruments/cloudnet_instrument.py +2 -1
- cloudnetpy/instruments/copernicus.py +27 -14
- cloudnetpy/instruments/disdrometer/common.py +51 -32
- cloudnetpy/instruments/disdrometer/parsivel.py +79 -48
- cloudnetpy/instruments/disdrometer/thies.py +10 -6
- cloudnetpy/instruments/galileo.py +23 -12
- cloudnetpy/instruments/hatpro.py +27 -11
- cloudnetpy/instruments/instruments.py +4 -1
- cloudnetpy/instruments/lufft.py +20 -11
- cloudnetpy/instruments/mira.py +60 -49
- cloudnetpy/instruments/mrr.py +31 -20
- cloudnetpy/instruments/nc_lidar.py +15 -6
- cloudnetpy/instruments/nc_radar.py +31 -22
- cloudnetpy/instruments/pollyxt.py +36 -21
- cloudnetpy/instruments/radiometrics.py +32 -18
- cloudnetpy/instruments/rpg.py +48 -22
- cloudnetpy/instruments/rpg_reader.py +39 -30
- cloudnetpy/instruments/vaisala.py +39 -27
- cloudnetpy/instruments/weather_station.py +15 -11
- cloudnetpy/metadata.py +3 -1
- cloudnetpy/model_evaluation/file_handler.py +31 -21
- cloudnetpy/model_evaluation/metadata.py +3 -1
- cloudnetpy/model_evaluation/model_metadata.py +1 -1
- cloudnetpy/model_evaluation/plotting/plot_tools.py +20 -15
- cloudnetpy/model_evaluation/plotting/plotting.py +114 -64
- cloudnetpy/model_evaluation/products/advance_methods.py +48 -28
- cloudnetpy/model_evaluation/products/grid_methods.py +44 -19
- cloudnetpy/model_evaluation/products/model_products.py +22 -18
- cloudnetpy/model_evaluation/products/observation_products.py +15 -9
- cloudnetpy/model_evaluation/products/product_resampling.py +14 -4
- cloudnetpy/model_evaluation/products/tools.py +16 -7
- cloudnetpy/model_evaluation/statistics/statistical_methods.py +28 -15
- cloudnetpy/model_evaluation/tests/e2e/conftest.py +3 -3
- cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +14 -13
- cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +14 -13
- cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +14 -13
- cloudnetpy/model_evaluation/tests/unit/conftest.py +11 -11
- cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +33 -27
- cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +83 -83
- cloudnetpy/model_evaluation/tests/unit/test_model_products.py +23 -21
- cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +24 -25
- cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +40 -39
- cloudnetpy/model_evaluation/tests/unit/test_plotting.py +12 -11
- cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +30 -30
- cloudnetpy/model_evaluation/tests/unit/test_tools.py +18 -17
- cloudnetpy/model_evaluation/utils.py +3 -2
- cloudnetpy/output.py +45 -19
- cloudnetpy/plotting/plot_meta.py +35 -11
- cloudnetpy/plotting/plotting.py +172 -104
- cloudnetpy/products/classification.py +20 -8
- cloudnetpy/products/der.py +25 -10
- cloudnetpy/products/drizzle.py +41 -26
- cloudnetpy/products/drizzle_error.py +10 -5
- cloudnetpy/products/drizzle_tools.py +43 -24
- cloudnetpy/products/ier.py +10 -5
- cloudnetpy/products/iwc.py +16 -9
- cloudnetpy/products/lwc.py +34 -12
- cloudnetpy/products/mwr_multi.py +4 -1
- cloudnetpy/products/mwr_single.py +4 -1
- cloudnetpy/products/product_tools.py +33 -10
- cloudnetpy/utils.py +175 -74
- cloudnetpy/version.py +1 -1
- {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/METADATA +11 -10
- cloudnetpy-1.55.22.dist-info/RECORD +114 -0
- docs/source/conf.py +2 -2
- cloudnetpy-1.55.20.dist-info/RECORD +0 -114
- {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/LICENSE +0 -0
- {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/WHEEL +0 -0
- {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/top_level.txt +0 -0
cloudnetpy/utils.py
CHANGED
@@ -1,12 +1,13 @@
|
|
1
|
-
"""
|
1
|
+
"""This module contains general helper functions."""
|
2
2
|
import datetime
|
3
3
|
import logging
|
4
4
|
import os
|
5
5
|
import re
|
6
6
|
import uuid
|
7
7
|
import warnings
|
8
|
+
from collections.abc import Iterator
|
8
9
|
from datetime import timezone
|
9
|
-
from typing import
|
10
|
+
from typing import Literal
|
10
11
|
|
11
12
|
import netCDF4
|
12
13
|
import numpy as np
|
@@ -14,31 +15,31 @@ from numpy import ma
|
|
14
15
|
from scipy import ndimage, stats
|
15
16
|
from scipy.interpolate import RectBivariateSpline, RegularGridInterpolator, griddata
|
16
17
|
|
18
|
+
from cloudnetpy.constants import SEC_IN_DAY, SEC_IN_HOUR, SEC_IN_MINUTE
|
17
19
|
from cloudnetpy.exceptions import ValidTimeStampError
|
18
20
|
|
19
21
|
Epoch = tuple[int, int, int]
|
20
22
|
Date = tuple[str, str, str]
|
21
23
|
|
22
|
-
SECONDS_PER_MINUTE: Final = 60
|
23
|
-
SECONDS_PER_HOUR: Final = 3600
|
24
|
-
SECONDS_PER_DAY: Final = 86400
|
25
|
-
|
26
24
|
|
27
25
|
def seconds2hours(time_in_seconds: np.ndarray) -> np.ndarray:
|
28
26
|
"""Converts seconds since some epoch to fraction hour.
|
29
27
|
|
30
28
|
Args:
|
29
|
+
----
|
31
30
|
time_in_seconds: 1-D array of seconds since some epoch that starts on midnight.
|
32
31
|
|
33
32
|
Returns:
|
33
|
+
-------
|
34
34
|
Time as fraction hour.
|
35
35
|
|
36
36
|
Notes:
|
37
|
+
-----
|
37
38
|
Excludes leap seconds.
|
38
39
|
|
39
40
|
"""
|
40
|
-
seconds_since_midnight = np.mod(time_in_seconds,
|
41
|
-
fraction_hour = seconds_since_midnight /
|
41
|
+
seconds_since_midnight = np.mod(time_in_seconds, SEC_IN_DAY)
|
42
|
+
fraction_hour = seconds_since_midnight / SEC_IN_HOUR
|
42
43
|
if fraction_hour[-1] == 0:
|
43
44
|
fraction_hour[-1] = 24
|
44
45
|
return fraction_hour
|
@@ -48,16 +49,18 @@ def seconds2time(time_in_seconds: float) -> list:
|
|
48
49
|
"""Converts seconds since some epoch to time of day.
|
49
50
|
|
50
51
|
Args:
|
52
|
+
----
|
51
53
|
time_in_seconds: seconds since some epoch.
|
52
54
|
|
53
55
|
Returns:
|
56
|
+
-------
|
54
57
|
list: [hours, minutes, seconds] formatted as '05' etc.
|
55
58
|
|
56
59
|
"""
|
57
|
-
seconds_since_midnight = np.mod(time_in_seconds,
|
58
|
-
hours = seconds_since_midnight //
|
59
|
-
minutes = seconds_since_midnight %
|
60
|
-
seconds = seconds_since_midnight %
|
60
|
+
seconds_since_midnight = np.mod(time_in_seconds, SEC_IN_DAY)
|
61
|
+
hours = seconds_since_midnight // SEC_IN_HOUR
|
62
|
+
minutes = seconds_since_midnight % SEC_IN_HOUR // SEC_IN_MINUTE
|
63
|
+
seconds = seconds_since_midnight % SEC_IN_MINUTE
|
61
64
|
time = [hours, minutes, seconds]
|
62
65
|
return [str(t).zfill(2) for t in time]
|
63
66
|
|
@@ -66,19 +69,21 @@ def seconds2date(time_in_seconds: float, epoch: Epoch = (2001, 1, 1)) -> list:
|
|
66
69
|
"""Converts seconds since some epoch to datetime (UTC).
|
67
70
|
|
68
71
|
Args:
|
72
|
+
----
|
69
73
|
time_in_seconds: Seconds since some epoch.
|
70
74
|
epoch: Epoch, default is (2001, 1, 1) (UTC).
|
71
75
|
|
72
76
|
Returns:
|
77
|
+
-------
|
73
78
|
[year, month, day, hours, minutes, seconds] formatted as '05' etc (UTC).
|
74
79
|
|
75
80
|
"""
|
76
81
|
epoch_in_seconds = datetime.datetime.timestamp(
|
77
|
-
datetime.datetime(*epoch, tzinfo=timezone.utc)
|
82
|
+
datetime.datetime(*epoch, tzinfo=timezone.utc),
|
78
83
|
)
|
79
84
|
timestamp = time_in_seconds + epoch_in_seconds
|
80
85
|
return (
|
81
|
-
datetime.datetime.
|
86
|
+
datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
|
82
87
|
.strftime("%Y %m %d %H %M %S")
|
83
88
|
.split()
|
84
89
|
)
|
@@ -89,7 +94,7 @@ def datetime2decimal_hours(data: np.ndarray | list) -> np.ndarray:
|
|
89
94
|
output = []
|
90
95
|
for timestamp in data:
|
91
96
|
t = timestamp.time()
|
92
|
-
decimal_hours = t.hour + t.minute /
|
97
|
+
decimal_hours = t.hour + t.minute / SEC_IN_MINUTE + t.second / SEC_IN_HOUR
|
93
98
|
output.append(decimal_hours)
|
94
99
|
return np.array(output)
|
95
100
|
|
@@ -101,18 +106,22 @@ def time_grid(time_step: int = 30) -> np.ndarray:
|
|
101
106
|
resolution (in seconds).
|
102
107
|
|
103
108
|
Args:
|
109
|
+
----
|
104
110
|
time_step: Time resolution in seconds, greater than 1. Default is 30.
|
105
111
|
|
106
112
|
Returns:
|
113
|
+
-------
|
107
114
|
Time vector between 0 and 24.
|
108
115
|
|
109
116
|
Raises:
|
117
|
+
------
|
110
118
|
ValueError: Bad resolution as input.
|
111
119
|
|
112
120
|
"""
|
113
121
|
if time_step < 1:
|
114
|
-
|
115
|
-
|
122
|
+
msg = "Time resolution should be >= 1 seconds"
|
123
|
+
raise ValueError(msg)
|
124
|
+
half_step = time_step / SEC_IN_HOUR / 2
|
116
125
|
return np.arange(half_step, 24 + half_step, half_step * 2)
|
117
126
|
|
118
127
|
|
@@ -120,12 +129,15 @@ def binvec(x: np.ndarray | list) -> np.ndarray:
|
|
120
129
|
"""Converts 1-D center points to bins with even spacing.
|
121
130
|
|
122
131
|
Args:
|
132
|
+
----
|
123
133
|
x: 1-D array of N real values.
|
124
134
|
|
125
135
|
Returns:
|
136
|
+
-------
|
126
137
|
ndarray: N + 1 edge values.
|
127
138
|
|
128
139
|
Examples:
|
140
|
+
--------
|
129
141
|
>>> binvec([1, 2, 3])
|
130
142
|
[0.5, 1.5, 2.5, 3.5]
|
131
143
|
|
@@ -145,6 +157,7 @@ def rebin_2d(
|
|
145
157
|
"""Rebins 2-D data in one dimension.
|
146
158
|
|
147
159
|
Args:
|
160
|
+
----
|
148
161
|
x_in: 1-D array with shape (n,).
|
149
162
|
array: 2-D input data with shape (n, m).
|
150
163
|
x_new: 1-D target vector (center points) with shape (N,).
|
@@ -153,9 +166,11 @@ def rebin_2d(
|
|
153
166
|
n_min: Minimum number of points to have good statistics in a bin. Default is 1.
|
154
167
|
|
155
168
|
Returns:
|
169
|
+
-------
|
156
170
|
tuple: Rebinned data with shape (N, m) and indices of bins without enough data.
|
157
171
|
|
158
172
|
Notes:
|
173
|
+
-----
|
159
174
|
0-values are masked in the returned array.
|
160
175
|
|
161
176
|
"""
|
@@ -166,7 +181,10 @@ def rebin_2d(
|
|
166
181
|
mask = ~values.mask
|
167
182
|
if ma.any(values[mask]):
|
168
183
|
result[:, ind], _, _ = stats.binned_statistic(
|
169
|
-
x_in[mask],
|
184
|
+
x_in[mask],
|
185
|
+
values[mask],
|
186
|
+
statistic=statistic,
|
187
|
+
bins=edges,
|
170
188
|
)
|
171
189
|
result[~np.isfinite(result)] = 0
|
172
190
|
masked_result = ma.masked_equal(result, 0)
|
@@ -179,7 +197,7 @@ def rebin_2d(
|
|
179
197
|
masked_result[ind, :] = ma.masked
|
180
198
|
empty_indices.append(ind)
|
181
199
|
if len(empty_indices) > 0:
|
182
|
-
logging.debug(
|
200
|
+
logging.debug("No radar data in %s bins", len(empty_indices))
|
183
201
|
|
184
202
|
return masked_result, empty_indices
|
185
203
|
|
@@ -193,6 +211,7 @@ def rebin_1d(
|
|
193
211
|
"""Rebins 1D array.
|
194
212
|
|
195
213
|
Args:
|
214
|
+
----
|
196
215
|
x_in: 1-D array with shape (n,).
|
197
216
|
array: 1-D input data with shape (m,).
|
198
217
|
x_new: 1-D target vector (center points) with shape (N,).
|
@@ -200,16 +219,20 @@ def rebin_1d(
|
|
200
219
|
Default is 'mean'.
|
201
220
|
|
202
221
|
Returns:
|
203
|
-
|
222
|
+
-------
|
223
|
+
Re-binned data with shape (N,).
|
204
224
|
|
205
225
|
"""
|
206
226
|
edges = binvec(x_new)
|
207
227
|
result = np.zeros(len(x_new))
|
208
228
|
array_screened = ma.masked_invalid(array, copy=True) # data may contain nan-values
|
209
|
-
mask = ~array_screened.mask
|
229
|
+
mask = ~array_screened.mask
|
210
230
|
if ma.any(array_screened[mask]):
|
211
231
|
result, _, _ = stats.binned_statistic(
|
212
|
-
x_in[mask],
|
232
|
+
x_in[mask],
|
233
|
+
array_screened[mask],
|
234
|
+
statistic=statistic,
|
235
|
+
bins=edges,
|
213
236
|
)
|
214
237
|
result[~np.isfinite(result)] = 0
|
215
238
|
return ma.masked_equal(result, 0)
|
@@ -219,12 +242,15 @@ def filter_isolated_pixels(array: np.ndarray) -> np.ndarray:
|
|
219
242
|
"""From a 2D boolean array, remove completely isolated single cells.
|
220
243
|
|
221
244
|
Args:
|
245
|
+
----
|
222
246
|
array: 2-D boolean array containing isolated values.
|
223
247
|
|
224
248
|
Returns:
|
249
|
+
-------
|
225
250
|
Cleaned array.
|
226
251
|
|
227
252
|
Examples:
|
253
|
+
--------
|
228
254
|
>>> filter_isolated_pixels([[0, 0, 0], [0, 1, 0], [0, 0, 0]])
|
229
255
|
array([[0, 0, 0],
|
230
256
|
[0, 0, 0],
|
@@ -239,15 +265,19 @@ def filter_x_pixels(array: np.ndarray) -> np.ndarray:
|
|
239
265
|
"""From a 2D boolean array, remove cells isolated in x-direction.
|
240
266
|
|
241
267
|
Args:
|
268
|
+
----
|
242
269
|
array: 2-D boolean array containing isolated pixels in x-direction.
|
243
270
|
|
244
271
|
Returns:
|
272
|
+
-------
|
245
273
|
Cleaned array.
|
246
274
|
|
247
275
|
Notes:
|
276
|
+
-----
|
248
277
|
Stronger cleaning than `filter_isolated_pixels()`
|
249
278
|
|
250
279
|
Examples:
|
280
|
+
--------
|
251
281
|
>>> filter_x_pixels([[1, 0, 0], [0, 1, 0], [0, 1, 1]])
|
252
282
|
array([[0, 0, 0],
|
253
283
|
[0, 1, 0],
|
@@ -268,30 +298,36 @@ def _filter(array: np.ndarray, structure: np.ndarray) -> np.ndarray:
|
|
268
298
|
|
269
299
|
|
270
300
|
def isbit(array: np.ndarray, nth_bit: int) -> np.ndarray:
|
271
|
-
"""Tests if nth bit (0,1,2
|
301
|
+
"""Tests if nth bit (0,1,2,...) is set.
|
272
302
|
|
273
303
|
Args:
|
304
|
+
----
|
274
305
|
array: Integer array.
|
275
306
|
nth_bit: Investigated bit.
|
276
307
|
|
277
308
|
Returns:
|
309
|
+
-------
|
278
310
|
Boolean array denoting values where nth_bit is set.
|
279
311
|
|
280
312
|
Raises:
|
313
|
+
------
|
281
314
|
ValueError: negative bit as input.
|
282
315
|
|
283
316
|
Examples:
|
317
|
+
--------
|
284
318
|
>>> isbit(4, 1)
|
285
319
|
False
|
286
320
|
>>> isbit(4, 2)
|
287
321
|
True
|
288
322
|
|
289
|
-
See
|
323
|
+
See Also:
|
324
|
+
--------
|
290
325
|
utils.setbit()
|
291
326
|
|
292
327
|
"""
|
293
328
|
if nth_bit < 0:
|
294
|
-
|
329
|
+
msg = "Negative bit number"
|
330
|
+
raise ValueError(msg)
|
295
331
|
mask = 1 << nth_bit
|
296
332
|
return array & mask > 0
|
297
333
|
|
@@ -300,27 +336,33 @@ def setbit(array: np.ndarray, nth_bit: int) -> np.ndarray:
|
|
300
336
|
"""Sets nth bit (0, 1, 2, ...) on number.
|
301
337
|
|
302
338
|
Args:
|
339
|
+
----
|
303
340
|
array: Integer array.
|
304
341
|
nth_bit: Bit to be set.
|
305
342
|
|
306
343
|
Returns:
|
344
|
+
-------
|
307
345
|
Integer where nth bit is set.
|
308
346
|
|
309
347
|
Raises:
|
348
|
+
------
|
310
349
|
ValueError: negative bit as input.
|
311
350
|
|
312
351
|
Examples:
|
352
|
+
--------
|
313
353
|
>>> setbit(1, 1)
|
314
354
|
3
|
315
355
|
>>> setbit(0, 2)
|
316
356
|
4
|
317
357
|
|
318
|
-
See
|
358
|
+
See Also:
|
359
|
+
--------
|
319
360
|
utils.isbit()
|
320
361
|
|
321
362
|
"""
|
322
363
|
if nth_bit < 0:
|
323
|
-
|
364
|
+
msg = "Negative bit number"
|
365
|
+
raise ValueError(msg)
|
324
366
|
mask = 1 << nth_bit
|
325
367
|
array |= mask
|
326
368
|
return array
|
@@ -336,6 +378,7 @@ def interpolate_2d(
|
|
336
378
|
"""Linear interpolation of gridded 2d data.
|
337
379
|
|
338
380
|
Args:
|
381
|
+
----
|
339
382
|
x: 1-D array.
|
340
383
|
y: 1-D array.
|
341
384
|
z: 2-D array at points (x, y).
|
@@ -343,9 +386,11 @@ def interpolate_2d(
|
|
343
386
|
y_new: 1-D array.
|
344
387
|
|
345
388
|
Returns:
|
389
|
+
-------
|
346
390
|
Interpolated data.
|
347
391
|
|
348
392
|
Notes:
|
393
|
+
-----
|
349
394
|
Does not work with nans. Ignores mask of masked data. Does not extrapolate.
|
350
395
|
|
351
396
|
"""
|
@@ -363,6 +408,7 @@ def interpolate_2d_mask(
|
|
363
408
|
"""2D linear interpolation preserving the mask.
|
364
409
|
|
365
410
|
Args:
|
411
|
+
----
|
366
412
|
x: 1D array, x-coordinates.
|
367
413
|
y: 1D array, y-coordinates.
|
368
414
|
z: 2D masked array, data values.
|
@@ -370,31 +416,35 @@ def interpolate_2d_mask(
|
|
370
416
|
y_new: 1D array, new y-coordinates.
|
371
417
|
|
372
418
|
Returns:
|
419
|
+
-------
|
373
420
|
Interpolated 2D masked array.
|
374
421
|
|
375
422
|
Notes:
|
423
|
+
-----
|
376
424
|
Points outside the original range will be nans (and masked). Uses linear
|
377
425
|
interpolation. Input data may contain nan-values.
|
378
426
|
|
379
427
|
"""
|
380
|
-
z = ma.array(ma.masked_invalid(z, copy=True))
|
428
|
+
z = ma.array(ma.masked_invalid(z, copy=True))
|
381
429
|
# Interpolate ignoring masked values:
|
382
|
-
valid_points = np.logical_not(z.mask)
|
430
|
+
valid_points = np.logical_not(z.mask)
|
383
431
|
xx, yy = np.meshgrid(y, x)
|
384
432
|
x_valid = xx[valid_points]
|
385
433
|
y_valid = yy[valid_points]
|
386
434
|
z_valid = z[valid_points]
|
387
435
|
xx_new, yy_new = np.meshgrid(y_new, x_new)
|
388
436
|
data = griddata(
|
389
|
-
(x_valid, y_valid),
|
437
|
+
(x_valid, y_valid),
|
438
|
+
z_valid.ravel(),
|
439
|
+
(xx_new, yy_new),
|
440
|
+
method="linear",
|
390
441
|
)
|
391
442
|
# Preserve mask:
|
392
443
|
mask_fun = RectBivariateSpline(x, y, z.mask[:], kx=1, ky=1)
|
393
444
|
mask = mask_fun(x_new, y_new)
|
394
445
|
mask[mask < 0.5] = 0
|
395
446
|
masked_array = ma.array(data, mask=mask.astype(bool))
|
396
|
-
|
397
|
-
return masked_array
|
447
|
+
return ma.masked_invalid(masked_array)
|
398
448
|
|
399
449
|
|
400
450
|
def interpolate_2d_nearest(
|
@@ -407,6 +457,7 @@ def interpolate_2d_nearest(
|
|
407
457
|
"""2D nearest neighbor interpolation preserving mask.
|
408
458
|
|
409
459
|
Args:
|
460
|
+
----
|
410
461
|
x: 1D array, x-coordinates.
|
411
462
|
y: 1D array, y-coordinates.
|
412
463
|
z: 2D masked array, data values.
|
@@ -414,9 +465,11 @@ def interpolate_2d_nearest(
|
|
414
465
|
y_new: 1D array, new y-coordinates.
|
415
466
|
|
416
467
|
Returns:
|
468
|
+
-------
|
417
469
|
Interpolated 2D masked array.
|
418
470
|
|
419
471
|
Notes:
|
472
|
+
-----
|
420
473
|
Points outside the original range will be interpolated but masked.
|
421
474
|
|
422
475
|
"""
|
@@ -438,7 +491,7 @@ def calc_relative_error(reference: np.ndarray, array: np.ndarray) -> np.ndarray:
|
|
438
491
|
|
439
492
|
|
440
493
|
def db2lin(array: float | np.ndarray, scale: int = 10) -> np.ndarray:
|
441
|
-
"""
|
494
|
+
"""DB to linear conversion."""
|
442
495
|
data = array / scale
|
443
496
|
with warnings.catch_warnings():
|
444
497
|
warnings.simplefilter("ignore", category=RuntimeWarning)
|
@@ -463,9 +516,11 @@ def l2norm(*args) -> ma.MaskedArray:
|
|
463
516
|
"""Returns l2 norm.
|
464
517
|
|
465
518
|
Args:
|
519
|
+
----
|
466
520
|
*args: Variable number of data (*array_like*) with the same shape.
|
467
521
|
|
468
522
|
Returns:
|
523
|
+
-------
|
469
524
|
The l2 norm.
|
470
525
|
|
471
526
|
"""
|
@@ -473,16 +528,18 @@ def l2norm(*args) -> ma.MaskedArray:
|
|
473
528
|
for arg in args:
|
474
529
|
if isinstance(arg, ma.MaskedArray):
|
475
530
|
# Raise only non-masked values, not sure if this is needed...
|
476
|
-
|
477
|
-
|
531
|
+
arg_cpy = ma.copy(arg)
|
532
|
+
arg_cpy[~arg.mask] = arg_cpy[~arg.mask] ** 2
|
478
533
|
else:
|
479
|
-
|
480
|
-
ss = ss +
|
534
|
+
arg_cpy = arg**2
|
535
|
+
ss = ss + arg_cpy
|
481
536
|
return ma.sqrt(ss)
|
482
537
|
|
483
538
|
|
484
539
|
def l2norm_weighted(
|
485
|
-
values: tuple,
|
540
|
+
values: tuple,
|
541
|
+
overall_scale: float,
|
542
|
+
term_weights: tuple,
|
486
543
|
) -> ma.MaskedArray:
|
487
544
|
"""Calculates scaled and weighted Euclidean distance.
|
488
545
|
|
@@ -491,12 +548,14 @@ def l2norm_weighted(
|
|
491
548
|
for the terms.
|
492
549
|
|
493
550
|
Args:
|
551
|
+
----
|
494
552
|
values: Tuple containing the values.
|
495
553
|
overall_scale: Scale factor for the calculated Euclidean distance.
|
496
554
|
term_weights: Weights for the terms. Must be single float or a list of numbers
|
497
555
|
(one per term).
|
498
556
|
|
499
557
|
Returns:
|
558
|
+
-------
|
500
559
|
Scaled and weighted Euclidean distance.
|
501
560
|
|
502
561
|
TODO: Use masked arrays instead of tuples.
|
@@ -511,58 +570,68 @@ def cumsumr(array: np.ndarray, axis: int = 0) -> np.ndarray:
|
|
511
570
|
"""Finds cumulative sum that resets on 0.
|
512
571
|
|
513
572
|
Args:
|
573
|
+
----
|
514
574
|
array: Input array.
|
515
575
|
axis: Axis where the sum is calculated. Default is 0.
|
516
576
|
|
517
577
|
Returns:
|
578
|
+
-------
|
518
579
|
Cumulative sum, restarted at 0.
|
519
580
|
|
520
581
|
Examples:
|
582
|
+
--------
|
521
583
|
>>> x = np.array([0, 0, 1, 1, 0, 0, 0, 1, 1, 1])
|
522
584
|
>>> cumsumr(x)
|
523
585
|
[0, 0, 1, 2, 0, 0, 0, 1, 2, 3]
|
524
586
|
|
525
587
|
"""
|
526
588
|
cums = array.cumsum(axis=axis)
|
527
|
-
return cums - np.maximum.accumulate(
|
528
|
-
cums * (array == 0), axis=axis
|
529
|
-
) # pylint: disable=E1101
|
589
|
+
return cums - np.maximum.accumulate(cums * (array == 0), axis=axis)
|
530
590
|
|
531
591
|
|
532
592
|
def ffill(array: np.ndarray, value: int = 0) -> np.ndarray:
|
533
593
|
"""Forward fills an array.
|
534
594
|
|
535
595
|
Args:
|
596
|
+
----
|
536
597
|
array: 1-D or 2-D array.
|
537
598
|
value: Value to be filled. Default is 0.
|
538
599
|
|
539
600
|
Returns:
|
601
|
+
-------
|
540
602
|
ndarray: Forward-filled array.
|
541
603
|
|
542
604
|
Examples:
|
605
|
+
--------
|
543
606
|
>>> x = np.array([0, 5, 0, 0, 2, 0])
|
544
607
|
>>> ffill(x)
|
545
608
|
[0, 5, 5, 5, 2, 2]
|
546
609
|
|
547
610
|
Notes:
|
611
|
+
-----
|
548
612
|
Works only in axis=1 direction.
|
549
613
|
|
550
614
|
"""
|
551
615
|
ndims = len(array.shape)
|
552
616
|
ran = np.arange(array.shape[ndims - 1])
|
553
617
|
idx = np.where((array != value), ran, 0)
|
554
|
-
idx = np.maximum.accumulate(idx, axis=ndims - 1)
|
618
|
+
idx = np.maximum.accumulate(idx, axis=ndims - 1)
|
555
619
|
if ndims == 2:
|
556
620
|
return array[np.arange(idx.shape[0])[:, None], idx]
|
557
621
|
return array[idx]
|
558
622
|
|
559
623
|
|
560
624
|
def init(
|
561
|
-
n_vars: int,
|
625
|
+
n_vars: int,
|
626
|
+
shape: tuple,
|
627
|
+
dtype: type = float,
|
628
|
+
*,
|
629
|
+
masked: bool = True,
|
562
630
|
) -> Iterator[np.ndarray | ma.MaskedArray]:
|
563
631
|
"""Initializes several numpy arrays.
|
564
632
|
|
565
633
|
Args:
|
634
|
+
----
|
566
635
|
n_vars: Number of arrays to be generated.
|
567
636
|
shape: Shape of the arrays, e.g. (2, 3).
|
568
637
|
dtype: The desired data-type for the arrays, e.g., int. Default is float.
|
@@ -570,9 +639,11 @@ def init(
|
|
570
639
|
Default is True.
|
571
640
|
|
572
641
|
Yields:
|
642
|
+
------
|
573
643
|
Iterator containing the empty arrays.
|
574
644
|
|
575
645
|
Examples:
|
646
|
+
--------
|
576
647
|
>>> a, b = init(2, (2, 3))
|
577
648
|
>>> a
|
578
649
|
masked_array(
|
@@ -593,17 +664,20 @@ def n_elements(array: np.ndarray, dist: float, var: str | None = None) -> int:
|
|
593
664
|
"""Returns the number of elements that cover certain distance.
|
594
665
|
|
595
666
|
Args:
|
667
|
+
----
|
596
668
|
array: Input array with arbitrary units or time in fraction hour. *x* should
|
597
669
|
be evenly spaced or at least close to.
|
598
670
|
dist: Distance to be covered. If x is fraction time, *dist* is in minutes.
|
599
|
-
Otherwise *x* and *dist* should have the same units.
|
671
|
+
Otherwise, *x* and *dist* should have the same units.
|
600
672
|
var: If 'time', input is fraction hour and distance in minutes, else inputs
|
601
673
|
have the same units. Default is None (same units).
|
602
674
|
|
603
675
|
Returns:
|
676
|
+
-------
|
604
677
|
Number of elements in the input array that cover *dist*.
|
605
678
|
|
606
679
|
Examples:
|
680
|
+
--------
|
607
681
|
>>> x = np.array([2, 4, 6, 8, 10])
|
608
682
|
>>> n_elements(x, 6)
|
609
683
|
3
|
@@ -624,16 +698,17 @@ def n_elements(array: np.ndarray, dist: float, var: str | None = None) -> int:
|
|
624
698
|
"""
|
625
699
|
n = dist / mdiff(array)
|
626
700
|
if var == "time":
|
627
|
-
n = n /
|
701
|
+
n = n / SEC_IN_MINUTE
|
628
702
|
return int(np.round(n))
|
629
703
|
|
630
704
|
|
631
|
-
def isscalar(array) -> bool:
|
705
|
+
def isscalar(array: np.ndarray | float | list) -> bool:
|
632
706
|
"""Tests if input is scalar.
|
633
707
|
|
634
708
|
By "scalar" we mean that array has a single value.
|
635
709
|
|
636
|
-
Examples
|
710
|
+
Examples
|
711
|
+
--------
|
637
712
|
>>> isscalar(1)
|
638
713
|
True
|
639
714
|
>>> isscalar([1])
|
@@ -645,18 +720,19 @@ def isscalar(array) -> bool:
|
|
645
720
|
|
646
721
|
"""
|
647
722
|
arr = ma.array(array)
|
648
|
-
|
649
|
-
return True
|
650
|
-
return False
|
723
|
+
return not hasattr(arr, "__len__") or arr.shape == () or len(arr) == 1
|
651
724
|
|
652
725
|
|
653
726
|
def get_time() -> str:
|
654
727
|
"""Returns current UTC-time."""
|
655
|
-
|
728
|
+
t_zone = datetime.timezone.utc
|
729
|
+
form = "%Y-%m-%d %H:%M:%S"
|
730
|
+
return f"{datetime.datetime.now(tz=t_zone).strftime(form)} +00:00"
|
656
731
|
|
657
732
|
|
658
733
|
def date_range(
|
659
|
-
start_date: datetime.date,
|
734
|
+
start_date: datetime.date,
|
735
|
+
end_date: datetime.date,
|
660
736
|
) -> Iterator[datetime.date]:
|
661
737
|
"""Returns range between two dates (datetimes)."""
|
662
738
|
for n in range(int((end_date - start_date).days)):
|
@@ -672,9 +748,11 @@ def get_wl_band(radar_frequency: float) -> int:
|
|
672
748
|
"""Returns integer corresponding to radar frequency.
|
673
749
|
|
674
750
|
Args:
|
751
|
+
----
|
675
752
|
radar_frequency: Radar frequency (GHz).
|
676
753
|
|
677
754
|
Returns:
|
755
|
+
-------
|
678
756
|
0 = 35GHz radar, 1 = 94Ghz radar.
|
679
757
|
|
680
758
|
"""
|
@@ -689,7 +767,8 @@ def get_frequency(wl_band: int) -> str:
|
|
689
767
|
def transpose(data: np.ndarray) -> np.ndarray:
|
690
768
|
"""Transposes numpy array of (n, ) to (n, 1)."""
|
691
769
|
if data.ndim != 1 or len(data) <= 1:
|
692
|
-
|
770
|
+
msg = "Invalid input array shape"
|
771
|
+
raise ValueError(msg)
|
693
772
|
return data[:, np.newaxis]
|
694
773
|
|
695
774
|
|
@@ -697,10 +776,12 @@ def del_dict_keys(data: dict, keys: tuple | list) -> dict:
|
|
697
776
|
"""Deletes multiple keys from dictionary.
|
698
777
|
|
699
778
|
Args:
|
779
|
+
----
|
700
780
|
data: A dictionary.
|
701
781
|
keys: Keys to be deleted.
|
702
782
|
|
703
783
|
Returns:
|
784
|
+
-------
|
704
785
|
Dictionary without the deleted keys.
|
705
786
|
|
706
787
|
"""
|
@@ -712,11 +793,16 @@ def del_dict_keys(data: dict, keys: tuple | list) -> dict:
|
|
712
793
|
|
713
794
|
|
714
795
|
def array_to_probability(
|
715
|
-
array: np.ndarray,
|
796
|
+
array: np.ndarray,
|
797
|
+
loc: float,
|
798
|
+
scale: float,
|
799
|
+
*,
|
800
|
+
invert: bool = False,
|
716
801
|
) -> np.ndarray:
|
717
802
|
"""Converts continuous variable into 0-1 probability.
|
718
803
|
|
719
804
|
Args:
|
805
|
+
----
|
720
806
|
array: Numpy array.
|
721
807
|
loc: Center of the distribution. Values smaller than this will have small
|
722
808
|
probability. Values greater than this will have large probability.
|
@@ -726,6 +812,7 @@ def array_to_probability(
|
|
726
812
|
Default is False.
|
727
813
|
|
728
814
|
Returns:
|
815
|
+
-------
|
729
816
|
Probability with the same shape as the input data.
|
730
817
|
|
731
818
|
"""
|
@@ -743,13 +830,16 @@ def range_to_height(range_los: np.ndarray, tilt_angle: float) -> np.ndarray:
|
|
743
830
|
"""Converts distances from a tilted instrument to height above the ground.
|
744
831
|
|
745
832
|
Args:
|
833
|
+
----
|
746
834
|
range_los: Distances towards the line of sign from the instrument.
|
747
835
|
tilt_angle: Angle in degrees from the zenith (0 = zenith).
|
748
836
|
|
749
837
|
Returns:
|
838
|
+
-------
|
750
839
|
Altitudes of the LOS points.
|
751
840
|
|
752
841
|
Notes:
|
842
|
+
-----
|
753
843
|
Uses plane parallel Earth approximation.
|
754
844
|
|
755
845
|
"""
|
@@ -776,9 +866,7 @@ def get_sorted_filenames(file_path: str, extension: str) -> list:
|
|
776
866
|
extension = extension.lower()
|
777
867
|
all_files = os.listdir(file_path)
|
778
868
|
files = [
|
779
|
-
"/".
|
780
|
-
for file in all_files
|
781
|
-
if file.lower().endswith(extension)
|
869
|
+
f"{file_path}/{file}" for file in all_files if file.lower().endswith(extension)
|
782
870
|
]
|
783
871
|
files.sort()
|
784
872
|
return files
|
@@ -808,7 +896,7 @@ def get_epoch(units: str) -> Epoch:
|
|
808
896
|
except ValueError:
|
809
897
|
return fallback
|
810
898
|
year, month, day = date_components
|
811
|
-
current_year = datetime.datetime.
|
899
|
+
current_year = datetime.datetime.now(tz=datetime.timezone.utc).year
|
812
900
|
if (1900 < year <= current_year) and (0 < month < 13) and (0 < day < 32):
|
813
901
|
return year, month, day
|
814
902
|
return fallback
|
@@ -818,14 +906,17 @@ def screen_by_time(data_in: dict, epoch: Epoch, expected_date: str) -> dict:
|
|
818
906
|
"""Screen data by time.
|
819
907
|
|
820
908
|
Args:
|
909
|
+
----
|
821
910
|
data_in: Dictionary containing at least 'time' key and other numpy arrays.
|
822
911
|
epoch: Epoch of the time array, e.g., (1970, 1, 1)
|
823
912
|
expected_date: Expected date in yyyy-mm-dd
|
824
913
|
|
825
914
|
Returns:
|
915
|
+
-------
|
826
916
|
data: Screened and sorted by the time vector.
|
827
917
|
|
828
918
|
Notes:
|
919
|
+
-----
|
829
920
|
- Requires 'time' key
|
830
921
|
- Works for dimensions 1, 2, 3 (time has to be at 0-axis)
|
831
922
|
- Does nothing for scalars
|
@@ -855,17 +946,21 @@ def find_valid_time_indices(time: np.ndarray, epoch: Epoch, expected_date: str)
|
|
855
946
|
"""Finds valid time array indices for the given date.
|
856
947
|
|
857
948
|
Args:
|
949
|
+
----
|
858
950
|
time: Time in seconds from some epoch.
|
859
951
|
epoch: Epoch of the time array, e.g., (1970, 1, 1)
|
860
952
|
expected_date: Expected date in yyyy-mm-dd
|
861
953
|
|
862
954
|
Returns:
|
955
|
+
-------
|
863
956
|
list: Valid indices for the given date in sorted order.
|
864
957
|
|
865
958
|
Raises:
|
959
|
+
------
|
866
960
|
RuntimeError: No valid timestamps.
|
867
961
|
|
868
962
|
Examples:
|
963
|
+
--------
|
869
964
|
>>> time = [1, 5, 1e6, 3]
|
870
965
|
>>> find_valid_time_indices(time, (1970, 1, 1) '1970-01-01')
|
871
966
|
[0, 3, 2]
|
@@ -886,6 +981,7 @@ def append_data(data_in: dict, key: str, array: np.ndarray) -> dict:
|
|
886
981
|
"""Appends data to a dictionary field (creates the field if not yet present).
|
887
982
|
|
888
983
|
Args:
|
984
|
+
----
|
889
985
|
data_in: Dictionary where data will be appended.
|
890
986
|
key: Key of the field.
|
891
987
|
array: Numpy array to be appended to data_in[key].
|
@@ -899,19 +995,19 @@ def append_data(data_in: dict, key: str, array: np.ndarray) -> dict:
|
|
899
995
|
return data
|
900
996
|
|
901
997
|
|
902
|
-
def edges2mid(data: np.ndarray, reference:
|
998
|
+
def edges2mid(data: np.ndarray, reference: Literal["upper", "lower"]) -> np.ndarray:
|
903
999
|
"""Shifts values half bin towards up or down.
|
904
1000
|
|
905
1001
|
Args:
|
1002
|
+
----
|
906
1003
|
data: 1D numpy array (e.g. range)
|
907
1004
|
reference: If 'lower', increase values by half bin. If 'upper', decrease values.
|
908
1005
|
|
909
1006
|
Returns:
|
1007
|
+
-------
|
910
1008
|
Shifted values.
|
911
1009
|
|
912
1010
|
"""
|
913
|
-
if reference not in ("lower", "upper"):
|
914
|
-
raise ValueError
|
915
1011
|
gaps = (data[1:] - data[0:-1]) / 2
|
916
1012
|
if reference == "lower":
|
917
1013
|
gaps = np.append(gaps, gaps[-1])
|
@@ -924,32 +1020,37 @@ def get_file_type(filename: str) -> str:
|
|
924
1020
|
"""Returns cloudnet file type from new and legacy files."""
|
925
1021
|
with netCDF4.Dataset(filename) as nc:
|
926
1022
|
if hasattr(nc, "cloudnet_file_type"):
|
927
|
-
|
928
|
-
return file_type
|
1023
|
+
return nc.cloudnet_file_type
|
929
1024
|
product = filename.split("_")[-1][:-3]
|
930
1025
|
if product in ("categorize", "classification", "drizzle"):
|
931
1026
|
return product
|
932
1027
|
if product[:3] in ("lwc", "iwc"):
|
933
1028
|
return product[:3]
|
934
|
-
|
1029
|
+
msg = "Unknown file type"
|
1030
|
+
raise ValueError(msg)
|
935
1031
|
|
936
1032
|
|
937
|
-
def get_files_with_common_range(
|
1033
|
+
def get_files_with_common_range(filenames: list) -> list:
|
938
1034
|
"""Returns files with the same (most common) number of range gates."""
|
939
1035
|
n_range = []
|
940
|
-
for file in
|
1036
|
+
for file in filenames:
|
941
1037
|
with netCDF4.Dataset(file) as nc:
|
942
1038
|
n_range.append(len(nc.variables["range"]))
|
943
1039
|
most_common = np.bincount(n_range).argmax()
|
944
|
-
n_removed
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
return [file for i, file in enumerate(
|
1040
|
+
if n_removed := len(filenames) - n_range.count(int(most_common)) > 0:
|
1041
|
+
logging.warning(
|
1042
|
+
"Removing %s files due to inconsistent height vector", n_removed
|
1043
|
+
)
|
1044
|
+
return [file for i, file in enumerate(filenames) if n_range[i] == most_common]
|
949
1045
|
|
950
1046
|
|
951
1047
|
def is_all_masked(array: np.ndarray) -> bool:
|
952
1048
|
"""Tests if all values are masked."""
|
953
|
-
|
954
|
-
|
955
|
-
|
1049
|
+
return ma.isMaskedArray(array) and hasattr(array, "mask") and array.mask.all()
|
1050
|
+
|
1051
|
+
|
1052
|
+
def find_masked_profiles_indices(array: ma.MaskedArray) -> list:
|
1053
|
+
"""Finds indices of masked profiles in a 2-D array."""
|
1054
|
+
non_masked_counts = np.ma.count(array, axis=1)
|
1055
|
+
masked_profiles_indices = np.where(non_masked_counts == 0)[0]
|
1056
|
+
return list(masked_profiles_indices)
|