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,6 +1,10 @@
|
|
|
1
1
|
"""Module for reading raw Galileo cloud radar data."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
2
4
|
import os
|
|
3
|
-
from
|
|
5
|
+
from os import PathLike
|
|
6
|
+
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
|
7
|
+
from uuid import UUID
|
|
4
8
|
|
|
5
9
|
import numpy as np
|
|
6
10
|
|
|
@@ -11,12 +15,12 @@ from cloudnetpy.metadata import MetaData
|
|
|
11
15
|
|
|
12
16
|
|
|
13
17
|
def galileo2nc(
|
|
14
|
-
raw_files: str,
|
|
15
|
-
output_file: str,
|
|
18
|
+
raw_files: str | PathLike,
|
|
19
|
+
output_file: str | PathLike,
|
|
16
20
|
site_meta: dict,
|
|
17
|
-
uuid: str | None = None,
|
|
18
|
-
date: str | None = None,
|
|
19
|
-
) ->
|
|
21
|
+
uuid: str | UUID | None = None,
|
|
22
|
+
date: str | datetime.date | None = None,
|
|
23
|
+
) -> UUID:
|
|
20
24
|
"""Converts 'Galileo' cloud radar data into Cloudnet Level 1b netCDF file.
|
|
21
25
|
|
|
22
26
|
Args:
|
|
@@ -41,6 +45,10 @@ def galileo2nc(
|
|
|
41
45
|
>>> galileo2nc('/one/day/of/galileo/files/', 'radar.nc', site_meta)
|
|
42
46
|
|
|
43
47
|
"""
|
|
48
|
+
if isinstance(date, str):
|
|
49
|
+
date = datetime.date.fromisoformat(date)
|
|
50
|
+
uuid = utils.get_uuid(uuid)
|
|
51
|
+
|
|
44
52
|
keymap = {
|
|
45
53
|
"ZED_HC": "Zh",
|
|
46
54
|
"VEL_HC": "v",
|
|
@@ -55,15 +63,25 @@ def galileo2nc(
|
|
|
55
63
|
"beamwidthH": "beamwidthH",
|
|
56
64
|
}
|
|
57
65
|
|
|
66
|
+
nc_filename: str | PathLike
|
|
58
67
|
with TemporaryDirectory() as temp_dir:
|
|
59
68
|
if os.path.isdir(raw_files):
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
with NamedTemporaryFile(
|
|
70
|
+
dir=temp_dir,
|
|
71
|
+
suffix=".nc",
|
|
72
|
+
delete=False,
|
|
73
|
+
) as temp_file:
|
|
74
|
+
nc_filename = temp_file.name
|
|
75
|
+
valid_filenames = utils.get_sorted_filenames(raw_files, ".nc")
|
|
76
|
+
valid_filenames = utils.get_files_with_variables(
|
|
77
|
+
valid_filenames, ["time", "ZED_HC"]
|
|
78
|
+
)
|
|
79
|
+
variables = list(keymap.keys())
|
|
80
|
+
concat_lib.concatenate_files(
|
|
81
|
+
valid_filenames,
|
|
82
|
+
nc_filename,
|
|
83
|
+
variables=variables,
|
|
84
|
+
)
|
|
67
85
|
else:
|
|
68
86
|
nc_filename = raw_files
|
|
69
87
|
|
|
@@ -74,20 +92,25 @@ def galileo2nc(
|
|
|
74
92
|
galileo.check_date(date)
|
|
75
93
|
galileo.sort_timestamps()
|
|
76
94
|
galileo.remove_duplicate_timestamps()
|
|
95
|
+
galileo.screen_negative_altitudes()
|
|
77
96
|
snr_limit = site_meta.get("snr_limit", 3)
|
|
78
97
|
galileo.screen_by_snr(snr_limit=snr_limit)
|
|
79
98
|
galileo.mask_clutter()
|
|
80
99
|
galileo.mask_invalid_data()
|
|
81
|
-
galileo.add_time_and_range()
|
|
82
100
|
galileo.add_radar_specific_variables()
|
|
83
101
|
galileo.add_nyquist_velocity(keymap)
|
|
84
102
|
galileo.add_site_geolocation()
|
|
85
|
-
valid_indices = galileo.add_zenith_and_azimuth_angles(
|
|
103
|
+
valid_indices = galileo.add_zenith_and_azimuth_angles(
|
|
104
|
+
elevation_threshold=1.1,
|
|
105
|
+
elevation_diff_threshold=0.1,
|
|
106
|
+
azimuth_diff_threshold=0.1,
|
|
107
|
+
)
|
|
86
108
|
galileo.screen_time_indices(valid_indices)
|
|
87
109
|
galileo.add_height()
|
|
110
|
+
galileo.test_if_all_masked()
|
|
88
111
|
attributes = output.add_time_attribute(ATTRIBUTES, galileo.date)
|
|
89
112
|
output.update_attributes(galileo.data, attributes)
|
|
90
|
-
|
|
113
|
+
output.save_level1b(galileo, output_file, uuid)
|
|
91
114
|
return uuid
|
|
92
115
|
|
|
93
116
|
|
|
@@ -100,23 +123,42 @@ class Galileo(ChilboltonRadar):
|
|
|
100
123
|
|
|
101
124
|
"""
|
|
102
125
|
|
|
103
|
-
def __init__(self, full_path: str, site_meta: dict):
|
|
126
|
+
def __init__(self, full_path: str | PathLike, site_meta: dict) -> None:
|
|
104
127
|
super().__init__(full_path, site_meta)
|
|
105
128
|
self.date = self._init_date()
|
|
106
129
|
self.instrument = GALILEO
|
|
107
130
|
|
|
108
|
-
def mask_clutter(self):
|
|
109
|
-
"""Masks clutter."""
|
|
131
|
+
def mask_clutter(self) -> None:
|
|
110
132
|
# Only strong Z values are valid
|
|
111
133
|
n_low_gates = 15
|
|
112
134
|
ind = np.where(self.data["Zh"][:, :n_low_gates] < -15) and np.where(
|
|
113
|
-
self.data["ldr"][:, :n_low_gates] > -5
|
|
135
|
+
self.data["ldr"][:, :n_low_gates] > -5,
|
|
114
136
|
)
|
|
115
137
|
self.data["v"].mask_indices(ind)
|
|
116
138
|
|
|
139
|
+
def screen_negative_altitudes(self) -> None:
|
|
140
|
+
range_var = self.data["range"][:]
|
|
141
|
+
valid_idx = np.where(range_var > 0)[0]
|
|
142
|
+
if valid_idx.size == 0:
|
|
143
|
+
msg = "No valid altitudes found."
|
|
144
|
+
raise ValueError(msg)
|
|
145
|
+
if valid_idx.size == range_var.shape[0]:
|
|
146
|
+
return
|
|
147
|
+
for arr in self.data.values():
|
|
148
|
+
if utils.isscalar(arr.data):
|
|
149
|
+
continue
|
|
150
|
+
if arr[:].shape[-1] == range_var.shape[0]:
|
|
151
|
+
arr.data = arr[..., valid_idx]
|
|
152
|
+
|
|
117
153
|
|
|
118
154
|
ATTRIBUTES = {
|
|
119
|
-
"antenna_diameter": MetaData(
|
|
120
|
-
|
|
121
|
-
|
|
155
|
+
"antenna_diameter": MetaData(
|
|
156
|
+
long_name="Antenna diameter", units="m", dimensions=("time",)
|
|
157
|
+
),
|
|
158
|
+
"beamwidthV": MetaData(
|
|
159
|
+
long_name="Vertical angular beamwidth", units="degree", dimensions=("time",)
|
|
160
|
+
),
|
|
161
|
+
"beamwidthH": MetaData(
|
|
162
|
+
long_name="Horizontal angular beamwidth", units="degree", dimensions=("time",)
|
|
163
|
+
),
|
|
122
164
|
}
|
cloudnetpy/instruments/hatpro.py
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
"""This module contains RPG Cloud Radar related functions."""
|
|
2
|
+
|
|
2
3
|
import datetime
|
|
3
4
|
import logging
|
|
4
5
|
from collections import defaultdict
|
|
6
|
+
from os import PathLike
|
|
5
7
|
from pathlib import Path
|
|
8
|
+
from typing import Literal
|
|
9
|
+
from uuid import UUID
|
|
6
10
|
|
|
7
|
-
import mwrpy
|
|
11
|
+
import mwrpy.rpg_mwr
|
|
8
12
|
import netCDF4
|
|
9
13
|
import numpy as np
|
|
14
|
+
from mwrpy.exceptions import MissingInputData
|
|
10
15
|
from mwrpy.level1.lev1_meta_nc import ATTRIBUTES_1B01
|
|
16
|
+
from mwrpy.level1.write_lev1_nc import lev1_to_nc
|
|
11
17
|
from mwrpy.version import __version__ as mwrpy_version
|
|
12
18
|
from numpy import ma
|
|
13
19
|
|
|
14
20
|
from cloudnetpy import output, utils
|
|
15
21
|
from cloudnetpy.cloudnetarray import CloudnetArray
|
|
16
|
-
from cloudnetpy.exceptions import ValidTimeStampError
|
|
22
|
+
from cloudnetpy.exceptions import HatproDataError, ValidTimeStampError
|
|
17
23
|
from cloudnetpy.instruments import rpg
|
|
18
|
-
from cloudnetpy.instruments.instruments import HATPRO
|
|
24
|
+
from cloudnetpy.instruments.instruments import HATPRO, LHATPRO, LHUMPRO_U90, Instrument
|
|
19
25
|
from cloudnetpy.instruments.rpg_reader import (
|
|
20
26
|
HatproBin,
|
|
21
27
|
HatproBinCombined,
|
|
@@ -23,36 +29,83 @@ from cloudnetpy.instruments.rpg_reader import (
|
|
|
23
29
|
HatproBinLwp,
|
|
24
30
|
)
|
|
25
31
|
|
|
32
|
+
IType = Literal["hatpro", "lhatpro", "lhumpro_u90"]
|
|
33
|
+
ITYPE_MAP: dict[IType, Instrument] = {
|
|
34
|
+
"hatpro": HATPRO,
|
|
35
|
+
"lhatpro": LHATPRO,
|
|
36
|
+
"lhumpro_u90": LHUMPRO_U90,
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
|
|
27
40
|
def hatpro2l1c(
|
|
28
|
-
mwr_dir: str,
|
|
29
|
-
output_file: str,
|
|
41
|
+
mwr_dir: str | PathLike,
|
|
42
|
+
output_file: str | PathLike,
|
|
30
43
|
site_meta: dict,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
instrument_type: IType = "hatpro",
|
|
45
|
+
lidar_file: str | PathLike | None = None,
|
|
46
|
+
uuid: str | UUID | None = None,
|
|
47
|
+
date: str | datetime.date | None = None,
|
|
48
|
+
) -> UUID:
|
|
34
49
|
"""Converts RPG HATPRO microwave radiometer data into Cloudnet Level 1c netCDF file.
|
|
35
50
|
|
|
36
51
|
Args:
|
|
37
52
|
mwr_dir: Folder containing one day of HATPRO files.
|
|
38
53
|
output_file: Output file name.
|
|
39
|
-
site_meta: Dictionary containing information about the site
|
|
54
|
+
site_meta: Dictionary containing information about the site and instrument
|
|
55
|
+
instrument_type: Specific type of the RPG microwave radiometer.
|
|
56
|
+
lidar_file: Path to a lidar file.
|
|
40
57
|
uuid: Set specific UUID for the file.
|
|
41
58
|
date: Expected date in the input files.
|
|
42
59
|
|
|
43
60
|
Returns:
|
|
44
61
|
UUID of the generated file.
|
|
45
62
|
"""
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
63
|
+
if isinstance(date, str):
|
|
64
|
+
date = datetime.date.fromisoformat(date)
|
|
65
|
+
uuid = utils.get_uuid(uuid)
|
|
66
|
+
|
|
67
|
+
coeff_files = site_meta.get("coefficientFiles")
|
|
68
|
+
time_offset = site_meta.get("time_offset")
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
hatpro_raw = lev1_to_nc(
|
|
72
|
+
"1C01",
|
|
73
|
+
str(mwr_dir),
|
|
74
|
+
instrument_type=instrument_type,
|
|
75
|
+
output_file=str(output_file),
|
|
76
|
+
lidar_path=lidar_file,
|
|
77
|
+
coeff_files=coeff_files,
|
|
78
|
+
instrument_config=site_meta,
|
|
79
|
+
date=date,
|
|
80
|
+
time_offset=time_offset,
|
|
81
|
+
)
|
|
82
|
+
except MissingInputData as err:
|
|
83
|
+
raise HatproDataError(str(err)) from err
|
|
84
|
+
|
|
85
|
+
hatpro = HatproL1c(hatpro_raw, site_meta, ITYPE_MAP[instrument_type])
|
|
86
|
+
|
|
87
|
+
flags = hatpro.data["quality_flag"][:]
|
|
88
|
+
bad_percentage = ma.sum(flags != 0) / flags.size * 100
|
|
89
|
+
if bad_percentage > 90:
|
|
90
|
+
msg = "More than 90% of brightness temperatures are flagged"
|
|
91
|
+
raise HatproDataError(msg)
|
|
49
92
|
|
|
50
93
|
timestamps = hatpro.data["time"][:]
|
|
51
94
|
if date is not None:
|
|
52
95
|
# Screen timestamps if these assertions start to fail
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
96
|
+
if not np.all(np.diff(timestamps) > 0):
|
|
97
|
+
msg = "Timestamps are not increasing"
|
|
98
|
+
raise RuntimeError(msg)
|
|
99
|
+
dates = [
|
|
100
|
+
datetime.datetime.fromtimestamp(t, tz=datetime.timezone.utc).date()
|
|
101
|
+
for t in timestamps
|
|
102
|
+
]
|
|
103
|
+
if len(set(dates)) != 1:
|
|
104
|
+
msg = f"Several dates, something is wrong: {set(dates)}"
|
|
105
|
+
raise RuntimeError(msg)
|
|
106
|
+
if date != dates[0]:
|
|
107
|
+
msg = f"Expected date {date}, got {dates[0]}"
|
|
108
|
+
raise RuntimeError(msg)
|
|
56
109
|
|
|
57
110
|
decimal_hours = utils.seconds2hours(timestamps)
|
|
58
111
|
hatpro.data["time"] = CloudnetArray(decimal_hours, "time", data_type="f8")
|
|
@@ -60,42 +113,55 @@ def hatpro2l1c(
|
|
|
60
113
|
hatpro.data["t_amb"].dimensions = ("time", "t_amb_nb")
|
|
61
114
|
|
|
62
115
|
for key in ("elevation_angle", "ir_elevation_angle"):
|
|
63
|
-
|
|
116
|
+
if key not in hatpro.data:
|
|
117
|
+
continue
|
|
118
|
+
zenith_angle = 90 - hatpro.data[key][:]
|
|
64
119
|
new_key = key.replace("elevation", "zenith")
|
|
65
120
|
hatpro.data[new_key] = CloudnetArray(zenith_angle, new_key)
|
|
66
121
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
122
|
+
if "ir_wavelength" in hatpro.data:
|
|
123
|
+
hatpro.data["ir_wavelength"].dimensions = ("ir_channel",)
|
|
124
|
+
if "irt" in hatpro.data:
|
|
125
|
+
hatpro.data["irt"].dimensions = ("time", "ir_channel")
|
|
126
|
+
|
|
127
|
+
utils.add_site_geolocation(hatpro.data, gps=True, site_meta=site_meta)
|
|
70
128
|
|
|
71
|
-
|
|
129
|
+
attrs_copy = ATTRIBUTES_1B01.copy()
|
|
130
|
+
attributes = output.add_time_attribute(attrs_copy, hatpro.date)
|
|
72
131
|
output.update_attributes(hatpro.data, attributes)
|
|
73
|
-
|
|
132
|
+
output.save_level1b(hatpro, output_file, uuid)
|
|
74
133
|
with netCDF4.Dataset(output_file, "a") as nc:
|
|
75
134
|
nc.cloudnet_file_type = "mwr-l1c"
|
|
76
135
|
nc.title = nc.title.replace("radiometer", "radiometer Level 1c")
|
|
77
136
|
nc.mwrpy_version = mwrpy_version
|
|
78
|
-
nc.mwrpy_coefficients = site_meta["
|
|
79
|
-
|
|
137
|
+
nc.mwrpy_coefficients = ", ".join(site_meta["coefficientLinks"])
|
|
138
|
+
nc.history = nc.history.replace("mwr", "mwr-l1c")
|
|
139
|
+
if lidar_file is not None:
|
|
140
|
+
with netCDF4.Dataset(lidar_file) as lidar_nc:
|
|
141
|
+
nc.source = f"{nc.source}\n{lidar_nc.source}"
|
|
142
|
+
nc.history = f"{nc.history}\n{lidar_nc.history}"
|
|
80
143
|
return uuid
|
|
81
144
|
|
|
82
145
|
|
|
83
146
|
class HatproL1c:
|
|
84
|
-
def __init__(
|
|
147
|
+
def __init__(
|
|
148
|
+
self, hatpro: mwrpy.rpg_mwr.Rpg, site_meta: dict, instrument: Instrument
|
|
149
|
+
) -> None:
|
|
85
150
|
self.raw_data = hatpro.raw_data
|
|
86
151
|
self.data = hatpro.data
|
|
87
|
-
self.date = hatpro.date
|
|
152
|
+
self.date = hatpro.date
|
|
88
153
|
self.site_meta = site_meta
|
|
89
|
-
self.instrument =
|
|
154
|
+
self.instrument = instrument
|
|
90
155
|
|
|
91
156
|
|
|
92
157
|
def hatpro2nc(
|
|
93
|
-
path_to_files: str,
|
|
94
|
-
output_file: str,
|
|
158
|
+
path_to_files: str | PathLike,
|
|
159
|
+
output_file: str | PathLike,
|
|
95
160
|
site_meta: dict,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
161
|
+
instrument_type: IType = "hatpro",
|
|
162
|
+
uuid: str | UUID | None = None,
|
|
163
|
+
date: str | datetime.date | None = None,
|
|
164
|
+
) -> tuple[UUID, list[Path]]:
|
|
99
165
|
"""Converts RPG HATPRO microwave radiometer data into Cloudnet Level 1b
|
|
100
166
|
netCDF file.
|
|
101
167
|
|
|
@@ -112,6 +178,7 @@ def hatpro2nc(
|
|
|
112
178
|
- `latitude` (optional).
|
|
113
179
|
- `longitude` (optional).
|
|
114
180
|
|
|
181
|
+
instrument_type: Specific type of the RPG microwave radiometer.
|
|
115
182
|
uuid: Set specific UUID for the file.
|
|
116
183
|
date: Expected date in the input files. If not set,
|
|
117
184
|
all files will be used. This might cause unexpected behavior if
|
|
@@ -133,28 +200,32 @@ def hatpro2nc(
|
|
|
133
200
|
>>> hatpro2nc('/path/to/files/', 'hatpro.nc', site_meta)
|
|
134
201
|
|
|
135
202
|
"""
|
|
203
|
+
if isinstance(date, str):
|
|
204
|
+
date = datetime.date.fromisoformat(date)
|
|
205
|
+
uuid = utils.get_uuid(uuid)
|
|
136
206
|
hatpro_objects, valid_files = _get_hatpro_objects(Path(path_to_files), date)
|
|
137
|
-
is_lwp_files = any(f.
|
|
138
|
-
is_iwv_files = any(f.
|
|
207
|
+
is_lwp_files = any(f.suffix == ".LWP" for f in valid_files)
|
|
208
|
+
is_iwv_files = any(f.suffix == ".IWV" for f in valid_files)
|
|
139
209
|
if not is_lwp_files:
|
|
140
210
|
raise ValidTimeStampError
|
|
141
211
|
if is_iwv_files:
|
|
142
212
|
_add_missing_variables(hatpro_objects, ("lwp", "iwv"))
|
|
143
213
|
one_day_of_data = rpg.create_one_day_data_record(hatpro_objects)
|
|
144
|
-
hatpro = rpg.Hatpro(one_day_of_data, site_meta)
|
|
145
|
-
hatpro.sort_timestamps()
|
|
146
|
-
hatpro.convert_time_to_fraction_hour("float64")
|
|
214
|
+
hatpro = rpg.Hatpro(one_day_of_data, site_meta, ITYPE_MAP[instrument_type])
|
|
147
215
|
hatpro.add_site_geolocation()
|
|
216
|
+
hatpro.convert_time_to_fraction_hour("float64")
|
|
217
|
+
hatpro.sort_timestamps()
|
|
148
218
|
hatpro.remove_duplicate_timestamps()
|
|
149
219
|
attributes = output.add_time_attribute({}, hatpro.date)
|
|
150
220
|
output.update_attributes(hatpro.data, attributes)
|
|
151
|
-
|
|
221
|
+
output.save_level1b(hatpro, output_file, uuid)
|
|
152
222
|
return uuid, valid_files
|
|
153
223
|
|
|
154
224
|
|
|
155
225
|
def _get_hatpro_objects(
|
|
156
|
-
directory: Path,
|
|
157
|
-
|
|
226
|
+
directory: Path,
|
|
227
|
+
expected_date: datetime.date | None,
|
|
228
|
+
) -> tuple[list[HatproBinCombined], list[Path]]:
|
|
158
229
|
objects = defaultdict(list)
|
|
159
230
|
for filename in directory.iterdir():
|
|
160
231
|
try:
|
|
@@ -170,40 +241,43 @@ def _get_hatpro_objects(
|
|
|
170
241
|
if expected_date is not None:
|
|
171
242
|
obj = _validate_date(obj, expected_date)
|
|
172
243
|
objects[filename.stem].append(obj)
|
|
173
|
-
except (TypeError, ValueError) as err:
|
|
174
|
-
logging.warning(
|
|
244
|
+
except (TypeError, ValueError, ValidTimeStampError) as err:
|
|
245
|
+
logging.warning("Ignoring file '%s': %s", filename, err)
|
|
175
246
|
continue
|
|
176
247
|
|
|
177
|
-
valid_files: list[
|
|
248
|
+
valid_files: list[Path] = []
|
|
178
249
|
combined_objs = []
|
|
179
250
|
for _stem, objs in sorted(objects.items()):
|
|
180
251
|
try:
|
|
181
252
|
combined_objs.append(HatproBinCombined(objs))
|
|
182
|
-
valid_files.extend(
|
|
253
|
+
valid_files.extend(obj.filename for obj in objs)
|
|
183
254
|
except (TypeError, ValueError) as err:
|
|
184
255
|
files = "'" + "', '".join(str(obj.filename) for obj in objs) + "'"
|
|
185
|
-
logging.warning(
|
|
256
|
+
logging.warning("Ignoring files %s: %s", files, err)
|
|
186
257
|
continue
|
|
187
258
|
|
|
188
259
|
return combined_objs, valid_files
|
|
189
260
|
|
|
190
261
|
|
|
191
|
-
def _validate_date(obj: HatproBin, expected_date:
|
|
262
|
+
def _validate_date(obj: HatproBin, expected_date: datetime.date) -> HatproBin:
|
|
192
263
|
if obj.header["_time_reference"] != 1:
|
|
193
|
-
|
|
264
|
+
msg = "Can not validate non-UTC dates"
|
|
265
|
+
raise ValueError(msg)
|
|
194
266
|
inds = []
|
|
195
267
|
for ind, timestamp in enumerate(obj.data["time"][:]):
|
|
196
|
-
date =
|
|
268
|
+
date = utils.seconds2date(timestamp).date()
|
|
197
269
|
if date == expected_date:
|
|
198
270
|
inds.append(ind)
|
|
199
271
|
if not inds:
|
|
200
|
-
|
|
272
|
+
msg = f"No valid timestamps found for date {expected_date}"
|
|
273
|
+
raise ValueError(msg)
|
|
201
274
|
obj.data = obj.data[:][inds]
|
|
202
275
|
return obj
|
|
203
276
|
|
|
204
277
|
|
|
205
278
|
def _add_missing_variables(
|
|
206
|
-
hatpro_objects: list[HatproBinCombined],
|
|
279
|
+
hatpro_objects: list[HatproBinCombined],
|
|
280
|
+
keys: tuple,
|
|
207
281
|
) -> list[HatproBinCombined]:
|
|
208
282
|
for obj in hatpro_objects:
|
|
209
283
|
for key in keys:
|
|
@@ -35,6 +35,14 @@ CL31 = Instrument(
|
|
|
35
35
|
wavelength=910.0,
|
|
36
36
|
)
|
|
37
37
|
|
|
38
|
+
CS135 = Instrument(
|
|
39
|
+
manufacturer="Campbell Scientific",
|
|
40
|
+
domain="lidar",
|
|
41
|
+
category="ceilometer",
|
|
42
|
+
model="CS135",
|
|
43
|
+
wavelength=905.0,
|
|
44
|
+
)
|
|
45
|
+
|
|
38
46
|
CT25K = Instrument(
|
|
39
47
|
manufacturer="Vaisala",
|
|
40
48
|
domain="lidar",
|
|
@@ -67,6 +75,14 @@ CHM15KX = Instrument(
|
|
|
67
75
|
wavelength=1064.0,
|
|
68
76
|
)
|
|
69
77
|
|
|
78
|
+
MIRA10 = Instrument(
|
|
79
|
+
manufacturer="METEK",
|
|
80
|
+
domain="radar",
|
|
81
|
+
category="cloud radar",
|
|
82
|
+
model="MIRA-10",
|
|
83
|
+
frequency=9.4, # 9.2 - 9.6 GHz
|
|
84
|
+
)
|
|
85
|
+
|
|
70
86
|
MIRA35 = Instrument(
|
|
71
87
|
manufacturer="METEK",
|
|
72
88
|
domain="radar",
|
|
@@ -108,7 +124,10 @@ FMCW35 = Instrument(
|
|
|
108
124
|
)
|
|
109
125
|
|
|
110
126
|
BASTA = Instrument(
|
|
111
|
-
domain="radar",
|
|
127
|
+
domain="radar",
|
|
128
|
+
category="cloud radar",
|
|
129
|
+
model="BASTA",
|
|
130
|
+
frequency=95.0,
|
|
112
131
|
)
|
|
113
132
|
|
|
114
133
|
HATPRO = Instrument(
|
|
@@ -118,6 +137,20 @@ HATPRO = Instrument(
|
|
|
118
137
|
model="HATPRO",
|
|
119
138
|
)
|
|
120
139
|
|
|
140
|
+
LHATPRO = Instrument(
|
|
141
|
+
manufacturer="RPG-Radiometer Physics",
|
|
142
|
+
domain="mwr",
|
|
143
|
+
category="microwave radiometer",
|
|
144
|
+
model="LHATPRO",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
LHUMPRO_U90 = Instrument(
|
|
148
|
+
manufacturer="RPG-Radiometer Physics",
|
|
149
|
+
domain="mwr",
|
|
150
|
+
category="microwave radiometer",
|
|
151
|
+
model="LHUMPRO-U90",
|
|
152
|
+
)
|
|
153
|
+
|
|
121
154
|
RADIOMETRICS = Instrument(
|
|
122
155
|
manufacturer="Radiometrics",
|
|
123
156
|
domain="mwr",
|
|
@@ -131,6 +164,34 @@ HALO = Instrument(
|
|
|
131
164
|
model="StreamLine",
|
|
132
165
|
)
|
|
133
166
|
|
|
167
|
+
WINDCUBE_WLS100S = Instrument(
|
|
168
|
+
manufacturer="Vaisala",
|
|
169
|
+
domain="lidar",
|
|
170
|
+
category="Doppler lidar",
|
|
171
|
+
model="WindCube WLS100S",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
WINDCUBE_WLS200S = Instrument(
|
|
175
|
+
manufacturer="Vaisala",
|
|
176
|
+
domain="lidar",
|
|
177
|
+
category="Doppler lidar",
|
|
178
|
+
model="WindCube WLS200S",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
WINDCUBE_WLS400S = Instrument(
|
|
182
|
+
manufacturer="Vaisala",
|
|
183
|
+
domain="lidar",
|
|
184
|
+
category="Doppler lidar",
|
|
185
|
+
model="WindCube WLS400S",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
WINDCUBE_WLS70 = Instrument(
|
|
189
|
+
manufacturer="Vaisala",
|
|
190
|
+
domain="lidar",
|
|
191
|
+
category="Doppler lidar",
|
|
192
|
+
model="WindCube WLS70",
|
|
193
|
+
)
|
|
194
|
+
|
|
134
195
|
PARSIVEL2 = Instrument(
|
|
135
196
|
manufacturer="OTT HydroMet",
|
|
136
197
|
domain="disdrometer",
|
|
@@ -145,7 +206,58 @@ THIES = Instrument(
|
|
|
145
206
|
model="LNM",
|
|
146
207
|
)
|
|
147
208
|
|
|
209
|
+
PLUVIO2 = Instrument(
|
|
210
|
+
manufacturer="OTT HydroMet",
|
|
211
|
+
domain="rain-gauge",
|
|
212
|
+
category="rain-gauge",
|
|
213
|
+
model="Pluvio2",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
PLUVIO2S = Instrument(
|
|
217
|
+
manufacturer="OTT HydroMet",
|
|
218
|
+
domain="rain-gauge",
|
|
219
|
+
category="rain-gauge",
|
|
220
|
+
model="Pluvio2S",
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
THIES_PT = Instrument(
|
|
224
|
+
manufacturer="Thies Clima",
|
|
225
|
+
domain="rain-gauge",
|
|
226
|
+
category="rain-gauge",
|
|
227
|
+
model="Precipitation Transmitter",
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
RAIN_E_H3 = Instrument(
|
|
231
|
+
manufacturer="LAMBRECHT meteo GmbH",
|
|
232
|
+
domain="rain-gauge",
|
|
233
|
+
category="rain-gauge",
|
|
234
|
+
model="rain[e]H3",
|
|
235
|
+
)
|
|
236
|
+
|
|
148
237
|
GENERIC_WEATHER_STATION = Instrument(
|
|
149
238
|
domain="weather-station",
|
|
150
239
|
category="weather station",
|
|
151
240
|
)
|
|
241
|
+
|
|
242
|
+
MRR_PRO = Instrument(
|
|
243
|
+
manufacturer="METEK",
|
|
244
|
+
domain="rain-radar",
|
|
245
|
+
category="rain radar",
|
|
246
|
+
model="MRR-PRO",
|
|
247
|
+
frequency=24.23,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
FD12P = Instrument(
|
|
251
|
+
manufacturer="Vaisala",
|
|
252
|
+
domain="weather-station",
|
|
253
|
+
category="present weather sensor",
|
|
254
|
+
model="FD12P",
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
DA10 = Instrument(
|
|
258
|
+
manufacturer="Vaisala",
|
|
259
|
+
domain="lidar",
|
|
260
|
+
category="differential absorption lidar",
|
|
261
|
+
model="DA10",
|
|
262
|
+
wavelength=911.0,
|
|
263
|
+
)
|