def _discretize(self) -> None: """Discretize all terms""" if not hasattr(self, "dof_manager"): self.dof_manager = pp.DofManager(self.gb) self.assembler = pp.Assembler(self.gb, self.dof_manager) tic = time.time() logger.info("Discretize") if self._use_ad: self._eq_manager.discretize(self.gb) else: self.assembler.discretize() logger.info("Done. Elapsed time {}".format(time.time() - tic))
def _discretize(self) -> None: """Discretize all terms""" if not hasattr(self, "dof_manager"): self.dof_manager = pp.DofManager(self.gb) if not hasattr(self, "assembler"): self.assembler = pp.Assembler(self.gb, self.dof_manager) g_max = self.gb.grids_of_dimension(self._Nd)[0] tic = time.time() logger.info("Discretize") if self._use_ad: self._eq_manager.discretize(self.gb) else: # Discretization is a bit cumbersome, as the Biot discetization removes the # one-to-one correspondence between discretization objects and blocks in # the matrix. # First, Discretize with the biot class self._discretize_biot() # Next, discretize term on the matrix grid not covered by the Biot discretization, # i.e. the diffusion, mass and source terms filt = pp.assembler_filters.ListFilter( grid_list=[g_max], term_list=["source", "mass", "diffusion"] ) self.assembler.discretize(filt=filt) # Build a list of all edges, and all couplings edge_list: List[ Union[ Tuple[pp.Grid, pp.Grid], Tuple[pp.Grid, pp.Grid, Tuple[pp.Grid, pp.Grid]], ] ] = [] for e, _ in self.gb.edges(): edge_list.append(e) edge_list.append((e[0], e[1], e)) if len(edge_list) > 0: filt = pp.assembler_filters.ListFilter(grid_list=edge_list) # type: ignore self.assembler.discretize(filt=filt) # Finally, discretize terms on the lower-dimensional grids. This can be done # in the traditional way, as there is no Biot discretization here. for dim in range(0, self._Nd): grid_list = self.gb.grids_of_dimension(dim) if len(grid_list) > 0: filt = pp.assembler_filters.ListFilter(grid_list=grid_list) self.assembler.discretize(filt=filt) logger.info("Done. Elapsed time {}".format(time.time() - tic))
else: d[pp.PARAMETERS][param_key]["bc_values"] = bc_values d[pp.PARAMETERS][param_key]["source"] = source_term #%% Set initial states for g, d in gb: cc = g.cell_centers pp.set_state(d) pp.set_iterate(d) d[pp.STATE][pressure_var] = p_ex(cc[0], cc[1], time * np.ones_like(cc[0])) d[pp.STATE][pp.ITERATE][pressure_var] = d[pp.STATE][pressure_var].copy() #%% AD variables and manager grid_list = [g for g, _ in gb] dof_manager = pp.DofManager(gb) equation_manager = pp.ad.EquationManager(gb, dof_manager) p = equation_manager.merge_variables([(g, pressure_var) for g in grid_list]) p_m = p.previous_iteration() p_n = p.previous_timestep() #%% We let the density to be a non-linear function of the pressure def rho(p): if isinstance(p, pp.ad.Ad_array): return rho_ref * pp.ad.exp(c * (p - p_ref)) else: return rho_ref * np.exp(c * (p - p_ref)) rho_ad = pp.ad.Function(rho, name="density") #%% Initialize exporter
def _discretize(self) -> None: """Discretize all terms""" if not hasattr(self, "dof_manager"): self.dof_manager = pp.DofManager(self.gb) if not hasattr(self, "assembler"): self.assembler = pp.Assembler(self.gb, self.dof_manager) tic = time.time() logger.info("Discretize") # Discretization is a bit cumbersome, as the Biot discetization removes the # one-to-one correspondence between discretization objects and blocks in the matrix. # First, Discretize with the biot class self._discretize_biot() self._copy_biot_discretizations() # Next, discretize term on the matrix grid not covered by the Biot discretization, # i.e. the source, diffusion and mass terms filt = pp.assembler_filters.ListFilter( grid_list=[self._nd_grid()], variable_list=[self.scalar_variable], term_list=["source", "diffusion", "mass"], ) self.assembler.discretize(filt=filt) # Then the temperature discretizations temperature_terms = ["source", "diffusion", "mass", self.advection_term] filt = pp.assembler_filters.ListFilter( grid_list=[self._nd_grid()], variable_list=[self.temperature_variable], term_list=temperature_terms, ) self.assembler.discretize(filt=filt) # Coupling terms coupling_terms = [self.s2t_coupling_term, self.t2s_coupling_term] filt = pp.assembler_filters.ListFilter( grid_list=[self._nd_grid()], variable_list=[self.temperature_variable, self.scalar_variable], term_list=coupling_terms, ) self.assembler.discretize(filt=filt) # Build a list of all edges, and all couplings edge_list: List[ Union[ Tuple[pp.Grid, pp.Grid], Tuple[pp.Grid, pp.Grid, Tuple[pp.Grid, pp.Grid]], ] ] = [] for e, _ in self.gb.edges(): edge_list.append(e) edge_list.append((e[0], e[1], e)) if len(edge_list) > 0: filt = pp.assembler_filters.ListFilter(grid_list=edge_list) # type: ignore self.assembler.discretize(filt=filt) # Finally, discretize terms on the lower-dimensional grids. This can be done # in the traditional way, as there is no Biot discretization here. for dim in range(0, self._Nd): grid_list = self.gb.grids_of_dimension(dim) if len(grid_list) > 0: filt = pp.assembler_filters.ListFilter(grid_list=grid_list) self.assembler.discretize(filt=filt) logger.info("Done. Elapsed time {}".format(time.time() - tic))
def test_md_flow(): # Three fractures, will create intersection lines and point frac_1 = np.array([[2, 4, 4, 2], [3, 3, 3, 3], [0, 0, 6, 6]]) frac_2 = np.array([[3, 3, 3, 3], [2, 4, 4, 2], [0, 0, 6, 6]]) frac_3 = np.array([[0, 6, 6, 0], [2, 2, 4, 4], [3, 3, 3, 3]]) gb = pp.meshing.cart_grid(fracs=[frac_1, frac_2, frac_3], nx=np.array([6, 6, 6])) gb.compute_geometry() pressure_variable = "pressure" flux_variable = "mortar_flux" keyword = "flow" discr = pp.Tpfa(keyword) source_discr = pp.ScalarSource(keyword) coupling_discr = pp.RobinCoupling(keyword, discr, discr) for g, d in gb: # Assign data if g.dim == gb.dim_max(): upper_left_ind = np.argmax(np.linalg.norm(g.face_centers, axis=0)) bc = pp.BoundaryCondition(g, np.array([0, upper_left_ind]), ["dir", "dir"]) bc_values = np.zeros(g.num_faces) bc_values[0] = 1 sources = np.random.rand(g.num_cells) * g.cell_volumes specified_parameters = { "bc": bc, "bc_values": bc_values, "source": sources } else: sources = np.random.rand(g.num_cells) * g.cell_volumes specified_parameters = {"source": sources} # Initialize data pp.initialize_default_data(g, d, keyword, specified_parameters) # Declare grid primary variable d[pp.PRIMARY_VARIABLES] = {pressure_variable: {"cells": 1}} # Assign discretization d[pp.DISCRETIZATION] = { pressure_variable: { "diff": discr, "source": source_discr } } # Initialize state d[pp.STATE] = { pressure_variable: np.zeros(g.num_cells), pp.ITERATE: { pressure_variable: np.zeros(g.num_cells) }, } for e, d in gb.edges(): mg = d["mortar_grid"] pp.initialize_data(mg, d, keyword, {"normal_diffusivity": 1}) d[pp.PRIMARY_VARIABLES] = {flux_variable: {"cells": 1}} d[pp.COUPLING_DISCRETIZATION] = {} d[pp.COUPLING_DISCRETIZATION]["coupling"] = { e[0]: (pressure_variable, "diff"), e[1]: (pressure_variable, "diff"), e: (flux_variable, coupling_discr), } d[pp.STATE] = { flux_variable: np.zeros(mg.num_cells), pp.ITERATE: { flux_variable: np.zeros(mg.num_cells) }, } dof_manager = pp.DofManager(gb) assembler = pp.Assembler(gb, dof_manager) assembler.discretize() # Reference discretization A_ref, b_ref = assembler.assemble_matrix_rhs() manager = pp.ad.EquationManager(gb, dof_manager) grid_list = [g for g, _ in gb] edge_list = [e for e, _ in gb.edges()] node_discr = pp.ad.MpfaAd(keyword, grid_list) edge_discr = pp.ad.RobinCouplingAd(keyword, edge_list) bc_val = pp.ad.BoundaryCondition(keyword, grid_list) source = pp.ad.ParameterArray(param_keyword=keyword, array_keyword='source', grids=grid_list) projections = pp.ad.MortarProjections(gb=gb) div = pp.ad.Divergence(grids=grid_list) p = manager.merge_variables([(g, pressure_variable) for g in grid_list]) lmbda = manager.merge_variables([(e, flux_variable) for e in edge_list]) flux = (node_discr.flux * p + node_discr.bound_flux * bc_val + node_discr.bound_flux * projections.mortar_to_primary_int * lmbda) flow_eq = div * flux - projections.mortar_to_secondary_int * lmbda - source interface_flux = edge_discr.mortar_scaling * ( projections.primary_to_mortar_avg * node_discr.bound_pressure_cell * p + projections.primary_to_mortar_avg * node_discr.bound_pressure_face * projections.mortar_to_primary_int * lmbda - projections.secondary_to_mortar_avg * p + edge_discr.mortar_discr * lmbda) flow_eq_ad = pp.ad.Expression(flow_eq, dof_manager, "flow on nodes") flow_eq_ad.discretize(gb) interface_eq_ad = pp.ad.Expression(interface_flux, dof_manager, "flow on interface") manager.equations += [flow_eq_ad, interface_eq_ad] state = np.zeros(gb.num_cells() + gb.num_mortar_cells()) A, b = manager.assemble_matrix_rhs(state=state) diff = A - A_ref if diff.data.size > 0: assert np.max(np.abs(diff.data)) < 1e-10 assert np.max(np.abs(b - b_ref)) < 1e-10
def test_ad_variable_vrappers(): fracs = [np.array([[0, 2], [1, 1]]), np.array([[1, 1], [0, 2]])] gb = pp.meshing.cart_grid(fracs, [2, 2]) state_map = {} iterate_map = {} state_map_2, iterate_map_2 = {}, {} var = 'foo' var2 = 'bar' mortar_var = 'mv' def _compare_ad_objects(a, b): va, ja = a.val, a.jac vb, jb = b.val, b.jac assert np.allclose(va, vb) assert ja.shape == jb.shape d = ja - jb if d.data.size > 0: assert np.max(np.abs(d.data)) < 1e-10 for g, d in gb: if g.dim == 1: num_dofs = 2 else: num_dofs = 1 d[pp.PRIMARY_VARIABLES] = {var: {'cells': num_dofs}} val_state = np.random.rand(g.num_cells * num_dofs) val_iterate = np.random.rand(g.num_cells * num_dofs) d[pp.STATE] = {var: val_state, pp.ITERATE: {var: val_iterate}} state_map[g] = val_state iterate_map[g] = val_iterate # Add a second variable to the 2d grid, just for the fun of it if g.dim == 2: d[pp.PRIMARY_VARIABLES][var2] = {'cells': 1} val_state = np.random.rand(g.num_cells) val_iterate = np.random.rand(g.num_cells) d[pp.STATE][var2] = val_state d[pp.STATE][pp.ITERATE][var2] = val_iterate state_map_2[g] = val_state iterate_map_2[g] = val_iterate for e, d in gb.edges(): mg = d['mortar_grid'] if mg.dim == 1: num_dofs = 2 else: num_dofs = 1 d[pp.PRIMARY_VARIABLES] = {mortar_var: {'cells': num_dofs}} val_state = np.random.rand(mg.num_cells * num_dofs) val_iterate = np.random.rand(mg.num_cells * num_dofs) d[pp.STATE] = { mortar_var: val_state, pp.ITERATE: { mortar_var: val_iterate } } state_map[e] = val_state iterate_map[e] = val_iterate dof_manager = pp.DofManager(gb) eq_manager = pp.ad.EquationManager(gb, dof_manager) # Manually assemble state and iterate true_state = np.zeros(dof_manager.num_dofs()) true_iterate = np.zeros(dof_manager.num_dofs()) # Also a state array that differs from the stored iterates double_iterate = np.zeros(dof_manager.num_dofs()) for (g, v) in dof_manager.block_dof: inds = dof_manager.dof_ind(g, v) if v == var2: true_state[inds] = state_map_2[g] true_iterate[inds] = iterate_map_2[g] double_iterate[inds] = 2 * iterate_map_2[g] else: true_state[inds] = state_map[g] true_iterate[inds] = iterate_map[g] double_iterate[inds] = 2 * iterate_map[g] grid_list = [ gb.grids_of_dimension(2)[0], *gb.grids_of_dimension(1), gb.grids_of_dimension(0)[0] ] # Generate merged variables via the EquationManager. var_ad = eq_manager.merge_variables([(g, var) for g in grid_list]) # Check equivalence between the two approaches to generation. eq_1 = pp.ad.Expression(var_ad, dof_manager) # Check that the state is correctly evaluated. inds_var = np.hstack([dof_manager.dof_ind(g, var) for g in grid_list]) assert np.allclose(true_iterate[inds_var], eq_1.to_ad(gb, true_iterate).val) # Check evaluation when no state is passed to the parser, and information must # instead be glued together from the GridBucket assert np.allclose(true_iterate[inds_var], eq_1.to_ad(gb).val) # Evaluate the equation using the double iterate assert np.allclose(2 * true_iterate[inds_var], eq_1.to_ad(gb, double_iterate).val) # Represent the variable on the previous time step. This should be a numpy array prev_var_ad = var_ad.previous_timestep() eq_prev = pp.ad.Expression(prev_var_ad, dof_manager) prev_evaluated = eq_prev.to_ad(gb) assert isinstance(prev_evaluated, np.ndarray) assert np.allclose(true_state[inds_var], prev_evaluated) # Also check that state values given to the ad parser are ignored for previous # values assert np.allclose(prev_evaluated, eq_prev.to_ad(gb, double_iterate)) ## Next, test edge variables. This should be much the same as the grid variables, # so the testing is less thorough. # Form an edge variable, evaluate this edge_list = [e for e, _ in gb.edges()] var_edge = eq_manager.merge_variables([(e, mortar_var) for e in edge_list]) eq_2 = pp.ad.Expression(var_edge, dof_manager) edge_inds = np.hstack( [dof_manager.dof_ind(e, mortar_var) for e in edge_list]) assert np.allclose(true_iterate[edge_inds], eq_2.to_ad(gb, true_iterate).val) # Finally, test a single variable; everything should work then as well g = gb.grids_of_dimension(2)[0] v1 = eq_manager.variable(g, var) v2 = eq_manager.variable(g, var2) eq_3 = pp.ad.Expression(v1, dof_manager) eq_4 = pp.ad.Expression(v2, dof_manager) ind1 = dof_manager.dof_ind(g, var) ind2 = dof_manager.dof_ind(g, var2) assert np.allclose(true_iterate[ind1], eq_3.to_ad(gb, true_iterate).val) assert np.allclose(true_iterate[ind2], eq_4.to_ad(gb, true_iterate).val) v1_prev = v1.previous_timestep() eq_5 = pp.ad.Expression(v1_prev, dof_manager) assert np.allclose(true_state[ind1], eq_5.to_ad(gb, true_iterate))
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
edge_list = [e for e, _ in gb.edges()] # The divergence div = pp.ad.Divergence(grid_list) # projection between subdomains and mortar grids if len(edge_list) > 0: mortar_projection = pp.ad.MortarProjections(gb=gb, nd=1) # end if # Boundary condionts bound_ad = pp.ad.BoundaryCondition(kw_f, grids=grid_list) #%% Set an dof and equation manager to keep track of the equations dof_manager = pp.DofManager(gb) equation_manager = pp.ad.EquationManager(gb, dof_manager) p = equation_manager.merge_variables([(g, grid_variable) for g in grid_list]) if len(edge_list) > 0: lam = equation_manager.merge_variables([(e, mortar_variable) for e in edge_list]) # end if #%% Discretize the flow problem # AD version of mpfa mpfa = pp.ad.MpfaAd(kw_f, grids=grid_list) interior_flux = mpfa.flux * p # the flux on the subdomains
def _verify(self, gb, split_faces): # Common function for all tests. # Propagate fracture, verify that variables are mapped correctly, and that # new variables are added with correct values # Individual grids g_2d = gb.grids_of_dimension(2)[0] g_1d = gb.grids_of_dimension(1) # Model used for fracture propagation model = MockPropagationModel({}) model.gb = gb # Cell variable in 2d. Should stay constant throughout the process. cell_val_2d = np.random.rand(g_2d.num_cells) ## Initialize variables for all grids # Cell variable in 1d. Sholud be expanded, but initial size is num_vars_2d var_sz_1d = 2 # 1d variables defined as a dict, indexed on the grid cell_val_1d = {g: np.random.rand(var_sz_1d) for g in g_1d} var_sz_mortar = 3 cell_val_mortar = {g: np.random.rand(var_sz_mortar * 2) for g in g_1d} # Define variables on all grids. # Initialize the state by the known variable, and the iterate as twice that value # (mostly a why not) d = gb.node_props(g_2d) d[pp.PRIMARY_VARIABLES] = {self.cv2: {"cells": 1}} d[pp.STATE] = { self.cv2: cell_val_2d, pp.ITERATE: { self.cv2: np.array(2 * cell_val_2d) }, } for g in g_1d: d = gb.node_props(g) d[pp.PRIMARY_VARIABLES] = {self.cv1: {"cells": var_sz_1d}} d[pp.STATE] = { self.cv1: cell_val_1d[g], pp.ITERATE: { self.cv1: np.array(2 * cell_val_1d[g]) }, } d = gb.edge_props((g_2d, g)) d[pp.PRIMARY_VARIABLES] = {self.mv: {"cells": var_sz_mortar}} d[pp.STATE] = { self.mv: cell_val_mortar[g], pp.ITERATE: { self.mv: np.array(2 * cell_val_mortar[g]) }, } # Define assembler, thereby a dof ordering dof_manager = pp.DofManager(gb) assembler = pp.Assembler(gb, dof_manager) model.assembler = assembler # Define and initialize a state vector x = np.zeros(dof_manager.full_dof.sum()) x[dof_manager.dof_ind(g_2d, self.cv2)] = cell_val_2d for g in g_1d: x[dof_manager.dof_ind(g, self.cv1)] = cell_val_1d[g] x[dof_manager.dof_ind((g_2d, g), self.mv)] = cell_val_mortar[g] # Keep track of the previous values for each grid. # Not needed in 2d, where no updates are expected val_1d_prev = {g: cell_val_1d[g] for g in g_1d} val_mortar_prev = {g: cell_val_mortar[g] for g in g_1d} val_1d_iterate_prev = {g: 2 * cell_val_1d[g] for g in g_1d} val_mortar_iterate_prev = {g: 2 * cell_val_mortar[g] for g in g_1d} # Loop over all propagation steps for i, split in enumerate(split_faces): # Propagate the fracture. This will also generate mappings from old to new # cells pp.propagate_fracture.propagate_fractures(gb, split) # Update variables x_new = model._map_variables(x) # The values of the 2d cell should not change self.assertTrue( np.all( x_new[dof_manager.dof_ind(g_2d, self.cv2)] == cell_val_2d)) # Also check that pp.STATE and ITERATE has been correctly updated d = gb.node_props(g_2d) self.assertTrue(np.all(d[pp.STATE][self.cv2] == cell_val_2d)) self.assertTrue( np.all(d[pp.STATE][pp.ITERATE][self.cv2] == 2 * cell_val_2d)) # Loop over all 1d grids, check both grid and the associated mortar grid for g in g_1d: num_new_cells = split[g].size # mapped variable x_1d = x_new[dof_manager.dof_ind(g, self.cv1)] # Extension of the 1d grid. All values should be 42 (see propagation class) extended_1d = np.full(var_sz_1d * num_new_cells, 42) # True values for 1d (both grid and iterate) truth_1d = np.r_[val_1d_prev[g], extended_1d] truth_iterate = np.r_[val_1d_iterate_prev[g], extended_1d] self.assertTrue(np.allclose(x_1d, truth_1d)) # Also check that pp.STATE and ITERATE has been correctly updated d = gb.node_props(g) self.assertTrue(np.all(d[pp.STATE][self.cv1] == truth_1d)) self.assertTrue( np.all(d[pp.STATE][pp.ITERATE][self.cv1] == truth_iterate)) # The propagation model will assign the value 42 to new cells also in the # next step. To be sure values from the first propagation are mapped # correctly, we alter the true value (add 1), and update this both in # the solution vector, state and previous iterate x_new[dof_manager.dof_ind(g, self.cv1)] = np.r_[val_1d_prev[g], extended_1d + 1] val_1d_prev[g] = x_new[dof_manager.dof_ind(g, self.cv1)] d[pp.STATE][self.cv1] = x_new[dof_manager.dof_ind(g, self.cv1)] val_1d_iterate_prev[g] = np.r_[val_1d_iterate_prev[g], extended_1d + 1] d[pp.STATE][pp.ITERATE][self.cv1] = val_1d_iterate_prev[g] ## Check mortar grid - see 1d case above for comments x_mortar = x_new[dof_manager.dof_ind((g_2d, g), self.mv)] sz = int(np.round(val_mortar_prev[g].size / 2)) # The new mortar cells are appended such that the cells are ordered # contiguously on the two sides. truth_mortar = np.r_[val_mortar_prev[g][:sz], np.full(var_sz_mortar * num_new_cells, 42), val_mortar_prev[g][sz:2 * sz], np.full(var_sz_mortar * num_new_cells, 42), ] truth_iterate = np.r_[val_mortar_iterate_prev[g][:sz], np.full(var_sz_mortar * num_new_cells, 42), val_mortar_iterate_prev[g][sz:2 * sz], np.full(var_sz_mortar * num_new_cells, 42), ] d = gb.edge_props((g_2d, g)) self.assertTrue(np.all(x_mortar == truth_mortar)) self.assertTrue(np.all(d[pp.STATE][self.mv] == truth_mortar)) self.assertTrue( np.all(d[pp.STATE][pp.ITERATE][self.mv] == truth_iterate)) x_new[dof_manager.dof_ind( (g_2d, g), self.mv)] = np.r_[val_mortar_prev[g][:sz], np.full(var_sz_mortar * num_new_cells, 43), val_mortar_prev[g][sz:2 * sz], np.full(var_sz_mortar * num_new_cells, 43), ] val_mortar_prev[g] = x_new[dof_manager.dof_ind((g_2d, g), self.mv)] val_mortar_iterate_prev[g] = np.r_[ val_mortar_iterate_prev[g][:sz], np.full(var_sz_mortar * num_new_cells, 43), val_mortar_iterate_prev[g][sz:2 * sz], np.full(var_sz_mortar * num_new_cells, 43), ] d[pp.STATE][self.mv] = val_mortar_prev[g] d[pp.STATE][pp.ITERATE][self.mv] = val_mortar_iterate_prev[g] x = x_new
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