cloudnetpy 1.71.2__py3-none-any.whl → 1.71.4__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.
- cloudnetpy/instruments/ceilo.py +1 -2
- cloudnetpy/instruments/vaisala.py +77 -352
- cloudnetpy/output.py +3 -0
- cloudnetpy/products/ier.py +2 -2
- cloudnetpy/products/iwc.py +2 -2
- cloudnetpy/version.py +1 -1
- {cloudnetpy-1.71.2.dist-info → cloudnetpy-1.71.4.dist-info}/METADATA +6 -7
- {cloudnetpy-1.71.2.dist-info → cloudnetpy-1.71.4.dist-info}/RECORD +12 -13
- {cloudnetpy-1.71.2.dist-info → cloudnetpy-1.71.4.dist-info}/WHEEL +1 -1
- cloudnetpy/instruments/campbell_scientific.py +0 -152
- {cloudnetpy-1.71.2.dist-info → cloudnetpy-1.71.4.dist-info}/entry_points.txt +0 -0
- {cloudnetpy-1.71.2.dist-info → cloudnetpy-1.71.4.dist-info/licenses}/LICENSE +0 -0
- {cloudnetpy-1.71.2.dist-info → cloudnetpy-1.71.4.dist-info}/top_level.txt +0 -0
cloudnetpy/instruments/ceilo.py
CHANGED
@@ -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
|
|
@@ -1,405 +1,130 @@
|
|
1
1
|
"""Module with classes for Vaisala ceilometers."""
|
2
2
|
|
3
|
-
import
|
4
|
-
import
|
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.
|
31
|
-
|
32
|
-
|
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
|
-
|
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
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
)
|
173
|
-
for
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
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)
|
54
|
+
def _sort_time(self):
|
228
55
|
"""Sorts timestamps and removes duplicates."""
|
229
|
-
time =
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
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
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
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
|
-
"""
|
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
|
-
|
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)
|
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
|
-
|
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
|
-
|
390
|
-
|
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
|
403
|
-
|
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
|
|
cloudnetpy/products/ier.py
CHANGED
@@ -83,9 +83,9 @@ class IerSource(IceSource):
|
|
83
83
|
|
84
84
|
|
85
85
|
def _add_ier_comment(attributes: dict, ier: IerSource) -> dict:
|
86
|
-
freq = ier.radar_frequency
|
86
|
+
freq = round(ier.radar_frequency, 3)
|
87
87
|
coeffs = ier.coefficients
|
88
|
-
factor =
|
88
|
+
factor = round(coeffs[0] / 0.93, 3)
|
89
89
|
attributes["ier"] = attributes["ier"]._replace(
|
90
90
|
comment=f"This variable was calculated from the {freq}-GHz radar\n"
|
91
91
|
f"reflectivity factor after correction for gaseous attenuation,\n"
|
cloudnetpy/products/iwc.py
CHANGED
@@ -113,9 +113,9 @@ def _add_iwc_error_comment(attributes: dict, lwp_prior, uncertainty: float) -> d
|
|
113
113
|
|
114
114
|
|
115
115
|
def _add_iwc_comment(attributes: dict, iwc: IwcSource) -> dict:
|
116
|
-
freq = iwc.radar_frequency
|
116
|
+
freq = round(iwc.radar_frequency, 3)
|
117
117
|
coeffs = iwc.coefficients
|
118
|
-
factor = round(
|
118
|
+
factor = round(coeffs[0] / 0.93, 3)
|
119
119
|
attributes["iwc"] = attributes["iwc"]._replace(
|
120
120
|
comment=f"This variable was calculated from the {freq}-GHz radar reflectivity\n"
|
121
121
|
"factor after correction for gaseous attenuation, and temperature taken from\n"
|
cloudnetpy/version.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: cloudnetpy
|
3
|
-
Version: 1.71.
|
3
|
+
Version: 1.71.4
|
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
|
37
|
-
Classifier:
|
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=
|
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=
|
12
|
+
cloudnetpy/version.py,sha256=EmL_GJ2W3CkjB5lLJ6fWDr5kRO3uL7qIDsyaCuK8Isc,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,8 +34,7 @@ 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/
|
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
|
@@ -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=
|
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
|
@@ -110,16 +109,16 @@ cloudnetpy/products/drizzle.py,sha256=58C9Mo6oRXR8KpbVPghbJvHvFX9GfS3xUp058pbf0q
|
|
110
109
|
cloudnetpy/products/drizzle_error.py,sha256=4GwlHRtNbk9ks7bGtXCco-wXbcDOKeAQwKmbhzut6Qk,6132
|
111
110
|
cloudnetpy/products/drizzle_tools.py,sha256=HLxUQ89mFNo6IIe6Cj3ZH-TPkJdpMxKCOt4cOOmcLs0,11002
|
112
111
|
cloudnetpy/products/epsilon.py,sha256=sVtOcl-tckvZCmM34etRQCzLg5NjvbHlt_5InRCjm1E,7734
|
113
|
-
cloudnetpy/products/ier.py,sha256=
|
114
|
-
cloudnetpy/products/iwc.py,sha256=
|
112
|
+
cloudnetpy/products/ier.py,sha256=XW4gg_H-JWMWKToMqLVl6v8kx1S65GBwclWDCn1EfSk,5991
|
113
|
+
cloudnetpy/products/iwc.py,sha256=WcPdAZx3zW0zaNJNp2vpAD4JnG0NJjFmCUAhDWzNxMg,9459
|
115
114
|
cloudnetpy/products/lwc.py,sha256=sl6Al2tuH3KkCBrPbWTmuz3jlD5UQJ4D6qBsn1tt2CQ,18962
|
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.4.dist-info/licenses/LICENSE,sha256=wcZF72bdaoG9XugpyE95Juo7lBQOwLuTKBOhhtANZMM,1094
|
119
119
|
docs/source/conf.py,sha256=IKiFWw6xhUd8NrCg0q7l596Ck1d61XWeVjIFHVSG9Og,1490
|
120
|
-
cloudnetpy-1.71.
|
121
|
-
cloudnetpy-1.71.
|
122
|
-
cloudnetpy-1.71.
|
123
|
-
cloudnetpy-1.71.
|
124
|
-
cloudnetpy-1.71.
|
125
|
-
cloudnetpy-1.71.2.dist-info/RECORD,,
|
120
|
+
cloudnetpy-1.71.4.dist-info/METADATA,sha256=uUSvEyqDNN90SKrBCU6Hl0lfO8qsFqkXTBkzKwdIeCw,5787
|
121
|
+
cloudnetpy-1.71.4.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
|
122
|
+
cloudnetpy-1.71.4.dist-info/entry_points.txt,sha256=HhY7LwCFk4qFgDlXx_Fy983ZTd831WlhtdPIzV-Y3dY,51
|
123
|
+
cloudnetpy-1.71.4.dist-info/top_level.txt,sha256=ibSPWRr6ojS1i11rtBFz2_gkIe68mggj7aeswYfaOo0,16
|
124
|
+
cloudnetpy-1.71.4.dist-info/RECORD,,
|
@@ -1,152 +0,0 @@
|
|
1
|
-
import binascii
|
2
|
-
import re
|
3
|
-
from datetime import datetime, timezone
|
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
|
-
class Cs135(Ceilometer):
|
15
|
-
def __init__(
|
16
|
-
self,
|
17
|
-
full_path: str,
|
18
|
-
site_meta: dict,
|
19
|
-
expected_date: str | None = None,
|
20
|
-
):
|
21
|
-
super().__init__()
|
22
|
-
self.full_path = full_path
|
23
|
-
self.site_meta = site_meta
|
24
|
-
self.expected_date = expected_date
|
25
|
-
self.data = {}
|
26
|
-
self.metadata = {}
|
27
|
-
self.instrument = instruments.CS135
|
28
|
-
|
29
|
-
def read_ceilometer_file(self, calibration_factor: float | None = None) -> None:
|
30
|
-
with open(self.full_path, mode="rb") as f:
|
31
|
-
content = f.read()
|
32
|
-
timestamps = []
|
33
|
-
profiles = []
|
34
|
-
tilt_angles = []
|
35
|
-
range_resolutions = []
|
36
|
-
|
37
|
-
parts = re.split(rb"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{6}),", content)
|
38
|
-
for i in range(1, len(parts), 2):
|
39
|
-
timestamp = datetime.strptime(
|
40
|
-
parts[i].decode(),
|
41
|
-
"%Y-%m-%dT%H:%M:%S.%f",
|
42
|
-
).replace(tzinfo=timezone.utc)
|
43
|
-
try:
|
44
|
-
self._check_timestamp(timestamp)
|
45
|
-
except ValidTimeStampError:
|
46
|
-
continue
|
47
|
-
try:
|
48
|
-
message = _read_message(parts[i + 1])
|
49
|
-
except InvalidMessageError:
|
50
|
-
continue
|
51
|
-
profile = (message.data[:-2] * 1e-8) * (message.scale / 100)
|
52
|
-
timestamps.append(timestamp)
|
53
|
-
profiles.append(profile)
|
54
|
-
tilt_angles.append(message.tilt_angle)
|
55
|
-
range_resolutions.append(message.range_resolution)
|
56
|
-
|
57
|
-
if len(timestamps) == 0:
|
58
|
-
msg = "No valid timestamps found in the file"
|
59
|
-
raise ValidTimeStampError(msg)
|
60
|
-
range_resolution = range_resolutions[0]
|
61
|
-
n_gates = len(profiles[0])
|
62
|
-
if any(res != range_resolution for res in range_resolutions):
|
63
|
-
msg = "Inconsistent range resolution"
|
64
|
-
raise InconsistentDataError(msg)
|
65
|
-
if any(len(profile) != n_gates for profile in profiles):
|
66
|
-
msg = "Inconsistent number of gates"
|
67
|
-
raise InconsistentDataError(msg)
|
68
|
-
|
69
|
-
self.data["beta_raw"] = np.array(profiles)
|
70
|
-
if calibration_factor is None:
|
71
|
-
calibration_factor = 1.0
|
72
|
-
self.data["beta_raw"] *= calibration_factor
|
73
|
-
self.data["calibration_factor"] = calibration_factor
|
74
|
-
self.data["range"] = (
|
75
|
-
np.arange(n_gates) * range_resolution + range_resolution / 2
|
76
|
-
)
|
77
|
-
self.data["time"] = utils.datetime2decimal_hours(timestamps)
|
78
|
-
self.data["zenith_angle"] = np.median(tilt_angles)
|
79
|
-
|
80
|
-
def _check_timestamp(self, timestamp: datetime) -> None:
|
81
|
-
timestamp_components = str(timestamp.date()).split("-")
|
82
|
-
if (
|
83
|
-
self.expected_date is not None
|
84
|
-
and timestamp_components != self.expected_date.split("-")
|
85
|
-
):
|
86
|
-
raise ValidTimeStampError
|
87
|
-
if not self.date:
|
88
|
-
self.date = timestamp_components
|
89
|
-
if timestamp_components != self.date:
|
90
|
-
msg = "Inconsistent dates in the file"
|
91
|
-
raise RuntimeError(msg)
|
92
|
-
|
93
|
-
|
94
|
-
class Message(NamedTuple):
|
95
|
-
scale: int
|
96
|
-
range_resolution: int
|
97
|
-
laser_pulse_energy: int
|
98
|
-
laser_temperature: int
|
99
|
-
tilt_angle: int
|
100
|
-
background_light: int
|
101
|
-
pulse_quantity: int
|
102
|
-
sample_rate: int
|
103
|
-
data: np.ndarray
|
104
|
-
|
105
|
-
|
106
|
-
class InvalidMessageError(Exception):
|
107
|
-
pass
|
108
|
-
|
109
|
-
|
110
|
-
def _read_message(message: bytes) -> Message:
|
111
|
-
end_idx = message.index(3)
|
112
|
-
content = message[1 : end_idx + 1]
|
113
|
-
expected_checksum = int(message[end_idx + 1 : end_idx + 5], 16)
|
114
|
-
actual_checksum = _crc16(content)
|
115
|
-
if expected_checksum != actual_checksum:
|
116
|
-
msg = (
|
117
|
-
"Invalid checksum: "
|
118
|
-
f"expected {expected_checksum:04x}, "
|
119
|
-
f"got {actual_checksum:04x}"
|
120
|
-
)
|
121
|
-
raise InvalidMessageError(msg)
|
122
|
-
lines = message.splitlines()
|
123
|
-
if len(lines[0]) != 11:
|
124
|
-
msg = f"Expected 11 characters in first line, got {len(lines[0])}"
|
125
|
-
raise NotImplementedError(msg)
|
126
|
-
if (msg_no := lines[0][-4:-1]) != b"002":
|
127
|
-
msg = f"Message number {msg_no.decode()} not implemented"
|
128
|
-
raise NotImplementedError(msg)
|
129
|
-
if len(lines) != 5:
|
130
|
-
msg = f"Expected 5 lines, got {len(lines)}"
|
131
|
-
raise InvalidMessageError(msg)
|
132
|
-
scale, res, n, energy, lt, ti, bl, pulse, rate, _sum = map(int, lines[2].split())
|
133
|
-
data = _read_backscatter(lines[3].strip(), n)
|
134
|
-
return Message(scale, res, energy, lt, ti, bl, pulse, rate, data)
|
135
|
-
|
136
|
-
|
137
|
-
def _read_backscatter(data: bytes, n_gates: int) -> np.ndarray:
|
138
|
-
"""Read backscatter values from hex-encoded two's complement values."""
|
139
|
-
n_chars = 5
|
140
|
-
n_bits = n_chars * 4
|
141
|
-
limit = (1 << (n_bits - 1)) - 1
|
142
|
-
offset = 1 << n_bits
|
143
|
-
out = np.array(
|
144
|
-
[int(data[i : i + n_chars], 16) for i in range(0, n_gates * n_chars, n_chars)],
|
145
|
-
)
|
146
|
-
out[out > limit] -= offset
|
147
|
-
return out
|
148
|
-
|
149
|
-
|
150
|
-
def _crc16(data: bytes) -> int:
|
151
|
-
"""Compute checksum similar to CRC-16-CCITT."""
|
152
|
-
return binascii.crc_hqx(data, 0xFFFF) ^ 0xFFFF
|
File without changes
|
File without changes
|
File without changes
|