cloudnetpy 1.55.20__py3-none-any.whl → 1.55.22__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/atmos.py +46 -14
- cloudnetpy/categorize/atmos_utils.py +11 -1
- cloudnetpy/categorize/categorize.py +38 -21
- cloudnetpy/categorize/classify.py +31 -9
- cloudnetpy/categorize/containers.py +19 -7
- cloudnetpy/categorize/droplet.py +24 -8
- cloudnetpy/categorize/falling.py +17 -7
- cloudnetpy/categorize/freezing.py +19 -5
- cloudnetpy/categorize/insects.py +27 -14
- cloudnetpy/categorize/lidar.py +38 -36
- cloudnetpy/categorize/melting.py +19 -9
- cloudnetpy/categorize/model.py +28 -9
- cloudnetpy/categorize/mwr.py +4 -2
- cloudnetpy/categorize/radar.py +58 -22
- cloudnetpy/cloudnetarray.py +15 -6
- cloudnetpy/concat_lib.py +39 -16
- cloudnetpy/constants.py +7 -0
- cloudnetpy/datasource.py +39 -19
- cloudnetpy/instruments/basta.py +6 -2
- cloudnetpy/instruments/campbell_scientific.py +33 -16
- cloudnetpy/instruments/ceilo.py +30 -13
- cloudnetpy/instruments/ceilometer.py +76 -37
- cloudnetpy/instruments/cl61d.py +8 -3
- cloudnetpy/instruments/cloudnet_instrument.py +2 -1
- cloudnetpy/instruments/copernicus.py +27 -14
- cloudnetpy/instruments/disdrometer/common.py +51 -32
- cloudnetpy/instruments/disdrometer/parsivel.py +79 -48
- cloudnetpy/instruments/disdrometer/thies.py +10 -6
- cloudnetpy/instruments/galileo.py +23 -12
- cloudnetpy/instruments/hatpro.py +27 -11
- cloudnetpy/instruments/instruments.py +4 -1
- cloudnetpy/instruments/lufft.py +20 -11
- cloudnetpy/instruments/mira.py +60 -49
- cloudnetpy/instruments/mrr.py +31 -20
- cloudnetpy/instruments/nc_lidar.py +15 -6
- cloudnetpy/instruments/nc_radar.py +31 -22
- cloudnetpy/instruments/pollyxt.py +36 -21
- cloudnetpy/instruments/radiometrics.py +32 -18
- cloudnetpy/instruments/rpg.py +48 -22
- cloudnetpy/instruments/rpg_reader.py +39 -30
- cloudnetpy/instruments/vaisala.py +39 -27
- cloudnetpy/instruments/weather_station.py +15 -11
- cloudnetpy/metadata.py +3 -1
- cloudnetpy/model_evaluation/file_handler.py +31 -21
- cloudnetpy/model_evaluation/metadata.py +3 -1
- cloudnetpy/model_evaluation/model_metadata.py +1 -1
- cloudnetpy/model_evaluation/plotting/plot_tools.py +20 -15
- cloudnetpy/model_evaluation/plotting/plotting.py +114 -64
- cloudnetpy/model_evaluation/products/advance_methods.py +48 -28
- cloudnetpy/model_evaluation/products/grid_methods.py +44 -19
- cloudnetpy/model_evaluation/products/model_products.py +22 -18
- cloudnetpy/model_evaluation/products/observation_products.py +15 -9
- cloudnetpy/model_evaluation/products/product_resampling.py +14 -4
- cloudnetpy/model_evaluation/products/tools.py +16 -7
- cloudnetpy/model_evaluation/statistics/statistical_methods.py +28 -15
- 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 +14 -13
- cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +14 -13
- cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +14 -13
- cloudnetpy/model_evaluation/tests/unit/conftest.py +11 -11
- cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +33 -27
- cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +83 -83
- cloudnetpy/model_evaluation/tests/unit/test_model_products.py +23 -21
- cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +24 -25
- cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +40 -39
- cloudnetpy/model_evaluation/tests/unit/test_plotting.py +12 -11
- cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +30 -30
- cloudnetpy/model_evaluation/tests/unit/test_tools.py +18 -17
- cloudnetpy/model_evaluation/utils.py +3 -2
- cloudnetpy/output.py +45 -19
- cloudnetpy/plotting/plot_meta.py +35 -11
- cloudnetpy/plotting/plotting.py +172 -104
- cloudnetpy/products/classification.py +20 -8
- cloudnetpy/products/der.py +25 -10
- cloudnetpy/products/drizzle.py +41 -26
- cloudnetpy/products/drizzle_error.py +10 -5
- cloudnetpy/products/drizzle_tools.py +43 -24
- cloudnetpy/products/ier.py +10 -5
- cloudnetpy/products/iwc.py +16 -9
- cloudnetpy/products/lwc.py +34 -12
- cloudnetpy/products/mwr_multi.py +4 -1
- cloudnetpy/products/mwr_single.py +4 -1
- cloudnetpy/products/product_tools.py +33 -10
- cloudnetpy/utils.py +175 -74
- cloudnetpy/version.py +1 -1
- {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/METADATA +11 -10
- cloudnetpy-1.55.22.dist-info/RECORD +114 -0
- docs/source/conf.py +2 -2
- cloudnetpy-1.55.20.dist-info/RECORD +0 -114
- {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/LICENSE +0 -0
- {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/WHEEL +0 -0
- {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/top_level.txt +0 -0
cloudnetpy/datasource.py
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
"""Datasource module, containing the :class:`DataSource class.`"""
|
2
2
|
import logging
|
3
3
|
import os
|
4
|
-
from
|
5
|
-
from
|
4
|
+
from collections.abc import Callable
|
5
|
+
from datetime import datetime, timezone
|
6
6
|
|
7
7
|
import netCDF4
|
8
8
|
import numpy as np
|
@@ -16,10 +16,12 @@ class DataSource:
|
|
16
16
|
"""Base class for all Cloudnet measurements and model data.
|
17
17
|
|
18
18
|
Args:
|
19
|
+
----
|
19
20
|
full_path: Calibrated instrument / model NetCDF file.
|
20
21
|
radar: Indicates if data is from cloud radar. Default is False.
|
21
22
|
|
22
23
|
Attributes:
|
24
|
+
----------
|
23
25
|
filename (str): Filename of the input file.
|
24
26
|
dataset (netCDF4.Dataset): A netCDF4 Dataset instance.
|
25
27
|
source (str): Global attribute `source` read from the input file.
|
@@ -44,9 +46,9 @@ class DataSource:
|
|
44
46
|
radar_frequency: float
|
45
47
|
data_dense: dict
|
46
48
|
data_sparse: dict
|
47
|
-
|
49
|
+
source_type: str
|
48
50
|
|
49
|
-
def __init__(self, full_path: os.PathLike | str, radar: bool = False):
|
51
|
+
def __init__(self, full_path: os.PathLike | str, *, radar: bool = False):
|
50
52
|
self.filename = os.path.basename(full_path)
|
51
53
|
self.dataset = netCDF4.Dataset(full_path)
|
52
54
|
self.source = getattr(self.dataset, "source", "")
|
@@ -63,30 +65,35 @@ class DataSource:
|
|
63
65
|
variables dictionary, fetched from the input netCDF file.
|
64
66
|
|
65
67
|
Args:
|
68
|
+
----
|
66
69
|
*args: possible names of the variable. The first match is returned.
|
67
70
|
|
68
71
|
Returns:
|
72
|
+
-------
|
69
73
|
ndarray: The actual data.
|
70
74
|
|
71
75
|
Raises:
|
76
|
+
------
|
72
77
|
RuntimeError: The variable is not found.
|
73
78
|
|
74
79
|
"""
|
75
80
|
for arg in args:
|
76
81
|
if arg in self.dataset.variables:
|
77
82
|
return self.dataset.variables[arg][:]
|
78
|
-
|
83
|
+
msg = f"Missing variable {args[0]} in the input file."
|
84
|
+
raise RuntimeError(msg)
|
79
85
|
|
80
86
|
def append_data(
|
81
87
|
self,
|
82
|
-
variable: netCDF4.Variable | np.ndarray | float
|
88
|
+
variable: netCDF4.Variable | np.ndarray | float,
|
83
89
|
key: str,
|
84
90
|
name: str | None = None,
|
85
91
|
units: str | None = None,
|
86
|
-
):
|
92
|
+
) -> None:
|
87
93
|
"""Adds new CloudnetVariable or RadarVariable into `data` attribute.
|
88
94
|
|
89
95
|
Args:
|
96
|
+
----
|
90
97
|
variable: netCDF variable or data array to be added.
|
91
98
|
key: Key used with *variable* when added to `data`
|
92
99
|
attribute (dictionary).
|
@@ -99,10 +106,12 @@ class DataSource:
|
|
99
106
|
def get_date(self) -> list:
|
100
107
|
"""Returns date components.
|
101
108
|
|
102
|
-
Returns
|
109
|
+
Returns
|
110
|
+
-------
|
103
111
|
list: Date components [YYYY, MM, DD].
|
104
112
|
|
105
|
-
Raises
|
113
|
+
Raises
|
114
|
+
------
|
106
115
|
RuntimeError: Not found or invalid date.
|
107
116
|
|
108
117
|
"""
|
@@ -110,11 +119,13 @@ class DataSource:
|
|
110
119
|
year = str(self.dataset.year)
|
111
120
|
month = str(self.dataset.month).zfill(2)
|
112
121
|
day = str(self.dataset.day).zfill(2)
|
113
|
-
datetime.strptime(f"{year}{month}{day}", "%Y%m%d")
|
122
|
+
datetime.strptime(f"{year}{month}{day}", "%Y%m%d").replace(
|
123
|
+
tzinfo=timezone.utc,
|
124
|
+
)
|
125
|
+
|
114
126
|
except (AttributeError, ValueError) as read_error:
|
115
|
-
|
116
|
-
|
117
|
-
) from read_error
|
127
|
+
msg = "Missing or invalid date in global attributes."
|
128
|
+
raise RuntimeError(msg) from read_error
|
118
129
|
return [year, month, day]
|
119
130
|
|
120
131
|
def close(self) -> None:
|
@@ -128,7 +139,8 @@ class DataSource:
|
|
128
139
|
if var.units == "km":
|
129
140
|
alt *= 1000
|
130
141
|
elif var.units not in ("m", "meters"):
|
131
|
-
|
142
|
+
msg = f"Unexpected unit: {var.units}"
|
143
|
+
raise ValueError(msg)
|
132
144
|
return alt
|
133
145
|
|
134
146
|
@staticmethod
|
@@ -138,13 +150,15 @@ class DataSource:
|
|
138
150
|
if var.units == "m":
|
139
151
|
alt /= 1000
|
140
152
|
elif var.units != "km":
|
141
|
-
|
153
|
+
msg = f"Unexpected unit: {var.units}"
|
154
|
+
raise ValueError(msg)
|
142
155
|
return alt
|
143
156
|
|
144
157
|
def _init_time(self) -> np.ndarray:
|
145
158
|
time = self.getvar("time")
|
146
159
|
if len(time) == 0:
|
147
|
-
|
160
|
+
msg = "Empty time vector"
|
161
|
+
raise ValidTimeStampError(msg)
|
148
162
|
if max(time) > 25:
|
149
163
|
logging.debug("Assuming time as seconds, converting to fraction hour")
|
150
164
|
time = utils.seconds2hours(time)
|
@@ -160,7 +174,7 @@ class DataSource:
|
|
160
174
|
return float(
|
161
175
|
altitude_above_sea
|
162
176
|
if utils.isscalar(altitude_above_sea)
|
163
|
-
else np.mean(altitude_above_sea)
|
177
|
+
else np.mean(altitude_above_sea),
|
164
178
|
)
|
165
179
|
return None
|
166
180
|
|
@@ -177,11 +191,13 @@ class DataSource:
|
|
177
191
|
"""Transforms netCDF4-variables into CloudnetArrays.
|
178
192
|
|
179
193
|
Args:
|
194
|
+
----
|
180
195
|
keys: netCDF4-variables to be converted. The results
|
181
196
|
are saved in *self.data* dictionary with *fields*
|
182
197
|
strings as keys.
|
183
198
|
|
184
199
|
Notes:
|
200
|
+
-----
|
185
201
|
The attributes of the variables are not copied. Just the data.
|
186
202
|
|
187
203
|
"""
|
@@ -193,11 +209,13 @@ class DataSource:
|
|
193
209
|
possible_names: tuple,
|
194
210
|
key: str,
|
195
211
|
units: str | None = None,
|
212
|
+
*,
|
196
213
|
ignore_mask: bool = False,
|
197
|
-
):
|
214
|
+
) -> None:
|
198
215
|
"""Transforms single netCDF4 variable into CloudnetArray.
|
199
216
|
|
200
217
|
Args:
|
218
|
+
----
|
201
219
|
possible_names: Tuple of strings containing the possible
|
202
220
|
names of the variable in the input NetCDF file.
|
203
221
|
key: Key for self.data dictionary and name-attribute
|
@@ -206,6 +224,7 @@ class DataSource:
|
|
206
224
|
ignore_mask: If true, always writes an ordinary numpy array.
|
207
225
|
|
208
226
|
Raises:
|
227
|
+
------
|
209
228
|
RuntimeError: No variable found.
|
210
229
|
|
211
230
|
"""
|
@@ -216,7 +235,8 @@ class DataSource:
|
|
216
235
|
array = np.array(array)
|
217
236
|
self.append_data(array, key, units=units)
|
218
237
|
return
|
219
|
-
|
238
|
+
msg = f"Missing variable {possible_names[0]} in the input file."
|
239
|
+
raise RuntimeError(msg)
|
220
240
|
|
221
241
|
def __enter__(self):
|
222
242
|
return self
|
cloudnetpy/instruments/basta.py
CHANGED
@@ -22,6 +22,7 @@ def basta2nc(
|
|
22
22
|
steps.
|
23
23
|
|
24
24
|
Args:
|
25
|
+
----
|
25
26
|
basta_file: Filename of a daily BASTA .nc file.
|
26
27
|
output_file: Output filename.
|
27
28
|
site_meta: Dictionary containing information about the site. Required key
|
@@ -30,12 +31,15 @@ def basta2nc(
|
|
30
31
|
date: Expected date of the measurements as YYYY-MM-DD.
|
31
32
|
|
32
33
|
Returns:
|
34
|
+
-------
|
33
35
|
UUID of the generated file.
|
34
36
|
|
35
37
|
Raises:
|
38
|
+
------
|
36
39
|
ValueError: Timestamps do not match the expected date.
|
37
40
|
|
38
41
|
Examples:
|
42
|
+
--------
|
39
43
|
>>> from cloudnetpy.instruments import basta2nc
|
40
44
|
>>> site_meta = {'name': 'Palaiseau', 'latitude': 48.718, 'longitude': 2.207}
|
41
45
|
>>> basta2nc('basta_file.nc', 'radar.nc', site_meta)
|
@@ -63,14 +67,14 @@ def basta2nc(
|
|
63
67
|
basta.remove_duplicate_timestamps()
|
64
68
|
attributes = output.add_time_attribute(ATTRIBUTES, basta.date)
|
65
69
|
output.update_attributes(basta.data, attributes)
|
66
|
-
|
67
|
-
return uuid
|
70
|
+
return output.save_level1b(basta, output_file, uuid)
|
68
71
|
|
69
72
|
|
70
73
|
class Basta(NcRadar):
|
71
74
|
"""Class for BASTA raw radar data. Child of NcRadar().
|
72
75
|
|
73
76
|
Args:
|
77
|
+
----
|
74
78
|
full_path: BASTA netCDF filename.
|
75
79
|
site_meta: Site properties in a dictionary. Required key is `name`.
|
76
80
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import binascii
|
2
2
|
import re
|
3
|
-
from datetime import datetime
|
3
|
+
from datetime import datetime, timezone
|
4
4
|
from typing import NamedTuple
|
5
5
|
|
6
6
|
import numpy as np
|
@@ -13,7 +13,10 @@ from cloudnetpy.instruments.ceilometer import Ceilometer
|
|
13
13
|
|
14
14
|
class Cs135(Ceilometer):
|
15
15
|
def __init__(
|
16
|
-
self,
|
16
|
+
self,
|
17
|
+
full_path: str,
|
18
|
+
site_meta: dict,
|
19
|
+
expected_date: str | None = None,
|
17
20
|
):
|
18
21
|
super().__init__()
|
19
22
|
self.full_path = full_path
|
@@ -33,7 +36,10 @@ class Cs135(Ceilometer):
|
|
33
36
|
|
34
37
|
parts = re.split(rb"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{6}),", content)
|
35
38
|
for i in range(1, len(parts), 2):
|
36
|
-
timestamp = datetime.strptime(
|
39
|
+
timestamp = datetime.strptime(
|
40
|
+
parts[i].decode(),
|
41
|
+
"%Y-%m-%dT%H:%M:%S.%f",
|
42
|
+
).replace(tzinfo=timezone.utc)
|
37
43
|
try:
|
38
44
|
self._check_timestamp(timestamp)
|
39
45
|
except ValidTimeStampError:
|
@@ -49,13 +55,16 @@ class Cs135(Ceilometer):
|
|
49
55
|
range_resolutions.append(message.range_resolution)
|
50
56
|
|
51
57
|
if len(timestamps) == 0:
|
52
|
-
|
58
|
+
msg = "No valid timestamps found in the file"
|
59
|
+
raise ValidTimeStampError(msg)
|
53
60
|
range_resolution = range_resolutions[0]
|
54
61
|
n_gates = len(profiles[0])
|
55
62
|
if any(res != range_resolution for res in range_resolutions):
|
56
|
-
|
63
|
+
msg = "Inconsistent range resolution"
|
64
|
+
raise InconsistentDataError(msg)
|
57
65
|
if any(len(profile) != n_gates for profile in profiles):
|
58
|
-
|
66
|
+
msg = "Inconsistent number of gates"
|
67
|
+
raise InconsistentDataError(msg)
|
59
68
|
|
60
69
|
self.data["beta_raw"] = np.array(profiles)
|
61
70
|
if calibration_factor is None:
|
@@ -68,14 +77,18 @@ class Cs135(Ceilometer):
|
|
68
77
|
self.data["time"] = utils.datetime2decimal_hours(timestamps)
|
69
78
|
self.data["zenith_angle"] = np.median(tilt_angles)
|
70
79
|
|
71
|
-
def _check_timestamp(self, timestamp: datetime):
|
80
|
+
def _check_timestamp(self, timestamp: datetime) -> None:
|
72
81
|
timestamp_components = str(timestamp.date()).split("-")
|
73
|
-
if
|
74
|
-
|
75
|
-
|
82
|
+
if (
|
83
|
+
self.expected_date is not None
|
84
|
+
and timestamp_components != self.expected_date.split("-")
|
85
|
+
):
|
86
|
+
raise ValidTimeStampError
|
76
87
|
if not self.date:
|
77
88
|
self.date = timestamp_components
|
78
|
-
|
89
|
+
if timestamp_components != self.date:
|
90
|
+
msg = "Inconsistent dates in the file"
|
91
|
+
raise RuntimeError(msg)
|
79
92
|
|
80
93
|
|
81
94
|
class Message(NamedTuple):
|
@@ -100,18 +113,22 @@ def _read_message(message: bytes) -> Message:
|
|
100
113
|
expected_checksum = int(message[end_idx + 1 : end_idx + 5], 16)
|
101
114
|
actual_checksum = _crc16(content)
|
102
115
|
if expected_checksum != actual_checksum:
|
103
|
-
|
116
|
+
msg = (
|
104
117
|
"Invalid checksum: "
|
105
118
|
f"expected {expected_checksum:04x}, "
|
106
119
|
f"got {actual_checksum:04x}"
|
107
120
|
)
|
121
|
+
raise InvalidMessageError(msg)
|
108
122
|
lines = message.splitlines()
|
109
123
|
if len(lines[0]) != 11:
|
110
|
-
|
124
|
+
msg = f"Expected 11 characters in first line, got {len(lines[0])}"
|
125
|
+
raise NotImplementedError(msg)
|
111
126
|
if (msg_no := lines[0][-4:-1]) != b"002":
|
112
|
-
|
127
|
+
msg = f"Message number {msg_no.decode()} not implemented"
|
128
|
+
raise NotImplementedError(msg)
|
113
129
|
if len(lines) != 5:
|
114
|
-
|
130
|
+
msg = f"Expected 5 lines, got {len(lines)}"
|
131
|
+
raise InvalidMessageError(msg)
|
115
132
|
scale, res, n, energy, lt, ti, bl, pulse, rate, _sum = map(int, lines[2].split())
|
116
133
|
data = _read_backscatter(lines[3].strip(), n)
|
117
134
|
return Message(scale, res, energy, lt, ti, bl, pulse, rate, data)
|
@@ -124,7 +141,7 @@ def _read_backscatter(data: bytes, n_gates: int) -> np.ndarray:
|
|
124
141
|
limit = (1 << (n_bits - 1)) - 1
|
125
142
|
offset = 1 << n_bits
|
126
143
|
out = np.array(
|
127
|
-
[int(data[i : i + n_chars], 16) for i in range(0, n_gates * n_chars, n_chars)]
|
144
|
+
[int(data[i : i + n_chars], 16) for i in range(0, n_gates * n_chars, n_chars)],
|
128
145
|
)
|
129
146
|
out[out > limit] -= offset
|
130
147
|
return out
|
cloudnetpy/instruments/ceilo.py
CHANGED
@@ -40,6 +40,7 @@ def ceilo2nc(
|
|
40
40
|
of weak aerosol layers and supercooled liquid clouds.
|
41
41
|
|
42
42
|
Args:
|
43
|
+
----
|
43
44
|
full_path: Ceilometer file name.
|
44
45
|
output_file: Output file name, e.g. 'ceilo.nc'.
|
45
46
|
site_meta: Dictionary containing information about the site and instrument.
|
@@ -53,12 +54,15 @@ def ceilo2nc(
|
|
53
54
|
date: Expected date as YYYY-MM-DD of all profiles in the file.
|
54
55
|
|
55
56
|
Returns:
|
57
|
+
-------
|
56
58
|
UUID of the generated file.
|
57
59
|
|
58
60
|
Raises:
|
61
|
+
------
|
59
62
|
RuntimeError: Failed to read or process raw ceilometer data.
|
60
63
|
|
61
64
|
Examples:
|
65
|
+
--------
|
62
66
|
>>> from cloudnetpy.instruments import ceilo2nc
|
63
67
|
>>> site_meta = {'name': 'Mace-Head', 'altitude': 5}
|
64
68
|
>>> ceilo2nc('vaisala_raw.txt', 'vaisala.nc', site_meta)
|
@@ -74,12 +78,18 @@ def ceilo2nc(
|
|
74
78
|
ceilo_obj.read_ceilometer_file(calibration_factor)
|
75
79
|
ceilo_obj.check_beta_raw_shape()
|
76
80
|
ceilo_obj.data["beta"] = ceilo_obj.calc_screened_product(
|
77
|
-
ceilo_obj.data["beta_raw"],
|
81
|
+
ceilo_obj.data["beta_raw"],
|
82
|
+
snr_limit,
|
83
|
+
range_corrected=range_corrected,
|
78
84
|
)
|
79
85
|
ceilo_obj.data["beta_smooth"] = ceilo_obj.calc_beta_smooth(
|
80
|
-
ceilo_obj.data["beta"],
|
86
|
+
ceilo_obj.data["beta"],
|
87
|
+
snr_limit,
|
88
|
+
range_corrected=range_corrected,
|
81
89
|
)
|
82
|
-
|
90
|
+
if ceilo_obj.instrument is None or ceilo_obj.instrument.model is None:
|
91
|
+
msg = "Failed to read ceilometer model"
|
92
|
+
raise RuntimeError(msg)
|
83
93
|
if "cl61" in ceilo_obj.instrument.model.lower():
|
84
94
|
# This kind of screening could be used with other ceilometers as well:
|
85
95
|
mask = ceilo_obj.data["beta_smooth"].mask
|
@@ -94,12 +104,13 @@ def ceilo2nc(
|
|
94
104
|
output.update_attributes(ceilo_obj.data, attributes)
|
95
105
|
for key in ("beta", "beta_smooth"):
|
96
106
|
ceilo_obj.add_snr_info(key, snr_limit)
|
97
|
-
|
98
|
-
return uuid
|
107
|
+
return output.save_level1b(ceilo_obj, output_file, uuid)
|
99
108
|
|
100
109
|
|
101
110
|
def _initialize_ceilo(
|
102
|
-
full_path: str,
|
111
|
+
full_path: str,
|
112
|
+
site_meta: dict,
|
113
|
+
date: str | None = None,
|
103
114
|
) -> ClCeilo | Ct25k | LufftCeilo | Cl61d | Cs135:
|
104
115
|
if "model" in site_meta:
|
105
116
|
if site_meta["model"] not in (
|
@@ -110,7 +121,8 @@ def _initialize_ceilo(
|
|
110
121
|
"chm15k",
|
111
122
|
"cs135",
|
112
123
|
):
|
113
|
-
|
124
|
+
msg = f"Invalid ceilometer model: {site_meta['model']}"
|
125
|
+
raise ValueError(msg)
|
114
126
|
if site_meta["model"] in ("cl31", "cl51"):
|
115
127
|
model = "cl31_or_cl51"
|
116
128
|
else:
|
@@ -129,21 +141,26 @@ def _initialize_ceilo(
|
|
129
141
|
|
130
142
|
|
131
143
|
def _find_ceilo_model(full_path: str) -> str:
|
144
|
+
model = None
|
132
145
|
try:
|
133
146
|
with netCDF4.Dataset(full_path) as nc:
|
134
147
|
title = nc.title
|
135
148
|
for identifier in ["cl61d", "cl61-d"]:
|
136
149
|
if identifier in title.lower() or identifier in full_path.lower():
|
137
|
-
|
138
|
-
|
150
|
+
model = "cl61d"
|
151
|
+
if model is None:
|
152
|
+
model = "chm15k"
|
139
153
|
except OSError:
|
140
154
|
with open(full_path, "rb") as file:
|
141
155
|
for line in islice(file, 100):
|
142
156
|
if line.startswith(b"\x01CL"):
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
157
|
+
model = "cl31_or_cl51"
|
158
|
+
elif line.startswith(b"\x01CT"):
|
159
|
+
model = "ct25k"
|
160
|
+
if model is None:
|
161
|
+
msg = "Unable to determine ceilometer model"
|
162
|
+
raise RuntimeError(msg)
|
163
|
+
return model
|
147
164
|
|
148
165
|
|
149
166
|
ATTRIBUTES = {
|