atmospheric_lidar/raymetrics.py

Wed, 11 Nov 2020 17:06:29 +0200

author
Ioannis <ibinietoglou@raymetrics.com>
date
Wed, 11 Nov 2020 17:06:29 +0200
changeset 205
4a556911e7e0
parent 194
809190df0dc8
child 209
f3bec37bfcd0
permissions
-rw-r--r--

Added optional azimuth_offset argument in FixedPointing files.

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

mercurial