cloudnetpy 1.49.9__py3-none-any.whl → 1.87.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cloudnetpy/categorize/__init__.py +1 -2
- cloudnetpy/categorize/atmos_utils.py +297 -67
- cloudnetpy/categorize/attenuation.py +31 -0
- cloudnetpy/categorize/attenuations/__init__.py +37 -0
- cloudnetpy/categorize/attenuations/gas_attenuation.py +30 -0
- cloudnetpy/categorize/attenuations/liquid_attenuation.py +84 -0
- cloudnetpy/categorize/attenuations/melting_attenuation.py +78 -0
- cloudnetpy/categorize/attenuations/rain_attenuation.py +84 -0
- cloudnetpy/categorize/categorize.py +332 -156
- cloudnetpy/categorize/classify.py +127 -125
- cloudnetpy/categorize/containers.py +107 -76
- cloudnetpy/categorize/disdrometer.py +40 -0
- cloudnetpy/categorize/droplet.py +23 -21
- cloudnetpy/categorize/falling.py +53 -24
- cloudnetpy/categorize/freezing.py +25 -12
- cloudnetpy/categorize/insects.py +35 -23
- cloudnetpy/categorize/itu.py +243 -0
- cloudnetpy/categorize/lidar.py +36 -41
- cloudnetpy/categorize/melting.py +34 -26
- cloudnetpy/categorize/model.py +84 -37
- cloudnetpy/categorize/mwr.py +18 -14
- cloudnetpy/categorize/radar.py +215 -102
- cloudnetpy/cli.py +578 -0
- cloudnetpy/cloudnetarray.py +43 -89
- cloudnetpy/concat_lib.py +218 -78
- cloudnetpy/constants.py +28 -10
- cloudnetpy/datasource.py +61 -86
- cloudnetpy/exceptions.py +49 -20
- cloudnetpy/instruments/__init__.py +5 -0
- cloudnetpy/instruments/basta.py +29 -12
- cloudnetpy/instruments/bowtie.py +135 -0
- cloudnetpy/instruments/ceilo.py +138 -115
- cloudnetpy/instruments/ceilometer.py +164 -80
- cloudnetpy/instruments/cl61d.py +21 -5
- cloudnetpy/instruments/cloudnet_instrument.py +74 -36
- cloudnetpy/instruments/copernicus.py +108 -30
- cloudnetpy/instruments/da10.py +54 -0
- cloudnetpy/instruments/disdrometer/common.py +126 -223
- cloudnetpy/instruments/disdrometer/parsivel.py +453 -94
- cloudnetpy/instruments/disdrometer/thies.py +254 -87
- cloudnetpy/instruments/fd12p.py +201 -0
- cloudnetpy/instruments/galileo.py +65 -23
- cloudnetpy/instruments/hatpro.py +123 -49
- cloudnetpy/instruments/instruments.py +113 -1
- cloudnetpy/instruments/lufft.py +39 -17
- cloudnetpy/instruments/mira.py +268 -61
- cloudnetpy/instruments/mrr.py +187 -0
- cloudnetpy/instruments/nc_lidar.py +19 -8
- cloudnetpy/instruments/nc_radar.py +109 -55
- cloudnetpy/instruments/pollyxt.py +135 -51
- cloudnetpy/instruments/radiometrics.py +313 -59
- cloudnetpy/instruments/rain_e_h3.py +171 -0
- cloudnetpy/instruments/rpg.py +321 -189
- cloudnetpy/instruments/rpg_reader.py +74 -40
- cloudnetpy/instruments/toa5.py +49 -0
- cloudnetpy/instruments/vaisala.py +95 -343
- cloudnetpy/instruments/weather_station.py +774 -105
- cloudnetpy/metadata.py +90 -19
- cloudnetpy/model_evaluation/file_handler.py +55 -52
- cloudnetpy/model_evaluation/metadata.py +46 -20
- cloudnetpy/model_evaluation/model_metadata.py +1 -1
- cloudnetpy/model_evaluation/plotting/plot_tools.py +32 -37
- cloudnetpy/model_evaluation/plotting/plotting.py +327 -117
- cloudnetpy/model_evaluation/products/advance_methods.py +92 -83
- cloudnetpy/model_evaluation/products/grid_methods.py +88 -63
- cloudnetpy/model_evaluation/products/model_products.py +43 -35
- cloudnetpy/model_evaluation/products/observation_products.py +41 -35
- cloudnetpy/model_evaluation/products/product_resampling.py +17 -7
- cloudnetpy/model_evaluation/products/tools.py +29 -20
- cloudnetpy/model_evaluation/statistics/statistical_methods.py +30 -20
- cloudnetpy/model_evaluation/tests/e2e/conftest.py +3 -3
- cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +15 -14
- cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +15 -14
- cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +15 -14
- cloudnetpy/model_evaluation/tests/unit/conftest.py +42 -41
- cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +41 -48
- cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +216 -194
- cloudnetpy/model_evaluation/tests/unit/test_model_products.py +23 -21
- cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +37 -38
- cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +43 -40
- cloudnetpy/model_evaluation/tests/unit/test_plotting.py +30 -36
- cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +68 -31
- cloudnetpy/model_evaluation/tests/unit/test_tools.py +33 -26
- cloudnetpy/model_evaluation/utils.py +2 -1
- cloudnetpy/output.py +170 -111
- cloudnetpy/plotting/__init__.py +2 -1
- cloudnetpy/plotting/plot_meta.py +562 -822
- cloudnetpy/plotting/plotting.py +1142 -704
- cloudnetpy/products/__init__.py +1 -0
- cloudnetpy/products/classification.py +370 -88
- cloudnetpy/products/der.py +85 -55
- cloudnetpy/products/drizzle.py +77 -34
- cloudnetpy/products/drizzle_error.py +15 -11
- cloudnetpy/products/drizzle_tools.py +79 -59
- cloudnetpy/products/epsilon.py +211 -0
- cloudnetpy/products/ier.py +27 -50
- cloudnetpy/products/iwc.py +55 -48
- cloudnetpy/products/lwc.py +96 -70
- cloudnetpy/products/mwr_tools.py +186 -0
- cloudnetpy/products/product_tools.py +170 -128
- cloudnetpy/utils.py +455 -240
- cloudnetpy/version.py +2 -2
- {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/METADATA +44 -40
- cloudnetpy-1.87.3.dist-info/RECORD +127 -0
- {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/WHEEL +1 -1
- cloudnetpy-1.87.3.dist-info/entry_points.txt +2 -0
- docs/source/conf.py +2 -2
- cloudnetpy/categorize/atmos.py +0 -361
- cloudnetpy/products/mwr_multi.py +0 -68
- cloudnetpy/products/mwr_single.py +0 -75
- cloudnetpy-1.49.9.dist-info/RECORD +0 -112
- {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info/licenses}/LICENSE +0 -0
- {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/top_level.txt +0 -0
|
@@ -1,15 +1,22 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from os import PathLike
|
|
3
|
+
from pathlib import Path
|
|
1
4
|
from typing import BinaryIO, Literal
|
|
2
5
|
|
|
3
6
|
import numpy as np
|
|
7
|
+
import numpy.typing as npt
|
|
4
8
|
from numpy import ma
|
|
5
9
|
from numpy.lib import recfunctions as rfn
|
|
6
10
|
from rpgpy import read_rpg
|
|
7
11
|
|
|
12
|
+
from cloudnetpy.constants import G_TO_KG
|
|
13
|
+
from cloudnetpy.exceptions import ValidTimeStampError
|
|
14
|
+
|
|
8
15
|
|
|
9
16
|
class Fmcw94Bin:
|
|
10
17
|
"""RPG Cloud Radar Level 1 data reader."""
|
|
11
18
|
|
|
12
|
-
def __init__(self, filename):
|
|
19
|
+
def __init__(self, filename: str | PathLike) -> None:
|
|
13
20
|
self.filename = filename
|
|
14
21
|
self.header, self.data = read_rpg(filename)
|
|
15
22
|
|
|
@@ -31,7 +38,7 @@ class Fmcw94Bin:
|
|
|
31
38
|
"CGProg": "program_number",
|
|
32
39
|
"ModelNo": "model_number",
|
|
33
40
|
"ProgName": "_program_name",
|
|
34
|
-
"CustName": "
|
|
41
|
+
"CustName": "customer_name",
|
|
35
42
|
"Freq": "radar_frequency",
|
|
36
43
|
"AntSep": "antenna_separation",
|
|
37
44
|
"AntDia": "antenna_diameter",
|
|
@@ -67,8 +74,8 @@ class Fmcw94Bin:
|
|
|
67
74
|
"QF": "quality_flag",
|
|
68
75
|
"RR": "rainfall_rate",
|
|
69
76
|
"RelHum": "relative_humidity",
|
|
70
|
-
"EnvTemp": "
|
|
71
|
-
"BaroP": "
|
|
77
|
+
"EnvTemp": "air_temperature",
|
|
78
|
+
"BaroP": "air_pressure",
|
|
72
79
|
"WS": "wind_speed",
|
|
73
80
|
"WD": "wind_direction",
|
|
74
81
|
"DDVolt": "voltage",
|
|
@@ -97,7 +104,7 @@ class Fmcw94Bin:
|
|
|
97
104
|
self.replace_keys(self.data, data_keymap)
|
|
98
105
|
|
|
99
106
|
@staticmethod
|
|
100
|
-
def replace_keys(d: dict, keymap: dict):
|
|
107
|
+
def replace_keys(d: dict, keymap: dict) -> None:
|
|
101
108
|
for key in d.copy():
|
|
102
109
|
if key in keymap:
|
|
103
110
|
new_key = keymap[key]
|
|
@@ -105,20 +112,22 @@ class Fmcw94Bin:
|
|
|
105
112
|
|
|
106
113
|
|
|
107
114
|
def _read_from_file(
|
|
108
|
-
file: BinaryIO,
|
|
115
|
+
file: BinaryIO,
|
|
116
|
+
fields: list[tuple[str, str]],
|
|
117
|
+
count: int | None = None,
|
|
109
118
|
) -> ma.MaskedArray:
|
|
110
|
-
arr = np.fromfile(file, np.dtype(fields), count)
|
|
119
|
+
arr = np.fromfile(file, np.dtype(fields), 1 if count is None else count)
|
|
111
120
|
masked_arr = ma.array(arr)
|
|
112
|
-
if count
|
|
121
|
+
if count is None:
|
|
113
122
|
return masked_arr[0]
|
|
114
123
|
return masked_arr
|
|
115
124
|
|
|
116
125
|
|
|
117
126
|
def _decode_angles(
|
|
118
|
-
x:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
Decode elevation and azimuth angles.
|
|
127
|
+
x: npt.NDArray,
|
|
128
|
+
version: Literal[1, 2],
|
|
129
|
+
) -> tuple[npt.NDArray, npt.NDArray]:
|
|
130
|
+
"""Decode elevation and azimuth angles.
|
|
122
131
|
|
|
123
132
|
>>> _decode_angles(np.array([1267438.5]), version=1)
|
|
124
133
|
(array([138.5]), array([267.4]))
|
|
@@ -128,7 +137,6 @@ def _decode_angles(
|
|
|
128
137
|
Based on `interpret_angle` from mwr_raw2l1 licensed under BSD 3-Clause:
|
|
129
138
|
https://github.com/MeteoSwiss/mwr_raw2l1/blob/0738490d22f77138cdf9329bf102f319c78be584/mwr_raw2l1/readers/reader_rpg_helpers.py#L30
|
|
130
139
|
"""
|
|
131
|
-
|
|
132
140
|
if version == 1:
|
|
133
141
|
# Description in the manual is quite unclear so here's an improved one:
|
|
134
142
|
# Ang=sign(El)*(|El|+1000*Az), -90°<=El<100°, 0°<=Az<360°. If El>=100°
|
|
@@ -150,9 +158,8 @@ def _decode_angles(
|
|
|
150
158
|
ele = np.sign(x) * (np.abs(x) // 1e5) / 100
|
|
151
159
|
azi = (np.abs(x) - np.abs(ele) * 1e7) / 100
|
|
152
160
|
else:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
)
|
|
161
|
+
msg = f"Known versions for angle encoding are 1 and 2, but received {version}"
|
|
162
|
+
raise NotImplementedError(msg)
|
|
156
163
|
|
|
157
164
|
return ele, azi
|
|
158
165
|
|
|
@@ -177,7 +184,7 @@ class HatproBin:
|
|
|
177
184
|
QUALITY_MEDIUM = 2
|
|
178
185
|
QUALITY_LOW = 3
|
|
179
186
|
|
|
180
|
-
def __init__(self, filename):
|
|
187
|
+
def __init__(self, filename: Path) -> None:
|
|
181
188
|
self.filename = filename
|
|
182
189
|
with open(self.filename, "rb") as file:
|
|
183
190
|
self._read_header(file)
|
|
@@ -185,21 +192,39 @@ class HatproBin:
|
|
|
185
192
|
self._remove_duplicate_timestamps()
|
|
186
193
|
self._add_zenith_angle()
|
|
187
194
|
|
|
188
|
-
def screen_bad_profiles(self):
|
|
195
|
+
def screen_bad_profiles(self) -> None:
|
|
196
|
+
# In Limassol 2025-06-06 for instance, LWP is all zero but IWV has
|
|
197
|
+
# values.
|
|
198
|
+
if ma.all(self.data[self.variable] == 0):
|
|
199
|
+
self.data[self.variable][:] = ma.masked
|
|
200
|
+
return
|
|
201
|
+
# Screen by quality flag.
|
|
189
202
|
is_bad = self.data["_quality_flag"] & 0b110 == self.QUALITY_LOW << 1
|
|
203
|
+
n_bad = np.count_nonzero(is_bad)
|
|
204
|
+
if n_bad == len(is_bad):
|
|
205
|
+
msg = "All data are low quality"
|
|
206
|
+
raise ValidTimeStampError(msg)
|
|
207
|
+
if n_bad:
|
|
208
|
+
percentage = round(100 * n_bad / len(is_bad))
|
|
209
|
+
logging.info(
|
|
210
|
+
"Screening %s %% (%s/%s) data points with low quality",
|
|
211
|
+
percentage,
|
|
212
|
+
n_bad,
|
|
213
|
+
len(is_bad),
|
|
214
|
+
)
|
|
190
215
|
self.data[self.variable][is_bad] = ma.masked
|
|
191
216
|
|
|
192
|
-
def _remove_duplicate_timestamps(self):
|
|
217
|
+
def _remove_duplicate_timestamps(self) -> None:
|
|
193
218
|
_, ind = np.unique(self.data["time"], return_index=True)
|
|
194
219
|
self.data = self.data[ind]
|
|
195
220
|
|
|
196
|
-
def _read_header(self, file: BinaryIO):
|
|
197
|
-
raise NotImplementedError
|
|
221
|
+
def _read_header(self, file: BinaryIO) -> None:
|
|
222
|
+
raise NotImplementedError
|
|
198
223
|
|
|
199
|
-
def _read_data(self, file: BinaryIO):
|
|
200
|
-
raise NotImplementedError
|
|
224
|
+
def _read_data(self, file: BinaryIO) -> None:
|
|
225
|
+
raise NotImplementedError
|
|
201
226
|
|
|
202
|
-
def _add_zenith_angle(self):
|
|
227
|
+
def _add_zenith_angle(self) -> None:
|
|
203
228
|
ele, _azi = _decode_angles(self.data["_instrument_angles"], self.version)
|
|
204
229
|
self.data = rfn.append_fields(self.data, "zenith_angle", 90 - ele)
|
|
205
230
|
|
|
@@ -209,7 +234,7 @@ class HatproBinLwp(HatproBin):
|
|
|
209
234
|
|
|
210
235
|
variable = "lwp"
|
|
211
236
|
|
|
212
|
-
def _read_header(self, file):
|
|
237
|
+
def _read_header(self, file: BinaryIO) -> None:
|
|
213
238
|
self.header = _read_from_file(
|
|
214
239
|
file,
|
|
215
240
|
[
|
|
@@ -226,9 +251,10 @@ class HatproBinLwp(HatproBin):
|
|
|
226
251
|
elif self.header["file_code"] == 934501000:
|
|
227
252
|
self.version = 2
|
|
228
253
|
else:
|
|
229
|
-
|
|
254
|
+
msg = f"Unknown HATPRO version. {self.header['file_code']}"
|
|
255
|
+
raise ValueError(msg)
|
|
230
256
|
|
|
231
|
-
def _read_data(self, file):
|
|
257
|
+
def _read_data(self, file: BinaryIO) -> None:
|
|
232
258
|
self.data = _read_from_file(
|
|
233
259
|
file,
|
|
234
260
|
[
|
|
@@ -237,9 +263,9 @@ class HatproBinLwp(HatproBin):
|
|
|
237
263
|
("lwp", "<f"),
|
|
238
264
|
("_instrument_angles", "<f" if self.version == 1 else "<i4"),
|
|
239
265
|
],
|
|
240
|
-
self.header["_n_samples"],
|
|
266
|
+
int(self.header["_n_samples"]),
|
|
241
267
|
)
|
|
242
|
-
self.data["lwp"] *=
|
|
268
|
+
self.data["lwp"] *= G_TO_KG
|
|
243
269
|
|
|
244
270
|
|
|
245
271
|
class HatproBinIwv(HatproBin):
|
|
@@ -247,7 +273,7 @@ class HatproBinIwv(HatproBin):
|
|
|
247
273
|
|
|
248
274
|
variable = "iwv"
|
|
249
275
|
|
|
250
|
-
def _read_header(self, file):
|
|
276
|
+
def _read_header(self, file: BinaryIO) -> None:
|
|
251
277
|
self.header = _read_from_file(
|
|
252
278
|
file,
|
|
253
279
|
[
|
|
@@ -264,9 +290,10 @@ class HatproBinIwv(HatproBin):
|
|
|
264
290
|
elif self.header["file_code"] == 594811000:
|
|
265
291
|
self.version = 2
|
|
266
292
|
else:
|
|
267
|
-
|
|
293
|
+
msg = f"Unknown HATPRO version. {self.header['file_code']}"
|
|
294
|
+
raise ValueError(msg)
|
|
268
295
|
|
|
269
|
-
def _read_data(self, file):
|
|
296
|
+
def _read_data(self, file: BinaryIO) -> None:
|
|
270
297
|
self.data = _read_from_file(
|
|
271
298
|
file,
|
|
272
299
|
[
|
|
@@ -275,17 +302,17 @@ class HatproBinIwv(HatproBin):
|
|
|
275
302
|
("iwv", "<f"),
|
|
276
303
|
("_instrument_angles", "<f" if self.version == 1 else "<i4"),
|
|
277
304
|
],
|
|
278
|
-
self.header["_n_samples"],
|
|
305
|
+
int(self.header["_n_samples"]),
|
|
279
306
|
)
|
|
280
307
|
|
|
281
308
|
|
|
282
309
|
class HatproBinCombined:
|
|
283
310
|
"""Combine HATPRO objects that share values of the given dimensions."""
|
|
284
311
|
|
|
285
|
-
header: dict[str,
|
|
286
|
-
data: dict[str,
|
|
312
|
+
header: dict[str, npt.NDArray]
|
|
313
|
+
data: dict[str, npt.NDArray]
|
|
287
314
|
|
|
288
|
-
def __init__(self, files: list[HatproBin]):
|
|
315
|
+
def __init__(self, files: list[HatproBin]) -> None:
|
|
289
316
|
self.header = {}
|
|
290
317
|
if len(files) == 1:
|
|
291
318
|
arr = files[0].data
|
|
@@ -298,16 +325,23 @@ class HatproBinCombined:
|
|
|
298
325
|
_combine_values(arr["zenith_angle1"], arr["zenith_angle2"]),
|
|
299
326
|
)
|
|
300
327
|
# Workaround because rfn.drop_fields seems to incorrectly drop mask...
|
|
301
|
-
# arr = rfn.drop_fields(arr, ["zenith_angle1", "zenith_angle2"])
|
|
302
328
|
arr = rfn.rename_fields(
|
|
303
|
-
arr,
|
|
329
|
+
arr,
|
|
330
|
+
{"zenith_angle1": "_tmp1", "zenith_angle2": "_tmp2"},
|
|
304
331
|
)
|
|
305
332
|
else:
|
|
306
|
-
|
|
333
|
+
msg = "Only implemented up to 2 files"
|
|
334
|
+
raise NotImplementedError(msg)
|
|
335
|
+
|
|
336
|
+
if arr.dtype.fields is None:
|
|
337
|
+
msg = "Data has no fields"
|
|
338
|
+
raise ValueError(msg)
|
|
339
|
+
|
|
307
340
|
self.data = {field: arr[field] for field in arr.dtype.fields}
|
|
308
341
|
|
|
309
342
|
|
|
310
343
|
def _combine_values(arr1: ma.MaskedArray, arr2: ma.MaskedArray) -> ma.MaskedArray:
|
|
311
344
|
if not ma.allequal(arr1, arr2):
|
|
312
|
-
|
|
345
|
+
msg = "Inconsistent values"
|
|
346
|
+
raise ValueError(msg)
|
|
313
347
|
return ma.where(~arr1.mask, arr1, arr2)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import datetime
|
|
3
|
+
from os import PathLike
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def read_toa5(
|
|
8
|
+
filename: str | PathLike,
|
|
9
|
+
) -> tuple[dict[str, str], dict[str, str], list[dict[str, Any]]]:
|
|
10
|
+
"""Read ASCII data from Campbell Scientific datalogger such as CR1000.
|
|
11
|
+
|
|
12
|
+
References:
|
|
13
|
+
CR1000 Measurement and Control System.
|
|
14
|
+
https://s.campbellsci.com/documents/us/manuals/cr1000.pdf
|
|
15
|
+
"""
|
|
16
|
+
with open(filename) as file:
|
|
17
|
+
reader = csv.reader(file)
|
|
18
|
+
origin_line = next(reader)
|
|
19
|
+
if len(origin_line) == 0 or origin_line[0] != "TOA5":
|
|
20
|
+
msg = "Invalid TOA5 file"
|
|
21
|
+
raise ValueError(msg)
|
|
22
|
+
header_line = next(reader)
|
|
23
|
+
units_line = next(reader)
|
|
24
|
+
process_line = next(reader)
|
|
25
|
+
output = []
|
|
26
|
+
units = dict(zip(header_line, units_line, strict=False))
|
|
27
|
+
process = dict(zip(header_line, process_line, strict=False))
|
|
28
|
+
|
|
29
|
+
row_template: dict[str, Any] = {}
|
|
30
|
+
for header in header_line:
|
|
31
|
+
if "(" in header:
|
|
32
|
+
row_template[header[: header.index("(")]] = []
|
|
33
|
+
|
|
34
|
+
for data_line in reader:
|
|
35
|
+
row = row_template.copy()
|
|
36
|
+
for key, value in zip(header_line, data_line, strict=False):
|
|
37
|
+
parsed_value: Any = value
|
|
38
|
+
if key == "TIMESTAMP":
|
|
39
|
+
parsed_value = datetime.datetime.strptime(
|
|
40
|
+
parsed_value, "%Y-%m-%d %H:%M:%S"
|
|
41
|
+
)
|
|
42
|
+
elif key == "RECORD":
|
|
43
|
+
parsed_value = int(parsed_value)
|
|
44
|
+
if "(" in key:
|
|
45
|
+
row[key[: key.index("(")]].append(parsed_value)
|
|
46
|
+
else:
|
|
47
|
+
row[key] = parsed_value
|
|
48
|
+
output.append(row)
|
|
49
|
+
return units, process, output
|