def reconstruct_stress(self, previous_iterate: bool = False) -> None: """ Compute the stress in the highest-dimensional grid based on the displacement and pressure 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. """ # First the mechanical part of the stress super().reconstruct_stress(previous_iterate) g = self._nd_grid() d = self.gb.node_props(g) matrix_dictionary: Dict[str, sps.spmatrix] = d[pp.DISCRETIZATION_MATRICES][ self.mechanics_parameter_key ] mpsa = pp.Biot(self.mechanics_parameter_key) if previous_iterate: p = d[pp.STATE][pp.ITERATE][self.scalar_variable] else: p = d[pp.STATE][self.scalar_variable] # Stress contribution from the scalar variable d[pp.STATE]["stress"] += matrix_dictionary[mpsa.grad_p_matrix_key] * p
def setup_biot(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", "biot_alpha": 1, } keyword_mech = "mechanics" keyword_flow = "flow" data = pp.initialize_default_data(g, {}, keyword_mech, specified_parameters=specified_data) data = pp.initialize_default_data(g, data, keyword_flow) discr = pp.Biot() discr.discretize(g, data) div_u = data[pp.DISCRETIZATION_MATRICES][keyword_flow][ discr.div_u_matrix_key] bound_div_u = data[pp.DISCRETIZATION_MATRICES][keyword_flow][ discr.bound_div_u_matrix_key] stab = data[pp.DISCRETIZATION_MATRICES][keyword_flow][ discr.stabilization_matrix_key] grad_p = data[pp.DISCRETIZATION_MATRICES][keyword_mech][ discr.grad_p_matrix_key] bound_pressure = data[pp.DISCRETIZATION_MATRICES][keyword_mech][ discr.bound_pressure_matrix_key] return g, stiffness, bnd, div_u, bound_div_u, grad_p, stab, bound_pressure
def __init__(self, keyword: str, grids: Union[pp.Grid, List[pp.Grid]]) -> None: if isinstance(grids, list): self._grids = grids else: self._grids = [grids] self._discretization = pp.Biot(keyword) self._name = "BiotMpsa" 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 self.div_u: _MergedOperator self.bound_div_u: _MergedOperator self.grad_p: _MergedOperator self.stabilization: _MergedOperator self.bound_pressure: _MergedOperator _wrap_discretization( obj=self, discr=self._discretization, grids=grids, mat_dict_key=self.keyword )
def test_face_vector_to_scalar(self): # Test of function face_vector_to_scalar nf = 3 nd = 2 rows = np.array([0, 0, 1, 1, 2, 2]) cols = np.arange(6) vals = np.ones(6) known_matrix = sps.coo_matrix((vals, (rows, cols))).tocsr().toarray() a = pp.Biot()._face_vector_to_scalar(nf, nd).toarray() self.assertTrue(np.allclose(known_matrix, a))
def _discretize_biot(self) -> None: """ To save computational time, the full Biot equation (without contact mechanics) is discretized once. This is to avoid computing the same terms multiple times. """ g = self._nd_grid() d = self.gb.node_props(g) biot = pp.Biot( mechanics_keyword=self.mechanics_parameter_key, flow_keyword=self.scalar_parameter_key, vector_variable=self.displacement_variable, scalar_variable=self.scalar_variable, ) biot.discretize(g, d)
def _discretize_biot(self, update_after_geometry_change: bool = False) -> None: """ To save computational time, the full Biot equation (without contact mechanics) is discretized once. This is to avoid computing the same terms multiple times. """ g = self._nd_grid() d = self.gb.node_props(g) biot = pp.Biot( mechanics_keyword=self.mechanics_parameter_key, flow_keyword=self.scalar_parameter_key, vector_variable=self.displacement_variable, scalar_variable=self.scalar_variable, ) if update_after_geometry_change: # This is primary indented for rediscretization after fracture propagation. biot.update_discretization(g, d) else: biot.discretize(g, d)
def test_no_dynamics_2d(self): g_list = setup_grids.setup_2d() kw_f = "flow" kw_m = "mechanics" discr = pp.Biot() for g in g_list: bound_mech, bound_flow = self.make_boundary_conditions(g) mu = np.ones(g.num_cells) c = pp.FourthOrderTensor(mu, mu) k = pp.SecondOrderTensor(np.ones(g.num_cells)) bound_val = np.zeros(g.num_faces) aperture = np.ones(g.num_cells) param = pp.Parameters(g, [kw_f, kw_m], [{}, {}]) param[kw_f]["bc"] = bound_flow param[kw_m]["bc"] = bound_mech param[kw_f]["aperture"] = aperture param[kw_m]["aperture"] = aperture param[kw_f]["second_order_tensor"] = k # permeability / viscosity param[kw_m]["fourth_order_tensor"] = c param[kw_f]["bc_values"] = bound_val param[kw_m]["bc_values"] = np.tile(bound_val, g.dim) param[kw_f]["biot_alpha"] = 1 param[kw_m]["biot_alpha"] = 1 param[kw_f]["time_step"] = 1 param[kw_f]["mass_weight"] = 0 # fluid compressibility * porosity param[kw_m]["inverter"] = "python" data = {pp.PARAMETERS: param} data[pp.DISCRETIZATION_MATRICES] = {kw_f: {}, kw_m: {}} discr.discretize(g, data) A, b = discr.assemble_matrix_rhs(g, data) sol = np.linalg.solve(A.todense(), b) self.assertTrue( np.isclose(sol, np.zeros(g.num_cells * (g.dim + 1))).all())
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) nd = g.dim nf = g.num_faces nc = g.num_cells grad_p = sps.csr_matrix((nf * nd, nc)) div_u = sps.csr_matrix((nc, nc * nd)) bound_div_u = sps.csr_matrix((nc, nf * nd)) stab = sps.csr_matrix((nc, nc)) bound_displacement_pressure = sps.csr_matrix((nf * nd, nc)) faces_covered = np.zeros(g.num_faces, np.bool) cells_covered = np.zeros(g.num_cells, np.bool) bnd = pp.BoundaryConditionVectorial(g) specified_data = { "fourth_order_tensor": stiffness, "bc": bnd, "inverter": "python", "biot_alpha": 1, } keyword_mech = "mechanics" keyword_flow = "flow" data = pp.initialize_default_data(g, {}, keyword_mech, specified_parameters=specified_data) data = pp.initialize_default_data(g, data, keyword_flow) discr = pp.Biot() discr.discretize(g, data) div_u_full = data[pp.DISCRETIZATION_MATRICES][keyword_flow][ discr.div_u_matrix_key] bound_div_u_full = data[pp.DISCRETIZATION_MATRICES][keyword_flow][ discr.bound_div_u_matrix_key] stab_full = data[pp.DISCRETIZATION_MATRICES][keyword_flow][ discr.stabilization_matrix_key] grad_p_full = data[pp.DISCRETIZATION_MATRICES][keyword_mech][ discr.grad_p_matrix_key] bound_pressure_full = data[pp.DISCRETIZATION_MATRICES][keyword_mech][ discr.bound_pressure_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_mech]["specified_nodes"] = nodes discr.discretize(g, data) partial_div_u = data[pp.DISCRETIZATION_MATRICES][keyword_flow][ discr.div_u_matrix_key] partial_bound_div_u = data[pp.DISCRETIZATION_MATRICES][ keyword_flow][discr.bound_div_u_matrix_key] partial_grad_p = data[pp.DISCRETIZATION_MATRICES][keyword_mech][ discr.grad_p_matrix_key] partial_stab = data[pp.DISCRETIZATION_MATRICES][keyword_flow][ discr.stabilization_matrix_key] partial_bound_pressure = data[pp.DISCRETIZATION_MATRICES][ keyword_mech][discr.bound_pressure_matrix_key] active_faces = data[pp.PARAMETERS][keyword_mech]["active_faces"] if np.any(faces_covered): del_faces = self.expand_indices_nd( np.where(faces_covered)[0], g.dim) del_cells = np.where(cells_covered)[0] pp.fvutils.remove_nonlocal_contribution( del_cells, 1, partial_div_u, partial_bound_div_u, partial_stab) # del_faces is already expanded, set dimension to 1 pp.fvutils.remove_nonlocal_contribution( del_faces, 1, partial_grad_p, partial_bound_pressure) faces_covered[active_faces] = True cells_covered[ci] = True div_u += partial_div_u bound_div_u += partial_bound_div_u grad_p += partial_grad_p stab += partial_stab bound_displacement_pressure += partial_bound_pressure self.assertTrue((div_u_full - div_u).max() < 1e-8) self.assertTrue((bound_div_u_full - bound_div_u).min() > -1e-8) self.assertTrue((grad_p_full - grad_p).max() < 1e-8) self.assertTrue((stab_full - stab).min() > -1e-8) self.assertTrue( (bound_displacement_pressure - bound_pressure_full).min() > -1e-8)
def test_bound_cell_node_keyword(self): # Compute update for a single cell on the ( g, stiffness, bnd, div_u, bound_div_u, grad_p, stab, bound_pressure, ) = self.setup_biot() 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]), "biot_alpha": 1, } keyword_mech = "mechanics" keyword_flow = "flow" data = pp.initialize_default_data(g, {}, keyword_mech, specified_parameters=specified_data) data = pp.initialize_default_data(g, data, keyword_flow) discr = pp.Biot() discr.discretize(g, data) partial_div_u = data[pp.DISCRETIZATION_MATRICES][keyword_flow][ discr.div_u_matrix_key] partial_bound_div_u = data[pp.DISCRETIZATION_MATRICES][keyword_flow][ discr.bound_div_u_matrix_key] partial_grad_p = data[pp.DISCRETIZATION_MATRICES][keyword_mech][ discr.grad_p_matrix_key] partial_stab = data[pp.DISCRETIZATION_MATRICES][keyword_flow][ discr.stabilization_matrix_key] partial_bound_pressure = data[pp.DISCRETIZATION_MATRICES][ keyword_mech][discr.bound_pressure_matrix_key] active_faces = data[pp.PARAMETERS][keyword_mech]["active_faces"] self.assertTrue(faces_of_cell.size == active_faces.size) self.assertTrue( np.all(np.sort(faces_of_cell) == np.sort(active_faces))) diff_div_u = (div_u - partial_div_u).todense() diff_bound_div_u = (bound_div_u - partial_bound_div_u).todense() diff_grad_p = (grad_p - partial_grad_p).todense() diff_stab = (stab - partial_stab).todense() diff_bound_pressure = (bound_pressure - partial_bound_pressure).todense() faces_of_cell_vec = self.expand_indices_nd(faces_of_cell, g.dim) self.assertTrue(np.max(np.abs(diff_div_u[inner_cell])) == 0) self.assertTrue(np.max(np.abs(diff_bound_div_u[inner_cell])) == 0) self.assertTrue(np.max(np.abs(diff_grad_p[faces_of_cell_vec])) == 0) self.assertTrue(np.max(np.abs(diff_stab[inner_cell])) == 0) self.assertTrue( np.max(np.abs(diff_bound_pressure[faces_of_cell_vec])) == 0) # Only the faces of the central cell should be zero pp.fvutils.remove_nonlocal_contribution(inner_cell, 1, partial_div_u, partial_bound_div_u, partial_stab) pp.fvutils.remove_nonlocal_contribution(faces_of_cell, g.dim, partial_grad_p, partial_bound_pressure) self.assertTrue(np.max(np.abs(partial_div_u.data)) == 0) self.assertTrue(np.max(np.abs(partial_bound_div_u.data)) == 0) self.assertTrue(np.max(np.abs(partial_grad_p.data)) == 0) self.assertTrue(np.max(np.abs(partial_stab.data)) == 0) self.assertTrue(np.max(np.abs(partial_bound_pressure.data)) == 0)
def biot_convergence_in_space(N): # coding: utf-8 # ### Source terms and analytical solutions # In[330]: def source_flow(g, tau): x1 = g.cell_centers[0] x2 = g.cell_centers[1] f_flow = tau*(2*np.sin(2*np.pi*x2) - \ 4*x1*np.pi**2*np.sin(2*np.pi*x2)*(x1 - 1)) - \ x1*np.sin(2*np.pi*x2) - \ np.sin(2*np.pi*x2)*(x1 - 1) + \ 2*np.pi*np.cos(2*np.pi*x2)*np.sin(2*np.pi*x1) return f_flow def source_mechanics(g): x1 = g.cell_centers[0] x2 = g.cell_centers[1] f_mech = np.zeros(g.num_cells * g.dim) f_mech[::2] = 6*np.sin(2*np.pi*x2) - \ x1*np.sin(2*np.pi*x2) - \ np.sin(2*np.pi*x2)*(x1 - 1) - \ 8*np.pi**2*np.cos(2*np.pi*x1)*np.cos(2*np.pi*x2) - \ 4*x1*np.pi**2*np.sin(2*np.pi*x2)*(x1 - 1) f_mech[1::2] = 4*np.pi*np.cos(2*np.pi*x2)*(x1 - 1) + \ 16*np.pi**2*np.sin(2*np.pi*x1)*np.sin(2*np.pi*x2) + \ 4*x1*np.pi*np.cos(2*np.pi*x2) - \ 2*x1*np.pi*np.cos(2*np.pi*x2)*(x1 - 1) return f_mech def analytical(g): sol = dict() x1 = g.cell_centers[0] x2 = g.cell_centers[1] sol['u'] = np.zeros(g.num_cells * g.dim) sol['u'][::2] = x1 * (1 - x1) * np.sin(2 * np.pi * x2) sol['u'][1::2] = np.sin(2 * np.pi * x1) * np.sin(2 * np.pi * x2) sol['p'] = sol['u'][::2] return sol # ### Getting mechanics boundary conditions # In[331]: def get_bc_mechanics(g, b_faces, x_min, x_max, west, east, y_min, y_max, south, north): # Setting the tags at each boundary side for the mechanics problem labels_mech = np.array([None] * b_faces.size) labels_mech[west] = 'dir' labels_mech[east] = 'dir' labels_mech[south] = 'dir' labels_mech[north] = 'dir' # Constructing the bc object for the mechanics problem bc_mech = pp.BoundaryConditionVectorial(g, b_faces, labels_mech) # Constructing the boundary values array for the mechanics problem bc_val_mech = np.zeros(g.num_faces * g.dim) return bc_mech, bc_val_mech # ### Getting flow boundary conditions # In[332]: def get_bc_flow(g, b_faces, x_min, x_max, west, east, y_min, y_max, south, north): # Setting the tags at each boundary side for the mechanics problem labels_flow = np.array([None] * b_faces.size) labels_flow[west] = 'dir' labels_flow[east] = 'dir' labels_flow[south] = 'dir' labels_flow[north] = 'dir' # Constructing the bc object for the flow problem bc_flow = pp.BoundaryCondition(g, b_faces, labels_flow) # Constructing the boundary values array for the flow problem bc_val_flow = np.zeros(g.num_faces) return bc_flow, bc_val_flow # ### Setting up the grid # In[333]: Nx = Ny = N Lx = 1 Ly = 1 g = pp.CartGrid([Nx, Ny], [Lx, Ly]) g.compute_geometry() V = g.cell_volumes # ### Physical parameters # In[334]: # Skeleton parameters mu_s = 1 # [Pa] Shear modulus lambda_s = 1 # [Pa] Lame parameter K_s = (2 / 3) * mu_s + lambda_s # [Pa] Bulk modulus E_s = mu_s * ((9 * K_s) / (3 * K_s + mu_s)) # [Pa] Young's modulus nu_s = (3 * K_s - 2 * mu_s) / (2 * (3 * K_s + mu_s) ) # [-] Poisson's coefficient k_s = 1 # [m^2] Permeabiliy # Fluid parameters mu_f = 1 # [Pa s] Dynamic viscosity # Porous medium parameters alpha_biot = 1. # [m^2] Intrinsic permeability S_m = 0 # [1/Pa] Specific Storage # ### Creating second and fourth order tensors # In[335]: # Permeability tensor perm = pp.SecondOrderTensor(g.dim, k_s * np.ones(g.num_cells)) # Stiffness matrix constit = pp.FourthOrderTensor(g.dim, mu_s * np.ones(g.num_cells), lambda_s * np.ones(g.num_cells)) # ### Time parameters # In[336]: t0 = 0 # [s] Initial time tf = 1 # [s] Final simulation time tLevels = 1 # [-] Time levels times = np.linspace(t0, tf, tLevels + 1) # [s] Vector of time evaluations dt = np.diff(times) # [s] Vector of time steps # ### Boundary conditions pre-processing # In[337]: b_faces = g.tags['domain_boundary_faces'].nonzero()[0] # Extracting indices of boundary faces w.r.t g x_min = b_faces[g.face_centers[0, b_faces] < 0.0001] x_max = b_faces[g.face_centers[0, b_faces] > 0.9999 * Lx] y_min = b_faces[g.face_centers[1, b_faces] < 0.0001] y_max = b_faces[g.face_centers[1, b_faces] > 0.9999 * Ly] # Extracting indices of boundary faces w.r.t b_faces west = np.in1d(b_faces, x_min).nonzero() east = np.in1d(b_faces, x_max).nonzero() south = np.in1d(b_faces, y_min).nonzero() north = np.in1d(b_faces, y_max).nonzero() # Mechanics boundary conditions bc_mech, bc_val_mech = get_bc_mechanics(g, b_faces, x_min, x_max, west, east, y_min, y_max, south, north) # FLOW BOUNDARY CONDITIONS bc_flow, bc_val_flow = get_bc_flow(g, b_faces, x_min, x_max, west, east, y_min, y_max, south, north) # ### Initialiazing solution and solver dicitionaries # In[338]: # Solution dictionary sol = dict() sol['time'] = np.zeros(tLevels + 1, dtype=float) sol['displacement'] = np.zeros((tLevels + 1, g.num_cells * g.dim), dtype=float) sol['displacement_faces'] = np.zeros( (tLevels + 1, g.num_faces * g.dim * 2), dtype=float) sol['pressure'] = np.zeros((tLevels + 1, g.num_cells), dtype=float) sol['traction'] = np.zeros((tLevels + 1, g.num_faces * g.dim), dtype=float) sol['flux'] = np.zeros((tLevels + 1, g.num_faces), dtype=float) sol['iter'] = np.array([], dtype=int) sol['time_step'] = np.array([], dtype=float) sol['residual'] = np.array([], dtype=float) # Solver dictionary newton_param = dict() newton_param['tol'] = 1E-8 # maximum tolerance newton_param['max_iter'] = 20 # maximum number of iterations newton_param['res_norm'] = 1000 # initializing residual newton_param['iter'] = 1 # iteration # ### Discrete operators and discrete equations # ### Flow operators # In[339]: F = lambda x: biot_F * x # Flux operator boundF = lambda x: biot_boundF * x # Bound Flux operator compat = lambda x: biot_compat * x # Compatibility operator (Stabilization term) divF = lambda x: biot_divF * x # Scalar divergence operator # ### Mechanics operators # In[340]: S = lambda x: biot_S * x # Stress operator boundS = lambda x: biot_boundS * x # Bound Stress operator divU = lambda x: biot_divU * x # Divergence of displacement field divS = lambda x: biot_divS * x # Vector divergence operator gradP = lambda x: biot_divS * biot_gradP * x # Pressure gradient operator boundDivU = lambda x: biot_boundDivU * x # Bound Divergence of displacement operator boundUCell = lambda x: biot_boundUCell * x # Contribution of displacement at cells -> Face displacement boundUFace = lambda x: biot_boundUFace * x # Contribution of bc_mech at the boundaries -> Face displacement boundUPressure = lambda x: biot_boundUPressure * x # Contribution of pressure at cells -> Face displacement # ### Discrete equations # In[341]: # Source terms f_mech = source_mechanics(g) f_flow = source_flow(g, dt[0]) # Generalized Hooke's law T = lambda u: S(u) + boundS(bc_val_mech) # Momentum conservation equation (I) u_eq1 = lambda u: divS(T(u)) # Momentum conservation equation (II) u_eq2 = lambda p: -gradP(p) + f_mech * V[0] # Darcy's law Q = lambda p: (1. / mu_f) * (F(p) + boundF(bc_val_flow)) # Mass conservation equation (I) p_eq1 = lambda u, u_n: alpha_biot * divU(u - u_n) # Mass conservation equation (II) p_eq2 = lambda p, p_n, dt: (p - p_n) * S_m * V + divF(Q( p)) * dt + alpha_biot * compat(p - p_n) * V[0] - (f_flow / dt) * V[0] # ## Creating AD variables # In[343]: # Create displacement AD-variable u_ad = Ad_array(np.zeros(g.num_cells * 2), sps.diags(np.ones(g.num_cells * g.dim))) # Create pressure AD-variable p_ad = Ad_array(np.zeros(g.num_cells), sps.diags(np.ones(g.num_cells))) # ## Performing discretization # In[344]: d = dict() # initialize dictionary to store data # Mechanics data object specified_parameters_mech = { "fourth_order_tensor": constit, "bc": bc_mech, "biot_alpha": 1., "bc_values": bc_val_mech } pp.initialize_default_data(g, d, "mechanics", specified_parameters_mech) # Flow data object specified_parameters_flow = { "second_order_tensor": perm, "bc": bc_flow, "biot_alpha": 1., "bc_values": bc_val_flow } pp.initialize_default_data(g, d, "flow", specified_parameters_flow) # Biot discretization solver_biot = pp.Biot("mechanics", "flow") solver_biot.discretize(g, d) # Mechanics discretization matrices biot_S = d['discretization_matrices']['mechanics']['stress'] biot_boundS = d['discretization_matrices']['mechanics']['bound_stress'] biot_divU = d['discretization_matrices']['mechanics']['div_d'] biot_gradP = d['discretization_matrices']['mechanics']['grad_p'] biot_boundDivU = d['discretization_matrices']['mechanics']['bound_div_d'] biot_boundUCell = d['discretization_matrices']['mechanics'][ 'bound_displacement_cell'] biot_boundUFace = d['discretization_matrices']['mechanics'][ 'bound_displacement_face'] biot_boundUPressure = d['discretization_matrices']['mechanics'][ 'bound_displacement_pressure'] biot_divS = pp.fvutils.vector_divergence(g) # Flow discretization matrices biot_F = d['discretization_matrices']['flow']['flux'] biot_boundF = d['discretization_matrices']['flow']['bound_flux'] biot_compat = d['discretization_matrices']['flow']['biot_stabilization'] biot_divF = pp.fvutils.scalar_divergence(g) # Saving initial condition sol['pressure'][0] = p_ad.val sol['displacement'][0] = u_ad.val sol['displacement_faces'][0] = (boundUCell(sol['displacement'][0]) + boundUFace(bc_val_mech) + boundUPressure(sol['pressure'][0])) sol['time'][0] = times[0] sol['traction'][0] = T(u_ad.val) sol['flux'][0] = Q(p_ad.val) # ## The time loop # In[345]: tt = 0 # time counter while times[tt] < times[-1]: tt += 1 # increasing time counter # Displacement and pressure at the previous time step u_n = u_ad.val.copy() p_n = p_ad.val.copy() # Updating residual and iteration at each time step newton_param.update({'res_norm': 1000, 'iter': 1}) # Newton loop while newton_param['res_norm'] > newton_param['tol'] and newton_param[ 'iter'] <= newton_param['max_iter']: # Calling equations eq1 = u_eq1(u_ad) eq2 = u_eq2(p_ad) eq3 = p_eq1(u_ad, u_n) eq4 = p_eq2(p_ad, p_n, dt[tt - 1]) # Assembling Jacobian of the coupled system J_mech = np.hstack( (eq1.jac, eq2.jac)) # Jacobian blocks (mechanics) J_flow = np.hstack((eq3.jac, eq4.jac)) # Jacobian blocks (flow) J = sps.bmat(np.vstack((J_mech, J_flow)), format='csc') # Jacobian (coupled) # Determining residual of the coupled system R_mech = eq1.val + eq2.val # Residual (mechanics) R_flow = eq3.val + eq4.val # Residual (flow) R = np.hstack((R_mech, R_flow)) # Residual (coupled) y = sps.linalg.spsolve(J, -R) # u_ad.val = u_ad.val + y[:g.dim * g.num_cells] # Newton update p_ad.val = p_ad.val + y[g.dim * g.num_cells:] # newton_param['res_norm'] = np.linalg.norm(R) # Updating residual if newton_param['res_norm'] <= newton_param[ 'tol'] and newton_param['iter'] <= newton_param['max_iter']: print('Iter: {} \t Error: {:.8f} [m]'.format( newton_param['iter'], newton_param['res_norm'])) elif newton_param['iter'] > newton_param['max_iter']: print('Error: Newton method did not converge!') else: newton_param['iter'] += 1 # Saving variables sol['iter'] = np.concatenate( (sol['iter'], np.array([newton_param['iter']]))) sol['residual'] = np.concatenate( (sol['residual'], np.array([newton_param['res_norm']]))) sol['time_step'] = np.concatenate((sol['time_step'], dt)) sol['pressure'][tt] = p_ad.val sol['displacement'][tt] = u_ad.val sol['displacement_faces'][tt] = (boundUCell(sol['displacement'][tt]) + boundUFace(bc_val_mech) + boundUPressure(sol['pressure'][tt])) sol['time'][tt] = times[tt] sol['traction'][tt] = T(u_ad.val) sol['flux'][tt] = Q(p_ad.val) # Determining analytical solution sol_anal = analytical(g) # Determining norms p_norm = np.linalg.norm(sol_anal['p'] - sol['pressure'][-1]) / ( np.linalg.norm(sol['pressure'][-1])) u_mag_num = np.sqrt(sol['displacement'][-1][::2]**2 + sol['displacement'][-1][1::2]**2) u_mag_ana = np.sqrt(sol_anal['u'][::2]**2 + sol_anal['u'][1::2]**2) u_norm = np.linalg.norm(u_mag_ana - u_mag_num) / np.linalg.norm(u_mag_num) return p_norm, u_norm
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 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)))
# 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] g_1, g_2 = gb.grids_of_dimension(1)
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))))
def test_biot(self): self.setup() g, g_larger = self.g, self.g_larger specified_data = {"inverter": "python", "biot_alpha": 1} mechanics_keyword = "mechanics" flow_keyword = "flow" data_small = pp.initialize_default_data( g, {}, mechanics_keyword, specified_parameters=specified_data) def add_flow_data(g, d): d[pp.DISCRETIZATION_MATRICES][flow_keyword] = {} d[pp.PARAMETERS][flow_keyword] = { "bc": pp.BoundaryCondition(g), "second_order_tensor": pp.SecondOrderTensor(np.ones(g.num_cells)), "bc_values": np.zeros(g.num_faces), "inverter": "python", "mass_weight": np.ones(g.num_cells), "biot_alpha": 1, } discr = pp.Biot(mechanics_keyword=mechanics_keyword, flow_keyword=flow_keyword) add_flow_data(g, data_small) discr.discretize(g, data_small) # Discretization on a small problem # 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, "biot_alpha": 1 } # Do a full discretization on the larger grid data_full = pp.initialize_default_data( g_larger, {}, mechanics_keyword, specified_parameters=specified_data_larger) add_flow_data(g_larger, data_full) 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, {}, mechanics_keyword, specified_parameters=specified_data_larger) add_flow_data(g_larger, data_partial) data_partial["update_discretization"] = updates self._update_and_compare( data_small, data_partial, data_full, g_larger, keywords=[flow_keyword, mechanics_keyword], discr=discr, )
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
def conv_test(n): # Analytical solution def mandel_solution(g, Nx, Ny, times, F, B, nu_u, nu, c_f, mu_s): # Some needed parameters a = np.max(g.face_centers[0]) # a = Lx x_cntr = g.cell_centers[0][:Nx] # [m] vector of x-centers y_cntr = g.cell_centers[1][::Nx] # [m] vector of y-centers # Solutions to tan(x) - ((1-nu)/(nu_u-nu)) x = 0 """ This is somehow tricky, we have to solve the equation numerically in order to find all the positive solutions to the equation. Later we will use them to compute the infinite sums. Experience has shown that 200 roots are more than enough to achieve accurate results. Note that we find the roots using the bisection method. """ f = lambda x: np.tan(x) - ( (1 - nu) / (nu_u - nu)) * x # define the algebraic eq. as a lambda function n_series = 200 # number of roots a_n = np.zeros(n_series) # initializing roots array x0 = 0 # initial point for i in range(0, len(a_n)): a_n[i] = opt.bisect( f, # function x0 + np.pi / 4, # left point x0 + np.pi / 2 - 10000 * 2.2204e-16, # right point (a tiny bit less than pi/2) xtol=1e-30, # absolute tolerance rtol=1e-15 # relative tolerance ) x0 += np.pi # apply a phase change of pi to get the next root # Creating dictionary to store analytical solutions mandel_sol = dict() mandel_sol['p'] = np.zeros((len(times), len(x_cntr))) mandel_sol['u_x'] = np.zeros((len(times), len(x_cntr))) mandel_sol['u_y'] = np.zeros((len(times), len(y_cntr))) mandel_sol['sigma_yy'] = np.zeros((len(times), len(x_cntr))) # Terms needed to compute the solutions (these are constants) p0 = (2 * F * B * (1 + nu_u)) / (3 * a) ux0_1 = ((F * nu) / (2 * mu_s * a)) ux0_2 = -((F * nu_u) / (mu_s * a)) ux0_3 = F / mu_s uy0_1 = (-F * (1 - nu)) / (2 * mu_s * a) uy0_2 = (F * (1 - nu_u) / (mu_s * a)) sigma0_1 = -F / a sigma0_2 = (-2 * F * B * (nu_u - nu)) / (a * (1 - nu)) sigma0_3 = (2 * F) / a # Saving solutions for the initial conditions mandel_sol['p'][0] = ((F * B * (1 + nu_u)) / (3 * a)) * np.ones(Nx) mandel_sol['u_x'][0] = (F * nu_u * x_cntr) / (2 * mu_s * a) mandel_sol['u_y'][0] = ((-F * (1 - nu_u)) / (2 * mu_s * a)) * y_cntr # Storing solutions for the subsequent time steps for ii in range(1, len(times)): # Analytical Pressures p_sum = 0 for n in range(len(a_n)): p_sum += (((np.sin(a_n[n])) / (a_n[n] - (np.sin(a_n[n]) * np.cos(a_n[n])))) * (np.cos((a_n[n] * x_cntr) / a) - np.cos(a_n[n])) * np.exp((-(a_n[n]**2) * c_f * times[ii]) / (a**2))) mandel_sol['p'][ii] = p0 * p_sum # Analytical horizontal displacements ux_sum1 = 0 ux_sum2 = 0 for n in range(len(a_n)): ux_sum1 += ((np.sin(a_n[n]) * np.cos(a_n[n])) / (a_n[n] - np.sin(a_n[n]) * np.cos(a_n[n])) * np.exp((-(a_n[n]**2) * c_f * times[ii]) / (a**2))) ux_sum2 += ((np.cos(a_n[n]) / (a_n[n] - (np.sin(a_n[n]) * np.cos(a_n[n])))) * np.sin(a_n[n] * (x_cntr / a)) * np.exp( (-(a_n[n]**2) * c_f * times[ii]) / (a**2))) mandel_sol['u_x'][ii] = ( ux0_1 + ux0_2 * ux_sum1) * x_cntr + ux0_3 * ux_sum2 # Analytical vertical displacements uy_sum = 0 for n in range(len(a_n)): uy_sum += (((np.sin(a_n[n]) * np.cos(a_n[n])) / (a_n[n] - np.sin(a_n[n]) * np.cos(a_n[n]))) * np.exp((-(a_n[n]**2) * c_f * times[ii]) / (a**2))) mandel_sol['u_y'][ii] = (uy0_1 + uy0_2 * uy_sum) * y_cntr # Analitical vertical stress sigma_sum1 = 0 sigma_sum2 = 0 for n in range(len(a_n)): sigma_sum1 += (((np.sin(a_n[n])) / (a_n[n] - (np.sin(a_n[n]) * np.cos(a_n[n])))) * np.cos(a_n[n] * (x_cntr / a)) * np.exp( (-(a_n[n]**2) * c_f * times[ii]) / (a**2))) sigma_sum2 += (((np.sin(a_n[n]) * np.cos(a_n[n])) / (a_n[n] - np.sin(a_n[n]) * np.cos(a_n[n]))) * np.exp( (-(a_n[n]**2) * c_f * times[ii]) / (a**2))) mandel_sol['sigma_yy'][ii] = (sigma0_1 + sigma0_2 * sigma_sum1) + ( sigma0_3 * sigma_sum2) return mandel_sol # Computing the initial condition def get_mandel_init_cond(g, F, B, nu_u, mu_s): # Initialing pressure and displacement arrays p0 = np.zeros(g.num_cells) u0 = np.zeros(g.num_cells * 2) # Some needed parameters a = np.max(g.face_centers[0]) # a = Lx p0 = ((F * B * (1 + nu_u)) / (3 * a)) * np.ones(g.num_cells) u0[::2] = (F * nu_u * g.cell_centers[0]) / (2 * mu_s * a) u0[1::2] = ((-F * (1 - nu_u)) / (2 * mu_s * a)) * g.cell_centers[1] return p0, u0 # Getting the time-dependent boundary condition def get_mandel_bc(g, y_max, times, F, B, nu_u, nu, c_f, mu_s): # Initializing top boundary array u_top = np.zeros((len(times), len(y_max))) # Some needed parameters a = np.max(g.face_centers[0]) # a = Lx b = np.max(g.face_centers[1]) # b = Ly y_top = g.face_centers[1][ y_max] # [m] y-coordinates at the top boundary # Solutions to tan(x) - ((1-nu)/(nu_u-nu)) x = 0 """ This is somehow tricky, we have to solve the equation numerically in order to find all the positive solutions to the equation. Later we will use them to compute the infinite sums. Experience has shown that 200 roots are more than enough to achieve accurate results. Note that we find the roots using the bisection method. """ f = lambda x: np.tan(x) - ( (1 - nu) / (nu_u - nu)) * x # define the algebraic eq. as a lambda function n_series = 200 # number of roots a_n = np.zeros(n_series) # initializing roots array x0 = 0 # initial point for i in range(0, len(a_n)): a_n[i] = opt.bisect( f, # function x0 + np.pi / 4, # left point x0 + np.pi / 2 - 10000 * 2.2204e-16, # right point (a tiny bit less than pi/2) xtol=1e-30, # absolute tolerance rtol=1e-15 # relative tolerance ) x0 += np.pi # apply a phase change of pi to get the next root # Terms needed to compute the solutions (these are constants) uy0_1 = (-F * (1 - nu)) / (2 * mu_s * a) uy0_2 = (F * (1 - nu_u) / (mu_s * a)) # For the initial condition: u_top[0] = ((-F * (1 - nu_u)) / (2 * mu_s * a)) * b for i in range(1, len(times)): # Analytical vertical displacements at the top boundary uy_sum = 0 for n in range(len(a_n)): uy_sum += (((np.sin(a_n[n]) * np.cos(a_n[n])) / (a_n[n] - np.sin(a_n[n]) * np.cos(a_n[n]))) * np.exp((-(a_n[n]**2) * c_f * times[i]) / (a**2))) u_top[i] = (uy0_1 + uy0_2 * uy_sum) * y_top # Returning array of u_y at the top boundary return u_top # Getting mechanics boundary conditions def get_bc_mechanics(g, u_top, times, b_faces, x_min, x_max, west, east, y_min, y_max, south, north): # Setting the tags at each boundary side for the mechanics problem labels_mech = np.array([None] * b_faces.size) labels_mech[west] = 'dir_x' # roller labels_mech[east] = 'neu' # traction free labels_mech[south] = 'dir_y' # roller labels_mech[ north] = 'dir_y' # roller (with non-zero displacement in the vertical direction) # Constructing the bc object for the mechanics problem bc_mech = pp.BoundaryConditionVectorial(g, b_faces, labels_mech) # Constructing the boundary values array for the mechanics problem bc_val_mech = np.zeros(( len(times), g.num_faces * g.dim, )) for i in range(len(times)): # West side boundary conditions (mech) bc_val_mech[i][2 * x_min] = 0 # [m] bc_val_mech[i][2 * x_min + 1] = 0 # [Pa] # East side boundary conditions (mech) bc_val_mech[i][2 * x_max] = 0 # [Pa] bc_val_mech[i][2 * x_max + 1] = 0 # [Pa] # South Side boundary conditions (mech) bc_val_mech[i][2 * y_min] = 0 # [Pa] bc_val_mech[i][2 * y_min + 1] = 0 # [m] # North Side boundary conditions (mech) bc_val_mech[i][2 * y_max] = 0 # [Pa] bc_val_mech[i][2 * y_max + 1] = u_top[i] # [m] return bc_mech, bc_val_mech # Getting flow boundary conditions def get_bc_flow(g, b_faces, x_min, x_max, west, east, y_min, y_max, south, north): # Setting the tags at each boundary side for the mechanics problem labels_flow = np.array([None] * b_faces.size) labels_flow[west] = 'neu' # no flow labels_flow[east] = 'dir' # constant pressure labels_flow[south] = 'neu' # no flow labels_flow[north] = 'neu' # no flow # Constructing the bc object for the flow problem bc_flow = pp.BoundaryCondition(g, b_faces, labels_flow) # Constructing the boundary values array for the flow problem bc_val_flow = np.zeros(g.num_faces) # West side boundary condition (flow) bc_val_flow[x_min] = 0 # [Pa] # East side boundary condition (flow) bc_val_flow[x_max] = 0 # [m^3/s] # South side boundary condition (flow) bc_val_flow[y_min] = 0 # [m^3/s] # North side boundary condition (flow) bc_val_flow[y_max] = 0 # [m^3/s] return bc_flow, bc_val_flow # ## Setting up the grid # In[7]: Nx = 40 Ny = 40 Lx = 100 Ly = 10 g = pp.CartGrid([Nx, Ny], [Lx, Ly]) #g.nodes = g.nodes + 1E-7*np.random.randn(3, g.num_nodes) g.compute_geometry() V = g.cell_volumes # Physical parameters # Skeleton parameters mu_s = 2.475E+09 # [Pa] Shear modulus lambda_s = 1.65E+09 # [Pa] Lame parameter K_s = (2 / 3) * mu_s + lambda_s # [Pa] Bulk modulus E_s = mu_s * ((9 * K_s) / (3 * K_s + mu_s)) # [Pa] Young's modulus nu_s = (3 * K_s - 2 * mu_s) / (2 * (3 * K_s + mu_s) ) # [-] Poisson's coefficient k_s = 100 * 9.869233E-13 # [m^2] Permeabiliy # Fluid parameters mu_f = 10.0E-3 # [Pa s] Dynamic viscosity # Porous medium parameters alpha_biot = 1. # [m^2] Intrinsic permeability S_m = 6.0606E-11 # [1/Pa] Specific Storage K_u = K_s + (alpha_biot**2) / S_m # [Pa] Undrained bulk modulus B = alpha_biot / (S_m * K_u) # [-] Skempton's coefficient nu_u = (3 * nu_s + B * (1 - 2 * nu_s)) / (3 - B * (1 - 2 * nu_s)) # [-] Undrained Poisson's ratio c_f = (2 * k_s * (B**2) * mu_s * (1 - nu_s) * (1 + nu_u)**2) / (9 * mu_f * (1 - nu_u) * (nu_u - nu_s)) # [m^2/s] Fluid diffusivity # Creating second and fourth order tensors # Permeability tensor perm = pp.SecondOrderTensor(g.dim, k_s * np.ones(g.num_cells)) # Stiffness matrix constit = pp.FourthOrderTensor(g.dim, mu_s * np.ones(g.num_cells), lambda_s * np.ones(g.num_cells)) # Time parameters t0 = 0 # [s] Initial time tf = 100 # [s] Final simulation time tLevels = 100 # [-] Time levels times = np.linspace(t0, tf, tLevels + 1) # [s] Vector of time evaluations dt = np.diff(times) # [s] Vector of time steps # Boundary conditions pre-processing b_faces = g.tags['domain_boundary_faces'].nonzero()[0] # Extracting indices of boundary faces w.r.t g x_min = b_faces[g.face_centers[0, b_faces] < 0.0001] x_max = b_faces[g.face_centers[0, b_faces] > 0.9999 * Lx] y_min = b_faces[g.face_centers[1, b_faces] < 0.0001] y_max = b_faces[g.face_centers[1, b_faces] > 0.9999 * Ly] # Extracting indices of boundary faces w.r.t b_faces west = np.in1d(b_faces, x_min).nonzero() east = np.in1d(b_faces, x_max).nonzero() south = np.in1d(b_faces, y_min).nonzero() north = np.in1d(b_faces, y_max).nonzero() # Applied load and top boundary condition F_load = 6.8E+8 # [N/m] Applied load u_top = get_mandel_bc(g, y_max, times, F_load, B, nu_u, nu_s, c_f, mu_s) # [m] Vector of imposed vertical displacements # MECHANICS BOUNDARY CONDITIONS bc_mech, bc_val_mech = get_bc_mechanics(g, u_top, times, b_faces, x_min, x_max, west, east, y_min, y_max, south, north) # FLOW BOUNDARY CONDITIONS bc_flow, bc_val_flow = get_bc_flow(g, b_faces, x_min, x_max, west, east, y_min, y_max, south, north) # Initialiazing solution and solver dicitionaries # Solution dictionary sol = dict() sol['time'] = np.zeros(tLevels + 1, dtype=float) sol['displacement'] = np.zeros((tLevels + 1, g.num_cells * g.dim), dtype=float) sol['displacement_faces'] = np.zeros( (tLevels + 1, g.num_faces * g.dim * 2), dtype=float) sol['pressure'] = np.zeros((tLevels + 1, g.num_cells), dtype=float) sol['traction'] = np.zeros((tLevels + 1, g.num_faces * g.dim), dtype=float) sol['flux'] = np.zeros((tLevels + 1, g.num_faces), dtype=float) sol['iter'] = np.array([], dtype=int) sol['time_step'] = np.array([], dtype=float) sol['residual'] = np.array([], dtype=float) # Solver dictionary newton_param = dict() newton_param['tol'] = 1E-6 # maximum tolerance newton_param['max_iter'] = 20 # maximum number of iterations newton_param['res_norm'] = 1000 # initializing residual newton_param['iter'] = 1 # iteration # Discrete operators and discrete equations # Flow operators F = lambda x: biot_F * x # Flux operator boundF = lambda x: biot_boundF * x # Bound Flux operator compat = lambda x: biot_compat * x # Compatibility operator (Stabilization term) divF = lambda x: biot_divF * x # Scalar divergence operator # Mechanics operators S = lambda x: biot_S * x # Stress operator boundS = lambda x: biot_boundS * x # Bound Stress operator divU = lambda x: biot_divU * x # Divergence of displacement field divS = lambda x: biot_divS * x # Vector divergence operator gradP = lambda x: biot_divS * biot_gradP * x # Pressure gradient operator boundDivU = lambda x: biot_boundDivU * x # Bound Divergence of displacement operator boundUCell = lambda x: biot_boundUCell * x # Contribution of displacement at cells -> Face displacement boundUFace = lambda x: biot_boundUFace * x # Contribution of bc_mech at the boundaries -> Face displacement boundUPressure = lambda x: biot_boundUPressure * x # Contribution of pressure at cells -> Face displacement # Discrete equations # Generalized Hooke's law T = lambda u, bc_val_mech: S(u) + boundS(bc_val_mech) # Momentum conservation equation (I) u_eq1 = lambda u, bc_val_mech: divS(T(u, bc_val_mech)) # Momentum conservation equation (II) u_eq2 = lambda p: -gradP(p) # Darcy's law Q = lambda p: (1. / mu_f) * (F(p) + boundF(bc_val_flow)) # Mass conservation equation (I) p_eq1 = lambda u, u_n, bc_val_mech, bc_val_mech_n: alpha_biot * (divU( u - u_n) + boundDivU(bc_val_mech - bc_val_mech_n)) # Mass conservation equation (II) p_eq2 = lambda p, p_n, dt: (p - p_n) * S_m * V + divF(Q( p)) * dt + alpha_biot * compat(p - p_n) # Creating AD variables # Retrieve initial conditions p_init, u_init = get_mandel_init_cond(g, F_load, B, nu_u, mu_s) # Create displacement AD-variable u_ad = Ad_array(u_init.copy(), sps.diags(np.ones(g.num_cells * g.dim))) # Create pressure AD-variable p_ad = Ad_array(p_init.copy(), sps.diags(np.ones(g.num_cells))) # The time loop tt = 0 # time counter while times[tt] < times[-1]: ################################ # Initializing data dictionary # ################################ d = dict() # initialize dictionary to store data ################################ # Creating the data objects # ################################ # Mechanics data object specified_parameters_mech = { "fourth_order_tensor": constit, "bc": bc_mech, "biot_alpha": 1., "bc_values": bc_val_mech[tt], "mass_weight": S_m } pp.initialize_default_data(g, d, "mechanics", specified_parameters_mech) # Flow data object specified_parameters_flow = { "second_order_tensor": perm, "bc": bc_flow, "biot_alpha": 1., "bc_values": bc_val_flow, "mass_weight": S_m, "time_step": dt[tt - 1] } pp.initialize_default_data(g, d, "flow", specified_parameters_flow) ################################ # CALLING MPFA/MPSA ROUTINES # ################################ # Biot discretization solver_biot = pp.Biot("mechanics", "flow") solver_biot.discretize(g, d) # Mechanics discretization matrices biot_S = d['discretization_matrices']['mechanics']['stress'] biot_boundS = d['discretization_matrices']['mechanics']['bound_stress'] biot_divU = d['discretization_matrices']['mechanics']['div_d'] biot_gradP = d['discretization_matrices']['mechanics']['grad_p'] biot_boundDivU = d['discretization_matrices']['mechanics'][ 'bound_div_d'] biot_boundUCell = d['discretization_matrices']['mechanics'][ 'bound_displacement_cell'] biot_boundUFace = d['discretization_matrices']['mechanics'][ 'bound_displacement_face'] biot_boundUPressure = d['discretization_matrices']['mechanics'][ 'bound_displacement_pressure'] biot_divS = pp.fvutils.vector_divergence(g) # Flow discretization matrices biot_F = d['discretization_matrices']['flow']['flux'] biot_boundF = d['discretization_matrices']['flow']['bound_flux'] biot_compat = d['discretization_matrices']['flow'][ 'biot_stabilization'] biot_divF = pp.fvutils.scalar_divergence(g) ################################ # Saving Initial Condition # ################################ if times[tt] == 0: sol['pressure'][tt] = p_ad.val sol['displacement'][tt] = u_ad.val sol['displacement_faces'][tt] = ( boundUCell(sol['displacement'][tt]) + boundUFace(bc_val_mech[tt]) + boundUPressure(sol['pressure'][tt])) sol['time'][tt] = times[tt] sol['traction'][tt] = T(u_ad.val, bc_val_mech[tt]) sol['flux'][tt] = Q(p_ad.val) tt += 1 # increasing time counter ################################ # Solving the set of PDE's # ################################ # Displacement and pressure at the previous time step u_n = u_ad.val.copy() p_n = p_ad.val.copy() # Updating residual and iteration at each time step newton_param.update({'res_norm': 1000, 'iter': 1}) # Newton loop while newton_param['res_norm'] > newton_param['tol'] and newton_param[ 'iter'] <= newton_param['max_iter']: # Calling equations eq1 = u_eq1(u_ad, bc_val_mech[tt]) eq2 = u_eq2(p_ad) eq3 = p_eq1(u_ad, u_n, bc_val_mech[tt], bc_val_mech[tt - 1]) eq4 = p_eq2(p_ad, p_n, dt[tt - 1]) # Assembling Jacobian of the coupled system J_mech = np.hstack( (eq1.jac, eq2.jac)) # Jacobian blocks (mechanics) J_flow = np.hstack((eq3.jac, eq4.jac)) # Jacobian blocks (flow) J = sps.bmat(np.vstack((J_mech, J_flow)), format='csc') # Jacobian (coupled) # Determining residual of the coupled system R_mech = eq1.val + eq2.val # Residual (mechanics) R_flow = eq3.val + eq4.val # Residual (flow) R = np.hstack((R_mech, R_flow)) # Residual (coupled) y = sps.linalg.spsolve(J, -R) # u_ad.val = u_ad.val + y[:g.dim * g.num_cells] # Newton update p_ad.val = p_ad.val + y[g.dim * g.num_cells:] # newton_param['res_norm'] = np.linalg.norm(R) # Updating residual if newton_param['res_norm'] <= newton_param[ 'tol'] and newton_param['iter'] <= newton_param['max_iter']: print('Iter: {} \t Error: {:.8f} [m]'.format( newton_param['iter'], newton_param['res_norm'])) elif newton_param['iter'] > newton_param['max_iter']: print('Error: Newton method did not converge!') else: newton_param['iter'] += 1 ################################ # Saving the variables # ################################ sol['iter'] = np.concatenate( (sol['iter'], np.array([newton_param['iter']]))) sol['residual'] = np.concatenate( (sol['residual'], np.array([newton_param['res_norm']]))) sol['time_step'] = np.concatenate((sol['time_step'], dt)) sol['pressure'][tt] = p_ad.val sol['displacement'][tt] = u_ad.val sol['displacement_faces'][tt] = (boundUCell(sol['displacement'][tt]) + boundUFace(bc_val_mech[tt]) + boundUPressure(sol['pressure'][tt])) sol['time'][tt] = times[tt] sol['traction'][tt] = T(u_ad.val, bc_val_mech[tt]) sol['flux'][tt] = Q(p_ad.val) # Calling analytical solution sol_mandel = mandel_solution(g, Nx, Ny, times, F_load, B, nu_u, nu_s, c_f, mu_s) # Creating analytical and numerical results arrays p_num = (Lx * sol['pressure'][-1][:Nx]) / F p_ana = (Lx * sol_mandel['p'][-1]) / F # Returning values return p_num, p_ana