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
@@ -1,11 +1,12 @@
1
1
  """Misc. plotting routines for Cloudnet products."""
2
2
  import os.path
3
- from datetime import date, datetime
3
+ from datetime import date, datetime, timezone
4
4
 
5
5
  import matplotlib.pyplot as plt
6
6
  import netCDF4
7
7
  import numpy as np
8
8
  from matplotlib import rcParams
9
+ from matplotlib.colorbar import Colorbar
9
10
  from matplotlib.colors import Colormap, ListedColormap
10
11
  from matplotlib.ticker import AutoMinorLocator
11
12
  from matplotlib.transforms import Affine2D, Bbox
@@ -57,13 +58,14 @@ class Dimensions:
57
58
  def generate_figure(
58
59
  nc_file: str,
59
60
  field_names: list,
60
- show: bool = True,
61
+ *,
61
62
  save_path: str | None = None,
62
63
  max_y: int = 12,
63
64
  dpi: int = 120,
64
65
  image_name: str | None = None,
65
66
  sub_title: bool = True,
66
67
  title: bool = True,
68
+ show: bool = True,
67
69
  add_grid: bool = False,
68
70
  include_xlimits: bool = False,
69
71
  add_sources: bool = False,
@@ -75,6 +77,7 @@ def generate_figure(
75
77
  """Generates a Cloudnet figure.
76
78
 
77
79
  Args:
80
+ ----
78
81
  nc_file (str): Input file.
79
82
  field_names (list): Variable names to be plotted.
80
83
  show (bool, optional): If True, shows the figure. Default is True.
@@ -109,9 +112,11 @@ def generate_figure(
109
112
  after the copyright_text (datetime.datetime.utcnow()
110
113
 
111
114
  Returns:
115
+ -------
112
116
  Dimensions of the generated figure in pixels.
113
117
 
114
118
  Examples:
119
+ --------
115
120
  >>> from cloudnetpy.plotting import generate_figure
116
121
  >>> generate_figure('categorize_file.nc', ['Z', 'v', 'width', 'ldr',
117
122
  'beta', 'lwp'])
@@ -135,7 +140,13 @@ def generate_figure(
135
140
  is_height = _is_height_dimension(nc_file)
136
141
  fig, axes = _initialize_figure(len(valid_fields), dpi)
137
142
 
138
- for ax, field, name, tb_ind in zip(axes, valid_fields, valid_names, indices):
143
+ for ax, field, name, tb_ind in zip(
144
+ axes,
145
+ valid_fields,
146
+ valid_names,
147
+ indices,
148
+ strict=True,
149
+ ):
139
150
  original_attrib = None # monkey patch
140
151
  if cloudnet_file_type == "rain-radar" and name == "rainfall_rate":
141
152
  original_attrib = ATTRIBUTES[name]
@@ -173,44 +184,65 @@ def generate_figure(
173
184
  source = ATTRIBUTES[name].source
174
185
  time = _read_time_vector(nc_file)
175
186
  try:
176
- tb_ind = int(tb_ind)
187
+ tb_index = int(tb_ind)
177
188
  except ValueError:
178
- tb_ind = None
179
- _plot_instrument_data(ax, field, name, source, time, unit, nc_file, tb_ind)
189
+ tb_index = None
190
+ _plot_instrument_data(
191
+ ax,
192
+ field,
193
+ name,
194
+ source,
195
+ time,
196
+ unit,
197
+ nc_file,
198
+ tb_index,
199
+ )
180
200
  continue
181
201
  ax_value = _read_ax_values(nc_file)
182
202
 
183
203
  if plot_type not in ("bar", "model"):
184
- time_new, field = _mark_gaps(ax_value[0], field)
204
+ time_new, field_with_gaps = _mark_gaps(ax_value[0], field)
185
205
  ax_value = (time_new, ax_value[1])
206
+ else:
207
+ field_with_gaps = field
186
208
 
187
- field, ax_value = _screen_high_altitudes(field, ax_value, max_y)
209
+ field_screened, ax_value = _screen_high_altitudes(
210
+ field_with_gaps,
211
+ ax_value,
212
+ max_y,
213
+ )
188
214
  set_yax(ax, max_y, ylabel=None)
189
215
  if plot_type == "bar":
190
216
  unit = _get_variable_unit(nc_file, name)
191
- _plot_bar_data(ax, field, ax_value[0], unit)
217
+ _plot_bar_data(ax, field_screened, ax_value[0], unit)
192
218
  set_yax(ax, 2, ATTRIBUTES[name].ylabel)
193
219
 
194
220
  elif plot_type == "segment":
195
- _plot_segment_data(ax, field, name, ax_value)
221
+ _plot_segment_data(ax, field_screened, name, ax_value)
196
222
 
197
223
  else:
198
- _plot_colormesh_data(ax, field, name, ax_value)
224
+ _plot_colormesh_data(ax, field_screened, name, ax_value)
199
225
  if original_attrib is not None:
200
226
  ATTRIBUTES[name] = original_attrib
201
- case_date = set_labels(fig, axes[-1], nc_file, sub_title)
227
+ case_date = set_labels(fig, axes[-1], nc_file, sub_title=sub_title)
202
228
 
203
229
  if add_copyright:
204
230
  display_watermark(fig, copyright_text, add_creation_time)
205
- handle_saving(image_name, save_path, show, case_date, valid_names)
231
+ handle_saving(image_name, save_path, case_date, valid_names, show=show)
206
232
  return Dimensions(fig, axes)
207
233
 
208
234
 
209
235
  def _mark_gaps(
210
- time: np.ndarray, data: ma.MaskedArray, max_allowed_gap: float = 1
236
+ time: np.ndarray,
237
+ data: ma.MaskedArray,
238
+ max_allowed_gap: float = 1,
211
239
  ) -> tuple:
212
- assert time[0] >= 0
213
- assert time[-1] <= 24
240
+ if time[0] < 0:
241
+ msg = "Negative time values in the file."
242
+ raise ValueError(msg)
243
+ if time[-1] > 24:
244
+ msg = "Time values exceed 24 hours."
245
+ raise ValueError(msg)
214
246
  max_gap = max_allowed_gap / 60
215
247
  if not ma.is_masked(data):
216
248
  mask_new = np.zeros(data.shape)
@@ -225,22 +257,22 @@ def _mark_gaps(
225
257
  temp_mask = np.ones((2, data.shape[1]))
226
258
  time_delta = 0.001
227
259
  for ind in np.sort(gap_indices)[::-1]:
228
- ind += 1
229
- data_new = np.insert(data_new, ind, temp_array, axis=0)
230
- mask_new = np.insert(mask_new, ind, temp_mask, axis=0)
231
- time_new = np.insert(time_new, ind, time[ind] - time_delta)
232
- time_new = np.insert(time_new, ind, time[ind - 1] + time_delta)
260
+ ind_gap = ind + 1
261
+ data_new = np.insert(data_new, ind_gap, temp_array, axis=0)
262
+ mask_new = np.insert(mask_new, ind_gap, temp_mask, axis=0)
263
+ time_new = np.insert(time_new, ind_gap, time[ind_gap] - time_delta)
264
+ time_new = np.insert(time_new, ind_gap, time[ind_gap - 1] + time_delta)
233
265
  if (time[0] - 0) > max_gap:
234
266
  data_new = np.insert(data_new, 0, temp_array, axis=0)
235
267
  mask_new = np.insert(mask_new, 0, temp_mask, axis=0)
236
268
  time_new = np.insert(time_new, 0, time[0] - time_delta)
237
269
  time_new = np.insert(time_new, 0, time_delta)
238
270
  if (24 - time[-1]) > max_gap:
239
- ind = mask_new.shape[0]
240
- data_new = np.insert(data_new, ind, temp_array, axis=0)
241
- mask_new = np.insert(mask_new, ind, temp_mask, axis=0)
242
- time_new = np.insert(time_new, ind, 24 - time_delta)
243
- time_new = np.insert(time_new, ind, time[-1] + time_delta)
271
+ ind_gap = mask_new.shape[0]
272
+ data_new = np.insert(data_new, ind_gap, temp_array, axis=0)
273
+ mask_new = np.insert(mask_new, ind_gap, temp_mask, axis=0)
274
+ time_new = np.insert(time_new, ind_gap, 24 - time_delta)
275
+ time_new = np.insert(time_new, ind_gap, time[-1] + time_delta)
244
276
  data_new.mask = mask_new
245
277
  return time_new, data_new
246
278
 
@@ -248,11 +280,12 @@ def _mark_gaps(
248
280
  def handle_saving(
249
281
  image_name: str | None,
250
282
  save_path: str | None,
251
- show: bool,
252
283
  case_date: date,
253
284
  field_names: list,
254
285
  fix: str = "",
255
- ):
286
+ *,
287
+ show: bool = False,
288
+ ) -> None:
256
289
  if image_name:
257
290
  plt.savefig(image_name, bbox_inches="tight")
258
291
  elif save_path:
@@ -271,7 +304,7 @@ def _get_relative_error(fields: list, ax_values: list, max_y: int) -> tuple:
271
304
  return _screen_high_altitudes(error, ax_values[1], max_y)
272
305
 
273
306
 
274
- def set_labels(fig, ax, nc_file: str, sub_title: bool = True) -> date:
307
+ def set_labels(fig, ax, nc_file: str, *, sub_title: bool = True) -> date:
275
308
  ax.set_xlabel("Time (UTC)", fontsize=13)
276
309
  case_date = read_date(nc_file)
277
310
  site_name = read_location(nc_file)
@@ -288,7 +321,7 @@ def display_watermark(
288
321
  fontsize: int = 7,
289
322
  ) -> None:
290
323
  if add_creation_time:
291
- now = datetime.utcnow().isoformat().split(".")[0].split("T")
324
+ now = datetime.now(tz=timezone.utc).isoformat().split(".")[0].split("T")
292
325
  copyright_text += " / Created on " + " ".join(now) + " UTC"
293
326
  # similar to add_subtitle
294
327
  fig.text(
@@ -298,12 +331,16 @@ def display_watermark(
298
331
  fontsize=fontsize,
299
332
  ha="left",
300
333
  va="bottom",
301
- # transform=ax.transAxes,
302
334
  )
303
335
 
304
336
 
305
337
  def display_datasources(
306
- ax, source: str, xpos: float = 0.01, ypos: float = 0.99, fontsize: int = 7, **kwargs
338
+ ax,
339
+ source: str,
340
+ xpos: float = 0.01,
341
+ ypos: float = 0.99,
342
+ fontsize: int = 7,
343
+ **kwargs,
307
344
  ) -> None:
308
345
  _ = "s" if "\n" in source else ""
309
346
  ax.text(
@@ -318,7 +355,7 @@ def display_datasources(
318
355
  )
319
356
 
320
357
 
321
- def _set_title(ax, field_name: str, identifier: str = " from CloudnetPy"):
358
+ def _set_title(ax, field_name: str, identifier: str = " from CloudnetPy") -> None:
322
359
  ax.set_title(f"{ATTRIBUTES[field_name].name}{identifier}", fontsize=14)
323
360
 
324
361
 
@@ -340,27 +377,28 @@ def _find_valid_fields(nc_file: str, names: list) -> tuple[list, list]:
340
377
  else:
341
378
  valid_names.remove(name)
342
379
  if not valid_names:
343
- raise ValueError("No fields to be plotted")
380
+ msg = "No valid fields to be plotted."
381
+ raise ValueError(msg)
344
382
  return valid_data, valid_names
345
383
 
346
384
 
347
385
  def _is_height_dimension(full_path: str) -> bool:
348
386
  with netCDF4.Dataset(full_path) as nc:
349
- is_height = any(key in nc.variables for key in ("height", "range"))
350
- return is_height
387
+ return any(key in nc.variables for key in ("height", "range"))
351
388
 
352
389
 
353
390
  def _get_variable_unit(full_path: str, name: str) -> str:
354
391
  with netCDF4.Dataset(full_path) as nc:
355
- var = nc.variables[name]
356
- unit = var.units
357
- return unit
392
+ return nc.variables[name].units
358
393
 
359
394
 
360
395
  def _initialize_figure(n_subplots: int, dpi) -> tuple:
361
396
  """Creates an empty figure according to the number of subplots."""
362
397
  fig, axes = plt.subplots(
363
- n_subplots, 1, figsize=(16, 4 + (n_subplots - 1) * 4.8), dpi=dpi
398
+ n_subplots,
399
+ 1,
400
+ figsize=(16, 4 + (n_subplots - 1) * 4.8),
401
+ dpi=dpi,
364
402
  )
365
403
  fig.subplots_adjust(left=0.06, right=0.73)
366
404
  if n_subplots == 1:
@@ -373,10 +411,7 @@ def _read_ax_values(full_path: str) -> tuple[ndarray, ndarray]:
373
411
  file_type = utils.get_file_type(full_path)
374
412
  with netCDF4.Dataset(full_path) as nc:
375
413
  is_height = "height" in nc.variables
376
- if is_height is not True:
377
- fields = ["time", "range"]
378
- else:
379
- fields = ["time", "height"]
414
+ fields = ["time", "range"] if is_height is not True else ["time", "height"]
380
415
  time, height = ptools.read_nc_fields(full_path, fields)
381
416
  if file_type == "model":
382
417
  height = ma.mean(height, axis=0)
@@ -400,6 +435,7 @@ def _screen_high_altitudes(data_field: ndarray, ax_values: tuple, max_y: int) ->
400
435
  saving fig. This fixes that bug till pcolorfast does fixing themselves.
401
436
 
402
437
  Args:
438
+ ----
403
439
  data_field (ndarray): 2D data array.
404
440
  ax_values (tuple): Time and height 1D arrays.
405
441
  max_y (int): Upper limit in the plots (km).
@@ -413,7 +449,7 @@ def _screen_high_altitudes(data_field: ndarray, ax_values: tuple, max_y: int) ->
413
449
  return data_field, (ax_values[0], alt)
414
450
 
415
451
 
416
- def set_xax(ax, include_xlimits: bool = False):
452
+ def set_xax(ax, *, include_xlimits: bool = False) -> None:
417
453
  """Sets xticks and xtick labels for plt.imshow()."""
418
454
  ticks_x_labels = _get_standard_time_ticks(include_xlimits=include_xlimits)
419
455
  ax.set_xticks(np.arange(0, 25, 4, dtype=int))
@@ -421,7 +457,7 @@ def set_xax(ax, include_xlimits: bool = False):
421
457
  ax.set_xlim(0, 24)
422
458
 
423
459
 
424
- def set_yax(ax, max_y: float, ylabel: str | None, min_y: float = 0.0):
460
+ def set_yax(ax, max_y: float, ylabel: str | None, min_y: float = 0.0) -> None:
425
461
  """Sets yticks, ylim and ylabel for yaxis of axis."""
426
462
  ax.set_ylim(min_y, max_y)
427
463
  ax.set_ylabel("Height (km)", fontsize=13)
@@ -430,7 +466,9 @@ def set_yax(ax, max_y: float, ylabel: str | None, min_y: float = 0.0):
430
466
 
431
467
 
432
468
  def _get_standard_time_ticks(
433
- resolution: int = 4, include_xlimits: bool = False
469
+ resolution: int = 4,
470
+ *,
471
+ include_xlimits: bool = False,
434
472
  ) -> list:
435
473
  """Returns typical ticks / labels for a time vector between 0-24h."""
436
474
  if include_xlimits:
@@ -444,10 +482,11 @@ def _get_standard_time_ticks(
444
482
  ]
445
483
 
446
484
 
447
- def _plot_bar_data(ax, data: np.ndarray, time: ndarray, unit: str):
485
+ def _plot_bar_data(ax, data: np.ndarray, time: ndarray, unit: str) -> None:
448
486
  """Plots 1D variable as bar plot.
449
487
 
450
488
  Args:
489
+ ----
451
490
  ax (obj): Axes object.
452
491
  data (maskedArray): 1D data array.
453
492
  time (ndarray): 1D time array.
@@ -455,11 +494,7 @@ def _plot_bar_data(ax, data: np.ndarray, time: ndarray, unit: str):
455
494
  """
456
495
  data = _convert_to_kg(data, unit)
457
496
  ax.plot(time, data, color="navy", zorder=_ZORDER)
458
-
459
- if isinstance(data, ma.MaskedArray):
460
- data_filled = data.filled(0)
461
- else:
462
- data_filled = data
497
+ data_filled = data.filled(0) if isinstance(data, ma.MaskedArray) else data
463
498
 
464
499
  ax.bar(
465
500
  time,
@@ -474,10 +509,11 @@ def _plot_bar_data(ax, data: np.ndarray, time: ndarray, unit: str):
474
509
  ax.set_position([pos.x0, pos.y0, pos.width * 0.965, pos.height])
475
510
 
476
511
 
477
- def _plot_segment_data(ax, data: ma.MaskedArray, name: str, axes: tuple):
512
+ def _plot_segment_data(ax, data: ma.MaskedArray, name: str, axes: tuple) -> None:
478
513
  """Plots categorical 2D variable.
479
514
 
480
515
  Args:
516
+ ----
481
517
  ax (obj): Axes object of subplot (1,2,3,.. [1,1,],[1,2]... etc.)
482
518
  data (ndarray): 2D data array.
483
519
  name (string): Name of plotted data.
@@ -488,7 +524,9 @@ def _plot_segment_data(ax, data: ma.MaskedArray, name: str, axes: tuple):
488
524
  def _hide_segments(
489
525
  data_in: ma.MaskedArray,
490
526
  ) -> tuple[ma.MaskedArray, list, list]:
491
- assert variables.clabel is not None
527
+ if variables.clabel is None:
528
+ msg = f"Labels not defined for {name}."
529
+ raise ValueError(msg)
492
530
  labels = [x[0] for x in variables.clabel]
493
531
  colors = [x[1] for x in variables.clabel]
494
532
  segments_to_hide = np.char.startswith(labels, "_")
@@ -513,23 +551,26 @@ def _plot_segment_data(ax, data: ma.MaskedArray, name: str, axes: tuple):
513
551
  zorder=_ZORDER,
514
552
  )
515
553
  colorbar = _init_colorbar(pl, ax)
516
- colorbar.set_ticks(np.arange(len(clabel)))
554
+ colorbar.set_ticks(np.arange(len(clabel)).tolist())
517
555
  colorbar.ax.set_yticklabels(clabel, fontsize=13)
518
556
 
519
557
 
520
- def _plot_colormesh_data(ax, data: ndarray, name: str, axes: tuple):
558
+ def _plot_colormesh_data(ax, data: ndarray, name: str, axes: tuple) -> None:
521
559
  """Plots continuous 2D variable.
522
560
 
523
561
  Creates only one plot, so can be used both one plot and subplot type of figs.
524
562
 
525
563
  Args:
564
+ ----
526
565
  ax (obj): Axes object of subplot (1,2,3,.. [1,1,],[1,2]... etc.)
527
566
  data (ndarray): 2D data array.
528
567
  name (string): Name of plotted data.
529
568
  axes (tuple): Time and height 1D arrays.
530
569
  """
531
570
  variables = ATTRIBUTES[name]
532
- assert variables.plot_range is not None
571
+ if variables.plot_range is None:
572
+ msg = f"Plot range not defined for {name}."
573
+ raise ValueError(msg)
533
574
 
534
575
  if name == "cloud_fraction":
535
576
  data[data < 0.1] = ma.masked
@@ -547,16 +588,21 @@ def _plot_colormesh_data(ax, data: ndarray, name: str, axes: tuple):
547
588
  data, vmin, vmax = lin2log(data, vmin, vmax)
548
589
 
549
590
  pl = ax.pcolorfast(
550
- *axes, data[:-1, :-1].T, vmin=vmin, vmax=vmax, cmap=color_map, zorder=_ZORDER
591
+ *axes,
592
+ data[:-1, :-1].T,
593
+ vmin=vmin,
594
+ vmax=vmax,
595
+ cmap=color_map,
596
+ zorder=_ZORDER,
551
597
  )
552
598
 
553
599
  if variables.plot_type != "bit":
554
600
  colorbar = _init_colorbar(pl, ax)
555
- colorbar.set_label(variables.clabel, fontsize=13)
601
+ colorbar.set_label(str(variables.clabel), fontsize=13)
556
602
 
557
603
  if variables.plot_scale == Scale.LOGARITHMIC:
558
604
  tick_labels = generate_log_cbar_ticklabel_list(vmin, vmax)
559
- colorbar.set_ticks(np.arange(vmin, vmax + 1))
605
+ colorbar.set_ticks(np.arange(vmin, vmax + 1).tolist())
560
606
  colorbar.ax.set_yticklabels(tick_labels)
561
607
 
562
608
 
@@ -569,7 +615,7 @@ def _plot_instrument_data(
569
615
  unit: str,
570
616
  full_path: str | None = None,
571
617
  tb_ind: int | None = None,
572
- ):
618
+ ) -> None:
573
619
  if product in ("mwr", "mwr-single"):
574
620
  _plot_mwr(ax, data, name, time, unit)
575
621
  if product == "disdrometer":
@@ -578,8 +624,8 @@ def _plot_instrument_data(
578
624
  _plot_weather_station(ax, data, time, name)
579
625
  if full_path is not None and tb_ind is not None:
580
626
  quality_flag_array = ptools.read_nc_fields(full_path, "quality_flag")
581
- assert isinstance(quality_flag_array, ndarray)
582
- quality_flag = quality_flag_array[:, tb_ind]
627
+ quality_flag_array_ma = ma.array(quality_flag_array)
628
+ quality_flag = quality_flag_array_ma[:, tb_ind]
583
629
  data = data[:, tb_ind]
584
630
  data_dict = {"tb": data, "quality_flag": quality_flag, "time": time}
585
631
  _plot_hatpro(ax, data_dict, full_path)
@@ -587,7 +633,7 @@ def _plot_instrument_data(
587
633
  ax.set_position([pos.x0, pos.y0, pos.width * 0.965, pos.height])
588
634
 
589
635
 
590
- def _plot_disdrometer(ax, data: ndarray, time: ndarray, name: str, unit: str):
636
+ def _plot_disdrometer(ax, data: ndarray, time: ndarray, name: str, unit: str) -> None:
591
637
  if name == "rainfall_rate":
592
638
  if unit == "m s-1":
593
639
  data *= 1000 * 3600
@@ -600,10 +646,15 @@ def _plot_disdrometer(ax, data: ndarray, time: ndarray, name: str, unit: str):
600
646
  set_yax(ax, ylim, "")
601
647
 
602
648
 
603
- def _plot_hatpro(ax, data: dict, full_path: str):
649
+ def _plot_hatpro(ax, data: dict, full_path: str) -> None:
604
650
  tb = _pointing_filter(full_path, data["tb"])
605
651
  ax.plot(
606
- data["time"], tb, color="royalblue", linestyle="-", linewidth=1, zorder=_ZORDER
652
+ data["time"],
653
+ tb,
654
+ color="royalblue",
655
+ linestyle="-",
656
+ linewidth=1,
657
+ zorder=_ZORDER,
607
658
  )
608
659
  set_yax(
609
660
  ax,
@@ -614,7 +665,10 @@ def _plot_hatpro(ax, data: dict, full_path: str):
614
665
 
615
666
 
616
667
  def _pointing_filter(
617
- full_path: str, data: ndarray, zenith_limit=5, status: int = 0
668
+ full_path: str,
669
+ data: ndarray,
670
+ zenith_limit=5,
671
+ status: int = 0,
618
672
  ) -> ndarray:
619
673
  """Filters data according to pointing flag and zenith angle."""
620
674
  with netCDF4.Dataset(full_path) as nc:
@@ -630,7 +684,7 @@ def _pointing_filter(
630
684
  return data
631
685
 
632
686
 
633
- def _plot_weather_station(ax, data: ndarray, time: ndarray, name: str):
687
+ def _plot_weather_station(ax, data: ndarray, time: ndarray, name: str) -> None:
634
688
  match name:
635
689
  case "air_temperature":
636
690
  unit = "K"
@@ -678,10 +732,11 @@ def _plot_weather_station(ax, data: ndarray, time: ndarray, name: str):
678
732
  ax.plot(time, data, color="royalblue", zorder=_ZORDER)
679
733
  set_yax(ax, min_y=min_y, max_y=max_y, ylabel=unit)
680
734
  case unknown:
681
- raise NotImplementedError(f"Not implemented for {unknown}")
735
+ msg = f"Not implemented for {unknown}"
736
+ raise NotImplementedError(msg)
682
737
 
683
738
 
684
- def _plot_mwr(ax, data_in: ma.MaskedArray, name: str, time: ndarray, unit: str):
739
+ def _plot_mwr(ax, data_in: ma.MaskedArray, name: str, time: ndarray, unit: str) -> None:
685
740
  data, time = _get_unmasked_values(data_in, time)
686
741
  data = _convert_to_kg(data, unit)
687
742
  rolling_mean, width = _calculate_rolling_mean(time, data)
@@ -714,7 +769,8 @@ def _plot_mwr(ax, data_in: ma.MaskedArray, name: str, time: ndarray, unit: str):
714
769
 
715
770
 
716
771
  def _get_unmasked_values(
717
- data: ma.MaskedArray, time: ndarray
772
+ data: ma.MaskedArray,
773
+ time: ndarray,
718
774
  ) -> tuple[np.ndarray, np.ndarray]:
719
775
  if ma.is_masked(data) is False:
720
776
  return data, time
@@ -732,8 +788,7 @@ def _find_time_gap_indices(time: ndarray) -> ndarray:
732
788
  """Finds time gaps bigger than 5min."""
733
789
  time_diff = np.diff(time)
734
790
  dec_hour_5min = 0.085
735
- gaps = np.where(time_diff > dec_hour_5min)[0]
736
- return gaps
791
+ return np.where(time_diff > dec_hour_5min)[0]
737
792
 
738
793
 
739
794
  def _get_plot_parameters(data: ndarray) -> tuple[int, float]:
@@ -769,7 +824,7 @@ def _filter_noise(data: ndarray, n: int) -> ndarray:
769
824
  return filtfilt(b, a, data)
770
825
 
771
826
 
772
- def _init_colorbar(plot, axis):
827
+ def _init_colorbar(plot, axis) -> Colorbar:
773
828
  divider = make_axes_locatable(axis)
774
829
  cax = divider.append_axes("right", size="1%", pad=0.25)
775
830
  return plt.colorbar(plot, fraction=1.0, ax=axis, cax=cax)
@@ -783,21 +838,19 @@ def generate_log_cbar_ticklabel_list(vmin: float, vmax: float) -> list:
783
838
  def read_location(nc_file: str) -> str:
784
839
  """Returns site name."""
785
840
  with netCDF4.Dataset(nc_file) as nc:
786
- site_name = nc.location
787
- return site_name
841
+ return nc.location
788
842
 
789
843
 
790
844
  def read_date(nc_file: str) -> date:
791
845
  """Returns measurement date."""
792
846
  with netCDF4.Dataset(nc_file) as nc:
793
- case_date = date(int(nc.year), int(nc.month), int(nc.day))
794
- return case_date
847
+ return date(int(nc.year), int(nc.month), int(nc.day))
795
848
 
796
849
 
797
- def read_source(nc_file: str, name: str, add_serial_number: bool = True) -> str:
850
+ def read_source(nc_file: str, name: str, *, add_serial_number: bool = True) -> str:
798
851
  """Returns source attr of field name or global one and maybe serial number ."""
799
852
  with netCDF4.Dataset(nc_file) as nc:
800
- if name in nc.variables.keys() and "source" in nc.variables[name].ncattrs():
853
+ if name in nc.variables and "source" in nc.variables[name].ncattrs():
801
854
  # single device has available src attr and maybe SN
802
855
  source = nc.variables[name].source
803
856
  # even if the attr is source_serial_number, it is possible that
@@ -811,21 +864,18 @@ def read_source(nc_file: str, name: str, add_serial_number: bool = True) -> str:
811
864
  source, sno = source.split("\n"), sno.split("\n")
812
865
  source = [
813
866
  f"{_source} (SN: {_sno})" if _sno else f"{_source}"
814
- for _source, _sno in zip(source, sno)
867
+ for _source, _sno in zip(source, sno, strict=True)
815
868
  ]
816
869
  source = "\n".join(source)
817
870
  else:
818
871
  # global src, a \n sep string-list
819
- if "source" in nc.ncattrs():
820
- source = nc.source
821
- else:
822
- # empty list means that the zip below runs for 0 times as
823
- # the assumption is if we do not have any sources we can't
824
- # have any serial numbers, i.e. no instrument type means
825
- # to instrument serial number. If this would be the case
826
- # something somewhere else is wrong and should not be
827
- # fixed here.
828
- source = []
872
+ source = nc.source if "source" in nc.ncattrs() else []
873
+ # empty list means that the zip below runs for 0 times as
874
+ # the assumption is if we do not have any sources we can't
875
+ # have any serial numbers, i.e. no instrument type means
876
+ # to instrument serial number. If this is the case
877
+ # something somewhere else is wrong and should not be
878
+ # fixed here.
829
879
  # who knows whether the cloudnet nc file actually has the SNs
830
880
  # so better check beforehand
831
881
  if add_serial_number and "source_serial_numbers" in nc.ncattrs():
@@ -835,14 +885,13 @@ def read_source(nc_file: str, name: str, add_serial_number: bool = True) -> str:
835
885
  source = source.split("\n")
836
886
  source = [
837
887
  f"{_source} (SN: {_sno})" if _sno else f"{_source}"
838
- for _source, _sno in zip(source, sno)
888
+ for _source, _sno in zip(source, sno, strict=True)
839
889
  ]
840
890
  source = "\n".join(source)
841
- source = source.rstrip("\n")
842
- return source
891
+ return source.rstrip("\n")
843
892
 
844
893
 
845
- def add_subtitle(fig, case_date: date, site_name: str):
894
+ def add_subtitle(fig, case_date: date, site_name: str) -> None:
846
895
  """Adds subtitle into figure."""
847
896
  text = _get_subtitle_text(case_date, site_name)
848
897
  fig.suptitle(
@@ -861,16 +910,24 @@ def _get_subtitle_text(case_date: date, site_name: str) -> str:
861
910
 
862
911
 
863
912
  def _create_save_name(
864
- save_path: str, case_date: date, field_names: list, fix: str = ""
913
+ save_path: str,
914
+ case_date: date,
915
+ field_names: list,
916
+ fix: str = "",
865
917
  ) -> str:
866
918
  """Creates file name for saved images."""
867
919
  date_string = case_date.strftime("%Y%m%d")
868
920
  return f"{save_path}{date_string}_{'_'.join(field_names)}{fix}.png"
869
921
 
870
922
 
871
- def _plot_relative_error(ax, error: ma.MaskedArray, ax_values: tuple):
923
+ def _plot_relative_error(ax, error: ma.MaskedArray, ax_values: tuple) -> None:
872
924
  pl = ax.pcolorfast(
873
- *ax_values, error[:-1, :-1].T, cmap="RdBu", vmin=-30, vmax=30, zorder=_ZORDER
925
+ *ax_values,
926
+ error[:-1, :-1].T,
927
+ cmap="RdBu",
928
+ vmin=-30,
929
+ vmax=30,
930
+ zorder=_ZORDER,
874
931
  )
875
932
  colorbar = _init_colorbar(pl, ax)
876
933
  colorbar.set_label("%", fontsize=13)
@@ -888,13 +945,14 @@ def lin2log(*args) -> list:
888
945
 
889
946
  def plot_2d(
890
947
  data: ma.MaskedArray,
891
- cbar: bool = True,
892
948
  cmap: str = "viridis",
893
949
  ncolors: int = 50,
894
950
  clim: tuple | None = None,
895
951
  ylim: tuple | None = None,
896
952
  xlim: tuple | None = None,
897
- ):
953
+ *,
954
+ cbar: bool = True,
955
+ ) -> None:
898
956
  """Simple plot of 2d variable."""
899
957
  plt.close()
900
958
  if cbar:
@@ -920,16 +978,18 @@ def plot_2d(
920
978
  def compare_files(
921
979
  nc_files: tuple[str, str],
922
980
  field_name: str,
923
- show: bool = True,
924
- relative_err: bool = False,
925
981
  save_path: str | None = None,
926
982
  max_y: int = 12,
927
983
  dpi: int = 120,
928
984
  image_name: str | None = None,
985
+ *,
986
+ show: bool = True,
987
+ relative_err: bool = False,
929
988
  ) -> Dimensions:
930
989
  """Plots one particular field from two Cloudnet files.
931
990
 
932
991
  Args:
992
+ ----
933
993
  nc_files (tuple): Filenames of the two files to be compared.
934
994
  field_name (str): Name of variable to be plotted.
935
995
  show (bool, optional): If True, shows the plot.
@@ -943,6 +1003,7 @@ def compare_files(
943
1003
  Overrides the *save_path* option. Default is None.
944
1004
 
945
1005
  Returns:
1006
+ -------
946
1007
  Dimensions of the generated figure in pixels.
947
1008
 
948
1009
  """
@@ -979,5 +1040,12 @@ def compare_files(
979
1040
  _plot_relative_error(axes[-1], error, ax_value)
980
1041
 
981
1042
  case_date = set_labels(fig, axes[-1], nc_files[0], sub_title=False)
982
- handle_saving(image_name, save_path, show, case_date, [field_name], "_comparison")
1043
+ handle_saving(
1044
+ image_name,
1045
+ save_path,
1046
+ case_date,
1047
+ [field_name],
1048
+ "_comparison",
1049
+ show=show,
1050
+ )
983
1051
  return Dimensions(fig, axes)