def mooring_array(od, Ymoor, Xmoor, **kwargs): """ Extract a mooring array section following the grid. Trajectories are great circle paths if coordinates are spherical. Parameters ---------- od: OceanDataset od that will be subsampled. Ymoor: 1D array_like, scalar Y coordinates of moorings. Xmoor: 1D array_like, scalar X coordinates of moorings. **kwargs: Keyword arguments for :py:func:`oceanspy.subsample.cutout`. Returns ------- od: OceanDataset Subsampled oceandataset. """ # Check _check_native_grid(od, 'mooring_array') # Convert variables to numpy arrays and make some check Ymoor = _check_range(od, Ymoor, 'Ymoor') Xmoor = _check_range(od, Xmoor, 'Xmoor') # Cutout if "YRange" not in kwargs: kwargs['YRange'] = Ymoor if "XRange" not in kwargs: kwargs['XRange'] = Xmoor if "add_Hbdr" not in kwargs: kwargs['add_Hbdr'] = True od = od.subsample.cutout(**kwargs) # Message print('Extracting mooring array.') # Unpack ds ds = od._ds # Useful variables YC = od._ds['YC'] XC = od._ds['XC'] R = od.parameters['rSphere'] shape = XC.shape Yindex = XC.dims.index('Y') Xindex = XC.dims.index('X') # Convert to cartesian if spherical if R is not None: x, y, z = _utils.spherical2cartesian(Y=Ymoor, X=Xmoor, R=R) else: x = Xmoor y = Ymoor z = _np.zeros(Ymoor.shape) # Create tree tree = od.create_tree(grid_pos='C') # Indexes of nearest grid points _, indexes = tree.query(_np.column_stack((x, y, z))) indexes = _np.unravel_index(indexes, shape) iY = _np.ndarray.tolist(indexes[Yindex]) iX = _np.ndarray.tolist(indexes[Xindex]) # Remove duplicates diff_iY = _np.diff(iY) diff_iX = _np.diff(iX) to_rem = [] for k, (diY, diX) in enumerate(zip(diff_iY, diff_iX)): if diY == 0 and diX == 0: to_rem = to_rem + [k] iY = _np.asarray([i for j, i in enumerate(iY) if j not in to_rem]) iX = _np.asarray([i for j, i in enumerate(iX) if j not in to_rem]) # Nearest coordinates near_Y = YC.isel(Y=_xr.DataArray(iY, dims=('tmp')), X=_xr.DataArray(iX, dims=('tmp'))).values near_X = XC.isel(Y=_xr.DataArray(iY, dims=('tmp')), X=_xr.DataArray(iX, dims=('tmp'))).values # Steps diff_iY = _np.fabs(_np.diff(iY)) diff_iX = _np.fabs(_np.diff(iX)) # Loop until all steps are 1 while any(diff_iY + diff_iX != 1): # Find where need to add grid points k = _np.argwhere(diff_iY + diff_iX != 1)[0][0] lat0 = near_Y[k] lon0 = near_X[k] lat1 = near_Y[k + 1] lon1 = near_X[k + 1] # Find grid point in the middle if R is not None: # SPHERICAL: follow great circle path dist = _great_circle((lat0, lon0), (lat1, lon1), radius=R).km # Divide dist by 2.1 to make sure that returns 3 points dist = dist / 2.1 this_Y, this_X, this_dists = _utils.great_circle_path( lat0, lon0, lat1, lon1, dist) # Cartesian coordinate of point in the middle x, y, z = _utils.spherical2cartesian(this_Y[1], this_X[1], R) else: # CARTESIAN: take the average x = (lon0 + lon1) / 2 y = (lat0 + lat1) / 2 z = 0 # Indexes of 3 nearest grid point _, indexes = tree.query(_np.column_stack((x, y, z)), k=3) indexes = _np.unravel_index(indexes, shape) new_iY = _np.ndarray.tolist(indexes[Yindex])[0] new_iX = _np.ndarray.tolist(indexes[Xindex])[0] # Extract just one point to_rem = [] for i, (this_iY, this_iX) in enumerate(zip(new_iY, new_iX)): check1 = (this_iY == iY[k] and this_iX == iX[k]) check2 = (this_iY == iY[k + 1] and this_iX == iX[k + 1]) if check1 or check2: to_rem = to_rem + [i] new_iY = _np.asarray( [i for j, i in enumerate(new_iY) if j not in to_rem])[0] new_iX = _np.asarray( [i for j, i in enumerate(new_iX) if j not in to_rem])[0] # Extract new lat and lon new_lat = YC.isel(Y=new_iY, X=new_iX).values new_lon = XC.isel(Y=new_iY, X=new_iX).values # Insert near_Y = _np.insert(near_Y, k + 1, new_lat) near_X = _np.insert(near_X, k + 1, new_lon) iY = _np.insert(iY, k + 1, new_iY) iX = _np.insert(iX, k + 1, new_iX) # Steps diff_iY = _np.fabs(_np.diff(iY)) diff_iX = _np.fabs(_np.diff(iX)) # New dimensions mooring = _xr.DataArray(_np.arange(len(iX)), dims=('mooring'), attrs={ 'long_name': 'index of mooring', 'units': 'none' }) y = _xr.DataArray(_np.arange(1), dims=('y'), attrs={ 'long_name': 'j-index of cell center', 'units': 'none' }) x = _xr.DataArray(_np.arange(1), dims=('x'), attrs={ 'long_name': 'i-index of cell corner', 'units': 'none' }) yp1 = _xr.DataArray(_np.arange(2), dims=('yp1'), attrs={ 'long_name': 'j-index of cell center', 'units': 'none' }) xp1 = _xr.DataArray(_np.arange(2), dims=('xp1'), attrs={ 'long_name': 'i-index of cell corner', 'units': 'none' }) # Transform indexes in DataArray iy = _xr.DataArray(_np.reshape(iY, (len(mooring), len(y))), coords={ 'mooring': mooring, 'y': y }, dims=('mooring', 'y')) ix = _xr.DataArray(_np.reshape(iX, (len(mooring), len(x))), coords={ 'mooring': mooring, 'x': x }, dims=('mooring', 'x')) iyp1 = _xr.DataArray(_np.stack((iY, iY + 1), 1), coords={ 'mooring': mooring, 'yp1': yp1 }, dims=('mooring', 'yp1')) ixp1 = _xr.DataArray(_np.stack((iX, iX + 1), 1), coords={ 'mooring': mooring, 'xp1': xp1 }, dims=('mooring', 'xp1')) # Initialize new dataset new_ds = _xr.Dataset( { 'mooring': mooring, 'Y': y.rename(y='Y'), 'Yp1': yp1.rename(yp1='Yp1'), 'X': x.rename(x='X'), 'Xp1': xp1.rename(xp1='Xp1') }, attrs=ds.attrs) # Loop and take out (looping is faster than apply to the whole dataset) all_vars = {var: new_ds[var] for var in new_ds} for var in ds.variables: if var in ['X', 'Y', 'Xp1', 'Yp1']: da = new_ds[var] da.attrs.update({ attr: ds[var].attrs[attr] for attr in ds[var].attrs if attr not in ['units', 'long_name'] }) continue elif not any(dim in ds[var].dims for dim in ['X', 'Y', 'Xp1', 'Yp1']): da = ds[var] else: for this_dims in [['Y', 'X'], ['Yp1', 'Xp1'], ['Y', 'Xp1'], ['Yp1', 'X']]: if set(this_dims).issubset(ds[var].dims): da = ds[var].isel({ dim: eval('i' + dim.lower(), {}, { 'iy': iy, 'ix': ix, 'iyp1': iyp1, 'ixp1': ixp1 }) for dim in this_dims }) da = da.drop(this_dims).rename( {dim.lower(): dim for dim in this_dims}) # Add to dictionary all_vars = {**all_vars, **{var: da}} new_ds = _xr.Dataset(all_vars) # Merge removes the attributes: put them back! new_ds.attrs = ds.attrs # Add distance dists = _np.zeros(near_Y.shape) for i in range(1, len(dists)): coord1 = (near_Y[i - 1], near_X[i - 1]) coord2 = (near_Y[i], near_X[i]) if R is not None: # SPHERICAL dists[i] = _great_circle(coord1, coord2, radius=R).km else: # CARTESIAN dists[i] = _np.sqrt((coord2[0] - coord1[0])**2 + (coord2[1] - coord1[1])**2) dists = _np.cumsum(dists) distance = _xr.DataArray( dists, coords={'mooring': mooring}, dims=('mooring'), attrs={'long_name': 'Distance from first mooring'}) if R is not None: # SPHERICAL distance.attrs['units'] = 'km' else: # CARTESIAN if 'units' in XC.attrs: distance.attrs['units'] = XC.attrs['units'] new_ds['mooring_dist'] = distance # Reset coordinates new_ds = new_ds.set_coords([coord for coord in ds.coords] + ['mooring_dist']) # Recreate od od._ds = new_ds od = od.set_grid_coords({'mooring': { 'mooring': -0.5 }}, add_midp=True, overwrite=False) # Create dist_midp _grid = od._grid dist_midp = _xr.DataArray(_grid.interp(od._ds['mooring_dist'], 'mooring'), attrs=od._ds['mooring_dist'].attrs) od = od.merge_into_oceandataset(dist_midp.rename('mooring_midp_dist')) od._ds = od._ds.set_coords([coord for coord in od._ds.coords] + ['mooring_midp_dist']) return od
def great_circle_path(lat1, lon1, lat2, lon2, delta_km=None, R=None): """ Generate a great circle trajectory specifying the distance resolution. Parameters ---------- lat1: scalar Latitude of vertex 1 [degrees N] lon1: scalar Longitude of vertex 1 [degrees E] lat2: scalar Latitude of vertex 2 [degrees N] lon2: scalar Longitude of vertex 2 [degrees E] delta_km: scalar, None Distance resolution [km] If None, only use vertices and return distance R: scalar, None Earth radius in km If None, use geopy default Returns ------- lats: 1D numpy.ndarray Great circle latitudes [degrees N] lons: 1D numpy.ndarray Great circle longitudes [degrees E] dist: 1D numpy.ndarray Distances from vertex 1 [km] References ---------- Converted to python and adapted from: `<https://ww2.mathworks.cn/matlabcentral/mlc-downloads/downloads/ submissions/8493/versions/2/previews/generate_great_circle_path.m /index.html?access_key=>`_ """ # Check parameters _check_instance( { "lat1": lat1, "lon1": lon1, "lat2": lat2, "lon2": lon2, "delta_km": delta_km, "R": R, }, { "lat1": "numpy.ScalarType", "lon1": "numpy.ScalarType", "lat2": "numpy.ScalarType", "lon2": "numpy.ScalarType", "delta_km": ["type(None)", "numpy.ScalarType"], "R": ["type(None)", "numpy.ScalarType"], }, ) # Check parameters if lat1 == lat2 and lon1 == lon2: raise ValueError("Vertexes are overlapping") if delta_km is not None and delta_km <= 0: raise ValueError("`delta_km` can not be zero or negative") # Check parameters if R is None: from geopy.distance import EARTH_RADIUS R = EARTH_RADIUS if delta_km is not None: # Convert to radians lat1 = _np.deg2rad(lat1) lon1 = _np.deg2rad(lon1) lat2 = _np.deg2rad(lat2) lon2 = _np.deg2rad(lon2) # Using the right-handed coordinate frame # with Z toward (lat,lon)=(0,0) and # Y toward (lat,lon)=(90,0), # the unit_radial of a (lat,lon) is given by: # [ cos(lat)*sin(lon) ] # unit_radial = [ sin(lat) ] # [ cos(lat)*cos(lon) ] unit_radial_1 = _np.array([ _np.cos(lat1) * _np.sin(lon1), _np.sin(lat1), _np.cos(lat1) * _np.cos(lon1), ]) unit_radial_2 = _np.array([ _np.cos(lat2) * _np.sin(lon2), _np.sin(lat2), _np.cos(lat2) * _np.cos(lon2), ]) # Define the vector that is normal to # both unit_radial_1 & unit_radial_2 normal_vec = _np.cross(unit_radial_1, unit_radial_2) unit_normal = normal_vec / _np.sqrt(_np.sum(normal_vec**2)) # Define the vector that is tangent to the great circle flight path at # (lat1,lon1) tangent_1_vec = _np.cross(unit_normal, unit_radial_1) unit_tangent_1 = tangent_1_vec / _np.sqrt(_np.sum(tangent_1_vec**2)) # Determine the total arc distance from 1 to 2 (radians) total_arc_angle_1_to_2 = _np.arccos(unit_radial_1.dot(unit_radial_2)) # Determine the set of arc angles to use # (approximately spaced by delta_km) angs2use = _np.linspace( 0, total_arc_angle_1_to_2, int(_np.ceil(total_arc_angle_1_to_2 / (delta_km / R))), ) # radians M = angs2use.size # unit_radials is a 3xM array of M unit radials term1 = _np.array([unit_radial_1]).transpose().dot(_np.ones((1, M))) term2 = _np.ones((3, 1)).dot(_np.array([_np.cos(angs2use)])) term3 = _np.array([unit_tangent_1]).transpose().dot(_np.ones((1, M))) term4 = _np.ones((3, 1)).dot(_np.array([_np.sin(angs2use)])) unit_radials = term1 * term2 + term3 * term4 # Convert to latitudes and longitudes lats = _np.rad2deg(_np.arcsin(unit_radials[1, :])) lons = _np.rad2deg(_np.arctan2(unit_radials[0, :], unit_radials[2, :])) else: # Use input lon and lat lats = _np.concatenate((_np.reshape(lat1, 1), _np.reshape(lat2, 1))) lons = _np.concatenate((_np.reshape(lon1, 1), _np.reshape(lon2, 1))) # Compute distance dists = _np.zeros(lons.shape) for i in range(1, len(lons)): coord1 = (lats[i - 1], lons[i - 1]) coord2 = (lats[i], lons[i]) dists[i] = _great_circle(coord1, coord2, radius=R).km dists = _np.cumsum(dists) return lats, lons, dists
def great_circle_path(lat1, lon1, lat2, lon2, delta_km=None, R=None): """ Generate a great circle trajectory specifying the distance resolution. Parameters ---------- lat1: scalar Latitude of vertex 1 [degrees N] lon1: scalar Longitude of vertex 1 [degrees E] lat2: scalar Latitude of vertex 2 [degrees N] lon2: scalar Longitude of vertex 2 [degrees E] delta_km: scalar, None Distance resolution [km] If None, only use vertices and return distance R: scalar Earth radius in km If None, use geopy default Returns ------- lats: 1D numpy.ndarray Great circle latitudes [degrees N] lons: 1D numpy.ndarray Great circle longitudes [degrees E] dist: 1D numpy.ndarray Distances from vertex 1 [km] References ---------- Converted to python and adapted from: https://ww2.mathworks.cn/matlabcentral/mlc-downloads/downloads/submissions/8493/versions/2/previews/generate_great_circle_path.m/index.html?access_key= """ # Check parameters if not isinstance(lat1, _np.ScalarType): raise TypeError('`lat1` must be scalar') if not isinstance(lon1, _np.ScalarType): raise TypeError('`lon1` must be scalar') if not isinstance(lat2, _np.ScalarType): raise TypeError('`lat2` must be scalar') if not isinstance(lon2, _np.ScalarType): raise TypeError('`lon2` must be scalar') if not isinstance(delta_km, (type(None), _np.ScalarType)): raise TypeError('`delta_km` must be scalar or None') if not isinstance(R, (_np.ScalarType, type(None))): raise TypeError('`R` must be scalar or None') if lat1 == lat2 and lon1 == lon2: raise TypeError('Vertexes are overlapping') if delta_km is not None and delta_km <= 0: raise TypeError('`delta_km` can not be zero or negative') # Check parameters if R is None: from geopy.distance import EARTH_RADIUS R = EARTH_RADIUS # Import packages from geopy.distance import great_circle as _great_circle if delta_km is not None: # Convert to radians lat1 = _np.deg2rad(lat1) lon1 = _np.deg2rad(lon1) lat2 = _np.deg2rad(lat2) lon2 = _np.deg2rad(lon2) # Using the right-handed coordinate frame with Z toward (lat,lon)=(0,0) and # Y toward (lat,lon)=(90,0), the unit_radial of a (lat,lon) is given by: # # [ cos(lat)*sin(lon) ] # unit_radial = [ sin(lat) ] # [ cos(lat)*cos(lon) ] unit_radial_1 = _np.array([ _np.cos(lat1) * _np.sin(lon1), _np.sin(lat1), _np.cos(lat1) * _np.cos(lon1) ]) unit_radial_2 = _np.array([ _np.cos(lat2) * _np.sin(lon2), _np.sin(lat2), _np.cos(lat2) * _np.cos(lon2) ]) # Define the vector that is normal to both unit_radial_1 & unit_radial_2 normal_vec = _np.cross(unit_radial_1, unit_radial_2) unit_normal = normal_vec / _np.sqrt(_np.sum(normal_vec**2)) # Define the vector that is tangent to the great circle flight path at # (lat1,lon1) tangent_1_vec = _np.cross(unit_normal, unit_radial_1) unit_tangent_1 = tangent_1_vec / _np.sqrt(_np.sum(tangent_1_vec**2)) # Determine the total arc distance from 1 to 2 total_arc_angle_1_to_2 = _np.arccos( unit_radial_1.dot(unit_radial_2)) # radians # Determine the set of arc angles to use # (approximately spaced by delta_km) angs2use = _np.linspace( 0, total_arc_angle_1_to_2, int(_np.ceil(total_arc_angle_1_to_2 / (delta_km / R)))) # radians M = angs2use.size # Now find the unit radials of the entire "trajectory" # [ cos(angs2use(m)) -sin(angs2use(m)) 0 ] [ 1 ] # unit_radial_m = [unit_radial_1 unit_tangent_1 unit_normal] * [ sin(angs2use(m)) cos(angs2use(m)) 0 ] * [ 0 ] # [ 0 0 1 ] [ 0 ] # equals # [ cos(angs2use(m)) ] # unit_radial_m = [unit_radial_1 unit_tangent_1 unit_normal] * [ sin(angs2use(m)) ] # [ 0 ] # equals # # unit_radial_m = [unit_radial_1*cos(angs2use(m)) + unit_tangent_1*sin(angs2use(m)) + 0] # # unit_radials is a 3xM array of M unit radials unit_radials = _np.array([unit_radial_1]).transpose().dot(_np.ones((1,M))) *\ _np.ones((3,1)).dot(_np.array([_np.cos(angs2use)])) +\ _np.array([unit_tangent_1]).transpose().dot(_np.ones((1,M))) *\ _np.ones((3,1)).dot(_np.array([_np.sin(angs2use)])) # Convert to latitudes and longitudes lats = _np.rad2deg(_np.arcsin(unit_radials[1, :])) lons = _np.rad2deg(_np.arctan2(unit_radials[0, :], unit_radials[2, :])) else: # Use input lon and lat lats = _np.concatenate((_np.reshape(lat1, 1), _np.reshape(lat2, 1))) lons = _np.concatenate((_np.reshape(lon1, 1), _np.reshape(lon2, 1))) # Compute distance dists = _np.zeros(lons.shape) for i in range(1, len(lons)): coord1 = (lats[i - 1], lons[i - 1]) coord2 = (lats[i], lons[i]) dists[i] = _great_circle(coord1, coord2, radius=R).km dists = _np.cumsum(dists) return lats, lons, dists
def mooring_array(ds, info, lats, lons, varList = None, depthRange = None, timeRange = None, timeFreq = None, sampMethod = 'snapshot', deep_copy = False): """ Extract a mooring array section avoiding interpolation. Sections are obtained following the grid. Every grid cell can have multiple moorings: 1 at the center (e.g., for Temp and S); 2 at the meridional boundaries (e.g., for V). Dimension Yv=['Yb', 'Yf'] 2 at the zonal boundaries (e.g., for U). Dimension Xu=['Xb', 'Xf'] 4 at the corners (e.g., for momVort3). Dimension XYg=['Xb Yb', 'Xb Yf', 'Xf Yb', 'Xf Yf'] Parameters ---------- ds: xarray.Dataset info: oceanspy.open_dataset._info lats: list Latitudes of moorings [degrees N]. lons: list Longitudes of moorings [degrees E] varList: list or None List of variables depthRange: list or None Depth limits. (e.g, [0, -float('Inf')]) timeRange: list or None Time limits. (e.g, ['2007-09-01T00', '2008-09-01T00']) timeFreq: str or None Time frequency. Available options are pandas Offset Aliases (e.g., '6H'): http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases sampMethod: {'snapshot', 'mean'} Downsampling method (only if timeFreq is not None) deep_copy: bool If True, deep copy ds and info Returns ------- ds: xarray.Dataset info: open_dataset._info """ # Deep copy if deep_copy: ds, info = _utils.deep_copy(ds, info) # Message print('Extracting mooring array') # Cutout ds, info = cutout(ds, info, varList = varList, latRange = [min(lats), max(lats)], lonRange = [min(lons), max(lons)], depthRange = depthRange, timeRange = timeRange, timeFreq = timeFreq, sampMethod = sampMethod) # Add missing variables varList = ['X', 'Y', 'Xp1', 'Yp1'] ds, info = _utils.compute_missing_variables(ds, info, varList) # Define variables X = ds['X']; Y = ds['Y']; Xp1 = ds['Xp1']; Yp1 = ds['Yp1'] # Find nearest coordinates near_lons = [X.sel(X=this_lons, method='nearest').values for this_lons in lons] near_lats = [Y.sel(Y=this_lats, method='nearest').values for this_lats in lats] # Remove duplicates diff_lons = _np.diff(near_lons); diff_lats = _np.diff(near_lats) to_rem = [] for k, (dlon, dlat) in enumerate(zip(diff_lons, diff_lats)): if dlon==0 and dlat==0: to_rem = to_rem + [k] near_lons = [i for j, i in enumerate(near_lons) if j not in to_rem] near_lats = [i for j, i in enumerate(near_lats) if j not in to_rem] # Add points using linear fit # Could it generate infinite loops? k=0 diff_lons = _np.diff(near_lons); diff_lats = _np.diff(near_lats) while k!=len(diff_lons)-1: K = k diff_lons = _np.diff(near_lons); diff_lats = _np.diff(near_lats) for k, (dlon, dlat) in enumerate(zip(diff_lons, diff_lats)): if k<K: continue # Additional points more_lons= X.where(_xr.ufuncs.logical_and(X>=_np.min(near_lons[k:k+2]), X<=_np.max(near_lons[k:k+2])), drop=True).values more_lats= Y.where(_xr.ufuncs.logical_and(Y>=_np.min(near_lats[k:k+2]), Y<=_np.max(near_lats[k:k+2])), drop=True).values # Sort if dlon<0: more_lons = -_np.sort(-more_lons) if dlat<0: more_lats = -_np.sort(-more_lats) if dlon!=0 and dlat!=0: if len(more_lons)>2 or len(more_lats)>2: x0=more_lons[0]; x1=more_lons[-1] y0=more_lats[0]; y1=more_lats[-1] if len(more_lats)>len(more_lons): new_lons=(more_lats-y0)*(x1-x0)/(y1-y0)+x0 new_lats=more_lats else: new_lons=more_lons new_lats=(more_lons-x0)*(y1-y0)/(x1-x0)+y0 # Find nearest coordinates new_lons = [X.sel(X=this_lons, method='nearest').values for this_lons in new_lons][1:-1] new_lats = [Y.sel(Y=this_lats, method='nearest').values for this_lats in new_lats][1:-1] # Remove duplicates diff_new_lons = _np.diff(new_lons); diff_new_lats = _np.diff(new_lats) to_rem = [] for kk, (dnlon, dnlat) in enumerate(zip(diff_new_lons, diff_new_lats)): if dnlon==0 and dnlat==0: to_rem = to_rem + [kk] new_lons = [i for j, i in enumerate(new_lons) if j not in to_rem] new_lats = [i for j, i in enumerate(new_lats) if j not in to_rem] # Insert near_lons = _np.insert(near_lons,k+1,new_lons) near_lats = _np.insert(near_lats,k+1,new_lats) diff_lons = _np.diff(near_lons); diff_lats = _np.diff(near_lats) break # Add points where there is a gap diff_lons = _np.diff(near_lons); diff_lats = _np.diff(near_lats) add_lons = [] add_lats = [] for k, (dlon, dlat) in enumerate(zip(diff_lons, diff_lats)): add_lons = add_lons + [float(near_lons[k])] add_lats = add_lats + [float(near_lats[k])] # Additional points more_lons= X.where(_xr.ufuncs.logical_and(X>=_np.min(near_lons[k:k+2]), X<=_np.max(near_lons[k:k+2])), drop=True).values more_lats= Y.where(_xr.ufuncs.logical_and(Y>=_np.min(near_lats[k:k+2]), Y<=_np.max(near_lats[k:k+2])), drop=True).values # Sort if dlon<0: more_lons = -_np.sort(-more_lons) if dlat<0: more_lats = -_np.sort(-more_lats) # Meridional and Zonal if len(more_lons)==1 and len(more_lats)>2: # Meridional for _, thislat in enumerate(more_lats[1:-1]): add_lons = add_lons + [float(more_lons)] add_lats = add_lats + [float(thislat)] elif len(more_lats)==1 and len(more_lons)>2: # Zonal for _, thislon in enumerate(more_lons[1:-1]): add_lons = add_lons + [float(thislon)] add_lats = add_lats + [float(more_lats)] # Diagonal if dlon!=0 and dlat!=0: if len(more_lons)==2 and len(more_lats)==2: x_diff = near_lons[k+1] - near_lons[k] y_diff = near_lats[k+1] - near_lats[k] den = _np.sqrt(y_diff**2 + x_diff**2) dist1 = _np.fabs(y_diff*near_lons[k+1] - x_diff*near_lats[k] + near_lons[k+1]*near_lats[k] - near_lats[k+1]*near_lons[k])/den dist2 = _np.fabs(y_diff*near_lons[k] - x_diff*near_lats[k+1] + near_lons[k+1]*near_lats[k] - near_lats[k+1]*near_lons[k])/den if dist1<dist2: add_lons = add_lons + [float(near_lons[k+1])] add_lats = add_lats + [float(near_lats[k])] else: add_lons = add_lons + [float(near_lons[k])] add_lats = add_lats + [float(near_lats[k+1])] else: raise ValueError('There is a bug: At this point of the function the gap should be smaller!') # Add add_lons = add_lons + [float(near_lons[k+1])] add_lats = add_lats + [float(near_lats[k+1])] # Create dimensions Xc = _np.asarray(add_lons) Yc = _np.asarray(add_lats) Xb = _np.asarray([Xp1.sel(Xp1=this_lons, method='bfill').values for this_lons in Xc]) Xf = _np.asarray([Xp1.sel(Xp1=this_lons, method='ffill').values for this_lons in Xc]) Yb = _np.asarray([Yp1.sel(Yp1=this_lats, method='bfill').values for this_lats in Yc]) Yf = _np.asarray([Yp1.sel(Yp1=this_lats, method='ffill').values for this_lats in Yc]) cell = _np.arange(len(Xc)) Xu = ['Xb', 'Xf'] Yv = ['Yb', 'Yf'] XYg = ['Xb Yb', 'Xb Yf', 'Xf Yb', 'Xf Yf'] # Create DataArrays cell = _xr.DataArray(cell, coords={'cell': cell}, dims=('cell'), attrs={'long_name': 'cells where moorings are located (adjacent cells have consecutive numbers)'}) Xu = _xr.DataArray(Xu, coords={'Xu': Xu}, dims=('Xu'), attrs={'long_name': 'X coordinates of moorings on cell boundaries'}) Yv = _xr.DataArray(Yv, coords={'Yv': Yv}, dims=('Yv'), attrs={'long_name': 'Y coordinates of moorings on cell boundaries'}) XYg = _xr.DataArray(XYg, coords={'XYg': XYg}, dims=('XYg'), attrs={'long_name': 'XY coordinates of moorings on cell corners'}) Xc = _xr.DataArray(Xc, coords={'cell': cell}, dims=('cell'), attrs={'long_name': 'longitude of moorings on cell center', 'units': 'degrees_east'}) Yc = _xr.DataArray(Yc, coords={'cell': cell}, dims=('cell'), attrs={'long_name': 'latitude of moorings on cell center', 'units': 'degrees_north'}) Xb = _xr.DataArray(Xb, coords={'cell': cell}, dims=('cell'), attrs={'long_name': 'longitude of moorings on cell boundaries (bfill of Xc)', 'units': 'degrees_east'}) Xf = _xr.DataArray(Xf, coords={'cell': cell}, dims=('cell'), attrs={'long_name': 'longitude of moorings on cell boundaries (ffill of Xc)', 'units': 'degrees_east'}) Yb = _xr.DataArray(Yb, coords={'cell': cell}, dims=('cell'), attrs={'long_name': 'latitude of moorings on cell boundaries (bfill of Yc)', 'units': 'degrees_north'}) Yf = _xr.DataArray(Yf, coords={'cell': cell}, dims=('cell'), attrs={'long_name': 'latitude of moorings on cell boundaries (ffill of Yc)', 'units': 'degrees_north'}) # Add to dataset coords = _xr.Dataset({'cell': cell, 'Xu': Xu, 'Yv': Yv, 'XYg': XYg, 'Xc': Xc, 'Yc': Yc, 'Xb': Xb, 'Xf': Xf, 'Yb': Yb, 'Yf': Yf}) ds = _xr.merge([ds, coords]) # Extract paths for var in [var for var in ds.variables if var not in ds.dims]: if all(dim in ds[var].dims for dim in ['X', 'Y']): ds[var] = ds[var].sel(X = Xc, Y = Yc) elif all(dim in ds[var].dims for dim in ['Xp1', 'Y']): var_x0 = ds[var].sel(Xp1 = Xb, Y = Yc).expand_dims('Xu') var_x1 = ds[var].sel(Xp1 = Xf, Y = Yc).expand_dims('Xu') ds[var] = _xr.concat([var_x0, var_x1],dim='Xu') elif all(dim in ds[var].dims for dim in ['X', 'Yp1']): var_y0 = ds[var].sel(X = Xc, Yp1 = Yb).expand_dims('Yv') var_y1 = ds[var].sel(X = Xc, Yp1 = Yf).expand_dims('Yv') ds[var] = _xr.concat([var_y0, var_y1],dim='Yv') elif all(dim in ds[var].dims for dim in ['Xp1', 'Yp1']): var_x0_y0 = ds[var].sel(Xp1 = Xb, Yp1 = Yb).expand_dims('XYg') var_x1_y0 = ds[var].sel(Xp1 = Xb, Yp1 = Yf).expand_dims('XYg') var_x0_y1 = ds[var].sel(Xp1 = Xf, Yp1 = Yb).expand_dims('XYg') var_x1_y1 = ds[var].sel(Xp1 = Xf, Yp1 = Yf).expand_dims('XYg') ds[var] = _xr.concat([var_x0_y0, var_x1_y0, var_x0_y1, var_x1_y1],dim='XYg') elif 'Xp1' in ds[var].dims: var_x0 = ds[var].isel(Xp1 = Xb).expand_dims('Xu') var_x1 = ds[var].isel(Xp1 = Xf).expand_dims('Xu') ds[var] = _xr.concat([var_x0, var_x1],dim='Xu') elif 'Yp1' in ds[var].dims: var_y0 = ds[var].isel(Yp1 = Yb).expand_dims('Yv') var_y1 = ds[var].isel(Yp1 = Yf).expand_dims('Yv') ds[var] = _xr.concat([var_y0, var_y1],dim='Yv') # Drop old variables for var in ds.variables: if var in ['X', 'Y', 'Xp1', 'Yp1']: ds = ds.drop(var) for dim in ds.dims: if dim in ['X', 'Y', 'Xp1', 'Yp1']: ds = ds.drop(dim) # Add distances from geopy.distance import great_circle as _great_circle dist = _np.zeros(Xc.shape) for i in range(1,len(Xc)): coord1 = (Yc[i-1],Xc[i-1]) coord2 = (Yc[i] ,Xc[i]) dist[i] = _great_circle(coord1,coord2).km ds['dist_array'] = _xr.DataArray(_np.cumsum(dist), coords={'cell': cell}, dims=('cell'), attrs={'long_name': 'Cumulative distance from mooring 0', 'units': 'km'}) # Update info info.grid = 'Mooring array' for name in ['cell', 'Xu', 'Yv', 'XYg', 'Xc', 'Yc', 'Xb', 'Xf', 'Yb', 'Yf', 'dist_array']: info.var_names[name] = name return _utils.reorder_ds(ds), info
def great_circle_path(lat1, lon1, lat2, lon2, delta_km): """ Generate a great circle trajectory specifying the resolution. Parameters ---------- lat1: number Latitude of vertex 1 [degrees N] lon1: number Longitude of vertex 1 [degrees E] lat2: number Latitude of vertex 2 [degrees N] lon2: number Longitude of vertex 2 [degrees E] delta_km: number Horizontal resolution [km] Returns ------- lats: vector Great circle latitudes [degrees N] lons: vector Great circle longitudes [degrees E] dist: vector Distances from vertex 1 [km] """ from geopy.distance import great_circle as _great_circle from geopy.distance import EARTH_RADIUS as _EARTH_RADIUS # Convert to radians lat1 = _np.deg2rad(lat1) lon1 = _np.deg2rad(lon1) lat2 = _np.deg2rad(lat2) lon2 = _np.deg2rad(lon2) # Using the right-handed coordinate frame with Z toward (lat,lon)=(0,0) and # Y toward (lat,lon)=(90,0), the unit_radial of a (lat,lon) is given by: # # [ cos(lat)*sin(lon) ] # unit_radial = [ sin(lat) ] # [ cos(lat)*cos(lon) ] unit_radial_1 = _np.array([ _np.cos(lat1) * _np.sin(lon1), _np.sin(lat1), _np.cos(lat1) * _np.cos(lon1) ]) unit_radial_2 = _np.array([ _np.cos(lat2) * _np.sin(lon2), _np.sin(lat2), _np.cos(lat2) * _np.cos(lon2) ]) # Define the vector that is normal to both unit_radial_1 & unit_radial_2 normal_vec = _np.cross(unit_radial_1, unit_radial_2) unit_normal = normal_vec / _np.sqrt(_np.sum(normal_vec**2)) # Define the vector that is tangent to the great circle flight path at # (lat1,lon1) tangent_1_vec = _np.cross(unit_normal, unit_radial_1) unit_tangent_1 = tangent_1_vec / _np.sqrt(_np.sum(tangent_1_vec**2)) # Determine the total arc distance from 1 to 2 total_arc_angle_1_to_2 = _np.arccos( unit_radial_1.dot(unit_radial_2)) # radians # Determine the set of arc angles to use # (approximately spaced by delta_km) R0 = _EARTH_RADIUS # km, radius of a circle having approximately the surface area of the earth angs2use = _np.linspace(0, total_arc_angle_1_to_2, _np.ceil(total_arc_angle_1_to_2 / (delta_km / R0))) # radians M = angs2use.size # Now find the unit radials of the entire "trajectory" # [ cos(angs2use(m)) -sin(angs2use(m)) 0 ] [ 1 ] # unit_radial_m = [unit_radial_1 unit_tangent_1 unit_normal] * [ sin(angs2use(m)) cos(angs2use(m)) 0 ] * [ 0 ] # [ 0 0 1 ] [ 0 ] # equals # [ cos(angs2use(m)) ] # unit_radial_m = [unit_radial_1 unit_tangent_1 unit_normal] * [ sin(angs2use(m)) ] # [ 0 ] # equals # # unit_radial_m = [unit_radial_1*cos(angs2use(m)) + unit_tangent_1*sin(angs2use(m)) + 0] # # unit_radials is a 3xM array of M unit radials unit_radials = _np.array([unit_radial_1]).transpose().dot(_np.ones((1,M))) *\ _np.ones((3,1)).dot(_np.array([_np.cos(angs2use)])) +\ _np.array([unit_tangent_1]).transpose().dot(_np.ones((1,M))) *\ _np.ones((3,1)).dot(_np.array([_np.sin(angs2use)])) # Convert to latitudes and longitudes lats = _np.rad2deg(_np.arcsin(unit_radials[1, :])) lons = _np.rad2deg(_np.arctan2(unit_radials[0, :], unit_radials[2, :])) # Compute distance dists = _np.zeros(lons.shape) for i in range(1, len(lons)): coord1 = (lats[i - 1], lons[i - 1]) coord2 = (lats[i], lons[i]) dists[i] = _great_circle(coord1, coord2).km dists = _np.cumsum(dists) return lats, lons, dists