cloudnetpy 1.49.9__py3-none-any.whl → 1.87.3__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 (116) hide show
  1. cloudnetpy/categorize/__init__.py +1 -2
  2. cloudnetpy/categorize/atmos_utils.py +297 -67
  3. cloudnetpy/categorize/attenuation.py +31 -0
  4. cloudnetpy/categorize/attenuations/__init__.py +37 -0
  5. cloudnetpy/categorize/attenuations/gas_attenuation.py +30 -0
  6. cloudnetpy/categorize/attenuations/liquid_attenuation.py +84 -0
  7. cloudnetpy/categorize/attenuations/melting_attenuation.py +78 -0
  8. cloudnetpy/categorize/attenuations/rain_attenuation.py +84 -0
  9. cloudnetpy/categorize/categorize.py +332 -156
  10. cloudnetpy/categorize/classify.py +127 -125
  11. cloudnetpy/categorize/containers.py +107 -76
  12. cloudnetpy/categorize/disdrometer.py +40 -0
  13. cloudnetpy/categorize/droplet.py +23 -21
  14. cloudnetpy/categorize/falling.py +53 -24
  15. cloudnetpy/categorize/freezing.py +25 -12
  16. cloudnetpy/categorize/insects.py +35 -23
  17. cloudnetpy/categorize/itu.py +243 -0
  18. cloudnetpy/categorize/lidar.py +36 -41
  19. cloudnetpy/categorize/melting.py +34 -26
  20. cloudnetpy/categorize/model.py +84 -37
  21. cloudnetpy/categorize/mwr.py +18 -14
  22. cloudnetpy/categorize/radar.py +215 -102
  23. cloudnetpy/cli.py +578 -0
  24. cloudnetpy/cloudnetarray.py +43 -89
  25. cloudnetpy/concat_lib.py +218 -78
  26. cloudnetpy/constants.py +28 -10
  27. cloudnetpy/datasource.py +61 -86
  28. cloudnetpy/exceptions.py +49 -20
  29. cloudnetpy/instruments/__init__.py +5 -0
  30. cloudnetpy/instruments/basta.py +29 -12
  31. cloudnetpy/instruments/bowtie.py +135 -0
  32. cloudnetpy/instruments/ceilo.py +138 -115
  33. cloudnetpy/instruments/ceilometer.py +164 -80
  34. cloudnetpy/instruments/cl61d.py +21 -5
  35. cloudnetpy/instruments/cloudnet_instrument.py +74 -36
  36. cloudnetpy/instruments/copernicus.py +108 -30
  37. cloudnetpy/instruments/da10.py +54 -0
  38. cloudnetpy/instruments/disdrometer/common.py +126 -223
  39. cloudnetpy/instruments/disdrometer/parsivel.py +453 -94
  40. cloudnetpy/instruments/disdrometer/thies.py +254 -87
  41. cloudnetpy/instruments/fd12p.py +201 -0
  42. cloudnetpy/instruments/galileo.py +65 -23
  43. cloudnetpy/instruments/hatpro.py +123 -49
  44. cloudnetpy/instruments/instruments.py +113 -1
  45. cloudnetpy/instruments/lufft.py +39 -17
  46. cloudnetpy/instruments/mira.py +268 -61
  47. cloudnetpy/instruments/mrr.py +187 -0
  48. cloudnetpy/instruments/nc_lidar.py +19 -8
  49. cloudnetpy/instruments/nc_radar.py +109 -55
  50. cloudnetpy/instruments/pollyxt.py +135 -51
  51. cloudnetpy/instruments/radiometrics.py +313 -59
  52. cloudnetpy/instruments/rain_e_h3.py +171 -0
  53. cloudnetpy/instruments/rpg.py +321 -189
  54. cloudnetpy/instruments/rpg_reader.py +74 -40
  55. cloudnetpy/instruments/toa5.py +49 -0
  56. cloudnetpy/instruments/vaisala.py +95 -343
  57. cloudnetpy/instruments/weather_station.py +774 -105
  58. cloudnetpy/metadata.py +90 -19
  59. cloudnetpy/model_evaluation/file_handler.py +55 -52
  60. cloudnetpy/model_evaluation/metadata.py +46 -20
  61. cloudnetpy/model_evaluation/model_metadata.py +1 -1
  62. cloudnetpy/model_evaluation/plotting/plot_tools.py +32 -37
  63. cloudnetpy/model_evaluation/plotting/plotting.py +327 -117
  64. cloudnetpy/model_evaluation/products/advance_methods.py +92 -83
  65. cloudnetpy/model_evaluation/products/grid_methods.py +88 -63
  66. cloudnetpy/model_evaluation/products/model_products.py +43 -35
  67. cloudnetpy/model_evaluation/products/observation_products.py +41 -35
  68. cloudnetpy/model_evaluation/products/product_resampling.py +17 -7
  69. cloudnetpy/model_evaluation/products/tools.py +29 -20
  70. cloudnetpy/model_evaluation/statistics/statistical_methods.py +30 -20
  71. cloudnetpy/model_evaluation/tests/e2e/conftest.py +3 -3
  72. cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +9 -5
  73. cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +15 -14
  74. cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +9 -5
  75. cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +15 -14
  76. cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +9 -5
  77. cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +15 -14
  78. cloudnetpy/model_evaluation/tests/unit/conftest.py +42 -41
  79. cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +41 -48
  80. cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +216 -194
  81. cloudnetpy/model_evaluation/tests/unit/test_model_products.py +23 -21
  82. cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +37 -38
  83. cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +43 -40
  84. cloudnetpy/model_evaluation/tests/unit/test_plotting.py +30 -36
  85. cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +68 -31
  86. cloudnetpy/model_evaluation/tests/unit/test_tools.py +33 -26
  87. cloudnetpy/model_evaluation/utils.py +2 -1
  88. cloudnetpy/output.py +170 -111
  89. cloudnetpy/plotting/__init__.py +2 -1
  90. cloudnetpy/plotting/plot_meta.py +562 -822
  91. cloudnetpy/plotting/plotting.py +1142 -704
  92. cloudnetpy/products/__init__.py +1 -0
  93. cloudnetpy/products/classification.py +370 -88
  94. cloudnetpy/products/der.py +85 -55
  95. cloudnetpy/products/drizzle.py +77 -34
  96. cloudnetpy/products/drizzle_error.py +15 -11
  97. cloudnetpy/products/drizzle_tools.py +79 -59
  98. cloudnetpy/products/epsilon.py +211 -0
  99. cloudnetpy/products/ier.py +27 -50
  100. cloudnetpy/products/iwc.py +55 -48
  101. cloudnetpy/products/lwc.py +96 -70
  102. cloudnetpy/products/mwr_tools.py +186 -0
  103. cloudnetpy/products/product_tools.py +170 -128
  104. cloudnetpy/utils.py +455 -240
  105. cloudnetpy/version.py +2 -2
  106. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/METADATA +44 -40
  107. cloudnetpy-1.87.3.dist-info/RECORD +127 -0
  108. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/WHEEL +1 -1
  109. cloudnetpy-1.87.3.dist-info/entry_points.txt +2 -0
  110. docs/source/conf.py +2 -2
  111. cloudnetpy/categorize/atmos.py +0 -361
  112. cloudnetpy/products/mwr_multi.py +0 -68
  113. cloudnetpy/products/mwr_single.py +0 -75
  114. cloudnetpy-1.49.9.dist-info/RECORD +0 -112
  115. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info/licenses}/LICENSE +0 -0
  116. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/top_level.txt +0 -0
@@ -1,15 +1,22 @@
1
+ import logging
2
+ from os import PathLike
3
+ from pathlib import Path
1
4
  from typing import BinaryIO, Literal
2
5
 
3
6
  import numpy as np
7
+ import numpy.typing as npt
4
8
  from numpy import ma
5
9
  from numpy.lib import recfunctions as rfn
6
10
  from rpgpy import read_rpg
7
11
 
12
+ from cloudnetpy.constants import G_TO_KG
13
+ from cloudnetpy.exceptions import ValidTimeStampError
14
+
8
15
 
9
16
  class Fmcw94Bin:
10
17
  """RPG Cloud Radar Level 1 data reader."""
11
18
 
12
- def __init__(self, filename):
19
+ def __init__(self, filename: str | PathLike) -> None:
13
20
  self.filename = filename
14
21
  self.header, self.data = read_rpg(filename)
15
22
 
@@ -31,7 +38,7 @@ class Fmcw94Bin:
31
38
  "CGProg": "program_number",
32
39
  "ModelNo": "model_number",
33
40
  "ProgName": "_program_name",
34
- "CustName": "_customer_name",
41
+ "CustName": "customer_name",
35
42
  "Freq": "radar_frequency",
36
43
  "AntSep": "antenna_separation",
37
44
  "AntDia": "antenna_diameter",
@@ -67,8 +74,8 @@ class Fmcw94Bin:
67
74
  "QF": "quality_flag",
68
75
  "RR": "rainfall_rate",
69
76
  "RelHum": "relative_humidity",
70
- "EnvTemp": "temperature",
71
- "BaroP": "pressure",
77
+ "EnvTemp": "air_temperature",
78
+ "BaroP": "air_pressure",
72
79
  "WS": "wind_speed",
73
80
  "WD": "wind_direction",
74
81
  "DDVolt": "voltage",
@@ -97,7 +104,7 @@ class Fmcw94Bin:
97
104
  self.replace_keys(self.data, data_keymap)
98
105
 
99
106
  @staticmethod
100
- def replace_keys(d: dict, keymap: dict):
107
+ def replace_keys(d: dict, keymap: dict) -> None:
101
108
  for key in d.copy():
102
109
  if key in keymap:
103
110
  new_key = keymap[key]
@@ -105,20 +112,22 @@ class Fmcw94Bin:
105
112
 
106
113
 
107
114
  def _read_from_file(
108
- file: BinaryIO, fields: list[tuple[str, str]], count: int = 1
115
+ file: BinaryIO,
116
+ fields: list[tuple[str, str]],
117
+ count: int | None = None,
109
118
  ) -> ma.MaskedArray:
110
- arr = np.fromfile(file, np.dtype(fields), count)
119
+ arr = np.fromfile(file, np.dtype(fields), 1 if count is None else count)
111
120
  masked_arr = ma.array(arr)
112
- if count == 1:
121
+ if count is None:
113
122
  return masked_arr[0]
114
123
  return masked_arr
115
124
 
116
125
 
117
126
  def _decode_angles(
118
- x: np.ndarray, version: Literal[1, 2]
119
- ) -> tuple[np.ndarray, np.ndarray]:
120
- """
121
- Decode elevation and azimuth angles.
127
+ x: npt.NDArray,
128
+ version: Literal[1, 2],
129
+ ) -> tuple[npt.NDArray, npt.NDArray]:
130
+ """Decode elevation and azimuth angles.
122
131
 
123
132
  >>> _decode_angles(np.array([1267438.5]), version=1)
124
133
  (array([138.5]), array([267.4]))
@@ -128,7 +137,6 @@ def _decode_angles(
128
137
  Based on `interpret_angle` from mwr_raw2l1 licensed under BSD 3-Clause:
129
138
  https://github.com/MeteoSwiss/mwr_raw2l1/blob/0738490d22f77138cdf9329bf102f319c78be584/mwr_raw2l1/readers/reader_rpg_helpers.py#L30
130
139
  """
131
-
132
140
  if version == 1:
133
141
  # Description in the manual is quite unclear so here's an improved one:
134
142
  # Ang=sign(El)*(|El|+1000*Az), -90°<=El<100°, 0°<=Az<360°. If El>=100°
@@ -150,9 +158,8 @@ def _decode_angles(
150
158
  ele = np.sign(x) * (np.abs(x) // 1e5) / 100
151
159
  azi = (np.abs(x) - np.abs(ele) * 1e7) / 100
152
160
  else:
153
- raise NotImplementedError(
154
- f"Known versions for angle encoding are 1 and 2, but received {version}"
155
- )
161
+ msg = f"Known versions for angle encoding are 1 and 2, but received {version}"
162
+ raise NotImplementedError(msg)
156
163
 
157
164
  return ele, azi
158
165
 
@@ -177,7 +184,7 @@ class HatproBin:
177
184
  QUALITY_MEDIUM = 2
178
185
  QUALITY_LOW = 3
179
186
 
180
- def __init__(self, filename):
187
+ def __init__(self, filename: Path) -> None:
181
188
  self.filename = filename
182
189
  with open(self.filename, "rb") as file:
183
190
  self._read_header(file)
@@ -185,21 +192,39 @@ class HatproBin:
185
192
  self._remove_duplicate_timestamps()
186
193
  self._add_zenith_angle()
187
194
 
188
- def screen_bad_profiles(self):
195
+ def screen_bad_profiles(self) -> None:
196
+ # In Limassol 2025-06-06 for instance, LWP is all zero but IWV has
197
+ # values.
198
+ if ma.all(self.data[self.variable] == 0):
199
+ self.data[self.variable][:] = ma.masked
200
+ return
201
+ # Screen by quality flag.
189
202
  is_bad = self.data["_quality_flag"] & 0b110 == self.QUALITY_LOW << 1
203
+ n_bad = np.count_nonzero(is_bad)
204
+ if n_bad == len(is_bad):
205
+ msg = "All data are low quality"
206
+ raise ValidTimeStampError(msg)
207
+ if n_bad:
208
+ percentage = round(100 * n_bad / len(is_bad))
209
+ logging.info(
210
+ "Screening %s %% (%s/%s) data points with low quality",
211
+ percentage,
212
+ n_bad,
213
+ len(is_bad),
214
+ )
190
215
  self.data[self.variable][is_bad] = ma.masked
191
216
 
192
- def _remove_duplicate_timestamps(self):
217
+ def _remove_duplicate_timestamps(self) -> None:
193
218
  _, ind = np.unique(self.data["time"], return_index=True)
194
219
  self.data = self.data[ind]
195
220
 
196
- def _read_header(self, file: BinaryIO):
197
- raise NotImplementedError()
221
+ def _read_header(self, file: BinaryIO) -> None:
222
+ raise NotImplementedError
198
223
 
199
- def _read_data(self, file: BinaryIO):
200
- raise NotImplementedError()
224
+ def _read_data(self, file: BinaryIO) -> None:
225
+ raise NotImplementedError
201
226
 
202
- def _add_zenith_angle(self):
227
+ def _add_zenith_angle(self) -> None:
203
228
  ele, _azi = _decode_angles(self.data["_instrument_angles"], self.version)
204
229
  self.data = rfn.append_fields(self.data, "zenith_angle", 90 - ele)
205
230
 
@@ -209,7 +234,7 @@ class HatproBinLwp(HatproBin):
209
234
 
210
235
  variable = "lwp"
211
236
 
212
- def _read_header(self, file):
237
+ def _read_header(self, file: BinaryIO) -> None:
213
238
  self.header = _read_from_file(
214
239
  file,
215
240
  [
@@ -226,9 +251,10 @@ class HatproBinLwp(HatproBin):
226
251
  elif self.header["file_code"] == 934501000:
227
252
  self.version = 2
228
253
  else:
229
- raise ValueError(f'Unknown HATPRO version. {self.header["file_code"]}')
254
+ msg = f"Unknown HATPRO version. {self.header['file_code']}"
255
+ raise ValueError(msg)
230
256
 
231
- def _read_data(self, file):
257
+ def _read_data(self, file: BinaryIO) -> None:
232
258
  self.data = _read_from_file(
233
259
  file,
234
260
  [
@@ -237,9 +263,9 @@ class HatproBinLwp(HatproBin):
237
263
  ("lwp", "<f"),
238
264
  ("_instrument_angles", "<f" if self.version == 1 else "<i4"),
239
265
  ],
240
- self.header["_n_samples"],
266
+ int(self.header["_n_samples"]),
241
267
  )
242
- self.data["lwp"] *= 1e-3 # Convert from g/m^2 to kg/m^2
268
+ self.data["lwp"] *= G_TO_KG
243
269
 
244
270
 
245
271
  class HatproBinIwv(HatproBin):
@@ -247,7 +273,7 @@ class HatproBinIwv(HatproBin):
247
273
 
248
274
  variable = "iwv"
249
275
 
250
- def _read_header(self, file):
276
+ def _read_header(self, file: BinaryIO) -> None:
251
277
  self.header = _read_from_file(
252
278
  file,
253
279
  [
@@ -264,9 +290,10 @@ class HatproBinIwv(HatproBin):
264
290
  elif self.header["file_code"] == 594811000:
265
291
  self.version = 2
266
292
  else:
267
- raise ValueError(f'Unknown HATPRO version. {self.header["file_code"]}')
293
+ msg = f"Unknown HATPRO version. {self.header['file_code']}"
294
+ raise ValueError(msg)
268
295
 
269
- def _read_data(self, file):
296
+ def _read_data(self, file: BinaryIO) -> None:
270
297
  self.data = _read_from_file(
271
298
  file,
272
299
  [
@@ -275,17 +302,17 @@ class HatproBinIwv(HatproBin):
275
302
  ("iwv", "<f"),
276
303
  ("_instrument_angles", "<f" if self.version == 1 else "<i4"),
277
304
  ],
278
- self.header["_n_samples"],
305
+ int(self.header["_n_samples"]),
279
306
  )
280
307
 
281
308
 
282
309
  class HatproBinCombined:
283
310
  """Combine HATPRO objects that share values of the given dimensions."""
284
311
 
285
- header: dict[str, np.ndarray]
286
- data: dict[str, np.ndarray]
312
+ header: dict[str, npt.NDArray]
313
+ data: dict[str, npt.NDArray]
287
314
 
288
- def __init__(self, files: list[HatproBin]):
315
+ def __init__(self, files: list[HatproBin]) -> None:
289
316
  self.header = {}
290
317
  if len(files) == 1:
291
318
  arr = files[0].data
@@ -298,16 +325,23 @@ class HatproBinCombined:
298
325
  _combine_values(arr["zenith_angle1"], arr["zenith_angle2"]),
299
326
  )
300
327
  # Workaround because rfn.drop_fields seems to incorrectly drop mask...
301
- # arr = rfn.drop_fields(arr, ["zenith_angle1", "zenith_angle2"])
302
328
  arr = rfn.rename_fields(
303
- arr, {"zenith_angle1": "_tmp1", "zenith_angle2": "_tmp2"}
329
+ arr,
330
+ {"zenith_angle1": "_tmp1", "zenith_angle2": "_tmp2"},
304
331
  )
305
332
  else:
306
- raise NotImplementedError("Only implemented up to 2 files")
333
+ msg = "Only implemented up to 2 files"
334
+ raise NotImplementedError(msg)
335
+
336
+ if arr.dtype.fields is None:
337
+ msg = "Data has no fields"
338
+ raise ValueError(msg)
339
+
307
340
  self.data = {field: arr[field] for field in arr.dtype.fields}
308
341
 
309
342
 
310
343
  def _combine_values(arr1: ma.MaskedArray, arr2: ma.MaskedArray) -> ma.MaskedArray:
311
344
  if not ma.allequal(arr1, arr2):
312
- raise ValueError("Inconsistent values")
345
+ msg = "Inconsistent values"
346
+ raise ValueError(msg)
313
347
  return ma.where(~arr1.mask, arr1, arr2)
@@ -0,0 +1,49 @@
1
+ import csv
2
+ import datetime
3
+ from os import PathLike
4
+ from typing import Any
5
+
6
+
7
+ def read_toa5(
8
+ filename: str | PathLike,
9
+ ) -> tuple[dict[str, str], dict[str, str], list[dict[str, Any]]]:
10
+ """Read ASCII data from Campbell Scientific datalogger such as CR1000.
11
+
12
+ References:
13
+ CR1000 Measurement and Control System.
14
+ https://s.campbellsci.com/documents/us/manuals/cr1000.pdf
15
+ """
16
+ with open(filename) as file:
17
+ reader = csv.reader(file)
18
+ origin_line = next(reader)
19
+ if len(origin_line) == 0 or origin_line[0] != "TOA5":
20
+ msg = "Invalid TOA5 file"
21
+ raise ValueError(msg)
22
+ header_line = next(reader)
23
+ units_line = next(reader)
24
+ process_line = next(reader)
25
+ output = []
26
+ units = dict(zip(header_line, units_line, strict=False))
27
+ process = dict(zip(header_line, process_line, strict=False))
28
+
29
+ row_template: dict[str, Any] = {}
30
+ for header in header_line:
31
+ if "(" in header:
32
+ row_template[header[: header.index("(")]] = []
33
+
34
+ for data_line in reader:
35
+ row = row_template.copy()
36
+ for key, value in zip(header_line, data_line, strict=False):
37
+ parsed_value: Any = value
38
+ if key == "TIMESTAMP":
39
+ parsed_value = datetime.datetime.strptime(
40
+ parsed_value, "%Y-%m-%d %H:%M:%S"
41
+ )
42
+ elif key == "RECORD":
43
+ parsed_value = int(parsed_value)
44
+ if "(" in key:
45
+ row[key[: key.index("(")]].append(parsed_value)
46
+ else:
47
+ row[key] = parsed_value
48
+ output.append(row)
49
+ return units, process, output