Source code for mpas_tools.ocean.transects

import xarray
import numpy


[docs] def find_transect_levels_and_weights(dsTransect, layerThickness, bottomDepth, maxLevelCell, zTransect=None): """ Construct a vertical coordinate for a transect produced by :py:func:`mpas_tools.viz.transects.find_transect_cells_and_weights()`, then break each resulting quadrilateral into 2 triangles that can later be visualized with functions like ``tripcolor`` and ``tricontourf``. Also, compute interpolation weights such that observations at points on the original transect and with vertical coordinate ``transectZ`` can be bilinearly interpolated to the nodes of the transect. Parameters ---------- dsTransect : xarray.Dataset A dataset that defines nodes of the transect, the results of calling :py:func:`mpas_tools.viz.transects.find_transect_cells_and_weights()` layerThickness : xarray.DataArray layer thicknesses on the MPAS mesh bottomDepth : xarray.DataArray the (positive down) depth of the seafloor on the MPAS mesh maxLevelCell : xarray.DataArray the vertical zero-based index of the bathymetry on the MPAS mesh zTransect : xarray.DataArray, optional the z coordinate of the transect (1D or 2D). If 2D, it must have the same along-transect dimension as the lon and lat passed to :py:func:`mpas_tools.viz.transects.find_transect_cells_and_weights()` Returns ------- dsTransectTriangles : xarray.Dataset A dataset that contains nodes and triangles that make up a 2D transect. For convenience in visualization, the quadrilaterals of each cell making up the transect have been divided into an upper and lower triangle. The nodes of the triangles are completely independent of one another, allowing for potential jumps in fields values between nodes of different triangles that are at the same location. This is convenient, for example, when visualizing data with constant values within each MPAS cell. There are ``nTransectTriangles = 2*nTransectCells`` triangles, each with ``nTriangleNodes = 3`` nodes, where ``nTransectCells`` is the number of valid transect cells (quadrilaterals) that are above the MPAS-Ocean bathymetry. In addition to the variables and coordinates in ``dsTransect``, the output dataset contains: - nodeHorizBoundsIndices: which of the ``nHorizBounds = 2`` bounds of a horizontal transect segment a given node is on - segmentIndices: the transect segment of a given triangle - cellIndices: the MPAS-Ocean cell of a given triangle - levelIndices: the MPAS-Ocean vertical level of a given triangle - zTransectNode: the vertical height of each triangle node - ssh, zSeaFloor: the sea-surface height and sea-floor height at each node of each transect segment - interpCellIndices, interpLevelIndices: the MPAS-Ocean cells and levels from which the value at a given triangle node are interpolated. This can involve up to ``nWeights = 12`` different cells and levels. - interpCellWeights: the weight to multiply each field value by to perform interpolation to a triangle node. - transectInterpVertIndices, transectInterpVertWeights - if ``zTransect`` is not ``None``, vertical indices and weights for interpolating from the original transect grid to MPAS-Ocean transect nodes. Interpolation of a DataArray from MPAS cells and levels to transect triangle nodes can be performed with ``interp_mpas_to_transect_triangle_nodes()``. Similarly, interpolation of a DataArray (e.g. an observation) from the original transect grid to transect triangle nodes can be performed with ``interp_transect_grid_to_transect_triangle_nodes()`` To visualize constant values on MPAS cells, a field can be sampled at indices ``nCells=cellIndices`` and ``nVertLevels=levelIndices``. If a smoother visualization is desired, bilinear interpolation can be performed by first sampling the field at indices ``nCells=interpCellIndices`` and ``nVertLevels=interpLevelIndices`` and then multiplying by ``interpCellWeights`` and summing over ``nWeights``. """ dsTransectCells = _get_transect_cells_and_nodes( dsTransect, layerThickness, bottomDepth, maxLevelCell) dsTransectTriangles = _transect_cells_to_triangles(dsTransectCells) if zTransect is not None: dsTransectTriangles = _add_vertical_interpolation_of_transect_points( dsTransectTriangles, zTransect) return dsTransectTriangles
[docs] def interp_mpas_to_transect_triangles(dsTransectTriangles, da): """ Interpolate a 3D (``nCells`` by ``nVertLevels``) MPAS-Ocean DataArray to transect nodes with constant values in each MPAS cell Parameters ---------- dsTransectTriangles : xarray.Dataset A dataset that defines triangles making up an MPAS-Ocean transect, the results of calling ``find_transect_levels_and_weights()`` da : xarray.DataArray An MPAS-Ocean 3D field with dimensions `nCells`` and ``nVertLevels`` (possibly among others) Returns ------- daNodes : xarray.DataArray The data array interpolated to transect nodes with dimensions ``nTransectTriangles`` and ``nTriangleNodes`` (in addition to whatever dimensions were in ``da`` besides ``nCells`` and ``nVertLevels``) """ cellIndices = dsTransectTriangles.cellIndices levelIndices = dsTransectTriangles.levelIndices daNodes = da.isel(nCells=cellIndices, nVertLevels=levelIndices) return daNodes
[docs] def interp_mpas_to_transect_triangle_nodes(dsTransectTriangles, da): """ Interpolate a 3D (``nCells`` by ``nVertLevels``) MPAS-Ocean DataArray to transect nodes, linearly interpolating fields between the closest neighboring cells Parameters ---------- dsTransectTriangles : xarray.Dataset A dataset that defines triangles making up an MPAS-Ocean transect, the results of calling ``find_transect_levels_and_weights()`` da : xarray.DataArray An MPAS-Ocean 3D field with dimensions `nCells`` and ``nVertLevels`` (possibly among others) Returns ------- daNodes : xarray.DataArray The data array interpolated to transect nodes with dimensions ``nTransectTriangles`` and ``nTriangleNodes`` (in addition to whatever dimensions were in ``da`` besides ``nCells`` and ``nVertLevels``) """ interpCellIndices = dsTransectTriangles.interpCellIndices interpLevelIndices = dsTransectTriangles.interpLevelIndices interpCellWeights = dsTransectTriangles.interpCellWeights da = da.isel(nCells=interpCellIndices, nVertLevels=interpLevelIndices) daNodes = (da*interpCellWeights).sum(dim='nWeights') return daNodes
[docs] def interp_transect_grid_to_transect_triangle_nodes(dsTransectTriangles, da): """ Interpolate a DataArray on the original transect grid to triangle nodes on the MPAS-Ocean transect. Parameters ---------- dsTransectTriangles : xarray.Dataset A dataset that defines triangles making up an MPAS-Ocean transect, the results of calling ``find_transect_levels_and_weights()`` da : xarray.DataArray An field on the original triangle mesh Returns ------- daNodes : xarray.DataArray The data array interpolated to transect nodes with dimensions ``nTransectTriangles`` and ``nTriangleNodes`` """ horizDim = dsTransectTriangles.dTransect.dims[0] zTransect = dsTransectTriangles.zTransect vertDim = None for dim in zTransect.dims: if dim != horizDim: vertDim = dim horizIndices = dsTransectTriangles.transectIndicesOnHorizNode horizWeights = dsTransectTriangles.transectWeightsOnHorizNode segmentIndices = dsTransectTriangles.segmentIndices nodeHorizBoundsIndices = dsTransectTriangles.nodeHorizBoundsIndices horizIndices = horizIndices.isel(nSegments=segmentIndices, nHorizBounds=nodeHorizBoundsIndices) horizWeights = horizWeights.isel(nSegments=segmentIndices, nHorizBounds=nodeHorizBoundsIndices) vertIndices = dsTransectTriangles.transectInterpVertIndices vertWeights = dsTransectTriangles.transectInterpVertWeights kwargs00 = {horizDim: horizIndices, vertDim: vertIndices} kwargs01 = {horizDim: horizIndices, vertDim: vertIndices+1} kwargs10 = {horizDim: horizIndices+1, vertDim: vertIndices} kwargs11 = {horizDim: horizIndices+1, vertDim: vertIndices+1} daNodes = (horizWeights * vertWeights * da.isel(**kwargs00) + horizWeights * (1.0 - vertWeights) * da.isel(**kwargs01) + (1.0 - horizWeights) * vertWeights * da.isel(**kwargs10) + (1.0 - horizWeights) * (1.0 - vertWeights) * da.isel(**kwargs11)) mask = numpy.logical_and(horizIndices != -1, vertIndices != -1) daNodes = daNodes.where(mask) return daNodes
[docs] def get_outline_segments(dsTransectTriangles, epsilon=1e-3): """Get a set of line segments that outline the given transect""" nSegments = dsTransectTriangles.sizes['nSegments'] dSeaFloor = dsTransectTriangles.dNode.values zSeaFloor = dsTransectTriangles.zSeaFloor.values ssh = dsTransectTriangles.ssh.values seaFloorJumps = numpy.abs(dSeaFloor[0:-1, 1] - dSeaFloor[1:, 0]) < epsilon nSeaFloorJumps = numpy.count_nonzero(seaFloorJumps) nSurface = nSegments nSeaFloor = nSegments + nSeaFloorJumps nLandJumps = nSegments - nSeaFloorJumps nOutline = nSurface + nSeaFloor + 2*nLandJumps d = numpy.zeros((nOutline, 2)) z = numpy.zeros((nOutline, 2)) d[0:nSegments, :] = dSeaFloor z[0:nSegments, :] = ssh d[nSegments:2*nSegments, :] = dSeaFloor z[nSegments:2*nSegments, :] = zSeaFloor dSeaFloorJump = numpy.vstack((dSeaFloor[0:-1, 1], dSeaFloor[1:, 0])).T zSeaFloorJump = numpy.vstack((zSeaFloor[0:-1, 1], zSeaFloor[1:, 0])).T slc = slice(2*nSegments, 2*nSegments+nSeaFloorJumps) d[slc, :] = dSeaFloorJump[seaFloorJumps, :] z[slc, :] = zSeaFloorJump[seaFloorJumps, :] landJumps1 = numpy.ones(nSegments, bool) landJumps1[1:] = numpy.logical_not(seaFloorJumps) landJumps2 = numpy.ones(nSegments, bool) landJumps2[0:-1] = numpy.logical_not(seaFloorJumps) offset = 2*nSegments+nSeaFloorJumps slc = slice(offset, offset + nLandJumps) d[slc, 0] = dSeaFloor[landJumps1, 0] d[slc, 1] = dSeaFloor[landJumps1, 0] z[slc, 0] = ssh[landJumps1, 0] z[slc, 1] = zSeaFloor[landJumps1, 0] offset = 2*nSegments+nSeaFloorJumps+nLandJumps slc = slice(offset, offset + nLandJumps) d[slc, 0] = dSeaFloor[landJumps2, 1] d[slc, 1] = dSeaFloor[landJumps2, 1] z[slc, 0] = ssh[landJumps2, 1] z[slc, 1] = zSeaFloor[landJumps2, 1] d = d.T z = z.T return d, z
def _get_transect_cells_and_nodes(dsTransect, layerThickness, bottomDepth, maxLevelCell): if 'Time' in layerThickness.dims: raise ValueError('Please select a single time level in layerThickness.') dsTransect = dsTransect.rename({'nBounds': 'nHorizBounds'}) zTop, zMid, zBot, ssh, zSeaFloor, interpCellIndices, interpCellWeights = \ _get_vertical_coordinate(dsTransect, layerThickness, bottomDepth, maxLevelCell) nVertLevels = layerThickness.sizes['nVertLevels'] levelIndices = xarray.DataArray(data=numpy.arange(2*nVertLevels)//2, dims='nHalfLevels') cellMask = (levelIndices <= maxLevelCell).transpose('nCells', 'nHalfLevels') dsTransectCells = _add_valid_cells_and_levels( dsTransect, dsTransect.horizCellIndices.values, levelIndices.values, cellMask.values) # transect cells are made up of half-levels, and each half-level has a top # and bottom interface, so we need 4 interfaces per MPAS level interpCellIndices, interpLevelIndices, interpCellWeights = \ _get_interp_indices_and_weights(layerThickness, maxLevelCell, interpCellIndices, interpCellWeights) levelIndex, tempIndex = numpy.meshgrid(numpy.arange(nVertLevels), numpy.arange(2), indexing='ij') levelIndex = xarray.DataArray(data=levelIndex.ravel(), dims='nHalfLevels') tempIndex = xarray.DataArray(data=tempIndex.ravel(), dims='nHalfLevels') zTop = xarray.concat((zTop, zMid), dim='nTemp') zTop = zTop.isel(nVertLevels=levelIndex, nTemp=tempIndex) zBot = xarray.concat((zMid, zBot), dim='nTemp') zBot = zBot.isel(nVertLevels=levelIndex, nTemp=tempIndex) zInterface = xarray.concat((zTop, zBot), dim='nVertBounds') segmentIndices = dsTransectCells.segmentIndices halfLevelIndices = dsTransectCells.halfLevelIndices dsTransectCells['interpCellIndices'] = interpCellIndices.isel( nSegments=segmentIndices, nHalfLevels=halfLevelIndices) dsTransectCells['interpLevelIndices'] = interpLevelIndices.isel( nSegments=segmentIndices, nHalfLevels=halfLevelIndices) dsTransectCells['interpCellWeights'] = interpCellWeights.isel( nSegments=segmentIndices, nHalfLevels=halfLevelIndices) dsTransectCells['zTransectNode'] = zInterface.isel( nSegments=segmentIndices, nHalfLevels=halfLevelIndices) dsTransectCells['ssh'] = ssh dsTransectCells['zSeaFloor'] = zSeaFloor dims = ['nSegments', 'nTransectCells', 'nHorizBounds', 'nVertBounds', 'nHorizWeights', 'nWeights'] for dim in dsTransectCells.dims: if dim not in dims: dims.insert(0, dim) dsTransectCells = dsTransectCells.transpose(*dims) return dsTransectCells def _get_vertical_coordinate(dsTransect, layerThickness, bottomDepth, maxLevelCell): nVertLevels = layerThickness.sizes['nVertLevels'] levelIndices = xarray.DataArray(data=numpy.arange(nVertLevels), dims='nVertLevels') cellMask = (levelIndices <= maxLevelCell).transpose('nCells', 'nVertLevels') ssh = -bottomDepth + layerThickness.sum(dim='nVertLevels') interpCellIndices = dsTransect.interpHorizCellIndices interpCellWeights = dsTransect.interpHorizCellWeights interpMask = numpy.logical_and(interpCellIndices > 0, cellMask.isel(nCells=interpCellIndices)) interpCellWeights = interpMask*interpCellWeights weightSum = interpCellWeights.sum(dim='nHorizWeights') cellIndices = dsTransect.horizCellIndices validCells = cellMask.isel(nCells=cellIndices) _, validWeights = xarray.broadcast(interpCellWeights, validCells) interpCellWeights = (interpCellWeights/weightSum).where(validWeights) layerThicknessTransect = layerThickness.isel(nCells=interpCellIndices) layerThicknessTransect = (layerThicknessTransect*interpCellWeights).sum( dim='nHorizWeights') interpMask = maxLevelCell.isel(nCells=interpCellIndices) >= 0 interpHorizCellWeights = interpMask*dsTransect.interpHorizCellWeights weightSum = interpHorizCellWeights.sum(dim='nHorizWeights') interpHorizCellWeights = \ (interpHorizCellWeights/weightSum).where(interpMask) sshTransect = ssh.isel(nCells=interpCellIndices) sshTransect = (sshTransect*interpHorizCellWeights).sum(dim='nHorizWeights') zBot = sshTransect - layerThicknessTransect.cumsum(dim='nVertLevels') zTop = zBot + layerThicknessTransect zMid = 0.5*(zTop + zBot) zSeaFloor = sshTransect - layerThicknessTransect.sum(dim='nVertLevels') return zTop, zMid, zBot, sshTransect, zSeaFloor, interpCellIndices, \ interpCellWeights def _add_valid_cells_and_levels(dsTransect, cellIndices, levelIndices, cellMask): dims = ('nTransectCells',) CellIndices, LevelIndices = numpy.meshgrid(cellIndices, levelIndices, indexing='ij') mask = numpy.logical_and(CellIndices >= 0, cellMask[cellIndices, :]) SegmentIndices, HalfLevelIndices = \ numpy.meshgrid(numpy.arange(len(cellIndices)), numpy.arange(len(levelIndices)), indexing='ij') segmentIndices = xarray.DataArray(data=SegmentIndices[mask], dims=dims) dsTransectCells = dsTransect dsTransectCells['cellIndices'] = (dims, CellIndices[mask]) dsTransectCells['levelIndices'] = (dims, LevelIndices[mask]) dsTransectCells['segmentIndices'] = segmentIndices dsTransectCells['halfLevelIndices'] = (dims, HalfLevelIndices[mask]) return dsTransectCells def _get_interp_indices_and_weights(layerThickness, maxLevelCell, interpCellIndices, interpCellWeights): interpCellIndices = interpCellIndices.rename({'nHorizWeights': 'nWeights'}) interpCellWeights = interpCellWeights.rename({'nHorizWeights': 'nWeights'}) nVertLevels = layerThickness.sizes['nVertLevels'] nHalfLevels = 2*nVertLevels nVertBounds = 2 interpMaxLevelCell = maxLevelCell.isel(nCells=interpCellIndices) levelIndices = xarray.DataArray( data=numpy.arange(nHalfLevels)//2, dims='nHalfLevels') valid = levelIndices <= interpMaxLevelCell topLevelIndices = -1*numpy.ones((nHalfLevels, nVertBounds), int) topLevelIndices[1:, 0] = numpy.arange(nHalfLevels-1)//2 topLevelIndices[:, 1] = numpy.arange(nHalfLevels)//2 topLevelIndices = xarray.DataArray( data=topLevelIndices, dims=('nHalfLevels', 'nVertBounds')) interpCellIndices, topLevelIndices = \ xarray.broadcast(interpCellIndices, topLevelIndices) topLevelIndices = topLevelIndices.where(valid, -1) botLevelIndices = numpy.zeros((nHalfLevels, nVertBounds), int) botLevelIndices[:, 0] = numpy.arange(nHalfLevels)//2 botLevelIndices[:, 1] = numpy.arange(1, nHalfLevels+1)//2 botLevelIndices = xarray.DataArray( data=botLevelIndices, dims=('nHalfLevels', 'nVertBounds')) _, botLevelIndices = xarray.broadcast(interpCellIndices, botLevelIndices) botLevelIndices = botLevelIndices.where(valid, -1) botLevelIndices = numpy.minimum(botLevelIndices, interpMaxLevelCell) interpLevelIndices = xarray.concat((topLevelIndices, botLevelIndices), dim='nWeights') topHalfLevelThickness = 0.5*layerThickness.isel( nCells=interpCellIndices, nVertLevels=topLevelIndices) topHalfLevelThickness = topHalfLevelThickness.where(topLevelIndices >= 0, other=0.) botHalfLevelThickness = 0.5*layerThickness.isel( nCells=interpCellIndices, nVertLevels=botLevelIndices) # vertical weights are proportional to the half-level thickness interpCellWeights = xarray.concat( (topHalfLevelThickness*interpCellWeights.isel(nVertLevels=topLevelIndices), botHalfLevelThickness*interpCellWeights.isel(nVertLevels=botLevelIndices)), dim='nWeights') weightSum = interpCellWeights.sum(dim='nWeights') _, outMask = xarray.broadcast(interpCellWeights, weightSum > 0.) interpCellWeights = (interpCellWeights/weightSum).where(outMask) interpCellIndices = xarray.concat((interpCellIndices, interpCellIndices), dim='nWeights') return interpCellIndices, interpLevelIndices, interpCellWeights def _transect_cells_to_triangles(dsTransectCells): nTransectCells = dsTransectCells.sizes['nTransectCells'] nTransectTriangles = 2*nTransectCells triangleTransectCellIndices = numpy.arange(nTransectTriangles)//2 nodeTransectCellIndices = numpy.zeros((nTransectTriangles, 3), int) nodeHorizBoundsIndices = numpy.zeros((nTransectTriangles, 3), int) nodeVertBoundsIndices = numpy.zeros((nTransectTriangles, 3), int) for index in range(3): nodeTransectCellIndices[:, index] = triangleTransectCellIndices # the upper triangle nodeHorizBoundsIndices[0::2, 0] = 0 nodeVertBoundsIndices[0::2, 0] = 0 nodeHorizBoundsIndices[0::2, 1] = 1 nodeVertBoundsIndices[0::2, 1] = 0 nodeHorizBoundsIndices[0::2, 2] = 0 nodeVertBoundsIndices[0::2, 2] = 1 # the lower triangle nodeHorizBoundsIndices[1::2, 0] = 0 nodeVertBoundsIndices[1::2, 0] = 1 nodeHorizBoundsIndices[1::2, 1] = 1 nodeVertBoundsIndices[1::2, 1] = 0 nodeHorizBoundsIndices[1::2, 2] = 1 nodeVertBoundsIndices[1::2, 2] = 1 triangleTransectCellIndices = xarray.DataArray( data=triangleTransectCellIndices, dims='nTransectTriangles') nodeTransectCellIndices = xarray.DataArray( data=nodeTransectCellIndices, dims=('nTransectTriangles', 'nTriangleNodes')) nodeHorizBoundsIndices = xarray.DataArray( data=nodeHorizBoundsIndices, dims=('nTransectTriangles', 'nTriangleNodes')) nodeVertBoundsIndices = xarray.DataArray( data=nodeVertBoundsIndices, dims=('nTransectTriangles', 'nTriangleNodes')) dsTransectTriangles = xarray.Dataset() dsTransectTriangles['nodeHorizBoundsIndices'] = \ nodeHorizBoundsIndices for var_name in dsTransectCells.data_vars: var = dsTransectCells[var_name] if 'nTransectCells' in var.dims: if 'nVertBounds' in var.dims: assert 'nHorizBounds' in var.dims dsTransectTriangles[var_name] = var.isel( nTransectCells=nodeTransectCellIndices, nHorizBounds=nodeHorizBoundsIndices, nVertBounds=nodeVertBoundsIndices) elif 'nHorizBounds' in var.dims: dsTransectTriangles[var_name] = var.isel( nTransectCells=nodeTransectCellIndices, nHorizBounds=nodeHorizBoundsIndices) else: dsTransectTriangles[var_name] = var.isel( nTransectCells=triangleTransectCellIndices) else: dsTransectTriangles[var_name] = var dsTransectTriangles = dsTransectTriangles.drop_vars('halfLevelIndices') return dsTransectTriangles def _add_vertical_interpolation_of_transect_points(dsTransectTriangles, zTransect): dTransect = dsTransectTriangles.dTransect # make sure zTransect is 2D zTransect, _ = xarray.broadcast(zTransect, dTransect) assert len(zTransect.dims) == 2 horizDim = dTransect.dims[0] vertDim = None for dim in zTransect.dims: if dim != horizDim: vertDim = dim assert vertDim is not None nzTransect = zTransect.sizes[vertDim] horizIndices = dsTransectTriangles.transectIndicesOnHorizNode horizWeights = dsTransectTriangles.transectWeightsOnHorizNode kwargs0 = {horizDim: horizIndices} kwargs1 = {horizDim: horizIndices+1} zTransectAtHorizNodes = horizWeights*zTransect.isel(**kwargs0) + \ (1.0 - horizWeights)*zTransect.isel(**kwargs1) zTriangleNode = dsTransectTriangles.zTransectNode segmentIndices = dsTransectTriangles.segmentIndices nodeHorizBoundsIndices = dsTransectTriangles.nodeHorizBoundsIndices nTransectTriangles = dsTransectTriangles.sizes['nTransectTriangles'] nTriangleNodes = dsTransectTriangles.sizes['nTriangleNodes'] transectInterpVertIndices = -1*numpy.ones( (nTransectTriangles, nTriangleNodes), int) transectInterpVertWeights = numpy.zeros( (nTransectTriangles, nTriangleNodes)) kwargs = {vertDim: 0, 'nSegments': segmentIndices, 'nHorizBounds': nodeHorizBoundsIndices} z0 = zTransectAtHorizNodes.isel(**kwargs) for zIndex in range(nzTransect-1): kwargs = {vertDim: zIndex+1, 'nSegments': segmentIndices, 'nHorizBounds': nodeHorizBoundsIndices} z1 = zTransectAtHorizNodes.isel(**kwargs) mask = numpy.logical_and(zTriangleNode <= z0, zTriangleNode > z1) mask = mask.values weights = (z1 - zTriangleNode)/(z1 - z0) transectInterpVertIndices[mask] = zIndex transectInterpVertWeights[mask] = weights.values[mask] z0 = z1 dsTransectTriangles['transectInterpVertIndices'] = ( ('nTransectTriangles', 'nTriangleNodes'), transectInterpVertIndices) dsTransectTriangles['transectInterpVertWeights'] = ( ('nTransectTriangles', 'nTriangleNodes'), transectInterpVertWeights) dsTransectTriangles['zTransect'] = zTransect return dsTransectTriangles