#include #include #include #include #include #include #include #include #include #include #include import mean_field; import test_helpers; namespace spherical_harmonic_preconditioner_test { using namespace mean_field; [[nodiscard]] operators::StellarEquilibriumDependencies dependencies() { return { .discretization = {.identity = 9201, .revision = 1}, .density = {.identity = 9203, .revision = 1}, .surfaceDeformation = {.identity = 9209, .revision = 1}, .gravityGradient = {.identity = 9221, .revision = 1}, .gravityPotential = {.identity = 9227, .revision = 1}, .enthalpy = {.identity = 9239, .revision = 1}, .bernoulliConstant = {.identity = 9241, .revision = 1}, .rotation = {.identity = 9257, .revision = 1}, .targetMass = {.identity = 9277, .revision = 1} }; } void checkClose( const mfem::Vector &actual, const mfem::Vector &expected ) { REQUIRE(actual.Size() == expected.Size()); mfem::Vector difference(actual); difference -= expected; const double error = difference.Norml2(); const double scale = std::max(actual.Norml2(), expected.Norml2()); REQUIRE(std::isfinite(error)); REQUIRE(std::isfinite(scale)); INFO("inverse error = " << error << ", scale = " << scale); CHECK(error <= 2.e-12 + 3.e-11 * scale); } // Construct the advertised triangular surrogate's RHS from a known // correction. This checks the inverse's parameter ordering and material // coupling, without asking an approximate inverse to invert the full J. template void checkTriangularInverse( const Prepared &inverse, const int modes ) { const auto &couplings = inverse.GetCoupledOperator(); const auto &offsets = couplings.GetOffsets(); REQUIRE(offsets[2] - offsets[1] == modes); REQUIRE(inverse.Width() == offsets[3]); REQUIRE(inverse.Height() == inverse.Width()); REQUIRE(inverse.GetSurfaceDiagonal().Size() == modes); mfem::Vector expected(inverse.Height()), rhs(inverse.Width()), actual(inverse.Height()); for (int i = 0; i < expected.Size(); ++i) expected(i) = 0.02 * std::sin(0.31 * (i + 1)); const mfem::Vector density(expected.GetData(), offsets[1]); const mfem::Vector shape(expected.GetData() + offsets[1], modes); const mfem::Vector enthalpy(expected.GetData() + offsets[2], offsets[3] - offsets[2]); mfem::Vector densityCoupling(density.Size()), enthalpyCoupling(enthalpy.Size()), cross(density.Size()); couplings.ApplySurfaceToMaterial(shape, densityCoupling, enthalpyCoupling); couplings.ApplyEnthalpyToDensity(enthalpy, cross); for (int i = 0; i < density.Size(); ++i) rhs(i) = inverse.GetDensityDiagonal()(i) * density(i) + densityCoupling(i) + cross(i); for (int i = 0; i < modes; ++i) rhs(offsets[1] + i) = inverse.GetSurfaceDiagonal()(i) * shape(i); for (int i = 0; i < enthalpy.Size(); ++i) rhs(offsets[2] + i) = inverse.GetEnthalpyDiagonal()(i) * enthalpy(i) + enthalpyCoupling(i); actual = 0.0; inverse.Mult(rhs, actual); checkClose(actual, expected); const mfem::Vector actualShape(actual.GetData() + offsets[1], modes); checkClose(actualShape, shape); rhs = 0.0; actual = 1.0; inverse.Mult(rhs, actual); CHECK(actual.Norml2() == 0.0); CHECK(inverse.IsCurrent()); } } // namespace spherical_harmonic_preconditioner_test TEST_CASE( "Harmonic Surface Mass And Calibrated Inverses Use Native Parameters And Reject Nodal H1", "[preconditioning][material_surface][spherical_harmonic][integration][serial]" ) { using namespace mean_field; using namespace spherical_harmonic_preconditioner_test; namespace backend = preconditioning::backend; int ranks = 0; MPI_Comm_size(MPI_COMM_WORLD, &ranks); if (ranks != 1) SKIP("The current radial domain extension supports one MPI rank."); const auto args = test_utils::setup_args(); auto finiteElements = fem::setup_fem(args.mesh_file, args, 0); REQUIRE(finiteElements.okay()); const auto stellarModel = model::StellarModel( eos::Polytrope({.n = 1.5, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}) ); // A restricted azimuthal set also rules out using (L+1)^2 as the block // length when the actual mode catalog has fewer independent coefficients. const deformation::SphericalHarmonicSurface prescription{{.maximumDegree = 2, .maximumAzimuthalOrder = 1}}; constexpr int modes = 7; auto problem = equilibrium::discretize( stellarModel, equilibrium::makeStellarDiscretization(std::move(finiteElements), normalization::Unnormalized{}, prescription) ); mfem::Vector state(problem.StateSize()); state = 0.0; const auto view = problem.GetManifest().stateView(state); view.block(utils::blocks::density_field.mass_term) = 1.0; view.block(utils::blocks::enthalpy_field.specific_term) = 0.7; view.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term) = 0.8; auto shape = view.block(utils::blocks::surface_deformation_field.parameters_term); REQUIRE(shape.Size() == modes); for (int i = 0; i < modes; ++i) shape(i) = 0.001 * std::sin(0.7 * (i + 1)); mfem::Vector omega(3), center(3); omega = 0.0; center = 0.0; const auto preparation = problem.TryPrepare(state, dependencies(), physics::RigidRotation{omega, center}); REQUIRE(preparation.has_value()); const auto &physical = problem.GetPhysicalOperator(); REQUIRE(physical.IsPrepared()); REQUIRE_FALSE(physical.GetDomainDeformation().hasNodalRadialParameters()); const auto block = preconditioning::materialSurfaceBlock(problem); STATIC_CHECK( std::is_same_v< typename std::remove_cvref_t::SurfaceSurrogate, preconditioning::SurfaceMassDiagonal> ); auto prepared = preconditioning::prepare(problem, block); CHECK(prepared.GetStatistics().surfaceRieszAssemblies == 1); CHECK(prepared.GetStatistics().surfaceJacobianProbes == 0); CHECK(prepared.GetSurfaceDiagonalQuality().minimumAbsoluteEntryBeforeRegularization > 0.0); checkTriangularInverse(prepared, modes); const auto unchanged = prepared.Refresh(physical); CHECK_FALSE(unchanged.DidAnyWork()); const auto calibratedBlock = preconditioning::materialSurfaceBlock( problem, backend::Diagonal{}, backend::Diagonal{}, preconditioning::SurfaceThenMaterialTriangular{}, {.surfaceCalibration = { .target = preconditioning::SurfaceRieszCalibrationTarget::surface_jacobian, .probeCount = 3, .objective = preconditioning::SurfaceRieszCalibrationObjective::operator_action }} ); auto calibrated = preconditioning::prepare(problem, calibratedBlock); const auto &calibration = calibrated.GetSurfaceCalibration(); REQUIRE(calibration.WasCalibrated()); REQUIRE(std::isfinite(calibration.scale)); REQUIRE(calibration.scale != 0.0); CHECK(calibration.probeCount == 3); CHECK(calibrated.GetStatistics().surfaceJacobianProbes == 3); for (int i = 0; i < modes; ++i) CHECK( calibrated.GetSurfaceDiagonal()(i) == Catch::Approx(calibration.scale * prepared.GetSurfaceDiagonal()(i)).epsilon(2.e-13) ); checkTriangularInverse(calibrated, modes); using FixedAMG = backend::HypreBoomerAMG; const auto h1 = preconditioning::materialSurfaceBlock( problem, backend::Diagonal{}, FixedAMG{backend::FixedCycles{.cycles = 1}}, preconditioning::ApproximateMaterialSurfaceLDU{}, preconditioning::SurfaceH1MassStiffness{} ); CHECK_THROWS_WITH( preconditioning::prepare(problem, h1), Catch::Matchers::ContainsSubstring("requires nodal radial parameters") ); CHECK(prepared.IsCurrent()); CHECK(calibrated.IsCurrent()); }