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/categorize/model.py
CHANGED
@@ -14,11 +14,13 @@ class Model(DataSource):
|
|
14
14
|
"""Model class, child of DataSource.
|
15
15
|
|
16
16
|
Args:
|
17
|
+
----
|
17
18
|
model_file: File name of the NWP model file.
|
18
19
|
alt_site: Altitude of the site above mean sea level (m).
|
19
20
|
|
20
21
|
Attributes:
|
21
|
-
|
22
|
+
----------
|
23
|
+
source_type (str): Model type, e.g. 'gdas1' or 'ecwmf'.
|
22
24
|
model_heights (ndarray): 2-D array of model heights (one for each time
|
23
25
|
step).
|
24
26
|
mean_height (ndarray): Mean of *model_heights*.
|
@@ -38,11 +40,11 @@ class Model(DataSource):
|
|
38
40
|
"specific_saturated_gas_atten",
|
39
41
|
"specific_liquid_atten",
|
40
42
|
)
|
41
|
-
fields_sparse = fields_dense
|
43
|
+
fields_sparse = (*fields_dense, "q", "uwind", "vwind")
|
42
44
|
|
43
45
|
def __init__(self, model_file: str, alt_site: float):
|
44
46
|
super().__init__(model_file)
|
45
|
-
self.
|
47
|
+
self.source_type = _find_model_type(model_file)
|
46
48
|
self.model_heights = self._get_model_heights(alt_site)
|
47
49
|
self.mean_height = _calc_mean_height(self.model_heights)
|
48
50
|
self.height: np.ndarray
|
@@ -54,6 +56,7 @@ class Model(DataSource):
|
|
54
56
|
"""Interpolates model variables to common height grid.
|
55
57
|
|
56
58
|
Args:
|
59
|
+
----
|
57
60
|
wl_band: Integer denoting the approximate wavelength band of the
|
58
61
|
cloud radar (0 = ~35.5 GHz, 1 = ~94 GHz).
|
59
62
|
|
@@ -61,7 +64,9 @@ class Model(DataSource):
|
|
61
64
|
|
62
65
|
def _interpolate_variable(data_in: ma.MaskedArray) -> CloudnetArray:
|
63
66
|
datai = ma.zeros((len(self.time), len(self.mean_height)))
|
64
|
-
for ind, (alt, prof) in enumerate(
|
67
|
+
for ind, (alt, prof) in enumerate(
|
68
|
+
zip(self.model_heights, data_in, strict=True),
|
69
|
+
):
|
65
70
|
if prof.mask.all():
|
66
71
|
datai[ind, :] = ma.masked
|
67
72
|
else:
|
@@ -78,14 +83,21 @@ class Model(DataSource):
|
|
78
83
|
self.data_sparse[key] = _interpolate_variable(data)
|
79
84
|
|
80
85
|
def interpolate_to_grid(
|
81
|
-
self,
|
82
|
-
|
86
|
+
self,
|
87
|
+
time_grid: np.ndarray,
|
88
|
+
height_grid: np.ndarray,
|
89
|
+
) -> list:
|
83
90
|
"""Interpolates model variables to Cloudnet's dense time / height grid.
|
84
91
|
|
85
92
|
Args:
|
93
|
+
----
|
86
94
|
time_grid: The target time array (fraction hour).
|
87
95
|
height_grid: The target height array (m).
|
88
96
|
|
97
|
+
Returns:
|
98
|
+
-------
|
99
|
+
Indices fully masked profiles.
|
100
|
+
|
89
101
|
"""
|
90
102
|
for key in self.fields_dense:
|
91
103
|
array = self.data_sparse[key][:]
|
@@ -93,9 +105,14 @@ class Model(DataSource):
|
|
93
105
|
if valid_profiles < 2:
|
94
106
|
raise ModelDataError
|
95
107
|
self.data_dense[key] = utils.interpolate_2d_mask(
|
96
|
-
self.time,
|
108
|
+
self.time,
|
109
|
+
self.mean_height,
|
110
|
+
array,
|
111
|
+
time_grid,
|
112
|
+
height_grid,
|
97
113
|
)
|
98
114
|
self.height = height_grid
|
115
|
+
return utils.find_masked_profiles_indices(self.data_dense["temperature"])
|
99
116
|
|
100
117
|
def calc_wet_bulb(self) -> None:
|
101
118
|
"""Calculates wet-bulb temperature in dense grid."""
|
@@ -116,7 +133,8 @@ class Model(DataSource):
|
|
116
133
|
try:
|
117
134
|
model_heights = self.dataset.variables["height"]
|
118
135
|
except KeyError as err:
|
119
|
-
|
136
|
+
msg = "No 'height' variable in the model file."
|
137
|
+
raise ModelDataError(msg) from err
|
120
138
|
return self.to_m(model_heights) + alt_site
|
121
139
|
|
122
140
|
|
@@ -131,7 +149,8 @@ def _find_model_type(file_name: str) -> str:
|
|
131
149
|
for key in possible_keys:
|
132
150
|
if key in file_name:
|
133
151
|
return key
|
134
|
-
|
152
|
+
msg = "Unknown model type"
|
153
|
+
raise ValueError(msg)
|
135
154
|
|
136
155
|
|
137
156
|
def _find_number_of_valid_profiles(array: np.ndarray) -> int:
|
cloudnetpy/categorize/mwr.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
import numpy as np
|
3
3
|
|
4
4
|
from cloudnetpy import utils
|
5
|
+
from cloudnetpy.constants import G_TO_KG
|
5
6
|
from cloudnetpy.datasource import DataSource
|
6
7
|
|
7
8
|
|
@@ -9,6 +10,7 @@ class Mwr(DataSource):
|
|
9
10
|
"""Microwave radiometer class, child of DataSource.
|
10
11
|
|
11
12
|
Args:
|
13
|
+
----
|
12
14
|
full_path: Cloudnet Level 1b mwr file.
|
13
15
|
|
14
16
|
"""
|
@@ -22,6 +24,7 @@ class Mwr(DataSource):
|
|
22
24
|
"""Approximates lwp and its error in a grid using mean.
|
23
25
|
|
24
26
|
Args:
|
27
|
+
----
|
25
28
|
time_grid: 1D target time grid.
|
26
29
|
|
27
30
|
"""
|
@@ -34,8 +37,7 @@ class Mwr(DataSource):
|
|
34
37
|
|
35
38
|
def _init_lwp_error(self) -> None:
|
36
39
|
random_error, bias = 0.25, 20
|
37
|
-
|
38
|
-
lwp_error = utils.l2norm(self.data["lwp"][:] * random_error, bias * g2kg)
|
40
|
+
lwp_error = utils.l2norm(self.data["lwp"][:] * random_error, bias * G_TO_KG)
|
39
41
|
self.append_data(lwp_error, "lwp_error", units="kg m-2")
|
40
42
|
self.data["lwp_error"].comment = (
|
41
43
|
"This variable is a rough estimate of the one-standard-deviation\n"
|
cloudnetpy/categorize/radar.py
CHANGED
@@ -8,6 +8,7 @@ from scipy import constants
|
|
8
8
|
|
9
9
|
from cloudnetpy import utils
|
10
10
|
from cloudnetpy.categorize.classify import ClassificationResult
|
11
|
+
from cloudnetpy.constants import SEC_IN_HOUR
|
11
12
|
from cloudnetpy.datasource import DataSource
|
12
13
|
|
13
14
|
|
@@ -15,21 +16,24 @@ class Radar(DataSource):
|
|
15
16
|
"""Radar class, child of DataSource.
|
16
17
|
|
17
18
|
Args:
|
19
|
+
----
|
18
20
|
full_path: Cloudnet Level 1 radar netCDF file.
|
19
21
|
|
20
22
|
Attributes:
|
23
|
+
----------
|
21
24
|
radar_frequency (float): Radar frequency (GHz).
|
22
25
|
folding_velocity (float): Radar's folding velocity (m/s).
|
23
26
|
location (str): Location of the radar, copied from the global attribute
|
24
27
|
`location` of the input file.
|
25
28
|
sequence_indices (list): Indices denoting the different altitude
|
26
29
|
regimes of the radar.
|
27
|
-
|
30
|
+
source_type (str): Type of the radar, copied from the global attribute
|
28
31
|
`source` of the *radar_file*. Can be free form string but must
|
29
32
|
include either 'rpg' or 'mira' denoting one of the two supported
|
30
33
|
radars.
|
31
34
|
|
32
35
|
See Also:
|
36
|
+
--------
|
33
37
|
:func:`instruments.rpg2nc()`, :func:`instruments.mira2nc()`
|
34
38
|
|
35
39
|
"""
|
@@ -40,7 +44,7 @@ class Radar(DataSource):
|
|
40
44
|
self.folding_velocity = self._get_folding_velocity()
|
41
45
|
self.sequence_indices = self._get_sequence_indices()
|
42
46
|
self.location = getattr(self.dataset, "location", "")
|
43
|
-
self.
|
47
|
+
self.source_type = getattr(self.dataset, "source", "")
|
44
48
|
self._init_data()
|
45
49
|
self._init_sigma_v()
|
46
50
|
self._get_folding_velocity_full()
|
@@ -49,6 +53,7 @@ class Radar(DataSource):
|
|
49
53
|
"""Rebins radar data in time using mean.
|
50
54
|
|
51
55
|
Args:
|
56
|
+
----
|
52
57
|
time_new: Target time array as fraction hour. Updates *time* attribute.
|
53
58
|
|
54
59
|
"""
|
@@ -83,7 +88,7 @@ class Radar(DataSource):
|
|
83
88
|
|
84
89
|
"""
|
85
90
|
good_ind = ~ma.getmaskarray(self.data["Z"][:]) & ~ma.getmaskarray(
|
86
|
-
self.data["v"][:]
|
91
|
+
self.data["v"][:],
|
87
92
|
)
|
88
93
|
|
89
94
|
if "width" in self.data:
|
@@ -121,15 +126,28 @@ class Radar(DataSource):
|
|
121
126
|
if n_profiles_with_data < 300:
|
122
127
|
return
|
123
128
|
n_vertical = self._filter(
|
124
|
-
data,
|
129
|
+
data,
|
130
|
+
1,
|
131
|
+
min_coverage=0.5,
|
132
|
+
z_limit=10,
|
133
|
+
distance=4,
|
134
|
+
n_blocks=100,
|
125
135
|
)
|
126
136
|
n_horizontal = self._filter(
|
127
|
-
data,
|
137
|
+
data,
|
138
|
+
0,
|
139
|
+
min_coverage=0.3,
|
140
|
+
z_limit=-30,
|
141
|
+
distance=3,
|
142
|
+
n_blocks=20,
|
128
143
|
)
|
129
144
|
if n_vertical > 0 or n_horizontal > 0:
|
130
145
|
logging.debug(
|
131
|
-
|
132
|
-
|
146
|
+
"Filtered %s vertical and %s horizontal stripes "
|
147
|
+
"from radar data using %s",
|
148
|
+
n_vertical,
|
149
|
+
n_horizontal,
|
150
|
+
variable,
|
133
151
|
)
|
134
152
|
|
135
153
|
def _filter(
|
@@ -162,7 +180,7 @@ class Radar(DataSource):
|
|
162
180
|
threshold = distance * (q3 - q1) + q3
|
163
181
|
|
164
182
|
indices = np.where(
|
165
|
-
(n_values > threshold) & (n_values > (min_coverage * data.shape[1]))
|
183
|
+
(n_values > threshold) & (n_values > (min_coverage * data.shape[1])),
|
166
184
|
)[0]
|
167
185
|
true_ind = [int(x) for x in (block_number * len_block + indices)]
|
168
186
|
n_removed = len(indices)
|
@@ -184,10 +202,12 @@ class Radar(DataSource):
|
|
184
202
|
"""Corrects radar echo for liquid and gas attenuation.
|
185
203
|
|
186
204
|
Args:
|
205
|
+
----
|
187
206
|
attenuations: 2-D attenuations due to atmospheric gases and liquid:
|
188
207
|
`radar_gas_atten`, `radar_liquid_atten`.
|
189
208
|
|
190
209
|
References:
|
210
|
+
----------
|
191
211
|
The method is based on Hogan R. and O'Connor E., 2004,
|
192
212
|
https://bit.ly/2Yjz9DZ and the original Cloudnet Matlab implementation.
|
193
213
|
|
@@ -198,7 +218,9 @@ class Radar(DataSource):
|
|
198
218
|
self.append_data(z_corrected, "Z")
|
199
219
|
|
200
220
|
def calc_errors(
|
201
|
-
self,
|
221
|
+
self,
|
222
|
+
attenuations: dict,
|
223
|
+
classification: ClassificationResult,
|
202
224
|
) -> None:
|
203
225
|
"""Calculates uncertainties of radar echo.
|
204
226
|
|
@@ -206,10 +228,12 @@ class Radar(DataSource):
|
|
206
228
|
:class:`CloudnetArray` instances to `data` attribute.
|
207
229
|
|
208
230
|
Args:
|
231
|
+
----
|
209
232
|
attenuations: 2-D attenuations due to atmospheric gases.
|
210
233
|
classification: The :class:`ClassificationResult` instance.
|
211
234
|
|
212
235
|
References:
|
236
|
+
----------
|
213
237
|
The method is based on Hogan R. and O'Connor E., 2004,
|
214
238
|
https://bit.ly/2Yjz9DZ and the original Cloudnet Matlab implementation.
|
215
239
|
|
@@ -238,8 +262,7 @@ class Radar(DataSource):
|
|
238
262
|
return z_error
|
239
263
|
|
240
264
|
def _number_of_independent_pulses() -> float:
|
241
|
-
|
242
|
-
dwell_time = utils.mdiff(self.time) * seconds_in_hour
|
265
|
+
dwell_time = utils.mdiff(self.time) * SEC_IN_HOUR
|
243
266
|
return (
|
244
267
|
dwell_time
|
245
268
|
* self.radar_frequency
|
@@ -271,7 +294,7 @@ class Radar(DataSource):
|
|
271
294
|
for key in ("time", "height", "radar_frequency"):
|
272
295
|
self.append_data(np.array(getattr(self, key)), key)
|
273
296
|
|
274
|
-
def _init_data(self):
|
297
|
+
def _init_data(self) -> None:
|
275
298
|
self.append_data(self.getvar("Zh"), "Z", units="dBZ")
|
276
299
|
for key in ("v", "ldr", "width", "sldr", "rainfall_rate"):
|
277
300
|
try:
|
@@ -281,13 +304,17 @@ class Radar(DataSource):
|
|
281
304
|
|
282
305
|
def _init_sigma_v(self) -> None:
|
283
306
|
"""Initializes std of the velocity field. The std will be calculated
|
284
|
-
later when re-binning the data.
|
307
|
+
later when re-binning the data.
|
308
|
+
"""
|
285
309
|
self.append_data(self.getvar("v"), "v_sigma")
|
286
310
|
|
287
311
|
def _get_sequence_indices(self) -> list:
|
288
312
|
"""Mira has only one sequence and one folding velocity. RPG has
|
289
|
-
several sequences with different folding velocities.
|
290
|
-
|
313
|
+
several sequences with different folding velocities.
|
314
|
+
"""
|
315
|
+
if self.height is None:
|
316
|
+
msg = "Height not found in the input file"
|
317
|
+
raise RuntimeError(msg)
|
291
318
|
all_indices = np.arange(len(self.height))
|
292
319
|
if not utils.isscalar(self.folding_velocity):
|
293
320
|
starting_indices = self.getvar("chirp_start_indices")
|
@@ -300,18 +327,24 @@ class Radar(DataSource):
|
|
300
327
|
if "prf" in self.dataset.variables:
|
301
328
|
prf = self.getvar("prf")
|
302
329
|
return _prf_to_folding_velocity(prf, self.radar_frequency)
|
303
|
-
|
330
|
+
msg = "Unable to determine folding velocity"
|
331
|
+
raise RuntimeError(msg)
|
304
332
|
|
305
|
-
def _get_folding_velocity_full(self):
|
333
|
+
def _get_folding_velocity_full(self) -> None:
|
306
334
|
folding_velocity: list | np.ndarray = []
|
307
335
|
if utils.isscalar(self.folding_velocity):
|
308
336
|
folding_velocity = np.repeat(
|
309
|
-
self.folding_velocity,
|
337
|
+
self.folding_velocity,
|
338
|
+
len(self.sequence_indices[0]),
|
310
339
|
)
|
311
340
|
else:
|
312
|
-
|
313
|
-
|
314
|
-
for indices, velocity in zip(
|
341
|
+
folding_velocity = list(folding_velocity)
|
342
|
+
self.folding_velocity = np.array(self.folding_velocity)
|
343
|
+
for indices, velocity in zip(
|
344
|
+
self.sequence_indices,
|
345
|
+
self.folding_velocity,
|
346
|
+
strict=True,
|
347
|
+
):
|
315
348
|
folding_velocity.append(np.repeat(velocity, len(indices)))
|
316
349
|
folding_velocity = np.hstack(folding_velocity)
|
317
350
|
self.append_data(folding_velocity, "nyquist_velocity")
|
@@ -319,4 +352,7 @@ class Radar(DataSource):
|
|
319
352
|
|
320
353
|
def _prf_to_folding_velocity(prf: np.ndarray, radar_frequency: float) -> float:
|
321
354
|
ghz_to_hz = 1e9
|
322
|
-
|
355
|
+
if len(prf) != 1:
|
356
|
+
msg = "Unable to determine folding velocity"
|
357
|
+
raise RuntimeError(msg)
|
358
|
+
return float(prf[0] * constants.c / (4 * radar_frequency * ghz_to_hz))
|
cloudnetpy/cloudnetarray.py
CHANGED
@@ -14,6 +14,7 @@ class CloudnetArray:
|
|
14
14
|
"""Stores netCDF4 variables, numpy arrays and scalars as CloudnetArrays.
|
15
15
|
|
16
16
|
Args:
|
17
|
+
----
|
17
18
|
variable: The netCDF4 :class:`Variable` instance,
|
18
19
|
numpy array (masked or regular), or scalar (float, int).
|
19
20
|
name: Name of the variable.
|
@@ -25,7 +26,7 @@ class CloudnetArray:
|
|
25
26
|
|
26
27
|
def __init__(
|
27
28
|
self,
|
28
|
-
variable: netCDF4.Variable | np.ndarray | float
|
29
|
+
variable: netCDF4.Variable | np.ndarray | float,
|
29
30
|
name: str,
|
30
31
|
units_from_user: str | None = None,
|
31
32
|
dimensions: Sequence[str] | None = None,
|
@@ -58,10 +59,12 @@ class CloudnetArray:
|
|
58
59
|
"""Rebins `data` in time.
|
59
60
|
|
60
61
|
Args:
|
62
|
+
----
|
61
63
|
time: 1D time array.
|
62
64
|
time_new: 1D new time array.
|
63
65
|
|
64
66
|
Returns:
|
67
|
+
-------
|
65
68
|
Time indices without data.
|
66
69
|
|
67
70
|
"""
|
@@ -69,7 +72,8 @@ class CloudnetArray:
|
|
69
72
|
self.data = utils.rebin_1d(time, self.data, time_new)
|
70
73
|
bad_indices = list(np.where(self.data == ma.masked)[0])
|
71
74
|
else:
|
72
|
-
|
75
|
+
if not isinstance(self.data, ma.MaskedArray):
|
76
|
+
self.data = ma.masked_array(self.data)
|
73
77
|
self.data, bad_indices = utils.rebin_2d(time, self.data, time_new)
|
74
78
|
return bad_indices
|
75
79
|
|
@@ -101,7 +105,7 @@ class CloudnetArray:
|
|
101
105
|
return self.variable
|
102
106
|
if isinstance(
|
103
107
|
self.variable,
|
104
|
-
|
108
|
+
int | float | np.float32 | np.int8 | np.float64 | np.int32 | np.uint16,
|
105
109
|
):
|
106
110
|
return np.array(self.variable)
|
107
111
|
if isinstance(self.variable, str):
|
@@ -110,7 +114,8 @@ class CloudnetArray:
|
|
110
114
|
return np.array(numeric_value)
|
111
115
|
except ValueError:
|
112
116
|
pass
|
113
|
-
|
117
|
+
msg = f"Incorrect CloudnetArray input: {self.variable}"
|
118
|
+
raise ValueError(msg)
|
114
119
|
|
115
120
|
def _init_units(self) -> str:
|
116
121
|
return getattr(self.variable, "units", "")
|
@@ -134,7 +139,8 @@ class CloudnetArray:
|
|
134
139
|
self._filter(utils.filter_x_pixels)
|
135
140
|
|
136
141
|
def _filter(self, fun) -> None:
|
137
|
-
|
142
|
+
if not isinstance(self.data, ma.MaskedArray):
|
143
|
+
self.data = ma.masked_array(self.data)
|
138
144
|
is_data = (~self.data.mask).astype(int)
|
139
145
|
is_data_filtered = fun(is_data)
|
140
146
|
self.data[is_data_filtered == 0] = ma.masked
|
@@ -143,14 +149,16 @@ class CloudnetArray:
|
|
143
149
|
"""Calculates std of radar velocity.
|
144
150
|
|
145
151
|
Args:
|
152
|
+
----
|
146
153
|
time: 1D time array.
|
147
154
|
time_new: 1D new time array.
|
148
155
|
|
149
156
|
Notes:
|
157
|
+
-----
|
150
158
|
The result is masked if the bin contains masked values.
|
151
159
|
"""
|
152
160
|
data_as_float = self.data.astype(float)
|
153
|
-
|
161
|
+
data_as_float = ma.masked_array(data_as_float)
|
154
162
|
self.data, _ = utils.rebin_2d(time, data_as_float, time_new, "std")
|
155
163
|
|
156
164
|
def rebin_velocity(
|
@@ -163,6 +171,7 @@ class CloudnetArray:
|
|
163
171
|
"""Rebins Doppler velocity in polar coordinates.
|
164
172
|
|
165
173
|
Args:
|
174
|
+
----
|
166
175
|
time: 1D time array.
|
167
176
|
time_new: 1D new time array.
|
168
177
|
folding_velocity: Folding velocity (m/s). Can be a float when
|
cloudnetpy/concat_lib.py
CHANGED
@@ -5,7 +5,7 @@ import numpy as np
|
|
5
5
|
from cloudnetpy.exceptions import InconsistentDataError
|
6
6
|
|
7
7
|
|
8
|
-
def truncate_netcdf_file(filename: str, output_file: str, n_profiles: int):
|
8
|
+
def truncate_netcdf_file(filename: str, output_file: str, n_profiles: int) -> None:
|
9
9
|
"""Truncates netcdf file in 'time' dimension taking only n_profiles.
|
10
10
|
Useful for creating small files for tests.
|
11
11
|
"""
|
@@ -13,7 +13,7 @@ def truncate_netcdf_file(filename: str, output_file: str, n_profiles: int):
|
|
13
13
|
netCDF4.Dataset(filename, "r") as nc,
|
14
14
|
netCDF4.Dataset(output_file, "w", format=nc.data_model) as nc_new,
|
15
15
|
):
|
16
|
-
for dim in nc.dimensions
|
16
|
+
for dim in nc.dimensions:
|
17
17
|
dim_len = None if dim == "time" else nc.dimensions[dim].size
|
18
18
|
nc_new.createDimension(dim, dim_len)
|
19
19
|
for attr in nc.ncattrs():
|
@@ -24,7 +24,11 @@ def truncate_netcdf_file(filename: str, output_file: str, n_profiles: int):
|
|
24
24
|
dimensions = nc.variables[key].dimensions
|
25
25
|
fill_value = getattr(nc.variables[key], "_FillValue", None)
|
26
26
|
var = nc_new.createVariable(
|
27
|
-
key,
|
27
|
+
key,
|
28
|
+
array.dtype,
|
29
|
+
dimensions,
|
30
|
+
zlib=True,
|
31
|
+
fill_value=fill_value,
|
28
32
|
)
|
29
33
|
if dimensions and "time" in dimensions[0]:
|
30
34
|
if array.ndim == 1:
|
@@ -43,13 +47,16 @@ def update_nc(old_file: str, new_file: str) -> int:
|
|
43
47
|
"""Appends data to existing netCDF file.
|
44
48
|
|
45
49
|
Args:
|
50
|
+
----
|
46
51
|
old_file: Filename of an existing netCDF file.
|
47
52
|
new_file: Filename of a new file whose data will be appended to the end.
|
48
53
|
|
49
54
|
Returns:
|
55
|
+
-------
|
50
56
|
1 = success, 0 = failed to add new data.
|
51
57
|
|
52
58
|
Notes:
|
59
|
+
-----
|
53
60
|
Requires 'time' variable with unlimited dimension.
|
54
61
|
|
55
62
|
"""
|
@@ -79,6 +86,7 @@ def concatenate_files(
|
|
79
86
|
"""Concatenate netCDF files in one dimension.
|
80
87
|
|
81
88
|
Args:
|
89
|
+
----
|
82
90
|
filenames: List of files to be concatenated.
|
83
91
|
output_file: Output file name.
|
84
92
|
concat_dimension: Dimension name for concatenation. Default is 'time'.
|
@@ -90,6 +98,7 @@ def concatenate_files(
|
|
90
98
|
another (value from the first file is saved).
|
91
99
|
|
92
100
|
Notes:
|
101
|
+
-----
|
93
102
|
Arrays without 'concat_dimension', scalars, and global attributes will be taken
|
94
103
|
from the first file. Groups, possibly present in a NETCDF4 formatted file,
|
95
104
|
are ignored.
|
@@ -105,7 +114,10 @@ class _Concat:
|
|
105
114
|
common_variables: set[str]
|
106
115
|
|
107
116
|
def __init__(
|
108
|
-
self,
|
117
|
+
self,
|
118
|
+
filenames: list,
|
119
|
+
output_file: str,
|
120
|
+
concat_dimension: str = "time",
|
109
121
|
):
|
110
122
|
self.filenames = sorted(filenames)
|
111
123
|
self.concat_dimension = concat_dimension
|
@@ -114,7 +126,7 @@ class _Concat:
|
|
114
126
|
self.concatenated_file = self._init_output_file(output_file)
|
115
127
|
self.common_variables = set()
|
116
128
|
|
117
|
-
def get_common_variables(self):
|
129
|
+
def get_common_variables(self) -> None:
|
118
130
|
"""Finds variables which should have the same values in all files."""
|
119
131
|
for key, value in self.first_file.variables.items():
|
120
132
|
if self.concat_dimension not in value.dimensions:
|
@@ -132,7 +144,7 @@ class _Concat:
|
|
132
144
|
variables: list | None,
|
133
145
|
ignore: list | None,
|
134
146
|
allow_vary: list | None,
|
135
|
-
):
|
147
|
+
) -> None:
|
136
148
|
"""Concatenates data arrays."""
|
137
149
|
self._write_initial_data(variables, ignore)
|
138
150
|
if len(self.filenames) > 1:
|
@@ -140,7 +152,7 @@ class _Concat:
|
|
140
152
|
self._append_data(filename, allow_vary)
|
141
153
|
|
142
154
|
def _write_initial_data(self, variables: list | None, ignore: list | None) -> None:
|
143
|
-
for key in self.first_file.variables
|
155
|
+
for key in self.first_file.variables:
|
144
156
|
if (
|
145
157
|
variables is not None
|
146
158
|
and key not in variables
|
@@ -151,7 +163,8 @@ class _Concat:
|
|
151
163
|
if ignore and key in ignore:
|
152
164
|
continue
|
153
165
|
|
154
|
-
|
166
|
+
auto_scale = False
|
167
|
+
self.first_file[key].set_auto_scale(auto_scale)
|
155
168
|
array = self.first_file[key][:]
|
156
169
|
dimensions = self.first_file[key].dimensions
|
157
170
|
fill_value = getattr(self.first_file[key], "_FillValue", None)
|
@@ -164,25 +177,28 @@ class _Concat:
|
|
164
177
|
shuffle=False,
|
165
178
|
fill_value=fill_value,
|
166
179
|
)
|
167
|
-
|
180
|
+
auto_scale = False
|
181
|
+
var.set_auto_scale(auto_scale)
|
168
182
|
var[:] = array
|
169
183
|
_copy_attributes(self.first_file[key], var)
|
170
184
|
|
171
185
|
def _append_data(self, filename: str, allow_vary: list | None) -> None:
|
172
186
|
with netCDF4.Dataset(filename) as file:
|
173
|
-
|
187
|
+
auto_scale = False
|
188
|
+
file.set_auto_scale(auto_scale)
|
174
189
|
ind0 = len(self.concatenated_file.variables[self.concat_dimension])
|
175
190
|
ind1 = ind0 + len(file.variables[self.concat_dimension])
|
176
|
-
for key in self.concatenated_file.variables
|
191
|
+
for key in self.concatenated_file.variables:
|
177
192
|
array = file[key][:]
|
178
193
|
if key in self.common_variables:
|
179
194
|
if allow_vary is not None and key in allow_vary:
|
180
195
|
continue
|
181
196
|
if not np.array_equal(self.first_file[key][:], array):
|
182
|
-
|
197
|
+
msg = (
|
183
198
|
f"Inconsistent values in variable '{key}' between "
|
184
199
|
f"files '{self.first_filename}' and '{filename}'"
|
185
200
|
)
|
201
|
+
raise InconsistentDataError(msg)
|
186
202
|
continue
|
187
203
|
if array.ndim == 0:
|
188
204
|
continue
|
@@ -196,7 +212,7 @@ class _Concat:
|
|
196
212
|
"NETCDF4" if self.first_file.data_model == "NETCDF4" else "NETCDF4_CLASSIC"
|
197
213
|
)
|
198
214
|
nc = netCDF4.Dataset(output_file, "w", format=data_model)
|
199
|
-
for dim in self.first_file.dimensions
|
215
|
+
for dim in self.first_file.dimensions:
|
200
216
|
dim_len = (
|
201
217
|
None
|
202
218
|
if dim == self.concat_dimension
|
@@ -205,7 +221,7 @@ class _Concat:
|
|
205
221
|
nc.createDimension(dim, dim_len)
|
206
222
|
return nc
|
207
223
|
|
208
|
-
def _close(self):
|
224
|
+
def _close(self) -> None:
|
209
225
|
self.first_file.close()
|
210
226
|
self.concatenated_file.close()
|
211
227
|
|
@@ -223,11 +239,18 @@ def _copy_attributes(source: netCDF4.Dataset, target: netCDF4.Dataset) -> None:
|
|
223
239
|
setattr(target, attr, value)
|
224
240
|
|
225
241
|
|
226
|
-
def _find_valid_time_indices(
|
242
|
+
def _find_valid_time_indices(
|
243
|
+
nc_old: netCDF4.Dataset,
|
244
|
+
nc_new: netCDF4.Dataset,
|
245
|
+
) -> np.ndarray:
|
227
246
|
return np.where(nc_new.variables["time"][:] > nc_old.variables["time"][-1])[0]
|
228
247
|
|
229
248
|
|
230
|
-
def _update_fields(
|
249
|
+
def _update_fields(
|
250
|
+
nc_old: netCDF4.Dataset,
|
251
|
+
nc_new: netCDF4.Dataset,
|
252
|
+
valid_ind: np.ndarray,
|
253
|
+
) -> None:
|
231
254
|
ind0 = len(nc_old.variables["time"])
|
232
255
|
idx = [ind0 + x for x in valid_ind]
|
233
256
|
concat_dimension = nc_old.variables["time"].dimensions[0]
|