92 return raw_info |
92 return raw_info |
93 |
93 |
94 def _assign_properties(self): |
94 def _assign_properties(self): |
95 """ Assign scanning-specific parameters found in the header as object properties.""" |
95 """ Assign scanning-specific parameters found in the header as object properties.""" |
96 super(ScanningFile, self)._assign_properties() |
96 super(ScanningFile, self)._assign_properties() |
97 self.azimuth_angle = float(self.raw_info['azimuth_angle']) |
97 self.azimuth_angle_raw = float(self.raw_info['azimuth_angle']) |
98 self.temperature = float(self.raw_info['temperature']) |
98 self.temperature = float(self.raw_info['temperature']) |
99 self.pressure = float(self.raw_info['pressure']) |
99 self.pressure = float(self.raw_info['pressure']) |
100 self.azimuth_start_raw = float(self.raw_info['azimuth_start']) |
100 self.azimuth_start_raw = float(self.raw_info['azimuth_start']) |
101 self.azimuth_stop_raw = float(self.raw_info['azimuth_stop']) |
101 self.azimuth_stop_raw = float(self.raw_info['azimuth_stop']) |
102 self.azimuth_step = float(self.raw_info['azimuth_step']) |
102 self.azimuth_step = float(self.raw_info['azimuth_step']) |
103 self.zenith_start_raw = float(self.raw_info['zenith_start']) |
103 self.zenith_start_raw = float(self.raw_info['zenith_start']) |
104 self.zenith_stop_raw = float(self.raw_info['zenith_stop']) |
104 self.zenith_stop_raw = float(self.raw_info['zenith_stop']) |
105 self.zenith_step = float(self.raw_info['zenith_step']) |
105 self.zenith_step = float(self.raw_info['zenith_step']) |
106 self.azimuth_offset = float(self.raw_info['azimuth_offset']) |
106 self.azimuth_offset = float(self.raw_info['azimuth_offset']) |
107 |
107 |
108 logger.warning("Azimuth offset correction not applied.") |
108 self.azimuth_angle = (self.azimuth_angle_raw + self.azimuth_offset) % 360 |
109 # TODO: Apply azimuth offset correction. |
109 self.azimuth_start = (self.azimuth_start_raw + self.azimuth_offset) % 360 |
110 self.azimuth_start = self.azimuth_start_raw |
110 self.azimuth_stop = (self.azimuth_stop_raw + self.azimuth_offset) % 360 |
111 self.azimuth_stop = self.azimuth_stop_raw |
|
112 |
111 |
113 if self.fix_zenith_angle: |
112 if self.fix_zenith_angle: |
114 logger.debug('Fixing zenith start and zenith stop angles.') |
113 logger.debug('Fixing zenith start and zenith stop angles.') |
115 self.zenith_start = self._correct_zenith_angle(self.zenith_start_raw) |
114 self.zenith_start = self._correct_zenith_angle(self.zenith_start_raw) |
116 self.zenith_stop = self._correct_zenith_angle(self.zenith_stop_raw) |
115 self.zenith_stop = self._correct_zenith_angle(self.zenith_stop_raw) |
132 self.zenith_stop = None |
131 self.zenith_stop = None |
133 self.zenith_step = None |
132 self.zenith_step = None |
134 self.azimuth_offset = None |
133 self.azimuth_offset = None |
135 self.zenith_angles = [] |
134 self.zenith_angles = [] |
136 self.azimuth_angles = [] |
135 self.azimuth_angles = [] |
|
136 self.temperature = [] |
|
137 self.pressure = [] |
137 |
138 |
138 def append_file(self, current_file, file_channel): |
139 def append_file(self, current_file, file_channel): |
139 """ Keep track of scanning-specific variable properties of each file. """ |
140 """ Keep track of scanning-specific variable properties of each file. """ |
140 super(ScanningChannel, self).append_file(current_file, file_channel) |
141 super(ScanningChannel, self).append_file(current_file, file_channel) |
141 self.zenith_angles.append(current_file.zenith_angle) |
142 self.zenith_angles.append(current_file.zenith_angle) |
142 self.azimuth_angles.append(current_file.azimuth_angle) |
143 self.azimuth_angles.append(current_file.azimuth_angle) |
|
144 self.temperature.append(current_file.temperature) |
|
145 self.pressure.append(current_file.pressure) |
143 |
146 |
144 def _assign_properties(self, current_file, file_channel): |
147 def _assign_properties(self, current_file, file_channel): |
145 """ Assign scanning-specific properties as object properties. Check that these are unique, |
148 """ Assign scanning-specific properties as object properties. Check that these are unique, |
146 i.e. that all files belong to the same measurements set. |
149 i.e. that all files belong to the same measurements set. |
147 |
150 |
153 A specific LicelChannelData object holding data found in the file. |
156 A specific LicelChannelData object holding data found in the file. |
154 """ |
157 """ |
155 super(ScanningChannel, self)._assign_properties(current_file, file_channel) |
158 super(ScanningChannel, self)._assign_properties(current_file, file_channel) |
156 self._assign_unique_property('azimuth_start', current_file.azimuth_start) |
159 self._assign_unique_property('azimuth_start', current_file.azimuth_start) |
157 self._assign_unique_property('azimuth_stop', current_file.azimuth_stop) |
160 self._assign_unique_property('azimuth_stop', current_file.azimuth_stop) |
|
161 self._assign_unique_property('azimuth_start_raw', current_file.azimuth_start_raw) |
|
162 self._assign_unique_property('azimuth_stop_raw', current_file.azimuth_stop_raw) |
158 self._assign_unique_property('azimuth_step', current_file.azimuth_step) |
163 self._assign_unique_property('azimuth_step', current_file.azimuth_step) |
159 self._assign_unique_property('zenith_start', current_file.zenith_start) |
164 self._assign_unique_property('zenith_start', current_file.zenith_start) |
160 self._assign_unique_property('zenith_stop', current_file.zenith_stop) |
165 self._assign_unique_property('zenith_stop', current_file.zenith_stop) |
|
166 self._assign_unique_property('zenith_start_raw', current_file.zenith_start_raw) |
|
167 self._assign_unique_property('zenith_stop_raw', current_file.zenith_stop_raw) |
161 self._assign_unique_property('zenith_step', current_file.zenith_step) |
168 self._assign_unique_property('zenith_step', current_file.zenith_step) |
162 |
169 |
163 def plot_ppi(self, figsize=(8, 4), signal_type='rc', z_min=0., z_max=12000., show_plot=True, |
170 def plot_ppi(self, figsize=(8, 4), signal_type='rc', z_min=0., z_max=12000., show_plot=True, |
164 cmap=plt.cm.jet, title=None, vmin=0, vmax=1.3 * 10 ** 7, mask_noise=True, noise_threshold=1.): |
171 cmap=plt.cm.jet, title=None, vmin=0, vmax=1.3 * 10 ** 7, mask_noise=True, noise_threshold=1.): |
165 """ |
172 """ |
239 Threshold to use in the noise masking routine. |
246 Threshold to use in the noise masking routine. |
240 """ |
247 """ |
241 fig = plt.figure(figsize=figsize) |
248 fig = plt.figure(figsize=figsize) |
242 ax1 = fig.add_subplot(111) |
249 ax1 = fig.add_subplot(111) |
243 |
250 |
244 self.draw_rhi(ax1, cmap=cmap, signal_type=signal_type, z_min=z_min, z_max=z_max, vmin=vmin, vmax=vmax, |
251 projection_angle = self.draw_rhi(ax1, cmap=cmap, signal_type=signal_type, z_min=z_min, z_max=z_max, vmin=vmin, vmax=vmax, |
245 mask_noise=mask_noise, noise_threshold=noise_threshold) |
252 mask_noise=mask_noise, noise_threshold=noise_threshold) |
246 |
253 |
247 if title: |
254 if title: |
248 ax1.set_title(title) |
255 ax1.set_title(title) |
249 else: |
256 else: |
250 ax1.set_title("RHI scan") |
257 ax1.set_title("RHI scan ({0}$^\circ$)".format(projection_angle)) |
251 |
258 |
252 if show_plot: |
259 if show_plot: |
253 plt.show() |
260 plt.show() |
254 |
261 |
|
262 def plot_scan(self, figsize=(8, 4), signal_type='rc', z_min=0., z_max=12000., show_plot=True, |
|
263 cmap=plt.cm.jet, vmin=0, vmax=1.3 * 10 ** 7, mask_noise=True, noise_threshold=1., cb_format='%.0e', box=False): |
|
264 """ |
|
265 Plot data as RHI and PPI scans. |
|
266 |
|
267 Parameters |
|
268 ---------- |
|
269 figsize : tuple |
|
270 (width, height) of the output figure (inches) |
|
271 signal_type : str |
|
272 If 'rc', the range corrected signal is ploted. Else, the raw signals are used. |
|
273 z_min : float |
|
274 Minimum z range |
|
275 z_max : float |
|
276 Maximum z range |
|
277 show_plot : bool |
|
278 If True, the show_plot command is run. |
|
279 cmap : cmap |
|
280 An instance of a matplotlib colormap to use. |
|
281 z0 : float |
|
282 The ground-level altitude. If provided the plot shows altitude above sea level. |
|
283 title : str |
|
284 Optional title for the plot. |
|
285 vmin : float |
|
286 Minimum value for the color scale. |
|
287 vmax : float |
|
288 Maximum value for the color scale. |
|
289 mask_noise : bool |
|
290 If True, remove noisy bins. |
|
291 noise_threshold : int |
|
292 Threshold to use in the noise masking routine. |
|
293 """ |
|
294 fig = plt.figure(figsize=figsize) |
|
295 ax1 = fig.add_subplot(121) |
|
296 ax2 = fig.add_subplot(122) |
|
297 |
|
298 self.draw_ppi(ax1, cmap=cmap, signal_type=signal_type, z_min=z_min, z_max=z_max, vmin=vmin, vmax=vmax, |
|
299 mask_noise=mask_noise, noise_threshold=noise_threshold, add_colorbar=False, cb_format=cb_format, |
|
300 box=box) |
|
301 |
|
302 projection_angle = self.draw_rhi(ax2, cmap=cmap, signal_type=signal_type, z_min=z_min, z_max=z_max, vmin=vmin, vmax=vmax, |
|
303 mask_noise=mask_noise, noise_threshold=noise_threshold, cb_format=cb_format, box=box) |
|
304 |
|
305 |
|
306 fig.suptitle("Channel {0}: {1} - {2}".format(self.name, |
|
307 self.start_time.strftime('%Y%m%dT%H%M'), |
|
308 self.stop_time.strftime('%Y%m%dT%H%M'))) |
|
309 |
|
310 ax1.set_title('PPI') |
|
311 ax2.set_title("RHI ({0:.1f}$^\circ$)".format(projection_angle)) |
|
312 |
|
313 plt.tight_layout() |
|
314 plt.subplots_adjust(top=0.85) |
|
315 |
|
316 if show_plot: |
|
317 plt.show() |
|
318 |
|
319 |
255 def draw_ppi(self, ax1, cmap=plt.cm.jet, signal_type='rc', |
320 def draw_ppi(self, ax1, cmap=plt.cm.jet, signal_type='rc', |
256 z_min=0, z_max=12000., add_colorbar=True, cmap_label='a.u.', cb_format=None, |
321 z_min=0, z_max=12000., add_colorbar=True, cmap_label='a.u.', cb_format=None, |
257 vmin=0, vmax=1.3 * 10 ** 7, mask_noise=True, noise_threshold=1., first_signal_bin=0): |
322 vmin=0, vmax=1.3 * 10 ** 7, mask_noise=True, noise_threshold=1., first_signal_bin=0, box=False): |
258 """ |
323 """ |
259 Draw channel data as a PPI plot. |
324 Draw channel data as a PPI plot. |
260 |
325 |
261 Parameters |
326 Parameters |
262 ---------- |
327 ---------- |
289 noise_threshold : int |
354 noise_threshold : int |
290 Threshold to use in the noise masking routine. |
355 Threshold to use in the noise masking routine. |
291 first_signal_bin : int |
356 first_signal_bin : int |
292 First signal bin. Can be used to fix analog bin shift of Licel channels. |
357 First signal bin. Can be used to fix analog bin shift of Licel channels. |
293 """ |
358 """ |
294 x, y = self._polar_to_ground(self.z, self.azimuth_angles, self.zenith_angles) |
359 x, y = self._polar_to_ground(self.z/1000., self.azimuth_angles, self.zenith_angles) |
295 |
360 |
296 self.draw_projection(ax1, x, y, cmap=cmap, signal_type=signal_type, |
361 self.draw_projection(ax1, x, y, cmap=cmap, signal_type=signal_type, |
297 z_min=z_min, z_max=z_max, add_colorbar=add_colorbar, cmap_label=cmap_label, |
362 z_min=z_min, z_max=z_max, add_colorbar=add_colorbar, cmap_label=cmap_label, |
298 cb_format=cb_format, vmin=vmin, vmax=vmax, mask_noise=mask_noise, |
363 cb_format=cb_format, vmin=vmin, vmax=vmax, mask_noise=mask_noise, |
299 noise_threshold=noise_threshold, first_signal_bin=first_signal_bin) |
364 noise_threshold=noise_threshold, first_signal_bin=first_signal_bin) |
300 |
365 |
|
366 if box: |
|
367 ax1.set_xlim(-z_max / 1000., z_max / 1000.) |
|
368 ax1.set_ylim(-z_max / 1000., z_max / 1000.) |
|
369 |
|
370 ax1.set_ylabel('South-North (km)') |
|
371 ax1.set_xlabel('West-East (km)') |
|
372 |
301 def draw_rhi(self, ax1, cmap=plt.cm.jet, signal_type='rc', |
373 def draw_rhi(self, ax1, cmap=plt.cm.jet, signal_type='rc', |
302 z_min=0, z_max=12000., add_colorbar=True, cmap_label='a.u.', cb_format=None, |
374 z_min=0, z_max=12000., add_colorbar=True, cmap_label='a.u.', cb_format=None, |
303 vmin=0, vmax=1.3 * 10 ** 7, mask_noise=True, noise_threshold=1., first_signal_bin=0): |
375 vmin=0, vmax=1.3 * 10 ** 7, mask_noise=True, noise_threshold=1., first_signal_bin=0, box=False): |
304 """ |
376 """ |
305 Draw channel data as a PPI plot. |
377 Draw channel data as a PPI plot. |
306 |
378 |
307 Parameters |
379 Parameters |
308 ---------- |
380 ---------- |
335 noise_threshold : int |
407 noise_threshold : int |
336 Threshold to use in the noise masking routine. |
408 Threshold to use in the noise masking routine. |
337 first_signal_bin : int |
409 first_signal_bin : int |
338 First signal bin. Can be used to fix analog bin shift of Licel channels. |
410 First signal bin. Can be used to fix analog bin shift of Licel channels. |
339 """ |
411 """ |
340 x, y = self._polar_to_cross_section(self.z, self.azimuth_angles, self.zenith_angles, np.mean(self.azimuth_angles)) |
412 projection_angle = np.mean(self.azimuth_angles) |
|
413 x, y = self._polar_to_cross_section(self.z / 1000., self.azimuth_angles, self.zenith_angles, projection_angle) |
341 |
414 |
342 self.draw_projection(ax1, x, y, cmap=cmap, signal_type=signal_type, |
415 self.draw_projection(ax1, x, y, cmap=cmap, signal_type=signal_type, |
343 z_min=z_min, z_max=z_max, add_colorbar=add_colorbar, cmap_label=cmap_label, |
416 z_min=z_min, z_max=z_max, add_colorbar=add_colorbar, cmap_label=cmap_label, |
344 cb_format=cb_format, vmin=vmin, vmax=vmax, mask_noise=mask_noise, |
417 cb_format=cb_format, vmin=vmin, vmax=vmax, mask_noise=mask_noise, |
345 noise_threshold=noise_threshold, first_signal_bin=first_signal_bin) |
418 noise_threshold=noise_threshold, first_signal_bin=first_signal_bin) |
|
419 |
|
420 padding = 0.5 # km |
|
421 |
|
422 if box: |
|
423 ax1.set_xlim(-z_max / 1000. - padding, z_max / 1000. + padding) |
|
424 ax1.set_ylim(-padding, z_max / 1000. + padding) |
|
425 |
|
426 ax1.set_xlabel('Distance (km)') |
|
427 ax1.set_ylabel('Height a.l. (km)') |
|
428 |
|
429 return projection_angle |
346 |
430 |
347 def draw_projection(self, ax1, x, y, cmap=plt.cm.jet, signal_type='rc', |
431 def draw_projection(self, ax1, x, y, cmap=plt.cm.jet, signal_type='rc', |
348 z_min=0, z_max=12000., add_colorbar=True, cmap_label='a.u.', cb_format=None, |
432 z_min=0, z_max=12000., add_colorbar=True, cmap_label='a.u.', cb_format=None, |
349 vmin=0, vmax=1.3 * 10 ** 7, mask_noise=True, noise_threshold=1., |
433 vmin=0, vmax=1.3 * 10 ** 7, mask_noise=True, noise_threshold=1., |
350 first_signal_bin=0): |
434 first_signal_bin=0): |
402 data_max_idx = z_max_idx + first_signal_bin |
486 data_max_idx = z_max_idx + first_signal_bin |
403 |
487 |
404 im1 = ax1.pcolormesh(x[:, z_min_idx:z_max_idx], y[:, z_min_idx:z_max_idx], data[:, data_min_idx:data_max_idx], |
488 im1 = ax1.pcolormesh(x[:, z_min_idx:z_max_idx], y[:, z_min_idx:z_max_idx], data[:, data_min_idx:data_max_idx], |
405 cmap=cmap, vmin=vmin, vmax=vmax) |
489 cmap=cmap, vmin=vmin, vmax=vmax) |
406 |
490 |
407 ax1.axis('equal') |
491 ax1.set(adjustable='box', aspect='equal') |
408 |
492 |
409 if add_colorbar: |
493 if add_colorbar: |
410 if cb_format: |
494 if cb_format: |
411 cb1 = plt.colorbar(im1, format=cb_format) |
495 cb1 = plt.colorbar(im1, format=cb_format) |
412 else: |
496 else: |
417 cb_font_size = mpl.rcParams['font.size'] - 2 |
501 cb_font_size = mpl.rcParams['font.size'] - 2 |
418 for ticklabels in cb1.ax.get_yticklabels(): |
502 for ticklabels in cb1.ax.get_yticklabels(): |
419 ticklabels.set_fontsize(cb_font_size) |
503 ticklabels.set_fontsize(cb_font_size) |
420 cb1.ax.yaxis.get_offset_text().set_fontsize(cb_font_size) |
504 cb1.ax.yaxis.get_offset_text().set_fontsize(cb_font_size) |
421 |
505 |
|
506 # Centered axis in center: https://stackoverflow.com/a/31558968 |
|
507 # Move left y-axis and bottim x-axis to centre, passing through (0,0) |
|
508 # ax1.spines['left'].set_position('center') |
|
509 # ax1.spines['bottom'].set_position('center') |
|
510 # |
|
511 # # Eliminate upper and right axes |
|
512 # ax1.spines['right'].set_color('none') |
|
513 # ax1.spines['top'].set_color('none') |
|
514 # |
|
515 # # Show ticks in the left and lower axes only |
|
516 # ax1.xaxis.set_ticks_position('bottom') |
|
517 # ax1.yaxis.set_ticks_position('left') |
|
518 |
|
519 |
422 @staticmethod |
520 @staticmethod |
423 def _polar_to_ground(z, azimuth, zenith): |
521 def _polar_to_ground(z, azimuth, zenith): |
424 """ |
522 """ |
425 Convert polar coordinates to cartesian project for a PPI scan |
523 Convert polar coordinates to cartesian project for a PPI scan |
426 |
524 |
445 azimuth_rad = np.deg2rad(azimuth) |
543 azimuth_rad = np.deg2rad(azimuth) |
446 |
544 |
447 Z, Zeniths = np.meshgrid(z, zenith_rad) |
545 Z, Zeniths = np.meshgrid(z, zenith_rad) |
448 Z_ground = Z * np.sin(Zeniths) |
546 Z_ground = Z * np.sin(Zeniths) |
449 |
547 |
450 x = Z_ground * np.cos(azimuth_rad)[:, np.newaxis] |
548 x = Z_ground * np.sin(azimuth_rad)[:, np.newaxis] |
451 y = Z_ground * np.sin(azimuth_rad)[:, np.newaxis] |
549 y = Z_ground * np.cos(azimuth_rad)[:, np.newaxis] |
452 |
550 |
453 return x, y |
551 return x, y |
454 |
552 |
455 @staticmethod |
553 @staticmethod |
456 def _polar_to_cross_section(z, azimuth, zenith, cross_section_azimuth): |
554 def _polar_to_cross_section(z, azimuth, zenith, cross_section_azimuth): |
477 """ |
575 """ |
478 |
576 |
479 zenith_rad = np.deg2rad(zenith) |
577 zenith_rad = np.deg2rad(zenith) |
480 |
578 |
481 # The angle between measurements and the cross section plance |
579 # The angle between measurements and the cross section plance |
482 azimuth_difference_rad = np.deg2rad(azimuth) - cross_section_azimuth |
580 azimuth_difference_rad = np.deg2rad(azimuth) - np.deg2rad(cross_section_azimuth) |
483 |
581 |
484 # Generate the mesh |
582 # Generate the mesh |
485 Z, Azimuth_differences = np.meshgrid(z, azimuth_difference_rad) |
583 Z, Azimuth_differences = np.meshgrid(z, azimuth_difference_rad) |
486 Z_cross_section = Z * np.sin(Azimuth_differences) |
584 |
487 |
585 x = Z * np.sin(zenith_rad)[:, np.newaxis] * np.cos(Azimuth_differences) |
488 x = Z_cross_section * np.sin(zenith_rad)[:, np.newaxis] |
586 y = Z * np.cos(zenith_rad)[:, np.newaxis] |
489 y = Z_cross_section * np.cos(zenith_rad)[:, np.newaxis] |
|
490 |
587 |
491 return x, y |
588 return x, y |
492 |
|
493 |
589 |
494 |
590 |
495 class ScanningLidarMeasurement(LicelLidarMeasurement): |
591 class ScanningLidarMeasurement(LicelLidarMeasurement): |
496 """ A class representing a scanning measurement set. |
592 """ A class representing a scanning measurement set. |
497 |
593 |