cloudnetpy 1.55.20__py3-none-any.whl → 1.55.22__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 (95) hide show
  1. cloudnetpy/categorize/atmos.py +46 -14
  2. cloudnetpy/categorize/atmos_utils.py +11 -1
  3. cloudnetpy/categorize/categorize.py +38 -21
  4. cloudnetpy/categorize/classify.py +31 -9
  5. cloudnetpy/categorize/containers.py +19 -7
  6. cloudnetpy/categorize/droplet.py +24 -8
  7. cloudnetpy/categorize/falling.py +17 -7
  8. cloudnetpy/categorize/freezing.py +19 -5
  9. cloudnetpy/categorize/insects.py +27 -14
  10. cloudnetpy/categorize/lidar.py +38 -36
  11. cloudnetpy/categorize/melting.py +19 -9
  12. cloudnetpy/categorize/model.py +28 -9
  13. cloudnetpy/categorize/mwr.py +4 -2
  14. cloudnetpy/categorize/radar.py +58 -22
  15. cloudnetpy/cloudnetarray.py +15 -6
  16. cloudnetpy/concat_lib.py +39 -16
  17. cloudnetpy/constants.py +7 -0
  18. cloudnetpy/datasource.py +39 -19
  19. cloudnetpy/instruments/basta.py +6 -2
  20. cloudnetpy/instruments/campbell_scientific.py +33 -16
  21. cloudnetpy/instruments/ceilo.py +30 -13
  22. cloudnetpy/instruments/ceilometer.py +76 -37
  23. cloudnetpy/instruments/cl61d.py +8 -3
  24. cloudnetpy/instruments/cloudnet_instrument.py +2 -1
  25. cloudnetpy/instruments/copernicus.py +27 -14
  26. cloudnetpy/instruments/disdrometer/common.py +51 -32
  27. cloudnetpy/instruments/disdrometer/parsivel.py +79 -48
  28. cloudnetpy/instruments/disdrometer/thies.py +10 -6
  29. cloudnetpy/instruments/galileo.py +23 -12
  30. cloudnetpy/instruments/hatpro.py +27 -11
  31. cloudnetpy/instruments/instruments.py +4 -1
  32. cloudnetpy/instruments/lufft.py +20 -11
  33. cloudnetpy/instruments/mira.py +60 -49
  34. cloudnetpy/instruments/mrr.py +31 -20
  35. cloudnetpy/instruments/nc_lidar.py +15 -6
  36. cloudnetpy/instruments/nc_radar.py +31 -22
  37. cloudnetpy/instruments/pollyxt.py +36 -21
  38. cloudnetpy/instruments/radiometrics.py +32 -18
  39. cloudnetpy/instruments/rpg.py +48 -22
  40. cloudnetpy/instruments/rpg_reader.py +39 -30
  41. cloudnetpy/instruments/vaisala.py +39 -27
  42. cloudnetpy/instruments/weather_station.py +15 -11
  43. cloudnetpy/metadata.py +3 -1
  44. cloudnetpy/model_evaluation/file_handler.py +31 -21
  45. cloudnetpy/model_evaluation/metadata.py +3 -1
  46. cloudnetpy/model_evaluation/model_metadata.py +1 -1
  47. cloudnetpy/model_evaluation/plotting/plot_tools.py +20 -15
  48. cloudnetpy/model_evaluation/plotting/plotting.py +114 -64
  49. cloudnetpy/model_evaluation/products/advance_methods.py +48 -28
  50. cloudnetpy/model_evaluation/products/grid_methods.py +44 -19
  51. cloudnetpy/model_evaluation/products/model_products.py +22 -18
  52. cloudnetpy/model_evaluation/products/observation_products.py +15 -9
  53. cloudnetpy/model_evaluation/products/product_resampling.py +14 -4
  54. cloudnetpy/model_evaluation/products/tools.py +16 -7
  55. cloudnetpy/model_evaluation/statistics/statistical_methods.py +28 -15
  56. cloudnetpy/model_evaluation/tests/e2e/conftest.py +3 -3
  57. cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +9 -5
  58. cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +14 -13
  59. cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +9 -5
  60. cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +14 -13
  61. cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +9 -5
  62. cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +14 -13
  63. cloudnetpy/model_evaluation/tests/unit/conftest.py +11 -11
  64. cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +33 -27
  65. cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +83 -83
  66. cloudnetpy/model_evaluation/tests/unit/test_model_products.py +23 -21
  67. cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +24 -25
  68. cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +40 -39
  69. cloudnetpy/model_evaluation/tests/unit/test_plotting.py +12 -11
  70. cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +30 -30
  71. cloudnetpy/model_evaluation/tests/unit/test_tools.py +18 -17
  72. cloudnetpy/model_evaluation/utils.py +3 -2
  73. cloudnetpy/output.py +45 -19
  74. cloudnetpy/plotting/plot_meta.py +35 -11
  75. cloudnetpy/plotting/plotting.py +172 -104
  76. cloudnetpy/products/classification.py +20 -8
  77. cloudnetpy/products/der.py +25 -10
  78. cloudnetpy/products/drizzle.py +41 -26
  79. cloudnetpy/products/drizzle_error.py +10 -5
  80. cloudnetpy/products/drizzle_tools.py +43 -24
  81. cloudnetpy/products/ier.py +10 -5
  82. cloudnetpy/products/iwc.py +16 -9
  83. cloudnetpy/products/lwc.py +34 -12
  84. cloudnetpy/products/mwr_multi.py +4 -1
  85. cloudnetpy/products/mwr_single.py +4 -1
  86. cloudnetpy/products/product_tools.py +33 -10
  87. cloudnetpy/utils.py +175 -74
  88. cloudnetpy/version.py +1 -1
  89. {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/METADATA +11 -10
  90. cloudnetpy-1.55.22.dist-info/RECORD +114 -0
  91. docs/source/conf.py +2 -2
  92. cloudnetpy-1.55.20.dist-info/RECORD +0 -114
  93. {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/LICENSE +0 -0
  94. {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/WHEEL +0 -0
  95. {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/top_level.txt +0 -0
@@ -13,7 +13,10 @@ class LufftCeilo(NcLidar):
13
13
  """Class for Lufft chm15k ceilometer."""
14
14
 
15
15
  def __init__(
16
- self, file_name: str, site_meta: dict, expected_date: str | None = None
16
+ self,
17
+ file_name: str,
18
+ site_meta: dict,
19
+ expected_date: str | None = None,
17
20
  ):
18
21
  super().__init__()
19
22
  self.file_name = file_name
@@ -31,32 +34,35 @@ class LufftCeilo(NcLidar):
31
34
  self._fetch_zenith_angle("zenith")
32
35
 
33
36
  def _fetch_beta_raw(self, calibration_factor: float | None = None) -> None:
34
- assert self.dataset is not None
35
37
  if calibration_factor is None:
36
38
  logging.warning("Using default calibration factor")
37
39
  calibration_factor = 3e-12
38
40
  beta_raw = self._getvar("beta_raw", "beta_att")
41
+ beta_raw = ma.masked_array(beta_raw)
39
42
  old_version = self._get_old_software_version()
40
43
  if old_version is not None:
41
44
  logging.warning(
42
- f"Software version {old_version}. Assuming data not range corrected."
45
+ "Software version %s. Assuming data not range corrected.",
46
+ old_version,
43
47
  )
44
48
  data_std = self._getvar("stddev")
45
49
  normalised_apd = self._get_nn()
46
- beta_raw *= utils.transpose(data_std / normalised_apd)
50
+ beta_raw *= utils.transpose(ma.masked_array(data_std / normalised_apd))
47
51
  beta_raw *= self.data["range"] ** 2
48
52
  beta_raw *= calibration_factor
49
53
  self.data["calibration_factor"] = float(calibration_factor)
50
54
  self.data["beta_raw"] = beta_raw
51
55
 
52
56
  def _get_old_software_version(self) -> str | None:
53
- assert self.dataset is not None
57
+ if self.dataset is None:
58
+ msg = "No dataset found"
59
+ raise RuntimeError(msg)
54
60
  version = self.dataset.software_version
55
61
  if len(str(version)) > 4:
56
62
  return None
57
63
  return version
58
64
 
59
- def _get_nn(self):
65
+ def _get_nn(self) -> float | ma.MaskedArray:
60
66
  nn1 = self._getvar("nn1", "NN1")
61
67
  median_nn1 = ma.median(nn1)
62
68
  # Parameters taken from the matlab code and should be verified
@@ -69,18 +75,21 @@ class LufftCeilo(NcLidar):
69
75
  return 1
70
76
  return step_factor ** (-(nn1 - reference) / scale)
71
77
 
72
- def _getvar(self, *args):
73
- assert self.dataset is not None
78
+ def _getvar(self, *args) -> float | ma.MaskedArray:
79
+ if self.dataset is None:
80
+ msg = "No dataset found"
81
+ raise RuntimeError(msg)
74
82
  for arg in args:
75
83
  if arg in self.dataset.variables:
76
84
  var = self.dataset.variables[arg]
77
85
  return var[0] if utils.isscalar(var) else var[:]
78
- raise ValueError("Unknown variable")
86
+ msg = f"Unable to find variable {args[0]}"
87
+ raise ValueError(msg)
79
88
 
80
- def _fetch_attributes(self):
89
+ def _fetch_attributes(self) -> None:
81
90
  self.serial_number = getattr(self.dataset, "device_name", None)
82
91
  if self.serial_number is None:
83
- self.serial_number = getattr(self.dataset, "source")
92
+ self.serial_number = getattr(self.dataset, "source", "")
84
93
  self.instrument = (
85
94
  instruments.CHM15KX
86
95
  if self.serial_number.startswith("CHX")
@@ -2,7 +2,7 @@
2
2
  import logging
3
3
  import os
4
4
  from collections import OrderedDict
5
- from tempfile import TemporaryDirectory
5
+ from tempfile import NamedTemporaryFile, TemporaryDirectory
6
6
 
7
7
  from numpy import ma
8
8
 
@@ -27,6 +27,7 @@ def mira2nc(
27
27
  steps.
28
28
 
29
29
  Args:
30
+ ----
30
31
  raw_mira: Filename of a daily MIRA .mmclx or .zncfile. Can be also a folder
31
32
  containing several non-concatenated .mmclx or .znc files from one day
32
33
  or list of files. znc files take precedence because they are the newer
@@ -38,15 +39,18 @@ def mira2nc(
38
39
  date: Expected date as YYYY-MM-DD of all profiles in the file.
39
40
 
40
41
  Returns:
42
+ -------
41
43
  UUID of the generated file.
42
44
 
43
45
  Raises:
46
+ ------
44
47
  ValidTimeStampError: No valid timestamps found.
45
48
  FileNotFoundError: No suitable input files found.
46
49
  ValueError: Wrong suffix in input file(s).
47
50
  TypeError: Mixed mmclx and znc files.
48
51
 
49
52
  Examples:
53
+ --------
50
54
  >>> from cloudnetpy.instruments import mira2nc
51
55
  >>> site_meta = {'name': 'Vehmasmaki'}
52
56
  >>> mira2nc('raw_radar.mmclx', 'radar.nc', site_meta)
@@ -55,7 +59,6 @@ def mira2nc(
55
59
  >>> mira2nc('/one/day/of/mira/znc/files/', 'radar.nc', site_meta)
56
60
 
57
61
  """
58
-
59
62
  with TemporaryDirectory() as temp_dir:
60
63
  input_filename, keymap = _parse_input_files(raw_mira, temp_dir)
61
64
 
@@ -79,14 +82,14 @@ def mira2nc(
79
82
  mira.test_if_all_masked()
80
83
  attributes = output.add_time_attribute(ATTRIBUTES, mira.date)
81
84
  output.update_attributes(mira.data, attributes)
82
- uuid = output.save_level1b(mira, output_file, uuid)
83
- return uuid
85
+ return output.save_level1b(mira, output_file, uuid)
84
86
 
85
87
 
86
88
  class Mira(NcRadar):
87
89
  """Class for MIRA-35 raw radar data. Child of NcRadar().
88
90
 
89
91
  Args:
92
+ ----
90
93
  full_path: Filename of a daily MIRA .mmclx NetCDF file.
91
94
  site_meta: Site properties in a dictionary. Required keys are: `name`.
92
95
 
@@ -115,7 +118,7 @@ class Mira(NcRadar):
115
118
  time_stamps = self.getvar("time")
116
119
  return utils.seconds2date(float(time_stamps[0]), self.epoch)[:3]
117
120
 
118
- def screen_invalid_ldr(self):
121
+ def screen_invalid_ldr(self) -> None:
119
122
  """Masks LDR in MIRA STSR mode data.
120
123
  Is there a better way to identify this mode?
121
124
  """
@@ -125,53 +128,60 @@ class Mira(NcRadar):
125
128
  if ma.mean(ldr) > 0:
126
129
  logging.warning(
127
130
  "LDR values suspiciously high. Mira in STSR mode? "
128
- "Screening all LDR for now."
131
+ "Screening all LDR for now.",
129
132
  )
130
133
  self.data["ldr"].data[:] = ma.masked
131
134
 
132
135
 
133
136
  def _parse_input_files(input_files: str | list[str], temp_dir: str) -> tuple:
134
137
  if isinstance(input_files, list) or os.path.isdir(input_files):
135
- input_filename = f"{temp_dir}/tmp.nc"
136
- if isinstance(input_files, list):
137
- valid_files = sorted(input_files)
138
- else:
139
- valid_files = utils.get_sorted_filenames(input_files, ".znc")
140
- if not valid_files:
141
- valid_files = utils.get_sorted_filenames(input_files, ".mmclx")
142
-
143
- if not valid_files:
144
- raise FileNotFoundError(
145
- "Neither znc nor mmclx files found "
146
- + f"{input_files}. Please check your input."
147
- )
148
-
149
- valid_files = utils.get_files_with_common_range(valid_files)
150
- filetypes = list({f.split(".")[-1].lower() for f in valid_files})
138
+ with NamedTemporaryFile(
139
+ dir=temp_dir,
140
+ suffix=".nc",
141
+ delete=False,
142
+ ) as temp_file:
143
+ input_filename = temp_file.name
144
+ if isinstance(input_files, list):
145
+ valid_files = sorted(input_files)
146
+ else:
147
+ valid_files = utils.get_sorted_filenames(input_files, ".znc")
148
+ if not valid_files:
149
+ valid_files = utils.get_sorted_filenames(input_files, ".mmclx")
151
150
 
152
- if len(filetypes) > 1:
153
- raise TypeError(
154
- "mira2nc only supports a singlefile type as input",
155
- "either mmclx or znc",
151
+ if not valid_files:
152
+ msg = (
153
+ (
154
+ f"Neither znc nor mmclx files found {input_files}. "
155
+ f"Please check your input."
156
+ ),
157
+ )
158
+ raise FileNotFoundError(msg)
159
+
160
+ valid_files = utils.get_files_with_common_range(valid_files)
161
+ filetypes = list({f.split(".")[-1].lower() for f in valid_files})
162
+
163
+ if len(filetypes) > 1:
164
+ err_msg = "Mixed mmclx and znc files. Please use only one filetype."
165
+ raise TypeError(err_msg)
166
+
167
+ keymap = _get_keymap(filetypes[0])
168
+
169
+ variables = list(keymap.keys())
170
+ concat_lib.concatenate_files(
171
+ valid_files,
172
+ input_filename,
173
+ variables=variables,
174
+ ignore=_get_ignored_variables(filetypes[0]),
175
+ # It's somewhat risky to use varying nfft values as the velocity
176
+ # resolution may differ, but this enables concatenation when switching
177
+ # between different nfft configurations. Spectral data is ignored
178
+ # anyway for now.
179
+ allow_difference=[
180
+ "nave",
181
+ "ovl",
182
+ "nfft",
183
+ ],
156
184
  )
157
-
158
- keymap = _get_keymap(filetypes[0])
159
-
160
- variables = list(keymap.keys())
161
- concat_lib.concatenate_files(
162
- valid_files,
163
- input_filename,
164
- variables=variables,
165
- ignore=_get_ignored_variables(filetypes[0]),
166
- # It's somewhat risky to use varying nfft values as the velocity resolution
167
- # may differ, but this enables concatenation when switching between
168
- # different nfft configurations. Spectral data is ignored anyway for now.
169
- allow_difference=[
170
- "nave",
171
- "ovl",
172
- "nfft",
173
- ],
174
- )
175
185
  else:
176
186
  input_filename = input_files
177
187
  keymap = _get_keymap(input_filename.split(".")[-1])
@@ -193,8 +203,8 @@ def _get_ignored_variables(filetype: str) -> list | None:
193
203
 
194
204
  def _get_keymap(filetype: str) -> dict:
195
205
  """Returns a dictionary mapping the variables in the raw data to the processed
196
- Cloudnet file."""
197
-
206
+ Cloudnet file.
207
+ """
198
208
  _check_file_type(filetype)
199
209
 
200
210
  # Order is relevant with the new znc files from STSR radar
@@ -218,7 +228,7 @@ def _get_keymap(filetype: str) -> dict:
218
228
  ("nave", "nave"),
219
229
  ("prf", "prf"),
220
230
  ("rg0", "rg0"),
221
- ]
231
+ ],
222
232
  ),
223
233
  "mmclx": {
224
234
  "Zg": "Zh",
@@ -240,10 +250,11 @@ def _get_keymap(filetype: str) -> dict:
240
250
  return keymaps.get(filetype.lower(), keymaps["mmclx"])
241
251
 
242
252
 
243
- def _check_file_type(filetype: str):
253
+ def _check_file_type(filetype: str) -> None:
244
254
  known_filetypes = ["znc", "mmclx"]
245
255
  if filetype.lower() not in known_filetypes:
246
- raise ValueError(f"Filetype must be one of {known_filetypes}")
256
+ msg = f"Filetype must be one of {known_filetypes}"
257
+ raise ValueError(msg)
247
258
 
248
259
 
249
260
  ATTRIBUTES = {
@@ -4,7 +4,7 @@ import re
4
4
  from collections.abc import Iterable
5
5
  from os import PathLike
6
6
  from pathlib import Path
7
- from tempfile import TemporaryDirectory
7
+ from tempfile import NamedTemporaryFile, TemporaryDirectory
8
8
  from uuid import UUID
9
9
 
10
10
  import netCDF4
@@ -29,6 +29,7 @@ def mrr2nc(
29
29
  contains only the relevant data.
30
30
 
31
31
  Args:
32
+ ----
32
33
  input_file: Filename of a daily MMR-PRO .nc file, path to directory
33
34
  containing several non-concatenated .nc files from one day, or list
34
35
  of filenames.
@@ -39,17 +40,19 @@ def mrr2nc(
39
40
  date: Expected date as YYYY-MM-DD of all profiles in the file.
40
41
 
41
42
  Returns:
43
+ -------
42
44
  UUID of the generated file.
43
45
 
44
46
  Raises:
47
+ ------
45
48
  ValidTimeStampError: No valid timestamps found.
46
49
 
47
50
  Examples:
51
+ --------
48
52
  >>> from cloudnetpy.instruments import mira2nc
49
53
  >>> site_meta = {'name': 'LIM', 'latitude': 51.333, 'longitude': 12.389}
50
54
  >>> mrr2nc('input.nc', 'output.nc', site_meta)
51
55
  """
52
-
53
56
  if isinstance(uuid, str):
54
57
  uuid = UUID(uuid)
55
58
  if isinstance(date, str):
@@ -70,26 +73,32 @@ def mrr2nc(
70
73
  with netCDF4.Dataset(file):
71
74
  yield file
72
75
  except OSError:
73
- logging.warning(f"Skipping invalid file: {file}")
74
-
75
- def concat_files(temp_dir: str, files: Iterable[PathLike | str]) -> str:
76
- tmp_filename = f"{temp_dir}/tmp.nc"
77
- variables = list(keymap.keys()) + ["elevation"]
78
- valid_files = list(valid_nc_files(files))
79
- concat_lib.concatenate_files(
80
- valid_files,
81
- tmp_filename,
82
- variables=variables,
83
- ignore=["time_coverage_start", "time_coverage_end"],
84
- )
85
- return tmp_filename
76
+ logging.warning("Skipping invalid file: %s", file)
77
+
78
+ def concat_files(dir_name: str, files: Iterable[PathLike | str]) -> str:
79
+ with NamedTemporaryFile(
80
+ dir=dir_name,
81
+ suffix=".nc",
82
+ delete=False,
83
+ ) as temp_file:
84
+ tmp_filename = temp_file.name
85
+ variables = [*keymap.keys(), "elevation"]
86
+ valid_files = list(valid_nc_files(files))
87
+ concat_lib.concatenate_files(
88
+ valid_files,
89
+ tmp_filename,
90
+ variables=variables,
91
+ ignore=["time_coverage_start", "time_coverage_end"],
92
+ )
93
+ return tmp_filename
86
94
 
87
95
  with TemporaryDirectory() as temp_dir:
88
96
  if isinstance(input_file, PathLike | str):
89
97
  path = Path(input_file)
90
98
  if path.is_dir():
91
99
  input_file = concat_files(
92
- temp_dir, (p for p in path.iterdir() if p.suffix.lower() == ".nc")
100
+ temp_dir,
101
+ (p for p in path.iterdir() if p.suffix.lower() == ".nc"),
93
102
  )
94
103
  else:
95
104
  input_file = concat_files(temp_dir, input_file)
@@ -108,14 +117,14 @@ def mrr2nc(
108
117
  mrr.sort_timestamps()
109
118
  attributes = output.add_time_attribute(ATTRIBUTES, mrr.date)
110
119
  output.update_attributes(mrr.data, attributes)
111
- uuid = output.save_level1b(mrr, output_file, uuid)
112
- return uuid
120
+ return output.save_level1b(mrr, output_file, uuid)
113
121
 
114
122
 
115
123
  class MrrPro(NcRadar):
116
124
  """Class for MRR-PRO raw data. Child of NcRadar().
117
125
 
118
126
  Args:
127
+ ----
119
128
  full_path: MRR-PRO netCDF filename.
120
129
  site_meta: Site properties in a dictionary. Required keys are `name`,
121
130
  `latitude`, `longitude` and `altitude`.
@@ -128,7 +137,9 @@ class MrrPro(NcRadar):
128
137
  super().__init__(full_path, site_meta)
129
138
  self.instrument = instruments.MRR_PRO
130
139
  if m := re.search(
131
- r"serial number:\s*(\w+)", self.dataset.instrument_name, re.I
140
+ r"serial number:\s*(\w+)",
141
+ self.dataset.instrument_name,
142
+ re.I,
132
143
  ):
133
144
  self.serial_number = m[1]
134
145
 
@@ -136,7 +147,7 @@ class MrrPro(NcRadar):
136
147
  time_stamps = self.getvar("time")
137
148
  return utils.seconds2date(time_stamps[0], (1970, 1, 1))[:3]
138
149
 
139
- def fix_units(self):
150
+ def fix_units(self) -> None:
140
151
  self.data["v"].data *= -1 # towards -> away from instrument
141
152
  self.data["rainfall_rate"].data /= 3600000 # mm h-1 -> m s-1
142
153
  self.data["lwc"].data *= 0.001 # g m-3 -> kg m-3
@@ -1,13 +1,16 @@
1
1
  """Module with a class for Lufft chm15k ceilometer."""
2
2
  import logging
3
+ from typing import TYPE_CHECKING, Literal
3
4
 
4
- import netCDF4
5
5
  import numpy as np
6
6
  from numpy import ma
7
7
 
8
8
  from cloudnetpy import utils
9
9
  from cloudnetpy.instruments.ceilometer import Ceilometer
10
10
 
11
+ if TYPE_CHECKING:
12
+ import netCDF4
13
+
11
14
 
12
15
  class NcLidar(Ceilometer):
13
16
  """Class for all lidars using netCDF files."""
@@ -16,25 +19,31 @@ class NcLidar(Ceilometer):
16
19
  super().__init__()
17
20
  self.dataset: netCDF4.Dataset | None = None
18
21
 
19
- def _fetch_range(self, reference: str) -> None:
20
- assert self.dataset is not None
22
+ def _fetch_range(self, reference: Literal["upper", "lower"]) -> None:
23
+ if self.dataset is None:
24
+ msg = "No dataset found"
25
+ raise RuntimeError(msg)
21
26
  range_instrument = self.dataset.variables["range"][:]
22
27
  self.data["range"] = utils.edges2mid(range_instrument, reference)
23
28
 
24
29
  def _fetch_time_and_date(self) -> None:
25
- assert self.dataset is not None
30
+ if self.dataset is None:
31
+ msg = "No dataset found"
32
+ raise RuntimeError(msg)
26
33
  time = self.dataset.variables["time"]
27
34
  self.data["time"] = time[:]
28
35
  epoch = utils.get_epoch(time.units)
29
36
  self.get_date_and_time(epoch)
30
37
 
31
38
  def _fetch_zenith_angle(self, key: str, default: float = 3.0) -> None:
32
- assert self.dataset is not None
39
+ if self.dataset is None:
40
+ msg = "No dataset found"
41
+ raise RuntimeError(msg)
33
42
  if key in self.dataset.variables:
34
43
  zenith_angle = ma.median(self.dataset.variables[key][:])
35
44
  else:
36
45
  zenith_angle = float(default)
37
- logging.warning(f"No zenith angle found, assuming {zenith_angle} degrees")
46
+ logging.warning("No zenith angle found, assuming %s degrees", zenith_angle)
38
47
  if zenith_angle == 0:
39
48
  logging.warning("Zenith angle 0 degrees - risk of specular reflection")
40
49
  self.data["zenith_angle"] = np.array(zenith_angle)
@@ -1,6 +1,7 @@
1
1
  """Module for reading raw cloud radar data."""
2
2
  import logging
3
3
  from os import PathLike
4
+ from typing import TYPE_CHECKING
4
5
 
5
6
  import numpy as np
6
7
  from numpy import ma
@@ -10,17 +11,21 @@ from cloudnetpy.cloudnetarray import CloudnetArray
10
11
  from cloudnetpy.datasource import DataSource
11
12
  from cloudnetpy.exceptions import RadarDataError, ValidTimeStampError
12
13
  from cloudnetpy.instruments.cloudnet_instrument import CloudnetInstrument
13
- from cloudnetpy.instruments.instruments import Instrument
14
+
15
+ if TYPE_CHECKING:
16
+ from cloudnetpy.instruments.instruments import Instrument
14
17
 
15
18
 
16
19
  class NcRadar(DataSource, CloudnetInstrument):
17
20
  """Class for radars providing netCDF files. Child of DataSource().
18
21
 
19
22
  Args:
23
+ ----
20
24
  full_path: Filename of a radar-produced netCDF file.
21
25
  site_meta: Some metadata of the site.
22
26
 
23
27
  Notes:
28
+ -----
24
29
  Used with BASTA, MIRA and Copernicus radars.
25
30
  """
26
31
 
@@ -37,7 +42,7 @@ class NcRadar(DataSource, CloudnetInstrument):
37
42
  try:
38
43
  array = self.getvar(key)
39
44
  except RuntimeError:
40
- logging.warning(f"Can not find variable {key} from the input file")
45
+ logging.warning("Can not find variable %s from the input file", key)
41
46
  continue
42
47
  array = np.array(array) if utils.isscalar(array) else array
43
48
  array[~np.isfinite(array)] = ma.masked
@@ -46,7 +51,7 @@ class NcRadar(DataSource, CloudnetInstrument):
46
51
  def add_time_and_range(self) -> None:
47
52
  """Adds time and range."""
48
53
  range_instru = np.array(
49
- self.getvar("range", "height")
54
+ self.getvar("range", "height"),
50
55
  ) # "height" in old BASTA files
51
56
  time = np.array(self.time)
52
57
  self.append_data(range_instru, "range")
@@ -78,7 +83,7 @@ class NcRadar(DataSource, CloudnetInstrument):
78
83
  cloudnet_array.mask_indices(z_mask)
79
84
  cloudnet_array.mask_indices(v_mask)
80
85
 
81
- def mask_first_range_gates(self, range_limit: float = 150):
86
+ def mask_first_range_gates(self, range_limit: float = 150) -> None:
82
87
  """Masks first range gates."""
83
88
  if "v" not in self.data or "range" not in self.data:
84
89
  return
@@ -88,13 +93,11 @@ class NcRadar(DataSource, CloudnetInstrument):
88
93
 
89
94
  def add_zenith_and_azimuth_angles(self) -> list:
90
95
  """Adds non-varying instrument zenith and azimuth angles and returns valid
91
- time indices."""
96
+ time indices.
97
+ """
92
98
  if "azimuth_velocity" in self.data:
93
99
  azimuth = self.data["azimuth_velocity"].data
94
- if np.all(azimuth == azimuth[0]):
95
- azimuth_reference = azimuth[0]
96
- else:
97
- azimuth_reference = 0
100
+ azimuth_reference = azimuth[0] if np.all(azimuth == azimuth[0]) else 0
98
101
  azimuth_tolerance = 1e-6
99
102
  else:
100
103
  azimuth = self.data["azimuth_angle"].data
@@ -105,19 +108,22 @@ class NcRadar(DataSource, CloudnetInstrument):
105
108
  zenith = 90 - elevation
106
109
  is_stable_zenith = np.isclose(zenith, ma.median(zenith), atol=0.1)
107
110
  is_stable_azimuth = np.isclose(
108
- azimuth, azimuth_reference, atol=azimuth_tolerance
111
+ azimuth,
112
+ azimuth_reference,
113
+ atol=azimuth_tolerance,
109
114
  )
110
115
  is_stable_profile = is_stable_zenith & is_stable_azimuth
111
116
  if ma.isMaskedArray(is_stable_profile):
112
117
  is_stable_profile[is_stable_profile.mask] = False
113
118
  n_removed = np.count_nonzero(~is_stable_profile)
114
119
  if n_removed >= len(zenith) - 1:
115
- raise ValidTimeStampError(
116
- "Less than two profiles with valid zenith / azimuth angles"
117
- )
120
+ msg = "Less than two profiles with valid zenith / azimuth angles"
121
+ raise ValidTimeStampError(msg)
122
+
118
123
  if n_removed > 0:
119
124
  logging.warning(
120
- f"Filtering {n_removed} profiles due to varying zenith / azimuth angle"
125
+ "Filtering %s profiles due to varying zenith / azimuth angle",
126
+ n_removed,
121
127
  )
122
128
  self.append_data(zenith, "zenith_angle")
123
129
  for key in ("elevation", "azimuth_velocity"):
@@ -125,9 +131,11 @@ class NcRadar(DataSource, CloudnetInstrument):
125
131
  del self.data[key]
126
132
  return list(is_stable_profile)
127
133
 
128
- def add_radar_specific_variables(self):
134
+ def add_radar_specific_variables(self) -> None:
129
135
  """Adds radar specific variables."""
130
- assert self.instrument is not None
136
+ if self.instrument is None:
137
+ msg = "Instrument not defined"
138
+ raise RuntimeError(msg)
131
139
  key = "radar_frequency"
132
140
  self.data[key] = CloudnetArray(self.instrument.frequency, key)
133
141
  try:
@@ -140,26 +148,27 @@ class NcRadar(DataSource, CloudnetInstrument):
140
148
  except RuntimeError:
141
149
  logging.warning("Unable to find nyquist_velocity")
142
150
 
143
- def test_if_all_masked(self):
151
+ def test_if_all_masked(self) -> None:
144
152
  """Tests if all data are masked."""
145
153
  if not np.any(~self.data["v"][:].mask):
146
- raise RadarDataError("All radar data are masked")
154
+ msg = "All radar data are masked"
155
+ raise RadarDataError(msg)
147
156
 
148
157
 
149
158
  class ChilboltonRadar(NcRadar):
150
159
  """Class for Chilbolton cloud radars Galileo and Copernicus."""
151
160
 
152
- def __init__(self, full_path: str, site_meta: dict):
161
+ def __init__(self, full_path: str, site_meta: dict) -> None:
153
162
  super().__init__(full_path, site_meta)
154
163
  self.date = self._init_date()
155
164
 
156
- def add_nyquist_velocity(self, keymap: dict):
165
+ def add_nyquist_velocity(self, keymap: dict) -> None:
157
166
  """Adds nyquist velocity."""
158
- key = [key for key, value in keymap.items() if value == "v"][0]
167
+ key = [key for key, value in keymap.items() if value == "v"][0] # noqa: RUF015
159
168
  folding_velocity = self.dataset.variables[key].folding_velocity
160
169
  self.append_data(np.array(folding_velocity), "nyquist_velocity")
161
170
 
162
- def check_date(self, date: str):
171
+ def check_date(self, date: str) -> None:
163
172
  if self.date != date.split("-"):
164
173
  raise ValidTimeStampError
165
174