def __init__(self, keyword, grids): if isinstance(grids, list): self._grids = grids else: self._grids = [grids] dim = np.unique([g[0].dim for g in grids]) low_dim_grids = [g[1] for g in grids] if not dim.size == 1: raise ValueError("Expected unique dimension of grids with contact problems") self._discretization = pp.ColoumbContact( keyword, ambient_dimension=dim[0], discr_h=pp.Mpsa(keyword) ) self._name = "Biot stabilization term" self.keyword = keyword self.traction: _MergedOperator self.displacement: _MergedOperator self.rhs: _MergedOperator _wrap_discretization( self, self._discretization, grids, mat_dict_grids=low_dim_grids )
def assign_discretizations(self): """ Assign discretizations to the nodes and edges of the grid bucket. """ # For the Nd domain we solve linear elasticity with mpsa. Nd = self.Nd gb = self.gb mpsa = pp.Mpsa(self.mechanics_parameter_key) # We need a void discretization for the contact traction variable defined on # the fractures. empty_discr = pp.VoidDiscretization(self.mechanics_parameter_key, ndof_cell=Nd) for g, d in gb: if g.dim == Nd: d[pp.DISCRETIZATION] = {self.displacement_variable: {"mpsa": mpsa}} elif g.dim == Nd - 1: d[pp.DISCRETIZATION] = { self.contact_traction_variable: {"empty": empty_discr} } # Define the contact condition on the mortar grid coloumb = pp.ColoumbContact(self.mechanics_parameter_key, Nd, mpsa) contact = pp.PrimalContactCoupling(self.mechanics_parameter_key, mpsa, coloumb) for e, d in gb.edges(): g_l, g_h = gb.nodes_of_edge(e) if g_h.dim == Nd: d[pp.COUPLING_DISCRETIZATION] = { self.friction_coupling_term: { g_h: (self.displacement_variable, "mpsa"), g_l: (self.contact_traction_variable, "empty"), (g_h, g_l): (self.mortar_displacement_variable, contact), } }
def setup(self): g = pp.CartGrid([5, 5]) g.compute_geometry() stiffness = pp.FourthOrderTensor(np.ones(g.num_cells), np.ones(g.num_cells)) bnd = pp.BoundaryConditionVectorial(g) specified_data = { "fourth_order_tensor": stiffness, "bc": bnd, "inverter": "python", } keyword = "mechanics" data = pp.initialize_default_data(g, {}, keyword, specified_parameters=specified_data) discr = pp.Mpsa(keyword) discr.discretize(g, data) stress = data[pp.DISCRETIZATION_MATRICES][keyword][ discr.stress_matrix_key] bound_stress = data[pp.DISCRETIZATION_MATRICES][keyword][ discr.bound_stress_matrix_key] return g, stiffness, bnd, stress, bound_stress
def test_cart_2d(self): g = pp.CartGrid([2, 1], physdims=(1, 1)) g.compute_geometry() right = g.face_centers[0] > 1 - 1e-10 top = g.face_centers[1] > 1 - 1e-10 bc = pp.BoundaryConditionVectorial(g) bc.is_dir[:, top] = True bc.is_dir[0, right] = True bc.is_neu[bc.is_dir] = False g = true_2d(g) subcell_topology = pp.fvutils.SubcellTopology(g) # Mat boundary to subfaces bc = pp.fvutils.boundary_to_sub_boundary(bc, subcell_topology) bound_exclusion = pp.fvutils.ExcludeBoundaries(subcell_topology, bc, g.dim) cell_node_blocks = np.array([[0, 0, 0, 0, 1, 1, 1, 1], [0, 1, 3, 4, 1, 2, 4, 5]]) ncasym_np = np.arange(32 * 32).reshape((32, 32)) ncasym = sps.csr_matrix(ncasym_np) pp.Mpsa("")._eliminate_ncasym_neumann(ncasym, subcell_topology, bound_exclusion, cell_node_blocks, 2) eliminate_ind = np.array([0, 1, 16, 17, 26, 27]) ncasym_np[eliminate_ind] = 0 self.assertTrue(np.allclose(ncasym.A, ncasym_np))
def test_structured_triang(self): nx = 1 ny = 1 g = pp.StructuredTriangleGrid([nx, ny], physdims=[1, 1]) g.compute_geometry() bot = g.face_centers[1] < 1e-10 top = g.face_centers[1] > 1 - 1e-10 left = g.face_centers[0] < 1e-10 right = g.face_centers[0] > 1 - 1e-10 is_dir = left is_neu = top is_rob = right + bot bnd = pp.BoundaryConditionVectorial(g) bnd.is_neu[:] = False bnd.is_dir[:, is_dir] = True bnd.is_rob[:, is_rob] = True bnd.is_neu[:, is_neu] = True sc_top = pp.fvutils.SubcellTopology(g) bnd = pp.fvutils.boundary_to_sub_boundary(bnd, sc_top) bnd_excl = pp.fvutils.ExcludeBoundaries(sc_top, bnd, g.dim) rhs = pp.Mpsa("")._create_bound_rhs(bnd, bnd_excl, sc_top, g, True) hf2f = pp.fvutils.map_hf_2_f(sc_top.fno_unique, sc_top.subfno_unique, g.dim) rhs = rhs * hf2f.T rhs_known = np.array([ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], ]) self.assertTrue(np.all(np.abs(rhs_known - rhs) < 1e-12))
def reconstruct_stress(self, previous_iterate: bool = False) -> None: """ Compute the stress in the highest-dimensional grid based on the displacement states in that grid, adjacent interfaces and global boundary conditions. The stress is stored in the data dictionary of the highest-dimensional grid, in [pp.STATE]['stress']. Parameters: previous_iterate (boolean, optional): If True, use values from previous iteration to compute the stress. Defaults to False. """ # Highest-dimensional grid and its data g = self._nd_grid() d = self.gb.node_props(g) # Pick the relevant displacemnet field if previous_iterate: u = d[pp.STATE][pp.ITERATE][self.displacement_variable] else: u = d[pp.STATE][self.displacement_variable] matrix_dictionary: Dict[str, sps.spmatrix] = d[ pp.DISCRETIZATION_MATRICES][self.mechanics_parameter_key] # Make a discretization object to get hold of the right keys to access the # matrix_dictionary mpsa = pp.Mpsa(self.mechanics_parameter_key) # Stress contribution from internal cell center displacements stress: np.ndarray = matrix_dictionary[mpsa.stress_matrix_key] * u # Contributions from global boundary conditions bound_stress_discr: sps.spmatrix = matrix_dictionary[ mpsa.bound_stress_matrix_key] global_bc_val: np.ndarray = d[pp.PARAMETERS][ self.mechanics_parameter_key]["bc_values"] stress += bound_stress_discr * global_bc_val # Contributions from the mortar displacement variables for e, d_e in self.gb.edges(): # Only contributions from interfaces to the highest dimensional grid mg: pp.MortarGrid = d_e["mortar_grid"] if mg.dim == self._Nd - 1: if previous_iterate: u_e: np.ndarray = d_e[pp.STATE][pp.ITERATE][ self.mortar_displacement_variable] else: u_e = d_e[pp.STATE][self.mortar_displacement_variable] stress += (bound_stress_discr * mg.mortar_to_primary_avg(nd=self._Nd) * u_e) d[pp.STATE]["stress"] = stress
def test_mpsa(self): self.setup() g, g_larger = self.g, self.g_larger specified_data = { "inverter": "python", } keyword = "mechanics" data_small = pp.initialize_default_data( g, {}, keyword, specified_parameters=specified_data) discr = pp.Mpsa(keyword) # Discretization on a small problem discr.discretize(g, data_small) # Perturb one node g_larger.nodes[0, 2] += 0.2 # Faces that have their geometry changed update_faces = np.array([2, 21, 22]) # Perturb the permeability in some cells on the larger grid mu, lmbda = np.ones(g_larger.num_cells), np.ones(g_larger.num_cells) high_coeff_cells = np.array([7, 12]) stiff_larger = pp.FourthOrderTensor(mu, lmbda) specified_data_larger = {"fourth_order_tensor": stiff_larger} # Do a full discretization on the larger grid data_full = pp.initialize_default_data( g_larger, {}, keyword, specified_parameters=specified_data_larger) discr.discretize(g_larger, data_full) # Cells that will be marked as updated, either due to changed parameters or # the newly defined topology update_cells = np.union1d(self.new_cells, high_coeff_cells) updates = { "modified_cells": update_cells, # "modified_faces": update_faces, "map_cells": self.cell_map, "map_faces": self.face_map, } # Data dictionary for the two-step discretization data_partial = pp.initialize_default_data( g_larger, {}, keyword, specified_parameters=specified_data_larger) data_partial["update_discretization"] = updates self._update_and_compare(data_small, data_partial, data_full, g_larger, [keyword], discr)
def _assign_discretizations(self) -> None: """ Assign discretizations to the nodes and edges of the grid bucket. Note the attribute subtract_fracture_pressure: Indicates whether or not to subtract the fracture pressure contribution for the contact traction. This should not be done if the scalar variable is temperature. """ super()._assign_discretizations() # Shorthand key_s, key_m = self.scalar_parameter_key, self.mechanics_parameter_key var_s, var_d = self.scalar_variable, self.displacement_variable # Define discretization # For the Nd domain we solve linear elasticity with mpsa. mpsa = pp.Mpsa(key_m) mass_disc_s = pp.MassMatrix(key_s) source_disc_s = pp.ScalarSource(key_s) # Coupling discretizations # Assign node discretizations for g, d in self.gb: if g.dim == self.Nd: mpsa = d[pp.DISCRETIZATION][var_d]["mpsa"] elif g.dim == self.Nd - 1: d[pp.DISCRETIZATION].update({ var_s: { "mass": mass_disc_s, "source": source_disc_s, }, }) fracture_scalar_to_force_balance = pp.FractureScalarToForceBalance( mpsa, mass_disc_s) for e, d in self.gb.edges(): g_l, g_h = self.gb.nodes_of_edge(e) if g_h.dim == self.Nd: d[pp.COUPLING_DISCRETIZATION].update({ "fracture_scalar_to_force_balance": { g_h: (var_d, "mpsa"), g_l: (var_s, "mass"), e: ( self.mortar_displacement_variable, fracture_scalar_to_force_balance, ), } })
def reconstruct_stress(self, previous_iterate: bool = False) -> None: """ Compute the stress in the highest-dimensional grid based on the displacement states in that grid, adjacent interfaces and global boundary conditions. The stress is stored in the data dictionary of the highest-dimensional grid, in [pp.STATE]['stress']. Parameters: previous_iterate (boolean, optional): If True, use values from previous iteration to compute the stress. Defaults to False. """ g = self._nd_grid() d = self.gb.node_props(g) mpsa = pp.Mpsa(self.mechanics_parameter_key) if previous_iterate: u = d[pp.STATE][pp.ITERATE][self.displacement_variable] else: u = d[pp.STATE][self.displacement_variable] matrix_dictionary = d[pp.DISCRETIZATION_MATRICES][ self.mechanics_parameter_key] # Stress contribution from internal cell center displacements stress = matrix_dictionary[mpsa.stress_matrix_key] * u # Contributions from global boundary conditions bound_stress_discr = matrix_dictionary[mpsa.bound_stress_matrix_key] global_bc_val = d[pp.PARAMETERS][ self.mechanics_parameter_key]["bc_values"] stress += bound_stress_discr * global_bc_val # Contributions from the mortar displacement variables for e, d_e in self.gb.edges(): # Only contributions from interfaces to the highest dimensional grid mg = d_e["mortar_grid"] if mg.dim == self.Nd - 1: if previous_iterate: u_e = d_e[pp.STATE][pp.ITERATE][ self.mortar_displacement_variable] else: u_e = d_e[pp.STATE][self.mortar_displacement_variable] stress += bound_stress_discr * mg.mortar_to_master_avg( nd=self.Nd) * u_e d[pp.STATE]["stress"] = stress
def assign_mechanics_discretizations(self) -> None: """ Assign discretizations to the nodes and edges of the grid bucket. """ gb = self.gb nd = self.Nd # Shorthand key_m, var_m = self.mechanics_parameter_key, self.displacement_variable var_contact = self.contact_traction_variable var_mortar = self.mortar_displacement_variable discr_key, coupling_discr_key = pp.DISCRETIZATION, pp.COUPLING_DISCRETIZATION # For the Nd domain we solve linear elasticity with mpsa. mpsa = pp.Mpsa(key_m) # We need a void discretization for the contact traction variable defined on # the fractures. empty_discr = pp.VoidDiscretization(key_m, ndof_cell=nd) # Assign node discretizations for g, d in gb: add_nonpresent_dictionary(d, discr_key) if g.dim == nd: d[discr_key].update( {var_m: {"mpsa": mpsa},} ) elif g.dim == nd - 1: d[discr_key].update( {var_contact: {"empty": empty_discr},} ) # Define the contact condition on the mortar grid coloumb = pp.ColoumbContact(key_m, nd, mpsa) contact = pp.PrimalContactCoupling(key_m, mpsa, coloumb) for e, d in gb.edges(): g_l, g_h = gb.nodes_of_edge(e) add_nonpresent_dictionary(d, coupling_discr_key) if g_h.dim == nd: d[coupling_discr_key].update( { self.friction_coupling_term: { g_h: (var_m, "mpsa"), g_l: (var_contact, "empty"), e: (var_mortar, contact), }, } )
def reconstruct_stress(self, previous_iterate: bool = False) -> None: """ Compute the stress in the highest-dimensional grid based on the displacement states in that grid, adjacent interfaces and global boundary conditions. The stress is stored in the data dictionary of the highest-dimensional grid, in [pp.STATE]["stress"]. Parameters: previous_iterate : bool If True, use values from previous iteration to compute the stress. Default: False. """ # TODO: Currently 'reconstruct_stress' does not work if 'previous_iterate = True' # since the displacement variable on Nd-grid is not stored in pp.ITERATE. if previous_iterate is True: raise ValueError("Not yet implemented.") g = self._nd_grid() d = self.gb.node_props(g) key_m = self.mechanics_parameter_key var_m = self.displacement_variable var_mortar = self.mortar_displacement_variable mpsa = pp.Mpsa(self.mechanics_parameter_key) if previous_iterate: u = d[pp.STATE][pp.ITERATE][var_m] else: u = d[pp.STATE][var_m] matrix_dictionary = d[pp.DISCRETIZATION_MATRICES][key_m] # Stress contribution from internal cell center displacements stress = matrix_dictionary[mpsa.stress_matrix_key] * u # Contributions from global boundary conditions bound_stress_discr = matrix_dictionary[mpsa.bound_stress_matrix_key] global_bc_val = d[pp.PARAMETERS][key_m]["bc_values"] stress += bound_stress_discr * global_bc_val # Contributions from the mortar displacement variables for e, d_e in self.gb.edges(): # Only contributions from interfaces to the highest dimensional grid mg: pp.MortarGrid = d_e["mortar_grid"] if mg.dim == self.Nd - 1: u_e = d_e[pp.STATE][var_mortar] stress += bound_stress_discr * mg.mortar_to_master_avg(nd=self.Nd) * u_e d[pp.STATE]["stress"] = stress
def test_bound_cell_node_keyword(self): # Compute update for a single cell on the g, stiffness, bnd, stress, bound_stress = self.setup() # inner_cell = 10 nodes_of_cell = np.array([12, 13, 18, 19]) faces_of_cell = np.array([12, 13, 40, 45]) bnd = pp.BoundaryConditionVectorial(g) specified_data = { "fourth_order_tensor": stiffness, "bc": bnd, "inverter": "python", "specified_nodes": np.array([nodes_of_cell]), } keyword = "mechanics" data = pp.initialize_default_data(g, {}, keyword, specified_parameters=specified_data) discr = pp.Mpsa(keyword) discr.discretize(g, data) partial_stress = data[pp.DISCRETIZATION_MATRICES][keyword][ discr.stress_matrix_key] partial_bound = data[pp.DISCRETIZATION_MATRICES][keyword][ discr.bound_stress_matrix_key] active_faces = data[pp.PARAMETERS][keyword]["active_faces"] self.assertTrue(faces_of_cell.size == active_faces.size) self.assertTrue( np.all(np.sort(faces_of_cell) == np.sort(active_faces))) faces_of_cell = self.expand_indices_nd(faces_of_cell, g.dim) diff_stress = (stress - partial_stress).todense() diff_bound = (bound_stress - partial_bound).todense() self.assertTrue(np.max(np.abs(diff_stress[faces_of_cell])) == 0) self.assertTrue(np.max(np.abs(diff_bound[faces_of_cell])) == 0) # Only the faces of the central cell should be non-zero. pp.fvutils.remove_nonlocal_contribution(faces_of_cell, 1, partial_stress, partial_bound) self.assertTrue(np.max(np.abs(partial_stress.data)) == 0) self.assertTrue(np.max(np.abs(partial_bound.data)) == 0)
def __init__(self, keyword, grids): if isinstance(grids, list): self._grids = grids else: self._grids = [grids] self._discretization = pp.Mpsa(keyword) self._name = "Mpsa" self.keyword = keyword # Declear attributes, these will be initialized by the below call to the # discretization wrapper. self.stress: _MergedOperator self.bound_stress: _MergedOperator self.bound_displacement_cell: _MergedOperator self.bound_displacement_face: _MergedOperator _wrap_discretization(self, self._discretization, grids)
def set_parameters(self, g, data_node, mg, data_edge): """ Set the parameters for the simulation. The stress is given in GPa. """ # Define the finite volume sub grid s_t = pp.fvutils.SubcellTopology(g) # Rock parameters rock = pp.Granite() lam = rock.LAMBDA * np.ones(g.num_cells) / self.pressure_scale mu = rock.MU * np.ones(g.num_cells) / self.pressure_scale F = self._friction_coefficient(g, mg, data_edge, s_t) k = pp.FourthOrderTensor(g.dim, mu, lam) # Define boundary regions top = g.face_centers[g.dim - 1] > np.max(g.nodes[1]) - 1e-9 bot = g.face_centers[g.dim - 1] < np.min(g.nodes[1]) + 1e-9 top_hf = top[s_t.fno_unique] bot_hf = bot[s_t.fno_unique] # Define boundary condition on sub_faces bc = pp.BoundaryConditionVectorial(g, top + bot, 'dir') bc = pp.fvutils.boundary_to_sub_boundary(bc, s_t) # Set the boundary values u_bc = np.zeros((g.dim, s_t.num_subfno_unique)) u_bc[1, top_hf] = -0.002 u_bc[0, top_hf] = 0.005 # Find the continuity points eta = 1 / 3 eta_vec = eta * np.ones(s_t.num_subfno_unique) cont_pnt = g.face_centers[:g.dim, s_t.fno_unique] + eta_vec * ( g.nodes[:g.dim, s_t.nno_unique] - g.face_centers[:g.dim, s_t.fno_unique]) # collect parameters in dictionary key = 'mech' key_m = 'mech' data_node = pp.initialize_data( g, data_node, key, { 'bc': bc, 'bc_values': u_bc.ravel('F'), 'source': 0, 'fourth_order_tensor': k, 'mpsa_eta': eta_vec, 'cont_pnt': cont_pnt, 'rock': rock, }) pp.initialize_data(mg, data_edge, key_m, { 'friction_coeff': F, 'c': 100 }) # Define discretization # For the 2D domain we solve linear elasticity with mpsa. mpsa = pp.Mpsa(key) data_node[pp.PRIMARY_VARIABLES] = {'u': {'cells': g.dim}} data_node[pp.DISCRETIZATION] = {'u': {'mpsa': mpsa}} # And define a Robin condition on the mortar grid contact = pp.numerics.interface_laws.elliptic_interface_laws.RobinContact( key_m, mpsa) data_edge[pp.PRIMARY_VARIABLES] = {'lam': {'cells': g.dim}} data_edge[pp.COUPLING_DISCRETIZATION] = { 'robin_disc': { g: ('u', 'mpsa'), g: ('u', 'mpsa'), (g, g): ('lam', contact), } } return key, key_m
def _assign_discretizations(self) -> None: """ Assign discretizations to the nodes and edges of the grid bucket. """ # For the Nd domain we solve linear elasticity with mpsa. Nd = self._Nd gb = self.gb if not self._use_ad: mpsa = pp.Mpsa(self.mechanics_parameter_key) # We need a void discretization for the contact traction variable defined on # the fractures. empty_discr = pp.VoidDiscretization(self.mechanics_parameter_key, ndof_cell=Nd) for g, d in gb: if g.dim == Nd: d[pp.DISCRETIZATION] = { self.displacement_variable: { "mpsa": mpsa } } elif g.dim == Nd - 1: d[pp.DISCRETIZATION] = { self.contact_traction_variable: { "empty": empty_discr } } # Define the contact condition on the mortar grid coloumb = pp.ColoumbContact(self.mechanics_parameter_key, Nd, mpsa) contact = pp.PrimalContactCoupling(self.mechanics_parameter_key, mpsa, coloumb) for e, d in gb.edges(): g_l, g_h = gb.nodes_of_edge(e) if g_h.dim == Nd: d[pp.COUPLING_DISCRETIZATION] = { self.friction_coupling_term: { g_h: (self.displacement_variable, "mpsa"), g_l: (self.contact_traction_variable, "empty"), (g_h, g_l): (self.mortar_displacement_variable, contact), } } else: dof_manager = pp.DofManager(gb) eq_manager = pp.ad.EquationManager(gb, dof_manager) g_primary: pp.Grid = gb.grids_of_dimension(Nd)[0] g_frac: List[pp.Grid] = gb.grids_of_dimension(Nd - 1).tolist() num_frac_cells = np.sum([g.num_cells for g in g_frac]) if len(gb.grids_of_dimension(Nd)) != 1: raise NotImplementedError("This will require further work") edge_list = [(g_primary, g) for g in g_frac] mortar_proj = pp.ad.MortarProjections(edges=edge_list, gb=gb, nd=self._Nd) subdomain_proj = pp.ad.SubdomainProjections(gb=gb, nd=self._Nd) tangential_proj_lists = [ gb.node_props( gf, "tangential_normal_projection").project_tangential_normal( gf.num_cells) for gf in g_frac ] tangential_proj = pp.ad.Matrix( sps.block_diag(tangential_proj_lists)) mpsa_ad = mpsa_ad = pp.ad.MpsaAd(self.mechanics_parameter_key, g_primary) bc_ad = pp.ad.BoundaryCondition(self.mechanics_parameter_key, grids=[g_primary]) div = pp.ad.Divergence(grids=[g_primary], dim=g_primary.dim) # Primary variables on Ad form u = eq_manager.variable(g_primary, self.displacement_variable) u_mortar = eq_manager.merge_variables([ (e, self.mortar_displacement_variable) for e in edge_list ]) contact_force = eq_manager.merge_variables([ (g, self.contact_traction_variable) for g in g_frac ]) u_mortar_prev = u_mortar.previous_timestep() # Stress in g_h stress = (mpsa_ad.stress * u + mpsa_ad.bound_stress * bc_ad + mpsa_ad.bound_stress * subdomain_proj.face_restriction(g_primary) * mortar_proj.mortar_to_primary_avg * u_mortar) # momentum balance equation in g_h momentum_eq = pp.ad.Expression(div * stress, dof_manager, name="momentuum", grid_order=[g_primary]) coloumb_ad = pp.ad.ColoumbContactAd(self.mechanics_parameter_key, edge_list) jump_rotate = (tangential_proj * subdomain_proj.cell_restriction(g_frac) * mortar_proj.mortar_to_secondary_avg * mortar_proj.sign_of_mortar_sides) # Contact conditions jump_discr = coloumb_ad.displacement * jump_rotate * u_mortar tmp = np.ones(num_frac_cells * self._Nd) tmp[self._Nd - 1::self._Nd] = 0 exclude_normal = pp.ad.Matrix( sps.dia_matrix((tmp, 0), shape=(tmp.size, tmp.size))) # Rhs of contact conditions rhs = (coloumb_ad.rhs + exclude_normal * coloumb_ad.displacement * jump_rotate * u_mortar_prev) contact_conditions = coloumb_ad.traction * contact_force + jump_discr - rhs contact_eq = pp.ad.Expression(contact_conditions, dof_manager, grid_order=g_frac, name="contact") # Force balance mat = None for _, d in gb.edges(): mg: pp.MortarGrid = d["mortar_grid"] if mg.dim < self._Nd - 1: continue faces_on_fracture_surface = mg.primary_to_mortar_int().tocsr( ).indices m = pp.grid_utils.switch_sign_if_inwards_normal( g_primary, self._Nd, faces_on_fracture_surface) if mat is None: mat = m else: mat += m sign_switcher = pp.ad.Matrix(mat) # Contact from primary grid and mortar displacements (via primary grid) contact_from_primary_mortar = ( mortar_proj.primary_to_mortar_int * subdomain_proj.face_prolongation(g_primary) * sign_switcher * stress) contact_from_secondary = ( mortar_proj.sign_of_mortar_sides * mortar_proj.secondary_to_mortar_int * subdomain_proj.cell_prolongation(g_frac) * tangential_proj.transpose() * contact_force) force_balance_eq = pp.ad.Expression( contact_from_primary_mortar + contact_from_secondary, dof_manager, name="force_balance", grid_order=edge_list, ) # eq2 = pp.ad.Equation(contact_from_secondary, dof_manager).to_ad(gb) eq_manager.equations += [momentum_eq, contact_eq, force_balance_eq] self._eq_manager = eq_manager
def test_cart_2d(self): nx = 1 ny = 1 g = pp.CartGrid([nx, ny], physdims=[2, 2]) g.compute_geometry() g = make_true_2d(g) sc_top = pp.fvutils.SubcellTopology(g) D_g, CC = pp.Mpsa("")._reconstruct_displacement(g, sc_top, eta=0) D_g_known = np.array([ [ -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, ], [ 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, ], ]) CC_known = np.array([ [1, 0.0], [0, 1.0], [1, 0.0], [0, 1.0], [1, 0.0], [0, 1.0], [1, 0.0], [0, 1.0], [1, 0.0], [0, 1.0], [1, 0.0], [0, 1.0], [1, 0.0], [0, 1.0], [1, 0.0], [0, 1.0], ]) self.assertTrue(np.all(np.abs(D_g - D_g_known) < 1e-12)) self.assertTrue(np.all(np.abs(CC - CC_known) < 1e-12))
def test_cart_3d(self): g = pp.CartGrid([1, 1, 1], physdims=[2, 2, 2]) g.compute_geometry() sc_top = pp.fvutils.SubcellTopology(g) D_g, CC = pp.Mpsa("")._reconstruct_displacement(g, sc_top, eta=1) data = np.array([ -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, ]) indices = np.array([ 0, 24, 48, 1, 25, 49, 2, 26, 50, 12, 33, 51, 13, 34, 52, 14, 35, 53, 3, 36, 57, 4, 37, 58, 5, 38, 59, 15, 45, 54, 16, 46, 55, 17, 47, 56, 9, 27, 60, 10, 28, 61, 11, 29, 62, 21, 30, 63, 22, 31, 64, 23, 32, 65, 6, 39, 69, 7, 40, 70, 8, 41, 71, 18, 42, 66, 19, 43, 67, 20, 44, 68, ]) indptr = np.array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, ]) CC_known = np.array([ [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], ]) D_g_known = sps.csc_matrix((data, indices, indptr)) self.assertTrue(np.all(np.abs(D_g - D_g_known).A < 1e-12)) self.assertTrue(np.all(np.abs(CC - CC_known) < 1e-12))
def set_parameters(self, g, data_node, mg, data_edge): """ Set the parameters for the simulation. The stress is given in GPa. """ # Define the finite volume sub grid s_t = pp.fvutils.SubcellTopology(g) # Rock parameters rock = pp.Granite() lam = rock.LAMBDA * np.ones(g.num_cells) / self.pressure_scale mu = rock.MU * np.ones(g.num_cells) / self.pressure_scale k = pp.FourthOrderTensor(g.dim, mu, lam) F = self._friction_coefficient(g, mg, data_edge, s_t) # Define boundary regions east = g.face_centers[0] > np.max(g.nodes[0]) - 1e-9 west = g.face_centers[0] < np.min(g.nodes[0]) + 1e-9 north = g.face_centers[1] > np.max(g.nodes[1]) - 1e-9 south = g.face_centers[1] < np.min(g.nodes[1]) + 1e-9 top = g.face_centers[2] > np.max(g.nodes[2]) - 1e-9 bot = g.face_centers[2] < np.min(g.nodes[2]) + 1e-9 top_hf = top[s_t.fno_unique] bot_hf = bot[s_t.fno_unique] # define boundary condition bc = pp.BoundaryConditionVectorial(g) bc.is_dir[0, east] = True # Rolling bc.is_dir[0, west] = True # Rolling bc.is_dir[1, north] = True # Rolling bc.is_dir[1, south] = True # Rolling bc.is_dir[:, bot] = True # Dirichlet bc.is_neu[bc.is_dir + bc.is_rob] = False # extract boundary condition to subcells bc = pp.fvutils.boundary_to_sub_boundary(bc, s_t) # Give boundary condition values A = g.face_areas[s_t.fno_unique][top_hf] / 3 #* pp.length_scale**g.dim u_bc = np.zeros((g.dim, s_t.num_subfno_unique)) u_bc[2, top_hf] = -4.5 * pp.MEGA * pp.PASCAL / self.pressure_scale * A # Find the continuity points eta = 1 / 3 eta_vec = eta * np.ones(s_t.num_subfno_unique) cont_pnt = g.face_centers[:g.dim, s_t.fno_unique] + eta_vec * ( g.nodes[:g.dim, s_t.nno_unique] - g.face_centers[:g.dim, s_t.fno_unique]) # collect parameters in dictionary key = 'mech' key_m = 'mech' data_node = pp.initialize_data( g, data_node, key, { 'bc': bc, 'bc_values': u_bc.ravel('F'), 'source': 0, 'fourth_order_tensor': k, 'mpsa_eta': eta_vec, 'cont_pnt': cont_pnt, 'rock': rock, }) pp.initialize_data( mg, data_edge, key_m, { 'friction_coeff': F, 'c': 100 * pp.GIGA * pp.PASCAL / self.pressure_scale }) # Define discretization # Solve linear elasticity with mpsa mpsa = pp.Mpsa(key) data_node[pp.PRIMARY_VARIABLES] = {'u': {'cells': g.dim}} data_node[pp.DISCRETIZATION] = {'u': {'mpsa': mpsa}} # Add a Robin condition to the mortar grid contact = pp.numerics.interface_laws.elliptic_interface_laws.RobinContact( key_m, mpsa) data_edge[pp.PRIMARY_VARIABLES] = {'lam': {'cells': g.dim}} data_edge[pp.COUPLING_DISCRETIZATION] = { 'robin_disc': { g: ('u', 'mpsa'), g: ('u', 'mpsa'), (g, g): ('lam', contact), } } return key, key_m
def test_simplex_2d(self): nx = 1 ny = 1 g = pp.StructuredTriangleGrid([nx, ny], physdims=[1, 1]) g.compute_geometry() g = make_true_2d(g) sc_top = pp.fvutils.SubcellTopology(g) D_g, CC = pp.Mpsa("")._reconstruct_displacement(g, sc_top, eta=0) D_g_known = np.array([ [ -1 / 6, -1 / 3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, -1 / 6, -1 / 3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, -1 / 6, -1 / 3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1 / 6, -1 / 3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1 / 3, -1 / 6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1 / 3, -1 / 6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1 / 3, -1 / 6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1 / 3, -1 / 6, 0.0, 0.0, 0.0, 0.0, ], [ -1 / 12, 1 / 12, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1 / 12, -1 / 12, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, -1 / 12, 1 / 12, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1 / 12, -1 / 12, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1 / 12, 1 / 12, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1 / 12, -1 / 12, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1 / 12, 1 / 12, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1 / 12, -1 / 12, ], [ 0.0, 0.0, 0.0, 0.0, 1 / 3, 1 / 6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1 / 3, 1 / 6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1 / 3, 1 / 6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1 / 3, 1 / 6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1 / 6, 1 / 3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1 / 6, 1 / 3, 0.0, 0.0, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1 / 6, 1 / 3, 0.0, 0.0, ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1 / 6, 1 / 3, ], ]) CC_known = np.array([ [1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0], [0.5, 0.0, 0.5, 0.0], [0.0, 0.5, 0.0, 0.5], [0.5, 0.0, 0.5, 0.0], [0.0, 0.5, 0.0, 0.5], [1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0], ]) self.assertTrue(np.all(np.abs(D_g - D_g_known).A < 1e-12)) self.assertTrue(np.all(np.abs(CC - CC_known) < 1e-12))
def set_parameters(self, g, data_node, mg, data_edge): """ Set the parameters for the simulation. The stress is given in GPa. """ # First set the parameters used in the pure elastic simulation key_m, key_c = super().set_parameters(g, data_node, mg, data_edge) key_f = 'flow' if not key_m == key_c: raise ValueError('Mechanics keyword must equal contact keyword') self.key_m = key_m self.key_f = key_f # Set fluid parameters kxx = self.k * np.ones(g.num_cells) / self.length_scale**2 viscosity = self.viscosity / self.pressure_scale K = pp.SecondOrderTensor(g.dim, kxx / viscosity) # Define Biot parameters alpha = 1 dt = self.end_time / 20 # Define the finite volume sub grid s_t = pp.fvutils.SubcellTopology(g) # Define boundary conditions for flow top = g.face_centers[2] > np.max(g.nodes[2]) - 1e-9 bot = g.face_centers[2] < np.min(g.nodes[2]) + 1e-9 east = g.face_centers[0] > np.max(g.nodes[0]) - 1e-9 bc_flow = pp.BoundaryCondition(g, top, 'dir') bc_flow = pp.fvutils.boundary_to_sub_boundary(bc_flow, s_t) # Set boundary condition values. p_bc = self.bc_values(g, dt, key_f) # Set initial solution u0 = np.zeros(g.dim * g.num_cells) p0 = np.zeros(g.num_cells) lam_u0 = np.zeros(g.dim * mg.num_cells) u_bc0 = self.bc_values(g, 0, key_m) u_bc = self.bc_values(g, dt, key_m) # Collect parameters in dictionaries # Add biot parameters to mechanics data_node[pp.PARAMETERS][key_m]['biot_alpha'] = alpha data_node[pp.PARAMETERS][key_m]['time_step'] = dt data_node[pp.PARAMETERS][key_m]['bc_values'] = u_bc data_node[pp.PARAMETERS][key_m]['state'] = { 'displacement': u0, 'bc_values': u_bc0 } data_edge[pp.PARAMETERS][key_c]['state'] = lam_u0 # Add fluid flow dictionary data_node = pp.initialize_data( g, data_node, key_f, { 'bc': bc_flow, 'bc_values': p_bc.ravel('F'), 'second_order_tensor': K, 'mass_weight': self.S, 'aperture': np.ones(g.num_cells), 'biot_alpha': alpha, 'time_step': dt, 'state': p0, }) # Define discretization. # For the domain we solve linear elasticity with mpsa and fluid flow with mpfa. # In addition we add a storage term (ImplicitMassMatrix) to the fluid mass balance. # The coupling terms are: # BiotStabilization, pressure contribution to the div u term. # GrapP, pressure contribution to stress equation. # div_u, displacement contribution to div u term. data_node[pp.PRIMARY_VARIABLES] = { "u": { "cells": g.dim }, "p": { "cells": 1 } } mpfa_disc = discretizations.ImplicitMpfa(key_f) data_node[pp.DISCRETIZATION] = { "u": { "div_sigma": pp.Mpsa(key_m) }, "p": { "flux": mpfa_disc, "mass": discretizations.ImplicitMassMatrix(key_f), "stab": pp.BiotStabilization(key_f), }, "u_p": { "grad_p": pp.GradP(key_m) }, "p_u": { "div_u": pp.DivD(key_m) }, } # On the mortar grid we define two variables and sets of equations. The first # adds a Robin condition to the elasticity equation. The second enforces full # fluid pressure and flux continuity over the fractures. We also have to be # carefull to obtain the contribution of the coupling discretizations gradP on # the Robin contact condition, and the contribution from the mechanical mortar # variable on the div_u term. # Contribution from fluid pressure on displacement jump at fractures gradP_disp = pp.numerics.interface_laws.elliptic_interface_laws.RobinContactBiotPressure( key_m, pp.numerics.fv.biot.GradP(key_m)) # Contribution from mechanics mortar on div_u term div_u_lam = pp.numerics.interface_laws.elliptic_interface_laws.DivU_StressMortar( key_m, pp.numerics.fv.biot.DivD(key_m)) # gradP_disp and pp.RobinContact will now give the correct Robin contact # condition. # div_u (from above) and div_u_lam will now give the correct div u term in the # fluid mass balance data_edge[pp.PRIMARY_VARIABLES] = {"lam_u": {"cells": g.dim}} data_edge[pp.COUPLING_DISCRETIZATION] = { "robin_discretization": { g: ("u", "div_sigma"), g: ("u", "div_sigma"), (g, g): ("lam_u", pp.RobinContact(key_m, pp.Mpsa(key_m))), }, "p_contribution_to_displacement": { g: ("p", "flux" ), # "flux" should be "grad_p", but the assembler does not g: ("p", "flux" ), # support this. However, in FV this is not used anyway. (g, g): ("lam_u", gradP_disp), }, "lam_u_contr_2_div_u": { g: ("p", "flux"), # "flux" -> "div_u" g: ("p", "flux"), (g, g): ("lam_u", div_u_lam), }, } # Discretize with biot pp.Biot(key_m, key_f).discretize(g, data_node) return key_m, key_f
def test_assemble_biot(self): """ Test the assembly of the Biot problem using the assembler. The test checks whether the discretization matches that of the Biot class. """ gb = pp.meshing.cart_grid([], [2, 1]) g = gb.grids_of_dimension(2)[0] d = gb.node_props(g) # Parameters identified by two keywords kw_m = "mechanics" kw_f = "flow" variable_m = "displacement" variable_f = "pressure" bound_mech, bound_flow = self.make_boundary_conditions(g) initial_disp, initial_pressure, initial_state = self.make_initial_conditions( g, x0=0, y0=0, p0=0) state = { variable_f: initial_pressure, variable_m: initial_disp, kw_m: { "bc_values": np.zeros(g.num_faces * g.dim) }, } parameters_m = {"bc": bound_mech, "biot_alpha": 1} parameters_f = {"bc": bound_flow, "biot_alpha": 1} pp.initialize_default_data(g, d, kw_m, parameters_m) pp.initialize_default_data(g, d, kw_f, parameters_f) pp.set_state(d, state) # Discretize the mechanics related terms using the Biot class biot_discretizer = pp.Biot() biot_discretizer._discretize_mech(g, d) # Set up the structure for the assembler. First define variables and equation # term names. v_0 = variable_m v_1 = variable_f term_00 = "stress_divergence" term_01 = "pressure_gradient" term_10 = "displacement_divergence" term_11_0 = "fluid_mass" term_11_1 = "fluid_flux" term_11_2 = "stabilization" d[pp.PRIMARY_VARIABLES] = {v_0: {"cells": g.dim}, v_1: {"cells": 1}} d[pp.DISCRETIZATION] = { v_0: { term_00: pp.Mpsa(kw_m) }, v_1: { term_11_0: pp.MassMatrix(kw_f), term_11_1: pp.Mpfa(kw_f), term_11_2: pp.BiotStabilization(kw_f), }, v_0 + "_" + v_1: { term_01: pp.GradP(kw_m) }, v_1 + "_" + v_0: { term_10: pp.DivU(kw_m) }, } # Assemble. Also discretizes the flow terms (fluid_mass and fluid_flux) general_assembler = pp.Assembler(gb) general_assembler.discretize(term_filter=["fluid_mass", "fluid_flux"]) A, b = general_assembler.assemble_matrix_rhs() # Re-discretize and assemble using the Biot class A_class, b_class = biot_discretizer.matrix_rhs(g, d, discretize=False) # Make sure the variable ordering of the matrix assembled by the assembler # matches that of the Biot class. grids = [g, g] variables = [v_0, v_1] A, b = permute_matrix_vector( A, b, general_assembler.block_dof, general_assembler.full_dof, grids, variables, ) # Compare the matrices and rhs vectors self.assertTrue(np.all(np.isclose(A.A, A_class.A))) self.assertTrue(np.all(np.isclose(b, b_class)))
def test_cart_3d(self): g = pp.CartGrid([1, 1, 1]) g.compute_geometry() bot = g.face_centers[2] < 1e-10 top = g.face_centers[2] > 1 - 1e-10 south = g.face_centers[1] < 1e-10 north = g.face_centers[1] > 1 - 1e-10 west = g.face_centers[0] < 1e-10 east = g.face_centers[0] > 1 - 1e-10 is_dir = south + top is_neu = east + west is_rob = north + bot bnd = pp.BoundaryConditionVectorial(g) bnd.is_neu[:] = False bnd.is_dir[:, is_dir] = True bnd.is_rob[:, is_rob] = True bnd.is_neu[:, is_neu] = True sc_top = pp.fvutils.SubcellTopology(g) bnd = pp.fvutils.boundary_to_sub_boundary(bnd, sc_top) bnd_excl = pp.fvutils.ExcludeBoundaries(sc_top, bnd, g.dim) rhs = pp.Mpsa("")._create_bound_rhs(bnd, bnd_excl, sc_top, g, True) hf2f = pp.fvutils.map_hf_2_f(sc_top.fno_unique, sc_top.subfno_unique, g.dim) rhs = rhs * hf2f.T rhs_indptr = np.array( [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, ], dtype=np.int32, ) rhs_indices = np.array( [ 0, 0, 0, 0, 3, 3, 3, 3, 1, 1, 1, 1, 4, 4, 4, 4, 2, 2, 2, 2, 5, 5, 5, 5, 9, 9, 9, 9, 12, 12, 12, 12, 10, 10, 10, 10, 13, 13, 13, 13, 11, 11, 11, 11, 14, 14, 14, 14, 6, 6, 6, 6, 15, 15, 15, 15, 7, 7, 7, 7, 16, 16, 16, 16, 8, 8, 8, 8, 17, 17, 17, 17, ], dtype=np.int32, ) rhs_data = np.array([ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, ]) rhs_known = sps.csr_matrix((rhs_data, rhs_indices, rhs_indptr), shape=(72, 18)) self.assertTrue(np.all(np.abs((rhs_known - rhs).data) < 1e-12))
def test_mixed_condition(self): nx = 2 ny = 1 g = pp.CartGrid([nx, ny], physdims=[1, 1]) g.compute_geometry() bot = g.face_centers[1] < 1e-10 top = g.face_centers[1] > 1 - 1e-10 left = g.face_centers[0] < 1e-10 right = g.face_centers[0] > 1 - 1e-10 bnd = pp.BoundaryConditionVectorial(g) bnd.is_neu[:] = False bnd.is_dir[0, left] = True bnd.is_neu[1, left] = True bnd.is_rob[0, right] = True bnd.is_dir[1, right] = True bnd.is_neu[0, bot] = True bnd.is_rob[1, bot] = True bnd.is_rob[0, top] = True bnd.is_dir[1, top] = True sc_top = pp.fvutils.SubcellTopology(g) bnd = pp.fvutils.boundary_to_sub_boundary(bnd, sc_top) bnd_excl = pp.fvutils.ExcludeBoundaries(sc_top, bnd, g.dim) rhs = pp.Mpsa("")._create_bound_rhs(bnd, bnd_excl, sc_top, g, True) hf2f = pp.fvutils.map_hf_2_f(sc_top.fno_unique, sc_top.subfno_unique, g.dim) rhs = rhs * hf2f.T rhs_known = np.array([ [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0 ], [ -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0 ], [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0 ], ]) self.assertTrue(np.all(np.abs(rhs_known - rhs) < 1e-12))
def discretize( grid_bucket, data_dictionary, parameter_keyword_flow, parameter_keyword_mechanics, variable_flow, variable_mechanics, ): """ Discretize the problem. Parameters: grid_bucket (PorePy object): Grid bucket data_dictionary (Dict): Model's data dictionary parameter_keyword_flow (String): Keyword for the flow parameter parameter_keyword_mechanics (String): Keyword for the mechanics parameter variable_flow (String): Primary variable of the flow problem variable_mechanics (String): Primary variable of the mechanics problem Output: assembler (PorePy object): Assembler containing discretization """ # The Mpfa discretization assumes unit viscosity. Hence we need to # overwrite the class to include it. class ImplicitMpfa(pp.Mpfa): def assemble_matrix_rhs(self, g, d): """ Overwrite MPFA method to be consistent with Biot's time discretization and inclusion of viscosity in Darcy's law """ viscosity = d[pp.PARAMETERS][self.keyword]["viscosity"] a, b = super().assemble_matrix_rhs(g, d) dt = d[pp.PARAMETERS][self.keyword]["time_step"] return a * (1 / viscosity) * dt, b * (1 / viscosity) * dt # Redefining input parameters gb = grid_bucket g = gb.grids_of_dimension(2)[0] d = data_dictionary kw_f = parameter_keyword_flow kw_m = parameter_keyword_mechanics v_0 = variable_mechanics v_1 = variable_flow # Discretize the subproblems using Biot's class, which employs # MPSA for the mechanics problem and MPFA for the flow problem biot_discretizer = pp.Biot(kw_m, kw_f, v_0, v_1) biot_discretizer._discretize_mech(g, d) # discretize mech problem biot_discretizer._discretize_flow(g, d) # discretize flow problem # Names of the five terms of the equation + additional stabilization term. ####################################### Term in the Biot equation: term_00 = "stress_divergence" ######## div symmetric grad u term_01 = "pressure_gradient" ######## alpha grad p term_10 = "displacement_divergence" ## d/dt alpha div u term_11_0 = "fluid_mass" ############# d/dt beta p term_11_1 = "fluid_flux" ############# div (rho g - K grad p) term_11_2 = "stabilization" ########## # Store in the data dictionary and specify discretization objects. d[pp.PRIMARY_VARIABLES] = {v_0: {"cells": g.dim}, v_1: {"cells": 1}} d[pp.DISCRETIZATION] = { v_0: { term_00: pp.Mpsa(kw_m) }, v_1: { term_11_0: ImplicitMassMatrix(kw_f, v_1), term_11_1: ImplicitMpfa(kw_f), term_11_2: pp.BiotStabilization(kw_f, v_1), }, v_0 + "_" + v_1: { term_01: pp.GradP(kw_m) }, v_1 + "_" + v_0: { term_10: pp.DivU(kw_m, kw_f, v_0) }, } assembler = pp.Assembler(gb) # Discretize the flow and accumulation terms - the other are already handled # by the biot_discretizer assembler.discretize(term_filter=[term_11_0, term_11_1]) return assembler
gb = pp.meshing.cart_grid(frac, [9, 3]) split_scheme = [[np.array([1]), np.array([8])], [np.array([49]), np.array([55])]] return gb, split_scheme # The main test function @pytest.mark.parametrize( "geometry", [ _two_fractures_overlapping_regions, _two_fractures_non_overlapping_regions, _two_fractures_regions_become_overlapping, ], ) @pytest.mark.parametrize( "method", [pp.Mpfa("flow"), pp.Mpsa("mechanics"), pp.Biot("mechanics", "flow")] ) def test_propagation(geometry, method): # Method to test partial discretization (aimed at finite volume methods) under # fracture propagation. The test is based on first discretizing, and then do one # or several fracture propagation steps. after each step, we do a partial # update of the discretization scheme, and compare with a full discretization on # the newly split grid. The test fails unless all discretization matrices generated # are identical. # # NOTE: Only the highest-dimensional grid in the GridBucket is used. # Get GridBucket and splitting schedule gb, faces_to_split = geometry() g = gb.grids_of_dimension(gb.dim_max())[0]
def assign_discretisations(self): """ Assign discretizations to the nodes and edges of the grid bucket. Note the attribute subtract_fracture_pressure: Indicates whether or not to subtract the fracture pressure contribution for the contact traction. This should not be done if the scalar variable is temperature. """ # Shorthand key_s, key_m = self.scalar_parameter_key, self.mechanics_parameter_key var_s, var_d = self.scalar_variable, self.displacement_variable # Define discretization # For the Nd domain we solve linear elasticity with mpsa. mpsa = pp.Mpsa(key_m) empty_discr = pp.VoidDiscretization(key_m, ndof_cell=self.Nd) # Scalar discretizations (all dimensions) diff_disc_s = IE_discretizations.ImplicitMpfa(key_s) mass_disc_s = IE_discretizations.ImplicitMassMatrix(key_s, var_s) source_disc_s = pp.ScalarSource(key_s) # Coupling discretizations # All dimensions div_u_disc = pp.DivU( key_m, key_s, variable=var_d, mortar_variable=self.mortar_displacement_variable, ) # Nd grad_p_disc = pp.GradP(key_m) stabilization_disc_s = pp.BiotStabilization(key_s, var_s) # Assign node discretizations for g, d in self.gb: if g.dim == self.Nd: d[pp.DISCRETIZATION] = { var_d: { "mpsa": mpsa }, var_s: { "diffusion": diff_disc_s, "mass": mass_disc_s, "stabilization": stabilization_disc_s, "source": source_disc_s, }, var_d + "_" + var_s: { "grad_p": grad_p_disc }, var_s + "_" + var_d: { "div_u": div_u_disc }, } elif g.dim == self.Nd - 1: d[pp.DISCRETIZATION] = { self.contact_traction_variable: { "empty": empty_discr }, var_s: { "diffusion": diff_disc_s, "mass": mass_disc_s, "source": source_disc_s, }, } # Define edge discretizations for the mortar grid contact_law = pp.ColoumbContact(self.mechanics_parameter_key, self.Nd) contact_discr = pp.PrimalContactCoupling(self.mechanics_parameter_key, mpsa, contact_law) # Account for the mortar displacements effect on scalar balance in the matrix, # as an internal boundary contribution, fracture, aperture changes appear as a # source contribution. div_u_coupling = pp.DivUCoupling(self.displacement_variable, div_u_disc, div_u_disc) # Account for the pressure contributions to the force balance on the fracture # (see contact_discr). # This discretization needs the keyword used to store the grad p discretization: grad_p_key = key_m matrix_scalar_to_force_balance = pp.MatrixScalarToForceBalance( grad_p_key, mass_disc_s, mass_disc_s) if self.subtract_fracture_pressure: fracture_scalar_to_force_balance = pp.FractureScalarToForceBalance( mass_disc_s, mass_disc_s) for e, d in self.gb.edges(): g_l, g_h = self.gb.nodes_of_edge(e) if g_h.dim == self.Nd: d[pp.COUPLING_DISCRETIZATION] = { self.friction_coupling_term: { g_h: (var_d, "mpsa"), g_l: (self.contact_traction_variable, "empty"), (g_h, g_l): (self.mortar_displacement_variable, contact_discr), }, self.scalar_coupling_term: { g_h: (var_s, "diffusion"), g_l: (var_s, "diffusion"), e: ( self.mortar_scalar_variable, pp.RobinCoupling(key_s, diff_disc_s), ), }, "div_u_coupling": { g_h: ( var_s, "mass", ), # This is really the div_u, but this is not implemented g_l: (var_s, "mass"), e: (self.mortar_displacement_variable, div_u_coupling), }, "matrix_scalar_to_force_balance": { g_h: (var_s, "mass"), g_l: (var_s, "mass"), e: ( self.mortar_displacement_variable, matrix_scalar_to_force_balance, ), }, } if self.subtract_fracture_pressure: d[pp.COUPLING_DISCRETIZATION].update({ "matrix_scalar_to_force_balance": { g_h: (var_s, "mass"), g_l: (var_s, "mass"), e: ( self.mortar_displacement_variable, fracture_scalar_to_force_balance, ), } }) else: raise ValueError( "assign_discretizations assumes no fracture intersections." )
def test_one_cell_a_time_node_keyword(self): # Update one and one cell, and verify that the result is the same as # with a single computation. The test is similar to what will happen # with a memory-constrained splitting. g = pp.CartGrid([3, 3]) g.compute_geometry() # Assign random permeabilities, for good measure np.random.seed(42) mu = np.random.random(g.num_cells) lmbda = np.random.random(g.num_cells) stiffness = pp.FourthOrderTensor(mu=mu, lmbda=lmbda) stress = sps.csr_matrix((g.num_faces * g.dim, g.num_cells * g.dim)) bound_stress = sps.csr_matrix( (g.num_faces * g.dim, g.num_faces * g.dim)) faces_covered = np.zeros(g.num_faces, np.bool) bnd = pp.BoundaryConditionVectorial(g) specified_data = { "fourth_order_tensor": stiffness, "bc": bnd, "inverter": "python", } keyword = "mechanics" data = pp.initialize_default_data(g, {}, keyword, specified_parameters=specified_data) discr = pp.Mpsa(keyword) discr.discretize(g, data) stress_full = data[pp.DISCRETIZATION_MATRICES][keyword][ discr.stress_matrix_key] bound_stress_full = data[pp.DISCRETIZATION_MATRICES][keyword][ discr.bound_stress_matrix_key] cn = g.cell_nodes() for ci in range(g.num_cells): ind = np.zeros(g.num_cells) ind[ci] = 1 nodes = np.squeeze(np.where(cn * ind > 0)) data[pp.PARAMETERS][keyword]["specified_nodes"] = nodes discr.discretize(g, data) partial_stress = data[pp.DISCRETIZATION_MATRICES][keyword][ discr.stress_matrix_key] partial_bound = data[pp.DISCRETIZATION_MATRICES][keyword][ discr.bound_stress_matrix_key] active_faces = data[pp.PARAMETERS][keyword]["active_faces"] if np.any(faces_covered): del_faces = self.expand_indices_nd( np.where(faces_covered)[0], g.dim) # del_faces is already expanded, set dimension to 1 pp.fvutils.remove_nonlocal_contribution( del_faces, 1, partial_stress, partial_bound) faces_covered[active_faces] = True stress += partial_stress bound_stress += partial_bound self.assertTrue((stress_full - stress).max() < 1e-8) self.assertTrue((stress_full - stress).min() > -1e-8) self.assertTrue((bound_stress - bound_stress_full).max() < 1e-8) self.assertTrue((bound_stress - bound_stress_full).min() > -1e-8)
return gb, split_scheme # The main test function @pytest.mark.parametrize( "geometry", [ _two_fractures_overlapping_regions, _two_fractures_non_overlapping_regions, _two_fractures_regions_become_overlapping, ], ) @pytest.mark.parametrize( "method", [pp.Mpfa("flow"), pp.Mpsa("mechanics"), pp.Biot("mechanics", "flow")]) def test_propagation(geometry, method): # Method to test partial discretization (aimed at finite volume methods) under # fracture propagation. The test is based on first discretizing, and then do one # or several fracture propagation steps. after each step, we do a partial # update of the discretization scheme, and compare with a full discretization on # the newly split grid. The test fails unless all discretization matrices generated # are identical. # # NOTE: Only the highest-dimensional grid in the GridBucket is used. # Get GridBucket and splitting schedule gb, faces_to_split = geometry() g = gb.grids_of_dimension(gb.dim_max())[0]
def _assign_discretizations(self) -> None: """ Assign discretizations to the nodes and edges of the grid bucket. Note the attribute subtract_fracture_pressure: Indicates whether or not to subtract the fracture pressure contribution for the contact traction. This should not be done if the scalar variable is temperature. """ if not self._use_ad: # Shorthand key_s, key_m = self.scalar_parameter_key, self.mechanics_parameter_key var_s, var_d = self.scalar_variable, self.displacement_variable # Define discretization # For the Nd domain we solve linear elasticity with mpsa. mpsa = pp.Mpsa(key_m) empty_discr = pp.VoidDiscretization(key_m, ndof_cell=self._Nd) # Scalar discretizations (all dimensions) diff_disc_s = IE_discretizations.ImplicitMpfa(key_s) mass_disc_s = IE_discretizations.ImplicitMassMatrix(key_s, var_s) source_disc_s = pp.ScalarSource(key_s) # Coupling discretizations # All dimensions div_u_disc = pp.DivU( key_m, key_s, variable=var_d, mortar_variable=self.mortar_displacement_variable, ) # Nd grad_p_disc = pp.GradP(key_m) stabilization_disc_s = pp.BiotStabilization(key_s, var_s) # Assign node discretizations for g, d in self.gb: if g.dim == self._Nd: d[pp.DISCRETIZATION] = { var_d: {"mpsa": mpsa}, var_s: { "diffusion": diff_disc_s, "mass": mass_disc_s, "stabilization": stabilization_disc_s, "source": source_disc_s, }, var_d + "_" + var_s: {"grad_p": grad_p_disc}, var_s + "_" + var_d: {"div_u": div_u_disc}, } elif g.dim == self._Nd - 1: d[pp.DISCRETIZATION] = { self.contact_traction_variable: {"empty": empty_discr}, var_s: { "diffusion": diff_disc_s, "mass": mass_disc_s, "source": source_disc_s, }, } else: d[pp.DISCRETIZATION] = { var_s: { "diffusion": diff_disc_s, "mass": mass_disc_s, "source": source_disc_s, } } # Define edge discretizations for the mortar grid contact_law = pp.ColoumbContact( self.mechanics_parameter_key, self._Nd, mpsa ) contact_discr = pp.PrimalContactCoupling( self.mechanics_parameter_key, mpsa, contact_law ) # Account for the mortar displacements effect on scalar balance in the matrix, # as an internal boundary contribution, fracture, aperture changes appear as a # source contribution. div_u_coupling = pp.DivUCoupling( self.displacement_variable, div_u_disc, div_u_disc ) # Account for the pressure contributions to the force balance on the fracture # (see contact_discr). # This discretization needs the keyword used to store the grad p discretization: grad_p_key = key_m matrix_scalar_to_force_balance = pp.MatrixScalarToForceBalance( grad_p_key, mass_disc_s, mass_disc_s ) if self.subtract_fracture_pressure: fracture_scalar_to_force_balance = pp.FractureScalarToForceBalance( mass_disc_s, mass_disc_s ) for e, d in self.gb.edges(): g_l, g_h = self.gb.nodes_of_edge(e) if g_h.dim == self._Nd: d[pp.COUPLING_DISCRETIZATION] = { self.friction_coupling_term: { g_h: (var_d, "mpsa"), g_l: (self.contact_traction_variable, "empty"), (g_h, g_l): ( self.mortar_displacement_variable, contact_discr, ), }, self.scalar_coupling_term: { g_h: (var_s, "diffusion"), g_l: (var_s, "diffusion"), e: ( self.mortar_scalar_variable, pp.RobinCoupling(key_s, diff_disc_s), ), }, "div_u_coupling": { g_h: ( var_s, "mass", ), # This is really the div_u, but this is not implemented g_l: (var_s, "mass"), e: (self.mortar_displacement_variable, div_u_coupling), }, "matrix_scalar_to_force_balance": { g_h: (var_s, "mass"), g_l: (var_s, "mass"), e: ( self.mortar_displacement_variable, matrix_scalar_to_force_balance, ), }, } if self.subtract_fracture_pressure: d[pp.COUPLING_DISCRETIZATION].update( { "fracture_scalar_to_force_balance": { g_h: (var_s, "mass"), g_l: (var_s, "mass"), e: ( self.mortar_displacement_variable, fracture_scalar_to_force_balance, ), } } ) else: d[pp.COUPLING_DISCRETIZATION] = { self.scalar_coupling_term: { g_h: (var_s, "diffusion"), g_l: (var_s, "diffusion"), e: ( self.mortar_scalar_variable, pp.RobinCoupling(key_s, diff_disc_s), ), } } else: gb = self.gb Nd = self._Nd dof_manager = pp.DofManager(gb) eq_manager = pp.ad.EquationManager(gb, dof_manager) g_primary = gb.grids_of_dimension(Nd)[0] g_frac = gb.grids_of_dimension(Nd - 1).tolist() grid_list = [ g_primary, *g_frac, *gb.grids_of_dimension(Nd - 2), *gb.grids_of_dimension(Nd - 3), ] if len(gb.grids_of_dimension(Nd)) != 1: raise NotImplementedError("This will require further work") edge_list_highest = [(g_primary, g) for g in g_frac] edge_list = [e for e, _ in gb.edges()] mortar_proj_scalar = pp.ad.MortarProjections(edges=edge_list, gb=gb, nd=1) mortar_proj_vector = pp.ad.MortarProjections( edges=edge_list_highest, gb=gb, nd=self._Nd ) subdomain_proj_scalar = pp.ad.SubdomainProjections(gb=gb) subdomain_proj_vector = pp.ad.SubdomainProjections(gb=gb, nd=self._Nd) tangential_normal_proj_list = [] normal_proj_list = [] for gf in g_frac: proj = gb.node_props(gf, "tangential_normal_projection") tangential_normal_proj_list.append( proj.project_tangential_normal(gf.num_cells) ) normal_proj_list.append(proj.project_normal(gf.num_cells)) tangential_normal_proj = pp.ad.Matrix( sps.block_diag(tangential_normal_proj_list) ) normal_proj = pp.ad.Matrix(sps.block_diag(normal_proj_list)) # Ad representation of discretizations mpsa_ad = pp.ad.BiotAd(self.mechanics_parameter_key, g_primary) grad_p_ad = pp.ad.GradPAd(self.mechanics_parameter_key, g_primary) mpfa_ad = pp.ad.MpfaAd(self.scalar_parameter_key, grid_list) mass_ad = pp.ad.MassMatrixAd(self.scalar_parameter_key, grid_list) robin_ad = pp.ad.RobinCouplingAd(self.scalar_parameter_key, edge_list) div_u_ad = pp.ad.DivUAd( self.mechanics_parameter_key, grids=g_primary, mat_dict_keyword=self.scalar_parameter_key, ) stab_biot_ad = pp.ad.BiotStabilizationAd( self.scalar_parameter_key, g_primary ) coloumb_ad = pp.ad.ColoumbContactAd( self.mechanics_parameter_key, edge_list_highest ) bc_ad = pp.ad.BoundaryCondition( self.mechanics_parameter_key, grids=[g_primary] ) div_vector = pp.ad.Divergence(grids=[g_primary], dim=g_primary.dim) # Primary variables on Ad form u = eq_manager.variable(g_primary, self.displacement_variable) u_mortar = eq_manager.merge_variables( [(e, self.mortar_displacement_variable) for e in edge_list_highest] ) contact_force = eq_manager.merge_variables( [(g, self.contact_traction_variable) for g in g_frac] ) p = eq_manager.merge_variables( [(g, self.scalar_variable) for g in grid_list] ) mortar_flux = eq_manager.merge_variables( [(e, self.mortar_scalar_variable) for e in edge_list] ) u_prev = u.previous_timestep() u_mortar_prev = u_mortar.previous_timestep() p_prev = p.previous_timestep() # Reference pressure, corresponding to an initial stress free state p_reference = pp.ad.ParameterArray( param_keyword=self.mechanics_parameter_key, array_keyword="p_reference", grids=[g_primary], gb=gb, ) # Stress in g_h stress = ( mpsa_ad.stress * u + mpsa_ad.bound_stress * bc_ad + mpsa_ad.bound_stress * subdomain_proj_vector.face_restriction(g_primary) * mortar_proj_vector.mortar_to_primary_avg * u_mortar + grad_p_ad.grad_p * subdomain_proj_scalar.cell_restriction(g_primary) * p # The reference pressure is only defined on g_primary, thus there is no need # for a subdomain projection. - grad_p_ad.grad_p * p_reference ) momentum_eq = pp.ad.Expression( div_vector * stress, dof_manager, "momentuum", grid_order=[g_primary] ) jump = ( subdomain_proj_vector.cell_restriction(g_frac) * mortar_proj_vector.mortar_to_secondary_avg * mortar_proj_vector.sign_of_mortar_sides ) jump_rotate = tangential_normal_proj * jump # Contact conditions num_frac_cells = np.sum([g.num_cells for g in g_frac]) jump_discr = coloumb_ad.displacement * jump_rotate * u_mortar tmp = np.ones(num_frac_cells * self._Nd) tmp[self._Nd - 1 :: self._Nd] = 0 exclude_normal = pp.ad.Matrix( sps.dia_matrix((tmp, 0), shape=(tmp.size, tmp.size)) ) # Rhs of contact conditions rhs = ( coloumb_ad.rhs + exclude_normal * coloumb_ad.displacement * jump_rotate * u_mortar_prev ) contact_conditions = coloumb_ad.traction * contact_force + jump_discr - rhs contact_eq = pp.ad.Expression( contact_conditions, dof_manager, "contact", grid_order=g_frac ) # Force balance mat = None for _, d in gb.edges(): mg: pp.MortarGrid = d["mortar_grid"] if mg.dim < self._Nd - 1: continue faces_on_fracture_surface = mg.primary_to_mortar_int().tocsr().indices m = pp.grid_utils.switch_sign_if_inwards_normal( g_primary, self._Nd, faces_on_fracture_surface ) if mat is None: mat = m else: mat += m sign_switcher = pp.ad.Matrix(mat) # Contact from primary grid and mortar displacements (via primary grid) contact_from_primary_mortar = ( mortar_proj_vector.primary_to_mortar_int * subdomain_proj_vector.face_prolongation(g_primary) * sign_switcher * stress ) contact_from_secondary = ( mortar_proj_vector.sign_of_mortar_sides * mortar_proj_vector.secondary_to_mortar_int * subdomain_proj_vector.cell_prolongation(g_frac) * tangential_normal_proj.transpose() * contact_force ) if self.subtract_fracture_pressure: # This gives an error because of -= mat = [] for e in edge_list_highest: mg = gb.edge_props(e, "mortar_grid") faces_on_fracture_surface = ( mg.primary_to_mortar_int().tocsr().indices ) sgn, _ = g_primary.signs_and_cells_of_boundary_faces( faces_on_fracture_surface ) fracture_normals = g_primary.face_normals[ : self._Nd, faces_on_fracture_surface ] outwards_fracture_normals = sgn * fracture_normals data = outwards_fracture_normals.ravel("F") row = np.arange(g_primary.dim * mg.num_cells) col = np.tile(np.arange(mg.num_cells), (g_primary.dim, 1)).ravel( "F" ) n_dot_I = sps.csc_matrix((data, (row, col))) # i) The scalar contribution to the contact stress is mapped to # the mortar grid and multiplied by n \dot I, with n being the # outwards normals on the two sides. # Note that by using different normals for the two sides, we do not need to # adjust the secondary pressure with the corresponding signs by applying # sign_of_mortar_sides as done in PrimalContactCoupling. # iii) The contribution should be subtracted so that we balance the primary # forces by # T_contact - n dot I p, # hence the minus. mat.append(n_dot_I * mg.secondary_to_mortar_int(nd=1)) # May need to do this as for tangential projections, additive that is normal_matrix = pp.ad.Matrix(sps.block_diag(mat)) p_frac = subdomain_proj_scalar.cell_restriction(g_frac) * p contact_from_secondary2 = normal_matrix * p_frac force_balance_eq = pp.ad.Expression( contact_from_primary_mortar + contact_from_secondary + contact_from_secondary2, dof_manager, "force_balance", grid_order=edge_list_highest, ) bc_val_scalar = pp.ad.BoundaryCondition( self.scalar_parameter_key, grid_list ) div_scalar = pp.ad.Divergence(grids=grid_list) dt = self.time_step # FIXME: Need bc for div_u term, including previous time step accumulation_primary = ( div_u_ad.div_u * (u - u_prev) + stab_biot_ad.stabilization * subdomain_proj_scalar.cell_restriction(g_primary) * (p - p_prev) + div_u_ad.bound_div_u * subdomain_proj_vector.face_restriction(g_primary) * mortar_proj_vector.mortar_to_primary_int * (u_mortar - u_mortar_prev) ) # Accumulation term on the fractures. frac_vol = np.hstack([g.cell_volumes for g in g_frac]) vol_mat = pp.ad.Matrix( sps.dia_matrix((frac_vol, 0), shape=(num_frac_cells, num_frac_cells)) ) accumulation_fracs = ( vol_mat * normal_proj * jump * (u_mortar - u_mortar_prev) ) accumulation_all = mass_ad.mass * (p - p_prev) flux = ( mpfa_ad.flux * p + mpfa_ad.bound_flux * bc_val_scalar + mpfa_ad.bound_flux * mortar_proj_scalar.mortar_to_primary_int * mortar_flux ) flow_md = ( dt * ( # Time scaling of flux terms, both inter-dimensional and from # the higher dimension div_scalar * flux - mortar_proj_scalar.mortar_to_secondary_int * mortar_flux ) + accumulation_all + subdomain_proj_scalar.cell_prolongation(g_primary) * accumulation_primary + subdomain_proj_scalar.cell_prolongation(g_frac) * accumulation_fracs ) interface_flow_eq = robin_ad.mortar_scaling * ( mortar_proj_scalar.primary_to_mortar_avg * mpfa_ad.bound_pressure_cell * p + mortar_proj_scalar.primary_to_mortar_avg * mpfa_ad.bound_pressure_face * ( mortar_proj_scalar.mortar_to_primary_int * mortar_flux + bc_val_scalar ) - mortar_proj_scalar.secondary_to_mortar_avg * p + robin_ad.mortar_discr * mortar_flux ) flow_eq = pp.ad.Expression( flow_md, dof_manager, "flow on nodes", grid_order=grid_list ) interface_eq = pp.ad.Expression( interface_flow_eq, dof_manager, "flow on interface", grid_order=edge_list, ) eq_manager.equations += [ momentum_eq, contact_eq, force_balance_eq, flow_eq, interface_eq, ] self._eq_manager = eq_manager
def test_assemble_biot_rhs_transient(self): """ Test the assembly of a Biot problem with a non-zero rhs using the assembler. The test checks whether the discretization matches that of the Biot class and that the solution reaches the expected steady state. """ gb = pp.meshing.cart_grid([], [3, 3], physdims=[1, 1]) g = gb.grids_of_dimension(2)[0] d = gb.node_props(g) # Parameters identified by two keywords. Non-default parameters of somewhat # arbitrary values are assigned to make the test more revealing. kw_m = "mechanics" kw_f = "flow" variable_m = "displacement" variable_f = "pressure" bound_mech, bound_flow = self.make_boundary_conditions(g) val_mech = np.ones(g.dim * g.num_faces) val_flow = np.ones(g.num_faces) initial_disp, initial_pressure, initial_state = self.make_initial_conditions( g, x0=1, y0=2, p0=0) dt = 1e0 biot_alpha = 0.6 state = { variable_f: initial_pressure, variable_m: initial_disp, kw_m: { "bc_values": val_mech }, } parameters_m = { "bc": bound_mech, "bc_values": val_mech, "time_step": dt, "biot_alpha": biot_alpha, } parameters_f = { "bc": bound_flow, "bc_values": val_flow, "time_step": dt, "biot_alpha": biot_alpha, "mass_weight": 0.1 * np.ones(g.num_cells), } pp.initialize_default_data(g, d, kw_m, parameters_m) pp.initialize_default_data(g, d, kw_f, parameters_f) pp.set_state(d, state) # Initial condition fot the Biot class # d["state"] = initial_state # Set up the structure for the assembler. First define variables and equation # term names. v_0 = variable_m v_1 = variable_f term_00 = "stress_divergence" term_01 = "pressure_gradient" term_10 = "displacement_divergence" term_11_0 = "fluid_mass" term_11_1 = "fluid_flux" term_11_2 = "stabilization" d[pp.PRIMARY_VARIABLES] = {v_0: {"cells": g.dim}, v_1: {"cells": 1}} d[pp.DISCRETIZATION] = { v_0: { term_00: pp.Mpsa(kw_m) }, v_1: { term_11_0: IE_discretizations.ImplicitMassMatrix(kw_f, v_1), term_11_1: IE_discretizations.ImplicitMpfa(kw_f), term_11_2: pp.BiotStabilization(kw_f), }, v_0 + "_" + v_1: { term_01: pp.GradP(kw_m) }, v_1 + "_" + v_0: { term_10: pp.DivU(kw_m) }, } # Discretize the mechanics related terms using the Biot class biot_discretizer = pp.Biot() biot_discretizer._discretize_mech(g, d) general_assembler = pp.Assembler(gb) # Discretize terms that are not handled by the call to biot_discretizer general_assembler.discretize(term_filter=["fluid_mass", "fluid_flux"]) times = np.arange(5) for _ in times: # Assemble. Also discretizes the flow terms (fluid_mass and fluid_flux) A, b = general_assembler.assemble_matrix_rhs() # Assemble using the Biot class A_class, b_class = biot_discretizer.matrix_rhs(g, d, discretize=False) # Make sure the variable ordering of the matrix assembled by the assembler # matches that of the Biot class. grids = [g, g] variables = [v_0, v_1] A, b = permute_matrix_vector( A, b, general_assembler.block_dof, general_assembler.full_dof, grids, variables, ) # Compare the matrices and rhs vectors self.assertTrue(np.all(np.isclose(A.A, A_class.A))) self.assertTrue(np.all(np.isclose(b, b_class))) # Store the current solution for the next time step. x_i = sps.linalg.spsolve(A_class, b_class) u_i = x_i[:(g.dim * g.num_cells)] p_i = x_i[(g.dim * g.num_cells):] state = {variable_f: p_i, variable_m: u_i} pp.set_state(d, state) # Check that the solution has converged to the expected, uniform steady state # dictated by the BCs. self.assertTrue( np.all(np.isclose(x_i, np.ones((g.dim + 1) * g.num_cells))))