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,390 +1,142 @@
1
1
  """Module with classes for Vaisala ceilometers."""
2
- import logging
3
2
 
3
+ import datetime
4
+ from collections.abc import Callable
5
+ from os import PathLike
6
+
7
+ import ceilopyter.version
4
8
  import numpy as np
9
+ import numpy.typing as npt
10
+ from ceilopyter import read_cl_file, read_cs_file, read_ct_file
5
11
 
6
- from cloudnetpy import utils
7
12
  from cloudnetpy.exceptions import ValidTimeStampError
8
13
  from cloudnetpy.instruments import instruments
9
14
  from cloudnetpy.instruments.ceilometer import Ceilometer, NoiseParam
10
15
 
11
- M2KM = 0.001
12
- SECONDS_IN_MINUTE = 60
13
- SECONDS_IN_HOUR = 3600
14
-
15
16
 
16
17
  class VaisalaCeilo(Ceilometer):
17
18
  """Base class for Vaisala ceilometers."""
18
19
 
19
20
  def __init__(
20
- self, full_path: str, site_meta: dict, expected_date: str | None = None
21
- ):
21
+ self,
22
+ reader: Callable,
23
+ full_path: str | PathLike,
24
+ site_meta: dict,
25
+ expected_date: datetime.date | None = None,
26
+ ) -> None:
22
27
  super().__init__(self.noise_param)
28
+ self.reader = reader
23
29
  self.full_path = full_path
24
30
  self.site_meta = site_meta
25
31
  self.expected_date = expected_date
26
- self._backscatter_scale_factor = 1.0
27
- self._hex_conversion_params: tuple[int, int, int] = (1, 1, 1)
28
- self._message_number: int
29
-
30
- def _is_ct25k(self) -> bool:
31
- if self.instrument is not None and self.instrument.model is not None:
32
- return "CT25k" in self.instrument.model
33
- return False
34
-
35
- def _fetch_data_lines(self) -> list:
36
- """Finds data lines (header + backscatter) from ceilometer file."""
37
- with open(self.full_path, "rb") as file:
38
- all_lines = file.readlines()
39
- return self._screen_invalid_lines(all_lines)
40
-
41
- def _calc_range(self) -> np.ndarray:
42
- """Calculates range vector from the resolution and number of gates."""
43
- if self._is_ct25k():
44
- range_resolution = 30
45
- n_gates = 256
46
- else:
47
- n_gates = int(self.metadata["number_of_gates"])
48
- range_resolution = int(self.metadata["range_resolution"])
49
- return np.arange(n_gates) * range_resolution + range_resolution / 2
50
-
51
- def _read_backscatter(self, lines: list) -> np.ndarray:
52
- """Converts backscatter profile from 2-complement hex to floats."""
53
- n_chars = self._hex_conversion_params[0]
54
- n_gates = int(len(lines[0]) / n_chars)
55
- profiles = np.zeros((len(lines), n_gates), dtype=int)
56
- ran = range(0, n_gates * n_chars, n_chars)
57
- for ind, line in enumerate(lines):
58
- try:
59
- profiles[ind, :] = [int(line[i : i + n_chars], 16) for i in ran]
60
- except ValueError:
61
- logging.warning("Bad value in raw ceilometer data")
62
- ind = profiles & self._hex_conversion_params[1] != 0
63
- profiles[ind] -= self._hex_conversion_params[2]
64
- return profiles.astype(float) / self._backscatter_scale_factor
65
-
66
- def _screen_invalid_lines(self, data_in: list) -> list:
67
- """Removes empty (and other weird) lines from the list of data."""
68
-
69
- def _filter_lines(data: list) -> list:
70
- output = []
71
- for line in data:
72
- try:
73
- output.append(line.decode("utf8"))
74
- except UnicodeDecodeError:
75
- continue
76
- return output
77
-
78
- def _find_timestamp_line_numbers(data: list) -> list:
79
- return [n for n, value in enumerate(data) if utils.is_timestamp(value)]
80
-
81
- def _find_correct_dates(data: list, line_numbers: list) -> list:
82
- return [
83
- n for n in line_numbers if data[n].strip("-")[:10] == self.expected_date
84
- ]
85
-
86
- def _find_number_of_data_lines(data: list, timestamp_line_number: int) -> int:
87
- for i, line in enumerate(data[timestamp_line_number:]):
88
- if utils.is_empty_line(line):
89
- return i
90
- raise RuntimeError("Can not parse number of data lines")
91
-
92
- def _parse_data_lines(data: list, starting_indices: list) -> list:
93
- return [
94
- [
95
- data[n + line_number]
96
- for n in starting_indices
97
- if (n + line_number) < len(data)
98
- ]
99
- for line_number in range(number_of_data_lines)
100
- ]
101
-
102
- valid_lines = _filter_lines(data_in)
103
- timestamp_line_numbers = _find_timestamp_line_numbers(valid_lines)
104
- if self.expected_date is not None:
105
- timestamp_line_numbers = _find_correct_dates(
106
- valid_lines, timestamp_line_numbers
107
- )
108
- if not timestamp_line_numbers:
109
- raise ValidTimeStampError
110
- number_of_data_lines = _find_number_of_data_lines(
111
- valid_lines, timestamp_line_numbers[0]
112
- )
113
- data_lines = _parse_data_lines(valid_lines, timestamp_line_numbers)
114
- return data_lines
115
-
116
- @staticmethod
117
- def _get_message_number(header_line_1: dict) -> int:
118
- msg_no = header_line_1["message_number"]
119
- assert len(np.unique(msg_no)) == 1, "Error: inconsistent message numbers."
120
- return int(msg_no[0])
121
-
122
- @staticmethod
123
- def _calc_time(time_lines: list) -> np.ndarray:
124
- """Returns the time vector as fraction hour."""
125
- time = [time_to_fraction_hour(line.split()[1]) for line in time_lines]
126
- return np.array(time)
127
-
128
- @staticmethod
129
- def _calc_date(time_lines) -> list:
130
- """Returns the date [yyyy, mm, dd]"""
131
- return time_lines[0].split()[0].strip("-").split("-")
132
-
133
- @classmethod
134
- def _handle_metadata(cls, header: list) -> dict:
135
- meta = cls._concatenate_meta(header)
136
- meta = cls._remove_meta_duplicates(meta)
137
- meta = cls._convert_meta_strings(meta)
138
- return meta
139
-
140
- @staticmethod
141
- def _concatenate_meta(header: list) -> dict:
142
- meta = {}
143
- for head in header:
144
- meta.update(head)
145
- return meta
146
-
147
- @staticmethod
148
- def _remove_meta_duplicates(meta: dict) -> dict:
149
- for field in meta:
150
- if len(np.unique(meta[field])) == 1:
151
- meta[field] = meta[field][0]
152
- return meta
153
-
154
- @staticmethod
155
- def _convert_meta_strings(meta: dict) -> dict:
156
- strings = (
157
- "cloud_base_data",
158
- "measurement_parameters",
159
- "cloud_amount_data",
160
- )
161
- for field in meta:
162
- if field in strings:
163
- continue
164
- values = meta[field]
165
- if isinstance(values, str): # only one unique value
166
- try:
167
- meta[field] = int(values)
168
- except (ValueError, TypeError):
169
- continue
170
- else:
171
- meta[field] = [None] * len(values)
172
- for ind, value in enumerate(values):
173
- try:
174
- meta[field][ind] = int(value)
175
- except (ValueError, TypeError):
176
- continue
177
- meta[field] = np.array(meta[field])
178
- return meta
179
-
180
- def _read_common_header_part(self) -> tuple[list, list]:
181
- header = []
182
- data_lines = self._fetch_data_lines()
183
- self.data["time"] = self._calc_time(data_lines[0])
184
- self.date = self._calc_date(data_lines[0])
185
- header.append(self._read_header_line_1(data_lines[1]))
186
- self._message_number = self._get_message_number(header[0])
187
- header.append(self._read_header_line_2(data_lines[2]))
188
- return header, data_lines
189
-
190
- def _read_header_line_1(self, lines: list) -> dict:
191
- """Reads all first header lines from CT25k and CL ceilometers."""
192
- fields = (
193
- "model_id",
194
- "unit_id",
195
- "software_level",
196
- "message_number",
197
- "message_subclass",
198
- )
199
- if self._is_ct25k():
200
- indices = [1, 3, 4, 6, 7, 8]
201
- else:
202
- indices = [1, 3, 4, 7, 8, 9]
203
- values = [split_string(line, indices) for line in lines]
204
- return values_to_dict(fields, values)
205
-
206
- @staticmethod
207
- def _read_header_line_2(lines: list) -> dict:
208
- """Reads the second header line."""
209
- fields = (
210
- "detection_status",
211
- "warning",
212
- "cloud_base_data",
213
- "warning_flags",
214
- )
215
- values = [[line[0], line[1], line[3:20], line[21:].strip()] for line in lines]
216
- return values_to_dict(fields, values)
217
-
218
-
219
- class ClCeilo(VaisalaCeilo):
220
- """Base class for Vaisala CL31/CL51 ceilometers."""
221
-
222
- noise_param = NoiseParam(noise_min=3.1e-8, noise_smooth_min=1.1e-8)
223
-
224
- def __init__(
225
- self, full_path: str, site_meta: dict, expected_date: str | None = None
226
- ):
227
- super().__init__(full_path, site_meta, expected_date)
228
- self._hex_conversion_params = (5, 524288, 1048576)
229
- self._backscatter_scale_factor = 1e8
32
+ self.software = {"ceilopyter": ceilopyter.version.__version__}
230
33
 
231
34
  def read_ceilometer_file(self, calibration_factor: float | None = None) -> None:
232
35
  """Read all lines of data from the file."""
233
- header, data_lines = self._read_common_header_part()
234
- header.append(self._read_header_line_4(data_lines[-3]))
235
- self.metadata = self._handle_metadata(header)
236
- self.data["range"] = self._calc_range()
237
- self.data["beta_raw"] = self._read_backscatter(data_lines[-2])
36
+ time, data = self.reader(self.full_path)
37
+ if not data:
38
+ msg = "No valid data found."
39
+ raise ValidTimeStampError(msg)
40
+ range_res = data[0].range_resolution
41
+ n_gates = len(data[0].beta)
42
+ self.data["time"] = np.array(time)
43
+ self.data["range"] = np.arange(n_gates) * range_res + range_res / 2
44
+ self.data["beta_raw"] = np.stack([d.beta for d in data])
238
45
  self.data["calibration_factor"] = calibration_factor or 1.0
239
46
  self.data["beta_raw"] *= self.data["calibration_factor"]
240
- self.data["zenith_angle"] = np.median(self.metadata["zenith_angle"])
47
+ self.data["zenith_angle"] = np.median([d.tilt_angle for d in data])
48
+ self.sort_time()
49
+ self.screen_date()
50
+ self.convert_to_fraction_hour()
241
51
  self._store_ceilometer_info()
242
- self._sort_time()
243
52
 
244
- def _sort_time(self):
53
+ def sort_time(self) -> None:
245
54
  """Sorts timestamps and removes duplicates."""
246
- time = np.copy(self.data["time"][:])
247
- ind_sorted = np.argsort(time)
248
- ind_valid: list[int] = []
249
- for ind in ind_sorted:
250
- if time[ind] not in time[ind_valid]:
251
- ind_valid.append(ind)
55
+ time = self.data["time"]
56
+ _time, ind = np.unique(time, return_index=True)
57
+ self._screen_time_indices(ind)
58
+
59
+ def screen_date(self) -> None:
60
+ time = self.data["time"]
61
+ self.date = time[0].date() if self.expected_date is None else self.expected_date
62
+ is_valid = np.array([t.date() == self.date for t in time])
63
+ self._screen_time_indices(is_valid)
64
+
65
+ def _screen_time_indices(
66
+ self, valid_indices: npt.NDArray[np.intp] | npt.NDArray[np.bool_]
67
+ ) -> None:
68
+ time = self.data["time"]
252
69
  n_time = len(time)
70
+ if len(valid_indices) == 0 or (
71
+ valid_indices.dtype == np.bool_ and not np.any(valid_indices)
72
+ ):
73
+ msg = "All timestamps screened"
74
+ raise ValidTimeStampError(msg)
253
75
  for key, array in self.data.items():
254
- if not hasattr(array, "shape"):
255
- continue
256
- if array.ndim == 1 and array.shape[0] == n_time:
257
- self.data[key] = self.data[key][ind_valid]
258
- if array.ndim == 2 and array.shape[0] == n_time:
259
- self.data[key] = self.data[key][ind_valid, :]
260
-
261
- def _store_ceilometer_info(self):
262
- n_gates = self.data["beta_raw"].shape[1]
263
- if n_gates < 1000:
264
- self.instrument = instruments.CL31
265
- else:
266
- self.instrument = instruments.CL51
267
-
268
- def _read_header_line_3(self, lines: list) -> dict:
269
- if self._message_number != 2:
270
- raise RuntimeError("Unsupported message number.")
271
- keys = ("cloud_detection_status", "cloud_amount_data")
272
- values = [[line[0:3], line[3:].strip()] for line in lines]
273
- return values_to_dict(keys, values)
274
-
275
- @staticmethod
276
- def _read_header_line_4(lines: list) -> dict:
277
- keys = (
278
- "scale",
279
- "range_resolution",
280
- "number_of_gates",
281
- "laser_energy",
282
- "laser_temperature",
283
- "window_transmission",
284
- "zenith_angle",
285
- "background_light",
286
- "measurement_parameters",
287
- "backscatter_sum",
288
- )
289
- values = [line.split() for line in lines]
290
- return values_to_dict(keys, values)
76
+ if hasattr(array, "shape") and array.shape[:1] == (n_time,):
77
+ self.data[key] = self.data[key][valid_indices]
291
78
 
79
+ def convert_to_fraction_hour(self) -> None:
80
+ time = self.data["time"]
81
+ midnight = time[0].replace(hour=0, minute=0, second=0, microsecond=0)
82
+ hour = datetime.timedelta(hours=1)
83
+ self.data["time"] = (time - midnight) / hour
292
84
 
293
- class Ct25k(VaisalaCeilo):
294
- """Class for Vaisala CT25k ceilometer.
85
+ def _store_ceilometer_info(self) -> None:
86
+ raise NotImplementedError
295
87
 
296
- References:
297
- https://www.manualslib.com/manual/1414094/Vaisala-Ct25k.html
298
88
 
299
- """
89
+ class ClCeilo(VaisalaCeilo):
90
+ """Class for Vaisala CL31/CL51 ceilometers."""
300
91
 
301
- noise_param = NoiseParam(noise_min=6e-7, noise_smooth_min=1e-7)
92
+ noise_param = NoiseParam(noise_min=3.1e-8, noise_smooth_min=1.1e-8)
302
93
 
303
94
  def __init__(
304
95
  self,
305
- input_file: str,
96
+ full_path: str | PathLike,
306
97
  site_meta: dict,
307
- expected_date: str | None = None,
308
- ):
309
- super().__init__(input_file, site_meta, expected_date)
310
- self._hex_conversion_params = (4, 32768, 65536)
311
- self._backscatter_scale_factor = 1e7
312
- self.instrument = instruments.CT25K
313
-
314
- def read_ceilometer_file(self, calibration_factor: float | None = None) -> None:
315
- """Read all lines of data from the file."""
316
- header, data_lines = self._read_common_header_part()
317
- header.append(self._read_header_line_3(data_lines[3]))
318
- self.metadata = self._handle_metadata(header)
319
- self.data["range"] = self._calc_range()
320
- hex_profiles = self._parse_hex_profiles(data_lines[4:20])
321
- self.data["beta_raw"] = self._read_backscatter(hex_profiles)
322
- self.data["calibration_factor"] = calibration_factor or 1.0
323
- self.data["beta_raw"] *= self.data["calibration_factor"]
324
- self.data["zenith_angle"] = np.median(self.metadata["zenith_angle"])
325
-
326
- @staticmethod
327
- def _parse_hex_profiles(lines: list) -> list:
328
- """Collects ct25k profiles into list (one profile / element)."""
329
- n_profiles = len(lines[0])
330
- return [
331
- "".join([lines[m][n][3:].strip() for m in range(16)])
332
- for n in range(n_profiles)
333
- ]
334
-
335
- def _read_header_line_3(self, lines: list) -> dict:
336
- if self._message_number in (1, 3, 6):
337
- raise RuntimeError(f"Unsupported message number: {self._message_number}")
338
- keys = (
339
- "measurement_mode",
340
- "laser_energy",
341
- "laser_temperature",
342
- "receiver_sensitivity",
343
- "window_contamination",
344
- "zenith_angle",
345
- "background_light",
346
- "measurement_parameters",
347
- "backscatter_sum",
348
- )
349
- values = [line.split() for line in lines]
350
- keys_out = ("scale",) + keys if len(values[0]) == 10 else keys
351
- return values_to_dict(keys_out, values)
98
+ expected_date: datetime.date | None = None,
99
+ ) -> None:
100
+ super().__init__(read_cl_file, full_path, site_meta, expected_date)
352
101
 
102
+ def _store_ceilometer_info(self) -> None:
103
+ n_gates = self.data["beta_raw"].shape[1]
104
+ if n_gates < 1540:
105
+ self.instrument = instruments.CL31
106
+ else:
107
+ self.instrument = instruments.CL51
353
108
 
354
- def split_string(string: str, indices: list) -> list:
355
- """Splits string between indices.
356
109
 
357
- Notes:
358
- It is possible to skip characters from the beginning and end of the
359
- string but not from the middle.
110
+ class Ct25k(VaisalaCeilo):
111
+ """Class for Vaisala CT25k ceilometer."""
360
112
 
361
- Examples:
362
- >>> s = 'abcde'
363
- >>> indices = [1, 2, 4]
364
- >>> split_string(s, indices)
365
- ['b', 'cd']
113
+ noise_param = NoiseParam(noise_min=0.7e-7, noise_smooth_min=1.2e-8)
366
114
 
367
- """
368
- return [string[n:m] for n, m in zip(indices[:-1], indices[1:])]
115
+ def __init__(
116
+ self,
117
+ full_path: str | PathLike,
118
+ site_meta: dict,
119
+ expected_date: datetime.date | None = None,
120
+ ) -> None:
121
+ super().__init__(read_ct_file, full_path, site_meta, expected_date)
122
+ self._store_ceilometer_info()
369
123
 
124
+ def _store_ceilometer_info(self) -> None:
125
+ self.instrument = instruments.CT25K
370
126
 
371
- def values_to_dict(keys: tuple, values: list) -> dict:
372
- """Converts list elements to dictionary.
373
127
 
374
- Examples:
375
- >>> keys = ('a', 'b')
376
- >>> values = [[1, 2], [1, 2], [1, 2], [1, 2]]
377
- >>> values_to_dict(keys, values)
378
- {'a': array([1, 1, 1, 1]), 'b': array([2, 2, 2, 2])}
128
+ class Cs135(VaisalaCeilo):
129
+ """Class for Campbell Scientific CS135 ceilometer."""
379
130
 
380
- """
381
- out = {}
382
- for i, key in enumerate(keys):
383
- out[key] = np.array([x[i] for x in values])
384
- return out
131
+ noise_param = NoiseParam()
385
132
 
133
+ def __init__(
134
+ self,
135
+ full_path: str | PathLike,
136
+ site_meta: dict,
137
+ expected_date: datetime.date | None = None,
138
+ ) -> None:
139
+ super().__init__(read_cs_file, full_path, site_meta, expected_date)
386
140
 
387
- def time_to_fraction_hour(time: str) -> float:
388
- """Returns time (hh:mm:ss) as fraction hour"""
389
- hour, minute, sec = time.split(":")
390
- return int(hour) + (int(minute) * SECONDS_IN_MINUTE + int(sec)) / SECONDS_IN_HOUR
141
+ def _store_ceilometer_info(self) -> None:
142
+ self.instrument = instruments.CS135