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 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(filename: str, output_file: str, n_profiles: int) -> None:
9
- """Truncates netcdf file in 'time' dimension taking only n_profiles.
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 == "time" else nc.dimensions[dim].size
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 "time" in dimensions[0]:
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
- valid_names = ("latitude", "longitude", "altitude")
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 valid_names:
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 /= value
22
+ self.data[key].data = self.data[key].data / value
163
23
  elif method == "add":
164
- self.data[key].data += value
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
- cls,
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 = cls._create_vectors(n_values, spreads, start)
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, Literal
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(CloudnetInstrument):
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
- Disdrometer.store_vectors(self.data, n_values, spreads, "velocity")
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
- Disdrometer.store_vectors(self.data, n_values, spreads, "diameter")
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, THIES, Disdrometer, _format_thies_date
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
- if date is not None:
45
- disdrometer.validate_date(date)
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__(self, filename: str, site_meta: dict):
59
- super().__init__(filename, site_meta, THIES)
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.date = self._init_date()
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 init_data(self) -> None:
68
- """According to
69
- https://www.biral.com/wp-content/uploads/2015/01/5.4110.xx_.xxx_.pdf
70
- """
71
- column_and_key = [
72
- (1, "_serial_number"),
73
- (2, "_software_version"),
74
- (3, "_date"),
75
- (4, "_time"),
76
- (5, "_synop_5min_ww"),
77
- (6, "_synop_5min_WaWa"),
78
- (7, "_metar_5min_4678"),
79
- (8, "_rainfall_rate_5min"),
80
- (9, "synop_WW"), # 1min
81
- (10, "synop_WaWa"), # 1min
82
- (11, "_metar_1_min_4678"),
83
- (12, "rainfall_rate_1min_total"),
84
- (13, "rainfall_rate"), # liquid, mm h-1
85
- (14, "rainfall_rate_1min_solid"),
86
- (15, "_precipition_amount"), # mm
87
- (16, "visibility"),
88
- (17, "radar_reflectivity"),
89
- (18, "measurement_quality"),
90
- (19, "maximum_hail_diameter"),
91
- (20, "status_laser"),
92
- (21, "static_signal"),
93
- (22, "status_T_laser_analogue"),
94
- (23, "status_T_laser_digital"),
95
- (24, "status_I_laser_analogue"),
96
- (25, "status_I_laser_digital"),
97
- (26, "status_sensor_supply"),
98
- (27, "status_laser_heating"),
99
- (28, "status_receiver_heating"),
100
- (29, "status_temperature_sensor"),
101
- (30, "status_heating_supply"),
102
- (31, "status_heating_housing"),
103
- (32, "status_heating_heads"),
104
- (33, "status_heating_carriers"),
105
- (34, "status_laser_power"),
106
- (35, "_status_reserve"),
107
- (36, "T_interior"),
108
- (37, "T_laser_driver"), # 0-80 C
109
- (38, "I_mean_laser"),
110
- (39, "V_control"), # mV 4005-4015
111
- (40, "V_optical_output"), # mV 2300-6500
112
- (41, "V_sensor_supply"), # 1/10V
113
- (42, "I_heating_laser_head"), # mA
114
- (43, "I_heating_receiver_head"), # mA
115
- (44, "T_ambient"), # C
116
- (45, "_V_heating_supply"),
117
- (46, "_I_housing"),
118
- (47, "_I_heating_heads"),
119
- (48, "_I_heating_carriers"),
120
- (49, "n_particles"),
121
- ]
122
- self._append_data(column_and_key)
123
- self._append_spectra()
124
-
125
- def _init_date(self) -> list:
126
- first_date = self._file_data["scalars"][0][3]
127
- first_date = _format_thies_date(first_date)
128
- return first_date.split("-")
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(self.data, n_values, spreads, "velocity")
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(self.data, n_values, spreads, "diameter", start=0.125)
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
@@ -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
- return self.options.plot_meta
285
- fallback = ATTRIBUTES["fallback"].get(self.variable.name, PlotMeta())
286
- file_attributes = ATTRIBUTES.get(file_type or "", {})
287
- plot_meta = file_attributes.get(self.variable.name, fallback)
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,4 +1,4 @@
1
1
  MAJOR = 1
2
- MINOR = 60
3
- PATCH = 3
2
+ MINOR = 61
3
+ PATCH = 0
4
4
  __version__ = f"{MAJOR}.{MINOR}.{PATCH}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloudnetpy
3
- Version: 1.60.3
3
+ Version: 1.61.0
4
4
  Summary: Python package for Cloudnet processing
5
5
  Author: Simo Tukiainen
6
6
  License: MIT License
@@ -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=YK5ho5msqwNxpPtPT8f2OewIJ8hTrbhCkaxHaBKLTI0,9809
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=fW-PBXVNQlnZz6np_OiXBmF_xTaIE-Lpj9n9IljdZOw,72
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=3z2pVgDuUMxM5sIJc-wYhdGYSOUsAG_Mp6MI8mPlJ5w,15843
52
- cloudnetpy/instruments/disdrometer/parsivel.py,sha256=_W0uW3DhD2myzuOWi6Izv6W6dcg9EDQrolSnMRVnrSI,26545
53
- cloudnetpy/instruments/disdrometer/thies.py,sha256=h7EwZ9tn47UUMiYqDQ68vkXv4q0rEqX1ZeFXd7XJYNg,5050
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=Qm1eQK1eTnXtHzmhxerYrKMpZTCxoOvwS8f1IxsZGaM,32428
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.60.3.dist-info/LICENSE,sha256=wcZF72bdaoG9XugpyE95Juo7lBQOwLuTKBOhhtANZMM,1094
111
- cloudnetpy-1.60.3.dist-info/METADATA,sha256=iMOBIyUZ82_uBThmxlSrdizjFks8-tsGx14amFGcahI,5784
112
- cloudnetpy-1.60.3.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
113
- cloudnetpy-1.60.3.dist-info/top_level.txt,sha256=ibSPWRr6ojS1i11rtBFz2_gkIe68mggj7aeswYfaOo0,16
114
- cloudnetpy-1.60.3.dist-info/RECORD,,
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,,