def __init__(self, g): """ Constructor for subcell topology Parameters ---------- g grid """ self.g = g # Indices of neighboring faces and cells. The indices are sorted to # simplify later treatment g.cell_faces.sort_indices() face_ind, cell_ind = g.cell_faces.nonzero() # Number of faces per node num_face_nodes = np.diff(g.face_nodes.indptr) # Duplicate cell and face indices, so that they can be matched with # the nodes cells_duplicated = matrix_compression.rldecode( cell_ind, num_face_nodes[face_ind]) faces_duplicated = matrix_compression.rldecode( face_ind, num_face_nodes[face_ind]) M = sps.coo_matrix((np.ones(face_ind.size), (face_ind, np.arange(face_ind.size))), shape=(face_ind.max() + 1, face_ind.size)) nodes_duplicated = g.face_nodes * M nodes_duplicated = nodes_duplicated.indices face_nodes_indptr = g.face_nodes.indptr face_nodes_indices = g.face_nodes.indices face_nodes_data = np.arange(face_nodes_indices.size) + 1 sub_face_mat = sps.csc_matrix((face_nodes_data, face_nodes_indices, face_nodes_indptr)) sub_faces = sub_face_mat * M sub_faces = sub_faces.data - 1 # Sort data idx = np.lexsort((sub_faces, faces_duplicated, nodes_duplicated, cells_duplicated)) self.nno = nodes_duplicated[idx] self.cno = cells_duplicated[idx] self.fno = faces_duplicated[idx] self.subfno = sub_faces[idx].astype(int) self.subhfno = np.arange(idx.size, dtype='>i4') self.num_subfno = self.subfno.max() + 1 self.num_cno = self.cno.max() + 1 # Make subface indices unique, that is, pair the indices from the two # adjacent cells _, unique_subfno = np.unique(self.subfno, return_index=True) # Reduce topology to one field per subface self.nno_unique = self.nno[unique_subfno] self.fno_unique = self.fno[unique_subfno] self.cno_unique = self.cno[unique_subfno] self.subfno_unique = self.subfno[unique_subfno] self.num_subfno_unique = self.subfno_unique.max() + 1 self.unique_subfno = unique_subfno
def cell_face_as_dense(self): """ Obtain the cell-face relation in the from of two rows, rather than a sparse matrix. This alterative format can be useful in some cases. Each column in the array corresponds to a face, and the elements in that column refers to cell indices. The value -1 signifies a boundary. The normal vector of the face points from the first to the second row. Returns: np.ndarray, 2 x num_faces: Array representation of face-cell relations """ n = self.cell_faces.tocsr() d = np.diff(n.indptr) rows = matrix_compression.rldecode(np.arange(d.size), d) # Increase the data by one to distinguish cell indices from boundary # cells data = n.indices + 1 cols = ((n.data + 1) / 2).astype('i') neighs = sps.coo_matrix((data, (rows, cols))).todense() # Subtract 1 to get back to real cell indices neighs -= 1 neighs = neighs.transpose().A.astype('int') # Finally, we need to switch order of rows to get normal vectors # pointing from first to second row. return neighs[::-1]
def block_diag_index(m, n=None): """ Get row and column indices for block diagonal matrix This is intended as the equivalent of the corresponding method in MRST. Examples: >>> m = np.array([2, 3]) >>> n = np.array([1, 2]) >>> i, j = block_diag_index(m, n) >>> i, j (array([0, 1, 2, 3, 4, 2, 3, 4]), array([0, 0, 1, 1, 1, 2, 2, 2])) >>> a = np.array([1, 3]) >>> i, j = block_diag_index(a) >>> i, j (array([0, 1, 2, 3, 1, 2, 3, 1, 2, 3]), array([0, 1, 1, 1, 2, 2, 2, 3, 3, 3])) Parameters: m - ndarray, dimension 1 n - ndarray, dimension 1, defaults to m """ if n is None: n = m start = np.hstack((np.zeros(1, dtype='int'), m)) pos = np.cumsum(start) p1 = pos[0:-1] p2 = pos[1:]-1 p1_full = matrix_compression.rldecode(p1, n) p2_full = matrix_compression.rldecode(p2, n) i = mcolon.mcolon(p1_full, p2_full) sumn = np.arange(np.sum(n)) m_n_full = matrix_compression.rldecode(m, n) j = matrix_compression.rldecode(sumn, m_n_full) return i, j
def compute_dist_face_cell(g, subcell_topology, eta): """ Compute vectors from cell centers continuity points on each sub-face. The location of the continuity point is given by x_cp = (1-eta) * x_facecenter + eta * x_vertex On the boundary, eta is set to zero, thus the continuity point is at the face center Parameters ---------- g: Grid subcell_topology: Of class subcell topology in this module eta: [0,1), eta = 0 gives cont. pt. at face midpoint, eta = 1 means at the vertex Returns ------- sps.csr() matrix representation of vectors. Size g.nf x (g.nc * g.nd) """ _, blocksz = matrix_compression.rlencode(np.vstack(( subcell_topology.cno, subcell_topology.nno))) dims = g.dim rows, cols = np.meshgrid(subcell_topology.subhfno, np.arange(dims)) cols += matrix_compression.rldecode(np.cumsum(blocksz)-blocksz[0], blocksz) eta_vec = eta*np.ones(subcell_topology.fno.size) # Set eta values to zero at the boundary bnd = np.argwhere(np.abs(g.cell_faces).sum(axis=1).A.squeeze() == 1).squeeze() eta_vec[bnd] = 0 cp = g.face_centers[:, subcell_topology.fno] \ + eta_vec * (g.nodes[:, subcell_topology.nno] - g.face_centers[:, subcell_topology.fno]) dist = cp - g.cell_centers[:, subcell_topology.cno] mat = sps.coo_matrix((dist.ravel(), (rows.ravel(), cols.ravel()))).tocsr() return subcell_topology.pair_over_subfaces(mat)
def _tensor_vector_prod(g, constit, subcell_topology): # Stack cells and nodes, and remove duplicate rows. Since subcell_mapping # defines cno and nno (and others) working cell-wise, this will # correspond to a unique rows (Matlab-style) from what I understand. # This also means that the pairs in cell_node_blocks uniquely defines # subcells, and can be used to index gradients etc. cell_node_blocks, blocksz = matrix_compression.rlencode(np.vstack(( subcell_topology.cno, subcell_topology.nno))) nd = g.dim # Duplicates in [cno, nno] corresponds to different faces meeting at the # same node. There should be exactly nd of these. This test will fail # for pyramids in 3D assert np.all(blocksz == nd) # Define row and column indices to be used for normal vector matrix # Rows are based on sub-face numbers. # Columns have nd elements for each sub-cell (to store a vector) and # is adjusted according to block sizes rn, cn = np.meshgrid(subcell_topology.subhfno, np.arange(nd)) sum_blocksz = np.cumsum(blocksz) cn += matrix_compression.rldecode(sum_blocksz - blocksz[0], blocksz) # Distribute faces equally on the sub-faces, and store in a matrix num_nodes = np.diff(g.face_nodes.indptr) normals = g.face_normals[:, subcell_topology.fno] / num_nodes[ subcell_topology.fno] normals_mat = sps.coo_matrix((normals.ravel(1), (rn.ravel('F'), cn.ravel('F')))).tocsr() # Then row and columns for stiffness matrix. There are nd^2 elements in # the gradient operator, and so the structure is somewhat different from # the normal vectors rc, cc = np.meshgrid(subcell_topology.subhfno, np.arange(nd**2)) sum_blocksz = np.cumsum(blocksz**2) cc += matrix_compression.rldecode(sum_blocksz - blocksz[0]**2, blocksz) # Splitt stiffness matrix into symmetric and anti-symmatric part sym_tensor, asym_tensor = _split_stiffness_matrix(constit) # Getting the right elements out of the constitutive laws was a bit # tricky, but the following code turned out to do the trick sym_tensor_swp = np.swapaxes(sym_tensor, 2, 0) asym_tensor_swp = np.swapaxes(asym_tensor, 2, 0) # The first dimension in csym and casym represent the contribution from # all dimensions to the stress in one dimension (in 2D, csym[0:2,:, # :] together gives stress in the x-direction etc. # Define index vector to access the right rows rind = np.arange(nd) # Empty matrices to initialize matrix-tensor products. Will be expanded # as we move on zr = np.zeros(0) ncsym = sps.coo_matrix((zr, (zr, zr)), shape=(0, cc.max() + 1)).tocsr() ncasym = sps.coo_matrix((zr, (zr, zr)), shape=(0, cc.max() + 1)).tocsr() # For the asymmetric part of the tensor, we will apply volume averaging. # Associate a volume with each sub-cell, and a node-volume as the sum of # all surrounding sub-cells num_cell_nodes = g.num_cell_nodes() cell_vol = g.cell_volumes / num_cell_nodes node_vol = np.bincount(subcell_topology.nno, weights=cell_vol[ subcell_topology.cno]) / g.dim num_elem = cell_node_blocks.shape[1] map_mat = sps.coo_matrix((np.ones(num_elem), (np.arange(num_elem), cell_node_blocks[1]))) weight_mat = sps.coo_matrix((cell_vol[cell_node_blocks[0]] / node_vol[ cell_node_blocks[1]], (cell_node_blocks[1], np.arange(num_elem)))) # Operator for carying out the average average = sps.kron(map_mat * weight_mat, sps.identity(nd)).tocsr() for iter1 in range(nd): # Pick out part of Hook's law associated with this dimension # The code here looks nasty, it should be possible to get the right # format of the submatrices in a simpler way, but I couldn't do it. sym_dim = np.hstack(sym_tensor_swp[:, :, rind]).transpose() asym_dim = np.hstack(asym_tensor_swp[:, :, rind]).transpose() # Distribute (relevant parts of) Hook's law on subcells # This will be nd rows, thus cell ci is associated with indices # ci*nd+np.arange(nd) sub_cell_ind = __expand_indices_nd(cell_node_blocks[0], nd) sym_vals = sym_dim[sub_cell_ind] asym_vals = asym_dim[sub_cell_ind] # Represent this part of the stiffness matrix in matrix form csym_mat = sps.coo_matrix((sym_vals.ravel('C'), (rc.ravel('F'), cc.ravel('F')))).tocsr() casym_mat = sps.coo_matrix((asym_vals.ravel(0), (rc.ravel('F'), cc.ravel('F')))).tocsr() # Compute average around vertexes casym_mat = average * casym_mat # Compute products of normal vectors and stiffness tensors, # and stack dimensions vertically ncsym = sps.vstack((ncsym, normals_mat * csym_mat)) ncasym = sps.vstack((ncasym, normals_mat * casym_mat)) # Increase index vector, so that we get rows contributing to forces # in the next dimension rind += nd grad_ind = cc[:, ::nd] return ncsym, ncasym, cell_node_blocks, grad_ind
def _tensor_vector_prod(g, k, subcell_topology): """ Compute product of normal vectors and tensors on a sub-cell level. This is essentially defining Darcy's law for each sub-face in terms of sub-cell gradients. Thus, we also implicitly define the global ordering of sub-cell gradient variables (via the interpretation of the columns in nk). NOTE: In the local numbering below, in particular in the variables i and j, it is tacitly assumed that g.dim == g.nodes.shape[0] == g.face_normals.shape[0] etc. See implementation note in main method. Parameters: g (core.grids.grid): Discretization grid k (core.constit.second_order_tensor): The permeability tensor subcell_topology (fvutils.SubcellTopology): Wrapper class containing subcell numbering. Returns: nk: sub-face wise product of normal vector and permeability tensor. cell_node_blocks pairings of node and cell indices, which together define a sub-cell. sub_cell_ind: index of all subcells """ # Stack cell and nodes, and remove duplicate rows. Since subcell_mapping # defines cno and nno (and others) working cell-wise, this will # correspond to a unique rows (Matlab-style) from what I understand. # This also means that the pairs in cell_node_blocks uniquely defines # subcells, and can be used to index gradients etc. cell_node_blocks, blocksz = matrix_compression.rlencode( np.vstack((subcell_topology.cno, subcell_topology.nno))) nd = g.dim # Duplicates in [cno, nno] corresponds to different faces meeting at the # same node. There should be exactly nd of these. This test will fail # for pyramids in 3D assert np.all(blocksz == nd) # Define row and column indices to be used for normal_vectors * perm. # Rows are based on sub-face numbers. # Columns have nd elements for each sub-cell (to store a gradient) and # is adjusted according to block sizes i, j = np.meshgrid(subcell_topology.subhfno, np.arange(nd)) sum_blocksz = np.cumsum(blocksz) j += matrix_compression.rldecode(sum_blocksz - blocksz[0], blocksz) # Distribute faces equally on the sub-faces num_nodes = np.diff(g.face_nodes.indptr) normals = g.face_normals[:, subcell_topology.fno] / num_nodes[ subcell_topology.fno] # Represent normals and permeability on matrix form normals_mat = sps.coo_matrix( (normals.ravel('F'), (i.ravel('F'), j.ravel('F')))).tocsr() k_mat = sps.coo_matrix((k.perm[::, ::, cell_node_blocks[0]].ravel('F'), (i.ravel('F'), j.ravel('F')))).tocsr() nk = normals_mat * k_mat # Unique sub-cell indexes are pulled from column indices, we only need # every nd column (since nd faces of the cell meet at each vertex) sub_cell_ind = j[::, 0::nd] return nk, cell_node_blocks, sub_cell_ind
def __compute_geometry_3d(self): """ Helper function to compute geometry for 3D grids The implementation is motivated by the similar MRST function. NOTE: The function is very long, and could have been broken up into parts (face and cell computations are an obvious solution). """ xn = self.nodes num_face_nodes = self.face_nodes.nnz face_node_ptr = self.face_nodes.indptr num_nodes_per_face = face_node_ptr[1:] - face_node_ptr[:-1] # Face-node relationships. Note that the elements here will also # serve as a representation of an edge along the face (face_nodes[i] # represents the edge running from face_nodes[i] to face_nodes[i+1]) face_nodes = self.face_nodes.indices # For each node, index of its parent face face_node_ind = matrix_compression.rldecode(np.arange(self.num_faces), num_nodes_per_face) # Index of next node on the edge list. Note that this assumes the # elements in face_nodes is stored in an ordered fasion next_node = np.arange(num_face_nodes) + 1 # Close loops, for face i, the next node is the first of face i next_node[face_node_ptr[1:] - 1] = face_node_ptr[:-1] # Mapping from cells to faces edge_2_face = sps.coo_matrix( (np.ones(num_face_nodes), (np.arange(num_face_nodes), face_node_ind))).tocsc() # Define temporary face center as the mean of the face nodes tmp_face_center = xn[:, face_nodes] * edge_2_face / num_nodes_per_face # Associate this value with all the edge of this face tmp_face_center = edge_2_face * tmp_face_center.transpose() # Vector along each edge along_edge = xn[:, face_nodes[next_node]] - xn[:, face_nodes] # Vector from face center to start node of each edge face_2_node = tmp_face_center.transpose() - xn[:, face_nodes] # Assign a normal vector with this edge, by taking the cross product # between along_edge and face_2_node # Divide by two to ensure that the normal vector has length equal to # the area of the face triangle (by properties of cross product) sub_normals = np.vstack(( along_edge[1] * face_2_node[2] - along_edge[2] * face_2_node[1], along_edge[2] * face_2_node[0] - along_edge[0] * face_2_node[2], along_edge[0] * face_2_node[1] - along_edge[1] * face_2_node[0], )) / 2 def nrm(v): return np.sqrt(np.sum(v * v, axis=0)) # Calculate area of sub-face associated with each edge - note that # the sub-normals are area weighted sub_areas = nrm(sub_normals) # Centers of sub-faces are given by the centroid coordinates, # e.g. the mean coordinate of the edge endpoints and the temporary # face center sub_centroids = (xn[:, face_nodes] + xn[:, face_nodes[next_node]] + tmp_face_center.transpose()) / 3 # Face normals are given as the sum of the sub-components face_normals = sub_normals * edge_2_face # Similar with face areas face_areas = edge_2_face.transpose() * sub_areas # Test whether the sub-normals are pointing in the same direction as # the main normal: Distribute the main normal onto the edges, # and take scalar product by element-wise multiplication with # sub-normals, and sum over the components (axis=0). # NOTE: There should be a built-in function for this in numpy? sub_normals_sign = np.sign( np.sum(sub_normals * (edge_2_face * face_normals.transpose()).transpose(), axis=0)) # Finally, face centers are the area weighted means of centroids of # the sub-faces face_centers = sub_areas * sub_centroids * edge_2_face / face_areas # .. and we're done with the faces. Store information self.face_centers = face_centers self.face_normals = face_normals self.face_areas = face_areas # Cells # Temporary cell center coordinates as the mean of the face center # coordinates. The cells are divided into sub-tetrahedra ( # corresponding to triangular sub-faces above), with the temporary # cell center as the final node # Mapping from edges to cells. Take absolute value of cell_faces, # since the elements are signed (contains the divergence). # Note that edge_2_cell will contain more elements than edge_2_face, # since the former will count internal faces twice (one for each # adjacent cell) edge_2_cell = edge_2_face * np.abs(self.cell_faces) # Sort indices to avoid messing up the mappings later edge_2_cell.sort_indices() # Obtain relations between edges, faces and cells, in the form of # index lists. Each element in the list corresponds to an edge seen # from a cell (e.g. edges on internal faces are seen twice). # Cell numbers are obtained from the columns in edge_2_cell. cell_numbers = matrix_compression.rldecode(np.arange(self.num_cells), np.diff(edge_2_cell.indptr)) # Edge numbers from the rows. Here it is crucial that the indices # are sorted edge_numbers = edge_2_cell.indices # Face numbers are obtained from the face-node relations (with the # nodes doubling as representation of edges) face_numbers = face_node_ind[edge_numbers] # Number of edges per cell num_cell_edges = edge_2_cell.indptr[1:] - edge_2_cell.indptr[:-1] def bincount_nd(arr, weights): """ Utility function to sum vector quantities by np.bincount. We could probably have used np.apply_along_axis, but I could not make it work. Intended use: Map sub-cell centroids to a quantity for the cell. """ dim = weights.shape[0] sz = arr.max() + 1 count = np.zeros((dim, sz)) for iter1 in range(dim): count[iter1] = np.bincount(arr, weights=weights[iter1], minlength=sz) return count # First estimate of cell centers as the mean of its faces' centers # Divide by num_cell_edges here since all edges bring in their faces tmp_cell_centers = bincount_nd( cell_numbers, face_centers[:, face_numbers] / num_cell_edges[cell_numbers]) # Distance from the temporary cell center to the sub-centroids (of # the tetrahedra associated with each edge) dist_cellcenter_subface = sub_centroids[:, edge_numbers] \ - tmp_cell_centers[:, cell_numbers] # Get sign of normal vectors, seen from all faces. # Make sure we get a numpy ndarray, and not a matrix (.A), and that # the array is 1D (squeeze) orientation = np.squeeze(self.cell_faces[face_numbers, cell_numbers].A) # Get outwards pointing sub-normals for all sub-faces: We need to # account for both the orientation of the face, and the orientation # of sub-faces relative to faces. outer_normals = sub_normals[:, edge_numbers] \ * orientation * sub_normals_sign[edge_numbers] # Volumes of tetrahedra are now given by the dot product between the # outer normal (which is area weighted, and thus represent the base # of the tet), with the distancance from temporary cell center (the # dot product gives the hight). tet_volumes = np.sum(dist_cellcenter_subface * outer_normals, axis=0) / 3 # Sometimes the sub-tet volumes can have a volume of numerical zero. # Why this is so is not clear, but for the moment, we allow for a # slightly negative value. assert np.all(tet_volumes > -1e-12) # On the fly test # The cell volumes are now found by summing sub-tetrahedra cell_volumes = np.bincount(cell_numbers, weights=tet_volumes) tri_centroids = 3 / 4 * dist_cellcenter_subface # Compute a correction to the temporary cell center, by a volume # weighted sum of the sub-tetrahedra rel_centroid = bincount_nd(cell_numbers, tet_volumes * tri_centroids) \ / cell_volumes cell_centers = tmp_cell_centers + rel_centroid # ... and we're done self.cell_centers = cell_centers self.cell_volumes = cell_volumes
def generate_coarse_grid(g, subdiv): """ Generate a coarse grid clustering the cells according to the flags given by subdiv. Subdiv should be long as the number of cells in the original grid, it contains integers (possibly not continuous) which represent the cells in the final mesh. The values computed in "compute_geometry" are not preserved and they should be computed out from this function. Note: there is no check for disconnected cells in the final grid. Parameters: g: the grid subdiv: a list of flags, one for each cell of the original grid How to use: subdiv = np.array([0,0,1,1,1,1,3,4,6,4,6,4]) g = generate_coarse_grid( g, subdiv ) """ subdiv = np.asarray(subdiv) assert (subdiv.size == g.num_cells) # declare the storage array to build the cell_faces map cell_faces = np.empty(0, dtype=g.cell_faces.indptr.dtype) cells = np.empty(0, dtype=cell_faces.dtype) orient = np.empty(0, dtype=g.cell_faces.data.dtype) # declare the storage array to build the face_nodes map face_nodes = np.empty(0, dtype=g.face_nodes.indptr.dtype) nodes = np.empty(0, dtype=face_nodes.dtype) visit = np.zeros(g.num_faces, dtype=np.bool) # compute the face_node indexes num_nodes_per_face = g.face_nodes.indptr[1:] - g.face_nodes.indptr[:-1] face_node_ind = matrix_compression.rldecode(np.arange(g.num_faces), \ num_nodes_per_face) cells_list = np.unique(subdiv) for cellId, cell in enumerate(cells_list): # extract the cells of the original mesh associated to a specific label cells_old = np.where(subdiv == cell)[0] # reconstruct the cell_faces mapping faces_old, _, orient_old = sps.find(g.cell_faces[:, cells_old]) mask = np.ones(faces_old.size, dtype=np.bool) mask[np.unique(faces_old, return_index=True)[1]] = False # extract the indexes of the internal edges, to be discared index = np.array([ np.where( faces_old == f )[0] \ for f in faces_old[mask]]).ravel() faces_new = np.delete(faces_old, index) cell_faces = np.r_[cell_faces, faces_new] cells = np.r_[cells, np.repeat(cellId, faces_new.shape[0])] orient = np.r_[orient, np.delete(orient_old, index)] # reconstruct the face_nodes mapping # consider only the unvisited faces not_visit = ~visit[faces_new] if not_visit.size == 0: continue # mask to consider only the external faces mask = np.sum( [ face_node_ind == f for f in faces_new[not_visit] ], \ axis = 0, dtype = np.bool ) face_nodes = np.r_[face_nodes, face_node_ind[mask]] nodes_new = g.face_nodes.indices[mask] nodes = np.r_[nodes, nodes_new] visit[faces_new] = True # Rename the faces cell_faces_unique = np.unique(cell_faces) cell_faces_id = np.arange(cell_faces_unique.size, dtype=cell_faces.dtype) cell_faces = np.array([cell_faces_id[np.where( cell_faces_unique == f )[0]]\ for f in cell_faces]).ravel() shape = (cell_faces_unique.size, cells_list.size) cell_faces = sps.csc_matrix((orient, (cell_faces, cells)), shape=shape) # Rename the nodes face_nodes = np.array([cell_faces_id[np.where( cell_faces_unique == f )[0]]\ for f in face_nodes]).ravel() nodes_list = np.unique(nodes) nodes_id = np.arange(nodes_list.size, dtype=nodes.dtype) nodes = np.array([nodes_id[np.where( nodes_list == n )[0]] \ for n in nodes]).ravel() # sort the nodes nodes = nodes[np.argsort(face_nodes, kind='mergesort')] data = np.ones(nodes.size, dtype=g.face_nodes.data.dtype) indptr = np.r_[0, np.cumsum(np.bincount(face_nodes))] face_nodes = sps.csc_matrix((data, nodes, indptr)) name = g.name name.append("coarse") return Grid(g.dim, g.nodes[:, nodes_list], face_nodes, cell_faces, name)