Example #1
0
def update_physical_high_grid(mg, g_new, g_old, tol):

    split_matrix = {}

    if mg.dim == 0:

        # retrieve the old faces and the corresponding coordinates
        _, old_faces, _ = sps.find(mg.high_to_mortar_int)
        old_nodes = g_old.face_centers[:, old_faces]

        # retrieve the boundary faces and the corresponding coordinates
        new_faces = g_new.get_boundary_faces()
        new_nodes = g_new.face_centers[:, new_faces]

        # we assume only one old node
        mask = cg.dist_point_pointset(old_nodes, new_nodes) < tol
        new_faces = new_faces[mask]

        shape = (g_old.num_faces, g_new.num_faces)
        matrix_DIJ = (np.ones(old_faces.shape), (old_faces, new_faces))
        split_matrix = sps.csc_matrix(matrix_DIJ, shape=shape)

    elif mg.dim == 1:
        # The case is conceptually similar to 0d, but quite a bit more
        # technical. Implementation is moved to separate function
        split_matrix = _match_grids_along_line_from_geometry(
            mg, g_new, g_old, tol)

    else:  # should be mg.dim == 2
        # It should be possible to use essentially the same approach as in 1d,
        # but this is not yet covered.
        raise NotImplementedError("Have not yet implemented this.")

    mg.update_high(split_matrix)
Example #2
0
def remesh_1d(g_old, num_nodes, tol=1e-6):
    """ Create a new 1d mesh covering the same domain as an old one.

    The new grid is equispaced, and there is no guarantee that the nodes in
    the old and new grids are coincinding. Use with care, in particular for
    grids with internal boundaries.

    Parameters:
        g_old (grid): 1d grid to be replaced.
        num_nodes (int): Number of nodes in the new grid.
        tol (double, optional): Tolerance used to compare node coornidates
            (for mapping of boundary conditions). Defaults to 1e-6.

    Returns:
        grid: New grid.

    """

    # Create equi-spaced nodes covering the same domain as the old grid
    theta = np.linspace(0, 1, num_nodes)
    start, end = g_old.get_boundary_nodes()
    # Not sure why the new axis was necessary.
    nodes = g_old.nodes[:, start,
                        np.newaxis] * theta + g_old.nodes[:, end,
                                                          np.newaxis] * (1. -
                                                                         theta)

    # Create the new grid, and assign nodes.
    g = TensorGrid(nodes[0, :])
    g.nodes = nodes
    g.compute_geometry()

    # map the tags from the old grid to the new one

    # retrieve the old faces and the corresponding coordinates
    old_frac_faces = np.where(g_old.tags["fracture_faces"].ravel())[0]

    # compute the mapping from the old boundary to the new boundary
    # we need to go through the coordinates

    new_frac_face = []

    for fi in old_frac_faces:
        nfi = np.where(
            cg.dist_point_pointset(g_old.face_centers[:, fi], nodes) < tol)[0]
        if len(nfi) > 0:
            new_frac_face.append(nfi[0])

    # This can probably be made more elegant
    g.tags["fracture_faces"][new_frac_face] = True

    # Fracture tips should be on the boundary only.
    if np.any(g_old.tags["tip_faces"]):
        g.tags["tip_faces"] = g.tags["domain_boundary_faces"]

    return g
Example #3
0
def remesh_1d(g_old, num_nodes, tol=1e-6):
    """ Create a new 1d mesh covering the same domain as an old one.

    The new grid is equispaced, and there is no guarantee that the nodes in
    the old and new grids are coincinding. Use with care, in particular for
    grids with internal boundaries.

    Parameters:
        g_old (grid): 1d grid to be replaced.
        num_nodes (int): Number of nodes in the new grid.
        tol (double, optional): Tolerance used to compare node coornidates
            (for mapping of boundary conditions). Defaults to 1e-6.

    Returns:
        grid: New grid.

    """

    # Create equi-spaced nodes covering the same domain as the old grid
    theta = np.linspace(0, 1, num_nodes)
    start, end = g_old.get_all_boundary_nodes()
    # Not sure why the new axis was necessary.
    nodes = g_old.nodes[:, start,
                        np.newaxis] * theta + g_old.nodes[:, end,
                                                          np.newaxis] * (1. -
                                                                         theta)

    # Create the new grid, and assign nodes.
    g = TensorGrid(nodes[0, :])
    g.nodes = nodes
    g.compute_geometry()

    # map the tags from the old grid to the new one
    # normally the tags are given at faces/point that are fixed the 1d mesh
    # we use this assumption to proceed.
    for f_old in np.arange(g_old.num_faces):
        # detect in the new grid which face is geometrically the same (upon a tolerance)
        # as in the old grid
        dist = cg.dist_point_pointset(g_old.face_centers[:, f_old],
                                      g.face_centers)
        f_new = np.where(dist < tol)[0]

        # if you find a match transfer all the tags from the face in the old grid to
        # the face in the new grid
        if f_new.size:
            if f_new.size != 1:
                raise ValueError(
                    "It cannot be more than one face, something went wrong")
            for tag in pp.utils.tags.standard_face_tags():
                g.tags[tag][f_new] = g_old.tags[tag][f_old]

    g.update_boundary_node_tag()

    return g
Example #4
0
def _find_nodes_on_line(g, nx, s_pt, e_pt):
    """
    We have the start and end point of the fracture. From this we find the 
    start and end node and use the structure of the cartesian grid to find
    the intermediate nodes.
    """
    s_node = np.argmin(cg.dist_point_pointset(s_pt, g.nodes))
    e_node = np.argmin(cg.dist_point_pointset(e_pt, g.nodes))

    # We make sure the nodes are ordered from low to high.
    if s_node > e_node:
        tmp = s_node
        s_node = e_node
        e_node = tmp
    # We now find the other grid nodes. We here use the node ordering of
    # meshgrid (which is used by the TensorGrid class).

    # We find the number of nodes along each dimension. From this we find the
    # jump in node number between two consecutive nodes.

    if np.all(np.isclose(s_pt[1:], e_pt[1:])):
        # x-line:
        nodes = np.arange(s_node, e_node + 1)
    elif np.all(np.isclose(s_pt[[0, 2]], e_pt[[0, 2]])):
        # y-line
        nodes = np.arange(s_node, e_node + 1, nx[0] + 1, dtype=int)

    elif nx.size == 3 and np.all(np.isclose(s_pt[0:2], e_pt[0:2])):
        # is z-line
        nodes = np.arange(s_node,
                          e_node + 1, (nx[0] + 1) * (nx[1] + 1),
                          dtype=int)
    else:
        raise RuntimeError(
            "Something went wrong. Found a diagonal intersection")
    return nodes
Example #5
0
def update_cell_faces(g,
                      delete_faces,
                      new_faces,
                      in_combined,
                      fn_orig,
                      node_coord_orig,
                      tol=1e-4):
    """ Replace faces in a cell-face map.

    If faces have been refined (or otherwise modified), it is necessary to
    update the cell-face relation as well. This function does so, while taking
    care that the (implicit) mapping between cells and nodes is ordered so that
    geometry computation still works.

    The changes of g.cell_faces are done in-place.

    It is assumed that the new faces that replace an old are ordered along the
    common line. E.g. if a face with node coordinates (0, 0) and (3, 0) is
    replaced by three new faces of unit length, they should be ordered as
    1. (0, 0) - (1, 0)
    2. (1, 0) - (2, 0)
    3. (2, 0) - (3, 0)
    Switching the order into 3, 2, 1 is okay, but, say, 1, 3, 2 will create
    problems.

    It is also tacitly assumed that each cell has at most one face deleted.
    Changing this may not be difficult, but has not been prioritized so far.

    The function has been tested in 2d only, reliability in 3d is unknown,
    but doubtful.

    Parameters:
        g (grid): To be updated.
        delete_faces (np.ndarray): Faces to be deleted, as found in
            g.cell_faces
        new_faces (np.ndarray): Index of new faces, as found in g.face_nodes
        in_combined (np.ndarray): Map between old and new faces.
            delete_faces[i] is replaced by
            new_faces[in_combined[i]:in_combined[i+1]].
        fn_orig (np.ndarray): Face-node relation of the orginial grid, before
            update of faces.
        node_coord_orig (np.ndarray): Node coordinates of orginal grid,
            before update of nodes.
        tol (double, defaults to 1e-4): Small tolerance, used to compare
            coordinates of points.

    """

    #

    nodes_per_face = g.dim

    cell_faces = g.cell_faces

    # Mapping from new
    deleted_2_new_faces = np.empty(in_combined.size - 1, dtype=object)

    # The nodes in the original 1d grid was sorted either in the same way, or
    # in the oposite order of the new grid. In the latter case, we need to
    # reverse the order of in_combined to reconstruct the old face-node
    # relations
    if in_combined[0] < in_combined[-1]:
        for i in range(deleted_2_new_faces.size):
            if in_combined[i] == in_combined[i + 1]:
                deleted_2_new_faces[i] = new_faces[in_combined[i]]
            else:
                deleted_2_new_faces[i] = new_faces[np.arange(
                    in_combined[i], in_combined[i + 1])]
#            assert deleted_2_new_faces[i].size > 0, \
#                str(i)+" "+str(in_combined[i])+" "+str(in_combined[i+1])+\
#                " "+str(np.arange(in_combined[i], in_combined[i+1]))
    else:
        for i in range(deleted_2_new_faces.size):
            if in_combined[i] == in_combined[i + 1]:
                print(new_faces)
                deleted_2_new_faces[i] = new_faces[in_combined[i]]
            else:
                deleted_2_new_faces[i] = new_faces[np.arange(
                    in_combined[i + 1], in_combined[i])]


#            assert deleted_2_new_faces[i].size > 0, \
#                str(i)+" "+str(in_combined[i+1])+" "+str(in_combined[i])+\
#                " "+str(np.arange(in_combined[i+1], in_combined[i]))

# Now that we have mapping from old to new faces, also update face tags
    update_face_tags(g, delete_faces, deleted_2_new_faces)

    # The cell-face relations
    cf = cell_faces.indices
    indptr = cell_faces.indptr

    # Find elements in the cell-face relation that are also along the
    # intersection, and should be replaced
    hit = np.where(np.in1d(cf, delete_faces))[0]

    # Mapping from cell_face of 2d grid to cells in 1d grid. Can be combined
    # with deleted_2_new_faces to match new and old faces
    # Safeguarding (or stupidity?): Only faces along 1d grid have non-negative
    # index, but we should never hit any of the other elements
    cf_2_f = -np.ones(delete_faces.max() + 1, dtype=np.int)
    cf_2_f[delete_faces] = np.arange(delete_faces.size)

    # Map from faces, as stored in cell_faces,to the corresponding cells
    face_2_cell = rldecode(np.arange(indptr.size), np.diff(indptr))

    # The cell-face map will go from 3 faces per cell to an arbitrary number.
    # Split mapping into list of arrays to prepare for this
    new_cf = [cf[indptr[i]:indptr[i + 1]] for i in range(g.num_cells)]
    # Similar treatment of direction of normal vectors
    new_sgn = [g.cell_faces.data[indptr[i]:indptr[i+1]] \
                   for i in range(g.num_cells)]

    # Create mapping to adjust face indices for deletions
    tmp = np.arange(cf.max() + 1)
    adjust_deleted = np.zeros_like(tmp)
    adjust_deleted[delete_faces] = 1
    face_adjustment = tmp - np.cumsum(adjust_deleted)

    # Face-node relations as array
    fn = g.face_nodes.indices.reshape((nodes_per_face, g.num_faces), order='F')

    # Collect indices of cells that have one of their faces on the fracture.
    hit_cell = []

    for i in hit:
        # The loop variable refers to indices in the face-cell map. Get cell
        # number.
        cell = face_2_cell[i]
        hit_cell.append(cell)
        # For this cell, find where in the cell-face map the fracture face is
        # placed.
        tr = np.where(new_cf[cell] == cf[i])[0]
        # There should be only one face on the fracture
        assert tr.size == 1
        tr = tr[0]

        # Implementation note: If we ever get negative indices here, something
        # has gone wrong related to cf_2_f, see above.
        # Digestion of loop: i (in hit) refers to elements in cell-face
        # cf[i] is specific face
        # cf_2_f[cf[i]] maps to deleted face along fracture
        # outermost is one-to-many map from deleted to new faces.
        new_faces_loc = deleted_2_new_faces[cf_2_f[cf[i]]]

        # Index of the replaced face
        ci = cf[i]

        # We need to sort the new face-cell relation so that the edges defined
        # by cell-face-> face_nodes form a closed, non-intersecting loop. If
        # this is not the case, geometry computation will go wrong.
        # By assumption, the new faces are defined so that their nodes are
        # contiguous along the line of the old face.

        # Coordinates of the nodes of the replaced face.
        # Note use of original coordinates here.
        ci_coord = node_coord_orig[:, fn_orig[:, ci]]
        # Coordinates of the nodes of the first new face
        fi_coord = g.nodes[:, fn[:, new_faces_loc[0]]]

        # Distance between the new nodes and the first node of the old face.
        dist = cg.dist_point_pointset(ci_coord[:, 0], fi_coord)
        # Length of the old face.
        length_face = cg.dist_point_pointset(ci_coord[:, 0], ci_coord[:, 1])[0]
        # If the minimum distance is larger than a (scaled) tolerance, the new
        # faces were defined from the second to the first node. Switch order.
        # This will create trouble if one of the new faces are very small.
        if dist.min() > length_face * tol:
            new_faces_loc = new_faces_loc[::-1]

        # Replace the cell-face relation for this cell.
        # At the same time (stupid!) also adjust indices of the surviving
        # faces.
        new_cf[cell] = np.hstack(
            (face_adjustment[new_cf[cell][:tr].ravel()], new_faces_loc,
             face_adjustment[new_cf[cell][tr + 1:].ravel()]))
        # Also replicate directions of normal vectors
        new_sgn[cell] = np.hstack(
            (new_sgn[cell][:tr].ravel(),
             np.tile(new_sgn[cell][tr],
                     new_faces_loc.size), new_sgn[cell][tr + 1:].ravel()))

    # Adjust face index of cells that have no contact with the updated faces
    for i in np.setdiff1d(np.arange(len(new_cf)), hit_cell):
        new_cf[i] = face_adjustment[new_cf[i]]

    # New pointer structure for cell-face relations
    num_cell_face = np.array([new_cf[i].size for i in range(len(new_cf))])
    indptr_new = np.hstack((0, np.cumsum(num_cell_face)))

    ind = np.concatenate(new_cf)
    data = np.concatenate(new_sgn)
    # All faces in the cell-face relation should be referred to by 1 or 2 cells
    assert np.bincount(ind).max() <= 2
    assert np.all(np.bincount(ind) > 0)

    g.cell_faces = sps.csc_matrix((data, ind, indptr_new))
Example #6
0
def cart_grid_3d(fracs, nx, physdims=None):
    """
    Create grids for a domain with possibly intersecting fractures in 3d.

    Based on rectangles describing the individual fractures, the method
    constructs grids in 3d (the whole domain), 2d (one for each individual
    fracture), 1d (along fracture intersections), and 0d (meeting between
    intersections).

    Parameters
    ----------
    fracs (list of np.ndarray, each 3x4): Vertexes in the rectangle for each
        fracture. The vertices must be sorted and aligned to the axis.
        The fractures will snap to the closest grid faces.
    nx (np.ndarray): Number of cells in each direction. Should be 3D.
    physdims (np.ndarray): Physical dimensions in each direction.
        Defaults to same as nx, that is, cells of unit size.

    Returns
    -------
    list (length 4): For each dimension (3 -> 0), a list of all grids in
        that dimension.

    Examples
    --------
    frac1 = np.array([[1,1,4,4], [1,4,4,1], [2,2,2,2]])
    frac2 = np.array([[2,2,2,2], [1,1,4,4], [1,4,4,1]])
    fracs = [frac1, frac2]
    gb = cart_grid_3d(fracs, [5,5,5])
    """

    nx = np.asarray(nx)
    if physdims is None:
        physdims = nx
    elif np.asarray(physdims).size != nx.size:
        raise ValueError("Physical dimension must equal grid dimension")
    else:
        physdims = np.asarray(physdims)

    # We create a 3D cartesian grid. The global node mapping is trivial.
    g_3d = structured.CartGrid(nx, physdims=physdims)
    g_3d.global_point_ind = np.arange(g_3d.num_nodes)
    g_3d.compute_geometry()
    g_2d = []
    g_1d = []
    g_0d = []
    # We set the tolerance for finding points in a plane. This can be any
    # small number, that is smaller than .25 of the cell sizes.
    tol = .1 * physdims / nx

    # Create 2D grids
    for fi, f in enumerate(fracs):
        assert np.all(f.shape == (3, 4)), "fractures must have shape [3,4]"
        is_xy_frac = np.allclose(f[2, 0], f[2])
        is_xz_frac = np.allclose(f[1, 0], f[1])
        is_yz_frac = np.allclose(f[0, 0], f[0])
        assert (is_xy_frac + is_xz_frac +
                is_yz_frac == 1), "Fracture must align to x-, y- or z-axis"
        # snap to grid
        f_s = (np.round(f * nx[:, np.newaxis] / physdims[:, np.newaxis]) *
               physdims[:, np.newaxis] / nx[:, np.newaxis])
        if is_xy_frac:
            flat_dim = [2]
            active_dim = [0, 1]
        elif is_xz_frac:
            flat_dim = [1]
            active_dim = [0, 2]
        else:
            flat_dim = [0]
            active_dim = [1, 2]
        # construct normal vectors. If the rectangle is ordered
        # clockwise we need to flip the normals so they point
        # outwards.
        sign = 2 * cg.is_ccw_polygon(f_s[active_dim]) - 1
        tangent = f_s.take(np.arange(f_s.shape[1]) + 1, axis=1,
                           mode="wrap") - f_s
        normal = tangent
        normal[active_dim] = tangent[active_dim[1::-1]]
        normal[active_dim[1]] = -normal[active_dim[1]]
        normal = sign * normal
        # We find all the faces inside the convex hull defined by the
        # rectangle. To find the faces on the fracture plane, we remove any
        # faces that are further than tol from the snapped fracture plane.
        in_hull = half_space.half_space_int(normal, f_s, g_3d.face_centers)
        f_tag = np.logical_and(
            in_hull,
            np.logical_and(
                f_s[flat_dim, 0] - tol[flat_dim] <=
                g_3d.face_centers[flat_dim],
                g_3d.face_centers[flat_dim] < f_s[flat_dim, 0] + tol[flat_dim],
            ),
        )
        f_tag = f_tag.ravel()
        nodes = sps.find(g_3d.face_nodes[:, f_tag])[0]
        nodes = np.unique(nodes)
        loc_coord = g_3d.nodes[:, nodes]
        g = _create_embedded_2d_grid(loc_coord, nodes)

        g.frac_num = fi
        g_2d.append(g)

    # Create 1D grids:
    # Here we make use of the network class to find the intersection of
    # fracture planes. We could maybe avoid this by doing something similar
    # as for the 2D-case, and count the number of faces belonging to each edge,
    # but we use the FractureNetwork class for now.
    frac_list = []
    for f in fracs:
        frac_list.append(fractures.Fracture(f))
    # Combine the fractures into a network
    network = fractures.FractureNetwork(frac_list)
    # Impose domain boundary. For the moment, the network should be immersed in
    # the domain, or else gmsh will complain.
    box = {
        "xmin": 0,
        "ymin": 0,
        "zmin": 0,
        "xmax": physdims[0],
        "ymax": physdims[1],
        "zmax": physdims[2],
    }
    network.impose_external_boundary(box)

    # Find intersections and split them.
    network.find_intersections()
    network.split_intersections()

    # Extract geometrical network information.
    pts = network.decomposition["points"]
    edges = network.decomposition["edges"]
    poly = network._poly_2_segment()
    # And tags identifying points and edges corresponding to normal
    # fractures, domain boundaries and subdomain boundaries. Only the
    # entities corresponding to normal fractures should actually be gridded.
    edge_tags, intersection_points = network._classify_edges(poly)
    const = constants.GmshConstants()
    auxiliary_points, edge_tags = network.on_domain_boundary(edges, edge_tags)
    bound_and_aux = np.array([const.DOMAIN_BOUNDARY_TAG, const.AUXILIARY_TAG])
    edges = np.vstack((edges, edge_tags))

    # Loop through the edges to make 1D grids. Ommit the auxiliary edges.
    for e in np.ravel(
            np.where(edges[2] == const.FRACTURE_INTERSECTION_LINE_TAG)):
        # We find the start and end point of each fracture intersection (1D
        # grid) and then the corresponding global node index.
        if np.isin(edge_tags[e], bound_and_aux):
            continue
        s_pt = pts[:, edges[0, e]]
        e_pt = pts[:, edges[1, e]]
        nodes = _find_nodes_on_line(g_3d, nx, s_pt, e_pt)
        loc_coord = g_3d.nodes[:, nodes]
        assert (loc_coord.shape[1] > 1), "1d grid in intersection should span\
            more than one node"

        g = mesh_2_grid.create_embedded_line_grid(loc_coord, nodes)
        g_1d.append(g)

    # Create 0D grids
    # Here we also use the intersection information from the FractureNetwork
    # class. No grids for auxiliary points.
    for p in intersection_points:
        if auxiliary_points[p]:
            continue
        node = np.argmin(cg.dist_point_pointset(pts[:, p], g_3d.nodes))
        assert np.allclose(g_3d.nodes[:, node], pts[:, p])
        g = point_grid.PointGrid(g_3d.nodes[:, node])
        g.global_point_ind = np.asarray(node)
        g_0d.append(g)

    grids = [[g_3d], g_2d, g_1d, g_0d]
    return grids