| 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 |