/* *********************************************************************** // // Copyright (C) 2025 -- The 4D-STAR Collaboration // File Author: Emily Boudreaux // Last Modified: March 19, 2025 // // 4DSSE is free software; you can use it and/or modify // it under the terms and restrictions the GNU General Library Public // License version 3 (GPLv3) as published by the Free Software Foundation. // // 4DSSE is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // See the GNU Library General Public License for more details. // // You should have received a copy of the GNU Library General Public License // along with this software; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // // *********************************************************************** */ #include "mfem.hpp" #include #include #include #include #include "polySolver.h" #include "4DSTARTypes.h" #include "config.h" #include "integrators.h" #include "operator.h" #include "polyCoeff.h" #include "probe.h" #include "resourceManager.h" #include "resourceManagerTypes.h" #include "quill/LogMacros.h" namespace laneEmden { double a (int k, double n) { // NOLINT(*-no-recursion) if ( k == 0 ) { return 1; } if ( k == 1 ) { return 0; } else { return -(c(k-2, n)/(std::pow(k, 2)+k)); } } double c(int m, double n) { // NOLINT(*-no-recursion) if ( m == 0 ) { return std::pow(a(0, n), n); } else { double termOne = 1.0/(m*a(0, n)); double acc = 0; for (int k = 1; k <= m; k++) { acc += (k*n-m+k)*a(k, n)*c(m-k, n); } return termOne*acc; } } double thetaSeriesExpansion(double xi, double n, int order) { double acc = 0; for (int k = 0; k < order; k++) { acc += a(k, n) * std::pow(xi, k); } return acc; } } PolySolver::PolySolver(double n, double order) { // --- Check the polytropic index --- if (n > 4.99 || n < 0.0) { LOG_ERROR(m_logger, "The polytropic index n must be less than 5.0 and greater than 0.0. Currently it is {}", m_polytropicIndex); throw std::runtime_error("The polytropic index n must be less than 5.0 and greater than 0.0. Currently it is " + std::to_string(m_polytropicIndex)); } m_polytropicIndex = n; m_feOrder = order; ResourceManager& rm = ResourceManager::getInstance(); const Resource& resource = rm.getResource("mesh:polySphere"); const auto &meshIO = std::get>(resource); meshIO->LinearRescale(polycoeff::x1(m_polytropicIndex)); m_mesh = std::make_unique(meshIO->GetMesh()); // Use feOrder - 1 for the RT space to satisfy Brezzi-Babuska condition // for the H1 and RT spaces m_fecH1 = std::make_unique(m_feOrder, m_mesh->SpaceDimension()); m_fecRT = std::make_unique(m_feOrder - 1, m_mesh->SpaceDimension()); m_feTheta = std::make_unique(m_mesh.get(), m_fecH1.get()); m_fePhi = std::make_unique(m_mesh.get(), m_fecRT.get()); m_theta = std::make_unique(m_feTheta.get()); m_phi = std::make_unique(m_fePhi.get()); assembleBlockSystem(); } PolySolver::~PolySolver() = default; void PolySolver::assembleBlockSystem() { // Start by defining the block structure of the system // Block 0: Theta (scalar space, uses m_feTheta) // Block 1: Phi (vector space, uses m_fePhi) mfem::Array feSpaces; feSpaces.Append(m_feTheta.get()); feSpaces.Append(m_fePhi.get()); // Create the block offsets. These define the start of each block in the combined vector. // Block offsets will be [0, thetaDofs, thetaDofs + phiDofs] mfem::Array blockOffsets; blockOffsets.SetSize(3); blockOffsets[0] = 0; blockOffsets[1] = feSpaces[0]->GetVSize(); blockOffsets[2] = feSpaces[1]->GetVSize(); blockOffsets.PartialSum(); // Add integrators to block form one by one // We add integrators corresponding to each term in the weak form // The block form of the residual matrix // ⎡ 0 -M ⎤ ⎡ θ ⎤ + ⎡f(θ)⎤ = ⎡ 0 ⎤ = R(X) // ⎣ -Q D ⎦ ⎣ Φ ⎦ ⎣ 0 ⎦ ⎣ 0 ⎦ // This then simplifies to // ⎡f(θ) - MΘ ⎤ = ⎡ 0 ⎤ = R // ⎣ -Qɸ Dθ ⎦ ⎣ 0 ⎦ // Here M, Q, and D are // M = ∫∇ψᶿ·Nᵠ dV (MixedVectorWeakDivergenceIntegrator) // D = ∫ψᵠ·Nᵠ dV (VectorFEMassIntegrator) // Q = ∫ψᵠ·∇Nᶿ dV (MixedVectorGradientIntegrator) // f(θ) = ∫ψᶿ·θⁿ dV (NonlinearPowerIntegrator) // Here ψᶿ and ψᵠ are the test functions for the theta and phi spaces, respectively // Nᵠ and Nᶿ are the basis functions for the theta and phi spaces, respectively // A full derivation of the weak form can be found in the 4DSSE documentation // --- Assemble the MixedBilinear and Bilinear forms (M, D, and Q) --- auto Mform = std::make_unique(m_fePhi.get(), m_feTheta.get()); auto Qform = std::make_unique(m_feTheta.get(), m_fePhi.get()); auto Dform = std::make_unique(m_fePhi.get()); // TODO: Check the sign on all of the integrators Mform->AddDomainIntegrator(new mfem::MixedVectorWeakDivergenceIntegrator()); Qform->AddDomainIntegrator(new mfem::MixedVectorGradientIntegrator()); Dform->AddDomainIntegrator(new mfem::VectorFEMassIntegrator()); Mform->Assemble(); Mform->Finalize(); Qform->Assemble(); Qform->Finalize(); Dform->Assemble(); Dform->Finalize(); // --- Assemble the NonlinearForm (f) --- auto fform = std::make_unique(m_feTheta.get()); mfem::ConstantCoefficient oneCoeff(1.0); fform->AddDomainIntegrator(new polyMFEMUtils::NonlinearPowerIntegrator(oneCoeff, m_polytropicIndex)); // TODO: Add essential boundary conditions to the nonlinear form // -- Build the BlockOperator -- m_polytropOperator = std::make_unique( std::move(Mform), std::move(Qform), std::move(Dform), std::move(fform), blockOffsets ); } void PolySolver::solve() const { // --- Set the initial guess for the solution --- setInitialGuess(); setupOperator(); // It's safer to get the offsets directly from the operator after finalization const mfem::Array& block_offsets = m_polytropOperator->GetBlockOffsets(); // Assuming a getter exists or accessing member if public/friend mfem::BlockVector state_vector(block_offsets); state_vector.GetBlock(0) = *m_theta; state_vector.GetBlock(1) = *m_phi; mfem::Vector zero_rhs(block_offsets.Last()); zero_rhs = 0.0; mfem::NewtonSolver newtonSolver = setupNewtonSolver(); // EMB 2025: Calling Mult gets the gradient of the operator for the NewtonSolver // This then is set as the operator for the solver for NewtonSolver // The solver (assuming it is an iterative solver) then sets the // operator for its preconditioner to this. // What this means is that there is no need to manually deal // with updating the preconditioner at every newton step as the // changes to the jacobian are automatically propagated through the // solving chain. This is at least true with MFEM 4.8-rc0 newtonSolver.Mult(zero_rhs, state_vector); // --- Save and view the solution --- saveAndViewSolution(state_vector); } SSE::MFEMArrayPairSet PolySolver::getEssentialTrueDof() const { mfem::Array theta_ess_tdof_list; mfem::Array phi_ess_tdof_list; mfem::Array thetaCenterDofs, phiCenterDofs; mfem::Array thetaCenterVals, phiCenterVals; std::tie(thetaCenterDofs, phi_ess_tdof_list) = findCenterElement(); thetaCenterVals.SetSize(thetaCenterDofs.Size()); phiCenterVals.SetSize(phi_ess_tdof_list.Size()); thetaCenterVals = 1.0; phiCenterVals = 0.0; mfem::Array ess_brd(m_mesh->bdr_attributes.Max()); ess_brd = 1; mfem::Array thetaSurfaceVals; m_feTheta->GetEssentialTrueDofs(ess_brd, theta_ess_tdof_list); thetaSurfaceVals.SetSize(theta_ess_tdof_list.Size()); thetaSurfaceVals = 0.0; // combine the essential dofs with the center dofs theta_ess_tdof_list.Append(thetaCenterDofs); thetaSurfaceVals.Append(thetaCenterVals); SSE::MFEMArrayPair thetaPair = std::make_pair(theta_ess_tdof_list, thetaSurfaceVals); SSE::MFEMArrayPair phiPair = std::make_pair(phi_ess_tdof_list, phiCenterVals); SSE::MFEMArrayPairSet pairSet = std::make_pair(thetaPair, phiPair); return pairSet; } std::pair, mfem::Array> PolySolver::findCenterElement() const { mfem::Array thetaCenterDofs; mfem::Array phiCenterDofs; mfem::DenseMatrix centerPoint(m_mesh->SpaceDimension(), 1); centerPoint(0, 0) = 0.0; centerPoint(1, 0) = 0.0; centerPoint(2, 0) = 0.0; mfem::Array elementIDs; mfem::Array ips; m_mesh->FindPoints(centerPoint, elementIDs, ips); mfem::Array tempDofs; for (int i = 0; i < elementIDs.Size(); i++) { m_feTheta->GetElementDofs(elementIDs[i], tempDofs); thetaCenterDofs.Append(tempDofs); m_fePhi->GetElementDofs(elementIDs[i], tempDofs); phiCenterDofs.Append(tempDofs); } return std::make_pair(thetaCenterDofs, phiCenterDofs); } void PolySolver::setInitialGuess() const { // --- Set the initial guess for the solution --- mfem::FunctionCoefficient thetaInitGuess ( [this](const mfem::Vector &x) { double r = x.Norml2(); double radius = Probe::getMeshRadius(*m_mesh); double u = 1/radius; return -std::pow((u*r), 2)+1.0; } ); m_theta->ProjectCoefficient(thetaInitGuess); mfem::GradientGridFunctionCoefficient phiInitGuess (m_theta.get()); m_phi->ProjectCoefficient(phiInitGuess); if (m_config.get("Poly:Solver:ViewInitialGuess", false)) { Probe::glVisView(*m_theta, *m_mesh, "θ init"); Probe::glVisView(*m_phi, *m_mesh, "ɸ init"); } } void PolySolver::saveAndViewSolution(const mfem::BlockVector& state_vector) const { mfem::BlockVector x_block(const_cast(state_vector), m_polytropOperator->GetBlockOffsets()); mfem::Vector& x_theta = x_block.GetBlock(0); mfem::Vector& x_phi = x_block.GetBlock(1); if (m_config.get("Poly:Output:View", false)) { Probe::glVisView(x_theta, *m_feTheta, "θ Solution"); Probe::glVisView(x_phi, *m_fePhi, "ɸ Solution"); } // --- Extract the Solution --- if (bool write11DSolution = m_config.get("Poly:Output:1D:Save", true)) { auto solutionPath = m_config.get("Poly:Output:1D:Path", "polytropeSolution_1D.csv"); auto derivSolPath = "d" + solutionPath; auto rayCoLatitude = m_config.get("Poly:Output:1D:RayCoLatitude", 0.0); auto rayLongitude = m_config.get("Poly:Output:1D:RayLongitude", 0.0); auto raySamples = m_config.get("Poly:Output:1D:RaySamples", 100); std::vector rayDirection = {rayCoLatitude, rayLongitude}; Probe::getRaySolution(x_theta, *m_feTheta, rayDirection, raySamples, solutionPath); // Probe::getRaySolution(x_phi, *m_fePhi, rayDirection, raySamples, derivSolPath); } } void PolySolver::setupOperator() const { SSE::MFEMArrayPairSet ess_tdof_pair_set = getEssentialTrueDof(); m_polytropOperator->SetEssentialTrueDofs(ess_tdof_pair_set); // -- Finalize the operator -- m_polytropOperator->finalize(); if (!m_polytropOperator->isFinalized()) { LOG_ERROR(m_logger, "PolytropeOperator is not finalized. Cannot solve."); throw std::runtime_error("PolytropeOperator is not finalized. Cannot solve."); } } void PolySolver::LoadSolverUserParams(double &newtonRelTol, double &newtonAbsTol, int &newtonMaxIter, int &newtonPrintLevel, double &gmresRelTol, double &gmresAbsTol, int &gmresMaxIter, int &gmresPrintLevel) const { newtonRelTol = m_config.get("Poly:Solver:Newton:RelTol", 1e-7); newtonAbsTol = m_config.get("Poly:Solver:Newton:AbsTol", 1e-7); newtonMaxIter = m_config.get("Poly:Solver:Newton:MaxIter", 200); newtonPrintLevel = m_config.get("Poly:Solver:Newton:PrintLevel", 1); gmresRelTol = m_config.get("Poly:Solver:GMRES:RelTol", 1e-10); gmresAbsTol = m_config.get("Poly:Solver:GMRES:AbsTol", 1e-12); gmresMaxIter = m_config.get("Poly:Solver:GMRES:MaxIter", 2000); gmresPrintLevel = m_config.get("Poly:Solver:GMRES:PrintLevel", 0); LOG_DEBUG(m_logger, "Newton Solver (relTol: {:0.2E}, absTol: {:0.2E}, maxIter: {}, printLevel: {})", newtonRelTol, newtonAbsTol, newtonMaxIter, newtonPrintLevel); LOG_DEBUG(m_logger, "GMRES Solver (relTol: {:0.2E}, absTol: {:0.2E}, maxIter: {}, printLevel: {})", gmresRelTol, gmresAbsTol, gmresMaxIter, gmresPrintLevel); } mfem::BlockDiagonalPreconditioner PolySolver::build_preconditioner() const { // --- Set up the preconditioners. The non-linear form will use a Chebyshev Preconditioner while the linear terms will use a simpler Jacobi preconditioner --- mfem::BlockDiagonalPreconditioner prec(m_polytropOperator->GetBlockOffsets()); const mfem::BlockOperator &jacobian = m_polytropOperator->GetJacobianOperator(); // Get all the blocks. J00 -> Non-linear form (df(θ)/dθ), J01-> -M, J10 -> -Q, J11 -> D const mfem::Operator& J00 = jacobian.GetBlock(0, 0); mfem::Vector J00diag; J00.AssembleDiagonal(J00diag); SSE::MFEMArrayPairSet ess_tdof_pair_set = m_polytropOperator->GetEssentialTrueDofs(); // TODO: This order may need to be tuned (EMB) // --- ess_tdof_pair_set.first -> (theta dof ids, theta dof vals).first -> theta dof ids // --- ess_tdof_pair_set.second -> (phi dof ids, phi dof vals).first -> phi dof ids mfem::OperatorChebyshevSmoother J00Prec(J00, J00diag, ess_tdof_pair_set.first.first, 2); mfem::OperatorJacobiSmoother J11Prec(m_polytropOperator->GetJ11diag(), ess_tdof_pair_set.second.first); prec.SetDiagonalBlock(0, &J00Prec); prec.SetDiagonalBlock(1, &J11Prec); return prec; } mfem::NewtonSolver PolySolver::setupNewtonSolver() const { // --- Load configuration parameters --- double newtonRelTol, newtonAbsTol, gmresRelTol, gmresAbsTol; int newtonMaxIter, newtonPrintLevel, gmresMaxIter, gmresPrintLevel; LoadSolverUserParams(newtonRelTol, newtonAbsTol, newtonMaxIter, newtonPrintLevel, gmresRelTol, gmresAbsTol, gmresMaxIter, gmresPrintLevel); // --- Set up the Newton solver --- mfem::NewtonSolver newtonSolver; newtonSolver.SetRelTol(newtonRelTol); newtonSolver.SetAbsTol(newtonAbsTol); newtonSolver.SetMaxIter(newtonMaxIter); newtonSolver.SetPrintLevel(newtonPrintLevel); newtonSolver.SetOperator(*m_polytropOperator); // --- Created the linear solver which is used to invert the jacobian --- mfem::GMRESSolver gmresSolver; gmresSolver.SetRelTol(gmresRelTol); gmresSolver.SetAbsTol(gmresAbsTol); gmresSolver.SetMaxIter(gmresMaxIter); gmresSolver.SetPrintLevel(gmresPrintLevel); // build_preconditioner(); newtonSolver.SetSolver(gmresSolver); return newtonSolver; }