def test_optional_arguments(self): # Test automatic check of ambient dimension proj = pp.TangentialNormalProjection(self.n2) self.assertTrue(proj.dim == 2) proj = pp.TangentialNormalProjection(self.n3) self.assertTrue(proj.dim == 3)
def test_normal_vectors(self): # Test that the normal vectors have proj = pp.TangentialNormalProjection(self.n2) for i in range(self.n2.shape[1]): self.assertTrue( np.allclose( np.sum(proj.normals[:, i] * self.n2_normalized[:, i]), 1)) proj = pp.TangentialNormalProjection(self.n3) for i in range(self.n3.shape[1]): self.assertTrue( np.allclose( np.sum(proj.normals[:, i] * self.n3_normalized[:, i]), 1))
def test_project_tangential_normal_3d(self): # Test projections into normal and tangential directions in 2d. # Only a single vector is projected, the multi-vector option is handled in # another test. s = 1 / np.sqrt(3) n = np.array([[s], [s], [s]]) proj = pp.TangentialNormalProjection(n) # Vectors to be projected vecs = [ n, np.array([[1], [0], [0]]), np.array([[0], [1], [0]]), np.array([[-1 / np.sqrt(2)], [1 / np.sqrt(2)], [0]]), ] # Length of the vectors when projected onto the tangent space known_tangential_projections = [0, np.sqrt(2 / 3), np.sqrt(2 / 3), 1] # length of the vectors when projected onto the normal space known_normal_projections = [1, s, s, 0] for i, v in enumerate(vecs): # Project tangential direciton tangential_length = np.linalg.norm(proj.project_tangential() * v) self.assertTrue( np.allclose(known_tangential_projections[i], tangential_length)) # project in normal direction normal_length = np.abs(proj.project_normal() * v) self.assertTrue( np.allclose(known_normal_projections[i], normal_length))
def test_project_tangential_normal_2d_several_vectors(self): # Test projection of several vectors, using the same projection direction n = np.array([[1], [0]]) proj = pp.TangentialNormalProjection(n) # Vector to be projected. When interpreted as three stacked vectors, the first # has only normal component, the second only tangential component, and the third # equal components of both vec = np.array([1, 0, 0, 1, 1, 1]).reshape((-1, 1)) known_tangential_parts = np.array([0, 1, 1]).reshape((-1, 1)) known_normal_parts = np.array([1, 0, 1]).reshape((-1, 1)) # Compute projection in tangential direction. # absolute value is needed to account for randomness in positive tangential # direction proj_tangential = np.abs(proj.project_tangential(3) * vec) self.assertTrue(np.allclose(known_tangential_parts, proj_tangential)) # No absolute value for normal direction proj_normal = proj.project_normal(3) * vec self.assertTrue(np.allclose(proj_normal, known_normal_parts)) known_tangential_normal = np.array([[0, 1, 1, 0, 1, 1]]).reshape( (-1, 1)) proj_full = proj.project_tangential_normal(3) * vec self.assertTrue(np.allclose(proj_full, known_tangential_normal))
def test_computed_basis_3d(self): # Test that the computed basis functions are orthonormal, and that the # correct normal vector is constructed proj = pp.TangentialNormalProjection(self.n3) self._verify_orthonormal(proj) known_projection_of_normal = np.array([0, 0, 1]) for i in range(self.n3.shape[1]): # Check that the projection of the normal vector only has a component in the normal direction projected_normal = proj.projection[:, :, i].dot(self.n3_normalized[:, i]) self.assertTrue( np.allclose(projected_normal, known_projection_of_normal))
def test_project_tangential_normal_30deg_2d(self): # Test projections into normal and tangential directions in 2d. # Only a single vector is projected, the multi-vector option is handled in # another test. c = np.cos(np.pi / 6) s = np.sin(np.pi / 6) n = np.array([[c], [s]]) proj = pp.TangentialNormalProjection(n) # Vectors to be projected vecs = [ n, np.array([[1], [0]]), np.array([[0], [1]]), np.array([[-s], [c]]) ] # Length of the vectors when projected onto the tangent space known_tangential_projections = [0, s, c, 1] # length of the vectors when projected onto the normal space known_normal_projections = [1, c, s, 0] known_projected_vecs = [ np.array([[0], [1]]), np.array([[s], [c]]), np.array([[c], [s]]), np.array([[1], [0]]), ] for i, v in enumerate(vecs): # Project tangential direction # Add an absolute value since we don't know the positive direction chosen # for the tangential space (this is chosen by random in the construction # of the projection object). tangential_length = np.abs(proj.project_tangential() * v) self.assertTrue( np.allclose(known_tangential_projections[i], tangential_length)) # project in normal direction # No need for absolute values here normal_length = proj.project_normal() * v self.assertTrue( np.allclose(known_normal_projections[i], normal_length)) pv = np.abs(proj.project_tangential_normal() * v) self.assertTrue(np.allclose(known_projected_vecs[i], pv))
def test_several_normal_vectors(self): dim = 2 s2 = np.sqrt(2) s3 = np.sqrt(3) n1 = np.array([[0.5], [s3 / 2]]) n2 = np.array([[s2 / 2], [s2 / 2]]) proj = pp.TangentialNormalProjection(np.hstack((n1, n2))) # Two 2d vectors stacked. Both have x-component 1, y 0 v = np.array([1, 0, 1, 0]).reshape((-1, 1)) t_proj = proj.project_tangential() * v n_proj = proj.project_normal() * v self.assertTrue( np.allclose(np.abs(t_proj), np.array([[s3 / 2], [s2 / 2]]))) self.assertTrue(np.allclose(n_proj, np.array([[0.5], [s2 / 2]])))
def test_projections_num_keyword(self): # Tests of the generated projection operators, using a single tangential/ # normal space, but generating several (equal) projection matrices. dim = 3 # Random normal and tangential space proj = pp.TangentialNormalProjection(np.random.rand(dim, 1)) num_reps = 4 # Random vector to be generated vector = np.random.rand(dim, 1) projection = proj.project_tangential_normal(num=num_reps) proj_vector = projection * np.tile(vector, (num_reps, 1)) for i in range(dim): for j in range(num_reps): self.assertTrue(proj_vector[i + j * dim], proj_vector[i])
def test_project_tangential_normal_3d_several_vectors(self): # Test projection of several vectors, using the same projection direction n = np.array([[1], [0], [0]]) proj = pp.TangentialNormalProjection(n) # Vector to be projected. When interpreted as three stacked vectors, the first # has only normal component, the second only tangential component, and the third # equal components of both vec = np.array([1, 0, 0, 0, 1, 0, 1, 1, 0]).reshape((-1, 1)) # Compute projection in tangential direction. proj_tangential = np.abs(proj.project_tangential(3) * vec) # The first two components should both be zero self.assertTrue(np.allclose(proj_tangential[:2], 0)) # The components corresponding to the second vector should together have unit # length self.assertTrue(np.allclose(np.linalg.norm(proj_tangential[2:4]), 1)) # The components corresponding to the third vector should together have unit # length self.assertTrue(np.allclose(np.linalg.norm(proj_tangential[4:]), 1))
def set_projections(gb): """ Define a local coordinate system, and projection matrices, for all grids of co-dimension 1. The function adds one item to the data dictionary of all GridBucket edges that neighbors a co-dimension 1 grid, defined as: key: tangential_normal_projection, value: pp.TangentialNormalProjection provides projection to the surface of the lower-dimensional grid Note that grids of co-dimension 2 and higher are ignored in this construction, as we do not plan to do contact mechanics on these objects. It is assumed that the surface is planar. """ # Information on the vector normal to the surface is not available directly # from the surface grid (it could be constructed from the surface geometry, # which spans the tangential plane). We instead get the normal vector from # the adjacent higher dimensional grid. # We therefore access the grids via the edges of the mixed-dimensional grid. for e, d_m in gb.edges(): mg = d_m["mortar_grid"] # Only consider edges where the lower-dimensional neighbor is of co-dimension 1 if not mg.dim == (gb.dim_max() - 1): continue # Neigboring grids _, g_h = gb.nodes_of_edge(e) # Find faces of the higher dimensional grid that coincide with the mortar # grid. Go via the master to mortar projection # Convert matrix to csr, then the relevant face indices are found from # the row indices faces_on_surface = mg.master_to_mortar_int().tocsr().indices # Find out whether the boundary faces have outwards pointing normal vectors # Negative sign implies that the normal vector points inwards. sgn = g_h.sign_of_faces(faces_on_surface) # Unit normal vector unit_normal = g_h.face_normals[: g_h.dim] / g_h.face_areas # Ensure all normal vectors on the relevant surface points outwards unit_normal[:, faces_on_surface] *= sgn # Now we need to pick out *one* normal vector of the higher dimensional grid # which coincides with this mortar grid. This could probably have been # done with face tags, but we instead project the normal vectors onto the # mortar grid to kill off all irrelevant faces. Restriction to a single # normal vector is done in the construction of the projection object # (below). # NOTE: Use a single normal vector to span the tangential and normal space, # thus assuming the surface is planar. outwards_unit_vector_mortar = mg.master_to_mortar_int().dot(unit_normal.T).T # NOTE: The normal vector is based on the first cell in the mortar grid, # and will be pointing from that cell towards the other side of the # mortar grid. This defines the positive direction in the normal direction. # Although a simpler implementation seems to be possible, going via the # first element in faces_on_surface, there is no guarantee that this will # give us a face on the positive (or negative) side, hence the more general # approach is preferred. # # NOTE: The basis for the tangential direction is determined by the # construction internally in TangentialNormalProjection. projection = pp.TangentialNormalProjection( outwards_unit_vector_mortar[:, 0].reshape((-1, 1)) ) # Store the projection operator in the mortar data d_m["tangential_normal_projection"] = projection
def set_projections( gb: pp.GridBucket, edges: Optional[List[Tuple[pp.Grid, pp.Grid]]] = None) -> None: """Define a local coordinate system, and projection matrices, for all grids of co-dimension 1. The function adds one item to the data dictionary of all GridBucket edges that neighbors a co-dimension 1 grid, defined as: key: tangential_normal_projection, value: pp.TangentialNormalProjection provides projection to the surface of the lower-dimensional grid Note that grids of co-dimension 2 and higher are ignored in this construction, as we do not plan to do contact mechanics on these objects. It is assumed that the surface is planar. """ if edges is None: edges = [e for e, _ in gb.edges()] # Information on the vector normal to the surface is not available directly # from the surface grid (it could be constructed from the surface geometry, # which spans the tangential plane). We instead get the normal vector from # the adjacent higher dimensional grid. # We therefore access the grids via the edges of the mixed-dimensional grid. for e in edges: d_m = gb.edge_props(e) mg = d_m["mortar_grid"] # Only consider edges where the lower-dimensional neighbor is of co-dimension 1 if not mg.dim == (gb.dim_max() - 1): continue # Neigboring grids g_l, g_h = gb.nodes_of_edge(e) # Find faces of the higher dimensional grid that coincide with the mortar # grid. Go via the primary to mortar projection # Convert matrix to csr, then the relevant face indices are found from # the (column) indices faces_on_surface = mg.primary_to_mortar_int().tocsr().indices # Find out whether the boundary faces have outwards pointing normal vectors # Negative sign implies that the normal vector points inwards. sgn, _ = g_h.signs_and_cells_of_boundary_faces(faces_on_surface) # Unit normal vector unit_normal = g_h.face_normals[:g_h.dim] / g_h.face_areas # Ensure all normal vectors on the relevant surface points outwards unit_normal[:, faces_on_surface] *= sgn # Now we need to pick out *one* normal vector of the higher dimensional grid # which coincides with this mortar grid, so we kill off all entries for the # "other" side: unit_normal[:, mg._ind_face_on_other_side] = 0 # Project to the mortar and then to the fracture outwards_unit_vector_mortar = mg.primary_to_mortar_int().dot( unit_normal.T).T normal_lower = mg.mortar_to_secondary_int().dot( outwards_unit_vector_mortar.T).T # NOTE: The normal vector is based on the first cell in the mortar grid, # and will be pointing from that cell towards the other side of the # mortar grid. This defines the positive direction in the normal direction. # Although a simpler implementation seems to be possible, going via the # first element in faces_on_surface, there is no guarantee that this will # give us a face on the positive (or negative) side, hence the more general # approach is preferred. # # NOTE: The basis for the tangential direction is determined by the # construction internally in TangentialNormalProjection. projection = pp.TangentialNormalProjection(normal_lower) d_l = gb.node_props(g_l) # Store the projection operator in the lower-dimensional data d_l["tangential_normal_projection"] = projection