cloudnetpy 1.49.9__py3-none-any.whl → 1.87.3__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 (116) hide show
  1. cloudnetpy/categorize/__init__.py +1 -2
  2. cloudnetpy/categorize/atmos_utils.py +297 -67
  3. cloudnetpy/categorize/attenuation.py +31 -0
  4. cloudnetpy/categorize/attenuations/__init__.py +37 -0
  5. cloudnetpy/categorize/attenuations/gas_attenuation.py +30 -0
  6. cloudnetpy/categorize/attenuations/liquid_attenuation.py +84 -0
  7. cloudnetpy/categorize/attenuations/melting_attenuation.py +78 -0
  8. cloudnetpy/categorize/attenuations/rain_attenuation.py +84 -0
  9. cloudnetpy/categorize/categorize.py +332 -156
  10. cloudnetpy/categorize/classify.py +127 -125
  11. cloudnetpy/categorize/containers.py +107 -76
  12. cloudnetpy/categorize/disdrometer.py +40 -0
  13. cloudnetpy/categorize/droplet.py +23 -21
  14. cloudnetpy/categorize/falling.py +53 -24
  15. cloudnetpy/categorize/freezing.py +25 -12
  16. cloudnetpy/categorize/insects.py +35 -23
  17. cloudnetpy/categorize/itu.py +243 -0
  18. cloudnetpy/categorize/lidar.py +36 -41
  19. cloudnetpy/categorize/melting.py +34 -26
  20. cloudnetpy/categorize/model.py +84 -37
  21. cloudnetpy/categorize/mwr.py +18 -14
  22. cloudnetpy/categorize/radar.py +215 -102
  23. cloudnetpy/cli.py +578 -0
  24. cloudnetpy/cloudnetarray.py +43 -89
  25. cloudnetpy/concat_lib.py +218 -78
  26. cloudnetpy/constants.py +28 -10
  27. cloudnetpy/datasource.py +61 -86
  28. cloudnetpy/exceptions.py +49 -20
  29. cloudnetpy/instruments/__init__.py +5 -0
  30. cloudnetpy/instruments/basta.py +29 -12
  31. cloudnetpy/instruments/bowtie.py +135 -0
  32. cloudnetpy/instruments/ceilo.py +138 -115
  33. cloudnetpy/instruments/ceilometer.py +164 -80
  34. cloudnetpy/instruments/cl61d.py +21 -5
  35. cloudnetpy/instruments/cloudnet_instrument.py +74 -36
  36. cloudnetpy/instruments/copernicus.py +108 -30
  37. cloudnetpy/instruments/da10.py +54 -0
  38. cloudnetpy/instruments/disdrometer/common.py +126 -223
  39. cloudnetpy/instruments/disdrometer/parsivel.py +453 -94
  40. cloudnetpy/instruments/disdrometer/thies.py +254 -87
  41. cloudnetpy/instruments/fd12p.py +201 -0
  42. cloudnetpy/instruments/galileo.py +65 -23
  43. cloudnetpy/instruments/hatpro.py +123 -49
  44. cloudnetpy/instruments/instruments.py +113 -1
  45. cloudnetpy/instruments/lufft.py +39 -17
  46. cloudnetpy/instruments/mira.py +268 -61
  47. cloudnetpy/instruments/mrr.py +187 -0
  48. cloudnetpy/instruments/nc_lidar.py +19 -8
  49. cloudnetpy/instruments/nc_radar.py +109 -55
  50. cloudnetpy/instruments/pollyxt.py +135 -51
  51. cloudnetpy/instruments/radiometrics.py +313 -59
  52. cloudnetpy/instruments/rain_e_h3.py +171 -0
  53. cloudnetpy/instruments/rpg.py +321 -189
  54. cloudnetpy/instruments/rpg_reader.py +74 -40
  55. cloudnetpy/instruments/toa5.py +49 -0
  56. cloudnetpy/instruments/vaisala.py +95 -343
  57. cloudnetpy/instruments/weather_station.py +774 -105
  58. cloudnetpy/metadata.py +90 -19
  59. cloudnetpy/model_evaluation/file_handler.py +55 -52
  60. cloudnetpy/model_evaluation/metadata.py +46 -20
  61. cloudnetpy/model_evaluation/model_metadata.py +1 -1
  62. cloudnetpy/model_evaluation/plotting/plot_tools.py +32 -37
  63. cloudnetpy/model_evaluation/plotting/plotting.py +327 -117
  64. cloudnetpy/model_evaluation/products/advance_methods.py +92 -83
  65. cloudnetpy/model_evaluation/products/grid_methods.py +88 -63
  66. cloudnetpy/model_evaluation/products/model_products.py +43 -35
  67. cloudnetpy/model_evaluation/products/observation_products.py +41 -35
  68. cloudnetpy/model_evaluation/products/product_resampling.py +17 -7
  69. cloudnetpy/model_evaluation/products/tools.py +29 -20
  70. cloudnetpy/model_evaluation/statistics/statistical_methods.py +30 -20
  71. cloudnetpy/model_evaluation/tests/e2e/conftest.py +3 -3
  72. cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +9 -5
  73. cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +15 -14
  74. cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +9 -5
  75. cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +15 -14
  76. cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +9 -5
  77. cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +15 -14
  78. cloudnetpy/model_evaluation/tests/unit/conftest.py +42 -41
  79. cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +41 -48
  80. cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +216 -194
  81. cloudnetpy/model_evaluation/tests/unit/test_model_products.py +23 -21
  82. cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +37 -38
  83. cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +43 -40
  84. cloudnetpy/model_evaluation/tests/unit/test_plotting.py +30 -36
  85. cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +68 -31
  86. cloudnetpy/model_evaluation/tests/unit/test_tools.py +33 -26
  87. cloudnetpy/model_evaluation/utils.py +2 -1
  88. cloudnetpy/output.py +170 -111
  89. cloudnetpy/plotting/__init__.py +2 -1
  90. cloudnetpy/plotting/plot_meta.py +562 -822
  91. cloudnetpy/plotting/plotting.py +1142 -704
  92. cloudnetpy/products/__init__.py +1 -0
  93. cloudnetpy/products/classification.py +370 -88
  94. cloudnetpy/products/der.py +85 -55
  95. cloudnetpy/products/drizzle.py +77 -34
  96. cloudnetpy/products/drizzle_error.py +15 -11
  97. cloudnetpy/products/drizzle_tools.py +79 -59
  98. cloudnetpy/products/epsilon.py +211 -0
  99. cloudnetpy/products/ier.py +27 -50
  100. cloudnetpy/products/iwc.py +55 -48
  101. cloudnetpy/products/lwc.py +96 -70
  102. cloudnetpy/products/mwr_tools.py +186 -0
  103. cloudnetpy/products/product_tools.py +170 -128
  104. cloudnetpy/utils.py +455 -240
  105. cloudnetpy/version.py +2 -2
  106. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/METADATA +44 -40
  107. cloudnetpy-1.87.3.dist-info/RECORD +127 -0
  108. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/WHEEL +1 -1
  109. cloudnetpy-1.87.3.dist-info/entry_points.txt +2 -0
  110. docs/source/conf.py +2 -2
  111. cloudnetpy/categorize/atmos.py +0 -361
  112. cloudnetpy/products/mwr_multi.py +0 -68
  113. cloudnetpy/products/mwr_single.py +0 -75
  114. cloudnetpy-1.49.9.dist-info/RECORD +0 -112
  115. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info/licenses}/LICENSE +0 -0
  116. {cloudnetpy-1.49.9.dist-info → cloudnetpy-1.87.3.dist-info}/top_level.txt +0 -0
@@ -1,18 +1,27 @@
1
1
  import logging
2
2
  import os
3
3
  import sys
4
+ from datetime import date
4
5
 
6
+ import matplotlib as mpl
5
7
  import matplotlib.pyplot as plt
8
+ import netCDF4
6
9
  import numpy as np
10
+ import numpy.typing as npt
11
+ from matplotlib.axes import Axes
12
+ from matplotlib.colorbar import Colorbar
13
+ from matplotlib.colorizer import ColorizingArtist
14
+ from matplotlib.colors import ListedColormap
7
15
  from matplotlib.patches import Patch
16
+ from matplotlib.pyplot import Figure
8
17
  from mpl_toolkits.axes_grid1 import make_axes_locatable
9
18
  from numpy import ma
10
19
 
11
20
  import cloudnetpy.model_evaluation.plotting.plot_tools as p_tools
12
- import cloudnetpy.plotting.plotting as cloud_plt
13
21
  from cloudnetpy.model_evaluation.model_metadata import MODELS
14
- from cloudnetpy.model_evaluation.plotting.plot_meta import ATTRIBUTES
22
+ from cloudnetpy.model_evaluation.plotting.plot_meta import ATTRIBUTES, PlotMeta
15
23
  from cloudnetpy.model_evaluation.statistics.statistical_methods import DayStatistics
24
+ from cloudnetpy.plotting.plotting import Dimensions, get_log_cbar_tick_labels, lin2log
16
25
 
17
26
  sys.path.append(os.path.dirname(os.path.abspath(__file__)))
18
27
 
@@ -21,6 +30,7 @@ def generate_L3_day_plots(
21
30
  nc_file: str,
22
31
  product: str,
23
32
  model: str,
33
+ *,
24
34
  title: bool = True,
25
35
  var_list: list | None = None,
26
36
  fig_type: str | None = "group",
@@ -28,7 +38,7 @@ def generate_L3_day_plots(
28
38
  save_path: str | None = None,
29
39
  image_name: str | None = None,
30
40
  show: bool | None = False,
31
- ):
41
+ ) -> list[Dimensions]:
32
42
  """Generate visualizations for level 3 dayscale products.
33
43
  With figure type visualizations can be subplot in group, pair, single or
34
44
  statistic of given product. In group fig_type all different methods are plot
@@ -38,6 +48,7 @@ def generate_L3_day_plots(
38
48
  Single fig_type will plot each product variable in a own figure.
39
49
  Statistic fig_type will plot select statistical method of all product method
40
50
  in same fig.
51
+
41
52
  Args:
42
53
  nc_file (str): Path to source file
43
54
  product (str): Name of product wanted to plot
@@ -63,6 +74,7 @@ def generate_L3_day_plots(
63
74
  In case of model cycles, cycles are visualized in their on figures same
64
75
  way as an individual model run would be visualized in its own in a group
65
76
  figure.
77
+
66
78
  Examples:
67
79
  >>> from cloudnetpy.model_evaluation.plotting.plotting
68
80
  import generate_L3_day_plots
@@ -76,17 +88,25 @@ def generate_L3_day_plots(
76
88
  >>> generate_L3_day_plots(l3_day_file, product, model,
77
89
  >>> fig_type='statistic', stats=['error'])
78
90
  """
91
+
92
+ def _check_cycle_names() -> None:
93
+ if not c_names:
94
+ raise AttributeError
95
+
79
96
  cls = __import__("plotting")
80
97
  model_info = MODELS[model]
81
98
  model_name = model_info.model_name
82
99
  name_set = p_tools.parse_wanted_names(nc_file, product, model, var_list)
83
- for names in name_set:
100
+ unique_tuples = {tuple(lst) for lst in name_set}
101
+ name_set_unique = tuple(list(tup) for tup in unique_tuples)
102
+
103
+ dimensions = []
104
+ for names in name_set_unique:
84
105
  if len(names) > 0:
85
106
  try:
86
107
  cycle_names, cycles = p_tools.sort_cycles(names, model)
87
108
  for i, c_names in enumerate(cycle_names):
88
- if not c_names:
89
- raise AttributeError
109
+ _check_cycle_names()
90
110
  params = [
91
111
  product,
92
112
  c_names,
@@ -95,10 +115,8 @@ def generate_L3_day_plots(
95
115
  model_name,
96
116
  save_path,
97
117
  image_name,
98
- show,
99
- cycles[i],
100
- title,
101
118
  ]
119
+ kwargs = {"show": show, "title": title, "cycle": cycles[i]}
102
120
  if fig_type == "statistic":
103
121
  params = [
104
122
  product,
@@ -109,11 +127,12 @@ def generate_L3_day_plots(
109
127
  stats,
110
128
  save_path,
111
129
  image_name,
112
- show,
113
- cycles[i],
114
- title,
115
130
  ]
116
- getattr(cls, f"get_{fig_type}_plots")(*params)
131
+ figs, axes = getattr(cls, f"get_{fig_type}_plots")(
132
+ *params, **kwargs
133
+ )
134
+ for fig, ax in zip(figs, axes, strict=False):
135
+ dimensions.append(Dimensions(fig, ax))
117
136
  except AttributeError:
118
137
  params = [
119
138
  product,
@@ -123,10 +142,8 @@ def generate_L3_day_plots(
123
142
  model_name,
124
143
  save_path,
125
144
  image_name,
126
- show,
127
- "",
128
- title,
129
145
  ]
146
+ kwargs = {"show": show, "title": title}
130
147
  if fig_type == "statistic":
131
148
  params = [
132
149
  product,
@@ -137,11 +154,11 @@ def generate_L3_day_plots(
137
154
  stats,
138
155
  save_path,
139
156
  image_name,
140
- show,
141
- "",
142
- title,
143
157
  ]
144
- getattr(cls, f"get_{fig_type}_plots")(*params)
158
+ figs, axes = getattr(cls, f"get_{fig_type}_plots")(*params, **kwargs)
159
+ for fig, ax in zip(figs, axes, strict=False):
160
+ dimensions.append(Dimensions(fig, ax))
161
+ return dimensions
145
162
 
146
163
 
147
164
  def get_group_plots(
@@ -152,14 +169,17 @@ def get_group_plots(
152
169
  model_name: str,
153
170
  save_path: str,
154
171
  image_name: str,
172
+ *,
155
173
  show: bool,
156
174
  cycle: str = "",
157
175
  title: bool = True,
158
- ):
176
+ include_xlimits: bool = False,
177
+ ) -> tuple:
159
178
  """Group subplot visualization for both standard and advection downsampling.
160
179
  Generates group subplot figure for product with model and all different
161
180
  downsampling methods. Generates separated figures for standard and advection
162
181
  timegrids. All model cycles if any will be generated to their own figures.
182
+
163
183
  Args:
164
184
  product (str): Name of the product
165
185
  names (list): List of variables to be visualized to same fig
@@ -171,6 +191,7 @@ def get_group_plots(
171
191
  show (bool): Show figure before saving if True
172
192
  cycle (str): Name of cycle if exists
173
193
  title (bool): True or False if wanted to add title to subfig
194
+ include_xlimits (bool): Show labels at the ends of x-axis
174
195
  """
175
196
  fig, ax = initialize_figure(len(names))
176
197
  model_run = model
@@ -178,22 +199,28 @@ def get_group_plots(
178
199
  j = 0
179
200
  for j, name in enumerate(names):
180
201
  variable_info = ATTRIBUTES[product]
181
- cloud_plt.set_ax(ax[j], 12, ylabel=None)
202
+ set_yax(ax[j], 12, ylabel=None)
203
+ set_xax(ax[j], include_xlimits=include_xlimits)
182
204
  if title:
183
205
  _set_title(ax[j], name, product, variable_info)
184
206
  if j == 0 and title:
185
207
  _set_title(ax[j], model, product, variable_info, model_name)
186
208
  data, x, y = p_tools.read_data_characters(nc_file, name, model)
187
209
  plot_colormesh(ax[j], data, (x, y), variable_info)
188
- casedate = cloud_plt.set_labels(fig, ax[j], nc_file)
210
+ casedate = set_labels(fig, ax[j], nc_file)
189
211
  if "adv" in name:
190
212
  product = product + "_adv"
191
213
  if len(cycle) > 1:
192
214
  fig.text(0.64, 0.885, f"Cycle: {cycle}", fontsize=13)
193
215
  model_run = f"{model}_{cycle}"
194
- cloud_plt.handle_saving(
195
- image_name, save_path, show, casedate, [product, model_run, "group"]
216
+ handle_saving(
217
+ image_name,
218
+ save_path,
219
+ casedate,
220
+ [product, model_run, "group"],
221
+ show=show,
196
222
  )
223
+ return fig, ax
197
224
 
198
225
 
199
226
  def get_pair_plots(
@@ -204,14 +231,17 @@ def get_pair_plots(
204
231
  model_name: str,
205
232
  save_path: str,
206
233
  image_name: str,
207
- show: bool,
208
234
  cycle: str = "",
235
+ *,
236
+ show: bool = False,
209
237
  title: bool = True,
210
- ):
238
+ include_xlimits: bool = False,
239
+ ) -> None:
211
240
  """Pair subplots of model and product method.
212
241
  In upper subplot is model product and lower subplot one of the
213
242
  downsampled method of select product. Function generates all product methods
214
243
  in a given nc-file in loop.
244
+
215
245
  Args:
216
246
  product (str): Name of the product
217
247
  names (list): List of variables to be visualized to same fig
@@ -223,6 +253,7 @@ def get_pair_plots(
223
253
  show (bool): Show figure before saving if True
224
254
  cycle (str): Name of cycle if exists
225
255
  title (bool): True or False if wanted add title to subfig
256
+ include_xlimits (bool): Show labels at the ends of x-axis
226
257
  """
227
258
  variable_info = ATTRIBUTES[product]
228
259
  model_ax = names[0]
@@ -230,8 +261,11 @@ def get_pair_plots(
230
261
  if i == 0:
231
262
  continue
232
263
  fig, ax = initialize_figure(2)
233
- cloud_plt.set_ax(ax[0], 12, ylabel=None)
234
- cloud_plt.set_ax(ax[-1], 12, ylabel=None)
264
+ set_yax(ax[0], 12, ylabel=None)
265
+ set_yax(ax[-1], 12, ylabel=None)
266
+ set_xax(ax[0], include_xlimits=include_xlimits)
267
+ set_xax(ax[-1], include_xlimits=include_xlimits)
268
+
235
269
  if title:
236
270
  _set_title(ax[0], model, product, variable_info, model_name)
237
271
  _set_title(ax[-1], name, product, variable_info)
@@ -239,10 +273,16 @@ def get_pair_plots(
239
273
  data, x, y = p_tools.read_data_characters(nc_file, name, model)
240
274
  plot_colormesh(ax[0], model_data, (mx, my), variable_info)
241
275
  plot_colormesh(ax[-1], data, (x, y), variable_info)
242
- casedate = cloud_plt.set_labels(fig, ax[-1], nc_file)
276
+ casedate = set_labels(fig, ax[-1], nc_file)
243
277
  if len(cycle) > 1:
244
278
  fig.text(0.64, 0.889, f"Cycle: {cycle}", fontsize=13)
245
- cloud_plt.handle_saving(image_name, save_path, show, casedate, [name, "pair"])
279
+ handle_saving(
280
+ image_name,
281
+ save_path,
282
+ casedate,
283
+ [name, "pair"],
284
+ show=show,
285
+ )
246
286
 
247
287
 
248
288
  def get_single_plots(
@@ -253,11 +293,14 @@ def get_single_plots(
253
293
  model_name: str,
254
294
  save_path: str,
255
295
  image_name: str,
296
+ *,
256
297
  show: bool,
257
298
  cycle: str = "",
258
299
  title: bool = True,
259
- ):
300
+ include_xlimits: bool = False,
301
+ ) -> tuple[list, list]:
260
302
  """Generates figures of each product variable from given file in loop.
303
+
261
304
  Args:
262
305
  product (str): Name of the product
263
306
  names (list): List of variables to be visualized to same fig
@@ -269,38 +312,55 @@ def get_single_plots(
269
312
  show (bool): Show figure before saving if True
270
313
  cycle (str): Name of cycle if exists
271
314
  title (bool): True or False if wanted to add title to subfig
315
+ include_xlimits (bool): Show labels at the ends of x-axis
272
316
  """
317
+ figs = []
318
+ axes = []
273
319
  variable_info = ATTRIBUTES[product]
274
- for _, name in enumerate(names):
320
+ for name in names:
275
321
  fig, ax = initialize_figure(1)
276
- cloud_plt.set_ax(ax[0], 12, ylabel=None)
322
+ figs.append(fig)
323
+ axes.append(ax)
324
+ set_yax(ax[0], 12, ylabel=None)
325
+ set_xax(ax[0], include_xlimits=include_xlimits)
326
+
277
327
  if title:
278
328
  _set_title(ax[0], name, product, variable_info)
279
329
  data, x, y = p_tools.read_data_characters(nc_file, name, model)
280
330
  plot_colormesh(ax[0], data, (x, y), variable_info)
281
- casedate = cloud_plt.set_labels(fig, ax[0], nc_file, sub_title=title)
331
+ casedate = set_labels(fig, ax[0], nc_file, sub_title=title)
282
332
  if title:
283
333
  if len(cycle) > 1:
284
334
  fig.text(0.64, 0.9, f"{model_name} cycle: {cycle}", fontsize=13)
285
335
  else:
286
336
  fig.text(0.64, 0.9, f"{model_name}", fontsize=13)
287
- cloud_plt.handle_saving(image_name, save_path, show, casedate, [name, "single"])
337
+ handle_saving(
338
+ image_name,
339
+ save_path,
340
+ casedate,
341
+ [name, "single"],
342
+ show=show,
343
+ )
344
+ return figs, axes
288
345
 
289
346
 
290
- def plot_colormesh(ax, data: np.ndarray, axes: tuple, variable_info):
347
+ def plot_colormesh(
348
+ ax: Axes, data: npt.NDArray, axes: tuple, variable_info: PlotMeta
349
+ ) -> None:
291
350
  vmin, vmax = variable_info.plot_range
292
351
  if variable_info.plot_scale == "logarithmic":
293
- data, vmin, vmax = cloud_plt.lin2log(data, vmin, vmax)
352
+ data, vmin, vmax = lin2log(data, vmin, vmax)
294
353
  cmap = plt.get_cmap(variable_info.cbar, 22)
295
354
  data[data < vmin] = ma.masked
296
355
  pl = ax.pcolormesh(*axes, data, vmin=vmin, vmax=vmax, cmap=cmap)
297
356
  colorbar = init_colorbar(pl, ax)
298
357
  if variable_info.plot_scale == "logarithmic":
299
- tick_labels = cloud_plt.generate_log_cbar_ticklabel_list(vmin, vmax)
300
- colorbar.set_ticks(np.arange(vmin, vmax + 1))
358
+ tick_labels = get_log_cbar_tick_labels(vmin, vmax)
359
+ colorbar.set_ticks(np.arange(vmin, vmax + 1).tolist()) # type: ignore[arg-type]
301
360
  colorbar.ax.set_yticklabels(tick_labels)
302
361
  ax.set_facecolor("white")
303
- colorbar.set_label(variable_info.clabel, fontsize=13)
362
+ if variable_info.clabel is not None:
363
+ colorbar.set_label(variable_info.clabel, fontsize=13)
304
364
 
305
365
 
306
366
  def get_statistic_plots(
@@ -312,10 +372,11 @@ def get_statistic_plots(
312
372
  stats: list,
313
373
  save_path: str,
314
374
  image_name: str,
375
+ *,
315
376
  show: bool,
316
377
  cycle: str = "",
317
378
  title: bool = True,
318
- ):
379
+ ) -> tuple:
319
380
  """Statistical subplots for day scale products.
320
381
  Statistical analysis can be done by day scale with relative error ('error'),
321
382
  total data area analysis ('area'), histogram ('hist') or vertical profiles
@@ -323,6 +384,7 @@ def get_statistic_plots(
323
384
  per statistical method for a select product. All different downsampled method
324
385
  are in a same fig. Standard and advection timegrids are separated to own figs
325
386
  as well as different cycle runs.
387
+
326
388
  Args:
327
389
  product (str): Name of the product
328
390
  names (list): List of variables to be visualized to same fig
@@ -335,18 +397,34 @@ def get_statistic_plots(
335
397
  image_name (str, optional): Saving name of generated fig
336
398
  show (bool): Show figure before saving if True
337
399
  cycle (str): Name of cycle if exists
400
+ title (bool): True or False if wanted to add title to subfig
338
401
  """
339
- # pylint: disable=too-many-branches
340
- # pylint: disable=too-many-nested-blocks
341
402
  model_run = model
342
403
  name = ""
343
404
  j = 0
405
+
406
+ def _check_data() -> None:
407
+ if model_missing and obs_missing:
408
+ _raise()
409
+
410
+ def _check_data2() -> None:
411
+ if "error" in stat and np.all(day_stat.model_stat.mask is True):
412
+ _raise()
413
+
414
+ def _raise() -> None:
415
+ err_msg = f"No data in {model_name} or observation"
416
+ raise ValueError(err_msg)
417
+
418
+ figs = []
419
+ axes = []
344
420
  for stat in stats:
345
421
  try:
346
422
  obs_missing = False
347
423
  model_missing = False
348
424
  variable_info = ATTRIBUTES[product]
349
425
  fig, ax = initialize_figure(len(names) - 1, stat)
426
+ figs.append(fig)
427
+ axes.append(ax)
350
428
  model_data, _, _ = p_tools.read_data_characters(nc_file, names[0], model)
351
429
  if np.all(model_data.mask is True):
352
430
  model_missing = True
@@ -354,65 +432,73 @@ def get_statistic_plots(
354
432
  data, x, y = p_tools.read_data_characters(nc_file, name, model)
355
433
  if np.all(data.mask is True):
356
434
  obs_missing = True
357
- if model_missing and obs_missing:
358
- raise ValueError("No data in either dataset")
359
- if product == "cf" and stat == "error":
360
- stat = "aerror"
435
+ _check_data()
436
+ statistics = "aerror" if product == "cf" and stat == "error" else stat
361
437
  if j > 0:
362
- name = ""
363
- name = _get_stat_titles(name, product, variable_info)
438
+ name_new = ""
439
+ name_new = _get_stat_titles(name_new, product, variable_info)
364
440
  day_stat = DayStatistics(
365
- stat, [product, model_name, name], model_data, data
441
+ statistics,
442
+ [product, model_name, name_new],
443
+ model_data,
444
+ data,
366
445
  )
367
- if "error" in stat:
368
- if np.all(day_stat.model_stat.mask is True):
369
- raise ValueError("No data to calculate relative error")
446
+ _check_data2()
370
447
  initialize_statistic_plots(
371
448
  j,
372
449
  len(names) - 1,
373
450
  ax[j - 1],
374
- stat,
451
+ statistics,
375
452
  day_stat,
376
453
  model_data,
377
454
  data,
378
455
  (x, y),
379
456
  variable_info,
380
- title,
457
+ title=title,
381
458
  )
382
- except ValueError as e:
383
- logging.error(e)
459
+ except ValueError:
460
+ logging.exception("Exception occurred")
384
461
  if stat not in ("hist", "vertical"):
385
- casedate = cloud_plt.set_labels(fig, ax[j - 1], nc_file, sub_title=title)
462
+ casedate = set_labels(fig, ax[j - 1], nc_file, sub_title=title)
386
463
  else:
387
- casedate = cloud_plt.read_date(nc_file)
388
- _name = cloud_plt.read_location(nc_file)
464
+ casedate = read_date(nc_file)
465
+ _name = read_location(nc_file)
389
466
  if title:
390
- cloud_plt.add_subtitle(fig, casedate, _name.capitalize())
467
+ add_subtitle(fig, casedate, _name.capitalize())
391
468
  if len(cycle) > 1:
392
469
  fig.text(0.64, 0.885, f"Cycle: {cycle}", fontsize=13)
393
470
  model_run = f"{model}_{cycle}"
394
- cloud_plt.handle_saving(
395
- image_name, save_path, show, casedate, [name, stat, model_run]
471
+ handle_saving(
472
+ image_name,
473
+ save_path,
474
+ casedate,
475
+ [name, stat, model_run],
476
+ show=show,
396
477
  )
478
+ return figs, axes
397
479
 
398
480
 
399
481
  def initialize_statistic_plots(
400
482
  j: int,
401
483
  max_len: int,
402
- ax,
484
+ ax: Axes,
403
485
  method: str,
404
486
  day_stat: DayStatistics,
405
487
  model: ma.MaskedArray,
406
488
  obs: ma.MaskedArray,
407
489
  args: tuple,
408
- variable_info,
490
+ variable_info: PlotMeta,
491
+ *,
409
492
  title: bool = True,
410
- ):
493
+ include_xlimits: bool = False,
494
+ ) -> None:
411
495
  if method in ("error", "aerror"):
412
496
  plot_relative_error(ax, day_stat.model_stat.T, args, method)
413
497
  if title:
414
498
  ax.set_title(day_stat.title, fontsize=14)
415
- cloud_plt.set_ax(ax, 12, ylabel=None)
499
+ set_yax(ax, 12, ylabel=None)
500
+ set_xax(ax, include_xlimits=include_xlimits)
501
+
416
502
  if method == "area":
417
503
  plot_data_area(ax, day_stat, model, obs, args, title=title)
418
504
  ax.text(
@@ -423,7 +509,9 @@ def initialize_statistic_plots(
423
509
  ha="center",
424
510
  transform=ax.transAxes,
425
511
  )
426
- cloud_plt.set_ax(ax, 12, ylabel=None)
512
+ set_yax(ax, 12, ylabel=None)
513
+ set_xax(ax, include_xlimits=include_xlimits)
514
+
427
515
  if method == "hist":
428
516
  plot_histogram(ax, day_stat, variable_info)
429
517
  if j == max_len - 1 and (max_len % 2) == 0:
@@ -459,19 +547,19 @@ def initialize_statistic_plots(
459
547
  )
460
548
 
461
549
 
462
- def plot_relative_error(ax, error: ma.MaskedArray, axes: tuple, method: str):
550
+ def plot_relative_error(
551
+ ax: Axes, error: ma.MaskedArray, axes: tuple, method: str
552
+ ) -> None:
463
553
  pl = ax.pcolormesh(*axes, error[:-1, :-1].T, cmap="RdBu_r", vmin=-50, vmax=50)
464
554
  colorbar = init_colorbar(pl, ax)
465
555
  colorbar.set_label("%", fontsize=13)
466
556
  error[np.isnan(error)] = ma.masked
467
- error = ma.round(error, decimals=4)
468
557
  median_error = ma.median(error.compressed())
469
- median_error = np.round(median_error, 3)
470
558
  if method == "aerror":
471
559
  ax.text(
472
560
  0.9,
473
561
  -0.17,
474
- f"Median absolute error: {median_error} %",
562
+ f"Median absolute error: {median_error:.2f} %",
475
563
  size=12,
476
564
  ha="center",
477
565
  transform=ax.transAxes,
@@ -480,7 +568,7 @@ def plot_relative_error(ax, error: ma.MaskedArray, axes: tuple, method: str):
480
568
  ax.text(
481
569
  0.9,
482
570
  -0.17,
483
- f"Median relative error: {median_error} %",
571
+ f"Median relative error: {median_error:.2f} %",
484
572
  size=12,
485
573
  ha="center",
486
574
  transform=ax.transAxes,
@@ -488,24 +576,39 @@ def plot_relative_error(ax, error: ma.MaskedArray, axes: tuple, method: str):
488
576
 
489
577
 
490
578
  def plot_data_area(
491
- ax,
579
+ ax: Axes,
492
580
  day_stat: DayStatistics,
493
581
  model: ma.MaskedArray,
494
582
  obs: ma.MaskedArray,
495
583
  axes: tuple,
584
+ *,
496
585
  title: bool = True,
497
- ):
498
- data, cmap = p_tools.create_segment_values([model.mask, obs.mask])
499
- pl = ax.pcolormesh(*axes, data, cmap=cmap)
586
+ ) -> None:
587
+ data = p_tools.create_segment_values(model, obs)
588
+
589
+ colors = mpl.colormaps["YlGnBu"]
590
+ newcolors = colors(np.linspace(0, 1, 256))
591
+ c_map = {
592
+ 0: "white",
593
+ 1: "khaki",
594
+ 2: newcolors[90],
595
+ 3: newcolors[140],
596
+ }
597
+ unique_values = sorted(np.unique(data))
598
+ c_list = [c_map[value] for value in unique_values if value in c_map]
599
+ cmap = ListedColormap(c_list)
600
+
601
+ ax.pcolormesh(*axes, data, cmap=cmap)
602
+
500
603
  if title:
501
- colorbar = init_colorbar(pl, ax)
502
- colorbar.set_ticks(np.arange(1, 1, 3))
503
604
  ax.set_title(f"{day_stat.title}", fontsize=14)
605
+
504
606
  ax.set_facecolor("black")
607
+
505
608
  legend_elements = [
506
609
  Patch(facecolor="khaki", edgecolor="k", label="Model"),
507
- Patch(facecolor=cmap(0.5), edgecolor="k", label="Common"),
508
- Patch(facecolor=cmap(1.0), edgecolor="k", label="Observation"),
610
+ Patch(facecolor=newcolors[90], edgecolor="k", label="Common"),
611
+ Patch(facecolor=newcolors[140], edgecolor="k", label="Observation"),
509
612
  ]
510
613
  ax.legend(
511
614
  handles=legend_elements,
@@ -516,13 +619,13 @@ def plot_data_area(
516
619
  )
517
620
 
518
621
 
519
- def plot_histogram(ax, day_stat: DayStatistics, variable_info):
622
+ def plot_histogram(ax: Axes, day_stat: DayStatistics, variable_info: PlotMeta) -> None:
520
623
  weights = np.ones_like(day_stat.model_stat) / float(len(day_stat.model_stat))
521
624
  hist_bins = np.histogram(day_stat.observation_stat, density=True)[-1]
522
625
  ax.hist(
523
626
  day_stat.model_stat,
524
627
  weights=weights,
525
- bins=hist_bins,
628
+ bins=list(hist_bins),
526
629
  alpha=0.7,
527
630
  facecolor="khaki",
528
631
  edgecolor="k",
@@ -530,32 +633,35 @@ def plot_histogram(ax, day_stat: DayStatistics, variable_info):
530
633
  )
531
634
 
532
635
  weights = np.ones_like(day_stat.observation_stat) / float(
533
- len(day_stat.observation_stat)
636
+ len(day_stat.observation_stat),
534
637
  )
535
638
  ax.hist(
536
639
  day_stat.observation_stat,
537
640
  weights=weights,
538
- bins=hist_bins,
641
+ bins=list(hist_bins),
539
642
  alpha=0.7,
540
643
  facecolor="steelblue",
541
644
  edgecolor="k",
542
645
  label="Observation",
543
646
  )
544
- ax.set_xlabel(variable_info.x_title, fontsize=13)
647
+ if variable_info.x_title is not None:
648
+ ax.set_xlabel(variable_info.x_title, fontsize=13)
545
649
  if variable_info.plot_scale == "logarithmic":
546
650
  ax.ticklabel_format(axis="x", style="sci", scilimits=(0, 0))
547
651
  ax.set_ylabel("Relative frequency %", fontsize=13)
548
- ax.yaxis.grid(True, "major")
652
+ ax.yaxis.grid(which="major")
549
653
  ax.set_title(f"{day_stat.title[-1]}", fontsize=14)
550
654
 
551
655
 
552
- def plot_vertical_profile(ax, day_stat: DayStatistics, axes: tuple, variable_info):
656
+ def plot_vertical_profile(
657
+ ax: Axes,
658
+ day_stat: DayStatistics,
659
+ axes: tuple,
660
+ variable_info: PlotMeta,
661
+ ) -> None:
553
662
  mrm = p_tools.rolling_mean(day_stat.model_stat)
554
663
  orm = p_tools.rolling_mean(day_stat.observation_stat)
555
- if len(axes[-1].shape) > 1:
556
- axes = axes[-1][0]
557
- else:
558
- axes = axes[-1]
664
+ axes = axes[-1][0] if len(axes[-1].shape) > 1 else axes[-1]
559
665
  ax.plot(day_stat.model_stat, axes, "o", markersize=5.5, color="k")
560
666
  ax.plot(day_stat.observation_stat, axes, "o", markersize=5.5, color="k")
561
667
  ax.plot(
@@ -588,15 +694,16 @@ def plot_vertical_profile(ax, day_stat: DayStatistics, axes: tuple, variable_inf
588
694
  ax.plot(orm, axes, "-", color="green", lw=2, label="Mean of observation")
589
695
 
590
696
  ax.set_title(f"{day_stat.title[-1]}", fontsize=14)
591
- ax.set_xlabel(variable_info.x_title, fontsize=13)
697
+ if variable_info.x_title is not None:
698
+ ax.set_xlabel(variable_info.x_title, fontsize=13)
592
699
  if variable_info.plot_scale == "logarithmic":
593
700
  ax.ticklabel_format(axis="x", style="sci", scilimits=(0, 0))
594
- ax.yaxis.grid(True, "major")
595
- ax.xaxis.grid(True, "major")
701
+ ax.yaxis.grid(which="major")
702
+ ax.xaxis.grid(which="major")
596
703
 
597
704
 
598
- def initialize_figure(n_subplots: int, stat: str = "") -> tuple:
599
- """Set up fig and ax object, if subplot"""
705
+ def initialize_figure(n_subplots: int, stat: str = "") -> tuple[Figure, list[Axes]]:
706
+ """Set up fig and ax object, if subplot."""
600
707
  if n_subplots <= 0:
601
708
  n_subplots = 1
602
709
  fig, axes = plt.subplots(n_subplots, 1, figsize=(16, 4 + (n_subplots - 1) * 4.8))
@@ -622,7 +729,6 @@ def initialize_figure(n_subplots: int, stat: str = "") -> tuple:
622
729
  right=0.75,
623
730
  hspace=0.2,
624
731
  )
625
- axes = axes.flatten()
626
732
  if stat == "vertical" and n_subplots > 1:
627
733
  fig, axes = plt.subplots(
628
734
  int(n_subplots / 2),
@@ -639,20 +745,24 @@ def initialize_figure(n_subplots: int, stat: str = "") -> tuple:
639
745
  right=0.75,
640
746
  hspace=0.16,
641
747
  )
642
- axes = axes.flatten()
643
- if n_subplots == 1:
644
- axes = [axes]
645
- return fig, axes
748
+ axes_list = [axes] if isinstance(axes, Axes) else axes.flatten().tolist()
749
+ return fig, axes_list
646
750
 
647
751
 
648
- def init_colorbar(plot, axis):
752
+ def init_colorbar(plot: ColorizingArtist, axis: Axes) -> Colorbar:
649
753
  divider = make_axes_locatable(axis)
650
754
  cax = divider.append_axes("right", size="1%", pad=0.25)
651
755
  return plt.colorbar(plot, fraction=1.0, ax=axis, cax=cax)
652
756
 
653
757
 
654
- def _set_title(ax, field_name: str, product: str, variable_info, model_name: str = ""):
655
- """Generates subtitles for different product types"""
758
+ def _set_title(
759
+ ax: Axes,
760
+ field_name: str,
761
+ product: str,
762
+ variable_info: PlotMeta,
763
+ model_name: str = "",
764
+ ) -> None:
765
+ """Generates subtitles for different product types."""
656
766
  parts = field_name.split("_")
657
767
  if parts[0] == product:
658
768
  title = _get_product_title(variable_info)
@@ -672,14 +782,14 @@ def _set_title(ax, field_name: str, product: str, variable_info, model_name: str
672
782
  ax.set_title(f"Simulated {name}")
673
783
 
674
784
 
675
- def _get_cf_title(field_name: str, variable_info) -> str:
785
+ def _get_cf_title(field_name: str, variable_info: PlotMeta) -> str:
676
786
  title = f"{variable_info.name}, Area"
677
787
  if "V" in field_name:
678
788
  title = f"{variable_info.name}, Volume"
679
789
  return title
680
790
 
681
791
 
682
- def _get_iwc_title(field_name: str, variable_info) -> str:
792
+ def _get_iwc_title(field_name: str, variable_info: PlotMeta) -> str:
683
793
  name = variable_info.name
684
794
  if "att" in field_name:
685
795
  title = f"{name} with good attenuation"
@@ -690,12 +800,11 @@ def _get_iwc_title(field_name: str, variable_info) -> str:
690
800
  return title
691
801
 
692
802
 
693
- def _get_product_title(variable_info) -> str:
694
- title = f"{variable_info.name}"
695
- return title
803
+ def _get_product_title(variable_info: PlotMeta) -> str:
804
+ return f"{variable_info.name}"
696
805
 
697
806
 
698
- def _get_stat_titles(field_name: str, product: str, variable_info) -> str:
807
+ def _get_stat_titles(field_name: str, product: str, variable_info: PlotMeta) -> str:
699
808
  title = _get_product_title_stat(variable_info)
700
809
  if product == "cf":
701
810
  title = _get_cf_title_stat(field_name, variable_info)
@@ -707,7 +816,7 @@ def _get_stat_titles(field_name: str, product: str, variable_info) -> str:
707
816
  return title
708
817
 
709
818
 
710
- def _get_cf_title_stat(field_name: str, variable_info) -> str:
819
+ def _get_cf_title_stat(field_name: str, variable_info: PlotMeta) -> str:
711
820
  name = variable_info.name
712
821
  title = f"{name} area"
713
822
  if "V" in field_name:
@@ -715,7 +824,7 @@ def _get_cf_title_stat(field_name: str, variable_info) -> str:
715
824
  return title
716
825
 
717
826
 
718
- def _get_iwc_title_stat(field_name: str, variable_info) -> str:
827
+ def _get_iwc_title_stat(field_name: str, variable_info: PlotMeta) -> str:
719
828
  name = variable_info.name
720
829
  if "att" in field_name:
721
830
  title = f"{name} with good attenuation"
@@ -726,7 +835,108 @@ def _get_iwc_title_stat(field_name: str, variable_info) -> str:
726
835
  return title
727
836
 
728
837
 
729
- def _get_product_title_stat(variable_info) -> str:
838
+ def _get_product_title_stat(variable_info: PlotMeta) -> str:
730
839
  name = variable_info.name
731
- title = f"{name}"
732
- return title
840
+ return f"{name}"
841
+
842
+
843
+ def set_yax(ax: Axes, max_y: float, ylabel: str | None, min_y: float = 0.0) -> None:
844
+ """Sets yticks, ylim and ylabel for yaxis of axis."""
845
+ ax.set_ylim(min_y, max_y)
846
+ ax.set_ylabel("Height (km)", fontsize=13)
847
+ if ylabel is not None:
848
+ ax.set_ylabel(ylabel, fontsize=13)
849
+
850
+
851
+ def set_xax(ax: Axes, *, include_xlimits: bool = False) -> None:
852
+ """Sets xticks and xtick labels for plt.imshow()."""
853
+ ticks_x_labels = _get_standard_time_ticks(include_xlimits=include_xlimits)
854
+ ax.set_xticks(np.arange(0, 25, 4, dtype=int))
855
+ ax.set_xticklabels(ticks_x_labels, fontsize=12)
856
+ ax.set_xlim(0, 24)
857
+
858
+
859
+ def _get_standard_time_ticks(
860
+ resolution: int = 4,
861
+ *,
862
+ include_xlimits: bool = False,
863
+ ) -> list:
864
+ """Returns typical ticks / labels for a time vector between 0-24h."""
865
+ if include_xlimits:
866
+ return [
867
+ f"{int(i):02d}:00" if 24 >= i >= 0 else ""
868
+ for i in np.arange(0, 24.01, resolution)
869
+ ]
870
+ return [
871
+ f"{int(i):02d}:00" if 24 > i > 0 else ""
872
+ for i in np.arange(0, 24.01, resolution)
873
+ ]
874
+
875
+
876
+ def set_labels(fig: Figure, ax: Axes, nc_file: str, *, sub_title: bool = True) -> date:
877
+ ax.set_xlabel("Time (UTC)", fontsize=13)
878
+ case_date = read_date(nc_file)
879
+ site_name = read_location(nc_file)
880
+ if sub_title:
881
+ add_subtitle(fig, case_date, site_name)
882
+ return case_date
883
+
884
+
885
+ def read_location(nc_file: str) -> str:
886
+ """Returns site name."""
887
+ with netCDF4.Dataset(nc_file) as nc:
888
+ return nc.location
889
+
890
+
891
+ def read_date(nc_file: str) -> date:
892
+ """Returns measurement date."""
893
+ with netCDF4.Dataset(nc_file) as nc:
894
+ return date(int(nc.year), int(nc.month), int(nc.day))
895
+
896
+
897
+ def add_subtitle(fig: Figure, case_date: date, site_name: str) -> None:
898
+ """Adds subtitle into figure."""
899
+ text = _get_subtitle_text(case_date, site_name)
900
+ fig.suptitle(
901
+ text,
902
+ fontsize=13,
903
+ y=0.885,
904
+ x=0.07,
905
+ horizontalalignment="left",
906
+ verticalalignment="bottom",
907
+ )
908
+
909
+
910
+ def _get_subtitle_text(case_date: date, site_name: str) -> str:
911
+ site_name = site_name.replace("-", " ")
912
+ return f"{site_name}, {case_date.strftime('%d %b %Y').lstrip('0')}"
913
+
914
+
915
+ def _create_save_name(
916
+ save_path: str,
917
+ case_date: date,
918
+ field_names: list,
919
+ fix: str = "",
920
+ ) -> str:
921
+ """Creates file name for saved images."""
922
+ date_string = case_date.strftime("%Y%m%d")
923
+ return f"{save_path}{date_string}_{'_'.join(field_names)}{fix}.png"
924
+
925
+
926
+ def handle_saving(
927
+ image_name: str | None,
928
+ save_path: str | None,
929
+ case_date: date,
930
+ field_names: list,
931
+ fix: str = "",
932
+ *,
933
+ show: bool = False,
934
+ ) -> None:
935
+ if image_name:
936
+ plt.savefig(image_name, bbox_inches="tight")
937
+ elif save_path:
938
+ file_name = _create_save_name(save_path, case_date, field_names, fix)
939
+ plt.savefig(file_name, bbox_inches="tight")
940
+ if show:
941
+ plt.show()
942
+ plt.close()