def __init__(self, gb, flow, model="flow"): self.model = model self.gb = gb self.data = None self.assembler = None # discretization operator name self.discr_name = self.model + "_flux" self.discr = pp.Tpfa(self.model) self.coupling_name = self.discr_name + "_coupling" self.coupling = pp.RobinCoupling(self.model, self.discr) self.source_name = self.model + "_source" self.source = pp.ScalarSource(self.model) # master variable name self.variable = self.model + "_variable" self.mortar = self.model + "_lambda" # post process variables self.pressure = "pressure" self.flux = "darcy_flux" # it has to be this one self.P0_flux = "P0_darcy_flux"
def solve_tpfa(gb, folder, discr_3d=None): # Choose and define the solvers and coupler flow_discretization = pp.Tpfa("flow") source_discretization = pp.ScalarSource("flow") run_flow(gb, flow_discretization, source_discretization, folder, is_FV=True, discr_3d=discr_3d)
def test_integral(self): g, d = setup_3d_grid() src_disc = pp.ScalarSource("flow") src_disc.discretize(g, d) lhs, rhs = src_disc.assemble_matrix_rhs(g, d) rhs_t = np.array([0, 0, 0, 0, 1, 0, 0, 0]) self.assertTrue(src_disc.ndof(g) == g.num_cells) self.assertTrue(np.all(rhs == rhs_t)) self.assertTrue(lhs.shape == (8, 8)) self.assertTrue(lhs.nnz == 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. """ 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 assign_scalar_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. """ gb = self.gb # Shorthand key_s = self.scalar_parameter_key var_s = self.scalar_variable discr_key, coupling_discr_key = pp.DISCRETIZATION, pp.COUPLING_DISCRETIZATION # Scalar discretizations (all dimensions) diff_disc_s = implicit_euler.ImplicitMpfa(key_s) mass_disc_s = implicit_euler.ImplicitMassMatrix(key_s, var_s) source_disc_s = pp.ScalarSource(key_s) # Assign node discretizations for _, d in gb: add_nonpresent_dictionary(d, discr_key) d[discr_key].update({ var_s: { "diffusion": diff_disc_s, "mass": mass_disc_s, "source": source_disc_s, }, }) # Assign edge discretizations for e, d in gb.edges(): g_l, g_h = gb.nodes_of_edge(e) add_nonpresent_dictionary(d, coupling_discr_key) d[coupling_discr_key].update({ 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), ), }, })
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. """ # from porepy.utils.derived_discretizations import ( # implicit_euler as IE_discretizations, # ) # Shorthand key_s = self.scalar_parameter_key var_s = self.scalar_variable # Scalar discretizations (all dimensions) diff_disc_s = pp.Mpfa(key_s) # mass_disc_s = pp.MassMatrix(key_s) # 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) # Assign node discretizations for g, d in self.gb: d[pp.DISCRETIZATION] = { var_s: { "diffusion": diff_disc_s, # "mass": mass_disc_s, "source": source_disc_s, }, } for e, d in self.gb.edges(): g_l, g_h = self.gb.nodes_of_edge(e) 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), ), }, }
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. """ # Call parent class for disrcetizations for the poro-elastic system. super().assign_discretizations() # What remains is terms related to temperature # Shorthand for parameter keywords key_t = self.temperature_parameter_key key_m = self.mechanics_parameter_key # distinguish the coupling terms from temperature terms (grad_p, as opposed to grad_T) key_mt = self.mechanics_temperature_parameter_key var_s = self.scalar_variable var_t = self.temperature_variable var_d = self.displacement_variable # Define discretization # Scalar discretizations (will be assigned to all dimensions) diff_disc_t = IE_discretizations.ImplicitMpfa(key_t) adv_disc_t = IE_discretizations.ImplicitUpwind(key_t) mass_disc_t = IE_discretizations.ImplicitMassMatrix(key_t, var_t) source_disc_t = pp.ScalarSource(key_t) # Coupling discretizations # All dimensions div_u_disc_t = pp.DivU( key_m, key_t, variable=var_d, mortar_variable=self.mortar_displacement_variable, ) # Nd grad_t_disc = pp.GradP( key_mt ) # pp.GradP(key_m) # Kanskje denne (og andre) b;r erstattes av spesiallagde # varianter som henter ut s-varianten og ganger med alpha/beta? stabilization_disc_t = pp.BiotStabilization(key_t, var_t) s2t_disc = IE_discretizations.ImplicitMassMatrix( keyword=self.s2t_parameter_key, variable=var_s) t2s_disc = IE_discretizations.ImplicitMassMatrix( keyword=self.t2s_parameter_key, variable=var_t) # Assign node discretizations for g, d in self.gb: if g.dim == self.Nd: d[pp.DISCRETIZATION].update( { # advection-diffusion equation for temperature var_t: { "diffusion": diff_disc_t, self.advection_term: adv_disc_t, "mass": mass_disc_t, # Also the stabilization term from Biot "stabilization": stabilization_disc_t, "source": source_disc_t, }, # grad T term in the momentuum equation var_d + "_" + var_t: {"grad_p": grad_t_disc}, # div u term in the energy equation var_t + "_" + var_d: {"div_u": div_u_disc_t}, # Accumulation of pressure in energy equation var_t + "_" + var_s: {self.s2t_coupling_term: s2t_disc}, # Accumulation of temperature / energy in pressure equation var_s + "_" + var_t: {self.t2s_coupling_term: t2s_disc}, } ) else: # Inside fracture network d[pp.DISCRETIZATION].update( { # Advection-diffusion equation, no biot stabilization var_t: { "diffusion": diff_disc_t, self.advection_term: adv_disc_t, "mass": mass_disc_t, "source": source_disc_t, }, # Standard coupling terms var_t + "_" + var_s: {self.s2t_coupling_term: s2t_disc}, var_s + "_" + var_t: {self.t2s_coupling_term: t2s_disc}, } ) # Next, the edges in the gb. # Account for the mortar displacements effect on energy balance in the matrix, # as an internal boundary contribution div_u_coupling_t = pp.DivUCoupling(self.displacement_variable, div_u_disc_t, div_u_disc_t) # Account for the temperature contributions to the force balance on the fracture # (see contact_discr). # This discretization needs the keyword used to store the grad p discretization: grad_t_key = key_mt matrix_temperature_to_force_balance = pp.MatrixScalarToForceBalance( grad_t_key, mass_disc_t, mass_disc_t) # Coupling of advection and diffusion terms in the energy equation adv_coupling_t = IE_discretizations.ImplicitUpwindCoupling(key_t) diff_coupling_t = pp.RobinCoupling(key_t, diff_disc_t) for e, d in self.gb.edges(): g_l, g_h = self.gb.nodes_of_edge(e) d[pp.COUPLING_DISCRETIZATION].update({ self.advection_coupling_term: { g_h: (self.temperature_variable, self.advection_term), g_l: (self.temperature_variable, self.advection_term), e: (self.mortar_temperature_advection_variable, adv_coupling_t), }, self.temperature_coupling_term: { g_h: (var_t, "diffusion"), g_l: (var_t, "diffusion"), e: (self.mortar_temperature_variable, diff_coupling_t), }, }) if g_h.dim == self.Nd: d[pp.COUPLING_DISCRETIZATION].update({ "div_u_coupling_t": { g_h: ( var_t, "mass", ), # This is really the div_u, but this is not implemented g_l: (var_t, "mass"), e: (self.mortar_displacement_variable, div_u_coupling_t), }, "matrix_temperature_to_force_balance": { g_h: (var_t, "mass"), g_l: (var_t, "mass"), e: ( self.mortar_displacement_variable, matrix_temperature_to_force_balance, ), }, })
def solve(self, gb, analytic_p): # Parameter-discretization keyword: kw = "flow" # Terms key_flux = "flux" key_src = "src" # Primary variables key_p = "pressure" # pressure name key_m = "mortar" # mortar name tpfa = pp.Tpfa(kw) src = pp.ScalarSource(kw) for g, d in gb: d[pp.DISCRETIZATION] = {key_p: {key_src: src, key_flux: tpfa}} d[pp.PRIMARY_VARIABLES] = {key_p: {"cells": 1}} for e, d in gb.edges(): g1, g2 = gb.nodes_of_edge(e) d[pp.PRIMARY_VARIABLES] = {key_m: {"cells": 1}} if g1.dim == g2.dim: mortar_disc = pp.FluxPressureContinuity(kw, tpfa) else: mortar_disc = pp.RobinCoupling(kw, tpfa) d[pp.COUPLING_DISCRETIZATION] = { key_flux: { g1: (key_p, key_flux), g2: (key_p, key_flux), e: (key_m, mortar_disc), } } assembler = pp.Assembler(gb) assembler.discretize() A, b = assembler.assemble_matrix_rhs() x = sps.linalg.spsolve(A, b) assembler.distribute_variable(x) # test pressure for g, d in gb: ap, _, _ = analytic_p(g.cell_centers) self.assertTrue(np.max(np.abs(d[pp.STATE][key_p] - ap)) < 5e-2) # test mortar solution for e, d_e in gb.edges(): mg = d_e["mortar_grid"] g1, g2 = gb.nodes_of_edge(e) if g1 == g2: left_to_m = mg.master_to_mortar_avg() right_to_m = mg.slave_to_mortar_avg() else: continue d1 = gb.node_props(g1) d2 = gb.node_props(g2) _, analytic_flux, _ = analytic_p(g1.face_centers) # the aperture is assumed constant left_flux = np.sum(analytic_flux * g1.face_normals[:2], 0) left_flux = left_to_m * ( d1[pp.DISCRETIZATION_MATRICES][kw]["bound_flux"] * left_flux ) # right flux is negative lambda right_flux = np.sum(analytic_flux * g2.face_normals[:2], 0) right_flux = -right_to_m * ( d2[pp.DISCRETIZATION_MATRICES][kw]["bound_flux"] * right_flux ) self.assertTrue(np.max(np.abs(d_e[pp.STATE][key_m] - left_flux)) < 5e-2) self.assertTrue(np.max(np.abs(d_e[pp.STATE][key_m] - right_flux)) < 5e-2)
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 _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 transport(gb, data, solver_name, folder, callback=None, save_every=1): grid_variable = "tracer" mortar_variable = "mortar_tracer" kw = "transport" # Identifier of the discretization operator on each grid advection_term = "advection" source_term = "source" mass_term = "mass" # Identifier of the discretization operator between grids advection_coupling_term = "advection_coupling" # Discretization objects node_discretization = pp.Upwind(kw) source_discretization = pp.ScalarSource(kw) mass_discretization = pp.MassMatrix(kw) edge_discretization = pp.UpwindCoupling(kw) # Loop over the nodes in the GridBucket, define primary variables and discretization schemes for g, d in gb: # Assign primary variables on this grid. It has one degree of freedom per cell. d[pp.PRIMARY_VARIABLES] = {grid_variable: {"cells": 1, "faces": 0}} # Assign discretization operator for the variable. d[pp.DISCRETIZATION] = { grid_variable: { advection_term: node_discretization, source_term: source_discretization, mass_term: mass_discretization, } } # Loop over the edges in the GridBucket, define primary variables and discretizations for e, d in gb.edges(): g1, g2 = gb.nodes_of_edge(e) # The mortar variable has one degree of freedom per cell in the mortar grid d[pp.PRIMARY_VARIABLES] = {mortar_variable: {"cells": 1}} d[pp.COUPLING_DISCRETIZATION] = { advection_coupling_term: { g1: (grid_variable, advection_term), g2: (grid_variable, advection_term), e: (mortar_variable, edge_discretization), } } d[pp.DISCRETIZATION_MATRICES] = {kw: {}} assembler = pp.Assembler() # Assemble the linear system, using the information stored in the GridBucket. By # not adding the matrices, we can arrange them at will to obtain the efficient # solver defined below, which LU factorizes the system only once for all time steps. A, b, block_dof, full_dof = assembler.assemble_matrix_rhs( gb, active_variables=[grid_variable, mortar_variable], add_matrices=False) advection_coupling_term += ("_" + mortar_variable + "_" + grid_variable + "_" + grid_variable) mass_term += "_" + grid_variable advection_term += "_" + grid_variable source_term += "_" + grid_variable lhs = A[mass_term] + data["time_step"] * (A[advection_term] + A[advection_coupling_term]) rhs_source_adv = b[source_term] + data["time_step"] * ( b[advection_term] + b[advection_coupling_term]) IEsolver = sps.linalg.factorized(lhs) n_steps = int(np.round(data["t_max"] / data["time_step"])) # Initial condition tracer = np.zeros(rhs_source_adv.size) assembler.distribute_variable( gb, tracer, block_dof, full_dof, variable_names=[grid_variable, mortar_variable]) # Exporter exporter = pp.Exporter(gb, name="tracer", folder=folder) export_fields = ["tracer"] # Keep track of the outflow for each time step outflow = np.empty(0) # Time loop for i in range(n_steps): # Export existing solution (final export is taken care of below) assembler.distribute_variable( gb, tracer, block_dof, full_dof, variable_names=[grid_variable, mortar_variable], ) if np.isclose(i % save_every, 0): exporter.write_vtk(export_fields, time_step=int(i // save_every)) tracer = IEsolver(A[mass_term] * tracer + rhs_source_adv) outflow = compute_flow_rate(gb, grid_variable, outflow) if callback is not None: callback(gb) exporter.write_vtk(export_fields, time_step=(n_steps // save_every)) time_steps = np.arange(0, data["t_max"] + data["time_step"], save_every * data["time_step"]) exporter.write_pvd(time_steps) return tracer, outflow, A, b, block_dof, full_dof
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 advdiff(gb, discr, param, bc_flag): model = "transport" model_data_adv, model_data_diff, model_data_src = data_advdiff( gb, model, param, bc_flag ) # discretization operator names adv_id = "advection" diff_id = "diffusion" src_id = "source" # variable names variable = "scalar" mortar_adv = "lambda_" + variable + "_" + adv_id mortar_diff = "lambda_" + variable + "_" + diff_id # save variable name for the post-process param["scalar"] = variable discr_adv = pp.Upwind(model_data_adv) discr_adv_interface = pp.CellDofFaceDofMap(model_data_adv) discr_diff = pp.Tpfa(model_data_diff) discr_diff_interface = pp.CellDofFaceDofMap(model_data_diff) coupling_adv = pp.UpwindCoupling(model_data_adv) coupling_diff = pp.FluxPressureContinuity( model_data_diff, discr_diff, discr_diff_interface ) # mass term mass_id = "mass" discr_mass = pp.MassMatrix(model_data_adv) discr_mass_interface = pp.CellDofFaceDofMap(model_data_adv) discr_src = pp.ScalarSource(model_data_src) for g, d in gb: d[pp.PRIMARY_VARIABLES] = {variable: {"cells": 1}} if g.dim == gb.dim_max(): d[pp.DISCRETIZATION] = { variable: {adv_id: discr_adv, diff_id: discr_diff, mass_id: discr_mass, src_id: discr_src} } else: d[pp.DISCRETIZATION] = { variable: {adv_id: discr_adv_interface, diff_id: discr_diff_interface, mass_id: discr_mass_interface} } for e, d in gb.edges(): g_slave, g_master = gb.nodes_of_edge(e) d[pp.PRIMARY_VARIABLES] = {mortar_adv: {"cells": 1}, mortar_diff: {"cells": 1}} d[pp.COUPLING_DISCRETIZATION] = { adv_id: { g_slave: (variable, adv_id), g_master: (variable, adv_id), e: (mortar_adv, coupling_adv), }, diff_id: { g_slave: (variable, diff_id), g_master: (variable, diff_id), e: (mortar_diff, coupling_diff), }, } # setup the advection-diffusion problem assembler = pp.Assembler(gb, active_variables=[variable, mortar_diff, mortar_adv]) logger.info("Assemble the advective and diffusive terms of the transport problem") block_A, block_b = assembler.assemble_matrix_rhs(add_matrices=False) logger.info("done") # unpack the matrices just computed diff_name = diff_id + "_" + variable adv_name = adv_id + "_" + variable mass_name = mass_id + "_" + variable source_name = src_id + "_" + variable diff_coupling_name = diff_id + "_" + mortar_diff + "_" + variable + "_" + variable adv_coupling_name = adv_id + "_" + mortar_adv + "_" + variable + "_" + variable # need a sign for the convention of the conservation equation M = block_A[mass_name] A = block_A[diff_name] + block_A[diff_coupling_name] + \ block_A[adv_name] + block_A[adv_coupling_name] b = block_b[diff_name] + block_b[diff_coupling_name] + \ block_b[adv_name] + block_b[adv_coupling_name] + \ block_b[source_name] M_t = M.copy() / param["time_step"] * param.get("mass_weight", 1) M_r = M.copy() * param.get("reaction", 0) # Perform an LU factorization to speedup the solver IE_solver = sps.linalg.factorized((M_t + A + M_r).tocsc()) # time loop logger.info("Prepare the exporting") save = pp.Exporter(gb, "solution", folder=param["folder"]) logger.info("done") variables = [variable, param["pressure"], "frac_num", "cell_volumes"] if discr["scheme"] is pp.MVEM or discr["scheme"] is pp.RT0: variables.append(param["P0_flux"]) # assign the initial condition x = np.zeros(A.shape[0]) assembler.distribute_variable(x) for g, d in gb: if g.dim == gb.dim_max(): d[pp.STATE][variable] = param.get("init_trans", 0) * np.ones(g.num_cells) x = assembler.merge_variable(variable) outflow = np.zeros(param["n_steps"]) logger.info("Start the time loop with " + str(param["n_steps"]) + " steps") for i in np.arange(param["n_steps"]): logger.info("Solve the linear system for time step " + str(i)) x = IE_solver(b + M_t.dot(x)) logger.info("done") logger.info("Variable post-process") assembler.distribute_variable(x) logger.info("done") logger.info("Export variable") save.write_vtk(variables, time_step=i) logger.info("done") logger.info("Compute the production") outflow[i] = compute_outflow(gb, param) logger.info("done") time = np.arange(param["n_steps"]) * param["time_step"] save.write_pvd(time) logger.info("Save outflow on file") file_out = param["folder"] + "/outflow.csv" data = np.vstack((time, outflow)).T np.savetxt(file_out, data, delimiter=",") logger.info("done") logger.info("Save dof on file") file_out = param["folder"] + "/dof_transport.csv" np.savetxt(file_out, get_dof(assembler), delimiter=",", fmt="%d") logger.info("done")