# This script demonstrates solving a symmetric saddle-point linear system using PETSc and HPDDM preconditioner
# It must be run with exactly 4 MPI processes
# Example run:
#   mpiexec -n 4 python3 saddle_point.py -ksp_monitor -ksp_type fgmres -ksp_max_it 10 -ksp_rtol 1e-4 -fieldsplit_ksp_max_it 100 -fieldsplit_pc_hpddm_levels_1_eps_nev 10 -fieldsplit_pc_hpddm_levels_1_st_share_sub_ksp -fieldsplit_pc_hpddm_has_neumann -fieldsplit_pc_hpddm_define_subdomains -fieldsplit_1_pc_hpddm_schur_precondition geneo -fieldsplit_pc_hpddm_coarse_pc_type cholesky -fieldsplit_pc_hpddm_levels_1_sub_pc_type lu -fieldsplit_ksp_type fgmres -fieldsplit_1_pc_hpddm_coarse_correction balanced -fieldsplit_1_pc_hpddm_levels_1_eps_gen_non_hermitian
# For more options, see ${PETSC_DIR}/src/ksp/ksp/tutorials/ex87.c

import sys
import petsc4py
# Initialize PETSc with command-line arguments
petsc4py.init(sys.argv)
from petsc4py import PETSc

# Function to load matrices and index sets from binary files
def mat_and_is_load(prefix, identifier, A, aux_IS, aux_Mat, rank, size):
# Load an index set (IS) from binary file
  sizes = PETSc.IS().load(PETSc.Viewer().createBinary(f"{prefix}{identifier}_sizes_{rank}_{size}.dat", "r", comm = PETSc.COMM_SELF))
# Get indices from the loaded IS
  idx   = sizes.getIndices()
# Set the local and global sizes of the matrix
  A.setSizes([[idx[0], idx[2]], [idx[1], idx[3]]])
# Configure matrix using runtime options
  A.setFromOptions()
# Load matrix A from binary file
  A = A.load(PETSc.Viewer().createBinary(f"{prefix}{identifier}.dat", "r", comm = PETSc.COMM_WORLD))

# Load an index set (IS) from binary file
  aux_IS.load(PETSc.Viewer().createBinary(f"{prefix}{identifier}_is_{rank}_{size}.dat", "r", comm = PETSc.COMM_SELF))
# Load the Neumann matrix of the current process
  aux_Mat.load(PETSc.Viewer().createBinary(f"{prefix}{identifier}_aux_{rank}_{size}.dat", "r", comm = PETSc.COMM_SELF))

# Get the size of the communicator
size = PETSc.COMM_WORLD.getSize()
# Get the rank of the current process
rank = PETSc.COMM_WORLD.getRank()
if size != 4:
  if rank == 0:
    print("This example requires 4 processes")
  quit()

# Problem type (either 'elasticity' or 'stokes')
system_str = PETSc.Options().getString("system", "elasticity")
id_sys = 0 if system_str == "elasticity" else 1
empty_A11 = False
# Lower-left (1,1) block is never zero when problem type is 'elasticity'
if id_sys == 1:
  empty_A11 = PETSc.Options().getBool("empty_A11", False)

# 2-by-2 block structure
A = [None, None, None, None]
# Auxiliary data only for the diagonal blocks
aux_Mat = [None, None]
aux_IS = [None, None]

# Create placeholder objects for the diagonal blocks
A[0] = PETSc.Mat().create(comm = PETSc.COMM_WORLD)
A[0].setFromOptions()
aux_IS[0] = PETSc.IS().create(comm = PETSc.COMM_SELF)
aux_Mat[0] = PETSc.Mat().create(comm = PETSc.COMM_SELF)
A[3] = PETSc.Mat().create(comm = PETSc.COMM_WORLD)
A[3].setFromOptions()
aux_IS[1] = PETSc.IS().create(comm = PETSc.COMM_SELF)
aux_Mat[1] = PETSc.Mat().create(comm = PETSc.COMM_SELF)

# Load directory for input data
load_dir = PETSc.Options().getString("load_dir", "${DATAFILESPATH}/matrices/hpddm/GENEO")
# Specific prefix for each problem
prefix = f"{load_dir}/{ 'B' if id_sys == 1 else 'A' }"

# Diagonal blocks and auxiliary data for PCHPDDM
mat_and_is_load(prefix, "00", A[0], aux_IS[0], aux_Mat[0], rank, size)
mat_and_is_load(prefix, "11", A[3], aux_IS[1], aux_Mat[1], rank, size)

# Coherent off-diagonal (0,1) block
A[2] = PETSc.Mat().create(comm = PETSc.COMM_WORLD)
n, _ = A[0].getLocalSize()
N, _ = A[0].getSize()
m, _ = A[3].getLocalSize()
M, _ = A[3].getSize()
# Set matrix sizes based on the sizes of (0,0) and (1,1) blocks
A[2].setSizes([[m, M], [n, N]])
A[2].setFromOptions()
A[2].load(PETSc.Viewer().createBinary(f"{load_dir}/{ 'B' if id_sys == 1 else 'A' }10.dat", "r", comm = PETSc.COMM_WORLD))
# Create a matrix that behaves likes A[1]' without explicitly assembling it
A[1] = PETSc.Mat().createTranspose(A[2])

# Global MatNest
S = PETSc.Mat().createNest([[A[0], A[1]], [A[2], A[3] if not empty_A11 else None]])

ksp = PETSc.KSP().create(comm = PETSc.COMM_WORLD)
ksp.setOperators(S)
pc = ksp.getPC()

# Use FIELDSPLIT as the outer preconditioner
pc.setType(PETSc.PC.Type.FIELDSPLIT)
# Use SCHUR since there are only two fields
pc.setFieldSplitType(PETSc.PC.CompositeType.SCHUR)
# Use SELF because HPDDM deals with a Schur complement matrix
pc.setFieldSplitSchurPreType(PETSc.PC.FieldSplitSchurPreType.SELF)
# Apply any command-line options (which may override options from the source file)
pc.setFromOptions()
# Setup the outer preconditioner so that one can query the inner preconditioners
pc.setUp()

# Retrieve the inner solvers (associated to the diagonal blocks)
ksp0, ksp1 = pc.getFieldSplitSubKSP()

# Upper-left (0,0) block
pc0 = ksp0.getPC()
# Use HPDDM as the preconditioner
pc0.setType(PETSc.PC.Type.HPDDM)
# Set the index set (local-to-global numbering) and auxiliary matrix (first diagonal block)
pc0.setHPDDMAuxiliaryMat(aux_IS[0], aux_Mat[0])
pc0.setFromOptions()

# Schur complement
pc1 = ksp1.getPC()
# Use HPDDM as the preconditioner
pc1.setType(PETSc.PC.Type.HPDDM)
if not empty_A11:
# Set the index set (local-to-global numbering) and auxiliary matrix (second diagonal block)
# If there is no block (-empty_A11), then these are computed automatically by HPDDM
  pc1.setHPDDMAuxiliaryMat(aux_IS[1], aux_Mat[1])
pc1.setFromOptions()

# Create RHS (b) and solution (x) vectors, load b from file, and solve the system
b, x = S.createVecs()
b.load(PETSc.Viewer().createBinary(f"{load_dir}/rhs_{ 'B' if id_sys == 1 else 'A' }.dat", "r", comm = PETSc.COMM_WORLD))
ksp.setFromOptions()
ksp.solve(b, x)
