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.
Files changed (95) hide show
  1. cloudnetpy/categorize/atmos.py +46 -14
  2. cloudnetpy/categorize/atmos_utils.py +11 -1
  3. cloudnetpy/categorize/categorize.py +38 -21
  4. cloudnetpy/categorize/classify.py +31 -9
  5. cloudnetpy/categorize/containers.py +19 -7
  6. cloudnetpy/categorize/droplet.py +24 -8
  7. cloudnetpy/categorize/falling.py +17 -7
  8. cloudnetpy/categorize/freezing.py +19 -5
  9. cloudnetpy/categorize/insects.py +27 -14
  10. cloudnetpy/categorize/lidar.py +38 -36
  11. cloudnetpy/categorize/melting.py +19 -9
  12. cloudnetpy/categorize/model.py +28 -9
  13. cloudnetpy/categorize/mwr.py +4 -2
  14. cloudnetpy/categorize/radar.py +58 -22
  15. cloudnetpy/cloudnetarray.py +15 -6
  16. cloudnetpy/concat_lib.py +39 -16
  17. cloudnetpy/constants.py +7 -0
  18. cloudnetpy/datasource.py +39 -19
  19. cloudnetpy/instruments/basta.py +6 -2
  20. cloudnetpy/instruments/campbell_scientific.py +33 -16
  21. cloudnetpy/instruments/ceilo.py +30 -13
  22. cloudnetpy/instruments/ceilometer.py +76 -37
  23. cloudnetpy/instruments/cl61d.py +8 -3
  24. cloudnetpy/instruments/cloudnet_instrument.py +2 -1
  25. cloudnetpy/instruments/copernicus.py +27 -14
  26. cloudnetpy/instruments/disdrometer/common.py +51 -32
  27. cloudnetpy/instruments/disdrometer/parsivel.py +79 -48
  28. cloudnetpy/instruments/disdrometer/thies.py +10 -6
  29. cloudnetpy/instruments/galileo.py +23 -12
  30. cloudnetpy/instruments/hatpro.py +27 -11
  31. cloudnetpy/instruments/instruments.py +4 -1
  32. cloudnetpy/instruments/lufft.py +20 -11
  33. cloudnetpy/instruments/mira.py +60 -49
  34. cloudnetpy/instruments/mrr.py +31 -20
  35. cloudnetpy/instruments/nc_lidar.py +15 -6
  36. cloudnetpy/instruments/nc_radar.py +31 -22
  37. cloudnetpy/instruments/pollyxt.py +36 -21
  38. cloudnetpy/instruments/radiometrics.py +32 -18
  39. cloudnetpy/instruments/rpg.py +48 -22
  40. cloudnetpy/instruments/rpg_reader.py +39 -30
  41. cloudnetpy/instruments/vaisala.py +39 -27
  42. cloudnetpy/instruments/weather_station.py +15 -11
  43. cloudnetpy/metadata.py +3 -1
  44. cloudnetpy/model_evaluation/file_handler.py +31 -21
  45. cloudnetpy/model_evaluation/metadata.py +3 -1
  46. cloudnetpy/model_evaluation/model_metadata.py +1 -1
  47. cloudnetpy/model_evaluation/plotting/plot_tools.py +20 -15
  48. cloudnetpy/model_evaluation/plotting/plotting.py +114 -64
  49. cloudnetpy/model_evaluation/products/advance_methods.py +48 -28
  50. cloudnetpy/model_evaluation/products/grid_methods.py +44 -19
  51. cloudnetpy/model_evaluation/products/model_products.py +22 -18
  52. cloudnetpy/model_evaluation/products/observation_products.py +15 -9
  53. cloudnetpy/model_evaluation/products/product_resampling.py +14 -4
  54. cloudnetpy/model_evaluation/products/tools.py +16 -7
  55. cloudnetpy/model_evaluation/statistics/statistical_methods.py +28 -15
  56. cloudnetpy/model_evaluation/tests/e2e/conftest.py +3 -3
  57. cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +9 -5
  58. cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +14 -13
  59. cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +9 -5
  60. cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +14 -13
  61. cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +9 -5
  62. cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +14 -13
  63. cloudnetpy/model_evaluation/tests/unit/conftest.py +11 -11
  64. cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +33 -27
  65. cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +83 -83
  66. cloudnetpy/model_evaluation/tests/unit/test_model_products.py +23 -21
  67. cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +24 -25
  68. cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +40 -39
  69. cloudnetpy/model_evaluation/tests/unit/test_plotting.py +12 -11
  70. cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +30 -30
  71. cloudnetpy/model_evaluation/tests/unit/test_tools.py +18 -17
  72. cloudnetpy/model_evaluation/utils.py +3 -2
  73. cloudnetpy/output.py +45 -19
  74. cloudnetpy/plotting/plot_meta.py +35 -11
  75. cloudnetpy/plotting/plotting.py +172 -104
  76. cloudnetpy/products/classification.py +20 -8
  77. cloudnetpy/products/der.py +25 -10
  78. cloudnetpy/products/drizzle.py +41 -26
  79. cloudnetpy/products/drizzle_error.py +10 -5
  80. cloudnetpy/products/drizzle_tools.py +43 -24
  81. cloudnetpy/products/ier.py +10 -5
  82. cloudnetpy/products/iwc.py +16 -9
  83. cloudnetpy/products/lwc.py +34 -12
  84. cloudnetpy/products/mwr_multi.py +4 -1
  85. cloudnetpy/products/mwr_single.py +4 -1
  86. cloudnetpy/products/product_tools.py +33 -10
  87. cloudnetpy/utils.py +175 -74
  88. cloudnetpy/version.py +1 -1
  89. {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/METADATA +11 -10
  90. cloudnetpy-1.55.22.dist-info/RECORD +114 -0
  91. docs/source/conf.py +2 -2
  92. cloudnetpy-1.55.20.dist-info/RECORD +0 -114
  93. {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/LICENSE +0 -0
  94. {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/WHEEL +0 -0
  95. {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/top_level.txt +0 -0
@@ -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
- type (str): Model type, e.g. 'gdas1' or 'ecwmf'.
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 + ("q", "uwind", "vwind")
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.type = _find_model_type(model_file)
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(zip(self.model_heights, data_in)):
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, time_grid: np.ndarray, height_grid: np.ndarray
82
- ) -> None:
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, self.mean_height, array, time_grid, height_grid
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
- raise ModelDataError("No 'height' variable in the model file.") from err
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
- raise ValueError("Unknown model type")
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:
@@ -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
- g2kg = 1e-3
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"
@@ -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
- type (str): Type of the radar, copied from the global attribute
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.type = getattr(self.dataset, "source", "")
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, 1, min_coverage=0.5, z_limit=10, distance=4, n_blocks=100
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, 0, min_coverage=0.3, z_limit=-30, distance=3, n_blocks=20
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
- f"Filtered {n_vertical} vertical and {n_horizontal} horizontal stripes "
132
- f"from radar data using {variable}"
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, attenuations: dict, classification: ClassificationResult
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
- seconds_in_hour = 3600
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
- assert self.height is not None
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
- raise RuntimeError("Unable to determine folding velocity")
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, len(self.sequence_indices[0])
337
+ self.folding_velocity,
338
+ len(self.sequence_indices[0]),
310
339
  )
311
340
  else:
312
- assert isinstance(folding_velocity, list)
313
- assert isinstance(self.folding_velocity, np.ndarray)
314
- for indices, velocity in zip(self.sequence_indices, self.folding_velocity):
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
- return float(prf * constants.c / (4 * radar_frequency * ghz_to_hz))
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))
@@ -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 | int,
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
- assert isinstance(self.data, ma.MaskedArray)
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
- (int, float, np.float32, np.int8, np.float64, np.int32, np.uint16),
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
- raise ValueError(f"Incorrect CloudnetArray input: {self.variable}")
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
- assert isinstance(self.data, ma.MaskedArray)
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
- assert isinstance(data_as_float, ma.MaskedArray)
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.keys():
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, array.dtype, dimensions, zlib=True, fill_value=fill_value
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, filenames: list, output_file: str, concat_dimension: str = "time"
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.keys():
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
- self.first_file[key].set_auto_scale(False)
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
- var.set_auto_scale(False)
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
- file.set_auto_scale(False)
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.keys():
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
- raise InconsistentDataError(
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.keys():
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(nc_old: netCDF4.Dataset, nc_new: netCDF4.Dataset):
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(nc_old: netCDF4.Dataset, nc_new: netCDF4.Dataset, valid_ind: list):
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]
cloudnetpy/constants.py CHANGED
@@ -18,3 +18,10 @@ RS: Final = 287.058
18
18
 
19
19
  # ice density kg m-3
20
20
  RHO_ICE: Final = 917
21
+
22
+ # other
23
+ SEC_IN_MINUTE: Final = 60
24
+ SEC_IN_HOUR: Final = 3600
25
+ SEC_IN_DAY: Final = 86400
26
+ MM_TO_M: Final = 1e-3
27
+ G_TO_KG: Final = 1e-3