def test_1d_A_2d_n(self): A = np.array([7,8,9]) n = np.array([[2],[3], [0]]) B1 = rldecode(A, n, axis=0) assert np.array_equal(B1, np.array([7, 7, 8, 8, 8])) # A only has one axis, so axis=1 does not make sense with pytest.raises(ValueError): B2 = rldecode(A, n, axis=1)
def getCellNoFaces(G): """ Get a list of all half faces, accounting for possible NNC. Synopsis: cellNo, cellFaces, isNNC = getCellNoFaces(G) Description: This utility function is used to produce a listing of all half faces in a grid along with the respective cells they belong to. While relatively trivial for most grids, this function specifically accounts for non-neighboring connections / NNC. Arguments: G (Grid): Grid structure with optional .nnc.cells attribute. Returns: cellNo (ndarray): Column array with shape (M,1) where M is the number of geometric half-faces + 2 * number of NNC, where each entry corresponds to cell index of that half face. cellFaces (ndarray): Column array with shape (M,1) with M as above, where each entry is the connection index. For the first entries, this is simply the face number. Otherwise, it is the entry of the NNC connection. See also: prst.utils.rldecode """ import prst.utils as utils # circular import cellNo = utils.rldecode(np.arange(G.cells.num), np.diff(G.cells.facePos, axis=0))[:, np.newaxis] cellFaces = G.cells.faces[:, 0][:, np.newaxis] # Mapping to show which entries in cellNo/cellFaces are resulting from # NNC and not actual geometric faces. isNNC = np.zeros((len(cellNo), 1), dtype=np.bool) # If NNC is present, we add these extra connections to cellNo # and cellFaces to allow consisten treatment. if hasattr(G, 'nnc') and hasattr(G.nnc, "cells"): prst.warning( "getCellNoFaces is untested for grids with NNC. Compare results with MRST." ) # Stack columns into a single column nnc_cells = np.r_[G.nnc.cells[:, 0], G.nnc.cells[:, 1]][:, np.newaxis] # NNC is located at the end after the regular faces nnc_faceno = G.faces.num + np.arange(G.nnc.cells.shape[0])[:, np.newaxis] cellNo = np.r_[cellNo, nnc_cells] # Stack as 2d column cellFaces = np.r_[cellFaces, nnc_faceno, nnc_faceno] # Stack as column # Added connections are NNC isNNC = np.r_[isNNC, np.ones((len(nnc_cells), 1), dtype=np.bool)] return cellNo, cellFaces, isNNC
def getCellNoFaces(G): """ Get a list of all half faces, accounting for possible NNC. Synopsis: cellNo, cellFaces, isNNC = getCellNoFaces(G) Description: This utility function is used to produce a listing of all half faces in a grid along with the respective cells they belong to. While relatively trivial for most grids, this function specifically accounts for non-neighboring connections / NNC. Arguments: G (Grid): Grid structure with optional .nnc.cells attribute. Returns: cellNo (ndarray): Column array with shape (M,1) where M is the number of geometric half-faces + 2 * number of NNC, where each entry corresponds to cell index of that half face. cellFaces (ndarray): Column array with shape (M,1) with M as above, where each entry is the connection index. For the first entries, this is simply the face number. Otherwise, it is the entry of the NNC connection. See also: prst.utils.rldecode """ import prst.utils as utils # circular import cellNo = utils.rldecode(np.arange(G.cells.num), np.diff(G.cells.facePos,axis=0))[:,np.newaxis] cellFaces = G.cells.faces[:,0][:,np.newaxis] # Mapping to show which entries in cellNo/cellFaces are resulting from # NNC and not actual geometric faces. isNNC = np.zeros((len(cellNo), 1), dtype=np.bool) # If NNC is present, we add these extra connections to cellNo # and cellFaces to allow consisten treatment. if hasattr(G, 'nnc') and hasattr(G.nnc, "cells"): prst.warning("getCellNoFaces is untested for grids with NNC. Compare results with MRST.") # Stack columns into a single column nnc_cells = np.r_[G.nnc.cells[:,0], G.nnc.cells[:,1]][:,np.newaxis] # NNC is located at the end after the regular faces nnc_faceno = G.faces.num + np.arange(G.nnc.cells.shape[0])[:,np.newaxis] cellNo = np.r_[cellNo, nnc_cells] # Stack as 2d column cellFaces = np.r_[cellFaces, nnc_faceno, nnc_faceno] # Stack as column # Added connections are NNC isNNC = np.r_[isNNC, np.ones((len(nnc_cells),1), dtype=np.bool)] return cellNo, cellFaces, isNNC
def _grav_pressure(G, omega): """Computes innerproduct cf (face_centroid - cell_centroid) * g for each face""" g_vec = prst.gravity if np.linalg.norm(g_vec[:G.gridDim]) > 0: dim = G.gridDim assert 1 <= dim <= 3, "Wrong grid dimension" cellno = utils.rldecode(np.arange(G.cells.num), np.diff(G.cells.facePos, axis=0)) cvec = G.faces.centroids[G.cells.faces[:,0],:] - G.cells.centroids[cellno,:] ff = omega[cellno] * np.dot(cvec, g_vec[:dim].reshape([3,1])) else: ff = np.zeros([G.cells.faces.shape[0], 1]) return ff
def computeGeometry(G, findNeighbors=False, hingenodes=None): """ Compute and add geometry attributes to grid object. Synopsis: G = computeGeometry(G) Arguments: G (Grid): Grid object. Input argument is mutated. findNeighbors (Optional[bool]): Force finding the neighbors array even if it exists. Defaults to False. Returns: G - Grid object with added attributes: - cells - volumes -- An array with size G.cells.num of cell volumes. - centroids -- An array with shape (G.cells.num, G.gridDim) of approximate cell centroids. - faces - areas -- An array with size G.faces.num of face areas. - normals -- An array with shape (G.faces.num, G.gridDim) of face normals - centroids -- An array with shape (G.faces.num, G.griddim) of approximate face centroids. Individual face normals have length (i.e. Euclidean norm) equal to the corresponding face areas. In other words, subject to numerical round-off, the identity np.linalg.norm(G.faces.normals[i,:], 2) == G.faces.areas[i] holds for all faces i in range(0, G.faces.num) . In three space dimensions, i.e. when G.gridDim == 3, the function `computeGeometry` assumes that the nodes on a given face `f` are ordered such that the face normal on `f` is directed from cell `G.faces.neighbors[f,0]` to cell `G.faces.neighbors[f,1]`. """ ## Setup assert hingenodes is None or G.gridDim == 3,\ "Hinge nodes are only supported for 3D grids" numCells = G.cells.num numFaces = G.faces.num ## Possibly find neighbors if findNeighbors: G.faces.neighbors = _findNeighbors(G) G = _findNormalDirections(G) else: if not hasattr(G.faces, "neighbors"): import prst prst.log.warn("No field faces.neighbors found. " + "Adding plausible values... proceed with caution!") G.faces.neighbors = _findNeighbors(G) G = _findNormalDirections(G) ## Main part if G.gridDim == 3: ## 3D grid assert G.nodes.coords.shape[1] == 3 faceNumbers = utils.rldecode(np.arange(G.faces.num), np.diff(G.faces.nodePos,axis=0)) nodePos = G.faces.nodePos; nextNode = np.arange(1, G.faces.nodes.size+1) nextNode[nodePos[1:,0]-1] = nodePos[:-1,0] # Divide each face into sub-triangles all having one node as pCenter = # sum(nodes) / numNodes. Compute area-weighted normals, and add to # obtain approx face-normals. Compute resulting areas and centroids. import prst prst.log.info("Computing normals, areas and centroids...") # Construct a sparse matrix with zeros and ones. localEdge2Face = scipy.sparse.csc_matrix(( np.ones(G.faces.nodes.size), (np.arange(G.faces.nodes.size), faceNumbers) )) # Divide each row in the matrix product by the numbers in the final # array elementwise pCenters = (localEdge2Face.transpose().dot( G.nodes.coords[G.faces.nodes[:,0]] )[None,:] / np.diff(G.faces.nodePos,axis=0))[0] pCenters = localEdge2Face.dot(pCenters) if hingenodes: raise NotImplementedError("hingenodes are not yet supported in PRST") subNormals = np.cross( G.nodes.coords[G.faces.nodes[nextNode,0],:] - G.nodes.coords[G.faces.nodes[:,0],:], pCenters - G.nodes.coords[G.faces.nodes[:,0],:]) / 2 subAreas = np.linalg.norm(subNormals, axis=1) subCentroids = (G.nodes.coords[G.faces.nodes[:,0],:] + G.nodes.coords[G.faces.nodes[nextNode,0],:] + pCenters) / 3 faceNormals = localEdge2Face.transpose().dot(subNormals) faceAreas = localEdge2Face.transpose().dot(subAreas) subNormalSigns = np.sign(np.sum( subNormals * (localEdge2Face * faceNormals), axis=1)) faceCentroids = localEdge2Face.transpose().dot( subAreas[:,np.newaxis] * subCentroids ) / faceAreas[:,np.newaxis] # Computation above does not make sense for faces with zero area zeroAreaIndices = np.where(faceAreas <= 0) if np.any(zeroAreaIndices): import prst prst.log.warning("Faces with zero area detected. Such faces should be" + "removed before calling computeGeometry") # Divide each cell into sub-tetrahedra according to sub-triangles above, # all having one node as cCenter = sum(faceCentroids) / #faceCentroids import prst prst.log.info("Computing cell volumes and centroids") cellVolumes = np.zeros(numCells) cellCentroids = np.zeros([numCells, 3]) lastIndex = 0 for cell in range(numCells): # Number of faces for the current cell cellNumFaces = G.cells.facePos[cell+1] - G.cells.facePos[cell] indexes = np.arange(cellNumFaces) + lastIndex # The indices to the faces for the current cell cellFaces = G.cells.faces[indexes,0] # triE are the row indices of the non-zero elements in the matrix, while # triF are the col indices of the same elements. # Original code based on MRST: # # tmp = localEdge2Face[:,cellFaces] # triEa, triFa = tmp.nonzero() # # Was very slow, so a custom function for extracting columns for a # csc_matrix was created. It is not fully tested, but is 4x faster. triF, triE = _csc_columns_nonzero(localEdge2Face.indptr, localEdge2Face.indices, cellFaces) cellFaceCentroids = faceCentroids[cellFaces,:] cellCenter = np.sum(cellFaceCentroids, axis=0) / cellNumFaces relSubC = subCentroids[triE,:] - cellCenter[np.newaxis,:] # The normal of a face f is directed from cell G.faces.neighbors[f,0] # to cell G.faces.neighbors[f,1]. If cell c is in the second column # for face f, then the normal must be multiplied by -1 to be an outer # normal. orientation = 2 * (G.faces.neighbors[G.cells.faces[indexes,0], 0] == cell) - 1 outNormals = subNormals[triE,:] * ( (subNormalSigns[triE] * orientation[triF])[:,np.newaxis] ) tetraVolumes = (1/3) * np.sum(relSubC * outNormals, axis=1) tetraCentroids = (3/4) * relSubC; cellVolume = np.sum(tetraVolumes) # Warning: This expression can be very close to zero, and often differs from # the same calculation in MATLAB. relCentroid = (tetraVolumes.dot(tetraCentroids)) / cellVolume cellCentroid = relCentroid + cellCenter cellVolumes[cell] = cellVolume cellCentroids[cell,:] = cellCentroid lastIndex += cellNumFaces elif G.gridDim == 2 and G.nodes.coords.shape[1] == 2: # Sometimes G.cells.faces has a second column with face directions. # So we retrieve the index column only. cellFaces = G.cells.faces[:,0] ## 2D grid in 2D space import prst prst.log.info("Computing normals, areas and centroids") edges = G.faces.nodes.reshape([-1,2], order="C") # Distance between edge nodes as a vector. "Length" is misleading. edgeLength = G.nodes.coords[edges[:,1],:] \ - G.nodes.coords[edges[:,0],:] # Since this is 2D, these are actually lengths faceAreas = np.linalg.norm(edgeLength, axis=1) faceCentroids = np.average(G.nodes.coords[edges], axis=1) faceNormals = np.column_stack([edgeLength[:,1], -edgeLength[:,0]]) import prst prst.log.info("Computing cell volumes and centroids") numFaces = np.diff(G.cells.facePos, axis=0)[:,0] cellNumbers = prst.utils.rldecode(np.arange(G.cells.num), numFaces) cellEdges = edges[cellFaces,:] r = G.faces.neighbors[cellFaces, 1] == cellNumbers # swap the two columns cellEdges[r, 0], cellEdges[r, 1] = cellEdges[r, 1], cellEdges[r, 0] cCenter = np.zeros([G.cells.num, 2]) # npg.aggregate is similar to accumarray in MATLAB cCenter[:,0] = aggregate(cellNumbers, faceCentroids[cellFaces, 0]) / numFaces cCenter[:,1] = aggregate(cellNumbers, faceCentroids[cellFaces, 1]) / numFaces a = G.nodes.coords[cellEdges[:,0],:] - cCenter[cellNumbers,:] b = G.nodes.coords[cellEdges[:,1],:] - cCenter[cellNumbers,:] subArea = 0.5 * (a[:,0]*b[:,1] - a[:,1] * b[:,0]) subCentroid = ( cCenter[cellNumbers,:] + 2*faceCentroids[cellFaces,:])/3 cellVolumes = aggregate(cellNumbers, subArea) cellCentroids = np.zeros([G.cells.num, 2], dtype=np.float64) cellCentroids[:,0] = aggregate( cellNumbers, subArea * subCentroid[:,0]) / cellVolumes cellCentroids[:,1] = aggregate( cellNumbers, subArea * subCentroid[:,1]) / cellVolumes elif G.gridDim == 2 and G.nodes.coords.shape[1] == 3: ## 2D grid in 3D space raise NotImplementedError( "computeGeometry not yet implemented for surface grids") else: raise ValueError("gridDim or nodes.coords have invalid values") ## Update grid G.faces.areas = faceAreas[:,np.newaxis] G.faces.normals = faceNormals G.faces.centroids = faceCentroids G.cells.volumes = cellVolumes[:,np.newaxis] G.cells.centroids = cellCentroids if not hasattr(G, "gridType"): import prst prst.log.warning("Input grid has no type") G.gridType = [] G.gridType.append("computeGeometry") return G
def computeTrans(G, rock, K_system="xyz", cellCenters=None, cellFaceCenters=None, verbose=False): """ Compute transmissibilities for a grid. Synopsis: T = computeTrans(G, rock) T = computeTrans(G, rock, **kwargs) Arguments: G (Grid): prst.gridprocessing.Grid instance. rock (Rock): prst.params.rock.Rock instance with `perm` attribute. The permeability is assumed to be in units of metres squared (m^2). Use constant `darcy` from prst.utils.units to convert to m^2, e.g., from prst.utils.units import * perm = convert(perm, from_=milli*darcy, to=meter**2) if the permeability is provided in units of millidarcies. The field rock.perm may have ONE column for a scalar permeability in each cell, TWO/THREE columns for a diagonal permeability in each cell (in 2/D D) and THREE/SIX columns for a symmetric full tensor permeability. In the latter case, each cell gets the permability tensor. K_i = [ k1 k2 ] in two space dimensions [ k2 k3 ] K_i = [ k1 k2 k3 ] in three space dimensions [ k2 k4 k5 ] [ k3 k5 k6 ] K_system (Optional[str]): The system permeability. Valid values are "xyz" and "loc_xyz". cellCenters (Optional[ndarray]): Compute transmissibilities based on supplied cellCenters rather than default G.cells.centroids. Must have shape (n,2) for 2D and (n,3) for 3D. cellFaceCenters (Optional[ndarray]): Compute transmissibilities based on supplied cellFaceCenters rather than default `G.faces.centroids[G.cells.faces[:,0], :]`. Returns: T: Half-transmissibilities for each local face of each grid cell in the grid. The number of half-transmissibilities equals the number of rows in `G.cells.faces`. 2D column array. Comments: PLEASE NOTE: Face normals are assumed to have length equal to the corresponding face areas. This property is guaranteed by function `computeGeometry`. See also: computeGeometry, computeMimeticIP (MRST), darcy, permTensor, Rock """ if K_system not in ["xyz", "loc_xyz"]: raise TypeError( "Specified permeability coordinate system must be a 'xyz' or 'loc_xyz'") if verbose: print("Computing one-sided transmissibilites.") # Vectors from cell centroids to face centroids assert G.cells.facePos.ndim == 2, "facePos has wrong dimensions" cellNo = rldecode(np.arange(G.cells.num), np.diff(G.cells.facePos, axis=0)) if cellCenters is None: C = G.cells.centroids else: C = cellCenters if cellFaceCenters is None: C = G.faces.centroids[G.cells.faces[:,0],:] - C[cellNo,:] else: C = cellFaceCenters - C[cellNo,:] # Normal vectors sgn = 2*(cellNo == G.faces.neighbors[G.cells.faces[:,0], 0]) - 1 N = sgn[:,np.newaxis] * G.faces.normals[G.cells.faces[:,0],:] if K_system == "xyz": K, i, j = permTensor(rock, G.gridDim, rowcols=True) assert K.shape[0] == G.cells.num, \ "Permeability must be defined in active cells only.\n"+\ "Got {} tensors, expected {} (== num cells)".format(K.shape[0], G.cells.num) # Compute T = C'*K*N / C'*C. Loop based to limit memory use. T = np.zeros(cellNo.size) for k in range(i.size): tmp = C[:,i[k]] * K[cellNo,k] * N[:,j[k]] # Handle both 1d and 2d array. if tmp.ndim == 1: T += tmp else: T += np.sum(tmp, axis=1) T = T / np.sum(C*C, axis=1) elif K_system == "loc_xyz": if rock.perm.shape[1] == 1: rock.perm = np.tile(rock.perm, (1, G.gridDim)) if rock.perm.shape[1] != G.cartDims.size: raise ValueError( "Permeability coordinate system `loc_xyz` is only "+\ "valid for diagonal tensor.") assert rock.perm.shape[0] == G.cells.num,\ "Permeability must be defined in active cells only. "+\ "Got {} tensors, expected {} == (num cells)".format(rock.perm.shape[0], G.cells.num) dim = np.ceil(G.cells.faces[:,1] / 2) raise NotImplementedError("Function not finished for K_system='loc_xyz'") # See MRST, solvers/computeTrans.m else: raise ValueError("Unknown permeability coordinate system {}.".format(K_system)) is_neg = T < 0 if np.any(is_neg): if verbose: prst.log.warn("Warning: {} negative transmissibilities. ".format(np.sum(is_neg))+ "Replaced by absolute values...") T[is_neg] = -T[is_neg] return np.atleast_2d(T).transpose()
def computeTrans(G, rock, K_system="xyz", cellCenters=None, cellFaceCenters=None, verbose=False): """ Compute transmissibilities for a grid. Synopsis: T = computeTrans(G, rock) T = computeTrans(G, rock, **kwargs) Arguments: G (Grid): prst.gridprocessing.Grid instance. rock (Rock): prst.params.rock.Rock instance with `perm` attribute. The permeability is assumed to be in units of metres squared (m^2). Use constant `darcy` from prst.utils.units to convert to m^2, e.g., from prst.utils.units import * perm = convert(perm, from_=milli*darcy, to=meter**2) if the permeability is provided in units of millidarcies. The field rock.perm may have ONE column for a scalar permeability in each cell, TWO/THREE columns for a diagonal permeability in each cell (in 2/D D) and THREE/SIX columns for a symmetric full tensor permeability. In the latter case, each cell gets the permability tensor. K_i = [ k1 k2 ] in two space dimensions [ k2 k3 ] K_i = [ k1 k2 k3 ] in three space dimensions [ k2 k4 k5 ] [ k3 k5 k6 ] K_system (Optional[str]): The system permeability. Valid values are "xyz" and "loc_xyz". cellCenters (Optional[ndarray]): Compute transmissibilities based on supplied cellCenters rather than default G.cells.centroids. Must have shape (n,2) for 2D and (n,3) for 3D. cellFaceCenters (Optional[ndarray]): Compute transmissibilities based on supplied cellFaceCenters rather than default `G.faces.centroids[G.cells.faces[:,0], :]`. Returns: T: Half-transmissibilities for each local face of each grid cell in the grid. The number of half-transmissibilities equals the number of rows in `G.cells.faces`. 2D column array. Comments: PLEASE NOTE: Face normals are assumed to have length equal to the corresponding face areas. This property is guaranteed by function `computeGeometry`. See also: computeGeometry, computeMimeticIP (MRST), darcy, permTensor, Rock """ if K_system not in ["xyz", "loc_xyz"]: raise TypeError( "Specified permeability coordinate system must be a 'xyz' or 'loc_xyz'" ) if verbose: print("Computing one-sided transmissibilites.") # Vectors from cell centroids to face centroids assert G.cells.facePos.ndim == 2, "facePos has wrong dimensions" cellNo = rldecode(np.arange(G.cells.num), np.diff(G.cells.facePos, axis=0)) if cellCenters is None: C = G.cells.centroids else: C = cellCenters if cellFaceCenters is None: C = G.faces.centroids[G.cells.faces[:, 0], :] - C[cellNo, :] else: C = cellFaceCenters - C[cellNo, :] # Normal vectors sgn = 2 * (cellNo == G.faces.neighbors[G.cells.faces[:, 0], 0]) - 1 N = sgn[:, np.newaxis] * G.faces.normals[G.cells.faces[:, 0], :] if K_system == "xyz": K, i, j = permTensor(rock, G.gridDim, rowcols=True) assert K.shape[0] == G.cells.num, \ "Permeability must be defined in active cells only.\n"+\ "Got {} tensors, expected {} (== num cells)".format(K.shape[0], G.cells.num) # Compute T = C'*K*N / C'*C. Loop based to limit memory use. T = np.zeros(cellNo.size) for k in range(i.size): tmp = C[:, i[k]] * K[cellNo, k] * N[:, j[k]] # Handle both 1d and 2d array. if tmp.ndim == 1: T += tmp else: T += np.sum(tmp, axis=1) T = T / np.sum(C * C, axis=1) elif K_system == "loc_xyz": if rock.perm.shape[1] == 1: rock.perm = np.tile(rock.perm, (1, G.gridDim)) if rock.perm.shape[1] != G.cartDims.size: raise ValueError( "Permeability coordinate system `loc_xyz` is only "+\ "valid for diagonal tensor.") assert rock.perm.shape[0] == G.cells.num,\ "Permeability must be defined in active cells only. "+\ "Got {} tensors, expected {} == (num cells)".format(rock.perm.shape[0], G.cells.num) dim = np.ceil(G.cells.faces[:, 1] / 2) raise NotImplementedError( "Function not finished for K_system='loc_xyz'") # See MRST, solvers/computeTrans.m else: raise ValueError( "Unknown permeability coordinate system {}.".format(K_system)) is_neg = T < 0 if np.any(is_neg): if verbose: prst.log.warn("Warning: {} negative transmissibilities. ".format( np.sum(is_neg)) + "Replaced by absolute values...") T[is_neg] = -T[is_neg] return np.atleast_2d(T).transpose()