Is it Possible to insert a vector into an assembled matrix?

Hello,

I want to ask that is it possible to modify our PETSc matrix or vector.

Let’s say I have assembled the matrix A from the bilinear form and the vector b from the linear form, now I have a m by m matrix (mass matrix) and m by 1 vector, and I want to get some like this

new_A = |A     b|      and   new_b = |b|                             
        |b^T   0|                    |0|

Does any one know that is it even possible after the matrix is already assembled? Or we need to use some intermediate matrix.

Any suggestions or comments are appreciated, thanks!

I would suggest using petsc-nest for this, see:
https://docs.fenicsproject.org/dolfinx/v0.7.3/python/demos/demo_stokes.html?highlight=nest#nested-matrix-solver

As @dokken mentioned, what you want are nested matrices and vectors. The specific nested matrix for new_A does have the subtlety that it is constructed from a PETSc vector. I think you will need to construct a PETSc matrix from that PETSc b vector. There may be an alternative way to do this and I’m not sure that this will in parallel, but this should be a good start:

from petsc4py import PETSc
from mpi4py import MPI

# A = some_assembled_PETSc_matrix
# b = some_assembled_PETSc_vector

#create new_A block matrices
b_mat = PETSc.Mat().create(comm=MPI.COMM_WORLD)
b_mat.setUp()
b_mat.setValues(range(b.getSize()),0,b.getValues(range(b.getSize())))
b0_mat = PETSc.Mat().create(comm=MPI.COMM_WORLD)
b_mat.setSizes((1,1))
b0_mat.setUp()
bT_mat = b_mat.transpose()

#create nestednew_A PETSc Matrix
new_A = PETSc.Mat()
new_A.createNest([[A,     b_mat],
                  [bT_mat,b0_mat]])
new_A.setUp()
new_A.assemble()

#create nested new_b matrix
new_b = PETSc.Vec()

b0 = PETSc.Vec()
b0.setSizes(1)
b0.setUp()
new_b = PETSc.Vec()
new_b.createNest([b,b0])
new_b.setUp()

Then solve using whatever PETSc solve you find appropriate. There are lots of examples of this on the forums, but a simple way is:

new_x = PETSc.Vec()
new_x.createNest([x0, x1]) #x0 and x1 are petsc vectors for your solution
new_x.setUp()

ksp = PETSc.KSP().create()
ksp.setType(PETSc.KSP.Type.CG)
ksp.setTolerances(rtol=1e-18)
ksp.setOperators(new_A)
ksp.setFromOptions()
ksp.solve(new_b,new_x)

I typically struggle finding petsc4py examples through the petsc4py documentation, so I hope this helps.

Very thanks for your comment, I think I was able to make the nested matrix and vector, say I had a 5 by 5 matrix A and 5 by 1 vector b

now I have [A b;b^T 0]

Mat Object: 1 MPI process
  type: nest
  Matrix object: 
    type=nest, rows=2, cols=2 
    MatNest structure: 
    (0,0) : type=seqaij, rows=5, cols=5 
    (0,1) : type=seqaij, rows=5, cols=1 
    (1,0) : type=seqaij, rows=1, cols=5 
    (1,1) : type=seqaij, rows=1, cols=1 

and [b 0]

Vec Object: 1 MPI process
  type: nest
  VecNest, rows=2,  structure: 
  (0) : type=mpi, rows=5 
    Vec Object: 1 MPI process
      type: mpi
    Process [0]
    -0.381975
    0.466858
    0.466858
    0.466858
    0.
  (1) : type=seq, rows=1 
    Vec Object: 1 MPI process
      type: seq
    0.

but can we use the above operators for linear systems? When I tried to solve it, I got the following error:

Could not locate a solver type for factorization type LU and matrix type nest.

Is there any special thing that I need to deal with when I use nested operator?

Thanks!

For LU you need to use mumps, see: Summary of Sparse Linear Solvers Available In PETSc — PETSc 3.21.0 documentation

Thanks very much, now I’m able to solve it!

I would appreciate if you could post your full solution here, as it might help others in the future with similar inqueries.

Sure.

BTW, I was actually working with Lagrange multiplyer for Poisson equation. Since dolfinx don’t have the real number space, this has been a problem for me.

Now I find a way to handle it:
We can just creat the linear form,

Lt = v * dx

where v is the test function. Then if you assemble the form Lt and get the vector b, you can just add it to the stiffness matrix A given by Poisson equation, [A b; b^T 0], this matrix is equivalent to the system given by:
Screenshot from 2024-04-05 15-17-21

since the basis function for real number space is just 1.

I’m solving a problem in the form:
[A b;b^T 0] x = [r; 0] where A is a m by m matrix, b and r are m by 1 vectors.

I attach my cpp codes here for those who have similar questions.

// create the vector you want to add to the matrix and its transpose
// Since we nest the vectors into a matrix, we define them in the matrix form
Mat bt;
Mat bt_transpose;


PetscInt b_size = 10;   // you can change the size
MatCreate(PETSC_COMM_WORLD, &bt);
MatSetSizes(bt, PETSC_DECIDE, PETSC_DECIDE, b_size, 1);
MatSetFromOptions(bt);
MatSetUp(bt);

for (PetscInt i = 0; i < b_size; ++i)
{
    PetscScalar value;  // this is something you can assign
    MatSetValue(bt, i, 0, value, INSERT_VALUES); 
}
// assemble the bt and get its transpose
MatAssemblyBegin(bt, MAT_FINAL_ASSEMBLY);
MatAssemblyEnd(bt, MAT_FINAL_ASSEMBLY);
MatTranspose(bt, MAT_INITIAL_MATRIX, &bt_transpose);


// Create the 0 matrix which is in the right-bottom corner
Mat b0_mat;
MatCreate(PETSC_COMM_WORLD, &b0_mat);
MatSetSizes(b0_mat, PETSC_DECIDE, PETSC_DECIDE, 1, 1);
MatSetFromOptions(b0_mat);
MatSetValue(b0_mat, 0, 0, 0.0, INSERT_VALUES); 
MatAssemblyBegin(b0_mat, MAT_FINAL_ASSEMBLY);
MatAssemblyEnd(b0_mat, MAT_FINAL_ASSEMBLY);

// The structure of our nested matrix is 2 by 2
PetscInt nr = 2;
PetscInt nc = 2;
IS is_row[2];
IS is_col[2];
PetscInt n_row, n_col;


MatGetSize(A, &n_row, &n_col);
// Create index sets for row and column indices
ISCreateStride(PETSC_COMM_WORLD, n_row, 0, 1, &is_row[0]); // Example: row indices for A
ISCreateStride(PETSC_COMM_WORLD, n_col, 0, 1, &is_col[0]); // Example: column indices for A
ISCreateStride(PETSC_COMM_WORLD, 1, n_row, 1, &is_row[1]); 
ISCreateStride(PETSC_COMM_WORLD, 1, n_col, 1, &is_col[1]); 


// Create the nested matrix
Mat nested_A;
Mat matrices[4] = {A, bt, bt_transpose, b0_mat};
MatCreateNest(PETSC_COMM_WORLD, nr, is_row, nc, is_col, matrices, &nested_A);
MatNestSetVecType(nested_A,VECNEST);
MatSetUp(nested_A);
MatAssemblyBegin(nested_A, MAT_FINAL_ASSEMBLY);
MatAssemblyEnd(nested_A, MAT_FINAL_ASSEMBLY);
MatSetOption(nested_A, MAT_SPD , PETSC_TRUE);



// Now we handle the nested vector
// Create the 0 entity in the bottom of RHS
Vec r0_vec;
VecCreate(PETSC_COMM_WORLD, &r0_vec);
VecSetSizes(r0_vec, 1, 1);
VecSetFromOptions(r0_vec);
PetscInt idx[] = {0};
PetscScalar val[] = {0};   // The value can be different if you want
VecSetValues(r0_vec, 1, idx, val, INSERT_VALUES);
VecSetFromOptions(r0_vec);
VecAssemblyBegin(r0_vec);
VecAssemblyEnd(r0_vec);
VecSetUp(r0_vec);

Vec nested_r;
PetscInt nb = 2;
ISCreateStride(PETSC_COMM_WORLD, n_row, 0, 1, &is_row[0]); // Example: column indices for r
ISCreateStride(PETSC_COMM_WORLD, 1, n_row, 1, &is_row[1]); //
Vec vectors[2] = {r, r0_vec};
VecCreateNest(PETSC_COMM_WORLD, nb, is_row, vectors, &nested_r); VecSetFromOptions(nested_r);
VecAssemblyBegin(nested_r);
VecAssemblyEnd(nested_r);
VecSetUp(nested_r);
  
// Create the nested vector to store the solution  
Vec nested_solutions;
vectors[0] = u;  // Replace it with your solution vector
VecCreateNest(PETSC_COMM_WORLD, nb, is_row, vectors, &nested_solutions);
VecSetFromOptions(nested_solutions);
VecAssemblyBegin(nested_solutions);
VecAssemblyEnd(nested_solutions);
VecSetUp(nested_solutions);

auto lu = std::make_shared<KrylovSolver>(MPI_COMM_WORLD);

// You can play with the pc and ksp type, but don't use LU, 
la::petsc::options::set("ksp_type", "cg");
la::petsc::options::set("pc_type", "hypre");
la::petsc::options::set("rtol", 1e-18);
lu->set_operator(nested_A);
lu->set_from_options();
lu->solve(nested_solutions, nested_b);

The nested matrix type is unsupported by standard direct solvers (e.g. MUMPS, suplerlu, superlu_dist). However, you can assemble the block structure into a single matrix (see, e.g. Stokes demo), but you’ll have to take care with the sparsity pattern of the matrix.

This example is neat, and I imagine quite useful for many DOLFINx users. If you would have I time I’d encourage you to propose a tidier, smaller and neater version (perhaps in Python), exemplifying your implementation of a Lagrange multiplier. Should you wish to do this, you could initially propose this here to see if the other developers agree.

Yes, sure.
I will do that after I got some reliable solutions, maybe we don’t have to use nested operator, I will check.

I think the nested-operator is the best (and scalable way) of implementing Lagrange multipliers, especially when it comes to using iterative solvers.

Greetings! I’d like to thank you all for the question and answers, it gave me a good idea for a problem I’m working now. I tried to reproduce the method, specially with the jkrokowski code, but I’m currently struggling with an issue when solving the system with nested matrices with PETSc.

I was able to assemble the matrix and vectors, but when I try to solve the system with a CG type PETSc KSP solver it returns the following error:

Error: error code 60
[0] KSPSolve() at /tmp/petsc-src/src/ksp/ksp/interface/itfunc.c:1082
[0] KSPSolve_Private() at /tmp/petsc-src/src/ksp/ksp/interface/itfunc.c:910
[0] KSPSolve_CG() at /tmp/petsc-src/src/ksp/ksp/impls/cg/cg.c:248
[0] KSP_MatMult() at /tmp/petsc-src/include/petsc/private/kspimpl.h:347
[0] MatMult() at /tmp/petsc-src/src/mat/interface/matrix.c:2608
[0] MatMult_Nest() at /tmp/petsc-src/src/mat/impls/nest/matnest.c:51
[0] MatMultAdd() at /tmp/petsc-src/src/mat/interface/matrix.c:2770
[0] Nonconforming object sizes
[0] Mat mat,Vec v1: global dim 1056 16

It looks like is having some trouble with the nested structure when using the iterative methods, and I’m not seeing what exactly is missing. Below a sample code in Python I made which returns this error. I also let a Colab file with the code, in case it helps to confirm the versions in use Google Colab .

mesh = dolfinx.mesh.create_unit_square(MPI.COMM_WORLD, 20, 20, dolfinx.mesh.CellType.quadrilateral)

#setting boundaries markers and indicator functions
#Electrodes e_j are displace in the middle of each boundary
boundaries = [
    (0, lambda x: np.logical_and( np.isclose(x[0],0), np.logical_and(x[1]>=0.25,x[1]<=0.75))),
    (1, lambda x: np.logical_and( np.isclose(x[0],1), np.logical_and(x[1]>=0.25,x[1]<=0.75))),
    (2, lambda x: np.logical_and( np.isclose(x[1],0), np.logical_and(x[0]>=0.25,x[0]<=0.75))),
    (3, lambda x: np.logical_and( np.isclose(x[1],1), np.logical_and(x[0]>=0.25,x[0]<=0.75)))
]

#creating facet tags
facet_indices, facet_markers = [], []
fdim = mesh.topology.dim - 1
for (marker, locator) in boundaries:
    facets = dolfinx.mesh.locate_entities(mesh, fdim, locator)
    facet_indices.append(facets)
    facet_markers.append(np.full_like(facets, marker))
facet_indices = np.hstack(facet_indices).astype(np.int32)
facet_markers = np.hstack(facet_markers).astype(np.int32)
sorted_facets = np.argsort(facet_indices)
facet_tag = dolfinx.mesh.meshtags(mesh, fdim, facet_indices[sorted_facets], facet_markers[sorted_facets])

#Creating measure object
ds = ufl.Measure("ds", domain=mesh, subdomain_data=facet_tag)



V = dolfinx.fem.FunctionSpace(mesh, ("CG", 1)) #Lagrange space for u
V0 = dolfinx.fem.FunctionSpace(mesh, ("DG",0)) #Discontinuous Garlekin for gamma

N = V.dofmap.index_map.size_global; L = 4; z_value = 1

gamma = dolfinx.fem.Constant(mesh, PETSc.ScalarType(1))
z = dolfinx.fem.Constant(mesh, PETSc.ScalarType(z_value))
I = np.zeros(L)
I[0] = 1; I[1] = -1

u = ufl.TrialFunction(V) #function u_h
v = ufl.TestFunction(V)

#Assembling A1
A1_sum_array = [(1/z)*ufl.inner(u,v)*ds(i) for i in range(L)]
A1_form = (gamma * ufl.inner(ufl.grad(u), ufl.grad(v)) * ufl.dx + np.sum(A1_sum_array))
A1_form_obj = dolfinx.fem.form(A1_form)
A1_matrix = dolfinx.fem.petsc.assemble_matrix(A1_form_obj)

###

#Assembling A2
A2_form_array = [-ufl.inner((1/z), v) * ds(i) for i in range(L)]
A2_form_obj = [dolfinx.fem.form(form) for form in A2_form_array]
A2_vector_array= [dolfinx.fem.petsc.assemble_vector(form) for form in A2_form_obj]
A2_matrix = PETSc.Mat().create(comm=MPI.COMM_WORLD)
A2_matrix.setSizes([N,L])
for j in range(L):
  A2_matrix.setValues(range(N),j,A2_vector_array[j].getValues(range(N)))
A2_matrix.assemble()
A2T_matrix = A2_matrix.transpose()
A2T_matrix.assemble()

###

#Assembling A4
cell_area_list = []
v = ufl.TrialFunction(V0) #trial object
for i in range(L):
  s = v*ds(i) # creates object of the form \int_{e_i} 1*ds
  cell_area_form = dolfinx.fem.form(s)
  cell_area = dolfinx.fem.assemble_scalar(cell_area_form)
  cell_area_list.append(cell_area.real)
cell_area_array = np.array(cell_area_list)
A4_diagonal = np.diag(cell_area_array*(1/z_value))
A4_matrix = PETSc.Mat().create(comm=MPI.COMM_WORLD)
A4_matrix.setSizes(L)
A4_matrix.setValues(range(L),range(L),A4_diagonal.ravel())
A4_matrix.assemble()


# Assembling matrix A
A = PETSc.Mat()
A.createNest([[A1_matrix,     A2_matrix],
              [A2T_matrix, A4_matrix]])
A.setUp()
A.assemble()

#Assembling b vector

zero_vec = PETSc.Vec().create(comm=MPI.COMM_WORLD)
zero_vec.setSizes(N)
zero_vec.setUp()
I_vec = PETSc.Vec().create(comm=MPI.COMM_WORLD)
I_vec.setSizes(L)
I_vec.setUp()
I_vec.setArray(I)
I_vec.assemble()
b_nested = PETSc.Vec()
b_nested.createNest([zero_vec,I_vec])
b_nested.setUp()



# Solving problem
#Create solution vector
u_vec = PETSc.Vec().create(comm=MPI.COMM_WORLD)
u_vec.setSizes(N)
U_vec = PETSc.Vec().create(comm=MPI.COMM_WORLD)
U_vec.setSizes(L)
u_vec.setUp()
U_vec.setUp()
u_nested = PETSc.Vec().create(comm=MPI.COMM_WORLD)
u_nested.createNest([u_vec, U_vec]) #x0 and x1 are petsc vectors for your solution
u_nested.setUp()

#Solving
ksp = PETSc.KSP().create(mesh.comm)
ksp.setType("cg")
ksp.setTolerances(rtol=1e-14)
ksp.setOperators(A)
ksp.setFromOptions()
ksp.solve(b_nested,u_nested)

For context, I’m working in the Electrical Impedance Tomography problem, more specific in the Complete Electrode Method. Here, we are looking to solve for a potential u:\Omega \to \mathbb R and respective noised measures U_1,...,U_L\in \mathbb R on the boundary. This model form looks like:

B((u,U),(v,V)) = \sum_{l=1}^L I_l V_l \\

where

B((u,U),(v,V)) = \iint_\Omega \gamma \nabla u \cdot \nabla v dx + \sum_{l=1}^L \frac{1}{z_l}\int_{E_l} (u-U_l)(v - V_l)d s ,

beeing u,v \in H^1(\Omega), U,V \in \mathbb R^L, \gamma \in L^\infty(\Omega), I_j, z_j \in \mathbb R and E_1,..., E_L a set of L integration domains on the boundary representing electrodes. As it needs to solve for a (u,U) \in H^1(\Omega)\times \mathbb R^L vector and there are no available spaces for \mathbb R^L domain, I tried to manually assemble the matrix in a block structure as suggested in this topic.

Given the discretized domain with N dofs and L integration domains, the assembled matrix has the block form

A = \begin{bmatrix} A^{(1)} & A^{(2)} \\ (A^{(2)})^T & A^{(4)} \end{bmatrix} \in \mathbb R^{(N+L) \times (N+L)}

with

A^{(1)}_{p,n} = \begin{bmatrix} \left( \sum_{j=1}^{K} \gamma_j \int_{T_j} \nabla \Upsilon_p \cdot \nabla \Upsilon_n dx \right) + \left( \sum_{j=1}^{L} \frac{1}{z_j} \int_{E_j} \Upsilon_p \Upsilon_n dS \right) \end{bmatrix} \in \mathbb{R}^{N \times N}
A^{(2)}_{p,n} = \begin{bmatrix} \left( \frac{1}{z_n} \int_{E_n} \Upsilon_p dS \right) \end{bmatrix}\in \mathbb{R}^{N \times L}
A^{(4)}_{p,k} = \begin{bmatrix} \left( \frac{1}{z_p} \int_{E_p} dS \right) \end{bmatrix}\in \mathbb{R}^{L \times L}

beeing \Upsilon the function element basis.

Thank you very much for the help!