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
@@ -22,10 +22,10 @@ def pollyxt2nc(
22
22
  uuid: str | None = None,
23
23
  date: str | None = None,
24
24
  ) -> str:
25
- """
26
- Converts PollyXT Raman lidar data into Cloudnet Level 1b netCDF file.
25
+ """Converts PollyXT Raman lidar data into Cloudnet Level 1b netCDF file.
27
26
 
28
27
  Args:
28
+ ----
29
29
  input_folder: Path to pollyxt netCDF files.
30
30
  output_file: Output filename.
31
31
  site_meta: Dictionary containing information about the site with keys:
@@ -40,9 +40,11 @@ def pollyxt2nc(
40
40
  date: Expected date of the measurements as YYYY-MM-DD.
41
41
 
42
42
  Returns:
43
+ -------
43
44
  UUID of the generated file.
44
45
 
45
46
  Examples:
47
+ --------
46
48
  >>> from cloudnetpy.instruments import pollyxt2nc
47
49
  >>> site_meta = {'name': 'Mindelo', 'altitude': 13, 'zenith_angle': 6,
48
50
  'snr_limit': 3}
@@ -61,8 +63,7 @@ def pollyxt2nc(
61
63
  attributes = output.add_time_attribute(ATTRIBUTES, polly.date)
62
64
  output.update_attributes(polly.data, attributes)
63
65
  polly.add_snr_info("beta", snr_limit)
64
- uuid = output.save_level1b(polly, output_file, uuid)
65
- return uuid
66
+ return output.save_level1b(polly, output_file, uuid)
66
67
 
67
68
 
68
69
  class PollyXt(Ceilometer):
@@ -72,16 +73,17 @@ class PollyXt(Ceilometer):
72
73
  self.expected_date = expected_date
73
74
  self.instrument = instruments.POLLYXT
74
75
 
75
- def mask_nan_values(self):
76
+ def mask_nan_values(self) -> None:
76
77
  for array in self.data.values():
77
78
  if getattr(array, "ndim", 0) > 0:
78
79
  array[np.isnan(array)] = ma.masked
79
80
 
80
- def calc_screened_products(self, snr_limit: float = 5.0):
81
+ def calc_screened_products(self, snr_limit: float = 5.0) -> None:
81
82
  keys = ("beta", "depolarisation")
82
83
  for key in keys:
83
84
  self.data[key] = ma.masked_where(
84
- self.data["snr"] < snr_limit, self.data[f"{key}_raw"]
85
+ self.data["snr"] < snr_limit,
86
+ self.data[f"{key}_raw"],
85
87
  )
86
88
  self.data["depolarisation"][self.data["depolarisation"] > 1] = ma.masked
87
89
  self.data["depolarisation"][self.data["depolarisation"] < 0] = ma.masked
@@ -99,19 +101,21 @@ class PollyXt(Ceilometer):
99
101
  bsc_files.sort()
100
102
  depol_files.sort()
101
103
  if not bsc_files:
102
- raise RuntimeError("No pollyxt files found")
104
+ msg = "No pollyxt bsc files found"
105
+ raise RuntimeError(msg)
103
106
  if len(bsc_files) != len(depol_files):
104
- raise InconsistentDataError(
105
- "Inconsistent number of pollyxt bsc / depol files"
106
- )
107
+ msg = "Inconsistent number of pollyxt bsc / depol files"
108
+ raise InconsistentDataError(msg)
107
109
  self._fetch_attributes(bsc_files[0])
108
110
  self.data["range"] = _read_array_from_multiple_files(
109
- bsc_files, depol_files, "height"
111
+ bsc_files,
112
+ depol_files,
113
+ "height",
110
114
  )
111
115
  calibration_factors: np.ndarray = np.array([])
112
116
  beta_channel = self._get_valid_beta_channel(bsc_files)
113
117
  bsc_key = f"attenuated_backscatter_{beta_channel}nm"
114
- for bsc_file, depol_file in zip(bsc_files, depol_files):
118
+ for bsc_file, depol_file in zip(bsc_files, depol_files, strict=True):
115
119
  with (
116
120
  netCDF4.Dataset(bsc_file, "r") as nc_bsc,
117
121
  netCDF4.Dataset(depol_file, "r") as nc_depol,
@@ -119,11 +123,14 @@ class PollyXt(Ceilometer):
119
123
  epoch = utils.get_epoch(nc_bsc["time"].unit)
120
124
  try:
121
125
  time = np.array(
122
- _read_array_from_file_pair(nc_bsc, nc_depol, "time")
126
+ _read_array_from_file_pair(nc_bsc, nc_depol, "time"),
123
127
  )
124
128
  except AssertionError as err:
125
129
  logging.warning(
126
- f"Ignoring files '{nc_bsc}' and '{nc_depol}': {err}"
130
+ "Ignoring files '%s' and '%s': %s",
131
+ nc_bsc,
132
+ nc_depol,
133
+ err,
127
134
  )
128
135
  continue
129
136
  beta_raw = nc_bsc.variables[bsc_key][:]
@@ -132,6 +139,7 @@ class PollyXt(Ceilometer):
132
139
  for array, key in zip(
133
140
  [beta_raw, depol_raw, time, snr],
134
141
  ["beta_raw", "depolarisation_raw", "time", "snr"],
142
+ strict=True,
135
143
  ):
136
144
  self.data = utils.append_data(self.data, key, array)
137
145
  calibration_factor = nc_bsc.variables[
@@ -139,7 +147,7 @@ class PollyXt(Ceilometer):
139
147
  ].Lidar_calibration_constant_used
140
148
  calibration_factor = np.repeat(calibration_factor, len(time))
141
149
  calibration_factors = np.concatenate(
142
- [calibration_factors, calibration_factor]
150
+ [calibration_factors, calibration_factor],
143
151
  )
144
152
  self.data["calibration_factor"] = calibration_factors
145
153
  return epoch
@@ -153,11 +161,16 @@ class PollyXt(Ceilometer):
153
161
  if not _only_zeros_or_masked(beta):
154
162
  if channel != polly_channels[0]:
155
163
  logging.warning(
156
- f"Using {channel}nm pollyXT channel for backscatter"
164
+ "Using %s nm pollyXT channel for backscatter",
165
+ channel,
157
166
  )
158
- self.instrument.wavelength = float(channel) # type: ignore
167
+ if self.instrument is None:
168
+ msg = "No instrument defined"
169
+ raise RuntimeError(msg)
170
+ self.instrument.wavelength = float(channel)
159
171
  return channel
160
- raise ValidTimeStampError("No functional pollyXT backscatter channels found")
172
+ msg = "No functional pollyXT backscatter channels found"
173
+ raise ValidTimeStampError(msg)
161
174
 
162
175
  def _fetch_attributes(self, file: str) -> None:
163
176
  with netCDF4.Dataset(file, "r") as nc:
@@ -167,7 +180,7 @@ class PollyXt(Ceilometer):
167
180
 
168
181
  def _read_array_from_multiple_files(files1: list, files2: list, key) -> np.ndarray:
169
182
  array: np.ndarray = np.array([])
170
- for ind, (file1, file2) in enumerate(zip(files1, files2)):
183
+ for ind, (file1, file2) in enumerate(zip(files1, files2, strict=True)):
171
184
  with netCDF4.Dataset(file1, "r") as nc1, netCDF4.Dataset(file2, "r") as nc2:
172
185
  array1 = _read_array_from_file_pair(nc1, nc2, key)
173
186
  if ind == 0:
@@ -177,7 +190,9 @@ def _read_array_from_multiple_files(files1: list, files2: list, key) -> np.ndarr
177
190
 
178
191
 
179
192
  def _read_array_from_file_pair(
180
- nc_file1: netCDF4.Dataset, nc_file2: netCDF4.Dataset, key: str
193
+ nc_file1: netCDF4.Dataset,
194
+ nc_file2: netCDF4.Dataset,
195
+ key: str,
181
196
  ) -> np.ndarray:
182
197
  array1 = nc_file1.variables[key][:]
183
198
  array2 = nc_file2.variables[key][:]
@@ -24,6 +24,7 @@ def radiometrics2nc(
24
24
  """Converts Radiometrics .csv file into Cloudnet Level 1b netCDF file.
25
25
 
26
26
  Args:
27
+ ----
27
28
  full_path: Input file name or folder containing multiple input files.
28
29
  output_file: Output file name, e.g. 'radiometrics.nc'.
29
30
  site_meta: Dictionary containing information about the site and instrument.
@@ -33,9 +34,11 @@ def radiometrics2nc(
33
34
  date: Expected date as YYYY-MM-DD of all profiles in the file.
34
35
 
35
36
  Returns:
37
+ -------
36
38
  UUID of the generated file.
37
39
 
38
40
  Examples:
41
+ --------
39
42
  >>> from cloudnetpy.instruments import radiometrics2nc
40
43
  >>> site_meta = {'name': 'Soverato', 'altitude': 21}
41
44
  >>> radiometrics2nc('radiometrics.csv', 'radiometrics.nc', site_meta)
@@ -61,11 +64,12 @@ def radiometrics2nc(
61
64
  radiometrics.time_to_fractional_hours()
62
65
  radiometrics.data_to_cloudnet_arrays()
63
66
  radiometrics.add_meta()
64
- assert radiometrics.date is not None
67
+ if radiometrics.date is None:
68
+ msg = "Failed to find valid timestamps from Radiometrics file(s)."
69
+ raise ValidTimeStampError(msg)
65
70
  attributes = output.add_time_attribute({}, radiometrics.date)
66
71
  output.update_attributes(radiometrics.data, attributes)
67
- uuid = output.save_level1b(radiometrics, output_file, uuid)
68
- return uuid
72
+ return output.save_level1b(radiometrics, output_file, uuid)
69
73
 
70
74
 
71
75
  class Record(NamedTuple):
@@ -79,7 +83,8 @@ class Record(NamedTuple):
79
83
  class Radiometrics:
80
84
  """Reader for level 2 files of Radiometrics microwave radiometers.
81
85
 
82
- References:
86
+ References
87
+ ----------
83
88
  Radiometrics (2008). Profiler Operator's Manual: MP-3000A, MP-2500A,
84
89
  MP-1500A, MP-183A.
85
90
  """
@@ -90,16 +95,18 @@ class Radiometrics:
90
95
  self.data: dict = {}
91
96
  self.instrument = instruments.RADIOMETRICS
92
97
 
93
- def read_raw_data(self):
98
+ def read_raw_data(self) -> None:
94
99
  """Reads Radiometrics raw data."""
95
100
  record_columns = {}
96
101
  unknown_record_types = set()
97
102
  rows = []
98
- with open(self.filename, mode="r", encoding="utf8") as infile:
103
+ with open(self.filename, encoding="utf8") as infile:
99
104
  reader = csv.reader(infile)
100
105
  for row in reader:
101
106
  if row[0] == "Record":
102
- assert row[1] == "Date/Time"
107
+ if row[1] != "Date/Time":
108
+ msg = "Unexpected header in Radiometrics file"
109
+ raise RuntimeError(msg)
103
110
  record_type = int(row[2])
104
111
  record_columns[record_type] = row[3:]
105
112
  else:
@@ -109,7 +116,7 @@ class Radiometrics:
109
116
  column_names = record_columns.get(block_type)
110
117
  if column_names is None:
111
118
  if record_type not in unknown_record_types:
112
- logging.info(f"Skipping unknown record type {record_type}")
119
+ logging.info("Skipping unknown record type %d", record_type)
113
120
  unknown_record_types.add(record_type)
114
121
  continue
115
122
  record = Record(
@@ -117,7 +124,7 @@ class Radiometrics:
117
124
  timestamp=_parse_datetime(row[1]),
118
125
  block_type=block_type,
119
126
  block_index=block_index,
120
- values=dict(zip(column_names, row[3:])),
127
+ values=dict(zip(column_names, row[3:], strict=True)),
121
128
  )
122
129
  rows.append(record)
123
130
 
@@ -130,7 +137,7 @@ class Radiometrics:
130
137
  if data_row.block_index == 0:
131
138
  self.raw_data.append(data_row)
132
139
 
133
- def read_data(self):
140
+ def read_data(self) -> None:
134
141
  """Reads values."""
135
142
  times = []
136
143
  lwps = []
@@ -165,7 +172,7 @@ class RadiometricsCombined:
165
172
  self.data = utils.append_data(self.data, key, obj.data[key])
166
173
  self.instrument = instruments.RADIOMETRICS
167
174
 
168
- def screen_time(self, expected_date: datetime.date | None):
175
+ def screen_time(self, expected_date: datetime.date | None) -> None:
169
176
  """Screens timestamps."""
170
177
  if expected_date is None:
171
178
  self.date = self.data["time"][0].astype(object).date()
@@ -177,22 +184,22 @@ class RadiometricsCombined:
177
184
  for key in self.data:
178
185
  self.data[key] = self.data[key][valid_mask]
179
186
 
180
- def time_to_fractional_hours(self):
187
+ def time_to_fractional_hours(self) -> None:
181
188
  base = self.data["time"][0].astype("datetime64[D]")
182
189
  self.data["time"] = (self.data["time"] - base) / np.timedelta64(1, "h")
183
190
 
184
- def data_to_cloudnet_arrays(self):
191
+ def data_to_cloudnet_arrays(self) -> None:
185
192
  """Converts arrays to CloudnetArrays."""
186
193
  for key, array in self.data.items():
187
194
  self.data[key] = CloudnetArray(array, key)
188
195
 
189
- def add_meta(self):
196
+ def add_meta(self) -> None:
190
197
  """Adds some metadata."""
191
198
  valid_keys = ("latitude", "longitude", "altitude")
192
199
  for key, value in self.site_meta.items():
193
- key = key.lower()
194
- if key in valid_keys:
195
- self.data[key] = CloudnetArray(float(value), key)
200
+ name = key.lower()
201
+ if name in valid_keys:
202
+ self.data[name] = CloudnetArray(float(value), key)
196
203
 
197
204
 
198
205
  def _parse_datetime(text: str) -> datetime.datetime:
@@ -201,4 +208,11 @@ def _parse_datetime(text: str) -> datetime.datetime:
201
208
  hour, minute, second = map(int, time.split(":"))
202
209
  if year < 100:
203
210
  year += 2000
204
- return datetime.datetime(year, month, day, hour, minute, second)
211
+ return datetime.datetime(
212
+ year,
213
+ month,
214
+ day,
215
+ hour,
216
+ minute,
217
+ second,
218
+ )
@@ -1,7 +1,8 @@
1
1
  """This module contains RPG Cloud Radar related functions."""
2
2
  import logging
3
3
  import math
4
- from typing import Sequence
4
+ from collections.abc import Sequence
5
+ from typing import TYPE_CHECKING
5
6
 
6
7
  import numpy as np
7
8
  from numpy import ma
@@ -10,13 +11,16 @@ from rpgpy import RPGFileError
10
11
  from cloudnetpy import output, utils
11
12
  from cloudnetpy.categorize.atmos_utils import mmh2ms
12
13
  from cloudnetpy.cloudnetarray import CloudnetArray
14
+ from cloudnetpy.constants import G_TO_KG
13
15
  from cloudnetpy.exceptions import InconsistentDataError, ValidTimeStampError
14
16
  from cloudnetpy.instruments import instruments
15
17
  from cloudnetpy.instruments.cloudnet_instrument import CloudnetInstrument
16
- from cloudnetpy.instruments.instruments import Instrument
17
18
  from cloudnetpy.instruments.rpg_reader import Fmcw94Bin, HatproBinCombined
18
19
  from cloudnetpy.metadata import MetaData
19
20
 
21
+ if TYPE_CHECKING:
22
+ from cloudnetpy.instruments.instruments import Instrument
23
+
20
24
 
21
25
  def rpg2nc(
22
26
  path_to_l1_files: str,
@@ -31,6 +35,7 @@ def rpg2nc(
31
35
  concatenates the data and writes a netCDF file.
32
36
 
33
37
  Args:
38
+ ----
34
39
  path_to_l1_files: Folder containing one day of RPG LV1 files.
35
40
  output_file: Output file name.
36
41
  site_meta: Dictionary containing information about the
@@ -43,15 +48,18 @@ def rpg2nc(
43
48
  only files that match the date will be used.
44
49
 
45
50
  Returns:
51
+ -------
46
52
  2-element tuple containing
47
53
 
48
54
  - UUID of the generated file.
49
55
  - Files used in the processing.
50
56
 
51
57
  Raises:
58
+ ------
52
59
  ValidTimeStampError: No valid timestamps found.
53
60
 
54
61
  Examples:
62
+ --------
55
63
  >>> from cloudnetpy.instruments import rpg2nc
56
64
  >>> site_meta = {'name': 'Hyytiala', 'altitude': 174}
57
65
  >>> rpg2nc('/path/to/files/', 'test.nc', site_meta)
@@ -89,7 +97,7 @@ def print_info(data: dict) -> None:
89
97
  mode = "LDR"
90
98
  else:
91
99
  mode = "STSR"
92
- logging.info(f"RPG cloud radar in {mode} mode")
100
+ logging.info("RPG cloud radar in %s mode", mode)
93
101
 
94
102
 
95
103
  RpgObjects = Sequence[Fmcw94Bin] | Sequence[HatproBinCombined]
@@ -107,12 +115,13 @@ def create_one_day_data_record(rpg_objects: RpgObjects) -> dict:
107
115
  def _stack_rpg_data(rpg_objects: RpgObjects) -> tuple[dict, dict]:
108
116
  """Combines data from hourly RPG objects.
109
117
 
110
- Notes:
118
+ Notes
119
+ -----
111
120
  Ignores variable names starting with an underscore.
112
121
 
113
122
  """
114
123
 
115
- def _stack(source, target, fun):
124
+ def _stack(source, target, fun) -> None:
116
125
  for name, value in source.items():
117
126
  if not name.startswith("_"):
118
127
  target[name] = fun((target[name], value)) if name in target else value
@@ -131,7 +140,7 @@ def _reduce_header(header: dict) -> dict:
131
140
  for key, data in header.items():
132
141
  first_profile_value = data[0]
133
142
  is_identical_value = bool(
134
- np.isclose(data, first_profile_value, rtol=1e-2).all()
143
+ np.isclose(data, first_profile_value, rtol=1e-2).all(),
135
144
  )
136
145
  if is_identical_value is False:
137
146
  msg = f"Inconsistent header: {key}: {data}"
@@ -154,6 +163,8 @@ def _mask_invalid_data(data_in: dict) -> dict:
154
163
  data = data_in.copy()
155
164
  fill_values = (-999, 1e-10)
156
165
  for name in data:
166
+ if np.issubdtype(data[name].dtype, np.integer) or name == "rainfall_rate":
167
+ continue
157
168
  data[name] = ma.masked_equal(data[name], 0)
158
169
  for value in fill_values:
159
170
  data[name][data[name] == value] = ma.masked
@@ -188,14 +199,19 @@ def _remove_files_with_bad_height(objects: list, files: list) -> tuple[list, lis
188
199
  most_common = np.bincount(lengths).argmax()
189
200
  files = [
190
201
  file
191
- for file, obj, length in zip(files, objects, lengths)
202
+ for file, obj, length in zip(files, objects, lengths, strict=True)
203
+ if length == most_common
204
+ ]
205
+ objects = [
206
+ obj
207
+ for obj, length in zip(objects, lengths, strict=True)
192
208
  if length == most_common
193
209
  ]
194
- objects = [obj for obj, length in zip(objects, lengths) if length == most_common]
195
210
  n_removed = len(lengths) - len(files)
196
211
  if n_removed > 0:
197
212
  logging.warning(
198
- f"Removed {n_removed} RPG-FMCW-94 files due to inconsistent height vector"
213
+ "Removed %s RPG-FMCW-94 files due to inconsistent height vector",
214
+ n_removed,
199
215
  )
200
216
  return objects, files
201
217
 
@@ -204,7 +220,8 @@ def _validate_date(obj, expected_date: str) -> None:
204
220
  for t in obj.data["time"][:]:
205
221
  date_str = "-".join(utils.seconds2date(t)[:3])
206
222
  if date_str != expected_date:
207
- raise ValueError("Ignoring a file (time stamps not what expected)")
223
+ msg = "Ignoring a file (time stamps not what expected)"
224
+ raise ValueError(msg)
208
225
 
209
226
 
210
227
  class Rpg(CloudnetInstrument):
@@ -220,10 +237,14 @@ class Rpg(CloudnetInstrument):
220
237
 
221
238
  def convert_time_to_fraction_hour(self, data_type: str | None = None) -> None:
222
239
  """Converts time to fraction hour."""
223
- key = "time"
224
- fraction_hour = utils.seconds2hours(self.raw_data[key])
225
- self.data[key] = CloudnetArray(
226
- np.array(fraction_hour), key, data_type=data_type
240
+ ms2s = 1e-3
241
+ total_time_sec = self.raw_data["time"] + self.raw_data.get("time_ms", 0) * ms2s
242
+ fraction_hour = utils.seconds2hours(total_time_sec)
243
+
244
+ self.data["time"] = CloudnetArray(
245
+ np.array(fraction_hour),
246
+ "time",
247
+ data_type=data_type,
227
248
  )
228
249
 
229
250
  def _get_date(self) -> list:
@@ -254,7 +275,8 @@ class Fmcw(Rpg):
254
275
  threshold = -35
255
276
  if "ldr" in self.data:
256
277
  self.data["ldr"].data = ma.masked_less_equal(
257
- self.data["ldr"].data, threshold
278
+ self.data["ldr"].data,
279
+ threshold,
258
280
  )
259
281
 
260
282
  def mask_invalid_width(self) -> None:
@@ -276,19 +298,21 @@ class Fmcw(Rpg):
276
298
  is_valid_zenith = _filter_zenith_angle(zenith)
277
299
  n_removed = len(is_valid_zenith) - np.count_nonzero(is_valid_zenith)
278
300
  if n_removed == len(zenith):
279
- raise ValidTimeStampError("No profiles with valid zenith angle")
301
+ msg = "No profiles with valid zenith angle"
302
+ raise ValidTimeStampError(msg)
280
303
  if n_removed > 0:
281
304
  logging.warning(
282
- f"Filtering {n_removed} profiles due to invalid zenith angle"
305
+ "Filtering %s profiles due to invalid zenith angle",
306
+ n_removed,
283
307
  )
284
308
  self.data["zenith_angle"] = CloudnetArray(zenith, "zenith_angle")
285
309
  del self.data["elevation"]
286
310
  return list(is_valid_zenith)
287
311
 
288
- def convert_units(self):
312
+ def convert_units(self) -> None:
289
313
  """Converts units."""
290
314
  self.data["rainfall_rate"].data = mmh2ms(self.data["rainfall_rate"].data)
291
- self.data["lwp"].data *= 1e-3 # g -> kg
315
+ self.data["lwp"].data *= G_TO_KG
292
316
 
293
317
  @staticmethod
294
318
  def _get_instrument(data: dict):
@@ -297,7 +321,8 @@ class Fmcw(Rpg):
297
321
  return instruments.FMCW35
298
322
  if math.isclose(frequency, 94, abs_tol=0.1):
299
323
  return instruments.FMCW94
300
- raise RuntimeError(f"Unknown RPG cloud radar frequency: {frequency}")
324
+ msg = f"Unknown RPG cloud radar frequency: {frequency}"
325
+ raise RuntimeError(msg)
301
326
 
302
327
 
303
328
  class Hatpro(Rpg):
@@ -315,7 +340,7 @@ def _filter_zenith_angle(zenith: ma.MaskedArray) -> np.ndarray:
315
340
  logging.warning("Can not determine zenith angle, assuming 0 degrees")
316
341
  limits = [-5, 15]
317
342
  ind_close_to_zenith = np.where(
318
- np.logical_and(zenith > limits[0], zenith < limits[1])
343
+ np.logical_and(zenith > limits[0], zenith < limits[1]),
319
344
  )
320
345
  if not ind_close_to_zenith[0].size:
321
346
  return np.zeros_like(zenith, dtype=bool)
@@ -365,7 +390,8 @@ RPG_ATTRIBUTES = {
365
390
  "srho_hv": MetaData(long_name="Slanted correlation coefficient", units="1"),
366
391
  "kdp": MetaData(long_name="Specific differential phase shift", units="rad km-1"),
367
392
  "differential_attenuation": MetaData(
368
- long_name="Differential attenuation", units="dB km-1"
393
+ long_name="Differential attenuation",
394
+ units="dB km-1",
369
395
  ),
370
396
  # All radars
371
397
  "file_code": MetaData(