Beispiel #1
0
    def __init__(self, gb, flow, model="flow"):

        self.model = model
        self.gb = gb
        self.data = None
        self.assembler = None

        # discretization operator name
        self.discr_name = self.model + "_flux"
        self.discr = pp.Tpfa(self.model)

        self.coupling_name = self.discr_name + "_coupling"
        self.coupling = pp.RobinCoupling(self.model, self.discr)

        self.source_name = self.model + "_source"
        self.source = pp.ScalarSource(self.model)

        # master variable name
        self.variable = self.model + "_variable"
        self.mortar = self.model + "_lambda"

        # post process variables
        self.pressure = "pressure"
        self.flux = "darcy_flux"  # it has to be this one
        self.P0_flux = "P0_darcy_flux"
Beispiel #2
0
def solve_tpfa(gb, folder, discr_3d=None):
    # Choose and define the solvers and coupler
    flow_discretization = pp.Tpfa("flow")
    source_discretization = pp.ScalarSource("flow")
    run_flow(gb,
             flow_discretization,
             source_discretization,
             folder,
             is_FV=True,
             discr_3d=discr_3d)
Beispiel #3
0
    def test_integral(self):
        g, d = setup_3d_grid()

        src_disc = pp.ScalarSource("flow")
        src_disc.discretize(g, d)
        lhs, rhs = src_disc.assemble_matrix_rhs(g, d)

        rhs_t = np.array([0, 0, 0, 0, 1, 0, 0, 0])

        self.assertTrue(src_disc.ndof(g) == g.num_cells)
        self.assertTrue(np.all(rhs == rhs_t))
        self.assertTrue(lhs.shape == (8, 8))
        self.assertTrue(lhs.nnz == 0)
    def _assign_discretizations(self) -> None:
        """
        Assign discretizations to the nodes and edges of the grid bucket.

        Note the attribute subtract_fracture_pressure: Indicates whether or not to
        subtract the fracture pressure contribution for the contact traction. This
        should not be done if the scalar variable is temperature.
        """
        super()._assign_discretizations()

        # Shorthand
        key_s, key_m = self.scalar_parameter_key, self.mechanics_parameter_key
        var_s, var_d = self.scalar_variable, self.displacement_variable

        # Define discretization
        # For the Nd domain we solve linear elasticity with mpsa.
        mpsa = pp.Mpsa(key_m)
        mass_disc_s = pp.MassMatrix(key_s)
        source_disc_s = pp.ScalarSource(key_s)
        # Coupling discretizations

        # Assign node discretizations
        for g, d in self.gb:
            if g.dim == self.Nd:
                mpsa = d[pp.DISCRETIZATION][var_d]["mpsa"]

            elif g.dim == self.Nd - 1:
                d[pp.DISCRETIZATION].update({
                    var_s: {
                        "mass": mass_disc_s,
                        "source": source_disc_s,
                    },
                })

        fracture_scalar_to_force_balance = pp.FractureScalarToForceBalance(
            mpsa, mass_disc_s)

        for e, d in self.gb.edges():
            g_l, g_h = self.gb.nodes_of_edge(e)

            if g_h.dim == self.Nd:
                d[pp.COUPLING_DISCRETIZATION].update({
                    "fracture_scalar_to_force_balance": {
                        g_h: (var_d, "mpsa"),
                        g_l: (var_s, "mass"),
                        e: (
                            self.mortar_displacement_variable,
                            fracture_scalar_to_force_balance,
                        ),
                    }
                })
Beispiel #5
0
    def assign_scalar_discretizations(self) -> None:
        """
        Assign discretizations to the nodes and edges of the grid bucket.

        Note the attribute subtract_fracture_pressure: Indicates whether or not to
        subtract the fracture pressure contribution for the contact traction. This
        should not be done if the scalar variable is temperature.
        """
        gb = self.gb
        # Shorthand
        key_s = self.scalar_parameter_key
        var_s = self.scalar_variable
        discr_key, coupling_discr_key = pp.DISCRETIZATION, pp.COUPLING_DISCRETIZATION

        # Scalar discretizations (all dimensions)
        diff_disc_s = implicit_euler.ImplicitMpfa(key_s)
        mass_disc_s = implicit_euler.ImplicitMassMatrix(key_s, var_s)
        source_disc_s = pp.ScalarSource(key_s)

        # Assign node discretizations
        for _, d in gb:
            add_nonpresent_dictionary(d, discr_key)

            d[discr_key].update({
                var_s: {
                    "diffusion": diff_disc_s,
                    "mass": mass_disc_s,
                    "source": source_disc_s,
                },
            })

        # Assign edge discretizations
        for e, d in gb.edges():
            g_l, g_h = gb.nodes_of_edge(e)
            add_nonpresent_dictionary(d, coupling_discr_key)

            d[coupling_discr_key].update({
                self.scalar_coupling_term: {
                    g_h: (var_s, "diffusion"),
                    g_l: (var_s, "diffusion"),
                    e: (
                        self.mortar_scalar_variable,
                        pp.RobinCoupling(key_s, diff_disc_s),
                    ),
                },
            })
    def assign_discretizations(self) -> None:
        """
        Assign discretizations to the nodes and edges of the grid bucket.

        Note the attribute subtract_fracture_pressure: Indicates whether or not to
        subtract the fracture pressure contribution for the contact traction. This
        should not be done if the scalar variable is temperature.
        """
        # from porepy.utils.derived_discretizations import (
        #     implicit_euler as IE_discretizations,
        # )

        # Shorthand
        key_s = self.scalar_parameter_key
        var_s = self.scalar_variable

        # Scalar discretizations (all dimensions)
        diff_disc_s = pp.Mpfa(key_s)
        # mass_disc_s = pp.MassMatrix(key_s)
        # diff_disc_s = IE_discretizations.ImplicitMpfa(key_s)
        # mass_disc_s = IE_discretizations.ImplicitMassMatrix(key_s, var_s)
        source_disc_s = pp.ScalarSource(key_s)

        # Assign node discretizations
        for g, d in self.gb:
            d[pp.DISCRETIZATION] = {
                var_s: {
                    "diffusion": diff_disc_s,
                    # "mass": mass_disc_s,
                    "source": source_disc_s,
                },
            }

        for e, d in self.gb.edges():
            g_l, g_h = self.gb.nodes_of_edge(e)
            d[pp.COUPLING_DISCRETIZATION] = {
                self.scalar_coupling_term: {
                    g_h: (var_s, "diffusion"),
                    g_l: (var_s, "diffusion"),
                    e: (
                        self.mortar_scalar_variable,
                        pp.RobinCoupling(key_s, diff_disc_s),
                    ),
                },
            }
Beispiel #7
0
    def assign_discretizations(self) -> None:
        """
        Assign discretizations to the nodes and edges of the grid bucket.

        Note the attribute subtract_fracture_pressure: Indicates whether or not to
        subtract the fracture pressure contribution for the contact traction. This
        should not be done if the scalar variable is temperature.
        """
        # Call parent class for disrcetizations for the poro-elastic system.
        super().assign_discretizations()

        # What remains is terms related to temperature

        # Shorthand for parameter keywords
        key_t = self.temperature_parameter_key
        key_m = self.mechanics_parameter_key

        # distinguish the coupling terms from temperature terms (grad_p, as opposed to grad_T)
        key_mt = self.mechanics_temperature_parameter_key
        var_s = self.scalar_variable
        var_t = self.temperature_variable
        var_d = self.displacement_variable

        # Define discretization
        # Scalar discretizations (will be assigned to all dimensions)
        diff_disc_t = IE_discretizations.ImplicitMpfa(key_t)
        adv_disc_t = IE_discretizations.ImplicitUpwind(key_t)
        mass_disc_t = IE_discretizations.ImplicitMassMatrix(key_t, var_t)
        source_disc_t = pp.ScalarSource(key_t)

        # Coupling discretizations
        # All dimensions
        div_u_disc_t = pp.DivU(
            key_m,
            key_t,
            variable=var_d,
            mortar_variable=self.mortar_displacement_variable,
        )

        # Nd
        grad_t_disc = pp.GradP(
            key_mt
        )  # pp.GradP(key_m) # Kanskje denne (og andre) b;r erstattes av spesiallagde
        # varianter som henter ut s-varianten og ganger med alpha/beta?

        stabilization_disc_t = pp.BiotStabilization(key_t, var_t)
        s2t_disc = IE_discretizations.ImplicitMassMatrix(
            keyword=self.s2t_parameter_key, variable=var_s)
        t2s_disc = IE_discretizations.ImplicitMassMatrix(
            keyword=self.t2s_parameter_key, variable=var_t)

        # Assign node discretizations
        for g, d in self.gb:
            if g.dim == self.Nd:
                d[pp.DISCRETIZATION].update(
                    {  # advection-diffusion equation for temperature
                        var_t: {
                            "diffusion": diff_disc_t,
                            self.advection_term: adv_disc_t,
                            "mass": mass_disc_t,
                            # Also the stabilization term from Biot
                            "stabilization": stabilization_disc_t,
                            "source": source_disc_t,
                        },
                        # grad T term in the momentuum equation
                        var_d + "_" + var_t: {"grad_p": grad_t_disc},
                        # div u term in the energy equation
                        var_t + "_" + var_d: {"div_u": div_u_disc_t},
                        # Accumulation of pressure in energy equation
                        var_t + "_" + var_s: {self.s2t_coupling_term: s2t_disc},
                        # Accumulation of temperature / energy in pressure equation
                        var_s + "_" + var_t: {self.t2s_coupling_term: t2s_disc},
                    }
                )
            else:
                # Inside fracture network
                d[pp.DISCRETIZATION].update(
                    {  # Advection-diffusion equation, no biot stabilization
                        var_t: {
                            "diffusion": diff_disc_t,
                            self.advection_term: adv_disc_t,
                            "mass": mass_disc_t,
                            "source": source_disc_t,
                        },
                        # Standard coupling terms
                        var_t + "_" + var_s: {self.s2t_coupling_term: s2t_disc},
                        var_s + "_" + var_t: {self.t2s_coupling_term: t2s_disc},
                    }
                )
        # Next, the edges in the gb.
        # Account for the mortar displacements effect on energy balance in the matrix,
        # as an internal boundary contribution
        div_u_coupling_t = pp.DivUCoupling(self.displacement_variable,
                                           div_u_disc_t, div_u_disc_t)
        # Account for the temperature contributions to the force balance on the fracture
        # (see contact_discr).
        # This discretization needs the keyword used to store the grad p discretization:
        grad_t_key = key_mt
        matrix_temperature_to_force_balance = pp.MatrixScalarToForceBalance(
            grad_t_key, mass_disc_t, mass_disc_t)

        # Coupling of advection and diffusion terms in the energy equation
        adv_coupling_t = IE_discretizations.ImplicitUpwindCoupling(key_t)
        diff_coupling_t = pp.RobinCoupling(key_t, diff_disc_t)

        for e, d in self.gb.edges():
            g_l, g_h = self.gb.nodes_of_edge(e)

            d[pp.COUPLING_DISCRETIZATION].update({
                self.advection_coupling_term: {
                    g_h: (self.temperature_variable, self.advection_term),
                    g_l: (self.temperature_variable, self.advection_term),
                    e: (self.mortar_temperature_advection_variable,
                        adv_coupling_t),
                },
                self.temperature_coupling_term: {
                    g_h: (var_t, "diffusion"),
                    g_l: (var_t, "diffusion"),
                    e: (self.mortar_temperature_variable, diff_coupling_t),
                },
            })
            if g_h.dim == self.Nd:
                d[pp.COUPLING_DISCRETIZATION].update({
                    "div_u_coupling_t": {
                        g_h: (
                            var_t,
                            "mass",
                        ),  # This is really the div_u, but this is not implemented
                        g_l: (var_t, "mass"),
                        e:
                        (self.mortar_displacement_variable, div_u_coupling_t),
                    },
                    "matrix_temperature_to_force_balance": {
                        g_h: (var_t, "mass"),
                        g_l: (var_t, "mass"),
                        e: (
                            self.mortar_displacement_variable,
                            matrix_temperature_to_force_balance,
                        ),
                    },
                })
Beispiel #8
0
    def solve(self, gb, analytic_p):

        # Parameter-discretization keyword:
        kw = "flow"
        # Terms
        key_flux = "flux"
        key_src = "src"
        # Primary variables
        key_p = "pressure"  # pressure name
        key_m = "mortar"  # mortar name

        tpfa = pp.Tpfa(kw)
        src = pp.ScalarSource(kw)
        for g, d in gb:
            d[pp.DISCRETIZATION] = {key_p: {key_src: src, key_flux: tpfa}}
            d[pp.PRIMARY_VARIABLES] = {key_p: {"cells": 1}}

        for e, d in gb.edges():
            g1, g2 = gb.nodes_of_edge(e)
            d[pp.PRIMARY_VARIABLES] = {key_m: {"cells": 1}}
            if g1.dim == g2.dim:
                mortar_disc = pp.FluxPressureContinuity(kw, tpfa)
            else:
                mortar_disc = pp.RobinCoupling(kw, tpfa)
            d[pp.COUPLING_DISCRETIZATION] = {
                key_flux: {
                    g1: (key_p, key_flux),
                    g2: (key_p, key_flux),
                    e: (key_m, mortar_disc),
                }
            }

        assembler = pp.Assembler(gb)
        assembler.discretize()
        A, b = assembler.assemble_matrix_rhs()
        x = sps.linalg.spsolve(A, b)

        assembler.distribute_variable(x)

        # test pressure
        for g, d in gb:
            ap, _, _ = analytic_p(g.cell_centers)
            self.assertTrue(np.max(np.abs(d[pp.STATE][key_p] - ap)) < 5e-2)

        # test mortar solution
        for e, d_e in gb.edges():
            mg = d_e["mortar_grid"]
            g1, g2 = gb.nodes_of_edge(e)
            if g1 == g2:
                left_to_m = mg.master_to_mortar_avg()
                right_to_m = mg.slave_to_mortar_avg()
            else:
                continue
            d1 = gb.node_props(g1)
            d2 = gb.node_props(g2)

            _, analytic_flux, _ = analytic_p(g1.face_centers)
            # the aperture is assumed constant
            left_flux = np.sum(analytic_flux * g1.face_normals[:2], 0)
            left_flux = left_to_m * (
                d1[pp.DISCRETIZATION_MATRICES][kw]["bound_flux"] * left_flux
            )
            # right flux is negative lambda
            right_flux = np.sum(analytic_flux * g2.face_normals[:2], 0)
            right_flux = -right_to_m * (
                d2[pp.DISCRETIZATION_MATRICES][kw]["bound_flux"] * right_flux
            )
            self.assertTrue(np.max(np.abs(d_e[pp.STATE][key_m] - left_flux)) < 5e-2)
            self.assertTrue(np.max(np.abs(d_e[pp.STATE][key_m] - right_flux)) < 5e-2)
Beispiel #9
0
def test_md_flow():

    # Three fractures, will create intersection lines and point
    frac_1 = np.array([[2, 4, 4, 2], [3, 3, 3, 3], [0, 0, 6, 6]])
    frac_2 = np.array([[3, 3, 3, 3], [2, 4, 4, 2], [0, 0, 6, 6]])
    frac_3 = np.array([[0, 6, 6, 0], [2, 2, 4, 4], [3, 3, 3, 3]])

    gb = pp.meshing.cart_grid(fracs=[frac_1, frac_2, frac_3],
                              nx=np.array([6, 6, 6]))
    gb.compute_geometry()

    pressure_variable = "pressure"
    flux_variable = "mortar_flux"

    keyword = "flow"
    discr = pp.Tpfa(keyword)
    source_discr = pp.ScalarSource(keyword)
    coupling_discr = pp.RobinCoupling(keyword, discr, discr)

    for g, d in gb:

        # Assign data
        if g.dim == gb.dim_max():
            upper_left_ind = np.argmax(np.linalg.norm(g.face_centers, axis=0))
            bc = pp.BoundaryCondition(g, np.array([0, upper_left_ind]),
                                      ["dir", "dir"])
            bc_values = np.zeros(g.num_faces)
            bc_values[0] = 1
            sources = np.random.rand(g.num_cells) * g.cell_volumes
            specified_parameters = {
                "bc": bc,
                "bc_values": bc_values,
                "source": sources
            }
        else:
            sources = np.random.rand(g.num_cells) * g.cell_volumes
            specified_parameters = {"source": sources}

        # Initialize data
        pp.initialize_default_data(g, d, keyword, specified_parameters)

        # Declare grid primary variable
        d[pp.PRIMARY_VARIABLES] = {pressure_variable: {"cells": 1}}

        # Assign discretization
        d[pp.DISCRETIZATION] = {
            pressure_variable: {
                "diff": discr,
                "source": source_discr
            }
        }

        # Initialize state
        d[pp.STATE] = {
            pressure_variable: np.zeros(g.num_cells),
            pp.ITERATE: {
                pressure_variable: np.zeros(g.num_cells)
            },
        }
    for e, d in gb.edges():
        mg = d["mortar_grid"]
        pp.initialize_data(mg, d, keyword, {"normal_diffusivity": 1})

        d[pp.PRIMARY_VARIABLES] = {flux_variable: {"cells": 1}}
        d[pp.COUPLING_DISCRETIZATION] = {}
        d[pp.COUPLING_DISCRETIZATION]["coupling"] = {
            e[0]: (pressure_variable, "diff"),
            e[1]: (pressure_variable, "diff"),
            e: (flux_variable, coupling_discr),
        }
        d[pp.STATE] = {
            flux_variable: np.zeros(mg.num_cells),
            pp.ITERATE: {
                flux_variable: np.zeros(mg.num_cells)
            },
        }

    dof_manager = pp.DofManager(gb)
    assembler = pp.Assembler(gb, dof_manager)
    assembler.discretize()

    # Reference discretization
    A_ref, b_ref = assembler.assemble_matrix_rhs()

    manager = pp.ad.EquationManager(gb, dof_manager)

    grid_list = [g for g, _ in gb]
    edge_list = [e for e, _ in gb.edges()]

    node_discr = pp.ad.MpfaAd(keyword, grid_list)

    edge_discr = pp.ad.RobinCouplingAd(keyword, edge_list)

    bc_val = pp.ad.BoundaryCondition(keyword, grid_list)

    source = pp.ad.ParameterArray(param_keyword=keyword,
                                  array_keyword='source',
                                  grids=grid_list)

    projections = pp.ad.MortarProjections(gb=gb)
    div = pp.ad.Divergence(grids=grid_list)

    p = manager.merge_variables([(g, pressure_variable) for g in grid_list])
    lmbda = manager.merge_variables([(e, flux_variable) for e in edge_list])

    flux = (node_discr.flux * p + node_discr.bound_flux * bc_val +
            node_discr.bound_flux * projections.mortar_to_primary_int * lmbda)
    flow_eq = div * flux - projections.mortar_to_secondary_int * lmbda - source

    interface_flux = edge_discr.mortar_scaling * (
        projections.primary_to_mortar_avg * node_discr.bound_pressure_cell * p
        + projections.primary_to_mortar_avg * node_discr.bound_pressure_face *
        projections.mortar_to_primary_int * lmbda -
        projections.secondary_to_mortar_avg * p +
        edge_discr.mortar_discr * lmbda)

    flow_eq_ad = pp.ad.Expression(flow_eq, dof_manager, "flow on nodes")
    flow_eq_ad.discretize(gb)

    interface_eq_ad = pp.ad.Expression(interface_flux, dof_manager,
                                       "flow on interface")

    manager.equations += [flow_eq_ad, interface_eq_ad]

    state = np.zeros(gb.num_cells() + gb.num_mortar_cells())
    A, b = manager.assemble_matrix_rhs(state=state)
    diff = A - A_ref
    if diff.data.size > 0:
        assert np.max(np.abs(diff.data)) < 1e-10
    assert np.max(np.abs(b - b_ref)) < 1e-10
    def _assign_discretizations(self) -> None:
        """
        Assign discretizations to the nodes and edges of the grid bucket.

        Note the attribute subtract_fracture_pressure: Indicates whether or not to
        subtract the fracture pressure contribution for the contact traction. This
        should not be done if the scalar variable is temperature.
        """
        if not self._use_ad:

            # Shorthand
            key_s, key_m = self.scalar_parameter_key, self.mechanics_parameter_key
            var_s, var_d = self.scalar_variable, self.displacement_variable

            # Define discretization
            # For the Nd domain we solve linear elasticity with mpsa.
            mpsa = pp.Mpsa(key_m)
            empty_discr = pp.VoidDiscretization(key_m, ndof_cell=self._Nd)
            # Scalar discretizations (all dimensions)
            diff_disc_s = IE_discretizations.ImplicitMpfa(key_s)
            mass_disc_s = IE_discretizations.ImplicitMassMatrix(key_s, var_s)
            source_disc_s = pp.ScalarSource(key_s)
            # Coupling discretizations
            # All dimensions
            div_u_disc = pp.DivU(
                key_m,
                key_s,
                variable=var_d,
                mortar_variable=self.mortar_displacement_variable,
            )
            # Nd
            grad_p_disc = pp.GradP(key_m)
            stabilization_disc_s = pp.BiotStabilization(key_s, var_s)

            # Assign node discretizations
            for g, d in self.gb:
                if g.dim == self._Nd:
                    d[pp.DISCRETIZATION] = {
                        var_d: {"mpsa": mpsa},
                        var_s: {
                            "diffusion": diff_disc_s,
                            "mass": mass_disc_s,
                            "stabilization": stabilization_disc_s,
                            "source": source_disc_s,
                        },
                        var_d + "_" + var_s: {"grad_p": grad_p_disc},
                        var_s + "_" + var_d: {"div_u": div_u_disc},
                    }

                elif g.dim == self._Nd - 1:
                    d[pp.DISCRETIZATION] = {
                        self.contact_traction_variable: {"empty": empty_discr},
                        var_s: {
                            "diffusion": diff_disc_s,
                            "mass": mass_disc_s,
                            "source": source_disc_s,
                        },
                    }
                else:
                    d[pp.DISCRETIZATION] = {
                        var_s: {
                            "diffusion": diff_disc_s,
                            "mass": mass_disc_s,
                            "source": source_disc_s,
                        }
                    }

            # Define edge discretizations for the mortar grid
            contact_law = pp.ColoumbContact(
                self.mechanics_parameter_key, self._Nd, mpsa
            )
            contact_discr = pp.PrimalContactCoupling(
                self.mechanics_parameter_key, mpsa, contact_law
            )
            # Account for the mortar displacements effect on scalar balance in the matrix,
            # as an internal boundary contribution, fracture, aperture changes appear as a
            # source contribution.
            div_u_coupling = pp.DivUCoupling(
                self.displacement_variable, div_u_disc, div_u_disc
            )
            # Account for the pressure contributions to the force balance on the fracture
            # (see contact_discr).
            # This discretization needs the keyword used to store the grad p discretization:
            grad_p_key = key_m
            matrix_scalar_to_force_balance = pp.MatrixScalarToForceBalance(
                grad_p_key, mass_disc_s, mass_disc_s
            )
            if self.subtract_fracture_pressure:
                fracture_scalar_to_force_balance = pp.FractureScalarToForceBalance(
                    mass_disc_s, mass_disc_s
                )

            for e, d in self.gb.edges():
                g_l, g_h = self.gb.nodes_of_edge(e)

                if g_h.dim == self._Nd:
                    d[pp.COUPLING_DISCRETIZATION] = {
                        self.friction_coupling_term: {
                            g_h: (var_d, "mpsa"),
                            g_l: (self.contact_traction_variable, "empty"),
                            (g_h, g_l): (
                                self.mortar_displacement_variable,
                                contact_discr,
                            ),
                        },
                        self.scalar_coupling_term: {
                            g_h: (var_s, "diffusion"),
                            g_l: (var_s, "diffusion"),
                            e: (
                                self.mortar_scalar_variable,
                                pp.RobinCoupling(key_s, diff_disc_s),
                            ),
                        },
                        "div_u_coupling": {
                            g_h: (
                                var_s,
                                "mass",
                            ),  # This is really the div_u, but this is not implemented
                            g_l: (var_s, "mass"),
                            e: (self.mortar_displacement_variable, div_u_coupling),
                        },
                        "matrix_scalar_to_force_balance": {
                            g_h: (var_s, "mass"),
                            g_l: (var_s, "mass"),
                            e: (
                                self.mortar_displacement_variable,
                                matrix_scalar_to_force_balance,
                            ),
                        },
                    }
                    if self.subtract_fracture_pressure:
                        d[pp.COUPLING_DISCRETIZATION].update(
                            {
                                "fracture_scalar_to_force_balance": {
                                    g_h: (var_s, "mass"),
                                    g_l: (var_s, "mass"),
                                    e: (
                                        self.mortar_displacement_variable,
                                        fracture_scalar_to_force_balance,
                                    ),
                                }
                            }
                        )
                else:
                    d[pp.COUPLING_DISCRETIZATION] = {
                        self.scalar_coupling_term: {
                            g_h: (var_s, "diffusion"),
                            g_l: (var_s, "diffusion"),
                            e: (
                                self.mortar_scalar_variable,
                                pp.RobinCoupling(key_s, diff_disc_s),
                            ),
                        }
                    }

        else:

            gb = self.gb
            Nd = self._Nd
            dof_manager = pp.DofManager(gb)
            eq_manager = pp.ad.EquationManager(gb, dof_manager)

            g_primary = gb.grids_of_dimension(Nd)[0]
            g_frac = gb.grids_of_dimension(Nd - 1).tolist()

            grid_list = [
                g_primary,
                *g_frac,
                *gb.grids_of_dimension(Nd - 2),
                *gb.grids_of_dimension(Nd - 3),
            ]

            if len(gb.grids_of_dimension(Nd)) != 1:
                raise NotImplementedError("This will require further work")

            edge_list_highest = [(g_primary, g) for g in g_frac]
            edge_list = [e for e, _ in gb.edges()]

            mortar_proj_scalar = pp.ad.MortarProjections(edges=edge_list, gb=gb, nd=1)
            mortar_proj_vector = pp.ad.MortarProjections(
                edges=edge_list_highest, gb=gb, nd=self._Nd
            )
            subdomain_proj_scalar = pp.ad.SubdomainProjections(gb=gb)
            subdomain_proj_vector = pp.ad.SubdomainProjections(gb=gb, nd=self._Nd)

            tangential_normal_proj_list = []
            normal_proj_list = []
            for gf in g_frac:
                proj = gb.node_props(gf, "tangential_normal_projection")
                tangential_normal_proj_list.append(
                    proj.project_tangential_normal(gf.num_cells)
                )
                normal_proj_list.append(proj.project_normal(gf.num_cells))

            tangential_normal_proj = pp.ad.Matrix(
                sps.block_diag(tangential_normal_proj_list)
            )
            normal_proj = pp.ad.Matrix(sps.block_diag(normal_proj_list))

            # Ad representation of discretizations
            mpsa_ad = pp.ad.BiotAd(self.mechanics_parameter_key, g_primary)
            grad_p_ad = pp.ad.GradPAd(self.mechanics_parameter_key, g_primary)

            mpfa_ad = pp.ad.MpfaAd(self.scalar_parameter_key, grid_list)
            mass_ad = pp.ad.MassMatrixAd(self.scalar_parameter_key, grid_list)
            robin_ad = pp.ad.RobinCouplingAd(self.scalar_parameter_key, edge_list)

            div_u_ad = pp.ad.DivUAd(
                self.mechanics_parameter_key,
                grids=g_primary,
                mat_dict_keyword=self.scalar_parameter_key,
            )
            stab_biot_ad = pp.ad.BiotStabilizationAd(
                self.scalar_parameter_key, g_primary
            )

            coloumb_ad = pp.ad.ColoumbContactAd(
                self.mechanics_parameter_key, edge_list_highest
            )

            bc_ad = pp.ad.BoundaryCondition(
                self.mechanics_parameter_key, grids=[g_primary]
            )
            div_vector = pp.ad.Divergence(grids=[g_primary], dim=g_primary.dim)

            # Primary variables on Ad form
            u = eq_manager.variable(g_primary, self.displacement_variable)
            u_mortar = eq_manager.merge_variables(
                [(e, self.mortar_displacement_variable) for e in edge_list_highest]
            )
            contact_force = eq_manager.merge_variables(
                [(g, self.contact_traction_variable) for g in g_frac]
            )
            p = eq_manager.merge_variables(
                [(g, self.scalar_variable) for g in grid_list]
            )
            mortar_flux = eq_manager.merge_variables(
                [(e, self.mortar_scalar_variable) for e in edge_list]
            )

            u_prev = u.previous_timestep()
            u_mortar_prev = u_mortar.previous_timestep()
            p_prev = p.previous_timestep()

            # Reference pressure, corresponding to an initial stress free state
            p_reference = pp.ad.ParameterArray(
                param_keyword=self.mechanics_parameter_key,
                array_keyword="p_reference",
                grids=[g_primary],
                gb=gb,
            )

            # Stress in g_h
            stress = (
                mpsa_ad.stress * u
                + mpsa_ad.bound_stress * bc_ad
                + mpsa_ad.bound_stress
                * subdomain_proj_vector.face_restriction(g_primary)
                * mortar_proj_vector.mortar_to_primary_avg
                * u_mortar
                + grad_p_ad.grad_p
                * subdomain_proj_scalar.cell_restriction(g_primary)
                * p
                # The reference pressure is only defined on g_primary, thus there is no need
                # for a subdomain projection.
                - grad_p_ad.grad_p * p_reference
            )

            momentum_eq = pp.ad.Expression(
                div_vector * stress, dof_manager, "momentuum", grid_order=[g_primary]
            )

            jump = (
                subdomain_proj_vector.cell_restriction(g_frac)
                * mortar_proj_vector.mortar_to_secondary_avg
                * mortar_proj_vector.sign_of_mortar_sides
            )
            jump_rotate = tangential_normal_proj * jump

            # Contact conditions
            num_frac_cells = np.sum([g.num_cells for g in g_frac])

            jump_discr = coloumb_ad.displacement * jump_rotate * u_mortar
            tmp = np.ones(num_frac_cells * self._Nd)
            tmp[self._Nd - 1 :: self._Nd] = 0
            exclude_normal = pp.ad.Matrix(
                sps.dia_matrix((tmp, 0), shape=(tmp.size, tmp.size))
            )
            # Rhs of contact conditions
            rhs = (
                coloumb_ad.rhs
                + exclude_normal * coloumb_ad.displacement * jump_rotate * u_mortar_prev
            )
            contact_conditions = coloumb_ad.traction * contact_force + jump_discr - rhs
            contact_eq = pp.ad.Expression(
                contact_conditions, dof_manager, "contact", grid_order=g_frac
            )

            # Force balance
            mat = None
            for _, d in gb.edges():
                mg: pp.MortarGrid = d["mortar_grid"]
                if mg.dim < self._Nd - 1:
                    continue

                faces_on_fracture_surface = mg.primary_to_mortar_int().tocsr().indices
                m = pp.grid_utils.switch_sign_if_inwards_normal(
                    g_primary, self._Nd, faces_on_fracture_surface
                )
                if mat is None:
                    mat = m
                else:
                    mat += m

            sign_switcher = pp.ad.Matrix(mat)

            # Contact from primary grid and mortar displacements (via primary grid)
            contact_from_primary_mortar = (
                mortar_proj_vector.primary_to_mortar_int
                * subdomain_proj_vector.face_prolongation(g_primary)
                * sign_switcher
                * stress
            )
            contact_from_secondary = (
                mortar_proj_vector.sign_of_mortar_sides
                * mortar_proj_vector.secondary_to_mortar_int
                * subdomain_proj_vector.cell_prolongation(g_frac)
                * tangential_normal_proj.transpose()
                * contact_force
            )
            if self.subtract_fracture_pressure:
                # This gives an error because of -=

                mat = []

                for e in edge_list_highest:
                    mg = gb.edge_props(e, "mortar_grid")

                    faces_on_fracture_surface = (
                        mg.primary_to_mortar_int().tocsr().indices
                    )
                    sgn, _ = g_primary.signs_and_cells_of_boundary_faces(
                        faces_on_fracture_surface
                    )
                    fracture_normals = g_primary.face_normals[
                        : self._Nd, faces_on_fracture_surface
                    ]
                    outwards_fracture_normals = sgn * fracture_normals

                    data = outwards_fracture_normals.ravel("F")
                    row = np.arange(g_primary.dim * mg.num_cells)
                    col = np.tile(np.arange(mg.num_cells), (g_primary.dim, 1)).ravel(
                        "F"
                    )
                    n_dot_I = sps.csc_matrix((data, (row, col)))
                    # i) The scalar contribution to the contact stress is mapped to
                    # the mortar grid  and multiplied by n \dot I, with n being the
                    # outwards normals on the two sides.
                    # Note that by using different normals for the two sides, we do not need to
                    # adjust the secondary pressure with the corresponding signs by applying
                    # sign_of_mortar_sides as done in PrimalContactCoupling.
                    # iii) The contribution should be subtracted so that we balance the primary
                    # forces by
                    # T_contact - n dot I p,
                    # hence the minus.
                    mat.append(n_dot_I * mg.secondary_to_mortar_int(nd=1))
                # May need to do this as for tangential projections, additive that is
                normal_matrix = pp.ad.Matrix(sps.block_diag(mat))

                p_frac = subdomain_proj_scalar.cell_restriction(g_frac) * p

                contact_from_secondary2 = normal_matrix * p_frac
            force_balance_eq = pp.ad.Expression(
                contact_from_primary_mortar
                + contact_from_secondary
                + contact_from_secondary2,
                dof_manager,
                "force_balance",
                grid_order=edge_list_highest,
            )

            bc_val_scalar = pp.ad.BoundaryCondition(
                self.scalar_parameter_key, grid_list
            )

            div_scalar = pp.ad.Divergence(grids=grid_list)

            dt = self.time_step

            # FIXME: Need bc for div_u term, including previous time step
            accumulation_primary = (
                div_u_ad.div_u * (u - u_prev)
                + stab_biot_ad.stabilization
                * subdomain_proj_scalar.cell_restriction(g_primary)
                * (p - p_prev)
                + div_u_ad.bound_div_u
                * subdomain_proj_vector.face_restriction(g_primary)
                * mortar_proj_vector.mortar_to_primary_int
                * (u_mortar - u_mortar_prev)
            )

            # Accumulation term on the fractures.
            frac_vol = np.hstack([g.cell_volumes for g in g_frac])
            vol_mat = pp.ad.Matrix(
                sps.dia_matrix((frac_vol, 0), shape=(num_frac_cells, num_frac_cells))
            )
            accumulation_fracs = (
                vol_mat * normal_proj * jump * (u_mortar - u_mortar_prev)
            )

            accumulation_all = mass_ad.mass * (p - p_prev)

            flux = (
                mpfa_ad.flux * p
                + mpfa_ad.bound_flux * bc_val_scalar
                + mpfa_ad.bound_flux
                * mortar_proj_scalar.mortar_to_primary_int
                * mortar_flux
            )
            flow_md = (
                dt
                * (  # Time scaling of flux terms, both inter-dimensional and from
                    # the higher dimension
                    div_scalar * flux
                    - mortar_proj_scalar.mortar_to_secondary_int * mortar_flux
                )
                + accumulation_all
                + subdomain_proj_scalar.cell_prolongation(g_primary)
                * accumulation_primary
                + subdomain_proj_scalar.cell_prolongation(g_frac) * accumulation_fracs
            )

            interface_flow_eq = robin_ad.mortar_scaling * (
                mortar_proj_scalar.primary_to_mortar_avg
                * mpfa_ad.bound_pressure_cell
                * p
                + mortar_proj_scalar.primary_to_mortar_avg
                * mpfa_ad.bound_pressure_face
                * (
                    mortar_proj_scalar.mortar_to_primary_int * mortar_flux
                    + bc_val_scalar
                )
                - mortar_proj_scalar.secondary_to_mortar_avg * p
                + robin_ad.mortar_discr * mortar_flux
            )

            flow_eq = pp.ad.Expression(
                flow_md, dof_manager, "flow on nodes", grid_order=grid_list
            )
            interface_eq = pp.ad.Expression(
                interface_flow_eq,
                dof_manager,
                "flow on interface",
                grid_order=edge_list,
            )

            eq_manager.equations += [
                momentum_eq,
                contact_eq,
                force_balance_eq,
                flow_eq,
                interface_eq,
            ]
            self._eq_manager = eq_manager
Beispiel #11
0
def transport(gb, data, solver_name, folder, callback=None, save_every=1):
    grid_variable = "tracer"
    mortar_variable = "mortar_tracer"
    kw = "transport"
    # Identifier of the discretization operator on each grid
    advection_term = "advection"
    source_term = "source"
    mass_term = "mass"

    # Identifier of the discretization operator between grids
    advection_coupling_term = "advection_coupling"

    # Discretization objects
    node_discretization = pp.Upwind(kw)
    source_discretization = pp.ScalarSource(kw)
    mass_discretization = pp.MassMatrix(kw)
    edge_discretization = pp.UpwindCoupling(kw)

    # Loop over the nodes in the GridBucket, define primary variables and discretization schemes
    for g, d in gb:
        # Assign primary variables on this grid. It has one degree of freedom per cell.
        d[pp.PRIMARY_VARIABLES] = {grid_variable: {"cells": 1, "faces": 0}}
        # Assign discretization operator for the variable.
        d[pp.DISCRETIZATION] = {
            grid_variable: {
                advection_term: node_discretization,
                source_term: source_discretization,
                mass_term: mass_discretization,
            }
        }

    # Loop over the edges in the GridBucket, define primary variables and discretizations
    for e, d in gb.edges():
        g1, g2 = gb.nodes_of_edge(e)
        # The mortar variable has one degree of freedom per cell in the mortar grid
        d[pp.PRIMARY_VARIABLES] = {mortar_variable: {"cells": 1}}

        d[pp.COUPLING_DISCRETIZATION] = {
            advection_coupling_term: {
                g1: (grid_variable, advection_term),
                g2: (grid_variable, advection_term),
                e: (mortar_variable, edge_discretization),
            }
        }
        d[pp.DISCRETIZATION_MATRICES] = {kw: {}}

    assembler = pp.Assembler()

    # Assemble the linear system, using the information stored in the GridBucket. By
    # not adding the matrices, we can arrange them at will to obtain the efficient
    # solver defined below, which LU factorizes the system only once for all time steps.
    A, b, block_dof, full_dof = assembler.assemble_matrix_rhs(
        gb,
        active_variables=[grid_variable, mortar_variable],
        add_matrices=False)

    advection_coupling_term += ("_" + mortar_variable + "_" + grid_variable +
                                "_" + grid_variable)
    mass_term += "_" + grid_variable
    advection_term += "_" + grid_variable
    source_term += "_" + grid_variable

    lhs = A[mass_term] + data["time_step"] * (A[advection_term] +
                                              A[advection_coupling_term])
    rhs_source_adv = b[source_term] + data["time_step"] * (
        b[advection_term] + b[advection_coupling_term])

    IEsolver = sps.linalg.factorized(lhs)
    n_steps = int(np.round(data["t_max"] / data["time_step"]))

    # Initial condition
    tracer = np.zeros(rhs_source_adv.size)
    assembler.distribute_variable(
        gb,
        tracer,
        block_dof,
        full_dof,
        variable_names=[grid_variable, mortar_variable])

    # Exporter
    exporter = pp.Exporter(gb, name="tracer", folder=folder)
    export_fields = ["tracer"]

    # Keep track of the outflow for each time step
    outflow = np.empty(0)

    # Time loop
    for i in range(n_steps):
        # Export existing solution (final export is taken care of below)
        assembler.distribute_variable(
            gb,
            tracer,
            block_dof,
            full_dof,
            variable_names=[grid_variable, mortar_variable],
        )
        if np.isclose(i % save_every, 0):
            exporter.write_vtk(export_fields, time_step=int(i // save_every))
        tracer = IEsolver(A[mass_term] * tracer + rhs_source_adv)

        outflow = compute_flow_rate(gb, grid_variable, outflow)
        if callback is not None:
            callback(gb)

    exporter.write_vtk(export_fields, time_step=(n_steps // save_every))
    time_steps = np.arange(0, data["t_max"] + data["time_step"],
                           save_every * data["time_step"])
    exporter.write_pvd(time_steps)
    return tracer, outflow, A, b, block_dof, full_dof
    def assign_discretisations(self):
        """
        Assign discretizations to the nodes and edges of the grid bucket.

        Note the attribute subtract_fracture_pressure: Indicates whether or not to
        subtract the fracture pressure contribution for the contact traction. This
        should not be done if the scalar variable is temperature.
        """
        # Shorthand
        key_s, key_m = self.scalar_parameter_key, self.mechanics_parameter_key
        var_s, var_d = self.scalar_variable, self.displacement_variable

        # Define discretization
        # For the Nd domain we solve linear elasticity with mpsa.
        mpsa = pp.Mpsa(key_m)
        empty_discr = pp.VoidDiscretization(key_m, ndof_cell=self.Nd)
        # Scalar discretizations (all dimensions)
        diff_disc_s = IE_discretizations.ImplicitMpfa(key_s)
        mass_disc_s = IE_discretizations.ImplicitMassMatrix(key_s, var_s)
        source_disc_s = pp.ScalarSource(key_s)
        # Coupling discretizations
        # All dimensions
        div_u_disc = pp.DivU(
            key_m,
            key_s,
            variable=var_d,
            mortar_variable=self.mortar_displacement_variable,
        )
        # Nd
        grad_p_disc = pp.GradP(key_m)
        stabilization_disc_s = pp.BiotStabilization(key_s, var_s)

        # Assign node discretizations
        for g, d in self.gb:
            if g.dim == self.Nd:
                d[pp.DISCRETIZATION] = {
                    var_d: {
                        "mpsa": mpsa
                    },
                    var_s: {
                        "diffusion": diff_disc_s,
                        "mass": mass_disc_s,
                        "stabilization": stabilization_disc_s,
                        "source": source_disc_s,
                    },
                    var_d + "_" + var_s: {
                        "grad_p": grad_p_disc
                    },
                    var_s + "_" + var_d: {
                        "div_u": div_u_disc
                    },
                }
            elif g.dim == self.Nd - 1:
                d[pp.DISCRETIZATION] = {
                    self.contact_traction_variable: {
                        "empty": empty_discr
                    },
                    var_s: {
                        "diffusion": diff_disc_s,
                        "mass": mass_disc_s,
                        "source": source_disc_s,
                    },
                }

        # Define edge discretizations for the mortar grid
        contact_law = pp.ColoumbContact(self.mechanics_parameter_key, self.Nd)
        contact_discr = pp.PrimalContactCoupling(self.mechanics_parameter_key,
                                                 mpsa, contact_law)
        # Account for the mortar displacements effect on scalar balance in the matrix,
        # as an internal boundary contribution, fracture, aperture changes appear as a
        # source contribution.
        div_u_coupling = pp.DivUCoupling(self.displacement_variable,
                                         div_u_disc, div_u_disc)
        # Account for the pressure contributions to the force balance on the fracture
        # (see contact_discr).
        # This discretization needs the keyword used to store the grad p discretization:
        grad_p_key = key_m
        matrix_scalar_to_force_balance = pp.MatrixScalarToForceBalance(
            grad_p_key, mass_disc_s, mass_disc_s)
        if self.subtract_fracture_pressure:
            fracture_scalar_to_force_balance = pp.FractureScalarToForceBalance(
                mass_disc_s, mass_disc_s)

        for e, d in self.gb.edges():
            g_l, g_h = self.gb.nodes_of_edge(e)

            if g_h.dim == self.Nd:
                d[pp.COUPLING_DISCRETIZATION] = {
                    self.friction_coupling_term: {
                        g_h: (var_d, "mpsa"),
                        g_l: (self.contact_traction_variable, "empty"),
                        (g_h, g_l):
                        (self.mortar_displacement_variable, contact_discr),
                    },
                    self.scalar_coupling_term: {
                        g_h: (var_s, "diffusion"),
                        g_l: (var_s, "diffusion"),
                        e: (
                            self.mortar_scalar_variable,
                            pp.RobinCoupling(key_s, diff_disc_s),
                        ),
                    },
                    "div_u_coupling": {
                        g_h: (
                            var_s,
                            "mass",
                        ),  # This is really the div_u, but this is not implemented
                        g_l: (var_s, "mass"),
                        e: (self.mortar_displacement_variable, div_u_coupling),
                    },
                    "matrix_scalar_to_force_balance": {
                        g_h: (var_s, "mass"),
                        g_l: (var_s, "mass"),
                        e: (
                            self.mortar_displacement_variable,
                            matrix_scalar_to_force_balance,
                        ),
                    },
                }
                if self.subtract_fracture_pressure:
                    d[pp.COUPLING_DISCRETIZATION].update({
                        "matrix_scalar_to_force_balance": {
                            g_h: (var_s, "mass"),
                            g_l: (var_s, "mass"),
                            e: (
                                self.mortar_displacement_variable,
                                fracture_scalar_to_force_balance,
                            ),
                        }
                    })
            else:
                raise ValueError(
                    "assign_discretizations assumes no fracture intersections."
                )
def advdiff(gb, discr, param, bc_flag):

    model = "transport"

    model_data_adv, model_data_diff, model_data_src = data_advdiff(
        gb, model, param, bc_flag
    )

    # discretization operator names
    adv_id = "advection"
    diff_id = "diffusion"
    src_id = "source"

    # variable names
    variable = "scalar"
    mortar_adv = "lambda_" + variable + "_" + adv_id
    mortar_diff = "lambda_" + variable + "_" + diff_id

    # save variable name for the post-process
    param["scalar"] = variable

    discr_adv = pp.Upwind(model_data_adv)
    discr_adv_interface = pp.CellDofFaceDofMap(model_data_adv)

    discr_diff = pp.Tpfa(model_data_diff)
    discr_diff_interface = pp.CellDofFaceDofMap(model_data_diff)

    coupling_adv = pp.UpwindCoupling(model_data_adv)
    coupling_diff = pp.FluxPressureContinuity(
        model_data_diff, discr_diff, discr_diff_interface
    )

    # mass term
    mass_id = "mass"
    discr_mass = pp.MassMatrix(model_data_adv)
    discr_mass_interface = pp.CellDofFaceDofMap(model_data_adv)

    discr_src = pp.ScalarSource(model_data_src)

    for g, d in gb:
        d[pp.PRIMARY_VARIABLES] = {variable: {"cells": 1}}
        if g.dim == gb.dim_max():
            d[pp.DISCRETIZATION] = {
                variable: {adv_id: discr_adv,
                diff_id: discr_diff,
                mass_id: discr_mass,
                src_id: discr_src}
            }
        else:
            d[pp.DISCRETIZATION] = {
                variable: {adv_id: discr_adv_interface,
                diff_id: discr_diff_interface,
                mass_id: discr_mass_interface}
            }

    for e, d in gb.edges():
        g_slave, g_master = gb.nodes_of_edge(e)
        d[pp.PRIMARY_VARIABLES] = {mortar_adv: {"cells": 1}, mortar_diff: {"cells": 1}}

        d[pp.COUPLING_DISCRETIZATION] = {
            adv_id: {
                g_slave: (variable, adv_id),
                g_master: (variable, adv_id),
                e: (mortar_adv, coupling_adv),
            },
            diff_id: {
                g_slave: (variable, diff_id),
                g_master: (variable, diff_id),
                e: (mortar_diff, coupling_diff),
            },
        }

    # setup the advection-diffusion problem
    assembler = pp.Assembler(gb, active_variables=[variable, mortar_diff, mortar_adv])
    logger.info("Assemble the advective and diffusive terms of the transport problem")
    block_A, block_b = assembler.assemble_matrix_rhs(add_matrices=False)
    logger.info("done")

    # unpack the matrices just computed
    diff_name = diff_id + "_" + variable
    adv_name = adv_id + "_" + variable
    mass_name = mass_id + "_" + variable
    source_name = src_id + "_" + variable

    diff_coupling_name = diff_id + "_" + mortar_diff + "_" + variable + "_" + variable
    adv_coupling_name = adv_id + "_" + mortar_adv + "_" + variable + "_" + variable

    # need a sign for the convention of the conservation equation
    M = block_A[mass_name]
    A = block_A[diff_name] + block_A[diff_coupling_name] + \
        block_A[adv_name] + block_A[adv_coupling_name]
    b = block_b[diff_name] + block_b[diff_coupling_name] + \
        block_b[adv_name] + block_b[adv_coupling_name] + \
        block_b[source_name]

    M_t = M.copy() / param["time_step"] * param.get("mass_weight", 1)
    M_r = M.copy() * param.get("reaction", 0)

    # Perform an LU factorization to speedup the solver
    IE_solver = sps.linalg.factorized((M_t + A + M_r).tocsc())

    # time loop
    logger.info("Prepare the exporting")
    save = pp.Exporter(gb, "solution", folder=param["folder"])
    logger.info("done")

    variables = [variable, param["pressure"], "frac_num", "cell_volumes"]
    if discr["scheme"] is pp.MVEM or discr["scheme"] is pp.RT0:
        variables.append(param["P0_flux"])

    # assign the initial condition
    x = np.zeros(A.shape[0])
    assembler.distribute_variable(x)
    for g, d in gb:
        if g.dim == gb.dim_max():
            d[pp.STATE][variable] = param.get("init_trans", 0) * np.ones(g.num_cells)

    x = assembler.merge_variable(variable)

    outflow = np.zeros(param["n_steps"])

    logger.info("Start the time loop with " + str(param["n_steps"]) + " steps")
    for i in np.arange(param["n_steps"]):
        logger.info("Solve the linear system for time step " + str(i))
        x = IE_solver(b + M_t.dot(x))
        logger.info("done")

        logger.info("Variable post-process")
        assembler.distribute_variable(x)
        logger.info("done")

        logger.info("Export variable")
        save.write_vtk(variables, time_step=i)
        logger.info("done")

        logger.info("Compute the production")
        outflow[i] = compute_outflow(gb, param)
        logger.info("done")

    time = np.arange(param["n_steps"]) * param["time_step"]
    save.write_pvd(time)

    logger.info("Save outflow on file")
    file_out = param["folder"] + "/outflow.csv"
    data = np.vstack((time, outflow)).T
    np.savetxt(file_out, data, delimiter=",")
    logger.info("done")

    logger.info("Save dof on file")
    file_out = param["folder"] + "/dof_transport.csv"
    np.savetxt(file_out, get_dof(assembler), delimiter=",", fmt="%d")
    logger.info("done")