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,26 +1,35 @@
|
|
|
1
1
|
"""Module for reading Radiometrics MP3014 microwave radiometer data."""
|
|
2
|
+
|
|
2
3
|
import csv
|
|
3
4
|
import datetime
|
|
4
5
|
import logging
|
|
6
|
+
import math
|
|
5
7
|
import os
|
|
8
|
+
import re
|
|
6
9
|
from operator import attrgetter
|
|
10
|
+
from os import PathLike
|
|
11
|
+
from pathlib import Path
|
|
7
12
|
from typing import Any, NamedTuple
|
|
13
|
+
from uuid import UUID
|
|
8
14
|
|
|
9
15
|
import numpy as np
|
|
16
|
+
import numpy.typing as npt
|
|
17
|
+
from numpy import ma
|
|
10
18
|
|
|
11
19
|
from cloudnetpy import output, utils
|
|
12
20
|
from cloudnetpy.cloudnetarray import CloudnetArray
|
|
13
|
-
from cloudnetpy.exceptions import ValidTimeStampError
|
|
21
|
+
from cloudnetpy.exceptions import InconsistentDataError, ValidTimeStampError
|
|
14
22
|
from cloudnetpy.instruments import instruments
|
|
23
|
+
from cloudnetpy.metadata import MetaData
|
|
15
24
|
|
|
16
25
|
|
|
17
26
|
def radiometrics2nc(
|
|
18
|
-
full_path: str,
|
|
19
|
-
output_file: str,
|
|
27
|
+
full_path: str | PathLike,
|
|
28
|
+
output_file: str | PathLike,
|
|
20
29
|
site_meta: dict,
|
|
21
|
-
uuid: str | None = None,
|
|
30
|
+
uuid: str | UUID | None = None,
|
|
22
31
|
date: str | datetime.date | None = None,
|
|
23
|
-
) ->
|
|
32
|
+
) -> UUID:
|
|
24
33
|
"""Converts Radiometrics .csv file into Cloudnet Level 1b netCDF file.
|
|
25
34
|
|
|
26
35
|
Args:
|
|
@@ -43,28 +52,24 @@ def radiometrics2nc(
|
|
|
43
52
|
"""
|
|
44
53
|
if isinstance(date, str):
|
|
45
54
|
date = datetime.date.fromisoformat(date)
|
|
46
|
-
|
|
55
|
+
uuid = utils.get_uuid(uuid)
|
|
47
56
|
if os.path.isdir(full_path):
|
|
48
|
-
valid_filenames =
|
|
57
|
+
valid_filenames = list(Path(full_path).iterdir())
|
|
49
58
|
else:
|
|
50
|
-
valid_filenames = [full_path]
|
|
51
|
-
|
|
52
|
-
objs = []
|
|
53
|
-
for filename in valid_filenames:
|
|
54
|
-
obj = Radiometrics(filename)
|
|
55
|
-
obj.read_raw_data()
|
|
56
|
-
obj.read_data()
|
|
57
|
-
objs.append(obj)
|
|
58
|
-
|
|
59
|
+
valid_filenames = [Path(full_path)]
|
|
60
|
+
objs = [_read_file(filename) for filename in valid_filenames]
|
|
59
61
|
radiometrics = RadiometricsCombined(objs, site_meta)
|
|
60
62
|
radiometrics.screen_time(date)
|
|
63
|
+
radiometrics.sort_timestamps()
|
|
61
64
|
radiometrics.time_to_fractional_hours()
|
|
62
65
|
radiometrics.data_to_cloudnet_arrays()
|
|
63
66
|
radiometrics.add_meta()
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
if radiometrics.date is None:
|
|
68
|
+
msg = "Failed to find valid timestamps from Radiometrics file(s)."
|
|
69
|
+
raise ValidTimeStampError(msg)
|
|
70
|
+
attributes = output.add_time_attribute(ATTRIBUTES, radiometrics.date)
|
|
66
71
|
output.update_attributes(radiometrics.data, attributes)
|
|
67
|
-
|
|
72
|
+
output.save_level1b(radiometrics, output_file, uuid)
|
|
68
73
|
return uuid
|
|
69
74
|
|
|
70
75
|
|
|
@@ -76,32 +81,43 @@ class Record(NamedTuple):
|
|
|
76
81
|
values: dict[str, Any]
|
|
77
82
|
|
|
78
83
|
|
|
79
|
-
class
|
|
80
|
-
"""Reader for level 2 files
|
|
84
|
+
class RadiometricsMP:
|
|
85
|
+
"""Reader for level 2 files (*.csv) from Radiometrics MP-3000A and similar
|
|
86
|
+
microwave radiometers.
|
|
81
87
|
|
|
82
88
|
References:
|
|
83
89
|
Radiometrics (2008). Profiler Operator's Manual: MP-3000A, MP-2500A,
|
|
84
90
|
MP-1500A, MP-183A.
|
|
85
91
|
"""
|
|
86
92
|
|
|
87
|
-
def __init__(self, filename:
|
|
93
|
+
def __init__(self, filename: Path) -> None:
|
|
88
94
|
self.filename = filename
|
|
89
95
|
self.raw_data: list[Record] = []
|
|
90
96
|
self.data: dict = {}
|
|
91
97
|
self.instrument = instruments.RADIOMETRICS
|
|
98
|
+
self.ranges: list[str] = []
|
|
92
99
|
|
|
93
|
-
def read_raw_data(self):
|
|
100
|
+
def read_raw_data(self) -> None:
|
|
94
101
|
"""Reads Radiometrics raw data."""
|
|
95
102
|
record_columns = {}
|
|
96
103
|
unknown_record_types = set()
|
|
97
104
|
rows = []
|
|
98
|
-
with open(self.filename,
|
|
99
|
-
reader = csv.reader(infile)
|
|
105
|
+
with open(self.filename, encoding="utf8") as infile:
|
|
106
|
+
reader = csv.reader(infile, skipinitialspace=True)
|
|
100
107
|
for row in reader:
|
|
101
108
|
if row[0] == "Record":
|
|
102
|
-
|
|
109
|
+
if row[1] != "Date/Time":
|
|
110
|
+
msg = "Unexpected header in Radiometrics file"
|
|
111
|
+
raise RuntimeError(msg)
|
|
103
112
|
record_type = int(row[2])
|
|
104
|
-
|
|
113
|
+
columns = row[3:]
|
|
114
|
+
record_columns[record_type] = columns
|
|
115
|
+
if record_type in (10, 400):
|
|
116
|
+
self.ranges = [
|
|
117
|
+
column
|
|
118
|
+
for column in columns
|
|
119
|
+
if re.fullmatch(r"\d+\.\d+", column)
|
|
120
|
+
]
|
|
105
121
|
else:
|
|
106
122
|
record_type = int(row[2])
|
|
107
123
|
block_type = record_type // 10 * 10
|
|
@@ -109,45 +125,201 @@ class Radiometrics:
|
|
|
109
125
|
column_names = record_columns.get(block_type)
|
|
110
126
|
if column_names is None:
|
|
111
127
|
if record_type not in unknown_record_types:
|
|
112
|
-
logging.info(
|
|
128
|
+
logging.info("Skipping unknown record type %d", record_type)
|
|
113
129
|
unknown_record_types.add(record_type)
|
|
114
130
|
continue
|
|
131
|
+
|
|
132
|
+
row_trimmed = [value for value in row if value != ""]
|
|
133
|
+
|
|
115
134
|
record = Record(
|
|
116
135
|
row_number=int(row[0]),
|
|
117
136
|
timestamp=_parse_datetime(row[1]),
|
|
118
137
|
block_type=block_type,
|
|
119
138
|
block_index=block_index,
|
|
120
|
-
values=dict(zip(column_names,
|
|
139
|
+
values=dict(zip(column_names, row_trimmed[3:], strict=True)),
|
|
121
140
|
)
|
|
122
141
|
rows.append(record)
|
|
123
142
|
|
|
124
|
-
|
|
125
|
-
rows.sort(key=attrgetter("row_number"))
|
|
143
|
+
self.raw_data = sorted(rows, key=attrgetter("row_number"))
|
|
126
144
|
|
|
127
|
-
|
|
128
|
-
# Use the first row of a block and skip the rest which should
|
|
129
|
-
# contain the same values in the first columns.
|
|
130
|
-
if data_row.block_index == 0:
|
|
131
|
-
self.raw_data.append(data_row)
|
|
132
|
-
|
|
133
|
-
def read_data(self):
|
|
145
|
+
def read_data(self) -> None:
|
|
134
146
|
"""Reads values."""
|
|
135
147
|
times = []
|
|
136
148
|
lwps = []
|
|
137
149
|
iwvs = []
|
|
150
|
+
irts = []
|
|
151
|
+
irt_times = []
|
|
152
|
+
temps = []
|
|
153
|
+
temp_times = []
|
|
154
|
+
rhs = []
|
|
155
|
+
rh_times = []
|
|
156
|
+
ahs = []
|
|
157
|
+
ah_times = []
|
|
158
|
+
block_titles = {}
|
|
159
|
+
superblock: list[Record] = []
|
|
160
|
+
|
|
161
|
+
def _parse_floats(record: Record) -> list[float]:
|
|
162
|
+
return [
|
|
163
|
+
float(record.values[column])
|
|
164
|
+
if record.values[column].replace(".", "", 1).isdigit()
|
|
165
|
+
else math.nan
|
|
166
|
+
for column in self.ranges
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
def _process_superblock() -> None:
|
|
170
|
+
# There can be multiple 301 records but they don't have "LV2
|
|
171
|
+
# Processor" column. We can deduce the "LV2 Processor" values from
|
|
172
|
+
# 401 records in the same "superblock" before or after the 301
|
|
173
|
+
# records.
|
|
174
|
+
procs = []
|
|
175
|
+
for record in superblock:
|
|
176
|
+
if record.block_type == 400 and record.block_index == 1:
|
|
177
|
+
procs.append(record.values["LV2 Processor"])
|
|
178
|
+
good_procs = ["0.00:19.80", "Zenith", "Zenith16", "Zenith18"]
|
|
179
|
+
curr_proc = 0
|
|
180
|
+
for record in superblock:
|
|
181
|
+
if record.block_type == 100:
|
|
182
|
+
block_type = int(record.values["Record Type"]) - 1
|
|
183
|
+
title = record.values["Title"]
|
|
184
|
+
block_titles[block_type] = title
|
|
185
|
+
if title := block_titles.get(record.block_type + record.block_index):
|
|
186
|
+
if record.values["LV2 Processor"] not in good_procs:
|
|
187
|
+
continue
|
|
188
|
+
if title == "Temperature (K)":
|
|
189
|
+
temp_times.append(record.timestamp)
|
|
190
|
+
temps.append(_parse_floats(record))
|
|
191
|
+
elif title == "Relative Humidity (%)":
|
|
192
|
+
rh_times.append(record.timestamp)
|
|
193
|
+
rhs.append(_parse_floats(record))
|
|
194
|
+
elif title == "Vapor Density (g/m^3)":
|
|
195
|
+
ah_times.append(record.timestamp)
|
|
196
|
+
ahs.append(_parse_floats(record))
|
|
197
|
+
elif record.block_type == 10:
|
|
198
|
+
if record.block_index == 0:
|
|
199
|
+
lwp = record.values["Lqint(mm)"]
|
|
200
|
+
iwv = record.values["Vint(cm)"]
|
|
201
|
+
irt = record.values["Tir(K)"]
|
|
202
|
+
times.append(record.timestamp)
|
|
203
|
+
lwps.append(float(lwp))
|
|
204
|
+
iwvs.append(float(iwv))
|
|
205
|
+
irt_times.append(record.timestamp)
|
|
206
|
+
irts.append([float(irt)])
|
|
207
|
+
temp_times.append(record.timestamp)
|
|
208
|
+
temps.append(_parse_floats(record))
|
|
209
|
+
elif record.block_index == 1:
|
|
210
|
+
ah_times.append(record.timestamp)
|
|
211
|
+
ahs.append(_parse_floats(record))
|
|
212
|
+
elif record.block_index == 2:
|
|
213
|
+
rh_times.append(record.timestamp)
|
|
214
|
+
rhs.append(_parse_floats(record))
|
|
215
|
+
elif record.block_type == 200:
|
|
216
|
+
irt_times.append(record.timestamp)
|
|
217
|
+
irt = record.values["Tir(K)"]
|
|
218
|
+
irts.append([float(irt)])
|
|
219
|
+
elif record.block_type == 300:
|
|
220
|
+
if procs:
|
|
221
|
+
curr_proc += 1
|
|
222
|
+
if procs[curr_proc - 1] not in good_procs:
|
|
223
|
+
continue
|
|
224
|
+
lwp = record.values["Int. Liquid(mm)"]
|
|
225
|
+
iwv = record.values["Int. Vapor(cm)"]
|
|
226
|
+
times.append(record.timestamp)
|
|
227
|
+
lwps.append(float(lwp))
|
|
228
|
+
iwvs.append(float(iwv))
|
|
229
|
+
|
|
138
230
|
for record in self.raw_data:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if iwv is not None:
|
|
147
|
-
iwvs.append(float(iwv) * 100) # g / m2
|
|
231
|
+
if record.block_type == 200 and superblock:
|
|
232
|
+
_process_superblock()
|
|
233
|
+
superblock.clear()
|
|
234
|
+
superblock.append(record)
|
|
235
|
+
if superblock:
|
|
236
|
+
_process_superblock()
|
|
237
|
+
|
|
148
238
|
self.data["time"] = np.array(times, dtype="datetime64[s]")
|
|
149
|
-
self.data["lwp"] = np.array(lwps
|
|
150
|
-
self.data["iwv"] = np.array(iwvs
|
|
239
|
+
self.data["lwp"] = np.array(lwps) # mm => kg m-2
|
|
240
|
+
self.data["iwv"] = np.array(iwvs) * 10 # cm => kg m-2
|
|
241
|
+
if irt_times:
|
|
242
|
+
self.data["irt"] = _find_closest(
|
|
243
|
+
np.array(irt_times, dtype="datetime64[s]"),
|
|
244
|
+
np.array(irts),
|
|
245
|
+
self.data["time"],
|
|
246
|
+
)
|
|
247
|
+
if temp_times:
|
|
248
|
+
self.data["temperature"] = _find_closest(
|
|
249
|
+
np.array(temp_times, dtype="datetime64[s]"),
|
|
250
|
+
np.array(temps),
|
|
251
|
+
self.data["time"],
|
|
252
|
+
)
|
|
253
|
+
if rh_times:
|
|
254
|
+
self.data["relative_humidity"] = _find_closest(
|
|
255
|
+
np.array(rh_times, dtype="datetime64[s]"),
|
|
256
|
+
np.array(rhs) / 100, # % => 1
|
|
257
|
+
self.data["time"],
|
|
258
|
+
)
|
|
259
|
+
if ah_times:
|
|
260
|
+
self.data["absolute_humidity"] = _find_closest(
|
|
261
|
+
np.array(ah_times, dtype="datetime64[s]"),
|
|
262
|
+
np.array(ahs) / 1000, # g m-3 => kg m-3
|
|
263
|
+
self.data["time"],
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class RadiometricsWVR:
|
|
268
|
+
"""Reader for *.los files from Radiometrics WVR-1100 microwave
|
|
269
|
+
radiometer.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
def __init__(self, filename: Path) -> None:
|
|
273
|
+
self.filename = filename
|
|
274
|
+
self.raw_data: dict = {}
|
|
275
|
+
self.data: dict = {}
|
|
276
|
+
self.instrument = instruments.RADIOMETRICS
|
|
277
|
+
self.ranges: list[str] = []
|
|
278
|
+
|
|
279
|
+
def read_raw_data(self) -> None:
|
|
280
|
+
with open(self.filename, encoding="utf8") as file:
|
|
281
|
+
for line in file:
|
|
282
|
+
columns = line.split()
|
|
283
|
+
if columns[:2] == ["date", "time"]:
|
|
284
|
+
headers = columns
|
|
285
|
+
break
|
|
286
|
+
else:
|
|
287
|
+
msg = "No headers found"
|
|
288
|
+
raise RuntimeError(msg)
|
|
289
|
+
for header in headers:
|
|
290
|
+
self.raw_data[header] = []
|
|
291
|
+
for line in file:
|
|
292
|
+
columns = line.split()
|
|
293
|
+
if len(columns) != len(headers):
|
|
294
|
+
continue
|
|
295
|
+
for header, column in zip(headers, columns, strict=True):
|
|
296
|
+
value: datetime.date | datetime.time | float
|
|
297
|
+
if header == "date":
|
|
298
|
+
value = _parse_date(column)
|
|
299
|
+
elif header == "time":
|
|
300
|
+
value = _parse_time(column)
|
|
301
|
+
else:
|
|
302
|
+
value = _parse_value(column)
|
|
303
|
+
self.raw_data[header].append(value)
|
|
304
|
+
|
|
305
|
+
def read_data(self) -> None:
|
|
306
|
+
self.data["time"] = np.array(
|
|
307
|
+
[
|
|
308
|
+
datetime.datetime.combine(date, time)
|
|
309
|
+
for date, time in zip(
|
|
310
|
+
self.raw_data["date"], self.raw_data["time"], strict=True
|
|
311
|
+
)
|
|
312
|
+
],
|
|
313
|
+
dtype="datetime64[s]",
|
|
314
|
+
)
|
|
315
|
+
self.data["lwp"] = np.array(self.raw_data["LiqCM"]) * 10 # cm => kg m-2
|
|
316
|
+
self.data["iwv"] = np.array(self.raw_data["VapCM"]) * 10 # cm => kg m-2
|
|
317
|
+
is_zenith = np.abs(np.array(self.raw_data["ELact"]) - 90.0) < 1.0
|
|
318
|
+
tb23_valid = np.array(self.raw_data["TbSky23"]) > 0
|
|
319
|
+
tb31_valid = np.array(self.raw_data["TbSky31"]) > 0
|
|
320
|
+
is_valid = is_zenith & tb23_valid & tb31_valid
|
|
321
|
+
for key in self.data:
|
|
322
|
+
self.data[key] = self.data[key][is_valid]
|
|
151
323
|
|
|
152
324
|
|
|
153
325
|
class RadiometricsCombined:
|
|
@@ -156,16 +328,25 @@ class RadiometricsCombined:
|
|
|
156
328
|
date: datetime.date | None
|
|
157
329
|
instrument: instruments.Instrument
|
|
158
330
|
|
|
159
|
-
def __init__(
|
|
331
|
+
def __init__(
|
|
332
|
+
self, objs: list[RadiometricsMP | RadiometricsWVR], site_meta: dict
|
|
333
|
+
) -> None:
|
|
160
334
|
self.site_meta = site_meta
|
|
161
335
|
self.data = {}
|
|
162
336
|
self.date = None
|
|
163
337
|
for obj in objs:
|
|
338
|
+
if obj.ranges != objs[0].ranges:
|
|
339
|
+
msg = "Inconsistent range between files"
|
|
340
|
+
raise InconsistentDataError(msg)
|
|
164
341
|
for key in obj.data:
|
|
165
342
|
self.data = utils.append_data(self.data, key, obj.data[key])
|
|
343
|
+
if objs[0].ranges:
|
|
344
|
+
ranges = [float(x) for x in objs[0].ranges]
|
|
345
|
+
self.data["range"] = np.array(ranges) * 1000 # m => km
|
|
346
|
+
self.data["height"] = self.data["range"] + self.site_meta["altitude"]
|
|
166
347
|
self.instrument = instruments.RADIOMETRICS
|
|
167
348
|
|
|
168
|
-
def screen_time(self, expected_date: datetime.date | None):
|
|
349
|
+
def screen_time(self, expected_date: datetime.date | None) -> None:
|
|
169
350
|
"""Screens timestamps."""
|
|
170
351
|
if expected_date is None:
|
|
171
352
|
self.date = self.data["time"][0].astype(object).date()
|
|
@@ -175,24 +356,68 @@ class RadiometricsCombined:
|
|
|
175
356
|
if np.count_nonzero(valid_mask) == 0:
|
|
176
357
|
raise ValidTimeStampError
|
|
177
358
|
for key in self.data:
|
|
359
|
+
if key in ("range", "height"):
|
|
360
|
+
continue
|
|
178
361
|
self.data[key] = self.data[key][valid_mask]
|
|
179
362
|
|
|
180
|
-
def
|
|
363
|
+
def sort_timestamps(self) -> None:
|
|
364
|
+
ind = np.argsort(self.data["time"])
|
|
365
|
+
for key in self.data:
|
|
366
|
+
if key in ("range", "height"):
|
|
367
|
+
continue
|
|
368
|
+
self.data[key] = self.data[key][ind]
|
|
369
|
+
|
|
370
|
+
def time_to_fractional_hours(self) -> None:
|
|
181
371
|
base = self.data["time"][0].astype("datetime64[D]")
|
|
182
372
|
self.data["time"] = (self.data["time"] - base) / np.timedelta64(1, "h")
|
|
183
373
|
|
|
184
|
-
def data_to_cloudnet_arrays(self):
|
|
374
|
+
def data_to_cloudnet_arrays(self) -> None:
|
|
185
375
|
"""Converts arrays to CloudnetArrays."""
|
|
186
376
|
for key, array in self.data.items():
|
|
187
|
-
|
|
377
|
+
dimensions = (
|
|
378
|
+
("time", "range")
|
|
379
|
+
if key in ("temperature", "relative_humidity", "absolute_humidity")
|
|
380
|
+
else None
|
|
381
|
+
)
|
|
382
|
+
array_masked = ma.masked_invalid(array) if np.isnan(array).any() else array
|
|
383
|
+
self.data[key] = CloudnetArray(array_masked, key, dimensions=dimensions)
|
|
188
384
|
|
|
189
|
-
def add_meta(self):
|
|
385
|
+
def add_meta(self) -> None:
|
|
190
386
|
"""Adds some metadata."""
|
|
191
387
|
valid_keys = ("latitude", "longitude", "altitude")
|
|
192
388
|
for key, value in self.site_meta.items():
|
|
193
|
-
|
|
194
|
-
if
|
|
195
|
-
self.data[
|
|
389
|
+
name = key.lower()
|
|
390
|
+
if name in valid_keys:
|
|
391
|
+
self.data[name] = CloudnetArray(float(value), key)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _read_file(filename: Path) -> RadiometricsMP | RadiometricsWVR:
|
|
395
|
+
with open(filename) as f:
|
|
396
|
+
first_line = f.readline()
|
|
397
|
+
obj = (
|
|
398
|
+
RadiometricsWVR(filename)
|
|
399
|
+
if "RETRIEVAL COEFFICIENTS" in first_line
|
|
400
|
+
else RadiometricsMP(filename)
|
|
401
|
+
)
|
|
402
|
+
obj.read_raw_data()
|
|
403
|
+
obj.read_data()
|
|
404
|
+
return obj
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _parse_value(text: str) -> float:
|
|
408
|
+
return math.nan if "*" in text else float(text)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _parse_date(text: str) -> datetime.date:
|
|
412
|
+
month, day, year = map(int, text.split("/"))
|
|
413
|
+
if year < 100:
|
|
414
|
+
year += 2000
|
|
415
|
+
return datetime.date(year, month, day)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _parse_time(text: str) -> datetime.time:
|
|
419
|
+
hour, minute, second = map(int, text.split(":"))
|
|
420
|
+
return datetime.time(hour, minute, second)
|
|
196
421
|
|
|
197
422
|
|
|
198
423
|
def _parse_datetime(text: str) -> datetime.datetime:
|
|
@@ -201,4 +426,33 @@ def _parse_datetime(text: str) -> datetime.datetime:
|
|
|
201
426
|
hour, minute, second = map(int, time.split(":"))
|
|
202
427
|
if year < 100:
|
|
203
428
|
year += 2000
|
|
204
|
-
return datetime.datetime(
|
|
429
|
+
return datetime.datetime(
|
|
430
|
+
year,
|
|
431
|
+
month,
|
|
432
|
+
day,
|
|
433
|
+
hour,
|
|
434
|
+
minute,
|
|
435
|
+
second,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _find_closest(x: npt.NDArray, y: npt.NDArray, x_new: npt.NDArray) -> npt.NDArray:
|
|
440
|
+
return y[np.argmin(np.abs(x_new - x[:, np.newaxis]), axis=0)]
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
ATTRIBUTES = {
|
|
444
|
+
"irt": MetaData(
|
|
445
|
+
long_name="Infrared brightness temperatures",
|
|
446
|
+
units="K",
|
|
447
|
+
dimensions=("time", "ir_channel"),
|
|
448
|
+
),
|
|
449
|
+
"temperature": MetaData(
|
|
450
|
+
long_name="Temperature", units="K", dimensions=("time", "range")
|
|
451
|
+
),
|
|
452
|
+
"relative_humidity": MetaData(
|
|
453
|
+
long_name="Relative humidity",
|
|
454
|
+
standard_name="relative_humidity",
|
|
455
|
+
units="1",
|
|
456
|
+
dimensions=("time", "range"),
|
|
457
|
+
),
|
|
458
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import datetime
|
|
3
|
+
from os import PathLike
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from cloudnetpy import output
|
|
9
|
+
from cloudnetpy.exceptions import ValidTimeStampError
|
|
10
|
+
from cloudnetpy.instruments import instruments
|
|
11
|
+
from cloudnetpy.instruments.cloudnet_instrument import CSVFile
|
|
12
|
+
from cloudnetpy.utils import get_uuid
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def rain_e_h32nc(
|
|
16
|
+
input_file: str | PathLike,
|
|
17
|
+
output_file: str | PathLike,
|
|
18
|
+
site_meta: dict,
|
|
19
|
+
uuid: str | UUID | None = None,
|
|
20
|
+
date: str | datetime.date | None = None,
|
|
21
|
+
) -> UUID:
|
|
22
|
+
"""Converts rain_e_h3 rain-gauge into Cloudnet Level 1b netCDF file.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
input_file: Filename of rain_e_h3 CSV file.
|
|
26
|
+
output_file: Output filename.
|
|
27
|
+
site_meta: Dictionary containing information about the site. Required key
|
|
28
|
+
is `name`.
|
|
29
|
+
uuid: Set specific UUID for the file.
|
|
30
|
+
date: Expected date of the measurements as YYYY-MM-DD or datetime.date object.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
UUID of the generated file.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ValidTimeStampError: No valid timestamps found.
|
|
37
|
+
"""
|
|
38
|
+
rain = RainEH3(site_meta)
|
|
39
|
+
if isinstance(date, str):
|
|
40
|
+
date = datetime.date.fromisoformat(date)
|
|
41
|
+
uuid = get_uuid(uuid)
|
|
42
|
+
rain.parse_input_file(input_file, date)
|
|
43
|
+
rain.add_data()
|
|
44
|
+
rain.add_date()
|
|
45
|
+
rain.convert_units()
|
|
46
|
+
rain.normalize_cumulative_amount("rainfall_amount")
|
|
47
|
+
rain.add_site_geolocation()
|
|
48
|
+
rain.sort_timestamps()
|
|
49
|
+
rain.remove_duplicate_timestamps()
|
|
50
|
+
attributes = output.add_time_attribute({}, rain.date)
|
|
51
|
+
output.update_attributes(rain.data, attributes)
|
|
52
|
+
output.save_level1b(rain, output_file, uuid)
|
|
53
|
+
return uuid
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class RainEH3(CSVFile):
|
|
57
|
+
def __init__(self, site_meta: dict) -> None:
|
|
58
|
+
super().__init__(site_meta)
|
|
59
|
+
self.instrument = instruments.RAIN_E_H3
|
|
60
|
+
self._data = {
|
|
61
|
+
"time": [],
|
|
62
|
+
"rainfall_rate": [],
|
|
63
|
+
"rainfall_amount": [],
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
def parse_input_file(
|
|
67
|
+
self, filepath: str | PathLike, date: datetime.date | None = None
|
|
68
|
+
) -> None:
|
|
69
|
+
with open(filepath, encoding="latin1") as f:
|
|
70
|
+
data = list(csv.reader(f, delimiter=";"))
|
|
71
|
+
n_values = np.median([len(row) for row in data]).astype(int)
|
|
72
|
+
|
|
73
|
+
if n_values == 22:
|
|
74
|
+
self._read_talker_protocol_22_columns(data, date)
|
|
75
|
+
elif n_values == 16:
|
|
76
|
+
self._read_talker_protocol_16_columns(data, date)
|
|
77
|
+
else:
|
|
78
|
+
msg = "Only talker protocol with 16 or 22 columns is supported."
|
|
79
|
+
raise NotImplementedError(msg)
|
|
80
|
+
|
|
81
|
+
def _read_talker_protocol_16_columns(
|
|
82
|
+
self, data: list, date: datetime.date | None = None
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Old Lindenberg data format.
|
|
85
|
+
|
|
86
|
+
0 date DD.MM.YYYY
|
|
87
|
+
1 time
|
|
88
|
+
2 precipitation intensity in mm/h
|
|
89
|
+
3 precipitation accumulation in mm
|
|
90
|
+
4 housing contact
|
|
91
|
+
5 top temperature
|
|
92
|
+
6 bottom temperature
|
|
93
|
+
7 heater status
|
|
94
|
+
8 error code
|
|
95
|
+
9 system status
|
|
96
|
+
10 talker interval in seconds
|
|
97
|
+
11 operating hours
|
|
98
|
+
12 device type
|
|
99
|
+
13 user data storage 1
|
|
100
|
+
14 user data storage 2
|
|
101
|
+
15 user data storage 3
|
|
102
|
+
|
|
103
|
+
"""
|
|
104
|
+
for row in data:
|
|
105
|
+
if len(row) != 16:
|
|
106
|
+
continue
|
|
107
|
+
try:
|
|
108
|
+
dt = datetime.datetime.strptime(
|
|
109
|
+
f"{row[0]} {row[1]}", "%d.%m.%Y %H:%M:%S"
|
|
110
|
+
)
|
|
111
|
+
except ValueError:
|
|
112
|
+
continue
|
|
113
|
+
if date and date != dt.date():
|
|
114
|
+
continue
|
|
115
|
+
self._data["time"].append(dt)
|
|
116
|
+
self._data["rainfall_rate"].append(float(row[2]))
|
|
117
|
+
self._data["rainfall_amount"].append(float(row[3]))
|
|
118
|
+
if not self._data["time"]:
|
|
119
|
+
raise ValidTimeStampError
|
|
120
|
+
|
|
121
|
+
def _read_talker_protocol_22_columns(
|
|
122
|
+
self, data: list, date: datetime.date | None = None
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Columns according to header in Lindenberg data.
|
|
125
|
+
|
|
126
|
+
0 datetime utc
|
|
127
|
+
1 date
|
|
128
|
+
2 time
|
|
129
|
+
3 precipitation intensity in mm/h
|
|
130
|
+
4 precipitation accumulation in mm
|
|
131
|
+
5 housing contact
|
|
132
|
+
6 top temperature
|
|
133
|
+
7 bottom temperature
|
|
134
|
+
8 heater status
|
|
135
|
+
9 error code
|
|
136
|
+
10 system status
|
|
137
|
+
11 talker interval in seconds
|
|
138
|
+
12 operating hours
|
|
139
|
+
13 device type
|
|
140
|
+
14 user data storage 1
|
|
141
|
+
15 user data storage 2
|
|
142
|
+
16 user data storage 3
|
|
143
|
+
17 user data storage 4
|
|
144
|
+
18 serial number
|
|
145
|
+
19 hardware version
|
|
146
|
+
20 firmware version
|
|
147
|
+
21 external temperature * checksum
|
|
148
|
+
|
|
149
|
+
"""
|
|
150
|
+
for row in data:
|
|
151
|
+
if len(row) != 22:
|
|
152
|
+
continue
|
|
153
|
+
try:
|
|
154
|
+
dt = datetime.datetime.strptime(f"{row[0]}", "%Y-%m-%d %H:%M:%S")
|
|
155
|
+
except ValueError:
|
|
156
|
+
continue
|
|
157
|
+
if date and date != dt.date():
|
|
158
|
+
continue
|
|
159
|
+
self._data["time"].append(dt)
|
|
160
|
+
self._data["rainfall_rate"].append(float(row[3]))
|
|
161
|
+
self._data["rainfall_amount"].append(float(row[4]))
|
|
162
|
+
self.serial_number = row[18]
|
|
163
|
+
if not self._data["time"]:
|
|
164
|
+
raise ValidTimeStampError
|
|
165
|
+
|
|
166
|
+
def convert_units(self) -> None:
|
|
167
|
+
rainfall_rate = self.data["rainfall_rate"][:]
|
|
168
|
+
self.data["rainfall_rate"].data = rainfall_rate / 3600 / 1000 # mm/h -> m/s
|
|
169
|
+
self.data["rainfall_amount"].data = (
|
|
170
|
+
self.data["rainfall_amount"][:] / 1000
|
|
171
|
+
) # mm -> m
|