|
1 from typing import List, Any |
|
2 |
|
3 import xarray as xr |
|
4 from config import * |
|
5 import plotly.graph_objects as go |
|
6 from plotly.subplots import make_subplots |
|
7 import netCDF4 as nc |
|
8 from datetime import datetime |
|
9 import numpy as np |
|
10 import sys |
|
11 from os.path import exists |
|
12 |
|
13 |
|
14 def read_variable_in_group(nc_file_path, group_name, variable_name): |
|
15 try: |
|
16 with nc.Dataset(nc_file_path, 'r') as dataset: |
|
17 # Find the group |
|
18 group = dataset.groups[group_name] |
|
19 |
|
20 # Access the variable |
|
21 try: |
|
22 variable = group.variables[variable_name] |
|
23 dimensions = variable.dimensions |
|
24 # print("Dimensions:", dimensions) |
|
25 # variable_metadata = { variable.name, variable.long_name, variable.units, variable.ndim} |
|
26 |
|
27 # variable_metadata.add = variable.long_name |
|
28 variable_metadata = \ |
|
29 {'name': variable.name.capitalize(), |
|
30 'long_name': variable.long_name.capitalize(), |
|
31 'units': variable.units, |
|
32 'ndim': variable.ndim} |
|
33 |
|
34 # Create an index array for slicing the variable |
|
35 index_array = [slice(None)] * variable.ndim |
|
36 # Access the variable data using the index array |
|
37 variable_data = variable[index_array] |
|
38 |
|
39 # Fetch the dimension values |
|
40 dimension_values = {} |
|
41 for dim_name in dimensions: |
|
42 dim_values = group.variables[dim_name][:] |
|
43 dimension_values[dim_name] = dim_values |
|
44 # Adjust the index array for slicing |
|
45 index_array[variable.dimensions.index(dim_name)] = 0 |
|
46 |
|
47 # Now you have the variable data and its associated dimensions in a dictionary |
|
48 result = {'data': variable_data, 'dimensions': dimension_values, 'metadata': variable_metadata} |
|
49 |
|
50 # Print or use the result as needed |
|
51 # print(result) |
|
52 return result |
|
53 |
|
54 except KeyError: |
|
55 print("The specified variable does not exist in the group.") |
|
56 |
|
57 except FileNotFoundError: |
|
58 print(f"The file '{nc_file_path}' does not exist.") |
|
59 except Exception as e: |
|
60 print(f"The group {e} does not exist.") |
|
61 |
|
62 |
|
63 def read_variable_in_group2(nc_file_path, group_name, variable_name): |
|
64 try: |
|
65 with nc.Dataset(nc_file_path, 'r') as dataset: |
|
66 # Find the group |
|
67 group = dataset.groups[group_name] |
|
68 |
|
69 # Access the variable |
|
70 try: |
|
71 variable = group.variables[variable_name] |
|
72 dimensions = variable.dimensions |
|
73 # print("Dimensions:", dimensions) |
|
74 # variable_metadata = { variable.name, variable.long_name, variable.units, variable.ndim} |
|
75 |
|
76 # variable_metadata.add = variable.long_name |
|
77 variable_metadata = \ |
|
78 {'name': variable.name.capitalize(), |
|
79 'long_name': variable.long_name.capitalize(), |
|
80 'units': variable.units, |
|
81 'ndim': variable.ndim} |
|
82 |
|
83 # Create an index array for slicing the variable |
|
84 index_array = [slice(None)] * variable.ndim |
|
85 # Access the variable data using the index array |
|
86 variable_data = variable[index_array] |
|
87 |
|
88 # Fetch the dimension values |
|
89 dimension_values = {} |
|
90 for dim_name in dimensions: |
|
91 dim_values = group.variables[dim_name][:] |
|
92 dimension_values[dim_name] = dim_values |
|
93 # Adjust the index array for slicing |
|
94 index_array[variable.dimensions.index(dim_name)] = 0 |
|
95 |
|
96 # Now you have the variable data and its associated dimensions in a dictionary |
|
97 # result = {'data': variable_data, 'dimensions': dimension_values, 'metadata': variable_metadata} |
|
98 |
|
99 # Print or use the result as needed |
|
100 # print(result) |
|
101 return variable_data, dimension_values, variable_metadata |
|
102 |
|
103 except KeyError: |
|
104 print(f"The variable {variable_name} does not exist in the {group_name} group.") |
|
105 except FileNotFoundError: |
|
106 print(f"The file {nc_file_path} does not exist.") |
|
107 except Exception as e: |
|
108 print(f"The group {e} does not exist.") |
|
109 |
|
110 |
|
111 def plot_vertical_profiles_plotly(variables, data, dimensions, y_array, positive_error_array, negative_error_array, |
|
112 xaxis_label, yaxis_label, title, timeinrows): |
|
113 """ |
|
114 ToDo Complete |
|
115 Plot vertical profiles using plotly |
|
116 |
|
117 Parameters: |
|
118 - |
|
119 |
|
120 Returns: |
|
121 - fig: html |
|
122 """ |
|
123 |
|
124 # Set number of columns, number of rows, and subtitles if needed |
|
125 ncols = len(variables) |
|
126 subtitles = None |
|
127 if timeinrows is True: |
|
128 nrows = len(dimensions['time']) |
|
129 if nrows > 1: |
|
130 for t in range(nrows): |
|
131 for v in range(ncols): |
|
132 if subtitles is None: |
|
133 subtitles = [str(datetime.fromtimestamp(dimensions['time'][t]))] |
|
134 else: |
|
135 subtitles += [str(datetime.fromtimestamp(dimensions['time'][t]))] |
|
136 else: |
|
137 nrows = 1 |
|
138 subtitles = '' |
|
139 |
|
140 # Create figure |
|
141 fig = make_subplots(rows=nrows, cols=ncols, |
|
142 subplot_titles=subtitles, |
|
143 shared_yaxes=True) |
|
144 |
|
145 # Define colours |
|
146 clrs = [['rgb(0,35,255)', 'rgb(0,20,150)', 'rgb(0,170,250)'], # UV |
|
147 ['rgb(90,210,50)', 'rgb(40,90,25)', 'rgb(120,250,80)'], # VIS |
|
148 ['rgb(250,30,30)', 'rgb(200,25,30)', 'rgb(250,125,0)']] # IR |
|
149 clrs = np.array(clrs) |
|
150 |
|
151 # Plot |
|
152 for v in range(len(variables)): |
|
153 for t in range(len(dimensions['time'])): |
|
154 for w in range(len(dimensions['wavelength'])): |
|
155 tn = str(datetime.fromtimestamp(dimensions['time'][t])) |
|
156 wn = str(dimensions['wavelength'][w]) |
|
157 if timeinrows == True: |
|
158 lgnd = wn + ' nm' |
|
159 rown = t + 1 |
|
160 colorint = 1 |
|
161 colorind = 0 |
|
162 subtitle = str(datetime.fromtimestamp(dimensions['time'][t])) |
|
163 else: |
|
164 lgnd = wn + ' nm - ' + tn |
|
165 rown = 1 |
|
166 colorind = t |
|
167 colorint = 1 - (0.4 * t) |
|
168 |
|
169 # colour definition |
|
170 if abs(dimensions['wavelength'][w] - 1064) < 1: |
|
171 clr = clrs[2, colorind] |
|
172 # clr = 'rgb(235,70,20)' # 'rgba(215,27,96,0.4)' |
|
173 elif abs(dimensions['wavelength'][w] - 532) < 1: |
|
174 clr = clrs[1, colorind] |
|
175 elif abs(dimensions['wavelength'][w] - 355) < 1: |
|
176 clr = clrs[0, colorind] |
|
177 else: |
|
178 clr = 'rgb(0,0,0)' |
|
179 |
|
180 data_values = data[variables[v]][w, t, :] |
|
181 error_values = positive_error_array[variables[v]][w, t, :] |
|
182 vertical_levels = y_array["data"][t, :] |
|
183 |
|
184 # Add trace |
|
185 fig.add_trace(go.Scatter( |
|
186 x=data_values, |
|
187 y=vertical_levels, |
|
188 error_x=dict(array=error_values, width=0, thickness=0.1), |
|
189 # ToDo Include distinction between positive and negative errors |
|
190 mode='lines', |
|
191 line=dict(width=2, color=clr), |
|
192 name=lgnd), |
|
193 row=rown, col=v + 1, |
|
194 ) |
|
195 |
|
196 # ToDo Check formatting options (showlegend, mapbox) |
|
197 fig.update_xaxes({"title": xaxis_label[v]}, row=rown, col=v + 1) |
|
198 fig.update_xaxes({"mirror": True, "ticks": 'outside', "showline": True, "color": "black"}, row=rown, |
|
199 col=v + 1) |
|
200 |
|
201 fig.update_xaxes(ticks="outside") |
|
202 fig.update_yaxes(ticks="outside", row=rown, col=v + 1) # ToDo check tickformat (i.e. tickformat=":.2e") |
|
203 if v == 0: |
|
204 fig.update_yaxes({"title": yaxis_label}, row=rown, col=v + 1) |
|
205 |
|
206 # Set axis labels and title |
|
207 fig.update_layout( |
|
208 yaxis_title=yaxis_label, |
|
209 title=title, |
|
210 template="seaborn", # You can change the template as per your preference |
|
211 # ['ggplot2', 'seaborn', 'simple_white', 'plotly', 'plotly_white', 'plotly_dark', 'presentation', |
|
212 # 'xgridoff', 'ygridoff', 'gridon', 'none'] |
|
213 height=max(600, 450 * rown), width=min(max(400 * len(variables), 750), 1400), |
|
214 ) |
|
215 |
|
216 return fig |
|
217 |
|
218 |
|
219 # Files for testing |
|
220 # netcdf_file = 'hpb_012_0000598_201810172100_201810172300_20181017oh00_ELDAmwl_v0.0.1.nc' # Backscatter and extinction, no error |
|
221 # netcdf_file = 'hpb_012_0000627_202308240200_202308240400_20230824hpb0200_ELDAmwl_v0.0.1.nc' # Extinction with error |
|
222 |
|
223 |
|
224 try: |
|
225 netcdf_file = sys.argv[1] |
|
226 except: |
|
227 print('Syntax is incorrect, you need to pass the filename of the file to be plotted.\n' |
|
228 'Example: ./eldamwl_plot.py hpb_012_0000598_201810172100_201810172300_20181017oh00_ELDAmwl_v0.0.1.nc') |
|
229 exit() |
|
230 |
|
231 netcdf_file_path = netcdf_path + netcdf_file |
|
232 |
|
233 file_exists = exists(netcdf_file_path) |
|
234 if not file_exists: |
|
235 print('The file that you provided does not exist.') |
|
236 exit() |
|
237 |
|
238 # Groups of data in the NetCDF file (low and high resolution) |
|
239 groups = ['lowres_products', 'highres_products'] |
|
240 groups_names = ['Low resolution', 'High resolution'] |
|
241 |
|
242 # Variables that we want to plot |
|
243 variable = ['backscatter', 'extinction', 'lidarratio', 'volumedepolarization', 'particledepolarization'] # ToDo Test particledepolarization when available |
|
244 # Errors associated to the variables |
|
245 variable_error = ['error_backscatter', 'error_extinction', 'error_lidarratio', |
|
246 'positive_systematic_error_volumedepolarization', 'error_particledepolarization'] |
|
247 variable_negative_error_volumedepol = 'negative_systematic_error_volumedepolarization' |
|
248 # Other variables: |
|
249 # cloud_mask (time, level) |
|
250 |
|
251 |
|
252 for g in range(len(groups)): |
|
253 # Read the altitude variable |
|
254 altitude = read_variable_in_group(netcdf_file_path, groups[g], 'altitude') |
|
255 |
|
256 if altitude['metadata']['units'] == 'm': |
|
257 altitude['data'] = altitude['data'] / 1000.0 |
|
258 altitude['metadata']['units'] = 'km' |
|
259 |
|
260 # Initialize variables |
|
261 data = None |
|
262 variables_with_data = None |
|
263 variables_axis = None |
|
264 |
|
265 # Read the data |
|
266 for v in range(len(variable)): |
|
267 # Initialize var |
|
268 var = None |
|
269 |
|
270 try: |
|
271 var, dim, met = read_variable_in_group2(netcdf_file_path, groups[g], variable[v]) |
|
272 except TypeError: |
|
273 print(f"The variable {variable[v]} is not present. Cannot unpack non-iterable NoneType object") |
|
274 |
|
275 if var is not None: # If the variable that we're trying to read exists |
|
276 if var.max() != var.min(): # If there is data in the array |
|
277 if variables_with_data is None: |
|
278 variables_with_data = [variable[v]] |
|
279 variables_axis = [met['long_name'] + ' [ ' + met['units'] + ' ]'] |
|
280 else: |
|
281 variables_with_data += [variable[v]] |
|
282 variables_axis += [met['long_name'] + ' [ ' + met['units'] + ' ]'] |
|
283 |
|
284 var_error, dim_error, met_error = read_variable_in_group2(netcdf_file_path, groups[g], |
|
285 variable_error[v]) |
|
286 if variable[v] == 'volumedepolarization': |
|
287 var_error_negative, dim, met = read_variable_in_group2(netcdf_file_path, groups[g], |
|
288 variable_negative_error_volumedepol) |
|
289 else: |
|
290 var_error_negative = var_error |
|
291 |
|
292 # Add read data to the data variable to have it all grouped |
|
293 if data is None: |
|
294 # Start populating data and data_error |
|
295 data = {variable[v]: var} |
|
296 data_error_positive = {variable[v]: var_error} |
|
297 data_error_negative = {variable[v]: var_error_negative} |
|
298 else: |
|
299 # Add more data to data and data_error |
|
300 data[variable[v]] = var |
|
301 data_error_positive[variable[v]] = var_error |
|
302 data_error_negative[variable[v]] = var_error_negative |
|
303 else: |
|
304 print(f'There is no data for {variable[v]} in the {groups_names[g]} group.') |
|
305 |
|
306 dimensions = dim |
|
307 |
|
308 # Ask user if he/she wants to plot different temporal profiles in different rows |
|
309 if g == 0: |
|
310 # Initialize |
|
311 timeinplots = False |
|
312 timeinrows = False |
|
313 # Ask user how he/she wants the plot |
|
314 if len(dimensions['time']) > 1: |
|
315 answer = input( |
|
316 f"There are {len(dimensions['time'])} temporal profiles, do you want to plot them on different plots [y/n]? ") |
|
317 if answer[0:1].lower() == 'y': |
|
318 timeinplots = True |
|
319 if timeinplots == False: |
|
320 answer = input( |
|
321 f"There are {len(dimensions['time'])} temporal profiles, do you want to plot them on different rows [y/n]? ") |
|
322 if answer[0:1].lower() == 'y': |
|
323 timeinrows = True |
|
324 |
|
325 if variables_with_data is not None: |
|
326 timeiter = dimensions['time'] |
|
327 if timeinplots == True: |
|
328 dimensions_t = dimensions |
|
329 for t in range(len(dimensions['time'])): |
|
330 dimensions_t['time'] = [timeiter[t]] |
|
331 for vr in range(len(variables_with_data)): |
|
332 if vr == 0: |
|
333 data_t = {variables_with_data[vr]: data[variables_with_data[vr]][:, t:t + 1, :]} |
|
334 else: |
|
335 data_t[variables_with_data[vr]] = data[variables_with_data[vr]][:, t:t + 1, :] |
|
336 |
|
337 fig = plot_vertical_profiles_plotly( |
|
338 variables=variables_with_data, |
|
339 data=data_t, |
|
340 dimensions=dimensions_t, |
|
341 y_array=altitude, |
|
342 positive_error_array=data_error_positive, |
|
343 negative_error_array=data_error_negative, |
|
344 xaxis_label=variables_axis, |
|
345 yaxis_label=altitude['metadata']['long_name'] + ' [' + altitude['metadata']['units'] + ']', |
|
346 title=netcdf_file[0:3].upper() + " - " + str( |
|
347 datetime.fromtimestamp(dimensions_t['time'][0]).strftime( |
|
348 '%d/%m/%Y %H:%M:%S')) + " - ELDA MWL " + |
|
349 groups_names[g], |
|
350 timeinrows=True |
|
351 ) |
|
352 |
|
353 # Show the plot (in Jupyter Notebook or Jupyter Lab) |
|
354 fig.show() |
|
355 |
|
356 # Save the plot |
|
357 filename = "ELDAmwl_" + netcdf_file[0:3].upper() + "_" + datetime.fromtimestamp( |
|
358 dimensions['time'][0]).strftime('%Y%m%d-%H%M%S') + "_" + groups[g] + ".html" |
|
359 fig.write_html(html_path + filename) |
|
360 |
|
361 else: |
|
362 # Plotting in one plot (even if there are different time profiles) |
|
363 fig = plot_vertical_profiles_plotly( |
|
364 variables_with_data, |
|
365 data, |
|
366 dimensions, |
|
367 altitude, |
|
368 data_error_positive, |
|
369 data_error_negative, |
|
370 xaxis_label=variables_axis, |
|
371 yaxis_label=altitude['metadata']['long_name'] + ' [' + altitude['metadata']['units'] + ']', |
|
372 title=netcdf_file[0:3].upper() + " - " + str( |
|
373 datetime.fromtimestamp(dimensions['time'][0]).strftime('%d/%m/%Y')) + " - ELDA MWL " + groups_names[ |
|
374 g], |
|
375 timeinrows=timeinrows |
|
376 ) |
|
377 |
|
378 # Show the plot (in Jupyter Notebook or Jupyter Lab) |
|
379 fig.show() |
|
380 |
|
381 # Save the plot |
|
382 if timeinrows == True: |
|
383 filename = "ELDAmwl_" + netcdf_file[0:3].upper() + "_" + datetime.fromtimestamp( |
|
384 dimensions['time'][0]).strftime('%Y%m%d%H%M%S') + '-' + datetime.fromtimestamp( |
|
385 dimensions['time'][-1]).strftime('%Y%m%d%H%M%S') + "_" + groups[g] + "_timeinrows.html" |
|
386 else: |
|
387 filename = "ELDAmwl_" + netcdf_file[0:3].upper() + "_" + datetime.fromtimestamp( |
|
388 dimensions['time'][0]).strftime('%Y%m%d%H%M%S') + '-' + datetime.fromtimestamp( |
|
389 dimensions['time'][-1]).strftime('%Y%m%d%H%M%S') + "_" + groups[g] + ".html" |
|
390 |
|
391 fig.write_html(html_path + filename) |
|
392 |
|
393 else: |
|
394 print(f'There is no data to be plotted in the {groups_names[g]} group.') |