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.
- cloudnetpy/categorize/__init__.py +1 -2
- cloudnetpy/categorize/atmos_utils.py +297 -67
- cloudnetpy/categorize/attenuation.py +31 -0
- cloudnetpy/categorize/attenuations/__init__.py +37 -0
- cloudnetpy/categorize/attenuations/gas_attenuation.py +30 -0
- cloudnetpy/categorize/attenuations/liquid_attenuation.py +84 -0
- cloudnetpy/categorize/attenuations/melting_attenuation.py +78 -0
- cloudnetpy/categorize/attenuations/rain_attenuation.py +84 -0
- cloudnetpy/categorize/categorize.py +332 -156
- cloudnetpy/categorize/classify.py +127 -125
- cloudnetpy/categorize/containers.py +107 -76
- cloudnetpy/categorize/disdrometer.py +40 -0
- cloudnetpy/categorize/droplet.py +23 -21
- cloudnetpy/categorize/falling.py +53 -24
- cloudnetpy/categorize/freezing.py +25 -12
- cloudnetpy/categorize/insects.py +35 -23
- cloudnetpy/categorize/itu.py +243 -0
- cloudnetpy/categorize/lidar.py +36 -41
- cloudnetpy/categorize/melting.py +34 -26
- cloudnetpy/categorize/model.py +84 -37
- cloudnetpy/categorize/mwr.py +18 -14
- cloudnetpy/categorize/radar.py +215 -102
- cloudnetpy/cli.py +578 -0
- cloudnetpy/cloudnetarray.py +43 -89
- cloudnetpy/concat_lib.py +218 -78
- cloudnetpy/constants.py +28 -10
- cloudnetpy/datasource.py +61 -86
- cloudnetpy/exceptions.py +49 -20
- cloudnetpy/instruments/__init__.py +5 -0
- cloudnetpy/instruments/basta.py +29 -12
- cloudnetpy/instruments/bowtie.py +135 -0
- cloudnetpy/instruments/ceilo.py +138 -115
- cloudnetpy/instruments/ceilometer.py +164 -80
- cloudnetpy/instruments/cl61d.py +21 -5
- cloudnetpy/instruments/cloudnet_instrument.py +74 -36
- cloudnetpy/instruments/copernicus.py +108 -30
- cloudnetpy/instruments/da10.py +54 -0
- cloudnetpy/instruments/disdrometer/common.py +126 -223
- cloudnetpy/instruments/disdrometer/parsivel.py +453 -94
- cloudnetpy/instruments/disdrometer/thies.py +254 -87
- cloudnetpy/instruments/fd12p.py +201 -0
- cloudnetpy/instruments/galileo.py +65 -23
- cloudnetpy/instruments/hatpro.py +123 -49
- cloudnetpy/instruments/instruments.py +113 -1
- cloudnetpy/instruments/lufft.py +39 -17
- cloudnetpy/instruments/mira.py +268 -61
- cloudnetpy/instruments/mrr.py +187 -0
- cloudnetpy/instruments/nc_lidar.py +19 -8
- cloudnetpy/instruments/nc_radar.py +109 -55
- cloudnetpy/instruments/pollyxt.py +135 -51
- cloudnetpy/instruments/radiometrics.py +313 -59
- cloudnetpy/instruments/rain_e_h3.py +171 -0
- cloudnetpy/instruments/rpg.py +321 -189
- cloudnetpy/instruments/rpg_reader.py +74 -40
- cloudnetpy/instruments/toa5.py +49 -0
- cloudnetpy/instruments/vaisala.py +95 -343
- cloudnetpy/instruments/weather_station.py +774 -105
- cloudnetpy/metadata.py +90 -19
- cloudnetpy/model_evaluation/file_handler.py +55 -52
- cloudnetpy/model_evaluation/metadata.py +46 -20
- cloudnetpy/model_evaluation/model_metadata.py +1 -1
- cloudnetpy/model_evaluation/plotting/plot_tools.py +32 -37
- cloudnetpy/model_evaluation/plotting/plotting.py +327 -117
- cloudnetpy/model_evaluation/products/advance_methods.py +92 -83
- cloudnetpy/model_evaluation/products/grid_methods.py +88 -63
- cloudnetpy/model_evaluation/products/model_products.py +43 -35
- cloudnetpy/model_evaluation/products/observation_products.py +41 -35
- cloudnetpy/model_evaluation/products/product_resampling.py +17 -7
- cloudnetpy/model_evaluation/products/tools.py +29 -20
- cloudnetpy/model_evaluation/statistics/statistical_methods.py +30 -20
- cloudnetpy/model_evaluation/tests/e2e/conftest.py +3 -3
- cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +15 -14
- cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +15 -14
- cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +15 -14
- cloudnetpy/model_evaluation/tests/unit/conftest.py +42 -41
- cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +41 -48
- cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +216 -194
- cloudnetpy/model_evaluation/tests/unit/test_model_products.py +23 -21
- cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +37 -38
- cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +43 -40
- cloudnetpy/model_evaluation/tests/unit/test_plotting.py +30 -36
- cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +68 -31
- cloudnetpy/model_evaluation/tests/unit/test_tools.py +33 -26
- cloudnetpy/model_evaluation/utils.py +2 -1
- cloudnetpy/output.py +170 -111
- cloudnetpy/plotting/__init__.py +2 -1
- cloudnetpy/plotting/plot_meta.py +562 -822
- cloudnetpy/plotting/plotting.py +1142 -704
- cloudnetpy/products/__init__.py +1 -0
- cloudnetpy/products/classification.py +370 -88
- cloudnetpy/products/der.py +85 -55
- cloudnetpy/products/drizzle.py +77 -34
- cloudnetpy/products/drizzle_error.py +15 -11
- cloudnetpy/products/drizzle_tools.py +79 -59
- cloudnetpy/products/epsilon.py +211 -0
- cloudnetpy/products/ier.py +27 -50
- cloudnetpy/products/iwc.py +55 -48
- cloudnetpy/products/lwc.py +96 -70
- cloudnetpy/products/mwr_tools.py +186 -0
- cloudnetpy/products/product_tools.py +170 -128
- cloudnetpy/utils.py +455 -240
- cloudnetpy/version.py +2 -2
- {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/METADATA +44 -40
- cloudnetpy-1.87.3.dist-info/RECORD +127 -0
- {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/WHEEL +1 -1
- cloudnetpy-1.87.3.dist-info/entry_points.txt +2 -0
- docs/source/conf.py +2 -2
- cloudnetpy/categorize/atmos.py +0 -361
- cloudnetpy/products/mwr_multi.py +0 -68
- cloudnetpy/products/mwr_single.py +0 -75
- cloudnetpy-1.49.9.dist-info/RECORD +0 -112
- {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info/licenses}/LICENSE +0 -0
- {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,
|
|
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.
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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(
|
|
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
|
|
53
|
+
def sort_time(self) -> None:
|
|
245
54
|
"""Sorts timestamps and removes duplicates."""
|
|
246
|
-
time =
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
|
255
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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=
|
|
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
|
-
|
|
96
|
+
full_path: str | PathLike,
|
|
306
97
|
site_meta: dict,
|
|
307
|
-
expected_date:
|
|
308
|
-
):
|
|
309
|
-
super().__init__(
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
string but not from the middle.
|
|
110
|
+
class Ct25k(VaisalaCeilo):
|
|
111
|
+
"""Class for Vaisala CT25k ceilometer."""
|
|
360
112
|
|
|
361
|
-
|
|
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
|
-
|
|
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
|
-
|
|
375
|
-
|
|
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
|
|
388
|
-
|
|
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
|