| |
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.') |