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
Example #3
0
    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
        )
Example #4
0
 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)
Example #7
0
    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]
Example #12
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
Example #13
0
    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)))
Example #14
0

# 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)
Example #15
0
    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))))
Example #16
0
    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,
        )
Example #17
0
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