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
@@ -22,10 +22,10 @@ def pollyxt2nc(
|
|
22
22
|
uuid: str | None = None,
|
23
23
|
date: str | None = None,
|
24
24
|
) -> str:
|
25
|
-
"""
|
26
|
-
Converts PollyXT Raman lidar data into Cloudnet Level 1b netCDF file.
|
25
|
+
"""Converts PollyXT Raman lidar data into Cloudnet Level 1b netCDF file.
|
27
26
|
|
28
27
|
Args:
|
28
|
+
----
|
29
29
|
input_folder: Path to pollyxt netCDF files.
|
30
30
|
output_file: Output filename.
|
31
31
|
site_meta: Dictionary containing information about the site with keys:
|
@@ -40,9 +40,11 @@ def pollyxt2nc(
|
|
40
40
|
date: Expected date of the measurements as YYYY-MM-DD.
|
41
41
|
|
42
42
|
Returns:
|
43
|
+
-------
|
43
44
|
UUID of the generated file.
|
44
45
|
|
45
46
|
Examples:
|
47
|
+
--------
|
46
48
|
>>> from cloudnetpy.instruments import pollyxt2nc
|
47
49
|
>>> site_meta = {'name': 'Mindelo', 'altitude': 13, 'zenith_angle': 6,
|
48
50
|
'snr_limit': 3}
|
@@ -61,8 +63,7 @@ def pollyxt2nc(
|
|
61
63
|
attributes = output.add_time_attribute(ATTRIBUTES, polly.date)
|
62
64
|
output.update_attributes(polly.data, attributes)
|
63
65
|
polly.add_snr_info("beta", snr_limit)
|
64
|
-
|
65
|
-
return uuid
|
66
|
+
return output.save_level1b(polly, output_file, uuid)
|
66
67
|
|
67
68
|
|
68
69
|
class PollyXt(Ceilometer):
|
@@ -72,16 +73,17 @@ class PollyXt(Ceilometer):
|
|
72
73
|
self.expected_date = expected_date
|
73
74
|
self.instrument = instruments.POLLYXT
|
74
75
|
|
75
|
-
def mask_nan_values(self):
|
76
|
+
def mask_nan_values(self) -> None:
|
76
77
|
for array in self.data.values():
|
77
78
|
if getattr(array, "ndim", 0) > 0:
|
78
79
|
array[np.isnan(array)] = ma.masked
|
79
80
|
|
80
|
-
def calc_screened_products(self, snr_limit: float = 5.0):
|
81
|
+
def calc_screened_products(self, snr_limit: float = 5.0) -> None:
|
81
82
|
keys = ("beta", "depolarisation")
|
82
83
|
for key in keys:
|
83
84
|
self.data[key] = ma.masked_where(
|
84
|
-
self.data["snr"] < snr_limit,
|
85
|
+
self.data["snr"] < snr_limit,
|
86
|
+
self.data[f"{key}_raw"],
|
85
87
|
)
|
86
88
|
self.data["depolarisation"][self.data["depolarisation"] > 1] = ma.masked
|
87
89
|
self.data["depolarisation"][self.data["depolarisation"] < 0] = ma.masked
|
@@ -99,19 +101,21 @@ class PollyXt(Ceilometer):
|
|
99
101
|
bsc_files.sort()
|
100
102
|
depol_files.sort()
|
101
103
|
if not bsc_files:
|
102
|
-
|
104
|
+
msg = "No pollyxt bsc files found"
|
105
|
+
raise RuntimeError(msg)
|
103
106
|
if len(bsc_files) != len(depol_files):
|
104
|
-
|
105
|
-
|
106
|
-
)
|
107
|
+
msg = "Inconsistent number of pollyxt bsc / depol files"
|
108
|
+
raise InconsistentDataError(msg)
|
107
109
|
self._fetch_attributes(bsc_files[0])
|
108
110
|
self.data["range"] = _read_array_from_multiple_files(
|
109
|
-
bsc_files,
|
111
|
+
bsc_files,
|
112
|
+
depol_files,
|
113
|
+
"height",
|
110
114
|
)
|
111
115
|
calibration_factors: np.ndarray = np.array([])
|
112
116
|
beta_channel = self._get_valid_beta_channel(bsc_files)
|
113
117
|
bsc_key = f"attenuated_backscatter_{beta_channel}nm"
|
114
|
-
for bsc_file, depol_file in zip(bsc_files, depol_files):
|
118
|
+
for bsc_file, depol_file in zip(bsc_files, depol_files, strict=True):
|
115
119
|
with (
|
116
120
|
netCDF4.Dataset(bsc_file, "r") as nc_bsc,
|
117
121
|
netCDF4.Dataset(depol_file, "r") as nc_depol,
|
@@ -119,11 +123,14 @@ class PollyXt(Ceilometer):
|
|
119
123
|
epoch = utils.get_epoch(nc_bsc["time"].unit)
|
120
124
|
try:
|
121
125
|
time = np.array(
|
122
|
-
_read_array_from_file_pair(nc_bsc, nc_depol, "time")
|
126
|
+
_read_array_from_file_pair(nc_bsc, nc_depol, "time"),
|
123
127
|
)
|
124
128
|
except AssertionError as err:
|
125
129
|
logging.warning(
|
126
|
-
|
130
|
+
"Ignoring files '%s' and '%s': %s",
|
131
|
+
nc_bsc,
|
132
|
+
nc_depol,
|
133
|
+
err,
|
127
134
|
)
|
128
135
|
continue
|
129
136
|
beta_raw = nc_bsc.variables[bsc_key][:]
|
@@ -132,6 +139,7 @@ class PollyXt(Ceilometer):
|
|
132
139
|
for array, key in zip(
|
133
140
|
[beta_raw, depol_raw, time, snr],
|
134
141
|
["beta_raw", "depolarisation_raw", "time", "snr"],
|
142
|
+
strict=True,
|
135
143
|
):
|
136
144
|
self.data = utils.append_data(self.data, key, array)
|
137
145
|
calibration_factor = nc_bsc.variables[
|
@@ -139,7 +147,7 @@ class PollyXt(Ceilometer):
|
|
139
147
|
].Lidar_calibration_constant_used
|
140
148
|
calibration_factor = np.repeat(calibration_factor, len(time))
|
141
149
|
calibration_factors = np.concatenate(
|
142
|
-
[calibration_factors, calibration_factor]
|
150
|
+
[calibration_factors, calibration_factor],
|
143
151
|
)
|
144
152
|
self.data["calibration_factor"] = calibration_factors
|
145
153
|
return epoch
|
@@ -153,11 +161,16 @@ class PollyXt(Ceilometer):
|
|
153
161
|
if not _only_zeros_or_masked(beta):
|
154
162
|
if channel != polly_channels[0]:
|
155
163
|
logging.warning(
|
156
|
-
|
164
|
+
"Using %s nm pollyXT channel for backscatter",
|
165
|
+
channel,
|
157
166
|
)
|
158
|
-
self.instrument
|
167
|
+
if self.instrument is None:
|
168
|
+
msg = "No instrument defined"
|
169
|
+
raise RuntimeError(msg)
|
170
|
+
self.instrument.wavelength = float(channel)
|
159
171
|
return channel
|
160
|
-
|
172
|
+
msg = "No functional pollyXT backscatter channels found"
|
173
|
+
raise ValidTimeStampError(msg)
|
161
174
|
|
162
175
|
def _fetch_attributes(self, file: str) -> None:
|
163
176
|
with netCDF4.Dataset(file, "r") as nc:
|
@@ -167,7 +180,7 @@ class PollyXt(Ceilometer):
|
|
167
180
|
|
168
181
|
def _read_array_from_multiple_files(files1: list, files2: list, key) -> np.ndarray:
|
169
182
|
array: np.ndarray = np.array([])
|
170
|
-
for ind, (file1, file2) in enumerate(zip(files1, files2)):
|
183
|
+
for ind, (file1, file2) in enumerate(zip(files1, files2, strict=True)):
|
171
184
|
with netCDF4.Dataset(file1, "r") as nc1, netCDF4.Dataset(file2, "r") as nc2:
|
172
185
|
array1 = _read_array_from_file_pair(nc1, nc2, key)
|
173
186
|
if ind == 0:
|
@@ -177,7 +190,9 @@ def _read_array_from_multiple_files(files1: list, files2: list, key) -> np.ndarr
|
|
177
190
|
|
178
191
|
|
179
192
|
def _read_array_from_file_pair(
|
180
|
-
nc_file1: netCDF4.Dataset,
|
193
|
+
nc_file1: netCDF4.Dataset,
|
194
|
+
nc_file2: netCDF4.Dataset,
|
195
|
+
key: str,
|
181
196
|
) -> np.ndarray:
|
182
197
|
array1 = nc_file1.variables[key][:]
|
183
198
|
array2 = nc_file2.variables[key][:]
|
@@ -24,6 +24,7 @@ def radiometrics2nc(
|
|
24
24
|
"""Converts Radiometrics .csv file into Cloudnet Level 1b netCDF file.
|
25
25
|
|
26
26
|
Args:
|
27
|
+
----
|
27
28
|
full_path: Input file name or folder containing multiple input files.
|
28
29
|
output_file: Output file name, e.g. 'radiometrics.nc'.
|
29
30
|
site_meta: Dictionary containing information about the site and instrument.
|
@@ -33,9 +34,11 @@ def radiometrics2nc(
|
|
33
34
|
date: Expected date as YYYY-MM-DD of all profiles in the file.
|
34
35
|
|
35
36
|
Returns:
|
37
|
+
-------
|
36
38
|
UUID of the generated file.
|
37
39
|
|
38
40
|
Examples:
|
41
|
+
--------
|
39
42
|
>>> from cloudnetpy.instruments import radiometrics2nc
|
40
43
|
>>> site_meta = {'name': 'Soverato', 'altitude': 21}
|
41
44
|
>>> radiometrics2nc('radiometrics.csv', 'radiometrics.nc', site_meta)
|
@@ -61,11 +64,12 @@ def radiometrics2nc(
|
|
61
64
|
radiometrics.time_to_fractional_hours()
|
62
65
|
radiometrics.data_to_cloudnet_arrays()
|
63
66
|
radiometrics.add_meta()
|
64
|
-
|
67
|
+
if radiometrics.date is None:
|
68
|
+
msg = "Failed to find valid timestamps from Radiometrics file(s)."
|
69
|
+
raise ValidTimeStampError(msg)
|
65
70
|
attributes = output.add_time_attribute({}, radiometrics.date)
|
66
71
|
output.update_attributes(radiometrics.data, attributes)
|
67
|
-
|
68
|
-
return uuid
|
72
|
+
return output.save_level1b(radiometrics, output_file, uuid)
|
69
73
|
|
70
74
|
|
71
75
|
class Record(NamedTuple):
|
@@ -79,7 +83,8 @@ class Record(NamedTuple):
|
|
79
83
|
class Radiometrics:
|
80
84
|
"""Reader for level 2 files of Radiometrics microwave radiometers.
|
81
85
|
|
82
|
-
References
|
86
|
+
References
|
87
|
+
----------
|
83
88
|
Radiometrics (2008). Profiler Operator's Manual: MP-3000A, MP-2500A,
|
84
89
|
MP-1500A, MP-183A.
|
85
90
|
"""
|
@@ -90,16 +95,18 @@ class Radiometrics:
|
|
90
95
|
self.data: dict = {}
|
91
96
|
self.instrument = instruments.RADIOMETRICS
|
92
97
|
|
93
|
-
def read_raw_data(self):
|
98
|
+
def read_raw_data(self) -> None:
|
94
99
|
"""Reads Radiometrics raw data."""
|
95
100
|
record_columns = {}
|
96
101
|
unknown_record_types = set()
|
97
102
|
rows = []
|
98
|
-
with open(self.filename,
|
103
|
+
with open(self.filename, encoding="utf8") as infile:
|
99
104
|
reader = csv.reader(infile)
|
100
105
|
for row in reader:
|
101
106
|
if row[0] == "Record":
|
102
|
-
|
107
|
+
if row[1] != "Date/Time":
|
108
|
+
msg = "Unexpected header in Radiometrics file"
|
109
|
+
raise RuntimeError(msg)
|
103
110
|
record_type = int(row[2])
|
104
111
|
record_columns[record_type] = row[3:]
|
105
112
|
else:
|
@@ -109,7 +116,7 @@ class Radiometrics:
|
|
109
116
|
column_names = record_columns.get(block_type)
|
110
117
|
if column_names is None:
|
111
118
|
if record_type not in unknown_record_types:
|
112
|
-
logging.info(
|
119
|
+
logging.info("Skipping unknown record type %d", record_type)
|
113
120
|
unknown_record_types.add(record_type)
|
114
121
|
continue
|
115
122
|
record = Record(
|
@@ -117,7 +124,7 @@ class Radiometrics:
|
|
117
124
|
timestamp=_parse_datetime(row[1]),
|
118
125
|
block_type=block_type,
|
119
126
|
block_index=block_index,
|
120
|
-
values=dict(zip(column_names, row[3:])),
|
127
|
+
values=dict(zip(column_names, row[3:], strict=True)),
|
121
128
|
)
|
122
129
|
rows.append(record)
|
123
130
|
|
@@ -130,7 +137,7 @@ class Radiometrics:
|
|
130
137
|
if data_row.block_index == 0:
|
131
138
|
self.raw_data.append(data_row)
|
132
139
|
|
133
|
-
def read_data(self):
|
140
|
+
def read_data(self) -> None:
|
134
141
|
"""Reads values."""
|
135
142
|
times = []
|
136
143
|
lwps = []
|
@@ -165,7 +172,7 @@ class RadiometricsCombined:
|
|
165
172
|
self.data = utils.append_data(self.data, key, obj.data[key])
|
166
173
|
self.instrument = instruments.RADIOMETRICS
|
167
174
|
|
168
|
-
def screen_time(self, expected_date: datetime.date | None):
|
175
|
+
def screen_time(self, expected_date: datetime.date | None) -> None:
|
169
176
|
"""Screens timestamps."""
|
170
177
|
if expected_date is None:
|
171
178
|
self.date = self.data["time"][0].astype(object).date()
|
@@ -177,22 +184,22 @@ class RadiometricsCombined:
|
|
177
184
|
for key in self.data:
|
178
185
|
self.data[key] = self.data[key][valid_mask]
|
179
186
|
|
180
|
-
def time_to_fractional_hours(self):
|
187
|
+
def time_to_fractional_hours(self) -> None:
|
181
188
|
base = self.data["time"][0].astype("datetime64[D]")
|
182
189
|
self.data["time"] = (self.data["time"] - base) / np.timedelta64(1, "h")
|
183
190
|
|
184
|
-
def data_to_cloudnet_arrays(self):
|
191
|
+
def data_to_cloudnet_arrays(self) -> None:
|
185
192
|
"""Converts arrays to CloudnetArrays."""
|
186
193
|
for key, array in self.data.items():
|
187
194
|
self.data[key] = CloudnetArray(array, key)
|
188
195
|
|
189
|
-
def add_meta(self):
|
196
|
+
def add_meta(self) -> None:
|
190
197
|
"""Adds some metadata."""
|
191
198
|
valid_keys = ("latitude", "longitude", "altitude")
|
192
199
|
for key, value in self.site_meta.items():
|
193
|
-
|
194
|
-
if
|
195
|
-
self.data[
|
200
|
+
name = key.lower()
|
201
|
+
if name in valid_keys:
|
202
|
+
self.data[name] = CloudnetArray(float(value), key)
|
196
203
|
|
197
204
|
|
198
205
|
def _parse_datetime(text: str) -> datetime.datetime:
|
@@ -201,4 +208,11 @@ def _parse_datetime(text: str) -> datetime.datetime:
|
|
201
208
|
hour, minute, second = map(int, time.split(":"))
|
202
209
|
if year < 100:
|
203
210
|
year += 2000
|
204
|
-
return datetime.datetime(
|
211
|
+
return datetime.datetime(
|
212
|
+
year,
|
213
|
+
month,
|
214
|
+
day,
|
215
|
+
hour,
|
216
|
+
minute,
|
217
|
+
second,
|
218
|
+
)
|
cloudnetpy/instruments/rpg.py
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
"""This module contains RPG Cloud Radar related functions."""
|
2
2
|
import logging
|
3
3
|
import math
|
4
|
-
from
|
4
|
+
from collections.abc import Sequence
|
5
|
+
from typing import TYPE_CHECKING
|
5
6
|
|
6
7
|
import numpy as np
|
7
8
|
from numpy import ma
|
@@ -10,13 +11,16 @@ from rpgpy import RPGFileError
|
|
10
11
|
from cloudnetpy import output, utils
|
11
12
|
from cloudnetpy.categorize.atmos_utils import mmh2ms
|
12
13
|
from cloudnetpy.cloudnetarray import CloudnetArray
|
14
|
+
from cloudnetpy.constants import G_TO_KG
|
13
15
|
from cloudnetpy.exceptions import InconsistentDataError, ValidTimeStampError
|
14
16
|
from cloudnetpy.instruments import instruments
|
15
17
|
from cloudnetpy.instruments.cloudnet_instrument import CloudnetInstrument
|
16
|
-
from cloudnetpy.instruments.instruments import Instrument
|
17
18
|
from cloudnetpy.instruments.rpg_reader import Fmcw94Bin, HatproBinCombined
|
18
19
|
from cloudnetpy.metadata import MetaData
|
19
20
|
|
21
|
+
if TYPE_CHECKING:
|
22
|
+
from cloudnetpy.instruments.instruments import Instrument
|
23
|
+
|
20
24
|
|
21
25
|
def rpg2nc(
|
22
26
|
path_to_l1_files: str,
|
@@ -31,6 +35,7 @@ def rpg2nc(
|
|
31
35
|
concatenates the data and writes a netCDF file.
|
32
36
|
|
33
37
|
Args:
|
38
|
+
----
|
34
39
|
path_to_l1_files: Folder containing one day of RPG LV1 files.
|
35
40
|
output_file: Output file name.
|
36
41
|
site_meta: Dictionary containing information about the
|
@@ -43,15 +48,18 @@ def rpg2nc(
|
|
43
48
|
only files that match the date will be used.
|
44
49
|
|
45
50
|
Returns:
|
51
|
+
-------
|
46
52
|
2-element tuple containing
|
47
53
|
|
48
54
|
- UUID of the generated file.
|
49
55
|
- Files used in the processing.
|
50
56
|
|
51
57
|
Raises:
|
58
|
+
------
|
52
59
|
ValidTimeStampError: No valid timestamps found.
|
53
60
|
|
54
61
|
Examples:
|
62
|
+
--------
|
55
63
|
>>> from cloudnetpy.instruments import rpg2nc
|
56
64
|
>>> site_meta = {'name': 'Hyytiala', 'altitude': 174}
|
57
65
|
>>> rpg2nc('/path/to/files/', 'test.nc', site_meta)
|
@@ -89,7 +97,7 @@ def print_info(data: dict) -> None:
|
|
89
97
|
mode = "LDR"
|
90
98
|
else:
|
91
99
|
mode = "STSR"
|
92
|
-
logging.info(
|
100
|
+
logging.info("RPG cloud radar in %s mode", mode)
|
93
101
|
|
94
102
|
|
95
103
|
RpgObjects = Sequence[Fmcw94Bin] | Sequence[HatproBinCombined]
|
@@ -107,12 +115,13 @@ def create_one_day_data_record(rpg_objects: RpgObjects) -> dict:
|
|
107
115
|
def _stack_rpg_data(rpg_objects: RpgObjects) -> tuple[dict, dict]:
|
108
116
|
"""Combines data from hourly RPG objects.
|
109
117
|
|
110
|
-
Notes
|
118
|
+
Notes
|
119
|
+
-----
|
111
120
|
Ignores variable names starting with an underscore.
|
112
121
|
|
113
122
|
"""
|
114
123
|
|
115
|
-
def _stack(source, target, fun):
|
124
|
+
def _stack(source, target, fun) -> None:
|
116
125
|
for name, value in source.items():
|
117
126
|
if not name.startswith("_"):
|
118
127
|
target[name] = fun((target[name], value)) if name in target else value
|
@@ -131,7 +140,7 @@ def _reduce_header(header: dict) -> dict:
|
|
131
140
|
for key, data in header.items():
|
132
141
|
first_profile_value = data[0]
|
133
142
|
is_identical_value = bool(
|
134
|
-
np.isclose(data, first_profile_value, rtol=1e-2).all()
|
143
|
+
np.isclose(data, first_profile_value, rtol=1e-2).all(),
|
135
144
|
)
|
136
145
|
if is_identical_value is False:
|
137
146
|
msg = f"Inconsistent header: {key}: {data}"
|
@@ -154,6 +163,8 @@ def _mask_invalid_data(data_in: dict) -> dict:
|
|
154
163
|
data = data_in.copy()
|
155
164
|
fill_values = (-999, 1e-10)
|
156
165
|
for name in data:
|
166
|
+
if np.issubdtype(data[name].dtype, np.integer) or name == "rainfall_rate":
|
167
|
+
continue
|
157
168
|
data[name] = ma.masked_equal(data[name], 0)
|
158
169
|
for value in fill_values:
|
159
170
|
data[name][data[name] == value] = ma.masked
|
@@ -188,14 +199,19 @@ def _remove_files_with_bad_height(objects: list, files: list) -> tuple[list, lis
|
|
188
199
|
most_common = np.bincount(lengths).argmax()
|
189
200
|
files = [
|
190
201
|
file
|
191
|
-
for file, obj, length in zip(files, objects, lengths)
|
202
|
+
for file, obj, length in zip(files, objects, lengths, strict=True)
|
203
|
+
if length == most_common
|
204
|
+
]
|
205
|
+
objects = [
|
206
|
+
obj
|
207
|
+
for obj, length in zip(objects, lengths, strict=True)
|
192
208
|
if length == most_common
|
193
209
|
]
|
194
|
-
objects = [obj for obj, length in zip(objects, lengths) if length == most_common]
|
195
210
|
n_removed = len(lengths) - len(files)
|
196
211
|
if n_removed > 0:
|
197
212
|
logging.warning(
|
198
|
-
|
213
|
+
"Removed %s RPG-FMCW-94 files due to inconsistent height vector",
|
214
|
+
n_removed,
|
199
215
|
)
|
200
216
|
return objects, files
|
201
217
|
|
@@ -204,7 +220,8 @@ def _validate_date(obj, expected_date: str) -> None:
|
|
204
220
|
for t in obj.data["time"][:]:
|
205
221
|
date_str = "-".join(utils.seconds2date(t)[:3])
|
206
222
|
if date_str != expected_date:
|
207
|
-
|
223
|
+
msg = "Ignoring a file (time stamps not what expected)"
|
224
|
+
raise ValueError(msg)
|
208
225
|
|
209
226
|
|
210
227
|
class Rpg(CloudnetInstrument):
|
@@ -220,10 +237,14 @@ class Rpg(CloudnetInstrument):
|
|
220
237
|
|
221
238
|
def convert_time_to_fraction_hour(self, data_type: str | None = None) -> None:
|
222
239
|
"""Converts time to fraction hour."""
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
240
|
+
ms2s = 1e-3
|
241
|
+
total_time_sec = self.raw_data["time"] + self.raw_data.get("time_ms", 0) * ms2s
|
242
|
+
fraction_hour = utils.seconds2hours(total_time_sec)
|
243
|
+
|
244
|
+
self.data["time"] = CloudnetArray(
|
245
|
+
np.array(fraction_hour),
|
246
|
+
"time",
|
247
|
+
data_type=data_type,
|
227
248
|
)
|
228
249
|
|
229
250
|
def _get_date(self) -> list:
|
@@ -254,7 +275,8 @@ class Fmcw(Rpg):
|
|
254
275
|
threshold = -35
|
255
276
|
if "ldr" in self.data:
|
256
277
|
self.data["ldr"].data = ma.masked_less_equal(
|
257
|
-
self.data["ldr"].data,
|
278
|
+
self.data["ldr"].data,
|
279
|
+
threshold,
|
258
280
|
)
|
259
281
|
|
260
282
|
def mask_invalid_width(self) -> None:
|
@@ -276,19 +298,21 @@ class Fmcw(Rpg):
|
|
276
298
|
is_valid_zenith = _filter_zenith_angle(zenith)
|
277
299
|
n_removed = len(is_valid_zenith) - np.count_nonzero(is_valid_zenith)
|
278
300
|
if n_removed == len(zenith):
|
279
|
-
|
301
|
+
msg = "No profiles with valid zenith angle"
|
302
|
+
raise ValidTimeStampError(msg)
|
280
303
|
if n_removed > 0:
|
281
304
|
logging.warning(
|
282
|
-
|
305
|
+
"Filtering %s profiles due to invalid zenith angle",
|
306
|
+
n_removed,
|
283
307
|
)
|
284
308
|
self.data["zenith_angle"] = CloudnetArray(zenith, "zenith_angle")
|
285
309
|
del self.data["elevation"]
|
286
310
|
return list(is_valid_zenith)
|
287
311
|
|
288
|
-
def convert_units(self):
|
312
|
+
def convert_units(self) -> None:
|
289
313
|
"""Converts units."""
|
290
314
|
self.data["rainfall_rate"].data = mmh2ms(self.data["rainfall_rate"].data)
|
291
|
-
self.data["lwp"].data *=
|
315
|
+
self.data["lwp"].data *= G_TO_KG
|
292
316
|
|
293
317
|
@staticmethod
|
294
318
|
def _get_instrument(data: dict):
|
@@ -297,7 +321,8 @@ class Fmcw(Rpg):
|
|
297
321
|
return instruments.FMCW35
|
298
322
|
if math.isclose(frequency, 94, abs_tol=0.1):
|
299
323
|
return instruments.FMCW94
|
300
|
-
|
324
|
+
msg = f"Unknown RPG cloud radar frequency: {frequency}"
|
325
|
+
raise RuntimeError(msg)
|
301
326
|
|
302
327
|
|
303
328
|
class Hatpro(Rpg):
|
@@ -315,7 +340,7 @@ def _filter_zenith_angle(zenith: ma.MaskedArray) -> np.ndarray:
|
|
315
340
|
logging.warning("Can not determine zenith angle, assuming 0 degrees")
|
316
341
|
limits = [-5, 15]
|
317
342
|
ind_close_to_zenith = np.where(
|
318
|
-
np.logical_and(zenith > limits[0], zenith < limits[1])
|
343
|
+
np.logical_and(zenith > limits[0], zenith < limits[1]),
|
319
344
|
)
|
320
345
|
if not ind_close_to_zenith[0].size:
|
321
346
|
return np.zeros_like(zenith, dtype=bool)
|
@@ -365,7 +390,8 @@ RPG_ATTRIBUTES = {
|
|
365
390
|
"srho_hv": MetaData(long_name="Slanted correlation coefficient", units="1"),
|
366
391
|
"kdp": MetaData(long_name="Specific differential phase shift", units="rad km-1"),
|
367
392
|
"differential_attenuation": MetaData(
|
368
|
-
long_name="Differential attenuation",
|
393
|
+
long_name="Differential attenuation",
|
394
|
+
units="dB km-1",
|
369
395
|
),
|
370
396
|
# All radars
|
371
397
|
"file_code": MetaData(
|