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/instruments/lufft.py
CHANGED
@@ -13,7 +13,10 @@ class LufftCeilo(NcLidar):
|
|
13
13
|
"""Class for Lufft chm15k ceilometer."""
|
14
14
|
|
15
15
|
def __init__(
|
16
|
-
self,
|
16
|
+
self,
|
17
|
+
file_name: str,
|
18
|
+
site_meta: dict,
|
19
|
+
expected_date: str | None = None,
|
17
20
|
):
|
18
21
|
super().__init__()
|
19
22
|
self.file_name = file_name
|
@@ -31,32 +34,35 @@ class LufftCeilo(NcLidar):
|
|
31
34
|
self._fetch_zenith_angle("zenith")
|
32
35
|
|
33
36
|
def _fetch_beta_raw(self, calibration_factor: float | None = None) -> None:
|
34
|
-
assert self.dataset is not None
|
35
37
|
if calibration_factor is None:
|
36
38
|
logging.warning("Using default calibration factor")
|
37
39
|
calibration_factor = 3e-12
|
38
40
|
beta_raw = self._getvar("beta_raw", "beta_att")
|
41
|
+
beta_raw = ma.masked_array(beta_raw)
|
39
42
|
old_version = self._get_old_software_version()
|
40
43
|
if old_version is not None:
|
41
44
|
logging.warning(
|
42
|
-
|
45
|
+
"Software version %s. Assuming data not range corrected.",
|
46
|
+
old_version,
|
43
47
|
)
|
44
48
|
data_std = self._getvar("stddev")
|
45
49
|
normalised_apd = self._get_nn()
|
46
|
-
beta_raw *= utils.transpose(data_std / normalised_apd)
|
50
|
+
beta_raw *= utils.transpose(ma.masked_array(data_std / normalised_apd))
|
47
51
|
beta_raw *= self.data["range"] ** 2
|
48
52
|
beta_raw *= calibration_factor
|
49
53
|
self.data["calibration_factor"] = float(calibration_factor)
|
50
54
|
self.data["beta_raw"] = beta_raw
|
51
55
|
|
52
56
|
def _get_old_software_version(self) -> str | None:
|
53
|
-
|
57
|
+
if self.dataset is None:
|
58
|
+
msg = "No dataset found"
|
59
|
+
raise RuntimeError(msg)
|
54
60
|
version = self.dataset.software_version
|
55
61
|
if len(str(version)) > 4:
|
56
62
|
return None
|
57
63
|
return version
|
58
64
|
|
59
|
-
def _get_nn(self):
|
65
|
+
def _get_nn(self) -> float | ma.MaskedArray:
|
60
66
|
nn1 = self._getvar("nn1", "NN1")
|
61
67
|
median_nn1 = ma.median(nn1)
|
62
68
|
# Parameters taken from the matlab code and should be verified
|
@@ -69,18 +75,21 @@ class LufftCeilo(NcLidar):
|
|
69
75
|
return 1
|
70
76
|
return step_factor ** (-(nn1 - reference) / scale)
|
71
77
|
|
72
|
-
def _getvar(self, *args):
|
73
|
-
|
78
|
+
def _getvar(self, *args) -> float | ma.MaskedArray:
|
79
|
+
if self.dataset is None:
|
80
|
+
msg = "No dataset found"
|
81
|
+
raise RuntimeError(msg)
|
74
82
|
for arg in args:
|
75
83
|
if arg in self.dataset.variables:
|
76
84
|
var = self.dataset.variables[arg]
|
77
85
|
return var[0] if utils.isscalar(var) else var[:]
|
78
|
-
|
86
|
+
msg = f"Unable to find variable {args[0]}"
|
87
|
+
raise ValueError(msg)
|
79
88
|
|
80
|
-
def _fetch_attributes(self):
|
89
|
+
def _fetch_attributes(self) -> None:
|
81
90
|
self.serial_number = getattr(self.dataset, "device_name", None)
|
82
91
|
if self.serial_number is None:
|
83
|
-
self.serial_number = getattr(self.dataset, "source")
|
92
|
+
self.serial_number = getattr(self.dataset, "source", "")
|
84
93
|
self.instrument = (
|
85
94
|
instruments.CHM15KX
|
86
95
|
if self.serial_number.startswith("CHX")
|
cloudnetpy/instruments/mira.py
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
import logging
|
3
3
|
import os
|
4
4
|
from collections import OrderedDict
|
5
|
-
from tempfile import TemporaryDirectory
|
5
|
+
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
6
6
|
|
7
7
|
from numpy import ma
|
8
8
|
|
@@ -27,6 +27,7 @@ def mira2nc(
|
|
27
27
|
steps.
|
28
28
|
|
29
29
|
Args:
|
30
|
+
----
|
30
31
|
raw_mira: Filename of a daily MIRA .mmclx or .zncfile. Can be also a folder
|
31
32
|
containing several non-concatenated .mmclx or .znc files from one day
|
32
33
|
or list of files. znc files take precedence because they are the newer
|
@@ -38,15 +39,18 @@ def mira2nc(
|
|
38
39
|
date: Expected date as YYYY-MM-DD of all profiles in the file.
|
39
40
|
|
40
41
|
Returns:
|
42
|
+
-------
|
41
43
|
UUID of the generated file.
|
42
44
|
|
43
45
|
Raises:
|
46
|
+
------
|
44
47
|
ValidTimeStampError: No valid timestamps found.
|
45
48
|
FileNotFoundError: No suitable input files found.
|
46
49
|
ValueError: Wrong suffix in input file(s).
|
47
50
|
TypeError: Mixed mmclx and znc files.
|
48
51
|
|
49
52
|
Examples:
|
53
|
+
--------
|
50
54
|
>>> from cloudnetpy.instruments import mira2nc
|
51
55
|
>>> site_meta = {'name': 'Vehmasmaki'}
|
52
56
|
>>> mira2nc('raw_radar.mmclx', 'radar.nc', site_meta)
|
@@ -55,7 +59,6 @@ def mira2nc(
|
|
55
59
|
>>> mira2nc('/one/day/of/mira/znc/files/', 'radar.nc', site_meta)
|
56
60
|
|
57
61
|
"""
|
58
|
-
|
59
62
|
with TemporaryDirectory() as temp_dir:
|
60
63
|
input_filename, keymap = _parse_input_files(raw_mira, temp_dir)
|
61
64
|
|
@@ -79,14 +82,14 @@ def mira2nc(
|
|
79
82
|
mira.test_if_all_masked()
|
80
83
|
attributes = output.add_time_attribute(ATTRIBUTES, mira.date)
|
81
84
|
output.update_attributes(mira.data, attributes)
|
82
|
-
|
83
|
-
return uuid
|
85
|
+
return output.save_level1b(mira, output_file, uuid)
|
84
86
|
|
85
87
|
|
86
88
|
class Mira(NcRadar):
|
87
89
|
"""Class for MIRA-35 raw radar data. Child of NcRadar().
|
88
90
|
|
89
91
|
Args:
|
92
|
+
----
|
90
93
|
full_path: Filename of a daily MIRA .mmclx NetCDF file.
|
91
94
|
site_meta: Site properties in a dictionary. Required keys are: `name`.
|
92
95
|
|
@@ -115,7 +118,7 @@ class Mira(NcRadar):
|
|
115
118
|
time_stamps = self.getvar("time")
|
116
119
|
return utils.seconds2date(float(time_stamps[0]), self.epoch)[:3]
|
117
120
|
|
118
|
-
def screen_invalid_ldr(self):
|
121
|
+
def screen_invalid_ldr(self) -> None:
|
119
122
|
"""Masks LDR in MIRA STSR mode data.
|
120
123
|
Is there a better way to identify this mode?
|
121
124
|
"""
|
@@ -125,53 +128,60 @@ class Mira(NcRadar):
|
|
125
128
|
if ma.mean(ldr) > 0:
|
126
129
|
logging.warning(
|
127
130
|
"LDR values suspiciously high. Mira in STSR mode? "
|
128
|
-
"Screening all LDR for now."
|
131
|
+
"Screening all LDR for now.",
|
129
132
|
)
|
130
133
|
self.data["ldr"].data[:] = ma.masked
|
131
134
|
|
132
135
|
|
133
136
|
def _parse_input_files(input_files: str | list[str], temp_dir: str) -> tuple:
|
134
137
|
if isinstance(input_files, list) or os.path.isdir(input_files):
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
)
|
148
|
-
|
149
|
-
valid_files = utils.get_files_with_common_range(valid_files)
|
150
|
-
filetypes = list({f.split(".")[-1].lower() for f in valid_files})
|
138
|
+
with NamedTemporaryFile(
|
139
|
+
dir=temp_dir,
|
140
|
+
suffix=".nc",
|
141
|
+
delete=False,
|
142
|
+
) as temp_file:
|
143
|
+
input_filename = temp_file.name
|
144
|
+
if isinstance(input_files, list):
|
145
|
+
valid_files = sorted(input_files)
|
146
|
+
else:
|
147
|
+
valid_files = utils.get_sorted_filenames(input_files, ".znc")
|
148
|
+
if not valid_files:
|
149
|
+
valid_files = utils.get_sorted_filenames(input_files, ".mmclx")
|
151
150
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
151
|
+
if not valid_files:
|
152
|
+
msg = (
|
153
|
+
(
|
154
|
+
f"Neither znc nor mmclx files found {input_files}. "
|
155
|
+
f"Please check your input."
|
156
|
+
),
|
157
|
+
)
|
158
|
+
raise FileNotFoundError(msg)
|
159
|
+
|
160
|
+
valid_files = utils.get_files_with_common_range(valid_files)
|
161
|
+
filetypes = list({f.split(".")[-1].lower() for f in valid_files})
|
162
|
+
|
163
|
+
if len(filetypes) > 1:
|
164
|
+
err_msg = "Mixed mmclx and znc files. Please use only one filetype."
|
165
|
+
raise TypeError(err_msg)
|
166
|
+
|
167
|
+
keymap = _get_keymap(filetypes[0])
|
168
|
+
|
169
|
+
variables = list(keymap.keys())
|
170
|
+
concat_lib.concatenate_files(
|
171
|
+
valid_files,
|
172
|
+
input_filename,
|
173
|
+
variables=variables,
|
174
|
+
ignore=_get_ignored_variables(filetypes[0]),
|
175
|
+
# It's somewhat risky to use varying nfft values as the velocity
|
176
|
+
# resolution may differ, but this enables concatenation when switching
|
177
|
+
# between different nfft configurations. Spectral data is ignored
|
178
|
+
# anyway for now.
|
179
|
+
allow_difference=[
|
180
|
+
"nave",
|
181
|
+
"ovl",
|
182
|
+
"nfft",
|
183
|
+
],
|
156
184
|
)
|
157
|
-
|
158
|
-
keymap = _get_keymap(filetypes[0])
|
159
|
-
|
160
|
-
variables = list(keymap.keys())
|
161
|
-
concat_lib.concatenate_files(
|
162
|
-
valid_files,
|
163
|
-
input_filename,
|
164
|
-
variables=variables,
|
165
|
-
ignore=_get_ignored_variables(filetypes[0]),
|
166
|
-
# It's somewhat risky to use varying nfft values as the velocity resolution
|
167
|
-
# may differ, but this enables concatenation when switching between
|
168
|
-
# different nfft configurations. Spectral data is ignored anyway for now.
|
169
|
-
allow_difference=[
|
170
|
-
"nave",
|
171
|
-
"ovl",
|
172
|
-
"nfft",
|
173
|
-
],
|
174
|
-
)
|
175
185
|
else:
|
176
186
|
input_filename = input_files
|
177
187
|
keymap = _get_keymap(input_filename.split(".")[-1])
|
@@ -193,8 +203,8 @@ def _get_ignored_variables(filetype: str) -> list | None:
|
|
193
203
|
|
194
204
|
def _get_keymap(filetype: str) -> dict:
|
195
205
|
"""Returns a dictionary mapping the variables in the raw data to the processed
|
196
|
-
Cloudnet file.
|
197
|
-
|
206
|
+
Cloudnet file.
|
207
|
+
"""
|
198
208
|
_check_file_type(filetype)
|
199
209
|
|
200
210
|
# Order is relevant with the new znc files from STSR radar
|
@@ -218,7 +228,7 @@ def _get_keymap(filetype: str) -> dict:
|
|
218
228
|
("nave", "nave"),
|
219
229
|
("prf", "prf"),
|
220
230
|
("rg0", "rg0"),
|
221
|
-
]
|
231
|
+
],
|
222
232
|
),
|
223
233
|
"mmclx": {
|
224
234
|
"Zg": "Zh",
|
@@ -240,10 +250,11 @@ def _get_keymap(filetype: str) -> dict:
|
|
240
250
|
return keymaps.get(filetype.lower(), keymaps["mmclx"])
|
241
251
|
|
242
252
|
|
243
|
-
def _check_file_type(filetype: str):
|
253
|
+
def _check_file_type(filetype: str) -> None:
|
244
254
|
known_filetypes = ["znc", "mmclx"]
|
245
255
|
if filetype.lower() not in known_filetypes:
|
246
|
-
|
256
|
+
msg = f"Filetype must be one of {known_filetypes}"
|
257
|
+
raise ValueError(msg)
|
247
258
|
|
248
259
|
|
249
260
|
ATTRIBUTES = {
|
cloudnetpy/instruments/mrr.py
CHANGED
@@ -4,7 +4,7 @@ import re
|
|
4
4
|
from collections.abc import Iterable
|
5
5
|
from os import PathLike
|
6
6
|
from pathlib import Path
|
7
|
-
from tempfile import TemporaryDirectory
|
7
|
+
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
8
8
|
from uuid import UUID
|
9
9
|
|
10
10
|
import netCDF4
|
@@ -29,6 +29,7 @@ def mrr2nc(
|
|
29
29
|
contains only the relevant data.
|
30
30
|
|
31
31
|
Args:
|
32
|
+
----
|
32
33
|
input_file: Filename of a daily MMR-PRO .nc file, path to directory
|
33
34
|
containing several non-concatenated .nc files from one day, or list
|
34
35
|
of filenames.
|
@@ -39,17 +40,19 @@ def mrr2nc(
|
|
39
40
|
date: Expected date as YYYY-MM-DD of all profiles in the file.
|
40
41
|
|
41
42
|
Returns:
|
43
|
+
-------
|
42
44
|
UUID of the generated file.
|
43
45
|
|
44
46
|
Raises:
|
47
|
+
------
|
45
48
|
ValidTimeStampError: No valid timestamps found.
|
46
49
|
|
47
50
|
Examples:
|
51
|
+
--------
|
48
52
|
>>> from cloudnetpy.instruments import mira2nc
|
49
53
|
>>> site_meta = {'name': 'LIM', 'latitude': 51.333, 'longitude': 12.389}
|
50
54
|
>>> mrr2nc('input.nc', 'output.nc', site_meta)
|
51
55
|
"""
|
52
|
-
|
53
56
|
if isinstance(uuid, str):
|
54
57
|
uuid = UUID(uuid)
|
55
58
|
if isinstance(date, str):
|
@@ -70,26 +73,32 @@ def mrr2nc(
|
|
70
73
|
with netCDF4.Dataset(file):
|
71
74
|
yield file
|
72
75
|
except OSError:
|
73
|
-
logging.warning(
|
74
|
-
|
75
|
-
def concat_files(
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
tmp_filename
|
82
|
-
variables=
|
83
|
-
|
84
|
-
|
85
|
-
|
76
|
+
logging.warning("Skipping invalid file: %s", file)
|
77
|
+
|
78
|
+
def concat_files(dir_name: str, files: Iterable[PathLike | str]) -> str:
|
79
|
+
with NamedTemporaryFile(
|
80
|
+
dir=dir_name,
|
81
|
+
suffix=".nc",
|
82
|
+
delete=False,
|
83
|
+
) as temp_file:
|
84
|
+
tmp_filename = temp_file.name
|
85
|
+
variables = [*keymap.keys(), "elevation"]
|
86
|
+
valid_files = list(valid_nc_files(files))
|
87
|
+
concat_lib.concatenate_files(
|
88
|
+
valid_files,
|
89
|
+
tmp_filename,
|
90
|
+
variables=variables,
|
91
|
+
ignore=["time_coverage_start", "time_coverage_end"],
|
92
|
+
)
|
93
|
+
return tmp_filename
|
86
94
|
|
87
95
|
with TemporaryDirectory() as temp_dir:
|
88
96
|
if isinstance(input_file, PathLike | str):
|
89
97
|
path = Path(input_file)
|
90
98
|
if path.is_dir():
|
91
99
|
input_file = concat_files(
|
92
|
-
temp_dir,
|
100
|
+
temp_dir,
|
101
|
+
(p for p in path.iterdir() if p.suffix.lower() == ".nc"),
|
93
102
|
)
|
94
103
|
else:
|
95
104
|
input_file = concat_files(temp_dir, input_file)
|
@@ -108,14 +117,14 @@ def mrr2nc(
|
|
108
117
|
mrr.sort_timestamps()
|
109
118
|
attributes = output.add_time_attribute(ATTRIBUTES, mrr.date)
|
110
119
|
output.update_attributes(mrr.data, attributes)
|
111
|
-
|
112
|
-
return uuid
|
120
|
+
return output.save_level1b(mrr, output_file, uuid)
|
113
121
|
|
114
122
|
|
115
123
|
class MrrPro(NcRadar):
|
116
124
|
"""Class for MRR-PRO raw data. Child of NcRadar().
|
117
125
|
|
118
126
|
Args:
|
127
|
+
----
|
119
128
|
full_path: MRR-PRO netCDF filename.
|
120
129
|
site_meta: Site properties in a dictionary. Required keys are `name`,
|
121
130
|
`latitude`, `longitude` and `altitude`.
|
@@ -128,7 +137,9 @@ class MrrPro(NcRadar):
|
|
128
137
|
super().__init__(full_path, site_meta)
|
129
138
|
self.instrument = instruments.MRR_PRO
|
130
139
|
if m := re.search(
|
131
|
-
r"serial number:\s*(\w+)",
|
140
|
+
r"serial number:\s*(\w+)",
|
141
|
+
self.dataset.instrument_name,
|
142
|
+
re.I,
|
132
143
|
):
|
133
144
|
self.serial_number = m[1]
|
134
145
|
|
@@ -136,7 +147,7 @@ class MrrPro(NcRadar):
|
|
136
147
|
time_stamps = self.getvar("time")
|
137
148
|
return utils.seconds2date(time_stamps[0], (1970, 1, 1))[:3]
|
138
149
|
|
139
|
-
def fix_units(self):
|
150
|
+
def fix_units(self) -> None:
|
140
151
|
self.data["v"].data *= -1 # towards -> away from instrument
|
141
152
|
self.data["rainfall_rate"].data /= 3600000 # mm h-1 -> m s-1
|
142
153
|
self.data["lwc"].data *= 0.001 # g m-3 -> kg m-3
|
@@ -1,13 +1,16 @@
|
|
1
1
|
"""Module with a class for Lufft chm15k ceilometer."""
|
2
2
|
import logging
|
3
|
+
from typing import TYPE_CHECKING, Literal
|
3
4
|
|
4
|
-
import netCDF4
|
5
5
|
import numpy as np
|
6
6
|
from numpy import ma
|
7
7
|
|
8
8
|
from cloudnetpy import utils
|
9
9
|
from cloudnetpy.instruments.ceilometer import Ceilometer
|
10
10
|
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
import netCDF4
|
13
|
+
|
11
14
|
|
12
15
|
class NcLidar(Ceilometer):
|
13
16
|
"""Class for all lidars using netCDF files."""
|
@@ -16,25 +19,31 @@ class NcLidar(Ceilometer):
|
|
16
19
|
super().__init__()
|
17
20
|
self.dataset: netCDF4.Dataset | None = None
|
18
21
|
|
19
|
-
def _fetch_range(self, reference:
|
20
|
-
|
22
|
+
def _fetch_range(self, reference: Literal["upper", "lower"]) -> None:
|
23
|
+
if self.dataset is None:
|
24
|
+
msg = "No dataset found"
|
25
|
+
raise RuntimeError(msg)
|
21
26
|
range_instrument = self.dataset.variables["range"][:]
|
22
27
|
self.data["range"] = utils.edges2mid(range_instrument, reference)
|
23
28
|
|
24
29
|
def _fetch_time_and_date(self) -> None:
|
25
|
-
|
30
|
+
if self.dataset is None:
|
31
|
+
msg = "No dataset found"
|
32
|
+
raise RuntimeError(msg)
|
26
33
|
time = self.dataset.variables["time"]
|
27
34
|
self.data["time"] = time[:]
|
28
35
|
epoch = utils.get_epoch(time.units)
|
29
36
|
self.get_date_and_time(epoch)
|
30
37
|
|
31
38
|
def _fetch_zenith_angle(self, key: str, default: float = 3.0) -> None:
|
32
|
-
|
39
|
+
if self.dataset is None:
|
40
|
+
msg = "No dataset found"
|
41
|
+
raise RuntimeError(msg)
|
33
42
|
if key in self.dataset.variables:
|
34
43
|
zenith_angle = ma.median(self.dataset.variables[key][:])
|
35
44
|
else:
|
36
45
|
zenith_angle = float(default)
|
37
|
-
logging.warning(
|
46
|
+
logging.warning("No zenith angle found, assuming %s degrees", zenith_angle)
|
38
47
|
if zenith_angle == 0:
|
39
48
|
logging.warning("Zenith angle 0 degrees - risk of specular reflection")
|
40
49
|
self.data["zenith_angle"] = np.array(zenith_angle)
|
@@ -1,6 +1,7 @@
|
|
1
1
|
"""Module for reading raw cloud radar data."""
|
2
2
|
import logging
|
3
3
|
from os import PathLike
|
4
|
+
from typing import TYPE_CHECKING
|
4
5
|
|
5
6
|
import numpy as np
|
6
7
|
from numpy import ma
|
@@ -10,17 +11,21 @@ from cloudnetpy.cloudnetarray import CloudnetArray
|
|
10
11
|
from cloudnetpy.datasource import DataSource
|
11
12
|
from cloudnetpy.exceptions import RadarDataError, ValidTimeStampError
|
12
13
|
from cloudnetpy.instruments.cloudnet_instrument import CloudnetInstrument
|
13
|
-
|
14
|
+
|
15
|
+
if TYPE_CHECKING:
|
16
|
+
from cloudnetpy.instruments.instruments import Instrument
|
14
17
|
|
15
18
|
|
16
19
|
class NcRadar(DataSource, CloudnetInstrument):
|
17
20
|
"""Class for radars providing netCDF files. Child of DataSource().
|
18
21
|
|
19
22
|
Args:
|
23
|
+
----
|
20
24
|
full_path: Filename of a radar-produced netCDF file.
|
21
25
|
site_meta: Some metadata of the site.
|
22
26
|
|
23
27
|
Notes:
|
28
|
+
-----
|
24
29
|
Used with BASTA, MIRA and Copernicus radars.
|
25
30
|
"""
|
26
31
|
|
@@ -37,7 +42,7 @@ class NcRadar(DataSource, CloudnetInstrument):
|
|
37
42
|
try:
|
38
43
|
array = self.getvar(key)
|
39
44
|
except RuntimeError:
|
40
|
-
logging.warning(
|
45
|
+
logging.warning("Can not find variable %s from the input file", key)
|
41
46
|
continue
|
42
47
|
array = np.array(array) if utils.isscalar(array) else array
|
43
48
|
array[~np.isfinite(array)] = ma.masked
|
@@ -46,7 +51,7 @@ class NcRadar(DataSource, CloudnetInstrument):
|
|
46
51
|
def add_time_and_range(self) -> None:
|
47
52
|
"""Adds time and range."""
|
48
53
|
range_instru = np.array(
|
49
|
-
self.getvar("range", "height")
|
54
|
+
self.getvar("range", "height"),
|
50
55
|
) # "height" in old BASTA files
|
51
56
|
time = np.array(self.time)
|
52
57
|
self.append_data(range_instru, "range")
|
@@ -78,7 +83,7 @@ class NcRadar(DataSource, CloudnetInstrument):
|
|
78
83
|
cloudnet_array.mask_indices(z_mask)
|
79
84
|
cloudnet_array.mask_indices(v_mask)
|
80
85
|
|
81
|
-
def mask_first_range_gates(self, range_limit: float = 150):
|
86
|
+
def mask_first_range_gates(self, range_limit: float = 150) -> None:
|
82
87
|
"""Masks first range gates."""
|
83
88
|
if "v" not in self.data or "range" not in self.data:
|
84
89
|
return
|
@@ -88,13 +93,11 @@ class NcRadar(DataSource, CloudnetInstrument):
|
|
88
93
|
|
89
94
|
def add_zenith_and_azimuth_angles(self) -> list:
|
90
95
|
"""Adds non-varying instrument zenith and azimuth angles and returns valid
|
91
|
-
time indices.
|
96
|
+
time indices.
|
97
|
+
"""
|
92
98
|
if "azimuth_velocity" in self.data:
|
93
99
|
azimuth = self.data["azimuth_velocity"].data
|
94
|
-
if np.all(azimuth == azimuth[0])
|
95
|
-
azimuth_reference = azimuth[0]
|
96
|
-
else:
|
97
|
-
azimuth_reference = 0
|
100
|
+
azimuth_reference = azimuth[0] if np.all(azimuth == azimuth[0]) else 0
|
98
101
|
azimuth_tolerance = 1e-6
|
99
102
|
else:
|
100
103
|
azimuth = self.data["azimuth_angle"].data
|
@@ -105,19 +108,22 @@ class NcRadar(DataSource, CloudnetInstrument):
|
|
105
108
|
zenith = 90 - elevation
|
106
109
|
is_stable_zenith = np.isclose(zenith, ma.median(zenith), atol=0.1)
|
107
110
|
is_stable_azimuth = np.isclose(
|
108
|
-
azimuth,
|
111
|
+
azimuth,
|
112
|
+
azimuth_reference,
|
113
|
+
atol=azimuth_tolerance,
|
109
114
|
)
|
110
115
|
is_stable_profile = is_stable_zenith & is_stable_azimuth
|
111
116
|
if ma.isMaskedArray(is_stable_profile):
|
112
117
|
is_stable_profile[is_stable_profile.mask] = False
|
113
118
|
n_removed = np.count_nonzero(~is_stable_profile)
|
114
119
|
if n_removed >= len(zenith) - 1:
|
115
|
-
|
116
|
-
|
117
|
-
|
120
|
+
msg = "Less than two profiles with valid zenith / azimuth angles"
|
121
|
+
raise ValidTimeStampError(msg)
|
122
|
+
|
118
123
|
if n_removed > 0:
|
119
124
|
logging.warning(
|
120
|
-
|
125
|
+
"Filtering %s profiles due to varying zenith / azimuth angle",
|
126
|
+
n_removed,
|
121
127
|
)
|
122
128
|
self.append_data(zenith, "zenith_angle")
|
123
129
|
for key in ("elevation", "azimuth_velocity"):
|
@@ -125,9 +131,11 @@ class NcRadar(DataSource, CloudnetInstrument):
|
|
125
131
|
del self.data[key]
|
126
132
|
return list(is_stable_profile)
|
127
133
|
|
128
|
-
def add_radar_specific_variables(self):
|
134
|
+
def add_radar_specific_variables(self) -> None:
|
129
135
|
"""Adds radar specific variables."""
|
130
|
-
|
136
|
+
if self.instrument is None:
|
137
|
+
msg = "Instrument not defined"
|
138
|
+
raise RuntimeError(msg)
|
131
139
|
key = "radar_frequency"
|
132
140
|
self.data[key] = CloudnetArray(self.instrument.frequency, key)
|
133
141
|
try:
|
@@ -140,26 +148,27 @@ class NcRadar(DataSource, CloudnetInstrument):
|
|
140
148
|
except RuntimeError:
|
141
149
|
logging.warning("Unable to find nyquist_velocity")
|
142
150
|
|
143
|
-
def test_if_all_masked(self):
|
151
|
+
def test_if_all_masked(self) -> None:
|
144
152
|
"""Tests if all data are masked."""
|
145
153
|
if not np.any(~self.data["v"][:].mask):
|
146
|
-
|
154
|
+
msg = "All radar data are masked"
|
155
|
+
raise RadarDataError(msg)
|
147
156
|
|
148
157
|
|
149
158
|
class ChilboltonRadar(NcRadar):
|
150
159
|
"""Class for Chilbolton cloud radars Galileo and Copernicus."""
|
151
160
|
|
152
|
-
def __init__(self, full_path: str, site_meta: dict):
|
161
|
+
def __init__(self, full_path: str, site_meta: dict) -> None:
|
153
162
|
super().__init__(full_path, site_meta)
|
154
163
|
self.date = self._init_date()
|
155
164
|
|
156
|
-
def add_nyquist_velocity(self, keymap: dict):
|
165
|
+
def add_nyquist_velocity(self, keymap: dict) -> None:
|
157
166
|
"""Adds nyquist velocity."""
|
158
|
-
key = [key for key, value in keymap.items() if value == "v"][0]
|
167
|
+
key = [key for key, value in keymap.items() if value == "v"][0] # noqa: RUF015
|
159
168
|
folding_velocity = self.dataset.variables[key].folding_velocity
|
160
169
|
self.append_data(np.array(folding_velocity), "nyquist_velocity")
|
161
170
|
|
162
|
-
def check_date(self, date: str):
|
171
|
+
def check_date(self, date: str) -> None:
|
163
172
|
if self.date != date.split("-"):
|
164
173
|
raise ValidTimeStampError
|
165
174
|
|