Using the DPG spectral projector

This package provides class SpectralProjDPG, which is a class of approximations to the spectral projector of an operator $\mathbb{A} : \mathbb{U} \rightarrow \mathbb{U}$, using DPG methods. Here $\mathbb U$ is a Hilbert space (usually $L^2$) and $\mathbb A$ is an unbounded operator on $\mathbb U$. The spectral projector $$ \mathbb{P} = \frac{1}{2 \pi i} \int_C (z-\mathbb{A})^{-1} dz $$ over a circular contour $C$ is approximated by replacing the integral by a trapezoidal quadrature and by replacing $\mathbb{A}$ by an approximation obtained from the primal DPG method. This and further documentation are available in the class file.

In [1]:
from pyeigfeast import  SpectralProjDPG
In [2]:
help(SpectralProjDPG)
Help on class SpectralProjDPG in module pyeigfeast.spectralproj.dpg.spectralprojdpg:

class SpectralProjDPG(pyeigfeast.spectralproj.spectralproj.SpectralProj)
 |  SpectralProjDPG(minus_bb_integrators, y_integrators, A, M, X, spaces, uindex, yindex, mesh, radius, center, npts, reduce_sym=False, quadrule='circ_trapez_shift', rhoinv=0.0, verbose=True)
 |  
 |  This class approximates the spectral projector of an operator
 |  AA : UU -> UU, namely
 |  
 |  PP = (2 pi i)^-1   \int_C  (z-AA)^-1  dz,
 |  
 |  by an approximation P which is obtained by approximating AA by the
 |  primal DPG method and the integral by the trapezoidal rule. The
 |  background for this class and the notations assigned to class data
 |  members follow this description:
 |  
 |  * UU is a Hilbert space (infinite dimensional). We are interested
 |    in approximating an eigenpair l, y satisfying AA * y = l * y.
 |  
 |  * U = a finite element subspace of UU, wherein the
 |    eigenvectors are computed.
 |  
 |  * The DPG approximation of the resolvent (z - AA)^-1 * f, for any
 |    f in U, is the function u in U computed by solving e in Y, u in
 |    U, and q in Q, satisfying
 |  
 |    y(e,d) + z * s(u,d) - b((u,q),d) = s(f,d)   for all d in Y,
 |             z * s(w,e) - b((w,r),e) = 0        for all w in U, r in Q.
 |  
 |    The left hand side is written in combined Hermitian form as
 |  
 |    B(u,q,e|w,r,d) =  y(e,d) + z * s(u,d) - b((u,q),d)
 |                   + conjugate(z * s(w,e) - b((w,r),e)).
 |  
 |  * Above, Y and Q are auxiliary spaces used by the DPG method to
 |    approximate error representation and other/interface variables.
 |    We assume that U is contained in Y.
 |  
 |  * y(.,.) = inner product of Y.
 |  
 |  * b(.,.) : (U x Q) x Y -> C, primal DPG weak form for AA.
 |  
 |  * s(.,.) : U x Y -> C,  form used to transfer functions from U to Y.
 |  
 |  * X = X[0] x X[1] x ...  is a Cartesian product of finite element
 |    spaces used by the DPG space to approximately solve (z - AA)u=f.
 |    Both U and Y are present as component spaces of X.
 |  
 |  * m(.,.) is the inner product of U and the Gram matrix of this
 |    inner product with respect to the working basis of U is M.
 |  
 |  * A is the stiffness matrix of the form   m(AA u,w).
 |  
 |  Method resolution order:
 |      SpectralProjDPG
 |      pyeigfeast.spectralproj.spectralproj.SpectralProj
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  __init__(self, minus_bb_integrators, y_integrators, A, M, X, spaces, uindex, yindex, mesh, radius, center, npts, reduce_sym=False, quadrule='circ_trapez_shift', rhoinv=0.0, verbose=True)
 |      Constructor of DPG spectral projection approximation:
 |      
 |      INPUTS:
 |      
 |      * y_integrators = list of ngsolve bilinear form integrators
 |        (bfi) to implement y(e,d).
 |      
 |      * minus_bb_integrators = list of ngsolve bfi's on X that
 |        implement  -b((u,q),d) - conjugate( b((w,r),e) ).
 |      
 |      * ss_integrators = list of ngsolve bfi to implement
 |        alpha * s(u,d) + beta * s(w,e).
 |      
 |      * M = mass matrix of m(u,w) inner product using U's basis.
 |      
 |      * A = stiffness matrix of the form m(A u, w) using U's basis.
 |      
 |      * mesh = finite element mesh as an ngsolve Mesh object.
 |      
 |      * X = finite element space on mesh containing U, Y and other
 |        needed component spaces.
 |      
 |      * uindex, yindex are (0-based) indices into X such that
 |        U = X[uindex] and Y = X[yindex].
 |      
 |      * radius, center, npts: Spectral projector approximation is
 |        based on an integral around a circular contour of given
 |        'center' and 'radius', discretized using the trapezoidal
 |        rule with 'npts' number of points. If 'npts' is odd, it is
 |        converted to the next higher even number.
 |      
 |      * reduce_sym: Reduce the number of quadrature points assuming
 |        that the resolvent approximation R(z) at a point z
 |        satisfies   R(conj(z)) * f = conj( R(z) * conj(f) ).
 |  
 |  __mul__(self, v)
 |      Return P * v where P is the spectral projector object
 |      and v is a span object.
 |  
 |  apply_resolvent(self, i, v)
 |      Return DPG approximation to  u  ~  (z[i] - AA)^-1 * v.
 |      Input v and output u are both in the space U.
 |  
 |  augment(self, y)
 |      Enrich the given span 'y' by more vectors, or somehow
 |      with more eigenspace content. This function is called when
 |      the feast algorithm (below) restarts after a convergence failure.
 |  
 |  compute_error(self, Y, lam, exactewf)
 |      Compute and report eigenvalue and eigenvector errors. Eigenvector
 |      error measure reported is the distance FROM interpolated
 |      exact eigenfunction TO computed eigenspace.
 |      
 |      INPUT: exactewf = (ew, ef) where
 |                ew[l] = l-th exact eigenvalue, sorted by real part,
 |                ef[l] = corresponding eigenfunction as ngspy coefficient.
 |             Y = computed eigenspace span.
 |             lam = computed eigenvalues.
 |      OUTPUT:
 |             ewdiff[l] = ewh[l] - ew[l],
 |                         where ewh equals lam sorted by its real part.
 |             efdiff[l] = || ef[l] - (projection of ef[l] into Y) ||_A.
 |  
 |  compute_errror_discrete2exact(self, Y, lam, exactewf)
 |      Compute and report eigenvalue and eigenvector errors,
 |      but compared against the span of the interpolants of
 |      the exact eigenfunctions contained in exactewf. The eigenvector
 |      error measure reported is
 |      
 |       distance FROM computed eigenfn TO exact (interpolated) eigenspace.
 |      
 |      Note that another method compute_error(...) yields the alternate
 |      eigenspace error measure
 |      
 |       distance FROM interpolated exact eigenfunction TO computed eigenspace.
 |      
 |      INPUT: exactewf = (ew, ef) where
 |                ew[l] = l-th exact eigenvalue, sorted by real part,
 |                ef[l] = corresponding eigenfunction as ngspy coefficient.
 |             Y = computed eigenspace span (with basis y[i] representing
 |                 output eigenfunctions)
 |             lam = computed eigenvalues.
 |      OUTPUT:
 |             ewdiff[l] = ewh[l] - ew[l],
 |                         where ewh equals lam sorted by its real part.
 |             efdiff_discrete2exact[l] =
 |                      || y[l] - (projection of y[l] into interpE) ||_A.
 |                         where eih is the lth eigenfunction computed by
 |                         FEAST, and interpE is set of exact eigenfunctions
 |                         interpolated.
 |  
 |  matrices_scipy(self)
 |      Return sparse matrices related to DPG resolvent computation.
 |      
 |      To describe the matrices, recall that the resolvent
 |      approximation u = R_h(z,f) solves
 |      
 |          B(u,q,e|w,r,d) =  s(f,d)
 |      
 |      where
 |      
 |      B(u,q,e|w,r,d) =  y(e,d) + z * s(u,d) - b((u,q),d)
 |                     +     conj( z * s(w,e) - b((w,r),e) )
 |      
 |      for some forms s and y (usually the L^2 and Y inner products).
 |      
 |      This function returns the following:
 |      
 |         B: stiffness matrix of b(.,.) (with no z-term)
 |      
 |         S: stiffness matrix of s(.,.)
 |      
 |         Bfree: list of free dof indices in B. These are (0-based)
 |              indices into the degrees of freedom of the space X.
 |              Indices not in Bfree are nodes that are constrained by
 |              Dirichlet or other conditions.
 |      
 |         space_dofs: The space X=X0 x X1 x …  has its degrees of
 |              freedom (dofs) split in component spaces as follows:
 |              The dofs of Xi are enumerated from space_dofs[i]
 |              through space_dofs[i+1]-1.
 |      
 |         M: The Gram matrix of the U-inner product (usually L^2
 |              inner product), which we denote by m(.,.).
 |      
 |         A: The matrix of the form a(u,w)=m(AA u,v) where AA is the
 |              exact operator.
 |      
 |         Afree: This is similar to Bfree, but the count is using the
 |              U-basis (instead of the X -basis).
 |  
 |  matrix_filter(self)
 |      Return the matrix approximation S_N^h of the exact spectral
 |      projector S, as a dense numpy matrix. (Memory intensive!)
 |      Only the restriction of the matrix to free dofs is returned.
 |  
 |  projectA(self, e, Y)
 |      Project the vector e onto the span object Y  in the A-norm.
 |      (Note that e should be input as BaseVector of U gridfunction.)
 |  
 |  rayleigh(self, q)
 |      Return matrices qAq and qMq whose entries are
 |      
 |      qAq[i,j] = ( A * q[j], q[i] )
 |      qMq[i,j] = ( q[j], q[i] )
 |  
 |  reduce_by_symmetry(self)
 |      Reduce the number of quadrature points assuming that
 |         R(conj(z)) * f = conj( R(z) * conj(f) ).
 |      When this assumption holds, the resolvent approximations
 |      are not made at conjugate quadrature points.
 |  
 |  residual(self, lam, y)
 |      Return suitable norms of the residuals A y[i] - lam[i] * y[i].
 |  
 |  zoom(self, ew, delta=None)
 |      simple, multiple = SpectralProjDPG.zoom(ew, delta)
 |      
 |      Computes spectral projectors, one corresponding to each
 |      eigenvalue in "ew", counting those eigenvalues that differ by
 |      less than "delta" as approximations of an eigenvalue of
 |      multiplicity>1.  The outputs "simple" and "multiple" are dicts
 |      documented in spectralproj.simple_multiple_zoom. This method
 |      adds 'specproj' to these dicts, the corresponding spectral projector
 |      object.
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from pyeigfeast.spectralproj.spectralproj.SpectralProj:
 |  
 |  feast(self, Y, stop_tol=1e-09, cut_tol=1e-09, nrestarts=10, niterations=30, checkafter=3, reportresiduals=False, exactewf=None)
 |      An implementation of the FEAST algorithm:
 |      
 |      INPUTS:
 |      
 |      Y: Initial space (a Span object).
 |      
 |      stop_tol: Stop when eigenvalues differ by this tolerance.
 |      
 |      cut_tol: Tolerance used in decisions to reduce span to match
 |      the eigenspace dimension.
 |      
 |      nrestarts: Number of times FEAST should restart and try again
 |      upon convergence failure.
 |      
 |      niterations: Number of FEAST iterations to do in each restart
 |      (unless convergence is reached).
 |      
 |      OUTPUTS:
 |      
 |      lam: Computed approximation of eigenvalues (a 1D numpy array).
 |      
 |      Y: Computed approximation of eigenspace (a span object).
 |      
 |      history = (ewerrors, eferrors, ewchanges, residuals),   where
 |              ewchanges: differences between successive FEAST
 |                         eigenvalue iterates,
 |              ewerrors:  if exact eigenvalue given, this gives the
 |                         eigenvalue errors at each FEAST iteration,
 |              eferrors:  if exact eigenvalue given, this gives the
 |                         eigenfunction errors, computed by the virtual
 |                         function "compute_error" at each FEAST iteration,
 |              residuals: if reportresiduals=True, this gives norms
 |                         of the residual (A x - lam x), computed by the
 |                         virtual function "residual" at each iteration.
 |  
 |  feast_hermitian_step(self, q, cut_tol=1e-09)
 |      Perform one step of the FEAST algorithm assuming that the
 |      operator A, and therefore its spectral projector P, is
 |      self-adjoint. The statement
 |      
 |      lamda, y = P.feast_hermitian(q)
 |      
 |      takes as input the span q and outputs a (possibly truncated
 |      according to 'cut_tol') span y that approximates the eigenspace
 |      and the corresponding eigenvalues lambda.
 |  
 |  filter(self, zeta, setn=None)
 |      Return the value of the filter function
 |      
 |      f(zeta) = sum_k  w[k] / (z[k] - zeta)
 |      
 |      for all values input in the 1d numpy array zeta.
 |  
 |  plotfilter(self, numplotpts=200, setn=None)
 |      Plot the real component of the filter function
 |      
 |      f(zeta) = 1/n * sum_k (z[k] - c) / (z[k] - zeta).
 |      
 |      Note that f(A) is trapezoidal approximation to our spectral
 |      projector
 |      
 |      (2 pi i)^-1  int_C (z - A)^-1 dz
 |      
 |      using n equally spaced points on the circle C.  If you like to
 |      set n to a value N that is different from the number of points
 |      of this object's trapezoidal rule, then use setn=N.
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors inherited from pyeigfeast.spectralproj.spectralproj.SpectralProj:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

Make the spectral projector object

  • In order to make an object of class SpectralProjDPG, we set these parameters.
In [3]:
# PARAMETERS:

h = 0.1   # mesh size for finite element discretization
p = 2     # polynomial degree of DPG discretization
c = 19.0  # integration contour center 
r = 40    # integration contour radius 
n = 10    # number of points in trapezoidal rule
  • The last three parameters above determine the integration contour $$ C = \{ z \in \mathbb C: | z - c | < r \} $$ and the number of points in the trapezoidal rule.
  • The first parameter h is used to create an unstructured mesh of the unit square using Netgen/NGSolve next:
In [4]:
import netgen.gui
%gui tk
from netgen.geom2d import unit_square
import ngsolve as ngs

# Make a mesh of unit square of mesh size h:
ngm = unit_square.GenerateMesh(maxh=h)
mesh = ngs.Mesh(ngm)
for i, bdries in enumerate(mesh.GetBoundaries()):
    ngm.SetBCName(i, 'allbdry')   
    
# Visualize the just created mesh 
ngs.Draw(mesh)  
  • Next, we need the forms and the spaces required to make the spectral projector approximation. The details of how to do this are in methods inputs_for_dpg within the script dirichletDPG.py. Postponing the discussion of these details, let us try these:
In [5]:
from pyeigfeast import inputs_for_dpg
        
# Get all the info required for the DPG Laplace operator
    
minus_bb_integrators, y_integrators, \
Amat, Mmat, X, \
component_spaces, uindex, yindex, mesh \
    = inputs_for_dpg(mesh, p)

# Make the spectral projector object 

P = SpectralProjDPG(minus_bb_integrators, y_integrators,
                    Amat, Mmat,
                    X, component_spaces, uindex, yindex, mesh,
                    r, c, n)
SpectralProj: Setting shifted trapezoidal rule quadrature on circular contour
SpectralProj: Radius=40, Center=19
SpectralProjDPG: Computing DPG resolvents along the contour:
SpectralProjDPG:   Factorizing at z = +57.042+12.361j
SpectralProjDPG:   Factorizing at z = +42.511+32.361j
SpectralProjDPG:   Factorizing at z = +19.000+40.000j
SpectralProjDPG:   Factorizing at z =  -4.511+32.361j
SpectralProjDPG:   Factorizing at z = -19.042+12.361j
SpectralProjDPG:   Factorizing at z = -19.042-12.361j
SpectralProjDPG:   Factorizing at z =  -4.511-32.361j
SpectralProjDPG:   Factorizing at z = +19.000-40.000j
SpectralProjDPG:   Factorizing at z = +42.511-32.361j
SpectralProjDPG:   Factorizing at z = +57.042-12.361j

Now you have a DPG spectral projector object called $P$. What can we do with it? We can ask it to run the FEAST algorithm, give us matrices in Scipy format, plot the rational filter, etc., as we shall see shortly.

A basic operation is to apply operator P to span objects (which are just a collection of vectors/functions). This is accomplished through the multiplication operator *. First, let us create a random span.

In [6]:
from pyeigfeast import  NGvecs

U = component_spaces[uindex]          # Get the space 
Y = NGvecs(U, 4,  Mmat)               # Create a span object 
Y.setrandom()                         # Set random values 
Y.draw()

In your netgen window, you should see a random function.

In fact Y has 4 component functions (see how we created it above).

In [7]:
Y.data
Out[7]:
[basevector, basevector, basevector, basevector]

What you are seeing in the netgen window os the first basevector represented as a grid function. Clicking on the Visual menu, and selecting various multidim-component you can visualize the other random functions.

We apply the spectral projector P to Y as follows:

In [8]:
PY = P * Y
PY.draw()

You see already that the random functions begin to look more structured, almost like the first eigenfunction. Before we compute all the eigenfunctions properly using FEAST, consider the following.

Reducing computations by symmetry

Go back to the outputs printed when we constructed P. It prints all the points (on a circular contour) where the resolvent approximations were made. Whenever your resolvent approximation $R_h(z)$ has the property that $$ R_h(\overline{z}) f = \overline{ R_h(z) * \overline{f} } $$ we may omit the factorization at $\overline{z}$ (as the application of the resolvent at $\overline{z}$ can be accomplished using the factorization at $z$). This reduction of factorizatoins by symmetry is possible for the above DPG discretization.

In [9]:
P.reduce_by_symmetry()
SpectralProjDPG: Attempting reduction by symmetry and recomputing factorizations
SpectralProjDPG: Computing DPG resolvents along the contour:
SpectralProjDPG:   Factorizing at z = +57.042+12.361j
SpectralProjDPG:   Factorizing at z = +42.511+32.361j
SpectralProjDPG:   Factorizing at z = +19.000+40.000j
SpectralProjDPG:   Factorizing at z =  -4.511+32.361j
SpectralProjDPG:   Factorizing at z = -19.042+12.361j

Now, P has been remade with only half the previous number of factorizations.

Let's double-check that after this reduction the action of P on the same span Y remains unaltered!

In [10]:
PY2 = P * Y
for i in range(len(PY.data)):
    PY2.data[i] -= PY.data[i]
    print(ngs.Norm(PY2.data[i]))

    
5.938492606032896e-14
6.793137904790364e-14
1.034962279694313e-13
6.412567517705383e-14

Indeed they are the same.

You could also have directly created P with this reduction by providing the kwarg reduce_sym=True to its constructor.

Plot filter function

Before you run FEAST, it is instructive to see the filter function $$ f(\zeta) = \frac 1 n \sum_{k=1}^n \frac{z_k - c}{ z_k - \zeta}. $$ where $n$ is the number of equally spaced points on the integration contour C, the points used by the trapezoidal rule.

The object $P$ knows how to plot its own filter function. Try help(P.filter) or help(P.plotfilter) to view the documentation. Make sure to load matplotlib first before plotting, as shown below.

In [11]:
%matplotlib inline
import matplotlib as mpl
import matplotlib.pyplot as plt

P.plotfilter()

The indicator function of [c-r, c+r] is the ideal filter. This above displayed filter, made using only 10 points on the large contour, is not a very sharp approximation of this indicator function.

You can ask $P$ to plot the filter one would have got, had one used, say, a 100 points. (Note that this will not change the points used within $P$. The temporary 100-point-setting will only be used for plotting the filter.)

In [12]:
P.plotfilter(setn=100)
SpectralProj: Setting shifted trapezoidal rule quadrature on circular contour
SpectralProj: Radius=40, Center=19
  • While this is clearly a better-looking filter, it is also much more expensive (requiring 100 boundary value problems to be solved for each application of $P$). Hence we will continue with the 10-point filter.

Run the FEAST algorithm

We continuing in the setting above. The exact (undiscretized) operator is $\mathbb A$, the Dirichlet operator on the unit square, whose exact eigenvalues are of the form $(\ell_1^2 + \ell_2^2) \pi^2$ for positive integers $\ell_1$ and $\ell_2$.

Clearly, within the above set contour $C = \{ z \in \mathbb C: | z - 19 | < 40 \}$, there are only three of these eigenvalues.

To run FEAST, we start by picking a random set of vectors. Below we pick 4 vectors and make a span object of class NGvecs consisting of vectors maintained in the NGSolve format. These vectors must be in the finite element subspace $U$ of $\mathbb U$. The inner product in this space is $L^2$. The mass matrix of this inner product is $M$, which was previously provided by inputs_for_Laplace_unitsqr(h,p). The space $U$ was also provided by the same function. We use them below.

In [13]:
from pyeigfeast import  NGvecs

U = component_spaces[uindex]          # Get the space
Y = NGvecs(U, 4,  Mmat)               # Create a span object 
Y.setrandom()                         # Set random values 
Out[13]:
<pyeigfeast.spectralproj.ngs.spectralprojngs.NGvecs at 0x12acad780>

Now you are ready to run FEAST by typing in the following line:

In [14]:
lam, Y, history = P.feast(Y)
=========== Starting FEAST iterations ===========
Trying with 4 vectors:

 ITERATION 1 with 4 vectors
   Real part of computed eigenvalues:
   [19.73986929 49.35877074 49.36168532 81.94981728]
   Relative eigenvalue changes from prior iterate:
   [2.5e+98 2.5e+98 2.5e+98 2.5e+98]
 ITERATION 2 with 4 vectors
   Real part of computed eigenvalues:
   [19.7398404  49.35791044 49.35853956 78.99850098]
   Relative eigenvalue changes from prior iterate:
   [7.22373432e-07 2.15075281e-05 7.86439855e-05 7.37829077e-02]
 ITERATION 3 with 4 vectors
   Real part of computed eigenvalues:
   [19.7398404  49.35791045 49.35853956 78.99686859]
   Relative eigenvalue changes from prior iterate:
   [1.48727253e-11 1.87112903e-10 2.56562771e-11 4.08096159e-05]
   Removing ew#[3] not enclosed by contour
 ITERATION 4 with 3 vectors
   Real part of computed eigenvalues:
   [19.7398404  49.35791046 49.35853956]
   Relative eigenvalue changes from prior iterate:
   [1.90957028e-10 1.80141058e-10 5.26780397e-11]

============= FEAST iterations done =============

Compare the computed eigenvalues with the exact ones. To view the computed eigenvalues, simply type in the name of the output array containing them:

In [15]:
lam
Out[15]:
array([19.7398404 , 49.35791046, 49.35853956])

Type help(P.feast) to learn more about the possible optional and output arguments of this implementation of the the FEAST algorithm.

Viewing the eigenfunctions

Using the visualization capabilities of Netgen, we can view the eigenfunctions (computed above in Y) after issuing the following command:

In [16]:
Y.draw()

This shows you one of the basis functions of the computed eigenspace Y.

Clicking on the Visual menu, and selecting various multidim-component you can see the other eigenfunctions in Y.

Y.data[0].real Y.data[1].real Y.data[2].real

Notes:

  • The computed eigenfunctions may be complex multiples of real eigenfunctions (so the imaginary part may be nonzero).

  • In the case of non-simple eigenvalues, FEAST computes the eigenspan (since specific eigenvectors may not be unique).

Matrices in scipy and matlab

You can ask P to give you the DPG matrices in scipy.sparse format (and scipy can convert them into Matlab, if you need).

In [17]:
# Extract matrices from pyeigfeast into scipy 

Xmatrix, S, Xfree, space_dofs, A, M, Ufree = P.matrices_scipy()

To understand what the above output scipy objects are, we need to understand a bit about the internals of the DPG method.

Background on DPG matrices

  • The DPG method uses a nonstandard form $ b( (u,q), y ) $ on some space $(U \times Q) \times Y$. The finite-dimensional space $U$, a subspace of the infinite-dimensional $\mathbb U$, is where the eigenfunction approximations lie. Let's ignore the details of the other spaces for now.

  • The spectral projector object $P$ computes an approximation $u$ to $(z - \mathbb A)^{-1} f$ using the DPG method on the space $X = U \times Q \times Y$. Namely, it solves for $(u,q,e) \in X$ such that $$ B( (u,q,e), (w,r,d) ) = s(f,d) $$ for all $(w,r,d) \in X$, where $$ \begin{aligned} B( (u,q,e), (w,r,d) ) & = (e,d)_Y + z s(u,d) - b((u,q),d) \\ & + \overline{ z s(w,e) - b((w,r),e)}. \end{aligned} $$

  • For the Dirichlet eigenproblem, $$ \begin{aligned} b( (u,q), y ) &= \sum_{\text{elements }K} \bigg[\int_K \nabla u \cdot \nabla v\; dx + \int_{\partial K} q \cdot n y \; ds \bigg], \\ s(u,y) & = \int_\Omega u y\; dx, \\ (e,d)_Y & = \sum_{\text{elements }K} \int_K \big( \nabla u \cdot \nabla v + u v \big) dx. \end{aligned} $$

Description of the output matrices

Using the above background, we can now describe the matrices output by P.matrices_scipy() above:

  • Xmatrix: The stiffness matrix of the form $B(\cdot, \cdot)$ with $z=0$ in the basis for $X$.
  • S: The matrix of $s(\cdot,\cdot)$ computed using the basis for $X$. Clearly this matrix will have a large number of zeros (and zeros are not stored in the output sparse format).
  • Xfree: These are (0-based) indices into the degrees of freedom of the space $X$. Indices not in Xfree are nodes that are constrained by Dirichlet or other conditions. You can expect the matrix Xmatrix to be invertible only after removing the rows and columns of indices not in Xfree.
  • space_dofs: The space $X = X_0 \times X_1 \times \ldots$ has its degrees of freedom (dofs) split in component spaces as follows: The dofs of $X_i$ are enumerated from space_dofs[i] through space_dofs[i+1]-1.
  • M: The Gram matrix of the inner product $m(\cdot,\cdot)$ in $U$ using the finite element basis for $U$. In the Dirichlet example, $$ m(u,w) = \int_\Omega u w\; dx. $$ Due to symmetry, only the lower triangular part is stored and returned.
  • A: The matrix of the form $a(u,w) = m(\mathbb{A} u, v)$. For the Dirichlet examples, this is $$ a(u,v) = \int_\Omega \nabla u \cdot \nabla v \; dx. $$
  • Ufree: This is similar to Xfree, but the count is using the $U$-basis (instead of the $X$-basis).

Conversion to Matlab

In [18]:
from scipy.io import savemat

# Convert 0-based scipy indices to 1-bases Matlab indices

Xf = Xfree + 1 
Uf = Ufree + 1
sd = space_dofs + 1

# Write scipy matrices and indices into matlab format 

mdict = {"Xmatrix": Xmatrix, "U2Ytransfer": S, "Xfree": Xf,
         "dimcounts": sd, "A": A, "M": M, "Ufree": Uf}

savemat("dpgmatrices", mdict)

All the matrices you need to work in Matlab or Octave are now saved in dpgmatrices.mat. An example of Matlab code illustrating how to use these matrices is in the file matexample.m which has been tested in Matlab and Octave).

Spectral projector as a matrix

One may think of the linear operator P as a matrix. In rare occasions, when you want this matrix, you can ask for it using the method matrix_filter(). However, since this matrix is likely to be a full dense matrix, this is a memory intensive operation. So let's not try it with the above P, but rather for a new spectral projector made with a very coarse mesh and the lowest possible polynomial degree.

In [19]:
ngm0 = unit_square.GenerateMesh(maxh=0.49)
mesh0 = ngs.Mesh(ngm0)
for i, bdries in enumerate(mesh0.GetBoundaries()):
    ngm0.SetBCName(i, 'allbdry')   
ngs.Draw(mesh0)  

You can see the mesh is now very coarse. Using p=1 as the degree, we make a new coarse spectral projector object P0 and extract it as a numpy matrix S0.

In [20]:
minus_bb_integrators, *rest = inputs_for_dpg(mesh0, 1)
P0 = SpectralProjDPG(minus_bb_integrators, *rest, r, c, n, reduce_sym=True)

S0 = P0.matrix_filter()
SpectralProj: Setting shifted trapezoidal rule quadrature on circular contour
SpectralProj: Radius=40, Center=19
SpectralProjDPG: Computing DPG resolvents along the contour:
SpectralProjDPG:   Factorizing at z = +57.042+12.361j
SpectralProjDPG:   Factorizing at z = +42.511+32.361j
SpectralProjDPG:   Factorizing at z = +19.000+40.000j
SpectralProjDPG:   Factorizing at z =  -4.511+32.361j
SpectralProjDPG:   Factorizing at z = -19.042+12.361j
In [21]:
import numpy as np
np.linalg.norm(S0.imag)
Out[21]:
1.463407881659135e-16

Note that the matrix S0 has virtually no imaginary part. Its real part looks like this:

In [22]:
S0.real
Out[22]:
array([[ 0.5090363 ,  0.19780894,  0.14394051,  0.17707665],
       [ 0.21806227,  0.40937704,  0.19492529, -0.01804949],
       [ 0.14007482,  0.17366323,  0.50721372,  0.19612739],
       [ 0.19392949, -0.02304703,  0.21212648,  0.4178111 ]])
In [23]:
ew, _ = np.linalg.eig(S0)
abs(ew)
Out[23]:
array([0.93267709, 0.11042978, 0.36010128, 0.44023002])

Despite the fact the spectrum of $S_0$ is on the real line, $S_0$ is not selfadjoint in the $L^2$ inner product. To check that $$ (S_0 u, v) \ne (u, S_0 v) $$ we need the mass matrix $M$ which gives the $L^2$ inner product by $ M [u] \cdot [v] = (u, v)$. Here $[u]$ denotes the vector of coefficients of the function $u$ in the finite element basis expansion.

In [24]:
_, _, _, _, A, M, Ufree = P0.matrices_scipy()
In [25]:
from scipy.sparse import diags

# M is a symmetric matrix (and only one half of it is stored),
# so to recover the full matrix we do this:

MM = M.T + M - diags(M.diagonal())

MM = MM.todense()
MM = MM[:, Ufree]
MM = MM[Ufree, :]
MM = np.array(MM)
In [26]:
D = MM @ S0 - S0.T @ MM
D.real
Out[26]:
array([[ 0.        , -0.00058146,  0.00011262, -0.0004681 ],
       [ 0.00058146,  0.        ,  0.00049318,  0.00014341],
       [-0.00011262, -0.00049318,  0.        , -0.00057003],
       [ 0.0004681 , -0.00014341,  0.00057003,  0.        ]])

This shows that as matrices, $MS_0 \ne S_0^* M,$ and therefore $(S_0 u, v) \ne (u, S_0 v)$.

In [27]:
AA = A.T + A - diags(A.diagonal())
AA = AA.todense()
AA = AA[:, Ufree]
AA = AA[Ufree, :]
AA = np.array(AA)

DA = AA @ S0 - S0.T @ AA
DA.real
Out[27]:
array([[ 0.        , -0.0274668 ,  0.00555951, -0.02188661],
       [ 0.0274668 ,  0.        ,  0.02251279,  0.00719587],
       [-0.00555951, -0.02251279,  0.        , -0.02703451],
       [ 0.02188661, -0.00719587,  0.02703451,  0.        ]])

This shows that $S_0$ is also not selfadjoint in the $H_0^1$-innerproduct (which is generated by $A$ above).

To show how this DPG situation is in contrast to the standard finite element case, we examine the FEM spectral projector case on the same coarse mesh:

In [28]:
from pyeigfeast import SpectralProjNG

X = ngs.H1(mesh0, order=1, dirichlet='allbdry', complex=True)
a = ngs.BilinearForm(X)
a += ngs.Laplace(1)
b = ngs.BilinearForm(X)
b += ngs.Mass(1)
a.Assemble()
b.Assemble()
free0 = X.FreeDofs()
free0 = np.where(free0)[0]

P1 = SpectralProjNG(X, a.mat, b.mat, r, c, n, reduce_sym=True)
SpectralProj: Setting shifted trapezoidal rule quadrature on circular contour
SpectralProj: Radius=40, Center=19
SpectralProjNG: Computing resolvents along the contour:
SpectralProjNG:   Factorizing at z = +57.042+12.361j
SpectralProjNG:   Factorizing at z = +42.511+32.361j
SpectralProjNG:   Factorizing at z = +19.000+40.000j
SpectralProjNG:   Factorizing at z =  -4.511+32.361j
SpectralProjNG:   Factorizing at z = -19.042+12.361j
In [29]:
S1 = P1.matrix_filter()
S1.real
Out[29]:
array([[0.33857689, 0.23556034, 0.31962964, 0.23015963],
       [0.2506526 , 0.20080354, 0.24028303, 0.1454341 ],
       [0.31689388, 0.22191843, 0.33298344, 0.23793648],
       [0.23940729, 0.14005725, 0.2473967 , 0.20581076]])
In [30]:
D1 = MM @ S1 - S1.T @ MM
np.linalg.norm(D1)
Out[30]:
1.4929792947803568e-17
In [31]:
D1A = AA @ S1 - S1.T @ AA
np.linalg.norm(D1A)
Out[31]:
1.1180304937855718e-15

Thus, the standard finite element spectral projector (based on a symmetric quadrature point configuration in the complex plane) is selfadjoint both in $L^2$ and $H_0^1$ inner products.