Navier-Stokes, stability, Jacobian and eigenvalues

Hi !

I’ve solved most of my issues from my previous post but now I’m stuck with weird eigenvectors. Let me explain what I’m trying to achieve.

I’m interested in looking at the stability of my system for a given Reynolds number. In other words, let \dfrac{\partial w}{\partial t} = \mathcal{F}(w) the representation of our Navier-Stokes equations.

We can decompose the state vector into a steady base state and an unsteady perturbation such that w(x,t)=\bar{w}(x)+\epsilon w'(x,t). Inserting this into the previous equation

\qquad \qquad \qquad \qquad \qquad \dfrac{\partial \bar{w}}{\partial t} + \dfrac{\partial w'}{\partial t} = \mathcal{F}(\bar{w}+w')

The base state is steady so the time derivative can be dropped. In our case, \mathcal{F} is a non-linear operator so we need to linearize it around the base state.

\qquad \qquad \qquad \qquad \qquad \mathcal{F}(\bar{w} + w')=\mathcal{F}(\bar{w})+\left. \dfrac{\partial \mathcal{F}(w)}{\partial w} \right\rvert_{\bar{w}}w' + \mathcal{O}(w'^2)

With \mathcal{F}(\bar{w}) the solution of the steady Navier-Stokes equations (i.e. \mathcal{F}(\bar{w})=0). By considering the previous equation, neglecting the higher order terms and writing \mathcal{J}(\bar{w})= \left. \dfrac{\partial \mathcal{F}(w)}{\partial w} \right\rvert_{\bar{w}} we obtain

\qquad \qquad \qquad \qquad \qquad \dfrac{\partial w'}{\partial t} = \mathcal{J}(\bar{w})w'

Where \mathcal{J}(\bar{w}) is the Jacobian matrix. The perturbations are solved by characterising them as normal modes w'=\hat{w}e^{\sigma t}. Putting this back in the last equation

\qquad \qquad \qquad \qquad \qquad \sigma \hat{w}= \mathcal{J}(\bar{w})\hat{w}

With \sigma and \hat{w} respectively the eigenvalues and eigenvectors of the system. For recall, if one eigenvalue has a positive real part (>0) then the system is absolutely unstable.

Now the implementation (MWE in the end), I’m considering as well known stable test case (i.e. all eigenvalues should be negative until Re = 47)

Computation of the baseflow OK and verified, assembly of the Jacobian UNSURE, solving the eigenproblem OK but weird eigenvalues/vectors when plotting.

For the Jacobian assembly I have two options:

  1. Replace my BC with homogeneous Dirichlet in order to have a well defined weak form, problem is I have different positive eigenvalues and weird eigenvectors
  2. Ignore BC, no high eigenvalues comming from the BC but still positive (should be all negative) and still weird eigenvectors

Most of the people studying Navier-Stokes stability are consedering linearized Navier-Stokes equations in order to assemble an approximation of the Jacobian. I would like to take advantage of FEniCS and the derivative function.

So far, I know that the baseflow calculation is working properly. I have some doubt on the derivative function and the way that I’m assembling the matrix since the eigenvectors are really weird (almost zero everywhere).

Meshfile needed to run the code:
https://drive.google.com/file/d/1gQWqMFP3Mk_5I9Ym6P20UrI3gvEW1m6l/view?usp=sharing

MWE:

from dolfin import *
import matplotlib.pyplot as plt

comm = MPI.comm_world
solfile = XDMFFile(comm, "BF_RealEigenvector.pvd")
solfile.parameters["rewrite_function_mesh"] = False
solfile.parameters["functions_share_mesh"] = True
parameters["form_compiler"]["optimize"] = True
parameters["form_compiler"]["cpp_optimize"] = True
parameters["std_out_all_processes"] = False

#Apply or not BC to the jacobian matrix
WithBC = False

nev = 50
Re = 40
nu = 1 / Re
mesh_file = "cylinder_coarse.h5"

#Read the mesh and the facet, 1 = left+top+bot, 2 = cylinder
mesh = Mesh()
try:
    with HDF5File(comm, mesh_file, "r") as h5file:
        h5file.read(mesh, "mesh", False)
        facet = MeshFunction("size_t", mesh, mesh.topology().dim() - 1)
        h5file.read(facet, "facet")
except FileNotFoundError as fnf_error:
    print(fnf_error)
    
#Taylor-Hood element [P2, P2]-P1
V = VectorElement("CG", mesh.ufl_cell(), 2)
Q = FiniteElement("CG", mesh.ufl_cell(), 1)
W = FunctionSpace(mesh, V * Q)

#All the test/trial functions needed to solve the problem
v, q = TestFunctions(W)
w = Function(W)
u, p = split(w)
u_export, p_export = w.split()
u_export.rename("Velocity", "0")
p_export.rename("Pressure", "1")

#BC
inflow =  Constant((1.0, 0.0))
noslip = Constant((0.0, 0.0))

inflowBC = DirichletBC(W.sub(0), inflow, facet, 1)
noslipBC = DirichletBC(W.sub(0), noslip, facet, 2)
bcs = [inflowBC, noslipBC]

#Weak form of Steady Navier-Stokes
F = (Constant(nu) * inner(grad(u), grad(v))\
     + dot(dot(grad(u), u), v)\
     - p * div(v)\
     - q * div(u)) * dx

#Creating the solver object
J = derivative(F, w)
problem = NonlinearVariationalProblem(F, w, bcs, J)
solver = NonlinearVariationalSolver(problem)

#Solving the problem
with Timer() as t:
    begin("BaseFlow computation at Re = %d" %Re)
    solver.solve()
    end()
    info("Computation duration = %f s" %t.elapsed()[0])

solfile.write(u_export, 0)
solfile.write(p_export, 0)

#So far no problem, the baseflow looks fine
#Now studying the stability of the system around the baseflow

if WithBC:
#New BC, homogeneous Dirichlet everywhere
    inflowBC = DirichletBC(W.sub(0), noslip, facet, 1)
    noslipBC = DirichletBC(W.sub(0), noslip, facet, 2)
    bcs = [inflowBC, noslipBC]
else:
    bcs = []

A = PETScMatrix()

#Assembling the jacobian of the weak form evaluated at
#the baseflow previously calculated and stored in w
J = derivative(F, w)
dummy = inner(Constant((1.0, 1.0)), v) * dx
assemble_system(J, dummy, bcs, A_tensor = A)

#Solving the standard eigenvalue problem, looking for the
#largest real part
eigensolver = SLEPcEigenSolver(A)
PETScOptions.set("eps_view")
eigensolver.parameters["spectrum"] = "largest real"

eigensolver.solve(nev)

#Exporting the real part of the eigenvectors and plotting
#eigenvalues
Real = []
Complex = []
for i in range(nev):
    r, c, rv, cv = eigensolver.get_eigenpair(i)
    w.vector()[:] = rv

    solfile.write(u_export, i + 1)
    solfile.write(p_export, i + 1)

    Real.append(r)
    Complex.append(c)

solfile.close()

plt.plot(Real, Complex, "ro")
plt.xlabel("Real part")
plt.ylabel("Complex part")
plt.show()

In case anyone has the same problem, I had a similar issue and solved it on my end.

On the subject of boundary conditions, my understanding of the problem is that under the hood, when a DirichletBC is specified at assembly, dolfin cancels the rows and columns associated with the relevant degrees of freedom, then set its diagonal terms to 1. In other words, given a boundary \Gamma it modifies A so that Ax=b also imposes x_\Gamma=b_\Gamma

Another very important physical thing about the code above is the lack of a mass matrix. Physically, you have the Navier-Stokes equations as :

\begin{cases}\partial_tu+(u\cdot \nabla)u=-\nabla p+\frac{1}{Re}\Delta u\\ \nabla\cdot u=0\end{cases}\Leftrightarrow\left[\begin{array}{cc}I&0\\0&0\end{array}\right]\partial_t\left[\begin{array}{c}u\\p\end{array}\right]=\mathcal{F}(w)\Leftrightarrow\mathcal{N}\partial_tw=\mathcal{F}(w)

Indeed one can infer from your weak form that your equations are incompressible. This means your eigenvalue problem really writes as a general eigenvalue problem :

\sigma\mathcal{N}\hat{w}=\mathcal{J}(\bar{w})\hat{w}

This \mathcal{N} matrix is fairly trivial to write in fenics, with the specificity that if you want your boundary conditions to be specified. You’re studying perturbations so your boundary conditions are homogeneous and you want \mathcal{N} to cancel on your boundary \Gamma.

I’m coding in dolfinx now, but I think in dolfin the most elegant way to fix your code would be to replace the central part by :

A = PETScMatrix()
B = PETScMatrix()
#Assembling the jacobian of the weak form evaluated at the baseflow previously calculated and stored in w
J = derivative(F, w)
really_meaningful = inner(u,v)*dx
assemble_system(J, really_meaningful, bcs, A_tensor = A, B_tensor=B)
#Solving the standard eigenvalue problem, looking for the largest real part
eigensolver = SLEPcEigenSolver(-A,B)

Hope that is of some use to someone someday.

1 Like

Hi there,
I’ve found that this code is relative to my work; however, I’m just after some clarification. In the above is B in assemble_system equivalent to the N in the above equation. I also found that in order to get assemble_system to work correctly, B has to be initialised via B = PETScVector(). This adds further complexity to SLEPcEigenSolver which take two PETScMatrix arguments as inputs. Therefore, I’ve gone through the process of converting B to a PETScMatrix, and after this manipulation the eigensolver seems not to be computing any eigenvalues. Any ideas why this may be??
Thanks,
Ben

Yes \mathcal{N} is equivalent to B and \frac{\partial \mathcal{F}}{\partial w} or J is equivalent to A. This is me translating one notation set in the fluid mechanics world into another notation set in the linear algebra world.

I advise that you look again at the above code I provided : B is a tensor that is initialised as a PETScMatrix. Think about it ; having a mass matrix (in the sense Ax=\lambda Bx) as a vector makes no sense.

I think your problem solely comes from this mistype error, but I would need your code for more detail. I believe my code snippet works fine.

Hi there,

Thanks for your reply. My research focuses on the vortex shedding behind hexagonal cylinders and I’ll try and explain briefly what I am trying to do. I have run a simulation previously to get a mean velocity and pressure fields over many shedding cycles externally using MATLAB. I then import these mean fields as functions for the ‘baseflow’, which look fine when I plot them in Paraview. From here, I am just aiming to study the stability of the system relative to this baseflow like the original post above. I’ve attached a link to the mesh/mean field data I have been using and my code is below.

Thanks for your help, I’m relatively new to FEniCS and of course understand that you may not have time to assist me at this moment.

Ben

from dolfin import *
import numpy as np
from scipy import sparse
from petsc4py import PETSc
import matplotlib.pyplot as plt

mesh = Mesh()

with XDMFFile("hex_mesh_bigdom.xdmf") as infile:
    infile.read(mesh)
mvc = MeshValueCollection("size_t", mesh, 1)

with XDMFFile("facet_mesh_bigdom.xdmf") as infile:
    infile.read(mvc, "name_to_read")
mf = cpp.mesh.MeshFunctionSizet(mesh, mvc)      # allows boundaries to be access

ds = Measure('ds', domain = mesh, subdomain_data = mf)

V = VectorFunctionSpace(mesh, 'CG', 2)
Q1 = FunctionSpace(mesh, 'CG', 1)
Q2 = FunctionSpace(mesh, 'CG', 2)

u0 = Function(V)
p0 = Function(Q1)

# IMPORT MEAN FLOW FROM FILES

umean_in = XDMFFile("hex_velocity_MEAN.xdmf")
umean_in.read_checkpoint(u0, "u_mean", 0)

pmean_in = XDMFFile("hex_pressure_MEAN.xdmf")
pmean_in.read_checkpoint(p0, "p_mean", 0)

## USE PROJECT TO GET PRESSURE/VELOCITY INTO CG 2 SPACE

p02 = project(p0, Q2)
u02 = project(u0, V)

##  CREATE MIXED ELEMENT SPACE

VM = VectorElement("CG", mesh.ufl_cell(), 2)
QM = FiniteElement("CG", mesh.ufl_cell(), 2)
W = FunctionSpace(mesh, MixedElement([VM,QM]))
funW = Function(W)

## Assign loaded functions to Mixed Space W.

assign(funW.sub(0), u02)
assign(funW.sub(1), p02)

#All the test/trial functions needed to solve the problem

v, q = TestFunctions(W)
u, p = split(funW)

nu = 8/1000

##   Define variational problem

F = (Constant(nu) * inner(grad(u), grad(v))\
     + dot(dot(grad(u), u), v)\
     - p * div(v)\
     - q * div(u)) * dx

## DIRICHLET BOUNDARY CONDITIONS

bcu_inflow = DirichletBC(W.sub(0), Constant((1.0, 0.0)), mf, 0)    # mf 0 = inflow boundary
bcu_top = DirichletBC(W.sub(0), Constant((1.0, 0.0)), mf, 1)       # mf 1 = top boundary
bcu_bottom = DirichletBC(W.sub(0), Constant((1.0, 0.0)), mf, 3)    # mf 3 = bottom boundary
bcu_hex = DirichletBC(W.sub(0), Constant((0.0, 0.0)), mf, 4)       # mf 4 = hexagon walls

bcp_outflow = DirichletBC(W.sub(1), Constant(0), mf, 2)      # mf 2 = outflow boundary

bcs = [bcu_inflow, bcu_top, bcu_bottom, bcu_hex, bcp_outflow]

## Form PETSc matrices to form the Jacobian

A = PETScMatrix()
B = PETScMatrix()

## Define the Jacobian and assemble matrix

J = derivative(F, funW)
really_meaningful = inner(u,v)*dx
assembledJ = assemble_system(J, really_meaningful, bcs, A_tensor = A, b_tensor = B)

eigensolver = SLEPcEigenSolver(-A, B)

https://drive.google.com/drive/folders/1nZhdFirxP0tbr2iqpbhNOwePJ9Jk9blZ?usp=sharing

What’s your problem ?

When I attempt to run the program I get the following error message:

Traceback (most recent call last):
  File "/home/homeblue01/1/sdrb41/level_4/fenics/extraction/extract_J.py", line 101, in <module>
    assembledJ = assemble_system(J, really_meaningful, bcs, A_tensor = A, b_tensor = B)
  File "/usr/lib/petsc/lib/python3/dist-packages/dolfin/fem/assembling.py", line 430, in assemble_system
    assembler.assemble(A_tensor, b_tensor)
TypeError: assemble(): incompatible function arguments. The following argument types are supported:
    1. (self: dolfin.cpp.fem.SystemAssembler, arg0: dolfin::GenericMatrix, arg1: dolfin::GenericVector) -> None
    2. (self: dolfin.cpp.fem.SystemAssembler, arg0: List[dolfin::GenericMatrix], arg1: List[dolfin::GenericVector]) -> None
    3. (self: dolfin.cpp.fem.SystemAssembler, arg0: dolfin::GenericMatrix) -> None
    4. (self: dolfin.cpp.fem.SystemAssembler, arg0: dolfin::GenericVector) -> None
    5. (self: dolfin.cpp.fem.SystemAssembler, arg0: dolfin::GenericMatrix, arg1: dolfin::GenericVector, arg2: dolfin::GenericVector) -> None
    6. (self: dolfin.cpp.fem.SystemAssembler, arg0: dolfin::GenericVector, arg1: dolfin::GenericVector) -> None

Invoked with: <dolfin.cpp.fem.SystemAssembler object at 0x7f40f86efd70>, <dolfin.cpp.la.PETScMatrix object at 0x7f40f8f98590>, <dolfin.cpp.la.PETScMatrix object at 0x7f40f8f98770>

Ok I get why you would want to have B as a vector now. I’m afraid I hadn’t tested the exact synthax I proposed.

I use dolfinx not dolfin to leverage complex matrices, my code is too long to post here but it works. I’ll give you a few (at most directly helpful, at worst inspiring) snippets. The first gets associated DoFs for a given BC :

# Compute DoFs
dofs = dolfinx.fem.locate_dofs_geometrical((sub_space, sub_space_collapsed), boundary)

The second actually assembles the A (or \mathcal{J}) matrix. It hinges on the key observation that all your boundary conditions for the perturbed flow are Dirichlet zeros, which is easy to implement in a PETSc Matrix :

J = dolfinx.fem.assemble_matrix(dform)
J.assemble()
J.zeroRowsColumnsLocal(dofs) # Impose homogeneous BCs (actually keeps a 1 on the diagonal)

The same holds for the B (or \mathcal{N}) matrix. The last one actually computes the eigenvalues :

# Solver
E = slepc4py.EPS(MPI.COMM_WORLD); E.create()
E.setOperators(-J,N) # Solve Ax=sigma*Mx
E.setWhichEigenpairs(E.Which.TARGET_MAGNITUDE) # Find eigenvalues close to sigma
E.setProblemType(slp.EPS.ProblemType.PGNHEP) # Specify that A is no hermitian, but M is semi-positive
# Spectral transform
ST  = E.getST();    ST.setType('sinvert')
# Krylov subspace
KSP = ST.getKSP(); KSP.setType('preonly')
# Preconditioner
PC  = KSP.getPC();  PC.setType('lu')
PC.setFactorSolverType('mumps')
E.setFromOptions()
E.solve()
n=E.getConverged()
for i in range(n):
   print(E.getEigenvalue(i))
   q=dfx.Function(self.Space)
   E.getEigenvector(i,q.vector)

If this feels like reinventing the wheel, it is because that’s what it is. I’m sure there’s stuff already implemented in dolfin, but my version works.

Thanks for your help, I think I’ll extract the matrices to MATLAB and try to solve the eigenvalue problem from there. However, I’m still unsure how I can assemble N in a simple way. Is there an easy way to do so?

Well, the form associated to B is pretty trivial. Something like : b_form=v*u*dx (using your code notations) should do the trick.

When I run the following:

really_meaningful = inner(u,v)*dx
B = assemble(really_meaningful)

I still get the vector:

<dolfin.cpp.la.Vector object at 0x7f9db350d220>

I think there’s something wrong in your code. Basically it’s wrong to mix baseflow and trial functions. I think you should rewrite F with real trial functions, differentiate it around funW (which is a constant really), and retry the assemble_system procedure.

You could be getting a vector for B because as far as your code is concerned, funW is constant, so u and p are constants too. I’d say you want to have a trial and a test function in your variational form F

Hi Quentin,
Thanks for your help. I worked on the problem some more last night and got a matrix output for N when I changed the input in ‘really_meaningful’ to a real trial function using the assemble() function. However, if I use this real trial function in the weak form of F, then FEniCS throws an error in assemble(J):

    jac = assemble(J)
  File "/usr/lib/petsc/lib/python3/dist-packages/dolfin/fem/assembling.py", line 206, in assemble
    tensor = _create_tensor(comm, form, dolfin_form.rank(), backend, tensor)
  File "/usr/lib/petsc/lib/python3/dist-packages/dolfin/fem/assembling.py", line 474, in _create_tensor
    raise RuntimeError("Unable to create tensors of rank %d." % rank)
RuntimeError: Unable to create tensors of rank 3.

So I just kept the baseflow functions in the definition of F. However, when I try and run the eigensolver now I get the following issue when running ‘get_eigenpair(1)’:

*** Error:   Unable to extract eigenpair from SLEPc eigenvalue solver.
*** Reason:  Requested eigenpair (1) has not been computed.
*** Where:   This error was encountered inside SLEPcEigenSolver.cpp.
*** Process: 0

Any help is obviously appreciated. However, I am very grateful for your help so far and understand that you may have other things on your plate at the minute!

Thanks,
Ben

Could you share a bit more of your code ? There could be multiple issues here…

I also tried with the full F and using automated derivative at first, it was clearly not worth trying to debug it. You do need to specify a direction for the derivative, like J=derivative(F, funW, [trial function)] if you want it to work properly in assemble_system (see https://fenicsproject.org/olddocs/dolfin/2017.2.0/python/programmers-reference/fem/formmanipulations/derivative.html).

Or, we could leverage the fact that writing J explicitly is fairly trivial (NS is almost linear after all) :

u,	p=split(TrialFunction(W))
u_b,_=ufl.split(funW) # Baseflow
v,	w=split(TestFunction(W))

# Incompressibility
J  = inner(div(u), 	   w)
# Momentum (different test functions and IBP)
J += inner(grad(u_b)*u, v)    	# Convection
J += inner(grad(u)*u_b, v)
J += inner(grad(u), grad(v))*Constant(nu) # Diffusion
J -= inner(p,div(v)) 	# Pressure

I think that this should assemble fine.

Hi Quenting,

I’ve attached the code I’m working with. It’s essentially trying 3 different ways of getting the jacobian:

  1. Using assemble_system() to get the Jacobian from the derivative (works)
  2. Using assemble() to get the Jacobian from the derivative (works)
  3. Using assemble() on the explicit Jacobian (throws error)

Here is my code:

from dolfin import *
import numpy as np
from scipy import sparse
from scipy.io import savemat
from petsc4py import PETSc
import matplotlib.pyplot as plt

mesh = Mesh()

with XDMFFile("hex_mesh_bigdom.xdmf") as infile:
    infile.read(mesh)
mvc = MeshValueCollection("size_t", mesh, 1)

with XDMFFile("facet_mesh_bigdom.xdmf") as infile:
    infile.read(mvc, "name_to_read")
mf = cpp.mesh.MeshFunctionSizet(mesh, mvc)      # allows boundaries to be access

ds = Measure('ds', domain = mesh, subdomain_data = mf)

V = VectorFunctionSpace(mesh, 'CG', 2)
Q1 = FunctionSpace(mesh, 'CG', 1)
Q2 = FunctionSpace(mesh, 'CG', 2)

u0 = Function(V)
p0 = Function(Q1)

## IMPORT MEAN FLOW FROM FILES

umean_in = XDMFFile("hex_velocity_MEAN.xdmf")
umean_in.read_checkpoint(u0, "u_mean", 0)

pmean_in = XDMFFile("hex_pressure_MEAN.xdmf")
pmean_in.read_checkpoint(p0, "p_mean", 0)

## USE PROJECT TO GET PRESSURE/VELOCITY INTO CG 2 SPACE
p02 = project(p0, Q2)
u02 = project(u0, V)

##  CREATE MIXED ELEMENT SPACE
VM = VectorElement("CG", mesh.ufl_cell(), 2)
QM = FiniteElement("CG", mesh.ufl_cell(), 2)
W = FunctionSpace(mesh, MixedElement([VM,QM]))
funW = Function(W)

## Assign loaded functions to Mixed Space W.
assign(funW.sub(0), u02)
assign(funW.sub(1), p02)

nu = 8/1000

#All the test/trial functions needed to solve the problem

u, p = split(TrialFunction(W))
u_b, p_b = split(funW)          #baseflow
v, w = split(TestFunction(W))

## WRITE JACOBIAN EXPLICITLY
J1 = inner(div(u), w)
J1 += inner(grad(u_b)*u, v)
J1 += inner(grad(u)*u_b, v)
J1 += inner(grad(u), grad(v))*Constant(nu)
J1 -= inner(p, div(v))

##Define variational problem

F = (Constant(nu) * inner(grad(u_b), grad(v))\
     + dot(dot(grad(u_b), u_b), v)\
     - p_b * div(v)\
     - w * div(u_b)) * dx

## DEFINE BOUNDARY CONDITIONS
bcu_inflow = DirichletBC(W.sub(0), Constant((1.0, 0.0)), mf, 0)    # mf 0 = inflow boundary
bcu_top = DirichletBC(W.sub(0), Constant((1.0, 0.0)), mf, 1)       # mf 1 = top boundary
bcu_bottom = DirichletBC(W.sub(0), Constant((1.0, 0.0)), mf, 3)    # mf 3 = bottom boundary
bcu_hex = DirichletBC(W.sub(0), Constant((0.0, 0.0)), mf, 4)       # mf 4 = hexagon walls
bcp_outflow = DirichletBC(W.sub(1), Constant(0), mf, 2)     # mf 2 = outflow

bcs = [bcu_inflow, bcu_top, bcu_bottom, bcu_hex, bcp_outflow]

## DEFINE J
J = derivative(F, funW, TrialFunction(W))

##  CREATE PETSc MATRICES FOR ASSEMBLE SYSTEM
A = PETScMatrix()
B = PETScVector()

## USE ASSEMBLE_SYSTEM() TO GET JACOBIAN
really_meaningful = inner(u_b,v)*dx
assembledJ = assemble_system(J, really_meaningful, bcs, A_tensor = A, b_tensor = B)

## USE ASSEMBLE() TO GET JACOBIAN

jac = assemble(J)

for bc in bcs:
    bc.apply(jac)

matjac = as_backend_type(jac).mat()     #convert Jacobian la.Matrix to PETSc
PETScjac = PETScMatrix(matjac)          #get PETScMatrix of Jacobian

## GET SPARSE JACOBIAN
matrixjac = PETScjac.mat()
convjac = matjac.convert("dense")
densejac = convjac.getDenseArray()
spsjac = sparse.csr_matrix(densejac)        

## TRY AND ASSEMBLE EXPLICIT JACOBIAN J1
jac1 = assemble(J1)

## GET N VIA ASSEMBLE() AND NOT ASSEMBLE_SYSTEM()
## GIVES MATRIX OF N NOT VECTOR
N = assemble(inner(u,v)*dx)

for bc in bcs:
    bc.apply(N)

matN = as_backend_type(N).mat()     #converting to PETSc and then sparse
PETScN = PETScMatrix(matN)
matrixN = PETScN.mat()
convN = matN.convert("dense")
denseN = convN.getDenseArray()
spsN = sparse.csr_matrix(denseN)

## DEFINE EIGENSOLVER
minusjac = -1*(PETScjac)

eigensolver = SLEPcEigenSolver(minusjac, PETScN)

The error being produced:

    jac1 = assemble(J1)
  File "/usr/lib/petsc/lib/python3/dist-packages/dolfin/fem/assembling.py", line 202, in assemble
    dolfin_form = _create_dolfin_form(form, form_compiler_parameters)
  File "/usr/lib/petsc/lib/python3/dist-packages/dolfin/fem/assembling.py", line 64, in _create_dolfin_form
    raise TypeError("Invalid form type %s" % (type(form),))
TypeError: Invalid form type <class 'ufl.algebra.Sum'>

My fault ! I had forgotten the *dx. Notice that the inner everywhere even for the scalars are a constraint from the complex world. Since you seem to be handling reals you can do without them as in your original code (you still need them for vector scalar products of course). Rolling back :

u,	p=split(TrialFunction(W))
u_b,_=ufl.split(funW) # Baseflow
v,	w=split(TestFunction(W))

# Incompressibility
J  = div(u)*w
# Momentum (different test functions and IBP)
J += inner(grad(u_b)*u, v)    	# Convection
J += inner(grad(u)*u_b, v)
J += inner(grad(u), grad(v))*Constant(nu) # Diffusion
J -= p*div(v) 	# Pressure
J=J*dx # Actually really important

What about the rest of the code ? Does the eigensolver works now ?

This fixed the issue so I can assemble this explicit J1, but I can’t apply the boundary conditions via apply(), which against doesn’t like the input type of the assembled matrix. Also, the eigensolver still isn’t working. I’ve put my sparse ‘N’ matrix into MATLAB and found that it is singular (det(N) = 0) which I think is causing some issues.

Of course the B matrix (or \mathcal{N}) operator is singular. It’s a mass matrix - it basically takes the whole (u\;p)^T vector as arguments and return u. Therefore, it produces 0 for any non zero input of the shape (0\;p)^T. Conceptually, it’s filled with 1 and 0.

But that should not be a problem with a generalised eigenvalue problem. In the formalism Ax=\lambda Bx, both A and B can be singular.

I’d advise going back to my snippets, assembling both matrices, applying boundary conditions manually, and enforcing a general eigenvalue problem in petsc4py. It’s a pain but it gives you total control (and understanding of important below the hood mechanics).

Yes sorry that’s my bad, it’s been a long day! I had an inkling that was the case. Just so you know I’ve produced the B matrix (or N) by using np.eye(dims) and then setting the entries relating to a pressure degree of freedom to 0 in a ‘for’ loop. I’ve also used a ‘for’ loop to set the boundary conditions (diagonal = 1) in a np.array representation of assemble(J) since I was getting a error with PETSc.zeroRowColumnsLocal().