cloudnetpy 1.60.3__py3-none-any.whl → 1.61.0__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 +10 -189
- cloudnetpy/instruments/disdrometer/parsivel.py +4 -31
- cloudnetpy/instruments/disdrometer/thies.py +211 -77
- cloudnetpy/instruments/toa5.py +45 -0
- cloudnetpy/plotting/plotting.py +5 -4
- cloudnetpy/version.py +2 -2
- {cloudnetpy-1.60.3.dist-info → cloudnetpy-1.61.0.dist-info}/METADATA +1 -1
- {cloudnetpy-1.60.3.dist-info → cloudnetpy-1.61.0.dist-info}/RECORD +12 -11
- {cloudnetpy-1.60.3.dist-info → cloudnetpy-1.61.0.dist-info}/LICENSE +0 -0
- {cloudnetpy-1.60.3.dist-info → cloudnetpy-1.61.0.dist-info}/WHEEL +0 -0
- {cloudnetpy-1.60.3.dist-info → cloudnetpy-1.61.0.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,46 @@
|
|
1
1
|
"""Module for reading / converting disdrometer data."""
|
2
|
-
import logging
|
3
2
|
|
4
3
|
import numpy as np
|
5
|
-
from numpy import ma
|
6
4
|
|
7
|
-
from cloudnetpy import utils
|
8
5
|
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
6
|
from cloudnetpy.instruments.cloudnet_instrument import CloudnetInstrument
|
12
|
-
from cloudnetpy.instruments.vaisala import values_to_dict
|
13
7
|
from cloudnetpy.metadata import MetaData
|
14
8
|
|
15
|
-
PARSIVEL = "OTT Parsivel-2"
|
16
|
-
THIES = "Thies-LNM"
|
17
|
-
|
18
9
|
|
19
10
|
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
11
|
def add_meta(self) -> None:
|
46
|
-
|
12
|
+
valid_keys = ("latitude", "longitude", "altitude")
|
47
13
|
for key, value in self.site_meta.items():
|
48
14
|
name = key.lower()
|
49
|
-
if name in
|
15
|
+
if name in valid_keys:
|
50
16
|
self.data[name] = CloudnetArray(float(value), name)
|
51
17
|
|
52
|
-
def validate_date(self, expected_date: str) -> None:
|
53
|
-
valid_ind = []
|
54
|
-
for ind, row in enumerate(self._file_data["scalars"]):
|
55
|
-
if self.source == PARSIVEL:
|
56
|
-
raise NotImplementedError
|
57
|
-
date = _format_thies_date(row[3])
|
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)
|
101
|
-
for key in keys:
|
102
|
-
if key.startswith("_"):
|
103
|
-
continue
|
104
|
-
invalid_value = -9999.0
|
105
|
-
float_array = ma.array([])
|
106
|
-
for value_str in data_dict[key]:
|
107
|
-
try:
|
108
|
-
float_array = ma.append(float_array, float(value_str))
|
109
|
-
except ValueError:
|
110
|
-
logging.warning(
|
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"
|
127
|
-
else:
|
128
|
-
data_type = "i4"
|
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
|
146
|
-
|
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
18
|
def _convert_data(self, keys: tuple, value: float, method: str = "divide") -> None:
|
159
19
|
for key in keys:
|
160
20
|
if key in self.data:
|
161
21
|
if method == "divide":
|
162
|
-
self.data[key].data
|
22
|
+
self.data[key].data = self.data[key].data / value
|
163
23
|
elif method == "add":
|
164
|
-
self.data[key].data
|
24
|
+
self.data[key].data = self.data[key].data + value
|
165
25
|
else:
|
166
26
|
raise ValueError
|
167
27
|
|
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
28
|
def store_vectors(
|
189
|
-
|
190
|
-
data,
|
29
|
+
self,
|
191
30
|
n_values: list,
|
192
31
|
spreads: list,
|
193
32
|
name: str,
|
194
33
|
start: float = 0.0,
|
195
34
|
):
|
196
|
-
mid, bounds, spread =
|
197
|
-
data[name] = CloudnetArray(mid, name, dimensions=(name,))
|
35
|
+
mid, bounds, spread = self._create_vectors(n_values, spreads, start)
|
36
|
+
self.data[name] = CloudnetArray(mid, name, dimensions=(name,))
|
198
37
|
key = f"{name}_spread"
|
199
|
-
data[key] = CloudnetArray(spread, key, dimensions=(name,))
|
38
|
+
self.data[key] = CloudnetArray(spread, key, dimensions=(name,))
|
200
39
|
key = f"{name}_bnds"
|
201
|
-
data[key] = CloudnetArray(bounds, key, dimensions=(name, "nv"))
|
40
|
+
self.data[key] = CloudnetArray(bounds, key, dimensions=(name, "nv"))
|
202
41
|
|
203
|
-
@staticmethod
|
204
42
|
def _create_vectors(
|
43
|
+
self,
|
205
44
|
n_values: list[int],
|
206
45
|
spreads: list[float],
|
207
46
|
start: float,
|
@@ -221,24 +60,6 @@ class Disdrometer(CloudnetInstrument):
|
|
221
60
|
return mid_value, bounds, spread
|
222
61
|
|
223
62
|
|
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
63
|
ATTRIBUTES = {
|
243
64
|
"velocity": MetaData(
|
244
65
|
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/plotting/plotting.py
CHANGED
@@ -281,10 +281,11 @@ class SubPlot:
|
|
281
281
|
|
282
282
|
def _read_plot_meta(self, file_type: str | None) -> PlotMeta:
|
283
283
|
if self.options.plot_meta is not None:
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
284
|
+
plot_meta = self.options.plot_meta
|
285
|
+
else:
|
286
|
+
fallback = ATTRIBUTES["fallback"].get(self.variable.name, PlotMeta())
|
287
|
+
file_attributes = ATTRIBUTES.get(file_type or "", {})
|
288
|
+
plot_meta = file_attributes.get(self.variable.name, fallback)
|
288
289
|
if plot_meta.clabel is None:
|
289
290
|
plot_meta = plot_meta._replace(clabel=_reformat_units(self.variable.units))
|
290
291
|
return plot_meta
|
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=yXaNRiv7_J3_1S8AdHMUGGwtfSHvLCTJ991S3zY0sbw,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=A9k4z4SwZaqORUca3fYVCG0aS1Emy84B2755kShGeY8,9040
|
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
|
@@ -93,7 +94,7 @@ cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py,sha256=Ra3r4V
|
|
93
94
|
cloudnetpy/model_evaluation/tests/unit/test_tools.py,sha256=Ia_VrLdV2NstX5gbx_3AZTOAlrgLAy_xFZ8fHYVX0xI,3817
|
94
95
|
cloudnetpy/plotting/__init__.py,sha256=lg9Smn4BI0dVBgnDLC3JVJ4GmwoSnO-qoSd4ApvwV6Y,107
|
95
96
|
cloudnetpy/plotting/plot_meta.py,sha256=cLdCZrhbP-gaobS_zjcf8d2xVALzl7zh2qpttxCHyrg,15983
|
96
|
-
cloudnetpy/plotting/plotting.py,sha256=
|
97
|
+
cloudnetpy/plotting/plotting.py,sha256=bve91iM9RcWmKaZOFWxVh2y3DPmupI1944MMYDdv17I,32459
|
97
98
|
cloudnetpy/products/__init__.py,sha256=2hRb5HG9hNrxH1if5laJkLeFeaZCd5W1q3hh4ewsX0E,273
|
98
99
|
cloudnetpy/products/classification.py,sha256=0E9OUGR3uLCsS1nORwQu0SqW0_8uX7n6LlRcVhtzKw4,7845
|
99
100
|
cloudnetpy/products/der.py,sha256=mam6jWV7A2h8V5WC3DIeFp6ou7UD1JOw9r7h2B0su-s,12403
|
@@ -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.0.dist-info/LICENSE,sha256=wcZF72bdaoG9XugpyE95Juo7lBQOwLuTKBOhhtANZMM,1094
|
112
|
+
cloudnetpy-1.61.0.dist-info/METADATA,sha256=ZxuqGJhzfPtzacU04IO88UaXFvUlchwavhzhduLpPtA,5784
|
113
|
+
cloudnetpy-1.61.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
114
|
+
cloudnetpy-1.61.0.dist-info/top_level.txt,sha256=ibSPWRr6ojS1i11rtBFz2_gkIe68mggj7aeswYfaOo0,16
|
115
|
+
cloudnetpy-1.61.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|