LCOV - code coverage report
Current view: top level - src/problems - NavierStokesProblem.C (source / functions) Hit Total Coverage
Test: idaholab/moose navier_stokes: ba1ead Lines: 118 128 92.2 %
Date: 2025-08-13 06:50:25 Functions: 10 10 100.0 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : //* This file is part of the MOOSE framework
       2             : //* https://mooseframework.inl.gov
       3             : //*
       4             : //* All rights reserved, see COPYRIGHT for full restrictions
       5             : //* https://github.com/idaholab/moose/blob/master/COPYRIGHT
       6             : //*
       7             : //* Licensed under LGPL 2.1, please see LICENSE for details
       8             : //* https://www.gnu.org/licenses/lgpl-2.1.html
       9             : 
      10             : #include "NavierStokesProblem.h"
      11             : #include "NonlinearSystemBase.h"
      12             : #include "libmesh/petsc_matrix.h"
      13             : 
      14             : using namespace libMesh;
      15             : 
      16             : registerMooseObject("NavierStokesApp", NavierStokesProblem);
      17             : 
      18             : InputParameters
      19         330 : NavierStokesProblem::validParams()
      20             : {
      21         330 :   InputParameters params = FEProblem::validParams();
      22         330 :   params.addClassDescription("A problem that handles Schur complement preconditioning of the "
      23             :                              "incompressible Navier-Stokes equations");
      24         660 :   params.addParam<TagName>(
      25             :       "mass_matrix", "", "The matrix tag name corresponding to the mass matrix.");
      26         660 :   params.addParam<TagName>(
      27             :       "L_matrix",
      28             :       "",
      29             :       "The matrix tag name corresponding to the diffusive part of the velocity equations.");
      30         660 :   params.addParam<std::vector<unsigned int>>(
      31             :       "schur_fs_index",
      32             :       {},
      33             :       "if not provided then the top field split is assumed to be the "
      34             :       "Schur split. This is a vector to allow recursive nesting");
      35         660 :   params.addParam<bool>("use_pressure_mass_matrix",
      36         660 :                         false,
      37             :                         "Whether to just use the pressure mass matrix as the preconditioner for "
      38             :                         "the Schur complement");
      39         660 :   params.addParam<bool>(
      40             :       "commute_lsc",
      41         660 :       false,
      42             :       "Whether to use the commuted form of the LSC preconditioner, created by Olshanskii");
      43         330 :   return params;
      44           0 : }
      45             : 
      46         165 : NavierStokesProblem::NavierStokesProblem(const InputParameters & parameters) : FEProblem(parameters)
      47             : #if PETSC_RELEASE_LESS_THAN(3, 20, 0)
      48             : {
      49             :   mooseError("The preconditioning techniques made available through this class require a PETSc "
      50             :              "version of at least 3.20");
      51             : }
      52             : #else
      53             :     ,
      54         165 :     _commute_lsc(getParam<bool>("commute_lsc")),
      55         330 :     _mass_matrix(getParam<TagName>("mass_matrix")),
      56         330 :     _L_matrix(getParam<TagName>("L_matrix")),
      57         165 :     _have_mass_matrix(!_mass_matrix.empty()),
      58         165 :     _have_L_matrix(!_L_matrix.empty()),
      59         330 :     _pressure_mass_matrix_as_pre(getParam<bool>("use_pressure_mass_matrix")),
      60         495 :     _schur_fs_index(getParam<std::vector<unsigned int>>("schur_fs_index"))
      61             : {
      62         165 :   if (_commute_lsc)
      63             :   {
      64          19 :     if (!_have_mass_matrix)
      65           0 :       paramError("mass_matrix",
      66             :                  "A pressure mass matrix must be provided if we are commuting the LSC commutator.");
      67          19 :     if (!_have_L_matrix)
      68           0 :       paramError("L_matrix",
      69             :                  "A matrix corresponding to the viscous component of the momentum equation must be "
      70             :                  "provided if we are commuting the LSC commutator.");
      71             :   }
      72         146 :   else if (_have_L_matrix)
      73           0 :     paramError("L_matrix",
      74             :                "If not commuting the LSC commutator, then the 'L_matrix' should not be provided "
      75             :                "because it will not be used. For Elman LSC preconditioning, L will be computed "
      76             :                "automatically using system matrix data (e.g. the off-diagonal blocks in the "
      77             :                "velocity-pressure system).");
      78             : 
      79         165 :   if (_pressure_mass_matrix_as_pre && !_have_mass_matrix)
      80           0 :     paramError("mass_matrix",
      81             :                "If 'use_pressure_mass_matrix', then a pressure 'mass_matrix' must be provided");
      82         165 : }
      83             : 
      84         330 : NavierStokesProblem::~NavierStokesProblem()
      85             : {
      86             :   auto ierr = (PetscErrorCode)0;
      87         165 :   if (_Q_scale)
      88             :   {
      89         116 :     ierr = MatDestroy(&_Q_scale);
      90         116 :     CHKERRABORT(this->comm().get(), ierr);
      91             :   }
      92         165 :   if (_L)
      93             :   {
      94          14 :     ierr = MatDestroy(&_L);
      95          14 :     CHKERRABORT(this->comm().get(), ierr);
      96             :   }
      97         330 : }
      98             : 
      99             : KSP
     100        3338 : NavierStokesProblem::findSchurKSP(KSP node, const unsigned int tree_position)
     101             : {
     102        3338 :   auto it = _schur_fs_index.begin() + tree_position;
     103        3338 :   if (it == _schur_fs_index.end())
     104             :     return node;
     105             : 
     106             :   PC fs_pc;
     107             :   PetscInt num_splits;
     108             :   KSP * subksp;
     109             :   KSP next_ksp;
     110             :   IS is;
     111             :   PetscBool is_fs;
     112             : 
     113        1432 :   auto sub_ksp_index = *it;
     114             : 
     115             :   // Get the preconditioner associated with the linear solver
     116        1432 :   LibmeshPetscCall(KSPGetPC(node, &fs_pc));
     117             : 
     118             :   // Verify the preconditioner is a field split preconditioner
     119        1432 :   LibmeshPetscCall(PetscObjectTypeCompare((PetscObject)fs_pc, PCFIELDSPLIT, &is_fs));
     120        1432 :   if (!is_fs)
     121           0 :     mooseError("Not a field split. Please check the 'schur_fs_index' parameter");
     122             : 
     123             :   // Setup the preconditioner. We need to call this first in order to be able to retrieve the sub
     124             :   // ksps and sub index sets associated with the splits
     125        1432 :   LibmeshPetscCall(PCSetUp(fs_pc));
     126             : 
     127             :   // Get the linear solvers associated with each split
     128        1432 :   LibmeshPetscCall(PCFieldSplitGetSubKSP(fs_pc, &num_splits, &subksp));
     129        1432 :   next_ksp = subksp[sub_ksp_index];
     130             : 
     131             :   // Get the index set for the split at this level of the tree we are traversing to the Schur
     132             :   // complement preconditioner
     133        1432 :   LibmeshPetscCall(PCFieldSplitGetISByIndex(fs_pc, sub_ksp_index, &is));
     134             : 
     135             :   // Store this tree level's index set, which we will eventually use to get the sub-matrices
     136             :   // required for our preconditioning process from the system matrix
     137        1432 :   _index_sets.push_back(is);
     138             : 
     139             :   // Free the array of sub linear solvers that got allocated in the PCFieldSplitGetSubKSP call
     140        1432 :   LibmeshPetscCall(PetscFree(subksp));
     141             : 
     142             :   // Continue traversing down the tree towards the Schur complement linear solver/preconditioner
     143        1432 :   return findSchurKSP(next_ksp, tree_position + 1);
     144             : }
     145             : 
     146             : void
     147        1906 : NavierStokesProblem::setupLSCMatrices(KSP schur_ksp)
     148             : {
     149             :   KSP * subksp; // This will be length two, with the former being the A KSP and the latter being the
     150             :                 // Schur complement KSP
     151             :   KSP schur_complement_ksp;
     152             :   PC schur_pc, lsc_pc;
     153             :   PetscInt num_splits;
     154             :   Mat lsc_pc_pmat;
     155             :   IS velocity_is, pressure_is;
     156             :   PetscInt rstart, rend;
     157             :   PetscBool is_lsc, is_fs;
     158             :   std::vector<Mat> intermediate_Qs;
     159             :   std::vector<Mat> intermediate_Ls;
     160             : 
     161             :   // Get the preconditioner for the linear solver. It must be a field split preconditioner
     162        1906 :   LibmeshPetscCall(KSPGetPC(schur_ksp, &schur_pc));
     163        1906 :   LibmeshPetscCall(PetscObjectTypeCompare((PetscObject)schur_pc, PCFIELDSPLIT, &is_fs));
     164        1906 :   if (!is_fs)
     165           0 :     mooseError("Not a field split. Please check the 'schur_fs_index' parameter");
     166             : 
     167             :   // The mass matrix. This will correspond to velocity degrees of freedom for Elman LSC and pressure
     168             :   // degrees of freedom for Olshanskii LSC or when directly using the mass matrix as a
     169             :   // preconditioner for the Schur complement
     170             :   Mat global_Q = nullptr;
     171        1906 :   if (_have_mass_matrix)
     172             :     global_Q =
     173        1906 :         static_cast<PetscMatrix<Number> &>(getNonlinearSystemBase(0).getMatrix(massMatrixTagID()))
     174        1906 :             .mat();
     175             :   // The Poisson operator matrix corresponding to the velocity degrees of freedom. This is only used
     176             :   // and is required for Olshanskii LSC preconditioning
     177             :   Mat global_L = nullptr;
     178        1906 :   if (_have_L_matrix)
     179             :     global_L =
     180          42 :         static_cast<PetscMatrix<Number> &>(getNonlinearSystemBase(0).getMatrix(LMatrixTagID()))
     181          42 :             .mat();
     182             : 
     183             :   //
     184             :   // Process down from our system matrix to the sub-matrix containing the velocity-pressure dofs for
     185             :   // which we are going to be doing the Schur complement preconditioning
     186             :   //
     187             : 
     188        1948 :   auto process_intermediate_mats = [this](auto & intermediate_mats, auto parent_mat)
     189             :   {
     190             :     mooseAssert(parent_mat, "This should be non-null");
     191        1948 :     intermediate_mats.resize(_index_sets.size());
     192        3380 :     for (const auto i : index_range(_index_sets))
     193             :     {
     194        1432 :       auto intermediate_is = _index_sets[i];
     195             :       Mat intermediate_mat;
     196        1432 :       LibmeshPetscCall(MatCreateSubMatrix(i == 0 ? parent_mat : intermediate_mats[i - 1],
     197             :                                           intermediate_is,
     198             :                                           intermediate_is,
     199             :                                           MAT_INITIAL_MATRIX,
     200             :                                           &intermediate_mat));
     201        1432 :       intermediate_mats[i] = intermediate_mat;
     202             :     }
     203        1948 :     return _index_sets.empty() ? parent_mat : intermediate_mats.back();
     204        1906 :   };
     205             : 
     206             :   Mat our_parent_Q = nullptr;
     207        1906 :   if (_have_mass_matrix)
     208        1906 :     our_parent_Q = process_intermediate_mats(intermediate_Qs, global_Q);
     209             :   Mat our_parent_L = nullptr;
     210        1906 :   if (_have_L_matrix)
     211          42 :     our_parent_L = process_intermediate_mats(intermediate_Ls, global_L);
     212             : 
     213             :   // Setup the preconditioner. We need to call this first in order to be able to retrieve the sub
     214             :   // ksps and sub index sets associated with the splits
     215        1906 :   LibmeshPetscCall(PCSetUp(schur_pc));
     216             : 
     217             :   // There are always two splits in a Schur complement split. The zeroth split is the split with the
     218             :   // on-diagonals, e.g. the velocity dofs. Here we retrive the velocity dofs/index set
     219        1906 :   LibmeshPetscCall(PCFieldSplitGetISByIndex(schur_pc, 0, &velocity_is));
     220             : 
     221             :   // Get the rows of the parent velocity-pressure matrix that our process owns
     222        1906 :   LibmeshPetscCall(MatGetOwnershipRange(our_parent_Q, &rstart, &rend));
     223             : 
     224        1906 :   if (_commute_lsc)
     225             :   {
     226             :     // If we're commuting LSC, e.g. doing Olshanskii, the user must have provided a Poisson operator
     227             :     // matrix
     228             :     mooseAssert(our_parent_L, "This should be non-null");
     229             : 
     230          42 :     if (!_L)
     231             :       // If this is our first time in this routine, then we create the matrix
     232          14 :       LibmeshPetscCall(
     233             :           MatCreateSubMatrix(our_parent_L, velocity_is, velocity_is, MAT_INITIAL_MATRIX, &_L));
     234             :     else
     235             :       // Else we reuse the matrix
     236          28 :       LibmeshPetscCall(
     237             :           MatCreateSubMatrix(our_parent_L, velocity_is, velocity_is, MAT_REUSE_MATRIX, &_L));
     238             :   }
     239             : 
     240             :   // Get the local index set complement corresponding to the pressure dofs from the velocity dofs
     241        1906 :   LibmeshPetscCall(ISComplement(velocity_is, rstart, rend, &pressure_is));
     242             : 
     243             :   auto create_q_scale_submat =
     244        1906 :       [our_parent_Q, this, velocity_is, pressure_is](const auto & mat_initialization)
     245             :   {
     246        1906 :     if (_commute_lsc || _pressure_mass_matrix_as_pre)
     247             :     {
     248             :       // If we are doing Olshanskii or we are using the pressure matrix directly as the
     249             :       // preconditioner (no LSC), then we must have access to a pressure mass matrix
     250             :       mooseAssert(our_parent_Q, "This should be non-null");
     251             :       // Create a sub-matrix corresponding to the pressure index set
     252         292 :       LibmeshPetscCall(MatCreateSubMatrix(
     253             :           our_parent_Q, pressure_is, pressure_is, mat_initialization, &_Q_scale));
     254             :     }
     255        1614 :     else if (_have_mass_matrix) // If we don't have a mass matrix and the user has requested scaling
     256             :                                 // then the diagonal of A will be used
     257             :     {
     258             :       // The user passed us a mass matrix tag; we better have been able to obtain a parent Q in that
     259             :       // case
     260             :       mooseAssert(our_parent_Q, "This should be non-null");
     261             :       // We are not commuting LSC, so we are doing Elman, and the user has passed us a mass matrix
     262             :       // tag. In this case we are creating a velocity mass matrix, so we use the velocity index set
     263        1614 :       LibmeshPetscCall(MatCreateSubMatrix(
     264             :           our_parent_Q, velocity_is, velocity_is, mat_initialization, &_Q_scale));
     265             :     }
     266        1906 :   };
     267             : 
     268        1906 :   if (!_Q_scale)
     269             :     // We haven't allocated the scaling matrix yet
     270         116 :     create_q_scale_submat(MAT_INITIAL_MATRIX);
     271             :   else
     272             :     // We have allocated the scaling matrix, so we can reuse
     273        1790 :     create_q_scale_submat(MAT_REUSE_MATRIX);
     274             : 
     275             :   // We don't need the pressure index set anymore
     276        1906 :   LibmeshPetscCall(ISDestroy(&pressure_is));
     277             : 
     278             :   // Nor the intermediate matrices
     279        3338 :   for (auto & mat : intermediate_Qs)
     280        1432 :     LibmeshPetscCall(MatDestroy(&mat));
     281        1906 :   for (auto & mat : intermediate_Ls)
     282           0 :     LibmeshPetscCall(MatDestroy(&mat));
     283             : 
     284             :   // Get the sub KSP for the Schur split that corresponds to the linear solver for the Schur
     285             :   // complement (e.g. rank equivalent to the pressure rank)
     286        1906 :   LibmeshPetscCall(PCFieldSplitGetSubKSP(schur_pc, &num_splits, &subksp));
     287        1906 :   if (num_splits != 2)
     288           0 :     mooseError("The number of splits should be two");
     289             :   // The Schur complement linear solver is always at the first index (for the pressure dofs;
     290             :   // velocity dof KSP is at index 0)
     291        1906 :   schur_complement_ksp = subksp[1];
     292             : 
     293        1906 :   if (_pressure_mass_matrix_as_pre)
     294             :   {
     295             :     mooseAssert(_Q_scale, "This should be non-null");
     296             :     Mat S;
     297             :     // Set the Schur complement preconditioner to be the pressure mass matrix
     298         250 :     LibmeshPetscCall(PCFieldSplitSetSchurPre(schur_pc, PC_FIELDSPLIT_SCHUR_PRE_USER, _Q_scale));
     299             :     // Get the Schur complement operator S, which in generic KSP speak is used for the operator A
     300         250 :     LibmeshPetscCall(KSPGetOperators(schur_complement_ksp, &S, NULL));
     301             :     // Set, in generic KSP speak, the operators A and P respectively. So our pressure mass matrix is
     302             :     // P
     303         250 :     LibmeshPetscCall(KSPSetOperators(schur_complement_ksp, S, _Q_scale));
     304             :   }
     305             :   else // We are doing LSC preconditioning
     306             :   {
     307             :     // Get the least squares commutator preconditioner for the Schur complement
     308        1656 :     LibmeshPetscCall(KSPGetPC(schur_complement_ksp, &lsc_pc));
     309             :     // Verify that it's indeed an LSC preconditioner
     310        1656 :     LibmeshPetscCall(PetscObjectTypeCompare(PetscObject(lsc_pc), PCLSC, &is_lsc));
     311        1656 :     if (!is_lsc)
     312           0 :       mooseError("Not an LSC PC. Please check the 'schur_fs_index' parameter");
     313             : 
     314             :     // Get the LSC preconditioner
     315        1656 :     LibmeshPetscCall(PCGetOperators(lsc_pc, NULL, &lsc_pc_pmat));
     316             : 
     317        1656 :     if (_commute_lsc)
     318             :     {
     319             :       // We're doing Olshanskii. We must have a user-provided Poisson operator
     320             :       mooseAssert(_L, "This should be non-null");
     321             :       // Attach our L matrix to the PETSc object. PETSc will use this during the preconditioner
     322             :       // application
     323          42 :       LibmeshPetscCall(PetscObjectCompose((PetscObject)lsc_pc_pmat, "LSC_L", (PetscObject)_L));
     324             :       // Olshanskii preconditioning requires a pressure mass matrix
     325             :       mooseAssert(_have_mass_matrix, "This is to verify we will enter the next conditional");
     326             :     }
     327        1656 :     if (_have_mass_matrix)
     328             :     {
     329             :       mooseAssert(_Q_scale, "This should be non-null");
     330             :       // Attach our scaling/mass matrix to the PETSc object. PETSc will use this during the
     331             :       // preconditioner application
     332        1656 :       LibmeshPetscCall(
     333             :           PetscObjectCompose((PetscObject)lsc_pc_pmat, "LSC_Qscale", (PetscObject)_Q_scale));
     334             :     }
     335             :   }
     336             : 
     337             :   // Free the sub-KSP array that was allocated during PCFieldSplitGetSubKSP
     338        1906 :   LibmeshPetscCall(PetscFree(subksp));
     339        1906 : }
     340             : 
     341             : PetscErrorCode
     342        1906 : navierStokesKSPPreSolve(KSP root_ksp, Vec /*rhs*/, Vec /*x*/, void * context)
     343             : {
     344             :   PetscFunctionBegin;
     345             : 
     346             :   auto * ns_problem = static_cast<NavierStokesProblem *>(context);
     347             :   ns_problem->clearIndexSets();
     348        1906 :   auto schur_ksp = ns_problem->findSchurKSP(root_ksp, 0);
     349        1906 :   ns_problem->setupLSCMatrices(schur_ksp);
     350             : 
     351        1906 :   PetscFunctionReturn(PETSC_SUCCESS);
     352             : }
     353             : 
     354             : void
     355         370 : NavierStokesProblem::initPetscOutputAndSomeSolverSettings()
     356             : {
     357         370 :   FEProblem::initPetscOutputAndSomeSolverSettings();
     358             : 
     359         370 :   if (!_have_mass_matrix)
     360             :   {
     361             :     mooseAssert(
     362             :         !_have_L_matrix,
     363             :         "If we don't have a mass matrix, which is only supported for traditional LSC "
     364             :         "preconditioning (e.g. Elman, not Olshanskii), then we also shouldn't have an L matrix "
     365             :         "because we automatically form the L matrix when doing traditional LSC preconditioning");
     366          14 :     return;
     367             :   }
     368             : 
     369             :   // Set the pre-KSP solve callback. At that time we will setup our Schur complement preconditioning
     370             :   KSP ksp;
     371         356 :   auto snes = currentNonlinearSystem().getSNES();
     372         356 :   LibmeshPetscCall(SNESGetKSP(snes, &ksp));
     373         356 :   LibmeshPetscCall(KSPSetPreSolve(ksp, &navierStokesKSPPreSolve, this));
     374             : }
     375             : 
     376             : #endif

Generated by: LCOV version 1.14