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,32 +1,39 @@
1
1
  """Module for reading / converting pollyxt data."""
2
+
3
+ import datetime
2
4
  import glob
3
5
  import logging
6
+ from collections import Counter
7
+ from collections.abc import Sequence
8
+ from fnmatch import fnmatch
9
+ from os import PathLike
10
+ from uuid import UUID
4
11
 
5
12
  import netCDF4
6
13
  import numpy as np
14
+ import numpy.typing as npt
7
15
  from numpy import ma
8
- from numpy.testing import assert_array_equal
16
+ from numpy.testing import assert_almost_equal
9
17
 
10
18
  from cloudnetpy import output, utils
11
19
  from cloudnetpy.exceptions import InconsistentDataError, ValidTimeStampError
12
20
  from cloudnetpy.instruments import instruments
13
21
  from cloudnetpy.instruments.ceilometer import Ceilometer
14
- from cloudnetpy.metadata import MetaData
15
- from cloudnetpy.utils import Epoch
22
+ from cloudnetpy.metadata import COMMON_ATTRIBUTES, MetaData
16
23
 
17
24
 
18
25
  def pollyxt2nc(
19
- input_folder: str,
20
- output_file: str,
26
+ input_data: str | PathLike | Sequence[str | PathLike],
27
+ output_file: str | PathLike,
21
28
  site_meta: dict,
22
- uuid: str | None = None,
23
- date: str | None = None,
24
- ) -> str:
25
- """
26
- Converts PollyXT Raman lidar data into Cloudnet Level 1b netCDF file.
29
+ uuid: str | UUID | None = None,
30
+ date: str | datetime.date | None = None,
31
+ ) -> UUID:
32
+ """Converts PollyXT Raman lidar data into Cloudnet Level 1b netCDF file.
27
33
 
28
34
  Args:
29
- input_folder: Path to pollyxt netCDF files.
35
+ input_data: Path to folder containing pollyxt netCDF files
36
+ or a sequence of filename paths.
30
37
  output_file: Output filename.
31
38
  site_meta: Dictionary containing information about the site with keys:
32
39
 
@@ -49,67 +56,87 @@ def pollyxt2nc(
49
56
  >>> pollyxt2nc('/path/to/files/', 'pollyxt.nc', site_meta)
50
57
 
51
58
  """
59
+ if isinstance(date, str):
60
+ date = datetime.date.fromisoformat(date)
61
+ uuid = utils.get_uuid(uuid)
52
62
  snr_limit = site_meta.get("snr_limit", 2)
53
63
  polly = PollyXt(site_meta, date)
54
- epoch = polly.fetch_data(input_folder)
64
+ epoch = polly.fetch_data(input_data)
55
65
  polly.get_date_and_time(epoch)
56
66
  polly.fetch_zenith_angle()
57
67
  polly.calc_screened_products(snr_limit)
58
68
  polly.mask_nan_values()
59
69
  polly.prepare_data()
60
- polly.data_to_cloudnet_arrays()
70
+ polly.screen_completely_masked_profiles()
71
+ polly.data_to_cloudnet_arrays(time_dtype="f8")
72
+ polly.add_site_geolocation()
61
73
  attributes = output.add_time_attribute(ATTRIBUTES, polly.date)
62
74
  output.update_attributes(polly.data, attributes)
63
75
  polly.add_snr_info("beta", snr_limit)
64
- uuid = output.save_level1b(polly, output_file, uuid)
76
+ output.save_level1b(polly, output_file, uuid)
65
77
  return uuid
66
78
 
67
79
 
68
80
  class PollyXt(Ceilometer):
69
- def __init__(self, site_meta: dict, expected_date: str | None):
81
+ def __init__(self, site_meta: dict, expected_date: datetime.date | None) -> None:
70
82
  super().__init__()
71
83
  self.site_meta = site_meta
72
84
  self.expected_date = expected_date
73
85
  self.instrument = instruments.POLLYXT
74
86
 
75
- def mask_nan_values(self):
87
+ def mask_nan_values(self) -> None:
76
88
  for array in self.data.values():
77
89
  if getattr(array, "ndim", 0) > 0:
78
90
  array[np.isnan(array)] = ma.masked
79
91
 
80
- def calc_screened_products(self, snr_limit: float = 5.0):
92
+ def calc_screened_products(self, snr_limit: float = 5.0) -> None:
81
93
  keys = ("beta", "depolarisation")
82
94
  for key in keys:
83
95
  self.data[key] = ma.masked_where(
84
- self.data["snr"] < snr_limit, self.data[f"{key}_raw"]
96
+ self.data["snr"] < snr_limit,
97
+ self.data[f"{key}_raw"],
85
98
  )
86
99
  self.data["depolarisation"][self.data["depolarisation"] > 1] = ma.masked
87
100
  self.data["depolarisation"][self.data["depolarisation"] < 0] = ma.masked
101
+ self.data["beta"][self.data["beta"] < 0] = ma.masked
88
102
  del self.data["snr"]
89
103
 
90
104
  def fetch_zenith_angle(self) -> None:
91
105
  default = 5
92
106
  self.data["zenith_angle"] = float(self.metadata.get("zenith_angle", default))
93
107
 
94
- def fetch_data(self, input_folder: str) -> Epoch:
95
- """Read input data."""
96
- bsc_files = glob.glob(f"{input_folder}/*[0-9]_att*.nc")
97
- depol_files = glob.glob(f"{input_folder}/*[0-9]_vol*.nc")
108
+ def fetch_data(
109
+ self, input_data: str | PathLike | Sequence[str | PathLike]
110
+ ) -> datetime.datetime:
111
+ att_id = "*[0-9]_att*.nc"
112
+ vol_id = "*[0-9]_vol*.nc"
113
+ if isinstance(input_data, (str, PathLike)):
114
+ bsc_files = glob.glob(f"{input_data}/{att_id}")
115
+ depol_files = glob.glob(f"{input_data}/{vol_id}")
116
+ else:
117
+ file_list = [str(f) for f in input_data]
118
+ bsc_files = [f for f in file_list if fnmatch(f, att_id)]
119
+ depol_files = [f for f in file_list if fnmatch(f, vol_id)]
120
+
98
121
  bsc_files.sort()
99
122
  depol_files.sort()
100
123
  if not bsc_files:
101
- raise RuntimeError("No pollyxt files found")
124
+ msg = "No pollyxt bsc files found"
125
+ raise RuntimeError(msg)
102
126
  if len(bsc_files) != len(depol_files):
103
- raise InconsistentDataError(
104
- "Inconsistent number of pollyxt bsc / depol files"
105
- )
106
- self.data["range"] = _read_array_from_multiple_files(
107
- bsc_files, depol_files, "height"
108
- )
109
- calibration_factors: np.ndarray = np.array([])
127
+ msg = "Inconsistent number of pollyxt bsc / depol files"
128
+ raise InconsistentDataError(msg)
129
+ bsc_files, depol_files = _fetch_files_with_same_range(bsc_files, depol_files)
130
+ if not bsc_files:
131
+ msg = "No pollyxt files with same range found"
132
+ raise InconsistentDataError(msg)
133
+ self._fetch_attributes(bsc_files[0])
134
+ with netCDF4.Dataset(bsc_files[0], "r") as nc:
135
+ self.data["range"] = nc.variables["height"][:]
136
+ calibration_factors: npt.NDArray = np.array([])
110
137
  beta_channel = self._get_valid_beta_channel(bsc_files)
111
138
  bsc_key = f"attenuated_backscatter_{beta_channel}nm"
112
- for bsc_file, depol_file in zip(bsc_files, depol_files):
139
+ for bsc_file, depol_file in zip(bsc_files, depol_files, strict=True):
113
140
  with (
114
141
  netCDF4.Dataset(bsc_file, "r") as nc_bsc,
115
142
  netCDF4.Dataset(depol_file, "r") as nc_depol,
@@ -117,19 +144,31 @@ class PollyXt(Ceilometer):
117
144
  epoch = utils.get_epoch(nc_bsc["time"].unit)
118
145
  try:
119
146
  time = np.array(
120
- _read_array_from_file_pair(nc_bsc, nc_depol, "time")
147
+ _read_array_from_file_pair(nc_bsc, nc_depol, "time"),
121
148
  )
122
149
  except AssertionError as err:
123
150
  logging.warning(
124
- f"Ignoring files '{nc_bsc}' and '{nc_depol}': {err}"
151
+ "Ignoring files '%s' and '%s': %s",
152
+ bsc_file,
153
+ depol_file,
154
+ err,
125
155
  )
126
156
  continue
127
157
  beta_raw = nc_bsc.variables[bsc_key][:]
128
158
  depol_raw = nc_depol.variables["volume_depolarization_ratio_532nm"][:]
129
- snr = nc_bsc.variables[f"SNR_{beta_channel}nm"][:]
159
+ try:
160
+ snr = nc_bsc.variables[f"SNR_{beta_channel}nm"][:]
161
+ except KeyError:
162
+ logging.warning(
163
+ "Ignoring files '%s' and '%s'",
164
+ bsc_file,
165
+ depol_file,
166
+ )
167
+ continue
130
168
  for array, key in zip(
131
169
  [beta_raw, depol_raw, time, snr],
132
170
  ["beta_raw", "depolarisation_raw", "time", "snr"],
171
+ strict=True,
133
172
  ):
134
173
  self.data = utils.append_data(self.data, key, array)
135
174
  calibration_factor = nc_bsc.variables[
@@ -137,11 +176,17 @@ class PollyXt(Ceilometer):
137
176
  ].Lidar_calibration_constant_used
138
177
  calibration_factor = np.repeat(calibration_factor, len(time))
139
178
  calibration_factors = np.concatenate(
140
- [calibration_factors, calibration_factor]
179
+ [calibration_factors, calibration_factor],
141
180
  )
142
181
  self.data["calibration_factor"] = calibration_factors
143
182
  return epoch
144
183
 
184
+ def screen_completely_masked_profiles(self) -> None:
185
+ valid_ind = ~np.all(np.ma.getmaskarray(self.data["beta_raw"]), axis=1)
186
+ for key, item in self.data.items():
187
+ if isinstance(item, np.ndarray) and item.shape[0] == len(valid_ind):
188
+ self.data[key] = item[valid_ind]
189
+
145
190
  def _get_valid_beta_channel(self, files: list) -> str:
146
191
  polly_channels = ("1064", "532", "355")
147
192
  for channel in polly_channels:
@@ -151,35 +196,70 @@ class PollyXt(Ceilometer):
151
196
  if not _only_zeros_or_masked(beta):
152
197
  if channel != polly_channels[0]:
153
198
  logging.warning(
154
- f"Using {channel}nm pollyXT channel for backscatter"
199
+ "Using %s nm pollyXT channel for backscatter",
200
+ channel,
155
201
  )
156
- self.instrument.wavelength = float(channel) # type: ignore
202
+ if self.instrument is None:
203
+ msg = "No instrument defined"
204
+ raise RuntimeError(msg)
205
+ self.instrument.wavelength = float(channel)
157
206
  return channel
158
- raise ValidTimeStampError("No functional pollyXT backscatter channels found")
207
+ msg = "No functional pollyXT backscatter channels found"
208
+ raise ValidTimeStampError(msg)
209
+
210
+ def _fetch_attributes(self, file: str) -> None:
211
+ with netCDF4.Dataset(file, "r") as nc:
212
+ if hasattr(nc, "source"):
213
+ self.serial_number = nc.source.lower()
214
+
215
+
216
+ def _fetch_files_with_same_range(
217
+ bsc_files: list[str], depol_files: list[str]
218
+ ) -> tuple[list[str], list[str]]:
219
+ def get_sum(file: str) -> float:
220
+ with netCDF4.Dataset(file, "r") as nc:
221
+ return np.sum(np.round(nc.variables["height"][:]))
222
+
223
+ bsc_sums = [get_sum(f) for f in bsc_files]
224
+ depol_sums = [get_sum(f) for f in depol_files]
225
+ all_sums = bsc_sums + depol_sums
159
226
 
227
+ filtered_sums = [item for item in all_sums if item is not ma.masked]
228
+ if not filtered_sums:
229
+ return [], []
160
230
 
161
- def _read_array_from_multiple_files(files1: list, files2: list, key) -> np.ndarray:
162
- array: np.ndarray = np.array([])
163
- for ind, (file1, file2) in enumerate(zip(files1, files2)):
164
- with netCDF4.Dataset(file1, "r") as nc1, netCDF4.Dataset(file2, "r") as nc2:
165
- array1 = _read_array_from_file_pair(nc1, nc2, key)
166
- if ind == 0:
167
- array = array1
168
- assert_array_equal(array, array1, f"Inconsistent variable '{key}'")
169
- return np.array(array)
231
+ most_common_sum = Counter(filtered_sums).most_common(1)[0][0]
232
+ valid_indices = [
233
+ i
234
+ for i, (bs, ds) in enumerate(zip(bsc_sums, depol_sums, strict=False))
235
+ if bs == most_common_sum and ds == most_common_sum
236
+ ]
237
+ if len(valid_indices) != len(bsc_files):
238
+ n_ignored = len(bsc_files) - len(valid_indices)
239
+ msg = f"Ignoring {n_ignored} file(s) with different range"
240
+ logging.warning(msg)
241
+ return (
242
+ [bsc_files[i] for i in valid_indices],
243
+ [depol_files[i] for i in valid_indices],
244
+ )
170
245
 
171
246
 
172
247
  def _read_array_from_file_pair(
173
- nc_file1: netCDF4.Dataset, nc_file2: netCDF4.Dataset, key: str
174
- ) -> np.ndarray:
248
+ nc_file1: netCDF4.Dataset,
249
+ nc_file2: netCDF4.Dataset,
250
+ key: str,
251
+ ) -> npt.NDArray:
175
252
  array1 = nc_file1.variables[key][:]
176
253
  array2 = nc_file2.variables[key][:]
177
- assert_array_equal(array1, array2, f"Inconsistent variable '{key}'")
254
+ assert_almost_equal(
255
+ array1, array2, err_msg=f"Inconsistent variable '{key}'", decimal=2
256
+ )
178
257
  return array1
179
258
 
180
259
 
181
260
  def _only_zeros_or_masked(data: ma.MaskedArray) -> bool:
182
- return ma.sum(data) == 0 or data.mask.all()
261
+ mask = ma.getmaskarray(data)
262
+ return bool(ma.sum(data) == 0 or mask.all())
183
263
 
184
264
 
185
265
  ATTRIBUTES = {
@@ -187,15 +267,19 @@ ATTRIBUTES = {
187
267
  long_name="Lidar volume linear depolarisation ratio",
188
268
  units="1",
189
269
  comment="SNR-screened lidar volume linear depolarisation ratio at 532 nm.",
270
+ dimensions=("time", "range"),
190
271
  ),
191
272
  "depolarisation_raw": MetaData(
192
273
  long_name="Lidar volume linear depolarisation ratio",
193
274
  units="1",
194
275
  comment="Non-screened lidar volume linear depolarisation ratio at 532 nm.",
276
+ dimensions=("time", "range"),
195
277
  ),
196
278
  "calibration_factor": MetaData(
197
279
  long_name="Attenuated backscatter calibration factor",
198
280
  units="1",
199
281
  comment="Calibration factor applied.",
282
+ dimensions=("time",),
200
283
  ),
284
+ "zenith_angle": COMMON_ATTRIBUTES["zenith_angle"]._replace(dimensions=None),
201
285
  }