Skip to content
Snippets Groups Projects
Select Git revision
  • 8e78757a1e50b5a1794bb6aa3c90011d29922afa
  • master default protected
  • parallelistation_of_subdomain_loop
  • parallelistation_test
  • nonzero_gli_term_for_nonwetting_in_TPR
  • testing_updating_pwi_as_pwiminus1_into_calculation_of_pnwi
  • v2.2
  • v2.1
  • v2.0
  • v1.0
10 results

domainPatch.py

Blame
  • domainPatch.py 70.09 KiB
    """Class definitions for subdomains representing soil patches for LDD-TPR simulation
    
    This file contains class definitions for the class DomainPatch which defines the
    subdomains used for the LDD simulation. Each subdomain of this class holds
    information about permeability, porosity, the model/physics used on the patch
    as well as information about which parts of the boundary are outer boundary of
    the global domain and  which parts are interfaces to neighboring soil patches of
    the same class.
    
    Additionally, a class BoundaryPart is defined, which accepts a List of dolfin
    Points vertices, representing the vertices of a 1D polygone. The class contains
    the method BoundaryPart.inside(meshpoint,on_boundary) which is used to mark
    faces of a given subdomain mesh that lie on the 1D-polygone defined by vertices.
    
    Assumed is the use of python 3.6
    """
    import dolfin as df
    import mshr
    import numpy as np
    # import domainPatch as dp
    import typing as tp
    import ufl as fl
    # Interface = tp.NewType('Interface', tp.any)
    #import domainPatch as domainPatch
    
    
    class DomainPatch(df.SubDomain):
        """ DomainPatch Class defines subdomains used in the LDD simulation
    
        The class DomainPatch defines subdomains for the LDD simulation. Each subdomain
        of this class holds information about the phyisics involved like viscosity,
        porosity, the model/physics that is assumed on the patch
        as well as information about which parts of the boundary are outer boundary of
        the global domain and  which parts are interfaces to neighboring soil patches of
        the same class.
        The constructor accepts
    
        # Parameters
        isRichards          #type: bool     return true if Richards model is
                                            assumed on patch.
    
        mesh                    #type df::Mesh & Dolfin mesh object, submesh of the
                                 subdomain.
    
        porosity                #type float   porosity of the the soil in the patch
                                 assumed to be constant in space.
    
        viscosity               #type tp.List[float]   viscosity(ies) of the the
                                 fluid phases in the patch viscosity[0] is assumed
                                 to be the viscosity of the wetting phase and if
                                 present (isRichards == False) viscosity[1] is
                                 assumed to be the viscosity of the non-wetting phase.
    
        interfaces:             #type tp.List[dp.Interface]   List of Interface class
                                                    objects from the LDDsimulation
                                                    class.
        has_interface           #type tp.List[int]: list of indices (w.r.t. the global
                                                    numbering of interfaces in
                                                    LDDsimulation.interfaces) indicating
                                                    which interfaces are part of the
                                                    subdomain.
    
        L                   #type tp.List[float]    L parameters for the L-scheme. In
                                                    case isRichards == False (i.e
                                                    two-phase flow is assumed on the
                                                    patch) we have two parameters, one
                                                    for each equation.
    
        lambda_param        #tp.List[float]         L parameters for the L-scheme. In
                                                    case isRichards == False (i.e
                                                    two-phase flow is assumed on the
                                                    patch) we have two parameters, one
                                                    for each equation.
    
    
        ## Public Variables
    
        isRichards              #type bool              set by input, see above
        mesh                    #type df.Mesh           set by input, see above
        porosity                #type float             set by input, see above
        viscosity               #type tp.List[float]    set by input, see above
        interfaces:             #type tp.List[dp.Interface] set by input, see above
        has_interface           #type tp.List[int]      set by input, see above
        L                       #type tp.List[float]    set by input, see above
        lambda_param            #type tp.List[float]    set by input, see above
        function_space          #type tp.Dict[str: df.Function]    function space
                                                        used for wetting
                                                        and nonwetting
                                                        pressures. This is
                                                        set by
                                                        self._init_function_space()
        parent_mesh_index       #type np.array          parent vertex map of the submesh.
        boundary_marker         #type df.MeshFunction   marker function to mark all
                                                        boundaries of the subdomain.
                                                        The marker value marking
                                                        interface i with global index
                                                        has_interface[i] will be
                                                        has_interface[i] aswell.
        ## Public Methods
    
    
        """
    
        ### constructor
        def __init__(self, #
                     subdomain_index: int,#
                     isRichards: bool,#
                     mesh: df.Mesh,#
                     porosity: float,#
                     viscosity: tp.Dict[str, float],#
                     outer_boundary_def_points: tp.Dict[str, tp.List[df.Point]],#
                     # tp.List[Interface] doesn't work, because it hasen't been defined yet.
                     interfaces: tp.List[object],#
                     has_interface: tp.List[int],#
                     L: tp.Dict[str, float],#
                     lambda_param: tp.Dict[str, float],#
                     relative_permeability: tp.Dict[str, tp.Callable[...,None]],#
                     saturation: tp.Callable[..., None],#
                     timestep_size: float,#
                     tol = None,#
                     ):
            # because we declare our own __init__ method for the derived class BoundaryPart,
            # we overwrite the __init__-method of the parent class df.SubDomain. However,
            # we need the methods from the parent class, so we call the __init__-method
            # from df.SubDomain to access it.
            df.SubDomain.__init__(self)
            if tol:
                self.tol = tol
            else:
                self.tol = df.DOLFIN_EPS
    
            ### Class Variables/Objects set by the input to the constructor
            self.subdomain_index = subdomain_index
            self.isRichards = isRichards
            self.mesh = mesh
            self.porosity = porosity
            self.viscosity = viscosity
            self.outer_boundary_def_points = outer_boundary_def_points
            self.interface = interfaces
            self.has_interface = has_interface
            self.L = L
            self.lambda_param = lambda_param
            self.relative_permeability = relative_permeability
            self.saturation = saturation
            # timestep size, tau in the paper
            self.timestep_size = timestep_size
            ### Class variables set by methods
            # dictionary holding the dof indices corresponding to an interface of
            # given interface. see self._calc_dof_indices_of_interfaces()
            self._dof_indices_of_interface = dict()
            # dictionary holding for each interface in self.has_interface the dof
            # indices that are shared with another interface (possibly none). This info is needed for
            # the calculation of the gli terms.
            self._interface_has_common_dof_indices = dict()
            # List of objects of clas outerBoundary initialised by self._init_markers()
            # can be NONE if no outer boundary is present.
            self.outer_boundary = []
            # measures are set by self._init_measures()
            self.iteration_number = 0
            self.dx = None
            self.ds = None
            # solution function(s) for the form set by _init_function_space
            self.pressure = None
            self.source = None
            # FEM function holding the previous iteration.
            self.pressure_prev_iter = None
            # FEM function holding the pressures of the previous timestep t_n.
            self.pressure_prev_timestep = None
            # test function(s) for the form set by _init_function_space
            self.testfunction = None
            # Dictionary of Arrays (one for 'wetting' and one for 'nonwetting')
            # corresponding to the dofs of an FEM function which is initiated
            # by calc_gli_term() holding the dofs of the assembled dt*gli*v*ds terms as
            # a sparse vector (here, dt = self.timestep_size). This vector is read by the solver and added to the rhs
            # which is given by self.govering_problem().
            #
            self.gli_assembled = dict()
            # Dictionary of FEM function of self.function_space holding the interface
            # dofs of the previous iteration of the neighbouring pressure. This is used
            # to assemble the gli terms in the method self.calc_gli_term().
            self.neighbouring_interface_pressure = None
    
            # sometimes we need to loop over the phases and the length of the loop is
            # determined by the model we are assuming
            if self.isRichards:
                self.has_phases =['wetting']
            else:
                self.has_phases =['wetting', 'nonwetting']
    
            self._init_function_space()
            self._init_dof_and_vertex_maps()
            self._init_markers()
            self._init_measures()
            self._calc_dof_indices_of_interfaces()
            self._calc_interface_has_common_dof_indices()
    
        # END constructor
    
        #### PUBLIC METHODS
        def write_pressure_to_interfaces(self, initial_values: bool = False):
            """ save the interface values of self.pressure to all neighbouring interfaces
    
            This method is used by the LDDsimulation.LDDsolver method.
    
            # parameters
            initial_values      bool        Flag to toggle wether to write self.pressure
                                            or self.pressure_prev_iter to the interface.
                                            This is needed for the first ever iteration
                                            at time = 0.
            """
            if initial_values:
                # this assumes, that self.pressure_prev_iter has been set by
                # LDDsimulation._init_initial_values().
                from_func = self.pressure_prev_iter
            else:
                from_func = self.pressure
    
            for ind in self.has_interface:
                for phase in self.has_phases:
                    self.interface[ind].read_pressure_dofs(from_function = from_func[phase], #
                                            interface_dofs = self._dof_indices_of_interface[ind][phase],#
                                            dof_to_vert_map = self.dof2vertex[phase],#
                                            local_to_parent_vertex_map = self.parent_mesh_index,#
                                            phase = phase,#
                                            subdomain_ind = self.subdomain_index)
    
    
        def governing_problem(self, phase: str) -> tp.Dict[str, fl.Form]:
            """ return the governing form and right hand side for phase phase as a dictionary.
    
            return the governing form ant right hand side for phase phase (without the gli terms) as a dictionary.
            CAUTION: the gli terms are not assembled in this method and need to be
            calculated by the solver separately and subtracted from the rhs!
            """
            # define measures
            dx = self.dx
            ds = self.ds
            dt = self.timestep_size
            porosity = self.porosity
            if self.isRichards:
                # this assumes that atmospheric pressure has been normalised to 0.
                pc_prev_iter = self.pressure_prev_iter['wetting']
                pc_prev_timestep = self.pressure_prev_timestep['wetting']
                if phase == 'nonwetting':
                    print("CAUTION: You invoked domainPatch.governing_problem() with\n", #
                    "Parameter phase = 'nonwetting'. But isRichards = True for",
                    "current subdomain. \nResetting phase = 'wetting'.\n")
                    phase = 'wetting'
            else:
                pw_prev_iter = self.pressure_prev_iter['wetting']
                pnw_prev_iter = self.pressure_prev_iter['nonwetting']
                pw_prev_timestep = self.pressure_prev_timestep['wetting']
                pnw_prev_timestep = self.pressure_prev_timestep['nonwetting']
                pc_prev_iter = pnw_prev_iter - pw_prev_iter
                pc_prev_timestep = pnw_prev_timestep - pw_prev_timestep
    
            La = self.L[phase]
            pa = self.trialfunction[phase]
            # this is pw_{i-1} in the paper
            pa_prev_iter = self.pressure_prev_iter[phase]
            phi_a  = self.testfunction[phase]
            Lambda_a = self.lambda_param[phase]
            ka = self.relative_permeability[phase]
            S = self.saturation
            mu_a = self.viscosity[phase]
            source_a = self.source[phase]
    
            # the first part of the form and rhs are the same for both wetting and nonwetting
            form1 = (La*pa*phi_a)*dx
            rhs1 = (La*pa_prev_iter*phi_a)*dx
            # the forms of wetting and nonwetting phase differ by a minus sign
            # and by interface_form terms if the neighbour of the current patch
            # assumes Richards model
            if phase == "wetting":
                # gather interface forms
                interface_forms = []
                for interface in self.has_interface:
                    # interface_forms += (dt*Lambda_a*pa*phi_a)*ds(interface)
                    interface_forms.append((dt*Lambda_a*pa*phi_a)*ds(interface))
                # form2 is different for wetting and nonwetting
                form2 = dt/mu_a*df.dot(ka(S(pc_prev_iter))*df.grad(pa), df.grad(phi_a))*dx
                rhs2 = -(porosity*(S(pc_prev_iter) - S(pc_prev_timestep))*phi_a)*dx
            elif phase == "nonwetting":
                # gather interface forms
                interface_forms = []
                for interface in self.has_interface:
                    if self.interface[interface].neighbour_isRichards[self.subdomain_index]:
                        # if the neighbour of our subdomain (with index self.subdomain_index)
                        # assumes a Richards model, we assume constant normalised atmospheric pressure
                        # and no interface term is practically appearing.
                        # interface_forms += (df.Constant(0)*phi_a)*ds(interface)
                        interface_forms.append((df.Constant(0)*phi_a)*ds(interface))
                    else:
                        # interface_forms += (dt*Lambda_a*pa*phi_a)*ds(interface)
                        interface_forms.append((dt*Lambda_a*pa*phi_a)*ds(interface))
                form2 = dt/mu_a*df.dot(ka(1 - S(pc_prev_iter))*df.grad(pa), df.grad(phi_a))*dx
                # the non wetting phase has a + before instead of a - before the bracket
                rhs2 = (porosity*(S(pc_prev_iter) - S(pc_prev_timestep))*phi_a)*dx
            else:
                raise RuntimeError('missmatch for input parameter phase.',
                'Expected either phase == wetting or phase == nonwetting ')
                return None
            form = form1 + form2 + sum(interface_forms)
            rhs_without_gli = rhs1 + rhs2 + dt*source_a*phi_a*dx
            form_and_rhs = {#
                'form': form,#
                'rhs_without_gli' : rhs_without_gli
            }
            return form_and_rhs
    
    
        def calc_gli_term(self) -> None: # tp.Dict[str, fl.Form]
            """calculate the gl terms for the iteration number of the subdomain
    
            calculate the gl terms for the iteration number of the subdomain and save them
            to self.gli_assembled. The term gets saved as an already assembled sparse
            ds form to be added to the rhs by the solver. This is due to the need of
            having to communicate dofs over the interface. Additionally, the dofs are
            being saved to an interface dictionary exactly as the pressures.
    
            Caution: The array self.gli_assembled needs to be added to the rhs with
            a minus sign by the solver!
            """
            iteration = self.iteration_number
            subdomain = self.subdomain_index
            dt = self.timestep_size
            Lambda = self.lambda_param
            # since we loop over all interfaces in self.has_interface we need a temporary
            # vector (numpy array) that we will use to add all dofs of the gli terms
            # into one array.
            gli_assembled_tmp = dict()
            if self.isRichards:
                # print("number of dofs: ", self.pressure_prev_iter['wetting'].vector()[:].size)
                gli_assembled_tmp = {
                    'wetting': np.zeros(self.pressure['wetting'].vector()[:].size, dtype=float)
                    }
            else:
                gli_assembled_tmp = {
                    'wetting': np.zeros(self.pressure['wetting'].vector()[:].size, dtype=float),
                    'nonwetting': np.zeros(self.pressure['nonwetting'].vector()[:].size, dtype=float)
                    }
            for ind in self.has_interface:
                interface = self.interface[ind]
                # neighbour index
                neighbour = interface.neighbour[subdomain]
                neighbour_iter_num = interface.current_iteration[neighbour]
                ds = self.ds(ind)
                # needed for the read_gli_dofs() functions
                interface_dofs = self._dof_indices_of_interface[ind]
                # for each interface, this is a list of dofs indices belonging to another
                # interface. The dofs corresponding to these indices must be multiplied
                # by 1/2 to to get an average of the gli terms for dofs belonging to
                # two different interfaces.
                dofs_in_common_with_other_interfaces = self._interface_has_common_dof_indices[ind]
    
                if iteration == 0:
                    n = df.FacetNormal(self.mesh)
                    pw_prev_iter = self.pressure_prev_timestep
                    k = self.relative_permeability
                    S = self.saturation
                    mu = self.viscosity
    
                    if not neighbour_iter_num == 0:
                        # save the gl term from the neighbour first to use it in the
                        # next iteration
                        interface.gli_term_prev[subdomain].update(#
                            {'wetting': interface.gli_term[neighbour]['wetting']}
                            )
                        # in case we have two-phase and the neighbour is two-phase, two,
                        # we need to Additionally save the nonwetting values.
                        if not interface.neighbour_isRichards[subdomain] and not self.isRichards:
                            interface.gli_term_prev[subdomain].update(#
                                {'nonwetting': interface.gli_term[neighbour]['nonwetting']}
                                )
    
                    if self.isRichards:
                        # assuming that we normalised atmospheric pressure to zero,
                        # pc = pw in the Richards case.
                        pc_prev = pw_prev_iter['wetting']
                    else:
                        pw_prev = pw_prev_iter['wetting']
                        pnw_prev = pw_prev_iter['nonwetting']
                        pc_prev = pnw_prev - pw_prev
    
                    for phase in self.has_phases:
                        # viscosity
                        mu_a = mu[phase]
                        # relative permeability
                        ka = k[phase]
                        # previous pressure iteration
                        pa_prev = pw_prev_iter[phase]
                        # testfunction
                        phi_a = self.testfunction[phase]
                        if phase == 'wetting':
                            # the wetting phase is always present and we always need
                            # to calculate a gl0 term.
                            # TODO: add intrinsic permeabilty!!!!
                            # TODO: add gravitiy term!!!!!
                            flux = -1/mu_a*ka(S(pc_prev))*df.grad(pa_prev)
                            gl0 = (df.dot(flux, n) - Lambda[phase]*pa_prev)
                            gl0_assembled = df.assemble(dt*gl0*phi_a*ds).get_local()
                            # if dofs_in_common_with_other_interfaces[phase].size == 0
                            # there are no dofs that lie on another interface and we can
                            # skip the following step.
                            if dofs_in_common_with_other_interfaces[phase].size != 0:
                                for common_dof in dofs_in_common_with_other_interfaces[phase]:
                                    # from the standpoint of the subdomain we are currently on,
                                    # each dof can belong maximally to two interfaces. On these
                                    # vertices, common to two interfaces, the dofs of the
                                    # corresponding gli terms of both interfaces are averaged
                                    # to give one value for this vertex. Because we achieve this
                                    # by adding the assembled gli terms together we have to
                                    # multiply the dofs that appear in more than one interface
                                    # by 1/2.
                                    gl0_assembled[common_dof] = 0.5*gl0_assembled[common_dof]
                            # now, add the just assembled and corrected term to the Global
                            # gli_assembled_tmp vector
                            gli_assembled_tmp[phase] += gl0_assembled
                        else:
                            # phase == 'nonwetting'
                            # if we are in the two-phase case but our neighbour assumes
                            # Richards equation, we hase a zero communication term.
                            if interface.neighbour_isRichards[subdomain]:
                                # if the neighbour of our subdomain (with index self.subdomain_index)
                                # assumes a Richards model, we don't have gli terms for the
                                # nonwetting phase and assemble the terms as zero form.
                                gli_assembled_tmp[phase] += df.assemble(df.Constant(0)*ds).get_local()
                            else:
                                # in this case the neighbour assumes two-phase flow
                                # and we need to calculte a gl0 torm for the nonwetting
                                # phase.
                                # we need anothre flux in this case
                                flux = -1/mu_a*ka(1-S(pc_prev))*df.grad(pa_prev)
                                gl0 = (df.dot(flux, n) - Lambda[phase]*pa_prev)
                                gl0_assembled = df.assemble(dt*gl0*phi_a*ds).get_local()
    
                                if dofs_in_common_with_other_interfaces[phase].size != 0:
                                    for common_dof in dofs_in_common_with_other_interfaces[phase]:
                                        # see previous case.
                                        gl0_assembled[common_dof] = 0.5*gl0_assembled[common_dof]
                                # now, add the just assembled and corrected term to the Global
                                # gli_assembled_tmp vector
                                gli_assembled_tmp[phase] += gl0_assembled
    
                        # writing out glo to the current iteration dictionary of the
                        # interface takes place after the loop, see below.
    
                    # END for loop over phases
                    interface.current_iteration[subdomain] += 1
    
                else: # iteration > 0
                    # the wetting phase update is always to be calculated even if
                    # only Richards is assumed on the current patch or the neighbour
                    # only assumes the Richards equation.
    
                    # in case neighbour_iter_num == iteration we need to first save
                    # the gli_term of the neighbour to gli_term_prev.
                    if neighbour_iter_num == iteration:
                        # in this case, first save the gli_terms of the neighbour to
                        # gli_prev_term of the current subdomain. This is because
                        # we assume, that the neighbouring patch has done a previous
                        # iteration and we need the value to calculate the gli term with
                        interface.gli_term_prev[subdomain].update(#
                            {'wetting': interface.gli_term[neighbour]['wetting']}
                            )
    
                        if not interface.neighbour_isRichards[subdomain] and not self.isRichards:
                            interface.gli_term_prev[subdomain].update(#
                                {'nonwetting': interface.gli_term[neighbour]['nonwetting']}
                                )
    
                    for phase in self.has_phases:
                        # in case phase == 'nonwetting' only proceed if the neighbouring
                        # subdomain does not assume a Richards model.
                        if (phase == 'wetting') or (not interface.neighbour_isRichards[subdomain]):
                            # then read gli_prev term from interface and write it to
                            # the gli_assembled array of the subdomain
                            # to be able to sum the gli terms of all interfaces together,
                            # we need a dummy vector to hold the dofs read from the interface.
                            gli_readout_tmp = np.zeros(gli_assembled_tmp[phase].size, dtype = float)
                            interface.write_gli_dofs(to_array = gli_readout_tmp, #
                                           interface_dofs = interface_dofs[phase],#
                                           dof_to_vert_map = self.dof2vertex[phase],#
                                           local_to_parent_vertex_map = self.parent_mesh_index,#
                                           phase = phase,#
                                           subdomain_ind = subdomain,#
                                           # this is set to True because we need gli_prev
                                           previous_iter = True
                                           )
    
                            # read neighbouring pressure values from interface and write them to
                            # the function self.neighbouring_interface_pressure
                            interface.write_pressure_dofs(to_function = self.neighbouring_interface_pressure[phase], #
                                           interface_dofs = interface_dofs[phase],#
                                           dof_to_vert_map = self.dof2vertex[phase],#
                                           local_to_parent_vertex_map = self.parent_mesh_index,#
                                           phase = phase,#
                                           subdomain_ind = neighbour,#
                                           previous_iter = False)
    
                            # use the above to calculate the gli update with
                            # in the paper p^{i-1}_{3-l}
                            pa_prev = self.neighbouring_interface_pressure[phase]
                            phi_a  = self.testfunction[phase]
                            gli_pressure_part = df.assemble(-2*dt*Lambda[phase]*pa_prev*phi_a*ds)
                            gli_p_part = gli_pressure_part.get_local()
                            gli_prev_neighbour = gli_readout_tmp
                            gli = gli_p_part + gli_prev_neighbour
                            # check if there are shared dofs with another interface.
                            if dofs_in_common_with_other_interfaces[phase].size != 0:
                                for common_dof in dofs_in_common_with_other_interfaces[phase]:
                                    # see previous case.
                                    gli[common_dof] = 0.5*gli[common_dof]
                            # now, add the just assembled and corrected term to the Global
                            # gli_assembled_tmp vector
                            gli_assembled_tmp[phase] += gli
    
                    if neighbour_iter_num -1 == iteration:
                        # gli_term_prev has been used by the above and can now safely
                        # be overwritten with the gli_term of the neighbour.
                        interface.gli_term_prev[subdomain].update(#
                            {'wetting': interface.gli_term[neighbour]['wetting']}
                            )
    
                        if not interface.neighbour_isRichards[subdomain] and not self.isRichards:
                            interface.gli_term_prev[subdomain].update(#
                                {'nonwetting': interface.gli_term[neighbour]['nonwetting']}
                                )
                    else:
                        print(f"neighbour_iter_num = {neighbour_iter_num} for neighbour = {neighbour}\n",
                        f"and iteration = {iteration} on subdomain {subdomain}\n",
                        "It is expected that this case should only be happening if the \n",
                        f"neighbour (index {neighbour}) has already finished its iteration, i.e. reached\n",
                        f"the error criterion. In this case we should have {iteration} > {neighbour_iter_num}.\n",
                        "If this is not the case revisite DomainPatch.calc_gli_term().")
    
                    # finally we are done and can step up the current iteration count.
                    interface.current_iteration[subdomain] += 1
    
            ### END for ind in self.has_interface:
            # save the result of the calculation to the subdomain and to the interface
            # dictionaries.
            if self.isRichards:
                self.gli_assembled.update(
                    {'wetting': gli_assembled_tmp['wetting']}
                    )
                for ind in self.has_interface:
                    # self._calc_gli_term_on(interface, iteration)
                    interface = self.interface[ind]
                    # write gli dofs to interface dicts for communiction
                    interface.read_gli_dofs(from_array = self.gli_assembled['wetting'], #
                                   interface_dofs = interface_dofs['wetting'],#
                                   dof_to_vert_map = self.dof2vertex['wetting'],#
                                   local_to_parent_vertex_map = self.parent_mesh_index,#
                                   phase = 'wetting',#
                                   subdomain_ind = subdomain,#
                                   previous_iter = False
                                   )
            else:
                self.gli_assembled.update({
                    'wetting': gli_assembled_tmp['wetting'],
                    'nonwetting': gli_assembled_tmp['nonwetting']
                    })
                for ind in self.has_interface:
                    # self._calc_gli_term_on(interface, iteration)
                    interface = self.interface[ind]
                    for phase in ['wetting', 'nonwetting']:
                        # write gli dofs to interface dicts for communiction
                        interface.read_gli_dofs(from_array = self.gli_assembled[phase], #
                                       interface_dofs = interface_dofs[phase],#
                                       dof_to_vert_map = self.dof2vertex[phase],#
                                       local_to_parent_vertex_map = self.parent_mesh_index,#
                                       phase = phase,#
                                       subdomain_ind = subdomain,#
                                       previous_iter = False
                                       )
        ### END calc_gli_term
    
        def null_all_interface_iteration_numbers(self) -> None:
            """ reset interface.current_iteration[subdomain] = 0 for all interfaces
            of the subdomain.
            """
            subdomain = self.subdomain_index
            for index in self.has_interface:
                self.interface[index].current_iteration[subdomain] = 0
    
        #### PRIVATE METHODS
        def _init_function_space(self) -> None:
            """ create function space for solution and trial functions
    
            Note that P1 FEM is hard coded here, as it is assumed in other methods
            aswell. This method sets the class variable
                self.function_space
                self.pressure
                self.testfunction
            """
            if self.isRichards:
                self.function_space = {'wetting': df.FunctionSpace(self.mesh, 'P', 1)}
                self.trialfunction = {
                    'wetting': df.TrialFunction(self.function_space['wetting'])
                    }
                self.testfunction = {
                    'wetting': df.TestFunction(self.function_space['wetting'])
                    }
                self.neighbouring_interface_pressure = {
                    'wetting' : df.interpolate(df.Constant(0), self.function_space['wetting'])
                    }
            else:
                self.function_space = {#
                    'wetting'    : df.FunctionSpace(self.mesh, 'P', 1),#
                    'nonwetting' : df.FunctionSpace(self.mesh, 'P', 1)#
                    }
                self.trialfunction = {
                    'wetting'    : df.TrialFunction(self.function_space['wetting']),
                    'nonwetting' : df.TrialFunction(self.function_space['nonwetting'])
                    }
                self.testfunction = {
                    'wetting'    : df.TestFunction(self.function_space['wetting']),
                    'nonwetting' : df.TestFunction(self.function_space['nonwetting'])
                    }
                self.neighbouring_interface_pressure = {#
                    'wetting' : df.interpolate(df.Constant(0), self.function_space['wetting']),#
                    'nonwetting' : df.interpolate(df.Constant(0), self.function_space['nonwetting'])
                }
    
        def _init_dof_and_vertex_maps(self) -> None:
            """ calculate dof maps and the vertex to parent map.
    
            This method sets the class Variables
                self.parent_mesh_index
                self.dof2vertex
                self.vertex2dof
            """
            mesh_data = self.mesh.data()
            # local to parent vertex index map
            self.parent_mesh_index = mesh_data.array('parent_vertex_indices',0)
            if self.isRichards:
                self.dof2vertex = {#
                    'wetting' :df.dof_to_vertex_map(self.function_space['wetting'])#
                    }
                self.vertex2dof = {#
                    'wetting' :df.vertex_to_dof_map(self.function_space['wetting'])#
                    }
            else:
                self.dof2vertex = {#
                    'wetting'   :df.dof_to_vertex_map(self.function_space['wetting']),#
                    'nonwetting':df.dof_to_vertex_map(self.function_space['nonwetting'])#
                    }
                self.vertex2dof = {#
                    'wetting'   :df.vertex_to_dof_map(self.function_space['wetting']),#
                    'nonwetting':df.vertex_to_dof_map(self.function_space['nonwetting'])#
                    }
    
        def _init_markers(self) -> None:
            """ define boundary markers
    
            This method sets the class Variables
                self.interface_marker
                self.outer_boundary_marker
    
            """
            self.outer_boundary_marker = df.MeshFunction('size_t', self.mesh, self.mesh.topology().dim()-1)
            self.outer_boundary_marker.set_all(0)
            self.interface_marker = df.MeshFunction('size_t', self.mesh, self.mesh.topology().dim()-1)
            self.interface_marker.set_all(0)
            for glob_index in self.has_interface:
                # each interface gets marked with the global interface index.
                self.interface[glob_index].mark(self.interface_marker, glob_index+1)
            # create outerBoundary objects and mark outer boundaries with 1
            for index, boundary_points in self.outer_boundary_def_points.items():
                # if our domain patch has no outer boundary, this should be reflected
                # by the value None in the dictionary
                if boundary_points is None:
                    #
                    self.outer_boundary = None
                else:
                    self.outer_boundary.append(#
                        BoundaryPart(vertices = boundary_points,#
                            internal = False,
                            tol= self.tol) #self.mesh.hmin()/100
                    )
                    self.outer_boundary[index].mark(self.outer_boundary_marker, 1)
    
    
        def _init_measures(self):
            """ define measures for the governing form
    
            This method sets the class Variables
                self.dx
                self.ds
            """
            # domain measure
            self.dx = df.Measure('dx', domain = self.mesh)
            # measure over the interfaces
            self.ds = df.Measure('ds', domain = self.mesh, subdomain_data = self.interface_marker)
    
        def _calc_dof_indices_of_interfaces(self):
            """ calculate dof indices of each interface """
            V = self.function_space
            marker = self.interface_marker
            for ind in self.has_interface:
                # the marker value for the interfaces is always global interface index+1
                marker_value = ind + 1
                self._dof_indices_of_interface.update(#
                    {ind: dict()}
                )
                for phase in self.has_phases:
                    self._dof_indices_of_interface[ind].update(#
                        {phase: self.interface[ind].dofs_on_interface(V[phase], marker, marker_value)}
                    )
                    print(f"subdomain{self.subdomain_index}: dofs on interface {ind}:", self._dof_indices_of_interface[ind][phase])
                # if self.isRichards:
                #     self._dof_indices_of_interface[ind].update(#
                #         {'wetting': self.interface[ind].dofs_on_interface(V['wetting'], marker, marker_value)}
                #     )
                #     print(f"subdomain{self.subdomain_index}: dofs on interface {ind}:", self._dof_indices_of_interface[ind]['wetting'])
                # else:
                #     self._dof_indices_of_interface[ind].update(#
                #         {'wetting': self.interface[ind].dofs_on_interface(V['wetting'], marker, marker_value),#
                #          'nonwetting': self.interface[ind].dofs_on_interface(V['nonwetting'], marker, marker_value)}
                #     )
    
        def _calc_interface_has_common_dof_indices(self):
            """ populate dictionary self._interface_has_common_dof_indices
    
            Determin for each interface in self.has_interface the dof indices that
            also belong to another interface and store them in the dictionary
            self._interface_has_common_dof_indices
            This method populates the dictionary
                self._interface_has_common_dof_indices
            """
            for interf_ind, interf_dofs in self._dof_indices_of_interface.items():
                # initialise bool numpy arry for the search.
                if self.isRichards:
                    is_part_of_neighbour_interface_w = np.zeros(interf_dofs['wetting'].size, dtype=bool)
                else:
                    is_part_of_neighbour_interface_w = np.zeros(interf_dofs['wetting'].size, dtype=bool)
                    is_part_of_neighbour_interface_nw = np.zeros(interf_dofs['nonwetting'].size, dtype=bool)
    
                for other_interf_ind, other_interf_dofs in self._dof_indices_of_interface.items():
                    if other_interf_ind is not interf_ind:
                        if self.isRichards:
                            is_part_of_neighbour_interface_w += np.isin(interf_dofs['wetting'], other_interf_dofs['wetting'], assume_unique=True)
                        else:
                            is_part_of_neighbour_interface_w += np.isin(interf_dofs['wetting'], other_interf_dofs['wetting'], assume_unique=True)
                            is_part_of_neighbour_interface_nw += np.isin(interf_dofs['nonwetting'], other_interf_dofs['nonwetting'], assume_unique=True)
                # this carries a numpyarray of the indices which belong to other
                # interfaces, too
                self._interface_has_common_dof_indices.update({interf_ind: dict()})
                if self.isRichards:
                    self._interface_has_common_dof_indices[interf_ind].update(
                        {'wetting': interf_dofs['wetting'][is_part_of_neighbour_interface_w]}
                    )
                else:
                    self._interface_has_common_dof_indices[interf_ind].update(
                        {'wetting': interf_dofs['wetting'][is_part_of_neighbour_interface_w]},
                        {'nonwetting': interf_dofs['nonwetting'][is_part_of_neighbour_interface_nw]}
                    )
    
    
    
    
    # END OF CLASS DomainPatch
    
    
    class BoundaryPart(df.SubDomain):
        """BoundaryPart class defines method self.inside to mark 1D polygonal parts
           of the boundary.
    
        The class BoundaryPart defines the method self.inside, which returns boolean
        True if a point x on the mesh of a given 2D polygonal subdomain lies on the
        part of the 1D polygonal subdomain boundary defined by the dolfin points in
        the array vertices.
    
        # Parameters
        vertices           #type List[df.Points], dolfin points defining a polyongal
                            part of the domain boundary.
        tol                #type float, the calculation tolerance
        internal           #type bool,  set weather the 1D polygone defined by vertices
                            is considered internal (an interface) or really a part
                            of the boundary. This influences how the public method
                            inside is defined.
        """
    
        def __init__(self, #
                     vertices: tp.List[df.Point], #
                     tol = None, #
                     internal: bool = False):
            # because we declare our own __init__ method for the derived class BoundaryPart,
            # we overwrite the __init__-method of the parent class df.SubDomain. However,
            # we need the methods from the parent class, so we call the __init__-method
            # from df.SubDomain to access it.
            super().__init__()
    
            self.vertices = vertices
            # if tol is not None, set it to passed value, else keep the default
            if tol:
                self.tol = tol
            else:
                self.tol = df.DOLFIN_EPS
            self.internal = internal
    
        ###### Public interface
        def inside(self, x: np.ndarray, on_boundary) -> bool:
            """inside returns true if x is on_boundary and on the part of the boundary
               defined by vertices
    
            # Parameters
            self         #type class BoundaryPart, reference to the class object
            x            #type np.ndarray,   grid point, used by parent class method df.SubDomain.mark.
            """
            if self.internal: # don't include test for on_boundary
                return self._is_on_boundary_part(x) #on_boundary and
    
            # include test for on_boundary in the default internal=False case
            return on_boundary and self._is_on_boundary_part(x)
        # END inside
    
        def coordinates(self, #
                        boundary_marker: df.MeshFunction, #
                        boundary_marker_value: int,#
                        print_coordinates: bool = False) -> np.array:
            """ return coordinates and numbering indices of nodes on mesh marked by
                boundary_marker with value boundary_marker.
    
            coordinates() returns coordinates and numbering indices of nodes on
            the mesh marked by boundary_marker with value
            boundary_marker. boundary_marker is a dolfin.MeshFunction of dimension
            boundary_marker.mesh().topology().dim()-1 that is supposed to mark edges
            of the mesh according to the method self.inside().
    
            # Parameters
            boundary_marker:        #type df.MeshFunction   marker function marking edges of i_mesh
                                                            according to self.inside
            boundary_marker_value:  #type int               marker value of boundary_marker
            print_coordinates       #type boolean           specify wether to print out the
                                                            coordinates or not.
            """
            mesh_coordinates = boundary_marker.mesh().coordinates()
            mesh_dimension = boundary_marker.mesh().topology().dim()
            # boundary_marker is a function defined on edges. Therefore
            # sum(boundary_marker.array() == boundary_marker_value) gives the number
            # of edges on the boundary. We need the number of nodes, however.
            number_of_boundary_nodes = sum(boundary_marker.array() == boundary_marker_value) + 1
            # we need one mesh_dimension + 1 columns to store the the index of the node.
            node_coordinates = np.zeros(shape = (number_of_boundary_nodes, mesh_dimension + 1)) #, dtype=float
            node_num = 0
            mesh_vertex_index = 0
            for x in mesh_coordinates:
                if self._is_on_boundary_part(x):
                    node_coordinates[node_num,:] = [x[0], x[1], mesh_vertex_index]
                    node_num += 1
    
                mesh_vertex_index += 1
    
            if print_coordinates:
                print("Interface Coordinates: \n", node_coordinates)
    
            return node_coordinates
    
    
        ###### Private methods
        def _is_on_boundary_part(self, point: np.ndarray) -> bool:
            """_is_on_boundary_part returns true if dist(point,boundary_part) < tol
    
            The function _is_on_boundary_part checks if point (a 2D/3D point) is
            close or on the part of the boundary defined by the array of dolfin points in vertices.
    
            # Parameters
            point          #type np.ndarray,    grid point to be testet.
            """
            #print("Annotations:", self._is_on_boundary_part.__annotations__)
            #print("Documentation String:", self._is_on_boundary_part.__doc__)
            tol = self.tol
    
            p = point
            # print("length of vertices:", len(self.vertices))
            # print("range(len(self.vertices))", range(len(self.vertices)))
            # check if p lies between p1 und p2 for all subsequent points in vertices
            for i in range(len(self.vertices) - 1):
                p1 = self.vertices[i]
                p2 = self.vertices[i+1]
                if self._is_on_line_segment(p1, p2, p):
                    return True
    
            # if _is_on_boundary_part reaches to here, p didn't lie on any line segment.
            return False
    
        def _is_on_line_segment(self, point1: df.Point, #
                               point2: df.Point, #
                               point: df.Point, #
                               dimension: int = 2) -> bool:
            """_is_on_line_segment returns true if dist(point,(point2 - point1)) < tol
    
            The function _is_on_line_segment checks if point (a 2D/3D point) is
            close or on the line segment defined by the two points point2 and point1.
            """
            #print("Annotations:", is_on_line_segment.__annotations__)
            #print("Documentation String:", is_on_line_segment.__doc__)
            tol = self.tol
            # point comes from the mesh and is inserted by the df.SubDomain.mark method.
            # It is already and np.ndarray and in the desired format.
            p =  point
            # point1 and point2 are df.Points. numpy functions expect points as
            # coordinate arrays and not as dolfin.Points.
            # Therefore we convert dolfin.Points to coordinate arrays.
            p1 = point1.array()
            p2 = point2.array()
            # dolfin Points are by default 3D. Zeros get appended to the 2d points we
            # used to define our subdomains with when using point1.array().
            # In 2d we cut this off.
            if dimension == 2:
                p1 = np.array([p1[0], p1[1]])
                p2 = np.array([p2[0], p2[1]])
    
            # check if p is either p1 or p2
            xmin = min(p1[0], p2[0])
            xmax = max(p1[0], p2[0])
            ymin = min(p1[1], p2[1])
            ymax = max(p1[1], p2[1])
            # print(f"test if point {p} is on line segment between {p1} or {p2}")
            # equality check
            if np.absolute((p[0] - xmax)*(p[0] - xmin)) < tol and np.absolute((p[1] - ymax)*(p[1] - ymin)) < tol:
                #print(f"point {p} is close to either {p1} or {p2}")
                return True
    
            # check there holds p1[0] < p[0] < p2[0]. If not, p  cannot be on the line segment
    
            # the test ((p[0] - xmax)*(p[0] - xmin) < 0) might fail if p[0] and one of p1[0]
            # or p2[0] are equal. In pp1 or pp2 is vertical. We need to still calculate
            # the distance to the line segment in this case
            if ((p[0] - xmax)*(p[0] - xmin) < tol) or np.absolute((p[0] - xmax)*(p[0] - xmin)) < tol:
                # here we have a chance to actually lie on the line segment.
                segment_direction = (p2 - p1)/np.linalg.norm(p2 - p1)
                distance = (p - p1) - np.dot(segment_direction, p - p1)*segment_direction
                # check again for equality with p1 and p2 just to be sure. =)
                if (np.linalg.norm(p - p1) < tol) or (np.linalg.norm(p - p2) < tol):
                    # print(f"grid node at {p} was close to either {p1} or {p2}")
                    return True
                elif np.linalg.norm(distance) < tol:
                    # print(f"point to be tested: p = {p}")
                    # print(f"the distance vector is: {distance}")
                    # print(f" Distance of grid node at {p} was close to segment {p1} - {p2}")
                    return True
                else:
                    return False
            else:
                return False
    
        def _print_annotations(self):
            print("Annotations:", self.__annotations__)
            print("Documentation String:", self.__doc__)
    
    ### End Class BoundaryPart
    
    class Interface(BoundaryPart):
        """Interface class adds interface specific methods like dof mapping from
            neighbouring domains to the class BoundaryPart.
    
        The class Interface builds on the class BoundaryPart, but adds mesh dependent
        functionality like vertex number mappings of the interface for the two neighbouring
        subdomains.
    
        # Parameters
        domain_index            #type int,   (global) index of the patch we are currently on
        adjacent_subdomains     #type int,   (global) index of the neighbour patch
                                of whitch this interface is an interface to.
        """
        def __init__(self, #
                    domain_index: int = None,#
                    adjacent_subdomains: np.array = None,#
                    # this is an array of bools
                    isRichards: np.array = None,#
                    **kwds):
            #
            self.isRichards = isRichards
            self.adjacent_subdomains = adjacent_subdomains
            # pressure_values is a dictionary of dictionaries, one for each of the
            # adjacent subdomains, holding the dof values over the interface of the
            # pressure (solution) values for communication. It is initialised by the
            # method init_interface_values().
            self.pressure_values = None
            self.pressure_values_prev = None
            # dictionaries holding the forms for the decoupling terms
            self.gli_term = None
            self.gli_term_prev = None
            # dictionary holding the current iteration number i for each of the adjacent
            # subdomains with index in adjacent_subdomains
            self.current_iteration = {#
                self.adjacent_subdomains[0]: 0,#
                self.adjacent_subdomains[1]: 0
            }
            # dictionary holding a bool variable for each subdomain index in self.adjacent_subdomains
            # indicating wether the neighbouring subdomain w.r.t. that index assumes
            # Richards model or not.
            self.neighbour_isRichards = {
                self.adjacent_subdomains[0]: self.isRichards[self.adjacent_subdomains[1]],#
                self.adjacent_subdomains[1]: self.isRichards[self.adjacent_subdomains[0]]
            }
            # dictionary holding the neighbouring domain index relative to the key index
            # this is used for calculating the gl_terms.
            self.neighbour = {
                self.adjacent_subdomains[0]: self.adjacent_subdomains[1],#
                self.adjacent_subdomains[1]: self.adjacent_subdomains[0]#
            }
    
    
            # make the parent class methods available.
            super().__init__(**kwds)
    
        #### Public Methods #####
        # def set_domain_index(self, domain_index: int):
        #     self.domain_index = domain_index
    
        def dofs_on_interface(self,#
                            function_space: df.FunctionSpace,#
                            interface_marker: df.MeshFunction, #
                            interface_marker_value: int) -> np.array:
            """ return the indices of the degrees of freedom on the interface.
    
            dofs_on_interface() returns the indices of the degrees of freedom on the
            interface with respect to the dof numbering on function_space.mesh().
            In essence, it is the restriction of the dof_to_vertex_map to the interface.
    
            # Parameters
            function_space:         #type df.functionSpace  the function space of whitch
                                                            to calculate the dof_to_vertex_map
                                                            restriction from.
            interface_marker:       #type df.MeshFunction   marker function marking edges of i_mesh
                                                            according to self.inside
            interface_marker_value: #type int               marker value of interface_marker
            """
            interface_vertex_indices = self._vertex_indices(interface_marker, interface_marker_value)
            dof2vertex = df.dof_to_vertex_map(function_space)
            # check which dofs actually lie on the interface using the dof2vertex map.
            is_a_dof_on_interface = np.isin(dof2vertex, interface_vertex_indices, assume_unique=True)
    
            return np.nonzero(is_a_dof_on_interface)[0]
    
        def init_interface_values(self,#
                                interface_marker: df.MeshFunction,#
                                interface_marker_value: int) -> None:
            """ allocate dictionaries that are used to store the interface dof values
                of the pressure, previous pressure, gl decoupling term as well as previous
                gl update term respectively as pairs(parent_mesh_index: dof_value)
    
            The MeshFunction interface_marker is supposed to be one defined on the
            global mesh, so that the indices used in the dictionary are the global
            vertex indices and not indicis of some submesh.
    
            # Parameters
            interface_marker:       #type df.MeshFunction   marker function marking
                                                            edges of i_mesh
                                                            according to self.inside
            interface_marker_value: #type int               marker value of interface_marker
            """
            vertex_indices = self._vertex_indices(interface_marker, #
                                                  interface_marker_value,#
                                                  print_vertex_indices = False)
            self.pressure_values = dict()
            #self.pressure_values is a dictionay which contains for each index in
            # adjacent_subdomains a dictionary for wetting and nonwetting phase with
            # as a value yet another dictionary holding the interface values. phew.
            self.pressure_values.update({self.adjacent_subdomains[0]: dict()})
            self.pressure_values.update({self.adjacent_subdomains[1]: dict()})
            self.pressure_values_prev = dict()
            self.pressure_values_prev.update({self.adjacent_subdomains[0]: dict()})
            self.pressure_values_prev.update({self.adjacent_subdomains[1]: dict()})
    
            self.gli_term = dict()
            self.gli_term.update({self.adjacent_subdomains[0]: dict()})
            self.gli_term.update({self.adjacent_subdomains[1]: dict()})
            self.gli_term_prev = dict()
            self.gli_term_prev.update({self.adjacent_subdomains[0]: dict()})
            self.gli_term_prev.update({self.adjacent_subdomains[1]: dict()})
            # we initialise all dictionaries to accomodate two phases, to be more
            # flexible with coupling Richards and Two-phase models.
            for subdom_ind in self.adjacent_subdomains:
                for phase in ['wetting', 'nonwetting']:
                    self.pressure_values[subdom_ind].update({phase: dict()})
                    self.pressure_values_prev[subdom_ind].update({phase: dict()})
                    # the gli_term will hold dofs of the gli terms for communications
                    # they are initiated with 0
                    self.gli_term[subdom_ind].update({phase: dict()})
                    self.gli_term_prev[subdom_ind].update({phase: dict()})
                    for vert_ind in vertex_indices:
                        self.pressure_values[subdom_ind][phase].update({vert_ind: 0.0})
                        self.pressure_values_prev[subdom_ind][phase].update({vert_ind: 0.0})
                        self.gli_term[subdom_ind][phase].update({vert_ind: 0.0})
                        self.gli_term_prev[subdom_ind][phase].update({vert_ind: 0.0})
            # end init_interface_values
    
        def read_pressure_dofs(self, from_function: df.Function, #
                       interface_dofs: np.array,#
                       dof_to_vert_map: np.array,#
                       local_to_parent_vertex_map: np.array,#
                       phase: str,#
                       subdomain_ind: int,#
                       previous_iter: bool = False,#
                       ) -> None:
            """ read dofs of from_function with indices interface_dofs and write to self.pressure_values
    
            Read the degrees of freedom of the function from_function with indices
            interface_dofs, i.e. the dofs of the interface, and write them to one of the dictionaries
            of the interface
                self.pressure_values[subdomain_ind][phase], (if pressure == True)
                self.pressure_values_prev[subdomain_ind][phase], (if previous_iter == True)
    
            # Parameters
            from_function:              #type: df.Function, function to save the dofs of
            interface_dofs:             #type: np.array,    index array of dofs to be saved
            dof_to_vert_map:            #type: np.array,    dof to vert map of the function
                                                            space on the submesh.
            local_to_parent_vertex_map: #type: np.array,
            phase                       #type: str          is either 'wetting' or
                                                            'nonwetting' and determins
                                                            the dict to be written to.
            subdomain_ind               #type: int          subdomain index
            previous_iter               #type: bool         determins wether a previous
                                                            iterate is being written or not
            """
            if self.pressure_values == None:
                raise RuntimeError("self.pressure_values not initiated. You forgot to \
                                    run self.init_interface_values")
    
            parent = local_to_parent_vertex_map
            d2v = dof_to_vert_map
            if not previous_iter:
                for dof_index in interface_dofs:
                    # from_function.vector() is orderd by dof and not by vertex numbering of the mesh.
                    # parent needs a mesh vertex index of the submesh, therefore d2v is needed.
                    self.pressure_values[subdomain_ind][phase].update({parent[d2v[dof_index]]: from_function.vector()[dof_index]})
                print("have written pressure interface values\n", self.pressure_values[subdomain_ind][phase])
            else:
                for dof_index in interface_dofs:
                    # from_function.vector() is orderd by dof and not by vertex numbering of the mesh.
                    # parent needs a mesh vertex index of the submesh, therefore d2v is needed.
                    self.pressure_values_prev[subdomain_ind][phase].update({parent[d2v[dof_index]]: from_function.vector()[dof_index]})
                print("have written previous pressure interface values\n", self.pressure_values_prev[subdomain_ind][phase])
    
    
        def read_gli_dofs(self, from_array: np.array, #
                       interface_dofs: np.array,#
                       dof_to_vert_map: np.array,#
                       local_to_parent_vertex_map: np.array,#
                       phase: str,#
                       subdomain_ind: int,#
                       previous_iter: bool = False,#
                       ) -> None:
            """ Read dofs of from_array with indices interface_dofs and write to self.gli_term
    
            Read the degrees of freedom of the array from_array with indices
            interface_dofs, i.e. the dofs of the interface, and write them to one of the dictionaries
            of the interface
                self.gli_term[subdomain_ind][phase], (if gl == True)
                self.gli_term_prev[subdomain_ind][phase] (if previous_iter == True)
    
            # Parameters
            from_array:                 #type: np.array,    array to save the dofs of
            interface_dofs:             #type: np.array,    index array of dofs to be saved
            dof_to_vert_map:            #type: np.array,    dof to vert map of the function
                                                            space on the submesh.
            local_to_parent_vertex_map: #type: np.array,
            phase                       #type: str          is either 'wetting' or
                                                            'nonwetting' and determins
                                                            the dict to be written to.
            subdomain_ind               #type: int          subdomain index
            previous_iter               #type: bool         determins wether a previous
                                                            iterate is being written or not
            """
            if self.pressure_values == None:
                raise RuntimeError("self.pressure_values not initiated. You forgot to \
                                    run self.init_interface_values")
    
            parent = local_to_parent_vertex_map
            d2v = dof_to_vert_map
            if not previous_iter:
                for dof_index in interface_dofs:
                    # from_array is orderd by dof and not by vertex numbering of the mesh.
                    # parent needs a mesh vertex index of the submesh, therefore d2v is needed.
                    self.gli_term[subdomain_ind][phase].update({parent[d2v[dof_index]]: from_array[dof_index]})
                print("have written gl interface values\n", self.gli_term[subdomain_ind][phase])
            else:
                for dof_index in interface_dofs:
                    # from_array is orderd by dof and not by vertex numbering of the mesh.
                    # parent needs a mesh vertex index of the submesh, therefore d2v is needed.
                    self.gli_term_prev[subdomain_ind][phase].update({parent[d2v[dof_index]]: from_array[dof_index]})
                print("have written previous gl interface values\n", self.gli_term_prev[subdomain_ind][phase])
    
    
        def write_pressure_dofs(self, to_function: df.Function, #
                      interface_dofs: np.array,#
                      dof_to_vert_map: np.array,#
                      local_to_parent_vertex_map: np.array,#
                      phase: str,#
                      subdomain_ind: int,#
                      # pressure: bool = False,#
                      # gl: bool = False,#
                      previous_iter: bool = False,#
                      ) -> None:
            """ read dofs off self.pressure_values and overwrite the dofs of to_function
                with indices interface_dofs
    
            Read the degrees of freedom of one of the dictionaries of the interface
                self.pressure_values[subdomain_ind][phase], (if pressure == True)
                self.pressure_values_prev[subdomain_ind][phase], (if previous_iter == True)
            and overwrite the dofs of the function to_function with indices
            interface_dofs, i.e. update the interface dofs of the function.
            .
    
            # Parameters
            to_function:               #type: df.Function, function to overwrite the
                                                            dofs of.
            interface_dofs:             #type: np.array,    index array of interface
                                                            dofs of from_function
            dof_to_vert_map:            #type: np.array,    dof to vert map of the function
                                                            space on the submesh.
            local_to_parent_vertex_map: #type: np.array,
            phase                       #type: str          is either 'wetting' or
                                                            'nonwetting' and determins
                                                            the dict to be read of.
            subdomain_ind               #type: int          subdomain index
            previous_iter               #type: bool         determins wether a previous
                                                            iterate is being read or not
            """
            if self.pressure_values == None:
                raise RuntimeError("self.pressure_values not initiated. You forgot to \
                                    run self.init_interface_values")
    
            parent = local_to_parent_vertex_map
            d2v = dof_to_vert_map
    
            if not previous_iter:
                for dof_index in interface_dofs:
                    # from_function.vector() is orderd by dof and not by vertex numbering of the mesh.
                    # parent needs a mesh vertex index of the submesh, therefore d2v is needed.
                    to_function.vector()[dof_index] = self.pressure_values[subdomain_ind][phase][parent[d2v[dof_index]]]
                print("read pressure interface values\n", self.pressure_values[subdomain_ind][phase])
                print("wrote these dofs to function \n", to_function.vector()[:])
            else:
                for dof_index in interface_dofs:
                    # from_function.vector() is orderd by dof and not by vertex numbering of the mesh.
                    # parent needs a mesh vertex index of the submesh, therefore d2v is needed.
                    to_function.vector()[dof_index] = self.pressure_values_prev[subdomain_ind][phase][parent[d2v[dof_index]]]
                print("read previous pressure interface values\n", self.pressure_values_prev[subdomain_ind][phase])
                print("wrote these dofs to function \n", to_function.vector()[:])
    
    
        def write_gli_dofs(self, to_array: np.array, #
                      interface_dofs: np.array,#
                      dof_to_vert_map: np.array,#
                      local_to_parent_vertex_map: np.array,#
                      phase: str,#
                      subdomain_ind: int,#
                      previous_iter: bool = False,#
                      ) -> None:
            """ read dofs off self.gli_term and overwrite the dofs stored in the array
                to_array with indices interface_dofs
    
            Read the degrees of freedom of one of the dictionaries of the interface
                self.gli_term[subdomain_ind][phase], (if gl == True)
                self.gli_term_prev[subdomain_ind][phase] (if previous_iter == True)
            and overwrite the values (holding dofs) of the array to_array with indices
            interface_dofs, i.e. update the interface dofs of the gli terms.
            .
    
            # Parameters
            to_array:               #type: np.array,    array to overwrite the
                                                            dofs of.
            interface_dofs:             #type: np.array,    index array of interface
                                                            dofs of from_function
            dof_to_vert_map:            #type: np.array,    dof to vert map of the function
                                                            space on the submesh.
            local_to_parent_vertex_map: #type: np.array,
            phase                       #type: str          is either 'wetting' or
                                                            'nonwetting' and determins
                                                            the dict to be read of.
            subdomain_ind               #type: int          subdomain index
    
            previous_iter               #type: bool         determins wether a previous
                                                            iterate is being read or not
            """
            if self.pressure_values == None:
                raise RuntimeError("self.pressure_values not initiated. You forgot to \
                                    run self.init_interface_values")
    
            parent = local_to_parent_vertex_map
            d2v = dof_to_vert_map
            if not previous_iter:
                for dof_index in interface_dofs:
                    # to_array is orderd by dof and not by vertex numbering of the mesh.
                    # parent needs a mesh vertex index of the submesh, therefore d2v is needed.
                    to_array[dof_index] = self.gli_term[subdomain_ind][phase][parent[d2v[dof_index]]]
                print("read gl interface values\n", self.gli_term[subdomain_ind][phase])
                print("wrote these dofs to array \n", to_array[:])
            else:
                for dof_index in interface_dofs:
                    # to_array is orderd by dof and not by vertex numbering of the mesh.
                    # parent needs a mesh vertex index of the submesh, therefore d2v is needed.
                    to_array[dof_index] = self.gli_term_prev[subdomain_ind][phase][parent[d2v[dof_index]]]
                print("read gl interface values\n", self.gli_term_prev[subdomain_ind][phase])
                print("wrote these dofs to array \n", to_array[:])
    
        #### Private Methods ####
    
        def _vertex_indices(self, #
                        interface_marker: df.MeshFunction, #
                        interface_marker_value: int,#
                        print_vertex_indices: bool = False) -> np.array:
            """ return indices of nodes on the mesh marked by
                interface_marker with value interface_marker.
    
            _vertex_indices() returns indices of nodes on
            the mesh marked by interface_marker with value
            interface_marker. interface_marker is a dolfin.MeshFunction of dimension
            interface_marker.mesh().topology().dim()-1 that is supposed to mark edges of i_mesh
            according to the method super().inside().
    
            # Parameters
            interface_marker:       #type df.MeshFunction   marker function marking edges of i_mesh
                                                            according to self.inside
            interface_marker_value: #type int               marker value of interface_marker
            print_vertex_indices      #type boolean           specify wether to print out the
                                                            node or not.
            """
            mesh_coordinates = interface_marker.mesh().coordinates()
            mesh_dimension = interface_marker.mesh().topology().dim()
            # interface_marker is a function defined on edges. Therefore
            # sum(interface_marker.array() == interface_marker_value) gives the number
            # of edges on the boundary. We need the number of nodes, however.
            number_of_interface_vertices = sum(interface_marker.array() == interface_marker_value) + 1
    
            # print(f"\nDetermined number of interface vertices as {number_of_interface_vertices}")
            # we need one mesh_dimension + 1 columns to store the the index of the node.
            vertex_indices = np.zeros(shape = number_of_interface_vertices, dtype=int)
            # print(f"allocated array for vertex_indices\n", vertex_indices) #
            interface_vertex_number = 0
            # mesh_vertex_index = 0
            for vert_num, x in enumerate(mesh_coordinates):
                if self._is_on_boundary_part(x):
                    # print(f"Vertex {x} with index {vert_num} is on interface")
                    # print(f"interface_vertex_number = {interface_vertex_number}")
                    vertex_indices[interface_vertex_number] = vert_num
                    interface_vertex_number += 1
    
            if print_vertex_indices:
                print("mesh node indices: \n", vertex_indices)
    
            return vertex_indices
    ### End Class Interface