cloudnetpy 1.56.7__py3-none-any.whl → 1.56.9__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.
@@ -1,2 +1,2 @@
1
1
  from .plot_meta import PlotMeta
2
- from .plotting import PlotParameters, generate_figure, plot_2d
2
+ from .plotting import Dimensions, PlotParameters, generate_figure, plot_2d
@@ -4,11 +4,28 @@ from typing import NamedTuple
4
4
 
5
5
 
6
6
  class PlotMeta(NamedTuple):
7
+ """
8
+ A class representing the metadata for plotting.
9
+
10
+ Attributes:
11
+ cmap: The colormap to be used for the plot.
12
+ clabel: The label for the colorbar. It can be a single string, a sequence
13
+ of tuples containing the label and units for each colorbar, or None
14
+ if no colorbar is needed.
15
+ plot_range: The range of values to be plotted. It can be a tuple
16
+ containing the minimum and maximum values, or None if the range should
17
+ be automatically determined.
18
+ log_scale: Whether to plot data values in a logarithmic scale.
19
+ moving_average: Whether to plot a moving average in a 1d plot.
20
+ contour: Whether to plot contours on top of a filled colormap.
21
+ """
22
+
7
23
  cmap: str = "viridis"
8
24
  clabel: str | Sequence[tuple[str, str]] | None = None
9
25
  plot_range: tuple[float, float] | None = None
10
26
  log_scale: bool = False
11
27
  moving_average: bool = True
28
+ contour: bool = False
12
29
 
13
30
 
14
31
  _COLORS = {
@@ -112,6 +129,51 @@ ATTRIBUTES = {
112
129
  plot_range=(0, 50 / 3600000),
113
130
  )
114
131
  },
132
+ "mwr-single": {
133
+ "temperature": PlotMeta(
134
+ cmap="coolwarm",
135
+ plot_range=(223.15, 323.15),
136
+ contour=True,
137
+ ),
138
+ "potential_temperature": PlotMeta(
139
+ cmap="coolwarm",
140
+ plot_range=(260, 320),
141
+ contour=True,
142
+ ),
143
+ "equivalent_potential_temperature": PlotMeta(
144
+ cmap="coolwarm",
145
+ plot_range=(260, 320),
146
+ contour=True,
147
+ ),
148
+ "relative_humidity": PlotMeta(
149
+ plot_range=(0, 120),
150
+ contour=True,
151
+ ),
152
+ "absolute_humidity": PlotMeta(
153
+ plot_range=(1e-4, 1e-2),
154
+ log_scale=True,
155
+ contour=True,
156
+ ),
157
+ },
158
+ "mwr-multi": {
159
+ "temperature": PlotMeta(
160
+ cmap="coolwarm",
161
+ plot_range=(223.15, 323.15),
162
+ contour=True,
163
+ ),
164
+ "potential_temperature": PlotMeta(
165
+ cmap="coolwarm",
166
+ plot_range=(260, 320),
167
+ contour=True,
168
+ ),
169
+ "equivalent_potential_temperature": PlotMeta(
170
+ cmap="coolwarm", plot_range=(260, 320), contour=True
171
+ ),
172
+ "relative_humidity": PlotMeta(
173
+ plot_range=(0, 120),
174
+ contour=True,
175
+ ),
176
+ },
115
177
  "fallback": {
116
178
  "ier": PlotMeta(
117
179
  plot_range=(2e-5, 6e-5),
@@ -222,21 +284,6 @@ ATTRIBUTES = {
222
284
  cmap="RdBu_r",
223
285
  plot_range=(223.15, 323.15),
224
286
  ),
225
- "potential_temperature": PlotMeta(
226
- cmap="RdBu_r",
227
- plot_range=(260, 320),
228
- ),
229
- "equivalent_potential_temperature": PlotMeta(
230
- cmap="RdBu_r",
231
- plot_range=(260, 320),
232
- ),
233
- "absolute_humidity": PlotMeta(
234
- plot_range=(1e-4, 1e-2),
235
- log_scale=True,
236
- ),
237
- "relative_humidity": PlotMeta(
238
- plot_range=(0, 120),
239
- ),
240
287
  "cloud_fraction": PlotMeta(
241
288
  cmap="Blues",
242
289
  plot_range=(0, 1),
@@ -1,5 +1,6 @@
1
1
  """Misc. plotting routines for Cloudnet products."""
2
2
  import os.path
3
+ import textwrap
3
4
  from dataclasses import dataclass
4
5
  from datetime import date
5
6
 
@@ -22,6 +23,23 @@ from cloudnetpy.plotting.plot_meta import ATTRIBUTES, PlotMeta
22
23
 
23
24
  @dataclass
24
25
  class PlotParameters:
26
+ """
27
+ Class representing the parameters for plotting.
28
+
29
+ Attributes:
30
+ dpi: The resolution of the plot in dots per inch.
31
+ max_y: Maximum y-axis value (km) in 2D time / height plots.
32
+ title: Whether to display the title of the plot.
33
+ subtitle: Whether to display the subtitle of the plot.
34
+ mark_data_gaps: Whether to mark data gaps in the plot.
35
+ grid: Whether to display grid lines in the plot.
36
+ edge_tick_labels: Whether to display tick labels on the edges of the plot.
37
+ show_sources: Whether to display the sources of plotted data (i.e.
38
+ instruments and model).
39
+ footer_text: The text to display in the footer of the plot.
40
+ plot_meta: Additional metadata for the plot.
41
+ """
42
+
25
43
  dpi: float = 120
26
44
  max_y: int = 12
27
45
  title: bool = True
@@ -35,14 +53,20 @@ class PlotParameters:
35
53
 
36
54
 
37
55
  class Dimensions:
38
- """Dimensions of a generated figure in pixels."""
39
-
40
- width: int
41
- height: int
42
- margin_top: int
43
- margin_right: int
44
- margin_bottom: int
45
- margin_left: int
56
+ """
57
+ Dimensions of a generated figure in pixels. Elements such as the figure
58
+ title, labels, colorbar and legend are exluded from the margins.
59
+
60
+ Attributes:
61
+ width (int): Figure width in pixels.
62
+ height (int): Figure height in pixels.
63
+ margin_top (int): Space between top edge of image and plotted data in pixels.
64
+ margin_right (int): Space between right edge of image and plotted data
65
+ in pixels.
66
+ margin_bottom (int): Space between bottom edge of image and plotted
67
+ data in pixels.
68
+ margin_left (int): Space between left edge of image and plotted data in pixels.
69
+ """
46
70
 
47
71
  def __init__(self, fig, axes, pad_inches: float | None = None):
48
72
  if pad_inches is None:
@@ -133,7 +157,7 @@ class FigureData:
133
157
  raise PlottingError(msg)
134
158
  return valid_variables, variable_indices
135
159
 
136
- def _get_height(self) -> np.ndarray | None:
160
+ def _get_height(self) -> ndarray | None:
137
161
  m2km = 1e-3
138
162
  file_type = getattr(self.file, "cloudnet_file_type", "")
139
163
  if file_type == "model":
@@ -194,10 +218,11 @@ class SubPlot:
194
218
  self.ax.set_title(title, fontsize=14)
195
219
 
196
220
  def add_grid(self) -> None:
221
+ zorder = _get_zorder("grid")
197
222
  self.ax.xaxis.set_minor_locator(AutoMinorLocator(4))
198
- self.ax.grid(which="major", axis="x", color="k", lw=0.1)
199
- self.ax.grid(which="minor", axis="x", lw=0.1, color="k", ls=":")
200
- self.ax.grid(which="major", axis="y", lw=0.1, color="k", ls=":")
223
+ self.ax.grid(which="major", axis="x", color="k", lw=0.1, zorder=zorder)
224
+ self.ax.grid(which="minor", axis="x", lw=0.1, color="k", ls=":", zorder=zorder)
225
+ self.ax.grid(which="major", axis="y", lw=0.1, color="k", ls=":", zorder=zorder)
201
226
 
202
227
  def add_sources(self, figure_data: FigureData) -> None:
203
228
  source = getattr(self.variable, "source", None) or (
@@ -226,11 +251,20 @@ class SubPlot:
226
251
  def set_xlabel(self) -> None:
227
252
  self.ax.set_xlabel("Time (UTC)", fontsize=13)
228
253
 
229
- def show_footer(self, fig: Figure):
254
+ def show_footer(self, fig: Figure, ax: Axes) -> None:
230
255
  if isinstance(self.options.footer_text, str):
256
+ n = 50
257
+ if len(self.options.footer_text) > n:
258
+ wrapped_text = textwrap.fill(self.options.footer_text, n)
259
+ self.options.footer_text = "\n".join(wrapped_text.splitlines())
260
+
261
+ n_lines = self.options.footer_text.count("\n") + 1
262
+ y0 = ax.get_position().y0
263
+ y1 = ax.get_position().y1
264
+ y = (y1 - y0) * (n_lines * 0.06 + 0.1)
231
265
  fig.text(
232
266
  0.06,
233
- -0.05 + len(fig.get_axes()) / 50,
267
+ y0 - y,
234
268
  self.options.footer_text,
235
269
  fontsize=11,
236
270
  ha="left",
@@ -257,7 +291,7 @@ class Plot:
257
291
  self._is_log = sub_plot.plot_meta.log_scale
258
292
  self._ax = sub_plot.ax
259
293
 
260
- def _convert_units(self) -> str | None:
294
+ def _convert_units(self) -> str:
261
295
  multiply, add = "multiply", "add"
262
296
  units_conversion = {
263
297
  "rainfall_rate": (multiply, 360000, "mm h$^{-1}$"),
@@ -276,6 +310,7 @@ class Plot:
276
310
  self._data += conversion
277
311
  self._data_orig += conversion
278
312
  if units is not None:
313
+ self._plot_meta = self._plot_meta._replace(clabel=units)
279
314
  return units
280
315
  units = getattr(self.sub_plot.variable, "units", "")
281
316
  return _reformat_units(units)
@@ -297,10 +332,10 @@ class Plot:
297
332
  batch,
298
333
  *self._get_y_limits(),
299
334
  hatch="//",
300
- facecolor="grey",
301
- edgecolor="black",
302
- alpha=0.15,
335
+ facecolor="whitesmoke",
336
+ edgecolor="lightgrey",
303
337
  label="_nolegend_",
338
+ zorder=_get_zorder("data_gap"),
304
339
  )
305
340
 
306
341
  def _mark_gaps(self, figure_data: FigureData) -> None:
@@ -351,7 +386,7 @@ class Plot:
351
386
  self._data = data_new
352
387
  figure_data.time_including_gaps = time_new
353
388
 
354
- def _read_flagged_data(self, figure_data: FigureData) -> np.ndarray:
389
+ def _read_flagged_data(self, figure_data: FigureData) -> ndarray:
355
390
  flag_names = [
356
391
  f"{self.sub_plot.variable.name}_quality_flag",
357
392
  "temperature_quality_flag",
@@ -392,18 +427,16 @@ class Plot2D(Plot):
392
427
  self._ax.fill_between(
393
428
  time_batch,
394
429
  *self._get_y_limits(),
395
- facecolor="whitesmoke",
430
+ facecolor="white",
396
431
  alpha=0.7,
397
- edgecolor="grey",
398
432
  label="_nolegend_",
433
+ zorder=_get_zorder("flags"),
399
434
  )
400
435
 
401
436
  def _plot_segment_data(self, figure_data: FigureData) -> None:
402
- def _hide_segments(
403
- data_in: ma.MaskedArray,
404
- ) -> tuple[ma.MaskedArray, list, list]:
437
+ def _hide_segments() -> tuple[list, list]:
405
438
  if self._plot_meta.clabel is None:
406
- msg = f"No clabel defined for {self.sub_plot.variable.name}."
439
+ msg = "Missing clabel"
407
440
  raise ValueError(msg)
408
441
  labels = [x[0] for x in self._plot_meta.clabel]
409
442
  colors = [x[1] for x in self._plot_meta.clabel]
@@ -411,11 +444,11 @@ class Plot2D(Plot):
411
444
  indices = np.where(segments_to_hide)[0]
412
445
  for ind in np.flip(indices):
413
446
  del labels[ind], colors[ind]
414
- data_in[data_in == ind] = ma.masked
415
- data_in[data_in > ind] -= 1
416
- return data_in, colors, labels
447
+ self._data[self._data == ind] = ma.masked
448
+ self._data[self._data > ind] -= 1
449
+ return colors, labels
417
450
 
418
- data, cbar, clabel = _hide_segments(self._data)
451
+ cbar, clabel = _hide_segments()
419
452
  alt = self._screen_data_by_max_y(figure_data)
420
453
  image = self._ax.pcolorfast(
421
454
  figure_data.time_including_gaps,
@@ -424,6 +457,7 @@ class Plot2D(Plot):
424
457
  cmap=ListedColormap(cbar),
425
458
  vmin=-0.5,
426
459
  vmax=len(cbar) - 0.5,
460
+ zorder=_get_zorder("data"),
427
461
  )
428
462
  colorbar = self._init_colorbar(image)
429
463
  colorbar.set_ticks(np.arange(len(clabel)).tolist())
@@ -438,6 +472,7 @@ class Plot2D(Plot):
438
472
  self._data, vmin, vmax = lin2log(self._data, vmin, vmax)
439
473
 
440
474
  alt = self._screen_data_by_max_y(figure_data)
475
+
441
476
  image = self._ax.pcolorfast(
442
477
  figure_data.time_including_gaps,
443
478
  alt,
@@ -445,14 +480,30 @@ class Plot2D(Plot):
445
480
  cmap=plt.get_cmap(str(self._plot_meta.cmap)),
446
481
  vmin=vmin,
447
482
  vmax=vmax,
483
+ zorder=_get_zorder("data"),
448
484
  )
449
485
  cbar = self._init_colorbar(image)
450
486
  cbar.set_label(str(self._plot_meta.clabel), fontsize=13)
487
+
451
488
  if self._is_log:
452
489
  cbar.set_ticks(np.arange(vmin, vmax + 1).tolist())
453
490
  tick_labels = get_log_cbar_tick_labels(vmin, vmax)
454
491
  cbar.ax.set_yticklabels(tick_labels)
455
492
 
493
+ if self._plot_meta.contour:
494
+ time_length = len(figure_data.time_including_gaps)
495
+ step = max(1, time_length // 200)
496
+ ind_time = np.arange(0, time_length, step)
497
+ self._ax.contour(
498
+ figure_data.time_including_gaps[ind_time],
499
+ alt,
500
+ self._data[ind_time, :].T,
501
+ levels=np.linspace(vmin, vmax, num=10),
502
+ colors="black",
503
+ linewidths=0.5,
504
+ zorder=_get_zorder("contour"),
505
+ )
506
+
456
507
  def _screen_data_by_max_y(self, figure_data: FigureData) -> ndarray:
457
508
  if figure_data.height is None:
458
509
  msg = "No height information in the file."
@@ -466,7 +517,29 @@ class Plot2D(Plot):
466
517
 
467
518
 
468
519
  class Plot1D(Plot):
469
- def plot_tb(self, figure_data: FigureData, freq_ind: int):
520
+ def plot(self, figure_data: FigureData) -> None:
521
+ units = self._convert_units()
522
+ self._mark_gaps(figure_data)
523
+ self._ax.plot(
524
+ figure_data.time_including_gaps,
525
+ self._data,
526
+ label="_nolegend_",
527
+ **self._get_plot_options(),
528
+ zorder=_get_zorder("data"),
529
+ )
530
+ if self._plot_meta.moving_average:
531
+ self._plot_moving_average(figure_data)
532
+ self._fill_between_data_gaps(figure_data)
533
+ self.sub_plot.set_yax(ylabel=units, y_limits=self._get_y_limits())
534
+ pos = self._ax.get_position()
535
+ self._ax.set_position((pos.x0, pos.y0, pos.width * 0.965, pos.height))
536
+ if figure_data.is_mwrpy_product():
537
+ flags = self._read_flagged_data(figure_data)
538
+ if np.any(flags):
539
+ self._plot_flag_data(figure_data.time[flags], self._data_orig[flags])
540
+ self._add_legend()
541
+
542
+ def plot_tb(self, figure_data: FigureData, freq_ind: int) -> None:
470
543
  self._data = self._data[:, freq_ind]
471
544
  self._data_orig = self._data_orig[:, freq_ind]
472
545
  is_bad_zenith = self._get_bad_zenith_profiles(figure_data)
@@ -475,11 +548,28 @@ class Plot1D(Plot):
475
548
  flags = self._read_flagged_data(figure_data)[:, freq_ind]
476
549
  flags[is_bad_zenith] = False
477
550
  if np.any(flags):
478
- self.plot_flag_data(figure_data.time[flags], self._data_orig[flags])
479
- self.add_legend()
551
+ self._plot_flag_data(figure_data.time[flags], self._data_orig[flags])
552
+ self._add_legend()
480
553
  self.plot(figure_data)
554
+ self._show_frequency(figure_data, freq_ind)
555
+
556
+ def _show_frequency(self, figure_data: FigureData, freq_ind: int) -> None:
557
+ frequency = figure_data.file.variables["frequency"][freq_ind]
558
+ self._ax.text(
559
+ 0.0,
560
+ -0.13,
561
+ f"Freq: {frequency:.2f} GHz",
562
+ transform=self._ax.transAxes,
563
+ fontsize=12,
564
+ color="dimgrey",
565
+ bbox={
566
+ "facecolor": "white",
567
+ "linewidth": 0,
568
+ "boxstyle": "round",
569
+ },
570
+ )
481
571
 
482
- def plot_flag_data(self, time: np.ndarray, values: np.ndarray) -> None:
572
+ def _plot_flag_data(self, time: ndarray, values: ndarray) -> None:
483
573
  self._ax.plot(
484
574
  time,
485
575
  values,
@@ -487,10 +577,10 @@ class Plot1D(Plot):
487
577
  marker=".",
488
578
  lw=0,
489
579
  markersize=3,
490
- zorder=10,
580
+ zorder=_get_zorder("flags"),
491
581
  )
492
582
 
493
- def add_legend(self):
583
+ def _add_legend(self) -> None:
494
584
  self._ax.legend(
495
585
  ["Flagged data"],
496
586
  markerscale=3,
@@ -498,27 +588,6 @@ class Plot1D(Plot):
498
588
  frameon=False,
499
589
  )
500
590
 
501
- def plot(self, figure_data: FigureData):
502
- units = self._convert_units()
503
- self._mark_gaps(figure_data)
504
- self._ax.plot(
505
- figure_data.time_including_gaps,
506
- self._data,
507
- label="_nolegend_",
508
- **self._get_plot_options(),
509
- )
510
- if self._plot_meta.moving_average:
511
- self._plot_moving_average(figure_data)
512
- self._fill_between_data_gaps(figure_data)
513
- self.sub_plot.set_yax(ylabel=units, y_limits=self._get_y_limits())
514
- pos = self._ax.get_position()
515
- self._ax.set_position((pos.x0, pos.y0, pos.width * 0.965, pos.height))
516
- if figure_data.is_mwrpy_product():
517
- flags = self._read_flagged_data(figure_data)
518
- if np.any(flags):
519
- self.plot_flag_data(figure_data.time[flags], self._data_orig[flags])
520
- self.add_legend()
521
-
522
591
  def _get_y_limits(self) -> tuple[float, float]:
523
592
  percent_gap = 0.05
524
593
  fallback = (-percent_gap, percent_gap)
@@ -553,12 +622,7 @@ class Plot1D(Plot):
553
622
 
554
623
  return default_options
555
624
 
556
- @staticmethod
557
- def _get_line_width(time: np.ndarray) -> float:
558
- line_width = np.median(np.diff(time)) * 1000
559
- return min(max(line_width, 0.25), 0.9)
560
-
561
- def _plot_moving_average(self, figure_data: FigureData):
625
+ def _plot_moving_average(self, figure_data: FigureData) -> None:
562
626
  time = figure_data.time.copy()
563
627
  data = self._data_orig.copy()
564
628
  data, time = self._get_unmasked_values(data, time)
@@ -567,20 +631,32 @@ class Plot1D(Plot):
567
631
  gaps = self._find_time_gap_indices(time, max_gap_min=gap_time)
568
632
  sma[gaps] = np.nan
569
633
  if len(sma) == len(time):
570
- self._ax.plot(time, sma, color="slateblue", lw=2, label="_nolegend_")
634
+ self._ax.plot(
635
+ time,
636
+ sma,
637
+ color="slateblue",
638
+ lw=2,
639
+ label="_nolegend_",
640
+ zorder=_get_zorder("mean_curve"),
641
+ )
642
+
643
+ @staticmethod
644
+ def _get_line_width(time: ndarray) -> float:
645
+ line_width = np.median(np.diff(time)) * 1000
646
+ return min(max(line_width, 0.25), 0.9)
571
647
 
572
648
  @staticmethod
573
649
  def _get_unmasked_values(
574
650
  data: ma.MaskedArray,
575
- time: np.ndarray,
576
- ) -> tuple[np.ndarray, np.ndarray]:
651
+ time: ndarray,
652
+ ) -> tuple[ndarray, ndarray]:
577
653
  if not ma.is_masked(data):
578
654
  return data, time
579
655
  good_values = ~data.mask
580
656
  return data[good_values], time[good_values]
581
657
 
582
658
  @staticmethod
583
- def _get_bad_zenith_profiles(figure_data: FigureData) -> np.ndarray:
659
+ def _get_bad_zenith_profiles(figure_data: FigureData) -> ndarray:
584
660
  zenith_limit = 5
585
661
  valid_pointing_status = 0
586
662
  if "pointing_flag" in figure_data.file.variables:
@@ -598,10 +674,12 @@ class Plot1D(Plot):
598
674
 
599
675
  @staticmethod
600
676
  def _calculate_moving_average(
601
- data: np.ndarray, time: np.ndarray, window: float = 5
602
- ) -> np.ndarray:
677
+ data: ndarray, time: ndarray, window: float = 5
678
+ ) -> ndarray:
603
679
  if len(data) == 0:
604
680
  return np.array([])
681
+ if len(data) == 1:
682
+ return data
605
683
  time_delta_hours = np.median(np.diff(time))
606
684
  window_size = int(window / 60 / time_delta_hours)
607
685
  if window_size < 1:
@@ -622,6 +700,20 @@ def generate_figure(
622
700
  output_filename: os.PathLike | str | None = None,
623
701
  options: PlotParameters | None = None,
624
702
  ) -> Dimensions:
703
+ """
704
+ Generate a figure based on the given filename and variables.
705
+
706
+ Args:
707
+ filename: The path to the input file.
708
+ variables: A list of variable names to plot.
709
+ show: Whether to display the figure. Defaults to True.
710
+ output_filename: The path to save the figure. Defaults to None.
711
+ options: Additional plot parameters. Defaults to None.
712
+
713
+ Returns:
714
+ Dimensions: Dimensions of a generated figure in pixels.
715
+
716
+ """
625
717
  if options is None:
626
718
  options = PlotParameters()
627
719
 
@@ -660,7 +752,7 @@ def generate_figure(
660
752
  subplot.set_xlabel()
661
753
 
662
754
  if options.footer_text is not None:
663
- subplot.show_footer(fig)
755
+ subplot.show_footer(fig, ax)
664
756
 
665
757
  if output_filename:
666
758
  plt.savefig(output_filename, bbox_inches="tight")
@@ -711,6 +803,34 @@ def _get_max_gap_in_minutes(figure_data: FigureData) -> float:
711
803
  return max_allowed_gap.get(file_type, 10)
712
804
 
713
805
 
806
+ def _get_zorder(name: str) -> int:
807
+ zorder = {
808
+ "contour": 2,
809
+ "data_gap": 2,
810
+ "flags": 2,
811
+ }
812
+ return zorder.get(name, -1)
813
+
814
+
815
+ def find_batches_of_ones(array: ndarray) -> list[tuple[int, int]]:
816
+ """Find batches of ones in a binary array."""
817
+ starts = np.where(np.diff(np.hstack(([0], array))) == 1)[0]
818
+ stops = np.where(np.diff(np.hstack((array, [0]))) == -1)[0]
819
+ return list(zip(starts, stops, strict=True))
820
+
821
+
822
+ def screen_completely_masked_profiles(time: ndarray, data: ma.MaskedArray) -> tuple:
823
+ if not ma.is_masked(data):
824
+ return time, data
825
+ good_ind = np.where(np.any(~data.mask, axis=1))[0]
826
+ if len(good_ind) == 0:
827
+ msg = "All values masked in the file."
828
+ raise PlottingError(msg)
829
+ good_ind = np.append(good_ind, good_ind[-1] + 1)
830
+ good_ind = np.clip(good_ind, 0, len(time) - 1)
831
+ return time[good_ind], data[good_ind, :]
832
+
833
+
714
834
  def plot_2d(
715
835
  data: ma.MaskedArray,
716
836
  cmap: str = "viridis",
@@ -741,22 +861,3 @@ def plot_2d(
741
861
  if xlim is not None:
742
862
  plt.xlim(xlim)
743
863
  plt.show()
744
-
745
-
746
- def find_batches_of_ones(array: np.ndarray) -> list[tuple[int, int]]:
747
- """Find batches of ones in a binary array."""
748
- starts = np.where(np.diff(np.hstack(([0], array))) == 1)[0]
749
- stops = np.where(np.diff(np.hstack((array, [0]))) == -1)[0]
750
- return list(zip(starts, stops, strict=True))
751
-
752
-
753
- def screen_completely_masked_profiles(time: np.ndarray, data: ma.MaskedArray) -> tuple:
754
- if not ma.is_masked(data):
755
- return time, data
756
- good_ind = np.where(np.any(~data.mask, axis=1))[0]
757
- if len(good_ind) == 0:
758
- msg = "All values masked in the file."
759
- raise PlottingError(msg)
760
- good_ind = np.append(good_ind, good_ind[-1] + 1)
761
- good_ind = np.clip(good_ind, 0, len(time) - 1)
762
- return time[good_ind], data[good_ind, :]
@@ -4,9 +4,11 @@ from typing import Literal
4
4
  import netCDF4
5
5
  from mwrpy.level2.lev2_collocated import generate_lev2_multi as gen_multi
6
6
  from mwrpy.level2.lev2_collocated import generate_lev2_single as gen_single
7
+ from mwrpy.level2.write_lev2_nc import MissingInputData
7
8
  from mwrpy.version import __version__ as mwrpy_version
8
9
 
9
10
  from cloudnetpy import output, utils
11
+ from cloudnetpy.exceptions import ValidTimeStampError
10
12
  from cloudnetpy.products import product_tools
11
13
 
12
14
 
@@ -59,7 +61,10 @@ def _generate_product(
59
61
  fun = gen_multi if product == "multi" else gen_single
60
62
  with tempfile.TemporaryDirectory() as temp_dir:
61
63
  coeffs = product_tools.get_read_mwrpy_coeffs(mwr_l1c_file, temp_dir)
62
- fun(None, mwr_l1c_file, output_file, coeff_files=coeffs)
64
+ try:
65
+ fun(None, mwr_l1c_file, output_file, coeff_files=coeffs)
66
+ except MissingInputData as err:
67
+ raise ValidTimeStampError from err
63
68
  with (
64
69
  netCDF4.Dataset(mwr_l1c_file, "r") as nc_input,
65
70
  netCDF4.Dataset(output_file, "r+") as nc_output,
cloudnetpy/version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  MAJOR = 1
2
2
  MINOR = 56
3
- PATCH = 7
3
+ PATCH = 9
4
4
  __version__ = f"{MAJOR}.{MINOR}.{PATCH}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloudnetpy
3
- Version: 1.56.7
3
+ Version: 1.56.9
4
4
  Summary: Python package for Cloudnet processing
5
5
  Author: Simo Tukiainen
6
6
  License: MIT License
@@ -8,7 +8,7 @@ cloudnetpy/metadata.py,sha256=Bcu1a9UyUq61jomuZ0_6hYIOzf61e5qCXeiwLm46ikw,5040
8
8
  cloudnetpy/output.py,sha256=jD1pfBb4OQhVOrlhPEk-8FAi4bUW7zjAL468r6BPkJg,14586
9
9
  cloudnetpy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  cloudnetpy/utils.py,sha256=yY5a5HLuAks2uzA4XbbqsGFEmXoyqECn_TjD3sMa0lI,27193
11
- cloudnetpy/version.py,sha256=IIq9FNZdB_vhpd0fv4xahirM-oLXvsNF6f9fhjHcO3E,72
11
+ cloudnetpy/version.py,sha256=L5yTOc1IUQ52ak-q9sb2zqco0z_mepJx_hFuzl6k6JY,72
12
12
  cloudnetpy/categorize/__init__.py,sha256=gP5q3Vis1y9u9OWgA_idlbjfWXYN_S0IBSWdwBhL_uU,69
13
13
  cloudnetpy/categorize/atmos.py,sha256=cax3iRmvr7S-VkUZqz0JCfAN3WEsUVbGfH4zSHy1APo,12384
14
14
  cloudnetpy/categorize/atmos_utils.py,sha256=wndpwJxc2-QnNTkV8tc8I11Vs_WkNz9sVMX1fuGgUC4,3777
@@ -90,9 +90,9 @@ cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py,sha256=YeeJdS2JO5F645z
90
90
  cloudnetpy/model_evaluation/tests/unit/test_plotting.py,sha256=h9V8JKmrO4v9bOvv-UjRa06sZJQPhDNVHGBSImDdtkI,3277
91
91
  cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py,sha256=Ra3r4V0qbqkpDuaTYvEIbaasl0nZ5gmTLR4eGC0glBQ,9724
92
92
  cloudnetpy/model_evaluation/tests/unit/test_tools.py,sha256=Ia_VrLdV2NstX5gbx_3AZTOAlrgLAy_xFZ8fHYVX0xI,3817
93
- cloudnetpy/plotting/__init__.py,sha256=2-8x8d1AfAhfU15RwWhusD0Wot_g6Ob_jJoywbrTC7A,95
94
- cloudnetpy/plotting/plot_meta.py,sha256=xbiEU9Okx5L_WG7KtVnljxjpCYcgqyOLywBc49gibSQ,13033
95
- cloudnetpy/plotting/plotting.py,sha256=CDQ-MM5aDXEHDzsX2MJ9tXnErCxjr4odxk3ZleWh7KE,26906
93
+ cloudnetpy/plotting/__init__.py,sha256=lg9Smn4BI0dVBgnDLC3JVJ4GmwoSnO-qoSd4ApvwV6Y,107
94
+ cloudnetpy/plotting/plot_meta.py,sha256=NWI8ECKMypN5YyM9XKCAp1WEthbFlKMvilxqXmYSEK4,14631
95
+ cloudnetpy/plotting/plotting.py,sha256=AU7WWEfemlXXQ17IgybagQMx8JRJBF4y9DSv5DF_2Ho,30683
96
96
  cloudnetpy/products/__init__.py,sha256=2hRb5HG9hNrxH1if5laJkLeFeaZCd5W1q3hh4ewsX0E,273
97
97
  cloudnetpy/products/classification.py,sha256=J_FOMUSyxvFaT-hvdKVVcKPtuQ0u3V9PsV5xaIKzMjg,7843
98
98
  cloudnetpy/products/der.py,sha256=HAdPvbJySEqkIwDrdZDPnli_wnN2qwm72_D1a82ZWIs,12398
@@ -103,11 +103,11 @@ cloudnetpy/products/ier.py,sha256=IcGPlQahbwJjp3vOOrxWSYW2FPzbSV0KQL5eYECc4kU,77
103
103
  cloudnetpy/products/iwc.py,sha256=MUPuVKWgqOuuLRCGk3QY74uBZB_7P1qlinlP8nEvz9o,10124
104
104
  cloudnetpy/products/lwc.py,sha256=TbIR6kMwjbm63ed5orB1pkqx9ZBm8C5TF2JmT8WKdKI,18794
105
105
  cloudnetpy/products/mie_lu_tables.nc,sha256=It4fYpqJXlqOgL8jeZ-PxGzP08PMrELIDVe55y9ob58,16637951
106
- cloudnetpy/products/mwr_tools.py,sha256=TsVEqNwzoDv90TgzUSnJjMuc3C1KQ-hwsIZ8t0IdDJ4,4407
106
+ cloudnetpy/products/mwr_tools.py,sha256=PRm5aCULccUehU-Byk55wYhhEHseMjoAjGBu5TSyHao,4621
107
107
  cloudnetpy/products/product_tools.py,sha256=E8CSijBY8cr70BH2JFa0lGQ-RzI9EcHQ0Fzt8CQ8rY4,10442
108
108
  docs/source/conf.py,sha256=IKiFWw6xhUd8NrCg0q7l596Ck1d61XWeVjIFHVSG9Og,1490
109
- cloudnetpy-1.56.7.dist-info/LICENSE,sha256=wcZF72bdaoG9XugpyE95Juo7lBQOwLuTKBOhhtANZMM,1094
110
- cloudnetpy-1.56.7.dist-info/METADATA,sha256=bDIsf6AIbV9hyR07hGgEkMkq2IXHgW4UDNpKr8j5U7M,5733
111
- cloudnetpy-1.56.7.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
112
- cloudnetpy-1.56.7.dist-info/top_level.txt,sha256=ibSPWRr6ojS1i11rtBFz2_gkIe68mggj7aeswYfaOo0,16
113
- cloudnetpy-1.56.7.dist-info/RECORD,,
109
+ cloudnetpy-1.56.9.dist-info/LICENSE,sha256=wcZF72bdaoG9XugpyE95Juo7lBQOwLuTKBOhhtANZMM,1094
110
+ cloudnetpy-1.56.9.dist-info/METADATA,sha256=HgNqhO700XNh1uBoHccsDiA73n4R48XgLctfsB8Qczs,5733
111
+ cloudnetpy-1.56.9.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
112
+ cloudnetpy-1.56.9.dist-info/top_level.txt,sha256=ibSPWRr6ojS1i11rtBFz2_gkIe68mggj7aeswYfaOo0,16
113
+ cloudnetpy-1.56.9.dist-info/RECORD,,