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
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from os import PathLike
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
import netCDF4
|
|
11
|
+
|
|
12
|
+
from cloudnetpy import concat_lib, output, utils
|
|
13
|
+
from cloudnetpy.instruments import instruments
|
|
14
|
+
from cloudnetpy.instruments.nc_radar import NcRadar
|
|
15
|
+
from cloudnetpy.metadata import MetaData
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def mrr2nc(
|
|
19
|
+
input_file: PathLike | str | Iterable[PathLike | str],
|
|
20
|
+
output_file: PathLike | str,
|
|
21
|
+
site_meta: dict,
|
|
22
|
+
uuid: str | UUID | None = None,
|
|
23
|
+
date: str | datetime.date | None = None,
|
|
24
|
+
) -> UUID:
|
|
25
|
+
"""Converts METEK MRR-PRO data into Cloudnet Level 1b netCDF file.
|
|
26
|
+
|
|
27
|
+
This function converts raw MRR file(s) into a much smaller file that
|
|
28
|
+
contains only the relevant data.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
input_file: Filename of a daily MMR-PRO .nc file, path to directory
|
|
32
|
+
containing several non-concatenated .nc files from one day, or list
|
|
33
|
+
of filenames.
|
|
34
|
+
output_file: Output filename.
|
|
35
|
+
site_meta: Dictionary containing information about the site. Required key
|
|
36
|
+
value pairs are `name`, `latitude`, `longitude` and `altitude`.
|
|
37
|
+
uuid: Set specific UUID for the file.
|
|
38
|
+
date: Expected date as YYYY-MM-DD of all profiles in the file.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
UUID of the generated file.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ValidTimeStampError: No valid timestamps found.
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
>>> from cloudnetpy.instruments import mira2nc
|
|
48
|
+
>>> site_meta = {'name': 'LIM', 'latitude': 51.333, 'longitude': 12.389}
|
|
49
|
+
>>> mrr2nc('input.nc', 'output.nc', site_meta)
|
|
50
|
+
"""
|
|
51
|
+
if isinstance(date, str):
|
|
52
|
+
date = datetime.date.fromisoformat(date)
|
|
53
|
+
uuid = utils.get_uuid(uuid)
|
|
54
|
+
|
|
55
|
+
keymap = {
|
|
56
|
+
"RR": "rainfall_rate",
|
|
57
|
+
"WIDTH": "width",
|
|
58
|
+
"VEL": "v",
|
|
59
|
+
"LWC": "lwc",
|
|
60
|
+
"Ze": "Zh",
|
|
61
|
+
"PIA": "pia",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
def valid_nc_files(files: Iterable[PathLike | str]) -> Iterable[PathLike | str]:
|
|
65
|
+
for file in files:
|
|
66
|
+
try:
|
|
67
|
+
with netCDF4.Dataset(file):
|
|
68
|
+
yield file
|
|
69
|
+
except OSError:
|
|
70
|
+
logging.warning("Skipping invalid file: %s", file)
|
|
71
|
+
|
|
72
|
+
def concat_files(dir_name: str, files: Iterable[PathLike | str]) -> str:
|
|
73
|
+
with NamedTemporaryFile(
|
|
74
|
+
dir=dir_name,
|
|
75
|
+
suffix=".nc",
|
|
76
|
+
delete=False,
|
|
77
|
+
) as temp_file:
|
|
78
|
+
tmp_filename = temp_file.name
|
|
79
|
+
variables = [*keymap.keys(), "elevation"]
|
|
80
|
+
valid_files = list(valid_nc_files(files))
|
|
81
|
+
concat_lib.concatenate_files(
|
|
82
|
+
valid_files,
|
|
83
|
+
tmp_filename,
|
|
84
|
+
variables=variables,
|
|
85
|
+
ignore=["time_coverage_start", "time_coverage_end"],
|
|
86
|
+
)
|
|
87
|
+
return tmp_filename
|
|
88
|
+
|
|
89
|
+
with TemporaryDirectory() as temp_dir:
|
|
90
|
+
if isinstance(input_file, PathLike | str):
|
|
91
|
+
path = Path(input_file)
|
|
92
|
+
if path.is_dir():
|
|
93
|
+
input_file = concat_files(
|
|
94
|
+
temp_dir,
|
|
95
|
+
(p for p in path.iterdir() if p.suffix.lower() == ".nc"),
|
|
96
|
+
)
|
|
97
|
+
else:
|
|
98
|
+
input_file = concat_files(temp_dir, input_file)
|
|
99
|
+
|
|
100
|
+
with MrrPro(input_file, site_meta) as mrr:
|
|
101
|
+
mrr.init_data(keymap)
|
|
102
|
+
mrr.fix_units()
|
|
103
|
+
mrr.date = mrr.init_date()
|
|
104
|
+
if date:
|
|
105
|
+
mrr.screen_by_date(date)
|
|
106
|
+
mrr.add_time_and_range()
|
|
107
|
+
mrr.fix_range()
|
|
108
|
+
mrr.add_site_geolocation()
|
|
109
|
+
mrr.add_zenith_angle()
|
|
110
|
+
mrr.add_radar_specific_variables()
|
|
111
|
+
mrr.add_height()
|
|
112
|
+
mrr.sort_timestamps()
|
|
113
|
+
attributes = output.add_time_attribute(ATTRIBUTES, mrr.date)
|
|
114
|
+
output.update_attributes(mrr.data, attributes)
|
|
115
|
+
output.save_level1b(mrr, output_file, uuid)
|
|
116
|
+
return uuid
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class MrrPro(NcRadar):
|
|
120
|
+
"""Class for MRR-PRO raw data. Child of NcRadar().
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
full_path: MRR-PRO netCDF filename.
|
|
124
|
+
site_meta: Site properties in a dictionary. Required keys are `name`,
|
|
125
|
+
`latitude`, `longitude` and `altitude`.
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
epoch = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
|
|
130
|
+
|
|
131
|
+
def __init__(self, full_path: PathLike | str, site_meta: dict) -> None:
|
|
132
|
+
super().__init__(full_path, site_meta)
|
|
133
|
+
self.instrument = instruments.MRR_PRO
|
|
134
|
+
if m := re.search(
|
|
135
|
+
r"serial number:\s*(\w+)",
|
|
136
|
+
self.dataset.instrument_name,
|
|
137
|
+
re.IGNORECASE,
|
|
138
|
+
):
|
|
139
|
+
self.serial_number = m[1]
|
|
140
|
+
|
|
141
|
+
def init_date(self) -> datetime.date:
|
|
142
|
+
time_stamps = self.getvar("time")
|
|
143
|
+
return utils.seconds2date(time_stamps[0], self.epoch).date()
|
|
144
|
+
|
|
145
|
+
def fix_units(self) -> None:
|
|
146
|
+
self.data["v"].data *= -1 # towards -> away from instrument
|
|
147
|
+
self.data["rainfall_rate"].data /= 3600000 # mm h-1 -> m s-1
|
|
148
|
+
self.data["lwc"].data *= 0.001 # g m-3 -> kg m-3
|
|
149
|
+
|
|
150
|
+
def add_zenith_angle(self) -> None:
|
|
151
|
+
elevation = self.getvar("elevation")
|
|
152
|
+
zenith = 90 - elevation
|
|
153
|
+
self.append_data(zenith, "zenith_angle")
|
|
154
|
+
|
|
155
|
+
def screen_by_date(self, expected_date: datetime.date) -> None:
|
|
156
|
+
"""Screens incorrect time stamps."""
|
|
157
|
+
time_stamps = self.getvar("time")
|
|
158
|
+
valid_indices = []
|
|
159
|
+
for ind, timestamp in enumerate(time_stamps):
|
|
160
|
+
date = utils.seconds2date(timestamp, self.epoch).date()
|
|
161
|
+
if date == expected_date:
|
|
162
|
+
valid_indices.append(ind)
|
|
163
|
+
self.screen_time_indices(valid_indices)
|
|
164
|
+
|
|
165
|
+
def fix_range(self) -> None:
|
|
166
|
+
# It seems like the "range" variable in MRR-PRO raw files is actually
|
|
167
|
+
# defined above mean sea level -> convert to range above ground level
|
|
168
|
+
range_data = self.data["range"].data
|
|
169
|
+
range_data -= range_data[0]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
ATTRIBUTES = {
|
|
173
|
+
"lwc": MetaData(
|
|
174
|
+
long_name="Liquid water content", units="kg m-3", dimensions=("time", "range")
|
|
175
|
+
),
|
|
176
|
+
"pia": MetaData(
|
|
177
|
+
long_name="Path integrated rain attenuation",
|
|
178
|
+
units="dB",
|
|
179
|
+
dimensions=("time", "range"),
|
|
180
|
+
),
|
|
181
|
+
"rainfall_rate": MetaData(
|
|
182
|
+
long_name="Rainfall rate",
|
|
183
|
+
standard_name="rainfall_rate",
|
|
184
|
+
units="m s-1",
|
|
185
|
+
dimensions=("time", "range"),
|
|
186
|
+
),
|
|
187
|
+
}
|
|
@@ -1,39 +1,50 @@
|
|
|
1
1
|
"""Module with a class for Lufft chm15k ceilometer."""
|
|
2
|
+
|
|
2
3
|
import logging
|
|
4
|
+
from typing import TYPE_CHECKING, Literal
|
|
3
5
|
|
|
4
|
-
import netCDF4
|
|
5
6
|
import numpy as np
|
|
7
|
+
from numpy import ma
|
|
6
8
|
|
|
7
9
|
from cloudnetpy import utils
|
|
8
10
|
from cloudnetpy.instruments.ceilometer import Ceilometer
|
|
9
11
|
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
import netCDF4
|
|
14
|
+
|
|
10
15
|
|
|
11
16
|
class NcLidar(Ceilometer):
|
|
12
17
|
"""Class for all lidars using netCDF files."""
|
|
13
18
|
|
|
14
|
-
def __init__(self):
|
|
19
|
+
def __init__(self) -> None:
|
|
15
20
|
super().__init__()
|
|
16
21
|
self.dataset: netCDF4.Dataset | None = None
|
|
17
22
|
|
|
18
|
-
def _fetch_range(self, reference:
|
|
19
|
-
|
|
23
|
+
def _fetch_range(self, reference: Literal["upper", "lower"]) -> None:
|
|
24
|
+
if self.dataset is None:
|
|
25
|
+
msg = "No dataset found"
|
|
26
|
+
raise RuntimeError(msg)
|
|
20
27
|
range_instrument = self.dataset.variables["range"][:]
|
|
21
28
|
self.data["range"] = utils.edges2mid(range_instrument, reference)
|
|
22
29
|
|
|
23
30
|
def _fetch_time_and_date(self) -> None:
|
|
24
|
-
|
|
31
|
+
if self.dataset is None:
|
|
32
|
+
msg = "No dataset found"
|
|
33
|
+
raise RuntimeError(msg)
|
|
25
34
|
time = self.dataset.variables["time"]
|
|
26
35
|
self.data["time"] = time[:]
|
|
27
36
|
epoch = utils.get_epoch(time.units)
|
|
28
37
|
self.get_date_and_time(epoch)
|
|
29
38
|
|
|
30
39
|
def _fetch_zenith_angle(self, key: str, default: float = 3.0) -> None:
|
|
31
|
-
|
|
40
|
+
if self.dataset is None:
|
|
41
|
+
msg = "No dataset found"
|
|
42
|
+
raise RuntimeError(msg)
|
|
32
43
|
if key in self.dataset.variables:
|
|
33
|
-
zenith_angle = self.dataset.variables[key][:]
|
|
44
|
+
zenith_angle = ma.median(self.dataset.variables[key][:])
|
|
34
45
|
else:
|
|
35
46
|
zenith_angle = float(default)
|
|
36
|
-
logging.warning(
|
|
47
|
+
logging.warning("No zenith angle found, assuming %s degrees", zenith_angle)
|
|
37
48
|
if zenith_angle == 0:
|
|
38
49
|
logging.warning("Zenith angle 0 degrees - risk of specular reflection")
|
|
39
50
|
self.data["zenith_angle"] = np.array(zenith_angle)
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
"""Module for reading raw cloud radar data."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
2
4
|
import logging
|
|
5
|
+
from os import PathLike
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
3
7
|
|
|
4
8
|
import numpy as np
|
|
5
9
|
from numpy import ma
|
|
@@ -7,9 +11,11 @@ from numpy import ma
|
|
|
7
11
|
from cloudnetpy import utils
|
|
8
12
|
from cloudnetpy.cloudnetarray import CloudnetArray
|
|
9
13
|
from cloudnetpy.datasource import DataSource
|
|
10
|
-
from cloudnetpy.exceptions import ValidTimeStampError
|
|
14
|
+
from cloudnetpy.exceptions import RadarDataError, ValidTimeStampError
|
|
11
15
|
from cloudnetpy.instruments.cloudnet_instrument import CloudnetInstrument
|
|
12
|
-
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from cloudnetpy.instruments.instruments import Instrument
|
|
13
19
|
|
|
14
20
|
|
|
15
21
|
class NcRadar(DataSource, CloudnetInstrument):
|
|
@@ -23,10 +29,10 @@ class NcRadar(DataSource, CloudnetInstrument):
|
|
|
23
29
|
Used with BASTA, MIRA and Copernicus radars.
|
|
24
30
|
"""
|
|
25
31
|
|
|
26
|
-
def __init__(self, full_path: str, site_meta: dict):
|
|
32
|
+
def __init__(self, full_path: PathLike | str, site_meta: dict) -> None:
|
|
27
33
|
super().__init__(full_path)
|
|
28
34
|
self.site_meta = site_meta
|
|
29
|
-
self.date:
|
|
35
|
+
self.date: datetime.date
|
|
30
36
|
self.instrument: Instrument | None = None
|
|
31
37
|
|
|
32
38
|
def init_data(self, keymap: dict) -> None:
|
|
@@ -35,28 +41,39 @@ class NcRadar(DataSource, CloudnetInstrument):
|
|
|
35
41
|
name = keymap[key]
|
|
36
42
|
try:
|
|
37
43
|
array = self.getvar(key)
|
|
38
|
-
except
|
|
39
|
-
logging.warning(
|
|
44
|
+
except KeyError:
|
|
45
|
+
logging.warning("Can not find variable %s from the input file", key)
|
|
40
46
|
continue
|
|
41
47
|
array = np.array(array) if utils.isscalar(array) else array
|
|
42
48
|
array[~np.isfinite(array)] = ma.masked
|
|
43
49
|
self.append_data(array, name)
|
|
44
50
|
|
|
45
|
-
def add_time_and_range(self) -> None:
|
|
51
|
+
def add_time_and_range(self, time_dtype: str | None = None) -> None:
|
|
46
52
|
"""Adds time and range."""
|
|
47
53
|
range_instru = np.array(
|
|
48
|
-
self.getvar("range", "height")
|
|
54
|
+
self.getvar("range", "height"),
|
|
49
55
|
) # "height" in old BASTA files
|
|
50
56
|
time = np.array(self.time)
|
|
51
57
|
self.append_data(range_instru, "range")
|
|
52
|
-
self.append_data(time, "time")
|
|
58
|
+
self.append_data(time, "time", dtype=time_dtype)
|
|
53
59
|
|
|
54
|
-
def screen_by_snr(self, snr_limit: float
|
|
60
|
+
def screen_by_snr(self, snr_limit: float) -> None:
|
|
55
61
|
"""Mask values where SNR smaller than threshold."""
|
|
56
62
|
ind = np.where(self.data["SNR"][:] < snr_limit)
|
|
57
63
|
for cloudnet_array in self.data.values():
|
|
58
64
|
if cloudnet_array.data.ndim == 2:
|
|
59
65
|
cloudnet_array.mask_indices(ind)
|
|
66
|
+
self.append_data(float(snr_limit), "snr_limit")
|
|
67
|
+
|
|
68
|
+
def screen_using_top_gates_snr(self, snr_limit: float = 2) -> None:
|
|
69
|
+
"""Masks values where SNR is smaller than mean SNR of top gates."""
|
|
70
|
+
n_gates = 50
|
|
71
|
+
snr = self.data["SNR"][:]
|
|
72
|
+
mean_snr = np.mean(snr[:, -n_gates:], axis=1)
|
|
73
|
+
for time_ind, snr_profile in enumerate(snr):
|
|
74
|
+
alt_ind = np.where(snr_profile < mean_snr[time_ind] + snr_limit)[0]
|
|
75
|
+
if len(alt_ind) > 0:
|
|
76
|
+
self.data["Zh"][:][time_ind, alt_ind] = ma.masked
|
|
60
77
|
|
|
61
78
|
def mask_invalid_data(self) -> None:
|
|
62
79
|
"""Makes sure Z and v masks are also in other 2d variables."""
|
|
@@ -67,76 +84,113 @@ class NcRadar(DataSource, CloudnetInstrument):
|
|
|
67
84
|
cloudnet_array.mask_indices(z_mask)
|
|
68
85
|
cloudnet_array.mask_indices(v_mask)
|
|
69
86
|
|
|
70
|
-
def
|
|
87
|
+
def mask_first_range_gates(self, range_limit: float = 150) -> None:
|
|
88
|
+
"""Masks first range gates."""
|
|
89
|
+
if "v" not in self.data or "range" not in self.data:
|
|
90
|
+
return
|
|
91
|
+
ind = np.where(self.data["range"][:] < range_limit)[0]
|
|
92
|
+
if len(ind) > 0:
|
|
93
|
+
self.data["v"].data[:, ind] = ma.masked
|
|
94
|
+
|
|
95
|
+
def add_zenith_and_azimuth_angles(
|
|
96
|
+
self,
|
|
97
|
+
elevation_threshold: float,
|
|
98
|
+
elevation_diff_threshold: float,
|
|
99
|
+
azimuth_diff_threshold: float,
|
|
100
|
+
zenith_offset: float | None = None,
|
|
101
|
+
azimuth_offset: float | None = None,
|
|
102
|
+
) -> list:
|
|
71
103
|
"""Adds non-varying instrument zenith and azimuth angles and returns valid
|
|
72
|
-
time indices.
|
|
73
|
-
|
|
74
|
-
azimuth = self.data["azimuth_velocity"].data
|
|
75
|
-
if np.all(azimuth == azimuth[0]):
|
|
76
|
-
azimuth_reference = azimuth[0]
|
|
77
|
-
else:
|
|
78
|
-
azimuth_reference = 0
|
|
79
|
-
azimuth_tolerance = 1e-6
|
|
80
|
-
else:
|
|
81
|
-
azimuth = self.data["azimuth_angle"].data
|
|
82
|
-
azimuth_reference = ma.median(azimuth)
|
|
83
|
-
azimuth_tolerance = 0.1
|
|
84
|
-
|
|
104
|
+
time indices.
|
|
105
|
+
"""
|
|
85
106
|
elevation = self.data["elevation"].data
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
107
|
+
if zenith_offset is not None:
|
|
108
|
+
zenith_offset = float(zenith_offset)
|
|
109
|
+
self.append_data(zenith_offset, "zenith_offset")
|
|
110
|
+
elevation -= zenith_offset
|
|
111
|
+
|
|
112
|
+
azimuth = self.data["azimuth_angle"].data
|
|
113
|
+
if azimuth_offset is not None:
|
|
114
|
+
azimuth_offset = float(azimuth_offset)
|
|
115
|
+
self.append_data(azimuth_offset, "azimuth_offset")
|
|
116
|
+
azimuth += azimuth_offset
|
|
117
|
+
|
|
118
|
+
if len(elevation) < 2 or len(azimuth) < 2:
|
|
119
|
+
msg = "Less than two profiles with valid zenith / azimuth angles"
|
|
120
|
+
raise ValidTimeStampError(msg)
|
|
121
|
+
|
|
122
|
+
elevation_diff = ma.diff(elevation, prepend=elevation[1])
|
|
123
|
+
azimuth_diff = ma.diff(azimuth, prepend=azimuth[1])
|
|
124
|
+
|
|
125
|
+
is_stable = np.abs(elevation - 90) < elevation_threshold
|
|
126
|
+
is_stable &= np.abs(elevation_diff) < elevation_diff_threshold
|
|
127
|
+
is_stable &= np.abs(azimuth_diff) < azimuth_diff_threshold
|
|
128
|
+
|
|
129
|
+
# If scanning unit is broken, data are missing
|
|
130
|
+
# (assume it's vertically pointing)
|
|
131
|
+
missing_info = elevation.mask & azimuth.mask
|
|
132
|
+
is_stable[missing_info] = True
|
|
133
|
+
|
|
134
|
+
if ma.isMaskedArray(is_stable):
|
|
135
|
+
is_stable[is_stable.mask] = False
|
|
136
|
+
n_removed = np.count_nonzero(~is_stable)
|
|
137
|
+
|
|
138
|
+
if n_removed >= len(elevation) - 1:
|
|
139
|
+
msg = "Less than two profiles with valid zenith / azimuth angles"
|
|
140
|
+
raise ValidTimeStampError(msg)
|
|
141
|
+
|
|
99
142
|
if n_removed > 0:
|
|
100
143
|
logging.warning(
|
|
101
|
-
|
|
144
|
+
"Filtering %s profiles due to varying zenith / azimuth angle",
|
|
145
|
+
n_removed,
|
|
102
146
|
)
|
|
147
|
+
zenith = 90 - elevation
|
|
103
148
|
self.append_data(zenith, "zenith_angle")
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
del self.data[key]
|
|
107
|
-
return list(is_stable_profile)
|
|
149
|
+
del self.data["elevation"]
|
|
150
|
+
return list(is_stable)
|
|
108
151
|
|
|
109
|
-
def add_radar_specific_variables(self):
|
|
152
|
+
def add_radar_specific_variables(self) -> None:
|
|
110
153
|
"""Adds radar specific variables."""
|
|
111
|
-
|
|
154
|
+
if self.instrument is None:
|
|
155
|
+
msg = "Instrument not defined"
|
|
156
|
+
raise RuntimeError(msg)
|
|
112
157
|
key = "radar_frequency"
|
|
113
|
-
|
|
158
|
+
if self.instrument.frequency is not None:
|
|
159
|
+
self.data[key] = CloudnetArray(self.instrument.frequency, key)
|
|
114
160
|
try:
|
|
115
161
|
possible_nyquist_names = ("ambiguous_velocity", "NyquistVelocity")
|
|
116
162
|
data = self.getvar(*possible_nyquist_names)
|
|
117
163
|
key = "nyquist_velocity"
|
|
118
|
-
self.data[key] = CloudnetArray(np.array(data), key)
|
|
119
|
-
|
|
164
|
+
self.data[key] = CloudnetArray(np.median(np.array(data)), key)
|
|
165
|
+
if "NyquistVelocity" in self.data:
|
|
166
|
+
del self.data["NyquistVelocity"]
|
|
167
|
+
except KeyError:
|
|
120
168
|
logging.warning("Unable to find nyquist_velocity")
|
|
121
169
|
|
|
170
|
+
def test_if_all_masked(self) -> None:
|
|
171
|
+
"""Tests if all data are masked."""
|
|
172
|
+
v = self.data["v"][:]
|
|
173
|
+
if ma.isMaskedArray(v) and np.all(v.mask):
|
|
174
|
+
msg = "All radar data are masked"
|
|
175
|
+
raise RadarDataError(msg)
|
|
176
|
+
|
|
122
177
|
|
|
123
178
|
class ChilboltonRadar(NcRadar):
|
|
124
179
|
"""Class for Chilbolton cloud radars Galileo and Copernicus."""
|
|
125
180
|
|
|
126
|
-
def __init__(self, full_path: str, site_meta: dict):
|
|
181
|
+
def __init__(self, full_path: str | PathLike, site_meta: dict) -> None:
|
|
127
182
|
super().__init__(full_path, site_meta)
|
|
128
183
|
self.date = self._init_date()
|
|
129
184
|
|
|
130
|
-
def add_nyquist_velocity(self, keymap: dict):
|
|
185
|
+
def add_nyquist_velocity(self, keymap: dict) -> None:
|
|
131
186
|
"""Adds nyquist velocity."""
|
|
132
|
-
key = [key for key, value in keymap.items() if value == "v"][0]
|
|
187
|
+
key = [key for key, value in keymap.items() if value == "v"][0] # noqa: RUF015
|
|
133
188
|
folding_velocity = self.dataset.variables[key].folding_velocity
|
|
134
189
|
self.append_data(np.array(folding_velocity), "nyquist_velocity")
|
|
135
190
|
|
|
136
|
-
def check_date(self, date:
|
|
137
|
-
if self.date != date
|
|
191
|
+
def check_date(self, date: datetime.date) -> None:
|
|
192
|
+
if self.date != date:
|
|
138
193
|
raise ValidTimeStampError
|
|
139
194
|
|
|
140
|
-
def _init_date(self) ->
|
|
141
|
-
|
|
142
|
-
return [str(x).zfill(2) for x in epoch]
|
|
195
|
+
def _init_date(self) -> datetime.date:
|
|
196
|
+
return utils.get_epoch(self.dataset["time"].units).date()
|