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.
- cloudnetpy/categorize/atmos.py +46 -14
- cloudnetpy/categorize/atmos_utils.py +11 -1
- cloudnetpy/categorize/categorize.py +38 -21
- cloudnetpy/categorize/classify.py +31 -9
- cloudnetpy/categorize/containers.py +19 -7
- cloudnetpy/categorize/droplet.py +24 -8
- cloudnetpy/categorize/falling.py +17 -7
- cloudnetpy/categorize/freezing.py +19 -5
- cloudnetpy/categorize/insects.py +27 -14
- cloudnetpy/categorize/lidar.py +38 -36
- cloudnetpy/categorize/melting.py +19 -9
- cloudnetpy/categorize/model.py +28 -9
- cloudnetpy/categorize/mwr.py +4 -2
- cloudnetpy/categorize/radar.py +58 -22
- cloudnetpy/cloudnetarray.py +15 -6
- cloudnetpy/concat_lib.py +39 -16
- cloudnetpy/constants.py +7 -0
- cloudnetpy/datasource.py +39 -19
- cloudnetpy/instruments/basta.py +6 -2
- cloudnetpy/instruments/campbell_scientific.py +33 -16
- cloudnetpy/instruments/ceilo.py +30 -13
- cloudnetpy/instruments/ceilometer.py +76 -37
- cloudnetpy/instruments/cl61d.py +8 -3
- cloudnetpy/instruments/cloudnet_instrument.py +2 -1
- cloudnetpy/instruments/copernicus.py +27 -14
- cloudnetpy/instruments/disdrometer/common.py +51 -32
- cloudnetpy/instruments/disdrometer/parsivel.py +79 -48
- cloudnetpy/instruments/disdrometer/thies.py +10 -6
- cloudnetpy/instruments/galileo.py +23 -12
- cloudnetpy/instruments/hatpro.py +27 -11
- cloudnetpy/instruments/instruments.py +4 -1
- cloudnetpy/instruments/lufft.py +20 -11
- cloudnetpy/instruments/mira.py +60 -49
- cloudnetpy/instruments/mrr.py +31 -20
- cloudnetpy/instruments/nc_lidar.py +15 -6
- cloudnetpy/instruments/nc_radar.py +31 -22
- cloudnetpy/instruments/pollyxt.py +36 -21
- cloudnetpy/instruments/radiometrics.py +32 -18
- cloudnetpy/instruments/rpg.py +48 -22
- cloudnetpy/instruments/rpg_reader.py +39 -30
- cloudnetpy/instruments/vaisala.py +39 -27
- cloudnetpy/instruments/weather_station.py +15 -11
- cloudnetpy/metadata.py +3 -1
- cloudnetpy/model_evaluation/file_handler.py +31 -21
- cloudnetpy/model_evaluation/metadata.py +3 -1
- cloudnetpy/model_evaluation/model_metadata.py +1 -1
- cloudnetpy/model_evaluation/plotting/plot_tools.py +20 -15
- cloudnetpy/model_evaluation/plotting/plotting.py +114 -64
- cloudnetpy/model_evaluation/products/advance_methods.py +48 -28
- cloudnetpy/model_evaluation/products/grid_methods.py +44 -19
- cloudnetpy/model_evaluation/products/model_products.py +22 -18
- cloudnetpy/model_evaluation/products/observation_products.py +15 -9
- cloudnetpy/model_evaluation/products/product_resampling.py +14 -4
- cloudnetpy/model_evaluation/products/tools.py +16 -7
- cloudnetpy/model_evaluation/statistics/statistical_methods.py +28 -15
- cloudnetpy/model_evaluation/tests/e2e/conftest.py +3 -3
- cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +14 -13
- cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +14 -13
- cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +9 -5
- cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +14 -13
- cloudnetpy/model_evaluation/tests/unit/conftest.py +11 -11
- cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +33 -27
- cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +83 -83
- cloudnetpy/model_evaluation/tests/unit/test_model_products.py +23 -21
- cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +24 -25
- cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +40 -39
- cloudnetpy/model_evaluation/tests/unit/test_plotting.py +12 -11
- cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +30 -30
- cloudnetpy/model_evaluation/tests/unit/test_tools.py +18 -17
- cloudnetpy/model_evaluation/utils.py +3 -2
- cloudnetpy/output.py +45 -19
- cloudnetpy/plotting/plot_meta.py +35 -11
- cloudnetpy/plotting/plotting.py +172 -104
- cloudnetpy/products/classification.py +20 -8
- cloudnetpy/products/der.py +25 -10
- cloudnetpy/products/drizzle.py +41 -26
- cloudnetpy/products/drizzle_error.py +10 -5
- cloudnetpy/products/drizzle_tools.py +43 -24
- cloudnetpy/products/ier.py +10 -5
- cloudnetpy/products/iwc.py +16 -9
- cloudnetpy/products/lwc.py +34 -12
- cloudnetpy/products/mwr_multi.py +4 -1
- cloudnetpy/products/mwr_single.py +4 -1
- cloudnetpy/products/product_tools.py +33 -10
- cloudnetpy/utils.py +175 -74
- cloudnetpy/version.py +1 -1
- {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/METADATA +11 -10
- cloudnetpy-1.55.22.dist-info/RECORD +114 -0
- docs/source/conf.py +2 -2
- cloudnetpy-1.55.20.dist-info/RECORD +0 -114
- {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/LICENSE +0 -0
- {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/WHEEL +0 -0
- {cloudnetpy-1.55.20.dist-info → cloudnetpy-1.55.22.dist-info}/top_level.txt +0 -0
cloudnetpy/plotting/plotting.py
CHANGED
@@ -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
|
-
|
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(
|
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
|
-
|
187
|
+
tb_index = int(tb_ind)
|
177
188
|
except ValueError:
|
178
|
-
|
179
|
-
_plot_instrument_data(
|
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,
|
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
|
-
|
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,
|
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,
|
221
|
+
_plot_segment_data(ax, field_screened, name, ax_value)
|
196
222
|
|
197
223
|
else:
|
198
|
-
_plot_colormesh_data(ax,
|
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,
|
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,
|
236
|
+
time: np.ndarray,
|
237
|
+
data: ma.MaskedArray,
|
238
|
+
max_allowed_gap: float = 1,
|
211
239
|
) -> tuple:
|
212
|
-
|
213
|
-
|
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
|
229
|
-
data_new = np.insert(data_new,
|
230
|
-
mask_new = np.insert(mask_new,
|
231
|
-
time_new = np.insert(time_new,
|
232
|
-
time_new = np.insert(time_new,
|
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
|
-
|
240
|
-
data_new = np.insert(data_new,
|
241
|
-
mask_new = np.insert(mask_new,
|
242
|
-
time_new = np.insert(time_new,
|
243
|
-
time_new = np.insert(time_new,
|
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.
|
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,
|
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
|
-
|
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
|
-
|
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
|
-
|
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,
|
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,
|
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
|
-
|
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
|
-
|
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,
|
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
|
-
|
582
|
-
quality_flag =
|
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"],
|
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,
|
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
|
-
|
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,
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
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
|
-
|
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,
|
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,
|
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(
|
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)
|