cloudnetpy 1.71.3__py3-none-any.whl → 1.71.5__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.
@@ -6,10 +6,9 @@ import netCDF4
6
6
  from numpy import ma
7
7
 
8
8
  from cloudnetpy import output, utils
9
- from cloudnetpy.instruments.campbell_scientific import Cs135
10
9
  from cloudnetpy.instruments.cl61d import Cl61d
11
10
  from cloudnetpy.instruments.lufft import LufftCeilo
12
- from cloudnetpy.instruments.vaisala import ClCeilo, Ct25k
11
+ from cloudnetpy.instruments.vaisala import ClCeilo, Cs135, Ct25k
13
12
  from cloudnetpy.metadata import MetaData
14
13
 
15
14
 
@@ -103,6 +103,8 @@ def hatpro2l1c(
103
103
 
104
104
  if "ir_wavelength" in hatpro.data:
105
105
  hatpro.data["ir_wavelength"].dimensions = ("ir_channel",)
106
+ if "irt" in hatpro.data:
107
+ hatpro.data["irt"].dimensions = ("time", "ir_channel")
106
108
 
107
109
  for key in ("latitude", "longitude", "altitude"):
108
110
  if key in site_meta:
@@ -1,405 +1,130 @@
1
1
  """Module with classes for Vaisala ceilometers."""
2
2
 
3
- import itertools
4
- import logging
3
+ import datetime
4
+ from collections.abc import Callable
5
5
 
6
+ import ceilopyter.version
6
7
  import numpy as np
8
+ import numpy.typing as npt
9
+ from ceilopyter import read_cl_file, read_cs_file, read_ct_file
7
10
 
8
- from cloudnetpy import utils
9
- from cloudnetpy.constants import SEC_IN_HOUR, SEC_IN_MINUTE
10
11
  from cloudnetpy.exceptions import ValidTimeStampError
11
12
  from cloudnetpy.instruments import instruments
12
13
  from cloudnetpy.instruments.ceilometer import Ceilometer, NoiseParam
13
14
 
14
- M2KM = 0.001
15
-
16
15
 
17
16
  class VaisalaCeilo(Ceilometer):
18
17
  """Base class for Vaisala ceilometers."""
19
18
 
20
19
  def __init__(
21
20
  self,
21
+ reader: Callable,
22
22
  full_path: str,
23
23
  site_meta: dict,
24
24
  expected_date: str | None = None,
25
25
  ):
26
26
  super().__init__(self.noise_param)
27
+ self.reader = reader
27
28
  self.full_path = full_path
28
29
  self.site_meta = site_meta
29
30
  self.expected_date = expected_date
30
- self._backscatter_scale_factor = 1.0
31
- self._hex_conversion_params: tuple[int, int, int] = (1, 1, 1)
32
- self._message_number: int
33
-
34
- def _is_ct25k(self) -> bool:
35
- if self.instrument is not None and self.instrument.model is not None:
36
- return "CT25k" in self.instrument.model
37
- return False
38
-
39
- def _fetch_data_lines(self) -> list:
40
- """Finds data lines (header + backscatter) from ceilometer file."""
41
- with open(self.full_path, "rb") as file:
42
- all_lines = file.readlines()
43
- return self._screen_invalid_lines(all_lines)
44
-
45
- def _calc_range(self) -> np.ndarray:
46
- """Calculates range vector from the resolution and number of gates."""
47
- if self._is_ct25k():
48
- range_resolution = 30
49
- n_gates = 256
50
- else:
51
- n_gates = int(self.metadata["number_of_gates"])
52
- range_resolution = int(self.metadata["range_resolution"])
53
- return np.arange(n_gates) * range_resolution + range_resolution / 2
54
-
55
- def _read_backscatter(self, lines: list) -> np.ndarray:
56
- """Converts backscatter profile from 2-complement hex to floats."""
57
- n_chars = self._hex_conversion_params[0]
58
- n_gates = len(self.data["range"])
59
- profiles = np.zeros((len(lines), n_gates), dtype=int)
60
- ran = range(0, n_gates * n_chars, n_chars)
61
- for ind, line in enumerate(lines):
62
- if int(len(line) / n_chars) != n_gates:
63
- logging.warning("Invalid line in raw ceilometer data")
64
- continue
65
- try:
66
- profiles[ind, :] = [int(line[i : i + n_chars], 16) for i in ran]
67
- except ValueError:
68
- logging.warning("Bad value in raw ceilometer data")
69
- ind = profiles & self._hex_conversion_params[1] != 0
70
- profiles[ind] -= self._hex_conversion_params[2]
71
- return profiles.astype(float) / self._backscatter_scale_factor
72
-
73
- def _screen_invalid_lines(self, data_in: list) -> list:
74
- """Removes empty (and other weird) lines from the list of data."""
75
-
76
- def _filter_lines(data: list) -> list:
77
- output = []
78
- for line in data:
79
- try:
80
- output.append(line.decode("utf8"))
81
- except UnicodeDecodeError:
82
- continue
83
- return output
84
-
85
- def _find_timestamp_line_numbers(data: list) -> list:
86
- return [n for n, value in enumerate(data) if utils.is_timestamp(value)]
87
-
88
- def _find_correct_dates(data: list, line_numbers: list) -> list:
89
- return [
90
- n for n in line_numbers if data[n].strip("-")[:10] == self.expected_date
91
- ]
92
-
93
- def _find_number_of_data_lines(data: list, timestamp_line_number: int) -> int:
94
- for i, line in enumerate(data[timestamp_line_number:]):
95
- if utils.is_empty_line(line):
96
- return i
97
- msg = "Can not parse number of data lines"
98
- raise RuntimeError(msg)
99
-
100
- def _parse_data_lines(data: list, starting_indices: list) -> list:
101
- iterator = range(number_of_data_lines)
102
- n_lines = max(iterator)
103
- return [
104
- [
105
- data[n + line_number]
106
- for n in starting_indices
107
- if (n + n_lines) < len(data)
108
- ]
109
- for line_number in iterator
110
- ]
111
-
112
- valid_lines = _filter_lines(data_in)
113
- timestamp_line_numbers = _find_timestamp_line_numbers(valid_lines)
114
- if self.expected_date is not None:
115
- timestamp_line_numbers = _find_correct_dates(
116
- valid_lines,
117
- timestamp_line_numbers,
118
- )
119
- if not timestamp_line_numbers:
120
- raise ValidTimeStampError
121
- number_of_data_lines = _find_number_of_data_lines(
122
- valid_lines,
123
- timestamp_line_numbers[0],
31
+ self.sane_date = (
32
+ datetime.date.fromisoformat(self.expected_date)
33
+ if self.expected_date
34
+ else None
124
35
  )
125
- return _parse_data_lines(valid_lines, timestamp_line_numbers)
126
-
127
- @staticmethod
128
- def _get_message_number(header_line_1: dict) -> int:
129
- msg_no = header_line_1["message_number"]
130
- if len(np.unique(msg_no)) != 1:
131
- msg = "Error: inconsistent message numbers."
132
- raise RuntimeError(msg)
133
- return int(msg_no[0])
134
-
135
- @staticmethod
136
- def _calc_time(time_lines: list) -> np.ndarray:
137
- """Returns the time vector as fraction hour."""
138
- time = [time_to_fraction_hour(line.split()[1]) for line in time_lines]
139
- return np.array(time)
140
-
141
- @staticmethod
142
- def _calc_date(time_lines) -> list:
143
- """Returns the date [yyyy, mm, dd]."""
144
- return time_lines[0].split()[0].strip("-").split("-")
145
-
146
- @classmethod
147
- def _handle_metadata(cls, header: list) -> dict:
148
- meta = cls._concatenate_meta(header)
149
- meta = cls._remove_meta_duplicates(meta)
150
- return cls._convert_meta_strings(meta)
151
-
152
- @staticmethod
153
- def _concatenate_meta(header: list) -> dict:
154
- meta = {}
155
- for head in header:
156
- meta.update(head)
157
- return meta
158
-
159
- @staticmethod
160
- def _remove_meta_duplicates(meta: dict) -> dict:
161
- for field in meta:
162
- if len(np.unique(meta[field])) == 1:
163
- meta[field] = meta[field][0]
164
- return meta
36
+ self.software = {"ceilopyter": ceilopyter.version.__version__}
165
37
 
166
- @staticmethod
167
- def _convert_meta_strings(meta: dict) -> dict:
168
- strings = (
169
- "cloud_base_data",
170
- "measurement_parameters",
171
- "cloud_amount_data",
172
- )
173
- for field in meta:
174
- if field in strings:
175
- continue
176
- values = meta[field]
177
- if isinstance(values, str): # only one unique value
178
- try:
179
- meta[field] = int(values)
180
- except (ValueError, TypeError):
181
- continue
182
- else:
183
- meta[field] = [None] * len(values)
184
- for ind, value in enumerate(values):
185
- try:
186
- meta[field][ind] = int(value)
187
- except (ValueError, TypeError):
188
- continue
189
- meta[field] = np.array(meta[field])
190
- return meta
191
-
192
- def _read_common_header_part(self) -> tuple[list, list]:
193
- header = []
194
- data_lines = self._fetch_data_lines()
195
- self.data["time"] = self._calc_time(data_lines[0])
196
- self.date = self._calc_date(data_lines[0])
197
- header.append(self._read_header_line_1(data_lines[1]))
198
- self._message_number = self._get_message_number(header[0])
199
- header.append(self._read_header_line_2(data_lines[2]))
200
- return header, data_lines
201
-
202
- def _read_header_line_1(self, lines: list) -> dict:
203
- """Reads all first header lines from CT25k and CL ceilometers."""
204
- fields = (
205
- "model_id",
206
- "unit_id",
207
- "software_level",
208
- "message_number",
209
- "message_subclass",
210
- )
211
- indices = [1, 3, 4, 6, 7, 8] if self._is_ct25k() else [1, 3, 4, 7, 8, 9]
212
- values = [split_string(line, indices) for line in lines]
213
- return values_to_dict(fields, values)
214
-
215
- @staticmethod
216
- def _read_header_line_2(lines: list) -> dict:
217
- """Reads the second header line."""
218
- fields = (
219
- "detection_status",
220
- "warning",
221
- "cloud_base_data",
222
- "warning_flags",
223
- )
224
- values = [[line[0], line[1], line[3:20], line[21:].strip()] for line in lines]
225
- return values_to_dict(fields, values)
38
+ def read_ceilometer_file(self, calibration_factor: float | None = None) -> None:
39
+ """Read all lines of data from the file."""
40
+ time, data = self.reader(self.full_path)
41
+ range_res = data[0].range_resolution
42
+ n_gates = len(data[0].beta)
43
+ self.data["time"] = np.array(time)
44
+ self.data["range"] = np.arange(n_gates) * range_res + range_res / 2
45
+ self.data["beta_raw"] = np.stack([d.beta for d in data])
46
+ self.data["calibration_factor"] = calibration_factor or 1.0
47
+ self.data["beta_raw"] *= self.data["calibration_factor"]
48
+ self.data["zenith_angle"] = np.median([d.tilt_angle for d in data])
49
+ self._sort_time()
50
+ self._screen_date()
51
+ self._convert_to_fraction_hour()
52
+ self._store_ceilometer_info()
226
53
 
227
- def _sort_time(self) -> None:
54
+ def _sort_time(self):
228
55
  """Sorts timestamps and removes duplicates."""
229
- time = np.copy(self.data["time"][:])
230
- ind_sorted = np.argsort(time)
231
- ind_valid: list[int] = []
232
- for ind in ind_sorted:
233
- if time[ind] not in time[ind_valid]:
234
- ind_valid.append(ind)
56
+ time = self.data["time"]
57
+ _time, ind = np.unique(time, return_index=True)
58
+ self._screen_time_indices(ind)
59
+
60
+ def _screen_date(self):
61
+ time = self.data["time"]
62
+ if self.sane_date is None:
63
+ self.sane_date = time[0].date()
64
+ self.expected_date = self.sane_date.isoformat()
65
+ is_valid = np.array([t.date() == self.sane_date for t in time])
66
+ self._screen_time_indices(is_valid)
67
+
68
+ def _screen_time_indices(
69
+ self, valid_indices: npt.NDArray[np.intp] | npt.NDArray[np.bool]
70
+ ):
71
+ time = self.data["time"]
235
72
  n_time = len(time)
73
+ if len(valid_indices) == 0 or (
74
+ valid_indices.dtype == np.bool and not np.any(valid_indices)
75
+ ):
76
+ msg = "All timestamps screened"
77
+ raise ValidTimeStampError(msg)
236
78
  for key, array in self.data.items():
237
- if not hasattr(array, "shape"):
238
- continue
239
- if array.ndim == 1 and array.shape[0] == n_time:
240
- self.data[key] = self.data[key][ind_valid]
241
- if array.ndim == 2 and array.shape[0] == n_time:
242
- self.data[key] = self.data[key][ind_valid, :]
79
+ if hasattr(array, "shape") and array.shape[:1] == (n_time,):
80
+ self.data[key] = self.data[key][valid_indices]
81
+
82
+ def _convert_to_fraction_hour(self):
83
+ time = self.data["time"]
84
+ midnight = time[0].replace(hour=0, minute=0, second=0, microsecond=0)
85
+ hour = datetime.timedelta(hours=1)
86
+ self.data["time"] = (time - midnight) / hour
87
+ self.date = self.expected_date.split("-") # type: ignore[union-attr]
88
+
89
+ def _store_ceilometer_info(self):
90
+ raise NotImplementedError
243
91
 
244
92
 
245
93
  class ClCeilo(VaisalaCeilo):
246
- """Base class for Vaisala CL31/CL51 ceilometers."""
94
+ """Class for Vaisala CL31/CL51 ceilometers."""
247
95
 
248
96
  noise_param = NoiseParam(noise_min=3.1e-8, noise_smooth_min=1.1e-8)
249
97
 
250
- def __init__(
251
- self,
252
- full_path: str,
253
- site_meta: dict,
254
- expected_date: str | None = None,
255
- ):
256
- super().__init__(full_path, site_meta, expected_date)
257
- self._hex_conversion_params = (5, 524288, 1048576)
258
- self._backscatter_scale_factor = 1e8
259
-
260
- def read_ceilometer_file(self, calibration_factor: float | None = None) -> None:
261
- """Read all lines of data from the file."""
262
- header, data_lines = self._read_common_header_part()
263
- header.append(self._read_header_line_4(data_lines[-3]))
264
- self.metadata = self._handle_metadata(header)
265
- self.data["range"] = self._calc_range()
266
- self.data["beta_raw"] = self._read_backscatter(data_lines[-2])
267
- self.data["calibration_factor"] = calibration_factor or 1.0
268
- self.data["beta_raw"] *= self.data["calibration_factor"]
269
- self.data["zenith_angle"] = np.median(self.metadata["zenith_angle"])
270
- self._store_ceilometer_info()
271
- self._sort_time()
98
+ def __init__(self, full_path, site_meta, expected_date=None):
99
+ super().__init__(read_cl_file, full_path, site_meta, expected_date)
272
100
 
273
- def _store_ceilometer_info(self) -> None:
101
+ def _store_ceilometer_info(self):
274
102
  n_gates = self.data["beta_raw"].shape[1]
275
103
  if n_gates < 1540:
276
104
  self.instrument = instruments.CL31
277
105
  else:
278
106
  self.instrument = instruments.CL51
279
107
 
280
- def _read_header_line_3(self, lines: list) -> dict:
281
- if self._message_number != 2:
282
- msg = f"Unsupported message number: {self._message_number}"
283
- raise RuntimeError(msg)
284
- keys = ("cloud_detection_status", "cloud_amount_data")
285
- values = [[line[0:3], line[3:].strip()] for line in lines]
286
- return values_to_dict(keys, values)
287
-
288
- @staticmethod
289
- def _read_header_line_4(lines: list) -> dict:
290
- keys = (
291
- "scale",
292
- "range_resolution",
293
- "number_of_gates",
294
- "laser_energy",
295
- "laser_temperature",
296
- "window_transmission",
297
- "zenith_angle",
298
- "background_light",
299
- "measurement_parameters",
300
- "backscatter_sum",
301
- )
302
- values = [line.split() for line in lines]
303
- return values_to_dict(keys, values)
304
-
305
108
 
306
109
  class Ct25k(VaisalaCeilo):
307
- """Class for Vaisala CT25k ceilometer.
308
-
309
- References:
310
- https://www.manualslib.com/manual/1414094/Vaisala-Ct25k.html
311
-
312
- """
110
+ """Class for Vaisala CT25k ceilometer."""
313
111
 
314
112
  noise_param = NoiseParam(noise_min=0.7e-7, noise_smooth_min=1.2e-8)
315
113
 
316
- def __init__(
317
- self,
318
- input_file: str,
319
- site_meta: dict,
320
- expected_date: str | None = None,
321
- ):
322
- super().__init__(input_file, site_meta, expected_date)
323
- self._hex_conversion_params = (4, 32768, 65536)
324
- self._backscatter_scale_factor = 1e7
325
- self.instrument = instruments.CT25K
326
-
327
- def read_ceilometer_file(self, calibration_factor: float | None = None) -> None:
328
- """Read all lines of data from the file."""
329
- header, data_lines = self._read_common_header_part()
330
- header.append(self._read_header_line_3(data_lines[3]))
331
- self.metadata = self._handle_metadata(header)
332
- self.data["range"] = self._calc_range()
333
- hex_profiles = self._parse_hex_profiles(data_lines[4:20])
334
- self.data["beta_raw"] = self._read_backscatter(hex_profiles)
335
- self.data["calibration_factor"] = calibration_factor or 1.0
336
- self.data["beta_raw"] *= self.data["calibration_factor"]
337
- self.data["zenith_angle"] = np.median(self.metadata["zenith_angle"])
338
- self._sort_time()
339
-
340
- @staticmethod
341
- def _parse_hex_profiles(lines: list) -> list:
342
- """Collects ct25k profiles into list (one profile / element)."""
343
- n_profiles = len(lines[0])
344
- return [
345
- "".join([lines[m][n][3:].strip() for m in range(16)])
346
- for n in range(n_profiles)
347
- ]
348
-
349
- def _read_header_line_3(self, lines: list) -> dict:
350
- if self._message_number in (1, 3, 6):
351
- msg = f"Unsupported message number: {self._message_number}"
352
- raise RuntimeError(msg)
353
- keys = (
354
- "measurement_mode",
355
- "laser_energy",
356
- "laser_temperature",
357
- "receiver_sensitivity",
358
- "window_contamination",
359
- "zenith_angle",
360
- "background_light",
361
- "measurement_parameters",
362
- "backscatter_sum",
363
- )
364
- values = [line.split() for line in lines]
365
- keys_out = ("scale", *keys) if len(values[0]) == 10 else keys
366
- return values_to_dict(keys_out, values)
367
-
368
-
369
- def split_string(string: str, indices: list) -> list:
370
- """Splits string between indices.
371
-
372
- Notes:
373
- It is possible to skip characters from the beginning and end of the
374
- string but not from the middle.
375
-
376
- Examples:
377
- >>> s = 'abcde'
378
- >>> indices = [1, 2, 4]
379
- >>> split_string(s, indices)
380
- ['b', 'cd']
381
-
382
- """
383
- return [string[n:m] for n, m in itertools.pairwise(indices)]
114
+ def __init__(self, full_path, site_meta, expected_date=None):
115
+ super().__init__(read_ct_file, full_path, site_meta, expected_date)
384
116
 
117
+ def _store_ceilometer_info(self):
118
+ self.instrument = instruments.CT25K
385
119
 
386
- def values_to_dict(keys: tuple, values: list) -> dict:
387
- """Converts list elements to dictionary.
388
120
 
389
- Examples:
390
- >>> keys = ('a', 'b')
391
- >>> values = [[1, 2], [1, 2], [1, 2], [1, 2]]
392
- >>> values_to_dict(keys, values)
393
- {'a': array([1, 1, 1, 1]), 'b': array([2, 2, 2, 2])}
121
+ class Cs135(VaisalaCeilo):
122
+ """Class for Campbell Scientific CS135 ceilometer."""
394
123
 
395
- """
396
- out = {}
397
- for i, key in enumerate(keys):
398
- out[key] = np.array([x[i] for x in values if len(x) == len(keys)])
399
- return out
124
+ noise_param = NoiseParam()
400
125
 
126
+ def __init__(self, full_path, site_meta, expected_date=None):
127
+ super().__init__(read_cs_file, full_path, site_meta, expected_date)
401
128
 
402
- def time_to_fraction_hour(time: str) -> float:
403
- """Returns time (hh:mm:ss) as fraction hour."""
404
- hour, minute, sec = time.split(":")
405
- return int(hour) + (int(minute) * SEC_IN_MINUTE + int(sec)) / SEC_IN_HOUR
129
+ def _store_ceilometer_info(self):
130
+ self.instrument = instruments.CS135
cloudnetpy/output.py CHANGED
@@ -44,6 +44,9 @@ def save_level1b(
44
44
  nc.source = get_l1b_source(obj.instrument)
45
45
  if hasattr(obj, "serial_number") and obj.serial_number is not None:
46
46
  nc.serial_number = obj.serial_number
47
+ if hasattr(obj, "software"):
48
+ for software, version in obj.software.items():
49
+ nc.setncattr(f"{software}_version", version)
47
50
  nc.references = get_references()
48
51
  return file_uuid
49
52
 
@@ -199,7 +199,7 @@ class FigureData:
199
199
  if "altitude" not in self.file.variables:
200
200
  msg = "No altitude information in the file."
201
201
  raise ValueError(msg)
202
- height -= self.file.variables["altitude"][:]
202
+ height -= np.median(self.file.variables["altitude"][:])
203
203
  return height * con.M_TO_KM
204
204
  if "range" in self.file.variables:
205
205
  return self.file.variables["range"][:] * con.M_TO_KM
cloudnetpy/version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  MAJOR = 1
2
2
  MINOR = 71
3
- PATCH = 3
3
+ PATCH = 5
4
4
  __version__ = f"{MAJOR}.{MINOR}.{PATCH}"
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: cloudnetpy
3
- Version: 1.71.3
3
+ Version: 1.71.5
4
4
  Summary: Python package for Cloudnet processing
5
5
  Author: Simo Tukiainen
6
6
  License: MIT License
@@ -33,14 +33,12 @@ Classifier: Development Status :: 5 - Production/Stable
33
33
  Classifier: Intended Audience :: Science/Research
34
34
  Classifier: License :: OSI Approved :: MIT License
35
35
  Classifier: Operating System :: OS Independent
36
- Classifier: Programming Language :: Python :: 3.10
37
- Classifier: Programming Language :: Python :: 3.11
38
- Classifier: Programming Language :: Python :: 3.12
39
- Classifier: Programming Language :: Python :: 3.13
40
- Classifier: Topic :: Scientific/Engineering
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
41
38
  Requires-Python: >=3.10
42
39
  Description-Content-Type: text/markdown
43
40
  License-File: LICENSE
41
+ Requires-Dist: ceilopyter
44
42
  Requires-Dist: cloudnetpy_qc>=1.15.0
45
43
  Requires-Dist: doppy>=0.5.0
46
44
  Requires-Dist: matplotlib
@@ -61,6 +59,7 @@ Requires-Dist: pre-commit; extra == "dev"
61
59
  Requires-Dist: release-version; extra == "dev"
62
60
  Provides-Extra: extras
63
61
  Requires-Dist: voodoonet>=0.1.7; extra == "extras"
62
+ Dynamic: license-file
64
63
 
65
64
  # CloudnetPy
66
65
 
@@ -6,10 +6,10 @@ cloudnetpy/constants.py,sha256=YnoSzZm35NDooJfhlulSJBc7g0eSchT3yGytRaTaJEI,845
6
6
  cloudnetpy/datasource.py,sha256=FcWS77jz56gIzwnbafDLdj-HjAyu0P_VtY7gkeVZThU,7952
7
7
  cloudnetpy/exceptions.py,sha256=hYbUtBwjCIfxnPe_5mELDEw87AWITBrwuo7WYIEKmJ8,1579
8
8
  cloudnetpy/metadata.py,sha256=BDEpgwZ58PHznc1gi11gtNNV4kFiMAmlHnF4huTy7nw,5982
9
- cloudnetpy/output.py,sha256=lq4YSeMT_d-j4rlQkKm9KIZ8boupTBBBKV1eUawpmCI,15672
9
+ cloudnetpy/output.py,sha256=l0LoOhcGCBrg2EJ4NT1xZ7-UKWdV7X7yQ0fJmhkwJVc,15829
10
10
  cloudnetpy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  cloudnetpy/utils.py,sha256=U0iMIKPiKLrLVAfs_u9pPuoWYW1RJHcM8dbLF9a4yIA,29796
12
- cloudnetpy/version.py,sha256=tsjo_vDV3Q5KrCVFxMEpgs-KBRK1bdqml45ItHKQVio,72
12
+ cloudnetpy/version.py,sha256=WGC_6FoGDMaIdA18EiKCMGcEd1CCikWt_zPUoQ2yXjs,72
13
13
  cloudnetpy/categorize/__init__.py,sha256=s-SJaysvVpVVo5kidiruWQO6p3gv2TXwY1wEHYO5D6I,44
14
14
  cloudnetpy/categorize/atmos_utils.py,sha256=RcmbKxm2COkE7WEya0mK3yX5rzUbrewRVh3ekm01RtM,10598
15
15
  cloudnetpy/categorize/attenuation.py,sha256=Y_-fzmQTltWTqIZTulJhovC7a6ifpMcaAazDJcnMIOc,990
@@ -34,14 +34,13 @@ cloudnetpy/categorize/attenuations/melting_attenuation.py,sha256=9c9xoZHtGUbjFYJ
34
34
  cloudnetpy/categorize/attenuations/rain_attenuation.py,sha256=qazJzRyXf9vbjJhh4yiFmABI4L57j5W_6YZ-6qjRiBI,2839
35
35
  cloudnetpy/instruments/__init__.py,sha256=2vAdceXCNxZhTujhArLf4NjYOfUdkhLpGM-NQa_LXdg,470
36
36
  cloudnetpy/instruments/basta.py,sha256=Lb_EhQTI93S5Bd9osDbCE_tC8gZreRsHz7D2_dFOjmE,3793
37
- cloudnetpy/instruments/campbell_scientific.py,sha256=c3f2W9aA52ghJ35_J994y7FShA3W-Bxk52BCbEm_gt0,6975
38
- cloudnetpy/instruments/ceilo.py,sha256=xrI7iYNftKvGZf-3C_ESUNsu-QhXV43iWkDuKp3biZU,9552
37
+ cloudnetpy/instruments/ceilo.py,sha256=rkP6Vo90eS5ZhnKxZpnkuiG5ZEP86lDK8zRqoMY1aqg,9498
39
38
  cloudnetpy/instruments/ceilometer.py,sha256=pdmLVljsuciyKpaGxWxJ_f1IrJK-UrkBC0lSeuirLlU,12095
40
39
  cloudnetpy/instruments/cl61d.py,sha256=g6DNBFju3wYhLFl32DKmC8pUup7y-EupXoUU0fuoGGA,1990
41
40
  cloudnetpy/instruments/cloudnet_instrument.py,sha256=086GJ6Nfp7sK9ZK8UygJOn-aiVPreez674_gbrOZj4I,5183
42
41
  cloudnetpy/instruments/copernicus.py,sha256=99idcn6-iKOSvSslNjwFRng3gwlTLFjKPiT1tnVytpQ,6613
43
42
  cloudnetpy/instruments/galileo.py,sha256=BjWE15_S3tTCOmAM5k--oicI3wghKaO0hv9EUBxtbl8,4830
44
- cloudnetpy/instruments/hatpro.py,sha256=5jeONT2gBlAFr_M6mPablAiWloBYta_9nvXv6kt75LU,8969
43
+ cloudnetpy/instruments/hatpro.py,sha256=z3mOc23LDz2Si9hasRaJvJFDEPR5rxYUFWfoKWK0LyE,9061
45
44
  cloudnetpy/instruments/instruments.py,sha256=97hHMjp8fp2IKihr0XJYY3BrOlBArU7gYwYmt3OxqvU,4124
46
45
  cloudnetpy/instruments/lufft.py,sha256=nIoEKuuFGKq2dLqkX7zW-HpAifefG472tZhKfXE1yoA,4212
47
46
  cloudnetpy/instruments/mira.py,sha256=Wofp8HbiAwJce_IbOLjpEFV07H_Kh4170C9Wygiz-ew,11401
@@ -54,7 +53,7 @@ cloudnetpy/instruments/rain_e_h3.py,sha256=9TdpP4UzMBNIt2iE2GL6K9dFldzTHPLOrU8Q3
54
53
  cloudnetpy/instruments/rpg.py,sha256=vfs_eGahPOxFjOIBczNywRwtdutOsJpSNeXZm99SIOo,17438
55
54
  cloudnetpy/instruments/rpg_reader.py,sha256=ThztFuVrWxhmWVAfZTfQDeUiKK1XMTbtv08IBe8GK98,11364
56
55
  cloudnetpy/instruments/toa5.py,sha256=CfmmBMv5iMGaWHIGBK01Rw24cuXC1R1RMNTXkmsm340,1760
57
- cloudnetpy/instruments/vaisala.py,sha256=MA6hAlanFrxJlF4suDxLt5FyjcSSzJg-SWRnkLQpu3E,14688
56
+ cloudnetpy/instruments/vaisala.py,sha256=RKAw_fVry4YOUF0i2_-2jLIc6_H85oL8USA4ji9rh0o,4583
58
57
  cloudnetpy/instruments/weather_station.py,sha256=pg5Rf6A0qkbAYV1NvC1-oQLkwe9-gmLxHJ8CYW7xgYI,20365
59
58
  cloudnetpy/instruments/disdrometer/__init__.py,sha256=lyjwttWvFvuwYxEkusoAvgRcbBmglmOp5HJOpXUqLWo,93
60
59
  cloudnetpy/instruments/disdrometer/common.py,sha256=g52iK2aNp3Z88kovUmGVpC54NZomPa9D871gzO0AmQ4,9267
@@ -102,7 +101,7 @@ cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py,sha256=Ra3r4V
102
101
  cloudnetpy/model_evaluation/tests/unit/test_tools.py,sha256=Ia_VrLdV2NstX5gbx_3AZTOAlrgLAy_xFZ8fHYVX0xI,3817
103
102
  cloudnetpy/plotting/__init__.py,sha256=lg9Smn4BI0dVBgnDLC3JVJ4GmwoSnO-qoSd4ApvwV6Y,107
104
103
  cloudnetpy/plotting/plot_meta.py,sha256=qfyZJNis937uM-NJseer8i4FO7I_v5jhQPyFl5Uszi8,17390
105
- cloudnetpy/plotting/plotting.py,sha256=Hi_n3TjzqR2KjZgalFHGxsb7jTsqub9nWzZyscfk6xA,38385
104
+ cloudnetpy/plotting/plotting.py,sha256=EEdTu9H3ly5dxILNyiWjmH5iDRyTFDlFIYGUp7Yvxxs,38396
106
105
  cloudnetpy/products/__init__.py,sha256=2hRb5HG9hNrxH1if5laJkLeFeaZCd5W1q3hh4ewsX0E,273
107
106
  cloudnetpy/products/classification.py,sha256=KwAiBSgFwDqhM114NIgYiUjj8HoYc7gAlc8E1QgcSig,8207
108
107
  cloudnetpy/products/der.py,sha256=soypE7uSEP4uHUCCQVEhyXsKY6e9mzV9B_2S5GUizqk,12729
@@ -116,10 +115,10 @@ cloudnetpy/products/lwc.py,sha256=sl6Al2tuH3KkCBrPbWTmuz3jlD5UQJ4D6qBsn1tt2CQ,18
116
115
  cloudnetpy/products/mie_lu_tables.nc,sha256=It4fYpqJXlqOgL8jeZ-PxGzP08PMrELIDVe55y9ob58,16637951
117
116
  cloudnetpy/products/mwr_tools.py,sha256=rd7UC67O4fsIE5SaHVZ4qWvUJTj41ZGwgQWPwZzOM14,5377
118
117
  cloudnetpy/products/product_tools.py,sha256=uu4l6reuGbPcW3TgttbaSrqIKbyYGhBVTdnC7opKvmg,11101
118
+ cloudnetpy-1.71.5.dist-info/licenses/LICENSE,sha256=wcZF72bdaoG9XugpyE95Juo7lBQOwLuTKBOhhtANZMM,1094
119
119
  docs/source/conf.py,sha256=IKiFWw6xhUd8NrCg0q7l596Ck1d61XWeVjIFHVSG9Og,1490
120
- cloudnetpy-1.71.3.dist-info/LICENSE,sha256=wcZF72bdaoG9XugpyE95Juo7lBQOwLuTKBOhhtANZMM,1094
121
- cloudnetpy-1.71.3.dist-info/METADATA,sha256=kMugQZKRTpx-hY86nY2NMhW5Q6oYE5p4a0eeM6H8vPo,5872
122
- cloudnetpy-1.71.3.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
123
- cloudnetpy-1.71.3.dist-info/entry_points.txt,sha256=HhY7LwCFk4qFgDlXx_Fy983ZTd831WlhtdPIzV-Y3dY,51
124
- cloudnetpy-1.71.3.dist-info/top_level.txt,sha256=ibSPWRr6ojS1i11rtBFz2_gkIe68mggj7aeswYfaOo0,16
125
- cloudnetpy-1.71.3.dist-info/RECORD,,
120
+ cloudnetpy-1.71.5.dist-info/METADATA,sha256=PPVhCnIGq2fq-b981uBPydzl4mR5Dia7_1JGhD7_LHI,5787
121
+ cloudnetpy-1.71.5.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
122
+ cloudnetpy-1.71.5.dist-info/entry_points.txt,sha256=HhY7LwCFk4qFgDlXx_Fy983ZTd831WlhtdPIzV-Y3dY,51
123
+ cloudnetpy-1.71.5.dist-info/top_level.txt,sha256=ibSPWRr6ojS1i11rtBFz2_gkIe68mggj7aeswYfaOo0,16
124
+ cloudnetpy-1.71.5.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (76.0.0)
2
+ Generator: setuptools (78.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,203 +0,0 @@
1
- import binascii
2
- import datetime
3
- import re
4
- from typing import NamedTuple
5
-
6
- import numpy as np
7
-
8
- from cloudnetpy import utils
9
- from cloudnetpy.exceptions import InconsistentDataError, ValidTimeStampError
10
- from cloudnetpy.instruments import instruments
11
- from cloudnetpy.instruments.ceilometer import Ceilometer
12
-
13
-
14
- def _date_format_to_regex(fmt: bytes) -> bytes:
15
- """Converts a date format string to a regex pattern."""
16
- mapping = {
17
- b"%Y": rb"\d{4}",
18
- b"%m": rb"0[1-9]|1[0-2]",
19
- b"%d": rb"0[1-9]|[12]\d|3[01]",
20
- b"%H": rb"[01]\d|2[0-3]",
21
- b"%M": rb"[0-5]\d",
22
- b"%S": rb"[0-5]\d",
23
- b"%f": rb"\d{6}",
24
- }
25
- pattern = re.escape(fmt)
26
- for key, value in mapping.items():
27
- pattern = pattern.replace(
28
- re.escape(key), b"(?P<" + key[1:] + b">" + value + b")"
29
- )
30
- return pattern
31
-
32
-
33
- FORMATS = [
34
- re.compile(_date_format_to_regex(fmt))
35
- for fmt in [
36
- b"%Y-%m-%dT%H:%M:%S.%f,",
37
- b"%%% %Y/%m/%d %H:%M:%S %%%\n",
38
- ]
39
- ]
40
-
41
-
42
- class Cs135(Ceilometer):
43
- def __init__(
44
- self,
45
- full_path: str,
46
- site_meta: dict,
47
- expected_date: str | None = None,
48
- ):
49
- super().__init__()
50
- self.full_path = full_path
51
- self.site_meta = site_meta
52
- self.expected_date = expected_date
53
- self.data = {}
54
- self.metadata = {}
55
- self.instrument = instruments.CS135
56
-
57
- def read_ceilometer_file(self, calibration_factor: float | None = None) -> None:
58
- with open(self.full_path, mode="rb") as f:
59
- content = f.read()
60
- timestamps = []
61
- profiles = []
62
- tilt_angles = []
63
- range_resolutions = []
64
-
65
- for fmt in FORMATS:
66
- parts = re.split(fmt, content)
67
- for i in range(1, len(parts), fmt.groups + 1):
68
- timestamp = datetime.datetime(
69
- int(parts[i + fmt.groupindex["Y"] - 1]),
70
- int(parts[i + fmt.groupindex["m"] - 1]),
71
- int(parts[i + fmt.groupindex["d"] - 1]),
72
- int(parts[i + fmt.groupindex["H"] - 1]),
73
- int(parts[i + fmt.groupindex["M"] - 1]),
74
- int(parts[i + fmt.groupindex["S"] - 1]),
75
- int(parts[i + fmt.groupindex["f"] - 1])
76
- if "f" in fmt.groupindex
77
- else 0,
78
- tzinfo=datetime.timezone.utc,
79
- )
80
- try:
81
- self._check_timestamp(timestamp)
82
- except ValidTimeStampError:
83
- continue
84
- try:
85
- message = _read_message(parts[i + fmt.groups])
86
- except InvalidMessageError:
87
- continue
88
- profile = (message.data[:-2] * 1e-8) * (message.scale / 100)
89
- timestamps.append(timestamp)
90
- profiles.append(profile)
91
- tilt_angles.append(message.tilt_angle)
92
- range_resolutions.append(message.range_resolution)
93
-
94
- if len(timestamps) == 0:
95
- msg = "No valid timestamps found in the file"
96
- raise ValidTimeStampError(msg)
97
- range_resolution = range_resolutions[0]
98
- n_gates = len(profiles[0])
99
- if any(res != range_resolution for res in range_resolutions):
100
- msg = "Inconsistent range resolution"
101
- raise InconsistentDataError(msg)
102
- if any(len(profile) != n_gates for profile in profiles):
103
- msg = "Inconsistent number of gates"
104
- raise InconsistentDataError(msg)
105
-
106
- self.data["beta_raw"] = np.array(profiles)
107
- if calibration_factor is None:
108
- calibration_factor = 1.0
109
- self.data["beta_raw"] *= calibration_factor
110
- self.data["calibration_factor"] = calibration_factor
111
- self.data["range"] = (
112
- np.arange(n_gates) * range_resolution + range_resolution / 2
113
- )
114
- self.data["time"] = utils.datetime2decimal_hours(timestamps)
115
- self.data["zenith_angle"] = np.median(tilt_angles)
116
-
117
- def _check_timestamp(self, timestamp: datetime.datetime) -> None:
118
- timestamp_components = str(timestamp.date()).split("-")
119
- if (
120
- self.expected_date is not None
121
- and timestamp_components != self.expected_date.split("-")
122
- ):
123
- raise ValidTimeStampError
124
- if not self.date:
125
- self.date = timestamp_components
126
- if timestamp_components != self.date:
127
- msg = "Inconsistent dates in the file"
128
- raise RuntimeError(msg)
129
-
130
-
131
- class Message(NamedTuple):
132
- scale: int
133
- range_resolution: int
134
- laser_pulse_energy: int
135
- laser_temperature: int
136
- tilt_angle: int
137
- background_light: int
138
- pulse_quantity: int
139
- sample_rate: int
140
- data: np.ndarray
141
-
142
-
143
- class InvalidMessageError(Exception):
144
- pass
145
-
146
-
147
- def _read_message(message: bytes) -> Message:
148
- end_idx = message.index(3)
149
- content = message[1 : end_idx + 1]
150
- expected_checksum = int(message[end_idx + 1 : end_idx + 5], 16)
151
- actual_checksum = _crc16(content)
152
- if expected_checksum != actual_checksum:
153
- msg = (
154
- "Invalid checksum: "
155
- f"expected {expected_checksum:04x}, "
156
- f"got {actual_checksum:04x}"
157
- )
158
- raise InvalidMessageError(msg)
159
- lines = message[1 : end_idx - 1].splitlines()
160
- n_lines = len(lines) + 1
161
- n_first = len(lines[0]) + 1
162
- if n_first != 11:
163
- msg = f"Expected 11 characters in first line, got {n_first}"
164
- raise NotImplementedError(msg)
165
- msg_no = lines[0][-4:-1]
166
- if msg_no == b"002":
167
- if n_lines != 5:
168
- msg = f"Expected 5 lines, got {len(lines)}"
169
- raise InvalidMessageError(msg)
170
- scale, res, n, energy, lt, ti, bl, pulse, rate, _sum = map(
171
- int, lines[2].split()
172
- )
173
- data = _read_backscatter(lines[3].strip(), n)
174
- return Message(scale, res, energy, lt, ti, bl, pulse, rate, data)
175
- if msg_no == b"004":
176
- if n_lines != 6:
177
- msg = f"Expected 6 lines, got {len(lines)}"
178
- raise InvalidMessageError(msg)
179
- scale, res, n, energy, lt, ti, bl, pulse, rate, _sum = map(
180
- int, lines[3].split()
181
- )
182
- data = _read_backscatter(lines[4].strip(), n)
183
- return Message(scale, res, energy, lt, ti, bl, pulse, rate, data)
184
- msg = f"Message number {msg_no.decode()} not implemented"
185
- raise NotImplementedError(msg)
186
-
187
-
188
- def _read_backscatter(data: bytes, n_gates: int) -> np.ndarray:
189
- """Read backscatter values from hex-encoded two's complement values."""
190
- n_chars = 5
191
- n_bits = n_chars * 4
192
- limit = (1 << (n_bits - 1)) - 1
193
- offset = 1 << n_bits
194
- out = np.array(
195
- [int(data[i : i + n_chars], 16) for i in range(0, n_gates * n_chars, n_chars)],
196
- )
197
- out[out > limit] -= offset
198
- return out
199
-
200
-
201
- def _crc16(data: bytes) -> int:
202
- """Compute checksum similar to CRC-16-CCITT."""
203
- return binascii.crc_hqx(data, 0xFFFF) ^ 0xFFFF