tobac 1.6.2__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 (53) hide show
  1. tobac/__init__.py +112 -0
  2. tobac/analysis/__init__.py +31 -0
  3. tobac/analysis/cell_analysis.py +628 -0
  4. tobac/analysis/feature_analysis.py +212 -0
  5. tobac/analysis/spatial.py +619 -0
  6. tobac/centerofgravity.py +226 -0
  7. tobac/feature_detection.py +1758 -0
  8. tobac/merge_split.py +324 -0
  9. tobac/plotting.py +2321 -0
  10. tobac/segmentation/__init__.py +10 -0
  11. tobac/segmentation/watershed_segmentation.py +1316 -0
  12. tobac/testing.py +1179 -0
  13. tobac/tests/segmentation_tests/test_iris_xarray_segmentation.py +0 -0
  14. tobac/tests/segmentation_tests/test_segmentation.py +1183 -0
  15. tobac/tests/segmentation_tests/test_segmentation_time_pad.py +104 -0
  16. tobac/tests/test_analysis_spatial.py +1109 -0
  17. tobac/tests/test_convert.py +265 -0
  18. tobac/tests/test_datetime.py +216 -0
  19. tobac/tests/test_decorators.py +148 -0
  20. tobac/tests/test_feature_detection.py +1321 -0
  21. tobac/tests/test_generators.py +273 -0
  22. tobac/tests/test_import.py +24 -0
  23. tobac/tests/test_iris_xarray_match_utils.py +244 -0
  24. tobac/tests/test_merge_split.py +351 -0
  25. tobac/tests/test_pbc_utils.py +497 -0
  26. tobac/tests/test_sample_data.py +197 -0
  27. tobac/tests/test_testing.py +747 -0
  28. tobac/tests/test_tracking.py +714 -0
  29. tobac/tests/test_utils.py +650 -0
  30. tobac/tests/test_utils_bulk_statistics.py +789 -0
  31. tobac/tests/test_utils_coordinates.py +328 -0
  32. tobac/tests/test_utils_internal.py +97 -0
  33. tobac/tests/test_xarray_utils.py +232 -0
  34. tobac/tracking.py +613 -0
  35. tobac/utils/__init__.py +27 -0
  36. tobac/utils/bulk_statistics.py +360 -0
  37. tobac/utils/datetime.py +184 -0
  38. tobac/utils/decorators.py +540 -0
  39. tobac/utils/general.py +753 -0
  40. tobac/utils/generators.py +87 -0
  41. tobac/utils/internal/__init__.py +2 -0
  42. tobac/utils/internal/coordinates.py +430 -0
  43. tobac/utils/internal/iris_utils.py +462 -0
  44. tobac/utils/internal/label_props.py +82 -0
  45. tobac/utils/internal/xarray_utils.py +439 -0
  46. tobac/utils/mask.py +364 -0
  47. tobac/utils/periodic_boundaries.py +419 -0
  48. tobac/wrapper.py +244 -0
  49. tobac-1.6.2.dist-info/METADATA +154 -0
  50. tobac-1.6.2.dist-info/RECORD +53 -0
  51. tobac-1.6.2.dist-info/WHEEL +5 -0
  52. tobac-1.6.2.dist-info/licenses/LICENSE +29 -0
  53. tobac-1.6.2.dist-info/top_level.txt +1 -0
tobac/plotting.py ADDED
@@ -0,0 +1,2321 @@
1
+ """Provide methods for plotting analyzed data.
2
+
3
+ Plotting routines including both visualizations for
4
+ the entire dataset including all tracks, and detailed
5
+ visualizations for individual cells and their properties.
6
+
7
+ References
8
+ ----------
9
+ .. Heikenfeld, M., Marinescu, P. J., Christensen, M.,
10
+ Watson-Parris, D., Senf, F., van den Heever, S. C.
11
+ & Stier, P. (2019). tobac 1.2: towards a flexible
12
+ framework for tracking and analysis of clouds in
13
+ diverse datasets. Geoscientific Model Development,
14
+ 12(11), 4551-4570.
15
+ """
16
+
17
+ from __future__ import annotations
18
+ import warnings
19
+ import logging
20
+
21
+ import numpy as np
22
+ import matplotlib as mpl
23
+
24
+ from tobac.analysis.cell_analysis import (
25
+ lifetime_histogram,
26
+ histogram_cellwise,
27
+ )
28
+ from tobac.analysis.feature_analysis import histogram_featurewise
29
+ from tobac.utils import decorators
30
+ from tobac.utils.internal.coordinates import find_dataframe_horizontal_coords
31
+
32
+
33
+ def plot_tracks_mask_field_loop(
34
+ track,
35
+ field,
36
+ mask,
37
+ features,
38
+ axes=None,
39
+ name=None,
40
+ plot_dir="./",
41
+ figsize=(10.0 / 2.54, 10.0 / 2.54),
42
+ dpi=300,
43
+ margin_left=0.05,
44
+ margin_right=0.05,
45
+ margin_bottom=0.05,
46
+ margin_top=0.05,
47
+ **kwargs,
48
+ ):
49
+ """Plot field, feature positions and segments
50
+ onto individual maps for all timeframes and
51
+ save them as pngs.
52
+
53
+ Parameters
54
+ ----------
55
+ track : pandas.DataFrame
56
+ Output of linking_trackpy.
57
+
58
+ field : iris.cube.Cube
59
+ Original input data.
60
+
61
+ mask : iris.cube.Cube
62
+ Cube containing mask (int id for tacked volumes, 0
63
+ everywhere else). Output of the segmentation step.
64
+
65
+ features : pandas.DataFrame
66
+ Output of the feature detection.
67
+
68
+ axes : cartopy.mpl.geoaxes.GeoAxesSubplot, optional
69
+ Not used. Default is None.
70
+
71
+ name : str, optional
72
+ Filename without file extension. Same for all pngs. If None,
73
+ the name of the field is used. Default is None.
74
+
75
+ plot_dir : str, optional
76
+ Path where the plots will be saved. Default is './'.
77
+
78
+ figsize : tuple of floats, optional
79
+ Width, height of the plot in inches.
80
+ Default is (10/2.54, 10/2.54).
81
+
82
+ dpi : int, optional
83
+ Plot resolution. Default is 300.
84
+
85
+ margin_left : float, optional
86
+ The position of the left edge of the axes, as a
87
+ fraction of the figure width. Default is 0.05.
88
+
89
+ margin_right : float, optional
90
+ The position of the right edge of the axes, as a
91
+ fraction of the figure width. Default is 0.05.
92
+
93
+ margin_bottom : float, optional
94
+ The position of the bottom edge of the axes, as a
95
+ fraction of the figure width. Default is 0.05.
96
+
97
+ margin_top : float, optional
98
+ The position of the top edge of the axes, as a
99
+ fraction of the figure width. Default is 0.05.
100
+
101
+ **kwargs
102
+
103
+ Returns
104
+ -------
105
+ None
106
+ """
107
+
108
+ mpl_backend = mpl.get_backend()
109
+ if mpl_backend != "agg":
110
+ warnings.warn(
111
+ "When using tobac plotting functions that render a figure, you may need "
112
+ "to set the Matplotlib backend to 'agg' by `matplotlib.use('agg')."
113
+ )
114
+ import matplotlib.pyplot as plt
115
+ import cartopy.crs as ccrs
116
+ import os
117
+ from iris import Constraint
118
+
119
+ os.makedirs(plot_dir, exist_ok=True)
120
+ time = mask.coord("time")
121
+ if name is None:
122
+ name = field.name()
123
+ for time_i in time.points:
124
+ datetime_i = time.units.num2date(time_i)
125
+ constraint_time = Constraint(time=datetime_i)
126
+ fig1, ax1 = plt.subplots(
127
+ ncols=1,
128
+ nrows=1,
129
+ figsize=figsize,
130
+ subplot_kw={"projection": ccrs.PlateCarree()},
131
+ )
132
+ datestring_file = datetime_i.strftime("%Y-%m-%d_%H:%M:%S")
133
+ field_i = field.extract(constraint_time)
134
+ mask_i = mask.extract(constraint_time)
135
+ track_i = track[track["time"] == datetime_i]
136
+ features_i = features[features["time"] == datetime_i]
137
+ ax1 = plot_tracks_mask_field(
138
+ track=track_i,
139
+ field=field_i,
140
+ mask=mask_i,
141
+ features=features_i,
142
+ axes=ax1,
143
+ **kwargs,
144
+ )
145
+ fig1.subplots_adjust(
146
+ left=margin_left,
147
+ bottom=margin_bottom,
148
+ right=1 - margin_right,
149
+ top=1 - margin_top,
150
+ )
151
+ os.makedirs(plot_dir, exist_ok=True)
152
+ savepath_png = os.path.join(plot_dir, name + "_" + datestring_file + ".png")
153
+ fig1.savefig(savepath_png, dpi=dpi)
154
+ logging.debug("Figure plotted to " + str(savepath_png))
155
+
156
+ plt.close()
157
+
158
+
159
+ @decorators.xarray_to_iris()
160
+ def plot_tracks_mask_field(
161
+ track,
162
+ field,
163
+ mask,
164
+ features,
165
+ axes=None,
166
+ axis_extent=None,
167
+ plot_outline=True,
168
+ plot_marker=True,
169
+ marker_track="x",
170
+ markersize_track=4,
171
+ plot_number=True,
172
+ plot_features=False,
173
+ marker_feature=None,
174
+ markersize_feature=None,
175
+ title=None,
176
+ title_str=None,
177
+ vmin=None,
178
+ vmax=None,
179
+ n_levels=50,
180
+ cmap="viridis",
181
+ extend="neither",
182
+ orientation_colorbar="horizontal",
183
+ pad_colorbar=0.05,
184
+ label_colorbar=None,
185
+ fraction_colorbar=0.046,
186
+ rasterized=True,
187
+ linewidth_contour=1,
188
+ ):
189
+ """Plot field, features and segments of a timeframe and
190
+ on a map projection. It is required to pass vmin, vmax,
191
+ axes and axis_extent as keyword arguments.
192
+
193
+ Parameters
194
+ ----------
195
+ track : pandas.DataFrame
196
+ One or more timeframes of a dataframe generated by
197
+ linking_trackpy.
198
+
199
+ field : iris.cube.Cube
200
+ One frame/time step of the original input data.
201
+
202
+ mask : iris.cube.Cube
203
+ One frame/time step of the Cube containing mask (int id
204
+ for tracked volumes 0 everywhere else), output of the
205
+ segmentation step.
206
+
207
+ features : pandas.DataFrame
208
+ Output of the feature detection, one or more frames/time steps.
209
+
210
+ axes : cartopy.mpl.geoaxes.GeoAxesSubplot
211
+ GeoAxesSubplot to use for plotting. Default is None.
212
+
213
+ axis_extent : ndarray
214
+ Array containing the bounds of the longitude and latitude
215
+ values. The structure is
216
+ [long_min, long_max, lat_min, lat_max]. Default is None.
217
+
218
+ plot_outline : bool, optional
219
+ Boolean defining whether the outlines of the segments are
220
+ plotted. Default is True.
221
+
222
+ plot_marker : bool, optional
223
+ Boolean defining whether the positions of the features from
224
+ the track dataframe are plotted. Default is True.
225
+
226
+ marker_track : str, optional
227
+ String defining the shape of the marker for the feature
228
+ positions from the track dataframe. Default is 'x'.
229
+
230
+ markersize_track : int, optional
231
+ Int defining the size of the marker for the feature
232
+ positions from the track dataframe. Default is 4.
233
+
234
+ plot_number : bool, optional
235
+ Boolean defining wether the index of the cells
236
+ is plotted next to the individual feature position.
237
+ Default is True.
238
+
239
+ plot_features : bool, optional
240
+ Boolean defining wether the positions of the features from
241
+ the features dataframe are plotted. Default is True.
242
+
243
+ marker_feature : optional
244
+ String defining the shape of the marker for the feature
245
+ positions from the features dataframe. Default is None.
246
+
247
+ markersize_feature : optional
248
+ Int defining the size of the marker for the feature
249
+ positions from the features dataframe. Default is None.
250
+
251
+ title : str, optional
252
+ Flag determining the title of the plot. 'datestr' uses
253
+ date and time of the field. None sets not title.
254
+ Default is None.
255
+
256
+ title_str : str, optional
257
+ Additional string added to the beginning of the title.
258
+ Default is None.
259
+
260
+ vmin : float
261
+ Lower bound of the colorbar. Default is None.
262
+
263
+ vmax : float
264
+ Upper bound of the colorbar. Default is None.
265
+
266
+ n_levels : int, optional
267
+ Number of levels of the contour plot of the field.
268
+ Default is 50.
269
+
270
+ cmap : {'viridis',...}, optional
271
+ Colormap of the countour plot of the field.
272
+ matplotlib.colors. Default is 'viridis'.
273
+
274
+ extend : str, optional
275
+ Determines the coloring of values that are
276
+ outside the levels range. If 'neither', values outside
277
+ the levels range are not colored. If 'min', 'max' or
278
+ 'both', color the values below, above or below and above
279
+ the levels range. Values below min(levels) and above
280
+ max(levels) are mapped to the under/over values of the
281
+ Colormap. Default is 'neither'.
282
+
283
+ orientation_colorbar : str, optional
284
+ Orientation of the colorbar, 'horizontal' or 'vertical'
285
+ Default is 'horizontal'.
286
+
287
+ pad_colorbar : float, optional
288
+ Fraction of original axes between colorbar and new
289
+ image axes. Default is 0.05.
290
+
291
+ label_colorbar : str, optional
292
+ Label of the colorbar. If none, name and unit of
293
+ the field are used. Default is None.
294
+
295
+ fraction_colorbar : float, optional
296
+ Fraction of original axes to use for colorbar.
297
+ Default is 0.046.
298
+
299
+ rasterized : bool, optional
300
+ True enables, False disables rasterization.
301
+ Default is True.
302
+
303
+ linewidth_contour : int, optional
304
+ Linewidth of the contour plot of the segments.
305
+ Default is 1.
306
+
307
+ Returns
308
+ -------
309
+ axes : cartopy.mpl.geoaxes.GeoAxesSubplot
310
+ Axes with the plot.
311
+
312
+ Raises
313
+ ------
314
+ ValueError
315
+ If axes are not cartopy.mpl.geoaxes.GeoAxesSubplot.
316
+
317
+ If mask.ndim is neither 2 nor 3.
318
+ """
319
+
320
+ import matplotlib.pyplot as plt
321
+
322
+ import cartopy
323
+ from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
324
+ import iris.plot as iplt
325
+ from matplotlib.ticker import MaxNLocator
326
+ import cartopy.feature as cfeature
327
+ from .utils import mask_features, mask_features_surface
328
+ from matplotlib import ticker
329
+
330
+ if type(axes) is not cartopy.mpl.geoaxes.GeoAxesSubplot:
331
+ raise ValueError("axes had to be cartopy.mpl.geoaxes.GeoAxesSubplot")
332
+
333
+ datestr = (
334
+ field.coord("time")
335
+ .units.num2date(field.coord("time").points[0])
336
+ .strftime("%Y-%m-%d %H:%M:%S")
337
+ )
338
+ if title == "datestr":
339
+ if title_str is None:
340
+ titlestring = datestr
341
+ elif type(title_str is str):
342
+ titlestring = title + " " + datestr
343
+ axes.set_title(titlestring, horizontalalignment="left", loc="left")
344
+
345
+ gl = axes.gridlines(draw_labels=True)
346
+ majorLocator = MaxNLocator(nbins=5, steps=[1, 2, 5, 10])
347
+ gl.xlocator = majorLocator
348
+ gl.ylocator = majorLocator
349
+ gl.xformatter = LONGITUDE_FORMATTER
350
+ axes.tick_params(axis="both", which="major")
351
+ gl.yformatter = LATITUDE_FORMATTER
352
+ gl.xlabels_top = False
353
+ gl.ylabels_right = False
354
+ axes.coastlines("10m")
355
+ # rivers=cfeature.NaturalEarthFeature(category='physical', name='rivers_lake_centerlines',scale='10m',facecolor='none')
356
+ lakes = cfeature.NaturalEarthFeature(
357
+ category="physical", name="lakes", scale="10m", facecolor="none"
358
+ )
359
+ axes.add_feature(lakes, edgecolor="black")
360
+ axes.set_xlabel("longitude")
361
+ axes.set_ylabel("latitude")
362
+
363
+ # Plot the background field
364
+ if np.any(
365
+ ~np.isnan(field.data)
366
+ ): # check if field to plot is not only nan, which causes error:
367
+ plot_field = iplt.contourf(
368
+ field,
369
+ coords=["longitude", "latitude"],
370
+ levels=np.linspace(vmin, vmax, num=n_levels),
371
+ extend=extend,
372
+ axes=axes,
373
+ cmap=cmap,
374
+ vmin=vmin,
375
+ vmax=vmax,
376
+ zorder=1,
377
+ )
378
+ if rasterized:
379
+ axes.set_rasterization_zorder(1)
380
+ # create colorbar for background field:
381
+ cbar = plt.colorbar(
382
+ plot_field,
383
+ orientation=orientation_colorbar,
384
+ pad=pad_colorbar,
385
+ fraction=fraction_colorbar,
386
+ ax=axes,
387
+ )
388
+ if label_colorbar is None:
389
+ label_colorbar = field.name() + "(" + field.units.symbol + ")"
390
+ if orientation_colorbar == "horizontal":
391
+ cbar.ax.set_xlabel(label_colorbar)
392
+ elif orientation_colorbar == "vertical":
393
+ cbar.ax.set_ylabel(label_colorbar)
394
+ tick_locator = ticker.MaxNLocator(nbins=5)
395
+ cbar.locator = tick_locator
396
+ cbar.update_ticks()
397
+
398
+ colors_mask = ["darkred", "orange", "crimson", "red", "darkorange"]
399
+
400
+ # if marker_feature is not explicitly given, set it to marker_track (will then be overwritten by the coloured markers)
401
+ if marker_feature is None:
402
+ maker_feature = marker_track
403
+ if markersize_feature is None:
404
+ makersize_feature = markersize_track
405
+
406
+ # Plot the identified features by looping over rows of DataFrame:
407
+ features_lat_dim, features_lon_dim, _ = find_dataframe_horizontal_coords(
408
+ features, coord_type="latlon"
409
+ )
410
+
411
+ if plot_features:
412
+ for _, row in features.iterrows():
413
+ axes.plot(
414
+ row[features_lon_dim],
415
+ row[features_lat_dim],
416
+ color="grey",
417
+ marker=maker_feature,
418
+ markersize=makersize_feature,
419
+ )
420
+
421
+ # restrict features to featues inside axis extent
422
+ track_lat_dim, track_lon_dim, _ = find_dataframe_horizontal_coords(
423
+ features, coord_type="latlon"
424
+ )
425
+
426
+ track = track.loc[
427
+ (track[track_lon_dim] > axis_extent[0])
428
+ & (track[track_lon_dim] < axis_extent[1])
429
+ & (track[track_lat_dim] > axis_extent[2])
430
+ & (track[track_lat_dim] < axis_extent[3])
431
+ ]
432
+
433
+ # Plot tracked features by looping over rows of Dataframe
434
+ for i_row, row in track.iterrows():
435
+ feature = row["feature"]
436
+ cell = row["cell"]
437
+ if not np.isnan(cell):
438
+ color = colors_mask[int(cell % len(colors_mask))]
439
+
440
+ if plot_number:
441
+ cell_string = " " + str(int(row["cell"]))
442
+ axes.text(
443
+ row[features_lon_dim],
444
+ row[features_lat_dim],
445
+ cell_string,
446
+ color=color,
447
+ fontsize=6,
448
+ clip_on=True,
449
+ )
450
+
451
+ else:
452
+ color = "grey"
453
+
454
+ if plot_outline:
455
+ mask_i = None
456
+ # if mask is 3D, create surface projection, if mask is 2D keep the mask
457
+ if mask.ndim == 2:
458
+ mask_i = mask_features(mask, feature, masked=False)
459
+ elif mask.ndim == 3:
460
+ mask_i = mask_features_surface(
461
+ mask, feature, masked=False, z_coord="model_level_number"
462
+ )
463
+ else:
464
+ raise ValueError("mask has shape that cannot be understood")
465
+ # plot countour lines around the edges of the mask
466
+ iplt.contour(
467
+ mask_i,
468
+ coords=["longitude", "latitude"],
469
+ levels=[0, feature],
470
+ colors=color,
471
+ linewidths=linewidth_contour,
472
+ axes=axes,
473
+ )
474
+
475
+ if plot_marker:
476
+ axes.plot(
477
+ row[features_lon_dim],
478
+ row[features_lat_dim],
479
+ color=color,
480
+ marker=marker_track,
481
+ markersize=markersize_track,
482
+ )
483
+
484
+ axes.set_extent(axis_extent)
485
+ return axes
486
+
487
+
488
+ @decorators.iris_to_xarray()
489
+ def animation_mask_field(
490
+ track, features, field, mask, interval=500, figsize=(10, 10), **kwargs
491
+ ):
492
+ """Create animation of field, features and segments of
493
+ all timeframes.
494
+
495
+ Parameters
496
+ ----------
497
+ track : pandas.DataFrame
498
+ Output of linking_trackpy.
499
+
500
+ features : pandas.DataFrame
501
+ Output of the feature detection.
502
+
503
+ field : iris.cube.Cube
504
+ Original input data.
505
+
506
+ mask : iris.cube.Cube
507
+ Cube containing mask (int id for tacked volumes 0
508
+ everywhere else), output of the segmentation step.
509
+
510
+ interval : int, optional
511
+ Delay between frames in milliseconds.
512
+ Default is 500.
513
+
514
+ figsize : tupel of float, optional
515
+ Width, height of the plot in inches.
516
+ Default is (10, 10).
517
+
518
+ **kwargs
519
+
520
+ Returns
521
+ -------
522
+ animation : matplotlib.animation.FuncAnimation
523
+ Created animation as object.
524
+ """
525
+
526
+ mpl_backend = mpl.get_backend()
527
+ if mpl_backend != "agg":
528
+ warnings.warn(
529
+ "When using tobac plotting functions that render a figure, you may need "
530
+ "to set the Matplotlib backend to 'agg' by `matplotlib.use('agg')."
531
+ )
532
+ import matplotlib.pyplot as plt
533
+
534
+ import cartopy.crs as ccrs
535
+ import matplotlib.pyplot as plt
536
+ import matplotlib.animation
537
+ from iris import Constraint
538
+
539
+ fig = plt.figure(figsize=figsize)
540
+ plt.close()
541
+
542
+ tracks_gb = track.groupby("time")
543
+ features_gb = features.groupby("time")
544
+
545
+ def update(time_in):
546
+ fig.clf()
547
+ ax = fig.add_subplot(111, projection=ccrs.PlateCarree())
548
+ field_i = field.sel(time=time_in)
549
+ mask_i = mask.sel(time=time_in)
550
+ track_i = tracks_gb.get_group(time_in)
551
+ features_i = features_gb.get_group(time_in)
552
+ # fig1,ax1=plt.subplots(ncols=1, nrows=1,figsize=figsize, subplot_kw={'projection': ccrs.PlateCarree()})
553
+ plot_tobac = plot_tracks_mask_field(
554
+ track_i, field=field_i, mask=mask_i, features=features_i, axes=ax, **kwargs
555
+ )
556
+ ax.set_title("{}".format(time_in))
557
+
558
+ animation = matplotlib.animation.FuncAnimation(
559
+ fig,
560
+ update,
561
+ init_func=None,
562
+ frames=field.time.values,
563
+ interval=interval,
564
+ blit=False,
565
+ )
566
+ return animation
567
+
568
+
569
+ def plot_mask_cell_track_follow(
570
+ cell,
571
+ track,
572
+ cog,
573
+ features,
574
+ mask_total,
575
+ field_contour,
576
+ field_filled,
577
+ width=10000,
578
+ name="test",
579
+ plotdir="./",
580
+ file_format=["png"],
581
+ figsize=(10 / 2.54, 10 / 2.54),
582
+ dpi=300,
583
+ **kwargs,
584
+ ):
585
+ """Make plots for all cells centred around cell and with one background field as filling and one background field as contrours
586
+ Input:
587
+ Output:
588
+ """
589
+ warnings.warn(
590
+ "plot_mask_cell_track_follow is depreciated and will be removed or significantly changed in v2.0.",
591
+ DeprecationWarning,
592
+ )
593
+
594
+ mpl_backend = mpl.get_backend()
595
+ if mpl_backend != "agg":
596
+ warnings.warn(
597
+ "When using tobac plotting functions that render a figure, you may need "
598
+ "to set the Matplotlib backend to 'agg' by `matplotlib.use('agg')."
599
+ )
600
+ import matplotlib.pyplot as plt
601
+
602
+ from iris import Constraint
603
+ from numpy import unique
604
+ import os
605
+
606
+ track_cell = track[track["cell"] == cell]
607
+ for i_row, row in track_cell.iterrows():
608
+ constraint_time = Constraint(time=row["time"])
609
+ constraint_x = Constraint(
610
+ projection_x_coordinate=lambda cell: row["projection_x_coordinate"] - width
611
+ < cell
612
+ < row["projection_x_coordinate"] + width
613
+ )
614
+ constraint_y = Constraint(
615
+ projection_y_coordinate=lambda cell: row["projection_y_coordinate"] - width
616
+ < cell
617
+ < row["projection_y_coordinate"] + width
618
+ )
619
+ constraint = constraint_time & constraint_x & constraint_y
620
+ mask_total_i = mask_total.extract(constraint)
621
+ if field_contour is None:
622
+ field_contour_i = None
623
+ else:
624
+ field_contour_i = field_contour.extract(constraint)
625
+ if field_filled is None:
626
+ field_filled_i = None
627
+ else:
628
+ field_filled_i = field_filled.extract(constraint)
629
+
630
+ cells = list(unique(mask_total_i.core_data()))
631
+ if cell not in cells:
632
+ cells.append(cell)
633
+ if 0 in cells:
634
+ cells.remove(0)
635
+ track_i = track[track["cell"].isin(cells)]
636
+ track_i = track_i[track_i["time"] == row["time"]]
637
+ if cog is None:
638
+ cog_i = None
639
+ else:
640
+ cog_i = cog[cog["cell"].isin(cells)]
641
+ cog_i = cog_i[cog_i["time"] == row["time"]]
642
+
643
+ if features is None:
644
+ features_i = None
645
+ else:
646
+ features_i = features[features["time"] == row["time"]]
647
+
648
+ fig1, ax1 = plt.subplots(ncols=1, nrows=1, figsize=figsize)
649
+ fig1.subplots_adjust(left=0.2, bottom=0.15, right=0.85, top=0.80)
650
+
651
+ datestring_stamp = row["time"].strftime("%Y-%m-%d %H:%M:%S")
652
+ celltime_stamp = "%02d:%02d:%02d" % (
653
+ row["time_cell"].dt.total_seconds() // 3600,
654
+ (row["time_cell"].dt.total_seconds() % 3600) // 60,
655
+ row["time_cell"].dt.total_seconds() % 60,
656
+ )
657
+ title = datestring_stamp + " , " + celltime_stamp
658
+ datestring_file = row["time"].strftime("%Y-%m-%d_%H%M%S")
659
+
660
+ ax1 = plot_mask_cell_individual_follow(
661
+ cell_i=cell,
662
+ track=track_i,
663
+ cog=cog_i,
664
+ features=features_i,
665
+ mask_total=mask_total_i,
666
+ field_contour=field_contour_i,
667
+ field_filled=field_filled_i,
668
+ width=width,
669
+ axes=ax1,
670
+ title=title,
671
+ **kwargs,
672
+ )
673
+
674
+ out_dir = os.path.join(plotdir, name)
675
+ os.makedirs(out_dir, exist_ok=True)
676
+ if "png" in file_format:
677
+ savepath_png = os.path.join(out_dir, name + "_" + datestring_file + ".png")
678
+ fig1.savefig(savepath_png, dpi=dpi)
679
+ logging.debug(
680
+ "field_contour field_filled Mask plot saved to " + savepath_png
681
+ )
682
+ if "pdf" in file_format:
683
+ savepath_pdf = os.path.join(out_dir, name + "_" + datestring_file + ".pdf")
684
+ fig1.savefig(savepath_pdf, dpi=dpi)
685
+ logging.debug(
686
+ "field_contour field_filled Mask plot saved to " + savepath_pdf
687
+ )
688
+ plt.close()
689
+ plt.clf()
690
+
691
+
692
+ def plot_mask_cell_individual_follow(
693
+ cell_i,
694
+ track,
695
+ cog,
696
+ features,
697
+ mask_total,
698
+ field_contour,
699
+ field_filled,
700
+ axes=None,
701
+ width=10000,
702
+ label_field_contour=None,
703
+ cmap_field_contour="Blues",
704
+ norm_field_contour=None,
705
+ linewidths_contour=0.8,
706
+ contour_labels=False,
707
+ vmin_field_contour=0,
708
+ vmax_field_contour=50,
709
+ levels_field_contour=None,
710
+ nlevels_field_contour=10,
711
+ label_field_filled=None,
712
+ cmap_field_filled="summer",
713
+ norm_field_filled=None,
714
+ vmin_field_filled=0,
715
+ vmax_field_filled=100,
716
+ levels_field_filled=None,
717
+ nlevels_field_filled=10,
718
+ title=None,
719
+ ):
720
+ """Make individual plot for cell centred around cell and with one background field as filling and one background field as contrours
721
+ Input:
722
+ Output:
723
+ """
724
+
725
+ import matplotlib.pyplot as plt
726
+
727
+ import numpy as np
728
+ from .utils import mask_cell_surface
729
+ from mpl_toolkits.axes_grid1 import make_axes_locatable
730
+ from matplotlib.colors import Normalize
731
+
732
+ warnings.warn(
733
+ "plot_mask_cell_individual_follow is depreciated and will be removed or significantly changed in v2.0.",
734
+ DeprecationWarning,
735
+ )
736
+
737
+ divider = make_axes_locatable(axes)
738
+
739
+ x_pos = track[track["cell"] == cell_i]["projection_x_coordinate"].item()
740
+ y_pos = track[track["cell"] == cell_i]["projection_y_coordinate"].item()
741
+ if field_filled is not None:
742
+ if levels_field_filled is None:
743
+ levels_field_filled = np.linspace(
744
+ vmin_field_filled, vmax_field_filled, nlevels_field_filled
745
+ )
746
+ plot_field_filled = axes.contourf(
747
+ (field_filled.coord("projection_x_coordinate").points - x_pos) / 1000,
748
+ (field_filled.coord("projection_y_coordinate").points - y_pos) / 1000,
749
+ field_filled.data,
750
+ cmap=cmap_field_filled,
751
+ norm=norm_field_filled,
752
+ levels=levels_field_filled,
753
+ vmin=vmin_field_filled,
754
+ vmax=vmax_field_filled,
755
+ )
756
+
757
+ cax_filled = divider.append_axes("right", size="5%", pad=0.1)
758
+ norm_filled = Normalize(vmin=vmin_field_filled, vmax=vmax_field_filled)
759
+ sm_filled = plt.cm.ScalarMappable(norm=norm_filled, cmap=plot_field_filled.cmap)
760
+ sm_filled.set_array([])
761
+
762
+ cbar_field_filled = plt.colorbar(
763
+ sm_filled, orientation="vertical", cax=cax_filled
764
+ )
765
+ cbar_field_filled.ax.set_ylabel(label_field_filled)
766
+ cbar_field_filled.set_clim(vmin_field_filled, vmax_field_filled)
767
+
768
+ if field_contour is not None:
769
+ if levels_field_contour is None:
770
+ levels_field_contour = np.linspace(
771
+ vmin_field_contour, vmax_field_contour, nlevels_field_contour
772
+ )
773
+ if norm_field_contour:
774
+ vmin_field_contour = (None,)
775
+ vmax_field_contour = (None,)
776
+
777
+ plot_field_contour = axes.contour(
778
+ (field_contour.coord("projection_x_coordinate").points - x_pos) / 1000,
779
+ (field_contour.coord("projection_y_coordinate").points - y_pos) / 1000,
780
+ field_contour.data,
781
+ cmap=cmap_field_contour,
782
+ norm=norm_field_contour,
783
+ levels=levels_field_contour,
784
+ vmin=vmin_field_contour,
785
+ vmax=vmax_field_contour,
786
+ linewidths=linewidths_contour,
787
+ )
788
+
789
+ if contour_labels:
790
+ axes.clabel(plot_field_contour, fontsize=10)
791
+
792
+ cax_contour = divider.append_axes("bottom", size="5%", pad=0.1)
793
+ if norm_field_contour:
794
+ vmin_field_contour = None
795
+ vmax_field_contour = None
796
+ norm_contour = norm_field_contour
797
+ else:
798
+ norm_contour = Normalize(vmin=vmin_field_contour, vmax=vmax_field_contour)
799
+
800
+ sm_contour = plt.cm.ScalarMappable(
801
+ norm=norm_contour, cmap=plot_field_contour.cmap
802
+ )
803
+ sm_contour.set_array([])
804
+
805
+ cbar_field_contour = plt.colorbar(
806
+ sm_contour,
807
+ orientation="horizontal",
808
+ ticks=levels_field_contour,
809
+ cax=cax_contour,
810
+ )
811
+ cbar_field_contour.ax.set_xlabel(label_field_contour)
812
+ cbar_field_contour.set_clim(vmin_field_contour, vmax_field_contour)
813
+
814
+ for i_row, row in track.iterrows():
815
+ cell = int(row["cell"])
816
+ if cell == cell_i:
817
+ color = "darkred"
818
+ else:
819
+ color = "darkorange"
820
+
821
+ cell_string = " " + str(int(row["cell"]))
822
+ axes.text(
823
+ (row["projection_x_coordinate"] - x_pos) / 1000,
824
+ (row["projection_y_coordinate"] - y_pos) / 1000,
825
+ cell_string,
826
+ color=color,
827
+ fontsize=6,
828
+ clip_on=True,
829
+ )
830
+
831
+ # Plot marker for tracked cell centre as a cross
832
+ axes.plot(
833
+ (row["projection_x_coordinate"] - x_pos) / 1000,
834
+ (row["projection_y_coordinate"] - y_pos) / 1000,
835
+ "x",
836
+ color=color,
837
+ markersize=4,
838
+ )
839
+
840
+ # Create surface projection of mask for the respective cell and plot it in the right color
841
+ z_coord = "model_level_number"
842
+ if len(mask_total.shape) == 3:
843
+ mask_total_i_surface = mask_cell_surface(
844
+ mask_total, cell, track, masked=False, z_coord=z_coord
845
+ )
846
+ elif len(mask_total.shape) == 2:
847
+ mask_total_i_surface = mask_total
848
+ axes.contour(
849
+ (mask_total_i_surface.coord("projection_x_coordinate").points - x_pos)
850
+ / 1000,
851
+ (mask_total_i_surface.coord("projection_y_coordinate").points - y_pos)
852
+ / 1000,
853
+ mask_total_i_surface.data,
854
+ levels=[0, cell],
855
+ colors=color,
856
+ linestyles=":",
857
+ linewidth=1,
858
+ )
859
+
860
+ if cog is not None:
861
+ for i_row, row in cog.iterrows():
862
+ cell = row["cell"]
863
+
864
+ if cell == cell_i:
865
+ color = "darkred"
866
+ else:
867
+ color = "darkorange"
868
+ # plot marker for centre of gravity as a circle
869
+ axes.plot(
870
+ (row["x_M"] - x_pos) / 1000,
871
+ (row["y_M"] - y_pos) / 1000,
872
+ "o",
873
+ markeredgecolor=color,
874
+ markerfacecolor="None",
875
+ markersize=4,
876
+ )
877
+
878
+ if features is not None:
879
+ for i_row, row in features.iterrows():
880
+ color = "purple"
881
+ axes.plot(
882
+ (row["projection_x_coordinate"] - x_pos) / 1000,
883
+ (row["projection_y_coordinate"] - y_pos) / 1000,
884
+ "+",
885
+ color=color,
886
+ markersize=3,
887
+ )
888
+
889
+ axes.set_xlabel("x (km)")
890
+ axes.set_ylabel("y (km)")
891
+ axes.set_xlim([-1 * width / 1000, width / 1000])
892
+ axes.set_ylim([-1 * width / 1000, width / 1000])
893
+ axes.xaxis.set_label_position("top")
894
+ axes.xaxis.set_ticks_position("top")
895
+ axes.set_title(title, pad=35, fontsize=10, horizontalalignment="left", loc="left")
896
+
897
+ return axes
898
+
899
+
900
+ def plot_mask_cell_track_static(
901
+ cell,
902
+ track,
903
+ cog,
904
+ features,
905
+ mask_total,
906
+ field_contour,
907
+ field_filled,
908
+ width=10000,
909
+ n_extend=1,
910
+ name="test",
911
+ plotdir="./",
912
+ file_format=["png"],
913
+ figsize=(10 / 2.54, 10 / 2.54),
914
+ dpi=300,
915
+ **kwargs,
916
+ ):
917
+ """Make plots for all cells with fixed frame including entire development of the cell and with one background field as filling and one background field as contrours
918
+ Input:
919
+ Output:
920
+ """
921
+
922
+ warnings.warn(
923
+ "plot_mask_cell_track_static is depreciated and will be removed or significantly changed in v2.0.",
924
+ DeprecationWarning,
925
+ )
926
+
927
+ mpl_backend = mpl.get_backend()
928
+ if mpl_backend != "agg":
929
+ warnings.warn(
930
+ "When using tobac plotting functions that render a figure, you may need "
931
+ "to set the Matplotlib backend to 'agg' by `matplotlib.use('agg')."
932
+ )
933
+ import matplotlib.pyplot as plt
934
+
935
+ from iris import Constraint
936
+ from numpy import unique
937
+ import os
938
+
939
+ track_cell = track[track["cell"] == cell]
940
+ x_min = track_cell["projection_x_coordinate"].min() - width
941
+ x_max = track_cell["projection_x_coordinate"].max() + width
942
+ y_min = track_cell["projection_y_coordinate"].min() - width
943
+ y_max = track_cell["projection_y_coordinate"].max() + width
944
+
945
+ # set up looping over time based on mask's time coordinate to allow for one timestep before and after the track
946
+ time_coord = mask_total.coord("time")
947
+ time = time_coord.units.num2date(time_coord.points)
948
+ i_start = max(0, np.where(time == track_cell["time"].values[0])[0][0] - n_extend)
949
+ i_end = min(
950
+ len(time) - 1,
951
+ np.where(time == track_cell["time"].values[-1])[0][0] + n_extend + 1,
952
+ )
953
+ time_cell = time[slice(i_start, i_end)]
954
+ for time_i in time_cell:
955
+ # for i_row,row in track_cell.iterrows():
956
+ # time_i=row['time']
957
+ # constraint_time = Constraint(time=row['time'])
958
+ constraint_time = Constraint(time=time_i)
959
+
960
+ constraint_x = Constraint(
961
+ projection_x_coordinate=lambda cell: x_min < cell < x_max
962
+ )
963
+ constraint_y = Constraint(
964
+ projection_y_coordinate=lambda cell: y_min < cell < y_max
965
+ )
966
+ constraint = constraint_time & constraint_x & constraint_y
967
+
968
+ mask_total_i = mask_total.extract(constraint)
969
+ if field_contour is None:
970
+ field_contour_i = None
971
+ else:
972
+ field_contour_i = field_contour.extract(constraint)
973
+ if field_filled is None:
974
+ field_filled_i = None
975
+ else:
976
+ field_filled_i = field_filled.extract(constraint)
977
+
978
+ track_i = track[track["time"] == time_i]
979
+
980
+ cells_mask = list(unique(mask_total_i.core_data()))
981
+ track_cells = track_i.loc[
982
+ (track_i["projection_x_coordinate"] > x_min)
983
+ & (track_i["projection_x_coordinate"] < x_max)
984
+ & (track_i["projection_y_coordinate"] > y_min)
985
+ & (track_i["projection_y_coordinate"] < y_max)
986
+ ]
987
+ cells_track = list(track_cells["cell"].values)
988
+ cells = list(set(cells_mask + cells_track))
989
+ if cell not in cells:
990
+ cells.append(cell)
991
+ if 0 in cells:
992
+ cells.remove(0)
993
+ track_i = track_i[track_i["cell"].isin(cells)]
994
+
995
+ if cog is None:
996
+ cog_i = None
997
+ else:
998
+ cog_i = cog[cog["cell"].isin(cells)]
999
+ cog_i = cog_i[cog_i["time"] == time_i]
1000
+
1001
+ if features is None:
1002
+ features_i = None
1003
+ else:
1004
+ features_i = features[features["time"] == time_i]
1005
+
1006
+ fig1, ax1 = plt.subplots(ncols=1, nrows=1, figsize=figsize)
1007
+ fig1.subplots_adjust(left=0.2, bottom=0.15, right=0.80, top=0.85)
1008
+
1009
+ datestring_stamp = time_i.strftime("%Y-%m-%d %H:%M:%S")
1010
+ if time_i in track_cell["time"].values:
1011
+ time_cell_i = track_cell[track_cell["time"].values == time_i]["time_cell"]
1012
+ celltime_stamp = "%02d:%02d:%02d" % (
1013
+ time_cell_i.dt.total_seconds() // 3600,
1014
+ (time_cell_i.dt.total_seconds() % 3600) // 60,
1015
+ time_cell_i.dt.total_seconds() % 60,
1016
+ )
1017
+ else:
1018
+ celltime_stamp = " - "
1019
+ title = datestring_stamp + " , " + celltime_stamp
1020
+ datestring_file = time_i.strftime("%Y-%m-%d_%H%M%S")
1021
+
1022
+ ax1 = plot_mask_cell_individual_static(
1023
+ cell_i=cell,
1024
+ track=track_i,
1025
+ cog=cog_i,
1026
+ features=features_i,
1027
+ mask_total=mask_total_i,
1028
+ field_contour=field_contour_i,
1029
+ field_filled=field_filled_i,
1030
+ xlim=[x_min / 1000, x_max / 1000],
1031
+ ylim=[y_min / 1000, y_max / 1000],
1032
+ axes=ax1,
1033
+ title=title,
1034
+ **kwargs,
1035
+ )
1036
+
1037
+ out_dir = os.path.join(plotdir, name)
1038
+ os.makedirs(out_dir, exist_ok=True)
1039
+ if "png" in file_format:
1040
+ savepath_png = os.path.join(out_dir, name + "_" + datestring_file + ".png")
1041
+ fig1.savefig(savepath_png, dpi=dpi)
1042
+ logging.debug("Mask static plot saved to " + savepath_png)
1043
+ if "pdf" in file_format:
1044
+ savepath_pdf = os.path.join(out_dir, name + "_" + datestring_file + ".pdf")
1045
+ fig1.savefig(savepath_pdf, dpi=dpi)
1046
+ logging.debug("Mask static plot saved to " + savepath_pdf)
1047
+ plt.close()
1048
+ plt.clf()
1049
+
1050
+
1051
+ def plot_mask_cell_individual_static(
1052
+ cell_i,
1053
+ track,
1054
+ cog,
1055
+ features,
1056
+ mask_total,
1057
+ field_contour,
1058
+ field_filled,
1059
+ axes=None,
1060
+ xlim=None,
1061
+ ylim=None,
1062
+ label_field_contour=None,
1063
+ cmap_field_contour="Blues",
1064
+ norm_field_contour=None,
1065
+ linewidths_contour=0.8,
1066
+ contour_labels=False,
1067
+ vmin_field_contour=0,
1068
+ vmax_field_contour=50,
1069
+ levels_field_contour=None,
1070
+ nlevels_field_contour=10,
1071
+ label_field_filled=None,
1072
+ cmap_field_filled="summer",
1073
+ norm_field_filled=None,
1074
+ vmin_field_filled=0,
1075
+ vmax_field_filled=100,
1076
+ levels_field_filled=None,
1077
+ nlevels_field_filled=10,
1078
+ title=None,
1079
+ feature_number=False,
1080
+ ):
1081
+ """Make plots for cell in fixed frame and with one background field as filling and one background field as contrours
1082
+ Input:
1083
+ Output:
1084
+ """
1085
+ import matplotlib.pyplot as plt
1086
+
1087
+ import numpy as np
1088
+ from .utils import mask_features, mask_features_surface
1089
+ from mpl_toolkits.axes_grid1 import make_axes_locatable
1090
+ from matplotlib.colors import Normalize
1091
+
1092
+ warnings.warn(
1093
+ "plot_mask_cell_individual_static is depreciated and will be removed or significantly changed in v2.0.",
1094
+ DeprecationWarning,
1095
+ )
1096
+
1097
+ divider = make_axes_locatable(axes)
1098
+
1099
+ if field_filled is not None:
1100
+ if levels_field_filled is None:
1101
+ levels_field_filled = np.linspace(vmin_field_filled, vmax_field_filled, 10)
1102
+ plot_field_filled = axes.contourf(
1103
+ field_filled.coord("projection_x_coordinate").points / 1000,
1104
+ field_filled.coord("projection_y_coordinate").points / 1000,
1105
+ field_filled.data,
1106
+ levels=levels_field_filled,
1107
+ norm=norm_field_filled,
1108
+ cmap=cmap_field_filled,
1109
+ vmin=vmin_field_filled,
1110
+ vmax=vmax_field_filled,
1111
+ )
1112
+
1113
+ cax_filled = divider.append_axes("right", size="5%", pad=0.1)
1114
+
1115
+ norm_filled = Normalize(vmin=vmin_field_filled, vmax=vmax_field_filled)
1116
+ sm1 = plt.cm.ScalarMappable(norm=norm_filled, cmap=plot_field_filled.cmap)
1117
+ sm1.set_array([])
1118
+
1119
+ cbar_field_filled = plt.colorbar(sm1, orientation="vertical", cax=cax_filled)
1120
+ cbar_field_filled.ax.set_ylabel(label_field_filled)
1121
+ cbar_field_filled.set_clim(vmin_field_filled, vmax_field_filled)
1122
+
1123
+ if field_contour is not None:
1124
+ if levels_field_contour is None:
1125
+ levels_field_contour = np.linspace(
1126
+ vmin_field_contour, vmax_field_contour, 5
1127
+ )
1128
+ plot_field_contour = axes.contour(
1129
+ field_contour.coord("projection_x_coordinate").points / 1000,
1130
+ field_contour.coord("projection_y_coordinate").points / 1000,
1131
+ field_contour.data,
1132
+ cmap=cmap_field_contour,
1133
+ norm=norm_field_contour,
1134
+ levels=levels_field_contour,
1135
+ vmin=vmin_field_contour,
1136
+ vmax=vmax_field_contour,
1137
+ linewidths=linewidths_contour,
1138
+ )
1139
+
1140
+ if contour_labels:
1141
+ axes.clabel(plot_field_contour, fontsize=10)
1142
+
1143
+ cax_contour = divider.append_axes("bottom", size="5%", pad=0.1)
1144
+ if norm_field_contour:
1145
+ vmin_field_contour = None
1146
+ vmax_field_contour = None
1147
+ norm_contour = norm_field_contour
1148
+ else:
1149
+ norm_contour = Normalize(vmin=vmin_field_contour, vmax=vmax_field_contour)
1150
+
1151
+ sm_contour = plt.cm.ScalarMappable(
1152
+ norm=norm_contour, cmap=plot_field_contour.cmap
1153
+ )
1154
+ sm_contour.set_array([])
1155
+
1156
+ cbar_field_contour = plt.colorbar(
1157
+ sm_contour,
1158
+ orientation="horizontal",
1159
+ ticks=levels_field_contour,
1160
+ cax=cax_contour,
1161
+ )
1162
+ cbar_field_contour.ax.set_xlabel(label_field_contour)
1163
+ cbar_field_contour.set_clim(vmin_field_contour, vmax_field_contour)
1164
+
1165
+ for i_row, row in track.iterrows():
1166
+ cell = row["cell"]
1167
+ feature = row["feature"]
1168
+ # logging.debug("cell: "+ str(row['cell']))
1169
+ # logging.debug("feature: "+ str(row['feature']))
1170
+
1171
+ if cell == cell_i:
1172
+ color = "darkred"
1173
+ if feature_number:
1174
+ cell_string = " " + str(int(cell)) + " (" + str(int(feature)) + ")"
1175
+ else:
1176
+ cell_string = " " + str(int(cell))
1177
+ elif np.isnan(cell):
1178
+ color = "gray"
1179
+ if feature_number:
1180
+ cell_string = " " + "(" + str(int(feature)) + ")"
1181
+ else:
1182
+ cell_string = " "
1183
+ else:
1184
+ color = "darkorange"
1185
+ if feature_number:
1186
+ cell_string = " " + str(int(cell)) + " (" + str(int(feature)) + ")"
1187
+ else:
1188
+ cell_string = " " + str(int(cell))
1189
+
1190
+ axes.text(
1191
+ row["projection_x_coordinate"] / 1000,
1192
+ row["projection_y_coordinate"] / 1000,
1193
+ cell_string,
1194
+ color=color,
1195
+ fontsize=6,
1196
+ clip_on=True,
1197
+ )
1198
+
1199
+ # Plot marker for tracked cell centre as a cross
1200
+ axes.plot(
1201
+ row["projection_x_coordinate"] / 1000,
1202
+ row["projection_y_coordinate"] / 1000,
1203
+ "x",
1204
+ color=color,
1205
+ markersize=4,
1206
+ )
1207
+
1208
+ # Create surface projection of mask for the respective cell and plot it in the right color
1209
+ z_coord = "model_level_number"
1210
+ if len(mask_total.shape) == 3:
1211
+ mask_total_i_surface = mask_features_surface(
1212
+ mask_total, feature, masked=False, z_coord=z_coord
1213
+ )
1214
+ elif len(mask_total.shape) == 2:
1215
+ mask_total_i_surface = mask_features(
1216
+ mask_total, feature, masked=False, z_coord=z_coord
1217
+ )
1218
+ axes.contour(
1219
+ mask_total_i_surface.coord("projection_x_coordinate").points / 1000,
1220
+ mask_total_i_surface.coord("projection_y_coordinate").points / 1000,
1221
+ mask_total_i_surface.data,
1222
+ levels=[0, feature],
1223
+ colors=color,
1224
+ linestyles=":",
1225
+ linewidth=1,
1226
+ )
1227
+ if cog is not None:
1228
+ for i_row, row in cog.iterrows():
1229
+ cell = row["cell"]
1230
+
1231
+ if cell == cell_i:
1232
+ color = "darkred"
1233
+ else:
1234
+ color = "darkorange"
1235
+ # plot marker for centre of gravity as a circle
1236
+ axes.plot(
1237
+ row["x_M"] / 1000,
1238
+ row["y_M"] / 1000,
1239
+ "o",
1240
+ markeredgecolor=color,
1241
+ markerfacecolor="None",
1242
+ markersize=4,
1243
+ )
1244
+
1245
+ if features is not None:
1246
+ for i_row, row in features.iterrows():
1247
+ color = "purple"
1248
+ axes.plot(
1249
+ row["projection_x_coordinate"] / 1000,
1250
+ row["projection_y_coordinate"] / 1000,
1251
+ "+",
1252
+ color=color,
1253
+ markersize=3,
1254
+ )
1255
+
1256
+ axes.set_xlabel("x (km)")
1257
+ axes.set_ylabel("y (km)")
1258
+ axes.set_xlim(xlim)
1259
+ axes.set_ylim(ylim)
1260
+ axes.xaxis.set_label_position("top")
1261
+ axes.xaxis.set_ticks_position("top")
1262
+ axes.set_title(title, pad=35, fontsize=10, horizontalalignment="left", loc="left")
1263
+
1264
+ return axes
1265
+
1266
+
1267
+ def plot_mask_cell_track_2D3Dstatic(
1268
+ cell,
1269
+ track,
1270
+ cog,
1271
+ features,
1272
+ mask_total,
1273
+ field_contour,
1274
+ field_filled,
1275
+ width=10000,
1276
+ n_extend=1,
1277
+ name="test",
1278
+ plotdir="./",
1279
+ file_format=["png"],
1280
+ figsize=(10 / 2.54, 10 / 2.54),
1281
+ dpi=300,
1282
+ ele=10,
1283
+ azim=30,
1284
+ **kwargs,
1285
+ ):
1286
+ """Make plots for all cells with fixed frame including entire development of the cell and with one background field as filling and one background field as contrours
1287
+ Input:
1288
+ Output:
1289
+ """
1290
+ warnings.warn(
1291
+ "plot_mask_cell_track_2D3Dstatic is depreciated and will be removed or significantly changed in v2.0.",
1292
+ DeprecationWarning,
1293
+ )
1294
+
1295
+ mpl_backend = mpl.get_backend()
1296
+ if mpl_backend != "agg":
1297
+ warnings.warn(
1298
+ "When using tobac plotting functions that render a figure, you may need "
1299
+ "to set the Matplotlib backend to 'agg' by `matplotlib.use('agg')."
1300
+ )
1301
+ import matplotlib.pyplot as plt
1302
+
1303
+ from iris import Constraint
1304
+ from numpy import unique
1305
+ import os
1306
+ from mpl_toolkits.mplot3d import Axes3D
1307
+ import matplotlib.gridspec as gridspec
1308
+
1309
+ track_cell = track[track["cell"] == cell]
1310
+ x_min = track_cell["projection_x_coordinate"].min() - width
1311
+ x_max = track_cell["projection_x_coordinate"].max() + width
1312
+ y_min = track_cell["projection_y_coordinate"].min() - width
1313
+ y_max = track_cell["projection_y_coordinate"].max() + width
1314
+
1315
+ # set up looping over time based on mask's time coordinate to allow for one timestep before and after the track
1316
+ time_coord = mask_total.coord("time")
1317
+ time = time_coord.units.num2date(time_coord.points)
1318
+ i_start = max(0, np.where(time == track_cell["time"].values[0])[0][0] - n_extend)
1319
+ i_end = min(
1320
+ len(time) - 1,
1321
+ np.where(time == track_cell["time"].values[-1])[0][0] + n_extend + 1,
1322
+ )
1323
+ time_cell = time[slice(i_start, i_end)]
1324
+ for time_i in time_cell:
1325
+ # for i_row,row in track_cell.iterrows():
1326
+ # time_i=row['time']
1327
+ # constraint_time = Constraint(time=row['time'])
1328
+ constraint_time = Constraint(time=time_i)
1329
+
1330
+ constraint_x = Constraint(
1331
+ projection_x_coordinate=lambda cell: x_min < cell < x_max
1332
+ )
1333
+ constraint_y = Constraint(
1334
+ projection_y_coordinate=lambda cell: y_min < cell < y_max
1335
+ )
1336
+ constraint = constraint_time & constraint_x & constraint_y
1337
+
1338
+ mask_total_i = mask_total.extract(constraint)
1339
+ if field_contour is None:
1340
+ field_contour_i = None
1341
+ else:
1342
+ field_contour_i = field_contour.extract(constraint)
1343
+ if field_filled is None:
1344
+ field_filled_i = None
1345
+ else:
1346
+ field_filled_i = field_filled.extract(constraint)
1347
+
1348
+ track_i = track[track["time"] == time_i]
1349
+
1350
+ cells_mask = list(unique(mask_total_i.core_data()))
1351
+ track_cells = track_i.loc[
1352
+ (track_i["projection_x_coordinate"] > x_min)
1353
+ & (track_i["projection_x_coordinate"] < x_max)
1354
+ & (track_i["projection_y_coordinate"] > y_min)
1355
+ & (track_i["projection_y_coordinate"] < y_max)
1356
+ ]
1357
+ cells_track = list(track_cells["cell"].values)
1358
+ cells = list(set(cells_mask + cells_track))
1359
+ if cell not in cells:
1360
+ cells.append(cell)
1361
+ if 0 in cells:
1362
+ cells.remove(0)
1363
+ track_i = track_i[track_i["cell"].isin(cells)]
1364
+
1365
+ if cog is None:
1366
+ cog_i = None
1367
+ else:
1368
+ cog_i = cog[cog["cell"].isin(cells)]
1369
+ cog_i = cog_i[cog_i["time"] == time_i]
1370
+
1371
+ if features is None:
1372
+ features_i = None
1373
+ else:
1374
+ features_i = features[features["time"] == time_i]
1375
+
1376
+ fig1 = plt.figure(figsize=(20 / 2.54, 10 / 2.54))
1377
+ fig1.subplots_adjust(
1378
+ left=0.1, bottom=0.15, right=0.9, top=0.9, wspace=0.3, hspace=0.25
1379
+ )
1380
+
1381
+ # make two subplots for figure:
1382
+ gs1 = gridspec.GridSpec(1, 2, width_ratios=[1, 1.2])
1383
+ fig1.add_subplot(gs1[0])
1384
+ fig1.add_subplot(gs1[1], projection="3d")
1385
+
1386
+ ax1 = fig1.get_axes()
1387
+
1388
+ datestring_stamp = time_i.strftime("%Y-%m-%d %H:%M:%S")
1389
+ if time_i in track_cell["time"].values:
1390
+ time_cell_i = track_cell[track_cell["time"].values == time_i]["time_cell"]
1391
+ celltime_stamp = "%02d:%02d:%02d" % (
1392
+ time_cell_i.dt.total_seconds() // 3600,
1393
+ (time_cell_i.dt.total_seconds() % 3600) // 60,
1394
+ time_cell_i.dt.total_seconds() % 60,
1395
+ )
1396
+ else:
1397
+ celltime_stamp = " - "
1398
+ title = datestring_stamp + " , " + celltime_stamp
1399
+ datestring_file = time_i.strftime("%Y-%m-%d_%H%M%S")
1400
+
1401
+ ax1[0] = plot_mask_cell_individual_static(
1402
+ cell_i=cell,
1403
+ track=track_i,
1404
+ cog=cog_i,
1405
+ features=features_i,
1406
+ mask_total=mask_total_i,
1407
+ field_contour=field_contour_i,
1408
+ field_filled=field_filled_i,
1409
+ xlim=[x_min / 1000, x_max / 1000],
1410
+ ylim=[y_min / 1000, y_max / 1000],
1411
+ axes=ax1[0],
1412
+ title=title,
1413
+ **kwargs,
1414
+ )
1415
+
1416
+ ax1[1] = plot_mask_cell_individual_3Dstatic(
1417
+ cell_i=cell,
1418
+ track=track_i,
1419
+ cog=cog_i,
1420
+ features=features_i,
1421
+ mask_total=mask_total_i,
1422
+ field_contour=field_contour_i,
1423
+ field_filled=field_filled_i,
1424
+ xlim=[x_min / 1000, x_max / 1000],
1425
+ ylim=[y_min / 1000, y_max / 1000],
1426
+ axes=ax1[1],
1427
+ title=title,
1428
+ ele=ele,
1429
+ azim=azim,
1430
+ **kwargs,
1431
+ )
1432
+
1433
+ out_dir = os.path.join(plotdir, name)
1434
+ os.makedirs(out_dir, exist_ok=True)
1435
+ if "png" in file_format:
1436
+ savepath_png = os.path.join(out_dir, name + "_" + datestring_file + ".png")
1437
+ fig1.savefig(savepath_png, dpi=dpi)
1438
+ logging.debug("Mask static 2d/3D plot saved to " + savepath_png)
1439
+ if "pdf" in file_format:
1440
+ savepath_pdf = os.path.join(out_dir, name + "_" + datestring_file + ".pdf")
1441
+ fig1.savefig(savepath_pdf, dpi=dpi)
1442
+ logging.debug("Mask static 2d/3D plot saved to " + savepath_pdf)
1443
+ plt.close()
1444
+ plt.clf()
1445
+
1446
+
1447
+ def plot_mask_cell_track_3Dstatic(
1448
+ cell,
1449
+ track,
1450
+ cog,
1451
+ features,
1452
+ mask_total,
1453
+ field_contour,
1454
+ field_filled,
1455
+ width=10000,
1456
+ n_extend=1,
1457
+ name="test",
1458
+ plotdir="./",
1459
+ file_format=["png"],
1460
+ figsize=(10 / 2.54, 10 / 2.54),
1461
+ dpi=300,
1462
+ **kwargs,
1463
+ ):
1464
+ """Make plots for all cells with fixed frame including entire development of the cell and with one background field as filling and one background field as contrours
1465
+ Input:
1466
+ Output:
1467
+ """
1468
+ warnings.warn(
1469
+ "plot_mask_cell_track_3Dstatic is depreciated and will be removed or significantly changed in v2.0.",
1470
+ DeprecationWarning,
1471
+ )
1472
+
1473
+ mpl_backend = mpl.get_backend()
1474
+ if mpl_backend != "agg":
1475
+ warnings.warn(
1476
+ "When using tobac plotting functions that render a figure, you may need "
1477
+ "to set the Matplotlib backend to 'agg' by `matplotlib.use('agg')."
1478
+ )
1479
+ import matplotlib.pyplot as plt
1480
+
1481
+ from iris import Constraint
1482
+ from numpy import unique
1483
+ import os
1484
+ from mpl_toolkits.mplot3d import Axes3D
1485
+
1486
+ track_cell = track[track["cell"] == cell]
1487
+ x_min = track_cell["projection_x_coordinate"].min() - width
1488
+ x_max = track_cell["projection_x_coordinate"].max() + width
1489
+ y_min = track_cell["projection_y_coordinate"].min() - width
1490
+ y_max = track_cell["projection_y_coordinate"].max() + width
1491
+
1492
+ # set up looping over time based on mask's time coordinate to allow for one timestep before and after the track
1493
+ time_coord = mask_total.coord("time")
1494
+ time = time_coord.units.num2date(time_coord.points)
1495
+ i_start = max(0, np.where(time == track_cell["time"].values[0])[0][0] - n_extend)
1496
+ i_end = min(
1497
+ len(time) - 1,
1498
+ np.where(time == track_cell["time"].values[-1])[0][0] + n_extend + 1,
1499
+ )
1500
+ time_cell = time[slice(i_start, i_end)]
1501
+ for time_i in time_cell:
1502
+ # for i_row,row in track_cell.iterrows():
1503
+ # time_i=row['time']
1504
+ # constraint_time = Constraint(time=row['time'])
1505
+ constraint_time = Constraint(time=time_i)
1506
+
1507
+ constraint_x = Constraint(
1508
+ projection_x_coordinate=lambda cell: x_min < cell < x_max
1509
+ )
1510
+ constraint_y = Constraint(
1511
+ projection_y_coordinate=lambda cell: y_min < cell < y_max
1512
+ )
1513
+ constraint = constraint_time & constraint_x & constraint_y
1514
+
1515
+ mask_total_i = mask_total.extract(constraint)
1516
+ if field_contour is None:
1517
+ field_contour_i = None
1518
+ else:
1519
+ field_contour_i = field_contour.extract(constraint)
1520
+ if field_filled is None:
1521
+ field_filled_i = None
1522
+ else:
1523
+ field_filled_i = field_filled.extract(constraint)
1524
+
1525
+ track_i = track[track["time"] == time_i]
1526
+
1527
+ cells_mask = list(unique(mask_total_i.core_data()))
1528
+ track_cells = track_i.loc[
1529
+ (track_i["projection_x_coordinate"] > x_min)
1530
+ & (track_i["projection_x_coordinate"] < x_max)
1531
+ & (track_i["projection_y_coordinate"] > y_min)
1532
+ & (track_i["projection_y_coordinate"] < y_max)
1533
+ ]
1534
+ cells_track = list(track_cells["cell"].values)
1535
+ cells = list(set(cells_mask + cells_track))
1536
+ if cell not in cells:
1537
+ cells.append(cell)
1538
+ if 0 in cells:
1539
+ cells.remove(0)
1540
+ track_i = track_i[track_i["cell"].isin(cells)]
1541
+
1542
+ if cog is None:
1543
+ cog_i = None
1544
+ else:
1545
+ cog_i = cog[cog["cell"].isin(cells)]
1546
+ cog_i = cog_i[cog_i["time"] == time_i]
1547
+
1548
+ if features is None:
1549
+ features_i = None
1550
+ else:
1551
+ features_i = features[features["time"] == time_i]
1552
+
1553
+ # fig1, ax1 = plt.subplots(ncols=1, nrows=1, figsize=figsize)
1554
+ # fig1.subplots_adjust(left=0.2, bottom=0.15, right=0.80, top=0.85)
1555
+ fig1, ax1 = plt.subplots(
1556
+ ncols=1,
1557
+ nrows=1,
1558
+ figsize=(10 / 2.54, 10 / 2.54),
1559
+ subplot_kw={"projection": "3d"},
1560
+ )
1561
+
1562
+ datestring_stamp = time_i.strftime("%Y-%m-%d %H:%M:%S")
1563
+ if time_i in track_cell["time"].values:
1564
+ time_cell_i = track_cell[track_cell["time"].values == time_i]["time_cell"]
1565
+ celltime_stamp = "%02d:%02d:%02d" % (
1566
+ time_cell_i.dt.total_seconds() // 3600,
1567
+ (time_cell_i.dt.total_seconds() % 3600) // 60,
1568
+ time_cell_i.dt.total_seconds() % 60,
1569
+ )
1570
+ else:
1571
+ celltime_stamp = " - "
1572
+ title = datestring_stamp + " , " + celltime_stamp
1573
+ datestring_file = time_i.strftime("%Y-%m-%d_%H%M%S")
1574
+
1575
+ ax1 = plot_mask_cell_individual_3Dstatic(
1576
+ cell_i=cell,
1577
+ track=track_i,
1578
+ cog=cog_i,
1579
+ features=features_i,
1580
+ mask_total=mask_total_i,
1581
+ field_contour=field_contour_i,
1582
+ field_filled=field_filled_i,
1583
+ xlim=[x_min / 1000, x_max / 1000],
1584
+ ylim=[y_min / 1000, y_max / 1000],
1585
+ axes=ax1,
1586
+ title=title,
1587
+ **kwargs,
1588
+ )
1589
+
1590
+ out_dir = os.path.join(plotdir, name)
1591
+ os.makedirs(out_dir, exist_ok=True)
1592
+ if "png" in file_format:
1593
+ savepath_png = os.path.join(out_dir, name + "_" + datestring_file + ".png")
1594
+ fig1.savefig(savepath_png, dpi=dpi)
1595
+ logging.debug("Mask static plot saved to " + savepath_png)
1596
+ if "pdf" in file_format:
1597
+ savepath_pdf = os.path.join(out_dir, name + "_" + datestring_file + ".pdf")
1598
+ fig1.savefig(savepath_pdf, dpi=dpi)
1599
+ logging.debug("Mask static plot saved to " + savepath_pdf)
1600
+ plt.close()
1601
+ plt.clf()
1602
+
1603
+
1604
+ def plot_mask_cell_individual_3Dstatic(
1605
+ cell_i,
1606
+ track,
1607
+ cog,
1608
+ features,
1609
+ mask_total,
1610
+ field_contour,
1611
+ field_filled,
1612
+ axes=None,
1613
+ xlim=None,
1614
+ ylim=None,
1615
+ label_field_contour=None,
1616
+ cmap_field_contour="Blues",
1617
+ norm_field_contour=None,
1618
+ linewidths_contour=0.8,
1619
+ contour_labels=False,
1620
+ vmin_field_contour=0,
1621
+ vmax_field_contour=50,
1622
+ levels_field_contour=None,
1623
+ nlevels_field_contour=10,
1624
+ label_field_filled=None,
1625
+ cmap_field_filled="summer",
1626
+ norm_field_filled=None,
1627
+ vmin_field_filled=0,
1628
+ vmax_field_filled=100,
1629
+ levels_field_filled=None,
1630
+ nlevels_field_filled=10,
1631
+ title=None,
1632
+ feature_number=False,
1633
+ ele=10.0,
1634
+ azim=210.0,
1635
+ ):
1636
+ """Make plots for cell in fixed frame and with one background field as filling and one background field as contrours
1637
+ Input:
1638
+ Output:
1639
+ """
1640
+ import matplotlib.pyplot as plt
1641
+
1642
+ import numpy as np
1643
+ from .utils import mask_features, mask_features_surface
1644
+
1645
+ # from mpl_toolkits.axes_grid1 import make_axes_locatable
1646
+ # from matplotlib.colors import Normalize
1647
+ from mpl_toolkits.mplot3d import Axes3D
1648
+
1649
+ warnings.warn(
1650
+ "plot_mask_cell_individual_3Dstatic is depreciated and will be removed or significantly changed in v2.0.",
1651
+ DeprecationWarning,
1652
+ )
1653
+
1654
+ axes.view_init(elev=ele, azim=azim)
1655
+ axes.grid(b=False)
1656
+ axes.set_frame_on(False)
1657
+
1658
+ # make the panes transparent
1659
+ axes.xaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
1660
+ axes.yaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
1661
+ axes.zaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
1662
+ # make the grid lines transparent
1663
+ axes.xaxis._axinfo["grid"]["color"] = (1, 1, 1, 0)
1664
+ axes.yaxis._axinfo["grid"]["color"] = (1, 1, 1, 0)
1665
+ axes.zaxis._axinfo["grid"]["color"] = (1, 1, 1, 0)
1666
+
1667
+ if title is not None:
1668
+ axes.set_title(title, horizontalalignment="left", loc="left")
1669
+
1670
+ # colors_mask = ['pink','darkred', 'orange', 'darkred', 'red', 'darkorange']
1671
+ x = mask_total.coord("projection_x_coordinate").points
1672
+ y = mask_total.coord("projection_y_coordinate").points
1673
+ z = mask_total.coord("model_level_number").points
1674
+
1675
+ # z = mask_total.coord('geopotential_height').points
1676
+ zz, yy, xx = np.meshgrid(z, y, x, indexing="ij")
1677
+ # z_alt = mask_total.coord('geopotential_height').points
1678
+
1679
+ # divider = make_axes_locatable(axes)
1680
+
1681
+ # if field_filled is not None:
1682
+ # if levels_field_filled is None:
1683
+ # levels_field_filled=np.linspace(vmin_field_filled,vmax_field_filled, 10)
1684
+ # plot_field_filled = axes.contourf(field_filled.coord('projection_x_coordinate').points/1000,
1685
+ # field_filled.coord('projection_y_coordinate').points/1000,
1686
+ # field_filled.data,
1687
+ # levels=levels_field_filled, norm=norm_field_filled,
1688
+ # cmap=cmap_field_filled, vmin=vmin_field_filled, vmax=vmax_field_filled)
1689
+
1690
+ # cax_filled = divider.append_axes("right", size="5%", pad=0.1)
1691
+
1692
+ # norm_filled= Normalize(vmin=vmin_field_filled, vmax=vmax_field_filled)
1693
+ # sm1= plt.cm.ScalarMappable(norm=norm_filled, cmap = plot_field_filled.cmap)
1694
+ # sm1.set_array([])
1695
+
1696
+ # cbar_field_filled = plt.colorbar(sm1, orientation='vertical',cax=cax_filled)
1697
+ # cbar_field_filled.ax.set_ylabel(label_field_filled)
1698
+ # cbar_field_filled.set_clim(vmin_field_filled, vmax_field_filled)
1699
+
1700
+ # if field_contour is not None:
1701
+ # if levels_field_contour is None:
1702
+ # levels_field_contour=np.linspace(vmin_field_contour, vmax_field_contour, 5)
1703
+ # plot_field_contour = axes.contour(field_contour.coord('projection_x_coordinate').points/1000,
1704
+ # field_contour.coord('projection_y_coordinate').points/1000,
1705
+ # field_contour.data,
1706
+ # cmap=cmap_field_contour,norm=norm_field_contour,
1707
+ # levels=levels_field_contour,vmin=vmin_field_contour, vmax=vmax_field_contour,
1708
+ # linewidths=linewidths_contour)
1709
+
1710
+ # if contour_labels:
1711
+ # axes.clabel(plot_field_contour, fontsize=10)
1712
+
1713
+ # cax_contour = divider.append_axes("bottom", size="5%", pad=0.1)
1714
+ # if norm_field_contour:
1715
+ # vmin_field_contour=None
1716
+ # vmax_field_contour=None
1717
+ # norm_contour=norm_field_contour
1718
+ # else:
1719
+ # norm_contour= Normalize(vmin=vmin_field_contour, vmax=vmax_field_contour)
1720
+ #
1721
+ # sm_contour= plt.cm.ScalarMappable(norm=norm_contour, cmap = plot_field_contour.cmap)
1722
+ # sm_contour.set_array([])
1723
+ #
1724
+ # cbar_field_contour = plt.colorbar(sm_contour, orientation='horizontal',ticks=levels_field_contour,cax=cax_contour)
1725
+ # cbar_field_contour.ax.set_xlabel(label_field_contour)
1726
+ # cbar_field_contour.set_clim(vmin_field_contour, vmax_field_contour)
1727
+ #
1728
+ for i_row, row in track.iterrows():
1729
+ cell = row["cell"]
1730
+ feature = row["feature"]
1731
+ # logging.debug("cell: "+ str(row['cell']))
1732
+ # logging.debug("feature: "+ str(row['feature']))
1733
+
1734
+ if cell == cell_i:
1735
+ color = "darkred"
1736
+ if feature_number:
1737
+ cell_string = " " + str(int(cell)) + " (" + str(int(feature)) + ")"
1738
+ else:
1739
+ cell_string = " " + str(int(cell))
1740
+ elif np.isnan(cell):
1741
+ color = "gray"
1742
+ if feature_number:
1743
+ cell_string = " " + "(" + str(int(feature)) + ")"
1744
+ else:
1745
+ cell_string = " "
1746
+ else:
1747
+ color = "darkorange"
1748
+ if feature_number:
1749
+ cell_string = " " + str(int(cell)) + " (" + str(int(feature)) + ")"
1750
+ else:
1751
+ cell_string = " " + str(int(cell))
1752
+
1753
+ # axes.text(row['projection_x_coordinate']/1000,
1754
+ # row['projection_y_coordinate']/1000,
1755
+ # 0,
1756
+ # cell_string,color=color,fontsize=6, clip_on=True)
1757
+
1758
+ # # Plot marker for tracked cell centre as a cross
1759
+ # axes.plot(row['projection_x_coordinate']/1000,
1760
+ # row['projection_y_coordinate']/1000,
1761
+ # 0,
1762
+ # 'x', color=color,markersize=4)
1763
+
1764
+ # Create surface projection of mask for the respective cell and plot it in the right color
1765
+ # z_coord = 'model_level_number'
1766
+ # if len(mask_total.shape)==3:
1767
+ # mask_total_i_surface = mask_features_surface(mask_total, feature, masked=False, z_coord=z_coord)
1768
+ # elif len(mask_total.shape)==2:
1769
+ # mask_total_i_surface=mask_features(mask_total, feature, masked=False, z_coord=z_coord)
1770
+ # axes.contour(mask_total_i_surface.coord('projection_x_coordinate').points/1000,
1771
+ # mask_total_i_surface.coord('projection_y_coordinate').points/1000,
1772
+ # 0,
1773
+ # mask_total_i_surface.data,
1774
+ # levels=[0, feature], colors=color, linestyles=':',linewidth=1)
1775
+ mask_feature = mask_total.data == feature
1776
+
1777
+ axes.scatter(
1778
+ # xx[mask_feature]/1000, yy[mask_feature]/1000, zz[mask_feature]/1000,
1779
+ xx[mask_feature] / 1000,
1780
+ yy[mask_feature] / 1000,
1781
+ zz[mask_feature],
1782
+ c=color,
1783
+ marker=",",
1784
+ s=5, # 60000.0 * TWC_i[Mask_particle],
1785
+ alpha=0.3,
1786
+ cmap="inferno",
1787
+ label=cell_string,
1788
+ rasterized=True,
1789
+ )
1790
+
1791
+ axes.set_xlim(xlim)
1792
+ axes.set_ylim(ylim)
1793
+ axes.set_zlim([0, 100])
1794
+
1795
+ # axes.set_zlim([0, 20])
1796
+ # axes.set_zticks([0, 5,10,15, 20])
1797
+ axes.set_xlabel("x (km)")
1798
+ axes.set_ylabel("y (km)")
1799
+ axes.zaxis.set_rotate_label(False) # disable automatic rotation
1800
+ # axes.set_zlabel('z (km)', rotation=90)
1801
+ axes.set_zlabel("model level", rotation=90)
1802
+
1803
+ return axes
1804
+
1805
+
1806
+ def plot_mask_cell_track_static_timeseries(
1807
+ cell,
1808
+ track,
1809
+ cog,
1810
+ features,
1811
+ mask_total,
1812
+ field_contour,
1813
+ field_filled,
1814
+ track_variable=None,
1815
+ variable=None,
1816
+ variable_ylabel=None,
1817
+ variable_label=[None],
1818
+ variable_legend=False,
1819
+ variable_color=None,
1820
+ width=10000,
1821
+ n_extend=1,
1822
+ name="test",
1823
+ plotdir="./",
1824
+ file_format=["png"],
1825
+ figsize=(20 / 2.54, 10 / 2.54),
1826
+ dpi=300,
1827
+ **kwargs,
1828
+ ):
1829
+ """Make plots for all cells with fixed frame including entire development of the cell and with one background field as filling and one background field as contrours
1830
+ Input:
1831
+ Output:
1832
+ """
1833
+ warnings.warn(
1834
+ "plot_mask_cell_track_static_timeseries is depreciated and will be removed or significantly changed in v2.0.",
1835
+ DeprecationWarning,
1836
+ )
1837
+
1838
+ mpl_backend = mpl.get_backend()
1839
+ if mpl_backend != "agg":
1840
+ warnings.warn(
1841
+ "When using tobac plotting functions that render a figure, you may need "
1842
+ "to set the Matplotlib backend to 'agg' by `matplotlib.use('agg')."
1843
+ )
1844
+ import matplotlib.pyplot as plt
1845
+
1846
+ from iris import Constraint
1847
+ from numpy import unique
1848
+ import os
1849
+ import pandas as pd
1850
+
1851
+ track_cell = track[track["cell"] == cell]
1852
+ x_min = track_cell["projection_x_coordinate"].min() - width
1853
+ x_max = track_cell["projection_x_coordinate"].max() + width
1854
+ y_min = track_cell["projection_y_coordinate"].min() - width
1855
+ y_max = track_cell["projection_y_coordinate"].max() + width
1856
+ time_min = track_cell["time"].min()
1857
+ # time_max=track_cell['time'].max()
1858
+
1859
+ track_variable_cell = track_variable[track_variable["cell"] == cell]
1860
+ track_variable_cell["time_cell"] = pd.to_timedelta(track_variable_cell["time_cell"])
1861
+ # track_variable_cell=track_variable_cell[(track_variable_cell['time']>=time_min) & (track_variable_cell['time']<=time_max)]
1862
+
1863
+ # set up looping over time based on mask's time coordinate to allow for one timestep before and after the track
1864
+ time_coord = mask_total.coord("time")
1865
+ time = time_coord.units.num2date(time_coord.points)
1866
+ i_start = max(0, np.where(time == track_cell["time"].values[0])[0][0] - n_extend)
1867
+ i_end = min(
1868
+ len(time) - 1,
1869
+ np.where(time == track_cell["time"].values[-1])[0][0] + n_extend + 1,
1870
+ )
1871
+ time_cell = time[slice(i_start, i_end)]
1872
+ for time_i in time_cell:
1873
+ constraint_time = Constraint(time=time_i)
1874
+ constraint_x = Constraint(
1875
+ projection_x_coordinate=lambda cell: x_min < cell < x_max
1876
+ )
1877
+ constraint_y = Constraint(
1878
+ projection_y_coordinate=lambda cell: y_min < cell < y_max
1879
+ )
1880
+ constraint = constraint_time & constraint_x & constraint_y
1881
+
1882
+ mask_total_i = mask_total.extract(constraint)
1883
+ if field_contour is None:
1884
+ field_contour_i = None
1885
+ else:
1886
+ field_contour_i = field_contour.extract(constraint)
1887
+ if field_filled is None:
1888
+ field_filled_i = None
1889
+ else:
1890
+ field_filled_i = field_filled.extract(constraint)
1891
+
1892
+ track_i = track[track["time"] == time_i]
1893
+ cells_mask = list(unique(mask_total_i.core_data()))
1894
+ track_cells = track_i.loc[
1895
+ (track_i["projection_x_coordinate"] > x_min)
1896
+ & (track_i["projection_x_coordinate"] < x_max)
1897
+ & (track_i["projection_y_coordinate"] > y_min)
1898
+ & (track_i["projection_y_coordinate"] < y_max)
1899
+ ]
1900
+ cells_track = list(track_cells["cell"].values)
1901
+ cells = list(set(cells_mask + cells_track))
1902
+ if cell not in cells:
1903
+ cells.append(cell)
1904
+ if 0 in cells:
1905
+ cells.remove(0)
1906
+ track_i = track_i[track_i["cell"].isin(cells)]
1907
+
1908
+ if cog is None:
1909
+ cog_i = None
1910
+ else:
1911
+ cog_i = cog[cog["cell"].isin(cells)]
1912
+ cog_i = cog_i[cog_i["time"] == time_i]
1913
+
1914
+ if features is None:
1915
+ features_i = None
1916
+ else:
1917
+ features_i = features[features["time"] == time_i]
1918
+
1919
+ fig1, ax1 = plt.subplots(ncols=2, nrows=1, figsize=figsize)
1920
+ fig1.subplots_adjust(left=0.1, bottom=0.15, right=0.90, top=0.85, wspace=0.3)
1921
+
1922
+ datestring_stamp = time_i.strftime("%Y-%m-%d %H:%M:%S")
1923
+ if time_i in track_cell["time"].values:
1924
+ time_cell_i = track_cell[track_cell["time"].values == time_i]["time_cell"]
1925
+ celltime_stamp = "%02d:%02d:%02d" % (
1926
+ time_cell_i.dt.total_seconds() // 3600,
1927
+ (time_cell_i.dt.total_seconds() % 3600) // 60,
1928
+ time_cell_i.dt.total_seconds() % 60,
1929
+ )
1930
+ else:
1931
+ celltime_stamp = " - "
1932
+ title = celltime_stamp + " , " + datestring_stamp
1933
+ datestring_file = time_i.strftime("%Y-%m-%d_%H%M%S")
1934
+
1935
+ # plot evolving timeseries of variable to second axis:
1936
+ ax1[0] = plot_mask_cell_individual_static(
1937
+ cell_i=cell,
1938
+ track=track_i,
1939
+ cog=cog_i,
1940
+ features=features_i,
1941
+ mask_total=mask_total_i,
1942
+ field_contour=field_contour_i,
1943
+ field_filled=field_filled_i,
1944
+ xlim=[x_min / 1000, x_max / 1000],
1945
+ ylim=[y_min / 1000, y_max / 1000],
1946
+ axes=ax1[0],
1947
+ title=title,
1948
+ **kwargs,
1949
+ )
1950
+
1951
+ track_variable_past = track_variable_cell[
1952
+ (track_variable_cell["time"] >= time_min)
1953
+ & (track_variable_cell["time"] <= time_i)
1954
+ ]
1955
+ track_variable_current = track_variable_cell[
1956
+ track_variable_cell["time"] == time_i
1957
+ ]
1958
+
1959
+ if variable_color is None:
1960
+ variable_color = "navy"
1961
+
1962
+ if type(variable) is str:
1963
+ # logging.debug('variable: '+str(variable))
1964
+ if type(variable_color) is str:
1965
+ variable_color = {variable: variable_color}
1966
+ variable = [variable]
1967
+
1968
+ for i_variable, variable_i in enumerate(variable):
1969
+ color = variable_color[variable_i]
1970
+ ax1[1].plot(
1971
+ track_variable_past["time_cell"].dt.total_seconds() / 60.0,
1972
+ track_variable_past[variable_i].values,
1973
+ color=color,
1974
+ linestyle="-",
1975
+ label=variable_label[i_variable],
1976
+ )
1977
+ ax1[1].plot(
1978
+ track_variable_current["time_cell"].dt.total_seconds() / 60.0,
1979
+ track_variable_current[variable_i].values,
1980
+ color=color,
1981
+ marker="o",
1982
+ markersize=4,
1983
+ fillstyle="full",
1984
+ )
1985
+ ax1[1].yaxis.tick_right()
1986
+ ax1[1].yaxis.set_label_position("right")
1987
+ ax1[1].set_xlim([0, 2 * 60])
1988
+ ax1[1].set_xticks(np.arange(0, 120, 15))
1989
+ ax1[1].set_ylim([0, max(10, 1.25 * track_variable_cell[variable].max().max())])
1990
+ ax1[1].set_xlabel("cell lifetime (min)")
1991
+ if variable_ylabel == None:
1992
+ variable_ylabel = variable
1993
+ ax1[1].set_ylabel(variable_ylabel)
1994
+ ax1[1].set_title(title)
1995
+
1996
+ # insert legend, if flag is True
1997
+ if variable_legend:
1998
+ if len(variable_label) < 5:
1999
+ ncol = 1
2000
+ else:
2001
+ ncol = 2
2002
+ ax1[1].legend(
2003
+ loc="upper right", bbox_to_anchor=(1, 1), ncol=ncol, fontsize=8
2004
+ )
2005
+
2006
+ out_dir = os.path.join(plotdir, name)
2007
+ os.makedirs(out_dir, exist_ok=True)
2008
+ if "png" in file_format:
2009
+ savepath_png = os.path.join(out_dir, name + "_" + datestring_file + ".png")
2010
+ fig1.savefig(savepath_png, dpi=dpi)
2011
+ logging.debug("Mask static plot saved to " + savepath_png)
2012
+ if "pdf" in file_format:
2013
+ savepath_pdf = os.path.join(out_dir, name + "_" + datestring_file + ".pdf")
2014
+ fig1.savefig(savepath_pdf, dpi=dpi)
2015
+ logging.debug("Mask static plot saved to " + savepath_pdf)
2016
+ plt.close()
2017
+ plt.clf()
2018
+
2019
+
2020
+ def map_tracks(
2021
+ track, axis_extent=None, figsize=None, axes=None, untracked_cell_value=-1
2022
+ ):
2023
+ """Plot the trajectories of the cells on a map.
2024
+
2025
+ Parameters
2026
+ ----------
2027
+ track : pandas.DataFrame
2028
+ Dataframe containing the linked features with a
2029
+ column 'cell'.
2030
+
2031
+ axis_extent : matplotlib.axes, optional
2032
+ Array containing the bounds of the longitude
2033
+ and latitude values. The structure is
2034
+ [long_min, long_max, lat_min, lat_max].
2035
+ Default is None.
2036
+
2037
+ figsize : tuple of floats, optional
2038
+ Width, height of the plot in inches.
2039
+ Default is (10, 10).
2040
+
2041
+ axes : cartopy.mpl.geoaxes.GeoAxesSubplot, optional
2042
+ GeoAxesSubplot to use for plotting. Default is None.
2043
+
2044
+ untracked_cell_value : int or np.nan, optional
2045
+ Value of untracked cells in track['cell'].
2046
+ Default is -1.
2047
+
2048
+ Returns
2049
+ -------
2050
+ axes : cartopy.mpl.geoaxes.GeoAxesSubplot
2051
+ Axes with the plotted trajectories.
2052
+
2053
+ Raises
2054
+ ------
2055
+ ValueError
2056
+ If no axes is passed.
2057
+ """
2058
+
2059
+ if figsize is not None:
2060
+ warnings.warn(
2061
+ "figsize is depreciated as this function does not create its own figure.",
2062
+ DeprecationWarning,
2063
+ )
2064
+ if axes is None:
2065
+ raise ValueError(
2066
+ "axes needed to plot tracks onto. Pass in an axis to axes to resolve this error."
2067
+ )
2068
+
2069
+ lat_dim, lon_dim, _ = find_dataframe_horizontal_coords(track, coord_type="latlon")
2070
+
2071
+ for cell in track["cell"].dropna().unique():
2072
+ if cell == untracked_cell_value:
2073
+ continue
2074
+ track_i = track[track["cell"] == cell]
2075
+ axes.plot(track_i[lon_dim], track_i[lat_dim], "-")
2076
+ if axis_extent:
2077
+ axes.set_extent(axis_extent)
2078
+ axes = make_map(axes)
2079
+ return axes
2080
+
2081
+
2082
+ def make_map(axes):
2083
+ """Configure the parameters of cartopy for plotting.
2084
+
2085
+ Parameters
2086
+ ----------
2087
+ axes : cartopy.mpl.geoaxes.GeoAxesSubplot
2088
+ GeoAxesSubplot to configure.
2089
+
2090
+ Returns
2091
+ -------
2092
+ axes : cartopy.mpl.geoaxes.GeoAxesSubplot
2093
+ Cartopy axes to configure
2094
+ """
2095
+
2096
+ import matplotlib.ticker as mticker
2097
+ import cartopy.crs as ccrs
2098
+ from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
2099
+
2100
+ gl = axes.gridlines(
2101
+ crs=ccrs.PlateCarree(),
2102
+ draw_labels=True,
2103
+ linewidth=2,
2104
+ color="gray",
2105
+ alpha=0.5,
2106
+ linestyle="-",
2107
+ )
2108
+ axes.coastlines("10m")
2109
+
2110
+ gl.xlabels_top = False
2111
+ gl.ylabels_right = False
2112
+ gl.xlocator = mticker.MaxNLocator(nbins=5, min_n_ticks=3, steps=None)
2113
+ gl.ylocator = mticker.MaxNLocator(nbins=5, min_n_ticks=3, steps=None)
2114
+ gl.xformatter = LONGITUDE_FORMATTER
2115
+ gl.yformatter = LATITUDE_FORMATTER
2116
+ # gl.xlabel_style = {'size': 15, 'color': 'gray'}
2117
+ # gl.xlabel_style = {'color': 'red', 'weight': 'bold'}
2118
+ return axes
2119
+
2120
+
2121
+ def plot_lifetime_histogram(
2122
+ track, axes=None, bin_edges=np.arange(0, 200, 20), density=False, **kwargs
2123
+ ):
2124
+ """Plot the liftetime histogram of the cells.
2125
+
2126
+ Parameters
2127
+ ----------
2128
+ track : pandas.DataFrame
2129
+ DataFrame of the features containing the columns
2130
+ 'cell' and 'time_cell'.
2131
+
2132
+ axes : matplotlib.axes.Axes, optional
2133
+ Matplotlib axes to plot on. Default is None.
2134
+
2135
+ bin_edges : int or ndarray, optional
2136
+ If bin_edges is an int, it defines the number of
2137
+ equal-width bins in the given range. If bins is
2138
+ a sequence, it defines a monotonically increasing
2139
+ array of bin edges, including the rightmost edge.
2140
+ Default is np.arange(0, 200, 20).
2141
+
2142
+ density : bool, optional
2143
+ If False, the result will contain the number of
2144
+ samples in each bin. If True, the result is the
2145
+ value of the probability density function at the
2146
+ bin, normalized such that the integral over the
2147
+ range is 1. Default is False.
2148
+
2149
+ **kwargs
2150
+
2151
+ Returns
2152
+ -------
2153
+ plot_hist : list
2154
+ List containing the matplotlib.lines.Line2D instance
2155
+ of the histogram
2156
+ """
2157
+
2158
+ hist, bin_edges, bin_centers = lifetime_histogram(
2159
+ track, bin_edges=bin_edges, density=density
2160
+ )
2161
+ plot_hist = axes.plot(bin_centers, hist, **kwargs)
2162
+ return plot_hist
2163
+
2164
+
2165
+ def plot_lifetime_histogram_bar(
2166
+ track,
2167
+ axes=None,
2168
+ bin_edges=np.arange(0, 200, 20),
2169
+ density=False,
2170
+ width_bar=1,
2171
+ shift=0.5,
2172
+ **kwargs,
2173
+ ):
2174
+ """Plot the liftetime histogram of the cells as bar plot.
2175
+
2176
+ Parameters
2177
+ ----------
2178
+ track : pandas.DataFrame
2179
+ DataFrame of the features containing the columns
2180
+ 'cell' and 'time_cell'.
2181
+
2182
+ axes : matplotlib.axes.Axes, optional
2183
+ Matplotlib axes to plot on. Default is None.
2184
+
2185
+ bin_edges : int or ndarray, optional
2186
+ If bin_edges is an int, it defines the number of
2187
+ equal-width bins in the given range. If bins is
2188
+ a sequence, it defines a monotonically increasing
2189
+ array of bin edges, including the rightmost edge.
2190
+
2191
+ density : bool, optional
2192
+ If False, the result will contain the number of
2193
+ samples in each bin. If True, the result is the
2194
+ value of the probability density function at the
2195
+ bin, normalized such that the integral over the
2196
+ range is 1. Default is False.
2197
+
2198
+ width_bar : float
2199
+ Width of the bars. Default is 1.
2200
+
2201
+ shift : float
2202
+ Value to shift the bin centers to the right.
2203
+ Default is 0.5.
2204
+
2205
+ **kwargs
2206
+
2207
+ Returns
2208
+ -------
2209
+ plot_hist : matplotlib.container.BarContainer
2210
+ matplotlib.container.BarContainer instance
2211
+ of the histogram
2212
+ """
2213
+
2214
+ hist, bin_edges, bin_centers = lifetime_histogram(
2215
+ track, bin_edges=bin_edges, density=density
2216
+ )
2217
+ plot_hist = axes.bar(bin_centers + shift, hist, width=width_bar, **kwargs)
2218
+ return plot_hist
2219
+
2220
+
2221
+ def plot_histogram_cellwise(
2222
+ track, bin_edges, variable, quantity, axes=None, density=False, **kwargs
2223
+ ):
2224
+ """Plot the histogram of a variable based on the cells.
2225
+
2226
+ Parameters
2227
+ ----------
2228
+ track : pandas.DataFrame
2229
+ DataFrame of the features containing the variable
2230
+ as column and a column 'cell'.
2231
+
2232
+ bin_edges : int or ndarray
2233
+ If bin_edges is an int, it defines the number of
2234
+ equal-width bins in the given range. If bins is
2235
+ a sequence, it defines a monotonically increasing
2236
+ array of bin edges, including the rightmost edge.
2237
+
2238
+ variable : string
2239
+ Column of the DataFrame with the variable on which the
2240
+ histogram is to be based on. Default is None.
2241
+
2242
+ quantity : {'max', 'min', 'mean'}, optional
2243
+ Flag determining wether to use maximum, minimum or mean
2244
+ of a variable from all timeframes the cell covers.
2245
+ Default is 'max'.
2246
+
2247
+ axes : matplotlib.axes.Axes, optional
2248
+ Matplotlib axes to plot on. Default is None.
2249
+
2250
+ density : bool, optional
2251
+ If False, the result will contain the number of
2252
+ samples in each bin. If True, the result is the
2253
+ value of the probability density function at the
2254
+ bin, normalized such that the integral over the
2255
+ range is 1. Default is False.
2256
+
2257
+ **kwargs
2258
+
2259
+ Returns
2260
+ -------
2261
+ plot_hist : list
2262
+ List containing the matplotlib.lines.Line2D instance
2263
+ of the histogram
2264
+ """
2265
+
2266
+ hist, bin_edges, bin_centers = histogram_cellwise(
2267
+ track,
2268
+ bin_edges=bin_edges,
2269
+ variable=variable,
2270
+ quantity=quantity,
2271
+ density=density,
2272
+ )
2273
+ plot_hist = axes.plot(bin_centers, hist, **kwargs)
2274
+ return plot_hist
2275
+
2276
+
2277
+ def plot_histogram_featurewise(
2278
+ Track, bin_edges, variable, axes=None, density=False, **kwargs
2279
+ ):
2280
+ """Plot the histogram of a variable based on the features.
2281
+
2282
+ Parameters
2283
+ ----------
2284
+ Track : pandas.DataFrame
2285
+ DataFrame of the features containing the variable
2286
+ as column.
2287
+
2288
+ bin_edges : int or ndarray
2289
+ If bin_edges is an int, it defines the number of
2290
+ equal-width bins in the given range. If bins is
2291
+ a sequence, it defines a monotonically increasing
2292
+ array of bin edges, including the rightmost edge.
2293
+
2294
+ variable : str
2295
+ Column of the DataFrame with the variable on which the
2296
+ histogram is to be based on.
2297
+
2298
+ axes : matplotlib.axes.Axes, optional
2299
+ Matplotlib axes to plot on. Default is None.
2300
+
2301
+ density : bool, optional
2302
+ If False, the result will contain the number of
2303
+ samples in each bin. If True, the result is the
2304
+ value of the probability density function at the
2305
+ bin, normalized such that the integral over the
2306
+ range is 1. Default is False.
2307
+
2308
+ **kwargs
2309
+
2310
+ Returns
2311
+ -------
2312
+ plot_hist : list
2313
+ List containing the matplotlib.lines.Line2D instance
2314
+ of the histogram
2315
+ """
2316
+
2317
+ hist, bin_edges, bin_centers = histogram_featurewise(
2318
+ Track, bin_edges=bin_edges, variable=variable, density=density
2319
+ )
2320
+ plot_hist = axes.plot(bin_centers, hist, **kwargs)
2321
+ return plot_hist