def soundings_plot_3d(self, mode: str = 'svcorr', color_by: str = 'depth', start_time: float = None, end_time: float = None): """ Plots a 3d representation of the alongtrack/acrosstrack/depth values generated by sv correct. If a time is provided, isolates that time. Parameters ---------- mode str, either 'svcorr' to plot the svcorrected offsets, or 'georef' to plot the georeferenced soundings color_by str, either 'depth' or 'sector' start_time start time in utc seconds, optional if you want to subset by time end_time end time in utc seconds, optional if you want to subset by time Returns ------- plt.Axes matplotlib axes object for plot """ if start_time is not None or start_time is not None: self.fqpr.subset_by_time(start_time, end_time) xvar, yvar, zvar = self._parse_plot_mode(mode) minz = self.fqpr.calc_min_var(zvar) maxz = self.fqpr.calc_max_var(zvar) miny = self.fqpr.calc_min_var(yvar) maxy = self.fqpr.calc_max_var(yvar) if mode == 'svcorr': # svcorrected is alongtrack/acrosstrack in meters. Want the scales to be equal so it doesnt look weird minx = miny maxx = maxy else: # georeferenced is northing/easting, scales cant be equal of course minx = self.fqpr.calc_min_var(xvar) maxx = self.fqpr.calc_max_var(xvar) fig = plt.figure() ax = fig.add_subplot(111, projection='3d') for rp in self.fqpr.multibeam.raw_ping: x_idx, x_stck = stack_nan_array(rp[xvar], stack_dims=('time', 'beam')) y_idx, y_stck = stack_nan_array(rp[yvar], stack_dims=('time', 'beam')) z_idx, z_stck = stack_nan_array(rp[zvar], stack_dims=('time', 'beam')) if color_by == 'depth': ax.scatter(x_stck.values, y_stck.values, z_stck.values, marker='o', s=10, c=z_stck.values) elif color_by == 'sector': sector_vals = rp.txsector_beam.values[x_idx] ax.scatter(x_stck.values, y_stck.values, z_stck.values, marker='o', s=10, c=sector_vals) ax.set_xlim(minx, maxx) ax.set_ylim(miny, maxy) ax.set_zlim(maxz, minz) if start_time is not None or start_time is not None: self.fqpr.restore_subset() return ax
def test_stack_and_reform_nan_array(self): # build an array with nans data = np.full((10, 400), 1.5) data[1, 50] = np.nan data[2, 75] = np.nan data[3, 150] = np.nan data_array = xr.DataArray(data, coords={ 'time': np.arange(10), 'beam': np.arange(400) }, dims=['time', 'beam']) # flatten the array and remove the nans by stacking the time and beam dimensions to get a 1dim result original_index, stacked_data = stack_nan_array(data_array) # size of the stacked data should be the original size minus the number of nans assert original_index[0].shape == (3997, ) assert original_index[1].shape == (3997, ) assert stacked_data.shape == (3997, ) assert np.all(stacked_data == stacked_data[0]) # we do the stack/reform thing so that we can operate on the data with functions that do not play well with nan # we can run a routine on the stacked result, and rebuild the original shape with the nans afterwards # multibeam data with varying beams or with beams with no data will show as nan in the time/beam arrays # get back to the original array by building an array of nans equal in shape to the original array and repopulate # with the original data orig_array = reform_nan_array(stacked_data, original_index, data_array.shape, data_array.coords, data_array.dims) assert np.isnan(orig_array[1, 50]) assert np.isnan(orig_array[2, 75]) assert np.isnan(orig_array[3, 150])
def get_times(pingtime: xr.DataArray, additional: xr.DataArray): """ Given ping time and beamwise time addition, return the time for each beam. Provides unique times, as by just adding ping time and twtt, you might end up with duplicate times. To get back to beamwise times, an index is provided that you can select by. Parameters ---------- pingtime 1dim array of timestamps representing time of ping additional 2dim (time/beam) array of timestamps representing additional delay Returns ------- tuple tuple of numpy arrays, 2d indices of original rx_tstmp Dataarray, used to reconstruct array np.array 1d indices of time of receive that can be used to reconstruct non-unique timestamps xr.DataArray 1 dim array of timestamps for unique times of receive """ rx_tstmp = pingtime + additional rx_tstmp_idx, rx_tstmp_stck = stack_nan_array(rx_tstmp, stack_dims=('time', 'beam')) unique_rx_times, inv_idx = np.unique(rx_tstmp_stck.values, return_inverse=True) rx_interptimes = xr.DataArray(unique_rx_times, coords=[unique_rx_times], dims=['time']).chunk() return rx_tstmp_idx, inv_idx, rx_interptimes
def transform_vyperdatum(x: xr.DataArray, y: xr.DataArray, z: xr.DataArray, source_datum: Union[str, int] = 'nad83', final_datum: str = 'mllw', vdatum_directory: str = None): """ When we specify a NOAA vertical datum (NOAA Mean Lower Low Water, NOAA Mean High Water) in Kluster, we use vyperdatum/VDatum to transform the points to the appropriate vertical datum. Parameters ---------- x easting for each point in source_datum coordinate system y northing for each point in source_datum coordinate system z depth offset for each point in source_datum coordinate system source_datum The horizontal coordinate system of the xyz provided, should be a string identifier ('nad83') or an EPSG code specifying the horizontal coordinate system final_datum The desired final_datum vertical datum as a string (one of 'mllw', 'mhw') vdatum_directory if 'NOAA MLLW' 'NOAA MHW' is the vertical reference, a path to the vdatum directory is required here Returns ------- xr.DataArray original z array with vertical transformation applied, this new z is at final_datum xr.DataArray uncertainty associated with the vertical transformation between the source and destination datum """ if vdatum_directory: vp = VyperPoints(vdatum_directory=vdatum_directory, silent=True) else: vp = VyperPoints(silent=True) if not os.path.exists(vp.vdatum.vdatum_path): raise EnvironmentError( 'Unable to find path to VDatum folder: {}'.format( vp.vdatum.vdatum_path)) z_idx, z_stck = stack_nan_array(z, stack_dims=('time', 'beam')) vp.transform_points(source_datum, final_datum, x, y, z=z_stck.values) z = reform_nan_array(np.around(vp.z, 3), z_idx, z.shape, z.coords, z.dims) xarray_unc = reform_nan_array(np.around(vp.unc, 3), z_idx, z.shape, z.coords, z.dims) return z, xarray_unc
def georef_by_worker(sv_corr: list, alt: xr.DataArray, lon: xr.DataArray, lat: xr.DataArray, hdng: xr.DataArray, heave: xr.DataArray, wline: float, vert_ref: str, input_crs: CRS, horizontal_crs: CRS, z_offset: float, vdatum_directory: str = None): """ Use the raw attitude/navigation to transform the vessel relative along/across/down offsets to georeferenced soundings. Will support transformation to geographic and projected coordinate systems and with a vertical reference that you select. Parameters ---------- sv_corr [x, y, z] offsets generated with sv_correct alt 1d (time) altitude in meters lon 1d (time) longitude in degrees lat 1d (time) latitude in degrees hdng 1d (time) heading in degrees heave 1d (time) heave in degrees wline waterline offset from reference point vert_ref vertical reference point, one of ['ellipse', 'vessel', 'waterline'] input_crs pyproj CRS object, input coordinate reference system information for this run horizontal_crs pyproj CRS object, destination coordinate reference system information for this run z_offset lever arm from reference point to transmitter vdatum_directory if 'NOAA MLLW' 'NOAA MHW' is the vertical reference, a path to the vdatum directory is required here Returns ------- list [xr.DataArray alongtrack offset (time, beam), xr.DataArray acrosstrack offset (time, beam), xr.DataArray down offset (time, beam), xr.DataArray corrected heave for TX - RP lever arm, all zeros if in 'ellipse' mode (time), xr.DataArray corrected altitude for TX - RP lever arm, all zeros if in 'vessel' or 'waterline' mode (time)] """ g = horizontal_crs.get_geod() # unpack the sv corrected data output alongtrack = sv_corr[0] acrosstrack = sv_corr[1] depthoffset = sv_corr[2] + z_offset # generate the corrected depth offset depending on the desired vertical reference corr_dpth = None corr_heave = None corr_altitude = None if vert_ref in kluster_variables.ellipse_based_vertical_references: corr_altitude = alt corr_heave = xr.zeros_like(corr_altitude) corr_dpth = (depthoffset - corr_altitude.values[:, None]).astype( np.float32) elif vert_ref == 'vessel': corr_heave = heave corr_altitude = xr.zeros_like(corr_heave) corr_dpth = (depthoffset + corr_heave.values[:, None]).astype( np.float32) elif vert_ref == 'waterline': corr_heave = heave corr_altitude = xr.zeros_like(corr_heave) corr_dpth = (depthoffset + corr_heave.values[:, None] - wline).astype( np.float32) # get the sv corrected alongtrack/acrosstrack offsets stacked without the NaNs (arrays have NaNs for beams that do not exist in that sector) at_idx, alongtrack_stck = stack_nan_array(alongtrack, stack_dims=('time', 'beam')) ac_idx, acrosstrack_stck = stack_nan_array(acrosstrack, stack_dims=('time', 'beam')) # determine the beam wise offsets bm_azimuth = np.rad2deg(np.arctan2(acrosstrack_stck, alongtrack_stck)) + np.float32( hdng[at_idx[0]].values) bm_radius = np.sqrt(acrosstrack_stck**2 + alongtrack_stck**2) pos = g.fwd(lon[at_idx[0]].values, lat[at_idx[0]].values, bm_azimuth.values, bm_radius.values) z = np.around(corr_dpth, 3) if vert_ref == 'NOAA MLLW': sep, vdatum_unc = transform_vyperdatum( pos[0], pos[1], xr.zeros_like(z), input_crs.to_epsg(), 'mllw', vdatum_directory=vdatum_directory) elif vert_ref == 'NOAA MHW': sep, vdatum_unc = transform_vyperdatum( pos[0], pos[1], xr.zeros_like(z), input_crs.to_epsg(), 'mhw', vdatum_directory=vdatum_directory) else: sep = 0 vdatum_unc = xr.zeros_like(z) z = z - sep if horizontal_crs.is_projected: # Transformer.transform input order is based on the CRS, see CRS.geodetic_crs.axis_info # - lon, lat - this appears to be valid when using CRS from proj4 string # - lat, lon - this appears to be valid when using CRS from epsg # use the always_xy option to force the transform to expect lon/lat order georef_transformer = Transformer.from_crs(input_crs, horizontal_crs, always_xy=True) newpos = georef_transformer.transform( pos[0], pos[1], errcheck=True) # longitude / latitude order (x/y) else: newpos = pos x = reform_nan_array(np.around(newpos[0], 3), at_idx, alongtrack.shape, alongtrack.coords, alongtrack.dims) y = reform_nan_array(np.around(newpos[1], 3), ac_idx, acrosstrack.shape, acrosstrack.coords, acrosstrack.dims) return [x, y, z, corr_heave, corr_altitude, vdatum_unc]
def soundings_plot_2d(self, mode: str = 'svcorr', color_by: str = 'depth', start_time: float = None, end_time: float = None): """ Plots a 2d representation of the acrosstrack/depth values generated by sv correct. If sector is provided, isolates that sector. If a time is provided, isolates that time. Parameters ---------- mode str, either 'svcorr' to plot the svcorrected offsets, or 'georef' to plot the georeferenced soundings color_by str, either 'depth' or 'sector' start_time start time in utc seconds, optional if you want to subset by time end_time end time in utc seconds, optional if you want to subset by time Returns ------- plt.Figure matplotlib.pyplot.figure instance """ if start_time is not None or start_time is not None: self.fqpr.subset_by_time(start_time, end_time) xvar, yvar, zvar = self._parse_plot_mode(mode) minz = self.fqpr.calc_min_var(zvar) maxz = self.fqpr.calc_max_var(zvar) miny = self.fqpr.calc_min_var(yvar) maxy = self.fqpr.calc_max_var(yvar) if mode == 'svcorr': # svcorrected is alongtrack/acrosstrack in meters. Want the scales to be equal so it doesnt look weird minx = miny maxx = maxy else: # georeferenced is northing/easting, scales cant be equal of course minx = self.fqpr.calc_min_var(xvar) maxx = self.fqpr.calc_max_var(xvar) fig = plt.figure() for rp in self.fqpr.multibeam.raw_ping: x_idx, x_stck = stack_nan_array(rp[xvar], stack_dims=('time', 'beam')) y_idx, y_stck = stack_nan_array(rp[yvar], stack_dims=('time', 'beam')) z_idx, z_stck = stack_nan_array(rp[zvar], stack_dims=('time', 'beam')) if color_by == 'depth': plt.scatter(y_stck, x_stck, marker='+', c=z_stck, cmap='coolwarm', s=5) plt.clim(minz, maxz) elif color_by == 'sector': sector_vals = rp.txsector_beam.values[x_idx] plt.scatter(y_stck, x_stck, marker='+', c=sector_vals, s=5) plt.xlim(miny, maxy) plt.ylim(minx, maxx) if color_by != 'sector': plt.colorbar().set_label(zvar, rotation=270, labelpad=10) plt.title('{}: {}/{} colored by {}'.format(mode, xvar, yvar, color_by)) if start_time is not None or start_time is not None: self.fqpr.restore_subset() return fig