atmospheric_lidar/raymetrics.py

Wed, 18 Nov 2020 15:11:18 +0000

author
George Doxastakis <gdoxastakis.ee@gmail.com>
date
Wed, 18 Nov 2020 15:11:18 +0000
changeset 207
53124a886152
parent 194
809190df0dc8
child 209
f3bec37bfcd0
permissions
-rw-r--r--

Added custom_info field support

i@125 1 """ Code to read Raymetrics version of Licel binary files."""
i@125 2 import logging
i@125 3
i@181 4 import matplotlib as mpl
i@174 5 import numpy as np
i@174 6 from matplotlib import pyplot as plt
i@174 7
i@130 8 from .licel import LicelFile, LicelLidarMeasurement, LicelChannel, PhotodiodeChannel
i@125 9
i@125 10 logger = logging.getLogger(__name__)
i@125 11
i@125 12
i@125 13 class ScanningFile(LicelFile):
i@132 14 """ Raymetrics is using a custom version of licel file format to store scanning lidar measurements.
i@132 15
i@132 16 The file includes one extra line describing the scan strategy of the dataset. The extra parameters are:
i@132 17
i@132 18 `azimuth_start`
i@132 19 Start azimuth angle for the scan, relative to instrument zero position (degrees).
i@132 20
i@132 21 `azimuth_stop`
i@132 22 Stop azimuth angle for the scan, relative to instrument zero position (degrees).
i@132 23
i@132 24 `azimuth_step`
i@132 25 Step of the azimuth scan (degrees).
i@132 26
i@132 27 `zenith_start`
i@132 28 Start zenith angle for the scan, relative to *nadir* (degrees). Take care that this is actually
i@132 29 nadir angle. Vertical measurements correspond to -90.
i@132 30
i@132 31 `zenith_stop`
i@132 32 Stop zenith angle for the scan, relative to *nadir* (degrees). Take care that this is actually
i@132 33 nadir angle. Vertical measurements correspond to -90.
i@132 34
i@132 35 `zenith_step`
i@132 36 Step of the zenith scan (degrees).
i@132 37
i@132 38 `azimuth_offset`
i@132 39 Offset of instrument zero from North (degrees). Using this value you can convert `azimuth_start` and
i@132 40 `azimuth_stop` to absolute values.
i@132 41
i@132 42 Moreover, four new parameters are added in the second line of the file:
i@132 43
i@132 44 `zenith_angle`
i@132 45 Zenith angle of the current file. Take care that this is actually
i@132 46 nadir angle. Vertical measurements correspond to -90.
i@132 47
i@132 48 `azimuth_angle`
i@132 49 Azimuth angle of the current file. Value relative to instrument zero position.
i@132 50
i@132 51 `temperature`
i@132 52 Ambient temperature (degrees C)
i@132 53
i@132 54 `pressure`
i@132 55 Ambient pressure (hPa)
i@132 56 """
i@132 57
i@132 58 # Specifications of the header lines.
i@129 59 licel_file_header_format = ['filename',
gdoxastakis@207 60 'start_date start_time end_date end_time altitude longitude latitude zenith_angle azimuth_angle temperature pressure custom_info',
i@129 61 # Appart from Site that is read manually
i@132 62 'azimuth_start azimuth_stop azimuth_step zenith_start zenith_stop zenith_step azimuth_offset',
i@129 63 'LS1 rate_1 LS2 rate_2 number_of_datasets', ]
i@132 64
i@132 65 # Specifications of the channel lines in the header
i@129 66 licel_file_channel_format = 'active analog_photon laser_used number_of_datapoints 1 HV bin_width wavelength d1 d2 d3 d4 ADCbits number_of_shots discriminator ID'
i@125 67
i@176 68 fix_zenith_angle = True
ioannis@186 69 skip_scan_overview_line = False # Skip the 3d line containing azimuth_start, stop etc. Used to overcome a bug in some output files.
i@176 70
i@125 71 def _read_rest_of_header(self, f):
i@132 72 """ Read the third and fourth row of of the header lines.
i@132 73
i@132 74 The first two rows are read in the licel class.
i@132 75
i@132 76 Parameters
i@132 77 ----------
i@132 78 f : file
i@132 79 An open file-like object.
i@132 80
i@132 81 Returns
i@132 82 -------
i@132 83 raw_info : dict
i@132 84 A dictionary containing all parameters of the third and fourth line of the header.
i@132 85 """
i@125 86 raw_info = {}
i@125 87
ioannis@170 88 third_line = f.readline().decode()
ioannis@194 89 self.header_lines.append(third_line.strip())
ioannis@194 90
i@125 91 raw_info.update(self.match_lines(third_line, self.licel_file_header_format[2]))
i@125 92
ioannis@170 93 fourth_line = f.readline().decode()
ioannis@194 94 self.header_lines.append(fourth_line.strip())
ioannis@194 95
i@125 96 raw_info.update(self.match_lines(fourth_line, self.licel_file_header_format[3]))
i@125 97 return raw_info
i@125 98
i@129 99 def _assign_properties(self):
i@132 100 """ Assign scanning-specific parameters found in the header as object properties."""
i@129 101 super(ScanningFile, self)._assign_properties()
ioannis@177 102 self.azimuth_angle_raw = float(self.raw_info['azimuth_angle'])
i@129 103 self.temperature = float(self.raw_info['temperature'])
i@129 104 self.pressure = float(self.raw_info['pressure'])
ioannis@186 105
ioannis@186 106 if not self.skip_scan_overview_line:
ioannis@186 107 self.azimuth_start_raw = float(self.raw_info['azimuth_start'])
ioannis@186 108 self.azimuth_stop_raw = float(self.raw_info['azimuth_stop'])
ioannis@186 109 self.azimuth_step = float(self.raw_info['azimuth_step'])
ioannis@186 110 self.zenith_start_raw = float(self.raw_info['zenith_start'])
ioannis@186 111 self.zenith_stop_raw = float(self.raw_info['zenith_stop'])
ioannis@186 112 self.zenith_step = float(self.raw_info['zenith_step'])
ioannis@186 113 self.azimuth_offset = float(self.raw_info['azimuth_offset'])
ioannis@186 114 else:
ioannis@186 115 self.azimuth_start_raw = np.nan
ioannis@186 116 self.azimuth_stop_raw = np.nan
ioannis@186 117 self.azimuth_step = np.nan
ioannis@186 118 self.zenith_start_raw = np.nan
ioannis@186 119 self.zenith_stop_raw = np.nan
ioannis@186 120 self.zenith_step = np.nan
ioannis@186 121 self.azimuth_offset = 0
i@129 122
ioannis@177 123 self.azimuth_angle = (self.azimuth_angle_raw + self.azimuth_offset) % 360
ioannis@177 124 self.azimuth_start = (self.azimuth_start_raw + self.azimuth_offset) % 360
ioannis@177 125 self.azimuth_stop = (self.azimuth_stop_raw + self.azimuth_offset) % 360
i@173 126
i@173 127 if self.fix_zenith_angle:
i@176 128 logger.debug('Fixing zenith start and zenith stop angles.')
i@173 129 self.zenith_start = self._correct_zenith_angle(self.zenith_start_raw)
i@173 130 self.zenith_stop = self._correct_zenith_angle(self.zenith_stop_raw)
i@173 131 else:
i@173 132 self.zenith_start = self.zenith_start_raw
i@173 133 self.zenith_stop = self.zenith_stop_raw
i@173 134
gdoxastakis@207 135 try:
gdoxastakis@207 136 self.custom_info = self.raw_info['custom_info'].strip('"')
gdoxastakis@207 137 except KeyError:
gdoxastakis@207 138 self.custom_info = None
gdoxastakis@207 139
i@181 140 def get_coordinates(self, channel_name):
i@181 141 """
i@181 142 Calculate the lat, lon, z coordinates for each measurement point.
i@181 143
i@181 144 Parameters
i@181 145 ----------
i@181 146 channel_name : str
i@181 147 The name of the channel. Only the channel object knows about the
i@181 148 range resolution.
i@181 149
i@181 150 Returns
i@181 151 -------
i@181 152 lat : array
i@181 153 Latitude array
i@181 154 lon : array
i@181 155 Longitude array
i@181 156 z : array
i@181 157 Altitude array in meters
i@181 158 """
i@181 159 R_earth = 6378137 # Earth radius in meters
i@181 160
i@181 161 # Shortcuts to make equations cleaner
i@181 162 lat_center = self.latitude
i@181 163 lon_center = self.longitude
i@181 164 r = self.channels[channel_name].z
i@181 165 azimuth = self.azimuth_angle
i@181 166 zenith = self.zenith_angle
i@181 167
i@181 168 # Convert all angles to radiants
i@181 169 zenith_rad = np.deg2rad(zenith)[:, np.newaxis]
i@181 170 azimuth_rad = np.deg2rad(azimuth)[:, np.newaxis]
i@181 171
i@181 172 lat_center_rad = np.deg2rad(lat_center)
i@181 173 lon_center_rad = np.deg2rad(lon_center)
i@181 174
i@181 175 # Generate the mesh
i@181 176 R, Zeniths = np.meshgrid(r, zenith_rad)
i@181 177 R_ground = R * np.sin(Zeniths)
i@181 178 Z = R * np.cos(Zeniths)
i@181 179
i@181 180 # Equations from https://www.movable-type.co.uk/scripts/latlong.html
i@181 181 delta = R_ground / R_earth
i@181 182 lat_out_rad = np.arcsin(np.sin(lat_center_rad) * np.cos(delta)
i@181 183 + np.cos(lat_center_rad) * np.sin(delta) * np.cos(azimuth_rad))
i@181 184 lon_out_rad = lon_center_rad + np.arctan2(np.sin(azimuth_rad) * np.sin(delta) * np.cos(lat_center_rad),
i@181 185 np.cos(delta) - np.sin(lat_center_rad) * np.sin(lat_out_rad))
i@181 186
i@181 187 # Convert back to degrees
i@181 188 lat_out = np.rad2deg(lat_out_rad)
i@181 189 lon_out = np.rad2deg(lon_out_rad)
i@181 190
i@181 191 return lat_out, lon_out, Z
i@181 192
i@129 193
i@129 194 class ScanningChannel(LicelChannel):
i@132 195 """ A class representing measurements of a specific lidar channel, during a scanning measurement. """
i@132 196
i@129 197 def __init__(self):
i@131 198 super(ScanningChannel, self).__init__()
i@131 199
i@129 200 self.azimuth_start = None
i@129 201 self.azimuth_stop = None
i@129 202 self.azimuth_step = None
i@129 203 self.zenith_start = None
i@132 204 self.zenith_stop = None
i@129 205 self.zenith_step = None
i@129 206 self.azimuth_offset = None
i@131 207 self.zenith_angles = []
i@131 208 self.azimuth_angles = []
ioannis@177 209 self.temperature = []
ioannis@177 210 self.pressure = []
i@131 211
i@131 212 def append_file(self, current_file, file_channel):
i@132 213 """ Keep track of scanning-specific variable properties of each file. """
i@131 214 super(ScanningChannel, self).append_file(current_file, file_channel)
i@131 215 self.zenith_angles.append(current_file.zenith_angle)
i@131 216 self.azimuth_angles.append(current_file.azimuth_angle)
ioannis@177 217 self.temperature.append(current_file.temperature)
ioannis@177 218 self.pressure.append(current_file.pressure)
i@131 219
i@131 220 def _assign_properties(self, current_file, file_channel):
i@132 221 """ Assign scanning-specific properties as object properties. Check that these are unique,
i@132 222 i.e. that all files belong to the same measurements set.
i@132 223
i@132 224 Parameters
i@132 225 ----------
i@132 226 current_file : ScanningFile object
i@132 227 A ScanningFile object being imported
i@132 228 file_channel : LicelChannelData object
i@132 229 A specific LicelChannelData object holding data found in the file.
i@132 230 """
i@131 231 super(ScanningChannel, self)._assign_properties(current_file, file_channel)
i@131 232 self._assign_unique_property('azimuth_start', current_file.azimuth_start)
i@131 233 self._assign_unique_property('azimuth_stop', current_file.azimuth_stop)
ioannis@177 234 self._assign_unique_property('azimuth_start_raw', current_file.azimuth_start_raw)
ioannis@177 235 self._assign_unique_property('azimuth_stop_raw', current_file.azimuth_stop_raw)
i@131 236 self._assign_unique_property('azimuth_step', current_file.azimuth_step)
i@131 237 self._assign_unique_property('zenith_start', current_file.zenith_start)
i@132 238 self._assign_unique_property('zenith_stop', current_file.zenith_stop)
ioannis@177 239 self._assign_unique_property('zenith_start_raw', current_file.zenith_start_raw)
ioannis@177 240 self._assign_unique_property('zenith_stop_raw', current_file.zenith_stop_raw)
i@131 241 self._assign_unique_property('zenith_step', current_file.zenith_step)
i@129 242
i@174 243 def plot_ppi(self, figsize=(8, 4), signal_type='rc', z_min=0., z_max=12000., show_plot=True,
i@181 244 cmap=plt.cm.jet, title=None, vmin=0, vmax=1.3 * 10 ** 7, mask_noise=True, noise_threshold=1.):
i@174 245 """
i@174 246 Plot a vertical project of channel data.
i@174 247
i@174 248 Parameters
i@174 249 ----------
i@174 250 figsize : tuple
i@174 251 (width, height) of the output figure (inches)
i@174 252 signal_type : str
i@174 253 If 'rc', the range corrected signal is ploted. Else, the raw signals are used.
i@174 254 z_min : float
i@174 255 Minimum z range
i@174 256 z_max : float
i@174 257 Maximum z range
i@174 258 show_plot : bool
i@174 259 If True, the show_plot command is run.
i@174 260 cmap : cmap
i@174 261 An instance of a matplotlib colormap to use.
i@174 262 z0 : float
i@174 263 The ground-level altitude. If provided the plot shows altitude above sea level.
i@174 264 title : str
i@174 265 Optional title for the plot.
i@174 266 vmin : float
i@174 267 Minimum value for the color scale.
i@174 268 vmax : float
i@174 269 Maximum value for the color scale.
i@174 270 mask_noise : bool
i@174 271 If True, remove noisy bins.
i@174 272 noise_threshold : int
i@174 273 Threshold to use in the noise masking routine.
i@174 274 """
i@174 275 fig = plt.figure(figsize=figsize)
i@174 276 ax1 = fig.add_subplot(111)
i@174 277
i@174 278 self.draw_ppi(ax1, cmap=cmap, signal_type=signal_type, z_min=z_min, z_max=z_max, vmin=vmin, vmax=vmax,
i@174 279 mask_noise=mask_noise, noise_threshold=noise_threshold)
i@174 280
i@174 281 if title:
i@174 282 ax1.set_title(title)
i@174 283 else:
i@174 284 ax1.set_title("PPI scan")
i@174 285
i@174 286 if show_plot:
i@174 287 plt.show()
i@174 288
i@176 289 def plot_rhi(self, figsize=(8, 4), signal_type='rc', z_min=0., z_max=12000., show_plot=True,
i@181 290 cmap=plt.cm.jet, title=None, vmin=0, vmax=1.3 * 10 ** 7, mask_noise=True, noise_threshold=1.):
i@176 291 """
i@176 292 Plot a vertical project of channel data.
i@176 293
i@176 294 Parameters
i@176 295 ----------
i@176 296 figsize : tuple
i@176 297 (width, height) of the output figure (inches)
i@176 298 signal_type : str
i@176 299 If 'rc', the range corrected signal is ploted. Else, the raw signals are used.
i@176 300 z_min : float
i@176 301 Minimum z range
i@176 302 z_max : float
i@176 303 Maximum z range
i@176 304 show_plot : bool
i@176 305 If True, the show_plot command is run.
i@176 306 cmap : cmap
i@176 307 An instance of a matplotlib colormap to use.
i@176 308 z0 : float
i@176 309 The ground-level altitude. If provided the plot shows altitude above sea level.
i@176 310 title : str
i@176 311 Optional title for the plot.
i@176 312 vmin : float
i@176 313 Minimum value for the color scale.
i@176 314 vmax : float
i@176 315 Maximum value for the color scale.
i@176 316 mask_noise : bool
i@176 317 If True, remove noisy bins.
i@176 318 noise_threshold : int
i@176 319 Threshold to use in the noise masking routine.
i@176 320 """
i@176 321 fig = plt.figure(figsize=figsize)
i@176 322 ax1 = fig.add_subplot(111)
i@176 323
i@181 324 projection_angle = self.draw_rhi(ax1, cmap=cmap, signal_type=signal_type, z_min=z_min, z_max=z_max, vmin=vmin,
i@181 325 vmax=vmax,
i@181 326 mask_noise=mask_noise, noise_threshold=noise_threshold)
i@176 327
i@176 328 if title:
i@176 329 ax1.set_title(title)
i@176 330 else:
ioannis@177 331 ax1.set_title("RHI scan ({0}$^\circ$)".format(projection_angle))
i@176 332
i@176 333 if show_plot:
i@176 334 plt.show()
i@176 335
ioannis@177 336 def plot_scan(self, figsize=(8, 4), signal_type='rc', z_min=0., z_max=12000., show_plot=True,
i@181 337 cmap=plt.cm.jet, vmin=0, vmax=1.3 * 10 ** 7, mask_noise=True, noise_threshold=1., cb_format='%.0e',
i@185 338 box=False, grid=(1, 4), ax1_position=(0, 0), ax1_span=2, ax2_position=(0, 2), ax2_span=2):
ioannis@177 339 """
ioannis@177 340 Plot data as RHI and PPI scans.
ioannis@177 341
ioannis@177 342 Parameters
ioannis@177 343 ----------
ioannis@177 344 figsize : tuple
ioannis@177 345 (width, height) of the output figure (inches)
ioannis@177 346 signal_type : str
ioannis@177 347 If 'rc', the range corrected signal is ploted. Else, the raw signals are used.
ioannis@177 348 z_min : float
ioannis@177 349 Minimum z range
ioannis@177 350 z_max : float
ioannis@177 351 Maximum z range
ioannis@177 352 show_plot : bool
ioannis@177 353 If True, the show_plot command is run.
ioannis@177 354 cmap : cmap
ioannis@177 355 An instance of a matplotlib colormap to use.
ioannis@177 356 z0 : float
ioannis@177 357 The ground-level altitude. If provided the plot shows altitude above sea level.
ioannis@177 358 title : str
ioannis@177 359 Optional title for the plot.
ioannis@177 360 vmin : float
ioannis@177 361 Minimum value for the color scale.
ioannis@177 362 vmax : float
ioannis@177 363 Maximum value for the color scale.
ioannis@177 364 mask_noise : bool
ioannis@177 365 If True, remove noisy bins.
ioannis@177 366 noise_threshold : int
ioannis@177 367 Threshold to use in the noise masking routine.
ioannis@177 368 """
ioannis@177 369 fig = plt.figure(figsize=figsize)
i@185 370
i@185 371 ax1 = plt.subplot2grid(grid, ax1_position, colspan=ax1_span)
i@185 372 ax2 = plt.subplot2grid(grid, ax2_position, colspan=ax2_span)
ioannis@177 373
ioannis@177 374 self.draw_ppi(ax1, cmap=cmap, signal_type=signal_type, z_min=z_min, z_max=z_max, vmin=vmin, vmax=vmax,
ioannis@177 375 mask_noise=mask_noise, noise_threshold=noise_threshold, add_colorbar=False, cb_format=cb_format,
ioannis@177 376 box=box)
ioannis@177 377
i@181 378 projection_angle = self.draw_rhi(ax2, cmap=cmap, signal_type=signal_type, z_min=z_min, z_max=z_max, vmin=vmin,
i@181 379 vmax=vmax,
i@181 380 mask_noise=mask_noise, noise_threshold=noise_threshold, cb_format=cb_format,
i@181 381 box=box)
ioannis@177 382
ioannis@177 383 fig.suptitle("Channel {0}: {1} - {2}".format(self.name,
ioannis@177 384 self.start_time.strftime('%Y%m%dT%H%M'),
ioannis@177 385 self.stop_time.strftime('%Y%m%dT%H%M')))
ioannis@177 386
ioannis@177 387 ax1.set_title('PPI')
ioannis@177 388 ax2.set_title("RHI ({0:.1f}$^\circ$)".format(projection_angle))
ioannis@177 389
ioannis@177 390 plt.tight_layout()
ioannis@177 391 plt.subplots_adjust(top=0.85)
ioannis@177 392
ioannis@177 393 if show_plot:
ioannis@177 394 plt.show()
ioannis@177 395
i@174 396 def draw_ppi(self, ax1, cmap=plt.cm.jet, signal_type='rc',
i@176 397 z_min=0, z_max=12000., add_colorbar=True, cmap_label='a.u.', cb_format=None,
ioannis@177 398 vmin=0, vmax=1.3 * 10 ** 7, mask_noise=True, noise_threshold=1., first_signal_bin=0, box=False):
i@174 399 """
i@174 400 Draw channel data as a PPI plot.
i@174 401
i@174 402 Parameters
i@174 403 ----------
i@174 404 ax1 : axis object
i@174 405 The axis object to draw.
i@176 406 x : array
i@176 407 X axis coordinates
i@176 408 y : array
i@176 409 Y axis coordinates
i@174 410 cmap : cmap
i@174 411 An instance of a matplotlib colormap to use.
i@174 412 signal_type : str
i@174 413 If 'rc', the range corrected signal is ploted. Else, the raw signals are used.
i@174 414 z_min : float
i@174 415 Minimum z range
i@174 416 z_max : float
i@174 417 Maximum z range
i@174 418 add_colorbar : bool
i@174 419 If True, a colorbar will be added to the plot.
i@174 420 cmap_label : str
i@174 421 Label for the colorbar. Ignored if add_colorbar is False.
i@174 422 cb_format : str
i@174 423 Colorbar tick format string.
i@174 424 vmin : float
i@174 425 Minimum value for the color scale.
i@174 426 vmax : float
i@174 427 Maximum value for the color scale.
i@174 428 mask_noise : bool
i@174 429 If True, remove noisy bins.
i@174 430 noise_threshold : int
i@174 431 Threshold to use in the noise masking routine.
i@176 432 first_signal_bin : int
i@176 433 First signal bin. Can be used to fix analog bin shift of Licel channels.
i@176 434 """
i@181 435 x, y = self._polar_to_ground(self.z / 1000., self.azimuth_angles, self.zenith_angles)
i@176 436
i@176 437 self.draw_projection(ax1, x, y, cmap=cmap, signal_type=signal_type,
i@176 438 z_min=z_min, z_max=z_max, add_colorbar=add_colorbar, cmap_label=cmap_label,
i@176 439 cb_format=cb_format, vmin=vmin, vmax=vmax, mask_noise=mask_noise,
i@176 440 noise_threshold=noise_threshold, first_signal_bin=first_signal_bin)
i@176 441
ioannis@177 442 if box:
ioannis@177 443 ax1.set_xlim(-z_max / 1000., z_max / 1000.)
ioannis@177 444 ax1.set_ylim(-z_max / 1000., z_max / 1000.)
ioannis@177 445
ioannis@177 446 ax1.set_ylabel('South-North (km)')
ioannis@177 447 ax1.set_xlabel('West-East (km)')
ioannis@177 448
i@176 449 def draw_rhi(self, ax1, cmap=plt.cm.jet, signal_type='rc',
i@176 450 z_min=0, z_max=12000., add_colorbar=True, cmap_label='a.u.', cb_format=None,
ioannis@177 451 vmin=0, vmax=1.3 * 10 ** 7, mask_noise=True, noise_threshold=1., first_signal_bin=0, box=False):
i@176 452 """
i@176 453 Draw channel data as a PPI plot.
i@176 454
i@176 455 Parameters
i@176 456 ----------
i@176 457 ax1 : axis object
i@176 458 The axis object to draw.
i@176 459 x : array
i@176 460 X axis coordinates
i@176 461 y : array
i@176 462 Y axis coordinates
i@176 463 cmap : cmap
i@176 464 An instance of a matplotlib colormap to use.
i@176 465 signal_type : str
i@176 466 If 'rc', the range corrected signal is plotted. Else, the raw signals are used.
i@176 467 z_min : float
i@176 468 Minimum z range
i@176 469 z_max : float
i@176 470 Maximum z range
i@176 471 add_colorbar : bool
i@176 472 If True, a colorbar will be added to the plot.
i@176 473 cmap_label : str
i@176 474 Label for the colorbar. Ignored if add_colorbar is False.
i@176 475 cb_format : str
i@176 476 Colorbar tick format string.
i@176 477 vmin : float
i@176 478 Minimum value for the color scale.
i@176 479 vmax : float
i@176 480 Maximum value for the color scale.
i@176 481 mask_noise : bool
i@176 482 If True, remove noisy bins.
i@176 483 noise_threshold : int
i@176 484 Threshold to use in the noise masking routine.
i@176 485 first_signal_bin : int
i@176 486 First signal bin. Can be used to fix analog bin shift of Licel channels.
i@176 487 """
ioannis@177 488 projection_angle = np.mean(self.azimuth_angles)
ioannis@177 489 x, y = self._polar_to_cross_section(self.z / 1000., self.azimuth_angles, self.zenith_angles, projection_angle)
i@176 490
i@176 491 self.draw_projection(ax1, x, y, cmap=cmap, signal_type=signal_type,
i@176 492 z_min=z_min, z_max=z_max, add_colorbar=add_colorbar, cmap_label=cmap_label,
i@176 493 cb_format=cb_format, vmin=vmin, vmax=vmax, mask_noise=mask_noise,
i@176 494 noise_threshold=noise_threshold, first_signal_bin=first_signal_bin)
i@176 495
i@181 496 padding = 0.5 # km
ioannis@177 497
ioannis@177 498 if box:
ioannis@177 499 ax1.set_xlim(-z_max / 1000. - padding, z_max / 1000. + padding)
ioannis@177 500 ax1.set_ylim(-padding, z_max / 1000. + padding)
ioannis@177 501
ioannis@177 502 ax1.set_xlabel('Distance (km)')
ioannis@177 503 ax1.set_ylabel('Height a.l. (km)')
ioannis@177 504
ioannis@177 505 return projection_angle
ioannis@177 506
i@176 507 def draw_projection(self, ax1, x, y, cmap=plt.cm.jet, signal_type='rc',
i@176 508 z_min=0, z_max=12000., add_colorbar=True, cmap_label='a.u.', cb_format=None,
i@176 509 vmin=0, vmax=1.3 * 10 ** 7, mask_noise=True, noise_threshold=1.,
i@176 510 first_signal_bin=0):
i@176 511 """
i@176 512 Draw channel data as a PPI plot.
i@176 513
i@176 514 Parameters
i@176 515 ----------
i@176 516 ax1 : axis object
i@176 517 The axis object to draw.
i@176 518 x : array
i@176 519 X axis coordinates
i@176 520 y : array
i@176 521 Y axis coordiantes
i@176 522 cmap : cmap
i@176 523 An instance of a matplotlib colormap to use.
i@176 524 signal_type : str
i@176 525 If 'rc', the range corrected signal is ploted. Else, the raw signals are used.
i@176 526 z_min : float
i@176 527 Minimum z range
i@176 528 z_max : float
i@176 529 Maximum z range
i@176 530 add_colorbar : bool
i@176 531 If True, a colorbar will be added to the plot.
i@176 532 cmap_label : str
i@176 533 Label for the colorbar. Ignored if add_colorbar is False.
i@176 534 cb_format : str
i@176 535 Colorbar tick format string.
i@176 536 vmin : float
i@176 537 Minimum value for the color scale.
i@176 538 vmax : float
i@176 539 Maximum value for the color scale.
i@176 540 mask_noise : bool
i@176 541 If True, remove noisy bins.
i@176 542 noise_threshold : int
i@176 543 Threshold to use in the noise masking routine.
i@176 544 first_signal_bin : int
i@176 545 First signal bin. Can be used to fix analog bin shift of Licel channels.
i@174 546 """
i@174 547 if signal_type == 'rc':
i@174 548 if len(self.rc) == 0:
i@174 549 self.calculate_rc()
i@174 550 data = self.rc
i@174 551 else:
i@174 552 data = self.matrix
i@174 553
i@174 554 if mask_noise:
i@174 555 mask = self.noise_mask(threshold=noise_threshold)
i@174 556 data = np.ma.masked_where(mask, data)
i@174 557
i@174 558 z_min_idx = self._index_at_height(z_min)
i@174 559 z_max_idx = self._index_at_height(z_max)
i@174 560
i@176 561 data_min_idx = z_min_idx + first_signal_bin
i@176 562 data_max_idx = z_max_idx + first_signal_bin
i@174 563
i@176 564 im1 = ax1.pcolormesh(x[:, z_min_idx:z_max_idx], y[:, z_min_idx:z_max_idx], data[:, data_min_idx:data_max_idx],
i@176 565 cmap=cmap, vmin=vmin, vmax=vmax)
i@174 566
ioannis@177 567 ax1.set(adjustable='box', aspect='equal')
i@174 568
i@174 569 if add_colorbar:
i@174 570 if cb_format:
i@174 571 cb1 = plt.colorbar(im1, format=cb_format)
i@174 572 else:
i@174 573 cb1 = plt.colorbar(im1)
i@174 574 cb1.ax.set_ylabel(cmap_label)
i@174 575
i@174 576 # Make the ticks of the colorbar smaller, two points smaller than the default font size
i@174 577 cb_font_size = mpl.rcParams['font.size'] - 2
i@174 578 for ticklabels in cb1.ax.get_yticklabels():
i@174 579 ticklabels.set_fontsize(cb_font_size)
i@174 580 cb1.ax.yaxis.get_offset_text().set_fontsize(cb_font_size)
i@174 581
ioannis@177 582 # Centered axis in center: https://stackoverflow.com/a/31558968
ioannis@177 583 # Move left y-axis and bottim x-axis to centre, passing through (0,0)
ioannis@177 584 # ax1.spines['left'].set_position('center')
ioannis@177 585 # ax1.spines['bottom'].set_position('center')
ioannis@177 586 #
ioannis@177 587 # # Eliminate upper and right axes
ioannis@177 588 # ax1.spines['right'].set_color('none')
ioannis@177 589 # ax1.spines['top'].set_color('none')
ioannis@177 590 #
ioannis@177 591 # # Show ticks in the left and lower axes only
ioannis@177 592 # ax1.xaxis.set_ticks_position('bottom')
ioannis@177 593 # ax1.yaxis.set_ticks_position('left')
ioannis@177 594
i@174 595 @staticmethod
i@174 596 def _polar_to_ground(z, azimuth, zenith):
i@174 597 """
i@174 598 Convert polar coordinates to cartesian project for a PPI scan
i@174 599
i@174 600 Parameters
i@174 601 ----------
i@174 602 z : array
i@174 603 Distance array in meters
i@174 604 azimuth : list
i@174 605 List of profile azimuth angles in degrees
i@174 606 zenith : list
i@174 607 List of profile zenith angles in degrees
i@174 608
i@174 609 Returns
i@174 610 -------
i@174 611 x : array
i@174 612 X axis in meters
i@174 613 y : array
i@174 614 Y axis in meters
i@174 615 """
i@174 616 # Generate the mesh
i@174 617 zenith_rad = np.deg2rad(zenith)
i@174 618 azimuth_rad = np.deg2rad(azimuth)
i@174 619
i@174 620 Z, Zeniths = np.meshgrid(z, zenith_rad)
i@174 621 Z_ground = Z * np.sin(Zeniths)
i@174 622
ioannis@177 623 x = Z_ground * np.sin(azimuth_rad)[:, np.newaxis]
ioannis@177 624 y = Z_ground * np.cos(azimuth_rad)[:, np.newaxis]
i@174 625
i@174 626 return x, y
i@174 627
i@176 628 @staticmethod
i@176 629 def _polar_to_cross_section(z, azimuth, zenith, cross_section_azimuth):
i@176 630 """
i@176 631 Convert polar coordinates to cartesian project for a PPI scan
i@176 632
i@176 633 Parameters
i@176 634 ----------
i@176 635 z : array
i@176 636 Distance array in meters
i@176 637 azimuth : list
i@176 638 List of profile azimuth angles in degrees
i@176 639 zenith : list
i@176 640 List of profile zenith angles in degrees
i@176 641 cross_section_azimuth : float
i@176 642 Azimuth angle of plane in degrees
i@176 643
i@176 644 Returns
i@176 645 -------
i@176 646 x : array
i@176 647 X axis in meters
i@176 648 y : array
i@176 649 Y axis in meters
i@176 650 """
i@176 651
i@176 652 zenith_rad = np.deg2rad(zenith)
i@176 653
i@176 654 # The angle between measurements and the cross section plance
ioannis@177 655 azimuth_difference_rad = np.deg2rad(azimuth) - np.deg2rad(cross_section_azimuth)
i@176 656
i@176 657 # Generate the mesh
i@176 658 Z, Azimuth_differences = np.meshgrid(z, azimuth_difference_rad)
i@176 659
ioannis@177 660 x = Z * np.sin(zenith_rad)[:, np.newaxis] * np.cos(Azimuth_differences)
ioannis@177 661 y = Z * np.cos(zenith_rad)[:, np.newaxis]
i@176 662
i@176 663 return x, y
i@176 664
i@181 665 def get_coordinates(self,):
i@181 666 """
i@181 667 Calculate the lat, lon, z coordinates for each measurement point.
i@181 668
i@181 669 Returns
i@181 670 -------
i@181 671 lat : array
i@181 672 Latitude array
i@181 673 lon : array
i@181 674 Longitude array
i@181 675 z : array
i@181 676 Altitude array in meters
i@181 677 """
i@181 678 R_earth = 6378137 # Earth radius in meters
i@181 679
i@181 680 # Shortcuts to make equations cleaner
i@181 681 lat_center = self.latitude
i@181 682 lon_center = self.longitude
i@181 683 r = self.z
i@181 684 azimuth = self.azimuth_angles
i@181 685 zenith = self.zenith_angles
i@181 686
i@181 687 # Convert all angles to radiants
i@181 688 zenith_rad = np.deg2rad(zenith)[:, np.newaxis]
i@181 689 azimuth_rad = np.deg2rad(azimuth)[:, np.newaxis]
i@181 690
i@181 691 lat_center_rad = np.deg2rad(lat_center)
i@181 692 lon_center_rad = np.deg2rad(lon_center)
i@181 693
i@181 694 # Generate the mesh
i@181 695 R, Zeniths = np.meshgrid(r, zenith_rad)
i@181 696 R_ground = R * np.sin(Zeniths)
i@181 697 Z = R * np.cos(Zeniths)
i@181 698
i@181 699 # Equations from https://www.movable-type.co.uk/scripts/latlong.html
i@181 700 delta = R_ground / R_earth
i@181 701 lat_out_rad = np.arcsin(np.sin(lat_center_rad) * np.cos(delta)
i@181 702 + np.cos(lat_center_rad) * np.sin(delta) * np.cos(azimuth_rad))
i@181 703 lon_out_rad = lon_center_rad + np.arctan2(np.sin(azimuth_rad) * np.sin(delta) * np.cos(lat_center_rad),
i@181 704 np.cos(delta) - np.sin(lat_center_rad) * np.sin(lat_out_rad))
i@181 705
i@181 706 # Convert back to degrees
i@181 707 lat_out = np.rad2deg(lat_out_rad)
i@181 708 lon_out = np.rad2deg(lon_out_rad)
i@181 709
i@181 710 return lat_out, lon_out, Z
i@181 711
i@176 712
ioannis@186 713 class ScanningFileMissingLine(ScanningFile):
ioannis@186 714 skip_scan_overview_line = True
ioannis@186 715
ioannis@186 716
i@130 717 class ScanningLidarMeasurement(LicelLidarMeasurement):
i@132 718 """ A class representing a scanning measurement set.
i@132 719
i@132 720 It useses `ScanningFile` and `ScanningChannel` classes for handling the data.
i@132 721 """
i@125 722 file_class = ScanningFile
i@131 723 channel_class = ScanningChannel
i@130 724 photodiode_class = PhotodiodeChannel
i@152 725
i@152 726
i@178 727 class FixedPointingFile(LicelFile):
i@152 728 """ Raymetrics is using a custom version of licel file format to store
i@152 729 vertical lidar measurements.
i@152 730
i@152 731 `temperature`
i@152 732 Ambient temperature (degrees C)
i@152 733
i@152 734 `pressure`
i@152 735 Ambient pressure (hPa)
i@152 736 """
i@152 737 # Specifications of the header lines.
i@152 738 licel_file_header_format = ['filename',
gdoxastakis@207 739 'start_date start_time end_date end_time altitude longitude latitude zenith_angle azimuth_angle temperature pressure custom_info',
i@152 740 # Appart from Site that is read manually
i@152 741 'LS1 rate_1 LS2 rate_2 number_of_datasets', ]
i@152 742
i@178 743 fix_zenith_angle = True
i@152 744
i@178 745 def _assign_properties(self):
i@178 746 """ Assign scanning-specific parameters found in the header as object properties."""
i@178 747 super(FixedPointingFile, self)._assign_properties()
i@183 748
i@178 749 self.temperature = float(self.raw_info['temperature'])
i@178 750 self.pressure = float(self.raw_info['pressure'])
i@184 751 self.azimuth_angle = float(self.raw_info['azimuth_angle'])
i@178 752
gdoxastakis@207 753 try:
gdoxastakis@207 754 self.custom_info = self.raw_info['custom_info'].strip('"')
gdoxastakis@207 755 except KeyError:
gdoxastakis@207 756 self.custom_info = None
gdoxastakis@207 757
i@178 758
i@178 759 class FixedPointingChannel(LicelChannel):
i@178 760 """ A class representing measurements of a specific lidar channel, during a fixed pointing measurement. """
i@178 761
i@178 762 def __init__(self):
i@178 763 super(FixedPointingChannel, self).__init__()
i@178 764 self.zenith_angles = []
i@178 765 self.azimuth_angles = []
i@178 766 self.temperature = []
i@178 767 self.pressure = []
i@178 768
i@178 769 def append_file(self, current_file, file_channel):
i@178 770 """ Keep track of scanning-specific variable properties of each file. """
i@178 771 super(FixedPointingChannel, self).append_file(current_file, file_channel)
i@178 772 self.zenith_angles.append(current_file.zenith_angle)
i@178 773 self.azimuth_angles.append(current_file.azimuth_angle)
i@178 774 self.temperature.append(current_file.temperature)
i@178 775 self.pressure.append(current_file.pressure)
i@178 776
i@178 777
i@178 778 class FixedPointingLidarMeasurement(LicelLidarMeasurement):
i@178 779 file_class = FixedPointingFile
i@181 780 channel_class = FixedPointingChannel

mercurial