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

1 Like

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.

2 Likes

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

1 Like

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.

1 Like

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.

1 Like

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);
1 Like

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.

1 Like

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.

2 Likes

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!