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
cloudnetpy/instruments/lufft.py
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"""Module with a class for Lufft chm15k ceilometer."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
2
4
|
import logging
|
|
5
|
+
from os import PathLike
|
|
3
6
|
|
|
4
7
|
import netCDF4
|
|
8
|
+
import numpy as np
|
|
5
9
|
from numpy import ma
|
|
6
10
|
|
|
7
11
|
from cloudnetpy import utils
|
|
@@ -12,16 +16,17 @@ from cloudnetpy.instruments.nc_lidar import NcLidar
|
|
|
12
16
|
class LufftCeilo(NcLidar):
|
|
13
17
|
"""Class for Lufft chm15k ceilometer."""
|
|
14
18
|
|
|
15
|
-
serial_number: str | None
|
|
16
|
-
|
|
17
19
|
def __init__(
|
|
18
|
-
self,
|
|
19
|
-
|
|
20
|
+
self,
|
|
21
|
+
file_name: str | PathLike,
|
|
22
|
+
site_meta: dict,
|
|
23
|
+
expected_date: datetime.date | None = None,
|
|
24
|
+
) -> None:
|
|
20
25
|
super().__init__()
|
|
21
26
|
self.file_name = file_name
|
|
22
27
|
self.site_meta = site_meta
|
|
23
28
|
self.expected_date = expected_date
|
|
24
|
-
self.
|
|
29
|
+
self.is_old_version = False
|
|
25
30
|
|
|
26
31
|
def read_ceilometer_file(self, calibration_factor: float | None = None) -> None:
|
|
27
32
|
"""Reads data and metadata from Jenoptik netCDF file."""
|
|
@@ -34,32 +39,46 @@ class LufftCeilo(NcLidar):
|
|
|
34
39
|
self._fetch_zenith_angle("zenith")
|
|
35
40
|
|
|
36
41
|
def _fetch_beta_raw(self, calibration_factor: float | None = None) -> None:
|
|
37
|
-
assert self.dataset is not None
|
|
38
42
|
if calibration_factor is None:
|
|
39
43
|
logging.warning("Using default calibration factor")
|
|
40
44
|
calibration_factor = 3e-12
|
|
41
45
|
beta_raw = self._getvar("beta_raw", "beta_att")
|
|
46
|
+
beta_raw = ma.masked_array(beta_raw)
|
|
42
47
|
old_version = self._get_old_software_version()
|
|
43
48
|
if old_version is not None:
|
|
49
|
+
self.is_old_version = True
|
|
44
50
|
logging.warning(
|
|
45
|
-
|
|
51
|
+
"Software version %s. Assuming data not range corrected.",
|
|
52
|
+
old_version,
|
|
46
53
|
)
|
|
47
54
|
data_std = self._getvar("stddev")
|
|
48
55
|
normalised_apd = self._get_nn()
|
|
49
|
-
beta_raw *= utils.transpose(data_std / normalised_apd)
|
|
56
|
+
beta_raw *= utils.transpose(ma.masked_array(data_std / normalised_apd))
|
|
50
57
|
beta_raw *= self.data["range"] ** 2
|
|
51
58
|
beta_raw *= calibration_factor
|
|
52
59
|
self.data["calibration_factor"] = float(calibration_factor)
|
|
53
60
|
self.data["beta_raw"] = beta_raw
|
|
54
61
|
|
|
55
62
|
def _get_old_software_version(self) -> str | None:
|
|
56
|
-
|
|
63
|
+
if self.dataset is None:
|
|
64
|
+
msg = "No dataset found"
|
|
65
|
+
raise RuntimeError(msg)
|
|
57
66
|
version = self.dataset.software_version
|
|
58
|
-
|
|
67
|
+
# In old files, the version is a single integer.
|
|
68
|
+
if isinstance(version, np.integer):
|
|
69
|
+
return str(version)
|
|
70
|
+
# In newer files, the version is a space-separated list: Operating
|
|
71
|
+
# system, FPGA, firmware, CloudDetectionMode (added in firmware 0.747).
|
|
72
|
+
if isinstance(version, str):
|
|
73
|
+
parts = version.split()
|
|
74
|
+
firmware = parts[2]
|
|
75
|
+
if firmware < "0.702":
|
|
76
|
+
return firmware
|
|
59
77
|
return None
|
|
60
|
-
|
|
78
|
+
msg = f"Cannot determine version: {version}"
|
|
79
|
+
raise RuntimeError(msg)
|
|
61
80
|
|
|
62
|
-
def _get_nn(self):
|
|
81
|
+
def _get_nn(self) -> float | ma.MaskedArray:
|
|
63
82
|
nn1 = self._getvar("nn1", "NN1")
|
|
64
83
|
median_nn1 = ma.median(nn1)
|
|
65
84
|
# Parameters taken from the matlab code and should be verified
|
|
@@ -72,18 +91,21 @@ class LufftCeilo(NcLidar):
|
|
|
72
91
|
return 1
|
|
73
92
|
return step_factor ** (-(nn1 - reference) / scale)
|
|
74
93
|
|
|
75
|
-
def _getvar(self, *args):
|
|
76
|
-
|
|
94
|
+
def _getvar(self, *args: str) -> float | ma.MaskedArray:
|
|
95
|
+
if self.dataset is None:
|
|
96
|
+
msg = "No dataset found"
|
|
97
|
+
raise RuntimeError(msg)
|
|
77
98
|
for arg in args:
|
|
78
99
|
if arg in self.dataset.variables:
|
|
79
100
|
var = self.dataset.variables[arg]
|
|
80
101
|
return var[0] if utils.isscalar(var) else var[:]
|
|
81
|
-
|
|
102
|
+
msg = f"Unable to find variable {args[0]}"
|
|
103
|
+
raise ValueError(msg)
|
|
82
104
|
|
|
83
|
-
def _fetch_attributes(self):
|
|
105
|
+
def _fetch_attributes(self) -> None:
|
|
84
106
|
self.serial_number = getattr(self.dataset, "device_name", None)
|
|
85
107
|
if self.serial_number is None:
|
|
86
|
-
self.serial_number = getattr(self.dataset, "source")
|
|
108
|
+
self.serial_number = getattr(self.dataset, "source", "")
|
|
87
109
|
self.instrument = (
|
|
88
110
|
instruments.CHM15KX
|
|
89
111
|
if self.serial_number.startswith("CHX")
|
cloudnetpy/instruments/mira.py
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
"""Module for reading raw cloud radar data."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import logging
|
|
2
5
|
import os
|
|
3
|
-
|
|
6
|
+
import re
|
|
7
|
+
from collections import OrderedDict
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from os import PathLike
|
|
10
|
+
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
|
11
|
+
from uuid import UUID
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from numpy import ma
|
|
4
15
|
|
|
5
16
|
from cloudnetpy import concat_lib, output, utils
|
|
6
|
-
from cloudnetpy.
|
|
7
|
-
from cloudnetpy.instruments.instruments import MIRA35
|
|
17
|
+
from cloudnetpy.instruments.instruments import MIRA10, MIRA35
|
|
8
18
|
from cloudnetpy.instruments.nc_radar import NcRadar
|
|
9
19
|
from cloudnetpy.metadata import MetaData
|
|
10
20
|
|
|
11
21
|
|
|
12
22
|
def mira2nc(
|
|
13
|
-
raw_mira: str |
|
|
14
|
-
output_file: str,
|
|
23
|
+
raw_mira: str | PathLike | Sequence[str | PathLike],
|
|
24
|
+
output_file: str | PathLike,
|
|
15
25
|
site_meta: dict,
|
|
16
|
-
uuid: str | None = None,
|
|
17
|
-
date: str | None = None,
|
|
18
|
-
) ->
|
|
26
|
+
uuid: str | UUID | None = None,
|
|
27
|
+
date: str | datetime.date | None = None,
|
|
28
|
+
) -> UUID:
|
|
19
29
|
"""Converts METEK MIRA-35 cloud radar data into Cloudnet Level 1b netCDF file.
|
|
20
30
|
|
|
21
31
|
This function converts raw MIRA file(s) into a much smaller file that
|
|
@@ -23,8 +33,10 @@ def mira2nc(
|
|
|
23
33
|
steps.
|
|
24
34
|
|
|
25
35
|
Args:
|
|
26
|
-
raw_mira: Filename of a daily MIRA .mmclx
|
|
27
|
-
several non-concatenated .mmclx files from one day
|
|
36
|
+
raw_mira: Filename of a daily MIRA .mmclx or .zncfile. Can be also a folder
|
|
37
|
+
containing several non-concatenated .mmclx or .znc files from one day
|
|
38
|
+
or list of files. znc files take precedence because they are the newer
|
|
39
|
+
filetype
|
|
28
40
|
output_file: Output filename.
|
|
29
41
|
site_meta: Dictionary containing information about the site. Required key
|
|
30
42
|
value pair is `name`.
|
|
@@ -36,66 +48,68 @@ def mira2nc(
|
|
|
36
48
|
|
|
37
49
|
Raises:
|
|
38
50
|
ValidTimeStampError: No valid timestamps found.
|
|
51
|
+
FileNotFoundError: No suitable input files found.
|
|
52
|
+
ValueError: Wrong suffix in input file(s).
|
|
53
|
+
TypeError: Mixed mmclx and znc files.
|
|
39
54
|
|
|
40
55
|
Examples:
|
|
41
56
|
>>> from cloudnetpy.instruments import mira2nc
|
|
42
57
|
>>> site_meta = {'name': 'Vehmasmaki'}
|
|
43
58
|
>>> mira2nc('raw_radar.mmclx', 'radar.nc', site_meta)
|
|
59
|
+
>>> mira2nc('raw_radar.znc', 'radar.nc', site_meta)
|
|
44
60
|
>>> mira2nc('/one/day/of/mira/mmclx/files/', 'radar.nc', site_meta)
|
|
61
|
+
>>> mira2nc('/one/day/of/mira/znc/files/', 'radar.nc', site_meta)
|
|
45
62
|
|
|
46
63
|
"""
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"RMSg": "width",
|
|
51
|
-
"LDRg": "ldr",
|
|
52
|
-
"SNRg": "SNR",
|
|
53
|
-
"elv": "elevation",
|
|
54
|
-
"azi": "azimuth_angle",
|
|
55
|
-
"aziv": "azimuth_velocity",
|
|
56
|
-
"nfft": "nfft",
|
|
57
|
-
"nave": "nave",
|
|
58
|
-
"prf": "prf",
|
|
59
|
-
"rg0": "rg0",
|
|
60
|
-
}
|
|
64
|
+
if isinstance(date, str):
|
|
65
|
+
date = datetime.date.fromisoformat(date)
|
|
66
|
+
uuid = utils.get_uuid(uuid)
|
|
61
67
|
|
|
62
68
|
with TemporaryDirectory() as temp_dir:
|
|
63
|
-
|
|
64
|
-
mmclx_filename = f"{temp_dir}/tmp.mmclx"
|
|
65
|
-
if isinstance(raw_mira, list):
|
|
66
|
-
valid_filenames = sorted(raw_mira)
|
|
67
|
-
else:
|
|
68
|
-
valid_filenames = utils.get_sorted_filenames(raw_mira, ".mmclx")
|
|
69
|
-
valid_filenames = utils.get_files_with_common_range(valid_filenames)
|
|
70
|
-
variables = list(keymap.keys())
|
|
71
|
-
concat_lib.concatenate_files(
|
|
72
|
-
valid_filenames,
|
|
73
|
-
mmclx_filename,
|
|
74
|
-
variables=variables,
|
|
75
|
-
allow_difference=["nave", "ovl"],
|
|
76
|
-
)
|
|
77
|
-
else:
|
|
78
|
-
mmclx_filename = raw_mira
|
|
69
|
+
input_filename, keymap = _parse_input_files(raw_mira, temp_dir)
|
|
79
70
|
|
|
80
|
-
with Mira(
|
|
71
|
+
with Mira(input_filename, site_meta) as mira:
|
|
81
72
|
mira.init_data(keymap)
|
|
82
73
|
if date is not None:
|
|
83
74
|
mira.screen_by_date(date)
|
|
84
|
-
mira.date = date
|
|
75
|
+
mira.date = date
|
|
85
76
|
mira.sort_timestamps()
|
|
86
77
|
mira.remove_duplicate_timestamps()
|
|
87
78
|
mira.linear_to_db(("Zh", "ldr", "SNR"))
|
|
88
|
-
mira.
|
|
79
|
+
mira.screen_low_power()
|
|
80
|
+
|
|
81
|
+
if "snr_limit" in site_meta and site_meta["snr_limit"] is not None:
|
|
82
|
+
snr_limit = site_meta["snr_limit"]
|
|
83
|
+
else:
|
|
84
|
+
# Empirical values, should be checked
|
|
85
|
+
snr_limit = -30 if mira.instrument == MIRA10 else -17
|
|
86
|
+
|
|
87
|
+
# Old MIRA files don't have angle variables.
|
|
88
|
+
if "elevation" not in mira.data:
|
|
89
|
+
mira.append_data(ma.masked_all_like(mira.time.data), "elevation")
|
|
90
|
+
if "azimuth_angle" not in mira.data:
|
|
91
|
+
mira.append_data(ma.masked_all_like(mira.time.data), "azimuth_angle")
|
|
92
|
+
|
|
93
|
+
mira.screen_by_snr(snr_limit)
|
|
94
|
+
mira.screen_invalid_ldr()
|
|
89
95
|
mira.mask_invalid_data()
|
|
96
|
+
mira.mask_bad_angles()
|
|
90
97
|
mira.add_time_and_range()
|
|
91
98
|
mira.add_site_geolocation()
|
|
92
99
|
mira.add_radar_specific_variables()
|
|
93
|
-
valid_indices = mira.add_zenith_and_azimuth_angles(
|
|
100
|
+
valid_indices = mira.add_zenith_and_azimuth_angles(
|
|
101
|
+
elevation_threshold=1.1,
|
|
102
|
+
elevation_diff_threshold=1e-6,
|
|
103
|
+
azimuth_diff_threshold=1e-3,
|
|
104
|
+
zenith_offset=site_meta.get("zenith_offset"),
|
|
105
|
+
azimuth_offset=site_meta.get("azimuth_offset"),
|
|
106
|
+
)
|
|
94
107
|
mira.screen_time_indices(valid_indices)
|
|
95
108
|
mira.add_height()
|
|
109
|
+
mira.test_if_all_masked()
|
|
96
110
|
attributes = output.add_time_attribute(ATTRIBUTES, mira.date)
|
|
97
111
|
output.update_attributes(mira.data, attributes)
|
|
98
|
-
|
|
112
|
+
output.save_level1b(mira, output_file, uuid)
|
|
99
113
|
return uuid
|
|
100
114
|
|
|
101
115
|
|
|
@@ -108,42 +122,235 @@ class Mira(NcRadar):
|
|
|
108
122
|
|
|
109
123
|
"""
|
|
110
124
|
|
|
111
|
-
epoch = (1970, 1, 1)
|
|
125
|
+
epoch = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
|
|
112
126
|
|
|
113
|
-
def __init__(self, full_path: str, site_meta: dict):
|
|
127
|
+
def __init__(self, full_path: str | PathLike, site_meta: dict) -> None:
|
|
114
128
|
super().__init__(full_path, site_meta)
|
|
115
129
|
self.date = self._init_mira_date()
|
|
116
|
-
|
|
130
|
+
if "model" not in site_meta or site_meta["model"] == "mira-35":
|
|
131
|
+
self.instrument = MIRA35
|
|
132
|
+
elif site_meta["model"] == "mira-10":
|
|
133
|
+
self.instrument = MIRA10
|
|
134
|
+
else:
|
|
135
|
+
msg = f"Invalid model: {site_meta['model']}"
|
|
136
|
+
raise ValueError(msg)
|
|
117
137
|
|
|
118
|
-
def screen_by_date(self, expected_date:
|
|
138
|
+
def screen_by_date(self, expected_date: datetime.date) -> None:
|
|
119
139
|
"""Screens incorrect time stamps."""
|
|
120
140
|
time_stamps = self.getvar("time")
|
|
121
141
|
valid_indices = []
|
|
122
142
|
for ind, timestamp in enumerate(time_stamps):
|
|
123
|
-
|
|
143
|
+
if not timestamp:
|
|
144
|
+
continue
|
|
145
|
+
date = utils.seconds2date(timestamp, self.epoch).date()
|
|
124
146
|
if date == expected_date:
|
|
125
147
|
valid_indices.append(ind)
|
|
126
|
-
if not valid_indices:
|
|
127
|
-
raise ValidTimeStampError
|
|
128
148
|
self.screen_time_indices(valid_indices)
|
|
129
149
|
|
|
130
|
-
def _init_mira_date(self) ->
|
|
150
|
+
def _init_mira_date(self) -> datetime.date:
|
|
131
151
|
time_stamps = self.getvar("time")
|
|
132
|
-
return utils.seconds2date(time_stamps[0], self.epoch)
|
|
152
|
+
return utils.seconds2date(float(time_stamps[0]), self.epoch).date()
|
|
153
|
+
|
|
154
|
+
def screen_low_power(self) -> None:
|
|
155
|
+
"""Screen times with average transmit power close to zero."""
|
|
156
|
+
if "tpow" not in self.data:
|
|
157
|
+
logging.warning("Variable tpow is missing")
|
|
158
|
+
return
|
|
159
|
+
tpow = self.data["tpow"][:]
|
|
160
|
+
# Threshold for abnormally low power e.g. Limassol 2024-10-20. Average
|
|
161
|
+
# power should 30 to 60 W according to MIRA-35 data sheet. Based on a
|
|
162
|
+
# random sample, typical range is 15 to 25 W. In Lampedusa, the power is
|
|
163
|
+
# constantly as low as 1.9 W.
|
|
164
|
+
is_low = tpow < 1
|
|
165
|
+
n_removed = np.count_nonzero(is_low)
|
|
166
|
+
if n_removed > 0:
|
|
167
|
+
logging.warning(
|
|
168
|
+
"Filtering %s profiles due to low average transmit power", n_removed
|
|
169
|
+
)
|
|
170
|
+
self.screen_time_indices(~is_low)
|
|
171
|
+
|
|
172
|
+
def screen_invalid_ldr(self) -> None:
|
|
173
|
+
"""Masks LDR in MIRA STSR mode data.
|
|
174
|
+
Is there a better way to identify this mode?
|
|
175
|
+
"""
|
|
176
|
+
if "ldr" not in self.data:
|
|
177
|
+
return
|
|
178
|
+
ldr = self.data["ldr"][:]
|
|
179
|
+
if ma.mean(ldr) > 0:
|
|
180
|
+
logging.warning(
|
|
181
|
+
"LDR values suspiciously high. Mira in STSR mode? "
|
|
182
|
+
"Screening all LDR for now.",
|
|
183
|
+
)
|
|
184
|
+
self.data["ldr"].data[:] = ma.masked
|
|
185
|
+
|
|
186
|
+
def mask_bad_angles(self) -> None:
|
|
187
|
+
"""Masks clearly bad elevation and azimuth angles."""
|
|
188
|
+
limits = {
|
|
189
|
+
"elevation": (0, 180),
|
|
190
|
+
"azimuth_angle": (-360, 360),
|
|
191
|
+
}
|
|
192
|
+
for key, (lower, upper) in limits.items():
|
|
193
|
+
if (array := self.data[key].data) is not None:
|
|
194
|
+
margin = (upper - lower) * 0.05
|
|
195
|
+
array[array < (lower - margin)] = ma.masked
|
|
196
|
+
array[array > (upper + margin)] = ma.masked
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _parse_input_files(
|
|
200
|
+
input_files: str | PathLike | Sequence[str | PathLike], temp_dir: str
|
|
201
|
+
) -> tuple[str | PathLike, dict[str, str]]:
|
|
202
|
+
input_filename: str | PathLike
|
|
203
|
+
if (
|
|
204
|
+
not isinstance(input_files, str) and isinstance(input_files, Sequence)
|
|
205
|
+
) or os.path.isdir(input_files):
|
|
206
|
+
with NamedTemporaryFile(
|
|
207
|
+
dir=temp_dir,
|
|
208
|
+
suffix=".nc",
|
|
209
|
+
delete=False,
|
|
210
|
+
) as temp_file:
|
|
211
|
+
input_filename = temp_file.name
|
|
212
|
+
if not isinstance(input_files, str) and isinstance(input_files, Sequence):
|
|
213
|
+
valid_files = sorted(map(str, input_files))
|
|
214
|
+
else:
|
|
215
|
+
valid_files = utils.get_sorted_filenames(input_files, ".znc")
|
|
216
|
+
if not valid_files:
|
|
217
|
+
valid_files = utils.get_sorted_filenames(input_files, ".mmclx")
|
|
218
|
+
|
|
219
|
+
if not valid_files:
|
|
220
|
+
msg = (
|
|
221
|
+
(
|
|
222
|
+
f"Neither znc nor mmclx files found {input_files}. "
|
|
223
|
+
f"Please check your input."
|
|
224
|
+
),
|
|
225
|
+
)
|
|
226
|
+
raise FileNotFoundError(msg)
|
|
227
|
+
|
|
228
|
+
filetypes = list({_get_suffix(f) for f in valid_files})
|
|
229
|
+
|
|
230
|
+
if len(filetypes) > 1:
|
|
231
|
+
err_msg = "Mixed mmclx and znc files. Please use only one filetype."
|
|
232
|
+
raise TypeError(err_msg)
|
|
233
|
+
|
|
234
|
+
keymap = _get_keymap(filetypes[0])
|
|
235
|
+
|
|
236
|
+
variables = list(keymap.keys())
|
|
237
|
+
concat_lib.concatenate_files(
|
|
238
|
+
valid_files,
|
|
239
|
+
input_filename,
|
|
240
|
+
variables=variables,
|
|
241
|
+
ignore=_get_ignored_variables(filetypes[0]),
|
|
242
|
+
)
|
|
243
|
+
else:
|
|
244
|
+
input_filename = input_files
|
|
245
|
+
keymap = _get_keymap(_get_suffix(input_filename))
|
|
246
|
+
|
|
247
|
+
return input_filename, keymap
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _get_ignored_variables(filetype: str) -> list | None:
|
|
251
|
+
"""Returns variables to ignore for METEK MIRA-35 cloud radar concat."""
|
|
252
|
+
_check_file_type(filetype)
|
|
253
|
+
# Ignore spectral variables for now
|
|
254
|
+
keymaps = {
|
|
255
|
+
"znc": ["DropSize", "SPCco", "SPCcx", "SPCcocxRe", "SPCcocxIm", "doppler"],
|
|
256
|
+
"mmclx": None,
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return keymaps.get(filetype.lower(), keymaps.get("mmclx"))
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _get_suffix(filename: str | PathLike) -> str:
|
|
263
|
+
m = re.search(r"\.(\w+)(\.\d+)?$", str(filename))
|
|
264
|
+
if m is None:
|
|
265
|
+
return ""
|
|
266
|
+
return m[1].lower()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _get_keymap(filetype: str) -> dict[str, str]:
|
|
270
|
+
"""Returns a dictionary mapping the variables in the raw data to the processed
|
|
271
|
+
Cloudnet file.
|
|
272
|
+
"""
|
|
273
|
+
_check_file_type(filetype)
|
|
274
|
+
|
|
275
|
+
# Order is relevant with the new znc files from STSR radar
|
|
276
|
+
keymaps = {
|
|
277
|
+
"znc": OrderedDict(
|
|
278
|
+
[
|
|
279
|
+
("Zg", "Zh"), # fallback
|
|
280
|
+
("Zh2l", "Zh"),
|
|
281
|
+
("VELg", "v"), # fallback
|
|
282
|
+
("VELh2l", "v"),
|
|
283
|
+
("RMSg", "width"), # fallback
|
|
284
|
+
("RMSh2l", "width"),
|
|
285
|
+
("LDRg", "ldr"), # fallback
|
|
286
|
+
("LDRh2l", "ldr"),
|
|
287
|
+
("SNRg", "SNR"), # fallback
|
|
288
|
+
("SNRh2l", "SNR"),
|
|
289
|
+
("elv", "elevation"),
|
|
290
|
+
("azi", "azimuth_angle"),
|
|
291
|
+
("nfft", "nfft"),
|
|
292
|
+
("nave", "nave"),
|
|
293
|
+
("prf", "prf"),
|
|
294
|
+
("rg0", "rg0"),
|
|
295
|
+
("tpow", "tpow"),
|
|
296
|
+
],
|
|
297
|
+
),
|
|
298
|
+
"mmclx": OrderedDict(
|
|
299
|
+
[
|
|
300
|
+
("Ze", "Zh"), # fallback for old mmclx files
|
|
301
|
+
("Zg", "Zh"),
|
|
302
|
+
("VELg", "v"),
|
|
303
|
+
("RMSg", "width"),
|
|
304
|
+
("LDRg", "ldr"),
|
|
305
|
+
("SNRg", "SNR"),
|
|
306
|
+
("elv", "elevation"),
|
|
307
|
+
("azi", "azimuth_angle"),
|
|
308
|
+
("nfft", "nfft"),
|
|
309
|
+
("nave", "nave"),
|
|
310
|
+
("prf", "prf"),
|
|
311
|
+
("rg0", "rg0"),
|
|
312
|
+
("NyquistVelocity", "NyquistVelocity"), # variable in some mmclx files
|
|
313
|
+
("tpow", "tpow"),
|
|
314
|
+
]
|
|
315
|
+
),
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return keymaps.get(filetype.lower(), keymaps["mmclx"])
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _check_file_type(filetype: str) -> None:
|
|
322
|
+
known_filetypes = ["znc", "mmclx"]
|
|
323
|
+
if filetype.lower() not in known_filetypes:
|
|
324
|
+
msg = f"Filetype must be one of {known_filetypes}"
|
|
325
|
+
raise ValueError(msg)
|
|
133
326
|
|
|
134
327
|
|
|
135
328
|
ATTRIBUTES = {
|
|
136
|
-
"nfft": MetaData(
|
|
137
|
-
long_name="Number of FFT points",
|
|
138
|
-
units="1",
|
|
139
|
-
),
|
|
329
|
+
"nfft": MetaData(long_name="Number of FFT points", units="1", dimensions=("time",)),
|
|
140
330
|
"nave": MetaData(
|
|
141
331
|
long_name="Number of spectral averages (not accounting for overlapping FFTs)",
|
|
142
332
|
units="1",
|
|
333
|
+
dimensions=("time",),
|
|
334
|
+
),
|
|
335
|
+
"rg0": MetaData(
|
|
336
|
+
long_name="Number of lowest range gates", units="1", dimensions=("time",)
|
|
143
337
|
),
|
|
144
|
-
"rg0": MetaData(long_name="Number of lowest range gates", units="1"),
|
|
145
338
|
"prf": MetaData(
|
|
146
|
-
long_name="Pulse Repetition Frequency",
|
|
147
|
-
|
|
339
|
+
long_name="Pulse Repetition Frequency", units="Hz", dimensions=("time",)
|
|
340
|
+
),
|
|
341
|
+
"tpow": MetaData(
|
|
342
|
+
long_name="Average Transmit Power", units="W", dimensions=("time",)
|
|
343
|
+
),
|
|
344
|
+
"zenith_offset": MetaData(
|
|
345
|
+
long_name="Zenith offset of the instrument",
|
|
346
|
+
units="degrees",
|
|
347
|
+
comment="Zenith offset applied.",
|
|
348
|
+
dimensions=None,
|
|
349
|
+
),
|
|
350
|
+
"azimuth_offset": MetaData(
|
|
351
|
+
long_name="Azimuth offset of the instrument (positive clockwise from north)",
|
|
352
|
+
units="degrees",
|
|
353
|
+
comment="Azimuth offset applied.",
|
|
354
|
+
dimensions=None,
|
|
148
355
|
),
|
|
149
356
|
}
|