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.
Files changed (116) hide show
  1. cloudnetpy/categorize/__init__.py +1 -2
  2. cloudnetpy/categorize/atmos_utils.py +297 -67
  3. cloudnetpy/categorize/attenuation.py +31 -0
  4. cloudnetpy/categorize/attenuations/__init__.py +37 -0
  5. cloudnetpy/categorize/attenuations/gas_attenuation.py +30 -0
  6. cloudnetpy/categorize/attenuations/liquid_attenuation.py +84 -0
  7. cloudnetpy/categorize/attenuations/melting_attenuation.py +78 -0
  8. cloudnetpy/categorize/attenuations/rain_attenuation.py +84 -0
  9. cloudnetpy/categorize/categorize.py +332 -156
  10. cloudnetpy/categorize/classify.py +127 -125
  11. cloudnetpy/categorize/containers.py +107 -76
  12. cloudnetpy/categorize/disdrometer.py +40 -0
  13. cloudnetpy/categorize/droplet.py +23 -21
  14. cloudnetpy/categorize/falling.py +53 -24
  15. cloudnetpy/categorize/freezing.py +25 -12
  16. cloudnetpy/categorize/insects.py +35 -23
  17. cloudnetpy/categorize/itu.py +243 -0
  18. cloudnetpy/categorize/lidar.py +36 -41
  19. cloudnetpy/categorize/melting.py +34 -26
  20. cloudnetpy/categorize/model.py +84 -37
  21. cloudnetpy/categorize/mwr.py +18 -14
  22. cloudnetpy/categorize/radar.py +215 -102
  23. cloudnetpy/cli.py +578 -0
  24. cloudnetpy/cloudnetarray.py +43 -89
  25. cloudnetpy/concat_lib.py +218 -78
  26. cloudnetpy/constants.py +28 -10
  27. cloudnetpy/datasource.py +61 -86
  28. cloudnetpy/exceptions.py +49 -20
  29. cloudnetpy/instruments/__init__.py +5 -0
  30. cloudnetpy/instruments/basta.py +29 -12
  31. cloudnetpy/instruments/bowtie.py +135 -0
  32. cloudnetpy/instruments/ceilo.py +138 -115
  33. cloudnetpy/instruments/ceilometer.py +164 -80
  34. cloudnetpy/instruments/cl61d.py +21 -5
  35. cloudnetpy/instruments/cloudnet_instrument.py +74 -36
  36. cloudnetpy/instruments/copernicus.py +108 -30
  37. cloudnetpy/instruments/da10.py +54 -0
  38. cloudnetpy/instruments/disdrometer/common.py +126 -223
  39. cloudnetpy/instruments/disdrometer/parsivel.py +453 -94
  40. cloudnetpy/instruments/disdrometer/thies.py +254 -87
  41. cloudnetpy/instruments/fd12p.py +201 -0
  42. cloudnetpy/instruments/galileo.py +65 -23
  43. cloudnetpy/instruments/hatpro.py +123 -49
  44. cloudnetpy/instruments/instruments.py +113 -1
  45. cloudnetpy/instruments/lufft.py +39 -17
  46. cloudnetpy/instruments/mira.py +268 -61
  47. cloudnetpy/instruments/mrr.py +187 -0
  48. cloudnetpy/instruments/nc_lidar.py +19 -8
  49. cloudnetpy/instruments/nc_radar.py +109 -55
  50. cloudnetpy/instruments/pollyxt.py +135 -51
  51. cloudnetpy/instruments/radiometrics.py +313 -59
  52. cloudnetpy/instruments/rain_e_h3.py +171 -0
  53. cloudnetpy/instruments/rpg.py +321 -189
  54. cloudnetpy/instruments/rpg_reader.py +74 -40
  55. cloudnetpy/instruments/toa5.py +49 -0
  56. cloudnetpy/instruments/vaisala.py +95 -343
  57. cloudnetpy/instruments/weather_station.py +774 -105
  58. cloudnetpy/metadata.py +90 -19
  59. cloudnetpy/model_evaluation/file_handler.py +55 -52
  60. cloudnetpy/model_evaluation/metadata.py +46 -20
  61. cloudnetpy/model_evaluation/model_metadata.py +1 -1
  62. cloudnetpy/model_evaluation/plotting/plot_tools.py +32 -37
  63. cloudnetpy/model_evaluation/plotting/plotting.py +327 -117
  64. cloudnetpy/model_evaluation/products/advance_methods.py +92 -83
  65. cloudnetpy/model_evaluation/products/grid_methods.py +88 -63
  66. cloudnetpy/model_evaluation/products/model_products.py +43 -35
  67. cloudnetpy/model_evaluation/products/observation_products.py +41 -35
  68. cloudnetpy/model_evaluation/products/product_resampling.py +17 -7
  69. cloudnetpy/model_evaluation/products/tools.py +29 -20
  70. cloudnetpy/model_evaluation/statistics/statistical_methods.py +30 -20
  71. cloudnetpy/model_evaluation/tests/e2e/conftest.py +3 -3
  72. cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +9 -5
  73. cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +15 -14
  74. cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +9 -5
  75. cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +15 -14
  76. cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +9 -5
  77. cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +15 -14
  78. cloudnetpy/model_evaluation/tests/unit/conftest.py +42 -41
  79. cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +41 -48
  80. cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +216 -194
  81. cloudnetpy/model_evaluation/tests/unit/test_model_products.py +23 -21
  82. cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +37 -38
  83. cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +43 -40
  84. cloudnetpy/model_evaluation/tests/unit/test_plotting.py +30 -36
  85. cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +68 -31
  86. cloudnetpy/model_evaluation/tests/unit/test_tools.py +33 -26
  87. cloudnetpy/model_evaluation/utils.py +2 -1
  88. cloudnetpy/output.py +170 -111
  89. cloudnetpy/plotting/__init__.py +2 -1
  90. cloudnetpy/plotting/plot_meta.py +562 -822
  91. cloudnetpy/plotting/plotting.py +1142 -704
  92. cloudnetpy/products/__init__.py +1 -0
  93. cloudnetpy/products/classification.py +370 -88
  94. cloudnetpy/products/der.py +85 -55
  95. cloudnetpy/products/drizzle.py +77 -34
  96. cloudnetpy/products/drizzle_error.py +15 -11
  97. cloudnetpy/products/drizzle_tools.py +79 -59
  98. cloudnetpy/products/epsilon.py +211 -0
  99. cloudnetpy/products/ier.py +27 -50
  100. cloudnetpy/products/iwc.py +55 -48
  101. cloudnetpy/products/lwc.py +96 -70
  102. cloudnetpy/products/mwr_tools.py +186 -0
  103. cloudnetpy/products/product_tools.py +170 -128
  104. cloudnetpy/utils.py +455 -240
  105. cloudnetpy/version.py +2 -2
  106. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/METADATA +44 -40
  107. cloudnetpy-1.87.3.dist-info/RECORD +127 -0
  108. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/WHEEL +1 -1
  109. cloudnetpy-1.87.3.dist-info/entry_points.txt +2 -0
  110. docs/source/conf.py +2 -2
  111. cloudnetpy/categorize/atmos.py +0 -361
  112. cloudnetpy/products/mwr_multi.py +0 -68
  113. cloudnetpy/products/mwr_single.py +0 -75
  114. cloudnetpy-1.49.9.dist-info/RECORD +0 -112
  115. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info/licenses}/LICENSE +0 -0
  116. {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: str) -> None:
19
- assert self.dataset is not None
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
- assert self.dataset is not None
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
- assert self.dataset is not None
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(f"No zenith angle found, assuming {zenith_angle} degrees")
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
- from cloudnetpy.instruments.instruments import Instrument
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: list[str]
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 RuntimeError:
39
- logging.warning(f"Can not find variable {key} from the input file")
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 = -17) -> None:
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 add_zenith_and_azimuth_angles(self) -> list:
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
- if "azimuth_velocity" in self.data:
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
- zenith = 90 - elevation
87
- is_stable_zenith = np.isclose(zenith, ma.median(zenith), atol=0.1)
88
- is_stable_azimuth = np.isclose(
89
- azimuth, azimuth_reference, atol=azimuth_tolerance
90
- )
91
- is_stable_profile = is_stable_zenith & is_stable_azimuth
92
- if ma.isMaskedArray(is_stable_profile):
93
- is_stable_profile[is_stable_profile.mask] = False
94
- n_removed = np.count_nonzero(~is_stable_profile)
95
- if n_removed >= len(zenith) - 1:
96
- raise ValidTimeStampError(
97
- "Less than two profiles with valid zenith / azimuth angles"
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
- f"Filtering {n_removed} profiles due to varying zenith / azimuth angle"
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
- for key in ("elevation", "azimuth_velocity"):
105
- if key in self.data:
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
- assert self.instrument is not None
154
+ if self.instrument is None:
155
+ msg = "Instrument not defined"
156
+ raise RuntimeError(msg)
112
157
  key = "radar_frequency"
113
- self.data[key] = CloudnetArray(self.instrument.frequency, key)
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
- except RuntimeError:
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: str):
137
- if self.date != date.split("-"):
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) -> list[str]:
141
- epoch = utils.get_epoch(self.dataset["time"].units)
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()