Module regpy.operators.ngsolve

PDE forward operators using NGSolve

Classes

class NGSolveOperator (domain: NgsVectorSpace,
codomain: NgsVectorSpace,
linear: bool = False)
Expand source code
class NGSolveOperator(Operator):
    def __init__(self, 
            domain : NgsVectorSpace, 
            codomain : NgsVectorSpace, 
            linear : bool = False)->None:
        super().__init__(domain = domain, codomain = codomain, linear = linear)
        self.gfu_read_in = ngs.GridFunction(self.domain.fes)

    '''Reads in a coefficient vector of the domain and interpolates in the codomain.
    The result is saved in gfu'''
    def _read_in(self, 
            vector, 
            gfu,
            definedonelements =None):
        """Read in of a numpy array into a ngsolve grid function. Note You can also read into
        ngsolve LinearForm.

        Parameters
        ----------
        vector : numpy.array
            Numpy array to be put into the `GridFunction`
        gfu : ngsolve.GridFunction or ngsolve.LinearForm
            ngsolve element to be written into.
        definedonelements : pyngcore.pyngcore.BitArray, optional
            BitArray representing the finite elements to be projected onto, by default None
        """
        gfu.vec.FV().NumPy()[:] = vector
        if definedonelements is not None:
            ngs.Projector(definedonelements, range=True).Project(gfu.vec)

    def _solve_dirichlet_problem(self, 
            bf : ngs.comp.BilinearForm, 
            lf : ngs.comp.LinearForm, 
            gf : ngs.comp.GridFunction, 
            prec : ngs.comp.Preconditioner) -> None:
        r"""Solves the problem 
        \begin{align*}
        b(u,v) = f(v) \;\forall v\; test\; functions,\\
        u|_\Gamma = g
        \end{align*}
        The boundary values are given by what `gf` has as values on the boundary. 

        Parameters
        ----------
        bf : ngs.BilinearForm
            the bilinear form of the problem.
        lf : ngs.LinearFrom
            the linear form of the problem. 
        gf : ngs.GridFunction
            The grid functions on which to solve the solution will be put into these and they have to satisfy 
            the boundary condition that you want.
        prec : ngs.Preconditioner
            preconditioner to be used with ngsolve.
        """
        gf.vec.data += bf.mat.Inverse(freedofs=self.codomain.fes.FreeDofs()) * (lf.vec - bf.mat * gf.vec)

Ancestors

Subclasses

Inherited members

class SecondOrderEllipticCoefficientPDE (domain: NgsVectorSpace,
sol_domain: NgsVectorSpace,
bdr_val: NgsBaseVector | None = None,
a_bdr_val: NgsBaseVector | None = None)
Expand source code
class SecondOrderEllipticCoefficientPDE(NGSolveOperator):
    r"""Provides a general setup for the forward problems mapping PDE coefficients to their solutions.
    That is we assume that as variational formulation one can use 
    \[
    \forall v:    b_0(u,v) + b_a(u,v) = F(v)
    \] 
    with \(b_0\) a bilinear form independent of the coefficient \(a\) and \(F\) some linear form.  
    Furthermore \(b_a\) the bilinear form that depends on \(a\) has to be linear in \(a\) so that 
    one can define a bilinear form \(c_u(a,v)=b_a(u,v)\). More over we may assume some Dirichlet 
    boundary conditions on \(u\).That is 
    \[
        F: a \mapsto u 
    \]

    The Frechét derivative \(F'[a]h\) in direction \(h\) is then given as the variational 
    solution \(u'\) to 
    \[
    \forall v: b_0(u',v) + b_a(u',v) = -c_u(h,v)
    \]
    with \(u=F(a)\). That is
    \[
        F'[a]: h \mapsto u' 
    \]

    It's adjoint \(F'[a]^\ast g\) is given as the linear form \(-c_u(\cdot,w)\) for \(u=F(a)\) and 
    \(w\) solving the problem 
    \[
    \forall v:  b_0(v,w) + b_a(v,w) = <g,v>.
    \]
    That is
    \[
        F'[a]^\ast: g \mapsto -c_u(\cdot,w)
    \]

    Notes
    -----
    A subclass implemented by a user has to at least implement the subroutine `_bf` which is the 
    implementation of the bilinear form depending on the coefficient \(a\). Optional can the 
    independent bilinear form \(b_0\) be implemented as formal integrator in `_bf_0` and the 
    linear form can be implemented in `_lf`. 

    Parameters
    ----------
    domain : NgsVectorSpace
        The NgsVectorSpace on which the coefficients defined are.
    sol_domain : NgsVectorSpace
        The NgsVectorSpace on which the PDE solutions defined are.
    bdr_val : NgsBaseVector, optional
        Boundary value of the PDE solution of the forward evaluation, by default None
    a_bdr_val : NgsBaseVector, optional
        Boundary value of the coefficients, by default None
    """
    def __init__(self, 
            domain : NgsVectorSpace, 
            sol_domain : NgsVectorSpace, 
            bdr_val : NgsBaseVector | types.NoneType = None, 
            a_bdr_val : NgsBaseVector | types.NoneType = None) -> None:
        super().__init__(domain, sol_domain, linear = False)
        self.gfu_a=ngs.GridFunction(self.domain.fes)
        self.gfu_h=ngs.GridFunction(self.domain.fes)
        self.a_bdr = a_bdr_val.vec if a_bdr_val is not None and self.domain.bdr is not None and self.domain.is_on_boundary(a_bdr_val) else self.domain.zeros().vec

        self.gfu_deriv=ngs.GridFunction(self.codomain.fes)
        self.gfu_adj_help=ngs.GridFunction(self.codomain.fes)
        self.gfu_eval=ngs.GridFunction(self.codomain.fes)
        if bdr_val is not None and bdr_val in self.codomain:
            self.gfu_eval.vec.data=bdr_val.vec
        
        self.u_a, self.v_a = self.domain.fes.TnT()
        self.u, self.v = self.codomain.fes.TnT()

        self.bf_mat = ngs.BilinearForm(self.codomain.fes)
        self.bf_mat += self._bf(self.gfu_a,self.u,self.v) 
        if self._bf_0() is not None:
            self.bf_mat += self._bf_0()
        
        self.first = True
        self.adj_first = True

        self.lf = self._lf()

        self.c_u = ngs.LinearForm(self.codomain.fes)
        self.c_u += self._bf(self.gfu_h,self.gfu_eval,self.v)

        self.lf_adj = ngs.LinearForm(self.domain.fes)
        self.lf_adj += -1*self._bf(self.v_a,self.gfu_eval,self.gfu_adj_help)        

    def _eval(self, 
            a : NgsBaseVector, 
            differentiate : bool = False, 
            adjoint_derivative : bool = False) -> NgsBaseVector:
        self.adj_first = True
        self.gfu_a.vec.data = ngs.Projector(self.domain.fes.FreeDofs(), range=True).Project(a.vec) + self.a_bdr
        # self.gfu_a.vec.data = a.vec + self.a_bdr
        
        self.bf_mat.Assemble()
        # if self.first:
        #     self.bf_mat_inv = self.bf_mat.mat.Inverse(freedofs=self.codomain.fes.FreeDofs())
        #     self.first = False 
        # else:
        #     self.bf_mat_inv.Update()
        self.bf_mat_inv = self.bf_mat.mat.Inverse(freedofs=self.codomain.fes.FreeDofs())
        self.gfu_eval.vec.data += self.bf_mat_inv * (self.lf.vec - self.bf_mat.mat * self.gfu_eval.vec)
        # print(self.gfu_eval.vec)
        return NgsBaseVector(self.gfu_eval.vec,make_copy=True)
    
    def _derivative(self, 
            h : NgsBaseVector) -> NgsBaseVector:
        lf = self._c_u(h.vec)
        self.gfu_deriv.vec.data += self.bf_mat_inv * (-lf.vec - self.bf_mat.mat * self.gfu_deriv.vec)
        return NgsBaseVector(self.gfu_deriv.vec,make_copy=True)

    def _adjoint(self, 
            g : NgsBaseVector) -> NgsBaseVector:
        if self.adj_first:
            self.bf_mat_adj = self.bf_mat.mat.CreateTranspose()
            self.bf_mat_adj_inv = self.bf_mat_adj.Inverse(freedofs=self.codomain.fes.FreeDofs())
        self.gfu_adj_help.vec.data += self.bf_mat_adj_inv * (g.vec - self.bf_mat_adj * self.gfu_adj_help.vec)
        self.lf_adj.Assemble()
        return NgsBaseVector(ngs.Projector(self.domain.fes.FreeDofs(), range=True).Project(self.lf_adj.vec) + self.a_bdr,make_copy=True)

    def _bf(self,
            a : ngs.fem.CoefficientFunction,
            u : ngs.fem.CoefficientFunction,
            v : ngs.fem.CoefficientFunction) -> ngs.comp.BilinearForm:
        r"""Implementation of \(b_a\) as `ngsolve.comp.SumOfIntegrals` that is something similar to
        `a*ngs.grad(u)*ngs.grad(v)*ngs.dx` where `u` ist used as trial functions and `v` as test 
        functions. This method has to be implemented by 

        Parameters
        ----------
        a : ngsolve.CoefficientFunction or ngsolve.GridFunction
            the coefficient in the PDE
        u : ngsolve.comp.ProxyFunction
            Trial functions for PDE
        v : ngsolve.comp.ProxyFunction
            Test functions for PDE

        Returns
        ------
        ngsolve.comp.SumOfIntegrals
            the formal Integration formula of the bilinear form depending on the coefficient
        """
        raise NotImplementedError
    
    def _bf_0(self) -> ngs.comp.BilinearForm | types.NoneType:
        r"""Implementation of \(b_0\) as `ngsolve.comp.SumOfIntegrals` is an optional method to be 
        overwritten with subclasses.  

        Returns
        -------
        ngsolve.comp.SumOfIntegrals
            the formal Integration formula of the bilinear form independent of the coefficient
        """
        return None
    
    def _lf(self) -> ngs.comp.LinearForm:
        r"""The Linear form of the PDE \(F\) implemented as a fixed Linear form. Note that the 
        Linear form has to be defined on the `codomain` as this is the domain of the solution of 
        the PDE. By default this is the empty Linear form. 

        Returns
        ------
        ngsolve.Linearform
            Linear form of the PDE \(F\) as `ngsolve.LinearForm`
        """
        return ngs.LinearForm(self.codomain.fes).Assemble()
        
    def _c_u(self,
            h : ngs.la.BaseVector) -> ngs.comp.BilinearForm:
        self.gfu_h.vec.data = 1*h
        ngs.Projector(self.domain.fes.FreeDofs(), range=True).Project(self.gfu_h.vec)
        return self.c_u.Assemble()

Provides a general setup for the forward problems mapping PDE coefficients to their solutions. That is we assume that as variational formulation one can use \forall v: b_0(u,v) + b_a(u,v) = F(v) with b_0 a bilinear form independent of the coefficient a and F some linear form.
Furthermore b_a the bilinear form that depends on a has to be linear in a so that one can define a bilinear form c_u(a,v)=b_a(u,v). More over we may assume some Dirichlet boundary conditions on u.That is F: a \mapsto u

The Frechét derivative F'[a]h in direction h is then given as the variational solution u' to \forall v: b_0(u',v) + b_a(u',v) = -c_u(h,v) with u=F(a). That is F'[a]: h \mapsto u'

It's adjoint F'[a]^\ast g is given as the linear form -c_u(\cdot,w) for u=F(a) and w solving the problem \forall v: b_0(v,w) + b_a(v,w) = <g,v>. That is F'[a]^\ast: g \mapsto -c_u(\cdot,w)

Notes

A subclass implemented by a user has to at least implement the subroutine _bf which is the implementation of the bilinear form depending on the coefficient a. Optional can the independent bilinear form b_0 be implemented as formal integrator in _bf_0 and the linear form can be implemented in _lf.

Parameters

domain : NgsVectorSpace
The NgsVectorSpace on which the coefficients defined are.
sol_domain : NgsVectorSpace
The NgsVectorSpace on which the PDE solutions defined are.
bdr_val : NgsBaseVector, optional
Boundary value of the PDE solution of the forward evaluation, by default None
a_bdr_val : NgsBaseVector, optional
Boundary value of the coefficients, by default None

Ancestors

Inherited members

class SolveSystem (domain: NgsVectorSpace,
bf: ngsolve.comp.BilinearForm)
Expand source code
class SolveSystem(NGSolveOperator):
    r"""Solve the system 
    \begin(align*)
    Lu = f \text{ in } \Omega, \\
    u = 0  \text{ in } \partial\Omega
    \begin{align*}
    given f. 

    Parameters
    ----------
    domain : NgsVectorSpace
        the underlying NgsVectorSpace.
    bf : ngs.BilinearForm
        The bilinear form describing \(L\)
    """
    def __init__(self, 
            domain : NgsVectorSpace, 
            bf : ngs.BilinearForm) -> None:
        super().__init__(domain=domain, codomain=domain, linear=True)
        self.bf=bf
        self.prec = ngs.Preconditioner(self.bf, 'local')
        self.gfu=ngs.GridFunction(self.domain.fes)
        self.gfu_adj=ngs.GridFunction(self.domain.fes)
        self.gfu_eval=ngs.GridFunction(self.domain.fes)
        u, v=self.domain.fes.TnT()
        
        self.f = ngs.LinearForm(self.domain.fes)
        self.f += self.gfu * v * ngs.dx
        
        self.f_adj = ngs.LinearForm(self.domain.fes)
        self.f_adj += self.gfu_adj * v * ngs.dx
        
    def _eval(self, 
            argument : NgsBaseVector) -> NgsBaseVector:
        self.gfu.vec.data = argument.vec
        self.f.Assemble()
        self._solve_dirichlet_problem(self.bf, self.f, self.gfu_eval, self.prec)
        return NgsBaseVector(self.gfu_eval.vec,make_copy=True)
    
    def _adjoint(self, 
            argument : NgsBaseVector) -> NgsBaseVector:
        self.gfu_adj.vec.data=self.bf.mat.CreateTranspose().Inverse()*argument.vec
        self.f_adj.Assemble()
        return NgsBaseVector(self.f_adj.vec,make_copy=True)

Solve the system \begin(align) Lu = f \text{ in } \Omega, \ u = 0 \text{ in } \partial\Omega \begin{align} given f.

Parameters

domain : NgsVectorSpace
the underlying NgsVectorSpace.
bf : ngs.BilinearForm
The bilinear form describing L

Ancestors

Inherited members

class LinearForm (domain: NgsVectorSpace)
Expand source code
class LinearForm(NGSolveOperator):
    def __init__(self, 
            domain : NgsVectorSpace) -> None:
        super().__init__(domain=domain, codomain=domain, linear=True)
        self.gfu=ngs.GridFunction(self.domain.fes)
        self.gfu_adj=ngs.GridFunction(self.domain.fes)
        u, v=self.domain.fes.TnT()
        
        self.f = ngs.LinearForm(self.domain.fes)
        self.f += self.gfu * v * ngs.dx
        self._y = NgsBaseVector(self.f.vec)
        
    def _eval(self, 
            argument : NgsBaseVector) -> NgsBaseVector:
        self.gfu.vec.data = argument.vec
        self.f.Assemble()
        return self._y.copy()
    
    def _adjoint(self, 
            argument : NgsBaseVector) -> NgsBaseVector:
        return self._eval(argument)

Ancestors

Inherited members

class LinearFormGrad (domain: NgsVectorSpace,
gfu_eval: ngsolve.comp.GridFunction)
Expand source code
class LinearFormGrad(NGSolveOperator):
    
    def __init__(self, 
        domain : NgsVectorSpace, 
        gfu_eval : ngs.GridFunction) -> None:

        super().__init__(domain=domain, codomain=domain, linear=True)
        self.gfu=ngs.GridFunction(self.domain.fes)
        self.gfu_adj=ngs.GridFunction(self.domain.fes)
        self.gfu_eval=gfu_eval
        u, v=self.domain.fes.TnT()
        
        self.f = ngs.LinearForm(self.domain.fes)
        self.f += ngs.grad(self.gfu) * ngs.grad(self.gfu_eval) * v * ngs.dx
        self._y = NgsBaseVector(self.f.vec)
        
        self.f_adj = ngs.LinearForm(self.domain.fes)
        self.f_adj += self.gfu_adj * ngs.grad(self.gfu_eval) * ngs.grad(v) * ngs.dx
        self._x = NgsBaseVector(self.f_adj.vec)
        
    def _eval(self,
        argument : NgsBaseVector) -> NgsBaseVector:
        self.gfu.vec.data = argument.vec
        self.f.Assemble()
        return self._y.copy()
    
    def _adjoint(self, 
        argument : NgsBaseVector) -> NgsBaseVector:
        self.gfu_adj.vec.data = argument.vec
        self.f_adj.Assemble()
        return self._x.copy()

Ancestors

Inherited members

class BilinearForm (domain: NgsVectorSpace,
bf: ngsolve.comp.BilinearForm)
Expand source code
class BilinearForm(NGSolveOperator):
    
    def __init__(self, 
        domain : NgsVectorSpace, 
        bf : ngs.BilinearForm) -> None:
        assert isinstance(domain,NgsVectorSpace)
        super().__init__(domain=domain, codomain=domain, linear=True)
        self.bf=bf
        
        self.gfu=ngs.GridFunction(self.domain.fes)
        self._y = NgsBaseVector(self.gfu.vec)
        self.gfu_adj=ngs.GridFunction(self.domain.fes)
        self._x = NgsBaseVector(self.gfu_adj.vec)
        
        self.f_eval = ngs.LinearForm(self.domain.fes)
        self.f_adj  = ngs.LinearForm(self.domain.fes)
        
    def _eval(self, 
        argument : NgsBaseVector) -> NgsBaseVector:
        self.gfu.vec.data=self.bf.mat.Inverse()*argument.vec
        return self._y.copy()
    
    def _adjoint(self, 
        argument : NgsBaseVector) -> NgsBaseVector:
        self.gfu_adj.vec.data=self.bf.mat.CreateTranspose().Inverse()*argument.conj().vec
        return self._x.conj().copy()

Ancestors

Inherited members

class Coefficient (domain: NgsVectorSpace,
rhs: ngsolve.fem.CoefficientFunction,
bc: ngsolve.fem.CoefficientFunction | None = None,
codomain: NgsVectorSpace | None = None,
diffusion: bool = False,
reaction: bool = True)
Expand source code
class Coefficient(NGSolveOperator):
    r"""Diffusion and reaction coefficient problem
    
    Identification of a diffusion coefficient:
    
    PDE: -div(a grad u)=rhs       in Omega
         u = 0            on dOmega

    Evaluate: 
        F: a \mapsto u
    Derivative:
        -div (a grad v)=div (h grad u) in Omega
        v = 0                        on dOmega

    Der: F'[u]: h \mapsto v

    Adjoint:
        div (a grad w)=q  in Omega
        w=0              on dOmega

    Adj: F'[s]^*: q \mapsto -grad(u) grad(w)
    
    
    
    
    Identification of a reaction coefficient:
    
    PDE: -Delta u +c u=f       in Omega
         u = 0            on dOmega

    Evaluate: 
        F: c \mapsto u
        
    Derivative:
        -Delta v + c v=-h u in Omega
        v = 0                        on dOmega

    Der: F'[u]: h \mapsto v

    Adjoint:
        -Delta w+c w=q  in Omega
        w=0              on dOmega

    Adj: F'[s]^*: q \mapsto -u^* w    
    """
    def __init__(
        self, 
        domain : NgsVectorSpace, 
        rhs : ngs.fem.CoefficientFunction, 
        bc: ngs.fem.CoefficientFunction | types.NoneType=None, 
        codomain : NgsVectorSpace | types.NoneType = None,
        diffusion : bool = False, 
        reaction : bool = True
    ) -> None:
        assert diffusion or reaction
        assert (diffusion and reaction) is False
        codomain = codomain or domain
        #Need to know the boundary to calculate Dirichlet bdr condition
        assert codomain.bdr is not None

        self.rhs = rhs
        super().__init__(domain, codomain)

        self.diffusion = diffusion
        self.reaction = reaction
        self.dim = domain.fes.mesh.dim

        bc = bc or 0

        # Define mesh and finite element space
        self.fes_domain = domain.fes
        self.fes_codomain = codomain.fes

        # grid functions for later use
        self.gfu_eval = ngs.GridFunction(self.fes_codomain)  # solution, return value of _eval
        self._y = NgsBaseVector(self.gfu_eval.vec)
        self.gfu_deriv = ngs.GridFunction(self.fes_codomain)  # return value of derivative
        self._y_deriv = NgsBaseVector(self.gfu_deriv.vec)
        self.gfu_adjoint = ngs.GridFunction(self.fes_domain)  # grid function for returning values in adjoint
        self._x = NgsBaseVector(self.gfu_adjoint.vec)

        self.gfu_bf = ngs.GridFunction(self.fes_domain) # grid function for defining integrator (bilinearform)
        self.gfu_lf = ngs.GridFunction(self.fes_codomain)  # grid function for defining right hand side (Linearform)

        self.gfu_inner_adj = ngs.GridFunction(self.fes_codomain) #computations in adjoint
        self.gfu_inner_deriv = ngs.GridFunction(self.fes_domain) #inner computations in derivative

        #Test and Trial Function
        u, v = self.fes_codomain.TnT()

        # Define Bilinearform, will be assembled later
        self.a = ngs.BilinearForm(self.fes_codomain, symmetric=True)
        if self.diffusion:
            self.a += ngs.grad(u) * ngs.grad(v) * self.gfu_bf * ngs.dx
        elif self.reaction:
            self.a += (ngs.grad(u) * ngs.grad(v) + u * v * self.gfu_bf) * ngs.dx

        # Define Linearform, will be assembled later
        self.f = ngs.LinearForm(self.fes_codomain)
        self.f += self.gfu_lf * v * ngs.dx
        # Assemble Linearform
        self.gfu_lf.Set(self.rhs)
        self._x_lf = NgsBaseVector(self.gfu_lf.vec)
        self.f.Assemble()

        if self.reaction:
            self.lf=LinearForm(self.codomain)
            
        self.gfu_eval.Set(bc, definedon=self.fes_codomain.mesh.Boundaries(codomain.bdr))

        #Initialize Preconditioner for solving the Dirichlet problems
        self.prec = ngs.Preconditioner(self.a, 'local')

        #Initialize homogenous Dirichlet problems for derivative and adjoint
        self.gfu_inner_adj.Set(0)
        self.gfu_inner_deriv.Set(0)

    def _eval(self, 
        diff : NgsBaseVector, 
        differentiate : bool = False, 
        adjoint_derivative : bool = False) -> NgsBaseVector:
        # Assemble Bilinearform
        self.gfu_bf.vec.data = diff.vec
        self.a.Assemble()
        if differentiate:
            self.bf=BilinearForm(self.codomain, self.a)

        # Solve system
        self._solve_dirichlet_problem(self.a, self.f, self.gfu_eval, self.prec)
        if differentiate and self.diffusion:
            self.lf=LinearFormGrad(self.codomain, self.gfu_eval)
        return self._y.copy()

    def _derivative(self, 
        argument : NgsBaseVector) -> NgsBaseVector:
        # Bilinearform already defined from _eval

        # Translate arguments in Coefficient Function and interpolate to codomain
        self.gfu_inner_deriv.vec.data = argument.vec
        
        if self.diffusion:
            self.gfu_deriv.Set(self.gfu_inner_deriv)
            return (self.bf*self.lf)(self._x_lf)

        elif self.reaction:
            self.gfu_deriv.Set(-self.gfu_inner_deriv * self.gfu_eval)
            return (self.bf*self.lf)(self._y_deriv)
        else:
            raise ValueError("Neither diffusion nor reaction was selected to be True")

    def _adjoint(self, 
        argument : NgsBaseVector) -> NgsBaseVector:
        if self.reaction:
            self.gfu_inner_adj.vec.data=self.lf._adjoint(self.bf._adjoint(argument)).vec
            self.gfu_adjoint.Set( -self.gfu_eval * self.gfu_inner_adj )
            return self._x
        
        elif self.diffusion:
            self.gfu_inner_adj.vec.data=self.lf._adjoint(self.bf._adjoint(argument)).vec
            self.gfu_adjoint.Set( self.gfu_inner_adj )
            return self._x
        else:
            raise ValueError("Neither diffusion nor reaction was selected to be True")

Diffusion and reaction coefficient problem

Identification of a diffusion coefficient:

PDE: -div(a grad u)=rhs in Omega u = 0 on dOmega

Evaluate: F: a \mapsto u

Derivative

-div (a grad v)=div (h grad u) in Omega v = 0 on dOmega

Der: F'[u]: h \mapsto v

Adjoint

div (a grad w)=q in Omega w=0 on dOmega

Adj: F'[s]^*: q \mapsto -grad(u) grad(w)

Identification of a reaction coefficient:

PDE: -Delta u +c u=f in Omega u = 0 on dOmega

Evaluate: F: c \mapsto u

Derivative

-Delta v + c v=-h u in Omega v = 0 on dOmega

Der: F'[u]: h \mapsto v

Adjoint

-Delta w+c w=q in Omega w=0 on dOmega

Adj: F'[s]^: q \mapsto -u^ w

Ancestors

Inherited members

class ProjectToBoundary (domain: NgsVectorSpace,
codomain: NgsVectorSpace | None = None)
Expand source code
class ProjectToBoundary(NGSolveOperator):
    """Projects an element to the boundary of codomain.bdr. Given the domain is the codomain 
    this simplifies to taking ngs.Projector vor the given vectors. Note that to prevent change
    in the argument the argument will be copied before using ngs.Projector.

    Parameters
    ----------
    domain : NgsVectorSpace
        Domain from which to project.
    codomain : NgsVectorSpace, optional
        Codomain onto which to project. Defaults: domain
    """

    def __init__(self, 
            domain: NgsVectorSpace, 
            codomain: NgsVectorSpace | types.NoneType = None) -> None:
        codomain = codomain or domain
        self.same_domain = codomain == domain
        super().__init__(domain, codomain)
        self.linear=True
        self.bdr = codomain.bdr
        if self.same_domain:
            self._x_eval = self.codomain.zeros()
        else:
            self.gfu_codomain = ngs.GridFunction(self.codomain.fes)
            self._x_eval = NgsBaseVector(self.gfu_codomain.vec)
            self.gfu_domain = ngs.GridFunction(self.domain.fes)
            self._y_eval = NgsBaseVector(self.gfu_domain.vec)
        
    def _eval(self, 
        x : NgsBaseVector) -> NgsBaseVector:
        if self.same_domain:
            self._x_eval = x.copy()
            ngs.Projector(~self.domain.fes.FreeDofs(), range=True).Project(self._x_eval.vec)
        else:
            self.gfu_domain.vec.data = x.vec
            self.gfu_codomain.Set(self.gfu_domain, definedon=self.codomain.fes.mesh.Boundaries(self.bdr))
        return self._x_eval

    def _adjoint(self, 
        x : NgsBaseVector) -> NgsBaseVector:
        if self.same_domain:
            self._x_eval = x.copy()
            ngs.Projector(~self.domain.fes.FreeDofs(), range=True).Project(self._x_eval.vec)
            return self._x_eval
        else:
            self.gfu_codomain.vec.data = x.vec
            self.gfu_domain.Set(self.gfu_codomain, definedon=self.codomain.fes.mesh.Boundaries(self.bdr))
            return self._y_eval

Projects an element to the boundary of codomain.bdr. Given the domain is the codomain this simplifies to taking ngs.Projector vor the given vectors. Note that to prevent change in the argument the argument will be copied before using ngs.Projector.

Parameters

domain : NgsVectorSpace
Domain from which to project.
codomain : NgsVectorSpace, optional
Codomain onto which to project. Defaults: domain

Ancestors

Inherited members

class EIT (domain, g, codomain=None, alpha=0.01)
Expand source code
class EIT(NGSolveOperator):
    r"""Electrical Impedance Tomography Problem
    
    PDE:
    \[
    -\textrm{div}(s \nabla u)+\alpha u=0 \;\text{ in } \Omega
    \]
    \[
         s \frac{\textrm{d}u}{\textrm{d}n} = g \;\text{ on } \partial\Omega
    \]
    Evaluate: \(F\colon s \mapsto \mathrm{tr}(u)\)
    Derivative:
    \[
    -\textrm{div}(s \nabla v)+\alpha v=\textrm{div}(h \nabla u) (=:f) 
    \]
    \[
         s \frac{\textrm{d}v}{\textrm{d}n} = 0 +(-h\frac{\textrm{d}u}{\textrm{d}n} \;\text{ [second term often omitted] } 
    \]

    Der: \(F'[s]\colon h \mapsto \textrm{tr}(v)\)

    Adjoint:
    \[
    -\textrm{div}(s \nabla w)+\alpha w=0 
    \]
    \[
         s \frac{\textrm{d}w}{\textrm{d}n} = q 
    \]
    
    Adj: \(F'[s]^*\colon q \mapsto -\nabla(u) \nabla(w)\)

    Proof:
    \[(F'h, q)=\int_{\partial\Omega} [\textrm{tr}(v) q] \]
    \[= \int_{\partial\Omega} [\textrm{tr}(v) s \frac{\textrm{d}w}{\textrm{d}n}] \] 
    \[= \int_{\Omega} [\textrm{div}(v s \nabla w )]\]
    Note \(\textrm{div}(s \nabla w) = \alpha*w\), thus above equation shows:
    \[(F'h, q) = (s \nabla v, \nabla w)+\alpha (v, w) \]
    \[= \int_\Omega [\textrm{div}( s \nabla v w)] +(-\textrm{div} (s \nabla v)), w)+\alpha (v, w)\]
    \[= \int_{\partial\Omega} [s dv/dn \textrm{tr}(w)]+(f, w)\]
    \[= (f, w)-\int_{\partial\Omega} [\textrm{tr}(w) h \frac{\textrm{d}u}{\textrm{d}n}]\]
    \[= (h, -\nabla u \nabla w) + \int_\Omega [\textrm{div}(h \nabla u w)]-\int_{\partial\Omega} [\textrm{tr}(w) h \frac{\textrm{d}u}{\textrm{d}n}] \]
    The last two terms are the same! It follows: \((F'h, q) = (h, -\nabla u \nabla w)\). Hence:
    Adjoint: \(q \mapsto -\nabla u \nabla w\)
    """

    def __init__(self, domain, g, codomain=None, alpha=0.01):
        codomain = codomain or domain
        #Need to know the boundary to calculate Neumann bdr condition
        assert codomain.bdr is not None
        super().__init__(domain, codomain)
        self.g = g
        self.nr_bc = len(self.g)

        self.fes_domain = domain.fes
        self.fes_codomain = codomain.fes

        #FES and Grid Function for reading in values
        self.fes_in = ngs.H1(self.fes_codomain.mesh, order=1)
        self.gfu_in = ngs.GridFunction(self.fes_in)

        # grid functions for later use
        self.gfu_eval = ngs.GridFunction(self.fes_codomain)  # solution, return value of _eval
        self.gfu_deriv = ngs.GridFunction(self.fes_codomain)  # grid function return value of derivative
        self.gfu_adjoint = ngs.GridFunction(self.fes_domain) #grid function return value of adjoint
        
        self.gfu_bf = ngs.GridFunction(self.fes_codomain) # grid function for defining integrator (bilinearform)
        self.gfu_lf = ngs.GridFunction(self.fes_codomain)  # grid function for defining right hand side (linearform), f
        self.gfu_b = ngs.GridFunction(self.fes_codomain)

        self.gfu_inner_adjoint = ngs.GridFunction(self.fes_codomain)  # grid function for inner computations in adjoint

        self.Number = ngs.NumberSpace(self.fes_codomain.mesh)
        #r, s = self.Number.TnT()

        u, v = self.fes_codomain.TnT()

        # Define Bilinearform, will be assembled later
        self.a = ngs.BilinearForm(self.fes_codomain, symmetric=True)
        self.a += (ngs.grad(u) * ngs.grad(v) * self.gfu_bf+alpha*u*v) * ngs.dx

        #Additional condition: The integral along the boundary vanishes
        #self.a += ngs.SymbolicBFI(u * s + v * r, definedon=self.fes_codomain.mesh.Boundaries("cyc"))
        #self.fes1 = ngs.H1(self.fes_codomain.mesh, order=4, definedon=self.fes_codomain.mesh.Boundaries("cyc"))

        # Define Linearform for evaluation, will be assembled later       
        self.b = ngs.LinearForm(self.fes_codomain)
        self.b += self.gfu_b*v*ngs.ds(codomain.bdr)

        # Define Linearform for derivative, will be assembled later
        self.f_deriv = ngs.LinearForm(self.fes_codomain)
        self.f_deriv += -self.gfu_lf * ngs.grad(self.gfu_eval) * ngs.grad(v) * ngs.dx

        # Initialize preconditioner for solving the Dirichlet problems by ngs.solvers.BVP
        self.prec = ngs.Preconditioner(self.a, 'direct')
    #Weak formulation:
    #0=int_Omega [-div(s grad u) v + alpha u v]=-int_dOmega [s du/dn trace(v)]+int_Omega [s grad u grad v + alpha u v]
    #Hence: int_Omega [s grad u grad v + alpha u v] = int_dOmega [g trace(v)]
    #Left term: Bilinearform self.a
    #Righ term: Linearform self.b
    def _eval(self, diff, differentiate=False, adjoint_derivative=False):
        # Assemble Bilinearform
        self._read_in(diff, self.gfu_bf)
        self.a.Assemble()

        # Assemble Linearform, boundary term
        toret = []
        for i in range(self.nr_bc):
            self.gfu_b.Set(self.g[i])
            self.b.Assemble()

        # Solve system
            if i == 0:
                self._solve_dirichlet_problem(bf=self.a, lf=self.b, gf=self.gfu_eval, prec=self.prec)
            else: 
                self._solve_dirichlet_problem(bf=self.a, lf=self.b, gf=self.gfu_eval, prec=self.prec)

            toret.append(self.gfu_eval.vec.FV().NumPy()[:].copy())

        return np.array(toret).flatten()

#Weak Formulation:
#0 = int_Omega [-div(s grad v) w + alpha v w]-int_Omega [div (h grad u) w]
#=-int_dOmega [s dv/dn trace(w)] + int_Omega [s grad v grad w + alpha v w]-int_dOmega [h du/dn trace(w)]+int_Omega [h grad u grad w]
#=int_Omega [s grad v grad w + alpha v w]+int_Omega [h grad u grad w]
#Hence: int_Omega [s grad v grad w + alpha v w] = int_Omega [-h grad u grad w]
#Left Term: Bilinearform self.a, already defined in _eval
#Right Term: Linearform f_deriv
    def _derivative(self, h, **kwargs):
        # Bilinearform already defined from _eval

        # Assemble Linearform
        toret = []
        for i in range(self.nr_bc):
            self._read_in(h[i], self.gfu_lf)
            self.f_deriv.Assemble()

            self.gfu_deriv.Set(0)
            self._solve_dirichlet_problem(bf=self.a, lf=self.f_deriv, gf=self.gfu_deriv, prec=self.prec)

            toret.append(self.gfu_deriv.vec.FV().NumPy()[:].copy())

        return np.array(toret).flatten()

#Same problem as in _eval
    def _adjoint(self, argument):
        # Bilinearform already defined from _eval

        # Definition of Linearform
        # But it only needs to be defined on boundary
        if self.nr_bc==1:
            argument_tuple = [argument]
        else:
            argument_tuple = self.codomain.split(argument)
        toret = np.zeros(np.size(self.gfu_adjoint.vec.FV().NumPy()))
        for i in range(self.nr_bc):
            self.gfu_b.vec.FV().NumPy()[:] = argument_tuple[i]
            self.b.Assemble()

            self._solve_dirichlet_problem(bf=self.a, lf=self.b, gf=self.gfu_inner_adjoint, prec=self.prec)

            self.gfu_adjoint.Set(-ngs.grad(self.gfu_inner_adjoint) * ngs.grad(self.gfu_eval))

            toret += self.gfu_adjoint.vec.FV().NumPy().copy()

        return toret

Electrical Impedance Tomography Problem

PDE: -\textrm{div}(s \nabla u)+\alpha u=0 \;\text{ in } \Omega s \frac{\textrm{d}u}{\textrm{d}n} = g \;\text{ on } \partial\Omega Evaluate: F\colon s \mapsto \mathrm{tr}(u) Derivative: -\textrm{div}(s \nabla v)+\alpha v=\textrm{div}(h \nabla u) (=:f) s \frac{\textrm{d}v}{\textrm{d}n} = 0 +(-h\frac{\textrm{d}u}{\textrm{d}n} \;\text{ [second term often omitted] }

Der: F'[s]\colon h \mapsto \textrm{tr}(v)

Adjoint: -\textrm{div}(s \nabla w)+\alpha w=0 s \frac{\textrm{d}w}{\textrm{d}n} = q

Adj: F'[s]^*\colon q \mapsto -\nabla(u) \nabla(w)

Proof: (F'h, q)=\int_{\partial\Omega} [\textrm{tr}(v) q] = \int_{\partial\Omega} [\textrm{tr}(v) s \frac{\textrm{d}w}{\textrm{d}n}] = \int_{\Omega} [\textrm{div}(v s \nabla w )] Note \textrm{div}(s \nabla w) = \alpha*w, thus above equation shows: (F'h, q) = (s \nabla v, \nabla w)+\alpha (v, w) = \int_\Omega [\textrm{div}( s \nabla v w)] +(-\textrm{div} (s \nabla v)), w)+\alpha (v, w) = \int_{\partial\Omega} [s dv/dn \textrm{tr}(w)]+(f, w) = (f, w)-\int_{\partial\Omega} [\textrm{tr}(w) h \frac{\textrm{d}u}{\textrm{d}n}] = (h, -\nabla u \nabla w) + \int_\Omega [\textrm{div}(h \nabla u w)]-\int_{\partial\Omega} [\textrm{tr}(w) h \frac{\textrm{d}u}{\textrm{d}n}] The last two terms are the same! It follows: (F'h, q) = (h, -\nabla u \nabla w). Hence: Adjoint: q \mapsto -\nabla u \nabla w

Ancestors

Inherited members

class ReactionNeumann (domain, g, codomain=None)
Expand source code
class ReactionNeumann(NGSolveOperator):
    """
    Estimation of the reaction coefficient from boundary value measurements

    PDE: -div(grad(u)) + s*u = 0 in Omega
         du/dn = g on dOmega

    Evaluate: F: s \mapsto trace(u)
    Derivative:
        -div(grad(v))+s*v = -h*u (=:f)
        dv/dn = 0 

    Der: F'[s]: h \mapsto trace(v)

    Adjoint: 
        -div(grad(w))+s*w = 0
        dw/dn = q
    Adj: F'[s]^*: q \mapsto -u*w

    proof:
    (F'h, q) = int_dOmega [trace(v) q] = int_dOmega [trace(v) dw/dn] = int_Omega [div(v grad w)] 
    = int_Omega [grad v grad w] + int_Omega [v div( grad w)] = int_Omega [div(w grad v)] - int_Omega [div(grad v) w] + int_Omega [v div (grad w)]
    = int_dOmega [trace(w) dv/dn] - int_Omega [h u w] - int_Omega [s v w] + int_Omega [v s w]
    Note that dv/dn=0 on dOmega. Hence:
    (F'h, q) = -int_Omega[h u w] = (h, -u w)
    """
    def __init__(self, domain, g, codomain=None):
        codomain = codomain or domain
        #Need to know the boundary to calculate Neumann bdr condition
        assert codomain.bdr is not None
        super().__init__(domain, codomain)
        self.g = g
        self.nr_bc = len(self.g)

        self.fes_domain = domain.fes
        self.fes_codomain = codomain.fes

        # grid functions for later use
        self.gfu_eval = ngs.GridFunction(self.fes_codomain)  # solution, return value of _eval
        self.gfu_deriv = ngs.GridFunction(self.fes_codomain)  # grid function: return value of derivative
        self.gfu_adjoint = ngs.GridFunction(self.fes_domain)  # grid function: return value of adjoint

        self.gfu_bf = ngs.GridFunction(self.fes_codomain)  # grid function for defining integrator of bilinearform
        self.gfu_lf = ngs.GridFunction(self.fes_domain) # grid function for defining linearform
        self.gfu_b = ngs.GridFunction(self.fes_codomain)  # grid function for defining the boundary term

        self.gfu_inner_adjoint = ngs.GridFunction(self.fes_codomain)  # grid function for inner computation in adjoint

        #Test and Trial Function
        u, v = self.fes_codomain.TnT()

        # Define Bilinearform, will be assembled later
        self.a = ngs.BilinearForm(self.fes_codomain, symmetric=True)
        self.a += (ngs.grad(u) * ngs.grad(v) + u * v * self.gfu_bf) * ngs.dx

        # Boundary term
        self.b = ngs.LinearForm(self.fes_codomain)
        self.b += -self.gfu_b * v.Trace() * ngs.ds(codomain.bdr)

        # Linearform (only appears in derivative)
        self.f_deriv = ngs.LinearForm(self.fes_codomain)
        self.f_deriv += -self.gfu_lf * self.gfu_eval * v * ngs.dx

        # Initialize preconditioner for solving the Dirichlet problems by ngs.solvers.BVP
        self.prec = ngs.Preconditioner(self.a, 'direct')


    def _eval(self, diff, differentiate=False, adjoint_derivative=False):
        # Assemble Bilinearform
        self._read_in(diff, self.gfu_bf)
        self.a.Assemble()

        # Assemble Linearform of boundary term
        toret = []
        for i in range(self.nr_bc):
            self.gfu_b.Set(self.g[i])
            self.b.Assemble()

        # Solve system
            if i == 0:
                self._solve_dirichlet_problem(bf=self.a, lf=self.b, gf=self.gfu_eval, prec=self.prec)
            else:
                self._solve_dirichlet_problem(bf=self.a, lf=self.b, gf=self.gfu_eval, prec=self.prec)

            toret.append(self.gfu_eval.vec.FV().NumPy()[:].copy())

        return np.array(toret).flatten()

    def _derivative(self, h):
        # Bilinearform already defined from _eval

        # Assemble Linearform of derivative
        toret = []
        for i in range(self.nr_bc):
            self._read_in(h, self.gfu_lf)
            self.f_deriv.Assemble()

            # Solve system
            self._solve_dirichlet_problem(bf=self.a, lf=self.f_deriv, gf=self.gfu_deriv, prec=self.prec)

            toret.append(self.gfu_deriv.vec.FV().NumPy()[:].copy())

        return np.array(toret).flatten()

    def _adjoint(self, argument):
        # Bilinearform already defined from _eval

        # Definition of Linearform
        # But it only needs to be defined on boundary
        if self.nr_bc==1:
            argument_tuple = [argument]
        else:
            argument_tuple = self.codomain.split(argument)
        toret = np.zeros(np.size(self.gfu_adjoint.vec.FV().NumPy()))
        for i in range(self.nr_bc):
            self.gfu_b.vec.FV().NumPy()[:] = argument_tuple[i]
            self.b.Assemble()

        # Solve system
            self._solve_dirichlet_problem(bf=self.a, lf=self.b, gf=self.gfu_inner_adjoint, prec=self.prec)

            self.gfu_adjoint.Set(self.gfu_inner_adjoint * self.gfu_eval)
        
            toret+=self.gfu_adjoint.vec.FV().NumPy().copy()

        return toret

Estimation of the reaction coefficient from boundary value measurements

PDE: -div(grad(u)) + s*u = 0 in Omega du/dn = g on dOmega

Evaluate: F: s \mapsto trace(u)

Derivative

-div(grad(v))+sv = -hu (=:f) dv/dn = 0

Der: F'[s]: h \mapsto trace(v)

Adjoint: -div(grad(w))+sw = 0 dw/dn = q Adj: F'[s]^: q \mapsto -u*w

proof: (F'h, q) = int_dOmega [trace(v) q] = int_dOmega [trace(v) dw/dn] = int_Omega [div(v grad w)] = int_Omega [grad v grad w] + int_Omega [v div( grad w)] = int_Omega [div(w grad v)] - int_Omega [div(grad v) w] + int_Omega [v div (grad w)] = int_dOmega [trace(w) dv/dn] - int_Omega [h u w] - int_Omega [s v w] + int_Omega [v s w] Note that dv/dn=0 on dOmega. Hence: (F'h, q) = -int_Omega[h u w] = (h, -u w)

Ancestors

Inherited members