cloudnetpy 1.60.4__py3-none-any.whl → 1.61.1__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/concat_lib.py +6 -4
- cloudnetpy/instruments/disdrometer/common.py +25 -193
- cloudnetpy/instruments/disdrometer/parsivel.py +4 -31
- cloudnetpy/instruments/disdrometer/thies.py +211 -77
- cloudnetpy/instruments/toa5.py +45 -0
- cloudnetpy/version.py +2 -2
- {cloudnetpy-1.60.4.dist-info → cloudnetpy-1.61.1.dist-info}/METADATA +1 -1
- {cloudnetpy-1.60.4.dist-info → cloudnetpy-1.61.1.dist-info}/RECORD +11 -10
- {cloudnetpy-1.60.4.dist-info → cloudnetpy-1.61.1.dist-info}/LICENSE +0 -0
- {cloudnetpy-1.60.4.dist-info → cloudnetpy-1.61.1.dist-info}/WHEEL +0 -0
- {cloudnetpy-1.60.4.dist-info → cloudnetpy-1.61.1.dist-info}/top_level.txt +0 -0
cloudnetpy/concat_lib.py
CHANGED
@@ -5,8 +5,10 @@ import numpy as np
|
|
5
5
|
from cloudnetpy.exceptions import InconsistentDataError
|
6
6
|
|
7
7
|
|
8
|
-
def truncate_netcdf_file(
|
9
|
-
|
8
|
+
def truncate_netcdf_file(
|
9
|
+
filename: str, output_file: str, n_profiles: int, dim_name: str = "time"
|
10
|
+
) -> None:
|
11
|
+
"""Truncates netcdf file in dim_name dimension taking only n_profiles.
|
10
12
|
Useful for creating small files for tests.
|
11
13
|
"""
|
12
14
|
with (
|
@@ -14,7 +16,7 @@ def truncate_netcdf_file(filename: str, output_file: str, n_profiles: int) -> No
|
|
14
16
|
netCDF4.Dataset(output_file, "w", format=nc.data_model) as nc_new,
|
15
17
|
):
|
16
18
|
for dim in nc.dimensions:
|
17
|
-
dim_len = None if dim ==
|
19
|
+
dim_len = None if dim == dim_name else nc.dimensions[dim].size
|
18
20
|
nc_new.createDimension(dim, dim_len)
|
19
21
|
for attr in nc.ncattrs():
|
20
22
|
value = getattr(nc, attr)
|
@@ -30,7 +32,7 @@ def truncate_netcdf_file(filename: str, output_file: str, n_profiles: int) -> No
|
|
30
32
|
zlib=True,
|
31
33
|
fill_value=fill_value,
|
32
34
|
)
|
33
|
-
if dimensions and
|
35
|
+
if dimensions and dim_name in dimensions[0]:
|
34
36
|
if array.ndim == 1:
|
35
37
|
var[:] = array[:n_profiles]
|
36
38
|
if array.ndim == 2:
|
@@ -1,207 +1,57 @@
|
|
1
1
|
"""Module for reading / converting disdrometer data."""
|
2
|
-
|
2
|
+
|
3
|
+
from typing import Literal
|
3
4
|
|
4
5
|
import numpy as np
|
5
|
-
from numpy import ma
|
6
6
|
|
7
|
-
from cloudnetpy import utils
|
8
7
|
from cloudnetpy.cloudnetarray import CloudnetArray
|
9
|
-
from cloudnetpy.constants import MM_TO_M, SEC_IN_HOUR, SEC_IN_MINUTE
|
10
|
-
from cloudnetpy.exceptions import DisdrometerDataError, ValidTimeStampError
|
11
8
|
from cloudnetpy.instruments.cloudnet_instrument import CloudnetInstrument
|
12
|
-
from cloudnetpy.instruments.vaisala import values_to_dict
|
13
9
|
from cloudnetpy.metadata import MetaData
|
14
10
|
|
15
|
-
PARSIVEL = "OTT Parsivel-2"
|
16
|
-
THIES = "Thies-LNM"
|
17
|
-
|
18
11
|
|
19
12
|
class Disdrometer(CloudnetInstrument):
|
20
|
-
def __init__(self, filename: str, site_meta: dict, source: str):
|
21
|
-
super().__init__()
|
22
|
-
self.filename = filename
|
23
|
-
self.site_meta = site_meta
|
24
|
-
self.source = source
|
25
|
-
self.date: list[str] = []
|
26
|
-
self.sensor_id = None
|
27
|
-
self.n_diameter: int = 0
|
28
|
-
self.n_velocity: int = 0
|
29
|
-
self._file_data = self._read_file()
|
30
|
-
|
31
|
-
def convert_units(self) -> None:
|
32
|
-
mmh_to_ms = SEC_IN_HOUR / MM_TO_M
|
33
|
-
c_to_k = 273.15
|
34
|
-
self._convert_data(("rainfall_rate_1min_total",), mmh_to_ms)
|
35
|
-
self._convert_data(("rainfall_rate",), mmh_to_ms)
|
36
|
-
self._convert_data(("rainfall_rate_1min_solid",), mmh_to_ms)
|
37
|
-
self._convert_data(("diameter", "diameter_spread", "diameter_bnds"), 1e3)
|
38
|
-
self._convert_data(("V_sensor_supply",), 10)
|
39
|
-
self._convert_data(("I_mean_laser",), 100)
|
40
|
-
self._convert_data(("T_sensor",), c_to_k, method="add")
|
41
|
-
self._convert_data(("T_interior",), c_to_k, method="add")
|
42
|
-
self._convert_data(("T_ambient",), c_to_k, method="add")
|
43
|
-
self._convert_data(("T_laser_driver",), c_to_k, method="add")
|
44
|
-
|
45
13
|
def add_meta(self) -> None:
|
46
|
-
|
14
|
+
valid_keys = ("latitude", "longitude", "altitude")
|
47
15
|
for key, value in self.site_meta.items():
|
48
16
|
name = key.lower()
|
49
|
-
if name in
|
17
|
+
if name in valid_keys:
|
50
18
|
self.data[name] = CloudnetArray(float(value), name)
|
51
19
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
if date == expected_date:
|
59
|
-
valid_ind.append(ind)
|
60
|
-
if not valid_ind:
|
61
|
-
raise ValidTimeStampError
|
62
|
-
for key, value in self._file_data.items():
|
63
|
-
if value:
|
64
|
-
self._file_data[key] = [self._file_data[key][ind] for ind in valid_ind]
|
65
|
-
self.date = expected_date.split("-")
|
66
|
-
|
67
|
-
def sort_time(self) -> None:
|
68
|
-
time = self.data["time"][:]
|
69
|
-
ind = time.argsort()
|
70
|
-
for _, data in self.data.items():
|
71
|
-
if data.data.shape[0] == len(time):
|
72
|
-
data.data[:] = data.data[ind]
|
73
|
-
|
74
|
-
def _read_file(self) -> dict:
|
75
|
-
data: dict = {"scalars": [], "vectors": [], "spectra": []}
|
76
|
-
with open(self.filename, encoding="utf8", errors="ignore") as file:
|
77
|
-
for row in file:
|
78
|
-
if row == "\n":
|
79
|
-
continue
|
80
|
-
if self.source == PARSIVEL:
|
81
|
-
values = row.split(";")
|
82
|
-
if "\n" in values:
|
83
|
-
values.remove("\n")
|
84
|
-
if len(values) != 1106:
|
85
|
-
continue
|
86
|
-
data["scalars"].append(values[:18])
|
87
|
-
data["vectors"].append(values[18 : 18 + 64])
|
88
|
-
data["spectra"].append(values[18 + 64 :])
|
89
|
-
else:
|
90
|
-
values = row.split(";")
|
91
|
-
data["scalars"].append(values[:79])
|
92
|
-
data["spectra"].append(values[79:-2])
|
93
|
-
if len(data["scalars"]) == 0:
|
94
|
-
raise ValueError
|
95
|
-
return data
|
96
|
-
|
97
|
-
def _append_data(self, column_and_key: list) -> None:
|
98
|
-
indices, keys = zip(*column_and_key, strict=True)
|
99
|
-
data = self._parse_useful_data(indices)
|
100
|
-
data_dict = values_to_dict(keys, data)
|
20
|
+
def _convert_data(
|
21
|
+
self,
|
22
|
+
keys: tuple[str, ...],
|
23
|
+
value: float,
|
24
|
+
method: Literal["divide", "add"] = "divide",
|
25
|
+
) -> None:
|
101
26
|
for key in keys:
|
102
|
-
if key.
|
27
|
+
if key not in self.data:
|
103
28
|
continue
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
"Invalid character: %s, masking a data point",
|
112
|
-
value_str,
|
113
|
-
)
|
114
|
-
float_array = ma.append(float_array, invalid_value)
|
115
|
-
float_array[float_array == invalid_value] = ma.masked
|
116
|
-
if key in (
|
117
|
-
"rainfall_rate",
|
118
|
-
"radar_reflectivity",
|
119
|
-
"T_sensor",
|
120
|
-
"I_heating",
|
121
|
-
"V_power_supply",
|
122
|
-
"T_interior",
|
123
|
-
"T_ambient",
|
124
|
-
"T_laser_driver",
|
125
|
-
):
|
126
|
-
data_type = "f4"
|
29
|
+
variable = self.data[key]
|
30
|
+
if method == "divide":
|
31
|
+
variable.data = variable.data.astype("f4") / value
|
32
|
+
variable.data_type = "f4"
|
33
|
+
elif method == "add":
|
34
|
+
variable.data = variable.data.astype("f4") + value
|
35
|
+
variable.data_type = "f4"
|
127
36
|
else:
|
128
|
-
|
129
|
-
self.data[key] = CloudnetArray(float_array, key, data_type=data_type)
|
130
|
-
self.data["time"] = self._convert_time(data_dict)
|
131
|
-
if "_serial_number" in data_dict:
|
132
|
-
first_id = data_dict["_serial_number"][0]
|
133
|
-
for sensor_id in data_dict["_serial_number"]:
|
134
|
-
if sensor_id != first_id:
|
135
|
-
msg = "Multiple serial numbers are not supported"
|
136
|
-
raise DisdrometerDataError(msg)
|
137
|
-
|
138
|
-
self.serial_number = first_id
|
139
|
-
|
140
|
-
def _parse_useful_data(self, indices: tuple) -> list:
|
141
|
-
data = []
|
142
|
-
for row in self._file_data["scalars"]:
|
143
|
-
useful_data = [row[ind] for ind in indices]
|
144
|
-
data.append(useful_data)
|
145
|
-
return data
|
37
|
+
raise ValueError
|
146
38
|
|
147
|
-
def _convert_time(self, data: dict) -> CloudnetArray:
|
148
|
-
seconds = []
|
149
|
-
for timestamp in data["_time"]:
|
150
|
-
if self.source == PARSIVEL:
|
151
|
-
raise NotImplementedError
|
152
|
-
hour, minute, sec = timestamp.split(":")
|
153
|
-
seconds.append(
|
154
|
-
int(hour) * SEC_IN_HOUR + int(minute) * SEC_IN_MINUTE + int(sec)
|
155
|
-
)
|
156
|
-
return CloudnetArray(utils.seconds2hours(np.array(seconds)), "time")
|
157
|
-
|
158
|
-
def _convert_data(self, keys: tuple, value: float, method: str = "divide") -> None:
|
159
|
-
for key in keys:
|
160
|
-
if key in self.data:
|
161
|
-
if method == "divide":
|
162
|
-
self.data[key].data /= value
|
163
|
-
elif method == "add":
|
164
|
-
self.data[key].data += value
|
165
|
-
else:
|
166
|
-
raise ValueError
|
167
|
-
|
168
|
-
def _append_spectra(self) -> None:
|
169
|
-
array = ma.masked_all(
|
170
|
-
(len(self._file_data["scalars"]), self.n_diameter, self.n_velocity),
|
171
|
-
)
|
172
|
-
for time_ind, row in enumerate(self._file_data["spectra"]):
|
173
|
-
values = _parse_int(row)
|
174
|
-
if len(values) != self.n_diameter * self.n_velocity:
|
175
|
-
continue
|
176
|
-
array[time_ind, :, :] = np.reshape(
|
177
|
-
values,
|
178
|
-
(self.n_diameter, self.n_velocity),
|
179
|
-
)
|
180
|
-
self.data["data_raw"] = CloudnetArray(
|
181
|
-
array,
|
182
|
-
"data_raw",
|
183
|
-
dimensions=("time", "diameter", "velocity"),
|
184
|
-
data_type="i2",
|
185
|
-
)
|
186
|
-
|
187
|
-
@classmethod
|
188
39
|
def store_vectors(
|
189
|
-
|
190
|
-
data,
|
40
|
+
self,
|
191
41
|
n_values: list,
|
192
42
|
spreads: list,
|
193
43
|
name: str,
|
194
44
|
start: float = 0.0,
|
195
45
|
):
|
196
|
-
mid, bounds, spread =
|
197
|
-
data[name] = CloudnetArray(mid, name, dimensions=(name,))
|
46
|
+
mid, bounds, spread = self._create_vectors(n_values, spreads, start)
|
47
|
+
self.data[name] = CloudnetArray(mid, name, dimensions=(name,))
|
198
48
|
key = f"{name}_spread"
|
199
|
-
data[key] = CloudnetArray(spread, key, dimensions=(name,))
|
49
|
+
self.data[key] = CloudnetArray(spread, key, dimensions=(name,))
|
200
50
|
key = f"{name}_bnds"
|
201
|
-
data[key] = CloudnetArray(bounds, key, dimensions=(name, "nv"))
|
51
|
+
self.data[key] = CloudnetArray(bounds, key, dimensions=(name, "nv"))
|
202
52
|
|
203
|
-
@staticmethod
|
204
53
|
def _create_vectors(
|
54
|
+
self,
|
205
55
|
n_values: list[int],
|
206
56
|
spreads: list[float],
|
207
57
|
start: float,
|
@@ -221,24 +71,6 @@ class Disdrometer(CloudnetInstrument):
|
|
221
71
|
return mid_value, bounds, spread
|
222
72
|
|
223
73
|
|
224
|
-
def _format_thies_date(date: str) -> str:
|
225
|
-
day, month, year = date.split(".")
|
226
|
-
year = f"20{year}"
|
227
|
-
return f"{year}-{month.zfill(2)}-{day.zfill(2)}"
|
228
|
-
|
229
|
-
|
230
|
-
def _parse_int(row: np.ndarray) -> np.ndarray:
|
231
|
-
values = ma.masked_all((len(row),))
|
232
|
-
for ind, value in enumerate(row):
|
233
|
-
try:
|
234
|
-
value_int = int(value)
|
235
|
-
if value_int != 0:
|
236
|
-
values[ind] = value_int
|
237
|
-
except ValueError:
|
238
|
-
pass
|
239
|
-
return values
|
240
|
-
|
241
|
-
|
242
74
|
ATTRIBUTES = {
|
243
75
|
"velocity": MetaData(
|
244
76
|
long_name="Center fall velocity of precipitation particles",
|
@@ -6,7 +6,7 @@ from collections import defaultdict
|
|
6
6
|
from collections.abc import Callable, Iterable, Iterator, Sequence
|
7
7
|
from itertools import islice
|
8
8
|
from os import PathLike
|
9
|
-
from typing import Any
|
9
|
+
from typing import Any
|
10
10
|
|
11
11
|
import numpy as np
|
12
12
|
from numpy import ma
|
@@ -16,7 +16,6 @@ from cloudnetpy.cloudnetarray import CloudnetArray
|
|
16
16
|
from cloudnetpy.constants import MM_TO_M, SEC_IN_HOUR
|
17
17
|
from cloudnetpy.exceptions import DisdrometerDataError
|
18
18
|
from cloudnetpy.instruments import instruments
|
19
|
-
from cloudnetpy.instruments.cloudnet_instrument import CloudnetInstrument
|
20
19
|
|
21
20
|
from .common import ATTRIBUTES, Disdrometer
|
22
21
|
|
@@ -78,7 +77,7 @@ def parsivel2nc(
|
|
78
77
|
return output.save_level1b(disdrometer, output_file, uuid)
|
79
78
|
|
80
79
|
|
81
|
-
class Parsivel(
|
80
|
+
class Parsivel(Disdrometer):
|
82
81
|
def __init__(
|
83
82
|
self,
|
84
83
|
filenames: Iterable[str | PathLike],
|
@@ -142,12 +141,12 @@ class Parsivel(CloudnetInstrument):
|
|
142
141
|
def _create_velocity_vectors(self) -> None:
|
143
142
|
n_values = [10, 5, 5, 5, 5, 2]
|
144
143
|
spreads = [0.1, 0.2, 0.4, 0.8, 1.6, 3.2]
|
145
|
-
|
144
|
+
self.store_vectors(n_values, spreads, "velocity")
|
146
145
|
|
147
146
|
def _create_diameter_vectors(self) -> None:
|
148
147
|
n_values = [10, 5, 5, 5, 5, 2]
|
149
148
|
spreads = [0.125, 0.25, 0.5, 1, 2, 3]
|
150
|
-
|
149
|
+
self.store_vectors(n_values, spreads, "diameter")
|
151
150
|
|
152
151
|
def mask_invalid_values(self) -> None:
|
153
152
|
if variable := self.data.get("number_concentration"):
|
@@ -166,32 +165,6 @@ class Parsivel(CloudnetInstrument):
|
|
166
165
|
if variable := self.data.get("number_concentration"):
|
167
166
|
variable.data = np.power(10, variable.data).round().astype(np.uint32)
|
168
167
|
|
169
|
-
def add_meta(self) -> None:
|
170
|
-
valid_keys = ("latitude", "longitude", "altitude")
|
171
|
-
for key, value in self.site_meta.items():
|
172
|
-
name = key.lower()
|
173
|
-
if name in valid_keys:
|
174
|
-
self.data[name] = CloudnetArray(float(value), name)
|
175
|
-
|
176
|
-
def _convert_data(
|
177
|
-
self,
|
178
|
-
keys: tuple[str, ...],
|
179
|
-
value: float,
|
180
|
-
method: Literal["divide", "add"] = "divide",
|
181
|
-
) -> None:
|
182
|
-
for key in keys:
|
183
|
-
if key not in self.data:
|
184
|
-
continue
|
185
|
-
variable = self.data[key]
|
186
|
-
if method == "divide":
|
187
|
-
variable.data = variable.data.astype("f4") / value
|
188
|
-
variable.data_type = "f4"
|
189
|
-
elif method == "add":
|
190
|
-
variable.data = variable.data.astype("f4") + value
|
191
|
-
variable.data_type = "f4"
|
192
|
-
else:
|
193
|
-
raise ValueError
|
194
|
-
|
195
168
|
|
196
169
|
CSV_HEADERS = {
|
197
170
|
"Date": "_date",
|
@@ -1,8 +1,70 @@
|
|
1
|
+
import datetime
|
2
|
+
from collections import defaultdict
|
3
|
+
from os import PathLike
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
import numpy as np
|
7
|
+
|
1
8
|
from cloudnetpy import output
|
9
|
+
from cloudnetpy.cloudnetarray import CloudnetArray
|
10
|
+
from cloudnetpy.constants import MM_TO_M, SEC_IN_HOUR
|
2
11
|
from cloudnetpy.exceptions import DisdrometerDataError
|
3
12
|
from cloudnetpy.instruments import instruments
|
13
|
+
from cloudnetpy.instruments.toa5 import read_toa5
|
4
14
|
|
5
|
-
from .common import ATTRIBUTES,
|
15
|
+
from .common import ATTRIBUTES, Disdrometer
|
16
|
+
|
17
|
+
TELEGRAM4 = [
|
18
|
+
(1, "_serial_number"),
|
19
|
+
(2, "_software_version"),
|
20
|
+
(3, "_date"),
|
21
|
+
(4, "_time"),
|
22
|
+
(5, "_synop_5min_ww"),
|
23
|
+
(6, "_synop_5min_WaWa"),
|
24
|
+
(7, "_metar_5min_4678"),
|
25
|
+
(8, "_rainfall_rate_5min"),
|
26
|
+
(9, "synop_WW"), # 1min
|
27
|
+
(10, "synop_WaWa"), # 1min
|
28
|
+
(11, "_metar_1_min_4678"),
|
29
|
+
(12, "rainfall_rate_1min_total"),
|
30
|
+
(13, "rainfall_rate"), # liquid, mm h-1
|
31
|
+
(14, "rainfall_rate_1min_solid"),
|
32
|
+
(15, "_precipition_amount"), # mm
|
33
|
+
(16, "visibility"),
|
34
|
+
(17, "radar_reflectivity"),
|
35
|
+
(18, "measurement_quality"),
|
36
|
+
(19, "maximum_hail_diameter"),
|
37
|
+
(20, "status_laser"),
|
38
|
+
(21, "static_signal"),
|
39
|
+
(22, "status_T_laser_analogue"),
|
40
|
+
(23, "status_T_laser_digital"),
|
41
|
+
(24, "status_I_laser_analogue"),
|
42
|
+
(25, "status_I_laser_digital"),
|
43
|
+
(26, "status_sensor_supply"),
|
44
|
+
(27, "status_laser_heating"),
|
45
|
+
(28, "status_receiver_heating"),
|
46
|
+
(29, "status_temperature_sensor"),
|
47
|
+
(30, "status_heating_supply"),
|
48
|
+
(31, "status_heating_housing"),
|
49
|
+
(32, "status_heating_heads"),
|
50
|
+
(33, "status_heating_carriers"),
|
51
|
+
(34, "status_laser_power"),
|
52
|
+
(35, "_status_reserve"),
|
53
|
+
(36, "T_interior"),
|
54
|
+
(37, "T_laser_driver"), # 0-80 C
|
55
|
+
(38, "I_mean_laser"),
|
56
|
+
(39, "V_control"), # mV 4005-4015
|
57
|
+
(40, "V_optical_output"), # mV 2300-6500
|
58
|
+
(41, "V_sensor_supply"), # 1/10V
|
59
|
+
(42, "I_heating_laser_head"), # mA
|
60
|
+
(43, "I_heating_receiver_head"), # mA
|
61
|
+
(44, "T_ambient"), # C
|
62
|
+
(45, "_V_heating_supply"),
|
63
|
+
(46, "_I_housing"),
|
64
|
+
(47, "_I_heating_heads"),
|
65
|
+
(48, "_I_heating_carriers"),
|
66
|
+
(49, "n_particles"),
|
67
|
+
]
|
6
68
|
|
7
69
|
|
8
70
|
def thies2nc(
|
@@ -10,7 +72,7 @@ def thies2nc(
|
|
10
72
|
output_file: str,
|
11
73
|
site_meta: dict,
|
12
74
|
uuid: str | None = None,
|
13
|
-
date: str | None = None,
|
75
|
+
date: str | datetime.date | None = None,
|
14
76
|
) -> str:
|
15
77
|
"""Converts Thies-LNM disdrometer data into Cloudnet Level 1b netCDF file.
|
16
78
|
|
@@ -36,17 +98,15 @@ def thies2nc(
|
|
36
98
|
>>> uuid = thies2nc('thies-lnm.log', 'thies-lnm.nc', site_meta)
|
37
99
|
|
38
100
|
"""
|
101
|
+
if isinstance(date, str):
|
102
|
+
date = datetime.date.fromisoformat(date)
|
39
103
|
try:
|
40
|
-
disdrometer = Thies(disdrometer_file, site_meta)
|
104
|
+
disdrometer = Thies(disdrometer_file, site_meta, date)
|
41
105
|
except (ValueError, IndexError) as err:
|
42
106
|
msg = "Unable to read disdrometer file"
|
43
107
|
raise DisdrometerDataError(msg) from err
|
44
|
-
|
45
|
-
|
46
|
-
disdrometer.init_data()
|
47
|
-
if date is not None:
|
48
|
-
disdrometer.sort_timestamps()
|
49
|
-
disdrometer.remove_duplicate_timestamps()
|
108
|
+
disdrometer.sort_timestamps()
|
109
|
+
disdrometer.remove_duplicate_timestamps()
|
50
110
|
disdrometer.add_meta()
|
51
111
|
disdrometer.convert_units()
|
52
112
|
attributes = output.add_time_attribute(ATTRIBUTES, disdrometer.date)
|
@@ -55,84 +115,158 @@ def thies2nc(
|
|
55
115
|
|
56
116
|
|
57
117
|
class Thies(Disdrometer):
|
58
|
-
def __init__(
|
59
|
-
|
118
|
+
def __init__(
|
119
|
+
self,
|
120
|
+
filename: str | PathLike,
|
121
|
+
site_meta: dict,
|
122
|
+
expected_date: datetime.date | None = None,
|
123
|
+
):
|
124
|
+
super().__init__()
|
125
|
+
self.instrument = instruments.THIES
|
60
126
|
self.n_velocity = 20
|
61
127
|
self.n_diameter = 22
|
62
|
-
self.
|
128
|
+
self.site_meta = site_meta
|
129
|
+
self.raw_data: dict[str, Any] = defaultdict(list)
|
130
|
+
self._read_data(filename)
|
131
|
+
self._screen_time(expected_date)
|
132
|
+
self.data = {}
|
133
|
+
self._append_data()
|
63
134
|
self._create_velocity_vectors()
|
64
135
|
self._create_diameter_vectors()
|
65
|
-
self.instrument = instruments.THIES
|
66
136
|
|
67
|
-
def
|
68
|
-
|
69
|
-
|
70
|
-
""
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
(
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
(
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
(
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
(
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
137
|
+
def convert_units(self) -> None:
|
138
|
+
mmh_to_ms = SEC_IN_HOUR / MM_TO_M
|
139
|
+
c_to_k = 273.15
|
140
|
+
self._convert_data(("rainfall_rate_1min_total",), mmh_to_ms)
|
141
|
+
self._convert_data(("rainfall_rate",), mmh_to_ms)
|
142
|
+
self._convert_data(("rainfall_rate_1min_solid",), mmh_to_ms)
|
143
|
+
self._convert_data(("diameter", "diameter_spread", "diameter_bnds"), 1e3)
|
144
|
+
self._convert_data(("V_sensor_supply",), 10)
|
145
|
+
self._convert_data(("I_mean_laser",), 100)
|
146
|
+
self._convert_data(("T_interior",), c_to_k, method="add")
|
147
|
+
self._convert_data(("T_ambient",), c_to_k, method="add")
|
148
|
+
self._convert_data(("T_laser_driver",), c_to_k, method="add")
|
149
|
+
|
150
|
+
def _read_data(self, filename: str | PathLike) -> None:
|
151
|
+
with open(filename) as file:
|
152
|
+
first_line = file.readline()
|
153
|
+
if "TOA5" in first_line:
|
154
|
+
for row in read_toa5(filename):
|
155
|
+
self._read_line(row["RawString"], row["TIMESTAMP"])
|
156
|
+
else:
|
157
|
+
with open(filename) as file:
|
158
|
+
for line in file:
|
159
|
+
self._read_line(line)
|
160
|
+
for key, value in self.raw_data.items():
|
161
|
+
array = np.array(value)
|
162
|
+
if key == "time":
|
163
|
+
array = array.astype("datetime64[s]")
|
164
|
+
self.raw_data[key] = array
|
165
|
+
|
166
|
+
def _append_data(self) -> None:
|
167
|
+
for key, values in self.raw_data.items():
|
168
|
+
if key.startswith("_"):
|
169
|
+
continue
|
170
|
+
name_out = key
|
171
|
+
values_out = values
|
172
|
+
match key:
|
173
|
+
case "spectrum":
|
174
|
+
name_out = "data_raw"
|
175
|
+
dimensions = ["time", "diameter", "velocity"]
|
176
|
+
case "time":
|
177
|
+
dimensions = []
|
178
|
+
base = values[0].astype("datetime64[D]")
|
179
|
+
values_out = (values - base) / np.timedelta64(1, "h")
|
180
|
+
case _:
|
181
|
+
dimensions = ["time"]
|
182
|
+
self.data[name_out] = CloudnetArray(
|
183
|
+
values_out, name_out, dimensions=dimensions
|
184
|
+
)
|
185
|
+
|
186
|
+
first_id = self.raw_data["_serial_number"][0]
|
187
|
+
for sensor_id in self.raw_data["_serial_number"]:
|
188
|
+
if sensor_id != first_id:
|
189
|
+
msg = "Multiple serial numbers are not supported"
|
190
|
+
raise DisdrometerDataError(msg)
|
191
|
+
self.serial_number = first_id
|
192
|
+
|
193
|
+
def _read_line(self, line: str, timestamp: datetime.datetime | None = None):
|
194
|
+
raw_values = line.split(";")
|
195
|
+
if len(raw_values) != 521:
|
196
|
+
return
|
197
|
+
for i, key in TELEGRAM4:
|
198
|
+
value: Any
|
199
|
+
if key == "_date":
|
200
|
+
value = _parse_date(raw_values[i])
|
201
|
+
elif key == "_time":
|
202
|
+
value = _parse_time(raw_values[i])
|
203
|
+
elif key in (
|
204
|
+
"I_heating",
|
205
|
+
"T_ambient",
|
206
|
+
"T_interior",
|
207
|
+
"T_laser_driver",
|
208
|
+
"V_power_supply",
|
209
|
+
"_precipition_amount",
|
210
|
+
"_rainfall_rate_5min",
|
211
|
+
"maximum_hail_diameter",
|
212
|
+
"radar_reflectivity",
|
213
|
+
"rainfall_rate",
|
214
|
+
"rainfall_rate_1min_solid",
|
215
|
+
"rainfall_rate_1min_total",
|
216
|
+
):
|
217
|
+
value = float(raw_values[i])
|
218
|
+
elif key in (
|
219
|
+
"_serial_number",
|
220
|
+
"_software_version",
|
221
|
+
"_metar_5min_4678",
|
222
|
+
"_metar_1_min_4678",
|
223
|
+
):
|
224
|
+
value = raw_values[i]
|
225
|
+
else:
|
226
|
+
value = int(raw_values[i])
|
227
|
+
self.raw_data[key].append(value)
|
228
|
+
self.raw_data["spectrum"].append(
|
229
|
+
np.array(list(map(int, raw_values[79:-2])), dtype="i2").reshape(
|
230
|
+
self.n_diameter, self.n_velocity
|
231
|
+
)
|
232
|
+
)
|
233
|
+
if timestamp is not None:
|
234
|
+
self.raw_data["time"].append(timestamp)
|
235
|
+
else:
|
236
|
+
self.raw_data["time"].append(
|
237
|
+
datetime.datetime.combine(
|
238
|
+
self.raw_data["_date"][-1], self.raw_data["_time"][-1]
|
239
|
+
)
|
240
|
+
)
|
241
|
+
|
242
|
+
def _screen_time(self, expected_date: datetime.date | None = None) -> None:
|
243
|
+
if expected_date is None:
|
244
|
+
self.date = self.raw_data["time"][0].astype(object).date()
|
245
|
+
return
|
246
|
+
self.date = expected_date
|
247
|
+
valid_mask = self.raw_data["time"].astype("datetime64[D]") == self.date
|
248
|
+
if np.count_nonzero(valid_mask) == 0:
|
249
|
+
msg = f"No data found on {expected_date}"
|
250
|
+
raise DisdrometerDataError(msg)
|
251
|
+
for key in self.raw_data:
|
252
|
+
self.raw_data[key] = self.raw_data[key][valid_mask]
|
129
253
|
|
130
254
|
def _create_velocity_vectors(self) -> None:
|
131
255
|
n_values = [5, 6, 7, 1, 1]
|
132
256
|
spreads = [0.2, 0.4, 0.8, 1, 10]
|
133
|
-
self.store_vectors(
|
257
|
+
self.store_vectors(n_values, spreads, "velocity")
|
134
258
|
|
135
259
|
def _create_diameter_vectors(self) -> None:
|
136
260
|
n_values = [3, 6, 13]
|
137
261
|
spreads = [0.125, 0.25, 0.5]
|
138
|
-
self.store_vectors(
|
262
|
+
self.store_vectors(n_values, spreads, "diameter", start=0.125)
|
263
|
+
|
264
|
+
|
265
|
+
def _parse_date(date: str) -> datetime.date:
|
266
|
+
day, month, year = map(int, date.split("."))
|
267
|
+
return datetime.date(2000 + year, month, day)
|
268
|
+
|
269
|
+
|
270
|
+
def _parse_time(time: str) -> datetime.time:
|
271
|
+
hour, minute, second = map(int, time.split(":"))
|
272
|
+
return datetime.time(hour, minute, second)
|
@@ -0,0 +1,45 @@
|
|
1
|
+
import csv
|
2
|
+
import datetime
|
3
|
+
from os import PathLike
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
|
7
|
+
def read_toa5(filename: str | PathLike) -> list[dict[str, Any]]:
|
8
|
+
"""Read ASCII data from Campbell Scientific datalogger such as CR1000.
|
9
|
+
|
10
|
+
References
|
11
|
+
CR1000 Measurement and Control System.
|
12
|
+
https://s.campbellsci.com/documents/us/manuals/cr1000.pdf
|
13
|
+
"""
|
14
|
+
with open(filename) as file:
|
15
|
+
reader = csv.reader(file)
|
16
|
+
origin_line = next(reader)
|
17
|
+
if len(origin_line) == 0 or origin_line[0] != "TOA5":
|
18
|
+
msg = "Invalid TOA5 file"
|
19
|
+
raise ValueError(msg)
|
20
|
+
header_line = next(reader)
|
21
|
+
_units_line = next(reader)
|
22
|
+
_process_line = next(reader)
|
23
|
+
output = []
|
24
|
+
|
25
|
+
row_template: dict[str, Any] = {}
|
26
|
+
for header in header_line:
|
27
|
+
if "(" in header:
|
28
|
+
row_template[header[: header.index("(")]] = []
|
29
|
+
|
30
|
+
for data_line in reader:
|
31
|
+
row = row_template.copy()
|
32
|
+
for key, value in zip(header_line, data_line, strict=False):
|
33
|
+
parsed_value: Any = value
|
34
|
+
if key == "TIMESTAMP":
|
35
|
+
parsed_value = datetime.datetime.strptime(
|
36
|
+
parsed_value, "%Y-%m-%d %H:%M:%S"
|
37
|
+
)
|
38
|
+
elif key == "RECORD":
|
39
|
+
parsed_value = int(parsed_value)
|
40
|
+
if "(" in key:
|
41
|
+
row[key[: key.index("(")]].append(parsed_value)
|
42
|
+
else:
|
43
|
+
row[key] = parsed_value
|
44
|
+
output.append(row)
|
45
|
+
return output
|
cloudnetpy/version.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
cloudnetpy/__init__.py,sha256=X_FqY-4yg5GUj5Edo14SToLEos6JIsC3fN-v1FUgQoA,43
|
2
2
|
cloudnetpy/cloudnetarray.py,sha256=HT6bLtjnimOVbGrdjQBqD0F8GW0KWkn2qhaIGFMKLAY,6987
|
3
|
-
cloudnetpy/concat_lib.py,sha256
|
3
|
+
cloudnetpy/concat_lib.py,sha256=-pXH7xjU7nm7tWdgwnrV6wC-g4PZOzYVPMYm1oOud-M,9845
|
4
4
|
cloudnetpy/constants.py,sha256=l7_ohQgLEQ6XEG9AMBarTPKp9OM8B1ElJ6fSN0ScdmM,733
|
5
5
|
cloudnetpy/datasource.py,sha256=CSiKQGVEX459tagRjLrww6hZMZcc3r1sR2WcaTKTTWo,7864
|
6
6
|
cloudnetpy/exceptions.py,sha256=wrI0bZTwmS5C_cqOmvlJ8XJSEFyzuD1eD4voGJc_Gjg,1584
|
@@ -8,7 +8,7 @@ cloudnetpy/metadata.py,sha256=v_VDo2vbdTxB0zIsfP69IcrwSKiRlLpsGdq6JPI4CoA,5306
|
|
8
8
|
cloudnetpy/output.py,sha256=WoVTNuxni0DUr163vZ-_mDr1brXhY15XSlGMrq9Aoqw,14700
|
9
9
|
cloudnetpy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
10
|
cloudnetpy/utils.py,sha256=0TlHm71YtSrKXBsRKctitnhQrvZPE-ulEVeAQW-oK58,27398
|
11
|
-
cloudnetpy/version.py,sha256=
|
11
|
+
cloudnetpy/version.py,sha256=Sl4s2YJM5mEMshNmab58jItvB7Cc6_rgzSXyvi83lp8,72
|
12
12
|
cloudnetpy/categorize/__init__.py,sha256=gP5q3Vis1y9u9OWgA_idlbjfWXYN_S0IBSWdwBhL_uU,69
|
13
13
|
cloudnetpy/categorize/atmos.py,sha256=fWW8ye_8HZASRAiYwURFKWzcGOYIA2RKeVxCq0lVOuM,12389
|
14
14
|
cloudnetpy/categorize/atmos_utils.py,sha256=wndpwJxc2-QnNTkV8tc8I11Vs_WkNz9sVMX1fuGgUC4,3777
|
@@ -45,12 +45,13 @@ cloudnetpy/instruments/pollyxt.py,sha256=SccV9htZ5MWrK7JEleOr4hbmeTr-lKktUzAt7H9
|
|
45
45
|
cloudnetpy/instruments/radiometrics.py,sha256=2ofeZ6KJ_JOWTd3UA-wSzJpM5cjN7R4jZeBLJCQKEYc,7624
|
46
46
|
cloudnetpy/instruments/rpg.py,sha256=U8nEOlOI74f2lk2w4C4xKZCrW6AkDZpQZYE3yv7SNHE,17130
|
47
47
|
cloudnetpy/instruments/rpg_reader.py,sha256=LAdXL3TmD5QzQbqtPOcemZji_qkXwmw6a6F8NmF6Zg8,11355
|
48
|
+
cloudnetpy/instruments/toa5.py,sha256=xYJYEVNykCWqIsESno0eBIWqkYb-LHXjFjUp3EoqGDU,1565
|
48
49
|
cloudnetpy/instruments/vaisala.py,sha256=GzESZvboOoXzWmmr9dC-y6oM6ogc-M-zT3KmBTaD0LI,14512
|
49
50
|
cloudnetpy/instruments/weather_station.py,sha256=gTY3Y5UATqJo9Gld4hm7WdsKBwcF8WgNTIK2nOfl3Nc,5739
|
50
51
|
cloudnetpy/instruments/disdrometer/__init__.py,sha256=lyjwttWvFvuwYxEkusoAvgRcbBmglmOp5HJOpXUqLWo,93
|
51
|
-
cloudnetpy/instruments/disdrometer/common.py,sha256=
|
52
|
-
cloudnetpy/instruments/disdrometer/parsivel.py,sha256=
|
53
|
-
cloudnetpy/instruments/disdrometer/thies.py,sha256=
|
52
|
+
cloudnetpy/instruments/disdrometer/common.py,sha256=g52iK2aNp3Z88kovUmGVpC54NZomPa9D871gzO0AmQ4,9267
|
53
|
+
cloudnetpy/instruments/disdrometer/parsivel.py,sha256=WiL-vCjw9Gmb5irvW3AXddsyprp8MGOfqcVAlfy0zpc,25521
|
54
|
+
cloudnetpy/instruments/disdrometer/thies.py,sha256=dNSpRlyZPjQDVA6cm-Xkh-ub1fwLxok-9-xZBxcYIJA,9645
|
54
55
|
cloudnetpy/model_evaluation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
55
56
|
cloudnetpy/model_evaluation/file_handler.py,sha256=oUGIblcEWLLv16YKUch-M5KA-dGRAcuHa-9anP3xtX4,6447
|
56
57
|
cloudnetpy/model_evaluation/metadata.py,sha256=7ZL87iDbaQJIMu8wfnMvb01cGVPkl8RtvEm_tt9uIHE,8413
|
@@ -107,8 +108,8 @@ cloudnetpy/products/mie_lu_tables.nc,sha256=It4fYpqJXlqOgL8jeZ-PxGzP08PMrELIDVe5
|
|
107
108
|
cloudnetpy/products/mwr_tools.py,sha256=PRm5aCULccUehU-Byk55wYhhEHseMjoAjGBu5TSyHao,4621
|
108
109
|
cloudnetpy/products/product_tools.py,sha256=rhx_Ru9FLlQqCNM-awoiHx18-Aq1eBwL9LiUaQoJs6k,10412
|
109
110
|
docs/source/conf.py,sha256=IKiFWw6xhUd8NrCg0q7l596Ck1d61XWeVjIFHVSG9Og,1490
|
110
|
-
cloudnetpy-1.
|
111
|
-
cloudnetpy-1.
|
112
|
-
cloudnetpy-1.
|
113
|
-
cloudnetpy-1.
|
114
|
-
cloudnetpy-1.
|
111
|
+
cloudnetpy-1.61.1.dist-info/LICENSE,sha256=wcZF72bdaoG9XugpyE95Juo7lBQOwLuTKBOhhtANZMM,1094
|
112
|
+
cloudnetpy-1.61.1.dist-info/METADATA,sha256=sAC98q1U1SacTP8qVR9H9UmYNDx8AZEU2FVSfDOnvGk,5784
|
113
|
+
cloudnetpy-1.61.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
114
|
+
cloudnetpy-1.61.1.dist-info/top_level.txt,sha256=ibSPWRr6ojS1i11rtBFz2_gkIe68mggj7aeswYfaOo0,16
|
115
|
+
cloudnetpy-1.61.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|