// Copyright (c) 2010-2023, Lawrence Livermore National Security, LLC. Produced // at the Lawrence Livermore National Laboratory. All Rights reserved. See files // LICENSE and NOTICE for details. LLNL-CODE-806117. // // This file is part of the MFEM library. For more information and source code // availability visit https://mfem.org. // // MFEM is free software; you can redistribute it and/or modify it under the // terms of the BSD-3 license. We welcome feedback and contributions, see file // CONTRIBUTING.md for details. // // --------------------------------------------------- // ParELAG Miniapp: AMGe solver for H(curl) and H(div) // --------------------------------------------------- // // This miniapp employs MFEM and ParELAG to solve H(curl)- and H(div)-elliptic // forms by an element based algebraic multigrid (AMGe). It uses: a multilevel // hierarchy of de Rham complexes of finite element spaces, built by ParELAG; // Hiptmair-type smoothers, implemented in ParELAG; and AMS (Auxiliary-space // Maxwell Solver) or ADS (Auxiliary-space Divergence Solver), from HYPRE, for // preconditioning or solving on the coarsest levels. See the README file in // this directory for more details. // // Compile with: see README // // Sample runs: MultilevelHcurlHdivSolver -curl -f \ // MultilevelHcurlSolver_pipe_example_parameters.xml // MultilevelHcurlHdivSolver -div -f \ // MultilevelHdivSolver_pipe_example_parameters.xml // // We recommend viewing MFEM's examples 3 and 4 before viewing this miniapp. #include #include #include #include #include #include #include #include "elag.hpp" #include "utilities/MPIDataTypes.hpp" using namespace mfem; using namespace parelag; using namespace std; void bdrfunc(const Vector &, Vector &); void rhsfunc(const Vector &, Vector &); int main(int argc, char *argv[]) { // Initialize MPI and HYPRE. Mpi::Init(); int num_ranks = Mpi::WorldSize(); int myid = Mpi::WorldRank(); Hypre::Init(); Timer total_timer = TimeManager::AddTimer("Program Execution -- Total"); Timer init_timer = TimeManager::AddTimer("Initial Setup"); if (!myid) cout << "-- This is an example of using a geometric-like multilevel " "hierarchy, constructed by ParELAG,\n" "-- to solve respective finite element H(curl) and H(div) forms: \n" "(alpha curl u, curl v) + (beta u, v);\n" "(alpha div u, div v) + (beta u, v).\n\n"; // Get basic parameters from command line. const char *xml_file_c = NULL; bool hcurl = true; bool visualize = false; double tolSVD = 1e-3; OptionsParser args(argc, argv); args.AddOption(&xml_file_c, "-f", "--xml-file", "XML parameter list (an XML file with detailed parameters)."); args.AddOption(&hcurl, "-curl", "--hcurl", "-div", "--hdiv", "Whether the H(curl) or H(div) form is being solved."); args.AddOption(&visualize, "-v", "--visualize", "-nv", "--no-visualize", "Use GLVis to visualize the final solution and the " "agglomerates."); args.AddOption(&tolSVD, "-s", "--svd-tol", "SVD tolerance. It is used for filtering out local linear " "dependencies in the basis construction and extension " "process in ParELAG. Namely, right singular vectors with " "singular values smaller than this tolerance are removed."); args.Parse(); if (!args.Good()) { if (!myid) { args.PrintUsage(cout); } return EXIT_FAILURE; } if (!xml_file_c) { if (hcurl) { xml_file_c = "MultilevelHcurlSolver_cube_example_parameters.xml"; } else { xml_file_c = "MultilevelHdivSolver_cube_example_parameters.xml"; } if (!myid) { cout << "No XML parameter list provided! Using default " << xml_file_c << "." << endl; } } if (!myid) { args.PrintOptions(cout); } string xml_file(xml_file_c); // Read and parse the detailed parameter list from file. unique_ptr master_list; ifstream xml_in(xml_file); if (!xml_in.good()) { if (!myid) { cerr << "ERROR: Cannot read from input file: " << xml_file << ".\n"; } return EXIT_FAILURE; } SimpleXMLParameterListReader reader; master_list = reader.GetParameterList(xml_in); xml_in.close(); // General parameters for the problem. ParameterList& prob_list = master_list->Sublist("Problem parameters", true); // The file from which to read the mesh. const string meshfile = prob_list.Get("Mesh file", ""); // The number of times to refine in serial. // Negative means refine until mesh is big enough to distribute, i.e., // until the number of elements is 6 times the number of processes. int ser_ref_levels = prob_list.Get("Serial refinement levels", -1); // The number of times to refine in parallel. This determines the // number of levels in the AMGe hierarchy, if amge_levels is set to 0. const int par_ref_levels = prob_list.Get("Parallel refinement levels", 2); // Number of levels in the AMGe hierarchy. Should not be larger than // par_ref_levels + 1. If set to 0, it will be interpreted as equal to // par_ref_levels + 1. const int amge_levels = prob_list.Get("AMGe levels", 0); // The order of the finite elements on the finest level. const int feorder = prob_list.Get("Finite element order", 0); // The order of the polynomials to include in the coarse spaces // (after interpolating them onto the fine space). const int upscalingOrder = prob_list.Get("Upscaling order", 0); // A list of 1s and 0s stating which boundary attribute is appointed as // essential. If only a single entry is given, it is applied to the whole // boundary. That is, if a single 0 is given the whole boundary is "natural", // while a single 1 means that the whole boundary is essential. vector par_ess_attr = prob_list.Get("Essential attributes", vector {1}); // A list of (piecewise) constant values for the coefficient 'alpha', in // accordance with the mesh attributes. If only a single entry is given, it // is applied to the whole mesh/domain. vector alpha_vals = prob_list.Get("alpha values", vector {1.0}); // A list of (piecewise) constant values for the coefficient 'beta', in // accordance with the mesh attributes. If only a single entry is given, it // is applied to the whole mesh/domain. vector beta_vals = prob_list.Get("beta values", vector {1.0}); // The list of solvers to invoke. auto list_of_solvers = prob_list.Get>("List of linear solvers"); ostringstream mesh_msg; if (!myid) { mesh_msg << '\n' << string(50, '*') << '\n' << "* Mesh: " << meshfile << "\n*\n" << "* FE order: " << feorder << '\n' << "* Upscaling order: " << upscalingOrder << "\n*\n"; } // Read the (serial) mesh from the given mesh file and uniformly refine it. shared_ptr pmesh; { if (!myid) { cout << "\nReading and refining serial mesh...\n"; cout << "Times to refine mesh in serial: " << ser_ref_levels << ".\n"; } ifstream imesh(meshfile); if (!imesh) { if (!myid) { cerr << "ERROR: Cannot open mesh file: " << meshfile << ".\n"; } return EXIT_FAILURE; } auto mesh = make_unique(imesh, true, true); imesh.close(); for (int l = 0; l < ser_ref_levels; ++l) { if (!myid) { cout << "Refining mesh in serial: " << l + 1 << "...\n"; } mesh->UniformRefinement(); } if (ser_ref_levels < 0) { ser_ref_levels = 0; for (; mesh->GetNE() < 6 * num_ranks; ++ser_ref_levels) { if (!myid) { cout << "Refining mesh in serial: " << ser_ref_levels + 1 << "...\n"; } mesh->UniformRefinement(); } } if (!myid) { cout << "Times refined mesh in serial: " << ser_ref_levels << ".\n"; cout << "Building and refining parallel mesh...\n"; cout << "Times to refine mesh in parallel: " << par_ref_levels << ".\n"; mesh_msg << "* Serial refinements: " << ser_ref_levels << '\n' << "* Coarse mesh size: " << mesh->GetNE() << "\n*\n"; } pmesh = make_shared(MPI_COMM_WORLD, *mesh); } // Mark essential boundary attributes. MFEM_VERIFY(par_ess_attr.size() <= 1 || par_ess_attr.size() == (unsigned) pmesh->bdr_attributes.Max(), "Incorrect size of the essential attributes vector in parameters" " input."); vector> ess_attr(1); ess_attr[0].SetSize(pmesh->bdr_attributes.Max()); if (par_ess_attr.size() == 0) { ess_attr[0] = 1; } else if (par_ess_attr.size() == 1) { ess_attr[0] = par_ess_attr[0]; } else { for (unsigned i = 0; i < par_ess_attr.size(); ++i) { ess_attr[0][i] = par_ess_attr[i]; } } // Initialize piecewise constant coefficients in the form. MFEM_VERIFY(alpha_vals.size() <= 1 || alpha_vals.size() == (unsigned) pmesh->attributes.Max(), "Incorrect size of the 'alpha' local values vector in parameters" " input."); MFEM_VERIFY(alpha_vals.size() <= 1 || alpha_vals.size() == (unsigned) pmesh->attributes.Max(), "Incorrect size of the 'alpha' local values vector in parameters" " input."); PWConstCoefficient alpha(pmesh->attributes.Max()); PWConstCoefficient beta(pmesh->attributes.Max()); if (alpha_vals.size() == 0) { alpha = 1.0; } else if (alpha_vals.size() == 1) { alpha = alpha_vals[0]; } else { for (unsigned i = 0; i < alpha_vals.size(); ++i) { alpha(i+1) = alpha_vals[i]; } } if (beta_vals.size() == 0) { beta = 1.0; } else if (beta_vals.size() == 1) { beta = beta_vals[0]; } else { for (unsigned i = 0; i < beta_vals.size(); ++i) { beta(i+1) = beta_vals[i]; } } // Refine the mesh in parallel. const int nDimensions = pmesh->Dimension(); // This is mainly because AMS and ADS (at least the way ParELAG uses them) // are bound to be used in 3D. Note that, for the purpose of demonstration, // some of the code below is still constructed in a way that is applicable in // 2D as well, taking into account that case as well. Also, in 2D, ParELAG // defaults to H(div) interpretation of form 1. MFEM_VERIFY(nDimensions == 3, "Only 3D problems are currently supported."); const int nLevels = amge_levels <= 0 ? par_ref_levels + 1 : amge_levels; MFEM_VERIFY(nLevels <= par_ref_levels + 1, "Number of AMGe levels too high relative to parallel" " refinements."); vector level_nElements(nLevels); for (int l = 0; l < par_ref_levels; ++l) { if (!myid) { cout << "Refining mesh in parallel: " << l + 1 << (par_ref_levels - l > nLevels ? " (not in hierarchy)" : " (in hierarchy)") << "...\n"; } if (par_ref_levels - l < nLevels) { level_nElements[par_ref_levels - l] = pmesh->GetNE(); } pmesh->UniformRefinement(); } level_nElements[0] = pmesh->GetNE(); if (!myid) { cout << "Times refined mesh in parallel: " << par_ref_levels << ".\n"; } { size_t local_num_elmts = pmesh->GetNE(), global_num_elmts; MPI_Reduce(&local_num_elmts, &global_num_elmts, 1, GetMPIType(0), MPI_SUM, 0, MPI_COMM_WORLD); if (!myid) { mesh_msg << "* Parallel refinements: " << par_ref_levels << '\n' << "* Fine mesh size: " << global_num_elmts << '\n' << "* Total levels: " << nLevels << '\n' << string(50, '*') << "\n\n"; } } if (!myid) { cout << mesh_msg.str(); } init_timer.Stop(); // Obtain the hierarchy of agglomerate topologies. Timer agg_timer = TimeManager::AddTimer("Mesh Agglomeration -- Total"); Timer agg0_timer = TimeManager::AddTimer("Mesh Agglomeration -- Level 0"); if (!myid) { cout << "Agglomerating topology for " << nLevels - 1 << " coarse levels...\n"; } constexpr auto AT_elem = AgglomeratedTopology::ELEMENT; // This partitioner simply geometrically coarsens the mesh by recovering the // geometric coarse elements as agglomerate elements. That is, it reverts the // MFEM uniform refinement procedure to provide agglomeration. MFEMRefinedMeshPartitioner partitioner(nDimensions); vector> topology(nLevels); if (!myid) { cout << "Agglomerating level: 0...\n"; } topology[0] = make_shared(pmesh, nDimensions); if (!myid) { cout << "Level 0 global number of mesh entities: " << topology[0]-> GetNumberGlobalTrueEntities((AgglomeratedTopology::Entity)0); for (int j = 1; j <= nDimensions; ++j) cout << ", " << topology[0]-> GetNumberGlobalTrueEntities((AgglomeratedTopology::Entity)j); cout << endl; } agg0_timer.Stop(); for (int l = 0; l < nLevels - 1; ++l) { Timer aggl_timer = TimeManager::AddTimer(std::string("Mesh " "Agglomeration -- Level "). append(std::to_string(l+1))); Array partitioning(topology[l]->GetNumberLocalEntities(AT_elem)); partitioner.Partition(topology[l]->GetNumberLocalEntities(AT_elem), level_nElements[l + 1], partitioning); if (!myid) { cout << "Agglomerating level: " << l + 1 << "...\n"; } topology[l + 1] = topology[l]->CoarsenLocalPartitioning(partitioning, false, false, 2); if (!myid) { cout << "Level " << l + 1 << " global number of mesh entities: " << topology[l + 1]-> GetNumberGlobalTrueEntities((AgglomeratedTopology::Entity)0); for (int j = 1; j <= nDimensions; ++j) { cout << ", " << topology[l + 1]-> GetNumberGlobalTrueEntities((AgglomeratedTopology::Entity)j); } cout << endl; } } agg_timer.Stop(); if (visualize && nDimensions <= 3) { for (int l = 1; l < nLevels; ++l) { ShowTopologyAgglomeratedElements(topology[l].get(), pmesh.get()); } } // Construct the hierarchy of spaces, thus forming a hierarchy of (partial) // de Rham sequences. Timer derham_timer = TimeManager::AddTimer("DeRhamSequence Construction -- " "Total"); Timer derham0_timer = TimeManager::AddTimer("DeRhamSequence Construction -- " "Level 0"); if (!myid) { cout << "Building the fine-level de Rham sequence...\n"; } vector> sequence(topology.size()); const int jform = DeRhamSequence::GetForm(nDimensions, hcurl ? DeRhamSequence::HCURL : DeRhamSequence::HDIV); if (nDimensions == 3) { sequence[0] = make_shared(topology[0], pmesh.get(), feorder, true, false); } else { MFEM_VERIFY(nDimensions == 2, "Only 2D or 3D problems are supported " "by the utilized ParELAG."); if (hcurl) { MFEM_ABORT("No H(curl) 2D interpretation of form 1 is implemented."); } sequence[0] = make_shared(topology[0], pmesh.get(), feorder, true, false); } // To build H(curl) (form 1 in 3D), it is needed to obtain all forms and // spaces with larger indices. To use the so called "Hiptmair smoothers", a // one form lower is needed (H1, form 0). Anyway, to use AMS all forms and // spaces to H1 (0 form) are needed. Therefore, the entire de Rham complex is // constructed. // To build H(div) (form 2 in 3D), it is needed to obtain all forms and // spaces with larger indices. To use the so called "Hiptmair smoothers", a // one form lower is needed (H(curl), form 1, in 3D). To use AMS and ADS, all // forms and spaces to H1 (0 form) are needed. Therefore, the entire de Rham // complex is constructed. sequence[0]->SetjformStart(0); DeRhamSequenceFE *DRSequence_FE = sequence[0]->FemSequence(); MFEM_VERIFY(DRSequence_FE, "Failed to obtain the fine-level de Rham sequence."); if (!myid) { cout << "Level 0 global number of dofs: " << DRSequence_FE->GetDofHandler(0)->GetDofTrueDof(). GetTrueGlobalSize(); for (int j = 1; j <= nDimensions; ++j) { cout << ", " << DRSequence_FE->GetDofHandler(j)->GetDofTrueDof(). GetTrueGlobalSize(); } cout << endl; } if (!myid) { cout << "Setting coefficients and computing fine-level local " << "matrices...\n"; } DRSequence_FE->ReplaceMassIntegrator(AT_elem, jform, make_unique(beta), false); if (hcurl && nDimensions == 3) { DRSequence_FE->ReplaceMassIntegrator(AT_elem, jform + 1, make_unique(alpha), true); } else { DRSequence_FE->ReplaceMassIntegrator(AT_elem, jform + 1, make_unique(alpha), true); } if (!myid) { cout << "Interpolating and setting polynomial targets...\n"; } DRSequence_FE->SetUpscalingTargets(nDimensions, upscalingOrder); derham0_timer.Stop(); if (!myid) { cout << "Building the coarse-level de Rham sequences...\n"; } for (int l = 0; l < nLevels - 1; ++l) { Timer derhaml_timer = TimeManager::AddTimer(std::string("DeRhamSequence " "Construction -- Level "). append(std::to_string(l+1))); if (!myid) { cout << "Building the level " << l + 1 << " de Rham sequences...\n"; } sequence[l]->SetSVDTol(tolSVD); sequence[l + 1] = sequence[l]->Coarsen(); if (!myid) { auto DRSequence = sequence[l + 1]; cout << "Level " << l + 1 << " global number of dofs: " << DRSequence->GetDofHandler(0)->GetDofTrueDof(). GetTrueGlobalSize(); for (int j = 1; j <= nDimensions; ++j) { cout << ", " << DRSequence->GetDofHandler(j)->GetDofTrueDof(). GetTrueGlobalSize(); } cout << endl; } } derham_timer.Stop(); Timer assemble_timer = TimeManager::AddTimer("Fine Matrix Assembly"); if (!myid) { cout << "Assembling the fine-level system...\n"; } VectorFunctionCoefficient rhscoeff(nDimensions, rhsfunc); VectorFunctionCoefficient solcoeff(nDimensions, bdrfunc); // Take the vector FE space and construct a RHS linear form on it. Then, move // the linear form to a vector. This is local, i.e. on all known dofs for the // process. FiniteElementSpace *fespace = DRSequence_FE->GetFeSpace(jform); auto rhsform = make_unique(fespace); rhsform->AddDomainIntegrator(new VectorFEDomainLFIntegrator(rhscoeff)); rhsform->Assemble(); unique_ptr rhs = move(rhsform); // Obtain the boundary data. This is local, i.e. on all known dofs for the // process. auto solgf = make_unique(fespace); solgf->ProjectCoefficient(solcoeff); unique_ptr sol = move(solgf); // Create the parallel linear system. const SharingMap& hcurlhdiv_dofTrueDof = sequence[0]->GetDofHandler(jform)->GetDofTrueDof(); // System RHS, B. It is defined on the true dofs owned by the process. Vector B(hcurlhdiv_dofTrueDof.GetTrueLocalSize()); // System matrix, A. shared_ptr A; { // Get the mass and derivative operators. // For H(curl): // M1 represents the form (beta u, v) on H(curl) vector fields. // M2 represents the form (alpha u, v) on H(div) vector fields, in 3D. // D1 is the curl operator from H(curl) vector fields to H(div) vector // fields, in 3D. // In 2D, instead of considering H(div) vector fields, L2 scalar fields // are to be considered. // Thus, D1^T * M2 * D1 represents the form (alpha curl u, curl v) on // H(curl) vector fields. // For H(div): // M1 represents the form (beta u, v) on H(div) vector fields. // M2 represents the form (alpha u, v) on L2 scalar fields. // D1 is the divergence operator from H(div) vector fields to L2 scalar // fields. // Thus, D1^T * M2 * D1 represents the form (alpha div u, div v) on H(div) // vector fields. auto M1 = sequence[0]->ComputeMassOperator(jform), M2 = sequence[0]->ComputeMassOperator(jform + 1); auto D1 = sequence[0]->GetDerivativeOperator(jform); // spA = D1^T * M2 * D1 + M1 represents the respective H(curl) or H(div) // form: // (alpha curl u, curl v) + (beta u, v), on H(curl) vector fields; // (alpha div u, div v) + (beta u, v), on H(div) vector fields. // This is local, i.e. on all known dofs for the process. auto spA = ToUnique(Add(*M1, *ToUnique(RAP(*D1, *M2, *D1)))); // Eliminate the boundary conditions Array marker(spA->Height()); marker = 0; sequence[0]->GetDofHandler(jform)->MarkDofsOnSelectedBndr(ess_attr[0], marker); for (int i = 0; i < spA->Height(); ++i) { if (marker[i]) { spA->EliminateRowCol(i, sol->Elem(i), *rhs); } } A = Assemble(hcurlhdiv_dofTrueDof, *spA, hcurlhdiv_dofTrueDof); hcurlhdiv_dofTrueDof.Assemble(*rhs, B); } if (!myid) { cout << "A size: " << A->GetGlobalNumRows() << 'x' << A->GetGlobalNumCols() << '\n' << " A NNZ: " << A->NNZ() << '\n'; } MFEM_VERIFY(B.Size() == A->Height(), "Matrix and vector size are incompatible."); assemble_timer.Stop(); // Perform the solves. Timer solvers_timer = TimeManager::AddTimer("Solvers -- Total"); if (!myid) { cout << "\nRunning fine-level solvers...\n\n"; } // Create the solver library. auto lib = SolverLibrary::CreateLibrary( master_list->Sublist("Preconditioner Library")); // Loop through the solvers. for (const auto& solver_name : list_of_solvers) { Timer solver_timer = TimeManager::AddTimer(std::string("Solver \""). append(solver_name). append("\" -- Total")); // Get the solver factory. auto solver_factory = lib->GetSolverFactory(solver_name); auto solver_state = solver_factory->GetDefaultState(); solver_state->SetDeRhamSequence(sequence[0]); solver_state->SetBoundaryLabels(ess_attr); solver_state->SetForms({jform}); // Build the solver. Timer build_timer = TimeManager::AddTimer(std::string("Solver \""). append(solver_name). append("\" -- Build")); if (!myid) { cout << "Building solver \"" << solver_name << "\"...\n"; } unique_ptr solver = solver_factory->BuildSolver(A, *solver_state); build_timer.Stop(); // Run the solver. Timer pre_timer = TimeManager::AddTimer(std::string("Solver \""). append(solver_name). append("\" -- Pre-solve")); if (!myid) { cout << "Solving system with \"" << solver_name << "\"...\n"; } // Note that X is on true dofs owned by the process, while x is on local // dofs that are known to the process. Vector X(A->Width()), x(sequence[0]->GetNumberOfDofs(jform)); X=0.0; { Vector tmp(A->Height()); A->Mult(X, tmp); tmp *= -1.0; tmp += B; double local_norm = tmp.Norml2(); local_norm *= local_norm; double global_norm; MPI_Reduce(&local_norm, &global_norm, 1, GetMPIType(local_norm), MPI_SUM, 0, MPI_COMM_WORLD); if (!myid) { cout << "Initial residual l2 norm: " << sqrt(global_norm) << '\n'; } } pre_timer.Stop(); // Perform the solve. Timer solve_timer = TimeManager::AddTimer(std::string("Solver \""). append(solver_name). append("\" -- Solve")); solver->Mult(B, X); solve_timer.Stop(); Timer post_timer = TimeManager::AddTimer(std::string("Solver \""). append(solver_name). append("\" -- Post-solve")); { Vector tmp(A->Height()); A->Mult(X, tmp); tmp *= -1.0; tmp += B; double local_norm = tmp.Norml2(); local_norm *= local_norm; double global_norm; MPI_Reduce(&local_norm, &global_norm, 1, GetMPIType(local_norm), MPI_SUM, 0, MPI_COMM_WORLD); if (!myid) { cout << "Final residual l2 norm: " << sqrt(global_norm) << '\n'; } } if (!myid) { cout << "Solver \"" << solver_name << "\" finished.\n"; } // Visualize the solution. if (visualize) { hcurlhdiv_dofTrueDof.Distribute(X, x); MultiVector tmp(x.GetData(), 1, x.Size()); sequence[0]->show(jform, tmp); } post_timer.Stop(); } solvers_timer.Stop(); total_timer.Stop(); TimeManager::Print(); if (!myid) { cout << "\nFinished.\n"; } return EXIT_SUCCESS; } // A vector field, used for setting boundary conditions. void bdrfunc(const Vector &p, Vector &F) { F = 0.0; } // The right hand side. void rhsfunc(const Vector &p, Vector &f) { f = 1.0; }